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/LICENSE.md +211 -0
- package/NOTICE.md +13 -0
- package/README.md +1 -0
- package/dist/index.js +1147 -0
- package/dist/index.js.map +6 -0
- package/dist/index.mjs +1124 -0
- package/dist/index.mjs.map +6 -0
- package/index.d.ts +623 -0
- package/package.json +48 -0
- package/src/buffers.ts +27 -0
- package/src/client.ts +228 -0
- package/src/cluster.ts +139 -0
- package/src/connection.ts +191 -0
- package/src/constants.ts +103 -0
- package/src/decode.ts +95 -0
- package/src/encode.ts +81 -0
- package/src/index.ts +16 -0
- package/src/internals.ts +41 -0
- package/src/server.ts +489 -0
- package/src/types.ts +263 -0
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
|
+
}
|