silgi 0.51.7 → 0.51.8

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/dist/silgi.d.mts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { AnySchema, InferSchemaInput, InferSchemaOutput } from "./core/schema.mjs";
2
2
  import { ErrorDef, GuardDef, GuardFn, InferClient, ProcedureDef, ResolveContext, RouterDef, WrapDef, WrapFn } from "./types.mjs";
3
3
  import { AnalyticsOptions } from "./plugins/analytics/types.mjs";
4
+ import { SchemaConverter } from "./core/schema-converter.mjs";
4
5
  import { ScalarOptions } from "./scalar.mjs";
5
6
  import { ProcedureBuilder } from "./builder.mjs";
6
7
  import { ServeOptions, SilgiServer } from "./core/serve.mjs";
@@ -37,11 +38,53 @@ interface SilgiHooks {
37
38
  port: number;
38
39
  hostname: string;
39
40
  }) => void;
41
+ /**
42
+ * Fires after base context is applied and params are merged, before input parsing.
43
+ * Framework plugins (e.g. analytics) use this to inject fields into `ctx`
44
+ * before any user code runs.
45
+ *
46
+ * @internal
47
+ */
48
+ 'request:prepare': (event: {
49
+ request: Request;
50
+ ctx: Record<string, unknown>;
51
+ }) => void;
52
+ /**
53
+ * Fires after the pipeline produces output, before the `Response` is built.
54
+ * Framework plugins use this to capture output for trace recording.
55
+ *
56
+ * @internal
57
+ */
58
+ 'response:finalize': (event: {
59
+ request: Request;
60
+ ctx: Record<string, unknown>;
61
+ output: unknown;
62
+ }) => void;
40
63
  }
41
64
  interface SilgiConfig<TCtx extends Record<string, unknown>> {
42
65
  context: (req: Request) => TCtx | Promise<TCtx>;
43
66
  /** Register lifecycle hooks */
44
67
  hooks?: Partial<{ [K in keyof SilgiHooks]: SilgiHooks[K] | SilgiHooks[K][] }>;
68
+ /**
69
+ * Schema converters for OpenAPI spec generation and analytics schema extraction.
70
+ *
71
+ * @remarks
72
+ * Pass a converter for each schema library you use. Schemas with a native
73
+ * `jsonSchema.input()` implementation (Valibot, ArkType, Zod v4) work
74
+ * without registering anything. Converters are required for libraries
75
+ * that do not implement the Standard JSON Schema extension.
76
+ *
77
+ * @example
78
+ * ```ts
79
+ * import { zodConverter } from 'silgi/zod'
80
+ *
81
+ * const k = silgi({
82
+ * context: (req) => ({ db: getDB() }),
83
+ * schemaConverters: [zodConverter],
84
+ * })
85
+ * ```
86
+ */
87
+ schemaConverters?: SchemaConverter[];
45
88
  /**
46
89
  * Storage configuration — mount drivers by path prefix.
47
90
  *
@@ -69,35 +112,106 @@ interface SilgiInstance<TBaseCtx extends Record<string, unknown>> {
69
112
  removeHook: Hookable<SilgiHooks>['removeHook'];
70
113
  /** Access storage with optional prefix — uses configured mounts */
71
114
  useStorage: typeof useStorage;
72
- /** Create a guard middleware (flat, zero-closure) */
115
+ /**
116
+ * Run `fn` inside this instance's per-request `AsyncLocalStorage` scope.
117
+ *
118
+ * @remarks
119
+ * Instrumented integrations (Drizzle, Better Auth) read the installed
120
+ * context via {@link SilgiInstance.currentContext}. Because each silgi
121
+ * instance owns its own bridge, calls across instances do not collide.
122
+ *
123
+ * @param ctx - Context to install for the duration of `fn`.
124
+ * @param fn - Function executed with `ctx` as the ambient context.
125
+ * @returns Whatever `fn` returns.
126
+ */
127
+ runInContext: <T>(ctx: TBaseCtx, fn: () => T) => T;
128
+ /**
129
+ * Read the context installed by the nearest enclosing
130
+ * {@link SilgiInstance.runInContext}, or `undefined` if none.
131
+ */
132
+ currentContext: () => TBaseCtx | undefined;
133
+ /**
134
+ * Await storage initialization.
135
+ *
136
+ * @remarks
137
+ * When `storage` is configured, resolves after `initStorage` completes.
138
+ * When storage is not configured, resolves immediately (no dynamic
139
+ * import). Errors during storage init reject this promise — no silent
140
+ * `console.error` fallback.
141
+ *
142
+ * `useStorage()` awaits this promise internally, so calling `ready()`
143
+ * is optional unless you need an explicit ordering guarantee before
144
+ * your first `useStorage()` call.
145
+ *
146
+ * @example
147
+ * ```ts
148
+ * const k = silgi({ context: () => ({}), storage: { cache: redisDriver() } })
149
+ * await k.ready() // storage driver connected
150
+ * ```
151
+ */
152
+ ready: () => Promise<void>;
153
+ /**
154
+ * Create a guard middleware — a flat, zero-closure helper that runs
155
+ * before the resolver and can throw or return partial context.
156
+ *
157
+ * @remarks
158
+ * Prefer `guard` over `wrap` when you only need a pre-step. The
159
+ * returned object is passed to `$use(guard)` on any builder.
160
+ */
73
161
  guard: GuardFactory<TBaseCtx>;
74
- /** Create a wrap middleware (onion, before+after) */
162
+ /**
163
+ * Create a wrap middleware — onion-style before/after hook that can
164
+ * short-circuit the pipeline or transform the output.
165
+ */
75
166
  wrap: (fn: WrapFn<TBaseCtx>) => WrapDef<TBaseCtx>;
76
- /** Start a builder — resolve only */
167
+ /** Start a builder chain set the resolver for a query procedure. */
77
168
  $resolve: ProcedureBuilder<'query', TBaseCtx>['$resolve'];
78
- /** Start a builder — set input schema */
169
+ /** Start a builder chain — set the input schema (Standard Schema). */
79
170
  $input: ProcedureBuilder<'query', TBaseCtx>['$input'];
80
- /** Start a builder — add middleware */
171
+ /** Start a builder chain — add guard/wrap middleware. */
81
172
  $use: ProcedureBuilder<'query', TBaseCtx>['$use'];
82
- /** Start a builder — set output schema */
173
+ /** Start a builder chain — set the output schema. */
83
174
  $output: ProcedureBuilder<'query', TBaseCtx>['$output'];
84
- /** Start a builder — set errors */
175
+ /** Start a builder chain declare typed errors. */
85
176
  $errors: ProcedureBuilder<'query', TBaseCtx>['$errors'];
86
- /** Start a builder — set route metadata */
177
+ /** Start a builder chain — set HTTP route metadata (method, path, etc). */
87
178
  $route: ProcedureBuilder<'query', TBaseCtx>['$route'];
88
- /** Start a builder — set custom metadata */
179
+ /** Start a builder chain attach custom metadata for tooling. */
89
180
  $meta: ProcedureBuilder<'query', TBaseCtx>['$meta'];
90
- /** Define a subscription (SSE stream) */
181
+ /** Define a subscription — returns an SSE stream of events. */
91
182
  subscription: SubscriptionFactory<TBaseCtx>;
92
- /** Start a builder — create a background task */
183
+ /**
184
+ * Start a builder chain — create a background/cron task.
185
+ *
186
+ * @remarks
187
+ * Tasks are collected from the router on `serve()` and scheduled via
188
+ * `croner` when a `cron` spec is provided.
189
+ */
93
190
  $task: ProcedureBuilder<'query', TBaseCtx>['$task'];
94
- /** Assemble router and compile pipelines */
191
+ /**
192
+ * Assemble a router from nested procedures and pre-compile each
193
+ * pipeline.
194
+ *
195
+ * @remarks
196
+ * The returned value is the same object you passed in — path
197
+ * assignment and compilation happen off to the side, cached in a
198
+ * `WeakMap` keyed on the def. Never mutate the router after handing
199
+ * it to `router()`; build a new one instead.
200
+ */
95
201
  router: <T extends RouterDef>(def: T) => T;
96
- /** Create a Fetch API handler: (Request) => Response */
202
+ /**
203
+ * Create a Fetch API handler — `(Request) => Response | Promise<Response>`.
204
+ *
205
+ * @remarks
206
+ * Use this from any Fetch-compatible adapter (Next.js App Router,
207
+ * SvelteKit, Remix, srvx, Cloudflare Workers, Bun, Deno, hono over
208
+ * Lambda, etc.). The router has subscriptions mounted automatically
209
+ * when `hasWsProcedures` is detected.
210
+ */
97
211
  handler: (router: RouterDef, options?: {
98
212
  /** URL path prefix (e.g. "/api"). Only requests matching this prefix are handled; others return 404. */basePath?: string; /** Enable Scalar API Reference UI at /api/reference and /api/openapi.json */
99
- scalar?: boolean | ScalarOptions; /** Enable analytics dashboard at /api/analytics */
100
- analytics?: boolean | AnalyticsOptions;
213
+ scalar?: boolean | ScalarOptions; /** Enable analytics dashboard at /api/analytics — requires `auth` to be set */
214
+ analytics?: AnalyticsOptions;
101
215
  }) => (request: Request) => Response | Promise<Response>;
102
216
  /** Create a direct caller — call procedures without HTTP. For testing and server-side usage. */
103
217
  createCaller: <T extends RouterDef>(router: T, options?: {
@@ -105,8 +219,24 @@ interface SilgiInstance<TBaseCtx extends Record<string, unknown>> {
105
219
  headers?: Record<string, string>; /** Default timeout in ms (default: 30000, null = no timeout) */
106
220
  timeout?: number | null;
107
221
  }) => InferClient<T>;
108
- /** Create & start a Node.js HTTP server. Returns a handle to gracefully shut down. */
109
- serve: (router: RouterDef, options?: ServeOptions) => Promise<SilgiServer>;
222
+ /**
223
+ * Create & start a Node.js HTTP server. Returns a handle to gracefully shut down.
224
+ *
225
+ * @remarks
226
+ * When `options.handleSignals` is `true`, registers `process.once('SIGINT')`
227
+ * and `process.once('SIGTERM')` listeners that invoke `server.close()`.
228
+ * Default `false` — opt in explicitly. The srvx-level graceful HTTP drain
229
+ * is controlled by `ServeOptions.gracefulShutdown`; `handleSignals`
230
+ * governs only the silgi-layer cron-stop wiring on OS signals.
231
+ */
232
+ serve: (router: RouterDef, options?: ServeOptions & {
233
+ /**
234
+ * Register `process.once('SIGINT')` / `'SIGTERM'` listeners that call
235
+ * `server.close()`. Default `false` (opt-in). The close wrapper stops
236
+ * cron jobs regardless of this setting when called explicitly.
237
+ */
238
+ handleSignals?: boolean;
239
+ }) => Promise<SilgiServer>;
110
240
  }
111
241
  interface GuardConfig<TBaseCtx, TReturn extends Record<string, unknown> | void, TErrors extends ErrorDef> {
112
242
  errors?: TErrors;
@@ -129,6 +259,20 @@ interface SubscriptionFactory<TBaseCtx extends Record<string, unknown>> {
129
259
  /**
130
260
  * Create a Silgi RPC instance with typed context.
131
261
  *
262
+ * @remarks
263
+ * Every call returns a self-contained instance with its own schema
264
+ * registry, `AsyncLocalStorage` bridge, hook emitter and storage state.
265
+ * Two `silgi()` instances in the same process never share mutable state
266
+ * — see [ARCHITECTURE.md §3](../ARCHITECTURE.md) for the "de-magic"
267
+ * invariants.
268
+ *
269
+ * @typeParam TBaseCtx - Shape of the base context returned by
270
+ * `config.context(req)`. Flows into every procedure's `ResolveContext`.
271
+ * @param config - Instance configuration. `context` is required; all
272
+ * other fields are opt-in.
273
+ * @returns A {@link SilgiInstance} exposing builder, router, handler,
274
+ * caller and server helpers.
275
+ *
132
276
  * @example
133
277
  * ```ts
134
278
  * const k = silgi({
@@ -139,7 +283,10 @@ interface SubscriptionFactory<TBaseCtx extends Record<string, unknown>> {
139
283
  * })
140
284
  * // k.$input(), k.$resolve(), k.guard(), k.router(), k.serve()
141
285
  * ```
286
+ *
287
+ * @see {@link SilgiInstance}
288
+ * @see {@link SilgiConfig}
142
289
  */
143
290
  declare function silgi<TBaseCtx extends Record<string, unknown>>(config: SilgiConfig<TBaseCtx>): SilgiInstance<TBaseCtx>;
144
291
  //#endregion
145
- export { SilgiConfig, SilgiInstance, silgi };
292
+ export { SilgiConfig, SilgiHooks, SilgiInstance, silgi };
package/dist/silgi.mjs CHANGED
@@ -3,8 +3,10 @@ import { createProcedureBuilder } from "./builder.mjs";
3
3
  import { assignPaths, routerCache } from "./core/router-utils.mjs";
4
4
  import { compileRouter } from "./compile.mjs";
5
5
  import { createCaller } from "./caller.mjs";
6
+ import { createContextBridge } from "./core/context-bridge.mjs";
6
7
  import { normalizePrefix } from "./core/url.mjs";
7
8
  import { createFetchHandler, wrapHandler } from "./core/handler.mjs";
9
+ import { createSchemaRegistry } from "./core/schema-converter.mjs";
8
10
  import { createHooks } from "hookable";
9
11
  //#region src/silgi.ts
10
12
  /**
@@ -45,6 +47,20 @@ function createProcedure(type, ...args) {
45
47
  /**
46
48
  * Create a Silgi RPC instance with typed context.
47
49
  *
50
+ * @remarks
51
+ * Every call returns a self-contained instance with its own schema
52
+ * registry, `AsyncLocalStorage` bridge, hook emitter and storage state.
53
+ * Two `silgi()` instances in the same process never share mutable state
54
+ * — see [ARCHITECTURE.md §3](../ARCHITECTURE.md) for the "de-magic"
55
+ * invariants.
56
+ *
57
+ * @typeParam TBaseCtx - Shape of the base context returned by
58
+ * `config.context(req)`. Flows into every procedure's `ResolveContext`.
59
+ * @param config - Instance configuration. `context` is required; all
60
+ * other fields are opt-in.
61
+ * @returns A {@link SilgiInstance} exposing builder, router, handler,
62
+ * caller and server helpers.
63
+ *
48
64
  * @example
49
65
  * ```ts
50
66
  * const k = silgi({
@@ -55,24 +71,32 @@ function createProcedure(type, ...args) {
55
71
  * })
56
72
  * // k.$input(), k.$resolve(), k.guard(), k.router(), k.serve()
57
73
  * ```
74
+ *
75
+ * @see {@link SilgiInstance}
76
+ * @see {@link SilgiConfig}
58
77
  */
59
78
  function silgi(config) {
60
79
  const contextFactory = config.context;
80
+ const schemaRegistry = createSchemaRegistry(config.schemaConverters ?? []);
81
+ const bridge = createContextBridge();
61
82
  const hooks = createHooks();
62
83
  if (config.hooks) {
63
84
  for (const [name, fn] of Object.entries(config.hooks)) if (Array.isArray(fn)) for (const f of fn) hooks.hook(name, f);
64
85
  else if (fn) hooks.hook(name, fn);
65
86
  }
66
- if (config.storage) import("./core/storage.mjs").then((m) => m.initStorage(config.storage)).catch((e) => {
67
- console.error(`[silgi] Failed to initialize storage: ${e instanceof Error ? e.message : e}`);
68
- });
87
+ const readyPromise = config.storage ? import("./core/storage.mjs").then((m) => {
88
+ m.initStorage(config.storage);
89
+ }) : Promise.resolve();
69
90
  const ctxFactory = () => contextFactory(new Request("http://localhost"));
70
91
  return {
71
92
  hook: hooks.hook.bind(hooks),
72
93
  removeHook: hooks.removeHook.bind(hooks),
73
94
  useStorage: (...args) => {
74
- return import("./core/storage.mjs").then((m) => m.useStorage(...args));
95
+ return readyPromise.then(() => import("./core/storage.mjs")).then((m) => m.useStorage(...args));
75
96
  },
97
+ runInContext: (ctx, fn) => bridge.run(ctx, fn),
98
+ currentContext: () => bridge.current(),
99
+ ready: () => readyPromise,
76
100
  guard: (fnOrConfig) => {
77
101
  if (typeof fnOrConfig === "function") return {
78
102
  kind: "guard",
@@ -115,7 +139,14 @@ function silgi(config) {
115
139
  },
116
140
  handler: (routerDef, options) => {
117
141
  const prefix = options?.basePath ? normalizePrefix(options.basePath) : void 0;
118
- const fetchHandler = wrapHandler(createFetchHandler(routerDef, contextFactory, hooks, prefix), routerDef, options, prefix);
142
+ const fetchHandler = wrapHandler(createFetchHandler(routerDef, contextFactory, hooks, prefix, bridge), routerDef, options ? {
143
+ ...options,
144
+ schemaRegistry,
145
+ hooks
146
+ } : {
147
+ schemaRegistry,
148
+ hooks
149
+ }, prefix);
119
150
  if (!(function checkWs(def) {
120
151
  if (!def || typeof def !== "object") return false;
121
152
  if (def.type === "subscription") return true;
@@ -146,7 +177,7 @@ function silgi(config) {
146
177
  },
147
178
  serve: async (routerDef, options) => {
148
179
  const { createServeHandler } = await import("./core/serve.mjs");
149
- const server = await createServeHandler(routerDef, contextFactory, hooks, options);
180
+ const server = await createServeHandler(routerDef, contextFactory, hooks, options, schemaRegistry, bridge);
150
181
  const { collectCronTasks, startCronJobs, stopCronJobs } = await import("./core/task.mjs");
151
182
  const cronTasks = collectCronTasks(routerDef);
152
183
  if (cronTasks.length > 0) {
@@ -154,14 +185,19 @@ function silgi(config) {
154
185
  console.log(` ${cronTasks.length} cron task(s) scheduled`);
155
186
  }
156
187
  const originalClose = server.close.bind(server);
157
- server.close = async (force) => {
188
+ const wrappedClose = async (force) => {
158
189
  stopCronJobs();
159
190
  return originalClose(force);
160
191
  };
161
- const onSignal = () => stopCronJobs();
162
- process.once("SIGINT", onSignal);
163
- process.once("SIGTERM", onSignal);
164
- return server;
192
+ const silgiServer = Object.assign(Object.create(Object.getPrototypeOf(server)), server, { close: wrappedClose });
193
+ if (options?.handleSignals) {
194
+ const onSignal = () => {
195
+ wrappedClose().catch(() => {});
196
+ };
197
+ process.once("SIGINT", onSignal);
198
+ process.once("SIGTERM", onSignal);
199
+ }
200
+ return silgiServer;
165
201
  }
166
202
  };
167
203
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "silgi",
3
- "version": "0.51.7",
3
+ "version": "0.51.8",
4
4
  "private": false,
5
5
  "description": "The fastest end-to-end type-safe RPC framework for TypeScript — compiled pipelines, single package, every runtime",
6
6
  "keywords": [
@@ -41,7 +41,9 @@
41
41
  "lib"
42
42
  ],
43
43
  "type": "module",
44
- "sideEffects": false,
44
+ "sideEffects": [
45
+ "./dist/integrations/zod/index.mjs"
46
+ ],
45
47
  "exports": {
46
48
  ".": {
47
49
  "import": "./dist/index.mjs",
@@ -286,6 +288,7 @@
286
288
  "pinia": "^3.0.4",
287
289
  "rou3": "^0.8.1",
288
290
  "tsdown": "^0.21.7",
291
+ "typedoc": "^0.28.19",
289
292
  "typescript": "^6.0.2",
290
293
  "vitest": "^4.1.2",
291
294
  "vue": "^3.5.31",
@@ -337,6 +340,7 @@
337
340
  "fix": "oxlint --fix . && oxfmt --ignore-path .oxfmtignore .",
338
341
  "test": "vitest run",
339
342
  "typecheck": "tsgo --noEmit",
343
+ "docs:check": "typedoc --options typedoc.json",
340
344
  "release": "bumpp"
341
345
  }
342
346
  }
@@ -1,13 +0,0 @@
1
- //#region src/core/trace-map.d.ts
2
- /**
3
- * Shared WeakMap for passing analytics traces between handler and analytics plugin.
4
- *
5
- * Lives in core/ so the dependency direction is correct:
6
- * core/handler.ts → core/trace-map.ts ← plugins/analytics.ts
7
- *
8
- * The WeakMap maps Request → RequestTrace, allowing the handler to inject
9
- * trace data into context without importing the analytics plugin.
10
- */
11
- declare const analyticsTraceMap: WeakMap<Request, unknown>;
12
- //#endregion
13
- export { analyticsTraceMap };
@@ -1,13 +0,0 @@
1
- //#region src/core/trace-map.ts
2
- /**
3
- * Shared WeakMap for passing analytics traces between handler and analytics plugin.
4
- *
5
- * Lives in core/ so the dependency direction is correct:
6
- * core/handler.ts → core/trace-map.ts ← plugins/analytics.ts
7
- *
8
- * The WeakMap maps Request → RequestTrace, allowing the handler to inject
9
- * trace data into context without importing the analytics plugin.
10
- */
11
- const analyticsTraceMap = /* @__PURE__ */ new WeakMap();
12
- //#endregion
13
- export { analyticsTraceMap };