Skip to content

Commit d7107ae

Browse files
stronger coupling to standard schema
1 parent 042cf3d commit d7107ae

8 files changed

Lines changed: 354 additions & 186 deletions

File tree

README.md

Lines changed: 65 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ const client = new pg.Client({
130130
await client.connect();
131131

132132
const processor = new EventProcessor({
133-
client: createProcessorClient({ querier: client }),
133+
client: createProcessorClient({ querier: client, eventSchemas }),
134134
handlerMap: {
135135
UserCreated: {
136136
// Handlers are processed concurrently and independently with retries
@@ -354,18 +354,20 @@ const incrementCounter: TxOBEventHandler = async (event) => {
354354

355355
#### Typed payloads with Standard Schema (Zod/ArkType/Valibot/etc.)
356356

357-
txob supports schema-driven event payload typing through the Standard Schema interface.
357+
txob uses schema-driven event payload typing through the Standard Schema interface.
358+
`eventSchemas` is required by both `createProcessorClient(...)` and `createEventProcessor(...)`.
359+
Use any validator that implements Standard Schema (for example Zod, ArkType, or Valibot).
358360

359361
```typescript
360362
import { z } from "zod";
361363
import {
362-
defineTxOBEventHandlerMap,
363-
defineTxOBEventSchemas,
364-
EventProcessor,
365-
type TxOBEventDataMapFromSchemas,
364+
createEventHandlerMap,
365+
createEventProcessor,
366+
type TxOBEventSchemaMap,
366367
} from "txob";
368+
import { createProcessorClient } from "txob/pg";
367369

368-
const eventSchemas = defineTxOBEventSchemas({
370+
const eventSchemas = {
369371
UserCreated: z.object({
370372
userId: z.string().uuid(),
371373
email: z.string().email(),
@@ -374,31 +376,32 @@ const eventSchemas = defineTxOBEventSchemas({
374376
orderId: z.string().uuid(),
375377
amount: z.number().positive(),
376378
}),
377-
});
379+
} satisfies TxOBEventSchemaMap<"UserCreated" | "OrderPlaced">;
378380

379-
type EventType = keyof typeof eventSchemas;
380-
type EventDataMap = TxOBEventDataMapFromSchemas<typeof eventSchemas>;
381-
382-
const handlers = defineTxOBEventHandlerMap(eventSchemas, {
383-
UserCreated: {
384-
sendWelcomeEmail: async (event) => {
385-
await emailService.send(event.data.email); // typed as string
381+
const handlerMap = createEventHandlerMap({
382+
eventSchemas,
383+
handlerMap: {
384+
UserCreated: {
385+
sendWelcomeEmail: async (event) => {
386+
await emailService.send(event.data.email); // typed as string
387+
},
386388
},
387-
},
388-
OrderPlaced: {
389-
sendReceipt: async (event) => {
390-
await receipts.send(event.data.orderId, event.data.amount); // strongly typed
389+
OrderPlaced: {
390+
sendReceipt: async (event) => {
391+
await receipts.send(event.data.orderId, event.data.amount); // strongly typed
392+
},
391393
},
392394
},
393395
});
394396

395-
const processor = new EventProcessor<EventType, EventDataMap>({
396-
client: createProcessorClient<EventType, EventDataMap>({ querier: client }),
397-
handlerMap: handlers,
397+
const processor = createEventProcessor({
398+
eventSchemas,
399+
client: createProcessorClient({ querier: client, eventSchemas }),
400+
handlerMap,
398401
});
399402
```
400403

401-
`defineTxOBEventSchemas(...)` is optional. It is a convenience helper for cleaner type inference. You can also define schema maps without it using explicit types.
404+
Schema-first inference via `eventSchemas` is the standard and required approach.
402405

403406
### Handler Results
404407

@@ -843,7 +846,7 @@ await client.connect();
843846

844847
// 3. Create and start the processor
845848
const processor = new EventProcessor<EventType>({
846-
client: createProcessorClient<EventType>({ querier: client }),
849+
client: createProcessorClient({ querier: client, eventSchemas }),
847850
handlerMap: {
848851
UserCreated: {
849852
sendEmail: async (event, { signal }) => {
@@ -963,7 +966,7 @@ gracefulShutdown(server, {
963966

964967
```typescript
965968
const processor = new EventProcessor({
966-
client: createProcessorClient({ querier: client }),
969+
client: createProcessorClient({ querier: client, eventSchemas }),
967970
handlerMap: {
968971
UserCreated: {
969972
sendWelcomeEmail: async (event) => {
@@ -1061,7 +1064,7 @@ const producer = kafka.producer();
10611064
await producer.connect();
10621065

10631066
const processor = new EventProcessor({
1064-
client: createProcessorClient({ querier: client }),
1067+
client: createProcessorClient({ querier: client, eventSchemas }),
10651068
handlerMap: {
10661069
UserCreated: {
10671070
// Publish to Kafka with guaranteed consistency
@@ -1118,7 +1121,7 @@ const client = new pg.Client({
11181121
await client.connect();
11191122

11201123
const processor = new EventProcessor({
1121-
client: createProcessorClient({ querier: client }),
1124+
client: createProcessorClient({ querier: client, eventSchemas }),
11221125
handlerMap: {
11231126
// All your handlers...
11241127
},
@@ -1191,11 +1194,12 @@ Creates a PostgreSQL processor client.
11911194
```typescript
11921195
import { createProcessorClient } from "txob/pg";
11931196

1194-
createProcessorClient<EventType>(opts: {
1197+
createProcessorClient(opts: {
11951198
querier: pg.Client;
1199+
eventSchemas: Record<string, StandardSchemaV1<unknown, unknown>>;
11961200
table?: string; // Default: "events"
11971201
limit?: number; // Default: 100
1198-
}): TxOBProcessorClient<EventType>
1202+
}): TxOBProcessorClient<...inferred from eventSchemas...>
11991203
```
12001204

12011205
### `createProcessorClient` (MongoDB)
@@ -1205,12 +1209,13 @@ Creates a MongoDB processor client.
12051209
```typescript
12061210
import { createProcessorClient } from "txob/mongodb";
12071211

1208-
createProcessorClient<EventType>(opts: {
1212+
createProcessorClient(opts: {
12091213
mongo: mongodb.MongoClient;
12101214
db: string; // Database name
1215+
eventSchemas: Record<string, StandardSchemaV1<unknown, unknown>>;
12111216
collection?: string; // Default: "events"
12121217
limit?: number; // Default: 100
1213-
}): TxOBProcessorClient<EventType>
1218+
}): TxOBProcessorClient<...inferred from eventSchemas...>
12141219
```
12151220

12161221
### `TxOBError`
@@ -1317,10 +1322,12 @@ type TxOBEventHandlerMap<
13171322
[TType in EventType]: Record<string, TxOBEventHandler<TType, EventDataMap[TType]>>;
13181323
};
13191324

1320-
// Standard Schema helpers
1321-
type TxOBEventDataMapFromSchemas<TSchemas> = {
1322-
[TType in keyof TSchemas & string]: /* schema output for TType */;
1323-
};
1325+
// Schema-first convenience API
1326+
createEventProcessor({
1327+
eventSchemas,
1328+
client: createProcessorClient({ querier, eventSchemas }),
1329+
handlerMap,
1330+
});
13241331

13251332
// Handler result tracking
13261333
type TxOBEventHandlerResult = {
@@ -1460,7 +1467,7 @@ If using `FOR UPDATE SKIP LOCKED` properly (which txob does), stuck events are n
14601467
- Lower `maxEventConcurrency`
14611468
- Profile handlers for memory leaks
14621469
- Archive old events
1463-
- Reduce `limit` in `createProcessorClient({ querier: client, table, limit })`
1470+
- Reduce `limit` in `createProcessorClient({ querier: client, eventSchemas, table, limit })`
14641471

14651472
### Duplicate handler executions
14661473

@@ -1765,7 +1772,7 @@ txob can emit spans and metrics without depending on a specific telemetry SDK. I
17651772
import { metrics, trace } from "@opentelemetry/api";
17661773

17671774
const processor = new EventProcessor({
1768-
client: createProcessorClient({ querier: client }),
1775+
client: createProcessorClient({ querier: client, eventSchemas }),
17691776
handlerMap: handlers,
17701777
telemetry: {
17711778
tracer: trace.getTracer("txob"),
@@ -1837,35 +1844,37 @@ Yes! txob is written in TypeScript and provides full type safety, including type
18371844
```typescript
18381845
import { z } from "zod";
18391846
import {
1840-
defineTxOBEventHandlerMap,
1841-
defineTxOBEventSchemas,
1842-
type TxOBEventDataMapFromSchemas,
1847+
createEventHandlerMap,
1848+
createEventProcessor,
1849+
type TxOBEventSchemaMap,
18431850
} from "txob";
1851+
import { createProcessorClient } from "txob/pg";
18441852

1845-
const eventSchemas = defineTxOBEventSchemas({
1853+
const eventSchemas = {
18461854
UserCreated: z.object({ userId: z.string().uuid(), email: z.string().email() }),
18471855
OrderPlaced: z.object({ orderId: z.string().uuid(), amount: z.number() }),
1848-
});
1849-
1850-
type EventType = keyof typeof eventSchemas;
1851-
type EventDataMap = TxOBEventDataMapFromSchemas<typeof eventSchemas>;
1856+
} satisfies TxOBEventSchemaMap<"UserCreated" | "OrderPlaced">;
18521857

1853-
const handlers = defineTxOBEventHandlerMap(eventSchemas, {
1854-
UserCreated: {
1855-
sendWelcomeEmail: async (event) => {
1856-
event.data.email; // string
1858+
const handlers = createEventHandlerMap({
1859+
eventSchemas,
1860+
handlerMap: {
1861+
UserCreated: {
1862+
sendWelcomeEmail: async (event) => {
1863+
event.data.email; // string
1864+
},
18571865
},
1858-
},
1859-
OrderPlaced: {
1860-
sendReceipt: async (event) => {
1861-
event.data.amount; // number
1866+
OrderPlaced: {
1867+
sendReceipt: async (event) => {
1868+
event.data.amount; // number
1869+
},
18621870
},
1871+
// Missing an event type? TypeScript error
18631872
},
1864-
// Missing an event type? TypeScript error
18651873
});
18661874

1867-
const processor = new EventProcessor<EventType, EventDataMap>({
1868-
client: createProcessorClient<EventType, EventDataMap>({ querier: client }),
1875+
const processor = createEventProcessor({
1876+
eventSchemas,
1877+
client: createProcessorClient({ querier: client, eventSchemas }),
18691878
handlerMap: handlers,
18701879
});
18711880
```

examples/pg/events.ts

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,21 @@
11
import { z } from "zod";
2-
import {
3-
defineTxOBEventSchemas,
4-
type TxOBEventDataMapFromSchemas,
5-
} from "../../src/index.js";
2+
import type { TxOBEventSchemaMap } from "../../src/index.js";
63

74
export const eventTypes = {
85
ResourceSaved: "ResourceSaved",
96
EventMaxErrorsReached: "EventMaxErrorsReached",
107
} as const;
118

12-
export const eventSchemas = defineTxOBEventSchemas({
9+
export type EventType = keyof typeof eventTypes;
10+
11+
export const eventSchemas = {
1312
[eventTypes.ResourceSaved]: z.object({
1413
type: z.literal("activity"),
15-
id: z.string().uuid(),
14+
id: z.uuid(),
1615
}),
1716
[eventTypes.EventMaxErrorsReached]: z.object({
18-
failedEventId: z.string().uuid(),
17+
failedEventId: z.uuid(),
1918
failedEventType: z.string(),
20-
failedEventCorrelationId: z.string().uuid(),
19+
failedEventCorrelationId: z.uuid(),
2120
}),
22-
});
23-
24-
export type EventType = keyof typeof eventSchemas;
25-
export type EventDataMap = TxOBEventDataMapFromSchemas<typeof eventSchemas>;
21+
} satisfies TxOBEventSchemaMap<EventType>;

0 commit comments

Comments
 (0)