@dabble/patches 0.1.1

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 (120) hide show
  1. package/README.md +632 -0
  2. package/dist/client/PatchDoc.d.ts +85 -0
  3. package/dist/client/PatchDoc.js +299 -0
  4. package/dist/client/index.d.ts +2 -0
  5. package/dist/client/index.js +1 -0
  6. package/dist/event-signal.d.ts +31 -0
  7. package/dist/event-signal.js +40 -0
  8. package/dist/index.d.ts +2 -0
  9. package/dist/index.js +1 -0
  10. package/dist/json-patch/JSONPatch.d.ts +126 -0
  11. package/dist/json-patch/JSONPatch.js +221 -0
  12. package/dist/json-patch/applyPatch.d.ts +11 -0
  13. package/dist/json-patch/applyPatch.js +37 -0
  14. package/dist/json-patch/composePatch.d.ts +2 -0
  15. package/dist/json-patch/composePatch.js +38 -0
  16. package/dist/json-patch/createJSONPatch.d.ts +35 -0
  17. package/dist/json-patch/createJSONPatch.js +41 -0
  18. package/dist/json-patch/index.d.ts +9 -0
  19. package/dist/json-patch/index.js +8 -0
  20. package/dist/json-patch/invertPatch.d.ts +2 -0
  21. package/dist/json-patch/invertPatch.js +31 -0
  22. package/dist/json-patch/ops/add.d.ts +2 -0
  23. package/dist/json-patch/ops/add.js +52 -0
  24. package/dist/json-patch/ops/bitmask.d.ts +14 -0
  25. package/dist/json-patch/ops/bitmask.js +48 -0
  26. package/dist/json-patch/ops/copy.d.ts +2 -0
  27. package/dist/json-patch/ops/copy.js +34 -0
  28. package/dist/json-patch/ops/increment.d.ts +5 -0
  29. package/dist/json-patch/ops/increment.js +21 -0
  30. package/dist/json-patch/ops/index.d.ts +22 -0
  31. package/dist/json-patch/ops/index.js +25 -0
  32. package/dist/json-patch/ops/move.d.ts +2 -0
  33. package/dist/json-patch/ops/move.js +211 -0
  34. package/dist/json-patch/ops/remove.d.ts +2 -0
  35. package/dist/json-patch/ops/remove.js +31 -0
  36. package/dist/json-patch/ops/replace.d.ts +2 -0
  37. package/dist/json-patch/ops/replace.js +44 -0
  38. package/dist/json-patch/ops/test.d.ts +2 -0
  39. package/dist/json-patch/ops/test.js +22 -0
  40. package/dist/json-patch/ops/text.d.ts +2 -0
  41. package/dist/json-patch/ops/text.js +57 -0
  42. package/dist/json-patch/patchProxy.d.ts +41 -0
  43. package/dist/json-patch/patchProxy.js +125 -0
  44. package/dist/json-patch/state.d.ts +2 -0
  45. package/dist/json-patch/state.js +8 -0
  46. package/dist/json-patch/transformPatch.d.ts +19 -0
  47. package/dist/json-patch/transformPatch.js +37 -0
  48. package/dist/json-patch/types.d.ts +52 -0
  49. package/dist/json-patch/types.js +1 -0
  50. package/dist/json-patch/utils/deepEqual.d.ts +1 -0
  51. package/dist/json-patch/utils/deepEqual.js +33 -0
  52. package/dist/json-patch/utils/exit.d.ts +2 -0
  53. package/dist/json-patch/utils/exit.js +4 -0
  54. package/dist/json-patch/utils/get.d.ts +2 -0
  55. package/dist/json-patch/utils/get.js +6 -0
  56. package/dist/json-patch/utils/getOpData.d.ts +2 -0
  57. package/dist/json-patch/utils/getOpData.js +10 -0
  58. package/dist/json-patch/utils/getType.d.ts +3 -0
  59. package/dist/json-patch/utils/getType.js +6 -0
  60. package/dist/json-patch/utils/index.d.ts +14 -0
  61. package/dist/json-patch/utils/index.js +14 -0
  62. package/dist/json-patch/utils/log.d.ts +2 -0
  63. package/dist/json-patch/utils/log.js +7 -0
  64. package/dist/json-patch/utils/ops.d.ts +14 -0
  65. package/dist/json-patch/utils/ops.js +103 -0
  66. package/dist/json-patch/utils/paths.d.ts +9 -0
  67. package/dist/json-patch/utils/paths.js +53 -0
  68. package/dist/json-patch/utils/pluck.d.ts +5 -0
  69. package/dist/json-patch/utils/pluck.js +30 -0
  70. package/dist/json-patch/utils/shallowCopy.d.ts +1 -0
  71. package/dist/json-patch/utils/shallowCopy.js +20 -0
  72. package/dist/json-patch/utils/softWrites.d.ts +7 -0
  73. package/dist/json-patch/utils/softWrites.js +18 -0
  74. package/dist/json-patch/utils/toArrayIndex.d.ts +1 -0
  75. package/dist/json-patch/utils/toArrayIndex.js +12 -0
  76. package/dist/json-patch/utils/toKeys.d.ts +1 -0
  77. package/dist/json-patch/utils/toKeys.js +15 -0
  78. package/dist/json-patch/utils/updateArrayIndexes.d.ts +5 -0
  79. package/dist/json-patch/utils/updateArrayIndexes.js +38 -0
  80. package/dist/json-patch/utils/updateArrayPath.d.ts +5 -0
  81. package/dist/json-patch/utils/updateArrayPath.js +45 -0
  82. package/dist/net/AbstractTransport.d.ts +47 -0
  83. package/dist/net/AbstractTransport.js +37 -0
  84. package/dist/net/PatchesOfflineFirst.d.ts +3 -0
  85. package/dist/net/PatchesOfflineFirst.js +3 -0
  86. package/dist/net/PatchesRealtime.d.ts +90 -0
  87. package/dist/net/PatchesRealtime.js +257 -0
  88. package/dist/net/index.d.ts +9 -0
  89. package/dist/net/index.js +8 -0
  90. package/dist/net/protocol/JSONRPCClient.d.ts +55 -0
  91. package/dist/net/protocol/JSONRPCClient.js +106 -0
  92. package/dist/net/protocol/types.d.ts +142 -0
  93. package/dist/net/protocol/types.js +1 -0
  94. package/dist/net/webrtc/WebRTCAwareness.d.ts +81 -0
  95. package/dist/net/webrtc/WebRTCAwareness.js +119 -0
  96. package/dist/net/webrtc/WebRTCTransport.d.ts +80 -0
  97. package/dist/net/webrtc/WebRTCTransport.js +157 -0
  98. package/dist/net/websocket/PatchesWebSocket.d.ts +107 -0
  99. package/dist/net/websocket/PatchesWebSocket.js +144 -0
  100. package/dist/net/websocket/SignalingService.d.ts +91 -0
  101. package/dist/net/websocket/SignalingService.js +140 -0
  102. package/dist/net/websocket/WebSocketTransport.d.ts +47 -0
  103. package/dist/net/websocket/WebSocketTransport.js +138 -0
  104. package/dist/persist/IndexedDBStore.d.ts +72 -0
  105. package/dist/persist/IndexedDBStore.js +283 -0
  106. package/dist/persist/index.d.ts +2 -0
  107. package/dist/persist/index.js +1 -0
  108. package/dist/server/BranchManager.d.ts +40 -0
  109. package/dist/server/BranchManager.js +138 -0
  110. package/dist/server/HistoryManager.d.ts +63 -0
  111. package/dist/server/HistoryManager.js +92 -0
  112. package/dist/server/PatchServer.d.ts +129 -0
  113. package/dist/server/PatchServer.js +358 -0
  114. package/dist/server/index.d.ts +4 -0
  115. package/dist/server/index.js +3 -0
  116. package/dist/types.d.ts +158 -0
  117. package/dist/types.js +1 -0
  118. package/dist/utils.d.ts +36 -0
  119. package/dist/utils.js +83 -0
  120. package/package.json +78 -0
@@ -0,0 +1,85 @@
1
+ import type { Change, PatchSnapshot } from '../types.js';
2
+ /**
3
+ * Represents a document synchronized using JSON patches.
4
+ * Manages committed state, pending (local-only) changes, and
5
+ * changes currently being sent to the server.
6
+ */
7
+ export declare class PatchDoc<T extends object> {
8
+ private _state;
9
+ private _committedState;
10
+ private _committedRev;
11
+ private _pendingChanges;
12
+ private _sendingChanges;
13
+ private _changeMetadata;
14
+ /** Subscribe to be notified before local state changes. */
15
+ readonly onBeforeChange: import("../event-signal.js").Signal<(change: Change) => void>;
16
+ /** Subscribe to be notified after local state changes are applied. */
17
+ readonly onChange: import("../event-signal.js").Signal<(change: Change) => void>;
18
+ /** Subscribe to be notified whenever state changes from any source. */
19
+ readonly onUpdate: import("../event-signal.js").Signal<(newState: T) => void>;
20
+ /**
21
+ * Creates an instance of PatchDoc.
22
+ * @param initialState Optional initial state.
23
+ * @param initialMetadata Optional metadata to add to generated changes.
24
+ */
25
+ constructor(initialState?: T, initialMetadata?: Record<string, any>);
26
+ /** Current local state (committed + sending + pending). */
27
+ get state(): T;
28
+ /** Last committed revision number from the server. */
29
+ get committedRev(): number;
30
+ /** Are there changes currently awaiting server confirmation? */
31
+ get isSending(): boolean;
32
+ /** Are there local changes that haven't been sent yet? */
33
+ get hasPending(): boolean;
34
+ /**
35
+ * Basic export for potential persistence (may lose sending state).
36
+ */
37
+ export(): PatchSnapshot<T>;
38
+ /**
39
+ * Basic import for potential persistence.
40
+ * Resets any `_sendingChanges` state and treats all imported changes as pending.
41
+ */
42
+ import(snapshot: PatchSnapshot<T>): void;
43
+ /**
44
+ * Sets metadata to be added to future changes.
45
+ * @param metadata Metadata to be added to future changes.
46
+ */
47
+ setChangeMetadata(metadata: Record<string, any>): void;
48
+ /**
49
+ * Applies an update to the local state, generating a patch and adding it to pending changes.
50
+ * @param mutator Function modifying a draft state.
51
+ * @returns The generated Change object or null if no changes occurred.
52
+ */
53
+ change(mutator: (draft: T) => void): Change | null;
54
+ /**
55
+ * Retrieves pending changes and marks them as sending.
56
+ * @returns Array of changes ready to be sent to the server.
57
+ * @throws Error if changes are already being sent.
58
+ */
59
+ getUpdatesForServer(): Change[];
60
+ /**
61
+ * Processes the server's response to a batch of changes sent via `getUpdatesForServer`.
62
+ * @param serverCommit The array of committed changes from the server.
63
+ * Expected to be empty (`[]`) if the sent batch was a no-op,
64
+ * or contain a single `Change` object representing the batch commit.
65
+ * @throws Error if the input format is unexpected or application fails.
66
+ */
67
+ applyServerConfirmation(serverCommit: Change[]): void;
68
+ /**
69
+ * Applies incoming changes from the server that were *not* initiated by this client.
70
+ * @param externalServerChanges An array of sequential changes from the server.
71
+ */
72
+ applyExternalServerUpdate(externalServerChanges: Change[]): void;
73
+ /**
74
+ * Handles the scenario where sending changes to the server failed.
75
+ * Moves the changes that were in the process of being sent back to the
76
+ * beginning of the pending queue to be retried later.
77
+ */
78
+ handleSendFailure(): void;
79
+ /** Recalculates _state from _committedState + _sendingChanges + _pendingChanges */
80
+ private _recalculateLocalState;
81
+ /**
82
+ * @deprecated Use export() - kept for backward compatibility if needed.
83
+ */
84
+ toJSON(): PatchSnapshot<T>;
85
+ }
@@ -0,0 +1,299 @@
1
+ import { createId } from 'crypto-id';
2
+ import { signal } from '../event-signal.js';
3
+ import { createJSONPatch } from '../json-patch/createJSONPatch.js';
4
+ import { applyChanges, rebaseChanges } from '../utils.js';
5
+ /**
6
+ * Represents a document synchronized using JSON patches.
7
+ * Manages committed state, pending (local-only) changes, and
8
+ * changes currently being sent to the server.
9
+ */
10
+ export class PatchDoc {
11
+ /**
12
+ * Creates an instance of PatchDoc.
13
+ * @param initialState Optional initial state.
14
+ * @param initialMetadata Optional metadata to add to generated changes.
15
+ */
16
+ constructor(initialState = {}, initialMetadata = {}) {
17
+ this._pendingChanges = []; // Changes made locally, not yet sent
18
+ this._sendingChanges = []; // Changes sent to server, awaiting confirmation
19
+ this._changeMetadata = {};
20
+ /** Subscribe to be notified before local state changes. */
21
+ this.onBeforeChange = signal();
22
+ /** Subscribe to be notified after local state changes are applied. */
23
+ this.onChange = signal();
24
+ /** Subscribe to be notified whenever state changes from any source. */
25
+ this.onUpdate = signal();
26
+ // Use structuredClone for robust deep cloning
27
+ this._committedState = structuredClone(initialState);
28
+ this._state = structuredClone(initialState);
29
+ this._committedRev = 0;
30
+ this._changeMetadata = initialMetadata;
31
+ }
32
+ /** Current local state (committed + sending + pending). */
33
+ get state() {
34
+ return this._state;
35
+ }
36
+ /** Last committed revision number from the server. */
37
+ get committedRev() {
38
+ return this._committedRev;
39
+ }
40
+ /** Are there changes currently awaiting server confirmation? */
41
+ get isSending() {
42
+ return this._sendingChanges.length > 0;
43
+ }
44
+ /** Are there local changes that haven't been sent yet? */
45
+ get hasPending() {
46
+ return this._pendingChanges.length > 0;
47
+ }
48
+ /**
49
+ * Basic export for potential persistence (may lose sending state).
50
+ */
51
+ export() {
52
+ return {
53
+ state: this._committedState,
54
+ rev: this._committedRev,
55
+ // Includes sending and pending changes. On import, all become pending.
56
+ changes: [...this._sendingChanges, ...this._pendingChanges],
57
+ };
58
+ }
59
+ /**
60
+ * Basic import for potential persistence.
61
+ * Resets any `_sendingChanges` state and treats all imported changes as pending.
62
+ */
63
+ import(snapshot) {
64
+ this._committedState = structuredClone(snapshot.state); // Use structuredClone
65
+ this._committedRev = snapshot.rev;
66
+ this._pendingChanges = snapshot.changes ?? []; // All imported changes become pending
67
+ this._sendingChanges = []; // Reset sending state on import
68
+ this._recalculateLocalState();
69
+ this.onUpdate.emit(this._state);
70
+ }
71
+ /**
72
+ * Sets metadata to be added to future changes.
73
+ * @param metadata Metadata to be added to future changes.
74
+ */
75
+ setChangeMetadata(metadata) {
76
+ this._changeMetadata = metadata;
77
+ }
78
+ /**
79
+ * Applies an update to the local state, generating a patch and adding it to pending changes.
80
+ * @param mutator Function modifying a draft state.
81
+ * @returns The generated Change object or null if no changes occurred.
82
+ */
83
+ change(mutator) {
84
+ const patch = createJSONPatch(this._state, mutator);
85
+ if (patch.ops.length === 0) {
86
+ return null;
87
+ }
88
+ const rev = this._pendingChanges[this._pendingChanges.length - 1]?.rev ??
89
+ this._sendingChanges[this._sendingChanges.length - 1]?.rev ??
90
+ this._committedRev;
91
+ // Note: Client-side 'rev' is just for local ordering and might be removed.
92
+ // It's the baseRev that matters for sending.
93
+ const change = {
94
+ id: createId(),
95
+ ops: patch.ops,
96
+ rev,
97
+ baseRev: this._committedRev, // Based on the last known committed state
98
+ created: Date.now(),
99
+ ...(Object.keys(this._changeMetadata).length > 0 && { metadata: { ...this._changeMetadata } }),
100
+ };
101
+ this.onBeforeChange.emit(change);
102
+ // Apply to local state immediately
103
+ this._state = patch.apply(this._state);
104
+ this._pendingChanges.push(change);
105
+ this.onChange.emit(change);
106
+ this.onUpdate.emit(this._state);
107
+ return change;
108
+ }
109
+ /**
110
+ * Retrieves pending changes and marks them as sending.
111
+ * @returns Array of changes ready to be sent to the server.
112
+ * @throws Error if changes are already being sent.
113
+ */
114
+ getUpdatesForServer() {
115
+ if (this.isSending) {
116
+ // It's generally simpler if the client waits for confirmation before sending more.
117
+ // If overlapping requests are needed, state management becomes much more complex.
118
+ throw new Error('Cannot get updates while previous batch is awaiting confirmation.');
119
+ }
120
+ if (!this.hasPending) {
121
+ return [];
122
+ }
123
+ this._sendingChanges = this._pendingChanges;
124
+ this._pendingChanges = [];
125
+ // Ensure the baseRev is set correctly based on the committedRev *at the time of sending*
126
+ // (The update method already does this, but double-check if logic changes)
127
+ this._sendingChanges.forEach(change => {
128
+ change.baseRev = this._committedRev;
129
+ });
130
+ return this._sendingChanges;
131
+ }
132
+ /**
133
+ * Processes the server's response to a batch of changes sent via `getUpdatesForServer`.
134
+ * @param serverCommit The array of committed changes from the server.
135
+ * Expected to be empty (`[]`) if the sent batch was a no-op,
136
+ * or contain a single `Change` object representing the batch commit.
137
+ * @throws Error if the input format is unexpected or application fails.
138
+ */
139
+ applyServerConfirmation(serverCommit) {
140
+ if (!Array.isArray(serverCommit)) {
141
+ throw new Error('Invalid server confirmation format: Expected an array.');
142
+ }
143
+ if (!this.isSending) {
144
+ console.warn('Received server confirmation but no changes were marked as sending.');
145
+ // Decide how to handle this - ignore? Apply if possible?
146
+ // For now, let's ignore if the server sent something unexpected.
147
+ if (serverCommit.length === 0)
148
+ return; // Ignore empty confirmations if not sending
149
+ // If server sent a commit unexpectedly, it implies a state mismatch. Hard to recover.
150
+ // Maybe apply cautiously if rev matches?
151
+ const commit = serverCommit[0];
152
+ if (commit && commit.rev === this._committedRev + 1) {
153
+ console.warn('Applying unexpected server commit cautiously.');
154
+ // Proceed as if confirmation was expected
155
+ }
156
+ else {
157
+ throw new Error('Received unexpected server commit with mismatching revision.');
158
+ }
159
+ }
160
+ if (serverCommit.length === 0) {
161
+ // Server confirmed the batch was a no-op (transformed away).
162
+ // The client's `_sendingChanges` are effectively discarded.
163
+ console.log('Server confirmed batch as no-op.');
164
+ this._sendingChanges = [];
165
+ // No change to _committedState or _committedRev
166
+ // Rebase any *new* pending changes against the *old* committed state (no server change occurred)
167
+ // Since baseRev didn't change, no rebase needed. Just recalculate state.
168
+ }
169
+ else if (serverCommit.length === 1) {
170
+ // Server confirmed the batch and returned the single resulting commit.
171
+ const committedChange = serverCommit[0];
172
+ if (!committedChange.rev || committedChange.rev <= this._committedRev) {
173
+ throw new Error(`Server commit invalid revision: ${committedChange.rev}, expected > ${this._committedRev}`);
174
+ }
175
+ // if (committedChange.rev !== this._committedRev + 1) {
176
+ // // This indicates a potential issue, maybe missed updates?
177
+ // // Or server batches multiple client requests?
178
+ // // For now, strictly expect +1 unless server protocol changes.
179
+ // console.warn(`Server commit revision ${committedChange.rev} !== expected ${this._committedRev + 1}`);
180
+ // // Decide recovery strategy: request resync? Accept gap?
181
+ // }
182
+ // 1. Apply the server's committed change to our committed state
183
+ try {
184
+ this._committedState = applyChanges(this._committedState, [committedChange]);
185
+ }
186
+ catch (error) {
187
+ console.error('Failed to apply server commit to committed state:', error);
188
+ const errorMessage = error instanceof Error ? error.message : String(error);
189
+ throw new Error(`Critical sync error applying server commit: ${errorMessage}`);
190
+ }
191
+ // 2. Update committed revision
192
+ this._committedRev = committedChange.rev;
193
+ // 3. Discard the confirmed _sendingChanges
194
+ const confirmedSentChanges = this._sendingChanges;
195
+ this._sendingChanges = [];
196
+ // 4. Rebase any *new* pending changes (added after getUpdatesForServer was called)
197
+ // against the change that the server *actually* applied.
198
+ if (this.hasPending) {
199
+ this._pendingChanges = rebaseChanges([committedChange], this._pendingChanges);
200
+ }
201
+ }
202
+ else {
203
+ throw new Error(`Unexpected server confirmation format: Expected 0 or 1 change, received ${serverCommit.length}`);
204
+ }
205
+ // 5. Recalculate the local state from the new committed state + rebased pending changes
206
+ this._recalculateLocalState();
207
+ // 6. Notify listeners
208
+ this.onUpdate.emit(this._state);
209
+ }
210
+ /**
211
+ * Applies incoming changes from the server that were *not* initiated by this client.
212
+ * @param externalServerChanges An array of sequential changes from the server.
213
+ */
214
+ applyExternalServerUpdate(externalServerChanges) {
215
+ if (externalServerChanges.length === 0) {
216
+ return;
217
+ }
218
+ const firstChange = externalServerChanges[0];
219
+ // Allow for gaps if server sends updates out of order, but warn.
220
+ if (firstChange.rev && firstChange.rev <= this._committedRev) {
221
+ console.warn(`Ignoring external server update starting at revision ${firstChange.rev} which is <= current committed ${this._committedRev}`);
222
+ return; // Ignore already processed or irrelevant changes
223
+ }
224
+ // if (firstChange.rev && firstChange.rev !== this._committedRev + 1) {
225
+ // console.warn(`External server update starting at ${firstChange.rev} does not directly follow committed ${this._committedRev}`);
226
+ // // Handle potential gaps - request resync? Apply cautiously?
227
+ // }
228
+ const lastChange = externalServerChanges[externalServerChanges.length - 1];
229
+ // 1. Apply to committed state
230
+ try {
231
+ this._committedState = applyChanges(this._committedState, externalServerChanges);
232
+ }
233
+ catch (error) {
234
+ console.error('Failed to apply external server update to committed state:', error);
235
+ const errorMessage = error instanceof Error ? error.message : String(error);
236
+ throw new Error(`Critical sync error applying external server update: ${errorMessage}`);
237
+ }
238
+ // 2. Update committed revision
239
+ if (lastChange.rev) {
240
+ this._committedRev = lastChange.rev;
241
+ }
242
+ else {
243
+ console.error('External server update missing revision on last change.');
244
+ // Cannot reliably update revision - potential state divergence
245
+ }
246
+ // 3. Rebase *both* sending and pending changes against the external changes
247
+ if (this.isSending) {
248
+ this._sendingChanges = rebaseChanges(externalServerChanges, this._sendingChanges);
249
+ }
250
+ if (this.hasPending) {
251
+ this._pendingChanges = rebaseChanges(externalServerChanges, this._pendingChanges);
252
+ }
253
+ // 4. Recalculate local state
254
+ this._recalculateLocalState();
255
+ // 5. Notify listeners
256
+ this.onUpdate.emit(this._state);
257
+ }
258
+ /**
259
+ * Handles the scenario where sending changes to the server failed.
260
+ * Moves the changes that were in the process of being sent back to the
261
+ * beginning of the pending queue to be retried later.
262
+ */
263
+ handleSendFailure() {
264
+ if (this.isSending) {
265
+ console.warn(`Handling send failure: Moving ${this._sendingChanges.length} changes back to pending queue.`);
266
+ // Prepend sending changes back to pending queue to maintain order and prioritize retry
267
+ this._pendingChanges.unshift(...this._sendingChanges);
268
+ this._sendingChanges = [];
269
+ // Do NOT recalculate state here, as the state didn't actually advance
270
+ // due to the failed send. The state should still reflect the last known
271
+ // good state + pending changes (which now includes the failed ones).
272
+ }
273
+ else {
274
+ console.warn('handleSendFailure called but no changes were marked as sending.');
275
+ }
276
+ }
277
+ /** Recalculates _state from _committedState + _sendingChanges + _pendingChanges */
278
+ _recalculateLocalState() {
279
+ try {
280
+ // Apply sending changes first, then pending changes over that result
281
+ // Note: The previous implementation combined them, which might be okay if applyChanges handles arrays,
282
+ // but separating them is clearer conceptually if applyChanges expects one state and one array of changes.
283
+ // Assuming applyChanges handles an array of changes correctly:
284
+ this._state = applyChanges(this._committedState, [...this._sendingChanges, ...this._pendingChanges]);
285
+ }
286
+ catch (error) {
287
+ console.error('CRITICAL: Error recalculating local state after update:', error);
288
+ // This indicates a potentially serious issue with patch application or rebasing logic.
289
+ // Re-throw the error to allow higher-level handling (e.g., trigger resync)
290
+ throw error;
291
+ }
292
+ }
293
+ /**
294
+ * @deprecated Use export() - kept for backward compatibility if needed.
295
+ */
296
+ toJSON() {
297
+ return this.export();
298
+ }
299
+ }
@@ -0,0 +1,2 @@
1
+ export { PatchDoc } from './PatchDoc';
2
+ export type { Change, PatchSnapshot, VersionMetadata } from '../types';
@@ -0,0 +1 @@
1
+ export { PatchDoc } from './PatchDoc';
@@ -0,0 +1,31 @@
1
+ export type SignalSubscriber = (...args: any[]) => any;
2
+ export type ErrorSubscriber = (error: Error) => any;
3
+ export type Unsubscriber = () => void;
4
+ type Args<T> = T extends (...args: infer A) => any ? A : never;
5
+ export type Signal<T extends SignalSubscriber = SignalSubscriber> = {
6
+ (subscriber: T): Unsubscriber;
7
+ error: (errorListener: ErrorSubscriber) => Unsubscriber;
8
+ emit: (...args: Args<T>) => Promise<void>;
9
+ clear: () => void;
10
+ };
11
+ /**
12
+ * Creates a signal, a function that can be used to subscribe to events. The signal can be called with a subscriber
13
+ * function to register event listeners. It has methods for emitting events, handling errors, and managing subscriptions.
14
+ *
15
+ * @example
16
+ * const onLoad = signal<(data: MyData) => void>();
17
+ *
18
+ * // Subscribe to data
19
+ * onLoad((data) => console.log('loaded', data));
20
+ *
21
+ * // Subscribe to errors
22
+ * onLoad.error((error) => console.error('error', error));
23
+ *
24
+ * // Emit data to subscribers
25
+ * await onLoad.emit('data'); // logs 'loaded data'
26
+ *
27
+ * // Clear all subscribers
28
+ * onLoad.clear();
29
+ */
30
+ export declare function signal<T extends SignalSubscriber = SignalSubscriber>(): Signal<T>;
31
+ export {};
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Creates a signal, a function that can be used to subscribe to events. The signal can be called with a subscriber
3
+ * function to register event listeners. It has methods for emitting events, handling errors, and managing subscriptions.
4
+ *
5
+ * @example
6
+ * const onLoad = signal<(data: MyData) => void>();
7
+ *
8
+ * // Subscribe to data
9
+ * onLoad((data) => console.log('loaded', data));
10
+ *
11
+ * // Subscribe to errors
12
+ * onLoad.error((error) => console.error('error', error));
13
+ *
14
+ * // Emit data to subscribers
15
+ * await onLoad.emit('data'); // logs 'loaded data'
16
+ *
17
+ * // Clear all subscribers
18
+ * onLoad.clear();
19
+ */
20
+ export function signal() {
21
+ const subscribers = new Set();
22
+ const errorListeners = new Set();
23
+ function signal(subscriber) {
24
+ subscribers.add(subscriber);
25
+ return () => subscribers.delete(subscriber);
26
+ }
27
+ signal.emit = async (...args) => {
28
+ const listeners = args[0] instanceof Error ? errorListeners : subscribers;
29
+ Array.from(listeners).map(listener => listener(...args));
30
+ };
31
+ signal.error = (errorListener) => {
32
+ errorListeners.add(errorListener);
33
+ return () => errorListeners.delete(errorListener);
34
+ };
35
+ signal.clear = () => {
36
+ subscribers.clear();
37
+ errorListeners.clear();
38
+ };
39
+ return signal;
40
+ }
@@ -0,0 +1,2 @@
1
+ export { PatchDoc } from './client/PatchDoc.js';
2
+ export type { Change, PatchSnapshot, PatchState, VersionMetadata } from './types';
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { PatchDoc } from './client/PatchDoc.js';
@@ -0,0 +1,126 @@
1
+ /*!
2
+ * Based on work from
3
+ * https://github.com/mohayonao/json-touch-patch
4
+ * (c) 2018 mohayonao
5
+ *
6
+ * MIT license
7
+ * (c) 2022 Jacob Wright
8
+ *
9
+ *
10
+ * WARNING: using /array/- syntax to indicate the end of the array makes it impossible to transform arrays correctly in
11
+ * all situaions. Please avoid using this syntax when using Operational Transformations.
12
+ */
13
+ import { Delta } from '@dabble/delta';
14
+ import type { ApplyJSONPatchOptions, JSONPatchOp, JSONPatchOpHandlerMap } from './types.js';
15
+ export type PathLike = string | {
16
+ toString(): string;
17
+ };
18
+ export interface WriteOptions {
19
+ soft?: boolean;
20
+ }
21
+ /**
22
+ * A JSONPatch helps with creating and applying one or more "JSON patches". It can track one or more changes
23
+ * together which may form a single operation or transaction.
24
+ */
25
+ export declare class JSONPatch {
26
+ ops: JSONPatchOp[];
27
+ custom: JSONPatchOpHandlerMap;
28
+ /**
29
+ * Create a new JSONPatch, optionally with an existing array of operations.
30
+ */
31
+ constructor(ops?: JSONPatchOp[], custom?: JSONPatchOpHandlerMap);
32
+ op(op: string, path: PathLike, value?: any, from?: PathLike, soft?: boolean): this;
33
+ /**
34
+ * Tests a value exists. If it doesn't, the patch is not applied.
35
+ */
36
+ test(path: PathLike, value: any): this;
37
+ /**
38
+ * Adds the value to an object or array, inserted before the given index.
39
+ */
40
+ add(path: PathLike, value: any, options?: WriteOptions): this;
41
+ /**
42
+ * Deletes the value at the given path or removes it from an array.
43
+ */
44
+ remove(path: PathLike): this;
45
+ /**
46
+ * Replaces a value (same as remove+add).
47
+ */
48
+ replace(path: PathLike, value: any, options?: WriteOptions): this;
49
+ /**
50
+ * Copies the value at `from` to `path`.
51
+ */
52
+ copy(from: PathLike, to: PathLike, options?: WriteOptions): this;
53
+ /**
54
+ * Moves the value at `from` to `path`.
55
+ */
56
+ move(from: PathLike, to: PathLike): this;
57
+ /**
58
+ * Increments a numeric value by 1 or the given amount.
59
+ */
60
+ increment(path: PathLike, value?: number): this;
61
+ /**
62
+ * Decrements a numeric value by 1 or the given amount.
63
+ */
64
+ decrement(path: PathLike, value?: number): this;
65
+ /**
66
+ * Flips a bit at the given index in a bitmask to the given value.
67
+ */
68
+ bit(path: PathLike, index: number, on: boolean): this;
69
+ /**
70
+ * Applies a delta to a text document.
71
+ */
72
+ text(path: PathLike, value: Delta | Delta['ops']): this;
73
+ /**
74
+ * Creates a patch from an object partial, updating each field. Set a field to undefined to delete it.
75
+ */
76
+ addUpdates(updates: {
77
+ [key: string]: any;
78
+ }, path?: string): this;
79
+ /**
80
+ * This will ensure an "add empty object" operation is created for each property along the path that does not exist.
81
+ */
82
+ addObjectsInPath(obj: any, path: PathLike): this;
83
+ /**
84
+ * Apply this patch to an object, returning a new object with the applied changes (or the same object if nothing
85
+ * changed in the patch). Optionally apply the page at the given path prefix.
86
+ */
87
+ apply<T>(obj: T, options?: ApplyJSONPatchOptions): T;
88
+ /**
89
+ * Transform the given patch against this one. This patch is considered to have happened first. Optionally provide
90
+ * the object these operations are being applied to if available to know for sure if a numerical path is an array
91
+ * index or object key. Otherwise, all numerical paths are treated as array indexes.
92
+ */
93
+ transform(patch: JSONPatch | JSONPatchOp[], obj?: any): this;
94
+ /**
95
+ * Create a patch which can reverse what this patch does. Because JSON Patches do not store previous values, you
96
+ * must provide the previous object to create a reverse patch.
97
+ */
98
+ invert(obj: any): this;
99
+ /**
100
+ * Compose/collapse patches into fewer operations.
101
+ */
102
+ compose(patch?: JSONPatch | JSONPatchOp[]): this;
103
+ /**
104
+ * Add two patches together.
105
+ */
106
+ concat(patch: JSONPatch | JSONPatchOp[]): this;
107
+ /**
108
+ * Returns an array of patch operations.
109
+ */
110
+ toJSON(): JSONPatchOp[];
111
+ /**
112
+ * Create a new JSONPatch with the provided JSON patch operations.
113
+ */
114
+ static fromJSON<T>(this: {
115
+ new (ops?: JSONPatchOp[], types?: JSONPatchOpHandlerMap): T;
116
+ }, ops?: JSONPatchOp[], types?: JSONPatchOpHandlerMap): T;
117
+ }
118
+ export type JSONPath<T> = T extends object ? T extends Array<infer U> ? {
119
+ readonly [K in keyof T & number]-?: JSONPathValue<U>;
120
+ } : {
121
+ readonly [K in keyof T as T[K] extends Function ? never : K]-?: JSONPathValue<NonNullable<T[K]>>;
122
+ } : never;
123
+ export type JSONPathValue<T> = {
124
+ toString(): string;
125
+ } & (T extends object ? JSONPath<T> : {});
126
+ export declare function createJSONPath<T = unknown>(): JSONPath<T>;