memx 0.0.1

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/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "memx",
3
+ "version": "0.0.1",
4
+ "description": "Simple and fast memcached client",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./dist/index.mjs",
10
+ "require": "./dist/index.js"
11
+ }
12
+ },
13
+ "types": "./index.d.ts",
14
+ "scripts": {
15
+ "build": "./build.sh",
16
+ "dev": "nodemon -e ts -x ./build.sh -w src -w test",
17
+ "lint": "eslint src test",
18
+ "prepare": "patch -N -p0 -i .nyc.patch || true"
19
+ },
20
+ "author": "Juit Developers <developers@juit.com>",
21
+ "license": "Apache-2.0",
22
+ "devDependencies": {
23
+ "@microsoft/api-extractor": "^7.19.4",
24
+ "@types/chai": "^4.3.0",
25
+ "@types/chai-as-promised": "^7.1.5",
26
+ "@types/memjs": "^1.2.4",
27
+ "@types/mocha": "^9.1.0",
28
+ "@types/node": "~16.11.26",
29
+ "@types/source-map-support": "^0.5.4",
30
+ "@typescript-eslint/eslint-plugin": "^5.14.0",
31
+ "@typescript-eslint/parser": "^5.14.0",
32
+ "chai": "^4.3.6",
33
+ "chai-as-promised": "^7.1.1",
34
+ "chai-exclude": "^2.1.0",
35
+ "esbuild": "^0.14.25",
36
+ "eslint": "^8.10.0",
37
+ "eslint-config-google": "^0.14.0",
38
+ "memjs": "^1.3.0",
39
+ "mocha": "^9.2.1",
40
+ "nodemon": "^2.0.15",
41
+ "nyc": "^15.1.0",
42
+ "source-map-support": "^0.5.21",
43
+ "typescript": "^4.6.2"
44
+ },
45
+ "directories": {
46
+ "test": "test"
47
+ }
48
+ }
package/src/buffers.ts ADDED
@@ -0,0 +1,27 @@
1
+ import { BUFFERS } from './constants'
2
+
3
+ const pool: Buffer[] = new Array(BUFFERS.POOL_SIZE)
4
+ let offset = -1
5
+
6
+ export interface RecyclableBuffer extends Buffer {
7
+ recycle(): void
8
+ }
9
+
10
+ export function allocateBuffer(size: number): RecyclableBuffer {
11
+ if (size > BUFFERS.BUFFER_SIZE) {
12
+ const buffer = Buffer.allocUnsafeSlow(size) as RecyclableBuffer
13
+ buffer.recycle = () => void 0
14
+ return buffer
15
+ }
16
+
17
+ const buffer = offset >= 0 ? pool[offset--] : Buffer.allocUnsafeSlow(BUFFERS.BUFFER_SIZE)
18
+ const recyclable = buffer.subarray(0, size) as RecyclableBuffer
19
+ let recycled = false
20
+ recyclable.recycle = (): void => queueMicrotask(() => {
21
+ if ((offset >= BUFFERS.POOL_SIZE) || recycled) return
22
+ pool[++offset] = buffer
23
+ recycled = true
24
+ })
25
+
26
+ return recyclable as RecyclableBuffer
27
+ }
package/src/client.ts ADDED
@@ -0,0 +1,228 @@
1
+ import assert from 'assert'
2
+ import { isTypedArray } from 'util/types'
3
+
4
+ import { ClusterAdapter, ClusterOptions } from './cluster'
5
+ import { EMPTY_BUFFER, FLAGS } from './constants'
6
+ import { typedArrayFlags } from './internals'
7
+ import { Adapter, Counter, Stats } from './types'
8
+
9
+ function toBuffer<T>(value: any, options: T): [ Buffer, T & { flags: number } ] {
10
+ if (Buffer.isBuffer(value)) return [ value, { ...options, flags: FLAGS.BUFFER } ]
11
+
12
+ switch (typeof value) {
13
+ case 'bigint':
14
+ return [ Buffer.from(value.toString(), 'utf-8'), { ...options, flags: FLAGS.BIGINT } ]
15
+ case 'boolean':
16
+ return [ Buffer.alloc(1, value ? 0xff : 0x00), { ...options, flags: FLAGS.BOOLEAN } ]
17
+ case 'number':
18
+ return [ Buffer.from(value.toString(), 'utf-8'), { ...options, flags: FLAGS.NUMBER } ]
19
+ case 'string':
20
+ return [ Buffer.from(value, 'utf-8'), { ...options, flags: FLAGS.STRING } ]
21
+ case 'object':
22
+ break // more checks below...
23
+ default:
24
+ assert.fail(`Unable to store value of type "${typeof value}"`)
25
+ }
26
+
27
+ // typed arrays are special
28
+ if (isTypedArray(value)) {
29
+ const flags = typedArrayFlags(value)
30
+ const buffer = Buffer.from(value.buffer, value.byteOffset, value.byteLength)
31
+ return [ buffer, { ...options, flags } ]
32
+ }
33
+
34
+ // null is also special...
35
+ if (value === null) return [ EMPTY_BUFFER, { ...options, flags: FLAGS.NULL } ]
36
+
37
+ // any other "object" gets serialized as JSON
38
+ return [ Buffer.from(JSON.stringify(value), 'utf-8'), { ...options, flags: FLAGS.JSON } ]
39
+ }
40
+
41
+ function makeTypedArray<T extends NodeJS.TypedArray>(
42
+ constructor: new (buffer: ArrayBuffer, offset?: number, length?: number) => T,
43
+ source: Buffer,
44
+ bytesPerValue: number,
45
+ ): T {
46
+ const clone = Buffer.from(source)
47
+ const { buffer, byteOffset, byteLength } = clone
48
+ return new constructor(buffer, byteOffset, byteLength / bytesPerValue)
49
+ }
50
+
51
+ export type Serializable = bigint | string | number | boolean | null | object
52
+ export type Appendable = string | NodeJS.TypedArray
53
+
54
+ export interface ClientResult<T extends Serializable> {
55
+ value: T
56
+ cas: bigint
57
+ }
58
+
59
+ export class Client {
60
+ #adapter!: Adapter
61
+
62
+ constructor()
63
+ constructor(adapter: Adapter)
64
+ constructor(options: ClusterOptions)
65
+
66
+ constructor(adapterOrOptions?: Adapter | ClusterOptions) {
67
+ if (! adapterOrOptions) {
68
+ this.#adapter = new ClusterAdapter()
69
+ } else if ('get' in adapterOrOptions) {
70
+ this.#adapter = adapterOrOptions
71
+ } else if ('hosts' in adapterOrOptions) {
72
+ this.#adapter = new ClusterAdapter(adapterOrOptions)
73
+ }
74
+
75
+ assert(this.#adapter, 'Invalid client constructor arguments')
76
+ }
77
+
78
+ get adapter(): Adapter {
79
+ return this.#adapter
80
+ }
81
+
82
+ async get<T extends Serializable>(key: string, options?: { ttl?: number }): Promise<ClientResult<T> | void> {
83
+ const result = await this.#adapter.get(key, options)
84
+ if (! result) return
85
+
86
+ try {
87
+ const { flags, value, cas } = result
88
+ switch (flags) {
89
+ case FLAGS.BIGINT:
90
+ return { value: BigInt(value.toString('utf-8')) as T, cas }
91
+ case FLAGS.BOOLEAN:
92
+ return { value: !!value[0] as T, cas }
93
+ case FLAGS.NUMBER:
94
+ return { value: Number(value.toString('utf-8' )) as T, cas }
95
+ case FLAGS.STRING:
96
+ return { value: value.toString('utf-8' ) as T, cas }
97
+
98
+ case FLAGS.NULL:
99
+ return { value: null as T, cas }
100
+ case FLAGS.JSON:
101
+ return { value: JSON.parse(value.toString('utf-8' )) as T, cas }
102
+
103
+ case FLAGS.UINT8ARRAY:
104
+ return { value: makeTypedArray(Uint8Array, value, 1) as T, cas }
105
+ case FLAGS.UINT8CLAMPEDARRAY:
106
+ return { value: makeTypedArray(Uint8ClampedArray, value, 1) as T, cas }
107
+ case FLAGS.UINT16ARRAY:
108
+ return { value: makeTypedArray(Uint16Array, value, 2) as T, cas }
109
+ case FLAGS.UINT32ARRAY:
110
+ return { value: makeTypedArray(Uint32Array, value, 4) as T, cas }
111
+ case FLAGS.INT8ARRAY:
112
+ return { value: makeTypedArray(Int8Array, value, 1) as T, cas }
113
+ case FLAGS.INT16ARRAY:
114
+ return { value: makeTypedArray(Int16Array, value, 2) as T, cas }
115
+ case FLAGS.INT32ARRAY:
116
+ return { value: makeTypedArray(Int32Array, value, 4) as T, cas }
117
+ case FLAGS.BIGUINT64ARRAY:
118
+ return { value: makeTypedArray(BigUint64Array, value, 8) as T, cas }
119
+ case FLAGS.BIGINT64ARRAY:
120
+ return { value: makeTypedArray(BigInt64Array, value, 8) as T, cas }
121
+ case FLAGS.FLOAT32ARRAY:
122
+ return { value: makeTypedArray(Float32Array, value, 4) as T, cas }
123
+ case FLAGS.FLOAT64ARRAY:
124
+ return { value: makeTypedArray(Float64Array, value, 8) as T, cas }
125
+
126
+ case FLAGS.BUFFER:
127
+ default:
128
+ return { value: Buffer.from(value) as T, cas }
129
+ }
130
+ } finally {
131
+ result.recycle()
132
+ }
133
+ }
134
+
135
+ async set(key: string, value: Serializable, options?: { cas?: bigint, ttl?: number }): Promise<bigint | void> {
136
+ return this.#adapter.set(key, ...toBuffer(value, options))
137
+ }
138
+
139
+ async add(key: string, value: Serializable, options?: { cas?: bigint, ttl?: number }): Promise<bigint | void> {
140
+ return this.#adapter.add(key, ...toBuffer(value, options))
141
+ }
142
+
143
+ async replace(key: string, value: Serializable, options?: { cas?: bigint, ttl?: number }): Promise<bigint | void> {
144
+ return this.#adapter.replace(key, ...toBuffer(value, options))
145
+ }
146
+
147
+ append(
148
+ key: string,
149
+ value: Appendable,
150
+ options?: { cas?: bigint },
151
+ ): Promise<boolean> {
152
+ return this.#adapter.append(key, ...toBuffer(value, options))
153
+ }
154
+
155
+ prepend(
156
+ key: string,
157
+ value: Appendable,
158
+ options?: { cas?: bigint },
159
+ ): Promise<boolean> {
160
+ return this.#adapter.prepend(key, ...toBuffer(value, options))
161
+ }
162
+
163
+ async increment(
164
+ key: string,
165
+ delta?: bigint | number,
166
+ options?: { initial?: bigint | number, cas?: bigint, ttl?: number },
167
+ ): Promise<Counter | void> {
168
+ const counter = await this.#adapter.increment(key, delta, options)
169
+
170
+ if (counter && options &&
171
+ (options.initial !== undefined) &&
172
+ (counter.value === BigInt(options.initial))) {
173
+ const cas = await this.replace(key, counter.value, { cas: counter.cas, ttl: options.ttl })
174
+ counter.cas = cas ?? counter.cas
175
+ }
176
+ return counter
177
+ }
178
+
179
+ async decrement(
180
+ key: string,
181
+ delta?: bigint | number,
182
+ options?: { initial?: bigint | number, cas?: bigint, ttl?: number },
183
+ ): Promise<Counter | void> {
184
+ const counter = await this.#adapter.decrement(key, delta, options)
185
+
186
+ if (counter && options &&
187
+ (options.initial !== undefined) &&
188
+ (counter.value === BigInt(options.initial))) {
189
+ const cas = await this.replace(key, counter.value, { cas: counter.cas, ttl: options.ttl })
190
+ counter.cas = cas ?? counter.cas
191
+ }
192
+ return counter
193
+ }
194
+
195
+ touch(
196
+ key: string,
197
+ options?: { ttl?: number },
198
+ ): Promise<boolean> {
199
+ return this.#adapter.touch(key, options)
200
+ }
201
+
202
+ delete(
203
+ key: string,
204
+ options?: { cas?: bigint },
205
+ ): Promise<boolean> {
206
+ return this.#adapter.delete(key, options)
207
+ }
208
+
209
+ flush(ttl?: number): Promise<void> {
210
+ return this.#adapter.flush(ttl)
211
+ }
212
+
213
+ noop(): Promise<void> {
214
+ return this.#adapter.noop()
215
+ }
216
+
217
+ quit(): Promise<void> {
218
+ return this.#adapter.quit()
219
+ }
220
+
221
+ version(): Promise<Record<string, string>> {
222
+ return this.#adapter.version()
223
+ }
224
+
225
+ stats(): Promise<Record<string, Stats>> {
226
+ return this.#adapter.stats()
227
+ }
228
+ }
package/src/cluster.ts ADDED
@@ -0,0 +1,139 @@
1
+ import { Adapter, Counter, AdapterResult, Stats } from './types'
2
+ import { ServerAdapter, ServerOptions } from './server'
3
+
4
+ function parseHosts(hosts?: string): ServerOptions[] {
5
+ const result: { host: string, port?: number }[] = []
6
+ if (! hosts) return result
7
+
8
+ for (const part of hosts.split(',')) {
9
+ const [ host, p ] = part.split(':')
10
+ const port = parseInt(p) || undefined
11
+ result.push({ host, port })
12
+ }
13
+
14
+ return result
15
+ }
16
+
17
+
18
+ export interface ClusterOptions {
19
+ hosts: string | string[] | ServerOptions[],
20
+ timeout?: number,
21
+ ttl?: number
22
+ }
23
+
24
+ export class ClusterAdapter implements Adapter {
25
+ readonly servers: readonly ServerAdapter[]
26
+
27
+ constructor()
28
+ constructor(servers: ServerAdapter[])
29
+ constructor(options: ClusterOptions)
30
+
31
+ constructor(serversOrOptions?: ServerAdapter[] | ClusterOptions) {
32
+ // If we have an array of servers, just copy it and use it
33
+ if (Array.isArray(serversOrOptions)) {
34
+ this.servers = [ ...serversOrOptions ]
35
+
36
+ // This was created with "options"... Convert and construct
37
+ } else if (serversOrOptions) {
38
+ const { ttl, timeout, hosts: defs } = serversOrOptions
39
+ const hosts: ServerOptions[] = []
40
+
41
+ if (Array.isArray(defs)) {
42
+ defs.forEach((def) => {
43
+ if (typeof def === 'string') hosts.push(...parseHosts(def))
44
+ else hosts.push({ port: 11211, ...def })
45
+ })
46
+ } else {
47
+ hosts.push(...parseHosts(defs))
48
+ }
49
+
50
+ this.servers = hosts.map((host) => new ServerAdapter({ ttl, timeout, ...host }))
51
+
52
+ // Anything else gets initialized from environment variables
53
+ } else {
54
+ const hosts = parseHosts(process.env.MEMCACHED_HOSTS)
55
+ const ttl = process.env.MEMCACHED_TTL && parseInt(process.env.MEMCACHED_TTL) || undefined
56
+ const timeout = process.env.MEMCACHED_TIMEOUT && parseInt(process.env.MEMCACHED_TIMEOUT) || undefined
57
+
58
+ this.servers = hosts.map((host) => new ServerAdapter({ ttl, timeout, ...host }))
59
+ }
60
+
61
+ // Validate and shortcut in case of single-servers setup
62
+ if (this.servers.length < 1) throw new Error('No hosts configured')
63
+ if (this.servers.length === 1) this.server = (): ServerAdapter => this.servers[0]
64
+
65
+ // Freeze our lists of servers
66
+ Object.freeze(this.servers)
67
+ }
68
+
69
+ server(key: string): ServerAdapter {
70
+ const length = key.length
71
+
72
+ let hash = 0
73
+ for (let i = 0; i < length; i ++) hash = hash * 31 + key.charCodeAt(i)
74
+
75
+ return this.servers[hash % this.servers.length]
76
+ }
77
+
78
+ get(key: string, options?: { ttl?: number | undefined }): Promise<void | AdapterResult> {
79
+ return this.server(key).get(key, options)
80
+ }
81
+
82
+ touch(key: string, options?: { ttl?: number | undefined }): Promise<boolean> {
83
+ return this.server(key).touch(key, options)
84
+ }
85
+
86
+ set(key: string, value: Buffer, options?: { flags?: number | undefined; cas?: bigint | undefined; ttl?: number | undefined }): Promise<bigint | void> {
87
+ return this.server(key).set(key, value, options)
88
+ }
89
+
90
+ add(key: string, value: Buffer, options?: { flags?: number | undefined; cas?: bigint | undefined; ttl?: number | undefined }): Promise<bigint | void> {
91
+ return this.server(key).add(key, value, options)
92
+ }
93
+
94
+ replace(key: string, value: Buffer, options?: { flags?: number | undefined; cas?: bigint | undefined; ttl?: number | undefined }): Promise<bigint | void> {
95
+ return this.server(key).replace(key, value, options)
96
+ }
97
+
98
+ append(key: string, value: Buffer, options?: { cas?: bigint | undefined }): Promise<boolean> {
99
+ return this.server(key).append(key, value, options)
100
+ }
101
+
102
+ prepend(key: string, value: Buffer, options?: { cas?: bigint | undefined }): Promise<boolean> {
103
+ return this.server(key).prepend(key, value, options)
104
+ }
105
+
106
+ increment(key: string, delta?: number | bigint, options?: { initial?: number | bigint | undefined; cas?: bigint | undefined; ttl?: number | undefined; create?: boolean | undefined }): Promise<void | Counter> {
107
+ return this.server(key).increment(key, delta, options)
108
+ }
109
+
110
+ decrement(key: string, delta?: number | bigint, options?: { initial?: number | bigint | undefined; cas?: bigint | undefined; ttl?: number | undefined; create?: boolean | undefined }): Promise<void | Counter> {
111
+ return this.server(key).decrement(key, delta, options)
112
+ }
113
+
114
+ delete(key: string, options?: { cas?: bigint | undefined }): Promise<boolean> {
115
+ return this.server(key).delete(key, options)
116
+ }
117
+
118
+ async flush(ttl?: number): Promise<void> {
119
+ await Promise.all(this.servers.map((server) => server.flush(ttl)))
120
+ }
121
+
122
+ async noop(): Promise<void> {
123
+ await Promise.all(this.servers.map((server) => server.noop()))
124
+ }
125
+
126
+ async quit(): Promise<void> {
127
+ await Promise.all(this.servers.map((server) => server.quit()))
128
+ }
129
+
130
+ async version(): Promise<Record<string, string>> {
131
+ const versions = await Promise.all(this.servers.map((server) => server.version()))
132
+ return versions.reduce((v1, v2) => ({ ...v1, ...v2 }))
133
+ }
134
+
135
+ async stats(): Promise<Record<string, Stats>> {
136
+ const stats = await Promise.all(this.servers.map((server) => server.stats()))
137
+ return stats.reduce((v1, v2) => ({ ...v1, ...v2 }))
138
+ }
139
+ }
@@ -0,0 +1,191 @@
1
+ import assert from 'assert'
2
+ import net, { Socket } from 'net'
3
+
4
+ import { Encoder, RawOutgoingPacket } from './encode'
5
+ import { Decoder, RawIncomingPacket } from './decode'
6
+ import { BUFFERS, OPCODE } from './constants'
7
+ import { socketFinalizationRegistry } from './internals'
8
+
9
+ type RawIncomingPackets = [ RawIncomingPacket, ...RawIncomingPacket[] ]
10
+
11
+ class Deferred {
12
+ #resolve!: (value: RawIncomingPackets) => void
13
+ #reject!: (reason: Error) => void
14
+ #packets: RawIncomingPacket[] = []
15
+
16
+ readonly promise: Promise<RawIncomingPackets>
17
+ readonly opcode: OPCODE
18
+
19
+ constructor(opcode: OPCODE) {
20
+ this.opcode = opcode
21
+
22
+ this.promise = new Promise((resolve, reject) => {
23
+ this.#resolve = resolve
24
+ this.#reject = reject
25
+ })
26
+ }
27
+
28
+ append(packet: RawIncomingPacket): void {
29
+ this.#packets.push(packet)
30
+ }
31
+
32
+ resolve(packet: RawIncomingPacket): void {
33
+ this.#packets.push(packet)
34
+ this.#resolve(this.#packets as RawIncomingPackets)
35
+ }
36
+
37
+ reject(error: Error): void {
38
+ this.#reject(error)
39
+ }
40
+ }
41
+
42
+ export interface ConnectionOptions {
43
+ host: string,
44
+ port?: number,
45
+ timeout?: number,
46
+ }
47
+
48
+ export class Connection {
49
+ readonly #decoder = new Decoder((packet) => this.#receive(packet))
50
+ readonly #encoder = new Encoder()
51
+ readonly #buffer = Buffer.allocUnsafeSlow(BUFFERS.BUFFER_SIZE)
52
+ readonly #defers = new Map<number, Deferred>()
53
+ readonly #factory: typeof net.connect
54
+
55
+ #socket?: Promise<Socket>
56
+ #sequence = 0
57
+
58
+ readonly #timeout: number
59
+ readonly #host: string
60
+ readonly #port: number
61
+
62
+ constructor(options: ConnectionOptions)
63
+ constructor(options: ConnectionOptions & { factory?: typeof net.connect }) {
64
+ assert(options, 'No options specified')
65
+
66
+ const {
67
+ host,
68
+ port = 11211,
69
+ timeout = 1000,
70
+ factory = net.connect,
71
+ } = options
72
+ this.#factory = factory
73
+
74
+ assert(host, 'No host name specified')
75
+ assert(port > 0 && port < 65536 && (Math.floor(port) == port), `Invalid port ${port}`)
76
+
77
+ this.#timeout = timeout
78
+ this.#host = host
79
+ this.#port = port
80
+ }
81
+
82
+ /* ======================================================================== */
83
+
84
+ get connected(): boolean {
85
+ return !! this.#socket
86
+ }
87
+
88
+ get host(): string {
89
+ return this.#host
90
+ }
91
+
92
+ get port(): number {
93
+ return this.#port
94
+ }
95
+
96
+ get timeout(): number {
97
+ return this.#timeout
98
+ }
99
+
100
+ /* ======================================================================== */
101
+
102
+ #connect(): Promise<Socket> {
103
+ return this.#socket || (this.#socket = new Promise((resolve, reject) => {
104
+ const socket: Socket = this.#factory({
105
+ host: this.#host,
106
+ port: this.#port,
107
+ timeout: this.#timeout,
108
+ onread: {
109
+ buffer: this.#buffer,
110
+ callback: (bytes: number, buffer: Buffer): boolean => {
111
+ this.#decoder.append(buffer, 0, bytes)
112
+ return true
113
+ },
114
+ },
115
+ })
116
+
117
+ socket.on('timeout', () => socket.destroy(new Error('Timeout')))
118
+ socket.on('error', reject)
119
+
120
+ socket.on('close', () => {
121
+ socketFinalizationRegistry.unregister(this)
122
+ this.#socket = undefined
123
+ })
124
+
125
+ socket.on('connect', () => {
126
+ socketFinalizationRegistry.register(this, socket, this)
127
+
128
+ socket.off('error', reject)
129
+ socket.on('error', (error) => {
130
+ for (const deferred of this.#defers.values()) {
131
+ process.nextTick(() => deferred.reject(error))
132
+ }
133
+ this.#defers.clear()
134
+ this.#socket = undefined
135
+ })
136
+
137
+ socket.unref()
138
+ resolve(socket)
139
+ })
140
+ }))
141
+ }
142
+
143
+ #receive(packet: RawIncomingPacket): void {
144
+ const deferred = this.#defers.get(packet.sequence)
145
+ if (deferred) {
146
+ if (deferred.opcode === packet.opcode) {
147
+ if ((packet.opcode === OPCODE.STAT) && (packet.key.length !== 0)) {
148
+ return deferred.append(packet)
149
+ }
150
+ return deferred.resolve(packet)
151
+ } else {
152
+ const sent = `0x${deferred.opcode.toString(16).padStart(2, '0')}`
153
+ const received = `0x${packet.opcode.toString(16).padStart(2, '0')}`
154
+ return deferred.reject(new Error(`Opcode mismatch (sent=${sent}, received=${received})`))
155
+ }
156
+ }
157
+ }
158
+
159
+ async send(packet: RawOutgoingPacket): Promise<RawIncomingPackets> {
160
+ const sequence = ++ this.#sequence
161
+ const buffer = this.#encoder.encode(packet, sequence)
162
+ const deferred = new Deferred(packet.opcode)
163
+
164
+ this.#defers.set(sequence, deferred)
165
+
166
+ const socket = await this.#connect()
167
+ socket.write(buffer, (error) => {
168
+ buffer.recycle()
169
+ if (error) return deferred.reject(error)
170
+ })
171
+
172
+ const timeout = setTimeout(() => deferred.reject(new Error('No response')), this.#timeout)
173
+
174
+ return deferred.promise.finally(() => {
175
+ clearTimeout(timeout)
176
+ this.#defers.delete(sequence)
177
+ })
178
+ }
179
+
180
+ async destroy(): Promise<boolean> {
181
+ const socket = await this.#socket
182
+ if (! socket) return false
183
+
184
+ return new Promise<boolean>((resolve, reject) => {
185
+ socket
186
+ .once('error', reject)
187
+ .once('close', resolve)
188
+ .destroy()
189
+ })
190
+ }
191
+ }