API reference
Everything the package exports:
import {
PackHash,
computeBuckets,
resolveAdapter,
fnv1a,
UnsupportedClientError,
} from "redis-packhash";
import type {
PackHashOptions,
ComputeBucketsOptions,
PackHashAdapter,
} from "redis-packhash";new PackHash(client, options?)
Creates a store.
const store = new PackHash(client, { namespace: "users", expectedKeys: 1_000_000 });client is an ioredis or node-redis instance, or any object exposing the three hash commands. Casing is detected: if the object has a callable hSet, the camelCase trio hSet/hGet/hDel is used (node-redis v4+), otherwise the lowercase trio hset/hget/hdel (ioredis and most others). Whichever trio is picked is then validated in full, so a partially implemented client fails now with UnsupportedClientError instead of crashing on a later command.
options is optional and defaults to {}. All three fields are validated at construction; see Configuration for how to choose them.
Constructor options
| Option | Type | Default | Notes |
|---|---|---|---|
expectedKeys | number | none (1024 buckets) | Finite and non-negative. Sizes the bucket count. |
namespace | string | "ph" | Non-empty. Prefix for bucket keys, <namespace>:<n>. |
maxListpackEntries | number | 512 | Positive integer. Mirrors hash-max-listpack-entries. |
Any value that fails its check throws a TypeError naming the option and the value it received.
Instance properties
All three are readonly and resolved at construction, which makes them the authoritative record of what the store actually decided.
| Property | Type | Description |
|---|---|---|
namespace | string | The resolved namespace. |
buckets | number | The resolved bucket count. This is the hash modulus. |
maxListpackEntries | number | The entry limit used for sizing. |
new PackHash(client, { expectedKeys: 1_000_000 }).buckets; //=> 2605
new PackHash(client).buckets; //=> 1024Instance methods
Values are strings throughout. Serialize with JSON.stringify and parse on the way back out; nothing is converted for you.
set(key, value)
set(key: string, value: string): Promise<void>Stores value under key, as a field inside the key's bucket. Resolves once the underlying HSET resolves.
Rejects with a TypeError if value is not a string. set is async, so the error arrives as a rejected promise, not a synchronous throw: a try/catch around an un-awaited set will not see it, and you get an unhandled rejection instead. await the call or attach a .catch. The message names the key, so a bad value inside a large mset is identifiable.
Nothing here enforces hash-max-listpack-value. key becomes the hash field name and is charged against that 64-byte limit alongside value; exceeding it on either silently promotes the whole bucket out of listpack encoding. See the other listpack limit.
get(key)
get(key: string): Promise<string | null>Returns the stored string, or null if the key is absent. Clients that signal absence with undefined are normalized to null.
del(key)
del(key: string): Promise<boolean>Deletes a key. Returns true if a value was removed, false if the key did not exist.
has(key)
has(key: string): Promise<boolean>Returns true if the key is present. Implemented as a get with a null check, so it costs one HGET and transfers the value. When you are going to read the value anyway, call get once and check for null rather than calling both.
mset(entries)
mset(entries: Iterable<readonly [string, string]>): Promise<void>Stores many [key, value] pairs by calling set on each in turn. Sequential: every HSET is awaited before the next is issued.
entries is iterated with for...of, so a generator is consumed one pair at a time and a large batch never materializes as an array. It must be a synchronous Iterable; an async generator or a database cursor cannot be passed directly.
await store.mset([
[
"f47ac10b-58cc-4372-a567-0e02b2c3d479",
JSON.stringify({ name: "Ada Lovelace", plan: "pro" }),
],
[
"9c858901-8a57-4791-81fe-4c455b099bc9",
JSON.stringify({ name: "Grace Hopper", plan: "free" }),
],
]);The first rejection propagates immediately and the remaining entries are never attempted. Writes that already succeeded stay written, so a failed batch leaves a partial dataset. Rerunning it is safe, because HSET is idempotent.
This is a convenience wrapper, not a throughput feature. One round trip per entry is the cost; see Bulk throughput.
mget(keys)
mget(keys: Iterable<string>): Promise<Map<string, string | null>>Reads many keys by calling get on each in turn, also sequentially. Returns a Map from each key to its string, or null where absent, in the order the keys were given.
const found = await store.mget([
"f47ac10b-58cc-4372-a567-0e02b2c3d479",
"6ba7b810-9dad-11d1-80b4-00c04fd430c8", // never cached
]);
found.get("6ba7b810-9dad-11d1-80b4-00c04fd430c8"); //=> nullA duplicate key in keys collapses to a single Map entry, so the result can be smaller than the input.
Not atomic
Neither bulk helper issues MULTI or a native pipeline(). They send individual commands. That is deliberate: it keeps the library client-agnostic and keeps it working on Redis Cluster, where buckets land in different hash slots and a single pipeline could not span them.
bucketKeyFor(key)
bucketKeyFor(key: string): stringReturns the Redis hash key a logical key maps to, `${namespace}:${fnv1a(key) % buckets}`. Pure, synchronous, and issues no command. Useful for debugging and for inspecting a bucket directly with redis-cli.
computeBuckets(options)
computeBuckets(options: ComputeBucketsOptions): numberReturns the bucket count for a dataset size without constructing a store. Both fields are required here, unlike the constructor.
computeBuckets({ expectedKeys: 1_000_000, maxListpackEntries: 512 });
//=> 2605The formula is ceil(expectedKeys / floor(maxListpackEntries × 0.75)). Both the divisor and the result are clamped to a minimum of 1, so maxListpackEntries: 1 yields one key per bucket rather than dividing by zero. The 0.75 load factor is internal and not configurable. Inputs are assumed valid: this is a pure calculation, and the constructor is where validation happens.
Use it to check whether a config change would remap your keys, as shown in Configuration.
Lower-level exports
You do not need these for normal use. They are exported so you can build on top of the same primitives the store uses.
resolveAdapter(client)
resolveAdapter(client: unknown): PackHashAdapterNormalizes a raw client into a PackHashAdapter, applying the same casing detection and validation the constructor does. Throws UnsupportedClientError when methods are missing.
Useful for validating a client at startup, before you are ready to build a store.
fnv1a(input)
fnv1a(input: string): numberFNV-1a, 32-bit. Returns an unsigned 32-bit integer. This is the hash that maps a key onto a bucket, exported so you can reproduce bucket assignment outside the library, for example in a migration script or a non-JavaScript consumer of the same dataset.
fnv1a("f47ac10b-58cc-4372-a567-0e02b2c3d479"); //=> 1533351232
1533351232 % 2605; //=> 1342It is not cryptographic and is not meant to be.
Errors
UnsupportedClientError
Thrown by the constructor and by resolveAdapter when the client cannot satisfy the required command surface.
class UnsupportedClientError extends Error {
readonly name: "UnsupportedClientError";
readonly missing: readonly string[];
}missing lists the method names that were absent, in the casing that was expected of that client:
try {
new PackHash({ hset: async () => {} });
} catch (error) {
if (error instanceof UnsupportedClientError) {
console.error(error.missing); //=> ["hget", "hdel"]
}
}TypeError
Thrown, not exported, in two situations. They surface differently:
- Invalid options at construction. The constructor is synchronous, so this is a real throw. See Constructor options.
- A non-string value passed to
set, directly or throughmset. Both areasync, so this arrives as a promise rejection.
Types
interface PackHashOptions {
expectedKeys?: number;
namespace?: string;
maxListpackEntries?: number;
}
interface ComputeBucketsOptions {
expectedKeys: number;
maxListpackEntries: number;
}
interface PackHashAdapter {
hset(key: string, field: string, value: string): Promise<unknown>;
hget(key: string, field: string): Promise<string | null>;
hdel(key: string, field: string): Promise<unknown>;
}PackHashAdapter is also the shape to implement directly when wrapping a client that does not expose either casing. Pass your implementation as the client argument.