Skip to content

fix(core): guard Deferred waiter cleanup in vendored effect patch - #41858

Merged
kitlangton merged 1 commit into
v2from
effect-deferred-guard
Aug 12, 2026
Merged

fix(core): guard Deferred waiter cleanup in vendored effect patch#41858
kitlangton merged 1 commit into
v2from
effect-deferred-guard

Conversation

@kitlangton

Copy link
Copy Markdown
Contributor

What

Extends patches/effect@4.0.0-beta.101.patch with a defensive guard in Deferred's _await cleanup, fixing a production crash:

TypeError: undefined is not an object (evaluating 'X.resumes.indexOf')

The race

Deferred._await registers a waiter and returns a cleanup that unregisters it via self.resumes!.indexOf(resume). Completion (doneUnsafe) resumes every waiter and then sets self.resumes = undefined. When a waiter's interrupt cleanup runs after completion — observed on GET /api/session/:id/question long-polls where a request abort interrupts the awaiting fiber while a concurrent completion lands — the cleanup dereferences undefined and the request 500s.

The invariant argument: a cleanup closure only exists if the deferred was pending at registration (resumes was an array), and the only line in the module that ever makes resumes undefined again is completion. The production TypeError is therefore proof that the complete-then-cleanup order is reachable.

Fix

return internalEffect.sync(() => {
  if (!self.resumes) return;
  const index = self.resumes.indexOf(resume);
  if (index >= 0) self.resumes.splice(index, 1);
});

Applied to both dist/Deferred.js (what runs) and src/Deferred.ts (shipped alongside). The bug is unfixed upstream at 4.0.0-beta.107 head; an upstream fix PR is in flight, but this patch stops the 500s at the next opencode release instead of waiting on an effect release + version bump.

Drive-by repair

While regenerating the patch with bun patch, discovered the existing SSE-identifier hunk for src/unstable/httpapi/HttpApiSchema.ts had wrong line offsets (407 vs the real 430), so bun had been fuzz-applying it mid-overload-signature — leaving syntactically broken TypeScript in the vendored src file. Latent (only dist executes), but this PR regenerates that hunk correctly from pristine + the intended edits. The dist output for the SSE change is unchanged.

Testing

  • Fresh bun install --force: patch applies cleanly; resolved effect/Deferred module contains the guard.
  • Behavioral smoke: complete-then-interrupt on a waiter exits without throwing.
  • Vendored src/unstable/httpapi/HttpApiSchema.ts is now valid TypeScript with the SSE change at the intended location.

Completion (doneUnsafe) resumes every waiter then sets resumes to
undefined. A waiter's interrupt cleanup running after completion
dereferenced the cleared array:

  TypeError: undefined is not an object (evaluating 'X.resumes.indexOf')

Observed in production as HTTP 500s on GET /api/session/:id/question
(request abort interrupting the long-poll's awaiting fiber racing a
concurrent completion). The bug is unfixed upstream at 4.0.0-beta.107;
an upstream PR is in flight, but this patch protects releases now.

Also repairs the existing SSE-identifier hunk for src/unstable/httpapi/
HttpApiSchema.ts: its line offsets were wrong (407 vs 430), so bun
fuzz-applied it mid-overload-signature, leaving syntactically broken
TypeScript in the vendored src file (latent - only dist executes).
Regenerated via bun patch from pristine + intended edits; dist output
is byte-identical to before for the SSE change.
@kitlangton
kitlangton merged commit dc82a44 into v2 Aug 12, 2026
10 checks passed
@kitlangton
kitlangton deleted the effect-deferred-guard branch August 12, 2026 00:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant