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.
- package/README.md +47 -0
- package/dist/adapters/_fetch-adapter.d.mts +6 -0
- package/dist/adapters/_fetch-adapter.mjs +14 -8
- package/dist/adapters/astro.mjs +1 -1
- package/dist/adapters/nextjs.mjs +1 -1
- package/dist/adapters/remix.mjs +1 -1
- package/dist/adapters/solidstart.mjs +1 -1
- package/dist/adapters/sveltekit.mjs +1 -1
- package/dist/client/client.d.mts +42 -4
- package/dist/client/client.mjs +42 -4
- package/dist/client/server.d.mts +27 -2
- package/dist/client/server.mjs +27 -2
- package/dist/compile.d.mts +10 -1
- package/dist/compile.mjs +13 -4
- package/dist/core/context-bridge.d.mts +49 -0
- package/dist/core/context-bridge.mjs +43 -7
- package/dist/core/context.d.mts +26 -0
- package/dist/core/ctx-symbols.mjs +21 -0
- package/dist/core/error.d.mts +183 -2
- package/dist/core/error.mjs +259 -16
- package/dist/core/handler.d.mts +15 -1
- package/dist/core/handler.mjs +33 -17
- package/dist/core/schema-converter.d.mts +131 -0
- package/dist/core/schema-converter.mjs +82 -0
- package/dist/core/serve.d.mts +2 -2
- package/dist/core/serve.mjs +9 -2
- package/dist/core/task.mjs +2 -2
- package/dist/index.d.mts +5 -2
- package/dist/index.mjs +4 -2
- package/dist/integrations/better-auth/index.d.mts +22 -1
- package/dist/integrations/better-auth/index.mjs +79 -11
- package/dist/integrations/drizzle/index.mjs +22 -5
- package/dist/integrations/zod/converter.d.mts +1 -1
- package/dist/integrations/zod/index.d.mts +29 -2
- package/dist/integrations/zod/index.mjs +60 -1
- package/dist/lazy.d.mts +40 -3
- package/dist/lazy.mjs +40 -3
- package/dist/map-input.mjs +1 -1
- package/dist/plugins/analytics/collector.d.mts +1 -1
- package/dist/plugins/analytics/trace.mjs +1 -1
- package/dist/plugins/analytics/types.d.mts +3 -3
- package/dist/plugins/analytics/utils.mjs +1 -4
- package/dist/plugins/analytics.d.mts +5 -3
- package/dist/plugins/analytics.mjs +16 -29
- package/dist/plugins/cache.mjs +1 -1
- package/dist/plugins/coerce.mjs +1 -1
- package/dist/scalar.d.mts +2 -1
- package/dist/scalar.mjs +9 -30
- package/dist/silgi.d.mts +165 -18
- package/dist/silgi.mjs +47 -11
- package/package.json +6 -2
- package/dist/core/trace-map.d.mts +0 -13
- 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 ?? (() => ({})),
|
|
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
|
-
*
|
|
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
|
|
34
|
+
const eventStore = new AsyncLocalStorage();
|
|
28
35
|
const handler = wrapHandler(createFetchHandler(router, (_req) => {
|
|
29
|
-
const eventRef =
|
|
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
|
-
},
|
|
39
|
+
}, options.hooks, prefix), router, options, prefix);
|
|
33
40
|
return (event) => {
|
|
34
41
|
const request = extractRequest(event);
|
|
35
|
-
|
|
36
|
-
return handler(request);
|
|
42
|
+
return eventStore.run(event, () => handler(request));
|
|
37
43
|
};
|
|
38
44
|
}
|
|
39
45
|
//#endregion
|
package/dist/adapters/astro.mjs
CHANGED
|
@@ -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:
|
|
14
|
+
* analytics: { auth: "your-secret-token" },
|
|
15
15
|
* })
|
|
16
16
|
*
|
|
17
17
|
* export const GET = handler
|
package/dist/adapters/nextjs.mjs
CHANGED
|
@@ -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:
|
|
14
|
+
* analytics: { auth: "your-secret-token" },
|
|
15
15
|
* })
|
|
16
16
|
*
|
|
17
17
|
* export { handler as GET, handler as POST }
|
package/dist/adapters/remix.mjs
CHANGED
|
@@ -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:
|
|
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:
|
|
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:
|
|
14
|
+
* analytics: { auth: "your-secret-token" },
|
|
15
15
|
* })
|
|
16
16
|
*
|
|
17
17
|
* export const GET = handler
|
package/dist/client/client.d.mts
CHANGED
|
@@ -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
|
-
*
|
|
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 {
|
|
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>;
|
package/dist/client/client.mjs
CHANGED
|
@@ -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
|
-
*
|
|
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 {
|
|
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) {
|
package/dist/client/server.d.mts
CHANGED
|
@@ -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
|
-
*
|
|
12
|
-
*
|
|
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
|
package/dist/client/server.mjs
CHANGED
|
@@ -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
|
-
*
|
|
25
|
-
*
|
|
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, []);
|
package/dist/compile.d.mts
CHANGED
|
@@ -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():
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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 {
|
|
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 };
|