feat(core): new async global state storage impl (#11794)

This commit is contained in:
CatsJuice
2025-04-23 05:28:21 +00:00
parent 9baef237f2
commit 9b2cf5cafa
5 changed files with 155 additions and 2 deletions

View File

@@ -1,8 +1,10 @@
import type { Memento } from '@toeverything/infra';
import type { AsyncMemento, Memento } from '@toeverything/infra';
import EventEmitter2 from 'eventemitter2';
import { type IDBPDatabase, openDB } from 'idb';
import { Observable } from 'rxjs';
import type {
CacheStorage,
GlobalCache,
GlobalSessionState,
GlobalState,
@@ -99,3 +101,131 @@ export class SessionStorageGlobalSessionState
super(sessionStorage, 'global-session-state:');
}
}
export class AsyncStorageMemento implements AsyncMemento {
// eventEmitter is used for same tab event
private readonly eventEmitter = new EventEmitter2();
// channel is used for cross-tab event
private readonly channel = new BroadcastChannel(this.dbName);
constructor(
private readonly dbName: string,
private readonly table: string
) {}
private _db: IDBPDatabase<any> | null = null;
private async getDB() {
const { dbName, table } = this;
if (!this._db) {
this._db = await openDB(dbName, 1, {
upgrade(db) {
if (!db.objectStoreNames.contains(table)) {
db.createObjectStore(table, { keyPath: 'key' });
}
},
});
}
return this._db;
}
async get<T>(key: string): Promise<T | undefined> {
const db = await this.getDB();
const tx = db.transaction(this.table, 'readonly');
const store = tx.objectStore(this.table);
const result = await store.get(key);
return result?.value;
}
watch<T>(key: string): Observable<T | undefined> {
return new Observable<T | undefined>(subscriber => {
// Get initial value
this.get<T>(key).then(
value => {
subscriber.next(value);
},
error => {
console.error('Error getting initial value:', error);
subscriber.next(undefined);
}
);
// Listen for same tab events
const eventEmitterCb = (value: T) => {
subscriber.next(value);
};
this.eventEmitter.on(key, eventEmitterCb);
// Listen for cross-tab events
// eslint-disable-next-line sonarjs/no-identical-functions
const channelCb = (event: MessageEvent) => {
if (event.data.key === key) {
subscriber.next(event.data.value);
}
};
this.channel.addEventListener('message', channelCb);
return () => {
this.eventEmitter.off(key, eventEmitterCb);
this.channel.removeEventListener('message', channelCb);
};
});
}
async set<T>(key: string, value: T | undefined): Promise<void> {
const db = await this.getDB();
const tx = db.transaction(this.table, 'readwrite');
const store = tx.objectStore(this.table);
if (value === undefined) {
await store.delete(key);
} else {
await store.put({ key, value });
}
// Emit events
this.eventEmitter.emit(key, value);
this.channel.postMessage({ key, value });
}
async del(key: string): Promise<void> {
const db = await this.getDB();
const tx = db.transaction(this.table, 'readwrite');
const store = tx.objectStore(this.table);
await store.delete(key);
// Emit events
this.eventEmitter.emit(key, undefined);
this.channel.postMessage({ key, value: undefined });
}
async clear(): Promise<void> {
const keys = await this.keys();
const db = await this.getDB();
const tx = db.transaction(this.table, 'readwrite');
const store = tx.objectStore(this.table);
await store.clear();
// Notify observers about each deleted key
for (const key of keys) {
this.eventEmitter.emit(key, undefined);
this.channel.postMessage({ key, value: undefined });
}
}
async keys(): Promise<string[]> {
const db = await this.getDB();
const tx = db.transaction(this.table, 'readonly');
const store = tx.objectStore(this.table);
const allObjects = await store.getAll();
return allObjects.map(obj => obj.key);
}
}
export class IDBGlobalState
extends AsyncStorageMemento
implements CacheStorage
{
constructor() {
super('global-storage', 'global-state');
}
}

View File

@@ -16,11 +16,13 @@ import { type Framework } from '@toeverything/infra';
import { DesktopApiService } from '../desktop-api';
import { ElectronGlobalCache, ElectronGlobalState } from './impls/electron';
import {
IDBGlobalState,
LocalStorageGlobalCache,
LocalStorageGlobalState,
SessionStorageGlobalSessionState,
} from './impls/storage';
import {
CacheStorage,
GlobalCache,
GlobalSessionState,
GlobalState,
@@ -43,11 +45,13 @@ export const configureStorageModule = (framework: Framework) => {
export function configureLocalStorageStateStorageImpls(framework: Framework) {
framework.impl(GlobalCache, LocalStorageGlobalCache);
framework.impl(GlobalState, LocalStorageGlobalState);
framework.impl(CacheStorage, IDBGlobalState);
}
export function configureElectronStateStorageImpls(framework: Framework) {
framework.impl(GlobalCache, ElectronGlobalCache, [DesktopApiService]);
framework.impl(GlobalState, ElectronGlobalState, [DesktopApiService]);
framework.impl(CacheStorage, IDBGlobalState);
}
export function configureCommonGlobalStorageImpls(framework: Framework) {

View File

@@ -1,4 +1,8 @@
import { createIdentifier, type Memento } from '@toeverything/infra';
import {
type AsyncMemento,
createIdentifier,
type Memento,
} from '@toeverything/infra';
/**
* A memento object that stores the entire application state.
@@ -25,3 +29,7 @@ export const GlobalCache = createIdentifier<GlobalCache>('GlobalCache');
export interface GlobalSessionState extends Memento {}
export const GlobalSessionState =
createIdentifier<GlobalSessionState>('GlobalSessionState');
export interface CacheStorage extends AsyncMemento {}
export const CacheStorage = createIdentifier<CacheStorage>('CacheStorage');