spiceflow 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/spiceflow.ts CHANGED
@@ -10,6 +10,8 @@ import {
10
10
  DefinitionBase,
11
11
  EphemeralType,
12
12
  ErrorHandler,
13
+ Handler,
14
+ HTTPMethod,
13
15
  InlineHandler,
14
16
  InputSchema,
15
17
  JoinPath,
@@ -24,20 +26,41 @@ import {
24
26
  RouteBase,
25
27
  RouteSchema,
26
28
  SingletonBase,
27
- UnwrapRoute
29
+ TypeSchema,
30
+ UnwrapRoute,
28
31
  } from './elysia-fork/types.js'
32
+ import addFormats from 'ajv-formats'
33
+ let globalIndex = 0
29
34
 
30
35
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
31
36
  // @ts-ignore
32
37
  import OriginalRouter from '@medley/router'
33
38
  import { TSchema } from '@sinclair/typebox'
34
- import Ajv from 'ajv'
39
+ import Ajv, { ValidateFunction } from 'ajv'
35
40
  import { Context } from './elysia-fork/context.js'
36
41
  import { isAsyncIterable } from './utils.js'
37
42
  import { redirect } from './elysia-fork/utils.js'
38
43
  import { ValidationError } from './elysia-fork/error.js'
44
+ import { zodToJsonSchema } from 'zod-to-json-schema'
45
+ import { z, ZodType } from 'zod'
46
+
47
+ const ajv = addFormats(new Ajv({ useDefaults: true }), [
48
+ 'date-time',
49
+ 'time',
50
+ 'date',
51
+ 'email',
52
+ 'hostname',
53
+ 'ipv4',
54
+ 'ipv6',
55
+ 'uri',
56
+ 'uri-reference',
57
+ 'uuid',
58
+ 'uri-template',
59
+ 'json-pointer',
60
+ 'relative-json-pointer',
61
+ 'regex',
62
+ ])
39
63
 
40
- const ajv = new Ajv()
41
64
  // Should be exported from `hono/router`
42
65
 
43
66
  type P = any
@@ -47,19 +70,28 @@ type AsyncResponse = Response | Promise<Response>
47
70
  type OnError = (x: { error: any; request: Request }) => AsyncResponse
48
71
 
49
72
  type RouterTree = {
73
+ id: number
50
74
  router: OriginalRouter
51
75
  prefix?: string
52
76
  onRequestHandlers: Function[]
53
77
  onErrorHandlers: OnError[]
54
78
  children: RouterTree[]
79
+ routes: InternalRoute[]
80
+ // default store for the router, used as default for context.store
55
81
  store: Record<any, any>
82
+ currentRoot?: RouterTree
56
83
  }
57
84
 
58
85
  type OnNoMatch = (request: Request, platform: P) => AsyncResponse
59
86
 
60
- type InternalRouterState = {
61
- hook: any
62
- handler: any
87
+ export type InternalRoute = {
88
+ method: HTTPMethod
89
+ path: string
90
+ handler: InlineHandler<any, any, any>
91
+ hooks: LocalHook<any, any, any, any, any, any, any>
92
+ validate?: ValidateFunction
93
+ prefix: string
94
+
63
95
  // store: Record<any, any>
64
96
  }
65
97
  /**
@@ -95,40 +127,82 @@ export class Spiceflow<
95
127
  derive: {}
96
128
  resolve: {}
97
129
  schema: {}
98
- }
130
+ },
99
131
  > {
100
132
  private onNoMatch: OnNoMatch
101
133
  // prefix: BasePath | undefined
102
- routerTree: RouterTree
134
+ private routerTree: RouterTree
135
+
136
+ getAllRoutes() {
137
+ let root = this.routerTree.currentRoot || this.routerTree
138
+ const allApps = bfs(root) || []
139
+ const allRoutes = allApps.flatMap((x) => {
140
+ const prefix = this.getRouteAndParents(x)
141
+ .map((x) => x.prefix)
142
+ .reverse()
143
+ .join('')
144
+
145
+ return x.routes.map((x) => ({ ...x, path: prefix + x.path }))
146
+ })
147
+ return allRoutes
148
+ }
103
149
 
104
- add({
150
+ private add({
105
151
  method,
106
152
  path,
153
+ hooks,
154
+ handler,
107
155
  ...rest
108
- }: InternalRouterState & {
109
- method: string
110
- path: string
111
- }) {
156
+ }: Partial<InternalRoute>) {
112
157
  const router = this.routerTree
113
158
  // if (router.prefix) {
114
159
  // path = router.prefix + path
115
160
  // }
116
161
 
162
+ let bodySchema: TypeSchema = hooks?.body
163
+ let validate: ValidateFunction | undefined
164
+
165
+ if (isZodSchema(bodySchema)) {
166
+ let jsonSchema = zodToJsonSchema(bodySchema, {})
167
+ validate = ajv.compile(jsonSchema)
168
+ } else if (bodySchema) {
169
+ // console.log(bodySchema)
170
+ validate = ajv.compile(bodySchema)
171
+ }
172
+
117
173
  const store = router.router.register(path)
118
- store[method] = { ...rest }
174
+ let route: InternalRoute = {
175
+ ...rest,
176
+
177
+ prefix: router.prefix || '',
178
+ method: (method || '') as any,
179
+ path: path || '',
180
+ // prefix,
181
+ handler: handler!,
182
+ hooks,
183
+ validate,
184
+ }
185
+ router.routes.push(route)
186
+ store[method] = route
119
187
  }
120
188
 
121
189
  private match(method: string, path: string) {
122
- const result = bfs(this.routerTree, (router) => {
123
- if (router.prefix && !path.startsWith(router.prefix)) {
190
+ let root = this.routerTree
191
+ const result = bfsFind(this.routerTree, (router) => {
192
+ router.currentRoot = root
193
+ let prefix = this.getRouteAndParents(router)
194
+ .map((x) => x.prefix)
195
+ .reverse()
196
+ .join('')
197
+ if (prefix && !path.startsWith(prefix)) {
124
198
  // console.log(
125
199
  // `router prefix: ${router.prefix} does not match path: ${path}`
126
200
  // )
127
201
  return
128
202
  }
129
203
  let pathWithoutPrefix = path
130
- if (router.prefix) {
131
- pathWithoutPrefix = path.replace(router.prefix, '')
204
+ if (prefix) {
205
+ pathWithoutPrefix = path.replace(prefix, '')
132
206
  }
133
207
  // console.log(`router prefix: ${router.prefix} matches path: ${path}`)
134
208
  const route = router.router.find(pathWithoutPrefix)
@@ -136,7 +210,7 @@ export class Spiceflow<
136
210
  return
137
211
  }
138
212
 
139
- let data: InternalRouterState = route['store'][method]
213
+ let data: InternalRoute = route['store'][method]
140
214
  if (data) {
141
215
  // console.log(`route found: ${method} ${path}`, route)
142
216
 
@@ -148,7 +222,7 @@ export class Spiceflow<
148
222
  store: router.store,
149
223
  onErrorHandlers,
150
224
  onRequestHandlers,
151
- params
225
+ params,
152
226
  }
153
227
  }
154
228
  })
@@ -158,7 +232,7 @@ export class Spiceflow<
158
232
 
159
233
  state<const Name extends string | number | symbol, Value>(
160
234
  name: Name,
161
- value: Value
235
+ value: Value,
162
236
  ): Spiceflow<
163
237
  BasePath,
164
238
  Scoped,
@@ -189,24 +263,25 @@ export class Spiceflow<
189
263
  */
190
264
  constructor(
191
265
  options: {
192
- /** Fallback handle if an error is thrown (500 response is default) */
193
- // onError?: OnError
266
+ name?: string
194
267
  scoped?: Scoped
195
268
  onNoMatch?: (request: Request, platform: P) => AsyncResponse
196
269
  basePath?: BasePath
197
- } = {}
270
+ } = {},
198
271
  ) {
199
272
  this.scoped = options.scoped
200
273
 
201
274
  this.onNoMatch =
202
275
  options.onNoMatch ?? (() => new Response(null, { status: 404 }))
203
276
  this.routerTree = {
277
+ id: globalIndex++,
204
278
  router: new OriginalRouter(),
205
279
  prefix: options.basePath,
206
280
  onRequestHandlers: [],
207
281
  onErrorHandlers: [],
208
282
  children: [],
209
- store: {}
283
+ store: {},
284
+ routes: [],
210
285
  }
211
286
 
212
287
  // Bind router methods
@@ -224,13 +299,12 @@ export class Spiceflow<
224
299
  Scoped: false as Scoped,
225
300
  Singleton: {} as Singleton,
226
301
  Definitions: {} as Definitions,
227
- Metadata: {} as Metadata
302
+ Metadata: {} as Metadata,
228
303
  }
229
304
 
230
305
  _ephemeral = {} as Ephemeral
231
306
  _volatile = {} as Volatile
232
307
 
233
-
234
308
  post<
235
309
  const Path extends string,
236
310
  const LocalSchema extends InputSchema<
@@ -250,7 +324,7 @@ export class Spiceflow<
250
324
  resolve: Ephemeral['resolve'] & Volatile['resolve']
251
325
  },
252
326
  JoinPath<BasePath, Path>
253
- >
327
+ >,
254
328
  >(
255
329
  path: Path,
256
330
  handler: Handle,
@@ -264,7 +338,7 @@ export class Spiceflow<
264
338
  Definitions['error'],
265
339
  Metadata['macro'],
266
340
  JoinPath<BasePath, Path>
267
- >
341
+ >,
268
342
  ): Spiceflow<
269
343
  BasePath,
270
344
  Scoped,
@@ -292,7 +366,7 @@ export class Spiceflow<
292
366
  Ephemeral,
293
367
  Volatile
294
368
  > {
295
- this.add({ method: 'POST', path, handler: handler, hook })
369
+ this.add({ method: 'POST', path, handler: handler, hooks: hook })
296
370
 
297
371
  return this as any
298
372
  }
@@ -317,7 +391,7 @@ export class Spiceflow<
317
391
  resolve: Ephemeral['resolve'] & Volatile['resolve']
318
392
  },
319
393
  JoinPath<BasePath, Path>
320
- >
394
+ >,
321
395
  >(
322
396
  path: Path,
323
397
  handler: Handle,
@@ -331,7 +405,7 @@ export class Spiceflow<
331
405
  Definitions['error'],
332
406
  Macro,
333
407
  JoinPath<BasePath, Path>
334
- >
408
+ >,
335
409
  ): Spiceflow<
336
410
  BasePath,
337
411
  Scoped,
@@ -359,7 +433,7 @@ export class Spiceflow<
359
433
  Ephemeral,
360
434
  Volatile
361
435
  > {
362
- this.add({ method: 'GET', path, handler: handler, hook })
436
+ this.add({ method: 'GET', path, handler: handler, hooks: hook })
363
437
  return this as any
364
438
  }
365
439
 
@@ -382,7 +456,7 @@ export class Spiceflow<
382
456
  resolve: Ephemeral['resolve'] & Volatile['resolve']
383
457
  },
384
458
  JoinPath<BasePath, Path>
385
- >
459
+ >,
386
460
  >(
387
461
  path: Path,
388
462
  handler: Handle,
@@ -396,7 +470,7 @@ export class Spiceflow<
396
470
  Definitions['error'],
397
471
  Metadata['macro'],
398
472
  JoinPath<BasePath, Path>
399
- >
473
+ >,
400
474
  ): Spiceflow<
401
475
  BasePath,
402
476
  Scoped,
@@ -424,7 +498,7 @@ export class Spiceflow<
424
498
  Ephemeral,
425
499
  Volatile
426
500
  > {
427
- this.add({ method: 'PUT', path, handler: handler, hook })
501
+ this.add({ method: 'PUT', path, handler: handler, hooks: hook })
428
502
 
429
503
  return this as any
430
504
  }
@@ -448,7 +522,7 @@ export class Spiceflow<
448
522
  resolve: Ephemeral['resolve'] & Volatile['resolve']
449
523
  },
450
524
  JoinPath<BasePath, Path>
451
- >
525
+ >,
452
526
  >(
453
527
  path: Path,
454
528
  handler: Handle,
@@ -462,7 +536,7 @@ export class Spiceflow<
462
536
  Definitions['error'],
463
537
  Metadata['macro'],
464
538
  JoinPath<BasePath, Path>
465
- >
539
+ >,
466
540
  ): Spiceflow<
467
541
  BasePath,
468
542
  Scoped,
@@ -490,7 +564,7 @@ export class Spiceflow<
490
564
  Ephemeral,
491
565
  Volatile
492
566
  > {
493
- this.add({ method: 'PATCH', path, handler: handler, hook })
567
+ this.add({ method: 'PATCH', path, handler: handler, hooks: hook })
494
568
 
495
569
  return this as any
496
570
  }
@@ -514,7 +588,7 @@ export class Spiceflow<
514
588
  resolve: Ephemeral['resolve'] & Volatile['resolve']
515
589
  },
516
590
  JoinPath<BasePath, Path>
517
- >
591
+ >,
518
592
  >(
519
593
  path: Path,
520
594
  handler: Handle,
@@ -528,7 +602,7 @@ export class Spiceflow<
528
602
  Definitions['error'],
529
603
  Metadata['macro'],
530
604
  JoinPath<BasePath, Path>
531
- >
605
+ >,
532
606
  ): Spiceflow<
533
607
  BasePath,
534
608
  Scoped,
@@ -556,7 +630,7 @@ export class Spiceflow<
556
630
  Ephemeral,
557
631
  Volatile
558
632
  > {
559
- this.add({ method: 'DELETE', path, handler: handler, hook })
633
+ this.add({ method: 'DELETE', path, handler: handler, hooks: hook })
560
634
 
561
635
  return this as any
562
636
  }
@@ -580,7 +654,7 @@ export class Spiceflow<
580
654
  resolve: Ephemeral['resolve'] & Volatile['resolve']
581
655
  },
582
656
  JoinPath<BasePath, Path>
583
- >
657
+ >,
584
658
  >(
585
659
  path: Path,
586
660
  handler: Handle,
@@ -594,7 +668,7 @@ export class Spiceflow<
594
668
  Definitions['error'],
595
669
  Metadata['macro'],
596
670
  JoinPath<BasePath, Path>
597
- >
671
+ >,
598
672
  ): Spiceflow<
599
673
  BasePath,
600
674
  Scoped,
@@ -622,7 +696,7 @@ export class Spiceflow<
622
696
  Ephemeral,
623
697
  Volatile
624
698
  > {
625
- this.add({ method: 'OPTIONS', path, handler: handler, hook })
699
+ this.add({ method: 'OPTIONS', path, handler: handler, hooks: hook })
626
700
 
627
701
  return this as any
628
702
  }
@@ -646,7 +720,7 @@ export class Spiceflow<
646
720
  resolve: Ephemeral['resolve'] & Volatile['resolve']
647
721
  },
648
722
  JoinPath<BasePath, Path>
649
- >
723
+ >,
650
724
  >(
651
725
  path: Path,
652
726
  handler: Handle,
@@ -660,7 +734,7 @@ export class Spiceflow<
660
734
  Definitions['error'],
661
735
  Metadata['macro'],
662
736
  JoinPath<BasePath, Path>
663
- >
737
+ >,
664
738
  ): Spiceflow<
665
739
  BasePath,
666
740
  Scoped,
@@ -689,7 +763,7 @@ export class Spiceflow<
689
763
  Volatile
690
764
  > {
691
765
  for (const method of METHODS) {
692
- this.add({ method, path, handler: handler, hook })
766
+ this.add({ method, path, handler: handler, hooks: hook })
693
767
  }
694
768
 
695
769
  return this as any
@@ -714,7 +788,7 @@ export class Spiceflow<
714
788
  resolve: Ephemeral['resolve'] & Volatile['resolve']
715
789
  },
716
790
  JoinPath<BasePath, Path>
717
- >
791
+ >,
718
792
  >(
719
793
  path: Path,
720
794
  handler: Handle,
@@ -728,7 +802,7 @@ export class Spiceflow<
728
802
  Definitions['error'],
729
803
  Metadata['macro'],
730
804
  JoinPath<BasePath, Path>
731
- >
805
+ >,
732
806
  ): Spiceflow<
733
807
  BasePath,
734
808
  Scoped,
@@ -756,7 +830,7 @@ export class Spiceflow<
756
830
  Ephemeral,
757
831
  Volatile
758
832
  > {
759
- this.add({ method: 'HEAD', path, handler: handler, hook })
833
+ this.add({ method: 'HEAD', path, handler: handler, hooks: hook })
760
834
 
761
835
  return this as any
762
836
  }
@@ -766,7 +840,7 @@ export class Spiceflow<
766
840
  *
767
841
  * @default false
768
842
  */
769
- scoped?: Scoped
843
+ private scoped?: Scoped
770
844
  get _scoped() {
771
845
  return this.scoped as Scoped
772
846
  }
@@ -811,7 +885,7 @@ export class Spiceflow<
811
885
  // }
812
886
 
813
887
  use<const NewSpiceflow extends AnySpiceflow>(
814
- instance: NewSpiceflow
888
+ instance: NewSpiceflow,
815
889
  ): NewSpiceflow['_scoped'] extends false
816
890
  ? Spiceflow<
817
891
  BasePath,
@@ -840,14 +914,7 @@ export class Spiceflow<
840
914
  > {
841
915
  const thisRouter = this.routerTree
842
916
  // TODO use scoped logic to add onRequest and onError on all routers if necessary, add them first
843
- this.routerTree.children.push(
844
- mapBfs(instance.routerTree, (r) => {
845
- return {
846
- ...r,
847
- prefix: (thisRouter.prefix || '') + r.prefix
848
- }
849
- })
850
- )
917
+ this.routerTree.children.push(instance.routerTree)
851
918
  return this as any
852
919
  }
853
920
 
@@ -866,7 +933,7 @@ export class Spiceflow<
866
933
  Ephemeral,
867
934
  Volatile
868
935
  >
869
- >
936
+ >,
870
937
  ): this {
871
938
  const router = this.routerTree
872
939
 
@@ -893,7 +960,7 @@ export class Spiceflow<
893
960
  resolve: {}
894
961
  }
895
962
  >
896
- >
963
+ >,
897
964
  ) {
898
965
  const router = this.routerTree
899
966
  router.onRequestHandlers ??= []
@@ -914,7 +981,7 @@ export class Spiceflow<
914
981
  const defaultContext = {
915
982
  redirect,
916
983
  error: null,
917
- path
984
+ path,
918
985
  }
919
986
  let onErrorHandlers: OnError[] = []
920
987
  try {
@@ -925,41 +992,44 @@ export class Spiceflow<
925
992
  if (!route) {
926
993
  return this.onNoMatch(request, platform)
927
994
  }
928
- onErrorHandlers = this.getRouteAndParents(route.router).flatMap(
929
- (x) => x.onErrorHandlers
930
- )
931
- const { params, store } = route
932
- const onReq = this.getRouteAndParents(route.router).flatMap(
933
- (x) => x.onRequestHandlers
934
- )
995
+ onErrorHandlers = this.getRouteAndParents(route.router)
996
+ .reverse()
997
+ .flatMap((x) => x.onErrorHandlers)
998
+ const { params, store: defaultStore } = route
999
+ const onReqHandlers = this.getRouteAndParents(route.router)
1000
+ .reverse()
1001
+ .flatMap((x) => x.onRequestHandlers)
1002
+ // console.log({ onReqHandlers })
1003
+ let store = { ...defaultStore }
935
1004
  // TODO add content type
936
1005
 
937
- let content = route?.hook?.content
1006
+ let content = route?.hooks?.content
938
1007
  let body = await getRequestBody({ request, content })
939
- let bodySchema: TSchema = route?.hook?.body
940
- if (bodySchema) {
941
- const validate = ajv.compile(bodySchema)
942
- const valid = validate(body)
1008
+
1009
+ if (route.validate) {
1010
+ // TODO move compile to the router
1011
+
1012
+ const valid = route.validate(body)
943
1013
  if (!valid) {
944
- const error = ajv.errorsText(validate.errors, {
945
- separator: '\n'
1014
+ const error = ajv.errorsText(route.validate.errors, {
1015
+ separator: '\n',
946
1016
  })
947
1017
 
948
1018
  return new Response(error, {
949
1019
  status: 400,
950
1020
  headers: {
951
- 'content-type': 'text/plain'
952
- }
1021
+ 'content-type': 'text/plain',
1022
+ },
953
1023
  })
954
1024
  }
955
1025
  }
956
- if (onReq.length > 0) {
957
- for (const handler of onReq) {
1026
+ if (onReqHandlers.length > 0) {
1027
+ for (const handler of onReqHandlers) {
958
1028
  const res = await handler({
959
1029
  request,
960
1030
  response,
961
1031
  store,
962
- path
1032
+ path,
963
1033
  } satisfies Context<any, any, any>)
964
1034
  if (res) {
965
1035
  return await turnHandlerResultIntoResponse(res)
@@ -976,7 +1046,7 @@ export class Spiceflow<
976
1046
  params: params as any,
977
1047
  store,
978
1048
  body,
979
- path
1049
+ path,
980
1050
 
981
1051
  // platform
982
1052
  } satisfies Context<any, any, string>)
@@ -984,7 +1054,7 @@ export class Spiceflow<
984
1054
  return await this.handleStream({
985
1055
  generator: res,
986
1056
  request,
987
- onErrorHandlers
1057
+ onErrorHandlers,
988
1058
  })
989
1059
  }
990
1060
 
@@ -993,11 +1063,11 @@ export class Spiceflow<
993
1063
  let res = await this.runErrorHandlers({
994
1064
  onErrorHandlers,
995
1065
  error: err,
996
- request
1066
+ request,
997
1067
  })
998
1068
  if (res) return res
999
1069
  return new Response(err?.message || 'Internal Server Error', {
1000
- status: 500
1070
+ status: 500,
1001
1071
  })
1002
1072
  }
1003
1073
  }
@@ -1005,7 +1075,7 @@ export class Spiceflow<
1005
1075
  private async runErrorHandlers({
1006
1076
  onErrorHandlers = [] as OnError[],
1007
1077
  error: err,
1008
- request
1078
+ request,
1009
1079
  }) {
1010
1080
  if (onErrorHandlers.length === 0) {
1011
1081
  console.error(`Spiceflow unhandled error:`, err)
@@ -1019,31 +1089,33 @@ export class Spiceflow<
1019
1089
  }
1020
1090
  }
1021
1091
 
1092
+ // get the route parents, the order is starting from the current router and going up to the root
1022
1093
  private getRouteAndParents(currentRouter?: RouterTree) {
1023
1094
  const parents: RouterTree[] = []
1024
1095
  let current = currentRouter
1025
1096
 
1097
+ let root = this.routerTree.currentRoot || this.routerTree
1026
1098
  // Perform BFS once to build a parent map
1027
- const parentMap = new Map<RouterTree, RouterTree>()
1028
- bfs(this.routerTree, (node) => {
1099
+ const parentMap = new Map<number, RouterTree>()
1100
+ bfsFind(root, (node) => {
1029
1101
  for (const child of node.children) {
1030
- parentMap.set(child, node)
1102
+ parentMap.set(child.id, node)
1031
1103
  }
1032
1104
  })
1033
1105
 
1034
1106
  // Traverse the parent map to get the parents
1035
1107
  while (current) {
1036
- parents.unshift(current)
1037
- current = parentMap.get(current)
1108
+ parents.push(current)
1109
+ current = parentMap.get(current.id)
1038
1110
  }
1039
1111
 
1040
- return parents.reverse().filter((x) => x !== undefined)
1112
+ return parents.filter((x) => x !== undefined)
1041
1113
  }
1042
1114
 
1043
- async handleStream({
1115
+ private async handleStream({
1044
1116
  onErrorHandlers,
1045
1117
  generator,
1046
- request
1118
+ request,
1047
1119
  }: {
1048
1120
  generator: Generator | AsyncGenerator
1049
1121
  onErrorHandlers: OnError[]
@@ -1076,9 +1148,9 @@ export class Spiceflow<
1076
1148
  controller.enqueue(
1077
1149
  Buffer.from(
1078
1150
  `event: message\ndata: ${JSON.stringify(
1079
- init.value
1080
- )}\n\n`
1081
- )
1151
+ init.value,
1152
+ )}\n\n`,
1153
+ ),
1082
1154
  )
1083
1155
 
1084
1156
  try {
@@ -1089,23 +1161,23 @@ export class Spiceflow<
1089
1161
  controller.enqueue(
1090
1162
  Buffer.from(
1091
1163
  `event: message\ndata: ${JSON.stringify(
1092
- chunk
1093
- )}\n\n`
1094
- )
1164
+ chunk,
1165
+ )}\n\n`,
1166
+ ),
1095
1167
  )
1096
1168
  }
1097
1169
  } catch (error: any) {
1098
1170
  let res = await self.runErrorHandlers({
1099
1171
  onErrorHandlers: onErrorHandlers,
1100
1172
  error,
1101
- request
1173
+ request,
1102
1174
  })
1103
1175
  controller.enqueue(
1104
1176
  Buffer.from(
1105
1177
  `event: error\ndata: ${JSON.stringify(
1106
- error.message || error.name || 'Error'
1107
- )}\n\n`
1108
- )
1178
+ error.message || error.name || 'Error',
1179
+ )}\n\n`,
1180
+ ),
1109
1181
  )
1110
1182
  }
1111
1183
 
@@ -1114,24 +1186,24 @@ export class Spiceflow<
1114
1186
  } catch {
1115
1187
  // nothing
1116
1188
  }
1117
- }
1189
+ },
1118
1190
  }),
1119
1191
  {
1120
1192
  // TODO add headers somehow
1121
1193
  headers: {
1122
1194
  // Manually set transfer-encoding for direct response, eg. app.handle, eden
1123
1195
  'transfer-encoding': 'chunked',
1124
- 'content-type': 'text/event-stream; charset=utf-8'
1196
+ 'content-type': 'text/event-stream; charset=utf-8',
1125
1197
  // ...set?.headers
1126
- }
1127
- }
1198
+ },
1199
+ },
1128
1200
  )
1129
1201
  }
1130
1202
  }
1131
1203
 
1132
1204
  async function getRequestBody({
1133
1205
  request,
1134
- content
1206
+ content,
1135
1207
  }: {
1136
1208
  content
1137
1209
  request: Request
@@ -1193,41 +1265,50 @@ const METHODS = [
1193
1265
  'PATCH',
1194
1266
  'POST',
1195
1267
  'PUT',
1196
- 'TRACE'
1268
+ 'TRACE',
1197
1269
  ] as const
1198
1270
 
1199
1271
  /** HTTP method string */
1200
1272
  export type Method = (typeof METHODS)[number]
1201
1273
 
1202
- function bfs<T>(
1274
+ function bfsFind<T>(
1203
1275
  tree: RouterTree,
1204
- onNode: (node: RouterTree) => T | undefined | void
1276
+ onNode: (node: RouterTree) => T | undefined | void,
1205
1277
  ): T | undefined {
1206
1278
  const queue = [tree]
1279
+
1207
1280
  while (queue.length > 0) {
1208
1281
  const node = queue.shift()!
1282
+
1209
1283
  const result = onNode(node)
1210
1284
  if (result) {
1211
1285
  return result
1212
1286
  }
1213
1287
  queue.push(...node.children)
1214
1288
  }
1215
- return undefined
1289
+ return
1216
1290
  }
1217
-
1218
- function mapBfs(
1219
- tree: RouterTree,
1220
- mapper: (node: RouterTree) => RouterTree
1221
- ): RouterTree {
1291
+ export function bfs(tree: RouterTree) {
1222
1292
  const queue = [tree]
1223
- const result: RouterTree = { ...mapper(tree), children: [] }
1293
+ let nodes: RouterTree[] = []
1224
1294
  while (queue.length > 0) {
1225
1295
  const node = queue.shift()!
1226
- const mappedNode = mapper(node)
1227
- result.children.push(mappedNode)
1228
- queue.push(...mappedNode.children)
1296
+ if (node) {
1297
+ nodes.push(node)
1298
+ }
1299
+ // const result = onNode(node)
1300
+
1301
+ queue.push(...node.children)
1229
1302
  }
1230
- return result
1303
+ return nodes
1304
+ }
1305
+ function mapTree<T>(
1306
+ tree: RouterTree,
1307
+ mapper: (node: RouterTree) => T,
1308
+ ): T & { children: (T & { children: any[] })[] } {
1309
+ const mappedNode = mapper(tree) as T & { children: any[] }
1310
+ mappedNode.children = tree.children.map((child) => mapTree(child, mapper))
1311
+ return mappedNode
1231
1312
  }
1232
1313
 
1233
1314
  export async function turnHandlerResultIntoResponse(result: any) {
@@ -1245,7 +1326,23 @@ export async function turnHandlerResultIntoResponse(result: any) {
1245
1326
  // }
1246
1327
  // if user returns an object, convert to json
1247
1328
 
1248
- return new Response(JSON.stringify(result))
1329
+ return new Response(JSON.stringify(result, null, 2), {
1330
+ headers: {
1331
+ 'content-type': 'application/json',
1332
+ },
1333
+ })
1249
1334
  }
1250
1335
 
1251
1336
  export type AnySpiceflow = Spiceflow<any, any, any, any, any, any, any, any>
1337
+
1338
+ export function isZodSchema(value: unknown): value is ZodType {
1339
+ return (
1340
+ value instanceof z.ZodType ||
1341
+ (typeof value === 'object' &&
1342
+ value !== null &&
1343
+ 'parse' in value &&
1344
+ 'safeParse' in value &&
1345
+ 'optional' in value &&
1346
+ 'nullable' in value)
1347
+ )
1348
+ }