@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
|
@@ -60,6 +60,8 @@ function consolidateOps(existingOps, newOps) {
|
|
|
60
60
|
const consolidated = consolidateFieldOp(existing, newOp);
|
|
61
61
|
if (consolidated !== null) {
|
|
62
62
|
opsToSave.push(consolidated);
|
|
63
|
+
} else if (!isSoftOp(newOp) && !combinableOps[newOp.op]) {
|
|
64
|
+
if (!opsToReturnMap.has(existing.path)) opsToReturnMap.set(existing.path, existing);
|
|
63
65
|
}
|
|
64
66
|
} else {
|
|
65
67
|
if (isSoftOp(newOp)) {
|
|
@@ -17,13 +17,7 @@ function applyCommittedChanges(snapshot, committedChangesFromServer) {
|
|
|
17
17
|
);
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
|
-
|
|
21
|
-
state = applyChanges(state, newServerChanges);
|
|
22
|
-
} catch (error) {
|
|
23
|
-
console.error("Failed to apply server changes to committed state:", error);
|
|
24
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
25
|
-
throw new Error(`Critical sync error applying server changes: ${errorMessage}`);
|
|
26
|
-
}
|
|
20
|
+
state = applyChanges(state, newServerChanges);
|
|
27
21
|
rev = lastChange.rev;
|
|
28
22
|
if (changes && changes.length > 0) {
|
|
29
23
|
changes = rebaseChanges(newServerChanges, changes);
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { OTStoreBackend } from '../../../server/types.js';
|
|
2
|
+
import { VersionMetadata, Change, PatchesState } from '../../../types.js';
|
|
3
|
+
import '../../../json-patch/types.js';
|
|
4
|
+
import '../../../json-patch/JSONPatch.js';
|
|
5
|
+
import '@dabble/delta';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Computes the document state at `version.startRev - 1`: the state just before
|
|
9
|
+
* this version's changes begin.
|
|
10
|
+
*
|
|
11
|
+
* Uses `version.parentId` (via `store.loadVersion`) when available for
|
|
12
|
+
* an efficient cached lookup. After loading the parent state, bridges any gap
|
|
13
|
+
* between the parent's `endRev` and `version.startRev` by applying intermediate
|
|
14
|
+
* changes. When no parentId exists (first version ever), builds from scratch.
|
|
15
|
+
*
|
|
16
|
+
* @param store - The store backend.
|
|
17
|
+
* @param docId - The document ID.
|
|
18
|
+
* @param version - The version whose pre-state to compute.
|
|
19
|
+
* @returns `{ state, rev }` at `version.startRev - 1`.
|
|
20
|
+
*/
|
|
21
|
+
declare function getBaseStateBeforeVersion(store: OTStoreBackend, docId: string, version: VersionMetadata): Promise<PatchesState>;
|
|
22
|
+
/**
|
|
23
|
+
* Returns the document state immediately before a version's changes begin,
|
|
24
|
+
* as a ReadableStream.
|
|
25
|
+
*
|
|
26
|
+
* Fast path: when the parent state needs no modification (no gap between the
|
|
27
|
+
* parent's `endRev` and `version.startRev`), the raw bytes are streamed
|
|
28
|
+
* directly from the store without parsing.
|
|
29
|
+
*
|
|
30
|
+
* Slow path: when there is a gap, the parent state is parsed, intermediate
|
|
31
|
+
* changes are applied, and the result is re-serialized.
|
|
32
|
+
*
|
|
33
|
+
* @param store - The store backend.
|
|
34
|
+
* @param docId - The document ID.
|
|
35
|
+
* @param version - The version whose pre-state to compute.
|
|
36
|
+
* @returns A ReadableStream of the JSON state at `version.startRev - 1`.
|
|
37
|
+
*/
|
|
38
|
+
declare function getStateBeforeVersionAsStream(store: OTStoreBackend, docId: string, version: VersionMetadata): Promise<ReadableStream<string>>;
|
|
39
|
+
/**
|
|
40
|
+
* Builds the document state for a version by computing the base state
|
|
41
|
+
* (via `getBaseStateBeforeVersion`) and applying the version's changes on top.
|
|
42
|
+
*
|
|
43
|
+
* @param store - The store backend to load previous version state from.
|
|
44
|
+
* @param docId - The document ID.
|
|
45
|
+
* @param version - The version metadata.
|
|
46
|
+
* @param changes - The changes included in this version.
|
|
47
|
+
* @returns The built state for the version.
|
|
48
|
+
*/
|
|
49
|
+
declare function buildVersionState(store: OTStoreBackend, docId: string, version: VersionMetadata, changes: Change[]): Promise<any>;
|
|
50
|
+
|
|
51
|
+
export { buildVersionState, getBaseStateBeforeVersion, getStateBeforeVersionAsStream };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import "../../../chunk-IZ2YBCUP.js";
|
|
2
|
+
import { jsonReadable, parseVersionState } from "../../../server/jsonReadable.js";
|
|
3
|
+
import { applyChanges } from "../shared/applyChanges.js";
|
|
4
|
+
async function getBaseStateBeforeVersion(store, docId, version) {
|
|
5
|
+
let baseState = null;
|
|
6
|
+
let baseRev = 0;
|
|
7
|
+
if (version.parentId) {
|
|
8
|
+
const [rawState, parentMeta] = await Promise.all([
|
|
9
|
+
store.loadVersionState(docId, version.parentId),
|
|
10
|
+
store.loadVersion(docId, version.parentId)
|
|
11
|
+
]);
|
|
12
|
+
if (parentMeta && rawState !== void 0) {
|
|
13
|
+
baseState = await parseVersionState(rawState);
|
|
14
|
+
baseRev = parentMeta.endRev;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
if (baseRev < version.startRev - 1) {
|
|
18
|
+
const gapChanges = await store.listChanges(docId, {
|
|
19
|
+
startAfter: baseRev,
|
|
20
|
+
endBefore: version.startRev
|
|
21
|
+
});
|
|
22
|
+
if (gapChanges.length > 0) {
|
|
23
|
+
baseState = applyChanges(baseState, gapChanges);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return { state: baseState, rev: version.startRev - 1 };
|
|
27
|
+
}
|
|
28
|
+
async function getStateBeforeVersionAsStream(store, docId, version) {
|
|
29
|
+
if (version.parentId) {
|
|
30
|
+
const [rawState, parentMeta] = await Promise.all([
|
|
31
|
+
store.loadVersionState(docId, version.parentId),
|
|
32
|
+
store.loadVersion(docId, version.parentId)
|
|
33
|
+
]);
|
|
34
|
+
if (parentMeta && rawState !== void 0) {
|
|
35
|
+
const baseRev = parentMeta.endRev;
|
|
36
|
+
if (baseRev >= version.startRev - 1) {
|
|
37
|
+
return typeof rawState === "string" ? jsonReadable(rawState) : rawState;
|
|
38
|
+
}
|
|
39
|
+
const baseState = await parseVersionState(rawState);
|
|
40
|
+
const gapChanges = await store.listChanges(docId, {
|
|
41
|
+
startAfter: baseRev,
|
|
42
|
+
endBefore: version.startRev
|
|
43
|
+
});
|
|
44
|
+
const state = gapChanges.length > 0 ? applyChanges(baseState, gapChanges) : baseState;
|
|
45
|
+
return jsonReadable(JSON.stringify(state));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (version.startRev > 1) {
|
|
49
|
+
const gapChanges = await store.listChanges(docId, {
|
|
50
|
+
startAfter: 0,
|
|
51
|
+
endBefore: version.startRev
|
|
52
|
+
});
|
|
53
|
+
if (gapChanges.length > 0) {
|
|
54
|
+
return jsonReadable(JSON.stringify(applyChanges(null, gapChanges)));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return jsonReadable("null");
|
|
58
|
+
}
|
|
59
|
+
async function buildVersionState(store, docId, version, changes) {
|
|
60
|
+
const { state: baseState } = await getBaseStateBeforeVersion(store, docId, version);
|
|
61
|
+
return applyChanges(baseState, changes);
|
|
62
|
+
}
|
|
63
|
+
export {
|
|
64
|
+
buildVersionState,
|
|
65
|
+
getBaseStateBeforeVersion,
|
|
66
|
+
getStateBeforeVersionAsStream
|
|
67
|
+
};
|
|
@@ -12,32 +12,34 @@ import '../../../net/protocol/types.js';
|
|
|
12
12
|
/**
|
|
13
13
|
* Commits a set of changes to a document, applying operational transformation as needed.
|
|
14
14
|
*
|
|
15
|
-
* ##
|
|
15
|
+
* ## Stateless Design
|
|
16
16
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* This function never loads or builds document state. It uses `getCurrentRev` to get the
|
|
18
|
+
* current revision and transforms changes against committed changes only (no state parameter
|
|
19
|
+
* passed to transformPatch). Bad ops become noops during transformation.
|
|
20
20
|
*
|
|
21
|
-
*
|
|
22
|
-
* 2. Returning all N changes as catchup for the client to apply
|
|
21
|
+
* ## Version Creation
|
|
23
22
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
23
|
+
* Versions are created (metadata + changes saved to store) when session timeouts are
|
|
24
|
+
* detected. After saving, `onVersionCreated` is emitted so subscribers can build and
|
|
25
|
+
* persist state out of band.
|
|
27
26
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
27
|
+
* ## Conflict Retry
|
|
28
|
+
*
|
|
29
|
+
* When a store's `saveChanges` throws `RevConflictError` (e.g. because another server
|
|
30
|
+
* instance committed the same revision), the function retries: re-reads `currentRev`,
|
|
31
|
+
* re-fetches committed changes, re-transforms, and re-saves. The retry naturally resolves
|
|
32
|
+
* because the fresh `currentRev` includes the conflicting commit.
|
|
30
33
|
*
|
|
31
34
|
* @param store - The backend store for persistence.
|
|
32
35
|
* @param docId - The ID of the document.
|
|
33
36
|
* @param changes - The changes to commit.
|
|
34
37
|
* @param sessionTimeoutMillis - Timeout for session-based versioning.
|
|
35
38
|
* @param options - Optional commit settings.
|
|
36
|
-
* @param maxStorageBytes - Optional max bytes per change for storage limits.
|
|
37
39
|
* @returns A CommitResult containing:
|
|
38
|
-
* - catchupChanges: Changes the client missed
|
|
40
|
+
* - catchupChanges: Changes the client missed
|
|
39
41
|
* - newChanges: The client's changes after transformation
|
|
40
42
|
*/
|
|
41
|
-
declare function commitChanges(store: OTStoreBackend, docId: string, changes: ChangeInput[], sessionTimeoutMillis: number, options?: CommitChangesOptions
|
|
43
|
+
declare function commitChanges(store: OTStoreBackend, docId: string, changes: ChangeInput[], sessionTimeoutMillis: number, options?: CommitChangesOptions): Promise<CommitResult>;
|
|
42
44
|
|
|
43
45
|
export { CommitChangesOptions, CommitResult, commitChanges };
|
|
@@ -1,34 +1,26 @@
|
|
|
1
1
|
import "../../../chunk-IZ2YBCUP.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { applyChanges } from "../shared/applyChanges.js";
|
|
5
|
-
import { createVersion } from "./createVersion.js";
|
|
6
|
-
import { getSnapshotAtRevision } from "./getSnapshotAtRevision.js";
|
|
7
|
-
import { getStateAtRevision } from "./getStateAtRevision.js";
|
|
2
|
+
import { RevConflictError } from "../../../server/RevConflictError.js";
|
|
3
|
+
import { createVersionAtRev } from "./createVersion.js";
|
|
8
4
|
import { handleOfflineSessionsAndBatches } from "./handleOfflineSessionsAndBatches.js";
|
|
9
5
|
import { transformIncomingChanges } from "./transformIncomingChanges.js";
|
|
10
|
-
|
|
6
|
+
const MAX_CONFLICT_RETRIES = 5;
|
|
7
|
+
async function commitChanges(store, docId, changes, sessionTimeoutMillis, options) {
|
|
11
8
|
if (changes.length === 0) {
|
|
12
9
|
return { catchupChanges: [], newChanges: [] };
|
|
13
10
|
}
|
|
14
11
|
const batchId = changes[0].batchId;
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
const currentRev = currentChanges.at(-1)?.rev ?? initialRev;
|
|
18
|
-
let baseRev = changes[0].baseRev ?? currentRev;
|
|
12
|
+
const initialRev = await store.getCurrentRev(docId);
|
|
13
|
+
let baseRev = changes[0].baseRev ?? initialRev;
|
|
19
14
|
const batchedContinuation = batchId && changes[0].rev > 1;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (changes[0].baseRev === 0 && currentRev > 0 && !batchedContinuation) {
|
|
15
|
+
let docReloadRequired;
|
|
16
|
+
if (changes[0].baseRev === 0 && initialRev > 0 && !batchedContinuation) {
|
|
23
17
|
const hasRootOp = changes.some((c) => c.ops.some((op) => op.path === ""));
|
|
24
18
|
if (!hasRootOp) {
|
|
25
|
-
|
|
26
|
-
baseRev =
|
|
27
|
-
|
|
19
|
+
docReloadRequired = true;
|
|
20
|
+
baseRev = initialRev;
|
|
21
|
+
for (const c of changes) {
|
|
28
22
|
c.baseRev = baseRev;
|
|
29
|
-
|
|
30
|
-
return c.ops.length > 0;
|
|
31
|
-
});
|
|
23
|
+
}
|
|
32
24
|
}
|
|
33
25
|
}
|
|
34
26
|
const serverNow = Date.now();
|
|
@@ -45,72 +37,63 @@ async function commitChanges(store, docId, changes, sessionTimeoutMillis, option
|
|
|
45
37
|
}
|
|
46
38
|
c.createdAt = c.createdAt ? Math.min(c.createdAt, serverNow) : serverNow;
|
|
47
39
|
});
|
|
48
|
-
if (baseRev >
|
|
40
|
+
if (baseRev > initialRev) {
|
|
49
41
|
throw new Error(
|
|
50
|
-
`Client baseRev (${baseRev}) is ahead of server revision (${
|
|
42
|
+
`Client baseRev (${baseRev}) is ahead of server revision (${initialRev}) for doc ${docId}. Client needs to reload the document.`
|
|
51
43
|
);
|
|
52
44
|
}
|
|
53
|
-
if (changes[0].baseRev === 0 &&
|
|
45
|
+
if (changes[0].baseRev === 0 && initialRev > 0 && !batchedContinuation && changes[0].ops[0]?.path === "") {
|
|
54
46
|
throw new Error(
|
|
55
|
-
`Document ${docId} already exists (rev ${
|
|
47
|
+
`Document ${docId} already exists (rev ${initialRev}). Cannot apply root-level replace (path: '') with baseRev 0 - this would overwrite the existing document. Load the existing document first, or use nested paths instead of replacing at root.`
|
|
56
48
|
);
|
|
57
49
|
}
|
|
58
|
-
const lastChange =
|
|
50
|
+
const [lastChange] = await store.listChanges(docId, { reverse: true, limit: 1 });
|
|
59
51
|
const compareTime = options?.historicalImport ? changes[0].createdAt ?? serverNow : serverNow;
|
|
60
52
|
if (lastChange && compareTime - lastChange.createdAt > sessionTimeoutMillis) {
|
|
61
|
-
await
|
|
62
|
-
}
|
|
63
|
-
const committedChanges = await store.listChanges(docId, {
|
|
64
|
-
startAfter: baseRev,
|
|
65
|
-
withoutBatchId: batchId
|
|
66
|
-
});
|
|
67
|
-
const committedIds = new Set(committedChanges.map((c) => c.id));
|
|
68
|
-
let incomingChanges = changes.filter((c) => !committedIds.has(c.id));
|
|
69
|
-
if (incomingChanges.length === 0) {
|
|
70
|
-
return { catchupChanges: committedChanges, newChanges: [] };
|
|
53
|
+
await createVersionAtRev(store, docId, lastChange.rev);
|
|
71
54
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
55
|
+
let offlineSessionsHandled = false;
|
|
56
|
+
for (let attempt = 0; attempt < MAX_CONFLICT_RETRIES; attempt++) {
|
|
57
|
+
try {
|
|
58
|
+
const currentRev = attempt === 0 ? initialRev : await store.getCurrentRev(docId);
|
|
59
|
+
const committedChanges = await store.listChanges(docId, {
|
|
60
|
+
startAfter: baseRev,
|
|
61
|
+
withoutBatchId: batchId
|
|
62
|
+
});
|
|
63
|
+
const committedIds = new Set(committedChanges.map((c) => c.id));
|
|
64
|
+
const incomingChanges = changes.filter((c) => !committedIds.has(c.id));
|
|
65
|
+
if (incomingChanges.length === 0) {
|
|
66
|
+
return { catchupChanges: committedChanges, newChanges: [], docReloadRequired };
|
|
67
|
+
}
|
|
68
|
+
const isOfflineTimestamp = serverNow - incomingChanges[0].createdAt > sessionTimeoutMillis;
|
|
69
|
+
if (isOfflineTimestamp || batchId) {
|
|
70
|
+
const canFastForward = committedChanges.length === 0;
|
|
71
|
+
if (!offlineSessionsHandled) {
|
|
72
|
+
const origin = options?.historicalImport ? "main" : canFastForward ? "main" : "offline-branch";
|
|
73
|
+
await handleOfflineSessionsAndBatches(store, sessionTimeoutMillis, docId, incomingChanges, origin);
|
|
74
|
+
offlineSessionsHandled = true;
|
|
75
|
+
}
|
|
76
|
+
if (canFastForward) {
|
|
77
|
+
await store.saveChanges(docId, incomingChanges);
|
|
78
|
+
return { catchupChanges: [], newChanges: incomingChanges, docReloadRequired };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const transformedChanges = transformIncomingChanges(
|
|
82
|
+
incomingChanges,
|
|
83
|
+
committedChanges,
|
|
84
|
+
currentRev,
|
|
85
|
+
options?.forceCommit
|
|
86
|
+
);
|
|
87
|
+
if (transformedChanges.length > 0) {
|
|
88
|
+
await store.saveChanges(docId, transformedChanges);
|
|
89
|
+
}
|
|
90
|
+
return { catchupChanges: committedChanges, newChanges: transformedChanges, docReloadRequired };
|
|
91
|
+
} catch (error) {
|
|
92
|
+
if (error instanceof RevConflictError && attempt < MAX_CONFLICT_RETRIES - 1) continue;
|
|
93
|
+
throw error;
|
|
89
94
|
}
|
|
90
95
|
}
|
|
91
|
-
|
|
92
|
-
const transformedChanges = transformIncomingChanges(
|
|
93
|
-
incomingChanges,
|
|
94
|
-
stateAtBaseRev,
|
|
95
|
-
committedChanges,
|
|
96
|
-
currentRev,
|
|
97
|
-
options?.forceCommit
|
|
98
|
-
);
|
|
99
|
-
if (transformedChanges.length > 0) {
|
|
100
|
-
await store.saveChanges(docId, transformedChanges);
|
|
101
|
-
}
|
|
102
|
-
if (needsSyntheticCatchup && clientBaseRev === 0) {
|
|
103
|
-
const syntheticCatchup = {
|
|
104
|
-
id: `catchup-${createId(8)}`,
|
|
105
|
-
baseRev: clientBaseRev,
|
|
106
|
-
rev: currentRev,
|
|
107
|
-
ops: [{ op: "replace", path: "", value: currentState }],
|
|
108
|
-
createdAt: serverNow,
|
|
109
|
-
committedAt: serverNow
|
|
110
|
-
};
|
|
111
|
-
return { catchupChanges: [syntheticCatchup], newChanges: transformedChanges };
|
|
112
|
-
}
|
|
113
|
-
return { catchupChanges: committedChanges, newChanges: transformedChanges };
|
|
96
|
+
throw new Error(`commitChanges: exhausted ${MAX_CONFLICT_RETRIES} retries for doc ${docId}`);
|
|
114
97
|
}
|
|
115
98
|
export {
|
|
116
99
|
commitChanges
|
|
@@ -1,18 +1,47 @@
|
|
|
1
1
|
import { OTStoreBackend } from '../../../server/types.js';
|
|
2
|
-
import {
|
|
2
|
+
import { EditableVersionMetadata, VersionMetadata, Change } from '../../../types.js';
|
|
3
3
|
import '../../../json-patch/types.js';
|
|
4
4
|
import '../../../json-patch/JSONPatch.js';
|
|
5
5
|
import '@dabble/delta';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
8
|
+
* Options for creating a version.
|
|
9
|
+
*/
|
|
10
|
+
interface CreateVersionOptions {
|
|
11
|
+
/** The origin of the version. Defaults to 'main'. */
|
|
12
|
+
origin?: 'main' | 'offline-branch';
|
|
13
|
+
/** ID of the parent version in the history chain. */
|
|
14
|
+
parentId?: string;
|
|
15
|
+
/** Optional additional metadata for the version. */
|
|
16
|
+
metadata?: EditableVersionMetadata;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Creates a version for all changes since the last version up to (and including) `endRev`.
|
|
20
|
+
*
|
|
21
|
+
* Looks up the last existing version to determine `startRev`, then loads all
|
|
22
|
+
* changes from there to `endRev` from the change log. This ensures the version
|
|
23
|
+
* covers the complete session, not just a single change.
|
|
24
|
+
*
|
|
25
|
+
* @param store The storage backend.
|
|
26
|
+
* @param docId The document ID.
|
|
27
|
+
* @param endRev The revision to end the version at (inclusive).
|
|
28
|
+
* @param options Options including origin and metadata.
|
|
29
|
+
* @returns The created version metadata, or undefined if no changes were found.
|
|
30
|
+
*/
|
|
31
|
+
declare function createVersionAtRev(store: OTStoreBackend, docId: string, endRev: number, options?: CreateVersionOptions): Promise<VersionMetadata | undefined>;
|
|
32
|
+
/**
|
|
33
|
+
* Creates a new version record from changes.
|
|
34
|
+
*
|
|
35
|
+
* Saves metadata and changes to the store via `store.createVersion`. The store
|
|
36
|
+
* implementation is responsible for building and persisting version state — inline
|
|
37
|
+
* or queued — and must throw if that fails.
|
|
38
|
+
*
|
|
9
39
|
* @param store The storage backend to save the version to.
|
|
10
40
|
* @param docId The document ID.
|
|
11
|
-
* @param
|
|
12
|
-
* @param
|
|
13
|
-
* @param metadata Optional additional metadata for the version.
|
|
41
|
+
* @param changes The changes since the last version.
|
|
42
|
+
* @param options Options including origin and metadata.
|
|
14
43
|
* @returns The created version metadata, or undefined if no changes provided.
|
|
15
44
|
*/
|
|
16
|
-
declare function createVersion(store: OTStoreBackend, docId: string,
|
|
45
|
+
declare function createVersion(store: OTStoreBackend, docId: string, changes: Change[], options?: CreateVersionOptions): Promise<VersionMetadata | undefined>;
|
|
17
46
|
|
|
18
|
-
export { createVersion };
|
|
47
|
+
export { type CreateVersionOptions, createVersion, createVersionAtRev };
|
|
@@ -1,22 +1,41 @@
|
|
|
1
1
|
import "../../../chunk-IZ2YBCUP.js";
|
|
2
2
|
import { createVersionMetadata } from "../../../data/version.js";
|
|
3
|
-
async function
|
|
3
|
+
async function createVersionAtRev(store, docId, endRev, options) {
|
|
4
|
+
const [lastVersion] = await store.listVersions(docId, {
|
|
5
|
+
limit: 1,
|
|
6
|
+
reverse: true,
|
|
7
|
+
orderBy: "endRev"
|
|
8
|
+
});
|
|
9
|
+
const startAfterRev = lastVersion?.endRev ?? 0;
|
|
10
|
+
const changes = await store.listChanges(docId, {
|
|
11
|
+
startAfter: startAfterRev,
|
|
12
|
+
endBefore: endRev + 1
|
|
13
|
+
});
|
|
14
|
+
if (changes.length === 0) return void 0;
|
|
15
|
+
return createVersion(store, docId, changes, {
|
|
16
|
+
...options,
|
|
17
|
+
parentId: options?.parentId ?? lastVersion?.id
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
async function createVersion(store, docId, changes, options) {
|
|
4
21
|
if (changes.length === 0) return;
|
|
5
22
|
const startRev = changes[0].rev;
|
|
6
23
|
if (startRev === void 0) {
|
|
7
24
|
throw new Error(`Client changes must include rev for doc ${docId}.`);
|
|
8
25
|
}
|
|
9
26
|
const sessionMetadata = createVersionMetadata({
|
|
10
|
-
origin: "main",
|
|
27
|
+
origin: options?.origin ?? "main",
|
|
11
28
|
startedAt: changes[0].createdAt,
|
|
12
29
|
endedAt: changes[changes.length - 1].createdAt,
|
|
13
30
|
endRev: changes[changes.length - 1].rev,
|
|
14
31
|
startRev,
|
|
15
|
-
...
|
|
32
|
+
...options?.parentId !== void 0 && { parentId: options.parentId },
|
|
33
|
+
...options?.metadata
|
|
16
34
|
});
|
|
17
|
-
await store.createVersion(docId, sessionMetadata,
|
|
35
|
+
await store.createVersion(docId, sessionMetadata, changes);
|
|
18
36
|
return sessionMetadata;
|
|
19
37
|
}
|
|
20
38
|
export {
|
|
21
|
-
createVersion
|
|
39
|
+
createVersion,
|
|
40
|
+
createVersionAtRev
|
|
22
41
|
};
|
|
@@ -7,10 +7,22 @@ import '@dabble/delta';
|
|
|
7
7
|
/**
|
|
8
8
|
* Retrieves the document state of the version before the given revision and changes after up to that revision or all
|
|
9
9
|
* changes since that version.
|
|
10
|
+
*
|
|
11
|
+
* Note: This is NOT used in the hot path (commitChanges). It's used by explicit operations
|
|
12
|
+
* like captureCurrentVersion and createBranch that need the parsed state.
|
|
13
|
+
*
|
|
10
14
|
* @param docId The document ID.
|
|
11
15
|
* @param rev The revision number. If not provided, the latest state, its revision, and all changes since are returned.
|
|
12
16
|
* @returns The document state at the last version before the revision, its revision number, and all changes up to the specified revision (or all changes if no revision is provided).
|
|
13
17
|
*/
|
|
14
18
|
declare function getSnapshotAtRevision(store: OTStoreBackend, docId: string, rev?: number): Promise<PatchesSnapshot>;
|
|
19
|
+
/**
|
|
20
|
+
* Returns a ReadableStream that streams the snapshot JSON envelope piece-by-piece:
|
|
21
|
+
* `{"state":...,"rev":N,"changes":[...]}`.
|
|
22
|
+
*
|
|
23
|
+
* The version state (typically the largest payload) flows through as a raw
|
|
24
|
+
* string from the store without parsing. Changes are stringified from the array.
|
|
25
|
+
*/
|
|
26
|
+
declare function getSnapshotStream(store: OTStoreBackend, docId: string): Promise<ReadableStream<string>>;
|
|
15
27
|
|
|
16
|
-
export { getSnapshotAtRevision };
|
|
28
|
+
export { getSnapshotAtRevision, getSnapshotStream };
|
|
@@ -1,28 +1,39 @@
|
|
|
1
1
|
import "../../../chunk-IZ2YBCUP.js";
|
|
2
|
-
|
|
2
|
+
import { concatStreams, parseVersionState } from "../../../server/jsonReadable.js";
|
|
3
|
+
async function getLatestMainVersion(store, docId, beforeRev) {
|
|
3
4
|
const versions = await store.listVersions(docId, {
|
|
4
5
|
limit: 1,
|
|
5
6
|
reverse: true,
|
|
6
|
-
startAfter:
|
|
7
|
+
startAfter: beforeRev ? beforeRev + 1 : void 0,
|
|
7
8
|
origin: "main",
|
|
8
9
|
orderBy: "endRev"
|
|
9
10
|
});
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
const version = versions[0];
|
|
12
|
+
return {
|
|
13
|
+
rawState: version ? await store.loadVersionState(docId, version.id) : void 0,
|
|
14
|
+
versionRev: version?.endRev ?? 0
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
async function getSnapshotAtRevision(store, docId, rev) {
|
|
18
|
+
const { rawState, versionRev } = await getLatestMainVersion(store, docId, rev);
|
|
19
|
+
const versionState = rawState ? await parseVersionState(rawState) : null;
|
|
13
20
|
const changesSinceVersion = await store.listChanges(docId, {
|
|
14
21
|
startAfter: versionRev,
|
|
15
22
|
endBefore: rev ? rev + 1 : void 0
|
|
16
23
|
});
|
|
17
24
|
return {
|
|
18
25
|
state: versionState,
|
|
19
|
-
// State from the base version
|
|
20
26
|
rev: versionRev,
|
|
21
|
-
// Revision of the base version's state
|
|
22
27
|
changes: changesSinceVersion
|
|
23
|
-
// Changes that occurred *after* the base version state
|
|
24
28
|
};
|
|
25
29
|
}
|
|
30
|
+
async function getSnapshotStream(store, docId) {
|
|
31
|
+
const { rawState, versionRev } = await getLatestMainVersion(store, docId);
|
|
32
|
+
const statePayload = rawState ?? "null";
|
|
33
|
+
const changes = await store.listChanges(docId, { startAfter: versionRev });
|
|
34
|
+
return concatStreams(`{"state":`, statePayload, `,"rev":${versionRev},"changes":`, JSON.stringify(changes), "}");
|
|
35
|
+
}
|
|
26
36
|
export {
|
|
27
|
-
getSnapshotAtRevision
|
|
37
|
+
getSnapshotAtRevision,
|
|
38
|
+
getSnapshotStream
|
|
28
39
|
};
|
|
@@ -6,20 +6,20 @@ import '@dabble/delta';
|
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Handles offline/large batch versioning logic for multi-batch uploads.
|
|
9
|
-
* Groups changes into sessions, merges with previous batch if needed, and creates/extends versions.
|
|
10
9
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* marked offline.
|
|
10
|
+
* Detects session boundaries from timestamps and creates versions for each session.
|
|
11
|
+
* Changes are never collapsed — they're preserved individually.
|
|
14
12
|
*
|
|
13
|
+
* Each session version gets a `parentId` linking it to the preceding version:
|
|
14
|
+
* - Session 1's parent: the last main-timeline version before the batch's first change
|
|
15
|
+
* - Session N's parent (N > 1): the immediately preceding session's version
|
|
16
|
+
*
|
|
17
|
+
* @param store Store backend for version creation
|
|
18
|
+
* @param sessionTimeoutMillis Timeout for detecting session boundaries
|
|
15
19
|
* @param docId Document ID
|
|
16
|
-
* @param changes The incoming changes
|
|
17
|
-
* @param
|
|
18
|
-
* @param batchId The batch identifier
|
|
19
|
-
* @param origin The origin to use for created versions (default: 'offline-branch')
|
|
20
|
-
* @param maxStorageBytes If set, break collapsed changes that exceed this size
|
|
21
|
-
* @returns The changes (collapsed into one if divergent, unchanged if fast-forward)
|
|
20
|
+
* @param changes The incoming changes
|
|
21
|
+
* @param origin The origin for version metadata
|
|
22
22
|
*/
|
|
23
|
-
declare function handleOfflineSessionsAndBatches(store: OTStoreBackend, sessionTimeoutMillis: number, docId: string, changes: Change[],
|
|
23
|
+
declare function handleOfflineSessionsAndBatches(store: OTStoreBackend, sessionTimeoutMillis: number, docId: string, changes: Change[], origin?: 'main' | 'offline-branch'): Promise<void>;
|
|
24
24
|
|
|
25
25
|
export { handleOfflineSessionsAndBatches };
|