@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 +1 -1
- package/src/event-bus.ts +20 -2
- package/src/vision-app.ts +119 -34
package/package.json
CHANGED
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(
|
|
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(
|
|
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
|
|
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
|
-
//
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
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 {
|
|
632
|
-
}
|
|
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
|
-
|
|
642
|
-
|
|
643
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
827
|
+
proc.on('error', (error) => {
|
|
755
828
|
console.error('❌ Failed to start Drizzle Studio:', error.message)
|
|
756
829
|
})
|
|
757
830
|
|
|
758
|
-
|
|
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():
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
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
|
}
|