effect-start 0.18.0 → 0.20.0

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 (203) hide show
  1. package/README.md +3 -3
  2. package/dist/Development.d.ts +8 -3
  3. package/dist/Development.js +14 -7
  4. package/dist/Effectify.d.ts +212 -0
  5. package/dist/Effectify.js +19 -0
  6. package/dist/FilePathPattern.d.ts +29 -0
  7. package/dist/FilePathPattern.js +86 -0
  8. package/dist/FileRouter.d.ts +39 -41
  9. package/dist/FileRouter.js +104 -158
  10. package/dist/FileRouterCodegen.d.ts +7 -8
  11. package/dist/FileRouterCodegen.js +97 -66
  12. package/dist/PlatformError.d.ts +46 -0
  13. package/dist/PlatformError.js +43 -0
  14. package/dist/PlatformRuntime.d.ts +27 -0
  15. package/dist/PlatformRuntime.js +51 -0
  16. package/dist/Route.d.ts +6 -2
  17. package/dist/Route.js +22 -0
  18. package/dist/RouteBody.d.ts +1 -1
  19. package/dist/RouteHttp.d.ts +1 -1
  20. package/dist/RouteHttp.js +12 -19
  21. package/dist/RouteMount.d.ts +2 -1
  22. package/dist/Start.d.ts +33 -6
  23. package/dist/Start.js +31 -13
  24. package/dist/Unique.d.ts +50 -0
  25. package/dist/Unique.js +187 -0
  26. package/dist/bun/BunHttpServer.js +5 -6
  27. package/dist/bun/BunPlatformHttpServer.d.ts +10 -0
  28. package/dist/bun/BunPlatformHttpServer.js +53 -0
  29. package/dist/bun/BunRoute.d.ts +4 -6
  30. package/dist/bun/BunRoute.js +10 -18
  31. package/dist/bun/BunRuntime.d.ts +2 -1
  32. package/dist/bun/BunRuntime.js +10 -5
  33. package/dist/bun/BunServer.d.ts +33 -0
  34. package/dist/bun/BunServer.js +133 -0
  35. package/dist/bun/BunServerRequest.d.ts +60 -0
  36. package/dist/bun/BunServerRequest.js +252 -0
  37. package/dist/bun/index.d.ts +1 -1
  38. package/dist/bun/index.js +1 -1
  39. package/dist/datastar/actions/fetch.d.ts +30 -0
  40. package/dist/datastar/actions/fetch.js +411 -0
  41. package/dist/datastar/actions/peek.d.ts +1 -0
  42. package/dist/datastar/actions/peek.js +14 -0
  43. package/dist/datastar/actions/setAll.d.ts +1 -0
  44. package/dist/datastar/actions/setAll.js +13 -0
  45. package/dist/datastar/actions/toggleAll.d.ts +1 -0
  46. package/dist/datastar/actions/toggleAll.js +13 -0
  47. package/dist/datastar/attributes/attr.d.ts +1 -0
  48. package/dist/datastar/attributes/attr.js +49 -0
  49. package/dist/datastar/attributes/bind.d.ts +1 -0
  50. package/dist/datastar/attributes/bind.js +183 -0
  51. package/dist/datastar/attributes/class.d.ts +1 -0
  52. package/dist/datastar/attributes/class.js +50 -0
  53. package/dist/datastar/attributes/computed.d.ts +1 -0
  54. package/dist/datastar/attributes/computed.js +27 -0
  55. package/dist/datastar/attributes/effect.d.ts +1 -0
  56. package/dist/datastar/attributes/effect.js +10 -0
  57. package/dist/datastar/attributes/indicator.d.ts +1 -0
  58. package/dist/datastar/attributes/indicator.js +32 -0
  59. package/dist/datastar/attributes/init.d.ts +1 -0
  60. package/dist/datastar/attributes/init.js +27 -0
  61. package/dist/datastar/attributes/jsonSignals.d.ts +1 -0
  62. package/dist/datastar/attributes/jsonSignals.js +31 -0
  63. package/dist/datastar/attributes/on.d.ts +1 -0
  64. package/dist/datastar/attributes/on.js +59 -0
  65. package/dist/datastar/attributes/onIntersect.d.ts +1 -0
  66. package/dist/datastar/attributes/onIntersect.js +54 -0
  67. package/dist/datastar/attributes/onInterval.d.ts +1 -0
  68. package/dist/datastar/attributes/onInterval.js +31 -0
  69. package/dist/datastar/attributes/onSignalPatch.d.ts +1 -0
  70. package/dist/datastar/attributes/onSignalPatch.js +44 -0
  71. package/dist/datastar/attributes/ref.d.ts +1 -0
  72. package/dist/datastar/attributes/ref.js +11 -0
  73. package/dist/datastar/attributes/show.d.ts +1 -0
  74. package/dist/datastar/attributes/show.js +32 -0
  75. package/dist/datastar/attributes/signals.d.ts +1 -0
  76. package/dist/datastar/attributes/signals.js +18 -0
  77. package/dist/datastar/attributes/style.d.ts +1 -0
  78. package/dist/datastar/attributes/style.js +56 -0
  79. package/dist/datastar/attributes/text.d.ts +1 -0
  80. package/dist/datastar/attributes/text.js +27 -0
  81. package/dist/datastar/engine.d.ts +156 -0
  82. package/dist/datastar/engine.js +971 -0
  83. package/dist/datastar/index.d.ts +24 -0
  84. package/dist/datastar/index.js +24 -0
  85. package/dist/datastar/load.d.ts +24 -0
  86. package/dist/datastar/load.js +24 -0
  87. package/dist/datastar/utils.d.ts +51 -0
  88. package/dist/datastar/utils.js +205 -0
  89. package/dist/datastar/watchers/patchElements.d.ts +1 -0
  90. package/dist/datastar/watchers/patchElements.js +420 -0
  91. package/dist/datastar/watchers/patchSignals.d.ts +1 -0
  92. package/dist/datastar/watchers/patchSignals.js +15 -0
  93. package/dist/index.d.ts +1 -0
  94. package/dist/index.js +1 -0
  95. package/dist/node/Effectify.d.ts +209 -0
  96. package/dist/node/Effectify.js +19 -0
  97. package/dist/node/FileSystem.d.ts +3 -5
  98. package/dist/node/FileSystem.js +42 -62
  99. package/dist/node/NodeFileSystem.d.ts +7 -0
  100. package/dist/node/NodeFileSystem.js +420 -0
  101. package/dist/node/NodeUtils.d.ts +2 -0
  102. package/dist/node/NodeUtils.js +20 -0
  103. package/dist/node/PlatformError.d.ts +46 -0
  104. package/dist/node/PlatformError.js +43 -0
  105. package/dist/testing/TestLogger.js +1 -1
  106. package/dist/x/tailwind/plugin.js +1 -1
  107. package/package.json +18 -7
  108. package/src/Development.ts +36 -40
  109. package/src/Effectify.ts +269 -0
  110. package/src/FilePathPattern.ts +115 -0
  111. package/src/FileRouter.ts +178 -255
  112. package/src/FileRouterCodegen.ts +135 -92
  113. package/src/PlatformError.ts +117 -0
  114. package/src/PlatformRuntime.ts +108 -0
  115. package/src/Route.ts +31 -2
  116. package/src/RouteBody.ts +1 -1
  117. package/src/RouteHttp.ts +15 -29
  118. package/src/RouteMount.ts +1 -1
  119. package/src/Start.ts +61 -27
  120. package/src/Unique.ts +232 -0
  121. package/src/bun/BunPlatformHttpServer.ts +88 -0
  122. package/src/bun/BunRoute.ts +14 -24
  123. package/src/bun/BunRuntime.ts +21 -5
  124. package/src/bun/BunServer.ts +228 -0
  125. package/src/bun/index.ts +1 -1
  126. package/src/datastar/README.md +18 -0
  127. package/src/datastar/actions/fetch.ts +609 -0
  128. package/src/datastar/actions/peek.ts +17 -0
  129. package/src/datastar/actions/setAll.ts +20 -0
  130. package/src/datastar/actions/toggleAll.ts +20 -0
  131. package/src/datastar/attributes/attr.ts +50 -0
  132. package/src/datastar/attributes/bind.ts +220 -0
  133. package/src/datastar/attributes/class.ts +57 -0
  134. package/src/datastar/attributes/computed.ts +33 -0
  135. package/src/datastar/attributes/effect.ts +11 -0
  136. package/src/datastar/attributes/indicator.ts +39 -0
  137. package/src/datastar/attributes/init.ts +35 -0
  138. package/src/datastar/attributes/jsonSignals.ts +38 -0
  139. package/src/datastar/attributes/on.ts +71 -0
  140. package/src/datastar/attributes/onIntersect.ts +65 -0
  141. package/src/datastar/attributes/onInterval.ts +39 -0
  142. package/src/datastar/attributes/onSignalPatch.ts +63 -0
  143. package/src/datastar/attributes/ref.ts +12 -0
  144. package/src/datastar/attributes/show.ts +33 -0
  145. package/src/datastar/attributes/signals.ts +22 -0
  146. package/src/datastar/attributes/style.ts +63 -0
  147. package/src/datastar/attributes/text.ts +30 -0
  148. package/src/datastar/engine.ts +1341 -0
  149. package/src/datastar/index.ts +25 -0
  150. package/src/datastar/utils.ts +286 -0
  151. package/src/datastar/watchers/patchElements.ts +554 -0
  152. package/src/datastar/watchers/patchSignals.ts +15 -0
  153. package/src/index.ts +1 -0
  154. package/src/node/{FileSystem.ts → NodeFileSystem.ts} +59 -97
  155. package/src/node/{Utils.ts → NodeUtils.ts} +2 -0
  156. package/src/testing/TestLogger.ts +1 -1
  157. package/src/x/tailwind/plugin.ts +1 -1
  158. package/dist/Random.d.ts +0 -5
  159. package/dist/Random.js +0 -49
  160. package/src/Commander.test.ts +0 -1639
  161. package/src/ContentNegotiation.test.ts +0 -603
  162. package/src/Development.test.ts +0 -119
  163. package/src/Entity.test.ts +0 -592
  164. package/src/FileRouterCodegen.todo.ts +0 -1133
  165. package/src/FileRouterPattern.test.ts +0 -147
  166. package/src/FileRouterPattern.ts +0 -59
  167. package/src/FileRouter_files.test.ts +0 -64
  168. package/src/FileRouter_path.test.ts +0 -145
  169. package/src/FileRouter_tree.test.ts +0 -132
  170. package/src/Http.test.ts +0 -319
  171. package/src/HttpAppExtra.test.ts +0 -103
  172. package/src/HttpUtils.test.ts +0 -85
  173. package/src/PathPattern.test.ts +0 -648
  174. package/src/Random.ts +0 -59
  175. package/src/RouteBody.test.ts +0 -232
  176. package/src/RouteHook.test.ts +0 -40
  177. package/src/RouteHttp.test.ts +0 -2909
  178. package/src/RouteMount.test.ts +0 -481
  179. package/src/RouteSchema.test.ts +0 -427
  180. package/src/RouteSse.test.ts +0 -249
  181. package/src/RouteTree.test.ts +0 -494
  182. package/src/RouteTrie.test.ts +0 -322
  183. package/src/RouterPattern.test.ts +0 -676
  184. package/src/RouterPattern.ts +0 -416
  185. package/src/StartApp.ts +0 -47
  186. package/src/Values.test.ts +0 -263
  187. package/src/bun/BunBundle.test.ts +0 -268
  188. package/src/bun/BunBundle_imports.test.ts +0 -48
  189. package/src/bun/BunHttpServer.test.ts +0 -251
  190. package/src/bun/BunHttpServer.ts +0 -306
  191. package/src/bun/BunImportTrackerPlugin.test.ts +0 -77
  192. package/src/bun/BunRoute.test.ts +0 -162
  193. package/src/bundler/BundleHttp.test.ts +0 -132
  194. package/src/effect/HttpRouter.test.ts +0 -548
  195. package/src/experimental/EncryptedCookies.test.ts +0 -488
  196. package/src/hyper/HyperHtml.test.ts +0 -209
  197. package/src/hyper/HyperRoute.test.tsx +0 -197
  198. package/src/middlewares/BasicAuthMiddleware.test.ts +0 -84
  199. package/src/testing/TestHttpClient.test.ts +0 -83
  200. package/src/testing/TestLogger.test.ts +0 -51
  201. package/src/x/datastar/Datastar.test.ts +0 -266
  202. package/src/x/tailwind/TailwindPlugin.test.ts +0 -333
  203. /package/src/bun/{BunHttpServer_web.ts → BunServerRequest.ts} +0 -0
package/src/RouteHttp.ts CHANGED
@@ -17,14 +17,6 @@ import * as RouteMount from "./RouteMount.ts"
17
17
  import * as RouteTree from "./RouteTree.ts"
18
18
  import * as StreamExtra from "./StreamExtra.ts"
19
19
 
20
- export {
21
- currentSpanNameGenerator,
22
- currentTracerDisabledWhen,
23
- parentSpanFromHeaders,
24
- withSpanNameGenerator,
25
- withTracerDisabledWhen,
26
- } from "./RouteHttpTracer.ts"
27
-
28
20
  type UnboundedRouteWithMethod = Route.Route.With<{
29
21
  method: RouteMount.RouteMount.Method
30
22
  format?: RouteBody.Format
@@ -74,6 +66,12 @@ const getStatusFromCause = (cause: Cause.Cause<unknown>): number => {
74
66
  return 500
75
67
  }
76
68
 
69
+ const respondError = (options: { status: number; message: string }): Response =>
70
+ new Response(JSON.stringify(options, null, 2), {
71
+ status: options.status,
72
+ headers: { "content-type": "application/json" },
73
+ })
74
+
77
75
  function streamResponse(
78
76
  stream: Stream.Stream<unknown, unknown, unknown>,
79
77
  headers: Record<string, string | null | undefined>,
@@ -211,9 +209,7 @@ export const toWebHandlerRuntime = <R>(
211
209
 
212
210
  if (methodRoutes.length === 0 && wildcards.length === 0) {
213
211
  return Promise.resolve(
214
- Response.json({ status: 405, message: "method not allowed" }, {
215
- status: 405,
216
- }),
212
+ respondError({ status: 405, message: "method not allowed" }),
217
213
  )
218
214
  }
219
215
 
@@ -234,9 +230,7 @@ export const toWebHandlerRuntime = <R>(
234
230
  && !hasWildcardFormatRoutes
235
231
  ) {
236
232
  return Promise.resolve(
237
- Response.json({ status: 406, message: "not acceptable" }, {
238
- status: 406,
239
- }),
233
+ respondError({ status: 406, message: "not acceptable" }),
240
234
  )
241
235
  }
242
236
 
@@ -315,9 +309,7 @@ export const toWebHandlerRuntime = <R>(
315
309
  : Entity.make(result, { status: 200 })
316
310
 
317
311
  if (entity.status === 404 && entity.body === undefined) {
318
- return Response.json({ status: 406, message: "not acceptable" }, {
319
- status: 406,
320
- })
312
+ return respondError({ status: 406, message: "not acceptable" })
321
313
  }
322
314
 
323
315
  return yield* toResponse(entity, selectedFormat, runtime)
@@ -380,9 +372,8 @@ export const toWebHandlerRuntime = <R>(
380
372
  Effect.gen(function*() {
381
373
  yield* Effect.logError(cause)
382
374
  const status = getStatusFromCause(cause)
383
- return Response.json({ status, message: Cause.pretty(cause) }, {
384
- status,
385
- })
375
+ const message = Cause.pretty(cause, { renderErrorCause: true })
376
+ return respondError({ status, message })
386
377
  })
387
378
  ),
388
379
  ),
@@ -401,17 +392,12 @@ export const toWebHandlerRuntime = <R>(
401
392
  resolve(exit.value)
402
393
  } else if (isClientAbort(exit.cause)) {
403
394
  resolve(
404
- Response.json({ status: 499, message: "client closed request" }, {
405
- status: 499,
406
- }),
395
+ respondError({ status: 499, message: "client closed request" }),
407
396
  )
408
397
  } else {
409
398
  const status = getStatusFromCause(exit.cause)
410
- resolve(
411
- Response.json({ status, message: Cause.pretty(exit.cause) }, {
412
- status,
413
- }),
414
- )
399
+ const message = Cause.pretty(exit.cause, { renderErrorCause: true })
400
+ resolve(respondError({ status, message }))
415
401
  }
416
402
  })
417
403
  })
@@ -438,6 +424,6 @@ export function* walkHandles(
438
424
 
439
425
  const toHandler = toWebHandlerRuntime(runtime)
440
426
  for (const [path, routes] of pathGroups) {
441
- yield [path, toHandler(routes as Iterable<UnboundedRouteWithMethod>)]
427
+ yield [path, toHandler(routes)]
442
428
  }
443
429
  }
package/src/RouteMount.ts CHANGED
@@ -142,7 +142,7 @@ export type MountedRoute = Route.Route.Route<
142
142
  {
143
143
  method: RouteMount.Method
144
144
  path: PathPattern.PathPattern
145
- format?: string
145
+ format?: RouteBody.Format
146
146
  },
147
147
  {},
148
148
  any,
package/src/Start.ts CHANGED
@@ -1,15 +1,11 @@
1
- import * as FetchHttpClient from "@effect/platform/FetchHttpClient"
2
1
  import * as FileSystem from "@effect/platform/FileSystem"
3
- import * as HttpClient from "@effect/platform/HttpClient"
4
- import * as HttpRouter from "@effect/platform/HttpRouter"
5
- import * as HttpServer from "@effect/platform/HttpServer"
2
+ import * as Context from "effect/Context"
6
3
  import * as Effect from "effect/Effect"
7
4
  import * as Function from "effect/Function"
8
5
  import * as Layer from "effect/Layer"
9
- import * as BunHttpServer from "./bun/BunHttpServer.ts"
10
6
  import * as BunRuntime from "./bun/BunRuntime.ts"
11
- import * as NodeFileSystem from "./node/FileSystem.ts"
12
- import * as StartApp from "./StartApp.ts"
7
+ import * as BunServer from "./bun/BunServer.ts"
8
+ import * as NodeFileSystem from "./node/NodeFileSystem.ts"
13
9
 
14
10
  export function layer<
15
11
  Layers extends [
@@ -24,17 +20,56 @@ export function layer<
24
20
  return Layer.mergeAll(...layers)
25
21
  }
26
22
 
27
- export function serve<ROut, E>(
23
+ /**
24
+ * Bundles layers together, wiring their dependencies automatically.
25
+ *
26
+ * Equivalent to chaining `Layer.provide` calls, but more concise.
27
+ *
28
+ * **Ordering: dependents first, dependencies last.**
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * // UserRepo needs Database, Database needs Logger
33
+ * const AppLayer = Start.pack(
34
+ * UserRepoLive, // needs Database, Logger
35
+ * DatabaseLive, // needs Logger
36
+ * LoggerLive, // no deps
37
+ * )
38
+ * // Result: Layer<UserRepo | Database | Logger, never, never>
39
+ * ```
40
+ *
41
+ * @since 1.0.0
42
+ * @category constructors
43
+ */
44
+ export function pack<
45
+ const Layers extends readonly [Layer.Layer.Any, ...Array<Layer.Layer.Any>],
46
+ >(
47
+ ...layers: Layers
48
+ ): Layer.Layer<
49
+ { [K in keyof Layers]: Layer.Layer.Success<Layers[K]> }[number],
50
+ { [K in keyof Layers]: Layer.Layer.Error<Layers[K]> }[number],
51
+ Exclude<
52
+ { [K in keyof Layers]: Layer.Layer.Context<Layers[K]> }[number],
53
+ { [K in keyof Layers]: Layer.Layer.Success<Layers[K]> }[number]
54
+ >
55
+ > {
56
+ type AnyLayer = Layer.Layer<any, any, any>
57
+ const layerArray = layers as unknown as ReadonlyArray<AnyLayer>
58
+ const result: AnyLayer = layerArray.reduce(
59
+ (acc: AnyLayer, layer: AnyLayer) => Layer.provideMerge(acc, layer),
60
+ Layer.succeedContext(Context.empty()) as unknown as AnyLayer,
61
+ )
62
+
63
+ return result as AnyLayer
64
+ }
65
+
66
+ export function serve<
67
+ ROut,
68
+ E,
69
+ RIn extends BunServer.BunServer | FileSystem.FileSystem,
70
+ >(
28
71
  load: () => Promise<{
29
- default: Layer.Layer<
30
- ROut,
31
- E,
32
- | HttpServer.HttpServer
33
- | HttpClient.HttpClient
34
- | HttpRouter.Default
35
- | FileSystem.FileSystem
36
- | BunHttpServer.BunHttpServer
37
- >
72
+ default: Layer.Layer<ROut, E, RIn>
38
73
  }>,
39
74
  ) {
40
75
  const appLayer = Function.pipe(
@@ -44,17 +79,16 @@ export function serve<ROut, E>(
44
79
  Layer.unwrapEffect,
45
80
  )
46
81
 
47
- return Function.pipe(
48
- BunHttpServer.layerAuto(),
49
- HttpServer.withLogAddress,
82
+ const composed = Function.pipe(
83
+ BunServer.layer(),
84
+ BunServer.withLogAddress,
50
85
  Layer.provide(appLayer),
51
- Layer.provide([
52
- FetchHttpClient.layer,
53
- HttpRouter.Default.Live,
54
- BunHttpServer.layer(),
55
- NodeFileSystem.layer,
56
- StartApp.layer(),
57
- ]),
86
+ Layer.provide(NodeFileSystem.layer),
87
+ Layer.provide(BunServer.layer()),
88
+ ) as Layer.Layer<BunServer.BunServer, never, never>
89
+
90
+ return Function.pipe(
91
+ composed,
58
92
  Layer.launch,
59
93
  BunRuntime.runMain,
60
94
  )
package/src/Unique.ts ADDED
@@ -0,0 +1,232 @@
1
+ export const ALPHABET_BASE32_CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
2
+ export const ALPHABET_BASE32_RFC4648 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"
3
+ export const ALPHABET_BASE64_URL =
4
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
5
+ export const ALPHABET_HEX = "0123456789abcdef"
6
+
7
+ /**
8
+ * Generate a random string for ids, session tokens, and API keys.
9
+ * It uses human-friendly crockford base32 encoding (5 bit of entropy per char)
10
+ *
11
+ * Minimal recommended length:
12
+ * - public ids: 16 chars (~80 bits)
13
+ * - API keys: 32 chars (~160 bits)
14
+ * - session tokens: 32-40 chars (~160-200 bits)
15
+ */
16
+ export function token(length = 32): string {
17
+ if (length <= 0) return ""
18
+
19
+ const buf = new Uint8Array(length)
20
+ crypto.getRandomValues(buf)
21
+
22
+ let result = ""
23
+ for (let i = 0; i < buf.length; i++) {
24
+ result += ALPHABET_BASE32_CROCKFORD[buf[i] & 31]
25
+ }
26
+
27
+ return result
28
+ }
29
+
30
+ export function bytes(length: number): Uint8Array {
31
+ const buf = new Uint8Array(length)
32
+ crypto.getRandomValues(buf)
33
+ return buf
34
+ }
35
+
36
+ export const UUID_NIL = "00000000-0000-0000-0000-000000000000"
37
+
38
+ export function uuid4(): string {
39
+ return formatUuid(uuid4bytes())
40
+ }
41
+
42
+ export function uuid7(time: number = Date.now()): string {
43
+ return formatUuid(uuid7Bytes(time))
44
+ }
45
+
46
+ function uuid4bytes(): Uint8Array {
47
+ const buf = bytes(16)
48
+ buf[6] = (buf[6] & 0x0f) | 0x40 // version 4
49
+ buf[8] = (buf[8] & 0x3f) | 0x80 // variant
50
+
51
+ return buf
52
+ }
53
+
54
+ /**
55
+ * Decode a 48-bit Unix timestamp (ms) from UUID7 or ULID.
56
+ *
57
+ * @example
58
+ * const bytes = Unique.uuid7Bytes()
59
+ * const timestamp = Unique.toTimestamp(bytes)
60
+ *
61
+ * @example
62
+ * const bytes = Unique.ulidBytes()
63
+ * const timestamp = Unique.toTimestamp(bytes)
64
+ */
65
+ export function toTimestamp(bytes: Uint8Array): number {
66
+ if (bytes.length < 6) return 0
67
+
68
+ return (
69
+ bytes[0] * 0x10000000000
70
+ + bytes[1] * 0x100000000
71
+ + bytes[2] * 0x1000000
72
+ + bytes[3] * 0x10000
73
+ + bytes[4] * 0x100
74
+ + bytes[5]
75
+ )
76
+ }
77
+
78
+ export function uuid7Bytes(time: number = Date.now()): Uint8Array {
79
+ const buf = new Uint8Array(16)
80
+ const timestamp = BigInt(toSafeTime(time))
81
+
82
+ // 48-bit timestamp (6 bytes)
83
+ buf[0] = Number((timestamp >> 40n) & 0xffn)
84
+ buf[1] = Number((timestamp >> 32n) & 0xffn)
85
+ buf[2] = Number((timestamp >> 24n) & 0xffn)
86
+ buf[3] = Number((timestamp >> 16n) & 0xffn)
87
+ buf[4] = Number((timestamp >> 8n) & 0xffn)
88
+ buf[5] = Number(timestamp & 0xffn)
89
+
90
+ // 12-bit random A (1.5 bytes)
91
+ crypto.getRandomValues(buf.subarray(6, 8))
92
+ buf[6] = (buf[6] & 0x0f) | 0x70 // version 7
93
+
94
+ // 2-bit variant + 62-bit random B (8 bytes)
95
+ crypto.getRandomValues(buf.subarray(8, 16))
96
+ buf[8] = (buf[8] & 0x3f) | 0x80 // variant
97
+
98
+ return buf
99
+ }
100
+
101
+ /**
102
+ * Convert UUID bytes to canonical (RFC9562) representation.
103
+ *
104
+ * @example
105
+ * Unique.formatUuid(new Uint8Array(16))
106
+ */
107
+ export function formatUuid(bytes: Uint8Array): string {
108
+ if (bytes.length === 0) return ""
109
+
110
+ let result = ""
111
+
112
+ for (let i = 0; i < bytes.length; i++) {
113
+ const byte = bytes[i]
114
+ result += ALPHABET_HEX[(byte >> 4) & 0x0f]
115
+ result += ALPHABET_HEX[byte & 0x0f]
116
+
117
+ if (i === 3 || i === 5 || i === 7 || i === 9) result += "-"
118
+ }
119
+
120
+ return result
121
+ }
122
+
123
+ export function ulid(time: number = Date.now()): string {
124
+ const bytes = ulidBytes(time)
125
+ return formatUlid(bytes)
126
+ }
127
+
128
+ export function ulidBytes(time: number = Date.now()): Uint8Array {
129
+ const buf = new Uint8Array(16)
130
+ const timestamp = BigInt(toSafeTime(time))
131
+
132
+ buf[0] = Number((timestamp >> 40n) & 0xffn)
133
+ buf[1] = Number((timestamp >> 32n) & 0xffn)
134
+ buf[2] = Number((timestamp >> 24n) & 0xffn)
135
+ buf[3] = Number((timestamp >> 16n) & 0xffn)
136
+ buf[4] = Number((timestamp >> 8n) & 0xffn)
137
+ buf[5] = Number(timestamp & 0xffn)
138
+
139
+ crypto.getRandomValues(buf.subarray(6, 16))
140
+
141
+ return buf
142
+ }
143
+
144
+ function formatUlid(bytes: Uint8Array): string {
145
+ if (bytes.length !== 16) return ""
146
+
147
+ const timestamp = toTimestamp(bytes)
148
+ const timePart = encodeUlidTime(timestamp)
149
+ const randomPart = toBase32(bytes.subarray(6, 16))
150
+
151
+ return `${timePart}${randomPart}`
152
+ }
153
+
154
+ function encodeUlidTime(time: number): string {
155
+ let value = BigInt(time)
156
+ const result = new Array<string>(10)
157
+
158
+ for (let i = 9; i >= 0; i--) {
159
+ result[i] = ALPHABET_BASE32_CROCKFORD[Number(value & 31n)]
160
+ value >>= 5n
161
+ }
162
+
163
+ return result.join("")
164
+ }
165
+
166
+ function toSafeTime(time: number): number {
167
+ if (!Number.isFinite(time)) return 0
168
+
169
+ return Math.max(0, Math.trunc(time))
170
+ }
171
+
172
+ /**
173
+ * Generate a nanoid-style random string.
174
+ *
175
+ * FUN_FACT: Original nanoid implementation uses base64url alphabet
176
+ * with non-standard custom order where charater form common words found
177
+ * in source code (like use, random, strict) to make gzip/brotli more efficient.
178
+ * It's qt lil opt from the times where web developers
179
+ * were competing to have the smallest possible bundle size.
180
+ */
181
+ export function nanoid(
182
+ size = 21,
183
+ alphabet = ALPHABET_BASE64_URL,
184
+ ): string {
185
+ if (size <= 0 || alphabet.length === 0) return ""
186
+
187
+ const length = alphabet.length
188
+ const mask = (2 << Math.floor(Math.log2(length - 1))) - 1
189
+ const step = Math.ceil((1.6 * mask * size) / length)
190
+
191
+ let id = ""
192
+ while (id.length < size) {
193
+ const bytes = new Uint8Array(step)
194
+ crypto.getRandomValues(bytes)
195
+
196
+ for (let i = 0; i < step && id.length < size; i++) {
197
+ const index = bytes[i] & mask
198
+ if (index < length) id += alphabet[index]
199
+ }
200
+ }
201
+ return id
202
+ }
203
+
204
+ function toBase32(
205
+ bytes: Uint8Array,
206
+ alphabet = ALPHABET_BASE32_CROCKFORD,
207
+ ): string {
208
+ if (bytes.length === 0) return ""
209
+
210
+ let result = ""
211
+ let buffer = 0
212
+ let bits = 0
213
+
214
+ for (const byte of bytes) {
215
+ buffer = (buffer << 8) | byte
216
+ bits += 8
217
+
218
+ while (bits >= 5) {
219
+ bits -= 5
220
+ const index = (buffer >> bits) & 31
221
+ result += alphabet[index]
222
+ buffer &= (1 << bits) - 1
223
+ }
224
+ }
225
+
226
+ if (bits > 0) {
227
+ const index = (buffer << (5 - bits)) & 31
228
+ result += alphabet[index]
229
+ }
230
+
231
+ return result
232
+ }
@@ -0,0 +1,88 @@
1
+ import * as HttpApp from "@effect/platform/HttpApp"
2
+ import * as HttpServer from "@effect/platform/HttpServer"
3
+ import * as HttpServerError from "@effect/platform/HttpServerError"
4
+ import * as HttpServerRequest from "@effect/platform/HttpServerRequest"
5
+ import type * as Bun from "bun"
6
+ import * as Effect from "effect/Effect"
7
+ import * as FiberSet from "effect/FiberSet"
8
+ import type * as Scope from "effect/Scope"
9
+ import * as BunServer from "./BunServer.ts"
10
+ import * as BunServerRequest from "./BunServerRequest.ts"
11
+
12
+ /**
13
+ * From times when we used @effect/platform
14
+ * Not used any more internally. Kept for the future,
15
+ * in case someone will need it for whatever reason. [2026]
16
+ */
17
+ export const make: Effect.Effect<
18
+ HttpServer.HttpServer,
19
+ never,
20
+ Scope.Scope | BunServer.BunServer
21
+ > = Effect.gen(function*() {
22
+ const bunServer = yield* BunServer.BunServer
23
+
24
+ return HttpServer.make({
25
+ address: {
26
+ _tag: "TcpAddress",
27
+ port: bunServer.server.port!,
28
+ hostname: bunServer.server.hostname!,
29
+ },
30
+ serve(httpApp, middleware) {
31
+ return Effect.gen(function*() {
32
+ const runFork = yield* FiberSet.makeRuntime<never>()
33
+ const runtime = yield* Effect.runtime<never>()
34
+ const app = HttpApp.toHandled(
35
+ httpApp,
36
+ (request, response) =>
37
+ Effect.sync(() => {
38
+ ;(request as BunServerRequest.ServerRequestImpl).resolve(
39
+ BunServerRequest.makeResponse(request, response, runtime),
40
+ )
41
+ }),
42
+ middleware,
43
+ )
44
+
45
+ function handler(
46
+ request: Request,
47
+ server: Bun.Server<BunServerRequest.WebSocketContext>,
48
+ ) {
49
+ return new Promise<Response>((resolve, _reject) => {
50
+ const fiber = runFork(Effect.provideService(
51
+ app,
52
+ HttpServerRequest.HttpServerRequest,
53
+ new BunServerRequest.ServerRequestImpl(
54
+ request,
55
+ resolve,
56
+ removeHost(request.url),
57
+ server,
58
+ ),
59
+ ))
60
+ request.signal.addEventListener("abort", () => {
61
+ runFork(
62
+ fiber.interruptAsFork(HttpServerError.clientAbortFiberId),
63
+ )
64
+ }, { once: true })
65
+ })
66
+ }
67
+
68
+ yield* Effect.acquireRelease(
69
+ Effect.sync(() => {
70
+ bunServer.pushHandler(handler)
71
+ }),
72
+ () =>
73
+ Effect.sync(() => {
74
+ bunServer.popHandler()
75
+ }),
76
+ )
77
+ })
78
+ },
79
+ })
80
+ })
81
+
82
+ const removeHost = (url: string) => {
83
+ if (url[0] === "/") {
84
+ return url
85
+ }
86
+ const index = url.indexOf("/", url.indexOf("//") + 2)
87
+ return index === -1 ? "/" : url.slice(index)
88
+ }
@@ -4,12 +4,12 @@ import * as Data from "effect/Data"
4
4
  import * as Effect from "effect/Effect"
5
5
  import * as Option from "effect/Option"
6
6
  import * as Entity from "../Entity.ts"
7
+ import * as FilePathPattern from "../FilePathPattern.ts"
7
8
  import * as Hyper from "../hyper/Hyper.ts"
8
9
  import * as HyperHtml from "../hyper/HyperHtml.ts"
9
- import * as Random from "../Random.ts"
10
10
  import * as Route from "../Route.ts"
11
- import * as RouterPattern from "../RouterPattern.ts"
12
- import * as BunHttpServer from "./BunHttpServer.ts"
11
+ import * as Unique from "../Unique.ts"
12
+ import * as BunServer from "./BunServer.ts"
13
13
 
14
14
  const INTERNAL_FETCH_HEADER = "x-effect-start-internal-fetch"
15
15
 
@@ -25,7 +25,7 @@ export type BunDescriptors = {
25
25
  }
26
26
 
27
27
  export function descriptors(
28
- route: Route.Route.Route,
28
+ route: Route.Route.Route<any, any, any, any, any>,
29
29
  ): BunDescriptors | undefined {
30
30
  const descriptor = Route.descriptor(route) as Partial<BunDescriptors>
31
31
  if (
@@ -40,7 +40,7 @@ export function descriptors(
40
40
  export function htmlBundle(
41
41
  load: () => Promise<Bun.HTMLBundle | { default: Bun.HTMLBundle }>,
42
42
  ) {
43
- const bunPrefix = `/.BunRoute-${Random.token(6)}`
43
+ const bunPrefix = `/.BunRoute-${Unique.token(10)}`
44
44
  const bunLoad = () => load().then(mod => "default" in mod ? mod.default : mod)
45
45
  const descriptors = { bunPrefix, bunLoad, format: "html" as const }
46
46
 
@@ -60,7 +60,7 @@ export function htmlBundle(
60
60
  { request: Request },
61
61
  string,
62
62
  BunRouteError,
63
- BunHttpServer.BunHttpServer
63
+ BunServer.BunServer
64
64
  >,
65
65
  ]
66
66
  > {
@@ -68,7 +68,7 @@ export function htmlBundle(
68
68
  BunDescriptors & { format: "html" } & { request: Request },
69
69
  string,
70
70
  BunRouteError,
71
- BunHttpServer.BunHttpServer
71
+ BunServer.BunServer
72
72
  > = (context, next) =>
73
73
  Effect.gen(function*() {
74
74
  const originalRequest = context.request
@@ -87,7 +87,7 @@ export function htmlBundle(
87
87
  )
88
88
  }
89
89
 
90
- const bunServer = yield* BunHttpServer.BunHttpServer
90
+ const bunServer = yield* BunServer.BunServer
91
91
  const url = new URL(originalRequest.url)
92
92
 
93
93
  const internalPath = `${bunPrefix}${url.pathname}`
@@ -152,7 +152,7 @@ export function htmlBundle(
152
152
  { request: Request },
153
153
  string,
154
154
  BunRouteError,
155
- BunHttpServer.BunHttpServer
155
+ BunServer.BunServer
156
156
  >(handler, descriptors)
157
157
 
158
158
  return Route.set(
@@ -181,9 +181,7 @@ export type BunRoutes = Record<string, BunServerRouteHandler>
181
181
  * - /exact - Exact match
182
182
  * - /users/:id - Full-segment named param
183
183
  * - /path/* - Directory wildcard
184
- * - /* - Catch-all
185
- * - /[[id]] - Optional param (implemented via `/` and `/:id`)
186
- * - /[[...rest]] - Optional rest param (implemented via `/` and `/*`)
184
+ * - /[[404]] - Catch-all / Rest
187
185
  *
188
186
  * Unsupported patterns (cannot be implemented in Bun):
189
187
  * - /pk_[id] - Prefix before param
@@ -196,24 +194,16 @@ export type BunRoutes = Record<string, BunServerRouteHandler>
196
194
  export function validateBunPattern(
197
195
  pattern: string,
198
196
  ): Option.Option<BunRouteError> {
199
- const segments = RouterPattern.parse(pattern)
197
+ const segs = FilePathPattern.segments(pattern)
200
198
 
201
- const unsupported = Array.findFirst(segments, (seg) => {
202
- if (seg._tag === "ParamSegment") {
203
- return seg.prefix !== undefined || seg.suffix !== undefined
204
- }
199
+ const invalid = Array.findFirst(segs, (seg) => seg._tag === "InvalidSegment")
205
200
 
206
- return false
207
- })
208
-
209
- if (Option.isSome(unsupported)) {
201
+ if (Option.isSome(invalid)) {
210
202
  return Option.some(
211
203
  new BunRouteError({
212
204
  reason: "UnsupportedPattern",
213
205
  pattern,
214
- message:
215
- `Pattern "${pattern}" uses prefixed/suffixed params (prefix_[param] or [param]_suffix) `
216
- + `which cannot be implemented in Bun.serve.`,
206
+ message: `Pattern "${pattern}" contains invalid segment.`,
217
207
  }),
218
208
  )
219
209
  }
@@ -1,11 +1,24 @@
1
- import { makeRunMain } from "@effect/platform/Runtime"
2
- import { constVoid } from "effect/Function"
1
+ import type * as Fiber from "effect/Fiber"
2
+ import * as GlobalValue from "effect/GlobalValue"
3
+ import * as MutableRef from "effect/MutableRef"
4
+ import * as PlatformRuntime from "../PlatformRuntime.ts"
3
5
 
4
- export const runMain = makeRunMain(({
6
+ const mainFiber = GlobalValue.globalValue(
7
+ Symbol.for("effect-start/BunRuntime/existingFiber"),
8
+ () =>
9
+ MutableRef.make<Fiber.RuntimeFiber<unknown, unknown> | undefined>(
10
+ undefined,
11
+ ),
12
+ )
13
+
14
+ export const runMain = PlatformRuntime.makeRunMain(({
5
15
  fiber,
6
16
  teardown,
7
17
  }) => {
8
- const keepAlive = setInterval(constVoid, 2 ** 31 - 1)
18
+ const prevFiber = MutableRef.get(mainFiber)
19
+
20
+ MutableRef.set(mainFiber, fiber)
21
+
9
22
  let receivedSignal = false
10
23
 
11
24
  fiber.addObserver((exit) => {
@@ -13,7 +26,6 @@ export const runMain = makeRunMain(({
13
26
  process.removeListener("SIGINT", onSigint)
14
27
  process.removeListener("SIGTERM", onSigint)
15
28
  }
16
- clearInterval(keepAlive)
17
29
  teardown(exit, (code) => {
18
30
  if (receivedSignal || code !== 0) {
19
31
  process.exit(code)
@@ -30,4 +42,8 @@ export const runMain = makeRunMain(({
30
42
 
31
43
  process.on("SIGINT", onSigint)
32
44
  process.on("SIGTERM", onSigint)
45
+
46
+ if (prevFiber) {
47
+ prevFiber.unsafeInterruptAsFork(prevFiber.id())
48
+ }
33
49
  })