@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.
@@ -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, syncing: null });
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
- this.updateState({ connected: false, syncing: err instanceof Error ? err : new Error(String(err)) });
123
- this.onError.emit(err);
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, syncing: null });
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({ syncing: "updating" });
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.options?.subscribeFilter?.(activeDocIds) || activeDocIds;
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({ syncing: null });
223
+ this.updateState({ syncStatus: "synced" });
182
224
  } catch (error) {
183
225
  console.error("Error during global sync:", error);
184
- this.updateState({ syncing: error instanceof Error ? error : new Error(String(error)) });
185
- this.onError.emit(error);
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.updateSyncing("updating");
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.updateSyncing(null);
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(err, { docId });
271
+ this.onError.emit(syncError, { docId });
225
272
  if (baseDoc) {
226
- baseDoc.updateSyncing(err instanceof Error ? err : new Error(String(err)));
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(err, { docId });
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 newSyncingState = isConnected ? this._state.syncing : isConnecting ? this._state.syncing : null;
313
- this.updateState({ connected: isConnected, syncing: newSyncingState });
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
- let subscribeIds = newIds;
334
- if (this.options?.subscribeFilter) {
335
- const alreadyTracked = this.options.subscribeFilter([...this.trackedDocs]);
336
- subscribeIds = subscribeIds.filter((id) => !alreadyTracked.includes(id));
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
- const unsubscribeIds = this.options?.subscribeFilter?.(existingIds) || existingIds;
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
  };
@@ -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 '../BaseDoc-_Rsau70J.js';
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(400, `INVALID_REQUEST: docId is required (got ${docId === "" ? "empty string" : String(docId)})`);
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(400, `INVALID_REQUEST: docId is required (got ${docId === "" ? "empty string" : String(docId)})`);
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-_Rsau70J.js';
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';
@@ -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 };
@@ -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
  };
@@ -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-_Rsau70J.js';
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.
@@ -6,5 +6,5 @@ import '../types.js';
6
6
  import '../json-patch/JSONPatch.js';
7
7
  import '@dabble/delta';
8
8
  import '../client/ClientAlgorithm.js';
9
- import '../BaseDoc-_Rsau70J.js';
9
+ import '../BaseDoc-BfVJNeCi.js';
10
10
  import '../client/PatchesStore.js';
@@ -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-_Rsau70J.js';
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-_Rsau70J.js';
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';
@@ -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.onSyncing((syncState) => {
32
- setLoading(syncState === "initial" || syncState === "updating");
33
- setError(syncState instanceof Error ? syncState : null);
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 { doc, setDoc, data, setData, loading, setLoading, error, setError, rev, setRev, hasPending, setHasPending, setupDoc, resetSignals, change, baseReturn };
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({ changeBehavior: "throw" });
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.syncing === "updating");
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.syncing === "updating");
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({ changeBehavior: "throw" });
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) => {
@@ -1 +1,5 @@
1
1
  export { areSetsEqual, fillPath } from '../shared/utils.js';
2
+ import '../types.js';
3
+ import '../json-patch/JSONPatch.js';
4
+ import '@dabble/delta';
5
+ import '../json-patch/types.js';