@noxfly/noxus 1.1.10 → 2.0.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/.github/copilot-instructions.md +32 -0
- package/README.md +104 -135
- package/dist/index-CI3OMzNR.d.mts +318 -0
- package/dist/index-CI3OMzNR.d.ts +318 -0
- package/dist/{noxus.d.mts → main.d.mts} +45 -233
- package/dist/{noxus.d.ts → main.d.ts} +45 -233
- package/dist/{noxus.js → main.js} +536 -59
- package/dist/{noxus.mjs → main.mjs} +523 -54
- package/dist/renderer.d.mts +1 -0
- package/dist/renderer.d.ts +1 -0
- package/dist/renderer.js +515 -0
- package/dist/renderer.mjs +485 -0
- package/package.json +73 -48
- package/scripts/postbuild.js +10 -5
- package/src/DI/injector-explorer.ts +1 -1
- package/src/app.ts +51 -46
- package/src/decorators/injectable.decorator.ts +6 -17
- package/src/decorators/injectable.metadata.ts +15 -0
- package/src/index.ts +2 -13
- package/src/main.ts +28 -0
- package/src/preload-bridge.ts +75 -0
- package/src/renderer-client.ts +338 -0
- package/src/renderer-events.ts +110 -0
- package/src/request.ts +27 -0
- package/src/router.ts +1 -1
- package/src/socket.ts +73 -0
- package/tsup.config.ts +2 -1
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Copilot Instructions
|
|
2
|
+
## Core Architecture
|
|
3
|
+
- bootstrapApplication in [src/bootstrap.ts](src/bootstrap.ts) waits for Electron app readiness, resolves NoxApp via DI, and returns a configured instance.
|
|
4
|
+
- [src/app.ts](src/app.ts) wires ipcMain events, spawns paired request/socket MessageChannels per renderer, and delegates handling to Router and NoxSocket.
|
|
5
|
+
- Package exports are split between renderer and main targets in [package.json](package.json); import Electron main APIs from @noxfly/noxus/main and renderer helpers from @noxfly/noxus or @noxfly/noxus/renderer.
|
|
6
|
+
- Dependency injection centers on RootInjector in [src/DI/app-injector.ts](src/DI/app-injector.ts); @Injectable triggers auto-registration through [src/DI/injector-explorer.ts](src/DI/injector-explorer.ts) and supports singleton, scope, and transient lifetimes.
|
|
7
|
+
- Modules decorated via [src/decorators/module.decorator.ts](src/decorators/module.decorator.ts) compose providers, controllers, and nested modules; bootstrapApplication rejects roots lacking Module metadata.
|
|
8
|
+
## Request Lifecycle
|
|
9
|
+
- Request objects from [src/request.ts](src/request.ts) wrap Electron MessageEvents and spawn per-request DI scopes on Request.context.
|
|
10
|
+
- Router in [src/router.ts](src/router.ts) indexes routes in a radix tree, merges controller-level and method-level decorators, and enforces root middlewares, route middlewares, then guards before invoking controller actions.
|
|
11
|
+
- ResponseException subclasses in [src/exceptions.ts](src/exceptions.ts) propagate status codes; throwing one short-circuits the pipeline so Router returns a structured error payload.
|
|
12
|
+
- Batch requests use HTTP method BATCH and normalization logic in Router.handleBatch; payloads must satisfy IBatchRequestPayload to fan out atomic subrequests.
|
|
13
|
+
## Communication Channels
|
|
14
|
+
- ipcMain listens for gimme-my-port and posts two transferable ports back to the renderer: index 0 carries request/response traffic, index 1 is reserved for socket-style push messages.
|
|
15
|
+
- NoxSocket in [src/socket.ts](src/socket.ts) maps sender IDs to {request, socket} channels and emits renderer events exclusively through channels.socket.port1.
|
|
16
|
+
- Renderer helpers in [src/renderer-events.ts](src/renderer-events.ts) expose RendererEventRegistry.tryDispatchFromMessageEvent to route push events; the preload script must start both ports and hand the second to this registry.
|
|
17
|
+
- Renderer-facing bootstrap lives in [src/renderer-client.ts](src/renderer-client.ts); NoxRendererClient requests ports, wires request/socket handlers, tracks pending promises, and surfaces RendererEventRegistry for push consumption.
|
|
18
|
+
- Preload scripts should call exposeNoxusBridge from [src/preload-bridge.ts](src/preload-bridge.ts) to publish window.noxus.requestPort; the helper sends 'gimme-my-port' and forwards both transferable ports with the configured init message.
|
|
19
|
+
- When adjusting preload or renderer glue, ensure window.postMessage('init-port', ...) forwards both ports so the socket channel stays alive alongside the request channel.
|
|
20
|
+
## Decorator Conventions
|
|
21
|
+
- Controller paths omit leading/trailing slashes; method decorators (Get, Post, etc.) auto-trim segments via [src/decorators/method.decorator.ts](src/decorators/method.decorator.ts).
|
|
22
|
+
- Guards registered through Authorize in [src/decorators/guards.decorator.ts](src/decorators/guards.decorator.ts) aggregate at controller and action scope; they must resolve truthy or Router throws UnauthorizedException.
|
|
23
|
+
- Injectable lifetimes default to scope; use singleton for app-wide utilities (window managers, sockets) and transient for short-lived resources.
|
|
24
|
+
## Logging and Utilities
|
|
25
|
+
- Logger in [src/utils/logger.ts](src/utils/logger.ts) standardizes color-coded log, warn, error, and comment output; use it when extending framework behavior.
|
|
26
|
+
- Path resolution relies on RadixTree from [src/utils/radix-tree.ts](src/utils/radix-tree.ts); normalize controller and route paths to avoid duplicate slashes.
|
|
27
|
+
- Request.params are filled by Router.extractParams; controllers read route params directly from Request without decorator helpers yet.
|
|
28
|
+
## Build and Tooling
|
|
29
|
+
- Run npm run build to invoke tsup with dual ESM/CJS outputs configured in [tsup.config.ts](tsup.config.ts); the postbuild script at [scripts/postbuild.js](scripts/postbuild.js) deduplicates license banners.
|
|
30
|
+
- Node 20 or newer is required; reflect-metadata is a peer dependency so host apps must install and import it before using decorators.
|
|
31
|
+
- Source uses baseUrl ./ with tsc-alias, so prefer absolute imports like src/module/file when editing framework code to match existing style.
|
|
32
|
+
- Dist artifacts live under dist/ and are published outputs; regenerate them via the build script rather than editing directly.
|
package/README.md
CHANGED
|
@@ -34,19 +34,13 @@ Noxus fills that gap.
|
|
|
34
34
|
|
|
35
35
|
## Installation
|
|
36
36
|
|
|
37
|
-
Install the package in your main process application :
|
|
37
|
+
Install the package in your main process application, and in your renderer as well :
|
|
38
38
|
|
|
39
39
|
```sh
|
|
40
40
|
npm i @noxfly/noxus
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
```sh
|
|
46
|
-
npm i -D @noxfly/noxus
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
Because you only need types during development, using the `-D` argument will make this package a `devDependency`, thus won't be present on your build.
|
|
43
|
+
> ⚠️ The default entry (`@noxfly/noxus`) only exposes renderer-friendly helpers and types. Import Electron main-process APIs from `@noxfly/noxus/main`.
|
|
50
44
|
|
|
51
45
|
## Basic use
|
|
52
46
|
|
|
@@ -61,7 +55,7 @@ However, you can feel free to keep both merged, this won't change anything, but
|
|
|
61
55
|
```ts
|
|
62
56
|
// main/index.ts
|
|
63
57
|
|
|
64
|
-
import { bootstrapApplication } from '@noxfly/noxus';
|
|
58
|
+
import { bootstrapApplication } from '@noxfly/noxus/main';
|
|
65
59
|
import { AppModule } from './modules/app.module.ts';
|
|
66
60
|
import { Application } from './modules/app.service.ts';
|
|
67
61
|
|
|
@@ -81,7 +75,7 @@ main();
|
|
|
81
75
|
```ts
|
|
82
76
|
// main/modules/app.service.ts
|
|
83
77
|
|
|
84
|
-
import { IApp, Injectable, Logger } from '@noxfly/noxus';
|
|
78
|
+
import { IApp, Injectable, Logger } from '@noxfly/noxus/main';
|
|
85
79
|
|
|
86
80
|
@Injectable('singleton')
|
|
87
81
|
export class Application implements IApp {
|
|
@@ -101,7 +95,7 @@ export class Application implements IApp {
|
|
|
101
95
|
```ts
|
|
102
96
|
// main/modules/app.module.ts
|
|
103
97
|
|
|
104
|
-
import { Module } from '@noxfly/noxus';
|
|
98
|
+
import { Module } from '@noxfly/noxus/main';
|
|
105
99
|
|
|
106
100
|
@Module({
|
|
107
101
|
imports: [UsersModule], // import modules to be found here
|
|
@@ -116,7 +110,7 @@ export class AppModule {}
|
|
|
116
110
|
```ts
|
|
117
111
|
// main/modules/users/users.module.ts
|
|
118
112
|
|
|
119
|
-
import { Module } from '@noxfly/noxus';
|
|
113
|
+
import { Module } from '@noxfly/noxus/main';
|
|
120
114
|
|
|
121
115
|
@Module({
|
|
122
116
|
providers: [UsersService],
|
|
@@ -128,7 +122,7 @@ export class UsersModule {}
|
|
|
128
122
|
```ts
|
|
129
123
|
// main/modules/users/users.service.ts
|
|
130
124
|
|
|
131
|
-
import { Injectable } from '@noxfly/noxus';
|
|
125
|
+
import { Injectable } from '@noxfly/noxus/main';
|
|
132
126
|
|
|
133
127
|
@Injectable()
|
|
134
128
|
export class UsersService {
|
|
@@ -147,7 +141,7 @@ export class UsersService {
|
|
|
147
141
|
```ts
|
|
148
142
|
// main/modules/users/users.controller.ts
|
|
149
143
|
|
|
150
|
-
import { Controller, Get } from '@noxfly/noxus';
|
|
144
|
+
import { Controller, Get } from '@noxfly/noxus/main';
|
|
151
145
|
import { UsersService } from './users.service.ts';
|
|
152
146
|
|
|
153
147
|
@Controller('users')
|
|
@@ -174,7 +168,7 @@ Further upgrades might include new decorators like `@Param()`, `@Body()` etc...
|
|
|
174
168
|
```ts
|
|
175
169
|
// main/guards/auth.guard.ts
|
|
176
170
|
|
|
177
|
-
import { IGuard, Injectable, MaybeAsync, Request } from '@noxfly/noxus';
|
|
171
|
+
import { IGuard, Injectable, MaybeAsync, Request } from '@noxfly/noxus/main';
|
|
178
172
|
|
|
179
173
|
@Injectable()
|
|
180
174
|
export class AuthGuard implements IGuard {
|
|
@@ -200,145 +194,62 @@ We need some configuration on the preload so the main process can give the rende
|
|
|
200
194
|
```ts
|
|
201
195
|
// main/preload.ts
|
|
202
196
|
|
|
203
|
-
import {
|
|
197
|
+
import { exposeNoxusBridge } from '@noxfly/noxus';
|
|
204
198
|
|
|
205
|
-
|
|
206
|
-
// .on -> back sends to front
|
|
207
|
-
|
|
208
|
-
type fn = (...args: any[]) => void;
|
|
209
|
-
|
|
210
|
-
contextBridge.exposeInMainWorld('ipcRenderer', {
|
|
211
|
-
requestPort: () => ipcRenderer.send('gimme-my-port'),
|
|
212
|
-
|
|
213
|
-
hereIsMyPort: () => ipcRenderer.once('port', (e, message) => {
|
|
214
|
-
e.ports[0]?.start();
|
|
215
|
-
window.postMessage({ type: 'init-port', senderId: message.senderId }, '*', [e.ports[0]!]);
|
|
216
|
-
}),
|
|
217
|
-
});
|
|
199
|
+
exposeNoxusBridge();
|
|
218
200
|
```
|
|
219
201
|
|
|
220
|
-
|
|
202
|
+
The helper uses `ipcRenderer.send('gimme-my-port')`, waits for the `'port'` response, starts both transferred `MessagePort`s, and forwards them to the renderer with `window.postMessage({ type: 'init-port', ... }, '*', [requestPort, socketPort])`. If you need to customise any channel names or the exposed property, pass `exposeNoxusBridge({ exposeAs: 'customNoxus', requestChannel: 'my-port', responseChannel: 'my-port-ready', initMessageType: 'custom-init' })`.
|
|
221
203
|
|
|
222
|
-
|
|
204
|
+
> ⚠️ As the Electron documentation warns, never expose the full `ipcRenderer` object. The helper only reveals a minimal `{ requestPort }` surface under `window.noxus` by default.
|
|
223
205
|
|
|
224
206
|
|
|
225
207
|
### Setup Renderer
|
|
226
208
|
|
|
227
|
-
|
|
209
|
+
Noxus ships with a `NoxRendererClient` helper that performs the renderer bootstrap: it asks the preload bridge for a port, expects two transferable `MessagePort`s (index `0` for request/response and index `1` for socket pushes), wires both, and exposes a promise-based `request`/`batch` API plus the `RendererEventRegistry` instance.
|
|
228
210
|
|
|
229
|
-
|
|
211
|
+
By default it calls `window.noxus.requestPort()`—the value registered by `exposeNoxusBridge()`—but you can pass a custom bridge through the constructor options if needed.
|
|
230
212
|
|
|
231
|
-
|
|
232
|
-
// renderer/anyFileAtStartup.ts
|
|
213
|
+
Call `await client.setup()` early in your renderer startup (for example inside the first Angular service that needs IPC). Once the promise resolves, `client.request(...)` automatically includes the negotiated `senderId`, and socket events are routed through `client.events`.
|
|
233
214
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
constructor() {
|
|
252
|
-
this.bridge = window as any;
|
|
253
|
-
this.ipcRenderer = this.bridge.ipcRenderer;
|
|
254
|
-
|
|
255
|
-
// when receiving the port given by the main renderer -> preload -> renderer
|
|
256
|
-
window.addEventListener('message', (event: MessageEvent) => {
|
|
257
|
-
if(event.data?.type === 'init-port' && event.ports.length > 0) {
|
|
258
|
-
this.port = event.ports[0]!;
|
|
259
|
-
this.senderId = event.data.senderId;
|
|
260
|
-
this.port.onmessage = onResponse;
|
|
261
|
-
}
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
ipcRenderer.requestPort();
|
|
215
|
+
```ts
|
|
216
|
+
// renderer/services/noxus.service.ts
|
|
217
|
+
|
|
218
|
+
import { Injectable } from '@angular/core';
|
|
219
|
+
import { from, Observable } from 'rxjs';
|
|
220
|
+
import {
|
|
221
|
+
IBatchRequestItem,
|
|
222
|
+
IBatchResponsePayload,
|
|
223
|
+
IRequest,
|
|
224
|
+
NoxRendererClient,
|
|
225
|
+
} from '@noxfly/noxus';
|
|
226
|
+
|
|
227
|
+
@Injectable({ providedIn: 'root' })
|
|
228
|
+
export class NoxusService extends NoxRendererClient {
|
|
229
|
+
public async init(): Promise<void> {
|
|
230
|
+
await this.setup();
|
|
265
231
|
}
|
|
266
232
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
*/
|
|
270
|
-
private onResponse(event: MessageEvent): void {
|
|
271
|
-
const response: IResponse<unknown> = event.data;
|
|
272
|
-
|
|
273
|
-
if(!response || !response.requestId) {
|
|
274
|
-
console.error('Received invalid response:', response);
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
const pending = this.pendingRequests.get(response.requestId);
|
|
279
|
-
|
|
280
|
-
if(!pending) {
|
|
281
|
-
console.error(`No handler found for request ID: ${response.requestId}`);
|
|
282
|
-
return;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
this.pendingRequests.delete(response.requestId);
|
|
286
|
-
|
|
287
|
-
let fn: (response: IResponse<unknown>) => void = pending.resolve;
|
|
288
|
-
|
|
289
|
-
console.groupCollapsed(`${response.status} ${pending.request.method} /${pending.request.path}`);
|
|
290
|
-
|
|
291
|
-
if(response.error) {
|
|
292
|
-
console.error('error message:', response.error);
|
|
293
|
-
fn = pending.reject;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
console.info('response:', response.body);
|
|
297
|
-
console.info('request:', pending.request);
|
|
298
|
-
|
|
299
|
-
console.groupEnd();
|
|
300
|
-
|
|
301
|
-
fn(response);
|
|
233
|
+
public request$<T, U = unknown>(request: Omit<IRequest<U>, 'requestId' | 'senderId'>): Observable<T> {
|
|
234
|
+
return from(this.request<T, U>(request));
|
|
302
235
|
}
|
|
303
236
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
*/
|
|
307
|
-
public request<T>(request: Omit<IRequest, 'requestId' | 'senderId'>): Promise<T> {
|
|
308
|
-
return new Promise<T>((resolve, reject) => {
|
|
309
|
-
if(!this.port || !this.senderId) {
|
|
310
|
-
return reject(new Error("MessagePort is not available"));
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
const req: IRequest = {
|
|
314
|
-
requestId: /* Create a random ID with the function of your choice */,
|
|
315
|
-
senderId: this.senderId,
|
|
316
|
-
...request,
|
|
317
|
-
};
|
|
318
|
-
|
|
319
|
-
this.pendingRequests.set(req.requestId, {
|
|
320
|
-
resolve: (response: IResponse<T>) => (response.status < 400)
|
|
321
|
-
? resolve(response.body as T)
|
|
322
|
-
: reject(response);
|
|
323
|
-
reject: (response?: IResponse<T>) => reject(response),
|
|
324
|
-
request: req,
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
this.port.postMessage(req);
|
|
328
|
-
});
|
|
237
|
+
public batch$<T = IBatchResponsePayload>(requests: Omit<IBatchRequestItem<unknown>, 'requestId'>[]): Observable<T> {
|
|
238
|
+
return from(this.batch(requests) as Promise<T>);
|
|
329
239
|
}
|
|
330
240
|
}
|
|
331
|
-
```
|
|
332
241
|
|
|
333
|
-
|
|
242
|
+
// Somewhere during app bootstrap
|
|
243
|
+
await noxusService.init();
|
|
334
244
|
|
|
335
|
-
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
path: 'users/all',
|
|
245
|
+
// Subscribe to push notifications
|
|
246
|
+
const subscription = noxusService.events.subscribe('users.updated', (payload) => {
|
|
247
|
+
console.log('Users updated:', payload);
|
|
339
248
|
});
|
|
340
249
|
```
|
|
341
250
|
|
|
251
|
+
If you need a custom bridge (for example a different preload shape), pass it via the constructor: `super({ bridge: window.customBridge })`. The class keeps promise-based semantics so frameworks can layer their own reactive wrappers as shown above.
|
|
252
|
+
|
|
342
253
|

|
|
343
254
|
|
|
344
255
|
|
|
@@ -352,7 +263,7 @@ If you throw any of these exception, the response to the renderer will contains
|
|
|
352
263
|
|
|
353
264
|
You can specify a message in the constructor.
|
|
354
265
|
|
|
355
|
-
You throw it as follow :
|
|
266
|
+
You throw it as follow :
|
|
356
267
|
```ts
|
|
357
268
|
throw new UnauthorizedException("Invalid credentials");
|
|
358
269
|
throw new BadRequestException("id is missing in the body");
|
|
@@ -394,7 +305,7 @@ throw new UnavailableException();
|
|
|
394
305
|
You can decide to inject an Injectable without passing by the constructor, as follow :
|
|
395
306
|
|
|
396
307
|
```ts
|
|
397
|
-
import { inject } from '@noxfly/noxus';
|
|
308
|
+
import { inject } from '@noxfly/noxus/main';
|
|
398
309
|
import { MyClass } from 'src/myclass';
|
|
399
310
|
|
|
400
311
|
const instance: MyClass = inject(MyClass);
|
|
@@ -407,7 +318,7 @@ Declare middlewares as follow :
|
|
|
407
318
|
```ts
|
|
408
319
|
// renderer/middlewares.ts
|
|
409
320
|
|
|
410
|
-
import { IMiddleware, Injectable, Request, IResponse, NextFunction } from '@noxfly/noxus';
|
|
321
|
+
import { IMiddleware, Injectable, Request, IResponse, NextFunction } from '@noxfly/noxus/main';
|
|
411
322
|
|
|
412
323
|
@Injectable()
|
|
413
324
|
export class MiddlewareA implements IMiddleware {
|
|
@@ -481,6 +392,64 @@ A -> B -> C -> D -> AuthGuard -> RoleGuard -> [action] -> D -> C -> B -> A.
|
|
|
481
392
|
|
|
482
393
|
if a middleware throw any exception or put the response status higher or equal to 400, the pipeline immediatly stops and the response is returned, weither it is done before or after the call to the next function.
|
|
483
394
|
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
## Listening to events from the main process
|
|
400
|
+
|
|
401
|
+
Starting from v1.2, the main process can push messages to renderer processes without the request/response flow.
|
|
402
|
+
|
|
403
|
+
```ts
|
|
404
|
+
// main/users/users.controller.ts
|
|
405
|
+
import { Controller, Post, Request, NoxSocket } from '@noxfly/noxus/main';
|
|
406
|
+
|
|
407
|
+
@Controller('users')
|
|
408
|
+
export class UsersController {
|
|
409
|
+
constructor(private readonly socket: NoxSocket) {}
|
|
410
|
+
|
|
411
|
+
@Post('create')
|
|
412
|
+
public async create(request: Request): Promise<void> {
|
|
413
|
+
const payload = { nickname: request.body.nickname };
|
|
414
|
+
|
|
415
|
+
this.socket.emitToRenderer(request.event.senderId, 'users:created', payload);
|
|
416
|
+
// or broadcast to every connected renderer: this.socket.emit('users:created', payload);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
On the renderer side, leverage the `RendererEventRegistry` to register and clean up listeners. The registry only handles push events, so it plays nicely with the existing request handling code above.
|
|
422
|
+
|
|
423
|
+
```ts
|
|
424
|
+
import { RendererEventRegistry, RendererEventSubscription } from '@noxfly/noxus';
|
|
425
|
+
|
|
426
|
+
private readonly events = new RendererEventRegistry();
|
|
427
|
+
|
|
428
|
+
constructor() {
|
|
429
|
+
// ... after the MessagePort is ready
|
|
430
|
+
this.port.onmessage = (event) => {
|
|
431
|
+
if(this.events.tryDispatchFromMessageEvent(event)) {
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
this.onMessage(event); // existing request handling
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
public onUsersCreated(): RendererEventSubscription {
|
|
440
|
+
return this.events.subscribe('users:created', (payload) => {
|
|
441
|
+
// react to the event
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
public teardown(): void {
|
|
446
|
+
this.events.clear();
|
|
447
|
+
}
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
|
|
484
453
|
## Contributing
|
|
485
454
|
|
|
486
455
|
1. Clone the repo
|