@arcblock/ws 1.28.9 → 1.29.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.
@@ -0,0 +1,12 @@
1
+ Object.defineProperty(exports, '__esModule', { value: true });
2
+ const require_rolldown_runtime = require('../_virtual/rolldown_runtime.cjs');
3
+ const require_client_base = require('./base.cjs');
4
+ let eventemitter3 = require("eventemitter3");
5
+ eventemitter3 = require_rolldown_runtime.__toESM(eventemitter3);
6
+ let phoenix = require("phoenix");
7
+
8
+ //#region src/client/browser.ts
9
+ var browser_default = require_client_base.default(phoenix.Socket, eventemitter3.default);
10
+
11
+ //#endregion
12
+ exports.default = browser_default;
@@ -0,0 +1,30 @@
1
+ import _default$1 from "../logger.cjs";
2
+ import { Channel, EventEmitterInstance, WsClientOptions } from "./base.cjs";
3
+
4
+ //#region src/client/browser.d.ts
5
+ declare const _default: {
6
+ new (endpoint: string, opts?: WsClientOptions): {
7
+ _logger: ReturnType<typeof _default$1>;
8
+ emitter: EventEmitterInstance;
9
+ on(event: string, handler: (data: unknown) => void, params?: object): void;
10
+ off(event: string, handler?: (data: unknown) => void): void;
11
+ disconnect(callback?: () => void, code?: number, reason?: string): void;
12
+ ensureJoinChannel(topic: string, params?: object): void;
13
+ ensureLeaveChannel(topic: string): void;
14
+ subscribe(topic: string, params?: object): Channel;
15
+ unsubscribe(topic: string): void;
16
+ channels: Channel[];
17
+ connect(): void;
18
+ isConnected(): boolean;
19
+ onOpen(cb: () => void): void;
20
+ onClose(cb: () => void): void;
21
+ onError(cb: (err: {
22
+ error: Error;
23
+ }) => void): void;
24
+ onMessage(cb: (message: unknown) => void): void;
25
+ channel(topic: string, params?: object | (() => object)): Channel;
26
+ remove(channel: Channel): void;
27
+ };
28
+ };
29
+ //#endregion
30
+ export { _default as default };
@@ -0,0 +1,14 @@
1
+ Object.defineProperty(exports, '__esModule', { value: true });
2
+ const require_rolldown_runtime = require('../_virtual/rolldown_runtime.cjs');
3
+ const require_client_base = require('./base.cjs');
4
+ let phoenix = require("phoenix");
5
+ let node_events = require("node:events");
6
+ node_events = require_rolldown_runtime.__toESM(node_events);
7
+ let ws = require("ws");
8
+ ws = require_rolldown_runtime.__toESM(ws);
9
+
10
+ //#region src/client/index.ts
11
+ var client_default = require_client_base.default(phoenix.Socket, node_events.default, ws.default);
12
+
13
+ //#endregion
14
+ exports.default = client_default;
@@ -0,0 +1,30 @@
1
+ import _default$1 from "../logger.cjs";
2
+ import { Channel, EventEmitterInstance, WsClientOptions } from "./base.cjs";
3
+
4
+ //#region src/client/index.d.ts
5
+ declare const _default: {
6
+ new (endpoint: string, opts?: WsClientOptions): {
7
+ _logger: ReturnType<typeof _default$1>;
8
+ emitter: EventEmitterInstance;
9
+ on(event: string, handler: (data: unknown) => void, params?: object): void;
10
+ off(event: string, handler?: (data: unknown) => void): void;
11
+ disconnect(callback?: () => void, code?: number, reason?: string): void;
12
+ ensureJoinChannel(topic: string, params?: object): void;
13
+ ensureLeaveChannel(topic: string): void;
14
+ subscribe(topic: string, params?: object): Channel;
15
+ unsubscribe(topic: string): void;
16
+ channels: Channel[];
17
+ connect(): void;
18
+ isConnected(): boolean;
19
+ onOpen(cb: () => void): void;
20
+ onClose(cb: () => void): void;
21
+ onError(cb: (err: {
22
+ error: Error;
23
+ }) => void): void;
24
+ onMessage(cb: (message: unknown) => void): void;
25
+ channel(topic: string, params?: object | (() => object)): Channel;
26
+ remove(channel: Channel): void;
27
+ };
28
+ };
29
+ //#endregion
30
+ export { _default as default };
package/lib/index.cjs ADDED
@@ -0,0 +1,5 @@
1
+ const require_client_index = require('./client/index.cjs');
2
+ const require_server_index = require('./server/index.cjs');
3
+
4
+ exports.WsClient = require_client_index.default;
5
+ exports.WsServer = require_server_index.default;
@@ -0,0 +1,3 @@
1
+ import _default from "./client/index.cjs";
2
+ import WsServer from "./server/index.cjs";
3
+ export { _default as WsClient, WsServer };
package/lib/logger.cjs ADDED
@@ -0,0 +1,19 @@
1
+ Object.defineProperty(exports, '__esModule', { value: true });
2
+ const require_rolldown_runtime = require('./_virtual/rolldown_runtime.cjs');
3
+ let debug = require("debug");
4
+ debug = require_rolldown_runtime.__toESM(debug);
5
+
6
+ //#region src/logger.ts
7
+ var logger_default = (subModule, silent = false) => {
8
+ const d = (0, debug.default)(["@arcblock/ws", subModule].join(":"));
9
+ return {
10
+ debug: d,
11
+ warn: d,
12
+ info: d,
13
+ trace: silent ? d : console.error,
14
+ error: silent ? d : console.error
15
+ };
16
+ };
17
+
18
+ //#endregion
19
+ exports.default = logger_default;
@@ -0,0 +1,13 @@
1
+ import debug from "debug";
2
+
3
+ //#region src/logger.d.ts
4
+ interface Logger {
5
+ debug: debug.Debugger | ((...args: unknown[]) => void);
6
+ warn?: debug.Debugger | ((...args: unknown[]) => void);
7
+ info: debug.Debugger | ((...args: unknown[]) => void);
8
+ trace?: (...args: unknown[]) => void;
9
+ error: (...args: unknown[]) => void;
10
+ }
11
+ declare const _default: (subModule: string, silent?: boolean) => Logger;
12
+ //#endregion
13
+ export { Logger, _default as default };
@@ -0,0 +1,479 @@
1
+ Object.defineProperty(exports, '__esModule', { value: true });
2
+ const require_rolldown_runtime = require('../_virtual/rolldown_runtime.cjs');
3
+ const require_logger = require('../logger.cjs');
4
+ let node_events = require("node:events");
5
+ node_events = require_rolldown_runtime.__toESM(node_events);
6
+ let ws = require("ws");
7
+ ws = require_rolldown_runtime.__toESM(ws);
8
+ let node_cluster = require("node:cluster");
9
+ node_cluster = require_rolldown_runtime.__toESM(node_cluster);
10
+ let lodash_get = require("lodash/get");
11
+ lodash_get = require_rolldown_runtime.__toESM(lodash_get);
12
+
13
+ //#region src/server/index.ts
14
+ const eventHub = node_cluster.default.isMaster ? require("@arcblock/event-hub/single").default : require("@arcblock/event-hub").default;
15
+ const nanoid = (length = 16) => [...Array(length)].map(() => Math.random().toString(36)[2]).join("");
16
+ const sleep = (timeout) => new Promise((resolve) => {
17
+ setTimeout(resolve, timeout);
18
+ });
19
+ const reply = ({ socket, topic, event, data = {}, status = "ok", ref = "", joinRef = "" }) => {
20
+ if (socket.readyState === ws.default.OPEN) {
21
+ const res = JSON.stringify([
22
+ joinRef,
23
+ ref,
24
+ topic,
25
+ event,
26
+ {
27
+ status,
28
+ response: data
29
+ }
30
+ ]);
31
+ socket.send(res);
32
+ }
33
+ };
34
+ const noop = () => {};
35
+ const defaultHooks = {
36
+ authenticateJoinChannel: noop,
37
+ preJoinChannel: noop,
38
+ postJoinChannel: noop,
39
+ preLeaveChannel: noop,
40
+ postLeaveChannel: noop,
41
+ postBroadcast: noop,
42
+ postSend: noop,
43
+ receiveMessage: noop
44
+ };
45
+ const refreshHeartbeat = (socket) => {
46
+ socket.heartbeatAt = Date.now();
47
+ };
48
+ const HEARTBEAT_TIMEOUT = 300 * 1e3;
49
+ /**
50
+ * Create a websocket server
51
+ *
52
+ * @param {Object} opts
53
+ * @param {String} opts.pathname - which path to mount the socket server
54
+ * @param {Object} opts.authenticate - authentication function to be called on connection
55
+ * @param {Object} opts.hooks - hooks to be called on events
56
+ * @param {Object} opts.logger - logger used to log messages
57
+ * @param {Object} opts.broadcastEventName - used in cluster mode, default is '@arcblock/ws:broadcast'
58
+ * @param {Object} opts.heartbeatTimeout - maximum non-response time of a connection socket
59
+ * @class WsServer
60
+ * @extends {EventEmitter}
61
+ */
62
+ var WsServer = class extends node_events.default {
63
+ constructor(opts = {}) {
64
+ super();
65
+ this.pathname = opts.pathname;
66
+ this.authenticate = opts.authenticate;
67
+ this.hooks = Object.assign({}, defaultHooks, opts.hooks || {});
68
+ this.logger = opts.logger || require_logger.default("server", opts.silent);
69
+ this.skipLogOnHookError = opts.skipLogOnHookError || false;
70
+ this.heartbeatTimeout = opts.heartbeatTimeout || HEARTBEAT_TIMEOUT;
71
+ this.wss = new ws.WebSocketServer({
72
+ noServer: true,
73
+ clientTracking: false
74
+ });
75
+ this.wss.on("connection", this.onWssConnection.bind(this));
76
+ this.wss.on("close", this.onWssClose.bind(this));
77
+ this.wss.on("error", this.onWssError.bind(this));
78
+ this.topics = {};
79
+ this.broadcastEventName = opts.broadcastEventName || "@arcblock/ws:broadcast";
80
+ eventHub.on(this.broadcastEventName, (data) => this._doBroadCast(data));
81
+ }
82
+ attach(server) {
83
+ server.on("upgrade", this.onConnect.bind(this));
84
+ return this;
85
+ }
86
+ onConnect(request, socket, head) {
87
+ const { pathname } = new URL(request.url, `http://${request.headers.host || "unknown"}`);
88
+ this.logger.debug("connect attempt", { pathname });
89
+ if (this.pathname && pathname !== this.pathname) {
90
+ socket.write("HTTP/1.1 404 Pathname mismatch\r\n\r\n");
91
+ socket.destroy();
92
+ return;
93
+ }
94
+ if (!this.authenticate) {
95
+ this.wss.handleUpgrade(request, socket, head, (ws$1) => {
96
+ this.wss.emit("connection", ws$1, request);
97
+ });
98
+ return;
99
+ }
100
+ this.authenticate(request, (err, authInfo) => {
101
+ if (err) {
102
+ socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
103
+ socket.destroy();
104
+ return;
105
+ }
106
+ this.wss.handleUpgrade(request, socket, head, (ws$1) => {
107
+ ws$1.authInfo = authInfo;
108
+ this.wss.emit("connection", ws$1, request);
109
+ });
110
+ });
111
+ }
112
+ /**
113
+ * Broadcast message to all subscribers of a topic, can be used as
114
+ * - broadcast(event, data) ==> broadcast(event, event, data)
115
+ * - broadcast(topic, event, data)
116
+ * - broadcast(topic, event, data, options)
117
+ */
118
+ async broadcast(...args) {
119
+ let topic;
120
+ let event;
121
+ let data;
122
+ let options = {};
123
+ let cb = () => {};
124
+ if (typeof args[args.length - 1] === "function") cb = args.pop();
125
+ if (args.length < 2) throw new Error("Broadcasting requires at least 2 arguments");
126
+ if (args.length === 2) {
127
+ [event, data] = args;
128
+ topic = event;
129
+ } else if (args.length === 3) [topic, event, data] = args;
130
+ else [topic, event, data, options] = args;
131
+ const enableLog = !!options.enableLog;
132
+ const { socketFilters, noCluster } = options;
133
+ const replyId = nanoid();
134
+ let count = 0;
135
+ if (noCluster) {
136
+ const { count: c } = this._doBroadCast({
137
+ topic,
138
+ event,
139
+ data,
140
+ enableLog,
141
+ socketFilters
142
+ });
143
+ count = c;
144
+ } else {
145
+ eventHub.on(replyId, ({ count: c } = {}) => {
146
+ if (c) count += c;
147
+ });
148
+ eventHub.broadcast(this.broadcastEventName, {
149
+ topic,
150
+ event,
151
+ data,
152
+ options,
153
+ enableLog,
154
+ replyId,
155
+ socketFilters
156
+ });
157
+ await sleep(600);
158
+ eventHub.off(replyId);
159
+ }
160
+ const opts = {
161
+ count,
162
+ topic,
163
+ event,
164
+ data,
165
+ options
166
+ };
167
+ cb(opts);
168
+ try {
169
+ await this.hooks.postBroadcast(opts);
170
+ } catch (error) {
171
+ if (!this.skipLogOnHookError) this.logger.error("postBroadcast error", { error });
172
+ }
173
+ }
174
+ _doBroadCast({ topic, event, data, enableLog, replyId, socketFilters } = {}) {
175
+ try {
176
+ let count = 0;
177
+ if (this.topics[topic]?.size) {
178
+ let conditions = null;
179
+ if (socketFilters && Object.keys(socketFilters).length) conditions = Object.entries(socketFilters);
180
+ this.topics[topic].forEach((socket) => {
181
+ const noHeartbeatTime = Date.now() - socket.heartbeatAt;
182
+ if (noHeartbeatTime > this.heartbeatTimeout) {
183
+ this.logger.error(`Socket has no heartbeat within ${Math.floor(noHeartbeatTime / 1e3)} seconds`, {
184
+ topic,
185
+ id: socket.id
186
+ });
187
+ this.topics[topic].delete(socket);
188
+ return;
189
+ }
190
+ if (conditions && !conditions.every(([key, value]) => (0, lodash_get.default)(socket, key) === value)) return;
191
+ count++;
192
+ if (enableLog) this.logger.info("broadcast message to", {
193
+ topic,
194
+ event,
195
+ id: socket.id
196
+ });
197
+ reply({
198
+ socket,
199
+ topic,
200
+ event,
201
+ data
202
+ });
203
+ });
204
+ }
205
+ if (count > 0 && replyId) eventHub.broadcast(replyId, { count });
206
+ return { count };
207
+ } catch (error) {
208
+ this.logger.error("_doBroadcast error", { error });
209
+ return {
210
+ count: 0,
211
+ error
212
+ };
213
+ }
214
+ }
215
+ /**
216
+ * Send message to 1 subscriber of a topic, can be used as
217
+ * - send(socket, event, data)
218
+ * - send(socket, topic, event, data)
219
+ * - send(socket, topic, event, data, options)
220
+ */
221
+ async send(...args) {
222
+ let socket;
223
+ let topic;
224
+ let event;
225
+ let data;
226
+ let options = {};
227
+ if (args.length < 3) throw new Error("send requires at least 3 arguments");
228
+ if (args.length === 3) {
229
+ [socket, event, data] = args;
230
+ topic = event;
231
+ } else if (args.length === 4) [socket, topic, event, data] = args;
232
+ else [socket, topic, event, data, options] = args;
233
+ const opts = {
234
+ enableLog: true,
235
+ ...options
236
+ };
237
+ if (!socket) {
238
+ this.logger.error("socket does not exist");
239
+ return;
240
+ }
241
+ if (opts.enableLog) this.logger.info("send message to", {
242
+ topic,
243
+ event,
244
+ id: socket.id
245
+ });
246
+ reply({
247
+ socket,
248
+ topic,
249
+ event,
250
+ data
251
+ });
252
+ try {
253
+ await this.hooks.postSend({
254
+ topic,
255
+ event,
256
+ data,
257
+ options
258
+ });
259
+ } catch (error) {
260
+ if (!this.skipLogOnHookError) this.logger.error("postSend error", { error });
261
+ }
262
+ }
263
+ /**
264
+ * private
265
+ */
266
+ async onWssConnection(socket) {
267
+ const wsSocket = socket;
268
+ wsSocket.id = nanoid();
269
+ wsSocket.channel = {};
270
+ refreshHeartbeat(wsSocket);
271
+ this.logger.debug("socket connected", { id: wsSocket.id });
272
+ wsSocket.on("message", async (msg) => {
273
+ this.logger.debug("socket onmessage", msg.toString());
274
+ let joinRef;
275
+ let ref;
276
+ let topic;
277
+ let event;
278
+ let payload;
279
+ try {
280
+ [joinRef, ref, topic, event, payload] = JSON.parse(msg.toString());
281
+ } catch (err) {
282
+ this.logger.error("parse socket message error", {
283
+ id: wsSocket.id,
284
+ error: err
285
+ });
286
+ return;
287
+ }
288
+ if (!topic || !event) {
289
+ this.logger.warn?.("Invalid message format, topic/event fields are required");
290
+ return;
291
+ }
292
+ if (topic === "phoenix" && event === "heartbeat") {
293
+ reply({
294
+ socket: wsSocket,
295
+ topic,
296
+ event,
297
+ ref
298
+ });
299
+ refreshHeartbeat(wsSocket);
300
+ return;
301
+ }
302
+ if (event === "phx_join") {
303
+ try {
304
+ const authInfo = await this.hooks.authenticateJoinChannel({
305
+ socket: wsSocket,
306
+ joinRef,
307
+ ref,
308
+ topic,
309
+ event,
310
+ payload
311
+ });
312
+ await this.hooks.preJoinChannel({
313
+ socket: wsSocket,
314
+ joinRef,
315
+ ref,
316
+ topic,
317
+ event,
318
+ payload
319
+ });
320
+ wsSocket.channel[topic] = { authInfo };
321
+ } catch (error) {
322
+ if (!this.skipLogOnHookError) this.logger.error("preJoinChannel error", { error });
323
+ reply({
324
+ socket: wsSocket,
325
+ topic,
326
+ event: `chan_reply_${ref}`,
327
+ data: { message: error.message },
328
+ status: "error",
329
+ ref,
330
+ joinRef
331
+ });
332
+ return;
333
+ }
334
+ if (!this.topics[topic]) this.topics[topic] = /* @__PURE__ */ new Set();
335
+ this.topics[topic].add(wsSocket);
336
+ reply({
337
+ socket: wsSocket,
338
+ topic,
339
+ event: `chan_reply_${ref}`,
340
+ ref,
341
+ joinRef
342
+ });
343
+ this.emit("channel.join", {
344
+ socket: wsSocket,
345
+ topic,
346
+ event,
347
+ payload
348
+ });
349
+ try {
350
+ await this.hooks.postJoinChannel({
351
+ socket: wsSocket,
352
+ joinRef,
353
+ ref,
354
+ topic,
355
+ event,
356
+ payload
357
+ });
358
+ } catch (error) {
359
+ if (!this.skipLogOnHookError) this.logger.error("postJoinChannel error", { error });
360
+ }
361
+ return;
362
+ }
363
+ if (event === "phx_leave") {
364
+ try {
365
+ await this.hooks.preLeaveChannel({
366
+ socket: wsSocket,
367
+ joinRef,
368
+ ref,
369
+ topic,
370
+ event,
371
+ payload
372
+ });
373
+ } catch (error) {
374
+ if (!this.skipLogOnHookError) this.logger.error("preLeaveChannel error", { error });
375
+ reply({
376
+ socket: wsSocket,
377
+ topic,
378
+ event: `chan_reply_${ref}`,
379
+ data: { message: error.message },
380
+ status: "error",
381
+ ref,
382
+ joinRef
383
+ });
384
+ return;
385
+ }
386
+ this._leaveChannel(wsSocket, topic);
387
+ reply({
388
+ socket: wsSocket,
389
+ topic,
390
+ event: `chan_reply_${ref}`,
391
+ ref,
392
+ joinRef
393
+ });
394
+ try {
395
+ await this.hooks.postLeaveChannel({
396
+ socket: wsSocket,
397
+ joinRef,
398
+ ref,
399
+ topic,
400
+ event,
401
+ payload
402
+ });
403
+ } catch (error) {
404
+ if (!this.skipLogOnHookError) this.logger.error("postLeaveChannel error", { error });
405
+ }
406
+ return;
407
+ }
408
+ try {
409
+ await this.hooks.receiveMessage({
410
+ socket: wsSocket,
411
+ joinRef,
412
+ ref,
413
+ topic,
414
+ event,
415
+ payload
416
+ });
417
+ } catch (error) {
418
+ if (!this.skipLogOnHookError) this.logger.error("receiveMessage error", { error });
419
+ reply({
420
+ socket: wsSocket,
421
+ topic,
422
+ event: `chan_reply_${ref}`,
423
+ data: { message: error.message },
424
+ status: "error",
425
+ ref,
426
+ joinRef
427
+ });
428
+ return;
429
+ }
430
+ reply({
431
+ socket: wsSocket,
432
+ topic,
433
+ event: `chan_reply_${ref}`,
434
+ ref,
435
+ joinRef
436
+ });
437
+ });
438
+ wsSocket.on("close", () => {
439
+ this.logger.debug("socket onclose", { id: wsSocket.id });
440
+ Object.keys(this.topics).forEach((topic) => this._leaveChannel(wsSocket, topic));
441
+ });
442
+ wsSocket.on("error", (err) => {
443
+ this.logger.error("socket onerror", {
444
+ id: wsSocket.id,
445
+ error: err
446
+ });
447
+ Object.keys(this.topics).forEach((topic) => this._leaveChannel(wsSocket, topic));
448
+ });
449
+ }
450
+ /**
451
+ * private
452
+ */
453
+ onWssClose() {
454
+ this.logger.debug("ws server onclose");
455
+ this.emit("close");
456
+ }
457
+ /**
458
+ * private
459
+ */
460
+ onWssError(error) {
461
+ this.logger.error("ws server error", { error });
462
+ this.emit("error", error);
463
+ }
464
+ _leaveChannel(socket, topic) {
465
+ if (this.topics[topic]) this.topics[topic].delete(socket);
466
+ this.emit("channel.leave", {
467
+ socket,
468
+ topic
469
+ });
470
+ if (!this.topics[topic] || !this.topics[topic].size) this.emit("channel.destroy", {
471
+ socket,
472
+ topic
473
+ });
474
+ }
475
+ };
476
+ var server_default = WsServer;
477
+
478
+ //#endregion
479
+ exports.default = server_default;