@fluidframework/tree 2.60.0 → 2.61.0-355516
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/.mocharc.cjs +2 -3
- package/api-report/tree.alpha.api.md +11 -28
- package/api-report/tree.beta.api.md +6 -23
- package/api-report/tree.legacy.beta.api.md +71 -22
- package/api-report/tree.legacy.public.api.md +5 -22
- package/api-report/tree.public.api.md +5 -22
- package/dist/alpha.d.ts +10 -5
- package/dist/beta.d.ts +8 -4
- package/dist/core/tree/anchorSet.d.ts +3 -3
- package/dist/core/tree/anchorSet.d.ts.map +1 -1
- package/dist/core/tree/anchorSet.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/legacy.d.ts +22 -4
- package/dist/packageVersion.d.ts +1 -1
- package/dist/packageVersion.d.ts.map +1 -1
- package/dist/packageVersion.js +1 -1
- package/dist/packageVersion.js.map +1 -1
- package/dist/public.d.ts +6 -3
- package/dist/shared-tree/treeCheckout.d.ts.map +1 -1
- package/dist/shared-tree/treeCheckout.js +1 -0
- package/dist/shared-tree/treeCheckout.js.map +1 -1
- package/dist/simple-tree/api/index.d.ts +1 -1
- package/dist/simple-tree/api/index.d.ts.map +1 -1
- package/dist/simple-tree/api/index.js.map +1 -1
- package/dist/simple-tree/api/schemaFactory.d.ts +11 -83
- package/dist/simple-tree/api/schemaFactory.d.ts.map +1 -1
- package/dist/simple-tree/api/schemaFactory.js +26 -82
- package/dist/simple-tree/api/schemaFactory.js.map +1 -1
- package/dist/simple-tree/api/schemaFactoryRecursive.d.ts +5 -3
- package/dist/simple-tree/api/schemaFactoryRecursive.d.ts.map +1 -1
- package/dist/simple-tree/api/schemaFactoryRecursive.js.map +1 -1
- package/dist/simple-tree/core/index.d.ts +1 -1
- package/dist/simple-tree/core/index.d.ts.map +1 -1
- package/dist/simple-tree/core/index.js +2 -1
- package/dist/simple-tree/core/index.js.map +1 -1
- package/dist/simple-tree/core/treeNodeKernel.d.ts +12 -1
- package/dist/simple-tree/core/treeNodeKernel.d.ts.map +1 -1
- package/dist/simple-tree/core/treeNodeKernel.js +188 -43
- package/dist/simple-tree/core/treeNodeKernel.js.map +1 -1
- package/dist/simple-tree/core/unhydratedFlexTree.d.ts +4 -3
- package/dist/simple-tree/core/unhydratedFlexTree.d.ts.map +1 -1
- package/dist/simple-tree/core/unhydratedFlexTree.js +22 -6
- package/dist/simple-tree/core/unhydratedFlexTree.js.map +1 -1
- package/dist/simple-tree/index.d.ts +2 -2
- package/dist/simple-tree/index.d.ts.map +1 -1
- package/dist/simple-tree/index.js +3 -2
- package/dist/simple-tree/index.js.map +1 -1
- package/dist/simple-tree/node-kinds/array/arrayNode.d.ts.map +1 -1
- package/dist/simple-tree/node-kinds/array/arrayNode.js +13 -6
- package/dist/simple-tree/node-kinds/array/arrayNode.js.map +1 -1
- package/dist/tableSchema.d.ts.map +1 -1
- package/dist/tableSchema.js +15 -10
- package/dist/tableSchema.js.map +1 -1
- package/dist/util/breakable.d.ts.map +1 -1
- package/dist/util/breakable.js +7 -1
- package/dist/util/breakable.js.map +1 -1
- package/lib/alpha.d.ts +10 -5
- package/lib/beta.d.ts +8 -4
- package/lib/core/tree/anchorSet.d.ts +3 -3
- package/lib/core/tree/anchorSet.d.ts.map +1 -1
- package/lib/core/tree/anchorSet.js.map +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js.map +1 -1
- package/lib/legacy.d.ts +22 -4
- package/lib/packageVersion.d.ts +1 -1
- package/lib/packageVersion.d.ts.map +1 -1
- package/lib/packageVersion.js +1 -1
- package/lib/packageVersion.js.map +1 -1
- package/lib/public.d.ts +6 -3
- package/lib/shared-tree/treeCheckout.d.ts.map +1 -1
- package/lib/shared-tree/treeCheckout.js +1 -0
- package/lib/shared-tree/treeCheckout.js.map +1 -1
- package/lib/simple-tree/api/index.d.ts +1 -1
- package/lib/simple-tree/api/index.d.ts.map +1 -1
- package/lib/simple-tree/api/index.js.map +1 -1
- package/lib/simple-tree/api/schemaFactory.d.ts +11 -83
- package/lib/simple-tree/api/schemaFactory.d.ts.map +1 -1
- package/lib/simple-tree/api/schemaFactory.js +25 -81
- package/lib/simple-tree/api/schemaFactory.js.map +1 -1
- package/lib/simple-tree/api/schemaFactoryRecursive.d.ts +5 -3
- package/lib/simple-tree/api/schemaFactoryRecursive.d.ts.map +1 -1
- package/lib/simple-tree/api/schemaFactoryRecursive.js.map +1 -1
- package/lib/simple-tree/core/index.d.ts +1 -1
- package/lib/simple-tree/core/index.d.ts.map +1 -1
- package/lib/simple-tree/core/index.js +1 -1
- package/lib/simple-tree/core/index.js.map +1 -1
- package/lib/simple-tree/core/treeNodeKernel.d.ts +12 -1
- package/lib/simple-tree/core/treeNodeKernel.d.ts.map +1 -1
- package/lib/simple-tree/core/treeNodeKernel.js +187 -43
- package/lib/simple-tree/core/treeNodeKernel.js.map +1 -1
- package/lib/simple-tree/core/unhydratedFlexTree.d.ts +4 -3
- package/lib/simple-tree/core/unhydratedFlexTree.d.ts.map +1 -1
- package/lib/simple-tree/core/unhydratedFlexTree.js +22 -6
- package/lib/simple-tree/core/unhydratedFlexTree.js.map +1 -1
- package/lib/simple-tree/index.d.ts +2 -2
- package/lib/simple-tree/index.d.ts.map +1 -1
- package/lib/simple-tree/index.js +1 -1
- package/lib/simple-tree/index.js.map +1 -1
- package/lib/simple-tree/node-kinds/array/arrayNode.d.ts.map +1 -1
- package/lib/simple-tree/node-kinds/array/arrayNode.js +14 -7
- package/lib/simple-tree/node-kinds/array/arrayNode.js.map +1 -1
- package/lib/tableSchema.d.ts.map +1 -1
- package/lib/tableSchema.js +16 -11
- package/lib/tableSchema.js.map +1 -1
- package/lib/tsdoc-metadata.json +1 -1
- package/lib/util/breakable.d.ts.map +1 -1
- package/lib/util/breakable.js +7 -1
- package/lib/util/breakable.js.map +1 -1
- package/package.json +27 -27
- package/src/core/tree/anchorSet.ts +2 -2
- package/src/index.ts +1 -0
- package/src/packageVersion.ts +1 -1
- package/src/shared-tree/treeCheckout.ts +1 -0
- package/src/simple-tree/api/index.ts +1 -0
- package/src/simple-tree/api/schemaFactory.ts +31 -103
- package/src/simple-tree/api/schemaFactoryRecursive.ts +41 -40
- package/src/simple-tree/core/index.ts +1 -0
- package/src/simple-tree/core/treeNodeKernel.ts +242 -44
- package/src/simple-tree/core/unhydratedFlexTree.ts +26 -3
- package/src/simple-tree/index.ts +2 -0
- package/src/simple-tree/node-kinds/array/arrayNode.ts +19 -11
- package/src/tableSchema.ts +15 -9
- package/src/util/breakable.ts +9 -1
|
@@ -4,8 +4,13 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { createEmitter } from "@fluid-internal/client-utils";
|
|
7
|
-
import type { Listenable, Off } from "@fluidframework/core-interfaces";
|
|
8
|
-
import {
|
|
7
|
+
import type { HasListeners, Listenable, Off } from "@fluidframework/core-interfaces/internal";
|
|
8
|
+
import {
|
|
9
|
+
assert,
|
|
10
|
+
fail,
|
|
11
|
+
debugAssert,
|
|
12
|
+
unreachableCase,
|
|
13
|
+
} from "@fluidframework/core-utils/internal";
|
|
9
14
|
import { UsageError } from "@fluidframework/telemetry-utils/internal";
|
|
10
15
|
|
|
11
16
|
import {
|
|
@@ -13,6 +18,7 @@ import {
|
|
|
13
18
|
type AnchorEvents,
|
|
14
19
|
type AnchorNode,
|
|
15
20
|
type AnchorSet,
|
|
21
|
+
type FieldKey,
|
|
16
22
|
type TreeValue,
|
|
17
23
|
type UpPath,
|
|
18
24
|
} from "../../core/index.js";
|
|
@@ -74,7 +80,6 @@ export function tryGetTreeNodeSchema(value: unknown): undefined | TreeNodeSchema
|
|
|
74
80
|
|
|
75
81
|
/** The {@link HydrationState} of a {@link TreeNodeKernel} before the kernel is hydrated */
|
|
76
82
|
interface UnhydratedState {
|
|
77
|
-
off: Off;
|
|
78
83
|
readonly innerNode: UnhydratedFlexTreeNode;
|
|
79
84
|
}
|
|
80
85
|
|
|
@@ -126,7 +131,7 @@ export class TreeNodeKernel {
|
|
|
126
131
|
* This means optimizations like skipping processing data in subtrees where no subtreeChanged events are subscribed to would be able to work,
|
|
127
132
|
* since the kernel does not unconditionally subscribe to those events (like a design which simply forwards all events would).
|
|
128
133
|
*/
|
|
129
|
-
readonly #
|
|
134
|
+
readonly #eventBuffer: KernelEventBuffer;
|
|
130
135
|
|
|
131
136
|
/**
|
|
132
137
|
* Create a TreeNodeKernel which can be looked up with {@link getKernel}.
|
|
@@ -149,38 +154,21 @@ export class TreeNodeKernel {
|
|
|
149
154
|
|
|
150
155
|
if (innerNode instanceof UnhydratedFlexTreeNode) {
|
|
151
156
|
// Unhydrated case
|
|
157
|
+
|
|
152
158
|
debugAssert(() => innerNode.treeNode === undefined);
|
|
153
159
|
innerNode.treeNode = node;
|
|
154
|
-
|
|
155
|
-
// These will be fired if the unhydrated node is edited, and will also be forwarded later to the hydrated node.
|
|
160
|
+
|
|
156
161
|
this.#hydrationState = {
|
|
157
162
|
innerNode,
|
|
158
|
-
off: innerNode.events.on("childrenChangedAfterBatch", ({ changedFields }) => {
|
|
159
|
-
this.#unhydratedEvents.value.emit("childrenChangedAfterBatch", {
|
|
160
|
-
changedFields,
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
let unhydratedNode: UnhydratedFlexTreeNode | undefined = innerNode;
|
|
164
|
-
while (unhydratedNode !== undefined) {
|
|
165
|
-
const treeNode = unhydratedNode.treeNode;
|
|
166
|
-
if (treeNode !== undefined) {
|
|
167
|
-
const kernel = getKernel(treeNode);
|
|
168
|
-
kernel.#unhydratedEvents.value.emit("subtreeChangedAfterBatch");
|
|
169
|
-
}
|
|
170
|
-
const parentNode: FlexTreeNode | undefined =
|
|
171
|
-
unhydratedNode.parentField.parent.parent;
|
|
172
|
-
assert(
|
|
173
|
-
parentNode === undefined || parentNode instanceof UnhydratedFlexTreeNode,
|
|
174
|
-
0xb76 /* Unhydrated node's parent should be an unhydrated node */,
|
|
175
|
-
);
|
|
176
|
-
unhydratedNode = parentNode;
|
|
177
|
-
}
|
|
178
|
-
}),
|
|
179
163
|
};
|
|
164
|
+
|
|
165
|
+
this.#eventBuffer = new KernelEventBuffer(innerNode.events);
|
|
180
166
|
} else {
|
|
181
167
|
// Hydrated case
|
|
182
168
|
this.#hydrationState = this.createHydratedState(innerNode.anchorNode);
|
|
183
169
|
this.#hydrationState.innerNode = innerNode;
|
|
170
|
+
|
|
171
|
+
this.#eventBuffer = new KernelEventBuffer(innerNode.anchorNode.events);
|
|
184
172
|
}
|
|
185
173
|
}
|
|
186
174
|
|
|
@@ -213,19 +201,8 @@ export class TreeNodeKernel {
|
|
|
213
201
|
this.#hydrationState = this.createHydratedState(anchorNode);
|
|
214
202
|
this.#hydrationState.offAnchorNode.add(() => anchors.forget(anchor));
|
|
215
203
|
|
|
216
|
-
//
|
|
217
|
-
|
|
218
|
-
const events = this.#unhydratedEvents.value;
|
|
219
|
-
for (const eventName of kernelEvents) {
|
|
220
|
-
if (events.hasListeners(eventName)) {
|
|
221
|
-
this.#hydrationState.offAnchorNode.add(
|
|
222
|
-
// Argument is forwarded between matching events, so the type should be correct.
|
|
223
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
224
|
-
anchorNode.events.on(eventName, (arg: any) => events.emit(eventName, arg)),
|
|
225
|
-
);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
}
|
|
204
|
+
// Lazily migrate existing event listeners to the anchor node
|
|
205
|
+
this.#eventBuffer.migrateEventSource(anchorNode.events);
|
|
229
206
|
}
|
|
230
207
|
|
|
231
208
|
private createHydratedState(anchorNode: AnchorNode): HydratedState {
|
|
@@ -267,10 +244,7 @@ export class TreeNodeKernel {
|
|
|
267
244
|
}
|
|
268
245
|
|
|
269
246
|
public get events(): Listenable<KernelEvents> {
|
|
270
|
-
|
|
271
|
-
return isHydrated(this.#hydrationState)
|
|
272
|
-
? this.#hydrationState.anchorNode.events
|
|
273
|
-
: this.#unhydratedEvents.value;
|
|
247
|
+
return this.#eventBuffer;
|
|
274
248
|
}
|
|
275
249
|
|
|
276
250
|
public dispose(): void {
|
|
@@ -281,6 +255,7 @@ export class TreeNodeKernel {
|
|
|
281
255
|
off();
|
|
282
256
|
}
|
|
283
257
|
}
|
|
258
|
+
this.#eventBuffer.dispose();
|
|
284
259
|
// TODO: go to the context and remove myself from withAnchors
|
|
285
260
|
}
|
|
286
261
|
|
|
@@ -354,6 +329,229 @@ const kernelEvents = ["childrenChangedAfterBatch", "subtreeChangedAfterBatch"] a
|
|
|
354
329
|
|
|
355
330
|
type KernelEvents = Pick<AnchorEvents, (typeof kernelEvents)[number]>;
|
|
356
331
|
|
|
332
|
+
// #region TreeNodeEventBuffer
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Whether or not events from {@link TreeNodeKernel} should be buffered instead of emitted immediately.
|
|
336
|
+
*/
|
|
337
|
+
let bufferTreeEvents: boolean = false;
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Call the provided callback with {@link TreeNode}s' events paused until after the callback's completion.
|
|
341
|
+
*
|
|
342
|
+
* Events that would otherwise have been emitted immediately are merged and buffered until after the
|
|
343
|
+
* provided callback has been completed.
|
|
344
|
+
*
|
|
345
|
+
* @remarks
|
|
346
|
+
* Note: this should be used with caution. User application behaviors are implicitly coupled to event timing.
|
|
347
|
+
* Disrupting this timing can lead to unexpected behavior.
|
|
348
|
+
*/
|
|
349
|
+
export function withBufferedTreeEvents(callback: () => void): void {
|
|
350
|
+
if (bufferTreeEvents) {
|
|
351
|
+
// Already buffering - just run the callback
|
|
352
|
+
callback();
|
|
353
|
+
} else {
|
|
354
|
+
bufferTreeEvents = true;
|
|
355
|
+
try {
|
|
356
|
+
callback();
|
|
357
|
+
} finally {
|
|
358
|
+
bufferTreeEvents = false;
|
|
359
|
+
flushEventsEmitter.emit("flush");
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Event emitter to notify subscribers when tree events buffered due to {@link withBufferedTreeEvents} should be flushed.
|
|
366
|
+
*/
|
|
367
|
+
const flushEventsEmitter = createEmitter<{
|
|
368
|
+
flush: () => void;
|
|
369
|
+
}>();
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Event emitter for {@link TreeNodeKernel}, which optionally buffers events based on {@link bufferTreeEvents}.
|
|
373
|
+
* @remarks Listens to {@link flushEventsEmitter} to know when to flush any buffered events.
|
|
374
|
+
*/
|
|
375
|
+
class KernelEventBuffer implements Listenable<KernelEvents> {
|
|
376
|
+
#disposed: boolean = false;
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Listen to {@link flushEventsEmitter} to know when to flush buffered events.
|
|
380
|
+
*/
|
|
381
|
+
readonly #disposeOnFlushListener = flushEventsEmitter.on("flush", () => {
|
|
382
|
+
this.flush();
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
readonly #events = createEmitter<KernelEvents>();
|
|
386
|
+
|
|
387
|
+
#eventSource: Listenable<KernelEvents> & HasListeners<KernelEvents>;
|
|
388
|
+
#disposeSourceListeners: Map<keyof KernelEvents, Off> = new Map();
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Buffer of fields that have changed since events were paused.
|
|
392
|
+
* When events are flushed, a single {@link AnchorEvents.childrenChangedAfterBatch} event will be emitted
|
|
393
|
+
* containing the accumulated set of changed fields.
|
|
394
|
+
*/
|
|
395
|
+
readonly #childrenChangedBuffer: Set<FieldKey> = new Set();
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Whether or not the subtree has changed since events were paused.
|
|
399
|
+
* When events are flushed, a single {@link AnchorEvents.subTreeChanged} event will be emitted if and only
|
|
400
|
+
* if the subtree has changed.
|
|
401
|
+
*/
|
|
402
|
+
#subTreeChangedBuffer: boolean = false;
|
|
403
|
+
|
|
404
|
+
public constructor(
|
|
405
|
+
/**
|
|
406
|
+
* Source of the kernel events.
|
|
407
|
+
* Subscriptions will be created on-demand when listeners are added to this.events,
|
|
408
|
+
* and those subscriptions will be cleaned up when all corresponding listeners have been removed.
|
|
409
|
+
*/
|
|
410
|
+
eventSource: Listenable<KernelEvents> & HasListeners<KernelEvents>,
|
|
411
|
+
) {
|
|
412
|
+
this.#eventSource = eventSource;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Migrate this event buffer to a new event source.
|
|
417
|
+
*
|
|
418
|
+
* @remarks
|
|
419
|
+
* Cleans up any existing event subscriptions from the old source.
|
|
420
|
+
* Binds events to the new source for each event with active listeners.
|
|
421
|
+
*/
|
|
422
|
+
public migrateEventSource(
|
|
423
|
+
newSource: Listenable<KernelEvents> & HasListeners<KernelEvents>,
|
|
424
|
+
): void {
|
|
425
|
+
// Unsubscribe from the old source
|
|
426
|
+
this.#disposeSourceListeners.forEach((off) => off());
|
|
427
|
+
this.#disposeSourceListeners.clear();
|
|
428
|
+
|
|
429
|
+
this.#eventSource = newSource;
|
|
430
|
+
|
|
431
|
+
if (this.#events.hasListeners("childrenChangedAfterBatch")) {
|
|
432
|
+
const off = this.#eventSource.on("childrenChangedAfterBatch", ({ changedFields }) =>
|
|
433
|
+
this.#emit("childrenChangedAfterBatch", { changedFields }),
|
|
434
|
+
);
|
|
435
|
+
this.#disposeSourceListeners.set("childrenChangedAfterBatch", off);
|
|
436
|
+
}
|
|
437
|
+
if (this.#events.hasListeners("subtreeChangedAfterBatch")) {
|
|
438
|
+
const off = this.#eventSource.on("subtreeChangedAfterBatch", () =>
|
|
439
|
+
this.#emit("subtreeChangedAfterBatch"),
|
|
440
|
+
);
|
|
441
|
+
this.#disposeSourceListeners.set("subtreeChangedAfterBatch", off);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
public on(eventName: keyof KernelEvents, listener: KernelEvents[typeof eventName]): Off {
|
|
446
|
+
// Lazily bind event listeners to the source.
|
|
447
|
+
// If we do not have any existing listeners for this event, then we need to bind to the source.
|
|
448
|
+
if (!this.#events.hasListeners(eventName)) {
|
|
449
|
+
assert(
|
|
450
|
+
!this.#disposeSourceListeners.has(eventName),
|
|
451
|
+
"Should not have a dispose function without listeners",
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
const off = this.#eventSource.on(eventName, (args) => this.#emit(eventName, args));
|
|
455
|
+
this.#disposeSourceListeners.set(eventName, off);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
this.#events.on(eventName, listener);
|
|
459
|
+
return () => this.off(eventName, listener);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
public off(eventName: keyof KernelEvents, listener: KernelEvents[typeof eventName]): void {
|
|
463
|
+
this.#events.off(eventName, listener);
|
|
464
|
+
|
|
465
|
+
// If there are no remaining listeners for the event, unbind from the source
|
|
466
|
+
if (!this.#events.hasListeners(eventName)) {
|
|
467
|
+
const off = this.#disposeSourceListeners.get(eventName);
|
|
468
|
+
off?.();
|
|
469
|
+
this.#disposeSourceListeners.delete(eventName);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
#emit(
|
|
474
|
+
eventName: keyof KernelEvents,
|
|
475
|
+
arg?: {
|
|
476
|
+
changedFields: ReadonlySet<FieldKey>;
|
|
477
|
+
},
|
|
478
|
+
): void {
|
|
479
|
+
this.#assertNotDisposed();
|
|
480
|
+
switch (eventName) {
|
|
481
|
+
case "childrenChangedAfterBatch":
|
|
482
|
+
assert(arg !== undefined, "childrenChangedAfterBatch should have arg");
|
|
483
|
+
return this.#handleChildrenChangedAfterBatch(arg.changedFields);
|
|
484
|
+
case "subtreeChangedAfterBatch":
|
|
485
|
+
return this.#handleSubtreeChangedAfterBatch();
|
|
486
|
+
default:
|
|
487
|
+
unreachableCase(eventName);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
#handleChildrenChangedAfterBatch(changedFields: ReadonlySet<FieldKey>): void {
|
|
492
|
+
if (bufferTreeEvents) {
|
|
493
|
+
for (const fieldKey of changedFields) {
|
|
494
|
+
this.#childrenChangedBuffer.add(fieldKey);
|
|
495
|
+
}
|
|
496
|
+
} else {
|
|
497
|
+
this.#events.emit("childrenChangedAfterBatch", { changedFields });
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
#handleSubtreeChangedAfterBatch(): void {
|
|
502
|
+
if (bufferTreeEvents) {
|
|
503
|
+
this.#subTreeChangedBuffer = true;
|
|
504
|
+
} else {
|
|
505
|
+
this.#events.emit("subtreeChangedAfterBatch");
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Flushes any events buffered due to {@link withBufferedTreeEvents}.
|
|
511
|
+
*/
|
|
512
|
+
public flush(): void {
|
|
513
|
+
this.#assertNotDisposed();
|
|
514
|
+
|
|
515
|
+
if (this.#childrenChangedBuffer.size > 0) {
|
|
516
|
+
this.#events.emit("childrenChangedAfterBatch", {
|
|
517
|
+
changedFields: this.#childrenChangedBuffer,
|
|
518
|
+
});
|
|
519
|
+
this.#childrenChangedBuffer.clear();
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (this.#subTreeChangedBuffer) {
|
|
523
|
+
this.#events.emit("subtreeChangedAfterBatch");
|
|
524
|
+
this.#subTreeChangedBuffer = false;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
#assertNotDisposed(): void {
|
|
529
|
+
assert(!this.#disposed, "Event handler disposed.");
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
public dispose(): void {
|
|
533
|
+
if (this.#disposed) {
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
assert(
|
|
538
|
+
this.#childrenChangedBuffer.size === 0 && !this.#subTreeChangedBuffer,
|
|
539
|
+
"Buffered kernel events should have been flushed before disposing.",
|
|
540
|
+
);
|
|
541
|
+
|
|
542
|
+
this.#disposeOnFlushListener();
|
|
543
|
+
this.#disposeSourceListeners.forEach((off) => off());
|
|
544
|
+
this.#disposeSourceListeners.clear();
|
|
545
|
+
|
|
546
|
+
this.#childrenChangedBuffer.clear();
|
|
547
|
+
this.#subTreeChangedBuffer = false;
|
|
548
|
+
|
|
549
|
+
this.#disposed = true;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// #endregion
|
|
554
|
+
|
|
357
555
|
/**
|
|
358
556
|
* For "cooked" nodes this is a HydratedFlexTreeNode thats a projection of forest content.
|
|
359
557
|
* For {@link Unhydrated} nodes this is a UnhydratedFlexTreeNode.
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { createEmitter } from "@fluid-internal/client-utils";
|
|
7
|
-
import type { Listenable } from "@fluidframework/core-interfaces";
|
|
7
|
+
import type { HasListeners, Listenable } from "@fluidframework/core-interfaces/internal";
|
|
8
8
|
import { assert, oob, fail } from "@fluidframework/core-utils/internal";
|
|
9
9
|
import { UsageError } from "@fluidframework/telemetry-utils/internal";
|
|
10
10
|
|
|
@@ -60,7 +60,10 @@ import type { TreeNode } from "./treeNode.js";
|
|
|
60
60
|
interface UnhydratedTreeSequenceFieldEditBuilder
|
|
61
61
|
extends SequenceFieldEditBuilder<FlexibleFieldContent, UnhydratedFlexTreeNode[]> {}
|
|
62
62
|
|
|
63
|
-
type UnhydratedFlexTreeNodeEvents = Pick<
|
|
63
|
+
type UnhydratedFlexTreeNodeEvents = Pick<
|
|
64
|
+
AnchorEvents,
|
|
65
|
+
"childrenChangedAfterBatch" | "subtreeChangedAfterBatch"
|
|
66
|
+
>;
|
|
64
67
|
|
|
65
68
|
/** A node's parent field and its index in that field */
|
|
66
69
|
type LocationInField = FlexTreeNode["parentField"];
|
|
@@ -96,7 +99,8 @@ export class UnhydratedFlexTreeNode
|
|
|
96
99
|
public readonly [flexTreeMarker] = FlexTreeEntityKind.Node as const;
|
|
97
100
|
|
|
98
101
|
private readonly _events = createEmitter<UnhydratedFlexTreeNodeEvents>();
|
|
99
|
-
public get events(): Listenable<UnhydratedFlexTreeNodeEvents>
|
|
102
|
+
public get events(): Listenable<UnhydratedFlexTreeNodeEvents> &
|
|
103
|
+
HasListeners<UnhydratedFlexTreeNodeEvents> {
|
|
100
104
|
return this._events;
|
|
101
105
|
}
|
|
102
106
|
|
|
@@ -244,6 +248,25 @@ export class UnhydratedFlexTreeNode
|
|
|
244
248
|
|
|
245
249
|
public emitChangedEvent(key: FieldKey): void {
|
|
246
250
|
this._events.emit("childrenChangedAfterBatch", { changedFields: new Set([key]) });
|
|
251
|
+
|
|
252
|
+
// Also emit subtree changed event for this node and all ancestors.
|
|
253
|
+
this.#emitSubtreeChangedEvents();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Emit subtree changed events for this node and all ancestors.
|
|
258
|
+
*/
|
|
259
|
+
#emitSubtreeChangedEvents(): void {
|
|
260
|
+
this._events.emit("subtreeChangedAfterBatch");
|
|
261
|
+
|
|
262
|
+
const parent = this.parentField.parent.parent;
|
|
263
|
+
assert(
|
|
264
|
+
parent === undefined || parent instanceof UnhydratedFlexTreeNode,
|
|
265
|
+
0xb76 /* Unhydrated node's parent should be an unhydrated node */,
|
|
266
|
+
);
|
|
267
|
+
if (parent !== undefined) {
|
|
268
|
+
parent.#emitSubtreeChangedEvents();
|
|
269
|
+
}
|
|
247
270
|
}
|
|
248
271
|
}
|
|
249
272
|
|
package/src/simple-tree/index.ts
CHANGED
|
@@ -56,6 +56,7 @@ export {
|
|
|
56
56
|
walkAllowedTypes,
|
|
57
57
|
type SchemaVisitor,
|
|
58
58
|
type SimpleNodeSchemaBase,
|
|
59
|
+
withBufferedTreeEvents,
|
|
59
60
|
} from "./core/index.js";
|
|
60
61
|
export { walkFieldSchema } from "./walkFieldSchema.js";
|
|
61
62
|
export type { UnsafeUnknownSchema, Insertable } from "./unsafeUnknownSchema.js";
|
|
@@ -164,6 +165,7 @@ export {
|
|
|
164
165
|
type SchemaStaticsAlpha,
|
|
165
166
|
KeyEncodingOptions,
|
|
166
167
|
type TreeParsingOptions,
|
|
168
|
+
type SchemaFactory_base,
|
|
167
169
|
} from "./api/index.js";
|
|
168
170
|
export type {
|
|
169
171
|
SimpleTreeSchema,
|
|
@@ -49,6 +49,7 @@ import {
|
|
|
49
49
|
type FlexContent,
|
|
50
50
|
type TreeNodeSchemaPrivateData,
|
|
51
51
|
convertAllowedTypes,
|
|
52
|
+
withBufferedTreeEvents,
|
|
52
53
|
} from "../../core/index.js";
|
|
53
54
|
import {
|
|
54
55
|
type FactoryContent,
|
|
@@ -1080,17 +1081,24 @@ abstract class CustomArrayNodeBase<const T extends ImplicitAllowedTypes>
|
|
|
1080
1081
|
);
|
|
1081
1082
|
}
|
|
1082
1083
|
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
destinationField
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1084
|
+
// We implement move here via subsequent `remove` and `insert`.
|
|
1085
|
+
// This is strictly an implementation detail and should not be observable by the user.
|
|
1086
|
+
// TODO:AB#47457: Implement proper move support for unhydrated trees.
|
|
1087
|
+
// As a temporary mitigation, we will pause tree events until both edits have been completed.
|
|
1088
|
+
// That way, users will only see a single change event for the array instead of 2.
|
|
1089
|
+
withBufferedTreeEvents(() => {
|
|
1090
|
+
if (sourceField !== destinationField || destinationGap < sourceStart) {
|
|
1091
|
+
destinationField.editor.insert(
|
|
1092
|
+
destinationGap,
|
|
1093
|
+
sourceField.editor.remove(sourceStart, movedCount),
|
|
1094
|
+
);
|
|
1095
|
+
} else if (destinationGap > sourceStart + movedCount) {
|
|
1096
|
+
destinationField.editor.insert(
|
|
1097
|
+
destinationGap - movedCount,
|
|
1098
|
+
sourceField.editor.remove(sourceStart, movedCount),
|
|
1099
|
+
);
|
|
1100
|
+
}
|
|
1101
|
+
});
|
|
1094
1102
|
} else {
|
|
1095
1103
|
if (!sourceField.context.isHydrated()) {
|
|
1096
1104
|
throw new UsageError(
|
package/src/tableSchema.ts
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
type UnannotateImplicitFieldSchema,
|
|
30
30
|
isArrayNodeSchema,
|
|
31
31
|
type InsertableField,
|
|
32
|
+
withBufferedTreeEvents,
|
|
32
33
|
} from "./simple-tree/index.js";
|
|
33
34
|
|
|
34
35
|
// Future improvement TODOs:
|
|
@@ -891,16 +892,21 @@ export namespace System_TableSchema {
|
|
|
891
892
|
private _applyEditsInBatch(applyEdits: () => void): void {
|
|
892
893
|
const branch = TreeAlpha.branch(this);
|
|
893
894
|
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
895
|
+
// Ensure events are paused until all of the edits are applied.
|
|
896
|
+
// This ensures that the user sees the corresponding table-level edit as atomic,
|
|
897
|
+
// and ensures they are not spammed with intermediate events.
|
|
898
|
+
withBufferedTreeEvents(() => {
|
|
899
|
+
if (branch === undefined) {
|
|
900
|
+
// If this node does not have a corresponding branch, then it is unhydrated.
|
|
901
|
+
// I.e., it is not part of a collaborative session yet.
|
|
902
|
+
// Therefore, we don't need to run the edits as a transaction.
|
|
901
903
|
applyEdits();
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
+
} else {
|
|
905
|
+
branch.runTransaction(() => {
|
|
906
|
+
applyEdits();
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
});
|
|
904
910
|
}
|
|
905
911
|
|
|
906
912
|
/**
|
package/src/util/breakable.ts
CHANGED
|
@@ -31,9 +31,17 @@ export class Breakable {
|
|
|
31
31
|
*/
|
|
32
32
|
public use(): void {
|
|
33
33
|
if (this.brokenBy !== undefined) {
|
|
34
|
-
|
|
34
|
+
const error = new UsageError(
|
|
35
35
|
`Invalid use of ${this.name} after it was put into an invalid state by another error.\nOriginal Error:\n${this.brokenBy}`,
|
|
36
36
|
);
|
|
37
|
+
|
|
38
|
+
// This "cause" field is added in ES2022, but using if even without that built in support, it is still helpful.
|
|
39
|
+
// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause
|
|
40
|
+
// TODO: remove this cast when targeting ES2022 lib or later.
|
|
41
|
+
(error as { cause?: unknown }).cause =
|
|
42
|
+
(this.brokenBy as { cause?: unknown }).cause ?? this.brokenBy;
|
|
43
|
+
|
|
44
|
+
throw error;
|
|
37
45
|
}
|
|
38
46
|
}
|
|
39
47
|
|