@btravstack/amqp-worker 0.10.0 → 0.11.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/README.md +71 -10
- package/dist/index.d.cts +14 -1
- package/dist/index.d.mts +14 -1
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -23,20 +23,79 @@ All six are peer dependencies — install them (`@opentelemetry/api` because
|
|
|
23
23
|
|
|
24
24
|
<!-- doctest: prelude
|
|
25
25
|
import { NonRetryableError, RetryableError } from "@amqp-contract/worker";
|
|
26
|
-
import {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
26
|
+
import {
|
|
27
|
+
defineContract,
|
|
28
|
+
defineEventConsumer,
|
|
29
|
+
defineEventPublisher,
|
|
30
|
+
defineExchange,
|
|
31
|
+
defineMessage,
|
|
32
|
+
defineQueue,
|
|
33
|
+
} from "@amqp-contract/contract";
|
|
34
|
+
import { Module, Port, type AnyModule } from "@btravstack/di";
|
|
35
|
+
import { TaggedError, type AsyncResult } from "unthrown";
|
|
32
36
|
import { observability } from "@btravstack/observability";
|
|
33
37
|
import { otel } from "@btravstack/observability/otel";
|
|
38
|
+
import { z } from "zod";
|
|
39
|
+
|
|
40
|
+
// The contract this README's worker serves: one event, broadcast to two
|
|
41
|
+
// subscribers' queues. It is the application's own artifact — a client takes
|
|
42
|
+
// it without this package.
|
|
43
|
+
const orders = defineExchange("orders");
|
|
44
|
+
const parked = defineExchange("orders-dlx", { type: "direct" });
|
|
45
|
+
const orderChanged = defineMessage(
|
|
46
|
+
z.object({
|
|
47
|
+
tenantId: z.uuidv7(),
|
|
48
|
+
kind: z.literal("order"),
|
|
49
|
+
id: z.uuidv7(),
|
|
50
|
+
occurredAt: z.string(),
|
|
51
|
+
payload: z.object({ quantity: z.number() }).nullable(),
|
|
52
|
+
}),
|
|
53
|
+
);
|
|
54
|
+
const orderChangedEvent = defineEventPublisher(orders, orderChanged, {
|
|
55
|
+
routingKey: "order.changed",
|
|
56
|
+
});
|
|
57
|
+
const queueOptions = {
|
|
58
|
+
deadLetter: { exchange: parked, externalConsumers: true },
|
|
59
|
+
retry: { mode: "ttl-backoff", maxRetries: 3, initialDelayMs: 10 },
|
|
60
|
+
} as const;
|
|
61
|
+
const orderContract = defineContract({
|
|
62
|
+
publishers: { orderChanged: orderChangedEvent },
|
|
63
|
+
consumers: {
|
|
64
|
+
orderNotifications: defineEventConsumer(
|
|
65
|
+
orderChangedEvent,
|
|
66
|
+
defineQueue("order-notifications", queueOptions),
|
|
67
|
+
),
|
|
68
|
+
orderAudit: defineEventConsumer(
|
|
69
|
+
orderChangedEvent,
|
|
70
|
+
defineQueue("order-audit", queueOptions),
|
|
71
|
+
),
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// The application's own layers, declared here for the same reason: the use
|
|
76
|
+
// case, its errors and its tenant id are yours, not this package's.
|
|
77
|
+
class DuplicateOrder extends TaggedError("DuplicateOrder")<{ readonly id: string }> {}
|
|
78
|
+
class InvalidOrderId extends TaggedError("InvalidOrderId")<{ readonly id: string }> {}
|
|
79
|
+
class InvalidQuantity extends TaggedError("InvalidQuantity")<{ readonly id: string }> {}
|
|
80
|
+
declare const TenantId: (value: string) => string;
|
|
81
|
+
class PlaceOrder extends Port("PlaceOrder")<{
|
|
82
|
+
readonly execute: (
|
|
83
|
+
tenantId: string,
|
|
84
|
+
id: string,
|
|
85
|
+
quantity: number,
|
|
86
|
+
) => AsyncResult<
|
|
87
|
+
{ readonly id: string },
|
|
88
|
+
DuplicateOrder | InvalidOrderId | InvalidQuantity
|
|
89
|
+
>;
|
|
90
|
+
}> {}
|
|
91
|
+
declare const OrderApplicationModule: Module<PlaceOrder, never, never>;
|
|
92
|
+
declare const OrderPersistenceModule: AnyModule;
|
|
34
93
|
|
|
35
94
|
// The application module the README's composition imports — the orders
|
|
36
95
|
// vertical plus its persistence, as an application would compose it.
|
|
37
96
|
const AppModule = Module("App")({
|
|
38
97
|
imports: [OrderApplicationModule, OrderPersistenceModule, observability(), otel()],
|
|
39
|
-
exports: [PlaceOrder,
|
|
98
|
+
exports: [PlaceOrder, Logger],
|
|
40
99
|
});
|
|
41
100
|
-->
|
|
42
101
|
|
|
@@ -102,9 +161,11 @@ rpc, typed by the key alone, and `AmqpHandlers(contract)([...])` composes an
|
|
|
102
161
|
array of them into the same handlers provider `AmqpModule` takes — the array
|
|
103
162
|
must cover every key the contract declares, and each piece's own port must
|
|
104
163
|
still be discharged (`provides`), since the composed provider's deps are the
|
|
105
|
-
pieces' ports, not what they close over.
|
|
106
|
-
|
|
107
|
-
|
|
164
|
+
pieces' ports, not what they close over. A consumer belongs to exactly one slice, and the
|
|
165
|
+
compiler holds only half of that: coverage is checked, injectivity is not, so
|
|
166
|
+
two slices claiming one key type-check together. It is di's duplicate-provider
|
|
167
|
+
defect at build once **both** pieces are discharged as providers in one graph
|
|
168
|
+
— wire in only one and the other's handler is silently never registered.
|
|
108
169
|
|
|
109
170
|
Its own test suite needs a **Docker daemon**: the specs run against the shared
|
|
110
171
|
`rabbitmq` container `internal/test-infra` starts once per machine and every
|
package/dist/index.d.cts
CHANGED
|
@@ -110,6 +110,19 @@ type KeyOfPiece<P> = P extends {
|
|
|
110
110
|
} ? K : never;
|
|
111
111
|
/** The consumers and rpcs no piece in `T` covers. */
|
|
112
112
|
type Uncovered<C extends AnyAmqpContract, T extends readonly PieceOf<C>[]> = Exclude<HandlerKeyOf<C>, KeyOfPiece<T[number]>>;
|
|
113
|
+
/**
|
|
114
|
+
* A refused array: as long as the array the caller wrote, its head the caller's
|
|
115
|
+
* own elements — which match — and its LAST element the marker paired with what
|
|
116
|
+
* is wrong.
|
|
117
|
+
*
|
|
118
|
+
* TypeScript compares two equal-length tuples element by element, so the extra
|
|
119
|
+
* diagnostic it reports lands on the trailing element and carries both the
|
|
120
|
+
* sentence and the missing key. A fixed two-element tuple named the key only
|
|
121
|
+
* when the array happened to be two elements long; every other arity was a
|
|
122
|
+
* length mismatch, and the developer diffed the contract against the array by
|
|
123
|
+
* hand.
|
|
124
|
+
*/
|
|
125
|
+
type Refuse<T extends readonly unknown[], Marker extends string, Detail> = T extends readonly [...infer Head, unknown] ? readonly [...Head, readonly [Marker, Detail]] : readonly [readonly [Marker, Detail]];
|
|
113
126
|
/**
|
|
114
127
|
* The composing arm. Declared LAST in the intersection below on purpose:
|
|
115
128
|
* TypeScript reports the last overload's failure, so a non-covering array is
|
|
@@ -119,7 +132,7 @@ type Uncovered<C extends AnyAmqpContract, T extends readonly PieceOf<C>[]> = Exc
|
|
|
119
132
|
* The marker is a sentence rather than a bare label because it prints last,
|
|
120
133
|
* after the caller's own several-hundred-character piece type.
|
|
121
134
|
*/
|
|
122
|
-
type Compose<C extends AnyAmqpContract> = <const T extends readonly PieceOf<C>[]>(pieces: [Uncovered<C, T>] extends [never] ? T :
|
|
135
|
+
type Compose<C extends AnyAmqpContract> = <const T extends readonly PieceOf<C>[]>(pieces: [Uncovered<C, T>] extends [never] ? T : Refuse<T, "UNCOVERED HANDLERS — the contract declares a consumer this array does not cover", Uncovered<C, T>>) => Provider<HandlersInstanceOf<C>, never, InstanceType<T[number]["port"]>> & {
|
|
123
136
|
readonly port: HandlersPortOf<C>;
|
|
124
137
|
};
|
|
125
138
|
/**
|
package/dist/index.d.mts
CHANGED
|
@@ -110,6 +110,19 @@ type KeyOfPiece<P> = P extends {
|
|
|
110
110
|
} ? K : never;
|
|
111
111
|
/** The consumers and rpcs no piece in `T` covers. */
|
|
112
112
|
type Uncovered<C extends AnyAmqpContract, T extends readonly PieceOf<C>[]> = Exclude<HandlerKeyOf<C>, KeyOfPiece<T[number]>>;
|
|
113
|
+
/**
|
|
114
|
+
* A refused array: as long as the array the caller wrote, its head the caller's
|
|
115
|
+
* own elements — which match — and its LAST element the marker paired with what
|
|
116
|
+
* is wrong.
|
|
117
|
+
*
|
|
118
|
+
* TypeScript compares two equal-length tuples element by element, so the extra
|
|
119
|
+
* diagnostic it reports lands on the trailing element and carries both the
|
|
120
|
+
* sentence and the missing key. A fixed two-element tuple named the key only
|
|
121
|
+
* when the array happened to be two elements long; every other arity was a
|
|
122
|
+
* length mismatch, and the developer diffed the contract against the array by
|
|
123
|
+
* hand.
|
|
124
|
+
*/
|
|
125
|
+
type Refuse<T extends readonly unknown[], Marker extends string, Detail> = T extends readonly [...infer Head, unknown] ? readonly [...Head, readonly [Marker, Detail]] : readonly [readonly [Marker, Detail]];
|
|
113
126
|
/**
|
|
114
127
|
* The composing arm. Declared LAST in the intersection below on purpose:
|
|
115
128
|
* TypeScript reports the last overload's failure, so a non-covering array is
|
|
@@ -119,7 +132,7 @@ type Uncovered<C extends AnyAmqpContract, T extends readonly PieceOf<C>[]> = Exc
|
|
|
119
132
|
* The marker is a sentence rather than a bare label because it prints last,
|
|
120
133
|
* after the caller's own several-hundred-character piece type.
|
|
121
134
|
*/
|
|
122
|
-
type Compose<C extends AnyAmqpContract> = <const T extends readonly PieceOf<C>[]>(pieces: [Uncovered<C, T>] extends [never] ? T :
|
|
135
|
+
type Compose<C extends AnyAmqpContract> = <const T extends readonly PieceOf<C>[]>(pieces: [Uncovered<C, T>] extends [never] ? T : Refuse<T, "UNCOVERED HANDLERS — the contract declares a consumer this array does not cover", Uncovered<C, T>>) => Provider<HandlersInstanceOf<C>, never, InstanceType<T[number]["port"]>> & {
|
|
123
136
|
readonly port: HandlersPortOf<C>;
|
|
124
137
|
};
|
|
125
138
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@btravstack/amqp-worker",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "The AMQP starter for @btravstack/core: the handlers as a port, one unit per delivery, and a drain the kernel's deadline releases",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"amqp",
|
|
@@ -57,19 +57,19 @@
|
|
|
57
57
|
"unthrown": "5.7.0",
|
|
58
58
|
"vitest": "4.1.11",
|
|
59
59
|
"zod": "4.5.3",
|
|
60
|
-
"@btravstack/config": "0.
|
|
61
|
-
"@btravstack/core": "0.
|
|
62
|
-
"@btravstack/di": "0.
|
|
60
|
+
"@btravstack/config": "0.11.0",
|
|
61
|
+
"@btravstack/core": "0.11.0",
|
|
62
|
+
"@btravstack/di": "0.11.0",
|
|
63
63
|
"@btravstack/internal-test-infra": "0.0.0",
|
|
64
|
-
"@btravstack/testing": "0.
|
|
64
|
+
"@btravstack/testing": "0.11.0"
|
|
65
65
|
},
|
|
66
66
|
"peerDependencies": {
|
|
67
67
|
"@amqp-contract/worker": "^3.0.0-beta.7",
|
|
68
68
|
"@opentelemetry/api": "^1",
|
|
69
69
|
"unthrown": "^5.0.0",
|
|
70
|
-
"@btravstack/config": "^0.
|
|
71
|
-
"@btravstack/core": "^0.
|
|
72
|
-
"@btravstack/di": "^0.
|
|
70
|
+
"@btravstack/config": "^0.11.0",
|
|
71
|
+
"@btravstack/core": "^0.11.0",
|
|
72
|
+
"@btravstack/di": "^0.11.0"
|
|
73
73
|
},
|
|
74
74
|
"engines": {
|
|
75
75
|
"node": ">=22"
|