@verdant-web/common 2.7.3 → 2.9.0

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 (52) hide show
  1. package/dist/esm/authz.js.map +1 -1
  2. package/dist/esm/baseline.d.ts +2 -1
  3. package/dist/esm/diffing.d.ts +40 -0
  4. package/dist/esm/diffing.js +317 -0
  5. package/dist/esm/diffing.js.map +1 -0
  6. package/dist/esm/diffing.test.d.ts +1 -0
  7. package/dist/esm/diffing.test.js +1680 -0
  8. package/dist/esm/diffing.test.js.map +1 -0
  9. package/dist/esm/index.d.ts +20 -19
  10. package/dist/esm/index.js +17 -16
  11. package/dist/esm/index.js.map +1 -1
  12. package/dist/esm/oids.d.ts +5 -0
  13. package/dist/esm/oids.js +23 -1
  14. package/dist/esm/oids.js.map +1 -1
  15. package/dist/esm/operation.d.ts +11 -24
  16. package/dist/esm/operation.js +32 -270
  17. package/dist/esm/operation.js.map +1 -1
  18. package/dist/esm/operation.test.js +122 -592
  19. package/dist/esm/operation.test.js.map +1 -1
  20. package/dist/esm/patch.d.ts +19 -15
  21. package/dist/esm/patch.js +79 -18
  22. package/dist/esm/patch.js.map +1 -1
  23. package/dist/esm/schema/validation.d.ts +8 -1
  24. package/dist/esm/schema/validation.js +133 -63
  25. package/dist/esm/schema/validation.js.map +1 -1
  26. package/dist/esm/schema/validation.test.d.ts +1 -0
  27. package/dist/esm/schema/validation.test.js +60 -0
  28. package/dist/esm/schema/validation.test.js.map +1 -0
  29. package/dist/esm/timestamp.d.ts +1 -0
  30. package/dist/esm/timestamp.js +3 -0
  31. package/dist/esm/timestamp.js.map +1 -1
  32. package/dist/esm/undo.d.ts +2 -1
  33. package/dist/esm/undo.js +53 -146
  34. package/dist/esm/undo.js.map +1 -1
  35. package/dist/esm/undo.test.d.ts +1 -0
  36. package/dist/esm/undo.test.js +81 -0
  37. package/dist/esm/undo.test.js.map +1 -0
  38. package/package.json +1 -1
  39. package/src/authz.ts +2 -0
  40. package/src/baseline.ts +3 -1
  41. package/src/diffing.test.ts +1803 -0
  42. package/src/diffing.ts +402 -0
  43. package/src/index.ts +21 -20
  44. package/src/oids.ts +23 -1
  45. package/src/operation.test.ts +148 -623
  46. package/src/operation.ts +45 -378
  47. package/src/patch.ts +146 -14
  48. package/src/schema/validation.test.ts +66 -0
  49. package/src/schema/validation.ts +156 -73
  50. package/src/timestamp.ts +4 -1
  51. package/src/undo.test.ts +100 -0
  52. package/src/undo.ts +83 -144
@@ -0,0 +1,1680 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { diffToPatches } from './diffing.js';
3
+ import { createFileRef } from './files.js';
4
+ import { assignOid, assignOidsToAllSubObjects, getOid, } from './oids.js';
5
+ import { applyOperations, deconstructSnapshotToRefs, groupPatchesByOid, substituteRefsWithObjects, } from './operation.js';
6
+ import { cloneDeep } from './utils.js';
7
+ function createClock(init = 0) {
8
+ let i = init;
9
+ const clock = () => (i++).toString();
10
+ Object.defineProperty(clock, 'current', {
11
+ get() {
12
+ return i;
13
+ },
14
+ });
15
+ return clock;
16
+ }
17
+ function expectDiffProducesResult(from, patches, to) {
18
+ // all required sub-objects should either be present on the from object,
19
+ // or represented by initialize operations in the patches. using these we can
20
+ // reconstruct the final representation.
21
+ const refTargets = new Map();
22
+ const fromCopy = cloneDeep(from); // retains OIDs
23
+ deconstructSnapshotToRefs(fromCopy, refTargets);
24
+ for (const patch of patches) {
25
+ if (patch.data.op === 'initialize') {
26
+ refTargets.set(patch.oid, patch.data.value);
27
+ }
28
+ }
29
+ const fromOid = getOid(from);
30
+ const fromRoot = refTargets.get(fromOid);
31
+ expect(fromRoot).toBeDefined();
32
+ const groupedPatches = groupPatchesByOid(patches);
33
+ for (const [oid, ops] of Object.entries(groupedPatches)) {
34
+ const target = refTargets.get(oid);
35
+ expect(target).toBeDefined();
36
+ refTargets.set(oid, applyOperations(target, ops));
37
+ }
38
+ // reconstruct the applied final object
39
+ substituteRefsWithObjects(fromRoot, refTargets);
40
+ expect(fromRoot).toEqual(to);
41
+ }
42
+ describe('generating diff operations', () => {
43
+ describe('on flat objects', () => {
44
+ it('generates and applies set and remove operations', () => {
45
+ const from = { foo: 'bar', baz: 'qux', zing: 1 };
46
+ assignOid(from, 'test/a');
47
+ const to = { foo: 'bar', baz: 'pop' };
48
+ assignOid(to, 'test/a');
49
+ const ops = diffToPatches(from, to, createClock());
50
+ expect(ops).toMatchInlineSnapshot(`
51
+ [
52
+ {
53
+ "authz": undefined,
54
+ "data": {
55
+ "name": "baz",
56
+ "op": "set",
57
+ "value": "pop",
58
+ },
59
+ "oid": "test/a",
60
+ "timestamp": "0",
61
+ },
62
+ {
63
+ "authz": undefined,
64
+ "data": {
65
+ "name": "zing",
66
+ "op": "remove",
67
+ },
68
+ "oid": "test/a",
69
+ "timestamp": "1",
70
+ },
71
+ ]
72
+ `);
73
+ expectDiffProducesResult(from, ops, to);
74
+ });
75
+ it('handles null values properly', () => {
76
+ const from = { foo: 'bar', baz: null };
77
+ assignOid(from, 'test/a');
78
+ const to = { foo: null, baz: null };
79
+ assignOid(to, 'test/a');
80
+ const ops = diffToPatches(from, to, createClock());
81
+ expect(ops).toEqual([
82
+ {
83
+ authz: undefined,
84
+ data: {
85
+ name: 'foo',
86
+ op: 'set',
87
+ value: null,
88
+ },
89
+ oid: 'test/a',
90
+ timestamp: '0',
91
+ },
92
+ ]);
93
+ expectDiffProducesResult(from, ops, to);
94
+ });
95
+ });
96
+ describe('on nested objects', () => {
97
+ it('replaces whole nested objects', () => {
98
+ const from = assignOid({
99
+ foo: 'bar',
100
+ baz: assignOid({ qux: 'corge' }, 'test/a:0'),
101
+ }, 'test/a');
102
+ const to = assignOid({
103
+ foo: 'bar',
104
+ // NOTE: this object is assigned an OID. it's even one related to the
105
+ // from object. but we still discard it. adding references to other
106
+ // already-known objects to a different entity is not supported due
107
+ // to the risk of having the same identity in multiple places
108
+ baz: assignOid({ qux: 'grault' }, 'test/a:1'),
109
+ }, 'test/a');
110
+ const patches = diffToPatches(from, to, createClock(), createClock(2));
111
+ expect(patches).toMatchInlineSnapshot(`
112
+ [
113
+ {
114
+ "data": {
115
+ "op": "initialize",
116
+ "value": {
117
+ "qux": "grault",
118
+ },
119
+ },
120
+ "oid": "test/a:2",
121
+ "timestamp": "0",
122
+ },
123
+ {
124
+ "authz": undefined,
125
+ "data": {
126
+ "name": "baz",
127
+ "op": "set",
128
+ "value": {
129
+ "@@type": "ref",
130
+ "id": "test/a:2",
131
+ },
132
+ },
133
+ "oid": "test/a",
134
+ "timestamp": "1",
135
+ },
136
+ {
137
+ "authz": undefined,
138
+ "data": {
139
+ "op": "delete",
140
+ },
141
+ "oid": "test/a:0",
142
+ "timestamp": "2",
143
+ },
144
+ ]
145
+ `);
146
+ expectDiffProducesResult(from, patches, to);
147
+ });
148
+ it('replaces whole nested objects with different ids even if fields are the same', () => {
149
+ const from = {
150
+ foo: 'bar',
151
+ baz: { qux: { cat: 'corge' } },
152
+ };
153
+ assignOid(from, 'test/a');
154
+ assignOid(from.baz, 'test/a:0');
155
+ assignOid(from.baz.qux, 'test/a:1');
156
+ const to = {
157
+ foo: 'bar',
158
+ baz: { qux: 'corge' },
159
+ };
160
+ assignOid(to, 'test/a');
161
+ assignOid(to.baz, 'test/a:2');
162
+ const patches = diffToPatches(from, to, createClock(), createClock(3));
163
+ expect(patches).toMatchInlineSnapshot(`
164
+ [
165
+ {
166
+ "data": {
167
+ "op": "initialize",
168
+ "value": {
169
+ "qux": "corge",
170
+ },
171
+ },
172
+ "oid": "test/a:3",
173
+ "timestamp": "0",
174
+ },
175
+ {
176
+ "authz": undefined,
177
+ "data": {
178
+ "name": "baz",
179
+ "op": "set",
180
+ "value": {
181
+ "@@type": "ref",
182
+ "id": "test/a:3",
183
+ },
184
+ },
185
+ "oid": "test/a",
186
+ "timestamp": "1",
187
+ },
188
+ {
189
+ "authz": undefined,
190
+ "data": {
191
+ "op": "delete",
192
+ },
193
+ "oid": "test/a:0",
194
+ "timestamp": "2",
195
+ },
196
+ {
197
+ "authz": undefined,
198
+ "data": {
199
+ "op": "delete",
200
+ },
201
+ "oid": "test/a:1",
202
+ "timestamp": "3",
203
+ },
204
+ ]
205
+ `);
206
+ expectDiffProducesResult(from, patches, to);
207
+ });
208
+ it('does not replace objects with the same identity and same fields', () => {
209
+ const from = {
210
+ foo: 'bar',
211
+ baz: { qux: 'corge' },
212
+ };
213
+ assignOid(from, 'test/a');
214
+ assignOid(from.baz, 'test/a:0');
215
+ const to = {
216
+ foo: 'bar',
217
+ baz: { qux: 'corge' },
218
+ };
219
+ assignOid(to, 'test/a');
220
+ assignOid(to.baz, 'test/a:0');
221
+ const patches = diffToPatches(from, to, createClock());
222
+ expect(patches).toEqual([]);
223
+ });
224
+ it('patches sub-objects with same identity', () => {
225
+ const from = {
226
+ foo: 'bar',
227
+ baz: { qux: 'corge' },
228
+ };
229
+ assignOid(from, 'test/a');
230
+ assignOid(from.baz, 'test/a:0');
231
+ const to = {
232
+ foo: 'bar',
233
+ baz: { qux: 'fig' },
234
+ };
235
+ assignOid(to, 'test/a');
236
+ assignOid(to.baz, 'test/a:0');
237
+ const patches = diffToPatches(from, to, createClock());
238
+ expect(patches).toMatchInlineSnapshot(`
239
+ [
240
+ {
241
+ "authz": undefined,
242
+ "data": {
243
+ "name": "qux",
244
+ "op": "set",
245
+ "value": "fig",
246
+ },
247
+ "oid": "test/a:0",
248
+ "timestamp": "0",
249
+ },
250
+ ]
251
+ `);
252
+ expectDiffProducesResult(from, patches, to);
253
+ });
254
+ it('retains keys which are undefined in new object if merge is set', () => {
255
+ const from = {
256
+ foo: 'bar',
257
+ baz: { qux: 'corge' },
258
+ };
259
+ assignOid(from, 'test/a');
260
+ assignOid(from.baz, 'test/a:0');
261
+ const to = {
262
+ foo: 'bip',
263
+ };
264
+ assignOid(to, 'test/a');
265
+ const patches = diffToPatches(from, to, createClock(), undefined, [], {
266
+ merge: true,
267
+ mergeUnknownObjects: true,
268
+ });
269
+ expect(patches).toMatchInlineSnapshot(`
270
+ [
271
+ {
272
+ "authz": undefined,
273
+ "data": {
274
+ "name": "foo",
275
+ "op": "set",
276
+ "value": "bip",
277
+ },
278
+ "oid": "test/a",
279
+ "timestamp": "0",
280
+ },
281
+ ]
282
+ `);
283
+ expectDiffProducesResult(from, patches, Object.assign(Object.assign({}, from), to));
284
+ });
285
+ it('deletes old complex objects when theyre replaced by primitives', () => {
286
+ const from = assignOid({
287
+ thing: { qux: { foo: 'bar' } },
288
+ }, 'test/a');
289
+ assignOidsToAllSubObjects(from, createClock());
290
+ const to = assignOid({
291
+ thing: 'foo',
292
+ }, 'test/a');
293
+ const patches = diffToPatches(from, to, createClock());
294
+ expect(patches).toMatchInlineSnapshot(`
295
+ [
296
+ {
297
+ "authz": undefined,
298
+ "data": {
299
+ "name": "thing",
300
+ "op": "set",
301
+ "value": "foo",
302
+ },
303
+ "oid": "test/a",
304
+ "timestamp": "0",
305
+ },
306
+ {
307
+ "authz": undefined,
308
+ "data": {
309
+ "op": "delete",
310
+ },
311
+ "oid": "test/a:0",
312
+ "timestamp": "1",
313
+ },
314
+ {
315
+ "authz": undefined,
316
+ "data": {
317
+ "op": "delete",
318
+ },
319
+ "oid": "test/a:1",
320
+ "timestamp": "2",
321
+ },
322
+ ]
323
+ `);
324
+ expectDiffProducesResult(from, patches, to);
325
+ });
326
+ });
327
+ describe('on lists of primitives', () => {
328
+ it('pushes new items', () => {
329
+ const from = assignOid([1, 2], 'test/a');
330
+ const to = assignOid([1, 2, 3], 'test/a');
331
+ const patches = diffToPatches(from, to, createClock());
332
+ expect(patches).toEqual([
333
+ {
334
+ data: {
335
+ op: 'list-push',
336
+ value: 3,
337
+ },
338
+ oid: 'test/a',
339
+ timestamp: '0',
340
+ },
341
+ ]);
342
+ expectDiffProducesResult(from, patches, to);
343
+ });
344
+ it('inserts items when remaining list is equal', () => {
345
+ const from = assignOid([1, 2, 4, 5], 'test/a');
346
+ const to = assignOid([1, 2, 3, 4, 5], 'test/a');
347
+ const patches = diffToPatches(from, to, createClock());
348
+ expect(patches).toEqual([
349
+ {
350
+ authz: undefined,
351
+ data: {
352
+ index: 2,
353
+ op: 'list-insert',
354
+ value: 3,
355
+ },
356
+ oid: 'test/a',
357
+ timestamp: '0',
358
+ },
359
+ ]);
360
+ expectDiffProducesResult(from, patches, to);
361
+ });
362
+ it('does not insert items when remaining list is unequal', () => {
363
+ const from = assignOid([1, 2, 4, 5, 6, 7], 'test/a');
364
+ const to = assignOid([1, 2, 3, 8, 9, 10], 'test/a');
365
+ const patches = diffToPatches(from, to, createClock());
366
+ expect(patches).toEqual([
367
+ {
368
+ authz: undefined,
369
+ data: {
370
+ op: 'list-set',
371
+ value: 3,
372
+ index: 2,
373
+ },
374
+ oid: 'test/a',
375
+ timestamp: '0',
376
+ },
377
+ {
378
+ authz: undefined,
379
+ data: {
380
+ op: 'list-set',
381
+ value: 8,
382
+ index: 3,
383
+ },
384
+ oid: 'test/a',
385
+ timestamp: '1',
386
+ },
387
+ {
388
+ authz: undefined,
389
+ data: {
390
+ op: 'list-set',
391
+ index: 4,
392
+ value: 9,
393
+ },
394
+ oid: 'test/a',
395
+ timestamp: '2',
396
+ },
397
+ {
398
+ authz: undefined,
399
+ data: {
400
+ op: 'list-set',
401
+ index: 5,
402
+ value: 10,
403
+ },
404
+ oid: 'test/a',
405
+ timestamp: '3',
406
+ },
407
+ ]);
408
+ expectDiffProducesResult(from, patches, to);
409
+ });
410
+ it('inserts items at the end of the new list if old list had more unmatched items at the end', () => {
411
+ const from = assignOid([1, 2, 4, 5, 6, 7], 'test/a');
412
+ const to = assignOid([1, 2, 3, 5, 8], 'test/a');
413
+ const patches = diffToPatches(from, to, createClock());
414
+ expect(patches).toMatchInlineSnapshot(`
415
+ [
416
+ {
417
+ "authz": undefined,
418
+ "data": {
419
+ "index": 2,
420
+ "op": "list-set",
421
+ "value": 3,
422
+ },
423
+ "oid": "test/a",
424
+ "timestamp": "0",
425
+ },
426
+ {
427
+ "authz": undefined,
428
+ "data": {
429
+ "index": 4,
430
+ "op": "list-insert",
431
+ "value": 8,
432
+ },
433
+ "oid": "test/a",
434
+ "timestamp": "1",
435
+ },
436
+ {
437
+ "authz": undefined,
438
+ "data": {
439
+ "only": "last",
440
+ "op": "list-remove",
441
+ "value": 7,
442
+ },
443
+ "oid": "test/a",
444
+ "timestamp": "2",
445
+ },
446
+ {
447
+ "authz": undefined,
448
+ "data": {
449
+ "only": "last",
450
+ "op": "list-remove",
451
+ "value": 6,
452
+ },
453
+ "oid": "test/a",
454
+ "timestamp": "3",
455
+ },
456
+ ]
457
+ `);
458
+ expectDiffProducesResult(from, patches, to);
459
+ });
460
+ });
461
+ describe('on lists of objects', () => {
462
+ it('pushes items', () => {
463
+ const from = assignOid([
464
+ { id: '1', value: 'Item one' },
465
+ { id: '2', value: 'Item two' },
466
+ ], 'test/a');
467
+ assignOidsToAllSubObjects(from, createClock());
468
+ const to = assignOid([
469
+ { id: '1', value: 'Item one' },
470
+ { id: '2', value: 'Item two' },
471
+ { id: '3', value: 'Item three' },
472
+ ], 'test/a');
473
+ assignOidsToAllSubObjects(to, createClock());
474
+ const patches = diffToPatches(from, to, createClock(), createClock(2));
475
+ expect(patches).toMatchInlineSnapshot(`
476
+ [
477
+ {
478
+ "data": {
479
+ "op": "initialize",
480
+ "value": {
481
+ "id": "3",
482
+ "value": "Item three",
483
+ },
484
+ },
485
+ "oid": "test/a:2",
486
+ "timestamp": "0",
487
+ },
488
+ {
489
+ "authz": undefined,
490
+ "data": {
491
+ "op": "list-push",
492
+ "value": {
493
+ "@@type": "ref",
494
+ "id": "test/a:2",
495
+ },
496
+ },
497
+ "oid": "test/a",
498
+ "timestamp": "1",
499
+ },
500
+ ]
501
+ `);
502
+ expectDiffProducesResult(from, patches, to);
503
+ });
504
+ it('deletes one item from a list', () => {
505
+ // this replicates a bug I found while testing pruning canonization
506
+ const from = assignOid([
507
+ { id: '1', value: 'Item one' },
508
+ { id: '2', value: 'Item two' },
509
+ {},
510
+ { id: '4', value: 'Item four' },
511
+ ], 'test/a');
512
+ assignOidsToAllSubObjects(from, createClock());
513
+ const to = assignOid([
514
+ assignOid({
515
+ id: '1',
516
+ value: 'Item one',
517
+ }, 'test/a:0'),
518
+ assignOid({ id: '2', value: 'Item two' }, 'test/a:1'),
519
+ assignOid({ id: '4', value: 'Item four' }, 'test/a:3'),
520
+ ], 'test/a');
521
+ const patches = diffToPatches(from, to, createClock(), createClock(4));
522
+ expect(patches).toMatchInlineSnapshot(`
523
+ [
524
+ {
525
+ "authz": undefined,
526
+ "data": {
527
+ "index": 2,
528
+ "op": "list-move-by-ref",
529
+ "value": {
530
+ "@@type": "ref",
531
+ "id": "test/a:3",
532
+ },
533
+ },
534
+ "oid": "test/a",
535
+ "timestamp": "0",
536
+ },
537
+ {
538
+ "authz": undefined,
539
+ "data": {
540
+ "op": "delete",
541
+ },
542
+ "oid": "test/a:2",
543
+ "timestamp": "1",
544
+ },
545
+ {
546
+ "authz": undefined,
547
+ "data": {
548
+ "only": "last",
549
+ "op": "list-remove",
550
+ "value": {
551
+ "@@type": "ref",
552
+ "id": "test/a:2",
553
+ },
554
+ },
555
+ "oid": "test/a",
556
+ "timestamp": "2",
557
+ },
558
+ ]
559
+ `);
560
+ expectDiffProducesResult(from, patches, to);
561
+ });
562
+ it('moves an item from the start to the end of the list', () => {
563
+ // FIXME: the operations end up very inefficient for this. each
564
+ // other item in the list gets moved before the target item is
565
+ // moved to the end. can this be improved?
566
+ const from = assignOid([
567
+ { id: '1', value: 'Item one' },
568
+ { id: '2', value: 'Item two' },
569
+ { id: '3', value: 'Item three' },
570
+ { id: '4', value: 'Item four' },
571
+ ], 'test/a');
572
+ assignOidsToAllSubObjects(from, createClock());
573
+ const to = assignOid([
574
+ assignOid({ id: '2', value: 'Item two' }, 'test/a:1'),
575
+ assignOid({ id: '3', value: 'Item three' }, 'test/a:2'),
576
+ assignOid({ id: '4', value: 'Item four' }, 'test/a:3'),
577
+ assignOid({ id: '1', value: 'Item one' }, 'test/a:0'),
578
+ ], 'test/a');
579
+ const patches = diffToPatches(from, to, createClock(), createClock(4));
580
+ expect(patches).toMatchInlineSnapshot(`
581
+ [
582
+ {
583
+ "authz": undefined,
584
+ "data": {
585
+ "index": 0,
586
+ "op": "list-move-by-ref",
587
+ "value": {
588
+ "@@type": "ref",
589
+ "id": "test/a:1",
590
+ },
591
+ },
592
+ "oid": "test/a",
593
+ "timestamp": "0",
594
+ },
595
+ {
596
+ "authz": undefined,
597
+ "data": {
598
+ "index": 1,
599
+ "op": "list-move-by-ref",
600
+ "value": {
601
+ "@@type": "ref",
602
+ "id": "test/a:2",
603
+ },
604
+ },
605
+ "oid": "test/a",
606
+ "timestamp": "1",
607
+ },
608
+ {
609
+ "authz": undefined,
610
+ "data": {
611
+ "index": 2,
612
+ "op": "list-move-by-ref",
613
+ "value": {
614
+ "@@type": "ref",
615
+ "id": "test/a:3",
616
+ },
617
+ },
618
+ "oid": "test/a",
619
+ "timestamp": "2",
620
+ },
621
+ {
622
+ "authz": undefined,
623
+ "data": {
624
+ "index": 3,
625
+ "op": "list-move-by-ref",
626
+ "value": {
627
+ "@@type": "ref",
628
+ "id": "test/a:0",
629
+ },
630
+ },
631
+ "oid": "test/a",
632
+ "timestamp": "3",
633
+ },
634
+ ]
635
+ `);
636
+ expectDiffProducesResult(from, patches, to);
637
+ });
638
+ it('moves an item from the end to the start of a list', () => {
639
+ const from = assignOid([
640
+ { id: '1', value: 'Item one' },
641
+ { id: '2', value: 'Item two' },
642
+ { id: '3', value: 'Item three' },
643
+ { id: '4', value: 'Item four' },
644
+ ], 'test/a');
645
+ assignOidsToAllSubObjects(from, createClock());
646
+ const to = assignOid([
647
+ assignOid({ id: '4', value: 'Item four' }, 'test/a:3'),
648
+ assignOid({ id: '1', value: 'Item one' }, 'test/a:0'),
649
+ assignOid({ id: '2', value: 'Item two' }, 'test/a:1'),
650
+ assignOid({ id: '3', value: 'Item three' }, 'test/a:2'),
651
+ ], 'test/a');
652
+ const patches = diffToPatches(from, to, createClock(), createClock(4));
653
+ expect(patches).toMatchInlineSnapshot(`
654
+ [
655
+ {
656
+ "authz": undefined,
657
+ "data": {
658
+ "index": 0,
659
+ "op": "list-move-by-ref",
660
+ "value": {
661
+ "@@type": "ref",
662
+ "id": "test/a:3",
663
+ },
664
+ },
665
+ "oid": "test/a",
666
+ "timestamp": "0",
667
+ },
668
+ {
669
+ "authz": undefined,
670
+ "data": {
671
+ "index": 1,
672
+ "op": "list-move-by-ref",
673
+ "value": {
674
+ "@@type": "ref",
675
+ "id": "test/a:0",
676
+ },
677
+ },
678
+ "oid": "test/a",
679
+ "timestamp": "1",
680
+ },
681
+ {
682
+ "authz": undefined,
683
+ "data": {
684
+ "index": 2,
685
+ "op": "list-move-by-ref",
686
+ "value": {
687
+ "@@type": "ref",
688
+ "id": "test/a:1",
689
+ },
690
+ },
691
+ "oid": "test/a",
692
+ "timestamp": "2",
693
+ },
694
+ {
695
+ "authz": undefined,
696
+ "data": {
697
+ "index": 3,
698
+ "op": "list-move-by-ref",
699
+ "value": {
700
+ "@@type": "ref",
701
+ "id": "test/a:2",
702
+ },
703
+ },
704
+ "oid": "test/a",
705
+ "timestamp": "3",
706
+ },
707
+ ]
708
+ `);
709
+ expectDiffProducesResult(from, patches, to);
710
+ });
711
+ it('swaps two items by reference', () => {
712
+ const from = assignOid([
713
+ { id: '1', value: 'Item one' },
714
+ { id: '2', value: 'Item two' },
715
+ { id: '3', value: 'Item three' },
716
+ { id: '4', value: 'Item four' },
717
+ ], 'test/a');
718
+ assignOidsToAllSubObjects(from, createClock());
719
+ const to = assignOid([
720
+ assignOid({ id: '1', value: 'Item one' }, 'test/a:0'),
721
+ assignOid({ id: '3', value: 'Item three' }, 'test/a:2'),
722
+ assignOid({ id: '2', value: 'Item two' }, 'test/a:1'),
723
+ assignOid({ id: '4', value: 'Item four' }, 'test/a:3'),
724
+ ], 'test/a');
725
+ const patches = diffToPatches(from, to, createClock(), createClock(4));
726
+ expect(patches).toMatchInlineSnapshot(`
727
+ [
728
+ {
729
+ "authz": undefined,
730
+ "data": {
731
+ "index": 1,
732
+ "op": "list-move-by-ref",
733
+ "value": {
734
+ "@@type": "ref",
735
+ "id": "test/a:2",
736
+ },
737
+ },
738
+ "oid": "test/a",
739
+ "timestamp": "0",
740
+ },
741
+ {
742
+ "authz": undefined,
743
+ "data": {
744
+ "index": 2,
745
+ "op": "list-move-by-ref",
746
+ "value": {
747
+ "@@type": "ref",
748
+ "id": "test/a:1",
749
+ },
750
+ },
751
+ "oid": "test/a",
752
+ "timestamp": "1",
753
+ },
754
+ ]
755
+ `);
756
+ expectDiffProducesResult(from, patches, to);
757
+ });
758
+ it('swaps a lot of items by reference', () => {
759
+ const from = assignOid([
760
+ { id: '1', value: 'Item one' },
761
+ { id: '2', value: 'Item two' },
762
+ { id: '3', value: 'Item three' },
763
+ { id: '4', value: 'Item four' },
764
+ ], 'test/a');
765
+ assignOidsToAllSubObjects(from, createClock());
766
+ const to = assignOid([
767
+ assignOid({ id: '1', value: 'Item one' }, 'test/a:0'),
768
+ assignOid({ id: '4', value: 'Item four' }, 'test/a:3'),
769
+ assignOid({ id: '2', value: 'Item two' }, 'test/a:1'),
770
+ assignOid({ id: '3', value: 'Item three' }, 'test/a:2'),
771
+ ], 'test/a');
772
+ const patches = diffToPatches(from, to, createClock(), createClock(4));
773
+ expect(patches).toMatchInlineSnapshot(`
774
+ [
775
+ {
776
+ "authz": undefined,
777
+ "data": {
778
+ "index": 1,
779
+ "op": "list-move-by-ref",
780
+ "value": {
781
+ "@@type": "ref",
782
+ "id": "test/a:3",
783
+ },
784
+ },
785
+ "oid": "test/a",
786
+ "timestamp": "0",
787
+ },
788
+ {
789
+ "authz": undefined,
790
+ "data": {
791
+ "index": 2,
792
+ "op": "list-move-by-ref",
793
+ "value": {
794
+ "@@type": "ref",
795
+ "id": "test/a:1",
796
+ },
797
+ },
798
+ "oid": "test/a",
799
+ "timestamp": "1",
800
+ },
801
+ {
802
+ "authz": undefined,
803
+ "data": {
804
+ "index": 3,
805
+ "op": "list-move-by-ref",
806
+ "value": {
807
+ "@@type": "ref",
808
+ "id": "test/a:2",
809
+ },
810
+ },
811
+ "oid": "test/a",
812
+ "timestamp": "2",
813
+ },
814
+ ]
815
+ `);
816
+ expectDiffProducesResult(from, patches, to);
817
+ });
818
+ it('produces changes which are resilient to intervening item moves', () => {
819
+ const from = assignOid([
820
+ { id: '1', value: 'Item one' },
821
+ { id: '2', value: 'Item two' },
822
+ { id: '3', value: 'Item three' },
823
+ { id: '4', value: 'Item four' },
824
+ ], 'test/a');
825
+ assignOidsToAllSubObjects(from, createClock());
826
+ const to = assignOid([
827
+ assignOid({ id: '1', value: 'Item one' }, 'test/a:0'),
828
+ assignOid({ id: '4', value: 'Item four' }, 'test/a:3'),
829
+ assignOid({ id: '2', value: 'Item two' }, 'test/a:1'),
830
+ assignOid({ id: '3', value: 'Item three' }, 'test/a:2'),
831
+ ], 'test/a');
832
+ const patchClock = createClock(10); // 'the future'
833
+ const patches = diffToPatches(from, to, patchClock, createClock(4));
834
+ expect(patches).toMatchInlineSnapshot(`
835
+ [
836
+ {
837
+ "authz": undefined,
838
+ "data": {
839
+ "index": 1,
840
+ "op": "list-move-by-ref",
841
+ "value": {
842
+ "@@type": "ref",
843
+ "id": "test/a:3",
844
+ },
845
+ },
846
+ "oid": "test/a",
847
+ "timestamp": "10",
848
+ },
849
+ {
850
+ "authz": undefined,
851
+ "data": {
852
+ "index": 2,
853
+ "op": "list-move-by-ref",
854
+ "value": {
855
+ "@@type": "ref",
856
+ "id": "test/a:1",
857
+ },
858
+ },
859
+ "oid": "test/a",
860
+ "timestamp": "11",
861
+ },
862
+ {
863
+ "authz": undefined,
864
+ "data": {
865
+ "index": 3,
866
+ "op": "list-move-by-ref",
867
+ "value": {
868
+ "@@type": "ref",
869
+ "id": "test/a:2",
870
+ },
871
+ },
872
+ "oid": "test/a",
873
+ "timestamp": "12",
874
+ },
875
+ ]
876
+ `);
877
+ // in the meantime, suppose a user manipulates the list...
878
+ const interveningClock = createClock(0);
879
+ const interveningChanges = [
880
+ {
881
+ oid: 'test/a',
882
+ data: {
883
+ op: 'list-move-by-index',
884
+ from: 1,
885
+ to: 2,
886
+ },
887
+ timestamp: interveningClock(),
888
+ },
889
+ {
890
+ oid: 'test/a',
891
+ data: {
892
+ op: 'list-delete',
893
+ index: 0,
894
+ count: 1,
895
+ },
896
+ timestamp: interveningClock(),
897
+ },
898
+ {
899
+ oid: 'test/a:x',
900
+ data: {
901
+ op: 'initialize',
902
+ value: {
903
+ id: 'x',
904
+ value: 'Item x',
905
+ },
906
+ },
907
+ timestamp: interveningClock(),
908
+ },
909
+ {
910
+ oid: 'test/a',
911
+ data: {
912
+ op: 'list-push',
913
+ value: {
914
+ '@@type': 'ref',
915
+ id: 'test/a:x',
916
+ },
917
+ },
918
+ timestamp: interveningClock(),
919
+ },
920
+ ];
921
+ expectDiffProducesResult(from, [...interveningChanges, ...patches], [
922
+ { id: '4', value: 'Item four' },
923
+ { id: '2', value: 'Item two' },
924
+ { id: 'x', value: 'Item x' },
925
+ { id: '3', value: 'Item three' },
926
+ ]);
927
+ });
928
+ });
929
+ describe('on lists of files', () => {
930
+ // FIXME: file refs should work the same as object refs
931
+ it('pushes items', () => {
932
+ const from = assignOid([createFileRef('file-1'), createFileRef('file-2')], 'test/a');
933
+ const to = assignOid([
934
+ createFileRef('file-1'),
935
+ createFileRef('file-2'),
936
+ createFileRef('file-3'),
937
+ ], 'test/a');
938
+ const patches = diffToPatches(from, to, createClock(), createClock(2));
939
+ expect(patches).toMatchInlineSnapshot(`
940
+ [
941
+ {
942
+ "authz": undefined,
943
+ "data": {
944
+ "op": "list-push",
945
+ "value": {
946
+ "@@type": "file",
947
+ "id": "file-3",
948
+ },
949
+ },
950
+ "oid": "test/a",
951
+ "timestamp": "0",
952
+ },
953
+ ]
954
+ `);
955
+ expectDiffProducesResult(from, patches, to);
956
+ });
957
+ it('deletes one item from a list', () => {
958
+ const from = assignOid([
959
+ createFileRef('file-1'),
960
+ createFileRef('file-2'),
961
+ createFileRef('file-3'),
962
+ createFileRef('file-4'),
963
+ ], 'test/a');
964
+ const to = assignOid([
965
+ createFileRef('file-1'),
966
+ createFileRef('file-2'),
967
+ createFileRef('file-4'),
968
+ ], 'test/a');
969
+ const patches = diffToPatches(from, to, createClock(), createClock(4));
970
+ expect(patches).toMatchInlineSnapshot(`
971
+ [
972
+ {
973
+ "authz": undefined,
974
+ "data": {
975
+ "index": 2,
976
+ "op": "list-insert",
977
+ "value": {
978
+ "@@type": "file",
979
+ "id": "file-4",
980
+ },
981
+ },
982
+ "oid": "test/a",
983
+ "timestamp": "0",
984
+ },
985
+ {
986
+ "authz": undefined,
987
+ "data": {
988
+ "only": "last",
989
+ "op": "list-remove",
990
+ "value": {
991
+ "@@type": "file",
992
+ "id": "file-4",
993
+ },
994
+ },
995
+ "oid": "test/a",
996
+ "timestamp": "1",
997
+ },
998
+ {
999
+ "authz": undefined,
1000
+ "data": {
1001
+ "only": "last",
1002
+ "op": "list-remove",
1003
+ "value": {
1004
+ "@@type": "file",
1005
+ "id": "file-3",
1006
+ },
1007
+ },
1008
+ "oid": "test/a",
1009
+ "timestamp": "2",
1010
+ },
1011
+ ]
1012
+ `);
1013
+ expectDiffProducesResult(from, patches, to);
1014
+ });
1015
+ it('moves an item from the start to the end of the list', () => {
1016
+ // FIXME: the operations end up very inefficient for this. each
1017
+ // other item in the list gets moved before the target item is
1018
+ // moved to the end. can this be improved?
1019
+ const from = assignOid([
1020
+ createFileRef('file-1'),
1021
+ createFileRef('file-2'),
1022
+ createFileRef('file-3'),
1023
+ createFileRef('file-4'),
1024
+ ], 'test/a');
1025
+ assignOidsToAllSubObjects(from, createClock());
1026
+ const to = assignOid([
1027
+ createFileRef('file-2'),
1028
+ createFileRef('file-3'),
1029
+ createFileRef('file-4'),
1030
+ createFileRef('file-1'),
1031
+ ], 'test/a');
1032
+ const patches = diffToPatches(from, to, createClock(), createClock(4));
1033
+ expect(patches).toMatchInlineSnapshot(`
1034
+ [
1035
+ {
1036
+ "authz": undefined,
1037
+ "data": {
1038
+ "index": 0,
1039
+ "op": "list-set",
1040
+ "value": {
1041
+ "@@type": "file",
1042
+ "id": "file-2",
1043
+ },
1044
+ },
1045
+ "oid": "test/a",
1046
+ "timestamp": "0",
1047
+ },
1048
+ {
1049
+ "authz": undefined,
1050
+ "data": {
1051
+ "index": 1,
1052
+ "op": "list-set",
1053
+ "value": {
1054
+ "@@type": "file",
1055
+ "id": "file-3",
1056
+ },
1057
+ },
1058
+ "oid": "test/a",
1059
+ "timestamp": "1",
1060
+ },
1061
+ {
1062
+ "authz": undefined,
1063
+ "data": {
1064
+ "index": 2,
1065
+ "op": "list-set",
1066
+ "value": {
1067
+ "@@type": "file",
1068
+ "id": "file-4",
1069
+ },
1070
+ },
1071
+ "oid": "test/a",
1072
+ "timestamp": "2",
1073
+ },
1074
+ {
1075
+ "authz": undefined,
1076
+ "data": {
1077
+ "index": 3,
1078
+ "op": "list-set",
1079
+ "value": {
1080
+ "@@type": "file",
1081
+ "id": "file-1",
1082
+ },
1083
+ },
1084
+ "oid": "test/a",
1085
+ "timestamp": "3",
1086
+ },
1087
+ ]
1088
+ `);
1089
+ expectDiffProducesResult(from, patches, to);
1090
+ });
1091
+ it('moves an item from the end to the start of a list', () => {
1092
+ const from = assignOid([
1093
+ createFileRef('file-1'),
1094
+ createFileRef('file-2'),
1095
+ createFileRef('file-3'),
1096
+ createFileRef('file-4'),
1097
+ ], 'test/a');
1098
+ assignOidsToAllSubObjects(from, createClock());
1099
+ const to = assignOid([
1100
+ createFileRef('file-4'),
1101
+ createFileRef('file-1'),
1102
+ createFileRef('file-2'),
1103
+ createFileRef('file-3'),
1104
+ ], 'test/a');
1105
+ const patches = diffToPatches(from, to, createClock(), createClock(4));
1106
+ expect(patches).toMatchInlineSnapshot(`
1107
+ [
1108
+ {
1109
+ "authz": undefined,
1110
+ "data": {
1111
+ "index": 0,
1112
+ "op": "list-insert",
1113
+ "value": {
1114
+ "@@type": "file",
1115
+ "id": "file-4",
1116
+ },
1117
+ },
1118
+ "oid": "test/a",
1119
+ "timestamp": "0",
1120
+ },
1121
+ {
1122
+ "authz": undefined,
1123
+ "data": {
1124
+ "only": "last",
1125
+ "op": "list-remove",
1126
+ "value": {
1127
+ "@@type": "file",
1128
+ "id": "file-4",
1129
+ },
1130
+ },
1131
+ "oid": "test/a",
1132
+ "timestamp": "1",
1133
+ },
1134
+ ]
1135
+ `);
1136
+ expectDiffProducesResult(from, patches, to);
1137
+ });
1138
+ it('swaps two items by reference', () => {
1139
+ const from = assignOid([
1140
+ createFileRef('file-1'),
1141
+ createFileRef('file-2'),
1142
+ createFileRef('file-3'),
1143
+ createFileRef('file-4'),
1144
+ ], 'test/a');
1145
+ assignOidsToAllSubObjects(from, createClock());
1146
+ const to = assignOid([
1147
+ createFileRef('file-1'),
1148
+ createFileRef('file-3'),
1149
+ createFileRef('file-2'),
1150
+ createFileRef('file-4'),
1151
+ ], 'test/a');
1152
+ const patches = diffToPatches(from, to, createClock(), createClock(4));
1153
+ expect(patches).toMatchInlineSnapshot(`
1154
+ [
1155
+ {
1156
+ "authz": undefined,
1157
+ "data": {
1158
+ "index": 1,
1159
+ "op": "list-insert",
1160
+ "value": {
1161
+ "@@type": "file",
1162
+ "id": "file-3",
1163
+ },
1164
+ },
1165
+ "oid": "test/a",
1166
+ "timestamp": "0",
1167
+ },
1168
+ {
1169
+ "authz": undefined,
1170
+ "data": {
1171
+ "index": 3,
1172
+ "op": "list-insert",
1173
+ "value": {
1174
+ "@@type": "file",
1175
+ "id": "file-4",
1176
+ },
1177
+ },
1178
+ "oid": "test/a",
1179
+ "timestamp": "1",
1180
+ },
1181
+ {
1182
+ "authz": undefined,
1183
+ "data": {
1184
+ "only": "last",
1185
+ "op": "list-remove",
1186
+ "value": {
1187
+ "@@type": "file",
1188
+ "id": "file-4",
1189
+ },
1190
+ },
1191
+ "oid": "test/a",
1192
+ "timestamp": "2",
1193
+ },
1194
+ {
1195
+ "authz": undefined,
1196
+ "data": {
1197
+ "only": "last",
1198
+ "op": "list-remove",
1199
+ "value": {
1200
+ "@@type": "file",
1201
+ "id": "file-3",
1202
+ },
1203
+ },
1204
+ "oid": "test/a",
1205
+ "timestamp": "3",
1206
+ },
1207
+ ]
1208
+ `);
1209
+ expectDiffProducesResult(from, patches, to);
1210
+ });
1211
+ it('swaps a lot of items by reference', () => {
1212
+ const from = assignOid([
1213
+ createFileRef('file-1'),
1214
+ createFileRef('file-2'),
1215
+ createFileRef('file-3'),
1216
+ createFileRef('file-4'),
1217
+ ], 'test/a');
1218
+ assignOidsToAllSubObjects(from, createClock());
1219
+ const to = assignOid([
1220
+ createFileRef('file-1'),
1221
+ createFileRef('file-4'),
1222
+ createFileRef('file-2'),
1223
+ createFileRef('file-3'),
1224
+ ], 'test/a');
1225
+ const patches = diffToPatches(from, to, createClock(), createClock(4));
1226
+ expect(patches).toMatchInlineSnapshot(`
1227
+ [
1228
+ {
1229
+ "authz": undefined,
1230
+ "data": {
1231
+ "index": 1,
1232
+ "op": "list-insert",
1233
+ "value": {
1234
+ "@@type": "file",
1235
+ "id": "file-4",
1236
+ },
1237
+ },
1238
+ "oid": "test/a",
1239
+ "timestamp": "0",
1240
+ },
1241
+ {
1242
+ "authz": undefined,
1243
+ "data": {
1244
+ "only": "last",
1245
+ "op": "list-remove",
1246
+ "value": {
1247
+ "@@type": "file",
1248
+ "id": "file-4",
1249
+ },
1250
+ },
1251
+ "oid": "test/a",
1252
+ "timestamp": "1",
1253
+ },
1254
+ ]
1255
+ `);
1256
+ expectDiffProducesResult(from, patches, to);
1257
+ });
1258
+ });
1259
+ it('should work for complex nested arrays and objects', () => {
1260
+ const from = {
1261
+ foo: {
1262
+ bar: [1, 2, 3],
1263
+ },
1264
+ baz: [
1265
+ {
1266
+ corge: true,
1267
+ },
1268
+ ],
1269
+ };
1270
+ assignOid(from, 'test/a');
1271
+ assignOid(from.foo, 'test/a:1');
1272
+ assignOid(from.foo.bar, 'test/a:2');
1273
+ assignOid(from.baz, 'test/a:3');
1274
+ assignOid(from.baz[0], 'test/a:4');
1275
+ const to = {
1276
+ foo: {
1277
+ bar: [1, 2],
1278
+ bop: [0],
1279
+ },
1280
+ baz: [
1281
+ {
1282
+ corge: false,
1283
+ },
1284
+ {
1285
+ corge: false,
1286
+ },
1287
+ ],
1288
+ };
1289
+ assignOid(to, 'test/a');
1290
+ assignOid(to.foo, 'test/a:1');
1291
+ assignOid(to.foo.bar, 'test/a:2');
1292
+ assignOid(to.foo.bop, 'test/a:6');
1293
+ assignOid(to.baz, 'test/a:3');
1294
+ assignOid(to.baz[0], 'test/a:4');
1295
+ assignOid(to.baz[1], 'test/a:5');
1296
+ const patches = diffToPatches(from, to, createClock(), createClock(7));
1297
+ expect(patches).toMatchInlineSnapshot(`
1298
+ [
1299
+ {
1300
+ "authz": undefined,
1301
+ "data": {
1302
+ "only": "last",
1303
+ "op": "list-remove",
1304
+ "value": 3,
1305
+ },
1306
+ "oid": "test/a:2",
1307
+ "timestamp": "0",
1308
+ },
1309
+ {
1310
+ "data": {
1311
+ "op": "initialize",
1312
+ "value": [
1313
+ 0,
1314
+ ],
1315
+ },
1316
+ "oid": "test/a:7",
1317
+ "timestamp": "1",
1318
+ },
1319
+ {
1320
+ "authz": undefined,
1321
+ "data": {
1322
+ "name": "bop",
1323
+ "op": "set",
1324
+ "value": {
1325
+ "@@type": "ref",
1326
+ "id": "test/a:7",
1327
+ },
1328
+ },
1329
+ "oid": "test/a:1",
1330
+ "timestamp": "2",
1331
+ },
1332
+ {
1333
+ "authz": undefined,
1334
+ "data": {
1335
+ "name": "corge",
1336
+ "op": "set",
1337
+ "value": false,
1338
+ },
1339
+ "oid": "test/a:4",
1340
+ "timestamp": "3",
1341
+ },
1342
+ {
1343
+ "data": {
1344
+ "op": "initialize",
1345
+ "value": {
1346
+ "corge": false,
1347
+ },
1348
+ },
1349
+ "oid": "test/a:8",
1350
+ "timestamp": "4",
1351
+ },
1352
+ {
1353
+ "authz": undefined,
1354
+ "data": {
1355
+ "op": "list-push",
1356
+ "value": {
1357
+ "@@type": "ref",
1358
+ "id": "test/a:8",
1359
+ },
1360
+ },
1361
+ "oid": "test/a:3",
1362
+ "timestamp": "5",
1363
+ },
1364
+ ]
1365
+ `);
1366
+ expectDiffProducesResult(from, patches, to);
1367
+ });
1368
+ it('should try to merge unknown objects if specified to do so', () => {
1369
+ const from = {
1370
+ foo: {
1371
+ bar: [1, 2, 3],
1372
+ },
1373
+ baz: [
1374
+ {
1375
+ corge: true,
1376
+ },
1377
+ ],
1378
+ };
1379
+ assignOid(from, 'test/a');
1380
+ assignOidsToAllSubObjects(from, createClock());
1381
+ const to = {
1382
+ foo: {
1383
+ bar: [1, 2],
1384
+ bop: [0],
1385
+ },
1386
+ baz: [
1387
+ {
1388
+ corge: false,
1389
+ },
1390
+ {
1391
+ corge: false,
1392
+ },
1393
+ ],
1394
+ };
1395
+ const patches = diffToPatches(from, to, createClock(), createClock(5), [], {
1396
+ mergeUnknownObjects: true,
1397
+ });
1398
+ expect(patches).toMatchInlineSnapshot(`
1399
+ [
1400
+ {
1401
+ "authz": undefined,
1402
+ "data": {
1403
+ "only": "last",
1404
+ "op": "list-remove",
1405
+ "value": 3,
1406
+ },
1407
+ "oid": "test/a:1",
1408
+ "timestamp": "0",
1409
+ },
1410
+ {
1411
+ "data": {
1412
+ "op": "initialize",
1413
+ "value": [
1414
+ 0,
1415
+ ],
1416
+ },
1417
+ "oid": "test/a:5",
1418
+ "timestamp": "1",
1419
+ },
1420
+ {
1421
+ "authz": undefined,
1422
+ "data": {
1423
+ "name": "bop",
1424
+ "op": "set",
1425
+ "value": {
1426
+ "@@type": "ref",
1427
+ "id": "test/a:5",
1428
+ },
1429
+ },
1430
+ "oid": "test/a:0",
1431
+ "timestamp": "2",
1432
+ },
1433
+ {
1434
+ "authz": undefined,
1435
+ "data": {
1436
+ "name": "corge",
1437
+ "op": "set",
1438
+ "value": false,
1439
+ },
1440
+ "oid": "test/a:3",
1441
+ "timestamp": "3",
1442
+ },
1443
+ {
1444
+ "data": {
1445
+ "op": "initialize",
1446
+ "value": {
1447
+ "corge": false,
1448
+ },
1449
+ },
1450
+ "oid": "test/a:6",
1451
+ "timestamp": "4",
1452
+ },
1453
+ {
1454
+ "authz": undefined,
1455
+ "data": {
1456
+ "op": "list-push",
1457
+ "value": {
1458
+ "@@type": "ref",
1459
+ "id": "test/a:6",
1460
+ },
1461
+ },
1462
+ "oid": "test/a:2",
1463
+ "timestamp": "5",
1464
+ },
1465
+ ]
1466
+ `);
1467
+ expectDiffProducesResult(from, patches, to);
1468
+ });
1469
+ it('should not diff file refs', () => {
1470
+ const from = {
1471
+ foo: {
1472
+ file: createFileRef('abc123'),
1473
+ },
1474
+ bar: [createFileRef('def456'), createFileRef('ghi789')],
1475
+ };
1476
+ assignOid(from, 'test/a');
1477
+ assignOidsToAllSubObjects(from, createClock());
1478
+ const to = {
1479
+ foo: {
1480
+ file: createFileRef('abc456'),
1481
+ },
1482
+ bar: [createFileRef('def456')],
1483
+ };
1484
+ const patches = diffToPatches(from, to, createClock(), createClock(5), [], {
1485
+ mergeUnknownObjects: true,
1486
+ });
1487
+ expect(patches).toMatchInlineSnapshot(`
1488
+ [
1489
+ {
1490
+ "authz": undefined,
1491
+ "data": {
1492
+ "name": "file",
1493
+ "op": "set",
1494
+ "value": {
1495
+ "@@type": "file",
1496
+ "id": "abc456",
1497
+ },
1498
+ },
1499
+ "oid": "test/a:0",
1500
+ "timestamp": "0",
1501
+ },
1502
+ {
1503
+ "authz": undefined,
1504
+ "data": {
1505
+ "only": "last",
1506
+ "op": "list-remove",
1507
+ "value": {
1508
+ "@@type": "file",
1509
+ "id": "ghi789",
1510
+ },
1511
+ },
1512
+ "oid": "test/a:1",
1513
+ "timestamp": "1",
1514
+ },
1515
+ ]
1516
+ `);
1517
+ expectDiffProducesResult(from, patches, to);
1518
+ });
1519
+ it('should assign a new OID to objects which had an incompatible existing OID', () => {
1520
+ const from = {
1521
+ foo: {
1522
+ bar: [1, 2, 3],
1523
+ },
1524
+ };
1525
+ assignOid(from, 'test/a');
1526
+ assignOidsToAllSubObjects(from, createClock());
1527
+ const to = {
1528
+ foo: {
1529
+ bar: [1, 2],
1530
+ },
1531
+ };
1532
+ assignOid(to, 'test/a');
1533
+ assignOid(to.foo, 'uff/b');
1534
+ assignOidsToAllSubObjects(to.foo, createClock());
1535
+ const patches = diffToPatches(from, to, createClock(), createClock(5), [], {
1536
+ mergeUnknownObjects: true,
1537
+ });
1538
+ expect(patches).toMatchInlineSnapshot(`
1539
+ [
1540
+ {
1541
+ "data": {
1542
+ "op": "initialize",
1543
+ "value": [
1544
+ 1,
1545
+ 2,
1546
+ ],
1547
+ },
1548
+ "oid": "test/a:6",
1549
+ "timestamp": "0",
1550
+ },
1551
+ {
1552
+ "data": {
1553
+ "op": "initialize",
1554
+ "value": {
1555
+ "bar": {
1556
+ "@@type": "ref",
1557
+ "id": "test/a:6",
1558
+ },
1559
+ },
1560
+ },
1561
+ "oid": "test/a:5",
1562
+ "timestamp": "1",
1563
+ },
1564
+ {
1565
+ "authz": undefined,
1566
+ "data": {
1567
+ "name": "foo",
1568
+ "op": "set",
1569
+ "value": {
1570
+ "@@type": "ref",
1571
+ "id": "test/a:5",
1572
+ },
1573
+ },
1574
+ "oid": "test/a",
1575
+ "timestamp": "2",
1576
+ },
1577
+ {
1578
+ "authz": undefined,
1579
+ "data": {
1580
+ "op": "delete",
1581
+ },
1582
+ "oid": "test/a:0",
1583
+ "timestamp": "3",
1584
+ },
1585
+ {
1586
+ "authz": undefined,
1587
+ "data": {
1588
+ "op": "delete",
1589
+ },
1590
+ "oid": "test/a:1",
1591
+ "timestamp": "4",
1592
+ },
1593
+ ]
1594
+ `);
1595
+ expectDiffProducesResult(from, patches, to);
1596
+ });
1597
+ // an edge case - if a subobject which contains nested objects changes identity,
1598
+ // this gets written as a new init patch tree - we want to ensure all init subobjects
1599
+ // in that tree have compatible OIDs.
1600
+ it('should assign a new OID to objects which have an incompatible OID when parent identity changes', () => {
1601
+ const from = {
1602
+ bar: [],
1603
+ };
1604
+ assignOid(from, 'test/a');
1605
+ assignOid(from.bar, 'test/random');
1606
+ const to = {
1607
+ bar: [{ baz: 1 }, { baz: 2 }],
1608
+ };
1609
+ assignOid(to, 'test/a');
1610
+ assignOidsToAllSubObjects(to, createClock());
1611
+ assignOid(to.bar[0], 'uff/b');
1612
+ const patches = diffToPatches(from, to, createClock(), createClock(5), [], {
1613
+ mergeUnknownObjects: true,
1614
+ });
1615
+ expect(patches).toMatchInlineSnapshot(`
1616
+ [
1617
+ {
1618
+ "data": {
1619
+ "op": "initialize",
1620
+ "value": {
1621
+ "baz": 1,
1622
+ },
1623
+ },
1624
+ "oid": "test/a:6",
1625
+ "timestamp": "0",
1626
+ },
1627
+ {
1628
+ "data": {
1629
+ "op": "initialize",
1630
+ "value": {
1631
+ "baz": 2,
1632
+ },
1633
+ },
1634
+ "oid": "test/a:2",
1635
+ "timestamp": "1",
1636
+ },
1637
+ {
1638
+ "data": {
1639
+ "op": "initialize",
1640
+ "value": [
1641
+ {
1642
+ "@@type": "ref",
1643
+ "id": "test/a:6",
1644
+ },
1645
+ {
1646
+ "@@type": "ref",
1647
+ "id": "test/a:2",
1648
+ },
1649
+ ],
1650
+ },
1651
+ "oid": "test/a:5",
1652
+ "timestamp": "2",
1653
+ },
1654
+ {
1655
+ "authz": undefined,
1656
+ "data": {
1657
+ "name": "bar",
1658
+ "op": "set",
1659
+ "value": {
1660
+ "@@type": "ref",
1661
+ "id": "test/a:5",
1662
+ },
1663
+ },
1664
+ "oid": "test/a",
1665
+ "timestamp": "3",
1666
+ },
1667
+ {
1668
+ "authz": undefined,
1669
+ "data": {
1670
+ "op": "delete",
1671
+ },
1672
+ "oid": "test/random",
1673
+ "timestamp": "4",
1674
+ },
1675
+ ]
1676
+ `);
1677
+ expectDiffProducesResult(from, patches, to);
1678
+ });
1679
+ });
1680
+ //# sourceMappingURL=diffing.test.js.map