revert: "Enable Synchronous Child Datastore Creation (#22962)" (#23133)

and collateral "Tag asserts for release (#23128)"

This reverts commits 67b5e4dca8 and
eefaaf3c2a.
This commit is contained in:
Jason Hartman 2024-11-18 17:39:41 -08:00 коммит произвёл GitHub
Родитель 3cd1f01a14
Коммит b92a003c1e
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
11 изменённых файлов: 105 добавлений и 579 удалений

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

@ -10,7 +10,6 @@
- [Key Features and Use Cases:](#key-features-and-use-cases)
- [Configuration and Compatibility Requirements:](#configuration-and-compatibility-requirements)
- [Usage Example:](#usage-example)
- [Enable Synchronous Child Datastore Creation (#22962)](#enable-synchronous-child-datastore-creation-22962)
- [Overview](#overview)
- [Key Benefits](#key-benefits)
- [Use Cases](#use-cases)
@ -83,83 +82,6 @@ Affected packages:
[⬆️ Table of contents](#contents)
### Enable Synchronous Child Datastore Creation ([#22962](https://github.com/microsoft/FluidFramework/issues/22962))
## Overview
This feature introduces a new pattern for creating datastores synchronously within the Fluid Framework. It allows for the synchronous creation of a child datastore from an existing datastore, provided that the child datastore is available synchronously via the existing datastore's registry and that the child's factory supports synchronous creation. This method also ensures strong typing for the consumer.
In this context, "child" refers specifically to the organization of factories and registries, not to any hierarchical or hosting relationship between datastores. The parent datastore does not control the runtime behaviors of the child datastore beyond its creation.
The synchronous creation of child datastores enhances the flexibility of datastore management within the Fluid Framework. It ensures type safety and provides a different way to manage datastores within a container. However, it is important to consider the overhead associated with datastores, as they are stored, summarized, garbage collected, loaded, and referenced independently. This overhead should be justified by the scenario's requirements.
Datastores offer increased capabilities, such as the ability to reference them via handles, allowing multiple references to exist and enabling those references to be moved, swapped, or changed. Additionally, datastores are garbage collected after becoming unreferenced, which can simplify final cleanup across clients. This is in contrast to subdirectories in a shared directory, which do not have native capabilities for referencing or garbage collection but are very low overhead to create.
Synchronous creation relies on both the factory and the datastore to support it. This means that asynchronous operations, such as resolving handles, some browser API calls, consensus-based operations, or other asynchronous tasks, cannot be performed during the creation flow. Therefore, synchronous child datastore creation is best limited to scenarios where the existing asynchronous process cannot be used, such as when a new datastore must be created in direct response to synchronous user input.
## Key Benefits
- **Synchronous Creation**: Allows for the immediate creation of child datastores without waiting for asynchronous operations.
- **Strong Typing**: Ensures type safety and better developer experience by leveraging TypeScript's type system.
## Use Cases
### Example 1: Creating a Child Datastore
In this example, we demonstrate how to support creating a child datastore synchronously from a parent datastore.
```typescript
/**
* This is the parent DataObject, which is also a datastore. It has a
* synchronous method to create child datastores, which could be called
* in response to synchronous user input, like a key press.
*/
class ParentDataObject extends DataObject {
get ParentDataObject() {
return this;
}
protected override async initializingFirstTime(): Promise<void> {
// create synchronously during initialization
this.createChild("parentCreation");
}
createChild(name: string): ChildDataStore {
assert(
this.context.createChildDataStore !== undefined,
"this.context.createChildDataStore",
);
// creates a detached context with a factory who's package path is the same
// as the current datastore, but with another copy of its own type.
const { entrypoint } = this.context.createChildDataStore(
ChildDataStoreFactory.instance,
);
const dir = this.root.createSubDirectory("children");
dir.set(name, entrypoint.handle);
entrypoint.setProperty("childValue", name);
return entrypoint;
}
getChild(name: string): IFluidHandle<ChildDataStore> | undefined {
const dir = this.root.getSubDirectory("children");
return dir?.get<IFluidHandle<ChildDataStore>>(name);
}
}
```
For a complete example see the follow test: <https://github.com/microsoft/FluidFramework/blob/main/packages/test/local-server-tests/src/test/synchronousDataStoreCreation.spec.ts>
#### Change details
Commit: [`67b5e4d`](https://github.com/microsoft/FluidFramework/commit/67b5e4dca8ae7ae2b2878ecb289e08624f467129)
Affected packages:
- @fluidframework/container-runtime
- @fluidframework/runtime-definitions
[⬆️ Table of contents](#contents)
## 🌳 SharedTree DDS Changes
### Provide more comprehensive replacement to the `commitApplied` event ([#22977](https://github.com/microsoft/FluidFramework/issues/22977))

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

@ -15,12 +15,7 @@ import {
type IEvent,
} from "@fluidframework/core-interfaces";
import { type IFluidHandleInternal } from "@fluidframework/core-interfaces/internal";
import {
assert,
isPromiseLike,
LazyPromise,
unreachableCase,
} from "@fluidframework/core-utils/internal";
import { assert, LazyPromise, unreachableCase } from "@fluidframework/core-utils/internal";
import { IClientDetails, IQuorumClients } from "@fluidframework/driver-definitions";
import {
IDocumentStorageService,
@ -60,7 +55,6 @@ import {
IInboundSignalMessage,
type IPendingMessagesState,
type IRuntimeMessageCollection,
type IFluidDataStoreFactory,
} from "@fluidframework/runtime-definitions/internal";
import {
addBlobToSummary,
@ -71,7 +65,6 @@ import {
LoggingError,
MonitoringContext,
ThresholdCounter,
UsageError,
createChildMonitoringContext,
extractSafePropertiesFromMessage,
generateStack,
@ -524,43 +517,6 @@ export abstract class FluidDataStoreContext
return factory;
}
createChildDataStore<T extends IFluidDataStoreFactory>(
childFactory: T,
): ReturnType<Exclude<T["createDataStore"], undefined>> {
const maybe = this.registry?.get(childFactory.type);
const isUndefined = maybe === undefined;
const isPromise = isPromiseLike(maybe);
const diffInstance = isPromise || maybe?.IFluidDataStoreFactory !== childFactory;
if (isUndefined || isPromise || diffInstance) {
throw new UsageError(
"The provided factory instance must be synchronously available as a child of this datastore",
{ isUndefined, isPromise, diffInstance },
);
}
if (childFactory?.createDataStore === undefined) {
throw new UsageError("createDataStore must exist on the provided factory", {
noCreateDataStore: true,
});
}
const context = this._containerRuntime.createDetachedDataStore([
...this.packagePath,
childFactory.type,
]);
assert(
context instanceof LocalDetachedFluidDataStoreContext,
0xa81 /* must be a LocalDetachedFluidDataStoreContext */,
);
const created = childFactory.createDataStore(context) as ReturnType<
Exclude<T["createDataStore"], undefined>
>;
context.unsafe_AttachRuntimeSync(created.runtime);
return created;
}
private async realizeCore(existing: boolean) {
const details = await this.getInitialSnapshotDetails();
// Base snapshot is the baseline where pending ops are applied to.
@ -1472,24 +1428,6 @@ export class LocalDetachedFluidDataStoreContext
return this.channelToDataStoreFn(await this.channelP);
}
/**
* This method provides a synchronous path for binding a runtime to the context.
*
* Due to its synchronous nature, it is unable to validate that the runtime
* represents a datastore which is instantiable by remote clients. This could
* happen if the runtime's package path does not return a factory when looked up
* in the container runtime's registry, or if the runtime's entrypoint is not
* properly initialized. As both of these validation's are asynchronous to preform.
*
* If used incorrectly, this function can result in permanent data corruption.
*/
public unsafe_AttachRuntimeSync(channel: IFluidDataStoreChannel) {
this.channelP = Promise.resolve(channel);
this.processPendingOps(channel);
this.completeBindingRuntime(channel);
return this.channelToDataStoreFn(channel);
}
public async getInitialSnapshotDetails(): Promise<ISnapshotDetails> {
if (this.detachedRuntimeCreation) {
throw new Error(

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

@ -33,12 +33,7 @@ export class FluidDataStoreRegistry implements IFluidDataStoreRegistry {
}
}
public get(
name: string,
):
| Promise<FluidDataStoreRegistryEntry | undefined>
| FluidDataStoreRegistryEntry
| undefined {
public async get(name: string): Promise<FluidDataStoreRegistryEntry | undefined> {
if (this.map.has(name)) {
return this.map.get(name);
}

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

@ -1,172 +0,0 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/
import { strict as assert } from "assert";
import { FluidErrorTypes } from "@fluidframework/core-interfaces/internal";
import { LazyPromise } from "@fluidframework/core-utils/internal";
import { IDocumentStorageService } from "@fluidframework/driver-definitions/internal";
import {
IFluidDataStoreChannel,
IFluidDataStoreFactory,
IFluidDataStoreRegistry,
IFluidParentContext,
type NamedFluidDataStoreRegistryEntries,
type IContainerRuntimeBase,
type ISummarizerNodeWithGC,
} from "@fluidframework/runtime-definitions/internal";
import { isFluidError } from "@fluidframework/telemetry-utils/internal";
import { MockFluidDataStoreRuntime } from "@fluidframework/test-runtime-utils/internal";
import {
FluidDataStoreContext,
LocalDetachedFluidDataStoreContext,
} from "../dataStoreContext.js";
describe("createChildDataStore", () => {
const throwNYI = () => {
throw new Error("Method not implemented.");
};
const testContext = class TestContext extends FluidDataStoreContext {
protected pkg = ["ParentDataStore"];
public registry: IFluidDataStoreRegistry | undefined;
public getInitialSnapshotDetails = throwNYI;
public setAttachState = throwNYI;
public getAttachSummary = throwNYI;
public getAttachGCData = throwNYI;
protected channel = new Proxy({} as any as IFluidDataStoreChannel, { get: throwNYI });
protected channelP = new LazyPromise(async () => this.channel);
};
const createRegistry = (
namedEntries?: NamedFluidDataStoreRegistryEntries,
): IFluidDataStoreRegistry => ({
get IFluidDataStoreRegistry() {
return this;
},
// eslint-disable-next-line @typescript-eslint/promise-function-async
get(name) {
return new Map(namedEntries).get(name);
},
});
const createContext = (namedEntries?: NamedFluidDataStoreRegistryEntries) => {
const registry = createRegistry(namedEntries);
const createSummarizerNodeFn = () =>
new Proxy({} as any as ISummarizerNodeWithGC, { get: throwNYI });
const storage = new Proxy({} as any as IDocumentStorageService, { get: throwNYI });
const parentContext = {
clientDetails: {
capabilities: { interactive: true },
},
containerRuntime: {
createDetachedDataStore(pkg, loadingGroupId) {
return new LocalDetachedFluidDataStoreContext({
channelToDataStoreFn: (channel) => ({
entryPoint: channel.entryPoint,
trySetAlias: throwNYI,
}),
createSummarizerNodeFn,
id: "child",
makeLocallyVisibleFn: throwNYI,
parentContext,
pkg,
scope: {},
snapshotTree: undefined,
storage,
loadingGroupId,
});
},
} satisfies Partial<IContainerRuntimeBase> as unknown as IContainerRuntimeBase,
} satisfies Partial<IFluidParentContext> as unknown as IFluidParentContext;
const context = new testContext(
{
createSummarizerNodeFn,
id: "parent",
parentContext,
scope: {},
storage,
},
false,
false,
throwNYI,
);
context.registry = registry;
return context;
};
const createFactory = (
createDataStore?: IFluidDataStoreFactory["createDataStore"],
): IFluidDataStoreFactory => ({
type: "ChildDataStore",
get IFluidDataStoreFactory() {
return this;
},
instantiateDataStore: throwNYI,
createDataStore,
});
it("Child factory does not support synchronous creation", async () => {
const factory = createFactory();
const context = createContext([[factory.type, factory]]);
try {
context.createChildDataStore(factory);
assert.fail("should fail");
} catch (e) {
assert(isFluidError(e));
assert(e.errorType === FluidErrorTypes.usageError);
assert(e.getTelemetryProperties().noCreateDataStore === true);
}
});
it("Child factory not registered", async () => {
const factory = createFactory();
const context = createContext();
try {
context.createChildDataStore(factory);
assert.fail("should fail");
} catch (e) {
assert(isFluidError(e));
assert(e.errorType === FluidErrorTypes.usageError);
assert(e.getTelemetryProperties().isUndefined === true);
}
});
it("Child factory is a promise", async () => {
const factory = createFactory();
const context = createContext([[factory.type, Promise.resolve(factory)]]);
try {
context.createChildDataStore(factory);
assert.fail("should fail");
} catch (e) {
assert(isFluidError(e));
assert(e.errorType === FluidErrorTypes.usageError);
assert(e.getTelemetryProperties().isPromise === true);
}
});
it("Child factory is a different instance", async () => {
const factory = createFactory();
const context = createContext([[factory.type, createFactory()]]);
try {
context.createChildDataStore(factory);
assert.fail("should fail");
} catch (e) {
assert(isFluidError(e));
assert(e.errorType === FluidErrorTypes.usageError);
assert(e.getTelemetryProperties().diffInstance === true);
}
});
it("createChildDataStore", async () => {
const factory = createFactory(() => ({ runtime: new MockFluidDataStoreRuntime() }));
const context = createContext([[factory.type, factory]]);
context.createChildDataStore(factory);
});
});

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

@ -150,7 +150,6 @@ export interface IFluidDataStoreChannel extends IDisposable {
export interface IFluidDataStoreContext extends IFluidParentContext {
// (undocumented)
readonly baseSnapshot: ISnapshotTree | undefined;
createChildDataStore?<T extends IFluidDataStoreFactory>(childFactory: T): ReturnType<Exclude<T["createDataStore"], undefined>>;
// @deprecated (undocumented)
readonly createProps?: any;
// @deprecated (undocumented)
@ -171,9 +170,6 @@ export const IFluidDataStoreFactory: keyof IProvideFluidDataStoreFactory;
// @alpha
export interface IFluidDataStoreFactory extends IProvideFluidDataStoreFactory {
createDataStore?(context: IFluidDataStoreContext): {
readonly runtime: IFluidDataStoreChannel;
};
instantiateDataStore(context: IFluidDataStoreContext, existing: boolean): Promise<IFluidDataStoreChannel>;
type: string;
}
@ -183,7 +179,8 @@ export const IFluidDataStoreRegistry: keyof IProvideFluidDataStoreRegistry;
// @alpha
export interface IFluidDataStoreRegistry extends IProvideFluidDataStoreRegistry {
get(name: string): Promise<FluidDataStoreRegistryEntry | undefined> | FluidDataStoreRegistryEntry | undefined;
// (undocumented)
get(name: string): Promise<FluidDataStoreRegistryEntry | undefined>;
}
// @alpha
@ -378,17 +375,11 @@ export interface LocalAttributionKey {
}
// @alpha
export type NamedFluidDataStoreRegistryEntries = Iterable<NamedFluidDataStoreRegistryEntry2>;
export type NamedFluidDataStoreRegistryEntries = Iterable<NamedFluidDataStoreRegistryEntry>;
// @alpha
export type NamedFluidDataStoreRegistryEntry = [string, Promise<FluidDataStoreRegistryEntry>];
// @alpha
export type NamedFluidDataStoreRegistryEntry2 = [
string,
Promise<FluidDataStoreRegistryEntry> | FluidDataStoreRegistryEntry
];
// @alpha
export interface OpAttributionKey {
seq: number;

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

@ -28,10 +28,7 @@ import type {
} from "@fluidframework/driver-definitions/internal";
import type { IIdCompressor } from "@fluidframework/id-compressor";
import type {
IFluidDataStoreFactory,
IProvideFluidDataStoreFactory,
} from "./dataStoreFactory.js";
import type { IProvideFluidDataStoreFactory } from "./dataStoreFactory.js";
import type { IProvideFluidDataStoreRegistry } from "./dataStoreRegistry.js";
import type {
IGarbageCollectionData,
@ -596,28 +593,6 @@ export interface IFluidDataStoreContext extends IFluidParentContext {
* and its children with the GC details from the previous summary.
*/
getBaseGCDetails(): Promise<IGarbageCollectionDetailsBase>;
/**
* Synchronously creates a detached child data store.
*
* The `createChildDataStoreSync` method allows for the synchronous creation of a detached child data store. This is particularly
* useful in scenarios where immediate availability of the child data store is required, such as during the initialization
* of a parent data store, or when creation is in response to synchronous user input.
*
* In order for this function to succeed:
* 1. The parent data store's factory must also be an `IFluidDataStoreRegistry`.
* 2. The parent data store's registry must include the same instance as the provided child factory.
* 3. The parent data store's registry must synchronously provide the child factory.
* 4. The child factory must implement the `createDataStore` method.
*
* These invariants ensure that the child data store can also be created by a remote client running the same code as this client.
*
* @param childFactory - The factory of the data store to be created.
* @returns The created data store channel.
*/
createChildDataStore?<T extends IFluidDataStoreFactory>(
childFactory: T,
): ReturnType<Exclude<T["createDataStore"], undefined>>;
}
/**

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

@ -21,63 +21,24 @@ export interface IProvideFluidDataStoreFactory {
}
/**
* The `IFluidDataStoreFactory` interface is responsible for creating data stores.
* A data store is a component that manages a specific set of data and its operations.
* It encapsulates the logic for data management, synchronization, and interaction
* with other components within a Fluid container.
*
* Data stores are fundamental building blocks in the Fluid Framework. They are used
* to store and manage state, handle operations, and provide APIs for interacting
* with the data. Each data store type is associated with a unique identifier (its `type` member)
* and is typically provided to consumers through a data store registry.
*
* The factory is responsible for creating new instances of data stores and loading existing ones.
* The factory ensures that the data store is correctly initialized.
*
* IFluidDataStoreFactory create data stores. It is associated with an identifier (its `type` member)
* and usually provided to consumers using this mapping through a data store registry.
* @legacy
* @alpha
*/
export interface IFluidDataStoreFactory extends IProvideFluidDataStoreFactory {
/**
* Uniquely identifies the type of data store created by this factory.
* String that uniquely identifies the type of data store created by this factory.
*/
type: string;
/**
* Asynchronously generates the runtime for the data store from the given context.
* Once created, the data store should be bound to the context.
*
* This method supports both creation and loading paths. It is important to differentiate
* between the two based on the `existing` parameter:
* - When `existing` is false, this method creates a new data store.
* - When `existing` is true, it loads a pre-existing data store.
*
* @param context - The context for the data store, providing necessary information and services.
* @param existing - A boolean indicating whether the data store is being instantiated from an existing file.
* @returns A promise that resolves to the created data store channel.
* Generates runtime for the data store from the data store context. Once created should be bound to the context.
* @param context - Context for the data store.
* @param existing - If instantiating from an existing file.
*/
instantiateDataStore(
context: IFluidDataStoreContext,
existing: boolean,
): Promise<IFluidDataStoreChannel>;
/**
* Synchronously creates a new runtime for a new data store from the provided context.
*
* This method enables a synchronous creation path. Specifically, if this factory is registered
* as a child factory in another data store's registry, and the registry synchronously provides
* this factory, it becomes eligible for synchronous creation via the parent data store's context.
* After creation, all subsequent loads of a data store created through this method will utilize
* the asynchronous `instantiateDataStore` method on this factory.
*
* Note: This method is optional. Not all data stores can or will support a synchronous creation path,
* as being synchronous imposes limitations on the capabilities that can be used. Generally, this
* creation path should only be implemented when synchronous creation is necessary.
*
* @param context - The context for the data store, providing the necessary information and services.
* @returns An object containing the runtime of the created data store channel.
*/
createDataStore?(context: IFluidDataStoreContext): {
readonly runtime: IFluidDataStoreChannel;
};
}

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

@ -21,23 +21,12 @@ export type FluidDataStoreRegistryEntry = Readonly<
* @alpha
*/
export type NamedFluidDataStoreRegistryEntry = [string, Promise<FluidDataStoreRegistryEntry>];
/**
* An associated pair of an identifier and registry entry. Registry entries
* may be dynamically loaded.
* @legacy
* @alpha
*/
export type NamedFluidDataStoreRegistryEntry2 = [
string,
Promise<FluidDataStoreRegistryEntry> | FluidDataStoreRegistryEntry,
];
/**
* An iterable identifier/registry entry pair list
* @legacy
* @alpha
*/
export type NamedFluidDataStoreRegistryEntries = Iterable<NamedFluidDataStoreRegistryEntry2>;
export type NamedFluidDataStoreRegistryEntries = Iterable<NamedFluidDataStoreRegistryEntry>;
/**
* @legacy
@ -61,20 +50,5 @@ export interface IProvideFluidDataStoreRegistry {
* @alpha
*/
export interface IFluidDataStoreRegistry extends IProvideFluidDataStoreRegistry {
/**
* Retrieves a data store registry entry by its identifier.
*
* @remarks
* The `get` function plays a crucial role in the lifecycle of a data store by providing access to the registry entry
* associated with a given identifier. This registry entry can then be used to create or load a data store.
*
* @param name - The unique identifier of the data store registry entry to retrieve.
* @returns A promise that resolves to the data store registry entry, or the entry itself, or undefined if not found.
*/
get(
name: string,
):
| Promise<FluidDataStoreRegistryEntry | undefined>
| FluidDataStoreRegistryEntry
| undefined;
get(name: string): Promise<FluidDataStoreRegistryEntry | undefined>;
}

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

@ -30,7 +30,6 @@ export type {
IProvideFluidDataStoreRegistry,
NamedFluidDataStoreRegistryEntries,
NamedFluidDataStoreRegistryEntry,
NamedFluidDataStoreRegistryEntry2,
} from "./dataStoreRegistry.js";
export { IFluidDataStoreRegistry } from "./dataStoreRegistry.js";
export type {

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

@ -1648,6 +1648,5 @@ export const shortCodeMap = {
"0xa7d": "Expected at least two types",
"0xa7e": "Expected at least two types",
"0xa7f": "Delta manager does not have inbound/outbound queues.",
"0xa80": "Invalid delta manager",
"0xa81": "must be a LocalDetachedFluidDataStoreContext"
"0xa80": "Invalid delta manager"
};

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

@ -3,15 +3,10 @@
* Licensed under the MIT License.
*/
import { DataObject } from "@fluidframework/aqueduct/internal";
import { DataObjectFactory } from "@fluidframework/aqueduct/internal";
import {
AttachState,
type IRuntimeFactory,
} from "@fluidframework/container-definitions/internal";
import type { IRuntimeFactory } from "@fluidframework/container-definitions/internal";
import { waitContainerToCatchUp } from "@fluidframework/container-loader/internal";
import { loadContainerRuntime } from "@fluidframework/container-runtime/internal";
import { IFluidHandle, type FluidObject } from "@fluidframework/core-interfaces/internal";
import type { FluidObject } from "@fluidframework/core-interfaces/internal";
import { assert } from "@fluidframework/core-utils/internal";
import { FluidDataStoreRuntime } from "@fluidframework/datastore/internal";
import {
@ -20,9 +15,10 @@ import {
} from "@fluidframework/datastore-definitions/internal";
import { ISharedMap, SharedMap } from "@fluidframework/map/internal";
import type {
IFluidDataStoreChannel,
FluidDataStoreRegistryEntry,
IFluidDataStoreContext,
IFluidDataStoreFactory,
IFluidDataStoreRegistry,
} from "@fluidframework/runtime-definitions/internal";
import { isFluidHandle } from "@fluidframework/runtime-utils/internal";
import { LocalDeltaConnectionServer } from "@fluidframework/server-local-server";
@ -32,58 +28,80 @@ import { createLoader } from "../utils.js";
const mapFactory = SharedMap.getFactory();
const sharedObjectRegistry = new Map<string, IChannelFactory>([[mapFactory.type, mapFactory]]);
/**
* This is the child datastore that will be created synchronously
*/
class ChildDataStore {
public static create(runtime: IFluidDataStoreRuntime) {
// a data store object which can create another instance of it self as synchronously as possible
class DataStoreWithSyncCreate {
public static create(context: IFluidDataStoreContext, runtime: IFluidDataStoreRuntime) {
const root = SharedMap.create(runtime, "root");
root.bindToContext();
return new ChildDataStore(runtime, root);
return new DataStoreWithSyncCreate(context, runtime, root);
}
public static async load(runtime: IFluidDataStoreRuntime) {
public static async load(context: IFluidDataStoreContext, runtime: IFluidDataStoreRuntime) {
const root = (await runtime.getChannel("root")) as unknown as ISharedMap;
return new ChildDataStore(runtime, root);
return new DataStoreWithSyncCreate(context, runtime, root);
}
public static readonly type = "DataStoreWithSyncCreate";
private constructor(
private readonly context: IFluidDataStoreContext,
private readonly runtime: IFluidDataStoreRuntime,
private readonly sharedMap: SharedMap,
public readonly sharedMap: ISharedMap,
) {}
get ChildDataStore() {
get DataStoreWithSyncCreate() {
return this;
}
public setProperty(key: string, value: string | number) {
this.sharedMap.set(key, value);
}
public getProperty(key: string): string | number | undefined {
return this.sharedMap.get(key);
}
get handle() {
return this.runtime.entryPoint;
}
createAnother() {
// creates a detached context with a factory who's package path is the same
// as the current datastore, but with another copy of its own type.
const context = this.context.containerRuntime.createDetachedDataStore([
...this.context.packagePath,
DataStoreWithSyncCreate.type,
]);
const runtime = new FluidDataStoreRuntime(
context,
sharedObjectRegistry,
false,
async () => dataStore,
);
const dataStore = DataStoreWithSyncCreate.create(context, runtime);
const attachRuntimeP = context.attachRuntime(
DataStoreWithSyncCreateFactory.instance,
runtime,
);
return { dataStore, attachRuntimeP };
}
}
/**
* This is the child datastore factory. It must implement
* createDataStore to support synchronous creation.
* instantiateDataStore will continue to be used after creation
* to load the datastore.
*/
class ChildDataStoreFactory implements IFluidDataStoreFactory {
static readonly instance = new ChildDataStoreFactory();
// a simple datastore factory that is also a registry so that it can create instances of itself
class DataStoreWithSyncCreateFactory
implements IFluidDataStoreFactory, IFluidDataStoreRegistry
{
static readonly instance = new DataStoreWithSyncCreateFactory();
public readonly type = DataStoreWithSyncCreate.type;
private constructor() {}
get IFluidDataStoreRegistry() {
return this;
}
async get(name: string): Promise<FluidDataStoreRegistryEntry | undefined> {
// this factory is also a registry, which only supports creating itself
if (name === this.type) {
return this;
}
}
get IFluidDataStoreFactory() {
return this;
}
public readonly type = "ChildDataStore";
async instantiateDataStore(context, existing) {
const runtime: FluidDataStoreRuntime = new FluidDataStoreRuntime(
context,
@ -91,73 +109,14 @@ class ChildDataStoreFactory implements IFluidDataStoreFactory {
existing,
async () => dataStore,
);
const dataStore = existing ? ChildDataStore.load(runtime) : ChildDataStore.create(runtime);
const dataStore = existing
? DataStoreWithSyncCreate.load(context, runtime)
: DataStoreWithSyncCreate.create(context, runtime);
return runtime;
}
createDataStore(context: IFluidDataStoreContext): {
runtime: IFluidDataStoreChannel;
entrypoint: ChildDataStore;
} {
const runtime = new FluidDataStoreRuntime(
context,
sharedObjectRegistry,
false,
async () => entrypoint,
);
const entrypoint = ChildDataStore.create(runtime);
return { runtime, entrypoint };
}
}
/**
* This is the parent DataObject, which is also a datastore. It has a
* synchronous method to create child datastores, which could be called
* in response to synchronous user input, like a key press.
*/
class ParentDataObject extends DataObject {
get ParentDataObject() {
return this;
}
protected override async initializingFirstTime(): Promise<void> {
// create synchronously during initialization
this.createChild("parentCreation");
}
createChild(name: string): ChildDataStore {
assert(
this.context.createChildDataStore !== undefined,
"this.context.createChildDataStore",
);
// creates a detached context with a factory who's package path is the same
// as the current datastore, but with another copy of its own type.
const { entrypoint } = this.context.createChildDataStore(ChildDataStoreFactory.instance);
const dir = this.root.createSubDirectory("children");
dir.set(name, entrypoint.handle);
entrypoint.setProperty("childValue", name);
return entrypoint;
}
getChild(name: string): IFluidHandle<ChildDataStore> | undefined {
const dir = this.root.getSubDirectory("children");
return dir?.get<IFluidHandle<ChildDataStore>>(name);
}
}
/**
* This is the parent DataObjects factory. It specifies the child data stores
* factory in a sub-registry. This is requires for synchronous creation of the child.
*/
const parentDataObjectFactory = new DataObjectFactory(
"ParentDataObject",
ParentDataObject,
undefined,
{},
[[ChildDataStoreFactory.instance.type, ChildDataStoreFactory.instance]],
);
// a simple container runtime factory with a single datastore aliased as default.
// the default datastore is also returned as the entrypoint
const runtimeFactory: IRuntimeFactory = {
@ -170,16 +129,14 @@ const runtimeFactory: IRuntimeFactory = {
existing,
registryEntries: [
[
parentDataObjectFactory.type,
// the parent is still async in the container registry
// this allows things like code splitting for dynamic loading
Promise.resolve(parentDataObjectFactory),
DataStoreWithSyncCreateFactory.instance.type,
Promise.resolve(DataStoreWithSyncCreateFactory.instance),
],
],
provideEntryPoint: async (rt) => {
const maybeRoot = await rt.getAliasedDataStoreEntryPoint("default");
if (maybeRoot === undefined) {
const ds = await rt.createDataStore(parentDataObjectFactory.type);
const ds = await rt.createDataStore(DataStoreWithSyncCreate.type);
await ds.trySetAlias("default");
}
const root = await rt.getAliasedDataStoreEntryPoint("default");
@ -191,7 +148,7 @@ const runtimeFactory: IRuntimeFactory = {
};
describe("Scenario Test", () => {
it("Synchronously create child data store", async () => {
it("Synchronously create nested data store", async () => {
const deltaConnectionServer = LocalDeltaConnectionServer.create();
const { loader, codeDetails, urlResolver } = createLoader({
@ -201,66 +158,53 @@ describe("Scenario Test", () => {
const container = await loader.createDetachedContainer(codeDetails);
await container.attach(urlResolver.createCreateNewRequest("test"));
const url = await container.getAbsoluteUrl("");
assert(url !== undefined, "container must have url");
{
const entrypoint: FluidObject<ParentDataObject> = await container.getEntryPoint();
const entrypoint: FluidObject<DataStoreWithSyncCreate> = await container.getEntryPoint();
assert(
entrypoint.ParentDataObject !== undefined,
"container entrypoint must be ParentDataStore",
entrypoint.DataStoreWithSyncCreate !== undefined,
"container entrypoint must be DataStoreWithSyncCreate",
);
// create a child while detached
entrypoint.ParentDataObject.createChild("detachedChildInstance");
const { attachRuntimeP, dataStore } = entrypoint.DataStoreWithSyncCreate.createAnother();
const attachP = container.attach(urlResolver.createCreateNewRequest("test"));
dataStore.sharedMap.set("childValue", "childValue");
if (container.attachState === AttachState.Attached) {
await new Promise<void>((resolve) => container.once("attaching", () => resolve()));
}
// create a child while attaching
entrypoint.ParentDataObject.createChild("attachingChildInstance");
await attachP;
// create a child once attached
entrypoint.ParentDataObject.createChild("attachedChildInstance");
// can we make this synchronous
await attachRuntimeP;
entrypoint.DataStoreWithSyncCreate.sharedMap.set("childInstance", dataStore.handle);
if (container.isDirty) {
await new Promise<void>((resolve) => container.once("saved", () => resolve()));
}
container.dispose();
}
const url = await container.getAbsoluteUrl("");
assert(url !== undefined, "container must have url");
container.dispose();
{
const container2 = await loader.resolve({ url });
await waitContainerToCatchUp(container2);
const entrypoint: FluidObject<ParentDataObject> = await container2.getEntryPoint();
const entrypoint: FluidObject<DataStoreWithSyncCreate> =
await container2.getEntryPoint();
assert(
entrypoint.ParentDataObject !== undefined,
"container2 entrypoint must be ParentDataStore",
entrypoint.DataStoreWithSyncCreate !== undefined,
"container2 entrypoint must be DataStoreWithSyncCreate",
);
for (const childKey of [
"parentCreation",
"detachedChildInstance",
"attachingChildInstance",
"attachedChildInstance",
]) {
const childHandle = entrypoint.ParentDataObject.getChild(childKey);
assert(childHandle !== undefined, `${childKey} must be defined`);
assert(isFluidHandle(childHandle), `${childKey} should be a handle`);
const child = (await childHandle.get()) as FluidObject<ChildDataStore>;
assert(child.ChildDataStore !== undefined, `${childKey} must be ChildDataStore`);
assert(
child.ChildDataStore.getProperty("childValue") === childKey,
"unexpected childValue",
);
}
const childHandle = entrypoint.DataStoreWithSyncCreate.sharedMap.get("childInstance");
assert(isFluidHandle(childHandle), "childInstance should be a handle");
const child = (await childHandle.get()) as FluidObject<DataStoreWithSyncCreate>;
assert(
child.DataStoreWithSyncCreate !== undefined,
"child must be DataStoreWithSyncCreate",
);
assert(
child.DataStoreWithSyncCreate.sharedMap.get("childValue") === "childValue",
"unexpected childValue",
);
container2.dispose();
}
});