Skip to content

Commit 4ea73bb

Browse files
authored
revert: refactor: native zlib support (discordjs#10314)
Revert "refactor: native zlib support (discordjs#10243)" This reverts commit 20258f9.
1 parent aae2faf commit 4ea73bb

File tree

4 files changed

+61
-158
lines changed

4 files changed

+61
-158
lines changed

packages/ws/README.md

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,7 @@ const manager = new WebSocketManager({
5050
intents: 0, // for no intents
5151
rest,
5252
// uncomment if you have zlib-sync installed and want to use compression
53-
// compression: CompressionMethod.ZlibSync,
54-
55-
// alternatively, we support compression using node's native `node:zlib` module:
56-
// compression: CompressionMethod.ZlibNative,
53+
// compression: CompressionMethod.ZlibStream,
5754
});
5855

5956
manager.on(WebSocketShardEvents.Dispatch, (event) => {

packages/ws/src/utils/constants.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,13 @@ export enum Encoding {
1818
* Valid compression methods
1919
*/
2020
export enum CompressionMethod {
21-
ZlibNative,
22-
ZlibSync,
21+
ZlibStream = 'zlib-stream',
2322
}
2423

2524
export const DefaultDeviceProperty = `@discordjs/ws [VI]{{inject}}[/VI]` as `@discordjs/ws ${string}`;
2625

2726
const getDefaultSessionStore = lazy(() => new Collection<number, SessionInfo | null>());
2827

29-
export const CompressionParameterMap = {
30-
[CompressionMethod.ZlibNative]: 'zlib-stream',
31-
[CompressionMethod.ZlibSync]: 'zlib-stream',
32-
} as const satisfies Record<CompressionMethod, string>;
33-
3428
/**
3529
* Default options used by the manager
3630
*/
@@ -52,7 +46,6 @@ export const DefaultWebSocketManagerOptions = {
5246
version: APIVersion,
5347
encoding: Encoding.JSON,
5448
compression: null,
55-
useIdentifyCompression: false,
5649
retrieveSessionInfo(shardId) {
5750
const store = getDefaultSessionStore();
5851
return store.get(shardId) ?? null;

packages/ws/src/ws/WebSocketManager.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,9 @@ export interface OptionalWebSocketManagerOptions {
9696
*/
9797
buildStrategy(manager: WebSocketManager): IShardingStrategy;
9898
/**
99-
* The transport compression method to use - mutually exclusive with `useIdentifyCompression`
99+
* The compression method to use
100100
*
101-
* @defaultValue `null` (no transport compression)
101+
* @defaultValue `null` (no compression)
102102
*/
103103
compression: CompressionMethod | null;
104104
/**
@@ -176,12 +176,6 @@ export interface OptionalWebSocketManagerOptions {
176176
* Function used to store session information for a given shard
177177
*/
178178
updateSessionInfo(shardId: number, sessionInfo: SessionInfo | null): Awaitable<void>;
179-
/**
180-
* Whether to use the `compress` option when identifying
181-
*
182-
* @defaultValue `false`
183-
*/
184-
useIdentifyCompression: boolean;
185179
/**
186180
* The gateway version to use
187181
*

packages/ws/src/ws/WebSocketShard.ts

Lines changed: 57 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1+
/* eslint-disable id-length */
12
import { Buffer } from 'node:buffer';
23
import { once } from 'node:events';
34
import { clearInterval, clearTimeout, setInterval, setTimeout } from 'node:timers';
45
import { setTimeout as sleep } from 'node:timers/promises';
56
import { URLSearchParams } from 'node:url';
67
import { TextDecoder } from 'node:util';
7-
import type * as nativeZlib from 'node:zlib';
8+
import { inflate } from 'node:zlib';
89
import { Collection } from '@discordjs/collection';
910
import { lazy, shouldUseGlobalFetchAndWebSocket } from '@discordjs/util';
1011
import { AsyncQueue } from '@sapphire/async-queue';
@@ -20,20 +21,13 @@ import {
2021
type GatewaySendPayload,
2122
} from 'discord-api-types/v10';
2223
import { WebSocket, type Data } from 'ws';
23-
import type * as ZlibSync from 'zlib-sync';
24-
import type { IContextFetchingStrategy } from '../strategies/context/IContextFetchingStrategy';
25-
import {
26-
CompressionMethod,
27-
CompressionParameterMap,
28-
ImportantGatewayOpcodes,
29-
getInitialSendRateLimitState,
30-
} from '../utils/constants.js';
24+
import type { Inflate } from 'zlib-sync';
25+
import type { IContextFetchingStrategy } from '../strategies/context/IContextFetchingStrategy.js';
26+
import { ImportantGatewayOpcodes, getInitialSendRateLimitState } from '../utils/constants.js';
3127
import type { SessionInfo } from './WebSocketManager.js';
3228

33-
/* eslint-disable promise/prefer-await-to-then */
29+
// eslint-disable-next-line promise/prefer-await-to-then
3430
const getZlibSync = lazy(async () => import('zlib-sync').then((mod) => mod.default).catch(() => null));
35-
const getNativeZlib = lazy(async () => import('node:zlib').then((mod) => mod).catch(() => null));
36-
/* eslint-enable promise/prefer-await-to-then */
3731

3832
export enum WebSocketShardEvents {
3933
Closed = 'closed',
@@ -92,9 +86,9 @@ const WebSocketConstructor: typeof WebSocket = shouldUseGlobalFetchAndWebSocket(
9286
export class WebSocketShard extends AsyncEventEmitter<WebSocketShardEventsMap> {
9387
private connection: WebSocket | null = null;
9488

95-
private nativeInflate: nativeZlib.Inflate | null = null;
89+
private useIdentifyCompress = false;
9690

97-
private zLibSyncInflate: ZlibSync.Inflate | null = null;
91+
private inflate: Inflate | null = null;
9892

9993
private readonly textDecoder = new TextDecoder();
10094

@@ -126,18 +120,6 @@ export class WebSocketShard extends AsyncEventEmitter<WebSocketShardEventsMap> {
126120

127121
#status: WebSocketShardStatus = WebSocketShardStatus.Idle;
128122

129-
private identifyCompressionEnabled = false;
130-
131-
/**
132-
* @privateRemarks
133-
*
134-
* This is needed because `this.strategy.options.compression` is not an actual reflection of the compression method
135-
* used, but rather the compression method that the user wants to use. This is because the libraries could just be missing.
136-
*/
137-
private get transportCompressionEnabled() {
138-
return this.strategy.options.compression !== null && (this.nativeInflate ?? this.zLibSyncInflate) !== null;
139-
}
140-
141123
public get status(): WebSocketShardStatus {
142124
return this.#status;
143125
}
@@ -179,63 +161,21 @@ export class WebSocketShard extends AsyncEventEmitter<WebSocketShardEventsMap> {
179161
throw new Error("Tried to connect a shard that wasn't idle");
180162
}
181163

182-
const { version, encoding, compression, useIdentifyCompression } = this.strategy.options;
183-
this.identifyCompressionEnabled = useIdentifyCompression;
184-
185-
// eslint-disable-next-line id-length
164+
const { version, encoding, compression } = this.strategy.options;
186165
const params = new URLSearchParams({ v: version, encoding });
187-
if (compression !== null) {
188-
if (useIdentifyCompression) {
189-
console.warn('WebSocketShard: transport compression is enabled, disabling identify compression');
190-
this.identifyCompressionEnabled = false;
191-
}
192-
193-
params.append('compress', CompressionParameterMap[compression]);
194-
195-
switch (compression) {
196-
case CompressionMethod.ZlibNative: {
197-
const zlib = await getNativeZlib();
198-
if (zlib) {
199-
const inflate = zlib.createInflate({
200-
chunkSize: 65_535,
201-
flush: zlib.constants.Z_SYNC_FLUSH,
202-
});
203-
204-
inflate.on('error', (error) => {
205-
this.emit(WebSocketShardEvents.Error, { error });
206-
});
207-
208-
this.nativeInflate = inflate;
209-
} else {
210-
console.warn('WebSocketShard: Compression is set to native but node:zlib is not available.');
211-
params.delete('compress');
212-
}
213-
214-
break;
215-
}
216-
217-
case CompressionMethod.ZlibSync: {
218-
const zlib = await getZlibSync();
219-
if (zlib) {
220-
this.zLibSyncInflate = new zlib.Inflate({
221-
chunkSize: 65_535,
222-
to: 'string',
223-
});
224-
} else {
225-
console.warn('WebSocketShard: Compression is set to zlib-sync, but it is not installed.');
226-
params.delete('compress');
227-
}
228-
229-
break;
230-
}
231-
}
232-
}
233-
234-
if (this.identifyCompressionEnabled) {
235-
const zlib = await getNativeZlib();
236-
if (!zlib) {
237-
console.warn('WebSocketShard: Identify compression is enabled, but node:zlib is not available.');
238-
this.identifyCompressionEnabled = false;
166+
if (compression) {
167+
const zlib = await getZlibSync();
168+
if (zlib) {
169+
params.append('compress', compression);
170+
this.inflate = new zlib.Inflate({
171+
chunkSize: 65_535,
172+
to: 'string',
173+
});
174+
} else if (!this.useIdentifyCompress) {
175+
this.useIdentifyCompress = true;
176+
console.warn(
177+
'WebSocketShard: Compression is enabled but zlib-sync is not installed, falling back to identify compress',
178+
);
239179
}
240180
}
241181

@@ -511,29 +451,28 @@ export class WebSocketShard extends AsyncEventEmitter<WebSocketShardEventsMap> {
511451
`shard id: ${this.id.toString()}`,
512452
`shard count: ${this.strategy.options.shardCount}`,
513453
`intents: ${this.strategy.options.intents}`,
514-
`compression: ${this.transportCompressionEnabled ? CompressionParameterMap[this.strategy.options.compression!] : this.identifyCompressionEnabled ? 'identify' : 'none'}`,
454+
`compression: ${this.inflate ? 'zlib-stream' : this.useIdentifyCompress ? 'identify' : 'none'}`,
515455
]);
516456

517-
const data: GatewayIdentifyData = {
457+
const d: GatewayIdentifyData = {
518458
token: this.strategy.options.token,
519459
properties: this.strategy.options.identifyProperties,
520460
intents: this.strategy.options.intents,
521-
compress: this.identifyCompressionEnabled,
461+
compress: this.useIdentifyCompress,
522462
shard: [this.id, this.strategy.options.shardCount],
523463
};
524464

525465
if (this.strategy.options.largeThreshold) {
526-
data.large_threshold = this.strategy.options.largeThreshold;
466+
d.large_threshold = this.strategy.options.largeThreshold;
527467
}
528468

529469
if (this.strategy.options.initialPresence) {
530-
data.presence = this.strategy.options.initialPresence;
470+
d.presence = this.strategy.options.initialPresence;
531471
}
532472

533473
await this.send({
534474
op: GatewayOpcodes.Identify,
535-
// eslint-disable-next-line id-length
536-
d: data,
475+
d,
537476
});
538477

539478
await this.waitForEvent(WebSocketShardEvents.Ready, this.strategy.options.readyTimeout);
@@ -551,7 +490,6 @@ export class WebSocketShard extends AsyncEventEmitter<WebSocketShardEventsMap> {
551490
this.replayedEvents = 0;
552491
return this.send({
553492
op: GatewayOpcodes.Resume,
554-
// eslint-disable-next-line id-length
555493
d: {
556494
token: this.strategy.options.token,
557495
seq: session.sequence,
@@ -569,22 +507,13 @@ export class WebSocketShard extends AsyncEventEmitter<WebSocketShardEventsMap> {
569507

570508
await this.send({
571509
op: GatewayOpcodes.Heartbeat,
572-
// eslint-disable-next-line id-length
573510
d: session?.sequence ?? null,
574511
});
575512

576513
this.lastHeartbeatAt = Date.now();
577514
this.isAck = false;
578515
}
579516

580-
private parseInflateResult(result: any): GatewayReceivePayload | null {
581-
if (!result) {
582-
return null;
583-
}
584-
585-
return JSON.parse(typeof result === 'string' ? result : this.textDecoder.decode(result)) as GatewayReceivePayload;
586-
}
587-
588517
private async unpackMessage(data: Data, isBinary: boolean): Promise<GatewayReceivePayload | null> {
589518
// Deal with no compression
590519
if (!isBinary) {
@@ -599,12 +528,10 @@ export class WebSocketShard extends AsyncEventEmitter<WebSocketShardEventsMap> {
599528
const decompressable = new Uint8Array(data as ArrayBuffer);
600529

601530
// Deal with identify compress
602-
if (this.identifyCompressionEnabled) {
603-
// eslint-disable-next-line no-async-promise-executor
604-
return new Promise(async (resolve, reject) => {
605-
const zlib = (await getNativeZlib())!;
531+
if (this.useIdentifyCompress) {
532+
return new Promise((resolve, reject) => {
606533
// eslint-disable-next-line promise/prefer-await-to-callbacks
607-
zlib.inflate(decompressable, { chunkSize: 65_535 }, (err, result) => {
534+
inflate(decompressable, { chunkSize: 65_535 }, (err, result) => {
608535
if (err) {
609536
reject(err);
610537
return;
@@ -615,50 +542,42 @@ export class WebSocketShard extends AsyncEventEmitter<WebSocketShardEventsMap> {
615542
});
616543
}
617544

618-
// Deal with transport compression
619-
if (this.transportCompressionEnabled) {
545+
// Deal with gw wide zlib-stream compression
546+
if (this.inflate) {
547+
const l = decompressable.length;
620548
const flush =
621-
decompressable.length >= 4 &&
622-
decompressable.at(-4) === 0x00 &&
623-
decompressable.at(-3) === 0x00 &&
624-
decompressable.at(-2) === 0xff &&
625-
decompressable.at(-1) === 0xff;
549+
l >= 4 &&
550+
decompressable[l - 4] === 0x00 &&
551+
decompressable[l - 3] === 0x00 &&
552+
decompressable[l - 2] === 0xff &&
553+
decompressable[l - 1] === 0xff;
626554

627-
if (this.nativeInflate) {
628-
this.nativeInflate.write(decompressable, 'binary');
555+
const zlib = (await getZlibSync())!;
556+
this.inflate.push(Buffer.from(decompressable), flush ? zlib.Z_SYNC_FLUSH : zlib.Z_NO_FLUSH);
629557

630-
if (!flush) {
631-
return null;
632-
}
633-
634-
const [result] = await once(this.nativeInflate, 'data');
635-
return this.parseInflateResult(result);
636-
} else if (this.zLibSyncInflate) {
637-
const zLibSync = (await getZlibSync())!;
638-
this.zLibSyncInflate.push(Buffer.from(decompressable), flush ? zLibSync.Z_SYNC_FLUSH : zLibSync.Z_NO_FLUSH);
639-
640-
if (this.zLibSyncInflate.err) {
641-
this.emit(WebSocketShardEvents.Error, {
642-
error: new Error(
643-
`${this.zLibSyncInflate.err}${this.zLibSyncInflate.msg ? `: ${this.zLibSyncInflate.msg}` : ''}`,
644-
),
645-
});
646-
}
558+
if (this.inflate.err) {
559+
this.emit(WebSocketShardEvents.Error, {
560+
error: new Error(`${this.inflate.err}${this.inflate.msg ? `: ${this.inflate.msg}` : ''}`),
561+
});
562+
}
647563

648-
if (!flush) {
649-
return null;
650-
}
564+
if (!flush) {
565+
return null;
566+
}
651567

652-
const { result } = this.zLibSyncInflate;
653-
return this.parseInflateResult(result);
568+
const { result } = this.inflate;
569+
if (!result) {
570+
return null;
654571
}
572+
573+
return JSON.parse(typeof result === 'string' ? result : this.textDecoder.decode(result)) as GatewayReceivePayload;
655574
}
656575

657576
this.debug([
658577
'Received a message we were unable to decompress',
659578
`isBinary: ${isBinary.toString()}`,
660-
`identifyCompressionEnabled: ${this.identifyCompressionEnabled.toString()}`,
661-
`inflate: ${this.transportCompressionEnabled ? CompressionMethod[this.strategy.options.compression!] : 'none'}`,
579+
`useIdentifyCompress: ${this.useIdentifyCompress.toString()}`,
580+
`inflate: ${Boolean(this.inflate).toString()}`,
662581
]);
663582

664583
return null;
@@ -919,7 +838,7 @@ export class WebSocketShard extends AsyncEventEmitter<WebSocketShardEventsMap> {
919838
messages.length > 1
920839
? `\n${messages
921840
.slice(1)
922-
.map((message) => ` ${message}`)
841+
.map((m) => ` ${m}`)
923842
.join('\n')}`
924843
: ''
925844
}`;

0 commit comments

Comments
 (0)