@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.
Files changed (45) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/event-bus.d.ts +87 -0
  3. package/dist/event-bus.d.ts.map +1 -0
  4. package/dist/event-bus.js +265 -0
  5. package/dist/event-bus.js.map +10 -0
  6. package/dist/event-registry.d.ts +79 -0
  7. package/dist/event-registry.d.ts.map +1 -0
  8. package/dist/event-registry.js +93 -0
  9. package/dist/event-registry.js.map +10 -0
  10. package/{src/index.ts → dist/index.d.ts} +14 -28
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +33 -0
  13. package/dist/index.js.map +10 -0
  14. package/dist/router.d.ts +16 -0
  15. package/dist/router.d.ts.map +1 -0
  16. package/dist/router.js +117 -0
  17. package/dist/router.js.map +10 -0
  18. package/dist/service.d.ts +151 -0
  19. package/dist/service.d.ts.map +1 -0
  20. package/dist/service.js +341 -0
  21. package/dist/service.js.map +10 -0
  22. package/dist/types.d.ts +71 -0
  23. package/dist/types.d.ts.map +1 -0
  24. package/dist/types.js +2 -0
  25. package/dist/types.js.map +9 -0
  26. package/dist/vision-app.d.ts +166 -0
  27. package/dist/vision-app.d.ts.map +1 -0
  28. package/dist/vision-app.js +611 -0
  29. package/dist/vision-app.js.map +10 -0
  30. package/dist/vision.d.ts +63 -0
  31. package/dist/vision.d.ts.map +1 -0
  32. package/dist/vision.js +223 -0
  33. package/dist/vision.js.map +10 -0
  34. package/package.json +13 -3
  35. package/.env.example +0 -3
  36. package/.eslintrc.cjs +0 -7
  37. package/.turbo/turbo-build.log +0 -1
  38. package/src/event-bus.ts +0 -409
  39. package/src/event-registry.ts +0 -158
  40. package/src/router.ts +0 -118
  41. package/src/service.ts +0 -618
  42. package/src/types.ts +0 -93
  43. package/src/vision-app.ts +0 -880
  44. package/src/vision.ts +0 -319
  45. package/tsconfig.json +0 -9
package/src/vision-app.ts DELETED
@@ -1,880 +0,0 @@
1
- import { Hono } from 'hono'
2
- import type { Env, Schema } from 'hono'
3
- import { VisionCore, runInTraceContext } from '@getvision/core'
4
- import type { RouteMetadata } from '@getvision/core'
5
- import { AsyncLocalStorage } from 'async_hooks'
6
- import { existsSync } from 'fs'
7
- import { spawn, spawnSync, type ChildProcess } from 'child_process'
8
- import { ServiceBuilder } from './service'
9
- import { EventBus } from './event-bus'
10
- import { eventRegistry } from './event-registry'
11
- import type { serve as honoServe } from '@hono/node-server'
12
- import type { QueueEventsOptions, QueueOptions, WorkerOptions } from "bullmq";
13
-
14
- export interface VisionALSContext {
15
- vision: VisionCore
16
- traceId: string
17
- rootSpanId: string
18
- }
19
-
20
- const visionContext = new AsyncLocalStorage<VisionALSContext>()
21
-
22
- // Global instance tracking for hot-reload cleanup
23
- // Must attach to globalThis because module-scoped variables are reset when the module is reloaded
24
- const GLOBAL_VISION_KEY = '__vision_instance_state'
25
- interface VisionGlobalState {
26
- instance: Vision<any, any, any> | null
27
- drizzleProcess: ChildProcess | null
28
- }
29
-
30
- // Initialize global state if needed
31
- if (!(globalThis as any)[GLOBAL_VISION_KEY]) {
32
- (globalThis as any)[GLOBAL_VISION_KEY] = {
33
- instance: null,
34
- drizzleProcess: null
35
- }
36
- }
37
-
38
- function getGlobalState(): VisionGlobalState {
39
- return (globalThis as any)[GLOBAL_VISION_KEY]
40
- }
41
-
42
- async function cleanupVisionInstance(instance: Vision<any, any, any>): Promise<void> {
43
- const existing = (instance as any)._cleanupPromise as Promise<void> | undefined
44
- if (existing) return existing;
45
-
46
- (instance as any)._cleanupPromise = (async () => {
47
- const server = (instance as any).bunServer
48
- const hasBunServer = server && typeof server.stop === 'function'
49
-
50
- try {
51
- if (hasBunServer) {
52
- server.stop()
53
- }
54
- if ((globalThis as any).__vision_bun_server === server) {
55
- (globalThis as any).__vision_bun_server = undefined
56
- }
57
- } catch {}
58
-
59
- try { stopDrizzleStudio({ log: false }) } catch {}
60
- try { await (instance as any).eventBus?.close() } catch {}
61
- })()
62
-
63
- return (instance as any)._cleanupPromise
64
- }
65
-
66
- type BunServeOptions = Parameters<typeof Bun['serve']>[0]
67
- type NodeServeOptions = Parameters<typeof honoServe>[0]
68
-
69
- type VisionStartOptions = Omit<Partial<BunServeOptions>, 'fetch' | 'port'> &
70
- Omit<Partial<NodeServeOptions>, 'fetch' | 'port'>
71
-
72
- // Simple deep merge utility (objects only, arrays are overwritten by source)
73
- function deepMerge<T extends Record<string, any>>(target: T, source: Partial<T>): T {
74
- const output: any = { ...target }
75
- if (source && typeof source === 'object') {
76
- for (const key of Object.keys(source)) {
77
- const srcVal = source[key]
78
- const tgtVal = output[key]
79
- if (
80
- srcVal &&
81
- typeof srcVal === 'object' &&
82
- !Array.isArray(srcVal) &&
83
- tgtVal &&
84
- typeof tgtVal === 'object' &&
85
- !Array.isArray(tgtVal)
86
- ) {
87
- output[key] = deepMerge(tgtVal, srcVal)
88
- } else {
89
- output[key] = srcVal
90
- }
91
- }
92
- }
93
- return output as T
94
- }
95
-
96
- /**
97
- * Vision Server configuration
98
- */
99
- export interface VisionConfig {
100
- service: {
101
- name: string
102
- version?: string
103
- description?: string
104
- integrations?: Record<string, string>
105
- drizzle?: {
106
- autoStart?: boolean
107
- port?: number
108
- }
109
- }
110
- vision?: {
111
- enabled?: boolean
112
- port?: number
113
- maxTraces?: number
114
- maxLogs?: number
115
- logging?: boolean
116
- apiUrl?: string // URL of the API server (for frontend to make HTTP requests)
117
- }
118
- routes?: {
119
- autodiscover?: boolean
120
- dirs?: string[]
121
- }
122
- pubsub?: {
123
- redis?: {
124
- host?: string
125
- port?: number
126
- password?: string
127
- /**
128
- * Enable keepalive to prevent connection timeouts (default: 30000ms)
129
- */
130
- keepAlive?: number
131
- /**
132
- * Max retry attempts for failed commands (default: 20)
133
- */
134
- maxRetriesPerRequest?: number
135
- /**
136
- * Enable ready check before executing commands (default: true)
137
- */
138
- enableReadyCheck?: boolean
139
- /**
140
- * Connection timeout in ms (default: 10000)
141
- */
142
- connectTimeout?: number
143
- /**
144
- * Enable offline queue (default: true)
145
- */
146
- enableOfflineQueue?: boolean
147
- }
148
- devMode?: boolean // Use in-memory event bus (no Redis required)
149
- eventBus?: EventBus // Share EventBus instance across apps (for sub-apps)
150
- /**
151
- * Default BullMQ worker concurrency for all handlers (overridable per handler)
152
- */
153
- workerConcurrency?: number
154
- queue?: Omit<QueueOptions, 'connection'>
155
- worker?: Omit<WorkerOptions, 'connection'>
156
- queueEvents?: Omit<QueueEventsOptions, 'connection'>
157
- }
158
- }
159
-
160
- /**
161
- * Vision - Meta-framework built on Hono with observability
162
- *
163
- * @example
164
- * ```ts
165
- * const app = new Vision({
166
- * service: {
167
- * name: 'My API',
168
- * version: '1.0.0'
169
- * }
170
- * })
171
- *
172
- * const userService = app.service('users')
173
- * .on('user/created', handler)
174
- * .endpoint('GET', '/users/:id', schema, handler)
175
- *
176
- * app.start(3000)
177
- * ```
178
- */
179
- export class Vision<
180
- E extends Env = Env,
181
- S extends Schema = {},
182
- BasePath extends string = '/'
183
- > extends Hono<E, S, BasePath> {
184
- private visionCore: VisionCore
185
- private eventBus: EventBus
186
- private config: VisionConfig
187
- private serviceBuilders: ServiceBuilder<any, E>[] = []
188
- private bunServer?: any
189
- private signalHandler?: () => Promise<void>
190
-
191
- constructor(config?: VisionConfig) {
192
- super()
193
-
194
- const defaultConfig: VisionConfig = {
195
- service: {
196
- name: 'Vision SubApp',
197
- },
198
- vision: {
199
- enabled: false,
200
- port: 9500,
201
- },
202
- // Do not set a default devMode here; let EventBus derive from Redis presence
203
- pubsub: {},
204
- routes: {
205
- autodiscover: true,
206
- dirs: ['app/routes'],
207
- },
208
- }
209
-
210
- // Deep merge to respect nested overrides
211
- this.config = deepMerge(defaultConfig, config || {})
212
-
213
- // Initialize Vision Core
214
- const visionEnabled = this.config.vision?.enabled !== false
215
- const visionPort = this.config.vision?.port ?? 9500
216
-
217
- if (visionEnabled) {
218
- this.visionCore = new VisionCore({
219
- port: visionPort,
220
- maxTraces: this.config.vision?.maxTraces ?? 1000,
221
- maxLogs: this.config.vision?.maxLogs ?? 10000,
222
- apiUrl: this.config.vision?.apiUrl,
223
- })
224
-
225
- // Detect and optionally start Drizzle Studio
226
- const drizzleInfo = detectDrizzle()
227
- let drizzleStudioUrl: string | undefined
228
-
229
- if (drizzleInfo.detected) {
230
- console.log(`🗄️ Drizzle detected (${drizzleInfo.configPath})`)
231
-
232
- if (this.config.service.drizzle?.autoStart) {
233
- const drizzlePort = this.config.service.drizzle.port || 4983
234
- const started = startDrizzleStudio(drizzlePort)
235
- if (started) {
236
- drizzleStudioUrl = 'https://local.drizzle.studio'
237
- }
238
- } else {
239
- console.log('💡 Tip: Enable Drizzle Studio auto-start with drizzle: { autoStart: true }')
240
- drizzleStudioUrl = 'https://local.drizzle.studio'
241
- }
242
- }
243
-
244
- // Clean integrations (remove undefined values)
245
- const cleanIntegrations = Object.fromEntries(
246
- Object.entries(this.config.service.integrations || {}).filter(([_, v]) => v !== undefined)
247
- )
248
-
249
- // Set app status
250
- this.visionCore.setAppStatus({
251
- name: this.config.service.name,
252
- version: this.config.service.version ?? '0.0.0',
253
- description: this.config.service.description,
254
- running: true,
255
- pid: process.pid,
256
- metadata: {
257
- framework: 'vision-server',
258
- integrations: Object.keys(cleanIntegrations).length > 0 ? cleanIntegrations : undefined,
259
- drizzle: drizzleInfo.detected
260
- ? {
261
- detected: true,
262
- configPath: drizzleInfo.configPath,
263
- studioUrl: drizzleStudioUrl,
264
- autoStarted: this.config.service.drizzle?.autoStart || false,
265
- }
266
- : {
267
- detected: false,
268
- configPath: undefined,
269
- studioUrl: undefined,
270
- autoStarted: false,
271
- },
272
- },
273
- })
274
- } else {
275
- // Create dummy Vision Core that does nothing
276
- this.visionCore = null as any
277
- }
278
-
279
- // Use provided EventBus or create a new one
280
- // Root app creates EventBus, sub-apps can share it via config.pubsub.eventBus
281
- this.eventBus = this.config.pubsub?.eventBus || new EventBus({
282
- redis: this.config.pubsub?.redis,
283
- devMode: this.config.pubsub?.devMode,
284
- workerConcurrency: this.config.pubsub?.workerConcurrency,
285
- queue: this.config.pubsub?.queue,
286
- worker: this.config.pubsub?.worker,
287
- queueEvents: this.config.pubsub?.queueEvents,
288
- })
289
-
290
- // Register JSON-RPC methods for events/cron
291
- if (visionEnabled) {
292
- this.registerEventMethods()
293
- }
294
-
295
- // Install Vision middleware automatically
296
- if (visionEnabled) {
297
- this.installVisionMiddleware()
298
- }
299
- }
300
-
301
- /**
302
- * Register JSON-RPC methods for events and cron jobs
303
- */
304
- private registerEventMethods() {
305
- const server = this.visionCore.getServer()
306
-
307
- // List all events
308
- server.registerMethod('events/list', async () => {
309
- const events = eventRegistry.getAllEvents()
310
- return events.map(event => ({
311
- name: event.name,
312
- description: event.description,
313
- icon: event.icon,
314
- tags: event.tags,
315
- handlers: event.handlers.length,
316
- lastTriggered: event.lastTriggered,
317
- totalCount: event.totalCount,
318
- failedCount: event.failedCount,
319
- }))
320
- })
321
-
322
- // List all cron jobs
323
- server.registerMethod('cron/list', async () => {
324
- const crons = eventRegistry.getAllCrons()
325
- return crons.map(cron => ({
326
- name: cron.name,
327
- schedule: cron.schedule,
328
- description: cron.description,
329
- icon: cron.icon,
330
- tags: cron.tags,
331
- lastRun: cron.lastRun,
332
- nextRun: cron.nextRun,
333
- totalRuns: cron.totalRuns,
334
- failedRuns: cron.failedRuns,
335
- }))
336
- })
337
- }
338
-
339
- /**
340
- * Install Vision tracing middleware
341
- */
342
- private installVisionMiddleware() {
343
- const logging = this.config.vision?.logging !== false
344
-
345
- this.use('*', async (c, next) => {
346
- // Skip OPTIONS requests
347
- if (c.req.method === 'OPTIONS') {
348
- return next()
349
- }
350
-
351
- const startTime = Date.now()
352
-
353
- // Create trace
354
- const trace = this.visionCore.createTrace(c.req.method, c.req.path)
355
-
356
- // Run request in AsyncLocalStorage context
357
- return visionContext.run(
358
- {
359
- vision: this.visionCore,
360
- traceId: trace.id,
361
- rootSpanId: ''
362
- },
363
- async () => {
364
- // Also set core trace context so VisionCore.addContext() works
365
- return runInTraceContext(trace.id, async () => {
366
- // Start main span
367
- const tracer = this.visionCore.getTracer()
368
- const rootSpan = tracer.startSpan('http.request', trace.id)
369
-
370
- // Update context with rootSpanId
371
- const ctx = visionContext.getStore()
372
- if (ctx) {
373
- ctx.rootSpanId = rootSpan.id
374
- }
375
-
376
- // Provide c.span and c.addContext globally for all downstream handlers (Vision/Hono sub-apps)
377
- if (!(c as any).span) {
378
- (c as any).addContext = (context: Record<string, unknown>) => {
379
- const current = visionContext.getStore()
380
- const currentTraceId = current?.traceId || trace.id
381
-
382
- // Add context to trace metadata via VisionCore
383
- const visionTrace = this.visionCore.getTraceStore().getTrace(currentTraceId)
384
- if (visionTrace) {
385
- visionTrace.metadata = { ...(visionTrace.metadata || {}), ...context }
386
- }
387
- }
388
-
389
- (c as any).span = <T>(
390
- name: string,
391
- attributes: Record<string, any> = {},
392
- fn?: () => T
393
- ): T => {
394
- const current = visionContext.getStore()
395
- const currentTraceId = current?.traceId || trace.id
396
- const currentRootSpanId = current?.rootSpanId || rootSpan.id
397
- const s = tracer.startSpan(name, currentTraceId, currentRootSpanId)
398
- for (const [k, v] of Object.entries(attributes)) tracer.setAttribute(s.id, k, v)
399
- try {
400
- const result = fn ? fn() : (undefined as any)
401
- const completed = tracer.endSpan(s.id)
402
- if (completed) this.visionCore.getTraceStore().addSpan(currentTraceId, completed)
403
- return result
404
- } catch (err) {
405
- tracer.setAttribute(s.id, 'error', true)
406
- tracer.setAttribute(s.id, 'error.message', err instanceof Error ? err.message : String(err))
407
- const completed = tracer.endSpan(s.id)
408
- if (completed) this.visionCore.getTraceStore().addSpan(currentTraceId, completed)
409
- throw err
410
- }
411
- }
412
- }
413
-
414
- // Add request attributes
415
- tracer.setAttribute(rootSpan.id, 'http.method', c.req.method)
416
- tracer.setAttribute(rootSpan.id, 'http.path', c.req.path)
417
- tracer.setAttribute(rootSpan.id, 'http.url', c.req.url)
418
-
419
- // Add query params if any
420
- const url = new URL(c.req.url)
421
- if (url.search) {
422
- tracer.setAttribute(rootSpan.id, 'http.query', url.search)
423
- }
424
-
425
- // Capture request metadata
426
- try {
427
- const rawReq = c.req.raw
428
- const headers: Record<string, string> = {}
429
- rawReq.headers.forEach((v, k) => { headers[k] = v })
430
-
431
- const urlObj = new URL(c.req.url)
432
- const query: Record<string, string> = {}
433
- urlObj.searchParams.forEach((v, k) => { query[k] = v })
434
-
435
- let body: unknown = undefined
436
- const ct = headers['content-type'] || headers['Content-Type']
437
- if (ct && ct.includes('application/json')) {
438
- try {
439
- body = await rawReq.clone().json()
440
- } catch {}
441
- }
442
-
443
- const sessionId = headers['x-vision-session']
444
- if (sessionId) {
445
- tracer.setAttribute(rootSpan.id, 'session.id', sessionId)
446
- trace.metadata = { ...(trace.metadata || {}), sessionId }
447
- }
448
-
449
- const requestMeta = {
450
- method: c.req.method,
451
- url: urlObj.pathname + (urlObj.search || ''),
452
- headers,
453
- query: Object.keys(query).length ? query : undefined,
454
- body,
455
- }
456
- tracer.setAttribute(rootSpan.id, 'http.request', requestMeta)
457
- trace.metadata = { ...(trace.metadata || {}), request: requestMeta }
458
-
459
- // Emit start log
460
- if (logging) {
461
- const parts = [
462
- `method=${c.req.method}`,
463
- `path=${c.req.path}`,
464
- ]
465
- if (sessionId) parts.push(`sessionId=${sessionId}`)
466
- parts.push(`traceId=${trace.id}`)
467
- console.info(`INF starting request ${parts.join(' ')}`)
468
- }
469
-
470
- // Execute request
471
- await next()
472
-
473
- // Add response attributes
474
- tracer.setAttribute(rootSpan.id, 'http.status_code', c.res.status)
475
- const resHeaders: Record<string, string> = {}
476
- c.res.headers?.forEach((v, k) => { resHeaders[k] = v as unknown as string })
477
-
478
- let respBody: unknown = undefined
479
- const resCt = c.res.headers?.get('content-type') || ''
480
- try {
481
- const clone = c.res.clone()
482
- if (resCt.includes('application/json')) {
483
- const txt = await clone.text()
484
- if (txt && txt.length <= 65536) {
485
- try { respBody = JSON.parse(txt) } catch { respBody = txt }
486
- }
487
- }
488
- } catch {}
489
-
490
- const responseMeta = {
491
- status: c.res.status,
492
- headers: Object.keys(resHeaders).length ? resHeaders : undefined,
493
- body: respBody,
494
- }
495
- tracer.setAttribute(rootSpan.id, 'http.response', responseMeta)
496
- trace.metadata = { ...(trace.metadata || {}), response: responseMeta }
497
-
498
- } catch (error) {
499
- // Track error
500
- tracer.addEvent(rootSpan.id, 'error', {
501
- message: error instanceof Error ? error.message : 'Unknown error',
502
- stack: error instanceof Error ? error.stack : undefined,
503
- })
504
-
505
- tracer.setAttribute(rootSpan.id, 'error', true)
506
- throw error
507
-
508
- } finally {
509
- // End span and add it to trace
510
- const completedSpan = tracer.endSpan(rootSpan.id)
511
- if (completedSpan) {
512
- this.visionCore.getTraceStore().addSpan(trace.id, completedSpan)
513
- }
514
-
515
- // Complete trace
516
- const duration = Date.now() - startTime
517
- this.visionCore.completeTrace(trace.id, c.res.status, duration)
518
-
519
- // Add trace ID to response headers
520
- c.header('X-Vision-Trace-Id', trace.id)
521
-
522
- // Emit completion log
523
- if (logging) {
524
- console.info(
525
- `INF request completed code=${c.res.status} duration=${duration}ms method=${c.req.method} path=${c.req.path} traceId=${trace.id}`
526
- )
527
- }
528
- }
529
- })
530
- }
531
- )
532
- })
533
- }
534
-
535
-
536
- /**
537
- * Create a new service with builder pattern
538
- *
539
- * @example
540
- * ```ts
541
- * const userService = app.service('users')
542
- * .endpoint('GET', '/users/:id', schema, handler)
543
- * .on('user/created', handler)
544
- * ```
545
- */
546
- service<E2 extends Env = E, TEvents extends Record<string, any> = {}>(name: string) {
547
- const builder = new ServiceBuilder<TEvents, E2>(name, this.eventBus, this.visionCore)
548
-
549
- // Preserve builder for registration in start()
550
- this.serviceBuilders.push(builder as unknown as ServiceBuilder<any, E>)
551
-
552
- return builder
553
- }
554
-
555
- /**
556
- * Get services and routes metadata without registering to this VisionCore
557
- */
558
- public getServiceSummaries(): Array<{ name: string; routes: RouteMetadata[] }> {
559
- const summaries: Array<{ name: string; routes: RouteMetadata[] }> = []
560
- for (const builder of this.serviceBuilders) {
561
- const name = (builder as any).getDisplayName?.() ?? 'Service'
562
- const rawRoutes = (builder as any).getRoutesMetadata?.()
563
- if (!rawRoutes || !Array.isArray(rawRoutes)) continue
564
- const routes: RouteMetadata[] = rawRoutes.map((r: any) => ({
565
- method: r.method,
566
- path: r.path,
567
- handler: name,
568
- queryParams: r.queryParams,
569
- requestBody: r.requestBody,
570
- responseBody: r.responseBody,
571
- }))
572
- summaries.push({ name, routes })
573
- }
574
- return summaries
575
- }
576
-
577
- /**
578
- * Build all service builders
579
- */
580
- public buildAllServices() {
581
- const allServices: Array<{ name: string; routes: RouteMetadata[] }> = []
582
-
583
- // Build all services (this populates allServices via builder.build)
584
- for (const builder of this.serviceBuilders) {
585
- builder.build(this as any, allServices)
586
- }
587
-
588
- // Don't register to VisionCore here - let start() handle it after sub-apps are loaded
589
- // Just return allServices so they can be registered later
590
- return allServices
591
- }
592
-
593
- /**
594
- * Get Vision Core instance
595
- */
596
- getVision(): VisionCore {
597
- return this.visionCore
598
- }
599
-
600
- /**
601
- * Get EventBus instance
602
- */
603
- getEventBus(): EventBus {
604
- return this.eventBus
605
- }
606
-
607
-
608
- /**
609
- * Autoload Vision/Hono sub-apps from configured directories
610
- */
611
- private async autoloadRoutes(): Promise<Array<{ name: string; routes: any[] }>> {
612
- const enabled = this.config.routes?.autodiscover !== false
613
- const dirs = this.config.routes?.dirs ?? ['app/routes']
614
- if (!enabled) return []
615
-
616
- const existing: string[] = []
617
- for (const d of dirs) {
618
- try { if (existsSync(d)) existing.push(d) } catch {}
619
- }
620
- if (existing.length === 0) return []
621
-
622
- const { loadSubApps } = await import('./router')
623
- let allSubAppSummaries: Array<{ name: string; routes: any[] }> = []
624
- for (const d of existing) {
625
- try {
626
- // Pass EventBus to sub-apps so they share the same instance
627
- const summaries = await loadSubApps(this as any, d, this.eventBus)
628
- allSubAppSummaries = allSubAppSummaries.concat(summaries)
629
- } catch (e) {
630
- console.error(`❌ Failed to load sub-apps from ${d}:`, (e as any)?.message || e)
631
- if (e instanceof Error && e.stack) {
632
- console.error('Stack:', e.stack)
633
- }
634
- }
635
- }
636
- return allSubAppSummaries
637
- }
638
-
639
- /**
640
- * Start the server (convenience method)
641
- */
642
- async start(port: number = 3000, options?: VisionStartOptions) {
643
- const { hostname, ...restOptions } = options || {}
644
- const { fetch: _bf, port: _bp, ...bunRest } = restOptions as Partial<BunServeOptions>
645
- const { fetch: _nf, port: _np, ...nodeRest } = restOptions as Partial<NodeServeOptions>
646
-
647
- // Build all services WITHOUT registering to VisionCore yet
648
- const rootSummaries = this.buildAllServices()
649
- // Autoload file-based Vision/Hono sub-apps if enabled (returns merged sub-app summaries)
650
- const subAppSummaries = await this.autoloadRoutes()
651
-
652
- // Merge root and sub-app services by name
653
- const allServices = new Map<string, { name: string; routes: any[] }>()
654
-
655
- // Add root services first
656
- for (const summary of rootSummaries || []) {
657
- allServices.set(summary.name, { name: summary.name, routes: [...summary.routes] })
658
- }
659
-
660
- // Merge sub-app services (combine routes if service name already exists)
661
- for (const summary of subAppSummaries || []) {
662
- if (allServices.has(summary.name)) {
663
- const existing = allServices.get(summary.name)!
664
- existing.routes.push(...summary.routes)
665
- } else {
666
- allServices.set(summary.name, { name: summary.name, routes: [...summary.routes] })
667
- }
668
- }
669
-
670
- // Register all services in one call
671
- if (this.visionCore && allServices.size > 0) {
672
- const servicesToRegister = Array.from(allServices.values())
673
- this.visionCore.registerServices(servicesToRegister)
674
- const flatRoutes = servicesToRegister.flatMap(s => s.routes)
675
- this.visionCore.registerRoutes(flatRoutes)
676
- console.log(`✅ Registered ${servicesToRegister.length} total services (${flatRoutes.length} routes)`)
677
- }
678
-
679
- // Cleanup previous instance before starting new one (hot-reload)
680
- const state = getGlobalState()
681
- if (state.instance && state.instance !== this) {
682
- await cleanupVisionInstance(state.instance)
683
- }
684
- state.instance = this
685
-
686
- console.log(`🚀 Starting ${this.config.service.name}...`)
687
- console.log(`📡 API Server: http://localhost:${port}`)
688
-
689
- // Register signal handlers (cleaned up on dispose)
690
- if (!this.signalHandler) {
691
- this.signalHandler = async () => {
692
- const s = getGlobalState()
693
- if (s.instance) {
694
- await cleanupVisionInstance(s.instance)
695
- }
696
- try { process.exit(0) } catch {}
697
- }
698
- }
699
-
700
- const handleSignal = this.signalHandler
701
-
702
- process.removeListener('SIGINT', handleSignal)
703
- process.removeListener('SIGTERM', handleSignal)
704
- try { process.removeListener('SIGQUIT', handleSignal) } catch {}
705
-
706
- process.on('SIGINT', handleSignal)
707
- process.on('SIGTERM', handleSignal)
708
- try { process.on('SIGQUIT', handleSignal) } catch {}
709
-
710
- // Bun hot-reload: register dispose callback
711
- try {
712
- const hot = (import.meta as any)?.hot
713
- if (hot && typeof hot.dispose === 'function') {
714
- hot.dispose(async () => {
715
- console.log('♻️ Hot reload: reloading...')
716
-
717
- // 1. Remove signal listeners to prevent accumulation
718
- process.off('SIGINT', handleSignal)
719
- process.off('SIGTERM', handleSignal)
720
- try { process.off('SIGQUIT', handleSignal) } catch {}
721
-
722
- // 2. Cleanup this instance
723
- const s = getGlobalState()
724
- await cleanupVisionInstance(this)
725
- if (s.instance === this) {
726
- s.instance = null
727
- }
728
- })
729
- }
730
- } catch {}
731
-
732
- // Prefer Bun if available, then Node.js; otherwise instruct the user to serve manually
733
- if (typeof process !== 'undefined' && process.versions?.bun) {
734
- const BunServe = (globalThis as any).Bun?.serve
735
- if (typeof BunServe === 'function') {
736
- try {
737
- const existing = (globalThis as any).__vision_bun_server
738
- if (existing && typeof existing.stop === 'function') {
739
- try { existing.stop() } catch {}
740
- }
741
- } catch {}
742
- this.bunServer = BunServe({
743
- ...bunRest,
744
- fetch: this.fetch.bind(this),
745
- port,
746
- hostname
747
- })
748
- try { (globalThis as any).__vision_bun_server = this.bunServer } catch {}
749
- } else {
750
- console.warn('Bun detected but Bun.serve is unavailable')
751
- return this
752
- }
753
- } else if (typeof process !== 'undefined' && process.versions?.node) {
754
- const { serve } = await import('@hono/node-server')
755
- serve({
756
- ...nodeRest,
757
- fetch: this.fetch.bind(this),
758
- port,
759
- hostname
760
- })
761
- } else {
762
- // For other runtimes, just return the app
763
- console.log('⚠️ Use your runtime\'s serve function')
764
- return this
765
- }
766
- }
767
-
768
- /**
769
- * Set the EventBus instance (used internally by router to inject shared EventBus)
770
- */
771
- setEventBus(eventBus: EventBus): void {
772
- this.eventBus = eventBus
773
- }
774
- }
775
-
776
- /**
777
- * Get Vision context (internal use)
778
- */
779
- export function getVisionContext(): VisionALSContext | undefined {
780
- return visionContext.getStore()
781
- }
782
-
783
- // ============================================================================
784
- // Drizzle Studio Integration
785
- // ============================================================================
786
-
787
- /**
788
- * Detect Drizzle configuration
789
- */
790
- function detectDrizzle(): { detected: boolean; configPath?: string } {
791
- const possiblePaths = [
792
- 'drizzle.config.ts',
793
- 'drizzle.config.js',
794
- 'drizzle.config.mjs',
795
- ]
796
-
797
- for (const path of possiblePaths) {
798
- if (existsSync(path)) {
799
- return { detected: true, configPath: path }
800
- }
801
- }
802
- return { detected: false }
803
- }
804
-
805
- /**
806
- * Start Drizzle Studio
807
- */
808
- function startDrizzleStudio(port: number): boolean {
809
- const state = getGlobalState()
810
- if (state.drizzleProcess) {
811
- console.log('⚠️ Drizzle Studio is already running')
812
- return false
813
- }
814
-
815
- // If Drizzle Studio is already listening on this port, skip spawning but report available
816
- try {
817
- if (process.platform === 'win32') {
818
- const res = spawnSync('powershell', ['-NoProfile', '-Command', `netstat -ano | Select-String -Pattern "LISTENING\\s+.*:${port}\\s"`], { encoding: 'utf-8' })
819
- if ((res.stdout || '').trim().length > 0) {
820
- console.log(`⚠️ Drizzle Studio port ${port} already in use; assuming it is running. Skipping auto-start.`)
821
- return true
822
- }
823
- } else {
824
- const res = spawnSync('lsof', ['-i', `tcp:${port}`, '-sTCP:LISTEN'], { encoding: 'utf-8' })
825
- if ((res.stdout || '').trim().length > 0) {
826
- console.log(`⚠️ Drizzle Studio port ${port} already in use; assuming it is running. Skipping auto-start.`)
827
- return true
828
- }
829
- }
830
- } catch {}
831
-
832
- try {
833
- const proc = spawn('npx', ['drizzle-kit', 'studio', '--port', String(port), '--host', '0.0.0.0'], {
834
- stdio: 'inherit',
835
- detached: false,
836
- shell: process.platform === 'win32',
837
- })
838
-
839
- state.drizzleProcess = proc
840
-
841
- proc.on('error', (error) => {
842
- console.error('❌ Failed to start Drizzle Studio:', error.message)
843
- })
844
-
845
- proc.on('exit', (code) => {
846
- if (code !== 0 && code !== null) {
847
- console.error(`❌ Drizzle Studio exited with code ${code}`)
848
- }
849
- // Clear global state if it matches this process
850
- const s = getGlobalState()
851
- if (s.drizzleProcess === proc) {
852
- s.drizzleProcess = null
853
- }
854
- })
855
-
856
- console.log(`✅ Drizzle Studio: https://local.drizzle.studio`)
857
- return true
858
- } catch (error) {
859
- console.error('❌ Failed to start Drizzle Studio:', error)
860
- return false
861
- }
862
- }
863
-
864
- /**
865
- * Stop Drizzle Studio
866
- */
867
- function stopDrizzleStudio(options?: { log?: boolean }): boolean {
868
- const state = getGlobalState()
869
- if (state.drizzleProcess) {
870
- // Remove all event listeners to prevent memory leaks
871
- state.drizzleProcess.removeAllListeners()
872
- state.drizzleProcess.kill()
873
- state.drizzleProcess = null
874
- if (options?.log !== false) {
875
- console.log('🛑 Drizzle Studio stopped')
876
- }
877
- return true
878
- }
879
- return false
880
- }