Distributed Cache with Jest Testing
Building a distributed cache is a common requirement in modern applications to improve performance and reduce database load. This challenge asks you to implement a simplified distributed cache using TypeScript and thoroughly test it using Jest. The goal is to create a cache that can store and retrieve data across multiple nodes, simulating a distributed environment.
Problem Description
You are tasked with implementing a DistributedCache class in TypeScript. This class should provide the following functionalities:
constructor(nodes: string[]): Initializes the cache with a list of node addresses (strings). These represent the locations where cache data will be stored.set(key: string, value: any): Stores a key-value pair in the cache. The cache should distribute the data across the nodes using a simple consistent hashing strategy (see Notes).get(key: string): Retrieves the value associated with a given key from the cache. If the key is not found, it should returnundefined.delete(key: string): Removes a key-value pair from the cache.
The cache should simulate a distributed environment. While you don't need to implement actual network communication, you do need to simulate the distribution of data across multiple nodes. The nodes array represents the available cache servers. The consistent hashing strategy should determine which node a given key is assigned to.
Key Requirements:
- Consistent Hashing: Implement a simple consistent hashing algorithm to map keys to nodes. A basic approach is to use the modulo operator (
%) on the key's hash code (e.g.,key.hashCode() % nodes.length) to determine the node index. - Node Simulation: The cache should internally maintain a data structure (e.g., a
Map) to represent the data stored on each node. Each node is represented by aMap<string, any>. - Error Handling: The
getanddeletemethods should handle cases where the key is not found gracefully, returningundefinedforget. - Testability: The code should be designed with testability in mind, making it easy to mock dependencies and verify behavior in Jest tests.
Expected Behavior:
setshould distribute keys across the nodes based on the consistent hashing algorithm.getshould retrieve data from the correct node based on the consistent hashing algorithm.deleteshould remove data from the correct node.- If a key is not found,
getshould returnundefined.
Edge Cases to Consider:
- Empty
nodesarray in the constructor. Should throw an error. - Duplicate keys. The last
setoperation should overwrite previous values. - Large number of nodes. The consistent hashing should still function correctly.
- Keys with unusual characters or lengths. The hashing function should handle these gracefully.
Examples
Example 1:
Input:
nodes = ["node1", "node2"]
set("key1", "value1")
set("key2", "value2")
get("key1")
Output: "value1"
Explanation: Assuming consistent hashing maps "key1" to "node1" and "key2" to "node2", get("key1") retrieves the value from "node1".
Example 2:
Input:
nodes = ["node1", "node2", "node3"]
set("key1", "value1")
set("key2", "value2")
set("key3", "value3")
delete("key2")
get("key2")
Output: undefined
Explanation: Assuming consistent hashing maps "key1" to "node1", "key2" to "node2", and "key3" to "node3", deleting "key2" removes it from "node2". `get("key2")` then returns `undefined`.
Example 3:
Input:
nodes = []
set("key1", "value1")
Output: Error thrown (e.g., "Nodes array cannot be empty")
Explanation: The constructor should throw an error if the nodes array is empty.
Constraints
- The number of nodes can range from 1 to 100.
- Keys are strings, and values can be of any type.
- The consistent hashing algorithm should be simple and efficient. Performance is not a primary concern for this challenge, but avoid excessively complex hashing functions.
- The
setoperation should complete within a reasonable time (e.g., less than 100ms). - The
getanddeleteoperations should also complete within a reasonable time.
Notes
- You can use a simple hashing function like
key.hashCode(). JavaScript doesn't have a built-inhashCode()method, so you can use a polyfill or a simple string hashing algorithm. For example:function hashCode(str) { let hash = 0; for (let i = 0; i < str.length; i++) { hash = (hash << 5) - hash + str.charCodeAt(i); } return hash;} - The consistent hashing strategy is crucial. Ensure that keys are distributed relatively evenly across the nodes.
- Focus on the core functionality and testability. You don't need to implement features like replication, persistence, or advanced consistency protocols.
- Write comprehensive Jest tests to cover all the functionalities and edge cases. Test the consistent hashing behavior to ensure keys are mapped to the correct nodes.
- Consider using
Mapobjects in TypeScript to represent the data stored on each node. This provides efficient key-value lookups. - The error handling for an empty
nodesarray should be explicit (e.g., throwing anErrorobject).