@taujs/server 0.4.1 → 0.4.3

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.
@@ -23,16 +23,17 @@ interface Logs extends BaseLogger {
23
23
  isDebugEnabled(category: DebugCategory): boolean;
24
24
  }
25
25
 
26
- type Schema<T> = (input: unknown) => T;
27
- type LooseSpec = Readonly<Record<string, ServiceMethod<any, Record<string, unknown>> | {
28
- handler: ServiceMethod<any, Record<string, unknown>>;
29
- params?: Schema<any>;
30
- result?: Schema<any>;
31
- parsers?: {
32
- params?: Schema<any>;
33
- result?: Schema<any>;
34
- };
35
- }>>;
26
+ type RegistryCaller<R extends ServiceRegistry = ServiceRegistry> = (serviceName: keyof R & string, methodName: string, args?: JsonObject) => Promise<JsonObject>;
27
+ type JsonPrimitive = string | number | boolean | null;
28
+ type JsonValue = JsonPrimitive | JsonValue[] | {
29
+ [k: string]: JsonValue;
30
+ };
31
+ type JsonObject = {
32
+ [k: string]: JsonValue;
33
+ };
34
+ type NarrowSchema<T> = {
35
+ parse: (u: unknown) => T;
36
+ } | ((u: unknown) => T);
36
37
  type ServiceContext = {
37
38
  signal?: AbortSignal;
38
39
  deadlineMs?: number;
@@ -42,20 +43,26 @@ type ServiceContext = {
42
43
  id: string;
43
44
  roles: string[];
44
45
  } | null;
46
+ call?: (service: string, method: string, args?: JsonObject) => Promise<JsonObject>;
45
47
  };
46
- type ServiceMethod<P, R extends Record<string, unknown>> = (params: P, ctx: ServiceContext) => Promise<R>;
47
- type ServiceMethodDescriptor<P, R extends Record<string, unknown>> = {
48
- handler: ServiceMethod<P, R>;
49
- parsers?: {
50
- params?: Schema<P>;
51
- result?: Schema<R>;
52
- };
48
+ declare function withDeadline(signal: AbortSignal | undefined, ms?: number): AbortSignal | undefined;
49
+ type ServiceMethod<P, R extends JsonObject = JsonObject> = (params: P, ctx: ServiceContext) => Promise<R>;
50
+ type ServiceDefinition = Readonly<Record<string, ServiceMethod<any, JsonObject>>>;
51
+ type ServiceRegistry = Readonly<Record<string, ServiceDefinition>>;
52
+ type ServiceDescriptor = {
53
+ serviceName: string;
54
+ serviceMethod: string;
55
+ args?: JsonObject;
53
56
  };
54
- type ServiceRegistry = Readonly<Record<string, Readonly<Record<string, ServiceMethodDescriptor<any, Record<string, unknown>>>>>>;
55
- declare const defineService: <T extends LooseSpec>(spec: T) => { [K in keyof T]: T[K] extends ServiceMethod<infer P, infer R> ? ServiceMethodDescriptor<P, R> : T[K] extends {
57
+ declare function defineService<T extends Record<string, ServiceMethod<any, JsonObject> | {
58
+ handler: ServiceMethod<any, JsonObject>;
59
+ params?: NarrowSchema<any>;
60
+ result?: NarrowSchema<any>;
61
+ }>>(spec: T): { [K in keyof T]: T[K] extends ServiceMethod<infer P, infer R> ? ServiceMethod<P, R> : T[K] extends {
56
62
  handler: ServiceMethod<infer P, infer R_1>;
57
- } ? ServiceMethodDescriptor<P, R_1> : never; };
63
+ } ? ServiceMethod<P, R_1> : never; };
58
64
  declare const defineServiceRegistry: <R extends ServiceRegistry>(registry: R) => R;
65
+ declare function callServiceMethod(registry: ServiceRegistry, serviceName: string, methodName: string, params: JsonObject | undefined, ctx: ServiceContext): Promise<JsonObject>;
59
66
 
60
67
  type RequestContext<L extends Logs = Logs> = {
61
68
  traceId: string;
@@ -73,6 +80,7 @@ type RouteCSPConfig = {
73
80
  req: FastifyRequest;
74
81
  }) => CSPDirectives);
75
82
  generateCSP?: (directives: CSPDirectives, nonce: string, req: FastifyRequest) => string;
83
+ reportOnly?: boolean;
76
84
  };
77
85
  type BaseMiddleware = {
78
86
  auth?: {
@@ -82,13 +90,12 @@ type BaseMiddleware = {
82
90
  };
83
91
  csp?: RouteCSPConfig | false;
84
92
  };
85
- type ServiceCall = {
86
- serviceName: string;
87
- serviceMethod: string;
88
- args?: Record<string, unknown>;
93
+ type DataResult = Record<string, unknown> | ServiceDescriptor;
94
+ type RequestServiceContext<L extends Logs = Logs> = RequestContext<L> & {
95
+ call: RegistryCaller<ServiceRegistry>;
96
+ headers?: Record<string, string>;
89
97
  };
90
- type DataResult = Record<string, unknown> | ServiceCall;
91
- type DataHandler<Params extends PathToRegExpParams, L extends Logs = Logs> = (params: Params, ctx: RequestContext<L> & {
98
+ type DataHandler<Params extends PathToRegExpParams, L extends Logs = Logs> = (params: Params, ctx: RequestServiceContext<L> & {
92
99
  [key: string]: unknown;
93
100
  }) => Promise<DataResult>;
94
101
  type PathToRegExpParams = Partial<Record<string, string | string[]>>;
@@ -129,6 +136,35 @@ type CSPViolationReport = {
129
136
  disposition?: 'enforce' | 'report';
130
137
  };
131
138
 
139
+ type ErrorKind = 'infra' | 'upstream' | 'domain' | 'validation' | 'auth' | 'canceled' | 'timeout';
140
+ declare class AppError extends Error {
141
+ readonly kind: ErrorKind;
142
+ readonly httpStatus: number;
143
+ readonly details?: unknown;
144
+ readonly safeMessage: string;
145
+ readonly code?: string;
146
+ constructor(message: string, kind: ErrorKind, options?: {
147
+ httpStatus?: number;
148
+ details?: unknown;
149
+ cause?: unknown;
150
+ safeMessage?: string;
151
+ code?: string;
152
+ });
153
+ private getSafeMessage;
154
+ private serialiseValue;
155
+ toJSON(): any;
156
+ static notFound(message: string, details?: unknown, code?: string): AppError;
157
+ static forbidden(message: string, details?: unknown, code?: string): AppError;
158
+ static badRequest(message: string, details?: unknown, code?: string): AppError;
159
+ static unprocessable(message: string, details?: unknown, code?: string): AppError;
160
+ static timeout(message: string, details?: unknown, code?: string): AppError;
161
+ static canceled(message: string, details?: unknown, code?: string): AppError;
162
+ static internal(message: string, cause?: unknown, details?: unknown, code?: string): AppError;
163
+ static upstream(message: string, cause?: unknown, details?: unknown, code?: string): AppError;
164
+ static serviceUnavailable(message: string, cause?: unknown, details?: unknown, code?: string): AppError;
165
+ static from(err: unknown, fallback?: string): AppError;
166
+ }
167
+
132
168
  /**
133
169
  * τjs [ taujs ] Orchestration System
134
170
  * (c) 2024-present Aoede Ltd
@@ -161,15 +197,15 @@ type AppConfig = {
161
197
  routes?: AppRoute[];
162
198
  };
163
199
  type TaujsConfig = {
200
+ apps: AppConfig[];
201
+ security?: SecurityConfig;
164
202
  server?: {
165
203
  host?: string;
166
204
  port?: number;
167
205
  hmrPort?: number;
168
206
  };
169
- security?: SecurityConfig;
170
- apps: AppConfig[];
171
207
  };
172
208
 
173
209
  declare function defineConfig<T extends TaujsConfig>(config: T): T;
174
210
 
175
- export { type AppConfig as A, type BaseLogger as B, type DebugConfig as D, type InitialRouteParams as I, type ServiceRegistry as S, type TaujsConfig as T, type SecurityConfig as a, type AppRoute as b, defineServiceRegistry as c, defineConfig as d, defineService as e };
211
+ export { type AppConfig as A, type BaseLogger as B, type DebugConfig as D, type InitialRouteParams as I, type RegistryCaller as R, type ServiceRegistry as S, type TaujsConfig as T, type SecurityConfig as a, type AppRoute as b, callServiceMethod as c, defineConfig as d, defineService as e, defineServiceRegistry as f, type ServiceContext as g, AppError as h, withDeadline as w };
package/dist/Config.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  import 'fastify';
2
2
  import 'vite';
3
- export { A as AppConfig, b as AppRoute, a as SecurityConfig, T as TaujsConfig, d as defineConfig, e as defineService, c as defineServiceRegistry } from './Config-LCDjtT9m.js';
3
+ export { A as AppConfig, h as AppError, b as AppRoute, R as RegistryCaller, a as SecurityConfig, g as ServiceContext, T as TaujsConfig, c as callServiceMethod, d as defineConfig, e as defineService, f as defineServiceRegistry, w as withDeadline } from './Config-CEhfq8Mm.js';
package/dist/Config.js CHANGED
@@ -1,19 +1,186 @@
1
1
  // src/utils/DataServices.ts
2
- var defineService = (spec) => {
2
+ import { performance } from "perf_hooks";
3
+
4
+ // src/logging/AppError.ts
5
+ var HTTP_STATUS = {
6
+ infra: 500,
7
+ upstream: 502,
8
+ domain: 404,
9
+ validation: 400,
10
+ auth: 403,
11
+ canceled: 499,
12
+ // Client Closed Request (nginx convention)
13
+ timeout: 504
14
+ };
15
+ var AppError = class _AppError extends Error {
16
+ kind;
17
+ httpStatus;
18
+ details;
19
+ safeMessage;
20
+ code;
21
+ constructor(message, kind, options = {}) {
22
+ super(message);
23
+ this.name = "AppError";
24
+ Object.setPrototypeOf(this, new.target.prototype);
25
+ if (options.cause !== void 0) {
26
+ Object.defineProperty(this, "cause", {
27
+ value: options.cause,
28
+ enumerable: false,
29
+ writable: false,
30
+ configurable: true
31
+ });
32
+ }
33
+ this.kind = kind;
34
+ this.httpStatus = options.httpStatus ?? HTTP_STATUS[kind];
35
+ this.details = options.details;
36
+ this.safeMessage = options.safeMessage ?? this.getSafeMessage(kind, message);
37
+ this.code = options.code;
38
+ if (Error.captureStackTrace) Error.captureStackTrace(this, this.constructor);
39
+ }
40
+ getSafeMessage(kind, message) {
41
+ return kind === "domain" || kind === "validation" || kind === "auth" ? message : "Internal Server Error";
42
+ }
43
+ serialiseValue(value, seen = /* @__PURE__ */ new WeakSet()) {
44
+ if (value === null || value === void 0) return value;
45
+ if (typeof value !== "object") return value;
46
+ if (seen.has(value)) return "[circular]";
47
+ seen.add(value);
48
+ if (value instanceof Error) {
49
+ return {
50
+ name: value.name,
51
+ message: value.message,
52
+ stack: value.stack,
53
+ ...value instanceof _AppError && {
54
+ kind: value.kind,
55
+ httpStatus: value.httpStatus,
56
+ code: value.code
57
+ }
58
+ };
59
+ }
60
+ if (Array.isArray(value)) return value.map((item) => this.serialiseValue(item, seen));
61
+ const result = {};
62
+ for (const [key, val] of Object.entries(value)) {
63
+ result[key] = this.serialiseValue(val, seen);
64
+ }
65
+ return result;
66
+ }
67
+ toJSON() {
68
+ return {
69
+ name: this.name,
70
+ kind: this.kind,
71
+ message: this.message,
72
+ safeMessage: this.safeMessage,
73
+ httpStatus: this.httpStatus,
74
+ ...this.code && { code: this.code },
75
+ details: this.serialiseValue(this.details),
76
+ stack: this.stack,
77
+ ...this.cause && {
78
+ cause: this.serialiseValue(this.cause)
79
+ }
80
+ };
81
+ }
82
+ static notFound(message, details, code) {
83
+ return new _AppError(message, "domain", { httpStatus: 404, details, code });
84
+ }
85
+ static forbidden(message, details, code) {
86
+ return new _AppError(message, "auth", { httpStatus: 403, details, code });
87
+ }
88
+ static badRequest(message, details, code) {
89
+ return new _AppError(message, "validation", { httpStatus: 400, details, code });
90
+ }
91
+ static unprocessable(message, details, code) {
92
+ return new _AppError(message, "validation", { httpStatus: 422, details, code });
93
+ }
94
+ static timeout(message, details, code) {
95
+ return new _AppError(message, "timeout", { details, code });
96
+ }
97
+ static canceled(message, details, code) {
98
+ return new _AppError(message, "canceled", { details, code });
99
+ }
100
+ static internal(message, cause, details, code) {
101
+ return new _AppError(message, "infra", { cause, details, code });
102
+ }
103
+ static upstream(message, cause, details, code) {
104
+ return new _AppError(message, "upstream", { cause, details, code });
105
+ }
106
+ static serviceUnavailable(message, cause, details, code) {
107
+ return new _AppError(message, "infra", { httpStatus: 503, cause, details, code });
108
+ }
109
+ static from(err, fallback = "Internal error") {
110
+ return err instanceof _AppError ? err : _AppError.internal(err?.message ?? fallback, err);
111
+ }
112
+ };
113
+
114
+ // src/utils/DataServices.ts
115
+ var runSchema = (schema, input) => {
116
+ if (!schema) return input;
117
+ return typeof schema.parse === "function" ? schema.parse(input) : schema(input);
118
+ };
119
+ function withDeadline(signal, ms) {
120
+ if (!ms) return signal;
121
+ const ctrl = new AbortController();
122
+ const onAbort = () => ctrl.abort(signal?.reason ?? new Error("Aborted"));
123
+ signal?.addEventListener("abort", onAbort, { once: true });
124
+ const t = setTimeout(() => ctrl.abort(new Error("DeadlineExceeded")), ms);
125
+ ctrl.signal.addEventListener(
126
+ "abort",
127
+ () => {
128
+ clearTimeout(t);
129
+ signal?.removeEventListener("abort", onAbort);
130
+ },
131
+ { once: true }
132
+ );
133
+ return ctrl.signal;
134
+ }
135
+ function defineService(spec) {
3
136
  const out = {};
4
- for (const [k, v] of Object.entries(spec)) {
137
+ for (const [name, v] of Object.entries(spec)) {
5
138
  if (typeof v === "function") {
6
- out[k] = { handler: v };
139
+ out[name] = v;
7
140
  } else {
8
- out[k] = {
9
- handler: v.handler,
10
- parsers: v.parsers ?? (v.params || v.result ? { params: v.params, result: v.result } : void 0)
141
+ const { handler, params: paramsSchema, result: resultSchema } = v;
142
+ out[name] = async (params, ctx) => {
143
+ const p = runSchema(paramsSchema, params);
144
+ const r = await handler(p, ctx);
145
+ return runSchema(resultSchema, r);
11
146
  };
12
147
  }
13
148
  }
14
- return out;
15
- };
16
- var defineServiceRegistry = (registry) => registry;
149
+ return Object.freeze(out);
150
+ }
151
+ var defineServiceRegistry = (registry) => Object.freeze(Object.fromEntries(Object.entries(registry).map(([k, v]) => [k, Object.freeze(v)])));
152
+ async function callServiceMethod(registry, serviceName, methodName, params, ctx) {
153
+ if (ctx.signal?.aborted) throw AppError.timeout("Request canceled");
154
+ const service = registry[serviceName];
155
+ if (!service) throw AppError.notFound(`Unknown service: ${serviceName}`);
156
+ const method = service[methodName];
157
+ if (!method) throw AppError.notFound(`Unknown method: ${serviceName}.${methodName}`);
158
+ const logger = ctx.logger?.child?.({
159
+ component: "service-call",
160
+ service: serviceName,
161
+ method: methodName,
162
+ traceId: ctx.traceId
163
+ });
164
+ const t0 = performance.now();
165
+ try {
166
+ const result = await method(params ?? {}, ctx);
167
+ if (typeof result !== "object" || result === null) {
168
+ throw AppError.internal(`Non-object result from ${serviceName}.${methodName}`);
169
+ }
170
+ logger?.debug?.({ ms: +(performance.now() - t0).toFixed(1) }, "Service method ok");
171
+ return result;
172
+ } catch (err) {
173
+ logger?.error?.(
174
+ {
175
+ params,
176
+ error: err instanceof Error ? { name: err.name, message: err.message, stack: err.stack } : String(err),
177
+ ms: +(performance.now() - t0).toFixed(1)
178
+ },
179
+ "Service method failed"
180
+ );
181
+ throw err instanceof AppError ? err : err instanceof Error ? AppError.internal(err.message, { cause: err }) : AppError.internal("Unknown error", { details: { err } });
182
+ }
183
+ }
17
184
 
18
185
  // src/Config.ts
19
186
  function defineConfig(config) {
@@ -21,7 +188,10 @@ function defineConfig(config) {
21
188
  return config;
22
189
  }
23
190
  export {
191
+ AppError,
192
+ callServiceMethod,
24
193
  defineConfig,
25
194
  defineService,
26
- defineServiceRegistry
195
+ defineServiceRegistry,
196
+ withDeadline
27
197
  };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { FastifyPluginCallback, FastifyPluginAsync, FastifyInstance } from 'fastify';
2
- import { T as TaujsConfig, S as ServiceRegistry, D as DebugConfig, B as BaseLogger, A as AppConfig } from './Config-LCDjtT9m.js';
3
- export { I as InitialRouteParams } from './Config-LCDjtT9m.js';
4
- import 'vite';
2
+ import { T as TaujsConfig, S as ServiceRegistry, D as DebugConfig, B as BaseLogger, A as AppConfig } from './Config-CEhfq8Mm.js';
3
+ export { I as InitialRouteParams } from './Config-CEhfq8Mm.js';
4
+ import { InlineConfig } from 'vite';
5
5
 
6
6
  type StaticMountEntry = {
7
7
  plugin: FastifyPluginCallback<any> | FastifyPluginAsync<any>;
@@ -42,13 +42,93 @@ declare const createServer: (opts: CreateServerOptions) => Promise<CreateServerR
42
42
  * including CSR, SSR, streaming, and middleware composition.
43
43
  */
44
44
 
45
- declare function taujsBuild({ config, projectRoot, clientBaseDir, isSSRBuild, }: {
45
+ type ViteBuildContext = {
46
+ appId: string;
47
+ entryPoint: string;
48
+ isSSRBuild: boolean;
49
+ clientRoot: string;
50
+ };
51
+ /**
52
+ * User-supplied vite config override.
53
+ * Can be a static config object or a function that receives build context.
54
+ *
55
+ * **Plugin order**: Framework applies plugins in this sequence:
56
+ * 1. `appConfig.plugins` (from taujs.config.ts)
57
+ * 2. `nodePolyfills({ include: ['stream'] })` (client builds only)
58
+ * 3. `userViteConfig.plugins` (from this option)
59
+ *
60
+ * If you need plugins before nodePolyfills, add them to `appConfig.plugins` instead.
61
+ *
62
+ * **Allowed customisations:**
63
+ * - `plugins`: Appended to framework plugin list
64
+ * - `define`: Shallow-merged with framework defines
65
+ * - `css.preprocessorOptions`: Deep-merged by preprocessor engine (scss, less, etc.)
66
+ * - `build.sourcemap`, `minify`, `terserOptions`: Direct overrides
67
+ * - `build.rollupOptions.external`: Direct override
68
+ * - `build.rollupOptions.output.manualChunks`: Merged into output config
69
+ * - `resolve.*` (except `alias`): Merged with framework resolve config
70
+ * - `esbuild`, `logLevel`, `optimizeDeps`: Direct overrides
71
+ *
72
+ * **Protected fields (cannot override):**
73
+ * - `root`, `base`, `publicDir`: Framework-controlled per-app paths
74
+ * - `build.outDir`: Framework manages `dist/client` vs `dist/ssr` separation
75
+ * - `build.ssr`, `ssrManifest`, `format`, `target`: Framework-controlled for SSR integrity
76
+ * - `build.rollupOptions.input`: Framework manages entry points
77
+ * - `resolve.alias`: Use top-level `alias` option in taujsBuild() instead
78
+ * - `server.*`: Ignored in builds (dev-mode only; configure in DevServer.ts)
79
+ *
80
+ * @example
81
+ * ```ts
82
+ * // Static config
83
+ * vite: {
84
+ * plugins: [visualizer()],
85
+ * build: { sourcemap: 'inline' }
86
+ * }
87
+ *
88
+ * // Function-based (conditional per app/mode)
89
+ * vite: ({ isSSRBuild, entryPoint }) => ({
90
+ * plugins: isSSRBuild ? [] : [visualizer()],
91
+ * logLevel: entryPoint === 'admin' ? 'info' : 'warn'
92
+ * })
93
+ * ```
94
+ */
95
+ type ViteConfigOverride = Partial<InlineConfig> | ((ctx: ViteBuildContext) => Partial<InlineConfig>);
96
+ declare function taujsBuild({ config, projectRoot, clientBaseDir, isSSRBuild, alias: userAlias, vite: userViteConfig, }: {
46
97
  config: {
47
98
  apps: AppConfig[];
48
99
  };
49
100
  projectRoot: string;
50
101
  clientBaseDir: string;
51
102
  isSSRBuild?: boolean;
103
+ /**
104
+ * Top-level alias overrides. Use this instead of `vite.resolve.alias`.
105
+ * User aliases are merged with framework defaults; user values win on conflicts.
106
+ *
107
+ * Framework provides:
108
+ * - `@client`: Resolves to current app's root
109
+ * - `@server`: Resolves to `src/server`
110
+ * - `@shared`: Resolves to `src/shared`
111
+ *
112
+ * @example
113
+ * ```ts
114
+ * alias: {
115
+ * '@utils': './src/utils',
116
+ * '@server': './custom-server', // overrides framework default
117
+ * }
118
+ * ```
119
+ */
120
+ alias?: Record<string, string>;
121
+ /** User-supplied Vite config overrides (plugins, tuning, etc.) */
122
+ vite?: ViteConfigOverride;
52
123
  }): Promise<void>;
53
124
 
54
- export { createServer, taujsBuild };
125
+ interface MessageMetaLogger {
126
+ debug?: (message?: string, meta?: Record<string, unknown>) => unknown;
127
+ info?: (message?: string, meta?: Record<string, unknown>) => unknown;
128
+ warn?: (message?: string, meta?: Record<string, unknown>) => unknown;
129
+ error?: (message?: string, meta?: Record<string, unknown>) => unknown;
130
+ child?: (bindings: Record<string, unknown>) => MessageMetaLogger | undefined;
131
+ }
132
+ declare function winstonAdapter(winston: MessageMetaLogger): BaseLogger;
133
+
134
+ export { BaseLogger, createServer, taujsBuild, winstonAdapter };