@getvision/server 0.2.7 → 0.3.1-3b055ce-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/CHANGELOG.md +11 -0
- package/package.json +2 -1
- package/src/event-bus.ts +34 -3
- package/src/router.ts +14 -1
- package/src/service.ts +8 -1
- package/src/vision-app.ts +39 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# @getvision/server
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 28c86e6: Added support for runtime-specific `start` options. Introduced new `VisionStartOptions` type for better handling of configuration parameters.
|
|
8
|
+
Added support for configurable worker concurrency per handler and default event bus settings
|
|
9
|
+
|
|
10
|
+
### Patch Changes
|
|
11
|
+
|
|
12
|
+
- 2d4e753: Fix priority for dynamic and static routes
|
|
13
|
+
|
|
3
14
|
## 0.2.7
|
|
4
15
|
|
|
5
16
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getvision/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1-3b055ce-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": {
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"@repo/eslint-config": "0.0.1",
|
|
27
27
|
"@repo/typescript-config": "0.0.1",
|
|
28
28
|
"@types/node": "^20.14.9",
|
|
29
|
+
"@types/bun": "^1.3.5",
|
|
29
30
|
"typescript": "5.9.3"
|
|
30
31
|
},
|
|
31
32
|
"keywords": [
|
package/src/event-bus.ts
CHANGED
|
@@ -11,6 +11,10 @@ export interface EventBusConfig {
|
|
|
11
11
|
port?: number
|
|
12
12
|
password?: string
|
|
13
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* Default BullMQ worker concurrency (per event). Per-handler options override this.
|
|
16
|
+
*/
|
|
17
|
+
workerConcurrency?: number
|
|
14
18
|
// Dev mode - use in-memory (no Redis required)
|
|
15
19
|
devMode?: boolean
|
|
16
20
|
}
|
|
@@ -72,6 +76,7 @@ export class EventBus {
|
|
|
72
76
|
this.config = {
|
|
73
77
|
devMode: resolvedDevMode,
|
|
74
78
|
redis: mergedRedis,
|
|
79
|
+
workerConcurrency: config.workerConcurrency,
|
|
75
80
|
}
|
|
76
81
|
}
|
|
77
82
|
|
|
@@ -178,7 +183,14 @@ export class EventBus {
|
|
|
178
183
|
*/
|
|
179
184
|
registerHandler<T>(
|
|
180
185
|
eventName: string,
|
|
181
|
-
handler: (data: T) => Promise<void
|
|
186
|
+
handler: (data: T) => Promise<void>,
|
|
187
|
+
options?: {
|
|
188
|
+
/**
|
|
189
|
+
* Max number of concurrent jobs this handler will process.
|
|
190
|
+
* Defaults to config.workerConcurrency (or 1).
|
|
191
|
+
*/
|
|
192
|
+
concurrency?: number
|
|
193
|
+
}
|
|
182
194
|
): void {
|
|
183
195
|
if (this.config.devMode) {
|
|
184
196
|
// Dev mode - store handlers in memory
|
|
@@ -191,6 +203,15 @@ export class EventBus {
|
|
|
191
203
|
host: 'localhost',
|
|
192
204
|
port: 6379,
|
|
193
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
|
+
|
|
194
215
|
const worker = new Worker(
|
|
195
216
|
eventName,
|
|
196
217
|
async (job) => {
|
|
@@ -203,10 +224,11 @@ export class EventBus {
|
|
|
203
224
|
},
|
|
204
225
|
{
|
|
205
226
|
connection,
|
|
227
|
+
concurrency: options?.concurrency ?? this.config.workerConcurrency ?? 1,
|
|
206
228
|
}
|
|
207
229
|
)
|
|
208
230
|
|
|
209
|
-
this.workers.set(
|
|
231
|
+
this.workers.set(workerKey, worker)
|
|
210
232
|
|
|
211
233
|
// Listen to queue events
|
|
212
234
|
if (!this.queueEvents.has(eventName)) {
|
|
@@ -253,6 +275,15 @@ export class EventBus {
|
|
|
253
275
|
host: 'localhost',
|
|
254
276
|
port: 6379,
|
|
255
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
|
+
|
|
256
287
|
const worker = new Worker(
|
|
257
288
|
cronName,
|
|
258
289
|
async (job) => {
|
|
@@ -274,7 +305,7 @@ export class EventBus {
|
|
|
274
305
|
}
|
|
275
306
|
)
|
|
276
307
|
|
|
277
|
-
this.workers.set(
|
|
308
|
+
this.workers.set(cronWorkerKey, worker)
|
|
278
309
|
|
|
279
310
|
// Listen to cron job events
|
|
280
311
|
if (!this.queueEvents.has(cronName)) {
|
package/src/router.ts
CHANGED
|
@@ -27,6 +27,10 @@ export async function loadSubApps(app: Hono, routesDir: string = './app/routes',
|
|
|
27
27
|
return '/' + segments.join('/')
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
function isDynamicSegment(name: string): boolean {
|
|
31
|
+
return name.startsWith('[') && name.endsWith(']')
|
|
32
|
+
}
|
|
33
|
+
|
|
30
34
|
async function scan(dir: string) {
|
|
31
35
|
const entries = readdirSync(dir)
|
|
32
36
|
// If folder contains index.ts or index.js, treat it as a sub-app root
|
|
@@ -73,7 +77,16 @@ export async function loadSubApps(app: Hono, routesDir: string = './app/routes',
|
|
|
73
77
|
}
|
|
74
78
|
}
|
|
75
79
|
// Recurse into child directories
|
|
76
|
-
|
|
80
|
+
// Sort entries: static folders first, then dynamic [param] folders
|
|
81
|
+
// This ensures static routes have priority over dynamic routes
|
|
82
|
+
const sortedEntries = [...entries].sort((a, b) => {
|
|
83
|
+
const aIsDynamic = isDynamicSegment(a)
|
|
84
|
+
const bIsDynamic = isDynamicSegment(b)
|
|
85
|
+
if (aIsDynamic && !bIsDynamic) return 1 // dynamic after static
|
|
86
|
+
if (!aIsDynamic && bIsDynamic) return -1 // static before dynamic
|
|
87
|
+
return a.localeCompare(b) // alphabetical within same type
|
|
88
|
+
})
|
|
89
|
+
for (const name of sortedEntries) {
|
|
77
90
|
const full = join(dir, name)
|
|
78
91
|
const st = statSync(full)
|
|
79
92
|
if (st.isDirectory()) await scan(full)
|
package/src/service.ts
CHANGED
|
@@ -240,6 +240,11 @@ export class ServiceBuilder<
|
|
|
240
240
|
description?: string
|
|
241
241
|
icon?: string
|
|
242
242
|
tags?: string[]
|
|
243
|
+
/**
|
|
244
|
+
* Max number of concurrent jobs this handler will process.
|
|
245
|
+
* Falls back to EventBus config.workerConcurrency (or 1).
|
|
246
|
+
*/
|
|
247
|
+
concurrency?: number
|
|
243
248
|
handler: (event: T, c: Context<E, any, I>) => Promise<void>
|
|
244
249
|
}
|
|
245
250
|
): ServiceBuilder<TEvents & { [key in K]: T }, E, I> {
|
|
@@ -268,7 +273,9 @@ export class ServiceBuilder<
|
|
|
268
273
|
)
|
|
269
274
|
|
|
270
275
|
// Register wrapped handler in event bus
|
|
271
|
-
this.eventBus.registerHandler(eventName, wrappedHandler
|
|
276
|
+
this.eventBus.registerHandler(eventName, wrappedHandler, {
|
|
277
|
+
concurrency: config.concurrency,
|
|
278
|
+
})
|
|
272
279
|
|
|
273
280
|
// Store for later reference
|
|
274
281
|
this.eventHandlers.set(eventName, config)
|
package/src/vision-app.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { spawn, spawnSync, type ChildProcess } from 'child_process'
|
|
|
8
8
|
import { ServiceBuilder } from './service'
|
|
9
9
|
import { EventBus } from './event-bus'
|
|
10
10
|
import { eventRegistry } from './event-registry'
|
|
11
|
+
import type { serve as honoServe } from '@hono/node-server'
|
|
11
12
|
|
|
12
13
|
export interface VisionALSContext {
|
|
13
14
|
vision: VisionCore
|
|
@@ -17,6 +18,12 @@ export interface VisionALSContext {
|
|
|
17
18
|
|
|
18
19
|
const visionContext = new AsyncLocalStorage<VisionALSContext>()
|
|
19
20
|
|
|
21
|
+
type BunServeOptions = Parameters<typeof Bun['serve']>[0]
|
|
22
|
+
type NodeServeOptions = Parameters<typeof honoServe>[0]
|
|
23
|
+
|
|
24
|
+
type VisionStartOptions = Omit<Partial<BunServeOptions>, 'fetch' | 'port'> &
|
|
25
|
+
Omit<Partial<NodeServeOptions>, 'fetch' | 'port'>
|
|
26
|
+
|
|
20
27
|
// Simple deep merge utility (objects only, arrays are overwritten by source)
|
|
21
28
|
function deepMerge<T extends Record<string, any>>(target: T, source: Partial<T>): T {
|
|
22
29
|
const output: any = { ...target }
|
|
@@ -75,6 +82,10 @@ export interface VisionConfig {
|
|
|
75
82
|
}
|
|
76
83
|
devMode?: boolean // Use in-memory event bus (no Redis required)
|
|
77
84
|
eventBus?: EventBus // Share EventBus instance across apps (for sub-apps)
|
|
85
|
+
/**
|
|
86
|
+
* Default BullMQ worker concurrency for all handlers (overridable per handler)
|
|
87
|
+
*/
|
|
88
|
+
workerConcurrency?: number
|
|
78
89
|
}
|
|
79
90
|
}
|
|
80
91
|
|
|
@@ -203,6 +214,7 @@ export class Vision<
|
|
|
203
214
|
this.eventBus = this.config.pubsub?.eventBus || new EventBus({
|
|
204
215
|
redis: this.config.pubsub?.redis,
|
|
205
216
|
devMode: this.config.pubsub?.devMode,
|
|
217
|
+
workerConcurrency: this.config.pubsub?.workerConcurrency,
|
|
206
218
|
})
|
|
207
219
|
|
|
208
220
|
// Register JSON-RPC methods for events/cron
|
|
@@ -567,7 +579,11 @@ export class Vision<
|
|
|
567
579
|
/**
|
|
568
580
|
* Start the server (convenience method)
|
|
569
581
|
*/
|
|
570
|
-
async start(port: number = 3000, options?:
|
|
582
|
+
async start(port: number = 3000, options?: VisionStartOptions) {
|
|
583
|
+
const { hostname, ...restOptions } = options || {}
|
|
584
|
+
const { fetch: _bf, port: _bp, ...bunRest } = restOptions as Partial<BunServeOptions>
|
|
585
|
+
const { fetch: _nf, port: _np, ...nodeRest } = restOptions as Partial<NodeServeOptions>
|
|
586
|
+
|
|
571
587
|
// Build all services WITHOUT registering to VisionCore yet
|
|
572
588
|
const rootSummaries = this.buildAllServices()
|
|
573
589
|
// Autoload file-based Vision/Hono sub-apps if enabled (returns merged sub-app summaries)
|
|
@@ -608,6 +624,10 @@ export class Vision<
|
|
|
608
624
|
if (this.shuttingDown) return
|
|
609
625
|
this.shuttingDown = true
|
|
610
626
|
console.log('🛑 Shutting down...')
|
|
627
|
+
try {
|
|
628
|
+
// Clear AsyncLocalStorage to prevent memory leaks
|
|
629
|
+
visionContext.disable()
|
|
630
|
+
} catch {}
|
|
611
631
|
try {
|
|
612
632
|
if (this.bunServer && typeof this.bunServer.stop === 'function') {
|
|
613
633
|
try { this.bunServer.stop() } catch {}
|
|
@@ -616,6 +636,7 @@ export class Vision<
|
|
|
616
636
|
} catch {}
|
|
617
637
|
try { stopDrizzleStudio() } catch {}
|
|
618
638
|
try { await this.eventBus.close() } catch {}
|
|
639
|
+
try { eventRegistry.clear() } catch {}
|
|
619
640
|
}
|
|
620
641
|
|
|
621
642
|
const wrappedCleanup = async () => {
|
|
@@ -625,6 +646,17 @@ export class Vision<
|
|
|
625
646
|
process.once('SIGINT', wrappedCleanup)
|
|
626
647
|
process.once('SIGTERM', wrappedCleanup)
|
|
627
648
|
try { process.once('SIGQUIT', wrappedCleanup) } catch {}
|
|
649
|
+
|
|
650
|
+
// Bun hot-reload: ensure resources are released between reloads.
|
|
651
|
+
// Note: dispose must NOT call process.exit().
|
|
652
|
+
try {
|
|
653
|
+
const hot = (import.meta as any)?.hot
|
|
654
|
+
if (hot && typeof hot.dispose === 'function') {
|
|
655
|
+
hot.dispose(() => {
|
|
656
|
+
void cleanup()
|
|
657
|
+
})
|
|
658
|
+
}
|
|
659
|
+
} catch {}
|
|
628
660
|
|
|
629
661
|
// Prefer Bun if available, then Node.js; otherwise instruct the user to serve manually
|
|
630
662
|
if (typeof process !== 'undefined' && process.versions?.bun) {
|
|
@@ -638,9 +670,10 @@ export class Vision<
|
|
|
638
670
|
}
|
|
639
671
|
} catch {}
|
|
640
672
|
this.bunServer = BunServe({
|
|
673
|
+
...bunRest,
|
|
641
674
|
fetch: this.fetch.bind(this),
|
|
642
675
|
port,
|
|
643
|
-
hostname
|
|
676
|
+
hostname
|
|
644
677
|
})
|
|
645
678
|
try { (globalThis as any).__vision_bun_server = this.bunServer } catch {}
|
|
646
679
|
} else {
|
|
@@ -651,9 +684,10 @@ export class Vision<
|
|
|
651
684
|
const { serve } = await import('@hono/node-server')
|
|
652
685
|
console.log(`Node.js detected`)
|
|
653
686
|
serve({
|
|
687
|
+
...nodeRest,
|
|
654
688
|
fetch: this.fetch.bind(this),
|
|
655
689
|
port,
|
|
656
|
-
hostname
|
|
690
|
+
hostname
|
|
657
691
|
})
|
|
658
692
|
} else {
|
|
659
693
|
// For other runtimes, just return the app
|
|
@@ -756,6 +790,8 @@ function startDrizzleStudio(port: number): boolean {
|
|
|
756
790
|
*/
|
|
757
791
|
function stopDrizzleStudio(): void {
|
|
758
792
|
if (drizzleStudioProcess) {
|
|
793
|
+
// Remove all event listeners to prevent memory leaks
|
|
794
|
+
drizzleStudioProcess.removeAllListeners()
|
|
759
795
|
drizzleStudioProcess.kill()
|
|
760
796
|
drizzleStudioProcess = null
|
|
761
797
|
console.log('🛑 Drizzle Studio stopped')
|