diff --git a/packages/test/local-server-tests/package.json b/packages/test/local-server-tests/package.json index 47f8c7a16e4a..83373fd2d0e0 100644 --- a/packages/test/local-server-tests/package.json +++ b/packages/test/local-server-tests/package.json @@ -30,7 +30,7 @@ "lint:fix": "fluid-build . --task eslint:fix --task format", "test": "npm run test:mocha", "test:coverage": "c8 npm test", - "test:mocha": "mocha \"dist/test/**/*.spec.*js\" --exit", + "test:mocha": "mocha \"lib/test/**/*.spec.*js\" --exit", "test:mocha:verbose": "cross-env FLUID_TEST_VERBOSE=1 npm run test:mocha" }, "c8": { @@ -53,8 +53,7 @@ ], "temp-directory": "nyc/.nyc_output" }, - "devDependencies": { - "@biomejs/biome": "~1.8.3", + "dependencies": { "@fluid-internal/client-utils": "workspace:~", "@fluid-internal/mocha-test-setup": "workspace:~", "@fluid-private/test-loader-utils": "workspace:~", @@ -91,7 +90,10 @@ "@fluidframework/server-local-server": "^5.0.0", "@fluidframework/shared-object-base": "workspace:~", "@fluidframework/telemetry-utils": "workspace:~", - "@fluidframework/test-utils": "workspace:~", + "@fluidframework/test-utils": "workspace:~" + }, + "devDependencies": { + "@biomejs/biome": "~1.8.3", "@types/mocha": "^9.1.1", "@types/nock": "^9.3.0", "@types/node": "^18.19.0", diff --git a/packages/test/local-server-tests/src/test/decoupledCreate.spec.ts b/packages/test/local-server-tests/src/test/decoupledCreate.spec.ts new file mode 100644 index 000000000000..f2293fa96878 --- /dev/null +++ b/packages/test/local-server-tests/src/test/decoupledCreate.spec.ts @@ -0,0 +1,128 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import { strict as assert } from "assert"; + +import type { IRequest } from "@fluidframework/core-interfaces"; +import type { FluidObject } from "@fluidframework/core-interfaces/internal"; +import { + IDocumentServiceFactory, + type IResolvedUrl, + type ISummaryTree, +} from "@fluidframework/driver-definitions/internal"; +import { + LocalDocumentServiceFactory, + LocalResolver, +} from "@fluidframework/local-driver/internal"; +import { + LocalDeltaConnectionServer, + type ILocalDeltaConnectionServer, +} from "@fluidframework/server-local-server"; +import type { ITestFluidObject } from "@fluidframework/test-utils/internal"; + +import { createLoader } from "../utils.js"; + +function createDSFWithOutOfBandCreate({ + deltaConnectionServer, + createContainerCallback, +}: { + deltaConnectionServer: ILocalDeltaConnectionServer; + createContainerCallback: ( + summary: ISummaryTree | undefined, + resolvedUrl: IResolvedUrl, + ) => Promise; +}) { + return new Proxy( + new LocalDocumentServiceFactory(deltaConnectionServer), + { + get: (t, p: keyof IDocumentServiceFactory, r) => { + if (p === "createContainer") { + return async (summary, resolvedUrl, logger, clientIsSummarizer) => { + const url = await createContainerCallback(summary, resolvedUrl); + // this is more like the load flow, where we resolve the url + // and create the document service, and it works here, as + // the callback actually does the work of creating the container. + const resolver = new LocalResolver(); + return t.createDocumentService( + await resolver.resolve(url), + logger, + clientIsSummarizer, + ); + }; + } + + return Reflect.get(t, p, r); + }, + }, + ); +} + +async function createContainerOutOfBand( + deltaConnectionServer: ILocalDeltaConnectionServer, + createContainerParams: { + summary: ISummaryTree | undefined; + resolvedUrl: IResolvedUrl; + }, +) { + // this actually creates the container + const { summary, resolvedUrl } = createContainerParams; + const documentServiceFactory = new LocalDocumentServiceFactory(deltaConnectionServer); + const documentService = await documentServiceFactory.createContainer(summary, resolvedUrl); + const resolver = new LocalResolver(); + return resolver.getAbsoluteUrl(documentService.resolvedUrl, ""); +} + +describe("Scenario Test", () => { + it("Create container via a decoupled out of band function and validate both attaching container and freshly loaded container both work.", async () => { + const deltaConnectionServer = LocalDeltaConnectionServer.create(); + + /* + * Setup a document service factory that uses a user specifiable createContainerCallback. + * This callback could make a different server call, and just needs to return the url + * of the newly created container/file. + */ + let request: IRequest | undefined; + const documentServiceFactory = createDSFWithOutOfBandCreate({ + deltaConnectionServer, + createContainerCallback: async (summary, resolvedUrl) => + (request = { + url: await createContainerOutOfBand(deltaConnectionServer, { + summary, + resolvedUrl, + }), + }), + }); + + const { loader, codeDetails, urlResolver } = createLoader({ + deltaConnectionServer, + documentServiceFactory, + }); + + const container = await loader.createDetachedContainer(codeDetails); + + { + // put a bit of data in the detached container so we can validate later + const entryPoint: FluidObject = await container.getEntryPoint(); + entryPoint.ITestFluidObject?.root.set("someKey", "someValue"); + } + + // kicking off attach will end up calling the create container callback + // which will actually create the container, and eventually finish the attach + await container.attach(urlResolver.createCreateNewRequest("test")); + + { + // just reuse the same server, nothing else from the initial create + const { loader: loader2 } = createLoader({ deltaConnectionServer }); + + // ensure and use the url we got from out of band create to load the container + assert(request !== undefined); + const container2 = await loader2.resolve(request); + + // ensure the newly loaded container has the data we expect. + const entryPoint: FluidObject = await container2.getEntryPoint(); + assert.strictEqual(entryPoint.ITestFluidObject?.root.get("someKey"), "someValue"); + } + }); +}); diff --git a/packages/test/local-server-tests/src/test/tsconfig.json b/packages/test/local-server-tests/src/test/tsconfig.json index 08301ad12bea..9a8f91284a62 100644 --- a/packages/test/local-server-tests/src/test/tsconfig.json +++ b/packages/test/local-server-tests/src/test/tsconfig.json @@ -1,11 +1,10 @@ { "extends": "../../../../../common/build/build-common/tsconfig.test.node16.json", "compilerOptions": { - "rootDir": "./", - "outDir": "../../dist/test", + "rootDir": "../", + "outDir": "../../lib", "types": ["mocha"], "noUncheckedIndexedAccess": false, "exactOptionalPropertyTypes": false, }, - "include": ["./**/*"], } diff --git a/packages/test/local-server-tests/src/utils.ts b/packages/test/local-server-tests/src/utils.ts new file mode 100644 index 000000000000..78efee473a68 --- /dev/null +++ b/packages/test/local-server-tests/src/utils.ts @@ -0,0 +1,108 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import { ContainerRuntimeFactoryWithDefaultDataStore } from "@fluidframework/aqueduct/internal"; +import { + type IFluidCodeDetails, + type IHostLoader, + type ILoaderOptions, + type IRuntimeFactory, + ICodeDetailsLoader, +} from "@fluidframework/container-definitions/internal"; +import { Loader } from "@fluidframework/container-loader/internal"; +import type { + IDocumentServiceFactory, + IUrlResolver, +} from "@fluidframework/driver-definitions/internal"; +import { + LocalDocumentServiceFactory, + LocalResolver, +} from "@fluidframework/local-driver/internal"; +import { SharedMap } from "@fluidframework/map/internal"; +import type { IFluidDataStoreFactory } from "@fluidframework/runtime-definitions/internal"; +import { ILocalDeltaConnectionServer } from "@fluidframework/server-local-server"; +import { TestFluidObjectFactory, LocalCodeLoader } from "@fluidframework/test-utils/internal"; + +/** + * This allows the input object to be general, + * and the default object to be specific, + * which maintains strong typing for both inputs, and the defaults in the result. + * So if a user specifies a value, that values type will be strongly specified on the Result. + * However if the user does not specify an option input, the result will also get a strong + * type based the default. + */ +export type OptionalToDefault = { + [P in keyof TDefault]: P extends keyof TInput + ? Exclude extends never + ? TDefault[P] + : TInput[P] + : TDefault[P]; +}; + +export interface CreateLoaderParams { + deltaConnectionServer: ILocalDeltaConnectionServer; + codeDetails?: IFluidCodeDetails; + defaultDataStoreFactory?: IFluidDataStoreFactory; + runtimeFactory?: IRuntimeFactory; + codeLoader?: ICodeDetailsLoader; + documentServiceFactory?: IDocumentServiceFactory; + urlResolver?: IUrlResolver; + options?: ILoaderOptions; +} + +export interface CreateLoaderDefaultResults + extends Required> { + documentServiceFactory: LocalDocumentServiceFactory; + urlResolver: LocalResolver; + codeLoader: LocalCodeLoader; + defaultDataStoreFactory: TestFluidObjectFactory; + runtimeFactory: ContainerRuntimeFactoryWithDefaultDataStore; + loader: IHostLoader; +} + +export function createLoader( + opts: T, +): OptionalToDefault { + const deltaConnectionServer = opts.deltaConnectionServer; + const documentServiceFactory = + opts.documentServiceFactory ?? new LocalDocumentServiceFactory(deltaConnectionServer); + + const urlResolver = opts.urlResolver ?? new LocalResolver(); + + const defaultDataStoreFactory = + opts.defaultDataStoreFactory ?? + new TestFluidObjectFactory([["map", SharedMap.getFactory()]], "default"); + + const runtimeFactory = + opts.runtimeFactory ?? + new ContainerRuntimeFactoryWithDefaultDataStore({ + defaultFactory: defaultDataStoreFactory, + registryEntries: [ + [defaultDataStoreFactory.type, Promise.resolve(defaultDataStoreFactory)], + ], + }); + + const codeDetails = opts.codeDetails ?? { package: "test" }; + + const codeLoader = opts.codeLoader ?? new LocalCodeLoader([[codeDetails, runtimeFactory]]); + + const loader = new Loader({ + codeLoader, + documentServiceFactory, + urlResolver, + }); + + const rtn: OptionalToDefault = { + deltaConnectionServer, + documentServiceFactory, + urlResolver, + codeDetails, + defaultDataStoreFactory, + runtimeFactory, + codeLoader, + loader, + }; + return rtn as OptionalToDefault; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a1f7bc048a97..5e4b0a8a14e9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13491,10 +13491,7 @@ importers: version: 5.1.4(webpack-bundle-analyzer@4.10.1)(webpack@5.94.0) packages/test/local-server-tests: - devDependencies: - '@biomejs/biome': - specifier: ~1.8.3 - version: 1.8.3 + dependencies: '@fluid-internal/client-utils': specifier: workspace:~ version: link:../../common/client-utils @@ -13606,6 +13603,10 @@ importers: '@fluidframework/test-utils': specifier: workspace:~ version: link:../test-utils + devDependencies: + '@biomejs/biome': + specifier: ~1.8.3 + version: 1.8.3 '@types/mocha': specifier: ^9.1.1 version: 9.1.1