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.
Files changed (80) hide show
  1. package/dist/Development.d.ts +7 -2
  2. package/dist/Development.js +12 -6
  3. package/dist/PlatformRuntime.d.ts +4 -0
  4. package/dist/PlatformRuntime.js +9 -0
  5. package/dist/Route.d.ts +6 -2
  6. package/dist/Route.js +22 -0
  7. package/dist/RouteHttp.d.ts +1 -1
  8. package/dist/RouteHttp.js +12 -19
  9. package/dist/RouteMount.d.ts +2 -1
  10. package/dist/Start.d.ts +1 -5
  11. package/dist/Start.js +1 -8
  12. package/dist/Unique.d.ts +50 -0
  13. package/dist/Unique.js +187 -0
  14. package/dist/bun/BunHttpServer.js +5 -6
  15. package/dist/bun/BunRoute.d.ts +1 -1
  16. package/dist/bun/BunRoute.js +2 -2
  17. package/dist/index.d.ts +1 -0
  18. package/dist/index.js +1 -0
  19. package/dist/node/Effectify.d.ts +209 -0
  20. package/dist/node/Effectify.js +19 -0
  21. package/dist/node/FileSystem.d.ts +3 -5
  22. package/dist/node/FileSystem.js +42 -62
  23. package/dist/node/PlatformError.d.ts +46 -0
  24. package/dist/node/PlatformError.js +43 -0
  25. package/dist/testing/TestLogger.js +1 -1
  26. package/package.json +10 -5
  27. package/src/Development.ts +13 -18
  28. package/src/PlatformRuntime.ts +11 -0
  29. package/src/Route.ts +31 -2
  30. package/src/RouteHttp.ts +15 -31
  31. package/src/RouteMount.ts +1 -1
  32. package/src/Start.ts +1 -15
  33. package/src/Unique.ts +232 -0
  34. package/src/bun/BunHttpServer.ts +6 -9
  35. package/src/bun/BunRoute.ts +3 -3
  36. package/src/index.ts +1 -0
  37. package/src/node/Effectify.ts +262 -0
  38. package/src/node/FileSystem.ts +59 -97
  39. package/src/node/PlatformError.ts +102 -0
  40. package/src/testing/TestLogger.ts +1 -1
  41. package/dist/Random.d.ts +0 -5
  42. package/dist/Random.js +0 -49
  43. package/src/Commander.test.ts +0 -1639
  44. package/src/ContentNegotiation.test.ts +0 -603
  45. package/src/Development.test.ts +0 -119
  46. package/src/Entity.test.ts +0 -592
  47. package/src/FileRouterPattern.test.ts +0 -147
  48. package/src/FileRouter_files.test.ts +0 -64
  49. package/src/FileRouter_path.test.ts +0 -145
  50. package/src/FileRouter_tree.test.ts +0 -132
  51. package/src/Http.test.ts +0 -319
  52. package/src/HttpAppExtra.test.ts +0 -103
  53. package/src/HttpUtils.test.ts +0 -85
  54. package/src/PathPattern.test.ts +0 -648
  55. package/src/Random.ts +0 -59
  56. package/src/RouteBody.test.ts +0 -232
  57. package/src/RouteHook.test.ts +0 -40
  58. package/src/RouteHttp.test.ts +0 -2909
  59. package/src/RouteMount.test.ts +0 -481
  60. package/src/RouteSchema.test.ts +0 -427
  61. package/src/RouteSse.test.ts +0 -249
  62. package/src/RouteTree.test.ts +0 -494
  63. package/src/RouteTrie.test.ts +0 -322
  64. package/src/RouterPattern.test.ts +0 -676
  65. package/src/Values.test.ts +0 -263
  66. package/src/bun/BunBundle.test.ts +0 -268
  67. package/src/bun/BunBundle_imports.test.ts +0 -48
  68. package/src/bun/BunHttpServer.test.ts +0 -251
  69. package/src/bun/BunImportTrackerPlugin.test.ts +0 -77
  70. package/src/bun/BunRoute.test.ts +0 -162
  71. package/src/bundler/BundleHttp.test.ts +0 -132
  72. package/src/effect/HttpRouter.test.ts +0 -548
  73. package/src/experimental/EncryptedCookies.test.ts +0 -488
  74. package/src/hyper/HyperHtml.test.ts +0 -209
  75. package/src/hyper/HyperRoute.test.tsx +0 -197
  76. package/src/middlewares/BasicAuthMiddleware.test.ts +0 -84
  77. package/src/testing/TestHttpClient.test.ts +0 -83
  78. package/src/testing/TestLogger.test.ts +0 -51
  79. package/src/x/datastar/Datastar.test.ts +0 -266
  80. package/src/x/tailwind/TailwindPlugin.test.ts +0 -333
@@ -1,5 +1,10 @@
1
- import { Context, Effect, Layer, PubSub, Stream } from "effect";
2
- import { Error, FileSystem } from "./node/FileSystem.ts";
1
+ import * as Context from "effect/Context";
2
+ import * as Effect from "effect/Effect";
3
+ import * as Layer from "effect/Layer";
4
+ import * as PubSub from "effect/PubSub";
5
+ import * as Stream from "effect/Stream";
6
+ import * as Error from "./node/PlatformError.ts";
7
+ import * as FileSystem from "@effect/platform/FileSystem";
3
8
  export type DevelopmentEvent = FileSystem.WatchEvent | {
4
9
  readonly _tag: "Reload";
5
10
  };
@@ -1,7 +1,13 @@
1
- import { Context, Effect, Function, Layer, Option, pipe, PubSub, Stream, } from "effect";
2
- import { globalValue } from "effect/GlobalValue";
3
- import { FileSystem, } from "./node/FileSystem.js";
4
- const devState = globalValue(Symbol.for("effect-start/Development"), () => ({
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 FileSystem from "@effect/platform/FileSystem";
10
+ const devState = GlobalValue.globalValue(Symbol.for("effect-start/Development"), () => ({
5
11
  count: 0,
6
12
  pubsub: null,
7
13
  }));
@@ -34,7 +40,7 @@ export const watch = (opts) => Effect.gen(function* () {
34
40
  if (devState.count === 1) {
35
41
  const pubsub = yield* PubSub.unbounded();
36
42
  devState.pubsub = pubsub;
37
- yield* pipe(watchSource({
43
+ yield* Function.pipe(watchSource({
38
44
  path: opts?.path,
39
45
  recursive: opts?.recursive,
40
46
  filter: opts?.filter ?? filterSourceFiles,
@@ -46,7 +52,7 @@ export const watch = (opts) => Effect.gen(function* () {
46
52
  return { events: devState.pubsub };
47
53
  });
48
54
  export const layerWatch = (opts) => Layer.scoped(Development, watch(opts));
49
- export const stream = () => Stream.unwrap(pipe(Effect.serviceOption(Development), Effect.map(Option.match({
55
+ export const stream = () => Stream.unwrap(Function.pipe(Effect.serviceOption(Development), Effect.map(Option.match({
50
56
  onNone: () => Stream.empty,
51
57
  onSome: (dev) => Stream.fromPubSub(dev.events),
52
58
  }))));
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Are we running within an agent harness, like Claude Code?
3
+ */
4
+ export declare function isAgentHarness(): string | false | undefined;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Are we running within an agent harness, like Claude Code?
3
+ */
4
+ export function isAgentHarness() {
5
+ return typeof process !== "undefined"
6
+ && !process.stdout.isTTY
7
+ && (process.env.CLAUDECODE
8
+ || process.env.CURSOR_AGENT);
9
+ }
package/dist/Route.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import * as Context from "effect/Context";
2
- import type * as Effect from "effect/Effect";
2
+ import * as Effect from "effect/Effect";
3
3
  import * as Layer from "effect/Layer";
4
4
  import * as Pipeable from "effect/Pipeable";
5
- import type * as Entity from "./Entity.ts";
5
+ import * as Entity from "./Entity.ts";
6
6
  import * as RouteBody from "./RouteBody.ts";
7
7
  import * as RouteTree from "./RouteTree.ts";
8
8
  import * as Values from "./Values.ts";
@@ -91,8 +91,12 @@ export declare const json: RouteBody.BuildReturn<Values.Json, "json">;
91
91
  export declare const bytes: RouteBody.BuildReturn<Uint8Array<ArrayBufferLike>, "bytes">;
92
92
  export { render, } from "./RouteBody.ts";
93
93
  export { sse, } from "./RouteSse.ts";
94
+ export declare function redirect(url: string | URL, status?: 301 | 302 | 303 | 307 | 308): Entity.Entity<"">;
94
95
  declare const Routes_base: Context.TagClass<Routes, "effect-start/Routes", RouteTree.RouteTree<RouteTree.RouteMap>>;
95
96
  export declare class Routes extends Routes_base {
96
97
  }
97
98
  export declare function layer(routes: RouteTree.RouteMap | RouteTree.RouteTree): Layer.Layer<Routes, never, never>;
98
99
  export { make as tree, } from "./RouteTree.ts";
100
+ export declare function lazy<T extends RouteSet.Any>(load: () => Promise<{
101
+ default: T;
102
+ }>): Effect.Effect<T, never, never>;
package/dist/Route.js CHANGED
@@ -1,7 +1,9 @@
1
1
  import * as Context from "effect/Context";
2
+ import * as Effect from "effect/Effect";
2
3
  import * as Layer from "effect/Layer";
3
4
  import * as Pipeable from "effect/Pipeable";
4
5
  import * as Predicate from "effect/Predicate";
6
+ import * as Entity from "./Entity.js";
5
7
  import * as RouteBody from "./RouteBody.js";
6
8
  import * as RouteTree from "./RouteTree.js";
7
9
  export const RouteItems = Symbol();
@@ -71,6 +73,14 @@ export const bytes = RouteBody.build({
71
73
  });
72
74
  export { render, } from "./RouteBody.js";
73
75
  export { sse, } from "./RouteSse.js";
76
+ export function redirect(url, status = 302) {
77
+ return Entity.make("", {
78
+ status,
79
+ headers: {
80
+ location: url instanceof URL ? url.href : url,
81
+ },
82
+ });
83
+ }
74
84
  export class Routes extends Context.Tag("effect-start/Routes")() {
75
85
  }
76
86
  export function layer(routes) {
@@ -79,3 +89,15 @@ export function layer(routes) {
79
89
  : RouteTree.make(routes));
80
90
  }
81
91
  export { make as tree, } from "./RouteTree.js";
92
+ export function lazy(load) {
93
+ let cached;
94
+ return Effect.suspend(() => {
95
+ if (cached !== undefined) {
96
+ return Effect.succeed(cached);
97
+ }
98
+ return Effect.promise(load).pipe(Effect.map((mod) => {
99
+ cached = mod.default;
100
+ return cached;
101
+ }));
102
+ });
103
+ }
@@ -5,7 +5,6 @@ import * as Route from "./Route.ts";
5
5
  import * as RouteBody from "./RouteBody.ts";
6
6
  import * as RouteMount from "./RouteMount.ts";
7
7
  import * as RouteTree from "./RouteTree.ts";
8
- export { currentSpanNameGenerator, currentTracerDisabledWhen, parentSpanFromHeaders, withSpanNameGenerator, withTracerDisabledWhen, } from "./RouteHttpTracer.ts";
9
8
  type UnboundedRouteWithMethod = Route.Route.With<{
10
9
  method: RouteMount.RouteMount.Method;
11
10
  format?: RouteBody.Format;
@@ -19,3 +18,4 @@ export declare const clientAbortFiberId: FiberId.Runtime;
19
18
  export declare const toWebHandlerRuntime: <R>(runtime: Runtime.Runtime<R>) => (routes: Iterable<UnboundedRouteWithMethod>) => Http.WebHandler;
20
19
  export declare const toWebHandler: (routes: Iterable<UnboundedRouteWithMethod>) => Http.WebHandler;
21
20
  export declare function walkHandles(tree: RouteTree.RouteTree, runtime?: Runtime.Runtime<never>): Generator<[path: string, handler: Http.WebHandler]>;
21
+ export {};
package/dist/RouteHttp.js CHANGED
@@ -12,7 +12,6 @@ import * as Route from "./Route.js";
12
12
  import * as RouteHttpTracer from "./RouteHttpTracer.js";
13
13
  import * as RouteTree from "./RouteTree.js";
14
14
  import * as StreamExtra from "./StreamExtra.js";
15
- export { currentSpanNameGenerator, currentTracerDisabledWhen, parentSpanFromHeaders, withSpanNameGenerator, withTracerDisabledWhen, } from "./RouteHttpTracer.js";
16
15
  // Used to match Accept headers against available route formats.
17
16
  // text/* matches any text type (text/plain, text/event-stream, text/markdown, etc.)
18
17
  const formatToMediaType = {
@@ -46,6 +45,10 @@ const getStatusFromCause = (cause) => {
46
45
  }
47
46
  return 500;
48
47
  };
48
+ const respondError = (options) => new Response(JSON.stringify(options, null, 2), {
49
+ status: options.status,
50
+ headers: { "content-type": "application/json" },
51
+ });
49
52
  function streamResponse(stream, headers, status, runtime) {
50
53
  const encoder = new TextEncoder();
51
54
  const byteStream = stream.pipe(Stream.map((chunk) => typeof chunk === "string"
@@ -116,9 +119,7 @@ export const toWebHandlerRuntime = (runtime) => {
116
119
  const accept = request.headers.get("accept");
117
120
  const methodRoutes = methodGroups[method] ?? [];
118
121
  if (methodRoutes.length === 0 && wildcards.length === 0) {
119
- return Promise.resolve(Response.json({ status: 405, message: "method not allowed" }, {
120
- status: 405,
121
- }));
122
+ return Promise.resolve(respondError({ status: 405, message: "method not allowed" }));
122
123
  }
123
124
  const allRoutes = [...wildcards, ...methodRoutes];
124
125
  const selectedFormat = determineSelectedFormat(accept, allRoutes);
@@ -130,9 +131,7 @@ export const toWebHandlerRuntime = (runtime) => {
130
131
  if (selectedFormat === undefined
131
132
  && hasSpecificFormatRoutes
132
133
  && !hasWildcardFormatRoutes) {
133
- return Promise.resolve(Response.json({ status: 406, message: "not acceptable" }, {
134
- status: 406,
135
- }));
134
+ return Promise.resolve(respondError({ status: 406, message: "not acceptable" }));
136
135
  }
137
136
  const createChain = (initialContext) => {
138
137
  let index = 0;
@@ -178,9 +177,7 @@ export const toWebHandlerRuntime = (runtime) => {
178
177
  ? result
179
178
  : Entity.make(result, { status: 200 });
180
179
  if (entity.status === 404 && entity.body === undefined) {
181
- return Response.json({ status: 406, message: "not acceptable" }, {
182
- status: 406,
183
- });
180
+ return respondError({ status: 406, message: "not acceptable" });
184
181
  }
185
182
  return yield* toResponse(entity, selectedFormat, runtime);
186
183
  });
@@ -217,9 +214,8 @@ export const toWebHandlerRuntime = (runtime) => {
217
214
  const fiber = runFork(effect.pipe(Effect.scoped, Effect.catchAllCause((cause) => Effect.gen(function* () {
218
215
  yield* Effect.logError(cause);
219
216
  const status = getStatusFromCause(cause);
220
- return Response.json({ status, message: Cause.pretty(cause) }, {
221
- status,
222
- });
217
+ const message = Cause.pretty(cause, { renderErrorCause: true });
218
+ return respondError({ status, message });
223
219
  }))));
224
220
  request.signal?.addEventListener("abort", () => {
225
221
  fiber.unsafeInterruptAsFork(clientAbortFiberId);
@@ -229,15 +225,12 @@ export const toWebHandlerRuntime = (runtime) => {
229
225
  resolve(exit.value);
230
226
  }
231
227
  else if (isClientAbort(exit.cause)) {
232
- resolve(Response.json({ status: 499, message: "client closed request" }, {
233
- status: 499,
234
- }));
228
+ resolve(respondError({ status: 499, message: "client closed request" }));
235
229
  }
236
230
  else {
237
231
  const status = getStatusFromCause(exit.cause);
238
- resolve(Response.json({ status, message: Cause.pretty(exit.cause) }, {
239
- status,
240
- }));
232
+ const message = Cause.pretty(exit.cause, { renderErrorCause: true });
233
+ resolve(respondError({ status, message }));
241
234
  }
242
235
  });
243
236
  });
@@ -2,6 +2,7 @@ import * as Types from "effect/Types";
2
2
  import * as Http from "./Http.ts";
3
3
  import * as PathPattern from "./PathPattern.ts";
4
4
  import * as Route from "./Route.ts";
5
+ import * as RouteBody from "./RouteBody.ts";
5
6
  type Module = typeof import("./RouteMount.ts");
6
7
  export type Self = RouteMount.Builder | Module;
7
8
  export declare const use: RouteMount.Describer<"*">;
@@ -16,7 +17,7 @@ export declare const add: RouteMount.Add;
16
17
  export type MountedRoute = Route.Route.Route<{
17
18
  method: RouteMount.Method;
18
19
  path: PathPattern.PathPattern;
19
- format?: string;
20
+ format?: RouteBody.Format;
20
21
  }, {}, any, any, any>;
21
22
  export declare namespace RouteMount {
22
23
  export type Method = "*" | Http.Method;
package/dist/Start.d.ts CHANGED
@@ -1,7 +1,3 @@
1
- import * as FileSystem from "@effect/platform/FileSystem";
2
- import * as HttpClient from "@effect/platform/HttpClient";
3
- import * as HttpRouter from "@effect/platform/HttpRouter";
4
- import * as HttpServer from "@effect/platform/HttpServer";
5
1
  import * as Layer from "effect/Layer";
6
2
  import * as BunHttpServer from "./bun/BunHttpServer.ts";
7
3
  export declare function layer<Layers extends [
@@ -15,5 +11,5 @@ export declare function layer<Layers extends [
15
11
  [k in keyof Layers]: Layer.Layer.Context<Layers[k]>;
16
12
  }[number]>;
17
13
  export declare function serve<ROut, E>(load: () => Promise<{
18
- default: Layer.Layer<ROut, E, HttpServer.HttpServer | HttpClient.HttpClient | HttpRouter.Default | FileSystem.FileSystem | BunHttpServer.BunHttpServer>;
14
+ default: Layer.Layer<ROut, E, BunHttpServer.BunHttpServer>;
19
15
  }>): void;
package/dist/Start.js CHANGED
@@ -1,23 +1,16 @@
1
- import * as FetchHttpClient from "@effect/platform/FetchHttpClient";
2
- import * as HttpRouter from "@effect/platform/HttpRouter";
3
- import * as HttpServer from "@effect/platform/HttpServer";
4
1
  import * as Effect from "effect/Effect";
5
2
  import * as Function from "effect/Function";
6
3
  import * as Layer from "effect/Layer";
7
4
  import * as BunHttpServer from "./bun/BunHttpServer.js";
8
5
  import * as BunRuntime from "./bun/BunRuntime.js";
9
- import * as NodeFileSystem from "./node/FileSystem.js";
10
6
  import * as StartApp from "./StartApp.js";
11
7
  export function layer(...layers) {
12
8
  return Layer.mergeAll(...layers);
13
9
  }
14
10
  export function serve(load) {
15
11
  const appLayer = Function.pipe(Effect.tryPromise(load), Effect.map(v => v.default), Effect.orDie, Layer.unwrapEffect);
16
- return Function.pipe(BunHttpServer.layerAuto(), HttpServer.withLogAddress, Layer.provide(appLayer), Layer.provide([
17
- FetchHttpClient.layer,
18
- HttpRouter.Default.Live,
12
+ return Function.pipe(BunHttpServer.layerAuto(), Layer.provide(appLayer), Layer.provide([
19
13
  BunHttpServer.layer(),
20
- NodeFileSystem.layer,
21
14
  StartApp.layer(),
22
15
  ]), Layer.launch, BunRuntime.runMain);
23
16
  }
@@ -0,0 +1,50 @@
1
+ export declare const ALPHABET_BASE32_CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
2
+ export declare const ALPHABET_BASE32_RFC4648 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
3
+ export declare const ALPHABET_BASE64_URL = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
4
+ export declare const ALPHABET_HEX = "0123456789abcdef";
5
+ /**
6
+ * Generate a random string for ids, session tokens, and API keys.
7
+ * It uses human-friendly crockford base32 encoding (5 bit of entropy per char)
8
+ *
9
+ * Minimal recommended length:
10
+ * - public ids: 16 chars (~80 bits)
11
+ * - API keys: 32 chars (~160 bits)
12
+ * - session tokens: 32-40 chars (~160-200 bits)
13
+ */
14
+ export declare function token(length?: number): string;
15
+ export declare function bytes(length: number): Uint8Array;
16
+ export declare const UUID_NIL = "00000000-0000-0000-0000-000000000000";
17
+ export declare function uuid4(): string;
18
+ export declare function uuid7(time?: number): string;
19
+ /**
20
+ * Decode a 48-bit Unix timestamp (ms) from UUID7 or ULID.
21
+ *
22
+ * @example
23
+ * const bytes = Unique.uuid7Bytes()
24
+ * const timestamp = Unique.toTimestamp(bytes)
25
+ *
26
+ * @example
27
+ * const bytes = Unique.ulidBytes()
28
+ * const timestamp = Unique.toTimestamp(bytes)
29
+ */
30
+ export declare function toTimestamp(bytes: Uint8Array): number;
31
+ export declare function uuid7Bytes(time?: number): Uint8Array;
32
+ /**
33
+ * Convert UUID bytes to canonical (RFC9562) representation.
34
+ *
35
+ * @example
36
+ * Unique.formatUuid(new Uint8Array(16))
37
+ */
38
+ export declare function formatUuid(bytes: Uint8Array): string;
39
+ export declare function ulid(time?: number): string;
40
+ export declare function ulidBytes(time?: number): Uint8Array;
41
+ /**
42
+ * Generate a nanoid-style random string.
43
+ *
44
+ * FUN_FACT: Original nanoid implementation uses base64url alphabet
45
+ * with non-standard custom order where charater form common words found
46
+ * in source code (like use, random, strict) to make gzip/brotli more efficient.
47
+ * It's qt lil opt from the times where web developers
48
+ * were competing to have the smallest possible bundle size.
49
+ */
50
+ export declare function nanoid(size?: number, alphabet?: string): string;
package/dist/Unique.js ADDED
@@ -0,0 +1,187 @@
1
+ export const ALPHABET_BASE32_CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
2
+ export const ALPHABET_BASE32_RFC4648 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
3
+ export const ALPHABET_BASE64_URL = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
4
+ export const ALPHABET_HEX = "0123456789abcdef";
5
+ /**
6
+ * Generate a random string for ids, session tokens, and API keys.
7
+ * It uses human-friendly crockford base32 encoding (5 bit of entropy per char)
8
+ *
9
+ * Minimal recommended length:
10
+ * - public ids: 16 chars (~80 bits)
11
+ * - API keys: 32 chars (~160 bits)
12
+ * - session tokens: 32-40 chars (~160-200 bits)
13
+ */
14
+ export function token(length = 32) {
15
+ if (length <= 0)
16
+ return "";
17
+ const buf = new Uint8Array(length);
18
+ crypto.getRandomValues(buf);
19
+ let result = "";
20
+ for (let i = 0; i < buf.length; i++) {
21
+ result += ALPHABET_BASE32_CROCKFORD[buf[i] & 31];
22
+ }
23
+ return result;
24
+ }
25
+ export function bytes(length) {
26
+ const buf = new Uint8Array(length);
27
+ crypto.getRandomValues(buf);
28
+ return buf;
29
+ }
30
+ export const UUID_NIL = "00000000-0000-0000-0000-000000000000";
31
+ export function uuid4() {
32
+ return formatUuid(uuid4bytes());
33
+ }
34
+ export function uuid7(time = Date.now()) {
35
+ return formatUuid(uuid7Bytes(time));
36
+ }
37
+ function uuid4bytes() {
38
+ const buf = bytes(16);
39
+ buf[6] = (buf[6] & 0x0f) | 0x40; // version 4
40
+ buf[8] = (buf[8] & 0x3f) | 0x80; // variant
41
+ return buf;
42
+ }
43
+ /**
44
+ * Decode a 48-bit Unix timestamp (ms) from UUID7 or ULID.
45
+ *
46
+ * @example
47
+ * const bytes = Unique.uuid7Bytes()
48
+ * const timestamp = Unique.toTimestamp(bytes)
49
+ *
50
+ * @example
51
+ * const bytes = Unique.ulidBytes()
52
+ * const timestamp = Unique.toTimestamp(bytes)
53
+ */
54
+ export function toTimestamp(bytes) {
55
+ if (bytes.length < 6)
56
+ return 0;
57
+ return (bytes[0] * 0x10000000000
58
+ + bytes[1] * 0x100000000
59
+ + bytes[2] * 0x1000000
60
+ + bytes[3] * 0x10000
61
+ + bytes[4] * 0x100
62
+ + bytes[5]);
63
+ }
64
+ export function uuid7Bytes(time = Date.now()) {
65
+ const buf = new Uint8Array(16);
66
+ const timestamp = BigInt(toSafeTime(time));
67
+ // 48-bit timestamp (6 bytes)
68
+ buf[0] = Number((timestamp >> 40n) & 0xffn);
69
+ buf[1] = Number((timestamp >> 32n) & 0xffn);
70
+ buf[2] = Number((timestamp >> 24n) & 0xffn);
71
+ buf[3] = Number((timestamp >> 16n) & 0xffn);
72
+ buf[4] = Number((timestamp >> 8n) & 0xffn);
73
+ buf[5] = Number(timestamp & 0xffn);
74
+ // 12-bit random A (1.5 bytes)
75
+ crypto.getRandomValues(buf.subarray(6, 8));
76
+ buf[6] = (buf[6] & 0x0f) | 0x70; // version 7
77
+ // 2-bit variant + 62-bit random B (8 bytes)
78
+ crypto.getRandomValues(buf.subarray(8, 16));
79
+ buf[8] = (buf[8] & 0x3f) | 0x80; // variant
80
+ return buf;
81
+ }
82
+ /**
83
+ * Convert UUID bytes to canonical (RFC9562) representation.
84
+ *
85
+ * @example
86
+ * Unique.formatUuid(new Uint8Array(16))
87
+ */
88
+ export function formatUuid(bytes) {
89
+ if (bytes.length === 0)
90
+ return "";
91
+ let result = "";
92
+ for (let i = 0; i < bytes.length; i++) {
93
+ const byte = bytes[i];
94
+ result += ALPHABET_HEX[(byte >> 4) & 0x0f];
95
+ result += ALPHABET_HEX[byte & 0x0f];
96
+ if (i === 3 || i === 5 || i === 7 || i === 9)
97
+ result += "-";
98
+ }
99
+ return result;
100
+ }
101
+ export function ulid(time = Date.now()) {
102
+ const bytes = ulidBytes(time);
103
+ return formatUlid(bytes);
104
+ }
105
+ export function ulidBytes(time = Date.now()) {
106
+ const buf = new Uint8Array(16);
107
+ const timestamp = BigInt(toSafeTime(time));
108
+ buf[0] = Number((timestamp >> 40n) & 0xffn);
109
+ buf[1] = Number((timestamp >> 32n) & 0xffn);
110
+ buf[2] = Number((timestamp >> 24n) & 0xffn);
111
+ buf[3] = Number((timestamp >> 16n) & 0xffn);
112
+ buf[4] = Number((timestamp >> 8n) & 0xffn);
113
+ buf[5] = Number(timestamp & 0xffn);
114
+ crypto.getRandomValues(buf.subarray(6, 16));
115
+ return buf;
116
+ }
117
+ function formatUlid(bytes) {
118
+ if (bytes.length !== 16)
119
+ return "";
120
+ const timestamp = toTimestamp(bytes);
121
+ const timePart = encodeUlidTime(timestamp);
122
+ const randomPart = toBase32(bytes.subarray(6, 16));
123
+ return `${timePart}${randomPart}`;
124
+ }
125
+ function encodeUlidTime(time) {
126
+ let value = BigInt(time);
127
+ const result = new Array(10);
128
+ for (let i = 9; i >= 0; i--) {
129
+ result[i] = ALPHABET_BASE32_CROCKFORD[Number(value & 31n)];
130
+ value >>= 5n;
131
+ }
132
+ return result.join("");
133
+ }
134
+ function toSafeTime(time) {
135
+ if (!Number.isFinite(time))
136
+ return 0;
137
+ return Math.max(0, Math.trunc(time));
138
+ }
139
+ /**
140
+ * Generate a nanoid-style random string.
141
+ *
142
+ * FUN_FACT: Original nanoid implementation uses base64url alphabet
143
+ * with non-standard custom order where charater form common words found
144
+ * in source code (like use, random, strict) to make gzip/brotli more efficient.
145
+ * It's qt lil opt from the times where web developers
146
+ * were competing to have the smallest possible bundle size.
147
+ */
148
+ export function nanoid(size = 21, alphabet = ALPHABET_BASE64_URL) {
149
+ if (size <= 0 || alphabet.length === 0)
150
+ return "";
151
+ const length = alphabet.length;
152
+ const mask = (2 << Math.floor(Math.log2(length - 1))) - 1;
153
+ const step = Math.ceil((1.6 * mask * size) / length);
154
+ let id = "";
155
+ while (id.length < size) {
156
+ const bytes = new Uint8Array(step);
157
+ crypto.getRandomValues(bytes);
158
+ for (let i = 0; i < step && id.length < size; i++) {
159
+ const index = bytes[i] & mask;
160
+ if (index < length)
161
+ id += alphabet[index];
162
+ }
163
+ }
164
+ return id;
165
+ }
166
+ function toBase32(bytes, alphabet = ALPHABET_BASE32_CROCKFORD) {
167
+ if (bytes.length === 0)
168
+ return "";
169
+ let result = "";
170
+ let buffer = 0;
171
+ let bits = 0;
172
+ for (const byte of bytes) {
173
+ buffer = (buffer << 8) | byte;
174
+ bits += 8;
175
+ while (bits >= 5) {
176
+ bits -= 5;
177
+ const index = (buffer >> bits) & 31;
178
+ result += alphabet[index];
179
+ buffer &= (1 << bits) - 1;
180
+ }
181
+ }
182
+ if (bits > 0) {
183
+ const index = (buffer << (5 - bits)) & 31;
184
+ result += alphabet[index];
185
+ }
186
+ return result;
187
+ }
@@ -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,19 +13,19 @@ 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 * as PathPattern from "../PathPattern.js";
17
- import * as Random from "../Random.js";
16
+ import * as PlataformRuntime from "../PlatformRuntime.js";
18
17
  import * as Route from "../Route.js";
19
18
  import * as RouteHttp from "../RouteHttp.js";
20
19
  import * as RouteTree from "../RouteTree.js";
20
+ import * as Unique from "../Unique.js";
21
21
  import EmptyHTML from "./_empty.html";
22
22
  import { makeResponse, ServerRequestImpl, } from "./BunHttpServer_web.js";
23
23
  import * as BunRoute from "./BunRoute.js";
24
24
  export const BunHttpServer = Context.GenericTag("effect-start/BunServer");
25
25
  export const make = (options) => Effect.gen(function* () {
26
26
  const port = yield* Config.number("PORT").pipe(Effect.catchTag("ConfigError", () => {
27
- if (typeof process !== "undefined"
28
- && !process.stdout.isTTY
29
- && process.env.CLAUDECODE) {
27
+ if (PlataformRuntime.isAgentHarness()) {
28
+ // use random port
30
29
  return Effect.succeed(0);
31
30
  }
32
31
  return Effect.succeed(3000);
@@ -41,7 +40,7 @@ export const make = (options) => Effect.gen(function* () {
41
40
  // Bun HMR doesn't work on successive calls to `server.reload` if there are no routes
42
41
  // on server start. We workaround that by passing a dummy HTMLBundle [2025-11-26]
43
42
  // see: https://github.com/oven-sh/bun/issues/23564
44
- currentRoutes[`/.BunEmptyHtml-${Random.token(6)}`] = EmptyHTML;
43
+ currentRoutes[`/.BunEmptyHtml-${Unique.token(10)}`] = EmptyHTML;
45
44
  const websocket = {
46
45
  open(ws) {
47
46
  Deferred.unsafeDone(ws.data.deferred, Exit.succeed(ws));
@@ -15,7 +15,7 @@ export type BunDescriptors = {
15
15
  bunPrefix: string;
16
16
  bunLoad: () => Promise<Bun.HTMLBundle>;
17
17
  };
18
- export declare function descriptors(route: Route.Route.Route): BunDescriptors | undefined;
18
+ export declare function descriptors(route: Route.Route.Route<any, any, any, any, any>): BunDescriptors | undefined;
19
19
  export declare function htmlBundle(load: () => Promise<Bun.HTMLBundle | {
20
20
  default: Bun.HTMLBundle;
21
21
  }>): <D extends Route.RouteDescriptor.Any, B extends {}, I extends Route.Route.Tuple>(self: Route.RouteSet.RouteSet<D, B, I>) => Route.RouteSet.RouteSet<D, B, [...I, Route.Route.Route<BunDescriptors & {
@@ -5,7 +5,7 @@ import * as Option from "effect/Option";
5
5
  import * as Entity from "../Entity.js";
6
6
  import * as Hyper from "../hyper/Hyper.js";
7
7
  import * as HyperHtml from "../hyper/HyperHtml.js";
8
- import * as Random from "../Random.js";
8
+ import * as Unique from "../Unique.js";
9
9
  import * as Route from "../Route.js";
10
10
  import * as RouterPattern from "../RouterPattern.js";
11
11
  import * as BunHttpServer from "./BunHttpServer.js";
@@ -21,7 +21,7 @@ export function descriptors(route) {
21
21
  return undefined;
22
22
  }
23
23
  export function htmlBundle(load) {
24
- const bunPrefix = `/.BunRoute-${Random.token(6)}`;
24
+ const bunPrefix = `/.BunRoute-${Unique.token(10)}`;
25
25
  const bunLoad = () => load().then(mod => "default" in mod ? mod.default : mod);
26
26
  const descriptors = { bunPrefix, bunLoad, format: "html" };
27
27
  return function (self) {
package/dist/index.d.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";
package/dist/index.js CHANGED
@@ -2,5 +2,6 @@ export * as Bundle from "./bundler/Bundle.js";
2
2
  export * as Development from "./Development.js";
3
3
  export * as Entity from "./Entity.js";
4
4
  export * as FileRouter from "./FileRouter.js";
5
+ export * as Unique from "./Unique.js";
5
6
  export * as Route from "./Route.js";
6
7
  export * as Start from "./Start.js";