@fluidframework/tree 2.93.0 → 2.100.0
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/CHANGELOG.md +47 -0
- package/INCREMENTAL_SUMMARY.md +89 -0
- package/README.md +6 -0
- package/api-report/tree.alpha.api.md +3 -0
- package/dist/feature-libraries/chunked-forest/basicChunk.d.ts +25 -1
- package/dist/feature-libraries/chunked-forest/basicChunk.d.ts.map +1 -1
- package/dist/feature-libraries/chunked-forest/basicChunk.js +71 -18
- package/dist/feature-libraries/chunked-forest/basicChunk.js.map +1 -1
- package/dist/feature-libraries/chunked-forest/codec/chunkDecoding.js +1 -1
- package/dist/feature-libraries/chunked-forest/codec/chunkDecoding.js.map +1 -1
- package/dist/feature-libraries/chunked-forest/codec/codecs.d.ts.map +1 -1
- package/dist/feature-libraries/chunked-forest/codec/codecs.js +1 -1
- package/dist/feature-libraries/chunked-forest/codec/codecs.js.map +1 -1
- package/dist/feature-libraries/chunked-forest/codec/compressedEncode.d.ts +1 -1
- package/dist/feature-libraries/chunked-forest/codec/compressedEncode.d.ts.map +1 -1
- package/dist/feature-libraries/chunked-forest/codec/compressedEncode.js +1 -1
- package/dist/feature-libraries/chunked-forest/codec/compressedEncode.js.map +1 -1
- package/dist/feature-libraries/chunked-forest/codec/format/formatGeneric.d.ts +1 -2
- package/dist/feature-libraries/chunked-forest/codec/format/formatGeneric.d.ts.map +1 -1
- package/dist/feature-libraries/chunked-forest/codec/format/formatGeneric.js +0 -1
- package/dist/feature-libraries/chunked-forest/codec/format/formatGeneric.js.map +1 -1
- package/dist/feature-libraries/chunked-forest/codec/format/index.d.ts +1 -1
- package/dist/feature-libraries/chunked-forest/codec/format/index.d.ts.map +1 -1
- package/dist/feature-libraries/chunked-forest/codec/format/index.js +2 -1
- package/dist/feature-libraries/chunked-forest/codec/format/index.js.map +1 -1
- package/dist/feature-libraries/chunked-forest/codec/format/versions.d.ts +10 -2
- package/dist/feature-libraries/chunked-forest/codec/format/versions.d.ts.map +1 -1
- package/dist/feature-libraries/chunked-forest/codec/format/versions.js +15 -1
- package/dist/feature-libraries/chunked-forest/codec/format/versions.js.map +1 -1
- package/dist/feature-libraries/chunked-forest/uniformChunk.d.ts +6 -1
- package/dist/feature-libraries/chunked-forest/uniformChunk.d.ts.map +1 -1
- package/dist/feature-libraries/chunked-forest/uniformChunk.js +25 -1
- package/dist/feature-libraries/chunked-forest/uniformChunk.js.map +1 -1
- package/dist/feature-libraries/forest-summary/incrementalSummaryBuilder.d.ts +19 -0
- package/dist/feature-libraries/forest-summary/incrementalSummaryBuilder.d.ts.map +1 -1
- package/dist/feature-libraries/forest-summary/incrementalSummaryBuilder.js +76 -22
- package/dist/feature-libraries/forest-summary/incrementalSummaryBuilder.js.map +1 -1
- 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/shared-tree/treeCheckout.d.ts +1 -1
- package/dist/shared-tree/treeCheckout.d.ts.map +1 -1
- package/dist/simple-tree/api/schemaFactoryAlpha.d.ts +17 -1
- package/dist/simple-tree/api/schemaFactoryAlpha.d.ts.map +1 -1
- package/dist/simple-tree/api/schemaFactoryAlpha.js +9 -0
- package/dist/simple-tree/api/schemaFactoryAlpha.js.map +1 -1
- package/docs/user-facing/isolated-declarations.md +147 -0
- package/lib/feature-libraries/chunked-forest/basicChunk.d.ts +25 -1
- package/lib/feature-libraries/chunked-forest/basicChunk.d.ts.map +1 -1
- package/lib/feature-libraries/chunked-forest/basicChunk.js +72 -19
- package/lib/feature-libraries/chunked-forest/basicChunk.js.map +1 -1
- package/lib/feature-libraries/chunked-forest/codec/chunkDecoding.js +2 -2
- package/lib/feature-libraries/chunked-forest/codec/chunkDecoding.js.map +1 -1
- package/lib/feature-libraries/chunked-forest/codec/codecs.d.ts.map +1 -1
- package/lib/feature-libraries/chunked-forest/codec/codecs.js +2 -2
- package/lib/feature-libraries/chunked-forest/codec/codecs.js.map +1 -1
- package/lib/feature-libraries/chunked-forest/codec/compressedEncode.d.ts +1 -1
- package/lib/feature-libraries/chunked-forest/codec/compressedEncode.d.ts.map +1 -1
- package/lib/feature-libraries/chunked-forest/codec/compressedEncode.js +2 -2
- package/lib/feature-libraries/chunked-forest/codec/compressedEncode.js.map +1 -1
- package/lib/feature-libraries/chunked-forest/codec/format/formatGeneric.d.ts +1 -2
- package/lib/feature-libraries/chunked-forest/codec/format/formatGeneric.d.ts.map +1 -1
- package/lib/feature-libraries/chunked-forest/codec/format/formatGeneric.js +0 -1
- package/lib/feature-libraries/chunked-forest/codec/format/formatGeneric.js.map +1 -1
- package/lib/feature-libraries/chunked-forest/codec/format/index.d.ts +1 -1
- package/lib/feature-libraries/chunked-forest/codec/format/index.d.ts.map +1 -1
- package/lib/feature-libraries/chunked-forest/codec/format/index.js +1 -1
- package/lib/feature-libraries/chunked-forest/codec/format/index.js.map +1 -1
- package/lib/feature-libraries/chunked-forest/codec/format/versions.d.ts +10 -2
- package/lib/feature-libraries/chunked-forest/codec/format/versions.d.ts.map +1 -1
- package/lib/feature-libraries/chunked-forest/codec/format/versions.js +13 -0
- package/lib/feature-libraries/chunked-forest/codec/format/versions.js.map +1 -1
- package/lib/feature-libraries/chunked-forest/uniformChunk.d.ts +6 -1
- package/lib/feature-libraries/chunked-forest/uniformChunk.d.ts.map +1 -1
- package/lib/feature-libraries/chunked-forest/uniformChunk.js +26 -2
- package/lib/feature-libraries/chunked-forest/uniformChunk.js.map +1 -1
- package/lib/feature-libraries/forest-summary/incrementalSummaryBuilder.d.ts +19 -0
- package/lib/feature-libraries/forest-summary/incrementalSummaryBuilder.d.ts.map +1 -1
- package/lib/feature-libraries/forest-summary/incrementalSummaryBuilder.js +76 -22
- package/lib/feature-libraries/forest-summary/incrementalSummaryBuilder.js.map +1 -1
- 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/shared-tree/treeCheckout.d.ts +1 -1
- package/lib/shared-tree/treeCheckout.d.ts.map +1 -1
- package/lib/simple-tree/api/schemaFactoryAlpha.d.ts +17 -1
- package/lib/simple-tree/api/schemaFactoryAlpha.d.ts.map +1 -1
- package/lib/simple-tree/api/schemaFactoryAlpha.js +9 -0
- package/lib/simple-tree/api/schemaFactoryAlpha.js.map +1 -1
- package/package.json +24 -24
- package/src/feature-libraries/chunked-forest/basicChunk.ts +76 -20
- package/src/feature-libraries/chunked-forest/codec/chunkDecoding.ts +2 -2
- package/src/feature-libraries/chunked-forest/codec/codecs.ts +2 -1
- package/src/feature-libraries/chunked-forest/codec/compressedEncode.ts +3 -2
- package/src/feature-libraries/chunked-forest/codec/format/formatGeneric.ts +0 -1
- package/src/feature-libraries/chunked-forest/codec/format/index.ts +1 -0
- package/src/feature-libraries/chunked-forest/codec/format/versions.ts +15 -0
- package/src/feature-libraries/chunked-forest/uniformChunk.ts +32 -2
- package/src/feature-libraries/forest-summary/incrementalSummaryBuilder.ts +116 -31
- package/src/packageVersion.ts +1 -1
- package/src/simple-tree/api/schemaFactoryAlpha.ts +34 -3
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Licensed under the MIT License.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { assert, oob, fail } from "@fluidframework/core-utils/internal";
|
|
6
|
+
import { assert, oob, fail, debugAssert } from "@fluidframework/core-utils/internal";
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
9
|
CursorLocationType,
|
|
@@ -166,10 +166,19 @@ export class BasicChunkCursor extends SynchronousCursor implements ChunkedCursor
|
|
|
166
166
|
if (this.nestedCursor !== undefined) {
|
|
167
167
|
return this.nestedCursor.mode;
|
|
168
168
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
169
|
+
this.assertChunkStacksMatchNodeDepth();
|
|
170
|
+
return this.siblingStack.length % 2 === 0
|
|
171
|
+
? CursorLocationType.Fields
|
|
172
|
+
: CursorLocationType.Nodes;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Asserts that the node-only stacks (`indexOfChunkStack` and `indexWithinChunkStack`) are in sync with `siblingStack`.
|
|
177
|
+
* Since `siblingStack` interleaves field and node levels while the node-only stacks are pushed/popped only on node-level transitions,
|
|
178
|
+
* their length should always equal the number of node levels traversed.
|
|
179
|
+
*/
|
|
180
|
+
private assertChunkStacksMatchNodeDepth(): void {
|
|
181
|
+
const halfHeight = this.getNodeOnlyHeightFromHeight();
|
|
173
182
|
assert(
|
|
174
183
|
this.indexOfChunkStack.length === halfHeight,
|
|
175
184
|
0x51c /* unexpected indexOfChunkStack */,
|
|
@@ -178,9 +187,6 @@ export class BasicChunkCursor extends SynchronousCursor implements ChunkedCursor
|
|
|
178
187
|
this.indexWithinChunkStack.length === halfHeight,
|
|
179
188
|
0x51d /* unexpected indexWithinChunkStack */,
|
|
180
189
|
);
|
|
181
|
-
return this.siblingStack.length % 2 === 0
|
|
182
|
-
? CursorLocationType.Fields
|
|
183
|
-
: CursorLocationType.Nodes;
|
|
184
190
|
}
|
|
185
191
|
|
|
186
192
|
public getFieldKey(): FieldKey {
|
|
@@ -203,9 +209,32 @@ export class BasicChunkCursor extends SynchronousCursor implements ChunkedCursor
|
|
|
203
209
|
return this.indexStack[height] ?? oob();
|
|
204
210
|
}
|
|
205
211
|
|
|
206
|
-
private
|
|
207
|
-
|
|
208
|
-
|
|
212
|
+
private getStackedChunkIndex(height: number): number {
|
|
213
|
+
assert(height % 2 === 1, 0xcf3 /* must be node height */);
|
|
214
|
+
assert(height >= 0, 0xcf4 /* must not be above root */);
|
|
215
|
+
return this.indexOfChunkStack[this.getNodeOnlyHeightFromHeight(height)] ?? oob();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private getStackedChunk(height: number): BasicChunk {
|
|
219
|
+
const index = this.getStackedChunkIndex(height);
|
|
220
|
+
const chunk = (this.siblingStack[height] as readonly TreeChunk[])[index];
|
|
221
|
+
debugAssert(() => chunk instanceof BasicChunk || "only basic chunks are expected");
|
|
222
|
+
return chunk as BasicChunk;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Converts a {@link height}, which contains field and node levels, into the corresponding depth/index
|
|
227
|
+
* for the node-only stacks ({@link indexOfChunkStack} and {@link indexWithinChunkStack}), which are
|
|
228
|
+
* only pushed on node-level transitions.
|
|
229
|
+
*
|
|
230
|
+
* @param height - A depth in {@link siblingStack} to convert. Defaults to {@link siblingStack}'s
|
|
231
|
+
* current length, which gives the current depth of the node-only stacks.
|
|
232
|
+
* @returns `floor(height / 2)` — the number of node levels at or below the given stack height.
|
|
233
|
+
*/
|
|
234
|
+
private getNodeOnlyHeightFromHeight(height: number = this.siblingStack.length): number {
|
|
235
|
+
// The bitwise shift computes the floor, which is valid assuming the depth is less than 2^31, which seems safe.
|
|
236
|
+
// eslint-disable-next-line no-bitwise
|
|
237
|
+
return height >> 1;
|
|
209
238
|
}
|
|
210
239
|
|
|
211
240
|
public getFieldLength(): number {
|
|
@@ -322,6 +351,11 @@ export class BasicChunkCursor extends SynchronousCursor implements ChunkedCursor
|
|
|
322
351
|
assert(this.mode === CursorLocationType.Nodes, 0x528 /* must be in nodes mode */);
|
|
323
352
|
this.siblingStack.push(this.siblings);
|
|
324
353
|
this.indexStack.push(this.index);
|
|
354
|
+
// Save the chunk array position of the current node. When siblings contain
|
|
355
|
+
// multi node chunks, the flat node index diverges from the array position,
|
|
356
|
+
// so getField needs this to locate the parent in the sibling array.
|
|
357
|
+
this.indexOfChunkStack.push(this.indexOfChunk);
|
|
358
|
+
this.indexWithinChunkStack.push(this.indexWithinChunk);
|
|
325
359
|
|
|
326
360
|
// For fields, siblings are only used for key lookup and
|
|
327
361
|
// nextField and which has arbitrary iteration order,
|
|
@@ -330,6 +364,7 @@ export class BasicChunkCursor extends SynchronousCursor implements ChunkedCursor
|
|
|
330
364
|
// at the cost of an allocation here.
|
|
331
365
|
this.index = 0;
|
|
332
366
|
this.siblings = [key];
|
|
367
|
+
this.assertChunkStacksMatchNodeDepth();
|
|
333
368
|
}
|
|
334
369
|
|
|
335
370
|
public nextField(): boolean {
|
|
@@ -355,8 +390,11 @@ export class BasicChunkCursor extends SynchronousCursor implements ChunkedCursor
|
|
|
355
390
|
|
|
356
391
|
this.siblingStack.push(this.siblings);
|
|
357
392
|
this.indexStack.push(this.index);
|
|
393
|
+
this.indexOfChunkStack.push(this.indexOfChunk);
|
|
394
|
+
this.indexWithinChunkStack.push(this.indexWithinChunk);
|
|
358
395
|
this.index = 0;
|
|
359
396
|
this.siblings = [...fields.keys()]; // TODO: avoid this copy
|
|
397
|
+
this.assertChunkStacksMatchNodeDepth();
|
|
360
398
|
return true;
|
|
361
399
|
}
|
|
362
400
|
|
|
@@ -422,12 +460,11 @@ export class BasicChunkCursor extends SynchronousCursor implements ChunkedCursor
|
|
|
422
460
|
}
|
|
423
461
|
this.siblingStack.push(this.siblings);
|
|
424
462
|
this.indexStack.push(this.index);
|
|
425
|
-
this.indexOfChunkStack.push(this.indexOfChunk);
|
|
426
|
-
this.indexWithinChunkStack.push(this.indexWithinChunk);
|
|
427
463
|
this.index = 0;
|
|
428
464
|
this.siblings = siblings;
|
|
429
465
|
this.indexOfChunk = 0;
|
|
430
466
|
this.indexWithinChunk = 0;
|
|
467
|
+
this.assertChunkStacksMatchNodeDepth();
|
|
431
468
|
this.initNestedCursor();
|
|
432
469
|
return true;
|
|
433
470
|
}
|
|
@@ -486,6 +523,12 @@ export class BasicChunkCursor extends SynchronousCursor implements ChunkedCursor
|
|
|
486
523
|
this.siblings =
|
|
487
524
|
this.siblingStack.pop() ?? fail(0xaf0 /* Unexpected siblingStack.length */);
|
|
488
525
|
this.index = this.indexStack.pop() ?? fail(0xaf1 /* Unexpected indexStack.length */);
|
|
526
|
+
this.indexOfChunk =
|
|
527
|
+
this.indexOfChunkStack.pop() ?? fail(0xcf5 /* Unexpected indexOfChunkStack.length */);
|
|
528
|
+
this.indexWithinChunk =
|
|
529
|
+
this.indexWithinChunkStack.pop() ??
|
|
530
|
+
fail(0xcf6 /* Unexpected indexWithinChunkStack.length */);
|
|
531
|
+
this.assertChunkStacksMatchNodeDepth();
|
|
489
532
|
}
|
|
490
533
|
|
|
491
534
|
public exitNode(): void {
|
|
@@ -502,18 +545,27 @@ export class BasicChunkCursor extends SynchronousCursor implements ChunkedCursor
|
|
|
502
545
|
this.siblings =
|
|
503
546
|
this.siblingStack.pop() ?? fail(0xaf2 /* Unexpected siblingStack.length */);
|
|
504
547
|
this.index = this.indexStack.pop() ?? fail(0xaf3 /* Unexpected indexStack.length */);
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
this.
|
|
508
|
-
|
|
509
|
-
|
|
548
|
+
// At the Fields level these aren't semantically used, but reset for consistent state
|
|
549
|
+
// (so a fully-iterated cursor matches a fresh cursor at the same logical position).
|
|
550
|
+
this.indexOfChunk = 0;
|
|
551
|
+
this.indexWithinChunk = 0;
|
|
552
|
+
this.assertChunkStacksMatchNodeDepth();
|
|
510
553
|
}
|
|
511
554
|
|
|
512
555
|
private getNode(): BasicChunk {
|
|
513
556
|
assert(this.mode === CursorLocationType.Nodes, 0x52f /* can only get node when in node */);
|
|
514
|
-
|
|
557
|
+
const chunk = (this.siblings as TreeChunk[])[this.indexOfChunk];
|
|
558
|
+
debugAssert(() => chunk instanceof BasicChunk || "only basic chunks are expected");
|
|
559
|
+
return chunk as BasicChunk;
|
|
515
560
|
}
|
|
516
561
|
|
|
562
|
+
/**
|
|
563
|
+
* Resolves the chunks that make up the field the cursor is currently in. At the root, this is
|
|
564
|
+
* {@link root} directly. Otherwise, the cursor must be in {@link CursorLocationType.Fields} mode,
|
|
565
|
+
* and the result is looked up on the parent node using the current field key.
|
|
566
|
+
*
|
|
567
|
+
* @returns The chunks that make up the field the cursor is currently in.
|
|
568
|
+
*/
|
|
517
569
|
private getField(): readonly TreeChunk[] {
|
|
518
570
|
if (this.siblingStack.length === 0) {
|
|
519
571
|
return this.root;
|
|
@@ -522,7 +574,11 @@ export class BasicChunkCursor extends SynchronousCursor implements ChunkedCursor
|
|
|
522
574
|
this.mode === CursorLocationType.Fields,
|
|
523
575
|
0x530 /* can only get field when in fields */,
|
|
524
576
|
);
|
|
525
|
-
|
|
577
|
+
// The parent node is the `BasicChunk` in the node array at the top of
|
|
578
|
+
// `siblingStack` while we are in `CursorLocationType.Fields` mode. We need the parent
|
|
579
|
+
// since a field's chunks are stored on the parent node's `BasicChunk.fields` map, not on
|
|
580
|
+
// the cursor itself.
|
|
581
|
+
const parent = this.getStackedChunk(this.siblingStack.length - 1);
|
|
526
582
|
const key: FieldKey = this.getFieldKey();
|
|
527
583
|
const field = parent.fields.get(key) ?? [];
|
|
528
584
|
return field;
|
|
@@ -49,8 +49,8 @@ import {
|
|
|
49
49
|
type EncodedNestedArrayShape,
|
|
50
50
|
type EncodedNodeShape,
|
|
51
51
|
type EncodedValueShape,
|
|
52
|
-
FieldBatchFormatVersion,
|
|
53
52
|
SpecialField,
|
|
53
|
+
supportsIncrementalEncoding,
|
|
54
54
|
} from "./format/index.js";
|
|
55
55
|
|
|
56
56
|
export interface IdDecodingContext {
|
|
@@ -256,7 +256,7 @@ export class IncrementalChunkDecoder implements ChunkDecoder {
|
|
|
256
256
|
|
|
257
257
|
const chunkDecoder = (batch: EncodedFieldBatchV2): TreeChunk => {
|
|
258
258
|
assert(
|
|
259
|
-
batch.version
|
|
259
|
+
supportsIncrementalEncoding(batch.version),
|
|
260
260
|
0xc9f /* Unsupported FieldBatchFormatVersion for incremental chunks; must be v2 or higher */,
|
|
261
261
|
);
|
|
262
262
|
const context = new DecoderContext(
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
EncodedFieldBatchV1,
|
|
31
31
|
EncodedFieldBatchV2,
|
|
32
32
|
FieldBatchFormatVersion,
|
|
33
|
+
supportsIncrementalEncoding,
|
|
33
34
|
type EncodedFieldBatchV1OrV2,
|
|
34
35
|
} from "./format/index.js";
|
|
35
36
|
import type { IncrementalEncodingPolicy } from "./incrementalEncodingPolicy.js";
|
|
@@ -147,7 +148,7 @@ function makeFieldBatchCodecForVersion(
|
|
|
147
148
|
}
|
|
148
149
|
case TreeCompressionStrategy.CompressedIncremental: {
|
|
149
150
|
assert(
|
|
150
|
-
version
|
|
151
|
+
supportsIncrementalEncoding(version),
|
|
151
152
|
0xca0 /* Unsupported FieldBatchFormatVersion for incremental encoding; must be v2 or higher */,
|
|
152
153
|
);
|
|
153
154
|
// Incremental encoding is only supported for CompressedIncremental.
|
|
@@ -36,8 +36,9 @@ import {
|
|
|
36
36
|
type EncodedFieldBatchV1OrV2,
|
|
37
37
|
type EncodedNestedArrayShape,
|
|
38
38
|
type EncodedValueShape,
|
|
39
|
-
FieldBatchFormatVersion,
|
|
39
|
+
type FieldBatchFormatVersion,
|
|
40
40
|
SpecialField,
|
|
41
|
+
supportsIncrementalEncoding,
|
|
41
42
|
} from "./format/index.js";
|
|
42
43
|
|
|
43
44
|
/**
|
|
@@ -461,7 +462,7 @@ export const incrementalFieldEncoder: FieldEncoder = {
|
|
|
461
462
|
0xc88 /* incremental encoder must be defined to use incrementalFieldEncoder */,
|
|
462
463
|
);
|
|
463
464
|
assert(
|
|
464
|
-
context.version
|
|
465
|
+
supportsIncrementalEncoding(context.version),
|
|
465
466
|
0xca1 /* Unsupported FieldBatchFormatVersion for incremental encoding; must be v2 or higher */,
|
|
466
467
|
);
|
|
467
468
|
|
|
@@ -31,6 +31,21 @@ export const FieldBatchFormatVersion = strictEnum("FieldBatchFormatVersion", {
|
|
|
31
31
|
v2: 2,
|
|
32
32
|
});
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Whether the given format version supports incremental chunk encoding.
|
|
36
|
+
*
|
|
37
|
+
* @remarks
|
|
38
|
+
* This helper should be used for comparison since experimental versions
|
|
39
|
+
* can be a string.
|
|
40
|
+
*/
|
|
41
|
+
export function supportsIncrementalEncoding(version: FieldBatchFormatVersion): boolean {
|
|
42
|
+
if (version === FieldBatchFormatVersion.v1) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
|
|
34
49
|
/**
|
|
35
50
|
* Encoded {@link FieldBatch} using V1 format.
|
|
36
51
|
* @remarks
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
cursorChunk,
|
|
21
21
|
dummyRoot,
|
|
22
22
|
} from "../../core/index.js";
|
|
23
|
-
import { ReferenceCountedBase, hasSome } from "../../util/index.js";
|
|
23
|
+
import { ReferenceCountedBase, getOrCreate, hasSome } from "../../util/index.js";
|
|
24
24
|
import { SynchronousCursor, prefixFieldPath, prefixPath } from "../treeCursorUtils.js";
|
|
25
25
|
|
|
26
26
|
/**
|
|
@@ -83,6 +83,23 @@ export class UniformChunk extends ReferenceCountedBase implements TreeChunk {
|
|
|
83
83
|
*/
|
|
84
84
|
export type FieldShape = readonly [FieldKey, TreeShape, number];
|
|
85
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Maximum topLevelLength value (exclusive) for which {@link TreeShape.withTopLevelLength}
|
|
88
|
+
* caches the resulting {@link ChunkShape}. Values at or above this threshold always
|
|
89
|
+
* create a new instance to prevent unbounded cache growth.
|
|
90
|
+
*
|
|
91
|
+
* @remarks
|
|
92
|
+
* This value is an estimation of the general size needed to cover current workflows,
|
|
93
|
+
* not a researched constant, and is safe to tune as workloads change.
|
|
94
|
+
*
|
|
95
|
+
* Raising this value captures more chunk sizes in the cache, at the cost of
|
|
96
|
+
* each `TreeShape` retaining up to `chunkShapeCacheLimit - 1` cached entries for the
|
|
97
|
+
* lifetime of the shape. Lowering it reduces memory held per `TreeShape` but forces
|
|
98
|
+
* small chunks, where the relative cost of rebuilding `positions` is highest, to pay
|
|
99
|
+
* the construction cost on every call.
|
|
100
|
+
*/
|
|
101
|
+
const chunkShapeCacheLimit = 8;
|
|
102
|
+
|
|
86
103
|
/**
|
|
87
104
|
* The "shape" of a tree.
|
|
88
105
|
* Does not contain the actual values from the tree, but describes everything else,
|
|
@@ -110,7 +127,13 @@ export class TreeShape {
|
|
|
110
127
|
public readonly mayContainCompressedIds: boolean;
|
|
111
128
|
|
|
112
129
|
/**
|
|
113
|
-
*
|
|
130
|
+
* Cache for ChunkShape instances created by {@link withTopLevelLength}.
|
|
131
|
+
* `topLevelLength` is always a positive integer (enforced by the {@link ChunkShape} constructor),
|
|
132
|
+
* so the cache only ever holds entries for values in `1..chunkShapeCacheLimit - 1` to prevent unbounded growth.
|
|
133
|
+
*/
|
|
134
|
+
private readonly chunkShapeCache: Map<number, ChunkShape> = new Map();
|
|
135
|
+
|
|
136
|
+
/**
|
|
114
137
|
* @param type - {@link TreeNodeSchemaIdentifier} used to compare shapes.
|
|
115
138
|
* @param hasValue - whether or not the TreeShape has a value.
|
|
116
139
|
* @param fieldsArray - an array of {@link FieldShape} values, which contains a TreeShape for each FieldKey.
|
|
@@ -179,6 +202,13 @@ export class TreeShape {
|
|
|
179
202
|
}
|
|
180
203
|
|
|
181
204
|
public withTopLevelLength(topLevelLength: number): ChunkShape {
|
|
205
|
+
if (topLevelLength < chunkShapeCacheLimit) {
|
|
206
|
+
return getOrCreate(
|
|
207
|
+
this.chunkShapeCache,
|
|
208
|
+
topLevelLength,
|
|
209
|
+
() => new ChunkShape(this, topLevelLength),
|
|
210
|
+
);
|
|
211
|
+
}
|
|
182
212
|
return new ChunkShape(this, topLevelLength);
|
|
183
213
|
}
|
|
184
214
|
}
|
|
@@ -50,10 +50,12 @@ interface ChunkLoadProperties {
|
|
|
50
50
|
*/
|
|
51
51
|
readonly encodedContents: EncodedFieldBatchV2;
|
|
52
52
|
/**
|
|
53
|
-
* The
|
|
54
|
-
*
|
|
53
|
+
* The reference ID of this chunk's parent in the summary tree, or `undefined` if this chunk is
|
|
54
|
+
* at the top level (directly under the forest summary tree).
|
|
55
|
+
* Stored here so that {@link ForestIncrementalSummaryBuilder.decodeIncrementalChunk} can
|
|
56
|
+
* reconstruct the correct {@link ChunkSummaryProperties} without re-parsing a path string.
|
|
55
57
|
*/
|
|
56
|
-
readonly
|
|
58
|
+
readonly parentReferenceId: ChunkReferenceId | undefined;
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
/**
|
|
@@ -68,10 +70,20 @@ interface ChunkSummaryProperties {
|
|
|
68
70
|
*/
|
|
69
71
|
readonly referenceId: ChunkReferenceId;
|
|
70
72
|
/**
|
|
71
|
-
* The
|
|
72
|
-
*
|
|
73
|
+
* The reference ID of this chunk's parent in the summary tree, or `undefined` if this chunk
|
|
74
|
+
* is at the top level (has no incremental parent).
|
|
75
|
+
*
|
|
76
|
+
* @remarks
|
|
77
|
+
* Storing only the immediate parent (rather than the full path string) keeps every chunk's
|
|
78
|
+
* tracking entry correct even when an ancestor is re-encoded and receives a new reference ID.
|
|
79
|
+
* The full summary path is computed on demand by {@link ForestIncrementalSummaryBuilder.computeHandlePathInLatestSummary}
|
|
80
|
+
* by walking up the parent chain through {@link TrackedSummaryProperties.latestSummaryRefIdMap}.
|
|
81
|
+
*
|
|
82
|
+
* If a parent chunk is encoded as a handle in the current summary its reference ID is unchanged,
|
|
83
|
+
* so its children's `parentReferenceId` values copied forward by `completeSummary` remain valid
|
|
84
|
+
* without any additional update.
|
|
73
85
|
*/
|
|
74
|
-
readonly
|
|
86
|
+
readonly parentReferenceId: ChunkReferenceId | undefined;
|
|
75
87
|
}
|
|
76
88
|
|
|
77
89
|
/**
|
|
@@ -109,6 +121,13 @@ interface TrackedSummaryProperties {
|
|
|
109
121
|
* Serializes content (including {@link (IFluidHandle:interface)}s) for adding to a summary blob.
|
|
110
122
|
*/
|
|
111
123
|
stringify: SummaryElementStringifier;
|
|
124
|
+
/**
|
|
125
|
+
* Reverse lookup map for the latest summary: maps each chunk's {@link ChunkReferenceId} to its
|
|
126
|
+
* {@link ChunkSummaryProperties}.
|
|
127
|
+
* Used by {@link ForestIncrementalSummaryBuilder.computeHandlePathInLatestSummary} to traverse
|
|
128
|
+
* the parent chain when generating handle paths.
|
|
129
|
+
*/
|
|
130
|
+
readonly latestSummaryRefIdMap: Map<ChunkReferenceId, ChunkSummaryProperties>;
|
|
112
131
|
}
|
|
113
132
|
|
|
114
133
|
/**
|
|
@@ -177,6 +196,16 @@ export class ForestIncrementalSummaryBuilder implements IncrementalEncoderDecode
|
|
|
177
196
|
/**
|
|
178
197
|
* For a given summary sequence number, keeps track of a chunk's properties that will be used to generate
|
|
179
198
|
* a summary handle for the chunk if it does not change between summaries.
|
|
199
|
+
*
|
|
200
|
+
* @remarks
|
|
201
|
+
* `chunk` (the TreeChunk object) is used as the map key by object identity.
|
|
202
|
+
* This assumes each chunk appears at exactly one position in the forest — an invariant that holds because every
|
|
203
|
+
* node in a tree has a single parent.
|
|
204
|
+
* If the forest ever introduced structural sharing (two positions backed by the same TreeChunk object),
|
|
205
|
+
* a second call here would silently overwrite the first entry, causing the first position's handle to point
|
|
206
|
+
* to the second position's parent in subsequent summaries. In theory, this should be fine from summary perspective
|
|
207
|
+
* because the chunk contents are the same. But, it could lead to confusing handle paths in the summary tree and
|
|
208
|
+
* may lead to other unexpected behavior. Adequate tests should be added if structural sharing is introduced.
|
|
180
209
|
*/
|
|
181
210
|
private readonly chunkTrackingPropertiesMap: NestedMap<
|
|
182
211
|
number,
|
|
@@ -249,12 +278,14 @@ export class ForestIncrementalSummaryBuilder implements IncrementalEncoderDecode
|
|
|
249
278
|
// the contents of incremental chunks in any sub-trees.
|
|
250
279
|
const downloadChunkContentsInTree = async (
|
|
251
280
|
snapshotTree: ISnapshotTree,
|
|
252
|
-
|
|
281
|
+
parentPathSegments: string[],
|
|
282
|
+
parentReferenceId: ChunkReferenceId | undefined,
|
|
253
283
|
): Promise<void> => {
|
|
254
284
|
// All trees in the snapshot tree are for incremental chunks. The key is the chunk's reference ID
|
|
255
285
|
// and the value is the snapshot tree for the chunk.
|
|
256
286
|
for (const [chunkReferenceId, chunkSnapshotTree] of Object.entries(snapshotTree.trees)) {
|
|
257
|
-
const
|
|
287
|
+
const chunkSubTreeSegments = [...parentPathSegments, chunkReferenceId];
|
|
288
|
+
const chunkSubTreePath = chunkSubTreeSegments.join("/");
|
|
258
289
|
const chunkContentsPath = `${chunkSubTreePath}/${summaryContentBlobKey}`;
|
|
259
290
|
if (!(await args.services.contains(chunkContentsPath))) {
|
|
260
291
|
throw new LoggingError(
|
|
@@ -266,7 +297,7 @@ export class ForestIncrementalSummaryBuilder implements IncrementalEncoderDecode
|
|
|
266
297
|
)) as EncodedFieldBatchV2; // TODO: this should use a codec to validate the data instead of just type casting.
|
|
267
298
|
this.loadedChunksMap.set(chunkReferenceId, {
|
|
268
299
|
encodedContents: chunkContents,
|
|
269
|
-
|
|
300
|
+
parentReferenceId,
|
|
270
301
|
});
|
|
271
302
|
|
|
272
303
|
const chunkReferenceIdNumber = Number(chunkReferenceId);
|
|
@@ -275,10 +306,15 @@ export class ForestIncrementalSummaryBuilder implements IncrementalEncoderDecode
|
|
|
275
306
|
);
|
|
276
307
|
|
|
277
308
|
// Recursively download the contents of chunks in this chunk's sub tree.
|
|
278
|
-
await downloadChunkContentsInTree(
|
|
309
|
+
await downloadChunkContentsInTree(
|
|
310
|
+
chunkSnapshotTree,
|
|
311
|
+
chunkSubTreeSegments,
|
|
312
|
+
brand(chunkReferenceIdNumber),
|
|
313
|
+
);
|
|
279
314
|
}
|
|
280
315
|
};
|
|
281
|
-
|
|
316
|
+
// parentReferenceId is undefined for the root of the forest tree.
|
|
317
|
+
await downloadChunkContentsInTree(forestTree, [], undefined /* parentReferenceId */);
|
|
282
318
|
}
|
|
283
319
|
|
|
284
320
|
/**
|
|
@@ -319,6 +355,19 @@ export class ForestIncrementalSummaryBuilder implements IncrementalEncoderDecode
|
|
|
319
355
|
}
|
|
320
356
|
|
|
321
357
|
this.latestSummarySequenceNumber = incrementalSummaryContext.latestSummarySequenceNumber;
|
|
358
|
+
|
|
359
|
+
// Build a reverse lookup map (referenceId → properties) for the latest summary so that
|
|
360
|
+
// computeHandlePathInLatestSummary can traverse the parent chain without iterating the whole map.
|
|
361
|
+
const latestSummaryRefIdMap: Map<ChunkReferenceId, ChunkSummaryProperties> = new Map();
|
|
362
|
+
const latestTracking = this.chunkTrackingPropertiesMap.get(
|
|
363
|
+
this.latestSummarySequenceNumber,
|
|
364
|
+
);
|
|
365
|
+
if (latestTracking !== undefined) {
|
|
366
|
+
for (const properties of latestTracking.values()) {
|
|
367
|
+
latestSummaryRefIdMap.set(properties.referenceId, properties);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
322
371
|
this.trackedSummaryProperties = {
|
|
323
372
|
summarySequenceNumber: incrementalSummaryContext.summarySequenceNumber,
|
|
324
373
|
latestSummaryBasePath: incrementalSummaryContext.summaryPath,
|
|
@@ -326,10 +375,39 @@ export class ForestIncrementalSummaryBuilder implements IncrementalEncoderDecode
|
|
|
326
375
|
parentSummaryBuilder: builder,
|
|
327
376
|
fullTree,
|
|
328
377
|
stringify,
|
|
378
|
+
latestSummaryRefIdMap,
|
|
329
379
|
};
|
|
330
380
|
return ForestIncrementalSummaryBehavior.Incremental;
|
|
331
381
|
}
|
|
332
382
|
|
|
383
|
+
/**
|
|
384
|
+
* Computes a chunk's path in the latest summary by traversing up the parent chain via
|
|
385
|
+
* {@link latestSummaryRefIdMap}.
|
|
386
|
+
*
|
|
387
|
+
* Each {@link ChunkSummaryProperties.parentReferenceId} points to the chunk's parent as it
|
|
388
|
+
* appeared in the summary where the entry was last written. Walking up the chain from the
|
|
389
|
+
* chunk to the root produces the full path that can be used in a summary handle path.
|
|
390
|
+
*/
|
|
391
|
+
private computeHandlePathInLatestSummary(chunkProperties: ChunkSummaryProperties): string {
|
|
392
|
+
const { latestSummaryRefIdMap } = this.requireTrackingSummary();
|
|
393
|
+
const pathSegments: string[] = [];
|
|
394
|
+
let current: ChunkSummaryProperties | undefined = chunkProperties;
|
|
395
|
+
while (current !== undefined) {
|
|
396
|
+
pathSegments.push(`${current.referenceId}`);
|
|
397
|
+
if (current.parentReferenceId === undefined) {
|
|
398
|
+
break;
|
|
399
|
+
}
|
|
400
|
+
current = latestSummaryRefIdMap.get(current.parentReferenceId);
|
|
401
|
+
assert(
|
|
402
|
+
current !== undefined,
|
|
403
|
+
0xcf7 /* Parent chunk not found in latest summary tracking */,
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
// Segments are collected leaf-to-root and then reversed. The alternative would be to use unshift
|
|
407
|
+
// instead of push and reverse. However, using push and reverse is O(n) whereas using unshift would be O(n²).
|
|
408
|
+
return pathSegments.reverse().join("/");
|
|
409
|
+
}
|
|
410
|
+
|
|
333
411
|
/**
|
|
334
412
|
* {@link IncrementalEncoder.encodeIncrementalField}
|
|
335
413
|
* @remarks Returns an empty array if the field has no content.
|
|
@@ -344,8 +422,6 @@ export class ForestIncrementalSummaryBuilder implements IncrementalEncoderDecode
|
|
|
344
422
|
const chunkReferenceIds: ChunkReferenceId[] = [];
|
|
345
423
|
const chunks = this.getChunkAtCursor(cursor);
|
|
346
424
|
for (const chunk of chunks) {
|
|
347
|
-
let chunkProperties: ChunkSummaryProperties;
|
|
348
|
-
|
|
349
425
|
// Try and get the properties of the chunk from the latest successful summary.
|
|
350
426
|
// If it exists and the summary is not a full tree, use the properties to generate a summary handle.
|
|
351
427
|
// If it does not exist, encode the chunk and generate new properties for it.
|
|
@@ -354,28 +430,29 @@ export class ForestIncrementalSummaryBuilder implements IncrementalEncoderDecode
|
|
|
354
430
|
this.latestSummarySequenceNumber,
|
|
355
431
|
chunk,
|
|
356
432
|
);
|
|
433
|
+
let chunkReferenceId: ChunkReferenceId;
|
|
357
434
|
if (previousChunkProperties !== undefined && !trackedSummaryProperties.fullTree) {
|
|
358
|
-
|
|
435
|
+
chunkReferenceId = previousChunkProperties.referenceId;
|
|
436
|
+
// Compute this chunk's path in the latest summary by traversing the parent chain.
|
|
437
|
+
// Using parentReferenceId traversal (rather than a stored path string) ensures the
|
|
438
|
+
// path is correct even when an ancestor was re-encoded in a prior summary and
|
|
439
|
+
// received a new referenceId — the stored summaryPath would have been stale in
|
|
440
|
+
// that case.
|
|
441
|
+
const handlePath = this.computeHandlePathInLatestSummary(previousChunkProperties);
|
|
359
442
|
trackedSummaryProperties.parentSummaryBuilder.addHandle(
|
|
360
|
-
`${
|
|
443
|
+
`${chunkReferenceId}`,
|
|
361
444
|
SummaryType.Tree,
|
|
362
|
-
`${trackedSummaryProperties.latestSummaryBasePath}/${
|
|
445
|
+
`${trackedSummaryProperties.latestSummaryBasePath}/${handlePath}`,
|
|
363
446
|
);
|
|
364
447
|
} else {
|
|
365
448
|
// Generate a new reference ID for the chunk.
|
|
366
449
|
const newReferenceId: ChunkReferenceId = brand(this.nextReferenceId++);
|
|
450
|
+
chunkReferenceId = newReferenceId;
|
|
367
451
|
|
|
368
|
-
// Add the reference ID of this chunk to the chunk summary path
|
|
369
|
-
//
|
|
370
|
-
// This is done before encoding the chunk so that the summary path is updated correctly when encoding
|
|
371
|
-
// any incremental chunks that are under this chunk.
|
|
452
|
+
// Add the reference ID of this chunk to the chunk summary path before encoding so
|
|
453
|
+
// that any incremental chunks in the subtree use the correct parent path.
|
|
372
454
|
trackedSummaryProperties.chunkSummaryPath.push(newReferenceId);
|
|
373
455
|
|
|
374
|
-
chunkProperties = {
|
|
375
|
-
referenceId: newReferenceId,
|
|
376
|
-
summaryPath: trackedSummaryProperties.chunkSummaryPath.join("/"),
|
|
377
|
-
};
|
|
378
|
-
|
|
379
456
|
const parentSummaryBuilder = trackedSummaryProperties.parentSummaryBuilder;
|
|
380
457
|
// Create a new summary builder for this chunk to build its summary tree which will be stored in the
|
|
381
458
|
// parent's summary tree under its reference ID.
|
|
@@ -400,13 +477,21 @@ export class ForestIncrementalSummaryBuilder implements IncrementalEncoderDecode
|
|
|
400
477
|
trackedSummaryProperties.chunkSummaryPath.pop();
|
|
401
478
|
}
|
|
402
479
|
|
|
480
|
+
// Get the parent reference ID from the current chunk summary path.
|
|
481
|
+
// For the root of the forest tree, the parent reference ID is undefined.
|
|
482
|
+
// For all other chunks, the parent reference ID is the last element in the current chunk summary path.
|
|
483
|
+
const chunkSummaryPathLength = trackedSummaryProperties.chunkSummaryPath.length;
|
|
484
|
+
const parentReferenceId: ChunkReferenceId | undefined =
|
|
485
|
+
chunkSummaryPathLength > 0
|
|
486
|
+
? trackedSummaryProperties.chunkSummaryPath[chunkSummaryPathLength - 1]
|
|
487
|
+
: undefined;
|
|
403
488
|
setInNestedMap(
|
|
404
489
|
this.chunkTrackingPropertiesMap,
|
|
405
490
|
trackedSummaryProperties.summarySequenceNumber,
|
|
406
491
|
chunk,
|
|
407
|
-
|
|
492
|
+
{ referenceId: chunkReferenceId, parentReferenceId },
|
|
408
493
|
);
|
|
409
|
-
chunkReferenceIds.push(
|
|
494
|
+
chunkReferenceIds.push(chunkReferenceId);
|
|
410
495
|
}
|
|
411
496
|
return chunkReferenceIds;
|
|
412
497
|
}
|
|
@@ -480,9 +565,9 @@ export class ForestIncrementalSummaryBuilder implements IncrementalEncoderDecode
|
|
|
480
565
|
referenceId: ChunkReferenceId,
|
|
481
566
|
chunkDecoder: (encoded: EncodedFieldBatchV2) => TreeChunk,
|
|
482
567
|
): TreeChunk {
|
|
483
|
-
const
|
|
484
|
-
assert(
|
|
485
|
-
const chunk = chunkDecoder(
|
|
568
|
+
const chunkLoadProperties = this.loadedChunksMap.get(`${referenceId}`);
|
|
569
|
+
assert(chunkLoadProperties !== undefined, 0xc86 /* Encoded incremental chunk not found */);
|
|
570
|
+
const chunk = chunkDecoder(chunkLoadProperties.encodedContents);
|
|
486
571
|
|
|
487
572
|
// Account for the reference about to be added in `chunkTrackingPropertiesMap`
|
|
488
573
|
// to ensure that no other users of this chunk think they have unique ownership.
|
|
@@ -493,7 +578,7 @@ export class ForestIncrementalSummaryBuilder implements IncrementalEncoderDecode
|
|
|
493
578
|
// when a new client starts to summarize.
|
|
494
579
|
setInNestedMap(this.chunkTrackingPropertiesMap, this.initialSequenceNumber, chunk, {
|
|
495
580
|
referenceId,
|
|
496
|
-
|
|
581
|
+
parentReferenceId: chunkLoadProperties.parentReferenceId,
|
|
497
582
|
});
|
|
498
583
|
return chunk;
|
|
499
584
|
}
|
package/src/packageVersion.ts
CHANGED