UNPKG

hamok

Version:

Lightweight Distributed Object Storage on RAFT consensus algorithm

312 lines (241 loc) 10.8 kB
## User Manual [Hamok](./index.md) / [HamokEmitter](./emitter.md) / [HamokMap](./map.md) / [HamokQueue](./queue.md) / [HamokRecord](./record.md) / HamokRemoteMap ## Table of Contents - [Overview](#overview) - [Configuration](#configuration) - [API Reference](#api-reference) - [Properties](#properties) - [Events](#events) - [Methods](#methods) - [Examples](#examples) - [FAQ](#faq) ## Overview `HamokRemoteMap` is a class that provides a distributed storage solution, enabling key-value pair manipulation across multiple instances with event-driven notifications. The primary distinction between `HamokMap` and `HamokRemoteMap` lies in the underlying storage mechanism. `HamokRemoteMap` leverages a `RemoteMap` as its base for storing key-value pairs, which can reside in a remote location (e.g., Redis, a database, etc.). Hamok is then used solely to ensure operational consistency across distributed instances. This design is ideal for scenarios where a large number of key-value pairs need to be managed by multiple instances. Instead of introducing a distributed locking mechanism, `HamokRemoteMap` utilizes the RAFT consensus algorithm to guarantee consistent operation execution, ensuring consistency and fault tolerance in distributed systems. ### Important Notes - RemoteMap relies on leader peer, as the leader peer is the only one which can executes action on a remote map. Therefore all peer which potentially can be the leader must create this map. ## Configuration ```typescript const config: HamokRemoteMapBuilderConfig<number, string> = { /** * The unique identifier for the map. */ mapId: "remoteMapId", /** * Optional. The timeout duration in milliseconds for requests. */ requestTimeoutInMs: 5000, /** * Optional. A codec for encoding and decoding keys in the map. * * DEFAULT: JSON codec */ keyCodec: { encode: (key: number) => (new DataView(new ArrayBuffer(4))).setInt32(0, key)), decode: (data: Uint8Array) => (new DataView(data)).getInt32(0), }, /** * Optional. A codec for encoding and decoding values in the map. * * DEFAULT: JSON Codec */ valueCodec: valueCodec?: { encode: (value: V) => Buffer.from(JSON.stringify(value)), decode: (data: Uint8Array) => JSON.parse(Buffer.from(data).toString()), }, /** * Optional. The maximum number of keys allowed in request or response messages. * * DEFAULT: 0 means Infinity */ maxOutboundMessageKeys: 1000, /** * Optional. The maximum number of values allowed in request or response messages. * * DEFAULT: 0 means Infinity */ maxOutboundMessageValues: 1000, /** * The remote map to be used to store the data. */ remoteMap: createMyRemoteMap(), /** * Flag indicate if the events should be emitted by the event emitter or not. * It also reduces the communication overhead if not needed, as for emitting events * the leader should send a message to all followers to emit an event. * In such case when it's not necessary (like cache maintenance) it can be disabled. * * DEFAULT: false */ noEvents: false, /** * Optional. A function to determine equality between two values. * Used for custom equality checking. */ equalValues: (a: V, b: V) => a === b, }; const remoteMap = hamok.createRemoteMap<number, string>(config); ``` ## API Reference ### Properties - **`id: string`**: Returns the unique identifier of the storage. - **`closed: boolean`**: Indicates whether the storage is closed. ### Events `HamokRemoteMap` extends `EventEmitter` and emits the following events: - **`insert(key: K, value: V)`**: Emitted when a new entry is inserted. - **`update(key: K, oldValue: V, newValue: V)`**: Emitted when an entry is updated. - **`remove(key: K, value: V)`**: Emitted when an entry is removed. - **`clear()`**: Emitted when the storage is cleared. - **`close()`**: Emitted when the storage is closed. ### Event Handling You can listen to these events using the standard `EventEmitter` API: ```typescript remoteMap.on("insert", (key, value) => { console.log(`Inserted: ${key} = ${value}`); }); remoteMap.on("update", (key, oldValue, newValue) => { console.log(`Updated: ${key} from ${oldValue} to ${newValue}`); }); remoteMap.on("remove", (key, value) => { console.log(`Removed: ${key} = ${value}`); }); ``` ### Methods - **`close(): void`** Closes the storage, disconnecting it from the network and releasing all resources. - **`size(): Promise<number>`** Returns the number of entries in the storage. - **`isEmpty(): Promise<boolean>`** Returns `true` if the storage is empty, `false` otherwise. - **`keys(): AsyncIterableIterator<K>`** Returns an iterator for the keys in the storage. - **`clear(): Promise<void>`** Clears all entries from the storage. - **`get(key: K): Promise<V | undefined>`** Retrieves the value associated with the specified key. - **`getAll(keys: IterableIterator<K> | K[]): Promise<ReadonlyMap<K, V>>`** Retrieves all values associated with the specified keys. - **`set(key: K, value: V): Promise<V | undefined>`** Sets a key-value pair in the storage. If the key already exists, the value is updated. - **`setAll(entries: ReadonlyMap<K, V>): Promise<ReadonlyMap<K, V>>`** Sets multiple key-value pairs in the storage. - **`insert(key: K, value: V): Promise<V | undefined>`** Inserts a key-value pair into the storage. If the key already exists, it will not be updated. - **`insertAll(entries: ReadonlyMap<K, V> | [K, V][]): Promise<ReadonlyMap<K, V>>`** Inserts multiple key-value pairs into the storage. - **`delete(key: K): Promise<boolean>`** Deletes a key-value pair from the storage by key. - **`deleteAll(keys: ReadonlySet<K> | K[]): Promise<ReadonlySet<K>>`** Deletes multiple key-value pairs from the storage by their keys. - **`remove(key: K): Promise<boolean>`** Removes a key-value pair from the storage by key. - **`removeAll(keys: ReadonlySet<K> | K[]): Promise<ReadonlyMap<K, V>>`** Removes multiple key-value pairs from the storage by their keys. - **`updateIf(key: K, value: V, oldValue: V): Promise<boolean>`** Updates a key-value pair in the storage if the current value matches the specified `oldValue`. - **`iterator(): AsyncIterableIterator<[K, V]>`** Returns an iterator for the key-value pairs in the storage. - **`sync(): Promise<void>`** Synchronizes the storage with the remote peers. (waiting for the commitHead in hamok) ## Examples - [use redis](https://github.com/balazskreith/hamok-ts/blob/main/examples/src/redis-remote-map-example.ts) ### Add Redis as RemoteMap ```typescript import { RemoteMap } from "hamok/lib/collections/RemoteMap"; import Redis from "ioredis"; const publisher = new Redis(); const subscriber = new Redis(); type CachedItem = { id: string; value: string; }; function createRemoteMap(mapId: string): RemoteMap<string, CachedItem> { return { async set(key, value, callback) { const oldValue = await publisher.hget(mapId, key); await publisher.hset(mapId, key, JSON.stringify(value)); callback?.(oldValue ? JSON.parse(oldValue) : undefined); }, async setAll(entries, callback) { const inserted: [string, CachedItem][] = []; const updated: [string, CachedItem, CachedItem][] = []; for (const [key, value] of entries) { const oldValue = await publisher.hget(mapId, key); if (oldValue) { updated.push([key, JSON.parse(oldValue), value]); } else { inserted.push([key, value]); } await publisher.hset(mapId, key, JSON.stringify(value)); } callback?.({ inserted, updated }); }, iterator() { async function* asyncIterator() { const keys = await publisher.hkeys(mapId); for (const key of keys) { const value = await publisher.hget(mapId, key); yield [key, value ? JSON.parse(value) : undefined] as [ string, CachedItem ]; } } return asyncIterator(); }, async get(key) { const value = await publisher.hget(mapId, key); return value ? JSON.parse(value) : undefined; }, async keys() { return (await publisher.hkeys(mapId)).values(); }, async getAll(keys) { const iteratedKeys = [...keys]; const values = await Promise.all( iteratedKeys.map((key) => publisher.hget(mapId, key)) ); const entries = iteratedKeys .map((key, index) => [ key, values[index] ? JSON.parse(values[index]) : undefined, ]) .filter(([, value]) => value !== undefined); return new Map(entries as [string, CachedItem][]); }, async remove(key) { const value = await publisher.hget(mapId, key); await publisher.hdel(mapId, key); return value ? JSON.parse(value) : undefined; }, async removeAll(keys) { const iteratedKeys = [...keys]; const values = await Promise.all( iteratedKeys.map((key) => publisher.hget(mapId, key)) ); const entries = iteratedKeys .map((key, index) => [ key, values[index] ? JSON.parse(values[index]) : undefined, ]) .filter(([, value]) => value !== undefined); await publisher.hdel(mapId, ...iteratedKeys); return new Map(entries as [string, CachedItem][]); }, async clear() { return publisher.del(mapId).then(() => void 0); }, async size() { return publisher.hlen(mapId); }, }; } ``` Note that the serialization and deserialization methods for keys and values differ between Hamok instances and between an instance and a `RemoteMap`. This is because Hamok requires binary serialization and deserialization for its messages, while the requirements for `RemoteMap` are unknown and left to the developer's discretion. In the example above, we used simple JSON serialization and deserialization for keys and values. ## FAQ ### **How does `HamokRemoteMap` ensure data consistency?** `HamokRemoteMap` uses Hamok which uses the Raft consensus algorithm to manage and replicate logs across distributed nodes, ensuring that all nodes agree on the operation order of the key-value store has to execute. ### **Can I use custom functions to compare values in `HamokRemoteMap`?** Yes, you can provide a custom equality function when initializing `HamokRemoteMap` to define how values should be compared. ### **What happens when the storage is closed?** When `HamokRemoteMap` is closed, it disconnects from the network, releases resources, and stops accepting or processing any new operations. All listeners are removed, and the `close` event is emitted.