json-crdt-repo
Version:
JSON CRDT server and syncing local-first browser client
148 lines (147 loc) • 6.61 kB
TypeScript
import { BehaviorSubject, type Observable } from 'rxjs';
import { Model, Patch } from 'json-joy/lib/json-crdt';
import type { ServerBatch, ServerHistory } from '../../remote/types';
import type { BlockId, LocalRepo, LocalRepoEvent, LocalRepoSyncRequest, LocalRepoSyncResponse, LocalRepoGetResponse, LocalRepoGetRequest, LocalRepoCreateResponse, LocalRepoCreateRequest, LocalRepoGetIfResponse, LocalRepoGetIfRequest, LocalRepoPullResponse } from '../types';
import type { BinStrLevel, BinStrLevelOperation, BlockMeta, SyncResult, LevelLocalRepoPubSub, LevelLocalRepoCursor, BlockSyncRecord } from './types';
import type { CrudLocalRepoCipher } from './types';
import type { Locks } from 'thingies/lib/Locks';
import type { JsonValueCodec } from '@jsonjoy.com/json-pack/lib/codecs/types';
export interface LevelLocalRepoOpts {
/**
* Session ID of the user on this device. The same session ID is reused across
* all tabs.
*/
readonly sid: number;
/**
* Local persistance LevelDB API.
*/
readonly kv: BinStrLevel;
/**
* Optional content encryption/decryption API.
*/
readonly cipher?: CrudLocalRepoCipher;
/**
* Cross-tab locking API.
*/
readonly locks: Locks;
/**
* Optional observable that emits `true` when the device is connected to the
* server and `false` when it's not.
*/
readonly connected$?: BehaviorSubject<boolean>;
/**
* RPC API for communication with the server.
*/
readonly rpc: ServerHistory;
/**
* Event bus.
*/
readonly pubsub?: LevelLocalRepoPubSub;
/**
* Number of milliseconds after which remote calls are considered timed out.
*/
readonly remoteTimeout?: number;
/**
* Minimum backoff time in milliseconds for the sync loop.
*/
readonly syncLoopMinBackoff?: number;
/**
* Maximum backoff time in milliseconds for the sync loop.
*/
readonly syncLoopMaxBackoff?: number;
/**
* If specified, background sync errors will be emitted here.
*/
readonly onSyncError?: (error: Error | unknown) => void;
}
export declare class LevelLocalRepo implements LocalRepo {
protected readonly opts: LevelLocalRepoOpts;
readonly kv: BinStrLevel;
readonly locks: Locks;
readonly sid: number;
readonly connected$: BehaviorSubject<boolean>;
protected readonly pubsub: LevelLocalRepoPubSub;
protected readonly cipher?: CrudLocalRepoCipher;
protected readonly codec: JsonValueCodec;
constructor(opts: LevelLocalRepoOpts);
private _conSub;
private _stopped;
stop(): void;
protected encrypt(blob: Uint8Array, zip: boolean): Promise<Uint8Array>;
protected decrypt(blob: Uint8Array, zip: boolean): Promise<Uint8Array>;
protected encode(value: unknown, zip: boolean): Promise<Uint8Array>;
protected decode(blob: Uint8Array, zip: boolean): Promise<unknown>;
/** @todo Encrypt collection and key. */
blockKeyBase(id: BlockId): Promise<string>;
frontierKeyBase(blockKeyBase: string): string;
frontierKey(blockKeyBase: string, time: number): string;
batchKeyBase(blockKeyBase: string): string;
batchKey(blockKeyBase: string, seq: number): string;
snapshotKeyBase(blockKeyBase: string): string;
snapshotKey(blockKeyBase: string, seq: number): string;
protected syncKey(keyBase: string): string;
protected _exists(keyBase: string): Promise<boolean>;
protected _modelWrOp(keyBase: string, model: Uint8Array): Promise<BinStrLevelOperation>;
protected _metaWrOp(keyBase: string, meta?: BlockMeta): Promise<BinStrLevelOperation>;
protected _modelWrOps(keyBase: string, model: Uint8Array, meta?: BlockMeta): Promise<BinStrLevelOperation[]>;
protected _wrModel(keyBase: string, model: Uint8Array, meta?: BlockMeta): Promise<void>;
protected load(id: BlockId): Promise<{
model: Model;
cursor: number;
}>;
protected readMeta(keyBase: string): Promise<BlockMeta>;
readModel0(keyBase: string): Promise<Uint8Array>;
readModel(keyBase: string): Promise<Model>;
readFrontierBlobs0(keyBase: string): AsyncGenerator<readonly [string, Uint8Array<ArrayBufferLike>], void, unknown>;
readFrontier0(keyBase: string): Promise<Patch[]>;
readFrontierTip(keyBase: string): Promise<Patch | undefined>;
protected lockBlock<T>(keyBase: string, fn: () => Promise<T>): Promise<T>;
protected lockBlockForSync<T>(keyBase: string, fn: () => Promise<T>): Promise<T>;
protected isBlockLockedForSync(keyBase: string): boolean;
protected markDirty(keyBase: string, id: BlockId): Promise<void>;
protected markDirtyAndSync(keyBase: string, id: BlockId): Promise<boolean>;
protected remoteTimeout(): number;
/**
* Pushes to remote.
* @param id Block ID.
* @param pull Whether to pull if there are no patches to push.
*/
protected push(keyBase: string, id: BlockId, doPull?: boolean): Promise<boolean>;
/**
* Iterates over all blocks marked as dirty.
*/
protected listDirty(): AsyncIterableIterator<BlockSyncRecord>;
protected listDirtyBatch(batchSize?: number): AsyncIterableIterator<BlockSyncRecord[]>;
syncItem(keyBase: string, id: BlockId): Promise<SyncResult>;
remoteSyncAll(): Promise<SyncResult[]>;
protected _remoteSyncLoopActive: boolean;
protected _remoteSyncDelayTimer: unknown;
protected runRemoteSyncLoop(): void;
/** ----------------------------------------------------- {@link LocalRepo} */
create({ id, patches }: LocalRepoCreateRequest): Promise<LocalRepoCreateResponse>;
get({ id, remote }: LocalRepoGetRequest): Promise<LocalRepoGetResponse>;
getIf(request: LocalRepoGetIfRequest): Promise<null | LocalRepoGetIfResponse>;
sync(req: LocalRepoSyncRequest): Promise<LocalRepoSyncResponse>;
private _syncCreate;
private _syncMerge;
protected readLocal0(keyBase: string): Promise<[model: Model, cursor: LevelLocalRepoCursor]>;
private _syncRead0;
private _syncRead;
del(id: BlockId): Promise<void>;
/**
* Pull from remote.
*/
pull(id: BlockId): Promise<LocalRepoPullResponse>;
protected pullNew(id: BlockId, keyBase: string): Promise<{
model: Model;
meta: BlockMeta;
}>;
protected pullExisting(id: BlockId, keyBase: string): Promise<{
model: Model;
meta: BlockMeta;
}>;
change$(id: BlockId): Observable<LocalRepoEvent>;
private _subs;
protected _subRemote(id: BlockId): Observable<void>;
protected _onUpd(id: BlockId, batch: ServerBatch): Promise<void>;
}