silgi 0.0.13 → 0.1.0-beta.1
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 +102 -1
- package/dist/_virtual/_rolldown/runtime.mjs +5 -0
- package/dist/adapters/astro.d.mts +17 -0
- package/dist/adapters/astro.mjs +24 -0
- package/dist/adapters/aws-lambda.d.mts +31 -0
- package/dist/adapters/aws-lambda.mjs +85 -0
- package/dist/adapters/elysia.d.mts +17 -0
- package/dist/adapters/elysia.mjs +76 -0
- package/dist/adapters/express.d.mts +16 -0
- package/dist/adapters/express.mjs +78 -0
- package/dist/adapters/fastify.d.mts +15 -0
- package/dist/adapters/fastify.mjs +78 -0
- package/dist/adapters/message-port.d.mts +37 -0
- package/dist/adapters/message-port.mjs +129 -0
- package/dist/adapters/nestjs.d.mts +25 -0
- package/dist/adapters/nestjs.mjs +91 -0
- package/dist/adapters/nextjs.d.mts +21 -0
- package/dist/adapters/nextjs.mjs +30 -0
- package/dist/adapters/peer.d.mts +27 -0
- package/dist/adapters/peer.mjs +36 -0
- package/dist/adapters/remix.d.mts +17 -0
- package/dist/adapters/remix.mjs +24 -0
- package/dist/adapters/solidstart.d.mts +14 -0
- package/dist/adapters/solidstart.mjs +30 -0
- package/dist/adapters/sveltekit.d.mts +18 -0
- package/dist/adapters/sveltekit.mjs +33 -0
- package/dist/analyze.mjs +26 -0
- package/dist/broker/index.d.mts +62 -0
- package/dist/broker/index.mjs +153 -0
- package/dist/broker/nats.d.mts +33 -0
- package/dist/broker/nats.mjs +31 -0
- package/dist/broker/redis.d.mts +51 -0
- package/dist/broker/redis.mjs +92 -0
- package/dist/builder.d.mts +36 -0
- package/dist/builder.mjs +51 -0
- package/dist/callable.d.mts +17 -0
- package/dist/callable.mjs +42 -0
- package/dist/client/adapters/fetch/index.d.mts +17 -0
- package/dist/client/adapters/fetch/index.mjs +61 -0
- package/dist/client/adapters/ofetch/index.d.mts +41 -0
- package/dist/client/adapters/ofetch/index.mjs +92 -0
- package/dist/client/client.d.mts +29 -0
- package/dist/client/client.mjs +54 -0
- package/dist/client/dynamic-link.d.mts +15 -0
- package/dist/client/dynamic-link.mjs +16 -0
- package/dist/client/index.d.mts +7 -0
- package/dist/client/index.mjs +6 -0
- package/dist/client/interceptor.d.mts +31 -0
- package/dist/client/interceptor.mjs +34 -0
- package/dist/client/merge.d.mts +28 -0
- package/dist/client/merge.mjs +30 -0
- package/dist/client/openapi.d.mts +29 -0
- package/dist/client/openapi.mjs +89 -0
- package/dist/client/plugins/batch.d.mts +20 -0
- package/dist/client/plugins/batch.mjs +64 -0
- package/dist/client/plugins/csrf.d.mts +13 -0
- package/dist/client/plugins/csrf.mjs +20 -0
- package/dist/client/plugins/dedupe.d.mts +10 -0
- package/dist/client/plugins/dedupe.mjs +28 -0
- package/dist/client/plugins/index.d.mts +5 -0
- package/dist/client/plugins/index.mjs +5 -0
- package/dist/client/plugins/retry.d.mts +11 -0
- package/dist/client/plugins/retry.mjs +21 -0
- package/dist/client/server.d.mts +16 -0
- package/dist/client/server.mjs +60 -0
- package/dist/client/types.d.mts +29 -0
- package/dist/codec/devalue.d.mts +21 -0
- package/dist/codec/devalue.mjs +32 -0
- package/dist/codec/msgpack.d.mts +21 -0
- package/dist/codec/msgpack.mjs +59 -0
- package/dist/compile.d.mts +52 -0
- package/dist/compile.mjs +304 -0
- package/dist/contract.d.mts +36 -0
- package/dist/contract.mjs +40 -0
- package/dist/core/error.d.mts +104 -0
- package/dist/core/error.mjs +139 -0
- package/dist/core/handler.mjs +546 -0
- package/dist/core/iterator.d.mts +17 -0
- package/dist/core/iterator.mjs +79 -0
- package/dist/core/router-utils.mjs +16 -0
- package/dist/core/schema.d.mts +19 -0
- package/dist/core/schema.mjs +26 -0
- package/dist/core/serve.mjs +38 -0
- package/dist/core/sse.d.mts +16 -0
- package/dist/core/sse.mjs +95 -0
- package/dist/core/storage.d.mts +21 -0
- package/dist/core/storage.mjs +63 -0
- package/dist/core/utils.mjs +21 -0
- package/dist/fast-stringify.mjs +125 -0
- package/dist/index.d.mts +15 -37
- package/dist/index.mjs +13 -7
- package/dist/integrations/ai/index.d.mts +25 -0
- package/dist/integrations/ai/index.mjs +116 -0
- package/dist/integrations/react/index.d.mts +83 -0
- package/dist/integrations/react/index.mjs +197 -0
- package/dist/integrations/tanstack-query/index.d.mts +120 -0
- package/dist/integrations/tanstack-query/index.mjs +100 -0
- package/dist/integrations/tanstack-query/ssr.d.mts +51 -0
- package/dist/integrations/tanstack-query/ssr.mjs +89 -0
- package/dist/integrations/zod/converter.d.mts +75 -0
- package/dist/integrations/zod/converter.mjs +345 -0
- package/dist/integrations/zod/index.d.mts +2 -0
- package/dist/integrations/zod/index.mjs +2 -0
- package/dist/lazy.d.mts +24 -0
- package/dist/lazy.mjs +27 -0
- package/dist/lifecycle.d.mts +36 -0
- package/dist/lifecycle.mjs +46 -0
- package/dist/map-input.d.mts +17 -0
- package/dist/map-input.mjs +24 -0
- package/dist/plugins/analytics.d.mts +168 -0
- package/dist/plugins/analytics.mjs +459 -0
- package/dist/plugins/batch-server.d.mts +20 -0
- package/dist/plugins/batch-server.mjs +86 -0
- package/dist/plugins/body-limit.d.mts +16 -0
- package/dist/plugins/body-limit.mjs +44 -0
- package/dist/plugins/cache.d.mts +170 -0
- package/dist/plugins/cache.mjs +200 -0
- package/dist/plugins/coerce.d.mts +21 -0
- package/dist/plugins/coerce.mjs +46 -0
- package/dist/plugins/compression.d.mts +19 -0
- package/dist/plugins/compression.mjs +23 -0
- package/dist/plugins/cookies.d.mts +44 -0
- package/dist/plugins/cookies.mjs +67 -0
- package/dist/plugins/cors.d.mts +39 -0
- package/dist/plugins/cors.mjs +56 -0
- package/dist/plugins/custom-serializer.d.mts +57 -0
- package/dist/plugins/custom-serializer.mjs +40 -0
- package/dist/plugins/file-upload.d.mts +38 -0
- package/dist/plugins/file-upload.mjs +100 -0
- package/dist/plugins/index.d.mts +16 -0
- package/dist/plugins/index.mjs +16 -0
- package/dist/plugins/otel.d.mts +35 -0
- package/dist/plugins/otel.mjs +40 -0
- package/dist/plugins/pino.d.mts +60 -0
- package/dist/plugins/pino.mjs +42 -0
- package/dist/plugins/pubsub.d.mts +50 -0
- package/dist/plugins/pubsub.mjs +53 -0
- package/dist/plugins/ratelimit.d.mts +51 -0
- package/dist/plugins/ratelimit.mjs +81 -0
- package/dist/plugins/signing.d.mts +41 -0
- package/dist/plugins/signing.mjs +115 -0
- package/dist/plugins/strict-get.d.mts +10 -0
- package/dist/plugins/strict-get.mjs +33 -0
- package/dist/route/add.mjs +240 -0
- package/dist/route/compiler.mjs +373 -0
- package/dist/route/context.mjs +12 -0
- package/dist/route/types.d.mts +11 -0
- package/dist/route/utils.mjs +17 -0
- package/dist/scalar.d.mts +53 -0
- package/dist/scalar.mjs +315 -0
- package/dist/silgi.d.mts +139 -0
- package/dist/silgi.mjs +113 -0
- package/dist/trpc-interop.d.mts +22 -0
- package/dist/trpc-interop.mjs +68 -0
- package/dist/types.d.mts +82 -0
- package/dist/ws.d.mts +42 -0
- package/dist/ws.mjs +137 -0
- package/lib/dashboard/index.html +123 -0
- package/lib/ocache.d.mts +1 -0
- package/lib/ocache.mjs +1 -0
- package/lib/ofetch.d.mts +1 -0
- package/lib/ofetch.mjs +1 -0
- package/lib/srvx.d.mts +1 -0
- package/lib/srvx.mjs +1 -0
- package/lib/unstorage.d.mts +1 -0
- package/lib/unstorage.mjs +1 -0
- package/package.json +291 -65
- package/bin/silgi.mjs +0 -3
- package/dist/chunks/generate.mjs +0 -933
- package/dist/chunks/init.mjs +0 -21
- package/dist/cli/config.d.mts +0 -19
- package/dist/cli/config.d.ts +0 -19
- package/dist/cli/config.mjs +0 -5
- package/dist/cli/index.d.mts +0 -2
- package/dist/cli/index.d.ts +0 -2
- package/dist/cli/index.mjs +0 -119
- package/dist/index.d.ts +0 -37
- package/dist/plugins/openapi.d.mts +0 -138
- package/dist/plugins/openapi.d.ts +0 -138
- package/dist/plugins/openapi.mjs +0 -204
- package/dist/plugins/scalar.d.mts +0 -14
- package/dist/plugins/scalar.d.ts +0 -14
- package/dist/plugins/scalar.mjs +0 -66
- package/dist/shared/silgi.BMCYk2cR.mjs +0 -841
- package/dist/shared/silgi.D5qK9QOm.d.mts +0 -301
- package/dist/shared/silgi.D5qK9QOm.d.ts +0 -301
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { SilgiError, fromSilgiErrorJSON, isSilgiErrorJSON } from "../../../core/error.mjs";
|
|
2
|
+
import { MSGPACK_CONTENT_TYPE, decode, encode } from "../../../codec/msgpack.mjs";
|
|
3
|
+
import { FetchError, ofetch } from "ofetch";
|
|
4
|
+
//#region src/client/adapters/ofetch/index.ts
|
|
5
|
+
/**
|
|
6
|
+
* ofetch-based RPC transport — v2 client link.
|
|
7
|
+
*
|
|
8
|
+
* Uses ofetch for: retry, timeout, interceptors, auto-JSON.
|
|
9
|
+
* Replaces manual fetch + retry/dedupe plugins with a single link.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Create a Silgi client link powered by ofetch.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* import { createClient } from "silgi/client"
|
|
17
|
+
* import { createLink } from "silgi/client/ofetch"
|
|
18
|
+
*
|
|
19
|
+
* const link = createLink({ url: "http://localhost:3000" })
|
|
20
|
+
* const client = createClient<AppRouter>(link)
|
|
21
|
+
* const users = await client.users.list({ limit: 10 })
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
function createLink(options) {
|
|
25
|
+
const baseUrl = options.url.endsWith("/") ? options.url.slice(0, -1) : options.url;
|
|
26
|
+
const defaultTimeout = options.timeout ?? 3e4;
|
|
27
|
+
const defaultRetry = options.retry;
|
|
28
|
+
const defaultRetryDelay = options.retryDelay ?? 0;
|
|
29
|
+
const binary = options.binary ?? false;
|
|
30
|
+
const useDevalue = options.devalue ?? false;
|
|
31
|
+
return { async call(path, input, callOptions) {
|
|
32
|
+
const url = `${baseUrl}/${path.map(encodeURIComponent).join("/")}`;
|
|
33
|
+
const headers = { ...typeof options.headers === "function" ? options.headers(callOptions) : options.headers };
|
|
34
|
+
let body;
|
|
35
|
+
if (binary) {
|
|
36
|
+
headers["content-type"] = MSGPACK_CONTENT_TYPE;
|
|
37
|
+
headers["accept"] = MSGPACK_CONTENT_TYPE;
|
|
38
|
+
body = input !== void 0 && input !== null ? encode(input) : void 0;
|
|
39
|
+
} else if (useDevalue) {
|
|
40
|
+
const { encode: devalueEncode, DEVALUE_CONTENT_TYPE } = await import("../../../codec/devalue.mjs");
|
|
41
|
+
headers["content-type"] = DEVALUE_CONTENT_TYPE;
|
|
42
|
+
headers["accept"] = DEVALUE_CONTENT_TYPE;
|
|
43
|
+
body = input !== void 0 && input !== null ? devalueEncode(input) : void 0;
|
|
44
|
+
} else body = input !== void 0 && input !== null ? input : void 0;
|
|
45
|
+
try {
|
|
46
|
+
const data = await ofetch(url, {
|
|
47
|
+
method: "POST",
|
|
48
|
+
headers,
|
|
49
|
+
body,
|
|
50
|
+
signal: callOptions.signal,
|
|
51
|
+
timeout: defaultTimeout,
|
|
52
|
+
retry: defaultRetry ?? 0,
|
|
53
|
+
retryDelay: defaultRetryDelay,
|
|
54
|
+
ignoreResponseError: true,
|
|
55
|
+
onRequest: options.onRequest,
|
|
56
|
+
onResponse: options.onResponse,
|
|
57
|
+
onRequestError: options.onRequestError,
|
|
58
|
+
onResponseError: options.onResponseError,
|
|
59
|
+
...binary ? { responseType: "arrayBuffer" } : useDevalue ? { responseType: "text" } : { parseResponse(text) {
|
|
60
|
+
if (!text) return void 0;
|
|
61
|
+
try {
|
|
62
|
+
return JSON.parse(text);
|
|
63
|
+
} catch {
|
|
64
|
+
return text;
|
|
65
|
+
}
|
|
66
|
+
} }
|
|
67
|
+
});
|
|
68
|
+
let decoded;
|
|
69
|
+
if (binary) decoded = decode(new Uint8Array(data));
|
|
70
|
+
else if (useDevalue) {
|
|
71
|
+
const { decode: devalueDecode } = await import("../../../codec/devalue.mjs");
|
|
72
|
+
decoded = data ? devalueDecode(data) : void 0;
|
|
73
|
+
} else decoded = data;
|
|
74
|
+
if (isSilgiErrorJSON(decoded)) throw fromSilgiErrorJSON(decoded);
|
|
75
|
+
return decoded;
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (error instanceof SilgiError) throw error;
|
|
78
|
+
if (error instanceof FetchError) {
|
|
79
|
+
const responseData = error.data;
|
|
80
|
+
if (isSilgiErrorJSON(responseData)) throw fromSilgiErrorJSON(responseData);
|
|
81
|
+
throw new SilgiError("INTERNAL_SERVER_ERROR", {
|
|
82
|
+
status: error.status ?? 500,
|
|
83
|
+
message: error.message,
|
|
84
|
+
data: responseData
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
} };
|
|
90
|
+
}
|
|
91
|
+
//#endregion
|
|
92
|
+
export { createLink };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { InferClient } from "../types.mjs";
|
|
2
|
+
import { ClientContext, ClientLink } from "./types.mjs";
|
|
3
|
+
|
|
4
|
+
//#region src/client/client.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Create a type-safe client from a link.
|
|
7
|
+
*
|
|
8
|
+
* Accepts either a router type (auto-inferred) or a pre-inferred client type:
|
|
9
|
+
* ```ts
|
|
10
|
+
* // Recommended — pass AppRouter directly
|
|
11
|
+
* const client = createClient<AppRouter>(link)
|
|
12
|
+
*
|
|
13
|
+
* // Also works — explicit InferClient
|
|
14
|
+
* const client = createClient<InferClient<AppRouter>>(link)
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
declare function createClient<T, TClientContext extends ClientContext = Record<never, never>>(link: ClientLink<TClientContext>): InferClient<T> extends never ? T : InferClient<T>;
|
|
18
|
+
/**
|
|
19
|
+
* Safe client wrapper — returns [error, data] tuples instead of throwing.
|
|
20
|
+
*/
|
|
21
|
+
interface SafeResult<TOutput, TError> {
|
|
22
|
+
error: TError | null;
|
|
23
|
+
data: TOutput | undefined;
|
|
24
|
+
isError: boolean;
|
|
25
|
+
isSuccess: boolean;
|
|
26
|
+
}
|
|
27
|
+
declare function safe<TOutput, TError = unknown>(promise: Promise<TOutput>): Promise<SafeResult<TOutput, TError>>;
|
|
28
|
+
//#endregion
|
|
29
|
+
export { SafeResult, createClient, safe };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
//#region src/client/client.ts
|
|
2
|
+
/**
|
|
3
|
+
* Create a type-safe client from a link.
|
|
4
|
+
*
|
|
5
|
+
* Accepts either a router type (auto-inferred) or a pre-inferred client type:
|
|
6
|
+
* ```ts
|
|
7
|
+
* // Recommended — pass AppRouter directly
|
|
8
|
+
* const client = createClient<AppRouter>(link)
|
|
9
|
+
*
|
|
10
|
+
* // Also works — explicit InferClient
|
|
11
|
+
* const client = createClient<InferClient<AppRouter>>(link)
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
function createClient(link) {
|
|
15
|
+
return createClientProxy(link, []);
|
|
16
|
+
}
|
|
17
|
+
function createClientProxy(link, path) {
|
|
18
|
+
const cache = /* @__PURE__ */ new Map();
|
|
19
|
+
const procedureClient = (input, options) => link.call(path, input, options ?? {});
|
|
20
|
+
return new Proxy(procedureClient, {
|
|
21
|
+
get(_target, prop) {
|
|
22
|
+
if (prop === "then") return void 0;
|
|
23
|
+
if (typeof prop !== "string") return void 0;
|
|
24
|
+
let cached = cache.get(prop);
|
|
25
|
+
if (!cached) {
|
|
26
|
+
cached = createClientProxy(link, Object.freeze([...path, prop]));
|
|
27
|
+
cache.set(prop, cached);
|
|
28
|
+
}
|
|
29
|
+
return cached;
|
|
30
|
+
},
|
|
31
|
+
apply(_target, _thisArg, args) {
|
|
32
|
+
return procedureClient(args[0], args[1]);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
async function safe(promise) {
|
|
37
|
+
try {
|
|
38
|
+
return {
|
|
39
|
+
error: null,
|
|
40
|
+
data: await promise,
|
|
41
|
+
isError: false,
|
|
42
|
+
isSuccess: true
|
|
43
|
+
};
|
|
44
|
+
} catch (error) {
|
|
45
|
+
return {
|
|
46
|
+
error,
|
|
47
|
+
data: void 0,
|
|
48
|
+
isError: true,
|
|
49
|
+
isSuccess: false
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
//#endregion
|
|
54
|
+
export { createClient, safe };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ClientContext, ClientLink, ClientOptions } from "./types.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/client/dynamic-link.d.ts
|
|
4
|
+
type LinkSelector<TClientContext extends ClientContext = ClientContext> = (path: readonly string[], input: unknown, options: ClientOptions<TClientContext>) => ClientLink<TClientContext>;
|
|
5
|
+
/**
|
|
6
|
+
* A link that delegates to other links based on a selector function.
|
|
7
|
+
* The selector runs on every call, so it can use dynamic state.
|
|
8
|
+
*/
|
|
9
|
+
declare class DynamicLink<TClientContext extends ClientContext = ClientContext> implements ClientLink<TClientContext> {
|
|
10
|
+
#private;
|
|
11
|
+
constructor(selector: LinkSelector<TClientContext>);
|
|
12
|
+
call(path: readonly string[], input: unknown, options: ClientOptions<TClientContext>): Promise<unknown>;
|
|
13
|
+
}
|
|
14
|
+
//#endregion
|
|
15
|
+
export { DynamicLink, LinkSelector };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/client/dynamic-link.ts
|
|
2
|
+
/**
|
|
3
|
+
* A link that delegates to other links based on a selector function.
|
|
4
|
+
* The selector runs on every call, so it can use dynamic state.
|
|
5
|
+
*/
|
|
6
|
+
var DynamicLink = class {
|
|
7
|
+
#selector;
|
|
8
|
+
constructor(selector) {
|
|
9
|
+
this.#selector = selector;
|
|
10
|
+
}
|
|
11
|
+
call(path, input, options) {
|
|
12
|
+
return this.#selector(path, input, options).call(path, input, options);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
//#endregion
|
|
16
|
+
export { DynamicLink };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { SilgiError, SilgiErrorCode, SilgiErrorJSON, isDefinedError } from "../core/error.mjs";
|
|
2
|
+
import { Client, ClientContext, ClientLink, ClientOptions, ClientRest, InferClientInputs, InferClientOutputs, NestedClient } from "./types.mjs";
|
|
3
|
+
import { SafeResult, createClient, safe } from "./client.mjs";
|
|
4
|
+
import { DynamicLink, LinkSelector } from "./dynamic-link.mjs";
|
|
5
|
+
import { mergeClients } from "./merge.mjs";
|
|
6
|
+
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 };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { SilgiError, isDefinedError } from "../core/error.mjs";
|
|
2
|
+
import { createClient, safe } from "./client.mjs";
|
|
3
|
+
import { DynamicLink } from "./dynamic-link.mjs";
|
|
4
|
+
import { mergeClients } from "./merge.mjs";
|
|
5
|
+
import { withInterceptors } from "./interceptor.mjs";
|
|
6
|
+
export { DynamicLink, SilgiError, createClient, isDefinedError, mergeClients, safe, withInterceptors };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { ClientContext, ClientLink, ClientOptions } from "./types.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/client/interceptor.d.ts
|
|
4
|
+
interface ClientInterceptors<TClientContext extends ClientContext = ClientContext> {
|
|
5
|
+
/** Called before every request. Can modify input or options. */
|
|
6
|
+
onRequest?: (event: {
|
|
7
|
+
path: readonly string[];
|
|
8
|
+
input: unknown;
|
|
9
|
+
options: ClientOptions<TClientContext>;
|
|
10
|
+
}) => void | Promise<void>;
|
|
11
|
+
/** Called after a successful response. */
|
|
12
|
+
onResponse?: (event: {
|
|
13
|
+
path: readonly string[];
|
|
14
|
+
input: unknown;
|
|
15
|
+
output: unknown;
|
|
16
|
+
durationMs: number;
|
|
17
|
+
}) => void | Promise<void>;
|
|
18
|
+
/** Called when a request fails. */
|
|
19
|
+
onError?: (event: {
|
|
20
|
+
path: readonly string[];
|
|
21
|
+
input: unknown;
|
|
22
|
+
error: unknown;
|
|
23
|
+
}) => void | Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Wrap a ClientLink with interceptor hooks.
|
|
27
|
+
* Returns a new link that calls the interceptors around the original.
|
|
28
|
+
*/
|
|
29
|
+
declare function withInterceptors<TClientContext extends ClientContext = ClientContext>(link: ClientLink<TClientContext>, interceptors: ClientInterceptors<TClientContext>): ClientLink<TClientContext>;
|
|
30
|
+
//#endregion
|
|
31
|
+
export { ClientInterceptors, withInterceptors };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
//#region src/client/interceptor.ts
|
|
2
|
+
/**
|
|
3
|
+
* Wrap a ClientLink with interceptor hooks.
|
|
4
|
+
* Returns a new link that calls the interceptors around the original.
|
|
5
|
+
*/
|
|
6
|
+
function withInterceptors(link, interceptors) {
|
|
7
|
+
return { async call(path, input, options) {
|
|
8
|
+
if (interceptors.onRequest) await interceptors.onRequest({
|
|
9
|
+
path,
|
|
10
|
+
input,
|
|
11
|
+
options
|
|
12
|
+
});
|
|
13
|
+
const t0 = performance.now();
|
|
14
|
+
try {
|
|
15
|
+
const output = await link.call(path, input, options);
|
|
16
|
+
if (interceptors.onResponse) await interceptors.onResponse({
|
|
17
|
+
path,
|
|
18
|
+
input,
|
|
19
|
+
output,
|
|
20
|
+
durationMs: performance.now() - t0
|
|
21
|
+
});
|
|
22
|
+
return output;
|
|
23
|
+
} catch (error) {
|
|
24
|
+
if (interceptors.onError) await interceptors.onError({
|
|
25
|
+
path,
|
|
26
|
+
input,
|
|
27
|
+
error
|
|
28
|
+
});
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
} };
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
34
|
+
export { withInterceptors };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
//#region src/client/merge.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Client merging — combine multiple typed clients into one.
|
|
4
|
+
*
|
|
5
|
+
* Useful when different parts of your API live on different servers
|
|
6
|
+
* or use different transports.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { mergeClients } from "silgi/client"
|
|
11
|
+
*
|
|
12
|
+
* const client = mergeClients({
|
|
13
|
+
* users: usersClient,
|
|
14
|
+
* billing: billingClient,
|
|
15
|
+
* analytics: analyticsClient,
|
|
16
|
+
* })
|
|
17
|
+
*
|
|
18
|
+
* await client.users.list({ limit: 10 })
|
|
19
|
+
* await client.billing.invoices()
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Merge multiple clients into a single typed object.
|
|
24
|
+
* Each key maps to a separate client — they can use different links.
|
|
25
|
+
*/
|
|
26
|
+
declare function mergeClients<T extends Record<string, unknown>>(clients: T): T;
|
|
27
|
+
//#endregion
|
|
28
|
+
export { mergeClients };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
//#region src/client/merge.ts
|
|
2
|
+
/**
|
|
3
|
+
* Client merging — combine multiple typed clients into one.
|
|
4
|
+
*
|
|
5
|
+
* Useful when different parts of your API live on different servers
|
|
6
|
+
* or use different transports.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { mergeClients } from "silgi/client"
|
|
11
|
+
*
|
|
12
|
+
* const client = mergeClients({
|
|
13
|
+
* users: usersClient,
|
|
14
|
+
* billing: billingClient,
|
|
15
|
+
* analytics: analyticsClient,
|
|
16
|
+
* })
|
|
17
|
+
*
|
|
18
|
+
* await client.users.list({ limit: 10 })
|
|
19
|
+
* await client.billing.invoices()
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Merge multiple clients into a single typed object.
|
|
24
|
+
* Each key maps to a separate client — they can use different links.
|
|
25
|
+
*/
|
|
26
|
+
function mergeClients(clients) {
|
|
27
|
+
return clients;
|
|
28
|
+
}
|
|
29
|
+
//#endregion
|
|
30
|
+
export { mergeClients };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { ClientContext, ClientLink, ClientOptions } from "./types.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/client/openapi.d.ts
|
|
4
|
+
interface OpenAPILinkOptions {
|
|
5
|
+
/** Base URL of the API */
|
|
6
|
+
url: string;
|
|
7
|
+
/** OpenAPI 3.x specification object (parsed JSON) */
|
|
8
|
+
spec?: Record<string, unknown>;
|
|
9
|
+
/** Default headers */
|
|
10
|
+
headers?: Record<string, string> | (() => Record<string, string>);
|
|
11
|
+
/** Custom fetch implementation */
|
|
12
|
+
fetch?: typeof globalThis.fetch;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* A ClientLink that makes REST requests to an OpenAPI endpoint.
|
|
16
|
+
*
|
|
17
|
+
* Maps procedure paths to REST paths:
|
|
18
|
+
* - `client.users.list({ limit: 10 })` → `GET /users/list?limit=10` or `POST /users/list`
|
|
19
|
+
*
|
|
20
|
+
* Without a spec, it defaults to POST requests with JSON body.
|
|
21
|
+
* With a spec, it uses the correct HTTP method and parameter placement.
|
|
22
|
+
*/
|
|
23
|
+
declare class OpenAPILink<TCtx extends ClientContext = ClientContext> implements ClientLink<TCtx> {
|
|
24
|
+
#private;
|
|
25
|
+
constructor(options: OpenAPILinkOptions);
|
|
26
|
+
call(path: readonly string[], input: unknown, options: ClientOptions<TCtx>): Promise<unknown>;
|
|
27
|
+
}
|
|
28
|
+
//#endregion
|
|
29
|
+
export { OpenAPILink, OpenAPILinkOptions };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { SilgiError } from "../core/error.mjs";
|
|
2
|
+
//#region src/client/openapi.ts
|
|
3
|
+
/**
|
|
4
|
+
* OpenAPI Client Link — consume any OpenAPI endpoint as a Silgi client.
|
|
5
|
+
*
|
|
6
|
+
* Unlike RPCLink (which uses Silgi's wire protocol), OpenAPILink sends
|
|
7
|
+
* standard REST requests based on an OpenAPI spec.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { OpenAPILink } from "silgi/client/openapi"
|
|
12
|
+
* import { createClient } from "silgi/client"
|
|
13
|
+
*
|
|
14
|
+
* const link = new OpenAPILink({
|
|
15
|
+
* url: "https://api.example.com",
|
|
16
|
+
* spec: await fetch("/openapi.json").then(r => r.json()),
|
|
17
|
+
* })
|
|
18
|
+
*
|
|
19
|
+
* const client = createClient<ExternalAPI>(link)
|
|
20
|
+
* const users = await client.users.list({ limit: 10 })
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* A ClientLink that makes REST requests to an OpenAPI endpoint.
|
|
25
|
+
*
|
|
26
|
+
* Maps procedure paths to REST paths:
|
|
27
|
+
* - `client.users.list({ limit: 10 })` → `GET /users/list?limit=10` or `POST /users/list`
|
|
28
|
+
*
|
|
29
|
+
* Without a spec, it defaults to POST requests with JSON body.
|
|
30
|
+
* With a spec, it uses the correct HTTP method and parameter placement.
|
|
31
|
+
*/
|
|
32
|
+
var OpenAPILink = class {
|
|
33
|
+
#url;
|
|
34
|
+
#spec;
|
|
35
|
+
#headers;
|
|
36
|
+
#fetch;
|
|
37
|
+
constructor(options) {
|
|
38
|
+
this.#url = options.url.replace(/\/$/, "");
|
|
39
|
+
this.#spec = options.spec;
|
|
40
|
+
this.#headers = options.headers ?? {};
|
|
41
|
+
this.#fetch = options.fetch ?? globalThis.fetch;
|
|
42
|
+
}
|
|
43
|
+
async call(path, input, options) {
|
|
44
|
+
const endpoint = "/" + path.join("/");
|
|
45
|
+
const url = this.#url + endpoint;
|
|
46
|
+
const headers = {
|
|
47
|
+
"content-type": "application/json",
|
|
48
|
+
...typeof this.#headers === "function" ? this.#headers() : this.#headers
|
|
49
|
+
};
|
|
50
|
+
let method = "POST";
|
|
51
|
+
if (this.#spec) {
|
|
52
|
+
const paths = this.#spec.paths;
|
|
53
|
+
if (paths?.[endpoint]) {
|
|
54
|
+
if (paths[endpoint].get) method = "GET";
|
|
55
|
+
else if (paths[endpoint].post) method = "POST";
|
|
56
|
+
else if (paths[endpoint].put) method = "PUT";
|
|
57
|
+
else if (paths[endpoint].patch) method = "PATCH";
|
|
58
|
+
else if (paths[endpoint].delete) method = "DELETE";
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
let requestUrl = url;
|
|
62
|
+
let body;
|
|
63
|
+
if (method === "GET" || method === "HEAD") {
|
|
64
|
+
if (input && typeof input === "object") {
|
|
65
|
+
const params = new URLSearchParams();
|
|
66
|
+
for (const [k, v] of Object.entries(input)) if (v !== void 0) params.set(k, String(v));
|
|
67
|
+
const qs = params.toString();
|
|
68
|
+
if (qs) requestUrl = `${url}?${qs}`;
|
|
69
|
+
}
|
|
70
|
+
} else body = input !== void 0 ? JSON.stringify(input) : void 0;
|
|
71
|
+
const response = await this.#fetch(requestUrl, {
|
|
72
|
+
method,
|
|
73
|
+
headers,
|
|
74
|
+
body,
|
|
75
|
+
signal: options.signal
|
|
76
|
+
});
|
|
77
|
+
if (!response.ok) {
|
|
78
|
+
const errorBody = await response.json().catch(() => null);
|
|
79
|
+
throw new SilgiError(errorBody?.code ?? `HTTP_${response.status}`, {
|
|
80
|
+
status: response.status,
|
|
81
|
+
message: errorBody?.message ?? response.statusText,
|
|
82
|
+
data: errorBody
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return response.json();
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
//#endregion
|
|
89
|
+
export { OpenAPILink };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ClientContext, ClientLink, ClientOptions } from "../types.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/client/plugins/batch.d.ts
|
|
4
|
+
interface BatchLinkOptions {
|
|
5
|
+
/** The underlying link to send the batch through */
|
|
6
|
+
link: ClientLink;
|
|
7
|
+
/** Batch endpoint path (default: /__batch__) */
|
|
8
|
+
path?: string;
|
|
9
|
+
/** Maximum batch size (default: 20) */
|
|
10
|
+
maxSize?: number;
|
|
11
|
+
/** URL for the batch endpoint */
|
|
12
|
+
url: string | URL;
|
|
13
|
+
}
|
|
14
|
+
declare class BatchLink<TClientContext extends ClientContext = ClientContext> implements ClientLink<TClientContext> {
|
|
15
|
+
#private;
|
|
16
|
+
constructor(options: BatchLinkOptions);
|
|
17
|
+
call(path: readonly string[], input: unknown, options: ClientOptions<TClientContext>): Promise<unknown>;
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
export { BatchLink, BatchLinkOptions };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
//#region src/client/plugins/batch.ts
|
|
2
|
+
var BatchLink = class {
|
|
3
|
+
#link;
|
|
4
|
+
#batchPath;
|
|
5
|
+
#maxSize;
|
|
6
|
+
#pending = [];
|
|
7
|
+
#scheduled = false;
|
|
8
|
+
constructor(options) {
|
|
9
|
+
this.#link = options.link;
|
|
10
|
+
this.#batchPath = options.path ?? "/__batch__";
|
|
11
|
+
this.#maxSize = options.maxSize ?? 20;
|
|
12
|
+
}
|
|
13
|
+
call(path, input, options) {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
this.#pending.push({
|
|
16
|
+
path,
|
|
17
|
+
input,
|
|
18
|
+
options,
|
|
19
|
+
resolve,
|
|
20
|
+
reject
|
|
21
|
+
});
|
|
22
|
+
if (!this.#scheduled) {
|
|
23
|
+
this.#scheduled = true;
|
|
24
|
+
queueMicrotask(() => this.#flush());
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
async #flush() {
|
|
29
|
+
this.#scheduled = false;
|
|
30
|
+
const batch = this.#pending.splice(0);
|
|
31
|
+
if (batch.length === 0) return;
|
|
32
|
+
const chunks = [];
|
|
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);
|
|
35
|
+
}
|
|
36
|
+
async #sendChunk(chunk) {
|
|
37
|
+
const batchBody = chunk.map((call) => ({
|
|
38
|
+
path: "/" + call.path.join("/"),
|
|
39
|
+
method: "POST",
|
|
40
|
+
body: call.input
|
|
41
|
+
}));
|
|
42
|
+
try {
|
|
43
|
+
const responses = await this.#link.call([this.#batchPath.slice(1)], batchBody, { signal: chunk[0]?.options.signal });
|
|
44
|
+
if (!Array.isArray(responses)) {
|
|
45
|
+
for (const call of chunk) call.reject(/* @__PURE__ */ new Error("Invalid batch response"));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
for (const response of responses) {
|
|
49
|
+
const call = chunk[response.index];
|
|
50
|
+
if (!call) continue;
|
|
51
|
+
if (response.status >= 400) call.reject(response.body ?? /* @__PURE__ */ new Error(`HTTP ${response.status}`));
|
|
52
|
+
else call.resolve(response.body);
|
|
53
|
+
}
|
|
54
|
+
for (let i = 0; i < chunk.length; i++) {
|
|
55
|
+
const call = chunk[i];
|
|
56
|
+
if (!responses.some((r) => r.index === i)) call.reject(/* @__PURE__ */ new Error("No response in batch"));
|
|
57
|
+
}
|
|
58
|
+
} catch (error) {
|
|
59
|
+
for (const call of chunk) call.reject(error);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
//#endregion
|
|
64
|
+
export { BatchLink };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { ClientContext, ClientLink } from "../types.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/client/plugins/csrf.d.ts
|
|
4
|
+
interface CSRFLinkOptions {
|
|
5
|
+
headerName?: string;
|
|
6
|
+
headerValue?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Wrap a link to automatically inject the CSRF header on every request.
|
|
10
|
+
*/
|
|
11
|
+
declare function withCSRF<TClientContext extends ClientContext>(link: ClientLink<TClientContext>, options?: CSRFLinkOptions): ClientLink<TClientContext>;
|
|
12
|
+
//#endregion
|
|
13
|
+
export { CSRFLinkOptions, withCSRF };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
//#region src/client/plugins/csrf.ts
|
|
2
|
+
/**
|
|
3
|
+
* Wrap a link to automatically inject the CSRF header on every request.
|
|
4
|
+
*/
|
|
5
|
+
function withCSRF(link, options = {}) {
|
|
6
|
+
const headerName = options.headerName ?? "x-csrf-token";
|
|
7
|
+
const headerValue = options.headerValue ?? "silgi";
|
|
8
|
+
return { call(path, input, callOptions) {
|
|
9
|
+
const enhancedOptions = {
|
|
10
|
+
...callOptions,
|
|
11
|
+
headers: {
|
|
12
|
+
...callOptions.headers,
|
|
13
|
+
[headerName]: headerValue
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
return link.call(path, input, enhancedOptions);
|
|
17
|
+
} };
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
export { withCSRF };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ClientContext, ClientLink } from "../types.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/client/plugins/dedupe.d.ts
|
|
4
|
+
interface DedupeOptions {
|
|
5
|
+
/** Custom key function. Default: JSON.stringify(path + input) */
|
|
6
|
+
keyFn?: (path: readonly string[], input: unknown) => string;
|
|
7
|
+
}
|
|
8
|
+
declare function withDedupe<TClientContext extends ClientContext>(link: ClientLink<TClientContext>, options?: DedupeOptions): ClientLink<TClientContext>;
|
|
9
|
+
//#endregion
|
|
10
|
+
export { DedupeOptions, withDedupe };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { stringifyJSON } from "../../core/utils.mjs";
|
|
2
|
+
//#region src/client/plugins/dedupe.ts
|
|
3
|
+
/**
|
|
4
|
+
* Dedupe Plugin — collapses identical concurrent requests into one.
|
|
5
|
+
*
|
|
6
|
+
* If two calls have the same path and serialized input within
|
|
7
|
+
* the same microtask, only one actual request is sent.
|
|
8
|
+
* All callers receive the same response.
|
|
9
|
+
*/
|
|
10
|
+
function withDedupe(link, options = {}) {
|
|
11
|
+
const inflight = /* @__PURE__ */ new Map();
|
|
12
|
+
const keyFn = options.keyFn ?? ((path, input) => stringifyJSON({
|
|
13
|
+
path,
|
|
14
|
+
input
|
|
15
|
+
}));
|
|
16
|
+
return { call(path, input, callOptions) {
|
|
17
|
+
const key = keyFn(path, input);
|
|
18
|
+
const existing = inflight.get(key);
|
|
19
|
+
if (existing) return existing;
|
|
20
|
+
const promise = link.call(path, input, callOptions).finally(() => {
|
|
21
|
+
inflight.delete(key);
|
|
22
|
+
});
|
|
23
|
+
inflight.set(key, promise);
|
|
24
|
+
return promise;
|
|
25
|
+
} };
|
|
26
|
+
}
|
|
27
|
+
//#endregion
|
|
28
|
+
export { withDedupe };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { RetryOptions, withRetry } from "./retry.mjs";
|
|
2
|
+
import { BatchLink, BatchLinkOptions } from "./batch.mjs";
|
|
3
|
+
import { DedupeOptions, withDedupe } from "./dedupe.mjs";
|
|
4
|
+
import { CSRFLinkOptions, withCSRF } from "./csrf.mjs";
|
|
5
|
+
export { BatchLink, type BatchLinkOptions, type CSRFLinkOptions, type DedupeOptions, type RetryOptions, withCSRF, withDedupe, withRetry };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ClientContext, ClientLink } from "../types.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/client/plugins/retry.d.ts
|
|
4
|
+
interface RetryOptions {
|
|
5
|
+
maxRetries?: number;
|
|
6
|
+
retryDelay?: number | ((attempt: number) => number);
|
|
7
|
+
shouldRetry?: (error: unknown) => boolean;
|
|
8
|
+
}
|
|
9
|
+
declare function withRetry<TClientContext extends ClientContext>(link: ClientLink<TClientContext>, options?: RetryOptions): ClientLink<TClientContext>;
|
|
10
|
+
//#endregion
|
|
11
|
+
export { RetryOptions, withRetry };
|