@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.
package/src/operation.ts CHANGED
@@ -1,12 +1,8 @@
1
1
  import { DocumentBaseline } from './baseline.js';
2
2
  import { FileRef, isFileRef } from './files.js';
3
3
  import {
4
- areOidsRelated,
5
4
  assignOid,
6
5
  assignOidsToAllSubObjects,
7
- createRef,
8
- createSubOid,
9
- ensureOid,
10
6
  getOid,
11
7
  getOidRoot,
12
8
  maybeGetOid,
@@ -14,7 +10,6 @@ import {
14
10
  ObjectIdentifier,
15
11
  removeOid,
16
12
  } from './oids.js';
17
- import { isOidKey } from './oidsLegacy.js';
18
13
  import { compareRefs, isRef } from './refs.js';
19
14
  import { assert, cloneDeep, findLastIndex, isObject } from './utils.js';
20
15
 
@@ -159,371 +154,6 @@ export type Operation = {
159
154
  authz?: string;
160
155
  };
161
156
 
162
- function isDiffableObject(val: any) {
163
- return isObject(val) && !isRef(val);
164
- }
165
-
166
- function compareNonDiffable(a: any, b: any) {
167
- if (a === b) return true;
168
- if (isRef(a) && isRef(b)) return compareRefs(a, b);
169
- return false;
170
- }
171
-
172
- export function diffToPatches<T extends { [key: string]: any } | any[]>(
173
- from: T,
174
- to: T,
175
- getNow: () => string,
176
- createSubId?: () => string,
177
- patches: Operation[] = [],
178
- options?: {
179
- /**
180
- * If an object is merged with another and the new one does not
181
- * have an OID assigned, assume it is the same identity as previous
182
- */
183
- mergeUnknownObjects?: boolean;
184
- /**
185
- * If an incoming value is not assigned on the new object, use the previous value.
186
- * If false, undefined properties will erase the previous value.
187
- */
188
- defaultUndefined?: boolean;
189
- /**
190
- * Authorization to apply to all created operations
191
- */
192
- authz?: string;
193
- },
194
- ): Operation[] {
195
- const authz = options?.authz;
196
- const oid = getOid(from);
197
-
198
- // FIXME: allocating this function inline seems wasteful.
199
- function diffItems(
200
- key: string | number,
201
- value: any,
202
- oldValue: any,
203
- isList: boolean,
204
- ) {
205
- if (!isDiffableObject(value)) {
206
- // for primitive fields, we can use plain sets and
207
- // do not need to recurse, of course
208
- if (!compareNonDiffable(value, oldValue)) {
209
- if (isList) {
210
- patches.push(
211
- addAuthzToOp(
212
- {
213
- oid,
214
- timestamp: getNow(),
215
- data: {
216
- op: 'list-set',
217
- index: key as number,
218
- value,
219
- },
220
- },
221
- authz,
222
- ),
223
- );
224
- } else {
225
- patches.push(
226
- addAuthzToOp(
227
- {
228
- oid,
229
- timestamp: getNow(),
230
- data: {
231
- op: 'set',
232
- name: key,
233
- value,
234
- },
235
- },
236
- authz,
237
- ),
238
- );
239
- }
240
- }
241
- } else {
242
- const oldValueOid = maybeGetOid(oldValue);
243
- let valueOid = maybeGetOid(value);
244
- // the user supplied an object which was already assigned an OID,
245
- // but that OID is from a separate entity: this object must
246
- // be cloned to a new identity.
247
- if (valueOid && !areOidsRelated(oid, valueOid)) {
248
- value = cloneDeep(value, false);
249
- valueOid = createSubOid(oid, createSubId);
250
- assignOid(value, valueOid);
251
- } else if (!options?.mergeUnknownObjects) {
252
- valueOid = ensureOid(value, oid, createSubId);
253
- } else {
254
- // if merge unknown objects is requested, we copy the previous value's oid
255
- // to any mirrored new value if it doesn't have one assigned already.
256
- if (!valueOid && oldValueOid) {
257
- assignOid(value, oldValueOid);
258
- valueOid = oldValueOid;
259
- } else {
260
- valueOid = ensureOid(value, oid, createSubId);
261
- }
262
- }
263
- assert(
264
- !!valueOid,
265
- 'Error: no value OID was resolved during diff. This is a bug in Verdant.',
266
- );
267
- assert(
268
- areOidsRelated(oid, valueOid),
269
- `Error: value OID ${valueOid} is not related to parent OID ${oid}. This is a bug in Verdant.`,
270
- );
271
-
272
- if (oldValue === undefined || valueOid !== oldValueOid) {
273
- // first case: previous value exists but the OIDs are different,
274
- // meaning the object identity has changed
275
- // we add the whole new object and also update the reference on this
276
- // key of the parent to point to the new object
277
- initialToPatches(value, valueOid, getNow, createSubId, patches);
278
- patches.push(
279
- addAuthzToOp(
280
- {
281
- oid,
282
- timestamp: getNow(),
283
- data: {
284
- op: 'set',
285
- name: key,
286
- value: createRef(valueOid),
287
- },
288
- },
289
- authz,
290
- ),
291
- );
292
- // if there was an old value, we need to delete it altogether.
293
- if (oldValueOid !== undefined) {
294
- patches.push(
295
- addAuthzToOp(
296
- {
297
- oid: oldValueOid,
298
- timestamp: getNow(),
299
- data: {
300
- op: 'delete',
301
- },
302
- },
303
- authz,
304
- ),
305
- );
306
- }
307
- } else {
308
- // third case: OIDs are the same, meaning the identity is the same,
309
- // and we must diff the objects
310
- diffToPatches(oldValue, value, getNow, createSubId, patches, options);
311
- }
312
- }
313
- }
314
-
315
- if (Array.isArray(from) && Array.isArray(to)) {
316
- // diffing is more naive than native array operations.
317
- // we can only look at each element and decide if it should
318
- // be replaced or removed - no moves or pushes, etc.
319
- for (let i = 0; i < to.length; i++) {
320
- const value = to[i];
321
- const oldValue = from[i];
322
- diffItems(i, value, oldValue, true);
323
- }
324
- // remove any remaining items at the end of the array
325
- const deletedItemsAtEnd = from.length - to.length;
326
- if (deletedItemsAtEnd > 0) {
327
- // if sub-items were objects, we need to delete them all
328
- for (let i = to.length; i < from.length; i++) {
329
- const value = from[i];
330
- const valueOid = maybeGetOid(value);
331
- if (valueOid) {
332
- patches.push(
333
- addAuthzToOp(
334
- {
335
- oid: valueOid,
336
- timestamp: getNow(),
337
- data: {
338
- op: 'delete',
339
- },
340
- },
341
- authz,
342
- ),
343
- );
344
- }
345
- }
346
- // push the list-delete for the deleted items
347
- patches.push(
348
- addAuthzToOp(
349
- {
350
- oid,
351
- timestamp: getNow(),
352
- data: {
353
- op: 'list-delete',
354
- index: to.length,
355
- count: deletedItemsAtEnd,
356
- },
357
- },
358
- authz,
359
- ),
360
- );
361
- }
362
- } else if (Array.isArray(from) || Array.isArray(to)) {
363
- throw new Error('Cannot diff an array with an object');
364
- } else if (isDiffableObject(from) && isDiffableObject(to)) {
365
- const oldKeys = new Set(Object.keys(from));
366
- for (const [key, value] of Object.entries(to)) {
367
- if (value === undefined && options?.defaultUndefined) continue;
368
-
369
- oldKeys.delete(key);
370
-
371
- if (isOidKey(key)) continue;
372
-
373
- const oldValue = from[key];
374
-
375
- diffItems(key, value, oldValue, false);
376
- }
377
-
378
- // this set now only contains keys which were not in the new object
379
- if (!options?.defaultUndefined) {
380
- for (const key of oldKeys) {
381
- if (isOidKey(key)) continue;
382
- // push the delete for the property
383
- patches.push(
384
- addAuthzToOp(
385
- {
386
- oid,
387
- timestamp: getNow(),
388
- data: {
389
- op: 'remove',
390
- name: key,
391
- },
392
- },
393
- authz,
394
- ),
395
- );
396
- }
397
- }
398
- }
399
-
400
- return patches;
401
- }
402
-
403
- export function shallowDiffToPatches(
404
- from: any,
405
- to: any,
406
- getNow: () => string,
407
- patches: Operation[] = [],
408
- options?: { authz?: string },
409
- ) {
410
- const oid = getOid(from);
411
- const authz = options?.authz;
412
-
413
- function diffItems(
414
- key: string | number,
415
- value: any,
416
- oldValue: any,
417
- isList: boolean,
418
- ) {
419
- if (!isDiffableObject(value)) {
420
- // for primitive fields, we can use plain sets and
421
- // do not need to recurse, of course
422
- if (!compareNonDiffable(value, oldValue)) {
423
- if (isList) {
424
- patches.push(
425
- addAuthzToOp(
426
- {
427
- oid,
428
- timestamp: getNow(),
429
- data: {
430
- op: 'list-set',
431
- index: key as number,
432
- value,
433
- },
434
- },
435
- authz,
436
- ),
437
- );
438
- } else {
439
- patches.push(
440
- addAuthzToOp(
441
- {
442
- oid,
443
- timestamp: getNow(),
444
- data: {
445
- op: 'set',
446
- name: key,
447
- value,
448
- },
449
- },
450
- authz,
451
- ),
452
- );
453
- }
454
- }
455
- } else {
456
- throw new Error(
457
- 'Shallow diff was given a nested object. This is an error in verdant!',
458
- );
459
- }
460
- }
461
-
462
- if (Array.isArray(from) && Array.isArray(to)) {
463
- // diffing is more naive than native array operations.
464
- // we can only look at each element and decide if it should
465
- // be replaced or removed - no moves or pushes, etc.
466
- for (let i = 0; i < to.length; i++) {
467
- const value = to[i];
468
- const oldValue = from[i];
469
- diffItems(i, value, oldValue, true);
470
- }
471
- // remove any remaining items at the end of the array
472
- const deletedItemsAtEnd = from.length - to.length;
473
- if (deletedItemsAtEnd > 0) {
474
- // push the list-delete for the deleted items
475
- patches.push(
476
- addAuthzToOp(
477
- {
478
- oid,
479
- timestamp: getNow(),
480
- data: {
481
- op: 'list-delete',
482
- index: to.length,
483
- count: deletedItemsAtEnd,
484
- },
485
- },
486
- authz,
487
- ),
488
- );
489
- }
490
- } else if (Array.isArray(from) || Array.isArray(to)) {
491
- throw new Error('Cannot diff an array with an object');
492
- } else if (isDiffableObject(from) && isDiffableObject(to)) {
493
- const oldKeys = new Set(Object.keys(from));
494
- for (const [key, value] of Object.entries(to)) {
495
- oldKeys.delete(key);
496
-
497
- if (isOidKey(key)) continue;
498
-
499
- const oldValue = from[key];
500
-
501
- diffItems(key, value, oldValue, false);
502
- }
503
-
504
- // this set now only contains keys which were not in the new object
505
- for (const key of oldKeys) {
506
- if (isOidKey(key)) continue;
507
- // push the delete for the property
508
- patches.push(
509
- addAuthzToOp(
510
- {
511
- oid,
512
- timestamp: getNow(),
513
- data: {
514
- op: 'remove',
515
- name: key,
516
- },
517
- },
518
- authz,
519
- ),
520
- );
521
- }
522
- }
523
-
524
- return patches;
525
- }
526
-
527
157
  /**
528
158
  * Takes a basic object and constructs a patch list to create it and
529
159
  * all of its nested objects.
@@ -575,7 +205,7 @@ export function shallowInitialToPatches(
575
205
 
576
206
  // saves a bit of network traffic by not attaching authz
577
207
  // key at all if not present
578
- function addAuthzToOp(op: Operation, authz?: string) {
208
+ export function addAuthzToOp(op: Operation, authz?: string) {
579
209
  if (authz) {
580
210
  op.authz = authz;
581
211
  }
@@ -875,6 +505,14 @@ export function substituteFirstLevelObjectsWithRefs<
875
505
  if (Array.isArray(base)) {
876
506
  for (let i = 0; i < base.length; i++) {
877
507
  const item = base[i];
508
+ // special handling of file refs, which are treated like primitives
509
+ if (isFileRef(item)) {
510
+ continue;
511
+ }
512
+ assert(
513
+ !isRef(item),
514
+ 'An object with refs was passed to substituteFirstLevelObjectsWithRefs, this is not allowed',
515
+ );
878
516
  if (isObject(item)) {
879
517
  const oid = getOid(item);
880
518
  base[i] = {
@@ -886,6 +524,14 @@ export function substituteFirstLevelObjectsWithRefs<
886
524
  }
887
525
  } else {
888
526
  for (const [key, value] of Object.entries(base)) {
527
+ // special handling of file refs, which are treated like primitives
528
+ if (isFileRef(value)) {
529
+ continue;
530
+ }
531
+ assert(
532
+ !isRef(value),
533
+ 'An object with refs was passed to substituteFirstLevelObjectsWithRefs, this is not allowed',
534
+ );
889
535
  if (isObject(value)) {
890
536
  base[key] = {
891
537
  '@@type': 'ref',
@@ -900,6 +546,26 @@ export function substituteFirstLevelObjectsWithRefs<
900
546
  return refObjects;
901
547
  }
902
548
 
549
+ /**
550
+ * Takes a snapshot with applied OIDs and deconstructs it entirely
551
+ * into individual objects with ref fields. Populates the supplied
552
+ * map with the final objects.
553
+ */
554
+ export function deconstructSnapshotToRefs(
555
+ snapshot: any,
556
+ refTargets: Map<ObjectIdentifier, any>,
557
+ ) {
558
+ if (typeof snapshot !== 'object') {
559
+ return;
560
+ }
561
+ refTargets.set(getOid(snapshot), snapshot);
562
+ const thisLevelOfTargets = substituteFirstLevelObjectsWithRefs(snapshot);
563
+ for (const [oid, obj] of thisLevelOfTargets.entries()) {
564
+ refTargets.set(oid, obj);
565
+ deconstructSnapshotToRefs(obj, refTargets);
566
+ }
567
+ }
568
+
903
569
  export function operationSupersedes(op: Operation): PropertyName | boolean {
904
570
  // todo: add 'remove'
905
571
  if (op.data.op === 'set') {