@getvision/server 0.3.0 → 0.3.1-9c3a582-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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getvision/server",
3
- "version": "0.3.0",
3
+ "version": "0.3.1-9c3a582-develop",
4
4
  "type": "module",
5
5
  "description": "Vision Server - Meta-framework with built-in observability, pub/sub, and type-safe APIs",
6
6
  "exports": {
package/src/event-bus.ts CHANGED
@@ -203,6 +203,15 @@ export class EventBus {
203
203
  host: 'localhost',
204
204
  port: 6379,
205
205
  }
206
+ const workerKey = `${eventName}-handler`
207
+
208
+ // Close existing worker if it exists
209
+ const existingWorker = this.workers.get(workerKey)
210
+ if (existingWorker) {
211
+ void existingWorker.close().catch(() => {})
212
+ this.workers.delete(workerKey)
213
+ }
214
+
206
215
  const worker = new Worker(
207
216
  eventName,
208
217
  async (job) => {
@@ -219,7 +228,7 @@ export class EventBus {
219
228
  }
220
229
  )
221
230
 
222
- this.workers.set(`${eventName}-${Date.now()}`, worker)
231
+ this.workers.set(workerKey, worker)
223
232
 
224
233
  // Listen to queue events
225
234
  if (!this.queueEvents.has(eventName)) {
@@ -266,6 +275,15 @@ export class EventBus {
266
275
  host: 'localhost',
267
276
  port: 6379,
268
277
  }
278
+ const cronWorkerKey = `${cronName}-handler`
279
+
280
+ // Close existing cron worker if it exists
281
+ const existingCronWorker = this.workers.get(cronWorkerKey)
282
+ if (existingCronWorker) {
283
+ void existingCronWorker.close().catch(() => {})
284
+ this.workers.delete(cronWorkerKey)
285
+ }
286
+
269
287
  const worker = new Worker(
270
288
  cronName,
271
289
  async (job) => {
@@ -287,7 +305,7 @@ export class EventBus {
287
305
  }
288
306
  )
289
307
 
290
- this.workers.set(`${cronName}-${Date.now()}`, worker)
308
+ this.workers.set(cronWorkerKey, worker)
291
309
 
292
310
  // Listen to cron job events
293
311
  if (!this.queueEvents.has(cronName)) {
package/src/vision-app.ts CHANGED
@@ -18,6 +18,51 @@ export interface VisionALSContext {
18
18
 
19
19
  const visionContext = new AsyncLocalStorage<VisionALSContext>()
20
20
 
21
+ // Global instance tracking for hot-reload cleanup
22
+ // Must attach to globalThis because module-scoped variables are reset when the module is reloaded
23
+ const GLOBAL_VISION_KEY = '__vision_instance_state'
24
+ interface VisionGlobalState {
25
+ instance: Vision<any, any, any> | null
26
+ drizzleProcess: ChildProcess | null
27
+ }
28
+
29
+ // Initialize global state if needed
30
+ if (!(globalThis as any)[GLOBAL_VISION_KEY]) {
31
+ (globalThis as any)[GLOBAL_VISION_KEY] = {
32
+ instance: null,
33
+ drizzleProcess: null
34
+ }
35
+ }
36
+
37
+ function getGlobalState(): VisionGlobalState {
38
+ return (globalThis as any)[GLOBAL_VISION_KEY]
39
+ }
40
+
41
+ async function cleanupVisionInstance(instance: Vision<any, any, any>): Promise<void> {
42
+ const existing = (instance as any)._cleanupPromise as Promise<void> | undefined
43
+ if (existing) return existing;
44
+
45
+ (instance as any)._cleanupPromise = (async () => {
46
+ const server = (instance as any).bunServer
47
+ const hasBunServer = server && typeof server.stop === 'function'
48
+
49
+ try {
50
+ if (hasBunServer) {
51
+ server.stop()
52
+ }
53
+ if ((globalThis as any).__vision_bun_server === server) {
54
+ (globalThis as any).__vision_bun_server = undefined
55
+ }
56
+ } catch {}
57
+
58
+ try { stopDrizzleStudio({ log: false }) } catch {}
59
+ try { await (instance as any).eventBus?.close() } catch {}
60
+ try { eventRegistry.clear() } catch {}
61
+ })()
62
+
63
+ return (instance as any)._cleanupPromise
64
+ }
65
+
21
66
  type BunServeOptions = Parameters<typeof Bun['serve']>[0]
22
67
  type NodeServeOptions = Parameters<typeof honoServe>[0]
23
68
 
@@ -119,7 +164,8 @@ export class Vision<
119
164
  private serviceBuilders: ServiceBuilder<any, E>[] = []
120
165
  private fileBasedRoutes: RouteMetadata[] = []
121
166
  private bunServer?: any
122
- private shuttingDown = false
167
+ private _cleanupPromise?: Promise<void>
168
+ private signalHandler?: () => Promise<void>
123
169
 
124
170
  constructor(config?: VisionConfig) {
125
171
  super()
@@ -616,37 +662,63 @@ export class Vision<
616
662
  console.log(`✅ Registered ${servicesToRegister.length} total services (${flatRoutes.length} routes)`)
617
663
  }
618
664
 
665
+ // Cleanup previous instance before starting new one (hot-reload)
666
+ const state = getGlobalState()
667
+ if (state.instance && state.instance !== this) {
668
+ await cleanupVisionInstance(state.instance)
669
+ }
670
+ state.instance = this
671
+
619
672
  console.log(`🚀 Starting ${this.config.service.name}...`)
620
673
  console.log(`📡 API Server: http://localhost:${port}`)
621
674
 
622
- // Setup cleanup on exit (ensure it runs once)
623
- const cleanup = async () => {
624
- if (this.shuttingDown) return
625
- this.shuttingDown = true
626
- console.log('🛑 Shutting down...')
627
- try {
628
- if (this.bunServer && typeof this.bunServer.stop === 'function') {
629
- try { this.bunServer.stop() } catch {}
675
+ // Register signal handlers (cleaned up on dispose)
676
+ if (!this.signalHandler) {
677
+ this.signalHandler = async () => {
678
+ const s = getGlobalState()
679
+ if (s.instance) {
680
+ await cleanupVisionInstance(s.instance)
630
681
  }
631
- try { if ((globalThis as any).__vision_bun_server === this.bunServer) (globalThis as any).__vision_bun_server = undefined } catch {}
632
- } catch {}
633
- try { stopDrizzleStudio() } catch {}
634
- try { await this.eventBus.close() } catch {}
635
- }
636
-
637
- const wrappedCleanup = async () => {
638
- await cleanup()
639
- try { process.exit(0) } catch {}
682
+ try { process.exit(0) } catch {}
683
+ }
640
684
  }
641
- process.once('SIGINT', wrappedCleanup)
642
- process.once('SIGTERM', wrappedCleanup)
643
- try { process.once('SIGQUIT', wrappedCleanup) } catch {}
685
+
686
+ const handleSignal = this.signalHandler
687
+
688
+ process.removeListener('SIGINT', handleSignal)
689
+ process.removeListener('SIGTERM', handleSignal)
690
+ try { process.removeListener('SIGQUIT', handleSignal) } catch {}
691
+
692
+ process.on('SIGINT', handleSignal)
693
+ process.on('SIGTERM', handleSignal)
694
+ try { process.on('SIGQUIT', handleSignal) } catch {}
695
+
696
+ // Bun hot-reload: register dispose callback
697
+ try {
698
+ const hot = (import.meta as any)?.hot
699
+ if (hot && typeof hot.dispose === 'function') {
700
+ hot.dispose(async () => {
701
+ console.log('♻️ Hot reload: reloading...')
702
+
703
+ // 1. Remove signal listeners to prevent accumulation
704
+ process.off('SIGINT', handleSignal)
705
+ process.off('SIGTERM', handleSignal)
706
+ try { process.off('SIGQUIT', handleSignal) } catch {}
707
+
708
+ // 2. Cleanup this instance
709
+ const s = getGlobalState()
710
+ await cleanupVisionInstance(this)
711
+ if (s.instance === this) {
712
+ s.instance = null
713
+ }
714
+ })
715
+ }
716
+ } catch {}
644
717
 
645
718
  // Prefer Bun if available, then Node.js; otherwise instruct the user to serve manually
646
719
  if (typeof process !== 'undefined' && process.versions?.bun) {
647
720
  const BunServe = (globalThis as any).Bun?.serve
648
721
  if (typeof BunServe === 'function') {
649
- console.log(`Bun detected`)
650
722
  try {
651
723
  const existing = (globalThis as any).__vision_bun_server
652
724
  if (existing && typeof existing.stop === 'function') {
@@ -666,7 +738,6 @@ export class Vision<
666
738
  }
667
739
  } else if (typeof process !== 'undefined' && process.versions?.node) {
668
740
  const { serve } = await import('@hono/node-server')
669
- console.log(`Node.js detected`)
670
741
  serve({
671
742
  ...nodeRest,
672
743
  fetch: this.fetch.bind(this),
@@ -699,8 +770,6 @@ export function getVisionContext(): VisionALSContext | undefined {
699
770
  // Drizzle Studio Integration
700
771
  // ============================================================================
701
772
 
702
- let drizzleStudioProcess: ChildProcess | null = null
703
-
704
773
  /**
705
774
  * Detect Drizzle configuration
706
775
  */
@@ -723,7 +792,8 @@ function detectDrizzle(): { detected: boolean; configPath?: string } {
723
792
  * Start Drizzle Studio
724
793
  */
725
794
  function startDrizzleStudio(port: number): boolean {
726
- if (drizzleStudioProcess) {
795
+ const state = getGlobalState()
796
+ if (state.drizzleProcess) {
727
797
  console.log('⚠️ Drizzle Studio is already running')
728
798
  return false
729
799
  }
@@ -746,19 +816,27 @@ function startDrizzleStudio(port: number): boolean {
746
816
  } catch {}
747
817
 
748
818
  try {
749
- drizzleStudioProcess = spawn('npx', ['drizzle-kit', 'studio', '--port', String(port), '--host', '0.0.0.0'], {
819
+ const proc = spawn('npx', ['drizzle-kit', 'studio', '--port', String(port), '--host', '0.0.0.0'], {
750
820
  stdio: 'inherit',
751
821
  detached: false,
822
+ shell: process.platform === 'win32',
752
823
  })
824
+
825
+ state.drizzleProcess = proc
753
826
 
754
- drizzleStudioProcess.on('error', (error) => {
827
+ proc.on('error', (error) => {
755
828
  console.error('❌ Failed to start Drizzle Studio:', error.message)
756
829
  })
757
830
 
758
- drizzleStudioProcess.on('exit', (code) => {
831
+ proc.on('exit', (code) => {
759
832
  if (code !== 0 && code !== null) {
760
833
  console.error(`❌ Drizzle Studio exited with code ${code}`)
761
834
  }
835
+ // Clear global state if it matches this process
836
+ const s = getGlobalState()
837
+ if (s.drizzleProcess === proc) {
838
+ s.drizzleProcess = null
839
+ }
762
840
  })
763
841
 
764
842
  console.log(`✅ Drizzle Studio: https://local.drizzle.studio`)
@@ -772,10 +850,17 @@ function startDrizzleStudio(port: number): boolean {
772
850
  /**
773
851
  * Stop Drizzle Studio
774
852
  */
775
- function stopDrizzleStudio(): void {
776
- if (drizzleStudioProcess) {
777
- drizzleStudioProcess.kill()
778
- drizzleStudioProcess = null
779
- console.log('🛑 Drizzle Studio stopped')
853
+ function stopDrizzleStudio(options?: { log?: boolean }): boolean {
854
+ const state = getGlobalState()
855
+ if (state.drizzleProcess) {
856
+ // Remove all event listeners to prevent memory leaks
857
+ state.drizzleProcess.removeAllListeners()
858
+ state.drizzleProcess.kill()
859
+ state.drizzleProcess = null
860
+ if (options?.log !== false) {
861
+ console.log('🛑 Drizzle Studio stopped')
862
+ }
863
+ return true
780
864
  }
865
+ return false
781
866
  }