@telorun/http-server 0.7.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,47 @@
1
1
  # @telorun/http-server
2
2
 
3
+ ## 0.8.1
4
+
5
+ ### Patch Changes
6
+
7
+ - e3146f3: Fix spurious request cancellation (HTTP 499) for body-bearing requests whose handler awaits before replying. Per-request cancellation was wired to the request stream's `close` event, which fires as normal cleanup once a request body has been fully received — so any `PUT`/`POST` whose handler did async work (e.g. a DB query) before sending a response was cancelled mid-flight and answered with 499. Cancellation now listens on the response socket, which only closes early on a genuine client disconnect; normal completions and synchronous rejects are unaffected.
8
+
9
+ ## 0.8.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 5331205: Add cooperative invoke cancellation via an out-of-band `InvokeContext`.
14
+
15
+ Every `invoke(inputs, ctx?)` now receives a second argument carrying a read-only
16
+ cancellation token (`ctx.cancellation`): poll `isCancelled`, subscribe via
17
+ `onCancelled`, bail with `throwIfCancelled`, or hand its `signal` to a Web API.
18
+ The SDK exposes the source/token split (`createCancellationSource`,
19
+ `CancellationSource`/`CancellationToken`), a never-cancellable sentinel, and the
20
+ `isCancellationError` helper. Deadlines are scheduled cancellation
21
+ (`source.cancelAt(epochMs)` / `cancelAfter(ms)`).
22
+
23
+ The kernel mints one cancellation scope per invocation tree (inherited by nested
24
+ invokes via a kernel-internal `AsyncLocalStorage`, always passed to controllers
25
+ as the explicit argument), refuses a not-yet-dispatched invoke whose tree was
26
+ cancelled with `ERR_INVOKE_CANCELLED`, and emits a scoped `InvokeCancelled`
27
+ event. `Kernel.invoke(ref, inputs, opts?)` accepts `{ signal, deadlineAt }`.
28
+ Sources are allocated lazily, so invokes that never touch cancellation pay no
29
+ extra allocation.
30
+
31
+ The boot `targets` run is also cancellable: `Runnable.run(ctx?)` now receives
32
+ the token, `Kernel.cancel(reason?)` cancels the boot scope, and the CLI's
33
+ SIGINT/SIGTERM handler calls it so Ctrl-C cooperatively stops honoring targets
34
+ and in-flight invoke trees (then unblocks graceful exit via `forceIdle`).
35
+
36
+ Honoring leaves: `Ai.Text` / `Ai.TextStream` / `Ai.Agent` forward the token's
37
+ signal into the model (aborting a live LLM stream on cancel); `http-client`
38
+ merges it with its request timeout. Triggers: `http-server` cancels on client
39
+ disconnect and returns 499; `lambda` arms cancellation at the AWS deadline.
40
+
41
+ ### Patch Changes
42
+
43
+ - @telorun/http-dispatch@0.4.1
44
+
3
45
  ## 0.7.0
4
46
 
5
47
  ### Minor Changes
@@ -1,6 +1,6 @@
1
1
  import { Type } from "@sinclair/typebox";
2
2
  import { CatchEntry, dispatchCatches, dispatchReturns, ReturnEntry, validateNoContentTypeHeader, validateStreamWhenDoesNotReferenceResult, } from "@telorun/http-dispatch";
3
- import { InvokeError, isInvokeError, Ref, Stream, } from "@telorun/sdk";
3
+ import { InvokeError, isCancellationError, isInvokeError, Ref, Stream, } from "@telorun/sdk";
4
4
  import { fastifyReplySink } from "./fastify-reply-sink.js";
5
5
  const HttpApiRouteManifest = Type.Object({
6
6
  request: Type.Object({
@@ -106,13 +106,29 @@ export class HttpServerApi {
106
106
  inputs: resolvedInputs,
107
107
  };
108
108
  const sink = fastifyReplySink(reply);
109
+ // Per-request cancellation: abandon downstream work when the client
110
+ // disconnects before the response is sent. Listen on the response
111
+ // socket, not the request stream — the latter's `close` fires as normal
112
+ // cleanup once a request body has been fully received, which would
113
+ // cancel any body-bearing request that awaits (e.g. a DB call) before
114
+ // replying. The response socket only closes early on a real disconnect.
115
+ const cancellation = this.ctx.createCancellationSource();
116
+ reply.raw.on("close", () => {
117
+ if (!reply.sent)
118
+ cancellation.cancel("client-disconnect");
119
+ });
109
120
  let result;
110
121
  try {
111
122
  result = handler
112
- ? await this.ctx.invokeResolved(handlerKind, handlerName, handler, invokeInput)
123
+ ? await this.ctx.invokeResolved(handlerKind, handlerName, handler, invokeInput, cancellation.context)
113
124
  : undefined;
114
125
  }
115
126
  catch (err) {
127
+ if (isCancellationError(err)) {
128
+ if (!reply.sent)
129
+ reply.code(499).send();
130
+ return;
131
+ }
116
132
  if (!isInvokeError(err))
117
133
  throw err;
118
134
  return dispatchCatches(route.catches, { code: err.code, message: err.message, data: err.data }, requestContext, acceptHeader, this.ctx.moduleContext, this.ctx.validateSchema.bind(this.ctx), sink);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telorun/http-server",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
4
4
  "description": "Telo HTTP Server module - HTTP server and API resource kinds for Telo manifests.",
5
5
  "keywords": [
6
6
  "telo",
@@ -49,7 +49,7 @@
49
49
  "@types/node": "^20.0.0",
50
50
  "typescript": "^5.0.0",
51
51
  "vitest": "^2.1.8",
52
- "@telorun/sdk": "0.18.0"
52
+ "@telorun/sdk": "0.19.0"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "@telorun/sdk": "*"
@@ -12,6 +12,7 @@ import {
12
12
  ControllerContext,
13
13
  Invocable,
14
14
  InvokeError,
15
+ isCancellationError,
15
16
  isInvokeError,
16
17
  KindRef,
17
18
  Ref,
@@ -146,12 +147,33 @@ export class HttpServerApi implements ResourceInstance {
146
147
 
147
148
  const sink = fastifyReplySink(reply);
148
149
 
150
+ // Per-request cancellation: abandon downstream work when the client
151
+ // disconnects before the response is sent. Listen on the response
152
+ // socket, not the request stream — the latter's `close` fires as normal
153
+ // cleanup once a request body has been fully received, which would
154
+ // cancel any body-bearing request that awaits (e.g. a DB call) before
155
+ // replying. The response socket only closes early on a real disconnect.
156
+ const cancellation = this.ctx.createCancellationSource();
157
+ reply.raw.on("close", () => {
158
+ if (!reply.sent) cancellation.cancel("client-disconnect");
159
+ });
160
+
149
161
  let result: unknown;
150
162
  try {
151
163
  result = handler
152
- ? await this.ctx.invokeResolved(handlerKind, handlerName, handler, invokeInput)
164
+ ? await this.ctx.invokeResolved(
165
+ handlerKind,
166
+ handlerName,
167
+ handler,
168
+ invokeInput,
169
+ cancellation.context,
170
+ )
153
171
  : undefined;
154
172
  } catch (err) {
173
+ if (isCancellationError(err)) {
174
+ if (!reply.sent) reply.code(499).send();
175
+ return;
176
+ }
155
177
  if (!isInvokeError(err)) throw err;
156
178
  return dispatchCatches(
157
179
  route.catches,