From aba3a1a571852cc823ae4e2afa84230e74259727 Mon Sep 17 00:00:00 2001 From: Tommy Brosman Date: Fri, 26 Jun 2026 16:12:25 -0700 Subject: [PATCH 1/3] Initial version of the summaryDelayLoadedModule polyfill. --- .../summary/summaryDelayLoadedModuleStub.ts | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 packages/runtime/container-runtime/src/summary/summaryDelayLoadedModuleStub.ts diff --git a/packages/runtime/container-runtime/src/summary/summaryDelayLoadedModuleStub.ts b/packages/runtime/container-runtime/src/summary/summaryDelayLoadedModuleStub.ts new file mode 100644 index 000000000000..2b188a6057cf --- /dev/null +++ b/packages/runtime/container-runtime/src/summary/summaryDelayLoadedModuleStub.ts @@ -0,0 +1,68 @@ +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + */ + +/* eslint-disable @typescript-eslint/no-extraneous-class -- Stub classes */ + +// Stub for Fluid container-runtime's delay-loaded summarizer module +// (`./summary/summaryDelayLoadedModule/index.js`, the "summarizerDelayLoadedModule" chunk). +// +// This stub is for clients that do not use the summarizer (for example, when summarization +// happens server-side). The summarizer is a large graph of modules that is not needed in the single-file +// bundle for such clients. The stub is used in the single-file bundle instead of the real summarizer +// module, which is replaced via NormalModuleReplacementPlugin in the webpack config. +// +// The stub re-exports all symbols that the real summarizer module exports, but all of them throw +// an error when instantiated. This ensures that if any code path reaches this stub, it fails fast +// and clearly indicates that the summarizer is unavailable in this client. +// +// Example Webpack rule implementing the replacement after summaryDelayLoadedModuleStub.ts is copied +// into the src/polyfills/ directory of the application: +// ```ts +// new webpack.NormalModuleReplacementPlugin( +// /summaryDelayLoadedModule[\\/]index\.js$/, +// path.resolve(__dirname, "src/polyfills/summaryDelayLoadedModuleStub.ts"), +// ), +// ``` +const unavailable = (name: string): never => { + throw new Error( + `${name} is unavailable: the summaryDelayLoadedModule chunk was stubbed out of the bundle.`, + ); +}; + +export class Summarizer { + public constructor() { + unavailable("Summarizer"); + } +} + +export class RunWhileConnectedCoordinator { + public constructor() { + unavailable("RunWhileConnectedCoordinator"); + } +} + +export class RunningSummarizer { + public constructor() { + unavailable("RunningSummarizer"); + } +} + +export class SummarizeHeuristicData { + public constructor() { + unavailable("SummarizeHeuristicData"); + } +} + +export class SummarizeHeuristicRunner { + public constructor() { + unavailable("SummarizeHeuristicRunner"); + } +} + +export const defaultMaxAttempts = 2; +export const defaultMaxAttemptsForSubmitFailures = 5; +export const neverCancelledSummaryToken = Object.freeze({ + cancelled: false as const, + waitCancelled: new Promise(() => {}), +}); From ac379011146da4039b681a4babfd18a1d06b1026 Mon Sep 17 00:00:00 2001 From: Tommy Brosman Date: Fri, 26 Jun 2026 17:20:50 -0700 Subject: [PATCH 2/3] - Type test against the real module - Lint fix --- .../summary/summaryDelayLoadedModuleStub.ts | 40 ++++++------ .../summaryDelayLoadedModuleStub.spec.ts | 62 +++++++++++++++++++ 2 files changed, 82 insertions(+), 20 deletions(-) create mode 100644 packages/runtime/container-runtime/src/test/summary/summaryDelayLoadedModuleStub.spec.ts diff --git a/packages/runtime/container-runtime/src/summary/summaryDelayLoadedModuleStub.ts b/packages/runtime/container-runtime/src/summary/summaryDelayLoadedModuleStub.ts index 2b188a6057cf..9d8cbcfb6dec 100644 --- a/packages/runtime/container-runtime/src/summary/summaryDelayLoadedModuleStub.ts +++ b/packages/runtime/container-runtime/src/summary/summaryDelayLoadedModuleStub.ts @@ -25,44 +25,44 @@ // ), // ``` const unavailable = (name: string): never => { - throw new Error( - `${name} is unavailable: the summaryDelayLoadedModule chunk was stubbed out of the bundle.`, - ); + throw new Error( + `${name} is unavailable: the summaryDelayLoadedModule chunk was stubbed out of the bundle.`, + ); }; export class Summarizer { - public constructor() { - unavailable("Summarizer"); - } + public constructor() { + unavailable("Summarizer"); + } } export class RunWhileConnectedCoordinator { - public constructor() { - unavailable("RunWhileConnectedCoordinator"); - } + public constructor() { + unavailable("RunWhileConnectedCoordinator"); + } } export class RunningSummarizer { - public constructor() { - unavailable("RunningSummarizer"); - } + public constructor() { + unavailable("RunningSummarizer"); + } } export class SummarizeHeuristicData { - public constructor() { - unavailable("SummarizeHeuristicData"); - } + public constructor() { + unavailable("SummarizeHeuristicData"); + } } export class SummarizeHeuristicRunner { - public constructor() { - unavailable("SummarizeHeuristicRunner"); - } + public constructor() { + unavailable("SummarizeHeuristicRunner"); + } } export const defaultMaxAttempts = 2; export const defaultMaxAttemptsForSubmitFailures = 5; export const neverCancelledSummaryToken = Object.freeze({ - cancelled: false as const, - waitCancelled: new Promise(() => {}), + cancelled: false as const, + waitCancelled: new Promise(() => {}), }); diff --git a/packages/runtime/container-runtime/src/test/summary/summaryDelayLoadedModuleStub.spec.ts b/packages/runtime/container-runtime/src/test/summary/summaryDelayLoadedModuleStub.spec.ts new file mode 100644 index 000000000000..f7ce4b0fe8dc --- /dev/null +++ b/packages/runtime/container-runtime/src/test/summary/summaryDelayLoadedModuleStub.spec.ts @@ -0,0 +1,62 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import type { requireAssignableTo } from "@fluidframework/build-tools"; + +// eslint-disable-next-line import-x/no-internal-modules -- the test deliberately compares the stub against the real module that it stands in for +import type * as real from "../../summary/summaryDelayLoadedModule/index.js"; +// eslint-disable-next-line import-x/no-internal-modules -- the stub is not part of the package's public surface and is imported directly under test +import * as stub from "../../summary/summaryDelayLoadedModuleStub.js"; + +/* + * Compile-time type tests (leveraging `requireAssignableTo` from the type-test infrastructure) that + * assert the shape of `summaryDelayLoadedModuleStub` stays in sync with the real delay-loaded + * summarizer module (`./summary/summaryDelayLoadedModule/index.js`) that it stands in for. + * + * The stub is swapped in for the real module (via webpack's NormalModuleReplacementPlugin) in + * single-file bundles that do not use the summarizer. For that swap to be safe, the stub must + * re-export exactly the same runtime (value) symbols as the real module. If the real module gains + * or loses a runtime export and the stub is not updated to match, the assertions below fail to + * compile, flagging that the stub needs to be updated. + * + * Purely type-only exports of the real module (e.g. `ISummarizeResults`) are intentionally not + * covered here: they are erased at runtime and so do not need to be stubbed, and `keyof typeof` + * only includes value-side exports anyway. + */ + +/** + * Every runtime export of the real module must also be exported by the stub. If this fails, the + * stub is missing an export that the real module provides, which would break a stubbed bundle. + */ +declare type _stubExportsAllRealValueSymbols = requireAssignableTo< + keyof typeof real, + keyof typeof stub +>; + +/** + * The stub must not export anything the real module does not. If this fails, the stub has a stray + * export to remove (or the real module dropped an export that the stub still references). + */ +declare type _stubExportsNothingExtra = requireAssignableTo< + keyof typeof stub, + keyof typeof real +>; + +/* + * The non-class (constant) exports must stay type-compatible with the real module so the stub + * remains a valid stand-in for any code that reads these values. + */ +declare type _defaultMaxAttempts = requireAssignableTo< + typeof stub.defaultMaxAttempts, + typeof real.defaultMaxAttempts +>; +declare type _defaultMaxAttemptsForSubmitFailures = requireAssignableTo< + typeof stub.defaultMaxAttemptsForSubmitFailures, + typeof real.defaultMaxAttemptsForSubmitFailures +>; +declare type _neverCancelledSummaryToken = requireAssignableTo< + typeof stub.neverCancelledSummaryToken, + typeof real.neverCancelledSummaryToken +>; From b9c25b4f70c5b436e83c048224a593ad2c25bad0 Mon Sep 17 00:00:00 2001 From: Tommy Brosman Date: Mon, 29 Jun 2026 09:23:11 -0700 Subject: [PATCH 3/3] Fixed copyright header. Claude did not get it right. --- .../src/summary/summaryDelayLoadedModuleStub.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/runtime/container-runtime/src/summary/summaryDelayLoadedModuleStub.ts b/packages/runtime/container-runtime/src/summary/summaryDelayLoadedModuleStub.ts index 9d8cbcfb6dec..f79b410ddcbd 100644 --- a/packages/runtime/container-runtime/src/summary/summaryDelayLoadedModuleStub.ts +++ b/packages/runtime/container-runtime/src/summary/summaryDelayLoadedModuleStub.ts @@ -1,5 +1,6 @@ /*! - * Copyright (c) Microsoft Corporation. All rights reserved. + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. */ /* eslint-disable @typescript-eslint/no-extraneous-class -- Stub classes */