@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.
- package/api-report/tree.alpha.api.md +8 -0
- package/dist/feature-libraries/flex-tree/index.d.ts +1 -0
- package/dist/feature-libraries/flex-tree/index.d.ts.map +1 -1
- package/dist/feature-libraries/flex-tree/index.js +4 -1
- package/dist/feature-libraries/flex-tree/index.js.map +1 -1
- package/dist/feature-libraries/flex-tree/lazyNode.d.ts.map +1 -1
- package/dist/feature-libraries/flex-tree/lazyNode.js +15 -8
- package/dist/feature-libraries/flex-tree/lazyNode.js.map +1 -1
- package/dist/feature-libraries/flex-tree/observer.d.ts +19 -0
- package/dist/feature-libraries/flex-tree/observer.d.ts.map +1 -0
- package/dist/feature-libraries/flex-tree/observer.js +30 -0
- package/dist/feature-libraries/flex-tree/observer.js.map +1 -0
- package/dist/feature-libraries/index.d.ts +1 -1
- package/dist/feature-libraries/index.d.ts.map +1 -1
- package/dist/feature-libraries/index.js +3 -1
- package/dist/feature-libraries/index.js.map +1 -1
- package/dist/packageVersion.d.ts +1 -1
- package/dist/packageVersion.js +1 -1
- package/dist/packageVersion.js.map +1 -1
- package/dist/shared-tree/treeAlpha.d.ts +28 -1
- package/dist/shared-tree/treeAlpha.d.ts.map +1 -1
- package/dist/shared-tree/treeAlpha.js +119 -0
- package/dist/shared-tree/treeAlpha.js.map +1 -1
- package/dist/simple-tree/core/unhydratedFlexTree.d.ts.map +1 -1
- package/dist/simple-tree/core/unhydratedFlexTree.js +7 -1
- package/dist/simple-tree/core/unhydratedFlexTree.js.map +1 -1
- package/lib/feature-libraries/flex-tree/index.d.ts +1 -0
- package/lib/feature-libraries/flex-tree/index.d.ts.map +1 -1
- package/lib/feature-libraries/flex-tree/index.js +1 -0
- package/lib/feature-libraries/flex-tree/index.js.map +1 -1
- package/lib/feature-libraries/flex-tree/lazyNode.d.ts.map +1 -1
- package/lib/feature-libraries/flex-tree/lazyNode.js +15 -8
- package/lib/feature-libraries/flex-tree/lazyNode.js.map +1 -1
- package/lib/feature-libraries/flex-tree/observer.d.ts +19 -0
- package/lib/feature-libraries/flex-tree/observer.d.ts.map +1 -0
- package/lib/feature-libraries/flex-tree/observer.js +32 -0
- package/lib/feature-libraries/flex-tree/observer.js.map +1 -0
- package/lib/feature-libraries/index.d.ts +1 -1
- package/lib/feature-libraries/index.d.ts.map +1 -1
- package/lib/feature-libraries/index.js +1 -1
- package/lib/feature-libraries/index.js.map +1 -1
- package/lib/packageVersion.d.ts +1 -1
- package/lib/packageVersion.js +1 -1
- package/lib/packageVersion.js.map +1 -1
- package/lib/shared-tree/treeAlpha.d.ts +28 -1
- package/lib/shared-tree/treeAlpha.d.ts.map +1 -1
- package/lib/shared-tree/treeAlpha.js +121 -2
- package/lib/shared-tree/treeAlpha.js.map +1 -1
- package/lib/simple-tree/core/unhydratedFlexTree.d.ts.map +1 -1
- package/lib/simple-tree/core/unhydratedFlexTree.js +8 -2
- package/lib/simple-tree/core/unhydratedFlexTree.js.map +1 -1
- package/package.json +22 -22
- package/src/feature-libraries/flex-tree/index.ts +2 -0
- package/src/feature-libraries/flex-tree/lazyNode.ts +13 -3
- package/src/feature-libraries/flex-tree/observer.ts +41 -0
- package/src/feature-libraries/index.ts +3 -0
- package/src/packageVersion.ts +1 -1
- package/src/shared-tree/treeAlpha.ts +188 -2
- 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
|
+
}
|
package/src/packageVersion.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|