@dabble/patches 0.5.16 → 0.5.17

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.
@@ -26,10 +26,10 @@ const add = {
26
26
  if (index < 0 || target.length < index) {
27
27
  return `[op:add] invalid array index: ${path}`;
28
28
  }
29
- pluckWithShallowCopy(state, keys, true).splice(index, 0, value);
29
+ pluckWithShallowCopy(state, keys, true, true).splice(index, 0, value);
30
30
  } else {
31
31
  if (!deepEqual(target[lastKey], value)) {
32
- pluckWithShallowCopy(state, keys, true)[lastKey] = value;
32
+ pluckWithShallowCopy(state, keys, true, true)[lastKey] = value;
33
33
  }
34
34
  }
35
35
  },
@@ -5,7 +5,7 @@ export { getType, getTypeLike } from './getType.js';
5
5
  export { log, verbose } from './log.js';
6
6
  export { isAdd, mapAndFilterOps, transformRemove, updateRemovedOps } from './ops.js';
7
7
  export { getArrayIndex, getArrayPrefixAndIndex, getIndexAndEnd, getPrefix, getPrefixAndProp, getProp, getPropAfter, isArrayPath } from './paths.js';
8
- export { EMPTY, getValue, pluck, pluckWithShallowCopy } from './pluck.js';
8
+ export { EMPTY, EMPTY_ARRAY, getValue, pluck, pluckWithShallowCopy } from './pluck.js';
9
9
  export { shallowCopy } from './shallowCopy.js';
10
10
  export { isEmptyObject, updateSoftWrites } from './softWrites.js';
11
11
  export { toArrayIndex } from './toArrayIndex.js';
@@ -1,8 +1,9 @@
1
1
  import { State } from '../types.js';
2
2
 
3
3
  declare const EMPTY: {};
4
+ declare const EMPTY_ARRAY: any[];
4
5
  declare function pluck(state: State, keys: string[]): any;
5
- declare function pluckWithShallowCopy(state: State, keys: string[], createMissingObjects?: boolean): any;
6
+ declare function pluckWithShallowCopy(state: State, keys: string[], createMissingObjects?: boolean, createMissingArrays?: boolean): any;
6
7
  declare function getValue(state: State, value: any, addKey?: string, addValue?: any): any;
7
8
 
8
- export { EMPTY, getValue, pluck, pluckWithShallowCopy };
9
+ export { EMPTY, EMPTY_ARRAY, getValue, pluck, pluckWithShallowCopy };
@@ -1,6 +1,7 @@
1
1
  import "../../chunk-IZ2YBCUP.js";
2
2
  import { shallowCopy } from "./shallowCopy.js";
3
3
  const EMPTY = {};
4
+ const EMPTY_ARRAY = [];
4
5
  function pluck(state, keys) {
5
6
  let object = state.root;
6
7
  for (let i = 0, imax = keys.length - 1; i < imax; i++) {
@@ -12,11 +13,12 @@ function pluck(state, keys) {
12
13
  }
13
14
  return object;
14
15
  }
15
- function pluckWithShallowCopy(state, keys, createMissingObjects) {
16
+ function pluckWithShallowCopy(state, keys, createMissingObjects, createMissingArrays) {
16
17
  let object = state.root;
17
18
  for (let i = 0, imax = keys.length - 1; i < imax; i++) {
18
19
  const key = keys[i];
19
- object = object[key] = createMissingObjects && !object[key] ? getValue(state, EMPTY) : getValue(state, object[key]);
20
+ const container = createMissingArrays && keys[i + 1] === "0" ? EMPTY_ARRAY : EMPTY;
21
+ object = object[key] = createMissingObjects && !object[key] ? getValue(state, container) : getValue(state, object[key]);
20
22
  }
21
23
  return object;
22
24
  }
@@ -30,6 +32,7 @@ function getValue(state, value, addKey, addValue) {
30
32
  }
31
33
  export {
32
34
  EMPTY,
35
+ EMPTY_ARRAY,
33
36
  getValue,
34
37
  pluck,
35
38
  pluckWithShallowCopy
@@ -101,8 +101,9 @@ declare class PatchesSync {
101
101
  /**
102
102
  * Flushes a document to the server.
103
103
  * @param docId The ID of the document to flush.
104
+ * @param pending Optional pending changes to flush, to avoid redundant store fetch.
104
105
  */
105
- protected flushDoc(docId: string): Promise<void>;
106
+ protected flushDoc(docId: string, pending?: Change[]): Promise<void>;
106
107
  /**
107
108
  * Receives committed changes from the server and applies them to the document. This is a blockable function, so it
108
109
  * is separate from applyServerChangesToDoc, which is called by other blockable functions. Ensuring this is blockable
@@ -114,7 +115,7 @@ declare class PatchesSync {
114
115
  * Applies server changes to a document using the centralized sync algorithm.
115
116
  * This ensures consistent OT behavior regardless of whether the doc is open in memory.
116
117
  */
117
- protected _applyServerChangesToDoc(docId: string, serverChanges: Change[], sentPendingRange?: [number, number]): Promise<void>;
118
+ protected _applyServerChangesToDoc(docId: string, serverChanges: Change[], sentPendingRange?: [number, number]): Promise<Change[]>;
118
119
  /**
119
120
  * Initiates the deletion process for a document both locally and on the server.
120
121
  * This now delegates the local tombstone marking to Patches.
@@ -133,6 +134,14 @@ declare class PatchesSync {
133
134
  * Helper to detect DOC_DELETED (410) errors from the server.
134
135
  */
135
136
  protected _isDocDeletedError(err: unknown): boolean;
137
+ /**
138
+ * Helper to detect "document already exists" errors from the server.
139
+ */
140
+ private _isDocExistsError;
141
+ /**
142
+ * Recovers from a "document already exists" error by fetching server state and retrying.
143
+ */
144
+ private _recoverFromDocExists;
136
145
  }
137
146
 
138
147
  export { PatchesSync, type PatchesSyncOptions, type PatchesSyncState };
@@ -169,7 +169,7 @@ class PatchesSync {
169
169
  try {
170
170
  const pending = await this.store.getPendingChanges(docId);
171
171
  if (pending.length > 0) {
172
- await this.flushDoc(docId);
172
+ await this.flushDoc(docId, pending);
173
173
  } else {
174
174
  const [committedRev] = await this.store.getLastRevs(docId);
175
175
  if (committedRev) {
@@ -203,8 +203,9 @@ class PatchesSync {
203
203
  /**
204
204
  * Flushes a document to the server.
205
205
  * @param docId The ID of the document to flush.
206
+ * @param pending Optional pending changes to flush, to avoid redundant store fetch.
206
207
  */
207
- async flushDoc(docId) {
208
+ async flushDoc(docId, pending) {
208
209
  if (!this.trackedDocs.has(docId)) {
209
210
  throw new Error(`Document ${docId} is not tracked`);
210
211
  }
@@ -212,7 +213,7 @@ class PatchesSync {
212
213
  throw new Error("Not connected to server");
213
214
  }
214
215
  try {
215
- let pending = await this.store.getPendingChanges(docId);
216
+ if (!pending) pending = await this.store.getPendingChanges(docId);
216
217
  if (!pending.length) {
217
218
  return;
218
219
  }
@@ -235,6 +236,10 @@ class PatchesSync {
235
236
  await this._handleRemoteDocDeleted(docId);
236
237
  return;
237
238
  }
239
+ if (this._isDocExistsError(err)) {
240
+ await this._recoverFromDocExists(docId);
241
+ return;
242
+ }
238
243
  console.error(`Flush failed for doc ${docId}:`, err);
239
244
  this.onError.emit(err, { docId });
240
245
  throw err;
@@ -255,7 +260,7 @@ class PatchesSync {
255
260
  const currentSnapshot = await this.store.getDoc(docId);
256
261
  if (!currentSnapshot) {
257
262
  console.warn(`Cannot apply server changes to non-existent doc: ${docId}`);
258
- return;
263
+ return [];
259
264
  }
260
265
  const doc = this.patches.getOpenDoc(docId);
261
266
  if (doc) {
@@ -276,6 +281,7 @@ class PatchesSync {
276
281
  this.store.saveCommittedChanges(docId, serverChanges, sentPendingRange),
277
282
  this.store.replacePendingChanges(docId, rebasedPendingChanges)
278
283
  ]);
284
+ return rebasedPendingChanges;
279
285
  }
280
286
  /**
281
287
  * Initiates the deletion process for a document both locally and on the server.
@@ -362,6 +368,23 @@ class PatchesSync {
362
368
  _isDocDeletedError(err) {
363
369
  return typeof err === "object" && err !== null && "code" in err && err.code === 410;
364
370
  }
371
+ /**
372
+ * Helper to detect "document already exists" errors from the server.
373
+ */
374
+ _isDocExistsError(err) {
375
+ const message = err?.message ?? "";
376
+ return message.includes("already exists");
377
+ }
378
+ /**
379
+ * Recovers from a "document already exists" error by fetching server state and retrying.
380
+ */
381
+ async _recoverFromDocExists(docId) {
382
+ const serverChanges = await this.ws.getChangesSince(docId, 0);
383
+ const rebasedPending = await this._applyServerChangesToDoc(docId, serverChanges);
384
+ if (rebasedPending.length > 0) {
385
+ await this.flushDoc(docId, rebasedPending);
386
+ }
387
+ }
365
388
  }
366
389
  _init = __decoratorStart(null);
367
390
  __decorateElement(_init, 1, "syncDoc", _syncDoc_dec, PatchesSync);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dabble/patches",
3
- "version": "0.5.16",
3
+ "version": "0.5.17",
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": {