@crossdelta/cloudevents 0.5.2 → 0.5.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/README.md CHANGED
@@ -29,7 +29,11 @@ bun add @crossdelta/cloudevents zod@4
29
29
 
30
30
  ## Quick Start
31
31
 
32
- **1. Create an event handler** (`src/events/orders-created.event.ts`):
32
+ > **Note:** Services **never create streams**. Streams must exist before services consume from them:
33
+ > - **Development**: Use [NATS CLI](https://docs.nats.io/running-a-nats-service/nats_admin/jetstream_admin/streams) or [Platform SDK](https://www.npmjs.com/package/@crossdelta/platform-sdk) (`pf dev` auto-creates from contracts)
34
+ > - **Production**: Use infrastructure-as-code (Pulumi, Terraform) or the [Stream Setup](#stream-setup) function below
35
+
36
+ **1. Create an event handler** (`src/events/orders-created.handler.ts`):
33
37
 
34
38
  ```typescript
35
39
  import { handleEvent } from '@crossdelta/cloudevents'
@@ -55,17 +59,7 @@ export default handleEvent(
55
59
  )
56
60
  ```
57
61
 
58
- **2. Ensure stream exists** (once, during setup):
59
-
60
- ```typescript
61
- import { ensureJetStreams } from '@crossdelta/cloudevents'
62
-
63
- await ensureJetStreams({
64
- streams: [{ stream: 'ORDERS', subjects: ['orders.*'] }],
65
- })
66
- ```
67
-
68
- **3. Start consuming:**
62
+ **2. Start consuming:**
69
63
 
70
64
  ```typescript
71
65
  import { consumeJetStreams } from '@crossdelta/cloudevents'
@@ -73,11 +67,11 @@ import { consumeJetStreams } from '@crossdelta/cloudevents'
73
67
  consumeJetStreams({
74
68
  streams: ['ORDERS'],
75
69
  consumer: 'my-service',
76
- discover: './src/events/**/*.event.ts',
70
+ discover: './src/events/**/*.handler.ts',
77
71
  })
78
72
  ```
79
73
 
80
- **4. Publish from another service:**
74
+ **3. Publish from another service:**
81
75
 
82
76
  ```typescript
83
77
  import { publish } from '@crossdelta/cloudevents'
@@ -87,6 +81,10 @@ await publish('orders.created', { orderId: 'ord_123', total: 99.99 })
87
81
 
88
82
  That's it. Handlers are auto-discovered, validated with Zod, and messages persist in JetStream.
89
83
 
84
+ > **Stream creation** is handled by your development environment or infrastructure — not by services:
85
+ > - Development: [NATS CLI](https://docs.nats.io/running-a-nats-service/nats_admin/jetstream_admin/streams), [@crossdelta/platform-sdk](https://www.npmjs.com/package/@crossdelta/platform-sdk), or manual setup
86
+ > - Production: See [Stream Setup](#stream-setup) below for `ensureJetStreams()` usage in infrastructure code
87
+
90
88
  ---
91
89
 
92
90
  ## Why use this?
@@ -125,10 +123,10 @@ export default handleEvent(
125
123
 
126
124
  ### Handlers
127
125
 
128
- Drop a `*.event.ts` file anywhere — it's auto-registered:
126
+ Drop a `*.handler.ts` file anywhere — it's auto-registered:
129
127
 
130
128
  ```typescript
131
- // src/events/user-signup.event.ts
129
+ // src/events/user-signup.handler.ts
132
130
  import { z } from 'zod'
133
131
 
134
132
  const UserSignupSchema = z.object({
@@ -158,7 +156,9 @@ await publish('orders.created', orderData)
158
156
 
159
157
  ### Stream Setup
160
158
 
161
- Create the stream once during infrastructure setup:
159
+ > **For infrastructure use only** - Services should never call this function.
160
+
161
+ Create streams during infrastructure setup (Pulumi, deployment scripts):
162
162
 
163
163
  ```typescript
164
164
  import { ensureJetStreams } from '@crossdelta/cloudevents'
@@ -175,6 +175,7 @@ await ensureJetStreams({
175
175
  },
176
176
  ],
177
177
  })
178
+ })
178
179
  ```
179
180
 
180
181
  ### Consuming
@@ -186,13 +187,13 @@ import { consumeJetStreams } from '@crossdelta/cloudevents'
186
187
  consumeJetStreams({
187
188
  streams: ['ORDERS'],
188
189
  consumer: 'billing',
189
- discover: './src/events/**/*.event.ts',
190
+ discover: './src/events/**/*.handler.ts',
190
191
  })
191
192
 
192
193
  // Core NATS — fire-and-forget, simpler
193
194
  await consumeNatsEvents({
194
195
  subjects: ['notifications.*'],
195
- discover: './src/events/**/*.event.ts',
196
+ discover: './src/events/**/*.handler.ts',
196
197
  })
197
198
  ```
198
199
 
@@ -239,7 +240,7 @@ consumeJetStreams({
239
240
  // Required
240
241
  streams: ['ORDERS'],
241
242
  consumer: 'my-service',
242
- discover: './src/events/**/*.event.ts',
243
+ discover: './src/events/**/*.handler.ts',
243
244
 
244
245
  // Optional
245
246
  servers: 'nats://localhost:4222',
@@ -254,6 +255,129 @@ consumeJetStreams({
254
255
 
255
256
  ## Advanced Features
256
257
 
258
+ <details>
259
+ <parameter name="summary"><b>Channel Metadata (Infrastructure Integration)</b></summary>
260
+
261
+ <br />
262
+
263
+ Contracts can include **channel metadata** to define which NATS JetStream stream they belong to. This enables infrastructure-as-code workflows where streams are materialized from contracts.
264
+
265
+ ### Basic Contract with Channel
266
+
267
+ ```typescript
268
+ import { createContract } from '@crossdelta/cloudevents'
269
+ import { z } from 'zod'
270
+
271
+ export const OrdersCreatedContract = createContract({
272
+ type: 'orders.created',
273
+ channel: {
274
+ stream: 'ORDERS',
275
+ // subject defaults to 'orders.created' if omitted
276
+ },
277
+ schema: z.object({
278
+ orderId: z.string(),
279
+ total: z.number(),
280
+ }),
281
+ })
282
+ ```
283
+
284
+ ### Channel Configuration
285
+
286
+ ```typescript
287
+ interface ChannelConfig {
288
+ stream: string // JetStream stream name (e.g., 'ORDERS')
289
+ subject?: string // NATS subject (defaults to event type)
290
+ }
291
+ ```
292
+
293
+ **Subject Defaulting:**
294
+ - If `subject` is **not provided**, it defaults to the event `type`
295
+ - This follows the convention: `orders.created` → subject `orders.created`
296
+ - Override when needed: `subject: 'orders.v1.created'`
297
+
298
+ ### Multiple Events, Same Stream
299
+
300
+ ```typescript
301
+ // orders.created → ORDERS stream
302
+ export const OrdersCreatedContract = createContract({
303
+ type: 'orders.created',
304
+ channel: { stream: 'ORDERS' },
305
+ schema: OrderCreatedSchema,
306
+ })
307
+
308
+ // orders.updated → ORDERS stream (same stream!)
309
+ export const OrdersUpdatedContract = createContract({
310
+ type: 'orders.updated',
311
+ channel: { stream: 'ORDERS' },
312
+ schema: OrderUpdatedSchema,
313
+ })
314
+ ```
315
+
316
+ **Result:** Both events share the `ORDERS` stream with subjects `orders.created` and `orders.updated`.
317
+
318
+ ### Infrastructure Materialization
319
+
320
+ Channel metadata is **conceptual** - it defines routing, not infrastructure policy.
321
+
322
+ **Contracts define:**
323
+ - Event type (`orders.created`)
324
+ - Stream routing (`ORDERS`)
325
+ - Subject mapping (`orders.created`)
326
+
327
+ **Infrastructure defines:**
328
+ - Retention (7 days, 14 days, etc.)
329
+ - Storage (disk, memory)
330
+ - Replicas (1, 3, etc.)
331
+ - Limits (max message size, etc.)
332
+
333
+ **Example Infrastructure (Pulumi):**
334
+
335
+ ```typescript
336
+ import { collectStreamDefinitions } from './helpers/streams'
337
+ import { ensureJetStreams } from '@crossdelta/cloudevents'
338
+
339
+ // Collect from contracts
340
+ const streams = collectStreamDefinitions()
341
+ // [{ stream: 'ORDERS', subjects: ['orders.created', 'orders.updated'] }]
342
+
343
+ // Materialize with policies
344
+ await ensureJetStreams({
345
+ streams: streams.map(({ stream, subjects }) => ({
346
+ stream,
347
+ subjects,
348
+ config: {
349
+ maxAge: 14 * 24 * 60 * 60 * 1000, // 14 days
350
+ replicas: 3, // High availability
351
+ storage: 'file', // Persistent
352
+ },
353
+ })),
354
+ })
355
+ ```
356
+
357
+ ### Development vs. Production
358
+
359
+ | Environment | Streams | How | Retention |
360
+ |-------------|---------|-----|-----------|
361
+ | **Development** | Ephemeral | Auto-created by services | None (memory) |
362
+ | **Production** | Persistent | Materialized via infrastructure | Explicit (7d, 14d, etc.) |
363
+
364
+ **Key Principle:** Contracts define **what** and **where**, infrastructure defines **how** and **how long**.
365
+
366
+ ### Backward Compatibility
367
+
368
+ Channel metadata is **optional** - contracts without `channel` work as before:
369
+
370
+ ```typescript
371
+ // Legacy contract (still works)
372
+ export const LegacyContract = createContract({
373
+ type: 'legacy.event',
374
+ schema: LegacySchema,
375
+ // No channel - handler still works
376
+ })
377
+ ```
378
+
379
+ </details>
380
+
257
381
  <details>
258
382
  <summary><b>Shared Contracts</b></summary>
259
383
 
@@ -343,7 +467,7 @@ const redisStore = {
343
467
  await consumeJetStreams({
344
468
  streams: ['ORDERS'],
345
469
  consumer: 'my-service',
346
- discover: './src/events/**/*.event.ts',
470
+ discover: './src/events/**/*.handler.ts',
347
471
  idempotencyStore: redisStore,
348
472
  })
349
473
  ```
@@ -359,7 +483,7 @@ Invalid messages are quarantined, not lost:
359
483
  await consumeJetStreams({
360
484
  streams: ['ORDERS'],
361
485
  consumer: 'my-service',
362
- discover: './src/events/**/*.event.ts',
486
+ discover: './src/events/**/*.handler.ts',
363
487
  quarantineTopic: 'events.quarantine', // For malformed messages
364
488
  errorTopic: 'events.errors', // For handler errors
365
489
  })
@@ -377,7 +501,7 @@ import { Hono } from 'hono'
377
501
  import { cloudEvents } from '@crossdelta/cloudevents'
378
502
 
379
503
  const app = new Hono()
380
- app.use('/events', cloudEvents({ discover: 'src/events/**/*.event.ts' }))
504
+ app.use('/events', cloudEvents({ discover: 'src/events/**/*.handler.ts' }))
381
505
  ```
382
506
 
383
507
  </details>
@@ -390,9 +514,8 @@ app.use('/events', cloudEvents({ discover: 'src/events/**/*.event.ts' }))
390
514
  |----------|---------|
391
515
  | `handleEvent(options, handler)` | Create a handler |
392
516
  | `createContract(options)` | Create shared event contract |
393
- | `ensureJetStreams(options)` | Create/update JetStream streams (preferred) |
394
- | `consumeJetStreams(options)` | Consume from multiple streams (preferred) |
395
- | `consumeJetStreamEvents(options)` | Consume single stream (legacy) |
517
+ | `ensureJetStreams(options)` | Create/update JetStream streams |
518
+ | `consumeJetStreams(options)` | Consume from multiple streams |
396
519
  | `consumeNatsEvents(options)` | Consume fire-and-forget |
397
520
  | `publish(type, data)` | Publish event |
398
521
 
@@ -1,26 +1,50 @@
1
1
  import type { ZodTypeAny } from 'zod';
2
- import type { HandleEventOptions } from './types';
2
+ import type { ChannelMetadata, HandleEventOptions } from './types';
3
3
  /**
4
- * Creates a type-safe event contract for Advanced Mode
4
+ * Creates a type-safe event contract with optional channel metadata
5
5
  *
6
6
  * This helper ensures proper type inference when using contracts
7
7
  * with handleEvent across package boundaries.
8
8
  *
9
9
  * @example
10
+ * Basic contract without channel:
10
11
  * ```typescript
11
- * import { createContract } from '@crossdelta/cloudevents'
12
- * import { z } from 'zod'
12
+ * export const OrderCreatedContract = createContract({
13
+ * type: 'orders.created',
14
+ * schema: z.object({
15
+ * orderId: z.string(),
16
+ * total: z.number(),
17
+ * }),
18
+ * })
19
+ * ```
13
20
  *
21
+ * @example
22
+ * Contract with channel metadata:
23
+ * ```typescript
14
24
  * export const OrderCreatedContract = createContract({
15
25
  * type: 'orders.created',
26
+ * channel: {
27
+ * stream: 'ORDERS',
28
+ * // subject defaults to 'orders.created' if omitted
29
+ * },
16
30
  * schema: z.object({
17
31
  * orderId: z.string(),
18
32
  * total: z.number(),
19
33
  * }),
20
34
  * })
35
+ * ```
21
36
  *
22
- * // Type is inferred: { orderId: string, total: number }
23
- * export type OrderCreatedData = typeof OrderCreatedContract._schema._output
37
+ * @example
38
+ * Contract with explicit subject:
39
+ * ```typescript
40
+ * export const OrderCreatedContract = createContract({
41
+ * type: 'orders.created',
42
+ * channel: {
43
+ * stream: 'ORDERS',
44
+ * subject: 'orders.v1.created', // Override default
45
+ * },
46
+ * schema: OrderSchema,
47
+ * })
24
48
  * ```
25
49
  */
26
50
  export declare function createContract<TSchema extends ZodTypeAny>(options: {
@@ -29,6 +53,11 @@ export declare function createContract<TSchema extends ZodTypeAny>(options: {
29
53
  match?: HandleEventOptions['match'];
30
54
  safeParse?: boolean;
31
55
  tenantId?: string | string[];
56
+ channel?: {
57
+ stream: string;
58
+ subject?: string;
59
+ };
32
60
  }): HandleEventOptions<TSchema> & {
33
61
  _schema: TSchema;
62
+ channel?: ChannelMetadata;
34
63
  };
@@ -1,29 +1,61 @@
1
1
  /**
2
- * Creates a type-safe event contract for Advanced Mode
2
+ * Creates a type-safe event contract with optional channel metadata
3
3
  *
4
4
  * This helper ensures proper type inference when using contracts
5
5
  * with handleEvent across package boundaries.
6
6
  *
7
7
  * @example
8
+ * Basic contract without channel:
8
9
  * ```typescript
9
- * import { createContract } from '@crossdelta/cloudevents'
10
- * import { z } from 'zod'
10
+ * export const OrderCreatedContract = createContract({
11
+ * type: 'orders.created',
12
+ * schema: z.object({
13
+ * orderId: z.string(),
14
+ * total: z.number(),
15
+ * }),
16
+ * })
17
+ * ```
11
18
  *
19
+ * @example
20
+ * Contract with channel metadata:
21
+ * ```typescript
12
22
  * export const OrderCreatedContract = createContract({
13
23
  * type: 'orders.created',
24
+ * channel: {
25
+ * stream: 'ORDERS',
26
+ * // subject defaults to 'orders.created' if omitted
27
+ * },
14
28
  * schema: z.object({
15
29
  * orderId: z.string(),
16
30
  * total: z.number(),
17
31
  * }),
18
32
  * })
33
+ * ```
19
34
  *
20
- * // Type is inferred: { orderId: string, total: number }
21
- * export type OrderCreatedData = typeof OrderCreatedContract._schema._output
35
+ * @example
36
+ * Contract with explicit subject:
37
+ * ```typescript
38
+ * export const OrderCreatedContract = createContract({
39
+ * type: 'orders.created',
40
+ * channel: {
41
+ * stream: 'ORDERS',
42
+ * subject: 'orders.v1.created', // Override default
43
+ * },
44
+ * schema: OrderSchema,
45
+ * })
22
46
  * ```
23
47
  */
24
48
  export function createContract(options) {
49
+ // Resolve channel metadata: default subject to type if not provided
50
+ const resolvedChannel = options.channel
51
+ ? {
52
+ stream: options.channel.stream,
53
+ subject: options.channel.subject ?? options.type,
54
+ }
55
+ : undefined;
25
56
  return {
26
57
  ...options,
27
58
  _schema: options.schema,
59
+ channel: resolvedChannel,
28
60
  };
29
61
  }
@@ -2,17 +2,17 @@ import type { HandlerConstructor } from './types';
2
2
  /**
3
3
  * Discovers event handlers from files matching the given glob pattern.
4
4
  *
5
- * @param pattern - Glob pattern to match handler files (e.g., 'events/*.event.ts')
5
+ * @param pattern - Glob pattern to match handler files (e.g., 'events/*.handler.ts')
6
6
  * @param options - Configuration options
7
7
  * @returns Promise resolving to array of discovered handler constructors
8
8
  *
9
9
  * @example
10
10
  * ```typescript
11
11
  * // Discover all handlers in events directory
12
- * const handlers = await discoverHandlers('events/*.event.ts')
12
+ * const handlers = await discoverHandlers('events/*.handler.ts')
13
13
  *
14
14
  * // With filtering and logging
15
- * const handlers = await discoverHandlers('events/*.event.ts', {
15
+ * const handlers = await discoverHandlers('events/*.handler.ts', {
16
16
  * filter: (name, handler) => name.includes('Customer'),
17
17
  * log: true
18
18
  * })
@@ -144,17 +144,17 @@ const discoverFiles = async (pattern, basePath, preferCompiled) => {
144
144
  /**
145
145
  * Discovers event handlers from files matching the given glob pattern.
146
146
  *
147
- * @param pattern - Glob pattern to match handler files (e.g., 'events/*.event.ts')
147
+ * @param pattern - Glob pattern to match handler files (e.g., 'events/*.handler.ts')
148
148
  * @param options - Configuration options
149
149
  * @returns Promise resolving to array of discovered handler constructors
150
150
  *
151
151
  * @example
152
152
  * ```typescript
153
153
  * // Discover all handlers in events directory
154
- * const handlers = await discoverHandlers('events/*.event.ts')
154
+ * const handlers = await discoverHandlers('events/*.handler.ts')
155
155
  *
156
156
  * // With filtering and logging
157
- * const handlers = await discoverHandlers('events/*.event.ts', {
157
+ * const handlers = await discoverHandlers('events/*.handler.ts', {
158
158
  * filter: (name, handler) => name.includes('Customer'),
159
159
  * log: true
160
160
  * })
@@ -1,6 +1,6 @@
1
- export { discoverHandlers } from './discovery';
2
1
  export { createContract } from './contract-helper';
2
+ export { discoverHandlers } from './discovery';
3
3
  export { eventSchema, handleEvent } from './handler-factory';
4
- export type { EnrichedEvent, EventHandler, HandleEventOptions, HandlerConstructor, HandlerMetadata, IdempotencyStore, InferEventData, MatchFn, RoutingConfig, } from './types';
4
+ export type { ChannelConfig, ChannelMetadata, EnrichedEvent, EventHandler, HandleEventOptions, HandlerConstructor, HandlerMetadata, IdempotencyStore, InferEventData, MatchFn, RoutingConfig, } from './types';
5
5
  export type { HandlerValidationError, ValidationErrorDetail } from './validation';
6
6
  export { extractTypeFromSchema, isValidHandler } from './validation';
@@ -1,4 +1,4 @@
1
- export { discoverHandlers } from './discovery';
2
1
  export { createContract } from './contract-helper';
2
+ export { discoverHandlers } from './discovery';
3
3
  export { eventSchema, handleEvent } from './handler-factory';
4
4
  export { extractTypeFromSchema, isValidHandler } from './validation';
@@ -43,6 +43,24 @@ export type HandlerMetadata<S extends ZodTypeAny> = {
43
43
  match?: MatchFn<unknown>;
44
44
  safeParse: boolean;
45
45
  };
46
+ /**
47
+ * Channel metadata for NATS JetStream routing
48
+ */
49
+ export interface ChannelMetadata {
50
+ /** JetStream stream name (e.g., 'ORDERS') */
51
+ stream: string;
52
+ /** NATS subject (defaults to event type if not specified) */
53
+ subject: string;
54
+ }
55
+ /**
56
+ * Channel configuration input (subject is optional, defaults to type)
57
+ */
58
+ export interface ChannelConfig {
59
+ /** JetStream stream name (e.g., 'ORDERS') */
60
+ stream: string;
61
+ /** NATS subject (optional, defaults to event type) */
62
+ subject?: string;
63
+ }
46
64
  /**
47
65
  * Options for creating event handlers
48
66
  *
@@ -56,6 +74,8 @@ export interface HandleEventOptions<S = ZodTypeAny> {
56
74
  safeParse?: boolean;
57
75
  /** Filter events by tenant ID(s). If set, only events matching these tenant(s) are processed. */
58
76
  tenantId?: string | string[];
77
+ /** Channel metadata for NATS JetStream routing (optional) */
78
+ channel?: ChannelConfig;
59
79
  }
60
80
  /**
61
81
  * Routing configuration for type → subject → stream mapping
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export type { EventContext } from './adapters/cloudevents';
2
2
  export { parseEventFromContext } from './adapters/cloudevents';
3
- export type { EnrichedEvent, HandleEventOptions, IdempotencyStore, InferEventData, RoutingConfig } from './domain';
3
+ export type { ChannelConfig, ChannelMetadata, EnrichedEvent, HandleEventOptions, IdempotencyStore, InferEventData, RoutingConfig, } from './domain';
4
4
  export { createContract, eventSchema, extractTypeFromSchema, handleEvent } from './domain';
5
5
  export { clearHandlerCache, cloudEvents } from './middlewares';
6
6
  export { checkAndMarkProcessed, createInMemoryIdempotencyStore, getDefaultIdempotencyStore, resetDefaultIdempotencyStore, } from './processing/idempotency';
@@ -28,7 +28,7 @@ export interface InMemoryIdempotencyStoreOptions {
28
28
  * stream: 'ORDERS',
29
29
  * subjects: ['orders.>'],
30
30
  * consumer: 'notifications',
31
- * discover: './src/handlers/**\/*.event.ts',
31
+ * discover: `./src/events/**\/*.handler.ts`,
32
32
  * idempotencyStore: store,
33
33
  * })
34
34
  * ```
@@ -18,7 +18,7 @@
18
18
  * stream: 'ORDERS',
19
19
  * subjects: ['orders.>'],
20
20
  * consumer: 'notifications',
21
- * discover: './src/handlers/**\/*.event.ts',
21
+ * discover: `./src/events/**\/*.handler.ts`,
22
22
  * idempotencyStore: store,
23
23
  * })
24
24
  * ```
@@ -1,59 +1,19 @@
1
1
  import type { ZodTypeAny } from 'zod';
2
2
  import { type RoutingConfig } from '../domain';
3
3
  export interface PublishNatsEventOptions {
4
- /**
5
- * NATS URL(s), e.g., "nats://localhost:4222".
6
- * Defaults to `process.env.NATS_URL` or `nats://localhost:4222`.
7
- */
8
4
  servers?: string;
9
- /**
10
- * CloudEvent source identifier (e.g., "orderboss://orders-service").
11
- */
12
5
  source?: string;
13
- /**
14
- * Optional CloudEvent subject (e.g., an order ID). Not to be confused with the NATS subject.
15
- */
16
6
  subject?: string;
17
- /**
18
- * Tenant identifier for multi-tenant event routing.
19
- * Will be added as a CloudEvent extension attribute.
20
- */
21
7
  tenantId?: string;
22
8
  }
23
- /**
24
- * Derives a NATS subject from a CloudEvent type using routing configuration.
25
- *
26
- * @example
27
- * ```typescript
28
- * const config: RoutingConfig = {
29
- * typeToSubjectMap: { 'orderboss.orders': 'orders' },
30
- * defaultSubjectPrefix: 'events',
31
- * }
32
- *
33
- * deriveSubjectFromType('orderboss.orders.created', config)
34
- * // Returns: 'orders.created'
35
- *
36
- * deriveSubjectFromType('unknown.event.type', config)
37
- * // Returns: 'events.unknown.event.type'
38
- * ```
39
- */
40
- export declare function deriveSubjectFromType(eventType: string, config?: RoutingConfig): string;
41
- /**
42
- * Derives a JetStream stream name from a CloudEvent type using routing configuration.
43
- */
44
- export declare function deriveStreamFromType(eventType: string, config?: RoutingConfig): string | undefined;
45
- export declare function publishNatsEvent<T extends ZodTypeAny>(subjectName: string, schema: T, eventData: unknown, options?: PublishNatsEventOptions): Promise<string>;
46
- export declare function publishNatsRawEvent(subjectName: string, eventType: string, eventData: unknown, options?: PublishNatsEventOptions): Promise<string>;
47
- /**
48
- * Simplified publish function where subject equals event type.
49
- *
50
- * @example
51
- * ```typescript
52
- * await publish('orders.created', { orderId: '123', total: 99.99 })
53
- * ```
54
- */
55
- export declare function publish(eventType: string, eventData: unknown, options?: PublishNatsEventOptions): Promise<string>;
56
- /**
57
- * @internal Resets the cached NATS connection. Intended for testing only.
58
- */
59
- export declare function __resetNatsPublisher(): void;
9
+ export declare const deriveSubjectFromType: (eventType: string, config?: RoutingConfig) => string;
10
+ export declare const deriveStreamFromType: (eventType: string, config?: RoutingConfig) => string | undefined;
11
+ export declare const publishNatsRawEvent: (subjectName: string, eventType: string, eventData: unknown, options?: PublishNatsEventOptions) => Promise<string>;
12
+ export declare const publishNatsEvent: <T extends ZodTypeAny>(subjectName: string, schema: T, eventData: unknown, options?: PublishNatsEventOptions) => Promise<string>;
13
+ export declare const publish: (eventTypeOrContract: string | {
14
+ type: string;
15
+ channel?: {
16
+ subject?: string;
17
+ };
18
+ }, eventData: unknown, options?: PublishNatsEventOptions) => Promise<string>;
19
+ export declare const __resetNatsPublisher: () => void;
@@ -2,49 +2,59 @@ import { connect, StringCodec } from 'nats';
2
2
  import { extractTypeFromSchema } from '../domain';
3
3
  import { createValidationError, logger } from '../infrastructure';
4
4
  const sc = StringCodec();
5
- /**
6
- * Derives a NATS subject from a CloudEvent type using routing configuration.
7
- *
8
- * @example
9
- * ```typescript
10
- * const config: RoutingConfig = {
11
- * typeToSubjectMap: { 'orderboss.orders': 'orders' },
12
- * defaultSubjectPrefix: 'events',
13
- * }
14
- *
15
- * deriveSubjectFromType('orderboss.orders.created', config)
16
- * // Returns: 'orders.created'
17
- *
18
- * deriveSubjectFromType('unknown.event.type', config)
19
- * // Returns: 'events.unknown.event.type'
20
- * ```
21
- */
22
- export function deriveSubjectFromType(eventType, config) {
5
+ let natsConnectionPromise = null;
6
+ const pluralize = (word) => {
7
+ if (word.endsWith('y') && !['a', 'e', 'i', 'o', 'u'].includes(word[word.length - 2])) {
8
+ return `${word.slice(0, -1)}ies`;
9
+ }
10
+ if (word.endsWith('s') || word.endsWith('sh') || word.endsWith('ch') || word.endsWith('x')) {
11
+ return `${word}es`;
12
+ }
13
+ return `${word}s`;
14
+ };
15
+ const deriveSubjectFromEventType = (eventType) => {
16
+ const parts = eventType.split('.');
17
+ if (parts.length < 2)
18
+ return eventType;
19
+ const domain = parts[0];
20
+ const action = parts.slice(1).join('.');
21
+ const pluralDomain = pluralize(domain);
22
+ return `${pluralDomain}.${action}`;
23
+ };
24
+ const getNatsConnection = async (servers) => {
25
+ if (!natsConnectionPromise) {
26
+ const url = servers ?? process.env.NATS_URL ?? 'nats://localhost:4222';
27
+ natsConnectionPromise = connect({ servers: url })
28
+ .then((connection) => {
29
+ logger.debug(`[NATS] connected to ${url}`);
30
+ return connection;
31
+ })
32
+ .catch((error) => {
33
+ logger.error('[NATS] connection error', error);
34
+ natsConnectionPromise = null;
35
+ throw error;
36
+ });
37
+ }
38
+ return natsConnectionPromise;
39
+ };
40
+ export const deriveSubjectFromType = (eventType, config) => {
23
41
  if (!config?.typeToSubjectMap) {
24
- // No mapping configured, use type as-is (replace dots with dots is a no-op)
25
42
  return config?.defaultSubjectPrefix ? `${config.defaultSubjectPrefix}.${eventType}` : eventType;
26
43
  }
27
- // Find the longest matching prefix
28
44
  const sortedPrefixes = Object.keys(config.typeToSubjectMap).sort((a, b) => b.length - a.length);
29
45
  for (const prefix of sortedPrefixes) {
30
46
  if (eventType.startsWith(prefix)) {
31
47
  const suffix = eventType.slice(prefix.length);
32
48
  const mappedPrefix = config.typeToSubjectMap[prefix];
33
- // Remove leading dot from suffix if present
34
49
  const cleanSuffix = suffix.startsWith('.') ? suffix.slice(1) : suffix;
35
50
  return cleanSuffix ? `${mappedPrefix}.${cleanSuffix}` : mappedPrefix;
36
51
  }
37
52
  }
38
- // No match found, use default prefix or type as-is
39
53
  return config.defaultSubjectPrefix ? `${config.defaultSubjectPrefix}.${eventType}` : eventType;
40
- }
41
- /**
42
- * Derives a JetStream stream name from a CloudEvent type using routing configuration.
43
- */
44
- export function deriveStreamFromType(eventType, config) {
54
+ };
55
+ export const deriveStreamFromType = (eventType, config) => {
45
56
  if (!config?.typeToStreamMap)
46
57
  return undefined;
47
- // Find the longest matching prefix
48
58
  const sortedPrefixes = Object.keys(config.typeToStreamMap).sort((a, b) => b.length - a.length);
49
59
  for (const prefix of sortedPrefixes) {
50
60
  if (eventType.startsWith(prefix)) {
@@ -52,25 +62,26 @@ export function deriveStreamFromType(eventType, config) {
52
62
  }
53
63
  }
54
64
  return undefined;
55
- }
56
- let natsConnectionPromise = null;
57
- async function getNatsConnection(servers) {
58
- if (!natsConnectionPromise) {
59
- const url = servers ?? process.env.NATS_URL ?? 'nats://localhost:4222';
60
- natsConnectionPromise = connect({ servers: url })
61
- .then((connection) => {
62
- logger.debug(`[NATS] connected to ${url}`);
63
- return connection;
64
- })
65
- .catch((error) => {
66
- logger.error('[NATS] connection error', error);
67
- natsConnectionPromise = null;
68
- throw error;
69
- });
70
- }
71
- return natsConnectionPromise;
72
- }
73
- export async function publishNatsEvent(subjectName, schema, eventData, options) {
65
+ };
66
+ export const publishNatsRawEvent = async (subjectName, eventType, eventData, options) => {
67
+ const cloudEvent = {
68
+ specversion: '1.0',
69
+ type: eventType,
70
+ source: options?.source || 'hono-service',
71
+ id: crypto.randomUUID(),
72
+ time: new Date().toISOString(),
73
+ datacontenttype: 'application/json',
74
+ data: eventData,
75
+ ...(options?.subject && { subject: options.subject }),
76
+ ...(options?.tenantId && { tenantid: options.tenantId }),
77
+ };
78
+ const data = JSON.stringify(cloudEvent);
79
+ const nc = await getNatsConnection(options?.servers);
80
+ nc.publish(subjectName, sc.encode(data));
81
+ logger.debug(`Published CloudEvent ${eventType} to NATS subject ${subjectName} (id=${cloudEvent.id})`);
82
+ return cloudEvent.id;
83
+ };
84
+ export const publishNatsEvent = async (subjectName, schema, eventData, options) => {
74
85
  const eventType = extractTypeFromSchema(schema);
75
86
  if (!eventType) {
76
87
  throw new Error('Could not extract event type from schema. Make sure your schema has proper metadata.');
@@ -91,39 +102,14 @@ export async function publishNatsEvent(subjectName, schema, eventData, options)
91
102
  throw createValidationError(eventType, [handlerValidationError]);
92
103
  }
93
104
  return publishNatsRawEvent(subjectName, eventType, validationResult.data, options);
94
- }
95
- export async function publishNatsRawEvent(subjectName, eventType, eventData, options) {
96
- const cloudEvent = {
97
- specversion: '1.0',
98
- type: eventType,
99
- source: options?.source || 'hono-service',
100
- id: crypto.randomUUID(),
101
- time: new Date().toISOString(),
102
- datacontenttype: 'application/json',
103
- data: eventData,
104
- ...(options?.subject && { subject: options.subject }),
105
- ...(options?.tenantId && { tenantid: options.tenantId }), // CloudEvents extension (lowercase)
106
- };
107
- const data = JSON.stringify(cloudEvent);
108
- const nc = await getNatsConnection(options?.servers);
109
- nc.publish(subjectName, sc.encode(data));
110
- logger.debug(`Published CloudEvent ${eventType} to NATS subject ${subjectName} (id=${cloudEvent.id})`);
111
- return cloudEvent.id;
112
- }
113
- /**
114
- * Simplified publish function where subject equals event type.
115
- *
116
- * @example
117
- * ```typescript
118
- * await publish('orders.created', { orderId: '123', total: 99.99 })
119
- * ```
120
- */
121
- export async function publish(eventType, eventData, options) {
122
- return publishNatsRawEvent(eventType, eventType, eventData, options);
123
- }
124
- /**
125
- * @internal Resets the cached NATS connection. Intended for testing only.
126
- */
127
- export function __resetNatsPublisher() {
105
+ };
106
+ export const publish = async (eventTypeOrContract, eventData, options) => {
107
+ const eventType = typeof eventTypeOrContract === 'string' ? eventTypeOrContract : eventTypeOrContract.type;
108
+ const natsSubject = typeof eventTypeOrContract === 'string'
109
+ ? deriveSubjectFromEventType(eventTypeOrContract)
110
+ : (eventTypeOrContract.channel?.subject ?? eventTypeOrContract.type);
111
+ return publishNatsRawEvent(natsSubject, eventType, eventData, options);
112
+ };
113
+ export const __resetNatsPublisher = () => {
128
114
  natsConnectionPromise = null;
129
- }
115
+ };
@@ -157,7 +157,7 @@ export declare function ensureJetStreamStreams(options: JetStreamStreamsOptions)
157
157
  * stream: 'ORDERS',
158
158
  * subjects: ['orders.>'],
159
159
  * consumer: 'notifications',
160
- * discover: './src/handlers/**\/*.event.ts',
160
+ * discover: `./src/events/**\/*.handler.ts`,
161
161
  * })
162
162
  * ```
163
163
  */
@@ -200,7 +200,7 @@ export interface JetStreamStreamsConsumerOptions extends Pick<CloudEventsOptions
200
200
  * await consumeJetStreamStreams({
201
201
  * streams: ['ORDERS', 'CUSTOMERS'],
202
202
  * consumer: 'notifications',
203
- * discover: './src/events/**\/*.event.ts',
203
+ * discover: `./src/events/**\/*.handler.ts`,
204
204
  * })
205
205
  * ```
206
206
  */
@@ -167,7 +167,7 @@ async function ensureConsumer(jsm, streamName, consumerName, options) {
167
167
  * stream: 'ORDERS',
168
168
  * subjects: ['orders.>'],
169
169
  * consumer: 'notifications',
170
- * discover: './src/handlers/**\/*.event.ts',
170
+ * discover: `./src/events/**\/*.handler.ts`,
171
171
  * })
172
172
  * ```
173
173
  */
@@ -264,7 +264,7 @@ export async function consumeJetStreamEvents(options) {
264
264
  * await consumeJetStreamStreams({
265
265
  * streams: ['ORDERS', 'CUSTOMERS'],
266
266
  * consumer: 'notifications',
267
- * discover: './src/events/**\/*.event.ts',
267
+ * discover: `./src/events/**\/*.handler.ts`,
268
268
  * })
269
269
  * ```
270
270
  */
@@ -44,7 +44,7 @@ export async function consumeNatsEvents(options) {
44
44
  const pass = options.pass ?? process.env.NATS_PASSWORD;
45
45
  // Cleanup existing consumer with same name (handles hot-reload)
46
46
  await cleanupConsumer(name);
47
- // 1) Discover handler classes from *.event.ts files
47
+ // 1) Discover handler classes from *.handler.ts files
48
48
  const handlerConstructors = await discoverHandlers(options.discover);
49
49
  const processedHandlers = handlerConstructors
50
50
  .map(processHandler)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crossdelta/cloudevents",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
4
4
  "description": "CloudEvents toolkit for TypeScript - Zod validation, handler discovery, NATS JetStream",
5
5
  "author": "crossdelta",
6
6
  "license": "MIT",
@@ -23,15 +23,15 @@
23
23
  ],
24
24
  "exports": {
25
25
  ".": {
26
+ "types": "./dist/index.d.ts",
26
27
  "import": "./dist/index.js",
27
28
  "require": "./dist/index.js",
28
- "types": "./dist/index.d.ts",
29
29
  "default": "./dist/index.js"
30
30
  },
31
31
  "./transports/nats": {
32
+ "types": "./dist/src/transports/nats/index.d.ts",
32
33
  "import": "./dist/src/transports/nats/index.js",
33
34
  "require": "./dist/src/transports/nats/index.js",
34
- "types": "./dist/src/transports/nats/index.d.ts",
35
35
  "default": "./dist/src/transports/nats/index.js"
36
36
  }
37
37
  },