hexok 2.2.3 → 2.2.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hexok",
3
- "version": "2.2.3",
3
+ "version": "2.2.5",
4
4
  "description": "TypeScript primitives for hexagonal software: Schema, Entity, UseCase, Port, Adapter, Mapper, Event, EventCatalog, EventHandler, and Errors.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -31,6 +31,7 @@ Read the reference before writing that concept. Each file is the supporting docu
31
31
  | UseCase | [references/use-case.md](references/use-case.md) | Application. One caller action. `UseCase.context` is in that file. |
32
32
  | Port | [references/port.md](references/port.md) | Application. The contract a use case or handler calls. |
33
33
  | EventHandler | [references/event-handler.md](references/event-handler.md) | Application. Work that follows one event. |
34
+ | Integration events | [references/integration-events.md](references/integration-events.md) | Contract between applications. Read it when another application that deploys on its own publishes or consumes the event. From `@hexok/integration-events`. |
34
35
  | Adapter | [references/adapter.md](references/adapter.md) | Interface adapter. The current implementation of one port. |
35
36
  | Mapper | [references/mapper.md](references/mapper.md) | Interface adapter. Read it when the stored document is not the domain props. |
36
37
 
@@ -72,6 +73,8 @@ When that work exists and the event does not, define the event and register it o
72
73
 
73
74
  When the story has no consequence beyond its own write, this step is `none`. Do not add an event so the step looks complete.
74
75
 
76
+ When the consequence runs in another application that deploys on its own, the event is an integration event, not a domain event. Read [references/integration-events.md](references/integration-events.md).
77
+
75
78
  ### 3.c. Event handlers
76
79
 
77
80
  When step 3.b named a side effect, implement the handler if it does not exist. The handler takes its own ports on a `deps` object in the constructor. `handle` receives the event. Emailing subscribers loads addresses through a subscriber port and sends mail through a mail port. Read [references/event-handler.md](references/event-handler.md).
@@ -0,0 +1,85 @@
1
+ # Integration events
2
+
3
+ An integration event is a fact one application publishes for other applications that deploy on their own, such as a second product or a service another team owns. It comes from `@hexok/integration-events`, not from `hexok`. Read the package README for the full guarantees.
4
+
5
+ A domain event stays inside one application and changes with it. An integration event is a contract. Do not publish a domain event to another application. Map it to an integration event at the edge.
6
+
7
+ Use an integration event only when a consumer deploys apart from the producer. Two processes of one application, such as an API and its worker, share a catalog and use `Event` and `EventCatalog`.
8
+
9
+ ## Contract
10
+
11
+ The producer owns the contract file. It imports `@hexok/integration-events`, a schema library, and nothing else from the producer. Consumers import it from a package the producer publishes, a shared workspace, or a copy.
12
+
13
+ ```ts
14
+ import { Contract, Message } from '@hexok/integration-events';
15
+
16
+ export class PostPublished extends Message('post.published', 1, z.object({
17
+ postId: z.string(),
18
+ title: z.string(),
19
+ })) {}
20
+
21
+ export class Blog extends Contract('blog', {
22
+ posts: [PostPublished],
23
+ }) {}
24
+ ```
25
+
26
+ `Message(type, version, schema)` is an `Event` with a `version`. `Contract(name, topics)` maps each key to its messages. The queue topic is `<name>.<key>`, here `blog.posts`.
27
+
28
+ Contract schemas describe JSON. `Date`, `bigint`, `Map`, and `Set` do not survive the queue. Use strings and numbers, such as an ISO string for a time. `publish` validates the JSON form of the payload and sends that form, so keys the schema would strip are still sent. A payload that is not JSON or fails its schema throws `VALIDATION` before anything is sent.
29
+
30
+ ## Producer
31
+
32
+ The use case depends on a port. The adapter wraps `IntegrationPublisher`.
33
+
34
+ ```ts
35
+ abstract class BlogIntegrationEvents extends Port('BlogIntegrationEvents') {
36
+ abstract publish(event: ContractEvent<typeof Blog>): Promise<string>;
37
+ }
38
+
39
+ class QueueBlogIntegrationEvents extends Adapter(BlogIntegrationEvents) {
40
+ readonly #publisher: IntegrationPublisher<typeof Blog>;
41
+
42
+ constructor(private readonly deps: { readonly queue: Queue }) {
43
+ super();
44
+ this.#publisher = new IntegrationPublisher(Blog, deps);
45
+ }
46
+
47
+ override publish(event: ContractEvent<typeof Blog>): Promise<string> {
48
+ return this.#publisher.publish(event);
49
+ }
50
+ }
51
+ ```
52
+
53
+ The use case publishes after its save succeeds, as it does a domain event. When the application already publishes a domain event, an event handler can map it to the integration event and publish that.
54
+
55
+ ## Consumer
56
+
57
+ Each handler is an `EventHandler` for one message. The worker entry subscribes them.
58
+
59
+ ```ts
60
+ class ProjectPost extends EventHandler('project-post', PostPublished) {
61
+ constructor(private readonly deps: { readonly posts: PostProjection }) {
62
+ super();
63
+ }
64
+
65
+ async handle(event: PostPublished, meta?: IntegrationMeta): Promise<void> {
66
+ await this.deps.posts.upsert(event.payload, meta?.id);
67
+ }
68
+ }
69
+
70
+ const blog = new IntegrationSubscriber(Blog, { queue, service: 'search', logger, quarantine });
71
+ await blog.subscribe({ maxDeliveries: 10, handlers: [new ProjectPost({ posts })] });
72
+ ```
73
+
74
+ `service` names the consuming application. Each handler gets the consumer group `<service>:<handler token>`. Give each handler its own token. Delivery is at least once. Make `handle` idempotent with `meta.id`.
75
+
76
+ ## Versions
77
+
78
+ - An additive change keeps the version. Consumer schemas accept unknown fields.
79
+ - A breaking change is a new `Message` of the same type and the next version, with `downgrade: { to, map }` to the previous one. Add it to the same topic. One `publish` of the newest version sends every version in the topic.
80
+ - `publish` accepts only the newest version.
81
+ - Retire an old version after every consumer moves. Remove it from the topic and remove the downgrade that targets it. Until a consumer moves, its handler for the retired version receives nothing and logs nothing, and the new contract makes its `subscribe` throw.
82
+
83
+ ## What the application owns
84
+
85
+ Retention, backfill for a new consumer, order between redeliveries, the outbox, and topic permissions in the broker. Every handler reads every entry on its topic, so keep topics narrow, such as one per aggregate.