Enable Synchronous Child Datastore Creation (#23143)

## 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

---------

Co-authored-by: jzaffiro <110866475+jzaffiro@users.noreply.github.com>
Co-authored-by: Joshua Smithrud <54606601+Josmithr@users.noreply.github.com>
This commit is contained in:
Tony Murphy 2024-11-19 11:33:14 -08:00 коммит произвёл GitHub
Родитель c11bc168c4
Коммит 3426b434df
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
10 изменённых файлов: 557 добавлений и 101 удалений

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

@ -0,0 +1,65 @@
---
"@fluidframework/container-runtime": minor
"@fluidframework/runtime-definitions": minor
---
---
"section": feature
---
Enable Synchronous Child Datastore Creation
## 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 {
createChild(name: string): ChildDataStore {
assert(
this.context.createChildDataStore !== undefined,
"this.context.createChildDataStore",
);
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 following test:
https://github.com/microsoft/FluidFramework/blob/main/packages/test/local-server-tests/src/test/synchronousDataStoreCreation.spec.ts

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

@ -55,6 +55,7 @@ import {
IInboundSignalMessage,
type IPendingMessagesState,
type IRuntimeMessageCollection,
type IFluidDataStoreFactory,
} from "@fluidframework/runtime-definitions/internal";
import {
addBlobToSummary,
@ -65,6 +66,7 @@ import {
LoggingError,
MonitoringContext,
ThresholdCounter,
UsageError,
createChildMonitoringContext,
extractSafePropertiesFromMessage,
generateStack,
@ -496,7 +498,7 @@ export abstract class FluidDataStoreContext
this.rejectDeferredRealize("No registry for package", lastPkg, packages);
}
lastPkg = pkg;
entry = await registry.get(pkg);
entry = registry.getSync?.(pkg) ?? (await registry.get(pkg));
if (!entry) {
this.rejectDeferredRealize(
"Registry does not contain entry for the package",
@ -517,6 +519,42 @@ export abstract class FluidDataStoreContext
return factory;
}
createChildDataStore<T extends IFluidDataStoreFactory>(
childFactory: T,
): ReturnType<Exclude<T["createDataStore"], undefined>> {
const maybe = this.registry?.getSync?.(childFactory.type);
const isUndefined = maybe === undefined;
const diffInstance = maybe?.IFluidDataStoreFactory !== childFactory;
if (isUndefined || diffInstance) {
throw new UsageError(
"The provided factory instance must be synchronously available as a child of this datastore",
{ isUndefined, 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,
"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.
@ -1428,6 +1466,24 @@ 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(

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

@ -3,6 +3,7 @@
* Licensed under the MIT License.
*/
import { isPromiseLike } from "@fluidframework/core-utils/internal";
import {
FluidDataStoreRegistryEntry,
IFluidDataStoreRegistry,
@ -40,4 +41,13 @@ export class FluidDataStoreRegistry implements IFluidDataStoreRegistry {
return undefined;
}
public getSync(name: string): FluidDataStoreRegistryEntry | undefined {
const entry = this.map.get(name);
if (!isPromiseLike(entry)) {
return entry;
}
return undefined;
}
}

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

@ -0,0 +1,161 @@
/*!
* 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 { isPromiseLike, 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;
},
async get(name) {
return new Map(namedEntries).get(name);
},
getSync(name) {
const entry = new Map(namedEntries).get(name);
return isPromiseLike(entry) ? undefined : entry;
},
});
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 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,6 +150,7 @@ 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)
@ -170,6 +171,9 @@ 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;
}
@ -179,8 +183,8 @@ export const IFluidDataStoreRegistry: keyof IProvideFluidDataStoreRegistry;
// @alpha
export interface IFluidDataStoreRegistry extends IProvideFluidDataStoreRegistry {
// (undocumented)
get(name: string): Promise<FluidDataStoreRegistryEntry | undefined>;
getSync?(name: string): FluidDataStoreRegistryEntry | undefined;
}
// @alpha
@ -375,11 +379,17 @@ export interface LocalAttributionKey {
}
// @alpha
export type NamedFluidDataStoreRegistryEntries = Iterable<NamedFluidDataStoreRegistryEntry>;
export type NamedFluidDataStoreRegistryEntries = Iterable<NamedFluidDataStoreRegistryEntry2>;
// @alpha
export type NamedFluidDataStoreRegistryEntry = [string, Promise<FluidDataStoreRegistryEntry>];
// @alpha
export type NamedFluidDataStoreRegistryEntry2 = [
string,
Promise<FluidDataStoreRegistryEntry> | FluidDataStoreRegistryEntry
];
// @alpha
export interface OpAttributionKey {
seq: number;

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

@ -28,7 +28,10 @@ import type {
} from "@fluidframework/driver-definitions/internal";
import type { IIdCompressor } from "@fluidframework/id-compressor";
import type { IProvideFluidDataStoreFactory } from "./dataStoreFactory.js";
import type {
IFluidDataStoreFactory,
IProvideFluidDataStoreFactory,
} from "./dataStoreFactory.js";
import type { IProvideFluidDataStoreRegistry } from "./dataStoreRegistry.js";
import type {
IGarbageCollectionData,
@ -593,6 +596,28 @@ 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 `createChildDataStore` 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 via the `getSync` method.
* 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,24 +21,65 @@ export interface IProvideFluidDataStoreFactory {
}
/**
* 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.
* 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.
*
* @legacy
* @alpha
*/
export interface IFluidDataStoreFactory extends IProvideFluidDataStoreFactory {
/**
* String that uniquely identifies the type of data store created by this factory.
* Uniquely identifies the type of data store created by this factory.
*/
type: string;
/**
* 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.
* Asynchronously generates the runtime for the data store from the given context.
* @remarks
* 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.
*/
instantiateDataStore(
context: IFluidDataStoreContext,
existing: boolean,
): Promise<IFluidDataStoreChannel>;
/**
* Synchronously creates a new runtime for a new data store from the provided context.
*
* @remarks
* 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,12 +21,23 @@ 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<NamedFluidDataStoreRegistryEntry>;
export type NamedFluidDataStoreRegistryEntries = Iterable<NamedFluidDataStoreRegistryEntry2>;
/**
* @legacy
@ -50,5 +61,27 @@ 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>;
/**
* Synchronously 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 The data store registry entry, or the entry itself, or undefined if not found.
*/
getSync?(name: string): FluidDataStoreRegistryEntry | undefined;
}

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

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

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

@ -3,10 +3,15 @@
* Licensed under the MIT License.
*/
import type { IRuntimeFactory } from "@fluidframework/container-definitions/internal";
import { DataObject } from "@fluidframework/aqueduct/internal";
import { DataObjectFactory } from "@fluidframework/aqueduct/internal";
import {
AttachState,
type IRuntimeFactory,
} from "@fluidframework/container-definitions/internal";
import { waitContainerToCatchUp } from "@fluidframework/container-loader/internal";
import { loadContainerRuntime } from "@fluidframework/container-runtime/internal";
import type { FluidObject } from "@fluidframework/core-interfaces/internal";
import { IFluidHandle, type FluidObject } from "@fluidframework/core-interfaces/internal";
import { assert } from "@fluidframework/core-utils/internal";
import { FluidDataStoreRuntime } from "@fluidframework/datastore/internal";
import {
@ -15,10 +20,9 @@ import {
} from "@fluidframework/datastore-definitions/internal";
import { ISharedMap, SharedMap } from "@fluidframework/map/internal";
import type {
FluidDataStoreRegistryEntry,
IFluidDataStoreChannel,
IFluidDataStoreContext,
IFluidDataStoreFactory,
IFluidDataStoreRegistry,
} from "@fluidframework/runtime-definitions/internal";
import { isFluidHandle } from "@fluidframework/runtime-utils/internal";
import { LocalDeltaConnectionServer } from "@fluidframework/server-local-server";
@ -28,80 +32,58 @@ import { createLoader } from "../utils.js";
const mapFactory = SharedMap.getFactory();
const sharedObjectRegistry = new Map<string, IChannelFactory>([[mapFactory.type, mapFactory]]);
// 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) {
/**
* This is the child datastore that will be created synchronously
*/
class ChildDataStore {
public static create(runtime: IFluidDataStoreRuntime) {
const root = SharedMap.create(runtime, "root");
root.bindToContext();
return new DataStoreWithSyncCreate(context, runtime, root);
return new ChildDataStore(runtime, root);
}
public static async load(context: IFluidDataStoreContext, runtime: IFluidDataStoreRuntime) {
public static async load(runtime: IFluidDataStoreRuntime) {
const root = (await runtime.getChannel("root")) as unknown as ISharedMap;
return new DataStoreWithSyncCreate(context, runtime, root);
return new ChildDataStore(runtime, root);
}
public static readonly type = "DataStoreWithSyncCreate";
private constructor(
private readonly context: IFluidDataStoreContext,
private readonly runtime: IFluidDataStoreRuntime,
public readonly sharedMap: ISharedMap,
private readonly sharedMap: SharedMap,
) {}
get DataStoreWithSyncCreate() {
get ChildDataStore() {
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 };
}
}
// 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;
/**
* 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();
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,
@ -109,14 +91,71 @@ class DataStoreWithSyncCreateFactory
existing,
async () => dataStore,
);
const dataStore = existing
? DataStoreWithSyncCreate.load(context, runtime)
: DataStoreWithSyncCreate.create(context, runtime);
const dataStore = existing ? ChildDataStore.load(runtime) : ChildDataStore.create(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",
);
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 = {
@ -129,14 +168,16 @@ const runtimeFactory: IRuntimeFactory = {
existing,
registryEntries: [
[
DataStoreWithSyncCreateFactory.instance.type,
Promise.resolve(DataStoreWithSyncCreateFactory.instance),
parentDataObjectFactory.type,
// the parent is still async in the container registry
// this allows things like code splitting for dynamic loading
Promise.resolve(parentDataObjectFactory),
],
],
provideEntryPoint: async (rt) => {
const maybeRoot = await rt.getAliasedDataStoreEntryPoint("default");
if (maybeRoot === undefined) {
const ds = await rt.createDataStore(DataStoreWithSyncCreate.type);
const ds = await rt.createDataStore(parentDataObjectFactory.type);
await ds.trySetAlias("default");
}
const root = await rt.getAliasedDataStoreEntryPoint("default");
@ -148,7 +189,7 @@ const runtimeFactory: IRuntimeFactory = {
};
describe("Scenario Test", () => {
it("Synchronously create nested data store", async () => {
it("Synchronously create child data store", async () => {
const deltaConnectionServer = LocalDeltaConnectionServer.create();
const { loader, codeDetails, urlResolver } = createLoader({
@ -158,53 +199,66 @@ 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<DataStoreWithSyncCreate> = await container.getEntryPoint();
const entrypoint: FluidObject<ParentDataObject> = await container.getEntryPoint();
assert(
entrypoint.DataStoreWithSyncCreate !== undefined,
"container entrypoint must be DataStoreWithSyncCreate",
entrypoint.ParentDataObject !== undefined,
"container entrypoint must be ParentDataStore",
);
const { attachRuntimeP, dataStore } = entrypoint.DataStoreWithSyncCreate.createAnother();
// create a child while detached
entrypoint.ParentDataObject.createChild("detachedChildInstance");
dataStore.sharedMap.set("childValue", "childValue");
const attachP = container.attach(urlResolver.createCreateNewRequest("test"));
// can we make this synchronous
await attachRuntimeP;
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");
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<DataStoreWithSyncCreate> =
await container2.getEntryPoint();
const entrypoint: FluidObject<ParentDataObject> = await container2.getEntryPoint();
assert(
entrypoint.DataStoreWithSyncCreate !== undefined,
"container2 entrypoint must be DataStoreWithSyncCreate",
entrypoint.ParentDataObject !== undefined,
"container2 entrypoint must be ParentDataStore",
);
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",
);
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",
);
}
container2.dispose();
}
});