@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.
- package/package.json +1 -1
- package/src/vision-app.ts +111 -44
package/package.json
CHANGED
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
|
|
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
|
-
//
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
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 {
|
|
636
|
-
}
|
|
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
|
-
|
|
651
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
827
|
+
proc.on('error', (error) => {
|
|
771
828
|
console.error('❌ Failed to start Drizzle Studio:', error.message)
|
|
772
829
|
})
|
|
773
830
|
|
|
774
|
-
|
|
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():
|
|
792
|
-
|
|
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
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
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
|
}
|