@iskra-bun/socket-kit 0.1.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/CHANGELOG.md +7 -0
- package/README.md +31 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.js +113 -0
- package/dist/index.js.map +1 -0
- package/package.json +55 -0
- package/src/driver.ts +95 -0
- package/src/errors.ts +19 -0
- package/src/index.ts +3 -0
- package/src/router.ts +22 -0
package/CHANGELOG.md
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# @iskra-bun/socket-kit
|
|
2
|
+
|
|
3
|
+
WebSocket nativo de Bun para Iskra, con router de mensajes y broadcast por topics.
|
|
4
|
+
|
|
5
|
+
## Instalacion
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun add @iskra-bun/socket-kit @iskra-bun/core
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Uso rapido
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { App } from '@iskra-bun/core'
|
|
15
|
+
import { SocketDriver } from '@iskra-bun/socket-kit'
|
|
16
|
+
|
|
17
|
+
const app = new App({ name: 'mi-app' })
|
|
18
|
+
app.register(new SocketDriver({ port: 3001 }))
|
|
19
|
+
|
|
20
|
+
await app.start()
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Define rutas de mensajes con el `SocketRouter` y emite a topics con broadcast; ver la guia para el protocolo.
|
|
24
|
+
|
|
25
|
+
## Documentacion
|
|
26
|
+
|
|
27
|
+
Guia completa: [docs/socket-kit.md](../../docs/socket-kit.md)
|
|
28
|
+
|
|
29
|
+
## Licencia
|
|
30
|
+
|
|
31
|
+
AGPL-3.0-or-later
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Context, Driver, App, IskraError } from '@iskra-bun/core';
|
|
2
|
+
import { ServerWebSocket } from 'bun';
|
|
3
|
+
|
|
4
|
+
interface SocketContext<T = any> extends Context<T> {
|
|
5
|
+
socket: ServerWebSocket<any>;
|
|
6
|
+
broadcast(event: string, payload: any): void;
|
|
7
|
+
}
|
|
8
|
+
type SocketHandler = (ctx: SocketContext) => Promise<void> | void;
|
|
9
|
+
declare class SocketRouter {
|
|
10
|
+
private handlers;
|
|
11
|
+
on(event: string, handler: SocketHandler): this;
|
|
12
|
+
getHandler(event: string): SocketHandler | undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
declare class SocketDriver implements Driver {
|
|
16
|
+
name: string;
|
|
17
|
+
private app;
|
|
18
|
+
private router;
|
|
19
|
+
private port;
|
|
20
|
+
private runningServer;
|
|
21
|
+
constructor(options?: {
|
|
22
|
+
port?: number;
|
|
23
|
+
router?: SocketRouter;
|
|
24
|
+
});
|
|
25
|
+
init(app: App): void;
|
|
26
|
+
start(): void;
|
|
27
|
+
broadcast(event: string, payload: any): void;
|
|
28
|
+
stop(): void;
|
|
29
|
+
private handleMessage;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
declare class SocketConnectionError extends IskraError {
|
|
33
|
+
constructor(message: string, options?: {
|
|
34
|
+
cause?: Error;
|
|
35
|
+
context?: Record<string, unknown>;
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
declare class SocketMessageError extends IskraError {
|
|
39
|
+
constructor(message: string, options?: {
|
|
40
|
+
cause?: Error;
|
|
41
|
+
context?: Record<string, unknown>;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export { SocketConnectionError, type SocketContext, SocketDriver, SocketMessageError, SocketRouter };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// src/router.ts
|
|
2
|
+
var SocketRouter = class {
|
|
3
|
+
handlers = /* @__PURE__ */ new Map();
|
|
4
|
+
on(event, handler) {
|
|
5
|
+
this.handlers.set(event, handler);
|
|
6
|
+
return this;
|
|
7
|
+
}
|
|
8
|
+
getHandler(event) {
|
|
9
|
+
return this.handlers.get(event);
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// src/errors.ts
|
|
14
|
+
import { IskraError, ErrorCodes } from "@iskra-bun/core";
|
|
15
|
+
var SocketConnectionError = class extends IskraError {
|
|
16
|
+
constructor(message, options) {
|
|
17
|
+
super(message, { code: ErrorCodes.SOCKET_CONNECTION_ERROR, ...options });
|
|
18
|
+
this.name = "SocketConnectionError";
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
var SocketMessageError = class extends IskraError {
|
|
22
|
+
constructor(message, options) {
|
|
23
|
+
super(message, { code: ErrorCodes.SOCKET_MESSAGE_ERROR, ...options });
|
|
24
|
+
this.name = "SocketMessageError";
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// src/driver.ts
|
|
29
|
+
var SocketDriver = class {
|
|
30
|
+
name = "SocketDriver";
|
|
31
|
+
app = null;
|
|
32
|
+
router;
|
|
33
|
+
port;
|
|
34
|
+
runningServer;
|
|
35
|
+
constructor(options = {}) {
|
|
36
|
+
this.port = options.port || 3001;
|
|
37
|
+
this.router = options.router || new SocketRouter();
|
|
38
|
+
}
|
|
39
|
+
init(app) {
|
|
40
|
+
this.app = app;
|
|
41
|
+
}
|
|
42
|
+
start() {
|
|
43
|
+
this.app?.logger.info(`Starting SocketDriver on port ${this.port}...`);
|
|
44
|
+
this.runningServer = Bun.serve({
|
|
45
|
+
port: this.port,
|
|
46
|
+
fetch(req, server) {
|
|
47
|
+
if (server.upgrade(req)) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
return new Response("Upgrade failed", { status: 500 });
|
|
51
|
+
},
|
|
52
|
+
websocket: {
|
|
53
|
+
open: (ws) => {
|
|
54
|
+
this.app?.logger.debug("Socket connected");
|
|
55
|
+
ws.subscribe("global");
|
|
56
|
+
this.app?.emit("socket:connected", { id: ws.remoteAddress });
|
|
57
|
+
},
|
|
58
|
+
message: async (ws, message) => {
|
|
59
|
+
await this.handleMessage(ws, message);
|
|
60
|
+
},
|
|
61
|
+
close: (ws) => {
|
|
62
|
+
ws.unsubscribe("global");
|
|
63
|
+
this.app?.logger.debug("Socket disconnected");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
broadcast(event, payload) {
|
|
69
|
+
if (this.runningServer) {
|
|
70
|
+
this.runningServer.publish("global", JSON.stringify({ event, payload }));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
stop() {
|
|
74
|
+
if (this.runningServer) {
|
|
75
|
+
this.runningServer.stop();
|
|
76
|
+
}
|
|
77
|
+
this.app?.logger.info("SocketDriver stopped");
|
|
78
|
+
}
|
|
79
|
+
async handleMessage(ws, message) {
|
|
80
|
+
try {
|
|
81
|
+
const text = typeof message === "string" ? message : new TextDecoder().decode(message);
|
|
82
|
+
const eventData = JSON.parse(text);
|
|
83
|
+
const { event, payload } = eventData;
|
|
84
|
+
if (!event) return;
|
|
85
|
+
const handler = this.router.getHandler(event);
|
|
86
|
+
if (handler && this.app) {
|
|
87
|
+
await handler({
|
|
88
|
+
app: this.app,
|
|
89
|
+
logger: this.app.logger.child({ source: "socket", event }),
|
|
90
|
+
payload: payload || {},
|
|
91
|
+
socket: ws,
|
|
92
|
+
reply: (data) => ws.send(JSON.stringify({ event: `${event}:reply`, payload: data })),
|
|
93
|
+
broadcast: (evt, data) => this.runningServer.publish(evt, JSON.stringify(data))
|
|
94
|
+
// Publish to topic/channel logic needed? Or just broadcast
|
|
95
|
+
});
|
|
96
|
+
} else {
|
|
97
|
+
this.app?.emit(`socket:${event}`, { socket: ws, payload });
|
|
98
|
+
}
|
|
99
|
+
} catch (err) {
|
|
100
|
+
const socketErr = new SocketMessageError("Failed to handle socket message", {
|
|
101
|
+
cause: err instanceof Error ? err : new Error(String(err))
|
|
102
|
+
});
|
|
103
|
+
this.app?.logger.error({ err: socketErr }, socketErr.message);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
export {
|
|
108
|
+
SocketConnectionError,
|
|
109
|
+
SocketDriver,
|
|
110
|
+
SocketMessageError,
|
|
111
|
+
SocketRouter
|
|
112
|
+
};
|
|
113
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/router.ts","../src/errors.ts","../src/driver.ts"],"sourcesContent":["import type { App, Context } from '@iskra-bun/core';\nimport type { ServerWebSocket } from 'bun';\n\nexport interface SocketContext<T = any> extends Context<T> {\n socket: ServerWebSocket<any>;\n broadcast(event: string, payload: any): void;\n}\n\nexport type SocketHandler = (ctx: SocketContext) => Promise<void> | void;\n\nexport class SocketRouter {\n private handlers: Map<string, SocketHandler> = new Map();\n\n on(event: string, handler: SocketHandler) {\n this.handlers.set(event, handler);\n return this;\n }\n\n getHandler(event: string): SocketHandler | undefined {\n return this.handlers.get(event);\n }\n}\n","import { IskraError, ErrorCodes } from '@iskra-bun/core';\n\n// ─── Socket Connection Error ─────────────────────────────────────────────────\n\nexport class SocketConnectionError extends IskraError {\n constructor(message: string, options?: { cause?: Error; context?: Record<string, unknown> }) {\n super(message, { code: ErrorCodes.SOCKET_CONNECTION_ERROR, ...options });\n this.name = 'SocketConnectionError';\n }\n}\n\n// ─── Socket Message Error ────────────────────────────────────────────────────\n\nexport class SocketMessageError extends IskraError {\n constructor(message: string, options?: { cause?: Error; context?: Record<string, unknown> }) {\n super(message, { code: ErrorCodes.SOCKET_MESSAGE_ERROR, ...options });\n this.name = 'SocketMessageError';\n }\n}\n","import type { App, Driver } from '@iskra-bun/core';\nimport { SocketRouter } from './router';\nimport type { ServerWebSocket } from 'bun';\nimport { SocketMessageError } from './errors';\n\nexport class SocketDriver implements Driver {\n name = 'SocketDriver';\n private app: App | null = null;\n private router: SocketRouter;\n private port: number;\n private runningServer: any;\n\n constructor(options: { port?: number; router?: SocketRouter } = {}) {\n this.port = options.port || 3001;\n this.router = options.router || new SocketRouter();\n }\n\n init(app: App) {\n this.app = app;\n }\n\n start() {\n this.app?.logger.info(`Starting SocketDriver on port ${this.port}...`);\n\n this.runningServer = Bun.serve({\n port: this.port,\n fetch(req, server) {\n // Upgrade logic\n if (server.upgrade(req)) {\n return; // Bun handles the rest\n }\n return new Response(\"Upgrade failed\", { status: 500 });\n },\n websocket: {\n open: (ws) => {\n this.app?.logger.debug('Socket connected');\n ws.subscribe('global');\n this.app?.emit('socket:connected', { id: ws.remoteAddress });\n },\n message: async (ws, message) => {\n await this.handleMessage(ws, message);\n },\n close: (ws) => {\n ws.unsubscribe('global');\n this.app?.logger.debug('Socket disconnected');\n }\n }\n });\n }\n\n public broadcast(event: string, payload: any) {\n if (this.runningServer) {\n this.runningServer.publish('global', JSON.stringify({ event, payload }));\n }\n }\n\n stop() {\n if (this.runningServer) {\n this.runningServer.stop();\n }\n this.app?.logger.info('SocketDriver stopped');\n }\n\n private async handleMessage(ws: ServerWebSocket<any>, message: string | Buffer) {\n try {\n const text = typeof message === 'string' ? message : new TextDecoder().decode(message);\n const eventData = JSON.parse(text);\n const { event, payload } = eventData;\n\n if (!event) return;\n\n // 1. Try Router\n const handler = this.router.getHandler(event);\n if (handler && this.app) {\n await handler({\n app: this.app,\n logger: this.app.logger.child({ source: 'socket', event }),\n payload: payload || {},\n socket: ws,\n reply: (data) => ws.send(JSON.stringify({ event: `${event}:reply`, payload: data })),\n broadcast: (evt, data) => this.runningServer.publish(evt, JSON.stringify(data)) // Publish to topic/channel logic needed? Or just broadcast\n });\n } else {\n // 2. Fallback to Global App Event\n this.app?.emit(`socket:${event}`, { socket: ws, payload });\n }\n\n } catch (err) {\n const socketErr = new SocketMessageError('Failed to handle socket message', {\n cause: err instanceof Error ? err : new Error(String(err)),\n });\n this.app?.logger.error({ err: socketErr }, socketErr.message);\n }\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@iskra-bun/socket-kit",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "WebSocket nativo de Bun para Iskra, con router y broadcast.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"iskra",
|
|
7
|
+
"bun",
|
|
8
|
+
"typescript",
|
|
9
|
+
"websocket",
|
|
10
|
+
"realtime",
|
|
11
|
+
"broadcast"
|
|
12
|
+
],
|
|
13
|
+
"author": "Joan Lascano",
|
|
14
|
+
"license": "AGPL-3.0-or-later",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/fearful/iskra.git",
|
|
18
|
+
"directory": "packages/socket-kit"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://github.com/fearful/iskra/tree/main/packages/socket-kit#readme",
|
|
21
|
+
"bugs": "https://github.com/fearful/iskra/issues",
|
|
22
|
+
"type": "module",
|
|
23
|
+
"main": "./dist/index.js",
|
|
24
|
+
"module": "./dist/index.js",
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"source": "./src/index.ts",
|
|
29
|
+
"bun": "./src/index.ts",
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"import": "./dist/index.js",
|
|
32
|
+
"default": "./dist/index.js"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist",
|
|
37
|
+
"src",
|
|
38
|
+
"README.md",
|
|
39
|
+
"CHANGELOG.md"
|
|
40
|
+
],
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"test": "bun test",
|
|
46
|
+
"build": "tsup --config ../../tsup.config.ts"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@iskra-bun/core": "0.1.0"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/bun": "^1.3.5",
|
|
53
|
+
"@types/node": "^22.10.2"
|
|
54
|
+
}
|
|
55
|
+
}
|
package/src/driver.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { App, Driver } from '@iskra-bun/core';
|
|
2
|
+
import { SocketRouter } from './router';
|
|
3
|
+
import type { ServerWebSocket } from 'bun';
|
|
4
|
+
import { SocketMessageError } from './errors';
|
|
5
|
+
|
|
6
|
+
export class SocketDriver implements Driver {
|
|
7
|
+
name = 'SocketDriver';
|
|
8
|
+
private app: App | null = null;
|
|
9
|
+
private router: SocketRouter;
|
|
10
|
+
private port: number;
|
|
11
|
+
private runningServer: any;
|
|
12
|
+
|
|
13
|
+
constructor(options: { port?: number; router?: SocketRouter } = {}) {
|
|
14
|
+
this.port = options.port || 3001;
|
|
15
|
+
this.router = options.router || new SocketRouter();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
init(app: App) {
|
|
19
|
+
this.app = app;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
start() {
|
|
23
|
+
this.app?.logger.info(`Starting SocketDriver on port ${this.port}...`);
|
|
24
|
+
|
|
25
|
+
this.runningServer = Bun.serve({
|
|
26
|
+
port: this.port,
|
|
27
|
+
fetch(req, server) {
|
|
28
|
+
// Upgrade logic
|
|
29
|
+
if (server.upgrade(req)) {
|
|
30
|
+
return; // Bun handles the rest
|
|
31
|
+
}
|
|
32
|
+
return new Response("Upgrade failed", { status: 500 });
|
|
33
|
+
},
|
|
34
|
+
websocket: {
|
|
35
|
+
open: (ws) => {
|
|
36
|
+
this.app?.logger.debug('Socket connected');
|
|
37
|
+
ws.subscribe('global');
|
|
38
|
+
this.app?.emit('socket:connected', { id: ws.remoteAddress });
|
|
39
|
+
},
|
|
40
|
+
message: async (ws, message) => {
|
|
41
|
+
await this.handleMessage(ws, message);
|
|
42
|
+
},
|
|
43
|
+
close: (ws) => {
|
|
44
|
+
ws.unsubscribe('global');
|
|
45
|
+
this.app?.logger.debug('Socket disconnected');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public broadcast(event: string, payload: any) {
|
|
52
|
+
if (this.runningServer) {
|
|
53
|
+
this.runningServer.publish('global', JSON.stringify({ event, payload }));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
stop() {
|
|
58
|
+
if (this.runningServer) {
|
|
59
|
+
this.runningServer.stop();
|
|
60
|
+
}
|
|
61
|
+
this.app?.logger.info('SocketDriver stopped');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private async handleMessage(ws: ServerWebSocket<any>, message: string | Buffer) {
|
|
65
|
+
try {
|
|
66
|
+
const text = typeof message === 'string' ? message : new TextDecoder().decode(message);
|
|
67
|
+
const eventData = JSON.parse(text);
|
|
68
|
+
const { event, payload } = eventData;
|
|
69
|
+
|
|
70
|
+
if (!event) return;
|
|
71
|
+
|
|
72
|
+
// 1. Try Router
|
|
73
|
+
const handler = this.router.getHandler(event);
|
|
74
|
+
if (handler && this.app) {
|
|
75
|
+
await handler({
|
|
76
|
+
app: this.app,
|
|
77
|
+
logger: this.app.logger.child({ source: 'socket', event }),
|
|
78
|
+
payload: payload || {},
|
|
79
|
+
socket: ws,
|
|
80
|
+
reply: (data) => ws.send(JSON.stringify({ event: `${event}:reply`, payload: data })),
|
|
81
|
+
broadcast: (evt, data) => this.runningServer.publish(evt, JSON.stringify(data)) // Publish to topic/channel logic needed? Or just broadcast
|
|
82
|
+
});
|
|
83
|
+
} else {
|
|
84
|
+
// 2. Fallback to Global App Event
|
|
85
|
+
this.app?.emit(`socket:${event}`, { socket: ws, payload });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
} catch (err) {
|
|
89
|
+
const socketErr = new SocketMessageError('Failed to handle socket message', {
|
|
90
|
+
cause: err instanceof Error ? err : new Error(String(err)),
|
|
91
|
+
});
|
|
92
|
+
this.app?.logger.error({ err: socketErr }, socketErr.message);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { IskraError, ErrorCodes } from '@iskra-bun/core';
|
|
2
|
+
|
|
3
|
+
// ─── Socket Connection Error ─────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
export class SocketConnectionError extends IskraError {
|
|
6
|
+
constructor(message: string, options?: { cause?: Error; context?: Record<string, unknown> }) {
|
|
7
|
+
super(message, { code: ErrorCodes.SOCKET_CONNECTION_ERROR, ...options });
|
|
8
|
+
this.name = 'SocketConnectionError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// ─── Socket Message Error ────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
export class SocketMessageError extends IskraError {
|
|
15
|
+
constructor(message: string, options?: { cause?: Error; context?: Record<string, unknown> }) {
|
|
16
|
+
super(message, { code: ErrorCodes.SOCKET_MESSAGE_ERROR, ...options });
|
|
17
|
+
this.name = 'SocketMessageError';
|
|
18
|
+
}
|
|
19
|
+
}
|
package/src/index.ts
ADDED
package/src/router.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { App, Context } from '@iskra-bun/core';
|
|
2
|
+
import type { ServerWebSocket } from 'bun';
|
|
3
|
+
|
|
4
|
+
export interface SocketContext<T = any> extends Context<T> {
|
|
5
|
+
socket: ServerWebSocket<any>;
|
|
6
|
+
broadcast(event: string, payload: any): void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type SocketHandler = (ctx: SocketContext) => Promise<void> | void;
|
|
10
|
+
|
|
11
|
+
export class SocketRouter {
|
|
12
|
+
private handlers: Map<string, SocketHandler> = new Map();
|
|
13
|
+
|
|
14
|
+
on(event: string, handler: SocketHandler) {
|
|
15
|
+
this.handlers.set(event, handler);
|
|
16
|
+
return this;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
getHandler(event: string): SocketHandler | undefined {
|
|
20
|
+
return this.handlers.get(event);
|
|
21
|
+
}
|
|
22
|
+
}
|