@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
|
-
|
|
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<
|
|
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 };
|
package/dist/net/PatchesSync.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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": {
|