@trifrost/core
Version:
Blazingly fast, runtime-agnostic server framework for modern edge and node environments
145 lines (144 loc) • 6.69 kB
JavaScript
;
/// <reference types="@cloudflare/workers-types" />
Object.defineProperty(exports, "__esModule", { value: true });
exports.TriFrostDurableObject = void 0;
const array_1 = require("@valkyriestudios/utils/array");
const number_1 = require("@valkyriestudios/utils/number");
const string_1 = require("@valkyriestudios/utils/string");
/* We bucket ttl per 10 seconds */
const BUCKET_INTERVAL = 10_000;
const BUCKET_ALARM = 60_000;
const BUCKET_PREFIX = 'ttl:bucket:';
/* Computes the ttl bucket for a specific timestamp */
const bucketFor = (ts) => Math.floor(ts / BUCKET_INTERVAL) * BUCKET_INTERVAL;
/**
* TriFrostDurableObject — backs modules like RateLimit and Cache via bucketed TTL expiry
*/
class TriFrostDurableObject {
#state;
constructor(state) {
this.#state = state;
}
/**
* Alarm — deletes expired keys from current and past buckets
*/
async alarm() {
const now = Date.now();
const buckets = await this.#state.storage.list({ prefix: BUCKET_PREFIX });
/* The next alarm will be scheduled to our bucket alarm (default of 60 seconds) */
let next_alarm = Date.now() + BUCKET_ALARM;
const to_delete = [];
for (const [bucket_key, keys] of buckets.entries()) {
const ts = parseInt(bucket_key.slice(BUCKET_PREFIX.length), 10);
/* If either the stored timestamp is below our current time OR the bucket timestamp is invalid, purge it */
if (!(0, number_1.isNum)(ts) || (0, number_1.isNumGte)(now, ts)) {
to_delete.push(bucket_key);
if (Array.isArray(keys)) {
for (let i = 0; i < keys.length; i++)
to_delete.push(keys[i]);
}
}
else if ((0, number_1.isNumGt)(next_alarm, ts)) {
/* Next alarm earlier */
next_alarm = ts;
}
}
/* Set next alarm */
await this.#state.storage.setAlarm(next_alarm);
/* Evict keys */
for (const batch of (0, array_1.split)(to_delete, 128)) {
await this.#state.storage.delete(batch);
}
}
/**
* Fetch — routes by /trifrost-{namespace}?key={key}
*/
async fetch(request) {
const url = new URL(request.url);
/* Ensure key exists */
const key = url.searchParams.get('key');
if (!(0, string_1.isNeString)(key))
return new Response('Missing key', { status: 400 });
/* Get namespace */
const match = url.pathname.match(/^\/trifrost-([a-z0-9_-]+)$/i);
if (!match || match.length < 1 || !(0, string_1.isNeString)(match[1]))
return new Response('Invalid namespace', { status: 400 });
/* Namespace key */
const N_KEY = `${match[1]}:${key}`;
switch (request.method) {
case 'GET': {
try {
const stored = await this.#state.storage.get(N_KEY);
if (!stored)
return new Response('null', { status: 200, headers: { 'content-type': 'application/json' } });
/* Lazy delete on read */
const now = Date.now();
if (!(0, number_1.isNum)(stored.exp) || (0, number_1.isNumGte)(now, stored.exp)) {
await this.#state.storage.delete(N_KEY);
return new Response('null', { status: 200, headers: { 'content-type': 'application/json' } });
}
return new Response(JSON.stringify(stored.v), { status: 200, headers: { 'content-type': 'application/json' } });
}
catch {
return new Response('Internal Error', { status: 500 });
}
}
case 'PUT': {
try {
/* Prevent consumers from writing to the ttl namespace */
if (N_KEY.startsWith(BUCKET_PREFIX))
return new Response('Invalid key: reserved prefix', { status: 400 });
if ((request.headers.get('content-type') || '').indexOf('application/json') < 0)
return new Response('Unsupported content type', { status: 415 });
const { v, ttl } = (await request.json());
if (!(0, number_1.isIntGt)(ttl, 0))
return new Response('Invalid TTL', { status: 400 });
const now = Date.now();
const exp = now + ttl * 1000;
const bucket = bucketFor(exp);
const bucket_key = BUCKET_PREFIX + bucket;
const set = new Set((await this.#state.storage.get(bucket_key)) || []);
set.add(N_KEY);
await Promise.all([
this.#state.storage.put(N_KEY, { v, exp }),
this.#state.storage.put(bucket_key, [...set]),
this.#state.storage.setAlarm(bucket),
]);
return new Response('OK', { status: 200 });
}
catch {
return new Response('Invalid body', { status: 400 });
}
}
case 'DELETE': {
try {
const pat_idx = N_KEY.indexOf('*');
if (pat_idx < 0) {
/* Run single delete */
await this.#state.storage.delete(N_KEY);
return new Response(null, { status: 204 });
}
else if (key.length === 1 || pat_idx !== N_KEY.length - 1) {
return new Response('Wildcard deletion must end with "*" (e.g. "prefix:*")', { status: 400 });
}
/* Run Pattern deletion */
const entries = await this.#state.storage.list({
prefix: N_KEY.slice(0, -1),
});
if (entries.size) {
for (const batch of (0, array_1.split)([...entries.keys()], 128)) {
await this.#state.storage.delete(batch);
}
}
return new Response(null, { status: 204 });
}
catch {
return new Response('Internal Error', { status: 500 });
}
}
default:
return new Response('Method not allowed', { status: 405 });
}
}
}
exports.TriFrostDurableObject = TriFrostDurableObject;