@dabble/patches 0.7.22 → 0.7.24
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.
- package/dist/{BaseDoc-CD5wZQMm.d.ts → BaseDoc-BT18xPxU.d.ts} +1 -1
- package/dist/algorithms/lww/consolidateOps.js +2 -0
- package/dist/algorithms/ot/client/applyCommittedChanges.js +1 -7
- package/dist/algorithms/ot/server/buildVersionState.d.ts +51 -0
- package/dist/algorithms/ot/server/buildVersionState.js +67 -0
- package/dist/algorithms/ot/server/commitChanges.d.ts +16 -14
- package/dist/algorithms/ot/server/commitChanges.js +58 -75
- package/dist/algorithms/ot/server/createVersion.d.ts +36 -7
- package/dist/algorithms/ot/server/createVersion.js +24 -5
- package/dist/algorithms/ot/server/getSnapshotAtRevision.d.ts +13 -1
- package/dist/algorithms/ot/server/getSnapshotAtRevision.js +20 -9
- package/dist/algorithms/ot/server/handleOfflineSessionsAndBatches.d.ts +11 -11
- package/dist/algorithms/ot/server/handleOfflineSessionsAndBatches.js +14 -52
- package/dist/algorithms/ot/server/transformIncomingChanges.d.ts +4 -3
- package/dist/algorithms/ot/server/transformIncomingChanges.js +2 -16
- package/dist/algorithms/ot/shared/applyChanges.js +5 -1
- package/dist/algorithms/ot/shared/changeBatching.js +3 -3
- package/dist/client/BaseDoc.d.ts +1 -1
- package/dist/client/ClientAlgorithm.d.ts +1 -1
- package/dist/client/LWWAlgorithm.d.ts +1 -1
- package/dist/client/LWWDoc.d.ts +1 -1
- package/dist/client/LWWInMemoryStore.js +4 -0
- package/dist/client/LWWIndexedDBStore.js +7 -0
- package/dist/client/OTAlgorithm.d.ts +1 -1
- package/dist/client/OTDoc.d.ts +1 -1
- package/dist/client/Patches.d.ts +1 -1
- package/dist/client/Patches.js +6 -2
- package/dist/client/PatchesDoc.d.ts +1 -1
- package/dist/client/factories.d.ts +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/compression/lz.js +3 -4
- package/dist/index.d.ts +1 -1
- package/dist/json-patch/invertPatch.js +2 -1
- package/dist/net/PatchesClient.d.ts +4 -1
- package/dist/net/PatchesSync.d.ts +1 -1
- package/dist/net/PatchesSync.js +14 -3
- package/dist/net/index.d.ts +1 -1
- package/dist/net/protocol/JSONRPCServer.js +15 -0
- package/dist/net/protocol/types.d.ts +5 -2
- package/dist/server/CompressedStoreBackend.d.ts +4 -3
- package/dist/server/CompressedStoreBackend.js +8 -13
- package/dist/server/LWWBranchManager.js +12 -3
- package/dist/server/LWWMemoryStoreBackend.d.ts +5 -8
- package/dist/server/LWWMemoryStoreBackend.js +11 -6
- package/dist/server/LWWServer.d.ts +16 -15
- package/dist/server/LWWServer.js +28 -27
- package/dist/server/OTBranchManager.js +9 -6
- package/dist/server/OTServer.d.ts +23 -19
- package/dist/server/OTServer.js +31 -27
- package/dist/server/PatchesHistoryManager.d.ts +23 -4
- package/dist/server/PatchesHistoryManager.js +42 -6
- package/dist/server/PatchesServer.d.ts +25 -9
- package/dist/server/RevConflictError.d.ts +13 -0
- package/dist/server/RevConflictError.js +10 -0
- package/dist/server/branchUtils.js +1 -1
- package/dist/server/index.d.ts +5 -1
- package/dist/server/index.js +15 -0
- package/dist/server/jsonReadable.d.ts +22 -0
- package/dist/server/jsonReadable.js +68 -0
- package/dist/server/types.d.ts +30 -16
- package/dist/shared/doc-manager.d.ts +1 -1
- package/dist/solid/context.d.ts +1 -1
- package/dist/solid/doc-manager.d.ts +1 -1
- package/dist/solid/index.d.ts +1 -1
- package/dist/solid/primitives.d.ts +1 -1
- package/dist/vue/composables.d.ts +1 -1
- package/dist/vue/doc-manager.d.ts +1 -1
- package/dist/vue/index.d.ts +1 -1
- package/dist/vue/managed-docs.d.ts +2 -2
- package/dist/vue/provider.d.ts +1 -1
- package/package.json +13 -12
|
@@ -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
|
-
|
|
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,
|
|
80
|
+
async createVersion(docId, metadata, _changes) {
|
|
78
81
|
const versions = this.versions.get(docId) || [];
|
|
79
|
-
versions.push({ metadata
|
|
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
|
|
117
|
+
async loadVersion(docId, versionId) {
|
|
115
118
|
const versions = this.versions.get(docId) || [];
|
|
116
|
-
|
|
117
|
-
|
|
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,
|
|
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,
|
|
55
|
+
constructor(store: LWWStoreBackend, _options?: LWWServerOptions);
|
|
62
56
|
/**
|
|
63
|
-
* Get the current state of a document.
|
|
64
|
-
*
|
|
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
|
|
62
|
+
* @returns A ReadableStream of JSON string chunks.
|
|
68
63
|
*/
|
|
69
|
-
getDoc(docId: string): Promise<
|
|
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
|
|
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
|
|
85
|
+
* @returns An object with the committed changes (0-1 elements).
|
|
91
86
|
*/
|
|
92
|
-
commitChanges(docId: string, changes: ChangeInput[], options?: CommitChangesOptions): Promise<
|
|
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.
|
package/dist/server/LWWServer.js
CHANGED
|
@@ -4,8 +4,8 @@ import { consolidateOps, convertDeltaOps } from "../algorithms/lww/consolidateOp
|
|
|
4
4
|
import { createChange } from "../data/change.js";
|
|
5
5
|
import { signal } from "easy-signal";
|
|
6
6
|
import { createJSONPatch } from "../json-patch/createJSONPatch.js";
|
|
7
|
-
import { JSONPatch } from "../json-patch/JSONPatch.js";
|
|
8
7
|
import { getClientId } from "../net/serverContext.js";
|
|
8
|
+
import { concatStreams } from "./jsonReadable.js";
|
|
9
9
|
import { createTombstoneIfSupported, removeTombstoneIfExists } from "./tombstone.js";
|
|
10
10
|
import { assertVersionMetadata } from "./utils.js";
|
|
11
11
|
class LWWServer {
|
|
@@ -21,33 +21,35 @@ class LWWServer {
|
|
|
21
21
|
undeleteDoc: "write"
|
|
22
22
|
};
|
|
23
23
|
store;
|
|
24
|
-
snapshotInterval;
|
|
25
24
|
/** Notifies listeners whenever a batch of changes is successfully committed. */
|
|
26
25
|
onChangesCommitted = signal();
|
|
27
26
|
/** Notifies listeners when a document is deleted. */
|
|
28
27
|
onDocDeleted = signal();
|
|
29
|
-
constructor(store,
|
|
28
|
+
constructor(store, _options = {}) {
|
|
30
29
|
this.store = store;
|
|
31
|
-
this.snapshotInterval = options.snapshotInterval ?? 200;
|
|
32
30
|
}
|
|
33
31
|
/**
|
|
34
|
-
* Get the current state of a document.
|
|
35
|
-
*
|
|
32
|
+
* Get the current state of a document as a ReadableStream of JSON.
|
|
33
|
+
* Streams `{"state":...,"rev":N,"changes":[...]}` with the snapshot state
|
|
34
|
+
* flowing through without parsing.
|
|
36
35
|
*
|
|
37
36
|
* @param docId - The document ID.
|
|
38
|
-
* @returns
|
|
37
|
+
* @returns A ReadableStream of JSON string chunks.
|
|
39
38
|
*/
|
|
40
39
|
async getDoc(docId) {
|
|
41
40
|
const snapshot = await this.store.getSnapshot(docId);
|
|
42
|
-
const baseState = snapshot?.state ?? {};
|
|
43
41
|
const baseRev = snapshot?.rev ?? 0;
|
|
44
42
|
const ops = await this.store.listOps(docId, { sinceRev: baseRev });
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
let changes = [];
|
|
44
|
+
if (ops.length > 0) {
|
|
45
|
+
const sortedOps = [...ops].sort((a, b) => (a.ts ?? 0) - (b.ts ?? 0));
|
|
46
|
+
const maxRev = Math.max(baseRev, ...ops.map((op) => op.rev ?? 0));
|
|
47
|
+
const maxTs = Math.max(...ops.map((op) => op.ts ?? 0));
|
|
48
|
+
changes = [createChange(baseRev, maxRev, sortedOps, { committedAt: maxTs || Date.now() })];
|
|
47
49
|
}
|
|
48
|
-
const
|
|
49
|
-
const rev =
|
|
50
|
-
return {
|
|
50
|
+
const statePayload = snapshot?.state ?? "{}";
|
|
51
|
+
const rev = changes[changes.length - 1]?.rev || baseRev;
|
|
52
|
+
return concatStreams(`{"state":`, statePayload, `,"rev":${rev},"changes":`, JSON.stringify(changes), "}");
|
|
51
53
|
}
|
|
52
54
|
/**
|
|
53
55
|
* Get changes that occurred after a specific revision.
|
|
@@ -55,7 +57,7 @@ class LWWServer {
|
|
|
55
57
|
*
|
|
56
58
|
* @param docId - The document ID.
|
|
57
59
|
* @param rev - The revision number to get changes after.
|
|
58
|
-
* @returns Array
|
|
60
|
+
* @returns Array of synthesized changes (0 or 1 elements).
|
|
59
61
|
*/
|
|
60
62
|
async getChangesSince(docId, rev) {
|
|
61
63
|
const ops = await this.store.listOps(docId, { sinceRev: rev });
|
|
@@ -78,11 +80,11 @@ class LWWServer {
|
|
|
78
80
|
* @param docId - The document ID.
|
|
79
81
|
* @param changes - The changes to commit (always 1 for LWW).
|
|
80
82
|
* @param options - Optional commit options (ignored for LWW).
|
|
81
|
-
* @returns
|
|
83
|
+
* @returns An object with the committed changes (0-1 elements).
|
|
82
84
|
*/
|
|
83
85
|
async commitChanges(docId, changes, options) {
|
|
84
86
|
if (changes.length === 0) {
|
|
85
|
-
return [];
|
|
87
|
+
return { changes: [] };
|
|
86
88
|
}
|
|
87
89
|
const change = changes[0];
|
|
88
90
|
const serverNow = Date.now();
|
|
@@ -96,10 +98,6 @@ class LWWServer {
|
|
|
96
98
|
if (opsToStore.length > 0 || pathsToDelete.length > 0) {
|
|
97
99
|
newRev = await this.store.saveOps(docId, opsToStore, pathsToDelete);
|
|
98
100
|
}
|
|
99
|
-
if (newRev > 0 && newRev % this.snapshotInterval === 0) {
|
|
100
|
-
const { state } = await this.getDoc(docId);
|
|
101
|
-
await this.store.saveSnapshot(docId, state, newRev);
|
|
102
|
-
}
|
|
103
101
|
const responseOps = [...opsToReturn];
|
|
104
102
|
if (clientRev !== void 0) {
|
|
105
103
|
const opsSince = await this.store.listOps(docId, { sinceRev: clientRev });
|
|
@@ -122,7 +120,7 @@ class LWWServer {
|
|
|
122
120
|
console.error(`Failed to notify clients about committed changes for doc ${docId}:`, error);
|
|
123
121
|
}
|
|
124
122
|
}
|
|
125
|
-
return [responseChange];
|
|
123
|
+
return { changes: [responseChange] };
|
|
126
124
|
}
|
|
127
125
|
/**
|
|
128
126
|
* Delete a document and emit deletion signal.
|
|
@@ -148,13 +146,14 @@ class LWWServer {
|
|
|
148
146
|
}
|
|
149
147
|
/**
|
|
150
148
|
* Make a server-side change to a document.
|
|
149
|
+
* Stateless — uses getCurrentRev instead of loading document state.
|
|
151
150
|
* @param docId - The document ID.
|
|
152
151
|
* @param mutator - A function that receives a JSONPatch and PathProxy to define the changes.
|
|
153
152
|
* @param metadata - Optional metadata for the change.
|
|
154
153
|
* @returns The created change, or null if no operations were generated.
|
|
155
154
|
*/
|
|
156
155
|
async change(docId, mutator, metadata) {
|
|
157
|
-
const
|
|
156
|
+
const rev = await this.store.getCurrentRev(docId);
|
|
158
157
|
const patch = createJSONPatch(mutator);
|
|
159
158
|
if (patch.ops.length === 0) {
|
|
160
159
|
return null;
|
|
@@ -162,13 +161,14 @@ class LWWServer {
|
|
|
162
161
|
const serverNow = Date.now();
|
|
163
162
|
const opsWithTs = patch.ops.map((op) => ({ ...op, ts: serverNow }));
|
|
164
163
|
const change = createChange(rev, rev + 1, opsWithTs, metadata);
|
|
165
|
-
patch.apply(state);
|
|
166
164
|
await this.commitChanges(docId, [change]);
|
|
167
165
|
return change;
|
|
168
166
|
}
|
|
169
167
|
/**
|
|
170
168
|
* Captures the current state of a document as a new version.
|
|
171
169
|
* Only works if store implements VersioningStoreBackend.
|
|
170
|
+
* Does NOT build state — creates version metadata, then emits `onVersionCreated`
|
|
171
|
+
* so subscribers can build and persist state out of band.
|
|
172
172
|
*
|
|
173
173
|
* @param docId - The document ID.
|
|
174
174
|
* @param metadata - Optional metadata for the version.
|
|
@@ -179,19 +179,20 @@ class LWWServer {
|
|
|
179
179
|
if (!this.isVersioningStore(this.store)) {
|
|
180
180
|
throw new Error("LWW versioning requires a store that implements VersioningStoreBackend");
|
|
181
181
|
}
|
|
182
|
-
const
|
|
182
|
+
const rev = await this.store.getCurrentRev(docId);
|
|
183
183
|
if (rev === 0) {
|
|
184
184
|
return null;
|
|
185
185
|
}
|
|
186
|
+
const now = Date.now();
|
|
186
187
|
const versionMetadata = createVersionMetadata({
|
|
187
188
|
origin: "main",
|
|
188
|
-
startedAt:
|
|
189
|
-
endedAt:
|
|
189
|
+
startedAt: now,
|
|
190
|
+
endedAt: now,
|
|
190
191
|
startRev: rev,
|
|
191
192
|
endRev: rev,
|
|
192
193
|
...metadata
|
|
193
194
|
});
|
|
194
|
-
await this.store.createVersion(docId, versionMetadata
|
|
195
|
+
await this.store.createVersion(docId, versionMetadata);
|
|
195
196
|
return versionMetadata.id;
|
|
196
197
|
}
|
|
197
198
|
/**
|
|
@@ -40,9 +40,13 @@ class OTBranchManager {
|
|
|
40
40
|
const { state: stateAtRev } = await getStateAtRevision(this.store, docId, rev);
|
|
41
41
|
const branchDocId = await generateBranchId(this.store, docId);
|
|
42
42
|
const now = Date.now();
|
|
43
|
+
const initialChange = createChange(0, 1, [{ op: "replace", path: "", value: stateAtRev }], {
|
|
44
|
+
createdAt: now,
|
|
45
|
+
committedAt: now
|
|
46
|
+
});
|
|
47
|
+
await this.store.saveChanges(branchDocId, [initialChange]);
|
|
43
48
|
const initialVersionMetadata = createVersionMetadata({
|
|
44
49
|
origin: "main",
|
|
45
|
-
// Branch doc versions are 'main' until merged
|
|
46
50
|
startedAt: now,
|
|
47
51
|
endedAt: now,
|
|
48
52
|
endRev: rev,
|
|
@@ -51,7 +55,7 @@ class OTBranchManager {
|
|
|
51
55
|
groupId: branchDocId,
|
|
52
56
|
branchName: metadata?.name
|
|
53
57
|
});
|
|
54
|
-
await this.store.createVersion(branchDocId, initialVersionMetadata,
|
|
58
|
+
await this.store.createVersion(branchDocId, initialVersionMetadata, [initialChange]);
|
|
55
59
|
const branch = createBranchRecord(branchDocId, docId, rev, metadata);
|
|
56
60
|
await this.store.createBranch(branch);
|
|
57
61
|
return branchDocId;
|
|
@@ -107,9 +111,8 @@ class OTBranchManager {
|
|
|
107
111
|
// Keep branchName for traceability
|
|
108
112
|
parentId: lastVersionId
|
|
109
113
|
});
|
|
110
|
-
const state = await this.store.loadVersionState(branchId, v.id);
|
|
111
114
|
const changes = await this.store.loadVersionChanges?.(branchId, v.id);
|
|
112
|
-
await this.store.createVersion(sourceDocId, newVersionMetadata,
|
|
115
|
+
await this.store.createVersion(sourceDocId, newVersionMetadata, changes);
|
|
113
116
|
lastVersionId = newVersionMetadata.id;
|
|
114
117
|
}
|
|
115
118
|
const committedMergeChanges = await wrapMergeCommit(branchId, sourceDocId, async () => {
|
|
@@ -120,7 +123,7 @@ class OTBranchManager {
|
|
|
120
123
|
rev: void 0
|
|
121
124
|
// Let commitChanges assign sequential revs
|
|
122
125
|
}));
|
|
123
|
-
return this.patchesServer.commitChanges(sourceDocId, adjustedChanges);
|
|
126
|
+
return (await this.patchesServer.commitChanges(sourceDocId, adjustedChanges)).changes;
|
|
124
127
|
} else {
|
|
125
128
|
const rev = branchStartRevOnSource + branchChanges.length;
|
|
126
129
|
const flattenedChange = createChange(
|
|
@@ -132,7 +135,7 @@ class OTBranchManager {
|
|
|
132
135
|
if (this.maxPayloadBytes) {
|
|
133
136
|
changesToCommit = breakChanges(changesToCommit, this.maxPayloadBytes);
|
|
134
137
|
}
|
|
135
|
-
return this.patchesServer.commitChanges(sourceDocId, changesToCommit);
|
|
138
|
+
return (await this.patchesServer.commitChanges(sourceDocId, changesToCommit)).changes;
|
|
136
139
|
}
|
|
137
140
|
});
|
|
138
141
|
await this.closeBranch(branchId, "merged");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as easy_signal from 'easy-signal';
|
|
2
2
|
import { PatchesServer } from './PatchesServer.js';
|
|
3
3
|
import { OTStoreBackend } from './types.js';
|
|
4
|
-
import { Change, CommitChangesOptions, DeleteDocOptions,
|
|
4
|
+
import { Change, CommitChangesOptions, DeleteDocOptions, ChangeInput, ChangeMutator, EditableVersionMetadata } from '../types.js';
|
|
5
5
|
import { ApiDefinition } from '../net/protocol/JSONRPCServer.js';
|
|
6
6
|
import '../json-patch/JSONPatch.js';
|
|
7
7
|
import '@dabble/delta';
|
|
@@ -19,11 +19,6 @@ interface OTServerOptions {
|
|
|
19
19
|
* Defaults to 30 minutes.
|
|
20
20
|
*/
|
|
21
21
|
sessionTimeoutMinutes?: number;
|
|
22
|
-
/**
|
|
23
|
-
* Maximum size in bytes for a single change's storage representation.
|
|
24
|
-
* Useful for databases with row size limits.
|
|
25
|
-
*/
|
|
26
|
-
maxStorageBytes?: number;
|
|
27
22
|
}
|
|
28
23
|
/**
|
|
29
24
|
* Handles the server-side Operational Transformation (OT) logic,
|
|
@@ -45,7 +40,6 @@ declare class OTServer implements PatchesServer {
|
|
|
45
40
|
*/
|
|
46
41
|
static api: ApiDefinition;
|
|
47
42
|
private readonly sessionTimeoutMillis;
|
|
48
|
-
private readonly maxStorageBytes?;
|
|
49
43
|
readonly store: OTStoreBackend;
|
|
50
44
|
/** Notifies listeners whenever a batch of changes is *successfully* committed. */
|
|
51
45
|
readonly onChangesCommitted: easy_signal.Signal<(docId: string, changes: Change[], options?: CommitChangesOptions, originClientId?: string) => void>;
|
|
@@ -53,39 +47,46 @@ declare class OTServer implements PatchesServer {
|
|
|
53
47
|
readonly onDocDeleted: easy_signal.Signal<(docId: string, options?: DeleteDocOptions, originClientId?: string) => void>;
|
|
54
48
|
constructor(store: OTStoreBackend, options?: OTServerOptions);
|
|
55
49
|
/**
|
|
56
|
-
* Get the current state of a document.
|
|
50
|
+
* Get the current state of a document as a ReadableStream of JSON.
|
|
51
|
+
* Streams `{"state":...,"rev":N,"changes":[...]}` with the version state
|
|
52
|
+
* flowing through without parsing.
|
|
57
53
|
* @param docId - The ID of the document.
|
|
58
|
-
* @returns
|
|
54
|
+
* @returns A ReadableStream of JSON string chunks.
|
|
59
55
|
*/
|
|
60
|
-
getDoc(docId: string): Promise<
|
|
56
|
+
getDoc(docId: string): Promise<ReadableStream<string>>;
|
|
61
57
|
/**
|
|
62
58
|
* Get changes that occurred after a specific revision.
|
|
63
59
|
* @param docId - The ID of the document.
|
|
64
60
|
* @param rev - The revision number.
|
|
65
|
-
* @returns
|
|
61
|
+
* @returns Array of changes after the given revision.
|
|
66
62
|
*/
|
|
67
63
|
getChangesSince(docId: string, rev: number): Promise<Change[]>;
|
|
68
64
|
/**
|
|
69
65
|
* Commits a set of changes to a document, applying operational transformation as needed.
|
|
70
66
|
*
|
|
71
|
-
* Returns all changes the client needs to apply
|
|
72
|
-
*
|
|
73
|
-
*
|
|
67
|
+
* Returns all changes the client needs to apply (catchup changes from others, followed
|
|
68
|
+
* by the client's own transformed changes), plus an optional `docReloadRequired` flag.
|
|
69
|
+
*
|
|
70
|
+
* When `docReloadRequired` is true, the client must call `getDoc` to reload the full
|
|
71
|
+
* current state before continuing — its local state is stale.
|
|
74
72
|
*
|
|
75
73
|
* @param docId - The ID of the document.
|
|
76
74
|
* @param changes - The changes to commit.
|
|
77
75
|
* @param options - Optional commit settings (e.g., forceCommit for migrations).
|
|
78
|
-
* @returns
|
|
76
|
+
* @returns An object with the committed changes and an optional reload flag.
|
|
79
77
|
*/
|
|
80
|
-
commitChanges(docId: string, changes: ChangeInput[], options?: CommitChangesOptions): Promise<
|
|
78
|
+
commitChanges(docId: string, changes: ChangeInput[], options?: CommitChangesOptions): Promise<{
|
|
79
|
+
changes: Change[];
|
|
80
|
+
docReloadRequired?: true;
|
|
81
|
+
}>;
|
|
81
82
|
/**
|
|
82
83
|
* Make a server-side change to a document.
|
|
83
|
-
*
|
|
84
|
-
* @returns
|
|
84
|
+
* Stateless — uses getCurrentRev instead of loading document state.
|
|
85
85
|
*/
|
|
86
86
|
change<T = Record<string, any>>(docId: string, mutator: ChangeMutator<T>, metadata?: Record<string, any>): Promise<Change | null>;
|
|
87
87
|
/**
|
|
88
88
|
* Deletes a document.
|
|
89
|
+
* Stateless — uses getCurrentRev instead of loading document state.
|
|
89
90
|
* @param docId The document ID.
|
|
90
91
|
* @param options - Optional deletion settings (e.g., skipTombstone for testing).
|
|
91
92
|
*/
|
|
@@ -98,9 +99,12 @@ declare class OTServer implements PatchesServer {
|
|
|
98
99
|
undeleteDoc(docId: string): Promise<boolean>;
|
|
99
100
|
/**
|
|
100
101
|
* Captures the current state of a document as a new version.
|
|
102
|
+
* Does NOT build state — creates version metadata + changes, then emits
|
|
103
|
+
* `onVersionCreated` so subscribers can build and persist state out of band.
|
|
104
|
+
*
|
|
101
105
|
* @param docId The document ID.
|
|
102
106
|
* @param metadata Optional metadata for the version.
|
|
103
|
-
* @returns The ID of the created version.
|
|
107
|
+
* @returns The ID of the created version, or null if no changes to capture.
|
|
104
108
|
*/
|
|
105
109
|
captureCurrentVersion(docId: string, metadata?: EditableVersionMetadata): Promise<string | null>;
|
|
106
110
|
}
|
package/dist/server/OTServer.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import "../chunk-IZ2YBCUP.js";
|
|
2
2
|
import { commitChanges } from "../algorithms/ot/server/commitChanges.js";
|
|
3
3
|
import { createVersion } from "../algorithms/ot/server/createVersion.js";
|
|
4
|
-
import { getSnapshotAtRevision } from "../algorithms/ot/server/getSnapshotAtRevision.js";
|
|
5
|
-
import { getStateAtRevision } from "../algorithms/ot/server/getStateAtRevision.js";
|
|
6
|
-
import { applyChanges } from "../algorithms/ot/shared/applyChanges.js";
|
|
4
|
+
import { getSnapshotAtRevision, getSnapshotStream } from "../algorithms/ot/server/getSnapshotAtRevision.js";
|
|
7
5
|
import { createChange } from "../data/change.js";
|
|
8
6
|
import { signal } from "easy-signal";
|
|
9
7
|
import { createJSONPatch } from "../json-patch/createJSONPatch.js";
|
|
@@ -23,7 +21,6 @@ class OTServer {
|
|
|
23
21
|
undeleteDoc: "write"
|
|
24
22
|
};
|
|
25
23
|
sessionTimeoutMillis;
|
|
26
|
-
maxStorageBytes;
|
|
27
24
|
store;
|
|
28
25
|
/** Notifies listeners whenever a batch of changes is *successfully* committed. */
|
|
29
26
|
onChangesCommitted = signal();
|
|
@@ -31,46 +28,48 @@ class OTServer {
|
|
|
31
28
|
onDocDeleted = signal();
|
|
32
29
|
constructor(store, options = {}) {
|
|
33
30
|
this.sessionTimeoutMillis = (options.sessionTimeoutMinutes ?? 30) * 60 * 1e3;
|
|
34
|
-
this.maxStorageBytes = options.maxStorageBytes;
|
|
35
31
|
this.store = store;
|
|
36
32
|
}
|
|
37
33
|
/**
|
|
38
|
-
* Get the current state of a document.
|
|
34
|
+
* Get the current state of a document as a ReadableStream of JSON.
|
|
35
|
+
* Streams `{"state":...,"rev":N,"changes":[...]}` with the version state
|
|
36
|
+
* flowing through without parsing.
|
|
39
37
|
* @param docId - The ID of the document.
|
|
40
|
-
* @returns
|
|
38
|
+
* @returns A ReadableStream of JSON string chunks.
|
|
41
39
|
*/
|
|
42
40
|
async getDoc(docId) {
|
|
43
|
-
return
|
|
41
|
+
return getSnapshotStream(this.store, docId);
|
|
44
42
|
}
|
|
45
43
|
/**
|
|
46
44
|
* Get changes that occurred after a specific revision.
|
|
47
45
|
* @param docId - The ID of the document.
|
|
48
46
|
* @param rev - The revision number.
|
|
49
|
-
* @returns
|
|
47
|
+
* @returns Array of changes after the given revision.
|
|
50
48
|
*/
|
|
51
|
-
getChangesSince(docId, rev) {
|
|
49
|
+
async getChangesSince(docId, rev) {
|
|
52
50
|
return this.store.listChanges(docId, { startAfter: rev });
|
|
53
51
|
}
|
|
54
52
|
/**
|
|
55
53
|
* Commits a set of changes to a document, applying operational transformation as needed.
|
|
56
54
|
*
|
|
57
|
-
* Returns all changes the client needs to apply
|
|
58
|
-
*
|
|
59
|
-
*
|
|
55
|
+
* Returns all changes the client needs to apply (catchup changes from others, followed
|
|
56
|
+
* by the client's own transformed changes), plus an optional `docReloadRequired` flag.
|
|
57
|
+
*
|
|
58
|
+
* When `docReloadRequired` is true, the client must call `getDoc` to reload the full
|
|
59
|
+
* current state before continuing — its local state is stale.
|
|
60
60
|
*
|
|
61
61
|
* @param docId - The ID of the document.
|
|
62
62
|
* @param changes - The changes to commit.
|
|
63
63
|
* @param options - Optional commit settings (e.g., forceCommit for migrations).
|
|
64
|
-
* @returns
|
|
64
|
+
* @returns An object with the committed changes and an optional reload flag.
|
|
65
65
|
*/
|
|
66
66
|
async commitChanges(docId, changes, options) {
|
|
67
|
-
const { catchupChanges, newChanges } = await commitChanges(
|
|
67
|
+
const { catchupChanges, newChanges, docReloadRequired } = await commitChanges(
|
|
68
68
|
this.store,
|
|
69
69
|
docId,
|
|
70
70
|
changes,
|
|
71
71
|
this.sessionTimeoutMillis,
|
|
72
|
-
options
|
|
73
|
-
this.maxStorageBytes
|
|
72
|
+
options
|
|
74
73
|
);
|
|
75
74
|
if (newChanges.length > 0) {
|
|
76
75
|
try {
|
|
@@ -79,32 +78,35 @@ class OTServer {
|
|
|
79
78
|
console.error(`Failed to notify clients about committed changes for doc ${docId}:`, error);
|
|
80
79
|
}
|
|
81
80
|
}
|
|
82
|
-
|
|
81
|
+
const result = {
|
|
82
|
+
changes: [...catchupChanges, ...newChanges]
|
|
83
|
+
};
|
|
84
|
+
if (docReloadRequired) result.docReloadRequired = true;
|
|
85
|
+
return result;
|
|
83
86
|
}
|
|
84
87
|
/**
|
|
85
88
|
* Make a server-side change to a document.
|
|
86
|
-
*
|
|
87
|
-
* @returns
|
|
89
|
+
* Stateless — uses getCurrentRev instead of loading document state.
|
|
88
90
|
*/
|
|
89
91
|
async change(docId, mutator, metadata) {
|
|
90
|
-
const
|
|
92
|
+
const rev = await this.store.getCurrentRev(docId);
|
|
91
93
|
const patch = createJSONPatch(mutator);
|
|
92
94
|
if (patch.ops.length === 0) {
|
|
93
95
|
return null;
|
|
94
96
|
}
|
|
95
97
|
const change = createChange(rev, rev + 1, patch.ops, metadata);
|
|
96
|
-
patch.apply(state);
|
|
97
98
|
await this.commitChanges(docId, [change]);
|
|
98
99
|
return change;
|
|
99
100
|
}
|
|
100
101
|
/**
|
|
101
102
|
* Deletes a document.
|
|
103
|
+
* Stateless — uses getCurrentRev instead of loading document state.
|
|
102
104
|
* @param docId The document ID.
|
|
103
105
|
* @param options - Optional deletion settings (e.g., skipTombstone for testing).
|
|
104
106
|
*/
|
|
105
107
|
async deleteDoc(docId, options) {
|
|
106
108
|
const clientId = getClientId();
|
|
107
|
-
const
|
|
109
|
+
const rev = await this.store.getCurrentRev(docId);
|
|
108
110
|
await createTombstoneIfSupported(this.store, docId, rev, clientId, options?.skipTombstone);
|
|
109
111
|
await this.store.deleteDoc(docId);
|
|
110
112
|
await this.onDocDeleted.emit(docId, options, clientId);
|
|
@@ -120,15 +122,17 @@ class OTServer {
|
|
|
120
122
|
// === Version Operations ===
|
|
121
123
|
/**
|
|
122
124
|
* Captures the current state of a document as a new version.
|
|
125
|
+
* Does NOT build state — creates version metadata + changes, then emits
|
|
126
|
+
* `onVersionCreated` so subscribers can build and persist state out of band.
|
|
127
|
+
*
|
|
123
128
|
* @param docId The document ID.
|
|
124
129
|
* @param metadata Optional metadata for the version.
|
|
125
|
-
* @returns The ID of the created version.
|
|
130
|
+
* @returns The ID of the created version, or null if no changes to capture.
|
|
126
131
|
*/
|
|
127
132
|
async captureCurrentVersion(docId, metadata) {
|
|
128
133
|
assertVersionMetadata(metadata);
|
|
129
|
-
const {
|
|
130
|
-
const
|
|
131
|
-
const version = await createVersion(this.store, docId, state, changes, metadata);
|
|
134
|
+
const { changes } = await getSnapshotAtRevision(this.store, docId);
|
|
135
|
+
const version = await createVersion(this.store, docId, changes, { metadata });
|
|
132
136
|
if (!version) {
|
|
133
137
|
return null;
|
|
134
138
|
}
|
|
@@ -44,12 +44,30 @@ declare class PatchesHistoryManager {
|
|
|
44
44
|
updateVersion(docId: string, versionId: string, metadata: EditableVersionMetadata): Promise<void>;
|
|
45
45
|
/**
|
|
46
46
|
* Loads the full document state snapshot for a specific version by its ID.
|
|
47
|
+
* Returns a ReadableStream so the state can be streamed to clients via RPC.
|
|
47
48
|
* @param docId - The ID of the document.
|
|
48
49
|
* @param versionId - The unique ID of the version.
|
|
49
|
-
* @returns
|
|
50
|
-
* @throws Error if
|
|
50
|
+
* @returns A ReadableStream of the JSON state, or a stream of 'null' if not found.
|
|
51
|
+
* @throws Error if state loading fails.
|
|
51
52
|
*/
|
|
52
|
-
getStateAtVersion(docId: string, versionId: string): Promise<
|
|
53
|
+
getStateAtVersion(docId: string, versionId: string): Promise<ReadableStream<string>>;
|
|
54
|
+
/**
|
|
55
|
+
* Returns the document state immediately before a version's changes begin.
|
|
56
|
+
*
|
|
57
|
+
* Uses the version's `parentId` chain to find the correct baseline — so
|
|
58
|
+
* offline-branch session 2 returns the state after session 1, not after the
|
|
59
|
+
* last main-timeline version. Bridges any gap between the parent's `endRev`
|
|
60
|
+
* and the version's `startRev` by applying intermediate changes.
|
|
61
|
+
*
|
|
62
|
+
* Use this as the baseline when scrubbing through a version's individual changes.
|
|
63
|
+
*
|
|
64
|
+
* Only available for OT stores.
|
|
65
|
+
*
|
|
66
|
+
* @param docId - The document ID.
|
|
67
|
+
* @param versionId - The version whose pre-state to compute.
|
|
68
|
+
* @returns A ReadableStream of the JSON state at `version.startRev - 1`.
|
|
69
|
+
*/
|
|
70
|
+
getStateBeforeVersion(docId: string, versionId: string): Promise<ReadableStream<string>>;
|
|
53
71
|
/**
|
|
54
72
|
* Loads the list of original client changes that were included in a specific version.
|
|
55
73
|
* Useful for replaying/scrubbing through the operations within an offline or online session.
|
|
@@ -61,8 +79,9 @@ declare class PatchesHistoryManager {
|
|
|
61
79
|
getChangesForVersion(docId: string, versionId: string): Promise<Change[]>;
|
|
62
80
|
/**
|
|
63
81
|
* Alias for getStateAtVersion for RPC API compatibility.
|
|
82
|
+
* Returns a ReadableStream for efficient JSON-RPC streaming.
|
|
64
83
|
*/
|
|
65
|
-
getVersionState(docId: string, versionId: string): Promise<
|
|
84
|
+
getVersionState(docId: string, versionId: string): Promise<ReadableStream<string>>;
|
|
66
85
|
/**
|
|
67
86
|
* Alias for getChangesForVersion for RPC API compatibility.
|
|
68
87
|
*/
|