silgi 0.1.0-beta.4 → 0.1.0-beta.6

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 (107) 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 +4 -6
  4. package/dist/adapters/astro.mjs +23 -16
  5. package/dist/adapters/aws-lambda.d.mts +15 -4
  6. package/dist/adapters/aws-lambda.mjs +40 -33
  7. package/dist/adapters/express.mjs +66 -24
  8. package/dist/adapters/message-port.d.mts +6 -1
  9. package/dist/adapters/message-port.mjs +24 -21
  10. package/dist/adapters/nestjs.mjs +5 -21
  11. package/dist/adapters/nextjs.d.mts +3 -10
  12. package/dist/adapters/nextjs.mjs +18 -19
  13. package/dist/adapters/remix.d.mts +4 -6
  14. package/dist/adapters/remix.mjs +22 -16
  15. package/dist/adapters/solidstart.d.mts +3 -5
  16. package/dist/adapters/solidstart.mjs +20 -21
  17. package/dist/adapters/sveltekit.d.mts +3 -7
  18. package/dist/adapters/sveltekit.mjs +19 -22
  19. package/dist/callable.d.mts +2 -0
  20. package/dist/callable.mjs +4 -4
  21. package/dist/caller.mjs +90 -0
  22. package/dist/client/adapters/fetch/index.mjs +14 -2
  23. package/dist/client/adapters/ofetch/index.d.mts +16 -2
  24. package/dist/client/adapters/ofetch/index.mjs +6 -7
  25. package/dist/client/client.mjs +1 -1
  26. package/dist/client/index.d.mts +1 -2
  27. package/dist/client/index.mjs +1 -2
  28. package/dist/client/plugins/batch.mjs +2 -2
  29. package/dist/client/plugins/circuit-breaker.d.mts +24 -0
  30. package/dist/client/plugins/circuit-breaker.mjs +60 -0
  31. package/dist/client/plugins/index.d.mts +3 -1
  32. package/dist/client/plugins/index.mjs +3 -1
  33. package/dist/client/plugins/retry.d.mts +23 -2
  34. package/dist/client/plugins/retry.mjs +51 -7
  35. package/dist/client/plugins/timeout.d.mts +10 -0
  36. package/dist/client/plugins/timeout.mjs +14 -0
  37. package/dist/codec/devalue.d.mts +2 -2
  38. package/dist/codec/devalue.mjs +4 -3
  39. package/dist/codec/msgpack.d.mts +3 -6
  40. package/dist/codec/msgpack.mjs +4 -18
  41. package/dist/codec/sanitize.mjs +38 -0
  42. package/dist/compile.d.mts +12 -20
  43. package/dist/compile.mjs +123 -96
  44. package/dist/core/codec.mjs +67 -0
  45. package/dist/core/dispatch.mjs +62 -0
  46. package/dist/core/handler.d.mts +6 -0
  47. package/dist/core/handler.mjs +94 -516
  48. package/dist/core/input.mjs +49 -0
  49. package/dist/core/router-utils.mjs +10 -4
  50. package/dist/core/schema.d.mts +2 -1
  51. package/dist/core/schema.mjs +11 -4
  52. package/dist/core/serve.d.mts +51 -0
  53. package/dist/core/serve.mjs +47 -9
  54. package/dist/core/sse.d.mts +5 -3
  55. package/dist/core/sse.mjs +21 -18
  56. package/dist/core/utils.mjs +3 -0
  57. package/dist/index.d.mts +5 -4
  58. package/dist/index.mjs +3 -3
  59. package/dist/integrations/ai/index.mjs +4 -3
  60. package/dist/integrations/react/index.mjs +2 -3
  61. package/dist/integrations/tanstack-query/ssr.d.mts +10 -1
  62. package/dist/integrations/tanstack-query/ssr.mjs +15 -2
  63. package/dist/lazy.d.mts +0 -2
  64. package/dist/lazy.mjs +14 -7
  65. package/dist/map-input.mjs +25 -2
  66. package/dist/plugins/analytics.d.mts +17 -1
  67. package/dist/plugins/analytics.mjs +195 -17
  68. package/dist/plugins/batch-server.mjs +5 -0
  69. package/dist/plugins/body-limit.d.mts +4 -1
  70. package/dist/plugins/body-limit.mjs +8 -3
  71. package/dist/plugins/cache.mjs +18 -6
  72. package/dist/plugins/coerce.d.mts +5 -2
  73. package/dist/plugins/coerce.mjs +29 -5
  74. package/dist/plugins/cookies.d.mts +8 -38
  75. package/dist/plugins/cookies.mjs +21 -40
  76. package/dist/plugins/cors.d.mts +8 -4
  77. package/dist/plugins/cors.mjs +12 -6
  78. package/dist/plugins/file-upload.mjs +11 -9
  79. package/dist/plugins/index.d.mts +1 -3
  80. package/dist/plugins/index.mjs +2 -4
  81. package/dist/plugins/ratelimit.d.mts +2 -0
  82. package/dist/plugins/ratelimit.mjs +11 -0
  83. package/dist/plugins/signing.mjs +4 -1
  84. package/dist/scalar.d.mts +0 -4
  85. package/dist/scalar.mjs +128 -147
  86. package/dist/silgi.d.mts +17 -14
  87. package/dist/silgi.mjs +34 -7
  88. package/dist/types.d.mts +24 -1
  89. package/dist/ws.d.mts +54 -8
  90. package/dist/ws.mjs +86 -18
  91. package/lib/dashboard/index.html +43 -43
  92. package/package.json +13 -14
  93. package/dist/adapters/fastify.d.mts +0 -15
  94. package/dist/adapters/fastify.mjs +0 -78
  95. package/dist/analyze.mjs +0 -26
  96. package/dist/client/merge.d.mts +0 -28
  97. package/dist/client/merge.mjs +0 -30
  98. package/dist/fast-stringify.mjs +0 -125
  99. package/dist/plugins/compression.d.mts +0 -19
  100. package/dist/plugins/compression.mjs +0 -23
  101. package/dist/plugins/custom-serializer.d.mts +0 -57
  102. package/dist/plugins/custom-serializer.mjs +0 -40
  103. package/dist/route/add.mjs +0 -240
  104. package/dist/route/compiler.mjs +0 -373
  105. package/dist/route/context.mjs +0 -12
  106. package/dist/route/types.d.mts +0 -11
  107. package/dist/route/utils.mjs +0 -17
@@ -1,24 +1,30 @@
1
+ import { createFetchAdapter } from "./_fetch-adapter.mjs";
1
2
  //#region src/adapters/remix.ts
2
3
  /**
4
+ * Remix adapter — use Silgi with Remix action/loader routes.
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * // app/routes/rpc.$.tsx
9
+ * import { silgiRemix } from "silgi/remix"
10
+ * import { appRouter } from "~/server/rpc"
11
+ *
12
+ * const handler = silgiRemix(appRouter, {
13
+ * prefix: "/rpc",
14
+ * context: (req) => ({ db: getDB() }),
15
+ * })
16
+ *
17
+ * export const action = handler
18
+ * export const loader = handler
19
+ * ```
20
+ */
21
+ /**
3
22
  * Create a Remix action/loader handler.
4
- * Uses Silgi's handler()full Fetch API + content negotiation.
23
+ * Remix passes { request, params } we extract request and delegate.
5
24
  */
6
25
  function silgiRemix(router, options = {}) {
7
- const prefix = options.prefix ?? "/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
+ const handler = createFetchAdapter(router, options, "/rpc");
27
+ return ({ request }) => handler(request);
22
28
  }
23
29
  //#endregion
24
30
  export { silgiRemix };
@@ -1,14 +1,12 @@
1
1
  import { RouterDef } from "../types.mjs";
2
+ import { FetchAdapterConfigWithEvent } from "./_fetch-adapter.mjs";
2
3
 
3
4
  //#region src/adapters/solidstart.d.ts
4
- interface SolidStartAdapterOptions<TCtx extends Record<string, unknown>> {
5
- context?: (event: any) => TCtx | Promise<TCtx>;
6
- prefix?: string;
7
- }
5
+ interface SolidStartAdapterOptions<TCtx extends Record<string, unknown>> extends FetchAdapterConfigWithEvent<TCtx> {}
8
6
  /**
9
7
  * Create a SolidStart API route handler.
10
8
  * SolidStart uses Fetch API events — uses Silgi's handler().
11
9
  */
12
- declare function silgiSolidStart<TCtx extends Record<string, unknown>>(router: RouterDef, options?: SolidStartAdapterOptions<TCtx>): (event: any) => Promise<Response>;
10
+ declare function silgiSolidStart<TCtx extends Record<string, unknown>>(router: RouterDef, options?: SolidStartAdapterOptions<TCtx>): (event: any) => Response | Promise<Response>;
13
11
  //#endregion
14
12
  export { SolidStartAdapterOptions, silgiSolidStart };
@@ -1,30 +1,29 @@
1
+ import { createEventFetchAdapter } from "./_fetch-adapter.mjs";
1
2
  //#region src/adapters/solidstart.ts
2
3
  /**
4
+ * SolidStart adapter — use Silgi with SolidStart API routes.
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * // src/routes/api/rpc/[...path].ts
9
+ * import { silgiSolidStart } from "silgi/solidstart"
10
+ * import { appRouter } from "~/server/rpc"
11
+ *
12
+ * const handler = silgiSolidStart(appRouter, {
13
+ * prefix: "/api/rpc",
14
+ * context: (event) => ({ db: getDB() }),
15
+ * })
16
+ *
17
+ * export const GET = handler
18
+ * export const POST = handler
19
+ * ```
20
+ */
21
+ /**
3
22
  * Create a SolidStart API route handler.
4
23
  * SolidStart uses Fetch API events — uses Silgi's handler().
5
24
  */
6
25
  function silgiSolidStart(router, options = {}) {
7
- const prefix = options.prefix ?? "/api/rpc";
8
- let _handler = null;
9
- let _currentEvent = null;
10
- return async (event) => {
11
- _currentEvent = event;
12
- if (!_handler) {
13
- const { silgi } = await import("../silgi.mjs");
14
- _handler = silgi({ context: (_req) => {
15
- if (options.context) return options.context(_currentEvent);
16
- return {};
17
- } }).handler(router);
18
- }
19
- const request = event.request ?? event;
20
- const url = new URL(request.url);
21
- let pathname = url.pathname;
22
- if (pathname.startsWith(prefix)) {
23
- pathname = pathname.slice(prefix.length);
24
- if (!pathname.startsWith("/")) pathname = "/" + pathname;
25
- }
26
- return _handler(new Request(new URL(pathname + url.search, url.origin), request));
27
- };
26
+ return createEventFetchAdapter(router, options, "/api/rpc", (event) => event.request ?? event);
28
27
  }
29
28
  //#endregion
30
29
  export { silgiSolidStart };
@@ -1,18 +1,14 @@
1
1
  import { RouterDef } from "../types.mjs";
2
+ import { FetchAdapterConfigWithEvent } from "./_fetch-adapter.mjs";
2
3
 
3
4
  //#region src/adapters/sveltekit.d.ts
4
- interface SvelteKitAdapterOptions<TCtx extends Record<string, unknown>> {
5
- /** Context factory — receives the SvelteKit RequestEvent */
6
- context?: (event: any) => TCtx | Promise<TCtx>;
7
- /** Route prefix to strip. Default: "/api/rpc" */
8
- prefix?: string;
9
- }
5
+ interface SvelteKitAdapterOptions<TCtx extends Record<string, unknown>> extends FetchAdapterConfigWithEvent<TCtx> {}
10
6
  /**
11
7
  * Create a SvelteKit request handler.
12
8
  *
13
9
  * SvelteKit passes a RequestEvent with `.request` (standard Request).
14
10
  * The handler uses Silgi's handler() for full protocol support.
15
11
  */
16
- declare function silgiSvelteKit<TCtx extends Record<string, unknown>>(router: RouterDef, options?: SvelteKitAdapterOptions<TCtx>): (event: any) => Promise<Response>;
12
+ declare function silgiSvelteKit<TCtx extends Record<string, unknown>>(router: RouterDef, options?: SvelteKitAdapterOptions<TCtx>): (event: any) => Response | Promise<Response>;
17
13
  //#endregion
18
14
  export { SvelteKitAdapterOptions, silgiSvelteKit };
@@ -1,33 +1,30 @@
1
+ import { createEventFetchAdapter } from "./_fetch-adapter.mjs";
1
2
  //#region src/adapters/sveltekit.ts
2
3
  /**
4
+ * SvelteKit adapter — use Silgi with SvelteKit API routes.
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * // src/routes/api/rpc/[...path]/+server.ts
9
+ * import { silgiSvelteKit } from "silgi/sveltekit"
10
+ * import { appRouter } from "$lib/server/rpc"
11
+ *
12
+ * const handler = silgiSvelteKit(appRouter, {
13
+ * context: (event) => ({ db: getDB(), user: event.locals.user }),
14
+ * })
15
+ *
16
+ * export const GET = handler
17
+ * export const POST = handler
18
+ * ```
19
+ */
20
+ /**
3
21
  * Create a SvelteKit request handler.
4
22
  *
5
23
  * SvelteKit passes a RequestEvent with `.request` (standard Request).
6
24
  * The handler uses Silgi's handler() for full protocol support.
7
25
  */
8
26
  function silgiSvelteKit(router, options = {}) {
9
- const prefix = options.prefix ?? "/api/rpc";
10
- let _handler = null;
11
- let _currentEvent = null;
12
- return async (event) => {
13
- _currentEvent = event;
14
- if (!_handler) {
15
- const { silgi } = await import("../silgi.mjs");
16
- _handler = silgi({ context: (_req) => {
17
- if (options.context) return options.context(_currentEvent);
18
- return {};
19
- } }).handler(router);
20
- }
21
- const req = event.request;
22
- const url = new URL(req.url);
23
- let pathname = url.pathname;
24
- if (pathname.startsWith(prefix)) {
25
- pathname = pathname.slice(prefix.length);
26
- if (!pathname.startsWith("/")) pathname = "/" + pathname;
27
- }
28
- const rewritten = new Request(new URL(pathname + url.search, url.origin), req);
29
- return _handler(rewritten);
30
- };
27
+ return createEventFetchAdapter(router, options, "/api/rpc", (event) => event.request);
31
28
  }
32
29
  //#endregion
33
30
  export { silgiSvelteKit };
@@ -4,6 +4,8 @@ import { ErrorDef, ProcedureDef } from "./types.mjs";
4
4
  interface CallableOptions<TCtx extends Record<string, unknown>> {
5
5
  /** Context factory — called on every invocation */
6
6
  context: () => TCtx | Promise<TCtx>;
7
+ /** Default timeout in ms. Default: 30000. Set null to disable. */
8
+ timeout?: number | null;
7
9
  }
8
10
  type CallableFn<TInput, TOutput> = undefined extends TInput ? (input?: TInput) => Promise<TOutput> : (input: TInput) => Promise<TOutput>;
9
11
  /**
package/dist/callable.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  import { compileProcedure } from "./compile.mjs";
2
+ import { applyContext } from "./core/dispatch.mjs";
2
3
  //#region src/callable.ts
3
4
  /**
4
5
  * callable() — turn a procedure into a directly invocable function.
@@ -29,13 +30,12 @@ import { compileProcedure } from "./compile.mjs";
29
30
  function callable(procedure, options) {
30
31
  const handler = compileProcedure(procedure);
31
32
  const contextFactory = options.context;
32
- const signal = new AbortController().signal;
33
+ const defaultTimeout = options.timeout !== void 0 ? options.timeout : 3e4;
33
34
  return (async (input) => {
34
35
  const ctx = await contextFactory();
35
36
  const ctxObj = Object.create(null);
36
- const keys = Object.keys(ctx);
37
- for (let i = 0; i < keys.length; i++) ctxObj[keys[i]] = ctx[keys[i]];
38
- return handler(ctxObj, input, signal);
37
+ applyContext(ctxObj, ctx);
38
+ return handler(ctxObj, input, defaultTimeout !== null ? AbortSignal.timeout(defaultTimeout) : new AbortController().signal);
39
39
  });
40
40
  }
41
41
  //#endregion
@@ -0,0 +1,90 @@
1
+ import { routerCache } from "./core/router-utils.mjs";
2
+ import { compileRouter, createContext, releaseContext } from "./compile.mjs";
3
+ import { applyContext } from "./core/dispatch.mjs";
4
+ //#region src/caller.ts
5
+ /**
6
+ * createCaller — call procedures directly without HTTP.
7
+ *
8
+ * Compiles the router, creates context, and runs the pipeline
9
+ * for each procedure call. Perfect for testing and server-side usage.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * const caller = s.createCaller(appRouter)
14
+ *
15
+ * // Call procedures directly
16
+ * const users = await caller.users.list({ limit: 10 })
17
+ * const user = await caller.users.get({ id: 1 })
18
+ *
19
+ * // With custom context override
20
+ * const adminCaller = s.createCaller(appRouter, {
21
+ * contextOverride: { user: { id: 1, role: 'admin' } },
22
+ * })
23
+ * ```
24
+ */
25
+ /**
26
+ * Create a direct caller for a router — no HTTP, no serialization.
27
+ *
28
+ * Returns a proxy that mirrors the router's nested structure.
29
+ * Calling a leaf procedure invokes the compiled pipeline directly.
30
+ */
31
+ function createCaller(routerDef, contextFactory, options) {
32
+ let compiledRouter = routerCache.get(routerDef);
33
+ if (!compiledRouter) {
34
+ compiledRouter = compileRouter(routerDef);
35
+ routerCache.set(routerDef, compiledRouter);
36
+ }
37
+ const router = compiledRouter;
38
+ const defaultTimeout = options?.timeout !== void 0 ? options.timeout : 3e4;
39
+ function createMockRequest(extraHeaders) {
40
+ const headers = new Headers(options?.headers);
41
+ if (extraHeaders) for (const [k, v] of Object.entries(extraHeaders)) headers.set(k, v);
42
+ return new Request("http://localhost/__caller", { headers });
43
+ }
44
+ async function resolveContext(perCallContext) {
45
+ const ctx = createContext();
46
+ if (contextFactory) {
47
+ const result = contextFactory(createMockRequest());
48
+ applyContext(ctx, result instanceof Promise ? await result : result);
49
+ }
50
+ if (options?.contextOverride) applyContext(ctx, options.contextOverride);
51
+ if (perCallContext) applyContext(ctx, perCallContext);
52
+ return ctx;
53
+ }
54
+ function createProxy(segments) {
55
+ const cache = /* @__PURE__ */ new Map();
56
+ return new Proxy(() => {}, {
57
+ get(_target, prop) {
58
+ if (typeof prop === "symbol") return void 0;
59
+ if (prop === "then" || prop === "toJSON" || prop === "toString" || prop === "$$typeof") return;
60
+ let sub = cache.get(prop);
61
+ if (!sub) {
62
+ sub = createProxy([...segments, prop]);
63
+ cache.set(prop, sub);
64
+ }
65
+ return sub;
66
+ },
67
+ apply(_target, _thisArg, args) {
68
+ const path = "/" + segments.join("/");
69
+ const input = args[0];
70
+ const callOptions = args[1];
71
+ return (async () => {
72
+ const match = router("", path);
73
+ if (!match) throw new Error(`Procedure not found: ${path}`);
74
+ const ctx = await resolveContext(callOptions?.context);
75
+ if (match.params) ctx.params = match.params;
76
+ const signal = callOptions?.signal ?? (defaultTimeout !== null ? AbortSignal.timeout(defaultTimeout) : void 0);
77
+ try {
78
+ const result = match.data.handler(ctx, input, signal);
79
+ return result instanceof Promise ? await result : result;
80
+ } finally {
81
+ releaseContext(ctx);
82
+ }
83
+ })();
84
+ }
85
+ });
86
+ }
87
+ return createProxy([]);
88
+ }
89
+ //#endregion
90
+ export { createCaller };
@@ -44,8 +44,20 @@ var RPCLink = class {
44
44
  body,
45
45
  signal: options.signal
46
46
  });
47
- const responseText = await response.text();
48
- const responseBody = responseText ? parseEmptyableJSON(responseText) : void 0;
47
+ const contentType = response.headers.get("content-type") ?? "";
48
+ let responseBody;
49
+ if (contentType.includes("msgpack")) {
50
+ const { decode } = await import("../../../codec/msgpack.mjs");
51
+ const buf = new Uint8Array(await response.arrayBuffer());
52
+ responseBody = buf.length > 0 ? decode(buf) : void 0;
53
+ } else if (contentType.includes("x-devalue")) {
54
+ const { decode } = await import("../../../codec/devalue.mjs");
55
+ const text = await response.text();
56
+ responseBody = text ? decode(text) : void 0;
57
+ } else {
58
+ const responseText = await response.text();
59
+ responseBody = responseText ? parseEmptyableJSON(responseText) : void 0;
60
+ }
49
61
  if (isErrorStatus(response.status)) {
50
62
  if (isSilgiErrorJSON(responseBody)) throw fromSilgiErrorJSON(responseBody);
51
63
  throw new SilgiError("INTERNAL_SERVER_ERROR", {
@@ -13,9 +13,23 @@ interface SilgiLinkOptions<TClientContext extends ClientContext = ClientContext>
13
13
  retryDelay?: number | ((ctx: FetchContext) => number);
14
14
  /** Timeout in ms (default: 30000) */
15
15
  timeout?: number;
16
- /** Use MessagePack binary protocol (2-4x faster, ~50% smaller payloads) */
16
+ /**
17
+ * Wire protocol for request/response encoding.
18
+ *
19
+ * - `'json'` — default, standard JSON
20
+ * - `'messagepack'` — 2-4x faster, ~50% smaller payloads
21
+ * - `'devalue'` — preserves Date, Map, Set, BigInt, circular refs
22
+ *
23
+ * @default 'json'
24
+ */
25
+ protocol?: 'json' | 'messagepack' | 'devalue';
26
+ /**
27
+ * @deprecated Use `protocol: 'messagepack'` instead.
28
+ */
17
29
  binary?: boolean;
18
- /** Use devalue protocol (preserves Date, Map, Set, BigInt, circular refs) */
30
+ /**
31
+ * @deprecated Use `protocol: 'devalue'` instead.
32
+ */
19
33
  devalue?: boolean;
20
34
  /** ofetch interceptors */
21
35
  onRequest?: FetchOptions['onRequest'];
@@ -26,17 +26,16 @@ function createLink(options) {
26
26
  const defaultTimeout = options.timeout ?? 3e4;
27
27
  const defaultRetry = options.retry;
28
28
  const defaultRetryDelay = options.retryDelay ?? 0;
29
- const binary = options.binary ?? false;
30
- const useDevalue = options.devalue ?? false;
29
+ const resolvedProtocol = options.protocol ?? (options.binary ? "messagepack" : void 0) ?? (options.devalue ? "devalue" : void 0) ?? "json";
31
30
  return { async call(path, input, callOptions) {
32
31
  const url = `${baseUrl}/${path.map(encodeURIComponent).join("/")}`;
33
32
  const headers = { ...typeof options.headers === "function" ? options.headers(callOptions) : options.headers };
34
33
  let body;
35
- if (binary) {
34
+ if (resolvedProtocol === "messagepack") {
36
35
  headers["content-type"] = MSGPACK_CONTENT_TYPE;
37
36
  headers["accept"] = MSGPACK_CONTENT_TYPE;
38
37
  body = input !== void 0 && input !== null ? encode(input) : void 0;
39
- } else if (useDevalue) {
38
+ } else if (resolvedProtocol === "devalue") {
40
39
  const { encode: devalueEncode, DEVALUE_CONTENT_TYPE } = await import("../../../codec/devalue.mjs");
41
40
  headers["content-type"] = DEVALUE_CONTENT_TYPE;
42
41
  headers["accept"] = DEVALUE_CONTENT_TYPE;
@@ -56,7 +55,7 @@ function createLink(options) {
56
55
  onResponse: options.onResponse,
57
56
  onRequestError: options.onRequestError,
58
57
  onResponseError: options.onResponseError,
59
- ...binary ? { responseType: "arrayBuffer" } : useDevalue ? { responseType: "text" } : { parseResponse(text) {
58
+ ...resolvedProtocol === "messagepack" ? { responseType: "arrayBuffer" } : resolvedProtocol === "devalue" ? { responseType: "text" } : { parseResponse(text) {
60
59
  if (!text) return void 0;
61
60
  try {
62
61
  return JSON.parse(text);
@@ -66,8 +65,8 @@ function createLink(options) {
66
65
  } }
67
66
  });
68
67
  let decoded;
69
- if (binary) decoded = decode(new Uint8Array(data));
70
- else if (useDevalue) {
68
+ if (resolvedProtocol === "messagepack") decoded = decode(new Uint8Array(data));
69
+ else if (resolvedProtocol === "devalue") {
71
70
  const { decode: devalueDecode } = await import("../../../codec/devalue.mjs");
72
71
  decoded = data ? devalueDecode(data) : void 0;
73
72
  } else decoded = data;
@@ -23,7 +23,7 @@ function createClientProxy(link, path) {
23
23
  if (typeof prop !== "string") return void 0;
24
24
  let cached = cache.get(prop);
25
25
  if (!cached) {
26
- cached = createClientProxy(link, Object.freeze([...path, prop]));
26
+ cached = createClientProxy(link, [...path, prop]);
27
27
  cache.set(prop, cached);
28
28
  }
29
29
  return cached;
@@ -2,6 +2,5 @@ import { SilgiError, SilgiErrorCode, SilgiErrorJSON, isDefinedError } from "../c
2
2
  import { Client, ClientContext, ClientLink, ClientOptions, ClientRest, InferClientInputs, InferClientOutputs, NestedClient } from "./types.mjs";
3
3
  import { SafeResult, createClient, safe } from "./client.mjs";
4
4
  import { DynamicLink, LinkSelector } from "./dynamic-link.mjs";
5
- import { mergeClients } from "./merge.mjs";
6
5
  import { ClientInterceptors, withInterceptors } from "./interceptor.mjs";
7
- export { type Client, type ClientContext, type ClientInterceptors, type ClientLink, type ClientOptions, type ClientRest, DynamicLink, type InferClientInputs, type InferClientOutputs, type LinkSelector, type NestedClient, type SafeResult, SilgiError, type SilgiErrorCode, type SilgiErrorJSON, createClient, isDefinedError, mergeClients, safe, withInterceptors };
6
+ export { type Client, type ClientContext, type ClientInterceptors, type ClientLink, type ClientOptions, type ClientRest, DynamicLink, type InferClientInputs, type InferClientOutputs, type LinkSelector, type NestedClient, type SafeResult, SilgiError, type SilgiErrorCode, type SilgiErrorJSON, createClient, isDefinedError, safe, withInterceptors };
@@ -1,6 +1,5 @@
1
1
  import { SilgiError, isDefinedError } from "../core/error.mjs";
2
2
  import { createClient, safe } from "./client.mjs";
3
3
  import { DynamicLink } from "./dynamic-link.mjs";
4
- import { mergeClients } from "./merge.mjs";
5
4
  import { withInterceptors } from "./interceptor.mjs";
6
- export { DynamicLink, SilgiError, createClient, isDefinedError, mergeClients, safe, withInterceptors };
5
+ export { DynamicLink, SilgiError, createClient, isDefinedError, safe, withInterceptors };
@@ -31,7 +31,7 @@ var BatchLink = class {
31
31
  if (batch.length === 0) return;
32
32
  const chunks = [];
33
33
  for (let i = 0; i < batch.length; i += this.#maxSize) chunks.push(batch.slice(i, i + this.#maxSize));
34
- for (const chunk of chunks) await this.#sendChunk(chunk);
34
+ await Promise.all(chunks.map((chunk) => this.#sendChunk(chunk)));
35
35
  }
36
36
  async #sendChunk(chunk) {
37
37
  const batchBody = chunk.map((call) => ({
@@ -40,7 +40,7 @@ var BatchLink = class {
40
40
  body: call.input
41
41
  }));
42
42
  try {
43
- const responses = await this.#link.call([this.#batchPath.slice(1)], batchBody, { signal: chunk[0]?.options.signal });
43
+ const responses = await this.#link.call([this.#batchPath.slice(1)], batchBody, { signal: chunk.length === 1 ? chunk[0].options.signal : AbortSignal.any(chunk.map((c) => c.options.signal).filter(Boolean)) });
44
44
  if (!Array.isArray(responses)) {
45
45
  for (const call of chunk) call.reject(/* @__PURE__ */ new Error("Invalid batch response"));
46
46
  return;
@@ -0,0 +1,24 @@
1
+ import { ClientContext, ClientLink } from "../types.mjs";
2
+
3
+ //#region src/client/plugins/circuit-breaker.d.ts
4
+ type CircuitState = 'closed' | 'open' | 'half-open';
5
+ declare class CircuitBreakerOpenError extends Error {
6
+ readonly state: CircuitState;
7
+ constructor();
8
+ }
9
+ interface CircuitBreakerOptions {
10
+ /** Number of consecutive failures before opening (default: 5) */
11
+ failureThreshold?: number;
12
+ /** Time in ms to wait before moving to half-open (default: 30000) */
13
+ resetTimeout?: number;
14
+ /** Called when circuit state changes */
15
+ onStateChange?: (state: CircuitState, info: {
16
+ failures: number;
17
+ }) => void;
18
+ }
19
+ declare function withCircuitBreaker<TClientContext extends ClientContext>(link: ClientLink<TClientContext>, options?: CircuitBreakerOptions): ClientLink<TClientContext> & {
20
+ getState: () => CircuitState;
21
+ reset: () => void;
22
+ };
23
+ //#endregion
24
+ export { CircuitBreakerOpenError, CircuitBreakerOptions, CircuitState, withCircuitBreaker };
@@ -0,0 +1,60 @@
1
+ //#region src/client/plugins/circuit-breaker.ts
2
+ var CircuitBreakerOpenError = class extends Error {
3
+ state = "open";
4
+ constructor() {
5
+ super("Circuit breaker is open — requests are blocked. Try again later.");
6
+ this.name = "CircuitBreakerOpenError";
7
+ }
8
+ };
9
+ function withCircuitBreaker(link, options = {}) {
10
+ const threshold = options.failureThreshold ?? 5;
11
+ const resetTimeout = options.resetTimeout ?? 3e4;
12
+ let state = "closed";
13
+ let failures = 0;
14
+ let openedAt = 0;
15
+ let probeSent = false;
16
+ function setState(newState) {
17
+ if (state !== newState) {
18
+ state = newState;
19
+ if (newState !== "half-open") probeSent = false;
20
+ options.onStateChange?.(state, { failures });
21
+ }
22
+ }
23
+ function recordSuccess() {
24
+ failures = 0;
25
+ setState("closed");
26
+ }
27
+ function recordFailure() {
28
+ failures++;
29
+ if (failures >= threshold) {
30
+ openedAt = Date.now();
31
+ setState("open");
32
+ }
33
+ }
34
+ return {
35
+ async call(path, input, callOptions) {
36
+ if (state === "open") if (Date.now() - openedAt >= resetTimeout) setState("half-open");
37
+ else throw new CircuitBreakerOpenError();
38
+ if (state === "half-open") {
39
+ if (probeSent) throw new CircuitBreakerOpenError();
40
+ probeSent = true;
41
+ }
42
+ try {
43
+ const result = await link.call(path, input, callOptions);
44
+ recordSuccess();
45
+ return result;
46
+ } catch (error) {
47
+ recordFailure();
48
+ throw error;
49
+ }
50
+ },
51
+ getState: () => state,
52
+ reset: () => {
53
+ failures = 0;
54
+ probeSent = false;
55
+ setState("closed");
56
+ }
57
+ };
58
+ }
59
+ //#endregion
60
+ export { CircuitBreakerOpenError, withCircuitBreaker };
@@ -1,5 +1,7 @@
1
1
  import { RetryOptions, withRetry } from "./retry.mjs";
2
+ import { CircuitBreakerOpenError, CircuitBreakerOptions, CircuitState, withCircuitBreaker } from "./circuit-breaker.mjs";
3
+ import { TimeoutOptions, withTimeout } from "./timeout.mjs";
2
4
  import { BatchLink, BatchLinkOptions } from "./batch.mjs";
3
5
  import { DedupeOptions, withDedupe } from "./dedupe.mjs";
4
6
  import { CSRFLinkOptions, withCSRF } from "./csrf.mjs";
5
- export { BatchLink, type BatchLinkOptions, type CSRFLinkOptions, type DedupeOptions, type RetryOptions, withCSRF, withDedupe, withRetry };
7
+ export { BatchLink, type BatchLinkOptions, type CSRFLinkOptions, CircuitBreakerOpenError, type CircuitBreakerOptions, type CircuitState, type DedupeOptions, type RetryOptions, type TimeoutOptions, withCSRF, withCircuitBreaker, withDedupe, withRetry, withTimeout };
@@ -1,5 +1,7 @@
1
1
  import { withRetry } from "./retry.mjs";
2
+ import { CircuitBreakerOpenError, withCircuitBreaker } from "./circuit-breaker.mjs";
3
+ import { withTimeout } from "./timeout.mjs";
2
4
  import { BatchLink } from "./batch.mjs";
3
5
  import { withDedupe } from "./dedupe.mjs";
4
6
  import { withCSRF } from "./csrf.mjs";
5
- export { BatchLink, withCSRF, withDedupe, withRetry };
7
+ export { BatchLink, CircuitBreakerOpenError, withCSRF, withCircuitBreaker, withDedupe, withRetry, withTimeout };
@@ -2,9 +2,30 @@ import { ClientContext, ClientLink } from "../types.mjs";
2
2
 
3
3
  //#region src/client/plugins/retry.d.ts
4
4
  interface RetryOptions {
5
+ /** Maximum number of retry attempts (default: 3) */
5
6
  maxRetries?: number;
6
- retryDelay?: number | ((attempt: number) => number);
7
- shouldRetry?: (error: unknown) => boolean;
7
+ /**
8
+ * Base delay in ms for exponential backoff (default: 1000).
9
+ * Actual delay: `baseDelay * 2^attempt + jitter`
10
+ * Or pass a function: `(attempt: number) => delayMs`
11
+ */
12
+ baseDelay?: number | ((attempt: number) => number);
13
+ /** Add random jitter 0-25% to prevent thundering herd (default: true) */
14
+ jitter?: boolean;
15
+ /**
16
+ * HTTP status codes to retry on (default: [408, 429, 500, 502, 503, 504]).
17
+ * Network errors (no status) are always retried unless shouldRetry returns false.
18
+ */
19
+ retryOn?: number[];
20
+ /** Custom retry predicate — return false to stop retrying */
21
+ shouldRetry?: (error: unknown, attempt: number) => boolean;
22
+ /** Called before each retry attempt */
23
+ onRetry?: (info: {
24
+ attempt: number;
25
+ delay: number;
26
+ error: unknown;
27
+ path: readonly string[];
28
+ }) => void;
8
29
  }
9
30
  declare function withRetry<TClientContext extends ClientContext>(link: ClientLink<TClientContext>, options?: RetryOptions): ClientLink<TClientContext>;
10
31
  //#endregion