@getvision/server 0.2.7 → 0.3.0

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 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.2.7",
3
+ "version": "0.3.0",
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
@@ -203,6 +215,7 @@ export class EventBus {
203
215
  },
204
216
  {
205
217
  connection,
218
+ concurrency: options?.concurrency ?? this.config.workerConcurrency ?? 1,
206
219
  }
207
220
  )
208
221
 
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
- for (const name of entries) {
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?: { hostname?: string }) {
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)
@@ -638,9 +654,10 @@ export class Vision<
638
654
  }
639
655
  } catch {}
640
656
  this.bunServer = BunServe({
657
+ ...bunRest,
641
658
  fetch: this.fetch.bind(this),
642
659
  port,
643
- hostname: options?.hostname
660
+ hostname
644
661
  })
645
662
  try { (globalThis as any).__vision_bun_server = this.bunServer } catch {}
646
663
  } else {
@@ -651,9 +668,10 @@ export class Vision<
651
668
  const { serve } = await import('@hono/node-server')
652
669
  console.log(`Node.js detected`)
653
670
  serve({
671
+ ...nodeRest,
654
672
  fetch: this.fetch.bind(this),
655
673
  port,
656
- hostname: options?.hostname
674
+ hostname
657
675
  })
658
676
  } else {
659
677
  // For other runtimes, just return the app