@noxfly/noxus 3.0.0-dev.3 → 3.0.0-dev.4
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/.github/copilot-instructions.md +110 -14
- package/AGENTS.md +5 -0
- package/README.md +114 -7
- package/dist/child.d.mts +7 -1
- package/dist/child.d.ts +7 -1
- package/dist/child.js +402 -862
- package/dist/child.mjs +389 -850
- package/dist/main.d.mts +171 -125
- package/dist/main.d.ts +171 -125
- package/dist/main.js +967 -886
- package/dist/main.mjs +914 -834
- package/dist/renderer.d.mts +17 -2
- package/dist/renderer.d.ts +17 -2
- package/dist/renderer.js +161 -118
- package/dist/renderer.mjs +150 -106
- package/package.json +1 -1
- package/src/DI/app-injector.ts +22 -9
- package/src/DI/injector-explorer.ts +78 -20
- package/src/internal/app.ts +9 -7
- package/src/internal/bootstrap.ts +33 -1
- package/src/internal/renderer-client.ts +36 -0
- package/src/internal/request.ts +6 -1
- package/src/internal/router.ts +14 -2
- package/src/internal/routes.ts +75 -11
- package/src/internal/socket.ts +8 -6
- package/src/utils/radix-tree.ts +58 -25
- package/src/window/window-manager.ts +34 -0
|
@@ -17,6 +17,15 @@ export interface RendererClientOptions {
|
|
|
17
17
|
initMessageType?: string;
|
|
18
18
|
windowRef?: Window;
|
|
19
19
|
generateRequestId?: () => string;
|
|
20
|
+
/**
|
|
21
|
+
* Timeout in milliseconds for IPC requests.
|
|
22
|
+
* If the main process does not respond within this duration,
|
|
23
|
+
* the request Promise is rejected and the pending entry cleaned up.
|
|
24
|
+
* Defaults to 10 000 ms. Set to 0 to disable.
|
|
25
|
+
*/
|
|
26
|
+
requestTimeout?: number;
|
|
27
|
+
/** @default true */
|
|
28
|
+
enableLogging?: boolean;
|
|
20
29
|
}
|
|
21
30
|
|
|
22
31
|
interface PendingRequest<T = unknown> {
|
|
@@ -24,6 +33,7 @@ interface PendingRequest<T = unknown> {
|
|
|
24
33
|
reject: (reason: IResponse<T>) => void;
|
|
25
34
|
request: IRequest;
|
|
26
35
|
submittedAt: number;
|
|
36
|
+
timer?: ReturnType<typeof setTimeout>;
|
|
27
37
|
}
|
|
28
38
|
|
|
29
39
|
const DEFAULT_INIT_EVENT = 'init-port';
|
|
@@ -97,18 +107,23 @@ export class NoxRendererClient {
|
|
|
97
107
|
private readonly initMessageType: string;
|
|
98
108
|
private readonly windowRef: Window;
|
|
99
109
|
private readonly generateRequestId: () => string;
|
|
110
|
+
private readonly requestTimeout: number;
|
|
100
111
|
|
|
101
112
|
private isReady = false;
|
|
102
113
|
private setupPromise: Promise<void> | undefined;
|
|
103
114
|
private setupResolve: (() => void) | undefined;
|
|
104
115
|
private setupReject: ((reason: Error) => void) | undefined;
|
|
105
116
|
|
|
117
|
+
private enableLogging: boolean;
|
|
118
|
+
|
|
106
119
|
constructor(options: RendererClientOptions = {}) {
|
|
107
120
|
this.windowRef = options.windowRef ?? window;
|
|
108
121
|
const resolvedBridge = options.bridge ?? resolveBridgeFromWindow(this.windowRef, options.bridgeName);
|
|
109
122
|
this.bridge = resolvedBridge ?? null;
|
|
110
123
|
this.initMessageType = options.initMessageType ?? DEFAULT_INIT_EVENT;
|
|
111
124
|
this.generateRequestId = options.generateRequestId ?? defaultRequestId;
|
|
125
|
+
this.requestTimeout = options.requestTimeout ?? 10_000;
|
|
126
|
+
this.enableLogging = options.enableLogging ?? true;
|
|
112
127
|
}
|
|
113
128
|
|
|
114
129
|
public async setup(): Promise<void> {
|
|
@@ -146,6 +161,12 @@ export class NoxRendererClient {
|
|
|
146
161
|
this.senderId = undefined;
|
|
147
162
|
this.isReady = false;
|
|
148
163
|
|
|
164
|
+
for(const pending of this.pendingRequests.values()) {
|
|
165
|
+
if(pending.timer !== undefined) {
|
|
166
|
+
clearTimeout(pending.timer);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
149
170
|
this.pendingRequests.clear();
|
|
150
171
|
}
|
|
151
172
|
|
|
@@ -177,6 +198,13 @@ export class NoxRendererClient {
|
|
|
177
198
|
submittedAt: Date.now(),
|
|
178
199
|
};
|
|
179
200
|
|
|
201
|
+
if(this.requestTimeout > 0) {
|
|
202
|
+
pending.timer = setTimeout(() => {
|
|
203
|
+
this.pendingRequests.delete(message.requestId);
|
|
204
|
+
reject(this.createErrorResponse<TResponse>(message.requestId, `Request timed out after ${this.requestTimeout}ms`) as IResponse<TResponse>);
|
|
205
|
+
}, this.requestTimeout);
|
|
206
|
+
}
|
|
207
|
+
|
|
180
208
|
this.pendingRequests.set(message.requestId, pending as PendingRequest);
|
|
181
209
|
|
|
182
210
|
this.requestPort!.postMessage(message);
|
|
@@ -260,6 +288,10 @@ export class NoxRendererClient {
|
|
|
260
288
|
return;
|
|
261
289
|
}
|
|
262
290
|
|
|
291
|
+
if(pending.timer !== undefined) {
|
|
292
|
+
clearTimeout(pending.timer);
|
|
293
|
+
}
|
|
294
|
+
|
|
263
295
|
this.pendingRequests.delete(response.requestId);
|
|
264
296
|
|
|
265
297
|
this.onRequestCompleted(pending, response);
|
|
@@ -273,6 +305,10 @@ export class NoxRendererClient {
|
|
|
273
305
|
};
|
|
274
306
|
|
|
275
307
|
protected onRequestCompleted(pending: PendingRequest, response: IResponse): void {
|
|
308
|
+
if(!this.enableLogging) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
276
312
|
if(typeof console.groupCollapsed === 'function') {
|
|
277
313
|
console.groupCollapsed(`${response.status} ${pending.request.method} /${pending.request.path}`);
|
|
278
314
|
}
|
package/src/internal/request.ts
CHANGED
|
@@ -17,6 +17,7 @@ export class Request {
|
|
|
17
17
|
public readonly context: AppInjector = RootInjector.createScope();
|
|
18
18
|
|
|
19
19
|
public readonly params: Record<string, string> = {};
|
|
20
|
+
public readonly query: Record<string, string>;
|
|
20
21
|
|
|
21
22
|
constructor(
|
|
22
23
|
public readonly event: Electron.MessageEvent,
|
|
@@ -24,9 +25,11 @@ export class Request {
|
|
|
24
25
|
public readonly id: string,
|
|
25
26
|
public readonly method: HttpMethod,
|
|
26
27
|
public readonly path: string,
|
|
27
|
-
public readonly body:
|
|
28
|
+
public readonly body: unknown,
|
|
29
|
+
query?: Record<string, string>,
|
|
28
30
|
) {
|
|
29
31
|
this.path = path.replace(/^\/|\/$/g, '');
|
|
32
|
+
this.query = query ?? {};
|
|
30
33
|
}
|
|
31
34
|
}
|
|
32
35
|
|
|
@@ -41,6 +44,7 @@ export interface IRequest<TBody = unknown> {
|
|
|
41
44
|
path: string;
|
|
42
45
|
method: HttpMethod;
|
|
43
46
|
body?: TBody;
|
|
47
|
+
query?: Record<string, string>;
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
export interface IBatchRequestItem<TBody = unknown> {
|
|
@@ -48,6 +52,7 @@ export interface IBatchRequestItem<TBody = unknown> {
|
|
|
48
52
|
path: string;
|
|
49
53
|
method: AtomicHttpMethod;
|
|
50
54
|
body?: TBody;
|
|
55
|
+
query?: Record<string, string>;
|
|
51
56
|
}
|
|
52
57
|
|
|
53
58
|
export interface IBatchRequestPayload {
|
package/src/internal/router.ts
CHANGED
|
@@ -119,6 +119,18 @@ export class Router {
|
|
|
119
119
|
return this;
|
|
120
120
|
}
|
|
121
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
|
+
|
|
122
134
|
// -------------------------------------------------------------------------
|
|
123
135
|
// Request handling
|
|
124
136
|
// -------------------------------------------------------------------------
|
|
@@ -167,7 +179,7 @@ export class Router {
|
|
|
167
179
|
response.body!.responses = await Promise.all(
|
|
168
180
|
payload.requests.map((item, i) => {
|
|
169
181
|
const id = item.requestId ?? `${request.id}:${i}`;
|
|
170
|
-
return this.handleAtomic(new Request(request.event, request.senderId, id, item.method, item.path, item.body));
|
|
182
|
+
return this.handleAtomic(new Request(request.event, request.senderId, id, item.method, item.path, item.body, item.query));
|
|
171
183
|
}),
|
|
172
184
|
);
|
|
173
185
|
}
|
|
@@ -225,7 +237,7 @@ export class Router {
|
|
|
225
237
|
entry.loading = null;
|
|
226
238
|
entry.load = null;
|
|
227
239
|
|
|
228
|
-
InjectorExplorer.flushAccumulated(entry.guards, entry.middlewares, prefix);
|
|
240
|
+
await InjectorExplorer.flushAccumulated(entry.guards, entry.middlewares, prefix);
|
|
229
241
|
|
|
230
242
|
entry.loaded = true;
|
|
231
243
|
|
package/src/internal/routes.ts
CHANGED
|
@@ -21,10 +21,12 @@ export interface RouteDefinition {
|
|
|
21
21
|
* Dynamic import function returning the controller file.
|
|
22
22
|
* The controller is loaded lazily on the first IPC request targeting this prefix.
|
|
23
23
|
*
|
|
24
|
+
* Optional when the route only serves as a parent for `children`.
|
|
25
|
+
*
|
|
24
26
|
* @example
|
|
25
27
|
* load: () => import('./modules/users/users.controller')
|
|
26
28
|
*/
|
|
27
|
-
load
|
|
29
|
+
load?: () => Promise<unknown>;
|
|
28
30
|
|
|
29
31
|
/**
|
|
30
32
|
* Guards applied to every action in this controller.
|
|
@@ -37,6 +39,12 @@ export interface RouteDefinition {
|
|
|
37
39
|
* Merged with action-level middlewares.
|
|
38
40
|
*/
|
|
39
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[];
|
|
40
48
|
}
|
|
41
49
|
|
|
42
50
|
/**
|
|
@@ -46,6 +54,9 @@ export interface RouteDefinition {
|
|
|
46
54
|
* This is the single source of truth for routing — no path is declared
|
|
47
55
|
* in @Controller(), preventing duplicate route prefixes across controllers.
|
|
48
56
|
*
|
|
57
|
+
* Supports nested routes via the `children` property. Guards and middlewares
|
|
58
|
+
* from parent entries are inherited (merged) into each child.
|
|
59
|
+
*
|
|
49
60
|
* @example
|
|
50
61
|
* export const routes = defineRoutes([
|
|
51
62
|
* {
|
|
@@ -54,25 +65,78 @@ export interface RouteDefinition {
|
|
|
54
65
|
* guards: [authGuard],
|
|
55
66
|
* },
|
|
56
67
|
* {
|
|
57
|
-
* path: '
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
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
|
+
* ],
|
|
61
74
|
* },
|
|
62
75
|
* ]);
|
|
63
76
|
*/
|
|
64
77
|
export function defineRoutes(routes: RouteDefinition[]): RouteDefinition[] {
|
|
65
|
-
const
|
|
66
|
-
|
|
78
|
+
const flat = flattenRoutes(routes);
|
|
79
|
+
|
|
80
|
+
const paths = flat.map(r => r.path);
|
|
67
81
|
|
|
82
|
+
// Check exact duplicates
|
|
83
|
+
const duplicates = paths.filter((p, i) => paths.indexOf(p) !== i);
|
|
68
84
|
if (duplicates.length > 0) {
|
|
69
85
|
throw new Error(
|
|
70
86
|
`[Noxus] Duplicate route prefixes detected: ${[...new Set(duplicates)].map(d => `"${d}"`).join(', ')}`
|
|
71
87
|
);
|
|
72
88
|
}
|
|
73
89
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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;
|
|
78
142
|
}
|
package/src/internal/socket.ts
CHANGED
|
@@ -37,7 +37,7 @@ export class NoxSocket {
|
|
|
37
37
|
return [...this.channels.keys()];
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
public emit<TPayload = unknown>(eventName: string, payload?: TPayload, targetSenderIds?: number[]):
|
|
40
|
+
public emit<TPayload = unknown>(eventName: string, payload?: TPayload, targetSenderIds?: number[]): void {
|
|
41
41
|
const normalizedEvent = eventName.trim();
|
|
42
42
|
|
|
43
43
|
if(normalizedEvent.length === 0) {
|
|
@@ -45,7 +45,6 @@ export class NoxSocket {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
const recipients = targetSenderIds ?? this.getSenderIds();
|
|
48
|
-
let delivered = 0;
|
|
49
48
|
|
|
50
49
|
for(const senderId of recipients) {
|
|
51
50
|
const channel = this.channels.get(senderId);
|
|
@@ -57,17 +56,20 @@ export class NoxSocket {
|
|
|
57
56
|
|
|
58
57
|
try {
|
|
59
58
|
channel.socket.port1.postMessage(createRendererEventMessage(normalizedEvent, payload));
|
|
60
|
-
delivered++;
|
|
61
59
|
}
|
|
62
60
|
catch(error) {
|
|
63
61
|
Logger.error(`[Noxus] Failed to emit "${normalizedEvent}" to sender ${senderId}.`, error);
|
|
64
62
|
}
|
|
65
63
|
}
|
|
66
|
-
|
|
67
|
-
return delivered;
|
|
68
64
|
}
|
|
69
65
|
|
|
70
66
|
public emitToRenderer<TPayload = unknown>(senderId: number, eventName: string, payload?: TPayload): boolean {
|
|
71
|
-
|
|
67
|
+
if(!this.channels.has(senderId)) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
this.emit(eventName, payload, [senderId]);
|
|
72
|
+
|
|
73
|
+
return true;
|
|
72
74
|
}
|
|
73
75
|
}
|
package/src/utils/radix-tree.ts
CHANGED
|
@@ -132,6 +132,31 @@ export class RadixTree<T> {
|
|
|
132
132
|
return this.searchRecursive(this.root, segments, {});
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Collects all values in the subtree rooted at the given node.
|
|
137
|
+
* This method traverses the subtree starting from the given node and collects all values
|
|
138
|
+
* @param node - The node to start collecting values from.
|
|
139
|
+
* @param values - An array to store the collected values. This parameter is optional and can be used for recursive calls.
|
|
140
|
+
* @returns An array of all values found in the subtree rooted at the given node.
|
|
141
|
+
*/
|
|
142
|
+
public collectValues(): T[];
|
|
143
|
+
public collectValues(node: RadixNode<T>, values: T[]): T[];
|
|
144
|
+
public collectValues(node?: RadixNode<T>, values: T[] = []): T[] {
|
|
145
|
+
if(!node) {
|
|
146
|
+
node = this.root;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if(node.value !== undefined) {
|
|
150
|
+
values.push(node.value);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
for(const child of node.children) {
|
|
154
|
+
this.collectValues(child, values);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return values;
|
|
158
|
+
}
|
|
159
|
+
|
|
135
160
|
/**
|
|
136
161
|
* Recursively searches for a path in the Radix Tree.
|
|
137
162
|
* This method traverses the tree and searches for the segments of the path, collecting parameters
|
|
@@ -154,40 +179,48 @@ export class RadixTree<T> {
|
|
|
154
179
|
|
|
155
180
|
const [segment, ...rest] = segments;
|
|
156
181
|
|
|
182
|
+
// Try static (exact) matches first, then param matches.
|
|
183
|
+
// This ensures e.g. 'addNote' is preferred over ':id'.
|
|
184
|
+
const staticChildren: RadixNode<T>[] = [];
|
|
185
|
+
const paramChildren: RadixNode<T>[] = [];
|
|
186
|
+
|
|
157
187
|
for(const child of node.children) {
|
|
158
188
|
if(child.isParam) {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
189
|
+
paramChildren.push(child);
|
|
190
|
+
}
|
|
191
|
+
else if(segment === child.segment) {
|
|
192
|
+
staticChildren.push(child);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
165
195
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
};
|
|
196
|
+
for(const child of staticChildren) {
|
|
197
|
+
if(rest.length === 0) {
|
|
198
|
+
// Only return leaf-level matches (has children for method nodes, or has a value)
|
|
199
|
+
if(child.value !== undefined || child.children.length > 0) {
|
|
200
|
+
return { node: child, params };
|
|
171
201
|
}
|
|
202
|
+
}
|
|
172
203
|
|
|
173
|
-
|
|
204
|
+
const result = this.searchRecursive(child, rest, params);
|
|
205
|
+
if(result) return result;
|
|
206
|
+
}
|
|
174
207
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
}
|
|
178
|
-
else if(segment === child.segment) {
|
|
179
|
-
if(rest.length === 0) {
|
|
180
|
-
return {
|
|
181
|
-
node: child,
|
|
182
|
-
params
|
|
183
|
-
};
|
|
184
|
-
}
|
|
208
|
+
for(const child of paramChildren) {
|
|
209
|
+
const paramName = child.paramName!;
|
|
185
210
|
|
|
186
|
-
|
|
211
|
+
const childParams: Params = {
|
|
212
|
+
...params,
|
|
213
|
+
[paramName]: segment ?? "",
|
|
214
|
+
};
|
|
187
215
|
|
|
188
|
-
|
|
189
|
-
|
|
216
|
+
if(rest.length === 0) {
|
|
217
|
+
if(child.value !== undefined || child.children.length > 0) {
|
|
218
|
+
return { node: child, params: childParams };
|
|
219
|
+
}
|
|
190
220
|
}
|
|
221
|
+
|
|
222
|
+
const result = this.searchRecursive(child, rest, childParams);
|
|
223
|
+
if(result) return result;
|
|
191
224
|
}
|
|
192
225
|
|
|
193
226
|
return undefined;
|
|
@@ -30,6 +30,12 @@ export interface WindowRecord {
|
|
|
30
30
|
id: number;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* @description
|
|
35
|
+
* The events emitted by WindowManager when windows are created, closed, focused, or blurred.
|
|
36
|
+
*/
|
|
37
|
+
export type WindowEvent = 'created' | 'closed' | 'focused' | 'blurred';
|
|
38
|
+
|
|
33
39
|
/**
|
|
34
40
|
* WindowManager is a singleton service that centralizes BrowserWindow lifecycle.
|
|
35
41
|
*
|
|
@@ -55,6 +61,8 @@ export interface WindowRecord {
|
|
|
55
61
|
@Injectable({ lifetime: 'singleton' })
|
|
56
62
|
export class WindowManager {
|
|
57
63
|
private readonly _windows = new Map<number, BrowserWindow>();
|
|
64
|
+
private readonly listeners = new Map<WindowEvent, Set<(win: BrowserWindow) => void>>();
|
|
65
|
+
|
|
58
66
|
private _mainWindowId: number | undefined;
|
|
59
67
|
|
|
60
68
|
// -------------------------------------------------------------------------
|
|
@@ -178,10 +186,12 @@ export class WindowManager {
|
|
|
178
186
|
/** Closes and destroys a window by id. */
|
|
179
187
|
public close(id: number): void {
|
|
180
188
|
const win = this._windows.get(id);
|
|
189
|
+
|
|
181
190
|
if (!win) {
|
|
182
191
|
Logger.warn(`[WindowManager] Window #${id} not found`);
|
|
183
192
|
return;
|
|
184
193
|
}
|
|
194
|
+
|
|
185
195
|
win.destroy();
|
|
186
196
|
}
|
|
187
197
|
|
|
@@ -218,6 +228,21 @@ export class WindowManager {
|
|
|
218
228
|
}
|
|
219
229
|
}
|
|
220
230
|
|
|
231
|
+
// -------------------------------------------------------------------------
|
|
232
|
+
// Events
|
|
233
|
+
// -------------------------------------------------------------------------
|
|
234
|
+
|
|
235
|
+
public on(event: WindowEvent, handler: (win: BrowserWindow) => void): () => void {
|
|
236
|
+
const set = this.listeners.get(event) ?? new Set();
|
|
237
|
+
set.add(handler);
|
|
238
|
+
this.listeners.set(event, set);
|
|
239
|
+
return () => set.delete(handler); // retourne unsubscribe
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private _emit(event: WindowEvent, win: BrowserWindow): void {
|
|
243
|
+
this.listeners.get(event)?.forEach(h => h(win));
|
|
244
|
+
}
|
|
245
|
+
|
|
221
246
|
// -------------------------------------------------------------------------
|
|
222
247
|
// Private
|
|
223
248
|
// -------------------------------------------------------------------------
|
|
@@ -229,12 +254,21 @@ export class WindowManager {
|
|
|
229
254
|
this._mainWindowId = win.id;
|
|
230
255
|
}
|
|
231
256
|
|
|
257
|
+
this._emit('created', win);
|
|
258
|
+
|
|
259
|
+
win.on('focus', () => this._emit('focused', win));
|
|
260
|
+
win.on('blur', () => this._emit('blurred', win));
|
|
261
|
+
|
|
232
262
|
win.once('closed', () => {
|
|
233
263
|
this._windows.delete(win.id);
|
|
264
|
+
|
|
234
265
|
if (this._mainWindowId === win.id) {
|
|
235
266
|
this._mainWindowId = undefined;
|
|
236
267
|
}
|
|
268
|
+
|
|
237
269
|
Logger.log(`[WindowManager] Window #${win.id} closed`);
|
|
270
|
+
|
|
271
|
+
this._emit('closed', win);
|
|
238
272
|
});
|
|
239
273
|
}
|
|
240
274
|
|