@fluidframework/tree 2.22.0 → 2.23.0-323641

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 (76) hide show
  1. package/dist/core/tree/anchorSet.d.ts.map +1 -1
  2. package/dist/core/tree/anchorSet.js +15 -11
  3. package/dist/core/tree/anchorSet.js.map +1 -1
  4. package/dist/feature-libraries/modular-schema/modularChangeFamily.d.ts.map +1 -1
  5. package/dist/feature-libraries/modular-schema/modularChangeFamily.js +5 -6
  6. package/dist/feature-libraries/modular-schema/modularChangeFamily.js.map +1 -1
  7. package/dist/packageVersion.d.ts +1 -1
  8. package/dist/packageVersion.d.ts.map +1 -1
  9. package/dist/packageVersion.js +1 -1
  10. package/dist/packageVersion.js.map +1 -1
  11. package/dist/shared-tree/sharedTree.d.ts +2 -1
  12. package/dist/shared-tree/sharedTree.d.ts.map +1 -1
  13. package/dist/shared-tree/sharedTree.js +3 -0
  14. package/dist/shared-tree/sharedTree.js.map +1 -1
  15. package/dist/shared-tree-core/editManager.d.ts +11 -2
  16. package/dist/shared-tree-core/editManager.d.ts.map +1 -1
  17. package/dist/shared-tree-core/editManager.js +63 -28
  18. package/dist/shared-tree-core/editManager.js.map +1 -1
  19. package/dist/shared-tree-core/resubmitMachine.d.ts +2 -0
  20. package/dist/shared-tree-core/resubmitMachine.d.ts.map +1 -1
  21. package/dist/shared-tree-core/resubmitMachine.js.map +1 -1
  22. package/dist/shared-tree-core/sharedTreeCore.d.ts +9 -1
  23. package/dist/shared-tree-core/sharedTreeCore.d.ts.map +1 -1
  24. package/dist/shared-tree-core/sharedTreeCore.js +42 -7
  25. package/dist/shared-tree-core/sharedTreeCore.js.map +1 -1
  26. package/dist/util/index.d.ts +1 -1
  27. package/dist/util/index.d.ts.map +1 -1
  28. package/dist/util/index.js +3 -2
  29. package/dist/util/index.js.map +1 -1
  30. package/dist/util/utils.d.ts +16 -0
  31. package/dist/util/utils.d.ts.map +1 -1
  32. package/dist/util/utils.js +29 -1
  33. package/dist/util/utils.js.map +1 -1
  34. package/lib/core/tree/anchorSet.d.ts.map +1 -1
  35. package/lib/core/tree/anchorSet.js +16 -12
  36. package/lib/core/tree/anchorSet.js.map +1 -1
  37. package/lib/feature-libraries/modular-schema/modularChangeFamily.d.ts.map +1 -1
  38. package/lib/feature-libraries/modular-schema/modularChangeFamily.js +6 -7
  39. package/lib/feature-libraries/modular-schema/modularChangeFamily.js.map +1 -1
  40. package/lib/packageVersion.d.ts +1 -1
  41. package/lib/packageVersion.d.ts.map +1 -1
  42. package/lib/packageVersion.js +1 -1
  43. package/lib/packageVersion.js.map +1 -1
  44. package/lib/shared-tree/sharedTree.d.ts +2 -1
  45. package/lib/shared-tree/sharedTree.d.ts.map +1 -1
  46. package/lib/shared-tree/sharedTree.js +3 -0
  47. package/lib/shared-tree/sharedTree.js.map +1 -1
  48. package/lib/shared-tree-core/editManager.d.ts +11 -2
  49. package/lib/shared-tree-core/editManager.d.ts.map +1 -1
  50. package/lib/shared-tree-core/editManager.js +63 -28
  51. package/lib/shared-tree-core/editManager.js.map +1 -1
  52. package/lib/shared-tree-core/resubmitMachine.d.ts +2 -0
  53. package/lib/shared-tree-core/resubmitMachine.d.ts.map +1 -1
  54. package/lib/shared-tree-core/resubmitMachine.js.map +1 -1
  55. package/lib/shared-tree-core/sharedTreeCore.d.ts +9 -1
  56. package/lib/shared-tree-core/sharedTreeCore.d.ts.map +1 -1
  57. package/lib/shared-tree-core/sharedTreeCore.js +42 -7
  58. package/lib/shared-tree-core/sharedTreeCore.js.map +1 -1
  59. package/lib/util/index.d.ts +1 -1
  60. package/lib/util/index.d.ts.map +1 -1
  61. package/lib/util/index.js +1 -1
  62. package/lib/util/index.js.map +1 -1
  63. package/lib/util/utils.d.ts +16 -0
  64. package/lib/util/utils.d.ts.map +1 -1
  65. package/lib/util/utils.js +27 -0
  66. package/lib/util/utils.js.map +1 -1
  67. package/package.json +23 -23
  68. package/src/core/tree/anchorSet.ts +34 -24
  69. package/src/feature-libraries/modular-schema/modularChangeFamily.ts +9 -6
  70. package/src/packageVersion.ts +1 -1
  71. package/src/shared-tree/sharedTree.ts +5 -0
  72. package/src/shared-tree-core/editManager.ts +78 -41
  73. package/src/shared-tree-core/resubmitMachine.ts +2 -0
  74. package/src/shared-tree-core/sharedTreeCore.ts +58 -12
  75. package/src/util/index.ts +1 -0
  76. package/src/util/utils.ts +32 -0
@@ -472,7 +472,7 @@ export class EditManager<
472
472
  // `EditManager` would have to be amended in one of two ways:
473
473
  // A) Changes made by the local session should be represented by a branch in `EditManager.branches`.
474
474
  // B) The contents of such a branch should be computed on demand based on the trunk.
475
- // Note that option (A) would be a simple change to `addSequencedChange` whereas (B) would likely require
475
+ // Note that option (A) would be a simple change to `addSequencedChanges` whereas (B) would likely require
476
476
  // rebasing trunk changes over the inverse of trunk changes.
477
477
  assert(
478
478
  this.localBranch.getHead() === this.trunk.getHead(),
@@ -646,24 +646,45 @@ export class EditManager<
646
646
  return Math.max(max, localPath.length);
647
647
  }
648
648
 
649
- public addSequencedChange(
650
- newCommit: Commit<TChangeset>,
649
+ /* eslint-disable jsdoc/check-indentation */
650
+ /**
651
+ * Add a bunch of sequenced changes. A bunch is a group of sequenced commits that have the following properties:
652
+ * - They are not interleaved with messages from other DDSes in the container.
653
+ * - They are all part of the same batch, which entails:
654
+ * - They are contiguous in sequencing order.
655
+ * - They are all from the same client.
656
+ * - They are all based on the same reference sequence number.
657
+ * - They are not interleaved with messages from other clients.
658
+ */
659
+ /* eslint-enable jsdoc/check-indentation */
660
+ public addSequencedChanges(
661
+ newCommits: readonly GraphCommit<TChangeset>[],
662
+ sessionId: SessionId,
651
663
  sequenceNumber: SeqNumber,
652
664
  referenceSequenceNumber: SeqNumber,
653
665
  ): void {
666
+ assert(newCommits.length > 0, "Expected at least one sequenced change");
654
667
  assert(
655
668
  sequenceNumber > this.minimumSequenceNumber,
656
669
  0x713 /* Expected change sequence number to exceed the last known minimum sequence number */,
657
670
  );
658
-
659
671
  assert(
660
672
  sequenceNumber >= // This is ">=", not ">" because changes in the same batch will have the same sequence number
661
673
  (this.sequenceMap.maxKey()?.sequenceNumber ?? minimumPossibleSequenceNumber),
662
674
  0xa64 /* Attempted to sequence change with an outdated sequence number */,
663
675
  );
664
676
 
677
+ // Returns the sequence id for the next commit to be processed in the bunch. Since all the commits have the
678
+ // same sequence number, only the index in the batch needs to be incremented.
679
+ const getNextSequenceId = (sequenceId: SequenceId): SequenceId => {
680
+ return {
681
+ sequenceNumber: sequenceId.sequenceNumber,
682
+ indexInBatch: (sequenceId.indexInBatch ?? 0) + 1,
683
+ };
684
+ };
665
685
  const commitsSequenceNumber = this.getBatch(sequenceNumber);
666
- const sequenceId: SequenceId =
686
+ // The sequence id for the next commit to be processed in the bunch.
687
+ let nextSequenceId =
667
688
  commitsSequenceNumber.length === 0
668
689
  ? {
669
690
  sequenceNumber,
@@ -673,49 +694,65 @@ export class EditManager<
673
694
  indexInBatch: commitsSequenceNumber.length,
674
695
  };
675
696
 
676
- if (newCommit.sessionId === this.localSessionId) {
677
- return this.fastForwardNextLocalCommit(sequenceId);
697
+ // Local changes, i.e., changes from this client are applied by fast forwarding the local branch commit onto
698
+ // the trunk.
699
+ if (sessionId === this.localSessionId) {
700
+ for (const _ of newCommits) {
701
+ this.fastForwardNextLocalCommit(nextSequenceId);
702
+ nextSequenceId = getNextSequenceId(nextSequenceId);
703
+ }
704
+ return;
678
705
  }
679
706
 
680
- // Get the revision that the remote change is based on
681
- const [, baseRevisionInTrunk] = this.getClosestTrunkCommit(referenceSequenceNumber);
682
- // Rebase that branch over the part of the trunk up to the base revision
683
- // This will be a no-op if the sending client has not advanced since the last time we received an edit from it
684
- const peerLocalBranch = getOrCreate(
685
- this.peerLocalBranches,
686
- newCommit.sessionId,
687
- () => new SharedTreeBranch(baseRevisionInTrunk, this.changeFamily, this.mintRevisionTag),
688
- );
689
- peerLocalBranch.rebaseOnto(this.trunk, baseRevisionInTrunk);
690
-
691
- if (peerLocalBranch.getHead() === this.trunk.getHead()) {
692
- // If the branch is fully caught up and empty after being rebased, then push to the trunk directly
693
- this.pushCommitToTrunk(sequenceId, newCommit);
694
- peerLocalBranch.setHead(this.trunk.getHead());
695
- } else {
696
- // Otherwise, rebase the change over the trunk and append it, and append the original change to the peer branch.
697
- const { duration, output: newChangeFullyRebased } = measure(() =>
698
- rebaseChange(
699
- this.changeFamily.rebaser,
700
- newCommit,
701
- peerLocalBranch.getHead(),
702
- this.trunk.getHead(),
703
- this.mintRevisionTag,
704
- ),
707
+ // Remote changes, i.e., changes from remote clients are applied in three steps.
708
+ for (const newCommit of newCommits) {
709
+ // Step 1 - Recreate the peer remote client's local environment.
710
+ // Get the revision that the remote change is based on
711
+ const [, baseRevisionInTrunk] = this.getClosestTrunkCommit(referenceSequenceNumber);
712
+ // Rebase that peer local branch over the part of the trunk up to the base revision
713
+ // This will be a no-op if the sending client has not advanced since the last time we received an edit from it
714
+ const peerLocalBranch = getOrCreate(
715
+ this.peerLocalBranches,
716
+ sessionId,
717
+ () =>
718
+ new SharedTreeBranch(baseRevisionInTrunk, this.changeFamily, this.mintRevisionTag),
705
719
  );
720
+ peerLocalBranch.rebaseOnto(this.trunk, baseRevisionInTrunk);
706
721
 
707
- this.telemetryEventBatcher?.accumulateAndLog({
708
- duration,
709
- ...newChangeFullyRebased.telemetryProperties,
710
- });
722
+ // Step 2 - Append the change to the peer branch. Rebase the change to the tip of the trunk.
723
+ if (peerLocalBranch.getHead() === this.trunk.getHead()) {
724
+ // If the branch is fully caught up and empty after being rebased, then push to the trunk directly
725
+ this.pushCommitToTrunk(nextSequenceId, { ...newCommit, sessionId });
726
+ peerLocalBranch.setHead(this.trunk.getHead());
727
+ } else {
728
+ // Otherwise, rebase the change over the trunk and append it, and append the original change to the peer branch.
729
+ const { duration, output: newChangeFullyRebased } = measure(() =>
730
+ rebaseChange(
731
+ this.changeFamily.rebaser,
732
+ newCommit,
733
+ peerLocalBranch.getHead(),
734
+ this.trunk.getHead(),
735
+ this.mintRevisionTag,
736
+ ),
737
+ );
711
738
 
712
- peerLocalBranch.apply(tagChange(newCommit.change, newCommit.revision));
713
- this.pushCommitToTrunk(sequenceId, {
714
- ...newCommit,
715
- change: newChangeFullyRebased.change,
716
- });
739
+ this.telemetryEventBatcher?.accumulateAndLog({
740
+ duration,
741
+ ...newChangeFullyRebased.telemetryProperties,
742
+ });
743
+
744
+ peerLocalBranch.apply(tagChange(newCommit.change, newCommit.revision));
745
+ this.pushCommitToTrunk(nextSequenceId, {
746
+ ...newCommit,
747
+ sessionId,
748
+ change: newChangeFullyRebased.change,
749
+ });
750
+ }
751
+
752
+ nextSequenceId = getNextSequenceId(nextSequenceId);
717
753
  }
718
754
 
755
+ // Step 3 - Rebase the local branch over the updated trunk.
719
756
  this.localBranch.rebaseOnto(this.trunk);
720
757
  }
721
758
 
@@ -39,6 +39,8 @@ export interface ResubmitMachine<TChange> {
39
39
 
40
40
  /**
41
41
  * Must be called after a sequenced commit is applied.
42
+ * Note that this may be called multiples times in a row after a number of sequenced commits have been applied
43
+ * (as opposed to always being called before the next sequenced commit is applied).
42
44
  * @param isLocal - whether the sequenced commit was generated by the local session.
43
45
  */
44
46
  onSequencedCommitApplied(isLocal: boolean): void;
@@ -5,10 +5,11 @@
5
5
 
6
6
  import { assert } from "@fluidframework/core-utils/internal";
7
7
  import type { IChannelStorageService } from "@fluidframework/datastore-definitions/internal";
8
- import type { IIdCompressor } from "@fluidframework/id-compressor";
8
+ import type { IIdCompressor, SessionId } from "@fluidframework/id-compressor";
9
9
  import type { ISequencedDocumentMessage } from "@fluidframework/driver-definitions/internal";
10
10
  import type {
11
11
  IExperimentalIncrementalSummaryContext,
12
+ IRuntimeMessageCollection,
12
13
  ISummaryTreeWithStats,
13
14
  ITelemetryContext,
14
15
  } from "@fluidframework/runtime-definitions/internal";
@@ -301,8 +302,9 @@ export class SharedTreeCore<TEditor extends ChangeFamilyEditor, TChange>
301
302
  if (this.detachedRevision !== undefined) {
302
303
  const newRevision: SeqNumber = brand((this.detachedRevision as number) + 1);
303
304
  this.detachedRevision = newRevision;
304
- this.editManager.addSequencedChange(
305
- { ...enrichedCommit, sessionId: this.editManager.localSessionId },
305
+ this.editManager.addSequencedChanges(
306
+ [enrichedCommit],
307
+ this.editManager.localSessionId,
306
308
  newRevision,
307
309
  this.detachedRevision,
308
310
  );
@@ -327,24 +329,68 @@ export class SharedTreeCore<TEditor extends ChangeFamilyEditor, TChange>
327
329
  this.resubmitMachine.onCommitSubmitted(enrichedCommit);
328
330
  }
329
331
 
332
+ /**
333
+ * Process a message from the runtime.
334
+ * @deprecated - Use processMessagesCore to process a bunch of messages together.
335
+ */
330
336
  public processCore(
331
337
  message: ISequencedDocumentMessage,
332
338
  local: boolean,
333
339
  localOpMetadata: unknown,
334
340
  ): void {
335
- // Empty context object is passed in, as our decode function is schema-agnostic.
336
- const { commit, sessionId } = this.messageCodec.decode(message.contents, {
337
- idCompressor: this.idCompressor,
341
+ this.processMessagesCore({
342
+ envelope: message,
343
+ local,
344
+ messagesContent: [
345
+ {
346
+ clientSequenceNumber: message.clientSequenceNumber,
347
+ contents: message.contents,
348
+ localOpMetadata,
349
+ },
350
+ ],
338
351
  });
352
+ }
353
+
354
+ /**
355
+ * Process a bunch of messages from the runtime. SharedObject will call this method with a bunch of messages.
356
+ */
357
+ public processMessagesCore(messagesCollection: IRuntimeMessageCollection): void {
358
+ const { envelope, local, messagesContent } = messagesCollection;
359
+ const commits: GraphCommit<TChange>[] = [];
360
+ let messagesSessionId: SessionId | undefined;
361
+
362
+ // Get a list of all the commits from the messages.
363
+ for (const messageContent of messagesContent) {
364
+ // Empty context object is passed in, as our decode function is schema-agnostic.
365
+ const { commit, sessionId } = this.messageCodec.decode(messageContent.contents, {
366
+ idCompressor: this.idCompressor,
367
+ });
368
+ commits.push(commit);
369
+
370
+ if (messagesSessionId !== undefined) {
371
+ assert(
372
+ messagesSessionId === sessionId,
373
+ "All messages in a bunch must have the same session ID",
374
+ );
375
+ }
376
+ messagesSessionId = sessionId;
377
+ }
378
+
379
+ assert(messagesSessionId !== undefined, "Messages must have a session ID");
339
380
 
340
- this.editManager.addSequencedChange(
341
- { ...commit, sessionId },
342
- brand(message.sequenceNumber),
343
- brand(message.referenceSequenceNumber),
381
+ this.editManager.addSequencedChanges(
382
+ commits,
383
+ messagesSessionId,
384
+ brand(envelope.sequenceNumber),
385
+ brand(envelope.referenceSequenceNumber),
344
386
  );
345
- this.resubmitMachine.onSequencedCommitApplied(local);
346
387
 
347
- this.editManager.advanceMinimumSequenceNumber(brand(message.minimumSequenceNumber));
388
+ // Update the resubmit machine for each commit applied.
389
+ for (const _ of messagesContent) {
390
+ this.resubmitMachine.onSequencedCommitApplied(local);
391
+ }
392
+
393
+ this.editManager.advanceMinimumSequenceNumber(brand(envelope.minimumSequenceNumber));
348
394
  }
349
395
 
350
396
  public getLocalBranch(): SharedTreeBranch<TEditor, TChange> {
package/src/util/index.ts CHANGED
@@ -56,6 +56,7 @@ export type {
56
56
  export { StackyIterator } from "./stackyIterator.js";
57
57
  export {
58
58
  asMutable,
59
+ balancedReduce,
59
60
  clone,
60
61
  compareSets,
61
62
  fail,
package/src/util/utils.ts CHANGED
@@ -616,3 +616,35 @@ export function copyPropertyIfDefined<
616
616
  }
617
617
  }
618
618
  }
619
+
620
+ /**
621
+ * Reduces an array of values into a single value.
622
+ * This is similar to `Array.prototype.reduce`,
623
+ * except that it recursively reduces the left and right halves of the input before reducing their respective reductions.
624
+ *
625
+ * When compared with an approach like reducing all the values left-to-right,
626
+ * this balanced approach is beneficial when the cost of invoking `callbackFn` is proportional to the number reduced values that its parameters collectively represent.
627
+ * For example, if `T` is an array, and `callbackFn` concatenates its inputs,
628
+ * then `balancedReduce` will have O(N*log(N)) time complexity instead of `Array.prototype.reduce`'s O(N²).
629
+ * However, if `callbackFn` is O(1) then both `balancedReduce` and `Array.prototype.reduce` will have O(N) complexity.
630
+ *
631
+ * @param array - The array to reduce.
632
+ * @param callbackFn - The function to execute for each pairwise reduction.
633
+ * @param emptyCase - A factory function that provides the value to return if the input array is empty.
634
+ */
635
+ export function balancedReduce<T>(
636
+ array: readonly T[],
637
+ callbackFn: (left: T, right: T) => T,
638
+ emptyCase: () => T,
639
+ ): T {
640
+ if (hasSingle(array)) {
641
+ return array[0];
642
+ }
643
+ if (!hasSome(array)) {
644
+ return emptyCase();
645
+ }
646
+ const mid = Math.floor(array.length / 2);
647
+ const left = balancedReduce(array.slice(0, mid), callbackFn, emptyCase);
648
+ const right = balancedReduce(array.slice(mid), callbackFn, emptyCase);
649
+ return callbackFn(left, right);
650
+ }