import LogMessage from './log-message' import { createContext, useCallback, useContext, useEffect, useMemo, useRef, 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 { useMe } from './me' export function WalletLogs ({ wallet, embedded }) { const logs = useWalletLogs(wallet) const tableRef = useRef() const showModal = useShowModal() return ( <>
{ showModal(onClose => ) }} >clear logs
{logs.length === 0 &&
empty
} {logs.map((log, i) => ( ))}
------ start of logs ------
) } function DeleteWalletLogsObstacle ({ wallet, onClose }) { const toaster = useToast() const { deleteLogs } = useWalletLogger(wallet) const prompt = `Do you really want to delete all ${wallet ? '' : 'wallet'} logs ${wallet ? 'of this wallet' : ''}?` return (
{prompt}
cancel
) } const WalletLoggerContext = createContext() const WalletLogsContext = createContext() 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')) } }) } 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([]) 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 [deleteServerWalletLogs] = useMutation( gql` mutation deleteWalletLogs($wallet: String) { deleteWalletLogs(wallet: $wallet) } `, { onCompleted: (_, { variables: { wallet: walletType } }) => { setLogs((logs) => { 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) => { 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)) } } } catch (e) { console.error('failed to delete logs', e) } } }, [me, setLogs]) return ( {children} ) } export function useWalletLogger (wallet) { const { appendLog, deleteLogs: innerDeleteLogs } = useContext(WalletLoggerContext) const log = useCallback(level => message => { if (!wallet) { console.error('cannot log: no wallet set') 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]) const logger = useMemo(() => ({ ok: (...message) => log('ok')(message.join(' ')), info: (...message) => log('info')(message.join(' ')), error: (...message) => log('error')(message.join(' ')) }), [log, wallet?.name]) const deleteLogs = useCallback((options) => { return innerDeleteLogs(wallet, options) }, [innerDeleteLogs, wallet]) return { logger, deleteLogs } } 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)) }