You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
docs: sync docs, READMEs, and agent rules with the 3.0 API
- fix non-compiling .getOrThrow() on E=never in the quick starts and the
stale unthrown-4 recover() comment
- add the missing MessageValidationError arm to the request-reply
tutorial's errCases snippets and prose
- document worker close() draining (drainTimeoutMs), middleware arrays,
standalone topology in defineContract (+ setupAmqpTopology escape
hatch), the { data, message? } RPC error shape, curated
ConsumerOptions, strict connectTimeoutMs, AsyncAPI 3.1 and the
failOnMissingConverter default flip, and the new exports
- AGENTS.md: fix the invariant-5 test pointer and extend the
load-bearing invariant map to 16 entries covering the new guards
Copy file name to clipboardExpand all lines: .agents/rules/handlers.md
+7-7Lines changed: 7 additions & 7 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -38,7 +38,7 @@ All handlers (consumer and RPC) receive a third `helpers` argument — `{ contex
38
38
39
39
When the RPC declares an `errors` map (`defineRpc(queue, { request, response, errors })`), the handler may also return `Err(rpcError(code, data))` for a declared code — the worker validates `data` against the declared schema, publishes an error reply (marked by the `RPC_ERROR_CODE_HEADER` header), and **acks the request**; typed business errors never enter the retry/DLQ pipeline. Undeclared codes or invalid error data are contract violations routed to the DLQ. See `packages/worker/src/worker.ts` (`publishRpcErrorReply`) and [docs/guide/error-model.md](../../docs/guide/error-model.md#typed-rpc-errors-rpcerror).
40
40
41
-
You can define RPC handlers either with `defineHandler` / `defineHandlers` (overloaded against `InferRpcNames<TContract>`, and `validateHandlerTargetExists` checks both `contract.consumers` and `contract.rpcs`) or inline inside `TypedAmqpWorker.create({ handlers: { … } })`. The inline `handlers` parameter is typed against `WorkerInferHandlers<TContract>`, so each name (consumer or RPC) gets the correct signature inferred:
41
+
You can define RPC handlers either with `declareHandler` / `declareHandlers` (overloaded against `InferRpcNames<TContract>`, and `validateHandlerTargetExists` checks both `contract.consumers` and `contract.rpcs`) or inline inside `TypedAmqpWorker.create({ handlers: { … } })`. The inline `handlers` parameter is typed against `WorkerInferHandlers<TContract>`, so each name (consumer or RPC) gets the correct signature inferred:
-**Client-side timeout** → call resolves to `Err(RpcTimeoutError)`; pending state is cleared. If a reply still arrives, it's logged at `warn` and counted via `recordLateRpcReply` (telemetry hook for tuning) — it's not retried.
92
92
-**Client closed mid-call** → call resolves to `Err(RpcCancelledError)`.
93
93
94
-
## Using `defineHandler` / `defineHandlers`
94
+
## Using `declareHandler` / `declareHandlers`
95
95
96
-
Use `defineHandler` (single) or `defineHandlers` (object) for full type inference and a runtime check that the name exists in the contract. Both helpers accept consumer **and** RPC names — they're overloaded against `InferConsumerNames` and `InferRpcNames`, and `validateHandlerTargetExists` inspects both `contract.consumers` and `contract.rpcs` (an unknown name throws _"Handler target X not found in contract"_).
96
+
Use `declareHandler` (single) or `declareHandlers` (object) for full type inference and a runtime check that the name exists in the contract. Both helpers accept consumer **and** RPC names — they're overloaded against `InferConsumerNames` and `InferRpcNames`, and `validateHandlerTargetExists` inspects both `contract.consumers` and `contract.rpcs` (an unknown name throws _"Handler target X not found in contract"_).
@@ -201,5 +201,5 @@ For the authoritative list, read [`packages/worker/src/index.ts`](../../packages
201
201
202
202
- Classes: `TypedAmqpWorker`, `RetryableError`, `NonRetryableError`, `MessageValidationError` (the error classes are unthrown `TaggedError`s). `HandlerError` is a **type** (`RetryableError | NonRetryableError`), not a class.
Copy file name to clipboardExpand all lines: .agents/rules/recipes.md
+3-3Lines changed: 3 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -8,17 +8,17 @@ End-to-end how-tos for the changes that come up most. Each recipe lists the exac
8
8
2.**Queue** — `defineQueue(...)` with a `deadLetter` and a `retry` mode (immediate-requeue or ttl-backoff). Quorum by default; classic only if you need priority/exclusive/auto-delete.
9
9
3.**Consumer entry** — `defineEventConsumer(eventPublisher, queue, { routingKey: ... })`. The queue↔exchange binding is auto-generated.
10
10
4.**Add to `defineContract`** under `consumers: { ... }`. Don't add the queue or binding yourself — they're auto-extracted.
11
-
5.**Handler** — implement with `defineHandler(contract, "yourConsumerName", ({ payload, headers }) => …)` returning `AsyncResult<void, HandlerError>`. See [handlers.md](./handlers.md).
11
+
5.**Handler** — implement with `declareHandler(contract, "yourConsumerName", ({ payload, headers }) => …)` returning `AsyncResult<void, HandlerError>`. See [handlers.md](./handlers.md).
12
12
6.**Tests** — integration test in `src/__tests__/<consumer>.spec.ts` using `it` from `@amqp-contract/testing/extension`. Mock the handler with `vi.fn().mockReturnValue(OkAsync(undefined))`.
13
13
7.**Changeset** — `pnpm changeset` with a minor bump. Public API surface grew.
14
14
15
15
## Add a new RPC
16
16
17
17
1.**Schemas** — request and response, both via `defineMessage`.
18
18
2.**Queue** — `defineQueue(...)` for the RPC. Quorum by default. **Configure a `deadLetter`** even though replies, not the queue, drive most failure modes: missing `replyTo` / `correlationId` and response-schema mismatches are surfaced as `NonRetryableError` and the worker `nack`s them without requeue, so without a DLX they're dropped silently.
3.**RPC entry** — `defineRpc(queue, { request, response })`. Typed business errors go in an optional `errors` map whose entries are `{ data: schema, message?: string }` (the raw Standard Schema, NOT `defineMessage`); the optional `message` is the default human message when the handler constructs the error without one.
20
20
4.**Add to `defineContract`** under `rpcs: { ... }`.
21
-
5.**Server-side handler** — define it with `defineHandler(contract, "yourRpcName", ({ payload }) => OkAsync({ /* response */ }))`, via `defineHandlers`, or inline in the `handlers` object passed to `TypedAmqpWorker.create({ handlers: { … } })`. All three are RPC-aware: `defineHandler` / `defineHandlers` are overloaded against `InferRpcNames` and validate the name against both `contract.consumers` and `contract.rpcs`. The worker validates the response against the response schema and publishes back automatically.
21
+
5.**Server-side handler** — define it with `declareHandler(contract, "yourRpcName", ({ payload }) => OkAsync({ /* response */ }))`, via `declareHandlers`, or inline in the `handlers` object passed to `TypedAmqpWorker.create({ handlers: { … } })`. All three are RPC-aware: `declareHandler` / `declareHandlers` are overloaded against `InferRpcNames` and validate the name against both `contract.consumers` and `contract.rpcs`. The worker validates the response against the response schema and publishes back automatically.
7.**Tests** — round-trip integration test (worker + client both wired up). For "no server" scenarios, just create the client without a worker; for "request validation fails", pass a deliberately wrong payload through `as unknown as ...`.
5.**Validation failures bypass the retry pipeline** (deterministic poison → DLQ on first delivery, zero retry publishes) — `packages/worker/src/__tests__/worker-retry.spec.ts` ("an invalid payload on a retry-configured queue goes straight to the DLQ with zero retries").
94
94
6.**A message is acked/nacked exactly once** — `packages/worker/src/__tests__/worker-double-ack.spec.ts`.
95
95
7.**Middleware short-circuit skips the handler; its result routes like a handler result** — `packages/worker/src/middleware.spec.ts` + `tests/src/middleware.spec.ts`.
96
96
8.**Middleware-substituted payloads are re-validated before the handler** — `packages/worker/src/middleware.spec.ts` ("threads substituted payloads…") + `tests/src/middleware.spec.ts` ("blocks handler execution when the substitution fails the schema").
97
-
9.**RPC replies require `replyTo` + `correlationId`; undeclared error codes/invalid error data → DLQ, never a malformed reply** — `tests/src/rpc.spec.ts` (undeclared-code and invalid-error-data timeout tests).
98
-
10.**Worker creation fails fast on missing handlers, before any connection is acquired** — `packages/worker/src/worker-cleanup.spec.ts`.
97
+
9.**RPC replies require `replyTo` + `correlationId`; undeclared error codes/invalid error data → DLQ, never a malformed reply** — `tests/src/rpc.spec.ts` (undeclared-code/invalid-error-data timeout tests + "RPC DLQ routing" describe: DLQ delivery for undeclared codes and reply-less requests).
98
+
10.**Worker creation fails fast on missing or non-callable handlers, before any connection is acquired** — `packages/worker/src/worker-cleanup.spec.ts`.
99
99
11.**`createContext` failure routes to DLQ; the handler never runs** — `tests/src/middleware.spec.ts` ("routes a throwing createContext…").
100
+
12.**Compressed and plain payloads survive the wire byte-for-byte** (the channel must never JSON-serialize a Buffer) — `tests/src/compression.spec.ts`.
101
+
13.**`worker.close()` drains in-flight handlers before closing the channel** (their acks land; no redelivery of completed work) — `packages/worker/src/__tests__/worker-close-drain.spec.ts`.
102
+
14.**A classic-queue immediate-requeue retry republishes to THIS queue via the default exchange** (never the original exchange — sibling queues must not see duplicates) — `packages/worker/src/invariants.spec.ts`.
103
+
15.**Connection-pool leases are idempotent and close/create-race-safe** (double release never underflows; an acquire racing the last release gets a fresh connection) — `packages/core/src/connection-manager.spec.ts`.
104
+
16.**Telemetry never throws into the data path** (a buggy provider degrades to "no telemetry") — `packages/core/src/telemetry.spec.ts` ("throwing telemetry providers") + `packages/client/src/interceptors.spec.ts` (sync-throwing interceptor → Defect).
// publish() returns an AsyncResult instead of throwing — awaiting it yields a
94
-
// Result. unthrown 4 gates get() to infallible results, so clear the error
95
-
// channel with recover() first (or use .match()). See the error model guide.
94
+
// Result. create()/close() have an empty error channel (E = never), so .get()
95
+
// is correct there; publish() still has a modeled error, so extract it with
96
+
// .getOrThrow() (or handle it with .match()). See the error model guide.
96
97
awaitclient
97
98
.publish("orderCreated", {
98
99
orderId: "ORD-123", // ✅ TypeScript knows!
@@ -101,8 +102,8 @@ await client
101
102
.getOrThrow();
102
103
103
104
// 7. Clean up
104
-
awaitclient.close().getOrThrow();
105
-
awaitworker.close().getOrThrow();
105
+
awaitclient.close().get();
106
+
awaitworker.close().get();
106
107
```
107
108
108
109
▶ For the full runnable version (including the RabbitMQ Docker command), follow the [5-minute quick start](https://btravstack.github.io/amqp-contract/guide/getting-started).
The two type parameters on `defineMiddleware` are the context going in and the context coming out. Without any middleware or `createContext`, handlers get `EmptyContext`.
49
+
The two type parameters on `declareMiddleware` are the context going in and the context coming out. Without any middleware or `createContext`, handlers get `EmptyContext`.
`composeMiddleware(outermost, …, innermost)` runs left to right. Context types accumulate along the chain, so `timing` sees the `tenantId` that `auth` added, and handlers see the final accumulated type.
65
65
66
+
The `middleware` option also accepts an array directly — `middleware: [auth, timing]` — with the first entry outermost, mirroring the client's interceptor arrays. At runtime it composes exactly like `composeMiddleware(...)`, but the array form does not thread the stepwise context types across entries: when middleware accumulate typed context for the handlers, pre-compose with `composeMiddleware` so the chain's final context type is inferred into `helpers.context`.
67
+
66
68
Because `next()` returns the handler's `AsyncResult`, a middleware can post-process it with `.tap`, `.mapErr` or `.flatMapErr`.
67
69
68
70
## Inject dependencies per message
@@ -97,7 +99,7 @@ For a dependency-injection graph, [demesne](https://btravstack.github.io/demesne
97
99
`next({ payload })` replaces the payload for everything downstream.
0 commit comments