silgi 0.51.7 → 0.52.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 (53) hide show
  1. package/README.md +47 -0
  2. package/dist/adapters/_fetch-adapter.d.mts +6 -0
  3. package/dist/adapters/_fetch-adapter.mjs +14 -8
  4. package/dist/adapters/astro.mjs +1 -1
  5. package/dist/adapters/nextjs.mjs +1 -1
  6. package/dist/adapters/remix.mjs +1 -1
  7. package/dist/adapters/solidstart.mjs +1 -1
  8. package/dist/adapters/sveltekit.mjs +1 -1
  9. package/dist/client/client.d.mts +42 -4
  10. package/dist/client/client.mjs +42 -4
  11. package/dist/client/server.d.mts +27 -2
  12. package/dist/client/server.mjs +27 -2
  13. package/dist/compile.d.mts +10 -1
  14. package/dist/compile.mjs +13 -4
  15. package/dist/core/context-bridge.d.mts +49 -0
  16. package/dist/core/context-bridge.mjs +43 -7
  17. package/dist/core/context.d.mts +26 -0
  18. package/dist/core/ctx-symbols.mjs +21 -0
  19. package/dist/core/error.d.mts +183 -2
  20. package/dist/core/error.mjs +259 -16
  21. package/dist/core/handler.d.mts +15 -1
  22. package/dist/core/handler.mjs +33 -17
  23. package/dist/core/schema-converter.d.mts +131 -0
  24. package/dist/core/schema-converter.mjs +82 -0
  25. package/dist/core/serve.d.mts +2 -2
  26. package/dist/core/serve.mjs +9 -2
  27. package/dist/core/task.mjs +2 -2
  28. package/dist/index.d.mts +5 -2
  29. package/dist/index.mjs +4 -2
  30. package/dist/integrations/better-auth/index.d.mts +22 -1
  31. package/dist/integrations/better-auth/index.mjs +79 -11
  32. package/dist/integrations/drizzle/index.mjs +22 -5
  33. package/dist/integrations/zod/converter.d.mts +1 -1
  34. package/dist/integrations/zod/index.d.mts +29 -2
  35. package/dist/integrations/zod/index.mjs +60 -1
  36. package/dist/lazy.d.mts +40 -3
  37. package/dist/lazy.mjs +40 -3
  38. package/dist/map-input.mjs +1 -1
  39. package/dist/plugins/analytics/collector.d.mts +1 -1
  40. package/dist/plugins/analytics/trace.mjs +1 -1
  41. package/dist/plugins/analytics/types.d.mts +3 -3
  42. package/dist/plugins/analytics/utils.mjs +1 -4
  43. package/dist/plugins/analytics.d.mts +5 -3
  44. package/dist/plugins/analytics.mjs +16 -29
  45. package/dist/plugins/cache.mjs +1 -1
  46. package/dist/plugins/coerce.mjs +1 -1
  47. package/dist/scalar.d.mts +2 -1
  48. package/dist/scalar.mjs +9 -30
  49. package/dist/silgi.d.mts +165 -18
  50. package/dist/silgi.mjs +47 -11
  51. package/package.json +6 -2
  52. package/dist/core/trace-map.d.mts +0 -13
  53. package/dist/core/trace-map.mjs +0 -13
package/README.md CHANGED
@@ -10,6 +10,53 @@
10
10
 
11
11
  End-to-end type-safe RPC framework for TypeScript. Single package — server, client, 15 plugins, 14 adapters. Full docs at [silgi.dev](https://silgi.dev).
12
12
 
13
+ ## Install
14
+
15
+ ```bash
16
+ pnpm add silgi
17
+ # or: npm install silgi / yarn add silgi / bun add silgi
18
+ ```
19
+
20
+ Requires Node.js `>=24`.
21
+
22
+ ## Minimal example
23
+
24
+ ```ts
25
+ import { silgi } from 'silgi'
26
+
27
+ const k = silgi({
28
+ context: (req) => ({ now: Date.now() }),
29
+ })
30
+
31
+ const hello = k.$resolve(({ ctx }) => ({ message: 'hi', at: ctx.now }))
32
+
33
+ const router = k.router({ hello })
34
+
35
+ export default k.handler(router)
36
+ ```
37
+
38
+ Export `handler` from any Fetch-compatible runtime (Next.js App Router,
39
+ SvelteKit, Remix, Astro, SolidStart, Hono, srvx, Bun, Deno, Cloudflare
40
+ Workers, AWS Lambda via the hono adapter, …). Dedicated adapters for
41
+ Express, Nitro, NestJS, and Node's raw `http` live under
42
+ `silgi/express`, `silgi/nextjs`, `silgi/sveltekit`, etc.
43
+
44
+ Run a standalone server:
45
+
46
+ ```ts
47
+ await k.serve(router, { port: 3000 })
48
+ ```
49
+
50
+ ## Documentation
51
+
52
+ - **[silgi.dev](https://silgi.dev)** — user guide, recipes, API reference.
53
+ - [`CONTRIBUTING.md`](./CONTRIBUTING.md) — dev setup, commands, PR checklist.
54
+ - [`ARCHITECTURE.md`](./ARCHITECTURE.md) — request pipeline, module layout, performance invariants.
55
+ - [`SECURITY.md`](./SECURITY.md) — threat model, reporting policy, security features.
56
+ - [`docs/rfcs/0001-de-magic.md`](./docs/rfcs/0001-de-magic.md) — the
57
+ refactor that removed module-global mutable state, explicit schema
58
+ converter injection, and per-instance context bridges.
59
+
13
60
  ## Credits
14
61
 
15
62
  - [oRPC](https://github.com/unnoq/orpc) — Pipeline architecture, client proxy, error handling, contract-first workflow
@@ -1,4 +1,6 @@
1
+ import { SilgiHooks } from "../silgi.mjs";
1
2
  import { WrapHandlerOptions } from "../core/handler.mjs";
3
+ import { Hookable } from "hookable";
2
4
 
3
5
  //#region src/adapters/_fetch-adapter.d.ts
4
6
  interface FetchAdapterConfig<TCtx extends Record<string, unknown>> extends WrapHandlerOptions {
@@ -6,6 +8,8 @@ interface FetchAdapterConfig<TCtx extends Record<string, unknown>> extends WrapH
6
8
  prefix?: string;
7
9
  /** Context factory — receives the Request (or framework event via eventMap). */
8
10
  context?: (req: Request) => TCtx | Promise<TCtx>;
11
+ /** Lifecycle hooks (request, response, error). */
12
+ hooks?: Hookable<SilgiHooks>;
9
13
  }
10
14
  /**
11
15
  * For adapters where the context factory needs access to a framework event
@@ -16,6 +20,8 @@ interface FetchAdapterConfigWithEvent<TCtx extends Record<string, unknown>, TEve
16
20
  prefix?: string;
17
21
  /** Context factory — receives the framework event, not raw Request. */
18
22
  context?: (event: TEvent) => TCtx | Promise<TCtx>;
23
+ /** Lifecycle hooks (request, response, error). */
24
+ hooks?: Hookable<SilgiHooks>;
19
25
  }
20
26
  //#endregion
21
27
  export { FetchAdapterConfig, FetchAdapterConfigWithEvent };
@@ -1,4 +1,5 @@
1
1
  import { createFetchHandler, wrapHandler } from "../core/handler.mjs";
2
+ import { AsyncLocalStorage } from "node:async_hooks";
2
3
  //#region src/adapters/_fetch-adapter.ts
3
4
  /**
4
5
  * Shared factory for fetch-passthrough adapters.
@@ -15,25 +16,30 @@ import { createFetchHandler, wrapHandler } from "../core/handler.mjs";
15
16
  */
16
17
  function createFetchAdapter(router, options, defaultPrefix) {
17
18
  const prefix = options.prefix ?? defaultPrefix;
18
- return wrapHandler(createFetchHandler(router, options.context ?? (() => ({})), void 0, prefix), router, options, prefix);
19
+ return wrapHandler(createFetchHandler(router, options.context ?? (() => ({})), options.hooks, prefix), router, options, prefix);
19
20
  }
20
21
  /**
21
22
  * Create a fetch-passthrough adapter for frameworks that pass an event object
22
23
  * with a `.request` property (SvelteKit, SolidStart).
23
- * Uses a WeakMap to safely pass the event into the context factory per-request.
24
+ *
25
+ * Propagates the framework event to the context factory via a per-adapter
26
+ * AsyncLocalStorage scope, so the lookup rides the async call chain instead
27
+ * of Request object identity. Middleware that clones or replaces the Request
28
+ * (body reads, URL rewrites, polyfills) no longer breaks context resolution.
29
+ *
30
+ * Resolves: https://github.com/productdevbook/silgi/issues/4
24
31
  */
25
32
  function createEventFetchAdapter(router, options, defaultPrefix, extractRequest) {
26
33
  const prefix = options.prefix ?? defaultPrefix;
27
- const requestEventMap = /* @__PURE__ */ new WeakMap();
34
+ const eventStore = new AsyncLocalStorage();
28
35
  const handler = wrapHandler(createFetchHandler(router, (_req) => {
29
- const eventRef = requestEventMap.get(_req);
30
- if (options.context && eventRef) return options.context(eventRef);
36
+ const eventRef = eventStore.getStore();
37
+ if (options.context && eventRef !== void 0) return options.context(eventRef);
31
38
  return {};
32
- }, void 0, prefix), router, options, prefix);
39
+ }, options.hooks, prefix), router, options, prefix);
33
40
  return (event) => {
34
41
  const request = extractRequest(event);
35
- requestEventMap.set(request, event);
36
- return handler(request);
42
+ return eventStore.run(event, () => handler(request));
37
43
  };
38
44
  }
39
45
  //#endregion
@@ -11,7 +11,7 @@ import { createFetchAdapter } from "./_fetch-adapter.mjs";
11
11
  *
12
12
  * const handler = createHandler(appRouter, {
13
13
  * context: (req) => ({ db: getDB() }),
14
- * analytics: true,
14
+ * analytics: { auth: "your-secret-token" },
15
15
  * })
16
16
  *
17
17
  * export const GET = handler
@@ -11,7 +11,7 @@ import { createFetchAdapter } from "./_fetch-adapter.mjs";
11
11
  *
12
12
  * const handler = createHandler(appRouter, {
13
13
  * context: (req) => ({ db: getDB() }),
14
- * analytics: true,
14
+ * analytics: { auth: "your-secret-token" },
15
15
  * })
16
16
  *
17
17
  * export { handler as GET, handler as POST }
@@ -11,7 +11,7 @@ import { createFetchAdapter } from "./_fetch-adapter.mjs";
11
11
  *
12
12
  * const handler = createHandler(appRouter, {
13
13
  * context: (req) => ({ db: getDB() }),
14
- * analytics: true,
14
+ * analytics: { auth: "your-secret-token" },
15
15
  * })
16
16
  *
17
17
  * export const action = handler
@@ -11,7 +11,7 @@ import { createEventFetchAdapter } from "./_fetch-adapter.mjs";
11
11
  *
12
12
  * const handler = createHandler(appRouter, {
13
13
  * context: (event) => ({ db: getDB() }),
14
- * analytics: true,
14
+ * analytics: { auth: "your-secret-token" },
15
15
  * })
16
16
  *
17
17
  * export const GET = handler
@@ -11,7 +11,7 @@ import { createEventFetchAdapter } from "./_fetch-adapter.mjs";
11
11
  *
12
12
  * const handler = createHandler(appRouter, {
13
13
  * context: (event) => ({ db: getDB(), user: event.locals.user }),
14
- * analytics: true,
14
+ * analytics: { auth: "your-secret-token" },
15
15
  * })
16
16
  *
17
17
  * export const GET = handler
@@ -4,11 +4,35 @@ import { ClientContext, ClientLink } from "./types.mjs";
4
4
 
5
5
  //#region src/client/client.d.ts
6
6
  /**
7
- * Create a type-safe client from a link.
7
+ * Create a type-safe client from a transport-level {@link ClientLink}.
8
8
  *
9
+ * @remarks
10
+ * The returned value is a `Proxy` that mirrors the shape of your server
11
+ * router at the type level. Nested property access builds a dotted
12
+ * procedure path; calling the terminal proxy invokes the link with
13
+ * `(path, input, options)`.
14
+ *
15
+ * Sub-proxies are cached in a `Map` on first access so repeated lookups
16
+ * on the same branch do not re-allocate a new proxy tree.
17
+ *
18
+ * @typeParam T - The inferred server router type (usually
19
+ * `typeof appRouter`).
20
+ * @typeParam TClientContext - Extra per-call context carried through
21
+ * `ClientOptions` (e.g. an abort signal, a request id).
22
+ * @param link - A transport link (fetch, ofetch, websocket, custom).
23
+ * @returns A typed client whose shape matches `T`.
24
+ *
25
+ * @example
9
26
  * ```ts
10
- * const client = createClient<typeof appRouter>(link)
27
+ * import { createClient, createFetchLink } from 'silgi/client'
28
+ *
29
+ * const client = createClient<typeof appRouter>(
30
+ * createFetchLink({ url: 'https://api.example.com' }),
31
+ * )
32
+ * const users = await client.users.list({ limit: 10 })
11
33
  * ```
34
+ *
35
+ * @see {@link createSafeClient} for a `[error, data]` variant.
12
36
  */
13
37
  declare function createClient<T, TClientContext extends ClientContext = Record<never, never>>(link: ClientLink<TClientContext>): InferClient<T>;
14
38
  /**
@@ -22,12 +46,26 @@ interface SafeResult<TOutput, TError> {
22
46
  }
23
47
  declare function safe<TOutput, TError = unknown>(promise: Promise<TOutput>): Promise<SafeResult<TOutput, TError>>;
24
48
  /**
25
- * Create a safe client — every procedure call returns { error, data } instead of throwing.
49
+ * Create a safe client — every procedure call returns a {@link SafeResult}
50
+ * tuple instead of throwing.
51
+ *
52
+ * @remarks
53
+ * Useful when the caller prefers discriminated-union error handling over
54
+ * `try`/`catch`. The underlying transport is unchanged; errors are
55
+ * caught by {@link safe} and surfaced as `{ error, data, isError,
56
+ * isSuccess }`.
57
+ *
58
+ * @typeParam T - The inferred server router type.
59
+ * @typeParam TClientContext - Extra per-call context.
60
+ * @param link - A transport link.
61
+ * @returns A client whose every procedure yields a
62
+ * `Promise<SafeResult<Output, SilgiError>>`.
26
63
  *
64
+ * @example
27
65
  * ```ts
28
66
  * const safeClient = createSafeClient<typeof appRouter>(link)
29
67
  * const { error, data } = await safeClient.users.list()
30
- * if (error) console.error(error)
68
+ * if (error) console.error(error.code, error.status)
31
69
  * ```
32
70
  */
33
71
  declare function createSafeClient<T, TClientContext extends ClientContext = Record<never, never>>(link: ClientLink<TClientContext>): InferSafeClient<T>;
@@ -1,10 +1,34 @@
1
1
  //#region src/client/client.ts
2
2
  /**
3
- * Create a type-safe client from a link.
3
+ * Create a type-safe client from a transport-level {@link ClientLink}.
4
4
  *
5
+ * @remarks
6
+ * The returned value is a `Proxy` that mirrors the shape of your server
7
+ * router at the type level. Nested property access builds a dotted
8
+ * procedure path; calling the terminal proxy invokes the link with
9
+ * `(path, input, options)`.
10
+ *
11
+ * Sub-proxies are cached in a `Map` on first access so repeated lookups
12
+ * on the same branch do not re-allocate a new proxy tree.
13
+ *
14
+ * @typeParam T - The inferred server router type (usually
15
+ * `typeof appRouter`).
16
+ * @typeParam TClientContext - Extra per-call context carried through
17
+ * `ClientOptions` (e.g. an abort signal, a request id).
18
+ * @param link - A transport link (fetch, ofetch, websocket, custom).
19
+ * @returns A typed client whose shape matches `T`.
20
+ *
21
+ * @example
5
22
  * ```ts
6
- * const client = createClient<typeof appRouter>(link)
23
+ * import { createClient, createFetchLink } from 'silgi/client'
24
+ *
25
+ * const client = createClient<typeof appRouter>(
26
+ * createFetchLink({ url: 'https://api.example.com' }),
27
+ * )
28
+ * const users = await client.users.list({ limit: 10 })
7
29
  * ```
30
+ *
31
+ * @see {@link createSafeClient} for a `[error, data]` variant.
8
32
  */
9
33
  function createClient(link) {
10
34
  return createClientProxy(link, []);
@@ -46,12 +70,26 @@ async function safe(promise) {
46
70
  }
47
71
  }
48
72
  /**
49
- * Create a safe client — every procedure call returns { error, data } instead of throwing.
73
+ * Create a safe client — every procedure call returns a {@link SafeResult}
74
+ * tuple instead of throwing.
75
+ *
76
+ * @remarks
77
+ * Useful when the caller prefers discriminated-union error handling over
78
+ * `try`/`catch`. The underlying transport is unchanged; errors are
79
+ * caught by {@link safe} and surfaced as `{ error, data, isError,
80
+ * isSuccess }`.
81
+ *
82
+ * @typeParam T - The inferred server router type.
83
+ * @typeParam TClientContext - Extra per-call context.
84
+ * @param link - A transport link.
85
+ * @returns A client whose every procedure yields a
86
+ * `Promise<SafeResult<Output, SilgiError>>`.
50
87
  *
88
+ * @example
51
89
  * ```ts
52
90
  * const safeClient = createSafeClient<typeof appRouter>(link)
53
91
  * const { error, data } = await safeClient.users.list()
54
- * if (error) console.error(error)
92
+ * if (error) console.error(error.code, error.status)
55
93
  * ```
56
94
  */
57
95
  function createSafeClient(link) {
@@ -8,8 +8,33 @@ interface ServerClientOptions<TCtx extends Record<string, unknown>> {
8
8
  /**
9
9
  * Create a type-safe client that calls procedures directly in-process.
10
10
  *
11
- * No HTTP, no serialization, no network — just compiled pipeline execution.
12
- * Uses the same compiled handlers as serve() and handler().
11
+ * @remarks
12
+ * No HTTP, no serialization, no network the client resolves
13
+ * procedures through the same compiled pipeline as `handler()` and
14
+ * `serve()`. Intended for SSR, server components, and tests where the
15
+ * typed client interface is convenient but a network round-trip is not.
16
+ *
17
+ * Note that this helper is a thin proxy over `compileRouter`; it does
18
+ * not run through `silgi.runInContext`, so integrations that read
19
+ * `silgi.currentContext()` should call `silgi.runInContext` around the
20
+ * invocation or use `silgi.createCaller()` instead.
21
+ *
22
+ * @typeParam TRouter - The router type (usually `typeof appRouter`).
23
+ * @typeParam TCtx - The context shape returned by `options.context`.
24
+ * @param router - The router definition to call into.
25
+ * @param options - Server-client configuration; must include a
26
+ * `context` factory.
27
+ * @returns A typed client mirroring `TRouter`.
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * import { createServerClient } from 'silgi/client/server'
32
+ *
33
+ * const client = createServerClient(appRouter, {
34
+ * context: () => ({ db: getDB() }),
35
+ * })
36
+ * const users = await client.users.list({ limit: 10 })
37
+ * ```
13
38
  */
14
39
  declare function createServerClient<TRouter extends RouterDef, TCtx extends Record<string, unknown>>(router: TRouter, options: ServerClientOptions<TCtx>): InferClient<TRouter>;
15
40
  //#endregion
@@ -21,8 +21,33 @@ import { compileRouter } from "../compile.mjs";
21
21
  /**
22
22
  * Create a type-safe client that calls procedures directly in-process.
23
23
  *
24
- * No HTTP, no serialization, no network — just compiled pipeline execution.
25
- * Uses the same compiled handlers as serve() and handler().
24
+ * @remarks
25
+ * No HTTP, no serialization, no network the client resolves
26
+ * procedures through the same compiled pipeline as `handler()` and
27
+ * `serve()`. Intended for SSR, server components, and tests where the
28
+ * typed client interface is convenient but a network round-trip is not.
29
+ *
30
+ * Note that this helper is a thin proxy over `compileRouter`; it does
31
+ * not run through `silgi.runInContext`, so integrations that read
32
+ * `silgi.currentContext()` should call `silgi.runInContext` around the
33
+ * invocation or use `silgi.createCaller()` instead.
34
+ *
35
+ * @typeParam TRouter - The router type (usually `typeof appRouter`).
36
+ * @typeParam TCtx - The context shape returned by `options.context`.
37
+ * @param router - The router definition to call into.
38
+ * @param options - Server-client configuration; must include a
39
+ * `context` factory.
40
+ * @returns A typed client mirroring `TRouter`.
41
+ *
42
+ * @example
43
+ * ```ts
44
+ * import { createServerClient } from 'silgi/client/server'
45
+ *
46
+ * const client = createServerClient(appRouter, {
47
+ * context: () => ({ db: getDB() }),
48
+ * })
49
+ * const users = await client.users.list({ limit: 10 })
50
+ * ```
26
51
  */
27
52
  function createServerClient(router, options) {
28
53
  return createServerProxy(compileRouter(router), options.context, []);
@@ -38,7 +38,16 @@ type CompiledRouterFn = (method: string, path: string) => MatchedRoute<CompiledR
38
38
  * Powered by rou3 (unjs) — battle-tested, fast, minimal.
39
39
  */
40
40
  declare function compileRouter(def: Record<string, unknown>): CompiledRouterFn;
41
+ /**
42
+ * Pooled context with built-in `using` support. `Symbol.dispose` calls
43
+ * `releaseContext` unless ownership has been transferred elsewhere
44
+ * (e.g. to a streaming Response) — in that case the new owner is
45
+ * expected to call `releaseContext` when the stream ends.
46
+ *
47
+ * Transfer ownership by calling `detachContext(ctx)` before returning.
48
+ */
49
+ type PooledContext = Record<string, unknown> & Disposable;
41
50
  /** Acquire a context object from the pool (or create one). */
42
- declare function createContext(): Record<string, unknown>;
51
+ declare function createContext(): PooledContext;
43
52
  //#endregion
44
53
  export { compileProcedure, compileRouter, createContext };
package/dist/compile.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import { validateSchema } from "./core/schema.mjs";
2
2
  import { SilgiError } from "./core/error.mjs";
3
3
  import { isProcedureDef } from "./core/router-utils.mjs";
4
+ import { RAW_INPUT } from "./core/ctx-symbols.mjs";
4
5
  import { addRoute, createRouter, findRoute } from "rou3";
5
6
  //#region src/compile.ts
6
7
  /**
@@ -10,8 +11,6 @@ import { addRoute, createRouter, findRoute } from "rou3";
10
11
  * 2. ZERO-ALLOC CONTEXT — Object.create(null) + pool reuse
11
12
  * 3. ROU3 ROUTER — unjs radix tree (same as h3/nitro)
12
13
  */
13
- /** Internal symbol for pipeline raw input — prevents collision with user context keys */
14
- const RAW_INPUT = Symbol.for("silgi.rawInput");
15
14
  function isThenable(value) {
16
15
  return value !== null && typeof value === "object" && typeof value.then === "function";
17
16
  }
@@ -319,8 +318,18 @@ const CTX_POOL = [];
319
318
  const CTX_POOL_MAX = 128;
320
319
  /** Acquire a context object from the pool (or create one). */
321
320
  function createContext() {
322
- return CTX_POOL.length > 0 ? CTX_POOL.pop() : Object.create(null);
321
+ const ctx = CTX_POOL.length > 0 ? CTX_POOL.pop() : Object.create(null);
322
+ ctx[Symbol.dispose] = disposeContext;
323
+ return ctx;
323
324
  }
325
+ /** Mark the context as owned elsewhere so `using` won't release it. */
326
+ function detachContext(ctx) {
327
+ ctx[Symbol.dispose] = noopDispose;
328
+ }
329
+ function disposeContext() {
330
+ releaseContext(this);
331
+ }
332
+ function noopDispose() {}
324
333
  /** Return a context object to the pool after request completes. */
325
334
  function releaseContext(ctx) {
326
335
  for (const key of Object.keys(ctx)) delete ctx[key];
@@ -328,4 +337,4 @@ function releaseContext(ctx) {
328
337
  if (CTX_POOL.length < CTX_POOL_MAX) CTX_POOL.push(ctx);
329
338
  }
330
339
  //#endregion
331
- export { RAW_INPUT, compileProcedure, compileRouter, createContext, releaseContext };
340
+ export { compileProcedure, compileRouter, createContext, detachContext, releaseContext };
@@ -0,0 +1,49 @@
1
+ //#region src/core/context-bridge.d.ts
2
+ /**
3
+ * Per-instance context bridge built on `AsyncLocalStorage`.
4
+ *
5
+ * @remarks
6
+ * Each silgi instance owns its own bridge, preventing context bleed
7
+ * between multiple `silgi()` instances in the same process. The bridge
8
+ * is created lazily in `silgi()` and exposed on the instance as
9
+ * {@link SilgiInstance.runInContext} / {@link SilgiInstance.currentContext}.
10
+ *
11
+ * The top-level `getCtx` / `runWithCtx` free functions that previously
12
+ * lived in this module have been removed — they were module-global and
13
+ * silently shared state across instances.
14
+ *
15
+ * @category Context
16
+ */
17
+ /**
18
+ * Wraps an `AsyncLocalStorage` with a small typed surface. Call
19
+ * {@link createContextBridge} to construct one.
20
+ *
21
+ * @typeParam TCtx - Context shape stored in the bridge.
22
+ * @category Context
23
+ */
24
+ interface ContextBridge<TCtx extends Record<string, unknown> = Record<string, unknown>> {
25
+ /** Execute `fn` with `ctx` installed on the current async scope. */
26
+ run<T>(ctx: TCtx, fn: () => T): T;
27
+ /** Read the context installed by the nearest enclosing `run()` call, or `undefined`. */
28
+ current(): TCtx | undefined;
29
+ }
30
+ /**
31
+ * Create a fresh {@link ContextBridge}. Each silgi instance calls this
32
+ * once internally; integrations that need their own ambient scope (e.g.
33
+ * for programmatic `withCtx()` helpers) can also call it.
34
+ *
35
+ * @typeParam TCtx - Context shape to store.
36
+ *
37
+ * @example
38
+ * ```ts
39
+ * const bridge = createContextBridge<{ userId: string }>()
40
+ * bridge.run({ userId: 'u_1' }, () => {
41
+ * bridge.current()?.userId // 'u_1'
42
+ * })
43
+ * ```
44
+ *
45
+ * @category Context
46
+ */
47
+ declare function createContextBridge<TCtx extends Record<string, unknown> = Record<string, unknown>>(): ContextBridge<TCtx>;
48
+ //#endregion
49
+ export { ContextBridge, createContextBridge };
@@ -1,11 +1,47 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
2
  //#region src/core/context-bridge.ts
3
- const ctxStorage = new AsyncLocalStorage();
4
- function runWithCtx(ctx, fn) {
5
- return ctxStorage.run(ctx, fn);
6
- }
7
- function getCtx() {
8
- return ctxStorage.getStore();
3
+ /**
4
+ * Per-instance context bridge built on `AsyncLocalStorage`.
5
+ *
6
+ * @remarks
7
+ * Each silgi instance owns its own bridge, preventing context bleed
8
+ * between multiple `silgi()` instances in the same process. The bridge
9
+ * is created lazily in `silgi()` and exposed on the instance as
10
+ * {@link SilgiInstance.runInContext} / {@link SilgiInstance.currentContext}.
11
+ *
12
+ * The top-level `getCtx` / `runWithCtx` free functions that previously
13
+ * lived in this module have been removed — they were module-global and
14
+ * silently shared state across instances.
15
+ *
16
+ * @category Context
17
+ */
18
+ /**
19
+ * Create a fresh {@link ContextBridge}. Each silgi instance calls this
20
+ * once internally; integrations that need their own ambient scope (e.g.
21
+ * for programmatic `withCtx()` helpers) can also call it.
22
+ *
23
+ * @typeParam TCtx - Context shape to store.
24
+ *
25
+ * @example
26
+ * ```ts
27
+ * const bridge = createContextBridge<{ userId: string }>()
28
+ * bridge.run({ userId: 'u_1' }, () => {
29
+ * bridge.current()?.userId // 'u_1'
30
+ * })
31
+ * ```
32
+ *
33
+ * @category Context
34
+ */
35
+ function createContextBridge() {
36
+ const storage = new AsyncLocalStorage();
37
+ return {
38
+ run(ctx, fn) {
39
+ return storage.run(ctx, fn);
40
+ },
41
+ current() {
42
+ return storage.getStore();
43
+ }
44
+ };
9
45
  }
10
46
  //#endregion
11
- export { getCtx, runWithCtx };
47
+ export { createContextBridge };
@@ -0,0 +1,26 @@
1
+ import { RequestTrace } from "../plugins/analytics/trace.mjs";
2
+ //#region src/core/context.d.ts
3
+ /**
4
+ * Fields that Silgi internals may place on every request's `ctx`.
5
+ *
6
+ * @remarks
7
+ * All fields are optional — they appear only when the relevant framework
8
+ * feature is active (e.g. `trace` only when analytics is enabled;
9
+ * `params` only when the matched route has URL parameters). Users extend
10
+ * their own context via the `context` factory passed to `silgi()`.
11
+ *
12
+ * At runtime the pipeline still treats `ctx` as a loose
13
+ * `Record<string, unknown>`; this interface exists so contributors and
14
+ * TypeScript users can see which keys are framework-reserved without
15
+ * grepping the codebase.
16
+ *
17
+ * @category Context
18
+ */
19
+ interface BaseContext {
20
+ /** URL path parameters from the matched route, when present. */
21
+ params?: Record<string, string>;
22
+ /** Per-request analytics trace, attached by the analytics plugin. */
23
+ trace?: RequestTrace;
24
+ }
25
+ //#endregion
26
+ export { BaseContext };
@@ -0,0 +1,21 @@
1
+ //#region src/core/ctx-symbols.ts
2
+ /**
3
+ * Framework-internal symbol keys for the shared pipeline context.
4
+ *
5
+ * @internal
6
+ *
7
+ * @remarks
8
+ * These keys are reserved by Silgi internals and MUST NOT be used as
9
+ * ordinary fields in user context objects. Centralizing them in one
10
+ * module makes the reserved surface trivial to audit — there is nowhere
11
+ * else in the codebase where silgi stamps a symbol key onto `ctx`.
12
+ */
13
+ /**
14
+ * Pipeline raw-input slot. The wrap onion reads the input off this slot
15
+ * so middleware (e.g. `mapInput`) can rewrite it before the resolver runs.
16
+ *
17
+ * @internal
18
+ */
19
+ const RAW_INPUT = Symbol.for("silgi.rawInput");
20
+ //#endregion
21
+ export { RAW_INPUT };