spiceflow 1.1.7 → 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 (61) 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 +65 -0
  21. package/dist/middleware.test.js.map +1 -1
  22. package/dist/openapi.d.ts.map +1 -1
  23. package/dist/openapi.js +1 -1
  24. package/dist/openapi.js.map +1 -1
  25. package/dist/openapi.test.js.map +1 -1
  26. package/dist/spiceflow.d.ts.map +1 -1
  27. package/dist/spiceflow.js +12 -5
  28. package/dist/spiceflow.js.map +1 -1
  29. package/dist/spiceflow.test.js.map +1 -1
  30. package/dist/stream.test.js +6 -6
  31. package/dist/stream.test.js.map +1 -1
  32. package/dist/types.d.ts +1 -1
  33. package/dist/types.d.ts.map +1 -1
  34. package/dist/types.js.map +1 -1
  35. package/dist/types.test.js +56 -6
  36. package/dist/types.test.js.map +1 -1
  37. package/dist/utils.d.ts.map +1 -1
  38. package/dist/utils.js.map +1 -1
  39. package/dist/zod.test.js.map +1 -1
  40. package/package.json +1 -1
  41. package/src/benchmark.benchmark.ts +8 -8
  42. package/src/client/errors.ts +17 -17
  43. package/src/client/index.ts +437 -469
  44. package/src/client/types.ts +168 -191
  45. package/src/client/utils.ts +5 -5
  46. package/src/client/ws.ts +87 -89
  47. package/src/client.test.ts +176 -183
  48. package/src/context.ts +82 -82
  49. package/src/cors.test.ts +38 -38
  50. package/src/cors.ts +87 -92
  51. package/src/error.ts +13 -13
  52. package/src/middleware.test.ts +221 -149
  53. package/src/openapi.test.ts +97 -97
  54. package/src/openapi.ts +365 -365
  55. package/src/spiceflow.test.ts +461 -467
  56. package/src/spiceflow.ts +1117 -1157
  57. package/src/stream.test.ts +310 -310
  58. package/src/types.test.ts +59 -39
  59. package/src/types.ts +698 -701
  60. package/src/utils.ts +79 -79
  61. 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,1047 +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: defaultStore },
770
- } = route
771
- const middlewares = appsInScope.flatMap((x) => x.middlewares)
772
- // console.log({ onReqHandlers })
773
- let state = { ...defaultStore }
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
-
805
- if (!result && index < middlewares.length) {
806
- return await next()
807
- } else if (result) {
808
- return await turnHandlerResultIntoResponse(result)
809
- }
810
- }
811
- if (handlerResponse) {
812
- return handlerResponse
813
- }
814
-
815
- context.query = runValidation(
816
- context.query,
817
- route.internalRoute?.validateQuery,
818
- )
819
- context.params = runValidation(
820
- context.params,
821
- route.internalRoute?.validateParams,
822
- )
823
-
824
- const res = route.internalRoute?.handler(context)
825
- if (isAsyncIterable(res)) {
826
- handlerResponse = await this.handleStream({
827
- generator: res,
828
- request,
829
- onErrorHandlers,
830
- })
831
- return handlerResponse
832
- }
833
-
834
- handlerResponse = await turnHandlerResultIntoResponse(res)
835
- return handlerResponse
836
- }
837
- const response = await next()
838
-
839
- return response
840
- } catch (err: any) {
841
- if (err instanceof Response) return err
842
- let res = await this.runErrorHandlers({
843
- onErrorHandlers,
844
- error: err,
845
- request,
846
- })
847
- if (res instanceof Response) return res
848
-
849
- let status = err?.status ?? 500
850
- res ||= new Response(err?.message || 'Internal Server Error', {
851
- status,
852
- })
853
- return res
854
- }
855
- }
856
-
857
- private async runErrorHandlers({
858
- onErrorHandlers = [] as OnError[],
859
- error: err,
860
- request,
861
- }) {
862
- if (onErrorHandlers.length === 0) {
863
- console.error(`Spiceflow unhandled error:`, err)
864
- } else {
865
- for (const errHandler of onErrorHandlers) {
866
- const res = errHandler({ error: err, request })
867
- if (res instanceof Response) {
868
- return res
869
- }
870
- }
871
- }
872
- }
873
-
874
- private getAppAndParents(currentApp?: AnySpiceflow) {
875
- let root = this.topLevelApp || this
876
-
877
- if (!root.childrenApps.length) {
878
- return [root]
879
- }
880
- const parents: AnySpiceflow[] = []
881
- let current = currentApp
882
-
883
- const parentMap = new Map<number, AnySpiceflow>()
884
- bfsFind(root, (node) => {
885
- for (const child of node.childrenApps) {
886
- parentMap.set(child.id, node)
887
- }
888
- })
889
-
890
- // Traverse the parent map to get the parents
891
- while (current) {
892
- parents.unshift(current)
893
- current = parentMap.get(current.id)
894
- }
895
-
896
- return parents.filter((x) => x !== undefined)
897
- }
898
-
899
- private getAppsInScope(currentApp?: AnySpiceflow) {
900
- let root = this.topLevelApp || this
901
- if (!root.childrenApps.length) {
902
- return [root]
903
- }
904
- const withParents = this.getAppAndParents(currentApp)
905
-
906
- const wantedOrder = bfs(root)
907
- const scopeFalseApps = wantedOrder.filter((x) => x.scoped === false)
908
- let appsInScope = [] as AnySpiceflow[]
909
- for (const app of wantedOrder) {
910
- if (scopeFalseApps.includes(app)) {
911
- appsInScope.push(app)
912
- continue
913
- }
914
- if (withParents.includes(app)) {
915
- appsInScope.push(app)
916
- continue
917
- }
918
- }
919
- return appsInScope
920
- }
921
-
922
- async listen(port: number, hostname: string = '127.0.0.1') {
923
- if (typeof Bun !== 'undefined') {
924
- const server = Bun.serve({
925
- port,
926
- development: !isProduction,
927
- hostname,
928
- reusePort: true,
929
- error(error) {
930
- console.error(error)
931
- return new Response('Internal Server Error', {
932
- status: 500,
933
- })
934
- },
935
-
936
- fetch: async (request) => {
937
- const res = await this.handle(request)
938
- return res
939
- },
940
- })
941
- process.on('beforeExit', () => {
942
- server.stop()
943
- })
944
- console.log(`Listening on http://localhost:${port}`)
945
- return server
946
- }
947
- return this.listenNode(port, hostname)
948
- }
949
-
950
- async listenNode(port: number, hostname: string = '0.0.0.0') {
951
- const { Readable } = await import('stream')
952
- const { createServer } = await import('http')
953
-
954
- const server = createServer(async (req, res) => {
955
- const abortController = new AbortController()
956
- const { signal } = abortController
957
-
958
- req.on('close', () => abortController.abort())
959
- req.on('error', (err) => {
960
- abortController.abort()
961
- })
962
- req.on('aborted', (err) => {
963
- abortController.abort()
964
- })
965
- // this is how you see when a request is aborted in Node.js, laughable
966
- res.on('close', function () {
967
- let aborted = !res.writableFinished
968
- if (aborted) {
969
- abortController.abort()
970
- }
971
- })
972
-
973
- const url = new URL(
974
- req.url || '',
975
- `http://${req.headers.host || hostname || 'localhost'}`,
976
- )
977
- const typedRequest = new SpiceflowRequest(url.toString(), {
978
- method: req.method,
979
- headers: req.headers as HeadersInit,
980
- body:
981
- req.method !== 'GET' && req.method !== 'HEAD'
982
- ? (Readable.toWeb(req) as any)
983
- : null,
984
- signal,
985
- })
986
-
987
- try {
988
- const response = await this.handle(typedRequest)
989
-
990
- res.statusCode = response.status
991
- for (const [key, value] of response.headers) {
992
- res.setHeader(key, value)
993
- }
994
-
995
- if (response.body) {
996
- const reader = response.body.getReader()
997
- while (true) {
998
- const { done, value } = await reader.read()
999
- if (done) break
1000
- res.write(value)
1001
- }
1002
- }
1003
- res.end()
1004
- } catch (error) {
1005
- console.error('Error handling request:', error)
1006
- res.statusCode = 500
1007
- res.end('Internal Server Error')
1008
- }
1009
- })
1010
-
1011
- await new Promise((resolve, reject) => {
1012
- server.listen(port, hostname, () => {
1013
- console.log(`Listening on http://localhost:${port}`)
1014
- resolve(null)
1015
- })
1016
- })
1017
-
1018
- return server
1019
- }
1020
-
1021
- private async handleStream({
1022
- onErrorHandlers,
1023
- generator,
1024
- request,
1025
- }: {
1026
- generator: Generator | AsyncGenerator
1027
- onErrorHandlers: OnError[]
1028
- request: Request
1029
- }) {
1030
- let init = generator.next()
1031
- if (init instanceof Promise) init = await init
1032
-
1033
- if (init?.done) {
1034
- return await turnHandlerResultIntoResponse(init.value)
1035
- }
1036
- // let errorHandlers = this.routerTree.onErrorHandlers
1037
- let self = this
1038
- return new Response(
1039
- new ReadableStream({
1040
- async start(controller) {
1041
- let end = false
1042
-
1043
- request?.signal.addEventListener('abort', () => {
1044
- end = true
1045
-
1046
- try {
1047
- controller.close()
1048
- } catch {
1049
- // nothing
1050
- }
1051
- })
1052
-
1053
- if (init?.value !== undefined && init?.value !== null)
1054
- controller.enqueue(
1055
- Buffer.from(
1056
- `event: message\ndata: ${JSON.stringify(
1057
- init.value,
1058
- )}\n\n`,
1059
- ),
1060
- )
1061
-
1062
- try {
1063
- for await (const chunk of generator) {
1064
- if (end) break
1065
- if (chunk === undefined || chunk === null) continue
1066
-
1067
- controller.enqueue(
1068
- Buffer.from(
1069
- `event: message\ndata: ${JSON.stringify(
1070
- chunk,
1071
- )}\n\n`,
1072
- ),
1073
- )
1074
- }
1075
- } catch (error: any) {
1076
- let res = await self.runErrorHandlers({
1077
- onErrorHandlers: onErrorHandlers,
1078
- error,
1079
- request,
1080
- })
1081
- controller.enqueue(
1082
- Buffer.from(
1083
- `event: error\ndata: ${JSON.stringify(
1084
- error.message || error.name || 'Error',
1085
- )}\n\n`,
1086
- ),
1087
- )
1088
- }
1089
-
1090
- try {
1091
- controller.close()
1092
- } catch {
1093
- // nothing
1094
- }
1095
- },
1096
- }),
1097
- {
1098
- // TODO add headers somehow
1099
- headers: {
1100
- // Manually set transfer-encoding for direct response, eg. app.handle, eden
1101
- 'transfer-encoding': 'chunked',
1102
- 'content-type': 'text/event-stream; charset=utf-8',
1103
- // ...set?.headers
1104
- },
1105
- },
1106
- )
1107
- }
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
+ }
1108
1066
  }
1109
1067
 
1110
1068
  // async function getRequestBody({
@@ -1162,119 +1120,121 @@ export class Spiceflow<
1162
1120
  // }
1163
1121
 
1164
1122
  const METHODS = [
1165
- 'ALL',
1166
- 'CONNECT',
1167
- 'DELETE',
1168
- 'GET',
1169
- 'HEAD',
1170
- 'OPTIONS',
1171
- 'PATCH',
1172
- 'POST',
1173
- 'PUT',
1174
- 'TRACE',
1123
+ 'ALL',
1124
+ 'CONNECT',
1125
+ 'DELETE',
1126
+ 'GET',
1127
+ 'HEAD',
1128
+ 'OPTIONS',
1129
+ 'PATCH',
1130
+ 'POST',
1131
+ 'PUT',
1132
+ 'TRACE',
1175
1133
  ] as const
1176
1134
 
1177
1135
  /** HTTP method string */
1178
1136
  export type Method = (typeof METHODS)[number]
1179
1137
 
1180
1138
  function bfsFind<T>(
1181
- tree: AnySpiceflow,
1182
- onNode: (node: AnySpiceflow) => T | undefined | void,
1139
+ tree: AnySpiceflow,
1140
+ onNode: (node: AnySpiceflow) => T | undefined | void,
1183
1141
  ): T | undefined {
1184
- const queue = [tree]
1185
-
1186
- while (queue.length > 0) {
1187
- const node = queue.shift()!
1188
-
1189
- const result = onNode(node)
1190
- if (result) {
1191
- return result
1192
- }
1193
- queue.push(...node.childrenApps)
1194
- }
1195
- 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
1196
1154
  }
1197
1155
  export class SpiceflowRequest<T = any> extends Request {
1198
- validateBody?: ValidateFunction
1156
+ validateBody?: ValidateFunction
1199
1157
 
1200
- async json(): Promise<T> {
1201
- const body = (await super.json()) as Promise<T>
1202
- return runValidation(body, this.validateBody)
1203
- }
1158
+ async json(): Promise<T> {
1159
+ const body = (await super.json()) as Promise<T>
1160
+ return runValidation(body, this.validateBody)
1161
+ }
1204
1162
  }
1205
1163
 
1206
1164
  export function bfs(tree: AnySpiceflow) {
1207
- const queue = [tree]
1208
- let nodes: AnySpiceflow[] = []
1209
- while (queue.length > 0) {
1210
- const node = queue.shift()!
1211
- if (node) {
1212
- nodes.push(node)
1213
- }
1214
- // const result = onNode(node)
1215
-
1216
- if (node?.childrenApps?.length) {
1217
- queue.push(...node.childrenApps)
1218
- }
1219
- }
1220
- 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
1221
1179
  }
1222
1180
  export async function turnHandlerResultIntoResponse(result: any) {
1223
- // if (result === undefined) return new Response('', { status: 404 })
1224
- // if user returns not a response, convert to json
1225
- if (result instanceof Response) {
1226
- return result
1227
- }
1228
- // if user returns a promise, await it
1229
- if (result instanceof Promise) {
1230
- result = await result
1231
- }
1232
- // // if user returns a string, convert to json
1233
- // if (typeof result === 'string') {
1234
- // result = new Response(result)
1235
- // }
1236
- // if user returns an object, convert to json
1237
-
1238
- return new Response(JSON.stringify(result ?? null, null, 2), {
1239
- headers: {
1240
- 'content-type': 'application/json',
1241
- },
1242
- })
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
+ })
1243
1201
  }
1244
1202
 
1245
1203
  export type AnySpiceflow = Spiceflow<any, any, any, any, any, any>
1246
1204
 
1247
1205
  export function isZodSchema(value: unknown): value is ZodType {
1248
- return (
1249
- value instanceof z.ZodType ||
1250
- (typeof value === 'object' &&
1251
- value !== null &&
1252
- 'parse' in value &&
1253
- 'safeParse' in value &&
1254
- 'optional' in value &&
1255
- 'nullable' in value)
1256
- )
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
+ )
1257
1215
  }
1258
1216
 
1259
1217
  function getValidateFunction(schema: TypeSchema) {
1260
- if (isZodSchema(schema)) {
1261
- let jsonSchema = zodToJsonSchema(schema, {})
1262
- return ajv.compile(jsonSchema)
1263
- }
1264
-
1265
- if (schema) {
1266
- return ajv.compile(schema)
1267
- }
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
+ }
1268
1228
  }
1269
1229
 
1270
1230
  function runValidation(value: any, validate?: ValidateFunction) {
1271
- if (!validate) return value
1272
- const valid = validate(value)
1273
- if (!valid) {
1274
- const error = ajv.errorsText(validate.errors, {
1275
- separator: '\n',
1276
- })
1277
- throw new ValidationError(error)
1278
- }
1279
- 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
1280
1240
  }