spiceflow 0.0.7 → 1.0.1

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