@mandujs/core 0.29.1 → 0.31.0
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/package.json +5 -1
- package/src/bundler/analyzer.ts +843 -0
- package/src/client/index.ts +11 -1
- package/src/client/rpc.ts +293 -140
- package/src/config/mandu.ts +117 -0
- package/src/config/validate.ts +92 -0
- package/src/contract/index.ts +18 -0
- package/src/contract/rpc.ts +443 -0
- package/src/filling/context.ts +166 -0
- package/src/filling/filling.ts +10 -1
- package/src/index.ts +32 -0
- package/src/middleware/index.ts +7 -0
- package/src/middleware/scheduler-cron.ts +96 -0
- package/src/observability/index.ts +26 -0
- package/src/observability/tracing.ts +694 -0
- package/src/runtime/cache.ts +197 -13
- package/src/runtime/index.ts +8 -0
- package/src/runtime/server.ts +542 -24
- package/src/scheduler/index.ts +547 -343
- package/src/scheduler/validate.ts +169 -0
package/src/client/index.ts
CHANGED
|
@@ -112,7 +112,17 @@ export { Link, NavLink, type LinkProps, type NavLinkProps } from "./Link";
|
|
|
112
112
|
export { Form, type FormProps, type FormState } from "./Form";
|
|
113
113
|
|
|
114
114
|
// RPC Client
|
|
115
|
-
export {
|
|
115
|
+
export {
|
|
116
|
+
createClient,
|
|
117
|
+
RpcError,
|
|
118
|
+
type RpcMethods,
|
|
119
|
+
type RpcRequestOptions,
|
|
120
|
+
type RpcClientOptions,
|
|
121
|
+
// Phase 18.κ — typed RPC proxy
|
|
122
|
+
createRpcClient,
|
|
123
|
+
RpcCallError,
|
|
124
|
+
type CreateRpcClientOptions,
|
|
125
|
+
} from "./rpc";
|
|
116
126
|
|
|
117
127
|
// useFetch Composable
|
|
118
128
|
export { useFetch, type UseFetchOptions, type UseFetchReturn } from "./use-fetch";
|
package/src/client/rpc.ts
CHANGED
|
@@ -1,140 +1,293 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mandu RPC Client
|
|
3
|
-
* Contract 정의에서 타입 안전한 API 클라이언트 생성
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
// ========== Types ==========
|
|
7
|
-
|
|
8
|
-
/** RPC 클라이언트 에러 */
|
|
9
|
-
export class RpcError extends Error {
|
|
10
|
-
constructor(
|
|
11
|
-
public readonly status: number,
|
|
12
|
-
public readonly body: unknown
|
|
13
|
-
) {
|
|
14
|
-
super(`API Error ${status}`);
|
|
15
|
-
this.name = "RpcError";
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface RpcRequestOptions {
|
|
20
|
-
query?: Record<string, unknown>;
|
|
21
|
-
body?: unknown;
|
|
22
|
-
params?: Record<string, string>;
|
|
23
|
-
headers?: Record<string, string>;
|
|
24
|
-
signal?: AbortSignal;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface RpcClientOptions {
|
|
28
|
-
/** API base URL (기본: 현재 origin) */
|
|
29
|
-
baseUrl?: string;
|
|
30
|
-
/** 공통 헤더 */
|
|
31
|
-
headers?: Record<string, string>;
|
|
32
|
-
/** 커스텀 fetch (테스트용) */
|
|
33
|
-
fetch?: typeof globalThis.fetch;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// ========== Implementation ==========
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Contract 기반 타입 안전 RPC 클라이언트 생성
|
|
40
|
-
*
|
|
41
|
-
* @example
|
|
42
|
-
* ```typescript
|
|
43
|
-
* import { createClient } from "@mandujs/core/client";
|
|
44
|
-
* import type todoContract from "../spec/contracts/api-todos.contract";
|
|
45
|
-
*
|
|
46
|
-
* const api = createClient<typeof todoContract>("/api/todos");
|
|
47
|
-
*
|
|
48
|
-
* // 타입 추론 동작
|
|
49
|
-
* const { todos } = await api.get({ query: { page: 2 } });
|
|
50
|
-
* const { id } = await api.post({ body: { title: "New" } });
|
|
51
|
-
* ```
|
|
52
|
-
*/
|
|
53
|
-
export function createClient<TContract = unknown>(
|
|
54
|
-
path: string,
|
|
55
|
-
options?: RpcClientOptions
|
|
56
|
-
): RpcMethods {
|
|
57
|
-
const baseFetch = options?.fetch ?? globalThis.fetch;
|
|
58
|
-
const baseUrl = options?.baseUrl ?? "";
|
|
59
|
-
const baseHeaders = options?.headers ?? {};
|
|
60
|
-
|
|
61
|
-
function makeRequest(method: string) {
|
|
62
|
-
return async (input?: RpcRequestOptions): Promise<unknown> => {
|
|
63
|
-
const url = new URL(`${baseUrl}${path}`, typeof window !== "undefined" ? window.location.origin : "http://localhost");
|
|
64
|
-
|
|
65
|
-
// URL 파라미터 치환
|
|
66
|
-
if (input?.params) {
|
|
67
|
-
let resolvedPath = url.pathname;
|
|
68
|
-
for (const [key, value] of Object.entries(input.params)) {
|
|
69
|
-
resolvedPath = resolvedPath.replace(`:${key}`, encodeURIComponent(value));
|
|
70
|
-
}
|
|
71
|
-
// 미해결 파라미터 검출
|
|
72
|
-
if (resolvedPath.includes(":")) {
|
|
73
|
-
throw new RpcError(0, `Unresolved path params in "${resolvedPath}". Check your params object.`);
|
|
74
|
-
}
|
|
75
|
-
url.pathname = resolvedPath;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Query 파라미터
|
|
79
|
-
if (input?.query) {
|
|
80
|
-
for (const [key, value] of Object.entries(input.query)) {
|
|
81
|
-
if (value !== undefined && value !== null) {
|
|
82
|
-
url.searchParams.set(key, String(value));
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const headers: Record<string, string> = {
|
|
88
|
-
...baseHeaders,
|
|
89
|
-
...input?.headers,
|
|
90
|
-
"Accept": "application/json",
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
const fetchOptions: RequestInit = {
|
|
94
|
-
method: method.toUpperCase(),
|
|
95
|
-
headers,
|
|
96
|
-
signal: input?.signal,
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
// Body (GET/HEAD 제외)
|
|
100
|
-
if (input?.body && method !== "GET" && method !== "HEAD") {
|
|
101
|
-
fetchOptions.body = JSON.stringify(input.body);
|
|
102
|
-
headers["Content-Type"] = "application/json";
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const response = await baseFetch(url.toString(), fetchOptions);
|
|
106
|
-
|
|
107
|
-
if (!response.ok) {
|
|
108
|
-
let body: unknown;
|
|
109
|
-
try {
|
|
110
|
-
body = await response.json();
|
|
111
|
-
} catch {
|
|
112
|
-
body = await response.text().catch(() => null);
|
|
113
|
-
}
|
|
114
|
-
throw new RpcError(response.status, body);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const contentType = response.headers.get("content-type") ?? "";
|
|
118
|
-
if (contentType.includes("application/json")) {
|
|
119
|
-
return response.json();
|
|
120
|
-
}
|
|
121
|
-
return response.text();
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
return {
|
|
126
|
-
get: makeRequest("GET"),
|
|
127
|
-
post: makeRequest("POST"),
|
|
128
|
-
put: makeRequest("PUT"),
|
|
129
|
-
patch: makeRequest("PATCH"),
|
|
130
|
-
delete: makeRequest("DELETE"),
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
export interface RpcMethods {
|
|
135
|
-
get: (input?: RpcRequestOptions) => Promise<unknown>;
|
|
136
|
-
post: (input?: RpcRequestOptions) => Promise<unknown>;
|
|
137
|
-
put: (input?: RpcRequestOptions) => Promise<unknown>;
|
|
138
|
-
patch: (input?: RpcRequestOptions) => Promise<unknown>;
|
|
139
|
-
delete: (input?: RpcRequestOptions) => Promise<unknown>;
|
|
140
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Mandu RPC Client
|
|
3
|
+
* Contract 정의에서 타입 안전한 API 클라이언트 생성
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// ========== Types ==========
|
|
7
|
+
|
|
8
|
+
/** RPC 클라이언트 에러 */
|
|
9
|
+
export class RpcError extends Error {
|
|
10
|
+
constructor(
|
|
11
|
+
public readonly status: number,
|
|
12
|
+
public readonly body: unknown
|
|
13
|
+
) {
|
|
14
|
+
super(`API Error ${status}`);
|
|
15
|
+
this.name = "RpcError";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface RpcRequestOptions {
|
|
20
|
+
query?: Record<string, unknown>;
|
|
21
|
+
body?: unknown;
|
|
22
|
+
params?: Record<string, string>;
|
|
23
|
+
headers?: Record<string, string>;
|
|
24
|
+
signal?: AbortSignal;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface RpcClientOptions {
|
|
28
|
+
/** API base URL (기본: 현재 origin) */
|
|
29
|
+
baseUrl?: string;
|
|
30
|
+
/** 공통 헤더 */
|
|
31
|
+
headers?: Record<string, string>;
|
|
32
|
+
/** 커스텀 fetch (테스트용) */
|
|
33
|
+
fetch?: typeof globalThis.fetch;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ========== Implementation ==========
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Contract 기반 타입 안전 RPC 클라이언트 생성
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```typescript
|
|
43
|
+
* import { createClient } from "@mandujs/core/client";
|
|
44
|
+
* import type todoContract from "../spec/contracts/api-todos.contract";
|
|
45
|
+
*
|
|
46
|
+
* const api = createClient<typeof todoContract>("/api/todos");
|
|
47
|
+
*
|
|
48
|
+
* // 타입 추론 동작
|
|
49
|
+
* const { todos } = await api.get({ query: { page: 2 } });
|
|
50
|
+
* const { id } = await api.post({ body: { title: "New" } });
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export function createClient<TContract = unknown>(
|
|
54
|
+
path: string,
|
|
55
|
+
options?: RpcClientOptions
|
|
56
|
+
): RpcMethods {
|
|
57
|
+
const baseFetch = options?.fetch ?? globalThis.fetch;
|
|
58
|
+
const baseUrl = options?.baseUrl ?? "";
|
|
59
|
+
const baseHeaders = options?.headers ?? {};
|
|
60
|
+
|
|
61
|
+
function makeRequest(method: string) {
|
|
62
|
+
return async (input?: RpcRequestOptions): Promise<unknown> => {
|
|
63
|
+
const url = new URL(`${baseUrl}${path}`, typeof window !== "undefined" ? window.location.origin : "http://localhost");
|
|
64
|
+
|
|
65
|
+
// URL 파라미터 치환
|
|
66
|
+
if (input?.params) {
|
|
67
|
+
let resolvedPath = url.pathname;
|
|
68
|
+
for (const [key, value] of Object.entries(input.params)) {
|
|
69
|
+
resolvedPath = resolvedPath.replace(`:${key}`, encodeURIComponent(value));
|
|
70
|
+
}
|
|
71
|
+
// 미해결 파라미터 검출
|
|
72
|
+
if (resolvedPath.includes(":")) {
|
|
73
|
+
throw new RpcError(0, `Unresolved path params in "${resolvedPath}". Check your params object.`);
|
|
74
|
+
}
|
|
75
|
+
url.pathname = resolvedPath;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Query 파라미터
|
|
79
|
+
if (input?.query) {
|
|
80
|
+
for (const [key, value] of Object.entries(input.query)) {
|
|
81
|
+
if (value !== undefined && value !== null) {
|
|
82
|
+
url.searchParams.set(key, String(value));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const headers: Record<string, string> = {
|
|
88
|
+
...baseHeaders,
|
|
89
|
+
...input?.headers,
|
|
90
|
+
"Accept": "application/json",
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const fetchOptions: RequestInit = {
|
|
94
|
+
method: method.toUpperCase(),
|
|
95
|
+
headers,
|
|
96
|
+
signal: input?.signal,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// Body (GET/HEAD 제외)
|
|
100
|
+
if (input?.body && method !== "GET" && method !== "HEAD") {
|
|
101
|
+
fetchOptions.body = JSON.stringify(input.body);
|
|
102
|
+
headers["Content-Type"] = "application/json";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const response = await baseFetch(url.toString(), fetchOptions);
|
|
106
|
+
|
|
107
|
+
if (!response.ok) {
|
|
108
|
+
let body: unknown;
|
|
109
|
+
try {
|
|
110
|
+
body = await response.json();
|
|
111
|
+
} catch {
|
|
112
|
+
body = await response.text().catch(() => null);
|
|
113
|
+
}
|
|
114
|
+
throw new RpcError(response.status, body);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
118
|
+
if (contentType.includes("application/json")) {
|
|
119
|
+
return response.json();
|
|
120
|
+
}
|
|
121
|
+
return response.text();
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
get: makeRequest("GET"),
|
|
127
|
+
post: makeRequest("POST"),
|
|
128
|
+
put: makeRequest("PUT"),
|
|
129
|
+
patch: makeRequest("PATCH"),
|
|
130
|
+
delete: makeRequest("DELETE"),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface RpcMethods {
|
|
135
|
+
get: (input?: RpcRequestOptions) => Promise<unknown>;
|
|
136
|
+
post: (input?: RpcRequestOptions) => Promise<unknown>;
|
|
137
|
+
put: (input?: RpcRequestOptions) => Promise<unknown>;
|
|
138
|
+
patch: (input?: RpcRequestOptions) => Promise<unknown>;
|
|
139
|
+
delete: (input?: RpcRequestOptions) => Promise<unknown>;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
143
|
+
// Phase 18.κ — Typed RPC Client (tRPC-like)
|
|
144
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
145
|
+
//
|
|
146
|
+
// `createRpcClient<typeof postsRpc>()` returns a Proxy whose properties
|
|
147
|
+
// are the RPC procedure names (`list`, `get`, …). Each access produces
|
|
148
|
+
// an async function whose input type is the procedure's Zod input
|
|
149
|
+
// type and whose return type is the procedure's Zod output type.
|
|
150
|
+
//
|
|
151
|
+
// Wire protocol:
|
|
152
|
+
// POST <baseUrl>/<method>
|
|
153
|
+
// body: { "input": <value> }
|
|
154
|
+
// response: { "ok": true, "data": <value> }
|
|
155
|
+
// | { "ok": false, "error": { code, message, issues? } }
|
|
156
|
+
//
|
|
157
|
+
// Errors throw {@link RpcCallError} with the structured fields
|
|
158
|
+
// preserved so UI code can surface field-level validation issues.
|
|
159
|
+
|
|
160
|
+
import type { RpcClient, RpcDefinition, RpcProcedureRecord, RpcWireEnvelope, RpcWireError } from "../contract/rpc";
|
|
161
|
+
|
|
162
|
+
/** Options passed to {@link createRpcClient}. */
|
|
163
|
+
export interface CreateRpcClientOptions {
|
|
164
|
+
/**
|
|
165
|
+
* Absolute or site-relative base URL for the RPC endpoint —
|
|
166
|
+
* typically `/api/rpc/<name>`. The method name is appended.
|
|
167
|
+
*/
|
|
168
|
+
baseUrl: string;
|
|
169
|
+
/** Extra headers sent with every call. */
|
|
170
|
+
headers?: Record<string, string>;
|
|
171
|
+
/** Custom fetch (e.g. test double or node-fetch polyfill). */
|
|
172
|
+
fetch?: typeof globalThis.fetch;
|
|
173
|
+
/**
|
|
174
|
+
* Optional per-call AbortSignal factory. Useful when the proxy is
|
|
175
|
+
* shared across React components that want independent cancellation.
|
|
176
|
+
*/
|
|
177
|
+
signal?: AbortSignal;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Typed error thrown by {@link createRpcClient} calls on non-OK
|
|
182
|
+
* envelopes. Carries the wire-level {@link RpcWireError} + HTTP
|
|
183
|
+
* status so callers can distinguish validation vs. handler errors
|
|
184
|
+
* without string-matching on `message`.
|
|
185
|
+
*/
|
|
186
|
+
export class RpcCallError extends Error {
|
|
187
|
+
constructor(
|
|
188
|
+
public readonly status: number,
|
|
189
|
+
public readonly error: RpcWireError
|
|
190
|
+
) {
|
|
191
|
+
super(`[Mandu RPC] ${error.code}: ${error.message}`);
|
|
192
|
+
this.name = "RpcCallError";
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Machine-readable code (forwarded from the server). */
|
|
196
|
+
get code(): string {
|
|
197
|
+
return this.error.code;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Field-level issues, if any. */
|
|
201
|
+
get issues(): RpcWireError["issues"] {
|
|
202
|
+
return this.error.issues;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Create a typed RPC client from an `RpcDefinition` type import.
|
|
208
|
+
*
|
|
209
|
+
* The implementation uses a `Proxy` so no codegen step is required —
|
|
210
|
+
* TypeScript infers call signatures from the imported `typeof` at
|
|
211
|
+
* compile time, and at runtime every property access produces a
|
|
212
|
+
* fetch wrapper.
|
|
213
|
+
*
|
|
214
|
+
* @example
|
|
215
|
+
* ```ts
|
|
216
|
+
* import { createRpcClient } from "@mandujs/core/client";
|
|
217
|
+
* import type { postsRpc } from "../server/rpc/posts";
|
|
218
|
+
*
|
|
219
|
+
* const api = createRpcClient<typeof postsRpc>({ baseUrl: "/api/rpc/posts" });
|
|
220
|
+
* const posts = await api.list({ limit: 20 }); // fully typed
|
|
221
|
+
* const post = await api.get({ id: "abc" }); // fully typed
|
|
222
|
+
* ```
|
|
223
|
+
*
|
|
224
|
+
* Type-check failures at the call site are real compile errors:
|
|
225
|
+
* `api.list({ limit: "not-a-number" })` is a TS2322.
|
|
226
|
+
*/
|
|
227
|
+
export function createRpcClient<TDef extends RpcDefinition<RpcProcedureRecord>>(
|
|
228
|
+
options: CreateRpcClientOptions
|
|
229
|
+
): RpcClient<TDef> {
|
|
230
|
+
const baseFetch = options.fetch ?? globalThis.fetch;
|
|
231
|
+
const baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
232
|
+
const baseHeaders = options.headers ?? {};
|
|
233
|
+
|
|
234
|
+
const call = async (method: string, input: unknown): Promise<unknown> => {
|
|
235
|
+
const url = `${baseUrl}/${method}`;
|
|
236
|
+
const payload = input === undefined ? { input: undefined } : { input };
|
|
237
|
+
const response = await baseFetch(url, {
|
|
238
|
+
method: "POST",
|
|
239
|
+
headers: {
|
|
240
|
+
"Content-Type": "application/json",
|
|
241
|
+
Accept: "application/json",
|
|
242
|
+
...baseHeaders,
|
|
243
|
+
},
|
|
244
|
+
body: JSON.stringify(payload),
|
|
245
|
+
signal: options.signal,
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Prefer JSON parse (every legitimate RPC response is JSON), but
|
|
249
|
+
// tolerate non-JSON failure paths (e.g. upstream proxy error).
|
|
250
|
+
let envelope: RpcWireEnvelope | null = null;
|
|
251
|
+
const text = await response.text();
|
|
252
|
+
try {
|
|
253
|
+
envelope = text.length > 0 ? (JSON.parse(text) as RpcWireEnvelope) : null;
|
|
254
|
+
} catch {
|
|
255
|
+
envelope = null;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (envelope && envelope.ok === true) {
|
|
259
|
+
return envelope.data;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Non-OK path — throw a structured error. Prefer server-emitted
|
|
263
|
+
// envelope; fall back to a synthesized one for transport errors.
|
|
264
|
+
let error: RpcWireError;
|
|
265
|
+
if (envelope && envelope.ok === false) {
|
|
266
|
+
error = envelope.error;
|
|
267
|
+
} else {
|
|
268
|
+
error = {
|
|
269
|
+
code: response.ok ? "BAD_RESPONSE" : `HTTP_${response.status}`,
|
|
270
|
+
message:
|
|
271
|
+
text.length > 0
|
|
272
|
+
? text.slice(0, 500)
|
|
273
|
+
: `RPC call to ${url} failed with HTTP ${response.status}`,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
throw new RpcCallError(response.status, error);
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
// A Proxy on a plain object: every property access returns a
|
|
280
|
+
// pre-curried call fn. Symbol keys (`Symbol.toStringTag`, etc.) fall
|
|
281
|
+
// through so `console.log(api)` does not throw.
|
|
282
|
+
const target = Object.create(null) as Record<string, unknown>;
|
|
283
|
+
return new Proxy(target, {
|
|
284
|
+
get(_t, prop) {
|
|
285
|
+
if (typeof prop !== "string") return undefined;
|
|
286
|
+
// Allow common non-method inspection hooks to no-op.
|
|
287
|
+
if (prop === "then") return undefined; // not thenable
|
|
288
|
+
if (prop === "toJSON") return undefined;
|
|
289
|
+
return (input?: unknown) => call(prop, input);
|
|
290
|
+
},
|
|
291
|
+
}) as RpcClient<TDef>;
|
|
292
|
+
}
|
|
293
|
+
|
package/src/config/mandu.ts
CHANGED
|
@@ -3,6 +3,8 @@ import { readJsonFile } from "../utils/bun";
|
|
|
3
3
|
import type { ManduAdapter } from "../runtime/adapter";
|
|
4
4
|
import type { ManduPlugin, ManduHooks } from "../plugins/hooks";
|
|
5
5
|
import type { Middleware } from "../middleware/define";
|
|
6
|
+
import type { RpcDefinition, RpcProcedureRecord } from "../contract/rpc";
|
|
7
|
+
import type { CronDef } from "../scheduler";
|
|
6
8
|
|
|
7
9
|
export type GuardRuleSeverity = "error" | "warn" | "warning" | "off";
|
|
8
10
|
|
|
@@ -170,6 +172,16 @@ export interface ManduConfig {
|
|
|
170
172
|
minify?: boolean;
|
|
171
173
|
sourcemap?: boolean;
|
|
172
174
|
splitting?: boolean;
|
|
175
|
+
/**
|
|
176
|
+
* Phase 18.η — emit `.mandu/analyze/report.html` + `report.json` after
|
|
177
|
+
* a successful build. Equivalent to `mandu build --analyze`. Default:
|
|
178
|
+
* `false`. Report artefacts are self-contained (no CDN, no external
|
|
179
|
+
* JS) and safe to commit to a private dashboard or inspect locally.
|
|
180
|
+
*
|
|
181
|
+
* The CLI `--analyze` flag wins over this field; `--analyze=json`
|
|
182
|
+
* writes JSON only (useful for CI, skips the HTML render cost).
|
|
183
|
+
*/
|
|
184
|
+
analyze?: boolean;
|
|
173
185
|
};
|
|
174
186
|
dev?: {
|
|
175
187
|
hmr?: boolean;
|
|
@@ -286,6 +298,64 @@ export interface ManduConfig {
|
|
|
286
298
|
heapEndpoint?: boolean;
|
|
287
299
|
/** `/_mandu/metrics` Prometheus text exposure toggle. */
|
|
288
300
|
metricsEndpoint?: boolean;
|
|
301
|
+
/**
|
|
302
|
+
* Phase 18.θ — OpenTelemetry-compatible request tracing.
|
|
303
|
+
*
|
|
304
|
+
* When enabled, every request opens a root server span
|
|
305
|
+
* (`http.request`) that chains child spans for middleware, loader,
|
|
306
|
+
* SSR, and sandbox execution. The root span's trace-id is
|
|
307
|
+
* propagated across AsyncLocalStorage and stamped onto outgoing
|
|
308
|
+
* fetches via `traceparent`.
|
|
309
|
+
*
|
|
310
|
+
* Exporters:
|
|
311
|
+
* - `"console"` (default) — pretty-prints spans to stderr, useful
|
|
312
|
+
* in `mandu dev`.
|
|
313
|
+
* - `"otlp"` — POSTs OTLP/HTTP JSON to `endpoint/v1/traces`.
|
|
314
|
+
* Compatible with Honeycomb, Grafana Tempo, AWS X-Ray (via the
|
|
315
|
+
* OTel Collector), and the standalone OpenTelemetry Collector.
|
|
316
|
+
*
|
|
317
|
+
* Setting the `MANDU_OTEL_ENDPOINT` env var overrides both
|
|
318
|
+
* `enabled` and `exporter` at runtime (shortcut for ops that want
|
|
319
|
+
* to enable tracing without a config change).
|
|
320
|
+
*
|
|
321
|
+
* Default: disabled (zero overhead when `observability.tracing` is
|
|
322
|
+
* omitted).
|
|
323
|
+
*/
|
|
324
|
+
tracing?: {
|
|
325
|
+
enabled?: boolean;
|
|
326
|
+
exporter?: "console" | "otlp";
|
|
327
|
+
endpoint?: string;
|
|
328
|
+
headers?: Record<string, string>;
|
|
329
|
+
serviceName?: string;
|
|
330
|
+
};
|
|
331
|
+
};
|
|
332
|
+
/**
|
|
333
|
+
* Phase 18.ζ — ISR / tag-based cache invalidation.
|
|
334
|
+
*
|
|
335
|
+
* - `defaultMaxAge` : fresh TTL (seconds) applied when a loader
|
|
336
|
+
* does not emit its own `_cache` / `ctx.cache`
|
|
337
|
+
* metadata. Set to a positive integer to enable
|
|
338
|
+
* automatic caching across every non-dynamic
|
|
339
|
+
* route (Next.js `export const revalidate`
|
|
340
|
+
* equivalent). Default `undefined` (no auto).
|
|
341
|
+
* - `defaultSwr` : stale-while-revalidate window (seconds)
|
|
342
|
+
* appended after the fresh TTL. Serves stale
|
|
343
|
+
* HTML instantly while background regeneration
|
|
344
|
+
* runs. Default `0`.
|
|
345
|
+
* - `maxEntries` : LRU bound for the in-memory store. Default
|
|
346
|
+
* `1000`.
|
|
347
|
+
* - `store` : backend. Currently `"memory"` only; reserved
|
|
348
|
+
* for future `"redis"` adapter.
|
|
349
|
+
*
|
|
350
|
+
* Disable entirely by omitting this block. Revalidation APIs live in
|
|
351
|
+
* `@mandujs/core/runtime`: `revalidate(tag)`, `revalidateTag(tag)`,
|
|
352
|
+
* `revalidatePath(path)`.
|
|
353
|
+
*/
|
|
354
|
+
cache?: {
|
|
355
|
+
defaultMaxAge?: number;
|
|
356
|
+
defaultSwr?: number;
|
|
357
|
+
maxEntries?: number;
|
|
358
|
+
store?: "memory";
|
|
289
359
|
};
|
|
290
360
|
plugins?: ManduPlugin[];
|
|
291
361
|
hooks?: Partial<ManduHooks>;
|
|
@@ -305,6 +375,53 @@ export interface ManduConfig {
|
|
|
305
375
|
* @see `docs/architect/middleware-composition.md`
|
|
306
376
|
*/
|
|
307
377
|
middleware?: Middleware[];
|
|
378
|
+
/**
|
|
379
|
+
* Phase 18.κ — tRPC-like typed RPC endpoints.
|
|
380
|
+
*
|
|
381
|
+
* Keys become URL segments: `endpoints.posts` is served from
|
|
382
|
+
* `/api/rpc/posts/<method>`. Each value is a `defineRpc()` result —
|
|
383
|
+
* a tagged object carrying Zod input/output schemas and handler
|
|
384
|
+
* functions. The runtime dispatcher (`runtime/server.ts`) registers
|
|
385
|
+
* every declared endpoint at `startServer()` time and validates
|
|
386
|
+
* both request inputs and handler outputs against the schemas.
|
|
387
|
+
*
|
|
388
|
+
* Wire protocol: `POST /api/rpc/<name>/<method>` with JSON body
|
|
389
|
+
* `{ "input": <value> }`; returns `{ ok: true, data }` or
|
|
390
|
+
* `{ ok: false, error: { code, message, issues? } }`.
|
|
391
|
+
*
|
|
392
|
+
* Client: `createRpcClient<typeof postsRpc>({ baseUrl: "/api/rpc/posts" })`.
|
|
393
|
+
*
|
|
394
|
+
* @see `docs/architect/typed-rpc.md`
|
|
395
|
+
* @see `@mandujs/core/contract/rpc` for `defineRpc`.
|
|
396
|
+
* @see `@mandujs/core/client/rpc` for `createRpcClient`.
|
|
397
|
+
*/
|
|
398
|
+
rpc?: {
|
|
399
|
+
endpoints?: Record<string, RpcDefinition<RpcProcedureRecord>>;
|
|
400
|
+
};
|
|
401
|
+
/**
|
|
402
|
+
* Phase 18.λ — declarative cron job scheduler.
|
|
403
|
+
*
|
|
404
|
+
* `scheduler.jobs` is an array of {@link CronDef} objects (from
|
|
405
|
+
* `@mandujs/core/scheduler`). At `startServer()` boot time the runtime
|
|
406
|
+
* filters the set by `runOn === "bun"` (or `runOn` omitted) and registers
|
|
407
|
+
* each surviving job with `Bun.cron`. At build time, when
|
|
408
|
+
* `--target=workers` is set, the CLI filters by `runOn === "workers"` (or
|
|
409
|
+
* omitted) and emits the schedule strings into the generated
|
|
410
|
+
* `wrangler.toml` `[triggers] crons = [...]` block.
|
|
411
|
+
*
|
|
412
|
+
* Schedule strings are validated synchronously at `defineCron` time, so
|
|
413
|
+
* malformed crontabs fail boot instead of silently never firing.
|
|
414
|
+
*
|
|
415
|
+
* `scheduler.disabled` is an escape hatch for environments where cron
|
|
416
|
+
* should not fire (e.g., a read-only replica). Default: `false`.
|
|
417
|
+
*
|
|
418
|
+
* @see `docs/architect/cron-scheduler.md`
|
|
419
|
+
* @see `@mandujs/core/scheduler` for `defineCron`.
|
|
420
|
+
*/
|
|
421
|
+
scheduler?: {
|
|
422
|
+
jobs?: CronDef[];
|
|
423
|
+
disabled?: boolean;
|
|
424
|
+
};
|
|
308
425
|
}
|
|
309
426
|
|
|
310
427
|
export const CONFIG_FILES = [
|