effect-start 0.17.2 → 0.19.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/dist/Development.d.ts +7 -2
- package/dist/Development.js +12 -6
- package/dist/PlatformRuntime.d.ts +4 -0
- package/dist/PlatformRuntime.js +9 -0
- package/dist/Route.d.ts +6 -2
- package/dist/Route.js +22 -0
- 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 +1 -5
- package/dist/Start.js +1 -8
- package/dist/Unique.d.ts +50 -0
- package/dist/Unique.js +187 -0
- package/dist/bun/BunHttpServer.js +5 -6
- package/dist/bun/BunRoute.d.ts +1 -1
- package/dist/bun/BunRoute.js +2 -2
- 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/PlatformError.d.ts +46 -0
- package/dist/node/PlatformError.js +43 -0
- package/dist/testing/TestLogger.js +1 -1
- package/package.json +10 -5
- package/src/Development.ts +13 -18
- package/src/PlatformRuntime.ts +11 -0
- package/src/Route.ts +31 -2
- package/src/RouteHttp.ts +15 -31
- package/src/RouteMount.ts +1 -1
- package/src/Start.ts +1 -15
- package/src/Unique.ts +232 -0
- package/src/bun/BunHttpServer.ts +6 -9
- package/src/bun/BunRoute.ts +3 -3
- package/src/index.ts +1 -0
- package/src/node/Effectify.ts +262 -0
- package/src/node/FileSystem.ts +59 -97
- package/src/node/PlatformError.ts +102 -0
- package/src/testing/TestLogger.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/FileRouterPattern.test.ts +0 -147
- 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/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/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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "effect-start",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"exports": {
|
|
@@ -44,6 +44,11 @@
|
|
|
44
44
|
"types": "./dist/middlewares/index.d.ts",
|
|
45
45
|
"default": "./dist/middlewares/index.js"
|
|
46
46
|
},
|
|
47
|
+
"./Unique": {
|
|
48
|
+
"bun": "./src/Unique.ts",
|
|
49
|
+
"types": "./dist/Unique.d.ts",
|
|
50
|
+
"default": "./dist/Unique.js"
|
|
51
|
+
},
|
|
47
52
|
"./experimental": {
|
|
48
53
|
"bun": "./src/experimental/index.ts",
|
|
49
54
|
"types": "./dist/experimental/index.d.ts",
|
|
@@ -74,14 +79,12 @@
|
|
|
74
79
|
"test": "bun test ./src/",
|
|
75
80
|
"deploy": "bunx --bun npm publish --access public"
|
|
76
81
|
},
|
|
77
|
-
"
|
|
82
|
+
"peerDependencies": {
|
|
78
83
|
"effect": ">=3.19.0",
|
|
79
84
|
"@effect/platform": ">=0.93.0"
|
|
80
85
|
},
|
|
81
|
-
"peerDependencies": {
|
|
82
|
-
"typescript": "^5.9.3"
|
|
83
|
-
},
|
|
84
86
|
"devDependencies": {
|
|
87
|
+
"typescript": "^5.9.3",
|
|
85
88
|
"@dprint/json": "^0.21.0",
|
|
86
89
|
"@dprint/markdown": "^0.20.0",
|
|
87
90
|
"@dprint/typescript": "^0.95.13",
|
|
@@ -98,6 +101,8 @@
|
|
|
98
101
|
},
|
|
99
102
|
"files": [
|
|
100
103
|
"src/",
|
|
104
|
+
"!src/**/*.test.ts",
|
|
105
|
+
"!src/**/*.test.tsx",
|
|
101
106
|
"dist/",
|
|
102
107
|
"README.md",
|
|
103
108
|
"package.json"
|
package/src/Development.ts
CHANGED
|
@@ -1,18 +1,13 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
import { globalValue } from "effect/GlobalValue"
|
|
12
|
-
import {
|
|
13
|
-
Error,
|
|
14
|
-
FileSystem,
|
|
15
|
-
} from "./node/FileSystem.ts"
|
|
1
|
+
import * as Context from "effect/Context"
|
|
2
|
+
import * as Effect from "effect/Effect"
|
|
3
|
+
import * as Function from "effect/Function"
|
|
4
|
+
import * as GlobalValue from "effect/GlobalValue"
|
|
5
|
+
import * as Layer from "effect/Layer"
|
|
6
|
+
import * as Option from "effect/Option"
|
|
7
|
+
import * as PubSub from "effect/PubSub"
|
|
8
|
+
import * as Stream from "effect/Stream"
|
|
9
|
+
import * as Error from "./node/PlatformError.ts"
|
|
10
|
+
import * as FileSystem from "@effect/platform/FileSystem"
|
|
16
11
|
|
|
17
12
|
export type DevelopmentEvent =
|
|
18
13
|
| FileSystem.WatchEvent
|
|
@@ -20,7 +15,7 @@ export type DevelopmentEvent =
|
|
|
20
15
|
readonly _tag: "Reload"
|
|
21
16
|
}
|
|
22
17
|
|
|
23
|
-
const devState = globalValue(
|
|
18
|
+
const devState = GlobalValue.globalValue(
|
|
24
19
|
Symbol.for("effect-start/Development"),
|
|
25
20
|
() => ({
|
|
26
21
|
count: 0,
|
|
@@ -99,7 +94,7 @@ export const watch = (
|
|
|
99
94
|
const pubsub = yield* PubSub.unbounded<DevelopmentEvent>()
|
|
100
95
|
devState.pubsub = pubsub
|
|
101
96
|
|
|
102
|
-
yield* pipe(
|
|
97
|
+
yield* Function.pipe(
|
|
103
98
|
watchSource({
|
|
104
99
|
path: opts?.path,
|
|
105
100
|
recursive: opts?.recursive,
|
|
@@ -125,7 +120,7 @@ export const layerWatch = (
|
|
|
125
120
|
|
|
126
121
|
export const stream = (): Stream.Stream<DevelopmentEvent> =>
|
|
127
122
|
Stream.unwrap(
|
|
128
|
-
pipe(
|
|
123
|
+
Function.pipe(
|
|
129
124
|
Effect.serviceOption(Development),
|
|
130
125
|
Effect.map(
|
|
131
126
|
Option.match({
|
package/src/Route.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import * as Context from "effect/Context"
|
|
2
|
-
import
|
|
2
|
+
import * as Effect from "effect/Effect"
|
|
3
3
|
import * as Layer from "effect/Layer"
|
|
4
4
|
import * as Pipeable from "effect/Pipeable"
|
|
5
5
|
import * as Predicate from "effect/Predicate"
|
|
6
|
-
import
|
|
6
|
+
import * as Entity from "./Entity.ts"
|
|
7
7
|
import * as RouteBody from "./RouteBody.ts"
|
|
8
8
|
import * as RouteTree from "./RouteTree.ts"
|
|
9
9
|
import * as Values from "./Values.ts"
|
|
@@ -327,6 +327,18 @@ export {
|
|
|
327
327
|
sse,
|
|
328
328
|
} from "./RouteSse.ts"
|
|
329
329
|
|
|
330
|
+
export function redirect(
|
|
331
|
+
url: string | URL,
|
|
332
|
+
status: 301 | 302 | 303 | 307 | 308 = 302,
|
|
333
|
+
): Entity.Entity<""> {
|
|
334
|
+
return Entity.make("", {
|
|
335
|
+
status,
|
|
336
|
+
headers: {
|
|
337
|
+
location: url instanceof URL ? url.href : url,
|
|
338
|
+
},
|
|
339
|
+
})
|
|
340
|
+
}
|
|
341
|
+
|
|
330
342
|
export class Routes extends Context.Tag("effect-start/Routes")<
|
|
331
343
|
Routes,
|
|
332
344
|
RouteTree.RouteTree
|
|
@@ -345,3 +357,20 @@ export function layer(routes: RouteTree.RouteMap | RouteTree.RouteTree) {
|
|
|
345
357
|
export {
|
|
346
358
|
make as tree,
|
|
347
359
|
} from "./RouteTree.ts"
|
|
360
|
+
|
|
361
|
+
export function lazy<T extends RouteSet.Any>(
|
|
362
|
+
load: () => Promise<{ default: T }>,
|
|
363
|
+
): Effect.Effect<T, never, never> {
|
|
364
|
+
let cached: T | undefined
|
|
365
|
+
return Effect.suspend(() => {
|
|
366
|
+
if (cached !== undefined) {
|
|
367
|
+
return Effect.succeed(cached)
|
|
368
|
+
}
|
|
369
|
+
return Effect.promise(load).pipe(
|
|
370
|
+
Effect.map((mod) => {
|
|
371
|
+
cached = mod.default
|
|
372
|
+
return cached
|
|
373
|
+
}),
|
|
374
|
+
)
|
|
375
|
+
})
|
|
376
|
+
}
|
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
|
),
|
|
@@ -400,18 +391,11 @@ export const toWebHandlerRuntime = <R>(
|
|
|
400
391
|
if (exit._tag === "Success") {
|
|
401
392
|
resolve(exit.value)
|
|
402
393
|
} else if (isClientAbort(exit.cause)) {
|
|
403
|
-
resolve(
|
|
404
|
-
Response.json({ status: 499, message: "client closed request" }, {
|
|
405
|
-
status: 499,
|
|
406
|
-
}),
|
|
407
|
-
)
|
|
394
|
+
resolve(respondError({ status: 499, message: "client closed request" }))
|
|
408
395
|
} else {
|
|
409
396
|
const status = getStatusFromCause(exit.cause)
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
status,
|
|
413
|
-
}),
|
|
414
|
-
)
|
|
397
|
+
const message = Cause.pretty(exit.cause, { renderErrorCause: true })
|
|
398
|
+
resolve(respondError({ status, message }))
|
|
415
399
|
}
|
|
416
400
|
})
|
|
417
401
|
})
|
|
@@ -438,6 +422,6 @@ export function* walkHandles(
|
|
|
438
422
|
|
|
439
423
|
const toHandler = toWebHandlerRuntime(runtime)
|
|
440
424
|
for (const [path, routes] of pathGroups) {
|
|
441
|
-
yield [path, toHandler(routes
|
|
425
|
+
yield [path, toHandler(routes)]
|
|
442
426
|
}
|
|
443
427
|
}
|
package/src/RouteMount.ts
CHANGED
package/src/Start.ts
CHANGED
|
@@ -1,14 +1,8 @@
|
|
|
1
|
-
import * as FetchHttpClient from "@effect/platform/FetchHttpClient"
|
|
2
|
-
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"
|
|
6
1
|
import * as Effect from "effect/Effect"
|
|
7
2
|
import * as Function from "effect/Function"
|
|
8
3
|
import * as Layer from "effect/Layer"
|
|
9
4
|
import * as BunHttpServer from "./bun/BunHttpServer.ts"
|
|
10
5
|
import * as BunRuntime from "./bun/BunRuntime.ts"
|
|
11
|
-
import * as NodeFileSystem from "./node/FileSystem.ts"
|
|
12
6
|
import * as StartApp from "./StartApp.ts"
|
|
13
7
|
|
|
14
8
|
export function layer<
|
|
@@ -29,11 +23,7 @@ export function serve<ROut, E>(
|
|
|
29
23
|
default: Layer.Layer<
|
|
30
24
|
ROut,
|
|
31
25
|
E,
|
|
32
|
-
|
|
33
|
-
| HttpClient.HttpClient
|
|
34
|
-
| HttpRouter.Default
|
|
35
|
-
| FileSystem.FileSystem
|
|
36
|
-
| BunHttpServer.BunHttpServer
|
|
26
|
+
BunHttpServer.BunHttpServer
|
|
37
27
|
>
|
|
38
28
|
}>,
|
|
39
29
|
) {
|
|
@@ -46,13 +36,9 @@ export function serve<ROut, E>(
|
|
|
46
36
|
|
|
47
37
|
return Function.pipe(
|
|
48
38
|
BunHttpServer.layerAuto(),
|
|
49
|
-
HttpServer.withLogAddress,
|
|
50
39
|
Layer.provide(appLayer),
|
|
51
40
|
Layer.provide([
|
|
52
|
-
FetchHttpClient.layer,
|
|
53
|
-
HttpRouter.Default.Live,
|
|
54
41
|
BunHttpServer.layer(),
|
|
55
|
-
NodeFileSystem.layer,
|
|
56
42
|
StartApp.layer(),
|
|
57
43
|
]),
|
|
58
44
|
Layer.launch,
|
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
|
+
}
|
package/src/bun/BunHttpServer.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
1
|
import * as HttpApp from "@effect/platform/HttpApp"
|
|
3
2
|
import * as HttpServer from "@effect/platform/HttpServer"
|
|
4
3
|
import * as HttpServerError from "@effect/platform/HttpServerError"
|
|
@@ -14,12 +13,13 @@ import * as FiberSet from "effect/FiberSet"
|
|
|
14
13
|
import * as Layer from "effect/Layer"
|
|
15
14
|
import * as Option from "effect/Option"
|
|
16
15
|
import type * as Scope from "effect/Scope"
|
|
17
|
-
import * as FileRouter from "../FileRouter.ts"
|
|
18
16
|
import * as PathPattern from "../PathPattern.ts"
|
|
19
|
-
import * as
|
|
17
|
+
import * as PlataformRuntime from "../PlatformRuntime.ts"
|
|
20
18
|
import * as Route from "../Route.ts"
|
|
21
19
|
import * as RouteHttp from "../RouteHttp.ts"
|
|
20
|
+
import * as RouteMount from "../RouteMount.ts"
|
|
22
21
|
import * as RouteTree from "../RouteTree.ts"
|
|
22
|
+
import * as Unique from "../Unique.ts"
|
|
23
23
|
import EmptyHTML from "./_empty.html"
|
|
24
24
|
import {
|
|
25
25
|
makeResponse,
|
|
@@ -69,11 +69,8 @@ export const make = (
|
|
|
69
69
|
Effect.gen(function*() {
|
|
70
70
|
const port = yield* Config.number("PORT").pipe(
|
|
71
71
|
Effect.catchTag("ConfigError", () => {
|
|
72
|
-
if (
|
|
73
|
-
|
|
74
|
-
&& !process.stdout.isTTY
|
|
75
|
-
&& process.env.CLAUDECODE
|
|
76
|
-
) {
|
|
72
|
+
if (PlataformRuntime.isAgentHarness()) {
|
|
73
|
+
// use random port
|
|
77
74
|
return Effect.succeed(0)
|
|
78
75
|
}
|
|
79
76
|
|
|
@@ -95,7 +92,7 @@ export const make = (
|
|
|
95
92
|
// Bun HMR doesn't work on successive calls to `server.reload` if there are no routes
|
|
96
93
|
// on server start. We workaround that by passing a dummy HTMLBundle [2025-11-26]
|
|
97
94
|
// see: https://github.com/oven-sh/bun/issues/23564
|
|
98
|
-
currentRoutes[`/.BunEmptyHtml-${
|
|
95
|
+
currentRoutes[`/.BunEmptyHtml-${Unique.token(10)}`] = EmptyHTML
|
|
99
96
|
|
|
100
97
|
const websocket: Bun.WebSocketHandler<WebSocketContext> = {
|
|
101
98
|
open(ws) {
|
package/src/bun/BunRoute.ts
CHANGED
|
@@ -6,7 +6,7 @@ import * as Option from "effect/Option"
|
|
|
6
6
|
import * as Entity from "../Entity.ts"
|
|
7
7
|
import * as Hyper from "../hyper/Hyper.ts"
|
|
8
8
|
import * as HyperHtml from "../hyper/HyperHtml.ts"
|
|
9
|
-
import * as
|
|
9
|
+
import * as Unique from "../Unique.ts"
|
|
10
10
|
import * as Route from "../Route.ts"
|
|
11
11
|
import * as RouterPattern from "../RouterPattern.ts"
|
|
12
12
|
import * as BunHttpServer from "./BunHttpServer.ts"
|
|
@@ -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
|
|
package/src/index.ts
CHANGED
|
@@ -2,5 +2,6 @@ export * as Bundle from "./bundler/Bundle.ts"
|
|
|
2
2
|
export * as Development from "./Development.ts"
|
|
3
3
|
export * as Entity from "./Entity.ts"
|
|
4
4
|
export * as FileRouter from "./FileRouter.ts"
|
|
5
|
+
export * as Unique from "./Unique.ts"
|
|
5
6
|
export * as Route from "./Route.ts"
|
|
6
7
|
export * as Start from "./Start.ts"
|