@dabble/patches 0.7.18 → 0.7.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.
- package/dist/{BaseDoc-BRIP2YZp.d.ts → BaseDoc-CD5wZQMm.d.ts} +63 -23
- package/dist/algorithms/ot/shared/rebaseChanges.js +2 -1
- package/dist/client/BaseDoc.d.ts +1 -1
- package/dist/client/BaseDoc.js +29 -2
- package/dist/client/ClientAlgorithm.d.ts +1 -1
- package/dist/client/LWWAlgorithm.d.ts +1 -1
- package/dist/client/LWWDoc.d.ts +20 -10
- package/dist/client/LWWDoc.js +43 -11
- package/dist/client/LWWInMemoryStore.js +0 -3
- package/dist/client/LWWIndexedDBStore.js +0 -4
- package/dist/client/OTAlgorithm.d.ts +1 -1
- package/dist/client/OTDoc.d.ts +1 -1
- package/dist/client/OTDoc.js +39 -8
- package/dist/client/Patches.d.ts +6 -2
- package/dist/client/Patches.js +18 -2
- 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 +1 -1
- package/dist/net/PatchesSync.d.ts +1 -1
- package/dist/net/index.d.ts +1 -1
- package/dist/shared/doc-manager.d.ts +1 -1
- package/dist/solid/context.d.ts +1 -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/vue/composables.d.ts +2 -2
- package/dist/vue/composables.js +1 -1
- 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 +1 -1
- package/package.json +1 -1
|
@@ -8,14 +8,16 @@ import { Change, PatchesSnapshot, DocSyncStatus, ChangeMutator } from './types.j
|
|
|
8
8
|
* Uses a snapshot-based approach with revision tracking and rebasing
|
|
9
9
|
* for handling concurrent edits.
|
|
10
10
|
*
|
|
11
|
-
* The `change()` method (inherited from BaseDoc)
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* The `change()` method (inherited from BaseDoc) applies ops optimistically
|
|
12
|
+
* to `state` and emits them via `onChange`. The OTAlgorithm packages ops into
|
|
13
|
+
* Changes, persists them, and calls `applyChanges()` to confirm the optimistic
|
|
14
|
+
* update (shifting from the FIFO queue and skipping the state setter).
|
|
14
15
|
*
|
|
15
16
|
* ## State Model
|
|
16
17
|
* - `_committedState`: Base state from server (at `_committedRev`)
|
|
17
18
|
* - `_pendingChanges`: Local changes not yet committed by server
|
|
18
|
-
* - `
|
|
19
|
+
* - `_optimisticOps` (from BaseDoc): Ops applied by change() but not yet confirmed
|
|
20
|
+
* - `state`: Live state = committedState + pendingChanges + optimistic ops applied
|
|
19
21
|
*
|
|
20
22
|
* ## Wire Efficiency
|
|
21
23
|
* For Worker-Tab communication, only changes are sent over the wire (not full state).
|
|
@@ -49,12 +51,17 @@ declare class OTDoc<T extends object = object> extends BaseDoc<T> {
|
|
|
49
51
|
*/
|
|
50
52
|
import(snapshot: PatchesSnapshot<T>): void;
|
|
51
53
|
/**
|
|
52
|
-
*
|
|
53
|
-
|
|
54
|
+
* Recomputes state from committed + pending + remaining optimistic ops.
|
|
55
|
+
*/
|
|
56
|
+
protected _recomputeState(): void;
|
|
57
|
+
/**
|
|
58
|
+
* Confirms changes from the algorithm pipeline.
|
|
54
59
|
*
|
|
55
|
-
*
|
|
56
|
-
* - `committedAt > 0`: Server-committed change (
|
|
57
|
-
*
|
|
60
|
+
* Distinguishes between committed and pending changes using `committedAt`:
|
|
61
|
+
* - `committedAt > 0`: Server-committed change (updates committed state, recomputes
|
|
62
|
+
* with remaining optimistic ops preserved)
|
|
63
|
+
* - `committedAt === 0`: Local change confirmation (shifts from optimistic queue,
|
|
64
|
+
* skips state update since change() already applied the ops)
|
|
58
65
|
*
|
|
59
66
|
* For server changes, all committed changes come first, followed by rebased pending.
|
|
60
67
|
*
|
|
@@ -119,8 +126,8 @@ interface PatchesDoc<T extends object = object> extends ReadonlyStore<T> {
|
|
|
119
126
|
*/
|
|
120
127
|
readonly onChange: Signal<(ops: JSONPatchOp[]) => void>;
|
|
121
128
|
/**
|
|
122
|
-
* Captures an update to the document,
|
|
123
|
-
*
|
|
129
|
+
* Captures an update to the document, applies it optimistically to state,
|
|
130
|
+
* and emits the ops via onChange for async persistence.
|
|
124
131
|
* @param mutator Function that uses JSONPatch methods with type-safe paths.
|
|
125
132
|
*/
|
|
126
133
|
change(mutator: ChangeMutator<T>): void;
|
|
@@ -130,15 +137,29 @@ interface PatchesDoc<T extends object = object> extends ReadonlyStore<T> {
|
|
|
130
137
|
* Abstract base class for document implementations.
|
|
131
138
|
* Contains shared state and methods used by both OTDoc and LWWDoc.
|
|
132
139
|
*
|
|
133
|
-
* The `change()` method captures ops
|
|
134
|
-
*
|
|
135
|
-
* the
|
|
140
|
+
* The `change()` method captures ops, applies them optimistically to `state`,
|
|
141
|
+
* and emits them via `onChange`. The algorithm handles packaging ops into Changes,
|
|
142
|
+
* persisting them, and confirming the optimistic update via `applyChanges()`.
|
|
143
|
+
*
|
|
144
|
+
* The mapping is strictly 1:1: one `change()` call produces one `applyChanges()`
|
|
145
|
+
* confirmation. A FIFO queue tracks outstanding optimistic ops so that
|
|
146
|
+
* `_recomputeState()` can reconstruct the full state (committed + pending +
|
|
147
|
+
* optimistic) when server changes arrive or during error recovery.
|
|
136
148
|
*
|
|
137
|
-
* Internal methods (updateSyncStatus, applyChanges, import)
|
|
138
|
-
* on the PatchesDoc interface, as they're only used
|
|
149
|
+
* Internal methods (updateSyncStatus, applyChanges, import, rollbackOptimistic)
|
|
150
|
+
* are on this class but not on the PatchesDoc interface, as they're only used
|
|
151
|
+
* by Algorithm and PatchesSync.
|
|
139
152
|
*/
|
|
140
153
|
declare abstract class BaseDoc<T extends object = object> extends ReadonlyStoreClass<T> implements PatchesDoc<T> {
|
|
141
154
|
protected _id: string;
|
|
155
|
+
/**
|
|
156
|
+
* FIFO queue of ops applied optimistically by change() but not yet confirmed
|
|
157
|
+
* by applyChanges(). The 1:1 mapping between change() and applyChanges()
|
|
158
|
+
* means confirmation simply shifts from the front. The ops are stored (not
|
|
159
|
+
* just counted) so _recomputeState() can reconstruct the full state when
|
|
160
|
+
* server changes arrive during the optimistic window.
|
|
161
|
+
*/
|
|
162
|
+
protected _optimisticOps: JSONPatchOp[][];
|
|
142
163
|
/** Current sync status of this document. */
|
|
143
164
|
readonly syncStatus: Store<DocSyncStatus>;
|
|
144
165
|
/** Whether the document has completed its initial load. Sticky: once true, never reverts to false. */
|
|
@@ -164,11 +185,30 @@ declare abstract class BaseDoc<T extends object = object> extends ReadonlyStoreC
|
|
|
164
185
|
/** Are there local changes that haven't been committed yet? */
|
|
165
186
|
abstract get hasPending(): boolean;
|
|
166
187
|
/**
|
|
167
|
-
* Captures an update to the document,
|
|
168
|
-
*
|
|
188
|
+
* Captures an update to the document, applies it optimistically to state,
|
|
189
|
+
* and emits the ops via onChange for async persistence.
|
|
190
|
+
*
|
|
191
|
+
* State is updated synchronously (easy-signal's store.set() drains
|
|
192
|
+
* subscribers synchronously) so the UI sees changes immediately.
|
|
193
|
+
* The algorithm later confirms via applyChanges(), which shifts from
|
|
194
|
+
* the FIFO queue and skips the state update (avoiding double notifications).
|
|
195
|
+
*
|
|
169
196
|
* @param mutator Function that uses JSONPatch methods with type-safe paths.
|
|
170
197
|
*/
|
|
171
198
|
change(mutator: ChangeMutator<T>): void;
|
|
199
|
+
/**
|
|
200
|
+
* Rolls back all outstanding optimistic applies and recomputes state from
|
|
201
|
+
* confirmed state only. Called by Patches._handleDocChange when the
|
|
202
|
+
* algorithm rejects ops. Any remaining in-flight changes will apply
|
|
203
|
+
* normally via the fallback path in applyChanges().
|
|
204
|
+
*/
|
|
205
|
+
rollbackOptimistic(): void;
|
|
206
|
+
/**
|
|
207
|
+
* Recomputes state from confirmed state (committed + pending) plus any
|
|
208
|
+
* remaining optimistic ops. Subclass-specific because each algorithm
|
|
209
|
+
* tracks committed/pending state differently.
|
|
210
|
+
*/
|
|
211
|
+
protected abstract _recomputeState(): void;
|
|
172
212
|
/**
|
|
173
213
|
* Updates the sync status of the document.
|
|
174
214
|
* Called by PatchesSync - not part of the app-facing PatchesDoc interface.
|
|
@@ -179,11 +219,11 @@ declare abstract class BaseDoc<T extends object = object> extends ReadonlyStoreC
|
|
|
179
219
|
/** Latches _isLoaded to true when the doc has data or sync has resolved. */
|
|
180
220
|
protected _checkLoaded(): void;
|
|
181
221
|
/**
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
* For
|
|
186
|
-
*
|
|
222
|
+
* Confirms changes from the algorithm pipeline.
|
|
223
|
+
* For local changes (committedAt === 0): shifts from the optimistic queue
|
|
224
|
+
* and skips the state update since change() already applied them.
|
|
225
|
+
* For server changes (committedAt > 0): updates committed state and
|
|
226
|
+
* recomputes with any remaining optimistic ops.
|
|
187
227
|
*
|
|
188
228
|
* @param changes Array of changes to apply.
|
|
189
229
|
*/
|
|
@@ -20,12 +20,13 @@ function rebaseChanges(serverChanges, localChanges) {
|
|
|
20
20
|
);
|
|
21
21
|
const baseRev = lastChange.rev;
|
|
22
22
|
let rev = lastChange.rev;
|
|
23
|
-
|
|
23
|
+
const result = filteredLocalChanges.map((change) => {
|
|
24
24
|
rev++;
|
|
25
25
|
const ops = transformPatch.transform(change.ops).ops;
|
|
26
26
|
if (!ops.length) return null;
|
|
27
27
|
return { ...change, baseRev, rev, ops };
|
|
28
28
|
}).filter(Boolean);
|
|
29
|
+
return result;
|
|
29
30
|
}
|
|
30
31
|
export {
|
|
31
32
|
rebaseChanges
|
package/dist/client/BaseDoc.d.ts
CHANGED
package/dist/client/BaseDoc.js
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import "../chunk-IZ2YBCUP.js";
|
|
2
2
|
import { ReadonlyStoreClass, signal, store } from "easy-signal";
|
|
3
|
+
import { applyPatch } from "../json-patch/applyPatch.js";
|
|
3
4
|
import { createJSONPatch } from "../json-patch/createJSONPatch.js";
|
|
4
5
|
import { isDocLoaded } from "../shared/utils.js";
|
|
5
6
|
class BaseDoc extends ReadonlyStoreClass {
|
|
6
7
|
_id;
|
|
8
|
+
/**
|
|
9
|
+
* FIFO queue of ops applied optimistically by change() but not yet confirmed
|
|
10
|
+
* by applyChanges(). The 1:1 mapping between change() and applyChanges()
|
|
11
|
+
* means confirmation simply shifts from the front. The ops are stored (not
|
|
12
|
+
* just counted) so _recomputeState() can reconstruct the full state when
|
|
13
|
+
* server changes arrive during the optimistic window.
|
|
14
|
+
*/
|
|
15
|
+
_optimisticOps = [];
|
|
7
16
|
/** Current sync status of this document. */
|
|
8
17
|
syncStatus = store("unsynced");
|
|
9
18
|
/** Whether the document has completed its initial load. Sticky: once true, never reverts to false. */
|
|
@@ -30,8 +39,14 @@ class BaseDoc extends ReadonlyStoreClass {
|
|
|
30
39
|
return this._id;
|
|
31
40
|
}
|
|
32
41
|
/**
|
|
33
|
-
* Captures an update to the document,
|
|
34
|
-
*
|
|
42
|
+
* Captures an update to the document, applies it optimistically to state,
|
|
43
|
+
* and emits the ops via onChange for async persistence.
|
|
44
|
+
*
|
|
45
|
+
* State is updated synchronously (easy-signal's store.set() drains
|
|
46
|
+
* subscribers synchronously) so the UI sees changes immediately.
|
|
47
|
+
* The algorithm later confirms via applyChanges(), which shifts from
|
|
48
|
+
* the FIFO queue and skips the state update (avoiding double notifications).
|
|
49
|
+
*
|
|
35
50
|
* @param mutator Function that uses JSONPatch methods with type-safe paths.
|
|
36
51
|
*/
|
|
37
52
|
change(mutator) {
|
|
@@ -39,8 +54,20 @@ class BaseDoc extends ReadonlyStoreClass {
|
|
|
39
54
|
if (patch.ops.length === 0) {
|
|
40
55
|
return;
|
|
41
56
|
}
|
|
57
|
+
this.state = applyPatch(this.state, patch.ops, { strict: true });
|
|
58
|
+
this._optimisticOps.push(patch.ops);
|
|
42
59
|
this.onChange.emit(patch.ops);
|
|
43
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Rolls back all outstanding optimistic applies and recomputes state from
|
|
63
|
+
* confirmed state only. Called by Patches._handleDocChange when the
|
|
64
|
+
* algorithm rejects ops. Any remaining in-flight changes will apply
|
|
65
|
+
* normally via the fallback path in applyChanges().
|
|
66
|
+
*/
|
|
67
|
+
rollbackOptimistic() {
|
|
68
|
+
this._optimisticOps = [];
|
|
69
|
+
this._recomputeState();
|
|
70
|
+
}
|
|
44
71
|
// --- Internal methods (not on PatchesDoc interface) ---
|
|
45
72
|
/**
|
|
46
73
|
* Updates the sync status of the document.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { JSONPatchOp } from '../json-patch/types.js';
|
|
2
2
|
import { PatchesSnapshot, Change } from '../types.js';
|
|
3
|
-
import { a as PatchesDoc } from '../BaseDoc-
|
|
3
|
+
import { a as PatchesDoc } from '../BaseDoc-CD5wZQMm.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 { a as PatchesDoc } from '../BaseDoc-
|
|
5
|
+
import { a as PatchesDoc } from '../BaseDoc-CD5wZQMm.js';
|
|
6
6
|
import { TrackedDoc } from './PatchesStore.js';
|
|
7
7
|
import '../json-patch/JSONPatch.js';
|
|
8
8
|
import '@dabble/delta';
|
package/dist/client/LWWDoc.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { PatchesSnapshot, Change } from '../types.js';
|
|
2
|
-
import { B as BaseDoc } from '../BaseDoc-
|
|
2
|
+
import { B as BaseDoc } from '../BaseDoc-CD5wZQMm.js';
|
|
3
3
|
import '../json-patch/JSONPatch.js';
|
|
4
4
|
import '@dabble/delta';
|
|
5
5
|
import '../json-patch/types.js';
|
|
@@ -8,11 +8,14 @@ import 'easy-signal';
|
|
|
8
8
|
/**
|
|
9
9
|
* LWW (Last-Write-Wins) document implementation.
|
|
10
10
|
*
|
|
11
|
-
* The `change()` method (inherited from BaseDoc)
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
11
|
+
* The `change()` method (inherited from BaseDoc) applies ops optimistically
|
|
12
|
+
* to `state` and emits them via `onChange`. The LWWAlgorithm packages ops
|
|
13
|
+
* with timestamps, persists them, and calls `applyChanges()` to confirm
|
|
14
|
+
* the optimistic update (shifting from the FIFO queue and skipping the state setter).
|
|
15
|
+
*
|
|
16
|
+
* Note: LWWAlgorithm adds `ts` (timestamp) metadata to ops before persisting.
|
|
17
|
+
* `applyPatch` ignores unknown op properties, so optimistic apply (raw ops)
|
|
18
|
+
* and confirmed apply (timestamped ops) produce identical state.
|
|
16
19
|
*
|
|
17
20
|
* Unlike OTDoc, LWWDoc doesn't need to track committed vs pending state
|
|
18
21
|
* separately - the algorithm handles all conflict resolution by timestamp.
|
|
@@ -24,6 +27,8 @@ import 'easy-signal';
|
|
|
24
27
|
declare class LWWDoc<T extends object = object> extends BaseDoc<T> {
|
|
25
28
|
protected _committedRev: number;
|
|
26
29
|
protected _hasPending: boolean;
|
|
30
|
+
/** Confirmed state (from algorithm pipeline), used to recompute after server changes. */
|
|
31
|
+
protected _baseState: T;
|
|
27
32
|
/**
|
|
28
33
|
* Creates an instance of LWWDoc.
|
|
29
34
|
* @param id The unique identifier for this document.
|
|
@@ -40,13 +45,18 @@ declare class LWWDoc<T extends object = object> extends BaseDoc<T> {
|
|
|
40
45
|
*/
|
|
41
46
|
import(snapshot: PatchesSnapshot<T>): void;
|
|
42
47
|
/**
|
|
43
|
-
*
|
|
44
|
-
|
|
48
|
+
* Recomputes state from the base state plus remaining optimistic ops.
|
|
49
|
+
*/
|
|
50
|
+
protected _recomputeState(): void;
|
|
51
|
+
/**
|
|
52
|
+
* Confirms changes from the algorithm pipeline.
|
|
45
53
|
*
|
|
46
54
|
* For LWW, all ops are applied in order. The method distinguishes between
|
|
47
55
|
* committed and pending changes using `committedAt`:
|
|
48
|
-
* - `committedAt > 0`: Server-committed change (updates committedRev
|
|
49
|
-
*
|
|
56
|
+
* - `committedAt > 0`: Server-committed change (updates committedRev, recomputes
|
|
57
|
+
* state with remaining optimistic ops preserved)
|
|
58
|
+
* - `committedAt === 0`: Pending local change (shifts from optimistic queue,
|
|
59
|
+
* skips state update since change() already applied the ops)
|
|
50
60
|
*
|
|
51
61
|
* @param changes Array of changes to apply
|
|
52
62
|
* @param hasPending If provided, overrides the inferred pending state.
|
package/dist/client/LWWDoc.js
CHANGED
|
@@ -4,6 +4,8 @@ import { BaseDoc } from "./BaseDoc.js";
|
|
|
4
4
|
class LWWDoc extends BaseDoc {
|
|
5
5
|
_committedRev;
|
|
6
6
|
_hasPending;
|
|
7
|
+
/** Confirmed state (from algorithm pipeline), used to recompute after server changes. */
|
|
8
|
+
_baseState;
|
|
7
9
|
/**
|
|
8
10
|
* Creates an instance of LWWDoc.
|
|
9
11
|
* @param id The unique identifier for this document.
|
|
@@ -23,6 +25,7 @@ class LWWDoc extends BaseDoc {
|
|
|
23
25
|
}
|
|
24
26
|
this.state = currentState;
|
|
25
27
|
}
|
|
28
|
+
this._baseState = this.state;
|
|
26
29
|
this._checkLoaded();
|
|
27
30
|
}
|
|
28
31
|
/** Last committed revision number from the server. */
|
|
@@ -40,6 +43,7 @@ class LWWDoc extends BaseDoc {
|
|
|
40
43
|
import(snapshot) {
|
|
41
44
|
this._committedRev = snapshot.rev;
|
|
42
45
|
this._hasPending = (snapshot.changes?.length ?? 0) > 0;
|
|
46
|
+
this._optimisticOps = [];
|
|
43
47
|
let currentState = snapshot.state;
|
|
44
48
|
if (snapshot.changes && snapshot.changes.length > 0) {
|
|
45
49
|
for (const change of snapshot.changes) {
|
|
@@ -48,17 +52,29 @@ class LWWDoc extends BaseDoc {
|
|
|
48
52
|
}
|
|
49
53
|
}
|
|
50
54
|
}
|
|
55
|
+
this._baseState = currentState;
|
|
51
56
|
this._checkLoaded();
|
|
52
57
|
this.state = currentState;
|
|
53
58
|
}
|
|
54
59
|
/**
|
|
55
|
-
*
|
|
56
|
-
|
|
60
|
+
* Recomputes state from the base state plus remaining optimistic ops.
|
|
61
|
+
*/
|
|
62
|
+
_recomputeState() {
|
|
63
|
+
let newState = this._baseState;
|
|
64
|
+
for (const ops of this._optimisticOps) {
|
|
65
|
+
newState = applyPatch(newState, ops, { strict: true });
|
|
66
|
+
}
|
|
67
|
+
this.state = newState;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Confirms changes from the algorithm pipeline.
|
|
57
71
|
*
|
|
58
72
|
* For LWW, all ops are applied in order. The method distinguishes between
|
|
59
73
|
* committed and pending changes using `committedAt`:
|
|
60
|
-
* - `committedAt > 0`: Server-committed change (updates committedRev
|
|
61
|
-
*
|
|
74
|
+
* - `committedAt > 0`: Server-committed change (updates committedRev, recomputes
|
|
75
|
+
* state with remaining optimistic ops preserved)
|
|
76
|
+
* - `committedAt === 0`: Pending local change (shifts from optimistic queue,
|
|
77
|
+
* skips state update since change() already applied the ops)
|
|
62
78
|
*
|
|
63
79
|
* @param changes Array of changes to apply
|
|
64
80
|
* @param hasPending If provided, overrides the inferred pending state.
|
|
@@ -66,17 +82,13 @@ class LWWDoc extends BaseDoc {
|
|
|
66
82
|
*/
|
|
67
83
|
applyChanges(changes, hasPending) {
|
|
68
84
|
if (changes.length === 0) return;
|
|
69
|
-
let currentState = this.state;
|
|
70
|
-
for (const change of changes) {
|
|
71
|
-
for (const op of change.ops) {
|
|
72
|
-
currentState = applyPatch(currentState, [op], { partial: true });
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
85
|
let lastCommittedRev = this._committedRev;
|
|
76
86
|
let hasPendingChanges = false;
|
|
87
|
+
let hasServerChanges = false;
|
|
77
88
|
for (const change of changes) {
|
|
78
89
|
if (change.committedAt > 0) {
|
|
79
90
|
lastCommittedRev = change.rev;
|
|
91
|
+
hasServerChanges = true;
|
|
80
92
|
} else {
|
|
81
93
|
hasPendingChanges = true;
|
|
82
94
|
}
|
|
@@ -84,7 +96,27 @@ class LWWDoc extends BaseDoc {
|
|
|
84
96
|
this._committedRev = lastCommittedRev;
|
|
85
97
|
this._hasPending = hasPending ?? hasPendingChanges;
|
|
86
98
|
this._checkLoaded();
|
|
87
|
-
|
|
99
|
+
if (hasServerChanges) {
|
|
100
|
+
let newBaseState = this._baseState;
|
|
101
|
+
for (const change of changes) {
|
|
102
|
+
for (const op of change.ops) {
|
|
103
|
+
newBaseState = applyPatch(newBaseState, [op], { partial: true });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
this._baseState = newBaseState;
|
|
107
|
+
this._recomputeState();
|
|
108
|
+
} else {
|
|
109
|
+
for (const change of changes) {
|
|
110
|
+
for (const op of change.ops) {
|
|
111
|
+
this._baseState = applyPatch(this._baseState, [op], { partial: true });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (this._optimisticOps.length > 0) {
|
|
115
|
+
this._optimisticOps.shift();
|
|
116
|
+
} else {
|
|
117
|
+
this._recomputeState();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
88
120
|
}
|
|
89
121
|
}
|
|
90
122
|
export {
|
|
@@ -158,9 +158,6 @@ class LWWInMemoryStore {
|
|
|
158
158
|
for (const op of buf.sendingChange.ops) {
|
|
159
159
|
buf.committedFields.set(op.path, op.value);
|
|
160
160
|
}
|
|
161
|
-
if (buf.sendingChange.rev > buf.committedRev) {
|
|
162
|
-
buf.committedRev = buf.sendingChange.rev;
|
|
163
|
-
}
|
|
164
161
|
buf.sendingChange = null;
|
|
165
162
|
}
|
|
166
163
|
/**
|
|
@@ -243,10 +243,6 @@ class LWWIndexedDBStore {
|
|
|
243
243
|
return;
|
|
244
244
|
}
|
|
245
245
|
await Promise.all(sending.change.ops.map((op) => committedOps.put({ ...op, docId })));
|
|
246
|
-
const docMeta = await docsStore.get(docId) ?? { docId, committedRev: 0, algorithm: "lww" };
|
|
247
|
-
if (sending.change.rev > docMeta.committedRev) {
|
|
248
|
-
await docsStore.put({ ...docMeta, committedRev: sending.change.rev });
|
|
249
|
-
}
|
|
250
246
|
await sendingChanges.delete(docId);
|
|
251
247
|
await tx.complete();
|
|
252
248
|
}
|
|
@@ -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 { P as PatchesDocOptions, a as PatchesDoc } from '../BaseDoc-
|
|
5
|
+
import { P as PatchesDocOptions, a as PatchesDoc } from '../BaseDoc-CD5wZQMm.js';
|
|
6
6
|
import { TrackedDoc } from './PatchesStore.js';
|
|
7
7
|
import '../json-patch/JSONPatch.js';
|
|
8
8
|
import '@dabble/delta';
|
package/dist/client/OTDoc.d.ts
CHANGED
package/dist/client/OTDoc.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import "../chunk-IZ2YBCUP.js";
|
|
2
2
|
import { createStateFromSnapshot } from "../algorithms/ot/client/createStateFromSnapshot.js";
|
|
3
3
|
import { applyChanges as applyChangesToState } from "../algorithms/ot/shared/applyChanges.js";
|
|
4
|
+
import { applyPatch } from "../json-patch/applyPatch.js";
|
|
4
5
|
import { BaseDoc } from "./BaseDoc.js";
|
|
5
6
|
class OTDoc extends BaseDoc {
|
|
6
7
|
/** Base state from the server at the committed revision. */
|
|
@@ -21,7 +22,21 @@ class OTDoc extends BaseDoc {
|
|
|
21
22
|
this._committedRev = snapshot?.rev ?? 0;
|
|
22
23
|
this._pendingChanges = snapshot?.changes ?? [];
|
|
23
24
|
if (this._pendingChanges.length > 0) {
|
|
24
|
-
|
|
25
|
+
try {
|
|
26
|
+
this.state = applyChangesToState(this._committedState, this._pendingChanges);
|
|
27
|
+
} catch {
|
|
28
|
+
let state = this._committedState;
|
|
29
|
+
const valid = [];
|
|
30
|
+
for (const c of this._pendingChanges) {
|
|
31
|
+
try {
|
|
32
|
+
state = applyPatch(state, c.ops, { strict: true });
|
|
33
|
+
valid.push(c);
|
|
34
|
+
} catch {
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
this._pendingChanges = valid;
|
|
38
|
+
this.state = state;
|
|
39
|
+
}
|
|
25
40
|
}
|
|
26
41
|
this._checkLoaded();
|
|
27
42
|
}
|
|
@@ -48,16 +63,28 @@ class OTDoc extends BaseDoc {
|
|
|
48
63
|
this._committedState = snapshot.state;
|
|
49
64
|
this._committedRev = snapshot.rev;
|
|
50
65
|
this._pendingChanges = snapshot.changes;
|
|
66
|
+
this._optimisticOps = [];
|
|
51
67
|
this._checkLoaded();
|
|
52
68
|
this.state = createStateFromSnapshot(snapshot);
|
|
53
69
|
}
|
|
54
70
|
/**
|
|
55
|
-
*
|
|
56
|
-
|
|
71
|
+
* Recomputes state from committed + pending + remaining optimistic ops.
|
|
72
|
+
*/
|
|
73
|
+
_recomputeState() {
|
|
74
|
+
let newState = applyChangesToState(this._committedState, this._pendingChanges);
|
|
75
|
+
for (const ops of this._optimisticOps) {
|
|
76
|
+
newState = applyPatch(newState, ops, { strict: true });
|
|
77
|
+
}
|
|
78
|
+
this.state = newState;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Confirms changes from the algorithm pipeline.
|
|
57
82
|
*
|
|
58
|
-
*
|
|
59
|
-
* - `committedAt > 0`: Server-committed change (
|
|
60
|
-
*
|
|
83
|
+
* Distinguishes between committed and pending changes using `committedAt`:
|
|
84
|
+
* - `committedAt > 0`: Server-committed change (updates committed state, recomputes
|
|
85
|
+
* with remaining optimistic ops preserved)
|
|
86
|
+
* - `committedAt === 0`: Local change confirmation (shifts from optimistic queue,
|
|
87
|
+
* skips state update since change() already applied the ops)
|
|
61
88
|
*
|
|
62
89
|
* For server changes, all committed changes come first, followed by rebased pending.
|
|
63
90
|
*
|
|
@@ -76,11 +103,15 @@ class OTDoc extends BaseDoc {
|
|
|
76
103
|
this._committedRev = serverChanges[serverChanges.length - 1].rev;
|
|
77
104
|
this._pendingChanges = rebasedPending;
|
|
78
105
|
this._checkLoaded();
|
|
79
|
-
this.
|
|
106
|
+
this._recomputeState();
|
|
80
107
|
} else {
|
|
81
108
|
this._pendingChanges.push(...changes);
|
|
82
109
|
this._checkLoaded();
|
|
83
|
-
|
|
110
|
+
if (this._optimisticOps.length > 0) {
|
|
111
|
+
this._optimisticOps.shift();
|
|
112
|
+
} else {
|
|
113
|
+
this.state = applyChangesToState(this.state, changes);
|
|
114
|
+
}
|
|
84
115
|
}
|
|
85
116
|
}
|
|
86
117
|
/**
|
package/dist/client/Patches.d.ts
CHANGED
|
@@ -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 { P as PatchesDocOptions, a as PatchesDoc } from '../BaseDoc-
|
|
6
|
+
import { P as PatchesDocOptions, a as PatchesDoc } from '../BaseDoc-CD5wZQMm.js';
|
|
7
7
|
import { AlgorithmName } from './PatchesStore.js';
|
|
8
8
|
import '../json-patch/JSONPatch.js';
|
|
9
9
|
import '@dabble/delta';
|
|
@@ -46,6 +46,7 @@ interface ManagedDoc<T extends object> {
|
|
|
46
46
|
declare class Patches {
|
|
47
47
|
protected options: PatchesOptions;
|
|
48
48
|
protected docs: Map<string, ManagedDoc<any>>;
|
|
49
|
+
private _changeQueues;
|
|
49
50
|
readonly docOptions: PatchesDocOptions;
|
|
50
51
|
readonly algorithms: Partial<Record<AlgorithmName, ClientAlgorithm>>;
|
|
51
52
|
readonly defaultAlgorithm: AlgorithmName;
|
|
@@ -124,9 +125,12 @@ declare class Patches {
|
|
|
124
125
|
close(): Promise<void>;
|
|
125
126
|
/**
|
|
126
127
|
* Internal handler for doc changes. Called when doc.onChange emits ops.
|
|
127
|
-
*
|
|
128
|
+
* Serializes calls per docId to prevent concurrent handleDocChange from
|
|
129
|
+
* creating changes with the same rev (which would overwrite each other
|
|
130
|
+
* in IndexedDB's [docId, rev] keyed pendingChanges store).
|
|
128
131
|
*/
|
|
129
132
|
protected _handleDocChange<T extends object>(docId: string, ops: JSONPatchOp[], doc: PatchesDoc<T>, algorithm: ClientAlgorithm, metadata: Record<string, any>): Promise<void>;
|
|
133
|
+
private _processDocChange;
|
|
130
134
|
}
|
|
131
135
|
|
|
132
136
|
export { type OpenDocOptions, Patches, type PatchesOptions };
|
package/dist/client/Patches.js
CHANGED
|
@@ -14,6 +14,7 @@ class Patches {
|
|
|
14
14
|
__runInitializers(_init, 5, this);
|
|
15
15
|
__publicField(this, "options");
|
|
16
16
|
__publicField(this, "docs", /* @__PURE__ */ new Map());
|
|
17
|
+
__publicField(this, "_changeQueues", /* @__PURE__ */ new Map());
|
|
17
18
|
__publicField(this, "docOptions");
|
|
18
19
|
__publicField(this, "algorithms");
|
|
19
20
|
__publicField(this, "defaultAlgorithm");
|
|
@@ -148,6 +149,7 @@ class Patches {
|
|
|
148
149
|
if (managed) {
|
|
149
150
|
managed.unsubscribe();
|
|
150
151
|
this.docs.delete(docId);
|
|
152
|
+
this._changeQueues.delete(docId);
|
|
151
153
|
if (untrack) {
|
|
152
154
|
await this.untrackDocs([docId]);
|
|
153
155
|
}
|
|
@@ -186,6 +188,7 @@ class Patches {
|
|
|
186
188
|
async close() {
|
|
187
189
|
this.docs.forEach((managed) => managed.unsubscribe());
|
|
188
190
|
this.docs.clear();
|
|
191
|
+
this._changeQueues.clear();
|
|
189
192
|
await Promise.all(Object.values(this.algorithms).map((s) => s?.close()));
|
|
190
193
|
this.onChange.clear();
|
|
191
194
|
this.onDeleteDoc.clear();
|
|
@@ -196,14 +199,27 @@ class Patches {
|
|
|
196
199
|
}
|
|
197
200
|
/**
|
|
198
201
|
* Internal handler for doc changes. Called when doc.onChange emits ops.
|
|
199
|
-
*
|
|
202
|
+
* Serializes calls per docId to prevent concurrent handleDocChange from
|
|
203
|
+
* creating changes with the same rev (which would overwrite each other
|
|
204
|
+
* in IndexedDB's [docId, rev] keyed pendingChanges store).
|
|
200
205
|
*/
|
|
201
|
-
|
|
206
|
+
_handleDocChange(docId, ops, doc, algorithm, metadata) {
|
|
207
|
+
const prev = this._changeQueues.get(docId) ?? Promise.resolve();
|
|
208
|
+
const current = prev.then(() => this._processDocChange(docId, ops, doc, algorithm, metadata));
|
|
209
|
+
this._changeQueues.set(docId, current.catch(() => {
|
|
210
|
+
}));
|
|
211
|
+
return current;
|
|
212
|
+
}
|
|
213
|
+
async _processDocChange(docId, ops, doc, algorithm, metadata) {
|
|
202
214
|
try {
|
|
203
215
|
await algorithm.handleDocChange(docId, ops, doc, metadata);
|
|
204
216
|
this.onChange.emit(docId);
|
|
205
217
|
} catch (err) {
|
|
206
218
|
console.error(`Error handling doc change for ${docId}:`, err);
|
|
219
|
+
const baseDoc = doc;
|
|
220
|
+
if (typeof baseDoc.rollbackOptimistic === "function") {
|
|
221
|
+
baseDoc.rollbackOptimistic();
|
|
222
|
+
}
|
|
207
223
|
this.onError.emit(err, { docId });
|
|
208
224
|
}
|
|
209
225
|
}
|
|
@@ -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, a as PatchesDoc, O as PatchesDocClass, P as PatchesDocOptions } from '../BaseDoc-
|
|
4
|
+
export { O as OTDoc, a as PatchesDoc, O as PatchesDocClass, P as PatchesDocOptions } from '../BaseDoc-CD5wZQMm.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 { P as PatchesDocOptions } from '../BaseDoc-
|
|
3
|
+
import { P as PatchesDocOptions } from '../BaseDoc-CD5wZQMm.js';
|
|
4
4
|
import '../types.js';
|
|
5
5
|
import '../json-patch/JSONPatch.js';
|
|
6
6
|
import '@dabble/delta';
|
package/dist/client/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { B as BaseDoc, O as OTDoc, a as PatchesDoc, O as PatchesDocClass, P as PatchesDocOptions } from '../BaseDoc-
|
|
1
|
+
export { B as BaseDoc, O as OTDoc, a as PatchesDoc, O as PatchesDocClass, P as PatchesDocOptions } from '../BaseDoc-CD5wZQMm.js';
|
|
2
2
|
export { IndexedDBFactoryOptions, MultiAlgorithmFactoryOptions, MultiAlgorithmIndexedDBFactoryOptions, PatchesFactoryOptions, createLWWIndexedDBPatches, createLWWPatches, createMultiAlgorithmIndexedDBPatches, createMultiAlgorithmPatches, createOTIndexedDBPatches, createOTPatches } 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, a as PatchesDoc, O as PatchesDocClass, P as PatchesDocOptions } from './BaseDoc-
|
|
2
|
+
export { B as BaseDoc, O as OTDoc, a as PatchesDoc, O as PatchesDocClass, P as PatchesDocOptions } from './BaseDoc-CD5wZQMm.js';
|
|
3
3
|
export { IndexedDBFactoryOptions, MultiAlgorithmFactoryOptions, MultiAlgorithmIndexedDBFactoryOptions, PatchesFactoryOptions, createLWWIndexedDBPatches, createLWWPatches, createMultiAlgorithmIndexedDBPatches, createMultiAlgorithmPatches, createOTIndexedDBPatches, createOTPatches } from './client/factories.js';
|
|
4
4
|
export { IDBStoreWrapper, IDBTransactionWrapper, IndexedDBStore } from './client/IndexedDBStore.js';
|
|
5
5
|
export { OTIndexedDBStore } from './client/OTIndexedDBStore.js';
|
package/dist/net/index.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ import 'easy-signal';
|
|
|
17
17
|
import '../algorithms/ot/shared/changeBatching.js';
|
|
18
18
|
import '../client/ClientAlgorithm.js';
|
|
19
19
|
import '../json-patch/types.js';
|
|
20
|
-
import '../BaseDoc-
|
|
20
|
+
import '../BaseDoc-CD5wZQMm.js';
|
|
21
21
|
import '../client/PatchesStore.js';
|
|
22
22
|
import '../client/Patches.js';
|
|
23
23
|
import '../server/types.js';
|
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-CD5wZQMm.js';
|
|
11
11
|
import '../client/PatchesStore.js';
|
|
12
12
|
import '../net/protocol/types.js';
|
|
13
13
|
import '../net/protocol/JSONRPCClient.js';
|
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-CD5wZQMm.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-CD5wZQMm.js';
|
|
4
4
|
import { JSONPatch } from '../json-patch/JSONPatch.js';
|
|
5
5
|
import { ChangeMutator } from '../types.js';
|
|
6
6
|
import 'easy-signal';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ShallowRef, Ref, MaybeRef } from 'vue';
|
|
2
2
|
import { OpenDocOptions } from '../client/Patches.js';
|
|
3
|
-
import { a as PatchesDoc } from '../BaseDoc-
|
|
3
|
+
import { a as PatchesDoc } from '../BaseDoc-CD5wZQMm.js';
|
|
4
4
|
import { JSONPatch } from '../json-patch/JSONPatch.js';
|
|
5
5
|
import { ChangeMutator } from '../types.js';
|
|
6
6
|
import 'easy-signal';
|
|
@@ -77,7 +77,7 @@ interface UsePatchesDocReturn<T extends object> {
|
|
|
77
77
|
* The underlying PatchesDoc instance.
|
|
78
78
|
* Useful for advanced operations.
|
|
79
79
|
*/
|
|
80
|
-
doc:
|
|
80
|
+
doc: ShallowRef<PatchesDoc<T> | undefined>;
|
|
81
81
|
}
|
|
82
82
|
/**
|
|
83
83
|
* Return type for usePatchesDoc composable (lazy mode).
|
package/dist/vue/composables.js
CHANGED
|
@@ -13,7 +13,7 @@ import { usePatchesContext } from "./provider.js";
|
|
|
13
13
|
import { getDocManager } from "./doc-manager.js";
|
|
14
14
|
function createDocReactiveState(options) {
|
|
15
15
|
const { initialLoading = true, hasSyncContext = false, transformState, changeBehavior } = options;
|
|
16
|
-
const doc =
|
|
16
|
+
const doc = shallowRef(void 0);
|
|
17
17
|
const data = shallowRef(void 0);
|
|
18
18
|
const loading = ref(initialLoading);
|
|
19
19
|
const error = ref();
|
package/dist/vue/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-CD5wZQMm.js';
|
|
15
15
|
import '../client/PatchesStore.js';
|
|
16
16
|
import '../net/PatchesSync.js';
|
|
17
17
|
import '../net/protocol/types.js';
|
package/dist/vue/provider.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-CD5wZQMm.js';
|
|
11
11
|
import '../client/PatchesStore.js';
|
|
12
12
|
import '../net/protocol/types.js';
|
|
13
13
|
import '../net/protocol/JSONRPCClient.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dabble/patches",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.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": {
|