@getvision/server 0.4.3-d4c761e-develop → 0.4.4-44d79d9-develop
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/CHANGELOG.md +8 -0
- package/dist/event-bus.d.ts +87 -0
- package/dist/event-bus.d.ts.map +1 -0
- package/dist/event-bus.js +265 -0
- package/dist/event-bus.js.map +10 -0
- package/dist/event-registry.d.ts +79 -0
- package/dist/event-registry.d.ts.map +1 -0
- package/dist/event-registry.js +93 -0
- package/dist/event-registry.js.map +10 -0
- package/{src/index.ts → dist/index.d.ts} +14 -28
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +10 -0
- package/dist/router.d.ts +16 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +117 -0
- package/dist/router.js.map +10 -0
- package/dist/service.d.ts +151 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +341 -0
- package/dist/service.js.map +10 -0
- package/dist/types.d.ts +71 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +9 -0
- package/dist/vision-app.d.ts +166 -0
- package/dist/vision-app.d.ts.map +1 -0
- package/dist/vision-app.js +611 -0
- package/dist/vision-app.js.map +10 -0
- package/dist/vision.d.ts +63 -0
- package/dist/vision.d.ts.map +1 -0
- package/dist/vision.js +223 -0
- package/dist/vision.js.map +10 -0
- package/package.json +13 -3
- package/.env.example +0 -3
- package/.eslintrc.cjs +0 -7
- package/.turbo/turbo-build.log +0 -1
- package/src/event-bus.ts +0 -409
- package/src/event-registry.ts +0 -158
- package/src/router.ts +0 -118
- package/src/service.ts +0 -618
- package/src/types.ts +0 -93
- package/src/vision-app.ts +0 -880
- package/src/vision.ts +0 -319
- package/tsconfig.json +0 -9
package/src/event-bus.ts
DELETED
|
@@ -1,409 +0,0 @@
|
|
|
1
|
-
import { Queue, Worker, QueueEvents } from 'bullmq'
|
|
2
|
-
import type { QueueEventsOptions, QueueOptions, WorkerOptions } from 'bullmq'
|
|
3
|
-
import { eventRegistry } from './event-registry'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* EventBus configuration
|
|
7
|
-
*/
|
|
8
|
-
export interface EventBusConfig {
|
|
9
|
-
redis?: {
|
|
10
|
-
host?: string
|
|
11
|
-
port?: number
|
|
12
|
-
password?: string
|
|
13
|
-
/**
|
|
14
|
-
* Enable keepalive to prevent connection timeouts (default: true in production)
|
|
15
|
-
*/
|
|
16
|
-
keepAlive?: number
|
|
17
|
-
/**
|
|
18
|
-
* Max retry attempts for failed commands (default: 20)
|
|
19
|
-
*/
|
|
20
|
-
maxRetriesPerRequest?: number
|
|
21
|
-
/**
|
|
22
|
-
* Enable ready check before executing commands (default: true)
|
|
23
|
-
*/
|
|
24
|
-
enableReadyCheck?: boolean
|
|
25
|
-
/**
|
|
26
|
-
* Connection timeout in ms (default: 10000)
|
|
27
|
-
*/
|
|
28
|
-
connectTimeout?: number
|
|
29
|
-
/**
|
|
30
|
-
* Enable offline queue (default: true)
|
|
31
|
-
*/
|
|
32
|
-
enableOfflineQueue?: boolean
|
|
33
|
-
}
|
|
34
|
-
queue?: Omit<QueueOptions, 'connection'>
|
|
35
|
-
worker?: Omit<WorkerOptions, 'connection'>
|
|
36
|
-
queueEvents?: Omit<QueueEventsOptions, 'connection'>
|
|
37
|
-
/**
|
|
38
|
-
* Default BullMQ worker concurrency (per event). Per-handler options override this.
|
|
39
|
-
*/
|
|
40
|
-
workerConcurrency?: number
|
|
41
|
-
// Dev mode - use in-memory (no Redis required)
|
|
42
|
-
devMode?: boolean
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* EventBus - Abstraction over BullMQ
|
|
47
|
-
*/
|
|
48
|
-
export class EventBus {
|
|
49
|
-
private queues = new Map<string, Queue>()
|
|
50
|
-
private workers = new Map<string, Worker>()
|
|
51
|
-
private queueEvents = new Map<string, QueueEvents>()
|
|
52
|
-
private config: EventBusConfig
|
|
53
|
-
private devModeHandlers = new Map<string, Array<(data: any) => Promise<void>>>()
|
|
54
|
-
|
|
55
|
-
constructor(config: EventBusConfig = {}) {
|
|
56
|
-
// Build Redis config from environment variables
|
|
57
|
-
const envUrl = process.env.REDIS_URL
|
|
58
|
-
let envRedis: { host?: string; port?: number; password?: string } | undefined
|
|
59
|
-
if (envUrl) {
|
|
60
|
-
try {
|
|
61
|
-
const u = new URL(envUrl)
|
|
62
|
-
envRedis = {
|
|
63
|
-
host: u.hostname || undefined,
|
|
64
|
-
port: u.port ? parseInt(u.port) : 6379,
|
|
65
|
-
// URL password takes precedence over REDIS_PASSWORD
|
|
66
|
-
password: u.password || process.env.REDIS_PASSWORD || undefined,
|
|
67
|
-
}
|
|
68
|
-
} catch {
|
|
69
|
-
// Fallback to individual env vars if URL is invalid
|
|
70
|
-
envRedis = undefined
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (!envRedis) {
|
|
75
|
-
envRedis = {
|
|
76
|
-
host: process.env.REDIS_HOST || 'localhost',
|
|
77
|
-
port: parseInt(process.env.REDIS_PORT || '6379'),
|
|
78
|
-
password: process.env.REDIS_PASSWORD || undefined,
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Merge: explicit config.redis overrides env-derived values
|
|
83
|
-
const mergedRedis = { ...(envRedis || {}), ...(config.redis || {}) }
|
|
84
|
-
|
|
85
|
-
// Determine if Redis is configured by env or explicit config
|
|
86
|
-
const hasRedisFromEnv = Boolean(envUrl)
|
|
87
|
-
const hasRedisFromConfig = Boolean(
|
|
88
|
-
config.redis && (config.redis.host || config.redis.port || config.redis.password)
|
|
89
|
-
)
|
|
90
|
-
const hasRedis = hasRedisFromEnv || hasRedisFromConfig
|
|
91
|
-
|
|
92
|
-
// devMode precedence:
|
|
93
|
-
// 1) Respect explicit config.devMode when provided (true/false)
|
|
94
|
-
// 2) Otherwise, if Redis is configured (env or config), use production mode (devMode=false)
|
|
95
|
-
// 3) Otherwise, default to devMode=true (in-memory)
|
|
96
|
-
const resolvedDevMode =
|
|
97
|
-
typeof config.devMode === 'boolean' ? config.devMode : !hasRedis
|
|
98
|
-
|
|
99
|
-
this.config = {
|
|
100
|
-
devMode: resolvedDevMode,
|
|
101
|
-
redis: mergedRedis,
|
|
102
|
-
workerConcurrency: config.workerConcurrency,
|
|
103
|
-
queue: config.queue,
|
|
104
|
-
worker: config.worker,
|
|
105
|
-
queueEvents: config.queueEvents,
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Get Redis connection configuration
|
|
111
|
-
* Includes keepalive, retry strategy, and connection pooling
|
|
112
|
-
*/
|
|
113
|
-
private getRedisConnection() {
|
|
114
|
-
const baseConfig = this.config.redis || {
|
|
115
|
-
host: 'localhost',
|
|
116
|
-
port: 6379,
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
return {
|
|
120
|
-
host: baseConfig.host,
|
|
121
|
-
port: baseConfig.port,
|
|
122
|
-
password: baseConfig.password,
|
|
123
|
-
keepAlive: baseConfig.keepAlive ?? 30000, // 30 seconds keepalive
|
|
124
|
-
maxRetriesPerRequest: baseConfig.maxRetriesPerRequest ?? 20,
|
|
125
|
-
enableReadyCheck: baseConfig.enableReadyCheck ?? true,
|
|
126
|
-
connectTimeout: baseConfig.connectTimeout ?? 10000,
|
|
127
|
-
enableOfflineQueue: baseConfig.enableOfflineQueue ?? true,
|
|
128
|
-
// Retry strategy for automatic reconnection
|
|
129
|
-
retryStrategy: (times: number) => {
|
|
130
|
-
if (times > 10) {
|
|
131
|
-
console.error('❌ Redis connection failed after 10 retries')
|
|
132
|
-
return null // Stop retrying
|
|
133
|
-
}
|
|
134
|
-
const delay = Math.min(times * 200, 3000)
|
|
135
|
-
console.log(`🔄 Redis reconnecting... attempt ${times}, delay ${delay}ms`)
|
|
136
|
-
return delay
|
|
137
|
-
},
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Get or create a queue for an event
|
|
143
|
-
*/
|
|
144
|
-
private getQueue(eventName: string): Queue {
|
|
145
|
-
if (this.config.devMode) {
|
|
146
|
-
// In dev mode, we don't use actual queues
|
|
147
|
-
return null as any
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
let queue = this.queues.get(eventName)
|
|
151
|
-
if (!queue) {
|
|
152
|
-
queue = new Queue(eventName, {
|
|
153
|
-
...(this.config.queue || {}),
|
|
154
|
-
connection: this.getRedisConnection(),
|
|
155
|
-
})
|
|
156
|
-
this.queues.set(eventName, queue)
|
|
157
|
-
}
|
|
158
|
-
return queue
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Get or create a queue for a cron job
|
|
163
|
-
*/
|
|
164
|
-
async getQueueForCron(cronName: string): Promise<Queue> {
|
|
165
|
-
if (this.config.devMode) {
|
|
166
|
-
// In dev mode, return a mock queue
|
|
167
|
-
return {
|
|
168
|
-
upsertJobScheduler: async () => {},
|
|
169
|
-
} as any
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
return this.getQueue(cronName)
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Emit an event
|
|
177
|
-
*/
|
|
178
|
-
async emit<T extends Record<string, any>>(
|
|
179
|
-
eventName: string,
|
|
180
|
-
data: T
|
|
181
|
-
): Promise<void> {
|
|
182
|
-
// Get event metadata from registry
|
|
183
|
-
const eventMeta = eventRegistry.getEvent(eventName)
|
|
184
|
-
if (!eventMeta) {
|
|
185
|
-
throw new Error(`Event "${eventName}" not registered. Did you forget to add .on('${eventName}', {...})?`)
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// Validate data with Zod schema (enforce no unknown keys when possible)
|
|
189
|
-
try {
|
|
190
|
-
// If the event schema is a ZodObject, use strict() to disallow unknown keys
|
|
191
|
-
const maybeStrictSchema: any = (eventMeta.schema as any)
|
|
192
|
-
const strictSchema = typeof maybeStrictSchema?.strict === 'function'
|
|
193
|
-
? maybeStrictSchema.strict()
|
|
194
|
-
: eventMeta.schema
|
|
195
|
-
|
|
196
|
-
const validatedData = (strictSchema as typeof eventMeta.schema).parse(data)
|
|
197
|
-
|
|
198
|
-
if (this.config.devMode) {
|
|
199
|
-
// Dev mode - execute handlers immediately (in-memory)
|
|
200
|
-
const handlers = this.devModeHandlers.get(eventName) || []
|
|
201
|
-
|
|
202
|
-
for (const handler of handlers) {
|
|
203
|
-
try {
|
|
204
|
-
await handler(validatedData)
|
|
205
|
-
eventRegistry.incrementEventCount(eventName, false)
|
|
206
|
-
} catch (error) {
|
|
207
|
-
console.error(`Error in handler for event "${eventName}":`, error)
|
|
208
|
-
eventRegistry.incrementEventCount(eventName, true)
|
|
209
|
-
throw error
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
} else {
|
|
213
|
-
// Production mode - use BullMQ
|
|
214
|
-
const queue = this.getQueue(eventName)
|
|
215
|
-
await queue.add(eventName, validatedData, {
|
|
216
|
-
attempts: 3,
|
|
217
|
-
backoff: {
|
|
218
|
-
type: 'exponential',
|
|
219
|
-
delay: 2000,
|
|
220
|
-
},
|
|
221
|
-
})
|
|
222
|
-
eventRegistry.incrementEventCount(eventName, false)
|
|
223
|
-
}
|
|
224
|
-
} catch (error) {
|
|
225
|
-
if (error instanceof Error && error.name === 'ZodError') {
|
|
226
|
-
const zodError = error as any
|
|
227
|
-
const errorMessages = zodError.errors?.map((e: any) => ` - ${e.path.join('.')}: ${e.message}`).join('\n') || error.message
|
|
228
|
-
throw new Error(
|
|
229
|
-
`Invalid data for event "${eventName}":\n${errorMessages}`
|
|
230
|
-
)
|
|
231
|
-
}
|
|
232
|
-
throw error
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Register event handler
|
|
238
|
-
*/
|
|
239
|
-
registerHandler<T>(
|
|
240
|
-
eventName: string,
|
|
241
|
-
handler: (data: T) => Promise<void>,
|
|
242
|
-
options?: {
|
|
243
|
-
/**
|
|
244
|
-
* Max number of concurrent jobs this handler will process.
|
|
245
|
-
* Defaults to config.workerConcurrency (or 1).
|
|
246
|
-
*/
|
|
247
|
-
concurrency?: number
|
|
248
|
-
}
|
|
249
|
-
): void {
|
|
250
|
-
if (this.config.devMode) {
|
|
251
|
-
// Dev mode - store handlers in memory
|
|
252
|
-
const handlers = this.devModeHandlers.get(eventName) || []
|
|
253
|
-
handlers.push(handler)
|
|
254
|
-
this.devModeHandlers.set(eventName, handlers)
|
|
255
|
-
} else {
|
|
256
|
-
// Production mode - create BullMQ worker
|
|
257
|
-
const workerKey = `${eventName}-handler`
|
|
258
|
-
|
|
259
|
-
// Close existing worker if it exists
|
|
260
|
-
const existingWorker = this.workers.get(workerKey)
|
|
261
|
-
if (existingWorker) {
|
|
262
|
-
void existingWorker.close().catch(() => {})
|
|
263
|
-
this.workers.delete(workerKey)
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const worker = new Worker(
|
|
267
|
-
eventName,
|
|
268
|
-
async (job) => {
|
|
269
|
-
try {
|
|
270
|
-
await handler(job.data)
|
|
271
|
-
} catch (error) {
|
|
272
|
-
eventRegistry.incrementEventCount(eventName, true)
|
|
273
|
-
throw error
|
|
274
|
-
}
|
|
275
|
-
},
|
|
276
|
-
{
|
|
277
|
-
...(this.config.worker || {}),
|
|
278
|
-
connection: this.getRedisConnection(),
|
|
279
|
-
concurrency:
|
|
280
|
-
options?.concurrency ??
|
|
281
|
-
this.config.workerConcurrency ??
|
|
282
|
-
this.config.worker?.concurrency ??
|
|
283
|
-
1,
|
|
284
|
-
}
|
|
285
|
-
)
|
|
286
|
-
|
|
287
|
-
this.workers.set(workerKey, worker)
|
|
288
|
-
|
|
289
|
-
// Listen to queue events
|
|
290
|
-
if (!this.queueEvents.has(eventName)) {
|
|
291
|
-
const queueEvents = new QueueEvents(eventName, {
|
|
292
|
-
...(this.config.queueEvents || {}),
|
|
293
|
-
connection: this.getRedisConnection(),
|
|
294
|
-
})
|
|
295
|
-
|
|
296
|
-
queueEvents.on('completed', ({ jobId }) => {
|
|
297
|
-
console.log(`✅ Event "${eventName}" completed (job: ${jobId})`)
|
|
298
|
-
})
|
|
299
|
-
|
|
300
|
-
queueEvents.on('failed', ({ jobId, failedReason }) => {
|
|
301
|
-
console.error(`❌ Event "${eventName}" failed (job: ${jobId}):`, failedReason)
|
|
302
|
-
})
|
|
303
|
-
|
|
304
|
-
this.queueEvents.set(eventName, queueEvents)
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
/**
|
|
310
|
-
* Register cron job handler
|
|
311
|
-
*/
|
|
312
|
-
registerCronHandler(
|
|
313
|
-
cronName: string,
|
|
314
|
-
handler: (context: any) => Promise<void>
|
|
315
|
-
): void {
|
|
316
|
-
if (this.config.devMode) {
|
|
317
|
-
// Dev mode - cron jobs run immediately for testing
|
|
318
|
-
// We'll implement a simple interval-based scheduler
|
|
319
|
-
console.log(`🧹 Cron job "${cronName}" registered (dev mode - manual trigger only)`)
|
|
320
|
-
|
|
321
|
-
// Store handler for manual trigger
|
|
322
|
-
const handlers = this.devModeHandlers.get(cronName) || []
|
|
323
|
-
handlers.push(handler)
|
|
324
|
-
this.devModeHandlers.set(cronName, handlers)
|
|
325
|
-
} else {
|
|
326
|
-
// Production mode - create BullMQ worker for cron jobs
|
|
327
|
-
const cronWorkerKey = `${cronName}-handler`
|
|
328
|
-
|
|
329
|
-
// Close existing cron worker if it exists
|
|
330
|
-
const existingCronWorker = this.workers.get(cronWorkerKey)
|
|
331
|
-
if (existingCronWorker) {
|
|
332
|
-
void existingCronWorker.close().catch(() => {})
|
|
333
|
-
this.workers.delete(cronWorkerKey)
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
const worker = new Worker(
|
|
337
|
-
cronName,
|
|
338
|
-
async (job) => {
|
|
339
|
-
try {
|
|
340
|
-
// Create a simple context for cron handler
|
|
341
|
-
const context = {
|
|
342
|
-
jobId: job.id,
|
|
343
|
-
timestamp: Date.now(),
|
|
344
|
-
}
|
|
345
|
-
await handler(context)
|
|
346
|
-
eventRegistry.incrementCronCount(cronName, false)
|
|
347
|
-
} catch (error) {
|
|
348
|
-
eventRegistry.incrementCronCount(cronName, true)
|
|
349
|
-
throw error
|
|
350
|
-
}
|
|
351
|
-
},
|
|
352
|
-
{
|
|
353
|
-
...(this.config.worker || {}),
|
|
354
|
-
connection: this.getRedisConnection(),
|
|
355
|
-
concurrency: this.config.worker?.concurrency ?? 1,
|
|
356
|
-
}
|
|
357
|
-
)
|
|
358
|
-
|
|
359
|
-
this.workers.set(cronWorkerKey, worker)
|
|
360
|
-
|
|
361
|
-
// Listen to cron job events
|
|
362
|
-
if (!this.queueEvents.has(cronName)) {
|
|
363
|
-
const queueEvents = new QueueEvents(cronName, {
|
|
364
|
-
...(this.config.queueEvents || {}),
|
|
365
|
-
connection: this.getRedisConnection(),
|
|
366
|
-
})
|
|
367
|
-
|
|
368
|
-
queueEvents.on('completed', ({ jobId }) => {
|
|
369
|
-
console.log(`✅ Cron job "${cronName}" completed (job: ${jobId})`)
|
|
370
|
-
})
|
|
371
|
-
|
|
372
|
-
queueEvents.on('failed', ({ jobId, failedReason }) => {
|
|
373
|
-
console.error(`❌ Cron job "${cronName}" failed (job: ${jobId}):`, failedReason)
|
|
374
|
-
})
|
|
375
|
-
|
|
376
|
-
this.queueEvents.set(cronName, queueEvents)
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
/**
|
|
382
|
-
* Close all connections
|
|
383
|
-
*/
|
|
384
|
-
async close(): Promise<void> {
|
|
385
|
-
if (this.config.devMode) {
|
|
386
|
-
this.devModeHandlers.clear()
|
|
387
|
-
return
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
// Close all queues
|
|
391
|
-
for (const queue of this.queues.values()) {
|
|
392
|
-
await queue.close()
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
// Close all workers
|
|
396
|
-
for (const worker of this.workers.values()) {
|
|
397
|
-
await worker.close()
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
// Close all queue events
|
|
401
|
-
for (const queueEvent of this.queueEvents.values()) {
|
|
402
|
-
await queueEvent.close()
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
this.queues.clear()
|
|
406
|
-
this.workers.clear()
|
|
407
|
-
this.queueEvents.clear()
|
|
408
|
-
}
|
|
409
|
-
}
|
package/src/event-registry.ts
DELETED
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
import type { z } from 'zod'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Event metadata for UI and runtime
|
|
5
|
-
*/
|
|
6
|
-
export interface EventMetadata<T = any> {
|
|
7
|
-
name: string
|
|
8
|
-
description?: string
|
|
9
|
-
icon?: string
|
|
10
|
-
tags?: string[]
|
|
11
|
-
schema: z.ZodSchema<T>
|
|
12
|
-
handlers: Array<(data: T) => Promise<void>>
|
|
13
|
-
lastTriggered?: Date
|
|
14
|
-
totalCount: number
|
|
15
|
-
failedCount: number
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Cron job metadata
|
|
20
|
-
*/
|
|
21
|
-
export interface CronMetadata {
|
|
22
|
-
name: string
|
|
23
|
-
schedule: string
|
|
24
|
-
description?: string
|
|
25
|
-
icon?: string
|
|
26
|
-
tags?: string[]
|
|
27
|
-
handler: (context: any) => Promise<void>
|
|
28
|
-
lastRun?: Date
|
|
29
|
-
nextRun?: Date
|
|
30
|
-
totalRuns: number
|
|
31
|
-
failedRuns: number
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Global event registry
|
|
36
|
-
*/
|
|
37
|
-
export class EventRegistry {
|
|
38
|
-
private events = new Map<string, EventMetadata>()
|
|
39
|
-
private crons = new Map<string, CronMetadata>()
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Register an event with schema and handler
|
|
43
|
-
*/
|
|
44
|
-
registerEvent<T>(
|
|
45
|
-
name: string,
|
|
46
|
-
schema: z.ZodSchema<T>,
|
|
47
|
-
handler: (data: T) => Promise<void>,
|
|
48
|
-
metadata?: {
|
|
49
|
-
description?: string
|
|
50
|
-
icon?: string
|
|
51
|
-
tags?: string[]
|
|
52
|
-
}
|
|
53
|
-
): void {
|
|
54
|
-
const existing = this.events.get(name)
|
|
55
|
-
|
|
56
|
-
if (existing) {
|
|
57
|
-
// Add handler to existing event
|
|
58
|
-
existing.handlers.push(handler)
|
|
59
|
-
} else {
|
|
60
|
-
// Create new event
|
|
61
|
-
this.events.set(name, {
|
|
62
|
-
name,
|
|
63
|
-
schema,
|
|
64
|
-
handlers: [handler],
|
|
65
|
-
description: metadata?.description,
|
|
66
|
-
icon: metadata?.icon,
|
|
67
|
-
tags: metadata?.tags,
|
|
68
|
-
totalCount: 0,
|
|
69
|
-
failedCount: 0,
|
|
70
|
-
})
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Register a cron job
|
|
76
|
-
*/
|
|
77
|
-
registerCron(
|
|
78
|
-
name: string,
|
|
79
|
-
schedule: string,
|
|
80
|
-
handler: (context: any) => Promise<void>,
|
|
81
|
-
metadata?: {
|
|
82
|
-
description?: string
|
|
83
|
-
icon?: string
|
|
84
|
-
tags?: string[]
|
|
85
|
-
}
|
|
86
|
-
): void {
|
|
87
|
-
this.crons.set(name, {
|
|
88
|
-
name,
|
|
89
|
-
schedule,
|
|
90
|
-
handler,
|
|
91
|
-
description: metadata?.description,
|
|
92
|
-
icon: metadata?.icon,
|
|
93
|
-
tags: metadata?.tags,
|
|
94
|
-
totalRuns: 0,
|
|
95
|
-
failedRuns: 0,
|
|
96
|
-
})
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Get event metadata
|
|
101
|
-
*/
|
|
102
|
-
getEvent(name: string): EventMetadata | undefined {
|
|
103
|
-
return this.events.get(name)
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Get all events
|
|
108
|
-
*/
|
|
109
|
-
getAllEvents(): EventMetadata[] {
|
|
110
|
-
return Array.from(this.events.values())
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Get all cron jobs
|
|
115
|
-
*/
|
|
116
|
-
getAllCrons(): CronMetadata[] {
|
|
117
|
-
return Array.from(this.crons.values())
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Increment event count
|
|
122
|
-
*/
|
|
123
|
-
incrementEventCount(name: string, failed = false): void {
|
|
124
|
-
const event = this.events.get(name)
|
|
125
|
-
if (event) {
|
|
126
|
-
event.totalCount++
|
|
127
|
-
if (failed) {
|
|
128
|
-
event.failedCount++
|
|
129
|
-
}
|
|
130
|
-
event.lastTriggered = new Date()
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Increment cron run count
|
|
136
|
-
*/
|
|
137
|
-
incrementCronCount(name: string, failed = false): void {
|
|
138
|
-
const cron = this.crons.get(name)
|
|
139
|
-
if (cron) {
|
|
140
|
-
cron.totalRuns++
|
|
141
|
-
if (failed) {
|
|
142
|
-
cron.failedRuns++
|
|
143
|
-
}
|
|
144
|
-
cron.lastRun = new Date()
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Clear all registrations (for testing)
|
|
150
|
-
*/
|
|
151
|
-
clear(): void {
|
|
152
|
-
this.events.clear()
|
|
153
|
-
this.crons.clear()
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Global singleton
|
|
158
|
-
export const eventRegistry = new EventRegistry()
|
package/src/router.ts
DELETED
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
import type { Hono } from 'hono'
|
|
2
|
-
import { readdirSync, statSync } from 'fs'
|
|
3
|
-
import { join, resolve, relative, sep } from 'path'
|
|
4
|
-
import { pathToFileURL } from 'url'
|
|
5
|
-
import type { EventBus } from './event-bus'
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Autoload Vision/Hono sub-apps from a directory structure like app/routes/.../index.ts
|
|
9
|
-
* Each folder becomes a base path. Dynamic segments [id] are converted to :id.
|
|
10
|
-
*
|
|
11
|
-
* Examples:
|
|
12
|
-
* - app/routes/users/index.ts -> /users
|
|
13
|
-
* - app/routes/users/[id]/index.ts -> /users/:id
|
|
14
|
-
* - app/routes/index.ts -> /
|
|
15
|
-
*/
|
|
16
|
-
export async function loadSubApps(app: Hono, routesDir: string = './app/routes', eventBus?: EventBus): Promise<Array<{ name: string; routes: any[] }>> {
|
|
17
|
-
const mounted: Array<{ base: string }> = []
|
|
18
|
-
const allSubAppSummaries: Array<{ name: string; routes: any[] }> = []
|
|
19
|
-
|
|
20
|
-
function toBasePath(dirPath: string): string {
|
|
21
|
-
const rel = relative(resolve(routesDir), resolve(dirPath))
|
|
22
|
-
if (!rel || rel === '' || rel === '.' ) return '/'
|
|
23
|
-
const segments = rel.split(sep).filter(Boolean).map((s) => {
|
|
24
|
-
if (s.startsWith('[') && s.endsWith(']')) return `:${s.slice(1, -1)}`
|
|
25
|
-
return s
|
|
26
|
-
})
|
|
27
|
-
return '/' + segments.join('/')
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function isDynamicSegment(name: string): boolean {
|
|
31
|
-
return name.startsWith('[') && name.endsWith(']')
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
async function scan(dir: string) {
|
|
35
|
-
const entries = readdirSync(dir)
|
|
36
|
-
// If folder contains index.ts or index.js, treat it as a sub-app root
|
|
37
|
-
const hasTs = entries.includes('index.ts')
|
|
38
|
-
const hasJs = entries.includes('index.js')
|
|
39
|
-
if (hasTs || hasJs) {
|
|
40
|
-
const indexFile = resolve(dir, hasTs ? 'index.ts' : 'index.js')
|
|
41
|
-
const modUrl = pathToFileURL(indexFile).href
|
|
42
|
-
const mod: any = await import(modUrl)
|
|
43
|
-
const subApp = mod?.default
|
|
44
|
-
if (subApp) {
|
|
45
|
-
// Inject EventBus into sub-app if it's a Vision instance
|
|
46
|
-
if (eventBus && typeof subApp?.setEventBus === 'function') {
|
|
47
|
-
subApp.setEventBus(eventBus)
|
|
48
|
-
}
|
|
49
|
-
const base = toBasePath(dir)
|
|
50
|
-
// If it's a Vision sub-app, build its services before mounting
|
|
51
|
-
try {
|
|
52
|
-
if (typeof (subApp as any)?.service === 'function') {
|
|
53
|
-
await (subApp as any).buildAllServices?.()
|
|
54
|
-
// Collect sub-app services/routes for bulk registration later
|
|
55
|
-
const summaries = (subApp as any).getServiceSummaries?.()
|
|
56
|
-
if (Array.isArray(summaries) && summaries.length > 0) {
|
|
57
|
-
// Prefix all route paths with the base path
|
|
58
|
-
const prefixedSummaries = summaries.map(s => ({
|
|
59
|
-
...s,
|
|
60
|
-
routes: s.routes.map((r: any) => ({
|
|
61
|
-
...r,
|
|
62
|
-
path: base === '/' ? r.path : base + (r.path === '/' ? '' : r.path)
|
|
63
|
-
}))
|
|
64
|
-
}))
|
|
65
|
-
allSubAppSummaries.push(...prefixedSummaries)
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
} catch (e) {
|
|
69
|
-
console.error(`❌ Error preparing sub-app ${dir}:`, (e as any)?.message || e)
|
|
70
|
-
}
|
|
71
|
-
// Mount the sub-app only if it looks like a Hono/Vision instance with routes
|
|
72
|
-
const routes = (subApp as any)?.routes
|
|
73
|
-
if (Array.isArray(routes)) {
|
|
74
|
-
;(app as any).route(base, subApp)
|
|
75
|
-
mounted.push({ base })
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
// Recurse into child directories
|
|
80
|
-
// Sort entries: static folders first, then dynamic [param] folders
|
|
81
|
-
// This ensures static routes have priority over dynamic routes
|
|
82
|
-
const sortedEntries = [...entries].sort((a, b) => {
|
|
83
|
-
const aIsDynamic = isDynamicSegment(a)
|
|
84
|
-
const bIsDynamic = isDynamicSegment(b)
|
|
85
|
-
if (aIsDynamic && !bIsDynamic) return 1 // dynamic after static
|
|
86
|
-
if (!aIsDynamic && bIsDynamic) return -1 // static before dynamic
|
|
87
|
-
return a.localeCompare(b) // alphabetical within same type
|
|
88
|
-
})
|
|
89
|
-
for (const name of sortedEntries) {
|
|
90
|
-
const full = join(dir, name)
|
|
91
|
-
const st = statSync(full)
|
|
92
|
-
if (st.isDirectory()) await scan(full)
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Only scan if directory exists
|
|
97
|
-
try {
|
|
98
|
-
statSync(routesDir)
|
|
99
|
-
} catch {
|
|
100
|
-
return []
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
await scan(routesDir)
|
|
104
|
-
|
|
105
|
-
// Merge services by name (combine routes from same service name)
|
|
106
|
-
const mergedServices = new Map<string, { name: string; routes: any[] }>()
|
|
107
|
-
for (const summary of allSubAppSummaries) {
|
|
108
|
-
if (mergedServices.has(summary.name)) {
|
|
109
|
-
const existing = mergedServices.get(summary.name)!
|
|
110
|
-
existing.routes.push(...summary.routes)
|
|
111
|
-
} else {
|
|
112
|
-
mergedServices.set(summary.name, { name: summary.name, routes: [...summary.routes] })
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Return merged services (don't register here - let caller handle it)
|
|
117
|
-
return Array.from(mergedServices.values())
|
|
118
|
-
}
|