Building an Eventually Consistent Key-Value Store in JavaScript
This challenge asks you to implement a simplified, eventually consistent key-value store using JavaScript. Eventually consistent systems prioritize availability and partition tolerance over immediate consistency, meaning updates might not be immediately visible to all clients. This is a common pattern in distributed systems where network latency and failures are expected.
Problem Description
You are tasked with creating a JavaScript class called EventuallyConsistentStore. This store should allow clients to put (write) and get (read) key-value pairs. Due to the "eventual consistency" nature, reads might not immediately reflect the latest writes. The store will internally use a queue to simulate asynchronous updates and eventual propagation of changes.
Key Requirements:
put(key, value): Adds a key-value pair to the store. This operation should be asynchronous, simulating a delayed update. Theputoperation itself should return immediately.get(key): Retrieves the value associated with a given key. This operation should return a Promise that resolves with the value if found, orundefinedif the key doesn't exist. The Promise should resolve eventually, reflecting the state of the store after any pending updates have been applied.- Internal Queue: The store should maintain an internal queue of pending updates (key-value pairs). These updates should be processed asynchronously at regular intervals.
- Asynchronous Processing: The processing of the update queue should be handled asynchronously (e.g., using
setTimeoutorsetInterval). - Concurrency: The store should handle concurrent
putandgetoperations gracefully.
Expected Behavior:
- After a
putoperation, the value might not be immediately available whengetis called. - The
getoperation should eventually return the latest value after the update has been processed. - If a key is not present,
getshould returnundefined. - Multiple
putoperations should be processed in the order they were added to the queue.
Edge Cases to Consider:
- What happens if
getis called before any updates have been processed? - What happens if
getis called while an update is being processed? - How should the store handle duplicate keys? (Last write wins is a reasonable assumption)
- What happens if the queue is empty?
Examples
Example 1:
Input:
store.put('a', 1);
setTimeout(() => store.put('a', 2), 100); // Put a new value after 100ms
setTimeout(() => store.get('a'), 200); // Get the value after 200ms
Output: (Promise resolves to 2)
Explanation: The first put sets the value to 1. After 100ms, the value is updated to 2. After another 100ms (total 200ms), the get operation retrieves the latest value, which is 2.
Example 2:
Input:
store.put('b', 'hello');
setTimeout(() => store.get('b'), 50); // Get before processing
setTimeout(() => store.put('b', 'world'), 100); // Update after 100ms
Output: (Promise resolves to 'world')
Explanation: The initial get is called before the update is processed. The update is processed after 100ms, and the subsequent get retrieves the updated value 'world'.
Example 3:
Input:
store.get('c');
Output: (Promise resolves to undefined)
Explanation: The key 'c' has never been put into the store, so get should return undefined.
Constraints
- The internal queue should have a maximum size of 100. If the queue is full when a new
putis called, the newputshould be ignored (no error should be thrown). - The asynchronous processing interval should be between 50ms and 200ms (inclusive).
- All operations must be asynchronous.
- The store should be implemented as a class.
- The
getmethod must return a Promise.
Notes
- Consider using
setTimeoutorsetIntervalto simulate asynchronous processing. - Think about how to handle concurrent access to the store's internal data. You might need to use techniques like closures or locking to prevent race conditions.
- The focus is on demonstrating the eventual consistency concept, not on building a production-ready distributed store. Error handling and more sophisticated concurrency control are not required.
- The order of operations is important. Ensure that updates are processed in the order they are added to the queue.
- You don't need to persist the data to disk; the in-memory store is sufficient.