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
JavaScript
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