@fluidframework/tree 2.61.0-355934 → 2.61.0-355990

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 (59) hide show
  1. package/api-report/tree.alpha.api.md +8 -0
  2. package/dist/feature-libraries/flex-tree/index.d.ts +1 -0
  3. package/dist/feature-libraries/flex-tree/index.d.ts.map +1 -1
  4. package/dist/feature-libraries/flex-tree/index.js +4 -1
  5. package/dist/feature-libraries/flex-tree/index.js.map +1 -1
  6. package/dist/feature-libraries/flex-tree/lazyNode.d.ts.map +1 -1
  7. package/dist/feature-libraries/flex-tree/lazyNode.js +15 -8
  8. package/dist/feature-libraries/flex-tree/lazyNode.js.map +1 -1
  9. package/dist/feature-libraries/flex-tree/observer.d.ts +19 -0
  10. package/dist/feature-libraries/flex-tree/observer.d.ts.map +1 -0
  11. package/dist/feature-libraries/flex-tree/observer.js +30 -0
  12. package/dist/feature-libraries/flex-tree/observer.js.map +1 -0
  13. package/dist/feature-libraries/index.d.ts +1 -1
  14. package/dist/feature-libraries/index.d.ts.map +1 -1
  15. package/dist/feature-libraries/index.js +3 -1
  16. package/dist/feature-libraries/index.js.map +1 -1
  17. package/dist/packageVersion.d.ts +1 -1
  18. package/dist/packageVersion.js +1 -1
  19. package/dist/packageVersion.js.map +1 -1
  20. package/dist/shared-tree/treeAlpha.d.ts +28 -1
  21. package/dist/shared-tree/treeAlpha.d.ts.map +1 -1
  22. package/dist/shared-tree/treeAlpha.js +119 -0
  23. package/dist/shared-tree/treeAlpha.js.map +1 -1
  24. package/dist/simple-tree/core/unhydratedFlexTree.d.ts.map +1 -1
  25. package/dist/simple-tree/core/unhydratedFlexTree.js +7 -1
  26. package/dist/simple-tree/core/unhydratedFlexTree.js.map +1 -1
  27. package/lib/feature-libraries/flex-tree/index.d.ts +1 -0
  28. package/lib/feature-libraries/flex-tree/index.d.ts.map +1 -1
  29. package/lib/feature-libraries/flex-tree/index.js +1 -0
  30. package/lib/feature-libraries/flex-tree/index.js.map +1 -1
  31. package/lib/feature-libraries/flex-tree/lazyNode.d.ts.map +1 -1
  32. package/lib/feature-libraries/flex-tree/lazyNode.js +15 -8
  33. package/lib/feature-libraries/flex-tree/lazyNode.js.map +1 -1
  34. package/lib/feature-libraries/flex-tree/observer.d.ts +19 -0
  35. package/lib/feature-libraries/flex-tree/observer.d.ts.map +1 -0
  36. package/lib/feature-libraries/flex-tree/observer.js +32 -0
  37. package/lib/feature-libraries/flex-tree/observer.js.map +1 -0
  38. package/lib/feature-libraries/index.d.ts +1 -1
  39. package/lib/feature-libraries/index.d.ts.map +1 -1
  40. package/lib/feature-libraries/index.js +1 -1
  41. package/lib/feature-libraries/index.js.map +1 -1
  42. package/lib/packageVersion.d.ts +1 -1
  43. package/lib/packageVersion.js +1 -1
  44. package/lib/packageVersion.js.map +1 -1
  45. package/lib/shared-tree/treeAlpha.d.ts +28 -1
  46. package/lib/shared-tree/treeAlpha.d.ts.map +1 -1
  47. package/lib/shared-tree/treeAlpha.js +121 -2
  48. package/lib/shared-tree/treeAlpha.js.map +1 -1
  49. package/lib/simple-tree/core/unhydratedFlexTree.d.ts.map +1 -1
  50. package/lib/simple-tree/core/unhydratedFlexTree.js +8 -2
  51. package/lib/simple-tree/core/unhydratedFlexTree.js.map +1 -1
  52. package/package.json +22 -22
  53. package/src/feature-libraries/flex-tree/index.ts +2 -0
  54. package/src/feature-libraries/flex-tree/lazyNode.ts +13 -3
  55. package/src/feature-libraries/flex-tree/observer.ts +41 -0
  56. package/src/feature-libraries/index.ts +3 -0
  57. package/src/packageVersion.ts +1 -1
  58. package/src/shared-tree/treeAlpha.ts +188 -2
  59. package/src/simple-tree/core/unhydratedFlexTree.ts +11 -2
@@ -0,0 +1,41 @@
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
+ export interface Observer {
11
+ observeNodeFields(node: FlexTreeNode): void;
12
+ observeNodeField(node: FlexTreeNode, key: FieldKey): void;
13
+ observeParentOf(node: FlexTreeNode): void;
14
+ }
15
+ /**
16
+ * The current observer, if any.
17
+ * @remarks
18
+ * Set via {@link setObserver} as used by {@link withObservation}.
19
+ */
20
+
21
+ export let currentObserver: Observer | undefined;
22
+ const observerStack: (Observer | undefined)[] = [];
23
+ function setObserver(newObserver: Observer | undefined): void {
24
+ observerStack.push(newObserver);
25
+ currentObserver = newObserver;
26
+ }
27
+ function clearObserver(): void {
28
+ debugAssert(() => observerStack.length > 0 || "Empty Observer stack on clear");
29
+ const popped = observerStack.pop();
30
+ debugAssert(() => popped === currentObserver || "Mismatched observer stack");
31
+ currentObserver = observerStack[observerStack.length - 1];
32
+ }
33
+
34
+ export function withObservation<T>(newObserver: Observer | undefined, f: () => T): T {
35
+ setObserver(newObserver);
36
+ try {
37
+ return f();
38
+ } finally {
39
+ clearObserver();
40
+ }
41
+ }
@@ -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-355990";
@@ -17,13 +17,13 @@ import type { IIdCompressor } from "@fluidframework/id-compressor";
17
17
  import {
18
18
  asIndex,
19
19
  getKernel,
20
- type TreeNode,
21
20
  type Unhydrated,
22
21
  TreeBeta,
23
22
  tryGetSchema,
24
23
  createFromCursor,
25
24
  FieldKind,
26
25
  normalizeFieldSchema,
26
+ TreeNode,
27
27
  type ImplicitFieldSchema,
28
28
  type InsertableField,
29
29
  type TreeFieldFromImplicitField,
@@ -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,166 @@ 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
+ class NodeSubscription {
458
+ /**
459
+ * If undefined, subscribes to all keys.
460
+ * Otherwise only subscribes to the keys in the set.
461
+ */
462
+ public keys: Set<FieldKey> | undefined;
463
+ public readonly unsubscribe: () => void;
464
+ public constructor(
465
+ public readonly onInvalidation: () => void,
466
+ flexNode: FlexTreeNode,
467
+ ) {
468
+ // TODO:Performance: It is possible to optimize this to not use the public TreeNode API.
469
+ const node = getOrCreateNodeFromInnerNode(flexNode);
470
+ assert(node instanceof TreeNode, "Unexpected leaf value");
471
+
472
+ const handler = (data: NodeChangedData): void => {
473
+ if (this.keys === undefined || data.changedProperties === undefined) {
474
+ this.onInvalidation();
475
+ } else {
476
+ let keyMap: ReadonlyMap<FieldKey, string> | undefined;
477
+ const schema = treeNodeApi.schema(node);
478
+ if (isObjectNodeSchema(schema)) {
479
+ keyMap = schema.storedKeyToPropertyKey;
480
+ }
481
+ // TODO:Performance: Ideally this would use Set.prototype.isDisjointFrom when available.
482
+ for (const flexKey of this.keys) {
483
+ // TODO:Performance: doing everything at the flex tree layer could avoid this translation
484
+ const key = keyMap?.get(flexKey) ?? flexKey;
485
+
486
+ if (data.changedProperties.has(key)) {
487
+ this.onInvalidation();
488
+ return;
489
+ }
490
+ }
491
+ }
492
+ };
493
+ this.unsubscribe = TreeBeta.on(node, "nodeChanged", handler);
494
+ }
495
+ }
496
+
497
+ function trackObservations<TResult>(
498
+ onInvalidation: () => void,
499
+ trackDuring: () => TResult,
500
+ onlyOnce = false,
501
+ ): { result: TResult; unsubscribe: () => void } {
502
+ let observing = true;
503
+
504
+ const invalidate = (): void => {
505
+ if (observing) {
506
+ throw new UsageError("Cannot invalidate while tracking observations");
507
+ }
508
+ onInvalidation();
509
+ };
510
+
511
+ const subscriptions = new Map<FlexTreeNode, NodeSubscription>();
512
+ const observer: Observer = {
513
+ observeNodeFields(flexNode: FlexTreeNode): void {
514
+ if (flexNode.value !== undefined) {
515
+ // Leaf value, nothing to observe.
516
+ return;
517
+ }
518
+ const subscription = subscriptions.get(flexNode);
519
+ if (subscription !== undefined) {
520
+ // Already subscribed to this node.
521
+ subscription.keys = undefined; // Now subscribed to all keys.
522
+ } else {
523
+ const newSubscription = new NodeSubscription(invalidate, flexNode);
524
+ subscriptions.set(flexNode, newSubscription);
525
+ }
526
+ },
527
+ observeNodeField(flexNode: FlexTreeNode, key: FieldKey): void {
528
+ if (flexNode.value !== undefined) {
529
+ // Leaf value, nothing to observe.
530
+ return;
531
+ }
532
+ const subscription = subscriptions.get(flexNode);
533
+ if (subscription !== undefined) {
534
+ // Already subscribed to this node: if not subscribed to all keys, subscribe to this one.
535
+ // TODO:Performance: due to how JavaScript set ordering works,
536
+ // it might be faster to check `has` and only add if not present in case the same field is viewed many times.
537
+ subscription.keys?.add(key);
538
+ } else {
539
+ const newSubscription = new NodeSubscription(invalidate, flexNode);
540
+ newSubscription.keys = new Set([key]);
541
+ subscriptions.set(flexNode, newSubscription);
542
+ }
543
+ },
544
+ observeParentOf(node: FlexTreeNode): void {
545
+ // Supporting parent tracking is more difficult that it might seem at first.
546
+ // There are two main complicating factors:
547
+ // 1. The parent may be undefined (the node is a root).
548
+ // 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.
549
+ //
550
+ // 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,
551
+ // and un-parent it, could then throw a usage error.
552
+
553
+ if (!onlyOnce) {
554
+ // TODO: better APIS should be provided which make handling this case practical.
555
+ throw new UsageError("Observation tracking for parents is currently not supported.");
556
+ }
557
+
558
+ const parent = withObservation(undefined, () => node.parentField.parent);
559
+
560
+ if (parent.parent === undefined) {
561
+ // TODO: better APIS should be provided which make handling this case practical.
562
+ throw new UsageError(
563
+ "Observation tracking for parents is currently not supported when parent is undefined.",
564
+ );
565
+ }
566
+ observer.observeNodeField(parent.parent, parent.key);
567
+ },
568
+ };
569
+ const result = withObservation(observer, trackDuring);
570
+ observing = false;
571
+
572
+ let subscribed = true;
573
+
574
+ return {
575
+ result,
576
+ unsubscribe: () => {
577
+ if (!subscribed) {
578
+ throw new UsageError("Already unsubscribed");
579
+ }
580
+ subscribed = false;
581
+ for (const subscription of subscriptions.values()) {
582
+ subscription.unsubscribe();
583
+ }
584
+ },
585
+ };
422
586
  }
423
587
 
424
588
  /**
@@ -427,6 +591,28 @@ export interface TreeAlpha {
427
591
  * @alpha
428
592
  */
429
593
  export const TreeAlpha: TreeAlpha = {
594
+ trackObservations<TResult>(
595
+ onInvalidation: () => void,
596
+ trackDuring: () => TResult,
597
+ ): { result: TResult; unsubscribe: () => void } {
598
+ return trackObservations(onInvalidation, trackDuring);
599
+ },
600
+
601
+ trackObservationsOnce<TResult>(
602
+ onInvalidation: () => void,
603
+ trackDuring: () => TResult,
604
+ ): { result: TResult; unsubscribe: () => void } {
605
+ const result = trackObservations(
606
+ () => {
607
+ result.unsubscribe();
608
+ onInvalidation();
609
+ },
610
+ trackDuring,
611
+ true,
612
+ );
613
+ return result;
614
+ },
615
+
430
616
  branch(node: TreeNode): TreeBranch | undefined {
431
617
  const kernel = getKernel(node);
432
618
  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