npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@resolute/memcache

v0.0.7

Published

Memcache client library based on binary protocol.

Downloads

48

Readme

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