@fluidframework/tree 2.61.0-355054 → 2.61.0-355603
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/alpha.d.ts +1 -1
- package/api-report/tree.alpha.api.md +10 -27
- package/api-report/tree.beta.api.md +5 -22
- package/api-report/tree.legacy.beta.api.md +5 -22
- package/api-report/tree.legacy.public.api.md +4 -21
- package/api-report/tree.public.api.md +4 -21
- package/beta.d.ts +1 -1
- package/dist/alpha.d.ts +1 -0
- package/dist/beta.d.ts +1 -0
- 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 +1 -0
- package/dist/packageVersion.d.ts +1 -1
- package/dist/packageVersion.js +1 -1
- package/dist/packageVersion.js.map +1 -1
- package/dist/public.d.ts +1 -0
- 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/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/internal.d.ts +1 -1
- package/legacy.d.ts +1 -1
- package/lib/alpha.d.ts +1 -0
- package/lib/beta.d.ts +1 -0
- 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 +1 -0
- package/lib/packageVersion.d.ts +1 -1
- package/lib/packageVersion.js +1 -1
- package/lib/packageVersion.js.map +1 -1
- package/lib/public.d.ts +1 -0
- 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/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/package.json +22 -22
- package/src/core/tree/anchorSet.ts +2 -2
- package/src/index.ts +1 -0
- package/src/packageVersion.ts +1 -1
- package/src/simple-tree/api/index.ts +1 -0
- package/src/simple-tree/api/schemaFactory.ts +31 -103
- 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
|
@@ -174,6 +174,33 @@ export type ScopedSchemaName<
|
|
|
174
174
|
|
|
175
175
|
const schemaStaticsPublic: SchemaStatics = schemaStatics;
|
|
176
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Create a class with `Statics` as both static properties and member properties.
|
|
179
|
+
* @privateRemarks
|
|
180
|
+
* An attempt was made to let this take in a base class so it could be used again on SchemaFactoryAlpha.
|
|
181
|
+
* This was unsuccessful, mostly due to issues with trying to manipulate constructor types.
|
|
182
|
+
*/
|
|
183
|
+
function classWithStatics<Statics extends object>(
|
|
184
|
+
statics: Statics,
|
|
185
|
+
): Statics & (new () => Statics) {
|
|
186
|
+
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
|
|
187
|
+
class WithStatics {}
|
|
188
|
+
|
|
189
|
+
Object.assign(WithStatics.prototype, statics);
|
|
190
|
+
Object.assign(WithStatics, statics);
|
|
191
|
+
return WithStatics as Statics & (new () => Statics);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Base class for SchemaFactory, exposes {@link SchemaStatics} as both static properties and member properties.
|
|
196
|
+
* @remarks
|
|
197
|
+
* Do not use this directly, use {@link SchemaFactory} instead.
|
|
198
|
+
* @privateRemarks
|
|
199
|
+
* Exported only as a workaround for {@link https://github.com/microsoft/TypeScript/issues/59550} and {@link https://github.com/microsoft/rushstack/issues/4429}.
|
|
200
|
+
* @system @public
|
|
201
|
+
*/
|
|
202
|
+
export const SchemaFactory_base = classWithStatics(schemaStaticsPublic);
|
|
203
|
+
|
|
177
204
|
// TODO:
|
|
178
205
|
// SchemaFactory.array references should link to the correct overloads, however the syntax for this does not seems to work currently for methods unless the they are not qualified with the class.
|
|
179
206
|
// API-Extractor requires such links to be qualified with the class, so it can't work.
|
|
@@ -289,8 +316,7 @@ const schemaStaticsPublic: SchemaStatics = schemaStatics;
|
|
|
289
316
|
export class SchemaFactory<
|
|
290
317
|
out TScope extends string | undefined = string | undefined,
|
|
291
318
|
TName extends number | string = string,
|
|
292
|
-
>
|
|
293
|
-
{
|
|
319
|
+
> extends SchemaFactory_base {
|
|
294
320
|
/**
|
|
295
321
|
* TODO:
|
|
296
322
|
* If users of this generate the same name because two different schema with the same identifier were used,
|
|
@@ -339,67 +365,9 @@ export class SchemaFactory<
|
|
|
339
365
|
* ```
|
|
340
366
|
*/
|
|
341
367
|
public readonly scope: TScope,
|
|
342
|
-
) {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
* {@inheritDoc SchemaStatics.string}
|
|
346
|
-
*/
|
|
347
|
-
public readonly string = schemaStaticsPublic.string;
|
|
348
|
-
|
|
349
|
-
/**
|
|
350
|
-
* {@inheritDoc SchemaStatics.number}
|
|
351
|
-
*/
|
|
352
|
-
public readonly number = schemaStaticsPublic.number;
|
|
353
|
-
|
|
354
|
-
/**
|
|
355
|
-
* {@inheritDoc SchemaStatics.boolean}
|
|
356
|
-
*/
|
|
357
|
-
public readonly boolean = schemaStaticsPublic.boolean;
|
|
358
|
-
|
|
359
|
-
/**
|
|
360
|
-
* {@inheritDoc SchemaStatics.null}
|
|
361
|
-
*/
|
|
362
|
-
public readonly null = schemaStaticsPublic.null;
|
|
363
|
-
|
|
364
|
-
/**
|
|
365
|
-
* {@inheritDoc SchemaStatics.handle}
|
|
366
|
-
*/
|
|
367
|
-
public readonly handle = schemaStaticsPublic.handle;
|
|
368
|
-
|
|
369
|
-
/**
|
|
370
|
-
* {@inheritDoc SchemaStatics.leaves}
|
|
371
|
-
*/
|
|
372
|
-
public readonly leaves = schemaStaticsPublic.leaves;
|
|
373
|
-
|
|
374
|
-
/**
|
|
375
|
-
* {@inheritDoc SchemaStatics.string}
|
|
376
|
-
*/
|
|
377
|
-
public static readonly string = schemaStaticsPublic.string;
|
|
378
|
-
|
|
379
|
-
/**
|
|
380
|
-
* {@inheritDoc SchemaStatics.number}
|
|
381
|
-
*/
|
|
382
|
-
public static readonly number = schemaStaticsPublic.number;
|
|
383
|
-
|
|
384
|
-
/**
|
|
385
|
-
* {@inheritDoc SchemaStatics.boolean}
|
|
386
|
-
*/
|
|
387
|
-
public static readonly boolean = schemaStaticsPublic.boolean;
|
|
388
|
-
|
|
389
|
-
/**
|
|
390
|
-
* {@inheritDoc SchemaStatics.null}
|
|
391
|
-
*/
|
|
392
|
-
public static readonly null = schemaStaticsPublic.null;
|
|
393
|
-
|
|
394
|
-
/**
|
|
395
|
-
* {@inheritDoc SchemaStatics.handle}
|
|
396
|
-
*/
|
|
397
|
-
public static readonly handle = schemaStaticsPublic.handle;
|
|
398
|
-
|
|
399
|
-
/**
|
|
400
|
-
* {@inheritDoc SchemaStatics.leaves}
|
|
401
|
-
*/
|
|
402
|
-
public static readonly leaves = schemaStaticsPublic.leaves;
|
|
368
|
+
) {
|
|
369
|
+
super();
|
|
370
|
+
}
|
|
403
371
|
|
|
404
372
|
/**
|
|
405
373
|
* Define a {@link TreeNodeSchemaClass} for a {@link TreeObjectNode}.
|
|
@@ -807,46 +775,6 @@ export class SchemaFactory<
|
|
|
807
775
|
>;
|
|
808
776
|
}
|
|
809
777
|
|
|
810
|
-
/**
|
|
811
|
-
* {@inheritDoc SchemaStatics.optional}
|
|
812
|
-
*/
|
|
813
|
-
public readonly optional = schemaStaticsPublic.optional;
|
|
814
|
-
|
|
815
|
-
/**
|
|
816
|
-
* {@inheritDoc SchemaStatics.required}
|
|
817
|
-
*/
|
|
818
|
-
public readonly required = schemaStaticsPublic.required;
|
|
819
|
-
|
|
820
|
-
/**
|
|
821
|
-
* {@inheritDoc SchemaStatics.optionalRecursive}
|
|
822
|
-
*/
|
|
823
|
-
public readonly optionalRecursive = schemaStaticsPublic.optionalRecursive;
|
|
824
|
-
|
|
825
|
-
/**
|
|
826
|
-
* {@inheritDoc SchemaStatics.requiredRecursive}
|
|
827
|
-
*/
|
|
828
|
-
public readonly requiredRecursive = schemaStaticsPublic.requiredRecursive;
|
|
829
|
-
|
|
830
|
-
/**
|
|
831
|
-
* {@inheritDoc SchemaStatics.optional}
|
|
832
|
-
*/
|
|
833
|
-
public static readonly optional = schemaStaticsPublic.optional;
|
|
834
|
-
|
|
835
|
-
/**
|
|
836
|
-
* {@inheritDoc SchemaStatics.required}
|
|
837
|
-
*/
|
|
838
|
-
public static readonly required = schemaStaticsPublic.required;
|
|
839
|
-
|
|
840
|
-
/**
|
|
841
|
-
* {@inheritDoc SchemaStatics.optionalRecursive}
|
|
842
|
-
*/
|
|
843
|
-
public static readonly optionalRecursive = schemaStaticsPublic.optionalRecursive;
|
|
844
|
-
|
|
845
|
-
/**
|
|
846
|
-
* {@inheritDoc SchemaStatics.requiredRecursive}
|
|
847
|
-
*/
|
|
848
|
-
public static readonly requiredRecursive = schemaStaticsPublic.requiredRecursive;
|
|
849
|
-
|
|
850
778
|
/**
|
|
851
779
|
* A special readonly field which holds an identifier string for an object node.
|
|
852
780
|
* @remarks
|
|
@@ -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
|
/**
|