ai-props 2.1.1 → 2.3.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/.dev.vars +2 -0
- package/CHANGELOG.md +24 -0
- package/README.md +131 -118
- package/package.json +30 -4
- package/src/ai.ts +12 -31
- package/src/cascade.ts +795 -0
- package/src/client.ts +440 -0
- package/src/durable-cascade.ts +743 -0
- package/src/event-bridge.ts +478 -0
- package/src/generate.ts +14 -12
- package/src/hoc.ts +15 -19
- package/src/hono-jsx.ts +675 -0
- package/src/index.ts +30 -0
- package/src/mdx-types.ts +169 -0
- package/src/mdx-utils.ts +437 -0
- package/src/mdx.ts +1008 -0
- package/src/rpc.ts +614 -0
- package/src/streaming.ts +618 -0
- package/src/validate.ts +15 -29
- package/src/worker.ts +547 -0
- package/test/cascade.test.ts +338 -0
- package/test/durable-cascade.test.ts +319 -0
- package/test/event-bridge.test.ts +351 -0
- package/test/generate.test.ts +6 -16
- package/test/mdx.test.ts +817 -0
- package/test/worker/capnweb-rpc.test.ts +1084 -0
- package/test/worker/full-flow.integration.test.ts +1463 -0
- package/test/worker/hono-jsx.test.ts +1258 -0
- package/test/worker/mdx-parsing.test.ts +1148 -0
- package/test/worker/setup.ts +56 -0
- package/test/worker.test.ts +595 -0
- package/tsconfig.json +2 -1
- package/vitest.config.js +6 -0
- package/vitest.config.ts +15 -1
- package/vitest.workers.config.ts +58 -0
- package/wrangler.jsonc +27 -0
- package/.turbo/turbo-build.log +0 -5
- package/dist/ai.d.ts +0 -125
- package/dist/ai.d.ts.map +0 -1
- package/dist/ai.js +0 -199
- package/dist/ai.js.map +0 -1
- package/dist/cache.d.ts +0 -66
- package/dist/cache.d.ts.map +0 -1
- package/dist/cache.js +0 -183
- package/dist/cache.js.map +0 -1
- package/dist/generate.d.ts +0 -69
- package/dist/generate.d.ts.map +0 -1
- package/dist/generate.js +0 -221
- package/dist/generate.js.map +0 -1
- package/dist/hoc.d.ts +0 -164
- package/dist/hoc.d.ts.map +0 -1
- package/dist/hoc.js +0 -236
- package/dist/hoc.js.map +0 -1
- package/dist/index.d.ts +0 -15
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -21
- package/dist/index.js.map +0 -1
- package/dist/types.d.ts +0 -152
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -7
- package/dist/types.js.map +0 -1
- package/dist/validate.d.ts +0 -58
- package/dist/validate.d.ts.map +0 -1
- package/dist/validate.js +0 -253
- package/dist/validate.js.map +0 -1
- package/src/ai.js +0 -198
- package/src/cache.js +0 -182
- package/src/generate.js +0 -220
- package/src/hoc.js +0 -235
- package/src/index.js +0 -20
- package/src/types.js +0 -6
- package/src/validate.js +0 -252
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event Bridge Tests
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
5
|
+
import {
|
|
6
|
+
EventBridge,
|
|
7
|
+
createQueueHandler,
|
|
8
|
+
createEventBridge,
|
|
9
|
+
type QueuedEvent,
|
|
10
|
+
type Queue,
|
|
11
|
+
type MessageBatch,
|
|
12
|
+
type QueueMessage,
|
|
13
|
+
} from '../src/event-bridge.js'
|
|
14
|
+
|
|
15
|
+
// Mock queue implementation for testing
|
|
16
|
+
function createMockQueue<T = unknown>(): Queue<T> & { messages: T[] } {
|
|
17
|
+
const messages: T[] = []
|
|
18
|
+
return {
|
|
19
|
+
messages,
|
|
20
|
+
async send(message: T): Promise<void> {
|
|
21
|
+
messages.push(message)
|
|
22
|
+
},
|
|
23
|
+
async sendBatch(batch: Iterable<{ body: T }>): Promise<void> {
|
|
24
|
+
for (const { body } of batch) {
|
|
25
|
+
messages.push(body)
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Mock message for testing
|
|
32
|
+
function createMockMessage<T>(body: T): QueueMessage<T> {
|
|
33
|
+
return {
|
|
34
|
+
id: 'msg-1',
|
|
35
|
+
timestamp: new Date(),
|
|
36
|
+
body,
|
|
37
|
+
ack: vi.fn(),
|
|
38
|
+
retry: vi.fn(),
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Mock message batch for testing
|
|
43
|
+
function createMockBatch<T>(messages: T[]): MessageBatch<T> {
|
|
44
|
+
return {
|
|
45
|
+
queue: 'test-queue',
|
|
46
|
+
messages: messages.map((body) => createMockMessage(body)),
|
|
47
|
+
ackAll: vi.fn(),
|
|
48
|
+
retryAll: vi.fn(),
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe('EventBridge', () => {
|
|
53
|
+
describe('basic event handling (no queue)', () => {
|
|
54
|
+
it('should emit and handle events synchronously without a queue', async () => {
|
|
55
|
+
const bridge = new EventBridge()
|
|
56
|
+
const handler = vi.fn()
|
|
57
|
+
|
|
58
|
+
bridge.on('test.event', handler)
|
|
59
|
+
await bridge.emit('test.event', { foo: 'bar' })
|
|
60
|
+
|
|
61
|
+
expect(handler).toHaveBeenCalledTimes(1)
|
|
62
|
+
expect(handler).toHaveBeenCalledWith(
|
|
63
|
+
{ foo: 'bar' },
|
|
64
|
+
expect.objectContaining({
|
|
65
|
+
type: 'test.event',
|
|
66
|
+
data: { foo: 'bar' },
|
|
67
|
+
})
|
|
68
|
+
)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('should support multiple handlers for the same event', async () => {
|
|
72
|
+
const bridge = new EventBridge()
|
|
73
|
+
const handler1 = vi.fn()
|
|
74
|
+
const handler2 = vi.fn()
|
|
75
|
+
|
|
76
|
+
bridge.on('test.event', handler1)
|
|
77
|
+
bridge.on('test.event', handler2)
|
|
78
|
+
await bridge.emit('test.event', { value: 123 })
|
|
79
|
+
|
|
80
|
+
expect(handler1).toHaveBeenCalledTimes(1)
|
|
81
|
+
expect(handler2).toHaveBeenCalledTimes(1)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('should support wildcard handlers', async () => {
|
|
85
|
+
const bridge = new EventBridge()
|
|
86
|
+
const wildcardHandler = vi.fn()
|
|
87
|
+
const specificHandler = vi.fn()
|
|
88
|
+
|
|
89
|
+
bridge.on('*', wildcardHandler)
|
|
90
|
+
bridge.on('specific.event', specificHandler)
|
|
91
|
+
|
|
92
|
+
await bridge.emit('specific.event', { data: 1 })
|
|
93
|
+
await bridge.emit('other.event', { data: 2 })
|
|
94
|
+
|
|
95
|
+
expect(wildcardHandler).toHaveBeenCalledTimes(2)
|
|
96
|
+
expect(specificHandler).toHaveBeenCalledTimes(1)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('should remove handlers with off()', async () => {
|
|
100
|
+
const bridge = new EventBridge()
|
|
101
|
+
const handler = vi.fn()
|
|
102
|
+
|
|
103
|
+
bridge.on('test.event', handler)
|
|
104
|
+
bridge.off('test.event', handler)
|
|
105
|
+
await bridge.emit('test.event', { value: 1 })
|
|
106
|
+
|
|
107
|
+
expect(handler).not.toHaveBeenCalled()
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('should support once() for one-time handlers', async () => {
|
|
111
|
+
const bridge = new EventBridge()
|
|
112
|
+
const handler = vi.fn()
|
|
113
|
+
|
|
114
|
+
bridge.once('test.event', handler)
|
|
115
|
+
await bridge.emit('test.event', { value: 1 })
|
|
116
|
+
await bridge.emit('test.event', { value: 2 })
|
|
117
|
+
|
|
118
|
+
expect(handler).toHaveBeenCalledTimes(1)
|
|
119
|
+
expect(handler).toHaveBeenCalledWith({ value: 1 }, expect.anything())
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
describe('queue-based event handling', () => {
|
|
124
|
+
it('should send events to queue when configured', async () => {
|
|
125
|
+
const queue = createMockQueue<QueuedEvent>()
|
|
126
|
+
const bridge = new EventBridge(queue)
|
|
127
|
+
|
|
128
|
+
await bridge.emit('test.event', { foo: 'bar' })
|
|
129
|
+
|
|
130
|
+
expect(queue.messages).toHaveLength(1)
|
|
131
|
+
expect(queue.messages[0]).toMatchObject({
|
|
132
|
+
type: 'test.event',
|
|
133
|
+
data: { foo: 'bar' },
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('should send batch events to queue', async () => {
|
|
138
|
+
const queue = createMockQueue<QueuedEvent>()
|
|
139
|
+
const bridge = new EventBridge(queue)
|
|
140
|
+
|
|
141
|
+
await bridge.emitBatch([
|
|
142
|
+
{ type: 'event.1', data: { a: 1 } },
|
|
143
|
+
{ type: 'event.2', data: { b: 2 } },
|
|
144
|
+
])
|
|
145
|
+
|
|
146
|
+
expect(queue.messages).toHaveLength(2)
|
|
147
|
+
expect(queue.messages[0].type).toBe('event.1')
|
|
148
|
+
expect(queue.messages[1].type).toBe('event.2')
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('should include delay when specified', async () => {
|
|
152
|
+
const queue = createMockQueue<QueuedEvent>()
|
|
153
|
+
const bridge = new EventBridge(queue)
|
|
154
|
+
|
|
155
|
+
await bridge.emit('test.event', { value: 1 }, { delaySeconds: 30 })
|
|
156
|
+
|
|
157
|
+
// The message should be queued (queue.send was called)
|
|
158
|
+
expect(queue.messages).toHaveLength(1)
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
describe('message handling', () => {
|
|
163
|
+
it('should process messages and ack on success', async () => {
|
|
164
|
+
const bridge = new EventBridge()
|
|
165
|
+
const handler = vi.fn()
|
|
166
|
+
bridge.on('test.event', handler)
|
|
167
|
+
|
|
168
|
+
const event: QueuedEvent = {
|
|
169
|
+
type: 'test.event',
|
|
170
|
+
data: { value: 1 },
|
|
171
|
+
timestamp: Date.now(),
|
|
172
|
+
id: 'event-1',
|
|
173
|
+
attempts: 0,
|
|
174
|
+
}
|
|
175
|
+
const message = createMockMessage(event)
|
|
176
|
+
|
|
177
|
+
await bridge.handleMessage(message)
|
|
178
|
+
|
|
179
|
+
expect(handler).toHaveBeenCalledTimes(1)
|
|
180
|
+
expect(message.ack).toHaveBeenCalled()
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it('should retry on handler failure', async () => {
|
|
184
|
+
const bridge = new EventBridge()
|
|
185
|
+
bridge.on('test.event', () => {
|
|
186
|
+
throw new Error('Handler failed')
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
const event: QueuedEvent = {
|
|
190
|
+
type: 'test.event',
|
|
191
|
+
data: { value: 1 },
|
|
192
|
+
timestamp: Date.now(),
|
|
193
|
+
id: 'event-1',
|
|
194
|
+
attempts: 0,
|
|
195
|
+
}
|
|
196
|
+
const message = createMockMessage(event)
|
|
197
|
+
|
|
198
|
+
await bridge.handleMessage(message)
|
|
199
|
+
|
|
200
|
+
expect(message.retry).toHaveBeenCalled()
|
|
201
|
+
expect(message.ack).not.toHaveBeenCalled()
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('should send to DLQ after max retries', async () => {
|
|
205
|
+
const dlq = createMockQueue<QueuedEvent>()
|
|
206
|
+
const bridge = new EventBridge(undefined, {
|
|
207
|
+
maxRetries: 3,
|
|
208
|
+
deadLetterQueue: dlq,
|
|
209
|
+
})
|
|
210
|
+
bridge.on('test.event', () => {
|
|
211
|
+
throw new Error('Handler failed')
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
const event: QueuedEvent = {
|
|
215
|
+
type: 'test.event',
|
|
216
|
+
data: { value: 1 },
|
|
217
|
+
timestamp: Date.now(),
|
|
218
|
+
id: 'event-1',
|
|
219
|
+
attempts: 2, // Will be incremented to 3 during processing
|
|
220
|
+
}
|
|
221
|
+
const message = createMockMessage(event)
|
|
222
|
+
|
|
223
|
+
await bridge.handleMessage(message)
|
|
224
|
+
|
|
225
|
+
expect(dlq.messages).toHaveLength(1)
|
|
226
|
+
expect(dlq.messages[0]).toMatchObject({
|
|
227
|
+
type: 'test.event',
|
|
228
|
+
error: 'Handler failed',
|
|
229
|
+
})
|
|
230
|
+
expect(message.ack).toHaveBeenCalled() // Ack after DLQ
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
it('should handle message batch', async () => {
|
|
234
|
+
const bridge = new EventBridge()
|
|
235
|
+
const handler = vi.fn()
|
|
236
|
+
bridge.on('test.event', handler)
|
|
237
|
+
|
|
238
|
+
const events: QueuedEvent[] = [
|
|
239
|
+
{ type: 'test.event', data: { n: 1 }, timestamp: Date.now(), id: '1' },
|
|
240
|
+
{ type: 'test.event', data: { n: 2 }, timestamp: Date.now(), id: '2' },
|
|
241
|
+
]
|
|
242
|
+
const batch = createMockBatch(events)
|
|
243
|
+
|
|
244
|
+
await bridge.handleBatch(batch)
|
|
245
|
+
|
|
246
|
+
expect(handler).toHaveBeenCalledTimes(2)
|
|
247
|
+
})
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
describe('validation', () => {
|
|
251
|
+
it('should validate events when validator is configured', async () => {
|
|
252
|
+
const bridge = new EventBridge(undefined, {
|
|
253
|
+
validator: (event) => event.type.startsWith('valid.'),
|
|
254
|
+
})
|
|
255
|
+
const handler = vi.fn()
|
|
256
|
+
bridge.on('valid.event', handler)
|
|
257
|
+
bridge.on('invalid.event', handler)
|
|
258
|
+
|
|
259
|
+
await bridge.emit('valid.event', { data: 1 })
|
|
260
|
+
expect(handler).toHaveBeenCalledTimes(1)
|
|
261
|
+
|
|
262
|
+
await expect(bridge.emit('invalid.event', { data: 2 })).rejects.toThrow(
|
|
263
|
+
'Event validation failed'
|
|
264
|
+
)
|
|
265
|
+
})
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
describe('serialization', () => {
|
|
269
|
+
it('should use custom serializer/deserializer', async () => {
|
|
270
|
+
const bridge = new EventBridge(undefined, {
|
|
271
|
+
serializer: (data) => JSON.stringify(data),
|
|
272
|
+
deserializer: (data) => JSON.parse(data as string),
|
|
273
|
+
})
|
|
274
|
+
const handler = vi.fn()
|
|
275
|
+
bridge.on('test.event', handler)
|
|
276
|
+
|
|
277
|
+
await bridge.emit('test.event', { value: 42 })
|
|
278
|
+
|
|
279
|
+
// The handler receives the original data (not serialized)
|
|
280
|
+
// because sync mode doesn't go through deserialize
|
|
281
|
+
expect(handler).toHaveBeenCalledWith(expect.anything(), expect.anything())
|
|
282
|
+
})
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
describe('utility methods', () => {
|
|
286
|
+
it('should return registered event types', () => {
|
|
287
|
+
const bridge = new EventBridge()
|
|
288
|
+
bridge.on('event.a', () => {})
|
|
289
|
+
bridge.on('event.b', () => {})
|
|
290
|
+
bridge.on('event.c', () => {})
|
|
291
|
+
|
|
292
|
+
const types = bridge.getEventTypes()
|
|
293
|
+
expect(types).toContain('event.a')
|
|
294
|
+
expect(types).toContain('event.b')
|
|
295
|
+
expect(types).toContain('event.c')
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
it('should check if handlers exist', () => {
|
|
299
|
+
const bridge = new EventBridge()
|
|
300
|
+
bridge.on('has.handler', () => {})
|
|
301
|
+
|
|
302
|
+
expect(bridge.hasHandlers('has.handler')).toBe(true)
|
|
303
|
+
expect(bridge.hasHandlers('no.handler')).toBe(false)
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
it('should clear all handlers', () => {
|
|
307
|
+
const bridge = new EventBridge()
|
|
308
|
+
bridge.on('event.a', () => {})
|
|
309
|
+
bridge.on('event.b', () => {})
|
|
310
|
+
bridge.on('*', () => {})
|
|
311
|
+
|
|
312
|
+
bridge.clearHandlers()
|
|
313
|
+
|
|
314
|
+
expect(bridge.getEventTypes()).toHaveLength(0)
|
|
315
|
+
expect(bridge.hasHandlers('event.a')).toBe(false)
|
|
316
|
+
})
|
|
317
|
+
})
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
describe('createQueueHandler', () => {
|
|
321
|
+
it('should create a valid queue handler', async () => {
|
|
322
|
+
const bridge = new EventBridge()
|
|
323
|
+
const handler = vi.fn()
|
|
324
|
+
bridge.on('test.event', handler)
|
|
325
|
+
|
|
326
|
+
const queueHandler = createQueueHandler(bridge)
|
|
327
|
+
|
|
328
|
+
const events: QueuedEvent[] = [
|
|
329
|
+
{ type: 'test.event', data: { n: 1 }, timestamp: Date.now(), id: '1' },
|
|
330
|
+
]
|
|
331
|
+
const batch = createMockBatch(events)
|
|
332
|
+
|
|
333
|
+
await queueHandler.queue(batch)
|
|
334
|
+
|
|
335
|
+
expect(handler).toHaveBeenCalledTimes(1)
|
|
336
|
+
})
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
describe('createEventBridge', () => {
|
|
340
|
+
it('should create EventBridge with default config', () => {
|
|
341
|
+
const bridge = createEventBridge()
|
|
342
|
+
expect(bridge).toBeInstanceOf(EventBridge)
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
it('should create EventBridge with queue and DLQ', () => {
|
|
346
|
+
const queue = createMockQueue()
|
|
347
|
+
const dlq = createMockQueue()
|
|
348
|
+
const bridge = createEventBridge(queue, dlq)
|
|
349
|
+
expect(bridge).toBeInstanceOf(EventBridge)
|
|
350
|
+
})
|
|
351
|
+
})
|
package/test/generate.test.ts
CHANGED
|
@@ -83,7 +83,7 @@ describe('getConfig', () => {
|
|
|
83
83
|
it('returns default config', () => {
|
|
84
84
|
const config = getConfig()
|
|
85
85
|
|
|
86
|
-
expect(config.model).toBe('sonnet')
|
|
86
|
+
expect(config.model).toBe('anthropic/claude-sonnet-4.5')
|
|
87
87
|
expect(config.cache).toBe(true)
|
|
88
88
|
expect(config.cacheTTL).toBe(5 * 60 * 1000)
|
|
89
89
|
})
|
|
@@ -103,7 +103,7 @@ describe('resetConfig', () => {
|
|
|
103
103
|
resetConfig()
|
|
104
104
|
|
|
105
105
|
const config = getConfig()
|
|
106
|
-
expect(config.model).toBe('sonnet')
|
|
106
|
+
expect(config.model).toBe('anthropic/claude-sonnet-4.5')
|
|
107
107
|
expect(config.cache).toBe(true)
|
|
108
108
|
})
|
|
109
109
|
})
|
|
@@ -142,7 +142,7 @@ describe('generateProps', () => {
|
|
|
142
142
|
})
|
|
143
143
|
|
|
144
144
|
expect(result.metadata).toBeDefined()
|
|
145
|
-
expect(result.metadata.model).toBe('sonnet')
|
|
145
|
+
expect(result.metadata.model).toBe('anthropic/claude-sonnet-4.5')
|
|
146
146
|
})
|
|
147
147
|
|
|
148
148
|
it('uses custom model', async () => {
|
|
@@ -289,10 +289,7 @@ describe('prefetchProps', () => {
|
|
|
289
289
|
})
|
|
290
290
|
|
|
291
291
|
it('prefetches multiple schemas', async () => {
|
|
292
|
-
await prefetchProps([
|
|
293
|
-
{ schema: { name: 'User name' } },
|
|
294
|
-
{ schema: { title: 'Page title' } },
|
|
295
|
-
])
|
|
292
|
+
await prefetchProps([{ schema: { name: 'User name' } }, { schema: { title: 'Page title' } }])
|
|
296
293
|
|
|
297
294
|
// Both should be cached now
|
|
298
295
|
const name = getPropsSync({ name: 'User name' })
|
|
@@ -373,10 +370,7 @@ describe('mergeWithGenerated', () => {
|
|
|
373
370
|
})
|
|
374
371
|
|
|
375
372
|
it('generates all props when none provided', async () => {
|
|
376
|
-
const result = await mergeWithGenerated(
|
|
377
|
-
{ name: 'User name', email: 'Email address' },
|
|
378
|
-
{}
|
|
379
|
-
)
|
|
373
|
+
const result = await mergeWithGenerated({ name: 'User name', email: 'Email address' }, {})
|
|
380
374
|
|
|
381
375
|
expect(result.name).toBe('generated-name')
|
|
382
376
|
expect(result.email).toBe('generated-email')
|
|
@@ -395,11 +389,7 @@ describe('mergeWithGenerated', () => {
|
|
|
395
389
|
})
|
|
396
390
|
|
|
397
391
|
it('accepts additional options', async () => {
|
|
398
|
-
const result = await mergeWithGenerated(
|
|
399
|
-
{ name: 'User name' },
|
|
400
|
-
{},
|
|
401
|
-
{ model: 'gpt-4' }
|
|
402
|
-
)
|
|
392
|
+
const result = await mergeWithGenerated({ name: 'User name' }, {}, { model: 'gpt-4' })
|
|
403
393
|
|
|
404
394
|
expect(result.name).toBe('generated-name')
|
|
405
395
|
})
|