parent
00bcd8c992
commit
5543a0755a
|
@ -351,20 +351,58 @@ const resolvers = {
|
|||
facts: history
|
||||
}
|
||||
},
|
||||
walletLogs: async (parent, args, { me, models }) => {
|
||||
walletLogs: async (parent, { type, from, to, cursor }, { me, models }) => {
|
||||
if (!me) {
|
||||
throw new GqlAuthenticationError()
|
||||
}
|
||||
|
||||
return await models.walletLog.findMany({
|
||||
where: {
|
||||
userId: me.id
|
||||
},
|
||||
orderBy: [
|
||||
{ createdAt: 'desc' },
|
||||
{ id: 'desc' }
|
||||
]
|
||||
})
|
||||
// we cursoring with the wallet logs on the client
|
||||
// if we have from, don't use cursor
|
||||
// regardless, store the state of the cursor for the next call
|
||||
|
||||
const decodedCursor = cursor ? decodeCursor(cursor) : { offset: 0, time: to ?? new Date() }
|
||||
|
||||
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: {
|
||||
|
|
|
@ -66,7 +66,7 @@ const typeDefs = `
|
|||
wallets: [Wallet!]!
|
||||
wallet(id: ID!): Wallet
|
||||
walletByType(type: String!): Wallet
|
||||
walletLogs: [WalletLog]!
|
||||
walletLogs(type: String, from: String, to: String, cursor: String): WalletLog!
|
||||
}
|
||||
|
||||
extend type Mutation {
|
||||
|
@ -154,6 +154,11 @@ const typeDefs = `
|
|||
}
|
||||
|
||||
type WalletLog {
|
||||
entries: [WalletLogEntry!]!
|
||||
cursor: String
|
||||
}
|
||||
|
||||
type WalletLogEntry {
|
||||
id: ID!
|
||||
createdAt: Date!
|
||||
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 { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import styles from '@/styles/log.module.css'
|
||||
import { Button } from 'react-bootstrap'
|
||||
import { useToast } from './toast'
|
||||
import { useShowModal } from './modal'
|
||||
import { WALLET_LOGS } from '@/fragments/wallet'
|
||||
import { getWalletByType } from 'wallets'
|
||||
import { gql, useMutation, useQuery } from '@apollo/client'
|
||||
import { gql, useLazyQuery, useMutation } from '@apollo/client'
|
||||
import { useMe } from './me'
|
||||
import useIndexedDB from './use-indexeddb'
|
||||
import { SSR } from '@/lib/constants'
|
||||
|
||||
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()
|
||||
|
||||
return (
|
||||
|
@ -21,13 +25,12 @@ export function WalletLogs ({ wallet, embedded }) {
|
|||
<span
|
||||
style={{ cursor: 'pointer' }}
|
||||
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
|
||||
</span>
|
||||
</div>
|
||||
<div ref={tableRef} className={`${styles.logTable} ${embedded ? styles.embedded : ''}`}>
|
||||
{logs.length === 0 && <div className='w-100 text-center'>empty</div>}
|
||||
<div className={`${styles.logTable} ${embedded ? styles.embedded : ''}`}>
|
||||
<table>
|
||||
<tbody>
|
||||
{logs.map((log, i) => (
|
||||
|
@ -39,15 +42,20 @@ export function WalletLogs ({ wallet, embedded }) {
|
|||
))}
|
||||
</tbody>
|
||||
</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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function DeleteWalletLogsObstacle ({ wallet, onClose }) {
|
||||
function DeleteWalletLogsObstacle ({ wallet, setLogs, onClose }) {
|
||||
const { deleteLogs } = useWalletLogger(wallet, setLogs)
|
||||
const toaster = useToast()
|
||||
const { deleteLogs } = useWalletLogger(wallet)
|
||||
|
||||
const prompt = `Do you really want to delete all ${wallet ? '' : 'wallet'} logs ${wallet ? 'of this wallet' : ''}?`
|
||||
return (
|
||||
|
@ -60,7 +68,7 @@ function DeleteWalletLogsObstacle ({ wallet, onClose }) {
|
|||
onClick={
|
||||
async () => {
|
||||
try {
|
||||
await deleteLogs()
|
||||
await deleteLogs(wallet)
|
||||
onClose()
|
||||
toaster.success('deleted wallet logs')
|
||||
} catch (err) {
|
||||
|
@ -76,72 +84,31 @@ function DeleteWalletLogsObstacle ({ wallet, onClose }) {
|
|||
)
|
||||
}
|
||||
|
||||
const WalletLoggerContext = createContext()
|
||||
const WalletLogsContext = createContext()
|
||||
const INDICES = [
|
||||
{ name: 'ts', keyPath: 'ts' },
|
||||
{ name: 'wallet_ts', keyPath: ['wallet', 'ts'] }
|
||||
]
|
||||
|
||||
const initIndexedDB = async (dbName, storeName) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!window.indexedDB) {
|
||||
return reject(new Error('IndexedDB not supported'))
|
||||
}
|
||||
|
||||
// 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'))
|
||||
}
|
||||
})
|
||||
function useWalletLogDB () {
|
||||
const { me } = useMe()
|
||||
const dbName = `app:storage${me ? `:${me.id}` : ''}`
|
||||
const idbStoreName = 'wallet_logs'
|
||||
const { add, getPage, clear, error: idbError } = useIndexedDB(dbName, idbStoreName, 1, INDICES)
|
||||
return { add, getPage, clear, error: idbError }
|
||||
}
|
||||
|
||||
export const WalletLoggerProvider = ({ children }) => {
|
||||
const { me } = useMe()
|
||||
const [logs, setLogs] = useState([])
|
||||
let dbName = 'app:storage'
|
||||
if (me) {
|
||||
dbName = `${dbName}:${me.id}`
|
||||
}
|
||||
const idbStoreName = 'wallet_logs'
|
||||
const idb = useRef()
|
||||
const logQueue = useRef([])
|
||||
export function useWalletLogger (wallet, setLogs) {
|
||||
const { add, clear } = useWalletLogDB()
|
||||
|
||||
useQuery(WALLET_LOGS, {
|
||||
fetchPolicy: 'network-only',
|
||||
// required to trigger onCompleted on refetches
|
||||
notifyOnNetworkStatusChange: true,
|
||||
onCompleted: ({ walletLogs }) => {
|
||||
setLogs((prevLogs) => {
|
||||
const existingIds = prevLogs.map(({ id }) => id)
|
||||
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)
|
||||
})
|
||||
const appendLog = useCallback(async (wallet, level, message) => {
|
||||
const log = { wallet: tag(wallet), level, message, ts: +new Date() }
|
||||
try {
|
||||
await add(log)
|
||||
setLogs?.(prevLogs => [log, ...prevLogs])
|
||||
} catch (error) {
|
||||
console.error('Failed to append log:', error)
|
||||
}
|
||||
})
|
||||
}, [add])
|
||||
|
||||
const [deleteServerWalletLogs] = useMutation(
|
||||
gql`
|
||||
|
@ -151,105 +118,25 @@ export const WalletLoggerProvider = ({ children }) => {
|
|||
`,
|
||||
{
|
||||
onCompleted: (_, { variables: { wallet: walletType } }) => {
|
||||
setLogs((logs) => {
|
||||
return logs.filter(l => walletType ? l.wallet !== getWalletByType(walletType).name : false)
|
||||
})
|
||||
setLogs?.(logs => 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) => {
|
||||
if ((!wallet || wallet.walletType) && !options?.clientOnly) {
|
||||
await deleteServerWalletLogs({ variables: { wallet: wallet?.walletType } })
|
||||
}
|
||||
if (!wallet || wallet.sendPayment) {
|
||||
try {
|
||||
const tx = idb.current.transaction(idbStoreName, 'readwrite')
|
||||
const objectStore = tx.objectStore(idbStoreName)
|
||||
const idx = objectStore.index('wallet_ts')
|
||||
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))
|
||||
}
|
||||
}
|
||||
const walletTag = wallet ? tag(wallet) : null
|
||||
await clear('wallet_ts', walletTag ? window.IDBKeyRange.bound([walletTag, 0], [walletTag, Infinity]) : null)
|
||||
setLogs?.(logs => logs.filter(l => wallet ? l.wallet !== tag(wallet) : false))
|
||||
} catch (e) {
|
||||
console.error('failed to delete logs', e)
|
||||
}
|
||||
}
|
||||
}, [me, 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)
|
||||
}, [clear, deleteServerWalletLogs, setLogs])
|
||||
|
||||
const log = useCallback(level => message => {
|
||||
if (!wallet) {
|
||||
|
@ -257,9 +144,6 @@ export function useWalletLogger (wallet) {
|
|||
return
|
||||
}
|
||||
|
||||
// TODO:
|
||||
// also send this to us if diagnostics was enabled,
|
||||
// very similar to how the service worker logger works.
|
||||
appendLog(wallet, level, message)
|
||||
console[level !== 'error' ? 'info' : 'error'](`[${tag(wallet)}]`, message)
|
||||
}, [appendLog, wallet])
|
||||
|
@ -270,10 +154,6 @@ export function useWalletLogger (wallet) {
|
|||
error: (...message) => log('error')(message.join(' '))
|
||||
}), [log, wallet?.name])
|
||||
|
||||
const deleteLogs = useCallback((options) => {
|
||||
return innerDeleteLogs(wallet, options)
|
||||
}, [innerDeleteLogs, wallet])
|
||||
|
||||
return { logger, deleteLogs }
|
||||
}
|
||||
|
||||
|
@ -281,7 +161,79 @@ function tag (wallet) {
|
|||
return wallet?.shortName || wallet?.name
|
||||
}
|
||||
|
||||
export function useWalletLogs (wallet) {
|
||||
const logs = useContext(WalletLogsContext)
|
||||
return logs.filter(l => !wallet || l.wallet === tag(wallet))
|
||||
export function useWalletLogs (wallet, initialPage = 1, logsPerPage = 10) {
|
||||
const [logs, setLogs] = useState([])
|
||||
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`
|
||||
query WalletLogs {
|
||||
walletLogs {
|
||||
id
|
||||
createdAt
|
||||
wallet
|
||||
level
|
||||
message
|
||||
query WalletLogs($type: String, $from: String, $to: String, $cursor: String) {
|
||||
walletLogs(type: $type, from: $from, to: $to, cursor: $cursor) {
|
||||
cursor
|
||||
entries {
|
||||
id
|
||||
createdAt
|
||||
wallet
|
||||
level
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
|
|
@ -17,7 +17,6 @@ import { SSR } from '@/lib/constants'
|
|||
import NProgress from 'nprogress'
|
||||
import 'nprogress/nprogress.css'
|
||||
import { LoggerProvider } from '@/components/logger'
|
||||
import { WalletLoggerProvider } from '@/components/wallet-logger'
|
||||
import { ChainFeeProvider } from '@/components/chain-fee.js'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { HasNewNotesProvider } from '@/components/use-has-new-notes'
|
||||
|
@ -107,30 +106,28 @@ export default function MyApp ({ Component, pageProps: { ...props } }) {
|
|||
<MeProvider me={me}>
|
||||
<HasNewNotesProvider>
|
||||
<LoggerProvider>
|
||||
<WalletLoggerProvider>
|
||||
<WebLnProvider>
|
||||
<ServiceWorkerProvider>
|
||||
<AccountProvider>
|
||||
<PriceProvider price={price}>
|
||||
<LightningProvider>
|
||||
<ToastProvider>
|
||||
<ShowModalProvider>
|
||||
<BlockHeightProvider blockHeight={blockHeight}>
|
||||
<ChainFeeProvider chainFee={chainFee}>
|
||||
<ErrorBoundary>
|
||||
<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} />}
|
||||
</ErrorBoundary>
|
||||
</ChainFeeProvider>
|
||||
</BlockHeightProvider>
|
||||
</ShowModalProvider>
|
||||
</ToastProvider>
|
||||
</LightningProvider>
|
||||
</PriceProvider>
|
||||
</AccountProvider>
|
||||
</ServiceWorkerProvider>
|
||||
</WebLnProvider>
|
||||
</WalletLoggerProvider>
|
||||
<WebLnProvider>
|
||||
<ServiceWorkerProvider>
|
||||
<AccountProvider>
|
||||
<PriceProvider price={price}>
|
||||
<LightningProvider>
|
||||
<ToastProvider>
|
||||
<ShowModalProvider>
|
||||
<BlockHeightProvider blockHeight={blockHeight}>
|
||||
<ChainFeeProvider chainFee={chainFee}>
|
||||
<ErrorBoundary>
|
||||
<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} />}
|
||||
</ErrorBoundary>
|
||||
</ChainFeeProvider>
|
||||
</BlockHeightProvider>
|
||||
</ShowModalProvider>
|
||||
</ToastProvider>
|
||||
</LightningProvider>
|
||||
</PriceProvider>
|
||||
</AccountProvider>
|
||||
</ServiceWorkerProvider>
|
||||
</WebLnProvider>
|
||||
</LoggerProvider>
|
||||
</HasNewNotesProvider>
|
||||
</MeProvider>
|
||||
|
|
Loading…
Reference in New Issue