@dabble/patches 0.8.12 → 0.8.14
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.
|
@@ -162,6 +162,10 @@ class LWWInMemoryStore {
|
|
|
162
162
|
const buf = this.docs.get(docId);
|
|
163
163
|
if (!buf?.sendingChange) return;
|
|
164
164
|
for (const op of buf.sendingChange.ops) {
|
|
165
|
+
const childPrefix = op.path + "/";
|
|
166
|
+
for (const key of buf.committedFields.keys()) {
|
|
167
|
+
if (key.startsWith(childPrefix)) buf.committedFields.delete(key);
|
|
168
|
+
}
|
|
165
169
|
buf.committedFields.set(op.path, op.value);
|
|
166
170
|
}
|
|
167
171
|
if (buf.sendingChange.rev > buf.committedRev) {
|
|
@@ -176,6 +180,10 @@ class LWWInMemoryStore {
|
|
|
176
180
|
const buf = this.getOrCreateBuffer(docId);
|
|
177
181
|
for (const change of serverChanges) {
|
|
178
182
|
for (const op of change.ops) {
|
|
183
|
+
const childPrefix = op.path + "/";
|
|
184
|
+
for (const key of buf.committedFields.keys()) {
|
|
185
|
+
if (key.startsWith(childPrefix)) buf.committedFields.delete(key);
|
|
186
|
+
}
|
|
179
187
|
buf.committedFields.set(op.path, op.value);
|
|
180
188
|
}
|
|
181
189
|
}
|
|
@@ -245,7 +245,12 @@ const _LWWIndexedDBStore = class _LWWIndexedDBStore {
|
|
|
245
245
|
await tx.complete();
|
|
246
246
|
return;
|
|
247
247
|
}
|
|
248
|
-
await Promise.all(
|
|
248
|
+
await Promise.all(
|
|
249
|
+
sending.change.ops.map(async (op) => {
|
|
250
|
+
await committedOps.delete([docId, op.path + "/"], [docId, op.path + "/\uFFFF"]);
|
|
251
|
+
await committedOps.put({ ...op, docId });
|
|
252
|
+
})
|
|
253
|
+
);
|
|
249
254
|
const rev = sending.change.rev;
|
|
250
255
|
if (rev !== void 0) {
|
|
251
256
|
const docMeta = await docsStore.get(docId) ?? { docId, committedRev: 0, algorithm: "lww" };
|
|
@@ -262,7 +267,12 @@ const _LWWIndexedDBStore = class _LWWIndexedDBStore {
|
|
|
262
267
|
"readwrite"
|
|
263
268
|
);
|
|
264
269
|
const allOps = serverChanges.flatMap((change) => change.ops);
|
|
265
|
-
await Promise.all(
|
|
270
|
+
await Promise.all(
|
|
271
|
+
allOps.map(async (op) => {
|
|
272
|
+
await committedOps.delete([docId, op.path + "/"], [docId, op.path + "/\uFFFF"]);
|
|
273
|
+
await committedOps.put({ ...op, docId });
|
|
274
|
+
})
|
|
275
|
+
);
|
|
266
276
|
const lastCommittedRev = serverChanges.at(-1)?.rev;
|
|
267
277
|
if (lastCommittedRev !== void 0) {
|
|
268
278
|
const docMeta = await docsStore.get(docId) ?? { docId, committedRev: 0, algorithm: "lww" };
|
|
@@ -26,8 +26,8 @@ interface PatchesBranchClientOptions {
|
|
|
26
26
|
* (offline-first, local store handles caching/pending/tombstones). The API shape
|
|
27
27
|
* determines merge behavior:
|
|
28
28
|
*
|
|
29
|
-
* - `BranchAPI`
|
|
30
|
-
* - `BranchClientStore`
|
|
29
|
+
* - `BranchAPI` — server performs the merge via `mergeBranch`
|
|
30
|
+
* - `BranchClientStore` — merge is not supported; call the server merge endpoint directly
|
|
31
31
|
*/
|
|
32
32
|
declare class PatchesBranchClient {
|
|
33
33
|
private readonly api;
|
|
@@ -81,16 +81,15 @@ declare class PatchesBranchClient {
|
|
|
81
81
|
/**
|
|
82
82
|
* Merge a branch's changes back into this document.
|
|
83
83
|
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
84
|
+
* Requires a `BranchAPI` (online mode) — the server performs the merge.
|
|
85
|
+
* Throws if the API is a `BranchClientStore` (offline-first mode) because
|
|
86
|
+
* client stores don't maintain full change history needed for correct merging.
|
|
87
|
+
* Offline-first consumers should call the server merge endpoint directly.
|
|
88
88
|
*/
|
|
89
89
|
mergeBranch(branchId: string): Promise<void>;
|
|
90
90
|
/** Clear state */
|
|
91
91
|
clear(): void;
|
|
92
92
|
private _createBranchOffline;
|
|
93
|
-
private _mergeBranchLocally;
|
|
94
93
|
}
|
|
95
94
|
|
|
96
95
|
export { PatchesBranchClient, type PatchesBranchClientOptions };
|
|
@@ -2,6 +2,7 @@ import "../chunk-IZ2YBCUP.js";
|
|
|
2
2
|
import { store } from "easy-signal";
|
|
3
3
|
import { breakChanges } from "../algorithms/ot/shared/changeBatching.js";
|
|
4
4
|
import { createChange } from "../data/change.js";
|
|
5
|
+
const OFFLINE_MERGE_ERROR = "Branch merging requires a server connection. Use a BranchAPI or call the server merge endpoint directly.";
|
|
5
6
|
class PatchesBranchClient {
|
|
6
7
|
constructor(id, api, patches, options) {
|
|
7
8
|
this.api = api;
|
|
@@ -87,18 +88,17 @@ class PatchesBranchClient {
|
|
|
87
88
|
/**
|
|
88
89
|
* Merge a branch's changes back into this document.
|
|
89
90
|
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
91
|
+
* Requires a `BranchAPI` (online mode) — the server performs the merge.
|
|
92
|
+
* Throws if the API is a `BranchClientStore` (offline-first mode) because
|
|
93
|
+
* client stores don't maintain full change history needed for correct merging.
|
|
94
|
+
* Offline-first consumers should call the server merge endpoint directly.
|
|
94
95
|
*/
|
|
95
96
|
async mergeBranch(branchId) {
|
|
96
|
-
if (
|
|
97
|
-
|
|
98
|
-
await this.listBranches();
|
|
99
|
-
return;
|
|
97
|
+
if (this.isOffline) {
|
|
98
|
+
throw new Error(OFFLINE_MERGE_ERROR);
|
|
100
99
|
}
|
|
101
|
-
await this.
|
|
100
|
+
await this.api.mergeBranch(branchId);
|
|
101
|
+
await this.listBranches();
|
|
102
102
|
}
|
|
103
103
|
/** Clear state */
|
|
104
104
|
clear() {
|
|
@@ -148,29 +148,6 @@ class PatchesBranchClient {
|
|
|
148
148
|
this.patches.onChange.emit(branchDocId);
|
|
149
149
|
return branchDocId;
|
|
150
150
|
}
|
|
151
|
-
async _mergeBranchLocally(branchId) {
|
|
152
|
-
const offlineApi = this.api;
|
|
153
|
-
const branch = this.branches.state.find((b) => b.id === branchId);
|
|
154
|
-
if (!branch) throw new Error(`Branch ${branchId} not found`);
|
|
155
|
-
const sourceDocId = branch.docId;
|
|
156
|
-
const algorithmName = this.options?.algorithm ?? this.patches.defaultAlgorithm;
|
|
157
|
-
const algorithm = this.patches.algorithms[algorithmName];
|
|
158
|
-
if (!algorithm?.listChanges) {
|
|
159
|
-
throw new Error("Offline merge requires an algorithm with listChanges support");
|
|
160
|
-
}
|
|
161
|
-
const startAfter = branch.lastMergedRev ?? (branch.contentStartRev ?? 2) - 1;
|
|
162
|
-
const branchChanges = await algorithm.listChanges(branchId, { startAfter });
|
|
163
|
-
if (branchChanges.length === 0) return;
|
|
164
|
-
const lastBranchRev = branchChanges[branchChanges.length - 1].rev;
|
|
165
|
-
for (const change of branchChanges) {
|
|
166
|
-
await this.patches.submitDocChange(sourceDocId, change.ops, { batchId: branchId });
|
|
167
|
-
}
|
|
168
|
-
await offlineApi.updateBranch(branchId, { lastMergedRev: lastBranchRev });
|
|
169
|
-
this.patches.onChange.emit(sourceDocId);
|
|
170
|
-
this.branches.state = this.branches.state.map(
|
|
171
|
-
(b) => b.id === branchId ? { ...b, lastMergedRev: lastBranchRev } : b
|
|
172
|
-
);
|
|
173
|
-
}
|
|
174
151
|
}
|
|
175
152
|
export {
|
|
176
153
|
PatchesBranchClient
|
package/dist/net/PatchesSync.js
CHANGED
|
@@ -191,7 +191,15 @@ class PatchesSync extends (_a = ReadonlyStoreClass, _syncDoc_dec = [serialGate],
|
|
|
191
191
|
for (const branch of creates) {
|
|
192
192
|
if (!this.state.connected) break;
|
|
193
193
|
try {
|
|
194
|
-
const {
|
|
194
|
+
const {
|
|
195
|
+
docId: sourceDocId,
|
|
196
|
+
branchedAtRev,
|
|
197
|
+
createdAt: _1,
|
|
198
|
+
modifiedAt: _2,
|
|
199
|
+
pendingOp: _3,
|
|
200
|
+
deleted: _4,
|
|
201
|
+
...metadata
|
|
202
|
+
} = branch;
|
|
195
203
|
await branchApi.createBranch(sourceDocId, branchedAtRev, metadata);
|
|
196
204
|
const synced = { ...branch, pendingOp: void 0 };
|
|
197
205
|
delete synced.pendingOp;
|
|
@@ -205,7 +213,17 @@ class PatchesSync extends (_a = ReadonlyStoreClass, _syncDoc_dec = [serialGate],
|
|
|
205
213
|
for (const branch of updates) {
|
|
206
214
|
if (!this.state.connected) break;
|
|
207
215
|
try {
|
|
208
|
-
const {
|
|
216
|
+
const {
|
|
217
|
+
id: _id,
|
|
218
|
+
docId: _did,
|
|
219
|
+
branchedAtRev: _bar,
|
|
220
|
+
createdAt: _ca,
|
|
221
|
+
modifiedAt: _ma,
|
|
222
|
+
contentStartRev: _csr,
|
|
223
|
+
pendingOp: _po,
|
|
224
|
+
deleted: _del,
|
|
225
|
+
...metadata
|
|
226
|
+
} = branch;
|
|
209
227
|
await branchApi.updateBranch(branch.id, metadata);
|
|
210
228
|
const synced = { ...branch, pendingOp: void 0 };
|
|
211
229
|
delete synced.pendingOp;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dabble/patches",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.14",
|
|
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": {
|