@hexok/integration-events 0.0.0-stage → 0.1.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 CHANGED
@@ -1,3 +1,136 @@
1
- # Temporary Holding Version
1
+ # @hexok/integration-events
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Versioned events that cross a product or service boundary. Install with `npm i @hexok/integration-events`. It publishes and subscribes through the `@hexok/queue` port, so any `Queue` adapter carries it, such as `@hexok/queue-redis`.
4
+
5
+ A domain event is internal to one application and changes with it. An integration event is a published contract between applications that deploy on their own. Map a domain event to an integration event at the edge. Do not publish the domain event.
6
+
7
+ ## Define the contract
8
+
9
+ The producing product owns the contract. The contract file imports `@hexok/integration-events`, a schema library, and nothing else from the product. Consumers import it from a package the producer publishes, from a shared workspace, or from a copy.
10
+
11
+ ```ts
12
+ import { Contract, Message } from '@hexok/integration-events';
13
+ import { z } from 'zod';
14
+
15
+ export class InvoicePaid extends Message('invoice.paid', 1, z.object({
16
+ invoiceId: z.string(),
17
+ amountCents: z.number().int(),
18
+ })) {}
19
+
20
+ export class InvoiceVoided extends Message('invoice.voided', 1, z.object({
21
+ invoiceId: z.string(),
22
+ })) {}
23
+
24
+ export class Billing extends Contract('billing', {
25
+ invoices: [InvoicePaid, InvoiceVoided],
26
+ }) {}
27
+ ```
28
+
29
+ `Message(type, version, schema)` is a hexok `Event` with a static and instance `version`. `parse` and `EventHandler` work on it.
30
+
31
+ `Contract(name, topics)` maps each key to the message classes on that topic. The queue topic is `<name>.<key>`, here `billing.invoices`. Names match `/^[A-Za-z0-9_-]+$/`. `Contract` throws when a topic is empty, when a class is not a `Message`, when a class is in two topics, or when two classes share a type and version in one topic.
32
+
33
+ ## Publish
34
+
35
+ ```ts
36
+ import { IntegrationPublisher } from '@hexok/integration-events';
37
+
38
+ const billing = new IntegrationPublisher(Billing, { queue });
39
+ await billing.publish(new InvoicePaid({ invoiceId: 'inv_1', amountCents: 500 }));
40
+ ```
41
+
42
+ `publish` takes any message in the contract and finds its topic. It validates the payload and throws `CodedError` `VALIDATION` before anything is sent. It resolves the envelope `id`.
43
+
44
+ Pass `{ id, time, subject }` to set those envelope attributes. `id` defaults to a random UUID. Pass a stable id, such as an outbox row id, so a retried publish carries the same id. `subject` is usually the aggregate id.
45
+
46
+ Use cases depend on a port. `ContractEvent<typeof Billing>` is the union of messages the publisher accepts.
47
+
48
+ ```ts
49
+ abstract publish(event: ContractEvent<typeof Billing>): Promise<string>;
50
+ ```
51
+
52
+ ## Subscribe
53
+
54
+ ```ts
55
+ import { EventHandler } from 'hexok';
56
+ import { IntegrationSubscriber, type IntegrationMeta } from '@hexok/integration-events';
57
+ import { Billing, InvoicePaid } from '@acme/billing-contract';
58
+
59
+ class SyncInvoice extends EventHandler('sync-invoice', InvoicePaid) {
60
+ async handle(event: InvoicePaid, meta?: IntegrationMeta): Promise<void> {
61
+ // meta.id to dedupe, meta.time and meta.subject to order
62
+ }
63
+ }
64
+
65
+ const billing = new IntegrationSubscriber(Billing, { queue, service: 'crm' });
66
+ await billing.subscribe({ maxDeliveries: 10, handlers: [new SyncInvoice(deps)] });
67
+ ```
68
+
69
+ `service` names the consuming application. Each handler subscribes to the topic of its message with its own consumer group, `<service>:<handler token>`. Two services never share a group, so each receives every event. A failing handler does not redeliver to the others.
70
+
71
+ `subscribe` checks every handler before it subscribes any. It throws when a handler's message is not in the contract, or when two handlers would share a group on one topic. It resolves `{ topic, group, type, version }` for each subscription. `maxDeliveries` and `startFrom` pass through to `Queue.subscribe`.
72
+
73
+ For each delivery:
74
+
75
+ - An event of another type or version is acked and skipped.
76
+ - A payload that is not an envelope, or data the message schema rejects, is logged, parked, and dropped. Pass a structural `logger` and `quarantine`. `@hexok/logger` and `@hexok/quarantine` fit them. A parked record has `source: 'integration-events'`, `operation: 'deliver'`, `subject` `topic:id`, and `context.group`. A schema rejection has `error.code` `VALIDATION`.
77
+ - `handle` receives the event and an `IntegrationMeta` of `{ id, source, time, subject }`. A throw follows the queue's retry rules.
78
+
79
+ Delivery is at least once. Make handlers idempotent with `meta.id`.
80
+
81
+ ## Versions
82
+
83
+ An additive change, such as a new optional field, keeps the version. Consumer schemas must accept unknown fields. Zod, Valibot, and TypeBox objects accept them by default.
84
+
85
+ A breaking change is a new version of the same type, with a downgrade to the previous version.
86
+
87
+ ```ts
88
+ export class InvoicePaidV2 extends Message('invoice.paid', 2, z.object({
89
+ invoiceId: z.string(),
90
+ amount: z.object({ cents: z.number().int(), currency: z.string() }),
91
+ }), {
92
+ downgrade: {
93
+ to: InvoicePaid,
94
+ map: (v2) => ({ invoiceId: v2.invoiceId, amountCents: v2.amount.cents }),
95
+ },
96
+ }) {}
97
+
98
+ export class Billing extends Contract('billing', {
99
+ invoices: [InvoicePaid, InvoicePaidV2, InvoiceVoided],
100
+ }) {}
101
+ ```
102
+
103
+ `map` is typed from both schemas. One `publish` of v2 sends v2, then v1. Every copy has the same `id`, `time`, and `subject`. Each payload is validated before it is downgraded, and a failure throws before anything is sent. A queue failure rejects `publish`, and copies already sent stay sent.
104
+
105
+ `publish` accepts only the newest version. An older version is a type error, and it throws at runtime. `ContractEvent` leaves it out.
106
+
107
+ The versions of one type in a topic form a chain. Each version downgrades to the one before it. `Contract` throws when a version has no downgrade, skips a version, or downgrades to a version that is not in the topic.
108
+
109
+ To retire v1, move every consumer to v2. Then remove v1 from the topic and the downgrade from v2. Publish that as a major version of the contract.
110
+
111
+ ## Envelope
112
+
113
+ The queue payload is a CloudEvents 1.0 event in structured JSON mode, with a `version` extension attribute. Applications in other languages read it as JSON.
114
+
115
+ ```json
116
+ {
117
+ "specversion": "1.0",
118
+ "id": "7f8d3a5e-…",
119
+ "source": "billing",
120
+ "type": "invoice.paid",
121
+ "version": 1,
122
+ "time": "2026-11-05T12:00:00.000Z",
123
+ "subject": "inv_1",
124
+ "data": { "invoiceId": "inv_1", "amountCents": 500 }
125
+ }
126
+ ```
127
+
128
+ `subject` is present only when the publisher passed one.
129
+
130
+ ## What it leaves to the application
131
+
132
+ - Retention. The queue adapter decides how long entries stay. With `@hexok/queue-redis`, the publisher's `retentionMs` and `maxLen` decide it, and a consumer that is down longer loses entries.
133
+ - Backfill. A new consumer reaches only the entries the queue still holds. Load earlier state from the producer another way.
134
+ - Order. A queue can redeliver an entry after newer ones. Put an aggregate version or timestamp in the payload, and ignore a change older than the state you hold.
135
+ - The outbox. A crash between a database commit and `publish` loses the event. A transactional outbox prevents that. Pass the outbox row id as `id`.
136
+ - Topic ownership. Grant the producer write access to its topics and consumers read access, in the broker.
@@ -0,0 +1,49 @@
1
+ import type { MessageConstructor } from './message.js';
2
+ /** A topic as a {@link Contract} stores it. `name` is the queue topic. */
3
+ export interface ContractTopic<Name extends string = string, Messages extends readonly MessageConstructor[] = readonly MessageConstructor[]> {
4
+ readonly name: Name;
5
+ readonly messages: Messages;
6
+ }
7
+ /** What `IntegrationPublisher` and `IntegrationSubscriber` read. */
8
+ export interface ContractDefinition {
9
+ readonly token: string;
10
+ readonly topics: Readonly<Record<string, ContractTopic>>;
11
+ }
12
+ type ResolvedTopics<Name extends string, Topics extends Record<string, readonly MessageConstructor[]>> = {
13
+ readonly [K in keyof Topics & string]: ContractTopic<`${Name}.${K}`, Topics[K]>;
14
+ };
15
+ /** Every message class in a contract. */
16
+ export type ContractMessage<C extends ContractDefinition> = C['topics'][keyof C['topics']]['messages'][number];
17
+ type Superseded<M> = M extends {
18
+ readonly downgrade: {
19
+ readonly to: infer To;
20
+ };
21
+ } ? To : never;
22
+ /**
23
+ * Instance of a message a publisher accepts: every message in the contract
24
+ * except a version that a newer version downgrades to.
25
+ *
26
+ * ```ts
27
+ * abstract publish(event: ContractEvent<typeof Billing>): Promise<string>
28
+ * ```
29
+ */
30
+ export type ContractEvent<C extends ContractDefinition> = InstanceType<Exclude<ContractMessage<C>, Superseded<ContractMessage<C>>>>;
31
+ /**
32
+ * The published events of one product. Each key is a topic. Its queue topic is
33
+ * `<name>.<key>`. Each message class belongs to one topic. Versions of one type
34
+ * in a topic form a chain, where each version downgrades to the one before it.
35
+ *
36
+ * ```ts
37
+ * class Billing extends Contract('billing', {
38
+ * invoices: [InvoicePaid, InvoiceVoided],
39
+ * }) {}
40
+ * ```
41
+ */
42
+ export declare function Contract<const Name extends string, const Topics extends Record<string, readonly MessageConstructor[]>>(name: Name, topics: Topics): (abstract new () => {}) & {
43
+ /** Contract name. The envelope `source` and the queue topic prefix. */
44
+ readonly token: Name;
45
+ /** Topics by key. Each has its queue topic `name` and its `messages`. */
46
+ readonly topics: ResolvedTopics<Name, Topics>;
47
+ };
48
+ export {};
49
+ //# sourceMappingURL=contract.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAIvD,0EAA0E;AAC1E,MAAM,WAAW,aAAa,CAC5B,IAAI,SAAS,MAAM,GAAG,MAAM,EAC5B,QAAQ,SACN,SAAS,kBAAkB,EAAE,GAAG,SAAS,kBAAkB,EAAE;IAE/D,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IACpB,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;CAC7B;AAED,oEAAoE;AACpE,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC,CAAC;CAC1D;AAED,KAAK,cAAc,CACjB,IAAI,SAAS,MAAM,EACnB,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,SAAS,kBAAkB,EAAE,CAAC,IAC1D;IACF,QAAQ,EAAE,CAAC,IAAI,MAAM,MAAM,GAAG,MAAM,GAAG,aAAa,CAClD,GAAG,IAAI,IAAI,CAAC,EAAE,EACd,MAAM,CAAC,CAAC,CAAC,CACV;CACF,CAAC;AAEF,yCAAyC;AACzC,MAAM,MAAM,eAAe,CAAC,CAAC,SAAS,kBAAkB,IACtD,CAAC,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC;AAErD,KAAK,UAAU,CAAC,CAAC,IAAI,CAAC,SAAS;IAC7B,QAAQ,CAAC,SAAS,EAAE;QAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;CAC/C,GACG,EAAE,GACF,KAAK,CAAC;AAEV;;;;;;;GAOG;AACH,MAAM,MAAM,aAAa,CAAC,CAAC,SAAS,kBAAkB,IAAI,YAAY,CACpE,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,EAAE,UAAU,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,CAC5D,CAAC;AAEF;;;;;;;;;;GAUG;AACH,wBAAgB,QAAQ,CACtB,KAAK,CAAC,IAAI,SAAS,MAAM,EACzB,KAAK,CAAC,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,SAAS,kBAAkB,EAAE,CAAC,EAClE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM;IA+BxB,uEAAuE;oBAChD,IAAI;IAC3B,yEAAyE;qBACjD,cAAc,CAAC,IAAI,EAAE,MAAM,CAAC;EAIvD"}
@@ -0,0 +1,84 @@
1
+ const namePattern = /^[A-Za-z0-9_-]+$/;
2
+ /**
3
+ * The published events of one product. Each key is a topic. Its queue topic is
4
+ * `<name>.<key>`. Each message class belongs to one topic. Versions of one type
5
+ * in a topic form a chain, where each version downgrades to the one before it.
6
+ *
7
+ * ```ts
8
+ * class Billing extends Contract('billing', {
9
+ * invoices: [InvoicePaid, InvoiceVoided],
10
+ * }) {}
11
+ * ```
12
+ */
13
+ export function Contract(name, topics) {
14
+ assertName('contract', name);
15
+ const owner = new Map();
16
+ const resolved = {};
17
+ for (const [key, messages] of Object.entries(topics)) {
18
+ assertName('topic', key);
19
+ const topic = `${name}.${key}`;
20
+ if (messages.length === 0) {
21
+ throw new Error(`integration-events: topic "${topic}" has no messages`);
22
+ }
23
+ for (const message of messages) {
24
+ assertMessage(topic, message);
25
+ const previous = owner.get(message);
26
+ if (previous !== undefined) {
27
+ throw new Error(`integration-events: "${message.token}" v${message.version} is in topics "${previous}" and "${topic}"`);
28
+ }
29
+ owner.set(message, topic);
30
+ }
31
+ assertVersionChains(topic, messages);
32
+ resolved[key] = Object.freeze({
33
+ name: topic,
34
+ messages: Object.freeze([...messages]),
35
+ });
36
+ }
37
+ const frozen = Object.freeze(resolved);
38
+ class ContractClass {
39
+ /** Contract name. The envelope `source` and the queue topic prefix. */
40
+ static token = name;
41
+ /** Topics by key. Each has its queue topic `name` and its `messages`. */
42
+ static topics = frozen;
43
+ }
44
+ return ContractClass;
45
+ }
46
+ function assertName(kind, name) {
47
+ if (typeof name !== 'string' || !namePattern.test(name)) {
48
+ throw new Error(`integration-events: ${kind} name "${String(name)}" must match ${namePattern}`);
49
+ }
50
+ }
51
+ function assertMessage(topic, message) {
52
+ const candidate = message;
53
+ if (typeof message !== 'function' ||
54
+ typeof candidate.token !== 'string' ||
55
+ typeof candidate.version !== 'number' ||
56
+ !('downgrade' in candidate)) {
57
+ throw new Error(`integration-events: topic "${topic}" holds a class that is not a Message`);
58
+ }
59
+ }
60
+ function assertVersionChains(topic, messages) {
61
+ const byType = new Map();
62
+ for (const message of messages) {
63
+ const versions = byType.get(message.token) ?? [];
64
+ versions.push(message);
65
+ byType.set(message.token, versions);
66
+ }
67
+ for (const [type, versions] of byType) {
68
+ versions.sort((a, b) => a.version - b.version);
69
+ versions.forEach((message, index) => {
70
+ const previous = versions[index - 1];
71
+ if (previous?.version === message.version) {
72
+ throw new Error(`integration-events: "${type}" v${message.version} is in topic "${topic}" twice`);
73
+ }
74
+ const to = message.downgrade?.to;
75
+ if (previous === undefined && to !== undefined) {
76
+ throw new Error(`integration-events: "${type}" v${message.version} downgrades to v${to.version}, which is not in topic "${topic}"`);
77
+ }
78
+ if (previous !== undefined && to !== previous) {
79
+ throw new Error(`integration-events: "${type}" v${message.version} must downgrade to the v${previous.version} in topic "${topic}"`);
80
+ }
81
+ });
82
+ }
83
+ }
84
+ //# sourceMappingURL=contract.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contract.js","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The queue payload of one integration event. It is a CloudEvents 1.0 event in
3
+ * structured JSON mode, with the `version` extension attribute.
4
+ */
5
+ export interface IntegrationEnvelope {
6
+ readonly specversion: '1.0';
7
+ /** Shared by every version that one `publish` emits. */
8
+ readonly id: string;
9
+ /** Contract name. */
10
+ readonly source: string;
11
+ /** Message type. */
12
+ readonly type: string;
13
+ /** Message version. */
14
+ readonly version: number;
15
+ /** ISO 8601 timestamp. */
16
+ readonly time: string;
17
+ /** Present only when the publisher passed one, such as an aggregate id. */
18
+ readonly subject?: string;
19
+ readonly data: unknown;
20
+ }
21
+ /** Envelope attributes a handler receives as its second argument. */
22
+ export interface IntegrationMeta {
23
+ readonly id: string;
24
+ readonly source: string;
25
+ readonly time: string;
26
+ readonly subject?: string;
27
+ }
28
+ /** The envelope, or `undefined` when `value` is not one. */
29
+ export declare function readEnvelope(value: unknown): IntegrationEnvelope | undefined;
30
+ export declare function metaOf(envelope: IntegrationEnvelope): IntegrationMeta;
31
+ //# sourceMappingURL=envelope.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"envelope.d.ts","sourceRoot":"","sources":["../src/envelope.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,WAAW,EAAE,KAAK,CAAC;IAC5B,wDAAwD;IACxD,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,qBAAqB;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,oBAAoB;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,uBAAuB;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,0BAA0B;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,2EAA2E;IAC3E,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED,qEAAqE;AACrE,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,4DAA4D;AAC5D,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,mBAAmB,GAAG,SAAS,CAa5E;AAED,wBAAgB,MAAM,CAAC,QAAQ,EAAE,mBAAmB,GAAG,eAAe,CASrE"}
@@ -0,0 +1,31 @@
1
+ /** The envelope, or `undefined` when `value` is not one. */
2
+ export function readEnvelope(value) {
3
+ if (typeof value !== 'object' || value === null)
4
+ return undefined;
5
+ const entry = value;
6
+ if (entry.specversion !== '1.0')
7
+ return undefined;
8
+ for (const key of ['id', 'source', 'type', 'time']) {
9
+ if (typeof entry[key] !== 'string')
10
+ return undefined;
11
+ }
12
+ if (!Number.isSafeInteger(entry.version))
13
+ return undefined;
14
+ if (entry.subject !== undefined && typeof entry.subject !== 'string') {
15
+ return undefined;
16
+ }
17
+ if (!('data' in entry))
18
+ return undefined;
19
+ return value;
20
+ }
21
+ export function metaOf(envelope) {
22
+ const meta = {
23
+ id: envelope.id,
24
+ source: envelope.source,
25
+ time: envelope.time,
26
+ };
27
+ return envelope.subject === undefined
28
+ ? meta
29
+ : { ...meta, subject: envelope.subject };
30
+ }
31
+ //# sourceMappingURL=envelope.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"envelope.js","sourceRoot":"","sources":["../src/envelope.ts"],"names":[],"mappings":"AA6BA,4DAA4D;AAC5D,MAAM,UAAU,YAAY,CAAC,KAAc;IACzC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,SAAS,CAAC;IAClE,MAAM,KAAK,GAAG,KAAgC,CAAC;IAC/C,IAAI,KAAK,CAAC,WAAW,KAAK,KAAK;QAAE,OAAO,SAAS,CAAC;IAClD,KAAK,MAAM,GAAG,IAAI,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAU,EAAE,CAAC;QAC5D,IAAI,OAAO,KAAK,CAAC,GAAG,CAAC,KAAK,QAAQ;YAAE,OAAO,SAAS,CAAC;IACvD,CAAC;IACD,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,OAAO,CAAC;QAAE,OAAO,SAAS,CAAC;IAC3D,IAAI,KAAK,CAAC,OAAO,KAAK,SAAS,IAAI,OAAO,KAAK,CAAC,OAAO,KAAK,QAAQ,EAAE,CAAC;QACrE,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,IAAI,CAAC,CAAC,MAAM,IAAI,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IACzC,OAAO,KAA4B,CAAC;AACtC,CAAC;AAED,MAAM,UAAU,MAAM,CAAC,QAA6B;IAClD,MAAM,IAAI,GAAG;QACX,EAAE,EAAE,QAAQ,CAAC,EAAE;QACf,MAAM,EAAE,QAAQ,CAAC,MAAM;QACvB,IAAI,EAAE,QAAQ,CAAC,IAAI;KACpB,CAAC;IACF,OAAO,QAAQ,CAAC,OAAO,KAAK,SAAS;QACnC,CAAC,CAAC,IAAI;QACN,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,QAAQ,CAAC,OAAO,EAAE,CAAC;AAC7C,CAAC"}
@@ -0,0 +1,6 @@
1
+ export { Contract, type ContractDefinition, type ContractEvent, type ContractMessage, type ContractTopic, } from './contract.js';
2
+ export type { IntegrationEnvelope, IntegrationMeta } from './envelope.js';
3
+ export { type Downgrade, Message, type MessageConstructor, type MessageInstance, type MessageOptions, type MessagePayload, } from './message.js';
4
+ export { IntegrationPublisher, type PublishOptions } from './publisher.js';
5
+ export { type IntegrationHandler, IntegrationSubscriber, type LogSink, type ParkInput, type ParkSink, type SubscribeOptions, type Subscription, } from './subscriber.js';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,QAAQ,EACR,KAAK,kBAAkB,EACvB,KAAK,aAAa,EAClB,KAAK,eAAe,EACpB,KAAK,aAAa,GACnB,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAC1E,OAAO,EACL,KAAK,SAAS,EACd,OAAO,EACP,KAAK,kBAAkB,EACvB,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,cAAc,GACpB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,oBAAoB,EAAE,KAAK,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAC3E,OAAO,EACL,KAAK,kBAAkB,EACvB,qBAAqB,EACrB,KAAK,OAAO,EACZ,KAAK,SAAS,EACd,KAAK,QAAQ,EACb,KAAK,gBAAgB,EACrB,KAAK,YAAY,GAClB,MAAM,iBAAiB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { Contract, } from './contract.js';
2
+ export { Message, } from './message.js';
3
+ export { IntegrationPublisher } from './publisher.js';
4
+ export { IntegrationSubscriber, } from './subscriber.js';
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,QAAQ,GAKT,MAAM,eAAe,CAAC;AAEvB,OAAO,EAEL,OAAO,GAKR,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,oBAAoB,EAAuB,MAAM,gBAAgB,CAAC;AAC3E,OAAO,EAEL,qBAAqB,GAMtB,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,70 @@
1
+ import { Event, type InferSchema, type SchemaSource, type StandardSchemaV1 } from 'hexok';
2
+ /** Instance of a {@link Message} class. */
3
+ export interface MessageInstance {
4
+ readonly token: string;
5
+ readonly version: number;
6
+ readonly payload: unknown;
7
+ }
8
+ /** A class made with {@link Message}. */
9
+ export interface MessageConstructor {
10
+ /** Message type. The same type across versions. */
11
+ readonly token: string;
12
+ /** Positive integer. A breaking change is a new version. */
13
+ readonly version: number;
14
+ /** Payload schema. */
15
+ readonly schema: StandardSchemaV1;
16
+ /** How a payload of this version becomes the previous version. */
17
+ readonly downgrade: Downgrade | undefined;
18
+ /** Validate `value` and construct the message. */
19
+ parse(value: unknown): object;
20
+ new (...args: any[]): MessageInstance;
21
+ }
22
+ /** A downgrade as {@link MessageConstructor} stores it. */
23
+ export interface Downgrade {
24
+ readonly to: MessageConstructor;
25
+ readonly map: (payload: never) => unknown;
26
+ }
27
+ /** Payload of a message class. */
28
+ export type MessagePayload<M extends MessageConstructor> = InstanceType<M>['payload'];
29
+ /**
30
+ * `to` is the previous version of the same type. `map` turns a payload of
31
+ * this version into a payload of `to`.
32
+ */
33
+ export interface MessageOptions<S extends SchemaSource, To extends MessageConstructor> {
34
+ readonly downgrade: {
35
+ readonly to: To;
36
+ readonly map: (payload: InferSchema<S>) => MessagePayload<To>;
37
+ };
38
+ }
39
+ type DowngradeOf<S extends SchemaSource, To extends MessageConstructor> = [
40
+ To
41
+ ] extends [never] ? undefined : MessageOptions<S, To>['downgrade'];
42
+ type EventBase<Type extends string, S extends SchemaSource> = ReturnType<typeof Event<Type, S>>;
43
+ /**
44
+ * The class {@link Message} returns. It is the hexok `Event` class for `Type`
45
+ * and `S`, with a static and instance `version` and a static `downgrade`.
46
+ */
47
+ export type MessageClass<Type extends string, Version extends number, S extends SchemaSource, To extends MessageConstructor> = {
48
+ new (payload: InferSchema<S>): InstanceType<EventBase<Type, S>> & {
49
+ readonly version: Version;
50
+ };
51
+ } & Omit<EventBase<Type, S>, 'prototype'> & {
52
+ /** Version. Literal type of the number passed to {@link Message}. */
53
+ readonly version: Version;
54
+ /** The previous version and how to reach it. `undefined` without one. */
55
+ readonly downgrade: DowngradeOf<S, To>;
56
+ };
57
+ /**
58
+ * A versioned event that crosses a product or service boundary. It is a hexok
59
+ * `Event`, so `parse` and `EventHandler` work on it.
60
+ *
61
+ * ```ts
62
+ * class InvoicePaid extends Message('invoice.paid', 1, invoicePaidSchema) {}
63
+ * class InvoicePaidV2 extends Message('invoice.paid', 2, invoicePaidV2Schema, {
64
+ * downgrade: { to: InvoicePaid, map: (v2) => ({ invoiceId: v2.id }) },
65
+ * }) {}
66
+ * ```
67
+ */
68
+ export declare function Message<const Type extends string, const Version extends number, S extends SchemaSource, To extends MessageConstructor = never>(type: Type, version: Version, schema: S, options?: MessageOptions<S, To>): MessageClass<Type, Version, S, To>;
69
+ export {};
70
+ //# sourceMappingURL=message.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"message.d.ts","sourceRoot":"","sources":["../src/message.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,44 @@
1
+ import { Event, } from 'hexok';
2
+ /**
3
+ * A versioned event that crosses a product or service boundary. It is a hexok
4
+ * `Event`, so `parse` and `EventHandler` work on it.
5
+ *
6
+ * ```ts
7
+ * class InvoicePaid extends Message('invoice.paid', 1, invoicePaidSchema) {}
8
+ * class InvoicePaidV2 extends Message('invoice.paid', 2, invoicePaidV2Schema, {
9
+ * downgrade: { to: InvoicePaid, map: (v2) => ({ invoiceId: v2.id }) },
10
+ * }) {}
11
+ * ```
12
+ */
13
+ export function Message(type, version, schema, options) {
14
+ if (typeof type !== 'string' || type === '') {
15
+ throw new Error('integration-events: message type must be a string');
16
+ }
17
+ if (!Number.isSafeInteger(version) || version < 1) {
18
+ throw new Error(`integration-events: "${type}" version must be a positive integer`);
19
+ }
20
+ const downgrade = options?.downgrade;
21
+ if (downgrade !== undefined) {
22
+ assertDowngrade(type, version, downgrade);
23
+ }
24
+ const stored = (downgrade === undefined ? undefined : Object.freeze({ ...downgrade }));
25
+ class VersionedEvent extends Event(type, schema) {
26
+ static version = version;
27
+ static downgrade = stored;
28
+ version = version;
29
+ }
30
+ return VersionedEvent;
31
+ }
32
+ function assertDowngrade(type, version, downgrade) {
33
+ const to = downgrade.to;
34
+ if (typeof downgrade.to !== 'function' ||
35
+ to.token !== type ||
36
+ typeof to.version !== 'number' ||
37
+ to.version >= version) {
38
+ throw new Error(`integration-events: "${type}" v${version} must downgrade to an earlier version of "${type}"`);
39
+ }
40
+ if (typeof downgrade.map !== 'function') {
41
+ throw new Error(`integration-events: "${type}" v${version} downgrade map must be a function`);
42
+ }
43
+ }
44
+ //# sourceMappingURL=message.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"message.js","sourceRoot":"","sources":["../src/message.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,GAIN,MAAM,OAAO,CAAC;AA+Ef;;;;;;;;;;GAUG;AACH,MAAM,UAAU,OAAO,CAMrB,IAAU,EACV,OAAgB,EAChB,MAAS,EACT,OAA+B;IAE/B,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,EAAE,EAAE,CAAC;QAC5C,MAAM,IAAI,KAAK,CAAC,mDAAmD,CAAC,CAAC;IACvE,CAAC;IACD,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,OAAO,CAAC,IAAI,OAAO,GAAG,CAAC,EAAE,CAAC;QAClD,MAAM,IAAI,KAAK,CACb,wBAAwB,IAAI,sCAAsC,CACnE,CAAC;IACJ,CAAC;IACD,MAAM,SAAS,GAAG,OAAO,EAAE,SAAS,CAAC;IACrC,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;QAC5B,eAAe,CAAC,IAAI,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC;IAC5C,CAAC;IACD,MAAM,MAAM,GAAG,CACb,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,SAAS,EAAE,CAAC,CAChD,CAAC;IAExB,MAAM,cAAe,SAAQ,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC;QAC9C,MAAM,CAAU,OAAO,GAAY,OAAO,CAAC;QAC3C,MAAM,CAAU,SAAS,GAAuB,MAAM,CAAC;QAC9C,OAAO,GAAY,OAAO,CAAC;;IAGtC,OAAO,cAAc,CAAC;AACxB,CAAC;AAED,SAAS,eAAe,CACtB,IAAY,EACZ,OAAe,EACf,SAA0D;IAE1D,MAAM,EAAE,GAAG,SAAS,CAAC,EAGpB,CAAC;IACF,IACE,OAAO,SAAS,CAAC,EAAE,KAAK,UAAU;QAClC,EAAE,CAAC,KAAK,KAAK,IAAI;QACjB,OAAO,EAAE,CAAC,OAAO,KAAK,QAAQ;QAC9B,EAAE,CAAC,OAAO,IAAI,OAAO,EACrB,CAAC;QACD,MAAM,IAAI,KAAK,CACb,wBAAwB,IAAI,MAAM,OAAO,6CAA6C,IAAI,GAAG,CAC9F,CAAC;IACJ,CAAC;IACD,IAAI,OAAO,SAAS,CAAC,GAAG,KAAK,UAAU,EAAE,CAAC;QACxC,MAAM,IAAI,KAAK,CACb,wBAAwB,IAAI,MAAM,OAAO,mCAAmC,CAC7E,CAAC;IACJ,CAAC;AACH,CAAC"}
@@ -0,0 +1,37 @@
1
+ import type { Queue } from '@hexok/queue';
2
+ import type { ContractDefinition, ContractEvent } from './contract.js';
3
+ /** Envelope attributes the caller may set. */
4
+ export interface PublishOptions {
5
+ /** Defaults to a random UUID. Pass a stable id, such as an outbox row id, so a retry carries the same id. */
6
+ readonly id?: string;
7
+ /** Defaults to now. */
8
+ readonly time?: Date;
9
+ /** Such as the aggregate id. Omitted from the envelope when unset. */
10
+ readonly subject?: string;
11
+ }
12
+ /**
13
+ * Publishes a contract's messages to its topics through a `Queue`.
14
+ *
15
+ * ```ts
16
+ * const billing = new IntegrationPublisher(Billing, { queue });
17
+ * await billing.publish(new InvoicePaid({ invoiceId, amountCents }));
18
+ * ```
19
+ */
20
+ export declare class IntegrationPublisher<C extends ContractDefinition> {
21
+ #private;
22
+ constructor(contract: C, deps: {
23
+ readonly queue: Queue;
24
+ });
25
+ /**
26
+ * Validate the payload, then publish it and one downgraded copy for each
27
+ * earlier version in the topic, newest first. Every copy has the same `id`,
28
+ * `time`, and `subject`. Resolves the `id`.
29
+ *
30
+ * Throws when the message is not in the contract, or when it has a newer
31
+ * version. Throws `CodedError` `VALIDATION` before publishing anything when
32
+ * a payload or a downgraded payload fails its schema. A queue failure
33
+ * rejects, and copies already published stay published.
34
+ */
35
+ publish(event: ContractEvent<C>, options?: PublishOptions): Promise<string>;
36
+ }
37
+ //# sourceMappingURL=publisher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"publisher.d.ts","sourceRoot":"","sources":["../src/publisher.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAE1C,OAAO,KAAK,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAIvE,8CAA8C;AAC9C,MAAM,WAAW,cAAc;IAC7B,6GAA6G;IAC7G,QAAQ,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC;IACrB,uBAAuB;IACvB,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC;IACrB,sEAAsE;IACtE,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;GAOG;AACH,qBAAa,oBAAoB,CAAC,CAAC,SAAS,kBAAkB;;gBAMhD,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;KAAE;IAYxD;;;;;;;;;OASG;IACG,OAAO,CACX,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,EACvB,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,MAAM,CAAC;CAiCnB"}
@@ -0,0 +1,90 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { validate, validationError } from 'hexok';
3
+ /**
4
+ * Publishes a contract's messages to its topics through a `Queue`.
5
+ *
6
+ * ```ts
7
+ * const billing = new IntegrationPublisher(Billing, { queue });
8
+ * await billing.publish(new InvoicePaid({ invoiceId, amountCents }));
9
+ * ```
10
+ */
11
+ export class IntegrationPublisher {
12
+ #source;
13
+ #queue;
14
+ #topicOf = new Map();
15
+ #superseded = new Set();
16
+ constructor(contract, deps) {
17
+ this.#source = contract.token;
18
+ this.#queue = deps.queue;
19
+ for (const topic of Object.values(contract.topics)) {
20
+ for (const message of topic.messages) {
21
+ this.#topicOf.set(message, topic.name);
22
+ const to = message.downgrade?.to;
23
+ if (to !== undefined)
24
+ this.#superseded.add(to);
25
+ }
26
+ }
27
+ }
28
+ /**
29
+ * Validate the payload, then publish it and one downgraded copy for each
30
+ * earlier version in the topic, newest first. Every copy has the same `id`,
31
+ * `time`, and `subject`. Resolves the `id`.
32
+ *
33
+ * Throws when the message is not in the contract, or when it has a newer
34
+ * version. Throws `CodedError` `VALIDATION` before publishing anything when
35
+ * a payload or a downgraded payload fails its schema. A queue failure
36
+ * rejects, and copies already published stay published.
37
+ */
38
+ async publish(event, options = {}) {
39
+ const instance = event;
40
+ const Newest = instance.constructor;
41
+ const topic = this.#topicOf.get(Newest);
42
+ if (topic === undefined) {
43
+ throw new Error(`integration-events: ${describe(event)} is not in contract "${this.#source}"`);
44
+ }
45
+ if (this.#superseded.has(Newest)) {
46
+ throw new Error(`integration-events: ${describe(event)} has a newer version in topic "${topic}"`);
47
+ }
48
+ const versions = chain(Newest, instance.payload);
49
+ const id = options.id ?? randomUUID();
50
+ const time = (options.time ?? new Date()).toISOString();
51
+ for (const { message, payload } of versions) {
52
+ const envelope = {
53
+ specversion: '1.0',
54
+ id,
55
+ source: this.#source,
56
+ type: message.token,
57
+ version: message.version,
58
+ time,
59
+ ...(options.subject !== undefined ? { subject: options.subject } : {}),
60
+ data: payload,
61
+ };
62
+ await this.#queue.publish(topic, envelope);
63
+ }
64
+ return id;
65
+ }
66
+ }
67
+ /** The payload at each version, newest first. Each payload is validated before it is downgraded. */
68
+ function chain(newest, payload) {
69
+ const versions = [];
70
+ let message = newest;
71
+ let current = payload;
72
+ while (message !== undefined) {
73
+ const parsed = validate(message.schema, current);
74
+ if (!parsed.ok) {
75
+ throw validationError(`integration-events: "${message.token}" v${message.version} validation failed`, parsed.issues);
76
+ }
77
+ versions.push({ message, payload: current });
78
+ const downgrade = message.downgrade;
79
+ if (downgrade === undefined)
80
+ break;
81
+ current = downgrade.map(current);
82
+ message = downgrade.to;
83
+ }
84
+ return versions;
85
+ }
86
+ function describe(event) {
87
+ const { token, version } = event;
88
+ return `"${String(token)}" v${String(version)}`;
89
+ }
90
+ //# sourceMappingURL=publisher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"publisher.js","sourceRoot":"","sources":["../src/publisher.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,79 @@
1
+ import type { Queue, QueueMaxDeliveries, QueueStartFrom } from '@hexok/queue';
2
+ import type { ContractDefinition, ContractMessage } from './contract.js';
3
+ /** Structural logger. `@hexok/logger` and `console` fit it. */
4
+ export interface LogSink {
5
+ error(message: string | Error, fields?: Record<string, unknown>): void;
6
+ }
7
+ /** Structurally `Quarantine.park`. This package does not import `@hexok/quarantine`. */
8
+ export interface ParkInput {
9
+ readonly source: string;
10
+ readonly operation: string;
11
+ readonly subject?: string;
12
+ readonly payload: unknown;
13
+ readonly error: {
14
+ readonly name: string;
15
+ readonly message: string;
16
+ readonly code?: string;
17
+ };
18
+ readonly occurredAt: Date;
19
+ readonly context?: Readonly<Record<string, string>>;
20
+ }
21
+ export interface ParkSink {
22
+ park(input: ParkInput): Promise<{
23
+ id: string;
24
+ }>;
25
+ }
26
+ /**
27
+ * A constructed `EventHandler` for a message in the contract. `handle` is
28
+ * typed `never` so a mixed array still passes. `EventHandler` types the event.
29
+ */
30
+ export interface IntegrationHandler<C extends ContractDefinition> {
31
+ readonly token: string;
32
+ readonly event: ContractMessage<C>;
33
+ handle(event: never, meta: never): Promise<void> | void;
34
+ }
35
+ export interface SubscribeOptions<C extends ContractDefinition> {
36
+ /** Passed to `Queue.subscribe` for each handler. */
37
+ readonly maxDeliveries: QueueMaxDeliveries;
38
+ /** Passed to `Queue.subscribe` for each handler. */
39
+ readonly startFrom?: QueueStartFrom;
40
+ readonly handlers: readonly IntegrationHandler<C>[];
41
+ }
42
+ /** One queue subscription that {@link IntegrationSubscriber.subscribe} made. */
43
+ export interface Subscription {
44
+ readonly topic: string;
45
+ readonly group: string;
46
+ readonly type: string;
47
+ readonly version: number;
48
+ }
49
+ /**
50
+ * Subscribes `EventHandler`s to a contract's topics through a `Queue`. Each
51
+ * handler gets its own consumer group, `<service>:<handler token>`.
52
+ *
53
+ * ```ts
54
+ * const billing = new IntegrationSubscriber(Billing, { queue, service: 'crm' });
55
+ * await billing.subscribe({ maxDeliveries: 10, handlers: [new SyncInvoice(deps)] });
56
+ * ```
57
+ */
58
+ export declare class IntegrationSubscriber<C extends ContractDefinition> {
59
+ #private;
60
+ constructor(contract: C, deps: {
61
+ readonly queue: Queue;
62
+ /** Name of the consuming service. Prefixes every consumer group. */
63
+ readonly service: string;
64
+ readonly logger?: LogSink;
65
+ readonly quarantine?: ParkSink;
66
+ });
67
+ /**
68
+ * Subscribe each handler to the topic of its message. Checks every handler
69
+ * before it subscribes any. Throws when a handler's message is not in the
70
+ * contract, or when two handlers would share a group on one topic.
71
+ *
72
+ * A delivery of another type or version is acked and skipped. A payload
73
+ * that is not an envelope, or data its schema rejects, is logged, parked
74
+ * when a quarantine sink is set, and dropped. A throw from `handle` follows
75
+ * the queue's retry rules.
76
+ */
77
+ subscribe(options: SubscribeOptions<C>): Promise<Subscription[]>;
78
+ }
79
+ //# sourceMappingURL=subscriber.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subscriber.d.ts","sourceRoot":"","sources":["../src/subscriber.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,KAAK,EAEL,kBAAkB,EAElB,cAAc,EACf,MAAM,cAAc,CAAC;AAEtB,OAAO,KAAK,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAMzE,+DAA+D;AAC/D,MAAM,WAAW,OAAO;IACtB,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,KAAK,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CACxE;AAED,wFAAwF;AACxF,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE;QACd,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;KACxB,CAAC;IACF,QAAQ,CAAC,UAAU,EAAE,IAAI,CAAC;IAC1B,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CACrD;AAED,MAAM,WAAW,QAAQ;IACvB,IAAI,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACjD;AAED;;;GAGG;AACH,MAAM,WAAW,kBAAkB,CAAC,CAAC,SAAS,kBAAkB;IAC9D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAC,CAAC,CAAC,CAAC;IACnC,MAAM,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CACzD;AAED,MAAM,WAAW,gBAAgB,CAAC,CAAC,SAAS,kBAAkB;IAC5D,oDAAoD;IACpD,QAAQ,CAAC,aAAa,EAAE,kBAAkB,CAAC;IAC3C,oDAAoD;IACpD,QAAQ,CAAC,SAAS,CAAC,EAAE,cAAc,CAAC;IACpC,QAAQ,CAAC,QAAQ,EAAE,SAAS,kBAAkB,CAAC,CAAC,CAAC,EAAE,CAAC;CACrD;AAED,gFAAgF;AAChF,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAQD;;;;;;;;GAQG;AACH,qBAAa,qBAAqB,CAAC,CAAC,SAAS,kBAAkB;;gBAU3D,QAAQ,EAAE,CAAC,EACX,IAAI,EAAE;QACJ,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;QACtB,oEAAoE;QACpE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;QAC1B,QAAQ,CAAC,UAAU,CAAC,EAAE,QAAQ,CAAC;KAChC;IAsBH;;;;;;;;;OASG;IACG,SAAS,CAAC,OAAO,EAAE,gBAAgB,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;CAoHvE"}
@@ -0,0 +1,145 @@
1
+ import { CodedError } from 'hexok';
2
+ import { metaOf, readEnvelope } from './envelope.js';
3
+ const servicePattern = /^[A-Za-z0-9_-]+$/;
4
+ /**
5
+ * Subscribes `EventHandler`s to a contract's topics through a `Queue`. Each
6
+ * handler gets its own consumer group, `<service>:<handler token>`.
7
+ *
8
+ * ```ts
9
+ * const billing = new IntegrationSubscriber(Billing, { queue, service: 'crm' });
10
+ * await billing.subscribe({ maxDeliveries: 10, handlers: [new SyncInvoice(deps)] });
11
+ * ```
12
+ */
13
+ export class IntegrationSubscriber {
14
+ #contract;
15
+ #queue;
16
+ #service;
17
+ #logger;
18
+ #quarantine;
19
+ #topicOf = new Map();
20
+ #groups = new Set();
21
+ constructor(contract, deps) {
22
+ if (typeof deps.service !== 'string' ||
23
+ !servicePattern.test(deps.service)) {
24
+ throw new Error(`integration-events: service "${String(deps.service)}" must match ${servicePattern}`);
25
+ }
26
+ this.#contract = contract;
27
+ this.#queue = deps.queue;
28
+ this.#service = deps.service;
29
+ this.#logger = deps.logger;
30
+ this.#quarantine = deps.quarantine;
31
+ for (const topic of Object.values(contract.topics)) {
32
+ for (const message of topic.messages) {
33
+ this.#topicOf.set(message, topic.name);
34
+ }
35
+ }
36
+ }
37
+ /**
38
+ * Subscribe each handler to the topic of its message. Checks every handler
39
+ * before it subscribes any. Throws when a handler's message is not in the
40
+ * contract, or when two handlers would share a group on one topic.
41
+ *
42
+ * A delivery of another type or version is acked and skipped. A payload
43
+ * that is not an envelope, or data its schema rejects, is logged, parked
44
+ * when a quarantine sink is set, and dropped. A throw from `handle` follows
45
+ * the queue's retry rules.
46
+ */
47
+ async subscribe(options) {
48
+ const plans = this.#plan(options.handlers);
49
+ const subscriptions = [];
50
+ for (const plan of plans) {
51
+ const { topic, group } = plan.subscription;
52
+ await this.#queue.subscribe(topic, {
53
+ consumerGroup: group,
54
+ maxDeliveries: options.maxDeliveries,
55
+ ...(options.startFrom !== undefined
56
+ ? { startFrom: options.startFrom }
57
+ : {}),
58
+ handler: (message) => this.#deliver(plan, message),
59
+ });
60
+ this.#groups.add(groupKey(topic, group));
61
+ subscriptions.push(plan.subscription);
62
+ }
63
+ return subscriptions;
64
+ }
65
+ #plan(handlers) {
66
+ const planned = new Map();
67
+ for (const handler of handlers) {
68
+ const message = handler.event;
69
+ const topic = this.#topicOf.get(message);
70
+ if (topic === undefined) {
71
+ throw new Error(`integration-events: handler "${handler.token}" handles "${message.token}" v${message.version}, which is not in contract "${this.#contract.token}"`);
72
+ }
73
+ const group = `${this.#service}:${handler.token}`;
74
+ const key = groupKey(topic, group);
75
+ const previous = planned.get(key);
76
+ if (previous !== undefined || this.#groups.has(key)) {
77
+ throw new Error(`integration-events: group "${group}" on topic "${topic}" is already subscribed. Give each handler its own token`);
78
+ }
79
+ planned.set(key, {
80
+ handler,
81
+ message,
82
+ subscription: {
83
+ topic,
84
+ group,
85
+ type: message.token,
86
+ version: message.version,
87
+ },
88
+ });
89
+ }
90
+ return [...planned.values()];
91
+ }
92
+ async #deliver(plan, delivery) {
93
+ const envelope = readEnvelope(delivery.payload);
94
+ if (envelope === undefined) {
95
+ await this.#reject(plan, delivery, new Error('integration-events: entry is not an integration event'));
96
+ return 'drop';
97
+ }
98
+ const { message } = plan;
99
+ if (envelope.type !== message.token ||
100
+ envelope.version !== message.version) {
101
+ return 'ack';
102
+ }
103
+ let event;
104
+ try {
105
+ event = message.parse(envelope.data);
106
+ }
107
+ catch (error) {
108
+ if (error instanceof CodedError && error.code === 'VALIDATION') {
109
+ await this.#reject(plan, delivery, error);
110
+ return 'drop';
111
+ }
112
+ throw error;
113
+ }
114
+ await plan.handler.handle(event, metaOf(envelope));
115
+ return 'ack';
116
+ }
117
+ async #reject(plan, delivery, error) {
118
+ const { topic, group } = plan.subscription;
119
+ this.#logger?.error(error, { topic, group, id: delivery.id });
120
+ if (this.#quarantine === undefined)
121
+ return;
122
+ try {
123
+ await this.#quarantine.park({
124
+ source: 'integration-events',
125
+ operation: 'deliver',
126
+ subject: `${topic}:${delivery.id}`,
127
+ payload: delivery.payload,
128
+ error: error instanceof CodedError
129
+ ? { name: error.name, message: error.message, code: error.code }
130
+ : { name: error.name, message: error.message },
131
+ occurredAt: new Date(),
132
+ context: { group },
133
+ });
134
+ }
135
+ catch (parkError) {
136
+ this.#logger?.error(parkError instanceof Error
137
+ ? parkError
138
+ : new Error('integration-events: park failed'), { topic, group, id: delivery.id });
139
+ }
140
+ }
141
+ }
142
+ function groupKey(topic, group) {
143
+ return `${topic}\0${group}`;
144
+ }
145
+ //# sourceMappingURL=subscriber.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subscriber.js","sourceRoot":"","sources":["../src/subscriber.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,52 @@
1
1
  {
2
2
  "name": "@hexok/integration-events",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Versioned events that cross product and service boundaries, over the hexok queue port.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "sideEffects": false,
8
+ "keywords": [
9
+ "hexok",
10
+ "integration-events",
11
+ "events",
12
+ "queue"
13
+ ],
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/crobinson42/hexok.git",
17
+ "directory": "packages/integration-events"
18
+ },
19
+ "homepage": "https://crobinson42.github.io/hexok/",
20
+ "bugs": {
21
+ "url": "https://github.com/crobinson42/hexok/issues"
22
+ },
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/index.d.ts",
29
+ "import": "./dist/index.js",
30
+ "default": "./dist/index.js"
31
+ },
32
+ "./package.json": "./package.json"
33
+ },
34
+ "files": [
35
+ "dist"
36
+ ],
37
+ "scripts": {
38
+ "build": "tsc -b",
39
+ "test": "vitest run",
40
+ "type-check": "tsc --noEmit -p tsconfig.check.json",
41
+ "lint": "biome check .",
42
+ "clean": "rm -rf dist *.tsbuildinfo"
43
+ },
44
+ "dependencies": {
45
+ "@hexok/queue": "^1.0.0",
46
+ "hexok": "^2.2.4"
47
+ },
48
+ "devDependencies": {
49
+ "@hexok/queue-redis": "^1.1.3",
50
+ "zod": "^4.1.5"
51
+ }
52
+ }