@crossdelta/cloudevents 0.3.3 → 0.4.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 +111 -17
- package/dist/domain/contract-helper.d.ts +34 -0
- package/dist/domain/contract-helper.js +29 -0
- package/dist/{src/domain → domain}/discovery.js +46 -6
- package/dist/{src/domain → domain}/handler-factory.d.ts +13 -2
- package/dist/{src/domain → domain}/handler-factory.js +12 -1
- package/dist/{src/domain → domain}/index.d.ts +1 -0
- package/dist/{src/domain → domain}/index.js +1 -0
- package/dist/{src/domain → domain}/types.d.ts +4 -1
- package/dist/{src/index.d.ts → index.d.ts} +3 -1
- package/dist/{src/index.js → index.js} +2 -0
- package/dist/{src/transports → transports}/nats/jetstream-consumer.d.ts +43 -5
- package/dist/{src/transports → transports}/nats/jetstream-consumer.js +64 -53
- package/package.json +7 -6
- /package/dist/{src/adapters → adapters}/cloudevents/cloudevents.d.ts +0 -0
- /package/dist/{src/adapters → adapters}/cloudevents/cloudevents.js +0 -0
- /package/dist/{src/adapters → adapters}/cloudevents/index.d.ts +0 -0
- /package/dist/{src/adapters → adapters}/cloudevents/index.js +0 -0
- /package/dist/{src/adapters → adapters}/cloudevents/parsers/binary-mode.d.ts +0 -0
- /package/dist/{src/adapters → adapters}/cloudevents/parsers/binary-mode.js +0 -0
- /package/dist/{src/adapters → adapters}/cloudevents/parsers/pubsub.d.ts +0 -0
- /package/dist/{src/adapters → adapters}/cloudevents/parsers/pubsub.js +0 -0
- /package/dist/{src/adapters → adapters}/cloudevents/parsers/raw-event.d.ts +0 -0
- /package/dist/{src/adapters → adapters}/cloudevents/parsers/raw-event.js +0 -0
- /package/dist/{src/adapters → adapters}/cloudevents/parsers/structured-mode.d.ts +0 -0
- /package/dist/{src/adapters → adapters}/cloudevents/parsers/structured-mode.js +0 -0
- /package/dist/{src/adapters → adapters}/cloudevents/types.d.ts +0 -0
- /package/dist/{src/adapters → adapters}/cloudevents/types.js +0 -0
- /package/dist/{src/domain → domain}/discovery.d.ts +0 -0
- /package/dist/{src/domain → domain}/types.js +0 -0
- /package/dist/{src/domain → domain}/validation.d.ts +0 -0
- /package/dist/{src/domain → domain}/validation.js +0 -0
- /package/dist/{src/infrastructure → infrastructure}/errors.d.ts +0 -0
- /package/dist/{src/infrastructure → infrastructure}/errors.js +0 -0
- /package/dist/{src/infrastructure → infrastructure}/index.d.ts +0 -0
- /package/dist/{src/infrastructure → infrastructure}/index.js +0 -0
- /package/dist/{src/infrastructure → infrastructure}/logging.d.ts +0 -0
- /package/dist/{src/infrastructure → infrastructure}/logging.js +0 -0
- /package/dist/{src/middlewares → middlewares}/cloudevents-middleware.d.ts +0 -0
- /package/dist/{src/middlewares → middlewares}/cloudevents-middleware.js +0 -0
- /package/dist/{src/middlewares → middlewares}/index.d.ts +0 -0
- /package/dist/{src/middlewares → middlewares}/index.js +0 -0
- /package/dist/{src/processing → processing}/dlq-safe.d.ts +0 -0
- /package/dist/{src/processing → processing}/dlq-safe.js +0 -0
- /package/dist/{src/processing → processing}/handler-cache.d.ts +0 -0
- /package/dist/{src/processing → processing}/handler-cache.js +0 -0
- /package/dist/{src/processing → processing}/idempotency.d.ts +0 -0
- /package/dist/{src/processing → processing}/idempotency.js +0 -0
- /package/dist/{src/processing → processing}/index.d.ts +0 -0
- /package/dist/{src/processing → processing}/index.js +0 -0
- /package/dist/{src/processing → processing}/validation.d.ts +0 -0
- /package/dist/{src/processing → processing}/validation.js +0 -0
- /package/dist/{src/publishing → publishing}/index.d.ts +0 -0
- /package/dist/{src/publishing → publishing}/index.js +0 -0
- /package/dist/{src/publishing → publishing}/nats.publisher.d.ts +0 -0
- /package/dist/{src/publishing → publishing}/nats.publisher.js +0 -0
- /package/dist/{src/publishing → publishing}/pubsub.publisher.d.ts +0 -0
- /package/dist/{src/publishing → publishing}/pubsub.publisher.js +0 -0
- /package/dist/{src/transports → transports}/nats/base-message-processor.d.ts +0 -0
- /package/dist/{src/transports → transports}/nats/base-message-processor.js +0 -0
- /package/dist/{src/transports → transports}/nats/index.d.ts +0 -0
- /package/dist/{src/transports → transports}/nats/index.js +0 -0
- /package/dist/{src/transports → transports}/nats/jetstream-message-processor.d.ts +0 -0
- /package/dist/{src/transports → transports}/nats/jetstream-message-processor.js +0 -0
- /package/dist/{src/transports → transports}/nats/nats-consumer.d.ts +0 -0
- /package/dist/{src/transports → transports}/nats/nats-consumer.js +0 -0
- /package/dist/{src/transports → transports}/nats/nats-message-processor.d.ts +0 -0
- /package/dist/{src/transports → transports}/nats/nats-message-processor.js +0 -0
package/README.md
CHANGED
|
@@ -29,13 +29,14 @@ bun add @crossdelta/cloudevents zod@4
|
|
|
29
29
|
|
|
30
30
|
## Quick Start
|
|
31
31
|
|
|
32
|
-
**1. Create an event handler** (`src/
|
|
32
|
+
**1. Create an event handler** (`src/events/order-created.event.ts`):
|
|
33
33
|
|
|
34
34
|
```typescript
|
|
35
35
|
import { handleEvent } from '@crossdelta/cloudevents'
|
|
36
36
|
import { z } from 'zod'
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
// Export schema for mock generation
|
|
39
|
+
export const OrderCreatedSchema = z.object({
|
|
39
40
|
orderId: z.string(),
|
|
40
41
|
total: z.number(),
|
|
41
42
|
})
|
|
@@ -54,20 +55,30 @@ export default handleEvent(
|
|
|
54
55
|
)
|
|
55
56
|
```
|
|
56
57
|
|
|
57
|
-
**2.
|
|
58
|
+
**2. Ensure stream exists** (once, during setup):
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
import { ensureJetStreamStream } from '@crossdelta/cloudevents'
|
|
62
|
+
|
|
63
|
+
await ensureJetStreamStream({
|
|
64
|
+
stream: 'ORDERS',
|
|
65
|
+
subjects: ['orders.*'],
|
|
66
|
+
})
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**3. Start consuming:**
|
|
58
70
|
|
|
59
71
|
```typescript
|
|
60
72
|
import { consumeJetStreamEvents } from '@crossdelta/cloudevents'
|
|
61
73
|
|
|
62
74
|
await consumeJetStreamEvents({
|
|
63
|
-
stream: 'ORDERS',
|
|
64
|
-
subjects: ['orders.*'],
|
|
75
|
+
stream: 'ORDERS',
|
|
65
76
|
consumer: 'my-service',
|
|
66
|
-
discover: './src/
|
|
77
|
+
discover: './src/events/**/*.event.ts',
|
|
67
78
|
})
|
|
68
79
|
```
|
|
69
80
|
|
|
70
|
-
**
|
|
81
|
+
**4. Publish from another service:**
|
|
71
82
|
|
|
72
83
|
```typescript
|
|
73
84
|
import { publish } from '@crossdelta/cloudevents'
|
|
@@ -118,7 +129,7 @@ export default handleEvent(
|
|
|
118
129
|
Drop a `*.event.ts` file anywhere — it's auto-registered:
|
|
119
130
|
|
|
120
131
|
```typescript
|
|
121
|
-
// src/
|
|
132
|
+
// src/events/user-signup.event.ts
|
|
122
133
|
import { z } from 'zod'
|
|
123
134
|
|
|
124
135
|
const UserSignupSchema = z.object({
|
|
@@ -146,21 +157,38 @@ export default handleEvent(
|
|
|
146
157
|
await publish('orders.created', orderData)
|
|
147
158
|
```
|
|
148
159
|
|
|
160
|
+
### Stream Setup
|
|
161
|
+
|
|
162
|
+
Create the stream once during infrastructure setup:
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
import { ensureJetStreamStream } from '@crossdelta/cloudevents'
|
|
166
|
+
|
|
167
|
+
await ensureJetStreamStream({
|
|
168
|
+
stream: 'ORDERS',
|
|
169
|
+
subjects: ['orders.>'],
|
|
170
|
+
config: {
|
|
171
|
+
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days retention
|
|
172
|
+
replicas: 3, // For HA
|
|
173
|
+
}
|
|
174
|
+
})
|
|
175
|
+
```
|
|
176
|
+
|
|
149
177
|
### Consuming
|
|
150
178
|
|
|
151
179
|
```typescript
|
|
152
180
|
// JetStream (recommended) — persistent, retries, exactly-once
|
|
153
181
|
await consumeJetStreamEvents({
|
|
154
182
|
stream: 'ORDERS',
|
|
155
|
-
subjects: ['orders.*'],
|
|
156
183
|
consumer: 'billing',
|
|
157
|
-
discover: './src/
|
|
184
|
+
discover: './src/events/**/*.event.ts',
|
|
185
|
+
filterSubjects: ['orders.created', 'orders.updated'], // Optional: filter at consumer level
|
|
158
186
|
})
|
|
159
187
|
|
|
160
188
|
// Core NATS — fire-and-forget, simpler
|
|
161
189
|
await consumeNatsEvents({
|
|
162
190
|
subjects: ['notifications.*'],
|
|
163
|
-
discover: './src/
|
|
191
|
+
discover: './src/events/**/*.event.ts',
|
|
164
192
|
})
|
|
165
193
|
```
|
|
166
194
|
|
|
@@ -176,21 +204,40 @@ NATS_USER=myuser # optional
|
|
|
176
204
|
NATS_PASSWORD=mypass # optional
|
|
177
205
|
```
|
|
178
206
|
|
|
207
|
+
### Stream Options
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
await ensureJetStreamStream({
|
|
211
|
+
// Required
|
|
212
|
+
stream: 'ORDERS',
|
|
213
|
+
subjects: ['orders.>'],
|
|
214
|
+
|
|
215
|
+
// Optional
|
|
216
|
+
servers: 'nats://localhost:4222',
|
|
217
|
+
config: {
|
|
218
|
+
maxAge: 7 * 24 * 60 * 60 * 1000, // Message retention (ms)
|
|
219
|
+
replicas: 1, // Replication factor
|
|
220
|
+
storage: 'file', // 'file' or 'memory'
|
|
221
|
+
retention: 'limits', // 'limits', 'interest', or 'workqueue'
|
|
222
|
+
}
|
|
223
|
+
})
|
|
224
|
+
```
|
|
225
|
+
|
|
179
226
|
### Consumer Options
|
|
180
227
|
|
|
181
228
|
```typescript
|
|
182
229
|
await consumeJetStreamEvents({
|
|
183
230
|
// Required
|
|
184
231
|
stream: 'ORDERS',
|
|
185
|
-
subjects: ['orders.*'],
|
|
186
232
|
consumer: 'my-service',
|
|
187
|
-
discover: './src/
|
|
233
|
+
discover: './src/events/**/*.event.ts',
|
|
188
234
|
|
|
189
235
|
// Optional
|
|
190
236
|
servers: 'nats://localhost:4222',
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
237
|
+
filterSubjects: ['orders.created'], // Filter specific subjects
|
|
238
|
+
maxDeliver: 5, // Retry attempts
|
|
239
|
+
ackWait: 30_000, // Timeout per attempt (ms)
|
|
240
|
+
quarantineTopic: 'dlq', // For poison messages
|
|
194
241
|
})
|
|
195
242
|
```
|
|
196
243
|
|
|
@@ -198,6 +245,51 @@ await consumeJetStreamEvents({
|
|
|
198
245
|
|
|
199
246
|
## Advanced Features
|
|
200
247
|
|
|
248
|
+
<details>
|
|
249
|
+
<summary><b>Shared Contracts</b></summary>
|
|
250
|
+
|
|
251
|
+
When multiple services consume the same event, use `createContract` to create shared event contracts:
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
// packages/contracts/src/schemas/order-created.schema.ts
|
|
255
|
+
import { createContract } from '@crossdelta/cloudevents'
|
|
256
|
+
import { z } from 'zod'
|
|
257
|
+
|
|
258
|
+
export const OrderCreatedSchema = z.object({
|
|
259
|
+
orderId: z.string(),
|
|
260
|
+
total: z.number(),
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
export type OrderCreatedData = z.infer<typeof OrderCreatedSchema>
|
|
264
|
+
|
|
265
|
+
export const OrderCreatedContract = createContract({
|
|
266
|
+
type: 'orders.created',
|
|
267
|
+
schema: OrderCreatedSchema,
|
|
268
|
+
})
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Then import and use in handlers:
|
|
272
|
+
|
|
273
|
+
```typescript
|
|
274
|
+
import { handleEvent } from '@crossdelta/cloudevents'
|
|
275
|
+
import { OrderCreatedContract } from '@my-org/contracts'
|
|
276
|
+
|
|
277
|
+
export default handleEvent(
|
|
278
|
+
OrderCreatedContract,
|
|
279
|
+
async (data) => {
|
|
280
|
+
// data is typed as OrderCreatedData
|
|
281
|
+
console.log(data.orderId)
|
|
282
|
+
},
|
|
283
|
+
)
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Benefits:
|
|
287
|
+
- Single source of truth for event schemas
|
|
288
|
+
- No schema duplication
|
|
289
|
+
- Type safety across services
|
|
290
|
+
|
|
291
|
+
</details>
|
|
292
|
+
|
|
201
293
|
<details>
|
|
202
294
|
<summary><b>Multi-Tenancy</b></summary>
|
|
203
295
|
|
|
@@ -270,7 +362,7 @@ import { Hono } from 'hono'
|
|
|
270
362
|
import { cloudEvents } from '@crossdelta/cloudevents'
|
|
271
363
|
|
|
272
364
|
const app = new Hono()
|
|
273
|
-
app.use('/events', cloudEvents({ discover: 'src/
|
|
365
|
+
app.use('/events', cloudEvents({ discover: 'src/events/**/*.event.ts' }))
|
|
274
366
|
```
|
|
275
367
|
|
|
276
368
|
</details>
|
|
@@ -282,6 +374,8 @@ app.use('/events', cloudEvents({ discover: 'src/handlers/**/*.event.ts' }))
|
|
|
282
374
|
| Function | Purpose |
|
|
283
375
|
|----------|---------|
|
|
284
376
|
| `handleEvent(options, handler)` | Create a handler |
|
|
377
|
+
| `createContract(options)` | Create shared event contract |
|
|
378
|
+
| `ensureJetStreamStream(options)` | Create/update JetStream stream |
|
|
285
379
|
| `consumeJetStreamEvents(options)` | Consume with persistence |
|
|
286
380
|
| `consumeNatsEvents(options)` | Consume fire-and-forget |
|
|
287
381
|
| `publish(type, data)` | Publish event |
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ZodTypeAny } from 'zod';
|
|
2
|
+
import type { HandleEventOptions } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Creates a type-safe event contract for Advanced Mode
|
|
5
|
+
*
|
|
6
|
+
* This helper ensures proper type inference when using contracts
|
|
7
|
+
* with handleEvent across package boundaries.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* import { createContract } from '@crossdelta/cloudevents'
|
|
12
|
+
* import { z } from 'zod'
|
|
13
|
+
*
|
|
14
|
+
* export const OrderCreatedContract = createContract({
|
|
15
|
+
* type: 'orders.created',
|
|
16
|
+
* schema: z.object({
|
|
17
|
+
* orderId: z.string(),
|
|
18
|
+
* total: z.number(),
|
|
19
|
+
* }),
|
|
20
|
+
* })
|
|
21
|
+
*
|
|
22
|
+
* // Type is inferred: { orderId: string, total: number }
|
|
23
|
+
* export type OrderCreatedData = typeof OrderCreatedContract._schema._output
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export declare function createContract<TSchema extends ZodTypeAny>(options: {
|
|
27
|
+
type: string;
|
|
28
|
+
schema: TSchema;
|
|
29
|
+
match?: HandleEventOptions['match'];
|
|
30
|
+
safeParse?: boolean;
|
|
31
|
+
tenantId?: string | string[];
|
|
32
|
+
}): HandleEventOptions<TSchema> & {
|
|
33
|
+
_schema: TSchema;
|
|
34
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a type-safe event contract for Advanced Mode
|
|
3
|
+
*
|
|
4
|
+
* This helper ensures proper type inference when using contracts
|
|
5
|
+
* with handleEvent across package boundaries.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { createContract } from '@crossdelta/cloudevents'
|
|
10
|
+
* import { z } from 'zod'
|
|
11
|
+
*
|
|
12
|
+
* export const OrderCreatedContract = createContract({
|
|
13
|
+
* type: 'orders.created',
|
|
14
|
+
* schema: z.object({
|
|
15
|
+
* orderId: z.string(),
|
|
16
|
+
* total: z.number(),
|
|
17
|
+
* }),
|
|
18
|
+
* })
|
|
19
|
+
*
|
|
20
|
+
* // Type is inferred: { orderId: string, total: number }
|
|
21
|
+
* export type OrderCreatedData = typeof OrderCreatedContract._schema._output
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export function createContract(options) {
|
|
25
|
+
return {
|
|
26
|
+
...options,
|
|
27
|
+
_schema: options.schema,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
2
3
|
import { fileURLToPath } from 'node:url';
|
|
3
4
|
import { glob } from 'glob';
|
|
4
5
|
import { logger } from '../infrastructure';
|
|
@@ -13,17 +14,28 @@ const getSearchDirectories = () => {
|
|
|
13
14
|
try {
|
|
14
15
|
const currentDir = dirname(fileURLToPath(import.meta.url));
|
|
15
16
|
directories.add(currentDir);
|
|
17
|
+
// For monorepo tests: also check packages/cloudevents if we're in dist/
|
|
18
|
+
if (currentDir.includes('/dist/')) {
|
|
19
|
+
const pkgRoot = currentDir.split('/dist/')[0];
|
|
20
|
+
if (pkgRoot)
|
|
21
|
+
directories.add(pkgRoot);
|
|
22
|
+
}
|
|
16
23
|
}
|
|
17
24
|
catch {
|
|
18
25
|
// Ignore resolution errors; fallback to process.cwd()
|
|
19
26
|
}
|
|
20
|
-
|
|
27
|
+
// Filter out directories that don't exist (e.g., during hot-reload)
|
|
28
|
+
return [...directories].filter((dir) => existsSync(dir));
|
|
21
29
|
};
|
|
22
30
|
/**
|
|
23
31
|
* Loads and validates handlers from a TypeScript/JavaScript file.
|
|
24
32
|
*/
|
|
25
33
|
const loadHandlers = async (filePath, filter) => {
|
|
26
34
|
try {
|
|
35
|
+
// Check if file exists before import to avoid ENOENT during hot-reload
|
|
36
|
+
if (!existsSync(filePath)) {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
27
39
|
const module = await import(filePath);
|
|
28
40
|
return Object.entries(module)
|
|
29
41
|
.filter(([name, handler]) => isValidHandler(handler) && (!filter || filter(name, handler)))
|
|
@@ -78,19 +90,47 @@ const expandPatternVariants = (pattern, preferCompiled) => {
|
|
|
78
90
|
const ordered = preferCompiled ? [...compiledVariants, basePattern] : [basePattern, ...compiledVariants];
|
|
79
91
|
return [...new Set(ordered)];
|
|
80
92
|
};
|
|
93
|
+
/**
|
|
94
|
+
* Check if a directory path should be scanned (exists and is accessible)
|
|
95
|
+
*/
|
|
96
|
+
const shouldScanDirectory = (basePath, variant) => {
|
|
97
|
+
if (!existsSync(basePath)) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
// For patterns with directory prefixes like "dist/...", check if prefix dir exists
|
|
101
|
+
const dirPrefix = variant.match(/^([^*{[]+)\//)?.[1];
|
|
102
|
+
if (dirPrefix) {
|
|
103
|
+
const fullDirPath = join(basePath, dirPrefix);
|
|
104
|
+
return existsSync(fullDirPath);
|
|
105
|
+
}
|
|
106
|
+
return true;
|
|
107
|
+
};
|
|
81
108
|
const discoverFiles = async (pattern, basePath, preferCompiled) => {
|
|
82
|
-
|
|
109
|
+
// Don't add src/dist prefix for test patterns or absolute paths
|
|
110
|
+
const isTestPattern = pattern.startsWith('test/');
|
|
111
|
+
const prefixedPattern = isTestPattern ? pattern : `{src,dist,build,lib,out}/${pattern}`;
|
|
83
112
|
const patterns = preferCompiled ? [prefixedPattern, pattern] : [pattern, prefixedPattern];
|
|
84
113
|
const allFiles = [];
|
|
85
114
|
for (const globPattern of patterns) {
|
|
86
115
|
const variants = expandPatternVariants(globPattern, preferCompiled);
|
|
87
116
|
for (const variant of variants) {
|
|
88
117
|
try {
|
|
89
|
-
|
|
90
|
-
|
|
118
|
+
// Check if directory should be scanned to avoid scandir errors during hot-reload
|
|
119
|
+
if (!shouldScanDirectory(basePath, variant)) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const files = await glob(variant, {
|
|
123
|
+
cwd: basePath,
|
|
124
|
+
absolute: true,
|
|
125
|
+
nodir: true,
|
|
126
|
+
windowsPathsNoEscape: true,
|
|
127
|
+
});
|
|
128
|
+
// Filter out files that no longer exist (race condition during hot-reload)
|
|
129
|
+
const existingFiles = files.filter((f) => existsSync(f));
|
|
130
|
+
allFiles.push(...existingFiles);
|
|
91
131
|
}
|
|
92
132
|
catch {
|
|
93
|
-
// Ignore errors
|
|
133
|
+
// Ignore errors silently - directory might have been deleted during hot-reload
|
|
94
134
|
}
|
|
95
135
|
}
|
|
96
136
|
}
|
|
@@ -5,7 +5,9 @@ import type { HandleEventOptions, HandlerConstructor } from './types';
|
|
|
5
5
|
* Creates an event handler using the handleEvent pattern
|
|
6
6
|
* Compatible with the new middleware system
|
|
7
7
|
*
|
|
8
|
-
*
|
|
8
|
+
* Supports both inline schemas (Basic Mode) and shared contracts (Advanced Mode).
|
|
9
|
+
*
|
|
10
|
+
* @example Basic Mode - Data-only schema inline
|
|
9
11
|
* ```typescript
|
|
10
12
|
* export default handleEvent({
|
|
11
13
|
* type: 'orderboss.orders.created',
|
|
@@ -15,6 +17,15 @@ import type { HandleEventOptions, HandlerConstructor } from './types';
|
|
|
15
17
|
* })
|
|
16
18
|
* ```
|
|
17
19
|
*
|
|
20
|
+
* @example Advanced Mode - With shared contract
|
|
21
|
+
* ```typescript
|
|
22
|
+
* import { OrderCreatedContract } from '@orderboss/contracts'
|
|
23
|
+
*
|
|
24
|
+
* export default handleEvent(OrderCreatedContract, async (data) => {
|
|
25
|
+
* console.log('Order created:', data.orderId)
|
|
26
|
+
* })
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
18
29
|
* @example With tenant filtering
|
|
19
30
|
* ```typescript
|
|
20
31
|
* export default handleEvent({
|
|
@@ -26,7 +37,7 @@ import type { HandleEventOptions, HandlerConstructor } from './types';
|
|
|
26
37
|
* })
|
|
27
38
|
* ```
|
|
28
39
|
*/
|
|
29
|
-
export declare function handleEvent<TSchema extends ZodTypeAny>(schemaOrOptions: TSchema | HandleEventOptions<TSchema
|
|
40
|
+
export declare function handleEvent<TSchema extends ZodTypeAny>(schemaOrOptions: TSchema | HandleEventOptions<TSchema> | HandleEventOptions, handler: (payload: TSchema['_output'], context?: EventContext) => Promise<void> | void, eventType?: string): HandlerConstructor;
|
|
30
41
|
/**
|
|
31
42
|
* Creates an event schema with type inference
|
|
32
43
|
* Automatically enforces the presence of a 'type' field
|
|
@@ -67,7 +67,9 @@ function combineMatchers(matchers) {
|
|
|
67
67
|
* Creates an event handler using the handleEvent pattern
|
|
68
68
|
* Compatible with the new middleware system
|
|
69
69
|
*
|
|
70
|
-
*
|
|
70
|
+
* Supports both inline schemas (Basic Mode) and shared contracts (Advanced Mode).
|
|
71
|
+
*
|
|
72
|
+
* @example Basic Mode - Data-only schema inline
|
|
71
73
|
* ```typescript
|
|
72
74
|
* export default handleEvent({
|
|
73
75
|
* type: 'orderboss.orders.created',
|
|
@@ -77,6 +79,15 @@ function combineMatchers(matchers) {
|
|
|
77
79
|
* })
|
|
78
80
|
* ```
|
|
79
81
|
*
|
|
82
|
+
* @example Advanced Mode - With shared contract
|
|
83
|
+
* ```typescript
|
|
84
|
+
* import { OrderCreatedContract } from '@orderboss/contracts'
|
|
85
|
+
*
|
|
86
|
+
* export default handleEvent(OrderCreatedContract, async (data) => {
|
|
87
|
+
* console.log('Order created:', data.orderId)
|
|
88
|
+
* })
|
|
89
|
+
* ```
|
|
90
|
+
*
|
|
80
91
|
* @example With tenant filtering
|
|
81
92
|
* ```typescript
|
|
82
93
|
* export default handleEvent({
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { discoverHandlers } from './discovery';
|
|
2
|
+
export { createContract } from './contract-helper';
|
|
2
3
|
export { eventSchema, handleEvent } from './handler-factory';
|
|
3
4
|
export type { EnrichedEvent, EventHandler, HandleEventOptions, HandlerConstructor, HandlerMetadata, IdempotencyStore, InferEventData, MatchFn, RoutingConfig, } from './types';
|
|
4
5
|
export type { HandlerValidationError, ValidationErrorDetail } from './validation';
|
|
@@ -45,8 +45,11 @@ export type HandlerMetadata<S extends ZodTypeAny> = {
|
|
|
45
45
|
};
|
|
46
46
|
/**
|
|
47
47
|
* Options for creating event handlers
|
|
48
|
+
*
|
|
49
|
+
* Note: In Zod v4, we use a more relaxed schema constraint to allow
|
|
50
|
+
* contracts defined in external packages to work correctly.
|
|
48
51
|
*/
|
|
49
|
-
export interface HandleEventOptions<S
|
|
52
|
+
export interface HandleEventOptions<S = ZodTypeAny> {
|
|
50
53
|
schema: S;
|
|
51
54
|
type?: string;
|
|
52
55
|
match?: MatchFn<unknown>;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
export type { EventContext } from './adapters/cloudevents';
|
|
2
2
|
export { parseEventFromContext } from './adapters/cloudevents';
|
|
3
|
-
export
|
|
3
|
+
export { createContract } from './domain';
|
|
4
|
+
export type { EnrichedEvent, HandleEventOptions, IdempotencyStore, InferEventData, RoutingConfig } from './domain';
|
|
4
5
|
export { eventSchema, extractTypeFromSchema, handleEvent } from './domain';
|
|
5
6
|
export { clearHandlerCache, cloudEvents } from './middlewares';
|
|
6
7
|
export { checkAndMarkProcessed, createInMemoryIdempotencyStore, getDefaultIdempotencyStore, resetDefaultIdempotencyStore, } from './processing/idempotency';
|
|
7
8
|
export * from './publishing';
|
|
9
|
+
export { consumeJetStreamEvents, consumeNatsEvents, ensureJetStreamStream } from './transports/nats';
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { parseEventFromContext } from './adapters/cloudevents';
|
|
2
|
+
export { createContract } from './domain';
|
|
2
3
|
export { eventSchema, extractTypeFromSchema, handleEvent } from './domain';
|
|
3
4
|
export { clearHandlerCache, cloudEvents } from './middlewares';
|
|
4
5
|
export { checkAndMarkProcessed, createInMemoryIdempotencyStore, getDefaultIdempotencyStore, resetDefaultIdempotencyStore, } from './processing/idempotency';
|
|
5
6
|
export * from './publishing';
|
|
7
|
+
export { consumeJetStreamEvents, consumeNatsEvents, ensureJetStreamStream } from './transports/nats';
|
|
@@ -13,21 +13,43 @@ export interface StreamConfig {
|
|
|
13
13
|
replicas?: number;
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
|
-
* JetStream
|
|
16
|
+
* Options for creating/ensuring a JetStream stream exists
|
|
17
17
|
*/
|
|
18
|
-
export interface
|
|
18
|
+
export interface JetStreamStreamOptions {
|
|
19
19
|
/** NATS server URL. Defaults to NATS_URL env or nats://localhost:4222 */
|
|
20
20
|
servers?: string;
|
|
21
21
|
/** NATS username for authentication (defaults to NATS_USER env var) */
|
|
22
22
|
user?: string;
|
|
23
23
|
/** NATS password for authentication (defaults to NATS_PASSWORD env var) */
|
|
24
24
|
pass?: string;
|
|
25
|
-
/** JetStream stream name
|
|
25
|
+
/** JetStream stream name */
|
|
26
26
|
stream: string;
|
|
27
27
|
/** Subjects to bind to the stream (e.g., ['orders.>', 'payments.>']) */
|
|
28
28
|
subjects: string[];
|
|
29
|
+
/** Stream configuration */
|
|
30
|
+
config?: StreamConfig;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* JetStream consumer configuration
|
|
34
|
+
*/
|
|
35
|
+
export interface JetStreamConsumerOptions extends Pick<CloudEventsOptions, 'quarantineTopic' | 'errorTopic' | 'projectId' | 'source'> {
|
|
36
|
+
/** NATS server URL. Defaults to NATS_URL env or nats://localhost:4222 */
|
|
37
|
+
servers?: string;
|
|
38
|
+
/** NATS username for authentication (defaults to NATS_USER env var) */
|
|
39
|
+
user?: string;
|
|
40
|
+
/** NATS password for authentication (defaults to NATS_PASSWORD env var) */
|
|
41
|
+
pass?: string;
|
|
42
|
+
/** JetStream stream name (must already exist or use ensureJetStreamStream first) */
|
|
43
|
+
stream: string;
|
|
29
44
|
/** Durable consumer name. Required for persistence across restarts */
|
|
30
45
|
consumer: string;
|
|
46
|
+
/**
|
|
47
|
+
* Optional filter subjects for this consumer.
|
|
48
|
+
* If specified, consumer only receives messages matching these subjects.
|
|
49
|
+
* If omitted, consumer receives all messages from the stream.
|
|
50
|
+
* @example ['orders.created', 'orders.updated']
|
|
51
|
+
*/
|
|
52
|
+
filterSubjects?: string[];
|
|
31
53
|
/** Glob pattern to discover event handlers */
|
|
32
54
|
discover: string;
|
|
33
55
|
/**
|
|
@@ -51,8 +73,6 @@ export interface JetStreamConsumerOptions extends Pick<CloudEventsOptions, 'quar
|
|
|
51
73
|
* @default 3
|
|
52
74
|
*/
|
|
53
75
|
maxDeliver?: number;
|
|
54
|
-
/** Stream configuration (only used when auto-creating) */
|
|
55
|
-
streamConfig?: StreamConfig;
|
|
56
76
|
/**
|
|
57
77
|
* Idempotency store for deduplication. Defaults to in-memory store.
|
|
58
78
|
* Pass `false` to disable idempotency checks entirely.
|
|
@@ -64,6 +84,24 @@ export interface JetStreamConsumerOptions extends Pick<CloudEventsOptions, 'quar
|
|
|
64
84
|
*/
|
|
65
85
|
idempotencyTtl?: number;
|
|
66
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* Ensures a JetStream stream exists with the given configuration.
|
|
89
|
+
* This is typically called once during application startup or in infrastructure setup.
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```typescript
|
|
93
|
+
* // In infrastructure/setup code:
|
|
94
|
+
* await ensureJetStreamStream({
|
|
95
|
+
* stream: 'ORDERS',
|
|
96
|
+
* subjects: ['orders.>'],
|
|
97
|
+
* config: {
|
|
98
|
+
* maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
|
|
99
|
+
* replicas: 3
|
|
100
|
+
* }
|
|
101
|
+
* })
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
export declare function ensureJetStreamStream(options: JetStreamStreamOptions): Promise<void>;
|
|
67
105
|
/**
|
|
68
106
|
* Consume CloudEvents from NATS JetStream with persistence and guaranteed delivery.
|
|
69
107
|
*
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { connect, StringCodec, } from 'nats';
|
|
2
2
|
import { discoverHandlers } from '../../domain';
|
|
3
|
+
// String literals matching NATS enum values (enums not reliably exported in CI/Bun)
|
|
4
|
+
// AckPolicy.Explicit = "explicit", DeliverPolicy.All/Last/New/StartTime, RetentionPolicy.Limits = "limits", StorageType.File = "file"
|
|
5
|
+
const ACK_EXPLICIT = 'explicit';
|
|
6
|
+
const DELIVER_ALL = 'all';
|
|
7
|
+
const DELIVER_LAST = 'last';
|
|
8
|
+
const DELIVER_NEW = 'new';
|
|
9
|
+
const DELIVER_START_TIME = 'by_start_time';
|
|
10
|
+
const RETENTION_LIMITS = 'limits';
|
|
11
|
+
const STORAGE_FILE = 'file';
|
|
3
12
|
import { logger } from '../../infrastructure/logging';
|
|
4
13
|
import { processHandler } from '../../processing/handler-cache';
|
|
5
14
|
import { checkAndMarkProcessed, getDefaultIdempotencyStore } from '../../processing/idempotency';
|
|
6
15
|
import { createJetStreamMessageProcessor } from './jetstream-message-processor';
|
|
7
16
|
const sc = StringCodec();
|
|
8
|
-
// Use globalThis to persist across hot-reloads
|
|
9
|
-
const JETSTREAM_REGISTRY_KEY = '__crossdelta_jetstream_consumers__';
|
|
10
|
-
function getJetStreamRegistry() {
|
|
11
|
-
if (!globalThis[JETSTREAM_REGISTRY_KEY]) {
|
|
12
|
-
;
|
|
13
|
-
globalThis[JETSTREAM_REGISTRY_KEY] = new Map();
|
|
14
|
-
}
|
|
15
|
-
return globalThis[JETSTREAM_REGISTRY_KEY];
|
|
16
|
-
}
|
|
17
17
|
// Default stream configuration
|
|
18
18
|
const DEFAULT_STREAM_CONFIG = {
|
|
19
19
|
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days in ms
|
|
@@ -21,7 +21,40 @@ const DEFAULT_STREAM_CONFIG = {
|
|
|
21
21
|
replicas: 1,
|
|
22
22
|
};
|
|
23
23
|
/**
|
|
24
|
-
* Ensures stream exists with the given configuration
|
|
24
|
+
* Ensures a JetStream stream exists with the given configuration.
|
|
25
|
+
* This is typically called once during application startup or in infrastructure setup.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```typescript
|
|
29
|
+
* // In infrastructure/setup code:
|
|
30
|
+
* await ensureJetStreamStream({
|
|
31
|
+
* stream: 'ORDERS',
|
|
32
|
+
* subjects: ['orders.>'],
|
|
33
|
+
* config: {
|
|
34
|
+
* maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
|
|
35
|
+
* replicas: 3
|
|
36
|
+
* }
|
|
37
|
+
* })
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export async function ensureJetStreamStream(options) {
|
|
41
|
+
const servers = options.servers ?? process.env.NATS_URL ?? 'nats://localhost:4222';
|
|
42
|
+
const user = options.user ?? process.env.NATS_USER;
|
|
43
|
+
const pass = options.pass ?? process.env.NATS_PASSWORD;
|
|
44
|
+
const nc = await connect({
|
|
45
|
+
servers,
|
|
46
|
+
...(user && pass ? { user, pass } : {}),
|
|
47
|
+
});
|
|
48
|
+
try {
|
|
49
|
+
const jsm = await nc.jetstreamManager();
|
|
50
|
+
await ensureStream(jsm, options.stream, options.subjects, options.config);
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
await nc.close();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Ensures stream exists with the given configuration (internal helper)
|
|
25
58
|
*/
|
|
26
59
|
async function ensureStream(jsm, name, subjects, config = {}) {
|
|
27
60
|
const streamConfig = { ...DEFAULT_STREAM_CONFIG, ...config };
|
|
@@ -42,8 +75,8 @@ async function ensureStream(jsm, name, subjects, config = {}) {
|
|
|
42
75
|
await jsm.streams.add({
|
|
43
76
|
name,
|
|
44
77
|
subjects,
|
|
45
|
-
retention:
|
|
46
|
-
storage:
|
|
78
|
+
retention: RETENTION_LIMITS,
|
|
79
|
+
storage: STORAGE_FILE,
|
|
47
80
|
max_age: streamConfig.maxAge * 1_000_000, // Convert ms to nanoseconds
|
|
48
81
|
max_bytes: streamConfig.maxBytes,
|
|
49
82
|
num_replicas: streamConfig.replicas,
|
|
@@ -58,11 +91,11 @@ async function ensureConsumer(jsm, streamName, consumerName, options) {
|
|
|
58
91
|
const deliverPolicy = (() => {
|
|
59
92
|
switch (options.startFrom) {
|
|
60
93
|
case 'all':
|
|
61
|
-
return
|
|
94
|
+
return DELIVER_ALL;
|
|
62
95
|
case 'last':
|
|
63
|
-
return
|
|
96
|
+
return DELIVER_LAST;
|
|
64
97
|
default:
|
|
65
|
-
return
|
|
98
|
+
return DELIVER_NEW;
|
|
66
99
|
}
|
|
67
100
|
})();
|
|
68
101
|
const optStartTime = options.startFrom instanceof Date ? options.startFrom : undefined;
|
|
@@ -74,30 +107,18 @@ async function ensureConsumer(jsm, streamName, consumerName, options) {
|
|
|
74
107
|
// Consumer doesn't exist, create it
|
|
75
108
|
await jsm.consumers.add(streamName, {
|
|
76
109
|
durable_name: consumerName,
|
|
77
|
-
ack_policy:
|
|
78
|
-
deliver_policy: optStartTime ?
|
|
110
|
+
ack_policy: ACK_EXPLICIT,
|
|
111
|
+
deliver_policy: optStartTime ? DELIVER_START_TIME : deliverPolicy,
|
|
79
112
|
opt_start_time: optStartTime?.toISOString(),
|
|
80
|
-
replay_policy
|
|
113
|
+
// replay_policy defaults to 'instant', no need to specify explicitly
|
|
81
114
|
ack_wait: (options.ackWait ?? 30_000) * 1_000_000, // Convert to nanoseconds
|
|
82
115
|
max_deliver: options.maxDeliver ?? 3,
|
|
116
|
+
// Filter subjects at consumer level (optional)
|
|
117
|
+
filter_subjects: options.filterSubjects,
|
|
83
118
|
});
|
|
84
119
|
logger.info(`[jetstream] created durable consumer ${consumerName} on stream ${streamName}`);
|
|
85
120
|
}
|
|
86
121
|
}
|
|
87
|
-
/**
|
|
88
|
-
* Cleanup function to close a JetStream consumer
|
|
89
|
-
*/
|
|
90
|
-
async function cleanupJetStreamConsumer(name) {
|
|
91
|
-
const registry = getJetStreamRegistry();
|
|
92
|
-
const consumer = registry.get(name);
|
|
93
|
-
if (consumer) {
|
|
94
|
-
logger.info(`[${name}] cleaning up JetStream consumer...`);
|
|
95
|
-
consumer.abortController.abort();
|
|
96
|
-
await consumer.messages.close();
|
|
97
|
-
await consumer.connection.drain();
|
|
98
|
-
registry.delete(name);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
122
|
/**
|
|
102
123
|
* Consume CloudEvents from NATS JetStream with persistence and guaranteed delivery.
|
|
103
124
|
*
|
|
@@ -119,13 +140,11 @@ async function cleanupJetStreamConsumer(name) {
|
|
|
119
140
|
* ```
|
|
120
141
|
*/
|
|
121
142
|
export async function consumeJetStreamEvents(options) {
|
|
122
|
-
const servers = options.servers ?? process.env.NATS_URL ?? 'nats://localhost:4222';
|
|
123
143
|
const name = options.consumer;
|
|
144
|
+
const servers = options.servers ?? process.env.NATS_URL ?? 'nats://localhost:4222';
|
|
124
145
|
// Authentication (from options or env vars)
|
|
125
146
|
const user = options.user ?? process.env.NATS_USER;
|
|
126
147
|
const pass = options.pass ?? process.env.NATS_PASSWORD;
|
|
127
|
-
// Cleanup existing consumer (handles hot-reload)
|
|
128
|
-
await cleanupJetStreamConsumer(name);
|
|
129
148
|
// 1) Discover handlers
|
|
130
149
|
const handlerConstructors = await discoverHandlers(options.discover);
|
|
131
150
|
const processedHandlers = handlerConstructors
|
|
@@ -142,19 +161,14 @@ export async function consumeJetStreamEvents(options) {
|
|
|
142
161
|
// 3) Setup JetStream
|
|
143
162
|
const jsm = await nc.jetstreamManager();
|
|
144
163
|
const js = nc.jetstream();
|
|
145
|
-
// 4) Ensure stream
|
|
146
|
-
await ensureStream(jsm, options.stream, options.subjects, options.streamConfig);
|
|
147
|
-
// 5) Ensure durable consumer exists
|
|
164
|
+
// 4) Ensure durable consumer exists (stream must already exist)
|
|
148
165
|
await ensureConsumer(jsm, options.stream, name, options);
|
|
149
|
-
//
|
|
166
|
+
// 5) Get consumer and start consuming
|
|
150
167
|
const consumer = await js.consumers.get(options.stream, name);
|
|
151
168
|
const messages = await consumer.consume({
|
|
152
169
|
max_messages: options.maxMessages ?? 100,
|
|
153
170
|
});
|
|
154
171
|
logger.info(`[${name}] consuming from stream ${options.stream}`);
|
|
155
|
-
// Track for cleanup
|
|
156
|
-
const abortController = new AbortController();
|
|
157
|
-
getJetStreamRegistry().set(name, { messages, connection: nc, abortController });
|
|
158
172
|
const dlqEnabled = Boolean(options.quarantineTopic || options.errorTopic);
|
|
159
173
|
// Setup idempotency store
|
|
160
174
|
const idempotencyStore = options.idempotencyStore === false ? null : (options.idempotencyStore ?? getDefaultIdempotencyStore());
|
|
@@ -197,18 +211,15 @@ export async function consumeJetStreamEvents(options) {
|
|
|
197
211
|
await handleUnhandledProcessingError(msg, error);
|
|
198
212
|
}
|
|
199
213
|
};
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
await processSingleMessage(msg);
|
|
214
|
+
(async () => {
|
|
215
|
+
try {
|
|
216
|
+
for await (const msg of messages) {
|
|
217
|
+
await processSingleMessage(msg);
|
|
218
|
+
}
|
|
206
219
|
}
|
|
207
|
-
|
|
208
|
-
processMessages().catch((err) => {
|
|
209
|
-
if (!abortController.signal.aborted) {
|
|
220
|
+
catch (err) {
|
|
210
221
|
logger.error(`[${name}] message processing loop crashed`, err);
|
|
211
222
|
}
|
|
212
|
-
});
|
|
223
|
+
})();
|
|
213
224
|
return messages;
|
|
214
225
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crossdelta/cloudevents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "CloudEvents toolkit for TypeScript - Zod validation, handler discovery, NATS JetStream",
|
|
5
5
|
"author": "crossdelta",
|
|
6
6
|
"license": "MIT",
|
|
@@ -14,15 +14,15 @@
|
|
|
14
14
|
"hono"
|
|
15
15
|
],
|
|
16
16
|
"type": "module",
|
|
17
|
-
"main": "dist/
|
|
18
|
-
"types": "dist/
|
|
17
|
+
"main": "dist/index.js",
|
|
18
|
+
"types": "dist/index.d.ts",
|
|
19
19
|
"files": [
|
|
20
|
-
"dist
|
|
20
|
+
"dist"
|
|
21
21
|
],
|
|
22
22
|
"exports": {
|
|
23
23
|
".": {
|
|
24
|
-
"import": "./dist/
|
|
25
|
-
"types": "./dist/
|
|
24
|
+
"import": "./dist/index.js",
|
|
25
|
+
"types": "./dist/index.d.ts"
|
|
26
26
|
},
|
|
27
27
|
"./transports/nats": {
|
|
28
28
|
"import": "./dist/src/transports/nats/index.js",
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"start:dev": "tsc -p tsconfig.json --watch",
|
|
38
38
|
"stryker": "bun stryker run",
|
|
39
39
|
"lint": "biome lint --fix",
|
|
40
|
+
"pretest": "rm -rf dist",
|
|
40
41
|
"test": "bun test",
|
|
41
42
|
"prepublishOnly": "bun run build"
|
|
42
43
|
},
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|