Skip to content

Update internal SharedTree types to use the .events pattern #23038

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Nov 8, 2024

Conversation

noencke
Copy link
Contributor

@noencke noencke commented Nov 8, 2024

Description

This changes all internal SharedTree objects to have an events: Listenable property rather than implementing Listenable directly. Using the .events pattern is preferable over the alternatives because it does not employ inheritance (like extending EventEmitter) and does not require any method implementation boilerplate (like implementing Listenable). It also means that any changes to EventEmitter or Listenable will not require changes to the object emitting the events - this is the practical motivation for this change, as the Listenable interface will soon be updated to have a new method. Without this preparatory change, all the implementations of Listenable would need to be updated at that time.

@noencke noencke requested a review from a team as a code owner November 8, 2024 19:40
@github-actions github-actions bot added area: dds Issues related to distributed data structures area: dds: tree base: main PRs targeted against main branch and removed area: dds Issues related to distributed data structures area: dds: tree labels Nov 8, 2024
@@ -121,7 +121,7 @@ export {
type TreeFieldStoredSchema,
ValueSchema,
TreeNodeStoredSchema,
type TreeStoredSchemaSubscription as TreeStoredSchemaSubscription,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is unrelated to the rest of this PR but I stumbled across it and it didn't seem worth checking in as its own PR.

export interface IForestSubscription extends Listenable<ForestEvents> {
export interface IForestSubscription {
/**
* Events for this forest.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For our internal types, I don't actually think it's practically necessary to document the events property at either the interface level or the implementation level since it's clear what is going on (and it's the events themselves, not the event emitter, that needs documentation). Nonetheless I added a brief doc comment in places where the surrounding properties also had consistent doc comments.

forkable: T,
// Branches are invariant over TChange
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function onForkTransitive<T extends SharedTreeBranch<ChangeFamilyEditor, any>>(
Copy link
Contributor Author

@noencke noencke Nov 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function was typed as it was to avoid having to use a generic any, but I decided it's just simpler to add a lint exception and make it hardcoded to the branch type. That also helped with some typing elsewhere in this class where onForkTransitive was being used that were acting up after switching to .events.

Comment on lines +65 to +66
protected readonly _events = createEmitter<SchemaEvents>();
public readonly events: Listenable<SchemaEvents> = this._events;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the motivation for having _events?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that to keep the ability to fire events encapsulated?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, exactly. Usually it's #events, but in this case it's protected so we use _events.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the future I'd like to explore if there is some clever way to have it work without having to define multiple fields, but I'm not sure if that's possible.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I was wondering the same thing.
Having usages of the class be indirected through an interface would take care of it.

Otherwise, I would find this pattern less ugly if we had one public member for listerning only and one private member for firing only.
You'd then initialize them in the ctor by doing {this.events, this.eventEmitter} = createEmitter<SchemaEvents>(); or something like that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's interesting! That could work. Although, I don't think it's possible to do the destructuring in one line like that - you'd have to use a local and at least three lines. But the splitting of the two does make sense.

Another option would be to do public readonly events = createEmitter() and have it already be the "listen-only" interface, but there's a static helper emit(this.events, key, ...) which does a cast inside to get the emitting version. That would be the least amount of boilerplate, but is kind of unconventional and weird (even if we ensured the cast was safe by e.g. using a listen-only class instead of merely a listen-only interface).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: eventEmitter might be a better property name for these to help clarify the difference

Comment on lines +444 to +450
function subscribeToLocalBranch<TChange>(
manager: EditManager<ChangeFamilyEditor, TChange, ChangeFamily<ChangeFamilyEditor, TChange>>,
): void {
manager.localBranch.events.on("afterChange", (branchChange) => {
// Reading the change property causes lazy computation to occur, and is important to accurately emulate SharedTree behavior
const _change = branchChange.change;
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Much improved, thank you!

@noencke noencke enabled auto-merge (squash) November 8, 2024 20:25
Copy link
Collaborator

@msfluid-bot msfluid-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Coverage Summary

↓ packages.dds.tree.src.core.schema-stored:
Line Coverage Change: No change    Branch Coverage Change: -0.14%
Metric NameBaseline coveragePR coverageCoverage Diff
Branch Coverage 93.75% 93.61% ↓ -0.14%
Line Coverage 99.70% 99.70% → No change
↓ packages.dds.tree.src.feature-libraries.object-forest:
Line Coverage Change: -0.02%    Branch Coverage Change: -0.06%
Metric NameBaseline coveragePR coverageCoverage Diff
Branch Coverage 95.40% 95.34% ↓ -0.06%
Line Coverage 97.66% 97.64% ↓ -0.02%
↓ packages.dds.tree.src.feature-libraries.chunked-forest:
Line Coverage Change: -0.01%    Branch Coverage Change: -0.03%
Metric NameBaseline coveragePR coverageCoverage Diff
Branch Coverage 86.98% 86.95% ↓ -0.03%
Line Coverage 95.62% 95.61% ↓ -0.01%
↓ packages.dds.tree.src.core.tree:
Line Coverage Change: -0.02%    Branch Coverage Change: -0.02%
Metric NameBaseline coveragePR coverageCoverage Diff
Branch Coverage 96.36% 96.34% ↓ -0.02%
Line Coverage 88.11% 88.09% ↓ -0.02%
↑ packages.dds.tree.src.core.forest:
Line Coverage Change: 0.01%    Branch Coverage Change: No change
Metric NameBaseline coveragePR coverageCoverage Diff
Branch Coverage 100.00% 100.00% → No change
Line Coverage 99.48% 99.49% ↑ 0.01%
↑ packages.dds.tree.src.simple-tree:
Line Coverage Change: No change    Branch Coverage Change: 0.01%
Metric NameBaseline coveragePR coverageCoverage Diff
Branch Coverage 94.06% 94.07% ↑ 0.01%
Line Coverage 97.21% 97.21% → No change
↑ packages.dds.tree.src.shared-tree:
Line Coverage Change: No change    Branch Coverage Change: 0.03%
Metric NameBaseline coveragePR coverageCoverage Diff
Branch Coverage 90.85% 90.88% ↑ 0.03%
Line Coverage 97.28% 97.28% → No change
↑ packages.dds.tree.src.simple-tree.api:
Line Coverage Change: No change    Branch Coverage Change: 0.03%
Metric NameBaseline coveragePR coverageCoverage Diff
Branch Coverage 88.73% 88.76% ↑ 0.03%
Line Coverage 82.19% 82.19% → No change
↑ packages.dds.tree.src.feature-libraries.flex-tree:
Line Coverage Change: 0.17%    Branch Coverage Change: No change
Metric NameBaseline coveragePR coverageCoverage Diff
Branch Coverage 93.42% 93.42% → No change
Line Coverage 90.06% 90.23% ↑ 0.17%
↑ packages.dds.tree.src.events:
Line Coverage Change: 1.10%    Branch Coverage Change: -0.34%
Metric NameBaseline coveragePR coverageCoverage Diff
Branch Coverage 92.00% 91.66% ↓ -0.34%
Line Coverage 70.55% 71.65% ↑ 1.10%

Baseline commit: bb64a8e
Baseline build: 305482
Happy Coding!!

Code coverage comparison check passed!!

@msfluid-bot
Copy link
Collaborator

@fluid-example/bundle-size-tests: +3.05 KB
Metric NameBaseline SizeCompare SizeSize Diff
aqueduct.js 463.74 KB 463.77 KB +35 Bytes
azureClient.js 562.69 KB 562.74 KB +49 Bytes
connectionState.js 724 Bytes 724 Bytes No change
containerRuntime.js 261.99 KB 262 KB +14 Bytes
fluidFramework.js 425.14 KB 426.56 KB +1.42 KB
loader.js 134.18 KB 134.19 KB +14 Bytes
map.js 42.71 KB 42.71 KB +7 Bytes
matrix.js 148.32 KB 148.33 KB +7 Bytes
odspClient.js 528.43 KB 528.48 KB +49 Bytes
odspDriver.js 97.84 KB 97.86 KB +21 Bytes
odspPrefetchSnapshot.js 42.81 KB 42.83 KB +14 Bytes
sharedString.js 164.17 KB 164.17 KB +7 Bytes
sharedTree.js 415.6 KB 417.01 KB +1.41 KB
Total Size 3.36 MB 3.36 MB +3.05 KB

Baseline commit: bb64a8e

Generated by 🚫 dangerJS against 321b9e4

@noencke noencke merged commit aa84db1 into microsoft:main Nov 8, 2024
59 checks passed
Comment on lines +105 to +106
* private readonly _events = createEmitter<MyEvents>();
* public readonly events: Listenable<MyEvents> = this._events;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* private readonly _events = createEmitter<MyEvents>();
* public readonly events: Listenable<MyEvents> = this._events;
* private readonly _events = createEmitter<MyEvents>();
* public readonly events: Listenable<MyEvents> = this._events;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ugh - that's weird. Thanks. I'll fix it in the next event PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
base: main PRs targeted against main branch
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants