@jarenjs/json 0.9.2 → 0.34.2

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.
Files changed (74) hide show
  1. package/ARCHITECTURE.md +86 -13
  2. package/README.md +248 -23
  3. package/dist/types/canonical.d.ts +37 -0
  4. package/dist/types/cow.d.ts +28 -0
  5. package/dist/types/errors.d.ts +45 -0
  6. package/dist/types/index.d.ts +3 -0
  7. package/dist/types/jslt/errors.d.ts +15 -8
  8. package/dist/types/jslt/index.d.ts +22 -0
  9. package/dist/types/jslt/packs/finance.d.ts +119 -0
  10. package/dist/types/jslt/packs/index.d.ts +310 -0
  11. package/dist/types/jslt/packs/math.d.ts +159 -0
  12. package/dist/types/jslt/packs/stats.d.ts +48 -0
  13. package/dist/types/jslt/registry.d.ts +65 -0
  14. package/dist/types/jtlt/errors.d.ts +3 -6
  15. package/dist/types/option-variants.d.ts +29 -0
  16. package/dist/types/patch.d.ts +214 -0
  17. package/dist/types/path.d.ts +139 -9
  18. package/dist/types/pointer.d.ts +100 -9
  19. package/dist/types/query/compile.d.ts +12 -0
  20. package/dist/types/query/errors.d.ts +72 -8
  21. package/dist/types/query/index.d.ts +317 -25
  22. package/dist/types/query/normalize.d.ts +24 -0
  23. package/dist/types/query/operators.d.ts +241 -1
  24. package/dist/types/query/runtime.d.ts +5 -8
  25. package/dist/types/query/types.d.ts +34 -0
  26. package/dist/types/segments.d.ts +31 -0
  27. package/dist/types/write.d.ts +204 -0
  28. package/dist/types/xquery/parse.d.ts +2 -3
  29. package/docs/JSLT-FORMAT.md +74 -3
  30. package/docs/JSLT-PRELUDE.md +1 -1
  31. package/docs/QUERY-FORMAT.md +695 -33
  32. package/package.json +18 -4
  33. package/schemas/geojson.draft-07.schema.json +323 -0
  34. package/schemas/geojson.jaren.schema.json +863 -0
  35. package/schemas/geojson.schema.json +172 -0
  36. package/schemas/jaren-jslt.authoring.schema.json +142 -0
  37. package/schemas/jaren-jslt.draft-07.schema.json +152 -11
  38. package/schemas/jaren-jslt.llm-profile.schema.json +782 -0
  39. package/schemas/jaren-jslt.schema.json +152 -11
  40. package/schemas/jaren-query.draft-07.schema.json +152 -11
  41. package/schemas/jaren-query.llm-profile.schema.json +619 -0
  42. package/schemas/jaren-query.schema.json +82 -15
  43. package/src/basic.js +1 -1
  44. package/src/canonical.js +170 -0
  45. package/src/cow.js +106 -0
  46. package/src/errors.js +68 -0
  47. package/src/index.js +3 -0
  48. package/src/jslt/dispatch.js +178 -28
  49. package/src/jslt/errors.js +19 -14
  50. package/src/jslt/index.js +37 -29
  51. package/src/jslt/packs/finance.js +49 -0
  52. package/src/jslt/packs/index.js +18 -0
  53. package/src/jslt/packs/math.js +46 -0
  54. package/src/jslt/packs/stats.js +65 -0
  55. package/src/jslt/registry.js +200 -0
  56. package/src/jslt/stylesheet.js +14 -23
  57. package/src/jtlt/desugar.js +2 -3
  58. package/src/jtlt/errors.js +6 -12
  59. package/src/jtlt/index.js +12 -29
  60. package/src/jtlt/template.js +9 -18
  61. package/src/option-variants.js +54 -0
  62. package/src/patch.js +1052 -0
  63. package/src/path.js +319 -52
  64. package/src/pointer.js +225 -44
  65. package/src/query/compile.js +790 -75
  66. package/src/query/errors.js +72 -12
  67. package/src/query/index.js +274 -42
  68. package/src/query/normalize.js +489 -78
  69. package/src/query/operators.js +620 -23
  70. package/src/query/runtime.js +5 -19
  71. package/src/query/types.js +213 -0
  72. package/src/segments.js +409 -64
  73. package/src/write.js +660 -0
  74. package/src/xquery/parse.js +37 -53
package/src/patch.js ADDED
@@ -0,0 +1,1052 @@
1
+ //#region JSON Patch (RFC 6902) + JSON Merge Patch (RFC 7396)
2
+ // JSON Patch: https://datatracker.ietf.org/doc/html/rfc6902
3
+ // JSON Merge Patch: https://datatracker.ietf.org/doc/html/rfc7396
4
+ //
5
+ // Both formats compile in the house two-stage style:
6
+ //
7
+ // 1. `compileJSONPatch` validates the patch document once (all `JP0xxx`
8
+ // errors, with a `docPath` into the *patch* document), pre-parses
9
+ // every `path`/`from` through the strict RFC 6901 parser and
10
+ // specializes one closure per operation. `compileMergePatch`
11
+ // pre-splits a merge patch into remove/set/merge plans.
12
+ // 2. Applying replays the closures against a copy-on-write state:
13
+ // the input document is never mutated, untouched subtrees are shared
14
+ // by reference with the result (the JSLT `share` discipline), and a
15
+ // failing operation aborts the whole application (RFC 6902
16
+ // section 5) for free because the input root was never touched.
17
+ //
18
+ // Copy-on-write: an application tracks the set of nodes it has already
19
+ // cloned ("owned"). The first write along a path shallow-clones the spine
20
+ // from the root down; later writes find the spine in the owned set and
21
+ // mutate the clones in place, so k operations touching one region cost
22
+ // one spine copy, not k.
23
+ //
24
+ // `createJSONPatch` and `createMergePatch` are the structural diffs;
25
+ // `applyJSONPatch(doc, createJSONPatch(a, b))` reproduces `b` from `a`.
26
+ //
27
+ // Error codes:
28
+ // JP0001 - the patch document or an operation has the wrong shape
29
+ // JP0002 - unknown or missing `op` member
30
+ // JP0003 - missing or invalid JSON Pointer (`path`/`from`)
31
+ // JP0004 - missing `value` member
32
+ // JP0005 - `from` is a proper prefix of `path` in a move
33
+ // JP2001 - the target location does not exist
34
+ // JP2002 - invalid array position
35
+ // JP2003 - the root of the document cannot be removed
36
+ // JP2004 - a `test` operation failed
37
+
38
+ import {
39
+ cloneJson,
40
+ equalsJson,
41
+ isJsonContainer,
42
+ isJsonObject,
43
+ setObjectMember,
44
+ stableStringify,
45
+ } from '@jarenjs/core/object';
46
+
47
+ import {
48
+ parseJSONPointer,
49
+ encodeJSONPointerSegment,
50
+ } from './pointer.js';
51
+
52
+ import { NOTHING, scanArrayIndex } from './segments.js';
53
+
54
+ import { CodedError } from '@jarenjs/core/errors';
55
+ import { CodedDocPathError } from './errors.js';
56
+
57
+ import {
58
+ makeState,
59
+ ownedRoot,
60
+ ownedChild,
61
+ readSteps,
62
+ } from './cow.js';
63
+
64
+ const hasOwn = Object.hasOwn;
65
+
66
+ //#region errors
67
+
68
+ /**
69
+ * Error thrown when a JSON Patch document is rejected at compile time
70
+ * (`JP0xxx` codes). `docPath` is an RFC 6901 JSON Pointer into the
71
+ * patch document (e.g. `/2/from`). A wrapped pointer syntax error is
72
+ * exposed through `cause`.
73
+ */
74
+ export class JsonPatchCompileError extends CodedDocPathError {
75
+ constructor(code, message, docPath, cause = undefined) {
76
+ super('JsonPatchCompileError', code, message, docPath, cause);
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Error thrown when applying a compiled JSON Patch fails (`JP2xxx`
82
+ * codes). `docPath` points at the failing operation in the patch
83
+ * document; `dataPath` is the operation's target location in the
84
+ * document being patched — both render, separately identifiable, per
85
+ * the `@jarenjs/core` coded contract (`at` = document, `in data` =
86
+ * data).
87
+ */
88
+ export class JsonPatchRuntimeError extends CodedError {
89
+ constructor(code, reason, docPath, dataPath) {
90
+ super('JsonPatchRuntimeError', code, reason, { docPath, dataPath });
91
+ }
92
+ }
93
+
94
+ //#endregion
95
+
96
+ //#region copy-on-write machinery
97
+ // The generic pieces (owned-set state, spine cloning, step reads) live
98
+ // in the package-internal cow.js, shared with the standalone write
99
+ // operations (write.js). This region keeps only what is specific to the
100
+ // patch engine: the walk that raises JsonPatchRuntimeError with both a
101
+ // patch docPath and a target dataPath.
102
+
103
+ function pathError(code, message, docPath, dataPath) {
104
+ return new JsonPatchRuntimeError(code, message, docPath, dataPath);
105
+ }
106
+
107
+ // Walk to the parent of the target location, cloning the spine on first
108
+ // touch. `t` is a compiled target; the walk covers segments [0, len-1).
109
+ function walkOwnedParent(state, t, docPath) {
110
+ const names = t.names;
111
+ const indexes = t.indexes;
112
+ const plen = t.len - 1;
113
+ let v = ownedRoot(state);
114
+ for (let i = 0; i < plen; i++) {
115
+ if (Array.isArray(v)) {
116
+ const idx = indexes[i];
117
+ if (idx < 0 || idx >= v.length)
118
+ throw pathError('JP2001', `the path '${t.pointer}' does not exist`, docPath, t.pointer);
119
+ v = ownedChild(state, v, v[idx], idx);
120
+ }
121
+ else if (typeof v === 'object' && v !== null) {
122
+ const name = names[i];
123
+ if (!hasOwn(v, name))
124
+ throw pathError('JP2001', `the path '${t.pointer}' does not exist`, docPath, t.pointer);
125
+ v = ownedChild(state, v, v[name], name);
126
+ }
127
+ else {
128
+ throw pathError('JP2001', `the path '${t.pointer}' does not exist`, docPath, t.pointer);
129
+ }
130
+ }
131
+ return v;
132
+ }
133
+
134
+ // Read the full target location without cloning anything.
135
+ function readTarget(root, t) {
136
+ return readSteps(root, t.names, t.indexes, t.len, NOTHING);
137
+ }
138
+
139
+ function containsOwned(v, owned) {
140
+ if (!isJsonContainer(v))
141
+ return false;
142
+ if (owned.has(v))
143
+ return true;
144
+ if (Array.isArray(v)) {
145
+ for (let i = 0; i < v.length; i++) {
146
+ if (containsOwned(v[i], owned))
147
+ return true;
148
+ }
149
+ return false;
150
+ }
151
+ for (const key in v) {
152
+ if (hasOwn(v, key) && containsOwned(v[key], owned))
153
+ return true;
154
+ }
155
+ return false;
156
+ }
157
+
158
+ // The value a `copy` inserts. Sharing the source reference is safe only
159
+ // while no clone of this application lives inside it: a later operation
160
+ // walking the second location would find an owned node and mutate both
161
+ // aliases. In-place mode owns everything, so it always deep-copies.
162
+ function copyForInsert(value, owned) {
163
+ if (!isJsonContainer(value))
164
+ return value;
165
+ if (owned === null)
166
+ return cloneJson(value);
167
+ if (owned.size !== 0 && containsOwned(value, owned))
168
+ return cloneJson(value);
169
+ return value;
170
+ }
171
+
172
+ //#endregion
173
+
174
+ //#region operation primitives
175
+
176
+ // Change tracking (`changes` option): one pointer per successful write,
177
+ // pushed by the operation primitives below. `state.changes` is `null`
178
+ // when tracking is off - a single monomorphic null check per write.
179
+ function recordChange(state, pointer) {
180
+ if (state.changes !== null)
181
+ state.changes.push(pointer);
182
+ }
183
+
184
+ // add semantics at a non-root target: array insert (with shift, `-`
185
+ // appends), object member set-or-replace (RFC 6902 section 4.1).
186
+ function insertAt(state, t, value, docPath) {
187
+ const parent = walkOwnedParent(state, t, docPath);
188
+ if (Array.isArray(parent)) {
189
+ const len = parent.length;
190
+ const idx = t.lastName === '-' ? len : t.lastIndex;
191
+ if (idx < 0 || idx > len)
192
+ throw pathError('JP2002', `invalid array position '${t.lastName}' in '${t.pointer}'`, docPath, t.pointer);
193
+ if (idx === len) {
194
+ parent.push(value);
195
+ // append shifts nothing: the new element's location is precise
196
+ recordChange(state, t.parentPointer + '/' + idx);
197
+ }
198
+ else {
199
+ parent.splice(idx, 0, value);
200
+ // insert shifts every later element: the whole array changed
201
+ recordChange(state, t.parentPointer);
202
+ }
203
+ return;
204
+ }
205
+ if (isJsonObject(parent)) {
206
+ setObjectMember(parent, t.lastName, value);
207
+ recordChange(state, t.pointer);
208
+ return;
209
+ }
210
+ throw pathError('JP2001', `the path '${t.pointer}' does not exist`, docPath, t.pointer);
211
+ }
212
+
213
+ // remove semantics at a non-root target; returns the removed value so
214
+ // `move` can re-insert it (RFC 6902 sections 4.2, 4.4).
215
+ function extractAt(state, t, docPath) {
216
+ const parent = walkOwnedParent(state, t, docPath);
217
+ if (Array.isArray(parent)) {
218
+ const idx = t.lastName === '-' ? -1 : t.lastIndex;
219
+ if (idx < 0)
220
+ throw pathError('JP2002', `invalid array position '${t.lastName}' in '${t.pointer}'`, docPath, t.pointer);
221
+ if (idx >= parent.length)
222
+ throw pathError('JP2001', `the path '${t.pointer}' does not exist`, docPath, t.pointer);
223
+ const value = parent[idx];
224
+ parent.splice(idx, 1);
225
+ // removal shifts every later element: the whole array changed
226
+ recordChange(state, t.parentPointer);
227
+ return value;
228
+ }
229
+ if (isJsonObject(parent)) {
230
+ const name = t.lastName;
231
+ if (!hasOwn(parent, name))
232
+ throw pathError('JP2001', `the path '${t.pointer}' does not exist`, docPath, t.pointer);
233
+ const value = parent[name];
234
+ delete parent[name];
235
+ recordChange(state, t.pointer);
236
+ return value;
237
+ }
238
+ throw pathError('JP2001', `the path '${t.pointer}' does not exist`, docPath, t.pointer);
239
+ }
240
+
241
+ // replace semantics at a non-root target: the location must already
242
+ // exist (RFC 6902 section 4.3).
243
+ function replaceAt(state, t, value, docPath) {
244
+ const parent = walkOwnedParent(state, t, docPath);
245
+ if (Array.isArray(parent)) {
246
+ const idx = t.lastName === '-' ? -1 : t.lastIndex;
247
+ if (idx < 0)
248
+ throw pathError('JP2002', `invalid array position '${t.lastName}' in '${t.pointer}'`, docPath, t.pointer);
249
+ if (idx >= parent.length)
250
+ throw pathError('JP2001', `the path '${t.pointer}' does not exist`, docPath, t.pointer);
251
+ parent[idx] = value;
252
+ recordChange(state, t.pointer);
253
+ return;
254
+ }
255
+ if (isJsonObject(parent)) {
256
+ const name = t.lastName;
257
+ if (!hasOwn(parent, name))
258
+ throw pathError('JP2001', `the path '${t.pointer}' does not exist`, docPath, t.pointer);
259
+ setObjectMember(parent, name, value);
260
+ recordChange(state, t.pointer);
261
+ return;
262
+ }
263
+ throw pathError('JP2001', `the path '${t.pointer}' does not exist`, docPath, t.pointer);
264
+ }
265
+
266
+ //#endregion
267
+
268
+ //#region patch compiler
269
+
270
+ /**
271
+ * A single RFC 6902 operation object.
272
+ * @typedef {{op: 'add'|'remove'|'replace'|'move'|'copy'|'test', path: string,
273
+ * value?: any, from?: string}} JsonPatchOperation
274
+ */
275
+
276
+ /**
277
+ * A compiled JSON Patch: applies the patch to a document and returns
278
+ * the patched document.
279
+ * @typedef {(doc: any) => any} JsonPatchApplier
280
+ */
281
+
282
+ /**
283
+ * A compiled JSON Patch with change tracking (`changes: true`): applies
284
+ * the patch and returns the patched document together with the changed
285
+ * locations (see `JsonPatchOptions`).
286
+ * @typedef {(doc: any) => { doc: any, changes: string[] }} JsonPatchChangesApplier
287
+ */
288
+
289
+ /**
290
+ * Options for `compileJSONPatch` / `applyJSONPatch`.
291
+ * @typedef {Object} JsonPatchOptions
292
+ * @property {boolean} [mutate] - Apply in place instead of copy-on-write.
293
+ * Faster, but the input document is modified and a failing operation
294
+ * leaves it partially patched (application is no longer atomic).
295
+ * @property {'share'|'fresh'} [values] - How operation values enter the
296
+ * result: `'share'` (default) inserts them by reference, so results of
297
+ * repeated applications share structure with the patch document and
298
+ * must be treated as immutable; `'fresh'` deep-copies per application.
299
+ * In-place mode always behaves as `'fresh'`.
300
+ * @property {boolean} [changes] - Track changed locations: the applier
301
+ * returns `{ doc, changes }` where `changes` is an array of JSON
302
+ * Pointers, one per successful write, in application order and not
303
+ * deduplicated. The reported pointer is chosen to be *sound for
304
+ * invalidation* — everything at or below it (plus the identity of its
305
+ * ancestors) may have changed, and nothing outside the reported set
306
+ * did: object writes, array replaces and array appends report the
307
+ * written location itself; array inserts and removes that shift later
308
+ * elements report the parent array's pointer; a root write reports
309
+ * `''`. `test` operations report nothing.
310
+ */
311
+
312
+ function compileError(code, message, docPath, cause) {
313
+ return new JsonPatchCompileError(code, message, docPath, cause);
314
+ }
315
+
316
+ // Parse and pre-compile a target pointer (`path` or `from`): decoded
317
+ // member names alongside pre-scanned array indexes (one token, two
318
+ // forms), plus the split-off last token for the mutating operations.
319
+ function parseTarget(op, index, member) {
320
+ const docPath = '/' + index + '/' + member;
321
+ if (!hasOwn(op, member))
322
+ throw compileError('JP0003', `the operation requires a '${member}' member`, docPath);
323
+ const pointer = op[member];
324
+ let names;
325
+ try {
326
+ names = parseJSONPointer(pointer);
327
+ }
328
+ catch (e) {
329
+ throw compileError('JP0003', `invalid JSON Pointer in '${member}'`, docPath, e);
330
+ }
331
+ const len = names.length;
332
+ const indexes = new Array(len);
333
+ for (let i = 0; i < len; i++)
334
+ indexes[i] = scanArrayIndex(names[i], 0, names[i].length);
335
+ return {
336
+ pointer,
337
+ names,
338
+ indexes,
339
+ len,
340
+ lastName: len === 0 ? '' : names[len - 1],
341
+ lastIndex: len === 0 ? -1 : indexes[len - 1],
342
+ // the parent location, for shift-style change reports (tokens never
343
+ // contain a raw '/', so the last separator bounds the last token)
344
+ parentPointer: len === 0 ? '' : pointer.slice(0, pointer.lastIndexOf('/')),
345
+ };
346
+ }
347
+
348
+ function parseValueGetter(op, index, fresh) {
349
+ if (!hasOwn(op, 'value'))
350
+ throw compileError('JP0004', "the operation requires a 'value' member", '/' + index);
351
+ const value = op.value;
352
+ if (!isJsonContainer(value))
353
+ return () => value;
354
+ if (fresh)
355
+ return () => cloneJson(value);
356
+ return () => value;
357
+ }
358
+
359
+ function isProperPrefix(a, b) {
360
+ const alen = a.length;
361
+ if (alen >= b.length)
362
+ return false;
363
+ for (let i = 0; i < alen; i++) {
364
+ if (a[i] !== b[i])
365
+ return false;
366
+ }
367
+ return true;
368
+ }
369
+
370
+ function readSource(state, from, docPath) {
371
+ const value = readTarget(state.root, from);
372
+ if (value === NOTHING)
373
+ throw pathError('JP2001', `the path '${from.pointer}' does not exist`, docPath, from.pointer);
374
+ return value;
375
+ }
376
+
377
+ // Compile one operation object into a `(state) => void` closure with
378
+ // every pointer parsed, every index scanned and every docPath string
379
+ // pre-bound.
380
+ function compileOperation(op, index, fresh) {
381
+ const docPath = '/' + index;
382
+ if (!isJsonObject(op))
383
+ throw compileError('JP0001', 'an operation must be an object', docPath);
384
+ if (!hasOwn(op, 'op') || typeof op.op !== 'string')
385
+ throw compileError('JP0002', "the operation requires a string 'op' member", docPath + '/op');
386
+ switch (op.op) {
387
+ case 'add': {
388
+ const t = parseTarget(op, index, 'path');
389
+ const getValue = parseValueGetter(op, index, fresh);
390
+ if (t.len === 0) {
391
+ return (state) => {
392
+ state.root = getValue();
393
+ recordChange(state, '');
394
+ };
395
+ }
396
+ return (state) => {
397
+ insertAt(state, t, getValue(), docPath);
398
+ };
399
+ }
400
+ case 'remove': {
401
+ const t = parseTarget(op, index, 'path');
402
+ if (t.len === 0) {
403
+ return () => {
404
+ throw pathError('JP2003', 'the root of the document cannot be removed', docPath, '');
405
+ };
406
+ }
407
+ return (state) => {
408
+ extractAt(state, t, docPath);
409
+ };
410
+ }
411
+ case 'replace': {
412
+ const t = parseTarget(op, index, 'path');
413
+ const getValue = parseValueGetter(op, index, fresh);
414
+ if (t.len === 0) {
415
+ return (state) => {
416
+ state.root = getValue();
417
+ recordChange(state, '');
418
+ };
419
+ }
420
+ return (state) => {
421
+ replaceAt(state, t, getValue(), docPath);
422
+ };
423
+ }
424
+ case 'move': {
425
+ const from = parseTarget(op, index, 'from');
426
+ const t = parseTarget(op, index, 'path');
427
+ if (isProperPrefix(from.names, t.names))
428
+ throw compileError('JP0005', "'from' may not be a proper prefix of 'path' in a move", docPath + '/from');
429
+ if (from.len === 0)
430
+ return () => { }; // '' to '' - moving the root onto itself
431
+ if (t.len === 0) {
432
+ return (state) => {
433
+ state.root = extractAt(state, from, docPath);
434
+ recordChange(state, '');
435
+ };
436
+ }
437
+ return (state) => {
438
+ insertAt(state, t, extractAt(state, from, docPath), docPath);
439
+ };
440
+ }
441
+ case 'copy': {
442
+ const from = parseTarget(op, index, 'from');
443
+ const t = parseTarget(op, index, 'path');
444
+ if (t.len === 0) {
445
+ return (state) => {
446
+ state.root = copyForInsert(readSource(state, from, docPath), state.owned);
447
+ recordChange(state, '');
448
+ };
449
+ }
450
+ return (state) => {
451
+ insertAt(state, t, copyForInsert(readSource(state, from, docPath), state.owned), docPath);
452
+ };
453
+ }
454
+ case 'test': {
455
+ const t = parseTarget(op, index, 'path');
456
+ if (!hasOwn(op, 'value'))
457
+ throw compileError('JP0004', "the operation requires a 'value' member", docPath);
458
+ const value = op.value;
459
+ return (state) => {
460
+ const actual = readTarget(state.root, t);
461
+ if (actual === NOTHING)
462
+ throw pathError('JP2004', `test failed: no value at '${t.pointer}'`, docPath, t.pointer);
463
+ if (!equalsJson(actual, value))
464
+ throw pathError('JP2004', `test failed at '${t.pointer}'`, docPath, t.pointer);
465
+ };
466
+ }
467
+ default:
468
+ throw compileError('JP0002', `unknown operation '${op.op}'`, docPath + '/op');
469
+ }
470
+ }
471
+
472
+ /**
473
+ * Compile a JSON Patch (RFC 6902) into a reusable applier.
474
+ *
475
+ * The patch document is validated once (`JsonPatchCompileError`,
476
+ * `JP0xxx`, with a `docPath` into the patch document); every pointer is
477
+ * pre-parsed and each operation becomes a specialized closure. Applying
478
+ * is copy-on-write: the input document is never mutated, untouched
479
+ * subtrees are shared with the result, and application is atomic - a
480
+ * failing operation (`JsonPatchRuntimeError`, `JP2xxx`) leaves nothing
481
+ * behind.
482
+ *
483
+ * With `changes: true` the applier is specialized at compile time to
484
+ * also report the changed locations: it returns `{ doc, changes }`,
485
+ * where `changes` holds one JSON Pointer per successful write with the
486
+ * invalidation-sound semantics documented on `JsonPatchOptions` — the
487
+ * primitive dirty-path consumers (view re-rendering, rule dependency
488
+ * memoization) build on.
489
+ *
490
+ * @param {JsonPatchOperation[]} patch - The RFC 6902 patch document
491
+ * @param {JsonPatchOptions} [options] - Application options
492
+ * @returns {JsonPatchApplier | JsonPatchChangesApplier} applier
493
+ * returning the patched document (or `{ doc, changes }` with the
494
+ * `changes` option)
495
+ * @throws {JsonPatchCompileError} When the patch document is invalid
496
+ * @example
497
+ * const apply = compileJSONPatch([
498
+ * { op: 'test', path: '/version', value: 5 },
499
+ * { op: 'replace', path: '/user/name', value: 'Bob' },
500
+ * { op: 'add', path: '/user/tags/-', value: 'admin' },
501
+ * ]);
502
+ * const next = apply(doc); // doc is untouched
503
+ * @example
504
+ * const applyTracked = compileJSONPatch(
505
+ * [{ op: 'replace', path: '/user/name', value: 'Bob' }],
506
+ * { changes: true });
507
+ * const { doc: next2, changes } = applyTracked(doc);
508
+ * // changes: ['/user/name']
509
+ */
510
+ export function compileJSONPatch(patch, options = undefined) {
511
+ let mutate = false;
512
+ let values = 'share';
513
+ let changes = false;
514
+ if (options !== undefined && options !== null) {
515
+ mutate = options.mutate === true;
516
+ if (options.values !== undefined) {
517
+ if (options.values !== 'share' && options.values !== 'fresh')
518
+ throw new TypeError(`compileJSONPatch: unknown 'values' option '${options.values}'`);
519
+ values = options.values;
520
+ }
521
+ changes = options.changes === true;
522
+ }
523
+ const fresh = mutate || values === 'fresh';
524
+ if (!Array.isArray(patch))
525
+ throw compileError('JP0001', 'a JSON Patch document must be an array of operations', '');
526
+ const plen = patch.length;
527
+ const ops = new Array(plen);
528
+ for (let i = 0; i < plen; i++)
529
+ ops[i] = compileOperation(patch[i], i, fresh);
530
+ if (changes) {
531
+ const owned = mutate ? null : undefined;
532
+ return function applyJsonPatchTracked(doc) {
533
+ const state = makeState(doc, owned === null ? null : new Set());
534
+ state.changes = [];
535
+ for (let i = 0; i < plen; i++)
536
+ ops[i](state);
537
+ return { doc: state.root, changes: state.changes };
538
+ };
539
+ }
540
+ if (mutate) {
541
+ return function applyJsonPatchInPlace(doc) {
542
+ const state = makeState(doc, null);
543
+ state.changes = null;
544
+ for (let i = 0; i < plen; i++)
545
+ ops[i](state);
546
+ return state.root;
547
+ };
548
+ }
549
+ return function applyJsonPatchCow(doc) {
550
+ const state = makeState(doc, new Set());
551
+ state.changes = null;
552
+ for (let i = 0; i < plen; i++)
553
+ ops[i](state);
554
+ return state.root;
555
+ };
556
+ }
557
+
558
+ /**
559
+ * Apply a JSON Patch (RFC 6902) to a document in one shot. Compiles the
560
+ * patch and applies it once; on hot paths prefer `compileJSONPatch` and
561
+ * reuse the applier.
562
+ *
563
+ * @param {any} doc - The document to patch (never mutated unless
564
+ * `options.mutate` is set)
565
+ * @param {JsonPatchOperation[]} patch - The RFC 6902 patch document
566
+ * @param {JsonPatchOptions} [options] - Application options
567
+ * @returns {any} The patched document, or `{ doc, changes }` when
568
+ * `options.changes` is set
569
+ * @throws {JsonPatchCompileError} When the patch document is invalid
570
+ * @throws {JsonPatchRuntimeError} When an operation fails to apply
571
+ */
572
+ export function applyJSONPatch(doc, patch, options = undefined) {
573
+ return compileJSONPatch(patch, options)(doc);
574
+ }
575
+
576
+ /**
577
+ * Returns true when `patch` is a structurally valid RFC 6902 patch
578
+ * document (an array of well-formed operation objects with valid
579
+ * pointers). Runtime applicability against a document is not checked.
580
+ * @param {any} patch - The candidate patch document
581
+ * @returns {boolean}
582
+ */
583
+ export function isValidJSONPatch(patch) {
584
+ try {
585
+ compileJSONPatch(patch);
586
+ return true;
587
+ }
588
+ catch (e) {
589
+ if (e instanceof JsonPatchCompileError)
590
+ return false;
591
+ /* c8 ignore next -- compile only throws JsonPatchCompileError */
592
+ throw e;
593
+ }
594
+ }
595
+
596
+ //#endregion
597
+
598
+ //#region structural diff (RFC 6902)
599
+
600
+ function appendPointer(path, key) {
601
+ return path + '/' + encodeJSONPointerSegment(key);
602
+ }
603
+
604
+ function diffObject(src, tgt, path, out, lcs) {
605
+ for (const key in src) {
606
+ if (!hasOwn(src, key))
607
+ continue;
608
+ if (!hasOwn(tgt, key))
609
+ out.push({ op: 'remove', path: appendPointer(path, key) });
610
+ else
611
+ diffValue(src[key], tgt[key], appendPointer(path, key), out, lcs);
612
+ }
613
+ for (const key in tgt) {
614
+ if (hasOwn(tgt, key) && !hasOwn(src, key))
615
+ out.push({ op: 'add', path: appendPointer(path, key), value: tgt[key] });
616
+ }
617
+ }
618
+
619
+ // Emit the ops for one gap between two aligned (deep-equal) elements:
620
+ // `sRun` source elements at src[si...] became `tRun` target elements at
621
+ // tgt[ti...]. Overlapping positions are diffed in place - an edited
622
+ // element stays one `replace` rather than a remove plus an add - and the
623
+ // length difference is appended or removed.
624
+ //
625
+ // `cur` is the position of src[si] in the document as the ops so far
626
+ // have left it: an `add` shifts everything after it right (so the next
627
+ // insert goes one further along), while repeated `remove`s all land on
628
+ // the same index (each one shifts the rest left onto it).
629
+ function diffArrayGap(src, si, sRun, tgt, ti, tRun, cur, path, out, lcs) {
630
+ const both = sRun < tRun ? sRun : tRun;
631
+ for (let k = 0; k < both; k++)
632
+ diffValue(src[si + k], tgt[ti + k], path + '/' + (cur + k), out, lcs);
633
+ if (tRun > both) {
634
+ for (let k = both; k < tRun; k++)
635
+ out.push({ op: 'add', path: path + '/' + (cur + k), value: tgt[ti + k] });
636
+ return cur + tRun;
637
+ }
638
+ const at = path + '/' + (cur + both);
639
+ for (let k = both; k < sRun; k++)
640
+ out.push({ op: 'remove', path: at });
641
+ return cur + both;
642
+ }
643
+
644
+ // Cell budget for the alignment table: beyond this the quadratic
645
+ // alignment costs more than the ops it saves, so the index-wise diff
646
+ // answers instead - still correct, just not minimal.
647
+ const ALIGN_MAX_CELLS = 1 << 20;
648
+
649
+ // Intern one element's stable serialization to an integer id, so the
650
+ // alignment's inner loop compares integers instead of walking two
651
+ // subtrees. The ids only ever act as a filter: different ids mean
652
+ // certainly different values, equal ids still have to pass the real
653
+ // comparison - so a collision costs one wasted comparison and can never
654
+ // align two unequal elements.
655
+ function internElement(ids, value) {
656
+ const key = stableStringify(value);
657
+ let id = ids.get(key);
658
+ if (id === undefined) {
659
+ id = ids.size;
660
+ ids.set(key, id);
661
+ }
662
+ return id;
663
+ }
664
+
665
+ // Align src[s0,s1) with tgt[t0,t1) and emit the ops for the alignment.
666
+ //
667
+ // The cost model is the patch itself: keeping a deep-equal pair is free,
668
+ // rewriting one position in place is one step, and so is inserting or
669
+ // deleting a single element. Minimizing that is edit distance WITH
670
+ // substitutions, not a longest common subsequence - LCS maximizes kept
671
+ // elements, which is a different thing and loses on a permutation, where
672
+ // it pays a delete plus an insert for what one rewrite covers.
673
+ //
674
+ // Because pairing every overlapping position is itself a valid
675
+ // alignment, the result never takes more steps than the index-wise diff.
676
+ //
677
+ // `cur` tracks where the current source element sits in the document as
678
+ // the ops emitted so far have left it: an insert shifts the rest right,
679
+ // a delete shifts it left onto the same index.
680
+ function diffArrayAligned(src, s0, s1, tgt, t0, t1, path, out, lcs) {
681
+ const m = s1 - s0;
682
+ const n = t1 - t0;
683
+ const ids = new Map();
684
+ const sk = new Int32Array(m);
685
+ const tk = new Int32Array(n);
686
+ for (let i = 0; i < m; i++)
687
+ sk[i] = internElement(ids, src[s0 + i]);
688
+ for (let j = 0; j < n; j++)
689
+ tk[j] = internElement(ids, tgt[t0 + j]);
690
+
691
+ // backward DP, so the alignment replays front to back - the order the
692
+ // ops have to be emitted in
693
+ const width = n + 1;
694
+ const dp = new Uint32Array((m + 1) * width);
695
+ const last = m * width;
696
+ for (let j = n - 1; j >= 0; j--)
697
+ dp[last + j] = n - j;
698
+ for (let i = m - 1; i >= 0; i--) {
699
+ const row = i * width;
700
+ const next = row + width;
701
+ dp[row + n] = m - i;
702
+ const key = sk[i];
703
+ for (let j = n - 1; j >= 0; j--) {
704
+ const same = key === tk[j] && equalsJson(src[s0 + i], tgt[t0 + j]);
705
+ let best = dp[next + j + 1] + (same ? 0 : 1);
706
+ const del = dp[next + j] + 1;
707
+ if (del < best)
708
+ best = del;
709
+ const ins = dp[row + j + 1] + 1;
710
+ if (ins < best)
711
+ best = ins;
712
+ dp[row + j] = best;
713
+ }
714
+ }
715
+
716
+ let i = 0;
717
+ let j = 0;
718
+ let cur = s0;
719
+ while (i < m && j < n) {
720
+ const row = i * width;
721
+ const next = row + width;
722
+ const same = sk[i] === tk[j] && equalsJson(src[s0 + i], tgt[t0 + j]);
723
+ const best = dp[row + j];
724
+ if (best === dp[next + j + 1] + (same ? 0 : 1)) {
725
+ if (!same)
726
+ diffValue(src[s0 + i], tgt[t0 + j], path + '/' + cur, out, lcs);
727
+ i++;
728
+ j++;
729
+ cur++;
730
+ }
731
+ else if (best === dp[next + j] + 1) {
732
+ out.push({ op: 'remove', path: path + '/' + cur });
733
+ i++;
734
+ }
735
+ else {
736
+ out.push({ op: 'add', path: path + '/' + cur, value: tgt[t0 + j] });
737
+ j++;
738
+ cur++;
739
+ }
740
+ }
741
+ for (; i < m; i++)
742
+ out.push({ op: 'remove', path: path + '/' + cur });
743
+ for (; j < n; j++, cur++)
744
+ out.push({ op: 'add', path: path + '/' + cur, value: tgt[t0 + j] });
745
+ }
746
+
747
+ // Array diff. Both modes first trim the deep-equal common prefix and
748
+ // suffix, which alone makes in-place edits and head/tail insertions
749
+ // minimal and bounds the work to the changed middle.
750
+ //
751
+ // What differs is the middle. The default pairs it up index-wise, which
752
+ // is linear but turns a mid-array insertion into a run of per-index
753
+ // replaces (correct, not minimal). `arrayDiff: 'minimal'` aligns the
754
+ // middle instead, so an insertion or deletion is emitted as one op and
755
+ // only genuinely changed positions are rewritten - at O(m*n) time and
756
+ // space in the size of that middle.
757
+ function diffArray(src, tgt, path, out, lcs) {
758
+ const slen = src.length;
759
+ const tlen = tgt.length;
760
+ const minLen = slen < tlen ? slen : tlen;
761
+ let start = 0;
762
+ while (start < minLen && equalsJson(src[start], tgt[start]))
763
+ start++;
764
+ let sEnd = slen;
765
+ let tEnd = tlen;
766
+ while (sEnd > start && tEnd > start && equalsJson(src[sEnd - 1], tgt[tEnd - 1])) {
767
+ sEnd--;
768
+ tEnd--;
769
+ }
770
+ const sMid = sEnd - start;
771
+ const tMid = tEnd - start;
772
+ if (sMid === 0 && tMid === 0)
773
+ return;
774
+
775
+ // one run of pure inserts or pure deletes needs no alignment, and the
776
+ // budget keeps a large middle from turning quadratic
777
+ if (!lcs || sMid === 0 || tMid === 0 || (sMid + 1) * (tMid + 1) > ALIGN_MAX_CELLS) {
778
+ diffArrayGap(src, start, sMid, tgt, start, tMid, start, path, out, lcs);
779
+ return;
780
+ }
781
+ diffArrayAligned(src, start, sEnd, tgt, start, tEnd, path, out, lcs);
782
+ }
783
+
784
+ function diffValue(src, tgt, path, out, lcs) {
785
+ if (src === tgt)
786
+ return;
787
+ const sArr = Array.isArray(src);
788
+ const tArr = Array.isArray(tgt);
789
+ if (sArr && tArr) {
790
+ diffArray(src, tgt, path, out, lcs);
791
+ return;
792
+ }
793
+ if (!sArr && !tArr && isJsonContainer(src) && isJsonContainer(tgt)) {
794
+ diffObject(src, tgt, path, out, lcs);
795
+ return;
796
+ }
797
+ if (!equalsJson(src, tgt))
798
+ out.push({ op: 'replace', path, value: tgt });
799
+ }
800
+
801
+ /**
802
+ * Compute a JSON Patch (RFC 6902) that transforms `source` into
803
+ * `target`: `applyJSONPatch(source, createJSONPatch(source, target))`
804
+ * is deep-equal to `target`.
805
+ *
806
+ * Objects diff member-wise; arrays trim the common prefix/suffix and
807
+ * diff the middle index-wise, so in-place edits and head/tail
808
+ * insertions produce minimal patches while a mid-array insertion falls
809
+ * back to correct (but larger) per-index replaces. Emitted `value`
810
+ * members share references with `target`.
811
+ *
812
+ * `arrayDiff: 'minimal'` aligns the changed middle instead, so a
813
+ * mid-array insertion or deletion is emitted as one `add`/`remove` —
814
+ * the smallest patch, which is what matters when patches go over the
815
+ * wire. The alignment minimizes the patch itself (edit distance with
816
+ * substitutions, not a longest common subsequence: LCS maximizes kept
817
+ * elements, which costs a delete plus an insert on a permutation where
818
+ * one rewrite would do), so it never takes more alignment steps than
819
+ * the index-wise pairing.
820
+ *
821
+ * It is opt-in because it costs O(m*n) time and space in the length of
822
+ * that middle, against the default's linear pass; above a fixed cell
823
+ * budget a single array falls back to the index-wise diff, so the mode
824
+ * never turns a large diff quadratic. Both modes produce patches that
825
+ * reproduce `target` exactly.
826
+ *
827
+ * @param {any} source - The original document
828
+ * @param {any} target - The desired document
829
+ * @param {{ arrayDiff?: 'index' | 'minimal' }} [options] - `arrayDiff`
830
+ * selects the array strategy: `'index'` (default) or `'minimal'`
831
+ * @returns {JsonPatchOperation[]} The patch document (empty when equal)
832
+ * @throws {TypeError} When `arrayDiff` is not a known mode
833
+ * @example
834
+ * createJSONPatch({ a: 1, b: 2 }, { a: 1, b: 3, c: 4 });
835
+ * // [{ op: 'replace', path: '/b', value: 3 },
836
+ * // { op: 'add', path: '/c', value: 4 }]
837
+ * @example
838
+ * const before = [{ id: 1 }, { id: 2 }, { id: 3, n: 0 }];
839
+ * const after = [{ id: 1 }, { id: 9 }, { id: 2 }, { id: 3, n: 1 }];
840
+ * createJSONPatch(before, after, { arrayDiff: 'minimal' });
841
+ * // [{ op: 'add', path: '/1', value: { id: 9 } },
842
+ * // { op: 'replace', path: '/3/n', value: 1 }]
843
+ */
844
+ export function createJSONPatch(source, target, options = undefined) {
845
+ const mode = options == null || options.arrayDiff === undefined
846
+ ? 'index'
847
+ : options.arrayDiff;
848
+ if (mode !== 'index' && mode !== 'minimal')
849
+ throw new TypeError(`unknown 'arrayDiff' option '${mode}'`);
850
+ const out = [];
851
+ diffValue(source, target, '', out, mode === 'minimal');
852
+ return out;
853
+ }
854
+
855
+ //#endregion
856
+
857
+ //#region JSON Merge Patch (RFC 7396)
858
+
859
+ /**
860
+ * A compiled JSON Merge Patch: applies the merge patch to a document
861
+ * and returns the patched document.
862
+ * @typedef {(doc: any) => any} JsonMergePatchApplier
863
+ */
864
+
865
+ // Compile one merge-patch object level into a plan closure. The patch
866
+ // splits once into removes (null members), scalar/array sets and
867
+ // nested-object merges; applying is identity-preserving - a level that
868
+ // changes nothing returns the target reference unchanged.
869
+ function compileMergeObjectNode(patch) {
870
+ const removes = [];
871
+ const setKeys = [];
872
+ const setVals = [];
873
+ const mergeKeys = [];
874
+ const mergeFns = [];
875
+ for (const key in patch) {
876
+ if (!hasOwn(patch, key))
877
+ continue;
878
+ const v = patch[key];
879
+ if (v === null) {
880
+ removes.push(key);
881
+ }
882
+ else if (isJsonObject(v)) {
883
+ mergeKeys.push(key);
884
+ mergeFns.push(compileMergeObjectNode(v));
885
+ }
886
+ else {
887
+ setKeys.push(key);
888
+ setVals.push(v);
889
+ }
890
+ }
891
+ const removeSet = removes.length > 0 ? new Set(removes) : null;
892
+ const rlen = removes.length;
893
+ const slen = setKeys.length;
894
+ const mlen = mergeKeys.length;
895
+ return function mergeNode(target) {
896
+ if (!isJsonObject(target)) {
897
+ // RFC 7396: a non-object target is replaced by merging into {}
898
+ const out = {};
899
+ for (let i = 0; i < slen; i++)
900
+ setObjectMember(out, setKeys[i], setVals[i]);
901
+ for (let i = 0; i < mlen; i++)
902
+ setObjectMember(out, mergeKeys[i], mergeFns[i](undefined));
903
+ return out;
904
+ }
905
+ let changed = false;
906
+ for (let i = 0; i < rlen; i++) {
907
+ if (hasOwn(target, removes[i])) {
908
+ changed = true;
909
+ break;
910
+ }
911
+ }
912
+ if (!changed) {
913
+ for (let i = 0; i < slen; i++) {
914
+ const key = setKeys[i];
915
+ if (!hasOwn(target, key) || target[key] !== setVals[i]) {
916
+ changed = true;
917
+ break;
918
+ }
919
+ }
920
+ }
921
+ const mres = mlen > 0 ? new Array(mlen) : null;
922
+ for (let i = 0; i < mlen; i++) {
923
+ const key = mergeKeys[i];
924
+ const prev = hasOwn(target, key) ? target[key] : undefined;
925
+ const sub = mergeFns[i](prev);
926
+ mres[i] = sub;
927
+ if (sub !== prev)
928
+ changed = true;
929
+ }
930
+ if (!changed)
931
+ return target;
932
+ const out = {};
933
+ for (const key in target) {
934
+ if (!hasOwn(target, key))
935
+ continue;
936
+ if (removeSet !== null && removeSet.has(key))
937
+ continue;
938
+ setObjectMember(out, key, target[key]);
939
+ }
940
+ for (let i = 0; i < slen; i++)
941
+ setObjectMember(out, setKeys[i], setVals[i]);
942
+ for (let i = 0; i < mlen; i++)
943
+ setObjectMember(out, mergeKeys[i], mres[i]);
944
+ return out;
945
+ };
946
+ }
947
+
948
+ /**
949
+ * Compile a JSON Merge Patch (RFC 7396) into a reusable applier.
950
+ *
951
+ * The patch pre-splits once into remove/set/merge plans per level.
952
+ * Applying is copy-on-write and identity-preserving: unchanged subtrees
953
+ * (and a wholly unchanged document) come back by reference, set values
954
+ * are shared with the patch document, and the input is never mutated.
955
+ *
956
+ * @param {any} patch - The merge patch (any JSON value; a non-object
957
+ * replaces the document wholesale)
958
+ * @returns {JsonMergePatchApplier} applier returning the patched document
959
+ * @example
960
+ * const apply = compileMergePatch({ age: 31, temp: null });
961
+ * apply({ name: 'Alice', age: 30, temp: 'x' });
962
+ * // { name: 'Alice', age: 31 }
963
+ */
964
+ export function compileMergePatch(patch) {
965
+ if (!isJsonObject(patch)) {
966
+ return function applyMergeReplace() {
967
+ return patch;
968
+ };
969
+ }
970
+ const node = compileMergeObjectNode(patch);
971
+ return function applyMerge(doc) {
972
+ return node(doc);
973
+ };
974
+ }
975
+
976
+ /**
977
+ * Apply a JSON Merge Patch (RFC 7396) to a document in one shot. On hot
978
+ * paths prefer `compileMergePatch` and reuse the applier.
979
+ *
980
+ * @param {any} doc - The document to patch (never mutated)
981
+ * @param {any} patch - The merge patch
982
+ * @returns {any} The patched document
983
+ */
984
+ export function applyMergePatch(doc, patch) {
985
+ return compileMergePatch(patch)(doc);
986
+ }
987
+
988
+ function isEmptyObjectShallow(obj) {
989
+ for (const key in obj) {
990
+ if (hasOwn(obj, key))
991
+ return false;
992
+ }
993
+ return true;
994
+ }
995
+
996
+ function diffMergeObjects(src, tgt) {
997
+ const patch = {};
998
+ for (const key in src) {
999
+ if (hasOwn(src, key) && !hasOwn(tgt, key))
1000
+ setObjectMember(patch, key, null);
1001
+ }
1002
+ for (const key in tgt) {
1003
+ if (!hasOwn(tgt, key))
1004
+ continue;
1005
+ const tv = tgt[key];
1006
+ if (!hasOwn(src, key)) {
1007
+ // an added member with value null is unrepresentable (null means
1008
+ // remove); either way the key ends up absent after applying
1009
+ if (tv !== null)
1010
+ setObjectMember(patch, key, tv);
1011
+ continue;
1012
+ }
1013
+ const sv = src[key];
1014
+ if (sv === tv)
1015
+ continue;
1016
+ if (isJsonObject(sv) && isJsonObject(tv)) {
1017
+ const sub = diffMergeObjects(sv, tv);
1018
+ if (!isEmptyObjectShallow(sub))
1019
+ setObjectMember(patch, key, sub);
1020
+ }
1021
+ else if (!equalsJson(sv, tv)) {
1022
+ setObjectMember(patch, key, tv);
1023
+ }
1024
+ }
1025
+ return patch;
1026
+ }
1027
+
1028
+ /**
1029
+ * Compute a JSON Merge Patch (RFC 7396) that transforms `source` into
1030
+ * `target`. Removed members become `null`, nested objects diff
1031
+ * recursively, and arrays (or any kind change) replace wholesale.
1032
+ *
1033
+ * RFC 7396 cannot represent a member whose target value is `null`:
1034
+ * the diff emits `null` (a removal), so applying yields an absent
1035
+ * member instead. Emitted values share references with `target`.
1036
+ *
1037
+ * @param {any} source - The original document
1038
+ * @param {any} target - The desired document
1039
+ * @returns {any} The merge patch (`{}` when nothing changed)
1040
+ * @example
1041
+ * createMergePatch({ a: 'b', c: 1 }, { a: 'x' });
1042
+ * // { a: 'x', c: null }
1043
+ */
1044
+ export function createMergePatch(source, target) {
1045
+ if (!isJsonObject(source) || !isJsonObject(target))
1046
+ return target;
1047
+ return diffMergeObjects(source, target);
1048
+ }
1049
+
1050
+ //#endregion
1051
+
1052
+ //#endregion