Skip to content

feat: multi-realm patch safety + handle.flush() for dispose parity (#11, #19)#40

Merged
AndresL230 merged 13 commits into
mainfrom
feat/11-19-multi-realm-dispose-parity
May 19, 2026
Merged

feat: multi-realm patch safety + handle.flush() for dispose parity (#11, #19)#40
AndresL230 merged 13 commits into
mainfrom
feat/11-19-multi-realm-dispose-parity

Conversation

@AndresL230
Copy link
Copy Markdown
Contributor

@AndresL230 AndresL230 commented May 19, 2026

Summary

Closes #11.
Closes #19.

Cross-SDK: a corresponding Python tracking issue will be filed in recost-dev/middleware-python once this PR is reviewed.

Tests

  • +14 vitest tests across two test files (2 typed-error class, 1 state-on-globalThis, 4 dual-package, 5 uninstall identity check, 2 handle.flush()).
  • Total: 267 vitest unit + 7 dist smoke (was 253 + 7 at baseline).

Test plan

  • CI: lint clean, build green, tests pass.
  • Manual smoke: `npm test` locally on a fresh clone.
  • Manual smoke: import + log the two new error classes from a small reproducer to confirm they ship in both ESM and CJS bundles (already verified locally — both bundles export the 7 expected names).

🤖 Generated with Claude Code

AndresL230 and others added 11 commits May 18, 2026 22:47
…11, #19)

Wave 4 (PR #38) is merged; this commit lands the Wave 5 plan + spec on the
implementation branch alongside the roadmap update. Wave 5 covers issues
#11 (multi-realm patch model — dual-package, uninstall identity check,
worker_threads docs) and #19 (Python sync vs Node async dispose parity —
adds RecostHandle.flush()).
#11)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
#11)

Replace five module-level `let` slots with a single `InterceptorState` object
stored on `globalThis` under `Symbol.for("@recost-dev/node:interceptor-state")`.
`patchedFetch`, `makeRequestWrapper`, `install`, `uninstall`, `isInstalled`, and
`getRawFetch` all read/write through `getState()`. Adds 1 regression test that
verifies `globalThis[STATE_KEY]` is populated with `installed=true` and the saved
original/patched fetch references after `install()`.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…test (review fixes for #11)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds setOnError() to wire a host-supplied callback into the interceptor
state; install() now fires RecostInterceptorAlreadyInstalledError via
that callback on a second call instead of silently no-oping. init()
calls setOnError(config.onError ?? null) before install() so the error
routes through the user's configured handler. setOnError is re-exported
from src/index.ts as part of the public API.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…kage test (review fixes for #11)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ering third-party patches (#11)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tity-check block (review fixes for #11)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… error classes (#11, #19)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 19, 2026

Warning

Rate limit exceeded

@AndresL230 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 13 minutes and 26 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 865738ff-5830-479d-9639-f3259fad735d

📥 Commits

Reviewing files that changed from the base of the PR and between d5ea9bd and 254b5b6.

📒 Files selected for processing (5)
  • README.md
  • docs/superpowers/roadmap-2026-05-13-issue-waves.md
  • docs/superpowers/specs/2026-05-18-multi-realm-and-dispose-parity-design.md
  • src/init.ts
  • tests/interceptor.test.ts
📝 Walkthrough

Walkthrough

This PR implements Wave 5 (Multi-realm Patch Safety & Dispose Parity) by migrating interceptor singleton state from module-level to globalThis-keyed shared storage, enabling dual-package coordination; adding advisory error types for conflict detection; introducing a new flush() handle method for window-bounded flushes; and documenting worker-thread instrumentation patterns.

Changes

Multi-realm Patch Safety & Dispose Parity

Layer / File(s) Summary
Error types and shared state contracts
src/core/types.ts, src/core/interceptor.ts
Introduces RecostInterceptorAlreadyInstalledError and RecostInterceptorPatchOverwrittenError (with skippedBindings), plus InterceptorBinding union type. Establishes STATE_KEY and InterceptorState interface on globalThis to coordinate across multiple SDK copies in the same realm.
Interceptor lifecycle with shared state and conflict handling
src/core/interceptor.ts
install() now initializes shared state, saves original function references, patches globals, and fires RecostInterceptorAlreadyInstalledError if already installed. uninstall() identity-checks each binding, restores only those still matching the interceptor's patch, and fires RecostInterceptorPatchOverwrittenError with skipped bindings when third-party overwrites are detected. setOnError() registers typed error handler in shared state.
Fetch wrapper with shared state and re-entrancy guard
src/core/interceptor.ts
Fetch wrapper reads state.callback and short-circuits to passthrough when detached or original unavailable. Guards against double-counting via state.inFetchWrapper around originalFetch call. All telemetry points updated to emit through state.callback rather than module variables.
HTTP/HTTPS wrapper with shared state
src/core/interceptor.ts
Request wrapper checks state.callback === null for detached passthrough and state.inFetchWrapper to prevent double-counting. Response and error telemetry updated to emit through state.callback.
Public API exports and RecostHandle.flush()
src/index.ts, src/init.ts
Exports new error classes, InterceptorBinding type, and setOnError function. Adds async flush() method to RecostHandle (no-op after dispose()). Wires setOnError(config.onError) during init() to route interceptor advisory errors. Implements flush() to trigger immediate window flush without interceptor/transport teardown.
Interceptor test coverage for global state and conflicts
tests/interceptor.test.ts
Validates install() populates globalThis state; second install() fires advisory error via setOnError; setOnError(null) clears handler; clean uninstall() allows re-install. Tests identity-check uninstall under third-party wrapping without clobbering wrapper chains. Includes typed error assertions for inheritance, name, and skippedBindings.
Handle flush() test coverage
tests/init.test.ts
Verifies handle.flush() sends in-flight aggregation to WebSocket without disposing, keeps handle usable, and post-dispose flush() resolves as no-op.
User documentation and planning artifacts
README.md, docs/superpowers/specs/2026-05-18-multi-realm-and-dispose-parity-design.md, docs/superpowers/plans/2026-05-18-multi-realm-and-dispose-parity.md, docs/superpowers/roadmap-2026-05-13-issue-waves.md
README updated with new error routing examples, flush() method behavior, and Worker threads section explaining per-worker init() calls. Spec document details architecture, error emission, and test expectations. Plan document enumerates Wave 5 tasks 1–8 with verification steps. Roadmap marks Wave 4 complete and Wave 5 in-progress with document paths.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 A singleton shared upon the throne,
GlobalThis claims state once its own,
No more two wraps, no clobbered flow—
Workers flush, and third-party knows,
One realm, one truth, harmonious glow!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main changes: multi-realm patch safety and handle.flush() for dispose parity, with references to issues #11 and #19.
Linked Issues check ✅ Passed The PR successfully implements all key requirements from #11 and #19: globalThis-keyed shared state for dual-package coordination, uninstall identity checks to avoid clobbering third-party patches, worker documentation, and RecostHandle.flush() for dispose parity.
Out of Scope Changes check ✅ Passed All changes are scoped to the stated objectives: interceptor state management, error handling, handle.flush() implementation, tests, and documentation updates. No unrelated modifications detected.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/11-19-multi-realm-dispose-parity

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/superpowers/plans/2026-05-18-multi-realm-and-dispose-parity.md`:
- Line 1525: Find each unlabeled fenced code block (the triple-backtick blocks
shown in the diff) and add an explicit language identifier after the opening
````` to satisfy MD040; for example convert ``` to ```markdown for prose
samples, ```ts or ```typescript for TypeScript snippets, and ```bash or ```sh
for shell snippets. Update the fence instances referenced in the review (the
empty ``` blocks around the discussed sections) so every fenced block in this
document has a language tag.

In `@docs/superpowers/roadmap-2026-05-13-issue-waves.md`:
- Around line 106-110: The "Wave 5 PR-shape guidance" text is stale: update the
Wave 5 section so it matches the linked spec/plan
(specs/2026-05-18-multi-realm-and-dispose-parity-design.md and
plans/2026-05-18-multi-realm-and-dispose-parity.md) by replacing the phrase "two
plans, two PRs" (currently at Line 121) with wording that reflects the bundled
approach (e.g., "single plan and single PR" or "one plan/PR"), and ensure any
surrounding sentences no longer reference two separate plans/PRs so the guidance
is consistent with the linked spec/plan.

In `@docs/superpowers/specs/2026-05-18-multi-realm-and-dispose-parity-design.md`:
- Around line 92-96: The spec is inconsistent: the pseudocode sets
state.installed = false while the conflict contract requires state.installed
remain true to block re-entry after bindings are skipped; update the pseudocode
so that state.installed is not cleared when uninstall was skipped (leave
state.installed = true) and add a clarifying comment next to state.installed,
state.callback and the install() re-entry gate; alternatively, if the intended
behavior is to allow re-install, update the conflict contract text to match and
explicitly describe when state.installed is toggled—referencing state.installed,
state.callback and install() to ensure both the code path and the contract are
aligned.

In `@README.md`:
- Around line 162-163: The README sample config shows apiKey (symbol: apiKey)
without the required projectId; update the example to include projectId whenever
apiKey is set so it matches the validation rules (reference symbols: apiKey and
projectId) and prevents runtime validation errors; edit the sample config block
to add a projectId entry adjacent to apiKey and ensure the onError block remains
unchanged.

In `@src/core/interceptor.ts`:
- Around line 51-52: The global boolean guard state.inFetchWrapper is causing
unrelated concurrent http.request/https.request calls to be suppressed; replace
this global re-entrancy flag with a per-async-context guard using Node's
AsyncLocalStorage so only requests causally triggered by the current fetch are
skipped. Specifically, introduce an AsyncLocalStorage store (e.g., fetchContext)
and set a value (like {inFetch: true}) around the wrapper that replaces
originalFetch, read that store in the interceptors that currently check
state.inFetchWrapper (references: state.inFetchWrapper, originalFetch, and the
fetch wrapper code paths), and update the checks at the locations noted (the
fetch wrapper enter/exit and the http/https request interceptors) to consult
fetchContext.getStore()?.inFetch instead of the global flag; ensure the store is
correctly propagated across async boundaries and cleaned up on completion.

In `@src/init.ts`:
- Around line 204-213: The flush() method currently swallows errors from
flushAndSend(); update its catch to route failures to the configured error
handler by calling the module's onError (or config.onError) with the caught
error (and context if available) instead of silently ignoring it; keep the
disposed guard and existing idempotency but ensure the catch block invokes
onError(err) so flush() preserves the documented error contract (symbols:
flush(), flushAndSend(), onError / config.onError, disposed).

In `@tests/interceptor.test.ts`:
- Around line 989-1010: The cleanup currently restores only fetch/http and then
deletes STATE_KEY, leaving any https monkey patches in place; import node:https
in this test file and update the cleanup branch that checks s?.installed to also
restore s.originalHttpsRequest and s.originalHttpsGet onto the https module
(e.g., (https as unknown as { request: typeof https.request }).request =
s.originalHttpsRequest and similarly for get) before deleting STATE_KEY, using
the existing STATE_KEY and s symbols to locate the state object.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 76e087d0-b0eb-48c0-9058-c4b30320980c

📥 Commits

Reviewing files that changed from the base of the PR and between c6c2a64 and d5ea9bd.

📒 Files selected for processing (10)
  • README.md
  • docs/superpowers/plans/2026-05-18-multi-realm-and-dispose-parity.md
  • docs/superpowers/roadmap-2026-05-13-issue-waves.md
  • docs/superpowers/specs/2026-05-18-multi-realm-and-dispose-parity-design.md
  • src/core/interceptor.ts
  • src/core/types.ts
  • src/index.ts
  • src/init.ts
  • tests/init.test.ts
  • tests/interceptor.test.ts

```

The main thread's `init()` does not propagate to workers, and the SDK does not detect or warn about worker spawns — instrumenting workers is the host's responsibility.
```
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add language identifiers to unlabeled fenced code blocks.

These fences trip MD040 in markdownlint. Use explicit languages (e.g., markdown, ts, bash) for each affected block to keep the doc lint-clean.

Also applies to: 1542-1542, 1567-1567, 1586-1586, 1614-1614

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 1525-1525: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/superpowers/plans/2026-05-18-multi-realm-and-dispose-parity.md` at line
1525, Find each unlabeled fenced code block (the triple-backtick blocks shown in
the diff) and add an explicit language identifier after the opening ````` to
satisfy MD040; for example convert ``` to ```markdown for prose samples, ```ts
or ```typescript for TypeScript snippets, and ```bash or ```sh for shell
snippets. Update the fence instances referenced in the review (the empty ```
blocks around the discussed sections) so every fenced block in this document has
a language tag.

Comment thread docs/superpowers/roadmap-2026-05-13-issue-waves.md
Comment thread docs/superpowers/specs/2026-05-18-multi-realm-and-dispose-parity-design.md Outdated
Comment thread README.md
Comment thread src/core/interceptor.ts
Comment on lines +51 to +52
inFetchWrapper: boolean;
originalFetch: typeof globalThis.fetch | null;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Global re-entrancy guard drops unrelated concurrent HTTP events.

Line 408 uses a process-wide state.inFetchWrapper flag. While one fetch() is in-flight (Line 287→380), any unrelated concurrent http.request/https.request is skipped as a false positive. This causes telemetry loss under normal concurrency.

Use a per-async-context guard (e.g., AsyncLocalStorage) so only HTTP calls causally triggered by the current fetch are suppressed.

Also applies to: 287-290, 380-381, 408-411

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/interceptor.ts` around lines 51 - 52, The global boolean guard
state.inFetchWrapper is causing unrelated concurrent http.request/https.request
calls to be suppressed; replace this global re-entrancy flag with a
per-async-context guard using Node's AsyncLocalStorage so only requests causally
triggered by the current fetch are skipped. Specifically, introduce an
AsyncLocalStorage store (e.g., fetchContext) and set a value (like {inFetch:
true}) around the wrapper that replaces originalFetch, read that store in the
interceptors that currently check state.inFetchWrapper (references:
state.inFetchWrapper, originalFetch, and the fetch wrapper code paths), and
update the checks at the locations noted (the fetch wrapper enter/exit and the
http/https request interceptors) to consult fetchContext.getStore()?.inFetch
instead of the global flag; ensure the store is correctly propagated across
async boundaries and cleaned up on completion.

Comment thread src/init.ts
Comment thread tests/interceptor.test.ts Outdated
AndresL230 and others added 2 commits May 19, 2026 00:34
handle.flush() was silently swallowing errors despite its documented
"never rejects, errors route through onError" contract; dispose() had
the same swallow with a comment falsely claiming flushAndSend routes
errors itself. Extract a reportFlushError helper and use it from all
five flush sites (periodic, wouldOverflow, maxBatchSize, dispose,
flush). Also restore https bindings in the conflict-state afterEach
cleanup so a future test that wraps https.* doesn't pollute later runs.

Co-Authored-By: CodeRabbit <noreply@coderabbit.ai>
- README: add projectId to the local-mode-unavailability sample so it
  satisfies validateConfig when copy-pasted with apiKey set.
- roadmap: replace the stale "two plans, two PRs" Wave 5 PR-shape note
  with the actual bundled-PR outcome.
- spec: resolve the uninstall pseudocode contradiction between
  state.installed=false and the conflict contract requiring installed=true
  to gate re-install. Split clearly into conflict path (preserves
  installed + originals + onError) and clean path (full reset).

Co-Authored-By: CodeRabbit <noreply@coderabbit.ai>
@AndresL230
Copy link
Copy Markdown
Contributor Author

CodeRabbit triage

Pushed two follow-up commits (38d45b0 + 254b5b6) addressing 5 of 7 findings.

Applied:

  • README.md:163 — added projectId to the sample so the snippet doesn't trip validateConfig when copy-pasted with apiKey set.
  • src/init.ts:213 — extracted a reportFlushError helper and routed handle.flush() + dispose() errors through it; both methods now honor their documented "never reject, errors go to onError" contract.
  • tests/interceptor.test.ts:1010 — added https to the conflict-state afterEach so a future test that wraps https.request/https.get won't leak.
  • docs/superpowers/roadmap-2026-05-13-issue-waves.md:121 — replaced the stale "two plans, two PRs" note with the actual bundled-PR outcome.
  • docs/superpowers/specs/...:96 — resolved the uninstall pseudocode contradiction by splitting the conflict path (preserves installed = true + originals + onError) and the clean path (full reset).

Deferred:

  • src/core/interceptor.ts:52state.inFetchWrapper is a process-wide guard that can drop unrelated concurrent http.request/https.request events while a fetch() is in flight. CodeRabbit's recommended fix (per-async-context AsyncLocalStorage) is correct. This is a pre-existing limitation (Task 3 only renamed _inFetchWrapperstate.inFetchWrapper; the global-flag design is older) and is properly its own focused PR. Tracking separately rather than expanding Wave 5 scope.
  • docs/superpowers/plans/2026-05-18-multi-realm-and-dispose-parity.md:1525 — MD040 fenced-code-language warnings on the plan doc. Skipping; the plan is internal scaffolding and not on the doc-lint path.

@AndresL230 AndresL230 merged commit 459bfaf into main May 19, 2026
1 check passed
@AndresL230 AndresL230 deleted the feat/11-19-multi-realm-dispose-parity branch May 21, 2026 04:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant