From 3a67577c8c24c73c38cfe5c2618c5bf1c2b795c7 Mon Sep 17 00:00:00 2001 From: Vlad Sudzilouski Date: Fri, 31 Jul 2020 11:16:40 -0700 Subject: [PATCH 01/24] Get rid of createDataStoreWithRealizationFn() completely. --- BREAKING.md | 7 ++-- .../experimental/table-document/src/slice.ts | 4 +-- .../experimental/todo/src/TextBox/TextBox.tsx | 4 +-- .../todo/src/TodoItem/TodoItem.tsx | 4 +-- .../sharedComponentFactory.ts | 30 +++++----------- .../aqueduct/src/components/index.ts | 1 + .../src/components/primedComponent.ts | 8 ++--- .../src/components/sharedComponent.ts | 32 +++++------------ .../src/fluidComponent/syncedComponent.ts | 2 +- .../container-runtime/src/componentContext.ts | 18 +++------- .../container-runtime/src/containerRuntime.ts | 34 ++----------------- .../src/componentContext.ts | 19 ++++------- .../src/componentFactory.ts | 8 ++++- 13 files changed, 50 insertions(+), 121 deletions(-) diff --git a/BREAKING.md b/BREAKING.md index 926d19cbbc9c..0e3664544a28 100644 --- a/BREAKING.md +++ b/BREAKING.md @@ -4,7 +4,7 @@ - [IComponentContextLegacy is removed](#IComponentContextLegacy-is-removed) - [IContainerRuntimeBase._createDataStoreWithProps() is removed](#IContainerRuntimeBase._createDataStoreWithProps-is-removed) - [_createDataStore() APIs are removed](#_createDataStore-APIs-are-removed) -- [createDataStoreWithRealizationFn() APIs moved](#createDataStoreWithRealizationFn()-APIs-moved) +- [createDataStoreWithRealizationFn() APIs are removed](#createDataStoreWithRealizationFn()-APIs-are-removed) ### IComponentContextLegacy is removed Deprecated in 0.18, removed. @@ -19,9 +19,10 @@ Please switch to using one of the following APIs: 1. `IContainerRuntime.createRootDataStore()` - data store created that way is automatically bound to container. It will immediately be visible to remote clients (when/if container is attached). Such data stores are never garbage collected. Note that this API is on `IContainerRuntime` interface, which is not directly accessible to data stores. The intention is that only container owners are creating roots. 2. `IContainerRuntimeBase.createDataStore()` - creates data store that is not bound to container. In order for this store to be bound to container (and thus be observable on remote clients), ensure that handle to it (or any of its objects / DDS) is stored into any other DDS that is already bound to container. In other words, newly created data store has to be reachable (there has to be a path) from some root data store in container. If, in future, such data store becomes unreachable from one of the roots, it will be garbage collected (implementation pending). -### createDataStoreWithRealizationFn() APIs moved +### createDataStoreWithRealizationFn() APIs are removed Removed from IFluidDataStoreContext & IContainerRuntime. -Temporarily exposed on IContainerRuntimeBase. The intent is to remove it altogether in same release (more info to follow) +Consider using (Pure)DataObject(Factory) for your objects - they support passing initial args. +Otherwise consider implementing similar flow of exposing interface from your fluid object that is used to initialize object after creation. ## 0.24 Breaking Changes This release only contains renames. There are no functional changes in this release. You should ensure you have integrated and validated up to release 0.23 before integrating this release. diff --git a/components/experimental/table-document/src/slice.ts b/components/experimental/table-document/src/slice.ts index 4863889ce30a..6e50d57a4bca 100644 --- a/components/experimental/table-document/src/slice.ts +++ b/components/experimental/table-document/src/slice.ts @@ -4,7 +4,7 @@ */ import { DataObject, DataObjectFactory } from "@fluidframework/aqueduct"; -import { IFluidHandle } from "@fluidframework/component-core-interfaces"; +import { IFluidHandle, IFluidObject } from "@fluidframework/component-core-interfaces"; import { ICombiningOp, PropertySet } from "@fluidframework/merge-tree"; import { CellRange } from "./cellrange"; import { ConfigKey } from "./configKey"; @@ -100,7 +100,7 @@ export class TableSlice extends DataObject<{}, ITableSliceConfig> implements ITa this.doc.removeCols(startCol, numCols); } - protected async initializingFirstTime(initialState?: ITableSliceConfig) { + protected async initializingFirstTime(_scope?: IFluidObject, initialState?: ITableSliceConfig) { if (!initialState) { throw new Error("TableSlice must be created with initial state"); } diff --git a/components/experimental/todo/src/TextBox/TextBox.tsx b/components/experimental/todo/src/TextBox/TextBox.tsx index be84dc63e7ac..e5c1aa98f994 100644 --- a/components/experimental/todo/src/TextBox/TextBox.tsx +++ b/components/experimental/todo/src/TextBox/TextBox.tsx @@ -4,7 +4,7 @@ */ import { DataObject } from "@fluidframework/aqueduct"; import { CollaborativeTextArea } from "@fluidframework/react-inputs"; -import { IFluidHandle } from "@fluidframework/component-core-interfaces"; +import { IFluidHandle, IFluidObject } from "@fluidframework/component-core-interfaces"; import { SharedString } from "@fluidframework/sequence"; import { IFluidHTMLView } from "@fluidframework/view-interfaces"; import React from "react"; @@ -26,7 +26,7 @@ export class TextBox extends DataObject<{}, string> implements IFluidHTMLView { /** * Do creation work */ - protected async initializingFirstTime(initialState?: string) { + protected async initializingFirstTime(_scope?: IFluidObject, initialState?: string) { // if initial state is provided then use it. const newItemText = initialState ?? "Important Things"; diff --git a/components/experimental/todo/src/TodoItem/TodoItem.tsx b/components/experimental/todo/src/TodoItem/TodoItem.tsx index dd03f5e4de3b..4cd6f2c13cde 100644 --- a/components/experimental/todo/src/TodoItem/TodoItem.tsx +++ b/components/experimental/todo/src/TodoItem/TodoItem.tsx @@ -7,7 +7,7 @@ import { ClickerInstantiationFactory } from "@fluid-example/clicker"; import { DataObject, DataObjectFactory, waitForAttach } from "@fluidframework/aqueduct"; import { ISharedCell, SharedCell } from "@fluidframework/cell"; import { - IFluidHandle, IFluidLoadable, + IFluidHandle, IFluidLoadable, IFluidObject, } from "@fluidframework/component-core-interfaces"; import { IValueChanged } from "@fluidframework/map"; import { SharedString } from "@fluidframework/sequence"; @@ -50,7 +50,7 @@ export class TodoItem extends DataObject<{}, ITodoItemInitialState> implements I /** * Do creation work */ - protected async initializingFirstTime(initialState?: ITodoItemInitialState) { + protected async initializingFirstTime(_scope?: IFluidObject, initialState?: ITodoItemInitialState) { // Set initial state if it was provided const newItemText = initialState?.startingText ?? "New Item"; diff --git a/packages/framework/aqueduct/src/componentFactories/sharedComponentFactory.ts b/packages/framework/aqueduct/src/componentFactories/sharedComponentFactory.ts index be4fe156bffa..04b74c27469b 100644 --- a/packages/framework/aqueduct/src/componentFactories/sharedComponentFactory.ts +++ b/packages/framework/aqueduct/src/componentFactories/sharedComponentFactory.ts @@ -24,6 +24,7 @@ import { requestFluidObject } from "@fluidframework/runtime-utils"; import { ISharedComponentProps, PureDataObject, + PureDataObjectInitialState, } from "../components"; /** @@ -77,18 +78,7 @@ export class PureDataObjectFactory

implem * * @param context - component context used to load a component runtime */ - public instantiateDataStore(context: IFluidDataStoreContext): void { - this.instantiateComponentWithInitialState(context, undefined); - } - - /** - * Private method for component instantiation that exposes initial state - * @param context - Component context used to load a component runtime - * @param initialState - The initial state to provide the created component - */ - private instantiateComponentWithInitialState( - context: IFluidDataStoreContext, - initialState?: S): void { + public instantiateDataStore(context: IFluidDataStoreContext, scope?: IFluidObject): void { // Create a new runtime for our component // The runtime is what Fluid uses to create DDS' and route to your component const runtime = FluidDataStoreRuntime.load( @@ -102,14 +92,14 @@ export class PureDataObjectFactory

implem // run the initialization. if (!this.onDemandInstantiation || !runtime.existing) { // Create a new instance of our component up front - instanceP = this.instantiateInstance(runtime, context, initialState); + instanceP = this.instantiateInstance(runtime, context, scope); } runtime.registerRequestHandler(async (request: IRequest) => { // eslint-disable-next-line @typescript-eslint/no-misused-promises if (!instanceP) { // Create a new instance of our component on demand - instanceP = this.instantiateInstance(runtime, context, initialState); + instanceP = this.instantiateInstance(runtime, context, scope); } const instance = await instanceP; return instance.request(request); @@ -124,13 +114,13 @@ export class PureDataObjectFactory

implem private async instantiateInstance( runtime: FluidDataStoreRuntime, context: IFluidDataStoreContext, - initialState?: S, + scope?: IFluidObject, ) { const dependencyContainer = new DependencyContainer(context.scope.IFluidDependencySynthesizer); const providers = dependencyContainer.synthesize

(this.optionalProviders, {}); // Create a new instance of our component const instance = new this.ctor({ runtime, context, providers }); - await instance.initialize(initialState); + await instance.initializeInternal(scope); return instance; } @@ -150,14 +140,10 @@ export class PureDataObjectFactory

implem if (this.type === "") { throw new Error("undefined type member"); } - const packagePath = await context.composeSubpackagePath(this.type); - const router = await context.containerRuntime.createDataStoreWithRealizationFn( - packagePath, - (newContext) => { this.instantiateComponentWithInitialState(newContext, initialState); }, - ); - + const scope = { [PureDataObjectInitialState]: initialState }; + const router = await context.containerRuntime.createDataStore(packagePath, scope as IFluidObject); return requestFluidObject>(router, "/"); } } diff --git a/packages/framework/aqueduct/src/components/index.ts b/packages/framework/aqueduct/src/components/index.ts index dba61df532fc..5bf60ab6311e 100644 --- a/packages/framework/aqueduct/src/components/index.ts +++ b/packages/framework/aqueduct/src/components/index.ts @@ -8,4 +8,5 @@ export { DataObject } from "./primedComponent"; export { ISharedComponentProps, PureDataObject, + PureDataObjectInitialState, } from "./sharedComponent"; diff --git a/packages/framework/aqueduct/src/components/primedComponent.ts b/packages/framework/aqueduct/src/components/primedComponent.ts index ebfc7252a2d3..06c2a0803976 100644 --- a/packages/framework/aqueduct/src/components/primedComponent.ts +++ b/packages/framework/aqueduct/src/components/primedComponent.ts @@ -96,7 +96,7 @@ export abstract class DataObject

{ + public async initializeInternal(scope?: IFluidObject): Promise { // Initialize task manager. this.internalTaskManager = await requestFluidObject( this.context.containerRuntime, @@ -106,7 +106,6 @@ export abstract class DataObject

{ readonly runtime: IFluidDataStoreRuntime, readonly context: IFluidDataStoreContext, @@ -40,7 +42,6 @@ export interface ISharedComponentProps

{ export abstract class PureDataObject

extends EventForwarder implements IFluidLoadable, IFluidRouter, IProvideFluidHandle { - private initializeP: Promise | undefined; private readonly innerHandle: IFluidHandle; private _disposed = false; @@ -93,19 +94,6 @@ export abstract class PureDataObject

{ - // We want to ensure if this gets called more than once it only executes the initialize code once. - if (!this.initializeP) { - this.initializeP = this.initializeInternal(this.context.createProps as S ?? initialState); - } - - await this.initializeP; - } - // #region IFluidRouter /** @@ -145,16 +133,12 @@ export abstract class PureDataObject

{ - if (!this.runtime.existing) { - // If it's the first time through - await this.initializingFirstTime(props); - } else { - // Else we are loading from existing + public async initializeInternal(scope?: IFluidObject): Promise { + if (this.runtime.existing) { await this.initializingFromExisting(); + } else { + await this.initializingFirstTime(scope, (scope as any)?.PureDataObjectInitialState); } - - // This always gets called at the end of initialize on FirstTime or from existing. await this.hasInitialized(); } @@ -222,9 +206,9 @@ export abstract class PureDataObject

{ } + protected async initializingFirstTime(scope?: IFluidObject, props?: S): Promise { } /** * Called every time but the first time the component is initialized (creations diff --git a/packages/framework/react/src/fluidComponent/syncedComponent.ts b/packages/framework/react/src/fluidComponent/syncedComponent.ts index 003add9cb9f7..e4ec0f37fd67 100644 --- a/packages/framework/react/src/fluidComponent/syncedComponent.ts +++ b/packages/framework/react/src/fluidComponent/syncedComponent.ts @@ -57,7 +57,7 @@ export abstract class SyncedComponent< * Runs the first time the component is generated and sets up all necessary data structures for the view * To extend this function, please call super() prior to adding to functionality to ensure correct initializing */ - protected async initializingFirstTime(props?: S): Promise { + protected async initializingFirstTime(): Promise { // Initialize our synced state map for the first time using our // syncedStateConfig values await this.initializeStateFirstTime(); diff --git a/packages/runtime/container-runtime/src/componentContext.ts b/packages/runtime/container-runtime/src/componentContext.ts index d648de67bb85..89864ae3e7d3 100644 --- a/packages/runtime/container-runtime/src/componentContext.ts +++ b/packages/runtime/container-runtime/src/componentContext.ts @@ -216,7 +216,10 @@ export abstract class FluidDataStoreContext extends EventEmitter implements return deferred.promise; } - public async realize(): Promise { + public async realize(scope?: IFluidObject): Promise { + // scope can be provided only on creation path, where first realize() call is guaranteed to be bring it + assert(scope === undefined || this.componentRuntimeDeferred === undefined); + if (!this.componentRuntimeDeferred) { this.componentRuntimeDeferred = new Deferred(); const details = await this.getInitialSnapshotDetails(); @@ -246,18 +249,7 @@ export abstract class FluidDataStoreContext extends EventEmitter implements } // During this call we will invoke the instantiate method - which will call back into us // via the bindRuntime call to resolve componentRuntimeDeferred - factory.instantiateDataStore(this); - } - - return this.componentRuntimeDeferred.promise; - } - - public async realizeWithFn( - realizationFn: (context: IFluidDataStoreContext) => void, - ): Promise { - if (!this.componentRuntimeDeferred) { - this.componentRuntimeDeferred = new Deferred(); - realizationFn(this); + factory.instantiateDataStore(this, scope); } return this.componentRuntimeDeferred.promise; diff --git a/packages/runtime/container-runtime/src/containerRuntime.ts b/packages/runtime/container-runtime/src/containerRuntime.ts index edd670cad261..ae129b3022a2 100644 --- a/packages/runtime/container-runtime/src/containerRuntime.ts +++ b/packages/runtime/container-runtime/src/containerRuntime.ts @@ -1137,8 +1137,8 @@ export class ContainerRuntime extends EventEmitter } } - public async createDataStore(pkg: string | string[]): Promise { - return this._createComponentContext(Array.isArray(pkg) ? pkg : [pkg]).realize(); + public async createDataStore(pkg: string | string[], scope?: IFluidObject): Promise { + return this._createComponentContext(Array.isArray(pkg) ? pkg : [pkg]).realize(scope); } public async createRootDataStore(pkg: string | string[], rootDataStoreId: string): Promise { @@ -1174,36 +1174,6 @@ export class ContainerRuntime extends EventEmitter return context; } - public async createDataStoreWithRealizationFn( - pkg: string[], - realizationFn?: (context: IFluidDataStoreContext) => void, - ): Promise { - this.verifyNotClosed(); - - // tslint:disable-next-line: no-unsafe-any - const id: string = uuid(); - this.notBoundedComponentContexts.add(id); - const context = new LocalFluidDataStoreContext( - id, - pkg, - this, - this.storage, - this.containerScope, - this.summaryTracker.createOrGetChild(id, this.deltaManager.lastSequenceNumber), - (cr: IFluidDataStoreChannel) => this.bindComponent(cr), - undefined /* #1635: Remove LocalFluidDataStoreContext createProps */); - - const deferred = new Deferred(); - this.contextsDeferred.set(id, deferred); - this.contexts.set(id, context); - - if (realizationFn) { - return context.realizeWithFn(realizationFn); - } else { - return context.realize(); - } - } - public getQuorum(): IQuorum { return this.context.quorum; } diff --git a/packages/runtime/runtime-definitions/src/componentContext.ts b/packages/runtime/runtime-definitions/src/componentContext.ts index c16b3a0af64c..f23081c846c0 100644 --- a/packages/runtime/runtime-definitions/src/componentContext.ts +++ b/packages/runtime/runtime-definitions/src/componentContext.ts @@ -100,20 +100,13 @@ export interface IContainerRuntimeBase extends * (or any of its parts, like DDS) into already attached DDS (or non-attached DDS that will eventually * gets attached to storage) will result in this store being attached to storage. * @param pkg - Package name of the data store factory + * @param scope - scope object provided by instantiator of data store. It defines an environment for data store + * It may contain various background services, factories, etc. It's responsibility of data store to + * serialize enough state in data store itself for future invocations to have same environment. + * Usually this is done by working with IFluidLoadable objects and storing handles to those objects in root + * directory for future reference. */ - createDataStore(pkg: string | string[]): Promise; - - /** - * Creates a new component using an optional realization function. This API does not allow specifying - * the component's id and instead generates a uuid. Consumers must save another reference to the - * component, such as the handle. - * @param pkg - Package name of the component - * @param realizationFn - Optional function to call to realize the component over the context default - */ - createDataStoreWithRealizationFn( - pkg: string[], - realizationFn?: (context: IFluidDataStoreContext) => void, - ): Promise; + createDataStore(pkg: string | string[], scope?: IFluidObject): Promise; /** * Get an absolute url for a provided container-relative request. diff --git a/packages/runtime/runtime-definitions/src/componentFactory.ts b/packages/runtime/runtime-definitions/src/componentFactory.ts index c2c1dbf83690..ddaf46d584ae 100644 --- a/packages/runtime/runtime-definitions/src/componentFactory.ts +++ b/packages/runtime/runtime-definitions/src/componentFactory.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. */ +import { IFluidObject } from "@fluidframework/component-core-interfaces"; import { IFluidDataStoreContext } from "./componentContext"; declare module "@fluidframework/component-core-interfaces" { @@ -31,6 +32,11 @@ export interface IFluidDataStoreFactory extends IProvideFluidDataStoreFactory { /** * Generates runtime for the component from the component context. Once created should be bound to the context. * @param context - Context for the component. + * @param scope - scope object provided by instantiator of data store on creation. It defines an environment for + * data store. It may contain various background services, factories, etc. It's responsibility of data store to + * serialize enough state in data store itself for future invocations to have same environment. + * Usually this is done by working with IFluidLoadable objects and storing handles to those objects in root + * directory for future reference. */ - instantiateDataStore(context: IFluidDataStoreContext): void; + instantiateDataStore(context: IFluidDataStoreContext, scope?: IFluidObject): void; } From 2d28d904c6174f55c6909a19c26a3be5896b2554 Mon Sep 17 00:00:00 2001 From: Vlad Sudzilouski Date: Fri, 31 Jul 2020 15:03:32 -0700 Subject: [PATCH 02/24] Removing scope from initializingFirstTime(). --- components/experimental/table-document/src/slice.ts | 4 ++-- components/experimental/todo/src/TextBox/TextBox.tsx | 4 ++-- components/experimental/todo/src/TodoItem/TodoItem.tsx | 4 ++-- .../src/componentFactories/sharedComponentFactory.ts | 2 +- .../framework/aqueduct/src/components/primedComponent.ts | 4 ++-- .../framework/aqueduct/src/components/sharedComponent.ts | 9 +++++---- 6 files changed, 14 insertions(+), 13 deletions(-) diff --git a/components/experimental/table-document/src/slice.ts b/components/experimental/table-document/src/slice.ts index 6e50d57a4bca..4863889ce30a 100644 --- a/components/experimental/table-document/src/slice.ts +++ b/components/experimental/table-document/src/slice.ts @@ -4,7 +4,7 @@ */ import { DataObject, DataObjectFactory } from "@fluidframework/aqueduct"; -import { IFluidHandle, IFluidObject } from "@fluidframework/component-core-interfaces"; +import { IFluidHandle } from "@fluidframework/component-core-interfaces"; import { ICombiningOp, PropertySet } from "@fluidframework/merge-tree"; import { CellRange } from "./cellrange"; import { ConfigKey } from "./configKey"; @@ -100,7 +100,7 @@ export class TableSlice extends DataObject<{}, ITableSliceConfig> implements ITa this.doc.removeCols(startCol, numCols); } - protected async initializingFirstTime(_scope?: IFluidObject, initialState?: ITableSliceConfig) { + protected async initializingFirstTime(initialState?: ITableSliceConfig) { if (!initialState) { throw new Error("TableSlice must be created with initial state"); } diff --git a/components/experimental/todo/src/TextBox/TextBox.tsx b/components/experimental/todo/src/TextBox/TextBox.tsx index e5c1aa98f994..be84dc63e7ac 100644 --- a/components/experimental/todo/src/TextBox/TextBox.tsx +++ b/components/experimental/todo/src/TextBox/TextBox.tsx @@ -4,7 +4,7 @@ */ import { DataObject } from "@fluidframework/aqueduct"; import { CollaborativeTextArea } from "@fluidframework/react-inputs"; -import { IFluidHandle, IFluidObject } from "@fluidframework/component-core-interfaces"; +import { IFluidHandle } from "@fluidframework/component-core-interfaces"; import { SharedString } from "@fluidframework/sequence"; import { IFluidHTMLView } from "@fluidframework/view-interfaces"; import React from "react"; @@ -26,7 +26,7 @@ export class TextBox extends DataObject<{}, string> implements IFluidHTMLView { /** * Do creation work */ - protected async initializingFirstTime(_scope?: IFluidObject, initialState?: string) { + protected async initializingFirstTime(initialState?: string) { // if initial state is provided then use it. const newItemText = initialState ?? "Important Things"; diff --git a/components/experimental/todo/src/TodoItem/TodoItem.tsx b/components/experimental/todo/src/TodoItem/TodoItem.tsx index 4cd6f2c13cde..dd03f5e4de3b 100644 --- a/components/experimental/todo/src/TodoItem/TodoItem.tsx +++ b/components/experimental/todo/src/TodoItem/TodoItem.tsx @@ -7,7 +7,7 @@ import { ClickerInstantiationFactory } from "@fluid-example/clicker"; import { DataObject, DataObjectFactory, waitForAttach } from "@fluidframework/aqueduct"; import { ISharedCell, SharedCell } from "@fluidframework/cell"; import { - IFluidHandle, IFluidLoadable, IFluidObject, + IFluidHandle, IFluidLoadable, } from "@fluidframework/component-core-interfaces"; import { IValueChanged } from "@fluidframework/map"; import { SharedString } from "@fluidframework/sequence"; @@ -50,7 +50,7 @@ export class TodoItem extends DataObject<{}, ITodoItemInitialState> implements I /** * Do creation work */ - protected async initializingFirstTime(_scope?: IFluidObject, initialState?: ITodoItemInitialState) { + protected async initializingFirstTime(initialState?: ITodoItemInitialState) { // Set initial state if it was provided const newItemText = initialState?.startingText ?? "New Item"; diff --git a/packages/framework/aqueduct/src/componentFactories/sharedComponentFactory.ts b/packages/framework/aqueduct/src/componentFactories/sharedComponentFactory.ts index 04b74c27469b..f9180412e5bf 100644 --- a/packages/framework/aqueduct/src/componentFactories/sharedComponentFactory.ts +++ b/packages/framework/aqueduct/src/componentFactories/sharedComponentFactory.ts @@ -120,7 +120,7 @@ export class PureDataObjectFactory

implem const providers = dependencyContainer.synthesize

(this.optionalProviders, {}); // Create a new instance of our component const instance = new this.ctor({ runtime, context, providers }); - await instance.initializeInternal(scope); + await instance.initializeInternal((scope as any)?.PureDataObjectInitialState); return instance; } diff --git a/packages/framework/aqueduct/src/components/primedComponent.ts b/packages/framework/aqueduct/src/components/primedComponent.ts index 06c2a0803976..8f40d1d43c84 100644 --- a/packages/framework/aqueduct/src/components/primedComponent.ts +++ b/packages/framework/aqueduct/src/components/primedComponent.ts @@ -96,7 +96,7 @@ export abstract class DataObject

{ + public async initializeInternal(props?: S): Promise { // Initialize task manager. this.internalTaskManager = await requestFluidObject( this.context.containerRuntime, @@ -122,7 +122,7 @@ export abstract class DataObject

{ + public async initializeInternal(props?: S): Promise { if (this.runtime.existing) { + assert(props === undefined); await this.initializingFromExisting(); } else { - await this.initializingFirstTime(scope, (scope as any)?.PureDataObjectInitialState); + await this.initializingFirstTime(props); } await this.hasInitialized(); } @@ -206,9 +208,8 @@ export abstract class PureDataObject

{ } + protected async initializingFirstTime(props?: S): Promise { } /** * Called every time but the first time the component is initialized (creations From 08a2f3373ec67b65a5e3d0fb424defa69ba68c63 Mon Sep 17 00:00:00 2001 From: Vlad Sudzilouski Date: Sun, 2 Aug 2020 12:19:26 -0700 Subject: [PATCH 03/24] unfinished --- packages/runtime/container-runtime/src/componentContext.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/runtime/container-runtime/src/componentContext.ts b/packages/runtime/container-runtime/src/componentContext.ts index 89864ae3e7d3..bebae7a7cbef 100644 --- a/packages/runtime/container-runtime/src/componentContext.ts +++ b/packages/runtime/container-runtime/src/componentContext.ts @@ -434,11 +434,7 @@ export abstract class FluidDataStoreContext extends EventEmitter implements throw new Error("runtime already bound"); } - // If this FluidDataStoreContext was created via `IContainerRuntime.createDataStoreContext`, the - // `componentRuntimeDeferred` promise hasn't yet been initialized. Do so now. - if (!this.componentRuntimeDeferred) { - this.componentRuntimeDeferred = new Deferred(); - } + assert (this.componentRuntimeDeferred !== undefined); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const pending = this.pending!; From 86d01e426a8231ecc41c27ec3c112d7a58aec7f1 Mon Sep 17 00:00:00 2001 From: Vlad Sudzilouski Date: Fri, 7 Aug 2020 12:06:22 -0700 Subject: [PATCH 04/24] Build breaks after merge & PR feedback --- .../sharedComponentFactory.ts | 26 ++++++++++++---- .../aqueduct/src/components/index.ts | 1 - .../src/components/sharedComponent.ts | 2 -- .../src/containerRuntime.ts | 8 +++-- .../container-runtime/src/containerRuntime.ts | 31 +++++++++++++++---- .../container-runtime/src/dataStoreContext.ts | 11 ++++--- .../src/componentContext.ts | 8 ++++- .../src/componentFactory.ts | 7 +++-- .../test/deRehydrateContainerTests.spec.ts | 11 +++---- 9 files changed, 73 insertions(+), 32 deletions(-) diff --git a/packages/framework/aqueduct/src/componentFactories/sharedComponentFactory.ts b/packages/framework/aqueduct/src/componentFactories/sharedComponentFactory.ts index 53ca4d5928da..e77580e578a9 100644 --- a/packages/framework/aqueduct/src/componentFactories/sharedComponentFactory.ts +++ b/packages/framework/aqueduct/src/componentFactories/sharedComponentFactory.ts @@ -13,6 +13,7 @@ import { IProvideFluidDataStoreRegistry, NamedFluidDataStoreRegistryEntries, NamedFluidDataStoreRegistryEntry, + IFluidDataStoreScope, } from "@fluidframework/runtime-definitions"; import { IChannelFactory } from "@fluidframework/datastore-definitions"; import { @@ -24,9 +25,13 @@ import { requestFluidObject } from "@fluidframework/runtime-utils"; import { ISharedComponentProps, PureDataObject, - PureDataObjectInitialState, } from "../components"; +interface IPureDataObjectInitialState{ + IPureDataObjectInitialState?: IPureDataObjectInitialState; + props?: TProps; +} + /** * PureDataObjectFactory is a barebones IFluidDataStoreFactory for use with PureDataObject. * Consumers should typically use DataObjectFactory instead unless creating @@ -78,7 +83,7 @@ export class PureDataObjectFactory

implem * * @param context - component context used to load a data store runtime */ - public instantiateDataStore(context: IFluidDataStoreContext, scope?: IFluidObject): void { + public instantiateDataStore(context: IFluidDataStoreContext, scope?: IFluidDataStoreScope): void { // Create a new runtime for our component // The runtime is what Fluid uses to create DDS' and route to your component const runtime = FluidDataStoreRuntime.load( @@ -114,13 +119,13 @@ export class PureDataObjectFactory

implem private async instantiateInstance( runtime: FluidDataStoreRuntime, context: IFluidDataStoreContext, - scope?: IFluidObject, + scope?: IFluidDataStoreScope, ) { const dependencyContainer = new DependencyContainer(context.scope.IFluidDependencySynthesizer); const providers = dependencyContainer.synthesize

(this.optionalProviders, {}); // Create a new instance of our component const instance = new this.ctor({ runtime, context, providers }); - await instance.initializeInternal((scope as any)?.PureDataObjectInitialState); + await instance.initializeInternal(scope?.runtimeEnvironment?.IPureDataObjectInitialState?.props); return instance; } @@ -142,8 +147,17 @@ export class PureDataObjectFactory

implem } const packagePath = await context.composeSubpackagePath(this.type); - const scope = { [PureDataObjectInitialState]: initialState }; - const router = await context.containerRuntime.createDataStore(packagePath, scope as IFluidObject); + const props: IPureDataObjectInitialState = { + props: initialState, + }; + + const scope: IFluidDataStoreScope = { + loadable: {}, + runtimeEnvironment: { + IPureDataObjectInitialState: props, + }, + }; + const router = await context.containerRuntime.createDataStore(packagePath, scope); return requestFluidObject>(router, "/"); } } diff --git a/packages/framework/aqueduct/src/components/index.ts b/packages/framework/aqueduct/src/components/index.ts index 5bf60ab6311e..dba61df532fc 100644 --- a/packages/framework/aqueduct/src/components/index.ts +++ b/packages/framework/aqueduct/src/components/index.ts @@ -8,5 +8,4 @@ export { DataObject } from "./primedComponent"; export { ISharedComponentProps, PureDataObject, - PureDataObjectInitialState, } from "./sharedComponent"; diff --git a/packages/framework/aqueduct/src/components/sharedComponent.ts b/packages/framework/aqueduct/src/components/sharedComponent.ts index a67442d2596f..34868e70955b 100644 --- a/packages/framework/aqueduct/src/components/sharedComponent.ts +++ b/packages/framework/aqueduct/src/components/sharedComponent.ts @@ -23,8 +23,6 @@ import { IEvent } from "@fluidframework/common-definitions"; import { requestFluidObject } from "@fluidframework/runtime-utils"; import { serviceRoutePathRoot } from "../containerServices"; -export const PureDataObjectInitialState = "PDO_InitArgs"; - export interface ISharedComponentProps

{ readonly runtime: IFluidDataStoreRuntime, readonly context: IFluidDataStoreContext, diff --git a/packages/runtime/container-runtime-definitions/src/containerRuntime.ts b/packages/runtime/container-runtime-definitions/src/containerRuntime.ts index effa27b6f0d7..6df877c10312 100644 --- a/packages/runtime/container-runtime-definitions/src/containerRuntime.ts +++ b/packages/runtime/container-runtime-definitions/src/containerRuntime.ts @@ -31,6 +31,7 @@ import { IFluidDataStoreChannel, IFluidDataStoreContext, IInboundSignalMessage, + IFluidDataStoreScope, } from "@fluidframework/runtime-definitions"; import { IProvideContainerRuntimeDirtyable } from "./containerRuntimeDirtyable"; @@ -91,7 +92,7 @@ export interface IContainerRuntime extends * @param id - Id supplied during creating the component. * @param wait - True if you want to wait for it. */ - getDataStore(id: string, wait?: boolean): Promise; + getDataStore(id: string, wait?: boolean, scope?: IFluidDataStoreScope): Promise; /** * Creates root data store in container. Such store is automatically bound to container, and thus is @@ -103,7 +104,10 @@ export interface IContainerRuntime extends * @param rootDataStoreId - data store ID. IDs naming space is global in container. If collision on name occurs, * it results in container corruption - loading this file after that will always result in error. */ - createRootDataStore(pkg: string | string[], rootDataStoreId: string): Promise; + createRootDataStore( + pkg: string | string[], + rootDataStoreId: string, + scope?: IFluidDataStoreScope): Promise; /** * Returns the current quorum. diff --git a/packages/runtime/container-runtime/src/containerRuntime.ts b/packages/runtime/container-runtime/src/containerRuntime.ts index bcd4ac4b160f..491ff5985bd9 100644 --- a/packages/runtime/container-runtime/src/containerRuntime.ts +++ b/packages/runtime/container-runtime/src/containerRuntime.ts @@ -70,6 +70,7 @@ import { import { FlushMode, IAttachMessage, + IFluidDataStoreScope, IFluidDataStoreContext, IFluidDataStoreRegistry, IFluidDataStoreChannel, @@ -1213,22 +1214,40 @@ export class ContainerRuntime extends EventEmitter } } - public async createDataStore(pkg: string | string[], scope?: IFluidObject): Promise { - return this._createComponentContext(Array.isArray(pkg) ? pkg : [pkg]).realize(scope); + public async createDataStore( + pkg: string | string[], + scope?: IFluidDataStoreScope): Promise + { + return this._createDataStore(pkg, uuid(), scope); } - public async createRootDataStore(pkg: string | string[], rootDataStoreId: string): Promise { - const context = this._createFluidDataStoreContext(Array.isArray(pkg) ? pkg : [pkg], rootDataStoreId); - const fluidDataStore = await context.realize(); + public async createRootDataStore( + pkg: string | string[], + rootDataStoreId: string, + scope?: IFluidDataStoreScope): Promise + { + const fluidDataStore = await this._createDataStore(pkg, rootDataStoreId, scope); fluidDataStore.bindToContext(); return fluidDataStore; } + private async _createDataStore( + pkg: string | string[], + id: string, + scope?: IFluidDataStoreScope): Promise + { + const initScope = scope ?? { + loadable: {}, + runtimeEnvironment: {}, + }; + return this._createFluidDataStoreContext(Array.isArray(pkg) ? pkg : [pkg], id).realize(initScope); + } + private canSendOps() { return this.connected && !this.deltaManager.readonly; } - private _createFluidDataStoreContext(pkg: string[], id = uuid()) { + private _createFluidDataStoreContext(pkg: string[], id) { this.verifyNotClosed(); assert(!this.contexts.has(id), "Creating store with existing ID"); diff --git a/packages/runtime/container-runtime/src/dataStoreContext.ts b/packages/runtime/container-runtime/src/dataStoreContext.ts index e623ec684570..e44227a7241c 100644 --- a/packages/runtime/container-runtime/src/dataStoreContext.ts +++ b/packages/runtime/container-runtime/src/dataStoreContext.ts @@ -38,6 +38,7 @@ import { FluidDataStoreRegistryEntry, IFluidDataStoreChannel, IAttachMessage, + IFluidDataStoreScope, IFluidDataStoreContext, IFluidDataStoreFactory, IFluidDataStoreRegistry, @@ -227,11 +228,11 @@ export abstract class FluidDataStoreContext extends EventEmitter implements return deferred.promise; } - public async realize(scope?: IFluidObject): Promise { - // scope can be provided only on creation path, where first realize() call is guaranteed to be bring it - assert(scope === undefined || this.componentRuntimeDeferred === undefined); + public async realize(scope?: IFluidDataStoreScope): Promise { + // scope can be provided only on creation path, where first realize() call is guaranteed to bring it + assert((scope === undefined) === (this.channelDeferred !== undefined)); - if (!this.componentRuntimeDeferred) { + if (!this.channelDeferred) { this.channelDeferred = new Deferred(); const details = await this.getInitialSnapshotDetails(); // Base snapshot is the baseline where pending ops are applied to. @@ -479,7 +480,7 @@ export abstract class FluidDataStoreContext extends EventEmitter implements throw new Error("Runtime already bound"); } - assert (this.componentRuntimeDeferred !== undefined); + assert (this.channelDeferred !== undefined); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const pending = this.pending!; diff --git a/packages/runtime/runtime-definitions/src/componentContext.ts b/packages/runtime/runtime-definitions/src/componentContext.ts index 6667cb904092..3cd5bc2881b8 100644 --- a/packages/runtime/runtime-definitions/src/componentContext.ts +++ b/packages/runtime/runtime-definitions/src/componentContext.ts @@ -7,6 +7,7 @@ import { EventEmitter } from "events"; import { ITelemetryLogger, IDisposable } from "@fluidframework/common-definitions"; import { IFluidObject, + IFluidLoadable, IFluidRouter, IProvideFluidHandleContext, IProvideFluidSerializer, @@ -99,7 +100,7 @@ export interface IContainerRuntimeBase extends * Usually this is done by working with IFluidLoadable objects and storing handles to those objects in root * directory for future reference. */ - createDataStore(pkg: string | string[], scope?: IFluidObject): Promise; + createDataStore(pkg: string | string[], scope?: IFluidDataStoreScope): Promise; /** * Get an absolute url for a provided container-relative request. @@ -343,3 +344,8 @@ export interface IFluidDataStoreContext extends EventEmitter { */ composeSubpackagePath(subpackage: string): Promise; } + +export interface IFluidDataStoreScope { + loadable: {[key: string]: IFluidObject & IFluidLoadable}; + runtimeEnvironment: {[key: string]: any }; +} diff --git a/packages/runtime/runtime-definitions/src/componentFactory.ts b/packages/runtime/runtime-definitions/src/componentFactory.ts index 43c1d2a865fb..e298165baecb 100644 --- a/packages/runtime/runtime-definitions/src/componentFactory.ts +++ b/packages/runtime/runtime-definitions/src/componentFactory.ts @@ -3,8 +3,7 @@ * Licensed under the MIT License. */ -import { IFluidObject } from "@fluidframework/component-core-interfaces"; -import { IFluidDataStoreContext } from "./componentContext"; +import { IFluidDataStoreContext, IFluidDataStoreScope } from "./componentContext"; declare module "@fluidframework/core-interfaces" { // eslint-disable-next-line @typescript-eslint/no-empty-interface @@ -35,6 +34,8 @@ export interface IFluidDataStoreFactory extends IProvideFluidDataStoreFactory { * serialize enough state in data store itself for future invocations to have same environment. * Usually this is done by working with IFluidLoadable objects and storing handles to those objects in root * directory for future reference. + * It's undefined when loading existing data store, and not undefined (maybe empty object if not provided by + * calleee) when create new data store. */ - instantiateDataStore(context: IFluidDataStoreContext, scope?: IFluidObject): void; + instantiateDataStore(context: IFluidDataStoreContext, scope?: IFluidDataStoreScope): void; } diff --git a/packages/test/end-to-end-tests/src/test/deRehydrateContainerTests.spec.ts b/packages/test/end-to-end-tests/src/test/deRehydrateContainerTests.spec.ts index 9f531bde5552..433be7e6d9c3 100644 --- a/packages/test/end-to-end-tests/src/test/deRehydrateContainerTests.spec.ts +++ b/packages/test/end-to-end-tests/src/test/deRehydrateContainerTests.spec.ts @@ -17,8 +17,8 @@ import { } from "@fluidframework/test-utils"; import { SharedMap } from "@fluidframework/map"; import { IDocumentAttributes } from "@fluidframework/protocol-definitions"; -import { IContainerRuntime } from "@fluidframework/container-runtime-definitions"; import { IContainerRuntimeBase } from "@fluidframework/runtime-definitions"; +import { requestFluidObject } from "@fluidframework/runtime-utils"; describe(`Dehydrate Rehydrate Container Test`, () => { const codeDetails: IFluidCodeDetails = { @@ -59,13 +59,12 @@ describe(`Dehydrate Rehydrate Container Test`, () => { const createPeerComponent = async ( containerRuntime: IContainerRuntimeBase, ) => { - const peerComponentRuntimeChannel = await (containerRuntime as IContainerRuntime) - .createDataStoreWithRealizationFn(["default"]); - const peerComponent = - (await peerComponentRuntimeChannel.request({ url: "/" })).value as ITestFluidComponent; + const peerComponent = await requestFluidObject( + await containerRuntime.createDataStore(["default"]), + "/"); return { peerComponent, - peerComponentRuntimeChannel, + peerComponentRuntimeChannel: peerComponent.channel, }; }; From 2af669577dec5901c4987623fc7b1c8ea39c0790 Mon Sep 17 00:00:00 2001 From: Vlad Sudzilouski Date: Fri, 7 Aug 2020 12:38:59 -0700 Subject: [PATCH 05/24] Back compat:: remove assert for now. Also small name tweaks in pure object initial state logic. --- .../src/componentFactories/sharedComponentFactory.ts | 9 ++++----- .../runtime/container-runtime/src/dataStoreContext.ts | 3 ++- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/framework/aqueduct/src/componentFactories/sharedComponentFactory.ts b/packages/framework/aqueduct/src/componentFactories/sharedComponentFactory.ts index e77580e578a9..0a4832377de9 100644 --- a/packages/framework/aqueduct/src/componentFactories/sharedComponentFactory.ts +++ b/packages/framework/aqueduct/src/componentFactories/sharedComponentFactory.ts @@ -29,7 +29,7 @@ import { interface IPureDataObjectInitialState{ IPureDataObjectInitialState?: IPureDataObjectInitialState; - props?: TProps; + initialState?: TProps; } /** @@ -125,7 +125,7 @@ export class PureDataObjectFactory

implem const providers = dependencyContainer.synthesize

(this.optionalProviders, {}); // Create a new instance of our component const instance = new this.ctor({ runtime, context, providers }); - await instance.initializeInternal(scope?.runtimeEnvironment?.IPureDataObjectInitialState?.props); + await instance.initializeInternal(scope?.runtimeEnvironment?.IPureDataObjectInitialState?.initialState); return instance; } @@ -147,9 +147,8 @@ export class PureDataObjectFactory

implem } const packagePath = await context.composeSubpackagePath(this.type); - const props: IPureDataObjectInitialState = { - props: initialState, - }; + const props: IPureDataObjectInitialState = { initialState }; + props.IPureDataObjectInitialState = props; const scope: IFluidDataStoreScope = { loadable: {}, diff --git a/packages/runtime/container-runtime/src/dataStoreContext.ts b/packages/runtime/container-runtime/src/dataStoreContext.ts index e44227a7241c..38035d6e4931 100644 --- a/packages/runtime/container-runtime/src/dataStoreContext.ts +++ b/packages/runtime/container-runtime/src/dataStoreContext.ts @@ -229,8 +229,9 @@ export abstract class FluidDataStoreContext extends EventEmitter implements } public async realize(scope?: IFluidDataStoreScope): Promise { + // Back-compat: Can't enable it in 0.25, enable in later version! // scope can be provided only on creation path, where first realize() call is guaranteed to bring it - assert((scope === undefined) === (this.channelDeferred !== undefined)); + // assert((scope === undefined) === (this.channelDeferred !== undefined)); if (!this.channelDeferred) { this.channelDeferred = new Deferred(); From e984172e4c487b017e4318f76195f28f862fb7fe Mon Sep 17 00:00:00 2001 From: Vlad Sudzilouski Date: Fri, 7 Aug 2020 18:04:35 -0700 Subject: [PATCH 06/24] Detached creation flow --- .../experimental/codemirror/src/codemirror.ts | 3 +- .../experimental/key-value-cache/src/index.ts | 4 +- .../progress-bars/src/progressBars.ts | 4 +- .../prosemirror/src/prosemirror.ts | 4 +- components/experimental/scribe/src/scribe.ts | 4 +- .../experimental/shared-text/src/component.ts | 4 +- .../experimental/shared-text/src/index.ts | 2 +- components/experimental/smde/src/smde.ts | 4 +- .../sharedComponentFactory.ts | 43 ++-- .../src/sharedcomponentfactory.ts | 4 +- .../runtime/agent-scheduler/src/scheduler.ts | 4 +- .../runtime/client-api/src/api/codeLoader.ts | 4 +- .../component-runtime/src/componentRuntime.ts | 1 - .../src/containerRuntime.ts | 8 +- .../container-runtime/src/containerRuntime.ts | 62 +++--- .../container-runtime/src/dataStoreContext.ts | 207 ++++++++++++------ .../src/test/dataStoreContext.spec.ts | 6 +- .../src/test/dataStoreCreation.spec.ts | 4 +- .../runtime/datastore/src/dataStoreRuntime.ts | 1 - .../src/componentContext.ts | 30 +-- .../src/componentFactory.ts | 11 +- .../test/test-utils/src/testFluidComponent.ts | 4 +- 22 files changed, 257 insertions(+), 161 deletions(-) diff --git a/components/experimental/codemirror/src/codemirror.ts b/components/experimental/codemirror/src/codemirror.ts index 6e4bd67bf678..5d24922d0ce6 100644 --- a/components/experimental/codemirror/src/codemirror.ts +++ b/components/experimental/codemirror/src/codemirror.ts @@ -269,7 +269,7 @@ class SmdeFactory implements IFluidDataStoreFactory { public get IFluidDataStoreFactory() { return this; } - public instantiateDataStore(context: IFluidDataStoreContext): void { + public async instantiateDataStore(context: IFluidDataStoreContext) { const dataTypes = new Map(); const mapFactory = SharedMap.getFactory(); const sequenceFactory = SharedString.getFactory(); @@ -286,6 +286,7 @@ class SmdeFactory implements IFluidDataStoreFactory { const progressCollection = await progressCollectionP; return progressCollection.request(request); }); + return runtime; } } diff --git a/components/experimental/key-value-cache/src/index.ts b/components/experimental/key-value-cache/src/index.ts index 2b71361366d3..40888ba55173 100644 --- a/components/experimental/key-value-cache/src/index.ts +++ b/components/experimental/key-value-cache/src/index.ts @@ -116,7 +116,7 @@ export class KeyValueFactoryComponent implements IRuntimeFactory, IFluidDataStor public get IRuntimeFactory() { return this; } public get IFluidDataStoreFactory() { return this; } - public instantiateDataStore(context: IFluidDataStoreContext): void { + public async instantiateDataStore(context: IFluidDataStoreContext) { const dataTypes = new Map(); const mapFactory = SharedMap.getFactory(); dataTypes.set(mapFactory.type, mapFactory); @@ -131,6 +131,8 @@ export class KeyValueFactoryComponent implements IRuntimeFactory, IFluidDataStor const keyValue = await keyValueP; return keyValue.request(request); }); + + return runtime; } public async instantiateRuntime(context: IContainerContext): Promise { diff --git a/components/experimental/progress-bars/src/progressBars.ts b/components/experimental/progress-bars/src/progressBars.ts index 83d93928a174..d7328d30fdd7 100644 --- a/components/experimental/progress-bars/src/progressBars.ts +++ b/components/experimental/progress-bars/src/progressBars.ts @@ -230,7 +230,7 @@ class ProgressBarsFactory implements IFluidDataStoreFactory { public get IFluidDataStoreFactory() { return this; } - public instantiateDataStore(context: IFluidDataStoreContext): void { + public async instantiateDataStore(context: IFluidDataStoreContext) { const dataTypes = new Map(); const mapFactory = SharedMap.getFactory(); dataTypes.set(mapFactory.type, mapFactory); @@ -245,6 +245,8 @@ class ProgressBarsFactory implements IFluidDataStoreFactory { const progressCollection = await progressCollectionP; return progressCollection.request(request); }); + + return runtime; } } diff --git a/components/experimental/prosemirror/src/prosemirror.ts b/components/experimental/prosemirror/src/prosemirror.ts index 40aa81c73ebe..0051cd8c9b53 100644 --- a/components/experimental/prosemirror/src/prosemirror.ts +++ b/components/experimental/prosemirror/src/prosemirror.ts @@ -175,7 +175,7 @@ class ProseMirrorFactory implements IFluidDataStoreFactory { public get IFluidDataStoreFactory() { return this; } - public instantiateDataStore(context: IFluidDataStoreContext): void { + public async instantiateDataStore(context: IFluidDataStoreContext) { const dataTypes = new Map(); const mapFactory = SharedMap.getFactory(); const sequenceFactory = SharedString.getFactory(); @@ -193,6 +193,8 @@ class ProseMirrorFactory implements IFluidDataStoreFactory { const proseMirror = await proseMirrorP; return proseMirror.request(request); }); + + return runtime; } } diff --git a/components/experimental/scribe/src/scribe.ts b/components/experimental/scribe/src/scribe.ts index 015da403b8bf..aa6a9b8a30ee 100644 --- a/components/experimental/scribe/src/scribe.ts +++ b/components/experimental/scribe/src/scribe.ts @@ -482,7 +482,7 @@ class ScribeFactory implements IFluidDataStoreFactory, IRuntimeFactory { return runtime; } - public instantiateDataStore(context: IFluidDataStoreContext): void { + public async instantiateDataStore(context: IFluidDataStoreContext) { const dataTypes = new Map(); const mapFactory = SharedMap.getFactory(); dataTypes.set(mapFactory.type, mapFactory); @@ -497,6 +497,8 @@ class ScribeFactory implements IFluidDataStoreFactory, IRuntimeFactory { const progressCollection = await progressCollectionP; return progressCollection.request(request); }); + + return runtime; } } diff --git a/components/experimental/shared-text/src/component.ts b/components/experimental/shared-text/src/component.ts index 712ee2096d17..67ff12ae741a 100644 --- a/components/experimental/shared-text/src/component.ts +++ b/components/experimental/shared-text/src/component.ts @@ -310,7 +310,7 @@ class TaskScheduler { } } -export function instantiateDataStore(context: IFluidDataStoreContext): void { +export function instantiateDataStore(context: IFluidDataStoreContext) { const modules = new Map(); // Create channel factories @@ -339,4 +339,6 @@ export function instantiateDataStore(context: IFluidDataStoreContext): void { const runner = await runnerP; return runner.request(request); }); + + return runtime; } diff --git a/components/experimental/shared-text/src/index.ts b/components/experimental/shared-text/src/index.ts index 216c079459fa..f132e64f92b0 100644 --- a/components/experimental/shared-text/src/index.ts +++ b/components/experimental/shared-text/src/index.ts @@ -85,7 +85,7 @@ class SharedTextFactoryComponent implements IFluidDataStoreFactory, IRuntimeFact public get IFluidDataStoreFactory() { return this; } public get IRuntimeFactory() { return this; } - public instantiateDataStore(context: IFluidDataStoreContext): void { + public async instantiateDataStore(context: IFluidDataStoreContext) { return sharedTextComponent.instantiateDataStore(context); } diff --git a/components/experimental/smde/src/smde.ts b/components/experimental/smde/src/smde.ts index 2071067b80f6..88a011660f07 100644 --- a/components/experimental/smde/src/smde.ts +++ b/components/experimental/smde/src/smde.ts @@ -218,7 +218,7 @@ class SmdeFactory implements IFluidDataStoreFactory { public get IFluidDataStoreFactory() { return this; } - public instantiateDataStore(context: IFluidDataStoreContext): void { + public async instantiateDataStore(context: IFluidDataStoreContext) { const dataTypes = new Map(); const mapFactory = SharedMap.getFactory(); const sequenceFactory = SharedString.getFactory(); @@ -236,6 +236,8 @@ class SmdeFactory implements IFluidDataStoreFactory { const progressCollection = await progressCollectionP; return progressCollection.request(request); }); + + return runtime; } } diff --git a/packages/framework/aqueduct/src/componentFactories/sharedComponentFactory.ts b/packages/framework/aqueduct/src/componentFactories/sharedComponentFactory.ts index 0a4832377de9..29375cbf7422 100644 --- a/packages/framework/aqueduct/src/componentFactories/sharedComponentFactory.ts +++ b/packages/framework/aqueduct/src/componentFactories/sharedComponentFactory.ts @@ -13,7 +13,6 @@ import { IProvideFluidDataStoreRegistry, NamedFluidDataStoreRegistryEntries, NamedFluidDataStoreRegistryEntry, - IFluidDataStoreScope, } from "@fluidframework/runtime-definitions"; import { IChannelFactory } from "@fluidframework/datastore-definitions"; import { @@ -27,11 +26,6 @@ import { PureDataObject, } from "../components"; -interface IPureDataObjectInitialState{ - IPureDataObjectInitialState?: IPureDataObjectInitialState; - initialState?: TProps; -} - /** * PureDataObjectFactory is a barebones IFluidDataStoreFactory for use with PureDataObject. * Consumers should typically use DataObjectFactory instead unless creating @@ -83,7 +77,16 @@ export class PureDataObjectFactory

implem * * @param context - component context used to load a data store runtime */ - public instantiateDataStore(context: IFluidDataStoreContext, scope?: IFluidDataStoreScope): void { + public async instantiateDataStore(context: IFluidDataStoreContext) { + return this.instantiateDataStoreCore(context); + } + + /** + * This is where we do component setup. + * + * @param context - component context used to load a data store runtime + */ + protected instantiateDataStoreCore(context: IFluidDataStoreContext, props?: S) { // Create a new runtime for our component // The runtime is what Fluid uses to create DDS' and route to your component const runtime = FluidDataStoreRuntime.load( @@ -97,18 +100,20 @@ export class PureDataObjectFactory

implem // run the initialization. if (!this.onDemandInstantiation || !runtime.existing) { // Create a new instance of our component up front - instanceP = this.instantiateInstance(runtime, context, scope); + instanceP = this.instantiateInstance(runtime, context, props); } runtime.registerRequestHandler(async (request: IRequest) => { // eslint-disable-next-line @typescript-eslint/no-misused-promises if (!instanceP) { // Create a new instance of our component on demand - instanceP = this.instantiateInstance(runtime, context, scope); + instanceP = this.instantiateInstance(runtime, context, props); } const instance = await instanceP; return instance.request(request); }); + + return runtime; } /** @@ -119,13 +124,13 @@ export class PureDataObjectFactory

implem private async instantiateInstance( runtime: FluidDataStoreRuntime, context: IFluidDataStoreContext, - scope?: IFluidDataStoreScope, + props?: S, ) { const dependencyContainer = new DependencyContainer(context.scope.IFluidDependencySynthesizer); const providers = dependencyContainer.synthesize

(this.optionalProviders, {}); // Create a new instance of our component const instance = new this.ctor({ runtime, context, providers }); - await instance.initializeInternal(scope?.runtimeEnvironment?.IPureDataObjectInitialState?.initialState); + await instance.initializeInternal(props); return instance; } @@ -147,16 +152,10 @@ export class PureDataObjectFactory

implem } const packagePath = await context.composeSubpackagePath(this.type); - const props: IPureDataObjectInitialState = { initialState }; - props.IPureDataObjectInitialState = props; - - const scope: IFluidDataStoreScope = { - loadable: {}, - runtimeEnvironment: { - IPureDataObjectInitialState: props, - }, - }; - const router = await context.containerRuntime.createDataStore(packagePath, scope); - return requestFluidObject>(router, "/"); + const newContext = context.containerRuntime.createDetachedDataStore(); + const runtime = this.instantiateDataStoreCore(newContext, initialState); + newContext.bindDetachedRuntime(runtime, packagePath); + + return requestFluidObject>(runtime, "/"); } } diff --git a/packages/framework/component-base/src/sharedcomponentfactory.ts b/packages/framework/component-base/src/sharedcomponentfactory.ts index cea4e6f54c5d..14bfe48ff593 100644 --- a/packages/framework/component-base/src/sharedcomponentfactory.ts +++ b/packages/framework/component-base/src/sharedcomponentfactory.ts @@ -48,7 +48,7 @@ export class PureDataObjectFactory implements IFluidDa public get IFluidDataStoreFactory() { return this; } - public instantiateDataStore(context: IFluidDataStoreContext): void { + public async instantiateDataStore(context: IFluidDataStoreContext) { const runtime = this.createRuntime(context); // Note this may synchronously return an instance or a deferred LazyPromise, @@ -66,6 +66,8 @@ export class PureDataObjectFactory implements IFluidDa return instance.request(request); }); + + return runtime; } public async create(parentContext: IFluidDataStoreContext, props?: any) { diff --git a/packages/runtime/agent-scheduler/src/scheduler.ts b/packages/runtime/agent-scheduler/src/scheduler.ts index 938d61134d70..e86aa52c3ace 100644 --- a/packages/runtime/agent-scheduler/src/scheduler.ts +++ b/packages/runtime/agent-scheduler/src/scheduler.ts @@ -480,7 +480,7 @@ export class AgentSchedulerFactory implements IFluidDataStoreFactory { public get IFluidDataStoreFactory() { return this; } - public instantiateDataStore(context: IFluidDataStoreContext): void { + public async instantiateDataStore(context: IFluidDataStoreContext) { const mapFactory = SharedMap.getFactory(); const consensusRegisterCollectionFactory = ConsensusRegisterCollection.getFactory(); const dataTypes = new Map(); @@ -499,5 +499,7 @@ export class AgentSchedulerFactory implements IFluidDataStoreFactory { const taskManager = await taskManagerP; return taskManager.request(request); }); + + return runtime; } } diff --git a/packages/runtime/client-api/src/api/codeLoader.ts b/packages/runtime/client-api/src/api/codeLoader.ts index a42c4824e14f..6e507e92ba9b 100644 --- a/packages/runtime/client-api/src/api/codeLoader.ts +++ b/packages/runtime/client-api/src/api/codeLoader.ts @@ -41,7 +41,7 @@ export class Chaincode implements IFluidDataStoreFactory { public constructor(private readonly closeFn: () => void) { } - public instantiateDataStore(context: IFluidDataStoreContext): void { + public async instantiateDataStore(context: IFluidDataStoreContext) { // Create channel factories const mapFactory = map.SharedMap.getFactory(); const sharedStringFactory = sequence.SharedString.getFactory(); @@ -95,6 +95,8 @@ export class Chaincode implements IFluidDataStoreFactory { value: document, }; }); + + return runtime; } } diff --git a/packages/runtime/component-runtime/src/componentRuntime.ts b/packages/runtime/component-runtime/src/componentRuntime.ts index f9f7e788e028..6c024d017b74 100644 --- a/packages/runtime/component-runtime/src/componentRuntime.ts +++ b/packages/runtime/component-runtime/src/componentRuntime.ts @@ -102,7 +102,6 @@ export class FluidDataStoreRuntime extends EventEmitter implements IFluidDataSto componentRegistry, logger); - context.bindRuntime(runtime); return runtime; } diff --git a/packages/runtime/container-runtime-definitions/src/containerRuntime.ts b/packages/runtime/container-runtime-definitions/src/containerRuntime.ts index 6df877c10312..effa27b6f0d7 100644 --- a/packages/runtime/container-runtime-definitions/src/containerRuntime.ts +++ b/packages/runtime/container-runtime-definitions/src/containerRuntime.ts @@ -31,7 +31,6 @@ import { IFluidDataStoreChannel, IFluidDataStoreContext, IInboundSignalMessage, - IFluidDataStoreScope, } from "@fluidframework/runtime-definitions"; import { IProvideContainerRuntimeDirtyable } from "./containerRuntimeDirtyable"; @@ -92,7 +91,7 @@ export interface IContainerRuntime extends * @param id - Id supplied during creating the component. * @param wait - True if you want to wait for it. */ - getDataStore(id: string, wait?: boolean, scope?: IFluidDataStoreScope): Promise; + getDataStore(id: string, wait?: boolean): Promise; /** * Creates root data store in container. Such store is automatically bound to container, and thus is @@ -104,10 +103,7 @@ export interface IContainerRuntime extends * @param rootDataStoreId - data store ID. IDs naming space is global in container. If collision on name occurs, * it results in container corruption - loading this file after that will always result in error. */ - createRootDataStore( - pkg: string | string[], - rootDataStoreId: string, - scope?: IFluidDataStoreScope): Promise; + createRootDataStore(pkg: string | string[], rootDataStoreId: string): Promise; /** * Returns the current quorum. diff --git a/packages/runtime/container-runtime/src/containerRuntime.ts b/packages/runtime/container-runtime/src/containerRuntime.ts index 491ff5985bd9..7a759480b846 100644 --- a/packages/runtime/container-runtime/src/containerRuntime.ts +++ b/packages/runtime/container-runtime/src/containerRuntime.ts @@ -70,8 +70,8 @@ import { import { FlushMode, IAttachMessage, - IFluidDataStoreScope, IFluidDataStoreContext, + IFluidDataStoreContextDetached, IFluidDataStoreRegistry, IFluidDataStoreChannel, IEnvelope, @@ -96,7 +96,12 @@ import { RequestParser, } from "@fluidframework/runtime-utils"; import { v4 as uuid } from "uuid"; -import { FluidDataStoreContext, LocalFluidDataStoreContext, RemotedFluidDataStoreContext } from "./dataStoreContext"; +import { + FluidDataStoreContext, + LocalFluidDataStoreContext, + LocalDetachedFluidDataStoreContext, + RemotedFluidDataStoreContext, +} from "./dataStoreContext"; import { FluidHandleContext } from "./dataStoreHandleContext"; import { FluidDataStoreRegistry } from "./dataStoreRegistry"; import { debug } from "./debug"; @@ -1214,33 +1219,36 @@ export class ContainerRuntime extends EventEmitter } } - public async createDataStore( - pkg: string | string[], - scope?: IFluidDataStoreScope): Promise + public async createDataStore(pkg: string | string[]): Promise { - return this._createDataStore(pkg, uuid(), scope); + return this._createDataStore(pkg); } - public async createRootDataStore( - pkg: string | string[], - rootDataStoreId: string, - scope?: IFluidDataStoreScope): Promise + public async createRootDataStore(pkg: string | string[], rootDataStoreId: string): Promise { - const fluidDataStore = await this._createDataStore(pkg, rootDataStoreId, scope); + const fluidDataStore = await this._createDataStore(pkg, rootDataStoreId); fluidDataStore.bindToContext(); return fluidDataStore; } - private async _createDataStore( - pkg: string | string[], - id: string, - scope?: IFluidDataStoreScope): Promise + public createDetachedDataStore(): IFluidDataStoreContextDetached { + const id = uuid(); + const context = new LocalDetachedFluidDataStoreContext( + id, + this, + this.storage, + this.containerScope, + this.summaryTracker.createOrGetChild(id, this.deltaManager.lastSequenceNumber), + this.summarizerNode.getCreateChildFn(id, { type: CreateSummarizerNodeSource.Local }), + (cr: IFluidDataStoreChannel) => this.bindFluidDataStore(cr), + ); + this.setupNewContext(context); + return context; + } + + private async _createDataStore(pkg: string | string[], id = uuid()): Promise { - const initScope = scope ?? { - loadable: {}, - runtimeEnvironment: {}, - }; - return this._createFluidDataStoreContext(Array.isArray(pkg) ? pkg : [pkg], id).realize(initScope); + return this._createFluidDataStoreContext(Array.isArray(pkg) ? pkg : [pkg], id).realize(); } private canSendOps() { @@ -1248,10 +1256,6 @@ export class ContainerRuntime extends EventEmitter } private _createFluidDataStoreContext(pkg: string[], id) { - this.verifyNotClosed(); - - assert(!this.contexts.has(id), "Creating store with existing ID"); - this.notBoundContexts.add(id); const context = new LocalFluidDataStoreContext( id, pkg, @@ -1262,12 +1266,18 @@ export class ContainerRuntime extends EventEmitter this.summarizerNode.getCreateChildFn(id, { type: CreateSummarizerNodeSource.Local }), (cr: IFluidDataStoreChannel) => this.bindFluidDataStore(cr), ); + this.setupNewContext(context); + return context; + } + private setupNewContext(context) { + this.verifyNotClosed(); + const id = context.id; + assert(!this.contexts.has(id), "Creating store with existing ID"); + this.notBoundContexts.add(id); const deferred = new Deferred(); this.contextsDeferred.set(id, deferred); this.contexts.set(id, context); - - return context; } public getQuorum(): IQuorum { diff --git a/packages/runtime/container-runtime/src/dataStoreContext.ts b/packages/runtime/container-runtime/src/dataStoreContext.ts index 38035d6e4931..8c117d05615f 100644 --- a/packages/runtime/container-runtime/src/dataStoreContext.ts +++ b/packages/runtime/container-runtime/src/dataStoreContext.ts @@ -38,8 +38,8 @@ import { FluidDataStoreRegistryEntry, IFluidDataStoreChannel, IAttachMessage, - IFluidDataStoreScope, IFluidDataStoreContext, + IFluidDataStoreContextDetached, IFluidDataStoreFactory, IFluidDataStoreRegistry, IInboundSignalMessage, @@ -161,6 +161,7 @@ export abstract class FluidDataStoreContext extends EventEmitter implements return this._attachState; } + protected detachedRuntimeCreation = false; public readonly bindToContext: (channel: IFluidDataStoreChannel) => void; protected channel: IFluidDataStoreChannel | undefined; private loaded = false; @@ -220,52 +221,54 @@ export abstract class FluidDataStoreContext extends EventEmitter implements // Error messages contain package names that is considered Personal Identifiable Information // Mark it as such, so that if it ever reaches telemetry pipeline, it has a chance to remove it. (error as any).containsPII = true; - - // This is always called with a channelDeferred in realize(); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const deferred = this.channelDeferred!; - deferred.reject(error); - return deferred.promise; + throw error; } - public async realize(scope?: IFluidDataStoreScope): Promise { - // Back-compat: Can't enable it in 0.25, enable in later version! - // scope can be provided only on creation path, where first realize() call is guaranteed to bring it - // assert((scope === undefined) === (this.channelDeferred !== undefined)); - + public async realize(): Promise { + assert(!this.detachedRuntimeCreation); if (!this.channelDeferred) { this.channelDeferred = new Deferred(); - const details = await this.getInitialSnapshotDetails(); - // Base snapshot is the baseline where pending ops are applied to. - // It is important that this be in sync with the pending ops, and also - // that it is set here, before bindRuntime is called. - this._baseSnapshot = details.snapshot; - const packages = details.pkg; - let entry: FluidDataStoreRegistryEntry | undefined; - let registry: IFluidDataStoreRegistry | undefined = this._containerRuntime.IFluidDataStoreRegistry; - let factory: IFluidDataStoreFactory | undefined; - let lastPkg: string | undefined; - for (const pkg of packages) { - if (!registry) { - return this.rejectDeferredRealize(`No registry for ${lastPkg} package`); - } - lastPkg = pkg; - entry = await registry.get(pkg); - if (!entry) { - return this.rejectDeferredRealize(`Registry does not contain entry for the package ${pkg}`); - } - factory = entry.IFluidDataStoreFactory; - registry = entry.IFluidDataStoreRegistry; + this.realizeCore().catch((error) => { + this.channelDeferred?.reject(error); + }); + } + return this.channelDeferred.promise; + } + + private async realizeCore(): Promise { + this.channelDeferred = new Deferred(); + const details = await this.getInitialSnapshotDetails(); + // Base snapshot is the baseline where pending ops are applied to. + // It is important that this be in sync with the pending ops, and also + // that it is set here, before bindRuntime is called. + this._baseSnapshot = details.snapshot; + const packages = details.pkg; + let entry: FluidDataStoreRegistryEntry | undefined; + let registry: IFluidDataStoreRegistry | undefined = this._containerRuntime.IFluidDataStoreRegistry; + let factory: IFluidDataStoreFactory | undefined; + let lastPkg: string | undefined; + for (const pkg of packages) { + if (!registry) { + return this.rejectDeferredRealize(`No registry for ${lastPkg} package`); } - if (factory === undefined) { - return this.rejectDeferredRealize(`Can't find factory for ${lastPkg} package`); + lastPkg = pkg; + entry = await registry.get(pkg); + if (!entry) { + return this.rejectDeferredRealize(`Registry does not contain entry for the package ${pkg}`); } - // During this call we will invoke the instantiate method - which will call back into us - // via the bindRuntime call to resolve channelDeferred - factory.instantiateDataStore(this, scope); + factory = entry.IFluidDataStoreFactory; + registry = entry.IFluidDataStoreRegistry; + } + if (factory === undefined) { + return this.rejectDeferredRealize(`Can't find factory for ${lastPkg} package`); } - return this.channelDeferred.promise; + const channel = await factory.instantiateDataStore(this); + + // back-compat: <= 0.25 allows returning nothing and calling bindRuntime() later directly. + if (channel !== undefined) { + this.bindRuntime(channel); + } } /** @@ -353,6 +356,8 @@ export abstract class FluidDataStoreContext extends EventEmitter implements } } + await this.realize(); + const { pkg } = await this.getInitialSnapshotDetails(); const attributes: IFluidDataStoreAttributes = { @@ -360,8 +365,6 @@ export abstract class FluidDataStoreContext extends EventEmitter implements snapshotFormatVersion: currentSnapshotFormatVersion, }; - await this.realize(); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const entries = await this.channel!.snapshotInternal(fullTree); @@ -375,6 +378,8 @@ export abstract class FluidDataStoreContext extends EventEmitter implements } private async summarizeInternal(fullTree: boolean): Promise { + await this.realize(); + const { pkg } = await this.getInitialSnapshotDetails(); const attributes: IFluidDataStoreAttributes = { @@ -382,8 +387,6 @@ export abstract class FluidDataStoreContext extends EventEmitter implements snapshotFormatVersion: currentSnapshotFormatVersion, }; - await this.realize(); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const channel = this.channel!; if (channel.summarize !== undefined) { @@ -481,30 +484,44 @@ export abstract class FluidDataStoreContext extends EventEmitter implements throw new Error("Runtime already bound"); } - assert (this.channelDeferred !== undefined); + try + { + if (this.channelDeferred === undefined) { + // create deferred first, such that we can reject it in catch() block if assert fires. + this.channelDeferred = new Deferred(); + assert(this.detachedRuntimeCreation); + this.detachedRuntimeCreation = false; + } else { + assert(!this.detachedRuntimeCreation); + } + // pkg should be set for all paths except possibly for detached creation + assert(this.pkg !== undefined, "Please call bindDetachedRuntime()!"); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const pending = this.pending!; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const pending = this.pending!; - if (pending.length > 0) { - // Apply all pending ops - for (const op of pending) { - channel.process(op, false, undefined /* localOpMetadata */); + if (pending.length > 0) { + // Apply all pending ops + for (const op of pending) { + channel.process(op, false, undefined /* localOpMetadata */); + } } - } - this.pending = undefined; + this.pending = undefined; - // And now mark the runtime active - this.loaded = true; - this.channel = channel; + // And now mark the runtime active + this.loaded = true; + this.channel = channel; - // Freeze the package path to ensure that someone doesn't modify it when it is - // returned in packagePath(). - Object.freeze(this.pkg); + // Freeze the package path to ensure that someone doesn't modify it when it is + // returned in packagePath(). + Object.freeze(this.pkg); - // And notify the pending promise it is now available - this.channelDeferred.resolve(this.channel); + // And notify the pending promise it is now available + this.channelDeferred.resolve(this.channel); + } catch (error) { + this.channelDeferred?.reject(error); + } // notify the runtime if they want to propagate up. Used for logging. this.containerRuntime.notifyDataStoreInstantiated(this); @@ -665,10 +682,10 @@ export class RemotedFluidDataStoreContext extends FluidDataStoreContext { } } -export class LocalFluidDataStoreContext extends FluidDataStoreContext { +export class LocalFluidDataStoreContextBase extends FluidDataStoreContext { constructor( id: string, - pkg: string[], + pkg: string[] | undefined, runtime: ContainerRuntime, storage: IDocumentStorageService, scope: IFluidObject & IFluidObject, @@ -732,3 +749,69 @@ export class LocalFluidDataStoreContext extends FluidDataStoreContext { }; } } + +export class LocalFluidDataStoreContext extends LocalFluidDataStoreContextBase { + constructor( + id: string, + pkg: string[], + runtime: ContainerRuntime, + storage: IDocumentStorageService, + scope: IFluidObject & IFluidObject, + summaryTracker: SummaryTracker, + createSummarizerNode: CreateChildSummarizerNodeFn, + bindChannel: (channel: IFluidDataStoreChannel) => void, + ) { + super( + id, + pkg, + runtime, + storage, + scope, + summaryTracker, + createSummarizerNode, + bindChannel); + } +} + +export class LocalDetachedFluidDataStoreContext + extends LocalFluidDataStoreContextBase + implements IFluidDataStoreContextDetached +{ + constructor( + id: string, + runtime: ContainerRuntime, + storage: IDocumentStorageService, + scope: IFluidObject & IFluidObject, + summaryTracker: SummaryTracker, + createSummarizerNode: CreateChildSummarizerNodeFn, + bindChannel: (channel: IFluidDataStoreChannel) => void, + ) { + super( + id, + undefined, // pkg + runtime, + storage, + scope, + summaryTracker, + createSummarizerNode, + bindChannel); + assert(this.pkg === undefined); + this.detachedRuntimeCreation = true; + } + + public bindDetachedRuntime(runtime, pkg: string[]) { + assert(this.detachedRuntimeCreation); + assert(this.pkg === undefined); + + assert(pkg !== undefined); + this.pkg = pkg; + super.bindRuntime(runtime); + } + + protected async getInitialSnapshotDetails(): Promise { + if (this.detachedRuntimeCreation) { + throw new Error("Detached Fluid Data Store context can't be realized! Please attach runtime first!"); + } + return super.getInitialSnapshotDetails(); + } +} diff --git a/packages/runtime/container-runtime/src/test/dataStoreContext.spec.ts b/packages/runtime/container-runtime/src/test/dataStoreContext.spec.ts index 9a6b26ac64c6..829e174993a8 100644 --- a/packages/runtime/container-runtime/src/test/dataStoreContext.spec.ts +++ b/packages/runtime/container-runtime/src/test/dataStoreContext.spec.ts @@ -55,7 +55,7 @@ describe("Data Store Context Tests", () => { beforeEach(async () => { const factory: IFluidDataStoreFactory = { get IFluidDataStoreFactory() { return factory; }, - instantiateDataStore: (context: IFluidDataStoreContext) => { }, + instantiateDataStore: async (context: IFluidDataStoreContext) => new MockFluidDataStoreRuntime(), }; const registry: IFluidDataStoreRegistry = { get IFluidDataStoreRegistry() { return registry; }, @@ -82,7 +82,6 @@ describe("Data Store Context Tests", () => { // eslint-disable-next-line @typescript-eslint/no-floating-promises localDataStoreContext.realize(); - localDataStoreContext.bindRuntime(new MockFluidDataStoreRuntime()); const attachMessage = localDataStoreContext.generateAttachMessage(); const blob = attachMessage.snapshot.entries[0].value as IBlob; @@ -145,7 +144,6 @@ describe("Data Store Context Tests", () => { // eslint-disable-next-line @typescript-eslint/no-floating-promises localDataStoreContext.realize(); - localDataStoreContext.bindRuntime(new MockFluidDataStoreRuntime()); const attachMessage = localDataStoreContext.generateAttachMessage(); const blob = attachMessage.snapshot.entries[0].value as IBlob; @@ -174,7 +172,7 @@ describe("Data Store Context Tests", () => { const factory: { [key: string]: any } = {}; factory.IFluidDataStoreFactory = factory; factory.instantiateDataStore = - (context: IFluidDataStoreContext) => { context.bindRuntime(new MockFluidDataStoreRuntime()); }; + (context: IFluidDataStoreContext) => new MockFluidDataStoreRuntime(); const registry: { [key: string]: any } = {}; registry.IFluidDataStoreRegistry = registry; registry.get = async (pkg) => Promise.resolve(factory); diff --git a/packages/runtime/container-runtime/src/test/dataStoreCreation.spec.ts b/packages/runtime/container-runtime/src/test/dataStoreCreation.spec.ts index 5749cc25c372..ec2a034beda7 100644 --- a/packages/runtime/container-runtime/src/test/dataStoreCreation.spec.ts +++ b/packages/runtime/container-runtime/src/test/dataStoreCreation.spec.ts @@ -56,9 +56,7 @@ describe("Data Store Creation Tests", () => { const registryEntries = new Map(entries); const factory: IFluidDataStoreFactory = { get IFluidDataStoreFactory() { return factory; }, - instantiateDataStore: (context: IFluidDataStoreContext) => { - context.bindRuntime(new MockFluidDataStoreRuntime()); - }, + instantiateDataStore: async (context: IFluidDataStoreContext) => new MockFluidDataStoreRuntime(), }; const registry: IFluidDataStoreRegistry = { get IFluidDataStoreRegistry() { return registry; }, diff --git a/packages/runtime/datastore/src/dataStoreRuntime.ts b/packages/runtime/datastore/src/dataStoreRuntime.ts index 511e93f39b90..879be359b3fd 100644 --- a/packages/runtime/datastore/src/dataStoreRuntime.ts +++ b/packages/runtime/datastore/src/dataStoreRuntime.ts @@ -102,7 +102,6 @@ export class FluidDataStoreRuntime extends EventEmitter implements IFluidDataSto dataStoreRegistry, logger); - context.bindRuntime(runtime); return runtime; } diff --git a/packages/runtime/runtime-definitions/src/componentContext.ts b/packages/runtime/runtime-definitions/src/componentContext.ts index 3cd5bc2881b8..184b2805af5e 100644 --- a/packages/runtime/runtime-definitions/src/componentContext.ts +++ b/packages/runtime/runtime-definitions/src/componentContext.ts @@ -7,7 +7,6 @@ import { EventEmitter } from "events"; import { ITelemetryLogger, IDisposable } from "@fluidframework/common-definitions"; import { IFluidObject, - IFluidLoadable, IFluidRouter, IProvideFluidHandleContext, IProvideFluidSerializer, @@ -94,13 +93,17 @@ export interface IContainerRuntimeBase extends * (or any of its parts, like DDS) into already attached DDS (or non-attached DDS that will eventually * gets attached to storage) will result in this store being attached to storage. * @param pkg - Package name of the data store factory - * @param scope - scope object provided by instantiator of data store. It defines an environment for data store - * It may contain various background services, factories, etc. It's responsibility of data store to - * serialize enough state in data store itself for future invocations to have same environment. - * Usually this is done by working with IFluidLoadable objects and storing handles to those objects in root - * directory for future reference. */ - createDataStore(pkg: string | string[], scope?: IFluidDataStoreScope): Promise; + createDataStore(pkg: string | string[]): Promise; + + /** + * Creates data store. Returns router of data store. Data store is not bound to container, + * store in such state is not persisted to storage (file). Storing a handle to this store + * (or any of its parts, like DDS) into already attached DDS (or non-attached DDS that will eventually + * gets attached to storage) will result in this store being attached to storage. + * @param pkg - Package name of the data store factory + */ + createDetachedDataStore(): IFluidDataStoreContextDetached; /** * Get an absolute url for a provided container-relative request. @@ -299,11 +302,6 @@ export interface IFluidDataStoreContext extends EventEmitter { */ submitSignal(type: string, content: any): void; - /** - * Binds a runtime to the context. - */ - bindRuntime(dataStoreRuntime: IFluidDataStoreChannel): void; - /** * Register the runtime to the container * @param dataStoreRuntime - runtime to attach @@ -345,7 +343,9 @@ export interface IFluidDataStoreContext extends EventEmitter { composeSubpackagePath(subpackage: string): Promise; } -export interface IFluidDataStoreScope { - loadable: {[key: string]: IFluidObject & IFluidLoadable}; - runtimeEnvironment: {[key: string]: any }; +export interface IFluidDataStoreContextDetached extends IFluidDataStoreContext { + /** + * Binds a runtime to the context. + */ + bindDetachedRuntime(dataStoreRuntime: IFluidDataStoreChannel, pkg: string[]): void; } diff --git a/packages/runtime/runtime-definitions/src/componentFactory.ts b/packages/runtime/runtime-definitions/src/componentFactory.ts index e298165baecb..9c410d5be674 100644 --- a/packages/runtime/runtime-definitions/src/componentFactory.ts +++ b/packages/runtime/runtime-definitions/src/componentFactory.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. */ -import { IFluidDataStoreContext, IFluidDataStoreScope } from "./componentContext"; +import { IFluidDataStoreContext, IFluidDataStoreChannel } from "./componentContext"; declare module "@fluidframework/core-interfaces" { // eslint-disable-next-line @typescript-eslint/no-empty-interface @@ -29,13 +29,6 @@ export interface IFluidDataStoreFactory extends IProvideFluidDataStoreFactory { /** * Generates runtime for the component from the component context. Once created should be bound to the context. * @param context - Context for the component. - * @param scope - scope object provided by instantiator of data store on creation. It defines an environment for - * data store. It may contain various background services, factories, etc. It's responsibility of data store to - * serialize enough state in data store itself for future invocations to have same environment. - * Usually this is done by working with IFluidLoadable objects and storing handles to those objects in root - * directory for future reference. - * It's undefined when loading existing data store, and not undefined (maybe empty object if not provided by - * calleee) when create new data store. */ - instantiateDataStore(context: IFluidDataStoreContext, scope?: IFluidDataStoreScope): void; + instantiateDataStore(context: IFluidDataStoreContext): Promise; } diff --git a/packages/test/test-utils/src/testFluidComponent.ts b/packages/test/test-utils/src/testFluidComponent.ts index 8d8cb3da7e9f..fda66bf14d4c 100644 --- a/packages/test/test-utils/src/testFluidComponent.ts +++ b/packages/test/test-utils/src/testFluidComponent.ts @@ -138,7 +138,7 @@ export class TestFluidComponentFactory implements IFluidDataStoreFactory { */ constructor(private readonly factoryEntries: ChannelFactoryRegistry) { } - public instantiateDataStore(context: IFluidDataStoreContext): void { + public async instantiateDataStore(context: IFluidDataStoreContext) { const dataTypes = new Map(); // Add SharedMap's factory which will be used to create the root map. @@ -171,5 +171,7 @@ export class TestFluidComponentFactory implements IFluidDataStoreFactory { const testFluidComponent = await testFluidComponentP; return testFluidComponent.request(request); }); + + return runtime; } } From c4cc9c6608fde085341d1d065308f04286f9a9a4 Mon Sep 17 00:00:00 2001 From: Vlad Sudzilouski Date: Fri, 7 Aug 2020 18:15:13 -0700 Subject: [PATCH 07/24] Make IFluidDataStoreFactory.type non-optional --- packages/framework/component-base/src/runtimefactory.ts | 2 +- packages/framework/component-base/src/sharedcomponentfactory.ts | 2 +- .../runtime/container-runtime/src/test/dataStoreContext.spec.ts | 1 + .../container-runtime/src/test/dataStoreCreation.spec.ts | 1 + packages/runtime/runtime-definitions/src/componentFactory.ts | 2 +- 5 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/framework/component-base/src/runtimefactory.ts b/packages/framework/component-base/src/runtimefactory.ts index 4b7b91c55369..7854285fc994 100644 --- a/packages/framework/component-base/src/runtimefactory.ts +++ b/packages/framework/component-base/src/runtimefactory.ts @@ -34,7 +34,7 @@ export class RuntimeFactory implements IRuntimeFactory { : components.concat(defaultComponent) ).map( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - (factory) => [factory.type!, factory]) as NamedFluidDataStoreRegistryEntries; + (factory) => [factory.type, factory]) as NamedFluidDataStoreRegistryEntries; } public get IRuntimeFactory() { return this; } diff --git a/packages/framework/component-base/src/sharedcomponentfactory.ts b/packages/framework/component-base/src/sharedcomponentfactory.ts index 14bfe48ff593..00afec68ec20 100644 --- a/packages/framework/component-base/src/sharedcomponentfactory.ts +++ b/packages/framework/component-base/src/sharedcomponentfactory.ts @@ -37,7 +37,7 @@ export class PureDataObjectFactory implements IFluidDa this.IFluidDataStoreRegistry = new FluidDataStoreRegistry( components.map( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - (factory) => [factory.type!, factory]) as NamedFluidDataStoreRegistryEntries); + (factory) => [factory.type, factory]) as NamedFluidDataStoreRegistryEntries); } this.ISharedObjectRegistry = new Map( diff --git a/packages/runtime/container-runtime/src/test/dataStoreContext.spec.ts b/packages/runtime/container-runtime/src/test/dataStoreContext.spec.ts index 829e174993a8..7d231d3b4b3f 100644 --- a/packages/runtime/container-runtime/src/test/dataStoreContext.spec.ts +++ b/packages/runtime/container-runtime/src/test/dataStoreContext.spec.ts @@ -54,6 +54,7 @@ describe("Data Store Context Tests", () => { let containerRuntime: ContainerRuntime; beforeEach(async () => { const factory: IFluidDataStoreFactory = { + type: "store-type", get IFluidDataStoreFactory() { return factory; }, instantiateDataStore: async (context: IFluidDataStoreContext) => new MockFluidDataStoreRuntime(), }; diff --git a/packages/runtime/container-runtime/src/test/dataStoreCreation.spec.ts b/packages/runtime/container-runtime/src/test/dataStoreCreation.spec.ts index ec2a034beda7..3807097cd785 100644 --- a/packages/runtime/container-runtime/src/test/dataStoreCreation.spec.ts +++ b/packages/runtime/container-runtime/src/test/dataStoreCreation.spec.ts @@ -55,6 +55,7 @@ describe("Data Store Creation Tests", () => { ): FluidDataStoreRegistryEntry { const registryEntries = new Map(entries); const factory: IFluidDataStoreFactory = { + type: "store-type", get IFluidDataStoreFactory() { return factory; }, instantiateDataStore: async (context: IFluidDataStoreContext) => new MockFluidDataStoreRuntime(), }; diff --git a/packages/runtime/runtime-definitions/src/componentFactory.ts b/packages/runtime/runtime-definitions/src/componentFactory.ts index 9c410d5be674..8baa7b90c7b7 100644 --- a/packages/runtime/runtime-definitions/src/componentFactory.ts +++ b/packages/runtime/runtime-definitions/src/componentFactory.ts @@ -24,7 +24,7 @@ export interface IFluidDataStoreFactory extends IProvideFluidDataStoreFactory { /** * String that uniquely identifies the type of component created by this factory. */ - type?: string; + type: string; /** * Generates runtime for the component from the component context. Once created should be bound to the context. From 940fde767be823d364b8a487421d0fd55aa693cc Mon Sep 17 00:00:00 2001 From: Vlad Sudzilouski Date: Fri, 7 Aug 2020 22:09:44 -0700 Subject: [PATCH 08/24] Remove IFluidDataStoreRegistry from FluidDataStoreRuntime --- .../componentFactories/sharedComponentFactory.ts | 10 ++++++++-- .../component-base/src/sharedcomponentfactory.ts | 1 - .../container-runtime/src/dataStoreContext.ts | 16 +++++++++++++--- .../runtime/datastore/src/dataStoreRuntime.ts | 5 ----- .../runtime-definitions/src/componentContext.ts | 7 +++++-- 5 files changed, 26 insertions(+), 13 deletions(-) diff --git a/packages/framework/aqueduct/src/componentFactories/sharedComponentFactory.ts b/packages/framework/aqueduct/src/componentFactories/sharedComponentFactory.ts index 29375cbf7422..e994136f5acc 100644 --- a/packages/framework/aqueduct/src/componentFactories/sharedComponentFactory.ts +++ b/packages/framework/aqueduct/src/componentFactories/sharedComponentFactory.ts @@ -10,6 +10,7 @@ import { IFluidDataStoreContext, IFluidDataStoreFactory, IFluidDataStoreRegistry, + FluidDataStoreRegistryEntry, IProvideFluidDataStoreRegistry, NamedFluidDataStoreRegistryEntries, NamedFluidDataStoreRegistryEntry, @@ -92,7 +93,6 @@ export class PureDataObjectFactory

implem const runtime = FluidDataStoreRuntime.load( context, this.sharedObjectRegistry, - this.registry, ); let instanceP: Promise; @@ -154,7 +154,13 @@ export class PureDataObjectFactory

implem const newContext = context.containerRuntime.createDetachedDataStore(); const runtime = this.instantiateDataStoreCore(newContext, initialState); - newContext.bindDetachedRuntime(runtime, packagePath); + + const entry: FluidDataStoreRegistryEntry = { + IFluidDataStoreRegistry: this.registry, + IFluidDataStoreFactory: this, + }; + + newContext.attachRuntime(runtime, packagePath, entry); return requestFluidObject>(runtime, "/"); } diff --git a/packages/framework/component-base/src/sharedcomponentfactory.ts b/packages/framework/component-base/src/sharedcomponentfactory.ts index 00afec68ec20..61a3bab2403d 100644 --- a/packages/framework/component-base/src/sharedcomponentfactory.ts +++ b/packages/framework/component-base/src/sharedcomponentfactory.ts @@ -107,7 +107,6 @@ export class PureDataObjectFactory implements IFluidDa return FluidDataStoreRuntime.load( context, this.ISharedObjectRegistry, - this.IFluidDataStoreRegistry, ); } } diff --git a/packages/runtime/container-runtime/src/dataStoreContext.ts b/packages/runtime/container-runtime/src/dataStoreContext.ts index 8c117d05615f..b027349cbf56 100644 --- a/packages/runtime/container-runtime/src/dataStoreContext.ts +++ b/packages/runtime/container-runtime/src/dataStoreContext.ts @@ -161,6 +161,8 @@ export abstract class FluidDataStoreContext extends EventEmitter implements return this._attachState; } + protected IFluidDataStoreRegistry: IFluidDataStoreRegistry | undefined; + protected detachedRuntimeCreation = false; public readonly bindToContext: (channel: IFluidDataStoreChannel) => void; protected channel: IFluidDataStoreChannel | undefined; @@ -263,6 +265,8 @@ export abstract class FluidDataStoreContext extends EventEmitter implements return this.rejectDeferredRealize(`Can't find factory for ${lastPkg} package`); } + assert(this.IFluidDataStoreRegistry === undefined); + this.IFluidDataStoreRegistry = registry; const channel = await factory.instantiateDataStore(this); // back-compat: <= 0.25 allows returning nothing and calling bindRuntime() later directly. @@ -495,7 +499,7 @@ export abstract class FluidDataStoreContext extends EventEmitter implements assert(!this.detachedRuntimeCreation); } // pkg should be set for all paths except possibly for detached creation - assert(this.pkg !== undefined, "Please call bindDetachedRuntime()!"); + assert(this.pkg !== undefined, "Please call attachRuntime()!"); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const pending = this.pending!; @@ -554,7 +558,7 @@ export abstract class FluidDataStoreContext extends EventEmitter implements // Look for the package entry in our sub-registry. If we find the entry, we need to add our path // to the packagePath. If not, look into the global registry and the packagePath becomes just the // passed package. - if (await this.channel?.IFluidDataStoreRegistry?.get(subpackage)) { + if (await this.IFluidDataStoreRegistry?.get(subpackage)) { packagePath.push(subpackage); } else { if (!(await this._containerRuntime.IFluidDataStoreRegistry.get(subpackage))) { @@ -799,12 +803,18 @@ export class LocalDetachedFluidDataStoreContext this.detachedRuntimeCreation = true; } - public bindDetachedRuntime(runtime, pkg: string[]) { + public attachRuntime(runtime, pkg: string[], entry: FluidDataStoreRegistryEntry) { assert(this.detachedRuntimeCreation); assert(this.pkg === undefined); assert(pkg !== undefined); this.pkg = pkg; + + assert(this.IFluidDataStoreRegistry === undefined); + this.IFluidDataStoreRegistry = entry.IFluidDataStoreRegistry; + + assert(entry.IFluidDataStoreFactory?.type === pkg[pkg.length - 1]); + super.bindRuntime(runtime); } diff --git a/packages/runtime/datastore/src/dataStoreRuntime.ts b/packages/runtime/datastore/src/dataStoreRuntime.ts index 879be359b3fd..27a2dd8328ac 100644 --- a/packages/runtime/datastore/src/dataStoreRuntime.ts +++ b/packages/runtime/datastore/src/dataStoreRuntime.ts @@ -42,7 +42,6 @@ import { import { IAttachMessage, IFluidDataStoreContext, - IFluidDataStoreRegistry, IFluidDataStoreChannel, IEnvelope, IInboundSignalMessage, @@ -83,7 +82,6 @@ export class FluidDataStoreRuntime extends EventEmitter implements IFluidDataSto public static load( context: IFluidDataStoreContext, sharedObjectRegistry: ISharedObjectRegistry, - dataStoreRegistry?: IFluidDataStoreRegistry, ): FluidDataStoreRuntime { const logger = ChildLogger.create(context.containerRuntime.logger, undefined, { dataStoreId: uuid() }); const runtime = new FluidDataStoreRuntime( @@ -99,7 +97,6 @@ export class FluidDataStoreRuntime extends EventEmitter implements IFluidDataSto context.getAudience(), context.snapshotFn, sharedObjectRegistry, - dataStoreRegistry, logger); return runtime; @@ -153,7 +150,6 @@ export class FluidDataStoreRuntime extends EventEmitter implements IFluidDataSto public get IFluidSerializer() { return this.dataStoreContext.containerRuntime.IFluidSerializer; } public get IFluidHandleContext() { return this; } - public get IFluidDataStoreRegistry() { return this.dataStoreRegistry; } private _disposed = false; public get disposed() { return this._disposed; } @@ -184,7 +180,6 @@ export class FluidDataStoreRuntime extends EventEmitter implements IFluidDataSto private readonly audience: IAudience, private readonly snapshotFn: (message: string) => Promise, private readonly sharedObjectRegistry: ISharedObjectRegistry, - private readonly dataStoreRegistry: IFluidDataStoreRegistry | undefined, public readonly logger: ITelemetryLogger, ) { super(); diff --git a/packages/runtime/runtime-definitions/src/componentContext.ts b/packages/runtime/runtime-definitions/src/componentContext.ts index 184b2805af5e..d17459c24af8 100644 --- a/packages/runtime/runtime-definitions/src/componentContext.ts +++ b/packages/runtime/runtime-definitions/src/componentContext.ts @@ -29,7 +29,7 @@ import { ISnapshotTree, ITreeEntry, } from "@fluidframework/protocol-definitions"; -import { IProvideFluidDataStoreRegistry } from "./componentRegistry"; +import { IProvideFluidDataStoreRegistry, FluidDataStoreRegistryEntry } from "./componentRegistry"; import { IInboundSignalMessage } from "./protocol"; import { ISummaryTreeWithStats, ISummarizerNode, SummarizeInternalFn, CreateChildSummarizerNodeParam } from "./summary"; @@ -347,5 +347,8 @@ export interface IFluidDataStoreContextDetached extends IFluidDataStoreContext { /** * Binds a runtime to the context. */ - bindDetachedRuntime(dataStoreRuntime: IFluidDataStoreChannel, pkg: string[]): void; + attachRuntime( + dataStoreRuntime: IFluidDataStoreChannel, + pkg: string[], + entry: FluidDataStoreRegistryEntry): void; } From c3370dff3eafd86f865b750910784ddd726cdb8a Mon Sep 17 00:00:00 2001 From: Vlad Sudzilouski Date: Fri, 7 Aug 2020 23:53:24 -0700 Subject: [PATCH 09/24] Fix UT --- .../src/test/dataStoreContext.spec.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/runtime/container-runtime/src/test/dataStoreContext.spec.ts b/packages/runtime/container-runtime/src/test/dataStoreContext.spec.ts index 7d231d3b4b3f..aa875acd740d 100644 --- a/packages/runtime/container-runtime/src/test/dataStoreContext.spec.ts +++ b/packages/runtime/container-runtime/src/test/dataStoreContext.spec.ts @@ -70,7 +70,7 @@ describe("Data Store Context Tests", () => { } as ContainerRuntime; }); - it("Check LocalDataStore Attributes", () => { + it("Check LocalDataStore Attributes", async () => { localDataStoreContext = new LocalFluidDataStoreContext( dataStoreId, ["TestDataStore1"], @@ -81,8 +81,7 @@ describe("Data Store Context Tests", () => { createSummarizerNodeFn, attachCb); - // eslint-disable-next-line @typescript-eslint/no-floating-promises - localDataStoreContext.realize(); + await localDataStoreContext.realize(); const attachMessage = localDataStoreContext.generateAttachMessage(); const blob = attachMessage.snapshot.entries[0].value as IBlob; @@ -125,7 +124,8 @@ describe("Data Store Context Tests", () => { registryWithSubRegistries.IFluidDataStoreFactory = registryWithSubRegistries; registryWithSubRegistries.IFluidDataStoreRegistry = registryWithSubRegistries; registryWithSubRegistries.get = async (pkg) => Promise.resolve(registryWithSubRegistries); - registryWithSubRegistries.instantiateDataStore = (context: IFluidDataStoreContext) => { }; + registryWithSubRegistries.instantiateDataStore = + async (context: IFluidDataStoreContext) => new MockFluidDataStoreRuntime(); // eslint-disable-next-line @typescript-eslint/consistent-type-assertions containerRuntime = { @@ -143,8 +143,7 @@ describe("Data Store Context Tests", () => { createSummarizerNodeFn, attachCb); - // eslint-disable-next-line @typescript-eslint/no-floating-promises - localDataStoreContext.realize(); + await localDataStoreContext.realize(); const attachMessage = localDataStoreContext.generateAttachMessage(); const blob = attachMessage.snapshot.entries[0].value as IBlob; From d3985545ab13917051e221952e15d1cf6c33e1d2 Mon Sep 17 00:00:00 2001 From: Vlad Sudzilouski Date: Sun, 9 Aug 2020 10:20:49 -0700 Subject: [PATCH 10/24] Improve createInstance workflow --- .../simple-component-embed/src/index.tsx | 2 +- .../src/waterPark.tsx | 4 +- .../constellation-model/src/model.ts | 2 +- components/experimental/spaces/src/spaces.tsx | 3 +- .../table-document/src/document.ts | 2 +- .../experimental/table-view/src/tableview.ts | 2 +- .../vltava/src/components/anchor/anchor.ts | 7 ++- .../vltava/src/components/vltava/vltava.tsx | 3 +- .../primedComponentFactory.ts | 10 +--- .../sharedComponentFactory.ts | 55 ++++++++++++------- .../container-runtime/src/dataStoreContext.ts | 22 +++++--- .../src/componentContext.ts | 2 +- .../src/test/componentHandle.spec.ts | 2 +- 13 files changed, 67 insertions(+), 49 deletions(-) diff --git a/components/examples/simple-component-embed/src/index.tsx b/components/examples/simple-component-embed/src/index.tsx index 0b1866d6d48e..59ce1ad76148 100644 --- a/components/examples/simple-component-embed/src/index.tsx +++ b/components/examples/simple-component-embed/src/index.tsx @@ -24,7 +24,7 @@ export class SimpleComponentEmbed extends DataObject implements IFluidHTMLView { * but in this scenario we only want it to be created once. */ protected async initializingFirstTime() { - const component = await this.createFluidObject(ClickerInstantiationFactory.type); + const component = await ClickerInstantiationFactory.createInstance(this.context); this.root.set("myEmbeddedCounter", component.handle); } diff --git a/components/experimental/external-component-loader/src/waterPark.tsx b/components/experimental/external-component-loader/src/waterPark.tsx index bee5434b80f6..6737fc089208 100644 --- a/components/experimental/external-component-loader/src/waterPark.tsx +++ b/components/experimental/external-component-loader/src/waterPark.tsx @@ -133,9 +133,9 @@ export class WaterPark extends DataObject implements IFluidHTMLView { } protected async initializingFirstTime() { - const storage = await this.createFluidObject(SpacesStorage.ComponentName); + const storage = await SpacesStorage.getFactory().createInstance(this.context); this.root.set(storageKey, storage.handle); - const loader = await this.createFluidObject(ExternalComponentLoader.ComponentName); + const loader = await ExternalComponentLoader.getFactory().createInstance(this.context); this.root.set(loaderKey, loader.handle); } diff --git a/components/experimental/multiview/constellation-model/src/model.ts b/components/experimental/multiview/constellation-model/src/model.ts index 14dd0484bfd1..64e80a9ad78b 100644 --- a/components/experimental/multiview/constellation-model/src/model.ts +++ b/components/experimental/multiview/constellation-model/src/model.ts @@ -63,7 +63,7 @@ export class Constellation extends DataObject implements IConstellation { public async addStar(x: number, y: number): Promise { const starHandles = this.root.get[]>(starListKey); - const newStar: Coordinate = (await Coordinate.getFactory().createInstance(this.context)) as Coordinate; + const newStar = await Coordinate.getFactory().createInstance(this.context); newStar.x = x; newStar.y = y; starHandles.push(newStar.handle); diff --git a/components/experimental/spaces/src/spaces.tsx b/components/experimental/spaces/src/spaces.tsx index c2152b89f5aa..549db9f891d5 100644 --- a/components/experimental/spaces/src/spaces.tsx +++ b/components/experimental/spaces/src/spaces.tsx @@ -126,8 +126,7 @@ export class Spaces extends DataObject implements IFluidHTMLView { } protected async initializingFirstTime() { - const storageComponent = - await this.createFluidObject>(SpacesStorage.ComponentName); + const storageComponent = await SpacesStorage.getFactory().createInstance(this.context); this.root.set(SpacesStorageKey, storageComponent.handle); // Set the saved template if there is a template query param const urlParams = new URLSearchParams(window.location.search); diff --git a/components/experimental/table-document/src/document.ts b/components/experimental/table-document/src/document.ts index e8a5309121bb..fe22a12144f0 100644 --- a/components/experimental/table-document/src/document.ts +++ b/components/experimental/table-document/src/document.ts @@ -79,7 +79,7 @@ export class TableDocument extends DataObject<{}, {}, ITableDocumentEvents> impl const component = await TableSlice.getFactory().createInstance( this.context, { docId: this.runtime.id, name, minRow, minCol, maxRow, maxCol }, - ) as TableSlice; + ); this.root.set(sliceId, component.handle); return component; } diff --git a/components/experimental/table-view/src/tableview.ts b/components/experimental/table-view/src/tableview.ts index a49c8bcc8a1e..7384e3fbfa12 100644 --- a/components/experimental/table-view/src/tableview.ts +++ b/components/experimental/table-view/src/tableview.ts @@ -92,7 +92,7 @@ export class TableView extends DataObject implements IFluidHTMLView { protected async initializingFirstTime() { // Set up internal table doc - const doc = await TableDocument.getFactory().createInstance(this.context) as TableDocument; + const doc = await TableDocument.getFactory().createInstance(this.context); this.root.set(innerDocKey, doc.handle); doc.insertRows(0, 5); doc.insertCols(0, 8); diff --git a/components/experimental/vltava/src/components/anchor/anchor.ts b/components/experimental/vltava/src/components/anchor/anchor.ts index 9c903d08561d..3a8ff01e06f3 100644 --- a/components/experimental/vltava/src/components/anchor/anchor.ts +++ b/components/experimental/vltava/src/components/anchor/anchor.ts @@ -8,9 +8,10 @@ import { DataObject, DataObjectFactory } from "@fluidframework/aqueduct"; import { IFluidLastEditedTracker, IProvideFluidLastEditedTracker, - LastEditedTrackerComponentName, + LastEditedTrackerComponent, } from "@fluidframework/last-edited-experimental"; import { IFluidHTMLView, IProvideFluidHTMLView } from "@fluidframework/view-interfaces"; +import { Vltava } from "../vltava"; export const AnchorName = "anchor"; @@ -48,10 +49,10 @@ export class Anchor extends DataObject implements IProvideFluidHTMLView, IProvid } protected async initializingFirstTime() { - const defaultComponent = await this.createFluidObject("vltava"); + const defaultComponent = await Vltava.getFactory().createInstance(this.context); this.root.set(this.defaultComponentId, defaultComponent.handle); - const lastEditedComponent = await this.createFluidObject(LastEditedTrackerComponentName); + const lastEditedComponent = await LastEditedTrackerComponent.getFactory().createInstance(this.context); this.root.set(this.lastEditedComponentId, lastEditedComponent.handle); } diff --git a/components/experimental/vltava/src/components/vltava/vltava.tsx b/components/experimental/vltava/src/components/vltava/vltava.tsx index 1cf401df94d7..708fcb544b5a 100644 --- a/components/experimental/vltava/src/components/vltava/vltava.tsx +++ b/components/experimental/vltava/src/components/vltava/vltava.tsx @@ -9,6 +9,7 @@ import { IFluidHTMLView } from "@fluidframework/view-interfaces"; import React from "react"; import ReactDOM from "react-dom"; +import { TabsComponent } from "../tabs"; import { IVltavaDataModel, VltavaDataModel } from "./dataModel"; import { VltavaView } from "./view"; @@ -37,7 +38,7 @@ export class Vltava extends DataObject implements IFluidHTMLView { public get IFluidHTMLView() { return this; } protected async initializingFirstTime() { - const tabsComponent = await this.createFluidObject("tabs"); + const tabsComponent = await TabsComponent.getFactory().createInstance(this.context); this.root.set("tabs-component-id", tabsComponent.handle); } diff --git a/packages/framework/aqueduct/src/componentFactories/primedComponentFactory.ts b/packages/framework/aqueduct/src/componentFactories/primedComponentFactory.ts index 0a61934ddaea..38c490e45abe 100644 --- a/packages/framework/aqueduct/src/componentFactories/primedComponentFactory.ts +++ b/packages/framework/aqueduct/src/componentFactories/primedComponentFactory.ts @@ -3,9 +3,6 @@ * Licensed under the MIT License. */ -import { - IFluidObject, -} from "@fluidframework/core-interfaces"; import { DirectoryFactory, MapFactory, @@ -30,14 +27,11 @@ import { PureDataObjectFactory } from "./sharedComponentFactory"; * P - represents a type that will define optional providers that will be injected * S - the initial state type that the produced component may take during creation */ -export class DataObjectFactory< - P extends IFluidObject = object, - S = undefined> - extends PureDataObjectFactory +export class DataObjectFactory, P, S> extends PureDataObjectFactory { constructor( type: string, - ctor: new (props: ISharedComponentProps

) => DataObject, + ctor: new (props: ISharedComponentProps

) => TObj, sharedObjects: readonly IChannelFactory[] = [], optionalProviders: ComponentSymbolProvider

, registryEntries?: NamedFluidDataStoreRegistryEntries, diff --git a/packages/framework/aqueduct/src/componentFactories/sharedComponentFactory.ts b/packages/framework/aqueduct/src/componentFactories/sharedComponentFactory.ts index e994136f5acc..53cf3ccef736 100644 --- a/packages/framework/aqueduct/src/componentFactories/sharedComponentFactory.ts +++ b/packages/framework/aqueduct/src/componentFactories/sharedComponentFactory.ts @@ -3,14 +3,14 @@ * Licensed under the MIT License. */ -import { IFluidObject, IFluidLoadable, IRequest } from "@fluidframework/core-interfaces"; +import assert from "assert"; +import { IRequest } from "@fluidframework/core-interfaces"; import { FluidDataStoreRuntime, ISharedObjectRegistry } from "@fluidframework/datastore"; import { FluidDataStoreRegistry } from "@fluidframework/container-runtime"; import { IFluidDataStoreContext, IFluidDataStoreFactory, IFluidDataStoreRegistry, - FluidDataStoreRegistryEntry, IProvideFluidDataStoreRegistry, NamedFluidDataStoreRegistryEntries, NamedFluidDataStoreRegistryEntry, @@ -20,7 +20,6 @@ import { ComponentSymbolProvider, DependencyContainer, } from "@fluidframework/synthesize"; -import { requestFluidObject } from "@fluidframework/runtime-utils"; import { ISharedComponentProps, @@ -36,21 +35,24 @@ import { * P - represents a type that will define optional providers that will be injected * S - the initial state type that the produced component may take during creation */ -export class PureDataObjectFactory

implements - IFluidDataStoreFactory, - Partial +export class PureDataObjectFactory, P, S> + implements IFluidDataStoreFactory, Partial { private readonly sharedObjectRegistry: ISharedObjectRegistry; private readonly registry: IFluidDataStoreRegistry | undefined; constructor( public readonly type: string, - private readonly ctor: new (props: ISharedComponentProps

) => PureDataObject, + private readonly ctor: new (props: ISharedComponentProps

) => TObj, sharedObjects: readonly IChannelFactory[], private readonly optionalProviders: ComponentSymbolProvider

, registryEntries?: NamedFluidDataStoreRegistryEntries, private readonly onDemandInstantiation = true, ) { + // empty string is not allowed! + if (!this.type) { + throw new Error("undefined type member"); + } if (registryEntries !== undefined) { this.registry = new FluidDataStoreRegistry(registryEntries); } @@ -146,22 +148,37 @@ export class PureDataObjectFactory

implem public async createInstance( context: IFluidDataStoreContext, initialState?: S, - ): Promise { - if (this.type === "") { - throw new Error("undefined type member"); - } - const packagePath = await context.composeSubpackagePath(this.type); + ) { + const parentPath = context.packagePath; + assert(parentPath.length > 0); + // A factory could not contain the registry for itself. So if it is the same the last snapshot + // pkg, return our package path. + assert(parentPath[parentPath.length - 1] !== this.type); + const packagePath = [...parentPath, this.type]; + + const factory = await context.IFluidDataStoreRegistry?.get(this.type); + assert(factory === this.IFluidDataStoreFactory); + + // const packagePath = await context.composeSubpackagePath(this.type); const newContext = context.containerRuntime.createDetachedDataStore(); - const runtime = this.instantiateDataStoreCore(newContext, initialState); - const entry: FluidDataStoreRegistryEntry = { - IFluidDataStoreRegistry: this.registry, - IFluidDataStoreFactory: this, - }; + // const runtime = this.instantiateDataStoreCore(newContext, initialState); + const runtime = FluidDataStoreRuntime.load( + newContext, + this.sharedObjectRegistry, + ); + + const instanceP = this.instantiateInstance(runtime, newContext, initialState); + + runtime.registerRequestHandler(async (request: IRequest) => { + const instance = await instanceP; + return instance.request(request); + }); - newContext.attachRuntime(runtime, packagePath, entry); + // FluidDataStoreRegistryEntry + newContext.attachRuntime(runtime, packagePath, this); - return requestFluidObject>(runtime, "/"); + return instanceP; } } diff --git a/packages/runtime/container-runtime/src/dataStoreContext.ts b/packages/runtime/container-runtime/src/dataStoreContext.ts index b027349cbf56..b9fc11e6f984 100644 --- a/packages/runtime/container-runtime/src/dataStoreContext.ts +++ b/packages/runtime/container-runtime/src/dataStoreContext.ts @@ -89,8 +89,8 @@ export abstract class FluidDataStoreContext extends EventEmitter implements public get packagePath(): readonly string[] { // The store must be loaded before the path is accessed. assert(this.loaded); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return this.pkg!; + assert(this.pkg !== undefined); + return this.pkg; } public get parentBranch(): string | null { @@ -161,7 +161,11 @@ export abstract class FluidDataStoreContext extends EventEmitter implements return this._attachState; } - protected IFluidDataStoreRegistry: IFluidDataStoreRegistry | undefined; + public get IFluidDataStoreRegistry(): IFluidDataStoreRegistry | undefined { + return this.registry; + } + + protected registry: IFluidDataStoreRegistry | undefined; protected detachedRuntimeCreation = false; public readonly bindToContext: (channel: IFluidDataStoreChannel) => void; @@ -245,6 +249,8 @@ export abstract class FluidDataStoreContext extends EventEmitter implements // that it is set here, before bindRuntime is called. this._baseSnapshot = details.snapshot; const packages = details.pkg; + assert(this.pkg === packages); + let entry: FluidDataStoreRegistryEntry | undefined; let registry: IFluidDataStoreRegistry | undefined = this._containerRuntime.IFluidDataStoreRegistry; let factory: IFluidDataStoreFactory | undefined; @@ -265,8 +271,8 @@ export abstract class FluidDataStoreContext extends EventEmitter implements return this.rejectDeferredRealize(`Can't find factory for ${lastPkg} package`); } - assert(this.IFluidDataStoreRegistry === undefined); - this.IFluidDataStoreRegistry = registry; + assert(this.registry === undefined); + this.registry = registry; const channel = await factory.instantiateDataStore(this); // back-compat: <= 0.25 allows returning nothing and calling bindRuntime() later directly. @@ -558,7 +564,7 @@ export abstract class FluidDataStoreContext extends EventEmitter implements // Look for the package entry in our sub-registry. If we find the entry, we need to add our path // to the packagePath. If not, look into the global registry and the packagePath becomes just the // passed package. - if (await this.IFluidDataStoreRegistry?.get(subpackage)) { + if (await this.registry?.get(subpackage)) { packagePath.push(subpackage); } else { if (!(await this._containerRuntime.IFluidDataStoreRegistry.get(subpackage))) { @@ -810,8 +816,8 @@ export class LocalDetachedFluidDataStoreContext assert(pkg !== undefined); this.pkg = pkg; - assert(this.IFluidDataStoreRegistry === undefined); - this.IFluidDataStoreRegistry = entry.IFluidDataStoreRegistry; + assert(this.registry === undefined); + this.registry = entry.IFluidDataStoreRegistry; assert(entry.IFluidDataStoreFactory?.type === pkg[pkg.length - 1]); diff --git a/packages/runtime/runtime-definitions/src/componentContext.ts b/packages/runtime/runtime-definitions/src/componentContext.ts index d17459c24af8..23a007925f1e 100644 --- a/packages/runtime/runtime-definitions/src/componentContext.ts +++ b/packages/runtime/runtime-definitions/src/componentContext.ts @@ -226,7 +226,7 @@ export type CreateChildSummarizerNodeFn = (summarizeInternal: SummarizeInternalF * Represents the context for the component. It is used by the data store runtime to * get information and call functionality to the container. */ -export interface IFluidDataStoreContext extends EventEmitter { +export interface IFluidDataStoreContext extends EventEmitter, Partial { readonly documentId: string; readonly id: string; /** diff --git a/packages/test/end-to-end-tests/src/test/componentHandle.spec.ts b/packages/test/end-to-end-tests/src/test/componentHandle.spec.ts index c0a48dc5d51a..b76f857ca086 100644 --- a/packages/test/end-to-end-tests/src/test/componentHandle.spec.ts +++ b/packages/test/end-to-end-tests/src/test/componentHandle.spec.ts @@ -85,7 +85,7 @@ describe("FluidOjectHandle", () => { const firstContainer = await createContainer(); firstContainerComponent1 = await requestFluidObject("default", firstContainer); firstContainerComponent2 = - await TestSharedComponentFactory.createInstance(firstContainerComponent1._context) as TestSharedComponent; + await TestSharedComponentFactory.createInstance(firstContainerComponent1._context); const secondContainer = await createContainer(); secondContainerComponent1 = await requestFluidObject("default", secondContainer); From c1973a167bbe331700a4c1f057b6852cb0ff212f Mon Sep 17 00:00:00 2001 From: Vlad Sudzilouski Date: Sun, 9 Aug 2020 23:10:33 -0700 Subject: [PATCH 11/24] Exploration into using sub-factories instead of names --- components/experimental/spaces/src/spaces.tsx | 4 +- .../experimental/spaces/src/spacesItemMap.ts | 29 ++++--- .../vltava/src/components/tabs/dataModel.ts | 28 ++++--- .../src/components/tabs/newTabButton.tsx | 10 ++- .../vltava/src/components/tabs/tabs.tsx | 8 +- .../vltava/src/components/tabs/view.tsx | 5 +- components/experimental/vltava/src/index.ts | 24 +++--- .../interfaces/componentInternalRegistry.ts | 5 +- .../sharedComponentFactory.ts | 77 +++++++++++++++---- .../src/components/sharedComponent.ts | 14 ---- 10 files changed, 122 insertions(+), 82 deletions(-) diff --git a/components/experimental/spaces/src/spaces.tsx b/components/experimental/spaces/src/spaces.tsx index 549db9f891d5..eca1014f7c72 100644 --- a/components/experimental/spaces/src/spaces.tsx +++ b/components/experimental/spaces/src/spaces.tsx @@ -9,6 +9,7 @@ import { Layout } from "react-grid-layout"; import { DataObject, DataObjectFactory, + getFluidObjectFactoryFromInstance, } from "@fluidframework/aqueduct"; import { IFluidHandle, @@ -183,8 +184,7 @@ export class Spaces extends DataObject implements IFluidHTMLView { throw new Error("Unknown item, can't add"); } - // Don't really want to hand out createFluidObject here, see spacesItemMap.ts for more info. - const serializableObject = await itemMapEntry.create(this.createFluidObject.bind(this)); + const serializableObject = await itemMapEntry.create(getFluidObjectFactoryFromInstance(this.context)); return this.storageComponent.addItem( { serializableObject, diff --git a/components/experimental/spaces/src/spacesItemMap.ts b/components/experimental/spaces/src/spacesItemMap.ts index 909ee646f2c3..c51278295798 100644 --- a/components/experimental/spaces/src/spacesItemMap.ts +++ b/components/experimental/spaces/src/spacesItemMap.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. */ -import { IFluidObject, IFluidHandle, IFluidLoadable } from "@fluidframework/core-interfaces"; +import { IFluidHandle, IFluidLoadable } from "@fluidframework/core-interfaces"; import { AsSerializable, Serializable } from "@fluidframework/datastore-definitions"; -import { NamedFluidDataStoreRegistryEntries } from "@fluidframework/runtime-definitions"; +import { NamedFluidDataStoreRegistryEntries, IFluidDataStoreFactory } from "@fluidframework/runtime-definitions"; import { ReactViewAdapter } from "@fluidframework/view-adapters"; import { fluidExport as cmfe } from "@fluid-example/codemirror/dist/codemirror"; import { CollaborativeText } from "@fluid-example/collaborative-textarea"; @@ -13,25 +13,24 @@ import { Coordinate } from "@fluid-example/multiview-coordinate-model"; import { SliderCoordinateView } from "@fluid-example/multiview-slider-coordinate-view"; import { fluidExport as pmfe } from "@fluid-example/prosemirror/dist/prosemirror"; import { ClickerInstantiationFactory } from "@fluid-example/clicker"; +import { IFluidDataObjectFactory } from "@fluidframework/aqueduct"; import * as React from "react"; import { Layout } from "react-grid-layout"; -export type ICreateAndAttachComponentFunction = - (pkg: string, props?: any) => Promise; - interface ISingleHandleItem { handle: IFluidHandle; } -const createSingleHandleItem = (type: string) => { - return async (createFluidObject: ICreateAndAttachComponentFunction): Promise => { - const component = await createFluidObject(type); +// eslint-disable-next-line @typescript-eslint/promise-function-async, prefer-arrow/prefer-arrow-functions +function createSingleHandleItem(subFactory: IFluidDataStoreFactory) { + return async (dataObjectFactory: IFluidDataObjectFactory): Promise => { + const component = await dataObjectFactory.createAnonymousInstance(subFactory); return { handle: component.handle, }; }; -}; +} const getAdaptedViewForSingleHandleItem = async (serializableObject: ISingleHandleItem) => { const handle = serializableObject.handle; @@ -51,42 +50,42 @@ const getSliderCoordinateView = async (serializableObject: ISingleHandleItem) => export interface ISpacesItemEntry> { // Would be better if items to bring their own subregistries, and their own ability to create components // This might be done by integrating these items with the Spaces subcomponent registry? - create: (createFluidObject: ICreateAndAttachComponentFunction) => Promise; + create: (createSubObject: IFluidDataObjectFactory) => Promise; getView: (serializableObject: T) => Promise; friendlyName: string; fabricIconName: string; } const clickerItemEntry: ISpacesItemEntry> = { - create: createSingleHandleItem(ClickerInstantiationFactory.type), + create: createSingleHandleItem(ClickerInstantiationFactory), getView: getAdaptedViewForSingleHandleItem, friendlyName: "Clicker", fabricIconName: "Touch", }; const codemirrorItemEntry: ISpacesItemEntry> = { - create: createSingleHandleItem(cmfe.type), + create: createSingleHandleItem(cmfe), getView: getAdaptedViewForSingleHandleItem, friendlyName: "Code", fabricIconName: "Code", }; const textboxItemEntry: ISpacesItemEntry> = { - create: createSingleHandleItem(CollaborativeText.ComponentName), + create: createSingleHandleItem(CollaborativeText.getFactory()), getView: getAdaptedViewForSingleHandleItem, friendlyName: "Text Box", fabricIconName: "Edit", }; const prosemirrorItemEntry: ISpacesItemEntry> = { - create: createSingleHandleItem(pmfe.type), + create: createSingleHandleItem(pmfe), getView: getAdaptedViewForSingleHandleItem, friendlyName: "Rich Text", fabricIconName: "FabricTextHighlight", }; const sliderCoordinateItemEntry: ISpacesItemEntry> = { - create: createSingleHandleItem(Coordinate.getFactory().type), + create: createSingleHandleItem(Coordinate.getFactory()), getView: getSliderCoordinateView, friendlyName: "Coordinate", fabricIconName: "NumberSymbol", diff --git a/components/experimental/vltava/src/components/tabs/dataModel.ts b/components/experimental/vltava/src/components/tabs/dataModel.ts index 2a8e65e492ce..44341c05287d 100644 --- a/components/experimental/vltava/src/components/tabs/dataModel.ts +++ b/components/experimental/vltava/src/components/tabs/dataModel.ts @@ -5,24 +5,29 @@ import { EventEmitter } from "events"; -import { IFluidObject, IFluidHandle, IFluidLoadable } from "@fluidframework/core-interfaces"; +import { + IFluidObject, + IFluidHandle, + IFluidLoadable, +} from "@fluidframework/core-interfaces"; import { ISharedDirectory, IDirectory, IDirectoryValueChanged, } from "@fluidframework/map"; -import { - ISequencedDocumentMessage, -} from "@fluidframework/protocol-definitions"; +import { ISequencedDocumentMessage } from "@fluidframework/protocol-definitions"; +import { IFluidDataStoreFactory } from "@fluidframework/runtime-definitions"; + +import { IFluidDataObjectFactory } from "@fluidframework/aqueduct"; import { v4 as uuid } from "uuid"; import { IComponentInternalRegistry } from "../../interfaces"; export interface ITabsTypes { - type: string; friendlyName: string; fabricIconName: string; + factory: IFluidDataStoreFactory; } export interface ITabsModel { @@ -33,7 +38,7 @@ export interface ITabsModel { export interface ITabsDataModel extends EventEmitter { getComponentTab(id: string): Promise; getTabIds(): string[]; - createTab(type: string): Promise; + createTab(factory: IFluidDataStoreFactory): Promise; getNewTabTypes(): ITabsTypes[]; } @@ -43,8 +48,7 @@ export class TabsDataModel extends EventEmitter implements ITabsDataModel { constructor( public root: ISharedDirectory, private readonly internalRegistry: IComponentInternalRegistry, - private readonly createFluidObject: - (pkg: string) => Promise, + private readonly createSubObject: IFluidDataObjectFactory, private readonly getFluidObjectFromDirectory: ( id: string, directory: IDirectory, @@ -73,11 +77,11 @@ export class TabsDataModel extends EventEmitter implements ITabsDataModel { return Array.from(this.tabs.keys()); } - public async createTab(type: string): Promise { + public async createTab(factory: IFluidDataStoreFactory): Promise { const newKey = uuid(); - const component = await this.createFluidObject(type); + const component = await this.createSubObject.createAnonymousInstance(factory); this.tabs.set(newKey, { - type, + type: factory.type, handleOrId: component.handle, }); @@ -99,9 +103,9 @@ export class TabsDataModel extends EventEmitter implements ITabsDataModel { const response: ITabsTypes[] = []; this.internalRegistry.getFromCapability("IFluidHTMLView").forEach((e) => { response.push({ - type: e.type, friendlyName: e.friendlyName, fabricIconName: e.fabricIconName, + factory: e.factory, }); }); return response; diff --git a/components/experimental/vltava/src/components/tabs/newTabButton.tsx b/components/experimental/vltava/src/components/tabs/newTabButton.tsx index e40717b1ff17..054b626d2676 100644 --- a/components/experimental/vltava/src/components/tabs/newTabButton.tsx +++ b/components/experimental/vltava/src/components/tabs/newTabButton.tsx @@ -3,6 +3,7 @@ * Licensed under the MIT License. */ +import { IFluidDataStoreFactory } from "@fluidframework/runtime-definitions"; import React from "react"; import { IButtonStyles, @@ -13,6 +14,7 @@ import { IIconProps, IContextualMenuItem, } from "office-ui-fabric-react"; +import { fluidExport as pmfe } from "@fluid-example/prosemirror/dist/prosemirror"; import { ITabsTypes } from "./dataModel"; @@ -23,7 +25,7 @@ export interface IButtonExampleProps { // These are set based on the toggles shown above the examples (not needed in real code) disabled?: boolean; checked?: boolean; - createTab: (type: string) => void; + createTab: (factory: IFluidDataStoreFactory) => void; components: ITabsTypes[]; } @@ -47,11 +49,11 @@ export const NewTabButton: React.FunctionComponent = props.components.forEach((component) => { items.push( { - key: component.type, + key: component.factory.type, text: component.friendlyName, iconProps: { iconName: component.fabricIconName }, onClick: () => { - props.createTab(component.type); + props.createTab(component.factory); }, }, ); @@ -66,7 +68,7 @@ export const NewTabButton: React.FunctionComponent = styles={customSplitButtonStyles} menuProps={menuProps} ariaLabel="New item" - onClick={() => props.createTab("prosemirror")} // this should be taken from the list + onClick={() => props.createTab(pmfe)} // this should be taken from the list disabled={disabled} checked={checked} text="hello" diff --git a/components/experimental/vltava/src/components/tabs/tabs.tsx b/components/experimental/vltava/src/components/tabs/tabs.tsx index 979a03fc46bc..da1f99b42296 100644 --- a/components/experimental/vltava/src/components/tabs/tabs.tsx +++ b/components/experimental/vltava/src/components/tabs/tabs.tsx @@ -3,7 +3,11 @@ * Licensed under the MIT License. */ -import { DataObject, DataObjectFactory } from "@fluidframework/aqueduct"; +import { + DataObject, + DataObjectFactory, + getFluidObjectFactoryFromInstance, +} from "@fluidframework/aqueduct"; import { IFluidObject } from "@fluidframework/core-interfaces"; import { IFluidHTMLView } from "@fluidframework/view-interfaces"; @@ -47,7 +51,7 @@ export class TabsComponent extends DataObject implements IFluidHTMLView { new TabsDataModel( this.root, registryDetails, - this.createFluidObject.bind(this), + getFluidObjectFactoryFromInstance(this.context), this.getFluidObjectFromDirectory.bind(this), ); } diff --git a/components/experimental/vltava/src/components/tabs/view.tsx b/components/experimental/vltava/src/components/tabs/view.tsx index 33dde046c0f1..0e4f9604acba 100644 --- a/components/experimental/vltava/src/components/tabs/view.tsx +++ b/components/experimental/vltava/src/components/tabs/view.tsx @@ -8,6 +8,7 @@ * Licensed under the MIT License. */ +import { IFluidDataStoreFactory } from "@fluidframework/runtime-definitions"; import { Tab, Tabs, TabList, TabPanel } from "react-tabs"; import React from "react"; @@ -89,7 +90,7 @@ export class TabsView extends React.Component { ); } - private async createNewTab(type: string) { - await this.props.dataModel.createTab(type); + private async createNewTab(factory: IFluidDataStoreFactory) { + await this.props.dataModel.createTab(factory); } } diff --git a/components/experimental/vltava/src/index.ts b/components/experimental/vltava/src/index.ts index 105093bb4bb7..5ea3ca57434a 100644 --- a/components/experimental/vltava/src/index.ts +++ b/components/experimental/vltava/src/index.ts @@ -18,6 +18,7 @@ import { import { IFluidDataStoreRegistry, IProvideFluidDataStoreFactory, + IFluidDataStoreFactory, NamedFluidDataStoreRegistryEntries, } from "@fluidframework/runtime-definitions"; @@ -44,7 +45,7 @@ export class InternalRegistry implements IFluidDataStoreRegistry, IComponentInte public async get(name: string): Promise> { const index = this.containerComponentArray.findIndex( - (containerComponent) => name === containerComponent.type, + (containerComponent) => name === containerComponent.factory.type, ); if (index >= 0) { return this.containerComponentArray[index].factory; @@ -60,7 +61,7 @@ export class InternalRegistry implements IFluidDataStoreRegistry, IComponentInte public hasCapability(type: string, capability: keyof IFluidObject) { const index = this.containerComponentArray.findIndex( - (containerComponent) => type === containerComponent.type, + (containerComponent) => type === containerComponent.factory.type, ); return index >= 0 && this.containerComponentArray[index].capabilities.includes(capability); } @@ -94,45 +95,40 @@ export class VltavaRuntimeFactory extends ContainerRuntimeFactoryWithDefaultData const generateFactory = () => { const containerComponentsDefinition: IInternalRegistryEntry[] = [ { - type: "clicker", - factory: Promise.resolve(ClickerInstantiationFactory), + factory: ClickerInstantiationFactory, capabilities: ["IFluidHTMLView", "IFluidLoadable"], friendlyName: "Clicker", fabricIconName: "NumberField", }, { - type: "tabs", - factory: Promise.resolve(TabsComponent.getFactory()), + factory: TabsComponent.getFactory(), capabilities: ["IFluidHTMLView", "IFluidLoadable"], friendlyName: "Tabs", fabricIconName: "BrowserTab", }, { - type: "spaces", - factory: Promise.resolve(Spaces.getFactory()), + factory: Spaces.getFactory(), capabilities: ["IFluidHTMLView", "IFluidLoadable"], friendlyName: "Spaces", fabricIconName: "SnapToGrid", }, { - type: "codemirror", - factory: Promise.resolve(cmfe), + factory: cmfe, capabilities: ["IFluidHTMLView", "IFluidLoadable"], friendlyName: "Codemirror", fabricIconName: "Code", }, { - type: "prosemirror", - factory: Promise.resolve(pmfe), + factory: pmfe, capabilities: ["IFluidHTMLView", "IFluidLoadable"], friendlyName: "Prosemirror", fabricIconName: "Edit", }, ]; - const containerComponents: [string, Promise][] = []; + const containerComponents: [string, Promise][] = []; containerComponentsDefinition.forEach((value) => { - containerComponents.push([value.type, value.factory]); + containerComponents.push([value.factory.type, Promise.resolve(value.factory)]); }); // The last edited tracker component provides container level tracking of last edits. This is the first diff --git a/components/experimental/vltava/src/interfaces/componentInternalRegistry.ts b/components/experimental/vltava/src/interfaces/componentInternalRegistry.ts index 1ac1efd75531..4395474f4a95 100644 --- a/components/experimental/vltava/src/interfaces/componentInternalRegistry.ts +++ b/components/experimental/vltava/src/interfaces/componentInternalRegistry.ts @@ -4,7 +4,7 @@ */ import { IFluidObject } from "@fluidframework/core-interfaces"; -import { IProvideFluidDataStoreFactory } from "@fluidframework/runtime-definitions"; +import { IFluidDataStoreFactory } from "@fluidframework/runtime-definitions"; declare module "@fluidframework/core-interfaces" { // eslint-disable-next-line @typescript-eslint/no-empty-interface @@ -29,8 +29,7 @@ export interface IComponentInternalRegistry extends IProvideComponentInternalReg * A registry entry, with extra metadata. */ export interface IInternalRegistryEntry { - type: string; - factory: Promise; + factory: IFluidDataStoreFactory; capabilities: (keyof (IFluidObject & IFluidObject))[]; friendlyName: string; fabricIconName: string; diff --git a/packages/framework/aqueduct/src/componentFactories/sharedComponentFactory.ts b/packages/framework/aqueduct/src/componentFactories/sharedComponentFactory.ts index 53cf3ccef736..b882d832772f 100644 --- a/packages/framework/aqueduct/src/componentFactories/sharedComponentFactory.ts +++ b/packages/framework/aqueduct/src/componentFactories/sharedComponentFactory.ts @@ -4,7 +4,7 @@ */ import assert from "assert"; -import { IRequest } from "@fluidframework/core-interfaces"; +import { IRequest, IFluidObject } from "@fluidframework/core-interfaces"; import { FluidDataStoreRuntime, ISharedObjectRegistry } from "@fluidframework/datastore"; import { FluidDataStoreRegistry } from "@fluidframework/container-runtime"; import { @@ -16,6 +16,7 @@ import { NamedFluidDataStoreRegistryEntry, } from "@fluidframework/runtime-definitions"; import { IChannelFactory } from "@fluidframework/datastore-definitions"; +import { requestFluidObject } from "@fluidframework/runtime-utils"; import { ComponentSymbolProvider, DependencyContainer, @@ -26,6 +27,64 @@ import { PureDataObject, } from "../components"; +async function buildSubPath(context: IFluidDataStoreContext, factory: IFluidDataStoreFactory) { + const parentPath = context.packagePath; + assert(parentPath.length > 0); + // A factory could not contain the registry for itself. So if it is the same the last snapshot + // pkg, return our package path. + assert(parentPath[parentPath.length - 1] !== factory.type); + const packagePath = [...parentPath, factory.type]; + + const factory2 = await context.IFluidDataStoreRegistry?.get(factory.type); + assert(factory2 === factory); + return packagePath; +} + +/* + * An association of identifiers to component registry entries, where the + * entries can be used to create components. + */ +export interface IFluidDataObjectFactory { + createInstance< + P, + S, + TObject extends PureDataObject, + TFactory extends PureDataObjectFactory> + (subFactory: TFactory, props?: S): Promise; + + createAnonymousInstance( + subFactory: IFluidDataStoreFactory, + request?: string | IRequest): Promise; +} + +class FluidDataObjectFactory { + constructor(private readonly context: IFluidDataStoreContext) { + } + + public async createInstance< + P, + S, + TObject extends PureDataObject, + TFactory extends PureDataObjectFactory>(subFactory: TFactory, props?: S) + { + return subFactory.createInstance(this.context, props); + } + + public async createAnonymousInstance( + subFactory: IFluidDataStoreFactory, + request: string | IRequest = "/") + { + const packagePath = await buildSubPath(this.context, subFactory); + const router = await this.context.containerRuntime.createDataStore(packagePath); + return requestFluidObject(router, request); + } +} + +export const getFluidObjectFactoryFromInstance = (context: IFluidDataStoreContext) => + new FluidDataObjectFactory(context); + +export type AnonymousPureDataObjectFactory = PureDataObjectFactory; + /** * PureDataObjectFactory is a barebones IFluidDataStoreFactory for use with PureDataObject. * Consumers should typically use DataObjectFactory instead unless creating @@ -146,22 +205,12 @@ export class PureDataObjectFactory, P, S> * for attaching the component to the provided runtime's container such as by storing its handle */ public async createInstance( - context: IFluidDataStoreContext, + parentContext: IFluidDataStoreContext, initialState?: S, ) { - const parentPath = context.packagePath; - assert(parentPath.length > 0); - // A factory could not contain the registry for itself. So if it is the same the last snapshot - // pkg, return our package path. - assert(parentPath[parentPath.length - 1] !== this.type); - const packagePath = [...parentPath, this.type]; - - const factory = await context.IFluidDataStoreRegistry?.get(this.type); - assert(factory === this.IFluidDataStoreFactory); - - // const packagePath = await context.composeSubpackagePath(this.type); + const packagePath = await buildSubPath(parentContext, this); - const newContext = context.containerRuntime.createDetachedDataStore(); + const newContext = parentContext.containerRuntime.createDetachedDataStore(); // const runtime = this.instantiateDataStoreCore(newContext, initialState); const runtime = FluidDataStoreRuntime.load( diff --git a/packages/framework/aqueduct/src/components/sharedComponent.ts b/packages/framework/aqueduct/src/components/sharedComponent.ts index 34868e70955b..137d583c8540 100644 --- a/packages/framework/aqueduct/src/components/sharedComponent.ts +++ b/packages/framework/aqueduct/src/components/sharedComponent.ts @@ -142,20 +142,6 @@ export abstract class PureDataObject

( - pkg: string, - ): Promise { - const packagePath = await this.context.composeSubpackagePath(pkg); - const router = await this.context.containerRuntime.createDataStore(packagePath); - return requestFluidObject(router, "/"); - } - /** * Retreive component using the handle get or the older requestFluidObject_UNSAFE call to fetch by ID * From 65b62bd83fe0e3dffcfb2b73e37701ef7d1dab98 Mon Sep 17 00:00:00 2001 From: Vlad Sudzilouski Date: Sun, 9 Aug 2020 23:11:54 -0700 Subject: [PATCH 12/24] createInstance() can take IContainerRuntimeBase in addition to context to allow creation from root --- .../sharedComponentFactory.ts | 25 ++++++++++++------- .../src/test/componentHandle.spec.ts | 2 +- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/packages/framework/aqueduct/src/componentFactories/sharedComponentFactory.ts b/packages/framework/aqueduct/src/componentFactories/sharedComponentFactory.ts index b882d832772f..75f4149d8185 100644 --- a/packages/framework/aqueduct/src/componentFactories/sharedComponentFactory.ts +++ b/packages/framework/aqueduct/src/componentFactories/sharedComponentFactory.ts @@ -9,6 +9,7 @@ import { FluidDataStoreRuntime, ISharedObjectRegistry } from "@fluidframework/da import { FluidDataStoreRegistry } from "@fluidframework/container-runtime"; import { IFluidDataStoreContext, + IContainerRuntimeBase, IFluidDataStoreFactory, IFluidDataStoreRegistry, IProvideFluidDataStoreRegistry, @@ -27,13 +28,18 @@ import { PureDataObject, } from "../components"; -async function buildSubPath(context: IFluidDataStoreContext, factory: IFluidDataStoreFactory) { - const parentPath = context.packagePath; - assert(parentPath.length > 0); - // A factory could not contain the registry for itself. So if it is the same the last snapshot - // pkg, return our package path. - assert(parentPath[parentPath.length - 1] !== factory.type); - const packagePath = [...parentPath, factory.type]; +async function buildSubPath(context: IFluidDataStoreContext | IContainerRuntimeBase, factory: IFluidDataStoreFactory) { + let packagePath: string[]; + if ("containerRuntime" in context) { + const parentPath = context.packagePath; + assert(parentPath.length > 0); + // A factory could not contain the registry for itself. So if it is the same the last snapshot + // pkg, return our package path. + assert(parentPath[parentPath.length - 1] !== factory.type); + packagePath = [...parentPath, factory.type]; + } else { + packagePath = [factory.type]; + } const factory2 = await context.IFluidDataStoreRegistry?.get(factory.type); assert(factory2 === factory); @@ -205,12 +211,13 @@ export class PureDataObjectFactory, P, S> * for attaching the component to the provided runtime's container such as by storing its handle */ public async createInstance( - parentContext: IFluidDataStoreContext, + parentContext: IFluidDataStoreContext | IContainerRuntimeBase, initialState?: S, ) { const packagePath = await buildSubPath(parentContext, this); - const newContext = parentContext.containerRuntime.createDetachedDataStore(); + const containerRuntime = "containerRuntime" in parentContext ? parentContext.containerRuntime : parentContext; + const newContext = containerRuntime.createDetachedDataStore(); // const runtime = this.instantiateDataStoreCore(newContext, initialState); const runtime = FluidDataStoreRuntime.load( diff --git a/packages/test/end-to-end-tests/src/test/componentHandle.spec.ts b/packages/test/end-to-end-tests/src/test/componentHandle.spec.ts index b76f857ca086..57271352d5f1 100644 --- a/packages/test/end-to-end-tests/src/test/componentHandle.spec.ts +++ b/packages/test/end-to-end-tests/src/test/componentHandle.spec.ts @@ -85,7 +85,7 @@ describe("FluidOjectHandle", () => { const firstContainer = await createContainer(); firstContainerComponent1 = await requestFluidObject("default", firstContainer); firstContainerComponent2 = - await TestSharedComponentFactory.createInstance(firstContainerComponent1._context); + await TestSharedComponentFactory.createInstance(firstContainerComponent1._context.containerRuntime); const secondContainer = await createContainer(); secondContainerComponent1 = await requestFluidObject("default", secondContainer); From 434c303dd24008b56bb857868788d6ae5c66aeec Mon Sep 17 00:00:00 2001 From: Vlad Sudzilouski Date: Sun, 9 Aug 2020 23:18:44 -0700 Subject: [PATCH 13/24] Address one of the UTs --- .../external-component-loader/src/externalComponentLoader.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/experimental/external-component-loader/src/externalComponentLoader.tsx b/components/experimental/external-component-loader/src/externalComponentLoader.tsx index aa1cbb1d4537..7ef56b61fb63 100644 --- a/components/experimental/external-component-loader/src/externalComponentLoader.tsx +++ b/components/experimental/external-component-loader/src/externalComponentLoader.tsx @@ -35,7 +35,7 @@ export class ExternalComponentLoader extends DataObject { * @param componentUrl - the URL of the component to create, adding it to the registry if needed. */ public async createComponentFromUrl(componentUrl: string): Promise { - const urlReg = await this.runtime.IFluidDataStoreRegistry?.get("url"); + const urlReg = await this.context.IFluidDataStoreRegistry?.get("url"); if (urlReg?.IFluidDataStoreRegistry === undefined) { throw new Error("Couldn't get url component registry"); } From 273290fbd4698ccf9343d93a17f02ec44d121496 Mon Sep 17 00:00:00 2001 From: Vlad Sudzilouski Date: Mon, 10 Aug 2020 00:07:26 -0700 Subject: [PATCH 14/24] Remove AnonymousPureDataObjectFactory as not used --- .../aqueduct/src/componentFactories/sharedComponentFactory.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/framework/aqueduct/src/componentFactories/sharedComponentFactory.ts b/packages/framework/aqueduct/src/componentFactories/sharedComponentFactory.ts index 75f4149d8185..dc3ba4aa0b53 100644 --- a/packages/framework/aqueduct/src/componentFactories/sharedComponentFactory.ts +++ b/packages/framework/aqueduct/src/componentFactories/sharedComponentFactory.ts @@ -89,8 +89,6 @@ class FluidDataObjectFactory { export const getFluidObjectFactoryFromInstance = (context: IFluidDataStoreContext) => new FluidDataObjectFactory(context); -export type AnonymousPureDataObjectFactory = PureDataObjectFactory; - /** * PureDataObjectFactory is a barebones IFluidDataStoreFactory for use with PureDataObject. * Consumers should typically use DataObjectFactory instead unless creating From 6dc11a9dffc35ace8cff3a13dbb796c753400363 Mon Sep 17 00:00:00 2001 From: Vlad Sudzilouski Date: Wed, 12 Aug 2020 23:33:53 -0700 Subject: [PATCH 15/24] Build breaks and incorrect conflict resolution --- .../container-runtime/src/dataStoreContext.ts | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/packages/runtime/container-runtime/src/dataStoreContext.ts b/packages/runtime/container-runtime/src/dataStoreContext.ts index 81da4fce8404..cc532c2df224 100644 --- a/packages/runtime/container-runtime/src/dataStoreContext.ts +++ b/packages/runtime/container-runtime/src/dataStoreContext.ts @@ -385,11 +385,6 @@ export abstract class FluidDataStoreContext extends EventEmitter implements const { pkg } = await this.getInitialSnapshotDetails(); - const attributes: IFluidDataStoreAttributes = { - pkg: JSON.stringify(pkg), - snapshotFormatVersion: currentSnapshotFormatVersion, - }; - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const entries = await this.channel!.snapshotInternal(fullTree); @@ -408,11 +403,6 @@ export abstract class FluidDataStoreContext extends EventEmitter implements const { pkg } = await this.getInitialSnapshotDetails(); - const attributes: IFluidDataStoreAttributes = { - pkg: JSON.stringify(pkg), - snapshotFormatVersion: currentSnapshotFormatVersion, - }; - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const channel = this.channel!; if (channel.summarize !== undefined) { @@ -711,9 +701,6 @@ export class RemotedFluidDataStoreContext extends FluidDataStoreContext { } export class LocalFluidDataStoreContextBase extends FluidDataStoreContext { - // Package is required at time of creation for local data stores - protected pkg: readonly string[]; - constructor( id: string, pkg: string[] | undefined, @@ -735,7 +722,6 @@ export class LocalFluidDataStoreContextBase extends FluidDataStoreContext { BindState.NotBound, bindChannel, pkg); - this.pkg = pkg; // TODO: avoid setting twice this.attachListeners(); } @@ -756,6 +742,7 @@ export class LocalFluidDataStoreContextBase extends FluidDataStoreContext { const snapshot: ITree = { entries, id: null }; + assert(this.pkg !== undefined); const attributesBlob = createAttributesBlob(this.pkg); snapshot.entries.push(attributesBlob); @@ -769,6 +756,7 @@ export class LocalFluidDataStoreContextBase extends FluidDataStoreContext { } protected async getInitialSnapshotDetails(): Promise { + assert(this.pkg !== undefined); return { pkg: this.pkg, snapshot: undefined, From 28b0cb5ade488450e84d34d0abef114c16db134b Mon Sep 17 00:00:00 2001 From: Vlad Sudzilouski Date: Thu, 13 Aug 2020 00:39:15 -0700 Subject: [PATCH 16/24] Move validation to runtime --- .../pureDataObjectFactory.ts | 26 +----------- .../container-runtime/src/dataStoreContext.ts | 40 +++++++++++++++---- .../runtime/container-runtime/src/index.ts | 1 + .../src/componentContext.ts | 8 ++-- 4 files changed, 41 insertions(+), 34 deletions(-) diff --git a/packages/framework/aqueduct/src/data-object-factories/pureDataObjectFactory.ts b/packages/framework/aqueduct/src/data-object-factories/pureDataObjectFactory.ts index 0cee7e2218b3..01eb9f30e1d0 100644 --- a/packages/framework/aqueduct/src/data-object-factories/pureDataObjectFactory.ts +++ b/packages/framework/aqueduct/src/data-object-factories/pureDataObjectFactory.ts @@ -3,10 +3,9 @@ * Licensed under the MIT License. */ -import assert from "assert"; import { IRequest, IFluidObject } from "@fluidframework/core-interfaces"; import { FluidDataStoreRuntime, ISharedObjectRegistry } from "@fluidframework/datastore"; -import { FluidDataStoreRegistry } from "@fluidframework/container-runtime"; +import { FluidDataStoreRegistry, buildSubPath } from "@fluidframework/container-runtime"; import { IFluidDataStoreContext, IContainerRuntimeBase, @@ -28,24 +27,6 @@ import { PureDataObject, } from "../data-objects"; -async function buildSubPath(context: IFluidDataStoreContext | IContainerRuntimeBase, factory: IFluidDataStoreFactory) { - let packagePath: string[]; - if ("containerRuntime" in context) { - const parentPath = context.packagePath; - assert(parentPath.length > 0); - // A factory could not contain the registry for itself. So if it is the same the last snapshot - // pkg, return our package path. - assert(parentPath[parentPath.length - 1] !== factory.type); - packagePath = [...parentPath, factory.type]; - } else { - packagePath = [factory.type]; - } - - const factory2 = await context.IFluidDataStoreRegistry?.get(factory.type); - assert(factory2 === factory); - return packagePath; -} - /* * An association of identifiers to component registry entries, where the * entries can be used to create components. @@ -212,8 +193,6 @@ export class PureDataObjectFactory, P, S> parentContext: IFluidDataStoreContext | IContainerRuntimeBase, initialState?: S, ) { - const packagePath = await buildSubPath(parentContext, this); - const containerRuntime = "containerRuntime" in parentContext ? parentContext.containerRuntime : parentContext; const newContext = containerRuntime.createDetachedDataStore(); @@ -230,8 +209,7 @@ export class PureDataObjectFactory, P, S> return instance.request(request); }); - // FluidDataStoreRegistryEntry - newContext.attachRuntime(runtime, packagePath, this); + await newContext.attachRuntime(parentContext, this, runtime); return instanceP; } diff --git a/packages/runtime/container-runtime/src/dataStoreContext.ts b/packages/runtime/container-runtime/src/dataStoreContext.ts index cc532c2df224..926656111920 100644 --- a/packages/runtime/container-runtime/src/dataStoreContext.ts +++ b/packages/runtime/container-runtime/src/dataStoreContext.ts @@ -50,6 +50,9 @@ import { CreateChildSummarizerNodeFn, SummarizeInternalFn, CreateChildSummarizerNodeParam, + IContainerRuntimeBase, + IProvideFluidDataStoreFactory, + IProvideFluidDataStoreRegistry, } from "@fluidframework/runtime-definitions"; import { SummaryTracker, addBlobToSummary, convertToSummaryTree } from "@fluidframework/runtime-utils"; import { ContainerRuntime } from "./containerRuntime"; @@ -59,6 +62,27 @@ const currentSnapshotFormatVersion = "0.1"; const attributesBlobKey = ".component"; +export async function buildSubPath( + context: IFluidDataStoreContext | IContainerRuntimeBase, + factory: IFluidDataStoreFactory) +{ + let packagePath: string[]; + if ("containerRuntime" in context) { + const parentPath = context.packagePath; + assert(parentPath.length > 0); + // A factory could not contain the registry for itself. So if it is the same the last snapshot + // pkg, return our package path. + assert(parentPath[parentPath.length - 1] !== factory.type); + packagePath = [...parentPath, factory.type]; + } else { + packagePath = [factory.type]; + } + + const factory2 = await context.IFluidDataStoreRegistry?.get(factory.type); + assert(factory2 === factory); + return packagePath; +} + function createAttributes(pkg: readonly string[]): IFluidDataStoreAttributes { const stringifiedPkg = JSON.stringify(pkg); return { @@ -813,19 +837,21 @@ export class LocalDetachedFluidDataStoreContext this.detachedRuntimeCreation = true; } - public attachRuntime(runtime, pkg: string[], entry: FluidDataStoreRegistryEntry) { + public async attachRuntime( + parentContext: IFluidDataStoreContext | IContainerRuntimeBase, + // FluidDataStoreRegistryEntry + factory: IProvideFluidDataStoreFactory & Partial, + dataStoreRuntime: IFluidDataStoreChannel) + { assert(this.detachedRuntimeCreation); assert(this.pkg === undefined); - assert(pkg !== undefined); - this.pkg = pkg; + this.pkg = await buildSubPath(parentContext, factory.IFluidDataStoreFactory); assert(this.registry === undefined); - this.registry = entry.IFluidDataStoreRegistry; - - assert(entry.IFluidDataStoreFactory?.type === pkg[pkg.length - 1]); + this.registry = factory.IFluidDataStoreRegistry; - super.bindRuntime(runtime); + super.bindRuntime(dataStoreRuntime); } protected async getInitialSnapshotDetails(): Promise { diff --git a/packages/runtime/container-runtime/src/index.ts b/packages/runtime/container-runtime/src/index.ts index 455793811e1d..175b05e4a051 100644 --- a/packages/runtime/container-runtime/src/index.ts +++ b/packages/runtime/container-runtime/src/index.ts @@ -5,6 +5,7 @@ export * from "./containerRuntime"; export * from "./deltaScheduler"; +export { buildSubPath } from "./dataStoreContext"; export * from "./dataStoreRegistry"; export * from "./runWhileConnectedCoordinator"; export * from "./summarizer"; diff --git a/packages/runtime/runtime-definitions/src/componentContext.ts b/packages/runtime/runtime-definitions/src/componentContext.ts index 2b4882303ad2..0bba111f388e 100644 --- a/packages/runtime/runtime-definitions/src/componentContext.ts +++ b/packages/runtime/runtime-definitions/src/componentContext.ts @@ -29,7 +29,8 @@ import { ISnapshotTree, ITreeEntry, } from "@fluidframework/protocol-definitions"; -import { IProvideFluidDataStoreRegistry, FluidDataStoreRegistryEntry } from "./componentRegistry"; +import { IProvideFluidDataStoreFactory } from "./componentFactory"; +import { IProvideFluidDataStoreRegistry } from "./componentRegistry"; import { IInboundSignalMessage } from "./protocol"; import { ISummaryTreeWithStats, ISummarizerNode, SummarizeInternalFn, CreateChildSummarizerNodeParam } from "./summary"; @@ -348,7 +349,8 @@ export interface IFluidDataStoreContextDetached extends IFluidDataStoreContext { * Binds a runtime to the context. */ attachRuntime( + parentContext: IFluidDataStoreContext | IContainerRuntimeBase, + factory: IProvideFluidDataStoreFactory & Partial, dataStoreRuntime: IFluidDataStoreChannel, - pkg: string[], - entry: FluidDataStoreRegistryEntry): void; + ): Promise; } From 5c359a1dcaa7b11eb754deff5ac43156337c37f7 Mon Sep 17 00:00:00 2001 From: Vlad Sudzilouski Date: Thu, 13 Aug 2020 10:45:16 -0700 Subject: [PATCH 17/24] Cleanup --- .../src/urlRegistry.ts | 4 +-- .../data-objects/shared-text/src/index.ts | 16 ++++++---- .../src/components/tabs/newTabButton.tsx | 1 - .../vltava/src/components/tabs/tabs.tsx | 2 +- examples/data-objects/vltava/src/index.ts | 2 +- .../pureDataObjectFactory.ts | 10 +++---- .../container-runtime/src/dataStoreContext.ts | 14 +++++---- .../runtime/container-runtime/src/index.ts | 2 +- .../src/componentRegistry.ts | 1 + .../test/test-utils/src/localCodeLoader.ts | 29 +++++++++++++------ 10 files changed, 50 insertions(+), 31 deletions(-) diff --git a/examples/data-objects/external-component-loader/src/urlRegistry.ts b/examples/data-objects/external-component-loader/src/urlRegistry.ts index b8d448550eb4..51074fee1ad6 100644 --- a/examples/data-objects/external-component-loader/src/urlRegistry.ts +++ b/examples/data-objects/external-component-loader/src/urlRegistry.ts @@ -9,8 +9,8 @@ import { IFluidCodeDetails, } from "@fluidframework/container-definitions"; import { + IProvideFluidDataStoreRegistry, FluidDataStoreRegistryEntry, - IFluidDataStoreRegistry, } from "@fluidframework/runtime-definitions"; import { IFluidObject } from "@fluidframework/core-interfaces"; import { WebCodeLoader, SemVerCdnCodeResolver } from "@fluidframework/web-code-loader"; @@ -18,7 +18,7 @@ import { WebCodeLoader, SemVerCdnCodeResolver } from "@fluidframework/web-code-l /** * A component registry that can load component via their url */ -export class UrlRegistry implements IFluidDataStoreRegistry { +export class UrlRegistry implements IProvideFluidDataStoreRegistry { private static readonly WindowKeyPrefix = "FluidExternalComponent"; private readonly urlRegistryMap = new Map>(); diff --git a/examples/data-objects/shared-text/src/index.ts b/examples/data-objects/shared-text/src/index.ts index f132e64f92b0..f408dbc8bf6e 100644 --- a/examples/data-objects/shared-text/src/index.ts +++ b/examples/data-objects/shared-text/src/index.ts @@ -7,12 +7,13 @@ // eslint-disable-next-line import/no-unassigned-import import "./publicpath"; +import assert from "assert"; import { IContainerContext, IRuntime, IRuntimeFactory } from "@fluidframework/container-definitions"; import { ContainerRuntime } from "@fluidframework/container-runtime"; import { IFluidDataStoreContext, - IFluidDataStoreFactory, - IFluidDataStoreRegistry, + IProvideFluidDataStoreFactory, + IProvideFluidDataStoreRegistry, NamedFluidDataStoreRegistryEntries, } from "@fluidframework/runtime-definitions"; import { @@ -56,7 +57,7 @@ const defaultRegistryEntries: NamedFluidDataStoreRegistryEntries = [ ["@fluid-example/image-collection", images.then((m) => m.fluidExport)], ]; -class MyRegistry implements IFluidDataStoreRegistry { +class MyRegistry implements IProvideFluidDataStoreRegistry { constructor( private readonly context: IContainerContext, private readonly defaultRegistry: string) { @@ -64,7 +65,7 @@ class MyRegistry implements IFluidDataStoreRegistry { public get IFluidDataStoreRegistry() { return this; } - public async get(name: string): Promise { + public async get(name: string): Promise { const scope = `${name.split("/")[0]}:cdn`; const config = {}; config[scope] = this.defaultRegistry; @@ -74,11 +75,14 @@ class MyRegistry implements IFluidDataStoreRegistry { config, }; const fluidModule = await this.context.codeLoader.load(codeDetails); - return fluidModule.fluidExport.IFluidDataStoreFactory; + const moduleExport = fluidModule.fluidExport; + assert(moduleExport.IFluidDataStoreFactory !== undefined || + moduleExport.IFluidDataStoreRegistry !== undefined); + return moduleExport as IProvideFluidDataStoreFactory | IProvideFluidDataStoreRegistry; } } -class SharedTextFactoryComponent implements IFluidDataStoreFactory, IRuntimeFactory { +class SharedTextFactoryComponent implements IProvideFluidDataStoreFactory, IRuntimeFactory { public static readonly type = "@fluid-example/shared-text"; public readonly type = SharedTextFactoryComponent.type; diff --git a/examples/data-objects/vltava/src/components/tabs/newTabButton.tsx b/examples/data-objects/vltava/src/components/tabs/newTabButton.tsx index e5eb3cfe74a4..054b626d2676 100644 --- a/examples/data-objects/vltava/src/components/tabs/newTabButton.tsx +++ b/examples/data-objects/vltava/src/components/tabs/newTabButton.tsx @@ -5,7 +5,6 @@ import { IFluidDataStoreFactory } from "@fluidframework/runtime-definitions"; import React from "react"; -import { fluidExport as pmfe } from "@fluid-example/prosemirror/dist/prosemirror"; import { IButtonStyles, IconButton, diff --git a/examples/data-objects/vltava/src/components/tabs/tabs.tsx b/examples/data-objects/vltava/src/components/tabs/tabs.tsx index da1f99b42296..a6eef373435d 100644 --- a/examples/data-objects/vltava/src/components/tabs/tabs.tsx +++ b/examples/data-objects/vltava/src/components/tabs/tabs.tsx @@ -44,7 +44,7 @@ export class TabsComponent extends DataObject implements IFluidHTMLView { } protected async hasInitialized() { - const registry = await this.context.containerRuntime.IFluidDataStoreRegistry.get(""); + const registry = await this.context.containerRuntime.IFluidDataStoreRegistry.get("internalRegistry"); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const registryDetails = (registry as IFluidObject).IComponentInternalRegistry!; this.dataModelInternal = diff --git a/examples/data-objects/vltava/src/index.ts b/examples/data-objects/vltava/src/index.ts index 4932c5e15600..c90b68ea44b4 100644 --- a/examples/data-objects/vltava/src/index.ts +++ b/examples/data-objects/vltava/src/index.ts @@ -137,7 +137,7 @@ const generateFactory = () => { // We don't want to include the default wrapper component in our list of available components Anchor.getFactory().registryEntry, Vltava.getFactory().registryEntry, - ["", Promise.resolve(new InternalRegistry(containerComponentsDefinition))], + ["internalRegistry", Promise.resolve(new InternalRegistry(containerComponentsDefinition))], ], ); }; diff --git a/packages/framework/aqueduct/src/data-object-factories/pureDataObjectFactory.ts b/packages/framework/aqueduct/src/data-object-factories/pureDataObjectFactory.ts index 01eb9f30e1d0..76738d47cefc 100644 --- a/packages/framework/aqueduct/src/data-object-factories/pureDataObjectFactory.ts +++ b/packages/framework/aqueduct/src/data-object-factories/pureDataObjectFactory.ts @@ -5,7 +5,7 @@ import { IRequest, IFluidObject } from "@fluidframework/core-interfaces"; import { FluidDataStoreRuntime, ISharedObjectRegistry } from "@fluidframework/datastore"; -import { FluidDataStoreRegistry, buildSubPath } from "@fluidframework/container-runtime"; +import { FluidDataStoreRegistry, buildRegistryPath } from "@fluidframework/container-runtime"; import { IFluidDataStoreContext, IContainerRuntimeBase, @@ -61,7 +61,7 @@ class FluidDataObjectFactory { subFactory: IFluidDataStoreFactory, request: string | IRequest = "/") { - const packagePath = await buildSubPath(this.context, subFactory); + const packagePath = await buildRegistryPath(this.context, subFactory); const router = await this.context.containerRuntime.createDataStore(packagePath); return requestFluidObject(router, request); } @@ -141,7 +141,7 @@ export class PureDataObjectFactory, P, S> this.sharedObjectRegistry, ); - let instanceP: Promise; + let instanceP: Promise; // For new runtime, we need to force the component instance to be create // run the initialization. if (!this.onDemandInstantiation || !runtime.existing) { @@ -171,7 +171,7 @@ export class PureDataObjectFactory, P, S> runtime: FluidDataStoreRuntime, context: IFluidDataStoreContext, props?: S, - ) { + ): Promise { const dependencyContainer = new DependencyContainer(context.scope.IFluidDependencySynthesizer); const providers = dependencyContainer.synthesize

(this.optionalProviders, {}); // Create a new instance of our component @@ -192,7 +192,7 @@ export class PureDataObjectFactory, P, S> public async createInstance( parentContext: IFluidDataStoreContext | IContainerRuntimeBase, initialState?: S, - ) { + ): Promise { const containerRuntime = "containerRuntime" in parentContext ? parentContext.containerRuntime : parentContext; const newContext = containerRuntime.createDetachedDataStore(); diff --git a/packages/runtime/container-runtime/src/dataStoreContext.ts b/packages/runtime/container-runtime/src/dataStoreContext.ts index 926656111920..b46ad2faf960 100644 --- a/packages/runtime/container-runtime/src/dataStoreContext.ts +++ b/packages/runtime/container-runtime/src/dataStoreContext.ts @@ -62,7 +62,13 @@ const currentSnapshotFormatVersion = "0.1"; const attributesBlobKey = ".component"; -export async function buildSubPath( +/** + * Takes context, and creates package path for a sub-entry (represented by factory) in context registry. + * Package path returned is used to reach given factory from root (container runtime) registry, and thus + * is used to serizlize and de-serialize future data store that such factory would create in future. + * Function validates that given factory is present in registry, otherwise it throws. + */ +export async function buildRegistryPath( context: IFluidDataStoreContext | IContainerRuntimeBase, factory: IFluidDataStoreFactory) { @@ -292,7 +298,6 @@ export abstract class FluidDataStoreContext extends EventEmitter implements let entry: FluidDataStoreRegistryEntry | undefined; let registry: IFluidDataStoreRegistry | undefined = this._containerRuntime.IFluidDataStoreRegistry; - let factory: IFluidDataStoreFactory | undefined; let lastPkg: string | undefined; for (const pkg of packages) { if (!registry) { @@ -303,9 +308,9 @@ export abstract class FluidDataStoreContext extends EventEmitter implements if (!entry) { return this.rejectDeferredRealize(`Registry does not contain entry for the package ${pkg}`); } - factory = entry.IFluidDataStoreFactory; registry = entry.IFluidDataStoreRegistry; } + const factory = entry?.IFluidDataStoreFactory; if (factory === undefined) { return this.rejectDeferredRealize(`Can't find factory for ${lastPkg} package`); } @@ -839,14 +844,13 @@ export class LocalDetachedFluidDataStoreContext public async attachRuntime( parentContext: IFluidDataStoreContext | IContainerRuntimeBase, - // FluidDataStoreRegistryEntry factory: IProvideFluidDataStoreFactory & Partial, dataStoreRuntime: IFluidDataStoreChannel) { assert(this.detachedRuntimeCreation); assert(this.pkg === undefined); - this.pkg = await buildSubPath(parentContext, factory.IFluidDataStoreFactory); + this.pkg = await buildRegistryPath(parentContext, factory.IFluidDataStoreFactory); assert(this.registry === undefined); this.registry = factory.IFluidDataStoreRegistry; diff --git a/packages/runtime/container-runtime/src/index.ts b/packages/runtime/container-runtime/src/index.ts index 175b05e4a051..b41d6d38d8d4 100644 --- a/packages/runtime/container-runtime/src/index.ts +++ b/packages/runtime/container-runtime/src/index.ts @@ -5,7 +5,7 @@ export * from "./containerRuntime"; export * from "./deltaScheduler"; -export { buildSubPath } from "./dataStoreContext"; +export { buildRegistryPath } from "./dataStoreContext"; export * from "./dataStoreRegistry"; export * from "./runWhileConnectedCoordinator"; export * from "./summarizer"; diff --git a/packages/runtime/runtime-definitions/src/componentRegistry.ts b/packages/runtime/runtime-definitions/src/componentRegistry.ts index 9e774fdc935e..63fefe8534b8 100644 --- a/packages/runtime/runtime-definitions/src/componentRegistry.ts +++ b/packages/runtime/runtime-definitions/src/componentRegistry.ts @@ -12,6 +12,7 @@ declare module "@fluidframework/core-interfaces" { /** * A single registry entry that may be used to create components + * It has to have either factory or registry, or both. */ export type FluidDataStoreRegistryEntry = Readonly>; diff --git a/packages/test/test-utils/src/localCodeLoader.ts b/packages/test/test-utils/src/localCodeLoader.ts index 833efa11ea02..c98d6b889af0 100644 --- a/packages/test/test-utils/src/localCodeLoader.ts +++ b/packages/test/test-utils/src/localCodeLoader.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. */ +import assert from "assert"; import { ContainerRuntimeFactoryWithDefaultDataStore } from "@fluidframework/aqueduct"; import { ICodeLoader, @@ -10,10 +11,16 @@ import { IFluidModule, IFluidCodeDetails, } from "@fluidframework/container-definitions"; -import { IProvideFluidDataStoreFactory } from "@fluidframework/runtime-definitions"; +import { IProvideFluidDataStoreFactory, IProvideFluidDataStoreRegistry } from "@fluidframework/runtime-definitions"; -// Represents the entry point for a fluid container. -export type fluidEntryPoint = Partial; +// Represents actia; entry point of fluid package +export type fluidEntryPointFinal = Partial< + IProvideRuntimeFactory & + IProvideFluidDataStoreFactory & + IProvideFluidDataStoreRegistry>; + +// Represents the entry point for a fluid package. +export type fluidEntryPoint = fluidEntryPointFinal & Partial; /** * A simple code loader that caches a mapping of package name to a fluid entry point. @@ -63,13 +70,17 @@ export class LocalCodeLoader implements ICodeLoader { if (entryPoint === undefined) { throw new Error(`Cannot find package ${pkdId}`); } - const factory: Partial = - entryPoint.fluidExport ?? entryPoint; - const runtimeFactory: IProvideRuntimeFactory = - factory.IRuntimeFactory ?? + + const factory = (entryPoint.fluidExport ?? entryPoint) as fluidEntryPointFinal; + + if (factory.IRuntimeFactory !== undefined) { + return { fluidExport: factory.IRuntimeFactory }; + } + + assert(factory.IFluidDataStoreFactory !== undefined || factory.IFluidDataStoreRegistry !== undefined); + const fluidExport: IProvideRuntimeFactory = new ContainerRuntimeFactoryWithDefaultDataStore("default", [["default", Promise.resolve(factory)]]); - const fluidModule: IFluidModule = { fluidExport: runtimeFactory }; - return fluidModule; + return { fluidExport }; } } From dfd5e71bcf8e8b9802d6c6d3de83ca06e8beecb1 Mon Sep 17 00:00:00 2001 From: Vlad Sudzilouski Date: Thu, 13 Aug 2020 12:24:40 -0700 Subject: [PATCH 18/24] Fix UTs, Break out createInstance API into multiple --- .../apps/spaces/src/fluid-object/index.tsx | 2 +- .../spaces/src/fluid-object/spacesItemMap.ts | 2 +- .../src/waterPark.tsx | 4 +- .../constellation-model/src/model.ts | 2 +- examples/data-objects/pond/src/index.tsx | 5 +- .../simple-component-embed/src/index.tsx | 2 +- .../table-document/src/document.ts | 2 +- .../data-objects/table-view/src/tableview.ts | 2 +- examples/data-objects/todo/src/Todo/Todo.tsx | 2 +- .../todo/src/TodoItem/TodoItem.tsx | 8 +- .../vltava/src/components/anchor/anchor.ts | 4 +- .../vltava/src/components/tabs/dataModel.ts | 2 +- .../vltava/src/components/vltava/vltava.tsx | 2 +- .../pureDataObjectFactory.ts | 82 ++++++++++++++++--- .../container-runtime/src/dataStoreContext.ts | 74 +++++++---------- .../runtime/container-runtime/src/index.ts | 1 - .../src/componentContext.ts | 4 +- .../src/test/componentHandle.spec.ts | 2 +- 18 files changed, 120 insertions(+), 82 deletions(-) diff --git a/examples/apps/spaces/src/fluid-object/index.tsx b/examples/apps/spaces/src/fluid-object/index.tsx index eca1014f7c72..27576eb0a596 100644 --- a/examples/apps/spaces/src/fluid-object/index.tsx +++ b/examples/apps/spaces/src/fluid-object/index.tsx @@ -127,7 +127,7 @@ export class Spaces extends DataObject implements IFluidHTMLView { } protected async initializingFirstTime() { - const storageComponent = await SpacesStorage.getFactory().createInstance(this.context); + const storageComponent = await SpacesStorage.getFactory().createChildInstance(this.context); this.root.set(SpacesStorageKey, storageComponent.handle); // Set the saved template if there is a template query param const urlParams = new URLSearchParams(window.location.search); diff --git a/examples/apps/spaces/src/fluid-object/spacesItemMap.ts b/examples/apps/spaces/src/fluid-object/spacesItemMap.ts index c51278295798..b2d4e0b27f9e 100644 --- a/examples/apps/spaces/src/fluid-object/spacesItemMap.ts +++ b/examples/apps/spaces/src/fluid-object/spacesItemMap.ts @@ -25,7 +25,7 @@ interface ISingleHandleItem { // eslint-disable-next-line @typescript-eslint/promise-function-async, prefer-arrow/prefer-arrow-functions function createSingleHandleItem(subFactory: IFluidDataStoreFactory) { return async (dataObjectFactory: IFluidDataObjectFactory): Promise => { - const component = await dataObjectFactory.createAnonymousInstance(subFactory); + const component = await dataObjectFactory.createAnonymousChildInstance(subFactory); return { handle: component.handle, }; diff --git a/examples/data-objects/external-component-loader/src/waterPark.tsx b/examples/data-objects/external-component-loader/src/waterPark.tsx index 6737fc089208..eec9f1f6f156 100644 --- a/examples/data-objects/external-component-loader/src/waterPark.tsx +++ b/examples/data-objects/external-component-loader/src/waterPark.tsx @@ -133,9 +133,9 @@ export class WaterPark extends DataObject implements IFluidHTMLView { } protected async initializingFirstTime() { - const storage = await SpacesStorage.getFactory().createInstance(this.context); + const storage = await SpacesStorage.getFactory().createChildInstance(this.context); this.root.set(storageKey, storage.handle); - const loader = await ExternalComponentLoader.getFactory().createInstance(this.context); + const loader = await ExternalComponentLoader.getFactory().createChildInstance(this.context); this.root.set(loaderKey, loader.handle); } diff --git a/examples/data-objects/multiview/constellation-model/src/model.ts b/examples/data-objects/multiview/constellation-model/src/model.ts index 64e80a9ad78b..ef2e451d0dc3 100644 --- a/examples/data-objects/multiview/constellation-model/src/model.ts +++ b/examples/data-objects/multiview/constellation-model/src/model.ts @@ -63,7 +63,7 @@ export class Constellation extends DataObject implements IConstellation { public async addStar(x: number, y: number): Promise { const starHandles = this.root.get[]>(starListKey); - const newStar = await Coordinate.getFactory().createInstance(this.context); + const newStar = await Coordinate.getFactory().createChildInstance(this.context); newStar.x = x; newStar.y = y; starHandles.push(newStar.handle); diff --git a/examples/data-objects/pond/src/index.tsx b/examples/data-objects/pond/src/index.tsx index a89bf43cab4c..a6d006b70840 100644 --- a/examples/data-objects/pond/src/index.tsx +++ b/examples/data-objects/pond/src/index.tsx @@ -42,10 +42,11 @@ export class Pond extends DataObject implements IFluidHTMLView { * Do setup work here */ protected async initializingFirstTime() { - const clickerComponent = await Clicker.getFactory().createInstance(this.context); + const clickerComponent = await Clicker.getFactory().createChildInstance(this.context); this.root.set(Clicker.ComponentName, clickerComponent.handle); - const clickerComponentUsingProvider = await ExampleUsingProviders.getFactory().createInstance(this.context); + const clickerComponentUsingProvider = + await ExampleUsingProviders.getFactory().createChildInstance(this.context); this.root.set(ExampleUsingProviders.ComponentName, clickerComponentUsingProvider.handle); } diff --git a/examples/data-objects/simple-component-embed/src/index.tsx b/examples/data-objects/simple-component-embed/src/index.tsx index 59ce1ad76148..66144db67586 100644 --- a/examples/data-objects/simple-component-embed/src/index.tsx +++ b/examples/data-objects/simple-component-embed/src/index.tsx @@ -24,7 +24,7 @@ export class SimpleComponentEmbed extends DataObject implements IFluidHTMLView { * but in this scenario we only want it to be created once. */ protected async initializingFirstTime() { - const component = await ClickerInstantiationFactory.createInstance(this.context); + const component = await ClickerInstantiationFactory.createChildInstance(this.context); this.root.set("myEmbeddedCounter", component.handle); } diff --git a/examples/data-objects/table-document/src/document.ts b/examples/data-objects/table-document/src/document.ts index fe22a12144f0..ca8eb1f19f86 100644 --- a/examples/data-objects/table-document/src/document.ts +++ b/examples/data-objects/table-document/src/document.ts @@ -76,7 +76,7 @@ export class TableDocument extends DataObject<{}, {}, ITableDocumentEvents> impl minCol: number, maxRow: number, maxCol: number): Promise { - const component = await TableSlice.getFactory().createInstance( + const component = await TableSlice.getFactory().createChildInstance( this.context, { docId: this.runtime.id, name, minRow, minCol, maxRow, maxCol }, ); diff --git a/examples/data-objects/table-view/src/tableview.ts b/examples/data-objects/table-view/src/tableview.ts index 7384e3fbfa12..3a9b02149b98 100644 --- a/examples/data-objects/table-view/src/tableview.ts +++ b/examples/data-objects/table-view/src/tableview.ts @@ -92,7 +92,7 @@ export class TableView extends DataObject implements IFluidHTMLView { protected async initializingFirstTime() { // Set up internal table doc - const doc = await TableDocument.getFactory().createInstance(this.context); + const doc = await TableDocument.getFactory().createChildInstance(this.context); this.root.set(innerDocKey, doc.handle); doc.insertRows(0, 5); doc.insertCols(0, 8); diff --git a/examples/data-objects/todo/src/Todo/Todo.tsx b/examples/data-objects/todo/src/Todo/Todo.tsx index 71facd7598f4..15e9983756fd 100644 --- a/examples/data-objects/todo/src/Todo/Todo.tsx +++ b/examples/data-objects/todo/src/Todo/Todo.tsx @@ -82,7 +82,7 @@ export class Todo extends DataObject implements IFluidHTMLView { public async addTodoItemComponent(props?: ITodoItemInitialState) { // Create a new todo item - const component = await TodoItem.getFactory().createInstance(this.context, props); + const component = await TodoItem.getFactory().createChildInstance(this.context, props); // Store the id of the component in our ids map so we can reference it later this.todoItemsMap.set(component.url, component.handle); diff --git a/examples/data-objects/todo/src/TodoItem/TodoItem.tsx b/examples/data-objects/todo/src/TodoItem/TodoItem.tsx index 3bfc5b696c13..127ebdb1e371 100644 --- a/examples/data-objects/todo/src/TodoItem/TodoItem.tsx +++ b/examples/data-objects/todo/src/TodoItem/TodoItem.tsx @@ -173,19 +173,19 @@ export class TodoItem extends DataObject<{}, ITodoItemInitialState> implements I let component: IFluidLoadable; switch (type) { case "todo": - component = await TodoItem.getFactory().createInstance( + component = await TodoItem.getFactory().createPeerInstance( this.context, { startingText: type }, ); break; case "clicker": - component = await ClickerInstantiationFactory.createInstance(this.context); + component = await ClickerInstantiationFactory.createChildInstance(this.context); break; case "textBox": - component = await TextBoxInstantiationFactory.createInstance(this.context, type); + component = await TextBoxInstantiationFactory.createChildInstance(this.context, type); break; case "textList": - component = await TextListInstantiationFactory.createInstance(this.context); + component = await TextListInstantiationFactory.createChildInstance(this.context); break; default: } diff --git a/examples/data-objects/vltava/src/components/anchor/anchor.ts b/examples/data-objects/vltava/src/components/anchor/anchor.ts index 4f03f23ca1ca..8af0638110e9 100644 --- a/examples/data-objects/vltava/src/components/anchor/anchor.ts +++ b/examples/data-objects/vltava/src/components/anchor/anchor.ts @@ -47,10 +47,10 @@ export class Anchor extends DataObject implements IProvideFluidHTMLView, IProvid } protected async initializingFirstTime() { - const defaultComponent = await Vltava.getFactory().createInstance(this.context); + const defaultComponent = await Vltava.getFactory().createChildInstance(this.context); this.root.set(this.defaultComponentId, defaultComponent.handle); - const lastEditedComponent = await LastEditedTrackerDataObject.getFactory().createInstance(this.context); + const lastEditedComponent = await LastEditedTrackerDataObject.getFactory().createChildInstance(this.context); this.root.set(this.lastEditedComponentId, lastEditedComponent.handle); } diff --git a/examples/data-objects/vltava/src/components/tabs/dataModel.ts b/examples/data-objects/vltava/src/components/tabs/dataModel.ts index 44341c05287d..fa9aa9c9cc1d 100644 --- a/examples/data-objects/vltava/src/components/tabs/dataModel.ts +++ b/examples/data-objects/vltava/src/components/tabs/dataModel.ts @@ -79,7 +79,7 @@ export class TabsDataModel extends EventEmitter implements ITabsDataModel { public async createTab(factory: IFluidDataStoreFactory): Promise { const newKey = uuid(); - const component = await this.createSubObject.createAnonymousInstance(factory); + const component = await this.createSubObject.createAnonymousChildInstance(factory); this.tabs.set(newKey, { type: factory.type, handleOrId: component.handle, diff --git a/examples/data-objects/vltava/src/components/vltava/vltava.tsx b/examples/data-objects/vltava/src/components/vltava/vltava.tsx index 83f9b642bcd3..3f0722f4907f 100644 --- a/examples/data-objects/vltava/src/components/vltava/vltava.tsx +++ b/examples/data-objects/vltava/src/components/vltava/vltava.tsx @@ -36,7 +36,7 @@ export class Vltava extends DataObject implements IFluidHTMLView { public get IFluidHTMLView() { return this; } protected async initializingFirstTime() { - const tabsComponent = await TabsComponent.getFactory().createInstance(this.context); + const tabsComponent = await TabsComponent.getFactory().createChildInstance(this.context); this.root.set("tabs-component-id", tabsComponent.handle); } diff --git a/packages/framework/aqueduct/src/data-object-factories/pureDataObjectFactory.ts b/packages/framework/aqueduct/src/data-object-factories/pureDataObjectFactory.ts index 76738d47cefc..f511d9207f6c 100644 --- a/packages/framework/aqueduct/src/data-object-factories/pureDataObjectFactory.ts +++ b/packages/framework/aqueduct/src/data-object-factories/pureDataObjectFactory.ts @@ -3,9 +3,10 @@ * Licensed under the MIT License. */ + import assert from "assert"; import { IRequest, IFluidObject } from "@fluidframework/core-interfaces"; import { FluidDataStoreRuntime, ISharedObjectRegistry } from "@fluidframework/datastore"; -import { FluidDataStoreRegistry, buildRegistryPath } from "@fluidframework/container-runtime"; +import { FluidDataStoreRegistry } from "@fluidframework/container-runtime"; import { IFluidDataStoreContext, IContainerRuntimeBase, @@ -27,19 +28,31 @@ import { PureDataObject, } from "../data-objects"; +function buildRegistryPath( + context: IFluidDataStoreContext, + factory: IFluidDataStoreFactory) +{ + const parentPath = context.packagePath; + assert(parentPath.length > 0); + // A factory could not contain the registry for itself. So if it is the same the last snapshot + // pkg, return our package path. + assert(parentPath[parentPath.length - 1] !== factory.type); + return [...parentPath, factory.type]; +} + /* * An association of identifiers to component registry entries, where the * entries can be used to create components. */ export interface IFluidDataObjectFactory { - createInstance< + createChildInstance< P, S, TObject extends PureDataObject, TFactory extends PureDataObjectFactory> (subFactory: TFactory, props?: S): Promise; - createAnonymousInstance( + createAnonymousChildInstance( subFactory: IFluidDataStoreFactory, request?: string | IRequest): Promise; } @@ -48,27 +61,29 @@ class FluidDataObjectFactory { constructor(private readonly context: IFluidDataStoreContext) { } - public async createInstance< + public async createChildInstance< P, S, TObject extends PureDataObject, TFactory extends PureDataObjectFactory>(subFactory: TFactory, props?: S) { - return subFactory.createInstance(this.context, props); + return subFactory.createChildInstance(this.context, props); } - public async createAnonymousInstance( + public async createAnonymousChildInstance( subFactory: IFluidDataStoreFactory, request: string | IRequest = "/") { - const packagePath = await buildRegistryPath(this.context, subFactory); - const router = await this.context.containerRuntime.createDataStore(packagePath); + const packagePath = buildRegistryPath(this.context, subFactory); + const factory2 = await this.context.IFluidDataStoreRegistry?.get(subFactory.type); + assert(factory2 === subFactory); + const router = await this.context.containerRuntime.createDataStore(packagePath); return requestFluidObject(router, request); } } export const getFluidObjectFactoryFromInstance = (context: IFluidDataStoreContext) => - new FluidDataObjectFactory(context); + new FluidDataObjectFactory(context) as IFluidDataObjectFactory; /** * PureDataObjectFactory is a barebones IFluidDataStoreFactory for use with PureDataObject. @@ -180,6 +195,25 @@ export class PureDataObjectFactory, P, S> return instance; } + /** + * Takes context, and creates package path for a sub-entry (represented by factory) in context registry. + * Package path returned is used to reach given factory from root (container runtime) registry, and thus + * is used to serizlize and de-serialize future data store that such factory would create in future. + * Function validates that given factory is present in registry, otherwise it throws. + */ + protected buildRegistryPath( + context: IFluidDataStoreContext | IContainerRuntimeBase) + { + let packagePath: string[]; + if ("containerRuntime" in context) { + packagePath = buildRegistryPath(context, this); + } else { + packagePath = [this.type]; + } + + return packagePath; + } + /** * Implementation of IFluidDataStoreFactory's createInstance method that also exposes an initial * state argument. Only specific factory instances are intended to take initial state. @@ -189,11 +223,33 @@ export class PureDataObjectFactory, P, S> * @returns A promise for a component that will have been initialized. Caller is responsible * for attaching the component to the provided runtime's container such as by storing its handle */ - public async createInstance( - parentContext: IFluidDataStoreContext | IContainerRuntimeBase, + public async createChildInstance( + parentContext: IFluidDataStoreContext, + initialState?: S, + ): Promise { + const packagePath = buildRegistryPath(parentContext, this); + return this.createInstanceCore(parentContext.containerRuntime, packagePath, initialState); + } + + public async createPeerInstance( + peerContext: IFluidDataStoreContext, + initialState?: S, + ): Promise { + return this.createInstanceCore(peerContext.containerRuntime, peerContext.packagePath, initialState); + } + + public async createRootInstance( + runtime: IContainerRuntimeBase, + initialState?: S, + ): Promise { + return this.createInstanceCore(runtime, [this.type], initialState); + } + + public async createInstanceCore( + containerRuntime: IContainerRuntimeBase, + packagePath: Readonly, initialState?: S, ): Promise { - const containerRuntime = "containerRuntime" in parentContext ? parentContext.containerRuntime : parentContext; const newContext = containerRuntime.createDetachedDataStore(); // const runtime = this.instantiateDataStoreCore(newContext, initialState); @@ -209,7 +265,7 @@ export class PureDataObjectFactory, P, S> return instance.request(request); }); - await newContext.attachRuntime(parentContext, this, runtime); + await newContext.attachRuntime(packagePath, this, runtime); return instanceP; } diff --git a/packages/runtime/container-runtime/src/dataStoreContext.ts b/packages/runtime/container-runtime/src/dataStoreContext.ts index b46ad2faf960..8552b9cbf39b 100644 --- a/packages/runtime/container-runtime/src/dataStoreContext.ts +++ b/packages/runtime/container-runtime/src/dataStoreContext.ts @@ -41,7 +41,6 @@ import { IAttachMessage, IFluidDataStoreContext, IFluidDataStoreContextDetached, - IFluidDataStoreFactory, IFluidDataStoreRegistry, IInboundSignalMessage, ISummarizeResult, @@ -50,9 +49,7 @@ import { CreateChildSummarizerNodeFn, SummarizeInternalFn, CreateChildSummarizerNodeParam, - IContainerRuntimeBase, IProvideFluidDataStoreFactory, - IProvideFluidDataStoreRegistry, } from "@fluidframework/runtime-definitions"; import { SummaryTracker, addBlobToSummary, convertToSummaryTree } from "@fluidframework/runtime-utils"; import { ContainerRuntime } from "./containerRuntime"; @@ -62,33 +59,6 @@ const currentSnapshotFormatVersion = "0.1"; const attributesBlobKey = ".component"; -/** - * Takes context, and creates package path for a sub-entry (represented by factory) in context registry. - * Package path returned is used to reach given factory from root (container runtime) registry, and thus - * is used to serizlize and de-serialize future data store that such factory would create in future. - * Function validates that given factory is present in registry, otherwise it throws. - */ -export async function buildRegistryPath( - context: IFluidDataStoreContext | IContainerRuntimeBase, - factory: IFluidDataStoreFactory) -{ - let packagePath: string[]; - if ("containerRuntime" in context) { - const parentPath = context.packagePath; - assert(parentPath.length > 0); - // A factory could not contain the registry for itself. So if it is the same the last snapshot - // pkg, return our package path. - assert(parentPath[parentPath.length - 1] !== factory.type); - packagePath = [...parentPath, factory.type]; - } else { - packagePath = [factory.type]; - } - - const factory2 = await context.IFluidDataStoreRegistry?.get(factory.type); - assert(factory2 === factory); - return packagePath; -} - function createAttributes(pkg: readonly string[]): IFluidDataStoreAttributes { const stringifiedPkg = JSON.stringify(pkg); return { @@ -267,7 +237,7 @@ export abstract class FluidDataStoreContext extends EventEmitter implements } } - private async rejectDeferredRealize(reason: string) { + private rejectDeferredRealize(reason: string): never { const error = new Error(reason); // Error messages contain package names that is considered Personal Identifiable Information // Mark it as such, so that if it ever reaches telemetry pipeline, it has a chance to remove it. @@ -286,14 +256,7 @@ export abstract class FluidDataStoreContext extends EventEmitter implements return this.channelDeferred.promise; } - private async realizeCore(): Promise { - this.channelDeferred = new Deferred(); - const details = await this.getInitialSnapshotDetails(); - // Base snapshot is the baseline where pending ops are applied to. - // It is important that this be in sync with the pending ops, and also - // that it is set here, before bindRuntime is called. - this._baseSnapshot = details.snapshot; - const packages = details.pkg; + protected async factoryFromPackagePath(packages) { assert(this.pkg === packages); let entry: FluidDataStoreRegistryEntry | undefined; @@ -301,22 +264,37 @@ export abstract class FluidDataStoreContext extends EventEmitter implements let lastPkg: string | undefined; for (const pkg of packages) { if (!registry) { - return this.rejectDeferredRealize(`No registry for ${lastPkg} package`); + this.rejectDeferredRealize(`No registry for ${lastPkg} package`); } lastPkg = pkg; entry = await registry.get(pkg); if (!entry) { - return this.rejectDeferredRealize(`Registry does not contain entry for the package ${pkg}`); + this.rejectDeferredRealize(`Registry does not contain entry for the package ${pkg}`); } registry = entry.IFluidDataStoreRegistry; } const factory = entry?.IFluidDataStoreFactory; if (factory === undefined) { - return this.rejectDeferredRealize(`Can't find factory for ${lastPkg} package`); + this.rejectDeferredRealize(`Can't find factory for ${lastPkg} package`); } + return { factory, registry }; + } + + private async realizeCore(): Promise { + this.channelDeferred = new Deferred(); + const details = await this.getInitialSnapshotDetails(); + // Base snapshot is the baseline where pending ops are applied to. + // It is important that this be in sync with the pending ops, and also + // that it is set here, before bindRuntime is called. + this._baseSnapshot = details.snapshot; + const packages = details.pkg; + + const { factory, registry } = await this.factoryFromPackagePath(packages); + assert(this.registry === undefined); this.registry = registry; + const channel = await factory.instantiateDataStore(this); // back-compat: <= 0.25 allows returning nothing and calling bindRuntime() later directly. @@ -843,17 +821,21 @@ export class LocalDetachedFluidDataStoreContext } public async attachRuntime( - parentContext: IFluidDataStoreContext | IContainerRuntimeBase, - factory: IProvideFluidDataStoreFactory & Partial, + packagePath: Readonly, + registry: IProvideFluidDataStoreFactory, dataStoreRuntime: IFluidDataStoreChannel) { assert(this.detachedRuntimeCreation); assert(this.pkg === undefined); - this.pkg = await buildRegistryPath(parentContext, factory.IFluidDataStoreFactory); + const factory = registry.IFluidDataStoreFactory; + this.pkg = packagePath; + + const entry = await this.factoryFromPackagePath(this.pkg); + assert(entry.factory === factory); assert(this.registry === undefined); - this.registry = factory.IFluidDataStoreRegistry; + this.registry = entry.registry; super.bindRuntime(dataStoreRuntime); } diff --git a/packages/runtime/container-runtime/src/index.ts b/packages/runtime/container-runtime/src/index.ts index b41d6d38d8d4..455793811e1d 100644 --- a/packages/runtime/container-runtime/src/index.ts +++ b/packages/runtime/container-runtime/src/index.ts @@ -5,7 +5,6 @@ export * from "./containerRuntime"; export * from "./deltaScheduler"; -export { buildRegistryPath } from "./dataStoreContext"; export * from "./dataStoreRegistry"; export * from "./runWhileConnectedCoordinator"; export * from "./summarizer"; diff --git a/packages/runtime/runtime-definitions/src/componentContext.ts b/packages/runtime/runtime-definitions/src/componentContext.ts index 46873a3f115e..c55a64c271b7 100644 --- a/packages/runtime/runtime-definitions/src/componentContext.ts +++ b/packages/runtime/runtime-definitions/src/componentContext.ts @@ -352,8 +352,8 @@ export interface IFluidDataStoreContextDetached extends IFluidDataStoreContext { * Binds a runtime to the context. */ attachRuntime( - parentContext: IFluidDataStoreContext | IContainerRuntimeBase, - factory: IProvideFluidDataStoreFactory & Partial, + packagePath: Readonly, + factory: IProvideFluidDataStoreFactory, dataStoreRuntime: IFluidDataStoreChannel, ): Promise; } diff --git a/packages/test/end-to-end-tests/src/test/componentHandle.spec.ts b/packages/test/end-to-end-tests/src/test/componentHandle.spec.ts index 57271352d5f1..624145712baa 100644 --- a/packages/test/end-to-end-tests/src/test/componentHandle.spec.ts +++ b/packages/test/end-to-end-tests/src/test/componentHandle.spec.ts @@ -85,7 +85,7 @@ describe("FluidOjectHandle", () => { const firstContainer = await createContainer(); firstContainerComponent1 = await requestFluidObject("default", firstContainer); firstContainerComponent2 = - await TestSharedComponentFactory.createInstance(firstContainerComponent1._context.containerRuntime); + await TestSharedComponentFactory.createRootInstance(firstContainerComponent1._context.containerRuntime); const secondContainer = await createContainer(); secondContainerComponent1 = await requestFluidObject("default", secondContainer); From da018d1bb0d41531f04b84c324046e54b94ca856 Mon Sep 17 00:00:00 2001 From: Vlad Sudzilouski Date: Thu, 13 Aug 2020 12:39:45 -0700 Subject: [PATCH 19/24] Incorrect merges in breaking.md --- BREAKING.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/BREAKING.md b/BREAKING.md index 38d8d0f2234b..71dd793c372d 100644 --- a/BREAKING.md +++ b/BREAKING.md @@ -8,7 +8,6 @@ - [IContainerRuntimeBase._createDataStoreWithProps() is removed](#IContainerRuntimeBase._createDataStoreWithProps-is-removed) - [_createDataStore() APIs are removed](#_createDataStore-APIs-are-removed) - [createDataStoreWithRealizationFn() APIs are removed](#createDataStoreWithRealizationFn()-APIs-are-removed) -- [createDataStoreWithRealizationFn() APIs moved](#createDataStoreWithRealizationFn()-APIs-moved) - [getDataStore() APIs is removed](#getDataStore()-APIs-is-removed) - [Package Renames](#package-renames) - [IComponent and IComponent Interfaces Removed](#IComponent-and-IComponent-Interfaces-Removed) @@ -27,7 +26,6 @@ The following telemetry event names have been updated to drop references to the ComponentRuntimeDisposeError -> ChannelDisposeError ComponentContextDisposeError -> FluidDataStoreContextDisposeError SignalComponentNotFound -> SignalFluidDataStoreNotFound ->>>>>>> c6c090ca861703c5a6773788d131e201a9424dc2 ### IComponentContextLegacy is removed Deprecated in 0.18, removed. From 5ab597959d5612051763659bf732e07a862e3790 Mon Sep 17 00:00:00 2001 From: Vlad Sudzilouski Date: Thu, 13 Aug 2020 13:00:48 -0700 Subject: [PATCH 20/24] Merge marker --- .../runtime/runtime-definitions/src/dataStoreRegistry.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/runtime/runtime-definitions/src/dataStoreRegistry.ts b/packages/runtime/runtime-definitions/src/dataStoreRegistry.ts index 6c274331e28c..197a5dc8e700 100644 --- a/packages/runtime/runtime-definitions/src/dataStoreRegistry.ts +++ b/packages/runtime/runtime-definitions/src/dataStoreRegistry.ts @@ -11,12 +11,8 @@ declare module "@fluidframework/core-interfaces" { } /** -<<<<<<< HEAD:packages/runtime/runtime-definitions/src/componentRegistry.ts - * A single registry entry that may be used to create components - * It has to have either factory or registry, or both. -======= * A single registry entry that may be used to create data stores ->>>>>>> 944fb36967742538c68e46cbbc782f7f37e920bf:packages/runtime/runtime-definitions/src/dataStoreRegistry.ts +* It has to have either factory or registry, or both. */ export type FluidDataStoreRegistryEntry = Readonly>; From e167a86b6590ee96626ee7533deaa7a8db4a05e3 Mon Sep 17 00:00:00 2001 From: Vlad Sudzilouski Date: Thu, 13 Aug 2020 17:03:11 -0700 Subject: [PATCH 21/24] Comments, cleanup --- .../pureDataObjectFactory.ts | 62 +++++++++++++---- .../container-runtime/src/dataStoreContext.ts | 68 +++++++------------ .../src/dataStoreContext.ts | 9 --- 3 files changed, 73 insertions(+), 66 deletions(-) diff --git a/packages/framework/aqueduct/src/data-object-factories/pureDataObjectFactory.ts b/packages/framework/aqueduct/src/data-object-factories/pureDataObjectFactory.ts index f511d9207f6c..20119e90a54b 100644 --- a/packages/framework/aqueduct/src/data-object-factories/pureDataObjectFactory.ts +++ b/packages/framework/aqueduct/src/data-object-factories/pureDataObjectFactory.ts @@ -41,22 +41,38 @@ function buildRegistryPath( } /* - * An association of identifiers to component registry entries, where the - * entries can be used to create components. + * This interface is exposed by data store objects to create sub-objects. + * It assumes that factories passed in to methods of this interface are present in registry of object's context + * that is represented by this interface. */ export interface IFluidDataObjectFactory { + /** + * Creates a new child instance of the object. Uses PureDataObjectFactory for that, and thus we + * have type information about object created and can pass in initia state. + * @param initialState - The initial state to provide to the created component. + * @returns an object created by this factory. Data store and objects created are not attached to container. + * They get attached only when a handle to one of them is attached to already attached objects. + */ createChildInstance< P, S, TObject extends PureDataObject, TFactory extends PureDataObjectFactory> - (subFactory: TFactory, props?: S): Promise; + (subFactory: TFactory, initialState?: S): Promise; + /** + * Similar to above, but uses any data store factory. Given that there is no type information about such factory + * (or objects it creates, hanse "Anonymous" in name), IFluidObject (by default) is returned by doing a request + * to created data store. + */ createAnonymousChildInstance( subFactory: IFluidDataStoreFactory, request?: string | IRequest): Promise; } +/** + * An implementation of IFluidDataObjectFactory for PureDataObjectFactory's objects (i.e. PureDataObject). + */ class FluidDataObjectFactory { constructor(private readonly context: IFluidDataStoreContext) { } @@ -65,9 +81,9 @@ class FluidDataObjectFactory { P, S, TObject extends PureDataObject, - TFactory extends PureDataObjectFactory>(subFactory: TFactory, props?: S) + TFactory extends PureDataObjectFactory>(subFactory: TFactory, initialState?: S) { - return subFactory.createChildInstance(this.context, props); + return subFactory.createChildInstance(this.context, initialState); } public async createAnonymousChildInstance( @@ -215,13 +231,15 @@ export class PureDataObjectFactory, P, S> } /** - * Implementation of IFluidDataStoreFactory's createInstance method that also exposes an initial - * state argument. Only specific factory instances are intended to take initial state. - * @param context - The component context being used to create the component - * (the created component will have its own new context created as well) + * Creates a new instance of the object. Uses parent context's registry to build package path to this factory. + * In other words, registry of context passed in has to contain this factory, with the name that matches + * this factory's type. + * It is intended to be used by data store objects that create sub-objects. + * @param context - The component context being used to create the object + * (the created object will have its own new context created as well) * @param initialState - The initial state to provide to the created component. - * @returns A promise for a component that will have been initialized. Caller is responsible - * for attaching the component to the provided runtime's container such as by storing its handle + * @returns an object created by this factory. Data store and objects created are not attached to container. + * They get attached only when a handle to one of them is attached to already attached objects. */ public async createChildInstance( parentContext: IFluidDataStoreContext, @@ -231,6 +249,16 @@ export class PureDataObjectFactory, P, S> return this.createInstanceCore(parentContext.containerRuntime, packagePath, initialState); } + /** + * Creates a new instance of the object. Uses peer context's registry and its package path to identify this factory. + * In other words, registry of context passed in has to have this factory. + * Intended to be used by data store objects that need to create peers (similar) instances of existing objects. + * @param context - The component context being used to create the object + * (the created object will have its own new context created as well) + * @param initialState - The initial state to provide to the created component. + * @returns an object created by this factory. Data store and objects created are not attached to container. + * They get attached only when a handle to one of them is attached to already attached objects. + */ public async createPeerInstance( peerContext: IFluidDataStoreContext, initialState?: S, @@ -238,6 +266,16 @@ export class PureDataObjectFactory, P, S> return this.createInstanceCore(peerContext.containerRuntime, peerContext.packagePath, initialState); } + /** + * Creates a new instance of the object. Uses container's registry (root) to find this factory. + * It's expected that only container owners would use this functionality, as only such developers + * have knowledge of root entries in container registry. + * The name in this registry for such record should match type of this factory. + * @param runtime - container runtime. It's registry is used to create an object. + * @param initialState - The initial state to provide to the created component. + * @returns an object created by this factory. Data store and objects created are not attached to container. + * They get attached only when a handle to one of them is attached to already attached objects. + */ public async createRootInstance( runtime: IContainerRuntimeBase, initialState?: S, @@ -245,7 +283,7 @@ export class PureDataObjectFactory, P, S> return this.createInstanceCore(runtime, [this.type], initialState); } - public async createInstanceCore( + protected async createInstanceCore( containerRuntime: IContainerRuntimeBase, packagePath: Readonly, initialState?: S, diff --git a/packages/runtime/container-runtime/src/dataStoreContext.ts b/packages/runtime/container-runtime/src/dataStoreContext.ts index 8552b9cbf39b..b70f7cf87ef7 100644 --- a/packages/runtime/container-runtime/src/dataStoreContext.ts +++ b/packages/runtime/container-runtime/src/dataStoreContext.ts @@ -187,7 +187,7 @@ export abstract class FluidDataStoreContext extends EventEmitter implements protected channel: IFluidDataStoreChannel | undefined; private loaded = false; protected pending: ISequencedDocumentMessage[] | undefined = []; - private channelDeferred: Deferred | undefined; + protected channelDeferred: Deferred | undefined; private _baseSnapshot: ISnapshotTree | undefined; protected _attachState: AttachState; protected readonly summarizerNode: ISummarizerNode; @@ -282,7 +282,6 @@ export abstract class FluidDataStoreContext extends EventEmitter implements } private async realizeCore(): Promise { - this.channelDeferred = new Deferred(); const details = await this.getInitialSnapshotDetails(); // Base snapshot is the baseline where pending ops are applied to. // It is important that this be in sync with the pending ops, and also @@ -511,16 +510,9 @@ export abstract class FluidDataStoreContext extends EventEmitter implements try { - if (this.channelDeferred === undefined) { - // create deferred first, such that we can reject it in catch() block if assert fires. - this.channelDeferred = new Deferred(); - assert(this.detachedRuntimeCreation); - this.detachedRuntimeCreation = false; - } else { - assert(!this.detachedRuntimeCreation); - } - // pkg should be set for all paths except possibly for detached creation - assert(this.pkg !== undefined, "Please call attachRuntime()!"); + assert(!this.detachedRuntimeCreation); + assert(this.channelDeferred !== undefined); + assert(this.pkg !== undefined); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const pending = this.pending!; @@ -559,39 +551,6 @@ export abstract class FluidDataStoreContext extends EventEmitter implements return this._containerRuntime.getAbsoluteUrl(relativeUrl); } - /** - * Take a package name and transform it into a path that can be used to find it - * from this context, such as by looking into subregistries - * @param subpackage - The subpackage to find in this context - * @returns A list of packages to the subpackage destination if found, - * otherwise the original subpackage - */ - public async composeSubpackagePath(subpackage: string): Promise { - const details = await this.getInitialSnapshotDetails(); - let packagePath: string[] = [...details.pkg]; - - // A factory could not contain the registry for itself. So if it is the same the last snapshot - // pkg, return our package path. - if (packagePath.length > 0 && subpackage === packagePath[packagePath.length - 1]) { - return packagePath; - } - - // Look for the package entry in our sub-registry. If we find the entry, we need to add our path - // to the packagePath. If not, look into the global registry and the packagePath becomes just the - // passed package. - if (await this.registry?.get(subpackage)) { - packagePath.push(subpackage); - } else { - if (!(await this._containerRuntime.IFluidDataStoreRegistry.get(subpackage))) { - throw new Error(`Registry does not contain entry for package '${subpackage}'`); - } - - packagePath = [subpackage]; - } - - return packagePath; - } - public abstract generateAttachMessage(): IAttachMessage; protected abstract getInitialSnapshotDetails(): Promise; @@ -707,6 +666,9 @@ export class RemotedFluidDataStoreContext extends FluidDataStoreContext { } } +/** + * Base class for detached & attached context classes + */ export class LocalFluidDataStoreContextBase extends FluidDataStoreContext { constructor( id: string, @@ -771,6 +733,12 @@ export class LocalFluidDataStoreContextBase extends FluidDataStoreContext { } } +/** + * context implementation for "attached" data store runtime. + * Various workflows (snapshot creation, requests) result in .realize() being called + * on context, resulting in instantiation and attachment of runtime. + * Runtime is created using data store factory that is associated with this context. + */ export class LocalFluidDataStoreContext extends LocalFluidDataStoreContextBase { constructor( id: string, @@ -794,6 +762,12 @@ export class LocalFluidDataStoreContext extends LocalFluidDataStoreContextBase { } } +/** + * Detached context. Data Store runtime will be attached to it by attachRuntime() call + * Before attachment happens, this context is not associated with particular type of runtime + * or factory, i.e. it's package path is undefined. + * Attachment process provides all missing parts - package path, data store runtime, and data store factory + */ export class LocalDetachedFluidDataStoreContext extends LocalFluidDataStoreContextBase implements IFluidDataStoreContextDetached @@ -826,6 +800,7 @@ export class LocalDetachedFluidDataStoreContext dataStoreRuntime: IFluidDataStoreChannel) { assert(this.detachedRuntimeCreation); + assert(this.channelDeferred === undefined); assert(this.pkg === undefined); const factory = registry.IFluidDataStoreFactory; @@ -837,6 +812,9 @@ export class LocalDetachedFluidDataStoreContext assert(this.registry === undefined); this.registry = entry.registry; + this.detachedRuntimeCreation = false; + this.channelDeferred = new Deferred(); + super.bindRuntime(dataStoreRuntime); } diff --git a/packages/runtime/runtime-definitions/src/dataStoreContext.ts b/packages/runtime/runtime-definitions/src/dataStoreContext.ts index 369c8c9c7078..4034bdcbebf7 100644 --- a/packages/runtime/runtime-definitions/src/dataStoreContext.ts +++ b/packages/runtime/runtime-definitions/src/dataStoreContext.ts @@ -333,15 +333,6 @@ export interface IFluidDataStoreContext extends EventEmitter, Partial; } export interface IFluidDataStoreContextDetached extends IFluidDataStoreContext { From dd0d502eb6580b6140d02eb3381ae9d738098a1a Mon Sep 17 00:00:00 2001 From: Vlad Sudzilouski Date: Thu, 13 Aug 2020 17:26:38 -0700 Subject: [PATCH 22/24] PR feedback --- .../src/data-object-factories/pureDataObjectFactory.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/framework/aqueduct/src/data-object-factories/pureDataObjectFactory.ts b/packages/framework/aqueduct/src/data-object-factories/pureDataObjectFactory.ts index 20119e90a54b..2fb786c0ffe0 100644 --- a/packages/framework/aqueduct/src/data-object-factories/pureDataObjectFactory.ts +++ b/packages/framework/aqueduct/src/data-object-factories/pureDataObjectFactory.ts @@ -124,8 +124,7 @@ export class PureDataObjectFactory, P, S> registryEntries?: NamedFluidDataStoreRegistryEntries, private readonly onDemandInstantiation = true, ) { - // empty string is not allowed! - if (!this.type) { + if (this.type === "") { throw new Error("undefined type member"); } if (registryEntries !== undefined) { From 8e45fb3774b8643f8a099b37c8b8646fc9d2ab5f Mon Sep 17 00:00:00 2001 From: Vlad Sudzilouski Date: Sun, 30 Aug 2020 23:02:14 -0700 Subject: [PATCH 23/24] Fix build & tests --- examples/data-objects/clicker/src/index.tsx | 2 +- .../data-objects/shared-text/src/component.ts | 8 +- .../vltava/src/components/anchor/anchor.ts | 66 -- .../vltava/src/fluidObjects/anchor/anchor.ts | 4 +- .../vltava/src/fluidObjects/vltava/vltava.tsx | 4 +- examples/data-objects/vltava/src/index.ts | 5 +- .../runtime/agent-scheduler/src/scheduler.ts | 13 +- .../component-runtime/src/componentRuntime.ts | 730 ------------------ .../container-runtime/src/containerRuntime.ts | 7 +- .../runtime/runtime-definitions/src/agent.ts | 3 +- .../src/test/fluidObjectHandle.spec.ts | 4 +- .../test/test-utils/src/localCodeLoader.ts | 8 +- 12 files changed, 29 insertions(+), 825 deletions(-) delete mode 100644 examples/data-objects/vltava/src/components/anchor/anchor.ts delete mode 100644 packages/runtime/component-runtime/src/componentRuntime.ts diff --git a/examples/data-objects/clicker/src/index.tsx b/examples/data-objects/clicker/src/index.tsx index 7bce1620289e..3b5967b767a4 100644 --- a/examples/data-objects/clicker/src/index.tsx +++ b/examples/data-objects/clicker/src/index.tsx @@ -62,7 +62,7 @@ export class Clicker extends DataObject implements IFluidHTMLView { instance: new ClickerAgent(this.counter), }; this.taskManager.register(agentTask); - this.taskManager.pick(this.url, "agent", true).then(() => { + this.taskManager.pick(agentTask.id, true).then(() => { console.log(`Picked`); }, (err) => { console.log(err); diff --git a/examples/data-objects/shared-text/src/component.ts b/examples/data-objects/shared-text/src/component.ts index 74e5be8803dc..ce4746cdd43d 100644 --- a/examples/data-objects/shared-text/src/component.ts +++ b/examples/data-objects/shared-text/src/component.ts @@ -106,9 +106,7 @@ export class SharedTextRunner } public async request(request: IRequest): Promise { - if (request.url.startsWith(this.taskManager.url)) { - return this.taskManager.request(request); - } else if (request.url === "" || request.url === "/") { + if (request.url === "" || request.url === "/") { return { status: 200, mimeType: "fluid/object", value: this }; } else { return { status: 404, mimeType: "text/plain", value: `${request.url} not found` }; @@ -199,7 +197,6 @@ export class SharedTextRunner const taskScheduler = new TaskScheduler( this.context, this.taskManager, - this.url, this.sharedString, this.insightsMap, ); @@ -275,7 +272,6 @@ class TaskScheduler { constructor( private readonly componentContext: IFluidDataStoreContext, private readonly taskManager: ITaskManager, - private readonly componentUrl: string, private readonly sharedString: SharedString, private readonly insightsMap: ISharedMap, ) { @@ -295,7 +291,7 @@ class TaskScheduler { instance: new TextAnalyzer(this.sharedString, this.insightsMap, intelTokens), }; this.taskManager.register(intelTask); - this.taskManager.pick(this.componentUrl, "intel").then(() => { + this.taskManager.pick(intelTask.id).then(() => { console.log(`Picked text analyzer`); }, (err) => { console.log(JSON.stringify(err)); diff --git a/examples/data-objects/vltava/src/components/anchor/anchor.ts b/examples/data-objects/vltava/src/components/anchor/anchor.ts deleted file mode 100644 index 8af0638110e9..000000000000 --- a/examples/data-objects/vltava/src/components/anchor/anchor.ts +++ /dev/null @@ -1,66 +0,0 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import { IFluidHandle } from "@fluidframework/core-interfaces"; -import { DataObject, DataObjectFactory } from "@fluidframework/aqueduct"; -import { - IFluidLastEditedTracker, - IProvideFluidLastEditedTracker, - LastEditedTrackerDataObject, -} from "@fluidframework/last-edited-experimental"; -import { IFluidHTMLView, IProvideFluidHTMLView } from "@fluidframework/view-interfaces"; -import { Vltava } from "../vltava"; - -/** - * Anchor is an default component is responsible for managing creation and the default component - */ -export class Anchor extends DataObject implements IProvideFluidHTMLView, IProvideFluidLastEditedTracker { - private readonly defaultComponentId = "default-component-id"; - private defaultComponentInternal: IFluidHTMLView | undefined; - private readonly lastEditedComponentId = "last-edited-component-id"; - private lastEditedComponent: IFluidLastEditedTracker | undefined; - - private get defaultComponent() { - if (!this.defaultComponentInternal) { - throw new Error("Default Component was not initialized properly"); - } - - return this.defaultComponentInternal; - } - - private static readonly factory = new DataObjectFactory("anchor", Anchor, [], {}); - - public static getFactory() { - return Anchor.factory; - } - - public get IFluidHTMLView() { return this.defaultComponent; } - - public get IFluidLastEditedTracker() { - if (!this.lastEditedComponent) { - throw new Error("LastEditedTrackerDataObject was not initialized properly"); - } - - return this.lastEditedComponent; - } - - protected async initializingFirstTime() { - const defaultComponent = await Vltava.getFactory().createChildInstance(this.context); - this.root.set(this.defaultComponentId, defaultComponent.handle); - - const lastEditedComponent = await LastEditedTrackerDataObject.getFactory().createChildInstance(this.context); - this.root.set(this.lastEditedComponentId, lastEditedComponent.handle); - } - - protected async hasInitialized() { - this.defaultComponentInternal = - (await this.root.get(this.defaultComponentId).get()) - .IFluidHTMLView; - - this.lastEditedComponent = - (await this.root.get(this.lastEditedComponentId).get()) - .IFluidLastEditedTracker; - } -} diff --git a/examples/data-objects/vltava/src/fluidObjects/anchor/anchor.ts b/examples/data-objects/vltava/src/fluidObjects/anchor/anchor.ts index 58ee733d1cb4..9826d035d4f1 100644 --- a/examples/data-objects/vltava/src/fluidObjects/anchor/anchor.ts +++ b/examples/data-objects/vltava/src/fluidObjects/anchor/anchor.ts @@ -47,10 +47,10 @@ export class Anchor extends DataObject implements IProvideFluidHTMLView, IProvid } protected async initializingFirstTime() { - const defaultFluidObject = await Vltava.getFactory().createInstance(this.context); + const defaultFluidObject = await Vltava.getFactory().createRootInstance(this.context.containerRuntime); this.root.set(this.defaultFluidObjectId, defaultFluidObject.handle); - const lastEditedFluidObject = await LastEditedTrackerDataObject.getFactory().createInstance(this.context); + const lastEditedFluidObject = await LastEditedTrackerDataObject.getFactory().createChildInstance(this.context); this.root.set(this.lastEditedFluidObjectId, lastEditedFluidObject.handle); } diff --git a/examples/data-objects/vltava/src/fluidObjects/vltava/vltava.tsx b/examples/data-objects/vltava/src/fluidObjects/vltava/vltava.tsx index dfd4efb7c0b0..7f94650a5fb4 100644 --- a/examples/data-objects/vltava/src/fluidObjects/vltava/vltava.tsx +++ b/examples/data-objects/vltava/src/fluidObjects/vltava/vltava.tsx @@ -9,7 +9,7 @@ import { IFluidHTMLView } from "@fluidframework/view-interfaces"; import React from "react"; import ReactDOM from "react-dom"; -import { TabsComponent } from "../tabs"; +import { TabsFluidObject } from "../tabs"; import { IVltavaDataModel, VltavaDataModel } from "./dataModel"; import { VltavaView } from "./view"; @@ -36,7 +36,7 @@ export class Vltava extends DataObject implements IFluidHTMLView { public get IFluidHTMLView() { return this; } protected async initializingFirstTime() { - const tabsFluidObject = await TabsComponent.getFactory().createChildInstance(this.context); + const tabsFluidObject = await TabsFluidObject.getFactory().createChildInstance(this.context); this.root.set("tabs-component-id", tabsFluidObject.handle); } diff --git a/examples/data-objects/vltava/src/index.ts b/examples/data-objects/vltava/src/index.ts index 2e9d45e7daea..88cb9742226e 100644 --- a/examples/data-objects/vltava/src/index.ts +++ b/examples/data-objects/vltava/src/index.ts @@ -18,7 +18,6 @@ import { import { IFluidDataStoreRegistry, IProvideFluidDataStoreFactory, - IFluidDataStoreFactory, NamedFluidDataStoreRegistryEntries, } from "@fluidframework/runtime-definitions"; import { requestFluidObject } from "@fluidframework/runtime-utils"; @@ -124,7 +123,7 @@ const generateFactory = () => { const containerFluidObjects: [string, Promise][] = []; containerFluidObjectsDefinition.forEach((value) => { - containerFluidObjects.push([value.factory.type, value.factory]); + containerFluidObjects.push([value.factory.type, Promise.resolve(value.factory)]); }); // TODO: You should be able to specify the default registry instead of just a list of fluidObjects @@ -137,7 +136,7 @@ const generateFactory = () => { // We don't want to include the default wrapper fluidObject in our list of available fluidObjects Anchor.getFactory().registryEntry, Vltava.getFactory().registryEntry, - [internalRegistry"", Promise.resolve(new InternalRegistry(containerFluidObjectsDefinition))], + ["internalRegistry", Promise.resolve(new InternalRegistry(containerFluidObjectsDefinition))], ], ); }; diff --git a/packages/runtime/agent-scheduler/src/scheduler.ts b/packages/runtime/agent-scheduler/src/scheduler.ts index 47a03a874659..6f7d404ece6d 100644 --- a/packages/runtime/agent-scheduler/src/scheduler.ts +++ b/packages/runtime/agent-scheduler/src/scheduler.ts @@ -24,7 +24,7 @@ import { IFluidDataStoreFactory, ITask, ITaskManager, - SchedulerType, + NamedFluidDataStoreRegistryEntry, } from "@fluidframework/runtime-definitions"; import debug from "debug"; import { v4 as uuid } from "uuid"; @@ -437,14 +437,13 @@ export class TaskManager implements ITaskManager { /** * {@inheritDoc ITaskManager.pick} */ - public async pick(dataStoreUrl: string, taskId: string, worker?: boolean): Promise { + public async pick(taskId: string, worker?: boolean): Promise { if (!this.context.deltaManager.clientDetails.capabilities.interactive) { return Promise.reject("Picking not allowed on secondary copy"); } else if (this.runtime.attachState !== AttachState.Attached) { return Promise.reject("Picking not allowed in detached container in task manager"); } else { - const urlWithSlash = dataStoreUrl.startsWith("/") ? dataStoreUrl : `/${dataStoreUrl}`; - const fullUrl = `${urlWithSlash}/${this.url}/${taskId}`; + const fullUrl = `/${this.runtime.id}/${this.url}/${taskId}`; return this.scheduler.pick( fullUrl, async () => this.runTask(fullUrl, worker !== undefined ? worker : false)); @@ -481,11 +480,15 @@ export class TaskManager implements ITaskManager { } export class AgentSchedulerFactory implements IFluidDataStoreFactory { - public static readonly type = SchedulerType; + public static readonly type = "_scheduler"; public readonly type = AgentSchedulerFactory.type; public get IFluidDataStoreFactory() { return this; } + public static get registryEntry(): NamedFluidDataStoreRegistryEntry { + return [this.type, Promise.resolve(new AgentSchedulerFactory())]; + } + public async instantiateDataStore(context: IFluidDataStoreContext) { const mapFactory = SharedMap.getFactory(); const consensusRegisterCollectionFactory = ConsensusRegisterCollection.getFactory(); diff --git a/packages/runtime/component-runtime/src/componentRuntime.ts b/packages/runtime/component-runtime/src/componentRuntime.ts deleted file mode 100644 index 61ab0f3dfcd1..000000000000 --- a/packages/runtime/component-runtime/src/componentRuntime.ts +++ /dev/null @@ -1,730 +0,0 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import { strict as assert } from "assert"; -import { EventEmitter } from "events"; -import { ITelemetryLogger } from "@fluidframework/common-definitions"; -import { - IFluidHandle, - IFluidHandleContext, - IRequest, - IResponse, -} from "@fluidframework/core-interfaces"; -import { - IAudience, - IBlobManager, - IDeltaManager, - IGenericBlob, - ContainerWarning, - ILoader, - BindState, - AttachState, -} from "@fluidframework/container-definitions"; -import { - Deferred, - unreachableCase, -} from "@fluidframework/common-utils"; -import { - ChildLogger, - raiseConnectedEvent, -} from "@fluidframework/telemetry-utils"; -import { buildSnapshotTree } from "@fluidframework/driver-utils"; -import { TreeTreeEntry } from "@fluidframework/protocol-base"; -import { - IClientDetails, - IDocumentMessage, - IQuorum, - ISequencedDocumentMessage, - ITreeEntry, -} from "@fluidframework/protocol-definitions"; -import { - IAttachMessage, - IFluidDataStoreContext, - IFluidDataStoreRegistry, - IFluidDataStoreChannel, - IEnvelope, - IInboundSignalMessage, - ISummaryTreeWithStats, - CreateSummarizerNodeSource, -} from "@fluidframework/runtime-definitions"; -import { generateHandleContextPath, SummaryTreeBuilder, requestFluidObject } from "@fluidframework/runtime-utils"; -import { IChannel, IFluidDataStoreRuntime, IChannelFactory } from "@fluidframework/datastore-definitions"; -import { v4 as uuid } from "uuid"; -import { IChannelContext, snapshotChannel } from "./channelContext"; -import { LocalChannelContext } from "./localChannelContext"; -import { RemoteChannelContext } from "./remoteChannelContext"; - -export enum ComponentMessageType { - // Creates a new channel - Attach = "attach", - ChannelOp = "op", -} - -export interface ISharedObjectRegistry { - // TODO consider making this async. A consequence is that either the creation of a distributed data type - // is async or we need a new API to split the synchronous vs. asynchronous creation. - get(name: string): IChannelFactory | undefined; -} - -/** - * Base component class - */ -export class FluidDataStoreRuntime extends EventEmitter implements IFluidDataStoreChannel, - IFluidDataStoreRuntime, IFluidHandleContext { - /** - * Loads the data store runtime - * @param context - The component context - * @param sharedObjectRegistry - The registry of shared objects used by this component - * @param activeCallback - The callback called when the data store runtime in active - * @param componentRegistry - The registry of components created and used by this component - */ - public static load( - context: IFluidDataStoreContext, - sharedObjectRegistry: ISharedObjectRegistry, - componentRegistry?: IFluidDataStoreRegistry, - ): FluidDataStoreRuntime { - const logger = ChildLogger.create(context.containerRuntime.logger, undefined, { componentId: uuid() }); - const runtime = new FluidDataStoreRuntime( - context, - context.documentId, - context.id, - context.parentBranch, - context.existing, - context.options, - context.blobManager, - context.deltaManager, - context.getQuorum(), - context.getAudience(), - context.snapshotFn, - sharedObjectRegistry, - componentRegistry, - logger); - - return runtime; - } - - public get IFluidRouter() { return this; } - - public get connected(): boolean { - return this.componentContext.connected; - } - - public get leader(): boolean { - return this.componentContext.leader; - } - - public get clientId(): string | undefined { - return this.componentContext.clientId; - } - - public get clientDetails(): IClientDetails { - return this.componentContext.containerRuntime.clientDetails; - } - - public get loader(): ILoader { - return this.componentContext.loader; - } - - public get isAttached(): boolean { - return this.attachState !== AttachState.Detached; - } - - public get attachState(): AttachState { - return this._attachState; - } - - /** - * @deprecated - 0.21 back-compat - */ - public get path(): string { - return this.id; - } - - public get absolutePath(): string { - return generateHandleContextPath(this.id, this.routeContext); - } - - public get routeContext(): IFluidHandleContext { - return this.componentContext.containerRuntime.IFluidHandleContext; - } - - public get IFluidSerializer() { return this.componentContext.containerRuntime.IFluidSerializer; } - - public get IFluidHandleContext() { return this; } - public get IFluidDataStoreRegistry() { return this.componentRegistry; } - - private _disposed = false; - public get disposed() { return this._disposed; } - - private readonly contexts = new Map(); - private readonly contextsDeferred = new Map>(); - private readonly pendingAttach = new Map(); - private requestHandler: ((request: IRequest) => Promise) | undefined; - private bindState: BindState; - // This is used to break the recursion while attaching the graph. Also tells the attach state of the graph. - private graphAttachState: AttachState = AttachState.Detached; - private readonly deferredAttached = new Deferred(); - private readonly localChannelContextQueue = new Map(); - private readonly notBoundedChannelContextSet = new Set(); - private boundhandles: Set | undefined; - private _attachState: AttachState; - - private constructor( - private readonly componentContext: IFluidDataStoreContext, - public readonly documentId: string, - public readonly id: string, - public readonly parentBranch: string | null, - public existing: boolean, - public readonly options: any, - private readonly blobManager: IBlobManager, - public readonly deltaManager: IDeltaManager, - private readonly quorum: IQuorum, - private readonly audience: IAudience, - private readonly snapshotFn: (message: string) => Promise, - private readonly sharedObjectRegistry: ISharedObjectRegistry, - private readonly componentRegistry: IFluidDataStoreRegistry | undefined, - public readonly logger: ITelemetryLogger, - ) { - super(); - - const tree = componentContext.baseSnapshot; - - // Must always receive the component type inside of the attributes - if (tree?.trees !== undefined) { - Object.keys(tree.trees).forEach((path) => { - const channelContext = new RemoteChannelContext( - this, - componentContext, - componentContext.storage, - (content, localOpMetadata) => this.submitChannelOp(path, content, localOpMetadata), - (address: string) => this.setChannelDirty(address), - path, - tree.trees[path], - this.sharedObjectRegistry, - undefined /* extraBlobs */, - componentContext.branch, - this.componentContext.summaryTracker.createOrGetChild( - path, - this.deltaManager.lastSequenceNumber, - ), - this.componentContext.getCreateChildSummarizerNodeFn( - path, - { type: CreateSummarizerNodeSource.FromSummary }, - )); - const deferred = new Deferred(); - deferred.resolve(channelContext); - - this.contexts.set(path, channelContext); - this.contextsDeferred.set(path, deferred); - }); - } - - this.attachListener(); - this.bindState = existing ? BindState.Bound : BindState.NotBound; - this._attachState = existing ? AttachState.Attached : AttachState.Detached; - - // If it's existing we know it has been attached. - if (existing) { - this.deferredAttached.resolve(); - } - } - - public dispose(): void { - if (this._disposed) { - return; - } - this._disposed = true; - - this.emit("dispose"); - } - - public async resolveHandle(request: IRequest): Promise { - return this.request(request); - } - - public async request(request: IRequest): Promise { - // Parse out the leading slash - const id = request.url.startsWith("/") ? request.url.substr(1) : request.url; - - // Check for a data type reference first - if (this.contextsDeferred.has(id)) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const value = await this.contextsDeferred.get(id)!.promise; - const channel = await value.getChannel(); - - return { mimeType: "fluid/object", status: 200, value: channel }; - } - - // Otherwise defer to an attached request handler - if (this.requestHandler === undefined) { - return { status: 404, mimeType: "text/plain", value: `${request.url} not found` }; - } else { - return this.requestHandler(request); - } - } - - public registerRequestHandler(handler: (request: IRequest) => Promise) { - this.requestHandler = handler; - } - - public async getChannel(id: string): Promise { - this.verifyNotClosed(); - - // TODO we don't assume any channels (even root) in the runtime. If you request a channel that doesn't exist - // we will never resolve the promise. May want a flag to getChannel that doesn't wait for the promise if - // it doesn't exist - if (!this.contextsDeferred.has(id)) { - this.contextsDeferred.set(id, new Deferred()); - } - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const context = await this.contextsDeferred.get(id)!.promise; - const channel = await context.getChannel(); - - return channel; - } - - public createChannel(id: string = uuid(), type: string): IChannel { - this.verifyNotClosed(); - - assert(!this.contexts.has(id), "createChannel() with existing ID"); - this.notBoundedChannelContextSet.add(id); - const context = new LocalChannelContext( - id, - this.sharedObjectRegistry, - type, - this, - this.componentContext, - this.componentContext.storage, - (content, localOpMetadata) => this.submitChannelOp(id, content, localOpMetadata), - (address: string) => this.setChannelDirty(address)); - this.contexts.set(id, context); - - if (this.contextsDeferred.has(id)) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.contextsDeferred.get(id)!.resolve(context); - } else { - const deferred = new Deferred(); - deferred.resolve(context); - this.contextsDeferred.set(id, deferred); - } - - return context.channel; - } - - /** - * Binds a channel with the runtime. If the runtime is attached we will attach the channel right away. - * If the runtime is not attached we will defer the attach until the runtime attaches. - * @param channel - channel to be registered. - */ - public bindChannel(channel: IChannel): void { - assert(this.notBoundedChannelContextSet.has(channel.id), "Channel to be binded should be in not bounded set"); - this.notBoundedChannelContextSet.delete(channel.id); - // If our Component is attached, then attach the channel. - if (this.isAttached) { - this.attachChannel(channel); - return; - } else { - this.bind(channel.handle); - - // If our Component is local then add the channel to the queue - if (!this.localChannelContextQueue.has(channel.id)) { - this.localChannelContextQueue.set(channel.id, this.contexts.get(channel.id) as LocalChannelContext); - } - } - } - - public attachGraph() { - if (this.graphAttachState !== AttachState.Detached) { - return; - } - this.graphAttachState = AttachState.Attaching; - if (this.boundhandles !== undefined) { - this.boundhandles.forEach((handle) => { - handle.attachGraph(); - }); - this.boundhandles = undefined; - } - - // Flush the queue to set any pre-existing channels to local - this.localChannelContextQueue.forEach((channel) => { - // When we are attaching the component we don't need to send attach for the registered services. - // This is because they will be captured as part of the Attach component snapshot - channel.attach(); - }); - - this.localChannelContextQueue.clear(); - this.bindToContext(); - this.graphAttachState = AttachState.Attached; - } - - /** - * Binds this runtime to the container - * This includes the following: - * 1. Sending an Attach op that includes all existing state - * 2. Attaching the graph if the component becomes attached. - */ - public bindToContext() { - if (this.bindState !== BindState.NotBound) { - return; - } - this.bindState = BindState.Binding; - // Attach the runtime to the container via this callback - this.componentContext.bindToContext(this); - - this.bindState = BindState.Bound; - } - - public bind(handle: IFluidHandle): void { - // If the component is already attached or its graph is already in attaching or attached state, - // then attach the incoming handle too. - if (this.isAttached || this.graphAttachState !== AttachState.Detached) { - handle.attachGraph(); - return; - } - if (this.boundhandles === undefined) { - this.boundhandles = new Set(); - } - - this.boundhandles.add(handle); - } - - public setConnectionState(connected: boolean, clientId?: string) { - this.verifyNotClosed(); - - for (const [, object] of this.contexts) { - object.setConnectionState(connected, clientId); - } - - raiseConnectedEvent(this.logger, this, connected, clientId); - } - - public getQuorum(): IQuorum { - return this.quorum; - } - - public getAudience(): IAudience { - return this.audience; - } - - // eslint-disable-next-line @typescript-eslint/promise-function-async - public snapshot(message: string): Promise { - this.verifyNotClosed(); - return this.snapshotFn(message); - } - - public async uploadBlob(file: IGenericBlob): Promise { - this.verifyNotClosed(); - - const blob = await this.blobManager.createBlob(file); - file.id = blob.id; - file.url = blob.url; - - return file; - } - - // eslint-disable-next-line @typescript-eslint/promise-function-async - public getBlob(blobId: string): Promise { - this.verifyNotClosed(); - - return this.blobManager.getBlob(blobId); - } - - public async getBlobMetadata(): Promise { - return this.blobManager.getBlobMetadata(); - } - - public process(message: ISequencedDocumentMessage, local: boolean, localOpMetadata: unknown) { - this.verifyNotClosed(); - switch (message.type) { - case ComponentMessageType.Attach: { - const attachMessage = message.contents as IAttachMessage; - const id = attachMessage.id; - - // If a non-local operation then go and create the object - // Otherwise mark it as officially attached. - if (local) { - assert(this.pendingAttach.has(id), "Unexpected attach (local) channel OP"); - this.pendingAttach.delete(id); - } else { - assert(!this.contexts.has(id), "Unexpected attach channel OP"); - - // Create storage service that wraps the attach data - const origin = message.origin?.id ?? this.documentId; - - const flatBlobs = new Map(); - const snapshotTreeP = buildSnapshotTree(attachMessage.snapshot.entries, flatBlobs); - // flatBlobsP's validity is contingent on snapshotTreeP's resolution - const flatBlobsP = snapshotTreeP.then((snapshotTree) => { return flatBlobs; }); - - const remoteChannelContext = new RemoteChannelContext( - this, - this.componentContext, - this.componentContext.storage, - (content, localContentMetadata) => this.submitChannelOp(id, content, localContentMetadata), - (address: string) => this.setChannelDirty(address), - id, - snapshotTreeP, - this.sharedObjectRegistry, - flatBlobsP, - origin, - this.componentContext.summaryTracker.createOrGetChild( - id, - message.sequenceNumber, - ), - this.componentContext.getCreateChildSummarizerNodeFn( - id, - { - type: CreateSummarizerNodeSource.FromAttach, - sequenceNumber: message.sequenceNumber, - snapshot: attachMessage.snapshot, - }, - ), - attachMessage.type); - - this.contexts.set(id, remoteChannelContext); - if (this.contextsDeferred.has(id)) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.contextsDeferred.get(id)!.resolve(remoteChannelContext); - } else { - const deferred = new Deferred(); - deferred.resolve(remoteChannelContext); - this.contextsDeferred.set(id, deferred); - } - } - break; - } - - case ComponentMessageType.ChannelOp: - this.processChannelOp(message, local, localOpMetadata); - break; - default: - } - - this.emit("op", message); - } - - public processSignal(message: IInboundSignalMessage, local: boolean) { - this.emit("signal", message, local); - } - - private isChannelAttached(id: string): boolean { - return ( - // Added in createChannel - // Removed when bindChannel is called - !this.notBoundedChannelContextSet.has(id) - // Added in bindChannel only if this is not attached yet - // Removed when this is attached by calling attachGraph - && !this.localChannelContextQueue.has(id) - // Added in attachChannel called by bindChannel - // Removed when attach op is broadcast - && !this.pendingAttach.has(id) - ); - } - - public async snapshotInternal(fullTree: boolean = false): Promise { - // Craft the .attributes file for each shared object - const entries = await Promise.all(Array.from(this.contexts) - .filter(([key, _]) => { - const isAttached = this.isChannelAttached(key); - // We are not expecting local dds! Summary may not capture local state. - assert(isAttached, "Not expecting detached channels during summarize"); - // If the object is registered - and we have received the sequenced op creating the object - // (i.e. it has a base mapping) - then we go ahead and snapshot - return isAttached; - }).map(async ([key, value]) => { - const snapshot = await value.snapshot(fullTree); - - // And then store the tree - return new TreeTreeEntry(key, snapshot); - })); - - return entries; - } - - public async summarize(fullTree = false): Promise { - const builder = new SummaryTreeBuilder(); - - // Iterate over each component and ask it to snapshot - await Promise.all(Array.from(this.contexts) - .filter(([key, _]) => { - const isAttached = this.isChannelAttached(key); - // We are not expecting local dds! Summary may not capture local state. - assert(isAttached, "Not expecting detached channels during summarize"); - // If the object is registered - and we have received the sequenced op creating the object - // (i.e. it has a base mapping) - then we go ahead and snapshot - return isAttached; - }).map(async ([key, value]) => { - const channelSummary = await value.summarize(fullTree); - builder.addWithStats(key, channelSummary); - })); - - return builder.getSummaryTree(); - } - - public getAttachSnapshot(): ITreeEntry[] { - const entries: ITreeEntry[] = []; - this.attachGraph(); - - // Craft the .attributes file for each shared object - for (const [objectId, value] of this.contexts) { - if (!(value instanceof LocalChannelContext)) { - throw new Error("Should only be called with local channel handles"); - } - - if (!this.notBoundedChannelContextSet.has(objectId)) { - const snapshot = value.getAttachSnapshot(); - - // And then store the tree - entries.push(new TreeTreeEntry(objectId, snapshot)); - } - } - - return entries; - } - - public submitMessage(type: ComponentMessageType, content: any, localOpMetadata: unknown) { - this.submit(type, content, localOpMetadata); - } - - public submitSignal(type: string, content: any) { - this.verifyNotClosed(); - return this.componentContext.submitSignal(type, content); - } - - /** - * Will return when the component is attached. - */ - public async waitAttached(): Promise { - return this.deferredAttached.promise; - } - - public raiseContainerWarning(warning: ContainerWarning): void { - this.componentContext.raiseContainerWarning(warning); - } - - /** - * Attach channel should only be called after the dataStoreRuntime has been attached - */ - private attachChannel(channel: IChannel): void { - this.verifyNotClosed(); - // If this handle is already attached no need to attach again. - if (channel.handle.isAttached) { - return; - } - - channel.handle.attachGraph(); - - assert(this.isAttached, "Component should be attached to attach the channel."); - // Get the object snapshot only if the component is Bound and its graph is attached too, - // because if the graph is attaching, then it would get included in the component snapshot. - if (this.bindState === BindState.Bound && this.graphAttachState === AttachState.Attached) { - const snapshot = snapshotChannel(channel); - - const message: IAttachMessage = { - id: channel.id, - snapshot, - type: channel.attributes.type, - }; - this.pendingAttach.set(channel.id, message); - this.submit(ComponentMessageType.Attach, message); - } - - const context = this.contexts.get(channel.id) as LocalChannelContext; - context.attach(); - } - - private submitChannelOp(address: string, contents: any, localOpMetadata: unknown) { - const envelope: IEnvelope = { address, contents }; - this.submit(ComponentMessageType.ChannelOp, envelope, localOpMetadata); - } - - private submit( - type: ComponentMessageType, - content: any, - localOpMetadata: unknown = undefined): void { - this.verifyNotClosed(); - this.componentContext.submitMessage(type, content, localOpMetadata); - } - - /** - * For messages of type MessageType.Operation, finds the right channel and asks it to resubmit the message. - * For all other messages, just submit it again. - * This typically happens when we reconnect and there are unacked messages. - * @param content - The content of the original message. - * @param localOpMetadata - The local metadata associated with the original message. - */ - public reSubmit(type: ComponentMessageType, content: any, localOpMetadata: unknown) { - this.verifyNotClosed(); - - switch (type) { - case ComponentMessageType.ChannelOp: - { - // For Operations, find the right channel and trigger resubmission on it. - const envelope = content as IEnvelope; - const channelContext = this.contexts.get(envelope.address); - assert(channelContext, "There should be a channel context for the op"); - channelContext.reSubmit(envelope.contents, localOpMetadata); - break; - } - case ComponentMessageType.Attach: - // For Attach messages, just submit them again. - this.submit(type, content, localOpMetadata); - break; - default: - unreachableCase(type); - } - } - - private setChannelDirty(address: string): void { - this.verifyNotClosed(); - this.componentContext.setChannelDirty(address); - } - - private processChannelOp(message: ISequencedDocumentMessage, local: boolean, localOpMetadata: unknown) { - this.verifyNotClosed(); - - const envelope = message.contents as IEnvelope; - - const transformed: ISequencedDocumentMessage = { - ...message, - contents: envelope.contents, - }; - - const channelContext = this.contexts.get(envelope.address); - assert(channelContext, "Channel not found"); - channelContext.processOp(transformed, local, localOpMetadata); - - return channelContext; - } - - private attachListener() { - this.setMaxListeners(Number.MAX_SAFE_INTEGER); - this.componentContext.on("leader", () => { - this.emit("leader"); - }); - this.componentContext.on("notleader", () => { - this.emit("notleader"); - }); - this.componentContext.once("attaching", () => { - assert(this.bindState !== BindState.NotBound, "Component attaching should not occur if it is not bound"); - this._attachState = AttachState.Attaching; - // This promise resolution will be moved to attached event once we fix the scheduler. - this.deferredAttached.resolve(); - this.emit("attaching"); - }); - this.componentContext.once("attached", () => { - assert(this.bindState === BindState.Bound, "Component should only be attached after it is bound"); - this._attachState = AttachState.Attached; - this.emit("attached"); - }); - } - - private verifyNotClosed() { - if (this._disposed) { - throw new Error("Runtime is closed"); - } - } -} diff --git a/packages/runtime/container-runtime/src/containerRuntime.ts b/packages/runtime/container-runtime/src/containerRuntime.ts index 73e644eb5bb5..d73cdc19d1b5 100644 --- a/packages/runtime/container-runtime/src/containerRuntime.ts +++ b/packages/runtime/container-runtime/src/containerRuntime.ts @@ -79,7 +79,6 @@ import { IInboundSignalMessage, ISignalEnvelop, NamedFluidDataStoreRegistryEntries, - SchedulerType, ISummaryTreeWithStats, ISummaryStats, ISummarizeInternalResult, @@ -435,14 +434,14 @@ export class ScheduleManager { } } -export const schedulerId = SchedulerType; +export const schedulerId = "_scheduler"; // Wraps the provided list of packages and augments with some system level services. class ContainerRuntimeDataStoreRegistry extends FluidDataStoreRegistry { constructor(namedEntries: NamedFluidDataStoreRegistryEntries) { super([ ...namedEntries, - [schedulerId, Promise.resolve(new AgentSchedulerFactory())], + AgentSchedulerFactory.registryEntry, ]); } } @@ -499,7 +498,7 @@ export class ContainerRuntime extends EventEmitter // Create all internal stores in first load. if (!context.existing) { - await runtime.createRootDataStore(schedulerId, schedulerId); + await runtime.createRootDataStore(AgentSchedulerFactory.type, schedulerId); } runtime.subscribeToLeadership(); diff --git a/packages/runtime/runtime-definitions/src/agent.ts b/packages/runtime/runtime-definitions/src/agent.ts index c93e2815e689..039088786a61 100644 --- a/packages/runtime/runtime-definitions/src/agent.ts +++ b/packages/runtime/runtime-definitions/src/agent.ts @@ -49,11 +49,10 @@ export interface ITaskManager extends IProvideTaskManager, IFluidLoadable, IFlui * * @param worker - Flag that will execute tasks in web worker if connected to a service that supports them. */ - pick(dataStoreUrl: string, taskId: string, worker?: boolean): Promise; + pick(taskId: string, worker?: boolean): Promise; } export const IAgentScheduler: keyof IProvideAgentScheduler = "IAgentScheduler"; -export const SchedulerType = "_scheduler"; export interface IProvideAgentScheduler { readonly IAgentScheduler: IAgentScheduler; diff --git a/packages/test/end-to-end-tests/src/test/fluidObjectHandle.spec.ts b/packages/test/end-to-end-tests/src/test/fluidObjectHandle.spec.ts index 5379f9c2930d..e17bb159e050 100644 --- a/packages/test/end-to-end-tests/src/test/fluidObjectHandle.spec.ts +++ b/packages/test/end-to-end-tests/src/test/fluidObjectHandle.spec.ts @@ -85,8 +85,8 @@ describe("FluidObjectHandle", () => { // Create a Container for the first client. const firstContainer = await createContainer(); firstContainerObject1 = await requestFluidObject(firstContainer, "default"); - firstContainerObject2 = await testSharedDataObjectFactory.createRootInstance(firstContainerObject1._context) - .containerRuntime; + firstContainerObject2 = await testSharedDataObjectFactory.createRootInstance( + firstContainerObject1._context.containerRuntime); // Load the Container that was created by the first client. const secondContainer = await loadContainer(); diff --git a/packages/test/test-utils/src/localCodeLoader.ts b/packages/test/test-utils/src/localCodeLoader.ts index 5098cda0ff38..b657c29eb30c 100644 --- a/packages/test/test-utils/src/localCodeLoader.ts +++ b/packages/test/test-utils/src/localCodeLoader.ts @@ -14,7 +14,11 @@ import { import { IProvideFluidDataStoreFactory, IProvideFluidDataStoreRegistry } from "@fluidframework/runtime-definitions"; // Represents the entry point for a Fluid container. -export type fluidEntryPoint = Partial; +export type fluidEntryPoint = Partial< + IProvideRuntimeFactory & + IProvideFluidDataStoreFactory & + IProvideFluidDataStoreRegistry & + IFluidModule>; /** * A simple code loader that caches a mapping of package name to a Fluid entry point. @@ -65,7 +69,7 @@ export class LocalCodeLoader implements ICodeLoader { throw new Error(`Cannot find package ${pkdId}`); } - const factory = (entryPoint.fluidExport ?? entryPoint) as fluidEntryPointFinal; + const factory = (entryPoint.fluidExport ?? entryPoint) as fluidEntryPoint; if (factory.IRuntimeFactory !== undefined) { return { fluidExport: factory.IRuntimeFactory }; From afb4ad802ed619702e75a913e074ebc666933795 Mon Sep 17 00:00:00 2001 From: Vlad Sudzilouski Date: Sun, 30 Aug 2020 23:52:45 -0700 Subject: [PATCH 24/24] self-review --- .../spaces/src/fluid-object/spacesItemMap.ts | 4 +- .../src/externalComponentLoader.tsx | 78 ------- .../src/urlRegistry.ts | 89 -------- .../src/waterPark.tsx | 206 ------------------ .../data-objects/shared-text/src/index.ts | 6 +- .../vltava/src/fluidObjects/anchor/anchor.ts | 2 +- .../pureDataObjectFactory.ts | 6 +- .../src/test/fluidObjectHandle.spec.ts | 2 +- 8 files changed, 11 insertions(+), 382 deletions(-) delete mode 100644 examples/data-objects/external-component-loader/src/externalComponentLoader.tsx delete mode 100644 examples/data-objects/external-component-loader/src/urlRegistry.ts delete mode 100644 examples/data-objects/external-component-loader/src/waterPark.tsx diff --git a/examples/apps/spaces/src/fluid-object/spacesItemMap.ts b/examples/apps/spaces/src/fluid-object/spacesItemMap.ts index b2d4e0b27f9e..3a51a39c46d2 100644 --- a/examples/apps/spaces/src/fluid-object/spacesItemMap.ts +++ b/examples/apps/spaces/src/fluid-object/spacesItemMap.ts @@ -25,9 +25,9 @@ interface ISingleHandleItem { // eslint-disable-next-line @typescript-eslint/promise-function-async, prefer-arrow/prefer-arrow-functions function createSingleHandleItem(subFactory: IFluidDataStoreFactory) { return async (dataObjectFactory: IFluidDataObjectFactory): Promise => { - const component = await dataObjectFactory.createAnonymousChildInstance(subFactory); + const object = await dataObjectFactory.createAnonymousChildInstance(subFactory); return { - handle: component.handle, + handle: object.handle, }; }; } diff --git a/examples/data-objects/external-component-loader/src/externalComponentLoader.tsx b/examples/data-objects/external-component-loader/src/externalComponentLoader.tsx deleted file mode 100644 index d6969950abde..000000000000 --- a/examples/data-objects/external-component-loader/src/externalComponentLoader.tsx +++ /dev/null @@ -1,78 +0,0 @@ -/*! -* Copyright (c) Microsoft Corporation. All rights reserved. -* Licensed under the MIT License. -*/ - -import { DataObject, DataObjectFactory } from "@fluidframework/aqueduct"; -import { - IFluidObject, - IFluidLoadable, - IFluidRouter, -} from "@fluidframework/core-interfaces"; -import { requestFluidObject } from "@fluidframework/runtime-utils"; -import { UrlRegistry } from "./urlRegistry"; - -/** - * Component that loads external components via their url - */ -export class ExternalComponentLoader extends DataObject { - public static get ComponentName() { return "@fluid-example/external-component-loader"; } - - private static readonly factory = new DataObjectFactory( - ExternalComponentLoader.ComponentName, - ExternalComponentLoader, - [], - {}, - [["url", Promise.resolve(new UrlRegistry())]], - ); - - public static getFactory() { - return ExternalComponentLoader.factory; - } - - /** - * Creates the component retrieved from the given location. Adds it to the registry dynamically if needed. - * @param componentUrl - the URL of the component to create, adding it to the registry if needed. - */ - public async createComponentFromUrl(componentUrl: string): Promise { - const urlReg = await this.context.IFluidDataStoreRegistry?.get("url"); - if (urlReg?.IFluidDataStoreRegistry === undefined) { - throw new Error("Couldn't get url component registry"); - } - - // Calling .get() on the urlReg registry will also add it to the registry if it's not already there. - const pkgReg = await urlReg.IFluidDataStoreRegistry.get(componentUrl) as IFluidObject; - let router: IFluidRouter; - if (pkgReg?.IFluidExportDefaultFactoryName !== undefined) { - router = await this.context.containerRuntime.createDataStore( - [ - ...this.context.packagePath, - "url", - componentUrl, - pkgReg.IFluidExportDefaultFactoryName.getDefaultFactoryName(), - ]); - } else if (pkgReg?.IFluidDataStoreFactory !== undefined) { - router = await this.context.containerRuntime.createDataStore( - [ - ...this.context.packagePath, - "url", - componentUrl, - ]); - } else { - throw new Error(`${componentUrl} is not a factory, and does not provide default component name`); - } - - let obj = await requestFluidObject(router, "/"); - if (obj.IFluidLoadable === undefined) { - throw new Error(`${componentUrl} must implement the IFluidLoadable interface to be loaded here`); - } - if (obj.IFluidObjectCollection !== undefined) { - obj = obj.IFluidObjectCollection.createCollectionItem(); - if (obj.IFluidLoadable === undefined) { - throw new Error(`${componentUrl} must implement the IFluidLoadable interface to be loaded here`); - } - } - - return obj.IFluidLoadable; - } -} diff --git a/examples/data-objects/external-component-loader/src/urlRegistry.ts b/examples/data-objects/external-component-loader/src/urlRegistry.ts deleted file mode 100644 index 51074fee1ad6..000000000000 --- a/examples/data-objects/external-component-loader/src/urlRegistry.ts +++ /dev/null @@ -1,89 +0,0 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import { - IFluidPackage, - isFluidPackage, - IFluidCodeDetails, -} from "@fluidframework/container-definitions"; -import { - IProvideFluidDataStoreRegistry, - FluidDataStoreRegistryEntry, -} from "@fluidframework/runtime-definitions"; -import { IFluidObject } from "@fluidframework/core-interfaces"; -import { WebCodeLoader, SemVerCdnCodeResolver } from "@fluidframework/web-code-loader"; - -/** - * A component registry that can load component via their url - */ -export class UrlRegistry implements IProvideFluidDataStoreRegistry { - private static readonly WindowKeyPrefix = "FluidExternalComponent"; - - private readonly urlRegistryMap = new Map>(); - private readonly loadingPackages: Map>; - private readonly webloader = new WebCodeLoader(new SemVerCdnCodeResolver()); - - constructor() { - // Stash on the window so multiple instance can coordinate - const loadingPackagesKey = `${UrlRegistry.WindowKeyPrefix}LoadingPackages`; - if (window[loadingPackagesKey] === undefined) { - window[loadingPackagesKey] = new Map>(); - } - this.loadingPackages = window[loadingPackagesKey] as Map>; - } - - public get IFluidDataStoreRegistry() { return this; } - - /** - * Gets a registry entry, or will try to load based on the passed name if not found. - * @param name - the registry name, which may be a URL to retrieve from or a published package name. - */ - public async get(name: string): Promise { - if (!this.urlRegistryMap.has(name)) { - this.urlRegistryMap.set(name, this.loadEntrypoint(name)); - } - - return this.urlRegistryMap.get(name); - } - - private async loadEntrypoint(name: string): Promise { - if (this.isUrl(name)) { - if (!this.loadingPackages.has(name)) { - this.loadingPackages.set(name, this.loadPackage(name)); - } - } - const fluidPackage = await this.loadingPackages.get(name) ?? name; - const codeDetails: IFluidCodeDetails = { - package: fluidPackage, - config: { - cdn: "https://pragueauspkn.azureedge.net", - }, - }; - const fluidModule = await this.webloader.load(codeDetails); - return fluidModule.fluidExport; - } - - private async loadPackage(url: string): Promise { - const response = await fetch(`${url}/package.json`); - if (!response.ok) { - throw new Error(`UrlRegistry: ${url}: fetch was no ok. status code: ${response.status}`); - } else { - const packageJson = await response.json(); - if (!isFluidPackage(packageJson)) { - throw new Error(`UrlRegistry: ${url}: Package json not deserializable as IFluidPackage`); - } - // we need to resolve the package here, as - // we don't know forsure where this http endpoint is - packageJson.fluid.browser.umd.files = - packageJson.fluid.browser.umd.files.map( - (file) => this.isUrl(file) ? file : `${url}/${file}`); - return packageJson; - } - } - - private isUrl(name: string) { - return name.startsWith("http://") || name.startsWith("https://"); - } -} diff --git a/examples/data-objects/external-component-loader/src/waterPark.tsx b/examples/data-objects/external-component-loader/src/waterPark.tsx deleted file mode 100644 index eec9f1f6f156..000000000000 --- a/examples/data-objects/external-component-loader/src/waterPark.tsx +++ /dev/null @@ -1,206 +0,0 @@ -/*! -* Copyright (c) Microsoft Corporation. All rights reserved. -* Licensed under the MIT License. -*/ - -import { DataObject, DataObjectFactory } from "@fluidframework/aqueduct"; -import { - IFluidHandle, - IRequest, - IResponse, -} from "@fluidframework/core-interfaces"; -import { IPackage } from "@fluidframework/container-definitions"; -import { ReactViewAdapter } from "@fluidframework/view-adapters"; -import { IFluidHTMLView } from "@fluidframework/view-interfaces"; -import { - SpacesStorage, - SpacesStorageView, -} from "@fluid-example/spaces"; -import React from "react"; -import ReactDOM from "react-dom"; -import { RequestParser } from "@fluidframework/runtime-utils"; -import { WaterParkToolbar } from "./waterParkToolbar"; -import { ExternalComponentLoader } from "./externalComponentLoader"; - -// eslint-disable-next-line @typescript-eslint/no-require-imports -const pkg = require("../package.json") as IPackage; - -const storageKey = "storage"; -const loaderKey = "loader"; - -// defaultComponents are the component options that are always available in the waterpark. -const defaultComponents = [ - "@fluid-example/todo", - "@fluid-example/math", - "@fluid-example/monaco", - "@fluid-example/image-collection", - "@fluid-example/pond", - "@fluid-example/clicker", - "@fluid-example/primitives", - "@fluid-example/table-view", -]; - -/** - * localComponentUrls facilitates local component development. Make sure the path points to a directory containing - * the package.json for the package, and also make sure you've run webpack there first. These will only be - * available when running on localhost. - */ -const localComponentUrls = [ - // "http://localhost:8080/file/C:\\git\\FluidFramework\\components\\experimental\\todo", - // "http://localhost:8080/file/C:\\git\\FluidFramework\\components\\experimental\\clicker", -]; - -// When locally developing, want to load the latest available patch version by default -const defaultVersionToLoad = pkg.version.endsWith(".0") ? `^${pkg.version}` : pkg.version; -const componentUrls = defaultComponents.map((url) => `${url}@${defaultVersionToLoad}`); - -// When running on localhost, add entries for local component development. -if (window.location.hostname === "localhost") { - componentUrls.push(...localComponentUrls); -} - -/** - * IWaterparkItem just stores a handle, and will assume that the handle points to something that a ReactViewAdapter - * can adapt for rendering purposes. - */ -export interface IWaterparkItem { - handle: IFluidHandle; -} - -/** - * WaterPark assembles the SpacesStorage with the ExternalComponentLoader to load other components. - */ -export class WaterPark extends DataObject implements IFluidHTMLView { - public get IFluidHTMLView() { return this; } - - public static get ComponentName() { return "@fluid-example/waterpark"; } - - private static readonly factory = new DataObjectFactory( - WaterPark.ComponentName, - WaterPark, - [], - {}, - [ - [SpacesStorage.ComponentName, Promise.resolve(SpacesStorage.getFactory())], - [ExternalComponentLoader.ComponentName, Promise.resolve(ExternalComponentLoader.getFactory())], - ], - ); - - public static getFactory() { - return WaterPark.factory; - } - - private storage: SpacesStorage | undefined; - private loader: ExternalComponentLoader | undefined; - private baseUrl: string | undefined; - - public render(element: HTMLElement) { - if (this.storage === undefined) { - throw new Error("Can't render, storage not found"); - } - ReactDOM.render( - `${this.baseUrl}/${itemId}`} - />, - element, - ); - } - - // In order to handle direct links to items, we'll link to the Waterpark component with a path of the itemId for - // the specific item we want. We route through Waterpark because it knows how to get a view out of an - // IWaterparkItem. - public async request(req: IRequest): Promise { - const requestParser = new RequestParser({ url: req.url }); - // The only time we have a path will be direct links to items. - if (requestParser.pathParts.length > 0) { - const itemId = requestParser.pathParts[0]; - const item = this.storage?.itemList.get(itemId); - if (item !== undefined) { - const viewForItem = await this.getViewForItem(item.serializableItemData); - return { - mimeType: "fluid/view", - status: 200, - value: viewForItem, - }; - } - } - - // If it's not a direct link to an item, then just do normal request handling. - return super.request(req); - } - - protected async initializingFirstTime() { - const storage = await SpacesStorage.getFactory().createChildInstance(this.context); - this.root.set(storageKey, storage.handle); - const loader = await ExternalComponentLoader.getFactory().createChildInstance(this.context); - this.root.set(loaderKey, loader.handle); - } - - protected async hasInitialized() { - this.storage = await this.root.get>>(storageKey)?.get(); - this.loader = await this.root.get>(loaderKey)?.get(); - // We'll cache this async result on initialization, since we need it synchronously during render. - this.baseUrl = await this.context.getAbsoluteUrl(this.url); - } - - /** - * addComponent is handed down to the WaterParkToolbar as the callback when an option is selected from the list. - */ - private readonly addComponent = async (componentUrl: string) => { - if (this.loader === undefined) { - throw new Error("Can't add component, loader not found"); - } - if (this.storage === undefined) { - throw new Error("Can't add component, storage not found"); - } - - const component = await this.loader.createComponentFromUrl(componentUrl); - if (component.handle === undefined) { - throw new Error("Can't add, component must have a handle"); - } - this.storage.addItem({ - handle: component.handle, - }); - }; - - private readonly getViewForItem = async (item: IWaterparkItem) => { - const component = await item.handle.get(); - - // This is where Spaces would do a lookup for how to get the view and call that. - // In Waterpark, we'll just assume the handle points to something we can adapt with a ReactViewAdapter. - if (ReactViewAdapter.canAdapt(component)) { - return ; - } - - return undefined; - }; -} - -interface IWaterParkViewProps { - storage: SpacesStorage; - onSelectOption: (componentUrl: string) => Promise; - getViewForItem: (item: IWaterparkItem) => Promise; - getUrlForItem: (itemId: string) => string; -} - -export const WaterParkView: React.FC = (props: React.PropsWithChildren) => { - const [editable, setEditable] = React.useState(props.storage.itemList.size === 0); - return ( - <> - setEditable(!editable)} - /> - - - ); -}; diff --git a/examples/data-objects/shared-text/src/index.ts b/examples/data-objects/shared-text/src/index.ts index f408dbc8bf6e..5216e26c656a 100644 --- a/examples/data-objects/shared-text/src/index.ts +++ b/examples/data-objects/shared-text/src/index.ts @@ -12,6 +12,8 @@ import { IContainerContext, IRuntime, IRuntimeFactory } from "@fluidframework/co import { ContainerRuntime } from "@fluidframework/container-runtime"; import { IFluidDataStoreContext, + IFluidDataStoreFactory, + IFluidDataStoreRegistry, IProvideFluidDataStoreFactory, IProvideFluidDataStoreRegistry, NamedFluidDataStoreRegistryEntries, @@ -57,7 +59,7 @@ const defaultRegistryEntries: NamedFluidDataStoreRegistryEntries = [ ["@fluid-example/image-collection", images.then((m) => m.fluidExport)], ]; -class MyRegistry implements IProvideFluidDataStoreRegistry { +class MyRegistry implements IFluidDataStoreRegistry { constructor( private readonly context: IContainerContext, private readonly defaultRegistry: string) { @@ -82,7 +84,7 @@ class MyRegistry implements IProvideFluidDataStoreRegistry { } } -class SharedTextFactoryComponent implements IProvideFluidDataStoreFactory, IRuntimeFactory { +class SharedTextFactoryComponent implements IFluidDataStoreFactory, IRuntimeFactory { public static readonly type = "@fluid-example/shared-text"; public readonly type = SharedTextFactoryComponent.type; diff --git a/examples/data-objects/vltava/src/fluidObjects/anchor/anchor.ts b/examples/data-objects/vltava/src/fluidObjects/anchor/anchor.ts index 9826d035d4f1..9588a3a74213 100644 --- a/examples/data-objects/vltava/src/fluidObjects/anchor/anchor.ts +++ b/examples/data-objects/vltava/src/fluidObjects/anchor/anchor.ts @@ -47,7 +47,7 @@ export class Anchor extends DataObject implements IProvideFluidHTMLView, IProvid } protected async initializingFirstTime() { - const defaultFluidObject = await Vltava.getFactory().createRootInstance(this.context.containerRuntime); + const defaultFluidObject = await Vltava.getFactory().createInstance(this.context.containerRuntime); this.root.set(this.defaultFluidObjectId, defaultFluidObject.handle); const lastEditedFluidObject = await LastEditedTrackerDataObject.getFactory().createChildInstance(this.context); diff --git a/packages/framework/aqueduct/src/data-object-factories/pureDataObjectFactory.ts b/packages/framework/aqueduct/src/data-object-factories/pureDataObjectFactory.ts index 92c251312353..de5b67dc1a58 100644 --- a/packages/framework/aqueduct/src/data-object-factories/pureDataObjectFactory.ts +++ b/packages/framework/aqueduct/src/data-object-factories/pureDataObjectFactory.ts @@ -266,16 +266,16 @@ export class PureDataObjectFactory, P, S> } /** - * Creates a new instance of the object. Uses container's registry (root) to find this factory. + * Creates a new instance of the object. Uses container's registry to find this factory. * It's expected that only container owners would use this functionality, as only such developers - * have knowledge of root entries in container registry. + * have knowledge of entries in container registry. * The name in this registry for such record should match type of this factory. * @param runtime - container runtime. It's registry is used to create an object. * @param initialState - The initial state to provide to the created component. * @returns an object created by this factory. Data store and objects created are not attached to container. * They get attached only when a handle to one of them is attached to already attached objects. */ - public async createRootInstance( + public async createInstance( runtime: IContainerRuntimeBase, initialState?: S, ): Promise { diff --git a/packages/test/end-to-end-tests/src/test/fluidObjectHandle.spec.ts b/packages/test/end-to-end-tests/src/test/fluidObjectHandle.spec.ts index e17bb159e050..62d92b082986 100644 --- a/packages/test/end-to-end-tests/src/test/fluidObjectHandle.spec.ts +++ b/packages/test/end-to-end-tests/src/test/fluidObjectHandle.spec.ts @@ -85,7 +85,7 @@ describe("FluidObjectHandle", () => { // Create a Container for the first client. const firstContainer = await createContainer(); firstContainerObject1 = await requestFluidObject(firstContainer, "default"); - firstContainerObject2 = await testSharedDataObjectFactory.createRootInstance( + firstContainerObject2 = await testSharedDataObjectFactory.createInstance( firstContainerObject1._context.containerRuntime); // Load the Container that was created by the first client.