create-nexwire 4.2.5 → 4.3.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/README.md +20 -8
- package/express/.env +1 -1
- package/express/package.json +1 -1
- package/express/src/config/database.ts +48 -29
- package/express/src/config/index.ts +83 -83
- package/express/src/config/openapi.ts +6 -24
- package/express/src/config/queue.ts +30 -30
- package/express/src/config/validate.ts +50 -40
- package/express/src/database/migrations/sqlite/0000_init.sql +47 -47
- package/express/src/database/migrations/sqlite/meta/0000_snapshot.json +1 -1
- package/express/src/database/migrations/sqlite/meta/_journal.json +1 -1
- package/express/src/framework/cache/cache.ts +40 -8
- package/express/src/framework/circuit-breaker/cb.ts +88 -60
- package/express/src/framework/database/connection.ts +52 -27
- package/express/src/framework/events/dispatcher.ts +61 -49
- package/express/src/framework/facade.ts +46 -3
- package/express/src/framework/http/app.ts +28 -22
- package/express/src/framework/http/logger.ts +27 -16
- package/express/src/framework/http/openapi.ts +9 -11
- package/express/src/framework/modules/routes.ts +26 -21
- package/express/src/framework/notification/channels/broadcast.ts +15 -0
- package/express/src/framework/notification/channels/mail.ts +40 -0
- package/express/src/framework/notification/contracts.ts +29 -0
- package/express/src/framework/notification/index.ts +19 -94
- package/express/src/framework/notification/registry.ts +15 -0
- package/express/src/framework/providers/adapter.ts +27 -17
- package/express/src/framework/queue/queue.ts +43 -44
- package/express/src/framework/realtime/broadcast.ts +29 -5
- package/express/src/framework/realtime/socket.ts +149 -119
- package/express/src/framework/redis/client.ts +148 -151
- package/express/src/framework/runtime/server-adapter.ts +39 -34
- package/express/src/framework/scheduler/lock.ts +128 -116
- package/express/src/framework/server.ts +33 -49
- package/express/src/framework/session/session.ts +69 -14
- package/express/src/framework/storage/storage.ts +744 -785
- package/express/src/framework/support/mail.ts +15 -7
- package/express/src/providers/notification.ts +4 -0
- package/express/src/resources/src/layouts/Layout/Header.vue +124 -105
- package/express/src/resources/src/plugins/pulse.ts +157 -147
- package/express/src/resources/vite.config.ts +54 -43
- package/express/src/storage/database/nexwire.sqlite +0 -0
- package/express/src/storage/logs/app.log +0 -0
- package/express/src/storage/logs/fatal.log +0 -0
- package/express/src/types/global.d.ts +32 -26
- package/hono/.env +1 -1
- package/hono/package.json +1 -1
- package/hono/src/config/database.ts +48 -29
- package/hono/src/config/index.ts +83 -83
- package/hono/src/config/openapi.ts +6 -24
- package/hono/src/config/queue.ts +30 -30
- package/hono/src/config/validate.ts +50 -40
- package/hono/src/database/migrations/sqlite/0000_init.sql +47 -47
- package/hono/src/database/migrations/sqlite/meta/0000_snapshot.json +1 -1
- package/hono/src/database/migrations/sqlite/meta/_journal.json +1 -1
- package/hono/src/framework/cache/cache.ts +40 -8
- package/hono/src/framework/circuit-breaker/cb.ts +88 -60
- package/hono/src/framework/database/connection.ts +52 -27
- package/hono/src/framework/events/dispatcher.ts +61 -49
- package/hono/src/framework/facade.ts +38 -1
- package/hono/src/framework/http/openapi.ts +7 -9
- package/hono/src/framework/notification/channels/broadcast.ts +15 -0
- package/hono/src/framework/notification/channels/mail.ts +40 -0
- package/hono/src/framework/notification/contracts.ts +29 -0
- package/hono/src/framework/notification/index.ts +19 -94
- package/hono/src/framework/notification/registry.ts +15 -0
- package/hono/src/framework/providers/adapter.ts +27 -17
- package/hono/src/framework/queue/queue.ts +43 -44
- package/hono/src/framework/realtime/broadcast.ts +29 -5
- package/hono/src/framework/realtime/socket.ts +217 -196
- package/hono/src/framework/redis/client.ts +148 -151
- package/hono/src/framework/scheduler/lock.ts +128 -116
- package/hono/src/framework/server.ts +27 -29
- package/hono/src/framework/session/session.ts +62 -14
- package/hono/src/framework/storage/storage.ts +744 -785
- package/hono/src/framework/support/mail.ts +56 -48
- package/hono/src/providers/notification.ts +4 -0
- package/hono/src/resources/src/layouts/Layout/Header.vue +124 -105
- package/hono/src/resources/src/plugins/pulse.ts +157 -147
- package/hono/src/resources/vite.config.ts +54 -43
- package/hono/src/storage/database/nexwire.sqlite +0 -0
- package/hono/src/storage/logs/app.log +0 -0
- package/hono/src/storage/logs/fatal.log +0 -0
- package/hono/src/types/global.d.ts +32 -26
- package/package.json +2 -2
- package/src/index.js +139 -14
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { env } from "@/env.js";
|
|
2
|
+
import { createCircuitBreaker, isCircuitBreakerError } from "@/framework/circuit-breaker/cb.js";
|
|
2
3
|
import { socketServer } from "@/framework/realtime/socket.js";
|
|
3
4
|
import { redisClientIfReady } from "@/framework/redis/client.js";
|
|
5
|
+
import { logger } from "@/framework/support/logger.js";
|
|
4
6
|
|
|
5
7
|
export type BroadcastOptions = {
|
|
6
8
|
users?: Array<string | number>;
|
|
@@ -10,6 +12,13 @@ export type BroadcastOptions = {
|
|
|
10
12
|
all?: boolean;
|
|
11
13
|
};
|
|
12
14
|
|
|
15
|
+
// Circuit breaker for the pub/sub relay (used by workers that don't have Socket.IO)
|
|
16
|
+
const broadcastBreaker = createCircuitBreaker({
|
|
17
|
+
name: "Realtime Broadcast Relay",
|
|
18
|
+
failureThreshold: 3,
|
|
19
|
+
resetTimeout: 30000
|
|
20
|
+
});
|
|
21
|
+
|
|
13
22
|
/**
|
|
14
23
|
* Why: Emits realtime events to targeted audiences.
|
|
15
24
|
* When: Domain events need websocket fan-out.
|
|
@@ -29,18 +38,33 @@ function emitViaIo(event: string, payload: any, options: BroadcastOptions) {
|
|
|
29
38
|
for (const room of options.rooms || []) io.to(room).emit(event, payload);
|
|
30
39
|
}
|
|
31
40
|
|
|
32
|
-
export function broadcast(event: string, payload: any, options: BroadcastOptions = {}) {
|
|
41
|
+
export async function broadcast(event: string, payload: any, options: BroadcastOptions = {}) {
|
|
33
42
|
const io = socketServer();
|
|
34
43
|
|
|
44
|
+
// If this process has Socket.IO (the main API server), emit directly
|
|
35
45
|
if (io) {
|
|
36
|
-
|
|
46
|
+
try {
|
|
47
|
+
emitViaIo(event, payload, options);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
logger.error("Failed to broadcast event via Socket.IO", { error, event });
|
|
50
|
+
}
|
|
37
51
|
return;
|
|
38
52
|
}
|
|
39
53
|
|
|
40
|
-
//
|
|
54
|
+
// If this is a background worker, relay through Redis
|
|
41
55
|
const redis = redisClientIfReady();
|
|
42
56
|
if (!redis) return;
|
|
43
57
|
|
|
44
|
-
|
|
45
|
-
|
|
58
|
+
try {
|
|
59
|
+
await broadcastBreaker.exec(async () => {
|
|
60
|
+
const channel = `${env.REDIS_PREFIX}:broadcast`;
|
|
61
|
+
await redis.publish(channel, JSON.stringify({ event, payload, options }));
|
|
62
|
+
});
|
|
63
|
+
} catch (error) {
|
|
64
|
+
if (isCircuitBreakerError(error)) {
|
|
65
|
+
logger.warn("Broadcast relay skipped: Circuit breaker is open");
|
|
66
|
+
} else {
|
|
67
|
+
logger.error("Failed to relay broadcast via Redis", { error, event });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
46
70
|
}
|
|
@@ -1,196 +1,217 @@
|
|
|
1
|
-
import { createAdapter } from "@socket.io/redis-adapter";
|
|
2
|
-
import { Server as SocketIOServer } from "socket.io";
|
|
3
|
-
import { appConfig, realtimeConfig, redisConfig } from "@/config/index.js";
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
} else {
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
1
|
+
import { createAdapter } from "@socket.io/redis-adapter";
|
|
2
|
+
import { Server as SocketIOServer } from "socket.io";
|
|
3
|
+
import { appConfig, realtimeConfig, redisConfig } from "@/config/index.js";
|
|
4
|
+
import { createCircuitBreaker, isCircuitBreakerError } from "@/framework/circuit-breaker/cb.js";
|
|
5
|
+
import { authFromSocketHandshake, unauthenticatedRealtimeAuth } from "@/framework/realtime/socket-cookie.js";
|
|
6
|
+
import type { RealtimeAuthContext } from "@/framework/realtime/types.js";
|
|
7
|
+
import { redisClientIfReady } from "@/framework/redis/client.js";
|
|
8
|
+
import { isBun } from "@/framework/runtime/runtime.js";
|
|
9
|
+
import { logger } from "@/framework/support/logger.js";
|
|
10
|
+
|
|
11
|
+
export const WS_PATH = "/socket.io";
|
|
12
|
+
|
|
13
|
+
// Create a breaker for the Socket.IO Redis adapter
|
|
14
|
+
const socketAdapterBreaker = createCircuitBreaker({
|
|
15
|
+
name: "Socket.IO Redis Adapter",
|
|
16
|
+
failureThreshold: 3,
|
|
17
|
+
resetTimeout: 30000
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Why: Describes runtime-specific WebSocket wiring for the HTTP server adapter.
|
|
22
|
+
* When: startHttpServer needs Bun websocket handlers from the bun engine.
|
|
23
|
+
* Where: Cross-runtime server bootstrap.
|
|
24
|
+
* How: `bun` carries the bun-engine handler's websocket options; `node` attaches
|
|
25
|
+
* Socket.IO directly to the http server so no node field is needed.
|
|
26
|
+
*/
|
|
27
|
+
export type RealtimeServeWebSocket = {
|
|
28
|
+
node?: any;
|
|
29
|
+
bun?: {
|
|
30
|
+
websocket: any;
|
|
31
|
+
idleTimeout?: number;
|
|
32
|
+
maxRequestBodySize?: number;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type RealtimeInit = {
|
|
37
|
+
io: SocketIOServer;
|
|
38
|
+
websocketServe: RealtimeServeWebSocket;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
let io: SocketIOServer | null = null;
|
|
42
|
+
let adapterSubClient: ReturnType<typeof redisClientIfReady> | null = null;
|
|
43
|
+
let serveWebSocket: RealtimeServeWebSocket = {};
|
|
44
|
+
|
|
45
|
+
function socketAllowedOrigins() {
|
|
46
|
+
const origins = [appConfig.url, appConfig.frontendUrl].filter(Boolean) as string[];
|
|
47
|
+
if (realtimeConfig.enabled) origins.push("https://admin.socket.io");
|
|
48
|
+
return [...new Set(origins)];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function socketCors() {
|
|
52
|
+
return { origin: socketAllowedOrigins(), credentials: true };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Why: Attaches the Redis pub/sub adapter so broadcasts span processes.
|
|
57
|
+
* When: Redis is available at realtime boot.
|
|
58
|
+
* Where: Realtime bootstrap.
|
|
59
|
+
* How: Duplicates the shared client for the adapter's subscription leg.
|
|
60
|
+
*/
|
|
61
|
+
async function attachRedisAdapter() {
|
|
62
|
+
if (!redisConfig.enabled) return;
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
await socketAdapterBreaker.exec(async () => {
|
|
66
|
+
const pubClient = redisClientIfReady();
|
|
67
|
+
if (!pubClient) throw new Error("Redis client not ready");
|
|
68
|
+
|
|
69
|
+
// Socket.IO requires a duplicate connection for subscriptions
|
|
70
|
+
adapterSubClient = pubClient.duplicate();
|
|
71
|
+
await adapterSubClient.connect();
|
|
72
|
+
|
|
73
|
+
io?.adapter(createAdapter(pubClient, adapterSubClient));
|
|
74
|
+
logger.debug("Socket.IO Redis adapter attached successfully");
|
|
75
|
+
});
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (isCircuitBreakerError(error)) {
|
|
78
|
+
logger.warn("Socket.IO Redis adapter skipped due to circuit breaker");
|
|
79
|
+
} else {
|
|
80
|
+
logger.error("Failed to attach Socket.IO Redis adapter", { error });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Why: Registers auth middleware and connection handling for the singleton.
|
|
87
|
+
* When: Realtime server is created.
|
|
88
|
+
* Where: Realtime bootstrap.
|
|
89
|
+
* How: Resolves auth from handshake cookies and joins user/role/auth rooms.
|
|
90
|
+
*/
|
|
91
|
+
function registerAuthAndRooms() {
|
|
92
|
+
io?.use(async (socket, next) => {
|
|
93
|
+
const auth = await authFromSocketHandshake(socket);
|
|
94
|
+
socket.data.auth = auth as RealtimeAuthContext;
|
|
95
|
+
|
|
96
|
+
if (auth.isAuthenticated) {
|
|
97
|
+
socket.join("auth");
|
|
98
|
+
if (auth.userId) socket.join(`user:${auth.userId}`);
|
|
99
|
+
for (const role of auth.roles) socket.join(`role:${role}`);
|
|
100
|
+
} else {
|
|
101
|
+
socket.join("guest");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
next();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
io?.on("connection", (socket) => {
|
|
108
|
+
const auth = (socket.data.auth || unauthenticatedRealtimeAuth()) as RealtimeAuthContext;
|
|
109
|
+
|
|
110
|
+
logger.debug("Socket connected", {
|
|
111
|
+
socketId: socket.id,
|
|
112
|
+
authenticated: auth.isAuthenticated,
|
|
113
|
+
userId: auth.userId
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
socket.on("join", (room) => socket.join(String(room)));
|
|
117
|
+
socket.on("disconnect", (reason) => {
|
|
118
|
+
logger.info("Socket disconnected", {
|
|
119
|
+
socketId: socket.id,
|
|
120
|
+
reason,
|
|
121
|
+
authenticated: auth.isAuthenticated,
|
|
122
|
+
userId: auth.userId
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Why: Boots the singleton Socket.IO server with auth-aware room assignment.
|
|
130
|
+
* When: HTTP server starts and realtime is enabled.
|
|
131
|
+
* Where: Framework server lifecycle.
|
|
132
|
+
* How: On node attaches to the created http server; on bun binds the bun-engine
|
|
133
|
+
* and registers its `/socket.io` route on the Hono app, returning the
|
|
134
|
+
* websocket options the Bun server needs.
|
|
135
|
+
*/
|
|
136
|
+
export async function initRealtime(target: { app?: any; httpServer?: any }): Promise<RealtimeInit | null> {
|
|
137
|
+
if (!realtimeConfig.enabled) {
|
|
138
|
+
logger.debug("Socket.IO disabled (disabled in config)");
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (io) return { io, websocketServe: serveWebSocket };
|
|
143
|
+
|
|
144
|
+
if (isBun()) {
|
|
145
|
+
const { Server: Engine } = await import("@socket.io/bun-engine");
|
|
146
|
+
const engine = new Engine({ path: WS_PATH, cors: socketCors() });
|
|
147
|
+
io = new SocketIOServer();
|
|
148
|
+
io.bind(engine);
|
|
149
|
+
|
|
150
|
+
const handler = engine.handler();
|
|
151
|
+
|
|
152
|
+
serveWebSocket.bun = {
|
|
153
|
+
websocket: handler.websocket,
|
|
154
|
+
idleTimeout: handler.idleTimeout || 120,
|
|
155
|
+
maxRequestBodySize: handler.maxRequestBodySize || 1024 * 1024
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// Store engine in app for later use
|
|
159
|
+
if (target.app) {
|
|
160
|
+
(target.app as any)._engine = engine;
|
|
161
|
+
}
|
|
162
|
+
} else {
|
|
163
|
+
io = new SocketIOServer(target.httpServer, { cors: socketCors() });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
await attachRedisAdapter();
|
|
167
|
+
registerAuthAndRooms();
|
|
168
|
+
|
|
169
|
+
logger.debug(`Socket.IO enabled at ${WS_PATH}`);
|
|
170
|
+
return { io, websocketServe: serveWebSocket };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Why: Returns the active Socket.IO server instance.
|
|
175
|
+
* When: Broadcast helpers need emitter access.
|
|
176
|
+
* Where: Realtime broadcast/event internals.
|
|
177
|
+
* How: Returns nullable singleton created by initRealtime.
|
|
178
|
+
*/
|
|
179
|
+
export function socketServer() {
|
|
180
|
+
return io;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Why: Alias for socketServer for facade ergonomics.
|
|
185
|
+
* When: Existing consumers use `io` naming.
|
|
186
|
+
* Where: Framework facade export compatibility.
|
|
187
|
+
* How: Returns same singleton Socket.IO instance.
|
|
188
|
+
*/
|
|
189
|
+
export function ioServer() {
|
|
190
|
+
return io;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Why: Closes the Socket.IO server gracefully on shutdown.
|
|
195
|
+
* When: Server stop hooks.
|
|
196
|
+
* Where: Framework runtime teardown.
|
|
197
|
+
* How: Awaits close callback and clears the singleton reference.
|
|
198
|
+
*/
|
|
199
|
+
export async function closeRealtime() {
|
|
200
|
+
if (!io) return;
|
|
201
|
+
|
|
202
|
+
await new Promise<void>((resolve) => {
|
|
203
|
+
io?.close(() => resolve());
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
if (adapterSubClient) {
|
|
207
|
+
try {
|
|
208
|
+
await adapterSubClient.quit();
|
|
209
|
+
} catch {
|
|
210
|
+
adapterSubClient.disconnect();
|
|
211
|
+
}
|
|
212
|
+
adapterSubClient = null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
io = null;
|
|
216
|
+
serveWebSocket = {};
|
|
217
|
+
}
|