silgi 0.0.14 → 0.1.0-beta.2
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 +54 -0
- package/dist/compile.mjs +305 -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 +330 -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,153 @@
|
|
|
1
|
+
import { SilgiError, toSilgiError } from "../core/error.mjs";
|
|
2
|
+
import { ValidationError } from "../core/schema.mjs";
|
|
3
|
+
import { compileRouter } from "../compile.mjs";
|
|
4
|
+
//#region src/broker/index.ts
|
|
5
|
+
/**
|
|
6
|
+
* Message Broker adapter — driver-based RPC over any message broker.
|
|
7
|
+
*
|
|
8
|
+
* Pluggable driver pattern (like unstorage) — bring your own broker client.
|
|
9
|
+
* Built-in memory driver for testing. NATS and Redis drivers available separately.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* // Server
|
|
14
|
+
* import { silgiBroker, memoryBroker } from "silgi/broker"
|
|
15
|
+
*
|
|
16
|
+
* const driver = memoryBroker()
|
|
17
|
+
* const dispose = await silgiBroker(appRouter, driver, {
|
|
18
|
+
* subject: "myapp.rpc",
|
|
19
|
+
* context: () => ({ db: getDB() }),
|
|
20
|
+
* })
|
|
21
|
+
*
|
|
22
|
+
* // Client
|
|
23
|
+
* import { BrokerLink } from "silgi/broker"
|
|
24
|
+
* import { createClient } from "silgi/client"
|
|
25
|
+
*
|
|
26
|
+
* const client = createClient<AppRouter>(new BrokerLink(driver, { subject: "myapp.rpc" }))
|
|
27
|
+
* const users = await client.users.list({ limit: 10 })
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* Attach Silgi to a message broker (server side).
|
|
32
|
+
*
|
|
33
|
+
* Listens for RPC messages on the given subject, dispatches to the compiled
|
|
34
|
+
* router, and replies with results or errors.
|
|
35
|
+
*
|
|
36
|
+
* Returns a cleanup function to stop listening.
|
|
37
|
+
*/
|
|
38
|
+
async function silgiBroker(router, driver, options = {}) {
|
|
39
|
+
const compiledRouter = compileRouter(router);
|
|
40
|
+
const subject = options.subject ?? "silgi";
|
|
41
|
+
const unsubscribe = await driver.subscribe(subject, (payload, reply) => {
|
|
42
|
+
let msg;
|
|
43
|
+
try {
|
|
44
|
+
msg = JSON.parse(payload);
|
|
45
|
+
} catch {
|
|
46
|
+
reply(JSON.stringify({ e: {
|
|
47
|
+
code: "BAD_REQUEST",
|
|
48
|
+
status: 400,
|
|
49
|
+
message: "Invalid payload"
|
|
50
|
+
} }));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const match = compiledRouter("POST", "/" + msg.p);
|
|
54
|
+
if (!match) {
|
|
55
|
+
reply(JSON.stringify({ e: {
|
|
56
|
+
code: "NOT_FOUND",
|
|
57
|
+
status: 404,
|
|
58
|
+
message: "Procedure not found"
|
|
59
|
+
} }));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const route = match.data;
|
|
63
|
+
(async () => {
|
|
64
|
+
try {
|
|
65
|
+
const ctx = Object.create(null);
|
|
66
|
+
if (match.params) ctx.params = match.params;
|
|
67
|
+
if (options.context) {
|
|
68
|
+
const baseCtx = await options.context();
|
|
69
|
+
const keys = Object.keys(baseCtx);
|
|
70
|
+
for (let i = 0; i < keys.length; i++) ctx[keys[i]] = baseCtx[keys[i]];
|
|
71
|
+
}
|
|
72
|
+
const signal = new AbortController().signal;
|
|
73
|
+
const res = { r: await route.handler(ctx, msg.i, signal) };
|
|
74
|
+
reply(JSON.stringify(res));
|
|
75
|
+
} catch (error) {
|
|
76
|
+
const res = { e: error instanceof ValidationError ? {
|
|
77
|
+
code: "BAD_REQUEST",
|
|
78
|
+
status: 400,
|
|
79
|
+
message: error.message,
|
|
80
|
+
data: { issues: error.issues }
|
|
81
|
+
} : error instanceof SilgiError ? error.toJSON() : toSilgiError(error).toJSON() };
|
|
82
|
+
reply(JSON.stringify(res));
|
|
83
|
+
}
|
|
84
|
+
})();
|
|
85
|
+
});
|
|
86
|
+
return typeof unsubscribe === "function" ? unsubscribe : () => {};
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Client-side broker link — sends RPC calls via a broker driver.
|
|
90
|
+
*/
|
|
91
|
+
var BrokerLink = class {
|
|
92
|
+
#driver;
|
|
93
|
+
#subject;
|
|
94
|
+
#timeout;
|
|
95
|
+
constructor(driver, options = {}) {
|
|
96
|
+
this.#driver = driver;
|
|
97
|
+
this.#subject = options.subject ?? "silgi";
|
|
98
|
+
this.#timeout = options.timeout ?? 1e4;
|
|
99
|
+
}
|
|
100
|
+
async call(path, input, _options) {
|
|
101
|
+
const req = {
|
|
102
|
+
p: path.join("/"),
|
|
103
|
+
i: input
|
|
104
|
+
};
|
|
105
|
+
const raw = await this.#driver.request(this.#subject, JSON.stringify(req), { timeout: this.#timeout });
|
|
106
|
+
const res = JSON.parse(raw);
|
|
107
|
+
if (res.e) throw new SilgiError(res.e.code, {
|
|
108
|
+
status: res.e.status,
|
|
109
|
+
message: res.e.message,
|
|
110
|
+
data: res.e.data
|
|
111
|
+
});
|
|
112
|
+
return res.r;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
/**
|
|
116
|
+
* In-memory broker driver — for testing and single-process development.
|
|
117
|
+
* Simulates request-reply without any external broker.
|
|
118
|
+
*/
|
|
119
|
+
function memoryBroker() {
|
|
120
|
+
const subscribers = /* @__PURE__ */ new Map();
|
|
121
|
+
return {
|
|
122
|
+
subscribe(subject, handler) {
|
|
123
|
+
let set = subscribers.get(subject);
|
|
124
|
+
if (!set) {
|
|
125
|
+
set = /* @__PURE__ */ new Set();
|
|
126
|
+
subscribers.set(subject, set);
|
|
127
|
+
}
|
|
128
|
+
set.add(handler);
|
|
129
|
+
return () => {
|
|
130
|
+
set.delete(handler);
|
|
131
|
+
if (set.size === 0) subscribers.delete(subject);
|
|
132
|
+
};
|
|
133
|
+
},
|
|
134
|
+
request(subject, payload, opts) {
|
|
135
|
+
return new Promise((resolve, reject) => {
|
|
136
|
+
const handlers = subscribers.get(subject);
|
|
137
|
+
if (!handlers || handlers.size === 0) {
|
|
138
|
+
reject(/* @__PURE__ */ new Error(`No subscriber for subject "${subject}"`));
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const timeout = opts?.timeout ?? 1e4;
|
|
142
|
+
const timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`Broker request timeout: ${subject}`)), timeout);
|
|
143
|
+
const handler = handlers.values().next().value;
|
|
144
|
+
handler(payload, (response) => {
|
|
145
|
+
clearTimeout(timer);
|
|
146
|
+
resolve(response);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
//#endregion
|
|
153
|
+
export { BrokerLink, memoryBroker, silgiBroker };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { BrokerDriver } from "./index.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/broker/nats.d.ts
|
|
4
|
+
interface NatsMsg {
|
|
5
|
+
data: Uint8Array;
|
|
6
|
+
respond(payload: Uint8Array): boolean;
|
|
7
|
+
}
|
|
8
|
+
interface NatsSub {
|
|
9
|
+
unsubscribe(): void;
|
|
10
|
+
}
|
|
11
|
+
interface NatsConnection {
|
|
12
|
+
subscribe(subject: string, opts?: {
|
|
13
|
+
callback?: (err: Error | null, msg: NatsMsg) => void;
|
|
14
|
+
queue?: string;
|
|
15
|
+
}): NatsSub;
|
|
16
|
+
request(subject: string, payload?: Uint8Array, opts?: {
|
|
17
|
+
timeout?: number;
|
|
18
|
+
}): Promise<NatsMsg>;
|
|
19
|
+
}
|
|
20
|
+
interface NatsBrokerOptions {
|
|
21
|
+
/** Queue group name for load-balanced consumption across instances */
|
|
22
|
+
queue?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Create a NATS broker driver from a NatsConnection.
|
|
26
|
+
*
|
|
27
|
+
* Uses NATS native request-reply for zero-overhead correlation.
|
|
28
|
+
* Supports queue groups for horizontal scaling — only one instance
|
|
29
|
+
* in the group handles each request.
|
|
30
|
+
*/
|
|
31
|
+
declare function natsBroker(nc: NatsConnection, options?: NatsBrokerOptions): BrokerDriver;
|
|
32
|
+
//#endregion
|
|
33
|
+
export { NatsBrokerOptions, NatsConnection, NatsMsg, NatsSub, natsBroker };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
//#region src/broker/nats.ts
|
|
2
|
+
const encoder = /* @__PURE__ */ new TextEncoder();
|
|
3
|
+
const decoder = /* @__PURE__ */ new TextDecoder();
|
|
4
|
+
/**
|
|
5
|
+
* Create a NATS broker driver from a NatsConnection.
|
|
6
|
+
*
|
|
7
|
+
* Uses NATS native request-reply for zero-overhead correlation.
|
|
8
|
+
* Supports queue groups for horizontal scaling — only one instance
|
|
9
|
+
* in the group handles each request.
|
|
10
|
+
*/
|
|
11
|
+
function natsBroker(nc, options = {}) {
|
|
12
|
+
return {
|
|
13
|
+
subscribe(subject, handler) {
|
|
14
|
+
const sub = nc.subscribe(subject, {
|
|
15
|
+
queue: options.queue,
|
|
16
|
+
callback: (_err, msg) => {
|
|
17
|
+
handler(decoder.decode(msg.data), (response) => {
|
|
18
|
+
msg.respond(encoder.encode(response));
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
return () => sub.unsubscribe();
|
|
23
|
+
},
|
|
24
|
+
async request(subject, payload, opts) {
|
|
25
|
+
const msg = await nc.request(subject, encoder.encode(payload), { timeout: opts?.timeout ?? 1e4 });
|
|
26
|
+
return decoder.decode(msg.data);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
export { natsBroker };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { BrokerDriver } from "./index.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/broker/redis.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Minimal pub/sub transport — implement this for your Redis client.
|
|
6
|
+
*
|
|
7
|
+
* `subscribe` returns a cleanup function to unsubscribe.
|
|
8
|
+
* Both sync and async returns are supported.
|
|
9
|
+
*/
|
|
10
|
+
interface RedisTransport {
|
|
11
|
+
publish(channel: string, message: string): unknown;
|
|
12
|
+
subscribe(channel: string, handler: (message: string) => void): (() => void) | Promise<() => void>;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Minimal ioredis-compatible interface.
|
|
16
|
+
* Works with `ioredis` and any client that matches this shape.
|
|
17
|
+
*/
|
|
18
|
+
interface IORedisLike {
|
|
19
|
+
publish(channel: string, message: string): Promise<number>;
|
|
20
|
+
subscribe(...channels: string[]): Promise<unknown>;
|
|
21
|
+
unsubscribe(...channels: string[]): Promise<unknown>;
|
|
22
|
+
on(event: 'message', listener: (channel: string, message: string) => void): void;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Create a RedisTransport from two ioredis connections.
|
|
26
|
+
*
|
|
27
|
+
* Redis requires separate connections for publishing and subscribing
|
|
28
|
+
* because `SUBSCRIBE` puts the connection into subscriber mode.
|
|
29
|
+
*
|
|
30
|
+
* @param pub - Publisher connection (regular Redis client)
|
|
31
|
+
* @param sub - Subscriber connection (dedicated for SUBSCRIBE)
|
|
32
|
+
*/
|
|
33
|
+
declare function ioredisTransport(pub: IORedisLike, sub: IORedisLike): RedisTransport;
|
|
34
|
+
interface RedisBrokerOptions {
|
|
35
|
+
/** Unique prefix for inbox channels. Auto-generated if not provided. */
|
|
36
|
+
inbox?: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Create a Redis broker driver from a RedisTransport.
|
|
40
|
+
*
|
|
41
|
+
* Simulates request-reply using temporary inbox channels:
|
|
42
|
+
* 1. Client subscribes to a unique inbox channel
|
|
43
|
+
* 2. Client publishes request with inbox address embedded
|
|
44
|
+
* 3. Server processes request, publishes response to inbox
|
|
45
|
+
* 4. Client receives response, unsubscribes from inbox
|
|
46
|
+
*
|
|
47
|
+
* Wire format: `"inbox-channel\npayload"` (newline separator, no double-serialization)
|
|
48
|
+
*/
|
|
49
|
+
declare function redisBroker(transport: RedisTransport, options?: RedisBrokerOptions): BrokerDriver;
|
|
50
|
+
//#endregion
|
|
51
|
+
export { IORedisLike, RedisBrokerOptions, RedisTransport, ioredisTransport, redisBroker };
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
//#region src/broker/redis.ts
|
|
2
|
+
/**
|
|
3
|
+
* Create a RedisTransport from two ioredis connections.
|
|
4
|
+
*
|
|
5
|
+
* Redis requires separate connections for publishing and subscribing
|
|
6
|
+
* because `SUBSCRIBE` puts the connection into subscriber mode.
|
|
7
|
+
*
|
|
8
|
+
* @param pub - Publisher connection (regular Redis client)
|
|
9
|
+
* @param sub - Subscriber connection (dedicated for SUBSCRIBE)
|
|
10
|
+
*/
|
|
11
|
+
function ioredisTransport(pub, sub) {
|
|
12
|
+
const handlers = /* @__PURE__ */ new Map();
|
|
13
|
+
sub.on("message", (channel, message) => {
|
|
14
|
+
const set = handlers.get(channel);
|
|
15
|
+
if (set) for (const handler of set) handler(message);
|
|
16
|
+
});
|
|
17
|
+
return {
|
|
18
|
+
publish(channel, message) {
|
|
19
|
+
return pub.publish(channel, message);
|
|
20
|
+
},
|
|
21
|
+
async subscribe(channel, handler) {
|
|
22
|
+
let set = handlers.get(channel);
|
|
23
|
+
if (!set) {
|
|
24
|
+
set = /* @__PURE__ */ new Set();
|
|
25
|
+
handlers.set(channel, set);
|
|
26
|
+
await sub.subscribe(channel);
|
|
27
|
+
}
|
|
28
|
+
set.add(handler);
|
|
29
|
+
return () => {
|
|
30
|
+
set.delete(handler);
|
|
31
|
+
if (set.size === 0) {
|
|
32
|
+
handlers.delete(channel);
|
|
33
|
+
sub.unsubscribe(channel);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
let globalSeq = 0;
|
|
40
|
+
/**
|
|
41
|
+
* Create a Redis broker driver from a RedisTransport.
|
|
42
|
+
*
|
|
43
|
+
* Simulates request-reply using temporary inbox channels:
|
|
44
|
+
* 1. Client subscribes to a unique inbox channel
|
|
45
|
+
* 2. Client publishes request with inbox address embedded
|
|
46
|
+
* 3. Server processes request, publishes response to inbox
|
|
47
|
+
* 4. Client receives response, unsubscribes from inbox
|
|
48
|
+
*
|
|
49
|
+
* Wire format: `"inbox-channel\npayload"` (newline separator, no double-serialization)
|
|
50
|
+
*/
|
|
51
|
+
function redisBroker(transport, options = {}) {
|
|
52
|
+
const inboxPrefix = options.inbox ?? `silgi:inbox:${Date.now().toString(36)}:${(++globalSeq).toString(36)}`;
|
|
53
|
+
let requestSeq = 0;
|
|
54
|
+
return {
|
|
55
|
+
async subscribe(subject, handler) {
|
|
56
|
+
return transport.subscribe(subject, (message) => {
|
|
57
|
+
const sep = message.indexOf("\n");
|
|
58
|
+
if (sep === -1) return;
|
|
59
|
+
const replyTo = message.slice(0, sep);
|
|
60
|
+
handler(message.slice(sep + 1), (response) => {
|
|
61
|
+
transport.publish(replyTo, response);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
},
|
|
65
|
+
async request(subject, payload, opts) {
|
|
66
|
+
const inbox = `${inboxPrefix}:${++requestSeq}`;
|
|
67
|
+
const timeout = opts?.timeout ?? 1e4;
|
|
68
|
+
const { promise, resolve, reject } = Promise.withResolvers();
|
|
69
|
+
let cleanup;
|
|
70
|
+
const timer = setTimeout(() => {
|
|
71
|
+
cleanup?.();
|
|
72
|
+
reject(/* @__PURE__ */ new Error(`Broker request timeout: ${subject}`));
|
|
73
|
+
}, timeout);
|
|
74
|
+
try {
|
|
75
|
+
const unsub = await transport.subscribe(inbox, (message) => {
|
|
76
|
+
clearTimeout(timer);
|
|
77
|
+
cleanup?.();
|
|
78
|
+
resolve(message);
|
|
79
|
+
});
|
|
80
|
+
cleanup = typeof unsub === "function" ? unsub : void 0;
|
|
81
|
+
await transport.publish(subject, inbox + "\n" + payload);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
clearTimeout(timer);
|
|
84
|
+
cleanup?.();
|
|
85
|
+
reject(err);
|
|
86
|
+
}
|
|
87
|
+
return promise;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
//#endregion
|
|
92
|
+
export { ioredisTransport, redisBroker };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { AnySchema, InferSchemaInput, InferSchemaOutput } from "./core/schema.mjs";
|
|
2
|
+
import { ErrorDef, Meta, MiddlewareDef, ProcedureDef, ProcedureType, ResolveContext, Route } from "./types.mjs";
|
|
3
|
+
|
|
4
|
+
//#region src/builder.d.ts
|
|
5
|
+
/** Initial builder — no input, no output, no errors yet */
|
|
6
|
+
interface ProcedureBuilder<TType extends ProcedureType, TBaseCtx extends Record<string, unknown>, TCtx extends Record<string, unknown> = TBaseCtx, TInput = undefined, TErrors extends ErrorDef = {}> {
|
|
7
|
+
/** Add middleware (guards and wraps) */
|
|
8
|
+
$use<_TReturn extends Record<string, unknown> | void>(...middleware: readonly MiddlewareDef[]): ProcedureBuilder<TType, TBaseCtx, TCtx, TInput, TErrors>;
|
|
9
|
+
/** Set input schema */
|
|
10
|
+
$input<TSchema extends AnySchema>(schema: TSchema): ProcedureBuilder<TType, TBaseCtx, TCtx, InferSchemaOutput<TSchema>, TErrors>;
|
|
11
|
+
/** Set output schema — enables return type autocomplete */
|
|
12
|
+
$output<TSchema extends AnySchema>(schema: TSchema): ProcedureBuilderWithOutput<TType, TBaseCtx, TCtx, TInput, InferSchemaInput<TSchema>, TErrors>;
|
|
13
|
+
/** Set typed errors */
|
|
14
|
+
$errors<TNewErrors extends ErrorDef>(errors: TNewErrors): ProcedureBuilder<TType, TBaseCtx, TCtx, TInput, TNewErrors & TErrors>;
|
|
15
|
+
/** Set route metadata */
|
|
16
|
+
$route(route: Route): ProcedureBuilder<TType, TBaseCtx, TCtx, TInput, TErrors>;
|
|
17
|
+
/** Set custom metadata */
|
|
18
|
+
$meta(meta: Meta): ProcedureBuilder<TType, TBaseCtx, TCtx, TInput, TErrors>;
|
|
19
|
+
/** Resolve — freely inferred return type (no output schema) */
|
|
20
|
+
$resolve<TOutput>(fn: TType extends 'subscription' ? (opts: ResolveContext<TCtx, TInput, TErrors>) => AsyncIterableIterator<TOutput> : (opts: ResolveContext<TCtx, TInput, TErrors>) => Promise<TOutput> | TOutput): ProcedureDef<TType, TInput, TOutput, TErrors>;
|
|
21
|
+
}
|
|
22
|
+
/** Builder after .$output() — return type is constrained for autocomplete */
|
|
23
|
+
interface ProcedureBuilderWithOutput<TType extends ProcedureType, TBaseCtx extends Record<string, unknown>, TCtx extends Record<string, unknown>, TInput, TOutputResolved, TErrors extends ErrorDef> {
|
|
24
|
+
/** Add middleware */
|
|
25
|
+
$use(...middleware: readonly MiddlewareDef[]): ProcedureBuilderWithOutput<TType, TBaseCtx, TCtx, TInput, TOutputResolved, TErrors>;
|
|
26
|
+
/** Set typed errors */
|
|
27
|
+
$errors<TNewErrors extends ErrorDef>(errors: TNewErrors): ProcedureBuilderWithOutput<TType, TBaseCtx, TCtx, TInput, TOutputResolved, TNewErrors & TErrors>;
|
|
28
|
+
/** Set route metadata */
|
|
29
|
+
$route(route: Route): ProcedureBuilderWithOutput<TType, TBaseCtx, TCtx, TInput, TOutputResolved, TErrors>;
|
|
30
|
+
/** Set custom metadata */
|
|
31
|
+
$meta(meta: Meta): ProcedureBuilderWithOutput<TType, TBaseCtx, TCtx, TInput, TOutputResolved, TErrors>;
|
|
32
|
+
/** Resolve — return type constrained to output schema (autocomplete works) */
|
|
33
|
+
$resolve(fn: TType extends 'subscription' ? (opts: ResolveContext<TCtx, TInput, TErrors>) => AsyncIterableIterator<TOutputResolved> : (opts: ResolveContext<TCtx, TInput, TErrors>) => Promise<TOutputResolved> | TOutputResolved): ProcedureDef<TType, TInput, TOutputResolved, TErrors>;
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
export { ProcedureBuilder, ProcedureBuilderWithOutput };
|
package/dist/builder.mjs
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
//#region src/builder.ts
|
|
2
|
+
var ProcBuilder = class {
|
|
3
|
+
type;
|
|
4
|
+
input = null;
|
|
5
|
+
output = null;
|
|
6
|
+
errors = null;
|
|
7
|
+
use = null;
|
|
8
|
+
resolve = null;
|
|
9
|
+
route = null;
|
|
10
|
+
meta = null;
|
|
11
|
+
constructor(type) {
|
|
12
|
+
this.type = type;
|
|
13
|
+
}
|
|
14
|
+
$use(...middleware) {
|
|
15
|
+
if (this.use) this.use.push(...middleware);
|
|
16
|
+
else this.use = middleware;
|
|
17
|
+
return this;
|
|
18
|
+
}
|
|
19
|
+
$input(schema) {
|
|
20
|
+
this.input = schema;
|
|
21
|
+
return this;
|
|
22
|
+
}
|
|
23
|
+
$output(schema) {
|
|
24
|
+
this.output = schema;
|
|
25
|
+
return this;
|
|
26
|
+
}
|
|
27
|
+
$errors(errors) {
|
|
28
|
+
this.errors = this.errors ? {
|
|
29
|
+
...this.errors,
|
|
30
|
+
...errors
|
|
31
|
+
} : errors;
|
|
32
|
+
return this;
|
|
33
|
+
}
|
|
34
|
+
$route(route) {
|
|
35
|
+
this.route = route;
|
|
36
|
+
return this;
|
|
37
|
+
}
|
|
38
|
+
$meta(meta) {
|
|
39
|
+
this.meta = meta;
|
|
40
|
+
return this;
|
|
41
|
+
}
|
|
42
|
+
$resolve(fn) {
|
|
43
|
+
this.resolve = fn;
|
|
44
|
+
return this;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
function createProcedureBuilder(type) {
|
|
48
|
+
return new ProcBuilder(type);
|
|
49
|
+
}
|
|
50
|
+
//#endregion
|
|
51
|
+
export { createProcedureBuilder };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ErrorDef, ProcedureDef } from "./types.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/callable.d.ts
|
|
4
|
+
interface CallableOptions<TCtx extends Record<string, unknown>> {
|
|
5
|
+
/** Context factory — called on every invocation */
|
|
6
|
+
context: () => TCtx | Promise<TCtx>;
|
|
7
|
+
}
|
|
8
|
+
type CallableFn<TInput, TOutput> = undefined extends TInput ? (input?: TInput) => Promise<TOutput> : (input: TInput) => Promise<TOutput>;
|
|
9
|
+
/**
|
|
10
|
+
* Convert a ProcedureDef into a directly callable async function.
|
|
11
|
+
*
|
|
12
|
+
* Compiles the full pipeline (guards, wraps, validation) once,
|
|
13
|
+
* then each call runs the compiled handler directly — no HTTP overhead.
|
|
14
|
+
*/
|
|
15
|
+
declare function callable<TInput, TOutput, TErrors extends ErrorDef, TCtx extends Record<string, unknown>>(procedure: ProcedureDef<any, TInput, TOutput, TErrors>, options: CallableOptions<TCtx>): CallableFn<TInput, TOutput>;
|
|
16
|
+
//#endregion
|
|
17
|
+
export { CallableOptions, callable };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { compileProcedure } from "./compile.mjs";
|
|
2
|
+
//#region src/callable.ts
|
|
3
|
+
/**
|
|
4
|
+
* callable() — turn a procedure into a directly invocable function.
|
|
5
|
+
*
|
|
6
|
+
* Useful for server-side code where you want to call a procedure
|
|
7
|
+
* without going through HTTP or the client proxy.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* const getUsers = k
|
|
12
|
+
* .$input(z.object({ limit: z.number() }))
|
|
13
|
+
* .$resolve(({ input, ctx }) => ctx.db.users.findMany({ take: input.limit }))
|
|
14
|
+
*
|
|
15
|
+
* const fn = callable(getUsers, {
|
|
16
|
+
* context: () => ({ db: getDB() }),
|
|
17
|
+
* })
|
|
18
|
+
*
|
|
19
|
+
* // Call it directly — no HTTP, no serialization
|
|
20
|
+
* const users = await fn({ limit: 10 })
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* Convert a ProcedureDef into a directly callable async function.
|
|
25
|
+
*
|
|
26
|
+
* Compiles the full pipeline (guards, wraps, validation) once,
|
|
27
|
+
* then each call runs the compiled handler directly — no HTTP overhead.
|
|
28
|
+
*/
|
|
29
|
+
function callable(procedure, options) {
|
|
30
|
+
const handler = compileProcedure(procedure);
|
|
31
|
+
const contextFactory = options.context;
|
|
32
|
+
const signal = new AbortController().signal;
|
|
33
|
+
return (async (input) => {
|
|
34
|
+
const ctx = await contextFactory();
|
|
35
|
+
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);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
//#endregion
|
|
42
|
+
export { callable };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ClientContext, ClientLink, ClientOptions } from "../../types.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/client/adapters/fetch/index.d.ts
|
|
4
|
+
interface RPCLinkOptions<TClientContext extends ClientContext = ClientContext> {
|
|
5
|
+
url: string | URL;
|
|
6
|
+
headers?: Record<string, string> | ((options: ClientOptions<TClientContext>) => Record<string, string>);
|
|
7
|
+
fetch?: typeof globalThis.fetch;
|
|
8
|
+
method?: 'GET' | 'POST';
|
|
9
|
+
maxUrlLength?: number;
|
|
10
|
+
}
|
|
11
|
+
declare class RPCLink<TClientContext extends ClientContext = ClientContext> implements ClientLink<TClientContext> {
|
|
12
|
+
#private;
|
|
13
|
+
constructor(options: RPCLinkOptions<TClientContext>);
|
|
14
|
+
call(path: readonly string[], input: unknown, options: ClientOptions<TClientContext>): Promise<unknown>;
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
17
|
+
export { RPCLink, RPCLinkOptions };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { SilgiError, fromSilgiErrorJSON, isErrorStatus, isSilgiErrorJSON } from "../../../core/error.mjs";
|
|
2
|
+
import { parseEmptyableJSON, stringifyJSON } from "../../../core/utils.mjs";
|
|
3
|
+
//#region src/client/adapters/fetch/index.ts
|
|
4
|
+
/**
|
|
5
|
+
* Fetch transport — HTTP client for browser and Node.js.
|
|
6
|
+
*/
|
|
7
|
+
var RPCLink = class {
|
|
8
|
+
#baseUrl;
|
|
9
|
+
#headers;
|
|
10
|
+
#fetch;
|
|
11
|
+
#method;
|
|
12
|
+
#maxUrlLength;
|
|
13
|
+
constructor(options) {
|
|
14
|
+
this.#baseUrl = typeof options.url === "string" ? options.url : options.url.href;
|
|
15
|
+
if (this.#baseUrl.endsWith("/")) this.#baseUrl = this.#baseUrl.slice(0, -1);
|
|
16
|
+
this.#headers = options.headers;
|
|
17
|
+
this.#fetch = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
18
|
+
this.#method = options.method ?? "POST";
|
|
19
|
+
this.#maxUrlLength = options.maxUrlLength ?? 2083;
|
|
20
|
+
}
|
|
21
|
+
async call(path, input, options) {
|
|
22
|
+
const urlPath = path.map(encodeURIComponent).join("/");
|
|
23
|
+
let url = `${this.#baseUrl}/${urlPath}`;
|
|
24
|
+
const headers = { ...typeof this.#headers === "function" ? this.#headers(options) : this.#headers };
|
|
25
|
+
let method = this.#method;
|
|
26
|
+
let body;
|
|
27
|
+
const hasInput = input !== void 0 && input !== null;
|
|
28
|
+
if (method === "GET" && hasInput) {
|
|
29
|
+
const data = stringifyJSON(input);
|
|
30
|
+
const candidateUrl = `${url}?data=${encodeURIComponent(data)}`;
|
|
31
|
+
if (candidateUrl.length <= this.#maxUrlLength) url = candidateUrl;
|
|
32
|
+
else {
|
|
33
|
+
method = "POST";
|
|
34
|
+
headers["content-type"] = "application/json";
|
|
35
|
+
body = data;
|
|
36
|
+
}
|
|
37
|
+
} else if (hasInput) {
|
|
38
|
+
headers["content-type"] = "application/json";
|
|
39
|
+
body = stringifyJSON(input);
|
|
40
|
+
}
|
|
41
|
+
const response = await this.#fetch(url, {
|
|
42
|
+
method,
|
|
43
|
+
headers,
|
|
44
|
+
body,
|
|
45
|
+
signal: options.signal
|
|
46
|
+
});
|
|
47
|
+
const responseText = await response.text();
|
|
48
|
+
const responseBody = responseText ? parseEmptyableJSON(responseText) : void 0;
|
|
49
|
+
if (isErrorStatus(response.status)) {
|
|
50
|
+
if (isSilgiErrorJSON(responseBody)) throw fromSilgiErrorJSON(responseBody);
|
|
51
|
+
throw new SilgiError("INTERNAL_SERVER_ERROR", {
|
|
52
|
+
status: response.status,
|
|
53
|
+
message: `HTTP ${response.status}`,
|
|
54
|
+
data: responseBody
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return responseBody;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
//#endregion
|
|
61
|
+
export { RPCLink };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { ClientContext, ClientLink, ClientOptions } from "../../types.mjs";
|
|
2
|
+
import { FetchContext, FetchOptions } from "ofetch";
|
|
3
|
+
|
|
4
|
+
//#region src/client/adapters/ofetch/index.d.ts
|
|
5
|
+
interface SilgiLinkOptions<TClientContext extends ClientContext = ClientContext> {
|
|
6
|
+
/** Server base URL (e.g. "http://localhost:3000") */
|
|
7
|
+
url: string;
|
|
8
|
+
/** Static headers or dynamic header factory */
|
|
9
|
+
headers?: Record<string, string> | ((options: ClientOptions<TClientContext>) => Record<string, string>);
|
|
10
|
+
/** Retry count for failed requests (default: 1 for queries, 0 for mutations) */
|
|
11
|
+
retry?: number | false;
|
|
12
|
+
/** Retry delay in ms, or function for backoff (default: 0) */
|
|
13
|
+
retryDelay?: number | ((ctx: FetchContext) => number);
|
|
14
|
+
/** Timeout in ms (default: 30000) */
|
|
15
|
+
timeout?: number;
|
|
16
|
+
/** Use MessagePack binary protocol (2-4x faster, ~50% smaller payloads) */
|
|
17
|
+
binary?: boolean;
|
|
18
|
+
/** Use devalue protocol (preserves Date, Map, Set, BigInt, circular refs) */
|
|
19
|
+
devalue?: boolean;
|
|
20
|
+
/** ofetch interceptors */
|
|
21
|
+
onRequest?: FetchOptions['onRequest'];
|
|
22
|
+
onResponse?: FetchOptions['onResponse'];
|
|
23
|
+
onRequestError?: FetchOptions['onRequestError'];
|
|
24
|
+
onResponseError?: FetchOptions['onResponseError'];
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Create a Silgi client link powered by ofetch.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* import { createClient } from "silgi/client"
|
|
32
|
+
* import { createLink } from "silgi/client/ofetch"
|
|
33
|
+
*
|
|
34
|
+
* const link = createLink({ url: "http://localhost:3000" })
|
|
35
|
+
* const client = createClient<AppRouter>(link)
|
|
36
|
+
* const users = await client.users.list({ limit: 10 })
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
declare function createLink<TClientContext extends ClientContext = ClientContext>(options: SilgiLinkOptions<TClientContext>): ClientLink<TClientContext>;
|
|
40
|
+
//#endregion
|
|
41
|
+
export { SilgiLinkOptions, createLink };
|