refactor: local storage

This commit is contained in:
DarkSky
2022-08-11 01:45:38 +08:00
parent 89191290e4
commit 86090be4a3
12 changed files with 318 additions and 74 deletions

View File

@@ -136,6 +136,7 @@ interface BlockInstance<C extends ContentOperation> {
interface AsyncDatabaseAdapter<C extends ContentOperation> {
inspector(): Record<string, any>;
reload(): void;
createBlock(
options: Pick<BlockItem<C>, 'type' | 'flavor'> & {
binary?: ArrayBuffer;
@@ -156,6 +157,33 @@ interface AsyncDatabaseAdapter<C extends ContentOperation> {
getUserId(): string;
}
export type DataExporter = (binary: Uint8Array) => Promise<void>;
export const getDataExporter = () => {
let exporter: DataExporter | undefined = undefined;
let importer: (() => Uint8Array | undefined) | undefined = undefined;
const importData = () => importer?.();
const exportData = (binary: Uint8Array) => exporter?.(binary);
const hasExporter = () => !!exporter;
const installExporter = (
initialData: Uint8Array | undefined,
cb: DataExporter
) => {
return new Promise<void>(resolve => {
importer = () => initialData;
exporter = async (data: Uint8Array) => {
exporter = cb;
await cb(data);
resolve();
};
});
};
return { importData, exportData, hasExporter, installExporter };
};
export type {
AsyncDatabaseAdapter,
BlockPosition,

View File

@@ -153,19 +153,21 @@ export class YjsAdapter implements AsyncDatabaseAdapter<YjsContentOperation> {
private readonly _doc: Doc; // doc instance
private readonly _awareness: Awareness; // lightweight state synchronization
private readonly _gatekeeper: GateKeeper; // Simple access control
private readonly _history: YjsHistoryManager;
private readonly _history!: YjsHistoryManager;
// Block Collection
// key is a randomly generated global id
private readonly _blocks: YMap<YMap<unknown>>;
private readonly _blockUpdated: YMap<number>;
private readonly _blocks!: YMap<YMap<unknown>>;
private readonly _blockUpdated!: YMap<number>;
// Maximum cache Block 1024, ttl 10 minutes
private readonly _blockCaches: LRUCache<string, YjsBlockInstance>;
private readonly _blockCaches!: LRUCache<string, YjsBlockInstance>;
private readonly _binaries: YjsRemoteBinaries;
private readonly _binaries!: YjsRemoteBinaries;
private readonly _listener: Map<string, BlockListener<any>>;
private readonly _reload: () => void;
static async init(
workspace: string,
options: YjsInitOptions
@@ -184,18 +186,28 @@ export class YjsAdapter implements AsyncDatabaseAdapter<YjsContentOperation> {
this._doc = providers.idb.doc;
this._awareness = providers.awareness;
this._gatekeeper = providers.gatekeeper;
const blocks = this._doc.getMap<YMap<any>>('blocks');
this._blocks =
blocks.get('content') || blocks.set('content', new YMap());
this._blockUpdated =
blocks.get('updated') || blocks.set('updated', new YMap());
this._blockCaches = new LRUCache({ max: 1024, ttl: 1000 * 60 * 10 });
this._binaries = new YjsRemoteBinaries(
providers.binariesIdb.doc.getMap(),
providers.remoteToken
);
this._history = new YjsHistoryManager(this._blocks);
this._reload = () => {
const blocks = this._doc.getMap<YMap<any>>('blocks');
// @ts-ignore
this._blocks =
blocks.get('content') || blocks.set('content', new YMap());
// @ts-ignore
this._blockUpdated =
blocks.get('updated') || blocks.set('updated', new YMap());
// @ts-ignore
this._blockCaches = new LRUCache({
max: 1024,
ttl: 1000 * 60 * 10,
});
// @ts-ignore
this._binaries = new YjsRemoteBinaries(
providers.binariesIdb.doc.getMap(),
providers.remoteToken
);
// @ts-ignore
this._history = new YjsHistoryManager(this._blocks);
};
this._reload();
this._listener = new Map();
@@ -281,6 +293,10 @@ export class YjsAdapter implements AsyncDatabaseAdapter<YjsContentOperation> {
});
}
reload() {
this._reload();
}
getUserId(): string {
return this._provider.userId;
}

View File

@@ -22,8 +22,9 @@ export type YjsProvider = (instances: YjsDefaultInstances) => Promise<void>;
export type YjsProviderOptions = {
backend: typeof BucketBackend[keyof typeof BucketBackend];
params?: Record<string, string>;
importData?: Uint8Array;
exportData?: (binary: Uint8Array) => void;
importData?: () => Promise<Uint8Array> | Uint8Array | undefined;
exportData?: (binary: Uint8Array) => Promise<void> | undefined;
hasExporter?: () => boolean;
};
export const getYjsProviders = (
@@ -31,13 +32,20 @@ export const getYjsProviders = (
): Record<string, YjsProvider> => {
return {
sqlite: async (instances: YjsDefaultInstances) => {
const fs = new SQLiteProvider(
instances.workspace,
instances.doc,
options.importData
);
if (options.exportData) fs.registerExporter(options.exportData);
await fs.whenSynced;
const fsHandle = setInterval(async () => {
if (options.hasExporter?.()) {
clearInterval(fsHandle);
const fs = new SQLiteProvider(
instances.workspace,
instances.doc,
await options.importData?.()
);
if (options.exportData) {
fs.registerExporter(options.exportData);
}
await fs.whenSynced;
}
}, 500);
},
ws: async (instances: YjsDefaultInstances) => {
if (instances.token) {

View File

@@ -14,6 +14,8 @@ import {
HistoryManager,
ContentTypes,
Connectivity,
DataExporter,
getDataExporter,
} from './adapter';
import {
getYjsProviders,
@@ -66,6 +68,10 @@ type BlockClientOptions = {
content?: BlockExporters<string>;
metadata?: BlockExporters<Array<[string, number | string | string[]]>>;
tagger?: BlockExporters<string[]>;
installExporter: (
initialData: Uint8Array,
exporter: DataExporter
) => Promise<void>;
};
export class BlockClient<
@@ -95,10 +101,15 @@ export class BlockClient<
private readonly _root: { node?: BaseBlock<B, C> };
private readonly _installExporter: (
initialData: Uint8Array,
exporter: DataExporter
) => Promise<void>;
private constructor(
adapter: A,
workspace: string,
options?: BlockClientOptions
options: BlockClientOptions
) {
this._adapter = adapter;
this._workspace = workspace;
@@ -142,6 +153,7 @@ export class BlockClient<
});
this._root = {};
this._installExporter = options.installExporter;
}
public addBlockListener(tag: string, listener: BlockListener) {
@@ -590,21 +602,34 @@ export class BlockClient<
return this._adapter.history();
}
public async setupDataExporter(initialData: Uint8Array, cb: DataExporter) {
await this._installExporter(initialData, cb);
this._adapter.reload();
}
public static async init(
workspace: string,
options: Partial<
YjsInitOptions & YjsProviderOptions & BlockClientOptions
> = {}
): Promise<BlockClientInstance> {
const { importData, exportData, hasExporter, installExporter } =
getDataExporter();
const instance = await YjsAdapter.init(workspace, {
provider: getYjsProviders({
backend: BucketBackend.YjsWebSocketAffine,
exportData: console.log.bind(console),
importData,
exportData,
hasExporter,
...options,
}),
...options,
});
return new BlockClient(instance, workspace, options);
return new BlockClient(instance, workspace, {
...options,
installExporter,
});
}
}