@telorun/http-server 0.7.0 → 0.8.0

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,41 @@
1
1
  # @telorun/http-server
2
2
 
3
+ ## 0.8.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 5331205: Add cooperative invoke cancellation via an out-of-band `InvokeContext`.
8
+
9
+ Every `invoke(inputs, ctx?)` now receives a second argument carrying a read-only
10
+ cancellation token (`ctx.cancellation`): poll `isCancelled`, subscribe via
11
+ `onCancelled`, bail with `throwIfCancelled`, or hand its `signal` to a Web API.
12
+ The SDK exposes the source/token split (`createCancellationSource`,
13
+ `CancellationSource`/`CancellationToken`), a never-cancellable sentinel, and the
14
+ `isCancellationError` helper. Deadlines are scheduled cancellation
15
+ (`source.cancelAt(epochMs)` / `cancelAfter(ms)`).
16
+
17
+ The kernel mints one cancellation scope per invocation tree (inherited by nested
18
+ invokes via a kernel-internal `AsyncLocalStorage`, always passed to controllers
19
+ as the explicit argument), refuses a not-yet-dispatched invoke whose tree was
20
+ cancelled with `ERR_INVOKE_CANCELLED`, and emits a scoped `InvokeCancelled`
21
+ event. `Kernel.invoke(ref, inputs, opts?)` accepts `{ signal, deadlineAt }`.
22
+ Sources are allocated lazily, so invokes that never touch cancellation pay no
23
+ extra allocation.
24
+
25
+ The boot `targets` run is also cancellable: `Runnable.run(ctx?)` now receives
26
+ the token, `Kernel.cancel(reason?)` cancels the boot scope, and the CLI's
27
+ SIGINT/SIGTERM handler calls it so Ctrl-C cooperatively stops honoring targets
28
+ and in-flight invoke trees (then unblocks graceful exit via `forceIdle`).
29
+
30
+ Honoring leaves: `Ai.Text` / `Ai.TextStream` / `Ai.Agent` forward the token's
31
+ signal into the model (aborting a live LLM stream on cancel); `http-client`
32
+ merges it with its request timeout. Triggers: `http-server` cancels on client
33
+ disconnect and returns 499; `lambda` arms cancellation at the AWS deadline.
34
+
35
+ ### Patch Changes
36
+
37
+ - @telorun/http-dispatch@0.4.1
38
+
3
39
  ## 0.7.0
4
40
 
5
41
  ### 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,25 @@ 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.
111
+ const cancellation = this.ctx.createCancellationSource();
112
+ request.raw.on("close", () => {
113
+ if (!reply.sent)
114
+ cancellation.cancel("client-disconnect");
115
+ });
109
116
  let result;
110
117
  try {
111
118
  result = handler
112
- ? await this.ctx.invokeResolved(handlerKind, handlerName, handler, invokeInput)
119
+ ? await this.ctx.invokeResolved(handlerKind, handlerName, handler, invokeInput, cancellation.context)
113
120
  : undefined;
114
121
  }
115
122
  catch (err) {
123
+ if (isCancellationError(err)) {
124
+ if (!reply.sent)
125
+ reply.code(499).send();
126
+ return;
127
+ }
116
128
  if (!isInvokeError(err))
117
129
  throw err;
118
130
  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.0",
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,29 @@ 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.
152
+ const cancellation = this.ctx.createCancellationSource();
153
+ request.raw.on("close", () => {
154
+ if (!reply.sent) cancellation.cancel("client-disconnect");
155
+ });
156
+
149
157
  let result: unknown;
150
158
  try {
151
159
  result = handler
152
- ? await this.ctx.invokeResolved(handlerKind, handlerName, handler, invokeInput)
160
+ ? await this.ctx.invokeResolved(
161
+ handlerKind,
162
+ handlerName,
163
+ handler,
164
+ invokeInput,
165
+ cancellation.context,
166
+ )
153
167
  : undefined;
154
168
  } catch (err) {
169
+ if (isCancellationError(err)) {
170
+ if (!reply.sent) reply.code(499).send();
171
+ return;
172
+ }
155
173
  if (!isInvokeError(err)) throw err;
156
174
  return dispatchCatches(
157
175
  route.catches,