feat(core): new async global state storage impl (#11794)
This commit is contained in:
10
packages/common/infra/src/storage/async-memento.ts
Normal file
10
packages/common/infra/src/storage/async-memento.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { type Observable } from 'rxjs';
|
||||||
|
|
||||||
|
export interface AsyncMemento {
|
||||||
|
watch<T>(key: string): Observable<T | undefined>;
|
||||||
|
get<T>(key: string): Promise<T | undefined>;
|
||||||
|
set<T>(key: string, value: T | undefined): Promise<void>;
|
||||||
|
del(key: string): Promise<void>;
|
||||||
|
clear(): Promise<void>;
|
||||||
|
keys(): Promise<string[]>;
|
||||||
|
}
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
|
export * from './async-memento';
|
||||||
export * from './kv';
|
export * from './kv';
|
||||||
export * from './memento';
|
export * from './memento';
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import type { Memento } from '@toeverything/infra';
|
import type { AsyncMemento, Memento } from '@toeverything/infra';
|
||||||
import EventEmitter2 from 'eventemitter2';
|
import EventEmitter2 from 'eventemitter2';
|
||||||
|
import { type IDBPDatabase, openDB } from 'idb';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
CacheStorage,
|
||||||
GlobalCache,
|
GlobalCache,
|
||||||
GlobalSessionState,
|
GlobalSessionState,
|
||||||
GlobalState,
|
GlobalState,
|
||||||
@@ -99,3 +101,131 @@ export class SessionStorageGlobalSessionState
|
|||||||
super(sessionStorage, 'global-session-state:');
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,11 +16,13 @@ import { type Framework } from '@toeverything/infra';
|
|||||||
import { DesktopApiService } from '../desktop-api';
|
import { DesktopApiService } from '../desktop-api';
|
||||||
import { ElectronGlobalCache, ElectronGlobalState } from './impls/electron';
|
import { ElectronGlobalCache, ElectronGlobalState } from './impls/electron';
|
||||||
import {
|
import {
|
||||||
|
IDBGlobalState,
|
||||||
LocalStorageGlobalCache,
|
LocalStorageGlobalCache,
|
||||||
LocalStorageGlobalState,
|
LocalStorageGlobalState,
|
||||||
SessionStorageGlobalSessionState,
|
SessionStorageGlobalSessionState,
|
||||||
} from './impls/storage';
|
} from './impls/storage';
|
||||||
import {
|
import {
|
||||||
|
CacheStorage,
|
||||||
GlobalCache,
|
GlobalCache,
|
||||||
GlobalSessionState,
|
GlobalSessionState,
|
||||||
GlobalState,
|
GlobalState,
|
||||||
@@ -43,11 +45,13 @@ export const configureStorageModule = (framework: Framework) => {
|
|||||||
export function configureLocalStorageStateStorageImpls(framework: Framework) {
|
export function configureLocalStorageStateStorageImpls(framework: Framework) {
|
||||||
framework.impl(GlobalCache, LocalStorageGlobalCache);
|
framework.impl(GlobalCache, LocalStorageGlobalCache);
|
||||||
framework.impl(GlobalState, LocalStorageGlobalState);
|
framework.impl(GlobalState, LocalStorageGlobalState);
|
||||||
|
framework.impl(CacheStorage, IDBGlobalState);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function configureElectronStateStorageImpls(framework: Framework) {
|
export function configureElectronStateStorageImpls(framework: Framework) {
|
||||||
framework.impl(GlobalCache, ElectronGlobalCache, [DesktopApiService]);
|
framework.impl(GlobalCache, ElectronGlobalCache, [DesktopApiService]);
|
||||||
framework.impl(GlobalState, ElectronGlobalState, [DesktopApiService]);
|
framework.impl(GlobalState, ElectronGlobalState, [DesktopApiService]);
|
||||||
|
framework.impl(CacheStorage, IDBGlobalState);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function configureCommonGlobalStorageImpls(framework: Framework) {
|
export function configureCommonGlobalStorageImpls(framework: Framework) {
|
||||||
|
|||||||
@@ -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.
|
* 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 interface GlobalSessionState extends Memento {}
|
||||||
export const GlobalSessionState =
|
export const GlobalSessionState =
|
||||||
createIdentifier<GlobalSessionState>('GlobalSessionState');
|
createIdentifier<GlobalSessionState>('GlobalSessionState');
|
||||||
|
|
||||||
|
export interface CacheStorage extends AsyncMemento {}
|
||||||
|
|
||||||
|
export const CacheStorage = createIdentifier<CacheStorage>('CacheStorage');
|
||||||
|
|||||||
Reference in New Issue
Block a user