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/src/server.ts ADDED
@@ -0,0 +1,489 @@
1
+ import { Adapter, Counter, AdapterResult, Stats } from './types'
2
+ import { Connection, ConnectionOptions } from './connection'
3
+ import { BUFFERS, OPCODE, STATUS } from './constants'
4
+ import { RawIncomingPacket } from './decode'
5
+
6
+ const statsBigInt: readonly string[] = [
7
+ 'auth_cmds', 'auth_errors', 'bytes', 'bytes_read', 'bytes_written', 'cas_badval', 'cas_hits', 'cas_misses',
8
+ 'cmd_flush', 'cmd_get', 'cmd_set', 'cmd_touch', 'conn_yields', 'crawler_items_checked', 'crawler_reclaimed',
9
+ 'curr_items', 'decr_hits', 'decr_misses', 'delete_hits', 'delete_misses', 'direct_reclaims', 'evicted_active',
10
+ 'evicted_unfetched', 'evictions', 'expired_unfetched', 'get_expired', 'get_flushed', 'get_hits', 'get_misses',
11
+ 'hash_bytes', 'idle_kicks', 'incr_hits', 'incr_misses', 'limit_maxbytes', 'listen_disabled_num', 'log_watcher_sent',
12
+ 'log_watcher_skipped', 'log_watchers', 'log_worker_dropped', 'log_worker_written', 'lru_crawler_running',
13
+ 'lru_crawler_starts', 'lru_maintainer_juggles', 'lrutail_reflocked', 'malloc_fails', 'moves_to_cold',
14
+ 'moves_to_warm', 'moves_within_lru', 'read_buf_bytes', 'read_buf_bytes_free', 'read_buf_count', 'read_buf_oom',
15
+ 'reclaimed', 'rejected_connections', 'response_obj_bytes', 'response_obj_count', 'response_obj_oom',
16
+ 'round_robin_fallback', 'slab_reassign_busy_deletes', 'slab_reassign_busy_items', 'slab_reassign_chunk_rescues',
17
+ 'slab_reassign_evictions_nomem', 'slab_reassign_inline_reclaim', 'slab_reassign_rescues', 'slabs_moved',
18
+ 'store_no_memory', 'store_too_large', 'time_in_listen_disabled_us', 'total_items', 'touch_hits', 'touch_misses',
19
+ 'unexpected_napi_ids',
20
+ ]
21
+
22
+ const statsNumber: readonly string[] = [
23
+ 'connection_structures', 'curr_connections', 'hash_power_level', 'max_connections', 'pid', 'pointer_size',
24
+ 'reserved_fds', 'slab_global_page_pool', 'threads', 'time', 'total_connections', 'uptime',
25
+ ] as const
26
+
27
+ const statsBoolean: readonly string[] = [ 'accepting_conns', 'hash_is_expanding', 'slab_reassign_running' ] as const
28
+
29
+ const statsMicroseconds: readonly string[] = [ 'rusage_system', 'rusage_user' ] as const
30
+
31
+ function injectStats(key: string, value: string, stats: any): Stats {
32
+ if (! key) return stats
33
+
34
+ if (statsBigInt.includes(key)) {
35
+ stats[key] = BigInt(value)
36
+ } else if (statsNumber.includes(key)) {
37
+ stats[key] = Number(value)
38
+ } else if (statsBoolean.includes(key)) {
39
+ stats[key] = !! Number(value)
40
+ } else if (statsMicroseconds.includes(key)) {
41
+ const splits = value.split('.')
42
+ stats[key] = BigInt(`${splits[0]}${splits[1].padEnd(6, '0')}`)
43
+ } else {
44
+ stats[key] = value
45
+ }
46
+ return stats
47
+ }
48
+
49
+
50
+ function fail(packet: RawIncomingPacket, key?: string): never {
51
+ const message = packet.value.toString('utf-8') || 'Unknown Error'
52
+ const status = STATUS[packet.status] || `0x${packet.status.toString(16).padStart(4, '0')}`
53
+ throw new Error(`${message} (status=${status}${key ? `, key=${key}` : ''})`)
54
+ }
55
+
56
+ export interface ServerOptions extends ConnectionOptions {
57
+ ttl?: number
58
+ }
59
+
60
+ export class ServerAdapter implements Adapter {
61
+ #buffer = Buffer.alloc(BUFFERS.KEY_TOO_BIG + 20) // 20 is the max extras we'll write
62
+
63
+ #connection!: Connection
64
+ #ttl!: number
65
+
66
+ readonly #id!: string
67
+
68
+ constructor(options: ServerOptions) {
69
+ this.#connection = new Connection(options)
70
+ this.#ttl = options.ttl || 0 // never
71
+ this.#id = `${this.#connection.host}:${this.#connection.port}`
72
+ }
73
+
74
+ /* ======================================================================== */
75
+
76
+ get connected(): boolean {
77
+ return this.#connection.connected
78
+ }
79
+
80
+ get host(): string {
81
+ return this.#connection.host
82
+ }
83
+
84
+ get port(): number {
85
+ return this.#connection.port
86
+ }
87
+
88
+ get timeout(): number {
89
+ return this.#connection.timeout
90
+ }
91
+
92
+ get ttl(): number {
93
+ return this.#ttl
94
+ }
95
+
96
+ get id(): string {
97
+ return this.#id
98
+ }
99
+
100
+ /* ======================================================================== */
101
+
102
+ #writeKey(key: string, offset: number = 0): number {
103
+ const keyLength = this.#buffer.write(key, offset, BUFFERS.KEY_TOO_BIG, 'utf-8')
104
+ if (keyLength > BUFFERS.KEY_SIZE) throw new Error(`Key too long (len=${keyLength})`)
105
+ return keyLength
106
+ }
107
+
108
+ /* ======================================================================== */
109
+
110
+ async get(
111
+ key: string,
112
+ options: { ttl?: number } = {},
113
+ ): Promise<AdapterResult | void> {
114
+ const { ttl } = options
115
+
116
+ let keyOffset = 0
117
+ if (ttl) keyOffset = this.#buffer.writeUInt32BE(ttl)
118
+ const keyLength = this.#writeKey(key, keyOffset)
119
+
120
+ const [ response ] = await this.#connection.send({
121
+ opcode: ttl ? OPCODE.GAT : OPCODE.GET,
122
+ extras: this.#buffer,
123
+ extrasOffset: 0,
124
+ extrasLength: keyOffset,
125
+ key: this.#buffer,
126
+ keyOffset,
127
+ keyLength,
128
+ })
129
+
130
+ switch (response.status) {
131
+ case STATUS.OK:
132
+ return {
133
+ value: response.value,
134
+ flags: response.extras.readUInt32BE(),
135
+ cas: response.cas,
136
+ recycle: () => response.recycle(),
137
+ }
138
+ case STATUS.KEY_NOT_FOUND:
139
+ return
140
+ default:
141
+ fail(response, key)
142
+ }
143
+ }
144
+
145
+ async touch(
146
+ key: string,
147
+ options: { ttl?: number } = {},
148
+ ): Promise<boolean> {
149
+ const { ttl = this.#ttl } = options
150
+
151
+ const keyOffset = this.#buffer.writeUInt32BE(ttl)
152
+ const keyLength = this.#writeKey(key, keyOffset)
153
+
154
+ const [ response ] = await this.#connection.send({
155
+ opcode: OPCODE.TOUCH,
156
+ extras: this.#buffer,
157
+ extrasOffset: 0,
158
+ extrasLength: keyOffset,
159
+ key: this.#buffer,
160
+ keyOffset,
161
+ keyLength,
162
+ })
163
+
164
+ try {
165
+ switch (response.status) {
166
+ case STATUS.OK:
167
+ return true
168
+ case STATUS.KEY_NOT_FOUND:
169
+ return false
170
+ default:
171
+ fail(response, key)
172
+ }
173
+ } finally {
174
+ response.recycle()
175
+ }
176
+ }
177
+
178
+ /* ======================================================================== */
179
+
180
+ async #sar(
181
+ opcode: OPCODE.SET | OPCODE.ADD | OPCODE.REPLACE,
182
+ key: string,
183
+ value: Buffer,
184
+ options: { flags?: number, cas?: bigint, ttl?: number },
185
+ ): Promise<bigint | void> {
186
+ const { flags = 0, cas = 0n, ttl = this.#ttl } = options
187
+
188
+ let keyOffset: number
189
+ keyOffset = this.#buffer.writeUInt32BE(flags)
190
+ keyOffset = this.#buffer.writeUInt32BE(ttl, keyOffset)
191
+ const keyLength = this.#writeKey(key, keyOffset)
192
+
193
+ const [ response ] = await this.#connection.send({
194
+ opcode: opcode,
195
+ cas,
196
+ extras: this.#buffer,
197
+ extrasOffset: 0,
198
+ extrasLength: keyOffset,
199
+ key: this.#buffer,
200
+ keyOffset,
201
+ keyLength,
202
+ value,
203
+ })
204
+
205
+ try {
206
+ switch (response.status) {
207
+ case STATUS.OK:
208
+ return response.cas
209
+ case STATUS.KEY_NOT_FOUND:
210
+ case STATUS.KEY_EXISTS:
211
+ return
212
+ default:
213
+ fail(response, key)
214
+ }
215
+ } finally {
216
+ response.recycle()
217
+ }
218
+ }
219
+
220
+ set(
221
+ key: string,
222
+ value: Buffer,
223
+ options: { flags?: number, cas?: bigint, ttl?: number } = {},
224
+ ): Promise<bigint | void> {
225
+ return this.#sar(OPCODE.SET, key, value, options)
226
+ }
227
+
228
+ add(
229
+ key: string,
230
+ value: Buffer,
231
+ options: { flags?: number, cas?: bigint, ttl?: number } = {},
232
+ ): Promise<bigint | void> {
233
+ return this.#sar(OPCODE.ADD, key, value, options)
234
+ }
235
+
236
+ replace(
237
+ key: string,
238
+ value: Buffer,
239
+ options: { flags?: number, cas?: bigint, ttl?: number } = {},
240
+ ): Promise<bigint | void> {
241
+ return this.#sar(OPCODE.REPLACE, key, value, options)
242
+ }
243
+
244
+ /* ======================================================================== */
245
+
246
+ async #pend(
247
+ opcode: OPCODE.APPEND | OPCODE.PREPEND,
248
+ key: string,
249
+ value: Buffer,
250
+ options: { cas?: bigint },
251
+ ): Promise<boolean> {
252
+ const { cas = 0n } = options
253
+
254
+ const keyLength = this.#writeKey(key)
255
+
256
+ const [ response ] = await this.#connection.send({
257
+ opcode: opcode,
258
+ cas,
259
+ key: Buffer.from(key, 'utf-8'),
260
+ keyOffset: 0,
261
+ keyLength,
262
+ value,
263
+ })
264
+
265
+ try {
266
+ switch (response.status) {
267
+ case STATUS.OK:
268
+ return true
269
+ case STATUS.ITEM_NOT_STORED:
270
+ case STATUS.KEY_EXISTS:
271
+ return false
272
+ default:
273
+ fail(response, key)
274
+ }
275
+ } finally {
276
+ response.recycle()
277
+ }
278
+ }
279
+
280
+ append(
281
+ key: string,
282
+ value: Buffer,
283
+ options: { cas?: bigint } = {},
284
+ ): Promise<boolean> {
285
+ return this.#pend(OPCODE.APPEND, key, value, options)
286
+ }
287
+
288
+ prepend(
289
+ key: string,
290
+ value: Buffer,
291
+ options: { cas?: bigint } = {},
292
+ ): Promise<boolean> {
293
+ return this.#pend(OPCODE.PREPEND, key, value, options)
294
+ }
295
+
296
+ /* ======================================================================== */
297
+
298
+ async #counter(
299
+ opcode: OPCODE.INCREMENT | OPCODE.DECREMENT,
300
+ key: string,
301
+ delta: bigint | number,
302
+ options: { initial?: bigint | number, cas?: bigint, ttl?: number },
303
+ ): Promise<Counter | void> {
304
+ const {
305
+ initial,
306
+ cas = 0n,
307
+ ttl = this.#ttl,
308
+ } = options
309
+
310
+ let keyOffset: number
311
+ keyOffset = this.#buffer.writeBigUInt64BE(BigInt(delta))
312
+ keyOffset = this.#buffer.writeBigUInt64BE(BigInt(initial || 0n), keyOffset)
313
+ keyOffset = this.#buffer.writeUInt32BE(initial == undefined ? 0xffffffff : ttl, keyOffset)
314
+ const keyLength = this.#writeKey(key, keyOffset)
315
+
316
+ const [ response ] = await this.#connection.send({
317
+ opcode: opcode,
318
+ extras: this.#buffer,
319
+ extrasOffset: 0,
320
+ extrasLength: keyOffset,
321
+ key: this.#buffer,
322
+ keyOffset,
323
+ keyLength,
324
+ cas,
325
+ })
326
+
327
+ try {
328
+ switch (response.status) {
329
+ case STATUS.OK:
330
+ return {
331
+ value: response.value.readBigUInt64BE(0),
332
+ cas: response.cas,
333
+ }
334
+ case STATUS.KEY_NOT_FOUND:
335
+ case STATUS.KEY_EXISTS:
336
+ return
337
+ default:
338
+ fail(response, key)
339
+ }
340
+ } finally {
341
+ response.recycle()
342
+ }
343
+ }
344
+
345
+ increment(
346
+ key: string,
347
+ delta: bigint | number = 1,
348
+ options: { initial?: bigint | number, cas?: bigint, ttl?: number, create?: boolean } = {},
349
+ ): Promise<Counter | void> {
350
+ return this.#counter(OPCODE.INCREMENT, key, delta, options)
351
+ }
352
+
353
+ decrement(
354
+ key: string,
355
+ delta: bigint | number = 1,
356
+ options: { initial?: bigint | number, cas?: bigint, ttl?: number, create?: boolean } = {},
357
+ ): Promise<Counter | void> {
358
+ return this.#counter(OPCODE.DECREMENT, key, delta, options)
359
+ }
360
+
361
+ /* ======================================================================== */
362
+
363
+ async delete(
364
+ key: string,
365
+ options: { cas?: bigint } = {},
366
+ ): Promise<boolean> {
367
+ const { cas = 0n } = options
368
+
369
+ const keyLength = this.#writeKey(key)
370
+
371
+ const [ response ] = await this.#connection.send({
372
+ opcode: OPCODE.DELETE,
373
+ key: this.#buffer,
374
+ keyOffset: 0,
375
+ keyLength,
376
+ cas,
377
+ })
378
+
379
+ try {
380
+ switch (response.status) {
381
+ case STATUS.OK:
382
+ return true
383
+ case STATUS.KEY_NOT_FOUND:
384
+ case STATUS.KEY_EXISTS:
385
+ return false
386
+ default:
387
+ fail(response, key)
388
+ }
389
+ } finally {
390
+ response.recycle()
391
+ }
392
+ }
393
+
394
+ async flush(ttl: number = 0): Promise<void> {
395
+ const extrasLength = ttl ? this.#buffer.writeUInt32BE(ttl) : 0
396
+
397
+ const [ response ] = await this.#connection.send({
398
+ opcode: OPCODE.FLUSH,
399
+ extras: this.#buffer,
400
+ extrasOffset: 0,
401
+ extrasLength,
402
+ })
403
+
404
+ try {
405
+ switch (response.status) {
406
+ case STATUS.OK:
407
+ return
408
+ default:
409
+ fail(response)
410
+ }
411
+ } finally {
412
+ response.recycle()
413
+ }
414
+ }
415
+
416
+ /* ======================================================================== */
417
+
418
+ async noop(): Promise<void> {
419
+ const [ response ] = await this.#connection.send({
420
+ opcode: OPCODE.NOOP,
421
+ })
422
+
423
+ try {
424
+ switch (response.status) {
425
+ case STATUS.OK:
426
+ return
427
+ default:
428
+ fail(response)
429
+ }
430
+ } finally {
431
+ response.recycle()
432
+ }
433
+ }
434
+
435
+ async quit(): Promise<void> {
436
+ const [ response ] = await this.#connection.send({
437
+ opcode: OPCODE.QUIT,
438
+ })
439
+
440
+ try {
441
+ switch (response.status) {
442
+ case STATUS.OK:
443
+ return
444
+ default:
445
+ fail(response)
446
+ }
447
+ } finally {
448
+ response.recycle()
449
+ }
450
+ }
451
+
452
+ async version(): Promise<Record<string, string>> {
453
+ const [ response ] = await this.#connection.send({
454
+ opcode: OPCODE.VERSION,
455
+ })
456
+
457
+ try {
458
+ switch (response.status) {
459
+ case STATUS.OK:
460
+ return { [this.#id]: response.value.toString('utf-8') }
461
+ default:
462
+ fail(response)
463
+ }
464
+ } finally {
465
+ response.recycle()
466
+ }
467
+ }
468
+
469
+ async stats(): Promise<Record<string, Stats>> {
470
+ const responses = await this.#connection.send({
471
+ opcode: OPCODE.STAT,
472
+ })
473
+
474
+ const stats = responses.reduce((result, packet) => {
475
+ try {
476
+ if (packet.status !== STATUS.OK) fail(packet)
477
+
478
+ const key = packet.key.toString('utf-8')
479
+ const value = packet.value.toString('utf-8')
480
+
481
+ return injectStats(key, value, result)
482
+ } finally {
483
+ packet.recycle()
484
+ }
485
+ }, {})
486
+
487
+ return { [this.#id]: stats as Stats }
488
+ }
489
+ }