Skip to content

Commit 21c4245

Browse files
Add createIndependentTree (microsoft#25785)
## Description IndependentView had some limitations, like being very difficult to use for schema evolution and out of schema handling tests. This new IndependentTree API provides a more general alternative.
1 parent 8ed4f25 commit 21c4245

File tree

12 files changed

+499
-91
lines changed

12 files changed

+499
-91
lines changed

.changeset/some-geese-mate.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
---
2+
"fluid-framework": minor
3+
"@fluidframework/tree": minor
4+
"__section": tree
5+
---
6+
Add IndependentTree API
7+
8+
New `IndependentTreeAlpha` and `IndependentTreeBeta` APIs provide similar utility to the existing alpha [`IndependentView`](https://fluidframework.com/docs/api/tree#independentview-function) API, except providing access to the [`ViewableTree`](https://fluidframework.com/docs/api/fluid-framework/viewabletree-interface).
9+
10+
This allows for multiple views (in sequence, not concurrently) to be created to test things like schema upgrades and incompatible view schema much more easily (see example below).
11+
For `IndependentTreeAlpha`, this also provides access to `exportVerbose` and `exportSimpleSchema` from [`ITreeAlpha`](https://fluidframework.com/docs/api/tree/itreealpha-interface).
12+
13+
An example of how to use `createIndependentTreeBeta` to create multiple views to test a schema upgrade:
14+
```typescript
15+
const tree = createIndependentTreeBeta();
16+
17+
const stagedConfig = new TreeViewConfiguration({
18+
schema: SchemaFactoryAlpha.types([
19+
SchemaFactory.number,
20+
SchemaFactoryAlpha.staged(SchemaFactory.string),
21+
]),
22+
});
23+
const afterConfig = new TreeViewConfigurationAlpha({
24+
schema: [SchemaFactory.number, SchemaFactory.string],
25+
});
26+
27+
// Initialize tree
28+
{
29+
const view = tree.viewWith(stagedConfig);
30+
view.initialize(1);
31+
view.dispose();
32+
}
33+
34+
// Do schema upgrade
35+
{
36+
const view = tree.viewWith(afterConfig);
37+
view.upgradeSchema();
38+
view.root = "A";
39+
view.dispose();
40+
}
41+
42+
// Can still view tree with staged schema
43+
{
44+
const view = tree.viewWith(stagedConfig);
45+
assert.equal(view.root, "A");
46+
view.dispose();
47+
}
48+
```

packages/dds/tree/api-report/tree.alpha.api.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,20 @@ export const contentSchemaSymbol: unique symbol;
172172
// @alpha
173173
export function createIdentifierIndex<TSchema extends ImplicitFieldSchema>(view: TreeView<TSchema>): IdentifierIndex;
174174

175+
// @alpha
176+
export function createIndependentTreeAlpha<const TSchema extends ImplicitFieldSchema>(options?: ForestOptions & (({
177+
idCompressor?: IIdCompressor | undefined;
178+
} & {
179+
content?: undefined;
180+
}) | (ICodecOptions & {
181+
content: ViewContent;
182+
} & {
183+
idCompressor?: undefined;
184+
}))): ViewableTree & Pick<ITreeAlpha, "exportVerbose" | "exportSimpleSchema">;
185+
186+
// @beta
187+
export function createIndependentTreeBeta<const TSchema extends ImplicitFieldSchema>(options?: ForestOptions): ViewableTree;
188+
175189
// @alpha
176190
export function createSimpleTreeIndex<TFieldSchema extends ImplicitFieldSchema, TKey extends TreeIndexKey, TValue>(view: TreeView<TFieldSchema>, indexer: (schema: TreeNodeSchema) => string | undefined, getValue: (nodes: TreeIndexNodes<TreeNode>) => TValue, isKeyValid: (key: TreeIndexKey) => key is TKey): SimpleTreeIndex<TKey, TValue>;
177191

packages/dds/tree/api-report/tree.beta.api.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,9 @@ export type ConciseTree<THandle = IFluidHandle> = Exclude<TreeLeafValue, IFluidH
9494
// @beta
9595
export function configuredSharedTreeBeta(options: SharedTreeOptionsBeta): SharedObjectKind<ITree>;
9696

97+
// @beta
98+
export function createIndependentTreeBeta<const TSchema extends ImplicitFieldSchema>(options?: ForestOptions): ViewableTree;
99+
97100
// @public @sealed @system
98101
interface DefaultProvider extends ErasedType<"@fluidframework/tree.FieldProvider"> {
99102
}

packages/dds/tree/api-report/tree.legacy.beta.api.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@ export function configuredSharedTreeBeta(options: SharedTreeOptionsBeta): Shared
9797
// @beta @legacy
9898
export function configuredSharedTreeBetaLegacy(options: SharedTreeOptionsBeta): ISharedObjectKind<ITree> & SharedObjectKind<ITree>;
9999

100+
// @beta
101+
export function createIndependentTreeBeta<const TSchema extends ImplicitFieldSchema>(options?: ForestOptions): ViewableTree;
102+
100103
// @public @sealed @system
101104
interface DefaultProvider extends ErasedType<"@fluidframework/tree.FieldProvider"> {
102105
}

packages/dds/tree/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ export {
7373
type ObservationResults,
7474
type TreeIdentifierUtils,
7575
independentView,
76+
createIndependentTreeBeta,
77+
createIndependentTreeAlpha,
7678
ForestTypeOptimized,
7779
ForestTypeExpensiveDebug,
7880
ForestTypeReference,

packages/dds/tree/src/shared-tree/independentView.ts

Lines changed: 147 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,9 @@ import {
1212

1313
import type { ICodecOptions } from "../codec/index.js";
1414
import {
15-
type ITreeCursorSynchronous,
1615
type RevisionTag,
1716
RevisionTagCodec,
1817
SchemaVersion,
19-
type TreeStoredSchema,
2018
TreeStoredSchemaRepository,
2119
} from "../core/index.js";
2220
import {
@@ -34,11 +32,23 @@ import type {
3432
TreeViewConfiguration,
3533
ImplicitFieldSchema,
3634
TreeViewAlpha,
35+
ITreeAlpha,
36+
ViewableTree,
37+
TreeView,
38+
ReadSchema,
39+
VerboseTree,
40+
SimpleTreeSchema,
3741
} from "../simple-tree/index.js";
38-
import { type JsonCompatibleReadOnly, type JsonCompatible, Breakable } from "../util/index.js";
42+
import {
43+
type JsonCompatibleReadOnly,
44+
type JsonCompatible,
45+
Breakable,
46+
oneFromIterable,
47+
} from "../util/index.js";
3948
import {
4049
buildConfiguredForest,
4150
defaultSharedTreeOptions,
51+
exportSimpleSchema,
4252
type ForestOptions,
4353
} from "./sharedTree.js";
4454
import { createTreeCheckout } from "./treeCheckout.js";
@@ -59,30 +69,9 @@ export function independentView<const TSchema extends ImplicitFieldSchema>(
5969
config: TreeViewConfiguration<TSchema>,
6070
options: ForestOptions & { idCompressor?: IIdCompressor | undefined },
6171
): TreeViewAlpha<TSchema> {
62-
const breaker = new Breakable("independentView");
63-
const idCompressor: IIdCompressor = options.idCompressor ?? createIdCompressor();
64-
const mintRevisionTag = (): RevisionTag => idCompressor.generateCompressedId();
65-
const revisionTagCodec = new RevisionTagCodec(idCompressor);
66-
const schema = new TreeStoredSchemaRepository();
67-
const forest = buildConfiguredForest(
68-
breaker,
69-
options.forest ?? defaultSharedTreeOptions.forest,
70-
schema,
71-
idCompressor,
72-
defaultIncrementalEncodingPolicy,
73-
);
74-
const checkout = createTreeCheckout(idCompressor, mintRevisionTag, revisionTagCodec, {
75-
forest,
76-
schema,
77-
breaker,
78-
});
79-
const out: TreeViewAlpha<TSchema> = new SchematizingSimpleTreeView<TSchema>(
80-
checkout,
81-
config,
82-
createNodeIdentifierManager(idCompressor),
83-
);
84-
return out;
72+
return createIndependentTreeAlpha(options).viewWith(config) as TreeViewAlpha<TSchema>;
8573
}
74+
8675
/**
8776
* Create an initialized {@link TreeView} that is not tied to any {@link ITree} instance.
8877
*
@@ -99,52 +88,101 @@ export function independentInitializedView<const TSchema extends ImplicitFieldSc
9988
options: ForestOptions & ICodecOptions,
10089
content: ViewContent,
10190
): TreeViewAlpha<TSchema> {
102-
const idCompressor: IIdCompressor = content.idCompressor;
103-
const fieldBatchCodec = makeFieldBatchCodec(options, 1);
104-
const schemaCodec = makeSchemaCodec(options, SchemaVersion.v1);
105-
106-
const schema = schemaCodec.decode(content.schema as Format);
107-
const context: FieldBatchEncodingContext = {
108-
encodeType: TreeCompressionStrategy.Compressed,
109-
idCompressor,
110-
originatorId: idCompressor.localSessionId, // Is this right? If so, why is is needed?
111-
schema: { schema, policy: defaultSchemaPolicy },
112-
};
113-
114-
const fieldCursors = fieldBatchCodec.decode(content.tree as JsonCompatibleReadOnly, context);
115-
assert(fieldCursors.length === 1, 0xa5b /* must have exactly 1 field in batch */);
116-
117-
return independentInitializedViewInternal(
91+
return createIndependentTreeAlpha({ ...options, content }).viewWith(
11892
config,
119-
options,
120-
schema,
121-
// Checked above.
122-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
123-
fieldCursors[0]!,
124-
idCompressor,
125-
);
93+
) as TreeViewAlpha<TSchema>;
12694
}
12795

12896
/**
129-
* {@link independentInitializedView} but using internal types instead of persisted data formats.
97+
* Create a {@link ViewableTree} that is not tied to any Fluid runtimes or services.
98+
*
99+
* @remarks
100+
* Such a tree can never experience collaboration or be persisted to to a Fluid Container.
101+
*
102+
* This can be useful for testing, as well as use-cases like working on local files instead of documents stored in some Fluid service.
103+
*
104+
* @example
105+
* ```typescript
106+
* const tree = createIndependentTreeBeta();
107+
*
108+
* const stagedConfig = new TreeViewConfiguration({
109+
* schema: SchemaFactoryAlpha.types([
110+
* SchemaFactory.number,
111+
* SchemaFactoryAlpha.staged(SchemaFactory.string),
112+
* ]),
113+
* });
114+
* const afterConfig = new TreeViewConfigurationAlpha({
115+
* schema: [SchemaFactory.number, SchemaFactory.string],
116+
* });
117+
*
118+
* // Initialize tree
119+
* {
120+
* const view = tree.viewWith(stagedConfig);
121+
* view.initialize(1);
122+
* view.dispose();
123+
* }
124+
*
125+
* // Do schema upgrade
126+
* {
127+
* const view = tree.viewWith(afterConfig);
128+
* view.upgradeSchema();
129+
* view.root = "A";
130+
* view.dispose();
131+
* }
132+
*
133+
* // Can still view tree with staged schema
134+
* {
135+
* const view = tree.viewWith(stagedConfig);
136+
* assert.equal(view.root, "A");
137+
* view.dispose();
138+
* }
139+
* ```
140+
* @privateRemarks
141+
* Before stabilizing this as public, consider if we can instead just expose a better way to create regular Fluid service based SharedTrees for tests.
142+
* Something like https://github.com/microsoft/FluidFramework/pull/25422 might be a better long term stable/public solution.
143+
* @beta
130144
*/
131-
export function independentInitializedViewInternal<const TSchema extends ImplicitFieldSchema>(
132-
config: TreeViewConfiguration<TSchema>,
133-
options: ForestOptions & ICodecOptions,
134-
schema: TreeStoredSchema,
135-
rootFieldCursor: ITreeCursorSynchronous,
136-
idCompressor: IIdCompressor,
137-
): SchematizingSimpleTreeView<TSchema> {
138-
const breaker = new Breakable("independentInitializedView");
139-
const revisionTagCodec = new RevisionTagCodec(idCompressor);
145+
export function createIndependentTreeBeta<const TSchema extends ImplicitFieldSchema>(
146+
options?: ForestOptions,
147+
): ViewableTree {
148+
return createIndependentTreeAlpha<TSchema>(options);
149+
}
150+
151+
/**
152+
* Alpha extensions to {@link createIndependentTreeBeta}.
153+
*
154+
* @param options - Configuration options for the independent tree.
155+
* This can be used to create an uninitialized tree, or `content` can be provided to create an initialized tree.
156+
* If content is provided, the idCompressor is a required part of it: otherwise it is optional and provided at the top level.
157+
*
158+
* @privateRemarks
159+
* TODO: Support more of {@link ITreeAlpha}, including branching APIs to allow for merges.
160+
* TODO: Better unify this logic with SharedTreeKernel and SharedTreeCore.
161+
*
162+
* Before further stabilizing: consider better ways to handle initialized vs uninitialized trees.
163+
* Perhaps it would be better to not allow initialize here at all, but expose the ability to load compressed tree content and stored schema via ITree or TreeView?
164+
* If keeping the option here, maybe a separate function of overload would be better? Or maybe flatten ViewContent inline to deduplicate the idCompressor options?
165+
* @alpha
166+
*/
167+
export function createIndependentTreeAlpha<const TSchema extends ImplicitFieldSchema>(
168+
options?: ForestOptions &
169+
(
170+
| ({ idCompressor?: IIdCompressor | undefined } & { content?: undefined })
171+
| (ICodecOptions & { content: ViewContent } & { idCompressor?: undefined })
172+
),
173+
): ViewableTree & Pick<ITreeAlpha, "exportVerbose" | "exportSimpleSchema"> {
174+
const breaker = new Breakable("independentView");
175+
const idCompressor: IIdCompressor =
176+
options?.idCompressor ?? options?.content?.idCompressor ?? createIdCompressor();
140177
const mintRevisionTag = (): RevisionTag => idCompressor.generateCompressedId();
178+
const revisionTagCodec = new RevisionTagCodec(idCompressor);
141179

142180
// To ensure the forest is in schema when constructed, start it with an empty schema and set the schema repository content later.
143181
const schemaRepository = new TreeStoredSchemaRepository();
144182

145183
const forest = buildConfiguredForest(
146184
breaker,
147-
options.forest ?? defaultSharedTreeOptions.forest,
185+
options?.forest ?? defaultSharedTreeOptions.forest,
148186
schemaRepository,
149187
idCompressor,
150188
defaultIncrementalEncodingPolicy,
@@ -156,18 +194,55 @@ export function independentInitializedViewInternal<const TSchema extends Implici
156194
breaker,
157195
});
158196

159-
initialize(
160-
checkout,
161-
schema,
162-
initializerFromChunk(checkout, () =>
163-
combineChunks(checkout.forest.chunkField(rootFieldCursor)),
164-
),
165-
);
166-
return new SchematizingSimpleTreeView<TSchema>(
167-
checkout,
168-
config,
169-
createNodeIdentifierManager(idCompressor),
170-
);
197+
if (options?.content !== undefined) {
198+
const schemaCodec = makeSchemaCodec(options, SchemaVersion.v1);
199+
const fieldBatchCodec = makeFieldBatchCodec(options, 1);
200+
const newSchema = schemaCodec.decode(options.content.schema as Format);
201+
202+
const context: FieldBatchEncodingContext = {
203+
encodeType: TreeCompressionStrategy.Compressed,
204+
idCompressor,
205+
originatorId: idCompressor.localSessionId, // Is this right? If so, why is is needed?
206+
schema: { schema: newSchema, policy: defaultSchemaPolicy },
207+
};
208+
const fieldCursors = fieldBatchCodec.decode(
209+
options.content.tree as JsonCompatibleReadOnly,
210+
context,
211+
);
212+
assert(fieldCursors.length === 1, 0xa5b /* must have exactly 1 field in batch */);
213+
214+
const fieldCursor = oneFromIterable(fieldCursors);
215+
assert(fieldCursor !== undefined, "expected exactly one field in batch");
216+
217+
initialize(
218+
checkout,
219+
newSchema,
220+
initializerFromChunk(checkout, () =>
221+
combineChunks(checkout.forest.chunkField(fieldCursor)),
222+
),
223+
);
224+
}
225+
226+
return {
227+
viewWith<TRoot extends ImplicitFieldSchema>(
228+
config: TreeViewConfiguration<TRoot>,
229+
): TreeView<TRoot> {
230+
const out: TreeViewAlpha<TSchema> = new SchematizingSimpleTreeView<TSchema>(
231+
checkout,
232+
config as TreeViewConfiguration as TreeViewConfiguration<ReadSchema<TSchema>>,
233+
createNodeIdentifierManager(idCompressor),
234+
);
235+
return out as unknown as TreeView<TRoot>;
236+
},
237+
238+
exportVerbose(): VerboseTree | undefined {
239+
return checkout.exportVerbose();
240+
},
241+
242+
exportSimpleSchema(): SimpleTreeSchema {
243+
return exportSimpleSchema(checkout.storedSchema);
244+
},
245+
};
171246
}
172247

173248
/**

packages/dds/tree/src/shared-tree/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ export {
3939

4040
export { SchematizingSimpleTreeView } from "./schematizingTreeView.js";
4141

42+
export { initialize, initializerFromChunk } from "./schematizeTree.js";
43+
4244
export type {
4345
ISharedTreeEditor,
4446
ISchemaEditor,
@@ -58,6 +60,8 @@ export {
5860
independentInitializedView,
5961
type ViewContent,
6062
independentView,
63+
createIndependentTreeBeta,
64+
createIndependentTreeAlpha,
6165
} from "./independentView.js";
6266

6367
export type { SharedTreeChange } from "./sharedTreeChangeTypes.js";

0 commit comments

Comments
 (0)