@fluidframework/tree 2.61.0-355934 → 2.61.0-356132

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 (66) hide show
  1. package/api-report/tree.alpha.api.md +8 -0
  2. package/dist/feature-libraries/flex-tree/flexTreeTypes.d.ts +2 -0
  3. package/dist/feature-libraries/flex-tree/flexTreeTypes.d.ts.map +1 -1
  4. package/dist/feature-libraries/flex-tree/flexTreeTypes.js.map +1 -1
  5. package/dist/feature-libraries/flex-tree/index.d.ts +1 -0
  6. package/dist/feature-libraries/flex-tree/index.d.ts.map +1 -1
  7. package/dist/feature-libraries/flex-tree/index.js +4 -1
  8. package/dist/feature-libraries/flex-tree/index.js.map +1 -1
  9. package/dist/feature-libraries/flex-tree/lazyNode.d.ts.map +1 -1
  10. package/dist/feature-libraries/flex-tree/lazyNode.js +15 -8
  11. package/dist/feature-libraries/flex-tree/lazyNode.js.map +1 -1
  12. package/dist/feature-libraries/flex-tree/observer.d.ts +27 -0
  13. package/dist/feature-libraries/flex-tree/observer.d.ts.map +1 -0
  14. package/dist/feature-libraries/flex-tree/observer.js +33 -0
  15. package/dist/feature-libraries/flex-tree/observer.js.map +1 -0
  16. package/dist/feature-libraries/index.d.ts +1 -1
  17. package/dist/feature-libraries/index.d.ts.map +1 -1
  18. package/dist/feature-libraries/index.js +3 -1
  19. package/dist/feature-libraries/index.js.map +1 -1
  20. package/dist/packageVersion.d.ts +1 -1
  21. package/dist/packageVersion.js +1 -1
  22. package/dist/packageVersion.js.map +1 -1
  23. package/dist/shared-tree/treeAlpha.d.ts +28 -1
  24. package/dist/shared-tree/treeAlpha.d.ts.map +1 -1
  25. package/dist/shared-tree/treeAlpha.js +139 -0
  26. package/dist/shared-tree/treeAlpha.js.map +1 -1
  27. package/dist/simple-tree/core/unhydratedFlexTree.d.ts.map +1 -1
  28. package/dist/simple-tree/core/unhydratedFlexTree.js +7 -1
  29. package/dist/simple-tree/core/unhydratedFlexTree.js.map +1 -1
  30. package/lib/feature-libraries/flex-tree/flexTreeTypes.d.ts +2 -0
  31. package/lib/feature-libraries/flex-tree/flexTreeTypes.d.ts.map +1 -1
  32. package/lib/feature-libraries/flex-tree/flexTreeTypes.js.map +1 -1
  33. package/lib/feature-libraries/flex-tree/index.d.ts +1 -0
  34. package/lib/feature-libraries/flex-tree/index.d.ts.map +1 -1
  35. package/lib/feature-libraries/flex-tree/index.js +1 -0
  36. package/lib/feature-libraries/flex-tree/index.js.map +1 -1
  37. package/lib/feature-libraries/flex-tree/lazyNode.d.ts.map +1 -1
  38. package/lib/feature-libraries/flex-tree/lazyNode.js +15 -8
  39. package/lib/feature-libraries/flex-tree/lazyNode.js.map +1 -1
  40. package/lib/feature-libraries/flex-tree/observer.d.ts +27 -0
  41. package/lib/feature-libraries/flex-tree/observer.d.ts.map +1 -0
  42. package/lib/feature-libraries/flex-tree/observer.js +35 -0
  43. package/lib/feature-libraries/flex-tree/observer.js.map +1 -0
  44. package/lib/feature-libraries/index.d.ts +1 -1
  45. package/lib/feature-libraries/index.d.ts.map +1 -1
  46. package/lib/feature-libraries/index.js +1 -1
  47. package/lib/feature-libraries/index.js.map +1 -1
  48. package/lib/packageVersion.d.ts +1 -1
  49. package/lib/packageVersion.js +1 -1
  50. package/lib/packageVersion.js.map +1 -1
  51. package/lib/shared-tree/treeAlpha.d.ts +28 -1
  52. package/lib/shared-tree/treeAlpha.d.ts.map +1 -1
  53. package/lib/shared-tree/treeAlpha.js +141 -2
  54. package/lib/shared-tree/treeAlpha.js.map +1 -1
  55. package/lib/simple-tree/core/unhydratedFlexTree.d.ts.map +1 -1
  56. package/lib/simple-tree/core/unhydratedFlexTree.js +8 -2
  57. package/lib/simple-tree/core/unhydratedFlexTree.js.map +1 -1
  58. package/package.json +21 -21
  59. package/src/feature-libraries/flex-tree/flexTreeTypes.ts +2 -0
  60. package/src/feature-libraries/flex-tree/index.ts +2 -0
  61. package/src/feature-libraries/flex-tree/lazyNode.ts +13 -3
  62. package/src/feature-libraries/flex-tree/observer.ts +58 -0
  63. package/src/feature-libraries/index.ts +3 -0
  64. package/src/packageVersion.ts +1 -1
  65. package/src/shared-tree/treeAlpha.ts +213 -2
  66. package/src/simple-tree/core/unhydratedFlexTree.ts +11 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fluidframework/tree",
3
- "version": "2.61.0-355934",
3
+ "version": "2.61.0-356132",
4
4
  "description": "Distributed tree",
5
5
  "homepage": "https://fluidframework.com",
6
6
  "repository": {
@@ -101,17 +101,17 @@
101
101
  "temp-directory": "nyc/.nyc_output"
102
102
  },
103
103
  "dependencies": {
104
- "@fluid-internal/client-utils": "2.61.0-355934",
105
- "@fluidframework/container-runtime": "2.61.0-355934",
106
- "@fluidframework/core-interfaces": "2.61.0-355934",
107
- "@fluidframework/core-utils": "2.61.0-355934",
108
- "@fluidframework/datastore-definitions": "2.61.0-355934",
109
- "@fluidframework/driver-definitions": "2.61.0-355934",
110
- "@fluidframework/id-compressor": "2.61.0-355934",
111
- "@fluidframework/runtime-definitions": "2.61.0-355934",
112
- "@fluidframework/runtime-utils": "2.61.0-355934",
113
- "@fluidframework/shared-object-base": "2.61.0-355934",
114
- "@fluidframework/telemetry-utils": "2.61.0-355934",
104
+ "@fluid-internal/client-utils": "2.61.0-356132",
105
+ "@fluidframework/container-runtime": "2.61.0-356132",
106
+ "@fluidframework/core-interfaces": "2.61.0-356132",
107
+ "@fluidframework/core-utils": "2.61.0-356132",
108
+ "@fluidframework/datastore-definitions": "2.61.0-356132",
109
+ "@fluidframework/driver-definitions": "2.61.0-356132",
110
+ "@fluidframework/id-compressor": "2.61.0-356132",
111
+ "@fluidframework/runtime-definitions": "2.61.0-356132",
112
+ "@fluidframework/runtime-utils": "2.61.0-356132",
113
+ "@fluidframework/shared-object-base": "2.61.0-356132",
114
+ "@fluidframework/telemetry-utils": "2.61.0-356132",
115
115
  "@sinclair/typebox": "^0.34.13",
116
116
  "@tylerbu/sorted-btree-es6": "^1.8.0",
117
117
  "@types/ungap__structured-clone": "^1.2.0",
@@ -121,19 +121,19 @@
121
121
  "devDependencies": {
122
122
  "@arethetypeswrong/cli": "^0.17.1",
123
123
  "@biomejs/biome": "~1.9.3",
124
- "@fluid-internal/mocha-test-setup": "2.61.0-355934",
125
- "@fluid-private/stochastic-test-utils": "2.61.0-355934",
126
- "@fluid-private/test-dds-utils": "2.61.0-355934",
127
- "@fluid-private/test-drivers": "2.61.0-355934",
124
+ "@fluid-internal/mocha-test-setup": "2.61.0-356132",
125
+ "@fluid-private/stochastic-test-utils": "2.61.0-356132",
126
+ "@fluid-private/test-dds-utils": "2.61.0-356132",
127
+ "@fluid-private/test-drivers": "2.61.0-356132",
128
128
  "@fluid-tools/benchmark": "^0.51.0",
129
129
  "@fluid-tools/build-cli": "^0.58.3",
130
130
  "@fluidframework/build-common": "^2.0.3",
131
131
  "@fluidframework/build-tools": "^0.58.3",
132
- "@fluidframework/container-definitions": "2.61.0-355934",
133
- "@fluidframework/container-loader": "2.61.0-355934",
132
+ "@fluidframework/container-definitions": "2.61.0-356132",
133
+ "@fluidframework/container-loader": "2.61.0-356132",
134
134
  "@fluidframework/eslint-config-fluid": "^6.0.0",
135
- "@fluidframework/test-runtime-utils": "2.61.0-355934",
136
- "@fluidframework/test-utils": "2.61.0-355934",
135
+ "@fluidframework/test-runtime-utils": "2.61.0-356132",
136
+ "@fluidframework/test-utils": "2.61.0-356132",
137
137
  "@fluidframework/tree-previous": "npm:@fluidframework/tree@2.60.0",
138
138
  "@microsoft/api-extractor": "7.52.11",
139
139
  "@types/diff": "^3.5.1",
@@ -227,7 +227,7 @@
227
227
  "test:customBenchmarks:verbose": "cross-env FLUID_TEST_VERBOSE=1 npm run test:customBenchmarks",
228
228
  "test:memory": "mocha --perfMode --config ./src/test/memory/.mocharc.cjs",
229
229
  "test:memory-profiling:report": "mocha --config ./src/test/memory/.mocharc.cjs",
230
- "test:mocha": "npm run test:mocha:esm && echo skipping cjs to avoid overhead - npm run test:mocha:cjs",
230
+ "test:mocha": "echo skipping non-smoke cjs to avoid overhead && npm run test:mocha:cjs -- --fgrep @Smoke && npm run test:mocha:esm",
231
231
  "test:mocha:cjs": "cross-env MOCHA_SPEC=dist/test mocha",
232
232
  "test:mocha:esm": "mocha",
233
233
  "test:mocha:verbose": "cross-env FLUID_TEST_VERBOSE=1 npm run test:mocha",
@@ -151,6 +151,8 @@ export enum TreeStatus {
151
151
  *
152
152
  * @remarks
153
153
  * All editing is actually done via {@link FlexTreeField}s: the nodes are immutable other than that they contain mutable fields.
154
+ *
155
+ * All implementations should track read access in {@link currentObserver}'s observation methods as appropriate.
154
156
  */
155
157
  export interface FlexTreeNode extends FlexTreeEntity, MapTreeNodeViewGeneric<FlexTreeNode> {
156
158
  readonly [flexTreeMarker]: FlexTreeEntityKind.Node;
@@ -42,3 +42,5 @@ export { getOrCreateHydratedFlexTreeNode } from "./lazyNode.js";
42
42
  export { getSchemaAndPolicy, indexForAt } from "./utilities.js";
43
43
 
44
44
  export { treeStatusFromAnchorCache } from "./utilities.js";
45
+
46
+ export { currentObserver, withObservation, type Observer } from "./observer.js";
@@ -33,6 +33,7 @@ import {
33
33
  } from "./flexTreeTypes.js";
34
34
  import { LazyEntity } from "./lazyEntity.js";
35
35
  import { makeField } from "./lazyField.js";
36
+ import { currentObserver } from "./observer.js";
36
37
 
37
38
  /**
38
39
  * Get or create a {@link HydratedFlexTreeNode} for the given context at node indicated by the cursor.
@@ -118,18 +119,23 @@ export class LazyTreeNode extends LazyEntity<Anchor> implements HydratedFlexTree
118
119
  public readonly fields: Pick<Map<FieldKey, FlexTreeField>, typeof Symbol.iterator | "get"> =
119
120
  {
120
121
  get: (key: FieldKey): FlexTreeField | undefined => this.tryGetField(key),
121
- [Symbol.iterator]: (): IterableIterator<[FieldKey, FlexTreeField]> =>
122
- mapCursorFields(this.cursor, (cursor) => {
122
+ [Symbol.iterator]: (): IterableIterator<[FieldKey, FlexTreeField]> => {
123
+ currentObserver?.observeNodeFields(this);
124
+
125
+ return mapCursorFields(this.cursor, (cursor) => {
123
126
  const key: FieldKey = cursor.getFieldKey();
124
127
  const pair: [FieldKey, FlexTreeField] = [
125
128
  key,
126
129
  makeField(this.context, this.storedSchema.getFieldSchema(key).kind, cursor),
127
130
  ];
128
131
  return pair;
129
- }).values(),
132
+ }).values();
133
+ },
130
134
  };
131
135
 
132
136
  public tryGetField(fieldKey: FieldKey): FlexTreeField | undefined {
137
+ currentObserver?.observeNodeField(this, fieldKey);
138
+
133
139
  const schema = this.storedSchema.getFieldSchema(fieldKey);
134
140
  return inCursorField(this.cursor, fieldKey, (cursor) => {
135
141
  if (cursor.getFieldLength() === 0) {
@@ -140,6 +146,8 @@ export class LazyTreeNode extends LazyEntity<Anchor> implements HydratedFlexTree
140
146
  }
141
147
 
142
148
  public getBoxed(key: FieldKey): FlexTreeField {
149
+ currentObserver?.observeNodeField(this, key);
150
+
143
151
  const fieldSchema = this.storedSchema.getFieldSchema(key);
144
152
  return inCursorField(this.cursor, key, (cursor) => {
145
153
  return makeField(this.context, fieldSchema.kind, cursor);
@@ -157,6 +165,8 @@ export class LazyTreeNode extends LazyEntity<Anchor> implements HydratedFlexTree
157
165
  }
158
166
 
159
167
  public get parentField(): { readonly parent: FlexTreeField; readonly index: number } {
168
+ currentObserver?.observeParentOf(this);
169
+
160
170
  const cursor = this.cursor;
161
171
  const index = this.anchorNode.parentIndex;
162
172
  assert(cursor.fieldIndex === index, 0x786 /* mismatched indexes */);
@@ -0,0 +1,58 @@
1
+ /*!
2
+ * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
+ * Licensed under the MIT License.
4
+ */
5
+
6
+ import { debugAssert } from "@fluidframework/core-utils/internal";
7
+ import type { FlexTreeNode } from "./flexTreeTypes.js";
8
+ import type { FieldKey } from "../../core/index.js";
9
+
10
+ /*
11
+ * This file sets up a static observation tracking system.
12
+ *
13
+ * This library used to contain a more general variant of this which was deleted in https://github.com/microsoft/FluidFramework/pull/18659.
14
+ * This pattern somewhat resembles the approach in https://github.com/tc39/proposal-signals.
15
+ */
16
+
17
+ /**
18
+ * An object informed about observation made to trees.
19
+ * @remarks
20
+ * See {@link withObservation} and {@link currentObserver}.
21
+ */
22
+ export interface Observer {
23
+ observeNodeFields(node: FlexTreeNode): void;
24
+ observeNodeField(node: FlexTreeNode, key: FieldKey): void;
25
+ observeParentOf(node: FlexTreeNode): void;
26
+ }
27
+ /**
28
+ * The current observer, if any.
29
+ * @remarks
30
+ * Set via {@link setObserver} as used by {@link withObservation}.
31
+ */
32
+ export let currentObserver: Observer | undefined;
33
+
34
+ const observerStack: (Observer | undefined)[] = [];
35
+
36
+ function setObserver(newObserver: Observer | undefined): void {
37
+ observerStack.push(newObserver);
38
+ currentObserver = newObserver;
39
+ }
40
+
41
+ function clearObserver(): void {
42
+ debugAssert(() => observerStack.length > 0 || "Empty Observer stack on clear");
43
+ const popped = observerStack.pop();
44
+ debugAssert(() => popped === currentObserver || "Mismatched observer stack");
45
+ currentObserver = observerStack[observerStack.length - 1];
46
+ }
47
+
48
+ /**
49
+ * For the duration of `f`, pushes `newObserver` onto the observer stack, making it the {@link currentObserver}.
50
+ */
51
+ export function withObservation<T>(newObserver: Observer | undefined, f: () => T): T {
52
+ setObserver(newObserver);
53
+ try {
54
+ return f();
55
+ } finally {
56
+ clearObserver();
57
+ }
58
+ }
@@ -171,6 +171,9 @@ export {
171
171
  type FlexTreeHydratedContextMinimal,
172
172
  type HydratedFlexTreeNode,
173
173
  getOrCreateHydratedFlexTreeNode,
174
+ currentObserver,
175
+ withObservation,
176
+ type Observer,
174
177
  } from "./flex-tree/index.js";
175
178
 
176
179
  export {
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  export const pkgName = "@fluidframework/tree";
9
- export const pkgVersion = "2.61.0-355934";
9
+ export const pkgVersion = "2.61.0-356132";
@@ -17,7 +17,7 @@ import type { IIdCompressor } from "@fluidframework/id-compressor";
17
17
  import {
18
18
  asIndex,
19
19
  getKernel,
20
- type TreeNode,
20
+ TreeNode,
21
21
  type Unhydrated,
22
22
  TreeBeta,
23
23
  tryGetSchema,
@@ -55,6 +55,7 @@ import {
55
55
  convertField,
56
56
  toUnhydratedSchema,
57
57
  type TreeParsingOptions,
58
+ type NodeChangedData,
58
59
  } from "../simple-tree/index.js";
59
60
  import { brand, extractFromOpaque, type JsonCompatible } from "../util/index.js";
60
61
  import {
@@ -63,7 +64,7 @@ import {
63
64
  type ICodecOptions,
64
65
  type CodecWriteOptions,
65
66
  } from "../codec/index.js";
66
- import { EmptyKey, type ITreeCursorSynchronous } from "../core/index.js";
67
+ import { EmptyKey, type FieldKey, type ITreeCursorSynchronous } from "../core/index.js";
67
68
  import {
68
69
  cursorForMapTreeField,
69
70
  defaultSchemaPolicy,
@@ -76,6 +77,9 @@ import {
76
77
  fluidVersionToFieldBatchCodecWriteVersion,
77
78
  type LocalNodeIdentifier,
78
79
  type FlexTreeSequenceField,
80
+ type FlexTreeNode,
81
+ type Observer,
82
+ withObservation,
79
83
  } from "../feature-libraries/index.js";
80
84
  import { independentInitializedView, type ViewContent } from "./independentView.js";
81
85
  import { SchematizingSimpleTreeView, ViewSlot } from "./schematizingTreeView.js";
@@ -419,6 +423,189 @@ export interface TreeAlpha {
419
423
  children(
420
424
  node: TreeNode,
421
425
  ): Iterable<[propertyKey: string | number, child: TreeNode | TreeLeafValue]>;
426
+
427
+ /**
428
+ * Track observations of any TreeNode content.
429
+ * @remarks
430
+ * This subscribes to changes to any nodes content observed during `trackDuring`.
431
+ *
432
+ * Currently this does not support tracking parentage (see {@link (TreeAlpha:interface).trackObservationsOnce} for a version which does):
433
+ * if accessing parentage during `trackDuring`, this will throw a usage error.
434
+ *
435
+ * This also does not track node status changes (e.g. whether a node is attached to a view or not).
436
+ * The current behavior of checking status is unspecified: future versions may track it, error, or ignore it.
437
+ *
438
+ * Even after onInvalidation is called, these subscriptions remain active until `unsubscribe` is called.
439
+ * See {@link (TreeAlpha:interface).trackObservationsOnce} for a version which automatically unsubscribes on the first invalidation.
440
+ */
441
+ trackObservations<TResult>(
442
+ onInvalidation: () => void,
443
+ trackDuring: () => TResult,
444
+ ): { result: TResult; unsubscribe: () => void };
445
+
446
+ /**
447
+ * {@link (TreeAlpha:interface).trackObservations} except automatically unsubscribes when the first invalidation occurs.
448
+ * @remarks
449
+ * This also supports tracking parentage, unlike {@link (TreeAlpha:interface).trackObservations}, as long as the parent is not undefined.
450
+ */
451
+ trackObservationsOnce<TResult>(
452
+ onInvalidation: () => void,
453
+ trackDuring: () => TResult,
454
+ ): { result: TResult; unsubscribe: () => void };
455
+ }
456
+
457
+ /**
458
+ * Subscription to changes on a single node.
459
+ * @remarks
460
+ * Either tracks some set of fields, or all fields and can be updated to track more fields.
461
+ */
462
+ class NodeSubscription {
463
+ /**
464
+ * If undefined, subscribes to all keys.
465
+ * Otherwise only subscribes to the keys in the set.
466
+ */
467
+ private keys: Set<FieldKey> | undefined;
468
+ private readonly unsubscribe: () => void;
469
+ private constructor(
470
+ private readonly onInvalidation: () => void,
471
+ flexNode: FlexTreeNode,
472
+ ) {
473
+ // TODO:Performance: It is possible to optimize this to not use the public TreeNode API.
474
+ const node = getOrCreateNodeFromInnerNode(flexNode);
475
+ assert(node instanceof TreeNode, "Unexpected leaf value");
476
+
477
+ const handler = (data: NodeChangedData): void => {
478
+ if (this.keys === undefined || data.changedProperties === undefined) {
479
+ this.onInvalidation();
480
+ } else {
481
+ let keyMap: ReadonlyMap<FieldKey, string> | undefined;
482
+ const schema = treeNodeApi.schema(node);
483
+ if (isObjectNodeSchema(schema)) {
484
+ keyMap = schema.storedKeyToPropertyKey;
485
+ }
486
+ // TODO:Performance: Ideally this would use Set.prototype.isDisjointFrom when available.
487
+ for (const flexKey of this.keys) {
488
+ // TODO:Performance: doing everything at the flex tree layer could avoid this translation
489
+ const key = keyMap?.get(flexKey) ?? flexKey;
490
+
491
+ if (data.changedProperties.has(key)) {
492
+ this.onInvalidation();
493
+ return;
494
+ }
495
+ }
496
+ }
497
+ };
498
+ this.unsubscribe = TreeBeta.on(node, "nodeChanged", handler);
499
+ }
500
+
501
+ /**
502
+ * Create an {@link Observer} which subscribes to what was observed in {@link NodeSubscription}s.
503
+ */
504
+ public static createObserver(
505
+ invalidate: () => void,
506
+ onlyOnce = false,
507
+ ): { observer: Observer; unsubscribe: () => void } {
508
+ const subscriptions = new Map<FlexTreeNode, NodeSubscription>();
509
+ const observer: Observer = {
510
+ observeNodeFields(flexNode: FlexTreeNode): void {
511
+ if (flexNode.value !== undefined) {
512
+ // Leaf value, nothing to observe.
513
+ return;
514
+ }
515
+ const subscription = subscriptions.get(flexNode);
516
+ if (subscription !== undefined) {
517
+ // Already subscribed to this node.
518
+ subscription.keys = undefined; // Now subscribed to all keys.
519
+ } else {
520
+ const newSubscription = new NodeSubscription(invalidate, flexNode);
521
+ subscriptions.set(flexNode, newSubscription);
522
+ }
523
+ },
524
+ observeNodeField(flexNode: FlexTreeNode, key: FieldKey): void {
525
+ if (flexNode.value !== undefined) {
526
+ // Leaf value, nothing to observe.
527
+ return;
528
+ }
529
+ const subscription = subscriptions.get(flexNode);
530
+ if (subscription !== undefined) {
531
+ // Already subscribed to this node: if not subscribed to all keys, subscribe to this one.
532
+ // TODO:Performance: due to how JavaScript set ordering works,
533
+ // it might be faster to check `has` and only add if not present in case the same field is viewed many times.
534
+ subscription.keys?.add(key);
535
+ } else {
536
+ const newSubscription = new NodeSubscription(invalidate, flexNode);
537
+ newSubscription.keys = new Set([key]);
538
+ subscriptions.set(flexNode, newSubscription);
539
+ }
540
+ },
541
+ observeParentOf(node: FlexTreeNode): void {
542
+ // Supporting parent tracking is more difficult that it might seem at first.
543
+ // There are two main complicating factors:
544
+ // 1. The parent may be undefined (the node is a root).
545
+ // 2. If tracking this by subscribing to the parent's changes, then which events are subscribed to needs to be updated after the parent changes.
546
+ //
547
+ // If not supporting the first case (undefined parents), the second case gets problematic since it would result in edits which take a node who's parent was observed,
548
+ // and un-parent it, could then throw a usage error.
549
+
550
+ if (!onlyOnce) {
551
+ // TODO: better APIS should be provided which make handling this case practical.
552
+ throw new UsageError("Observation tracking for parents is currently not supported.");
553
+ }
554
+
555
+ const parent = withObservation(undefined, () => node.parentField.parent);
556
+
557
+ if (parent.parent === undefined) {
558
+ // TODO: better APIS should be provided which make handling this case practical.
559
+ throw new UsageError(
560
+ "Observation tracking for parents is currently not supported when parent is undefined.",
561
+ );
562
+ }
563
+ observer.observeNodeField(parent.parent, parent.key);
564
+ },
565
+ };
566
+
567
+ let subscribed = true;
568
+
569
+ return {
570
+ observer,
571
+ unsubscribe: () => {
572
+ if (!subscribed) {
573
+ throw new UsageError("Already unsubscribed");
574
+ }
575
+ subscribed = false;
576
+ for (const subscription of subscriptions.values()) {
577
+ subscription.unsubscribe();
578
+ }
579
+ },
580
+ };
581
+ }
582
+ }
583
+
584
+ /**
585
+ * Handles both {@link (TreeAlpha:interface).trackObservations} and {@link (TreeAlpha:interface).trackObservationsOnce}.
586
+ */
587
+ function trackObservations<TResult>(
588
+ onInvalidation: () => void,
589
+ trackDuring: () => TResult,
590
+ onlyOnce = false,
591
+ ): { result: TResult; unsubscribe: () => void } {
592
+ let observing = true;
593
+
594
+ const invalidate = (): void => {
595
+ if (observing) {
596
+ throw new UsageError("Cannot invalidate while tracking observations");
597
+ }
598
+ onInvalidation();
599
+ };
600
+
601
+ const { observer, unsubscribe } = NodeSubscription.createObserver(invalidate, onlyOnce);
602
+ const result = withObservation(observer, trackDuring);
603
+ observing = false;
604
+
605
+ return {
606
+ result,
607
+ unsubscribe,
608
+ };
422
609
  }
423
610
 
424
611
  /**
@@ -427,6 +614,30 @@ export interface TreeAlpha {
427
614
  * @alpha
428
615
  */
429
616
  export const TreeAlpha: TreeAlpha = {
617
+ trackObservations<TResult>(
618
+ onInvalidation: () => void,
619
+ trackDuring: () => TResult,
620
+ ): { result: TResult; unsubscribe: () => void } {
621
+ return trackObservations(onInvalidation, trackDuring);
622
+ },
623
+
624
+ trackObservationsOnce<TResult>(
625
+ onInvalidation: () => void,
626
+ trackDuring: () => TResult,
627
+ ): { result: TResult; unsubscribe: () => void } {
628
+ const result = trackObservations(
629
+ () => {
630
+ // trackObservations ensures no invalidation occurs while its running,
631
+ // so this callback can only run after trackObservations has returns and thus result is defined.
632
+ result.unsubscribe();
633
+ onInvalidation();
634
+ },
635
+ trackDuring,
636
+ true,
637
+ );
638
+ return result;
639
+ },
640
+
430
641
  branch(node: TreeNode): TreeBranch | undefined {
431
642
  const kernel = getKernel(node);
432
643
  if (!kernel.isHydrated()) {
@@ -50,6 +50,7 @@ import {
50
50
  type HydratedFlexTreeNode,
51
51
  cursorForMapTreeField,
52
52
  type MinimalFieldMap,
53
+ currentObserver,
53
54
  } from "../../feature-libraries/index.js";
54
55
  import { brand, filterIterable, getOrCreate, mapIterable } from "../../util/index.js";
55
56
 
@@ -144,8 +145,10 @@ export class UnhydratedFlexTreeNode
144
145
  */
145
146
  public readonly fields: MinimalFieldMap<UnhydratedFlexTreeField> = {
146
147
  get: (key: FieldKey): UnhydratedFlexTreeField | undefined => this.tryGetField(key),
147
- [Symbol.iterator]: (): IterableIterator<[FieldKey, UnhydratedFlexTreeField]> =>
148
- filterIterable(this.fieldsAll, ([, field]) => field.length > 0),
148
+ [Symbol.iterator]: (): IterableIterator<[FieldKey, UnhydratedFlexTreeField]> => {
149
+ currentObserver?.observeNodeFields(this);
150
+ return filterIterable(this.fieldsAll, ([, field]) => field.length > 0);
151
+ },
149
152
  };
150
153
 
151
154
  public [Symbol.iterator](): IterableIterator<UnhydratedFlexTreeField> {
@@ -218,6 +221,7 @@ export class UnhydratedFlexTreeNode
218
221
  * @remarks If this node is unparented, this method will return the special {@link unparentedLocation} as the parent.
219
222
  */
220
223
  public get parentField(): LocationInField {
224
+ currentObserver?.observeParentOf(this);
221
225
  return this.location;
222
226
  }
223
227
 
@@ -226,6 +230,8 @@ export class UnhydratedFlexTreeNode
226
230
  }
227
231
 
228
232
  public tryGetField(key: FieldKey): UnhydratedFlexTreeField | undefined {
233
+ currentObserver?.observeNodeField(this, key);
234
+
229
235
  const field = this.fieldsAll.get(key);
230
236
  // Only return the field if it is not empty, in order to fulfill the contract of `tryGetField`.
231
237
  if (field !== undefined && field.length > 0) {
@@ -235,6 +241,9 @@ export class UnhydratedFlexTreeNode
235
241
 
236
242
  public getBoxed(key: string): UnhydratedFlexTreeField {
237
243
  const fieldKey: FieldKey = brand(key);
244
+
245
+ currentObserver?.observeNodeField(this, fieldKey);
246
+
238
247
  return this.getOrCreateField(fieldKey);
239
248
  }
240
249