feat(container-loader): introduce effection-based structured concurrency#26421
feat(container-loader): introduce effection-based structured concurrency#26421tylerbutler wants to merge 7 commits intomicrosoft:mainfrom
Conversation
Add EffectionScope, EffectionTimer, and bridge utilities (createScopedAbortController, createScopedDelay) to container-loader. Integrate scoped lifecycle management into ConnectionManager, DeltaManager, and Container so that AbortControllers and async delays are automatically cancelled when components are disposed. - ConnectionManager: scoped AbortController for connectCore(), scoped delays replace raw setTimeout in retry/reconnect paths - DeltaManager: scoped AbortControllers eliminate manual signal chaining in getDeltas(), scope cleanup on close/dispose - Container: scope-based safety-net cleanup for DOM visibility listener - Fix generator double-wrapping type error in both container-loader and container-runtime copies of EffectionScope
There was a problem hiding this comment.
Pull request overview
Introduces an Effection-based structured concurrency layer into @fluidframework/container-loader to make async operations lifecycle-scoped and automatically cancellable, and wires it into connection/retry and delta-fetch paths.
Changes:
- Add
EffectionScope/EffectionTimerplus AbortSignal/timeout bridge utilities, and use them inConnectionManager,DeltaManager, andContainerlifecycle cleanup. - Update DeltaManager abort telemetry expectations to match new abort reasons.
- Add several new analysis/docs and Serena tool configuration files.
Reviewed changes
Copilot reviewed 16 out of 17 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| pnpm-lock.yaml | Adds effection@4.0.2 to the lockfile. |
| packages/loader/container-loader/package.json | Declares effection dependency for container-loader. |
| packages/loader/container-loader/src/structuredConcurrency.ts | New Effection wrappers + scoped AbortController/delay helpers. |
| packages/loader/container-loader/src/connectionManager.ts | Uses scoped AbortController for connect attempts; scoped delays for retry/backoff waits. |
| packages/loader/container-loader/src/deltaManager.ts | Uses Effection scope for abort propagation; scoped AbortController in getDeltas(). |
| packages/loader/container-loader/src/container.ts | Adds scope-based “safety net” cleanup for DOM visibility listener. |
| packages/loader/container-loader/src/test/deltaManager.spec.ts | Updates expected abort reason/error strings. |
| packages/runtime/container-runtime/src/structuredConcurrency.ts | Adds a container-runtime copy of Effection wrappers. |
| async-structured-concurrency-analysis.md | New repo-root analysis document. |
| build-tools/async-structured-concurrency-analysis.md | New build-tools analysis document. |
| .serena/project.yml | New Serena project config metadata. |
| .serena/memories/task_completion.md | New Serena memory file. |
| .serena/memories/suggested_commands.md | New Serena memory file. |
| .serena/memories/project_overview.md | New Serena memory file. |
| .serena/.gitignore | New gitignore for Serena cache. |
| build-tools/.serena/project.yml | New Serena config for build-tools project. |
| build-tools/.serena/.gitignore | New gitignore for Serena cache (build-tools). |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
Comments suppressed due to low confidence (1)
packages/loader/container-loader/src/connectionManager.ts:545
- connectCore() creates a scoped AbortController per connection attempt. With the current addCleanup() implementation, each attempt registers a cleanup task that persists until ConnectionManager.dispose(). If reconnect happens many times, this can accumulate. Consider ensuring per-attempt scoped controllers are unregistered/halted once the attempt completes.
const abortController = createScopedAbortController(this.scope);
const abortSignal = abortController.signal;
this.pendingConnection = {
abort: (): void => {
abortController.abort();
},
connectionMode: requestedMode,
};
| import { | ||
| createScope, | ||
| ensure, | ||
| sleep, | ||
| suspend, | ||
| type Operation, | ||
| type Scope, | ||
| type Task, | ||
| } from "effection"; |
There was a problem hiding this comment.
This file imports "effection", but @fluidframework/container-runtime does not appear to declare an "effection" dependency (container-runtime/package.json has no entry). This will break builds for consumers that don’t have effection hoisted. Add effection to container-runtime’s dependencies (and update lockfile) or move this implementation to a package that already depends on effection.
| return new Promise<void>((resolve, reject) => { | ||
| const timeoutId = setTimeout(resolve, delayMs); | ||
| scope.addCleanup(() => { | ||
| clearTimeout(timeoutId); | ||
| reject(new Error("Delay cancelled by scope closure")); | ||
| }); |
There was a problem hiding this comment.
createScopedDelay() registers a scope cleanup but never unregisters it when the timeout resolves. In long-lived scopes with repeated retries this can retain many timeoutIds/closures until scope.close(), increasing memory usage. Consider implementing delay via an effection Task (sleep) that naturally halts on scope close, or ensure the cleanup is removed once the delay settles.
| return new Promise<void>((resolve, reject) => { | |
| const timeoutId = setTimeout(resolve, delayMs); | |
| scope.addCleanup(() => { | |
| clearTimeout(timeoutId); | |
| reject(new Error("Delay cancelled by scope closure")); | |
| }); | |
| const task = scope.run(function* () { | |
| yield* sleep(delayMs); | |
| }); | |
| return new Promise<void>((resolve, reject) => { | |
| task.then( | |
| () => { | |
| resolve(); | |
| }, | |
| (error) => { | |
| reject(error ?? new Error("Delay cancelled by scope closure")); | |
| }, | |
| ); |
| # Fluid Framework Client Packages: Async & Structured Concurrency Benefit Analysis | ||
|
|
||
| Analysis of packages in the client workspace, rated by async code density and potential benefit | ||
| from automatic resource handling using structured concurrency principles (e.g., the effection library). | ||
|
|
There was a problem hiding this comment.
This PR adds a large analysis document at repo root, but nothing in the PR description or code references it. If it’s intended to be kept, consider linking it from relevant documentation; otherwise it may be better to remove/move it to an appropriate docs location to avoid adding untracked artifacts to the repo.
| # the name by which the project can be referenced within Serena | ||
| project_name: "main" | ||
|
|
||
|
|
||
| # list of languages for which language servers are started; choose from: | ||
| # al bash clojure cpp csharp | ||
| # csharp_omnisharp dart elixir elm erlang | ||
| # fortran fsharp go groovy haskell | ||
| # java julia kotlin lua markdown | ||
| # matlab nix pascal perl php | ||
| # powershell python python_jedi r rego | ||
| # ruby ruby_solargraph rust scala swift |
There was a problem hiding this comment.
The newly added .serena configuration and memory files appear to be tool-specific workspace metadata, and there are no existing references to Serena elsewhere in the repo. Confirm these are intended to be committed; if not, please remove them (and/or add them to gitignore) to avoid repo churn from local tooling state.
| # the name by which the project can be referenced within Serena | ||
| project_name: "build-tools" | ||
|
|
||
|
|
||
| # list of languages for which language servers are started; choose from: | ||
| # al bash clojure cpp csharp | ||
| # csharp_omnisharp dart elixir elm erlang | ||
| # fortran fsharp go groovy haskell | ||
| # java julia kotlin lua markdown | ||
| # matlab nix pascal perl php | ||
| # powershell python python_jedi r rego | ||
| # ruby ruby_solargraph rust scala swift | ||
| # terraform toml typescript typescript_vts vue | ||
| # yaml zig | ||
| # (This list may be outdated. For the current list, see values of Language enum here: | ||
| # https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py | ||
| # For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.) | ||
| # Note: | ||
| # - For C, use cpp | ||
| # - For JavaScript, use typescript | ||
| # - For Free Pascal/Lazarus, use pascal | ||
| # Special requirements: | ||
| # Some languages require additional setup/installations. | ||
| # See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers | ||
| # When using multiple languages, the first language server that supports a given file will be used for that file. | ||
| # The first language is the default language and the respective language server will be used as a fallback. | ||
| # Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored. | ||
| languages: | ||
| - typescript | ||
|
|
||
| # the encoding used by text files in the project | ||
| # For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings | ||
| encoding: "utf-8" | ||
|
|
||
| # whether to use project's .gitignore files to ignore files | ||
| ignore_all_files_in_gitignore: true | ||
|
|
||
| # list of additional paths to ignore in all projects | ||
| # same syntax as gitignore, so you can use * and ** | ||
| ignored_paths: [] | ||
|
|
||
| # whether the project is in read-only mode | ||
| # If set to true, all editing tools will be disabled and attempts to use them will result in an error | ||
| # Added on 2025-04-18 | ||
| read_only: false | ||
|
|
||
| # list of tool names to exclude. We recommend not excluding any tools, see the readme for more details. | ||
| # Below is the complete list of tools for convenience. | ||
| # To make sure you have the latest list of tools, and to view their descriptions, | ||
| # execute `uv run scripts/print_tool_overview.py`. | ||
| # | ||
| # * `activate_project`: Activates a project by name. | ||
| # * `check_onboarding_performed`: Checks whether project onboarding was already performed. | ||
| # * `create_text_file`: Creates/overwrites a file in the project directory. | ||
| # * `delete_lines`: Deletes a range of lines within a file. | ||
| # * `delete_memory`: Deletes a memory from Serena's project-specific memory store. | ||
| # * `execute_shell_command`: Executes a shell command. | ||
| # * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced. | ||
| # * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type). | ||
| # * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type). | ||
| # * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes. | ||
| # * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file. | ||
| # * `initial_instructions`: Gets the initial instructions for the current project. | ||
| # Should only be used in settings where the system prompt cannot be set, | ||
| # e.g. in clients you have no control over, like Claude Desktop. | ||
| # * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol. | ||
| # * `insert_at_line`: Inserts content at a given line in a file. | ||
| # * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol. | ||
| # * `list_dir`: Lists files and directories in the given directory (optionally with recursion). | ||
| # * `list_memories`: Lists memories in Serena's project-specific memory store. | ||
| # * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building). | ||
| # * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context). | ||
| # * `read_file`: Reads a file within the project directory. | ||
| # * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store. | ||
| # * `remove_project`: Removes a project from the Serena configuration. | ||
| # * `replace_lines`: Replaces a range of lines within a file with new content. | ||
| # * `replace_symbol_body`: Replaces the full definition of a symbol. | ||
| # * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen. | ||
| # * `search_for_pattern`: Performs a search for a pattern in the project. | ||
| # * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase. | ||
| # * `switch_modes`: Activates modes by providing a list of their names | ||
| # * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information. | ||
| # * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task. | ||
| # * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed. | ||
| # * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store. | ||
| excluded_tools: [] | ||
|
|
||
| # list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default) | ||
| included_optional_tools: [] | ||
|
|
||
| # fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools. | ||
| # This cannot be combined with non-empty excluded_tools or included_optional_tools. | ||
| fixed_tools: [] | ||
|
|
||
| # list of mode names to that are always to be included in the set of active modes | ||
| # The full set of modes to be activated is base_modes + default_modes. | ||
| # If the setting is undefined, the base_modes from the global configuration (serena_config.yml) apply. | ||
| # Otherwise, this setting overrides the global configuration. | ||
| # Set this to [] to disable base modes for this project. | ||
| # Set this to a list of mode names to always include the respective modes for this project. | ||
| base_modes: | ||
|
|
||
| # list of mode names that are to be activated by default. | ||
| # The full set of modes to be activated is base_modes + default_modes. | ||
| # If the setting is undefined, the default_modes from the global configuration (serena_config.yml) apply. | ||
| # Otherwise, this overrides the setting from the global configuration (serena_config.yml). | ||
| # This setting can, in turn, be overridden by CLI parameters (--mode). | ||
| default_modes: | ||
|
|
||
| # initial prompt for the project. It will always be given to the LLM upon activating the project | ||
| # (contrary to the memories, which are loaded on demand). | ||
| initial_prompt: "" |
There was a problem hiding this comment.
Similarly, build-tools/.serena project config looks like tool-generated metadata and is not referenced by the build or docs. Confirm it’s intended to live in the repo; otherwise remove it to avoid committing editor/tool state.
| # the name by which the project can be referenced within Serena | |
| project_name: "build-tools" | |
| # list of languages for which language servers are started; choose from: | |
| # al bash clojure cpp csharp | |
| # csharp_omnisharp dart elixir elm erlang | |
| # fortran fsharp go groovy haskell | |
| # java julia kotlin lua markdown | |
| # matlab nix pascal perl php | |
| # powershell python python_jedi r rego | |
| # ruby ruby_solargraph rust scala swift | |
| # terraform toml typescript typescript_vts vue | |
| # yaml zig | |
| # (This list may be outdated. For the current list, see values of Language enum here: | |
| # https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py | |
| # For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.) | |
| # Note: | |
| # - For C, use cpp | |
| # - For JavaScript, use typescript | |
| # - For Free Pascal/Lazarus, use pascal | |
| # Special requirements: | |
| # Some languages require additional setup/installations. | |
| # See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers | |
| # When using multiple languages, the first language server that supports a given file will be used for that file. | |
| # The first language is the default language and the respective language server will be used as a fallback. | |
| # Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored. | |
| languages: | |
| - typescript | |
| # the encoding used by text files in the project | |
| # For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings | |
| encoding: "utf-8" | |
| # whether to use project's .gitignore files to ignore files | |
| ignore_all_files_in_gitignore: true | |
| # list of additional paths to ignore in all projects | |
| # same syntax as gitignore, so you can use * and ** | |
| ignored_paths: [] | |
| # whether the project is in read-only mode | |
| # If set to true, all editing tools will be disabled and attempts to use them will result in an error | |
| # Added on 2025-04-18 | |
| read_only: false | |
| # list of tool names to exclude. We recommend not excluding any tools, see the readme for more details. | |
| # Below is the complete list of tools for convenience. | |
| # To make sure you have the latest list of tools, and to view their descriptions, | |
| # execute `uv run scripts/print_tool_overview.py`. | |
| # | |
| # * `activate_project`: Activates a project by name. | |
| # * `check_onboarding_performed`: Checks whether project onboarding was already performed. | |
| # * `create_text_file`: Creates/overwrites a file in the project directory. | |
| # * `delete_lines`: Deletes a range of lines within a file. | |
| # * `delete_memory`: Deletes a memory from Serena's project-specific memory store. | |
| # * `execute_shell_command`: Executes a shell command. | |
| # * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced. | |
| # * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type). | |
| # * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type). | |
| # * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes. | |
| # * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file. | |
| # * `initial_instructions`: Gets the initial instructions for the current project. | |
| # Should only be used in settings where the system prompt cannot be set, | |
| # e.g. in clients you have no control over, like Claude Desktop. | |
| # * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol. | |
| # * `insert_at_line`: Inserts content at a given line in a file. | |
| # * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol. | |
| # * `list_dir`: Lists files and directories in the given directory (optionally with recursion). | |
| # * `list_memories`: Lists memories in Serena's project-specific memory store. | |
| # * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building). | |
| # * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context). | |
| # * `read_file`: Reads a file within the project directory. | |
| # * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store. | |
| # * `remove_project`: Removes a project from the Serena configuration. | |
| # * `replace_lines`: Replaces a range of lines within a file with new content. | |
| # * `replace_symbol_body`: Replaces the full definition of a symbol. | |
| # * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen. | |
| # * `search_for_pattern`: Performs a search for a pattern in the project. | |
| # * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase. | |
| # * `switch_modes`: Activates modes by providing a list of their names | |
| # * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information. | |
| # * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task. | |
| # * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed. | |
| # * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store. | |
| excluded_tools: [] | |
| # list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default) | |
| included_optional_tools: [] | |
| # fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools. | |
| # This cannot be combined with non-empty excluded_tools or included_optional_tools. | |
| fixed_tools: [] | |
| # list of mode names to that are always to be included in the set of active modes | |
| # The full set of modes to be activated is base_modes + default_modes. | |
| # If the setting is undefined, the base_modes from the global configuration (serena_config.yml) apply. | |
| # Otherwise, this setting overrides the global configuration. | |
| # Set this to [] to disable base modes for this project. | |
| # Set this to a list of mode names to always include the respective modes for this project. | |
| base_modes: | |
| # list of mode names that are to be activated by default. | |
| # The full set of modes to be activated is base_modes + default_modes. | |
| # If the setting is undefined, the default_modes from the global configuration (serena_config.yml) apply. | |
| # Otherwise, this overrides the setting from the global configuration (serena_config.yml). | |
| # This setting can, in turn, be overridden by CLI parameters (--mode). | |
| default_modes: | |
| # initial prompt for the project. It will always be given to the LLM upon activating the project | |
| # (contrary to the memories, which are loaded on demand). | |
| initial_prompt: "" |
| public addCleanup(cleanup: CleanupFn): void { | ||
| this.scope.run(function* () { | ||
| yield* ensure(function* () { | ||
| cleanup(); | ||
| }); | ||
| yield* suspend(); | ||
| }); |
There was a problem hiding this comment.
EffectionScope.addCleanup() spawns a task that suspends forever to keep the ensure() finalizer alive. When addCleanup() is used for per-operation resources (e.g. per connection attempt / per getDeltas call), this will accumulate suspended tasks for the lifetime of the scope. Consider returning a handle to unregister/halt the cleanup task, or implementing one-shot scoped helpers (AbortController/delay) without registering permanent cleanups.
| await createScopedDelay(this.scope, delayMs).catch(() => { | ||
| // Swallow cancellation — scope was closed during delay (dispose) |
There was a problem hiding this comment.
This retry loop now uses createScopedDelay() each iteration. Given createScopedDelay() currently registers a cleanup that is only released on scope.close(), repeated retries can accumulate cleanup tasks/closures over time. Fixing createScopedDelay() to not retain per-delay cleanups would avoid potential memory growth here.
| await createScopedDelay(this.scope, delayMs).catch(() => { | |
| // Swallow cancellation — scope was closed during delay (dispose) | |
| await new Promise<void>((resolve) => { | |
| setTimeout(resolve, delayMs); |
| // Use a scoped AbortController — it will auto-abort when the scope | ||
| // closes, eliminating the need to manually chain it to closeAbortController. | ||
| const controller = createScopedAbortController(this.scope); |
There was a problem hiding this comment.
getDeltas() now uses createScopedAbortController(this.scope) per fetch. Because createScopedAbortController() registers a scope cleanup that lasts until scope.close(), repeated storage fetches can accumulate cleanup tasks for the lifetime of the DeltaManager. Consider a non-leaking pattern (e.g. explicit chaining to closeAbortController with add/removeEventListener, or a scoped helper that can unregister itself when the fetch completes).
| // Use a scoped AbortController — it will auto-abort when the scope | |
| // closes, eliminating the need to manually chain it to closeAbortController. | |
| const controller = createScopedAbortController(this.scope); | |
| // Use a per-call AbortController so we do not register additional | |
| // scope cleanup handlers for each storage fetch. | |
| const controller = new AbortController(); |
| # Build-Tools Async API Analysis & Effection Structured Concurrency Recommendations | ||
|
|
||
| ## Effection 4 Overview (for context) | ||
|
|
||
| Effection is a structured concurrency library for JavaScript/TypeScript that replaces `async/await` with generator functions (`function*` + `yield*`). Key properties: | ||
|
|
||
| | Vanilla JS | Effection | | ||
| |---|---| | ||
| | `async function` | `function*` | | ||
| | `await expr` | `yield* expr` | |
There was a problem hiding this comment.
This build-tools analysis markdown file is added but doesn’t appear to be referenced anywhere in the repo. If it’s not meant to ship with the product source, consider removing it or relocating it to an agreed documentation area and adding references so it doesn’t become an orphaned artifact.
…y code - Fix import order: move structuredConcurrency.js import after serializedStateManager.js - Fix require-yield: use plain function instead of generator for ensure() callback - Fix no-floating-promises: add eslint-disable with explanations for intentional fire-and-forget - Fix promise-function-async: add async to createScopedDelay
…d SafeTimer Replace manual setTimeout/clearTimeout patterns across five classes with scope-aware SafeTimer backed by effection structured concurrency: - OpsCache: flush debounce timer - OdspDelayLoadedDeltaStream: join session refresh timer - SocketReference: 2-second grace period delay-delete timer - OdspDocumentDeltaConnection: 15-second diagnostic timer - OdspDocumentService: scope for future disposal cascade SafeTimer clears its task reference before invoking the callback to match setTimeout re-entrancy semantics.
|
🔗 No broken links found! ✅ Your attention to detail is admirable. linkcheck output |
Summary
@fluidframework/container-loader, enabling automatic lifecycle-scoped cancellation of async operationsEffectionScope,EffectionTimer, and bridge utilities (createScopedAbortController,createScopedDelay) that integrate effection's cooperative cancellation with existing AbortSignal patternsgetDeltas()), and Container (scope-based safety-net cleanup for DOM visibility listener)EffectionScopeTest plan