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