tree: Import / Export APIs and demo (#22566)

## Description

Added import/export options for tree content and schema, and example
script using them.
This commit is contained in:
Craig Macomber (Microsoft) 2024-11-01 14:15:10 -07:00 коммит произвёл GitHub
Родитель ebfbfaa126
Коммит 18a23e8816
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
43 изменённых файлов: 1781 добавлений и 108 удалений

Просмотреть файл

@ -0,0 +1,25 @@
---
"fluid-framework": minor
"@fluidframework/tree": minor
---
---
"section": tree
---
New Alpha APIs for tree data import and export
A collection of new `@alpha` APIs for importing and exporting tree content and schema from SharedTrees has been added to `TreeAlpha`.
These include import and export APIs for `VerboseTree`, `ConciseTree` and compressed tree formats.
`TreeAlpha.create` is also added to allow constructing trees with a more general API instead of having to use the schema constructor directly (since that doesn't handle polymorphic roots, or non-schema aware code).
The function `independentInitializedView` has been added to provide a way to combine data from the existing `extractPersistedSchema` and new `TreeAlpha.exportCompressed` back into a `TreeView` in a way which can support safely importing data which could have been exported with a different schema.
This allows replicating the schema evolution process for Fluid documents stored in a service, but entirely locally without involving any collaboration services.
`independentView` has also been added, which is similar but handles the case of creating a new view without an existing schema or tree.
Together these APIs address several use-cases:
1. Using SharedTree as an in-memory non-collaborative datastore.
2. Importing and exporting data from a SharedTree to and from other services or storage locations (such as locally saved files).
3. Testing various scenarios without relying on a service.
4. Using SharedTree libraries for just the schema system and encode/decode support.

Просмотреть файл

@ -58,6 +58,8 @@
"packages/tools/fluid-runner/src/test/localOdspSnapshots/**",
"packages/tools/fluid-runner/src/test/telemetryExpectedOutputs/**",
"tools/api-markdown-documenter/src/test/snapshots/**",
// TODO: why does examples/apps/tree-cli-app/*.json not work?
"**/data/*.json",
// Generated type-tests
"**/*.generated.ts",

Просмотреть файл

@ -0,0 +1,11 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/
module.exports = {
extends: [require.resolve("@fluidframework/eslint-config-fluid/strict"), "prettier"],
parserOptions: {
project: ["./tsconfig.json", "./src/test/tsconfig.json"],
},
};

Просмотреть файл

@ -0,0 +1,14 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/
"use strict";
const getFluidTestMochaConfig = require("@fluid-internal/mocha-test-setup/mocharc-common");
const packageDir = __dirname;
const config = getFluidTestMochaConfig(packageDir);
config.spec = "lib/test";
module.exports = config;

Просмотреть файл

@ -0,0 +1,7 @@
# @fluid-example/tree-cli-app
Example application using Shared-Tree to create a non-collaborative file editing CLI application.
Note that it's perfectly possible to write a collaborative online CLI app using tree as well: this simply is not an example of that.
Run the app with `pnpm run app` after building.

Просмотреть файл

@ -0,0 +1,4 @@
{
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
"extends": ["../../../biome.jsonc"]
}

Просмотреть файл

@ -0,0 +1 @@
{"version":1,"identifiers":[],"shapes":[{"c":{"type":"com.fluidframework.leaf.number","value":true}},{"c":{"type":"com.fluidframework.example.cli.List","value":false,"fields":[["",5]]}},{"c":{"type":"com.fluidframework.example.cli.Item","value":false,"fields":[["location",3],["name",4]]}},{"c":{"type":"com.fluidframework.example.cli.Point","value":false,"fields":[["x",0],["y",0]]}},{"c":{"type":"com.fluidframework.leaf.string","value":true}},{"a":6},{"d":0}],"data":[[1,[2,0,0,"default"]]]}

Просмотреть файл

@ -0,0 +1 @@
[{"location":{"x":0,"y":0},"name":"default"}]

Просмотреть файл

@ -0,0 +1 @@
[{"position":{"x":0,"y":0},"name":"default"}]

Просмотреть файл

@ -0,0 +1 @@
{"tree":{"version":1,"identifiers":[],"shapes":[{"c":{"type":"com.fluidframework.leaf.number","value":true}},{"c":{"type":"com.fluidframework.example.cli.List","value":false,"fields":[["",5]]}},{"c":{"type":"com.fluidframework.example.cli.Item","value":false,"fields":[["location",3],["name",4]]}},{"c":{"type":"com.fluidframework.example.cli.Point","value":false,"fields":[["x",0],["y",0]]}},{"c":{"type":"com.fluidframework.leaf.string","value":true}},{"a":6},{"d":0}],"data":[[1,[2,0,0,"default"]]]},"schema":{"version":1,"nodes":{"com.fluidframework.example.cli.Item":{"object":{"location":{"kind":"Value","types":["com.fluidframework.example.cli.Point"]},"name":{"kind":"Value","types":["com.fluidframework.leaf.string"]}}},"com.fluidframework.example.cli.List":{"object":{"":{"kind":"Sequence","types":["com.fluidframework.example.cli.Item","com.fluidframework.leaf.string"]}}},"com.fluidframework.example.cli.Point":{"object":{"x":{"kind":"Value","types":["com.fluidframework.leaf.number"]},"y":{"kind":"Value","types":["com.fluidframework.leaf.number"]}}},"com.fluidframework.leaf.number":{"leaf":0},"com.fluidframework.leaf.string":{"leaf":1}},"root":{"kind":"Value","types":["com.fluidframework.example.cli.List"]}},"idCompressor":"AAAAAAAAAEAAAAAAAADwPwAAAAAAAPA/AAAAAAAAAAAnHwqpeTHgaoAxFokW+gsBAAAAAAAAAAAAAAAAAADwPwAAAAAAAAAA"}

Просмотреть файл

@ -0,0 +1 @@
{"type":"com.fluidframework.example.cli.List","fields":[{"type":"com.fluidframework.example.cli.Item","fields":{"location":{"type":"com.fluidframework.example.cli.Point","fields":{"x":0,"y":0}},"name":"default"}}]}

Просмотреть файл

@ -0,0 +1 @@
{"type":"com.fluidframework.example.cli.List","fields":[{"type":"com.fluidframework.example.cli.Item","fields":{"position":{"type":"com.fluidframework.example.cli.Point","fields":{"x":0,"y":0}},"name":"default"}}]}

Просмотреть файл

@ -0,0 +1,58 @@
{
"name": "@fluid-example/tree-cli-app",
"version": "2.5.0",
"private": true,
"description": "SharedTree CLI app demo",
"homepage": "https://fluidframework.com",
"repository": {
"type": "git",
"url": "https://github.com/microsoft/FluidFramework.git",
"directory": "examples/apps/tree-cli-app"
},
"license": "MIT",
"author": "Microsoft and contributors",
"type": "module",
"scripts": {
"app": "node ./lib/index.js",
"build": "fluid-build . --task build",
"build:compile": "fluid-build . --task compile",
"build:esnext": "tsc --project ./tsconfig.json",
"build:test": "npm run build:test:esm",
"build:test:esm": "tsc --project ./src/test/tsconfig.json",
"check:biome": "biome check .",
"check:format": "npm run check:biome",
"clean": "rimraf --glob dist lib \"**/*.tsbuildinfo\" \"**/*.build.log\" nyc",
"eslint": "eslint --format stylish src",
"eslint:fix": "eslint --format stylish src --fix --fix-type problem,suggestion,layout",
"format": "npm run format:biome",
"format:biome": "biome check . --write",
"lint": "fluid-build . --task lint",
"lint:fix": "fluid-build . --task eslint:fix --task format",
"test": "npm run test:mocha",
"test:mocha": "npm run test:mocha:esm",
"test:mocha:esm": "mocha",
"test:mocha:verbose": "cross-env FLUID_TEST_VERBOSE=1 npm run test:mocha"
},
"dependencies": {
"@fluidframework/core-interfaces": "workspace:~",
"@fluidframework/id-compressor": "workspace:~",
"@fluidframework/runtime-utils": "workspace:~",
"@fluidframework/tree": "workspace:~",
"@sinclair/typebox": "^0.32.29"
},
"devDependencies": {
"@biomejs/biome": "~1.9.3",
"@fluid-internal/mocha-test-setup": "workspace:~",
"@fluidframework/build-tools": "^0.49.0",
"@fluidframework/eslint-config-fluid": "^5.4.0",
"@types/mocha": "^9.1.1",
"@types/node": "^18.19.0",
"cross-env": "^7.0.3",
"eslint": "~8.55.0",
"mocha": "^10.2.0",
"mocha-json-output-reporter": "^2.0.1",
"mocha-multi-reporters": "^1.5.1",
"rimraf": "^4.4.0",
"typescript": "~5.4.5"
}
}

Просмотреть файл

@ -0,0 +1,44 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/
// This is a node powered CLI application, so using node makes sense:
/* eslint-disable unicorn/no-process-exit */
import { applyEdit, loadDocument, saveDocument } from "./utils.js";
const args = process.argv.slice(2);
console.log(`Requires arguments: [<source>] [<destination>] [<edit>]`);
console.log();
console.log(
`Example to load the default tree, insert 10 strings and 100 items, and save the result in the concise format:`,
);
console.log(`default data/large.concise.json string:10,item:100`);
console.log();
console.log(`Example to load data/large.concise.json, and log it to the console:`);
console.log(`data/large.concise.json`);
console.log();
console.log(
`File formats are specified by extension, for example ".verbose.json" uses the "verbose" format.`,
);
console.log(
`See implementation for supported formats and edit syntax: this is just a demo, not a nice app!`,
);
console.log();
console.log(`Running with augments: ${args}`);
if (args.length > 3) {
process.exit(1);
}
const [sourceArg, destinationArg, editArg] = args;
const node = loadDocument(sourceArg);
if (editArg !== undefined) {
applyEdit(editArg, node);
}
saveDocument(destinationArg, node);

Просмотреть файл

@ -0,0 +1,38 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/
import { SchemaFactory, TreeViewConfiguration } from "@fluidframework/tree";
/**
* The SchemaFactory.
*/
export const schemaBuilder = new SchemaFactory("com.fluidframework.example.cli");
class Point extends schemaBuilder.object("Point", {
x: schemaBuilder.number,
y: schemaBuilder.number,
}) {}
/**
* Complex list item.
*/
export class Item extends schemaBuilder.object("Item", {
position: schemaBuilder.required(Point, { key: "location" }),
name: schemaBuilder.string,
}) {}
/**
* List node.
*/
export class List extends schemaBuilder.array("List", [schemaBuilder.string, Item]) {}
/**
* Tree configuration.
*/
export const config = new TreeViewConfiguration({
schema: List,
enableSchemaValidation: true,
preventAmbiguity: true,
});

Просмотреть файл

@ -0,0 +1,144 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/
import { strict as assert } from "node:assert";
import {
comparePersistedSchema,
extractPersistedSchema,
typeboxValidator,
type ForestOptions,
type ICodecOptions,
type JsonCompatible,
// eslint-disable-next-line import/no-internal-modules
} from "@fluidframework/tree/alpha";
import { List } from "../schema.js";
// This file demonstrates how applications can write tests which ensure they maintain compatibility with the schema from previously released versions.
describe("schema", () => {
it("current schema matches latest historical schema", () => {
const current = extractPersistedSchema(List);
// For compatibility with deep equality and simple objects, round trip via JSON to erase prototypes.
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const currentRoundTripped: JsonCompatible = JSON.parse(JSON.stringify(current));
const previous = historicalSchema.at(-1);
assert(previous !== undefined);
// This ensures that historicalSchema's last entry is up to date with the current application code.
// This can catch:
// 1. Forgetting to update historicalSchema when intentionally making schema changes.
// 2. Accidentally changing schema in a way that impacts document compatibility.
assert.deepEqual(currentRoundTripped, previous.schema);
});
it("historical schema can be upgraded to current schema", () => {
const options: ForestOptions & ICodecOptions = { jsonValidator: typeboxValidator };
for (let documentIndex = 0; documentIndex < historicalSchema.length; documentIndex++) {
for (let viewIndex = 0; viewIndex < historicalSchema.length; viewIndex++) {
const compat = comparePersistedSchema(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
historicalSchema[documentIndex]!.schema,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
historicalSchema[viewIndex]!.schema,
options,
false,
);
// We do not expect duplicates in historicalSchema.
assert.equal(compat.isEquivalent, documentIndex === viewIndex);
// Currently collaboration is only allowed between identical versions
assert.equal(compat.canView, documentIndex === viewIndex);
// Older versions should be upgradable to newer versions, but not the reverse.
assert.equal(compat.canUpgrade, documentIndex <= viewIndex);
}
}
});
});
/**
* List of schema from previous versions of this application.
* Storing these as .json files in a folder may make more sense for more complex applications.
*
* The `schema` field is generated by passing the schema to `extractPersistedSchema`.
*/
const historicalSchema: { version: string; schema: JsonCompatible }[] = [
{
version: "1.0",
schema: {
version: 1,
nodes: {
"com.fluidframework.example.cli.List": {
object: {
"": {
kind: "Sequence",
types: ["com.fluidframework.leaf.string"],
},
},
},
"com.fluidframework.leaf.string": {
leaf: 1,
},
},
root: {
kind: "Value",
types: ["com.fluidframework.example.cli.List"],
},
},
},
{
version: "2.0",
schema: {
version: 1,
nodes: {
"com.fluidframework.example.cli.Item": {
object: {
location: {
kind: "Value",
types: ["com.fluidframework.example.cli.Point"],
},
name: {
kind: "Value",
types: ["com.fluidframework.leaf.string"],
},
},
},
"com.fluidframework.example.cli.List": {
object: {
"": {
kind: "Sequence",
types: ["com.fluidframework.example.cli.Item", "com.fluidframework.leaf.string"],
},
},
},
"com.fluidframework.example.cli.Point": {
object: {
x: {
kind: "Value",
types: ["com.fluidframework.leaf.number"],
},
y: {
kind: "Value",
types: ["com.fluidframework.leaf.number"],
},
},
},
"com.fluidframework.leaf.number": {
leaf: 0,
},
"com.fluidframework.leaf.string": {
leaf: 1,
},
},
root: {
kind: "Value",
types: ["com.fluidframework.example.cli.List"],
},
},
},
];

Просмотреть файл

@ -0,0 +1,23 @@
{
"extends": "../../../../../common/build/build-common/tsconfig.test.node16.json",
"compilerOptions": {
"rootDir": "./",
"outDir": "../../lib/test",
"types": ["mocha", "node"],
// Allows writing type checking expression without having to use the results.
"noUnusedLocals": false,
// Allow testing that declarations work properly
"declaration": true,
// Needed to ensure testExport's produce a valid d.ts
"skipLibCheck": false,
// Due to several of our own packages' exports failing to build with "exactOptionalPropertyTypes",
// disable it to prevent that from erroring when combined with "skipLibCheck".
"exactOptionalPropertyTypes": false,
},
"include": ["./**/*"],
"references": [
{
"path": "../..",
},
],
}

Просмотреть файл

@ -0,0 +1,249 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/
// This is a node powered CLI application, so using node makes sense:
/* eslint-disable unicorn/no-process-exit */
/* eslint-disable import/no-nodejs-modules */
import { readFileSync, writeFileSync } from "node:fs";
import type { IFluidHandle } from "@fluidframework/core-interfaces";
import type { SerializedIdCompressorWithOngoingSession } from "@fluidframework/id-compressor/internal";
import {
createIdCompressor,
deserializeIdCompressor,
} from "@fluidframework/id-compressor/internal";
import { isFluidHandle } from "@fluidframework/runtime-utils";
import { TreeArrayNode, type InsertableTypedNode } from "@fluidframework/tree";
import {
extractPersistedSchema,
FluidClientVersion,
independentInitializedView,
typeboxValidator,
type ForestOptions,
type ICodecOptions,
type JsonCompatible,
type VerboseTree,
type ViewContent,
type ConciseTree,
TreeAlpha,
// eslint-disable-next-line import/no-internal-modules
} from "@fluidframework/tree/alpha";
import { type Static, Type } from "@sinclair/typebox";
import type { Item } from "./schema.js";
import { config, List } from "./schema.js";
/**
* Examples showing how to import data in a variety of formats.
*
* @param source - What data to load.
* If "default" or `undefined` data will come from a hard coded small default tree.
* Otherwise assumed to be a file path ending in a file matching `*.FORMAT.json` where format defines how to parse the file.
* See implementation for supported formats and how they are encoded.
*/
export function loadDocument(source: string | undefined): List {
if (source === undefined || source === "default") {
return new List([{ name: "default", position: { x: 0, y: 0 } }]);
}
const parts = source.split(".");
if (parts.length < 3 || parts.at(-1) !== "json") {
console.log(`Invalid source: ${source}`);
process.exit(1);
}
// Data parsed from JSON is safe to consider JsonCompatible.
// If file is invalid JSON, that will throw and is fine for this app.
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const fileData: JsonCompatible = JSON.parse(readFileSync(source).toString());
switch (parts.at(-2)) {
case "concise": {
return TreeAlpha.importConcise(List, fileData as ConciseTree);
}
case "verbose": {
return TreeAlpha.importVerbose(List, fileData as VerboseTree);
}
case "verbose-stored": {
return TreeAlpha.importVerbose(List, fileData as VerboseTree, {
useStoredKeys: true,
});
}
case "compressed": {
return TreeAlpha.importCompressed(List, fileData, { jsonValidator: typeboxValidator });
}
case "snapshot": {
// TODO: This should probably do a validating parse of the data (probably using type box) rather than just casting it.
const combo: File = fileData as File;
const content: ViewContent = {
schema: combo.schema,
tree: combo.tree,
idCompressor: deserializeIdCompressor(combo.idCompressor),
};
const view = independentInitializedView(config, options, content);
return view.root;
}
default: {
console.log(`Invalid source format: ${parts.at(-2)}`);
process.exit(1);
}
}
}
/**
* Examples showing how to export data in a variety of formats.
*
* @param destination - Where to save the data, and in what format.
* If `undefined` data will logged to the console.
* Otherwise see {@link exportContent}.
*/
export function saveDocument(destination: string | undefined, tree: List): void {
if (destination === undefined || destination === "default") {
console.log("Tree Content:");
console.log(tree);
return;
}
const parts = destination.split(".");
if (parts.length < 3 || parts.at(-1) !== "json") {
console.log(`Invalid destination: ${destination}`);
process.exit(1);
}
const fileData: JsonCompatible = exportContent(destination, tree);
console.log(`Writing: ${destination}`);
writeFileSync(destination, JSON.stringify(fileData, rejectHandles));
}
/**
* Examples showing how to export data in a variety of formats.
*
* @param destination - File path used to select the format.
* Assumed to be a file path ending in a file matching `*.FORMAT.json` where format defines how to parse the file.
* See implementation for supported formats and how they are encoded.
*/
export function exportContent(destination: string, tree: List): JsonCompatible {
const parts = destination.split(".");
if (parts.length < 3 || parts.at(-1) !== "json") {
console.log(`Invalid destination: ${destination}`);
process.exit(1);
}
switch (parts.at(-2)) {
case "concise": {
return TreeAlpha.exportConcise(tree) as JsonCompatible;
}
case "verbose": {
return TreeAlpha.exportVerbose(tree) as JsonCompatible;
}
case "concise-stored": {
return TreeAlpha.exportConcise(tree, { useStoredKeys: true }) as JsonCompatible;
}
case "verbose-stored": {
return TreeAlpha.exportVerbose(tree, { useStoredKeys: true }) as JsonCompatible;
}
case "compressed": {
return TreeAlpha.exportCompressed(tree, {
...options,
oldestCompatibleClient: FluidClientVersion.v2_3,
}) as JsonCompatible;
}
case "snapshot": {
// TODO: This should be made better. See privateRemarks on TreeAlpha.exportCompressed.
const idCompressor = createIdCompressor();
const file: File = {
tree: TreeAlpha.exportCompressed(tree, {
oldestCompatibleClient: FluidClientVersion.v2_3,
idCompressor,
}),
schema: extractPersistedSchema(List),
idCompressor: idCompressor.serialize(true),
};
return file as JsonCompatible;
}
default: {
console.log(`Invalid source format: ${parts.at(-2)}`);
process.exit(1);
}
}
}
/**
* Example of editing a tree.
* This allows for some basic editing of `list` sufficient to add and remove items to create trees of different sizes.
*
* This interprets `edits` as a comma separated list of edits to apply to `tree`.
*
* Each edit is in the format `kind:count`.
* Count is a number and indicates how many nodes to add when positive, and how many to remove when negative.
*
* Positive numbers have valid kinds of "string" and "item" to insert that many strings or items to `list`.
* Negative numbers have valid kinds of "start" or "end" to indicate if the items should be removed from the start or end of `list`.
*/
export function applyEdit(edits: string, list: List): void {
for (const edit of edits.split(",")) {
console.log(`Applying edit ${edit}`);
const parts = edit.split(":");
if (parts.length !== 2) {
throw new Error(`Invalid edit ${edit}`);
}
const [kind, countString] = parts;
const count = Number(countString);
if (count === 0 || !Number.isInteger(count)) {
throw new TypeError(`Invalid count in edit ${edit}`);
}
if (count > 0) {
let data: InsertableTypedNode<typeof Item> | string;
switch (kind) {
case "string": {
data = "x";
break;
}
case "item": {
data = { position: { x: 0, y: 0 }, name: "item" };
break;
}
default: {
throw new TypeError(`Invalid kind in insert edit ${edit}`);
}
}
// eslint-disable-next-line unicorn/no-new-array
list.insertAtEnd(TreeArrayNode.spread(new Array(count).fill(data)));
} else {
switch (kind) {
case "start": {
list.removeRange(0, -count);
break;
}
case "end": {
list.removeRange(list.length + count, -count);
break;
}
default: {
throw new TypeError(`Invalid end in remove edit ${edit}`);
}
}
}
}
}
/**
* Throw if handle.
*/
export function rejectHandles(key: string, value: unknown): unknown {
if (isFluidHandle(value)) {
throw new Error("Fluid handles are not supported");
}
return value;
}
const options: ForestOptions & ICodecOptions = { jsonValidator: typeboxValidator };
const File = Type.Object({
tree: Type.Unsafe<JsonCompatible<IFluidHandle>>(),
schema: Type.Unsafe<JsonCompatible>(),
idCompressor: Type.Unsafe<SerializedIdCompressorWithOngoingSession>(),
});
type File = Static<typeof File>;

Просмотреть файл

@ -0,0 +1,12 @@
{
"extends": "../../../common/build/build-common/tsconfig.node16.json",
"include": ["src/**/*"],
"exclude": ["src/test/**/*"],
"compilerOptions": {
"rootDir": "./src",
"outDir": "./lib",
"exactOptionalPropertyTypes": false,
"noUnusedLocals": false,
"types": ["node"],
},
}

Просмотреть файл

@ -49,10 +49,21 @@ export interface CommitMetadata {
// @alpha
export function comparePersistedSchema(persisted: JsonCompatible, view: JsonCompatible, options: ICodecOptions, canInitialize: boolean): SchemaCompatibilityStatus;
// @alpha
export type ConciseTree<THandle = IFluidHandle> = Exclude<TreeLeafValue, IFluidHandle> | THandle | ConciseTree<THandle>[] | {
[key: string]: ConciseTree<THandle>;
};
// @public @sealed
interface DefaultProvider extends ErasedType<"@fluidframework/tree.FieldProvider"> {
}
// @alpha
export interface EncodeOptions<TCustom> {
readonly useStoredKeys?: boolean;
valueConverter(data: IFluidHandle): TCustom;
}
// @alpha
export function enumFromStrings<TScope extends string, const Members extends readonly string[]>(factory: SchemaFactory<TScope>, members: Members): (<TValue extends Members[number]>(value: TValue) => TreeNode & {
readonly value: TValue;
@ -136,6 +147,14 @@ type FlexList<Item = unknown> = readonly LazyItem<Item>[];
// @public
type FlexListToUnion<TList extends FlexList> = ExtractItemType<TList[number]>;
// @alpha
export enum FluidClientVersion {
v2_0 = "v2_0",
v2_1 = "v2_1",
v2_2 = "v2_2",
v2_3 = "v2_3"
}
// @alpha
export interface ForestOptions {
readonly forest?: ForestType;
@ -168,6 +187,14 @@ export type ImplicitAllowedTypes = AllowedTypes | TreeNodeSchema;
// @public
export type ImplicitFieldSchema = FieldSchema | ImplicitAllowedTypes;
// @alpha
export function independentInitializedView<const TSchema extends ImplicitFieldSchema>(config: TreeViewConfiguration<TSchema>, options: ForestOptions & ICodecOptions, content: ViewContent): TreeViewAlpha<TSchema>;
// @alpha
export function independentView<const TSchema extends ImplicitFieldSchema>(config: TreeViewConfiguration<TSchema>, options: ForestOptions & {
idCompressor?: IIdCompressor_2 | undefined;
}): TreeViewAlpha<TSchema>;
// @public
type _InlineTrick = 0;
@ -306,11 +333,11 @@ export interface JsonArrayNodeSchema extends JsonNodeSchemaBase<NodeKind.Array,
}
// @alpha
export type JsonCompatible = string | number | boolean | null | JsonCompatible[] | JsonCompatibleObject;
export type JsonCompatible<TExtra = never> = string | number | boolean | null | JsonCompatible<TExtra>[] | JsonCompatibleObject<TExtra> | TExtra;
// @alpha
export type JsonCompatibleObject = {
[P in string]?: JsonCompatible;
export type JsonCompatibleObject<TExtra = never> = {
[P in string]?: JsonCompatible<TExtra>;
};
// @alpha @sealed
@ -444,11 +471,17 @@ type ObjectFromSchemaRecordUnsafe<T extends Unenforced<RestrictiveStringRecord<I
// @public
export type Off = () => void;
// @alpha
export interface ParseOptions<TCustom> {
readonly useStoredKeys?: boolean;
valueConverter(data: VerboseTree<TCustom>): TreeLeafValue | VerboseTreeNode<TCustom>;
}
// @alpha
export type PopUnion<Union, AsOverloadedFunction = UnionToIntersection<Union extends unknown ? (f: Union) => void : never>> = AsOverloadedFunction extends (a: infer First) => void ? First : never;
// @alpha
export type ReadableField<TSchema extends ImplicitFieldSchema | UnsafeUnknownSchema> = TSchema extends ImplicitFieldSchema ? TreeFieldFromImplicitField<TSchema> : TreeLeafValue | TreeNode;
export type ReadableField<TSchema extends ImplicitFieldSchema | UnsafeUnknownSchema> = TreeFieldFromImplicitField<ReadSchema<TSchema>>;
// @public @sealed
export interface ReadonlyArrayNode<out T = TreeNode | TreeLeafValue> extends ReadonlyArray<T>, Awaited<TreeNode & WithType<string, NodeKind.Array>> {
@ -602,6 +635,25 @@ export type TransactionConstraint = NodeInDocumentConstraint;
// @public
export const Tree: TreeApi;
// @alpha @sealed
export const TreeAlpha: {
create<const TSchema extends ImplicitFieldSchema | UnsafeUnknownSchema>(schema: UnsafeUnknownSchema extends TSchema ? ImplicitFieldSchema : TSchema & ImplicitFieldSchema, data: InsertableField<TSchema>): Unhydrated<TSchema extends ImplicitFieldSchema ? TreeFieldFromImplicitField<TSchema> : TreeNode | TreeLeafValue | undefined>;
importConcise<const TSchema extends ImplicitFieldSchema | UnsafeUnknownSchema>(schema: UnsafeUnknownSchema extends TSchema ? ImplicitFieldSchema : TSchema & ImplicitFieldSchema, data: ConciseTree | undefined): Unhydrated<TSchema extends ImplicitFieldSchema ? TreeFieldFromImplicitField<TSchema> : TreeNode | TreeLeafValue | undefined>;
importVerbose<const TSchema extends ImplicitFieldSchema>(schema: TSchema, data: VerboseTree | undefined, options?: Partial<ParseOptions<IFluidHandle>>): Unhydrated<TreeFieldFromImplicitField<TSchema>>;
importVerbose<const TSchema extends ImplicitFieldSchema, THandle>(schema: TSchema, data: VerboseTree<THandle> | undefined, options: ParseOptions<THandle>): Unhydrated<TreeFieldFromImplicitField<TSchema>>;
exportConcise(node: TreeNode | TreeLeafValue, options?: Partial<EncodeOptions<IFluidHandle>>): ConciseTree;
exportConcise<THandle>(node: TreeNode | TreeLeafValue, options: EncodeOptions<THandle>): ConciseTree<THandle>;
exportVerbose(node: TreeNode | TreeLeafValue, options?: Partial<EncodeOptions<IFluidHandle>>): VerboseTree;
exportVerbose<T>(node: TreeNode | TreeLeafValue, options: EncodeOptions<T>): VerboseTree<T>;
exportCompressed(tree: TreeNode | TreeLeafValue, options: {
oldestCompatibleClient: FluidClientVersion;
idCompressor?: IIdCompressor;
}): JsonCompatible<IFluidHandle>;
importCompressed<const TSchema extends ImplicitFieldSchema>(schema: TSchema, compressedData: JsonCompatible<IFluidHandle>, options: {
idCompressor?: IIdCompressor;
} & ICodecOptions): Unhydrated<TreeFieldFromImplicitField<TSchema>>;
};
// @public @sealed
interface TreeApi extends TreeNodeApi {
contains(node: TreeNode, other: TreeNode): boolean;
@ -864,11 +916,29 @@ export type ValidateRecursiveSchema<T extends TreeNodeSchemaClass<string, NodeKi
[NodeKind.Map]: ImplicitAllowedTypes;
}[T["kind"]]>> = true;
// @alpha
export type VerboseTree<THandle = IFluidHandle> = VerboseTreeNode<THandle> | Exclude<TreeLeafValue, IFluidHandle> | THandle;
// @alpha
export interface VerboseTreeNode<THandle = IFluidHandle> {
fields: VerboseTree<THandle>[] | {
[key: string]: VerboseTree<THandle>;
};
type: string;
}
// @public @sealed
export interface ViewableTree {
viewWith<TRoot extends ImplicitFieldSchema>(config: TreeViewConfiguration<TRoot>): TreeView<TRoot>;
}
// @alpha
export interface ViewContent {
readonly idCompressor: IIdCompressor_2;
readonly schema: JsonCompatible;
readonly tree: JsonCompatible<IFluidHandle>;
}
// @public @sealed
export interface WithType<out TName extends string = string, out TKind extends NodeKind = NodeKind, out TInfo = unknown> {
// @deprecated

Просмотреть файл

@ -328,3 +328,28 @@ export function withSchemaValidation<
},
};
}
/**
* Versions of Fluid Framework client packages.
* @remarks
* Used to express compatibility requirements by indicating the oldest version with which compatibility must be maintained.
* @privateRemarks
* This scheme assumes a single version will always be enough to communicate compatibility.
* For this to work, compatibility has to be strictly increasing.
* If this is violated (for example a subset of incompatible features from 3.x that are not in 3.0 are back ported to 2.x),
* a more complex scheme may be needed to allow safely opting into incompatible features in those cases:
* such a system can be added if/when its needed since it will be opt in and thus non-breaking.
*
* TODO: this should likely be defined higher in the stack and specified when creating the container, possibly as part of its schema.
* @alpha
*/
export enum FluidClientVersion {
/** Fluid Framework Client 2.0 and newer. */
v2_0 = "v2_0",
/** Fluid Framework Client 2.1 and newer. */
v2_1 = "v2_1",
/** Fluid Framework Client 2.2 and newer. */
v2_2 = "v2_2",
/** Fluid Framework Client 2.4 and newer. */
v2_3 = "v2_3",
}

Просмотреть файл

@ -18,6 +18,7 @@ export {
unitCodec,
withDefaultBinaryEncoding,
withSchemaValidation,
FluidClientVersion,
} from "./codec.js";
export {
DiscriminatedUnionDispatcher,

Просмотреть файл

@ -9,6 +9,7 @@ import {
type FieldKey,
type FieldKindIdentifier,
type FieldUpPath,
type ITreeCursorSynchronous,
type TreeNodeSchemaIdentifier,
type TreeValue,
anchorSlot,
@ -184,6 +185,14 @@ export interface FlexTreeNode extends FlexTreeEntity {
* If well-formed, it must follow this schema.
*/
readonly schema: TreeNodeSchemaIdentifier;
/**
* Get a cursor for the underlying data.
* @remarks
* This cursor might be one the node uses in its implementation, and thus must be returned to its original location before using any other APIs to interact with the tree.
* Must not be held onto across edits or any other tree API use.
*/
borrowCursor(): ITreeCursorSynchronous;
}
/**

Просмотреть файл

@ -10,6 +10,7 @@ import {
type AnchorNode,
CursorLocationType,
type FieldKey,
type ITreeCursorSynchronous,
type FieldKindIdentifier,
type ITreeSubscriptionCursor,
type TreeNavigationResult,
@ -91,6 +92,10 @@ export class LazyTreeNode extends LazyEntity<Anchor> implements FlexTreeNode {
this.#removeDeleteCallback = anchorNode.on("afterDestroy", cleanupTree);
}
public borrowCursor(): ITreeCursorSynchronous {
return this[cursorSymbol] as ITreeCursorSynchronous;
}
protected override [tryMoveCursorToAnchorSymbol](
cursor: ITreeSubscriptionCursor,
): TreeNavigationResult {

Просмотреть файл

@ -60,6 +60,10 @@ export {
getBranch,
type TreeBranch,
type TreeBranchFork,
independentInitializedView,
type ViewContent,
TreeAlpha,
independentView,
} from "./shared-tree/index.js";
export {
@ -142,8 +146,13 @@ export {
// Beta APIs
TreeBeta,
type TreeChangeEventsBeta,
type VerboseTreeNode,
type EncodeOptions,
type ParseOptions,
type VerboseTree,
extractPersistedSchema,
comparePersistedSchema,
type ConciseTree,
// Back to normal types
type JsonTreeSchema,
type JsonSchemaId,
@ -170,10 +179,11 @@ export {
configuredSharedTree,
} from "./treeFactory.js";
export type {
ICodecOptions,
JsonValidator,
SchemaValidationFunction,
export {
type ICodecOptions,
type JsonValidator,
type SchemaValidationFunction,
FluidClientVersion,
} from "./codec/index.js";
export { noopValidator } from "./codec/index.js";
export { typeboxValidator } from "./external-utilities/index.js";

Просмотреть файл

@ -0,0 +1,176 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/
import type { IFluidHandle } from "@fluidframework/core-interfaces";
import { assert } from "@fluidframework/core-utils/internal";
import {
type IIdCompressor,
createIdCompressor,
} from "@fluidframework/id-compressor/internal";
import type { ICodecOptions } from "../codec/index.js";
import {
type RevisionTag,
RevisionTagCodec,
TreeStoredSchemaRepository,
initializeForest,
type ITreeCursorSynchronous,
mapCursorField,
} from "../core/index.js";
import {
createNodeKeyManager,
makeFieldBatchCodec,
makeSchemaCodec,
type FieldBatchEncodingContext,
defaultSchemaPolicy,
chunkTree,
defaultChunkPolicy,
TreeCompressionStrategy,
} from "../feature-libraries/index.js";
// eslint-disable-next-line import/no-internal-modules
import type { Format } from "../feature-libraries/schema-index/format.js";
import type {
TreeViewConfiguration,
ImplicitFieldSchema,
TreeViewAlpha,
} from "../simple-tree/index.js";
import type { JsonCompatibleReadOnly, JsonCompatible } from "../util/index.js";
import {
buildConfiguredForest,
defaultSharedTreeOptions,
type ForestOptions,
} from "./sharedTree.js";
import { createTreeCheckout } from "./treeCheckout.js";
import { SchematizingSimpleTreeView } from "./schematizingTreeView.js";
/**
* Create an uninitialized {@link TreeView} that is not tied to any {@link ITree} instance.
*
* @remarks
* Such a view can never experience collaboration or be persisted to to a Fluid Container.
*
* This can be useful for testing, as well as use-cases like working on local files instead of documents stored in some Fluid service.
* @alpha
*/
export function independentView<const TSchema extends ImplicitFieldSchema>(
config: TreeViewConfiguration<TSchema>,
options: ForestOptions & { idCompressor?: IIdCompressor | undefined },
): TreeViewAlpha<TSchema> {
const idCompressor: IIdCompressor = options.idCompressor ?? createIdCompressor();
const mintRevisionTag = (): RevisionTag => idCompressor.generateCompressedId();
const revisionTagCodec = new RevisionTagCodec(idCompressor);
const schema = new TreeStoredSchemaRepository();
const forest = buildConfiguredForest(
options.forest ?? defaultSharedTreeOptions.forest,
schema,
idCompressor,
);
const checkout = createTreeCheckout(idCompressor, mintRevisionTag, revisionTagCodec, {
forest,
schema,
});
const out: TreeViewAlpha<TSchema> = new SchematizingSimpleTreeView<TSchema>(
checkout,
config,
createNodeKeyManager(idCompressor),
);
return out;
}
/**
* Create an initialized {@link TreeView} that is not tied to any {@link ITree} instance.
*
* @remarks
* Such a view can never experience collaboration or be persisted to to a Fluid Container.
*
* This can be useful for testing, as well as use-cases like working on local files instead of documents stored in some Fluid service.
* @alpha
*/
export function independentInitializedView<const TSchema extends ImplicitFieldSchema>(
config: TreeViewConfiguration<TSchema>,
options: ForestOptions & ICodecOptions,
content: ViewContent,
): TreeViewAlpha<TSchema> {
const idCompressor: IIdCompressor = content.idCompressor;
const mintRevisionTag = (): RevisionTag => idCompressor.generateCompressedId();
const revisionTagCodec = new RevisionTagCodec(idCompressor);
const fieldBatchCodec = makeFieldBatchCodec(options, 1);
const schemaCodec = makeSchemaCodec(options);
const schema = new TreeStoredSchemaRepository(schemaCodec.decode(content.schema as Format));
const forest = buildConfiguredForest(
options.forest ?? defaultSharedTreeOptions.forest,
schema,
idCompressor,
);
const context: FieldBatchEncodingContext = {
encodeType: TreeCompressionStrategy.Compressed,
idCompressor,
originatorId: idCompressor.localSessionId, // Is this right? If so, why is is needed?
schema: { schema, policy: defaultSchemaPolicy },
};
const fieldCursors = fieldBatchCodec.decode(content.tree as JsonCompatibleReadOnly, context);
assert(fieldCursors.length === 1, "must have exactly 1 field in batch");
// Checked above.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const cursors = fieldCursorToNodesCursors(fieldCursors[0]!);
initializeForest(forest, cursors, revisionTagCodec, idCompressor, false);
const checkout = createTreeCheckout(idCompressor, mintRevisionTag, revisionTagCodec, {
forest,
schema,
});
const out: TreeViewAlpha<TSchema> = new SchematizingSimpleTreeView<TSchema>(
checkout,
config,
createNodeKeyManager(idCompressor),
);
return out;
}
function fieldCursorToNodesCursors(
fieldCursor: ITreeCursorSynchronous,
): ITreeCursorSynchronous[] {
return mapCursorField(fieldCursor, copyNodeCursor);
}
/**
* TODO: avoid needing this, or optimize it.
*/
function copyNodeCursor(cursor: ITreeCursorSynchronous): ITreeCursorSynchronous {
const copy = chunkTree(cursor, {
policy: defaultChunkPolicy,
idCompressor: undefined,
}).cursor();
copy.enterNode(0);
return copy;
}
/**
* The portion of SharedTree data typically persisted by the container.
* Usable with {@link independentInitializedView} to create a {@link TreeView}
* without loading a container.
* @alpha
*/
export interface ViewContent {
/**
* Compressed tree from {@link TreeAlpha.exportCompressed}.
* @remarks
* This is an owning reference:
* consumers of this content might modify this data in place (for example when applying edits) to avoid copying.
*/
readonly tree: JsonCompatible<IFluidHandle>;
/**
* Persisted schema from {@link extractPersistedSchema}.
*/
readonly schema: JsonCompatible;
/**
* IIdCompressor which will be used to decompress any compressed identifiers in `tree`
* as well as for any other identifiers added to the view.
*/
readonly idCompressor: IIdCompressor;
}

Просмотреть файл

@ -46,3 +46,11 @@ export {
type RunTransaction,
rollback,
} from "./treeApi.js";
export { TreeAlpha } from "./treeApiAlpha.js";
export {
independentInitializedView,
type ViewContent,
independentView,
} from "./independentView.js";

Просмотреть файл

@ -0,0 +1,374 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/
import { createIdCompressor } from "@fluidframework/id-compressor/internal";
import { UsageError } from "@fluidframework/telemetry-utils/internal";
import type { IFluidHandle } from "@fluidframework/core-interfaces";
import type { IIdCompressor } from "@fluidframework/id-compressor";
import {
getKernel,
type TreeNode,
type Unhydrated,
TreeBeta,
tryGetSchema,
createFromCursor,
createFromInsertable,
cursorFromInsertable,
FieldKind,
normalizeFieldSchema,
type ImplicitFieldSchema,
type InsertableField,
type TreeFieldFromImplicitField,
type TreeLeafValue,
type UnsafeUnknownSchema,
conciseFromCursor,
type ConciseTree,
applySchemaToParserOptions,
cursorFromVerbose,
verboseFromCursor,
type ParseOptions,
type VerboseTree,
type VerboseTreeNode,
toStoredSchema,
type EncodeOptions,
extractPersistedSchema,
TreeViewConfiguration,
} from "../simple-tree/index.js";
import { fail, type JsonCompatible } from "../util/index.js";
import { noopValidator, type FluidClientVersion, type ICodecOptions } from "../codec/index.js";
import type { ITreeCursorSynchronous } from "../core/index.js";
import {
cursorForMapTreeField,
defaultSchemaPolicy,
isTreeValue,
makeFieldBatchCodec,
mapTreeFromCursor,
TreeCompressionStrategy,
type FieldBatch,
type FieldBatchEncodingContext,
} from "../feature-libraries/index.js";
import { independentInitializedView, type ViewContent } from "./independentView.js";
/**
* Extensions to {@link Tree} and {@link TreeBeta} which are not yet stable.
* @sealed @alpha
*/
export const TreeAlpha: {
/**
* Construct tree content that is compatible with the field defined by the provided `schema`.
* @param schema - The schema for what to construct. As this is an {@link ImplicitFieldSchema}, a {@link FieldSchema}, {@link TreeNodeSchema} or {@link AllowedTypes} array can be provided.
* @param data - The data used to construct the field content.
* @remarks
* When providing a {@link TreeNodeSchemaClass}, this is the same as invoking its constructor except that an unhydrated node can also be provided.
* This function exists as a generalization that can be used in other cases as well,
* such as when `undefined` might be allowed (for an optional field), or when the type should be inferred from the data when more than one type is possible.
*
* Like with {@link TreeNodeSchemaClass}'s constructor, it's an error to provide an existing node to this API.
* For that case, use {@link TreeBeta.clone}.
* @privateRemarks
* There should be a way to provide a source for defaulted identifiers, wither via this API or some way to add them to its output later.
*/
create<const TSchema extends ImplicitFieldSchema | UnsafeUnknownSchema>(
schema: UnsafeUnknownSchema extends TSchema
? ImplicitFieldSchema
: TSchema & ImplicitFieldSchema,
data: InsertableField<TSchema>,
): Unhydrated<
TSchema extends ImplicitFieldSchema
? TreeFieldFromImplicitField<TSchema>
: TreeNode | TreeLeafValue | undefined
>;
/**
* Less type safe version of {@link TreeAlpha.create}, suitable for importing data.
* @remarks
* Due to {@link ConciseTree} relying on type inference from the data, its use is somewhat limited.
* This does not support {@link ConciseTree|ConciseTrees} with customized handle encodings or using persisted keys.
* Use "compressed" or "verbose" formats for more flexibility.
*
* When using this function,
* it is recommend to ensure your schema is unambiguous with {@link ITreeConfigurationOptions.preventAmbiguity}.
* If the schema is ambiguous, consider using {@link TreeAlpha.create} and {@link Unhydrated} nodes where needed,
* or using {@link TreeAlpha.(importVerbose:1)} and specify all types.
*
* Documented (and thus recoverable) error handling/reporting for this is not yet implemented,
* but for now most invalid inputs will throw a recoverable error.
*/
importConcise<const TSchema extends ImplicitFieldSchema | UnsafeUnknownSchema>(
schema: UnsafeUnknownSchema extends TSchema
? ImplicitFieldSchema
: TSchema & ImplicitFieldSchema,
data: ConciseTree | undefined,
): Unhydrated<
TSchema extends ImplicitFieldSchema
? TreeFieldFromImplicitField<TSchema>
: TreeNode | TreeLeafValue | undefined
>;
/**
* Construct tree content compatible with a field defined by the provided `schema`.
* @param schema - The schema for what to construct. As this is an {@link ImplicitFieldSchema}, a {@link FieldSchema}, {@link TreeNodeSchema} or {@link AllowedTypes} array can be provided.
* @param data - The data used to construct the field content. See {@link TreeAlpha.(exportVerbose:1)}.
* @remarks
* This overload requires that any {@link @fluidframework/core-interfaces#IFluidHandle|IFluidHandles} are encoded as actual {@link @fluidframework/core-interfaces#IFluidHandle|IFluidHandles} in the input.
*/
importVerbose<const TSchema extends ImplicitFieldSchema>(
schema: TSchema,
data: VerboseTree | undefined,
options?: Partial<ParseOptions<IFluidHandle>>,
): Unhydrated<TreeFieldFromImplicitField<TSchema>>;
/**
* Construct tree content compatible with a field defined by the provided `schema`.
* @param schema - The schema for what to construct. As this is an {@link ImplicitFieldSchema}, a {@link FieldSchema}, {@link TreeNodeSchema} or {@link AllowedTypes} array can be provided.
* @param data - The data used to construct the field content. See {@link TreeAlpha.(exportVerbose:2)}.
*
* @typeparam THandle - How {@link @fluidframework/core-interfaces#IFluidHandle|IFluidHandles} in the input `data` are encoded.
* A converter from this encoding to {@link @fluidframework/core-interfaces#IFluidHandle} is required in `options`.
*/
importVerbose<const TSchema extends ImplicitFieldSchema, THandle>(
schema: TSchema,
data: VerboseTree<THandle> | undefined,
options: ParseOptions<THandle>,
): Unhydrated<TreeFieldFromImplicitField<TSchema>>;
/**
* Same as {@link TreeAlpha.(exportConcise:2)}, except leaves handles as is.
*/
exportConcise(
node: TreeNode | TreeLeafValue,
options?: Partial<EncodeOptions<IFluidHandle>>,
): ConciseTree;
/**
* Copy a snapshot of the current version of a TreeNode into a {@link ConciseTree}.
*
* @typeparam THandle - How {@link @fluidframework/core-interfaces#IFluidHandle|IFluidHandles} in the output should be encoded.
* A converter from from {@link @fluidframework/core-interfaces#IFluidHandle} to this format is required in `options`.
*/
exportConcise<THandle>(
node: TreeNode | TreeLeafValue,
options: EncodeOptions<THandle>,
): ConciseTree<THandle>;
/**
* Same {@link TreeAlpha.(exportVerbose:2)} except leaves handles as is.
*/
exportVerbose(
node: TreeNode | TreeLeafValue,
options?: Partial<EncodeOptions<IFluidHandle>>,
): VerboseTree;
/**
* Copy a snapshot of the current version of a TreeNode into a JSON compatible plain old JavaScript Object.
* Verbose tree format, with explicit type on every node.
*
* @typeparam THandle - How {@link @fluidframework/core-interfaces#IFluidHandle|IFluidHandles} in the output should be encoded.
* A converter from from {@link @fluidframework/core-interfaces#IFluidHandle} to this format is required in `options`.
*
* @remarks
* There are several cases this may be preferred to {@link TreeAlpha.(exportConcise:2)}:
*
* 1. When not using {@link ITreeConfigurationOptions.preventAmbiguity} (or when using `useStableFieldKeys`), `exportConcise` can produce ambiguous data (the type may be unclear on some nodes).
* `exportVerbose` will always be unambiguous and thus lossless.
*
* 2. When the data might be interpreted without access to the exact same view schema. In such cases, the types may be unknowable if not included.
*
* 3. When easy access to the type is desired.
*/
exportVerbose<T>(node: TreeNode | TreeLeafValue, options: EncodeOptions<T>): VerboseTree<T>;
/**
* Export the content of the provided `tree` in a compressed JSON compatible format.
* @remarks
* If an `idCompressor` is provided, it will be used to compress identifiers and thus will be needed to decompress the data.
*
* Always uses "stored" keys.
* See {@link EncodeOptions.useStoredKeys} for details.
* @privateRemarks
* TODO: It is currently not clear how to work with the idCompressors correctly in the package API.
* Better APIs should probably be provided as there is currently no way to associate an un-hydrated tree with an idCompressor,
* Nor get the correct idCompressor from a subtree to use when exporting it.
* Additionally using `createIdCompressor` to make an idCompressor is `@legacy` and thus not intended for use in this API surface.
* It would probably make more sense if we provided a way to get an idCompressor from the context of a node,
* which could be optional (and settable if missing) for un0hydrated nodes and required for hydrated ones.
* Add in a stable public APi for creating idCompressors, and a way to get them from a tree (without view schema), and that should address the anticipated use-cases.
*/
exportCompressed(
tree: TreeNode | TreeLeafValue,
options: { oldestCompatibleClient: FluidClientVersion; idCompressor?: IIdCompressor },
): JsonCompatible<IFluidHandle>;
/**
* Import data encoded by {@link TreeAlpha.exportCompressed}.
*
* @param schema - Schema with which the data must be compatible. This compatibility is not verified and must be ensured by the caller.
* @param compressedData - Data compressed by {@link TreeAlpha.exportCompressed}.
* @param options - If {@link TreeAlpha.exportCompressed} was given an `idCompressor`, it must be provided here.
*
* @remarks
* If the data could have been encoded with a different schema, consider encoding the schema along side it using {@link extractPersistedSchema} and loading the data using {@link independentView}.
*
* @privateRemarks
* This API could be improved:
*
* 1. It could validate that the schema is compatible, and return or throw an error in the invalid case (maybe add a "try" version).
* 2. A "try" version of this could return an error if the data isn't in a supported format (as determined by version and/or JasonValidator).
* 3. Requiring the caller provide a JsonValidator isn't the most friendly API. It might be practical to provide a default.
*/
importCompressed<const TSchema extends ImplicitFieldSchema>(
schema: TSchema,
compressedData: JsonCompatible<IFluidHandle>,
options: { idCompressor?: IIdCompressor } & ICodecOptions,
): Unhydrated<TreeFieldFromImplicitField<TSchema>>;
} = {
create: createFromInsertable,
importConcise<TSchema extends ImplicitFieldSchema | UnsafeUnknownSchema>(
schema: UnsafeUnknownSchema extends TSchema
? ImplicitFieldSchema
: TSchema & ImplicitFieldSchema,
data: ConciseTree | undefined,
): Unhydrated<
TSchema extends ImplicitFieldSchema
? TreeFieldFromImplicitField<TSchema>
: TreeNode | TreeLeafValue | undefined
> {
return createFromInsertable<UnsafeUnknownSchema>(
schema,
data as InsertableField<UnsafeUnknownSchema>,
) as Unhydrated<
TSchema extends ImplicitFieldSchema
? TreeFieldFromImplicitField<TSchema>
: TreeNode | TreeLeafValue | undefined
>;
},
importVerbose<const TSchema extends ImplicitFieldSchema, THandle>(
schema: TSchema,
data: VerboseTree<THandle> | undefined,
options?: Partial<ParseOptions<THandle>>,
): Unhydrated<TreeFieldFromImplicitField<TSchema>> {
const config: ParseOptions<THandle> = {
valueConverter: (input: VerboseTree<THandle>) => {
return input as TreeLeafValue | VerboseTreeNode<THandle>;
},
...options,
};
// Create a config which is standalone, and thus can be used without having to refer back to the schema.
const schemalessConfig = applySchemaToParserOptions(schema, config);
if (data === undefined) {
const field = normalizeFieldSchema(schema);
if (field.kind !== FieldKind.Optional) {
throw new UsageError("undefined provided for non-optional field.");
}
return undefined as Unhydrated<TreeFieldFromImplicitField<TSchema>>;
}
const cursor = cursorFromVerbose<THandle>(data, schemalessConfig);
return createFromCursor(schema, cursor);
},
exportConcise<T>(
node: TreeNode | TreeLeafValue,
options?: Partial<EncodeOptions<T>>,
): ConciseTree<T> {
const config: EncodeOptions<T> = {
valueConverter(handle: IFluidHandle): T {
return handle as T;
},
...options,
};
const cursor = borrowCursorFromTreeNodeOrValue(node);
return conciseFromCursor(cursor, tryGetSchema(node) ?? fail("invalid input"), config);
},
exportVerbose<T>(
node: TreeNode | TreeLeafValue,
options?: Partial<EncodeOptions<T>>,
): VerboseTree<T> {
const config: EncodeOptions<T> = {
valueConverter(handle: IFluidHandle): T {
return handle as T;
},
...options,
};
const cursor = borrowCursorFromTreeNodeOrValue(node);
return verboseFromCursor(cursor, tryGetSchema(node) ?? fail("invalid input"), config);
},
exportCompressed(
node: TreeNode | TreeLeafValue,
options: {
oldestCompatibleClient: FluidClientVersion;
idCompressor?: IIdCompressor;
},
): JsonCompatible<IFluidHandle> {
const schema = tryGetSchema(node) ?? fail("invalid input");
const format = versionToFormat[options.oldestCompatibleClient];
const codec = makeFieldBatchCodec({ jsonValidator: noopValidator }, format);
const cursor = borrowFieldCursorFromTreeNodeOrValue(node);
const batch: FieldBatch = [cursor];
// If none provided, create a compressor which will not compress anything.
const idCompressor = options.idCompressor ?? createIdCompressor();
const context: FieldBatchEncodingContext = {
encodeType: TreeCompressionStrategy.Compressed,
idCompressor,
originatorId: idCompressor.localSessionId, // TODO: Why is this needed?
schema: { schema: toStoredSchema(schema), policy: defaultSchemaPolicy },
};
const result = codec.encode(batch, context);
return result;
},
importCompressed<const TSchema extends ImplicitFieldSchema>(
schema: TSchema,
compressedData: JsonCompatible<IFluidHandle>,
options: {
idCompressor?: IIdCompressor;
} & ICodecOptions,
): Unhydrated<TreeFieldFromImplicitField<TSchema>> {
const content: ViewContent = {
schema: extractPersistedSchema(schema),
tree: compressedData,
idCompressor: options.idCompressor ?? createIdCompressor(),
};
const config = new TreeViewConfiguration({ schema });
const view = independentInitializedView(config, options, content);
return TreeBeta.clone<TSchema>(view.root);
},
};
function borrowCursorFromTreeNodeOrValue(
node: TreeNode | TreeLeafValue,
): ITreeCursorSynchronous {
if (isTreeValue(node)) {
return cursorFromInsertable<UnsafeUnknownSchema>(
tryGetSchema(node) ?? fail("missing schema"),
node,
);
}
const kernel = getKernel(node);
const cursor = kernel.getOrCreateInnerNode().borrowCursor();
return cursor;
}
function borrowFieldCursorFromTreeNodeOrValue(
node: TreeNode | TreeLeafValue,
): ITreeCursorSynchronous {
const cursor = borrowCursorFromTreeNodeOrValue(node);
// TODO: avoid copy
const mapTree = mapTreeFromCursor(cursor);
return cursorForMapTreeField([mapTree]);
}
const versionToFormat = {
v2_0: 1,
v2_1: 1,
v2_2: 1,
v2_3: 1,
};

Просмотреть файл

@ -23,6 +23,7 @@ import { getUnhydratedContext } from "../createContext.js";
* @privateRemarks
* This can store all possible simple trees,
* but it can not store all possible trees representable by our internal representations like FlexTree and JsonableTree.
* @alpha
*/
export type ConciseTree<THandle = IFluidHandle> =
| Exclude<TreeLeafValue, IFluidHandle>

Просмотреть файл

@ -3,18 +3,17 @@
* Licensed under the MIT License.
*/
import type { IFluidHandle } from "@fluidframework/core-interfaces";
import { assert } from "@fluidframework/core-utils/internal";
import type { ITreeCursorSynchronous, SchemaAndPolicy } from "../../core/index.js";
import type {
TreeLeafValue,
ImplicitFieldSchema,
TreeFieldFromImplicitField,
FieldSchema,
FieldKind,
UnsafeUnknownSchema,
InsertableField,
TreeLeafValue,
} from "../schemaTypes.js";
import {
getOrCreateNodeFromInnerNode,
@ -32,13 +31,6 @@ import {
import { isFieldInSchema } from "../../feature-libraries/index.js";
import { toStoredSchema } from "../toStoredSchema.js";
import { inSchemaOrThrow, mapTreeFromNodeData } from "../toMapTree.js";
import {
applySchemaToParserOptions,
cursorFromVerbose,
type ParseOptions,
type VerboseTree,
type VerboseTreeNode,
} from "./verboseTree.js";
import { getUnhydratedContext } from "../createContext.js";
/**
@ -118,46 +110,6 @@ export function cursorFromInsertable<
return cursorForMapTreeNode(mapTree);
}
/**
* Construct tree content compatible with a field defined by the provided `schema`.
* @param schema - The schema for what to construct. As this is an {@link ImplicitFieldSchema}, a {@link FieldSchema}, {@link TreeNodeSchema} or {@link AllowedTypes} array can be provided.
* @param data - The data used to construct the field content. See `Tree.cloneToJSONVerbose`.
* @privateRemarks
* This could be exposed as a public `Tree.createFromVerbose` function.
*/
export function createFromVerbose<TSchema extends ImplicitFieldSchema, THandle>(
schema: TSchema,
data: VerboseTreeNode<THandle> | undefined,
options: ParseOptions<THandle>,
): Unhydrated<TreeFieldFromImplicitField<TSchema>>;
/**
* Construct tree content compatible with a field defined by the provided `schema`.
* @param schema - The schema for what to construct. As this is an {@link ImplicitFieldSchema}, a {@link FieldSchema}, {@link TreeNodeSchema} or {@link AllowedTypes} array can be provided.
* @param data - The data used to construct the field content. See `Tree.cloneToJSONVerbose`.
*/
export function createFromVerbose<TSchema extends ImplicitFieldSchema>(
schema: TSchema,
data: VerboseTreeNode | undefined,
options?: Partial<ParseOptions<IFluidHandle>>,
): Unhydrated<TreeFieldFromImplicitField<TSchema>>;
export function createFromVerbose<TSchema extends ImplicitFieldSchema, THandle>(
schema: TSchema,
data: VerboseTreeNode<THandle> | undefined,
options?: Partial<ParseOptions<THandle>>,
): Unhydrated<TreeFieldFromImplicitField<TSchema>> {
const config: ParseOptions<THandle> = {
valueConverter: (input: VerboseTree<THandle>) => {
return input as TreeLeafValue | VerboseTreeNode<THandle>;
},
...options,
};
const schemalessConfig = applySchemaToParserOptions(schema, config);
const cursor = cursorFromVerbose(data, schemalessConfig);
return createFromCursor(schema, cursor);
}
/**
* Creates an unhydrated simple-tree field from a cursor in nodes mode.
*/

Просмотреть файл

@ -28,6 +28,7 @@ import { isObjectNodeSchema } from "../objectNodeTypes.js";
/**
* Options for how to encode a tree.
* @alpha
*/
export interface EncodeOptions<TCustom> {
/**

Просмотреть файл

@ -24,8 +24,8 @@ export {
enumFromStrings,
singletonSchema,
} from "./schemaCreationUtilities.js";
export { treeNodeApi, type TreeNodeApi } from "./treeNodeApi.js";
export { createFromInsertable, cursorFromInsertable } from "./create.js";
export { treeNodeApi, type TreeNodeApi, tryGetSchema } from "./treeNodeApi.js";
export { createFromInsertable, cursorFromInsertable, createFromCursor } from "./create.js";
export type { SimpleTreeSchema } from "./simpleSchema.js";
export {
type JsonSchemaId,
@ -69,15 +69,18 @@ export type {
InsertableTreeNodeFromAllowedTypesUnsafe,
} from "./typesUnsafe.js";
export type {
VerboseTreeNode,
ParseOptions,
VerboseTree,
export {
type VerboseTreeNode,
type ParseOptions,
type VerboseTree,
applySchemaToParserOptions,
cursorFromVerbose,
verboseFromCursor,
} from "./verboseTree.js";
export type { EncodeOptions } from "./customTree.js";
export type { ConciseTree } from "./conciseTree.js";
export { type ConciseTree, conciseFromCursor } from "./conciseTree.js";
export { TreeBeta, type NodeChangedData, type TreeChangeEventsBeta } from "./treeApiBeta.js";

Просмотреть файл

@ -30,6 +30,14 @@ import type { MakeNominal } from "../../util/index.js";
import { walkFieldSchema } from "../walkFieldSchema.js";
/**
* A tree from which a {@link TreeView} can be created.
*
* @privateRemarks
* TODO:
* Add stored key versions of {@link TreeAlpha.(exportVerbose:2)}, {@link TreeAlpha.(exportConcise:2)} and {@link TreeAlpha.exportCompressed} here so tree content can be accessed without a view schema.
* Add exportSimpleSchema and exportJsonSchema methods (which should exactly match the concise format, and match the free functions for exporting view schema).
* Maybe rename "exportJsonSchema" to align on "concise" terminology.
* Ensure schema exporting APIs here align and reference APIs for exporting view schema to the same formats (which should include stored vs property key choice).
* Make sure users of independentView can use these export APIs (maybe provide a reference back to the ViewableTree from the TreeView to accomplish that).
* @system @sealed @public
*/
export interface ViewableTree {
@ -49,7 +57,7 @@ export interface ViewableTree {
* Only one schematized view may exist for a given ITree at a time.
* If creating a second, the first must be disposed before calling `viewWith` again.
*
* @privateRemarks
*
* TODO: Provide a way to make a generic view schema for any document.
* TODO: Support adapters for handling out-of-schema data.
*
@ -243,7 +251,7 @@ export class TreeViewConfiguration<
if (ambiguityErrors.length !== 0) {
// Duplicate errors are common since when two types conflict, both orders error:
const deduplicated = new Set(ambiguityErrors);
throw new UsageError(`Ambigious schema found:\n${[...deduplicated].join("\n")}`);
throw new UsageError(`Ambiguous schema found:\n${[...deduplicated].join("\n")}`);
}
// Eagerly perform this conversion to surface errors sooner.

Просмотреть файл

@ -12,14 +12,9 @@ import {
type Unhydrated,
type WithType,
} from "../core/index.js";
import type {
ImplicitFieldSchema,
TreeFieldFromImplicitField,
UnsafeUnknownSchema,
} from "../schemaTypes.js";
import { treeNodeApi } from "./treeNodeApi.js";
import { createFromCursor, cursorFromInsertable } from "./create.js";
import type { ITreeCursorSynchronous } from "../../core/index.js";
import { createFromCursor } from "./create.js";
import type { ImplicitFieldSchema, TreeFieldFromImplicitField } from "../schemaTypes.js";
/**
* Data included for {@link TreeChangeEventsBeta.nodeChanged}.
@ -128,6 +123,25 @@ export const TreeBeta: {
clone<const TSchema extends ImplicitFieldSchema>(
node: TreeFieldFromImplicitField<TSchema>,
): TreeFieldFromImplicitField<TSchema>;
// TODO: support more clone options
// /**
// * Like {@link TreeBeta.create}, except deeply clones existing nodes.
// * @remarks
// * This only clones the persisted data associated with a node.
// * Local state, such as properties added to customized schema classes, will not be cloned:
// * they will be initialized however they end up after running the constructor, just like if a remote client had inserted the same nodes.
// */
// clone<const TSchema extends ImplicitFieldSchema>(
// original: TreeFieldFromImplicitField<TSchema>,
// options?: {
// /**
// * If set, all identifier's in the cloned tree (See {@link SchemaFactory.identifier}) will be replaced with new ones allocated using the default identifier allocation schema.
// * Otherwise any identifiers will be preserved as is.
// */
// replaceIdentifiers?: true;
// },
// ): TreeFieldFromImplicitField<TSchema>;
} = {
on<K extends keyof TreeChangeEventsBeta<TNode>, TNode extends TreeNode>(
node: TNode,
@ -145,26 +159,9 @@ export const TreeBeta: {
}
const kernel = getKernel(node);
/*
* For unhydrated nodes, we can create a cursor by calling `cursorFromInsertable` because the node
* hasn't been inserted yet. We can then create a new node from the cursor.
*/
if (!kernel.isHydrated()) {
return createFromCursor(
kernel.schema,
cursorFromInsertable<UnsafeUnknownSchema>(kernel.schema, node),
) as Unhydrated<TreeFieldFromImplicitField<TSchema>>;
}
// For hydrated nodes, create a new cursor in the forest and then create a new node from the cursor.
const forest = kernel.context.flexContext.checkout.forest;
const cursor = forest.allocateCursor("tree.clone");
forest.moveCursorToPath(kernel.anchorNode, cursor);
const clonedNode = createFromCursor(
kernel.schema,
cursor as ITreeCursorSynchronous,
) as Unhydrated<TreeFieldFromImplicitField<TSchema>>;
cursor.free();
return clonedNode;
const cursor = kernel.getOrCreateInnerNode().borrowCursor();
return createFromCursor(kernel.schema, cursor) as Unhydrated<
TreeFieldFromImplicitField<TSchema>
>;
},
};

Просмотреть файл

@ -53,6 +53,7 @@ import { getUnhydratedContext } from "../createContext.js";
* @privateRemarks
* This can store all possible simple trees,
* but it can not store all possible trees representable by our internal representations like FlexTree and JsonableTree.
* @alpha
*/
export type VerboseTree<THandle = IFluidHandle> =
| VerboseTreeNode<THandle>
@ -82,6 +83,7 @@ export type VerboseTree<THandle = IFluidHandle> =
* Unlike `JsonableTree`, leaf nodes are not boxed into node objects, and instead have their schema inferred from the value.
* Additionally, sequence fields can only occur on a node that has a single sequence field (with the empty key)
* replicating the behavior of simple-tree ArrayNodes.
* @alpha
*/
export interface VerboseTreeNode<THandle = IFluidHandle> {
/**
@ -109,6 +111,7 @@ export interface VerboseTreeNode<THandle = IFluidHandle> {
/**
* Options for how to interpret a `VerboseTree<TCustom>` when schema information is available.
* @alpha
*/
export interface ParseOptions<TCustom> {
/**

Просмотреть файл

@ -15,6 +15,7 @@ import {
type FieldKindIdentifier,
type FieldUpPath,
forbiddenFieldKindIdentifier,
type ITreeCursorSynchronous,
type MapTree,
type SchemaPolicy,
type TreeNodeSchemaIdentifier,
@ -39,6 +40,7 @@ import {
type FlexFieldKind,
FieldKinds,
type SequenceFieldEditBuilder,
cursorForMapTreeNode,
} from "../../feature-libraries/index.js";
import type { Context } from "./context.js";
import { createEmitter, type Listenable } from "../../events/index.js";
@ -168,6 +170,10 @@ export class UnhydratedFlexTreeNode implements UnhydratedFlexTreeNode {
return this.location;
}
public borrowCursor(): ITreeCursorSynchronous {
return cursorForMapTreeNode(this.mapTree);
}
public tryGetField(key: FieldKey): UnhydratedFlexTreeField | undefined {
const field = this.mapTree.fields.get(key);
// Only return the field if it is not empty, in order to fulfill the contract of `tryGetField`.

Просмотреть файл

@ -23,6 +23,7 @@ export {
HydratedContext,
SimpleContextSlot,
getOrCreateInnerNode,
getKernel,
} from "./core/index.js";
export {
type ITree,
@ -97,6 +98,12 @@ export {
type TreeNodeSchemaNonClassUnsafe,
type InsertableTreeNodeFromAllowedTypesUnsafe,
type TreeViewAlpha,
tryGetSchema,
applySchemaToParserOptions,
cursorFromVerbose,
verboseFromCursor,
conciseFromCursor,
createFromCursor,
} from "./api/index.js";
export {
type NodeFromSchema,

Просмотреть файл

@ -502,9 +502,7 @@ export type InsertableField<TSchema extends ImplicitFieldSchema | UnsafeUnknownS
* @system @alpha
*/
export type ReadableField<TSchema extends ImplicitFieldSchema | UnsafeUnknownSchema> =
TSchema extends ImplicitFieldSchema
? TreeFieldFromImplicitField<TSchema>
: TreeLeafValue | TreeNode;
TreeFieldFromImplicitField<ReadSchema<TSchema>>;
/**
* Adapter to remove {@link (UnsafeUnknownSchema:type)} from a schema type so it can be used with types for generating APIs for reading data.

Просмотреть файл

@ -6,8 +6,7 @@
import { strict as assert } from "node:assert";
import { createFromInsertable, SchemaFactory } from "../../../simple-tree/index.js";
// eslint-disable-next-line import/no-internal-modules
import { createFromVerbose } from "../../../simple-tree/api/create.js";
import { TreeAlpha } from "../../../shared-tree/index.js";
const schema = new SchemaFactory("com.example");
@ -25,7 +24,7 @@ describe("simple-tree create", () => {
});
it("createFromVerbose", () => {
const canvas1 = createFromVerbose(Canvas, {
const canvas1 = TreeAlpha.importVerbose(Canvas, {
type: Canvas.identifier,
fields: { stuff: { type: NodeList.identifier, fields: [] } },
});

Просмотреть файл

@ -17,13 +17,16 @@ import {
type StableNodeKey,
} from "../../../feature-libraries/index.js";
import {
isTreeNode,
type NodeFromSchema,
SchemaFactory,
treeNodeApi as Tree,
TreeBeta,
type TreeChangeEvents,
type TreeLeafValue,
type TreeNode,
TreeViewConfiguration,
type UnsafeUnknownSchema,
} from "../../../simple-tree/index.js";
import { getView, validateUsageError } from "../../utils.js";
import { getViewForForkedBranch, hydrate } from "../utils.js";
@ -39,6 +42,10 @@ import {
} from "../../../simple-tree/leafNodeSchema.js";
// eslint-disable-next-line import/no-internal-modules
import { tryGetSchema } from "../../../simple-tree/api/treeNodeApi.js";
import { testSimpleTrees } from "../../testTrees.js";
import { FluidClientVersion } from "../../../codec/index.js";
import { ajvValidator } from "../../codec/index.js";
import { TreeAlpha } from "../../../shared-tree/index.js";
const schema = new SchemaFactory("com.example");
@ -1116,5 +1123,247 @@ describe("treeNodeApi", () => {
const clonedMetadata = TreeBeta.clone<typeof schema.string>(topLeftPoint.metadata);
assert.equal(clonedMetadata, topLeftPoint.metadata, "String not cloned properly");
});
describe("test-trees", () => {
for (const testCase of testSimpleTrees) {
it(testCase.name, () => {
const tree = TreeAlpha.create<UnsafeUnknownSchema>(testCase.schema, testCase.root());
const exported = TreeBeta.clone(tree);
if (isTreeNode(tree)) {
// New instance
assert.notEqual(tree, exported);
}
expectTreesEqual(tree, exported);
});
}
});
});
// create is mostly the same as node constructors which have their own tests, so just cover the new cases (optional and top level unions) here.
describe("create", () => {
it("undefined", () => {
// Valid
assert.equal(TreeAlpha.create(schema.optional([]), undefined), undefined);
// Undefined where not allowed
assert.throws(
() => TreeAlpha.create(schema.required([]), undefined as never),
validateUsageError(/undefined for non-optional field/),
);
// Undefined required, not provided
assert.throws(
() => TreeAlpha.create(schema.optional([]), 1 as unknown as undefined),
validateUsageError(/incompatible/),
);
});
it("union", () => {
// Valid
assert.equal(TreeAlpha.create([schema.null, schema.number], null), null);
// invalid
assert.throws(
() => TreeAlpha.create([schema.null, schema.number], "x" as unknown as number),
validateUsageError(/incompatible/),
);
});
// Integration test object complex objects work (mainly covered by tests elsewhere)
it("object", () => {
const A = schema.object("A", { x: schema.number });
const a = TreeAlpha.create(A, { x: 1 });
assert.deepEqual(a, { x: 1 });
});
});
describe("concise", () => {
describe("importConcise", () => {
it("undefined", () => {
// Valid
assert.equal(TreeAlpha.importConcise(schema.optional([]), undefined), undefined);
// Undefined where not allowed
assert.throws(
() => TreeAlpha.importConcise(schema.required([]), undefined),
validateUsageError(/Got undefined for non-optional field/),
);
// Undefined required, not provided
assert.throws(
() => TreeAlpha.importConcise(schema.optional([]), 1),
validateUsageError(/incompatible with all of the types allowed/),
);
});
it("union", () => {
// Valid
assert.equal(TreeAlpha.importConcise([schema.null, schema.number], null), null);
// invalid
assert.throws(
() => TreeAlpha.importConcise([schema.null, schema.number], "x"),
validateUsageError(/The provided data is incompatible/),
);
});
it("object", () => {
const A = schema.object("A", { x: schema.number });
const a = TreeAlpha.importConcise(A, { x: 1 });
assert.deepEqual(a, { x: 1 });
});
});
describe("roundtrip", () => {
for (const testCase of testSimpleTrees) {
if (testCase.root() !== undefined) {
it(testCase.name, () => {
const tree = TreeAlpha.create<UnsafeUnknownSchema>(
testCase.schema,
testCase.root(),
);
assert(tree !== undefined);
const exported = TreeAlpha.exportConcise(tree);
if (testCase.ambiguous) {
assert.throws(
() => TreeAlpha.importConcise<UnsafeUnknownSchema>(testCase.schema, exported),
validateUsageError(/compatible with more than one type/),
);
} else {
const imported = TreeAlpha.importConcise<UnsafeUnknownSchema>(
testCase.schema,
exported,
);
expectTreesEqual(tree, imported);
}
});
}
}
});
describe("export-stored", () => {
for (const testCase of testSimpleTrees) {
if (testCase.root() !== undefined) {
it(testCase.name, () => {
const tree = TreeAlpha.create<UnsafeUnknownSchema>(
testCase.schema,
testCase.root(),
);
assert(tree !== undefined);
const _exported = TreeAlpha.exportConcise(tree, { useStoredKeys: true });
// We have nothing that imports concise trees with stored keys, so no validation here.
});
}
}
});
});
describe("verbose", () => {
describe("importVerbose", () => {
it("undefined", () => {
// Valid
assert.equal(TreeAlpha.importVerbose(schema.optional([]), undefined), undefined);
// Undefined where not allowed
assert.throws(
() => TreeAlpha.importVerbose(schema.required([]), undefined),
validateUsageError(/non-optional/),
);
// Undefined required, not provided
assert.throws(
() => TreeAlpha.importVerbose(schema.optional([]), 1),
validateUsageError(/does not conform to schema/),
);
});
it("union", () => {
// Valid
assert.equal(TreeAlpha.importVerbose([schema.null, schema.number], null), null);
// invalid
assert.throws(
() => TreeAlpha.importVerbose([schema.null, schema.number], "x"),
validateUsageError(/does not conform to schema/),
);
});
it("object", () => {
const A = schema.object("A", { x: schema.number });
const a = TreeAlpha.importVerbose(A, { type: A.identifier, fields: { x: 1 } });
assert.deepEqual(a, { x: 1 });
});
});
describe("roundtrip", () => {
for (const testCase of testSimpleTrees) {
if (testCase.root() !== undefined) {
it(testCase.name, () => {
const tree = TreeAlpha.create<UnsafeUnknownSchema>(
testCase.schema,
testCase.root(),
);
assert(tree !== undefined);
const exported = TreeAlpha.exportVerbose(tree);
const imported = TreeAlpha.importVerbose(testCase.schema, exported);
expectTreesEqual(tree, imported);
});
}
}
});
describe("roundtrip-stored", () => {
for (const testCase of testSimpleTrees) {
if (testCase.root() !== undefined) {
it(testCase.name, () => {
const tree = TreeAlpha.create<UnsafeUnknownSchema>(
testCase.schema,
testCase.root(),
);
assert(tree !== undefined);
const exported = TreeAlpha.exportVerbose(tree, { useStoredKeys: true });
const imported = TreeAlpha.importVerbose(testCase.schema, exported, {
useStoredKeys: true,
});
expectTreesEqual(tree, imported);
});
}
}
});
});
describe("compressed", () => {
describe("roundtrip", () => {
for (const testCase of testSimpleTrees) {
if (testCase.root() !== undefined) {
it(testCase.name, () => {
const tree = TreeAlpha.create<UnsafeUnknownSchema>(
testCase.schema,
testCase.root(),
);
assert(tree !== undefined);
const exported = TreeAlpha.exportCompressed(tree, {
oldestCompatibleClient: FluidClientVersion.v2_0,
});
const imported = TreeAlpha.importCompressed(testCase.schema, exported, {
jsonValidator: ajvValidator,
});
expectTreesEqual(tree, imported);
});
}
}
});
});
});
function expectTreesEqual(
a: TreeNode | TreeLeafValue | undefined,
b: TreeNode | TreeLeafValue | undefined,
): void {
if (a === undefined || b === undefined) {
assert.equal(a === undefined, b === undefined);
return;
}
// Validate the same schema objects are used.
assert.equal(Tree.schema(a), Tree.schema(b));
// This should catch all cases, assuming exportVerbose works correctly.
assert.deepEqual(TreeAlpha.exportVerbose(a), TreeAlpha.exportVerbose(b));
// Since this uses some of the tools to compare trees that this is testing for, perform the comparison in a few ways to reduce risk of a bug making this pass when it shouldn't:
// This case could have false negatives (two trees with ambiguous schema could export the same concise tree),
// but should have no false positives since equal trees always have the same concise tree.
assert.deepEqual(TreeAlpha.exportConcise(a), TreeAlpha.exportConcise(b));
}

Просмотреть файл

@ -219,19 +219,24 @@ export function count(iterable: Iterable<unknown>): number {
/**
* Use for Json compatible data.
*
* @typeparam TExtra - Type permitted in addition to the normal JSON types.
* Commonly used for to allow {@link @fluidframework/core-interfaces#IFluidHandle} within the otherwise JSON compatible content.
*
* @remarks
* This does not robustly forbid non json comparable data via type checking,
* but instead mostly restricts access to it.
* @alpha
*/
export type JsonCompatible =
export type JsonCompatible<TExtra = never> =
| string
| number
| boolean
// eslint-disable-next-line @rushstack/no-new-null
| null
| JsonCompatible[]
| JsonCompatibleObject;
| JsonCompatible<TExtra>[]
| JsonCompatibleObject<TExtra>
| TExtra;
/**
* Use for Json object compatible data.
@ -240,7 +245,7 @@ export type JsonCompatible =
* but instead mostly restricts access to it.
* @alpha
*/
export type JsonCompatibleObject = { [P in string]?: JsonCompatible };
export type JsonCompatibleObject<TExtra = never> = { [P in string]?: JsonCompatible<TExtra> };
/**
* Use for readonly view of Json compatible data.

Просмотреть файл

@ -56,6 +56,11 @@ export interface CommitMetadata {
// @alpha
export function comparePersistedSchema(persisted: JsonCompatible, view: JsonCompatible, options: ICodecOptions, canInitialize: boolean): SchemaCompatibilityStatus;
// @alpha
export type ConciseTree<THandle = IFluidHandle> = Exclude<TreeLeafValue, IFluidHandle> | THandle | ConciseTree<THandle>[] | {
[key: string]: ConciseTree<THandle>;
};
// @alpha
export function configuredSharedTree(options: SharedTreeOptions): SharedObjectKind<ITree>;
@ -91,6 +96,12 @@ export interface ContainerSchema {
interface DefaultProvider extends ErasedType<"@fluidframework/tree.FieldProvider"> {
}
// @alpha
export interface EncodeOptions<TCustom> {
readonly useStoredKeys?: boolean;
valueConverter(data: IFluidHandle): TCustom;
}
// @alpha
export function enumFromStrings<TScope extends string, const Members extends readonly string[]>(factory: SchemaFactory<TScope>, members: Members): (<TValue extends Members[number]>(value: TValue) => TreeNode & {
readonly value: TValue;
@ -180,6 +191,14 @@ type FlexList<Item = unknown> = readonly LazyItem<Item>[];
// @public
type FlexListToUnion<TList extends FlexList> = ExtractItemType<TList[number]>;
// @alpha
export enum FluidClientVersion {
v2_0 = "v2_0",
v2_1 = "v2_1",
v2_2 = "v2_2",
v2_3 = "v2_3"
}
// @public
export type FluidObject<T = unknown> = {
[P in FluidObjectProviderKeys<T>]?: T[P];
@ -473,6 +492,14 @@ export type ImplicitAllowedTypes = AllowedTypes | TreeNodeSchema;
// @public
export type ImplicitFieldSchema = FieldSchema | ImplicitAllowedTypes;
// @alpha
export function independentInitializedView<const TSchema extends ImplicitFieldSchema>(config: TreeViewConfiguration<TSchema>, options: ForestOptions & ICodecOptions, content: ViewContent): TreeViewAlpha<TSchema>;
// @alpha
export function independentView<const TSchema extends ImplicitFieldSchema>(config: TreeViewConfiguration<TSchema>, options: ForestOptions & {
idCompressor?: IIdCompressor_2 | undefined;
}): TreeViewAlpha<TSchema>;
// @public
export type InitialObjects<T extends ContainerSchema> = {
[K in keyof T["initialObjects"]]: T["initialObjects"][K] extends SharedObjectKind<infer TChannel> ? TChannel : never;
@ -646,11 +673,11 @@ export interface JsonArrayNodeSchema extends JsonNodeSchemaBase<NodeKind.Array,
}
// @alpha
export type JsonCompatible = string | number | boolean | null | JsonCompatible[] | JsonCompatibleObject;
export type JsonCompatible<TExtra = never> = string | number | boolean | null | JsonCompatible<TExtra>[] | JsonCompatibleObject<TExtra> | TExtra;
// @alpha
export type JsonCompatibleObject = {
[P in string]?: JsonCompatible;
export type JsonCompatibleObject<TExtra = never> = {
[P in string]?: JsonCompatible<TExtra>;
};
// @alpha @sealed
@ -792,11 +819,17 @@ type ObjectFromSchemaRecordUnsafe<T extends Unenforced<RestrictiveStringRecord<I
// @public
export type Off = () => void;
// @alpha
export interface ParseOptions<TCustom> {
readonly useStoredKeys?: boolean;
valueConverter(data: VerboseTree<TCustom>): TreeLeafValue | VerboseTreeNode<TCustom>;
}
// @alpha
export type PopUnion<Union, AsOverloadedFunction = UnionToIntersection<Union extends unknown ? (f: Union) => void : never>> = AsOverloadedFunction extends (a: infer First) => void ? First : never;
// @alpha
export type ReadableField<TSchema extends ImplicitFieldSchema | UnsafeUnknownSchema> = TSchema extends ImplicitFieldSchema ? TreeFieldFromImplicitField<TSchema> : TreeLeafValue | TreeNode;
export type ReadableField<TSchema extends ImplicitFieldSchema | UnsafeUnknownSchema> = TreeFieldFromImplicitField<ReadSchema<TSchema>>;
// @public @sealed
export interface ReadonlyArrayNode<out T = TreeNode | TreeLeafValue> extends ReadonlyArray<T>, Awaited<TreeNode & WithType<string, NodeKind.Array>> {
@ -977,6 +1010,25 @@ export type TransformedEvent<TThis, E, A extends any[]> = (event: E, listener: (
// @public
export const Tree: TreeApi;
// @alpha @sealed
export const TreeAlpha: {
create<const TSchema extends ImplicitFieldSchema | UnsafeUnknownSchema>(schema: UnsafeUnknownSchema extends TSchema ? ImplicitFieldSchema : TSchema & ImplicitFieldSchema, data: InsertableField<TSchema>): Unhydrated<TSchema extends ImplicitFieldSchema ? TreeFieldFromImplicitField<TSchema> : TreeNode | TreeLeafValue | undefined>;
importConcise<const TSchema extends ImplicitFieldSchema | UnsafeUnknownSchema>(schema: UnsafeUnknownSchema extends TSchema ? ImplicitFieldSchema : TSchema & ImplicitFieldSchema, data: ConciseTree | undefined): Unhydrated<TSchema extends ImplicitFieldSchema ? TreeFieldFromImplicitField<TSchema> : TreeNode | TreeLeafValue | undefined>;
importVerbose<const TSchema extends ImplicitFieldSchema>(schema: TSchema, data: VerboseTree | undefined, options?: Partial<ParseOptions<IFluidHandle>>): Unhydrated<TreeFieldFromImplicitField<TSchema>>;
importVerbose<const TSchema extends ImplicitFieldSchema, THandle>(schema: TSchema, data: VerboseTree<THandle> | undefined, options: ParseOptions<THandle>): Unhydrated<TreeFieldFromImplicitField<TSchema>>;
exportConcise(node: TreeNode | TreeLeafValue, options?: Partial<EncodeOptions<IFluidHandle>>): ConciseTree;
exportConcise<THandle>(node: TreeNode | TreeLeafValue, options: EncodeOptions<THandle>): ConciseTree<THandle>;
exportVerbose(node: TreeNode | TreeLeafValue, options?: Partial<EncodeOptions<IFluidHandle>>): VerboseTree;
exportVerbose<T>(node: TreeNode | TreeLeafValue, options: EncodeOptions<T>): VerboseTree<T>;
exportCompressed(tree: TreeNode | TreeLeafValue, options: {
oldestCompatibleClient: FluidClientVersion;
idCompressor?: IIdCompressor;
}): JsonCompatible<IFluidHandle>;
importCompressed<const TSchema extends ImplicitFieldSchema>(schema: TSchema, compressedData: JsonCompatible<IFluidHandle>, options: {
idCompressor?: IIdCompressor;
} & ICodecOptions): Unhydrated<TreeFieldFromImplicitField<TSchema>>;
};
// @public @sealed
interface TreeApi extends TreeNodeApi {
contains(node: TreeNode, other: TreeNode): boolean;
@ -1239,11 +1291,29 @@ export type ValidateRecursiveSchema<T extends TreeNodeSchemaClass<string, NodeKi
[NodeKind.Map]: ImplicitAllowedTypes;
}[T["kind"]]>> = true;
// @alpha
export type VerboseTree<THandle = IFluidHandle> = VerboseTreeNode<THandle> | Exclude<TreeLeafValue, IFluidHandle> | THandle;
// @alpha
export interface VerboseTreeNode<THandle = IFluidHandle> {
fields: VerboseTree<THandle>[] | {
[key: string]: VerboseTree<THandle>;
};
type: string;
}
// @public @sealed
export interface ViewableTree {
viewWith<TRoot extends ImplicitFieldSchema>(config: TreeViewConfiguration<TRoot>): TreeView<TRoot>;
}
// @alpha
export interface ViewContent {
readonly idCompressor: IIdCompressor_2;
readonly schema: JsonCompatible;
readonly tree: JsonCompatible<IFluidHandle>;
}
// @public @sealed
export interface WithType<out TName extends string = string, out TKind extends NodeKind = NodeKind, out TInfo = unknown> {
// @deprecated

Просмотреть файл

@ -1243,6 +1243,64 @@ importers:
specifier: ^6.0.1
version: 6.0.1
examples/apps/tree-cli-app:
dependencies:
'@fluidframework/core-interfaces':
specifier: workspace:~
version: link:../../../packages/common/core-interfaces
'@fluidframework/id-compressor':
specifier: workspace:~
version: link:../../../packages/runtime/id-compressor
'@fluidframework/runtime-utils':
specifier: workspace:~
version: link:../../../packages/runtime/runtime-utils
'@fluidframework/tree':
specifier: workspace:~
version: link:../../../packages/dds/tree
'@sinclair/typebox':
specifier: ^0.32.29
version: 0.32.35
devDependencies:
'@biomejs/biome':
specifier: ~1.9.3
version: 1.9.3
'@fluid-internal/mocha-test-setup':
specifier: workspace:~
version: link:../../../packages/test/mocha-test-setup
'@fluidframework/build-tools':
specifier: ^0.49.0
version: 0.49.0
'@fluidframework/eslint-config-fluid':
specifier: ^5.4.0
version: 5.4.0(eslint@8.55.0)(typescript@5.4.5)
'@types/mocha':
specifier: ^9.1.1
version: 9.1.1
'@types/node':
specifier: ^18.19.0
version: 18.19.54
cross-env:
specifier: ^7.0.3
version: 7.0.3
eslint:
specifier: ~8.55.0
version: 8.55.0
mocha:
specifier: ^10.2.0
version: 10.7.3
mocha-json-output-reporter:
specifier: ^2.0.1
version: 2.1.0(mocha@10.7.3)(moment@2.30.1)
mocha-multi-reporters:
specifier: ^1.5.1
version: 1.5.1(mocha@10.7.3)
rimraf:
specifier: ^4.4.0
version: 4.4.1
typescript:
specifier: ~5.4.5
version: 5.4.5
examples/apps/tree-comparison:
dependencies:
'@fluid-example/example-utils':
@ -28383,6 +28441,7 @@ packages:
/eslint@6.8.0:
resolution: {integrity: sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==}
engines: {node: ^8.10.0 || ^10.13.0 || >=11.10.1}
deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options.
hasBin: true
dependencies:
'@babel/code-frame': 7.25.7