@getvision/server 0.3.1-3b055ce-develop → 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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/vision-app.ts +111 -44
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getvision/server",
3
- "version": "0.3.1-3b055ce-develop",
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/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,44 +662,55 @@ 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
- // Clear AsyncLocalStorage to prevent memory leaks
629
- visionContext.disable()
630
- } catch {}
631
- try {
632
- if (this.bunServer && typeof this.bunServer.stop === 'function') {
633
- 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)
634
681
  }
635
- try { if ((globalThis as any).__vision_bun_server === this.bunServer) (globalThis as any).__vision_bun_server = undefined } catch {}
636
- } catch {}
637
- try { stopDrizzleStudio() } catch {}
638
- try { await this.eventBus.close() } catch {}
639
- try { eventRegistry.clear() } catch {}
640
- }
641
-
642
- const wrappedCleanup = async () => {
643
- await cleanup()
644
- try { process.exit(0) } catch {}
682
+ try { process.exit(0) } catch {}
683
+ }
645
684
  }
646
- process.once('SIGINT', wrappedCleanup)
647
- process.once('SIGTERM', wrappedCleanup)
648
- try { process.once('SIGQUIT', wrappedCleanup) } catch {}
649
685
 
650
- // Bun hot-reload: ensure resources are released between reloads.
651
- // Note: dispose must NOT call process.exit().
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
652
697
  try {
653
698
  const hot = (import.meta as any)?.hot
654
699
  if (hot && typeof hot.dispose === 'function') {
655
- hot.dispose(() => {
656
- void cleanup()
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
+ }
657
714
  })
658
715
  }
659
716
  } catch {}
@@ -662,7 +719,6 @@ export class Vision<
662
719
  if (typeof process !== 'undefined' && process.versions?.bun) {
663
720
  const BunServe = (globalThis as any).Bun?.serve
664
721
  if (typeof BunServe === 'function') {
665
- console.log(`Bun detected`)
666
722
  try {
667
723
  const existing = (globalThis as any).__vision_bun_server
668
724
  if (existing && typeof existing.stop === 'function') {
@@ -682,7 +738,6 @@ export class Vision<
682
738
  }
683
739
  } else if (typeof process !== 'undefined' && process.versions?.node) {
684
740
  const { serve } = await import('@hono/node-server')
685
- console.log(`Node.js detected`)
686
741
  serve({
687
742
  ...nodeRest,
688
743
  fetch: this.fetch.bind(this),
@@ -715,8 +770,6 @@ export function getVisionContext(): VisionALSContext | undefined {
715
770
  // Drizzle Studio Integration
716
771
  // ============================================================================
717
772
 
718
- let drizzleStudioProcess: ChildProcess | null = null
719
-
720
773
  /**
721
774
  * Detect Drizzle configuration
722
775
  */
@@ -739,7 +792,8 @@ function detectDrizzle(): { detected: boolean; configPath?: string } {
739
792
  * Start Drizzle Studio
740
793
  */
741
794
  function startDrizzleStudio(port: number): boolean {
742
- if (drizzleStudioProcess) {
795
+ const state = getGlobalState()
796
+ if (state.drizzleProcess) {
743
797
  console.log('⚠️ Drizzle Studio is already running')
744
798
  return false
745
799
  }
@@ -762,19 +816,27 @@ function startDrizzleStudio(port: number): boolean {
762
816
  } catch {}
763
817
 
764
818
  try {
765
- 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'], {
766
820
  stdio: 'inherit',
767
821
  detached: false,
822
+ shell: process.platform === 'win32',
768
823
  })
824
+
825
+ state.drizzleProcess = proc
769
826
 
770
- drizzleStudioProcess.on('error', (error) => {
827
+ proc.on('error', (error) => {
771
828
  console.error('❌ Failed to start Drizzle Studio:', error.message)
772
829
  })
773
830
 
774
- drizzleStudioProcess.on('exit', (code) => {
831
+ proc.on('exit', (code) => {
775
832
  if (code !== 0 && code !== null) {
776
833
  console.error(`❌ Drizzle Studio exited with code ${code}`)
777
834
  }
835
+ // Clear global state if it matches this process
836
+ const s = getGlobalState()
837
+ if (s.drizzleProcess === proc) {
838
+ s.drizzleProcess = null
839
+ }
778
840
  })
779
841
 
780
842
  console.log(`✅ Drizzle Studio: https://local.drizzle.studio`)
@@ -788,12 +850,17 @@ function startDrizzleStudio(port: number): boolean {
788
850
  /**
789
851
  * Stop Drizzle Studio
790
852
  */
791
- function stopDrizzleStudio(): void {
792
- if (drizzleStudioProcess) {
853
+ function stopDrizzleStudio(options?: { log?: boolean }): boolean {
854
+ const state = getGlobalState()
855
+ if (state.drizzleProcess) {
793
856
  // Remove all event listeners to prevent memory leaks
794
- drizzleStudioProcess.removeAllListeners()
795
- drizzleStudioProcess.kill()
796
- drizzleStudioProcess = null
797
- console.log('🛑 Drizzle Studio stopped')
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
798
864
  }
865
+ return false
799
866
  }