@verdant-web/common 1.11.0 → 1.11.1

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 (141) hide show
  1. package/dist/cjs/EventSubscriber.d.ts +17 -17
  2. package/dist/cjs/EventSubscriber.js +66 -66
  3. package/dist/cjs/baseline.d.ts +5 -5
  4. package/dist/cjs/baseline.js +2 -2
  5. package/dist/cjs/batching.d.ts +41 -41
  6. package/dist/cjs/batching.js +88 -88
  7. package/dist/cjs/files.d.ts +19 -19
  8. package/dist/cjs/files.js +18 -18
  9. package/dist/cjs/index.d.ts +18 -18
  10. package/dist/cjs/index.js +35 -35
  11. package/dist/cjs/indexes.d.ts +14 -14
  12. package/dist/cjs/indexes.js +115 -115
  13. package/dist/cjs/indexes.test.d.ts +1 -1
  14. package/dist/cjs/indexes.test.js +127 -127
  15. package/dist/cjs/migration.d.ts +98 -98
  16. package/dist/cjs/migration.js +127 -127
  17. package/dist/cjs/oids.d.ts +87 -87
  18. package/dist/cjs/oids.js +374 -374
  19. package/dist/cjs/oids.test.d.ts +1 -1
  20. package/dist/cjs/oids.test.js +245 -245
  21. package/dist/cjs/operation.d.ts +139 -139
  22. package/dist/cjs/operation.js +532 -532
  23. package/dist/cjs/operation.test.d.ts +1 -1
  24. package/dist/cjs/operation.test.js +484 -484
  25. package/dist/cjs/patch.d.ts +26 -26
  26. package/dist/cjs/patch.js +226 -226
  27. package/dist/cjs/presence.d.ts +27 -27
  28. package/dist/cjs/presence.js +2 -2
  29. package/dist/cjs/protocol.d.ts +145 -145
  30. package/dist/cjs/protocol.js +3 -3
  31. package/dist/cjs/refs.d.ts +5 -5
  32. package/dist/cjs/refs.js +21 -21
  33. package/dist/cjs/replica.d.ts +28 -28
  34. package/dist/cjs/replica.js +28 -28
  35. package/dist/cjs/schema/defaults.d.ts +4 -4
  36. package/dist/cjs/schema/defaults.js +71 -71
  37. package/dist/cjs/schema/defaults.test.d.ts +1 -1
  38. package/dist/cjs/schema/defaults.test.js +20 -20
  39. package/dist/cjs/schema/index.d.ts +8 -8
  40. package/dist/cjs/schema/index.js +28 -28
  41. package/dist/cjs/schema/indexFilters.d.ts +6 -6
  42. package/dist/cjs/schema/indexFilters.js +30 -30
  43. package/dist/cjs/schema/types/collection.d.ts +51 -51
  44. package/dist/cjs/schema/types/collection.js +2 -2
  45. package/dist/cjs/schema/types/compounds.d.ts +12 -12
  46. package/dist/cjs/schema/types/compounds.js +2 -2
  47. package/dist/cjs/schema/types/fields.d.ts +59 -59
  48. package/dist/cjs/schema/types/fields.js +2 -2
  49. package/dist/cjs/schema/types/filters.d.ts +29 -29
  50. package/dist/cjs/schema/types/filters.js +2 -2
  51. package/dist/cjs/schema/types/shapes.d.ts +21 -21
  52. package/dist/cjs/schema/types/shapes.js +2 -2
  53. package/dist/cjs/schema/types/synthetics.d.ts +28 -28
  54. package/dist/cjs/schema/types/synthetics.js +2 -2
  55. package/dist/cjs/schema/types.d.ts +18 -18
  56. package/dist/cjs/schema/types.js +22 -22
  57. package/dist/cjs/timestamp.d.ts +48 -48
  58. package/dist/cjs/timestamp.js +243 -243
  59. package/dist/cjs/timestamp.test.d.ts +1 -1
  60. package/dist/cjs/timestamp.test.js +93 -93
  61. package/dist/cjs/undo.d.ts +3 -3
  62. package/dist/cjs/undo.js +198 -198
  63. package/dist/cjs/utils.d.ts +15 -15
  64. package/dist/cjs/utils.js +127 -127
  65. package/dist/cjs/utils.test.d.ts +1 -1
  66. package/dist/cjs/utils.test.js +36 -36
  67. package/dist/esm/EventSubscriber.d.ts +17 -17
  68. package/dist/esm/EventSubscriber.js +62 -62
  69. package/dist/esm/baseline.d.ts +5 -5
  70. package/dist/esm/baseline.js +1 -1
  71. package/dist/esm/batching.d.ts +41 -41
  72. package/dist/esm/batching.js +83 -83
  73. package/dist/esm/files.d.ts +19 -19
  74. package/dist/esm/files.js +12 -12
  75. package/dist/esm/index.d.ts +18 -18
  76. package/dist/esm/index.js +15 -15
  77. package/dist/esm/indexes.d.ts +14 -14
  78. package/dist/esm/indexes.js +105 -105
  79. package/dist/esm/indexes.test.d.ts +1 -1
  80. package/dist/esm/indexes.test.js +125 -125
  81. package/dist/esm/migration.d.ts +98 -98
  82. package/dist/esm/migration.js +121 -121
  83. package/dist/esm/oids.d.ts +87 -87
  84. package/dist/esm/oids.js +347 -347
  85. package/dist/esm/oids.test.d.ts +1 -1
  86. package/dist/esm/oids.test.js +243 -243
  87. package/dist/esm/operation.d.ts +139 -139
  88. package/dist/esm/operation.js +517 -517
  89. package/dist/esm/operation.test.d.ts +1 -1
  90. package/dist/esm/operation.test.js +482 -482
  91. package/dist/esm/patch.d.ts +26 -26
  92. package/dist/esm/patch.js +222 -222
  93. package/dist/esm/presence.d.ts +27 -27
  94. package/dist/esm/presence.js +1 -1
  95. package/dist/esm/protocol.d.ts +145 -145
  96. package/dist/esm/protocol.js +2 -2
  97. package/dist/esm/refs.d.ts +5 -5
  98. package/dist/esm/refs.js +16 -16
  99. package/dist/esm/replica.d.ts +28 -28
  100. package/dist/esm/replica.js +25 -25
  101. package/dist/esm/schema/defaults.d.ts +4 -4
  102. package/dist/esm/schema/defaults.js +65 -65
  103. package/dist/esm/schema/defaults.test.d.ts +1 -1
  104. package/dist/esm/schema/defaults.test.js +18 -18
  105. package/dist/esm/schema/index.d.ts +8 -8
  106. package/dist/esm/schema/index.js +9 -9
  107. package/dist/esm/schema/indexFilters.d.ts +6 -6
  108. package/dist/esm/schema/indexFilters.js +22 -22
  109. package/dist/esm/schema/types/collection.d.ts +51 -51
  110. package/dist/esm/schema/types/collection.js +1 -1
  111. package/dist/esm/schema/types/compounds.d.ts +12 -12
  112. package/dist/esm/schema/types/compounds.js +1 -1
  113. package/dist/esm/schema/types/fields.d.ts +59 -59
  114. package/dist/esm/schema/types/fields.js +1 -1
  115. package/dist/esm/schema/types/filters.d.ts +29 -29
  116. package/dist/esm/schema/types/filters.js +1 -1
  117. package/dist/esm/schema/types/shapes.d.ts +21 -21
  118. package/dist/esm/schema/types/shapes.js +1 -1
  119. package/dist/esm/schema/types/synthetics.d.ts +28 -28
  120. package/dist/esm/schema/types/synthetics.js +1 -1
  121. package/dist/esm/schema/types.d.ts +18 -18
  122. package/dist/esm/schema/types.js +6 -6
  123. package/dist/esm/timestamp.d.ts +48 -48
  124. package/dist/esm/timestamp.js +228 -228
  125. package/dist/esm/timestamp.test.d.ts +1 -1
  126. package/dist/esm/timestamp.test.js +91 -91
  127. package/dist/esm/undo.d.ts +3 -3
  128. package/dist/esm/undo.js +194 -194
  129. package/dist/esm/utils.d.ts +15 -15
  130. package/dist/esm/utils.js +113 -113
  131. package/dist/esm/utils.test.d.ts +1 -1
  132. package/dist/esm/utils.test.js +34 -34
  133. package/dist/tsconfig-cjs.tsbuildinfo +1 -1
  134. package/dist/tsconfig.tsbuildinfo +1 -1
  135. package/package.json +1 -1
  136. package/dist/cjs/files.test.d.ts +0 -1
  137. package/dist/cjs/files.test.js +0 -81
  138. package/dist/cjs/files.test.js.map +0 -1
  139. package/dist/esm/files.test.d.ts +0 -1
  140. package/dist/esm/files.test.js +0 -79
  141. package/dist/esm/files.test.js.map +0 -1
@@ -1,518 +1,518 @@
1
- import { assignOid, assignOidsToAllSubObjects, createRef, ensureOid, getOid, getOidRoot, isOidKey, maybeGetOid, normalize, removeOid, } from './oids.js';
2
- import { compareRefs, isRef } from './refs.js';
3
- import { isObject, assert, cloneDeep, findLastIndex } from './utils.js';
4
- export function isObjectRef(obj) {
5
- return obj && typeof obj === 'object' && obj['@@type'] === 'ref';
6
- }
7
- function isDiffableObject(val) {
8
- return isObject(val) && !isRef(val);
9
- }
10
- function compareNonDiffable(a, b) {
11
- if (a === b)
12
- return true;
13
- if (isRef(a) && isRef(b))
14
- return compareRefs(a, b);
15
- return false;
16
- }
17
- export function diffToPatches(from, to, getNow, keyPath, createSubId, patches = [], options = {}) {
18
- const oid = getOid(from);
19
- function diffItems(key, value, oldValue) {
20
- if (!isDiffableObject(value)) {
21
- // for primitive fields, we can use plain sets and
22
- // do not need to recurse, of course
23
- if (!compareNonDiffable(value, oldValue)) {
24
- patches.push({
25
- oid,
26
- timestamp: getNow(),
27
- data: {
28
- op: 'set',
29
- name: key,
30
- value,
31
- },
32
- });
33
- }
34
- }
35
- else {
36
- const oldValueOid = maybeGetOid(oldValue);
37
- let valueOid;
38
- if (!options.mergeUnknownObjects) {
39
- valueOid = ensureOid(value, oid, key, createSubId);
40
- }
41
- else {
42
- // if merge unknown objects is requested, we copy the previous value's oid
43
- // to any mirrored new value if it doesn't have one assigned already.
44
- valueOid = maybeGetOid(value);
45
- if (!valueOid && oldValueOid) {
46
- assignOid(value, oldValueOid);
47
- valueOid = oldValueOid;
48
- }
49
- else {
50
- valueOid = ensureOid(value, oid, key, createSubId);
51
- }
52
- }
53
- if (oldValue === undefined || valueOid !== oldValueOid) {
54
- // first case: previous value exists but the OIDs are different,
55
- // meaning the object identity has changed
56
- // we add the whole new object and also update the reference on this
57
- // key of the parent to point to the new object
58
- initialToPatches(value, valueOid, getNow, createSubId, patches);
59
- patches.push({
60
- oid,
61
- timestamp: getNow(),
62
- data: {
63
- op: 'set',
64
- name: key,
65
- value: createRef(valueOid),
66
- },
67
- });
68
- // if there was an old value, we need to delete it altogether.
69
- if (oldValueOid !== undefined) {
70
- patches.push({
71
- oid: oldValueOid,
72
- timestamp: getNow(),
73
- data: {
74
- op: 'delete',
75
- },
76
- });
77
- }
78
- }
79
- else {
80
- // third case: OIDs are the same, meaning the identity is the same,
81
- // and we must diff the objects
82
- diffToPatches(oldValue, value, getNow, [...keyPath, key], createSubId, patches, options);
83
- }
84
- }
85
- }
86
- if (Array.isArray(from) && Array.isArray(to)) {
87
- // diffing is more naive than native array operations.
88
- // we can only look at each element and decide if it should
89
- // be replaced or removed - no moves or pushes, etc.
90
- for (let i = 0; i < to.length; i++) {
91
- const value = to[i];
92
- const oldValue = from[i];
93
- diffItems(i, value, oldValue);
94
- }
95
- // remove any remaining items at the end of the array
96
- const deletedItemsAtEnd = from.length - to.length;
97
- if (deletedItemsAtEnd > 0) {
98
- // if sub-items were objects, we need to delete them all
99
- for (let i = to.length; i < from.length; i++) {
100
- const value = from[i];
101
- const valueOid = maybeGetOid(value);
102
- if (valueOid) {
103
- patches.push({
104
- oid: valueOid,
105
- timestamp: getNow(),
106
- data: {
107
- op: 'delete',
108
- },
109
- });
110
- }
111
- }
112
- // push the list-delete for the deleted items
113
- patches.push({
114
- oid,
115
- timestamp: getNow(),
116
- data: {
117
- op: 'list-delete',
118
- index: to.length,
119
- count: deletedItemsAtEnd,
120
- },
121
- });
122
- }
123
- }
124
- else if (Array.isArray(from) || Array.isArray(to)) {
125
- throw new Error('Cannot diff an array with an object');
126
- }
127
- else if (isDiffableObject(from) && isDiffableObject(to)) {
128
- const oldKeys = new Set(Object.keys(from));
129
- for (const [key, value] of Object.entries(to)) {
130
- if (value === undefined && options.defaultUndefined)
131
- continue;
132
- oldKeys.delete(key);
133
- if (isOidKey(key))
134
- continue;
135
- const oldValue = from[key];
136
- diffItems(key, value, oldValue);
137
- }
138
- // this set now only contains keys which were not in the new object
139
- if (!options.defaultUndefined) {
140
- for (const key of oldKeys) {
141
- if (isOidKey(key))
142
- continue;
143
- // push the delete for the property
144
- patches.push({
145
- oid,
146
- timestamp: getNow(),
147
- data: {
148
- op: 'remove',
149
- name: key,
150
- },
151
- });
152
- }
153
- }
154
- }
155
- return patches;
156
- }
157
- export function shallowDiffToPatches(from, to, getNow, patches = []) {
158
- const oid = getOid(from);
159
- function diffItems(key, value, oldValue) {
160
- if (!isDiffableObject(value)) {
161
- // for primitive fields, we can use plain sets and
162
- // do not need to recurse, of course
163
- if (!compareNonDiffable(value, oldValue)) {
164
- patches.push({
165
- oid,
166
- timestamp: getNow(),
167
- data: {
168
- op: 'set',
169
- name: key,
170
- value,
171
- },
172
- });
173
- }
174
- }
175
- else {
176
- throw new Error('Shallow diff was given a nested object. This is an error in verdant!');
177
- }
178
- }
179
- if (Array.isArray(from) && Array.isArray(to)) {
180
- // diffing is more naive than native array operations.
181
- // we can only look at each element and decide if it should
182
- // be replaced or removed - no moves or pushes, etc.
183
- for (let i = 0; i < to.length; i++) {
184
- const value = to[i];
185
- const oldValue = from[i];
186
- diffItems(i, value, oldValue);
187
- }
188
- // remove any remaining items at the end of the array
189
- const deletedItemsAtEnd = from.length - to.length;
190
- if (deletedItemsAtEnd > 0) {
191
- // push the list-delete for the deleted items
192
- patches.push({
193
- oid,
194
- timestamp: getNow(),
195
- data: {
196
- op: 'list-delete',
197
- index: to.length,
198
- count: deletedItemsAtEnd,
199
- },
200
- });
201
- }
202
- }
203
- else if (Array.isArray(from) || Array.isArray(to)) {
204
- throw new Error('Cannot diff an array with an object');
205
- }
206
- else if (isDiffableObject(from) && isDiffableObject(to)) {
207
- const oldKeys = new Set(Object.keys(from));
208
- for (const [key, value] of Object.entries(to)) {
209
- oldKeys.delete(key);
210
- if (isOidKey(key))
211
- continue;
212
- const oldValue = from[key];
213
- diffItems(key, value, oldValue);
214
- }
215
- // this set now only contains keys which were not in the new object
216
- for (const key of oldKeys) {
217
- if (isOidKey(key))
218
- continue;
219
- // push the delete for the property
220
- patches.push({
221
- oid,
222
- timestamp: getNow(),
223
- data: {
224
- op: 'remove',
225
- name: key,
226
- },
227
- });
228
- }
229
- }
230
- return patches;
231
- }
232
- /**
233
- * Takes a basic object and constructs a patch list to create it and
234
- * all of its nested objects.
235
- */
236
- export function initialToPatches(initial, rootOid, getNow, createSubId, patches = []) {
237
- assignOid(initial, rootOid);
238
- assignOidsToAllSubObjects(initial, createSubId);
239
- const normalized = normalize(initial);
240
- for (const key of normalized.keys()) {
241
- const value = normalized.get(key);
242
- patches.push({
243
- oid: key,
244
- timestamp: getNow(),
245
- data: {
246
- op: 'initialize',
247
- value: removeOid(value),
248
- },
249
- });
250
- }
251
- return patches;
252
- }
253
- export function shallowInitialToPatches(initial, rootOid, getNow, patches = []) {
254
- patches.push({
255
- oid: rootOid,
256
- timestamp: getNow(),
257
- data: {
258
- op: 'initialize',
259
- value: initial,
260
- },
261
- });
262
- return patches;
263
- }
264
- export function groupPatchesByIdentifier(patches) {
265
- const grouped = {};
266
- for (const patch of patches) {
267
- if (patch.oid in grouped) {
268
- grouped[patch.oid].push(patch);
269
- }
270
- else {
271
- grouped[patch.oid] = [patch];
272
- }
273
- }
274
- return grouped;
275
- }
276
- export function groupPatchesByRootOid(patches) {
277
- const grouped = {};
278
- for (const patch of patches) {
279
- const root = getOidRoot(patch.oid);
280
- if (root in grouped) {
281
- grouped[root].push(patch);
282
- }
283
- else {
284
- grouped[root] = [patch];
285
- }
286
- }
287
- return grouped;
288
- }
289
- export function groupBaselinesByRootOid(baselines) {
290
- const grouped = {};
291
- for (const patch of baselines) {
292
- const root = getOidRoot(patch.oid);
293
- if (root in grouped) {
294
- grouped[root].push(patch);
295
- }
296
- else {
297
- grouped[root] = [patch];
298
- }
299
- }
300
- return grouped;
301
- }
302
- function listCheck(obj) {
303
- if (!Array.isArray(obj)) {
304
- console.error(`Cannot apply list patch; expected array, received ${JSON.stringify(obj)}. This suggests your data is changing from a list to an object over time. (OID: ${maybeGetOid(obj)})`);
305
- return false;
306
- }
307
- else {
308
- return true;
309
- }
310
- }
311
- /**
312
- * The incoming object should already be normalized!
313
- * This function will mutate the base object.
314
- */
315
- export function applyPatch(base, patch,
316
- /**
317
- * Optionally supply a list to which any refs which
318
- * are removed during the patch will be appended.
319
- */
320
- deletedRefs) {
321
- // deleted objects are represented by undefined
322
- // and remain deleted unless re-initialized
323
- if ((base === undefined || base === null) && patch.op !== 'initialize') {
324
- return base;
325
- }
326
- // ditch typing internally.
327
- const baseAsAny = base;
328
- let index;
329
- let spliceResult;
330
- // a helper, pass it a value which is about
331
- // to be removed and it will append it to the list
332
- // if it was a ref
333
- function checkRef(field) {
334
- // don't bother if not supplied
335
- if (!deletedRefs)
336
- return;
337
- if (isRef(field)) {
338
- deletedRefs.push(field);
339
- }
340
- }
341
- switch (patch.op) {
342
- case 'set':
343
- checkRef(baseAsAny[patch.name]);
344
- baseAsAny[patch.name] = patch.value;
345
- break;
346
- case 'remove':
347
- checkRef(baseAsAny[patch.name]);
348
- delete baseAsAny[patch.name];
349
- break;
350
- case 'list-push':
351
- if (listCheck(base)) {
352
- base.push(patch.value);
353
- }
354
- break;
355
- case 'list-delete':
356
- if (listCheck(base)) {
357
- checkRef(base[patch.index]);
358
- base.splice(patch.index, patch.count);
359
- }
360
- break;
361
- case 'list-move-by-index':
362
- if (listCheck(base)) {
363
- spliceResult = base.splice(patch.from, 1);
364
- base.splice(patch.to, 0, spliceResult[0]);
365
- }
366
- break;
367
- case 'list-remove':
368
- if (listCheck(base)) {
369
- do {
370
- const valueToRemove = patch.value;
371
- if (patch.only === 'last') {
372
- if (isObjectRef(valueToRemove)) {
373
- index = findLastIndex(base, (item) => item.id === valueToRemove.id);
374
- }
375
- else {
376
- index = base.lastIndexOf(valueToRemove);
377
- }
378
- }
379
- else {
380
- if (isObjectRef(valueToRemove)) {
381
- index = base.findIndex((item) => item.id === valueToRemove.id);
382
- }
383
- else {
384
- index = base.indexOf(valueToRemove);
385
- }
386
- }
387
- if (index !== -1) {
388
- checkRef(base[index]);
389
- base.splice(index, 1);
390
- }
391
- } while (!patch.only && index !== -1);
392
- }
393
- break;
394
- case 'list-add':
395
- if (listCheck(base)) {
396
- const alreadyHas = base.some((item) => {
397
- if (isObjectRef(item) && isObjectRef(patch.value)) {
398
- return item.id === patch.value.id;
399
- }
400
- else {
401
- return item === patch.value;
402
- }
403
- });
404
- if (!alreadyHas) {
405
- base.push(patch.value);
406
- }
407
- }
408
- break;
409
- case 'list-move-by-ref':
410
- if (listCheck(base)) {
411
- index = base.indexOf(patch.value);
412
- spliceResult = base.splice(index, 1);
413
- base.splice(patch.index, 0, spliceResult[0]);
414
- }
415
- break;
416
- case 'list-insert':
417
- if (listCheck(base)) {
418
- if (!patch.value && !patch.values) {
419
- throw new Error(`Cannot apply list insert patch; expected value or values, received ${JSON.stringify(patch)}`);
420
- }
421
- if (patch.value) {
422
- base.splice(patch.index, 0, patch.value);
423
- }
424
- else {
425
- base.splice(patch.index, 0, ...patch.values);
426
- }
427
- }
428
- break;
429
- case 'delete':
430
- // collect all refs that are deleted
431
- if (Array.isArray(base)) {
432
- base.forEach(checkRef);
433
- }
434
- else if (isObject(base)) {
435
- Object.values(base || {}).forEach(checkRef);
436
- }
437
- return undefined;
438
- case 'initialize':
439
- return cloneDeep(patch.value);
440
- default:
441
- throw new Error(`Unsupported patch operation: ${patch.op}`);
442
- }
443
- return base;
444
- }
445
- export function applyOperations(base, operations, deletedRefs) {
446
- let cur = base;
447
- for (const op of operations) {
448
- cur = applyPatch(cur, op.data, deletedRefs);
449
- }
450
- return cur;
451
- }
452
- /**
453
- * Mutates the original object in place. Returns all referenced
454
- * objects oids.
455
- */
456
- export function substituteRefsWithObjects(base, refs, used = []) {
457
- if (Array.isArray(base)) {
458
- for (let i = 0; i < base.length; i++) {
459
- const item = base[i];
460
- base[i] = dereference(item, refs, used);
461
- if (isObject(base[i])) {
462
- substituteRefsWithObjects(base[i], refs, used);
463
- }
464
- }
465
- }
466
- else if (isObject(base)) {
467
- // not sure where to put this assertion but it's important to make
468
- // sure all nested objects include an OID
469
- assert(maybeGetOid(base), `Object ${JSON.stringify(base)} must have an oid`);
470
- for (const key of Object.keys(base)) {
471
- base[key] = dereference(base[key], refs, used);
472
- // now that objects are in place, recursively substitute
473
- if (isObject(base[key])) {
474
- substituteRefsWithObjects(base[key], refs, used);
475
- }
476
- }
477
- }
478
- return used;
479
- }
480
- function dereference(input, refs, used) {
481
- if (isObjectRef(input)) {
482
- used.push(input.id);
483
- const resolved = refs.get(input.id);
484
- assert(!!resolved, `No value was found in object map for ${input.id}`);
485
- return assignOid(resolved, input.id);
486
- }
487
- else {
488
- return input;
489
- }
490
- }
491
- export function substituteFirstLevelObjectsWithRefs(base, refObjects = new Map()) {
492
- if (Array.isArray(base)) {
493
- for (let i = 0; i < base.length; i++) {
494
- const item = base[i];
495
- if (isObject(item)) {
496
- const oid = getOid(item);
497
- base[i] = {
498
- '@@type': 'ref',
499
- id: oid,
500
- };
501
- refObjects.set(oid, item);
502
- }
503
- }
504
- }
505
- else {
506
- for (const [key, value] of Object.entries(base)) {
507
- if (isObject(value)) {
508
- base[key] = {
509
- '@@type': 'ref',
510
- id: getOid(value),
511
- };
512
- refObjects.set(getOid(value), value);
513
- }
514
- }
515
- }
516
- return refObjects;
517
- }
1
+ import { assignOid, assignOidsToAllSubObjects, createRef, ensureOid, getOid, getOidRoot, isOidKey, maybeGetOid, normalize, removeOid, } from './oids.js';
2
+ import { compareRefs, isRef } from './refs.js';
3
+ import { isObject, assert, cloneDeep, findLastIndex } from './utils.js';
4
+ export function isObjectRef(obj) {
5
+ return obj && typeof obj === 'object' && obj['@@type'] === 'ref';
6
+ }
7
+ function isDiffableObject(val) {
8
+ return isObject(val) && !isRef(val);
9
+ }
10
+ function compareNonDiffable(a, b) {
11
+ if (a === b)
12
+ return true;
13
+ if (isRef(a) && isRef(b))
14
+ return compareRefs(a, b);
15
+ return false;
16
+ }
17
+ export function diffToPatches(from, to, getNow, keyPath, createSubId, patches = [], options = {}) {
18
+ const oid = getOid(from);
19
+ function diffItems(key, value, oldValue) {
20
+ if (!isDiffableObject(value)) {
21
+ // for primitive fields, we can use plain sets and
22
+ // do not need to recurse, of course
23
+ if (!compareNonDiffable(value, oldValue)) {
24
+ patches.push({
25
+ oid,
26
+ timestamp: getNow(),
27
+ data: {
28
+ op: 'set',
29
+ name: key,
30
+ value,
31
+ },
32
+ });
33
+ }
34
+ }
35
+ else {
36
+ const oldValueOid = maybeGetOid(oldValue);
37
+ let valueOid;
38
+ if (!options.mergeUnknownObjects) {
39
+ valueOid = ensureOid(value, oid, key, createSubId);
40
+ }
41
+ else {
42
+ // if merge unknown objects is requested, we copy the previous value's oid
43
+ // to any mirrored new value if it doesn't have one assigned already.
44
+ valueOid = maybeGetOid(value);
45
+ if (!valueOid && oldValueOid) {
46
+ assignOid(value, oldValueOid);
47
+ valueOid = oldValueOid;
48
+ }
49
+ else {
50
+ valueOid = ensureOid(value, oid, key, createSubId);
51
+ }
52
+ }
53
+ if (oldValue === undefined || valueOid !== oldValueOid) {
54
+ // first case: previous value exists but the OIDs are different,
55
+ // meaning the object identity has changed
56
+ // we add the whole new object and also update the reference on this
57
+ // key of the parent to point to the new object
58
+ initialToPatches(value, valueOid, getNow, createSubId, patches);
59
+ patches.push({
60
+ oid,
61
+ timestamp: getNow(),
62
+ data: {
63
+ op: 'set',
64
+ name: key,
65
+ value: createRef(valueOid),
66
+ },
67
+ });
68
+ // if there was an old value, we need to delete it altogether.
69
+ if (oldValueOid !== undefined) {
70
+ patches.push({
71
+ oid: oldValueOid,
72
+ timestamp: getNow(),
73
+ data: {
74
+ op: 'delete',
75
+ },
76
+ });
77
+ }
78
+ }
79
+ else {
80
+ // third case: OIDs are the same, meaning the identity is the same,
81
+ // and we must diff the objects
82
+ diffToPatches(oldValue, value, getNow, [...keyPath, key], createSubId, patches, options);
83
+ }
84
+ }
85
+ }
86
+ if (Array.isArray(from) && Array.isArray(to)) {
87
+ // diffing is more naive than native array operations.
88
+ // we can only look at each element and decide if it should
89
+ // be replaced or removed - no moves or pushes, etc.
90
+ for (let i = 0; i < to.length; i++) {
91
+ const value = to[i];
92
+ const oldValue = from[i];
93
+ diffItems(i, value, oldValue);
94
+ }
95
+ // remove any remaining items at the end of the array
96
+ const deletedItemsAtEnd = from.length - to.length;
97
+ if (deletedItemsAtEnd > 0) {
98
+ // if sub-items were objects, we need to delete them all
99
+ for (let i = to.length; i < from.length; i++) {
100
+ const value = from[i];
101
+ const valueOid = maybeGetOid(value);
102
+ if (valueOid) {
103
+ patches.push({
104
+ oid: valueOid,
105
+ timestamp: getNow(),
106
+ data: {
107
+ op: 'delete',
108
+ },
109
+ });
110
+ }
111
+ }
112
+ // push the list-delete for the deleted items
113
+ patches.push({
114
+ oid,
115
+ timestamp: getNow(),
116
+ data: {
117
+ op: 'list-delete',
118
+ index: to.length,
119
+ count: deletedItemsAtEnd,
120
+ },
121
+ });
122
+ }
123
+ }
124
+ else if (Array.isArray(from) || Array.isArray(to)) {
125
+ throw new Error('Cannot diff an array with an object');
126
+ }
127
+ else if (isDiffableObject(from) && isDiffableObject(to)) {
128
+ const oldKeys = new Set(Object.keys(from));
129
+ for (const [key, value] of Object.entries(to)) {
130
+ if (value === undefined && options.defaultUndefined)
131
+ continue;
132
+ oldKeys.delete(key);
133
+ if (isOidKey(key))
134
+ continue;
135
+ const oldValue = from[key];
136
+ diffItems(key, value, oldValue);
137
+ }
138
+ // this set now only contains keys which were not in the new object
139
+ if (!options.defaultUndefined) {
140
+ for (const key of oldKeys) {
141
+ if (isOidKey(key))
142
+ continue;
143
+ // push the delete for the property
144
+ patches.push({
145
+ oid,
146
+ timestamp: getNow(),
147
+ data: {
148
+ op: 'remove',
149
+ name: key,
150
+ },
151
+ });
152
+ }
153
+ }
154
+ }
155
+ return patches;
156
+ }
157
+ export function shallowDiffToPatches(from, to, getNow, patches = []) {
158
+ const oid = getOid(from);
159
+ function diffItems(key, value, oldValue) {
160
+ if (!isDiffableObject(value)) {
161
+ // for primitive fields, we can use plain sets and
162
+ // do not need to recurse, of course
163
+ if (!compareNonDiffable(value, oldValue)) {
164
+ patches.push({
165
+ oid,
166
+ timestamp: getNow(),
167
+ data: {
168
+ op: 'set',
169
+ name: key,
170
+ value,
171
+ },
172
+ });
173
+ }
174
+ }
175
+ else {
176
+ throw new Error('Shallow diff was given a nested object. This is an error in verdant!');
177
+ }
178
+ }
179
+ if (Array.isArray(from) && Array.isArray(to)) {
180
+ // diffing is more naive than native array operations.
181
+ // we can only look at each element and decide if it should
182
+ // be replaced or removed - no moves or pushes, etc.
183
+ for (let i = 0; i < to.length; i++) {
184
+ const value = to[i];
185
+ const oldValue = from[i];
186
+ diffItems(i, value, oldValue);
187
+ }
188
+ // remove any remaining items at the end of the array
189
+ const deletedItemsAtEnd = from.length - to.length;
190
+ if (deletedItemsAtEnd > 0) {
191
+ // push the list-delete for the deleted items
192
+ patches.push({
193
+ oid,
194
+ timestamp: getNow(),
195
+ data: {
196
+ op: 'list-delete',
197
+ index: to.length,
198
+ count: deletedItemsAtEnd,
199
+ },
200
+ });
201
+ }
202
+ }
203
+ else if (Array.isArray(from) || Array.isArray(to)) {
204
+ throw new Error('Cannot diff an array with an object');
205
+ }
206
+ else if (isDiffableObject(from) && isDiffableObject(to)) {
207
+ const oldKeys = new Set(Object.keys(from));
208
+ for (const [key, value] of Object.entries(to)) {
209
+ oldKeys.delete(key);
210
+ if (isOidKey(key))
211
+ continue;
212
+ const oldValue = from[key];
213
+ diffItems(key, value, oldValue);
214
+ }
215
+ // this set now only contains keys which were not in the new object
216
+ for (const key of oldKeys) {
217
+ if (isOidKey(key))
218
+ continue;
219
+ // push the delete for the property
220
+ patches.push({
221
+ oid,
222
+ timestamp: getNow(),
223
+ data: {
224
+ op: 'remove',
225
+ name: key,
226
+ },
227
+ });
228
+ }
229
+ }
230
+ return patches;
231
+ }
232
+ /**
233
+ * Takes a basic object and constructs a patch list to create it and
234
+ * all of its nested objects.
235
+ */
236
+ export function initialToPatches(initial, rootOid, getNow, createSubId, patches = []) {
237
+ assignOid(initial, rootOid);
238
+ assignOidsToAllSubObjects(initial, createSubId);
239
+ const normalized = normalize(initial);
240
+ for (const key of normalized.keys()) {
241
+ const value = normalized.get(key);
242
+ patches.push({
243
+ oid: key,
244
+ timestamp: getNow(),
245
+ data: {
246
+ op: 'initialize',
247
+ value: removeOid(value),
248
+ },
249
+ });
250
+ }
251
+ return patches;
252
+ }
253
+ export function shallowInitialToPatches(initial, rootOid, getNow, patches = []) {
254
+ patches.push({
255
+ oid: rootOid,
256
+ timestamp: getNow(),
257
+ data: {
258
+ op: 'initialize',
259
+ value: initial,
260
+ },
261
+ });
262
+ return patches;
263
+ }
264
+ export function groupPatchesByIdentifier(patches) {
265
+ const grouped = {};
266
+ for (const patch of patches) {
267
+ if (patch.oid in grouped) {
268
+ grouped[patch.oid].push(patch);
269
+ }
270
+ else {
271
+ grouped[patch.oid] = [patch];
272
+ }
273
+ }
274
+ return grouped;
275
+ }
276
+ export function groupPatchesByRootOid(patches) {
277
+ const grouped = {};
278
+ for (const patch of patches) {
279
+ const root = getOidRoot(patch.oid);
280
+ if (root in grouped) {
281
+ grouped[root].push(patch);
282
+ }
283
+ else {
284
+ grouped[root] = [patch];
285
+ }
286
+ }
287
+ return grouped;
288
+ }
289
+ export function groupBaselinesByRootOid(baselines) {
290
+ const grouped = {};
291
+ for (const patch of baselines) {
292
+ const root = getOidRoot(patch.oid);
293
+ if (root in grouped) {
294
+ grouped[root].push(patch);
295
+ }
296
+ else {
297
+ grouped[root] = [patch];
298
+ }
299
+ }
300
+ return grouped;
301
+ }
302
+ function listCheck(obj) {
303
+ if (!Array.isArray(obj)) {
304
+ console.error(`Cannot apply list patch; expected array, received ${JSON.stringify(obj)}. This suggests your data is changing from a list to an object over time. (OID: ${maybeGetOid(obj)})`);
305
+ return false;
306
+ }
307
+ else {
308
+ return true;
309
+ }
310
+ }
311
+ /**
312
+ * The incoming object should already be normalized!
313
+ * This function will mutate the base object.
314
+ */
315
+ export function applyPatch(base, patch,
316
+ /**
317
+ * Optionally supply a list to which any refs which
318
+ * are removed during the patch will be appended.
319
+ */
320
+ deletedRefs) {
321
+ // deleted objects are represented by undefined
322
+ // and remain deleted unless re-initialized
323
+ if ((base === undefined || base === null) && patch.op !== 'initialize') {
324
+ return base;
325
+ }
326
+ // ditch typing internally.
327
+ const baseAsAny = base;
328
+ let index;
329
+ let spliceResult;
330
+ // a helper, pass it a value which is about
331
+ // to be removed and it will append it to the list
332
+ // if it was a ref
333
+ function checkRef(field) {
334
+ // don't bother if not supplied
335
+ if (!deletedRefs)
336
+ return;
337
+ if (isRef(field)) {
338
+ deletedRefs.push(field);
339
+ }
340
+ }
341
+ switch (patch.op) {
342
+ case 'set':
343
+ checkRef(baseAsAny[patch.name]);
344
+ baseAsAny[patch.name] = patch.value;
345
+ break;
346
+ case 'remove':
347
+ checkRef(baseAsAny[patch.name]);
348
+ delete baseAsAny[patch.name];
349
+ break;
350
+ case 'list-push':
351
+ if (listCheck(base)) {
352
+ base.push(patch.value);
353
+ }
354
+ break;
355
+ case 'list-delete':
356
+ if (listCheck(base)) {
357
+ checkRef(base[patch.index]);
358
+ base.splice(patch.index, patch.count);
359
+ }
360
+ break;
361
+ case 'list-move-by-index':
362
+ if (listCheck(base)) {
363
+ spliceResult = base.splice(patch.from, 1);
364
+ base.splice(patch.to, 0, spliceResult[0]);
365
+ }
366
+ break;
367
+ case 'list-remove':
368
+ if (listCheck(base)) {
369
+ do {
370
+ const valueToRemove = patch.value;
371
+ if (patch.only === 'last') {
372
+ if (isObjectRef(valueToRemove)) {
373
+ index = findLastIndex(base, (item) => item.id === valueToRemove.id);
374
+ }
375
+ else {
376
+ index = base.lastIndexOf(valueToRemove);
377
+ }
378
+ }
379
+ else {
380
+ if (isObjectRef(valueToRemove)) {
381
+ index = base.findIndex((item) => item.id === valueToRemove.id);
382
+ }
383
+ else {
384
+ index = base.indexOf(valueToRemove);
385
+ }
386
+ }
387
+ if (index !== -1) {
388
+ checkRef(base[index]);
389
+ base.splice(index, 1);
390
+ }
391
+ } while (!patch.only && index !== -1);
392
+ }
393
+ break;
394
+ case 'list-add':
395
+ if (listCheck(base)) {
396
+ const alreadyHas = base.some((item) => {
397
+ if (isObjectRef(item) && isObjectRef(patch.value)) {
398
+ return item.id === patch.value.id;
399
+ }
400
+ else {
401
+ return item === patch.value;
402
+ }
403
+ });
404
+ if (!alreadyHas) {
405
+ base.push(patch.value);
406
+ }
407
+ }
408
+ break;
409
+ case 'list-move-by-ref':
410
+ if (listCheck(base)) {
411
+ index = base.indexOf(patch.value);
412
+ spliceResult = base.splice(index, 1);
413
+ base.splice(patch.index, 0, spliceResult[0]);
414
+ }
415
+ break;
416
+ case 'list-insert':
417
+ if (listCheck(base)) {
418
+ if (!patch.value && !patch.values) {
419
+ throw new Error(`Cannot apply list insert patch; expected value or values, received ${JSON.stringify(patch)}`);
420
+ }
421
+ if (patch.value) {
422
+ base.splice(patch.index, 0, patch.value);
423
+ }
424
+ else {
425
+ base.splice(patch.index, 0, ...patch.values);
426
+ }
427
+ }
428
+ break;
429
+ case 'delete':
430
+ // collect all refs that are deleted
431
+ if (Array.isArray(base)) {
432
+ base.forEach(checkRef);
433
+ }
434
+ else if (isObject(base)) {
435
+ Object.values(base || {}).forEach(checkRef);
436
+ }
437
+ return undefined;
438
+ case 'initialize':
439
+ return cloneDeep(patch.value);
440
+ default:
441
+ throw new Error(`Unsupported patch operation: ${patch.op}`);
442
+ }
443
+ return base;
444
+ }
445
+ export function applyOperations(base, operations, deletedRefs) {
446
+ let cur = base;
447
+ for (const op of operations) {
448
+ cur = applyPatch(cur, op.data, deletedRefs);
449
+ }
450
+ return cur;
451
+ }
452
+ /**
453
+ * Mutates the original object in place. Returns all referenced
454
+ * objects oids.
455
+ */
456
+ export function substituteRefsWithObjects(base, refs, used = []) {
457
+ if (Array.isArray(base)) {
458
+ for (let i = 0; i < base.length; i++) {
459
+ const item = base[i];
460
+ base[i] = dereference(item, refs, used);
461
+ if (isObject(base[i])) {
462
+ substituteRefsWithObjects(base[i], refs, used);
463
+ }
464
+ }
465
+ }
466
+ else if (isObject(base)) {
467
+ // not sure where to put this assertion but it's important to make
468
+ // sure all nested objects include an OID
469
+ assert(maybeGetOid(base), `Object ${JSON.stringify(base)} must have an oid`);
470
+ for (const key of Object.keys(base)) {
471
+ base[key] = dereference(base[key], refs, used);
472
+ // now that objects are in place, recursively substitute
473
+ if (isObject(base[key])) {
474
+ substituteRefsWithObjects(base[key], refs, used);
475
+ }
476
+ }
477
+ }
478
+ return used;
479
+ }
480
+ function dereference(input, refs, used) {
481
+ if (isObjectRef(input)) {
482
+ used.push(input.id);
483
+ const resolved = refs.get(input.id);
484
+ assert(!!resolved, `No value was found in object map for ${input.id}`);
485
+ return assignOid(resolved, input.id);
486
+ }
487
+ else {
488
+ return input;
489
+ }
490
+ }
491
+ export function substituteFirstLevelObjectsWithRefs(base, refObjects = new Map()) {
492
+ if (Array.isArray(base)) {
493
+ for (let i = 0; i < base.length; i++) {
494
+ const item = base[i];
495
+ if (isObject(item)) {
496
+ const oid = getOid(item);
497
+ base[i] = {
498
+ '@@type': 'ref',
499
+ id: oid,
500
+ };
501
+ refObjects.set(oid, item);
502
+ }
503
+ }
504
+ }
505
+ else {
506
+ for (const [key, value] of Object.entries(base)) {
507
+ if (isObject(value)) {
508
+ base[key] = {
509
+ '@@type': 'ref',
510
+ id: getOid(value),
511
+ };
512
+ refObjects.set(getOid(value), value);
513
+ }
514
+ }
515
+ }
516
+ return refObjects;
517
+ }
518
518
  //# sourceMappingURL=operation.js.map