spiceflow 1.1.8 → 1.1.9

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 (58) hide show
  1. package/README.md +177 -92
  2. package/dist/benchmark.benchmark.js.map +1 -1
  3. package/dist/client/errors.d.ts.map +1 -1
  4. package/dist/client/errors.js.map +1 -1
  5. package/dist/client/index.d.ts.map +1 -1
  6. package/dist/client/index.js +8 -12
  7. package/dist/client/index.js.map +1 -1
  8. package/dist/client/types.d.ts.map +1 -1
  9. package/dist/client/utils.js.map +1 -1
  10. package/dist/client/ws.d.ts.map +1 -1
  11. package/dist/client/ws.js +1 -3
  12. package/dist/client/ws.js.map +1 -1
  13. package/dist/client.test.js.map +1 -1
  14. package/dist/context.d.ts.map +1 -1
  15. package/dist/cors.d.ts.map +1 -1
  16. package/dist/cors.js.map +1 -1
  17. package/dist/cors.test.js.map +1 -1
  18. package/dist/error.d.ts.map +1 -1
  19. package/dist/error.js.map +1 -1
  20. package/dist/middleware.test.js.map +1 -1
  21. package/dist/openapi.d.ts.map +1 -1
  22. package/dist/openapi.js +1 -1
  23. package/dist/openapi.js.map +1 -1
  24. package/dist/openapi.test.js.map +1 -1
  25. package/dist/spiceflow.d.ts.map +1 -1
  26. package/dist/spiceflow.js +4 -2
  27. package/dist/spiceflow.js.map +1 -1
  28. package/dist/spiceflow.test.js.map +1 -1
  29. package/dist/stream.test.js.map +1 -1
  30. package/dist/types.d.ts.map +1 -1
  31. package/dist/types.js.map +1 -1
  32. package/dist/types.test.js +2 -6
  33. package/dist/types.test.js.map +1 -1
  34. package/dist/utils.d.ts.map +1 -1
  35. package/dist/utils.js.map +1 -1
  36. package/dist/zod.test.js.map +1 -1
  37. package/package.json +1 -1
  38. package/src/benchmark.benchmark.ts +8 -8
  39. package/src/client/errors.ts +17 -17
  40. package/src/client/index.ts +437 -469
  41. package/src/client/types.ts +168 -191
  42. package/src/client/utils.ts +5 -5
  43. package/src/client/ws.ts +87 -89
  44. package/src/client.test.ts +176 -183
  45. package/src/context.ts +82 -82
  46. package/src/cors.test.ts +38 -38
  47. package/src/cors.ts +87 -92
  48. package/src/error.ts +13 -13
  49. package/src/middleware.test.ts +201 -201
  50. package/src/openapi.test.ts +97 -97
  51. package/src/openapi.ts +365 -365
  52. package/src/spiceflow.test.ts +461 -467
  53. package/src/spiceflow.ts +1117 -1161
  54. package/src/stream.test.ts +310 -310
  55. package/src/types.test.ts +46 -50
  56. package/src/types.ts +698 -701
  57. package/src/utils.ts +79 -79
  58. package/src/zod.test.ts +64 -64
package/src/spiceflow.ts CHANGED
@@ -6,26 +6,26 @@ export { Type as t }
6
6
 
7
7
  import addFormats from 'ajv-formats'
8
8
  import {
9
- ComposeSpiceflowResponse,
10
- CreateEden,
11
- DefinitionBase,
12
- ErrorHandler,
13
- HTTPMethod,
14
- InlineHandler,
15
- InputSchema,
16
- IsAny,
17
- JoinPath,
18
- LocalHook,
19
- MaybeArray,
20
- MetadataBase,
21
- MiddlewareHandler,
22
- Reconcile,
23
- ResolvePath,
24
- RouteBase,
25
- RouteSchema,
26
- SingletonBase,
27
- TypeSchema,
28
- UnwrapRoute,
9
+ ComposeSpiceflowResponse,
10
+ CreateEden,
11
+ DefinitionBase,
12
+ ErrorHandler,
13
+ HTTPMethod,
14
+ InlineHandler,
15
+ InputSchema,
16
+ IsAny,
17
+ JoinPath,
18
+ LocalHook,
19
+ MaybeArray,
20
+ MetadataBase,
21
+ MiddlewareHandler,
22
+ Reconcile,
23
+ ResolvePath,
24
+ RouteBase,
25
+ RouteSchema,
26
+ SingletonBase,
27
+ TypeSchema,
28
+ UnwrapRoute,
29
29
  } from './types.js'
30
30
  let globalIndex = 0
31
31
 
@@ -38,23 +38,23 @@ import { isProduction, ValidationError } from './error.js'
38
38
  import { isAsyncIterable, redirect } from './utils.js'
39
39
 
40
40
  const ajv = (addFormats.default || addFormats)(
41
- new (Ajv.default || Ajv)({ useDefaults: true }),
42
- [
43
- 'date-time',
44
- 'time',
45
- 'date',
46
- 'email',
47
- 'hostname',
48
- 'ipv4',
49
- 'ipv6',
50
- 'uri',
51
- 'uri-reference',
52
- 'uuid',
53
- 'uri-template',
54
- 'json-pointer',
55
- 'relative-json-pointer',
56
- 'regex',
57
- ],
41
+ new (Ajv.default || Ajv)({ useDefaults: true }),
42
+ [
43
+ 'date-time',
44
+ 'time',
45
+ 'date',
46
+ 'email',
47
+ 'hostname',
48
+ 'ipv4',
49
+ 'ipv6',
50
+ 'uri',
51
+ 'uri-reference',
52
+ 'uuid',
53
+ 'uri-template',
54
+ 'json-pointer',
55
+ 'relative-json-pointer',
56
+ 'regex',
57
+ ],
58
58
  )
59
59
 
60
60
  // Should be exported from `hono/router`
@@ -64,1051 +64,1005 @@ type AsyncResponse = Response | Promise<Response>
64
64
  type OnError = (x: { error: any; request: Request }) => AsyncResponse
65
65
 
66
66
  export type InternalRoute = {
67
- method: HTTPMethod
68
- path: string
69
- handler: InlineHandler<any, any, any>
70
- hooks: LocalHook<any, any, any, any, any, any, any>
71
- validateBody?: ValidateFunction
72
- validateQuery?: ValidateFunction
73
- validateParams?: ValidateFunction
74
- // prefix: string
67
+ method: HTTPMethod
68
+ path: string
69
+ handler: InlineHandler<any, any, any>
70
+ hooks: LocalHook<any, any, any, any, any, any, any>
71
+ validateBody?: ValidateFunction
72
+ validateQuery?: ValidateFunction
73
+ validateParams?: ValidateFunction
74
+ // prefix: string
75
75
  }
76
76
 
77
77
  type MedleyRouter = {
78
- find: (path: string) =>
79
- | {
80
- store: Record<string, InternalRoute> //
81
- params: Record<string, any>
82
- }
83
- | undefined
84
- register: (path: string | undefined) => Record<string, InternalRoute>
78
+ find: (path: string) =>
79
+ | {
80
+ store: Record<string, InternalRoute> //
81
+ params: Record<string, any>
82
+ }
83
+ | undefined
84
+ register: (path: string | undefined) => Record<string, InternalRoute>
85
85
  }
86
86
 
87
87
  const notFoundHandler = (c) => {
88
- return new Response('Not Found', { status: 404 })
88
+ return new Response('Not Found', { status: 404 })
89
89
  }
90
90
 
91
91
  export class Spiceflow<
92
- const in out BasePath extends string = '',
93
- const in out Scoped extends boolean = true,
94
- const in out Singleton extends SingletonBase = {
95
- state: {}
96
- },
97
- const in out Definitions extends DefinitionBase = {
98
- type: {}
99
- error: {}
100
- },
101
- const in out Metadata extends MetadataBase = {
102
- schema: {}
103
- macro: {}
104
- macroFn: {}
105
- },
106
- const out Routes extends RouteBase = {},
92
+ const in out BasePath extends string = '',
93
+ const in out Scoped extends boolean = true,
94
+ const in out Singleton extends SingletonBase = {
95
+ state: {}
96
+ },
97
+ const in out Definitions extends DefinitionBase = {
98
+ type: {}
99
+ error: {}
100
+ },
101
+ const in out Metadata extends MetadataBase = {
102
+ schema: {}
103
+ macro: {}
104
+ macroFn: {}
105
+ },
106
+ const out Routes extends RouteBase = {},
107
107
  > {
108
- private id: number = globalIndex++
109
- private router: MedleyRouter = new OriginalRouter()
110
- private middlewares: Function[] = []
111
- private onErrorHandlers: OnError[] = []
112
- private routes: InternalRoute[] = []
113
- private defaultState: Record<any, any> = {}
114
- private topLevelApp?: AnySpiceflow
115
-
116
- /** @internal */
117
- prefix?: string
118
-
119
- /** @internal */
120
- childrenApps: AnySpiceflow[] = []
121
-
122
- /** @internal */
123
- getAllRoutes() {
124
- let root = this.topLevelApp || this
125
- const allApps = bfs(root) || []
126
- const allRoutes = allApps.flatMap((x) => {
127
- const prefix = this.getAppAndParents(x)
128
- .map((x) => x.prefix)
129
- .join('')
130
-
131
- return x.routes.map((x) => ({ ...x, path: prefix + x.path }))
132
- })
133
- return allRoutes
134
- }
135
-
136
- private add({
137
- method,
138
- path,
139
- hooks,
140
- handler,
141
- ...rest
142
- }: Partial<InternalRoute>) {
143
- let bodySchema: TypeSchema = hooks?.body
144
- let validateBody = getValidateFunction(bodySchema)
145
- let validateQuery = getValidateFunction(hooks?.query)
146
- let validateParams = getValidateFunction(hooks?.params)
147
-
148
- const store = this.router.register(path)
149
- let route: InternalRoute = {
150
- ...rest,
151
- method: (method || '') as any,
152
- path: path || '',
153
- handler: handler!,
154
- hooks,
155
- validateBody,
156
- validateParams,
157
- validateQuery,
158
- }
159
- this.routes.push(route)
160
- store[method!] = route
161
- }
162
-
163
- private match(method: string, path: string) {
164
- let root = this
165
- let foundApp: AnySpiceflow | undefined
166
- const result = bfsFind(this, (app) => {
167
- app.topLevelApp = root
168
- let prefix = this.getAppAndParents(app)
169
- .map((x) => x.prefix)
170
- .join('')
171
- if (prefix && !path.startsWith(prefix)) {
172
- return
173
- }
174
- let pathWithoutPrefix = path
175
- if (prefix) {
176
- pathWithoutPrefix = path.replace(prefix, '')
177
- }
178
- const medleyRoute = app.router.find(pathWithoutPrefix)
179
- if (!medleyRoute) {
180
- foundApp = app
181
- return
182
- }
183
-
184
- let internalRoute: InternalRoute = medleyRoute.store[method]
185
-
186
- if (internalRoute) {
187
- const params = medleyRoute.params || {}
188
-
189
- const res = {
190
- app,
191
- internalRoute: internalRoute,
192
- params,
193
- }
194
- return res
195
- }
196
- if (method === 'HEAD') {
197
- let internalRouteGet: InternalRoute = medleyRoute.store['GET']
198
- if (!internalRouteGet?.handler) {
199
- return
200
- }
201
- return {
202
- app,
203
- internalRoute: {
204
- hooks: {},
205
- handler: async (c) => {
206
- const response = await internalRouteGet.handler(c)
207
- if (response instanceof Response) {
208
- return new Response('', {
209
- status: response.status,
210
- statusText: response.statusText,
211
- headers: response.headers,
212
- })
213
- }
214
- return new Response(null, { status: 200 })
215
- },
216
- method,
217
- path,
218
- } as InternalRoute,
219
- params: medleyRoute.params,
220
- }
221
- }
222
- })
223
-
224
- return (
225
- result || {
226
- app: foundApp || root,
227
- internalRoute: {
228
- hooks: {},
229
- handler: notFoundHandler,
230
- method,
231
- path,
232
- } as InternalRoute,
233
- params: {},
234
- }
235
- )
236
- }
237
-
238
- state<const Name extends string | number | symbol, Value>(
239
- name: Name,
240
- value: Value,
241
- ): Spiceflow<
242
- BasePath,
243
- Scoped,
244
- {
245
- state: Reconcile<
246
- Singleton['state'],
247
- {
248
- [name in Name]: Value
249
- }
250
- >
251
- },
252
- Definitions,
253
- Metadata,
254
- Routes
255
- > {
256
- this.defaultState[name] = value
257
- return this as any
258
- }
259
-
260
- /**
261
- * Create a new Router
262
- * @param options {@link RouterOptions} {@link Platform}
263
- */
264
- constructor(
265
- options: {
266
- name?: string
267
- scoped?: Scoped
268
-
269
- basePath?: BasePath
270
- } = {},
271
- ) {
272
- this.scoped = options.scoped
273
-
274
- this.prefix = options.basePath
275
- }
276
-
277
- _routes: Routes = {} as any
278
-
279
- _types = {
280
- Prefix: '' as BasePath,
281
- Scoped: false as Scoped,
282
- Singleton: {} as Singleton,
283
- Definitions: {} as Definitions,
284
- Metadata: {} as Metadata,
285
- }
286
-
287
- post<
288
- const Path extends string,
289
- const LocalSchema extends InputSchema<
290
- keyof Definitions['type'] & string
291
- >,
292
- const Schema extends UnwrapRoute<LocalSchema, Definitions['type']>,
293
- const Handle extends InlineHandler<
294
- Schema,
295
- Singleton,
296
- JoinPath<BasePath, Path>
297
- >,
298
- >(
299
- path: Path,
300
- handler: Handle,
301
- hook?: LocalHook<
302
- LocalSchema,
303
- Schema,
304
- Singleton,
305
- Definitions['error'],
306
- Metadata['macro'],
307
- JoinPath<BasePath, Path>
308
- >,
309
- ): Spiceflow<
310
- BasePath,
311
- Scoped,
312
- Singleton,
313
- Definitions,
314
- Metadata,
315
- Routes &
316
- CreateEden<
317
- JoinPath<BasePath, Path>,
318
- {
319
- post: {
320
- body: Schema['body']
321
- params: undefined extends Schema['params']
322
- ? ResolvePath<Path>
323
- : Schema['params']
324
- query: Schema['query']
325
- response: ComposeSpiceflowResponse<
326
- Schema['response'],
327
- Handle
328
- >
329
- }
330
- }
331
- >
332
- > {
333
- this.add({ method: 'POST', path, handler: handler, hooks: hook })
334
-
335
- return this as any
336
- }
337
-
338
- get<
339
- const Path extends string,
340
- const LocalSchema extends InputSchema<
341
- keyof Definitions['type'] & string
342
- >,
343
- const Schema extends UnwrapRoute<LocalSchema, Definitions['type']>,
344
- const Macro extends Metadata['macro'],
345
- const Handle extends InlineHandler<
346
- Schema,
347
- Singleton,
348
- JoinPath<BasePath, Path>
349
- >,
350
- >(
351
- path: Path,
352
- handler: Handle,
353
- hook?: LocalHook<
354
- LocalSchema,
355
- Schema,
356
- Singleton,
357
- Definitions['error'],
358
- Macro,
359
- JoinPath<BasePath, Path>
360
- >,
361
- ): Spiceflow<
362
- BasePath,
363
- Scoped,
364
- Singleton,
365
- Definitions,
366
- Metadata,
367
- Routes &
368
- CreateEden<
369
- JoinPath<BasePath, Path>,
370
- {
371
- get: {
372
- body: Schema['body']
373
- params: undefined extends Schema['params']
374
- ? ResolvePath<Path>
375
- : Schema['params']
376
- query: Schema['query']
377
-
378
- response: ComposeSpiceflowResponse<
379
- Schema['response'],
380
- Handle
381
- >
382
- }
383
- }
384
- >
385
- > {
386
- this.add({ method: 'GET', path, handler: handler, hooks: hook })
387
- return this as any
388
- }
389
-
390
- put<
391
- const Path extends string,
392
- const LocalSchema extends InputSchema<
393
- keyof Definitions['type'] & string
394
- >,
395
- const Schema extends UnwrapRoute<LocalSchema, Definitions['type']>,
396
- const Handle extends InlineHandler<
397
- Schema,
398
- Singleton,
399
- JoinPath<BasePath, Path>
400
- >,
401
- >(
402
- path: Path,
403
- handler: Handle,
404
- hook?: LocalHook<
405
- LocalSchema,
406
- Schema,
407
- Singleton,
408
- Definitions['error'],
409
- Metadata['macro'],
410
- JoinPath<BasePath, Path>
411
- >,
412
- ): Spiceflow<
413
- BasePath,
414
- Scoped,
415
- Singleton,
416
- Definitions,
417
- Metadata,
418
- Routes &
419
- CreateEden<
420
- JoinPath<BasePath, Path>,
421
- {
422
- put: {
423
- body: Schema['body']
424
- params: undefined extends Schema['params']
425
- ? ResolvePath<Path>
426
- : Schema['params']
427
- query: Schema['query']
428
-
429
- response: ComposeSpiceflowResponse<
430
- Schema['response'],
431
- Handle
432
- >
433
- }
434
- }
435
- >
436
- > {
437
- this.add({ method: 'PUT', path, handler: handler, hooks: hook })
438
-
439
- return this as any
440
- }
441
-
442
- patch<
443
- const Path extends string,
444
- const LocalSchema extends InputSchema<
445
- keyof Definitions['type'] & string
446
- >,
447
- const Schema extends UnwrapRoute<LocalSchema, Definitions['type']>,
448
- const Handle extends InlineHandler<
449
- Schema,
450
- Singleton,
451
- JoinPath<BasePath, Path>
452
- >,
453
- >(
454
- path: Path,
455
- handler: Handle,
456
- hook?: LocalHook<
457
- LocalSchema,
458
- Schema,
459
- Singleton,
460
- Definitions['error'],
461
- Metadata['macro'],
462
- JoinPath<BasePath, Path>
463
- >,
464
- ): Spiceflow<
465
- BasePath,
466
- Scoped,
467
- Singleton,
468
- Definitions,
469
- Metadata,
470
- Routes &
471
- CreateEden<
472
- JoinPath<BasePath, Path>,
473
- {
474
- patch: {
475
- body: Schema['body']
476
- params: undefined extends Schema['params']
477
- ? ResolvePath<Path>
478
- : Schema['params']
479
- query: Schema['query']
480
-
481
- response: ComposeSpiceflowResponse<
482
- Schema['response'],
483
- Handle
484
- >
485
- }
486
- }
487
- >
488
- > {
489
- this.add({ method: 'PATCH', path, handler: handler, hooks: hook })
490
-
491
- return this as any
492
- }
493
-
494
- delete<
495
- const Path extends string,
496
- const LocalSchema extends InputSchema<
497
- keyof Definitions['type'] & string
498
- >,
499
- const Schema extends UnwrapRoute<LocalSchema, Definitions['type']>,
500
- const Handle extends InlineHandler<
501
- Schema,
502
- Singleton,
503
- JoinPath<BasePath, Path>
504
- >,
505
- >(
506
- path: Path,
507
- handler: Handle,
508
- hook?: LocalHook<
509
- LocalSchema,
510
- Schema,
511
- Singleton,
512
- Definitions['error'],
513
- Metadata['macro'],
514
- JoinPath<BasePath, Path>
515
- >,
516
- ): Spiceflow<
517
- BasePath,
518
- Scoped,
519
- Singleton,
520
- Definitions,
521
- Metadata,
522
- Routes &
523
- CreateEden<
524
- JoinPath<BasePath, Path>,
525
- {
526
- delete: {
527
- body: Schema['body']
528
- params: undefined extends Schema['params']
529
- ? ResolvePath<Path>
530
- : Schema['params']
531
- query: Schema['query']
532
-
533
- response: ComposeSpiceflowResponse<
534
- Schema['response'],
535
- Handle
536
- >
537
- }
538
- }
539
- >
540
- > {
541
- this.add({ method: 'DELETE', path, handler: handler, hooks: hook })
542
-
543
- return this as any
544
- }
545
-
546
- options<
547
- const Path extends string,
548
- const LocalSchema extends InputSchema<
549
- keyof Definitions['type'] & string
550
- >,
551
- const Schema extends UnwrapRoute<LocalSchema, Definitions['type']>,
552
- const Handle extends InlineHandler<
553
- Schema,
554
- Singleton,
555
- JoinPath<BasePath, Path>
556
- >,
557
- >(
558
- path: Path,
559
- handler: Handle,
560
- hook?: LocalHook<
561
- LocalSchema,
562
- Schema,
563
- Singleton,
564
- Definitions['error'],
565
- Metadata['macro'],
566
- JoinPath<BasePath, Path>
567
- >,
568
- ): Spiceflow<
569
- BasePath,
570
- Scoped,
571
- Singleton,
572
- Definitions,
573
- Metadata,
574
- Routes &
575
- CreateEden<
576
- JoinPath<BasePath, Path>,
577
- {
578
- options: {
579
- body: Schema['body']
580
- params: undefined extends Schema['params']
581
- ? ResolvePath<Path>
582
- : Schema['params']
583
- query: Schema['query']
584
-
585
- response: ComposeSpiceflowResponse<
586
- Schema['response'],
587
- Handle
588
- >
589
- }
590
- }
591
- >
592
- > {
593
- this.add({ method: 'OPTIONS', path, handler: handler, hooks: hook })
594
-
595
- return this as any
596
- }
597
-
598
- all<
599
- const Path extends string,
600
- const LocalSchema extends InputSchema<
601
- keyof Definitions['type'] & string
602
- >,
603
- const Schema extends UnwrapRoute<LocalSchema, Definitions['type']>,
604
- const Handle extends InlineHandler<
605
- Schema,
606
- Singleton,
607
- JoinPath<BasePath, Path>
608
- >,
609
- >(
610
- path: Path,
611
- handler: Handle,
612
- hook?: LocalHook<
613
- LocalSchema,
614
- Schema,
615
- Singleton,
616
- Definitions['error'],
617
- Metadata['macro'],
618
- JoinPath<BasePath, Path>
619
- >,
620
- ): Spiceflow<
621
- BasePath,
622
- Scoped,
623
- Singleton,
624
- Definitions,
625
- Metadata,
626
- Routes &
627
- CreateEden<
628
- JoinPath<BasePath, Path>,
629
- {
630
- [method in string]: {
631
- body: Schema['body']
632
- params: undefined extends Schema['params']
633
- ? ResolvePath<Path>
634
- : Schema['params']
635
- query: Schema['query']
636
-
637
- response: ComposeSpiceflowResponse<
638
- Schema['response'],
639
- Handle
640
- >
641
- }
642
- }
643
- >
644
- > {
645
- for (const method of METHODS) {
646
- this.add({ method, path, handler: handler, hooks: hook })
647
- }
648
-
649
- return this as any
650
- }
651
-
652
- head<
653
- const Path extends string,
654
- const LocalSchema extends InputSchema<
655
- keyof Definitions['type'] & string
656
- >,
657
- const Schema extends UnwrapRoute<LocalSchema, Definitions['type']>,
658
- const Handle extends InlineHandler<
659
- Schema,
660
- Singleton,
661
- JoinPath<BasePath, Path>
662
- >,
663
- >(
664
- path: Path,
665
- handler: Handle,
666
- hook?: LocalHook<
667
- LocalSchema,
668
- Schema,
669
- Singleton,
670
- Definitions['error'],
671
- Metadata['macro'],
672
- JoinPath<BasePath, Path>
673
- >,
674
- ): Spiceflow<
675
- BasePath,
676
- Scoped,
677
- Singleton,
678
- Definitions,
679
- Metadata,
680
- Routes &
681
- CreateEden<
682
- JoinPath<BasePath, Path>,
683
- {
684
- head: {
685
- body: Schema['body']
686
- params: undefined extends Schema['params']
687
- ? ResolvePath<Path>
688
- : Schema['params']
689
- query: Schema['query']
690
-
691
- response: ComposeSpiceflowResponse<
692
- Schema['response'],
693
- Handle
694
- >
695
- }
696
- }
697
- >
698
- > {
699
- this.add({ method: 'HEAD', path, handler: handler, hooks: hook })
700
-
701
- return this as any
702
- }
703
-
704
- private scoped?: Scoped = true as Scoped
705
-
706
- use<const NewSpiceflow extends AnySpiceflow>(
707
- instance: NewSpiceflow,
708
- ): IsAny<NewSpiceflow> extends true
709
- ? this
710
- : Spiceflow<
711
- BasePath,
712
- Scoped,
713
- Singleton,
714
- Definitions,
715
- Metadata,
716
- BasePath extends ``
717
- ? Routes & NewSpiceflow['_routes']
718
- : Routes & CreateEden<BasePath, NewSpiceflow['_routes']>
719
- >
720
- use<const Schema extends RouteSchema>(
721
- handler: MiddlewareHandler<
722
- Schema,
723
- {
724
- state: Singleton['state']
725
- }
726
- >,
727
- ): this
728
-
729
- use(appOrHandler) {
730
- if (appOrHandler instanceof Spiceflow) {
731
- this.childrenApps.push(appOrHandler)
732
- } else if (typeof appOrHandler === 'function') {
733
- this.middlewares ??= []
734
- this.middlewares.push(appOrHandler)
735
- }
736
- return this
737
- }
738
-
739
- onError<const Schema extends RouteSchema>(
740
- handler: MaybeArray<
741
- ErrorHandler<Definitions['error'], Schema, Singleton>
742
- >,
743
- ): this {
744
- this.onErrorHandlers ??= []
745
- this.onErrorHandlers.push(handler as any)
746
-
747
- return this
748
- }
749
-
750
- async handle(request: Request): Promise<Response> {
751
- let u = new URL(request.url, 'http://localhost')
752
- let path = u.pathname + u.search
753
- const defaultContext = {
754
- redirect,
755
- error: null,
756
- path,
757
- }
758
- const root = this.topLevelApp || this
759
- let onErrorHandlers: OnError[] = []
760
- try {
761
- // Get all middleware and method specific routes in order
762
-
763
- const route = this.match(request.method, path)
764
-
765
- const appsInScope = this.getAppsInScope(route.app)
766
- onErrorHandlers = appsInScope.flatMap((x) => x.onErrorHandlers)
767
- let {
768
- params: _params,
769
- app: { defaultState },
770
- } = route
771
- const middlewares = appsInScope.flatMap((x) => x.middlewares)
772
- // console.log({ onReqHandlers })
773
- let state = structuredClone(defaultState)
774
-
775
- let content = route?.internalRoute?.hooks?.content
776
-
777
- if (route.internalRoute?.validateBody) {
778
- // TODO don't clone the request
779
- let typedRequest =
780
- request instanceof SpiceflowRequest
781
- ? request
782
- : new SpiceflowRequest(request)
783
- typedRequest.validateBody = route.internalRoute?.validateBody
784
- request = typedRequest
785
- }
786
-
787
- let index = 0
788
- let context = {
789
- ...defaultContext,
790
- request,
791
- state,
792
- path,
793
- query: parseQuery.parse((u.search || '').slice(1)),
794
- params: _params,
795
- redirect,
796
- } satisfies MiddlewareContext<any>
797
- let handlerResponse: Response | undefined
798
- const next = async () => {
799
- if (index < middlewares.length) {
800
- const middleware = middlewares[index]
801
- index++
802
-
803
- const result = await middleware(context, next)
804
- if (result instanceof Response) {
805
- handlerResponse = result
806
- }
807
- if (!result && index < middlewares.length) {
808
- return await next()
809
- } else if (result) {
810
- return await turnHandlerResultIntoResponse(result)
811
- }
812
- }
813
- if (handlerResponse) {
814
- return handlerResponse
815
- }
816
-
817
- context.query = runValidation(
818
- context.query,
819
- route.internalRoute?.validateQuery,
820
- )
821
- context.params = runValidation(
822
- context.params,
823
- route.internalRoute?.validateParams,
824
- )
825
-
826
- const res = route.internalRoute?.handler(context)
827
- if (isAsyncIterable(res)) {
828
- handlerResponse = await this.handleStream({
829
- generator: res,
830
- request,
831
- onErrorHandlers,
832
- })
833
- return handlerResponse
834
- }
835
-
836
- handlerResponse = await turnHandlerResultIntoResponse(res)
837
- return handlerResponse
838
- }
839
- const response = await next()
840
-
841
- return response
842
- } catch (err: any) {
843
- if (err instanceof Response) return err
844
- let res = await this.runErrorHandlers({
845
- onErrorHandlers,
846
- error: err,
847
- request,
848
- })
849
- if (res instanceof Response) return res
850
-
851
- let status = err?.status ?? 500
852
- res ||= new Response(err?.message || 'Internal Server Error', {
853
- status,
854
- })
855
- return res
856
- }
857
- }
858
-
859
- private async runErrorHandlers({
860
- onErrorHandlers = [] as OnError[],
861
- error: err,
862
- request,
863
- }) {
864
- if (onErrorHandlers.length === 0) {
865
- console.error(`Spiceflow unhandled error:`, err)
866
- } else {
867
- for (const errHandler of onErrorHandlers) {
868
- const res = errHandler({ error: err, request })
869
- if (res instanceof Response) {
870
- return res
871
- }
872
- }
873
- }
874
- }
875
-
876
- private getAppAndParents(currentApp?: AnySpiceflow) {
877
- let root = this.topLevelApp || this
878
-
879
- if (!root.childrenApps.length) {
880
- return [root]
881
- }
882
- const parents: AnySpiceflow[] = []
883
- let current = currentApp
884
-
885
- const parentMap = new Map<number, AnySpiceflow>()
886
- bfsFind(root, (node) => {
887
- for (const child of node.childrenApps) {
888
- parentMap.set(child.id, node)
889
- }
890
- })
891
-
892
- // Traverse the parent map to get the parents
893
- while (current) {
894
- parents.unshift(current)
895
- current = parentMap.get(current.id)
896
- }
897
-
898
- return parents.filter((x) => x !== undefined)
899
- }
900
-
901
- private getAppsInScope(currentApp?: AnySpiceflow) {
902
- let root = this.topLevelApp || this
903
- if (!root.childrenApps.length) {
904
- return [root]
905
- }
906
- const withParents = this.getAppAndParents(currentApp)
907
-
908
- const wantedOrder = bfs(root)
909
- const scopeFalseApps = wantedOrder.filter((x) => x.scoped === false)
910
- let appsInScope = [] as AnySpiceflow[]
911
- for (const app of wantedOrder) {
912
- if (scopeFalseApps.includes(app)) {
913
- appsInScope.push(app)
914
- continue
915
- }
916
- if (withParents.includes(app)) {
917
- appsInScope.push(app)
918
- continue
919
- }
920
- }
921
- return appsInScope
922
- }
923
-
924
- async listen(port: number, hostname: string = '127.0.0.1') {
925
- if (typeof Bun !== 'undefined') {
926
- const server = Bun.serve({
927
- port,
928
- development: !isProduction,
929
- hostname,
930
- reusePort: true,
931
- error(error) {
932
- console.error(error)
933
- return new Response('Internal Server Error', {
934
- status: 500,
935
- })
936
- },
937
-
938
- fetch: async (request) => {
939
- const res = await this.handle(request)
940
- return res
941
- },
942
- })
943
- process.on('beforeExit', () => {
944
- server.stop()
945
- })
946
- console.log(`Listening on http://localhost:${port}`)
947
- return server
948
- }
949
- return this.listenNode(port, hostname)
950
- }
951
-
952
- async listenNode(port: number, hostname: string = '0.0.0.0') {
953
- const { Readable } = await import('stream')
954
- const { createServer } = await import('http')
955
-
956
- const server = createServer(async (req, res) => {
957
- const abortController = new AbortController()
958
- const { signal } = abortController
959
-
960
- req.on('error', (err) => {
961
- abortController.abort()
962
- })
963
- req.on('aborted', (err) => {
964
- abortController.abort()
965
- })
966
- // this is how you see when a request is aborted in Node.js, laughable
967
- res.on('close', function () {
968
- let aborted = !res.writableFinished
969
- if (aborted) {
970
- abortController.abort()
971
- }
972
- })
973
-
974
- const url = new URL(
975
- req.url || '',
976
- `http://${req.headers.host || hostname || 'localhost'}`,
977
- )
978
- const typedRequest = new SpiceflowRequest(url.toString(), {
979
- method: req.method,
980
- headers: req.headers as HeadersInit,
981
- body:
982
- req.method !== 'GET' && req.method !== 'HEAD'
983
- ? (Readable.toWeb(req) as any)
984
- : null,
985
- signal,
986
- // @ts-ignore
987
- duplex: 'half',
988
- // keepalive: true,
989
- })
990
-
991
- try {
992
- const response = await this.handle(typedRequest)
993
-
994
- res.statusCode = response.status
995
- for (const [key, value] of response.headers) {
996
- res.setHeader(key, value)
997
- }
998
-
999
- if (response.body) {
1000
- const reader = response.body.getReader()
1001
- while (true) {
1002
- const { done, value } = await reader.read()
1003
- if (done) break
1004
- res.write(value)
1005
- }
1006
- }
1007
- res.end()
1008
- } catch (error) {
1009
- console.error('Error handling request:', error)
1010
- res.statusCode = 500
1011
- res.end('Internal Server Error')
1012
- }
1013
- })
1014
-
1015
- await new Promise((resolve, reject) => {
1016
- server.listen(port, hostname, () => {
1017
- console.log(`Listening on http://localhost:${port}`)
1018
- resolve(null)
1019
- })
1020
- })
1021
-
1022
- return server
1023
- }
1024
-
1025
- private async handleStream({
1026
- onErrorHandlers,
1027
- generator,
1028
- request,
1029
- }: {
1030
- generator: Generator | AsyncGenerator
1031
- onErrorHandlers: OnError[]
1032
- request: Request
1033
- }) {
1034
- let init = generator.next()
1035
- if (init instanceof Promise) init = await init
1036
-
1037
- if (init?.done) {
1038
- return await turnHandlerResultIntoResponse(init.value)
1039
- }
1040
- // let errorHandlers = this.routerTree.onErrorHandlers
1041
- let self = this
1042
- return new Response(
1043
- new ReadableStream({
1044
- async start(controller) {
1045
- let end = false
1046
-
1047
- request?.signal.addEventListener('abort', () => {
1048
- end = true
1049
-
1050
- try {
1051
- controller.close()
1052
- } catch {
1053
- // nothing
1054
- }
1055
- })
1056
-
1057
- if (init?.value !== undefined && init?.value !== null)
1058
- controller.enqueue(
1059
- Buffer.from(
1060
- `event: message\ndata: ${JSON.stringify(
1061
- init.value,
1062
- )}\n\n`,
1063
- ),
1064
- )
1065
-
1066
- try {
1067
- for await (const chunk of generator) {
1068
- if (end) break
1069
- if (chunk === undefined || chunk === null) continue
1070
-
1071
- controller.enqueue(
1072
- Buffer.from(
1073
- `event: message\ndata: ${JSON.stringify(
1074
- chunk,
1075
- )}\n\n`,
1076
- ),
1077
- )
1078
- }
1079
- } catch (error: any) {
1080
- let res = await self.runErrorHandlers({
1081
- onErrorHandlers: onErrorHandlers,
1082
- error,
1083
- request,
1084
- })
1085
- controller.enqueue(
1086
- Buffer.from(
1087
- `event: error\ndata: ${JSON.stringify(
1088
- error.message || error.name || 'Error',
1089
- )}\n\n`,
1090
- ),
1091
- )
1092
- }
1093
-
1094
- try {
1095
- controller.close()
1096
- } catch {
1097
- // nothing
1098
- }
1099
- },
1100
- }),
1101
- {
1102
- // TODO add headers somehow
1103
- headers: {
1104
- // Manually set transfer-encoding for direct response, eg. app.handle, eden
1105
- 'transfer-encoding': 'chunked',
1106
- 'content-type': 'text/event-stream; charset=utf-8',
1107
- // ...set?.headers
1108
- },
1109
- },
1110
- )
1111
- }
108
+ private id: number = globalIndex++
109
+ private router: MedleyRouter = new OriginalRouter()
110
+ private middlewares: Function[] = []
111
+ private onErrorHandlers: OnError[] = []
112
+ private routes: InternalRoute[] = []
113
+ private defaultState: Record<any, any> = {}
114
+ private topLevelApp?: AnySpiceflow
115
+
116
+ /** @internal */
117
+ prefix?: string
118
+
119
+ /** @internal */
120
+ childrenApps: AnySpiceflow[] = []
121
+
122
+ /** @internal */
123
+ getAllRoutes() {
124
+ let root = this.topLevelApp || this
125
+ const allApps = bfs(root) || []
126
+ const allRoutes = allApps.flatMap((x) => {
127
+ const prefix = this.getAppAndParents(x)
128
+ .map((x) => x.prefix)
129
+ .join('')
130
+
131
+ return x.routes.map((x) => ({ ...x, path: prefix + x.path }))
132
+ })
133
+ return allRoutes
134
+ }
135
+
136
+ private add({
137
+ method,
138
+ path,
139
+ hooks,
140
+ handler,
141
+ ...rest
142
+ }: Partial<InternalRoute>) {
143
+ let bodySchema: TypeSchema = hooks?.body
144
+ let validateBody = getValidateFunction(bodySchema)
145
+ let validateQuery = getValidateFunction(hooks?.query)
146
+ let validateParams = getValidateFunction(hooks?.params)
147
+
148
+ const store = this.router.register(path)
149
+ let route: InternalRoute = {
150
+ ...rest,
151
+ method: (method || '') as any,
152
+ path: path || '',
153
+ handler: handler!,
154
+ hooks,
155
+ validateBody,
156
+ validateParams,
157
+ validateQuery,
158
+ }
159
+ this.routes.push(route)
160
+ store[method!] = route
161
+ }
162
+
163
+ private match(method: string, path: string) {
164
+ let root = this
165
+ let foundApp: AnySpiceflow | undefined
166
+ const result = bfsFind(this, (app) => {
167
+ app.topLevelApp = root
168
+ let prefix = this.getAppAndParents(app)
169
+ .map((x) => x.prefix)
170
+ .join('')
171
+ if (prefix && !path.startsWith(prefix)) {
172
+ return
173
+ }
174
+ let pathWithoutPrefix = path
175
+ if (prefix) {
176
+ pathWithoutPrefix = path.replace(prefix, '')
177
+ }
178
+ const medleyRoute = app.router.find(pathWithoutPrefix)
179
+ if (!medleyRoute) {
180
+ foundApp = app
181
+ return
182
+ }
183
+
184
+ let internalRoute: InternalRoute = medleyRoute.store[method]
185
+
186
+ if (internalRoute) {
187
+ const params = medleyRoute.params || {}
188
+
189
+ const res = {
190
+ app,
191
+ internalRoute: internalRoute,
192
+ params,
193
+ }
194
+ return res
195
+ }
196
+ if (method === 'HEAD') {
197
+ let internalRouteGet: InternalRoute = medleyRoute.store['GET']
198
+ if (!internalRouteGet?.handler) {
199
+ return
200
+ }
201
+ return {
202
+ app,
203
+ internalRoute: {
204
+ hooks: {},
205
+ handler: async (c) => {
206
+ const response = await internalRouteGet.handler(c)
207
+ if (response instanceof Response) {
208
+ return new Response('', {
209
+ status: response.status,
210
+ statusText: response.statusText,
211
+ headers: response.headers,
212
+ })
213
+ }
214
+ return new Response(null, { status: 200 })
215
+ },
216
+ method,
217
+ path,
218
+ } as InternalRoute,
219
+ params: medleyRoute.params,
220
+ }
221
+ }
222
+ })
223
+
224
+ return (
225
+ result || {
226
+ app: foundApp || root,
227
+ internalRoute: {
228
+ hooks: {},
229
+ handler: notFoundHandler,
230
+ method,
231
+ path,
232
+ } as InternalRoute,
233
+ params: {},
234
+ }
235
+ )
236
+ }
237
+
238
+ state<const Name extends string | number | symbol, Value>(
239
+ name: Name,
240
+ value: Value,
241
+ ): Spiceflow<
242
+ BasePath,
243
+ Scoped,
244
+ {
245
+ state: Reconcile<
246
+ Singleton['state'],
247
+ {
248
+ [name in Name]: Value
249
+ }
250
+ >
251
+ },
252
+ Definitions,
253
+ Metadata,
254
+ Routes
255
+ > {
256
+ this.defaultState[name] = value
257
+ return this as any
258
+ }
259
+
260
+ /**
261
+ * Create a new Router
262
+ * @param options {@link RouterOptions} {@link Platform}
263
+ */
264
+ constructor(
265
+ options: {
266
+ name?: string
267
+ scoped?: Scoped
268
+
269
+ basePath?: BasePath
270
+ } = {},
271
+ ) {
272
+ this.scoped = options.scoped
273
+
274
+ this.prefix = options.basePath
275
+ }
276
+
277
+ _routes: Routes = {} as any
278
+
279
+ _types = {
280
+ Prefix: '' as BasePath,
281
+ Scoped: false as Scoped,
282
+ Singleton: {} as Singleton,
283
+ Definitions: {} as Definitions,
284
+ Metadata: {} as Metadata,
285
+ }
286
+
287
+ post<
288
+ const Path extends string,
289
+ const LocalSchema extends InputSchema<keyof Definitions['type'] & string>,
290
+ const Schema extends UnwrapRoute<LocalSchema, Definitions['type']>,
291
+ const Handle extends InlineHandler<
292
+ Schema,
293
+ Singleton,
294
+ JoinPath<BasePath, Path>
295
+ >,
296
+ >(
297
+ path: Path,
298
+ handler: Handle,
299
+ hook?: LocalHook<
300
+ LocalSchema,
301
+ Schema,
302
+ Singleton,
303
+ Definitions['error'],
304
+ Metadata['macro'],
305
+ JoinPath<BasePath, Path>
306
+ >,
307
+ ): Spiceflow<
308
+ BasePath,
309
+ Scoped,
310
+ Singleton,
311
+ Definitions,
312
+ Metadata,
313
+ Routes &
314
+ CreateEden<
315
+ JoinPath<BasePath, Path>,
316
+ {
317
+ post: {
318
+ body: Schema['body']
319
+ params: undefined extends Schema['params']
320
+ ? ResolvePath<Path>
321
+ : Schema['params']
322
+ query: Schema['query']
323
+ response: ComposeSpiceflowResponse<Schema['response'], Handle>
324
+ }
325
+ }
326
+ >
327
+ > {
328
+ this.add({ method: 'POST', path, handler: handler, hooks: hook })
329
+
330
+ return this as any
331
+ }
332
+
333
+ get<
334
+ const Path extends string,
335
+ const LocalSchema extends InputSchema<keyof Definitions['type'] & string>,
336
+ const Schema extends UnwrapRoute<LocalSchema, Definitions['type']>,
337
+ const Macro extends Metadata['macro'],
338
+ const Handle extends InlineHandler<
339
+ Schema,
340
+ Singleton,
341
+ JoinPath<BasePath, Path>
342
+ >,
343
+ >(
344
+ path: Path,
345
+ handler: Handle,
346
+ hook?: LocalHook<
347
+ LocalSchema,
348
+ Schema,
349
+ Singleton,
350
+ Definitions['error'],
351
+ Macro,
352
+ JoinPath<BasePath, Path>
353
+ >,
354
+ ): Spiceflow<
355
+ BasePath,
356
+ Scoped,
357
+ Singleton,
358
+ Definitions,
359
+ Metadata,
360
+ Routes &
361
+ CreateEden<
362
+ JoinPath<BasePath, Path>,
363
+ {
364
+ get: {
365
+ body: Schema['body']
366
+ params: undefined extends Schema['params']
367
+ ? ResolvePath<Path>
368
+ : Schema['params']
369
+ query: Schema['query']
370
+
371
+ response: ComposeSpiceflowResponse<Schema['response'], Handle>
372
+ }
373
+ }
374
+ >
375
+ > {
376
+ this.add({ method: 'GET', path, handler: handler, hooks: hook })
377
+ return this as any
378
+ }
379
+
380
+ put<
381
+ const Path extends string,
382
+ const LocalSchema extends InputSchema<keyof Definitions['type'] & string>,
383
+ const Schema extends UnwrapRoute<LocalSchema, Definitions['type']>,
384
+ const Handle extends InlineHandler<
385
+ Schema,
386
+ Singleton,
387
+ JoinPath<BasePath, Path>
388
+ >,
389
+ >(
390
+ path: Path,
391
+ handler: Handle,
392
+ hook?: LocalHook<
393
+ LocalSchema,
394
+ Schema,
395
+ Singleton,
396
+ Definitions['error'],
397
+ Metadata['macro'],
398
+ JoinPath<BasePath, Path>
399
+ >,
400
+ ): Spiceflow<
401
+ BasePath,
402
+ Scoped,
403
+ Singleton,
404
+ Definitions,
405
+ Metadata,
406
+ Routes &
407
+ CreateEden<
408
+ JoinPath<BasePath, Path>,
409
+ {
410
+ put: {
411
+ body: Schema['body']
412
+ params: undefined extends Schema['params']
413
+ ? ResolvePath<Path>
414
+ : Schema['params']
415
+ query: Schema['query']
416
+
417
+ response: ComposeSpiceflowResponse<Schema['response'], Handle>
418
+ }
419
+ }
420
+ >
421
+ > {
422
+ this.add({ method: 'PUT', path, handler: handler, hooks: hook })
423
+
424
+ return this as any
425
+ }
426
+
427
+ patch<
428
+ const Path extends string,
429
+ const LocalSchema extends InputSchema<keyof Definitions['type'] & string>,
430
+ const Schema extends UnwrapRoute<LocalSchema, Definitions['type']>,
431
+ const Handle extends InlineHandler<
432
+ Schema,
433
+ Singleton,
434
+ JoinPath<BasePath, Path>
435
+ >,
436
+ >(
437
+ path: Path,
438
+ handler: Handle,
439
+ hook?: LocalHook<
440
+ LocalSchema,
441
+ Schema,
442
+ Singleton,
443
+ Definitions['error'],
444
+ Metadata['macro'],
445
+ JoinPath<BasePath, Path>
446
+ >,
447
+ ): Spiceflow<
448
+ BasePath,
449
+ Scoped,
450
+ Singleton,
451
+ Definitions,
452
+ Metadata,
453
+ Routes &
454
+ CreateEden<
455
+ JoinPath<BasePath, Path>,
456
+ {
457
+ patch: {
458
+ body: Schema['body']
459
+ params: undefined extends Schema['params']
460
+ ? ResolvePath<Path>
461
+ : Schema['params']
462
+ query: Schema['query']
463
+
464
+ response: ComposeSpiceflowResponse<Schema['response'], Handle>
465
+ }
466
+ }
467
+ >
468
+ > {
469
+ this.add({ method: 'PATCH', path, handler: handler, hooks: hook })
470
+
471
+ return this as any
472
+ }
473
+
474
+ delete<
475
+ const Path extends string,
476
+ const LocalSchema extends InputSchema<keyof Definitions['type'] & string>,
477
+ const Schema extends UnwrapRoute<LocalSchema, Definitions['type']>,
478
+ const Handle extends InlineHandler<
479
+ Schema,
480
+ Singleton,
481
+ JoinPath<BasePath, Path>
482
+ >,
483
+ >(
484
+ path: Path,
485
+ handler: Handle,
486
+ hook?: LocalHook<
487
+ LocalSchema,
488
+ Schema,
489
+ Singleton,
490
+ Definitions['error'],
491
+ Metadata['macro'],
492
+ JoinPath<BasePath, Path>
493
+ >,
494
+ ): Spiceflow<
495
+ BasePath,
496
+ Scoped,
497
+ Singleton,
498
+ Definitions,
499
+ Metadata,
500
+ Routes &
501
+ CreateEden<
502
+ JoinPath<BasePath, Path>,
503
+ {
504
+ delete: {
505
+ body: Schema['body']
506
+ params: undefined extends Schema['params']
507
+ ? ResolvePath<Path>
508
+ : Schema['params']
509
+ query: Schema['query']
510
+
511
+ response: ComposeSpiceflowResponse<Schema['response'], Handle>
512
+ }
513
+ }
514
+ >
515
+ > {
516
+ this.add({ method: 'DELETE', path, handler: handler, hooks: hook })
517
+
518
+ return this as any
519
+ }
520
+
521
+ options<
522
+ const Path extends string,
523
+ const LocalSchema extends InputSchema<keyof Definitions['type'] & string>,
524
+ const Schema extends UnwrapRoute<LocalSchema, Definitions['type']>,
525
+ const Handle extends InlineHandler<
526
+ Schema,
527
+ Singleton,
528
+ JoinPath<BasePath, Path>
529
+ >,
530
+ >(
531
+ path: Path,
532
+ handler: Handle,
533
+ hook?: LocalHook<
534
+ LocalSchema,
535
+ Schema,
536
+ Singleton,
537
+ Definitions['error'],
538
+ Metadata['macro'],
539
+ JoinPath<BasePath, Path>
540
+ >,
541
+ ): Spiceflow<
542
+ BasePath,
543
+ Scoped,
544
+ Singleton,
545
+ Definitions,
546
+ Metadata,
547
+ Routes &
548
+ CreateEden<
549
+ JoinPath<BasePath, Path>,
550
+ {
551
+ options: {
552
+ body: Schema['body']
553
+ params: undefined extends Schema['params']
554
+ ? ResolvePath<Path>
555
+ : Schema['params']
556
+ query: Schema['query']
557
+
558
+ response: ComposeSpiceflowResponse<Schema['response'], Handle>
559
+ }
560
+ }
561
+ >
562
+ > {
563
+ this.add({ method: 'OPTIONS', path, handler: handler, hooks: hook })
564
+
565
+ return this as any
566
+ }
567
+
568
+ all<
569
+ const Path extends string,
570
+ const LocalSchema extends InputSchema<keyof Definitions['type'] & string>,
571
+ const Schema extends UnwrapRoute<LocalSchema, Definitions['type']>,
572
+ const Handle extends InlineHandler<
573
+ Schema,
574
+ Singleton,
575
+ JoinPath<BasePath, Path>
576
+ >,
577
+ >(
578
+ path: Path,
579
+ handler: Handle,
580
+ hook?: LocalHook<
581
+ LocalSchema,
582
+ Schema,
583
+ Singleton,
584
+ Definitions['error'],
585
+ Metadata['macro'],
586
+ JoinPath<BasePath, Path>
587
+ >,
588
+ ): Spiceflow<
589
+ BasePath,
590
+ Scoped,
591
+ Singleton,
592
+ Definitions,
593
+ Metadata,
594
+ Routes &
595
+ CreateEden<
596
+ JoinPath<BasePath, Path>,
597
+ {
598
+ [method in string]: {
599
+ body: Schema['body']
600
+ params: undefined extends Schema['params']
601
+ ? ResolvePath<Path>
602
+ : Schema['params']
603
+ query: Schema['query']
604
+
605
+ response: ComposeSpiceflowResponse<Schema['response'], Handle>
606
+ }
607
+ }
608
+ >
609
+ > {
610
+ for (const method of METHODS) {
611
+ this.add({ method, path, handler: handler, hooks: hook })
612
+ }
613
+
614
+ return this as any
615
+ }
616
+
617
+ head<
618
+ const Path extends string,
619
+ const LocalSchema extends InputSchema<keyof Definitions['type'] & string>,
620
+ const Schema extends UnwrapRoute<LocalSchema, Definitions['type']>,
621
+ const Handle extends InlineHandler<
622
+ Schema,
623
+ Singleton,
624
+ JoinPath<BasePath, Path>
625
+ >,
626
+ >(
627
+ path: Path,
628
+ handler: Handle,
629
+ hook?: LocalHook<
630
+ LocalSchema,
631
+ Schema,
632
+ Singleton,
633
+ Definitions['error'],
634
+ Metadata['macro'],
635
+ JoinPath<BasePath, Path>
636
+ >,
637
+ ): Spiceflow<
638
+ BasePath,
639
+ Scoped,
640
+ Singleton,
641
+ Definitions,
642
+ Metadata,
643
+ Routes &
644
+ CreateEden<
645
+ JoinPath<BasePath, Path>,
646
+ {
647
+ head: {
648
+ body: Schema['body']
649
+ params: undefined extends Schema['params']
650
+ ? ResolvePath<Path>
651
+ : Schema['params']
652
+ query: Schema['query']
653
+
654
+ response: ComposeSpiceflowResponse<Schema['response'], Handle>
655
+ }
656
+ }
657
+ >
658
+ > {
659
+ this.add({ method: 'HEAD', path, handler: handler, hooks: hook })
660
+
661
+ return this as any
662
+ }
663
+
664
+ private scoped?: Scoped = true as Scoped
665
+
666
+ use<const NewSpiceflow extends AnySpiceflow>(
667
+ instance: NewSpiceflow,
668
+ ): IsAny<NewSpiceflow> extends true
669
+ ? this
670
+ : Spiceflow<
671
+ BasePath,
672
+ Scoped,
673
+ Singleton,
674
+ Definitions,
675
+ Metadata,
676
+ BasePath extends ``
677
+ ? Routes & NewSpiceflow['_routes']
678
+ : Routes & CreateEden<BasePath, NewSpiceflow['_routes']>
679
+ >
680
+ use<const Schema extends RouteSchema>(
681
+ handler: MiddlewareHandler<
682
+ Schema,
683
+ {
684
+ state: Singleton['state']
685
+ }
686
+ >,
687
+ ): this
688
+
689
+ use(appOrHandler) {
690
+ if (appOrHandler instanceof Spiceflow) {
691
+ this.childrenApps.push(appOrHandler)
692
+ } else if (typeof appOrHandler === 'function') {
693
+ this.middlewares ??= []
694
+ this.middlewares.push(appOrHandler)
695
+ }
696
+ return this
697
+ }
698
+
699
+ onError<const Schema extends RouteSchema>(
700
+ handler: MaybeArray<ErrorHandler<Definitions['error'], Schema, Singleton>>,
701
+ ): this {
702
+ this.onErrorHandlers ??= []
703
+ this.onErrorHandlers.push(handler as any)
704
+
705
+ return this
706
+ }
707
+
708
+ async handle(request: Request): Promise<Response> {
709
+ let u = new URL(request.url, 'http://localhost')
710
+ let path = u.pathname + u.search
711
+ const defaultContext = {
712
+ redirect,
713
+ error: null,
714
+ path,
715
+ }
716
+ const root = this.topLevelApp || this
717
+ let onErrorHandlers: OnError[] = []
718
+ try {
719
+ // Get all middleware and method specific routes in order
720
+
721
+ const route = this.match(request.method, path)
722
+
723
+ const appsInScope = this.getAppsInScope(route.app)
724
+ onErrorHandlers = appsInScope.flatMap((x) => x.onErrorHandlers)
725
+ let {
726
+ params: _params,
727
+ app: { defaultState },
728
+ } = route
729
+ const middlewares = appsInScope.flatMap((x) => x.middlewares)
730
+ // console.log({ onReqHandlers })
731
+ let state = structuredClone(defaultState)
732
+
733
+ let content = route?.internalRoute?.hooks?.content
734
+
735
+ if (route.internalRoute?.validateBody) {
736
+ // TODO don't clone the request
737
+ let typedRequest =
738
+ request instanceof SpiceflowRequest
739
+ ? request
740
+ : new SpiceflowRequest(u, request)
741
+ typedRequest.validateBody = route.internalRoute?.validateBody
742
+ request = typedRequest
743
+ }
744
+
745
+ let index = 0
746
+ let context = {
747
+ ...defaultContext,
748
+ request,
749
+ state,
750
+ path,
751
+ query: parseQuery.parse((u.search || '').slice(1)),
752
+ params: _params,
753
+ redirect,
754
+ } satisfies MiddlewareContext<any>
755
+ let handlerResponse: Response | undefined
756
+ const next = async () => {
757
+ if (index < middlewares.length) {
758
+ const middleware = middlewares[index]
759
+ index++
760
+
761
+ const result = await middleware(context, next)
762
+ if (result instanceof Response) {
763
+ handlerResponse = result
764
+ }
765
+ if (!result && index < middlewares.length) {
766
+ return await next()
767
+ } else if (result) {
768
+ return await turnHandlerResultIntoResponse(result)
769
+ }
770
+ }
771
+ if (handlerResponse) {
772
+ return handlerResponse
773
+ }
774
+
775
+ context.query = runValidation(
776
+ context.query,
777
+ route.internalRoute?.validateQuery,
778
+ )
779
+ context.params = runValidation(
780
+ context.params,
781
+ route.internalRoute?.validateParams,
782
+ )
783
+
784
+ const res = route.internalRoute?.handler(context)
785
+ if (isAsyncIterable(res)) {
786
+ handlerResponse = await this.handleStream({
787
+ generator: res,
788
+ request,
789
+ onErrorHandlers,
790
+ })
791
+ return handlerResponse
792
+ }
793
+
794
+ handlerResponse = await turnHandlerResultIntoResponse(res)
795
+ return handlerResponse
796
+ }
797
+ const response = await next()
798
+
799
+ return response
800
+ } catch (err: any) {
801
+ if (err instanceof Response) return err
802
+ let res = await this.runErrorHandlers({
803
+ onErrorHandlers,
804
+ error: err,
805
+ request,
806
+ })
807
+ if (res instanceof Response) return res
808
+
809
+ let status = err?.status ?? 500
810
+ res ||= new Response(err?.message || 'Internal Server Error', {
811
+ status,
812
+ })
813
+ return res
814
+ }
815
+ }
816
+
817
+ private async runErrorHandlers({
818
+ onErrorHandlers = [] as OnError[],
819
+ error: err,
820
+ request,
821
+ }) {
822
+ if (onErrorHandlers.length === 0) {
823
+ console.error(`Spiceflow unhandled error:`, err)
824
+ } else {
825
+ for (const errHandler of onErrorHandlers) {
826
+ const res = errHandler({ error: err, request })
827
+ if (res instanceof Response) {
828
+ return res
829
+ }
830
+ }
831
+ }
832
+ }
833
+
834
+ private getAppAndParents(currentApp?: AnySpiceflow) {
835
+ let root = this.topLevelApp || this
836
+
837
+ if (!root.childrenApps.length) {
838
+ return [root]
839
+ }
840
+ const parents: AnySpiceflow[] = []
841
+ let current = currentApp
842
+
843
+ const parentMap = new Map<number, AnySpiceflow>()
844
+ bfsFind(root, (node) => {
845
+ for (const child of node.childrenApps) {
846
+ parentMap.set(child.id, node)
847
+ }
848
+ })
849
+
850
+ // Traverse the parent map to get the parents
851
+ while (current) {
852
+ parents.unshift(current)
853
+ current = parentMap.get(current.id)
854
+ }
855
+
856
+ return parents.filter((x) => x !== undefined)
857
+ }
858
+
859
+ private getAppsInScope(currentApp?: AnySpiceflow) {
860
+ let root = this.topLevelApp || this
861
+ if (!root.childrenApps.length) {
862
+ return [root]
863
+ }
864
+ const withParents = this.getAppAndParents(currentApp)
865
+
866
+ const wantedOrder = bfs(root)
867
+ const scopeFalseApps = wantedOrder.filter((x) => x.scoped === false)
868
+ let appsInScope = [] as AnySpiceflow[]
869
+ for (const app of wantedOrder) {
870
+ if (scopeFalseApps.includes(app)) {
871
+ appsInScope.push(app)
872
+ continue
873
+ }
874
+ if (withParents.includes(app)) {
875
+ appsInScope.push(app)
876
+ continue
877
+ }
878
+ }
879
+ return appsInScope
880
+ }
881
+
882
+ async listen(port: number, hostname: string = '127.0.0.1') {
883
+ if (typeof Bun !== 'undefined') {
884
+ const server = Bun.serve({
885
+ port,
886
+ development: !isProduction,
887
+ hostname,
888
+ reusePort: true,
889
+ error(error) {
890
+ console.error(error)
891
+ return new Response('Internal Server Error', {
892
+ status: 500,
893
+ })
894
+ },
895
+
896
+ fetch: async (request) => {
897
+ const res = await this.handle(request)
898
+ return res
899
+ },
900
+ })
901
+ process.on('beforeExit', () => {
902
+ server.stop()
903
+ })
904
+ console.log(`Listening on http://localhost:${port}`)
905
+ return server
906
+ }
907
+ return this.listenNode(port, hostname)
908
+ }
909
+
910
+ async listenNode(port: number, hostname: string = '0.0.0.0') {
911
+ const { Readable } = await import('stream')
912
+ const { createServer } = await import('http')
913
+
914
+ const server = createServer(async (req, res) => {
915
+ const abortController = new AbortController()
916
+ const { signal } = abortController
917
+
918
+ req.on('error', (err) => {
919
+ abortController.abort()
920
+ })
921
+ req.on('aborted', (err) => {
922
+ abortController.abort()
923
+ })
924
+ // this is how you see when a request is aborted in Node.js, laughable
925
+ res.on('close', function () {
926
+ let aborted = !res.writableFinished
927
+ if (aborted) {
928
+ abortController.abort()
929
+ }
930
+ })
931
+
932
+ const url = new URL(
933
+ req.url || '',
934
+ `http://${req.headers.host || hostname || 'localhost'}`,
935
+ )
936
+ const typedRequest = new SpiceflowRequest(url.toString(), {
937
+ method: req.method,
938
+ headers: req.headers as HeadersInit,
939
+ body:
940
+ req.method !== 'GET' && req.method !== 'HEAD'
941
+ ? (Readable.toWeb(req) as any)
942
+ : null,
943
+ signal,
944
+ // @ts-ignore
945
+ duplex: 'half',
946
+ // keepalive: true,
947
+ })
948
+
949
+ try {
950
+ const response = await this.handle(typedRequest)
951
+
952
+ res.statusCode = response.status
953
+ for (const [key, value] of response.headers) {
954
+ res.setHeader(key, value)
955
+ }
956
+
957
+ if (response.body) {
958
+ const reader = response.body.getReader()
959
+ while (true) {
960
+ const { done, value } = await reader.read()
961
+ if (done) break
962
+ res.write(value)
963
+ }
964
+ }
965
+ res.end()
966
+ } catch (error) {
967
+ console.error('Error handling request:', error)
968
+ res.statusCode = 500
969
+ res.end('Internal Server Error')
970
+ }
971
+ })
972
+
973
+ await new Promise((resolve, reject) => {
974
+ server.listen(port, hostname, () => {
975
+ console.log(`Listening on http://localhost:${port}`)
976
+ resolve(null)
977
+ })
978
+ })
979
+
980
+ return server
981
+ }
982
+
983
+ private async handleStream({
984
+ onErrorHandlers,
985
+ generator,
986
+ request,
987
+ }: {
988
+ generator: Generator | AsyncGenerator
989
+ onErrorHandlers: OnError[]
990
+ request: Request
991
+ }) {
992
+ let init = generator.next()
993
+ if (init instanceof Promise) init = await init
994
+
995
+ if (init?.done) {
996
+ return await turnHandlerResultIntoResponse(init.value)
997
+ }
998
+ // let errorHandlers = this.routerTree.onErrorHandlers
999
+ let self = this
1000
+ return new Response(
1001
+ new ReadableStream({
1002
+ async start(controller) {
1003
+ let end = false
1004
+
1005
+ request?.signal.addEventListener('abort', () => {
1006
+ end = true
1007
+
1008
+ try {
1009
+ controller.close()
1010
+ } catch {
1011
+ // nothing
1012
+ }
1013
+ })
1014
+
1015
+ if (init?.value !== undefined && init?.value !== null)
1016
+ controller.enqueue(
1017
+ Buffer.from(
1018
+ `event: message\ndata: ${JSON.stringify(init.value)}\n\n`,
1019
+ ),
1020
+ )
1021
+
1022
+ try {
1023
+ for await (const chunk of generator) {
1024
+ if (end) break
1025
+ if (chunk === undefined || chunk === null) continue
1026
+
1027
+ controller.enqueue(
1028
+ Buffer.from(
1029
+ `event: message\ndata: ${JSON.stringify(chunk)}\n\n`,
1030
+ ),
1031
+ )
1032
+ }
1033
+ } catch (error: any) {
1034
+ let res = await self.runErrorHandlers({
1035
+ onErrorHandlers: onErrorHandlers,
1036
+ error,
1037
+ request,
1038
+ })
1039
+ controller.enqueue(
1040
+ Buffer.from(
1041
+ `event: error\ndata: ${JSON.stringify(
1042
+ error.message || error.name || 'Error',
1043
+ )}\n\n`,
1044
+ ),
1045
+ )
1046
+ }
1047
+
1048
+ try {
1049
+ controller.close()
1050
+ } catch {
1051
+ // nothing
1052
+ }
1053
+ },
1054
+ }),
1055
+ {
1056
+ // TODO add headers somehow
1057
+ headers: {
1058
+ // Manually set transfer-encoding for direct response, eg. app.handle, eden
1059
+ 'transfer-encoding': 'chunked',
1060
+ 'content-type': 'text/event-stream; charset=utf-8',
1061
+ // ...set?.headers
1062
+ },
1063
+ },
1064
+ )
1065
+ }
1112
1066
  }
1113
1067
 
1114
1068
  // async function getRequestBody({
@@ -1166,119 +1120,121 @@ export class Spiceflow<
1166
1120
  // }
1167
1121
 
1168
1122
  const METHODS = [
1169
- 'ALL',
1170
- 'CONNECT',
1171
- 'DELETE',
1172
- 'GET',
1173
- 'HEAD',
1174
- 'OPTIONS',
1175
- 'PATCH',
1176
- 'POST',
1177
- 'PUT',
1178
- 'TRACE',
1123
+ 'ALL',
1124
+ 'CONNECT',
1125
+ 'DELETE',
1126
+ 'GET',
1127
+ 'HEAD',
1128
+ 'OPTIONS',
1129
+ 'PATCH',
1130
+ 'POST',
1131
+ 'PUT',
1132
+ 'TRACE',
1179
1133
  ] as const
1180
1134
 
1181
1135
  /** HTTP method string */
1182
1136
  export type Method = (typeof METHODS)[number]
1183
1137
 
1184
1138
  function bfsFind<T>(
1185
- tree: AnySpiceflow,
1186
- onNode: (node: AnySpiceflow) => T | undefined | void,
1139
+ tree: AnySpiceflow,
1140
+ onNode: (node: AnySpiceflow) => T | undefined | void,
1187
1141
  ): T | undefined {
1188
- const queue = [tree]
1189
-
1190
- while (queue.length > 0) {
1191
- const node = queue.shift()!
1192
-
1193
- const result = onNode(node)
1194
- if (result) {
1195
- return result
1196
- }
1197
- queue.push(...node.childrenApps)
1198
- }
1199
- return
1142
+ const queue = [tree]
1143
+
1144
+ while (queue.length > 0) {
1145
+ const node = queue.shift()!
1146
+
1147
+ const result = onNode(node)
1148
+ if (result) {
1149
+ return result
1150
+ }
1151
+ queue.push(...node.childrenApps)
1152
+ }
1153
+ return
1200
1154
  }
1201
1155
  export class SpiceflowRequest<T = any> extends Request {
1202
- validateBody?: ValidateFunction
1156
+ validateBody?: ValidateFunction
1203
1157
 
1204
- async json(): Promise<T> {
1205
- const body = (await super.json()) as Promise<T>
1206
- return runValidation(body, this.validateBody)
1207
- }
1158
+ async json(): Promise<T> {
1159
+ const body = (await super.json()) as Promise<T>
1160
+ return runValidation(body, this.validateBody)
1161
+ }
1208
1162
  }
1209
1163
 
1210
1164
  export function bfs(tree: AnySpiceflow) {
1211
- const queue = [tree]
1212
- let nodes: AnySpiceflow[] = []
1213
- while (queue.length > 0) {
1214
- const node = queue.shift()!
1215
- if (node) {
1216
- nodes.push(node)
1217
- }
1218
- // const result = onNode(node)
1219
-
1220
- if (node?.childrenApps?.length) {
1221
- queue.push(...node.childrenApps)
1222
- }
1223
- }
1224
- return nodes
1165
+ const queue = [tree]
1166
+ let nodes: AnySpiceflow[] = []
1167
+ while (queue.length > 0) {
1168
+ const node = queue.shift()!
1169
+ if (node) {
1170
+ nodes.push(node)
1171
+ }
1172
+ // const result = onNode(node)
1173
+
1174
+ if (node?.childrenApps?.length) {
1175
+ queue.push(...node.childrenApps)
1176
+ }
1177
+ }
1178
+ return nodes
1225
1179
  }
1226
1180
  export async function turnHandlerResultIntoResponse(result: any) {
1227
- // if (result === undefined) return new Response('', { status: 404 })
1228
- // if user returns not a response, convert to json
1229
- if (result instanceof Response) {
1230
- return result
1231
- }
1232
- // if user returns a promise, await it
1233
- if (result instanceof Promise) {
1234
- result = await result
1235
- }
1236
- // // if user returns a string, convert to json
1237
- // if (typeof result === 'string') {
1238
- // result = new Response(result)
1239
- // }
1240
- // if user returns an object, convert to json
1241
-
1242
- return new Response(JSON.stringify(result ?? null, null, 2), {
1243
- headers: {
1244
- 'content-type': 'application/json',
1245
- },
1246
- })
1181
+ // if (result === undefined) return new Response('', { status: 404 })
1182
+ // if user returns not a response, convert to json
1183
+ if (result instanceof Response) {
1184
+ return result
1185
+ }
1186
+ // if user returns a promise, await it
1187
+ if (result instanceof Promise) {
1188
+ result = await result
1189
+ }
1190
+ // // if user returns a string, convert to json
1191
+ // if (typeof result === 'string') {
1192
+ // result = new Response(result)
1193
+ // }
1194
+ // if user returns an object, convert to json
1195
+
1196
+ return new Response(JSON.stringify(result ?? null, null, 2), {
1197
+ headers: {
1198
+ 'content-type': 'application/json',
1199
+ },
1200
+ })
1247
1201
  }
1248
1202
 
1249
1203
  export type AnySpiceflow = Spiceflow<any, any, any, any, any, any>
1250
1204
 
1251
1205
  export function isZodSchema(value: unknown): value is ZodType {
1252
- return (
1253
- value instanceof z.ZodType ||
1254
- (typeof value === 'object' &&
1255
- value !== null &&
1256
- 'parse' in value &&
1257
- 'safeParse' in value &&
1258
- 'optional' in value &&
1259
- 'nullable' in value)
1260
- )
1206
+ return (
1207
+ value instanceof z.ZodType ||
1208
+ (typeof value === 'object' &&
1209
+ value !== null &&
1210
+ 'parse' in value &&
1211
+ 'safeParse' in value &&
1212
+ 'optional' in value &&
1213
+ 'nullable' in value)
1214
+ )
1261
1215
  }
1262
1216
 
1263
1217
  function getValidateFunction(schema: TypeSchema) {
1264
- if (isZodSchema(schema)) {
1265
- let jsonSchema = zodToJsonSchema(schema, {})
1266
- return ajv.compile(jsonSchema)
1267
- }
1268
-
1269
- if (schema) {
1270
- return ajv.compile(schema)
1271
- }
1218
+ if (isZodSchema(schema)) {
1219
+ let jsonSchema = zodToJsonSchema(schema, {
1220
+ removeAdditionalStrategy: 'strict',
1221
+ })
1222
+ return ajv.compile(jsonSchema)
1223
+ }
1224
+
1225
+ if (schema) {
1226
+ return ajv.compile(schema)
1227
+ }
1272
1228
  }
1273
1229
 
1274
1230
  function runValidation(value: any, validate?: ValidateFunction) {
1275
- if (!validate) return value
1276
- const valid = validate(value)
1277
- if (!valid) {
1278
- const error = ajv.errorsText(validate.errors, {
1279
- separator: '\n',
1280
- })
1281
- throw new ValidationError(error)
1282
- }
1283
- return value
1231
+ if (!validate) return value
1232
+ const valid = validate(value)
1233
+ if (!valid) {
1234
+ const error = ajv.errorsText(validate.errors, {
1235
+ separator: '\n',
1236
+ })
1237
+ throw new ValidationError(error)
1238
+ }
1239
+ return value
1284
1240
  }