@getvision/server 0.2.0-b49d8db-develop → 0.2.1-648a711-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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # @getvision/server
2
2
 
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - b49d8db: Added per-endpoint rate limiting with hono-rate-limiter: configure ratelimit in EndpointConfig with requests, window, and optional store for distributed caching. Includes docs and example.
8
+
3
9
  ## 0.1.0
4
10
 
5
11
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getvision/server",
3
- "version": "0.2.0-b49d8db-develop",
3
+ "version": "0.2.1-648a711-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/event-bus.ts CHANGED
@@ -26,12 +26,52 @@ export class EventBus {
26
26
  private devModeHandlers = new Map<string, Array<(data: any) => Promise<void>>>()
27
27
 
28
28
  constructor(config: EventBusConfig = {}) {
29
- this.config = {
30
- devMode: config.devMode ?? process.env.NODE_ENV === 'development',
31
- redis: config.redis ?? {
29
+ // Build Redis config from environment variables
30
+ const envUrl = process.env.REDIS_URL
31
+ let envRedis: { host?: string; port?: number; password?: string } | undefined
32
+ if (envUrl) {
33
+ try {
34
+ const u = new URL(envUrl)
35
+ envRedis = {
36
+ host: u.hostname || undefined,
37
+ port: u.port ? parseInt(u.port) : 6379,
38
+ // URL password takes precedence over REDIS_PASSWORD
39
+ password: u.password || process.env.REDIS_PASSWORD || undefined,
40
+ }
41
+ } catch {
42
+ // Fallback to individual env vars if URL is invalid
43
+ envRedis = undefined
44
+ }
45
+ }
46
+
47
+ if (!envRedis) {
48
+ envRedis = {
32
49
  host: process.env.REDIS_HOST || 'localhost',
33
50
  port: parseInt(process.env.REDIS_PORT || '6379'),
34
- },
51
+ password: process.env.REDIS_PASSWORD || undefined,
52
+ }
53
+ }
54
+
55
+ // Merge: explicit config.redis overrides env-derived values
56
+ const mergedRedis = { ...(envRedis || {}), ...(config.redis || {}) }
57
+
58
+ // Determine if Redis is configured by env or explicit config
59
+ const hasRedisFromEnv = Boolean(envUrl)
60
+ const hasRedisFromConfig = Boolean(
61
+ config.redis && (config.redis.host || config.redis.port || config.redis.password)
62
+ )
63
+ const hasRedis = hasRedisFromEnv || hasRedisFromConfig
64
+
65
+ // devMode precedence:
66
+ // 1) Respect explicit config.devMode when provided (true/false)
67
+ // 2) Otherwise, if Redis is configured (env or config), use production mode (devMode=false)
68
+ // 3) Otherwise, default to devMode=true (in-memory)
69
+ const resolvedDevMode =
70
+ typeof config.devMode === 'boolean' ? config.devMode : !hasRedis
71
+
72
+ this.config = {
73
+ devMode: resolvedDevMode,
74
+ redis: mergedRedis,
35
75
  }
36
76
  }
37
77
 
package/src/service.ts CHANGED
@@ -32,6 +32,44 @@ function getClientKey(c: Context, method: string, path: string): string {
32
32
  return `${ip || ua}:${method}:${path}`
33
33
  }
34
34
 
35
+ /**
36
+ * Create a minimal Hono-like Context for event handlers
37
+ * so service-level middleware can populate c.set(...)
38
+ */
39
+ function createEventContext<E extends Env = any, I extends Input = {}>(): Context<E, any, I> {
40
+ const store: Record<string, any> = {}
41
+ const fake: Partial<Context<E, any, I>> = {
42
+ get: (key: string) => store[key],
43
+ set: (key: string, value: any) => { store[key] = value },
44
+ req: {
45
+ header: () => undefined,
46
+ param: () => ({}),
47
+ query: () => ({}),
48
+ json: async () => ({}),
49
+ raw: {} as any,
50
+ } as any,
51
+ }
52
+ return fake as Context<E, any, I>
53
+ }
54
+
55
+ /**
56
+ * Run Hono middleware chain on a context
57
+ */
58
+ async function runMiddlewareChain<E extends Env, I extends Input>(
59
+ middlewares: MiddlewareHandler<E, string, any, any>[],
60
+ c: Context<E, any, I>
61
+ ): Promise<void> {
62
+ let index = -1
63
+ const dispatch = async (i: number): Promise<void> => {
64
+ if (i <= index) throw new Error('next() called multiple times')
65
+ index = i
66
+ const mw = middlewares[i]
67
+ if (!mw) return
68
+ await mw(c, () => dispatch(i + 1))
69
+ }
70
+ await dispatch(0)
71
+ }
72
+
35
73
  /**
36
74
  * Event schema map - accumulates event types as they're registered
37
75
  */
@@ -202,7 +240,7 @@ export class ServiceBuilder<
202
240
  description?: string
203
241
  icon?: string
204
242
  tags?: string[]
205
- handler: (event: T) => Promise<void>
243
+ handler: (event: T, c: Context<E, any, I>) => Promise<void>
206
244
  }
207
245
  ): ServiceBuilder<TEvents & { [key in K]: T }, E, I> {
208
246
  const { schema, handler, description, icon, tags } = config
@@ -210,16 +248,27 @@ export class ServiceBuilder<
210
248
  // Store schema for type inference
211
249
  this.eventSchemas[eventName] = schema
212
250
 
213
- // Register in event registry
251
+ // Wrap handler to provide Hono-like context with middleware support
252
+ const wrappedHandler = async (data: T) => {
253
+ const ctx = createEventContext<E, I>()
254
+ // Run service-level middleware to populate ctx (e.g., c.set('db', db))
255
+ if (this.globalMiddleware.length > 0) {
256
+ await runMiddlewareChain(this.globalMiddleware, ctx)
257
+ }
258
+ // Call original handler with event data and context
259
+ await handler(data, ctx)
260
+ }
261
+
262
+ // Register wrapped handler in event registry (for metadata/stats)
214
263
  eventRegistry.registerEvent(
215
264
  eventName,
216
265
  schema,
217
- handler,
266
+ wrappedHandler,
218
267
  { description, icon, tags }
219
268
  )
220
269
 
221
- // Register handler in event bus
222
- this.eventBus.registerHandler(eventName, handler)
270
+ // Register wrapped handler in event bus
271
+ this.eventBus.registerHandler(eventName, wrappedHandler)
223
272
 
224
273
  // Store for later reference
225
274
  this.eventHandlers.set(eventName, config)
package/src/vision-app.ts CHANGED
@@ -17,6 +17,30 @@ export interface VisionALSContext {
17
17
 
18
18
  const visionContext = new AsyncLocalStorage<VisionALSContext>()
19
19
 
20
+ // Simple deep merge utility (objects only, arrays are overwritten by source)
21
+ function deepMerge<T extends Record<string, any>>(target: T, source: Partial<T>): T {
22
+ const output: any = { ...target }
23
+ if (source && typeof source === 'object') {
24
+ for (const key of Object.keys(source)) {
25
+ const srcVal = source[key]
26
+ const tgtVal = output[key]
27
+ if (
28
+ srcVal &&
29
+ typeof srcVal === 'object' &&
30
+ !Array.isArray(srcVal) &&
31
+ tgtVal &&
32
+ typeof tgtVal === 'object' &&
33
+ !Array.isArray(tgtVal)
34
+ ) {
35
+ output[key] = deepMerge(tgtVal, srcVal)
36
+ } else {
37
+ output[key] = srcVal
38
+ }
39
+ }
40
+ }
41
+ return output as T
42
+ }
43
+
20
44
  /**
21
45
  * Vision Server configuration
22
46
  */
@@ -61,20 +85,13 @@ export interface VisionConfig {
61
85
  * service: {
62
86
  * name: 'My API',
63
87
  * version: '1.0.0'
64
- * },
65
- * pubsub: {
66
- * schemas: {
67
- * 'user/created': {
68
- * data: z.object({ userId: z.string() })
69
- * }
70
- * }
71
88
  * }
72
89
  * })
73
90
  *
74
91
  * const userService = app.service('users')
75
- * .endpoint('GET', '/users/:id', schema, handler)
76
92
  * .on('user/created', handler)
77
- *
93
+ * .endpoint('GET', '/users/:id', schema, handler)
94
+ *
78
95
  * app.start(3000)
79
96
  * ```
80
97
  */
@@ -100,24 +117,16 @@ export class Vision<
100
117
  enabled: false,
101
118
  port: 9500,
102
119
  },
103
- pubsub: {
104
- devMode: true,
105
- },
120
+ // Do not set a default devMode here; let EventBus derive from Redis presence
121
+ pubsub: {},
106
122
  routes: {
107
123
  autodiscover: true,
108
124
  dirs: ['app/routes'],
109
125
  },
110
126
  }
111
127
 
112
- // Merge shallowly (good enough for our config structure)
113
- this.config = {
114
- ...defaultConfig,
115
- ...(config || {}),
116
- service: { ...defaultConfig.service, ...(config?.service || {}) },
117
- vision: { ...defaultConfig.vision, ...(config?.vision || {}) },
118
- pubsub: { ...defaultConfig.pubsub, ...(config?.pubsub || {}) },
119
- routes: { ...defaultConfig.routes, ...(config?.routes || {}) },
120
- }
128
+ // Deep merge to respect nested overrides
129
+ this.config = deepMerge(defaultConfig, config || {})
121
130
 
122
131
  // Initialize Vision Core
123
132
  const visionEnabled = this.config.vision?.enabled !== false