json-crdt-repo
Version:
JSON CRDT server and syncing local-first browser client
211 lines (210 loc) • 7.17 kB
TypeScript
import type { Model, Patch } from 'json-joy/lib/json-crdt';
import type { Observable } from 'rxjs';
export type BlockId = [...collection: string[], id: string] | string[];
/**
* The local repo persists data on the local device. It is the primary building
* block for the local-first applications. The local repo also facilitates
* synchronization and conflict resolution between multiple tabs or processes
* running on the same device.
*/
export interface LocalRepo {
/**
* Creates a new block (document) in the local repo.
*/
create(request: LocalRepoCreateRequest): Promise<LocalRepoCreateResponse>;
/**
* Reads a block (document) from the local repo. Simply fetches the current
* state of the block stored in the repo.
*/
get(request: LocalRepoGetRequest): Promise<LocalRepoGetResponse>;
/**
* Reads a block from the local repo, if the model clock or the remote cursor
* is behind.
*/
getIf(request: LocalRepoGetIfRequest): Promise<null | LocalRepoGetIfResponse>;
/**
* Synchronizes an in-memory editing session changes to the locally stored
* data. The `sync` call is used to create, read, and update data.
*/
sync(request: LocalRepoSyncRequest): Promise<LocalRepoSyncResponse>;
/**
* Retrieves the latest state of the block from the remote.
*
* @param id Unique ID of the block.
*/
pull(id: BlockId): Promise<LocalRepoPullResponse>;
/**
* Deletes a block (document) from the local repo.
*/
del(id: BlockId): Promise<void>;
/**
* Emits an event every time a block is updated.
*
* @param id Unique ID of the block.
*/
change$(id: BlockId): Observable<LocalRepoEvent>;
}
export interface LocalRepoCreateRequest {
id: BlockId;
patches?: Patch[];
}
export interface LocalRepoCreateResponse {
/**
* Promise that resolves when the local changes have been successfully
* synchronized with the server or remote peers.
*/
remote: Promise<void>;
/**
* Model snapshot that the client should reset its "start" state to. The
* `Model` is sent when `rebase` patches are not available, or when the
* patch set is too large.
*/
model: Model;
}
/**
* A single "sync" call servers as three different operations: "create", "read",
* and "update".
*
* - When `cursor` is not set and `batch` is set, the call is equivalent to "create".
* - When `cursor` and `batch` ar both not set, the call is equivalent to "read".
* - When `cursor` is set and `batch` is set, the call is equivalent to "update".
*/
export interface LocalRepoSyncRequest {
/**
* Unique ID of the block.
*/
id: BlockId;
/**
* Logical clock time of the local operations which the client has caught up
* to.
*/
time?: number;
/**
* The last known cursor returned in the `.sync()` call response. The cursor
* should be omitted in the first `.sync()` call, and then set to the value
* returned in the previous `.sync()` call.
*/
cursor?: undefined | unknown;
/**
* List of changes that the client wants to persist.
*/
patches?: Patch[];
/**
* The session ID, which originated this sync call. The session ID is
* forwarded in events, so the client can distinguish between its own changes
* and changes made by other clients.
*/
session?: number;
}
export interface LocalRepoSyncResponse {
/**
* The last known cursor.
*/
cursor: undefined | unknown;
/**
* Model snapshot that the client should reset its "start" state to. The
* `Model` is sent when the *sync* call detects that the client is behind the
* remote or the local frontier.
*/
model?: Model;
/**
* List of patches that the client should apply to the local editing session.
*/
merge?: Patch[];
/**
* Promise that resolves when the local changes have been successfully
* synchronized with the server or remote peers.
*/
remote?: Promise<void>;
}
export interface LocalRepoPullResponse {
/**
* Cursor that the client should use in the next `.sync()` call. If the cursor
* is not set, the client should use the cursor from the previous `.sync()`
* call.
*/
cursor: undefined | unknown;
/** The latest state of the block. */
model: Model;
}
export interface LocalRepoGetRequest {
/**
* Unique ID of the block.
*/
id: BlockId;
/**
* Whether to load the block from the remote, if it does not exist locally.
* Defaults to `false`.
*/
remote?: boolean;
}
export interface LocalRepoGetResponse {
/**
* Cursor that the client should use in the next `.sync()` call. If the cursor
* is not set, the client should use the cursor from the previous `.sync()`
* call.
*/
cursor: undefined | unknown;
/** The latest state of the block. */
model: Model;
}
export interface LocalRepoGetIfRequest {
/**
* Unique ID of the block.
*/
id: BlockId;
/**
* The last known cursor returned in the `.sync()` call response.
*/
cursor?: unknown | undefined;
/**
* The last model clock time.
*/
time?: number;
}
export interface LocalRepoGetIfResponse {
model: Model;
cursor: unknown;
}
/**
* The change event. It is emitted every time a block is updated, either by
* the local client or by a remote client. It contains various types of changes
* that can be applied to the local editing session.
*/
export type LocalRepoEvent = LocalRepoMergeEvent | LocalRepoRebaseEvent | LocalRepoResetEvent | LocalRepoDeleteEvent;
export interface LocalRepoMergeEvent {
/**
* List of patches that the client should apply to the local editing session.
* They can be applied "on top" of the current editing session state, without
* the need to reset or rebase the editing session.
*/
merge: Patch[];
cursor: unknown;
}
export interface LocalRepoRebaseEvent {
/**
* List of patches that the client should rebase its editing session on top
* of. The rebase patches usually result from the changes happening in another
* local editing session, for example, another tab. This is because the tabs
* reuse the same session ID, hence, for the timestamps to be unique, the
* timestamps of the in-progress editing session are "rebased".
*
* In practice, this should almost never happen, as by the time the user
* switches tabs, the changes are already synchronized.
*/
rebase: Patch[];
session?: number;
}
export interface LocalRepoResetEvent {
/**
* The new model snapshot that the client should reset its editing session to.
* This happens when the changes are too large to be sent as patches, or when
* the changes are too many and the client should reset its editing session
* to the new state. When resetting, the client might still need to apply
* `merge` and `rebase` patches on top of the new model.
*/
reset: Model;
}
export interface LocalRepoDeleteEvent {
del: true;
}