Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,11 @@ export class DiceRollerFactory implements IFluidDataStoreFactory {
context: IFluidDataStoreContext,
existing: boolean,
): Promise<IFluidDataStoreChannel> {
// This is the goo from the runtime.
let map: ISharedMap;

const provideEntryPoint = async (entryPointRuntime: IFluidDataStoreRuntime) => {
const map = (await entryPointRuntime.getChannel(mapId)) as ISharedMap;
return new DiceRoller(map);
return diceRoller;
};

const runtime: FluidDataStoreRuntime = new FluidDataStoreRuntime(
Expand All @@ -83,12 +85,18 @@ export class DiceRollerFactory implements IFluidDataStoreFactory {
provideEntryPoint,
);

if (!existing) {
const map = runtime.createChannel(mapId, mapFactory.type) as ISharedMap;
if (existing) {
map = (await runtime.getChannel(mapId)) as ISharedMap;
} else {
map = runtime.createChannel(mapId, mapFactory.type) as ISharedMap;
map.set(diceValueKey, 1);
map.bindToContext();
}

// The EntryPoint
const diceRoller = new DiceRoller(map);

// return [runtime, diceRoller];
return runtime;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,9 @@ export {
MigrationDataObjectFactory,
type MigrationDataObjectFactoryProps,
} from "./migrationDataObjectFactory.js";
export {
MultiFormatDataStoreFactory,
type MultiFormatDataStoreFactoryProps,
type MultiFormatModelDescriptor,
} from "./multiFormatDataStoreFactory.js";
export { pureDataObjectModelDescriptor } from "./pureDataObjectModelDescriptor.js";
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
/* eslint-disable import/no-internal-modules -- //* TEMP */
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/

import type { FluidObject } from "@fluidframework/core-interfaces";
import { assert } from "@fluidframework/core-utils/internal";
import { FluidDataStoreRuntime } from "@fluidframework/datastore/internal";
import type {
IFluidDataStoreRuntime,
IChannelFactory,
} from "@fluidframework/datastore-definitions/internal";
import type {
IFluidDataStoreFactory,
IFluidDataStoreContext,
IFluidDataStorePolicies,
IFluidDataStoreChannel,
} from "@fluidframework/runtime-definitions/internal";

import type { DataObjectTypes, IDataObjectProps } from "../data-objects/types.js";

import type { DataObjectFactoryProps } from "./pureDataObjectFactory.js";

//* TODO: Do a thorough pass over PureDataObjectFactory and MultiFormatDataStoreFactory to ensure feature parity
//* e.g. MFDSF is missing the Runtime mixin
//* The idea is that if you use a single pureDataObjectModelDescriptor with this Factory, it's equivalent to PureDataObjectFactory

/**
* Descriptor that supplies (or can create) a model format within a multi-format data store.
*
* - `create`: Called only for the first descriptor when newly created (i.e. !existing) to establish initial DDSes/schema.
* - `probe`: Used when loading an existing data store; first descriptor whose probe returns true is selected.
* - `get`: Returns (or produces) the entry point Fluid object after selection.
*/
export interface MultiFormatModelDescriptor<
TEntryPoint extends FluidObject = FluidObject,
I extends DataObjectTypes = DataObjectTypes,
> {
/**
* Initialize a brand-new data store to this format (only invoked on descriptor[0] when !existing).
*/
create(props: IDataObjectProps<I>): Promise<void>; //* Would be nice to be able to have it synchronous?
/**
* Return true if this descriptor's format matches the persisted contents of the runtime.
*/
probe(runtime: IFluidDataStoreRuntime): Promise<boolean> | boolean;
/**
* Provide the entry point object for this model.
*/
get(props: IDataObjectProps<I>): Promise<TEntryPoint> | TEntryPoint;
/**
* Shared object (DDS) factories required specifically for this model format. Multiple descriptors can
* contribute shared objects; duplicates (by type) are ignored with first-wins semantics.
*
* @remarks
* It's recommended to use delay-loaded factories for DDSes only needed for a specific format (esp old formats)
*/
readonly sharedObjects?: readonly IChannelFactory[];
}

/**
* Parameter object accepted by `MultiFormatDataStoreFactory` constructor (mirrors the style of
* `DataObjectFactoryProps` while focusing only on multi-format aspects for now).
*/
export interface MultiFormatDataStoreFactoryProps<I extends DataObjectTypes = DataObjectTypes>
extends Omit<DataObjectFactoryProps<never, I>, "ctor" | "sharedObjects"> {
/**
* Ordered list of model descriptors (first used for creation; probed in order for existing).
*/
readonly modelDescriptors: readonly MultiFormatModelDescriptor[];
}

/**
* A minimal multi-format data store factory.
*
* It defers format selection until the entry point is requested. For an existing data store it runs the
* supplied descriptors' `probe` functions in order and picks the first that matches. For a new data store
* it eagerly invokes `create` on the first descriptor (if present) and then uses that descriptor.
*
* Future work: accept a richer set of options similar to `DataObjectFactoryProps`.
*/
export class MultiFormatDataStoreFactory implements IFluidDataStoreFactory {
public readonly type: string;

private readonly descriptors: readonly MultiFormatModelDescriptor[];
private readonly sharedObjectRegistry: Map<string, IChannelFactory>;
private readonly runtimeClass: typeof FluidDataStoreRuntime;
private readonly policies?: Partial<IFluidDataStorePolicies>;
private readonly optionalProviders?: FluidObject; //* TODO: Figure out how to express this

public constructor(props: MultiFormatDataStoreFactoryProps) {
const { type, modelDescriptors, runtimeClass, policies, optionalProviders } = props;
if (type === "") {
throw new Error("type must be a non-empty string");
}
if (modelDescriptors.length === 0) {
throw new Error("At least one model descriptor must be supplied");
}
this.type = type;
this.descriptors = modelDescriptors;
this.runtimeClass = runtimeClass ?? FluidDataStoreRuntime;
this.policies = policies;
this.optionalProviders = optionalProviders;
// Build combined shared object registry (first descriptor wins on duplicates)
this.sharedObjectRegistry = new Map();
for (const d of modelDescriptors) {
for (const so of d.sharedObjects ?? []) {
//* BEWARE: collisions could be theoretically possible. Maybe via configuredSharedTree
if (!this.sharedObjectRegistry.has(so.type)) {
this.sharedObjectRegistry.set(so.type, so);
}
}
}
}

// Provider pattern convenience (mirrors other factories in the codebase)
public get IFluidDataStoreFactory(): this {
return this;
}

public async instantiateDataStore(
context: IFluidDataStoreContext,
existing: boolean,
): Promise<IFluidDataStoreChannel> {
let selected: MultiFormatModelDescriptor | undefined; // chosen descriptor (per-instance)
const provideEntryPoint = async (rt: IFluidDataStoreRuntime): Promise<FluidObject> => {
// Select descriptor lazily when entry point requested.
if (selected !== undefined) {
// Already selected for this runtime; return its entry point immediately.
return selected.get({
context,
runtime: rt,
providers: this.optionalProviders ?? {},
initProps: {}, //* TODO: Plumb this through
});
}

if (existing) {
for (const d of this.descriptors) {
try {
//* Await ensureFactoriesLoaded for the descriptor first to support delay-loading
const match = await d.probe(rt);
if (match) {
selected = d;
break;
}
} catch {
// Swallow probe errors and continue trying other descriptors.
}
}
} else {
// New data store path: use first descriptor
selected = this.descriptors[0];
}
//* TODO: Switch to an error with errorType.
assert(selected !== undefined, "Should have found a model selector");

//* TODO: Switch probe style to return the object directly rather than a boolean followed by this .get call?
return selected.get({
context,
runtime: rt,
providers: this.optionalProviders ?? {},
initProps: {}, //* TODO: Plumb this through
});
};

const runtime = new this.runtimeClass(
context,
this.sharedObjectRegistry,
existing,
provideEntryPoint,
this.policies, //* TODO: How do we union these?
);

// For a new data store, initialize using the first descriptor before returning the runtime.
if (!existing) {
const first = this.descriptors[0];
//* TODO: Update the type to express that there's at least one
if (first === undefined) {
throw new Error("Invariant: descriptors array unexpectedly empty");
}
await first.create({
context,
runtime,
providers: this.optionalProviders ?? {},
initProps: {}, //* TODO: Plumb this through
});
}

return runtime;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/

import type { FluidObject } from "@fluidframework/core-interfaces";
import type {
IChannelFactory,
IFluidDataStoreRuntime,
} from "@fluidframework/datastore-definitions/internal";

import type {
PureDataObject,
DataObjectTypes,
IDataObjectProps,
} from "../data-objects/index.js";

import type { MultiFormatModelDescriptor } from "./multiFormatDataStoreFactory.js";

/**
* Creates a {@link MultiFormatModelDescriptor} for a {@link PureDataObject} ctor.
*
* When supplied as the sole descriptor to a {@link MultiFormatDataStoreFactory} the resulting data store behaves
* equivalently (for typical usage) to one produced by {@link PureDataObjectFactory} for the same `ctor` & shared objects:
*
* - New data store creation eagerly constructs the object and runs its first-time initialization before attachment.
* - Loading an existing data store constructs the object lazily when the entry point is first requested.
* - Subsequent `get` calls return the same instance.
*/
export function pureDataObjectModelDescriptor<
TObj extends PureDataObject<I> & FluidObject,
I extends DataObjectTypes = DataObjectTypes,
>(
ctor: new (props: IDataObjectProps<I>) => TObj,
sharedObjects?: readonly IChannelFactory[],
): MultiFormatModelDescriptor<TObj, I> {
// Map runtime => instantiated data object. Each runtime instance corresponds to one data store instance.
const instances = new WeakMap<IFluidDataStoreRuntime, TObj>();

return {
async create(props: IDataObjectProps<I>): Promise<void> {
const instance = new ctor(props);
instances.set(props.runtime, instance);
// For new data stores run first-time initialization before attachment (mirrors PureDataObjectFactory behavior).
await instance.finishInitialization(false);
},
// Single-format helpers can always report a positive probe. If multiple descriptors are used callers should
// provide a more selective probe implementation.
probe(): boolean {
return true;
},
async get(props: IDataObjectProps<I>): Promise<TObj> {
let instance = instances.get(props.runtime);
if (instance === undefined) {
// Existing data store path: lazily construct & complete existing initialization on first access.
instance = new ctor(props);
instances.set(props.runtime, instance);
await instance.finishInitialization(true);
}
return instance;
},
sharedObjects,
};
}
1 change: 1 addition & 0 deletions packages/framework/aqueduct/src/data-objects/dataObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export abstract class DataObject<
* Caller is responsible for ensuring this is only invoked once.
*/
public override async initializeInternal(existing: boolean): Promise<void> {
//* TODO: Reimplement in terms of intialize primitives and let super.initializeInternal do the stitching together
if (existing) {
// data store has a root directory so we just need to set it before calling initializingFromExisting
this.internalRoot = (await this.runtime.getChannel(
Expand Down
Loading