@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.
Files changed (36) hide show
  1. package/dist/algorithms/client/makeChange.js +2 -2
  2. package/dist/algorithms/server/commitChanges.d.ts +4 -3
  3. package/dist/algorithms/server/commitChanges.js +27 -8
  4. package/dist/algorithms/server/createVersion.js +3 -2
  5. package/dist/algorithms/server/handleOfflineSessionsAndBatches.d.ts +5 -2
  6. package/dist/algorithms/server/handleOfflineSessionsAndBatches.js +19 -11
  7. package/dist/algorithms/server/transformIncomingChanges.d.ts +2 -1
  8. package/dist/algorithms/server/transformIncomingChanges.js +11 -9
  9. package/dist/algorithms/shared/changeBatching.d.ts +19 -0
  10. package/dist/algorithms/{client/breakChange.js → shared/changeBatching.js} +56 -3
  11. package/dist/data/change.js +3 -3
  12. package/dist/index.d.ts +2 -2
  13. package/dist/net/PatchesClient.d.ts +3 -2
  14. package/dist/net/PatchesClient.js +3 -2
  15. package/dist/net/PatchesSync.js +2 -2
  16. package/dist/net/index.d.ts +5 -5
  17. package/dist/net/protocol/types.d.ts +3 -3
  18. package/dist/net/websocket/RPCServer.d.ts +3 -1
  19. package/dist/net/websocket/RPCServer.js +3 -2
  20. package/dist/net/websocket/SignalingService.d.ts +21 -21
  21. package/dist/net/websocket/SignalingService.js +43 -39
  22. package/dist/server/PatchesBranchManager.d.ts +2 -1
  23. package/dist/server/PatchesBranchManager.js +33 -11
  24. package/dist/server/PatchesServer.d.ts +12 -4
  25. package/dist/server/PatchesServer.js +8 -3
  26. package/dist/server/index.d.ts +2 -2
  27. package/dist/server/types.d.ts +5 -0
  28. package/dist/types.d.ts +23 -4
  29. package/dist/utils/dates.d.ts +13 -22
  30. package/dist/utils/dates.js +18 -28
  31. package/package.json +1 -1
  32. package/dist/algorithms/client/batching.d.ts +0 -9
  33. package/dist/algorithms/client/batching.js +0 -42
  34. package/dist/algorithms/client/breakChange.d.ts +0 -15
  35. package/dist/algorithms/client/getJSONByteSize.d.ts +0 -4
  36. 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 { breakChange } from "./breakChange.js";
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 = breakChange(newChangesArray[0], maxPayloadBytes);
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, createServerTimestamp, timestampDiff } from "../../utils/dates.js";
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 = createServerTimestamp();
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 = serverNow;
28
- c.createdAt = clampTimestamp(c.createdAt, serverNow);
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
- if (lastChange && timestampDiff(serverNow, lastChange.createdAt) > sessionTimeoutMillis) {
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(incomingChanges, stateAtBaseRev, committedChanges, currentRev);
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: new Date(changes[0].createdAt).toISOString(),
13
- endedAt: new Date(changes[changes.length - 1].createdAt).toISOString(),
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
- * @returns The collapsed changes for transformation
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
- await store.saveChanges(docId, sessionChanges);
34
- await store.updateVersion(docId, lastVersion.id, {});
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: "offline",
44
+ origin,
45
+ isOffline,
43
46
  // Convert client timestamps to UTC for version metadata (enables lexicographic sorting)
44
- startedAt: new Date(sessionChanges[0].createdAt).toISOString(),
45
- endedAt: new Date(sessionChanges[sessionChanges.length - 1].createdAt).toISOString(),
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
- return [
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
- try {
14
- const previous = state;
15
- state = applyPatch(state, transformedOps, { strict: true });
16
- if (previous === state) {
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
- import { getJSONByteSize } from "./getJSONByteSize.js";
4
- function breakChange(orig, maxBytes) {
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
- breakChange
259
+ breakChanges,
260
+ breakChangesIntoBatches,
261
+ getJSONByteSize
209
262
  };
@@ -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 { createClientTimestamp } from "../utils/dates.js";
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: createClientTimestamp(),
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: createClientTimestamp(),
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, createClientTimestamp, createServerTimestamp, extractTimezoneOffset, formatDateWithOffset, getLocalTimezoneOffset, parseTimestamp, timestampDiff } from './utils/dates.js';
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.
@@ -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 { breakIntoBatches } from "../algorithms/client/batching.js";
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 = breakIntoBatches(pending, this.maxPayloadBytes);
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");
@@ -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, SendFn, SignalingService } from './websocket/SignalingService.js';
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 { AwarenessUpdateNotificationParams, ClientTransport, ConnectionState, JsonRpcNotification, JsonRpcRequest, JsonRpcResponse, ListOptions, Message, PatchesAPI, PatchesNotificationParams, ServerTransport, SignalNotificationParams };
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
- private clients;
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(send: SendFn, id?: string): string;
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
- * @private
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
- private respond;
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
- * @private
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
- private respondError;
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, type SendFn, SignalingService };
71
+ export { type JsonRpcMessage, SignalingService };