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.
- package/README.md +3 -3
- package/dist/Development.d.ts +8 -3
- package/dist/Development.js +14 -7
- package/dist/Effectify.d.ts +212 -0
- package/dist/Effectify.js +19 -0
- package/dist/FilePathPattern.d.ts +29 -0
- package/dist/FilePathPattern.js +86 -0
- package/dist/FileRouter.d.ts +39 -41
- package/dist/FileRouter.js +104 -158
- package/dist/FileRouterCodegen.d.ts +7 -8
- package/dist/FileRouterCodegen.js +97 -66
- package/dist/PlatformError.d.ts +46 -0
- package/dist/PlatformError.js +43 -0
- package/dist/PlatformRuntime.d.ts +27 -0
- package/dist/PlatformRuntime.js +51 -0
- package/dist/Route.d.ts +6 -2
- package/dist/Route.js +22 -0
- package/dist/RouteBody.d.ts +1 -1
- package/dist/RouteHttp.d.ts +1 -1
- package/dist/RouteHttp.js +12 -19
- package/dist/RouteMount.d.ts +2 -1
- package/dist/Start.d.ts +33 -6
- package/dist/Start.js +31 -13
- package/dist/Unique.d.ts +50 -0
- package/dist/Unique.js +187 -0
- package/dist/bun/BunHttpServer.js +5 -6
- package/dist/bun/BunPlatformHttpServer.d.ts +10 -0
- package/dist/bun/BunPlatformHttpServer.js +53 -0
- package/dist/bun/BunRoute.d.ts +4 -6
- package/dist/bun/BunRoute.js +10 -18
- package/dist/bun/BunRuntime.d.ts +2 -1
- package/dist/bun/BunRuntime.js +10 -5
- package/dist/bun/BunServer.d.ts +33 -0
- package/dist/bun/BunServer.js +133 -0
- package/dist/bun/BunServerRequest.d.ts +60 -0
- package/dist/bun/BunServerRequest.js +252 -0
- package/dist/bun/index.d.ts +1 -1
- package/dist/bun/index.js +1 -1
- package/dist/datastar/actions/fetch.d.ts +30 -0
- package/dist/datastar/actions/fetch.js +411 -0
- package/dist/datastar/actions/peek.d.ts +1 -0
- package/dist/datastar/actions/peek.js +14 -0
- package/dist/datastar/actions/setAll.d.ts +1 -0
- package/dist/datastar/actions/setAll.js +13 -0
- package/dist/datastar/actions/toggleAll.d.ts +1 -0
- package/dist/datastar/actions/toggleAll.js +13 -0
- package/dist/datastar/attributes/attr.d.ts +1 -0
- package/dist/datastar/attributes/attr.js +49 -0
- package/dist/datastar/attributes/bind.d.ts +1 -0
- package/dist/datastar/attributes/bind.js +183 -0
- package/dist/datastar/attributes/class.d.ts +1 -0
- package/dist/datastar/attributes/class.js +50 -0
- package/dist/datastar/attributes/computed.d.ts +1 -0
- package/dist/datastar/attributes/computed.js +27 -0
- package/dist/datastar/attributes/effect.d.ts +1 -0
- package/dist/datastar/attributes/effect.js +10 -0
- package/dist/datastar/attributes/indicator.d.ts +1 -0
- package/dist/datastar/attributes/indicator.js +32 -0
- package/dist/datastar/attributes/init.d.ts +1 -0
- package/dist/datastar/attributes/init.js +27 -0
- package/dist/datastar/attributes/jsonSignals.d.ts +1 -0
- package/dist/datastar/attributes/jsonSignals.js +31 -0
- package/dist/datastar/attributes/on.d.ts +1 -0
- package/dist/datastar/attributes/on.js +59 -0
- package/dist/datastar/attributes/onIntersect.d.ts +1 -0
- package/dist/datastar/attributes/onIntersect.js +54 -0
- package/dist/datastar/attributes/onInterval.d.ts +1 -0
- package/dist/datastar/attributes/onInterval.js +31 -0
- package/dist/datastar/attributes/onSignalPatch.d.ts +1 -0
- package/dist/datastar/attributes/onSignalPatch.js +44 -0
- package/dist/datastar/attributes/ref.d.ts +1 -0
- package/dist/datastar/attributes/ref.js +11 -0
- package/dist/datastar/attributes/show.d.ts +1 -0
- package/dist/datastar/attributes/show.js +32 -0
- package/dist/datastar/attributes/signals.d.ts +1 -0
- package/dist/datastar/attributes/signals.js +18 -0
- package/dist/datastar/attributes/style.d.ts +1 -0
- package/dist/datastar/attributes/style.js +56 -0
- package/dist/datastar/attributes/text.d.ts +1 -0
- package/dist/datastar/attributes/text.js +27 -0
- package/dist/datastar/engine.d.ts +156 -0
- package/dist/datastar/engine.js +971 -0
- package/dist/datastar/index.d.ts +24 -0
- package/dist/datastar/index.js +24 -0
- package/dist/datastar/load.d.ts +24 -0
- package/dist/datastar/load.js +24 -0
- package/dist/datastar/utils.d.ts +51 -0
- package/dist/datastar/utils.js +205 -0
- package/dist/datastar/watchers/patchElements.d.ts +1 -0
- package/dist/datastar/watchers/patchElements.js +420 -0
- package/dist/datastar/watchers/patchSignals.d.ts +1 -0
- package/dist/datastar/watchers/patchSignals.js +15 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/node/Effectify.d.ts +209 -0
- package/dist/node/Effectify.js +19 -0
- package/dist/node/FileSystem.d.ts +3 -5
- package/dist/node/FileSystem.js +42 -62
- package/dist/node/NodeFileSystem.d.ts +7 -0
- package/dist/node/NodeFileSystem.js +420 -0
- package/dist/node/NodeUtils.d.ts +2 -0
- package/dist/node/NodeUtils.js +20 -0
- package/dist/node/PlatformError.d.ts +46 -0
- package/dist/node/PlatformError.js +43 -0
- package/dist/testing/TestLogger.js +1 -1
- package/dist/x/tailwind/plugin.js +1 -1
- package/package.json +18 -7
- package/src/Development.ts +36 -40
- package/src/Effectify.ts +269 -0
- package/src/FilePathPattern.ts +115 -0
- package/src/FileRouter.ts +178 -255
- package/src/FileRouterCodegen.ts +135 -92
- package/src/PlatformError.ts +117 -0
- package/src/PlatformRuntime.ts +108 -0
- package/src/Route.ts +31 -2
- package/src/RouteBody.ts +1 -1
- package/src/RouteHttp.ts +15 -29
- package/src/RouteMount.ts +1 -1
- package/src/Start.ts +61 -27
- package/src/Unique.ts +232 -0
- package/src/bun/BunPlatformHttpServer.ts +88 -0
- package/src/bun/BunRoute.ts +14 -24
- package/src/bun/BunRuntime.ts +21 -5
- package/src/bun/BunServer.ts +228 -0
- package/src/bun/index.ts +1 -1
- package/src/datastar/README.md +18 -0
- package/src/datastar/actions/fetch.ts +609 -0
- package/src/datastar/actions/peek.ts +17 -0
- package/src/datastar/actions/setAll.ts +20 -0
- package/src/datastar/actions/toggleAll.ts +20 -0
- package/src/datastar/attributes/attr.ts +50 -0
- package/src/datastar/attributes/bind.ts +220 -0
- package/src/datastar/attributes/class.ts +57 -0
- package/src/datastar/attributes/computed.ts +33 -0
- package/src/datastar/attributes/effect.ts +11 -0
- package/src/datastar/attributes/indicator.ts +39 -0
- package/src/datastar/attributes/init.ts +35 -0
- package/src/datastar/attributes/jsonSignals.ts +38 -0
- package/src/datastar/attributes/on.ts +71 -0
- package/src/datastar/attributes/onIntersect.ts +65 -0
- package/src/datastar/attributes/onInterval.ts +39 -0
- package/src/datastar/attributes/onSignalPatch.ts +63 -0
- package/src/datastar/attributes/ref.ts +12 -0
- package/src/datastar/attributes/show.ts +33 -0
- package/src/datastar/attributes/signals.ts +22 -0
- package/src/datastar/attributes/style.ts +63 -0
- package/src/datastar/attributes/text.ts +30 -0
- package/src/datastar/engine.ts +1341 -0
- package/src/datastar/index.ts +25 -0
- package/src/datastar/utils.ts +286 -0
- package/src/datastar/watchers/patchElements.ts +554 -0
- package/src/datastar/watchers/patchSignals.ts +15 -0
- package/src/index.ts +1 -0
- package/src/node/{FileSystem.ts → NodeFileSystem.ts} +59 -97
- package/src/node/{Utils.ts → NodeUtils.ts} +2 -0
- package/src/testing/TestLogger.ts +1 -1
- package/src/x/tailwind/plugin.ts +1 -1
- package/dist/Random.d.ts +0 -5
- package/dist/Random.js +0 -49
- package/src/Commander.test.ts +0 -1639
- package/src/ContentNegotiation.test.ts +0 -603
- package/src/Development.test.ts +0 -119
- package/src/Entity.test.ts +0 -592
- package/src/FileRouterCodegen.todo.ts +0 -1133
- package/src/FileRouterPattern.test.ts +0 -147
- package/src/FileRouterPattern.ts +0 -59
- package/src/FileRouter_files.test.ts +0 -64
- package/src/FileRouter_path.test.ts +0 -145
- package/src/FileRouter_tree.test.ts +0 -132
- package/src/Http.test.ts +0 -319
- package/src/HttpAppExtra.test.ts +0 -103
- package/src/HttpUtils.test.ts +0 -85
- package/src/PathPattern.test.ts +0 -648
- package/src/Random.ts +0 -59
- package/src/RouteBody.test.ts +0 -232
- package/src/RouteHook.test.ts +0 -40
- package/src/RouteHttp.test.ts +0 -2909
- package/src/RouteMount.test.ts +0 -481
- package/src/RouteSchema.test.ts +0 -427
- package/src/RouteSse.test.ts +0 -249
- package/src/RouteTree.test.ts +0 -494
- package/src/RouteTrie.test.ts +0 -322
- package/src/RouterPattern.test.ts +0 -676
- package/src/RouterPattern.ts +0 -416
- package/src/StartApp.ts +0 -47
- package/src/Values.test.ts +0 -263
- package/src/bun/BunBundle.test.ts +0 -268
- package/src/bun/BunBundle_imports.test.ts +0 -48
- package/src/bun/BunHttpServer.test.ts +0 -251
- package/src/bun/BunHttpServer.ts +0 -306
- package/src/bun/BunImportTrackerPlugin.test.ts +0 -77
- package/src/bun/BunRoute.test.ts +0 -162
- package/src/bundler/BundleHttp.test.ts +0 -132
- package/src/effect/HttpRouter.test.ts +0 -548
- package/src/experimental/EncryptedCookies.test.ts +0 -488
- package/src/hyper/HyperHtml.test.ts +0 -209
- package/src/hyper/HyperRoute.test.tsx +0 -197
- package/src/middlewares/BasicAuthMiddleware.test.ts +0 -84
- package/src/testing/TestHttpClient.test.ts +0 -83
- package/src/testing/TestLogger.test.ts +0 -51
- package/src/x/datastar/Datastar.test.ts +0 -266
- package/src/x/tailwind/TailwindPlugin.test.ts +0 -333
- /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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
384
|
-
|
|
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
|
-
|
|
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
|
-
|
|
411
|
-
|
|
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
|
|
427
|
+
yield [path, toHandler(routes)]
|
|
442
428
|
}
|
|
443
429
|
}
|
package/src/RouteMount.ts
CHANGED
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
|
|
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
|
|
12
|
-
import * as
|
|
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
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
82
|
+
const composed = Function.pipe(
|
|
83
|
+
BunServer.layer(),
|
|
84
|
+
BunServer.withLogAddress,
|
|
50
85
|
Layer.provide(appLayer),
|
|
51
|
-
Layer.provide(
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
+
}
|
package/src/bun/BunRoute.ts
CHANGED
|
@@ -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
|
|
12
|
-
import * as
|
|
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-${
|
|
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
|
-
|
|
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
|
-
|
|
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*
|
|
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
|
-
|
|
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
|
-
* -
|
|
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
|
|
197
|
+
const segs = FilePathPattern.segments(pattern)
|
|
200
198
|
|
|
201
|
-
const
|
|
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
|
-
|
|
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
|
}
|
package/src/bun/BunRuntime.ts
CHANGED
|
@@ -1,11 +1,24 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
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
|
-
|
|
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
|
|
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
|
})
|