parent
00bcd8c992
commit
5543a0755a
|
@ -351,20 +351,58 @@ const resolvers = {
|
||||||
facts: history
|
facts: history
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
walletLogs: async (parent, args, { me, models }) => {
|
walletLogs: async (parent, { type, from, to, cursor }, { me, models }) => {
|
||||||
if (!me) {
|
if (!me) {
|
||||||
throw new GqlAuthenticationError()
|
throw new GqlAuthenticationError()
|
||||||
}
|
}
|
||||||
|
|
||||||
return await models.walletLog.findMany({
|
// we cursoring with the wallet logs on the client
|
||||||
where: {
|
// if we have from, don't use cursor
|
||||||
userId: me.id
|
// regardless, store the state of the cursor for the next call
|
||||||
},
|
|
||||||
orderBy: [
|
const decodedCursor = cursor ? decodeCursor(cursor) : { offset: 0, time: to ?? new Date() }
|
||||||
{ createdAt: 'desc' },
|
|
||||||
{ id: 'desc' }
|
let logs = []
|
||||||
]
|
let nextCursor
|
||||||
})
|
if (from) {
|
||||||
|
logs = await models.walletLog.findMany({
|
||||||
|
where: {
|
||||||
|
userId: me.id,
|
||||||
|
wallet: type ?? undefined,
|
||||||
|
createdAt: {
|
||||||
|
gte: from ? new Date(Number(from)) : undefined,
|
||||||
|
lte: to ? new Date(Number(to)) : undefined
|
||||||
|
}
|
||||||
|
},
|
||||||
|
orderBy: [
|
||||||
|
{ createdAt: 'desc' },
|
||||||
|
{ id: 'desc' }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
nextCursor = nextCursorEncoded(decodedCursor, logs.length)
|
||||||
|
} else {
|
||||||
|
logs = await models.walletLog.findMany({
|
||||||
|
where: {
|
||||||
|
userId: me.id,
|
||||||
|
wallet: type ?? undefined,
|
||||||
|
createdAt: {
|
||||||
|
lte: decodedCursor.time
|
||||||
|
}
|
||||||
|
},
|
||||||
|
orderBy: [
|
||||||
|
{ createdAt: 'desc' },
|
||||||
|
{ id: 'desc' }
|
||||||
|
],
|
||||||
|
take: LIMIT,
|
||||||
|
skip: decodedCursor.offset
|
||||||
|
})
|
||||||
|
nextCursor = logs.length === LIMIT ? nextCursorEncoded(decodedCursor, logs.length) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
cursor: nextCursor,
|
||||||
|
entries: logs
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Wallet: {
|
Wallet: {
|
||||||
|
|
|
@ -66,7 +66,7 @@ const typeDefs = `
|
||||||
wallets: [Wallet!]!
|
wallets: [Wallet!]!
|
||||||
wallet(id: ID!): Wallet
|
wallet(id: ID!): Wallet
|
||||||
walletByType(type: String!): Wallet
|
walletByType(type: String!): Wallet
|
||||||
walletLogs: [WalletLog]!
|
walletLogs(type: String, from: String, to: String, cursor: String): WalletLog!
|
||||||
}
|
}
|
||||||
|
|
||||||
extend type Mutation {
|
extend type Mutation {
|
||||||
|
@ -154,6 +154,11 @@ const typeDefs = `
|
||||||
}
|
}
|
||||||
|
|
||||||
type WalletLog {
|
type WalletLog {
|
||||||
|
entries: [WalletLogEntry!]!
|
||||||
|
cursor: String
|
||||||
|
}
|
||||||
|
|
||||||
|
type WalletLogEntry {
|
||||||
id: ID!
|
id: ID!
|
||||||
createdAt: Date!
|
createdAt: Date!
|
||||||
wallet: ID!
|
wallet: ID!
|
||||||
|
|
|
@ -0,0 +1,273 @@
|
||||||
|
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||||
|
|
||||||
|
function useIndexedDB (dbName, storeName, version = 1, indices = []) {
|
||||||
|
const [db, setDb] = useState(null)
|
||||||
|
const [error, setError] = useState(null)
|
||||||
|
const operationQueue = useRef([])
|
||||||
|
|
||||||
|
const handleError = useCallback((error) => {
|
||||||
|
console.error('IndexedDB error:', error)
|
||||||
|
setError(error)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const processQueue = useCallback((db) => {
|
||||||
|
if (!db) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
// try to run a noop to see if the db is ready
|
||||||
|
db.transaction(storeName)
|
||||||
|
while (operationQueue.current.length > 0) {
|
||||||
|
const operation = operationQueue.current.shift()
|
||||||
|
operation(db)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error)
|
||||||
|
}
|
||||||
|
}, [storeName, handleError])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let isMounted = true
|
||||||
|
const request = window.indexedDB.open(dbName, version)
|
||||||
|
|
||||||
|
request.onerror = (event) => {
|
||||||
|
handleError(new Error('Error opening database'))
|
||||||
|
}
|
||||||
|
|
||||||
|
request.onsuccess = (event) => {
|
||||||
|
if (isMounted) {
|
||||||
|
const database = event.target.result
|
||||||
|
database.onversionchange = () => {
|
||||||
|
database.close()
|
||||||
|
setDb(null)
|
||||||
|
handleError(new Error('Database is outdated, please reload the page'))
|
||||||
|
}
|
||||||
|
setDb(database)
|
||||||
|
processQueue(database)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
request.onupgradeneeded = (event) => {
|
||||||
|
const database = event.target.result
|
||||||
|
try {
|
||||||
|
const store = database.createObjectStore(storeName, { keyPath: 'id', autoIncrement: true })
|
||||||
|
|
||||||
|
indices.forEach(index => {
|
||||||
|
store.createIndex(index.name, index.keyPath, index.options)
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
handleError(new Error('Error upgrading database: ' + error.message))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isMounted = false
|
||||||
|
if (db) {
|
||||||
|
db.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [dbName, storeName, version, indices, handleError, processQueue])
|
||||||
|
|
||||||
|
const queueOperation = useCallback((operation) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const wrappedOperation = (db) => {
|
||||||
|
try {
|
||||||
|
const result = operation(db)
|
||||||
|
resolve(result)
|
||||||
|
} catch (error) {
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
operationQueue.current.push(wrappedOperation)
|
||||||
|
processQueue(db)
|
||||||
|
})
|
||||||
|
}, [processQueue, db])
|
||||||
|
|
||||||
|
const add = useCallback((value) => {
|
||||||
|
return queueOperation((db) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const transaction = db.transaction(storeName, 'readwrite')
|
||||||
|
const store = transaction.objectStore(storeName)
|
||||||
|
const request = store.add(value)
|
||||||
|
|
||||||
|
request.onerror = () => reject(new Error('Error adding data'))
|
||||||
|
request.onsuccess = () => resolve(request.result)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}, [queueOperation, storeName])
|
||||||
|
|
||||||
|
const get = useCallback((key) => {
|
||||||
|
return queueOperation((db) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const transaction = db.transaction(storeName, 'readonly')
|
||||||
|
const store = transaction.objectStore(storeName)
|
||||||
|
const request = store.get(key)
|
||||||
|
|
||||||
|
request.onerror = () => reject(new Error('Error getting data'))
|
||||||
|
request.onsuccess = () => resolve(request.result ? request.result : undefined)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}, [queueOperation, storeName])
|
||||||
|
|
||||||
|
const getAll = useCallback(() => {
|
||||||
|
return queueOperation((db) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const transaction = db.transaction(storeName, 'readonly')
|
||||||
|
const store = transaction.objectStore(storeName)
|
||||||
|
const request = store.getAll()
|
||||||
|
|
||||||
|
request.onerror = () => reject(new Error('Error getting all data'))
|
||||||
|
request.onsuccess = () => resolve(request.result)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}, [queueOperation, storeName])
|
||||||
|
|
||||||
|
const update = useCallback((key, value) => {
|
||||||
|
return queueOperation((db) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const transaction = db.transaction(storeName, 'readwrite')
|
||||||
|
const store = transaction.objectStore(storeName)
|
||||||
|
const request = store.get(key)
|
||||||
|
|
||||||
|
request.onerror = () => reject(new Error('Error updating data'))
|
||||||
|
request.onsuccess = () => {
|
||||||
|
const updatedValue = { ...request.result, ...value }
|
||||||
|
const updateRequest = store.put(updatedValue)
|
||||||
|
updateRequest.onerror = () => reject(new Error('Error updating data'))
|
||||||
|
updateRequest.onsuccess = () => resolve(updateRequest.result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}, [queueOperation, storeName])
|
||||||
|
|
||||||
|
const remove = useCallback((key) => {
|
||||||
|
return queueOperation((db) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const transaction = db.transaction(storeName, 'readwrite')
|
||||||
|
const store = transaction.objectStore(storeName)
|
||||||
|
const request = store.delete(key)
|
||||||
|
|
||||||
|
request.onerror = () => reject(new Error('Error removing data'))
|
||||||
|
request.onsuccess = () => resolve()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}, [queueOperation, storeName])
|
||||||
|
|
||||||
|
const clear = useCallback((indexName = null, query = null) => {
|
||||||
|
return queueOperation((db) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const transaction = db.transaction(storeName, 'readwrite')
|
||||||
|
const store = transaction.objectStore(storeName)
|
||||||
|
|
||||||
|
if (!query) {
|
||||||
|
// Clear all data if no query is provided
|
||||||
|
const request = store.clear()
|
||||||
|
request.onerror = () => reject(new Error('Error clearing all data'))
|
||||||
|
request.onsuccess = () => resolve()
|
||||||
|
} else {
|
||||||
|
// Clear data based on the query
|
||||||
|
const index = indexName ? store.index(indexName) : store
|
||||||
|
const request = index.openCursor(query)
|
||||||
|
let deletedCount = 0
|
||||||
|
|
||||||
|
request.onerror = () => reject(new Error('Error clearing data based on query'))
|
||||||
|
request.onsuccess = (event) => {
|
||||||
|
const cursor = event.target.result
|
||||||
|
if (cursor) {
|
||||||
|
const deleteRequest = cursor.delete()
|
||||||
|
deleteRequest.onerror = () => reject(new Error('Error deleting item'))
|
||||||
|
deleteRequest.onsuccess = () => {
|
||||||
|
deletedCount++
|
||||||
|
cursor.continue()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resolve(deletedCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}, [queueOperation, storeName])
|
||||||
|
|
||||||
|
const getByIndex = useCallback((indexName, key) => {
|
||||||
|
return queueOperation((db) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const transaction = db.transaction(storeName, 'readonly')
|
||||||
|
const store = transaction.objectStore(storeName)
|
||||||
|
const index = store.index(indexName)
|
||||||
|
const request = index.get(key)
|
||||||
|
|
||||||
|
request.onerror = () => reject(new Error('Error getting data by index'))
|
||||||
|
request.onsuccess = () => resolve(request.result)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}, [queueOperation, storeName])
|
||||||
|
|
||||||
|
const getAllByIndex = useCallback((indexName, query, direction = 'next', limit = Infinity) => {
|
||||||
|
return queueOperation((db) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const transaction = db.transaction(storeName, 'readonly')
|
||||||
|
const store = transaction.objectStore(storeName)
|
||||||
|
const index = store.index(indexName)
|
||||||
|
const request = index.openCursor(query, direction)
|
||||||
|
const results = []
|
||||||
|
|
||||||
|
request.onerror = () => reject(new Error('Error getting data by index'))
|
||||||
|
request.onsuccess = (event) => {
|
||||||
|
const cursor = event.target.result
|
||||||
|
if (cursor && results.length < limit) {
|
||||||
|
results.push(cursor.value)
|
||||||
|
cursor.continue()
|
||||||
|
} else {
|
||||||
|
resolve(results)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}, [queueOperation, storeName])
|
||||||
|
|
||||||
|
const getPage = useCallback((page = 1, pageSize = 10, indexName = null, query = null, direction = 'next') => {
|
||||||
|
return queueOperation((db) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const transaction = db.transaction(storeName, 'readonly')
|
||||||
|
const store = transaction.objectStore(storeName)
|
||||||
|
const target = indexName ? store.index(indexName) : store
|
||||||
|
const request = target.openCursor(query, direction)
|
||||||
|
const results = []
|
||||||
|
let skipped = 0
|
||||||
|
let hasMore = false
|
||||||
|
|
||||||
|
request.onerror = () => reject(new Error('Error getting page'))
|
||||||
|
request.onsuccess = (event) => {
|
||||||
|
const cursor = event.target.result
|
||||||
|
if (cursor) {
|
||||||
|
if (skipped < (page - 1) * pageSize) {
|
||||||
|
skipped++
|
||||||
|
cursor.continue()
|
||||||
|
} else if (results.length < pageSize) {
|
||||||
|
results.push(cursor.value)
|
||||||
|
cursor.continue()
|
||||||
|
} else {
|
||||||
|
hasMore = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hasMore || !cursor) {
|
||||||
|
const countRequest = target.count()
|
||||||
|
countRequest.onsuccess = () => {
|
||||||
|
resolve({
|
||||||
|
data: results,
|
||||||
|
total: countRequest.result,
|
||||||
|
hasMore
|
||||||
|
})
|
||||||
|
}
|
||||||
|
countRequest.onerror = () => reject(new Error('Error counting items'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}, [queueOperation, storeName])
|
||||||
|
|
||||||
|
return { add, get, getAll, update, remove, clear, getByIndex, getAllByIndex, getPage, error }
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useIndexedDB
|
|
@ -1,18 +1,22 @@
|
||||||
import LogMessage from './log-message'
|
import LogMessage from './log-message'
|
||||||
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
import styles from '@/styles/log.module.css'
|
import styles from '@/styles/log.module.css'
|
||||||
import { Button } from 'react-bootstrap'
|
import { Button } from 'react-bootstrap'
|
||||||
import { useToast } from './toast'
|
import { useToast } from './toast'
|
||||||
import { useShowModal } from './modal'
|
import { useShowModal } from './modal'
|
||||||
import { WALLET_LOGS } from '@/fragments/wallet'
|
import { WALLET_LOGS } from '@/fragments/wallet'
|
||||||
import { getWalletByType } from 'wallets'
|
import { getWalletByType } from 'wallets'
|
||||||
import { gql, useMutation, useQuery } from '@apollo/client'
|
import { gql, useLazyQuery, useMutation } from '@apollo/client'
|
||||||
import { useMe } from './me'
|
import { useMe } from './me'
|
||||||
|
import useIndexedDB from './use-indexeddb'
|
||||||
|
import { SSR } from '@/lib/constants'
|
||||||
|
|
||||||
export function WalletLogs ({ wallet, embedded }) {
|
export function WalletLogs ({ wallet, embedded }) {
|
||||||
const logs = useWalletLogs(wallet)
|
const { logs, setLogs, hasMore, loadMore, loadLogs, loading } = useWalletLogs(wallet)
|
||||||
|
useEffect(() => {
|
||||||
|
loadLogs()
|
||||||
|
}, [wallet])
|
||||||
|
|
||||||
const tableRef = useRef()
|
|
||||||
const showModal = useShowModal()
|
const showModal = useShowModal()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -21,13 +25,12 @@ export function WalletLogs ({ wallet, embedded }) {
|
||||||
<span
|
<span
|
||||||
style={{ cursor: 'pointer' }}
|
style={{ cursor: 'pointer' }}
|
||||||
className='text-muted fw-bold nav-link ms-auto' onClick={() => {
|
className='text-muted fw-bold nav-link ms-auto' onClick={() => {
|
||||||
showModal(onClose => <DeleteWalletLogsObstacle wallet={wallet} onClose={onClose} />)
|
showModal(onClose => <DeleteWalletLogsObstacle wallet={wallet} setLogs={setLogs} onClose={onClose} />)
|
||||||
}}
|
}}
|
||||||
>clear logs
|
>clear logs
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div ref={tableRef} className={`${styles.logTable} ${embedded ? styles.embedded : ''}`}>
|
<div className={`${styles.logTable} ${embedded ? styles.embedded : ''}`}>
|
||||||
{logs.length === 0 && <div className='w-100 text-center'>empty</div>}
|
|
||||||
<table>
|
<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
{logs.map((log, i) => (
|
{logs.map((log, i) => (
|
||||||
|
@ -39,15 +42,20 @@ export function WalletLogs ({ wallet, embedded }) {
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div className='w-100 text-center'>------ start of logs ------</div>
|
{loading
|
||||||
|
? <div className='w-100 text-center'>loading...</div>
|
||||||
|
: logs.length === 0 && <div className='w-100 text-center'>empty</div>}
|
||||||
|
{hasMore
|
||||||
|
? <Button onClick={loadMore} size='sm' className='mt-3'>Load More</Button>
|
||||||
|
: <div className='w-100 text-center'>------ start of logs ------</div>}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function DeleteWalletLogsObstacle ({ wallet, onClose }) {
|
function DeleteWalletLogsObstacle ({ wallet, setLogs, onClose }) {
|
||||||
|
const { deleteLogs } = useWalletLogger(wallet, setLogs)
|
||||||
const toaster = useToast()
|
const toaster = useToast()
|
||||||
const { deleteLogs } = useWalletLogger(wallet)
|
|
||||||
|
|
||||||
const prompt = `Do you really want to delete all ${wallet ? '' : 'wallet'} logs ${wallet ? 'of this wallet' : ''}?`
|
const prompt = `Do you really want to delete all ${wallet ? '' : 'wallet'} logs ${wallet ? 'of this wallet' : ''}?`
|
||||||
return (
|
return (
|
||||||
|
@ -60,7 +68,7 @@ function DeleteWalletLogsObstacle ({ wallet, onClose }) {
|
||||||
onClick={
|
onClick={
|
||||||
async () => {
|
async () => {
|
||||||
try {
|
try {
|
||||||
await deleteLogs()
|
await deleteLogs(wallet)
|
||||||
onClose()
|
onClose()
|
||||||
toaster.success('deleted wallet logs')
|
toaster.success('deleted wallet logs')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -76,72 +84,31 @@ function DeleteWalletLogsObstacle ({ wallet, onClose }) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const WalletLoggerContext = createContext()
|
const INDICES = [
|
||||||
const WalletLogsContext = createContext()
|
{ name: 'ts', keyPath: 'ts' },
|
||||||
|
{ name: 'wallet_ts', keyPath: ['wallet', 'ts'] }
|
||||||
|
]
|
||||||
|
|
||||||
const initIndexedDB = async (dbName, storeName) => {
|
function useWalletLogDB () {
|
||||||
return new Promise((resolve, reject) => {
|
const { me } = useMe()
|
||||||
if (!window.indexedDB) {
|
const dbName = `app:storage${me ? `:${me.id}` : ''}`
|
||||||
return reject(new Error('IndexedDB not supported'))
|
const idbStoreName = 'wallet_logs'
|
||||||
}
|
const { add, getPage, clear, error: idbError } = useIndexedDB(dbName, idbStoreName, 1, INDICES)
|
||||||
|
return { add, getPage, clear, error: idbError }
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB
|
|
||||||
const request = window.indexedDB.open(dbName, 1)
|
|
||||||
|
|
||||||
let db
|
|
||||||
request.onupgradeneeded = () => {
|
|
||||||
// this only runs if version was changed during open
|
|
||||||
db = request.result
|
|
||||||
if (!db.objectStoreNames.contains(storeName)) {
|
|
||||||
const objectStore = db.createObjectStore(storeName, { autoIncrement: true })
|
|
||||||
objectStore.createIndex('ts', 'ts')
|
|
||||||
objectStore.createIndex('wallet_ts', ['wallet', 'ts'])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
request.onsuccess = () => {
|
|
||||||
// this gets called after onupgradeneeded finished
|
|
||||||
db = request.result
|
|
||||||
resolve(db)
|
|
||||||
}
|
|
||||||
|
|
||||||
request.onerror = () => {
|
|
||||||
reject(new Error('failed to open IndexedDB'))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WalletLoggerProvider = ({ children }) => {
|
export function useWalletLogger (wallet, setLogs) {
|
||||||
const { me } = useMe()
|
const { add, clear } = useWalletLogDB()
|
||||||
const [logs, setLogs] = useState([])
|
|
||||||
let dbName = 'app:storage'
|
|
||||||
if (me) {
|
|
||||||
dbName = `${dbName}:${me.id}`
|
|
||||||
}
|
|
||||||
const idbStoreName = 'wallet_logs'
|
|
||||||
const idb = useRef()
|
|
||||||
const logQueue = useRef([])
|
|
||||||
|
|
||||||
useQuery(WALLET_LOGS, {
|
const appendLog = useCallback(async (wallet, level, message) => {
|
||||||
fetchPolicy: 'network-only',
|
const log = { wallet: tag(wallet), level, message, ts: +new Date() }
|
||||||
// required to trigger onCompleted on refetches
|
try {
|
||||||
notifyOnNetworkStatusChange: true,
|
await add(log)
|
||||||
onCompleted: ({ walletLogs }) => {
|
setLogs?.(prevLogs => [log, ...prevLogs])
|
||||||
setLogs((prevLogs) => {
|
} catch (error) {
|
||||||
const existingIds = prevLogs.map(({ id }) => id)
|
console.error('Failed to append log:', error)
|
||||||
const logs = walletLogs
|
|
||||||
.filter(({ id }) => !existingIds.includes(id))
|
|
||||||
.map(({ createdAt, wallet: walletType, ...log }) => {
|
|
||||||
return {
|
|
||||||
ts: +new Date(createdAt),
|
|
||||||
wallet: tag(getWalletByType(walletType)),
|
|
||||||
...log
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return [...prevLogs, ...logs].sort((a, b) => b.ts - a.ts)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
}, [add])
|
||||||
|
|
||||||
const [deleteServerWalletLogs] = useMutation(
|
const [deleteServerWalletLogs] = useMutation(
|
||||||
gql`
|
gql`
|
||||||
|
@ -151,105 +118,25 @@ export const WalletLoggerProvider = ({ children }) => {
|
||||||
`,
|
`,
|
||||||
{
|
{
|
||||||
onCompleted: (_, { variables: { wallet: walletType } }) => {
|
onCompleted: (_, { variables: { wallet: walletType } }) => {
|
||||||
setLogs((logs) => {
|
setLogs?.(logs => logs.filter(l => walletType ? l.wallet !== getWalletByType(walletType).name : false))
|
||||||
return logs.filter(l => walletType ? l.wallet !== getWalletByType(walletType).name : false)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const saveLog = useCallback((log) => {
|
|
||||||
if (!idb.current) {
|
|
||||||
// IDB may not be ready yet
|
|
||||||
return logQueue.current.push(log)
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const tx = idb.current.transaction(idbStoreName, 'readwrite')
|
|
||||||
const request = tx.objectStore(idbStoreName).add(log)
|
|
||||||
request.onerror = () => console.error('failed to save log:', log)
|
|
||||||
} catch (e) {
|
|
||||||
console.error('failed to save log:', log, e)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
initIndexedDB(dbName, idbStoreName)
|
|
||||||
.then(db => {
|
|
||||||
idb.current = db
|
|
||||||
|
|
||||||
// load all logs from IDB
|
|
||||||
const tx = idb.current.transaction(idbStoreName, 'readonly')
|
|
||||||
const store = tx.objectStore(idbStoreName)
|
|
||||||
const index = store.index('ts')
|
|
||||||
const request = index.getAll()
|
|
||||||
request.onsuccess = () => {
|
|
||||||
let logs = request.result
|
|
||||||
setLogs((prevLogs) => {
|
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
|
||||||
// in dev mode, useEffect runs twice, so we filter out duplicates here
|
|
||||||
const existingIds = prevLogs.map(({ id }) => id)
|
|
||||||
logs = logs.filter(({ id }) => !existingIds.includes(id))
|
|
||||||
}
|
|
||||||
// sort oldest first to keep same order as logs are appended
|
|
||||||
return [...prevLogs, ...logs].sort((a, b) => b.ts - a.ts)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// flush queued logs to IDB
|
|
||||||
logQueue.current.forEach(q => {
|
|
||||||
const isLog = !!q.wallet
|
|
||||||
if (isLog) saveLog(q)
|
|
||||||
})
|
|
||||||
|
|
||||||
logQueue.current = []
|
|
||||||
})
|
|
||||||
.catch(console.error)
|
|
||||||
return () => idb.current?.close()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const appendLog = useCallback((wallet, level, message) => {
|
|
||||||
const log = { wallet: tag(wallet), level, message, ts: +new Date() }
|
|
||||||
saveLog(log)
|
|
||||||
setLogs((prevLogs) => [log, ...prevLogs])
|
|
||||||
}, [saveLog])
|
|
||||||
|
|
||||||
const deleteLogs = useCallback(async (wallet, options) => {
|
const deleteLogs = useCallback(async (wallet, options) => {
|
||||||
if ((!wallet || wallet.walletType) && !options?.clientOnly) {
|
if ((!wallet || wallet.walletType) && !options?.clientOnly) {
|
||||||
await deleteServerWalletLogs({ variables: { wallet: wallet?.walletType } })
|
await deleteServerWalletLogs({ variables: { wallet: wallet?.walletType } })
|
||||||
}
|
}
|
||||||
if (!wallet || wallet.sendPayment) {
|
if (!wallet || wallet.sendPayment) {
|
||||||
try {
|
try {
|
||||||
const tx = idb.current.transaction(idbStoreName, 'readwrite')
|
const walletTag = wallet ? tag(wallet) : null
|
||||||
const objectStore = tx.objectStore(idbStoreName)
|
await clear('wallet_ts', walletTag ? window.IDBKeyRange.bound([walletTag, 0], [walletTag, Infinity]) : null)
|
||||||
const idx = objectStore.index('wallet_ts')
|
setLogs?.(logs => logs.filter(l => wallet ? l.wallet !== tag(wallet) : false))
|
||||||
const request = wallet ? idx.openCursor(window.IDBKeyRange.bound([tag(wallet), -Infinity], [tag(wallet), Infinity])) : idx.openCursor()
|
|
||||||
request.onsuccess = function (event) {
|
|
||||||
const cursor = event.target.result
|
|
||||||
if (cursor) {
|
|
||||||
cursor.delete()
|
|
||||||
cursor.continue()
|
|
||||||
} else {
|
|
||||||
// finished
|
|
||||||
setLogs((logs) => logs.filter(l => wallet ? l.wallet !== tag(wallet) : false))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('failed to delete logs', e)
|
console.error('failed to delete logs', e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [me, setLogs])
|
}, [clear, deleteServerWalletLogs, setLogs])
|
||||||
|
|
||||||
return (
|
|
||||||
<WalletLogsContext.Provider value={logs}>
|
|
||||||
<WalletLoggerContext.Provider value={{ appendLog, deleteLogs }}>
|
|
||||||
{children}
|
|
||||||
</WalletLoggerContext.Provider>
|
|
||||||
</WalletLogsContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useWalletLogger (wallet) {
|
|
||||||
const { appendLog, deleteLogs: innerDeleteLogs } = useContext(WalletLoggerContext)
|
|
||||||
|
|
||||||
const log = useCallback(level => message => {
|
const log = useCallback(level => message => {
|
||||||
if (!wallet) {
|
if (!wallet) {
|
||||||
|
@ -257,9 +144,6 @@ export function useWalletLogger (wallet) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO:
|
|
||||||
// also send this to us if diagnostics was enabled,
|
|
||||||
// very similar to how the service worker logger works.
|
|
||||||
appendLog(wallet, level, message)
|
appendLog(wallet, level, message)
|
||||||
console[level !== 'error' ? 'info' : 'error'](`[${tag(wallet)}]`, message)
|
console[level !== 'error' ? 'info' : 'error'](`[${tag(wallet)}]`, message)
|
||||||
}, [appendLog, wallet])
|
}, [appendLog, wallet])
|
||||||
|
@ -270,10 +154,6 @@ export function useWalletLogger (wallet) {
|
||||||
error: (...message) => log('error')(message.join(' '))
|
error: (...message) => log('error')(message.join(' '))
|
||||||
}), [log, wallet?.name])
|
}), [log, wallet?.name])
|
||||||
|
|
||||||
const deleteLogs = useCallback((options) => {
|
|
||||||
return innerDeleteLogs(wallet, options)
|
|
||||||
}, [innerDeleteLogs, wallet])
|
|
||||||
|
|
||||||
return { logger, deleteLogs }
|
return { logger, deleteLogs }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -281,7 +161,79 @@ function tag (wallet) {
|
||||||
return wallet?.shortName || wallet?.name
|
return wallet?.shortName || wallet?.name
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useWalletLogs (wallet) {
|
export function useWalletLogs (wallet, initialPage = 1, logsPerPage = 10) {
|
||||||
const logs = useContext(WalletLogsContext)
|
const [logs, setLogs] = useState([])
|
||||||
return logs.filter(l => !wallet || l.wallet === tag(wallet))
|
const [page, setPage] = useState(initialPage)
|
||||||
|
const [hasMore, setHasMore] = useState(true)
|
||||||
|
const [total, setTotal] = useState(0)
|
||||||
|
const [cursor, setCursor] = useState(null)
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
|
const { getPage, error: idbError } = useWalletLogDB()
|
||||||
|
const [getWalletLogs] = useLazyQuery(WALLET_LOGS, SSR ? {} : { fetchPolicy: 'cache-and-network' })
|
||||||
|
|
||||||
|
const loadLogsPage = useCallback(async (page, pageSize, wallet) => {
|
||||||
|
try {
|
||||||
|
let result = { data: [], hasMore: false }
|
||||||
|
const indexName = wallet ? 'wallet_ts' : 'ts'
|
||||||
|
const query = wallet ? window.IDBKeyRange.bound([tag(wallet), -Infinity], [tag(wallet), Infinity]) : null
|
||||||
|
result = await getPage(page, pageSize, indexName, query, 'prev')
|
||||||
|
// no walletType means we're using the local IDB
|
||||||
|
if (wallet && !wallet.walletType) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data } = await getWalletLogs({
|
||||||
|
variables: {
|
||||||
|
type: wallet?.walletType,
|
||||||
|
// if it client logs has more, page based on it's range
|
||||||
|
from: result?.data[result.data.length - 1]?.ts && result.hasMore ? String(result.data[result.data.length - 1].ts) : null,
|
||||||
|
// if we have a cursor (this isn't the first page), page based on it's range
|
||||||
|
to: result?.data[0]?.ts && cursor ? String(result.data[0].ts) : null,
|
||||||
|
cursor
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const newLogs = data.walletLogs.entries.map(({ createdAt, wallet: walletType, ...log }) => ({
|
||||||
|
ts: +new Date(createdAt),
|
||||||
|
wallet: tag(getWalletByType(walletType)),
|
||||||
|
...log
|
||||||
|
}))
|
||||||
|
const combinedLogs = Array.from(new Set([...result.data, ...newLogs].map(JSON.stringify))).map(JSON.parse).sort((a, b) => b.ts - a.ts)
|
||||||
|
|
||||||
|
setCursor(data.walletLogs.cursor)
|
||||||
|
return { ...result, data: combinedLogs, hasMore: result.hasMore || !!data.walletLogs.cursor }
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading logs from IndexedDB:', error)
|
||||||
|
return { data: [], total: 0, hasMore: false }
|
||||||
|
}
|
||||||
|
}, [getPage, setCursor, cursor])
|
||||||
|
|
||||||
|
if (idbError) {
|
||||||
|
console.error('IndexedDB error:', idbError)
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadMore = useCallback(async () => {
|
||||||
|
if (hasMore) {
|
||||||
|
setLoading(true)
|
||||||
|
const result = await loadLogsPage(page, logsPerPage, wallet)
|
||||||
|
setLogs(prevLogs => [...prevLogs, ...result.data])
|
||||||
|
setHasMore(result.hasMore)
|
||||||
|
setTotal(result.total)
|
||||||
|
setPage(prevPage => prevPage + 1)
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}, [loadLogsPage, page, logsPerPage, wallet, hasMore])
|
||||||
|
|
||||||
|
const loadLogs = useCallback(async () => {
|
||||||
|
setLoading(true)
|
||||||
|
const result = await loadLogsPage(1, logsPerPage, wallet)
|
||||||
|
setLogs(result.data)
|
||||||
|
setHasMore(result.hasMore)
|
||||||
|
setTotal(result.total)
|
||||||
|
setPage(1)
|
||||||
|
setLoading(false)
|
||||||
|
}, [wallet, loadLogsPage])
|
||||||
|
|
||||||
|
return { logs, hasMore, total, loadMore, loadLogs, setLogs, loading }
|
||||||
}
|
}
|
||||||
|
|
|
@ -197,13 +197,16 @@ export const WALLETS = gql`
|
||||||
`
|
`
|
||||||
|
|
||||||
export const WALLET_LOGS = gql`
|
export const WALLET_LOGS = gql`
|
||||||
query WalletLogs {
|
query WalletLogs($type: String, $from: String, $to: String, $cursor: String) {
|
||||||
walletLogs {
|
walletLogs(type: $type, from: $from, to: $to, cursor: $cursor) {
|
||||||
id
|
cursor
|
||||||
createdAt
|
entries {
|
||||||
wallet
|
id
|
||||||
level
|
createdAt
|
||||||
message
|
wallet
|
||||||
|
level
|
||||||
|
message
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
|
@ -17,7 +17,6 @@ import { SSR } from '@/lib/constants'
|
||||||
import NProgress from 'nprogress'
|
import NProgress from 'nprogress'
|
||||||
import 'nprogress/nprogress.css'
|
import 'nprogress/nprogress.css'
|
||||||
import { LoggerProvider } from '@/components/logger'
|
import { LoggerProvider } from '@/components/logger'
|
||||||
import { WalletLoggerProvider } from '@/components/wallet-logger'
|
|
||||||
import { ChainFeeProvider } from '@/components/chain-fee.js'
|
import { ChainFeeProvider } from '@/components/chain-fee.js'
|
||||||
import dynamic from 'next/dynamic'
|
import dynamic from 'next/dynamic'
|
||||||
import { HasNewNotesProvider } from '@/components/use-has-new-notes'
|
import { HasNewNotesProvider } from '@/components/use-has-new-notes'
|
||||||
|
@ -107,30 +106,28 @@ export default function MyApp ({ Component, pageProps: { ...props } }) {
|
||||||
<MeProvider me={me}>
|
<MeProvider me={me}>
|
||||||
<HasNewNotesProvider>
|
<HasNewNotesProvider>
|
||||||
<LoggerProvider>
|
<LoggerProvider>
|
||||||
<WalletLoggerProvider>
|
<WebLnProvider>
|
||||||
<WebLnProvider>
|
<ServiceWorkerProvider>
|
||||||
<ServiceWorkerProvider>
|
<AccountProvider>
|
||||||
<AccountProvider>
|
<PriceProvider price={price}>
|
||||||
<PriceProvider price={price}>
|
<LightningProvider>
|
||||||
<LightningProvider>
|
<ToastProvider>
|
||||||
<ToastProvider>
|
<ShowModalProvider>
|
||||||
<ShowModalProvider>
|
<BlockHeightProvider blockHeight={blockHeight}>
|
||||||
<BlockHeightProvider blockHeight={blockHeight}>
|
<ChainFeeProvider chainFee={chainFee}>
|
||||||
<ChainFeeProvider chainFee={chainFee}>
|
<ErrorBoundary>
|
||||||
<ErrorBoundary>
|
<Component ssrData={ssrData} {...otherProps} />
|
||||||
<Component ssrData={ssrData} {...otherProps} />
|
{!router?.query?.disablePrompt && <PWAPrompt copyBody='This website has app functionality. Add it to your home screen to use it in fullscreen and receive notifications. In Safari:' promptOnVisit={2} />}
|
||||||
{!router?.query?.disablePrompt && <PWAPrompt copyBody='This website has app functionality. Add it to your home screen to use it in fullscreen and receive notifications. In Safari:' promptOnVisit={2} />}
|
</ErrorBoundary>
|
||||||
</ErrorBoundary>
|
</ChainFeeProvider>
|
||||||
</ChainFeeProvider>
|
</BlockHeightProvider>
|
||||||
</BlockHeightProvider>
|
</ShowModalProvider>
|
||||||
</ShowModalProvider>
|
</ToastProvider>
|
||||||
</ToastProvider>
|
</LightningProvider>
|
||||||
</LightningProvider>
|
</PriceProvider>
|
||||||
</PriceProvider>
|
</AccountProvider>
|
||||||
</AccountProvider>
|
</ServiceWorkerProvider>
|
||||||
</ServiceWorkerProvider>
|
</WebLnProvider>
|
||||||
</WebLnProvider>
|
|
||||||
</WalletLoggerProvider>
|
|
||||||
</LoggerProvider>
|
</LoggerProvider>
|
||||||
</HasNewNotesProvider>
|
</HasNewNotesProvider>
|
||||||
</MeProvider>
|
</MeProvider>
|
||||||
|
|
Loading…
Reference in New Issue