@noxfly/noxus 3.0.0-dev.4 → 3.0.0-dev.5
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/dist/child.js.map +1 -0
- package/dist/child.mjs.map +1 -0
- package/dist/main.js.map +1 -0
- package/dist/main.mjs.map +1 -0
- package/dist/preload.js.map +1 -0
- package/dist/preload.mjs.map +1 -0
- package/dist/renderer.js.map +1 -0
- package/dist/renderer.mjs.map +1 -0
- package/package.json +10 -9
- package/.editorconfig +0 -16
- package/.github/copilot-instructions.md +0 -128
- package/.vscode/settings.json +0 -3
- package/AGENTS.md +0 -5
- package/eslint.config.js +0 -109
- package/scripts/postbuild.js +0 -31
- package/src/DI/app-injector.ts +0 -173
- package/src/DI/injector-explorer.ts +0 -201
- package/src/DI/token.ts +0 -53
- package/src/decorators/controller.decorator.ts +0 -58
- package/src/decorators/guards.decorator.ts +0 -15
- package/src/decorators/injectable.decorator.ts +0 -81
- package/src/decorators/method.decorator.ts +0 -66
- package/src/decorators/middleware.decorator.ts +0 -15
- package/src/index.ts +0 -10
- package/src/internal/app.ts +0 -219
- package/src/internal/bootstrap.ts +0 -141
- package/src/internal/exceptions.ts +0 -57
- package/src/internal/preload-bridge.ts +0 -75
- package/src/internal/renderer-client.ts +0 -374
- package/src/internal/renderer-events.ts +0 -110
- package/src/internal/request.ts +0 -102
- package/src/internal/router.ts +0 -365
- package/src/internal/routes.ts +0 -142
- package/src/internal/socket.ts +0 -75
- package/src/main.ts +0 -26
- package/src/non-electron-process.ts +0 -22
- package/src/preload.ts +0 -10
- package/src/renderer.ts +0 -13
- package/src/utils/forward-ref.ts +0 -31
- package/src/utils/logger.ts +0 -430
- package/src/utils/radix-tree.ts +0 -243
- package/src/utils/types.ts +0 -21
- package/src/window/window-manager.ts +0 -302
- package/tsconfig.json +0 -29
- package/tsup.config.ts +0 -50
package/src/internal/request.ts
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @copyright 2025 NoxFly
|
|
3
|
-
* @license MIT
|
|
4
|
-
* @author NoxFly
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
import { AtomicHttpMethod, HttpMethod } from '../decorators/method.decorator';
|
|
9
|
-
import { AppInjector, RootInjector } from '../DI/app-injector';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* The Request class represents an HTTP request in the Noxus framework.
|
|
13
|
-
* It encapsulates the request data, including the event, ID, method, path, and body.
|
|
14
|
-
* It also provides a context for dependency injection through the AppInjector.
|
|
15
|
-
*/
|
|
16
|
-
export class Request {
|
|
17
|
-
public readonly context: AppInjector = RootInjector.createScope();
|
|
18
|
-
|
|
19
|
-
public readonly params: Record<string, string> = {};
|
|
20
|
-
public readonly query: Record<string, string>;
|
|
21
|
-
|
|
22
|
-
constructor(
|
|
23
|
-
public readonly event: Electron.MessageEvent,
|
|
24
|
-
public readonly senderId: number,
|
|
25
|
-
public readonly id: string,
|
|
26
|
-
public readonly method: HttpMethod,
|
|
27
|
-
public readonly path: string,
|
|
28
|
-
public readonly body: unknown,
|
|
29
|
-
query?: Record<string, string>,
|
|
30
|
-
) {
|
|
31
|
-
this.path = path.replace(/^\/|\/$/g, '');
|
|
32
|
-
this.query = query ?? {};
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* The IRequest interface defines the structure of a request object.
|
|
38
|
-
* It includes properties for the sender ID, request ID, path, method, and an optional body.
|
|
39
|
-
* This interface is used to standardize the request data across the application.
|
|
40
|
-
*/
|
|
41
|
-
export interface IRequest<TBody = unknown> {
|
|
42
|
-
senderId: number;
|
|
43
|
-
requestId: string;
|
|
44
|
-
path: string;
|
|
45
|
-
method: HttpMethod;
|
|
46
|
-
body?: TBody;
|
|
47
|
-
query?: Record<string, string>;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export interface IBatchRequestItem<TBody = unknown> {
|
|
51
|
-
requestId?: string;
|
|
52
|
-
path: string;
|
|
53
|
-
method: AtomicHttpMethod;
|
|
54
|
-
body?: TBody;
|
|
55
|
-
query?: Record<string, string>;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export interface IBatchRequestPayload {
|
|
59
|
-
requests: IBatchRequestItem[];
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Creates a Request object from the IPC event data.
|
|
64
|
-
* This function extracts the necessary information from the IPC event and constructs a Request instance.
|
|
65
|
-
*/
|
|
66
|
-
export interface IResponse<TBody = unknown> {
|
|
67
|
-
requestId: string;
|
|
68
|
-
status: number;
|
|
69
|
-
body?: TBody;
|
|
70
|
-
error?: string;
|
|
71
|
-
stack?: string;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export interface IBatchResponsePayload {
|
|
75
|
-
responses: IResponse[];
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export const RENDERER_EVENT_TYPE = 'noxus:event';
|
|
79
|
-
|
|
80
|
-
export interface IRendererEventMessage<TPayload = unknown> {
|
|
81
|
-
type: typeof RENDERER_EVENT_TYPE;
|
|
82
|
-
event: string;
|
|
83
|
-
payload?: TPayload;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export function createRendererEventMessage<TPayload = unknown>(event: string, payload?: TPayload): IRendererEventMessage<TPayload> {
|
|
87
|
-
return {
|
|
88
|
-
type: RENDERER_EVENT_TYPE,
|
|
89
|
-
event,
|
|
90
|
-
payload,
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export function isRendererEventMessage(value: unknown): value is IRendererEventMessage {
|
|
95
|
-
if(value === null || typeof value !== 'object') {
|
|
96
|
-
return false;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const possibleMessage = value as Partial<IRendererEventMessage>;
|
|
100
|
-
|
|
101
|
-
return possibleMessage.type === RENDERER_EVENT_TYPE && typeof possibleMessage.event === 'string';
|
|
102
|
-
}
|
package/src/internal/router.ts
DELETED
|
@@ -1,365 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @copyright 2025 NoxFly
|
|
3
|
-
* @license MIT
|
|
4
|
-
* @author NoxFly
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { getControllerMetadata } from '../decorators/controller.decorator';
|
|
8
|
-
import { Guard } from '../decorators/guards.decorator';
|
|
9
|
-
import { Injectable } from '../decorators/injectable.decorator';
|
|
10
|
-
import { getRouteMetadata, isAtomicHttpMethod } from '../decorators/method.decorator';
|
|
11
|
-
import { Middleware, NextFunction } from '../decorators/middleware.decorator';
|
|
12
|
-
import { InjectorExplorer } from '../DI/injector-explorer';
|
|
13
|
-
import { Logger } from '../utils/logger';
|
|
14
|
-
import { RadixTree } from '../utils/radix-tree';
|
|
15
|
-
import { Type } from '../utils/types';
|
|
16
|
-
import {
|
|
17
|
-
BadRequestException,
|
|
18
|
-
NotFoundException,
|
|
19
|
-
ResponseException,
|
|
20
|
-
UnauthorizedException
|
|
21
|
-
} from './exceptions';
|
|
22
|
-
import { IBatchRequestItem, IBatchRequestPayload, IBatchResponsePayload, IResponse, Request } from './request';
|
|
23
|
-
|
|
24
|
-
export interface ILazyRoute {
|
|
25
|
-
load: () => Promise<unknown>;
|
|
26
|
-
guards: Guard[];
|
|
27
|
-
middlewares: Middleware[];
|
|
28
|
-
loading: Promise<void> | null;
|
|
29
|
-
loaded: boolean;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
interface LazyRouteEntry {
|
|
33
|
-
load: (() => Promise<unknown>) | null;
|
|
34
|
-
guards: Guard[];
|
|
35
|
-
middlewares: Middleware[];
|
|
36
|
-
loading: Promise<void> | null;
|
|
37
|
-
loaded: boolean;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface IRouteDefinition {
|
|
41
|
-
method: string;
|
|
42
|
-
path: string;
|
|
43
|
-
controller: Type<unknown>;
|
|
44
|
-
handler: string;
|
|
45
|
-
guards: Guard[];
|
|
46
|
-
middlewares: Middleware[];
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export type ControllerAction = (request: Request, response: IResponse) => unknown;
|
|
50
|
-
|
|
51
|
-
@Injectable({ lifetime: 'singleton' })
|
|
52
|
-
export class Router {
|
|
53
|
-
private readonly routes = new RadixTree<IRouteDefinition>();
|
|
54
|
-
private readonly rootMiddlewares: Middleware[] = [];
|
|
55
|
-
private readonly lazyRoutes = new Map<string, LazyRouteEntry>();
|
|
56
|
-
|
|
57
|
-
// -------------------------------------------------------------------------
|
|
58
|
-
// Registration
|
|
59
|
-
// -------------------------------------------------------------------------
|
|
60
|
-
|
|
61
|
-
public registerController(
|
|
62
|
-
controllerClass: Type<unknown>,
|
|
63
|
-
pathPrefix: string,
|
|
64
|
-
routeGuards: Guard[] = [],
|
|
65
|
-
routeMiddlewares: Middleware[] = [],
|
|
66
|
-
): this {
|
|
67
|
-
const meta = getControllerMetadata(controllerClass);
|
|
68
|
-
|
|
69
|
-
if (!meta) {
|
|
70
|
-
throw new Error(`[Noxus] Missing @Controller decorator on ${controllerClass.name}`);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const routeMeta = getRouteMetadata(controllerClass);
|
|
74
|
-
|
|
75
|
-
for (const def of routeMeta) {
|
|
76
|
-
const fullPath = `${pathPrefix}/${def.path}`.replace(/\/+/g, '/').replace(/\/$/, '') || '/';
|
|
77
|
-
|
|
78
|
-
// Route-level guards/middlewares from defineRoutes() + action-level ones
|
|
79
|
-
const guards = [...new Set([...routeGuards, ...def.guards])];
|
|
80
|
-
const middlewares = [...new Set([...routeMiddlewares, ...def.middlewares])];
|
|
81
|
-
|
|
82
|
-
const routeDef: IRouteDefinition = {
|
|
83
|
-
method: def.method,
|
|
84
|
-
path: fullPath,
|
|
85
|
-
controller: controllerClass,
|
|
86
|
-
handler: def.handler,
|
|
87
|
-
guards,
|
|
88
|
-
middlewares,
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
this.routes.insert(fullPath + '/' + def.method, routeDef);
|
|
92
|
-
|
|
93
|
-
const guardInfo = guards.length ? `<${guards.map(g => g.name).join('|')}>` : '';
|
|
94
|
-
Logger.log(`Mapped {${def.method} /${fullPath}}${guardInfo} route`);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const ctrlGuardInfo = routeGuards.length
|
|
98
|
-
? `<${routeGuards.map(g => g.name).join('|')}>`
|
|
99
|
-
: '';
|
|
100
|
-
Logger.log(`Mapped ${controllerClass.name}${ctrlGuardInfo} controller's routes`);
|
|
101
|
-
|
|
102
|
-
return this;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
public registerLazyRoute(
|
|
106
|
-
pathPrefix: string,
|
|
107
|
-
load: () => Promise<unknown>,
|
|
108
|
-
guards: Guard[] = [],
|
|
109
|
-
middlewares: Middleware[] = [],
|
|
110
|
-
): this {
|
|
111
|
-
const normalized = pathPrefix.replace(/^\/+|\/+$/g, '');
|
|
112
|
-
this.lazyRoutes.set(normalized, { load, guards, middlewares, loading: null, loaded: false });
|
|
113
|
-
Logger.log(`Registered lazy route prefix {${normalized}}`);
|
|
114
|
-
return this;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
public defineRootMiddleware(middleware: Middleware): this {
|
|
118
|
-
this.rootMiddlewares.push(middleware);
|
|
119
|
-
return this;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
public getRegisteredRoutes(): Array<{ method: string; path: string; }> {
|
|
123
|
-
const allRoutes = this.routes.collectValues();
|
|
124
|
-
return allRoutes.map(r => ({ method: r.method, path: r.path }));
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
public getLazyRoutes(): Array<{ prefix: string; loaded: boolean; }> {
|
|
128
|
-
return [...this.lazyRoutes.entries()].map(([prefix, entry]) => ({
|
|
129
|
-
prefix,
|
|
130
|
-
loaded: entry.loaded,
|
|
131
|
-
}));
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// -------------------------------------------------------------------------
|
|
135
|
-
// Request handling
|
|
136
|
-
// -------------------------------------------------------------------------
|
|
137
|
-
|
|
138
|
-
public async handle(request: Request): Promise<IResponse> {
|
|
139
|
-
return request.method === 'BATCH'
|
|
140
|
-
? this.handleBatch(request)
|
|
141
|
-
: this.handleAtomic(request);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
private async handleAtomic(request: Request): Promise<IResponse> {
|
|
145
|
-
Logger.comment(`> ${request.method} /${request.path}`);
|
|
146
|
-
const t0 = performance.now();
|
|
147
|
-
|
|
148
|
-
const response: IResponse = { requestId: request.id, status: 200, body: null };
|
|
149
|
-
let isCritical = false;
|
|
150
|
-
|
|
151
|
-
try {
|
|
152
|
-
const routeDef = await this.findRoute(request);
|
|
153
|
-
await this.resolveController(request, response, routeDef);
|
|
154
|
-
|
|
155
|
-
if (response.status >= 400) throw new ResponseException(response.status, response.error);
|
|
156
|
-
}
|
|
157
|
-
catch (error) {
|
|
158
|
-
this.fillErrorResponse(response, error, (c) => { isCritical = c; });
|
|
159
|
-
}
|
|
160
|
-
finally {
|
|
161
|
-
this.logResponse(request, response, performance.now() - t0, isCritical);
|
|
162
|
-
return response;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
private async handleBatch(request: Request): Promise<IResponse> {
|
|
167
|
-
Logger.comment(`> ${request.method} /${request.path}`);
|
|
168
|
-
const t0 = performance.now();
|
|
169
|
-
|
|
170
|
-
const response: IResponse<IBatchResponsePayload> = {
|
|
171
|
-
requestId: request.id,
|
|
172
|
-
status: 200,
|
|
173
|
-
body: { responses: [] },
|
|
174
|
-
};
|
|
175
|
-
let isCritical = false;
|
|
176
|
-
|
|
177
|
-
try {
|
|
178
|
-
const payload = this.normalizeBatchPayload(request.body);
|
|
179
|
-
response.body!.responses = await Promise.all(
|
|
180
|
-
payload.requests.map((item, i) => {
|
|
181
|
-
const id = item.requestId ?? `${request.id}:${i}`;
|
|
182
|
-
return this.handleAtomic(new Request(request.event, request.senderId, id, item.method, item.path, item.body, item.query));
|
|
183
|
-
}),
|
|
184
|
-
);
|
|
185
|
-
}
|
|
186
|
-
catch (error) {
|
|
187
|
-
this.fillErrorResponse(response, error, (c) => { isCritical = c; });
|
|
188
|
-
}
|
|
189
|
-
finally {
|
|
190
|
-
this.logResponse(request, response, performance.now() - t0, isCritical);
|
|
191
|
-
return response;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// -------------------------------------------------------------------------
|
|
196
|
-
// Route resolution
|
|
197
|
-
// -------------------------------------------------------------------------
|
|
198
|
-
|
|
199
|
-
private tryFindRoute(request: Request): IRouteDefinition | undefined {
|
|
200
|
-
const matched = this.routes.search(request.path);
|
|
201
|
-
if (!matched?.node || matched.node.children.length === 0) return undefined;
|
|
202
|
-
return matched.node.findExactChild(request.method)?.value;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
private async findRoute(request: Request): Promise<IRouteDefinition> {
|
|
206
|
-
const direct = this.tryFindRoute(request);
|
|
207
|
-
if (direct) return direct;
|
|
208
|
-
|
|
209
|
-
await this.tryLoadLazyRoute(request.path);
|
|
210
|
-
|
|
211
|
-
const afterLazy = this.tryFindRoute(request);
|
|
212
|
-
if (afterLazy) return afterLazy;
|
|
213
|
-
|
|
214
|
-
throw new NotFoundException(`No route matches ${request.method} ${request.path}`);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
private async tryLoadLazyRoute(requestPath: string): Promise<void> {
|
|
218
|
-
const firstSegment = requestPath.replace(/^\/+/, '').split('/')[0] ?? '';
|
|
219
|
-
|
|
220
|
-
for (const [prefix, entry] of this.lazyRoutes) {
|
|
221
|
-
if (entry.loaded) continue;
|
|
222
|
-
const normalized = requestPath.replace(/^\/+/, '');
|
|
223
|
-
if (normalized === prefix || normalized.startsWith(prefix + '/') || firstSegment === prefix) {
|
|
224
|
-
if (!entry.loading) entry.loading = this.loadLazyModule(prefix, entry);
|
|
225
|
-
await entry.loading;
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
private async loadLazyModule(prefix: string, entry: LazyRouteEntry): Promise<void> {
|
|
232
|
-
const t0 = performance.now();
|
|
233
|
-
InjectorExplorer.beginAccumulate();
|
|
234
|
-
|
|
235
|
-
await entry.load?.();
|
|
236
|
-
|
|
237
|
-
entry.loading = null;
|
|
238
|
-
entry.load = null;
|
|
239
|
-
|
|
240
|
-
await InjectorExplorer.flushAccumulated(entry.guards, entry.middlewares, prefix);
|
|
241
|
-
|
|
242
|
-
entry.loaded = true;
|
|
243
|
-
|
|
244
|
-
Logger.info(`Lazy-loaded module for prefix {${prefix}} in ${Math.round(performance.now() - t0)}ms`);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// -------------------------------------------------------------------------
|
|
248
|
-
// Pipeline
|
|
249
|
-
// -------------------------------------------------------------------------
|
|
250
|
-
|
|
251
|
-
private async resolveController(request: Request, response: IResponse, routeDef: IRouteDefinition): Promise<void> {
|
|
252
|
-
const instance = request.context.resolve(routeDef.controller);
|
|
253
|
-
Object.assign(request.params, this.extractParams(request.path, routeDef.path));
|
|
254
|
-
await this.runPipeline(request, response, routeDef, instance);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
private async runPipeline(
|
|
258
|
-
request: Request,
|
|
259
|
-
response: IResponse,
|
|
260
|
-
routeDef: IRouteDefinition,
|
|
261
|
-
controllerInstance: unknown,
|
|
262
|
-
): Promise<void> {
|
|
263
|
-
const middlewares = [...new Set([...this.rootMiddlewares, ...routeDef.middlewares])];
|
|
264
|
-
const mwMax = middlewares.length - 1;
|
|
265
|
-
const guardMax = mwMax + routeDef.guards.length;
|
|
266
|
-
let index = -1;
|
|
267
|
-
|
|
268
|
-
const dispatch = async (i: number): Promise<void> => {
|
|
269
|
-
if (i <= index) throw new Error('next() called multiple times');
|
|
270
|
-
index = i;
|
|
271
|
-
|
|
272
|
-
if (i <= mwMax) {
|
|
273
|
-
await this.runMiddleware(request, response, dispatch.bind(null, i + 1), middlewares[i]!);
|
|
274
|
-
if (response.status >= 400) throw new ResponseException(response.status, response.error);
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
if (i <= guardMax) {
|
|
279
|
-
await this.runGuard(request, routeDef.guards[i - middlewares.length]!);
|
|
280
|
-
await dispatch(i + 1);
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
const action = (controllerInstance as Record<string, ControllerAction>)[routeDef.handler]!;
|
|
285
|
-
response.body = await action.call(controllerInstance, request, response);
|
|
286
|
-
if (response.body === undefined) response.body = {};
|
|
287
|
-
};
|
|
288
|
-
|
|
289
|
-
await dispatch(0);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
private async runMiddleware(request: Request, response: IResponse, next: NextFunction, middleware: Middleware): Promise<void> {
|
|
293
|
-
await middleware(request, response, next);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
private async runGuard(request: Request, guard: Guard): Promise<void> {
|
|
297
|
-
if (!await guard(request)) {
|
|
298
|
-
throw new UnauthorizedException(`Unauthorized for ${request.method} ${request.path}`);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// -------------------------------------------------------------------------
|
|
303
|
-
// Utilities
|
|
304
|
-
// -------------------------------------------------------------------------
|
|
305
|
-
|
|
306
|
-
private extractParams(actual: string, template: string): Record<string, string> {
|
|
307
|
-
const aParts = actual.split('/');
|
|
308
|
-
const tParts = template.split('/');
|
|
309
|
-
const params: Record<string, string> = {};
|
|
310
|
-
tParts.forEach((part, i) => {
|
|
311
|
-
if (part.startsWith(':')) params[part.slice(1)] = aParts[i] ?? '';
|
|
312
|
-
});
|
|
313
|
-
return params;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
private normalizeBatchPayload(body: unknown): IBatchRequestPayload {
|
|
317
|
-
if (body === null || typeof body !== 'object') {
|
|
318
|
-
throw new BadRequestException('Batch payload must be an object containing a requests array.');
|
|
319
|
-
}
|
|
320
|
-
const { requests } = body as Partial<IBatchRequestPayload>;
|
|
321
|
-
if (!Array.isArray(requests)) throw new BadRequestException('Batch payload must define a requests array.');
|
|
322
|
-
return { requests: requests.map((e, i) => this.normalizeBatchItem(e, i)) };
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
private normalizeBatchItem(entry: unknown, index: number): IBatchRequestItem {
|
|
326
|
-
if (entry === null || typeof entry !== 'object') throw new BadRequestException(`Batch request at index ${index} must be an object.`);
|
|
327
|
-
const { requestId, path, method, body } = entry as Partial<IBatchRequestItem> & { method?: unknown };
|
|
328
|
-
if (requestId !== undefined && typeof requestId !== 'string') throw new BadRequestException(`Batch request at index ${index} has an invalid requestId.`);
|
|
329
|
-
if (typeof path !== 'string' || !path.length) throw new BadRequestException(`Batch request at index ${index} must define a non-empty path.`);
|
|
330
|
-
if (typeof method !== 'string') throw new BadRequestException(`Batch request at index ${index} must define an HTTP method.`);
|
|
331
|
-
const normalized = method.toUpperCase();
|
|
332
|
-
if (!isAtomicHttpMethod(normalized)) throw new BadRequestException(`Batch request at index ${index} uses unsupported method ${method}.`);
|
|
333
|
-
return { requestId, path, method: normalized, body };
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
private fillErrorResponse(response: IResponse, error: unknown, setCritical: (v: boolean) => void): void {
|
|
337
|
-
response.body = undefined;
|
|
338
|
-
if (error instanceof ResponseException) {
|
|
339
|
-
response.status = error.status;
|
|
340
|
-
response.error = error.message;
|
|
341
|
-
response.stack = error.stack;
|
|
342
|
-
} else if (error instanceof Error) {
|
|
343
|
-
setCritical(true);
|
|
344
|
-
response.status = 500;
|
|
345
|
-
response.error = error.message || 'Internal Server Error';
|
|
346
|
-
response.stack = error.stack;
|
|
347
|
-
} else {
|
|
348
|
-
setCritical(true);
|
|
349
|
-
response.status = 500;
|
|
350
|
-
response.error = 'Unknown error occurred';
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
private logResponse(request: Request, response: IResponse, ms: number, isCritical: boolean): void {
|
|
355
|
-
const msg = `< ${response.status} ${request.method} /${request.path} ${Logger.colors.yellow}${Math.round(ms)}ms${Logger.colors.initial}`;
|
|
356
|
-
if (response.status < 400) Logger.log(msg);
|
|
357
|
-
else if (response.status < 500) Logger.warn(msg);
|
|
358
|
-
else isCritical ? Logger.critical(msg) : Logger.error(msg);
|
|
359
|
-
|
|
360
|
-
if (response.error) {
|
|
361
|
-
isCritical ? Logger.critical(response.error) : Logger.error(response.error);
|
|
362
|
-
if (response.stack) Logger.errorStack(response.stack);
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
}
|
package/src/internal/routes.ts
DELETED
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @copyright 2025 NoxFly
|
|
3
|
-
* @license MIT
|
|
4
|
-
* @author NoxFly
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { Guard } from '../decorators/guards.decorator';
|
|
8
|
-
import { Middleware } from '../decorators/middleware.decorator';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* A single route entry in the application routing table.
|
|
12
|
-
*/
|
|
13
|
-
export interface RouteDefinition {
|
|
14
|
-
/**
|
|
15
|
-
* The path prefix for this route (e.g. 'users', 'orders').
|
|
16
|
-
* All actions defined in the controller will be prefixed with this path.
|
|
17
|
-
*/
|
|
18
|
-
path: string;
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Dynamic import function returning the controller file.
|
|
22
|
-
* The controller is loaded lazily on the first IPC request targeting this prefix.
|
|
23
|
-
*
|
|
24
|
-
* Optional when the route only serves as a parent for `children`.
|
|
25
|
-
*
|
|
26
|
-
* @example
|
|
27
|
-
* load: () => import('./modules/users/users.controller')
|
|
28
|
-
*/
|
|
29
|
-
load?: () => Promise<unknown>;
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Guards applied to every action in this controller.
|
|
33
|
-
* Merged with action-level guards.
|
|
34
|
-
*/
|
|
35
|
-
guards?: Guard[];
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Middlewares applied to every action in this controller.
|
|
39
|
-
* Merged with action-level middlewares.
|
|
40
|
-
*/
|
|
41
|
-
middlewares?: Middleware[];
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Nested child routes. Guards and middlewares declared here are
|
|
45
|
-
* inherited (merged) by all children.
|
|
46
|
-
*/
|
|
47
|
-
children?: RouteDefinition[];
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Defines the application routing table.
|
|
52
|
-
* Each entry maps a path prefix to a lazily-loaded controller.
|
|
53
|
-
*
|
|
54
|
-
* This is the single source of truth for routing — no path is declared
|
|
55
|
-
* in @Controller(), preventing duplicate route prefixes across controllers.
|
|
56
|
-
*
|
|
57
|
-
* Supports nested routes via the `children` property. Guards and middlewares
|
|
58
|
-
* from parent entries are inherited (merged) into each child.
|
|
59
|
-
*
|
|
60
|
-
* @example
|
|
61
|
-
* export const routes = defineRoutes([
|
|
62
|
-
* {
|
|
63
|
-
* path: 'users',
|
|
64
|
-
* load: () => import('./modules/users/users.controller'),
|
|
65
|
-
* guards: [authGuard],
|
|
66
|
-
* },
|
|
67
|
-
* {
|
|
68
|
-
* path: 'admin',
|
|
69
|
-
* guards: [authGuard, adminGuard],
|
|
70
|
-
* children: [
|
|
71
|
-
* { path: 'users', load: () => import('./admin/users.controller') },
|
|
72
|
-
* { path: 'products', load: () => import('./admin/products.controller') },
|
|
73
|
-
* ],
|
|
74
|
-
* },
|
|
75
|
-
* ]);
|
|
76
|
-
*/
|
|
77
|
-
export function defineRoutes(routes: RouteDefinition[]): RouteDefinition[] {
|
|
78
|
-
const flat = flattenRoutes(routes);
|
|
79
|
-
|
|
80
|
-
const paths = flat.map(r => r.path);
|
|
81
|
-
|
|
82
|
-
// Check exact duplicates
|
|
83
|
-
const duplicates = paths.filter((p, i) => paths.indexOf(p) !== i);
|
|
84
|
-
if (duplicates.length > 0) {
|
|
85
|
-
throw new Error(
|
|
86
|
-
`[Noxus] Duplicate route prefixes detected: ${[...new Set(duplicates)].map(d => `"${d}"`).join(', ')}`
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Check overlapping prefixes (e.g. 'users' and 'users/admin')
|
|
91
|
-
const sorted = [...paths].sort();
|
|
92
|
-
for (let i = 0; i < sorted.length - 1; i++) {
|
|
93
|
-
const a = sorted[i]!;
|
|
94
|
-
const b = sorted[i + 1]!;
|
|
95
|
-
if (b.startsWith(a + '/')) {
|
|
96
|
-
throw new Error(
|
|
97
|
-
`[Noxus] Overlapping route prefixes detected: "${a}" and "${b}". ` +
|
|
98
|
-
`Use nested children under "${a}" instead of declaring both as top-level routes.`
|
|
99
|
-
);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
return flat;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Recursively flattens nested route definitions, merging parent guards / middlewares.
|
|
108
|
-
*/
|
|
109
|
-
function flattenRoutes(
|
|
110
|
-
routes: RouteDefinition[],
|
|
111
|
-
parentPath = '',
|
|
112
|
-
parentGuards: Guard[] = [],
|
|
113
|
-
parentMiddlewares: Middleware[] = [],
|
|
114
|
-
): RouteDefinition[] {
|
|
115
|
-
const result: RouteDefinition[] = [];
|
|
116
|
-
|
|
117
|
-
for (const route of routes) {
|
|
118
|
-
const path = [parentPath, route.path.replace(/^\/+|\/+$/g, '')]
|
|
119
|
-
.filter(Boolean)
|
|
120
|
-
.join('/');
|
|
121
|
-
|
|
122
|
-
const guards = [...new Set([...parentGuards, ...(route.guards ?? [])])];
|
|
123
|
-
const middlewares = [...new Set([...parentMiddlewares, ...(route.middlewares ?? [])])];
|
|
124
|
-
|
|
125
|
-
if (route.load) {
|
|
126
|
-
result.push({ ...route, path, guards, middlewares });
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (route.children?.length) {
|
|
130
|
-
result.push(...flattenRoutes(route.children, path, guards, middlewares));
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (!route.load && !route.children?.length) {
|
|
134
|
-
throw new Error(
|
|
135
|
-
`[Noxus] Route "${path}" has neither a load function nor children. ` +
|
|
136
|
-
`It must have at least one of them.`
|
|
137
|
-
);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return result;
|
|
142
|
-
}
|
package/src/internal/socket.ts
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @copyright 2025 NoxFly
|
|
3
|
-
* @license MIT
|
|
4
|
-
* @author NoxFly
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Centralizes MessagePort storage for renderer communication and handles
|
|
9
|
-
* push-event delivery back to renderer processes.
|
|
10
|
-
*/
|
|
11
|
-
import { Injectable } from '../decorators/injectable.decorator';
|
|
12
|
-
import { Logger } from '../utils/logger';
|
|
13
|
-
import { createRendererEventMessage } from './request';
|
|
14
|
-
|
|
15
|
-
interface RendererChannels {
|
|
16
|
-
request: Electron.MessageChannelMain;
|
|
17
|
-
socket: Electron.MessageChannelMain;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
@Injectable({ lifetime: 'singleton' })
|
|
21
|
-
export class NoxSocket {
|
|
22
|
-
private readonly channels = new Map<number, RendererChannels>();
|
|
23
|
-
|
|
24
|
-
public register(senderId: number, requestChannel: Electron.MessageChannelMain, socketChannel: Electron.MessageChannelMain): void {
|
|
25
|
-
this.channels.set(senderId, { request: requestChannel, socket: socketChannel });
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
public get(senderId: number): RendererChannels | undefined {
|
|
29
|
-
return this.channels.get(senderId);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
public unregister(senderId: number): void {
|
|
33
|
-
this.channels.delete(senderId);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
public getSenderIds(): number[] {
|
|
37
|
-
return [...this.channels.keys()];
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
public emit<TPayload = unknown>(eventName: string, payload?: TPayload, targetSenderIds?: number[]): void {
|
|
41
|
-
const normalizedEvent = eventName.trim();
|
|
42
|
-
|
|
43
|
-
if(normalizedEvent.length === 0) {
|
|
44
|
-
throw new Error('Renderer event name must be a non-empty string.');
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const recipients = targetSenderIds ?? this.getSenderIds();
|
|
48
|
-
|
|
49
|
-
for(const senderId of recipients) {
|
|
50
|
-
const channel = this.channels.get(senderId);
|
|
51
|
-
|
|
52
|
-
if(!channel) {
|
|
53
|
-
Logger.warn(`No message channel found for sender ID: ${senderId} while emitting "${normalizedEvent}".`);
|
|
54
|
-
continue;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
try {
|
|
58
|
-
channel.socket.port1.postMessage(createRendererEventMessage(normalizedEvent, payload));
|
|
59
|
-
}
|
|
60
|
-
catch(error) {
|
|
61
|
-
Logger.error(`[Noxus] Failed to emit "${normalizedEvent}" to sender ${senderId}.`, error);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
public emitToRenderer<TPayload = unknown>(senderId: number, eventName: string, payload?: TPayload): boolean {
|
|
67
|
-
if(!this.channels.has(senderId)) {
|
|
68
|
-
return false;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
this.emit(eventName, payload, [senderId]);
|
|
72
|
-
|
|
73
|
-
return true;
|
|
74
|
-
}
|
|
75
|
-
}
|