@dabble/patches 0.5.1 → 0.5.3
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/client/makeChange.js +2 -2
- package/dist/algorithms/server/commitChanges.d.ts +4 -3
- package/dist/algorithms/server/commitChanges.js +27 -8
- package/dist/algorithms/server/createVersion.js +3 -2
- package/dist/algorithms/server/handleOfflineSessionsAndBatches.d.ts +5 -2
- package/dist/algorithms/server/handleOfflineSessionsAndBatches.js +19 -11
- package/dist/algorithms/server/transformIncomingChanges.d.ts +2 -1
- package/dist/algorithms/server/transformIncomingChanges.js +11 -9
- package/dist/algorithms/shared/changeBatching.d.ts +19 -0
- package/dist/algorithms/{client/breakChange.js → shared/changeBatching.js} +56 -3
- package/dist/data/change.js +3 -3
- package/dist/index.d.ts +2 -2
- package/dist/net/PatchesClient.d.ts +3 -2
- package/dist/net/PatchesClient.js +3 -2
- package/dist/net/PatchesSync.js +2 -2
- package/dist/net/index.d.ts +5 -5
- package/dist/net/protocol/types.d.ts +3 -3
- package/dist/net/websocket/RPCServer.d.ts +3 -1
- package/dist/net/websocket/RPCServer.js +3 -2
- package/dist/net/websocket/SignalingService.d.ts +21 -21
- package/dist/net/websocket/SignalingService.js +43 -39
- package/dist/server/PatchesBranchManager.d.ts +2 -1
- package/dist/server/PatchesBranchManager.js +33 -11
- package/dist/server/PatchesServer.d.ts +12 -4
- package/dist/server/PatchesServer.js +8 -3
- package/dist/server/index.d.ts +2 -2
- package/dist/server/types.d.ts +5 -0
- package/dist/types.d.ts +23 -4
- package/dist/utils/dates.d.ts +13 -22
- package/dist/utils/dates.js +18 -28
- package/package.json +1 -1
- package/dist/algorithms/client/batching.d.ts +0 -9
- package/dist/algorithms/client/batching.js +0 -42
- package/dist/algorithms/client/breakChange.d.ts +0 -15
- package/dist/algorithms/client/getJSONByteSize.d.ts +0 -4
- package/dist/algorithms/client/getJSONByteSize.js +0 -13
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import "../../chunk-IZ2YBCUP.js";
|
|
2
2
|
import { createChange } from "../../data/change.js";
|
|
3
3
|
import { createJSONPatch } from "../../json-patch/createJSONPatch.js";
|
|
4
|
-
import {
|
|
4
|
+
import { breakChanges } from "../shared/changeBatching.js";
|
|
5
5
|
import { createStateFromSnapshot } from "./createStateFromSnapshot.js";
|
|
6
6
|
function makeChange(snapshot, mutator, changeMetadata, maxPayloadBytes) {
|
|
7
7
|
const pendingChanges = snapshot.changes;
|
|
@@ -20,7 +20,7 @@ function makeChange(snapshot, mutator, changeMetadata, maxPayloadBytes) {
|
|
|
20
20
|
throw new Error(`Failed to apply change to state during makeChange: ${error}`);
|
|
21
21
|
}
|
|
22
22
|
if (maxPayloadBytes) {
|
|
23
|
-
newChangesArray =
|
|
23
|
+
newChangesArray = breakChanges(newChangesArray, maxPayloadBytes);
|
|
24
24
|
}
|
|
25
25
|
return newChangesArray;
|
|
26
26
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { PatchesStoreBackend } from '../../server/types.js';
|
|
2
|
-
import { ChangeInput, Change } from '../../types.js';
|
|
2
|
+
import { ChangeInput, CommitChangesOptions, Change } from '../../types.js';
|
|
3
3
|
import '../../json-patch/JSONPatch.js';
|
|
4
4
|
import '@dabble/delta';
|
|
5
5
|
import '../../json-patch/types.js';
|
|
@@ -9,10 +9,11 @@ import '../../json-patch/types.js';
|
|
|
9
9
|
* @param docId - The ID of the document.
|
|
10
10
|
* @param changes - The changes to commit.
|
|
11
11
|
* @param originClientId - The ID of the client that initiated the commit.
|
|
12
|
+
* @param options - Optional commit settings.
|
|
12
13
|
* @returns A tuple of [committedChanges, transformedChanges] where:
|
|
13
14
|
* - committedChanges: Changes that were already committed to the server after the client's base revision
|
|
14
15
|
* - transformedChanges: The client's changes after being transformed against concurrent changes
|
|
15
16
|
*/
|
|
16
|
-
declare function commitChanges(store: PatchesStoreBackend, docId: string, changes: ChangeInput[], sessionTimeoutMillis: number): Promise<[Change[], Change[]]>;
|
|
17
|
+
declare function commitChanges(store: PatchesStoreBackend, docId: string, changes: ChangeInput[], sessionTimeoutMillis: number, options?: CommitChangesOptions, maxPayloadBytes?: number): Promise<[Change[], Change[]]>;
|
|
17
18
|
|
|
18
|
-
export { commitChanges };
|
|
19
|
+
export { CommitChangesOptions, commitChanges };
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import "../../chunk-IZ2YBCUP.js";
|
|
2
|
-
import { clampTimestamp,
|
|
2
|
+
import { clampTimestamp, getISO, timestampDiff } from "../../utils/dates.js";
|
|
3
3
|
import { applyChanges } from "../shared/applyChanges.js";
|
|
4
4
|
import { createVersion } from "./createVersion.js";
|
|
5
5
|
import { getSnapshotAtRevision } from "./getSnapshotAtRevision.js";
|
|
6
6
|
import { getStateAtRevision } from "./getStateAtRevision.js";
|
|
7
7
|
import { handleOfflineSessionsAndBatches } from "./handleOfflineSessionsAndBatches.js";
|
|
8
8
|
import { transformIncomingChanges } from "./transformIncomingChanges.js";
|
|
9
|
-
async function commitChanges(store, docId, changes, sessionTimeoutMillis) {
|
|
9
|
+
async function commitChanges(store, docId, changes, sessionTimeoutMillis, options, maxPayloadBytes) {
|
|
10
10
|
if (changes.length === 0) {
|
|
11
11
|
return [[], []];
|
|
12
12
|
}
|
|
@@ -15,7 +15,7 @@ async function commitChanges(store, docId, changes, sessionTimeoutMillis) {
|
|
|
15
15
|
const currentState = applyChanges(initialState, currentChanges);
|
|
16
16
|
const currentRev = currentChanges.at(-1)?.rev ?? initialRev;
|
|
17
17
|
const baseRev = changes[0].baseRev ?? currentRev;
|
|
18
|
-
const serverNow =
|
|
18
|
+
const serverNow = getISO();
|
|
19
19
|
let rev = baseRev + 1;
|
|
20
20
|
changes.forEach((c) => {
|
|
21
21
|
if (c.baseRev == null) c.baseRev = baseRev;
|
|
@@ -24,8 +24,10 @@ async function commitChanges(store, docId, changes, sessionTimeoutMillis) {
|
|
|
24
24
|
}
|
|
25
25
|
if (c.rev == null) c.rev = rev++;
|
|
26
26
|
else rev = c.rev + 1;
|
|
27
|
-
c.committedAt
|
|
28
|
-
|
|
27
|
+
if (!options?.historicalImport || !c.committedAt) {
|
|
28
|
+
c.committedAt = serverNow;
|
|
29
|
+
}
|
|
30
|
+
c.createdAt = c.createdAt ? clampTimestamp(c.createdAt, serverNow) : serverNow;
|
|
29
31
|
});
|
|
30
32
|
if (baseRev > currentRev) {
|
|
31
33
|
throw new Error(
|
|
@@ -39,7 +41,8 @@ async function commitChanges(store, docId, changes, sessionTimeoutMillis) {
|
|
|
39
41
|
);
|
|
40
42
|
}
|
|
41
43
|
const lastChange = currentChanges[currentChanges.length - 1];
|
|
42
|
-
|
|
44
|
+
const compareTime = options?.historicalImport ? changes[0].createdAt : serverNow;
|
|
45
|
+
if (lastChange && timestampDiff(compareTime, lastChange.createdAt) > sessionTimeoutMillis) {
|
|
43
46
|
await createVersion(store, docId, currentState, currentChanges);
|
|
44
47
|
}
|
|
45
48
|
const committedChanges = await store.listChanges(docId, {
|
|
@@ -53,17 +56,33 @@ async function commitChanges(store, docId, changes, sessionTimeoutMillis) {
|
|
|
53
56
|
}
|
|
54
57
|
const isOfflineTimestamp = timestampDiff(serverNow, incomingChanges[0].createdAt) > sessionTimeoutMillis;
|
|
55
58
|
if (isOfflineTimestamp || batchId) {
|
|
59
|
+
const canFastForward = committedChanges.length === 0;
|
|
60
|
+
const origin = options?.historicalImport ? "main" : canFastForward ? "main" : "offline-branch";
|
|
56
61
|
incomingChanges = await handleOfflineSessionsAndBatches(
|
|
57
62
|
store,
|
|
58
63
|
sessionTimeoutMillis,
|
|
59
64
|
docId,
|
|
60
65
|
incomingChanges,
|
|
61
66
|
baseRev,
|
|
62
|
-
batchId
|
|
67
|
+
batchId,
|
|
68
|
+
origin,
|
|
69
|
+
true,
|
|
70
|
+
// isOffline
|
|
71
|
+
maxPayloadBytes
|
|
63
72
|
);
|
|
73
|
+
if (canFastForward) {
|
|
74
|
+
await store.saveChanges(docId, incomingChanges);
|
|
75
|
+
return [[], incomingChanges];
|
|
76
|
+
}
|
|
64
77
|
}
|
|
65
78
|
const stateAtBaseRev = (await getStateAtRevision(store, docId, baseRev)).state;
|
|
66
|
-
const transformedChanges = transformIncomingChanges(
|
|
79
|
+
const transformedChanges = transformIncomingChanges(
|
|
80
|
+
incomingChanges,
|
|
81
|
+
stateAtBaseRev,
|
|
82
|
+
committedChanges,
|
|
83
|
+
currentRev,
|
|
84
|
+
options?.forceCommit
|
|
85
|
+
);
|
|
67
86
|
if (transformedChanges.length > 0) {
|
|
68
87
|
await store.saveChanges(docId, transformedChanges);
|
|
69
88
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import "../../chunk-IZ2YBCUP.js";
|
|
2
2
|
import { createVersionMetadata } from "../../data/version.js";
|
|
3
|
+
import { getISO } from "../../utils/dates.js";
|
|
3
4
|
async function createVersion(store, docId, state, changes, metadata) {
|
|
4
5
|
if (changes.length === 0) return;
|
|
5
6
|
const baseRev = changes[0].baseRev;
|
|
@@ -9,8 +10,8 @@ async function createVersion(store, docId, state, changes, metadata) {
|
|
|
9
10
|
const sessionMetadata = createVersionMetadata({
|
|
10
11
|
origin: "main",
|
|
11
12
|
// Convert client timestamps to UTC for version metadata (enables lexicographic sorting)
|
|
12
|
-
startedAt:
|
|
13
|
-
endedAt:
|
|
13
|
+
startedAt: getISO(changes[0].createdAt),
|
|
14
|
+
endedAt: getISO(changes[changes.length - 1].createdAt),
|
|
14
15
|
rev: changes[changes.length - 1].rev,
|
|
15
16
|
baseRev,
|
|
16
17
|
...metadata
|
|
@@ -11,8 +11,11 @@ import '../../json-patch/types.js';
|
|
|
11
11
|
* @param changes The incoming changes (all with the same batchId)
|
|
12
12
|
* @param baseRev The base revision for the batch
|
|
13
13
|
* @param batchId The batch identifier
|
|
14
|
-
* @
|
|
14
|
+
* @param origin The origin to use for created versions (default: 'offline-branch')
|
|
15
|
+
* @param isOffline Whether these changes were created offline (metadata flag)
|
|
16
|
+
* @param maxPayloadBytes If set, break collapsed changes that exceed this size
|
|
17
|
+
* @returns The changes (collapsed into one if divergent, unchanged if fast-forward)
|
|
15
18
|
*/
|
|
16
|
-
declare function handleOfflineSessionsAndBatches(store: PatchesStoreBackend, sessionTimeoutMillis: number, docId: string, changes: Change[], baseRev: number, batchId?: string): Promise<Change[]>;
|
|
19
|
+
declare function handleOfflineSessionsAndBatches(store: PatchesStoreBackend, sessionTimeoutMillis: number, docId: string, changes: Change[], baseRev: number, batchId?: string, origin?: 'main' | 'offline-branch', isOffline?: boolean, maxPayloadBytes?: number): Promise<Change[]>;
|
|
17
20
|
|
|
18
21
|
export { handleOfflineSessionsAndBatches };
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import "../../chunk-IZ2YBCUP.js";
|
|
2
2
|
import { createSortableId } from "crypto-id";
|
|
3
3
|
import { createVersionMetadata } from "../../data/version.js";
|
|
4
|
-
import { timestampDiff } from "../../utils/dates.js";
|
|
4
|
+
import { getISO, timestampDiff } from "../../utils/dates.js";
|
|
5
5
|
import { applyChanges } from "../shared/applyChanges.js";
|
|
6
|
+
import { breakChanges } from "../shared/changeBatching.js";
|
|
6
7
|
import { getStateAtRevision } from "./getStateAtRevision.js";
|
|
7
|
-
async function handleOfflineSessionsAndBatches(store, sessionTimeoutMillis, docId, changes, baseRev, batchId) {
|
|
8
|
+
async function handleOfflineSessionsAndBatches(store, sessionTimeoutMillis, docId, changes, baseRev, batchId, origin = "offline-branch", isOffline = true, maxPayloadBytes) {
|
|
8
9
|
const groupId = batchId ?? createSortableId();
|
|
9
10
|
const [lastVersion] = await store.listVersions(docId, {
|
|
10
11
|
groupId,
|
|
@@ -30,8 +31,9 @@ async function handleOfflineSessionsAndBatches(store, sessionTimeoutMillis, docI
|
|
|
30
31
|
const isContinuation = !!lastVersion && timestampDiff(sessionChanges[0].createdAt, lastVersion.endedAt) <= sessionTimeoutMillis;
|
|
31
32
|
if (isContinuation) {
|
|
32
33
|
const mergedState = applyChanges(offlineBaseState, sessionChanges);
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
const newEndedAt = getISO(sessionChanges[sessionChanges.length - 1].createdAt);
|
|
35
|
+
const newRev = sessionChanges[sessionChanges.length - 1].rev;
|
|
36
|
+
await store.appendVersionChanges(docId, lastVersion.id, sessionChanges, newEndedAt, newRev, mergedState);
|
|
35
37
|
offlineBaseState = mergedState;
|
|
36
38
|
parentId = lastVersion.parentId;
|
|
37
39
|
} else {
|
|
@@ -39,10 +41,11 @@ async function handleOfflineSessionsAndBatches(store, sessionTimeoutMillis, docI
|
|
|
39
41
|
const sessionMetadata = createVersionMetadata({
|
|
40
42
|
parentId,
|
|
41
43
|
groupId,
|
|
42
|
-
origin
|
|
44
|
+
origin,
|
|
45
|
+
isOffline,
|
|
43
46
|
// Convert client timestamps to UTC for version metadata (enables lexicographic sorting)
|
|
44
|
-
startedAt:
|
|
45
|
-
endedAt:
|
|
47
|
+
startedAt: getISO(sessionChanges[0].createdAt),
|
|
48
|
+
endedAt: getISO(sessionChanges[sessionChanges.length - 1].createdAt),
|
|
46
49
|
rev: sessionChanges[sessionChanges.length - 1].rev,
|
|
47
50
|
baseRev
|
|
48
51
|
});
|
|
@@ -53,12 +56,17 @@ async function handleOfflineSessionsAndBatches(store, sessionTimeoutMillis, docI
|
|
|
53
56
|
}
|
|
54
57
|
}
|
|
55
58
|
}
|
|
56
|
-
|
|
57
|
-
changes.reduce((firstChange, nextChange) => {
|
|
59
|
+
if (origin === "offline-branch") {
|
|
60
|
+
const collapsed = changes.reduce((firstChange, nextChange) => {
|
|
58
61
|
firstChange.ops = [...firstChange.ops, ...nextChange.ops];
|
|
59
62
|
return firstChange;
|
|
60
|
-
})
|
|
61
|
-
|
|
63
|
+
});
|
|
64
|
+
if (maxPayloadBytes) {
|
|
65
|
+
return breakChanges([collapsed], maxPayloadBytes);
|
|
66
|
+
}
|
|
67
|
+
return [collapsed];
|
|
68
|
+
}
|
|
69
|
+
return changes;
|
|
62
70
|
}
|
|
63
71
|
export {
|
|
64
72
|
handleOfflineSessionsAndBatches
|
|
@@ -10,8 +10,9 @@ import '../../json-patch/types.js';
|
|
|
10
10
|
* @param stateAtBaseRev The server state *at the client's baseRev*.
|
|
11
11
|
* @param committedChanges The committed changes that happened *after* the client's baseRev.
|
|
12
12
|
* @param currentRev The current/latest revision number (these changes will have their `rev` set > `currentRev`).
|
|
13
|
+
* @param forceCommit If true, skip filtering of no-op changes (useful for migrations).
|
|
13
14
|
* @returns The transformed changes.
|
|
14
15
|
*/
|
|
15
|
-
declare function transformIncomingChanges(changes: Change[], stateAtBaseRev: any, committedChanges: Change[], currentRev: number): Change[];
|
|
16
|
+
declare function transformIncomingChanges(changes: Change[], stateAtBaseRev: any, committedChanges: Change[], currentRev: number, forceCommit?: boolean): Change[];
|
|
16
17
|
|
|
17
18
|
export { transformIncomingChanges };
|
|
@@ -1,24 +1,26 @@
|
|
|
1
1
|
import "../../chunk-IZ2YBCUP.js";
|
|
2
2
|
import { applyPatch } from "../../json-patch/applyPatch.js";
|
|
3
3
|
import { transformPatch } from "../../json-patch/transformPatch.js";
|
|
4
|
-
function transformIncomingChanges(changes, stateAtBaseRev, committedChanges, currentRev) {
|
|
4
|
+
function transformIncomingChanges(changes, stateAtBaseRev, committedChanges, currentRev, forceCommit = false) {
|
|
5
5
|
const committedOps = committedChanges.flatMap((c) => c.ops);
|
|
6
6
|
let state = stateAtBaseRev;
|
|
7
7
|
let rev = currentRev + 1;
|
|
8
8
|
return changes.map((change) => {
|
|
9
9
|
const transformedOps = transformPatch(stateAtBaseRev, committedOps, change.ops);
|
|
10
|
-
if (transformedOps.length === 0) {
|
|
10
|
+
if (transformedOps.length === 0 && !forceCommit) {
|
|
11
11
|
return null;
|
|
12
12
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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);
|
|
17
22
|
return null;
|
|
18
23
|
}
|
|
19
|
-
} catch (error) {
|
|
20
|
-
console.error(`Error applying change ${change.id} to state:`, error);
|
|
21
|
-
return null;
|
|
22
24
|
}
|
|
23
25
|
return { ...change, rev: rev++, ops: transformedOps };
|
|
24
26
|
}).filter(Boolean);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Change } from '../../types.js';
|
|
2
|
+
import '../../json-patch/JSONPatch.js';
|
|
3
|
+
import '@dabble/delta';
|
|
4
|
+
import '../../json-patch/types.js';
|
|
5
|
+
|
|
6
|
+
/** Estimate JSON string byte size. */
|
|
7
|
+
declare function getJSONByteSize(data: unknown): number;
|
|
8
|
+
/**
|
|
9
|
+
* Break changes into smaller changes so that each change's JSON string size never exceeds `maxBytes`.
|
|
10
|
+
*
|
|
11
|
+
* - Splits first by JSON-Patch *ops*
|
|
12
|
+
* - If an individual op is still too big and is a "@txt" op,
|
|
13
|
+
* split its Delta payload into smaller Deltas
|
|
14
|
+
*/
|
|
15
|
+
declare function breakChanges(changes: Change[], maxBytes: number): Change[];
|
|
16
|
+
/** Break changes into batches based on maxPayloadBytes. */
|
|
17
|
+
declare function breakChangesIntoBatches(changes: Change[], maxPayloadBytes?: number): Change[][];
|
|
18
|
+
|
|
19
|
+
export { breakChanges, breakChangesIntoBatches, getJSONByteSize };
|
|
@@ -1,7 +1,58 @@
|
|
|
1
1
|
import "../../chunk-IZ2YBCUP.js";
|
|
2
|
+
import { createId } from "crypto-id";
|
|
2
3
|
import { createChange } from "../../data/change.js";
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
function getJSONByteSize(data) {
|
|
5
|
+
try {
|
|
6
|
+
const stringified = JSON.stringify(data);
|
|
7
|
+
return stringified ? new TextEncoder().encode(stringified).length : 0;
|
|
8
|
+
} catch (e) {
|
|
9
|
+
console.error("Error calculating JSON size:", e);
|
|
10
|
+
throw new Error("Error calculating JSON size: " + e);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function breakChanges(changes, maxBytes) {
|
|
14
|
+
const results = [];
|
|
15
|
+
for (const change of changes) {
|
|
16
|
+
results.push(...breakSingleChange(change, maxBytes));
|
|
17
|
+
}
|
|
18
|
+
return results;
|
|
19
|
+
}
|
|
20
|
+
function breakChangesIntoBatches(changes, maxPayloadBytes) {
|
|
21
|
+
if (!maxPayloadBytes || getJSONByteSize(changes) < maxPayloadBytes) {
|
|
22
|
+
return [changes];
|
|
23
|
+
}
|
|
24
|
+
const batchId = createId(12);
|
|
25
|
+
const batches = [];
|
|
26
|
+
let currentBatch = [];
|
|
27
|
+
let currentSize = 2;
|
|
28
|
+
for (const change of changes) {
|
|
29
|
+
const changeWithBatchId = { ...change, batchId };
|
|
30
|
+
const individualActualSize = getJSONByteSize(changeWithBatchId);
|
|
31
|
+
let itemsToProcess;
|
|
32
|
+
if (individualActualSize > maxPayloadBytes) {
|
|
33
|
+
itemsToProcess = breakSingleChange(changeWithBatchId, maxPayloadBytes).map((c) => ({ ...c, batchId }));
|
|
34
|
+
} else {
|
|
35
|
+
itemsToProcess = [changeWithBatchId];
|
|
36
|
+
}
|
|
37
|
+
for (const item of itemsToProcess) {
|
|
38
|
+
const itemActualSize = getJSONByteSize(item);
|
|
39
|
+
const itemSizeForBatching = itemActualSize + (currentBatch.length > 0 ? 1 : 0);
|
|
40
|
+
if (currentBatch.length > 0 && currentSize + itemSizeForBatching > maxPayloadBytes) {
|
|
41
|
+
batches.push(currentBatch);
|
|
42
|
+
currentBatch = [];
|
|
43
|
+
currentSize = 2;
|
|
44
|
+
}
|
|
45
|
+
const actualItemContribution = itemActualSize + (currentBatch.length > 0 ? 1 : 0);
|
|
46
|
+
currentBatch.push(item);
|
|
47
|
+
currentSize += actualItemContribution;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (currentBatch.length > 0) {
|
|
51
|
+
batches.push(currentBatch);
|
|
52
|
+
}
|
|
53
|
+
return batches;
|
|
54
|
+
}
|
|
55
|
+
function breakSingleChange(orig, maxBytes) {
|
|
5
56
|
if (getJSONByteSize(orig) <= maxBytes) return [orig];
|
|
6
57
|
const byOps = [];
|
|
7
58
|
let group = [];
|
|
@@ -205,5 +256,7 @@ function deriveNewChange(origChange, rev, ops) {
|
|
|
205
256
|
return createChange(origChange.baseRev, rev, ops, metadata);
|
|
206
257
|
}
|
|
207
258
|
export {
|
|
208
|
-
|
|
259
|
+
breakChanges,
|
|
260
|
+
breakChangesIntoBatches,
|
|
261
|
+
getJSONByteSize
|
|
209
262
|
};
|
package/dist/data/change.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import "../chunk-IZ2YBCUP.js";
|
|
2
2
|
import { inc } from "alphacounter";
|
|
3
3
|
import { createId } from "crypto-id";
|
|
4
|
-
import {
|
|
4
|
+
import { getLocalISO } from "../utils/dates.js";
|
|
5
5
|
function createChangeId(rev) {
|
|
6
6
|
return inc.from(rev) + createId(4);
|
|
7
7
|
}
|
|
@@ -10,7 +10,7 @@ function createChange(baseRev, rev, ops, metadata) {
|
|
|
10
10
|
return {
|
|
11
11
|
id: createId(8),
|
|
12
12
|
ops: baseRev,
|
|
13
|
-
createdAt:
|
|
13
|
+
createdAt: getLocalISO(),
|
|
14
14
|
...rev
|
|
15
15
|
};
|
|
16
16
|
} else {
|
|
@@ -19,7 +19,7 @@ function createChange(baseRev, rev, ops, metadata) {
|
|
|
19
19
|
baseRev,
|
|
20
20
|
rev,
|
|
21
21
|
ops,
|
|
22
|
-
createdAt:
|
|
22
|
+
createdAt: getLocalISO(),
|
|
23
23
|
...metadata
|
|
24
24
|
};
|
|
25
25
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -17,8 +17,8 @@ export { createPathProxy, pathProxy } from './json-patch/pathProxy.js';
|
|
|
17
17
|
export { transformPatch } from './json-patch/transformPatch.js';
|
|
18
18
|
export { JSONPatch, PathLike, WriteOptions } from './json-patch/JSONPatch.js';
|
|
19
19
|
export { ApplyJSONPatchOptions, JSONPatchOpHandlerMap as JSONPatchCustomTypes, JSONPatchOp } from './json-patch/types.js';
|
|
20
|
-
export { Branch, BranchStatus, Change, ChangeInput, ChangeMutator, EditableBranchMetadata, EditableVersionMetadata, ListChangesOptions, ListVersionsOptions, PatchesSnapshot, PatchesState, PathProxy, SyncingState, VersionMetadata } from './types.js';
|
|
21
|
-
export { clampTimestamp,
|
|
20
|
+
export { Branch, BranchStatus, Change, ChangeInput, ChangeMutator, CommitChangesOptions, EditableBranchMetadata, EditableVersionMetadata, ListChangesOptions, ListVersionsOptions, PatchesSnapshot, PatchesState, PathProxy, SyncingState, VersionMetadata } from './types.js';
|
|
21
|
+
export { clampTimestamp, extractTimezoneOffset, getISO, getLocalISO, getLocalTimezoneOffset, timestampDiff } from './utils/dates.js';
|
|
22
22
|
export { add } from './json-patch/ops/add.js';
|
|
23
23
|
export { copy } from './json-patch/ops/copy.js';
|
|
24
24
|
export { increment } from './json-patch/ops/increment.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Signal } from '../event-signal.js';
|
|
2
|
-
import { Change, PatchesState, ChangeInput, EditableVersionMetadata, ListVersionsOptions, VersionMetadata, PatchesSnapshot } from '../types.js';
|
|
2
|
+
import { Change, PatchesState, ChangeInput, CommitChangesOptions, EditableVersionMetadata, ListVersionsOptions, VersionMetadata, PatchesSnapshot } from '../types.js';
|
|
3
3
|
import { JSONRPCClient } from './protocol/JSONRPCClient.js';
|
|
4
4
|
import { PatchesAPI, ClientTransport } from './protocol/types.js';
|
|
5
5
|
import '../json-patch/JSONPatch.js';
|
|
@@ -52,9 +52,10 @@ declare class PatchesClient implements PatchesAPI {
|
|
|
52
52
|
* Applies a set of client-generated changes to a document on the server.
|
|
53
53
|
* @param docId - The ID of the document.
|
|
54
54
|
* @param changes - An array of changes to apply.
|
|
55
|
+
* @param options - Optional commit settings (e.g., forceCommit for migrations).
|
|
55
56
|
* @returns A promise resolving with the changes as committed by the server (potentially transformed).
|
|
56
57
|
*/
|
|
57
|
-
commitChanges(docId: string, changes: ChangeInput[]): Promise<Change[]>;
|
|
58
|
+
commitChanges(docId: string, changes: ChangeInput[], options?: CommitChangesOptions): Promise<Change[]>;
|
|
58
59
|
/**
|
|
59
60
|
* Deletes a document on the server.
|
|
60
61
|
* @param docId - The ID of the document to delete.
|
|
@@ -60,10 +60,11 @@ class PatchesClient {
|
|
|
60
60
|
* Applies a set of client-generated changes to a document on the server.
|
|
61
61
|
* @param docId - The ID of the document.
|
|
62
62
|
* @param changes - An array of changes to apply.
|
|
63
|
+
* @param options - Optional commit settings (e.g., forceCommit for migrations).
|
|
63
64
|
* @returns A promise resolving with the changes as committed by the server (potentially transformed).
|
|
64
65
|
*/
|
|
65
|
-
async commitChanges(docId, changes) {
|
|
66
|
-
return this.rpc.call("commitChanges", { docId, changes });
|
|
66
|
+
async commitChanges(docId, changes, options) {
|
|
67
|
+
return this.rpc.call("commitChanges", { docId, changes, options });
|
|
67
68
|
}
|
|
68
69
|
/**
|
|
69
70
|
* Deletes a document on the server.
|
package/dist/net/PatchesSync.js
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
var __receiveCommittedChanges_dec, _syncDoc_dec, _init;
|
|
9
9
|
import { isEqual } from "@dabble/delta";
|
|
10
10
|
import { applyCommittedChanges } from "../algorithms/client/applyCommittedChanges.js";
|
|
11
|
-
import {
|
|
11
|
+
import { breakChangesIntoBatches } from "../algorithms/shared/changeBatching.js";
|
|
12
12
|
import { Patches } from "../client/Patches.js";
|
|
13
13
|
import { signal } from "../event-signal.js";
|
|
14
14
|
import { blockable } from "../utils/concurrency.js";
|
|
@@ -185,7 +185,7 @@ class PatchesSync {
|
|
|
185
185
|
if (!pending.length) {
|
|
186
186
|
return;
|
|
187
187
|
}
|
|
188
|
-
const batches =
|
|
188
|
+
const batches = breakChangesIntoBatches(pending, this.maxPayloadBytes);
|
|
189
189
|
for (const batch of batches) {
|
|
190
190
|
if (!this.state.connected) {
|
|
191
191
|
throw new Error("Disconnected during flush");
|
package/dist/net/index.d.ts
CHANGED
|
@@ -10,14 +10,11 @@ export { Access, AuthContext, AuthorizationProvider, allowAll, denyAll } from '.
|
|
|
10
10
|
export { onlineState } from './websocket/onlineState.js';
|
|
11
11
|
export { PatchesWebSocket } from './websocket/PatchesWebSocket.js';
|
|
12
12
|
export { RPCServer, RPCServerOptions } from './websocket/RPCServer.js';
|
|
13
|
-
export { JsonRpcMessage,
|
|
13
|
+
export { JsonRpcMessage, SignalingService } from './websocket/SignalingService.js';
|
|
14
14
|
export { WebSocketServer } from './websocket/WebSocketServer.js';
|
|
15
15
|
export { WebSocketOptions, WebSocketTransport } from './websocket/WebSocketTransport.js';
|
|
16
|
+
export { CommitChangesOptions } from '../types.js';
|
|
16
17
|
import '../event-signal.js';
|
|
17
|
-
import '../types.js';
|
|
18
|
-
import '../json-patch/JSONPatch.js';
|
|
19
|
-
import '@dabble/delta';
|
|
20
|
-
import '../json-patch/types.js';
|
|
21
18
|
import '../client/Patches.js';
|
|
22
19
|
import '../client/PatchesDoc.js';
|
|
23
20
|
import '../client/PatchesStore.js';
|
|
@@ -25,3 +22,6 @@ import '../server/PatchesBranchManager.js';
|
|
|
25
22
|
import '../server/PatchesServer.js';
|
|
26
23
|
import '../server/types.js';
|
|
27
24
|
import '../server/PatchesHistoryManager.js';
|
|
25
|
+
import '../json-patch/JSONPatch.js';
|
|
26
|
+
import '@dabble/delta';
|
|
27
|
+
import '../json-patch/types.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Unsubscriber } from '../../event-signal.js';
|
|
2
|
-
import { PatchesState, Change, ChangeInput, EditableVersionMetadata, ListVersionsOptions, VersionMetadata } from '../../types.js';
|
|
2
|
+
import { PatchesState, Change, ChangeInput, CommitChangesOptions, EditableVersionMetadata, ListVersionsOptions, VersionMetadata } from '../../types.js';
|
|
3
3
|
import '../../json-patch/JSONPatch.js';
|
|
4
4
|
import '@dabble/delta';
|
|
5
5
|
import '../../json-patch/types.js';
|
|
@@ -123,7 +123,7 @@ interface PatchesAPI {
|
|
|
123
123
|
/** Get changes that occurred after a specific revision. */
|
|
124
124
|
getChangesSince(docId: string, rev: number): Promise<Change[]>;
|
|
125
125
|
/** Apply a set of changes from the client to a document. Returns the committed changes. */
|
|
126
|
-
commitChanges(docId: string, changes: ChangeInput[]): Promise<Change[]>;
|
|
126
|
+
commitChanges(docId: string, changes: ChangeInput[], options?: CommitChangesOptions): Promise<Change[]>;
|
|
127
127
|
/** Delete a document. */
|
|
128
128
|
deleteDoc(docId: string): Promise<void>;
|
|
129
129
|
/** Create a new named version snapshot of a document's current state. */
|
|
@@ -153,4 +153,4 @@ interface SignalNotificationParams {
|
|
|
153
153
|
data: any;
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
export type
|
|
156
|
+
export { type AwarenessUpdateNotificationParams, type ClientTransport, CommitChangesOptions, type ConnectionState, type JsonRpcNotification, type JsonRpcRequest, type JsonRpcResponse, type ListOptions, type Message, type PatchesAPI, type PatchesNotificationParams, type ServerTransport, type SignalNotificationParams };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PatchesState, Change, ListVersionsOptions, VersionMetadata, EditableVersionMetadata, ListChangesOptions, Branch } from '../../types.js';
|
|
1
|
+
import { PatchesState, Change, CommitChangesOptions, ListVersionsOptions, VersionMetadata, EditableVersionMetadata, ListChangesOptions, Branch } from '../../types.js';
|
|
2
2
|
import { PatchesBranchManager } from '../../server/PatchesBranchManager.js';
|
|
3
3
|
import { PatchesHistoryManager } from '../../server/PatchesHistoryManager.js';
|
|
4
4
|
import { PatchesServer } from '../../server/PatchesServer.js';
|
|
@@ -64,10 +64,12 @@ declare class RPCServer {
|
|
|
64
64
|
* @param params - The change parameters
|
|
65
65
|
* @param params.docId - The ID of the document
|
|
66
66
|
* @param params.changes - An array of changes to apply
|
|
67
|
+
* @param params.options - Optional commit settings (e.g., forceCommit for migrations)
|
|
67
68
|
*/
|
|
68
69
|
commitChanges(params: {
|
|
69
70
|
docId: string;
|
|
70
71
|
changes: Change[];
|
|
72
|
+
options?: CommitChangesOptions;
|
|
71
73
|
}, ctx?: AuthContext): Promise<Change[]>;
|
|
72
74
|
/**
|
|
73
75
|
* Deletes a document on the server.
|
|
@@ -76,11 +76,12 @@ class RPCServer {
|
|
|
76
76
|
* @param params - The change parameters
|
|
77
77
|
* @param params.docId - The ID of the document
|
|
78
78
|
* @param params.changes - An array of changes to apply
|
|
79
|
+
* @param params.options - Optional commit settings (e.g., forceCommit for migrations)
|
|
79
80
|
*/
|
|
80
81
|
async commitChanges(params, ctx) {
|
|
81
|
-
const { docId, changes } = params;
|
|
82
|
+
const { docId, changes, options } = params;
|
|
82
83
|
await this.assertWrite(ctx, docId, "commitChanges", params);
|
|
83
|
-
const [priorChanges, newChanges] = await this.patches.commitChanges(docId, changes, ctx?.clientId);
|
|
84
|
+
const [priorChanges, newChanges] = await this.patches.commitChanges(docId, changes, options, ctx?.clientId);
|
|
84
85
|
return [...priorChanges, ...newChanges];
|
|
85
86
|
}
|
|
86
87
|
/**
|
|
@@ -7,30 +7,38 @@ import '../../json-patch/types.js';
|
|
|
7
7
|
|
|
8
8
|
/** Union type for all possible JSON-RPC message types */
|
|
9
9
|
type JsonRpcMessage = JsonRpcRequest | JsonRpcResponse;
|
|
10
|
-
/** Function type for sending JSON-RPC messages */
|
|
11
|
-
type SendFn = (message: JsonRpcMessage) => void;
|
|
12
10
|
/**
|
|
13
11
|
* Service that facilitates WebRTC connection establishment by relaying signaling messages.
|
|
14
12
|
* Acts as a central hub for WebRTC peers to exchange connection information.
|
|
15
13
|
*/
|
|
16
|
-
declare class SignalingService {
|
|
17
|
-
|
|
14
|
+
declare abstract class SignalingService {
|
|
15
|
+
protected clients: Set<string>;
|
|
16
|
+
abstract send(id: string, message: JsonRpcMessage): void | Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* Returns the list of all connected client IDs.
|
|
19
|
+
* @returns Array of client IDs
|
|
20
|
+
*/
|
|
21
|
+
getClients(): Promise<Set<string>>;
|
|
22
|
+
/**
|
|
23
|
+
* Sets the list of all connected client IDs.
|
|
24
|
+
* @param clients - Set of client IDs
|
|
25
|
+
*/
|
|
26
|
+
setClients(clients: Set<string>): Promise<void>;
|
|
18
27
|
/**
|
|
19
28
|
* Registers a new client connection with the signaling service.
|
|
20
29
|
* Assigns a unique ID to the client and informs them of other connected peers.
|
|
21
30
|
*
|
|
22
|
-
* @param send - Function to send messages to this client
|
|
23
31
|
* @param id - Optional client ID (generated if not provided)
|
|
24
32
|
* @returns The client's assigned ID
|
|
25
33
|
*/
|
|
26
|
-
onClientConnected(
|
|
34
|
+
onClientConnected(id?: string): Promise<string>;
|
|
27
35
|
/**
|
|
28
36
|
* Handles a client disconnection by removing them from the registry
|
|
29
37
|
* and notifying all other connected clients.
|
|
30
38
|
*
|
|
31
39
|
* @param id - ID of the disconnected client
|
|
32
40
|
*/
|
|
33
|
-
onClientDisconnected(id: string): void
|
|
41
|
+
onClientDisconnected(id: string): Promise<void>;
|
|
34
42
|
/**
|
|
35
43
|
* Handles a signaling message from a client, relaying WebRTC session data
|
|
36
44
|
* between peers to facilitate connection establishment.
|
|
@@ -39,33 +47,25 @@ declare class SignalingService {
|
|
|
39
47
|
* @param message - The JSON-RPC message or its string representation
|
|
40
48
|
* @returns True if the message was a valid signaling message and was handled, false otherwise
|
|
41
49
|
*/
|
|
42
|
-
handleClientMessage(fromId: string, message: string | JsonRpcRequest): boolean
|
|
50
|
+
handleClientMessage(fromId: string, message: string | JsonRpcRequest): Promise<boolean>;
|
|
43
51
|
/**
|
|
44
52
|
* Sends a successful JSON-RPC response to a client.
|
|
45
53
|
*
|
|
46
|
-
* @
|
|
54
|
+
* @protected
|
|
47
55
|
* @param toId - ID of the client to send the response to
|
|
48
56
|
* @param id - Request ID to match in the response
|
|
49
57
|
* @param result - Result data to include in the response
|
|
50
58
|
*/
|
|
51
|
-
|
|
59
|
+
protected respond(toId: string, id: number, result: any): Promise<void>;
|
|
52
60
|
/**
|
|
53
61
|
* Sends an error JSON-RPC response to a client.
|
|
54
62
|
*
|
|
55
|
-
* @
|
|
63
|
+
* @protected
|
|
56
64
|
* @param toId - ID of the client to send the error response to
|
|
57
65
|
* @param id - Request ID to match in the response, or undefined for notifications
|
|
58
66
|
* @param message - Error message to include
|
|
59
67
|
*/
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Broadcasts a message to all connected clients, optionally excluding one.
|
|
63
|
-
*
|
|
64
|
-
* @private
|
|
65
|
-
* @param message - The message to broadcast
|
|
66
|
-
* @param excludeId - Optional ID of a client to exclude from the broadcast
|
|
67
|
-
*/
|
|
68
|
-
private broadcast;
|
|
68
|
+
protected respondError(toId: string, id: number | undefined, message: string): Promise<void>;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
export { type JsonRpcMessage,
|
|
71
|
+
export { type JsonRpcMessage, SignalingService };
|