@dabble/patches 0.2.32 → 0.3.0

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 (79) hide show
  1. package/dist/algorithms/client/applyCommittedChanges.d.ts +8 -0
  2. package/dist/algorithms/client/applyCommittedChanges.js +40 -0
  3. package/dist/{utils → algorithms/client}/batching.d.ts +1 -1
  4. package/dist/{utils → algorithms/client}/batching.js +2 -2
  5. package/dist/{utils → algorithms/client}/breakChange.d.ts +2 -3
  6. package/dist/algorithms/client/breakChange.js +258 -0
  7. package/dist/algorithms/client/createStateFromSnapshot.d.ts +7 -0
  8. package/dist/algorithms/client/createStateFromSnapshot.js +9 -0
  9. package/dist/algorithms/client/getJSONByteSize.js +12 -0
  10. package/dist/algorithms/client/makeChange.d.ts +3 -0
  11. package/dist/algorithms/client/makeChange.js +37 -0
  12. package/dist/algorithms/server/commitChanges.d.ts +12 -0
  13. package/dist/algorithms/server/commitChanges.js +80 -0
  14. package/dist/algorithms/server/createVersion.d.ts +12 -0
  15. package/dist/algorithms/server/createVersion.js +28 -0
  16. package/dist/algorithms/server/getSnapshotAtRevision.d.ts +10 -0
  17. package/dist/algorithms/server/getSnapshotAtRevision.js +29 -0
  18. package/dist/algorithms/server/getStateAtRevision.d.ts +9 -0
  19. package/dist/algorithms/server/getStateAtRevision.js +18 -0
  20. package/dist/algorithms/server/handleOfflineSessionsAndBatches.d.ts +12 -0
  21. package/dist/algorithms/server/handleOfflineSessionsAndBatches.js +80 -0
  22. package/dist/algorithms/server/transformIncomingChanges.d.ts +11 -0
  23. package/dist/algorithms/server/transformIncomingChanges.js +40 -0
  24. package/dist/algorithms/shared/applyChanges.d.ts +10 -0
  25. package/dist/algorithms/shared/applyChanges.js +17 -0
  26. package/dist/{utils.d.ts → algorithms/shared/rebaseChanges.d.ts} +1 -11
  27. package/dist/{utils.js → algorithms/shared/rebaseChanges.js} +3 -43
  28. package/dist/client/InMemoryStore.d.ts +2 -1
  29. package/dist/client/InMemoryStore.js +9 -3
  30. package/dist/client/IndexedDBStore.d.ts +34 -2
  31. package/dist/client/IndexedDBStore.js +399 -282
  32. package/dist/client/Patches.d.ts +11 -41
  33. package/dist/client/Patches.js +197 -208
  34. package/dist/client/PatchesDoc.d.ts +24 -41
  35. package/dist/client/PatchesDoc.js +57 -214
  36. package/dist/client/PatchesHistoryClient.js +1 -1
  37. package/dist/client/PatchesStore.d.ts +186 -9
  38. package/dist/data/change.d.ts +3 -0
  39. package/dist/data/change.js +20 -0
  40. package/dist/data/version.d.ts +12 -0
  41. package/dist/data/version.js +17 -0
  42. package/dist/json-patch/ops/add.js +1 -1
  43. package/dist/json-patch/ops/move.js +1 -1
  44. package/dist/json-patch/ops/remove.js +1 -1
  45. package/dist/json-patch/ops/replace.js +1 -1
  46. package/dist/json-patch/utils/get.js +0 -1
  47. package/dist/json-patch/utils/log.d.ts +4 -1
  48. package/dist/json-patch/utils/log.js +2 -5
  49. package/dist/json-patch/utils/ops.d.ts +1 -1
  50. package/dist/json-patch/utils/ops.js +4 -1
  51. package/dist/json-patch/utils/paths.js +2 -2
  52. package/dist/json-patch/utils/toArrayIndex.js +1 -1
  53. package/dist/net/PatchesSync.d.ts +55 -24
  54. package/dist/net/PatchesSync.js +336 -258
  55. package/dist/net/websocket/AuthorizationProvider.d.ts +9 -2
  56. package/dist/net/websocket/AuthorizationProvider.js +14 -2
  57. package/dist/net/websocket/PatchesWebSocket.d.ts +2 -2
  58. package/dist/net/websocket/PatchesWebSocket.js +3 -2
  59. package/dist/net/websocket/RPCServer.d.ts +2 -2
  60. package/dist/net/websocket/RPCServer.js +3 -3
  61. package/dist/net/websocket/SignalingService.js +1 -1
  62. package/dist/net/websocket/WebSocketServer.d.ts +1 -1
  63. package/dist/net/websocket/WebSocketServer.js +2 -2
  64. package/dist/net/websocket/WebSocketTransport.js +1 -1
  65. package/dist/net/websocket/onlineState.d.ts +1 -1
  66. package/dist/net/websocket/onlineState.js +8 -2
  67. package/dist/server/PatchesBranchManager.js +9 -16
  68. package/dist/server/PatchesHistoryManager.js +1 -1
  69. package/dist/server/PatchesServer.d.ts +11 -38
  70. package/dist/server/PatchesServer.js +32 -255
  71. package/dist/types.d.ts +8 -6
  72. package/dist/utils/concurrency.d.ts +26 -0
  73. package/dist/utils/concurrency.js +60 -0
  74. package/dist/utils/deferred.d.ts +7 -0
  75. package/dist/utils/deferred.js +23 -0
  76. package/package.json +11 -5
  77. package/dist/utils/breakChange.js +0 -302
  78. package/dist/utils/getJSONByteSize.js +0 -12
  79. /package/dist/{utils → algorithms/client}/getJSONByteSize.d.ts +0 -0
@@ -0,0 +1,80 @@
1
+ import { createSortableId } from 'crypto-id';
2
+ import { createVersionMetadata } from '../../data/version';
3
+ import { applyChanges } from '../shared/applyChanges';
4
+ import { getStateAtRevision } from './getStateAtRevision';
5
+ /**
6
+ * Handles offline/large batch versioning logic for multi-batch uploads.
7
+ * Groups changes into sessions, merges with previous batch if needed, and creates/extends versions.
8
+ * @param docId Document ID
9
+ * @param changes The incoming changes (all with the same batchId)
10
+ * @param baseRev The base revision for the batch
11
+ * @param batchId The batch identifier
12
+ * @returns The collapsed changes for transformation
13
+ */
14
+ export async function handleOfflineSessionsAndBatches(store, sessionTimeoutMillis, docId, changes, baseRev, batchId) {
15
+ // Use batchId as groupId for multi-batch uploads; default offline sessions have no groupId
16
+ const groupId = batchId ?? createSortableId();
17
+ // Find the last version for this groupId (if any)
18
+ const [lastVersion] = await store.listVersions(docId, {
19
+ groupId,
20
+ reverse: true,
21
+ limit: 1,
22
+ });
23
+ let offlineBaseState;
24
+ let parentId;
25
+ if (lastVersion) {
26
+ // Continue from the last version's state
27
+ // loadVersionState returns a PatchState ({state, rev}); extract the .state
28
+ const vs = await store.loadVersionState(docId, lastVersion.id);
29
+ offlineBaseState = vs.state ?? vs;
30
+ parentId = lastVersion.id;
31
+ }
32
+ else {
33
+ // First batch for this batchId: start at baseRev
34
+ offlineBaseState = (await getStateAtRevision(store, docId, baseRev)).state;
35
+ }
36
+ let sessionStartIndex = 0;
37
+ for (let i = 1; i <= changes.length; i++) {
38
+ const isLastChange = i === changes.length;
39
+ const timeDiff = isLastChange ? Infinity : changes[i].created - changes[i - 1].created;
40
+ // Session ends if timeout exceeded OR it's the last change in the batch
41
+ if (timeDiff > sessionTimeoutMillis || isLastChange) {
42
+ const sessionChanges = changes.slice(sessionStartIndex, i);
43
+ if (sessionChanges.length > 0) {
44
+ // Check if this is a continuation of the previous session (merge/extend)
45
+ const isContinuation = !!lastVersion && sessionChanges[0].created - lastVersion.endDate <= sessionTimeoutMillis;
46
+ if (isContinuation) {
47
+ // Merge/extend the existing version
48
+ const mergedState = applyChanges(offlineBaseState, sessionChanges);
49
+ await store.saveChanges(docId, sessionChanges);
50
+ await store.updateVersion(docId, lastVersion.id, {}); // metadata already updated above
51
+ offlineBaseState = mergedState;
52
+ parentId = lastVersion.parentId;
53
+ }
54
+ else {
55
+ // Create a new version for this session
56
+ offlineBaseState = applyChanges(offlineBaseState, sessionChanges);
57
+ const sessionMetadata = createVersionMetadata({
58
+ parentId,
59
+ groupId,
60
+ origin: 'offline',
61
+ startDate: sessionChanges[0].created,
62
+ endDate: sessionChanges[sessionChanges.length - 1].created,
63
+ rev: sessionChanges[sessionChanges.length - 1].rev,
64
+ baseRev,
65
+ });
66
+ await store.createVersion(docId, sessionMetadata, offlineBaseState, sessionChanges);
67
+ parentId = sessionMetadata.id;
68
+ }
69
+ sessionStartIndex = i;
70
+ }
71
+ }
72
+ }
73
+ // Collapse all changes into one for transformation
74
+ return [
75
+ changes.reduce((firstChange, nextChange) => {
76
+ firstChange.ops = [...firstChange.ops, ...nextChange.ops];
77
+ return firstChange;
78
+ }),
79
+ ];
80
+ }
@@ -0,0 +1,11 @@
1
+ import type { Change } from '../../types';
2
+ /**
3
+ * Transforms incoming changes against committed changes that happened *after* the client's baseRev.
4
+ * The state used for transformation should be the server state *at the client's baseRev*.
5
+ * @param changes The incoming changes.
6
+ * @param stateAtBaseRev The server state *at the client's baseRev*.
7
+ * @param committedChanges The committed changes that happened *after* the client's baseRev.
8
+ * @param currentRev The current/latest revision number (these changes will have their `rev` set > `currentRev`).
9
+ * @returns The transformed changes.
10
+ */
11
+ export declare function transformIncomingChanges(changes: Change[], stateAtBaseRev: any, committedChanges: Change[], currentRev: number): Change[];
@@ -0,0 +1,40 @@
1
+ import { applyPatch } from '../../json-patch/applyPatch.js';
2
+ import { transformPatch } from '../../json-patch/transformPatch.js';
3
+ /**
4
+ * Transforms incoming changes against committed changes that happened *after* the client's baseRev.
5
+ * The state used for transformation should be the server state *at the client's baseRev*.
6
+ * @param changes The incoming changes.
7
+ * @param stateAtBaseRev The server state *at the client's baseRev*.
8
+ * @param committedChanges The committed changes that happened *after* the client's baseRev.
9
+ * @param currentRev The current/latest revision number (these changes will have their `rev` set > `currentRev`).
10
+ * @returns The transformed changes.
11
+ */
12
+ export function transformIncomingChanges(changes, stateAtBaseRev, committedChanges, currentRev) {
13
+ const committedOps = committedChanges.flatMap(c => c.ops);
14
+ let state = stateAtBaseRev;
15
+ let rev = currentRev + 1;
16
+ // Apply transformation based on state at baseRev
17
+ return changes
18
+ .map(change => {
19
+ // Transform the incoming change's ops against the ops committed since baseRev
20
+ const transformedOps = transformPatch(stateAtBaseRev, committedOps, change.ops);
21
+ if (transformedOps.length === 0) {
22
+ return null; // Change is obsolete after transformation
23
+ }
24
+ try {
25
+ const previous = state;
26
+ state = applyPatch(state, transformedOps, { strict: true });
27
+ if (previous === state) {
28
+ // Changes were no-ops, we can skip this change
29
+ return null;
30
+ }
31
+ }
32
+ catch (error) {
33
+ console.error(`Error applying change ${change.id} to state:`, error);
34
+ return null;
35
+ }
36
+ // Return a new change object with transformed ops and original metadata
37
+ return { ...change, rev: rev++, ops: transformedOps };
38
+ })
39
+ .filter(Boolean);
40
+ }
@@ -0,0 +1,10 @@
1
+ import type { Change } from '../../types.js';
2
+ /**
3
+ * Applies a sequence of changes to a state object.
4
+ * Each change is applied in sequence using the applyPatch function.
5
+ *
6
+ * @param state - The initial state to apply changes to
7
+ * @param changes - Array of changes to apply
8
+ * @returns The state after all changes have been applied
9
+ */
10
+ export declare function applyChanges<T>(state: T, changes: Change[]): T;
@@ -0,0 +1,17 @@
1
+ import { applyPatch } from '../../json-patch/applyPatch.js';
2
+ /**
3
+ * Applies a sequence of changes to a state object.
4
+ * Each change is applied in sequence using the applyPatch function.
5
+ *
6
+ * @param state - The initial state to apply changes to
7
+ * @param changes - Array of changes to apply
8
+ * @returns The state after all changes have been applied
9
+ */
10
+ export function applyChanges(state, changes) {
11
+ if (!changes.length)
12
+ return state;
13
+ for (const change of changes) {
14
+ state = applyPatch(state, change.ops, { strict: true });
15
+ }
16
+ return state;
17
+ }
@@ -1,13 +1,4 @@
1
- import type { Change, Deferred } from './types.js';
2
- /**
3
- * Applies a sequence of changes to a state object.
4
- * Each change is applied in sequence using the applyPatch function.
5
- *
6
- * @param state - The initial state to apply changes to
7
- * @param changes - Array of changes to apply
8
- * @returns The state after all changes have been applied
9
- */
10
- export declare function applyChanges<T>(state: T, changes: Change[]): T;
1
+ import type { Change } from '../../types.js';
11
2
  /**
12
3
  * Rebases local changes against server changes using operational transformation.
13
4
  * This function handles the transformation of local changes to be compatible with server changes
@@ -24,4 +15,3 @@ export declare function applyChanges<T>(state: T, changes: Change[]): T;
24
15
  * @returns Array of rebased local changes with updated revision numbers
25
16
  */
26
17
  export declare function rebaseChanges(serverChanges: Change[], localChanges: Change[]): Change[];
27
- export declare function deferred<T = void>(): Deferred<T>;
@@ -1,21 +1,4 @@
1
- import { applyPatch } from './json-patch/applyPatch.js';
2
- import { JSONPatch } from './json-patch/JSONPatch.js';
3
- /**
4
- * Applies a sequence of changes to a state object.
5
- * Each change is applied in sequence using the applyPatch function.
6
- *
7
- * @param state - The initial state to apply changes to
8
- * @param changes - Array of changes to apply
9
- * @returns The state after all changes have been applied
10
- */
11
- export function applyChanges(state, changes) {
12
- if (!changes.length)
13
- return state;
14
- for (const change of changes) {
15
- state = applyPatch(state, change.ops, { strict: true });
16
- }
17
- return state;
18
- }
1
+ import { JSONPatch } from '../../json-patch/JSONPatch.js';
19
2
  /**
20
3
  * Rebases local changes against server changes using operational transformation.
21
4
  * This function handles the transformation of local changes to be compatible with server changes
@@ -54,7 +37,7 @@ export function rebaseChanges(serverChanges, localChanges) {
54
37
  .map(change => change.ops)
55
38
  .flat());
56
39
  // Rebase local changes against server changes
57
- const base = lastChange.rev;
40
+ const baseRev = lastChange.rev;
58
41
  let rev = lastChange.rev;
59
42
  return filteredLocalChanges
60
43
  .map(change => {
@@ -62,30 +45,7 @@ export function rebaseChanges(serverChanges, localChanges) {
62
45
  const ops = transformPatch.transform(change.ops).ops;
63
46
  if (!ops.length)
64
47
  return null;
65
- return { ...change, base, rev, ops };
48
+ return { ...change, baseRev, rev, ops };
66
49
  })
67
50
  .filter(Boolean);
68
51
  }
69
- export function deferred() {
70
- let resolve;
71
- let reject;
72
- let _status = 'pending';
73
- const promise = new Promise((_resolve, _reject) => {
74
- resolve = (value) => {
75
- _resolve(value);
76
- _status = 'fulfilled';
77
- };
78
- reject = (reason) => {
79
- _reject(reason);
80
- _status = 'rejected';
81
- };
82
- });
83
- return {
84
- promise,
85
- resolve,
86
- reject,
87
- get status() {
88
- return _status;
89
- },
90
- };
91
- }
@@ -12,8 +12,9 @@ export declare class InMemoryStore implements PatchesStore {
12
12
  getLastRevs(docId: string): Promise<[number, number]>;
13
13
  listDocs(includeDeleted?: boolean): Promise<TrackedDoc[]>;
14
14
  saveDoc(docId: string, snapshot: PatchesState): Promise<void>;
15
- savePendingChange(docId: string, change: Change): Promise<void>;
15
+ savePendingChanges(docId: string, changes: Change[]): Promise<void>;
16
16
  saveCommittedChanges(docId: string, changes: Change[], sentPendingRange?: [number, number]): Promise<void>;
17
+ replacePendingChanges(docId: string, changes: Change[]): Promise<void>;
17
18
  trackDocs(docIds: string[]): Promise<void>;
18
19
  untrackDocs(docIds: string[]): Promise<void>;
19
20
  deleteDoc(docId: string): Promise<void>;
@@ -1,5 +1,5 @@
1
+ import { applyChanges } from '../algorithms/shared/applyChanges.js';
1
2
  import { transformPatch } from '../json-patch/transformPatch.js';
2
- import { applyChanges } from '../utils.js';
3
3
  /**
4
4
  * A trivial in‑memory implementation of OfflineStore (soon PatchesStore).
5
5
  * All data lives in JS objects – nothing survives a page reload.
@@ -55,11 +55,11 @@ export class InMemoryStore {
55
55
  async saveDoc(docId, snapshot) {
56
56
  this.docs.set(docId, { snapshot, committed: [], pending: [] });
57
57
  }
58
- async savePendingChange(docId, change) {
58
+ async savePendingChanges(docId, changes) {
59
59
  const buf = this.docs.get(docId) ?? { committed: [], pending: [] };
60
60
  if (!this.docs.has(docId))
61
61
  this.docs.set(docId, buf);
62
- buf.pending.push(change);
62
+ buf.pending.push(...changes);
63
63
  }
64
64
  async saveCommittedChanges(docId, changes, sentPendingRange) {
65
65
  const buf = this.docs.get(docId) ?? { committed: [], pending: [] };
@@ -71,6 +71,12 @@ export class InMemoryStore {
71
71
  buf.pending = buf.pending.filter(p => p.rev < min || p.rev > max);
72
72
  }
73
73
  }
74
+ async replacePendingChanges(docId, changes) {
75
+ const buf = this.docs.get(docId) ?? { committed: [], pending: [] };
76
+ if (!this.docs.has(docId))
77
+ this.docs.set(docId, buf);
78
+ buf.pending = [...changes];
79
+ }
74
80
  // ─── Metadata / Tracking ───────────────────────────────────────────
75
81
  async trackDocs(docIds) {
76
82
  for (const docId of docIds) {
@@ -47,15 +47,34 @@ export declare class IndexedDBStore implements PatchesStore {
47
47
  * Completely remove all data for this docId and mark it as deleted (tombstone).
48
48
  */
49
49
  deleteDoc(docId: string): Promise<void>;
50
+ /**
51
+ * Confirm the deletion of a document.
52
+ * @param docId - The ID of the document to delete.
53
+ */
50
54
  confirmDeleteDoc(docId: string): Promise<void>;
55
+ /**
56
+ * Save a document's state to the store.
57
+ * @param docId - The ID of the document to save.
58
+ * @param docState - The state of the document to save.
59
+ */
51
60
  saveDoc(docId: string, docState: PatchesState): Promise<void>;
52
61
  /**
53
62
  * Append an array of local changes to the pending queue.
54
63
  * Called *before* you attempt to send them to the server.
55
64
  */
56
- savePendingChange(docId: string, change: Change): Promise<void>;
57
- /** Read back all pending changes for this docId (in order). */
65
+ savePendingChanges(docId: string, changes: Change[]): Promise<void>;
66
+ /**
67
+ * Read back all pending changes for this docId (in order).
68
+ * @param docId - The ID of the document to get the pending changes for.
69
+ * @returns The pending changes.
70
+ */
58
71
  getPendingChanges(docId: string): Promise<Change[]>;
72
+ /**
73
+ * Replace all pending changes for a document (used after rebasing).
74
+ * @param docId - The ID of the document to replace the pending changes for.
75
+ * @param changes - The changes to replace the pending changes with.
76
+ */
77
+ replacePendingChanges(docId: string, changes: Change[]): Promise<void>;
59
78
  /**
60
79
  * Store server‐confirmed changes. Will:
61
80
  * - persist them in the committedChanges store
@@ -67,8 +86,21 @@ export declare class IndexedDBStore implements PatchesStore {
67
86
  * from the server in response to a patchesDoc request.
68
87
  */
69
88
  saveCommittedChanges(docId: string, changes: Change[], sentPendingRange?: [number, number]): Promise<void>;
89
+ /**
90
+ * List all documents in the store.
91
+ * @param includeDeleted - Whether to include deleted documents.
92
+ * @returns The list of documents.
93
+ */
70
94
  listDocs(includeDeleted?: boolean): Promise<TrackedDoc[]>;
95
+ /**
96
+ * Track a document.
97
+ * @param docIds - The IDs of the documents to track.
98
+ */
71
99
  trackDocs(docIds: string[]): Promise<void>;
100
+ /**
101
+ * Untrack a document.
102
+ * @param docIds - The IDs of the documents to untrack.
103
+ */
72
104
  untrackDocs(docIds: string[]): Promise<void>;
73
105
  /**
74
106
  * Tell me the last committed revision you have *and* the highest