silgi 0.1.0-beta.10 → 0.1.0-beta.12

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.
@@ -36,8 +36,8 @@ interface LinkOptions<TClientContext extends ClientContext = ClientContext> {
36
36
  onResponse?: FetchOptions['onResponse'];
37
37
  onRequestError?: FetchOptions['onRequestError'];
38
38
  onResponseError?: FetchOptions['onResponseError'];
39
- /** Route metadata — pass extractRoutes(router) or the full router. Required when procedures use $route({ path }) */
40
- routes?: unknown;
39
+ /** Route metadata — pass extractRoutes(router), the full router, or a Promise that resolves to routes. Required when procedures use $route({ path }) */
40
+ routes?: unknown | Promise<unknown>;
41
41
  }
42
42
  /**
43
43
  * Create a Silgi client link powered by ofetch.
@@ -28,9 +28,15 @@ function createLink(options) {
28
28
  const defaultRetry = options.retry;
29
29
  const defaultRetryDelay = options.retryDelay ?? 0;
30
30
  const resolvedProtocol = options.protocol ?? (options.binary ? "messagepack" : void 0) ?? (options.devalue ? "devalue" : void 0) ?? "json";
31
- const routes = options.routes;
31
+ let resolvedRoutes = void 0;
32
+ const routesOption = options.routes;
33
+ if (routesOption && typeof routesOption?.then === "function") routesOption.then((r) => {
34
+ resolvedRoutes = r;
35
+ });
36
+ else resolvedRoutes = routesOption;
32
37
  return { async call(path, input, callOptions) {
33
- const resolved = routes ? resolveRoute(routes, path) : void 0;
38
+ if (routesOption && !resolvedRoutes && typeof routesOption?.then === "function") resolvedRoutes = await routesOption;
39
+ const resolved = resolvedRoutes ? resolveRoute(resolvedRoutes, path) : void 0;
34
40
  let urlPath = resolved ? resolved.path : "/" + path.map(encodeURIComponent).join("/");
35
41
  if (resolved) {
36
42
  const sub = substituteParams(urlPath, input);
@@ -30,7 +30,58 @@ type ImplementRouter<T extends ContractRouter> = { [K in keyof T]: T[K] extends
30
30
  * Type-safe: implementation must match the contract.
31
31
  */
32
32
  declare function implement<T extends ContractRouter>(contractDef: T, implementations: ImplementRouter<T>): RouterDef;
33
+ declare class ContractBuilder {
34
+ private _route?;
35
+ private _input?;
36
+ private _output?;
37
+ private _errors?;
38
+ private _type?;
39
+ private _description?;
40
+ route(r: Route): this;
41
+ input<T extends AnySchema>(schema: T): ContractBuilder & {
42
+ _input: T;
43
+ };
44
+ output<T extends AnySchema>(schema: T): ContractBuilder & {
45
+ _output: T;
46
+ };
47
+ errors<T extends ErrorDef>(e: T): ContractBuilder & {
48
+ _errors: T;
49
+ };
50
+ description(d: string): this;
51
+ /** Build the procedure contract */
52
+ build(): ProcedureContract;
53
+ }
54
+ /**
55
+ * Contract builder — oRPC-style chainable API for defining procedure contracts.
56
+ *
57
+ * @example
58
+ * ```ts
59
+ * import { oc } from 'silgi/contract'
60
+ *
61
+ * const listUsers = oc
62
+ * .route({ method: 'GET', path: '/api/users' })
63
+ * .input(z.object({ limit: z.number().optional() }))
64
+ * .output(z.array(UserSchema))
65
+ * .build()
66
+ * ```
67
+ */
68
+ declare const oc: ContractBuilder;
69
+ /**
70
+ * Strip all non-metadata from a contract or router, preserving only
71
+ * { route: { path, method }, type } for each procedure.
72
+ * Safe to serialize to JSON and ship to client bundles.
73
+ *
74
+ * oRPC equivalent: `minifyContractRouter(router)`
75
+ *
76
+ * @example
77
+ * ```ts
78
+ * import { minifyContractRouter } from 'silgi/contract'
79
+ * const minified = minifyContractRouter(appRouter)
80
+ * fs.writeFileSync('routes.json', JSON.stringify(minified))
81
+ * ```
82
+ */
83
+ declare function minifyContractRouter(def: unknown): Record<string, unknown>;
33
84
  /** Infer client type from a contract (no server code needed) */
34
85
  type InferContractClient<T extends ContractRouter> = { [K in keyof T]: T[K] extends ProcedureContract<any, infer TInput, infer TOutput> ? TInput extends AnySchema ? (input: InferSchemaInput<TInput>) => Promise<TOutput extends AnySchema ? InferSchemaOutput<TOutput> : unknown> : () => Promise<TOutput extends AnySchema ? InferSchemaOutput<TOutput> : unknown> : T[K] extends ContractRouter ? InferContractClient<T[K]> : never };
35
86
  //#endregion
36
- export { ContractRouter, InferContractClient, ProcedureContract, contract, implement };
87
+ export { ContractRouter, InferContractClient, ProcedureContract, contract, implement, minifyContractRouter, oc };
package/dist/contract.mjs CHANGED
@@ -36,5 +36,94 @@ function isProcedureContract(v) {
36
36
  if (typeof v !== "object" || v === null) return false;
37
37
  return "input" in v || "output" in v || "type" in v || "errors" in v;
38
38
  }
39
+ var ContractBuilder = class {
40
+ _route;
41
+ _input;
42
+ _output;
43
+ _errors;
44
+ _type;
45
+ _description;
46
+ route(r) {
47
+ this._route = {
48
+ ...this._route,
49
+ ...r
50
+ };
51
+ return this;
52
+ }
53
+ input(schema) {
54
+ this._input = schema;
55
+ return this;
56
+ }
57
+ output(schema) {
58
+ this._output = schema;
59
+ return this;
60
+ }
61
+ errors(e) {
62
+ this._errors = e;
63
+ return this;
64
+ }
65
+ description(d) {
66
+ this._description = d;
67
+ return this;
68
+ }
69
+ /** Build the procedure contract */
70
+ build() {
71
+ return {
72
+ type: this._type,
73
+ input: this._input,
74
+ output: this._output,
75
+ errors: this._errors,
76
+ route: this._route,
77
+ description: this._description
78
+ };
79
+ }
80
+ };
81
+ /**
82
+ * Contract builder — oRPC-style chainable API for defining procedure contracts.
83
+ *
84
+ * @example
85
+ * ```ts
86
+ * import { oc } from 'silgi/contract'
87
+ *
88
+ * const listUsers = oc
89
+ * .route({ method: 'GET', path: '/api/users' })
90
+ * .input(z.object({ limit: z.number().optional() }))
91
+ * .output(z.array(UserSchema))
92
+ * .build()
93
+ * ```
94
+ */
95
+ const oc = new ContractBuilder();
96
+ /**
97
+ * Strip all non-metadata from a contract or router, preserving only
98
+ * { route: { path, method }, type } for each procedure.
99
+ * Safe to serialize to JSON and ship to client bundles.
100
+ *
101
+ * oRPC equivalent: `minifyContractRouter(router)`
102
+ *
103
+ * @example
104
+ * ```ts
105
+ * import { minifyContractRouter } from 'silgi/contract'
106
+ * const minified = minifyContractRouter(appRouter)
107
+ * fs.writeFileSync('routes.json', JSON.stringify(minified))
108
+ * ```
109
+ */
110
+ function minifyContractRouter(def) {
111
+ const result = {};
112
+ if (def == null || typeof def !== "object") return result;
113
+ for (const [key, value] of Object.entries(def)) {
114
+ if (typeof value !== "object" || value === null) continue;
115
+ const obj = value;
116
+ const route = obj.route;
117
+ if (route?.path) result[key] = {
118
+ path: route.path,
119
+ method: (route.method ?? "POST").toUpperCase()
120
+ };
121
+ else if ("resolve" in obj || "input" in obj || "output" in obj || "type" in obj) {} else {
122
+ const nested = minifyContractRouter(value);
123
+ if (Object.keys(nested).length > 0) result[key] = nested;
124
+ }
125
+ }
126
+ return result;
127
+ }
39
128
  //#endregion
40
- export { contract, implement };
129
+ export { contract, implement, minifyContractRouter, oc };
@@ -120,6 +120,10 @@ function createFetchHandler(routerDef, contextFactory, hooks) {
120
120
  const reqTrace = analyticsTraceMap.get(request);
121
121
  if (reqTrace) ctx.__analyticsTrace = reqTrace;
122
122
  if (!route.passthrough) rawInput = await parseInput(request, url, qMark);
123
+ if (match.params && Object.keys(match.params).length > 0) rawInput = rawInput != null && typeof rawInput === "object" ? {
124
+ ...match.params,
125
+ ...rawInput
126
+ } : match.params;
123
127
  callHook("request", {
124
128
  path: pathname,
125
129
  input: rawInput
@@ -31,18 +31,26 @@ function assignPaths(def, prefix = []) {
31
31
  function extractRoutes(def, prefix = []) {
32
32
  const result = {};
33
33
  if (def == null || typeof def !== "object") return result;
34
- for (const [key, value] of Object.entries(def)) if (isProcedureDef(value)) {
35
- const route = value.route;
34
+ for (const [key, value] of Object.entries(def)) {
35
+ const route = getRouteFromEntry(value);
36
36
  if (route?.path) result[key] = {
37
37
  path: route.path,
38
38
  method: (route.method ?? "POST").toUpperCase()
39
39
  };
40
- } else if (typeof value === "object" && value !== null) {
41
- const nested = extractRoutes(value, [...prefix, key]);
42
- if (Object.keys(nested).length > 0) result[key] = nested;
40
+ else if (typeof value === "object" && value !== null && !isProcedureDef(value)) {
41
+ const nested = extractRoutes(value, [...prefix, key]);
42
+ if (Object.keys(nested).length > 0) result[key] = nested;
43
+ }
43
44
  }
44
45
  return result;
45
46
  }
47
+ /** Extract route metadata from a ProcedureDef or ProcedureContract */
48
+ function getRouteFromEntry(value) {
49
+ if (typeof value !== "object" || value === null) return void 0;
50
+ const obj = value;
51
+ if (isProcedureDef(value)) return obj.route;
52
+ if ("route" in obj && typeof obj.route === "object" && obj.route !== null) return obj.route;
53
+ }
46
54
  /** Check if a value is an extracted route leaf (has path + method, not a nested object) */
47
55
  function isExtractedRoute(value) {
48
56
  return typeof value === "object" && value !== null && "path" in value && "method" in value && !("resolve" in value);
@@ -55,14 +63,11 @@ function resolveRoute(routes, path) {
55
63
  current = current[segment];
56
64
  }
57
65
  if (isExtractedRoute(current)) return current;
58
- if (isProcedureDef(current)) {
59
- const route = current.route;
60
- if (!route?.path) return void 0;
61
- return {
62
- path: route.path,
63
- method: (route.method ?? "POST").toUpperCase()
64
- };
65
- }
66
+ const route = getRouteFromEntry(current);
67
+ if (route?.path) return {
68
+ path: route.path,
69
+ method: (route.method ?? "POST").toUpperCase()
70
+ };
66
71
  }
67
72
  /**
68
73
  * Substitute :param placeholders in a route path with values from input.
package/dist/silgi.mjs CHANGED
@@ -142,12 +142,20 @@ function silgi(config) {
142
142
  serve: async (routerDef, options) => {
143
143
  const { createServeHandler } = await import("./core/serve.mjs");
144
144
  const server = await createServeHandler(routerDef, contextFactory, hooks, options);
145
- const { collectCronTasks, startCronJobs } = await import("./core/task.mjs");
145
+ const { collectCronTasks, startCronJobs, stopCronJobs } = await import("./core/task.mjs");
146
146
  const cronTasks = collectCronTasks(routerDef);
147
147
  if (cronTasks.length > 0) {
148
148
  await startCronJobs(cronTasks);
149
149
  console.log(` ${cronTasks.length} cron task(s) scheduled`);
150
150
  }
151
+ const originalClose = server.close.bind(server);
152
+ server.close = async (force) => {
153
+ stopCronJobs();
154
+ return originalClose(force);
155
+ };
156
+ const onSignal = () => stopCronJobs();
157
+ process.once("SIGINT", onSignal);
158
+ process.once("SIGTERM", onSignal);
151
159
  return server;
152
160
  }
153
161
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "silgi",
3
- "version": "0.1.0-beta.10",
3
+ "version": "0.1.0-beta.12",
4
4
  "private": false,
5
5
  "description": "The fastest end-to-end type-safe RPC framework for TypeScript — compiled pipelines, single package, every runtime",
6
6
  "keywords": [
@@ -270,6 +270,7 @@
270
270
  "ocache": "^0.1.4",
271
271
  "ofetch": "2.0.0-alpha.3",
272
272
  "ohash": "^2.0.11",
273
+ "silgi": "link:",
273
274
  "srvx": "^0.11.13",
274
275
  "unstorage": "^2.0.0-alpha.7"
275
276
  },
@@ -333,7 +334,7 @@
333
334
  "packageManager": "pnpm@10.33.0",
334
335
  "pnpm": {
335
336
  "overrides": {
336
- "silgi": "workspace:*"
337
+ "silgi": "link:"
337
338
  }
338
339
  }
339
340
  }