@fluxerjs/ws 1.0.2

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,113 @@
1
+ import { EventEmitter } from 'events';
2
+ import { GatewaySendPayload } from '@fluxerjs/types';
3
+
4
+ type WebSocketLike = {
5
+ send(data: string | ArrayBufferLike): void;
6
+ close(code?: number): void;
7
+ readyState: number;
8
+ addEventListener?(type: string, listener: (e: unknown) => void): void;
9
+ on?(event: string, cb: (data?: unknown) => void): void;
10
+ };
11
+ type WebSocketConstructor$1 = new (url: string) => WebSocketLike;
12
+ interface WebSocketShardOptions {
13
+ url: string;
14
+ token: string;
15
+ intents: number;
16
+ shardId: number;
17
+ numShards: number;
18
+ /** Gateway API version (e.g. "1" for Fluxer). Defaults to "1" when not set. */
19
+ version?: string;
20
+ WebSocket?: WebSocketConstructor$1;
21
+ }
22
+ declare class WebSocketShard extends EventEmitter {
23
+ private ws;
24
+ private readonly options;
25
+ private heartbeatInterval;
26
+ private heartbeatAt;
27
+ /** True until we send a heartbeat; then false until we get HeartbeatAck. Avoids closing before first heartbeat. */
28
+ private lastHeartbeatAck;
29
+ private sessionId;
30
+ private seq;
31
+ private destroying;
32
+ private readonly url;
33
+ private readonly WS;
34
+ /** Current reconnect delay in ms; resets on successful connect. */
35
+ private reconnectDelayMs;
36
+ private reconnectTimeout;
37
+ constructor(options: WebSocketShardOptions);
38
+ get id(): number;
39
+ get status(): number;
40
+ connect(): void;
41
+ private debug;
42
+ private handlePayload;
43
+ private handleHello;
44
+ private startHeartbeat;
45
+ private stopHeartbeat;
46
+ send(payload: GatewaySendPayload): void;
47
+ destroy(): void;
48
+ }
49
+
50
+ type WebSocketConstructor = WebSocketConstructor$1;
51
+ interface WebSocketManagerOptions {
52
+ token: string;
53
+ intents: number;
54
+ rest: {
55
+ get: (route: string) => Promise<unknown>;
56
+ };
57
+ version?: string;
58
+ shardIds?: number[];
59
+ shardCount?: number;
60
+ WebSocket?: WebSocketConstructor;
61
+ }
62
+ declare class WebSocketManager extends EventEmitter {
63
+ private readonly options;
64
+ private shards;
65
+ private gatewayUrl;
66
+ private shardCount;
67
+ constructor(options: WebSocketManagerOptions);
68
+ connect(): Promise<void>;
69
+ send(shardId: number, payload: Parameters<WebSocketShard['send']>[0]): void;
70
+ destroy(): void;
71
+ getShardCount(): number;
72
+ }
73
+
74
+ /**
75
+ * Returns the WebSocket implementation to use.
76
+ * Uses global WebSocket (browser, Node 22+, Deno, Bun) when available;
77
+ * otherwise uses the bundled `ws` package (Node 18/20).
78
+ * Users never need to install ws themselves.
79
+ */
80
+ type WSConstructor = new (url: string) => {
81
+ send(data: string | ArrayBufferLike): void;
82
+ close(code?: number): void;
83
+ readyState: number;
84
+ addEventListener?(type: string, listener: (e: unknown) => void): void;
85
+ on?(event: string, cb: (data?: unknown) => void): void;
86
+ };
87
+ declare function getDefaultWebSocketSync(): WSConstructor;
88
+ /** Async version for ESM where we need dynamic import('ws'). */
89
+ declare function getDefaultWebSocket(): Promise<WSConstructor>;
90
+
91
+ /** Fluxer gateway close codes (aligned with docs / fluxerapp/fluxer). */
92
+ declare const GatewayCloseCodes: {
93
+ readonly Normal: 1000;
94
+ readonly GoingAway: 1001;
95
+ readonly ProtocolError: 1002;
96
+ readonly UnsupportedData: 1003;
97
+ readonly NoStatusReceived: 1005;
98
+ readonly AbnormalClosure: 1006;
99
+ readonly UnknownError: 4000;
100
+ readonly UnknownOpcode: 4001;
101
+ readonly DecodeError: 4002;
102
+ readonly NotAuthenticated: 4003;
103
+ readonly AuthenticationFailed: 4004;
104
+ readonly AlreadyAuthenticated: 4005;
105
+ readonly InvalidSeq: 4007;
106
+ readonly RateLimited: 4008;
107
+ readonly SessionTimeout: 4009;
108
+ readonly InvalidShard: 4010;
109
+ readonly ShardingRequired: 4011;
110
+ readonly InvalidAPIVersion: 4012;
111
+ };
112
+
113
+ export { GatewayCloseCodes, type WebSocketConstructor, type WebSocketLike, WebSocketManager, type WebSocketManagerOptions, WebSocketShard, type WebSocketShardOptions, getDefaultWebSocket, getDefaultWebSocketSync };
@@ -0,0 +1,113 @@
1
+ import { EventEmitter } from 'events';
2
+ import { GatewaySendPayload } from '@fluxerjs/types';
3
+
4
+ type WebSocketLike = {
5
+ send(data: string | ArrayBufferLike): void;
6
+ close(code?: number): void;
7
+ readyState: number;
8
+ addEventListener?(type: string, listener: (e: unknown) => void): void;
9
+ on?(event: string, cb: (data?: unknown) => void): void;
10
+ };
11
+ type WebSocketConstructor$1 = new (url: string) => WebSocketLike;
12
+ interface WebSocketShardOptions {
13
+ url: string;
14
+ token: string;
15
+ intents: number;
16
+ shardId: number;
17
+ numShards: number;
18
+ /** Gateway API version (e.g. "1" for Fluxer). Defaults to "1" when not set. */
19
+ version?: string;
20
+ WebSocket?: WebSocketConstructor$1;
21
+ }
22
+ declare class WebSocketShard extends EventEmitter {
23
+ private ws;
24
+ private readonly options;
25
+ private heartbeatInterval;
26
+ private heartbeatAt;
27
+ /** True until we send a heartbeat; then false until we get HeartbeatAck. Avoids closing before first heartbeat. */
28
+ private lastHeartbeatAck;
29
+ private sessionId;
30
+ private seq;
31
+ private destroying;
32
+ private readonly url;
33
+ private readonly WS;
34
+ /** Current reconnect delay in ms; resets on successful connect. */
35
+ private reconnectDelayMs;
36
+ private reconnectTimeout;
37
+ constructor(options: WebSocketShardOptions);
38
+ get id(): number;
39
+ get status(): number;
40
+ connect(): void;
41
+ private debug;
42
+ private handlePayload;
43
+ private handleHello;
44
+ private startHeartbeat;
45
+ private stopHeartbeat;
46
+ send(payload: GatewaySendPayload): void;
47
+ destroy(): void;
48
+ }
49
+
50
+ type WebSocketConstructor = WebSocketConstructor$1;
51
+ interface WebSocketManagerOptions {
52
+ token: string;
53
+ intents: number;
54
+ rest: {
55
+ get: (route: string) => Promise<unknown>;
56
+ };
57
+ version?: string;
58
+ shardIds?: number[];
59
+ shardCount?: number;
60
+ WebSocket?: WebSocketConstructor;
61
+ }
62
+ declare class WebSocketManager extends EventEmitter {
63
+ private readonly options;
64
+ private shards;
65
+ private gatewayUrl;
66
+ private shardCount;
67
+ constructor(options: WebSocketManagerOptions);
68
+ connect(): Promise<void>;
69
+ send(shardId: number, payload: Parameters<WebSocketShard['send']>[0]): void;
70
+ destroy(): void;
71
+ getShardCount(): number;
72
+ }
73
+
74
+ /**
75
+ * Returns the WebSocket implementation to use.
76
+ * Uses global WebSocket (browser, Node 22+, Deno, Bun) when available;
77
+ * otherwise uses the bundled `ws` package (Node 18/20).
78
+ * Users never need to install ws themselves.
79
+ */
80
+ type WSConstructor = new (url: string) => {
81
+ send(data: string | ArrayBufferLike): void;
82
+ close(code?: number): void;
83
+ readyState: number;
84
+ addEventListener?(type: string, listener: (e: unknown) => void): void;
85
+ on?(event: string, cb: (data?: unknown) => void): void;
86
+ };
87
+ declare function getDefaultWebSocketSync(): WSConstructor;
88
+ /** Async version for ESM where we need dynamic import('ws'). */
89
+ declare function getDefaultWebSocket(): Promise<WSConstructor>;
90
+
91
+ /** Fluxer gateway close codes (aligned with docs / fluxerapp/fluxer). */
92
+ declare const GatewayCloseCodes: {
93
+ readonly Normal: 1000;
94
+ readonly GoingAway: 1001;
95
+ readonly ProtocolError: 1002;
96
+ readonly UnsupportedData: 1003;
97
+ readonly NoStatusReceived: 1005;
98
+ readonly AbnormalClosure: 1006;
99
+ readonly UnknownError: 4000;
100
+ readonly UnknownOpcode: 4001;
101
+ readonly DecodeError: 4002;
102
+ readonly NotAuthenticated: 4003;
103
+ readonly AuthenticationFailed: 4004;
104
+ readonly AlreadyAuthenticated: 4005;
105
+ readonly InvalidSeq: 4007;
106
+ readonly RateLimited: 4008;
107
+ readonly SessionTimeout: 4009;
108
+ readonly InvalidShard: 4010;
109
+ readonly ShardingRequired: 4011;
110
+ readonly InvalidAPIVersion: 4012;
111
+ };
112
+
113
+ export { GatewayCloseCodes, type WebSocketConstructor, type WebSocketLike, WebSocketManager, type WebSocketManagerOptions, WebSocketShard, type WebSocketShardOptions, getDefaultWebSocket, getDefaultWebSocketSync };
package/dist/index.js ADDED
@@ -0,0 +1,414 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ GatewayCloseCodes: () => GatewayCloseCodes,
34
+ WebSocketManager: () => WebSocketManager,
35
+ WebSocketShard: () => WebSocketShard,
36
+ getDefaultWebSocket: () => getDefaultWebSocket,
37
+ getDefaultWebSocketSync: () => getDefaultWebSocketSync
38
+ });
39
+ module.exports = __toCommonJS(index_exports);
40
+
41
+ // src/WebSocketManager.ts
42
+ var import_events2 = require("events");
43
+
44
+ // src/WebSocketShard.ts
45
+ var import_events = require("events");
46
+ var import_types = require("@fluxerjs/types");
47
+
48
+ // src/utils/getWebSocket.ts
49
+ var cached = null;
50
+ function getDefaultWebSocketSync() {
51
+ if (cached) return cached;
52
+ if (typeof globalThis.WebSocket !== "undefined") {
53
+ cached = globalThis.WebSocket;
54
+ return cached;
55
+ }
56
+ if (typeof require === "function") {
57
+ try {
58
+ cached = require("ws");
59
+ return cached;
60
+ } catch {
61
+ }
62
+ }
63
+ throw new Error(
64
+ 'No WebSocket implementation. Use Node 22+, or run with CommonJS. The "ws" package is bundled with @fluxerjs/ws.'
65
+ );
66
+ }
67
+ async function getDefaultWebSocket() {
68
+ if (cached) return cached;
69
+ if (typeof globalThis.WebSocket !== "undefined") {
70
+ cached = globalThis.WebSocket;
71
+ return cached;
72
+ }
73
+ if (typeof require === "function") {
74
+ try {
75
+ cached = require("ws");
76
+ return cached;
77
+ } catch {
78
+ }
79
+ }
80
+ const mod = await import("ws");
81
+ cached = mod.default ?? mod;
82
+ return cached;
83
+ }
84
+
85
+ // src/WebSocketShard.ts
86
+ var RECONNECT_INITIAL_MS = 1e3;
87
+ var RECONNECT_MAX_MS = 3e4;
88
+ var WebSocketShard = class extends import_events.EventEmitter {
89
+ ws = null;
90
+ options;
91
+ heartbeatInterval = null;
92
+ heartbeatAt = 0;
93
+ /** True until we send a heartbeat; then false until we get HeartbeatAck. Avoids closing before first heartbeat. */
94
+ lastHeartbeatAck = true;
95
+ sessionId = null;
96
+ seq = null;
97
+ destroying = false;
98
+ url;
99
+ WS;
100
+ /** Current reconnect delay in ms; resets on successful connect. */
101
+ reconnectDelayMs = RECONNECT_INITIAL_MS;
102
+ reconnectTimeout = null;
103
+ constructor(options) {
104
+ super();
105
+ this.options = options;
106
+ this.WS = options.WebSocket ?? getDefaultWebSocketSync();
107
+ const version = options.version ?? "1";
108
+ const params = new URLSearchParams({ v: version, encoding: "json" });
109
+ this.url = `${options.url}?${params}`;
110
+ }
111
+ get id() {
112
+ return this.options.shardId;
113
+ }
114
+ get status() {
115
+ if (!this.ws) return 0;
116
+ switch (this.ws.readyState) {
117
+ case 0:
118
+ return 1;
119
+ // Connecting
120
+ case 1:
121
+ return 2;
122
+ // Ready
123
+ case 2:
124
+ return 3;
125
+ // Closing
126
+ case 3:
127
+ return 0;
128
+ default:
129
+ return 0;
130
+ }
131
+ }
132
+ connect() {
133
+ if (this.ws?.readyState === 1) return;
134
+ this.destroying = false;
135
+ this.debug("Connecting");
136
+ const ws = new this.WS(this.url);
137
+ this.ws = ws;
138
+ const handleMessage = (data) => {
139
+ try {
140
+ const str = typeof data === "string" ? data : data.toString();
141
+ this.handlePayload(JSON.parse(str));
142
+ } catch (err) {
143
+ this.emit("error", err instanceof Error ? err : new Error(String(err)));
144
+ }
145
+ };
146
+ const scheduleReconnect = () => {
147
+ if (this.destroying) return;
148
+ const delay = Math.min(
149
+ RECONNECT_MAX_MS,
150
+ this.reconnectDelayMs * (0.75 + Math.random() * 0.5)
151
+ );
152
+ this.reconnectDelayMs = Math.min(RECONNECT_MAX_MS, this.reconnectDelayMs * 1.5);
153
+ this.debug(`Reconnecting in ${Math.round(delay)}ms\u2026`);
154
+ this.reconnectTimeout = setTimeout(() => {
155
+ this.reconnectTimeout = null;
156
+ this.connect();
157
+ }, delay);
158
+ };
159
+ const handleClose = (code) => {
160
+ this.ws = null;
161
+ this.stopHeartbeat();
162
+ this.emit("close", code);
163
+ this.debug(`Closed: ${code}`);
164
+ if (!this.destroying && shouldReconnectOnClose(code)) {
165
+ scheduleReconnect();
166
+ }
167
+ };
168
+ const handleError = (err) => {
169
+ const error = err instanceof Error ? err : new Error("WebSocket error");
170
+ this.emit("error", error);
171
+ if (!this.destroying && this.ws) {
172
+ this.ws = null;
173
+ this.stopHeartbeat();
174
+ this.debug("Connection error; will retry\u2026");
175
+ scheduleReconnect();
176
+ }
177
+ };
178
+ const handleOpen = () => {
179
+ this.debug("Socket open");
180
+ this.reconnectDelayMs = RECONNECT_INITIAL_MS;
181
+ };
182
+ if (typeof ws.addEventListener === "function") {
183
+ ws.addEventListener("open", handleOpen);
184
+ ws.addEventListener("message", (e) => handleMessage(e.data));
185
+ ws.addEventListener("close", (e) => handleClose(e.code));
186
+ ws.addEventListener("error", () => handleError(new Error("WebSocket error")));
187
+ } else if (typeof ws.on === "function") {
188
+ ws.on("open", handleOpen);
189
+ ws.on("message", (d) => handleMessage(d));
190
+ ws.on("close", (code) => handleClose(code ?? 1006));
191
+ ws.on("error", (err) => handleError(err));
192
+ }
193
+ }
194
+ debug(message) {
195
+ this.emit("debug", `[Shard ${this.id}] ${message}`);
196
+ }
197
+ handlePayload(payload) {
198
+ switch (payload.op) {
199
+ case import_types.GatewayOpcodes.Hello:
200
+ this.handleHello(payload.d);
201
+ break;
202
+ case import_types.GatewayOpcodes.HeartbeatAck:
203
+ this.lastHeartbeatAck = true;
204
+ break;
205
+ case import_types.GatewayOpcodes.Dispatch:
206
+ if (payload.t === "READY") {
207
+ const d = payload.d;
208
+ this.sessionId = d.session_id;
209
+ this.reconnectDelayMs = RECONNECT_INITIAL_MS;
210
+ this.emit("ready", payload.d);
211
+ } else if (payload.t === "RESUMED") {
212
+ this.reconnectDelayMs = RECONNECT_INITIAL_MS;
213
+ this.emit("resumed");
214
+ }
215
+ if (payload.s !== void 0) this.seq = payload.s;
216
+ this.emit("dispatch", payload);
217
+ break;
218
+ case import_types.GatewayOpcodes.InvalidSession:
219
+ this.debug(`Invalid session (d=${payload.d}), reconnecting`);
220
+ this.sessionId = null;
221
+ this.seq = null;
222
+ setTimeout(() => this.connect(), 1e3 + Math.random() * 4e3);
223
+ break;
224
+ case import_types.GatewayOpcodes.Reconnect:
225
+ this.debug("Reconnect requested");
226
+ this.ws?.close(1e3);
227
+ setTimeout(() => this.connect(), 100);
228
+ break;
229
+ default:
230
+ break;
231
+ }
232
+ }
233
+ handleHello(data) {
234
+ const jitter = Math.random() * data.heartbeat_interval;
235
+ this.heartbeatAt = Date.now() + jitter;
236
+ this.startHeartbeat(data.heartbeat_interval);
237
+ if (this.sessionId && this.seq !== null) {
238
+ this.send({
239
+ op: import_types.GatewayOpcodes.Resume,
240
+ d: {
241
+ token: this.options.token,
242
+ session_id: this.sessionId,
243
+ seq: this.seq
244
+ }
245
+ });
246
+ } else {
247
+ this.send({
248
+ op: import_types.GatewayOpcodes.Identify,
249
+ d: {
250
+ token: this.options.token,
251
+ intents: this.options.intents,
252
+ properties: {
253
+ os: process.platform ?? "unknown",
254
+ browser: "fluxer-core.js",
255
+ device: "fluxer-core.js"
256
+ }
257
+ }
258
+ });
259
+ }
260
+ }
261
+ startHeartbeat(interval) {
262
+ this.stopHeartbeat();
263
+ this.lastHeartbeatAck = true;
264
+ this.heartbeatInterval = setInterval(() => {
265
+ if (!this.lastHeartbeatAck && this.seq !== null) {
266
+ this.ws?.close(1e3);
267
+ return;
268
+ }
269
+ this.lastHeartbeatAck = false;
270
+ this.send({ op: import_types.GatewayOpcodes.Heartbeat, d: this.seq ?? null });
271
+ }, interval);
272
+ }
273
+ stopHeartbeat() {
274
+ if (this.heartbeatInterval) {
275
+ clearInterval(this.heartbeatInterval);
276
+ this.heartbeatInterval = null;
277
+ }
278
+ }
279
+ send(payload) {
280
+ if (this.ws?.readyState !== 1) return;
281
+ this.ws.send(JSON.stringify(payload));
282
+ }
283
+ destroy() {
284
+ this.destroying = true;
285
+ if (this.reconnectTimeout) {
286
+ clearTimeout(this.reconnectTimeout);
287
+ this.reconnectTimeout = null;
288
+ }
289
+ this.stopHeartbeat();
290
+ this.ws?.close(1e3);
291
+ this.ws = null;
292
+ this.sessionId = null;
293
+ this.seq = null;
294
+ }
295
+ };
296
+ function shouldReconnectOnClose(code) {
297
+ switch (code) {
298
+ case 1e3:
299
+ // Normal Closure (server may close with this to ask for reconnect)
300
+ case 1001:
301
+ // Going Away
302
+ case 1011:
303
+ return true;
304
+ case 4e3:
305
+ // Unknown error
306
+ case 4007:
307
+ // Invalid seq
308
+ case 4009:
309
+ // Session timeout
310
+ case 4010:
311
+ // Invalid shard
312
+ case 4011:
313
+ // Sharding required
314
+ case 4012:
315
+ return true;
316
+ default:
317
+ return false;
318
+ }
319
+ }
320
+
321
+ // src/WebSocketManager.ts
322
+ var WebSocketManager = class extends import_events2.EventEmitter {
323
+ options;
324
+ shards = /* @__PURE__ */ new Map();
325
+ gatewayUrl = null;
326
+ shardCount = 1;
327
+ constructor(options) {
328
+ super();
329
+ this.options = options;
330
+ }
331
+ async connect() {
332
+ let WS = this.options.WebSocket;
333
+ if (!WS) {
334
+ try {
335
+ WS = await getDefaultWebSocket();
336
+ } catch (err) {
337
+ const e = err instanceof Error ? err : new Error(String(err));
338
+ this.emit("error", { shardId: -1, error: e });
339
+ throw e;
340
+ }
341
+ }
342
+ try {
343
+ const gateway = await this.options.rest.get("/gateway/bot");
344
+ this.gatewayUrl = gateway.url;
345
+ this.shardCount = this.options.shardCount ?? gateway.shards;
346
+ } catch (err) {
347
+ const e = err instanceof Error ? err : new Error(String(err));
348
+ this.emit("error", { shardId: -1, error: e });
349
+ throw e;
350
+ }
351
+ const ids = this.options.shardIds ?? [...Array(this.shardCount).keys()];
352
+ const version = this.options.version ?? "1";
353
+ for (const id of ids) {
354
+ const shard = new WebSocketShard({
355
+ url: this.gatewayUrl,
356
+ token: this.options.token,
357
+ intents: this.options.intents,
358
+ shardId: id,
359
+ numShards: this.shardCount,
360
+ version,
361
+ WebSocket: WS
362
+ });
363
+ shard.on("ready", (data) => this.emit("ready", { shardId: id, data }));
364
+ shard.on("resumed", () => this.emit("resumed", id));
365
+ shard.on("dispatch", (payload) => this.emit("dispatch", { shardId: id, payload }));
366
+ shard.on("close", (code) => this.emit("close", { shardId: id, code }));
367
+ shard.on("error", (err) => this.emit("error", { shardId: id, error: err }));
368
+ shard.on("debug", (msg) => this.emit("debug", msg));
369
+ this.shards.set(id, shard);
370
+ shard.connect();
371
+ }
372
+ }
373
+ send(shardId, payload) {
374
+ this.shards.get(shardId)?.send(payload);
375
+ }
376
+ destroy() {
377
+ for (const shard of this.shards.values()) shard.destroy();
378
+ this.shards.clear();
379
+ this.gatewayUrl = null;
380
+ }
381
+ getShardCount() {
382
+ return this.shardCount;
383
+ }
384
+ };
385
+
386
+ // src/utils/constants.ts
387
+ var GatewayCloseCodes = {
388
+ Normal: 1e3,
389
+ GoingAway: 1001,
390
+ ProtocolError: 1002,
391
+ UnsupportedData: 1003,
392
+ NoStatusReceived: 1005,
393
+ AbnormalClosure: 1006,
394
+ UnknownError: 4e3,
395
+ UnknownOpcode: 4001,
396
+ DecodeError: 4002,
397
+ NotAuthenticated: 4003,
398
+ AuthenticationFailed: 4004,
399
+ AlreadyAuthenticated: 4005,
400
+ InvalidSeq: 4007,
401
+ RateLimited: 4008,
402
+ SessionTimeout: 4009,
403
+ InvalidShard: 4010,
404
+ ShardingRequired: 4011,
405
+ InvalidAPIVersion: 4012
406
+ };
407
+ // Annotate the CommonJS export names for ESM import in node:
408
+ 0 && (module.exports = {
409
+ GatewayCloseCodes,
410
+ WebSocketManager,
411
+ WebSocketShard,
412
+ getDefaultWebSocket,
413
+ getDefaultWebSocketSync
414
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,380 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
+
8
+ // src/WebSocketManager.ts
9
+ import { EventEmitter as EventEmitter2 } from "events";
10
+
11
+ // src/WebSocketShard.ts
12
+ import { EventEmitter } from "events";
13
+ import { GatewayOpcodes } from "@fluxerjs/types";
14
+
15
+ // src/utils/getWebSocket.ts
16
+ var cached = null;
17
+ function getDefaultWebSocketSync() {
18
+ if (cached) return cached;
19
+ if (typeof globalThis.WebSocket !== "undefined") {
20
+ cached = globalThis.WebSocket;
21
+ return cached;
22
+ }
23
+ if (typeof __require === "function") {
24
+ try {
25
+ cached = __require("ws");
26
+ return cached;
27
+ } catch {
28
+ }
29
+ }
30
+ throw new Error(
31
+ 'No WebSocket implementation. Use Node 22+, or run with CommonJS. The "ws" package is bundled with @fluxerjs/ws.'
32
+ );
33
+ }
34
+ async function getDefaultWebSocket() {
35
+ if (cached) return cached;
36
+ if (typeof globalThis.WebSocket !== "undefined") {
37
+ cached = globalThis.WebSocket;
38
+ return cached;
39
+ }
40
+ if (typeof __require === "function") {
41
+ try {
42
+ cached = __require("ws");
43
+ return cached;
44
+ } catch {
45
+ }
46
+ }
47
+ const mod = await import("ws");
48
+ cached = mod.default ?? mod;
49
+ return cached;
50
+ }
51
+
52
+ // src/WebSocketShard.ts
53
+ var RECONNECT_INITIAL_MS = 1e3;
54
+ var RECONNECT_MAX_MS = 3e4;
55
+ var WebSocketShard = class extends EventEmitter {
56
+ ws = null;
57
+ options;
58
+ heartbeatInterval = null;
59
+ heartbeatAt = 0;
60
+ /** True until we send a heartbeat; then false until we get HeartbeatAck. Avoids closing before first heartbeat. */
61
+ lastHeartbeatAck = true;
62
+ sessionId = null;
63
+ seq = null;
64
+ destroying = false;
65
+ url;
66
+ WS;
67
+ /** Current reconnect delay in ms; resets on successful connect. */
68
+ reconnectDelayMs = RECONNECT_INITIAL_MS;
69
+ reconnectTimeout = null;
70
+ constructor(options) {
71
+ super();
72
+ this.options = options;
73
+ this.WS = options.WebSocket ?? getDefaultWebSocketSync();
74
+ const version = options.version ?? "1";
75
+ const params = new URLSearchParams({ v: version, encoding: "json" });
76
+ this.url = `${options.url}?${params}`;
77
+ }
78
+ get id() {
79
+ return this.options.shardId;
80
+ }
81
+ get status() {
82
+ if (!this.ws) return 0;
83
+ switch (this.ws.readyState) {
84
+ case 0:
85
+ return 1;
86
+ // Connecting
87
+ case 1:
88
+ return 2;
89
+ // Ready
90
+ case 2:
91
+ return 3;
92
+ // Closing
93
+ case 3:
94
+ return 0;
95
+ default:
96
+ return 0;
97
+ }
98
+ }
99
+ connect() {
100
+ if (this.ws?.readyState === 1) return;
101
+ this.destroying = false;
102
+ this.debug("Connecting");
103
+ const ws = new this.WS(this.url);
104
+ this.ws = ws;
105
+ const handleMessage = (data) => {
106
+ try {
107
+ const str = typeof data === "string" ? data : data.toString();
108
+ this.handlePayload(JSON.parse(str));
109
+ } catch (err) {
110
+ this.emit("error", err instanceof Error ? err : new Error(String(err)));
111
+ }
112
+ };
113
+ const scheduleReconnect = () => {
114
+ if (this.destroying) return;
115
+ const delay = Math.min(
116
+ RECONNECT_MAX_MS,
117
+ this.reconnectDelayMs * (0.75 + Math.random() * 0.5)
118
+ );
119
+ this.reconnectDelayMs = Math.min(RECONNECT_MAX_MS, this.reconnectDelayMs * 1.5);
120
+ this.debug(`Reconnecting in ${Math.round(delay)}ms\u2026`);
121
+ this.reconnectTimeout = setTimeout(() => {
122
+ this.reconnectTimeout = null;
123
+ this.connect();
124
+ }, delay);
125
+ };
126
+ const handleClose = (code) => {
127
+ this.ws = null;
128
+ this.stopHeartbeat();
129
+ this.emit("close", code);
130
+ this.debug(`Closed: ${code}`);
131
+ if (!this.destroying && shouldReconnectOnClose(code)) {
132
+ scheduleReconnect();
133
+ }
134
+ };
135
+ const handleError = (err) => {
136
+ const error = err instanceof Error ? err : new Error("WebSocket error");
137
+ this.emit("error", error);
138
+ if (!this.destroying && this.ws) {
139
+ this.ws = null;
140
+ this.stopHeartbeat();
141
+ this.debug("Connection error; will retry\u2026");
142
+ scheduleReconnect();
143
+ }
144
+ };
145
+ const handleOpen = () => {
146
+ this.debug("Socket open");
147
+ this.reconnectDelayMs = RECONNECT_INITIAL_MS;
148
+ };
149
+ if (typeof ws.addEventListener === "function") {
150
+ ws.addEventListener("open", handleOpen);
151
+ ws.addEventListener("message", (e) => handleMessage(e.data));
152
+ ws.addEventListener("close", (e) => handleClose(e.code));
153
+ ws.addEventListener("error", () => handleError(new Error("WebSocket error")));
154
+ } else if (typeof ws.on === "function") {
155
+ ws.on("open", handleOpen);
156
+ ws.on("message", (d) => handleMessage(d));
157
+ ws.on("close", (code) => handleClose(code ?? 1006));
158
+ ws.on("error", (err) => handleError(err));
159
+ }
160
+ }
161
+ debug(message) {
162
+ this.emit("debug", `[Shard ${this.id}] ${message}`);
163
+ }
164
+ handlePayload(payload) {
165
+ switch (payload.op) {
166
+ case GatewayOpcodes.Hello:
167
+ this.handleHello(payload.d);
168
+ break;
169
+ case GatewayOpcodes.HeartbeatAck:
170
+ this.lastHeartbeatAck = true;
171
+ break;
172
+ case GatewayOpcodes.Dispatch:
173
+ if (payload.t === "READY") {
174
+ const d = payload.d;
175
+ this.sessionId = d.session_id;
176
+ this.reconnectDelayMs = RECONNECT_INITIAL_MS;
177
+ this.emit("ready", payload.d);
178
+ } else if (payload.t === "RESUMED") {
179
+ this.reconnectDelayMs = RECONNECT_INITIAL_MS;
180
+ this.emit("resumed");
181
+ }
182
+ if (payload.s !== void 0) this.seq = payload.s;
183
+ this.emit("dispatch", payload);
184
+ break;
185
+ case GatewayOpcodes.InvalidSession:
186
+ this.debug(`Invalid session (d=${payload.d}), reconnecting`);
187
+ this.sessionId = null;
188
+ this.seq = null;
189
+ setTimeout(() => this.connect(), 1e3 + Math.random() * 4e3);
190
+ break;
191
+ case GatewayOpcodes.Reconnect:
192
+ this.debug("Reconnect requested");
193
+ this.ws?.close(1e3);
194
+ setTimeout(() => this.connect(), 100);
195
+ break;
196
+ default:
197
+ break;
198
+ }
199
+ }
200
+ handleHello(data) {
201
+ const jitter = Math.random() * data.heartbeat_interval;
202
+ this.heartbeatAt = Date.now() + jitter;
203
+ this.startHeartbeat(data.heartbeat_interval);
204
+ if (this.sessionId && this.seq !== null) {
205
+ this.send({
206
+ op: GatewayOpcodes.Resume,
207
+ d: {
208
+ token: this.options.token,
209
+ session_id: this.sessionId,
210
+ seq: this.seq
211
+ }
212
+ });
213
+ } else {
214
+ this.send({
215
+ op: GatewayOpcodes.Identify,
216
+ d: {
217
+ token: this.options.token,
218
+ intents: this.options.intents,
219
+ properties: {
220
+ os: process.platform ?? "unknown",
221
+ browser: "fluxer-core.js",
222
+ device: "fluxer-core.js"
223
+ }
224
+ }
225
+ });
226
+ }
227
+ }
228
+ startHeartbeat(interval) {
229
+ this.stopHeartbeat();
230
+ this.lastHeartbeatAck = true;
231
+ this.heartbeatInterval = setInterval(() => {
232
+ if (!this.lastHeartbeatAck && this.seq !== null) {
233
+ this.ws?.close(1e3);
234
+ return;
235
+ }
236
+ this.lastHeartbeatAck = false;
237
+ this.send({ op: GatewayOpcodes.Heartbeat, d: this.seq ?? null });
238
+ }, interval);
239
+ }
240
+ stopHeartbeat() {
241
+ if (this.heartbeatInterval) {
242
+ clearInterval(this.heartbeatInterval);
243
+ this.heartbeatInterval = null;
244
+ }
245
+ }
246
+ send(payload) {
247
+ if (this.ws?.readyState !== 1) return;
248
+ this.ws.send(JSON.stringify(payload));
249
+ }
250
+ destroy() {
251
+ this.destroying = true;
252
+ if (this.reconnectTimeout) {
253
+ clearTimeout(this.reconnectTimeout);
254
+ this.reconnectTimeout = null;
255
+ }
256
+ this.stopHeartbeat();
257
+ this.ws?.close(1e3);
258
+ this.ws = null;
259
+ this.sessionId = null;
260
+ this.seq = null;
261
+ }
262
+ };
263
+ function shouldReconnectOnClose(code) {
264
+ switch (code) {
265
+ case 1e3:
266
+ // Normal Closure (server may close with this to ask for reconnect)
267
+ case 1001:
268
+ // Going Away
269
+ case 1011:
270
+ return true;
271
+ case 4e3:
272
+ // Unknown error
273
+ case 4007:
274
+ // Invalid seq
275
+ case 4009:
276
+ // Session timeout
277
+ case 4010:
278
+ // Invalid shard
279
+ case 4011:
280
+ // Sharding required
281
+ case 4012:
282
+ return true;
283
+ default:
284
+ return false;
285
+ }
286
+ }
287
+
288
+ // src/WebSocketManager.ts
289
+ var WebSocketManager = class extends EventEmitter2 {
290
+ options;
291
+ shards = /* @__PURE__ */ new Map();
292
+ gatewayUrl = null;
293
+ shardCount = 1;
294
+ constructor(options) {
295
+ super();
296
+ this.options = options;
297
+ }
298
+ async connect() {
299
+ let WS = this.options.WebSocket;
300
+ if (!WS) {
301
+ try {
302
+ WS = await getDefaultWebSocket();
303
+ } catch (err) {
304
+ const e = err instanceof Error ? err : new Error(String(err));
305
+ this.emit("error", { shardId: -1, error: e });
306
+ throw e;
307
+ }
308
+ }
309
+ try {
310
+ const gateway = await this.options.rest.get("/gateway/bot");
311
+ this.gatewayUrl = gateway.url;
312
+ this.shardCount = this.options.shardCount ?? gateway.shards;
313
+ } catch (err) {
314
+ const e = err instanceof Error ? err : new Error(String(err));
315
+ this.emit("error", { shardId: -1, error: e });
316
+ throw e;
317
+ }
318
+ const ids = this.options.shardIds ?? [...Array(this.shardCount).keys()];
319
+ const version = this.options.version ?? "1";
320
+ for (const id of ids) {
321
+ const shard = new WebSocketShard({
322
+ url: this.gatewayUrl,
323
+ token: this.options.token,
324
+ intents: this.options.intents,
325
+ shardId: id,
326
+ numShards: this.shardCount,
327
+ version,
328
+ WebSocket: WS
329
+ });
330
+ shard.on("ready", (data) => this.emit("ready", { shardId: id, data }));
331
+ shard.on("resumed", () => this.emit("resumed", id));
332
+ shard.on("dispatch", (payload) => this.emit("dispatch", { shardId: id, payload }));
333
+ shard.on("close", (code) => this.emit("close", { shardId: id, code }));
334
+ shard.on("error", (err) => this.emit("error", { shardId: id, error: err }));
335
+ shard.on("debug", (msg) => this.emit("debug", msg));
336
+ this.shards.set(id, shard);
337
+ shard.connect();
338
+ }
339
+ }
340
+ send(shardId, payload) {
341
+ this.shards.get(shardId)?.send(payload);
342
+ }
343
+ destroy() {
344
+ for (const shard of this.shards.values()) shard.destroy();
345
+ this.shards.clear();
346
+ this.gatewayUrl = null;
347
+ }
348
+ getShardCount() {
349
+ return this.shardCount;
350
+ }
351
+ };
352
+
353
+ // src/utils/constants.ts
354
+ var GatewayCloseCodes = {
355
+ Normal: 1e3,
356
+ GoingAway: 1001,
357
+ ProtocolError: 1002,
358
+ UnsupportedData: 1003,
359
+ NoStatusReceived: 1005,
360
+ AbnormalClosure: 1006,
361
+ UnknownError: 4e3,
362
+ UnknownOpcode: 4001,
363
+ DecodeError: 4002,
364
+ NotAuthenticated: 4003,
365
+ AuthenticationFailed: 4004,
366
+ AlreadyAuthenticated: 4005,
367
+ InvalidSeq: 4007,
368
+ RateLimited: 4008,
369
+ SessionTimeout: 4009,
370
+ InvalidShard: 4010,
371
+ ShardingRequired: 4011,
372
+ InvalidAPIVersion: 4012
373
+ };
374
+ export {
375
+ GatewayCloseCodes,
376
+ WebSocketManager,
377
+ WebSocketShard,
378
+ getDefaultWebSocket,
379
+ getDefaultWebSocketSync
380
+ };
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@fluxerjs/ws",
3
+ "publishConfig": {
4
+ "access": "public"
5
+ },
6
+ "version": "1.0.2",
7
+ "description": "WebSocket manager for the Fluxer Gateway",
8
+ "main": "./dist/index.js",
9
+ "module": "./dist/index.mjs",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.mjs",
15
+ "require": "./dist/index.js"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist"
20
+ ],
21
+ "dependencies": {
22
+ "ws": "^8.18.0",
23
+ "@fluxerjs/types": "1.0.2"
24
+ },
25
+ "devDependencies": {
26
+ "@types/node": "^20.0.0",
27
+ "tsup": "^8.3.0",
28
+ "typescript": "^5.6.0"
29
+ },
30
+ "scripts": {
31
+ "build": "tsup src/index.ts --format cjs,esm --dts",
32
+ "clean": "rm -rf dist"
33
+ }
34
+ }