Skip to content

Commit

Permalink
Update internal SharedTree types to use the .events pattern (micros…
Browse files Browse the repository at this point in the history
…oft#23038)

## 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.
  • Loading branch information
noencke authored Nov 8, 2024
1 parent bb64a8e commit aa84db1
Show file tree
Hide file tree
Showing 32 changed files with 191 additions and 180 deletions.
7 changes: 6 additions & 1 deletion packages/dds/tree/src/core/forest/forest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,12 @@ export interface ForestEvents {
*
* When invalidating, all outstanding cursors must be freed or cleared.
*/
export interface IForestSubscription extends Listenable<ForestEvents> {
export interface IForestSubscription {
/**
* Events for this forest.
*/
readonly events: Listenable<ForestEvents>;

/**
* Set of anchors this forest is tracking.
*
Expand Down
2 changes: 1 addition & 1 deletion packages/dds/tree/src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ export {
type TreeFieldStoredSchema,
ValueSchema,
TreeNodeStoredSchema,
type TreeStoredSchemaSubscription as TreeStoredSchemaSubscription,
type TreeStoredSchemaSubscription,
type MutableTreeStoredSchema,
type FieldKindIdentifier,
type FieldKindData,
Expand Down
23 changes: 10 additions & 13 deletions packages/dds/tree/src/core/schema-stored/storedSchemaRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,12 @@ export interface SchemaEvents {
/**
* A collection of stored schema that fires events in response to changes.
*/
export interface TreeStoredSchemaSubscription
extends Listenable<SchemaEvents>,
TreeStoredSchema {}
export interface TreeStoredSchemaSubscription extends TreeStoredSchema {
/**
* Events for this schema subscription.
*/
readonly events: Listenable<SchemaEvents>;
}

/**
* Mutable collection of stored schema.
Expand All @@ -59,7 +62,8 @@ export interface MutableTreeStoredSchema extends TreeStoredSchemaSubscription {
export class TreeStoredSchemaRepository implements MutableTreeStoredSchema {
protected nodeSchemaData: BTree<TreeNodeSchemaIdentifier, TreeNodeStoredSchema>;
protected rootFieldSchemaData: TreeFieldStoredSchema;
protected readonly events = createEmitter<SchemaEvents>();
protected readonly _events = createEmitter<SchemaEvents>();
public readonly events: Listenable<SchemaEvents> = this._events;

/**
* Copies in the provided schema. If `data` is an TreeStoredSchemaRepository, it will be cheap-cloned.
Expand Down Expand Up @@ -92,13 +96,6 @@ export class TreeStoredSchemaRepository implements MutableTreeStoredSchema {
}
}

public on<K extends keyof SchemaEvents>(
eventName: K,
listener: SchemaEvents[K],
): () => void {
return this.events.on(eventName, listener);
}

public get nodeSchema(): ReadonlyMap<TreeNodeSchemaIdentifier, TreeNodeStoredSchema> {
// Btree implements iterator, but not in a type-safe way
return this.nodeSchemaData as unknown as ReadonlyMap<
Expand All @@ -112,12 +109,12 @@ export class TreeStoredSchemaRepository implements MutableTreeStoredSchema {
}

public apply(newSchema: TreeStoredSchema): void {
this.events.emit("beforeSchemaChange", newSchema);
this._events.emit("beforeSchemaChange", newSchema);
const clone = new TreeStoredSchemaRepository(newSchema);
// In the future, we could use btree's delta functionality to do a more efficient update
this.rootFieldSchemaData = clone.rootFieldSchemaData;
this.nodeSchemaData = clone.nodeSchemaData;
this.events.emit("afterSchemaChange", newSchema);
this._events.emit("afterSchemaChange", newSchema);
}

public clone(): TreeStoredSchemaRepository {
Expand Down
33 changes: 13 additions & 20 deletions packages/dds/tree/src/core/tree/anchorSet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,12 @@ export interface AnchorSetRootEvents {
/**
* Node in a tree of anchors.
*/
export interface AnchorNode extends UpPath<AnchorNode>, Listenable<AnchorEvents> {
export interface AnchorNode extends UpPath<AnchorNode> {
/**
* Events for this anchor node.
*/
readonly events: Listenable<AnchorEvents>;

/**
* Allows access to data stored on the Anchor in "slots".
* Use {@link anchorSlot} to create slots.
Expand Down Expand Up @@ -277,8 +282,10 @@ export function anchorSlot<TContent>(): AnchorSlot<TContent> {
*
* @sealed
*/
export class AnchorSet implements Listenable<AnchorSetRootEvents>, AnchorLocator {
private readonly events = createEmitter<AnchorSetRootEvents>();
export class AnchorSet implements AnchorLocator {
readonly #events = createEmitter<AnchorSetRootEvents>();
public readonly events: Listenable<AnchorSetRootEvents> = this.#events;

/**
* Incrementing counter to give each anchor in this set a unique index for its identifier.
* "0" is reserved for the `NeverAnchor`.
Expand Down Expand Up @@ -312,7 +319,7 @@ export class AnchorSet implements Listenable<AnchorSetRootEvents>, AnchorLocator
private activeVisitor?: DeltaVisitor;

public constructor() {
this.on("treeChanging", () => {
this.events.on("treeChanging", () => {
this.generationNumber += 1;
});
}
Expand Down Expand Up @@ -344,13 +351,6 @@ export class AnchorSet implements Listenable<AnchorSetRootEvents>, AnchorLocator
}
}

public on<K extends keyof AnchorSetRootEvents>(
eventName: K,
listener: AnchorSetRootEvents[K],
): () => void {
return this.events.on(eventName, listener);
}

/**
* Check if there are currently no anchors tracked.
* Mainly for testing anchor cleanup.
Expand Down Expand Up @@ -792,7 +792,7 @@ export class AnchorSet implements Listenable<AnchorSetRootEvents>, AnchorLocator
notifyChildrenChanging(): void {
this.maybeWithNode(
(p) => p.events.emit("childrenChanging", p),
() => this.anchorSet.events.emit("childrenChanging", this.anchorSet),
() => this.anchorSet.#events.emit("childrenChanging", this.anchorSet),
);
},
notifyChildrenChanged(): void {
Expand Down Expand Up @@ -1098,7 +1098,7 @@ export class AnchorSet implements Listenable<AnchorSetRootEvents>, AnchorLocator
this.parentField = undefined;
},
};
this.events.emit("treeChanging", this);
this.#events.emit("treeChanging", this);
this.activeVisitor = visitor;
return visitor;
}
Expand Down Expand Up @@ -1209,13 +1209,6 @@ class PathNode extends ReferenceCountedBase implements UpPath<PathNode>, AnchorN
super(1);
}

public on<K extends keyof AnchorEvents>(
eventName: K,
listener: AnchorEvents[K],
): () => void {
return this.events.on(eventName, listener);
}

public child(key: FieldKey, index: number): UpPath<AnchorNode> {
// Fast path: if child exists, return it.
return (
Expand Down
16 changes: 15 additions & 1 deletion packages/dds/tree/src/events/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export interface HasListeners<TListeners extends Listeners<TListeners>> {
/**
* Provides an API for subscribing to and listening to events.
*
* @remarks Classes wishing to emit events may either extend this class or compose over it.
* @remarks Classes wishing to emit events may either extend this class, compose over it, or expose it as a property of type {@link Listenable}.
*
* @example Extending this class
*
Expand Down Expand Up @@ -97,6 +97,20 @@ export interface HasListeners<TListeners extends Listeners<TListeners>> {
* }
* }
* ```
*
* @example Exposing this class as a property
*
* ```typescript
* class MyExposingClass {
* private readonly _events = createEmitter<MyEvents>();
* public readonly events: Listenable<MyEvents> = this._events;
*
* private load() {
* this._events.emit("loaded");
* const results: number[] = this._events.emitAndCollect("computed");
* }
* }
* ```
*/
export class EventEmitter<TListeners extends Listeners<TListeners>>
implements Listenable<TListeners>, HasListeners<TListeners>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ export class Chunker implements IChunker {
if (cached !== undefined) {
return cached;
}
this.unregisterSchemaCallback = this.schema.on("afterSchemaChange", () =>
this.unregisterSchemaCallback = this.schema.events.on("afterSchemaChange", () =>
this.schemaChanged(),
);
return this.tryShapeFromSchema(this.schema, this.policy, schema, this.typeShapes);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {
mapCursorField,
rootFieldKey,
} from "../../core/index.js";
import { createEmitter } from "../../events/index.js";
import { createEmitter, type Listenable } from "../../events/index.js";
import { assertValidRange, brand, fail, getOrAddEmptyToMap } from "../../util/index.js";

import { BasicChunk, BasicChunkCursor, type SiblingsOrKey } from "./basicChunk.js";
Expand All @@ -53,7 +53,8 @@ interface StackNode {
export class ChunkedForest implements IEditableForest {
private activeVisitor?: DeltaVisitor;

private readonly events = createEmitter<ForestEvents>();
readonly #events = createEmitter<ForestEvents>();
public readonly events: Listenable<ForestEvents> = this.#events;

/**
* @param roots - dummy node above the root under which detached fields are stored. All content of the forest is reachable from this.
Expand All @@ -73,13 +74,6 @@ export class ChunkedForest implements IEditableForest {
return this.roots.fields.size === 0;
}

public on<K extends keyof ForestEvents>(
eventName: K,
listener: ForestEvents[K],
): () => void {
return this.events.on(eventName, listener);
}

public clone(schema: TreeStoredSchemaSubscription, anchors: AnchorSet): ChunkedForest {
this.roots.referenceAdded();
return new ChunkedForest(this.roots, schema, this.chunker.clone(schema), anchors);
Expand Down Expand Up @@ -119,19 +113,19 @@ export class ChunkedForest implements IEditableForest {
this.forest.activeVisitor = undefined;
},
destroy(detachedField: FieldKey, count: number): void {
this.forest.events.emit("beforeChange");
this.forest.#events.emit("beforeChange");
this.forest.roots.fields.delete(detachedField);
},
create(content: ProtoNodes, destination: FieldKey): void {
this.forest.events.emit("beforeChange");
this.forest.#events.emit("beforeChange");
const chunks: TreeChunk[] = content.map((c) =>
chunkTree(c, {
policy: this.forest.chunker,
idCompressor: this.forest.idCompressor,
}),
);
this.forest.roots.fields.set(destination, chunks);
this.forest.events.emit("afterRootFieldCreated", destination);
this.forest.#events.emit("afterRootFieldCreated", destination);
},
attach(source: FieldKey, count: number, destination: PlaceIndex): void {
this.attachEdit(source, count, destination);
Expand All @@ -146,7 +140,7 @@ export class ChunkedForest implements IEditableForest {
* @param destination - The index in the current field at which to attach the content.
*/
attachEdit(source: FieldKey, count: number, destination: PlaceIndex): void {
this.forest.events.emit("beforeChange");
this.forest.#events.emit("beforeChange");
const sourceField = this.forest.roots.fields.get(source) ?? [];
this.forest.roots.fields.delete(source);
if (sourceField.length === 0) {
Expand All @@ -166,7 +160,7 @@ export class ChunkedForest implements IEditableForest {
* If not specified, the detached range is destroyed.
*/
detachEdit(source: Range, destination: FieldKey | undefined): void {
this.forest.events.emit("beforeChange");
this.forest.#events.emit("beforeChange");
const parent = this.getParent();
const sourceField = parent.mutableChunk.fields.get(parent.key) ?? [];

Expand Down
12 changes: 5 additions & 7 deletions packages/dds/tree/src/feature-libraries/flex-tree/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ export interface FlexTreeContext {
* A common context of a "forest" of FlexTrees.
* It handles group operations like transforming cursors into anchors for edits.
*/
export interface FlexTreeHydratedContext extends FlexTreeContext, Listenable<ForestEvents> {
export interface FlexTreeHydratedContext extends FlexTreeContext {
readonly events: Listenable<ForestEvents>;
/**
* Gets the root field of the tree.
*/
Expand Down Expand Up @@ -92,7 +93,7 @@ export class Context implements FlexTreeHydratedContext, IDisposable {
public readonly nodeKeyManager: NodeKeyManager,
) {
this.eventUnregister = [
this.checkout.forest.on("beforeChange", () => {
this.checkout.forest.events.on("beforeChange", () => {
this.prepareForEdit();
}),
];
Expand Down Expand Up @@ -160,11 +161,8 @@ export class Context implements FlexTreeHydratedContext, IDisposable {
return field;
}

public on<K extends keyof ForestEvents>(
eventName: K,
listener: ForestEvents[K],
): () => void {
return this.checkout.forest.on(eventName, listener);
public get events(): Listenable<ForestEvents> {
return this.checkout.forest.events;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ export abstract class LazyField extends LazyEntity<FieldAnchor> implements FlexT
const anchorNode =
context.checkout.forest.anchors.locate(fieldAnchor.parent) ??
fail("parent anchor node should always exist since field is under a node");
this.offAfterDestroy = anchorNode.on("afterDestroy", () => {
this.offAfterDestroy = anchorNode.events.on("afterDestroy", () => {
this[disposeSymbol]();
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export class LazyTreeNode extends LazyEntity<Anchor> implements FlexTreeNode {
this.storedSchema = context.schema.nodeSchema.get(this.schema) ?? fail("missing schema");
assert(cursor.mode === CursorLocationType.Nodes, 0x783 /* must be in nodes mode */);
anchorNode.slots.set(flexTreeSlot, this);
this.#removeDeleteCallback = anchorNode.on("afterDestroy", cleanupTree);
this.#removeDeleteCallback = anchorNode.events.on("afterDestroy", cleanupTree);
}

public borrowCursor(): ITreeCursorSynchronous {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import {
aboveRootPlaceholder,
deepCopyMapTree,
} from "../../core/index.js";
import { createEmitter } from "../../events/index.js";
import { createEmitter, type Listenable } from "../../events/index.js";
import {
assertNonNegativeSafeInteger,
assertValidIndex,
Expand Down Expand Up @@ -73,7 +73,8 @@ export class ObjectForest implements IEditableForest {
// All cursors that are in the "Current" state. Must be empty when editing.
public readonly currentCursors: Set<Cursor> = new Set();

private readonly events = createEmitter<ForestEvents>();
readonly #events = createEmitter<ForestEvents>();
public readonly events: Listenable<ForestEvents> = this.#events;

readonly #roots: MutableMapTree;
public get roots(): MapTree {
Expand All @@ -98,13 +99,6 @@ export class ObjectForest implements IEditableForest {
return this.roots.fields.size === 0;
}

public on<K extends keyof ForestEvents>(
eventName: K,
listener: ForestEvents[K],
): () => void {
return this.events.on(eventName, listener);
}

public clone(_: TreeStoredSchemaSubscription, anchors: AnchorSet): ObjectForest {
return new ObjectForest(anchors, this.additionalAsserts, this.roots);
}
Expand Down Expand Up @@ -133,7 +127,7 @@ export class ObjectForest implements IEditableForest {
* This is required for each change since there may be app facing change event handlers which create cursors.
*/
const preEdit = (): void => {
this.events.emit("beforeChange");
this.#events.emit("beforeChange");
assert(
this.currentCursors.has(cursor),
0x995 /* missing visitor cursor while editing */,
Expand Down Expand Up @@ -168,7 +162,7 @@ export class ObjectForest implements IEditableForest {
public create(content: ProtoNodes, destination: FieldKey): void {
preEdit();
this.forest.add(content, destination);
this.forest.events.emit("afterRootFieldCreated", destination);
this.forest.#events.emit("afterRootFieldCreated", destination);
}
public attach(source: FieldKey, count: number, destination: PlaceIndex): void {
preEdit();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export class SchemaSummarizer implements Summarizable {
collabWindow: CollabWindow,
) {
this.codec = makeSchemaCodec(options);
this.schema.on("afterSchemaChange", () => {
this.schema.events.on("afterSchemaChange", () => {
// Invalidate the cache, as we need to regenerate the blob if the schema changes
// We are assuming that schema changes from remote ops are valid, as we are in a summarization context.
this.schemaIndexLastChangedSeq = collabWindow.getCurrentSeq();
Expand Down
Loading

0 comments on commit aa84db1

Please sign in to comment.