import { get, set, isArray } from 'lodash'; import { LowSync, LocalStorage } from 'lowdb'; import * as Sentry from '@sentry/browser'; import { isBrowser } from './browser'; type Data = { sys: Record; database: Record; }; /** 这里在 index.html 中也用到了,改的时候需要同步改一下 */ const adapter = isBrowser ? new LocalStorage('dodoex-app') : null; const db = isBrowser && adapter ? new LowSync(adapter) : null; export default db; function modifyDatabase({ dbData, dbName, dbNestName, }: { dbData: Data; dbName: keyof Data; dbNestName: 'user' | 'public'; }) { if (!db) { return; } try { if ( dbData[dbName] && dbData[dbName][dbNestName] && isArray(dbData[dbName][dbNestName]) ) { const oldData = dbData[dbName][dbNestName] as Array; const newData: Record = {}; oldData.forEach((d, i) => { if (d) { newData[String(i)] = d; } }); set(dbData, `${dbName}.${dbNestName}`, newData); // side effect db.write(); console.warn( `[lowdb migrate ${dbName}.${dbNestName} success], new data: `, dbData, ); } } catch (error) { console.error(`[lowdb migrate ${dbName}.${dbNestName} fail]: `, error); } } /** * Checks if the path exists and initializes if it doesn't. */ export function pathInit({ path, user, dbName = 'database', validator = () => true, defaultValue, }: { path: string; user?: string; dbName?: keyof Data; validator?: (v: unknown) => true; defaultValue?: unknown; }): string { if (!db) { return ''; } let currentPath = `${dbName}.public.${path}`; if (user) { currentPath = `${dbName}.user.${user}.${path}`; } db.read(); if (db.data === null) { db.data = { sys: {}, database: { public: { // 占位;避免在初次访问时保存数组到 localstorage 中 '0': { why: 'placeholder', }, }, }, }; } // 迁移 lowdb1.0 到 3.0,旧版在 aurora 新链下 chainId 变为 1313161554 导致保存失败,升级到新版同时将数据由数组形式改为对象 modifyDatabase({ dbData: db.data, dbName, dbNestName: 'public', }); modifyDatabase({ dbData: db.data, dbName, dbNestName: 'user', }); const value = get(db.data, currentPath); if (!(value !== undefined && validator(value))) { set(db.data, currentPath, defaultValue); db.write(); } return currentPath; } /** * Store data to a specific location * Path does not exist will be initialized automatically */ export function dbSet({ dbName, path, value, user, }: { dbName?: keyof Data; path: string; value: T; user?: string; }) { if (!db) { return; } try { const setPath = pathInit({ path, user, dbName }); db.read(); if (db.data !== null) { set(db.data, setPath, value); db.write(); } } catch (error) { const report = async () => { let size = 0; const storageSizeObject: { [key in string]: number } = {}; for (const item in localStorage) { if (Object.prototype.hasOwnProperty.call(localStorage, item)) { const itemSize = localStorage.getItem(item)?.length || 0; size += itemSize; storageSizeObject[item] = itemSize; } } let quota: StorageEstimate | undefined; if (navigator.storage && navigator.storage.estimate) { quota = await navigator.storage.estimate(); } if (Sentry) { Sentry.withScope(function (scope) { const api = '[dbSet-localstorage]'; scope.setTag('api', api); const params = { size, storageSizeObject, quota, error, }; scope.setExtra('params', params); Sentry.captureException(error); console.error('api:', api, 'params:', params, 'error:', error); }); } else { console.error(error); } }; report(); } } /** * Get data */ export function dbGet({ dbName, path, defaultValue, user, }: { dbName?: keyof Data; path: string; defaultValue?: T; user?: string; }) { if (!db) { return defaultValue; } const getPath = pathInit({ path, user, dbName, defaultValue }); db.read(); return get(db.data, getPath, defaultValue); }