@resolute/memcache
TypeScript icon, indicating that this package has built-in type declarations

0.0.7 • Public • Published

Memcache

Memcache client library based on binary protocol.

Build Status codecov Total alerts Language grade: JavaScript Dependencies install size

Key Features

Installation

npm i @resolute/memcache

Client Setup

Every instance of memcache() represents an encapsulated connection to a server through either a specified TCP host:port or a Unix socket path. No options are shared with other instances. By default, the connection to the server is kept alive and always tries to reconnect with incremental backoff when errors occur. Additionally, compression and serialization/deserialization is handled automatically and is designed to handle most popular use cases. This client also provides reliable timeout for all commands. This document covers specific scenarios where you may wish to disable or change the default behavior.

const memcache = require('@resolute/memcache');
const cache = memcache({ /* options */ });

Options

Property Default Description
port 11211 TCP port for the socket.
host '127.0.0.1' Host for the socket.
path undefined Socket path filename. See Identifying paths for IPC connections. If provided, the TCP-specific options above are ignored.
queueSize Infinity Number of requests queued internally (in Node) when Socket.write() is busy.
maxKeySize 250 Max byte size for any key.
maxValueSize 1_048_576 Max byte size for any value.
connectTimeout 2_000 Milliseconds connecting can take before being terminated and retried.
multiResponseOpCodes [0x10] Array of Memcached OpCodes that return multiple responses for a single request. Default: array of only stat’s OpCode (0x10).
retries Infinity Maximum number of reconnection attempts before emitting kill event.
minDelay 100 Milliseconds used as initial incremental backoff for reconnection attempts.
maxDelay 30_000 Maximum milliseconds between reconnection attempts.
backoff (incremental) Backoff function called between retry attempts.
username undefined SASL username.
password undefined SASL password.
ttl 0 Default TTL in seconds, Dates may not be used for default TTL. See Expiration Time.
commandTimeout 4_000 Milliseconds any command may take before rejecting. See Timeouts.
compression (enabled) Compression options or false to disable.
serialization (enabled) Serialization options or false to disable.

Timeouts

This client provides two timeout options, connectTimeout and commandTimeout.

The connectTimeout is simply the number of milliseconds the Socket.connect() method must take to connect to the server. This also applies to any reconnection attempts. The importance of the connectTimeout is much less significant than the commandTimeout for most applications.

The commandTimeout option is the most important timeout to specify. It marks the time from when you issue an command up until the complete response is returned from the server. This ensures that your command will complete or fail within the configured commandTimeout milliseconds.

Many things could happen that could cause delays: network degradation, server flapping, etc. Regardless of the reason, you can be confident that your commands will always succeed or fail within the specified number of milliseconds. You may then decide to poll the data source if this occurs without wasting time for what should be a fast cache lookup.

For example, If you’re using Memcache to store SQL responses, you may wish to configure a low commandTimeout like 200 milliseconds. If the server becomes unavailable for whatever reason, the cache retrieval will fail in 200 milliseconds and your code could continue to query SQL for the response and return it to the end user. In this scenario, you are mitigating the amount of time your app will wait for a degraded Memcache connection.

Compression

Property Default Description
flag 0b1 Compression bitmask.
options { level: 1 } See Zlib Options.
threshold maxValueSize Compress values larger than threshold.
compress zlib.gzip
decompress zlib.gunzip

By default, the client uses Node’s internal zlib.gzip and zlib.gunzip for values that exceed compression.threshold.

To disable compression completely for both storage and retrieval, pass { compression: false } when initializing the Memcache client.

To change the compression format, simply pass any compression utility. For example, to use Brotli compression instead of Gzip:

const { brotliCompress, brotliDecompress } = require('zlib');
const compression = {
  compress: brotliCompress,
  decompress: brotliDecompress,
  flag: 0b100000 // match another brotli-enabled client
}
const cache = memcache({ compression });

Serialization

Property Default Description
stringFlag 0 Bitmask flag to identify value stored as a string.
jsonFlag 0b10 Bitmask flag to identify value stored as JSON.
binaryFlag 0b100 Bitmask flag to identify value stored as binary/blob.
numberFlag 0b1000 Bitmask flag to identify value stored as a number.
serialize JSON.stringify
deserialize JSON.parse

All values are sent to and received from the server as a Buffer over the binary protocol. By default, the client performs useful transformations accordingly:

typeof value flags set Buffer Encoding
undefined 0 Buffer.alloc(0)
string stringFlag Buffer.from(<string>)
number numberFlag Buffer.from(<number>.toString())
Buffer-like binaryFlag as-is
any* jsonFlag Buffer.from(serialize(<value>))

* object (non-Buffer), array, boolean, null, etc. is passed to serialize then converted to Buffer and jsonFlag is set.

Similarly, retrieval commands (get, gat) decode the response value by:

response.flag response.value Buffer Decoding
binaryFlag <Buffer> (as is)
stringFlag <Buffer>.toString()
numberFlag Number(<Buffer>.toString())
jsonFlag if <Buffer>.length === 0 then undefined otherwise deserialize(<Buffer>.toString())

You may substitute the default JSON serializer/deserializer with with other powerful alternatives, like:

yieldable-json:

const { stringifyAsync, parseAsync } = require('yieldable-json');
const serialization = {
  serialize: stringifyAsync,
  deserialize: parseAsync,
};
const { get, set } = memcache({ serialization })

fast-json-stable-stringify:

const fastJsonStableStringify = require('fast-json-stable-stringify');
const serialization = {
  serialize: fastJsonStableStringify,
  // and still use the default JSON.parse for deserialize
};
const { get, set } = memcache({ serialization })

Expiration Time

The set, add, replace, incr, decr, touch, gat, flush commands accept a ttl expiration time.

You may specify either a Date object in the future or the number of seconds (from now) when a key should expire.

0 means never expire.

If you pass a number, this can be up to 30 days (2,592,000 seconds). After 30 days, it is treated as a unix timestamp of an exact date.

Date objects are converted to the number of seconds until that date if it is less than or equal to 30 days otherwise it will be converted to a unix timestamp in seconds, abiding by this protocol. This is the easiest way to set longer expiration times without confusion.

Note: you may not use a Date object when specifying a default ttl option for the memcache client. Only a number of seconds is allowed in this case.

Check And Set

All commands that resolve to a MemcacheResponse include a cas (check and set) property. This is a unique representation of the value after the command has completed (See Binary Protocol Specification for more information). You may pass a cas back to any write operation (set, replace, incr, decr, append, prepend) in order to ensure that the value has not changed since the previous command. In other words, if the value of specified key has changed, your operation will fail. This is useful for resolving race conditions.

const { ERR_KEY_EXISTS } = require('@resolute/memcache/error');
const { set } = memcache();
const { value, cas } = await set('foo', 'abc');
// another process mutates the value
try {
  await append('foo', 'def', { cas });
} catch (error) {
  if (error.status === ERR_KEY_EXISTS) {
    // 'foo' has changed since we last `set`
  }
}

SASL

Some Memcached servers require SASL authentication. Please note that SASL does not provide any encryption or even any real protection to your data. It may only be regarded as a simple way to prevent unintentional access/corruption on trusted networks.

Note: most servers require the username in the “user@host” format.

Commands

get

Get the value for the given key.

Param Type
key string | Buffer

Returns: Promise<MemcacheResponse<T>>

Throws: If key does not exist.

Example

const { ERR_KEY_NOT_FOUND } = require('@resolute/memcache/error');
const { get } = memcache();
try {
  const { value, cas } = await get('foo');
  return {
    // value for “foo”
    value,
    // “check-and-set” buffer that can be
    // passed as option to another command.
    cas
  }
} catch (error) {
  if (error.status === ERR_KEY_NOT_FOUND) {
    // not found → '' (empty string)
    return '';
  } else {
    // re-throw any other error
    throw error;
  }
}

See: gat

set

Set the value for the given key.

Param Type
key string | Buffer
value *
options object | ttl
options.ttl number | Date
options.cas MemcacheResponse | Buffer
options.flags number

Returns: Promise<MemcacheResponse<void>>

Throws: If unable to store value for any reason.

Note: Unlike add, this method will overwrite any existing value associated with given key.

Example

const { set } = memcache();
try {
  // expire in 1 minute
  await set('foo', 'bar', 60);
} catch (error) {
  // any error means that the
  // value was not stored
}

See: add, replace

add

Add a value for the given key.

Param Type
key string | Buffer
value *
options object | ttl
options.ttl number | Date
options.flags number

Returns: Promise<MemcacheResponse<void>>

Throws: If key exists.

Note: Unlike set, this method will fail if a value is already assigned to the given key.

Example

const { ERR_KEY_EXISTS } = require('@resolute/memcache/error');
const { add } = memcache();
try {
  await add('foo', 'bar'); // works
  await add('foo', 'baz'); // fails
} catch (error) {
  // error.status === ERR_KEY_EXISTS
  // 'bar' is still the value
}

See: set, replace

replace

Replace a value for the given key.

Param Type
key string | Buffer
value *
options object | ttl
options.ttl number | Date
options.cas MemcacheResponse | Buffer
options.flags number

Returns: Promise<MemcacheResponse<void>>

Throws: If key does not exist.

Note: Conversely to add, this method will fail the key has expired or does not exist.

Example

const { ERR_KEY_NOT_FOUND } = require('@resolute/memcache/error');
const { replace, set, del } = memcache();
try {
  await set('foo', 'bar');
  await replace('foo', 'baz'); // works
  await del('foo');
  await replace('foo', 'bar'); // fails
} catch (error) {
  // error.status === ERR_KEY_NOT_FOUND
}

See: set, add

del

delete (alias to del)

Delete the given key.

Param Type
key string | Buffer

Returns: Promise<MemcacheResponse<void>>

Throws: If key does not exist.

Note: del throws an error if the key does not exist as well as for many other issues. However, you might consider that a “key not found” error satisfies the deletion of a key. This common pattern is demonstrated in the example.

Example

const { ERR_KEY_NOT_FOUND } = require('@resolute/memcache/error');
const { del } = memcache();
try {
  await del('foo');
} catch (error) {
  if (error.status !== ERR_KEY_NOT_FOUND) {
    throw error; // rethrow any other error
  }
}

incr

increment (alias to incr)

Increment numeric value of given key.

Param Type
key string | Buffer
amount number
options object | ttl
options.ttl number | Date
options.cas MemcacheResponse | Buffer
options.initial number

Returns: Promise<MemcacheResponse<number>>

Throws: If key contains non-numeric value.

Note: If the key is does not exist, the key will be “set” with the initial value (default: 0). However, no flags will be set and a subsequent get will return a string or Buffer instead of a number. Use caution by either type checking the MemcacheResponse.value during get or using await incr(key, 0) to retrieve the number. See Incr/Decr.

Example

const { incr, del } = memcache();

// example of unexpected `typeof response.value`:
await del('foo').catch(() => {}); // ignore any error
await incr('foo', 1, { initial: 1 }); // but no flags set
const { value } = await get('foo');
typeof value === 'string'; // true
value; // '1'

// this time, it would be a numeric response:
await set('foo', 0);
await incr('foo', 1);
const { value } = await get('foo');
typeof value === 'number'; // true
value; // 1

See: decr

decr

decrement (alias to decr)

Decrement numeric value of the given key.

Param Type
key string | Buffer
amount number
options object | ttl
options.ttl number | Date
options.cas MemcacheResponse | Buffer
options.initial number

Returns: Promise<MemcacheResponse<number>>

Throws: If key contains non-numeric value.

Note: Decrementing a counter will never result in a “negative value” (or cause the counter to “wrap”). Instead the counter is set to 0. Incrementing the counter may cause the counter to wrap.

Example

const { decr, del } = memcache();
await del('foo').catch(() => {}); // ignore any error
await decr('foo', 1, { initial: 10 }); // .value === 10
await decr('foo', 1); // .value === 9
await decr('foo', 10); // .value === 0 (not -1)

See: incr

append

Append the specified value to the given key.

Param Type
key string | Buffer
value string | Buffer
cas MemcacheResponse | Buffer

Returns: Promise<MemcacheResponse<void>>

Throws: If key does not exist.

Example

const { append, set, get } = memcache();
await set('foo', 'ab');
await append('foo', 'c');
await get('foo'); // 'abc'

See: prepend

prepend

Prepend the specified value to the given key.

Param Type
key string | Buffer
value string | Buffer
cas MemcacheResponse | Buffer

Returns: Promise<MemcacheResponse<void>>

Throws: If key does not exist.

Example

const { prepend, set, get } = memcache();
await set('foo', 'bc');
await prepend('foo', 'a');
await get('foo'); // 'abc'

See: append

touch

Set a new expiration time for an existing item.

Param Type
key string | Buffer
ttl number | Date

Returns: Promise<MemcacheResponse<void>>

Throws: ERR_KEY_NOT_FOUND if key does not exist.

Example

const { touch } = memcache();
await touch('foo', 3600); // expire in 1 hour

See: gat

gat

Get And Touch is used to set a new expiration time for an existing item and retrieve its value.

Param Type
key string | Buffer
ttl number | Date

Returns: Promise<MemcacheResponse<T>>

Throws: If key does not exist.

Example

const { gat } = memcache();
await gat('foo', 3600); // expire in 1 hour

See: get, touch

flush

Flush the items in the cache now or some time in the future as specified by the optional ttl parameter.

Param Type
ttl number | Date

Returns: Promise<MemcacheResponse<void>>

Note: If ttl is unspecified, then it will default to 0not the configured default ttl.

Example

const { flush } = memcache();
await flush(); // delete all keys immediately

version

Version string in the body with the following format: “x.y.z”

Returns: Promise<string>

Example

const { version } = memcache();
await version(); // '1.5.14'

stat

Statistics. Without a key specified the server will respond with a “default” set of statistics information.

Param Type
key string

Returns: Promise<{Object.<string, string>}>

Note: supported key options: 'slabs', 'settings', 'sizes', but others may work depending on your server.

Example

const { stat } = memcache();
await stat('slabs');

Keepalive

By default, the client will constantly try to reconnect to the server when connection errors occur. When disabled, any network error will cause the client to emit the kill event and no further connection attempts will be made. Any command issued to a client that has emitted the kill event will immediately reject with the error causing the connection kill event.

Backoff

The internal backoff is simply a function of attempt * minDelay until it exceeds maxDelay. The following example shows how to implement exponential backoff if preferred:

const cache = memcache({
  backoff: (attempt) => 100 * (2 ** attempt), // exponential backoff
  maxDelay: 30_000 // never wait longer than 30 seconds
})

Kill Event

By default, client will try to re-establish the connection when encountering connection errors. However, if failures attempts has been reached, SASL auth is required and fails, or you explicitly invoke the kill() method, all reconnection attempts will be terminated and no further attempts will be made. This library will emit a kill event when any of these scenarios occurs.

const cache = memcache();
cache.on('kill', (error) => {
  // This connection has either:
  // 1. reached the maximum number of `failures` attempts, or
  // 2. SASL authentication failed, or
  // 3. `cache.kill()` was invoked.
})

Flags

The set, add, replace commands accept a flags property on the optional options parameter. When using the default serialization and/or the compression functions, flags are set using bitmask against stringFlag, jsonFlag, binaryFlag, numberFlag, and compressionFlag.

If you require special behavior, you may disable these serializes and compression and/or provide your own flags property to these commands. When you retrieve your key through get or gat, you may reference and test the flags of any MemcacheResponse using the .flags getter.

Buffer

All Buffer parameters will also accept Buffer-like types such as: ArrayBuffer, SharedArrayBuffer, DataView.

Incr/Decr

The incr and decr commands are very handy, but can easily have unexpected results. Take the following example:

const { incr, get } = memcache();
await incr('foo', 1, { initial: 1 });
const { value } = await get('foo');
console.log(typeof value); // string, expected number

This is because the incr and decr commands do not accept a flags parameter. If the key does not already exist, then these commands will set the value to the initial value (or 0 if not specified). When this happens, the flags for that key will be set to 0. As a result, it is not guaranteed that you will receive the value as a number on a subsequent get.

Because the incr and decr commands always return the value as a number after performing the command, you may simply issue a incr(key, 0) to read the value as a number as illustrated below. While this approach guarantees that the response value is a number, it will also create the key if it doesn’t exist.

const { incr } = memcache();
await incr('foo', 1, { initial: 1 });
// ... some time later, you want to check the current value:
const { value } = await incr('foo', 0);
console.log(typeof value); // number (always)
console.log(value); // 1 (or possibly 0 if it didn’t exist)

Additionally, if another process changes a value to something other than a number, your incr/decr commands will throw a ERR_INCR_DECR_ON_NON_NUMERIC_VALUE error.

Note: Memcached allows for 64-bit integers, but JavaScript bitwise operations are only supported on 32-bit integers. Node v12 introduces native support for bigint. This package may optionally allow to represent these values as bigint at some point in the future.

MemcacheResponse

MemcacheResponse is a small wrapper for the binary data returned from the server. The rawValue (getter) property will always return the raw Buffer sent by the server. When using compression and/or serialization, the value (getter) property will return the uncompressed deserialized value for the given request. Use the cas (getter) property to reference the check-and-set value. flags may also be referenced if you have special requirements.

const response = await get('foo');

// the final value after all deserialization and decompression:
response.value;

// raw Buffer response from server before any deserialization or decompression:
response.rawValue;

// check-and-set 8-byte Buffer:
response.cas;

// 32-bit integer:
response.flags;

MemcacheError

All commands may reject with an error object represented by the following properties:

MemcacheError {
  message: '…', // descriptive error
  status: number, // one of the codes returned by client or server
  type: 'client|server', // origin of error
  request: MemcacheRequest, // request that caused the error
  response: MemcacheResponse, // server response, if one exists
  error: Error // if another error caused this error (ex. gzip failed)
}
.type .status Code
server ERR_KEY_NOT_FOUND 0x0001
server ERR_KEY_EXISTS 0x0002
server ERR_VALUE_TOO_LARGE 0x0003
server ERR_INVALID_ARGUMENTS 0x0004
server ERR_ITEM_NOT_STORED 0x0005
server ERR_INCR_DECR_ON_NON_NUMERIC_VALUE 0x0006
server ERR_THE_VBUCKET_BELONGS_TO_ANOTHER_SERVER 0x0007
server ERR_AUTHENTICATION_ERROR 0x0008
server ERR_AUTHENTICATION_CONTINUE 0x0009
server ERR_AUTHENTICATION_FAILED 0x0020
server ERR_UNKNOWN_COMMAND 0x0081
server ERR_OUT_OF_MEMORY 0x0082
server ERR_NOT_SUPPORTED 0x0083
server ERR_INTERNAL_ERROR 0x0084
server ERR_BUSY 0x0085
server ERR_TEMPORARY_FAILURE 0x0086
client ERR_UNEXPECTED 0x0100
client ERR_CONNECTION 0x0101
client ERR_INVALID 0x0102
client ERR_COMPRESSION 0x0103
client ERR_SERIALIZATION 0x0104
const { ERR_KEY_NOT_FOUND } = require('@resolute/memcache/error');
const { get } = memcache();
try {
  const { value } = await get('foo');
  return value;
} catch (error) {
  switch (error.status) {
    case ERR_KEY_NOT_FOUND:
      return undefined;
    default
      throw error; // rethrow
  }
 }

Cluster

The focus of this client is to provide an extremely reliable connection to a single server. It intentionally does not provide cluster/hashring support. Cluster/hashring may be considered in a separate package that would use this reliable client for each of the nodes.

AWS

Currently, this client does not support AWS ElastiCache specific config get cluster commands. Pull requests welcomed for this feature. AWS supported Java client reference.

Testing

Testing this client requires local access to a SASL-enabled memcached server. bin/memcached-sasl.sh contains a sample shell script for downloading and compiling the latest version of Memcached with SASL support.

Overloading

TODO

Package Sidebar

Install

npm i @resolute/memcache

Weekly Downloads

8

Version

0.0.7

License

MIT

Unpacked Size

108 kB

Total Files

21

Last publish

Collaborators

  • adamchal