silgi 0.1.0-beta.1 → 0.1.0-beta.10

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 (152) hide show
  1. package/dist/adapters/_fetch-adapter.d.mts +18 -0
  2. package/dist/adapters/_fetch-adapter.mjs +53 -0
  3. package/dist/adapters/astro.d.mts +6 -8
  4. package/dist/adapters/astro.mjs +25 -18
  5. package/dist/adapters/aws-lambda.d.mts +16 -5
  6. package/dist/adapters/aws-lambda.mjs +44 -37
  7. package/dist/adapters/express.d.mts +2 -2
  8. package/dist/adapters/express.mjs +70 -28
  9. package/dist/adapters/message-port.d.mts +8 -3
  10. package/dist/adapters/message-port.mjs +28 -25
  11. package/dist/adapters/nestjs.d.mts +3 -3
  12. package/dist/adapters/nestjs.mjs +11 -27
  13. package/dist/adapters/nextjs.d.mts +4 -11
  14. package/dist/adapters/nextjs.mjs +20 -21
  15. package/dist/adapters/peer.mjs +2 -2
  16. package/dist/adapters/remix.d.mts +6 -8
  17. package/dist/adapters/remix.mjs +24 -18
  18. package/dist/adapters/solidstart.d.mts +4 -6
  19. package/dist/adapters/solidstart.mjs +22 -23
  20. package/dist/adapters/sveltekit.d.mts +4 -8
  21. package/dist/adapters/sveltekit.mjs +21 -24
  22. package/dist/broker/index.d.mts +2 -2
  23. package/dist/broker/index.mjs +5 -5
  24. package/dist/builder.d.mts +24 -5
  25. package/dist/builder.mjs +22 -3
  26. package/dist/callable.d.mts +2 -0
  27. package/dist/callable.mjs +4 -4
  28. package/dist/caller.mjs +90 -0
  29. package/dist/client/adapters/fetch/index.d.mts +2 -0
  30. package/dist/client/adapters/fetch/index.mjs +26 -5
  31. package/dist/client/adapters/ofetch/index.d.mts +21 -5
  32. package/dist/client/adapters/ofetch/index.mjs +17 -9
  33. package/dist/client/adapters/websocket/index.d.mts +20 -0
  34. package/dist/client/adapters/websocket/index.mjs +101 -0
  35. package/dist/client/client.d.mts +16 -8
  36. package/dist/client/client.mjs +34 -8
  37. package/dist/client/consume.d.mts +50 -0
  38. package/dist/client/consume.mjs +66 -0
  39. package/dist/client/dynamic-link.d.mts +2 -1
  40. package/dist/client/dynamic-link.mjs +4 -1
  41. package/dist/client/index.d.mts +2 -3
  42. package/dist/client/index.mjs +2 -3
  43. package/dist/client/plugins/batch.d.mts +6 -0
  44. package/dist/client/plugins/batch.mjs +2 -2
  45. package/dist/client/plugins/circuit-breaker.d.mts +24 -0
  46. package/dist/client/plugins/circuit-breaker.mjs +60 -0
  47. package/dist/client/plugins/index.d.mts +4 -1
  48. package/dist/client/plugins/index.mjs +4 -1
  49. package/dist/client/plugins/otel.d.mts +12 -0
  50. package/dist/client/plugins/otel.mjs +27 -0
  51. package/dist/client/plugins/retry.d.mts +25 -2
  52. package/dist/client/plugins/retry.mjs +66 -8
  53. package/dist/client/plugins/timeout.d.mts +10 -0
  54. package/dist/client/plugins/timeout.mjs +14 -0
  55. package/dist/client/server.mjs +8 -6
  56. package/dist/codec/devalue.d.mts +2 -2
  57. package/dist/codec/devalue.mjs +4 -3
  58. package/dist/codec/msgpack.d.mts +3 -6
  59. package/dist/codec/msgpack.mjs +4 -18
  60. package/dist/codec/sanitize.mjs +38 -0
  61. package/dist/compile.d.mts +14 -20
  62. package/dist/compile.mjs +125 -97
  63. package/dist/core/codec.mjs +67 -0
  64. package/dist/core/dispatch.mjs +62 -0
  65. package/dist/core/handler.d.mts +6 -0
  66. package/dist/core/handler.mjs +97 -495
  67. package/dist/core/input.mjs +49 -0
  68. package/dist/core/router-utils.d.mts +25 -0
  69. package/dist/core/router-utils.mjs +86 -4
  70. package/dist/core/schema.d.mts +2 -1
  71. package/dist/core/schema.mjs +11 -4
  72. package/dist/core/serve.d.mts +51 -0
  73. package/dist/core/serve.mjs +48 -10
  74. package/dist/core/sse.d.mts +5 -3
  75. package/dist/core/sse.mjs +21 -18
  76. package/dist/core/storage.d.mts +5 -6
  77. package/dist/core/storage.mjs +30 -32
  78. package/dist/core/task.d.mts +62 -0
  79. package/dist/core/task.mjs +165 -0
  80. package/dist/core/utils.mjs +3 -0
  81. package/dist/index.d.mts +7 -4
  82. package/dist/index.mjs +7 -5
  83. package/dist/integrations/ai/index.mjs +4 -3
  84. package/dist/integrations/better-auth/index.d.mts +61 -0
  85. package/dist/integrations/better-auth/index.mjs +332 -0
  86. package/dist/integrations/drizzle/index.d.mts +27 -0
  87. package/dist/integrations/drizzle/index.mjs +286 -0
  88. package/dist/integrations/hey-api/index.d.mts +2 -0
  89. package/dist/integrations/hey-api/index.mjs +2 -0
  90. package/dist/integrations/hey-api/to-client.d.mts +20 -0
  91. package/dist/integrations/hey-api/to-client.mjs +39 -0
  92. package/dist/integrations/pinia-colada/general-utils.d.mts +13 -0
  93. package/dist/integrations/pinia-colada/general-utils.mjs +9 -0
  94. package/dist/integrations/pinia-colada/index.d.mts +6 -0
  95. package/dist/integrations/pinia-colada/index.mjs +5 -0
  96. package/dist/integrations/pinia-colada/key.d.mts +11 -0
  97. package/dist/integrations/pinia-colada/key.mjs +11 -0
  98. package/dist/integrations/pinia-colada/procedure-utils.d.mts +25 -0
  99. package/dist/integrations/pinia-colada/procedure-utils.mjs +33 -0
  100. package/dist/integrations/pinia-colada/router-utils.d.mts +17 -0
  101. package/dist/integrations/pinia-colada/router-utils.mjs +30 -0
  102. package/dist/integrations/pinia-colada/types.d.mts +25 -0
  103. package/dist/integrations/react/index.mjs +2 -3
  104. package/dist/integrations/tanstack-query/ssr.d.mts +10 -1
  105. package/dist/integrations/tanstack-query/ssr.mjs +15 -2
  106. package/dist/lazy.d.mts +0 -2
  107. package/dist/lazy.mjs +14 -7
  108. package/dist/map-input.mjs +25 -2
  109. package/dist/plugins/analytics.d.mts +72 -4
  110. package/dist/plugins/analytics.mjs +387 -30
  111. package/dist/plugins/batch-server.mjs +6 -1
  112. package/dist/plugins/body-limit.d.mts +4 -1
  113. package/dist/plugins/body-limit.mjs +8 -3
  114. package/dist/plugins/cache.mjs +18 -6
  115. package/dist/plugins/coerce.d.mts +5 -2
  116. package/dist/plugins/coerce.mjs +29 -5
  117. package/dist/plugins/cookies.d.mts +8 -38
  118. package/dist/plugins/cookies.mjs +21 -40
  119. package/dist/plugins/cors.d.mts +8 -4
  120. package/dist/plugins/cors.mjs +12 -6
  121. package/dist/plugins/file-upload.mjs +11 -9
  122. package/dist/plugins/index.d.mts +2 -4
  123. package/dist/plugins/index.mjs +1 -3
  124. package/dist/plugins/ratelimit.d.mts +2 -0
  125. package/dist/plugins/ratelimit.mjs +11 -0
  126. package/dist/plugins/signing.mjs +4 -1
  127. package/dist/scalar.d.mts +0 -4
  128. package/dist/scalar.mjs +189 -193
  129. package/dist/silgi.d.mts +19 -14
  130. package/dist/silgi.mjs +57 -14
  131. package/dist/types.d.mts +29 -3
  132. package/dist/ws.d.mts +54 -8
  133. package/dist/ws.mjs +86 -18
  134. package/lib/dashboard/index.html +53 -53
  135. package/package.json +63 -30
  136. package/dist/adapters/elysia.d.mts +0 -17
  137. package/dist/adapters/elysia.mjs +0 -76
  138. package/dist/adapters/fastify.d.mts +0 -15
  139. package/dist/adapters/fastify.mjs +0 -78
  140. package/dist/analyze.mjs +0 -26
  141. package/dist/client/merge.d.mts +0 -28
  142. package/dist/client/merge.mjs +0 -30
  143. package/dist/fast-stringify.mjs +0 -125
  144. package/dist/plugins/compression.d.mts +0 -19
  145. package/dist/plugins/compression.mjs +0 -23
  146. package/dist/plugins/custom-serializer.d.mts +0 -57
  147. package/dist/plugins/custom-serializer.mjs +0 -40
  148. package/dist/route/add.mjs +0 -240
  149. package/dist/route/compiler.mjs +0 -373
  150. package/dist/route/context.mjs +0 -12
  151. package/dist/route/types.d.mts +0 -11
  152. package/dist/route/utils.mjs +0 -17
@@ -0,0 +1,18 @@
1
+ //#region src/adapters/_fetch-adapter.d.ts
2
+ interface FetchAdapterConfig<TCtx extends Record<string, unknown>> {
3
+ /** Route prefix to strip. Default: "/api/rpc" */
4
+ prefix?: string;
5
+ /** Context factory — receives the Request (or framework event via eventMap). */
6
+ context?: (req: Request) => TCtx | Promise<TCtx>;
7
+ }
8
+ /**
9
+ * For adapters where the context factory needs access to a framework event
10
+ * (SvelteKit RequestEvent, SolidStart event), use this extended config.
11
+ */
12
+ interface FetchAdapterConfigWithEvent<TCtx extends Record<string, unknown>, TEvent = any> {
13
+ prefix?: string;
14
+ /** Context factory — receives the framework event, not raw Request. */
15
+ context?: (event: TEvent) => TCtx | Promise<TCtx>;
16
+ }
17
+ //#endregion
18
+ export { FetchAdapterConfig, FetchAdapterConfigWithEvent };
@@ -0,0 +1,53 @@
1
+ import { createFetchHandler } from "../core/handler.mjs";
2
+ //#region src/adapters/_fetch-adapter.ts
3
+ /**
4
+ * Shared factory for fetch-passthrough adapters.
5
+ *
6
+ * Next.js, Astro, Remix, SvelteKit, and SolidStart all do the same thing:
7
+ * strip URL prefix → rewrite request → dispatch to fetch handler.
8
+ *
9
+ * This module eliminates the duplication. Each adapter file becomes a thin
10
+ * wrapper that extracts the framework-specific Request and calls this factory.
11
+ */
12
+ /** Strip prefix from request URL and create a rewritten Request. */
13
+ function rewriteRequest(request, prefix) {
14
+ const url = new URL(request.url);
15
+ let pathname = url.pathname;
16
+ if (pathname.startsWith(prefix)) {
17
+ pathname = pathname.slice(prefix.length);
18
+ if (!pathname.startsWith("/")) pathname = "/" + pathname;
19
+ }
20
+ return new Request(new URL(pathname + url.search, url.origin), request);
21
+ }
22
+ /**
23
+ * Create a fetch-passthrough adapter that strips a prefix and delegates to handler.
24
+ * Used by adapters that receive a standard Request (Next.js, Astro, Remix).
25
+ */
26
+ function createFetchAdapter(router, options, defaultPrefix) {
27
+ const prefix = options.prefix ?? defaultPrefix;
28
+ const handler = createFetchHandler(router, options.context ?? (() => ({})));
29
+ return (request) => {
30
+ return handler(rewriteRequest(request, prefix));
31
+ };
32
+ }
33
+ /**
34
+ * Create a fetch-passthrough adapter for frameworks that pass an event object
35
+ * with a `.request` property (SvelteKit, SolidStart).
36
+ * Uses a WeakMap to safely pass the event into the context factory per-request.
37
+ */
38
+ function createEventFetchAdapter(router, options, defaultPrefix, extractRequest) {
39
+ const prefix = options.prefix ?? defaultPrefix;
40
+ const requestEventMap = /* @__PURE__ */ new WeakMap();
41
+ const handler = createFetchHandler(router, (_req) => {
42
+ const eventRef = requestEventMap.get(_req);
43
+ if (options.context && eventRef) return options.context(eventRef);
44
+ return {};
45
+ });
46
+ return (event) => {
47
+ const rewritten = rewriteRequest(extractRequest(event), prefix);
48
+ requestEventMap.set(rewritten, event);
49
+ return handler(rewritten);
50
+ };
51
+ }
52
+ //#endregion
53
+ export { createEventFetchAdapter, createFetchAdapter };
@@ -1,17 +1,15 @@
1
1
  import { RouterDef } from "../types.mjs";
2
+ import { FetchAdapterConfig } from "./_fetch-adapter.mjs";
2
3
 
3
4
  //#region src/adapters/astro.d.ts
4
- interface AstroAdapterOptions<TCtx extends Record<string, unknown>> {
5
- context?: (request: Request) => TCtx | Promise<TCtx>;
6
- prefix?: string;
7
- }
5
+ interface AstroAdapterOptions<TCtx extends Record<string, unknown>> extends FetchAdapterConfig<TCtx> {}
8
6
  /**
9
7
  * Create an Astro API route handler.
10
- * Astro passes { request: Request, params } — uses Silgi's handler().
8
+ * Astro passes { request, params } — we extract request and delegate.
11
9
  */
12
- declare function silgiAstro<TCtx extends Record<string, unknown>>(router: RouterDef, options?: AstroAdapterOptions<TCtx>): (ctx: {
10
+ declare function createHandler<TCtx extends Record<string, unknown>>(router: RouterDef, options?: AstroAdapterOptions<TCtx>): (ctx: {
13
11
  request: Request;
14
12
  params: Record<string, string>;
15
- }) => Promise<Response>;
13
+ }) => Response | Promise<Response>;
16
14
  //#endregion
17
- export { AstroAdapterOptions, silgiAstro };
15
+ export { AstroAdapterOptions, createHandler };
@@ -1,24 +1,31 @@
1
+ import { createFetchAdapter } from "./_fetch-adapter.mjs";
1
2
  //#region src/adapters/astro.ts
2
3
  /**
4
+ * Astro adapter — use Silgi with Astro API routes.
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * // src/pages/api/rpc/[...path].ts
9
+ * import { createHandler } from "silgi/astro"
10
+ * import { appRouter } from "~/server/rpc"
11
+ *
12
+ * const handler = createHandler(appRouter, {
13
+ * prefix: "/api/rpc",
14
+ * context: (req) => ({ db: getDB() }),
15
+ * })
16
+ *
17
+ * export const GET = handler
18
+ * export const POST = handler
19
+ * export const ALL = handler
20
+ * ```
21
+ */
22
+ /**
3
23
  * Create an Astro API route handler.
4
- * Astro passes { request: Request, params } — uses Silgi's handler().
24
+ * Astro passes { request, params } — we extract request and delegate.
5
25
  */
6
- function silgiAstro(router, options = {}) {
7
- const prefix = options.prefix ?? "/api/rpc";
8
- let _handler = null;
9
- return async ({ request }) => {
10
- if (!_handler) {
11
- const { silgi } = await import("../silgi.mjs");
12
- _handler = silgi({ context: options.context ?? (() => ({})) }).handler(router);
13
- }
14
- const url = new URL(request.url);
15
- let pathname = url.pathname;
16
- if (pathname.startsWith(prefix)) {
17
- pathname = pathname.slice(prefix.length);
18
- if (!pathname.startsWith("/")) pathname = "/" + pathname;
19
- }
20
- return _handler(new Request(new URL(pathname + url.search, url.origin), request));
21
- };
26
+ function createHandler(router, options = {}) {
27
+ const handler = createFetchAdapter(router, options, "/api/rpc");
28
+ return ({ request }) => handler(request);
22
29
  }
23
30
  //#endregion
24
- export { silgiAstro };
31
+ export { createHandler };
@@ -8,14 +8,25 @@ interface LambdaAdapterOptions<TCtx extends Record<string, unknown>> {
8
8
  prefix?: string;
9
9
  }
10
10
  interface LambdaEvent {
11
- httpMethod: string;
12
- path: string;
11
+ httpMethod?: string;
12
+ path?: string;
13
+ version?: string;
14
+ rawPath?: string;
15
+ requestContext?: {
16
+ http?: {
17
+ method: string;
18
+ path: string;
19
+ };
20
+ [key: string]: unknown;
21
+ };
13
22
  body: string | null;
14
23
  headers: Record<string, string>;
15
24
  queryStringParameters: Record<string, string> | null;
16
- requestContext?: Record<string, unknown>;
17
25
  isBase64Encoded?: boolean;
18
26
  }
27
+ interface LambdaContext {
28
+ getRemainingTimeInMillis?: () => number;
29
+ }
19
30
  interface LambdaResponse {
20
31
  statusCode: number;
21
32
  headers: Record<string, string>;
@@ -26,6 +37,6 @@ interface LambdaResponse {
26
37
  *
27
38
  * Supports API Gateway v1 (REST) and v2 (HTTP) event formats.
28
39
  */
29
- declare function silgiLambda<TCtx extends Record<string, unknown>>(router: RouterDef, options?: LambdaAdapterOptions<TCtx>): (event: LambdaEvent) => Promise<LambdaResponse>;
40
+ declare function createHandler<TCtx extends Record<string, unknown>>(router: RouterDef, options?: LambdaAdapterOptions<TCtx>): (event: LambdaEvent, context?: LambdaContext) => Promise<LambdaResponse>;
30
41
  //#endregion
31
- export { LambdaAdapterOptions, silgiLambda };
42
+ export { LambdaAdapterOptions, createHandler };
@@ -1,15 +1,14 @@
1
- import { SilgiError, toSilgiError } from "../core/error.mjs";
2
- import { ValidationError } from "../core/schema.mjs";
3
1
  import { compileRouter } from "../compile.mjs";
2
+ import { buildContext, isMethodAllowed, parseQueryData, serializeError } from "../core/dispatch.mjs";
4
3
  //#region src/adapters/aws-lambda.ts
5
4
  /**
6
5
  * AWS Lambda adapter — deploy Silgi as a Lambda function.
7
6
  *
8
7
  * @example
9
8
  * ```ts
10
- * import { silgiLambda } from "silgi/aws-lambda"
9
+ * import { createHandler } from "silgi/aws-lambda"
11
10
  *
12
- * export const handler = silgiLambda(appRouter, {
11
+ * export const handler = createHandler(appRouter, {
13
12
  * context: (event) => ({ db: getDB(), userId: event.requestContext?.authorizer?.userId }),
14
13
  * })
15
14
  * ```
@@ -19,17 +18,20 @@ import { compileRouter } from "../compile.mjs";
19
18
  *
20
19
  * Supports API Gateway v1 (REST) and v2 (HTTP) event formats.
21
20
  */
22
- function silgiLambda(router, options = {}) {
21
+ function createHandler(router, options = {}) {
23
22
  const flatRouter = compileRouter(router);
24
23
  const prefix = options.prefix ?? "";
25
- return async (event) => {
26
- let pathname = event.path;
24
+ const JSON_HDR = { "content-type": "application/json" };
25
+ return async (event, context) => {
26
+ const isV2 = event.version === "2.0";
27
+ const method = isV2 ? event.requestContext?.http?.method ?? "GET" : event.httpMethod ?? "GET";
28
+ let pathname = isV2 ? event.rawPath ?? "/" : event.path ?? "/";
27
29
  if (prefix && pathname.startsWith(prefix)) pathname = pathname.slice(prefix.length);
28
30
  if (pathname.startsWith("/")) pathname = pathname.slice(1);
29
- const match = flatRouter(event.httpMethod, "/" + pathname);
31
+ const match = flatRouter(method, "/" + pathname);
30
32
  if (!match) return {
31
33
  statusCode: 404,
32
- headers: { "content-type": "application/json" },
34
+ headers: JSON_HDR,
33
35
  body: JSON.stringify({
34
36
  code: "NOT_FOUND",
35
37
  status: 404,
@@ -37,49 +39,54 @@ function silgiLambda(router, options = {}) {
37
39
  })
38
40
  };
39
41
  const route = match.data;
42
+ if (!isMethodAllowed(method, route.method)) return {
43
+ statusCode: 405,
44
+ headers: {
45
+ ...JSON_HDR,
46
+ allow: route.method
47
+ },
48
+ body: JSON.stringify({
49
+ code: "METHOD_NOT_ALLOWED",
50
+ status: 405,
51
+ message: `Method ${method} not allowed`
52
+ })
53
+ };
40
54
  try {
41
- const ctx = Object.create(null);
42
- if (match.params) ctx.params = match.params;
43
- if (options.context) {
44
- const baseCtx = await options.context(event);
45
- const keys = Object.keys(baseCtx);
46
- for (let i = 0; i < keys.length; i++) ctx[keys[i]] = baseCtx[keys[i]];
47
- }
55
+ const ctx = buildContext(options.context ? await options.context(event) : void 0, match.params);
48
56
  let input;
49
57
  if (event.body) {
50
58
  const body = event.isBase64Encoded ? Buffer.from(event.body, "base64").toString("utf-8") : event.body;
51
59
  try {
52
60
  input = JSON.parse(body);
53
- } catch {}
54
- } else if (event.queryStringParameters?.data) try {
55
- input = JSON.parse(event.queryStringParameters.data);
56
- } catch {}
57
- const signal = AbortSignal.timeout(3e4);
61
+ } catch {
62
+ return {
63
+ statusCode: 400,
64
+ headers: JSON_HDR,
65
+ body: JSON.stringify({
66
+ code: "BAD_REQUEST",
67
+ status: 400,
68
+ message: "Invalid JSON body"
69
+ })
70
+ };
71
+ }
72
+ } else if (event.queryStringParameters?.data) input = parseQueryData(event.queryStringParameters.data);
73
+ const timeoutMs = context?.getRemainingTimeInMillis ? context.getRemainingTimeInMillis() - 500 : 3e4;
74
+ const signal = AbortSignal.timeout(Math.max(timeoutMs, 1e3));
58
75
  const output = await route.handler(ctx, input, signal);
59
76
  return {
60
77
  statusCode: 200,
61
- headers: { "content-type": "application/json" },
78
+ headers: JSON_HDR,
62
79
  body: JSON.stringify(output)
63
80
  };
64
81
  } catch (error) {
65
- if (error instanceof ValidationError) return {
66
- statusCode: 400,
67
- headers: { "content-type": "application/json" },
68
- body: JSON.stringify({
69
- code: "BAD_REQUEST",
70
- status: 400,
71
- message: error.message,
72
- data: { issues: error.issues }
73
- })
74
- };
75
- const e = error instanceof SilgiError ? error : toSilgiError(error);
82
+ const body = serializeError(error);
76
83
  return {
77
- statusCode: e.status,
78
- headers: { "content-type": "application/json" },
79
- body: JSON.stringify(e.toJSON())
84
+ statusCode: body.status,
85
+ headers: JSON_HDR,
86
+ body: JSON.stringify(body)
80
87
  };
81
88
  }
82
89
  };
83
90
  }
84
91
  //#endregion
85
- export { silgiLambda };
92
+ export { createHandler };
@@ -11,6 +11,6 @@ interface ExpressAdapterOptions<TCtx extends Record<string, unknown>> {
11
11
  * Mount at a prefix — the remainder of the path is the procedure name.
12
12
  * Requires `express.json()` middleware for POST body parsing.
13
13
  */
14
- declare function silgiExpress<TCtx extends Record<string, unknown>>(router: RouterDef, options?: ExpressAdapterOptions<TCtx>): (req: any, res: any, next: any) => void;
14
+ declare function createHandler<TCtx extends Record<string, unknown>>(router: RouterDef, options?: ExpressAdapterOptions<TCtx>): (req: any, res: any, next: any) => void;
15
15
  //#endregion
16
- export { ExpressAdapterOptions, silgiExpress };
16
+ export { ExpressAdapterOptions, createHandler };
@@ -1,6 +1,6 @@
1
- import { SilgiError, toSilgiError } from "../core/error.mjs";
2
- import { ValidationError } from "../core/schema.mjs";
3
1
  import { compileRouter } from "../compile.mjs";
2
+ import { buildContext, isMethodAllowed, serializeError } from "../core/dispatch.mjs";
3
+ import { iteratorToEventStream } from "../core/sse.mjs";
4
4
  //#region src/adapters/express.ts
5
5
  /**
6
6
  * Express adapter — use Silgi as Express middleware.
@@ -8,10 +8,10 @@ import { compileRouter } from "../compile.mjs";
8
8
  * @example
9
9
  * ```ts
10
10
  * import express from "express"
11
- * import { silgiExpress } from "silgi/express"
11
+ * import { createHandler } from "silgi/express"
12
12
  *
13
13
  * const app = express()
14
- * app.use("/rpc", silgiExpress(appRouter, {
14
+ * app.use("/rpc", createHandler(appRouter, {
15
15
  * context: (req) => ({ db: getDB(), user: req.user }),
16
16
  * }))
17
17
  * app.listen(3000)
@@ -23,7 +23,7 @@ import { compileRouter } from "../compile.mjs";
23
23
  * Mount at a prefix — the remainder of the path is the procedure name.
24
24
  * Requires `express.json()` middleware for POST body parsing.
25
25
  */
26
- function silgiExpress(router, options = {}) {
26
+ function createHandler(router, options = {}) {
27
27
  const flatRouter = compileRouter(router);
28
28
  return (req, res, next) => {
29
29
  let pathname = req.path ?? req.url ?? "";
@@ -31,15 +31,17 @@ function silgiExpress(router, options = {}) {
31
31
  const match = flatRouter(req.method, "/" + pathname);
32
32
  if (!match) return next();
33
33
  const route = match.data;
34
+ if (!isMethodAllowed(req.method, route.method)) {
35
+ res.status(405).set("allow", route.method).json({
36
+ code: "METHOD_NOT_ALLOWED",
37
+ status: 405,
38
+ message: `Method ${req.method} not allowed`
39
+ });
40
+ return;
41
+ }
34
42
  const handle = async () => {
35
43
  try {
36
- const ctx = Object.create(null);
37
- if (match.params) ctx.params = match.params;
38
- if (options.context) {
39
- const baseCtx = await options.context(req);
40
- const keys = Object.keys(baseCtx);
41
- for (let i = 0; i < keys.length; i++) ctx[keys[i]] = baseCtx[keys[i]];
42
- }
44
+ const ctx = buildContext(options.context ? await options.context(req) : void 0, match.params);
43
45
  let input;
44
46
  if (req.method === "POST" || req.method === "PUT" || req.method === "PATCH") input = req.body;
45
47
  else if (req.query?.data) if (typeof req.query.data === "string") try {
@@ -54,25 +56,65 @@ function silgiExpress(router, options = {}) {
54
56
  }
55
57
  else input = req.query.data;
56
58
  const ac = new AbortController();
57
- req.on("close", () => ac.abort());
58
- const output = await route.handler(ctx, input, ac.signal);
59
- res.json(output);
60
- } catch (error) {
61
- if (error instanceof ValidationError) {
62
- res.status(400).json({
63
- code: "BAD_REQUEST",
64
- status: 400,
65
- message: error.message,
66
- data: { issues: error.issues }
67
- });
68
- return;
59
+ const onClose = () => ac.abort();
60
+ req.on("close", onClose);
61
+ try {
62
+ const output = await route.handler(ctx, input, ac.signal);
63
+ if (output instanceof Response) {
64
+ res.status(output.status);
65
+ output.headers.forEach((v, k) => res.setHeader(k, v));
66
+ const body = output.body ? Buffer.from(await output.arrayBuffer()) : "";
67
+ res.end(body);
68
+ } else if (output instanceof ReadableStream) {
69
+ res.setHeader("content-type", "application/octet-stream");
70
+ const reader = output.getReader();
71
+ req.on("close", () => reader.cancel());
72
+ try {
73
+ while (true) {
74
+ const { done, value } = await reader.read();
75
+ if (done) {
76
+ res.end();
77
+ break;
78
+ }
79
+ res.write(value);
80
+ }
81
+ } finally {
82
+ reader.releaseLock();
83
+ }
84
+ } else if (output && typeof output === "object" && Symbol.asyncIterator in output) {
85
+ const stream = iteratorToEventStream(output);
86
+ res.setHeader("content-type", "text/event-stream");
87
+ res.setHeader("cache-control", "no-cache");
88
+ const reader = stream.getReader();
89
+ req.on("close", () => reader.cancel());
90
+ try {
91
+ while (true) {
92
+ const { done, value } = await reader.read();
93
+ if (done) {
94
+ res.end();
95
+ break;
96
+ }
97
+ res.write(value);
98
+ }
99
+ } finally {
100
+ reader.releaseLock();
101
+ }
102
+ } else res.json(output);
103
+ } finally {
104
+ req.removeListener("close", onClose);
69
105
  }
70
- const e = error instanceof SilgiError ? error : toSilgiError(error);
71
- res.status(e.status).json(e.toJSON());
106
+ } catch (error) {
107
+ const body = serializeError(error);
108
+ res.status(body.status).json(body);
72
109
  }
73
110
  };
74
- handle();
111
+ handle().catch((error) => {
112
+ if (!res.headersSent) {
113
+ const body = serializeError(error);
114
+ res.status(body.status).json(body);
115
+ }
116
+ });
75
117
  };
76
118
  }
77
119
  //#endregion
78
- export { silgiExpress };
120
+ export { createHandler };
@@ -10,7 +10,7 @@ interface MessagePortAdapterOptions<TCtx extends Record<string, unknown>> {
10
10
  * Listens for RPC messages and responds with results.
11
11
  * Returns a dispose function to stop listening.
12
12
  */
13
- declare function silgiMessagePort<TCtx extends Record<string, unknown>>(router: RouterDef, port: {
13
+ declare function createHandler<TCtx extends Record<string, unknown>>(router: RouterDef, port: {
14
14
  postMessage(msg: unknown): void;
15
15
  addEventListener(type: 'message', handler: (event: {
16
16
  data: unknown;
@@ -30,8 +30,13 @@ declare class MessagePortLink<TCtx extends ClientContext = ClientContext> implem
30
30
  addEventListener(type: 'message', handler: (event: {
31
31
  data: unknown;
32
32
  }) => void): void;
33
+ removeEventListener?(type: 'message', handler: (event: {
34
+ data: unknown;
35
+ }) => void): void;
33
36
  });
34
- call(path: readonly string[], input: unknown, _options: ClientOptions<TCtx>): Promise<unknown>;
37
+ call(path: readonly string[], input: unknown, options: ClientOptions<TCtx>): Promise<unknown>;
38
+ /** Reject all pending calls and stop listening. */
39
+ dispose(): void;
35
40
  }
36
41
  //#endregion
37
- export { MessagePortAdapterOptions, MessagePortLink, silgiMessagePort };
42
+ export { MessagePortAdapterOptions, MessagePortLink, createHandler };
@@ -1,6 +1,6 @@
1
- import { SilgiError, toSilgiError } from "../core/error.mjs";
2
- import { ValidationError } from "../core/schema.mjs";
1
+ import { SilgiError } from "../core/error.mjs";
3
2
  import { compileRouter } from "../compile.mjs";
3
+ import { buildContext, serializeError } from "../core/dispatch.mjs";
4
4
  //#region src/adapters/message-port.ts
5
5
  /**
6
6
  * Message Port adapter — use Silgi over MessagePort/MessageChannel.
@@ -11,9 +11,9 @@ import { compileRouter } from "../compile.mjs";
11
11
  * @example
12
12
  * ```ts
13
13
  * // Worker / Electron main
14
- * import { silgiMessagePort } from "silgi/message-port"
14
+ * import { createHandler } from "silgi/message-port"
15
15
  *
16
- * const dispose = silgiMessagePort(appRouter, port, {
16
+ * const dispose = createHandler(appRouter, port, {
17
17
  * context: () => ({ db: getDB() }),
18
18
  * })
19
19
  *
@@ -30,7 +30,7 @@ import { compileRouter } from "../compile.mjs";
30
30
  * Listens for RPC messages and responds with results.
31
31
  * Returns a dispose function to stop listening.
32
32
  */
33
- function silgiMessagePort(router, port, options = {}) {
33
+ function createHandler(router, port, options = {}) {
34
34
  const flatRouter = compileRouter(router);
35
35
  const handler = async (event) => {
36
36
  const msg = event.data;
@@ -51,15 +51,9 @@ function silgiMessagePort(router, port, options = {}) {
51
51
  }
52
52
  const route = match.data;
53
53
  try {
54
- const ctx = Object.create(null);
55
- if (match.params) ctx.params = match.params;
56
- if (options.context) {
57
- const baseCtx = await options.context();
58
- const keys = Object.keys(baseCtx);
59
- for (let i = 0; i < keys.length; i++) ctx[keys[i]] = baseCtx[keys[i]];
60
- }
61
- const signal = new AbortController().signal;
62
- const result = await route.handler(ctx, msg.input, signal);
54
+ const ctx = buildContext(options.context ? await options.context() : void 0, match.params);
55
+ const ac = new AbortController();
56
+ const result = await route.handler(ctx, msg.input, ac.signal);
63
57
  port.postMessage({
64
58
  __silgi: true,
65
59
  __type: "response",
@@ -67,17 +61,11 @@ function silgiMessagePort(router, port, options = {}) {
67
61
  result
68
62
  });
69
63
  } catch (error) {
70
- const e = error instanceof ValidationError ? {
71
- code: "BAD_REQUEST",
72
- status: 400,
73
- message: error.message,
74
- data: { issues: error.issues }
75
- } : error instanceof SilgiError ? error.toJSON() : toSilgiError(error).toJSON();
76
64
  port.postMessage({
77
65
  __silgi: true,
78
66
  __type: "response",
79
67
  id: msg.id,
80
- error: e
68
+ error: serializeError(error)
81
69
  });
82
70
  }
83
71
  };
@@ -92,9 +80,10 @@ var MessagePortLink = class {
92
80
  #port;
93
81
  #pending = /* @__PURE__ */ new Map();
94
82
  #nextId = 1;
83
+ #messageHandler;
95
84
  constructor(port) {
96
85
  this.#port = port;
97
- port.addEventListener("message", (event) => {
86
+ this.#messageHandler = (event) => {
98
87
  const msg = event.data;
99
88
  if (!msg || typeof msg !== "object" || !msg.__silgi || msg.__type !== "response") return;
100
89
  const pending = this.#pending.get(msg.id);
@@ -106,15 +95,23 @@ var MessagePortLink = class {
106
95
  data: msg.error.data
107
96
  }));
108
97
  else pending.resolve(msg.result);
109
- });
98
+ };
99
+ port.addEventListener("message", this.#messageHandler);
110
100
  }
111
- call(path, input, _options) {
101
+ call(path, input, options) {
112
102
  return new Promise((resolve, reject) => {
113
103
  const id = String(this.#nextId++);
114
104
  this.#pending.set(id, {
115
105
  resolve,
116
106
  reject
117
107
  });
108
+ if (options.signal) options.signal.addEventListener("abort", () => {
109
+ const pending = this.#pending.get(id);
110
+ if (pending) {
111
+ this.#pending.delete(id);
112
+ pending.reject(new DOMException("Aborted", "AbortError"));
113
+ }
114
+ }, { once: true });
118
115
  this.#port.postMessage({
119
116
  __silgi: true,
120
117
  __type: "request",
@@ -124,6 +121,12 @@ var MessagePortLink = class {
124
121
  });
125
122
  });
126
123
  }
124
+ /** Reject all pending calls and stop listening. */
125
+ dispose() {
126
+ for (const [, pending] of this.#pending) pending.reject(new DOMException("Link disposed", "AbortError"));
127
+ this.#pending.clear();
128
+ this.#port.removeEventListener?.("message", this.#messageHandler);
129
+ }
127
130
  };
128
131
  //#endregion
129
- export { MessagePortLink, silgiMessagePort };
132
+ export { MessagePortLink, createHandler };
@@ -11,15 +11,15 @@ interface NestAdapterOptions<TCtx extends Record<string, unknown>> {
11
11
  * Use inside a `@Controller` with `@All("*")`.
12
12
  * Handles routing internally — NestJS only needs to mount the prefix.
13
13
  */
14
- declare function silgiNestHandler<TCtx extends Record<string, unknown>>(router: RouterDef, options?: NestAdapterOptions<TCtx>): (req: any, res: any) => Promise<void>;
14
+ declare function createHandler<TCtx extends Record<string, unknown>>(router: RouterDef, options?: NestAdapterOptions<TCtx>): (req: any, res: any) => Promise<void>;
15
15
  /**
16
16
  * Create a NestJS module configuration for Silgi.
17
17
  *
18
18
  * Returns an object that can be used with NestJS's dynamic module pattern.
19
19
  */
20
- declare function createSilgiModule(router: RouterDef, options?: NestAdapterOptions<any>): {
20
+ declare function createModule(router: RouterDef, options?: NestAdapterOptions<any>): {
21
21
  handler: (req: any, res: any) => Promise<void>;
22
22
  router: RouterDef;
23
23
  };
24
24
  //#endregion
25
- export { NestAdapterOptions, createSilgiModule, silgiNestHandler };
25
+ export { NestAdapterOptions, createHandler, createModule };