@hieuxyz/rpc 1.2.1 → 1.2.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.
package/README.md CHANGED
@@ -15,6 +15,7 @@ An easy-to-use and powerful Discord Rich Presence (RPC) library built for the No
15
15
 
16
16
  ## Outstanding features
17
17
 
18
+ - **Multi-RPC Support:** Display multiple activities simultaneously (e.g., Playing a game AND Listening to music) on a single client connection.
18
19
  - **Flexible Builder Pattern:** Easily build your RPC state with intuitive chainable methods.
19
20
  - **Easy to use:** The `Client` class abstracts away all the complex connection and setup logic, letting you get started with just a few lines of code.
20
21
 
@@ -105,6 +106,31 @@ start().catch(err => {
105
106
  });
106
107
  ```
107
108
 
109
+ ## Multi-RPC Usage
110
+
111
+ You can display multiple statuses at once (Discord limits how these are shown, but the data is sent).
112
+
113
+ ```typescript
114
+ // Configure the default RPC
115
+ client.rpc
116
+ .setName("Visual Studio Code")
117
+ .setState("Coding...")
118
+ .setType(0); // Playing
119
+
120
+ // Create a second RPC instance
121
+ const musicRpc = client.createRPC();
122
+ musicRpc
123
+ .setName("Spotify")
124
+ .setDetails("Listening to music")
125
+ .setApplicationId('12345678901234567') // A different ID is needed than the one already in use
126
+ .setType(2); // Listening
127
+
128
+ await client.rpc.build();
129
+
130
+ // To remove a specific RPC later and free memory:
131
+ // client.removeRPC(musicRpc);
132
+ ```
133
+
108
134
  ## Advanced Usage
109
135
 
110
136
  ### Client Spoofing
@@ -212,6 +238,8 @@ Main builder class for RPC.
212
238
  - `.build()`: Builds and sends the presence payload to Discord.
213
239
  - `.updateRPC()`: Alias for `build()`.
214
240
  - `.clear()`: Clears the Rich Presence from the user's profile and resets the builder.
241
+ - `.clearCache()`
242
+ - `.destroy()`
215
243
 
216
244
  ### Types of images
217
245
 
@@ -39,10 +39,14 @@ export interface ClientOptions {
39
39
  */
40
40
  export declare class Client {
41
41
  /**
42
- * Provides access to RPC constructor methods.
43
- * Use this to set your Rich Presence state details.
42
+ * The default RPC instance.
43
+ * Use this to set your main Rich Presence state details.
44
44
  */
45
45
  readonly rpc: HieuxyzRPC;
46
+ /**
47
+ * List of all RPC instances managed by this client.
48
+ */
49
+ private rpcs;
46
50
  /**
47
51
  * Information about the logged-in user.
48
52
  * Populated after run() resolves.
@@ -57,6 +61,22 @@ export declare class Client {
57
61
  * @throws {Error} If no token is provided in the options.
58
62
  */
59
63
  constructor(options: ClientOptions);
64
+ /**
65
+ * Create a new RPC instance.
66
+ * Use this if you want to display multiple activities simultaneously (Multi-RPC).
67
+ * @returns {HieuxyzRPC} A new RPC builder instance.
68
+ */
69
+ createRPC(): HieuxyzRPC;
70
+ /**
71
+ * Removes an RPC instance and cleans up its resources.
72
+ * @param {HieuxyzRPC} rpcInstance The RPC instance to remove.
73
+ */
74
+ removeRPC(rpcInstance: HieuxyzRPC): void;
75
+ /**
76
+ * Aggregates activities from all RPC instances and sends them to Discord.
77
+ * Uses Promise.all for parallel asset resolution.
78
+ */
79
+ private sendAllActivities;
60
80
  /**
61
81
  * Displays information about the library.
62
82
  */
@@ -20,10 +20,14 @@ const types_1 = require("./gateway/entities/types");
20
20
  */
21
21
  class Client {
22
22
  /**
23
- * Provides access to RPC constructor methods.
24
- * Use this to set your Rich Presence state details.
23
+ * The default RPC instance.
24
+ * Use this to set your main Rich Presence state details.
25
25
  */
26
26
  rpc;
27
+ /**
28
+ * List of all RPC instances managed by this client.
29
+ */
30
+ rpcs = [];
27
31
  /**
28
32
  * Information about the logged-in user.
29
33
  * Populated after run() resolves.
@@ -48,14 +52,59 @@ class Client {
48
52
  properties: options.properties,
49
53
  connectionTimeout: options.connectionTimeout,
50
54
  });
51
- this.rpc = new HieuxyzRPC_1.HieuxyzRPC(this.websocket, this.imageService);
55
+ this.rpc = this.createRPC();
52
56
  this.printAbout();
53
57
  }
58
+ /**
59
+ * Create a new RPC instance.
60
+ * Use this if you want to display multiple activities simultaneously (Multi-RPC).
61
+ * @returns {HieuxyzRPC} A new RPC builder instance.
62
+ */
63
+ createRPC() {
64
+ const newRpc = new HieuxyzRPC_1.HieuxyzRPC(this.imageService, async () => {
65
+ await this.sendAllActivities();
66
+ });
67
+ this.rpcs.push(newRpc);
68
+ return newRpc;
69
+ }
70
+ /**
71
+ * Removes an RPC instance and cleans up its resources.
72
+ * @param {HieuxyzRPC} rpcInstance The RPC instance to remove.
73
+ */
74
+ removeRPC(rpcInstance) {
75
+ const index = this.rpcs.indexOf(rpcInstance);
76
+ if (index > -1) {
77
+ rpcInstance.destroy();
78
+ this.rpcs.splice(index, 1);
79
+ this.sendAllActivities();
80
+ }
81
+ }
82
+ /**
83
+ * Aggregates activities from all RPC instances and sends them to Discord.
84
+ * Uses Promise.all for parallel asset resolution.
85
+ */
86
+ async sendAllActivities() {
87
+ const potentialActivities = await Promise.all(this.rpcs.map((rpc) => rpc.buildActivity()));
88
+ const activities = potentialActivities.filter((a) => a !== null);
89
+ let status = 'online';
90
+ for (let i = this.rpcs.length - 1; i >= 0; i--) {
91
+ if (this.rpcs[i].currentStatus) {
92
+ status = this.rpcs[i].currentStatus;
93
+ break;
94
+ }
95
+ }
96
+ this.websocket.sendActivity({
97
+ since: 0,
98
+ activities: activities,
99
+ status: status,
100
+ afk: true,
101
+ });
102
+ }
54
103
  /**
55
104
  * Displays information about the library.
56
105
  */
57
106
  printAbout() {
58
- const version = '1.2.01';
107
+ const version = '1.2.2';
59
108
  console.log(`
60
109
  _ _
61
110
  | |__ (_) ___ _ ___ ___ _ ______
@@ -200,7 +249,7 @@ class Client {
200
249
  * even if `alwaysReconnect` is enabled. Defaults to false.
201
250
  */
202
251
  close(force = false) {
203
- this.rpc.stopBackgroundRenewal();
252
+ this.rpcs.forEach((rpc) => rpc.destroy());
204
253
  this.websocket.close(force);
205
254
  }
206
255
  }
@@ -1,5 +1,4 @@
1
- import { DiscordWebSocket } from '../gateway/DiscordWebSocket';
2
- import { SettableActivityType } from '../gateway/entities/types';
1
+ import { Activity, SettableActivityType } from '../gateway/entities/types';
3
2
  import { ImageService } from './ImageService';
4
3
  import { RpcImage } from './RpcImage';
5
4
  /**
@@ -24,12 +23,13 @@ interface RpcSecrets {
24
23
  match?: string;
25
24
  }
26
25
  export type DiscordPlatform = 'desktop' | 'android' | 'ios' | 'samsung' | 'xbox' | 'ps4' | 'ps5' | 'embedded';
26
+ type UpdateCallback = () => Promise<void>;
27
27
  /**
28
28
  * Class built for creating and managing Discord Rich Presence states.
29
29
  */
30
30
  export declare class HieuxyzRPC {
31
- private websocket;
32
31
  private imageService;
32
+ private onUpdate;
33
33
  private activity;
34
34
  private assets;
35
35
  private status;
@@ -41,13 +41,17 @@ export declare class HieuxyzRPC {
41
41
  * Value: The resolved asset key (e.g., "mp:attachments/...").
42
42
  */
43
43
  private resolvedAssetsCache;
44
+ /**
45
+ * Maximum number of items in cache to prevent memory leaks/bloat over long runtime.
46
+ */
47
+ private readonly MAX_CACHE_SIZE;
44
48
  private renewalInterval;
45
49
  /**
46
50
  * Cache for Application Assets (Bot Assets).
47
51
  * Map<ApplicationID, Map<AssetName, AssetID>>
48
52
  */
49
53
  private applicationAssetsCache;
50
- constructor(websocket: DiscordWebSocket, imageService: ImageService);
54
+ constructor(imageService: ImageService, onUpdate: UpdateCallback);
51
55
  /**
52
56
  * Returns the URL of the large image asset, if available.
53
57
  * @type {string | null}
@@ -186,9 +190,14 @@ export declare class HieuxyzRPC {
186
190
  */
187
191
  private ensureAppAssetsLoaded;
188
192
  private resolveImage;
189
- private buildActivity;
190
193
  /**
191
- * Build the final Rich Presence payload and send it to Discord.
194
+ * Publicly accessible method to build the Activity object.
195
+ * Used by Client to aggregate activities from multiple RPC instances.
196
+ * @returns {Promise<Activity | null>} The constructed activity or null if empty.
197
+ */
198
+ buildActivity(): Promise<Activity | null>;
199
+ /**
200
+ * Build the final Rich Presence payload and notify the Client to send it to Discord.
192
201
  * @returns {Promise<void>}
193
202
  */
194
203
  build(): Promise<void>;
@@ -199,11 +208,21 @@ export declare class HieuxyzRPC {
199
208
  */
200
209
  updateRPC(): Promise<void>;
201
210
  /**
202
- * Clears the current Rich Presence from Discord and resets the builder state.
203
- * This sends an empty activity payload to Discord and then resets all configured
204
- * options (name, details, images, etc.) to their default values, allowing you
205
- * to build a new presence from scratch.
211
+ * Clears the current Rich Presence from the user's profile and resets the builder.
206
212
  */
207
213
  clear(): void;
214
+ /**
215
+ * Manually clear the asset cache to free memory.
216
+ */
217
+ clearCache(): void;
218
+ /**
219
+ * Permanently destroy this RPC instance.
220
+ * Stops renewal timers and clears memory.
221
+ */
222
+ destroy(): void;
223
+ /**
224
+ * Get the current status set for this RPC instance.
225
+ */
226
+ get currentStatus(): "online" | "dnd" | "idle" | "invisible" | "offline";
208
227
  }
209
228
  export {};
@@ -21,8 +21,8 @@ var ActivityFlags;
21
21
  * Class built for creating and managing Discord Rich Presence states.
22
22
  */
23
23
  class HieuxyzRPC {
24
- websocket;
25
24
  imageService;
25
+ onUpdate;
26
26
  activity = {};
27
27
  assets = {};
28
28
  status = 'online';
@@ -34,15 +34,19 @@ class HieuxyzRPC {
34
34
  * Value: The resolved asset key (e.g., "mp:attachments/...").
35
35
  */
36
36
  resolvedAssetsCache = new Map();
37
+ /**
38
+ * Maximum number of items in cache to prevent memory leaks/bloat over long runtime.
39
+ */
40
+ MAX_CACHE_SIZE = 50;
37
41
  renewalInterval = null;
38
42
  /**
39
43
  * Cache for Application Assets (Bot Assets).
40
44
  * Map<ApplicationID, Map<AssetName, AssetID>>
41
45
  */
42
46
  applicationAssetsCache = new Map();
43
- constructor(websocket, imageService) {
44
- this.websocket = websocket;
47
+ constructor(imageService, onUpdate) {
45
48
  this.imageService = imageService;
49
+ this.onUpdate = onUpdate;
46
50
  this.startBackgroundRenewal();
47
51
  }
48
52
  /**
@@ -350,7 +354,7 @@ class HieuxyzRPC {
350
354
  async renewAssetIfNeeded(cacheKey, assetKey) {
351
355
  const expiryTimeMs = this.getExpiryTime(assetKey);
352
356
  if (expiryTimeMs && expiryTimeMs < Date.now() + 3600000) {
353
- logger_1.logger.info(`Asset ${cacheKey} is expiring soon. Renewing...`);
357
+ // logger.info(`Asset ${cacheKey} is expiring soon. Renewing...`);
354
358
  const assetId = assetKey.split('mp:attachments/')[1];
355
359
  const newAsset = await this.imageService.renewImage(assetId);
356
360
  if (newAsset) {
@@ -366,7 +370,7 @@ class HieuxyzRPC {
366
370
  clearInterval(this.renewalInterval);
367
371
  }
368
372
  this.renewalInterval = setInterval(async () => {
369
- logger_1.logger.info('Running background asset renewal check...');
373
+ // logger.info('Running background asset renewal check...');
370
374
  for (const [cacheKey, assetKey] of this.resolvedAssetsCache.entries()) {
371
375
  await this.renewAssetIfNeeded(cacheKey, assetKey);
372
376
  }
@@ -379,7 +383,7 @@ class HieuxyzRPC {
379
383
  if (this.renewalInterval) {
380
384
  clearInterval(this.renewalInterval);
381
385
  this.renewalInterval = null;
382
- logger_1.logger.info('Stopped background asset renewal process.');
386
+ // logger.info('Stopped background asset renewal process.');
383
387
  }
384
388
  }
385
389
  /**
@@ -412,6 +416,12 @@ class HieuxyzRPC {
412
416
  }
413
417
  return assetId;
414
418
  }
419
+ if (this.resolvedAssetsCache.size >= this.MAX_CACHE_SIZE && !this.resolvedAssetsCache.has(cacheKey)) {
420
+ const oldestKey = this.resolvedAssetsCache.keys().next().value;
421
+ if (oldestKey) {
422
+ this.resolvedAssetsCache.delete(oldestKey);
423
+ }
424
+ }
415
425
  const cachedAsset = this.resolvedAssetsCache.get(cacheKey);
416
426
  if (cachedAsset) {
417
427
  return await this.renewAssetIfNeeded(cacheKey, cachedAsset);
@@ -425,7 +435,15 @@ class HieuxyzRPC {
425
435
  }
426
436
  return resolvedAsset;
427
437
  }
438
+ /**
439
+ * Publicly accessible method to build the Activity object.
440
+ * Used by Client to aggregate activities from multiple RPC instances.
441
+ * @returns {Promise<Activity | null>} The constructed activity or null if empty.
442
+ */
428
443
  async buildActivity() {
444
+ if (Object.keys(this.activity).length === 0 && !this.assets.large_image && !this.assets.small_image) {
445
+ return null;
446
+ }
429
447
  const large_image = await this.resolveImage(this.assets.large_image);
430
448
  const small_image = await this.resolveImage(this.assets.small_image);
431
449
  const finalAssets = {
@@ -449,18 +467,11 @@ class HieuxyzRPC {
449
467
  return this.cleanupNulls(finalActivity);
450
468
  }
451
469
  /**
452
- * Build the final Rich Presence payload and send it to Discord.
470
+ * Build the final Rich Presence payload and notify the Client to send it to Discord.
453
471
  * @returns {Promise<void>}
454
472
  */
455
473
  async build() {
456
- const activity = await this.buildActivity();
457
- const presencePayload = {
458
- since: 0,
459
- activities: [activity],
460
- status: this.status,
461
- afk: true,
462
- };
463
- this.websocket.sendActivity(presencePayload);
474
+ await this.onUpdate();
464
475
  }
465
476
  /**
466
477
  * Sends an update to an existing RPC.
@@ -471,25 +482,39 @@ class HieuxyzRPC {
471
482
  await this.build();
472
483
  }
473
484
  /**
474
- * Clears the current Rich Presence from Discord and resets the builder state.
475
- * This sends an empty activity payload to Discord and then resets all configured
476
- * options (name, details, images, etc.) to their default values, allowing you
477
- * to build a new presence from scratch.
485
+ * Clears the current Rich Presence from the user's profile and resets the builder.
478
486
  */
479
487
  clear() {
480
- const clearPayload = {
481
- since: 0,
482
- activities: [],
483
- status: this.status,
484
- afk: true,
485
- };
486
- this.websocket.sendActivity(clearPayload);
487
- logger_1.logger.info('Rich Presence cleared from Discord.');
488
488
  this.activity = {};
489
489
  this.assets = {};
490
490
  this.applicationId = '1416676323459469363'; // Reset to default
491
491
  this.platform = 'desktop'; // Reset to default
492
- logger_1.logger.info('RPC builder has been reset to its initial state.');
492
+ logger_1.logger.info('RPC instance cleared.');
493
+ this.onUpdate();
494
+ }
495
+ /**
496
+ * Manually clear the asset cache to free memory.
497
+ */
498
+ clearCache() {
499
+ this.resolvedAssetsCache.clear();
500
+ this.applicationAssetsCache.clear();
501
+ logger_1.logger.info('RPC Asset cache has been cleared.');
502
+ }
503
+ /**
504
+ * Permanently destroy this RPC instance.
505
+ * Stops renewal timers and clears memory.
506
+ */
507
+ destroy() {
508
+ this.stopBackgroundRenewal();
509
+ this.clearCache();
510
+ this.activity = {};
511
+ this.assets = {};
512
+ }
513
+ /**
514
+ * Get the current status set for this RPC instance.
515
+ */
516
+ get currentStatus() {
517
+ return this.status;
493
518
  }
494
519
  }
495
520
  exports.HieuxyzRPC = HieuxyzRPC;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hieuxyz/rpc",
3
- "version": "1.2.01",
3
+ "version": "1.2.2",
4
4
  "description": "A Discord Rich Presence library for Node.js",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -10,8 +10,15 @@
10
10
  "LICENSE"
11
11
  ],
12
12
  "scripts": {
13
+ "tsx": "tsx",
14
+ "esbuild": "esbuild",
15
+ "check": "tsc --noEmit",
16
+ "dev": "tsx --env-file=.env examples/main.ts",
17
+ "dev:ts": "node -r ts-node/register --env-file=.env examples/main.ts",
13
18
  "build": "tsc",
14
- "dev": "node -r ts-node/register --env-file=.env examples/main.ts",
19
+ "build:es": "esbuild src/index.ts --bundle --platform=node --minify --outfile=dist/index.ts",
20
+ "build:ex": "esbuild examples/main.ts --bundle --platform=node --minify --outfile=dist/main.js",
21
+ "start:ex": "node --env-file=.env dist/main.js",
15
22
  "lint": "eslint \"src/**/*.ts\" --report-unused-disable-directives --max-warnings 0",
16
23
  "lint:fix": "eslint \"src/**/*.ts\" --fix",
17
24
  "format": "prettier --write \"src/**/*.ts\" \"examples/**/*.ts\""
@@ -38,11 +45,13 @@
38
45
  "devDependencies": {
39
46
  "@types/node": "^25.0.3",
40
47
  "@types/ws": "^8.18.1",
48
+ "esbuild": "^0.27.2",
41
49
  "eslint": "^9.39.2",
42
50
  "eslint-config-prettier": "^10.1.8",
43
51
  "eslint-plugin-prettier": "^5.5.4",
44
52
  "prettier": "^3.7.4",
45
53
  "ts-node": "^10.9.2",
54
+ "tsx": "^4.21.0",
46
55
  "typescript": "^5.9.3",
47
56
  "typescript-eslint": "^8.51.0"
48
57
  }