@dabble/patches 0.7.8 → 0.7.10
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/dist/{BaseDoc-_Rsau70J.d.ts → BaseDoc-BfVJNeCi.d.ts} +29 -16
- package/dist/client/BaseDoc.d.ts +1 -1
- package/dist/client/BaseDoc.js +31 -11
- package/dist/client/ClientAlgorithm.d.ts +1 -1
- package/dist/client/LWWAlgorithm.d.ts +1 -1
- package/dist/client/LWWDoc.d.ts +1 -1
- package/dist/client/LWWDoc.js +3 -0
- package/dist/client/OTAlgorithm.d.ts +1 -1
- package/dist/client/OTDoc.d.ts +1 -1
- package/dist/client/OTDoc.js +3 -0
- package/dist/client/Patches.d.ts +1 -1
- package/dist/client/PatchesDoc.d.ts +1 -1
- package/dist/client/factories.d.ts +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/net/PatchesSync.d.ts +42 -4
- package/dist/net/PatchesSync.js +158 -23
- package/dist/net/index.d.ts +4 -3
- package/dist/net/protocol/JSONRPCServer.js +8 -2
- package/dist/shared/doc-manager.d.ts +1 -1
- package/dist/shared/utils.d.ts +11 -1
- package/dist/shared/utils.js +5 -1
- package/dist/solid/context.d.ts +2 -1
- package/dist/solid/doc-manager.d.ts +1 -1
- package/dist/solid/index.d.ts +1 -1
- package/dist/solid/primitives.d.ts +1 -1
- package/dist/solid/primitives.js +53 -13
- package/dist/solid/utils.d.ts +4 -0
- package/dist/types.d.ts +24 -7
- package/dist/vue/composables.d.ts +1 -1
- package/dist/vue/composables.js +37 -14
- package/dist/vue/doc-manager.d.ts +1 -1
- package/dist/vue/index.d.ts +1 -1
- package/dist/vue/managed-docs.d.ts +1 -1
- package/dist/vue/provider.d.ts +2 -1
- package/dist/vue/utils.d.ts +4 -0
- package/package.json +1 -1
package/dist/net/PatchesSync.js
CHANGED
|
@@ -11,9 +11,17 @@ import { breakChangesIntoBatches } from "../algorithms/ot/shared/changeBatching.
|
|
|
11
11
|
import { BaseDoc } from "../client/BaseDoc.js";
|
|
12
12
|
import { Patches } from "../client/Patches.js";
|
|
13
13
|
import { signal } from "../event-signal.js";
|
|
14
|
+
import { isDocLoaded } from "../shared/utils.js";
|
|
14
15
|
import { blockable } from "../utils/concurrency.js";
|
|
15
16
|
import { PatchesWebSocket } from "./websocket/PatchesWebSocket.js";
|
|
16
17
|
import { onlineState } from "./websocket/onlineState.js";
|
|
18
|
+
const EMPTY_SYNCED_DOC = {
|
|
19
|
+
committedRev: 0,
|
|
20
|
+
hasPending: false,
|
|
21
|
+
syncStatus: "unsynced",
|
|
22
|
+
syncError: null,
|
|
23
|
+
isLoaded: false
|
|
24
|
+
};
|
|
17
25
|
_syncDoc_dec = [blockable], __receiveCommittedChanges_dec = [blockable];
|
|
18
26
|
class PatchesSync {
|
|
19
27
|
constructor(patches, url, options) {
|
|
@@ -27,7 +35,8 @@ class PatchesSync {
|
|
|
27
35
|
__publicField(this, "trackedDocs");
|
|
28
36
|
/** Maps docId to the algorithm name used for that doc */
|
|
29
37
|
__publicField(this, "docAlgorithms", /* @__PURE__ */ new Map());
|
|
30
|
-
__publicField(this, "_state", { online: false, connected: false,
|
|
38
|
+
__publicField(this, "_state", { online: false, connected: false, syncStatus: "unsynced", syncError: null });
|
|
39
|
+
__publicField(this, "_syncedDocs", {});
|
|
31
40
|
/**
|
|
32
41
|
* Signal emitted when the sync state changes.
|
|
33
42
|
*/
|
|
@@ -41,6 +50,10 @@ class PatchesSync {
|
|
|
41
50
|
* Provides the pending changes that were discarded so the application can handle them.
|
|
42
51
|
*/
|
|
43
52
|
__publicField(this, "onRemoteDocDeleted", signal());
|
|
53
|
+
/**
|
|
54
|
+
* Signal emitted when the synced doc map changes.
|
|
55
|
+
*/
|
|
56
|
+
__publicField(this, "onSyncedDocsChange", signal());
|
|
44
57
|
this.patches = patches;
|
|
45
58
|
this.maxPayloadBytes = options?.maxPayloadBytes;
|
|
46
59
|
this.maxStorageBytes = options?.maxStorageBytes ?? patches.docOptions?.maxStorageBytes;
|
|
@@ -93,6 +106,13 @@ class PatchesSync {
|
|
|
93
106
|
get state() {
|
|
94
107
|
return this._state;
|
|
95
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Map of all tracked documents and their current sync status.
|
|
111
|
+
* Updated immutably — new object reference on every change.
|
|
112
|
+
*/
|
|
113
|
+
get syncedDocs() {
|
|
114
|
+
return this._syncedDocs;
|
|
115
|
+
}
|
|
96
116
|
/**
|
|
97
117
|
* Gets the JSON-RPC client for making custom RPC calls.
|
|
98
118
|
* Useful for application-specific methods not part of the Patches protocol.
|
|
@@ -106,6 +126,9 @@ class PatchesSync {
|
|
|
106
126
|
*/
|
|
107
127
|
updateState(update) {
|
|
108
128
|
const newState = { ...this._state, ...update };
|
|
129
|
+
if (newState.syncStatus !== "error" && newState.syncError) {
|
|
130
|
+
newState.syncError = null;
|
|
131
|
+
}
|
|
109
132
|
if (!isEqual(this._state, newState)) {
|
|
110
133
|
this._state = newState;
|
|
111
134
|
this.onStateChange.emit(this._state);
|
|
@@ -119,8 +142,9 @@ class PatchesSync {
|
|
|
119
142
|
await this.ws.connect();
|
|
120
143
|
} catch (err) {
|
|
121
144
|
console.error("PatchesSync connection failed:", err);
|
|
122
|
-
|
|
123
|
-
this.
|
|
145
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
146
|
+
this.updateState({ connected: false, syncStatus: "error", syncError: error });
|
|
147
|
+
this.onError.emit(error);
|
|
124
148
|
throw err;
|
|
125
149
|
}
|
|
126
150
|
}
|
|
@@ -129,14 +153,15 @@ class PatchesSync {
|
|
|
129
153
|
*/
|
|
130
154
|
disconnect() {
|
|
131
155
|
this.ws.disconnect();
|
|
132
|
-
this.updateState({ connected: false,
|
|
156
|
+
this.updateState({ connected: false, syncStatus: "unsynced" });
|
|
157
|
+
this._resetSyncingStatuses();
|
|
133
158
|
}
|
|
134
159
|
/**
|
|
135
160
|
* Syncs all known docs when initially connected.
|
|
136
161
|
*/
|
|
137
162
|
async syncAllKnownDocs() {
|
|
138
163
|
if (!this.state.connected) return;
|
|
139
|
-
this.updateState({
|
|
164
|
+
this.updateState({ syncStatus: "syncing" });
|
|
140
165
|
try {
|
|
141
166
|
const allTracked = [];
|
|
142
167
|
for (const algorithm of Object.values(this.patches.algorithms)) {
|
|
@@ -153,9 +178,26 @@ class PatchesSync {
|
|
|
153
178
|
const deletedDocs = allTracked.filter((t) => t.deleted);
|
|
154
179
|
const activeDocIds = activeDocs.map((t) => t.docId);
|
|
155
180
|
this.trackedDocs = new Set(activeDocIds);
|
|
181
|
+
const syncedEntries = {};
|
|
182
|
+
for (const doc of activeDocs) {
|
|
183
|
+
const algorithm = this._getAlgorithm(doc.docId);
|
|
184
|
+
const pending = await algorithm.getPendingToSend(doc.docId);
|
|
185
|
+
const entry = {
|
|
186
|
+
committedRev: doc.committedRev,
|
|
187
|
+
hasPending: pending != null && pending.length > 0,
|
|
188
|
+
syncStatus: doc.committedRev === 0 ? "unsynced" : "synced",
|
|
189
|
+
syncError: null,
|
|
190
|
+
isLoaded: false
|
|
191
|
+
};
|
|
192
|
+
const existing = this._syncedDocs[doc.docId];
|
|
193
|
+
entry.isLoaded = existing?.isLoaded || isDocLoaded(entry);
|
|
194
|
+
syncedEntries[doc.docId] = entry;
|
|
195
|
+
}
|
|
196
|
+
this._syncedDocs = syncedEntries;
|
|
197
|
+
this.onSyncedDocsChange.emit(this._syncedDocs);
|
|
156
198
|
if (activeDocIds.length > 0) {
|
|
157
199
|
try {
|
|
158
|
-
const subscribeIds = this.
|
|
200
|
+
const subscribeIds = this._filterSubscribeIds(activeDocIds);
|
|
159
201
|
if (subscribeIds.length) {
|
|
160
202
|
await this.ws.subscribe(subscribeIds);
|
|
161
203
|
}
|
|
@@ -178,20 +220,22 @@ class PatchesSync {
|
|
|
178
220
|
}
|
|
179
221
|
});
|
|
180
222
|
await Promise.all([...activeSyncPromises, ...deletePromises]);
|
|
181
|
-
this.updateState({
|
|
223
|
+
this.updateState({ syncStatus: "synced" });
|
|
182
224
|
} catch (error) {
|
|
183
225
|
console.error("Error during global sync:", error);
|
|
184
|
-
|
|
185
|
-
this.
|
|
226
|
+
const syncError = error instanceof Error ? error : new Error(String(error));
|
|
227
|
+
this.updateState({ syncStatus: "error", syncError });
|
|
228
|
+
this.onError.emit(syncError);
|
|
186
229
|
}
|
|
187
230
|
}
|
|
188
231
|
async syncDoc(docId) {
|
|
189
232
|
if (!this.state.connected) return;
|
|
233
|
+
this._updateSyncedDoc(docId, { syncStatus: "syncing" });
|
|
190
234
|
const doc = this.patches.getOpenDoc(docId);
|
|
191
235
|
const algorithm = this._getAlgorithm(docId);
|
|
192
236
|
const baseDoc = doc;
|
|
193
237
|
if (baseDoc) {
|
|
194
|
-
baseDoc.
|
|
238
|
+
baseDoc.updateSyncStatus("syncing");
|
|
195
239
|
}
|
|
196
240
|
try {
|
|
197
241
|
const pending = await algorithm.getPendingToSend(docId);
|
|
@@ -212,18 +256,21 @@ class PatchesSync {
|
|
|
212
256
|
}
|
|
213
257
|
}
|
|
214
258
|
}
|
|
259
|
+
this._updateSyncedDoc(docId, { syncStatus: "synced" });
|
|
215
260
|
if (baseDoc) {
|
|
216
|
-
baseDoc.
|
|
261
|
+
baseDoc.updateSyncStatus("synced");
|
|
217
262
|
}
|
|
218
263
|
} catch (err) {
|
|
219
264
|
if (this._isDocDeletedError(err)) {
|
|
220
265
|
await this._handleRemoteDocDeleted(docId);
|
|
221
266
|
return;
|
|
222
267
|
}
|
|
268
|
+
const syncError = err instanceof Error ? err : new Error(String(err));
|
|
269
|
+
this._updateSyncedDoc(docId, { syncStatus: "error", syncError });
|
|
223
270
|
console.error(`Error syncing doc ${docId}:`, err);
|
|
224
|
-
this.onError.emit(
|
|
271
|
+
this.onError.emit(syncError, { docId });
|
|
225
272
|
if (baseDoc) {
|
|
226
|
-
baseDoc.
|
|
273
|
+
baseDoc.updateSyncStatus("error", syncError);
|
|
227
274
|
}
|
|
228
275
|
}
|
|
229
276
|
}
|
|
@@ -261,13 +308,17 @@ class PatchesSync {
|
|
|
261
308
|
await algorithm.confirmSent(docId, batch);
|
|
262
309
|
pending = await algorithm.getPendingToSend(docId) ?? [];
|
|
263
310
|
}
|
|
311
|
+
const stillHasPending = pending != null && pending.length > 0;
|
|
312
|
+
this._updateSyncedDoc(docId, { hasPending: stillHasPending, syncStatus: "synced" });
|
|
264
313
|
} catch (err) {
|
|
265
314
|
if (this._isDocDeletedError(err)) {
|
|
266
315
|
await this._handleRemoteDocDeleted(docId);
|
|
267
316
|
return;
|
|
268
317
|
}
|
|
318
|
+
const flushError = err instanceof Error ? err : new Error(String(err));
|
|
319
|
+
this._updateSyncedDoc(docId, { syncStatus: "error", syncError: flushError });
|
|
269
320
|
console.error(`Flush failed for doc ${docId}:`, err);
|
|
270
|
-
this.onError.emit(
|
|
321
|
+
this.onError.emit(flushError, { docId });
|
|
271
322
|
throw err;
|
|
272
323
|
}
|
|
273
324
|
}
|
|
@@ -286,6 +337,10 @@ class PatchesSync {
|
|
|
286
337
|
const doc = this.patches.getOpenDoc(docId);
|
|
287
338
|
const algorithm = this._getAlgorithm(docId);
|
|
288
339
|
await algorithm.applyServerChanges(docId, serverChanges, doc);
|
|
340
|
+
if (serverChanges.length > 0) {
|
|
341
|
+
const lastRev = serverChanges[serverChanges.length - 1].rev;
|
|
342
|
+
this._updateSyncedDoc(docId, { committedRev: lastRev });
|
|
343
|
+
}
|
|
289
344
|
}
|
|
290
345
|
/**
|
|
291
346
|
* Initiates the deletion process for a document both locally and on the server.
|
|
@@ -309,15 +364,18 @@ class PatchesSync {
|
|
|
309
364
|
_handleConnectionChange(connectionState) {
|
|
310
365
|
const isConnected = connectionState === "connected";
|
|
311
366
|
const isConnecting = connectionState === "connecting";
|
|
312
|
-
const
|
|
313
|
-
this.updateState({ connected: isConnected,
|
|
367
|
+
const newSyncStatus = isConnected ? this._state.syncStatus : isConnecting ? this._state.syncStatus : "unsynced";
|
|
368
|
+
this.updateState({ connected: isConnected, syncStatus: newSyncStatus });
|
|
314
369
|
if (isConnected) {
|
|
315
370
|
void this.syncAllKnownDocs();
|
|
371
|
+
} else if (!isConnecting) {
|
|
372
|
+
this._resetSyncingStatuses();
|
|
316
373
|
}
|
|
317
374
|
}
|
|
318
375
|
async _handleDocsTracked(docIds) {
|
|
319
376
|
const newIds = docIds.filter((id) => !this.trackedDocs.has(id));
|
|
320
377
|
if (!newIds.length) return;
|
|
378
|
+
const alreadySubscribed = this._getActiveSubscriptions();
|
|
321
379
|
newIds.forEach((id) => this.trackedDocs.add(id));
|
|
322
380
|
for (const docId of newIds) {
|
|
323
381
|
for (const algorithm of Object.values(this.patches.algorithms)) {
|
|
@@ -330,13 +388,24 @@ class PatchesSync {
|
|
|
330
388
|
}
|
|
331
389
|
}
|
|
332
390
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
const
|
|
336
|
-
|
|
391
|
+
for (const docId of newIds) {
|
|
392
|
+
const algorithm = this._getAlgorithm(docId);
|
|
393
|
+
const committedRev = await algorithm.getCommittedRev(docId);
|
|
394
|
+
const pending = await algorithm.getPendingToSend(docId);
|
|
395
|
+
this._updateSyncedDoc(
|
|
396
|
+
docId,
|
|
397
|
+
{
|
|
398
|
+
committedRev,
|
|
399
|
+
hasPending: pending != null && pending.length > 0,
|
|
400
|
+
syncStatus: committedRev === 0 ? "unsynced" : "synced"
|
|
401
|
+
},
|
|
402
|
+
false
|
|
403
|
+
);
|
|
337
404
|
}
|
|
405
|
+
this._emitSyncedChange();
|
|
338
406
|
if (this.state.connected) {
|
|
339
407
|
try {
|
|
408
|
+
const subscribeIds = this._filterSubscribeIds(newIds).filter((id) => !alreadySubscribed.has(id));
|
|
340
409
|
if (subscribeIds.length) {
|
|
341
410
|
await this.ws.subscribe(subscribeIds);
|
|
342
411
|
}
|
|
@@ -350,8 +419,12 @@ class PatchesSync {
|
|
|
350
419
|
async _handleDocsUntracked(docIds) {
|
|
351
420
|
const existingIds = docIds.filter((id) => this.trackedDocs.has(id));
|
|
352
421
|
if (!existingIds.length) return;
|
|
422
|
+
const subscribedBefore = this._getActiveSubscriptions();
|
|
353
423
|
existingIds.forEach((id) => this.trackedDocs.delete(id));
|
|
354
|
-
|
|
424
|
+
existingIds.forEach((id) => this._updateSyncedDoc(id, void 0, false));
|
|
425
|
+
this._emitSyncedChange();
|
|
426
|
+
const subscribedAfter = this._getActiveSubscriptions();
|
|
427
|
+
const unsubscribeIds = [...subscribedBefore].filter((id) => !subscribedAfter.has(id));
|
|
355
428
|
if (this.state.connected && unsubscribeIds.length) {
|
|
356
429
|
try {
|
|
357
430
|
await this.ws.unsubscribe(unsubscribeIds);
|
|
@@ -361,8 +434,10 @@ class PatchesSync {
|
|
|
361
434
|
}
|
|
362
435
|
}
|
|
363
436
|
async _handleDocChange(docId) {
|
|
364
|
-
if (!this.state.connected) return;
|
|
365
437
|
if (!this.trackedDocs.has(docId)) return;
|
|
438
|
+
this._updateSyncedDoc(docId, { hasPending: true }, !this.state.connected);
|
|
439
|
+
if (!this.state.connected) return;
|
|
440
|
+
this._updateSyncedDoc(docId, { syncStatus: "syncing" });
|
|
366
441
|
await this.flushDoc(docId);
|
|
367
442
|
}
|
|
368
443
|
/**
|
|
@@ -377,9 +452,68 @@ class PatchesSync {
|
|
|
377
452
|
await this.patches.closeDoc(docId);
|
|
378
453
|
}
|
|
379
454
|
this.trackedDocs.delete(docId);
|
|
455
|
+
this._updateSyncedDoc(docId, void 0);
|
|
380
456
|
await algorithm.confirmDeleteDoc(docId);
|
|
381
457
|
await this.onRemoteDocDeleted.emit(docId, pendingChanges);
|
|
382
458
|
}
|
|
459
|
+
/**
|
|
460
|
+
* Adds, updates, or removes a synced doc entry immutably and emits onSyncedChange.
|
|
461
|
+
* - Pass a full SyncedDoc to add a new entry or overwrite an existing one.
|
|
462
|
+
* - Pass a Partial<SyncedDoc> to merge into an existing entry (no-ops if doc not in map).
|
|
463
|
+
* - Pass undefined to remove the entry.
|
|
464
|
+
* No-ops if nothing actually changed.
|
|
465
|
+
*/
|
|
466
|
+
_updateSyncedDoc(docId, updates, emit = true) {
|
|
467
|
+
if (updates === void 0) {
|
|
468
|
+
if (!(docId in this._syncedDocs)) return;
|
|
469
|
+
this._syncedDocs = { ...this._syncedDocs };
|
|
470
|
+
delete this._syncedDocs[docId];
|
|
471
|
+
} else {
|
|
472
|
+
const updated = { ...EMPTY_SYNCED_DOC, ...this._syncedDocs[docId], ...updates };
|
|
473
|
+
if (updated.syncStatus !== "error" && updated.syncError) {
|
|
474
|
+
updated.syncError = null;
|
|
475
|
+
}
|
|
476
|
+
if (!updated.isLoaded) {
|
|
477
|
+
updated.isLoaded = isDocLoaded(updated);
|
|
478
|
+
}
|
|
479
|
+
if (isEqual(this._syncedDocs[docId], updated)) return;
|
|
480
|
+
this._syncedDocs = { ...this._syncedDocs, [docId]: updated };
|
|
481
|
+
}
|
|
482
|
+
if (emit) this._emitSyncedChange();
|
|
483
|
+
}
|
|
484
|
+
_emitSyncedChange() {
|
|
485
|
+
this.onSyncedDocsChange.emit(this._syncedDocs);
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Resets any docs with status 'syncing' back to a stable state on disconnect.
|
|
489
|
+
* Uses hasPending to decide: pending docs become 'synced' (they have local data),
|
|
490
|
+
* docs with no pending and no committed rev become 'unsynced'.
|
|
491
|
+
*/
|
|
492
|
+
_resetSyncingStatuses() {
|
|
493
|
+
let changed = false;
|
|
494
|
+
for (const [docId, doc] of Object.entries(this._syncedDocs)) {
|
|
495
|
+
if (doc.syncStatus === "syncing") {
|
|
496
|
+
const newStatus = doc.committedRev === 0 && !doc.hasPending ? "unsynced" : "synced";
|
|
497
|
+
this._updateSyncedDoc(docId, { syncStatus: newStatus }, false);
|
|
498
|
+
changed = true;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
if (changed) this._emitSyncedChange();
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Applies the subscribeFilter option to a list of doc IDs, returning the subset
|
|
505
|
+
* that should be sent to ws.subscribe/unsubscribe. Returns the full list if no filter is set.
|
|
506
|
+
*/
|
|
507
|
+
_filterSubscribeIds(docIds) {
|
|
508
|
+
return this.options?.subscribeFilter?.(docIds) || docIds;
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Returns the set of doc IDs currently subscribed on the server, derived by
|
|
512
|
+
* applying the subscribe filter to the full tracked set.
|
|
513
|
+
*/
|
|
514
|
+
_getActiveSubscriptions() {
|
|
515
|
+
return new Set(this._filterSubscribeIds([...this.trackedDocs]));
|
|
516
|
+
}
|
|
383
517
|
/**
|
|
384
518
|
* Helper to detect DOC_DELETED (410) errors from the server.
|
|
385
519
|
*/
|
|
@@ -392,5 +526,6 @@ __decorateElement(_init, 1, "syncDoc", _syncDoc_dec, PatchesSync);
|
|
|
392
526
|
__decorateElement(_init, 1, "_receiveCommittedChanges", __receiveCommittedChanges_dec, PatchesSync);
|
|
393
527
|
__decoratorMetadata(_init, PatchesSync);
|
|
394
528
|
export {
|
|
395
|
-
PatchesSync
|
|
529
|
+
PatchesSync,
|
|
530
|
+
isDocLoaded
|
|
396
531
|
};
|
package/dist/net/index.d.ts
CHANGED
|
@@ -12,14 +12,15 @@ export { PatchesWebSocket } from './websocket/PatchesWebSocket.js';
|
|
|
12
12
|
export { JsonRpcMessage, SignalingService } from './websocket/SignalingService.js';
|
|
13
13
|
export { WebSocketServer, WebSocketServerOptions } from './websocket/WebSocketServer.js';
|
|
14
14
|
export { WebSocketOptions, WebSocketTransport } from './websocket/WebSocketTransport.js';
|
|
15
|
+
export { isDocLoaded } from '../shared/utils.js';
|
|
15
16
|
export { CommitChangesOptions } from '../types.js';
|
|
16
17
|
import '../event-signal.js';
|
|
17
18
|
import '../algorithms/ot/shared/changeBatching.js';
|
|
18
|
-
import '../client/Patches.js';
|
|
19
|
-
import '../json-patch/types.js';
|
|
20
19
|
import '../client/ClientAlgorithm.js';
|
|
21
|
-
import '../
|
|
20
|
+
import '../json-patch/types.js';
|
|
21
|
+
import '../BaseDoc-BfVJNeCi.js';
|
|
22
22
|
import '../client/PatchesStore.js';
|
|
23
|
+
import '../client/Patches.js';
|
|
23
24
|
import '../server/types.js';
|
|
24
25
|
import '../utils/deferred.js';
|
|
25
26
|
import '../json-patch/JSONPatch.js';
|
|
@@ -54,7 +54,10 @@ class JSONRPCServer {
|
|
|
54
54
|
this.registerMethod(method, async (...args) => {
|
|
55
55
|
const docId = args[0];
|
|
56
56
|
if (typeof docId !== "string" || !docId) {
|
|
57
|
-
throw new StatusError(
|
|
57
|
+
throw new StatusError(
|
|
58
|
+
400,
|
|
59
|
+
`INVALID_REQUEST: docId is required (got ${docId === "" ? "empty string" : String(docId)})`
|
|
60
|
+
);
|
|
58
61
|
}
|
|
59
62
|
const ctx = getAuthContext();
|
|
60
63
|
await this.assertAccess(access, ctx, method, args);
|
|
@@ -137,7 +140,10 @@ class JSONRPCServer {
|
|
|
137
140
|
if (!this.auth) return;
|
|
138
141
|
const docId = args?.[0];
|
|
139
142
|
if (typeof docId !== "string" || !docId) {
|
|
140
|
-
throw new StatusError(
|
|
143
|
+
throw new StatusError(
|
|
144
|
+
400,
|
|
145
|
+
`INVALID_REQUEST: docId is required (got ${docId === "" ? "empty string" : String(docId)})`
|
|
146
|
+
);
|
|
141
147
|
}
|
|
142
148
|
const ok = await this.auth.canAccess(ctx, docId, access, method);
|
|
143
149
|
if (!ok) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Patches, OpenDocOptions } from '../client/Patches.js';
|
|
2
|
-
import { a as PatchesDoc } from '../BaseDoc-
|
|
2
|
+
import { a as PatchesDoc } from '../BaseDoc-BfVJNeCi.js';
|
|
3
3
|
import '../event-signal.js';
|
|
4
4
|
import '../json-patch/types.js';
|
|
5
5
|
import '../types.js';
|
package/dist/shared/utils.d.ts
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
import { SyncedDoc } from '../types.js';
|
|
2
|
+
import '../json-patch/JSONPatch.js';
|
|
3
|
+
import '@dabble/delta';
|
|
4
|
+
import '../json-patch/types.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Returns true if a document has completed its initial load — i.e., it has data
|
|
8
|
+
* to display (server data, cached data, or local changes) or sync has resolved.
|
|
9
|
+
*/
|
|
10
|
+
declare function isDocLoaded(doc: Pick<SyncedDoc, 'committedRev' | 'hasPending' | 'syncStatus'>): boolean;
|
|
1
11
|
/**
|
|
2
12
|
* Resolves a path template by replacing `:param` placeholders with values.
|
|
3
13
|
*
|
|
@@ -19,4 +29,4 @@ declare function fillPath(template: string, params: Record<string, string>): str
|
|
|
19
29
|
*/
|
|
20
30
|
declare function areSetsEqual(a: Set<string>, b: Set<string>): boolean;
|
|
21
31
|
|
|
22
|
-
export { areSetsEqual, fillPath };
|
|
32
|
+
export { areSetsEqual, fillPath, isDocLoaded };
|
package/dist/shared/utils.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import "../chunk-IZ2YBCUP.js";
|
|
2
|
+
function isDocLoaded(doc) {
|
|
3
|
+
return doc.committedRev > 0 || doc.hasPending || doc.syncStatus === "synced" || doc.syncStatus === "error";
|
|
4
|
+
}
|
|
2
5
|
function fillPath(template, params) {
|
|
3
6
|
return template.replace(/:(\w+)/g, (match, name) => {
|
|
4
7
|
const value = params[name];
|
|
@@ -18,5 +21,6 @@ function areSetsEqual(a, b) {
|
|
|
18
21
|
}
|
|
19
22
|
export {
|
|
20
23
|
areSetsEqual,
|
|
21
|
-
fillPath
|
|
24
|
+
fillPath,
|
|
25
|
+
isDocLoaded
|
|
22
26
|
};
|
package/dist/solid/context.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ import '../types.js';
|
|
|
7
7
|
import '../json-patch/JSONPatch.js';
|
|
8
8
|
import '@dabble/delta';
|
|
9
9
|
import '../client/ClientAlgorithm.js';
|
|
10
|
-
import '../BaseDoc-
|
|
10
|
+
import '../BaseDoc-BfVJNeCi.js';
|
|
11
11
|
import '../client/PatchesStore.js';
|
|
12
12
|
import '../net/protocol/types.js';
|
|
13
13
|
import '../net/protocol/JSONRPCClient.js';
|
|
@@ -16,6 +16,7 @@ import '../net/PatchesClient.js';
|
|
|
16
16
|
import '../net/websocket/WebSocketTransport.js';
|
|
17
17
|
import '../utils/deferred.js';
|
|
18
18
|
import '../algorithms/ot/shared/changeBatching.js';
|
|
19
|
+
import '../shared/utils.js';
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* Context value containing Patches and optional PatchesSync instances.
|
package/dist/solid/index.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ import '../types.js';
|
|
|
11
11
|
import '../json-patch/JSONPatch.js';
|
|
12
12
|
import '@dabble/delta';
|
|
13
13
|
import '../client/ClientAlgorithm.js';
|
|
14
|
-
import '../BaseDoc-
|
|
14
|
+
import '../BaseDoc-BfVJNeCi.js';
|
|
15
15
|
import '../client/PatchesStore.js';
|
|
16
16
|
import '../net/PatchesSync.js';
|
|
17
17
|
import '../net/protocol/types.js';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Accessor } from 'solid-js';
|
|
2
2
|
import { OpenDocOptions } from '../client/Patches.js';
|
|
3
|
-
import { a as PatchesDoc } from '../BaseDoc-
|
|
3
|
+
import { a as PatchesDoc } from '../BaseDoc-BfVJNeCi.js';
|
|
4
4
|
import { JSONPatch } from '../json-patch/JSONPatch.js';
|
|
5
5
|
import { ChangeMutator } from '../types.js';
|
|
6
6
|
import '../event-signal.js';
|
package/dist/solid/primitives.js
CHANGED
|
@@ -11,7 +11,7 @@ import { JSONPatch } from "../json-patch/JSONPatch.js";
|
|
|
11
11
|
import { usePatchesContext } from "./context.js";
|
|
12
12
|
import { getDocManager } from "./doc-manager.js";
|
|
13
13
|
function createDocReactiveState(options) {
|
|
14
|
-
const { initialLoading = true, transformState, changeBehavior } = options;
|
|
14
|
+
const { initialLoading = true, hasSyncContext = false, transformState, changeBehavior } = options;
|
|
15
15
|
const [doc, setDoc] = createSignal(void 0);
|
|
16
16
|
const [data, setData] = createSignal(void 0);
|
|
17
17
|
const [loading, setLoading] = createSignal(initialLoading);
|
|
@@ -20,6 +20,19 @@ function createDocReactiveState(options) {
|
|
|
20
20
|
const [hasPending, setHasPending] = createSignal(false);
|
|
21
21
|
function setupDoc(patchesDoc) {
|
|
22
22
|
setDoc(patchesDoc);
|
|
23
|
+
let loaded = false;
|
|
24
|
+
function updateLoading() {
|
|
25
|
+
if (loaded) return;
|
|
26
|
+
if (patchesDoc.isLoaded) {
|
|
27
|
+
loaded = true;
|
|
28
|
+
setLoading(false);
|
|
29
|
+
} else if (patchesDoc.syncStatus === "syncing") {
|
|
30
|
+
setLoading(true);
|
|
31
|
+
} else if (!hasSyncContext) {
|
|
32
|
+
loaded = true;
|
|
33
|
+
setLoading(false);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
23
36
|
const unsubState = patchesDoc.subscribe((state) => {
|
|
24
37
|
if (transformState && state) {
|
|
25
38
|
state = transformState(state, patchesDoc);
|
|
@@ -27,12 +40,12 @@ function createDocReactiveState(options) {
|
|
|
27
40
|
setData(() => state);
|
|
28
41
|
setRev(patchesDoc.committedRev);
|
|
29
42
|
setHasPending(patchesDoc.hasPending);
|
|
43
|
+
updateLoading();
|
|
30
44
|
});
|
|
31
|
-
const unsubSync = patchesDoc.
|
|
32
|
-
|
|
33
|
-
setError(
|
|
45
|
+
const unsubSync = patchesDoc.onSyncStatus((status) => {
|
|
46
|
+
updateLoading();
|
|
47
|
+
setError(status === "error" ? patchesDoc.syncError : null);
|
|
34
48
|
});
|
|
35
|
-
setLoading(patchesDoc.syncing !== null);
|
|
36
49
|
return () => {
|
|
37
50
|
unsubState();
|
|
38
51
|
unsubSync();
|
|
@@ -66,7 +79,24 @@ function createDocReactiveState(options) {
|
|
|
66
79
|
change,
|
|
67
80
|
doc
|
|
68
81
|
};
|
|
69
|
-
return {
|
|
82
|
+
return {
|
|
83
|
+
doc,
|
|
84
|
+
setDoc,
|
|
85
|
+
data,
|
|
86
|
+
setData,
|
|
87
|
+
loading,
|
|
88
|
+
setLoading,
|
|
89
|
+
error,
|
|
90
|
+
setError,
|
|
91
|
+
rev,
|
|
92
|
+
setRev,
|
|
93
|
+
hasPending,
|
|
94
|
+
setHasPending,
|
|
95
|
+
setupDoc,
|
|
96
|
+
resetSignals,
|
|
97
|
+
change,
|
|
98
|
+
baseReturn
|
|
99
|
+
};
|
|
70
100
|
}
|
|
71
101
|
function usePatchesDoc(docIdOrOptions, options) {
|
|
72
102
|
if (typeof docIdOrOptions === "string" || typeof docIdOrOptions === "function") {
|
|
@@ -75,12 +105,16 @@ function usePatchesDoc(docIdOrOptions, options) {
|
|
|
75
105
|
return _usePatchesDocLazy(docIdOrOptions ?? {});
|
|
76
106
|
}
|
|
77
107
|
function _usePatchesDocEager(docId, options) {
|
|
78
|
-
const { patches } = usePatchesContext();
|
|
108
|
+
const { patches, sync } = usePatchesContext();
|
|
79
109
|
const { autoClose = false, algorithm, metadata } = options;
|
|
80
110
|
const shouldUntrack = autoClose === "untrack";
|
|
81
111
|
const openDocOpts = { algorithm, metadata };
|
|
82
112
|
const manager = getDocManager(patches);
|
|
83
|
-
const { setupDoc, setError, setLoading, baseReturn } = createDocReactiveState({
|
|
113
|
+
const { setupDoc, setError, setLoading, baseReturn } = createDocReactiveState({
|
|
114
|
+
initialLoading: !!autoClose,
|
|
115
|
+
hasSyncContext: !!sync,
|
|
116
|
+
changeBehavior: "throw"
|
|
117
|
+
});
|
|
84
118
|
const docIdAccessor = toAccessor(docId);
|
|
85
119
|
if (autoClose) {
|
|
86
120
|
const [docResource] = createResource(docIdAccessor, async (id) => {
|
|
@@ -122,10 +156,11 @@ function _usePatchesDocEager(docId, options) {
|
|
|
122
156
|
return baseReturn;
|
|
123
157
|
}
|
|
124
158
|
function _usePatchesDocLazy(options) {
|
|
125
|
-
const { patches } = usePatchesContext();
|
|
159
|
+
const { patches, sync } = usePatchesContext();
|
|
126
160
|
const { idProp } = options;
|
|
127
161
|
const { setupDoc, resetSignals, setError, setLoading, baseReturn } = createDocReactiveState({
|
|
128
162
|
initialLoading: false,
|
|
163
|
+
hasSyncContext: !!sync,
|
|
129
164
|
changeBehavior: "noop",
|
|
130
165
|
transformState: idProp ? (state, patchesDoc) => ({ ...state, [idProp]: patchesDoc.id }) : void 0
|
|
131
166
|
});
|
|
@@ -143,6 +178,7 @@ function _usePatchesDocLazy(options) {
|
|
|
143
178
|
await patches.closeDoc(prevPath);
|
|
144
179
|
}
|
|
145
180
|
setPath(docPath);
|
|
181
|
+
setLoading(true);
|
|
146
182
|
try {
|
|
147
183
|
const patchesDoc = await patches.openDoc(docPath, options2);
|
|
148
184
|
unsubscribe = setupDoc(patchesDoc);
|
|
@@ -180,11 +216,11 @@ function usePatchesSync() {
|
|
|
180
216
|
throw new Error("PatchesSync not found in context. Did you forget to pass sync to PatchesProvider?");
|
|
181
217
|
}
|
|
182
218
|
const [connected, setConnected] = createSignal(sync.state.connected);
|
|
183
|
-
const [syncing, setSyncing] = createSignal(sync.state.
|
|
219
|
+
const [syncing, setSyncing] = createSignal(sync.state.syncStatus === "syncing");
|
|
184
220
|
const [online, setOnline] = createSignal(sync.state.online);
|
|
185
221
|
const unsubscribe = sync.onStateChange((state) => {
|
|
186
222
|
setConnected(state.connected);
|
|
187
|
-
setSyncing(state.
|
|
223
|
+
setSyncing(state.syncStatus === "syncing");
|
|
188
224
|
setOnline(state.online);
|
|
189
225
|
});
|
|
190
226
|
onCleanup(() => {
|
|
@@ -202,12 +238,16 @@ function toAccessor(value) {
|
|
|
202
238
|
function createPatchesDoc(name) {
|
|
203
239
|
const Context = createContext();
|
|
204
240
|
function Provider(props) {
|
|
205
|
-
const { patches } = usePatchesContext();
|
|
241
|
+
const { patches, sync } = usePatchesContext();
|
|
206
242
|
const manager = getDocManager(patches);
|
|
207
243
|
const autoClose = props.autoClose ?? false;
|
|
208
244
|
const shouldUntrack = autoClose === "untrack";
|
|
209
245
|
const openDocOpts = { algorithm: props.algorithm, metadata: props.metadata };
|
|
210
|
-
const { setupDoc, setError, setLoading, baseReturn } = createDocReactiveState({
|
|
246
|
+
const { setupDoc, setError, setLoading, baseReturn } = createDocReactiveState({
|
|
247
|
+
initialLoading: !!autoClose,
|
|
248
|
+
hasSyncContext: !!sync,
|
|
249
|
+
changeBehavior: "throw"
|
|
250
|
+
});
|
|
211
251
|
const docIdAccessor = toAccessor(props.docId);
|
|
212
252
|
if (autoClose) {
|
|
213
253
|
const [docResource] = createResource(docIdAccessor, async (id) => {
|