UNPKG

@trifrost/core

Version:

Blazingly fast, runtime-agnostic server framework for modern edge and node environments

145 lines (144 loc) 6.69 kB
"use strict"; /// <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;