@hieuxyz/rpc 1.0.7 → 1.0.8

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 CHANGED
@@ -43,7 +43,7 @@ DISCORD_USER_TOKEN="YOUR_DISCORD_USER_TOKEN_HERE"
43
43
 
44
44
  ```typescript
45
45
  import * as path from 'path';
46
- import { Client, RawImage, LocalImage, logger } from '@hieuxyz/rpc';
46
+ import { Client, LocalImage, logger } from '@hieuxyz/rpc';
47
47
 
48
48
  async function start() {
49
49
  const token = process.env.DISCORD_USER_TOKEN;
@@ -54,7 +54,10 @@ async function start() {
54
54
  }
55
55
 
56
56
  // Initialize client with token
57
- const client = new Client({ token });
57
+ const client = new Client({
58
+ token,
59
+ alwaysReconnect: true,
60
+ });
58
61
 
59
62
  await client.run();
60
63
 
@@ -66,11 +69,8 @@ async function start() {
66
69
  .setType(0) // 0: Playing
67
70
  .setTimestamps(Date.now())
68
71
  .setParty(1, 5)
69
- .setLargeImage(new RawImage("mp:external/b7uybXM7LoJRB6_ig-65aX6dCHm2qGCEe8CiS5j7c2M/https/cdn.worldvectorlogo.com/logos/typescript.svg"), "TypeScript")
70
- .setSmallImage(new LocalImage(path.join(__dirname, 'vscode.png')), "VS Code")
71
- .setButtons([
72
- { label: "View on GitHub", url: "https://github.com/hieuxyz00/hieuxyz_rpc" }
73
- ]);
72
+ .setLargeImage("https://i.ibb.co/MDP0hfTM/typescript.png", "TypeScript")
73
+ .setSmallImage(new LocalImage(path.join(__dirname, 'vscode.png')), "VS Code");
74
74
 
75
75
  await client.rpc.build();
76
76
 
@@ -139,8 +139,11 @@ This is the main starting point.
139
139
  - `new Client(options)`: Create a new instance.
140
140
  - `options.token` (required): Your Discord user token.
141
141
  - `options.apiBaseUrl` (optional): Override the default image proxy service URL.
142
+ - `options.alwaysReconnect` (optional): If `true`, the client will attempt to reconnect even after a normal close (e.g., from `client.close()` or a Discord-initiated close). Defaults to `false`.
142
143
  - `client.run()`: Start connecting to Discord Gateway.
143
144
  - `client.rpc`: Access the instance of `HieuxyzRPC` to build the state.
145
+ - `client.close(force?: boolean)`: Closes the connection to the Discord Gateway.
146
+ - `force` (optional, boolean): If set to `true`, the client will close permanently and will not attempt to reconnect, overriding the `alwaysReconnect` option. Defaults to `false`.
144
147
 
145
148
  ### Class `HieuxyzRPC`
146
149
 
@@ -48,8 +48,8 @@ export declare class Client {
48
48
  run(): Promise<void>;
49
49
  /**
50
50
  * Close the connection to Discord Gateway.
51
- * Terminate RPC and clean up resources.
52
- * If `alwaysReconnect` is true, the client will attempt to reconnect after this.
51
+ * @param {boolean} [force=false] - If true, the client will close permanently and will not attempt to reconnect,
52
+ * even if `alwaysReconnect` is enabled. Defaults to false.
53
53
  */
54
- close(): void;
54
+ close(force?: boolean): void;
55
55
  }
@@ -55,12 +55,12 @@ class Client {
55
55
  }
56
56
  /**
57
57
  * Close the connection to Discord Gateway.
58
- * Terminate RPC and clean up resources.
59
- * If `alwaysReconnect` is true, the client will attempt to reconnect after this.
58
+ * @param {boolean} [force=false] - If true, the client will close permanently and will not attempt to reconnect,
59
+ * even if `alwaysReconnect` is enabled. Defaults to false.
60
60
  */
61
- close() {
61
+ close(force = false) {
62
62
  this.rpc.stopBackgroundRenewal();
63
- this.websocket.close();
63
+ this.websocket.close(force);
64
64
  }
65
65
  }
66
66
  exports.Client = Client;
@@ -11,9 +11,12 @@ export declare class DiscordWebSocket {
11
11
  private ws;
12
12
  private sequence;
13
13
  private heartbeatInterval;
14
+ private heartbeatIntervalValue;
14
15
  private sessionId;
15
16
  private resumeGatewayUrl;
16
17
  private options;
18
+ private isReconnecting;
19
+ private permanentClose;
17
20
  private resolveReady;
18
21
  /**
19
22
  * A promise will be resolved when the Gateway connection is ready.
@@ -34,9 +37,9 @@ export declare class DiscordWebSocket {
34
37
  * If there was a previous session, it will try to resume.
35
38
  */
36
39
  connect(): void;
37
- private reconnect;
38
40
  private onMessage;
39
41
  private startHeartbeating;
42
+ private sendHeartbeat;
40
43
  private identify;
41
44
  private resume;
42
45
  /**
@@ -46,9 +49,10 @@ export declare class DiscordWebSocket {
46
49
  sendActivity(presence: PresenceUpdatePayload): void;
47
50
  private sendJson;
48
51
  /**
49
- * Close the WebSocket connection and clean up the resources.
52
+ * Closes the WebSocket connection.
53
+ * @param force If true, prevents any automatic reconnection attempts.
50
54
  */
51
- close(): void;
55
+ close(force?: boolean): void;
52
56
  private cleanupHeartbeat;
53
57
  private shouldReconnect;
54
58
  }
@@ -17,9 +17,12 @@ class DiscordWebSocket {
17
17
  ws = null;
18
18
  sequence = null;
19
19
  heartbeatInterval = null;
20
+ heartbeatIntervalValue = 0;
20
21
  sessionId = null;
21
22
  resumeGatewayUrl = null;
22
23
  options;
24
+ isReconnecting = false;
25
+ permanentClose = false;
23
26
  resolveReady = () => { };
24
27
  /**
25
28
  * A promise will be resolved when the Gateway connection is ready.
@@ -51,18 +54,40 @@ class DiscordWebSocket {
51
54
  * If there was a previous session, it will try to resume.
52
55
  */
53
56
  connect() {
57
+ if (this.isReconnecting) {
58
+ logger_1.logger.info("Connection attempt aborted: reconnection already in progress.");
59
+ return;
60
+ }
61
+ this.permanentClose = false;
62
+ this.isReconnecting = true;
54
63
  this.resetReadyPromise();
55
64
  const url = this.resumeGatewayUrl || "wss://gateway.discord.gg/?v=10&encoding=json";
56
65
  logger_1.logger.info(`Attempting to connect to ${url}...`);
57
66
  this.ws = new ws_1.default(url);
58
- this.ws.on('open', () => logger_1.logger.info(`Connected to Discord Gateway at ${url}.`));
67
+ this.ws.on('open', () => {
68
+ logger_1.logger.info(`Successfully connected to Discord Gateway at ${url}.`);
69
+ this.isReconnecting = false;
70
+ });
59
71
  this.ws.on('message', this.onMessage.bind(this));
60
72
  this.ws.on('close', (code, reason) => {
61
- logger_1.logger.warn(`Connection closed: ${code} - ${reason.toString()}`);
73
+ logger_1.logger.warn(`Connection closed: ${code} - ${reason.toString('utf-8')}`);
62
74
  this.cleanupHeartbeat();
75
+ if (this.permanentClose) {
76
+ logger_1.logger.info("Connection permanently closed by client. Not reconnecting.");
77
+ return;
78
+ }
79
+ if (this.isReconnecting)
80
+ return;
63
81
  if (this.shouldReconnect(code)) {
64
- logger_1.logger.info("Attempting to reconnect...");
65
- this.reconnect(code === 4004);
82
+ setTimeout(() => {
83
+ const canResume = code !== 4004 && !!this.sessionId;
84
+ if (!canResume) {
85
+ this.sessionId = null;
86
+ this.sequence = null;
87
+ this.resumeGatewayUrl = null;
88
+ }
89
+ this.connect();
90
+ }, 500);
66
91
  }
67
92
  else {
68
93
  logger_1.logger.info("Not attempting to reconnect based on close code and client options.");
@@ -72,15 +97,6 @@ class DiscordWebSocket {
72
97
  logger_1.logger.error(`WebSocket Error: ${err.message}`);
73
98
  });
74
99
  }
75
- reconnect(forceNewSession = false) {
76
- this.ws?.terminate();
77
- if (forceNewSession) {
78
- logger_1.logger.warn("Forcing a new session. Clearing previous session data.");
79
- this.sessionId = null;
80
- this.resumeGatewayUrl = null;
81
- }
82
- setTimeout(() => this.connect(), 3000);
83
- }
84
100
  onMessage(data) {
85
101
  const payload = JSON.parse(data.toString());
86
102
  if (payload.s) {
@@ -88,8 +104,10 @@ class DiscordWebSocket {
88
104
  }
89
105
  switch (payload.op) {
90
106
  case OpCode_1.OpCode.HELLO:
91
- this.startHeartbeating(payload.d.heartbeat_interval);
92
- if (this.sessionId) {
107
+ this.heartbeatIntervalValue = payload.d.heartbeat_interval;
108
+ logger_1.logger.info(`Received HELLO. Setting heartbeat interval to ${this.heartbeatIntervalValue}ms.`);
109
+ this.startHeartbeating();
110
+ if (this.sessionId && this.sequence) {
93
111
  this.resume();
94
112
  }
95
113
  else {
@@ -99,8 +117,8 @@ class DiscordWebSocket {
99
117
  case OpCode_1.OpCode.DISPATCH:
100
118
  if (payload.t === 'READY') {
101
119
  this.sessionId = payload.d.session_id;
102
- this.resumeGatewayUrl = payload.d.resume_gateway_url;
103
- logger_1.logger.info(`Session is READY. Session ID: ${this.sessionId}`);
120
+ this.resumeGatewayUrl = payload.d.resume_gateway_url + "/?v=10&encoding=json";
121
+ logger_1.logger.info(`Session READY. Session ID: ${this.sessionId}. Resume URL set.`);
104
122
  this.resolveReady();
105
123
  }
106
124
  else if (payload.t === 'RESUMED') {
@@ -112,28 +130,27 @@ class DiscordWebSocket {
112
130
  logger_1.logger.info("Heartbeat acknowledged.");
113
131
  break;
114
132
  case OpCode_1.OpCode.INVALID_SESSION:
115
- logger_1.logger.warn(`Invalid session received. Resumable: ${payload.d}`);
116
- if (payload.d === false) {
117
- this.ws?.close(4004, "Session not resumable");
133
+ logger_1.logger.warn(`Received INVALID_SESSION. Resumable: ${payload.d}`);
134
+ if (payload.d) {
135
+ this.ws?.close(4000, "Invalid session, attempting to resume.");
118
136
  }
119
137
  else {
120
- this.ws?.close(4000, "Session is resumable, retrying");
138
+ this.ws?.close(4004, "Invalid session, starting a new session.");
121
139
  }
122
140
  break;
123
141
  case OpCode_1.OpCode.RECONNECT:
124
- logger_1.logger.info("Gateway requested reconnect. Closing and reconnecting.");
125
- this.ws?.close(4000, "Reconnect request");
142
+ logger_1.logger.info("Gateway requested RECONNECT. Closing to reconnect and resume.");
143
+ this.ws?.close(4000, "Gateway requested reconnect.");
126
144
  break;
127
145
  default:
128
146
  break;
129
147
  }
130
148
  }
131
- startHeartbeating(interval) {
149
+ startHeartbeating() {
132
150
  this.cleanupHeartbeat();
133
151
  setTimeout(() => {
134
152
  if (this.ws?.readyState === ws_1.default.OPEN) {
135
- this.sendJson({ op: OpCode_1.OpCode.HEARTBEAT, d: this.sequence });
136
- logger_1.logger.info(`Initial heartbeat sent with sequence ${this.sequence}.`);
153
+ this.sendHeartbeat();
137
154
  }
138
155
  this.heartbeatInterval = setInterval(() => {
139
156
  if (this.ws?.readyState !== ws_1.default.OPEN) {
@@ -141,10 +158,15 @@ class DiscordWebSocket {
141
158
  this.cleanupHeartbeat();
142
159
  return;
143
160
  }
144
- this.sendJson({ op: OpCode_1.OpCode.HEARTBEAT, d: this.sequence });
145
- logger_1.logger.info(`Heartbeat sent with sequence ${this.sequence}.`);
146
- }, interval);
147
- }, interval * Math.random());
161
+ this.sendHeartbeat();
162
+ }, this.heartbeatIntervalValue);
163
+ }, this.heartbeatIntervalValue * Math.random());
164
+ }
165
+ sendHeartbeat() {
166
+ if (this.ws?.readyState !== ws_1.default.OPEN)
167
+ return;
168
+ this.sendJson({ op: OpCode_1.OpCode.HEARTBEAT, d: this.sequence });
169
+ logger_1.logger.info(`Heartbeat sent with sequence ${this.sequence}.`);
148
170
  }
149
171
  identify() {
150
172
  const identifyPayload = (0, identify_1.getIdentifyPayload)(this.token);
@@ -182,10 +204,21 @@ class DiscordWebSocket {
182
204
  }
183
205
  }
184
206
  /**
185
- * Close the WebSocket connection and clean up the resources.
207
+ * Closes the WebSocket connection.
208
+ * @param force If true, prevents any automatic reconnection attempts.
186
209
  */
187
- close() {
188
- this.ws?.close(1000, "Client closed connection");
210
+ close(force = false) {
211
+ if (force) {
212
+ logger_1.logger.info("Forcing permanent closure. Reconnects will be disabled.");
213
+ this.permanentClose = true;
214
+ }
215
+ else {
216
+ logger_1.logger.info("Closing connection manually...");
217
+ }
218
+ this.isReconnecting = false;
219
+ if (this.ws) {
220
+ this.ws.close(1000, "Client initiated closure");
221
+ }
189
222
  }
190
223
  cleanupHeartbeat() {
191
224
  if (this.heartbeatInterval) {
@@ -288,7 +288,7 @@ class HieuxyzRPC {
288
288
  since: 0,
289
289
  activities: [activity],
290
290
  status: this.status,
291
- afk: false,
291
+ afk: true,
292
292
  };
293
293
  this.websocket.sendActivity(presencePayload);
294
294
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hieuxyz/rpc",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "description": "A Discord Rich Presence library for Node.js",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",