@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.
- 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/client/LWWInMemoryStore.js +4 -0
- package/dist/client/LWWIndexedDBStore.js +7 -0
- package/dist/client/Patches.js +5 -2
- package/dist/net/PatchesClient.d.ts +4 -1
- package/dist/net/PatchesSync.js +14 -3
- 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 +40 -4
- 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/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/package.json +1 -1
|
@@ -1,22 +1,17 @@
|
|
|
1
1
|
import "../../../chunk-IZ2YBCUP.js";
|
|
2
|
-
import {
|
|
3
|
-
|
|
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 (
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
29
|
-
|
|
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
|
-
*
|
|
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[],
|
|
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,
|
|
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(
|
|
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
|
-
|
|
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
|
}
|
package/dist/client/Patches.js
CHANGED
|
@@ -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(
|
|
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<
|
|
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.
|
package/dist/net/PatchesSync.js
CHANGED
|
@@ -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
|
-
|
|
291
|
-
|
|
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<
|
|
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,
|
|
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
|
-
|
|
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,
|
|
38
|
+
async createVersion(docId, metadata, changes) {
|
|
39
39
|
const compressedChanges = changes?.map((c) => this.compressChange(c));
|
|
40
|
-
return this.store.createVersion(docId, metadata,
|
|
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
|
|
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,
|
|
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,
|
|
47
|
+
createVersion(docId: string, metadata: VersionMetadata, _changes?: Change[]): Promise<void>;
|
|
52
48
|
listVersions(docId: string, options?: ListVersionsOptions): Promise<VersionMetadata[]>;
|
|
53
|
-
|
|
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
|
-
|
|
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.
|