@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.
- package/dist/core/tree/anchorSet.d.ts.map +1 -1
- package/dist/core/tree/anchorSet.js +15 -11
- package/dist/core/tree/anchorSet.js.map +1 -1
- package/dist/feature-libraries/modular-schema/modularChangeFamily.d.ts.map +1 -1
- package/dist/feature-libraries/modular-schema/modularChangeFamily.js +5 -6
- package/dist/feature-libraries/modular-schema/modularChangeFamily.js.map +1 -1
- package/dist/packageVersion.d.ts +1 -1
- package/dist/packageVersion.d.ts.map +1 -1
- package/dist/packageVersion.js +1 -1
- package/dist/packageVersion.js.map +1 -1
- package/dist/shared-tree/sharedTree.d.ts +2 -1
- package/dist/shared-tree/sharedTree.d.ts.map +1 -1
- package/dist/shared-tree/sharedTree.js +3 -0
- package/dist/shared-tree/sharedTree.js.map +1 -1
- package/dist/shared-tree-core/editManager.d.ts +11 -2
- package/dist/shared-tree-core/editManager.d.ts.map +1 -1
- package/dist/shared-tree-core/editManager.js +63 -28
- package/dist/shared-tree-core/editManager.js.map +1 -1
- package/dist/shared-tree-core/resubmitMachine.d.ts +2 -0
- package/dist/shared-tree-core/resubmitMachine.d.ts.map +1 -1
- package/dist/shared-tree-core/resubmitMachine.js.map +1 -1
- package/dist/shared-tree-core/sharedTreeCore.d.ts +9 -1
- package/dist/shared-tree-core/sharedTreeCore.d.ts.map +1 -1
- package/dist/shared-tree-core/sharedTreeCore.js +42 -7
- package/dist/shared-tree-core/sharedTreeCore.js.map +1 -1
- package/dist/util/index.d.ts +1 -1
- package/dist/util/index.d.ts.map +1 -1
- package/dist/util/index.js +3 -2
- package/dist/util/index.js.map +1 -1
- package/dist/util/utils.d.ts +16 -0
- package/dist/util/utils.d.ts.map +1 -1
- package/dist/util/utils.js +29 -1
- package/dist/util/utils.js.map +1 -1
- package/lib/core/tree/anchorSet.d.ts.map +1 -1
- package/lib/core/tree/anchorSet.js +16 -12
- package/lib/core/tree/anchorSet.js.map +1 -1
- package/lib/feature-libraries/modular-schema/modularChangeFamily.d.ts.map +1 -1
- package/lib/feature-libraries/modular-schema/modularChangeFamily.js +6 -7
- package/lib/feature-libraries/modular-schema/modularChangeFamily.js.map +1 -1
- package/lib/packageVersion.d.ts +1 -1
- package/lib/packageVersion.d.ts.map +1 -1
- package/lib/packageVersion.js +1 -1
- package/lib/packageVersion.js.map +1 -1
- package/lib/shared-tree/sharedTree.d.ts +2 -1
- package/lib/shared-tree/sharedTree.d.ts.map +1 -1
- package/lib/shared-tree/sharedTree.js +3 -0
- package/lib/shared-tree/sharedTree.js.map +1 -1
- package/lib/shared-tree-core/editManager.d.ts +11 -2
- package/lib/shared-tree-core/editManager.d.ts.map +1 -1
- package/lib/shared-tree-core/editManager.js +63 -28
- package/lib/shared-tree-core/editManager.js.map +1 -1
- package/lib/shared-tree-core/resubmitMachine.d.ts +2 -0
- package/lib/shared-tree-core/resubmitMachine.d.ts.map +1 -1
- package/lib/shared-tree-core/resubmitMachine.js.map +1 -1
- package/lib/shared-tree-core/sharedTreeCore.d.ts +9 -1
- package/lib/shared-tree-core/sharedTreeCore.d.ts.map +1 -1
- package/lib/shared-tree-core/sharedTreeCore.js +42 -7
- package/lib/shared-tree-core/sharedTreeCore.js.map +1 -1
- package/lib/util/index.d.ts +1 -1
- package/lib/util/index.d.ts.map +1 -1
- package/lib/util/index.js +1 -1
- package/lib/util/index.js.map +1 -1
- package/lib/util/utils.d.ts +16 -0
- package/lib/util/utils.d.ts.map +1 -1
- package/lib/util/utils.js +27 -0
- package/lib/util/utils.js.map +1 -1
- package/package.json +23 -23
- package/src/core/tree/anchorSet.ts +34 -24
- package/src/feature-libraries/modular-schema/modularChangeFamily.ts +9 -6
- package/src/packageVersion.ts +1 -1
- package/src/shared-tree/sharedTree.ts +5 -0
- package/src/shared-tree-core/editManager.ts +78 -41
- package/src/shared-tree-core/resubmitMachine.ts +2 -0
- package/src/shared-tree-core/sharedTreeCore.ts +58 -12
- package/src/util/index.ts +1 -0
- 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 `
|
|
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
|
-
|
|
650
|
-
|
|
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
|
-
|
|
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
|
-
|
|
677
|
-
|
|
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
|
-
//
|
|
681
|
-
const
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
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
|
-
|
|
708
|
-
|
|
709
|
-
|
|
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
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
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.
|
|
305
|
-
|
|
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
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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.
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
brand(
|
|
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
|
-
|
|
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
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
|
+
}
|