@verdant-web/common 2.7.3 → 2.8.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.
@@ -1,18 +1,19 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { createFileRef, FileRef } from './files.js';
2
+ import { FileRef } from './files.js';
3
3
  import {
4
4
  assignOid,
5
5
  assignOidsToAllSubObjects,
6
6
  createRef,
7
7
  getOid,
8
+ ObjectIdentifier,
8
9
  } from './oids.js';
9
10
  import {
10
- applyPatch,
11
11
  applyOperations,
12
- diffToPatches,
12
+ applyPatch,
13
+ deconstructSnapshotToRefs,
13
14
  initialToPatches,
14
- substituteRefsWithObjects,
15
15
  ObjectRef,
16
+ substituteRefsWithObjects,
16
17
  } from './operation.js';
17
18
 
18
19
  function createClock(init = 0) {
@@ -26,212 +27,7 @@ function createClock(init = 0) {
26
27
  return clock;
27
28
  }
28
29
 
29
- describe('creating diff patch operations', () => {
30
- describe('on flat objects', () => {
31
- it('generates and applies set and remove operations', () => {
32
- const from = { foo: 'bar', baz: 'qux', zing: 1 };
33
- assignOid(from, 'test/a');
34
- const to = { foo: 'bar', baz: 'pop' };
35
- assignOid(to, 'test/a');
36
- const ops = diffToPatches(from, to, createClock());
37
- expect(ops).toMatchInlineSnapshot(`
38
- [
39
- {
40
- "data": {
41
- "name": "baz",
42
- "op": "set",
43
- "value": "pop",
44
- },
45
- "oid": "test/a",
46
- "timestamp": "0",
47
- },
48
- {
49
- "data": {
50
- "name": "zing",
51
- "op": "remove",
52
- },
53
- "oid": "test/a",
54
- "timestamp": "1",
55
- },
56
- ]
57
- `);
58
- const result = applyOperations(from, ops);
59
- expect(result).toEqual(to);
60
- });
61
- });
62
- describe('on nested objects', () => {
63
- it('replaces whole nested objects', () => {
64
- const from = assignOid(
65
- {
66
- foo: 'bar',
67
- baz: assignOid({ qux: 'corge' }, 'test/a:0'),
68
- },
69
- 'test/a',
70
- );
71
- const to = assignOid(
72
- {
73
- foo: 'bar',
74
- baz: assignOid({ qux: 'grault' }, 'test/a:1'),
75
- },
76
- 'test/a',
77
- );
78
- const patches = diffToPatches(from, to, createClock());
79
- expect(patches).toMatchInlineSnapshot(`
80
- [
81
- {
82
- "data": {
83
- "op": "initialize",
84
- "value": {
85
- "qux": "grault",
86
- },
87
- },
88
- "oid": "test/a:1",
89
- "timestamp": "0",
90
- },
91
- {
92
- "data": {
93
- "name": "baz",
94
- "op": "set",
95
- "value": {
96
- "@@type": "ref",
97
- "id": "test/a:1",
98
- },
99
- },
100
- "oid": "test/a",
101
- "timestamp": "1",
102
- },
103
- {
104
- "data": {
105
- "op": "delete",
106
- },
107
- "oid": "test/a:0",
108
- "timestamp": "2",
109
- },
110
- ]
111
- `);
112
- });
113
- it('replaces whole nested objects with different ids even if fields are the same', () => {
114
- const from = {
115
- foo: 'bar',
116
- baz: { qux: 'corge' },
117
- };
118
- assignOid(from, 'test/a');
119
- assignOid(from.baz, 'test/a:0');
120
- const to = {
121
- foo: 'bar',
122
- baz: { qux: 'corge' },
123
- };
124
- assignOid(to, 'test/a');
125
- assignOid(to.baz, 'test/a:1');
126
- const patches = diffToPatches(from, to, createClock());
127
- expect(patches).toMatchInlineSnapshot(`
128
- [
129
- {
130
- "data": {
131
- "op": "initialize",
132
- "value": {
133
- "qux": "corge",
134
- },
135
- },
136
- "oid": "test/a:1",
137
- "timestamp": "0",
138
- },
139
- {
140
- "data": {
141
- "name": "baz",
142
- "op": "set",
143
- "value": {
144
- "@@type": "ref",
145
- "id": "test/a:1",
146
- },
147
- },
148
- "oid": "test/a",
149
- "timestamp": "1",
150
- },
151
- {
152
- "data": {
153
- "op": "delete",
154
- },
155
- "oid": "test/a:0",
156
- "timestamp": "2",
157
- },
158
- ]
159
- `);
160
- });
161
- it('does not replace objects with the same identity and same fields', () => {
162
- const from = {
163
- foo: 'bar',
164
- baz: { qux: 'corge' },
165
- };
166
- assignOid(from, 'test/a');
167
- assignOid(from.baz, 'test/a:0');
168
- const to = {
169
- foo: 'bar',
170
- baz: { qux: 'corge' },
171
- };
172
- assignOid(to, 'test/a');
173
- assignOid(to.baz, 'test/a:0');
174
- const patches = diffToPatches(from, to, createClock());
175
- expect(patches).toEqual([]);
176
- });
177
- it('patches sub-objects with same identity', () => {
178
- const from = {
179
- foo: 'bar',
180
- baz: { qux: 'corge' },
181
- };
182
- assignOid(from, 'test/a');
183
- assignOid(from.baz, 'test/a:0');
184
- const to = {
185
- foo: 'bar',
186
- baz: { qux: 'fig' },
187
- };
188
- assignOid(to, 'test/a');
189
- assignOid(to.baz, 'test/a:0');
190
- const patches = diffToPatches(from, to, createClock());
191
- expect(patches).toMatchInlineSnapshot(`
192
- [
193
- {
194
- "data": {
195
- "name": "qux",
196
- "op": "set",
197
- "value": "fig",
198
- },
199
- "oid": "test/a:0",
200
- "timestamp": "0",
201
- },
202
- ]
203
- `);
204
- });
205
- it('retains keys which are undefined in new object if defaultUndefined is set', () => {
206
- const from = {
207
- foo: 'bar',
208
- baz: { qux: 'corge' },
209
- };
210
- assignOid(from, 'test/a');
211
- assignOid(from.baz, 'test/a:0');
212
- const to = {
213
- foo: 'bip',
214
- };
215
- assignOid(to, 'test/a');
216
- const patches = diffToPatches(from, to, createClock(), undefined, [], {
217
- defaultUndefined: true,
218
- mergeUnknownObjects: true,
219
- });
220
- expect(patches).toMatchInlineSnapshot(`
221
- [
222
- {
223
- "data": {
224
- "name": "foo",
225
- "op": "set",
226
- "value": "bip",
227
- },
228
- "oid": "test/a",
229
- "timestamp": "0",
230
- },
231
- ]
232
- `);
233
- });
234
- });
30
+ describe('applying operations', () => {
235
31
  describe('on lists of primitives', () => {
236
32
  it('pushes items', async () => {
237
33
  expect(
@@ -247,423 +43,84 @@ describe('creating diff patch operations', () => {
247
43
  ]),
248
44
  ).toEqual(assignOid(['foo', 'bar', 'baz'], 'test/a'));
249
45
  });
250
- it.todo('sets items');
251
- it.todo('inserts items');
252
- it.todo('removes items by index');
253
- it.todo('removes items by value');
254
- it.todo('moves items by index');
255
- it.todo('moves items by value');
256
- });
257
- describe.todo('on lists of objects', () => {
258
- it.todo('pushes items');
259
- it.todo('moves objects by identity');
260
- it.todo('removes objects by identity');
46
+ it('sets items', () => {
47
+ expect(
48
+ applyOperations(assignOid(['foo', 'bar'], 'test/a'), [
49
+ {
50
+ oid: 'test/a',
51
+ timestamp: 'meh',
52
+ data: {
53
+ op: 'list-set',
54
+ index: 1,
55
+ value: 'baz',
56
+ },
57
+ },
58
+ ]),
59
+ ).toEqual(assignOid(['foo', 'baz'], 'test/a'));
60
+ });
61
+ it('inserts items', () => {
62
+ expect(
63
+ applyOperations(assignOid(['foo', 'bar'], 'test/a'), [
64
+ {
65
+ oid: 'test/a',
66
+ timestamp: 'meh',
67
+ data: {
68
+ op: 'list-insert',
69
+ index: 1,
70
+ values: ['baz'],
71
+ },
72
+ },
73
+ ]),
74
+ ).toEqual(assignOid(['foo', 'baz', 'bar'], 'test/a'));
75
+ });
76
+ it('removes items by index', () => {
77
+ expect(
78
+ applyOperations(assignOid(['foo', 'bar'], 'test/a'), [
79
+ {
80
+ oid: 'test/a',
81
+ timestamp: 'meh',
82
+ data: {
83
+ op: 'list-delete',
84
+ index: 1,
85
+ count: 1,
86
+ },
87
+ },
88
+ ]),
89
+ ).toEqual(assignOid(['foo'], 'test/a'));
90
+ });
91
+ it('removes items by value', () => {
92
+ expect(
93
+ applyOperations(assignOid(['bar', 'foo', 'bar'], 'test/a'), [
94
+ {
95
+ oid: 'test/a',
96
+ timestamp: 'meh',
97
+ data: {
98
+ op: 'list-remove',
99
+ value: 'bar',
100
+ },
101
+ },
102
+ ]),
103
+ ).toEqual(assignOid(['foo'], 'test/a'));
104
+ });
105
+ it('moves items by index', () => {
106
+ expect(
107
+ applyOperations(assignOid(['foo', 'bar', 'baz'], 'test/a'), [
108
+ {
109
+ oid: 'test/a',
110
+ timestamp: 'meh',
111
+ data: {
112
+ op: 'list-move-by-index',
113
+ from: 2,
114
+ to: 0,
115
+ },
116
+ },
117
+ ]),
118
+ ).toEqual(assignOid(['baz', 'foo', 'bar'], 'test/a'));
119
+ });
261
120
  });
262
121
  describe.todo('on lists of lists', () => {
263
122
  it.todo('rejects operations by value or identity');
264
123
  });
265
- it('should work for complex nested arrays and objects', () => {
266
- const from = {
267
- foo: {
268
- bar: [1, 2, 3],
269
- },
270
- baz: [
271
- {
272
- corge: true,
273
- },
274
- ],
275
- };
276
- assignOid(from, 'test/a');
277
- assignOid(from.foo, 'test/a:1');
278
- assignOid(from.foo.bar, 'test/a:2');
279
- assignOid(from.baz, 'test/a:3');
280
- assignOid(from.baz[0], 'test/a:4');
281
-
282
- const to = {
283
- foo: {
284
- bar: [1, 2],
285
- bop: [0],
286
- },
287
- baz: [
288
- {
289
- corge: false,
290
- },
291
- {
292
- corge: false,
293
- },
294
- ],
295
- };
296
- assignOid(to, 'test/a');
297
- assignOid(to.foo, 'test/a:1');
298
- assignOid(to.foo.bar, 'test/a:2');
299
- assignOid(to.foo.bop, 'test/a:6');
300
- assignOid(to.baz, 'test/a:3');
301
- assignOid(to.baz[0], 'test/a:4');
302
- assignOid(to.baz[1], 'test/a:5');
303
- expect(diffToPatches(from, to, createClock())).toMatchInlineSnapshot(`
304
- [
305
- {
306
- "data": {
307
- "count": 1,
308
- "index": 2,
309
- "op": "list-delete",
310
- },
311
- "oid": "test/a:2",
312
- "timestamp": "0",
313
- },
314
- {
315
- "data": {
316
- "op": "initialize",
317
- "value": [
318
- 0,
319
- ],
320
- },
321
- "oid": "test/a:6",
322
- "timestamp": "1",
323
- },
324
- {
325
- "data": {
326
- "name": "bop",
327
- "op": "set",
328
- "value": {
329
- "@@type": "ref",
330
- "id": "test/a:6",
331
- },
332
- },
333
- "oid": "test/a:1",
334
- "timestamp": "2",
335
- },
336
- {
337
- "data": {
338
- "name": "corge",
339
- "op": "set",
340
- "value": false,
341
- },
342
- "oid": "test/a:4",
343
- "timestamp": "3",
344
- },
345
- {
346
- "data": {
347
- "op": "initialize",
348
- "value": {
349
- "corge": false,
350
- },
351
- },
352
- "oid": "test/a:5",
353
- "timestamp": "4",
354
- },
355
- {
356
- "data": {
357
- "name": 1,
358
- "op": "set",
359
- "value": {
360
- "@@type": "ref",
361
- "id": "test/a:5",
362
- },
363
- },
364
- "oid": "test/a:3",
365
- "timestamp": "5",
366
- },
367
- ]
368
- `);
369
- });
370
-
371
- it('should try to merge unknown objects if specified to do so', () => {
372
- const from = {
373
- foo: {
374
- bar: [1, 2, 3],
375
- },
376
- baz: [
377
- {
378
- corge: true,
379
- },
380
- ],
381
- };
382
- assignOid(from, 'test/a');
383
- assignOidsToAllSubObjects(from, createClock());
384
-
385
- const to = {
386
- foo: {
387
- bar: [1, 2],
388
- bop: [0],
389
- },
390
- baz: [
391
- {
392
- corge: false,
393
- },
394
- {
395
- corge: false,
396
- },
397
- ],
398
- };
399
- expect(
400
- diffToPatches(from, to, createClock(), createClock(5), [], {
401
- mergeUnknownObjects: true,
402
- }),
403
- ).toMatchInlineSnapshot(`
404
- [
405
- {
406
- "data": {
407
- "count": 1,
408
- "index": 2,
409
- "op": "list-delete",
410
- },
411
- "oid": "test/a:1",
412
- "timestamp": "0",
413
- },
414
- {
415
- "data": {
416
- "op": "initialize",
417
- "value": [
418
- 0,
419
- ],
420
- },
421
- "oid": "test/a:5",
422
- "timestamp": "1",
423
- },
424
- {
425
- "data": {
426
- "name": "bop",
427
- "op": "set",
428
- "value": {
429
- "@@type": "ref",
430
- "id": "test/a:5",
431
- },
432
- },
433
- "oid": "test/a:0",
434
- "timestamp": "2",
435
- },
436
- {
437
- "data": {
438
- "name": "corge",
439
- "op": "set",
440
- "value": false,
441
- },
442
- "oid": "test/a:3",
443
- "timestamp": "3",
444
- },
445
- {
446
- "data": {
447
- "op": "initialize",
448
- "value": {
449
- "corge": false,
450
- },
451
- },
452
- "oid": "test/a:6",
453
- "timestamp": "4",
454
- },
455
- {
456
- "data": {
457
- "name": 1,
458
- "op": "set",
459
- "value": {
460
- "@@type": "ref",
461
- "id": "test/a:6",
462
- },
463
- },
464
- "oid": "test/a:2",
465
- "timestamp": "5",
466
- },
467
- ]
468
- `);
469
- });
470
-
471
- it('should not diff file refs', () => {
472
- const from = {
473
- foo: {
474
- file: createFileRef('abc123'),
475
- },
476
- bar: [createFileRef('def456'), createFileRef('ghi789')],
477
- };
478
- assignOid(from, 'test/a');
479
- assignOidsToAllSubObjects(from, createClock());
480
-
481
- const to = {
482
- foo: {
483
- file: createFileRef('abc456'),
484
- },
485
- bar: [createFileRef('def456')],
486
- };
487
- expect(
488
- diffToPatches(from, to, createClock(), createClock(5), [], {
489
- mergeUnknownObjects: true,
490
- }),
491
- ).toMatchInlineSnapshot(`
492
- [
493
- {
494
- "data": {
495
- "name": "file",
496
- "op": "set",
497
- "value": {
498
- "@@type": "file",
499
- "id": "abc456",
500
- },
501
- },
502
- "oid": "test/a:0",
503
- "timestamp": "0",
504
- },
505
- {
506
- "data": {
507
- "count": 1,
508
- "index": 1,
509
- "op": "list-delete",
510
- },
511
- "oid": "test/a:1",
512
- "timestamp": "1",
513
- },
514
- ]
515
- `);
516
- });
517
-
518
- it('should assign a new OID to objects which had an incompatible existing OID', () => {
519
- const from = {
520
- foo: {
521
- bar: [1, 2, 3],
522
- },
523
- };
524
- assignOid(from, 'test/a');
525
- assignOidsToAllSubObjects(from, createClock());
526
- const to = {
527
- foo: {
528
- bar: [1, 2],
529
- },
530
- };
531
- assignOid(to, 'test/a');
532
- assignOid(to.foo, 'uff/b');
533
- assignOidsToAllSubObjects(to.foo, createClock());
534
- expect(
535
- diffToPatches(from, to, createClock(), createClock(5), [], {
536
- mergeUnknownObjects: true,
537
- }),
538
- ).toMatchInlineSnapshot(`
539
- [
540
- {
541
- "data": {
542
- "op": "initialize",
543
- "value": [
544
- 1,
545
- 2,
546
- ],
547
- },
548
- "oid": "test/a:6",
549
- "timestamp": "0",
550
- },
551
- {
552
- "data": {
553
- "op": "initialize",
554
- "value": {
555
- "bar": {
556
- "@@type": "ref",
557
- "id": "test/a:6",
558
- },
559
- },
560
- },
561
- "oid": "test/a:5",
562
- "timestamp": "1",
563
- },
564
- {
565
- "data": {
566
- "name": "foo",
567
- "op": "set",
568
- "value": {
569
- "@@type": "ref",
570
- "id": "test/a:5",
571
- },
572
- },
573
- "oid": "test/a",
574
- "timestamp": "2",
575
- },
576
- {
577
- "data": {
578
- "op": "delete",
579
- },
580
- "oid": "test/a:0",
581
- "timestamp": "3",
582
- },
583
- ]
584
- `);
585
- });
586
-
587
- // an edge case - if a subobject which contains nested objects changes identity,
588
- // this gets written as a new init patch tree - we want to ensure all init subobjects
589
- // in that tree have compatible OIDs.
590
- it('should assign a new OID to objects which have an incompatible OID when parent identity changes', () => {
591
- const from = {
592
- bar: [],
593
- };
594
- assignOid(from, 'test/a');
595
- assignOid(from.bar, 'test/random');
596
- const to = {
597
- bar: [{ baz: 1 }, { baz: 2 }],
598
- };
599
- assignOid(to, 'test/a');
600
- assignOidsToAllSubObjects(to, createClock());
601
- assignOid(to.bar[0], 'uff/b');
602
- expect(
603
- diffToPatches(from, to, createClock(), createClock(5), [], {
604
- mergeUnknownObjects: true,
605
- }),
606
- ).toMatchInlineSnapshot(`
607
- [
608
- {
609
- "data": {
610
- "op": "initialize",
611
- "value": {
612
- "baz": 1,
613
- },
614
- },
615
- "oid": "test/a:5",
616
- "timestamp": "0",
617
- },
618
- {
619
- "data": {
620
- "op": "initialize",
621
- "value": {
622
- "baz": 2,
623
- },
624
- },
625
- "oid": "test/a:2",
626
- "timestamp": "1",
627
- },
628
- {
629
- "data": {
630
- "op": "initialize",
631
- "value": [
632
- {
633
- "@@type": "ref",
634
- "id": "test/a:5",
635
- },
636
- {
637
- "@@type": "ref",
638
- "id": "test/a:2",
639
- },
640
- ],
641
- },
642
- "oid": "test/a:0",
643
- "timestamp": "2",
644
- },
645
- {
646
- "data": {
647
- "name": "bar",
648
- "op": "set",
649
- "value": {
650
- "@@type": "ref",
651
- "id": "test/a:0",
652
- },
653
- },
654
- "oid": "test/a",
655
- "timestamp": "3",
656
- },
657
- {
658
- "data": {
659
- "op": "delete",
660
- },
661
- "oid": "test/random",
662
- "timestamp": "4",
663
- },
664
- ]
665
- `);
666
- });
667
124
  });
668
125
 
669
126
  describe('substituting refs with objects', () => {
@@ -839,6 +296,74 @@ describe('substituting refs with objects', () => {
839
296
  it.todo('substitutes arrays of arrays of objects');
840
297
  });
841
298
 
299
+ describe('substituting objects with refs', () => {
300
+ it('deconstructs nested objects and arrays properly', () => {
301
+ const snapshot = assignOid(
302
+ {
303
+ foo: {
304
+ bar: {
305
+ baz: [{ corge: 'qux' }, { grault: 'garply' }],
306
+ },
307
+ prim: 'hi',
308
+ },
309
+ cat: {
310
+ percol: 'simmi',
311
+ },
312
+ },
313
+ 'test/a',
314
+ );
315
+ assignOidsToAllSubObjects(snapshot, createClock());
316
+ const deconstructed = new Map<ObjectIdentifier, any>();
317
+ deconstructSnapshotToRefs(snapshot, deconstructed);
318
+ expect(deconstructed).toMatchInlineSnapshot(`
319
+ Map {
320
+ "test/a" => {
321
+ "cat": {
322
+ "@@type": "ref",
323
+ "id": "test/a:5",
324
+ },
325
+ "foo": {
326
+ "@@type": "ref",
327
+ "id": "test/a:0",
328
+ },
329
+ },
330
+ "test/a:0" => {
331
+ "bar": {
332
+ "@@type": "ref",
333
+ "id": "test/a:1",
334
+ },
335
+ "prim": "hi",
336
+ },
337
+ "test/a:1" => {
338
+ "baz": {
339
+ "@@type": "ref",
340
+ "id": "test/a:2",
341
+ },
342
+ },
343
+ "test/a:2" => [
344
+ {
345
+ "@@type": "ref",
346
+ "id": "test/a:3",
347
+ },
348
+ {
349
+ "@@type": "ref",
350
+ "id": "test/a:4",
351
+ },
352
+ ],
353
+ "test/a:3" => {
354
+ "corge": "qux",
355
+ },
356
+ "test/a:4" => {
357
+ "grault": "garply",
358
+ },
359
+ "test/a:5" => {
360
+ "percol": "simmi",
361
+ },
362
+ }
363
+ `);
364
+ });
365
+ });
366
+
842
367
  describe('creating patches from initial state', () => {
843
368
  it('adds oids to all sub-objects', async () => {
844
369
  let i = 0;