@rotorsoft/act-http 1.10.17 → 1.10.18
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/README.md +17 -161
- package/dist/.tsbuildinfo +1 -1
- package/dist/@types/trpc/index.d.ts.map +1 -1
- package/dist/trpc/index.cjs.map +1 -1
- package/dist/trpc/index.js.map +1 -1
- package/package.json +5 -8
package/README.md
CHANGED
|
@@ -156,137 +156,31 @@ On failure: the adapter responds with the framework's idiomatic 400 (`missing-ke
|
|
|
156
156
|
|
|
157
157
|
The framework-agnostic core (`checkWebhook`) and the underlying primitives (`extractIdempotencyKey`, `verifyWebhook`) are exported from `@rotorsoft/act-http/receiver` for receivers whose framework isn't in the adapter list (Koa, raw Node `http`, gRPC-over-HTTP, …) or for receivers with custom policy (e.g. "missing key falls back to body-derived dedup"). Use the `receiver` builder when you can; fall back to the framework `webhookMiddleware`, then the primitives.
|
|
158
158
|
|
|
159
|
-
### `trpc` —
|
|
159
|
+
### `trpc`, `hono`, `openapi` — APIs generated from the registry
|
|
160
|
+
|
|
161
|
+
Each generator walks `app.registry.actions` and emits one route per action, so adding an action to the app adds it to every transport:
|
|
160
162
|
|
|
161
163
|
```ts
|
|
164
|
+
import { hono } from "@rotorsoft/act-http/hono";
|
|
165
|
+
import { openapi } from "@rotorsoft/act-http/openapi";
|
|
162
166
|
import { trpc } from "@rotorsoft/act-http/trpc";
|
|
163
|
-
import { initTRPC } from "@trpc/server";
|
|
164
|
-
import { createHTTPServer } from "@trpc/server/adapters/standalone";
|
|
165
167
|
|
|
166
168
|
const router = trpc(app, {
|
|
167
|
-
// ActorExtractor from `@rotorsoft/act-http/api` — host's auth seam.
|
|
168
169
|
actor: (ctx) => ({ id: ctx.user.id, name: ctx.user.name }),
|
|
169
|
-
// Resolve the target stream per call; singleton aggregates return a constant.
|
|
170
170
|
stream: (action, input, ctx) => `tenant-${ctx.tenant}`,
|
|
171
|
-
// Optional optimistic concurrency — return `undefined` to skip the check.
|
|
172
|
-
expectedVersion: (action, input, ctx) => readIfMatchHeader(ctx),
|
|
173
171
|
});
|
|
174
172
|
|
|
175
|
-
createHTTPServer({ router }).listen(4000);
|
|
176
|
-
|
|
177
|
-
// Client (same types, no codegen):
|
|
178
|
-
// await client.OpenTicket.mutate({ title: "support" });
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
The generator walks `app.registry.actions` once and emits a flat top-level mutation per action. The internal `authenticated(extractor)` middleware runs `options.actor` once per call and injects the resolved `Actor` onto `ctx.actor`, so any procedure logic downstream of the chain sees the same actor. `actor` and `stream` are required; `idempotency` is optional — when configured, the mutation reads `Idempotency-Key` via `keyFrom(ctx)` and throws `CONFLICT` on duplicate claims (the contract intentionally doesn't cache original results, matching the receiver-side "ack the duplicate" semantics).
|
|
182
|
-
|
|
183
|
-
Errors map through the shared `toApiError(...)` table at `@rotorsoft/act-http/api`: `ConcurrencyError` → `CONFLICT`, `InvariantError` → `CONFLICT`, `ValidationError` → `UNPROCESSABLE_CONTENT`, `StreamClosedError` → `PRECONDITION_FAILED`, `NonRetryableError` → `BAD_REQUEST`, anything else → `INTERNAL_SERVER_ERROR`. The same envelope flows from every transport sibling so a client speaking two of them never sees two shapes for the same framework error.
|
|
184
|
-
|
|
185
|
-
```ts
|
|
186
|
-
// Compose the auth middleware into your own procedure chain instead:
|
|
187
|
-
import { authenticated } from "@rotorsoft/act-http/trpc";
|
|
188
|
-
|
|
189
|
-
const t = initTRPC.context<MyCtx>().create();
|
|
190
|
-
const authed = t.procedure.use(authenticated(myExtractor));
|
|
191
|
-
// `authed` carries `ctx.actor: Actor` downstream — use it for hand-written
|
|
192
|
-
// procedures alongside the generated ones.
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
### `hono` — auto-generated REST surface
|
|
196
|
-
|
|
197
|
-
```ts
|
|
198
|
-
import { hono } from "@rotorsoft/act-http/hono";
|
|
199
|
-
import { serve } from "@hono/node-server";
|
|
200
|
-
|
|
201
173
|
const api = hono(app, {
|
|
202
|
-
// ActorExtractor from `@rotorsoft/act-http/api` — host's auth seam.
|
|
203
174
|
actor: (c) => resolveActorFromJwt(c),
|
|
204
|
-
// Resolve the target stream per call; singleton aggregates return a constant.
|
|
205
175
|
stream: (action, input, c) => `tenant-${c.req.header("x-tenant")}`,
|
|
206
|
-
|
|
207
|
-
expectedVersion: (action, input, c) => {
|
|
208
|
-
const v = c.req.header("if-match");
|
|
209
|
-
return v ? Number.parseInt(v, 10) : undefined;
|
|
210
|
-
},
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
serve({ fetch: api.fetch, port: 4000 });
|
|
214
|
-
// POST /api/actions/OpenTicket body: { title }
|
|
215
|
-
// → 200 [Snapshot] | 4xx ApiError envelope
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
The generator walks `app.registry.actions` once and registers one `POST /actions/<actionName>` per action under the configured `basePath` (default `/api`). Bodies are validated with `@hono/zod-validator` against the action's registered Zod schema; failures short-circuit with `400`. The internal `authenticated(extractor)` middleware runs `options.actor` once per request and stashes the resolved `Actor` under `c.get("actor")`, available to any downstream middleware (logging, tracing, hand-written routes mounted on the same Hono instance). `Idempotency-Key` is read from the request header by default; `keyFrom` overrides for hosts that prefer a custom convention.
|
|
219
|
-
|
|
220
|
-
Errors map through the shared `toApiError(...)` table at `@rotorsoft/act-http/api`: `ConcurrencyError → 412 / CONCURRENCY`, `InvariantError → 409 / INVARIANT`, `ValidationError → 422 / VALIDATION`, `StreamClosedError → 410 / STREAM_CLOSED`, `NonRetryableError → 400 / NON_RETRYABLE`. The envelope (`{ error, detail?, code? }`) is the same shape every other act-http transport ships so a client speaking REST plus tRPC never sees two formats for the same framework error.
|
|
221
|
-
|
|
222
|
-
Edge-runtime ready — Hono runs unchanged on Node, Bun, Cloudflare Workers, Vercel Edge, and AWS Lambda. Operators wiring `idempotency` on edge runtimes should verify the `IdempotencyStore` is edge-compatible (in-memory works per worker; cross-worker requires a distributed store).
|
|
223
|
-
|
|
224
|
-
```ts
|
|
225
|
-
// Compose the auth middleware into your own Hono chain instead:
|
|
226
|
-
import { Hono } from "hono";
|
|
227
|
-
import { authenticated, type ActMiddlewareVariables } from "@rotorsoft/act-http/hono";
|
|
228
|
-
|
|
229
|
-
const api = new Hono<{ Variables: ActMiddlewareVariables }>();
|
|
230
|
-
api.use("*", authenticated((c) => resolveActorFromJwt(c)));
|
|
231
|
-
// Hand-written routes alongside the generated ones now see c.get("actor") typed.
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
### `openapi` — OpenAPI 3.1 document emitter
|
|
235
|
-
|
|
236
|
-
```ts
|
|
237
|
-
import { openapi } from "@rotorsoft/act-http/openapi";
|
|
238
|
-
|
|
239
|
-
const doc = openapi(app, {
|
|
240
|
-
info: { title: "Wolfdesk API", version: "1.0.0" },
|
|
241
|
-
servers: [{ url: "https://api.example.com" }],
|
|
242
|
-
// Document the optional cross-cutting headers that the live REST API accepts.
|
|
243
|
-
idempotency: true,
|
|
244
|
-
expectedVersion: true,
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
// Serve alongside the Hono adapter:
|
|
248
|
-
api.get("/openapi.json", (c) => c.json(doc));
|
|
249
|
-
```
|
|
250
|
-
|
|
251
|
-
Pure data emit — no runtime dep on Hono or tRPC. The doc walks `app.registry.actions` once at function call and emits one `POST <basePath>/actions/<actionName>` operation per action, deriving the request-body schema from each action's Zod definition via Zod 4's native `z.toJSONSchema` (OpenAPI 3.1 uses JSON Schema 2020-12 as its schema dialect — no conversion layer needed).
|
|
252
|
-
|
|
253
|
-
**The doc describes the Hono REST surface, not tRPC.** tRPC's URL convention (`POST /trpc/<procedure>`, JSON-RPC-style body framing, batching) doesn't model cleanly as OpenAPI operations; tRPC consumers share types directly via `typeof router` and don't need a doc. The path shape this emitter produces matches `@rotorsoft/act-http/hono` by construction: same default `basePath` (`/api`), same request/response shapes, same error envelope. If the operator overrides `basePath` on the Hono adapter, pass the same value here so the doc keeps describing the live routes. tRPC and Hono can run side-by-side at different mount points on the same Act instance.
|
|
254
|
-
|
|
255
|
-
The same shared `ApiError` envelope underwrites cross-transport consistency: it's referenced once from `components.responses` and every error code (`400`, `409`, `410`, `412`, `422`, `500`) points at it. The doc and the live REST API agree on framework-error shapes by construction.
|
|
256
|
-
|
|
257
|
-
Output is deterministic given the same registry — entries land in `Object.entries(app.registry.actions)` iteration order. CI can snapshot the result to catch unintended API-surface changes; one merge that quietly changes a Zod schema or adds a new action surfaces as a doc diff in the same PR.
|
|
258
|
-
|
|
259
|
-
### `trpc` + `hono` SSE subscriptions — live state from the generators
|
|
260
|
-
|
|
261
|
-
Both generators accept an optional `sse` option that walks the registry once and emits a typed subscription per unique state name, all reading from a host-supplied `BroadcastChannel`. Hosts continue to own publication (`channel.publish(streamId, state, patches)` after each `app.do(...)`); the generators own subscription, accounting, cleanup, and the wire format.
|
|
262
|
-
|
|
263
|
-
```ts
|
|
264
|
-
import { BroadcastChannel } from "@rotorsoft/act-http/sse";
|
|
265
|
-
import { trpc } from "@rotorsoft/act-http/trpc";
|
|
266
|
-
import { hono } from "@rotorsoft/act-http/hono";
|
|
267
|
-
|
|
268
|
-
const broadcast = new BroadcastChannel<MyState>();
|
|
269
|
-
|
|
270
|
-
// Server-side: after every app.do(...), publish the derived state.
|
|
271
|
-
const snaps = await app.do(action, target, payload);
|
|
272
|
-
broadcast.publish(target.stream, deriveState(snaps), snaps.map((s) => s.patch).filter(Boolean));
|
|
273
|
-
|
|
274
|
-
// tRPC — emits `router.subscribe.<stateName>.useSubscription({ stream })`.
|
|
275
|
-
const router = trpc(app, {
|
|
276
|
-
actor,
|
|
277
|
-
stream,
|
|
278
|
-
sse: { channel: broadcast, maxConnections: 500, heartbeatMs: 30_000 },
|
|
279
|
-
});
|
|
176
|
+
}); // POST /api/actions/<action>
|
|
280
177
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
stream,
|
|
285
|
-
sse: { channel: broadcast },
|
|
286
|
-
});
|
|
178
|
+
api.get("/openapi.json", (c) =>
|
|
179
|
+
c.json(openapi(app, { info: { title: "My API", version: "1.0.0" } }))
|
|
180
|
+
);
|
|
287
181
|
```
|
|
288
182
|
|
|
289
|
-
|
|
183
|
+
Framework errors share one envelope (`{ error, detail?, code? }`) and one status table: `ValidationError` → 422, `InvariantError` → 409, `ConcurrencyError` → 412, `StreamClosedError` → 410, `NonRetryableError` → 400, anything else → 500. tRPC has no 410, so `StreamClosedError` is `NOT_FOUND` there. Optimistic concurrency (`expectedVersion`), `Idempotency-Key` handling, SSE subscriptions (`sse: { channel }`), auth composition and deployment recipes are in the [auto-generated API guide](https://rotorsoft.github.io/act-root/docs/guides/auto-generated-api).
|
|
290
184
|
|
|
291
185
|
### `sse` — live state broadcast
|
|
292
186
|
|
|
@@ -342,45 +236,14 @@ Each framework adapter exports a single function `webhookMiddleware(options)` th
|
|
|
342
236
|
| `/receiver/fastify` | `request.idempotency` | `reply.status(...).send({ error: reason })` |
|
|
343
237
|
| `/receiver/hono` | `c.get("idempotency")` (typed via `Variables`) | `c.json({ error: reason }, status)` |
|
|
344
238
|
|
|
345
|
-
### `/api`
|
|
346
|
-
|
|
347
|
-
Shared utilities consumed by every transport in the auto-generated API umbrella (act-http-api epic #835). Three concerns surfaced once, not per-transport:
|
|
348
|
-
|
|
349
|
-
- **`ActorExtractor`** — type alias `(request: unknown) => Actor | Promise<Actor>`. The host-supplied closure resolving an `Actor` from an incoming request. Required on every transport (`trpc(app, { actor })`, `hono(app, { actor })`). Auth (JWT, session, API key) stays in the host; the package only asks for this function.
|
|
350
|
-
- **`ApiError`** — uniform envelope `{ error, detail?, code? }` shipped over the wire by every transport. Hosts get the same shape from REST, tRPC, and OpenAPI.
|
|
351
|
-
- **`ERROR_MAP`** — `as const` table mapping framework error types to `{ status, code }`. `ValidationError → 422 / VALIDATION`, `InvariantError → 409 / INVARIANT`, `ConcurrencyError → 412 / CONCURRENCY`, `StreamClosedError → 410 / STREAM_CLOSED`, `NonRetryableError → 400 / NON_RETRYABLE`.
|
|
352
|
-
- **`toApiError(err) → { status, body }`** — the single mapping helper every transport calls in its error boundary. Known framework errors map per `ERROR_MAP`; everything else surfaces as 500 / `INTERNAL` (with `detail` only when the throw was an `Error` — thrown strings or objects don't leak payloads).
|
|
353
|
-
- **`withIdempotency(store, key, handler)`** — wraps an action handler in a two-phase `Idempotency-Key` claim: it `claim`s tentatively, runs the handler, then `commit`s the key on success or `release`s it and re-throws on a handler rejection (so a transient failure re-processes on retry rather than deduping into a silent success). Reuses `@rotorsoft/act-ops/idempotency` — same contract `@rotorsoft/act-http/receiver` already speaks, so one `IdempotencyStore` covers both halves of the "Act over the wire" surface. Returns `{ deduped: false, result }` on fresh claim, `{ deduped: true }` on duplicate (handler is not called).
|
|
354
|
-
- **`SseOptions`** — `{ channel: BroadcastChannel<S>, maxConnections?, heartbeatMs?, maxPendingPerConnection? }`. Shared SSE wiring options consumed by both `trpc(app, { sse })` and `hono(app, { sse })`. Defaults: `maxConnections=500` (validated `[1, 10_000]`), `heartbeatMs=30_000` (validated `[15_000, 300_000]`), `maxPendingPerConnection=256` (validated `[1, 100_000]`). Out-of-range values throw `RangeError` at transport construction so misconfiguration surfaces at startup, not at first connection.
|
|
355
|
-
- **`SseConnectionCounter`** — per-process slot counter. The 501st concurrent open is refused (`503 / SSE_BUSY` with `Retry-After: 1` on Hono, `TOO_MANY_REQUESTS` on tRPC). Internal — both transports construct one each, shared by every state-name's subscription on a single generator instance.
|
|
356
|
-
- **`runSseSubscription(channel, streamId, accounting?, signal?, on_cap_exceeded?)`** — the shared subscription loop both transports run. Acquires one slot (when an `accounting` is supplied), yields the cached state if present, then forwards every channel publication as a `{ kind: "patch", data }` frame until the consumer breaks or the signal aborts. Used internally; exported for adopters who want to build their own transport surface against the same accounting / cancellation discipline.
|
|
357
|
-
- **`resolveSseConfig(options)`** + **`DEFAULT_SSE_HEARTBEAT_MS`** / **`DEFAULT_SSE_MAX_CONNECTIONS`** — validation helper and exposed defaults.
|
|
358
|
-
- **Types**: `IdempotencyResult<T>`, `ErrorMapEntry`, `SseConfig`, `SseAccounting`, `SseSubscriptionFrame<S>`.
|
|
359
|
-
|
|
360
|
-
### `/trpc` subpath
|
|
361
|
-
|
|
362
|
-
Auto-generated tRPC router for the act-http-api epic (#835).
|
|
363
|
-
|
|
364
|
-
- **`trpc(app, options) → Router`** — generator function. Walks `app.registry.actions` once and emits a flat top-level mutation per action. Each procedure runs the internal `authenticated(options.actor)` middleware (so `ctx.actor: Actor` is set on the downstream context), resolves the target stream via `options.stream(action, input, ctx)`, and calls `app.do(action, { stream, actor }, input)`. Known framework errors map through `toApiError` to the conventional tRPC codes; unknown throws surface as `INTERNAL_SERVER_ERROR`.
|
|
365
|
-
- **`authenticated(extractor) → middleware`** — standalone export of the auth middleware the generator uses internally. Hosts composing their own procedure chain (logging + tracing + custom auth flavors) use `t.procedure.use(authenticated(extractor))` to inject `ctx.actor` without going through the generator. The middleware is structurally-typed so any host `t` instance accepts it.
|
|
366
|
-
- **`TrpcOptions<Ctx>`** — `{ actor, stream, expectedVersion?, idempotency?, sse? }`. `actor` is the `ActorExtractor` from `@rotorsoft/act-http/api`. `stream` returns the target stream per call. `expectedVersion` is optional — `(action, input, ctx) => number | undefined` — when set, the procedure threads the resolved value through `Target.expectedVersion` so `app.do` enforces optimistic concurrency; hosts typically read it from an `If-Match` header or the client's last-known snapshot, and returning `undefined` skips the check for that call. `idempotency` is optional — `{ store: IdempotencyStore; keyFrom: (ctx) => string | undefined }` — when set, the procedure honors `Idempotency-Key` via `withIdempotency`. Duplicate claims throw `CONFLICT` (the contract intentionally doesn't cache the original handler's result). `sse` is optional — when set, the generator emits one subscription per unique registered state name under the nested `router.subscribe.<stateName>` namespace; each yields `{ kind: "state", data }` once (when the channel has a cached state) and then `{ kind: "patch", data }` per channel publication until the consumer breaks. The cap surfaces as `TOO_MANY_REQUESTS`.
|
|
239
|
+
### `/api`, `/trpc`, `/hono`, `/openapi` subpaths
|
|
367
240
|
|
|
368
|
-
|
|
241
|
+
- **`/api`** — what every transport shares: `ActorExtractor`, the `ApiError` envelope, `ERROR_MAP` and `toApiError(err)`, `withIdempotency(store, key, handler)`, and the SSE wiring (`SseOptions`, `resolveSseConfig`, `runSseSubscription`). Bad SSE options throw `ZodError` when the transport is built.
|
|
242
|
+
- **`/trpc`** — `trpc(app, options)`, the `authenticated(extractor)` middleware, and `TrpcOptions`.
|
|
243
|
+
- **`/hono`** — `hono(app, options)`, the `authenticated(extractor)` middleware, `HonoOptions` and `ActMiddlewareVariables`. A body that fails its action's schema returns `422` with the `ApiError` envelope.
|
|
244
|
+
- **`/openapi`** — `openapi(app, options)`, which returns an OpenAPI 3.1 document describing the `hono` routes, plus `OpenAPIOptions` and the document types.
|
|
369
245
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
- **`hono(app, options) → Hono`** — generator function. Walks `app.registry.actions` once and registers a `POST /actions/<actionName>` per action under `basePath` (default `/api`). Body is validated with `@hono/zod-validator` against the action's Zod schema; failures short-circuit with `400`. The internal `authenticated(options.actor)` middleware stashes the resolved `Actor` under `c.get("actor")`. Routes return `200 Snapshot[]` on success, `4xx ApiError` envelope (with the conventional HTTP status) on framework errors, `500 / INTERNAL` on unknown throws.
|
|
373
|
-
- **`authenticated(extractor) → MiddlewareHandler`** — standalone export of the auth middleware the generator uses internally. Hosts composing their own Hono chain wire `api.use("*", authenticated(extractor))` and downstream routes read `c.get("actor"): Actor`. Errors thrown by the extractor surface as `401 / UNAUTHORIZED`.
|
|
374
|
-
- **`HonoOptions`** — `{ actor, stream, expectedVersion?, idempotency?, sse?, basePath? }`. `actor` is the `ActorExtractor`. `stream` returns the target stream per call. `expectedVersion` threads `Target.expectedVersion` for optimistic concurrency — returning `undefined` skips the check. `idempotency` is optional — `{ store: IdempotencyStore; keyFrom?: (c) => string | undefined }` — when set, the route honors `Idempotency-Key` via `withIdempotency`; default `keyFrom` reads the `Idempotency-Key` header. Duplicate claims return `409 / CONFLICT`. `sse` is optional — when set, the generator emits one `GET <basePath>/sse/<stateName>?stream=<id>` route per unique registered state name; each route opens a `text/event-stream`, yields the cached state (`event: state`) when present, then forwards every channel publication as `event: patch`. The connection cap surfaces as `503 / SSE_BUSY` with `Retry-After: 1`. A heartbeat ping every `heartbeatMs` keeps proxies from idling the connection out. `basePath` defaults to `/api`.
|
|
375
|
-
- **`ActMiddlewareVariables`** — `{ actor: Actor }`. Use as the `Variables` generic on a host Hono instance to get `c.get("actor")` typed downstream of `authenticated`.
|
|
376
|
-
|
|
377
|
-
### `/openapi` subpath
|
|
378
|
-
|
|
379
|
-
OpenAPI 3.1 document emitter for the act-http-api epic (#835).
|
|
380
|
-
|
|
381
|
-
- **`openapi(app, options) → OpenAPIDocument`** — generator function. Walks `app.registry.actions` once and returns a valid OpenAPI 3.1 document object. Each action becomes a `POST <basePath>/actions/<actionName>` operation; the request-body schema comes from Zod 4's `z.toJSONSchema` against the action's registered schema. Error responses reference the shared `#/components/responses/ApiError`. Output is pure data — serve as `/openapi.json`, write to disk during CI, pipe to client codegen.
|
|
382
|
-
- **`OpenAPIOptions`** — `{ info, servers?, basePath?, idempotency?, expectedVersion? }`. `info.title` and `info.version` are required non-empty strings. `servers[].url` may contain `{variable}` template syntax; bare URLs are validated through `URL`'s parser. `basePath` defaults to `/api` (mirrors the Hono sibling). `idempotency` and `expectedVersion` are booleans (default `false`); when `true`, every mutation operation documents the corresponding header — `Idempotency-Key` is marked **required** (the route 400s without it), `If-Match` is optional.
|
|
383
|
-
- **`OpenAPIDocument`** + a structural subset of the OpenAPI 3.1 types (`OpenAPIInfo`, `OpenAPIServer`, `OpenAPIPathItem`, `OpenAPIOperation`, `OpenAPIParameter`, `OpenAPIRequestBody`, `OpenAPIResponse`, `OpenAPIComponents`). Intentionally lighter than the full `openapi-types` package so post-processing doesn't fight the type system; cast to a fuller type when downstream consumers need it.
|
|
246
|
+
Every option is described in the [auto-generated API guide](https://rotorsoft.github.io/act-root/docs/guides/auto-generated-api).
|
|
384
247
|
|
|
385
248
|
### `/sse` subpath
|
|
386
249
|
|
|
@@ -442,14 +305,7 @@ Generic catch sites that don't care about HTTP specifics can match on the base `
|
|
|
442
305
|
|
|
443
306
|
### Recovering a blocked stream
|
|
444
307
|
|
|
445
|
-
When `webhook` blocks a stream
|
|
446
|
-
|
|
447
|
-
```ts
|
|
448
|
-
await app.unblock(["webhooks-out-customer-42"]); // by name
|
|
449
|
-
await app.unblock({ stream: "^webhooks-out-" }); // bulk by pattern
|
|
450
|
-
```
|
|
451
|
-
|
|
452
|
-
Use `app.blocked_streams()` to discover what's currently blocked.
|
|
308
|
+
When `webhook` blocks a stream (on a 4xx, or after exhausting retries), recover with `app.unblock(...)`, which resumes from where the stream stopped. Never `app.reset()` it: that replays from event 0 and re-fires every historical webhook. See [Blocked streams](https://rotorsoft.github.io/act-root/docs/concepts/error-handling#blocked-streams).
|
|
453
309
|
|
|
454
310
|
### SSE wire format
|
|
455
311
|
|