mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 05:14:54 +00:00
feat(core): new async global state storage impl (#11794)
This commit is contained in:
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user