Skip to content

feat(core): operationResource request deadline and disconnect signal (#78) - #109

Draft
tnramalho wants to merge 1 commit into
feat/schema-engine-104from
feat/operation-deadline-78
Draft

feat(core): operationResource request deadline and disconnect signal (#78)#109
tnramalho wants to merge 1 commit into
feat/schema-engine-104from
feat/operation-deadline-78

Conversation

@tnramalho

Copy link
Copy Markdown
Collaborator

Pull Request

Summary

Adds an optional deadlineMs to operationResource operations and an
AbortSignal on every operation context. Closes #78.

Replaces #94, which bundled this with a file-storage seam. That half is
dropped: @concepta/rockets-storage (#107) is the storage abstraction, and
core should not carry a second one. #94 will be closed.

Based on feat/schema-engine-104 (#105), not main. #105 reshaped the
operation compiler (inputDtoinputSchema, applyOutputDto
applyOutputSchema) and renumbered CONFIGURATION.md §6b/§6c. Rebasing #94's
commits produced conflicts through the middle of both functions, so this is
reimplemented against the current code rather than replayed. Retarget to main
once #105 lands.

Changes

  • ctx.signal: AbortSignal on every operation context. Pass it to whatever
    actually waits — a fetch, a driver call — so a request nobody is waiting
    for stops burning work.
  • Optional deadlineMs on the operation descriptor and on the op.read /
    op.write / op.delete builders. Elapsing it aborts the signal and resolves
    504 Gateway Timeout.
  • A client disconnecting before the handler settles aborts the signal too.
    Nothing is written back (the socket is gone) and it never reaches the
    exceptions filter — no 5xx, no error log.
  • CONFIGURATION.md §6f, plus changelog and core README.

Deliberate boundaries

  • The deadline bounds what the CLIENT waits for, not the work. A handler
    that ignores ctx.signal keeps running in the background after the 504 is
    sent. Stopping the work is cooperative, by design — core cannot kill an
    arbitrary handler mid-flight.
  • operationResource only. CRUD command handlers have the same gap (a
    long-running create, e.g. PDF generation) and are not covered here.
  • op.sse() exposes no deadlineMs, and its ctx.signal is inert. The
    handler returns its Observable immediately, so a deadline would race the
    setup call rather than the stream, and cutting a long-lived SSE connection is
    the opposite of what SSE is for. The guard is torn down before the first
    event, so nothing is left to abort the signal — an SSE client going away is
    observed through the Observable's own unsubscription (finalize /
    takeUntil). Stated on the field, in the changelog, and in §6f rather than
    left for someone to discover.

Breaking change

Type-level only: OperationContext gained a required signal field. A
hand-constructed context — a unit test mocking one instead of reading it from a
route — needs to add it. The framework-constructed context every route receives
already does.

Two edge cases the implementation handles

Both were found by reproduction, and both have a comment at the site:

  1. Unhandled rejection killing the process. The abort promise can reject
    while routeHandler is still awaiting DI resolution, before race() is
    ever called. Under Node's default --unhandled-rejections=throw that takes
    down the process on a single slow request. A permanent no-op catch keeps
    it handled; race() still observes the same rejection.
  2. Promise.race resolving by array order. When both inputs are already
    settled, Promise.race picks by argument position, not by which settled
    first — so an already-fulfilled handler result beat an earlier-fired
    deadline and returned 200 well past deadlineMs. race() now rejects
    immediately when the signal is already aborted.

Type of Change

  • Bug fix
  • New feature
  • Breaking change (type-level, see above)
  • Documentation update
  • Refactoring
  • Dependency update

Verification

Run against this branch, on the #105 base:

  • yarn build
  • yarn api:report:update — 9 lines, only deadlineMs and signal
  • yarn typecheck:spec
  • yarn test — 106 files, 1112 tests
  • yarn test:e2e (packages/rockets-core) — 29 files, 315 tests,
    including 6 new deadline cases
  • yarn lint:all

Checklist

  • My code follows the existing patterns in the codebase
  • I have updated relevant documentation
  • I have added tests for new functionality

@tnramalho
tnramalho marked this pull request as draft August 27, 2026 17:47
…78)

Every operation carries `ctx.signal: AbortSignal`, exposed through an
optional `deadlineMs` on the operation descriptor. Elapsing the deadline
aborts the signal and resolves `504 Gateway Timeout`; a client
disconnecting before the handler settles aborts it too, without writing
anything back and without surfacing as an application error.

A handler that never reads `ctx.signal` keeps running after the timeout
response is sent — the deadline bounds what the client waits for, not the
work itself.

`op.sse()` exposes no `deadlineMs`, and `ctx.signal` is inert there: the
handler returns its Observable before the guard is torn down, so an SSE
client going away is observed through the Observable's own
unsubscription. Documented on the field, in the changelog, and in
CONFIGURATION.md §6f.

Type-level breaking change: `OperationContext` gained a required `signal`
field. A hand-constructed context (a unit test mocking one instead of
reading it from a route) needs to add it; the framework-constructed
context every route receives already does.

Reimplemented on top of #105 rather than rebased: the original #78/#86
branch predates the schema engine, and its file-storage half is dropped
here so core keeps one storage abstraction instead of two.
@tnramalho
tnramalho force-pushed the feat/operation-deadline-78 branch from f659b8f to f72140c Compare August 27, 2026 22:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant