Sequence, persist, acknowledge, retry, and conservatively rebase Sheetwrite document operations.
Sheetwrite provides transport-, database-, and authentication-neutral collaboration primitives. The host still owns server sequencing, durable document storage, user identity, authorization, and network lifecycle.
Load, mount, queue, and acknowledge
Load a validated snapshot before mounting, then let one SyncCoordinator
observe local grid transactions. Do not also save event.changes: that list is
cell-oriented rollback detail and omits document metadata operations.
Partial example
import {
createGridFromSnapshotfunction createGridFromSnapshot(host: HTMLElement, snapshot: unknown, options?: SnapshotGridOptions): GridfunctioncreateGridFromSnapshot(host:HTMLElement,snapshot:unknown,options?:SnapshotGridOptions,):GridfunctioncreateGridFromSnapshot(host:HTMLElement,snapshot:unknown,options?:SnapshotGridOptions,):GridMount a grid over a validated, non-dirty snapshot. The grid owns and disposes the hydrated store just like one created through createGrid.API reference →,
SyncCoordinatorclass SyncCoordinatorclassSyncCoordinatorclassSyncCoordinatorDeterministic, transport-neutral optimistic sync. Local rendering is never blocked: changes queue immediately, while hosts explicitly call sendNext or retry to perform network work.API reference →,
typePersistenceAdapterinterface PersistenceAdapterinterfacePersistenceAdapterinterfacePersistenceAdapterHost load and commit contract for versioned workbook persistence.API reference →,
typeRemoteOperationSourceinterface RemoteOperationSourceinterfaceRemoteOperationSourceinterfaceRemoteOperationSourceHost subscription contract for ordered versioned operations. Sources that can pause intake should await the listener promise to preserve backpressure.API reference →,
constgridconst grid: Gridconstgrid:Gridconstgrid:GridThe imperative core grid this controller owns.API reference →=createGridFromSnapshotfunction createGridFromSnapshot(host: HTMLElement, snapshot: unknown, options?: SnapshotGridOptions): GridfunctioncreateGridFromSnapshot(host:HTMLElement,snapshot:unknown,options?:SnapshotGridOptions,):GridfunctioncreateGridFromSnapshot(host:HTMLElement,snapshot:unknown,options?:SnapshotGridOptions,):GridMount a grid over a validated, non-dirty snapshot. The grid owns and disposes the hydrated store just like one created through createGrid.API reference →(documentvar document: Documentvar document:Documentvar document:Documentwindow.document returns a reference to the document contained in the window. MDN Reference.querySelectorParentNode.querySelector<HTMLElement>(selectors: string): HTMLElement | null (+4 overloads)ParentNode.querySelector<HTMLElement>(selectors: string): HTMLElement |null (+4 overloads)ParentNode.querySelector<HTMLElement>(selectors: string): HTMLElement |null (+4 overloads)Returns the first element that is a descendant of node that matches selectors. MDN Reference("#grid")!, snapshotconst snapshot: WorkbookSnapshotconstsnapshot:WorkbookSnapshotconstsnapshot:WorkbookSnapshot);
constsyncconst sync: SyncCoordinatorconstsync:SyncCoordinatorconstsync:SyncCoordinator=newSyncCoordinatornew SyncCoordinator(grid: Grid, adapter: PersistenceAdapter, options: SyncCoordinatorOptions): SyncCoordinatornewSyncCoordinator(grid: Grid, adapter: PersistenceAdapter, options: SyncCoordinatorOptions): SyncCoordinatornewSyncCoordinator(grid: Grid, adapter: PersistenceAdapter, options: SyncCoordinatorOptions): SyncCoordinatorDeterministic, transport-neutral optimistic sync. Local rendering is never blocked: changes queue immediately, while hosts explicitly call sendNext or retry to perform network work.API reference →(gridconst grid: Gridconstgrid:Gridconstgrid:GridThe imperative core grid this controller owns.API reference →, persistenceAdapterconst persistenceAdapter: PersistenceAdapterconstpersistenceAdapter:PersistenceAdapterconstpersistenceAdapter:PersistenceAdapter, {
consolevar console: Consolevar console:Consolevar console:Console.errorConsole.error(...data: any[]): void (+1 overload)Console.error(...data: any[]): void (+1 overload)Console.error(...data: any[]): void (+1 overload)Log to stderr in your terminal Appears in red@param data something to display("conflict retained for host recovery", eventevent: {
type: "conflict";
mutation: SyncMutationRecord;
response: Extract<PersistenceCommitResponse, {
status: "conflict";
}>;
}let event: {type:"conflict";mutation:SyncMutationRecord;response:Extract<PersistenceCommitResponse,{status:"conflict";}>;}let event: {type:"conflict";mutation:SyncMutationRecord;response:Extract<PersistenceCommitResponse,{status:"conflict";}>;}.mutationmutation: SyncMutationRecordlet mutation:SyncMutationRecordlet mutation:SyncMutationRecord);
}
});
awaitsyncconst sync: SyncCoordinatorconstsync:SyncCoordinatorconstsync:SyncCoordinator.readySyncCoordinator.ready(): Promise<void>SyncCoordinator.ready(): Promise<void>SyncCoordinator.ready(): Promise<void>Resolves after durable work is restored and every startup commit is visible locally.API reference →();
saveButtonconst saveButton: HTMLButtonElementconstsaveButton:HTMLButtonElementconstsaveButton:HTMLButtonElement.addEventListenerHTMLButtonElement.addEventListener<"click">(type: "click", listener: (this: HTMLButtonElement, ev: PointerEvent) => any, options?: boolean | AddEventListenerOptions): void (+1 overload)HTMLButtonElement.addEventListener<"click">(type: "click", listener: (this:HTMLButtonElement, ev:PointerEvent) => any, options?: boolean | AddEventListenerOptions): void (+1 overload)HTMLButtonElement.addEventListener<"click">(type: "click", listener: (this:HTMLButtonElement, ev:PointerEvent) => any, options?: boolean | AddEventListenerOptions): void (+1 overload)The addEventListener() method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target. MDN Reference Adds a new handler for the type event. Any given listener is added only once per type and per capture option value. If the once option is true, the listener is removed after the next time a type event is dispatched. The capture option is not used by Node.js in any functional way other than tracking registered event listeners per the EventTarget specification. Specifically, the capture option is used as part of the key when registering a listener. Any individual listener may be added once with capture = false, and once with capture = true. The addEventListener() method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target. MDN Reference Adds a new handler for the type event. Any given listener is added only once per type and per capture option value. If the once option is true, the listener is removed after the next time a type event is dispatched. The capture option is not used by Node.js in any functional way other than tracking registered event listeners per the EventTarget specification. Specifically, the capture option is used as part of the key when registering a listener. Any individual listener may be added once with capture = false, and once with capture = true.("click", () => {
voidsyncconst sync: SyncCoordinatorconstsync:SyncCoordinatorconstsync:SyncCoordinator.flushSyncCoordinator.flush(): Promise<readonly PersistenceCommitResponse[]>SyncCoordinator.flush(): Promise<readonly PersistenceCommitResponse[]>SyncCoordinator.flush(): Promise<readonly PersistenceCommitResponse[]>(); // sends in order; each response acknowledges only its mutation ID
});
// Teardown:
// unsubscribeRemote();
// sync.destroy();
// grid.destroy();
SyncCoordinator subscribes to Grid changes itself. Local rendering remains
optimistic; the host controls when to call sendNext, flush, or retry.
applied and duplicate responses acknowledge the matching operations and
remove that mutation. A transport error leaves the immutable record pending.
An HTTP adapter can stay transport-neutral at the core boundary:
Partial example
importtype {
PersistenceAdapterinterface PersistenceAdapterinterfacePersistenceAdapterinterfacePersistenceAdapterHost load and commit contract for versioned workbook persistence.API reference →,
PersistenceCommitRequestinterface PersistenceCommitRequestinterfacePersistenceCommitRequestinterfacePersistenceCommitRequestCancellable pending commit submitted to a persistence adapter.API reference →,
asyncfunctionresponseJson<T(type parameter) T in responseJson<T>(response: Response): Promise<T>(type parameter) TinresponseJson<T>(response: Response): Promise<T>(type parameter) TinresponseJson<T>(response: Response): Promise<T>>(response:Responseinterface ResponseinterfaceResponseinterfaceResponseThe Response interface of the Fetch API represents the response to a request. MDN Reference):Promiseinterface Promise<T>interfacePromise<T>interfacePromise<T>Represents the completion of an asynchronous operation<T(type parameter) T in responseJson<T>(response: Response): Promise<T>(type parameter) TinresponseJson<T>(response: Response): Promise<T>(type parameter) TinresponseJson<T>(response: Response): Promise<T>> {
if (!responseresponse: Responselet response:Responselet response:Response.okResponse.ok: booleaninterfaceResponse {ok:boolean;}interfaceResponse {ok:boolean;}The ok read-only property of the Response interface contains a Boolean stating whether the response was successful (status in the range 200-299) or not. MDN Reference) thrownewErrorvar Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+2 overloads)var Error:ErrorConstructornew (message?:string, options?:ErrorOptions) =>Error (+2 overloads)var Error:ErrorConstructornew (message?:string, options?:ErrorOptions) =>Error (+2 overloads)(`Persistence request failed: ${responseresponse: Responselet response:Responselet response:Response.statusResponse.status: numberinterfaceResponse {status:number;}interfaceResponse {status:number;}The status read-only property of the Response interface contains the HTTP status codes of the response. MDN Reference}`);
`/api/documents/${encodeURIComponentfunction encodeURIComponent(uriComponent: string | number | boolean): stringfunctionencodeURIComponent(uriComponent:string|number|boolean,):stringfunctionencodeURIComponent(uriComponent:string|number|boolean,):stringEncodes a text string as a valid component of a Uniform Resource Identifier (URI).@param uriComponent A value representing an unencoded URI component.(requestrequest: PersistenceCommitRequestlet request:PersistenceCommitRequestlet request:PersistenceCommitRequest.documentIdPendingCommit.documentId: stringinterfacePendingCommit {documentId:string;}interfacePendingCommit {documentId:string;})}/operations`,
{
methodRequestInit.method?: string | undefinedinterfaceRequestInit {method?:string|undefined;}interfaceRequestInit {method?:string|undefined;}A string to set request's method.: "POST",
headersRequestInit.headers?: HeadersInit | undefinedinterfaceRequestInit {headers?:HeadersInit|undefined;}interfaceRequestInit {headers?:HeadersInit|undefined;}A Headers object, an object literal, or an array of two-item arrays to set request's headers.: { "content-type": "application/json" },
bodyRequestInit.body?: BodyInit | null | undefinedinterfaceRequestInit {body?:BodyInit|null|undefined;}interfaceRequestInit {body?:BodyInit|null|undefined;}A BodyInit object or null to set request's body.: JSONvar JSON: JSONvarJSON:JSONvarJSON:JSONAn intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format..stringifyJSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)JSON.stringify(value: any, replacer?: (this:any, key:string, value:any) => any, space?: string | number): string (+1 overload)JSON.stringify(value: any, replacer?: (this:any, key:string, value:any) => any, space?: string | number): string (+1 overload)Converts a JavaScript value to a JavaScript Object Notation (JSON) string.@param value A JavaScript value, usually an object or array, to be converted.@param replacer A function that transforms the results.@param space Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read.@throws {TypeError} If a circular reference or a BigInt value is found.(bodyconst body: {
documentId: string;
baseVersion: number;
clientMutationId: string;
operations: readonly DocumentOp[];
}constbody: {documentId:string;baseVersion:number;clientMutationId:string;operations:readonlyDocumentOp[];}constbody: {documentId:string;baseVersion:number;clientMutationId:string;operations:readonlyDocumentOp[];}),
signalRequestInit.signal?: AbortSignal | null | undefinedinterfaceRequestInit {signal?:AbortSignal|null|undefined;}interfaceRequestInit {signal?:|AbortSignal|null|undefined;}An AbortSignal to set request's signal.API reference →,
The server must authenticate the request, authorize every operation, validate
the document schema/policy, and assign the version. Client protection metadata
is not authorization.
Database-neutral server model
A snapshot plus append-only operation log is the default persistence shape:
Illustrative excerpt
documents(
document_id primary key,
current_version integernot null,
snapshot_version integernot null,
snapshot_json jsonnot null
)
document_operations(
document_id not null,
versionintegernot null,
client_mutation_id textnot null,
operations_json jsonnot null,
created_at server_timestamp not null,
primary key(document_id, version),
unique(document_id, client_mutation_id)
)
Handle commit() in one database transaction:
Return duplicate with the existing version when
(document_id, client_mutation_id) already exists.
Lock/read the document version. If it differs from baseVersion, return
conflict with ordered operations since base or a current snapshot.
Validate and authorize the submitted DocumentOp[] on the server.
Append at current_version + 1, update the document version, and return
applied with the same mutation ID.
Periodically fold the log into snapshot_json; never rewrite operation
versions or accept client-authored server timestamps.
Normalized cell tables are an alternative for products that need SQL queries
over cell values. They do not replace the protocol: the host must still
reconstruct a complete schema-versioned WorkbookSnapshot, preserve formula
source/style/metadata, and serialize structural operations under one document
version. Mixing independent cell writes with the operation log breaks atomic
row/column/formula semantics.
Durable pending commits
SyncCoordinator can receive a PendingCommitStorage adapter. With one configured, it:
persists each immutable commit before it can be sent;
restores pending commits on startup and reapplies them locally as remote-source operations, so restoration does not echo into the outgoing queue;
retries with the original clientMutationId after reconnect or reload;
removes durable work only after an applied or duplicate acknowledgement; and
retains conflicts and storage failures for explicit host recovery.
Await coordinator.ready() before declaring startup synchronized. Use initialConnection: "offline" for offline-first startup, setOnline(false) on disconnect, and setOnline(true) to reconnect and drain pending work in order. coordinator.state distinguishes connection state from hydration, persistence, sending, conflict, and error activity.
The browser IndexedDB implementation is intentionally isolated from Node and SSR entrypoints:
Partial example
import { SyncCoordinatorclass SyncCoordinatorclassSyncCoordinatorclassSyncCoordinatorDeterministic, transport-neutral optimistic sync. Local rendering is never blocked: changes queue immediately, while hosts explicitly call sendNext or retry to perform network work.API reference → } from"@sheetwrite/core";
import { IndexedDbPendingCommitStorageclass IndexedDbPendingCommitStorageclassIndexedDbPendingCommitStorageclassIndexedDbPendingCommitStorageBrowser-only durable pending queue. Import it from @sheetwrite/core/browser; the package's root entrypoint never evaluates IndexedDB globals.API reference → } from"@sheetwrite/core/browser";
constpendingStorageconst pendingStorage: IndexedDbPendingCommitStorageconstpendingStorage:IndexedDbPendingCommitStorageconstpendingStorage:IndexedDbPendingCommitStorage=newIndexedDbPendingCommitStoragenew IndexedDbPendingCommitStorage(options?: IndexedDbPendingCommitStorageOptions): IndexedDbPendingCommitStoragenewIndexedDbPendingCommitStorage(options?: IndexedDbPendingCommitStorageOptions): IndexedDbPendingCommitStoragenewIndexedDbPendingCommitStorage(options?: IndexedDbPendingCommitStorageOptions): IndexedDbPendingCommitStorageBrowser-only durable pending queue. Import it from @sheetwrite/core/browser; the package's root entrypoint never evaluates IndexedDB globals.API reference →({
initialConnectionSyncCoordinatorOptions.initialConnection?: "online" | "offline" | undefinedinterfaceSyncCoordinatorOptions {initialConnection?:"online"|"offline"|undefined;}interfaceSyncCoordinatorOptions {initialConnection?:|"online"|"offline"|undefined;}: navigatorvar navigator: Navigatorvar navigator:Navigatorvar navigator:NavigatorThe Window.navigator read-only property returns a reference to the Navigator object, which has methods and properties about the application running the script. MDN Reference.onLineNavigatorOnLine.onLine: booleaninterfaceNavigatorOnLine {onLine:boolean;}interfaceNavigatorOnLine {onLine:boolean;}MDN Reference?"online":"offline",
});
awaitsyncconst sync: SyncCoordinatorconstsync:SyncCoordinatorconstsync:SyncCoordinator.readySyncCoordinator.ready(): Promise<void>SyncCoordinator.ready(): Promise<void>SyncCoordinator.ready(): Promise<void>Resolves after durable work is restored and every startup commit is visible locally.API reference →();
windowvar window: Window & typeof globalThisvar window:Window&typeof globalThisvar window:Window&typeof globalThisThe window property of a Window object points to the window object itself. MDN Reference.addEventListeneraddEventListener<"online">(type: "online", listener: (this: Window, ev: Event) => any, options?: boolean | AddEventListenerOptions): void (+3 overloads)addEventListener<"online">(type: "online", listener: (this:Window, ev:Event) => any, options?: boolean | AddEventListenerOptions): void (+3 overloads)addEventListener<"online">(type: "online", listener: (this:Window, ev:Event) => any, options?: boolean | AddEventListenerOptions): void (+3 overloads)The addEventListener() method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target. MDN Reference Adds a new handler for the type event. Any given listener is added only once per type and per capture option value. If the once option is true, the listener is removed after the next time a type event is dispatched. The capture option is not used by Node.js in any functional way other than tracking registered event listeners per the EventTarget specification. Specifically, the capture option is used as part of the key when registering a listener. Any individual listener may be added once with capture = false, and once with capture = true.("online", () =>syncconst sync: SyncCoordinatorconstsync:SyncCoordinatorconstsync:SyncCoordinator.setOnlineSyncCoordinator.setOnline(online: boolean): voidSyncCoordinator.setOnline(online: boolean): voidSyncCoordinator.setOnline(online: boolean): voidUpdate connectivity. Reconnection drains durable work in order; going offline aborts in-flight requests so retries retain their mutation IDs.API reference →(true));
windowvar window: Window & typeof globalThisvar window:Window&typeof globalThisvar window:Window&typeof globalThisThe window property of a Window object points to the window object itself. MDN Reference.addEventListeneraddEventListener<"offline">(type: "offline", listener: (this: Window, ev: Event) => any, options?: boolean | AddEventListenerOptions): void (+3 overloads)addEventListener<"offline">(type: "offline", listener: (this:Window, ev:Event) => any, options?: boolean | AddEventListenerOptions): void (+3 overloads)addEventListener<"offline">(type: "offline", listener: (this:Window, ev:Event) => any, options?: boolean | AddEventListenerOptions): void (+3 overloads)The addEventListener() method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target. MDN Reference Adds a new handler for the type event. Any given listener is added only once per type and per capture option value. If the once option is true, the listener is removed after the next time a type event is dispatched. The capture option is not used by Node.js in any functional way other than tracking registered event listeners per the EventTarget specification. Specifically, the capture option is used as part of the key when registering a listener. Any individual listener may be added once with capture = false, and once with capture = true.("offline", () =>syncconst sync: SyncCoordinatorconstsync:SyncCoordinatorconstsync:SyncCoordinator.setOnlineSyncCoordinator.setOnline(online: boolean): voidSyncCoordinator.setOnline(online: boolean): voidSyncCoordinator.setOnline(online: boolean): voidUpdate connectivity. Reconnection drains durable work in order; going offline aborts in-flight requests so retries retain their mutation IDs.API reference →(false));
IndexedDB record migration is versioned. Unsupported future schemas, blocked upgrades, quota exhaustion, transaction failures, and aborts surface as typed IndexedDbPendingCommitStorageError codes. Core continues to work without IndexedDB when no durable adapter is supplied.
Remote operations and version gaps
SyncCoordinator.subscribe accepts either RemoteOperationSource or AsyncIterable<VersionedOperation>. Operations apply only at serverVersion + 1; own echoed mutation IDs and already acknowledged IDs are deduplicated. Remote application uses Grid.applyRemoteOperations, so it does not create outgoing dirty work or local undo entries.
A gap emits reload-required. Prefer fetching missing ordered operations through
recoverVersionGap; the coordinator applies them in sequence. A returned
snapshot is a host remount signal, not an automatic store replacement.
resumeAfterReload(snapshot) updates retained queue base versions only after the
host has installed that snapshot and reapplied local work; it does not hydrate a
grid or attach the coordinator to a newly created grid.
Conflict and snapshot reload
Never acknowledge or discard a conflicted mutation implicitly. If the server
returns both operationsSinceBase and a current snapshot, the host can gate
every pending transaction through the conservative rebaser, clear the old
durable IDs, remount, and submit the safe results as new mutations:
Partial example
import {
createGridFromSnapshotfunction createGridFromSnapshot(host: HTMLElement, snapshot: unknown, options?: SnapshotGridOptions): GridfunctioncreateGridFromSnapshot(host:HTMLElement,snapshot:unknown,options?:SnapshotGridOptions,):GridfunctioncreateGridFromSnapshot(host:HTMLElement,snapshot:unknown,options?:SnapshotGridOptions,):GridMount a grid over a validated, non-dirty snapshot. The grid owns and disposes the hydrated store just like one created through createGrid.API reference →,
rebaseDocumentOperationsfunction rebaseDocumentOperations(localOperations: readonly DocumentOp[], remoteOperations: readonly DocumentOp[]): DocumentRebaseResultfunctionrebaseDocumentOperations(localOperations:readonlyDocumentOp[],remoteOperations:readonlyDocumentOp[],):DocumentRebaseResultfunctionrebaseDocumentOperations(localOperations:readonlyDocumentOp[],remoteOperations:readonlyDocumentOp[],):DocumentRebaseResultConservative server-ordered rebase for pending offline work. Non-overlapping literal edits are shifted across row/column insertion and deletion. Ambiguous formula, overlapping, sheet-lifecycle, and move cases become explicit conflicts instead of lossy guesses. This is the collaboration design gate; no CRDT dependency is required for the supported cases.API reference →,
SyncCoordinatorclass SyncCoordinatorclassSyncCoordinatorclassSyncCoordinatorDeterministic, transport-neutral optimistic sync. Local rendering is never blocked: changes queue immediately, while hosts explicitly call sendNext or retry to perform network work.API reference →,
typeGridinterface GridinterfaceGridinterfaceGridImperative grid handle for document commands, events, rendering, and teardown.API reference →,
typeConflictResponsetype ConflictResponse = {
status: "conflict";
currentVersion: number;
operationsSinceBase?: readonly VersionedOperation[];
snapshot?: WorkbookSnapshot;
}typeConflictResponse= {status:"conflict";currentVersion:number;operationsSinceBase?:readonlyVersionedOperation[];snapshot?:WorkbookSnapshot;};typeConflictResponse= {status:"conflict";currentVersion:number;operationsSinceBase?:readonlyVersionedOperation[];snapshot?:WorkbookSnapshot;};=Extracttype Extract<T, U> = T extends U ? T : nevertypeExtract<T, U> =TextendsU?T:never;typeExtract<T, U> =TextendsU?T:never;Extract from T those types that are assignable to U<PersistenceCommitResponsetype PersistenceCommitResponse = {
status: "applied";
version: number;
clientMutationId: string;
} | {
status: "duplicate";
version: number;
clientMutationId: string;
} | {
status: "conflict";
currentVersion: number;
operationsSinceBase?: readonly VersionedOperation[];
snapshot?: WorkbookSnapshot;
}typePersistenceCommitResponse=| {status:"applied";version:number;clientMutationId:string;}| {status:"duplicate";version:number;clientMutationId:string;}| {status:"conflict";currentVersion:number;operationsSinceBase?:readonlyVersionedOperation[];snapshot?:WorkbookSnapshot;};typePersistenceCommitResponse=| {status:"applied";version:number;clientMutationId:string;}| {status:"duplicate";version:number;clientMutationId:string;}| {status:"conflict";currentVersion:number;operationsSinceBase?:readonlyVersionedOperation[];snapshot?:WorkbookSnapshot;};Applied, duplicate, or conflict acknowledgement from persistence. applied confirms the submitted operations unchanged; normalization must conflict.API reference →, { status:"conflict" }>;
interfaceSyncSession {
gridSyncSession.grid: GridinterfaceSyncSession {grid:Grid;}interfaceSyncSession {grid:Grid;}The imperative core grid this controller owns.API reference →:Gridinterface GridinterfaceGridinterfaceGridImperative grid handle for document commands, events, rendering, and teardown.API reference →;
syncSyncSession.sync: SyncCoordinatorinterfaceSyncSession {sync:SyncCoordinator;}interfaceSyncSession {sync:SyncCoordinator;}:SyncCoordinatorclass SyncCoordinatorclassSyncCoordinatorclassSyncCoordinatorDeterministic, transport-neutral optimistic sync. Local rendering is never blocked: changes queue immediately, while hosts explicitly call sendNext or retry to perform network work.API reference →;
):Promiseinterface Promise<T>interfacePromise<T>interfacePromise<T>Represents the completion of an asynchronous operation<SyncSessioninterface SyncSessioninterfaceSyncSessioninterfaceSyncSession> {
consolevar console: Consolevar console:Consolevar console:Console.errorConsole.error(...data: any[]): void (+1 overload)Console.error(...data: any[]): void (+1 overload)Console.error(...data: any[]): void (+1 overload)Log to stderr in your terminal Appears in red@param data something to display("Manual conflict review required: server did not return an operation tail");
constremoteOperationsconst remoteOperations: DocumentOp[]constremoteOperations:DocumentOp[]constremoteOperations:DocumentOp[]=responseresponse: {
status: "conflict";
currentVersion: number;
operationsSinceBase?: readonly VersionedOperation[];
snapshot?: WorkbookSnapshot;
}let response: {status:"conflict";currentVersion:number;operationsSinceBase?:readonlyVersionedOperation[];snapshot?:WorkbookSnapshot;}let response: {status:"conflict";currentVersion:number;operationsSinceBase?:readonlyVersionedOperation[];snapshot?:WorkbookSnapshot;}.operationsSinceBaseoperationsSinceBase?: readonly VersionedOperation[]operationsSinceBase?: readonly VersionedOperation[]operationsSinceBase?: readonly VersionedOperation[].flatMapReadonlyArray<VersionedOperation>.flatMap<DocumentOp, undefined>(callback: (this: undefined, value: VersionedOperation, index: number, array: VersionedOperation[]) => DocumentOp | readonly DocumentOp[], thisArg?: undefined): DocumentOp[]ReadonlyArray<VersionedOperation>.flatMap<DocumentOp, undefined>(callback: (this:undefined, value:VersionedOperation, index:number, array:VersionedOperation[]) => DocumentOp | readonly DocumentOp[], thisArg?:undefined): DocumentOp[]ReadonlyArray<VersionedOperation>.flatMap<DocumentOp, undefined>(callback: (this:undefined, value:VersionedOperation, index:number, array:VersionedOperation[]) => DocumentOp | readonly DocumentOp[], thisArg?:undefined): DocumentOp[]Calls a defined callback function on each element of an array. Then, flattens the result into a new array. This is identical to a map followed by flat with depth 1.@param callback A function that accepts up to three arguments. The flatMap method calls the callback function one time for each element in the array.@param thisArg An object to which the this keyword can refer in the callback function. If thisArg is omitted, undefined is used as the this value.((entryentry: VersionedOperationlet entry:VersionedOperationlet entry:VersionedOperation) => [
constsafeBatchesconst safeBatches: ({
status: "rebased";
operations: readonly DocumentOp[];
} & {
status: "rebased";
})[]constsafeBatches: ({status:"rebased";operations:readonlyDocumentOp[];} & {status:"rebased";})[]constsafeBatches: ({status:"rebased";operations:readonlyDocumentOp[];} & {status:"rebased";})[]:Arrayinterface Array<T>interfaceArray<T>interfaceArray<T><ReturnTypetype ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : anytypeReturnType<Textends (...args:any) =>any> =Textends (...args:any) =>inferR?R:any;typeReturnType<Textends (...args:any) =>any,> =Textends (...args:any) =>inferR?R:any;Obtain the return type of a function type<typeofrebaseDocumentOperationsfunction rebaseDocumentOperations(localOperations: readonly DocumentOp[], remoteOperations: readonly DocumentOp[]): DocumentRebaseResultfunctionrebaseDocumentOperations(localOperations:readonlyDocumentOp[],remoteOperations:readonlyDocumentOp[],):DocumentRebaseResultfunctionrebaseDocumentOperations(localOperations:readonlyDocumentOp[],remoteOperations:readonlyDocumentOp[],):DocumentRebaseResultConservative server-ordered rebase for pending offline work. Non-overlapping literal edits are shifted across row/column insertion and deletion. Ambiguous formula, overlapping, sheet-lifecycle, and move cases become explicit conflicts instead of lossy guesses. This is the collaboration design gate; no CRDT dependency is required for the supported cases.API reference →> & {
status:"rebased";
}> = [];
for (constrecordconst record: SyncMutationRecordconstrecord:SyncMutationRecordconstrecord:SyncMutationRecordofpendingconst pending: readonly SyncMutationRecord[]constpending:readonlySyncMutationRecord[]constpending:readonlySyncMutationRecord[]) {
constresultconst result: DocumentRebaseResultconstresult:DocumentRebaseResultconstresult:DocumentRebaseResultSearch state after the replacement (matches re-scanned against the new data).API reference →=rebaseDocumentOperationsfunction rebaseDocumentOperations(localOperations: readonly DocumentOp[], remoteOperations: readonly DocumentOp[]): DocumentRebaseResultfunctionrebaseDocumentOperations(localOperations:readonlyDocumentOp[],remoteOperations:readonlyDocumentOp[],):DocumentRebaseResultfunctionrebaseDocumentOperations(localOperations:readonlyDocumentOp[],remoteOperations:readonlyDocumentOp[],):DocumentRebaseResultConservative server-ordered rebase for pending offline work. Non-overlapping literal edits are shifted across row/column insertion and deletion. Ambiguous formula, overlapping, sheet-lifecycle, and move cases become explicit conflicts instead of lossy guesses. This is the collaboration design gate; no CRDT dependency is required for the supported cases.API reference →(recordconst record: SyncMutationRecordconstrecord:SyncMutationRecordconstrecord:SyncMutationRecord.operationsPendingCommit.operations: readonly DocumentOp[]interfacePendingCommit {operations:readonlyDocumentOp[];}interfacePendingCommit {operations:readonlyDocumentOp[];}, remoteOperationsconst remoteOperations: DocumentOp[]constremoteOperations:DocumentOp[]constremoteOperations:DocumentOp[]);
if (resultconst result: DocumentRebaseResultconstresult:DocumentRebaseResultconstresult:DocumentRebaseResultSearch state after the replacement (matches re-scanned against the new data).API reference →.statusstatus: "conflict" | "rebased"let status:"conflict"|"rebased"let status:"conflict"|"rebased"==="conflict") {
consolevar console: Consolevar console:Consolevar console:Console.errorConsole.error(...data: any[]): void (+1 overload)Console.error(...data: any[]): void (+1 overload)Console.error(...data: any[]): void (+1 overload)Log to stderr in your terminal Appears in red@param data something to display("Manual conflict review required", resultconst result: {
status: "conflict";
conflict: RebaseConflict;
}constresult: {status:"conflict";conflict:RebaseConflict;}constresult: {status:"conflict";conflict:RebaseConflict;}Search state after the replacement (matches re-scanned against the new data).API reference →.conflictconflict: RebaseConflictlet conflict:RebaseConflictlet conflict:RebaseConflict);
returnsessionsession: SyncSessionlet session:SyncSessionlet session:SyncSession; // old queue and grid remain intact
}
safeBatchesconst safeBatches: ({
status: "rebased";
operations: readonly DocumentOp[];
} & {
status: "rebased";
})[]constsafeBatches: ({status:"rebased";operations:readonlyDocumentOp[];} & {status:"rebased";})[]constsafeBatches: ({status:"rebased";operations:readonlyDocumentOp[];} & {status:"rebased";})[].pushArray<{ status: "rebased"; operations: readonly DocumentOp[]; } & { status: "rebased"; }>.push(...items: ({
status: "rebased";
operations: readonly DocumentOp[];
} & {
status: "rebased";
})[]): numberArray<{ status: "rebased"; operations: readonly DocumentOp[]; } & { status: "rebased"; }>.push(...items: ({status: "rebased";operations: readonly DocumentOp[];} & {status: "rebased";})[]): numberArray<{ status: "rebased"; operations: readonly DocumentOp[]; } & { status: "rebased"; }>.push(...items: ({status: "rebased";operations: readonly DocumentOp[];} & {status: "rebased";})[]): numberAppends new elements to the end of an array, and returns the new length of the array.@param items New elements to add to the array.(resultconst result: {
status: "rebased";
operations: readonly DocumentOp[];
}constresult: {status:"rebased";operations:readonlyDocumentOp[];}constresult: {status:"rebased";operations:readonlyDocumentOp[];}Search state after the replacement (matches re-scanned against the new data).API reference →);
sessionsession: SyncSessionlet session:SyncSessionlet session:SyncSession.syncSyncSession.sync: SyncCoordinatorinterfaceSyncSession {sync:SyncCoordinator;}interfaceSyncSession {sync:SyncCoordinator;}.destroySyncCoordinator.destroy(): voidSyncCoordinator.destroy(): voidSyncCoordinator.destroy(): voidDetach every event subscription and destroy the grid. Call exactly once.API reference →();
sessionsession: SyncSessionlet session:SyncSessionlet session:SyncSession.gridSyncSession.grid: GridinterfaceSyncSession {grid:Grid;}interfaceSyncSession {grid:Grid;}The imperative core grid this controller owns.API reference →.destroyGrid.destroy(): voidGrid.destroy(): voidGrid.destroy(): voidDetach every event subscription and destroy the grid. Call exactly once.API reference →();
constgridconst grid: Gridconstgrid:Gridconstgrid:GridThe imperative core grid this controller owns.API reference →=createGridFromSnapshotfunction createGridFromSnapshot(host: HTMLElement, snapshot: unknown, options?: SnapshotGridOptions): GridfunctioncreateGridFromSnapshot(host:HTMLElement,snapshot:unknown,options?:SnapshotGridOptions,):GridfunctioncreateGridFromSnapshot(host:HTMLElement,snapshot:unknown,options?:SnapshotGridOptions,):GridMount a grid over a validated, non-dirty snapshot. The grid owns and disposes the hydrated store just like one created through createGrid.API reference →(documentvar document: Documentvar document:Documentvar document:Documentwindow.document returns a reference to the document contained in the window. MDN Reference.querySelectorParentNode.querySelector<HTMLElement>(selectors: string): HTMLElement | null (+4 overloads)ParentNode.querySelector<HTMLElement>(selectors: string): HTMLElement |null (+4 overloads)ParentNode.querySelector<HTMLElement>(selectors: string): HTMLElement |null (+4 overloads)Returns the first element that is a descendant of node that matches selectors. MDN Reference("#grid")!, latestconst latest: WorkbookSnapshotconstlatest:WorkbookSnapshotconstlatest:WorkbookSnapshot);
constsyncconst sync: SyncCoordinatorconstsync:SyncCoordinatorconstsync:SyncCoordinator=newSyncCoordinatornew SyncCoordinator(grid: Grid, adapter: PersistenceAdapter, options: SyncCoordinatorOptions): SyncCoordinatornewSyncCoordinator(grid: Grid, adapter: PersistenceAdapter, options: SyncCoordinatorOptions): SyncCoordinatornewSyncCoordinator(grid: Grid, adapter: PersistenceAdapter, options: SyncCoordinatorOptions): SyncCoordinatorDeterministic, transport-neutral optimistic sync. Local rendering is never blocked: changes queue immediately, while hosts explicitly call sendNext or retry to perform network work.API reference →(gridconst grid: Gridconstgrid:Gridconstgrid:GridThe imperative core grid this controller owns.API reference →, persistenceAdapterconst persistenceAdapter: PersistenceAdapterconstpersistenceAdapter:PersistenceAdapterconstpersistenceAdapter:PersistenceAdapter, {
awaitsyncconst sync: SyncCoordinatorconstsync:SyncCoordinatorconstsync:SyncCoordinator.readySyncCoordinator.ready(): Promise<void>SyncCoordinator.ready(): Promise<void>SyncCoordinator.ready(): Promise<void>Resolves after durable work is restored and every startup commit is visible locally.API reference →(); // the stale IDs were removed before hydration
When the rebaser reports conflict—overlap, formulas across structural changes,
concurrent moves, or sheet lifecycle ambiguity—keep the original queue and show
product-specific conflict UI. If the server returns only a snapshot, the host
cannot infer a safe transform; offer explicit discard/export/manual re-entry
instead. Reloading a snapshot over pending edits without this decision loses
intent.
Presence
PresenceCoordinator publishes actor metadata, active sheet, and bounded selection ranges through a host PresenceTransport. Presence:
is never written to document operations, snapshots, dirty state, or history;
expires according to local heartbeat receipt time;
is bounded by actor and range limits;
clamps ranges to valid visible grid geometry and hides other-sheet or filtered selections;
reuses the grid overlay element pool rather than mutating canvas cells; and
supports privacy controls for display name, selections, inbound presence, and actor filtering.
Presence is advisory UI state. Do not use it for authorization, locking, or conflict prevention.
Revisions
RevisionCoordinator delegates revision listing, loading, and restore to a RevisionAdapter. Preview snapshots pass through the optional host schema migrator and mount with readOnly: true. Restore requests include targetVersion, current baseVersion, and a stable mutation ID. The adapter contract requires restore to create a newer auditable server version; it must never rewind database state in place.
Comments
Comments are document-adjacent server entities, not fields on every cell. CommentCoordinator stores stable thread/message IDs and range or cell anchors, while adapters supply authenticated author references and server timestamps. Create, reply, and resolve requests contain no client-authored identity or time fields. Authorization remains entirely host/server-owned.
Comment streams are versioned independently. Out-of-order events emit a gap rather than being applied speculatively.
Concurrent structural editing decision
Sheetwrite uses deterministic server ordering plus conservative rebase, not a CRDT or general OT layer.
rebaseDocumentOperations safely shifts non-overlapping literal edits and references across server-ordered row/column insertions and deletions. It reports explicit conflicts for:
edits or range pastes overlapping inserted/deleted boundaries;
targets or references deleted by a concurrent operation;
formula source crossing structural edits, because text-only rewriting cannot preserve sheet-aware reference intent;
concurrent row/column moves;
conflicting sheet lifecycle operations; and
metadata families that need domain-specific transforms.
This choice preserves stable sheet IDs and authoritative formula source without lossy adapters. A CRDT/OT dependency is not justified by current scenarios: short-lived offline and online collaboration converge under server sequencing, while long-lived offline concurrent structural edits require product-level conflict UX and spreadsheet-specific formula/range semantics that generic sequence CRDTs do not supply. Revisit only if requirements demand automatic preservation of those ambiguous edits and scenario tests define the intended result for every structural/formula case.