UNPKG

bentocache

Version:

Multi-tier cache module for Node.js. Redis, Upstash, CloudfareKV, File, in-memory and others drivers

214 lines (213 loc) 5.56 kB
import { BaseDriver } from "../../chunk-BO75WXSS.js"; // src/drivers/dynamodb.ts import { chunkify } from "@julr/utils/array/chunkify"; import { DynamoDBClient, GetItemCommand, PutItemCommand, DeleteItemCommand, BatchWriteItemCommand, ScanCommand, ConditionalCheckFailedException } from "@aws-sdk/client-dynamodb"; function dynamoDbDriver(options) { return { options, factory: (config) => new DynamoDbDriver(config) }; } var DynamoDbDriver = class _DynamoDbDriver extends BaseDriver { type = "l2"; /** * DynamoDB client */ #client; /** * Name of the table to use * Defaults to `cache` */ #tableName; constructor(config) { super(config); this.#tableName = this.config.table.name ?? "cache"; if (config.client) { this.#client = config.client; return; } this.#client = new DynamoDBClient({ region: config.region, credentials: config.credentials, endpoint: config.endpoint }); } /** * Try to delete an item from the cache. * If the item doesn't exist, a `ConditionalCheckFailedException` is thrown. */ async #deleteItem(key) { await this.#client.send( new DeleteItemCommand({ TableName: this.#tableName, Key: { key: { S: this.getItemKey(key) } }, ConditionExpression: "attribute_exists(#key)", ExpressionAttributeNames: { "#key": "key" } }) ); } /** * Scan the table for items with our prefix * Returns a paginated list of items */ async #getStoredItems(exclusiveStartKey) { return await this.#client.send( new ScanCommand({ TableName: this.#tableName, ProjectionExpression: "#key", FilterExpression: "begins_with(#key, :prefix)", ExpressionAttributeNames: { "#key": "key" }, ExpressionAttributeValues: { ":prefix": { S: `${this.prefix}:` } }, ExclusiveStartKey: exclusiveStartKey }) ); } /** * Delete multiple items from our table */ async #batchDeleteItems(items) { const requests = items.map((item) => ({ DeleteRequest: { Key: item } })); const command = new BatchWriteItemCommand({ RequestItems: { [this.#tableName]: requests } }); await this.#client.send(command); } /** * Check if the given item TTL is expired. * * We have to do this manually for local execution against * the dynamodb-local docker image since it doesn't support * TTLs. */ #isItemExpired(item) { if (!item.ttl) return false; const now = Math.floor(Date.now() / 1e3); return Number(item.ttl.N) < now; } /** * Convert a TTL duration in miliseconds to * a UNIX timestamp in seconds since DynamoDB * accepts this format. */ #computeTtl(ttl) { return Math.floor((Date.now() + ttl) / 1e3).toString(); } /** * Generate the payload for a WriteRequest * * We append the TTL attribute only if a TTL is defined. * If no TTL is defined, the item will never expire. */ #createItemPayload(key, value, ttl) { return { key: { S: this.getItemKey(key) }, value: { S: value }, ...ttl ? { ttl: { N: this.#computeTtl(ttl) } } : {} }; } /** * Returns a new instance of the driver with the given namespace. */ namespace(namespace) { return new _DynamoDbDriver({ ...this.config, client: this.#client, prefix: this.createNamespacePrefix(namespace) }); } /** * Get a value from the cache */ async get(key) { const command = new GetItemCommand({ Key: { key: { S: this.getItemKey(key) } }, TableName: this.#tableName }); const data = await this.#client.send(command); if (!data.Item || this.#isItemExpired(data.Item)) { return void 0; } return data.Item.value.S ?? data.Item.value.N; } /** * Get the value of a key and delete it * * Returns the value if the key exists, undefined otherwise */ async pull(key) { const value = await this.get(key); if (value === void 0) { return void 0; } await this.delete(key); return value; } /** * Put a value in the cache * Returns true if the value was set, false otherwise */ async set(key, value, ttl) { const command = new PutItemCommand({ TableName: this.#tableName, Item: this.#createItemPayload(key, value, ttl) }); await this.#client.send(command); return true; } /** * Remove all items from the cache */ async clear() { let exclusiveStartKey; do { const result = await this.#getStoredItems(exclusiveStartKey); const chunkedItems = chunkify(result.Items ?? [], 25); for (const chunk of chunkedItems) { await this.#batchDeleteItems(chunk); } exclusiveStartKey = result.LastEvaluatedKey; } while (exclusiveStartKey); } /** * Delete a key from the cache * Returns true if the key was deleted, false otherwise */ async delete(key) { try { await this.#deleteItem(key); return true; } catch (error) { if (error instanceof ConditionalCheckFailedException) { return false; } throw error; } } /** * Delete multiple keys from the cache */ async deleteMany(keys) { if (keys.length === 0) return true; await Promise.all(keys.map((key) => this.delete(key))); return true; } /** * Closes the connection to the cache */ async disconnect() { this.#client.destroy(); } }; export { DynamoDbDriver, dynamoDbDriver }; //# sourceMappingURL=dynamodb.js.map