@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(sending.change.ops.map((op) => committedOps.put({ ...op, docId })));
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(allOps.map((op) => committedOps.put({ ...op, docId })));
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` has `mergeBranch` — server performs the merge
30
- * - `BranchClientStore` has `updateBranch` client merges locally, updates `lastMergedRev`
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
- * Online (BranchAPI with `mergeBranch`): server performs the merge.
85
- * Offline (BranchClientStore with `updateBranch`): client reads branch changes,
86
- * re-stamps them with `batchId: branchId`, submits via algorithm.handleDocChange
87
- * on the source doc, then updates `lastMergedRev` locally.
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
- * Online (BranchAPI with `mergeBranch`): server performs the merge.
91
- * Offline (BranchClientStore with `updateBranch`): client reads branch changes,
92
- * re-stamps them with `batchId: branchId`, submits via algorithm.handleDocChange
93
- * on the source doc, then updates `lastMergedRev` locally.
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 (!this.isOffline) {
97
- await this.api.mergeBranch(branchId);
98
- await this.listBranches();
99
- return;
97
+ if (this.isOffline) {
98
+ throw new Error(OFFLINE_MERGE_ERROR);
100
99
  }
101
- await this._mergeBranchLocally(branchId);
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
@@ -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 { docId: sourceDocId, branchedAtRev, createdAt: _1, modifiedAt: _2, pendingOp: _3, deleted: _4, ...metadata } = branch;
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 { id: _id, docId: _did, branchedAtRev: _bar, createdAt: _ca, modifiedAt: _ma, contentStartRev: _csr, pendingOp: _po, deleted: _del, ...metadata } = branch;
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.12",
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": {