@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.
- package/README.md +221 -208
- package/dist/BaseDoc-DkP3tUhT.d.ts +206 -0
- package/dist/algorithms/client/applyCommittedChanges.d.ts +7 -0
- package/dist/algorithms/client/applyCommittedChanges.js +6 -3
- package/dist/algorithms/lww/consolidateOps.d.ts +40 -0
- package/dist/algorithms/lww/consolidateOps.js +103 -0
- package/dist/algorithms/lww/index.d.ts +2 -0
- package/dist/algorithms/lww/index.js +1 -0
- package/dist/algorithms/lww/mergeServerWithLocal.d.ts +22 -0
- package/dist/algorithms/lww/mergeServerWithLocal.js +32 -0
- package/dist/algorithms/server/commitChanges.d.ts +32 -8
- package/dist/algorithms/server/commitChanges.js +24 -10
- package/dist/algorithms/server/createVersion.d.ts +1 -1
- package/dist/algorithms/server/createVersion.js +2 -4
- package/dist/algorithms/server/getSnapshotAtRevision.d.ts +1 -1
- package/dist/algorithms/server/getStateAtRevision.d.ts +1 -1
- package/dist/algorithms/server/handleOfflineSessionsAndBatches.d.ts +1 -1
- package/dist/algorithms/server/handleOfflineSessionsAndBatches.js +5 -7
- package/dist/client/BaseDoc.d.ts +6 -0
- package/dist/client/BaseDoc.js +70 -0
- package/dist/client/ClientAlgorithm.d.ts +101 -0
- package/dist/client/ClientAlgorithm.js +0 -0
- package/dist/client/InMemoryStore.d.ts +5 -7
- package/dist/client/InMemoryStore.js +6 -35
- package/dist/client/IndexedDBStore.d.ts +39 -73
- package/dist/client/IndexedDBStore.js +17 -220
- package/dist/client/LWWAlgorithm.d.ts +43 -0
- package/dist/client/LWWAlgorithm.js +87 -0
- package/dist/client/LWWClientStore.d.ts +73 -0
- package/dist/client/LWWClientStore.js +0 -0
- package/dist/client/LWWDoc.d.ts +56 -0
- package/dist/client/LWWDoc.js +84 -0
- package/dist/client/LWWInMemoryStore.d.ts +88 -0
- package/dist/client/LWWInMemoryStore.js +208 -0
- package/dist/client/LWWIndexedDBStore.d.ts +91 -0
- package/dist/client/LWWIndexedDBStore.js +275 -0
- package/dist/client/OTAlgorithm.d.ts +42 -0
- package/dist/client/OTAlgorithm.js +113 -0
- package/dist/client/OTClientStore.d.ts +50 -0
- package/dist/client/OTClientStore.js +0 -0
- package/dist/client/OTDoc.d.ts +6 -0
- package/dist/client/OTDoc.js +97 -0
- package/dist/client/OTIndexedDBStore.d.ts +84 -0
- package/dist/client/OTIndexedDBStore.js +163 -0
- package/dist/client/Patches.d.ts +36 -16
- package/dist/client/Patches.js +60 -27
- package/dist/client/PatchesDoc.d.ts +4 -113
- package/dist/client/PatchesDoc.js +3 -153
- package/dist/client/PatchesStore.d.ts +8 -105
- package/dist/client/factories.d.ts +72 -0
- package/dist/client/factories.js +80 -0
- package/dist/client/index.d.ts +14 -5
- package/dist/client/index.js +9 -0
- package/dist/compression/index.d.ts +1 -1
- package/dist/data/change.js +4 -3
- package/dist/fractionalIndex.d.ts +67 -0
- package/dist/fractionalIndex.js +241 -0
- package/dist/index.d.ts +13 -4
- package/dist/index.js +1 -1
- package/dist/json-patch/types.d.ts +2 -0
- package/dist/net/PatchesClient.js +15 -15
- package/dist/net/PatchesSync.d.ts +24 -12
- package/dist/net/PatchesSync.js +56 -64
- package/dist/net/index.d.ts +6 -10
- package/dist/net/index.js +6 -1
- package/dist/net/protocol/JSONRPCClient.d.ts +4 -4
- package/dist/net/protocol/JSONRPCClient.js +6 -4
- package/dist/net/protocol/JSONRPCServer.d.ts +45 -9
- package/dist/net/protocol/JSONRPCServer.js +63 -8
- package/dist/net/serverContext.d.ts +38 -0
- package/dist/net/serverContext.js +20 -0
- package/dist/net/webrtc/WebRTCTransport.js +1 -1
- package/dist/net/websocket/AuthorizationProvider.d.ts +3 -3
- package/dist/net/websocket/WebSocketServer.d.ts +29 -20
- package/dist/net/websocket/WebSocketServer.js +23 -12
- package/dist/server/BranchManager.d.ts +50 -0
- package/dist/server/BranchManager.js +0 -0
- package/dist/server/CompressedStoreBackend.d.ts +8 -6
- package/dist/server/CompressedStoreBackend.js +3 -9
- package/dist/server/LWWBranchManager.d.ts +82 -0
- package/dist/server/LWWBranchManager.js +99 -0
- package/dist/server/LWWMemoryStoreBackend.d.ts +78 -0
- package/dist/server/LWWMemoryStoreBackend.js +191 -0
- package/dist/server/LWWServer.d.ts +130 -0
- package/dist/server/LWWServer.js +207 -0
- package/dist/server/{PatchesBranchManager.d.ts → OTBranchManager.d.ts} +32 -12
- package/dist/server/{PatchesBranchManager.js → OTBranchManager.js} +26 -42
- package/dist/server/OTServer.d.ts +108 -0
- package/dist/server/OTServer.js +141 -0
- package/dist/server/PatchesHistoryManager.d.ts +20 -7
- package/dist/server/PatchesHistoryManager.js +26 -3
- package/dist/server/PatchesServer.d.ts +70 -81
- package/dist/server/PatchesServer.js +0 -176
- package/dist/server/branchUtils.d.ts +82 -0
- package/dist/server/branchUtils.js +66 -0
- package/dist/server/index.d.ts +17 -6
- package/dist/server/index.js +33 -4
- package/dist/server/tombstone.d.ts +29 -0
- package/dist/server/tombstone.js +32 -0
- package/dist/server/types.d.ts +129 -27
- package/dist/server/utils.d.ts +12 -0
- package/dist/server/utils.js +23 -0
- package/dist/solid/context.d.ts +5 -4
- package/dist/solid/doc-manager.d.ts +3 -3
- package/dist/solid/index.d.ts +5 -4
- package/dist/solid/primitives.d.ts +2 -3
- package/dist/types.d.ts +16 -14
- package/dist/vue/composables.d.ts +2 -3
- package/dist/vue/doc-manager.d.ts +3 -3
- package/dist/vue/index.d.ts +5 -4
- package/dist/vue/provider.d.ts +5 -4
- package/package.json +1 -1
- package/dist/algorithms/client/collapsePendingChanges.d.ts +0 -30
- package/dist/algorithms/client/collapsePendingChanges.js +0 -78
- package/dist/net/websocket/RPCServer.d.ts +0 -141
- package/dist/net/websocket/RPCServer.js +0 -204
- package/dist/utils/dates.d.ts +0 -43
- 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 {
|
|
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 {
|
|
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
|
|
8
|
-
import
|
|
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 '../
|
|
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
|
|
116
|
-
*
|
|
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[]
|
|
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
|
|
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.
|
package/dist/net/PatchesSync.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
168
|
-
|
|
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
|
|
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
|
|
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
|
|
184
|
-
if (
|
|
185
|
-
|
|
203
|
+
await algorithm.store.saveDoc(docId, snapshot);
|
|
204
|
+
if (baseDoc) {
|
|
205
|
+
baseDoc.import({ ...snapshot, changes: [] });
|
|
186
206
|
}
|
|
187
207
|
}
|
|
188
208
|
}
|
|
189
|
-
if (
|
|
190
|
-
|
|
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 (
|
|
200
|
-
|
|
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)
|
|
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
|
|
244
|
-
|
|
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
|
|
265
|
-
*
|
|
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
|
|
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
|
-
|
|
275
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
362
|
+
await algorithm.confirmDeleteDoc(docId);
|
|
371
363
|
await this.onRemoteDocDeleted.emit(docId, pendingChanges);
|
|
372
364
|
}
|
|
373
365
|
/**
|
package/dist/net/index.d.ts
CHANGED
|
@@ -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 '../
|
|
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
|
|
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,
|
|
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
|
|
37
|
+
* @param args - The arguments to pass to the remote procedure (sent as array)
|
|
38
38
|
*/
|
|
39
|
-
notify(method: string,
|
|
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
|
|
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,
|
|
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
|
|
38
|
+
* @param args - The arguments to pass to the remote procedure (sent as array)
|
|
38
39
|
*/
|
|
39
|
-
notify(method,
|
|
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<
|
|
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<
|
|
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
|
-
*
|
|
72
|
-
*
|
|
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
|
-
* @
|
|
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
|
-
*
|
|
89
|
-
*
|
|
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
|
-
* @
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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 {
|