spiceflow 1.0.8 → 1.1.1

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.
Files changed (76) hide show
  1. package/README.md +23 -8
  2. package/dist/benchmark.test.d.ts +2 -0
  3. package/dist/benchmark.test.d.ts.map +1 -0
  4. package/dist/benchmark.test.js +8 -0
  5. package/dist/benchmark.test.js.map +1 -0
  6. package/dist/client/index.d.ts +1 -1
  7. package/dist/client/index.d.ts.map +1 -1
  8. package/dist/client/index.js.map +1 -1
  9. package/dist/client/types.d.ts +1 -1
  10. package/dist/client/types.d.ts.map +1 -1
  11. package/dist/client/ws.d.ts +1 -1
  12. package/dist/client/ws.d.ts.map +1 -1
  13. package/dist/client.test.js +1 -18
  14. package/dist/client.test.js.map +1 -1
  15. package/dist/{elysia-fork/context.d.ts → context.d.ts} +8 -7
  16. package/dist/context.d.ts.map +1 -0
  17. package/dist/{elysia-fork/context.js.map → context.js.map} +1 -1
  18. package/dist/error.d.ts.map +1 -0
  19. package/dist/error.js.map +1 -0
  20. package/dist/index.d.ts +1 -2
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +1 -1
  23. package/dist/index.js.map +1 -1
  24. package/dist/middleware.test.d.ts +2 -0
  25. package/dist/middleware.test.d.ts.map +1 -0
  26. package/dist/middleware.test.js +99 -0
  27. package/dist/middleware.test.js.map +1 -0
  28. package/dist/openapi.d.ts +4 -15
  29. package/dist/openapi.d.ts.map +1 -1
  30. package/dist/spiceflow.d.ts +41 -120
  31. package/dist/spiceflow.d.ts.map +1 -1
  32. package/dist/spiceflow.js +223 -169
  33. package/dist/spiceflow.js.map +1 -1
  34. package/dist/spiceflow.test.js +54 -16
  35. package/dist/spiceflow.test.js.map +1 -1
  36. package/dist/types.d.ts +407 -0
  37. package/dist/types.d.ts.map +1 -1
  38. package/dist/types.js +1 -1
  39. package/dist/types.js.map +1 -1
  40. package/dist/utils.d.ts +72 -0
  41. package/dist/utils.d.ts.map +1 -1
  42. package/dist/utils.js +69 -0
  43. package/dist/utils.js.map +1 -1
  44. package/package.json +1 -1
  45. package/src/benchmark.test.ts +8 -0
  46. package/src/client/index.ts +1 -3
  47. package/src/client/types.ts +6 -13
  48. package/src/client/ws.ts +1 -1
  49. package/src/client.test.ts +1 -19
  50. package/src/context.ts +128 -0
  51. package/src/index.ts +1 -2
  52. package/src/middleware.test.ts +107 -0
  53. package/src/openapi.ts +1 -1
  54. package/src/spiceflow.test.ts +74 -16
  55. package/src/spiceflow.ts +324 -391
  56. package/src/types.test.ts +1 -1
  57. package/src/types.ts +929 -0
  58. package/src/utils.ts +84 -0
  59. package/dist/elysia-fork/context.d.ts.map +0 -1
  60. package/dist/elysia-fork/error.d.ts.map +0 -1
  61. package/dist/elysia-fork/error.js.map +0 -1
  62. package/dist/elysia-fork/types.d.ts +0 -560
  63. package/dist/elysia-fork/types.d.ts.map +0 -1
  64. package/dist/elysia-fork/types.js +0 -2
  65. package/dist/elysia-fork/types.js.map +0 -1
  66. package/dist/elysia-fork/utils.d.ts +0 -73
  67. package/dist/elysia-fork/utils.d.ts.map +0 -1
  68. package/dist/elysia-fork/utils.js +0 -70
  69. package/dist/elysia-fork/utils.js.map +0 -1
  70. package/src/elysia-fork/context.ts +0 -166
  71. package/src/elysia-fork/types.ts +0 -1281
  72. package/src/elysia-fork/utils.ts +0 -85
  73. /package/dist/{elysia-fork/context.js → context.js} +0 -0
  74. /package/dist/{elysia-fork/error.d.ts → error.d.ts} +0 -0
  75. /package/dist/{elysia-fork/error.js → error.js} +0 -0
  76. /package/src/{elysia-fork/error.ts → error.ts} +0 -0
package/src/spiceflow.ts CHANGED
@@ -4,13 +4,12 @@ import { Type } from '@sinclair/typebox'
4
4
 
5
5
  export { Type as t }
6
6
 
7
+ import addFormats from 'ajv-formats'
7
8
  import {
8
9
  ComposeSpiceflowResponse,
9
10
  CreateEden,
10
11
  DefinitionBase,
11
- EphemeralType,
12
12
  ErrorHandler,
13
- Handler,
14
13
  HTTPMethod,
15
14
  InlineHandler,
16
15
  InputSchema,
@@ -20,8 +19,7 @@ import {
20
19
  MaybeArray,
21
20
  MergeSchema,
22
21
  MetadataBase,
23
- PreHandler,
24
- Prettify2,
22
+ MiddlewareHandler,
25
23
  Reconcile,
26
24
  ResolvePath,
27
25
  RouteBase,
@@ -29,21 +27,18 @@ import {
29
27
  SingletonBase,
30
28
  TypeSchema,
31
29
  UnwrapRoute,
32
- } from './elysia-fork/types.js'
33
- import addFormats from 'ajv-formats'
30
+ } from './types.js'
34
31
  let globalIndex = 0
35
32
 
36
33
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
37
34
  // @ts-ignore
38
35
  import OriginalRouter from '@medley/router'
39
- import { TSchema } from '@sinclair/typebox'
40
36
  import Ajv, { ValidateFunction } from 'ajv'
41
- import { Context } from './elysia-fork/context.js'
42
- import { isAsyncIterable } from './utils.js'
43
- import { redirect } from './elysia-fork/utils.js'
44
- import { ValidationError } from './elysia-fork/error.js'
45
- import { zodToJsonSchema } from 'zod-to-json-schema'
46
37
  import { z, ZodType } from 'zod'
38
+ import { zodToJsonSchema } from 'zod-to-json-schema'
39
+ import { Context, MiddlewareContext } from './context.js'
40
+ import { isProduction, NotFoundError, ValidationError } from './error.js'
41
+ import { isAsyncIterable, redirect } from './utils.js'
47
42
 
48
43
  const ajv = (addFormats.default || addFormats)(
49
44
  new (Ajv.default || Ajv)({ useDefaults: true }),
@@ -67,27 +62,10 @@ const ajv = (addFormats.default || addFormats)(
67
62
 
68
63
  // Should be exported from `hono/router`
69
64
 
70
- type P = any
71
-
72
65
  type AsyncResponse = Response | Promise<Response>
73
66
 
74
67
  type OnError = (x: { error: any; request: Request }) => AsyncResponse
75
68
 
76
- type RouterTree = {
77
- id: number
78
- router: OriginalRouter
79
- prefix?: string
80
- onRequestHandlers: Function[]
81
- onErrorHandlers: OnError[]
82
- children: RouterTree[]
83
- routes: InternalRoute[]
84
- // default store for the router, used as default for context.store
85
- store: Record<any, any>
86
- currentRoot?: RouterTree
87
- }
88
-
89
- type OnNoMatch = (request: Request, platform: P) => AsyncResponse
90
-
91
69
  export type InternalRoute = {
92
70
  method: HTTPMethod
93
71
  path: string
@@ -96,11 +74,20 @@ export type InternalRoute = {
96
74
  validateBody?: ValidateFunction
97
75
  validateQuery?: ValidateFunction
98
76
  validateParams?: ValidateFunction
99
-
100
77
  prefix: string
101
78
 
102
79
  // store: Record<any, any>
103
80
  }
81
+
82
+ type MedleyRouter = {
83
+ find: (path: string) =>
84
+ | {
85
+ store: Record<string, InternalRoute> //
86
+ params: Record<string, any>
87
+ }
88
+ | undefined
89
+ register: (path: string | undefined) => Record<string, InternalRoute>
90
+ }
104
91
  /**
105
92
  * Router class
106
93
  */
@@ -108,10 +95,7 @@ export class Spiceflow<
108
95
  const in out BasePath extends string = '',
109
96
  const in out Scoped extends boolean = true,
110
97
  const in out Singleton extends SingletonBase = {
111
- decorator: {}
112
98
  store: {}
113
- derive: {}
114
- resolve: {}
115
99
  },
116
100
  const in out Definitions extends DefinitionBase = {
117
101
  type: {}
@@ -123,30 +107,28 @@ export class Spiceflow<
123
107
  macroFn: {}
124
108
  },
125
109
  const out Routes extends RouteBase = {},
126
- // ? scoped
127
- const in out Ephemeral extends EphemeralType = {
128
- derive: {}
129
- resolve: {}
130
- schema: {}
131
- },
132
- // ? local
133
- const in out Volatile extends EphemeralType = {
134
- derive: {}
135
- resolve: {}
136
- schema: {}
137
- },
138
110
  > {
139
- private onNoMatch: OnNoMatch
140
- // prefix: BasePath | undefined
141
- private routerTree: RouterTree
111
+ private id: number = globalIndex++
112
+ private router: MedleyRouter = new OriginalRouter()
113
+ private middlewares: Function[] = []
114
+ private onErrorHandlers: OnError[] = []
115
+ private routes: InternalRoute[] = []
116
+ private defaultStore: Record<any, any> = {}
117
+ private topLevelApp?: AnySpiceflow
118
+
119
+ /** @internal */
120
+ prefix?: string
142
121
 
122
+ /** @internal */
123
+ childrenApps: AnySpiceflow[] = []
124
+
125
+ /** @internal */
143
126
  getAllRoutes() {
144
- let root = this.routerTree.currentRoot || this.routerTree
127
+ let root = this.topLevelApp || this
145
128
  const allApps = bfs(root) || []
146
129
  const allRoutes = allApps.flatMap((x) => {
147
- const prefix = this.getRouteAndParents(x)
130
+ const prefix = this.getAppAndParents(x)
148
131
  .map((x) => x.prefix)
149
- .reverse()
150
132
  .join('')
151
133
 
152
134
  return x.routes.map((x) => ({ ...x, path: prefix + x.path }))
@@ -161,73 +143,56 @@ export class Spiceflow<
161
143
  handler,
162
144
  ...rest
163
145
  }: Partial<InternalRoute>) {
164
- const router = this.routerTree
165
- // if (router.prefix) {
166
- // path = router.prefix + path
167
- // }
168
-
169
146
  let bodySchema: TypeSchema = hooks?.body
170
147
  let validateBody = getValidateFunction(bodySchema)
171
148
  let validateQuery = getValidateFunction(hooks?.query)
172
149
  let validateParams = getValidateFunction(hooks?.params)
173
150
 
174
- const store = router.router.register(path)
151
+ const store = this.router.register(path)
175
152
  let route: InternalRoute = {
176
153
  ...rest,
177
-
178
- prefix: router.prefix || '',
154
+ prefix: this.prefix || '',
179
155
  method: (method || '') as any,
180
156
  path: path || '',
181
- // prefix,
182
157
  handler: handler!,
183
158
  hooks,
184
159
  validateBody,
185
160
  validateParams,
186
161
  validateQuery,
187
162
  }
188
- router.routes.push(route)
189
- store[method] = route
163
+ this.routes.push(route)
164
+ store[method!] = route
190
165
  }
191
166
 
192
167
  private match(method: string, path: string) {
193
- let root = this.routerTree
194
- const result = bfsFind(this.routerTree, (router) => {
195
- router.currentRoot = root
196
- let prefix = this.getRouteAndParents(router)
168
+ let root = this
169
+ const result = bfsFind(this, (app) => {
170
+ app.topLevelApp = root
171
+ let prefix = this.getAppAndParents(app)
197
172
  .map((x) => x.prefix)
198
- .reverse()
199
173
  .join('')
200
174
  if (prefix && !path.startsWith(prefix)) {
201
- // console.log(
202
- // `router prefix: ${router.prefix} does not match path: ${path}`
203
- // )
204
175
  return
205
176
  }
206
177
  let pathWithoutPrefix = path
207
178
  if (prefix) {
208
179
  pathWithoutPrefix = path.replace(prefix, '')
209
180
  }
210
- // console.log(`router prefix: ${router.prefix} matches path: ${path}`)
211
- const route = router.router.find(pathWithoutPrefix)
212
- if (!route) {
181
+ const medleyRoute = app.router.find(pathWithoutPrefix)
182
+ if (!medleyRoute) {
213
183
  return
214
184
  }
215
185
 
216
- let data: InternalRoute = route['store'][method]
217
- if (data) {
218
- // console.log(`route found: ${method} ${path}`, route)
219
-
220
- const { onErrorHandlers, onRequestHandlers } = router
221
- const params = route['params'] || {}
186
+ let internalRoute: InternalRoute = medleyRoute.store[method]
187
+ if (internalRoute) {
188
+ const params = medleyRoute.params || {}
222
189
 
223
- return {
224
- ...data,
225
- router,
226
- store: router.store,
227
- onErrorHandlers,
228
- onRequestHandlers,
190
+ const res = {
191
+ app,
192
+ internalRoute: internalRoute,
229
193
  params,
230
194
  }
195
+ return res
231
196
  }
232
197
  })
233
198
 
@@ -241,23 +206,18 @@ export class Spiceflow<
241
206
  BasePath,
242
207
  Scoped,
243
208
  {
244
- decorator: Singleton['decorator']
245
209
  store: Reconcile<
246
210
  Singleton['store'],
247
211
  {
248
212
  [name in Name]: Value
249
213
  }
250
214
  >
251
- derive: Singleton['derive']
252
- resolve: Singleton['resolve']
253
215
  },
254
216
  Definitions,
255
217
  Metadata,
256
- Routes,
257
- Ephemeral,
258
- Volatile
218
+ Routes
259
219
  > {
260
- this.routerTree.store[name] = value
220
+ this.defaultStore[name] = value
261
221
  return this as any
262
222
  }
263
223
 
@@ -269,31 +229,13 @@ export class Spiceflow<
269
229
  options: {
270
230
  name?: string
271
231
  scoped?: Scoped
272
- onNoMatch?: (request: Request, platform: P) => AsyncResponse
232
+
273
233
  basePath?: BasePath
274
234
  } = {},
275
235
  ) {
276
236
  this.scoped = options.scoped
277
237
 
278
- this.onNoMatch =
279
- options.onNoMatch ?? (() => new Response(null, { status: 404 }))
280
- this.routerTree = {
281
- id: globalIndex++,
282
- router: new OriginalRouter(),
283
- prefix: options.basePath,
284
- onRequestHandlers: [],
285
- onErrorHandlers: [],
286
- children: [],
287
- store: {},
288
- routes: [],
289
- }
290
-
291
- // Bind router methods
292
- // for (const method of METHODS) {
293
- // this.#routes.set(method as Method, [])
294
- // const key = method.toLowerCase() as Lowercase<Method>
295
- // this[key as any] = this.#add.bind(this, method)
296
- // }
238
+ this.prefix = options.basePath
297
239
  }
298
240
 
299
241
  _routes: Routes = {} as any
@@ -306,27 +248,15 @@ export class Spiceflow<
306
248
  Metadata: {} as Metadata,
307
249
  }
308
250
 
309
- _ephemeral = {} as Ephemeral
310
- _volatile = {} as Volatile
311
-
312
251
  post<
313
252
  const Path extends string,
314
253
  const LocalSchema extends InputSchema<
315
254
  keyof Definitions['type'] & string
316
255
  >,
317
- const Schema extends MergeSchema<
318
- UnwrapRoute<LocalSchema, Definitions['type']>,
319
- MergeSchema<
320
- Volatile['schema'],
321
- MergeSchema<Ephemeral['schema'], Metadata['schema']>
322
- >
323
- >,
256
+ const Schema extends UnwrapRoute<LocalSchema, Definitions['type']>,
324
257
  const Handle extends InlineHandler<
325
258
  Schema,
326
- Singleton & {
327
- derive: Ephemeral['derive'] & Volatile['derive']
328
- resolve: Ephemeral['resolve'] & Volatile['resolve']
329
- },
259
+ Singleton,
330
260
  JoinPath<BasePath, Path>
331
261
  >,
332
262
  >(
@@ -335,10 +265,7 @@ export class Spiceflow<
335
265
  hook?: LocalHook<
336
266
  LocalSchema,
337
267
  Schema,
338
- Singleton & {
339
- derive: Ephemeral['derive'] & Volatile['derive']
340
- resolve: Ephemeral['resolve'] & Volatile['resolve']
341
- },
268
+ Singleton,
342
269
  Definitions['error'],
343
270
  Metadata['macro'],
344
271
  JoinPath<BasePath, Path>
@@ -359,16 +286,13 @@ export class Spiceflow<
359
286
  ? ResolvePath<Path>
360
287
  : Schema['params']
361
288
  query: Schema['query']
362
-
363
289
  response: ComposeSpiceflowResponse<
364
290
  Schema['response'],
365
291
  Handle
366
292
  >
367
293
  }
368
294
  }
369
- >,
370
- Ephemeral,
371
- Volatile
295
+ >
372
296
  > {
373
297
  this.add({ method: 'POST', path, handler: handler, hooks: hook })
374
298
 
@@ -380,20 +304,11 @@ export class Spiceflow<
380
304
  const LocalSchema extends InputSchema<
381
305
  keyof Definitions['type'] & string
382
306
  >,
383
- const Schema extends MergeSchema<
384
- UnwrapRoute<LocalSchema, Definitions['type']>,
385
- MergeSchema<
386
- Volatile['schema'],
387
- MergeSchema<Ephemeral['schema'], Metadata['schema']>
388
- >
389
- >,
307
+ const Schema extends UnwrapRoute<LocalSchema, Definitions['type']>,
390
308
  const Macro extends Metadata['macro'],
391
309
  const Handle extends InlineHandler<
392
310
  Schema,
393
- Singleton & {
394
- derive: Ephemeral['derive'] & Volatile['derive']
395
- resolve: Ephemeral['resolve'] & Volatile['resolve']
396
- },
311
+ Singleton,
397
312
  JoinPath<BasePath, Path>
398
313
  >,
399
314
  >(
@@ -402,10 +317,7 @@ export class Spiceflow<
402
317
  hook?: LocalHook<
403
318
  LocalSchema,
404
319
  Schema,
405
- Singleton & {
406
- derive: Ephemeral['derive'] & Volatile['derive']
407
- resolve: Ephemeral['resolve'] & Volatile['resolve']
408
- },
320
+ Singleton,
409
321
  Definitions['error'],
410
322
  Macro,
411
323
  JoinPath<BasePath, Path>
@@ -433,9 +345,7 @@ export class Spiceflow<
433
345
  >
434
346
  }
435
347
  }
436
- >,
437
- Ephemeral,
438
- Volatile
348
+ >
439
349
  > {
440
350
  this.add({ method: 'GET', path, handler: handler, hooks: hook })
441
351
  return this as any
@@ -446,19 +356,10 @@ export class Spiceflow<
446
356
  const LocalSchema extends InputSchema<
447
357
  keyof Definitions['type'] & string
448
358
  >,
449
- const Schema extends MergeSchema<
450
- UnwrapRoute<LocalSchema, Definitions['type']>,
451
- MergeSchema<
452
- Volatile['schema'],
453
- MergeSchema<Ephemeral['schema'], Metadata['schema']>
454
- >
455
- >,
359
+ const Schema extends UnwrapRoute<LocalSchema, Definitions['type']>,
456
360
  const Handle extends InlineHandler<
457
361
  Schema,
458
- Singleton & {
459
- derive: Ephemeral['derive'] & Volatile['derive']
460
- resolve: Ephemeral['resolve'] & Volatile['resolve']
461
- },
362
+ Singleton,
462
363
  JoinPath<BasePath, Path>
463
364
  >,
464
365
  >(
@@ -467,10 +368,7 @@ export class Spiceflow<
467
368
  hook?: LocalHook<
468
369
  LocalSchema,
469
370
  Schema,
470
- Singleton & {
471
- derive: Ephemeral['derive'] & Volatile['derive']
472
- resolve: Ephemeral['resolve'] & Volatile['resolve']
473
- },
371
+ Singleton,
474
372
  Definitions['error'],
475
373
  Metadata['macro'],
476
374
  JoinPath<BasePath, Path>
@@ -498,9 +396,7 @@ export class Spiceflow<
498
396
  >
499
397
  }
500
398
  }
501
- >,
502
- Ephemeral,
503
- Volatile
399
+ >
504
400
  > {
505
401
  this.add({ method: 'PUT', path, handler: handler, hooks: hook })
506
402
 
@@ -512,19 +408,10 @@ export class Spiceflow<
512
408
  const LocalSchema extends InputSchema<
513
409
  keyof Definitions['type'] & string
514
410
  >,
515
- const Schema extends MergeSchema<
516
- UnwrapRoute<LocalSchema, Definitions['type']>,
517
- MergeSchema<
518
- Volatile['schema'],
519
- MergeSchema<Ephemeral['schema'], Metadata['schema']>
520
- >
521
- >,
411
+ const Schema extends UnwrapRoute<LocalSchema, Definitions['type']>,
522
412
  const Handle extends InlineHandler<
523
413
  Schema,
524
- Singleton & {
525
- derive: Ephemeral['derive'] & Volatile['derive']
526
- resolve: Ephemeral['resolve'] & Volatile['resolve']
527
- },
414
+ Singleton,
528
415
  JoinPath<BasePath, Path>
529
416
  >,
530
417
  >(
@@ -533,10 +420,7 @@ export class Spiceflow<
533
420
  hook?: LocalHook<
534
421
  LocalSchema,
535
422
  Schema,
536
- Singleton & {
537
- derive: Ephemeral['derive'] & Volatile['derive']
538
- resolve: Ephemeral['resolve'] & Volatile['resolve']
539
- },
423
+ Singleton,
540
424
  Definitions['error'],
541
425
  Metadata['macro'],
542
426
  JoinPath<BasePath, Path>
@@ -564,9 +448,7 @@ export class Spiceflow<
564
448
  >
565
449
  }
566
450
  }
567
- >,
568
- Ephemeral,
569
- Volatile
451
+ >
570
452
  > {
571
453
  this.add({ method: 'PATCH', path, handler: handler, hooks: hook })
572
454
 
@@ -578,19 +460,10 @@ export class Spiceflow<
578
460
  const LocalSchema extends InputSchema<
579
461
  keyof Definitions['type'] & string
580
462
  >,
581
- const Schema extends MergeSchema<
582
- UnwrapRoute<LocalSchema, Definitions['type']>,
583
- MergeSchema<
584
- Volatile['schema'],
585
- MergeSchema<Ephemeral['schema'], Metadata['schema']>
586
- >
587
- >,
463
+ const Schema extends UnwrapRoute<LocalSchema, Definitions['type']>,
588
464
  const Handle extends InlineHandler<
589
465
  Schema,
590
- Singleton & {
591
- derive: Ephemeral['derive'] & Volatile['derive']
592
- resolve: Ephemeral['resolve'] & Volatile['resolve']
593
- },
466
+ Singleton,
594
467
  JoinPath<BasePath, Path>
595
468
  >,
596
469
  >(
@@ -599,10 +472,7 @@ export class Spiceflow<
599
472
  hook?: LocalHook<
600
473
  LocalSchema,
601
474
  Schema,
602
- Singleton & {
603
- derive: Ephemeral['derive'] & Volatile['derive']
604
- resolve: Ephemeral['resolve'] & Volatile['resolve']
605
- },
475
+ Singleton,
606
476
  Definitions['error'],
607
477
  Metadata['macro'],
608
478
  JoinPath<BasePath, Path>
@@ -630,9 +500,7 @@ export class Spiceflow<
630
500
  >
631
501
  }
632
502
  }
633
- >,
634
- Ephemeral,
635
- Volatile
503
+ >
636
504
  > {
637
505
  this.add({ method: 'DELETE', path, handler: handler, hooks: hook })
638
506
 
@@ -644,19 +512,10 @@ export class Spiceflow<
644
512
  const LocalSchema extends InputSchema<
645
513
  keyof Definitions['type'] & string
646
514
  >,
647
- const Schema extends MergeSchema<
648
- UnwrapRoute<LocalSchema, Definitions['type']>,
649
- MergeSchema<
650
- Volatile['schema'],
651
- MergeSchema<Ephemeral['schema'], Metadata['schema']>
652
- >
653
- >,
515
+ const Schema extends UnwrapRoute<LocalSchema, Definitions['type']>,
654
516
  const Handle extends InlineHandler<
655
517
  Schema,
656
- Singleton & {
657
- derive: Ephemeral['derive'] & Volatile['derive']
658
- resolve: Ephemeral['resolve'] & Volatile['resolve']
659
- },
518
+ Singleton,
660
519
  JoinPath<BasePath, Path>
661
520
  >,
662
521
  >(
@@ -665,10 +524,7 @@ export class Spiceflow<
665
524
  hook?: LocalHook<
666
525
  LocalSchema,
667
526
  Schema,
668
- Singleton & {
669
- derive: Ephemeral['derive'] & Volatile['derive']
670
- resolve: Ephemeral['resolve'] & Volatile['resolve']
671
- },
527
+ Singleton,
672
528
  Definitions['error'],
673
529
  Metadata['macro'],
674
530
  JoinPath<BasePath, Path>
@@ -696,9 +552,7 @@ export class Spiceflow<
696
552
  >
697
553
  }
698
554
  }
699
- >,
700
- Ephemeral,
701
- Volatile
555
+ >
702
556
  > {
703
557
  this.add({ method: 'OPTIONS', path, handler: handler, hooks: hook })
704
558
 
@@ -710,19 +564,10 @@ export class Spiceflow<
710
564
  const LocalSchema extends InputSchema<
711
565
  keyof Definitions['type'] & string
712
566
  >,
713
- const Schema extends MergeSchema<
714
- UnwrapRoute<LocalSchema, Definitions['type']>,
715
- MergeSchema<
716
- Volatile['schema'],
717
- MergeSchema<Ephemeral['schema'], Metadata['schema']>
718
- >
719
- >,
567
+ const Schema extends UnwrapRoute<LocalSchema, Definitions['type']>,
720
568
  const Handle extends InlineHandler<
721
569
  Schema,
722
- Singleton & {
723
- derive: Ephemeral['derive'] & Volatile['derive']
724
- resolve: Ephemeral['resolve'] & Volatile['resolve']
725
- },
570
+ Singleton,
726
571
  JoinPath<BasePath, Path>
727
572
  >,
728
573
  >(
@@ -731,10 +576,7 @@ export class Spiceflow<
731
576
  hook?: LocalHook<
732
577
  LocalSchema,
733
578
  Schema,
734
- Singleton & {
735
- derive: Ephemeral['derive'] & Volatile['derive']
736
- resolve: Ephemeral['resolve'] & Volatile['resolve']
737
- },
579
+ Singleton,
738
580
  Definitions['error'],
739
581
  Metadata['macro'],
740
582
  JoinPath<BasePath, Path>
@@ -762,9 +604,7 @@ export class Spiceflow<
762
604
  >
763
605
  }
764
606
  }
765
- >,
766
- Ephemeral,
767
- Volatile
607
+ >
768
608
  > {
769
609
  for (const method of METHODS) {
770
610
  this.add({ method, path, handler: handler, hooks: hook })
@@ -778,19 +618,10 @@ export class Spiceflow<
778
618
  const LocalSchema extends InputSchema<
779
619
  keyof Definitions['type'] & string
780
620
  >,
781
- const Schema extends MergeSchema<
782
- UnwrapRoute<LocalSchema, Definitions['type']>,
783
- MergeSchema<
784
- Volatile['schema'],
785
- MergeSchema<Ephemeral['schema'], Metadata['schema']>
786
- >
787
- >,
621
+ const Schema extends UnwrapRoute<LocalSchema, Definitions['type']>,
788
622
  const Handle extends InlineHandler<
789
623
  Schema,
790
- Singleton & {
791
- derive: Ephemeral['derive'] & Volatile['derive']
792
- resolve: Ephemeral['resolve'] & Volatile['resolve']
793
- },
624
+ Singleton,
794
625
  JoinPath<BasePath, Path>
795
626
  >,
796
627
  >(
@@ -799,10 +630,7 @@ export class Spiceflow<
799
630
  hook?: LocalHook<
800
631
  LocalSchema,
801
632
  Schema,
802
- Singleton & {
803
- derive: Ephemeral['derive'] & Volatile['derive']
804
- resolve: Ephemeral['resolve'] & Volatile['resolve']
805
- },
633
+ Singleton,
806
634
  Definitions['error'],
807
635
  Metadata['macro'],
808
636
  JoinPath<BasePath, Path>
@@ -830,24 +658,14 @@ export class Spiceflow<
830
658
  >
831
659
  }
832
660
  }
833
- >,
834
- Ephemeral,
835
- Volatile
661
+ >
836
662
  > {
837
663
  this.add({ method: 'HEAD', path, handler: handler, hooks: hook })
838
664
 
839
665
  return this as any
840
666
  }
841
667
 
842
- /**
843
- * If set to true, other Spiceflow handler will not inherits global life-cycle, store, decorators from the current instance
844
- *
845
- * @default false
846
- */
847
- private scoped?: Scoped
848
- get _scoped() {
849
- return this.scoped as Scoped
850
- }
668
+ private scoped?: Scoped = true as Scoped
851
669
 
852
670
  // group is not needed, you can add another prefixed app instead
853
671
  // group<
@@ -900,75 +718,47 @@ export class Spiceflow<
900
718
  Metadata,
901
719
  BasePath extends ``
902
720
  ? Routes & NewSpiceflow['_routes']
903
- : Routes & CreateEden<BasePath, NewSpiceflow['_routes']>,
904
- Ephemeral,
905
- Volatile
906
- > {
907
- const thisRouter = this.routerTree
908
- // TODO use scoped logic to add onRequest and onError on all routers if necessary, add them first
909
- this.routerTree.children.push(instance.routerTree)
910
- return this as any
911
- }
912
-
913
- onError<const Schema extends RouteSchema>(
721
+ : Routes & CreateEden<BasePath, NewSpiceflow['_routes']>
722
+ >
723
+ use<const Schema extends RouteSchema>(
914
724
  handler: MaybeArray<
915
- ErrorHandler<
916
- Definitions['error'],
917
- MergeSchema<
918
- Schema,
919
- MergeSchema<
920
- Volatile['schema'],
921
- MergeSchema<Ephemeral['schema'], Metadata['schema']>
922
- >
923
- >,
924
- Singleton,
925
- Ephemeral,
926
- Volatile
725
+ MiddlewareHandler<
726
+ Schema,
727
+ {
728
+ store: Singleton['store']
729
+ }
927
730
  >
928
731
  >,
929
- ): this {
930
- const router = this.routerTree
931
-
932
- router.onErrorHandlers ??= []
933
- router.onErrorHandlers.push(handler as any)
934
-
732
+ ): this
733
+
734
+ use(appOrHandler) {
735
+ if (appOrHandler instanceof Spiceflow) {
736
+ this.childrenApps.push(appOrHandler)
737
+ } else if (typeof appOrHandler === 'function') {
738
+ this.middlewares ??= []
739
+ this.middlewares.push(appOrHandler)
740
+ }
935
741
  return this
936
742
  }
937
743
 
938
- onRequest<const Schema extends RouteSchema>(
744
+ onError<const Schema extends RouteSchema>(
939
745
  handler: MaybeArray<
940
- PreHandler<
941
- MergeSchema<
942
- Schema,
943
- MergeSchema<
944
- Volatile['schema'],
945
- MergeSchema<Ephemeral['schema'], Metadata['schema']>
946
- >
947
- >,
948
- {
949
- decorator: Singleton['decorator']
950
- store: Singleton['store']
951
- derive: {}
952
- resolve: {}
953
- }
954
- >
746
+ ErrorHandler<Definitions['error'], Schema, Singleton>
955
747
  >,
956
- ) {
957
- const router = this.routerTree
958
- router.onRequestHandlers ??= []
959
- router.onRequestHandlers.push(handler as any)
748
+ ): this {
749
+ this.onErrorHandlers ??= []
750
+ this.onErrorHandlers.push(handler as any)
960
751
 
961
752
  return this
962
753
  }
754
+
963
755
  /**
964
756
  * Pass a request through all matching route handles and return a response
965
757
  * @param request The `Request`
966
758
  * @param platform Platform specific context {@link Platform}
967
759
  * @returns The final `Response`
968
760
  */
969
- async handle(request: Request, platform?: P): Promise<Response> {
970
- platform ??= {} as P
971
-
761
+ async handle(request: Request): Promise<Response> {
972
762
  let u = new URL(request.url, 'http://localhost')
973
763
  let path = u.pathname + u.search
974
764
  const defaultContext = {
@@ -976,79 +766,108 @@ export class Spiceflow<
976
766
  error: null,
977
767
  path,
978
768
  }
769
+ const root = this.topLevelApp || this
979
770
  let onErrorHandlers: OnError[] = []
980
771
  try {
981
- let response: Response | undefined
982
772
  // Get all middleware and method specific routes in order
983
773
 
984
774
  const route = this.match(request.method, path)
775
+
985
776
  if (!route) {
986
- return this.onNoMatch(request, platform)
777
+ const error = new NotFoundError(`${path} not found`)
778
+ const res = await this.runErrorHandlers({
779
+ onErrorHandlers,
780
+ error,
781
+ request,
782
+ })
783
+ if (res) return res
784
+ return new Response(`Not Found`, {
785
+ status: 404,
786
+ })
987
787
  }
988
- onErrorHandlers = this.getRouteAndParents(route.router)
989
- .reverse()
990
- .flatMap((x) => x.onErrorHandlers)
991
- let { params, store: defaultStore } = route
992
- const onReqHandlers = this.getRouteAndParents(route.router)
993
- .reverse()
994
- .flatMap((x) => x.onRequestHandlers)
788
+ onErrorHandlers = this.getAppsInScope(route.app).flatMap(
789
+ (x) => x.onErrorHandlers,
790
+ )
791
+ let {
792
+ params,
793
+ app: { defaultStore },
794
+ } = route
795
+ const middlewares = this.getAppsInScope(route.app).flatMap(
796
+ (x) => x.middlewares,
797
+ )
995
798
  // console.log({ onReqHandlers })
996
799
  let store = { ...defaultStore }
997
- // TODO add content type
998
800
 
999
- let content = route?.hooks?.content
1000
- // let body = await getRequestBody({ request, content })
801
+ let content = route?.internalRoute?.hooks?.content
1001
802
 
1002
- if (route.validateBody) {
803
+ if (route.internalRoute?.validateBody) {
1003
804
  // TODO don't clone the request
1004
- let typedRequest = new TypedRequest(request)
1005
- typedRequest.validateBody = route.validateBody
805
+ let typedRequest =
806
+ request instanceof TypedRequest
807
+ ? request
808
+ : new TypedRequest(request)
809
+ typedRequest.validateBody = route.internalRoute?.validateBody
1006
810
  request = typedRequest
1007
811
  }
1008
812
 
1009
813
  let query = parseQuery.parse((u.search || '').slice(1))
1010
814
 
1011
- if (onReqHandlers.length > 0) {
1012
- for (const handler of onReqHandlers) {
1013
- const res = await handler({
815
+ let index = 0
816
+
817
+ const next = async () => {
818
+ if (index < middlewares.length) {
819
+ const middleware = middlewares[index]
820
+ index++
821
+ let context = {
1014
822
  request,
1015
- response,
1016
823
  store,
1017
824
  path,
1018
825
  query,
1019
- } satisfies Context<any, any, any>)
1020
- if (res) {
1021
- return await turnHandlerResultIntoResponse(res)
826
+ params,
827
+ redirect,
828
+ } satisfies MiddlewareContext<any>
829
+ const result = await middleware(context, next)
830
+
831
+ if (!result && index < middlewares.length) {
832
+ return await next()
833
+ } else if (result) {
834
+ return await turnHandlerResultIntoResponse(result)
1022
835
  }
1023
836
  }
1024
- }
1025
837
 
1026
- query = runValidation(query, route.validateQuery)
1027
- params = runValidation(params, route.validateParams)
838
+ query = runValidation(query, route.internalRoute?.validateQuery)
839
+ params = runValidation(
840
+ params,
841
+ route.internalRoute?.validateParams,
842
+ )
1028
843
 
1029
- // console.log(route)
844
+ // console.log(route)
1030
845
 
1031
- const res = route.handler({
1032
- ...defaultContext,
1033
- request,
1034
- response,
1035
- params: params as any,
1036
- store,
1037
- query,
1038
- // body,
1039
- path,
1040
-
1041
- // platform
1042
- } satisfies Context<any, any, string>)
1043
- if (isAsyncIterable(res)) {
1044
- return await this.handleStream({
1045
- generator: res,
846
+ const res = route.internalRoute?.handler({
847
+ ...defaultContext,
1046
848
  request,
1047
- onErrorHandlers,
1048
- })
849
+ params: params as any,
850
+ redirect,
851
+ store,
852
+ query,
853
+ // body,
854
+ path,
855
+
856
+ // platform
857
+ } satisfies Context<any, any, any>)
858
+ if (isAsyncIterable(res)) {
859
+ return await this.handleStream({
860
+ generator: res,
861
+ request,
862
+ onErrorHandlers,
863
+ })
864
+ }
865
+
866
+ return await turnHandlerResultIntoResponse(res)
1049
867
  }
868
+ const response = await next()
1050
869
 
1051
- return await turnHandlerResultIntoResponse(res)
870
+ return response
1052
871
  } catch (err: any) {
1053
872
  if (err instanceof Response) return err
1054
873
  let res = await this.runErrorHandlers({
@@ -1081,29 +900,149 @@ export class Spiceflow<
1081
900
  }
1082
901
  }
1083
902
 
1084
- // get the route parents, the order is starting from the current router and going up to the root
1085
- private getRouteAndParents(currentRouter?: RouterTree) {
1086
- const parents: RouterTree[] = []
1087
- let current = currentRouter
903
+ private getAppAndParents(currentApp?: AnySpiceflow) {
904
+ let root = this.topLevelApp || this
1088
905
 
1089
- let root = this.routerTree.currentRoot || this.routerTree
1090
- // Perform BFS once to build a parent map
1091
- const parentMap = new Map<number, RouterTree>()
906
+ if (!root.childrenApps.length) {
907
+ return [root]
908
+ }
909
+ const parents: AnySpiceflow[] = []
910
+ let current = currentApp
911
+
912
+ const parentMap = new Map<number, AnySpiceflow>()
1092
913
  bfsFind(root, (node) => {
1093
- for (const child of node.children) {
914
+ for (const child of node.childrenApps) {
1094
915
  parentMap.set(child.id, node)
1095
916
  }
1096
917
  })
1097
918
 
1098
919
  // Traverse the parent map to get the parents
1099
920
  while (current) {
1100
- parents.push(current)
921
+ parents.unshift(current)
1101
922
  current = parentMap.get(current.id)
1102
923
  }
1103
924
 
1104
925
  return parents.filter((x) => x !== undefined)
1105
926
  }
1106
927
 
928
+ private getAppsInScope(currentApp?: AnySpiceflow) {
929
+ let root = this.topLevelApp || this
930
+ if (!root.childrenApps.length) {
931
+ return [root]
932
+ }
933
+ const withParents = this.getAppAndParents(currentApp)
934
+
935
+ const wantedOrder = bfs(root)
936
+ const scopeFalseApps = wantedOrder.filter((x) => x.scoped === false)
937
+ let appsInScope = [] as AnySpiceflow[]
938
+ for (const app of wantedOrder) {
939
+ if (scopeFalseApps.includes(app)) {
940
+ appsInScope.push(app)
941
+ continue
942
+ }
943
+ if (withParents.includes(app)) {
944
+ appsInScope.push(app)
945
+ continue
946
+ }
947
+ }
948
+ return appsInScope
949
+ }
950
+
951
+ async listen(port: number, hostname: string = '127.0.0.1') {
952
+ if (typeof Bun !== 'undefined') {
953
+ const server = Bun.serve({
954
+ port,
955
+ development: !isProduction,
956
+ hostname,
957
+ error(error) {
958
+ console.error(error)
959
+ return new Response('Internal Server Error', {
960
+ status: 500,
961
+ })
962
+ },
963
+
964
+ fetch: async (request) => {
965
+ const res = await this.handle(request)
966
+ return res
967
+ },
968
+ })
969
+ console.log(`Listening on http://localhost:${port}`)
970
+ return server
971
+ }
972
+ return this.listenNode(port, hostname)
973
+ }
974
+
975
+ async listenNode(port: number, hostname: string = '0.0.0.0') {
976
+ const { Readable } = await import('stream')
977
+ const { createServer } = await import('http')
978
+
979
+ const server = createServer(async (req, res) => {
980
+ const abortController = new AbortController()
981
+ const { signal } = abortController
982
+
983
+ req.on('close', () => abortController.abort())
984
+ req.on('error', (err) => {
985
+ abortController.abort()
986
+ })
987
+ req.on('aborted', (err) => {
988
+ abortController.abort()
989
+ })
990
+ // this is how you see when a request is aborted in Node.js, laughable
991
+ res.on('close', function () {
992
+ let aborted = !res.writableFinished
993
+ if (aborted) {
994
+ abortController.abort()
995
+ }
996
+ })
997
+
998
+ const url = new URL(
999
+ req.url || '',
1000
+ `http://${req.headers.host || hostname || 'localhost'}`,
1001
+ )
1002
+ const typedRequest = new TypedRequest(url.toString(), {
1003
+ method: req.method,
1004
+ headers: req.headers as HeadersInit,
1005
+ body:
1006
+ req.method !== 'GET' && req.method !== 'HEAD'
1007
+ ? (Readable.toWeb(req) as any)
1008
+ : null,
1009
+ signal,
1010
+ })
1011
+
1012
+ try {
1013
+ const response = await this.handle(typedRequest)
1014
+
1015
+ res.statusCode = response.status
1016
+ for (const [key, value] of response.headers) {
1017
+ res.setHeader(key, value)
1018
+ }
1019
+
1020
+ if (response.body) {
1021
+ const reader = response.body.getReader()
1022
+ while (true) {
1023
+ const { done, value } = await reader.read()
1024
+ if (done) break
1025
+ res.write(value)
1026
+ }
1027
+ }
1028
+ res.end()
1029
+ } catch (error) {
1030
+ console.error('Error handling request:', error)
1031
+ res.statusCode = 500
1032
+ res.end('Internal Server Error')
1033
+ }
1034
+ })
1035
+
1036
+ await new Promise((resolve, reject) => {
1037
+ server.listen(port, hostname, () => {
1038
+ console.log(`Listening on http://localhost:${port}`)
1039
+ resolve(null)
1040
+ })
1041
+ })
1042
+
1043
+ return server
1044
+ }
1045
+
1107
1046
  private async handleStream({
1108
1047
  onErrorHandlers,
1109
1048
  generator,
@@ -1264,8 +1203,8 @@ const METHODS = [
1264
1203
  export type Method = (typeof METHODS)[number]
1265
1204
 
1266
1205
  function bfsFind<T>(
1267
- tree: RouterTree,
1268
- onNode: (node: RouterTree) => T | undefined | void,
1206
+ tree: AnySpiceflow,
1207
+ onNode: (node: AnySpiceflow) => T | undefined | void,
1269
1208
  ): T | undefined {
1270
1209
  const queue = [tree]
1271
1210
 
@@ -1276,7 +1215,7 @@ function bfsFind<T>(
1276
1215
  if (result) {
1277
1216
  return result
1278
1217
  }
1279
- queue.push(...node.children)
1218
+ queue.push(...node.childrenApps)
1280
1219
  }
1281
1220
  return
1282
1221
  }
@@ -1289,9 +1228,9 @@ export class TypedRequest<T = any> extends Request {
1289
1228
  }
1290
1229
  }
1291
1230
 
1292
- export function bfs(tree: RouterTree) {
1231
+ export function bfs(tree: AnySpiceflow) {
1293
1232
  const queue = [tree]
1294
- let nodes: RouterTree[] = []
1233
+ let nodes: AnySpiceflow[] = []
1295
1234
  while (queue.length > 0) {
1296
1235
  const node = queue.shift()!
1297
1236
  if (node) {
@@ -1299,20 +1238,14 @@ export function bfs(tree: RouterTree) {
1299
1238
  }
1300
1239
  // const result = onNode(node)
1301
1240
 
1302
- queue.push(...node.children)
1241
+ if (node?.childrenApps?.length) {
1242
+ queue.push(...node.childrenApps)
1243
+ }
1303
1244
  }
1304
1245
  return nodes
1305
1246
  }
1306
- function mapTree<T>(
1307
- tree: RouterTree,
1308
- mapper: (node: RouterTree) => T,
1309
- ): T & { children: (T & { children: any[] })[] } {
1310
- const mappedNode = mapper(tree) as T & { children: any[] }
1311
- mappedNode.children = tree.children.map((child) => mapTree(child, mapper))
1312
- return mappedNode
1313
- }
1314
-
1315
1247
  export async function turnHandlerResultIntoResponse(result: any) {
1248
+ // if (result === undefined) return new Response('', { status: 404 })
1316
1249
  // if user returns not a response, convert to json
1317
1250
  if (result instanceof Response) {
1318
1251
  return result
@@ -1327,14 +1260,14 @@ export async function turnHandlerResultIntoResponse(result: any) {
1327
1260
  // }
1328
1261
  // if user returns an object, convert to json
1329
1262
 
1330
- return new Response(JSON.stringify(result, null, 2), {
1263
+ return new Response(JSON.stringify(result ?? null, null, 2), {
1331
1264
  headers: {
1332
1265
  'content-type': 'application/json',
1333
1266
  },
1334
1267
  })
1335
1268
  }
1336
1269
 
1337
- export type AnySpiceflow = Spiceflow<any, any, any, any, any, any, any, any>
1270
+ export type AnySpiceflow = Spiceflow<any, any, any, any, any, any>
1338
1271
 
1339
1272
  export function isZodSchema(value: unknown): value is ZodType {
1340
1273
  return (