Skip to content

Commit f078b58

Browse files
authored
Merge pull request #1 from BackendStack21/adding-jump-consistent-hash-algorithm
Add Jump Consistent Hash implementation with tests
2 parents e49095f + d93117d commit f078b58

File tree

9 files changed

+276
-10
lines changed

9 files changed

+276
-10
lines changed

README.md

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
# fast-hashring
22

3-
fast-hashring is a lightweight JavaScript library offering an efficient implementation of consistent hashing using virtual nodes. Designed for high performance, it provides fast and scalable key distribution, ideal for load balancing, caching, and data sharding in distributed systems.
3+
fast-hashring is a lightweight JavaScript library offering efficient implementations of consistent hashing:
4+
5+
- A classic consistent hash ring with virtual nodes (balanced distribution, minimal remapping)
6+
- Jump Consistent Hash ([Lamping & Veach](https://arxiv.org/abs/1406.2294)) mapping keys directly to bucket indexes with O(1) memory
7+
8+
Designed for high performance, it provides fast and scalable key distribution, ideal for load balancing, caching, and data sharding in distributed systems.
49

510
## Installation
611

@@ -21,14 +26,37 @@ yarn add fast-hashring
2126
Here’s a basic example of how to integrate fast-hashring into your project:
2227

2328
```js
24-
import ConsistentHash from 'fast-hashring'
29+
import ConsistentHash from "fast-hashring";
2530

26-
const ch = new ConsistentHash({ virtualNodes: 100 })
27-
ch.addNode('server1')
28-
ch.addNode('server2')
31+
const ch = new ConsistentHash({ virtualNodes: 100 });
32+
ch.addNode("server1");
33+
ch.addNode("server2");
2934

30-
const node = ch.getNode('my-key')
31-
console.log(`Key is assigned to node: ${node}`)
35+
const node = ch.getNode("my-key");
36+
console.log(`Key is assigned to node: ${node}`);
37+
```
38+
39+
### Jump Consistent Hash (no ring, O(1) memory)
40+
41+
Jump Consistent Hash maps a string key to a stable index in [0, N). This is useful when you just need a bucket index (e.g., shard number) without maintaining a ring of nodes.
42+
43+
```js
44+
// Note: runtime import path uses the file path; default export remains ConsistentHash
45+
import JumpConsistentHash from "fast-hashring/jump-hash.js";
46+
47+
const jch = new JumpConsistentHash(16); // 16 buckets
48+
const idx = jch.getIndex("user-123"); // deterministic value in 0..15
49+
```
50+
51+
TypeScript note: types for both classes are exposed via the package types entry.
52+
53+
```ts
54+
// Values
55+
import ConsistentHash from "fast-hashring";
56+
import JumpConsistentHash from "fast-hashring/jump-hash.js";
57+
58+
// Types (optional)
59+
import type { ConsistentHash as ConsistentHashType, JumpConsistentHash as JumpConsistentHashType } from "fast-hashring";
3260
```
3361

3462
## Features
@@ -39,6 +67,9 @@ console.log(`Key is assigned to node: ${node}`)
3967
- **Binary Search for Rapid Lookups:**
4068
Maintains a sorted hash ring of virtual nodes, enabling quick key lookups using an optimized binary search algorithm.
4169

70+
- **Jump Consistent Hash (Lamping & Veach):**
71+
Minimal-memory O(1) approach that maps keys directly to bucket indexes in [0, N), great for sharding. Buckets must be numbered 0..N-1.
72+
4273
- **High Performance & Scalability:**
4374
The design focuses on speed and efficiency, ensuring low latency and high throughput even with large-scale deployments.
4475

demos/consistent-hash.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import ConsistentHash from "../";
2+
3+
const ch = new ConsistentHash({ virtualNodes: 100 });
4+
ch.addNode("server1");
5+
ch.addNode("server2");
6+
7+
const node = ch.getNode("my-key");
8+
console.log(`Key is assigned to node: ${node}`);

demos/jump-consistent-hash.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
const JumpConsistentHash = require("../jump-hash.js").default;
2+
3+
const jch = new JumpConsistentHash(16);
4+
const idx = jch.getIndex("user-123");
5+
6+
console.log(`Index for 'user-123': ${idx}`);

demos/jump-consistent-hash.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import JumpConsistentHash from "../jump-hash.js";
2+
3+
const jch = new JumpConsistentHash(16);
4+
const idx = jch.getIndex("user-123");
5+
6+
console.log(`Index for 'user-123': ${idx}`);

index.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export { default as ConsistentHash } from "./consistent-hash.js";
2+
export { default as JumpConsistentHash } from "./jump-hash.js";
3+
4+
// Expose the default export to match runtime default export (ConsistentHash)
5+
import ConsistentHashDefault from "./consistent-hash.js";
6+
export default ConsistentHashDefault;

jump-hash.d.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/**
2+
* JumpConsistentHash maps string keys to an integer index in [0, N)
3+
* using the Jump Consistent Hash algorithm by Lamping & Veach.
4+
*/
5+
declare class JumpConsistentHash {
6+
/**
7+
* Create a new JumpConsistentHash instance.
8+
*
9+
* @param indexes - Total number of indexes (buckets), must be a positive integer.
10+
*/
11+
constructor(indexes: number);
12+
13+
/**
14+
* Update number of indexes (buckets).
15+
*
16+
* @param indexes - Positive integer number of buckets.
17+
*/
18+
setIndexes(indexes: number): void;
19+
20+
/**
21+
* Get current number of indexes (buckets).
22+
*/
23+
size(): number;
24+
25+
/**
26+
* Compute stable index in [0, indexes) for the given key.
27+
*
28+
* @param key - Non-empty string key.
29+
* @returns Index between 0 (inclusive) and indexes (exclusive).
30+
*/
31+
getIndex(key: string): number;
32+
}
33+
34+
export default JumpConsistentHash;

jump-hash.js

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import crypto from "crypto";
2+
3+
/**
4+
* JumpConsistentHash maps string keys to an integer index in [0, N)
5+
* using the Jump Consistent Hash algorithm by Lamping & Veach.
6+
*
7+
* Usage:
8+
* const jch = new JumpConsistentHash(16)
9+
* const idx = jch.getIndex('some-key') // 0..15
10+
*/
11+
class JumpConsistentHash {
12+
/**
13+
* @param {number} indexes - Total number of indexes (buckets), must be >= 1 integer
14+
*/
15+
constructor(indexes) {
16+
this.setIndexes(indexes);
17+
}
18+
19+
/**
20+
* Update number of indexes (buckets).
21+
* @param {number} indexes
22+
*/
23+
setIndexes(indexes) {
24+
if (!Number.isInteger(indexes) || indexes <= 0) {
25+
throw new Error("Indexes must be a positive integer");
26+
}
27+
this.indexes = indexes;
28+
}
29+
30+
/**
31+
* @returns {number} Current number of indexes (buckets)
32+
*/
33+
size() {
34+
return this.indexes;
35+
}
36+
37+
/**
38+
* Compute stable index in [0, indexes) for the given key.
39+
* @param {string} key
40+
* @returns {number}
41+
*/
42+
getIndex(key) {
43+
if (!key || typeof key !== "string") {
44+
throw new Error("Key must be a non-empty string");
45+
}
46+
const k = this.#hash64(key);
47+
return this.#jumpHash(k, this.indexes);
48+
}
49+
50+
/**
51+
* Hash string to unsigned 64-bit BigInt using SHA-1 (first 8 bytes).
52+
* Endianness choice is arbitrary but consistent (little-endian).
53+
* @param {string} key
54+
* @returns {bigint}
55+
*/
56+
#hash64(key) {
57+
const digest = crypto.createHash("sha1").update(key).digest();
58+
let x = 0n;
59+
const len = Math.min(8, digest.length);
60+
for (let i = 0; i < len; i++) {
61+
x |= BigInt(digest[i]) << BigInt(8 * i);
62+
}
63+
return x;
64+
}
65+
66+
/**
67+
* Jump Consistent Hash (Lamping & Veach) implemented with 64-bit arithmetic.
68+
* @param {bigint} key - 64-bit unsigned key
69+
* @param {number} buckets - number of buckets (indexes)
70+
* @returns {number}
71+
*/
72+
#jumpHash(key, buckets) {
73+
// Constants per paper
74+
const mul = 2862933555777941757n;
75+
let b = -1;
76+
let j = 0;
77+
while (j < buckets) {
78+
b = j;
79+
key = (key * mul + 1n) & ((1n << 64n) - 1n); // mod 2^64
80+
// (key >> 33) fits in 31 bits => safe to convert to Number
81+
const r = Number(key >> 33n) + 1;
82+
j = Math.floor((b + 1) * (2147483648 / r));
83+
}
84+
return b;
85+
}
86+
}
87+
88+
export default JumpConsistentHash;

jump-hash.test.js

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { describe, test, expect } from "bun:test";
2+
import JumpConsistentHash from "./jump-hash.js";
3+
4+
describe("JumpConsistentHash", () => {
5+
describe("constructor & size()", () => {
6+
test("initializes with provided number of indexes", () => {
7+
const jch = new JumpConsistentHash(16);
8+
expect(jch.size()).toBe(16);
9+
});
10+
11+
test("throws for invalid indexes in constructor", () => {
12+
expect(() => new JumpConsistentHash()).toThrow("Indexes must be a positive integer");
13+
expect(() => new JumpConsistentHash(0)).toThrow("Indexes must be a positive integer");
14+
expect(() => new JumpConsistentHash(-1)).toThrow("Indexes must be a positive integer");
15+
expect(() => new JumpConsistentHash(1.2)).toThrow("Indexes must be a positive integer");
16+
});
17+
18+
test("setIndexes() updates and validates", () => {
19+
const jch = new JumpConsistentHash(4);
20+
expect(jch.size()).toBe(4);
21+
jch.setIndexes(32);
22+
expect(jch.size()).toBe(32);
23+
expect(() => jch.setIndexes(0)).toThrow("Indexes must be a positive integer");
24+
});
25+
});
26+
27+
describe("getIndex()", () => {
28+
test("throws for empty key", () => {
29+
const jch = new JumpConsistentHash(8);
30+
expect(() => jch.getIndex("")).toThrow("Key must be a non-empty string");
31+
});
32+
33+
test("returns index within range", () => {
34+
const jch = new JumpConsistentHash(8);
35+
const idx = jch.getIndex("alpha");
36+
expect(idx).toBeGreaterThanOrEqual(0);
37+
expect(idx).toBeLessThan(8);
38+
});
39+
40+
test("deterministic for same key", () => {
41+
const jch = new JumpConsistentHash(8);
42+
const k = "user-12345";
43+
const a = jch.getIndex(k);
44+
const b = jch.getIndex(k);
45+
expect(a).toBe(b);
46+
});
47+
48+
test("one bucket always maps to 0", () => {
49+
const jch = new JumpConsistentHash(1);
50+
const keys = ["a", "b", "c", "d", "e"];
51+
for (const k of keys) {
52+
expect(jch.getIndex(k)).toBe(0);
53+
}
54+
});
55+
56+
test("changes reflect with updated bucket count", () => {
57+
const jch = new JumpConsistentHash(4);
58+
const k = "remap-key";
59+
const before = jch.getIndex(k);
60+
expect(before).toBeGreaterThanOrEqual(0);
61+
expect(before).toBeLessThan(4);
62+
jch.setIndexes(9);
63+
const after = jch.getIndex(k);
64+
expect(after).toBeGreaterThanOrEqual(0);
65+
expect(after).toBeLessThan(9);
66+
});
67+
68+
test("distributes keys relatively evenly", () => {
69+
const buckets = 8;
70+
const jch = new JumpConsistentHash(buckets);
71+
const totalKeys = 10000;
72+
const counts = new Array(buckets).fill(0);
73+
for (let i = 0; i < totalKeys; i++) {
74+
const idx = jch.getIndex(`key-${i}`);
75+
counts[idx]++;
76+
}
77+
const avg = totalKeys / buckets;
78+
const tolerance = avg * 0.25; // 25% tolerance
79+
for (const c of counts) {
80+
expect(Math.abs(c - avg)).toBeLessThanOrEqual(tolerance);
81+
}
82+
});
83+
});
84+
});

package.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,27 @@
11
{
22
"name": "fast-hashring",
33
"version": "1.0.1",
4-
"description": "A library for consistent hashing using virtual nodes for improved key distribution.",
4+
"description": "A library for consistent hashing.",
55
"main": "consistent-hash.js",
66
"type": "module",
77
"scripts": {
8-
"test": "bun test"
8+
"test": "bun test --coverage"
99
},
1010
"keywords": [
1111
"consistent-hashing",
1212
"hashing",
1313
"virtual-nodes",
1414
"npm-module"
1515
],
16-
"types": "consistent-hash.d.ts",
16+
"types": "index.d.ts",
1717
"author": "Rolando Santamaria Maso <kyberneees@gmail.com>",
1818
"license": "MIT",
1919
"files": [
2020
"consistent-hash.js",
2121
"consistent-hash.d.ts",
22+
"jump-hash.js",
23+
"jump-hash.d.ts",
24+
"index.d.ts",
2225
"README.md",
2326
"LICENSE"
2427
]

0 commit comments

Comments
 (0)