@dabble/patches 0.8.5 → 0.8.6

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.
@@ -144,7 +144,7 @@ declare class PatchesSync extends ReadonlyStoreClass<PatchesSyncState> {
144
144
  protected _handleConnectionChange(connectionState: ConnectionState): void;
145
145
  protected _handleDocsTracked(docIds: string[], algorithmName?: AlgorithmName): Promise<void>;
146
146
  protected _handleDocsUntracked(docIds: string[]): Promise<void>;
147
- protected _handleDocChange(docId: string): Promise<void>;
147
+ protected _handleDocChange(docId: string): void;
148
148
  /**
149
149
  * Unified handler for remote document deletion (both real-time notifications and offline discovery).
150
150
  * Cleans up local state and notifies the application with any pending changes that were lost.
@@ -12,7 +12,7 @@ import { breakChangesIntoBatches } from "../algorithms/ot/shared/changeBatching.
12
12
  import { BaseDoc } from "../client/BaseDoc.js";
13
13
  import { Patches } from "../client/Patches.js";
14
14
  import { isDocLoaded } from "../shared/utils.js";
15
- import { blockable } from "../utils/concurrency.js";
15
+ import { blockable, serialGate } from "../utils/concurrency.js";
16
16
  import { ErrorCodes, StatusError } from "./error.js";
17
17
  import { PatchesWebSocket } from "./websocket/PatchesWebSocket.js";
18
18
  import { onlineState } from "./websocket/onlineState.js";
@@ -22,7 +22,7 @@ const EMPTY_DOC_STATE = {
22
22
  syncStatus: "unsynced",
23
23
  isLoaded: false
24
24
  };
25
- class PatchesSync extends (_a = ReadonlyStoreClass, _syncDoc_dec = [blockable], __receiveCommittedChanges_dec = [blockable], _a) {
25
+ class PatchesSync extends (_a = ReadonlyStoreClass, _syncDoc_dec = [serialGate], __receiveCommittedChanges_dec = [blockable], _a) {
26
26
  constructor(patches, urlOrConnection, options) {
27
27
  super({
28
28
  online: onlineState.isOnline,
@@ -232,7 +232,7 @@ class PatchesSync extends (_a = ReadonlyStoreClass, _syncDoc_dec = [blockable],
232
232
  }
233
233
  }
234
234
  async syncDoc(docId) {
235
- if (!this.state.connected) return;
235
+ if (!this.state.connected || !this.trackedDocs.has(docId)) return;
236
236
  this._updateDocSyncState(docId, { syncStatus: "syncing" });
237
237
  const doc = this.patches.getOpenDoc(docId);
238
238
  const algorithm = this._getAlgorithm(docId);
@@ -460,11 +460,11 @@ class PatchesSync extends (_a = ReadonlyStoreClass, _syncDoc_dec = [blockable],
460
460
  }
461
461
  }
462
462
  }
463
- async _handleDocChange(docId) {
463
+ _handleDocChange(docId) {
464
464
  if (!this.trackedDocs.has(docId)) return;
465
465
  this._updateDocSyncState(docId, { hasPending: true });
466
466
  if (!this.state.connected) return;
467
- await this.syncDoc(docId);
467
+ this.syncDoc(docId);
468
468
  }
469
469
  /**
470
470
  * Unified handler for remote document deletion (both real-time notifications and offline discovery).
@@ -5,10 +5,10 @@ import { LWWServer } from './LWWServer.js';
5
5
  import { LWWStoreBackend, BranchingStoreBackend } from './types.js';
6
6
  import 'easy-signal';
7
7
  import '../net/websocket/AuthorizationProvider.js';
8
- import '../net/protocol/types.js';
8
+ import '../json-patch/types.js';
9
9
  import '../json-patch/JSONPatch.js';
10
10
  import '@dabble/delta';
11
- import '../json-patch/types.js';
11
+ import '../net/protocol/types.js';
12
12
  import './PatchesServer.js';
13
13
 
14
14
  /**
@@ -4,10 +4,10 @@ import { Change, CommitChangesOptions, DeleteDocOptions, ChangeInput, ChangeMuta
4
4
  import { PatchesServer } from './PatchesServer.js';
5
5
  import { LWWStoreBackend } from './types.js';
6
6
  import '../net/websocket/AuthorizationProvider.js';
7
- import '../net/protocol/types.js';
7
+ import '../json-patch/types.js';
8
8
  import '../json-patch/JSONPatch.js';
9
9
  import '@dabble/delta';
10
- import '../json-patch/types.js';
10
+ import '../net/protocol/types.js';
11
11
 
12
12
  /**
13
13
  * Configuration options for LWWServer.
@@ -1,5 +1,5 @@
1
1
  import { JSONPatchOp } from '../json-patch/types.js';
2
- import { VersionMetadata, Change, ListVersionsOptions, EditableVersionMetadata, ListChangesOptions, DocumentTombstone, Branch } from '../types.js';
2
+ import { DocumentTombstone, VersionMetadata, Change, ListVersionsOptions, EditableVersionMetadata, ListChangesOptions, Branch } from '../types.js';
3
3
  import '../json-patch/JSONPatch.js';
4
4
  import '@dabble/delta';
5
5
 
@@ -18,6 +18,33 @@ declare function blocking<T extends (docId: string, ...args: any[]) => Promise<a
18
18
  * Also, a Typescript decorator for functions whose response should be blocked when needed.
19
19
  */
20
20
  declare function blockableResponse<T extends (docId: string, ...args: any[]) => Promise<any>>(target: T): T;
21
+ /**
22
+ * Wraps a function so that only one invocation per key runs at a time.
23
+ * While in-flight, any additional calls for the same key are collapsed into
24
+ * exactly one queued follow-up. When the in-flight call finishes, the follow-up
25
+ * runs once (picking up all work that accumulated in the window). Further calls
26
+ * while the follow-up is itself in-flight queue another single follow-up, and
27
+ * so on — naturally serialising all work without ever dropping it.
28
+ *
29
+ * Contrast with `singleInvocation(true)`, which collapses concurrent calls but
30
+ * does not schedule a follow-up, so work that arrives mid-flight is lost until
31
+ * the next external trigger.
32
+ *
33
+ * ### Example
34
+ * ```ts
35
+ * const syncDoc = serialGate(async (docId: string) => {
36
+ * const pending = await store.getPending(docId);
37
+ * await server.commit(docId, pending);
38
+ * });
39
+ *
40
+ * // Three rapid calls: only one commitChanges in-flight at a time,
41
+ * // one follow-up picks up everything that arrived during the window.
42
+ * syncDoc('doc1');
43
+ * syncDoc('doc1');
44
+ * syncDoc('doc1');
45
+ * ```
46
+ */
47
+ declare function serialGate<T extends (key: string, ...args: any[]) => Promise<void>>(target: T): T;
21
48
  /**
22
49
  * Wrap a function to only return the result of the first call.
23
50
  *
@@ -30,4 +57,4 @@ declare function blockableResponse<T extends (docId: string, ...args: any[]) =>
30
57
  declare function singleInvocation<T extends (...args: any[]) => Promise<any>>(target: T): T;
31
58
  declare function singleInvocation<T extends (...args: any[]) => Promise<any>>(matchOnFirstArg: boolean): (target: T) => T;
32
59
 
33
- export { blockable, blockableResponse, blocking, releaseConcurrency, singleInvocation };
60
+ export { blockable, blockableResponse, blocking, releaseConcurrency, serialGate, singleInvocation };
@@ -27,6 +27,39 @@ function blockableResponse(target) {
27
27
  return concurrency(args[0]).blockResponse(target.apply(this, args));
28
28
  };
29
29
  }
30
+ function serialGate(target) {
31
+ const instances = /* @__PURE__ */ new WeakMap();
32
+ function getState(thisArg) {
33
+ let state = instances.get(thisArg);
34
+ if (!state) {
35
+ state = { inFlight: /* @__PURE__ */ new Map(), queued: /* @__PURE__ */ new Set() };
36
+ instances.set(thisArg, state);
37
+ }
38
+ return state;
39
+ }
40
+ function run(thisArg, key, args) {
41
+ const { inFlight, queued } = getState(thisArg);
42
+ const promise = target.apply(thisArg, [key, ...args]).finally(() => {
43
+ if (inFlight.get(key) === promise) {
44
+ inFlight.delete(key);
45
+ if (queued.has(key)) {
46
+ queued.delete(key);
47
+ run(thisArg, key, args);
48
+ }
49
+ }
50
+ });
51
+ inFlight.set(key, promise);
52
+ return promise;
53
+ }
54
+ return function(key, ...args) {
55
+ const { inFlight, queued } = getState(this);
56
+ if (inFlight.has(key)) {
57
+ queued.add(key);
58
+ return inFlight.get(key);
59
+ }
60
+ return run(this, key, args);
61
+ };
62
+ }
30
63
  function singleInvocation(matchOnFirstArgOrTarget) {
31
64
  if (typeof matchOnFirstArgOrTarget === "function") {
32
65
  return singleInvocation(false)(matchOnFirstArgOrTarget);
@@ -50,5 +83,6 @@ export {
50
83
  blockableResponse,
51
84
  blocking,
52
85
  releaseConcurrency,
86
+ serialGate,
53
87
  singleInvocation
54
88
  };
@@ -100,13 +100,21 @@ function _usePatchesDocEager(docId, options) {
100
100
  });
101
101
  let unsubscribe = null;
102
102
  if (autoClose) {
103
+ let unmounted = false;
103
104
  manager.openDoc(patches, docId, openDocOpts).then((patchesDoc) => {
105
+ if (unmounted) {
106
+ manager.closeDoc(patches, docId, shouldUntrack);
107
+ return;
108
+ }
104
109
  unsubscribe = setupDoc(patchesDoc);
105
110
  }).catch((err) => {
106
- baseReturn.error.value = err;
107
- baseReturn.loading.value = false;
111
+ if (!unmounted) {
112
+ baseReturn.error.value = err;
113
+ baseReturn.loading.value = false;
114
+ }
108
115
  });
109
116
  onBeforeUnmount(() => {
117
+ unmounted = true;
110
118
  unsubscribe?.();
111
119
  manager.closeDoc(patches, docId, shouldUntrack);
112
120
  });
@@ -219,6 +227,7 @@ function providePatchesDoc(name, docId, options = {}) {
219
227
  });
220
228
  const currentDocId = ref(unref(docId));
221
229
  let unsubscribe = null;
230
+ let providerUnmounted = false;
222
231
  async function initDoc(id) {
223
232
  currentDocId.value = id;
224
233
  unsubscribe?.();
@@ -226,10 +235,16 @@ function providePatchesDoc(name, docId, options = {}) {
226
235
  if (autoClose) {
227
236
  try {
228
237
  const patchesDoc = await manager.openDoc(patches, id, openDocOpts);
238
+ if (providerUnmounted || currentDocId.value !== id) {
239
+ manager.closeDoc(patches, id, shouldUntrack);
240
+ return;
241
+ }
229
242
  unsubscribe = setupDoc(patchesDoc);
230
243
  } catch (err) {
231
- baseReturn.error.value = err;
232
- baseReturn.loading.value = false;
244
+ if (!providerUnmounted && currentDocId.value === id) {
245
+ baseReturn.error.value = err;
246
+ baseReturn.loading.value = false;
247
+ }
233
248
  }
234
249
  } else {
235
250
  try {
@@ -264,6 +279,7 @@ function providePatchesDoc(name, docId, options = {}) {
264
279
  });
265
280
  }
266
281
  onBeforeUnmount(async () => {
282
+ providerUnmounted = true;
267
283
  unsubscribe?.();
268
284
  if (autoClose) {
269
285
  await manager.closeDoc(patches, currentDocId.value, shouldUntrack);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dabble/patches",
3
- "version": "0.8.5",
3
+ "version": "0.8.6",
4
4
  "description": "Immutable JSON Patch implementation based on RFC 6902 supporting operational transformation and last-writer-wins",
5
5
  "author": "Jacob Wright <jacwright@gmail.com>",
6
6
  "bugs": {