@dabble/patches 0.8.18 → 0.8.20

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.
@@ -47,7 +47,16 @@ declare class OTDoc<T extends object = object> extends BaseDoc<T> {
47
47
  getPendingChanges(): Change[];
48
48
  /**
49
49
  * Imports document state from a snapshot (e.g., for recovery when out of sync).
50
- * Resets state and treats all imported changes as pending.
50
+ * Resets committed/pending state from the snapshot but PRESERVES outstanding
51
+ * optimistic ops (re-applied on top of the new state, dropping any that fail).
52
+ *
53
+ * Why preserve optimistic ops: import() can be called by sync recovery /
54
+ * cross-tab snapshot broadcast paths while the user is mid-typing. Wiping
55
+ * `_optimisticOps` would silently regress the input back to the snapshot
56
+ * value, causing visible "text jumps" and lost characters.
57
+ *
58
+ * Stale-snapshot guard: snapshots older than the current `_committedRev`
59
+ * are ignored — we already know more than the caller does.
51
60
  */
52
61
  import(snapshot: PatchesSnapshot<T>): void;
53
62
  /**
@@ -237,4 +246,4 @@ declare abstract class BaseDoc<T extends object = object> extends ReadonlyStoreC
237
246
  abstract import(snapshot: PatchesSnapshot<T>): void;
238
247
  }
239
248
 
240
- export { BaseDoc as B, OTDoc as O, type PatchesDoc as P, type PatchesDocOptions as a };
249
+ export { BaseDoc as B, OTDoc as O, type PatchesDocOptions as P, type PatchesDoc as a };
@@ -1,5 +1,5 @@
1
1
  import { OTStoreBackend } from '../../../server/types.js';
2
- import { EditableVersionMetadata, Change, VersionMetadata } from '../../../types.js';
2
+ import { EditableVersionMetadata, VersionMetadata, Change } from '../../../types.js';
3
3
  import '../../../json-patch/types.js';
4
4
  import '../../../json-patch/JSONPatch.js';
5
5
  import '@dabble/delta';
@@ -1,6 +1,6 @@
1
1
  import 'easy-signal';
2
2
  import '../json-patch/types.js';
3
3
  import '../types.js';
4
- export { B as BaseDoc } from '../BaseDoc-BT18xPxU.js';
4
+ export { B as BaseDoc } from '../BaseDoc-CXHXcW18.js';
5
5
  import '../json-patch/JSONPatch.js';
6
6
  import '@dabble/delta';
@@ -1,6 +1,6 @@
1
1
  import { JSONPatchOp } from '../json-patch/types.js';
2
2
  import { PatchesSnapshot, Change } from '../types.js';
3
- import { P as PatchesDoc } from '../BaseDoc-BT18xPxU.js';
3
+ import { a as PatchesDoc } from '../BaseDoc-CXHXcW18.js';
4
4
  import { PatchesStore, TrackedDoc } from './PatchesStore.js';
5
5
  import '../json-patch/JSONPatch.js';
6
6
  import '@dabble/delta';
@@ -2,7 +2,7 @@ import { JSONPatchOp } from '../json-patch/types.js';
2
2
  import { PatchesSnapshot, Change } from '../types.js';
3
3
  import { ClientAlgorithm } from './ClientAlgorithm.js';
4
4
  import { LWWClientStore } from './LWWClientStore.js';
5
- import { P as PatchesDoc } from '../BaseDoc-BT18xPxU.js';
5
+ import { a as PatchesDoc } from '../BaseDoc-CXHXcW18.js';
6
6
  import { TrackedDoc } from './PatchesStore.js';
7
7
  import '../json-patch/JSONPatch.js';
8
8
  import '@dabble/delta';
@@ -26,7 +26,12 @@ class LWWAlgorithm {
26
26
  const committedRev = doc?.committedRev ?? await this.store.getCommittedRev(docId);
27
27
  const changes = [createChange(committedRev, committedRev + 1, timedOps, metadata)];
28
28
  if (doc) {
29
- doc.applyChanges(changes, true);
29
+ const existingByPath = new Map(existingOps.map((op) => [op.path, op]));
30
+ const retiredInFlightOps = opsToSave.map((op) => existingByPath.get(op.path)).filter((op) => op !== void 0);
31
+ doc.applyChanges(changes, true, {
32
+ inFlightOpsOverride: opsToSave,
33
+ retiredInFlightOps
34
+ });
30
35
  }
31
36
  return changes;
32
37
  }
@@ -1,8 +1,8 @@
1
+ import { JSONPatchOp } from '../json-patch/types.js';
1
2
  import { PatchesSnapshot, Change } from '../types.js';
2
- import { B as BaseDoc } from '../BaseDoc-BT18xPxU.js';
3
+ import { B as BaseDoc } from '../BaseDoc-CXHXcW18.js';
3
4
  import '../json-patch/JSONPatch.js';
4
5
  import '@dabble/delta';
5
- import '../json-patch/types.js';
6
6
  import 'easy-signal';
7
7
 
8
8
  /**
@@ -29,6 +29,22 @@ declare class LWWDoc<T extends object = object> extends BaseDoc<T> {
29
29
  protected _hasPending: boolean;
30
30
  /** Confirmed state (from algorithm pipeline), used to recompute after server changes. */
31
31
  protected _baseState: T;
32
+ /**
33
+ * Content keys of pending local ops that have been applied to `_baseState` via the
34
+ * local-confirmation path (`committedAt === 0`) but not yet echoed back from the server.
35
+ *
36
+ * We key by op CONTENT (JSON.stringify) rather than by Change id because the LWW
37
+ * algorithm coalesces pending ops in its store and reissues a fresh sending Change
38
+ * (with a new id) in `getPendingToSend()`. The server then echoes that re-issued id
39
+ * back, so id-based echo detection would always fail. Op content (path + op + value
40
+ * + ts) survives the reissue intact, since the LWW algorithm preserves the original
41
+ * timestamped ops end-to-end.
42
+ *
43
+ * On a pure echo we skip both `_baseState += ops` (already applied locally) and
44
+ * `_recomputeState()` (would just produce a structurally-identical state with a
45
+ * fresh object identity, causing a spurious store emit mid-typing).
46
+ */
47
+ protected _inFlightOpKeys: Set<string>;
32
48
  /**
33
49
  * Creates an instance of LWWDoc.
34
50
  * @param id The unique identifier for this document.
@@ -41,7 +57,16 @@ declare class LWWDoc<T extends object = object> extends BaseDoc<T> {
41
57
  get hasPending(): boolean;
42
58
  /**
43
59
  * Imports document state from a snapshot (e.g., for recovery when out of sync).
44
- * Resets state completely from the snapshot.
60
+ * Resets `_baseState` from the snapshot but PRESERVES outstanding optimistic ops
61
+ * (re-applied on top of the new state, dropping any that fail).
62
+ *
63
+ * Why preserve optimistic ops: import() can be called by sync recovery /
64
+ * cross-tab snapshot broadcast paths while the user is mid-typing. Wiping
65
+ * `_optimisticOps` would silently regress the input back to the snapshot
66
+ * value, causing visible "text jumps" and lost characters.
67
+ *
68
+ * Stale-snapshot guard: snapshots older than the current `_committedRev`
69
+ * are ignored — we already know more than the caller does.
45
70
  */
46
71
  import(snapshot: PatchesSnapshot<T>): void;
47
72
  /**
@@ -61,8 +86,27 @@ declare class LWWDoc<T extends object = object> extends BaseDoc<T> {
61
86
  * @param changes Array of changes to apply
62
87
  * @param hasPending If provided, overrides the inferred pending state.
63
88
  * Used by LWWAlgorithm which knows the true pending state from the store.
89
+ * @param options Algorithm-only escape hatches for in-flight echo tracking:
90
+ * - `inFlightOpsOverride`: tracks these ops in `_inFlightOpKeys` (and uses
91
+ * them for echo detection) on local-confirm changes, INSTEAD of `c.ops`.
92
+ * Required for combinable ops (`@inc`/`@bit`/`@max`/`@min`) where the
93
+ * local-confirm Change carries the user's raw intent op but the algorithm
94
+ * ships a consolidated op to the server. Without the override, the server
95
+ * echo of the consolidated op wouldn't match any tracked key and we'd
96
+ * emit a spurious recompute.
97
+ * - `retiredInFlightOps`: drops these keys from `_inFlightOpKeys` on the
98
+ * same call. Used by the algorithm to prune prior pending-op keys at
99
+ * paths that are about to be overwritten by `inFlightOpsOverride`,
100
+ * preventing unbounded orphan accumulation during fast typing into the
101
+ * same field.
102
+ *
103
+ * External callers (Worker-Tab sync, tests) should leave `options` undefined
104
+ * and the legacy `c.ops`-based tracker will run.
64
105
  */
65
- applyChanges(changes: Change[], hasPending?: boolean): void;
106
+ applyChanges(changes: Change[], hasPending?: boolean, options?: {
107
+ inFlightOpsOverride?: JSONPatchOp[];
108
+ retiredInFlightOps?: JSONPatchOp[];
109
+ }): void;
66
110
  }
67
111
 
68
112
  export { LWWDoc };
@@ -6,6 +6,22 @@ class LWWDoc extends BaseDoc {
6
6
  _hasPending;
7
7
  /** Confirmed state (from algorithm pipeline), used to recompute after server changes. */
8
8
  _baseState;
9
+ /**
10
+ * Content keys of pending local ops that have been applied to `_baseState` via the
11
+ * local-confirmation path (`committedAt === 0`) but not yet echoed back from the server.
12
+ *
13
+ * We key by op CONTENT (JSON.stringify) rather than by Change id because the LWW
14
+ * algorithm coalesces pending ops in its store and reissues a fresh sending Change
15
+ * (with a new id) in `getPendingToSend()`. The server then echoes that re-issued id
16
+ * back, so id-based echo detection would always fail. Op content (path + op + value
17
+ * + ts) survives the reissue intact, since the LWW algorithm preserves the original
18
+ * timestamped ops end-to-end.
19
+ *
20
+ * On a pure echo we skip both `_baseState += ops` (already applied locally) and
21
+ * `_recomputeState()` (would just produce a structurally-identical state with a
22
+ * fresh object identity, causing a spurious store emit mid-typing).
23
+ */
24
+ _inFlightOpKeys = /* @__PURE__ */ new Set();
9
25
  /**
10
26
  * Creates an instance of LWWDoc.
11
27
  * @param id The unique identifier for this document.
@@ -19,6 +35,7 @@ class LWWDoc extends BaseDoc {
19
35
  if (snapshot?.changes && snapshot.changes.length > 0) {
20
36
  const allOps = snapshot.changes.flatMap((c) => c.ops);
21
37
  this.state = applyPatch(this.state, allOps, { partial: true });
38
+ for (const op of allOps) this._inFlightOpKeys.add(opKey(op));
22
39
  }
23
40
  this._baseState = this.state;
24
41
  this._checkLoaded();
@@ -33,23 +50,43 @@ class LWWDoc extends BaseDoc {
33
50
  }
34
51
  /**
35
52
  * Imports document state from a snapshot (e.g., for recovery when out of sync).
36
- * Resets state completely from the snapshot.
53
+ * Resets `_baseState` from the snapshot but PRESERVES outstanding optimistic ops
54
+ * (re-applied on top of the new state, dropping any that fail).
55
+ *
56
+ * Why preserve optimistic ops: import() can be called by sync recovery /
57
+ * cross-tab snapshot broadcast paths while the user is mid-typing. Wiping
58
+ * `_optimisticOps` would silently regress the input back to the snapshot
59
+ * value, causing visible "text jumps" and lost characters.
60
+ *
61
+ * Stale-snapshot guard: snapshots older than the current `_committedRev`
62
+ * are ignored — we already know more than the caller does.
37
63
  */
38
64
  import(snapshot) {
65
+ if (snapshot.rev < this._committedRev) return;
39
66
  this._committedRev = snapshot.rev;
40
67
  this._hasPending = (snapshot.changes?.length ?? 0) > 0;
41
- this._optimisticOps = [];
42
- let currentState = snapshot.state;
68
+ this._inFlightOpKeys.clear();
69
+ let baseState = snapshot.state;
43
70
  if (snapshot.changes && snapshot.changes.length > 0) {
44
- currentState = applyPatch(
45
- currentState,
46
- snapshot.changes.flatMap((c) => c.ops),
47
- { partial: true }
48
- );
71
+ const allOps = snapshot.changes.flatMap((c) => c.ops);
72
+ baseState = applyPatch(baseState, allOps, { partial: true });
73
+ for (const op of allOps) this._inFlightOpKeys.add(opKey(op));
74
+ }
75
+ this._baseState = baseState;
76
+ let nextState = baseState;
77
+ if (this._optimisticOps.length > 0) {
78
+ const surviving = [];
79
+ for (const ops of this._optimisticOps) {
80
+ try {
81
+ nextState = applyPatch(nextState, ops, { strict: true });
82
+ surviving.push(ops);
83
+ } catch {
84
+ }
85
+ }
86
+ this._optimisticOps = surviving;
49
87
  }
50
- this._baseState = currentState;
51
88
  this._checkLoaded();
52
- this.state = currentState;
89
+ this.state = nextState;
53
90
  }
54
91
  /**
55
92
  * Recomputes state from the base state plus remaining optimistic ops.
@@ -74,8 +111,24 @@ class LWWDoc extends BaseDoc {
74
111
  * @param changes Array of changes to apply
75
112
  * @param hasPending If provided, overrides the inferred pending state.
76
113
  * Used by LWWAlgorithm which knows the true pending state from the store.
114
+ * @param options Algorithm-only escape hatches for in-flight echo tracking:
115
+ * - `inFlightOpsOverride`: tracks these ops in `_inFlightOpKeys` (and uses
116
+ * them for echo detection) on local-confirm changes, INSTEAD of `c.ops`.
117
+ * Required for combinable ops (`@inc`/`@bit`/`@max`/`@min`) where the
118
+ * local-confirm Change carries the user's raw intent op but the algorithm
119
+ * ships a consolidated op to the server. Without the override, the server
120
+ * echo of the consolidated op wouldn't match any tracked key and we'd
121
+ * emit a spurious recompute.
122
+ * - `retiredInFlightOps`: drops these keys from `_inFlightOpKeys` on the
123
+ * same call. Used by the algorithm to prune prior pending-op keys at
124
+ * paths that are about to be overwritten by `inFlightOpsOverride`,
125
+ * preventing unbounded orphan accumulation during fast typing into the
126
+ * same field.
127
+ *
128
+ * External callers (Worker-Tab sync, tests) should leave `options` undefined
129
+ * and the legacy `c.ops`-based tracker will run.
77
130
  */
78
- applyChanges(changes, hasPending) {
131
+ applyChanges(changes, hasPending, options) {
79
132
  if (changes.length === 0) return;
80
133
  let lastCommittedRev = this._committedRev;
81
134
  let hasPendingChanges = false;
@@ -91,7 +144,20 @@ class LWWDoc extends BaseDoc {
91
144
  this._committedRev = lastCommittedRev;
92
145
  this._hasPending = hasPending ?? hasPendingChanges;
93
146
  this._checkLoaded();
147
+ const isPureEcho = hasServerChanges && !hasPendingChanges && changes.every((c) => c.ops.every((op) => this._inFlightOpKeys.has(opKey(op))));
148
+ if (options?.retiredInFlightOps) {
149
+ for (const op of options.retiredInFlightOps) this._inFlightOpKeys.delete(opKey(op));
150
+ }
151
+ for (const c of changes) {
152
+ if (c.committedAt > 0) {
153
+ for (const op of c.ops) this._inFlightOpKeys.delete(opKey(op));
154
+ } else {
155
+ const opsToTrack = options?.inFlightOpsOverride ?? c.ops;
156
+ for (const op of opsToTrack) this._inFlightOpKeys.add(opKey(op));
157
+ }
158
+ }
94
159
  if (hasServerChanges) {
160
+ if (isPureEcho) return;
95
161
  const allOps = changes.flatMap((c) => c.ops);
96
162
  this._baseState = applyPatch(this._baseState, allOps, { partial: true });
97
163
  this._recomputeState();
@@ -106,6 +172,9 @@ class LWWDoc extends BaseDoc {
106
172
  }
107
173
  }
108
174
  }
175
+ function opKey(op) {
176
+ return JSON.stringify(op);
177
+ }
109
178
  export {
110
179
  LWWDoc
111
180
  };
@@ -2,7 +2,7 @@ import { JSONPatchOp } from '../json-patch/types.js';
2
2
  import { PatchesSnapshot, Change } from '../types.js';
3
3
  import { ClientAlgorithm } from './ClientAlgorithm.js';
4
4
  import { OTClientStore } from './OTClientStore.js';
5
- import { a as PatchesDocOptions, P as PatchesDoc } from '../BaseDoc-BT18xPxU.js';
5
+ import { P as PatchesDocOptions, a as PatchesDoc } from '../BaseDoc-CXHXcW18.js';
6
6
  import { TrackedDoc } from './PatchesStore.js';
7
7
  import '../json-patch/JSONPatch.js';
8
8
  import '@dabble/delta';
@@ -1,5 +1,5 @@
1
1
  import '../types.js';
2
- export { O as OTDoc } from '../BaseDoc-BT18xPxU.js';
2
+ export { O as OTDoc } from '../BaseDoc-CXHXcW18.js';
3
3
  import '../json-patch/JSONPatch.js';
4
4
  import '@dabble/delta';
5
5
  import '../json-patch/types.js';
@@ -57,15 +57,36 @@ class OTDoc extends BaseDoc {
57
57
  }
58
58
  /**
59
59
  * Imports document state from a snapshot (e.g., for recovery when out of sync).
60
- * Resets state and treats all imported changes as pending.
60
+ * Resets committed/pending state from the snapshot but PRESERVES outstanding
61
+ * optimistic ops (re-applied on top of the new state, dropping any that fail).
62
+ *
63
+ * Why preserve optimistic ops: import() can be called by sync recovery /
64
+ * cross-tab snapshot broadcast paths while the user is mid-typing. Wiping
65
+ * `_optimisticOps` would silently regress the input back to the snapshot
66
+ * value, causing visible "text jumps" and lost characters.
67
+ *
68
+ * Stale-snapshot guard: snapshots older than the current `_committedRev`
69
+ * are ignored — we already know more than the caller does.
61
70
  */
62
71
  import(snapshot) {
72
+ if (snapshot.rev < this._committedRev) return;
63
73
  this._committedState = snapshot.state;
64
74
  this._committedRev = snapshot.rev;
65
75
  this._pendingChanges = snapshot.changes;
66
- this._optimisticOps = [];
67
76
  this._checkLoaded();
68
- this.state = createStateFromSnapshot(snapshot);
77
+ let newState = createStateFromSnapshot(snapshot);
78
+ if (this._optimisticOps.length > 0) {
79
+ const surviving = [];
80
+ for (const ops of this._optimisticOps) {
81
+ try {
82
+ newState = applyPatch(newState, ops, { strict: true });
83
+ surviving.push(ops);
84
+ } catch {
85
+ }
86
+ }
87
+ this._optimisticOps = surviving;
88
+ }
89
+ this.state = newState;
69
90
  }
70
91
  /**
71
92
  * Recomputes state from committed + pending + remaining optimistic ops.
@@ -99,11 +120,15 @@ class OTDoc extends BaseDoc {
99
120
  if (this._committedRev !== serverChanges[0].rev - 1) {
100
121
  throw new Error("Cannot apply committed changes to a doc that is not at the correct revision");
101
122
  }
123
+ const priorPendingIds = new Set(this._pendingChanges.map((c) => c.id));
124
+ const isPureEcho = serverChanges.length > 0 && serverChanges.every((c) => priorPendingIds.has(c.id));
102
125
  this._committedState = applyChangesToState(this._committedState, serverChanges);
103
126
  this._committedRev = serverChanges[serverChanges.length - 1].rev;
104
127
  this._pendingChanges = rebasedPending;
105
128
  this._checkLoaded();
106
- this._recomputeState();
129
+ if (!isPureEcho) {
130
+ this._recomputeState();
131
+ }
107
132
  } else {
108
133
  this._pendingChanges.push(...changes);
109
134
  this._checkLoaded();
@@ -3,7 +3,7 @@ import { Unsubscriber } from 'easy-signal';
3
3
  import { JSONPatchOp } from '../json-patch/types.js';
4
4
  import { Change } from '../types.js';
5
5
  import { ClientAlgorithm } from './ClientAlgorithm.js';
6
- import { a as PatchesDocOptions, P as PatchesDoc } from '../BaseDoc-BT18xPxU.js';
6
+ import { P as PatchesDocOptions, a as PatchesDoc } from '../BaseDoc-CXHXcW18.js';
7
7
  import { AlgorithmName } from './PatchesStore.js';
8
8
  import '../json-patch/JSONPatch.js';
9
9
  import '@dabble/delta';
@@ -8,7 +8,7 @@ import '../json-patch/JSONPatch.js';
8
8
  import '@dabble/delta';
9
9
  import '../json-patch/types.js';
10
10
  import './ClientAlgorithm.js';
11
- import '../BaseDoc-BT18xPxU.js';
11
+ import '../BaseDoc-CXHXcW18.js';
12
12
 
13
13
  interface PatchesBranchClientOptions {
14
14
  /** Algorithm to use for the branch document (defaults to the Patches instance default). */
@@ -1,6 +1,6 @@
1
1
  import 'easy-signal';
2
2
  import '../json-patch/types.js';
3
3
  import '../types.js';
4
- export { O as OTDoc, P as PatchesDoc, a as PatchesDocOptions } from '../BaseDoc-BT18xPxU.js';
4
+ export { O as OTDoc, a as PatchesDoc, P as PatchesDocOptions } from '../BaseDoc-CXHXcW18.js';
5
5
  import '../json-patch/JSONPatch.js';
6
6
  import '@dabble/delta';
@@ -1,6 +1,6 @@
1
1
  import { AlgorithmName } from './PatchesStore.js';
2
2
  import { Patches } from './Patches.js';
3
- import { a as PatchesDocOptions } from '../BaseDoc-BT18xPxU.js';
3
+ import { P as PatchesDocOptions } from '../BaseDoc-CXHXcW18.js';
4
4
  import '../types.js';
5
5
  import '../json-patch/JSONPatch.js';
6
6
  import '@dabble/delta';
@@ -1,4 +1,4 @@
1
- export { B as BaseDoc, O as OTDoc, P as PatchesDoc, a as PatchesDocOptions } from '../BaseDoc-BT18xPxU.js';
1
+ export { B as BaseDoc, O as OTDoc, a as PatchesDoc, P as PatchesDocOptions } from '../BaseDoc-CXHXcW18.js';
2
2
  export { IndexedDBFactoryOptions, MultiAlgorithmFactoryOptions, MultiAlgorithmIndexedDBFactoryOptions, PatchesFactoryOptions, createLWWIndexedDBPatches, createLWWPatches, createMultiAlgorithmExternalDBPatches, createMultiAlgorithmIndexedDBPatches, createMultiAlgorithmPatches, createOTIndexedDBPatches, createOTPatches, upgradePatchesDB } from './factories.js';
3
3
  export { IDBStoreWrapper, IDBTransactionWrapper, IndexedDBStore } from './IndexedDBStore.js';
4
4
  export { OTIndexedDBStore } from './OTIndexedDBStore.js';
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { Delta } from '@dabble/delta';
2
- export { B as BaseDoc, O as OTDoc, P as PatchesDoc, a as PatchesDocOptions } from './BaseDoc-BT18xPxU.js';
2
+ export { B as BaseDoc, O as OTDoc, a as PatchesDoc, P as PatchesDocOptions } from './BaseDoc-CXHXcW18.js';
3
3
  export { IndexedDBFactoryOptions, MultiAlgorithmFactoryOptions, MultiAlgorithmIndexedDBFactoryOptions, PatchesFactoryOptions, createLWWIndexedDBPatches, createLWWPatches, createMultiAlgorithmExternalDBPatches, createMultiAlgorithmIndexedDBPatches, createMultiAlgorithmPatches, createOTIndexedDBPatches, createOTPatches, upgradePatchesDB } from './client/factories.js';
4
4
  export { IDBStoreWrapper, IDBTransactionWrapper, IndexedDBStore } from './client/IndexedDBStore.js';
5
5
  export { OTIndexedDBStore } from './client/OTIndexedDBStore.js';
@@ -3,6 +3,7 @@ import { getTypes } from "./ops/index.js";
3
3
  import { runWithObject } from "./state.js";
4
4
  import { exit } from "./utils/exit.js";
5
5
  import { getType } from "./utils/getType.js";
6
+ import { isSoftOp, pathExistsInState } from "./utils/softWrites.js";
6
7
  function applyPatch(object, patches, opts = {}, custom) {
7
8
  if (patches.length === 0) {
8
9
  return object;
@@ -14,6 +15,9 @@ function applyPatch(object, patches, opts = {}, custom) {
14
15
  return runWithObject(object, types, patches.length > 1, (state) => {
15
16
  for (let i = 0, imax = patches.length; i < imax; i++) {
16
17
  const patch = patches[i];
18
+ if (isSoftOp(patch) && pathExistsInState(state.root[""], patch.path)) {
19
+ continue;
20
+ }
17
21
  const handler = getType(state, patch)?.apply;
18
22
  const error = handler ? handler(state, "" + patch.path, patch.from || patch.value) : `[op:${patch.op}] unknown`;
19
23
  if (error) {
@@ -8,7 +8,7 @@ export { isAdd, mapAndFilterOps, transformRemove, updateRemovedOps } from './ops
8
8
  export { getArrayIndex, getArrayPrefixAndIndex, getIndexAndEnd, getPrefix, getPrefixAndProp, getProp, getPropAfter, isArrayPath } from './paths.js';
9
9
  export { EMPTY, EMPTY_ARRAY, getValue, pluck, pluckWithShallowCopy } from './pluck.js';
10
10
  export { shallowCopy } from './shallowCopy.js';
11
- export { filterSoftWritesAgainstState, isEmptyContainer, updateSoftWrites } from './softWrites.js';
11
+ export { filterSoftWritesAgainstState, isEmptyContainer, isSoftOp, pathExistsInState, updateSoftWrites } from './softWrites.js';
12
12
  export { toArrayIndex } from './toArrayIndex.js';
13
13
  export { toKeys } from './toKeys.js';
14
14
  export { updateArrayIndexes } from './updateArrayIndexes.js';
@@ -7,10 +7,17 @@ declare function isEmptyContainer(value: any): boolean;
7
7
  * for any value that already exists.
8
8
  */
9
9
  declare function updateSoftWrites(overPath: string, ops: JSONPatchOp[], originalValue: any): JSONPatchOp[];
10
+ /**
11
+ * Returns true when an op carries soft semantics — either an explicit
12
+ * `soft: true` flag, or an empty-container `add` (treated as initialization
13
+ * by convention). Mirrors the check used by the LWW consolidation algorithm.
14
+ */
15
+ declare function isSoftOp(op: JSONPatchOp): boolean;
10
16
  /**
11
17
  * Filters out soft writes that would overwrite existing data in state.
12
18
  * Used when baseRev: 0 is jumped forward, bypassing normal transformation.
13
19
  */
14
20
  declare function filterSoftWritesAgainstState(ops: JSONPatchOp[], state: any): JSONPatchOp[];
21
+ declare function pathExistsInState(state: any, path: string): boolean;
15
22
 
16
- export { filterSoftWritesAgainstState, isEmptyContainer, updateSoftWrites };
23
+ export { filterSoftWritesAgainstState, isEmptyContainer, isSoftOp, pathExistsInState, updateSoftWrites };
@@ -16,10 +16,12 @@ function updateSoftWrites(overPath, ops, originalValue) {
16
16
  return op;
17
17
  });
18
18
  }
19
+ function isSoftOp(op) {
20
+ return op.soft === true || op.op === "add" && isEmptyContainer(op.value);
21
+ }
19
22
  function filterSoftWritesAgainstState(ops, state) {
20
23
  return ops.filter((op) => {
21
- const isSoft = op.soft || op.op === "add" && isEmptyContainer(op.value);
22
- if (!isSoft) return true;
24
+ if (!isSoftOp(op)) return true;
23
25
  return !pathExistsInState(state, op.path);
24
26
  });
25
27
  }
@@ -37,5 +39,7 @@ function pathExistsInState(state, path) {
37
39
  export {
38
40
  filterSoftWritesAgainstState,
39
41
  isEmptyContainer,
42
+ isSoftOp,
43
+ pathExistsInState,
40
44
  updateSoftWrites
41
45
  };
@@ -13,7 +13,7 @@ import { WebSocketOptions } from './websocket/WebSocketTransport.js';
13
13
  import '../json-patch/JSONPatch.js';
14
14
  import '@dabble/delta';
15
15
  import '../json-patch/types.js';
16
- import '../BaseDoc-BT18xPxU.js';
16
+ import '../BaseDoc-CXHXcW18.js';
17
17
  import '../utils/deferred.js';
18
18
 
19
19
  interface PatchesSyncState {
@@ -22,7 +22,7 @@ import '../algorithms/ot/shared/changeBatching.js';
22
22
  import '../client/BranchClientStore.js';
23
23
  import '../client/ClientAlgorithm.js';
24
24
  import '../json-patch/types.js';
25
- import '../BaseDoc-BT18xPxU.js';
25
+ import '../BaseDoc-CXHXcW18.js';
26
26
  import '../client/PatchesStore.js';
27
27
  import '../client/Patches.js';
28
28
  import '../server/types.js';
@@ -1,5 +1,5 @@
1
1
  import { ApiDefinition } from '../net/protocol/JSONRPCServer.js';
2
- import { EditableBranchMetadata, Branch, CreateBranchMetadata } from '../types.js';
2
+ import { EditableBranchMetadata, CreateBranchMetadata, Branch } from '../types.js';
3
3
  import 'easy-signal';
4
4
  import '../net/websocket/AuthorizationProvider.js';
5
5
  import './types.js';
@@ -1,5 +1,5 @@
1
1
  import { Patches, OpenDocOptions } from '../client/Patches.js';
2
- import { P as PatchesDoc } from '../BaseDoc-BT18xPxU.js';
2
+ import { a as PatchesDoc } from '../BaseDoc-CXHXcW18.js';
3
3
  import 'easy-signal';
4
4
  import '../json-patch/types.js';
5
5
  import '../types.js';
@@ -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-BT18xPxU.js';
10
+ import '../BaseDoc-CXHXcW18.js';
11
11
  import '../client/PatchesStore.js';
12
12
  import '../algorithms/ot/shared/changeBatching.js';
13
13
  import '../client/BranchClientStore.js';
@@ -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-BT18xPxU.js';
9
+ import '../BaseDoc-CXHXcW18.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-BT18xPxU.js';
14
+ import '../BaseDoc-CXHXcW18.js';
15
15
  import '../client/PatchesStore.js';
16
16
  import '../net/PatchesSync.js';
17
17
  import '../algorithms/ot/shared/changeBatching.js';
@@ -1,6 +1,6 @@
1
1
  import { Accessor } from 'solid-js';
2
2
  import { OpenDocOptions } from '../client/Patches.js';
3
- import { P as PatchesDoc } from '../BaseDoc-BT18xPxU.js';
3
+ import { a as PatchesDoc } from '../BaseDoc-CXHXcW18.js';
4
4
  import { ChangeMutator } from '../types.js';
5
5
  import 'easy-signal';
6
6
  import '../json-patch/types.js';
@@ -1,6 +1,6 @@
1
- import { ShallowRef, Ref, MaybeRef, MaybeRefOrGetter } from 'vue';
1
+ import { ShallowRef, Ref, MaybeRefOrGetter, MaybeRef } from 'vue';
2
2
  import { OpenDocOptions } from '../client/Patches.js';
3
- import { P as PatchesDoc } from '../BaseDoc-BT18xPxU.js';
3
+ import { a as PatchesDoc } from '../BaseDoc-CXHXcW18.js';
4
4
  import { ChangeMutator } from '../types.js';
5
5
  import 'easy-signal';
6
6
  import '../json-patch/types.js';
@@ -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-BT18xPxU.js';
9
+ import '../BaseDoc-CXHXcW18.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-BT18xPxU.js';
14
+ import '../BaseDoc-CXHXcW18.js';
15
15
  import '../client/PatchesStore.js';
16
16
  import '../net/PatchesSync.js';
17
17
  import '../algorithms/ot/shared/changeBatching.js';
@@ -1,4 +1,4 @@
1
- import { ShallowRef, Ref } from 'vue';
1
+ import { Ref, ShallowRef } from 'vue';
2
2
  import { OpenDocOptions } from '../client/Patches.js';
3
3
  import 'easy-signal';
4
4
  import '../json-patch/types.js';
@@ -6,7 +6,7 @@ import '../types.js';
6
6
  import '../json-patch/JSONPatch.js';
7
7
  import '@dabble/delta';
8
8
  import '../client/ClientAlgorithm.js';
9
- import '../BaseDoc-BT18xPxU.js';
9
+ import '../BaseDoc-CXHXcW18.js';
10
10
  import '../client/PatchesStore.js';
11
11
 
12
12
  /**
@@ -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-BT18xPxU.js';
10
+ import '../BaseDoc-CXHXcW18.js';
11
11
  import '../client/PatchesStore.js';
12
12
  import '../algorithms/ot/shared/changeBatching.js';
13
13
  import '../client/BranchClientStore.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dabble/patches",
3
- "version": "0.8.18",
3
+ "version": "0.8.20",
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": {