@dabble/patches 0.5.22 → 0.7.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 (118) hide show
  1. package/README.md +221 -208
  2. package/dist/BaseDoc-DkP3tUhT.d.ts +206 -0
  3. package/dist/algorithms/client/applyCommittedChanges.d.ts +7 -0
  4. package/dist/algorithms/client/applyCommittedChanges.js +6 -3
  5. package/dist/algorithms/lww/consolidateOps.d.ts +40 -0
  6. package/dist/algorithms/lww/consolidateOps.js +103 -0
  7. package/dist/algorithms/lww/index.d.ts +2 -0
  8. package/dist/algorithms/lww/index.js +1 -0
  9. package/dist/algorithms/lww/mergeServerWithLocal.d.ts +22 -0
  10. package/dist/algorithms/lww/mergeServerWithLocal.js +32 -0
  11. package/dist/algorithms/server/commitChanges.d.ts +32 -8
  12. package/dist/algorithms/server/commitChanges.js +24 -10
  13. package/dist/algorithms/server/createVersion.d.ts +1 -1
  14. package/dist/algorithms/server/createVersion.js +2 -4
  15. package/dist/algorithms/server/getSnapshotAtRevision.d.ts +1 -1
  16. package/dist/algorithms/server/getStateAtRevision.d.ts +1 -1
  17. package/dist/algorithms/server/handleOfflineSessionsAndBatches.d.ts +1 -1
  18. package/dist/algorithms/server/handleOfflineSessionsAndBatches.js +5 -7
  19. package/dist/client/BaseDoc.d.ts +6 -0
  20. package/dist/client/BaseDoc.js +70 -0
  21. package/dist/client/ClientAlgorithm.d.ts +101 -0
  22. package/dist/client/ClientAlgorithm.js +0 -0
  23. package/dist/client/InMemoryStore.d.ts +5 -7
  24. package/dist/client/InMemoryStore.js +6 -35
  25. package/dist/client/IndexedDBStore.d.ts +39 -73
  26. package/dist/client/IndexedDBStore.js +17 -220
  27. package/dist/client/LWWAlgorithm.d.ts +43 -0
  28. package/dist/client/LWWAlgorithm.js +87 -0
  29. package/dist/client/LWWClientStore.d.ts +73 -0
  30. package/dist/client/LWWClientStore.js +0 -0
  31. package/dist/client/LWWDoc.d.ts +56 -0
  32. package/dist/client/LWWDoc.js +84 -0
  33. package/dist/client/LWWInMemoryStore.d.ts +88 -0
  34. package/dist/client/LWWInMemoryStore.js +208 -0
  35. package/dist/client/LWWIndexedDBStore.d.ts +91 -0
  36. package/dist/client/LWWIndexedDBStore.js +275 -0
  37. package/dist/client/OTAlgorithm.d.ts +42 -0
  38. package/dist/client/OTAlgorithm.js +113 -0
  39. package/dist/client/OTClientStore.d.ts +50 -0
  40. package/dist/client/OTClientStore.js +0 -0
  41. package/dist/client/OTDoc.d.ts +6 -0
  42. package/dist/client/OTDoc.js +97 -0
  43. package/dist/client/OTIndexedDBStore.d.ts +84 -0
  44. package/dist/client/OTIndexedDBStore.js +163 -0
  45. package/dist/client/Patches.d.ts +36 -16
  46. package/dist/client/Patches.js +60 -27
  47. package/dist/client/PatchesDoc.d.ts +4 -113
  48. package/dist/client/PatchesDoc.js +3 -153
  49. package/dist/client/PatchesStore.d.ts +8 -105
  50. package/dist/client/factories.d.ts +72 -0
  51. package/dist/client/factories.js +80 -0
  52. package/dist/client/index.d.ts +14 -5
  53. package/dist/client/index.js +9 -0
  54. package/dist/compression/index.d.ts +1 -1
  55. package/dist/data/change.js +4 -3
  56. package/dist/fractionalIndex.d.ts +67 -0
  57. package/dist/fractionalIndex.js +241 -0
  58. package/dist/index.d.ts +13 -4
  59. package/dist/index.js +1 -1
  60. package/dist/json-patch/types.d.ts +2 -0
  61. package/dist/net/PatchesClient.js +15 -15
  62. package/dist/net/PatchesSync.d.ts +24 -12
  63. package/dist/net/PatchesSync.js +56 -64
  64. package/dist/net/index.d.ts +6 -10
  65. package/dist/net/index.js +6 -1
  66. package/dist/net/protocol/JSONRPCClient.d.ts +4 -4
  67. package/dist/net/protocol/JSONRPCClient.js +6 -4
  68. package/dist/net/protocol/JSONRPCServer.d.ts +45 -9
  69. package/dist/net/protocol/JSONRPCServer.js +63 -8
  70. package/dist/net/serverContext.d.ts +38 -0
  71. package/dist/net/serverContext.js +20 -0
  72. package/dist/net/webrtc/WebRTCTransport.js +1 -1
  73. package/dist/net/websocket/AuthorizationProvider.d.ts +3 -3
  74. package/dist/net/websocket/WebSocketServer.d.ts +29 -20
  75. package/dist/net/websocket/WebSocketServer.js +23 -12
  76. package/dist/server/BranchManager.d.ts +50 -0
  77. package/dist/server/BranchManager.js +0 -0
  78. package/dist/server/CompressedStoreBackend.d.ts +8 -6
  79. package/dist/server/CompressedStoreBackend.js +3 -9
  80. package/dist/server/LWWBranchManager.d.ts +82 -0
  81. package/dist/server/LWWBranchManager.js +99 -0
  82. package/dist/server/LWWMemoryStoreBackend.d.ts +78 -0
  83. package/dist/server/LWWMemoryStoreBackend.js +191 -0
  84. package/dist/server/LWWServer.d.ts +130 -0
  85. package/dist/server/LWWServer.js +207 -0
  86. package/dist/server/{PatchesBranchManager.d.ts → OTBranchManager.d.ts} +32 -12
  87. package/dist/server/{PatchesBranchManager.js → OTBranchManager.js} +26 -42
  88. package/dist/server/OTServer.d.ts +108 -0
  89. package/dist/server/OTServer.js +141 -0
  90. package/dist/server/PatchesHistoryManager.d.ts +20 -7
  91. package/dist/server/PatchesHistoryManager.js +26 -3
  92. package/dist/server/PatchesServer.d.ts +70 -81
  93. package/dist/server/PatchesServer.js +0 -176
  94. package/dist/server/branchUtils.d.ts +82 -0
  95. package/dist/server/branchUtils.js +66 -0
  96. package/dist/server/index.d.ts +17 -6
  97. package/dist/server/index.js +33 -4
  98. package/dist/server/tombstone.d.ts +29 -0
  99. package/dist/server/tombstone.js +32 -0
  100. package/dist/server/types.d.ts +129 -27
  101. package/dist/server/utils.d.ts +12 -0
  102. package/dist/server/utils.js +23 -0
  103. package/dist/solid/context.d.ts +5 -4
  104. package/dist/solid/doc-manager.d.ts +3 -3
  105. package/dist/solid/index.d.ts +5 -4
  106. package/dist/solid/primitives.d.ts +2 -3
  107. package/dist/types.d.ts +16 -14
  108. package/dist/vue/composables.d.ts +2 -3
  109. package/dist/vue/doc-manager.d.ts +3 -3
  110. package/dist/vue/index.d.ts +5 -4
  111. package/dist/vue/provider.d.ts +5 -4
  112. package/package.json +1 -1
  113. package/dist/algorithms/client/collapsePendingChanges.d.ts +0 -30
  114. package/dist/algorithms/client/collapsePendingChanges.js +0 -78
  115. package/dist/net/websocket/RPCServer.d.ts +0 -141
  116. package/dist/net/websocket/RPCServer.js +0 -204
  117. package/dist/utils/dates.d.ts +0 -43
  118. package/dist/utils/dates.js +0 -47
@@ -1,18 +1,19 @@
1
+ import { JSONRPCClient } from './protocol/JSONRPCClient.js';
1
2
  import { Signal } from '../event-signal.js';
2
- import { ConnectionState } from './protocol/types.js';
3
+ import { SizeCalculator } from '../algorithms/shared/changeBatching.js';
4
+ import { Patches } from '../client/Patches.js';
5
+ import { AlgorithmName, ClientAlgorithm } from '../client/ClientAlgorithm.js';
3
6
  import { SyncingState, Change } from '../types.js';
4
- import { JSONRPCClient } from './protocol/JSONRPCClient.js';
7
+ import { ConnectionState } from './protocol/types.js';
5
8
  import { PatchesWebSocket } from './websocket/PatchesWebSocket.js';
6
9
  import { WebSocketOptions } from './websocket/WebSocketTransport.js';
7
- import { SizeCalculator } from '../algorithms/shared/changeBatching.js';
8
- import { Patches } from '../client/Patches.js';
9
- import { PatchesStore } from '../client/PatchesStore.js';
10
+ import '../json-patch/types.js';
11
+ import '../BaseDoc-DkP3tUhT.js';
10
12
  import '../json-patch/JSONPatch.js';
11
13
  import '@dabble/delta';
12
- import '../json-patch/types.js';
14
+ import '../client/PatchesStore.js';
13
15
  import './PatchesClient.js';
14
16
  import '../utils/deferred.js';
15
- import '../client/PatchesDoc.js';
16
17
 
17
18
  interface PatchesSyncState {
18
19
  online: boolean;
@@ -32,16 +33,22 @@ interface PatchesSyncOptions {
32
33
  /**
33
34
  * Handles WebSocket connection, document subscriptions, and syncing logic between
34
35
  * the Patches instance and the server.
36
+ *
37
+ * PatchesSync is algorithm-agnostic. It delegates to algorithm methods for:
38
+ * - Getting pending changes to send
39
+ * - Applying server changes
40
+ * - Confirming sent changes
35
41
  */
36
42
  declare class PatchesSync {
37
43
  protected options?: PatchesSyncOptions | undefined;
38
44
  protected ws: PatchesWebSocket;
39
45
  protected patches: Patches;
40
- protected store: PatchesStore;
41
46
  protected maxPayloadBytes?: number;
42
47
  protected maxStorageBytes?: number;
43
48
  protected sizeCalculator?: SizeCalculator;
44
49
  protected trackedDocs: Set<string>;
50
+ /** Maps docId to the algorithm name used for that doc */
51
+ protected docAlgorithms: Map<string, AlgorithmName>;
45
52
  protected _state: PatchesSyncState;
46
53
  /**
47
54
  * Signal emitted when the sync state changes.
@@ -59,6 +66,11 @@ declare class PatchesSync {
59
66
  */
60
67
  readonly onRemoteDocDeleted: Signal<(docId: string, pendingChanges: Change[]) => void>;
61
68
  constructor(patches: Patches, url: string, options?: PatchesSyncOptions | undefined);
69
+ /**
70
+ * Gets the algorithm for a document. Uses the open doc's algorithm if available,
71
+ * otherwise falls back to the default algorithm.
72
+ */
73
+ protected _getAlgorithm(docId: string): ClientAlgorithm;
62
74
  /**
63
75
  * Gets the URL of the WebSocket connection.
64
76
  */
@@ -112,10 +124,10 @@ declare class PatchesSync {
112
124
  */
113
125
  protected _receiveCommittedChanges(docId: string, serverChanges: Change[]): Promise<void>;
114
126
  /**
115
- * Applies server changes to a document using the centralized sync algorithm.
116
- * This ensures consistent OT behavior regardless of whether the doc is open in memory.
127
+ * Applies server changes to a document using the algorithm.
128
+ * The algorithm handles all algorithm-specific logic (OT rebasing, LWW merging, etc).
117
129
  */
118
- protected _applyServerChangesToDoc(docId: string, serverChanges: Change[], sentPendingRange?: [number, number]): Promise<Change[]>;
130
+ protected _applyServerChangesToDoc(docId: string, serverChanges: Change[]): Promise<void>;
119
131
  /**
120
132
  * Initiates the deletion process for a document both locally and on the server.
121
133
  * This now delegates the local tombstone marking to Patches.
@@ -124,7 +136,7 @@ declare class PatchesSync {
124
136
  protected _handleConnectionChange(connectionState: ConnectionState): void;
125
137
  protected _handleDocsTracked(docIds: string[]): Promise<void>;
126
138
  protected _handleDocsUntracked(docIds: string[]): Promise<void>;
127
- protected _handleDocChange(docId: string, _changes: Change[]): Promise<void>;
139
+ protected _handleDocChange(docId: string): Promise<void>;
128
140
  /**
129
141
  * Unified handler for remote document deletion (both real-time notifications and offline discovery).
130
142
  * Cleans up local state and notifies the application with any pending changes that were lost.
@@ -7,9 +7,8 @@ import {
7
7
  } from "../chunk-IZ2YBCUP.js";
8
8
  var __receiveCommittedChanges_dec, _syncDoc_dec, _init;
9
9
  import { isEqual } from "@dabble/delta";
10
- import { applyCommittedChanges } from "../algorithms/client/applyCommittedChanges.js";
11
- import { collapsePendingChanges } from "../algorithms/client/collapsePendingChanges.js";
12
10
  import { breakChangesIntoBatches } from "../algorithms/shared/changeBatching.js";
11
+ import { BaseDoc } from "../client/BaseDoc.js";
13
12
  import { Patches } from "../client/Patches.js";
14
13
  import { signal } from "../event-signal.js";
15
14
  import { blockable } from "../utils/concurrency.js";
@@ -22,11 +21,12 @@ class PatchesSync {
22
21
  __runInitializers(_init, 5, this);
23
22
  __publicField(this, "ws");
24
23
  __publicField(this, "patches");
25
- __publicField(this, "store");
26
24
  __publicField(this, "maxPayloadBytes");
27
25
  __publicField(this, "maxStorageBytes");
28
26
  __publicField(this, "sizeCalculator");
29
27
  __publicField(this, "trackedDocs");
28
+ /** Maps docId to the algorithm name used for that doc */
29
+ __publicField(this, "docAlgorithms", /* @__PURE__ */ new Map());
30
30
  __publicField(this, "_state", { online: false, connected: false, syncing: null });
31
31
  /**
32
32
  * Signal emitted when the sync state changes.
@@ -42,7 +42,6 @@ class PatchesSync {
42
42
  */
43
43
  __publicField(this, "onRemoteDocDeleted", signal());
44
44
  this.patches = patches;
45
- this.store = patches.store;
46
45
  this.maxPayloadBytes = options?.maxPayloadBytes;
47
46
  this.maxStorageBytes = options?.maxStorageBytes ?? patches.docOptions?.maxStorageBytes;
48
47
  this.sizeCalculator = options?.sizeCalculator ?? patches.docOptions?.sizeCalculator;
@@ -58,6 +57,20 @@ class PatchesSync {
58
57
  patches.onDeleteDoc(this._handleDocDeleted.bind(this));
59
58
  patches.onChange(this._handleDocChange.bind(this));
60
59
  }
60
+ /**
61
+ * Gets the algorithm for a document. Uses the open doc's algorithm if available,
62
+ * otherwise falls back to the default algorithm.
63
+ */
64
+ _getAlgorithm(docId) {
65
+ const docAlgorithm = this.patches.getDocAlgorithm(docId);
66
+ if (docAlgorithm) return docAlgorithm;
67
+ const algorithmName = this.docAlgorithms.get(docId) ?? this.patches.defaultAlgorithm;
68
+ const algorithm = this.patches.algorithms[algorithmName];
69
+ if (!algorithm) {
70
+ throw new Error(`Algorithm '${algorithmName}' not found for doc ${docId}`);
71
+ }
72
+ return algorithm;
73
+ }
61
74
  /**
62
75
  * Gets the URL of the WebSocket connection.
63
76
  */
@@ -125,7 +138,11 @@ class PatchesSync {
125
138
  if (!this.state.connected) return;
126
139
  this.updateState({ syncing: "updating" });
127
140
  try {
128
- const tracked = await this.store.listDocs(true);
141
+ const defaultAlgorithm = this.patches.algorithms[this.patches.defaultAlgorithm];
142
+ if (!defaultAlgorithm) {
143
+ throw new Error("Default algorithm not found");
144
+ }
145
+ const tracked = await defaultAlgorithm.listDocs(true);
129
146
  const activeDocs = tracked.filter((t) => !t.deleted);
130
147
  const deletedDocs = tracked.filter((t) => t.deleted);
131
148
  const activeDocIds = activeDocs.map((t) => t.docId);
@@ -146,7 +163,8 @@ class PatchesSync {
146
163
  try {
147
164
  console.info(`Attempting server delete for tombstoned doc: ${docId}`);
148
165
  await this.ws.deleteDoc(docId);
149
- await this.store.confirmDeleteDoc(docId);
166
+ const algorithm = this._getAlgorithm(docId);
167
+ await algorithm.confirmDeleteDoc(docId);
150
168
  console.info(`Successfully deleted and untracked doc: ${docId}`);
151
169
  } catch (err) {
152
170
  console.warn(`Server delete failed for ${docId}, keeping tombstone:`, err);
@@ -164,15 +182,17 @@ class PatchesSync {
164
182
  async syncDoc(docId) {
165
183
  if (!this.state.connected) return;
166
184
  const doc = this.patches.getOpenDoc(docId);
167
- if (doc) {
168
- doc.updateSyncing("updating");
185
+ const algorithm = this._getAlgorithm(docId);
186
+ const baseDoc = doc;
187
+ if (baseDoc) {
188
+ baseDoc.updateSyncing("updating");
169
189
  }
170
190
  try {
171
- const pending = await this.store.getPendingChanges(docId);
172
- if (pending.length > 0) {
191
+ const pending = await algorithm.getPendingToSend(docId);
192
+ if (pending && pending.length > 0) {
173
193
  await this.flushDoc(docId, pending);
174
194
  } else {
175
- const [committedRev] = await this.store.getLastRevs(docId);
195
+ const committedRev = await algorithm.getCommittedRev(docId);
176
196
  if (committedRev) {
177
197
  const serverChanges = await this.ws.getChangesSince(docId, committedRev);
178
198
  if (serverChanges.length > 0) {
@@ -180,14 +200,14 @@ class PatchesSync {
180
200
  }
181
201
  } else {
182
202
  const snapshot = await this.ws.getDoc(docId);
183
- await this.store.saveDoc(docId, snapshot);
184
- if (doc) {
185
- doc.import({ ...snapshot, changes: [] });
203
+ await algorithm.store.saveDoc(docId, snapshot);
204
+ if (baseDoc) {
205
+ baseDoc.import({ ...snapshot, changes: [] });
186
206
  }
187
207
  }
188
208
  }
189
- if (doc) {
190
- doc.updateSyncing(null);
209
+ if (baseDoc) {
210
+ baseDoc.updateSyncing(null);
191
211
  }
192
212
  } catch (err) {
193
213
  if (this._isDocDeletedError(err)) {
@@ -196,8 +216,8 @@ class PatchesSync {
196
216
  }
197
217
  console.error(`Error syncing doc ${docId}:`, err);
198
218
  this.onError.emit(err, { docId });
199
- if (doc) {
200
- doc.updateSyncing(err instanceof Error ? err : new Error(String(err)));
219
+ if (baseDoc) {
220
+ baseDoc.updateSyncing(err instanceof Error ? err : new Error(String(err)));
201
221
  }
202
222
  }
203
223
  }
@@ -213,18 +233,14 @@ class PatchesSync {
213
233
  if (!this.state.connected) {
214
234
  throw new Error("Not connected to server");
215
235
  }
236
+ const algorithm = this._getAlgorithm(docId);
216
237
  try {
217
- if (!pending) pending = await this.store.getPendingChanges(docId);
238
+ if (!pending) {
239
+ pending = await algorithm.getPendingToSend(docId) ?? [];
240
+ }
218
241
  if (!pending.length) {
219
242
  return;
220
243
  }
221
- if (this.store.getLastAttemptedSubmissionRev && this.store.setLastAttemptedSubmissionRev) {
222
- const afterRev = await this.store.getLastAttemptedSubmissionRev(docId);
223
- pending = collapsePendingChanges(pending, afterRev);
224
- if (!pending.length) {
225
- return;
226
- }
227
- }
228
244
  const batches = breakChangesIntoBatches(pending, {
229
245
  maxPayloadBytes: this.maxPayloadBytes,
230
246
  maxStorageBytes: this.maxStorageBytes,
@@ -234,14 +250,10 @@ class PatchesSync {
234
250
  if (!this.state.connected) {
235
251
  throw new Error("Disconnected during flush");
236
252
  }
237
- if (this.store.setLastAttemptedSubmissionRev) {
238
- const lastRevInBatch = batch[batch.length - 1].rev;
239
- await this.store.setLastAttemptedSubmissionRev(docId, lastRevInBatch);
240
- }
241
- const range = [batch[0].rev, batch[batch.length - 1].rev];
242
253
  const committed = await this.ws.commitChanges(docId, batch);
243
- await this._applyServerChangesToDoc(docId, committed, range);
244
- pending = await this.store.getPendingChanges(docId);
254
+ await this._applyServerChangesToDoc(docId, committed);
255
+ await algorithm.confirmSent(docId, batch);
256
+ pending = await algorithm.getPendingToSend(docId) ?? [];
245
257
  }
246
258
  } catch (err) {
247
259
  if (this._isDocDeletedError(err)) {
@@ -261,35 +273,13 @@ class PatchesSync {
261
273
  }
262
274
  }
263
275
  /**
264
- * Applies server changes to a document using the centralized sync algorithm.
265
- * This ensures consistent OT behavior regardless of whether the doc is open in memory.
276
+ * Applies server changes to a document using the algorithm.
277
+ * The algorithm handles all algorithm-specific logic (OT rebasing, LWW merging, etc).
266
278
  */
267
- async _applyServerChangesToDoc(docId, serverChanges, sentPendingRange) {
268
- const currentSnapshot = await this.store.getDoc(docId);
269
- if (!currentSnapshot) {
270
- console.warn(`Cannot apply server changes to non-existent doc: ${docId}`);
271
- return [];
272
- }
279
+ async _applyServerChangesToDoc(docId, serverChanges) {
273
280
  const doc = this.patches.getOpenDoc(docId);
274
- if (doc) {
275
- const inMemoryPendingChanges = doc?.getPendingChanges();
276
- const latestRev = currentSnapshot.changes[currentSnapshot.changes.length - 1]?.rev || currentSnapshot.rev;
277
- const newChanges = inMemoryPendingChanges.filter((change) => change.rev > latestRev);
278
- currentSnapshot.changes.push(...newChanges);
279
- }
280
- const { state, rev, changes: rebasedPendingChanges } = applyCommittedChanges(currentSnapshot, serverChanges);
281
- if (doc) {
282
- if (doc.committedRev === serverChanges[0].rev - 1) {
283
- doc.applyCommittedChanges(serverChanges, rebasedPendingChanges);
284
- } else {
285
- doc.import({ state, rev, changes: rebasedPendingChanges });
286
- }
287
- }
288
- await Promise.all([
289
- this.store.saveCommittedChanges(docId, serverChanges, sentPendingRange),
290
- this.store.replacePendingChanges(docId, rebasedPendingChanges)
291
- ]);
292
- return rebasedPendingChanges;
281
+ const algorithm = this._getAlgorithm(docId);
282
+ await algorithm.applyServerChanges(docId, serverChanges, doc);
293
283
  }
294
284
  /**
295
285
  * Initiates the deletion process for a document both locally and on the server.
@@ -298,8 +288,9 @@ class PatchesSync {
298
288
  async _handleDocDeleted(docId) {
299
289
  if (this.state.connected) {
300
290
  try {
291
+ const algorithm = this._getAlgorithm(docId);
301
292
  await this.ws.deleteDoc(docId);
302
- await this.store.confirmDeleteDoc(docId);
293
+ await algorithm.confirmDeleteDoc(docId);
303
294
  } catch (err) {
304
295
  console.error(`Server delete failed for doc ${docId}, will retry on reconnect/resync.`, err);
305
296
  this.onError.emit(err, { docId });
@@ -351,7 +342,7 @@ class PatchesSync {
351
342
  }
352
343
  }
353
344
  }
354
- async _handleDocChange(docId, _changes) {
345
+ async _handleDocChange(docId) {
355
346
  if (!this.state.connected) return;
356
347
  if (!this.trackedDocs.has(docId)) return;
357
348
  await this.flushDoc(docId);
@@ -361,13 +352,14 @@ class PatchesSync {
361
352
  * Cleans up local state and notifies the application with any pending changes that were lost.
362
353
  */
363
354
  async _handleRemoteDocDeleted(docId) {
364
- const pendingChanges = await this.store.getPendingChanges(docId);
355
+ const algorithm = this._getAlgorithm(docId);
356
+ const pendingChanges = await algorithm.getPendingToSend(docId) ?? [];
365
357
  const doc = this.patches.getOpenDoc(docId);
366
358
  if (doc) {
367
359
  await this.patches.closeDoc(docId);
368
360
  }
369
361
  this.trackedDocs.delete(docId);
370
- await this.store.confirmDeleteDoc(docId);
362
+ await algorithm.confirmDeleteDoc(docId);
371
363
  await this.onRemoteDocDeleted.emit(docId, pendingChanges);
372
364
  }
373
365
  /**
@@ -2,30 +2,26 @@ export { FetchTransport } from './http/FetchTransport.js';
2
2
  export { PatchesClient } from './PatchesClient.js';
3
3
  export { PatchesSync, PatchesSyncOptions, PatchesSyncState } from './PatchesSync.js';
4
4
  export { JSONRPCClient } from './protocol/JSONRPCClient.js';
5
- export { ConnectionSignalSubscriber, JSONRPCServer, MessageHandler } from './protocol/JSONRPCServer.js';
5
+ export { AccessLevel, ApiDefinition, ConnectionSignalSubscriber, JSONRPCServer, JSONRPCServerOptions, MessageHandler } from './protocol/JSONRPCServer.js';
6
+ export { getAuthContext, getClientId } from './serverContext.js';
6
7
  export { AwarenessUpdateNotificationParams, ClientTransport, ConnectionState, JsonRpcNotification, JsonRpcRequest, JsonRpcResponse, ListOptions, Message, PatchesAPI, PatchesNotificationParams, ServerTransport, SignalNotificationParams } from './protocol/types.js';
7
8
  export { rpcError, rpcNotification, rpcResponse } from './protocol/utils.js';
8
9
  export { PatchesState, SyncingState } from './types.js';
9
10
  export { Access, AuthContext, AuthorizationProvider, allowAll, assertNotDeleted, denyAll } from './websocket/AuthorizationProvider.js';
10
11
  export { onlineState } from './websocket/onlineState.js';
11
12
  export { PatchesWebSocket } from './websocket/PatchesWebSocket.js';
12
- export { RPCServer, RPCServerOptions } from './websocket/RPCServer.js';
13
13
  export { JsonRpcMessage, SignalingService } from './websocket/SignalingService.js';
14
- export { WebSocketServer } from './websocket/WebSocketServer.js';
14
+ export { WebSocketServer, WebSocketServerOptions } from './websocket/WebSocketServer.js';
15
15
  export { WebSocketOptions, WebSocketTransport } from './websocket/WebSocketTransport.js';
16
16
  export { CommitChangesOptions } from '../types.js';
17
17
  import '../event-signal.js';
18
18
  import '../algorithms/shared/changeBatching.js';
19
19
  import '../client/Patches.js';
20
- import '../client/PatchesDoc.js';
20
+ import '../json-patch/types.js';
21
+ import '../client/ClientAlgorithm.js';
22
+ import '../BaseDoc-DkP3tUhT.js';
21
23
  import '../client/PatchesStore.js';
22
24
  import '../server/types.js';
23
- import '../server/PatchesBranchManager.js';
24
- import '../server/PatchesServer.js';
25
- import '../compression/index.js';
26
- import '../algorithms/shared/lz.js';
27
- import '../json-patch/types.js';
28
- import '../server/PatchesHistoryManager.js';
29
25
  import '../utils/deferred.js';
30
26
  import '../json-patch/JSONPatch.js';
31
27
  import '@dabble/delta';
package/dist/net/index.js CHANGED
@@ -1,13 +1,18 @@
1
+ import "../chunk-IZ2YBCUP.js";
1
2
  export * from "./http/FetchTransport.js";
2
3
  export * from "./PatchesClient.js";
3
4
  export * from "./PatchesSync.js";
4
5
  export * from "./protocol/JSONRPCClient.js";
5
6
  export * from "./protocol/JSONRPCServer.js";
7
+ import { getAuthContext, getClientId } from "./serverContext.js";
6
8
  export * from "./protocol/utils.js";
7
9
  export * from "./websocket/AuthorizationProvider.js";
8
10
  export * from "./websocket/onlineState.js";
9
11
  export * from "./websocket/PatchesWebSocket.js";
10
- export * from "./websocket/RPCServer.js";
11
12
  export * from "./websocket/SignalingService.js";
12
13
  export * from "./websocket/WebSocketServer.js";
13
14
  export * from "./websocket/WebSocketTransport.js";
15
+ export {
16
+ getAuthContext,
17
+ getClientId
18
+ };
@@ -25,18 +25,18 @@ declare class JSONRPCClient {
25
25
  * Sends a JSON-RPC request to the server and returns a promise for the response.
26
26
  *
27
27
  * @param method - The name of the remote procedure to call
28
- * @param params - The parameters to pass to the remote procedure (optional)
28
+ * @param args - The arguments to pass to the remote procedure (sent as array)
29
29
  * @returns A promise that resolves with the result of the procedure call or rejects with an error
30
30
  * @template T - The expected return type of the remote procedure
31
31
  */
32
- call<T = any>(method: string, params?: any): Promise<T>;
32
+ call<T = any>(method: string, ...args: any[]): Promise<T>;
33
33
  /**
34
34
  * Sends a JSON-RPC notification to the server (no response expected).
35
35
  *
36
36
  * @param method - The name of the remote procedure to call
37
- * @param params - The parameters to pass to the remote procedure (optional)
37
+ * @param args - The arguments to pass to the remote procedure (sent as array)
38
38
  */
39
- notify(method: string, params?: any): void;
39
+ notify(method: string, ...args: any[]): void;
40
40
  /**
41
41
  * Subscribes to server-sent notifications for a specific method.
42
42
  *
@@ -18,12 +18,13 @@ class JSONRPCClient {
18
18
  * Sends a JSON-RPC request to the server and returns a promise for the response.
19
19
  *
20
20
  * @param method - The name of the remote procedure to call
21
- * @param params - The parameters to pass to the remote procedure (optional)
21
+ * @param args - The arguments to pass to the remote procedure (sent as array)
22
22
  * @returns A promise that resolves with the result of the procedure call or rejects with an error
23
23
  * @template T - The expected return type of the remote procedure
24
24
  */
25
- async call(method, params) {
25
+ async call(method, ...args) {
26
26
  const id = this.nextId++;
27
+ const params = args.length > 0 ? args : void 0;
27
28
  const message = { jsonrpc: "2.0", id, method, params };
28
29
  return new Promise((resolve, reject) => {
29
30
  this.pending.set(id, { resolve, reject });
@@ -34,9 +35,10 @@ class JSONRPCClient {
34
35
  * Sends a JSON-RPC notification to the server (no response expected).
35
36
  *
36
37
  * @param method - The name of the remote procedure to call
37
- * @param params - The parameters to pass to the remote procedure (optional)
38
+ * @param args - The arguments to pass to the remote procedure (sent as array)
38
39
  */
39
- notify(method, params) {
40
+ notify(method, ...args) {
41
+ const params = args.length > 0 ? args : void 0;
40
42
  const message = { jsonrpc: "2.0", method, params };
41
43
  this.transport.send(JSON.stringify(message));
42
44
  }
@@ -1,14 +1,23 @@
1
1
  import { Signal, Unsubscriber } from '../../event-signal.js';
2
- import { AuthContext } from '../websocket/AuthorizationProvider.js';
2
+ import { AuthorizationProvider, AuthContext } from '../websocket/AuthorizationProvider.js';
3
3
  import { JsonRpcNotification, Message, JsonRpcResponse } from './types.js';
4
4
  import '../../server/types.js';
5
+ import '../../json-patch/types.js';
5
6
  import '../../types.js';
6
7
  import '../../json-patch/JSONPatch.js';
7
8
  import '@dabble/delta';
8
- import '../../json-patch/types.js';
9
9
 
10
10
  type ConnectionSignalSubscriber = (params: any, clientId?: string) => any;
11
- type MessageHandler<P = any, R = any> = (params: P, ctx?: AuthContext) => Promise<R> | R;
11
+ type MessageHandler<R = any> = (...args: any[]) => Promise<R> | R;
12
+ /** Access level for API methods */
13
+ type AccessLevel = 'read' | 'write';
14
+ /** Static API definition mapping method names to access levels */
15
+ type ApiDefinition = Record<string, AccessLevel>;
16
+ /** Options for creating a JSONRPCServer */
17
+ interface JSONRPCServerOptions {
18
+ /** Authorization provider for document access control */
19
+ auth?: AuthorizationProvider;
20
+ }
12
21
  /**
13
22
  * Lightweight JSON-RPC 2.0 server adapter for {@link PatchesServer}.
14
23
  *
@@ -30,15 +39,31 @@ declare class JSONRPCServer {
30
39
  private readonly handlers;
31
40
  /** Allow external callers to emit server-initiated notifications. */
32
41
  private readonly notificationSignals;
42
+ /** Authorization provider for document access control */
43
+ readonly auth?: AuthorizationProvider;
33
44
  /** Allow external callers to emit server-initiated notifications. */
34
45
  readonly onNotify: Signal<(msg: JsonRpcNotification, exceptConnectionId?: string) => void>;
46
+ /**
47
+ * Creates a new JSONRPCServer instance.
48
+ * @param options - Configuration options
49
+ */
50
+ constructor(options?: JSONRPCServerOptions);
35
51
  /**
36
52
  * Registers a JSON-RPC method.
37
53
  *
38
54
  * @param method Fully-qualified method name (e.g. "patches.subscribe").
39
55
  * @param handler Function that performs the work and returns the result.
56
+ * Receives spread arguments followed by AuthContext.
40
57
  */
41
- registerMethod<TParams = any, TResult = any>(method: string, handler: MessageHandler<TParams, TResult>): void;
58
+ registerMethod<TResult = any>(method: string, handler: MessageHandler<TResult>): void;
59
+ /**
60
+ * Registers all methods from an object that has a static `api` property.
61
+ * The `api` property should map method names to access levels ('read' | 'write').
62
+ *
63
+ * @param obj - Object instance with methods to register
64
+ * @throws Error if the object's constructor doesn't have a static `api` property
65
+ */
66
+ register<T extends object>(obj: T): void;
42
67
  /**
43
68
  * Subscribes to server-sent notifications for a specific method.
44
69
  *
@@ -68,13 +93,24 @@ declare class JSONRPCServer {
68
93
  processMessage(raw: string, ctx?: AuthContext): Promise<string | undefined>;
69
94
  processMessage(message: Message, ctx?: AuthContext): Promise<JsonRpcResponse | undefined>;
70
95
  /**
71
- * Maps JSON-RPC method names to {@link PatchesServer} calls.
72
- * @param connectionId - The WebSocket transport object.
96
+ * Checks access control before method invocation.
97
+ * Called before each method invocation when using `register()`.
98
+ *
99
+ * @param access - The required access level ('read' or 'write')
100
+ * @param ctx - The authentication context
101
+ * @param method - The method being called
102
+ * @param args - The method arguments (first arg is typically docId)
103
+ * @throws StatusError if access is denied
104
+ */
105
+ protected assertAccess(access: AccessLevel, ctx: AuthContext | undefined, method: string, args?: any[]): Promise<void>;
106
+ /**
107
+ * Maps JSON-RPC method names to handler calls.
73
108
  * @param method - The JSON-RPC method name.
74
- * @param params - The JSON-RPC parameters.
75
- * @returns The result of the {@link PatchesServer} call.
109
+ * @param params - The JSON-RPC parameters (array of arguments).
110
+ * @param ctx - The authentication context.
111
+ * @returns The result of the handler call.
76
112
  */
77
113
  protected _dispatch(method: string, params: any, ctx?: AuthContext): Promise<any>;
78
114
  }
79
115
 
80
- export { type ConnectionSignalSubscriber, JSONRPCServer, type MessageHandler };
116
+ export { type AccessLevel, type ApiDefinition, type ConnectionSignalSubscriber, JSONRPCServer, type JSONRPCServerOptions, type MessageHandler };
@@ -1,13 +1,24 @@
1
1
  import "../../chunk-IZ2YBCUP.js";
2
2
  import { signal } from "../../event-signal.js";
3
+ import { StatusError } from "../error.js";
4
+ import { clearAuthContext, getAuthContext, setAuthContext } from "../serverContext.js";
3
5
  import { rpcError, rpcNotification, rpcResponse } from "./utils.js";
4
6
  class JSONRPCServer {
5
7
  /** Map of fully-qualified JSON-RPC method → handler function */
6
8
  handlers = /* @__PURE__ */ new Map();
7
9
  /** Allow external callers to emit server-initiated notifications. */
8
10
  notificationSignals = /* @__PURE__ */ new Map();
11
+ /** Authorization provider for document access control */
12
+ auth;
9
13
  /** Allow external callers to emit server-initiated notifications. */
10
14
  onNotify = signal();
15
+ /**
16
+ * Creates a new JSONRPCServer instance.
17
+ * @param options - Configuration options
18
+ */
19
+ constructor(options = {}) {
20
+ this.auth = options.auth;
21
+ }
11
22
  // -------------------------------------------------------------------------
12
23
  // Registration API
13
24
  // -------------------------------------------------------------------------
@@ -16,6 +27,7 @@ class JSONRPCServer {
16
27
  *
17
28
  * @param method Fully-qualified method name (e.g. "patches.subscribe").
18
29
  * @param handler Function that performs the work and returns the result.
30
+ * Receives spread arguments followed by AuthContext.
19
31
  */
20
32
  registerMethod(method, handler) {
21
33
  if (this.handlers.has(method)) {
@@ -23,6 +35,29 @@ class JSONRPCServer {
23
35
  }
24
36
  this.handlers.set(method, handler);
25
37
  }
38
+ /**
39
+ * Registers all methods from an object that has a static `api` property.
40
+ * The `api` property should map method names to access levels ('read' | 'write').
41
+ *
42
+ * @param obj - Object instance with methods to register
43
+ * @throws Error if the object's constructor doesn't have a static `api` property
44
+ */
45
+ register(obj) {
46
+ const api = obj.constructor.api;
47
+ if (!api) {
48
+ throw new Error("Object must have static api property");
49
+ }
50
+ for (const [method, access] of Object.entries(api)) {
51
+ if (typeof obj[method] !== "function") {
52
+ throw new Error(`Method '${method}' not found on object`);
53
+ }
54
+ this.registerMethod(method, async (...args) => {
55
+ const ctx = getAuthContext();
56
+ await this.assertAccess(access, ctx, method, args);
57
+ return obj[method](...args);
58
+ });
59
+ }
60
+ }
26
61
  // -------------------------------------------------------------------------
27
62
  // Public helpers
28
63
  // -------------------------------------------------------------------------
@@ -85,21 +120,41 @@ class JSONRPCServer {
85
120
  }
86
121
  }
87
122
  /**
88
- * Maps JSON-RPC method names to {@link PatchesServer} calls.
89
- * @param connectionId - The WebSocket transport object.
123
+ * Checks access control before method invocation.
124
+ * Called before each method invocation when using `register()`.
125
+ *
126
+ * @param access - The required access level ('read' or 'write')
127
+ * @param ctx - The authentication context
128
+ * @param method - The method being called
129
+ * @param args - The method arguments (first arg is typically docId)
130
+ * @throws StatusError if access is denied
131
+ */
132
+ async assertAccess(access, ctx, method, args) {
133
+ if (!this.auth) return;
134
+ const docId = args?.[0];
135
+ if (typeof docId !== "string") return;
136
+ const ok = await this.auth.canAccess(ctx, docId, access, method);
137
+ if (!ok) {
138
+ throw new StatusError(401, `${access.toUpperCase()}_FORBIDDEN:${docId}`);
139
+ }
140
+ }
141
+ /**
142
+ * Maps JSON-RPC method names to handler calls.
90
143
  * @param method - The JSON-RPC method name.
91
- * @param params - The JSON-RPC parameters.
92
- * @returns The result of the {@link PatchesServer} call.
144
+ * @param params - The JSON-RPC parameters (array of arguments).
145
+ * @param ctx - The authentication context.
146
+ * @returns The result of the handler call.
93
147
  */
94
148
  async _dispatch(method, params, ctx) {
95
149
  const handler = this.handlers.get(method);
96
150
  if (!handler) {
97
151
  throw new Error(`Unknown method '${method}'.`);
98
152
  }
99
- if (!params || typeof params !== "object" || Array.isArray(params)) {
100
- throw new Error(`Invalid parameters for method '${method}'.`);
101
- }
102
- return handler(params, ctx);
153
+ const args = Array.isArray(params) ? params : params === void 0 ? [] : [params];
154
+ setAuthContext(ctx);
155
+ const response = handler(...args);
156
+ clearAuthContext();
157
+ return response;
103
158
  }
104
159
  }
105
160
  export {