hexok 2.2.2 → 2.2.4
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
package/skills/hexok/SKILL.md
CHANGED
|
@@ -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).
|
|
@@ -52,7 +52,7 @@ A port parameter typed as the catalog class does not check event key or payload,
|
|
|
52
52
|
|
|
53
53
|
`parse` throws `hexok: catalog "blog" expected { key, payload }` when `value` lacks a string `key` or a `payload`. An unknown key throws `hexok: event "<key>" is not in catalog "blog"`. A payload the event schema rejects throws `CodedError` `VALIDATION` from that event's `parse`.
|
|
54
54
|
|
|
55
|
-
`examples/app-template` keeps one catalog in the app, `DomainEvents`.
|
|
55
|
+
`examples/app-template` keeps one catalog in the app, `DomainEvents`. The publisher calls `DomainEvents.message` so an event from outside the catalog fails. It publishes the event token as the queue topic and the message payload as the body. The worker calls `handler.event.parse` before `handle`. `@hexok/queue` does not own the catalog.
|
|
56
56
|
|
|
57
57
|
`EventInstance<typeof BlogEvents>` is the union of event instances in the catalog. Each instance carries its token. `EventMessage<typeof BlogEvents>` is the matching `{ key, payload }` union. The second type argument keeps one entry. `key` on a message is the map key. The event token is `BlogEvents.get(message.key).token`, and `event.token` on the instance.
|
|
58
58
|
|
|
@@ -0,0 +1,83 @@
|
|
|
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
|
+
## Producer
|
|
29
|
+
|
|
30
|
+
The use case depends on a port. The adapter wraps `IntegrationPublisher`.
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
abstract class BlogIntegrationEvents extends Port('BlogIntegrationEvents') {
|
|
34
|
+
abstract publish(event: ContractEvent<typeof Blog>): Promise<string>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
class QueueBlogIntegrationEvents extends Adapter(BlogIntegrationEvents) {
|
|
38
|
+
readonly #publisher: IntegrationPublisher<typeof Blog>;
|
|
39
|
+
|
|
40
|
+
constructor(private readonly deps: { readonly queue: Queue }) {
|
|
41
|
+
super();
|
|
42
|
+
this.#publisher = new IntegrationPublisher(Blog, deps);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
override publish(event: ContractEvent<typeof Blog>): Promise<string> {
|
|
46
|
+
return this.#publisher.publish(event);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
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.
|
|
52
|
+
|
|
53
|
+
## Consumer
|
|
54
|
+
|
|
55
|
+
Each handler is an `EventHandler` for one message. The worker entry subscribes them.
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
class ProjectPost extends EventHandler('project-post', PostPublished) {
|
|
59
|
+
constructor(private readonly deps: { readonly posts: PostProjection }) {
|
|
60
|
+
super();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async handle(event: PostPublished, meta?: IntegrationMeta): Promise<void> {
|
|
64
|
+
await this.deps.posts.upsert(event.payload, meta?.id);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const blog = new IntegrationSubscriber(Blog, { queue, service: 'search', logger, quarantine });
|
|
69
|
+
await blog.subscribe({ maxDeliveries: 10, handlers: [new ProjectPost({ posts })] });
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`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`.
|
|
73
|
+
|
|
74
|
+
## Versions
|
|
75
|
+
|
|
76
|
+
- An additive change keeps the version. Consumer schemas accept unknown fields.
|
|
77
|
+
- 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.
|
|
78
|
+
- `publish` accepts only the newest version.
|
|
79
|
+
- Retire an old version after every consumer moves. Remove it from the topic and remove the downgrade that targets it.
|
|
80
|
+
|
|
81
|
+
## What the application owns
|
|
82
|
+
|
|
83
|
+
Retention, backfill for a new consumer, order between redeliveries, the outbox, and topic permissions in the broker.
|