@dabble/patches 0.7.21 → 0.7.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/algorithms/lww/consolidateOps.js +2 -0
  2. package/dist/algorithms/ot/client/applyCommittedChanges.js +1 -7
  3. package/dist/algorithms/ot/server/buildVersionState.d.ts +51 -0
  4. package/dist/algorithms/ot/server/buildVersionState.js +67 -0
  5. package/dist/algorithms/ot/server/commitChanges.d.ts +16 -14
  6. package/dist/algorithms/ot/server/commitChanges.js +58 -75
  7. package/dist/algorithms/ot/server/createVersion.d.ts +36 -7
  8. package/dist/algorithms/ot/server/createVersion.js +24 -5
  9. package/dist/algorithms/ot/server/getSnapshotAtRevision.d.ts +13 -1
  10. package/dist/algorithms/ot/server/getSnapshotAtRevision.js +20 -9
  11. package/dist/algorithms/ot/server/handleOfflineSessionsAndBatches.d.ts +11 -11
  12. package/dist/algorithms/ot/server/handleOfflineSessionsAndBatches.js +14 -52
  13. package/dist/algorithms/ot/server/transformIncomingChanges.d.ts +4 -3
  14. package/dist/algorithms/ot/server/transformIncomingChanges.js +2 -16
  15. package/dist/algorithms/ot/shared/applyChanges.js +5 -1
  16. package/dist/client/LWWInMemoryStore.js +4 -0
  17. package/dist/client/LWWIndexedDBStore.js +7 -0
  18. package/dist/client/Patches.js +5 -2
  19. package/dist/net/PatchesClient.d.ts +4 -1
  20. package/dist/net/PatchesSync.js +14 -3
  21. package/dist/net/protocol/JSONRPCServer.js +15 -0
  22. package/dist/net/protocol/types.d.ts +5 -2
  23. package/dist/server/CompressedStoreBackend.d.ts +4 -3
  24. package/dist/server/CompressedStoreBackend.js +8 -13
  25. package/dist/server/LWWBranchManager.js +12 -3
  26. package/dist/server/LWWMemoryStoreBackend.d.ts +5 -8
  27. package/dist/server/LWWMemoryStoreBackend.js +11 -6
  28. package/dist/server/LWWServer.d.ts +16 -15
  29. package/dist/server/LWWServer.js +28 -27
  30. package/dist/server/OTBranchManager.js +9 -6
  31. package/dist/server/OTServer.d.ts +23 -19
  32. package/dist/server/OTServer.js +31 -27
  33. package/dist/server/PatchesHistoryManager.d.ts +23 -4
  34. package/dist/server/PatchesHistoryManager.js +40 -4
  35. package/dist/server/PatchesServer.d.ts +25 -9
  36. package/dist/server/RevConflictError.d.ts +13 -0
  37. package/dist/server/RevConflictError.js +10 -0
  38. package/dist/server/index.d.ts +5 -1
  39. package/dist/server/index.js +15 -0
  40. package/dist/server/jsonReadable.d.ts +22 -0
  41. package/dist/server/jsonReadable.js +68 -0
  42. package/dist/server/types.d.ts +30 -16
  43. package/package.json +1 -1
@@ -1,22 +1,17 @@
1
1
  import "../../../chunk-IZ2YBCUP.js";
2
- import { createVersionMetadata } from "../../../data/version.js";
3
- import { applyChanges } from "../shared/applyChanges.js";
4
- import { breakChanges } from "../shared/changeBatching.js";
5
- import { getStateAtRevision } from "./getStateAtRevision.js";
6
- async function handleOfflineSessionsAndBatches(store, sessionTimeoutMillis, docId, changes, baseRev, batchId, origin = "offline-branch", maxStorageBytes) {
7
- const [lastVersion] = await store.listVersions(docId, {
8
- groupId: batchId,
9
- reverse: true,
10
- limit: 1
11
- });
12
- let offlineBaseState;
2
+ import { createVersion } from "./createVersion.js";
3
+ async function handleOfflineSessionsAndBatches(store, sessionTimeoutMillis, docId, changes, origin = "offline-branch") {
13
4
  let parentId;
14
- if (lastVersion) {
15
- const vs = await store.loadVersionState(docId, lastVersion.id);
16
- offlineBaseState = vs.state ?? vs;
17
- parentId = lastVersion.id;
18
- } else {
19
- offlineBaseState = (await getStateAtRevision(store, docId, baseRev)).state;
5
+ if (origin === "offline-branch" && changes.length > 0) {
6
+ const firstRev = changes[0].rev;
7
+ const mainVersions = await store.listVersions(docId, {
8
+ limit: 1,
9
+ reverse: true,
10
+ startAfter: firstRev,
11
+ origin: "main",
12
+ orderBy: "endRev"
13
+ });
14
+ parentId = mainVersions[0]?.id;
20
15
  }
21
16
  let sessionStartIndex = 0;
22
17
  for (let i = 1; i <= changes.length; i++) {
@@ -25,45 +20,12 @@ async function handleOfflineSessionsAndBatches(store, sessionTimeoutMillis, docI
25
20
  if (timeDiff > sessionTimeoutMillis || isLastChange) {
26
21
  const sessionChanges = changes.slice(sessionStartIndex, i);
27
22
  if (sessionChanges.length > 0) {
28
- const isContinuation = !!lastVersion && sessionChanges[0].createdAt - lastVersion.endedAt <= sessionTimeoutMillis;
29
- if (isContinuation && store.appendVersionChanges) {
30
- const mergedState = applyChanges(offlineBaseState, sessionChanges);
31
- const newEndedAt = sessionChanges[sessionChanges.length - 1].createdAt;
32
- const newRev = sessionChanges[sessionChanges.length - 1].rev;
33
- await store.appendVersionChanges(docId, lastVersion.id, sessionChanges, newEndedAt, newRev, mergedState);
34
- offlineBaseState = mergedState;
35
- parentId = lastVersion.parentId;
36
- } else {
37
- offlineBaseState = applyChanges(offlineBaseState, sessionChanges);
38
- const isOffline = sessionChanges[0].committedAt - sessionChanges[0].createdAt > sessionTimeoutMillis;
39
- const sessionMetadata = createVersionMetadata({
40
- parentId,
41
- groupId: batchId,
42
- origin,
43
- ...isOffline ? { isOffline } : {},
44
- startedAt: sessionChanges[0].createdAt,
45
- endedAt: sessionChanges[sessionChanges.length - 1].createdAt,
46
- endRev: sessionChanges[sessionChanges.length - 1].rev,
47
- startRev: sessionChanges[0].rev
48
- });
49
- await store.createVersion(docId, sessionMetadata, offlineBaseState, sessionChanges);
50
- parentId = sessionMetadata.id;
51
- }
23
+ const version = await createVersion(store, docId, sessionChanges, { origin, parentId });
24
+ parentId = version?.id;
52
25
  sessionStartIndex = i;
53
26
  }
54
27
  }
55
28
  }
56
- if (origin === "offline-branch") {
57
- const collapsed = changes.reduce((firstChange, nextChange) => {
58
- firstChange.ops = [...firstChange.ops, ...nextChange.ops];
59
- return firstChange;
60
- });
61
- if (maxStorageBytes) {
62
- return breakChanges([collapsed], maxStorageBytes);
63
- }
64
- return [collapsed];
65
- }
66
- return changes;
67
29
  }
68
30
  export {
69
31
  handleOfflineSessionsAndBatches
@@ -5,14 +5,15 @@ import '../../../json-patch/types.js';
5
5
 
6
6
  /**
7
7
  * Transforms incoming changes against committed changes that happened *after* the client's baseRev.
8
- * The state used for transformation should be the server state *at the client's baseRev*.
8
+ * Stateless: passes null to transformPatch (no server state needed for transformation).
9
+ * Bad transformations produce empty ops and are filtered as noops.
10
+ *
9
11
  * @param changes The incoming changes.
10
- * @param stateAtBaseRev The server state *at the client's baseRev*.
11
12
  * @param committedChanges The committed changes that happened *after* the client's baseRev.
12
13
  * @param currentRev The current/latest revision number (these changes will have their `rev` set > `currentRev`).
13
14
  * @param forceCommit If true, skip filtering of no-op changes (useful for migrations).
14
15
  * @returns The transformed changes.
15
16
  */
16
- declare function transformIncomingChanges(changes: Change[], stateAtBaseRev: any, committedChanges: Change[], currentRev: number, forceCommit?: boolean): Change[];
17
+ declare function transformIncomingChanges(changes: Change[], committedChanges: Change[], currentRev: number, forceCommit?: boolean): Change[];
17
18
 
18
19
  export { transformIncomingChanges };
@@ -1,27 +1,13 @@
1
1
  import "../../../chunk-IZ2YBCUP.js";
2
- import { applyPatch } from "../../../json-patch/applyPatch.js";
3
2
  import { transformPatch } from "../../../json-patch/transformPatch.js";
4
- function transformIncomingChanges(changes, stateAtBaseRev, committedChanges, currentRev, forceCommit = false) {
3
+ function transformIncomingChanges(changes, committedChanges, currentRev, forceCommit = false) {
5
4
  const committedOps = committedChanges.flatMap((c) => c.ops);
6
- let state = stateAtBaseRev;
7
5
  let rev = currentRev + 1;
8
6
  return changes.map((change) => {
9
- const transformedOps = transformPatch(stateAtBaseRev, committedOps, change.ops);
7
+ const transformedOps = transformPatch(null, committedOps, change.ops);
10
8
  if (transformedOps.length === 0 && !forceCommit) {
11
9
  return null;
12
10
  }
13
- if (transformedOps.length > 0) {
14
- try {
15
- const previous = state;
16
- state = applyPatch(state, transformedOps, { strict: true });
17
- if (previous === state && !forceCommit) {
18
- return null;
19
- }
20
- } catch (error) {
21
- console.error(`Error applying change ${change.id} to state:`, error);
22
- return null;
23
- }
24
- }
25
11
  return { ...change, rev: rev++, ops: transformedOps };
26
12
  }).filter(Boolean);
27
13
  }
@@ -3,7 +3,11 @@ import { applyPatch } from "../../../json-patch/applyPatch.js";
3
3
  function applyChanges(state, changes) {
4
4
  if (!changes.length) return state;
5
5
  for (const change of changes) {
6
- state = applyPatch(state, change.ops, { strict: true });
6
+ try {
7
+ state = applyPatch(state, change.ops, { strict: true });
8
+ } catch (e) {
9
+ console.error(`applyChanges: skipping bad change ${change.id} (rev ${change.rev}):`, e);
10
+ }
7
11
  }
8
12
  return state;
9
13
  }
@@ -158,6 +158,10 @@ class LWWInMemoryStore {
158
158
  for (const op of buf.sendingChange.ops) {
159
159
  buf.committedFields.set(op.path, op.value);
160
160
  }
161
+ const changeRev = buf.sendingChange.rev;
162
+ if (changeRev !== void 0 && changeRev > buf.committedRev) {
163
+ buf.committedRev = changeRev;
164
+ }
161
165
  buf.sendingChange = null;
162
166
  }
163
167
  /**
@@ -243,6 +243,13 @@ class LWWIndexedDBStore {
243
243
  return;
244
244
  }
245
245
  await Promise.all(sending.change.ops.map((op) => committedOps.put({ ...op, docId })));
246
+ const changeRev = sending.change.rev;
247
+ if (changeRev !== void 0) {
248
+ const docMeta = await docsStore.get(docId) ?? { docId, committedRev: 0, algorithm: "lww" };
249
+ if (changeRev > docMeta.committedRev) {
250
+ await docsStore.put({ ...docMeta, committedRev: changeRev });
251
+ }
252
+ }
246
253
  await sendingChanges.delete(docId);
247
254
  await tx.complete();
248
255
  }
@@ -206,8 +206,11 @@ class Patches {
206
206
  _handleDocChange(docId, ops, doc, algorithm, metadata) {
207
207
  const prev = this._changeQueues.get(docId) ?? Promise.resolve();
208
208
  const current = prev.then(() => this._processDocChange(docId, ops, doc, algorithm, metadata));
209
- this._changeQueues.set(docId, current.catch(() => {
210
- }));
209
+ this._changeQueues.set(
210
+ docId,
211
+ current.catch(() => {
212
+ })
213
+ );
211
214
  return current;
212
215
  }
213
216
  async _processDocChange(docId, ops, doc, algorithm, metadata) {
@@ -57,7 +57,10 @@ declare class PatchesClient implements PatchesAPI {
57
57
  * @param options - Optional commit settings (e.g., forceCommit for migrations).
58
58
  * @returns A promise resolving with the changes as committed by the server (potentially transformed).
59
59
  */
60
- commitChanges(docId: string, changes: ChangeInput[], options?: CommitChangesOptions): Promise<Change[]>;
60
+ commitChanges(docId: string, changes: ChangeInput[], options?: CommitChangesOptions): Promise<{
61
+ changes: Change[];
62
+ docReloadRequired?: true;
63
+ }>;
61
64
  /**
62
65
  * Deletes a document on the server.
63
66
  * @param docId - The ID of the document to delete.
@@ -286,9 +286,20 @@ class PatchesSync extends (_a = ReadonlyStoreClass, _syncDoc_dec = [blockable],
286
286
  if (!this.state.connected) {
287
287
  throw new Error("Disconnected during flush");
288
288
  }
289
- const committed = await this.ws.commitChanges(docId, changeBatch);
290
- await this._applyServerChangesToDoc(docId, committed);
291
- await algorithm.confirmSent(docId, changeBatch);
289
+ const { changes: committed, docReloadRequired } = await this.ws.commitChanges(docId, changeBatch);
290
+ if (docReloadRequired) {
291
+ await algorithm.confirmSent(docId, changeBatch);
292
+ const snapshot = await this.ws.getDoc(docId);
293
+ await algorithm.store.saveDoc(docId, snapshot);
294
+ this._updateDocSyncState(docId, { committedRev: snapshot.rev });
295
+ const openDoc = this.patches.getOpenDoc(docId);
296
+ if (openDoc) {
297
+ openDoc.import({ ...snapshot, changes: [] });
298
+ }
299
+ } else {
300
+ await this._applyServerChangesToDoc(docId, committed);
301
+ await algorithm.confirmSent(docId, changeBatch);
302
+ }
292
303
  pending = await algorithm.getPendingToSend(docId) ?? [];
293
304
  }
294
305
  const stillHasPending = await algorithm.hasPending(docId);
@@ -111,6 +111,21 @@ class JSONRPCServer {
111
111
  if ("id" in message && message.id !== void 0) {
112
112
  try {
113
113
  const result = await this._dispatch(message.method, message.params, ctx);
114
+ if (result && typeof result === "object" && typeof result.getReader === "function") {
115
+ const reader = result.getReader();
116
+ const chunks = [];
117
+ for (; ; ) {
118
+ const { done, value } = await reader.read();
119
+ if (done) break;
120
+ chunks.push(value);
121
+ }
122
+ const json = chunks.join("");
123
+ if (typeof raw === "string") {
124
+ return `{"jsonrpc":"2.0","id":${JSON.stringify(message.id)},"result":${json}}`;
125
+ } else {
126
+ return { jsonrpc: "2.0", id: message.id, result: JSON.parse(json) };
127
+ }
128
+ }
114
129
  const response = rpcResponse(result, message.id);
115
130
  return respond(response);
116
131
  } catch (err) {
@@ -122,8 +122,11 @@ interface PatchesAPI {
122
122
  getDoc(docId: string): Promise<PatchesState>;
123
123
  /** Get changes that occurred after a specific revision. */
124
124
  getChangesSince(docId: string, rev: number): Promise<Change[]>;
125
- /** Apply a set of changes from the client to a document. Returns the committed changes. */
126
- commitChanges(docId: string, changes: ChangeInput[], options?: CommitChangesOptions): Promise<Change[]>;
125
+ /** Apply a set of changes from the client to a document. Returns the committed changes and an optional reload flag. */
126
+ commitChanges(docId: string, changes: ChangeInput[], options?: CommitChangesOptions): Promise<{
127
+ changes: Change[];
128
+ docReloadRequired?: true;
129
+ }>;
127
130
  /** Delete a document. */
128
131
  deleteDoc(docId: string): Promise<void>;
129
132
  /** Create a new named version snapshot of a document's current state. */
@@ -34,12 +34,13 @@ declare class CompressedStoreBackend implements OTStoreBackend, Partial<Tombston
34
34
  private decompressChange;
35
35
  saveChanges(docId: string, changes: Change[]): Promise<void>;
36
36
  listChanges(docId: string, options: ListChangesOptions): Promise<Change[]>;
37
- createVersion(docId: string, metadata: VersionMetadata, state: any, changes?: Change[]): Promise<void>;
38
- appendVersionChanges(docId: string, versionId: string, changes: Change[], newEndedAt: number, newRev: number, newState: any): Promise<void>;
37
+ createVersion(docId: string, metadata: VersionMetadata, changes?: Change[]): Promise<void>;
39
38
  loadVersionChanges(docId: string, versionId: string): Promise<Change[]>;
39
+ getCurrentRev(docId: string): Promise<number>;
40
40
  updateVersion(docId: string, versionId: string, metadata: EditableVersionMetadata): Promise<void>;
41
41
  listVersions(docId: string, options: ListVersionsOptions): Promise<VersionMetadata[]>;
42
- loadVersionState(docId: string, versionId: string): Promise<any | undefined>;
42
+ loadVersion(docId: string, versionId: string): Promise<VersionMetadata | undefined>;
43
+ loadVersionState(docId: string, versionId: string): Promise<string | ReadableStream<string> | undefined>;
43
44
  deleteDoc(docId: string): Promise<void>;
44
45
  createTombstone(tombstone: DocumentTombstone): Promise<void>;
45
46
  getTombstone(docId: string): Promise<DocumentTombstone | undefined>;
@@ -35,32 +35,27 @@ class CompressedStoreBackend {
35
35
  return stored.map((s) => this.decompressChange(s));
36
36
  }
37
37
  // === Version Operations (compress changes and state ops) ===
38
- async createVersion(docId, metadata, state, changes) {
38
+ async createVersion(docId, metadata, changes) {
39
39
  const compressedChanges = changes?.map((c) => this.compressChange(c));
40
- return this.store.createVersion(docId, metadata, state, compressedChanges);
41
- }
42
- async appendVersionChanges(docId, versionId, changes, newEndedAt, newRev, newState) {
43
- const compressedChanges = changes.map((c) => this.compressChange(c));
44
- return this.store.appendVersionChanges?.(
45
- docId,
46
- versionId,
47
- compressedChanges,
48
- newEndedAt,
49
- newRev,
50
- newState
51
- );
40
+ return this.store.createVersion(docId, metadata, compressedChanges);
52
41
  }
53
42
  async loadVersionChanges(docId, versionId) {
54
43
  const stored = await this.store.loadVersionChanges?.(docId, versionId);
55
44
  return stored?.map((s) => this.decompressChange(s)) ?? [];
56
45
  }
57
46
  // === Pass-through Operations (no compression needed) ===
47
+ async getCurrentRev(docId) {
48
+ return this.store.getCurrentRev(docId);
49
+ }
58
50
  async updateVersion(docId, versionId, metadata) {
59
51
  return this.store.updateVersion(docId, versionId, metadata);
60
52
  }
61
53
  async listVersions(docId, options) {
62
54
  return this.store.listVersions(docId, options);
63
55
  }
56
+ async loadVersion(docId, versionId) {
57
+ return this.store.loadVersion(docId, versionId);
58
+ }
64
59
  async loadVersionState(docId, versionId) {
65
60
  return this.store.loadVersionState(docId, versionId);
66
61
  }
@@ -1,4 +1,5 @@
1
1
  import "../chunk-IZ2YBCUP.js";
2
+ import { JSONPatch } from "../json-patch/JSONPatch.js";
2
3
  import {
3
4
  assertBranchMetadata,
4
5
  assertBranchOpenForMerge,
@@ -8,6 +9,7 @@ import {
8
9
  generateBranchId,
9
10
  wrapMergeCommit
10
11
  } from "./branchUtils.js";
12
+ import { readStreamAsString } from "./jsonReadable.js";
11
13
  class LWWBranchManager {
12
14
  constructor(store, lwwServer) {
13
15
  this.store = store;
@@ -36,10 +38,17 @@ class LWWBranchManager {
36
38
  */
37
39
  async createBranch(docId, atPoint, metadata) {
38
40
  await assertNotABranch(this.store, docId);
39
- const doc = await this.lwwServer.getDoc(docId);
41
+ const snapshot = await this.store.getSnapshot(docId);
42
+ const baseRev = snapshot?.rev ?? 0;
43
+ let state = snapshot ? JSON.parse(await readStreamAsString(snapshot.state)) : {};
40
44
  const ops = await this.store.listOps(docId);
45
+ const opsAfterSnapshot = ops.filter((op) => (op.rev ?? 0) > baseRev);
46
+ if (opsAfterSnapshot.length > 0) {
47
+ state = new JSONPatch(opsAfterSnapshot).apply(state);
48
+ }
49
+ const rev = ops.length > 0 ? Math.max(baseRev, ...ops.map((op) => op.rev ?? 0)) : baseRev;
41
50
  const branchDocId = await generateBranchId(this.store, docId);
42
- await this.store.saveSnapshot(branchDocId, doc.state, doc.rev);
51
+ await this.store.saveSnapshot(branchDocId, state, rev);
43
52
  if (ops.length > 0) {
44
53
  await this.store.saveOps(branchDocId, ops);
45
54
  }
@@ -85,7 +94,7 @@ class LWWBranchManager {
85
94
  await this.closeBranch(branchId, "merged");
86
95
  return [];
87
96
  }
88
- const committedChanges = await wrapMergeCommit(
97
+ const { changes: committedChanges } = await wrapMergeCommit(
89
98
  branchId,
90
99
  sourceDocId,
91
100
  () => this.lwwServer.commitChanges(sourceDocId, branchChanges)
@@ -1,6 +1,6 @@
1
1
  import { JSONPatchOp } from '../json-patch/types.js';
2
2
  import { DocumentTombstone, VersionMetadata, Change, ListVersionsOptions, EditableVersionMetadata, Branch } from '../types.js';
3
- import { LWWStoreBackend, VersioningStoreBackend, TombstoneStoreBackend, BranchingStoreBackend, ListFieldsOptions } from './types.js';
3
+ import { LWWStoreBackend, VersioningStoreBackend, TombstoneStoreBackend, BranchingStoreBackend, SnapshotResult, ListFieldsOptions } from './types.js';
4
4
  import '../json-patch/JSONPatch.js';
5
5
  import '@dabble/delta';
6
6
 
@@ -14,7 +14,6 @@ interface DocData {
14
14
  }
15
15
  interface VersionData {
16
16
  metadata: VersionMetadata;
17
- state: any;
18
17
  }
19
18
  /**
20
19
  * In-memory implementation of LWWStoreBackend for testing.
@@ -37,10 +36,7 @@ declare class LWWMemoryStoreBackend implements LWWStoreBackend, VersioningStoreB
37
36
  private branches;
38
37
  private getOrCreateDoc;
39
38
  getCurrentRev(docId: string): Promise<number>;
40
- getSnapshot(docId: string): Promise<{
41
- state: any;
42
- rev: number;
43
- } | null>;
39
+ getSnapshot(docId: string): Promise<SnapshotResult | null>;
44
40
  saveSnapshot(docId: string, state: any, rev: number): Promise<void>;
45
41
  saveOps(docId: string, newOps: JSONPatchOp[], pathsToDelete?: string[]): Promise<number>;
46
42
  listOps(docId: string, options?: ListFieldsOptions): Promise<JSONPatchOp[]>;
@@ -48,9 +44,10 @@ declare class LWWMemoryStoreBackend implements LWWStoreBackend, VersioningStoreB
48
44
  createTombstone(tombstone: DocumentTombstone): Promise<void>;
49
45
  getTombstone(docId: string): Promise<DocumentTombstone | undefined>;
50
46
  removeTombstone(docId: string): Promise<void>;
51
- createVersion(docId: string, metadata: VersionMetadata, state: any, _changes?: Change[]): Promise<void>;
47
+ createVersion(docId: string, metadata: VersionMetadata, _changes?: Change[]): Promise<void>;
52
48
  listVersions(docId: string, options?: ListVersionsOptions): Promise<VersionMetadata[]>;
53
- loadVersionState(docId: string, versionId: string): Promise<any | undefined>;
49
+ loadVersion(docId: string, versionId: string): Promise<VersionMetadata | undefined>;
50
+ loadVersionState(_docId: string, _versionId: string): Promise<string | undefined>;
54
51
  updateVersion(docId: string, versionId: string, metadata: EditableVersionMetadata): Promise<void>;
55
52
  listBranches(docId: string): Promise<Branch[]>;
56
53
  loadBranch(branchId: string): Promise<Branch | null>;
@@ -1,4 +1,5 @@
1
1
  import "../chunk-IZ2YBCUP.js";
2
+ import { jsonReadable } from "./jsonReadable.js";
2
3
  class LWWMemoryStoreBackend {
3
4
  docs = /* @__PURE__ */ new Map();
4
5
  tombstones = /* @__PURE__ */ new Map();
@@ -18,7 +19,9 @@ class LWWMemoryStoreBackend {
18
19
  }
19
20
  // === Snapshot ===
20
21
  async getSnapshot(docId) {
21
- return this.docs.get(docId)?.snapshot ?? null;
22
+ const snapshot = this.docs.get(docId)?.snapshot;
23
+ if (!snapshot) return null;
24
+ return { rev: snapshot.rev, state: jsonReadable(JSON.stringify(snapshot.state)) };
22
25
  }
23
26
  async saveSnapshot(docId, state, rev) {
24
27
  const doc = this.getOrCreateDoc(docId);
@@ -74,9 +77,9 @@ class LWWMemoryStoreBackend {
74
77
  this.tombstones.delete(docId);
75
78
  }
76
79
  // === Versioning ===
77
- async createVersion(docId, metadata, state, _changes) {
80
+ async createVersion(docId, metadata, _changes) {
78
81
  const versions = this.versions.get(docId) || [];
79
- versions.push({ metadata, state });
82
+ versions.push({ metadata });
80
83
  this.versions.set(docId, versions);
81
84
  }
82
85
  async listVersions(docId, options) {
@@ -111,10 +114,12 @@ class LWWMemoryStoreBackend {
111
114
  }
112
115
  return result;
113
116
  }
114
- async loadVersionState(docId, versionId) {
117
+ async loadVersion(docId, versionId) {
115
118
  const versions = this.versions.get(docId) || [];
116
- const version = versions.find((v) => v.metadata.id === versionId);
117
- return version?.state;
119
+ return versions.find((v) => v.metadata.id === versionId)?.metadata;
120
+ }
121
+ async loadVersionState(_docId, _versionId) {
122
+ return void 0;
118
123
  }
119
124
  async updateVersion(docId, versionId, metadata) {
120
125
  const versions = this.versions.get(docId) || [];
@@ -1,6 +1,6 @@
1
1
  import * as easy_signal from 'easy-signal';
2
2
  import { ApiDefinition } from '../net/protocol/JSONRPCServer.js';
3
- import { Change, CommitChangesOptions, DeleteDocOptions, PatchesState, ChangeInput, ChangeMutator, EditableVersionMetadata } from '../types.js';
3
+ import { Change, CommitChangesOptions, DeleteDocOptions, ChangeInput, ChangeMutator, EditableVersionMetadata } from '../types.js';
4
4
  import { PatchesServer } from './PatchesServer.js';
5
5
  import { LWWStoreBackend } from './types.js';
6
6
  import '../net/websocket/AuthorizationProvider.js';
@@ -13,11 +13,6 @@ import '../net/protocol/types.js';
13
13
  * Configuration options for LWWServer.
14
14
  */
15
15
  interface LWWServerOptions {
16
- /**
17
- * Number of revisions between automatic snapshots.
18
- * Defaults to 200.
19
- */
20
- snapshotInterval?: number;
21
16
  }
22
17
  /**
23
18
  * Last-Write-Wins (LWW) server implementation.
@@ -53,27 +48,27 @@ declare class LWWServer implements PatchesServer {
53
48
  */
54
49
  static api: ApiDefinition;
55
50
  readonly store: LWWStoreBackend;
56
- private readonly snapshotInterval;
57
51
  /** Notifies listeners whenever a batch of changes is successfully committed. */
58
52
  readonly onChangesCommitted: easy_signal.Signal<(docId: string, changes: Change[], options?: CommitChangesOptions, originClientId?: string) => void>;
59
53
  /** Notifies listeners when a document is deleted. */
60
54
  readonly onDocDeleted: easy_signal.Signal<(docId: string, options?: DeleteDocOptions, originClientId?: string) => void>;
61
- constructor(store: LWWStoreBackend, options?: LWWServerOptions);
55
+ constructor(store: LWWStoreBackend, _options?: LWWServerOptions);
62
56
  /**
63
- * Get the current state of a document.
64
- * Reconstructs state from snapshot + ops changed since snapshot.
57
+ * Get the current state of a document as a ReadableStream of JSON.
58
+ * Streams `{"state":...,"rev":N,"changes":[...]}` with the snapshot state
59
+ * flowing through without parsing.
65
60
  *
66
61
  * @param docId - The document ID.
67
- * @returns The document state and revision, or `{ state: {}, rev: 0 }` if not found.
62
+ * @returns A ReadableStream of JSON string chunks.
68
63
  */
69
- getDoc(docId: string): Promise<PatchesState>;
64
+ getDoc(docId: string): Promise<ReadableStream<string>>;
70
65
  /**
71
66
  * Get changes that occurred after a specific revision.
72
67
  * LWW doesn't store changes, so this synthesizes a change from ops.
73
68
  *
74
69
  * @param docId - The document ID.
75
70
  * @param rev - The revision number to get changes after.
76
- * @returns Array containing 0 or 1 synthesized changes.
71
+ * @returns Array of synthesized changes (0 or 1 elements).
77
72
  */
78
73
  getChangesSince(docId: string, rev: number): Promise<Change[]>;
79
74
  /**
@@ -87,9 +82,12 @@ declare class LWWServer implements PatchesServer {
87
82
  * @param docId - The document ID.
88
83
  * @param changes - The changes to commit (always 1 for LWW).
89
84
  * @param options - Optional commit options (ignored for LWW).
90
- * @returns Array containing 0-1 changes with catchup ops and new rev.
85
+ * @returns An object with the committed changes (0-1 elements).
91
86
  */
92
- commitChanges(docId: string, changes: ChangeInput[], options?: CommitChangesOptions): Promise<Change[]>;
87
+ commitChanges(docId: string, changes: ChangeInput[], options?: CommitChangesOptions): Promise<{
88
+ changes: Change[];
89
+ docReloadRequired?: true;
90
+ }>;
93
91
  /**
94
92
  * Delete a document and emit deletion signal.
95
93
  * Creates a tombstone if the store supports it.
@@ -106,6 +104,7 @@ declare class LWWServer implements PatchesServer {
106
104
  undeleteDoc(docId: string): Promise<boolean>;
107
105
  /**
108
106
  * Make a server-side change to a document.
107
+ * Stateless — uses getCurrentRev instead of loading document state.
109
108
  * @param docId - The document ID.
110
109
  * @param mutator - A function that receives a JSONPatch and PathProxy to define the changes.
111
110
  * @param metadata - Optional metadata for the change.
@@ -115,6 +114,8 @@ declare class LWWServer implements PatchesServer {
115
114
  /**
116
115
  * Captures the current state of a document as a new version.
117
116
  * Only works if store implements VersioningStoreBackend.
117
+ * Does NOT build state — creates version metadata, then emits `onVersionCreated`
118
+ * so subscribers can build and persist state out of band.
118
119
  *
119
120
  * @param docId - The document ID.
120
121
  * @param metadata - Optional metadata for the version.