@fluid-experimental/tree 0.59.3003 → 0.59.4000

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 (54) hide show
  1. package/dist/Forest.js +1 -1
  2. package/dist/Forest.js.map +1 -1
  3. package/dist/id-compressor/IdCompressor.d.ts +19 -45
  4. package/dist/id-compressor/IdCompressor.d.ts.map +1 -1
  5. package/dist/id-compressor/IdCompressor.js +151 -151
  6. package/dist/id-compressor/IdCompressor.js.map +1 -1
  7. package/dist/id-compressor/SessionIdNormalizer.d.ts +1 -16
  8. package/dist/id-compressor/SessionIdNormalizer.d.ts.map +1 -1
  9. package/dist/id-compressor/SessionIdNormalizer.js +23 -21
  10. package/dist/id-compressor/SessionIdNormalizer.js.map +1 -1
  11. package/dist/id-compressor/persisted-types/0.0.1.d.ts +23 -4
  12. package/dist/id-compressor/persisted-types/0.0.1.d.ts.map +1 -1
  13. package/dist/id-compressor/persisted-types/0.0.1.js.map +1 -1
  14. package/lib/Forest.js +1 -1
  15. package/lib/Forest.js.map +1 -1
  16. package/lib/id-compressor/IdCompressor.d.ts +19 -45
  17. package/lib/id-compressor/IdCompressor.d.ts.map +1 -1
  18. package/lib/id-compressor/IdCompressor.js +152 -152
  19. package/lib/id-compressor/IdCompressor.js.map +1 -1
  20. package/lib/id-compressor/SessionIdNormalizer.d.ts +1 -16
  21. package/lib/id-compressor/SessionIdNormalizer.d.ts.map +1 -1
  22. package/lib/id-compressor/SessionIdNormalizer.js +23 -21
  23. package/lib/id-compressor/SessionIdNormalizer.js.map +1 -1
  24. package/lib/id-compressor/persisted-types/0.0.1.d.ts +23 -4
  25. package/lib/id-compressor/persisted-types/0.0.1.d.ts.map +1 -1
  26. package/lib/id-compressor/persisted-types/0.0.1.js.map +1 -1
  27. package/lib/test/IdCompressor.perf.tests.js +47 -67
  28. package/lib/test/IdCompressor.perf.tests.js.map +1 -1
  29. package/lib/test/IdCompressor.tests.js +196 -101
  30. package/lib/test/IdCompressor.tests.js.map +1 -1
  31. package/lib/test/Summary.tests.d.ts +0 -1
  32. package/lib/test/Summary.tests.d.ts.map +1 -1
  33. package/lib/test/Summary.tests.js +0 -3
  34. package/lib/test/Summary.tests.js.map +1 -1
  35. package/lib/test/Virtualization.tests.js +0 -4
  36. package/lib/test/Virtualization.tests.js.map +1 -1
  37. package/lib/test/utilities/IdCompressorTestUtilities.d.ts +14 -7
  38. package/lib/test/utilities/IdCompressorTestUtilities.d.ts.map +1 -1
  39. package/lib/test/utilities/IdCompressorTestUtilities.js +40 -20
  40. package/lib/test/utilities/IdCompressorTestUtilities.js.map +1 -1
  41. package/lib/test/utilities/SharedTreeTests.d.ts.map +1 -1
  42. package/lib/test/utilities/SharedTreeTests.js +0 -1
  43. package/lib/test/utilities/SharedTreeTests.js.map +1 -1
  44. package/lib/test/utilities/SummaryLoadPerfTests.js +1 -1
  45. package/lib/test/utilities/SummaryLoadPerfTests.js.map +1 -1
  46. package/lib/test/utilities/TestCommon.d.ts +4 -0
  47. package/lib/test/utilities/TestCommon.d.ts.map +1 -1
  48. package/lib/test/utilities/TestCommon.js +6 -0
  49. package/lib/test/utilities/TestCommon.js.map +1 -1
  50. package/package.json +24 -19
  51. package/src/Forest.ts +1 -1
  52. package/src/id-compressor/IdCompressor.ts +171 -198
  53. package/src/id-compressor/SessionIdNormalizer.ts +29 -41
  54. package/src/id-compressor/persisted-types/0.0.1.ts +25 -4
@@ -31,7 +31,7 @@ import {
31
31
  AttributionId,
32
32
  } from '../Identifiers';
33
33
  import { assertIsStableId, assertIsUuidString, isStableId } from '../UuidUtilities';
34
- import { AppendOnlyDoublySortedMap, AppendOnlySortedMap } from './AppendOnlySortedMap';
34
+ import { AppendOnlySortedMap } from './AppendOnlySortedMap';
35
35
  import { getIds } from './IdRange';
36
36
  import {
37
37
  numericUuidEquals,
@@ -54,6 +54,7 @@ import type {
54
54
  UnackedLocalId,
55
55
  VersionedSerializedIdCompressor,
56
56
  } from './persisted-types';
57
+ import { SessionIdNormalizer } from './SessionIdNormalizer';
57
58
 
58
59
  /**
59
60
  * A cluster of final (sequenced via consensus), sequentially allocated compressed IDs.
@@ -91,6 +92,11 @@ interface IdCluster {
91
92
  * string are created by different sessions before any have been finalized. This can occur due to concurrency or offline.
92
93
  * In this case, the string is stored for the final ID that got sequenced first, and that final ID is stored associated with
93
94
  * all subsequent final IDs with the same override.
95
+ * When a final ID which is safely reserved via consensus as part of a cluster (but is not yet sequenced) is allocated with an
96
+ * override, this collection will be temporarily inaccurate as it will not contain an entry for that final ID. This absence indicates
97
+ * the uncertainty about what the final ID associated with that override will be after finalizing the range (which could change due
98
+ * to unification of a concurrent duplicate override). This table will be adjusted to reflect the override when that final ID is
99
+ * finalized via consensus, and decompression will use `clustersAndOverridesInversion` until that point.
94
100
  */
95
101
  overrides?: Map<FinalCompressedId, string | UnifiedOverride>;
96
102
  }
@@ -154,31 +160,6 @@ export function isLocalId(id: CompressedId): id is LocalCompressedId {
154
160
  return id < 0;
155
161
  }
156
162
 
157
- /**
158
- * A object for retrieving the session-space IDs for a range of IDs.
159
- * Optimized to avoid allocating an array of IDs.
160
- */
161
- export interface IdRange {
162
- /**
163
- * The length of the ID range.
164
- */
165
- readonly length: number;
166
-
167
- /**
168
- * Returns the ID in range at the provided index.
169
- */
170
- get(index: number): SessionSpaceCompressedId;
171
- }
172
-
173
- /**
174
- * A serializable descriptor of a range of session-space IDs.
175
- * The contained IDs must be retrieved by calling `getIdsFromRange`, which returns an `IdRange`.
176
- */
177
- export interface IdRangeDescriptor<TId extends LocalCompressedId | OpSpaceCompressedId> {
178
- readonly first: TId;
179
- readonly count: number;
180
- }
181
-
182
163
  /**
183
164
  * A cluster in `clustersAndOverridesInversion`, which is mapped from the first stable ID in a cluster.
184
165
  */
@@ -352,15 +333,21 @@ export class IdCompressor {
352
333
  private readonly localOverrides = new AppendOnlySortedMap<LocalCompressedId, string>(compareFiniteNumbersReversed);
353
334
 
354
335
  /**
355
- * Maps local IDs to the cluster they belong to (if any). This can be used to efficiently convert a local ID to a
356
- * final ID by finding an entry \<= a given local ID (to find the cluster it is associated with) and checking
357
- * it against `numFinalizedLocalIds`.
336
+ * Maps local IDs to the final ID they are associated with (if any), and maps final IDs to the corresponding local ID (if any).
337
+ * This is used to efficiently compute normalization. This map can be thought of as mapping ranges of "optimistic uncertainty"
338
+ * (local IDs) to the result of consensus (reserved ranges of final IDs, a.k.a. clusters). Any given range of local IDs
339
+ * does not necessarily span an entire cluster, as some session-space IDs may be allocated *after* a cluster has been allocated
340
+ * but before it is full. In this case, there is no uncertainty, as the range of final IDs was reserved when the cluster was created.
341
+ * However, there is always a range of local IDs with size \>= 1 associated with the beginning of every cluster, as clusters are only
342
+ * created *after* they are needed and thus there is some period of uncertainty after local IDs have been handed out but before the
343
+ * range containing them has been finalized. There may also be ranges of local IDs that do not start at the beginning of a
344
+ * cluster; this happens when a cluster is expanded instead of allocating a new one.
345
+ * Additionally, session space IDs associated with an override string will also always be local IDs, because there is uncertainty as
346
+ * to whether another client simultaneously allocated the same override and could get sequenced first (a.k.a. unification) and its
347
+ * final ID would be associated with that override.
348
+ * See `SessionIdNormalizer` for more.
358
349
  */
359
- private readonly localIdToCluster: AppendOnlyDoublySortedMap<
360
- LocalCompressedId,
361
- [FinalCompressedId, IdCluster],
362
- FinalCompressedId
363
- > = new AppendOnlyDoublySortedMap(compareFiniteNumbersReversed, (value) => value[0], compareFiniteNumbers);
350
+ private sessionIdNormalizer = new SessionIdNormalizer<IdCluster>();
364
351
 
365
352
  /**
366
353
  * Contains entries for cluster base UUIDs and override strings (both local and final).
@@ -383,13 +370,6 @@ export class IdCompressor {
383
370
  compareFiniteNumbers
384
371
  );
385
372
 
386
- /**
387
- * Helper comparator for searching append-only sorted maps.
388
- */
389
- private static overrideComparator<T extends number>(search: T, element: readonly [T, unknown]): number {
390
- return compareFiniteNumbers(search, element[0]);
391
- }
392
-
393
373
  /**
394
374
  * @param localSessionId - the `IdCompressor`'s current local session ID.
395
375
  * @param reservedIdCount - the number of IDs that will be known by this compressor without relying on consensus.
@@ -467,11 +447,8 @@ export class IdCompressor {
467
447
  /**
468
448
  * Returns an iterable of all IDs created by this compressor.
469
449
  */
470
- public *getAllIdsFromLocalSession(): IterableIterator<SessionSpaceCompressedId> {
471
- // TODO: this will change when final IDs are returned eagerly
472
- for (let i = 1; i <= this.localIdCount; i++) {
473
- yield -i as LocalCompressedId;
474
- }
450
+ public getAllIdsFromLocalSession(): IterableIterator<SessionSpaceCompressedId> {
451
+ return this.sessionIdNormalizer[Symbol.iterator]();
475
452
  }
476
453
 
477
454
  /**
@@ -482,74 +459,16 @@ export class IdCompressor {
482
459
  if (isLocalId(opSpaceNormalizedId)) {
483
460
  return this.attributionId;
484
461
  }
485
- const [_, cluster] =
486
- this.getClusterForFinalId(opSpaceNormalizedId) ?? fail('Cluster does not exist for final ID');
487
-
488
- return cluster.session.attributionId;
489
- }
490
-
491
- /**
492
- * Provides the session-space IDs corresponding to a range of IDs.
493
- * See `IdRange` for more details.
494
- */
495
- public getIdsFromRange(
496
- rangeDescriptor: IdRangeDescriptor<SessionSpaceCompressedId>,
497
- sessionId: SessionId
498
- ): IdRange {
499
- const { first, count } = rangeDescriptor;
500
- if (sessionId === this.localSessionId) {
501
- return {
502
- length: count,
503
- get: (index: number) => {
504
- if (index < 0 || index >= count) {
505
- fail('Index out of bounds of range.');
506
- }
507
- return (first - index) as LocalCompressedId;
508
- },
509
- };
510
- } else {
511
- const session = this.sessions.get(sessionId) ?? fail('Unknown session, range may not be finalized.');
512
- const firstNumericUuid = incrementUuid(session.sessionUuid, -first - 1);
513
- const firstFinal =
514
- this.compressNumericUuid(firstNumericUuid) ??
515
- fail('Remote range must be finalized before getting IDs.');
516
- assert(
517
- isFinalId(firstFinal),
518
- 'ID from a remote session ID must have final form, as overrides are impossible by definition.'
519
- );
520
- const [baseFinalId, cluster] = this.getClusterForFinalId(firstFinal) ?? fail();
521
- const numIdsRemainingInFirstCluster = cluster.capacity - (firstFinal - baseFinalId);
522
- let pivotFinal: FinalCompressedId | undefined;
523
- if (count > numIdsRemainingInFirstCluster) {
524
- const compressedPivot = this.compressNumericUuid(
525
- incrementUuid(firstNumericUuid, numIdsRemainingInFirstCluster)
526
- );
527
- // Looking up the actual cluster can be avoided, as it is guaranteed that at most one new cluster will be
528
- // created when finalizing a range (regardless of size) due to the expansion optimization.
529
- if (compressedPivot === undefined || isLocalId(compressedPivot)) {
530
- fail(
531
- 'ID from a remote session ID must have final form, as overrides are impossible by definition.'
532
- );
533
- } else {
534
- pivotFinal = compressedPivot;
535
- }
462
+ const closestCluster = this.getClusterForFinalId(opSpaceNormalizedId);
463
+ if (closestCluster === undefined) {
464
+ if (this.sessionIdNormalizer.getCreationIndex(opSpaceNormalizedId) !== undefined) {
465
+ return this.attributionId;
466
+ } else {
467
+ fail('Cluster does not exist for final ID');
536
468
  }
537
-
538
- return {
539
- length: count,
540
- get: (index: number) => {
541
- if (index < 0 || index >= count) {
542
- fail('Index out of bounds of range.');
543
- }
544
- if (index < numIdsRemainingInFirstCluster) {
545
- return (firstFinal + index) as FinalCompressedId & SessionSpaceCompressedId;
546
- } else {
547
- return ((pivotFinal ?? fail('Pivot must exist if range spans clusters.')) +
548
- (index - numIdsRemainingInFirstCluster)) as FinalCompressedId & SessionSpaceCompressedId;
549
- }
550
- },
551
- };
552
469
  }
470
+ const [_, cluster] = closestCluster;
471
+ return cluster.session.attributionId;
553
472
  }
554
473
 
555
474
  /**
@@ -636,19 +555,35 @@ export class IdCompressor {
636
555
  cluster: undefined,
637
556
  clusterBase: undefined,
638
557
  };
558
+ const currentClusterExists = currentCluster !== undefined && currentBaseFinalId !== undefined;
639
559
 
640
- const normalizedLastFinalized = session.lastFinalizedLocalId ?? 0;
641
- const { first: newFirstFinalizedLocalId, last: newLastFinalizedLocalId } = ids;
642
- assert(newFirstFinalizedLocalId === normalizedLastFinalized - 1, 'Ranges finalized out of order.');
560
+ const normalizedLastFinalizedLocal = session.lastFinalizedLocalId ?? 0;
561
+ const { first: newFirstFinalizedLocal, last: newLastFinalizedLocal } = ids;
562
+ assert(newFirstFinalizedLocal === normalizedLastFinalizedLocal - 1, 'Ranges finalized out of order.');
643
563
 
644
564
  // The total number of session-local IDs to finalize
645
- const finalizeCount = normalizedLastFinalized - newLastFinalizedLocalId;
565
+ const finalizeCount = normalizedLastFinalizedLocal - newLastFinalizedLocal;
646
566
  assert(finalizeCount >= 1, 'Cannot finalize an empty range.');
647
567
 
648
568
  let initialClusterCount = 0;
649
569
  let remainingCount = finalizeCount;
650
570
  let newBaseUuid: NumericUuid | undefined;
651
- if (currentCluster !== undefined && currentBaseFinalId !== undefined) {
571
+ if (currentClusterExists) {
572
+ if (isLocal) {
573
+ const lastKnownFinal =
574
+ this.sessionIdNormalizer.getLastFinalId() ??
575
+ fail('Cluster exists but normalizer does not have an entry for it.');
576
+ const lastFinalInCluster = (currentBaseFinalId +
577
+ Math.min(currentCluster.count + finalizeCount, currentCluster.capacity) -
578
+ 1) as FinalCompressedId;
579
+ if (lastFinalInCluster > lastKnownFinal) {
580
+ this.sessionIdNormalizer.addFinalIds(
581
+ (lastKnownFinal + 1) as FinalCompressedId,
582
+ lastFinalInCluster,
583
+ currentCluster
584
+ );
585
+ }
586
+ }
652
587
  initialClusterCount = currentCluster.count;
653
588
  const remainingCapacity = currentCluster.capacity - initialClusterCount;
654
589
  const overflow = remainingCount - remainingCapacity;
@@ -668,6 +603,21 @@ export class IdCompressor {
668
603
  'The number of allocated final IDs must not exceed the JS maximum safe integer.'
669
604
  );
670
605
  this.checkClusterForCollision(currentCluster);
606
+ if (isLocal) {
607
+ // Example with cluster size of 3:
608
+ // Ids generated so far: -1 1 2 -4 -5 <-- note positive numbers are eager finals
609
+ // Cluster: [ 0 1 2 ]
610
+ // ~ finalizing happens, causing expansion ~
611
+ // Cluster: [ 0 1 2 3 4 5 ]
612
+ // corresponding locals: -1 -4
613
+ // lastFinalizedLocalId^ ^newLastFinalizedLocalId = -6
614
+ // overflow = 2: ----
615
+ // localIdPivot^
616
+ // lastFinalizedFinal^
617
+ const lastFinalizedFinal = (currentBaseFinalId + currentCluster.count - 1) as FinalCompressedId;
618
+ const finalPivot = (lastFinalizedFinal - overflow + 1) as FinalCompressedId;
619
+ this.sessionIdNormalizer.addFinalIds(finalPivot, lastFinalizedFinal, currentCluster);
620
+ }
671
621
  }
672
622
  } else {
673
623
  // The range cannot be fully allocated in the existing cluster, so allocate any space left in it and
@@ -710,10 +660,11 @@ export class IdCompressor {
710
660
  };
711
661
 
712
662
  const usedCapacity = finalizeCount - remainingCount;
713
- localIdPivot = (newFirstFinalizedLocalId - usedCapacity) as LocalCompressedId;
663
+ localIdPivot = (newFirstFinalizedLocal - usedCapacity) as LocalCompressedId;
714
664
 
715
665
  if (isLocal) {
716
- this.localIdToCluster.append(localIdPivot, [newBaseFinalId, newCluster]);
666
+ const lastFinalizedFinal = (newBaseFinalId + newCluster.count - 1) as FinalCompressedId;
667
+ this.sessionIdNormalizer.addFinalIds(newBaseFinalId, lastFinalizedFinal, newCluster);
717
668
  }
718
669
 
719
670
  this.checkClusterForCollision(newCluster);
@@ -737,11 +688,8 @@ export class IdCompressor {
737
688
  const [overriddenLocal, override] = overrides[i];
738
689
  // Note: recall that local IDs are negative
739
690
  assert(i === 0 || overriddenLocal < overrides[i - 1][0], 'Override IDs must be in sorted order.');
740
- assert(overriddenLocal < normalizedLastFinalized, 'Ranges finalized out of order.');
741
- assert(
742
- overriddenLocal >= newLastFinalizedLocalId,
743
- 'Malformed range: override ID ahead of range start.'
744
- );
691
+ assert(overriddenLocal < normalizedLastFinalizedLocal, 'Ranges finalized out of order.');
692
+ assert(overriddenLocal >= newLastFinalizedLocal, 'Malformed range: override ID ahead of range start.');
745
693
  let cluster: IdCluster;
746
694
  let overriddenFinal: FinalCompressedId;
747
695
  if (localIdPivot !== undefined && overriddenLocal <= localIdPivot) {
@@ -761,7 +709,7 @@ export class IdCompressor {
761
709
  cluster = currentCluster;
762
710
  overriddenFinal = (currentBaseFinalId +
763
711
  initialClusterCount +
764
- (normalizedLastFinalized - overriddenLocal) -
712
+ (normalizedLastFinalizedLocal - overriddenLocal) -
765
713
  1) as FinalCompressedId;
766
714
  }
767
715
  cluster.overrides ??= new Map();
@@ -831,7 +779,7 @@ export class IdCompressor {
831
779
  }
832
780
  }
833
781
 
834
- session.lastFinalizedLocalId = newLastFinalizedLocalId;
782
+ session.lastFinalizedLocalId = newLastFinalizedLocal;
835
783
  }
836
784
 
837
785
  private checkClusterForCollision(cluster: IdCluster): void {
@@ -926,9 +874,9 @@ export class IdCompressor {
926
874
  (IdCompressor.isStableInversionKey(inversionKey) ? inversionKey : undefined);
927
875
 
928
876
  if (override !== undefined) {
929
- const localId = this.getLocalIdForStableId(override);
930
- if (localId !== undefined) {
931
- return localId;
877
+ const sessionSpaceId = this.getCompressedIdForStableId(override);
878
+ if (sessionSpaceId !== undefined) {
879
+ return sessionSpaceId;
932
880
  }
933
881
  }
934
882
 
@@ -974,44 +922,51 @@ export class IdCompressor {
974
922
  * @returns an existing ID if one already exists for `override`, and a new local ID otherwise. The returned ID is in session space.
975
923
  */
976
924
  public generateCompressedId(override?: string): SessionSpaceCompressedId {
977
- // If any ID exists for this override (locally or remotely allocated), return it (after ensuring it is in session-space).
925
+ let overrideInversionKey: InversionKey | undefined;
978
926
  if (override !== undefined) {
979
- const inversionKey = IdCompressor.createInversionKey(override);
980
- const existingIds = this.getExistingIdsForNewOverride(inversionKey, false);
927
+ overrideInversionKey = IdCompressor.createInversionKey(override);
928
+ const existingIds = this.getExistingIdsForNewOverride(overrideInversionKey, false);
981
929
  if (existingIds !== undefined) {
982
930
  return typeof existingIds === 'number' ? existingIds : existingIds[0];
983
- } else {
984
- const newLocalId = this.generateNextLocalId();
985
- this.localOverrides.append(newLocalId, override);
986
- // Since the local ID was just created, it is in both session and op space
987
- const compressionMapping = newLocalId as UnackedLocalId;
988
- this.clustersAndOverridesInversion.set(inversionKey, compressionMapping);
989
- return newLocalId;
990
931
  }
991
- } else {
992
- return this.generateNextLocalId();
993
932
  }
994
- }
995
933
 
996
- /**
997
- * Generates a range of compressed IDs.
998
- * This should ONLY be called to generate IDs for local operations.
999
- * @param count - the number of IDs to generate, must be \> 0.
1000
- * @returns a persistable descriptor of the ID range.
1001
- */
1002
- public generateCompressedIdRange(count: number): IdRangeDescriptor<LocalCompressedId> {
1003
- assert(count > 0, 'Must generate a nonzero number of IDs.');
1004
- assert(
1005
- count <= Number.MAX_SAFE_INTEGER,
1006
- 'The number of allocated local IDs must not exceed the JS maximum safe integer.'
1007
- );
1008
- const first = this.generateNextLocalId();
1009
- this.localIdCount += count - 1;
1010
- return { first, count };
1011
- }
934
+ // Bump local counter regardless, then attempt to optimistically return a final ID.
935
+ // If the local session has reserved a cluster range via consensus, it is safe to hand out final IDs prior to
936
+ // finalizing the range that includes these locals.
937
+ const newLocalId = -++this.localIdCount as LocalCompressedId;
938
+ const { currentClusterDetails } = this.localSession;
939
+ const { sessionIdNormalizer } = this;
940
+ let eagerFinalId: (FinalCompressedId & SessionSpaceCompressedId) | undefined;
941
+ let cluster: IdCluster | undefined;
942
+ if (currentClusterDetails !== undefined) {
943
+ const { clusterBase } = currentClusterDetails;
944
+ cluster = currentClusterDetails.cluster;
945
+ const lastFinalKnown = sessionIdNormalizer.getLastFinalId();
946
+ if (lastFinalKnown !== undefined && lastFinalKnown - clusterBase + 1 < cluster.capacity) {
947
+ eagerFinalId = (lastFinalKnown + 1) as FinalCompressedId & SessionSpaceCompressedId;
948
+ }
949
+ }
1012
950
 
1013
- private generateNextLocalId(): LocalCompressedId {
1014
- return -++this.localIdCount as LocalCompressedId;
951
+ if (overrideInversionKey !== undefined) {
952
+ const registeredLocal = sessionIdNormalizer.addLocalId();
953
+ assert(registeredLocal === newLocalId, 'TODO');
954
+ if (eagerFinalId !== undefined) {
955
+ sessionIdNormalizer.addFinalIds(eagerFinalId, eagerFinalId, cluster ?? fail());
956
+ }
957
+ this.localOverrides.append(newLocalId, override ?? fail());
958
+ // Since the local ID was just created, it is in both session and op space
959
+ const compressionMapping = newLocalId as UnackedLocalId;
960
+ this.clustersAndOverridesInversion.set(overrideInversionKey, compressionMapping);
961
+ } else if (eagerFinalId !== undefined) {
962
+ sessionIdNormalizer.addFinalIds(eagerFinalId, eagerFinalId, cluster ?? fail());
963
+ return eagerFinalId;
964
+ } else {
965
+ const registeredLocal = sessionIdNormalizer.addLocalId();
966
+ assert(registeredLocal === newLocalId, 'TODO');
967
+ }
968
+
969
+ return newLocalId;
1015
970
  }
1016
971
 
1017
972
  /**
@@ -1032,6 +987,11 @@ export class IdCompressor {
1032
987
  if (isFinalId(id)) {
1033
988
  const possibleCluster = this.getClusterForFinalId(id);
1034
989
  if (possibleCluster === undefined) {
990
+ // It may be an unfinalized eager final ID, so check with normalizer to get the offset from the session UUID
991
+ const creationIndex = this.sessionIdNormalizer.getCreationIndex(id);
992
+ if (creationIndex !== undefined) {
993
+ return stableIdFromNumericUuid(this.localSession.sessionUuid, creationIndex);
994
+ }
1035
995
  return undefined;
1036
996
  } else {
1037
997
  const [baseFinalId, cluster] = possibleCluster;
@@ -1099,12 +1059,6 @@ export class IdCompressor {
1099
1059
  if (IdCompressor.isUnfinalizedOverride(compressionMapping)) {
1100
1060
  return compressionMapping;
1101
1061
  } else {
1102
- const cluster = compressionMapping.cluster;
1103
- assert(
1104
- IdCompressor.tryGetOverride(cluster, compressionMapping.originalOverridingFinal) !==
1105
- undefined,
1106
- 'No override for cluster marked as having one.'
1107
- );
1108
1062
  return (
1109
1063
  compressionMapping.associatedLocalId ??
1110
1064
  (compressionMapping.originalOverridingFinal as SessionSpaceCompressedId)
@@ -1136,9 +1090,9 @@ export class IdCompressor {
1136
1090
 
1137
1091
  if (isStable) {
1138
1092
  // May have already computed the numeric UUID, so avoid recomputing if possible
1139
- const localId = this.getLocalIdForStableId(numericUuid ?? inversionKey);
1140
- if (localId !== undefined) {
1141
- return localId;
1093
+ const sessionSpaceId = this.getCompressedIdForStableId(numericUuid ?? inversionKey);
1094
+ if (sessionSpaceId !== undefined) {
1095
+ return sessionSpaceId;
1142
1096
  }
1143
1097
  }
1144
1098
  return undefined;
@@ -1153,13 +1107,20 @@ export class IdCompressor {
1153
1107
  if (isFinalId(id)) {
1154
1108
  return id;
1155
1109
  }
1110
+
1156
1111
  // Check if this local ID has not been allocated yet
1157
1112
  if (-id > this.localIdCount) {
1158
1113
  fail('Supplied local ID was not created by this compressor.');
1159
1114
  }
1160
- // Check if this local ID has not been finalized yet
1115
+
1116
+ // Check if this local ID has not been finalized yet.
1117
+ // Comparing lastFinalizedLocalId is a safe check for eager final IDs because the local IDs corresponding to them
1118
+ // are never handed out to a consumer, and thus could not be passed into this method.
1161
1119
  const { lastFinalizedLocalId } = this.localSession;
1162
1120
  if (lastFinalizedLocalId === undefined || id < lastFinalizedLocalId) {
1121
+ // Eager final IDs do not have overrides in the cluster until finalizing
1122
+ // This means that using the normalizer to get the final/cluster associated would succeed but would not have the override,
1123
+ // so checking localOverrides first is necessary.
1163
1124
  const override = this.localOverrides.get(id);
1164
1125
  if (override !== undefined) {
1165
1126
  const inversionKey = IdCompressor.createInversionKey(override);
@@ -1175,10 +1136,9 @@ export class IdCompressor {
1175
1136
  }
1176
1137
  return id as OpSpaceCompressedId;
1177
1138
  }
1178
- const [localBase, [finalBase, cluster]] =
1179
- this.localIdToCluster.getPairOrNextLower(id) ??
1139
+ const [correspondingFinal, cluster] =
1140
+ this.sessionIdNormalizer.getFinalId(id) ??
1180
1141
  fail('Locally created cluster should be added to the map when allocated');
1181
- const correspondingFinal = (finalBase + (localBase - id)) as FinalCompressedId;
1182
1142
  if (cluster.overrides) {
1183
1143
  const override = cluster.overrides.get(correspondingFinal);
1184
1144
  if (typeof override === 'object' && override.originalOverridingFinal !== undefined) {
@@ -1207,33 +1167,27 @@ export class IdCompressor {
1207
1167
  */
1208
1168
  public normalizeToSessionSpace(id: FinalCompressedId): SessionSpaceCompressedId;
1209
1169
 
1210
- public normalizeToSessionSpace(id: OpSpaceCompressedId, originSessionId?: SessionId): SessionSpaceCompressedId {
1211
- const isLocalSession = originSessionId === this.localSessionId;
1170
+ public normalizeToSessionSpace(id: OpSpaceCompressedId, sessionIdIfLocal?: SessionId): SessionSpaceCompressedId {
1212
1171
  if (isLocalId(id)) {
1213
- if (isLocalSession) {
1172
+ if (sessionIdIfLocal === undefined || sessionIdIfLocal === this.localSessionId) {
1214
1173
  const localIndex = -id;
1215
1174
  if (localIndex > this.localIdCount) {
1216
1175
  fail('Supplied local ID was not created by this compressor.');
1217
1176
  }
1218
1177
  return id;
1219
1178
  } else {
1220
- const session = this.sessions.get(originSessionId ?? fail());
1221
- if (session === undefined) {
1179
+ const session =
1180
+ this.sessions.get(sessionIdIfLocal) ??
1222
1181
  fail('No IDs have ever been finalized by the supplied session.');
1223
- }
1224
1182
  const localCount = -id;
1225
1183
  const numericUuid = incrementUuid(session.sessionUuid, localCount - 1);
1226
1184
  return this.compressNumericUuid(numericUuid) ?? fail('ID is not known to this compressor.');
1227
1185
  }
1228
1186
  }
1229
1187
 
1230
- const closestResult = this.localIdToCluster.getPairOrNextLowerByValue(id);
1231
- if (closestResult !== undefined) {
1232
- const [localBase, [finalBase, cluster]] = closestResult;
1233
- const indexInCluster = id - finalBase;
1234
- if (indexInCluster < cluster.count) {
1235
- return (localBase - indexInCluster) as LocalCompressedId;
1236
- }
1188
+ const normalizedId = this.sessionIdNormalizer.getSessionSpaceId(id);
1189
+ if (normalizedId !== undefined) {
1190
+ return normalizedId;
1237
1191
  }
1238
1192
 
1239
1193
  // Check for a unified override finalized first by another session but to which the local session
@@ -1276,14 +1230,19 @@ export class IdCompressor {
1276
1230
  return sessionSpaceId;
1277
1231
  }
1278
1232
 
1279
- private getLocalIdForStableId(stableId: StableId | NumericUuid): LocalCompressedId | undefined {
1233
+ /**
1234
+ * Returns a compressed ID for the supplied stable ID if it was created by the local session, and undefined otherwise.
1235
+ */
1236
+ private getCompressedIdForStableId(stableId: StableId | NumericUuid): SessionSpaceCompressedId | undefined {
1280
1237
  const numericUuid = typeof stableId === 'string' ? numericUuidFromStableId(stableId) : stableId;
1281
- const offset = getPositiveDelta(numericUuid, this.localSession.sessionUuid, this.localIdCount - 1);
1282
- if (offset === undefined) {
1283
- return undefined;
1238
+ const creationIndex = getPositiveDelta(numericUuid, this.localSession.sessionUuid, this.localIdCount - 1);
1239
+ if (creationIndex !== undefined) {
1240
+ const sessionSpaceId = this.sessionIdNormalizer.getIdByCreationIndex(creationIndex);
1241
+ if (sessionSpaceId !== undefined) {
1242
+ return sessionSpaceId;
1243
+ }
1284
1244
  }
1285
-
1286
- return (-offset - 1) as LocalCompressedId;
1245
+ return undefined;
1287
1246
  }
1288
1247
 
1289
1248
  private getClusterForFinalId(
@@ -1324,6 +1283,13 @@ export class IdCompressor {
1324
1283
  ) {
1325
1284
  return false;
1326
1285
  }
1286
+ if (
1287
+ !this.sessionIdNormalizer.equals(other.sessionIdNormalizer, (a, b) =>
1288
+ IdCompressor.idClustersEqual(a, b, false, compareLocalState)
1289
+ )
1290
+ ) {
1291
+ return false;
1292
+ }
1327
1293
  } else {
1328
1294
  for (const [keyA, valueA] of this.sessions) {
1329
1295
  const valueB = other.sessions.get(keyA);
@@ -1588,6 +1554,7 @@ export class IdCompressor {
1588
1554
  localIdCount: this.localIdCount,
1589
1555
  overrides: [...this.localOverrides.entries()].map((entry) => [...entry]),
1590
1556
  lastTakenLocalId: this.lastTakenLocalId,
1557
+ sessionNormalizer: this.sessionIdNormalizer.serialize(),
1591
1558
  };
1592
1559
  }
1593
1560
 
@@ -1714,12 +1681,6 @@ export class IdCompressor {
1714
1681
 
1715
1682
  const lastFinalizedNormalized = lastFinalizedLocalId ?? 0;
1716
1683
  const clusterBase = compressor.nextClusterBaseFinalId;
1717
- if (serializedLocalState !== undefined && sessionId === compressor.localSessionId) {
1718
- compressor.localIdToCluster.append((lastFinalizedNormalized - 1) as LocalCompressedId, [
1719
- clusterBase,
1720
- cluster,
1721
- ]);
1722
- }
1723
1684
 
1724
1685
  session.lastFinalizedLocalId = (lastFinalizedNormalized - count) as LocalCompressedId;
1725
1686
  session.currentClusterDetails = { clusterBase, cluster };
@@ -1777,6 +1738,18 @@ export class IdCompressor {
1777
1738
  }
1778
1739
  }
1779
1740
 
1741
+ if (serializedLocalState !== undefined) {
1742
+ compressor.sessionIdNormalizer = SessionIdNormalizer.deserialize(
1743
+ serializedLocalState.sessionNormalizer,
1744
+ (finalId) => {
1745
+ const [_, cluster] =
1746
+ compressor.finalIdToCluster.getPairOrNextLower(finalId) ??
1747
+ fail('Final in serialized normalizer was never created.');
1748
+ return cluster;
1749
+ }
1750
+ );
1751
+ }
1752
+
1780
1753
  assert(
1781
1754
  compressor.localSession.lastFinalizedLocalId === undefined ||
1782
1755
  compressor.localIdCount >= -compressor.localSession.lastFinalizedLocalId