UNPKG

flareutils

Version:

Small utilities and little goodies that make developing with Cloudflare easier and faster.

1,330 lines (1,320 loc) 42 kB
var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); // src/Isaac/helpers.ts function toIntArray(str) { let w1; let w2; let u; const r4 = []; const r = []; let i = 0; const s = `${str}\0\0\0`; const l = s.length - 1; while (i < l) { w1 = s.charCodeAt(i++); w2 = s.charCodeAt(i + 1); if (w1 < 128) r4.push(w1); else if (w1 < 2048) { r4.push(w1 >>> 6 & 31 | 192); r4.push(w1 >>> 0 & 63 | 128); } else if ((w1 & 63488) !== 55296) { r4.push(w1 >>> 12 & 15 | 224); r4.push(w1 >>> 6 & 63 | 128); r4.push(w1 >>> 0 & 63 | 128); } else if ((w1 & 64512) === 55296 && (w2 & 64512) === 56320) { u = (w2 & 63 | (w1 & 63) << 10) + 65536; r4.push(u >>> 18 & 7 | 240); r4.push(u >>> 12 & 63 | 128); r4.push(u >>> 6 & 63 | 128); r4.push(u >>> 0 & 63 | 128); i++; } if (r4.length > 3) { r.push( r4.shift() << 0 | r4.shift() << 8 | r4.shift() << 16 | r4.shift() << 24 ); } } return r; } function add(a, b) { const lsb = (a & 65535) + (b & 65535); const msb = (a >>> 16) + (b >>> 16) + (lsb >>> 16); return msb << 16 | lsb & 65535; } function seed_mix(arr) { arr[0] ^= arr[1] << 11; arr[3] = add(arr[3], arr[0]); arr[1] = add(arr[1], arr[2]); arr[1] ^= arr[2] >>> 2; arr[4] = add(arr[4], arr[1]); arr[2] = add(arr[2], arr[3]); arr[2] ^= arr[3] << 8; arr[5] = add(arr[5], arr[2]); arr[3] = add(arr[3], arr[4]); arr[3] ^= arr[4] >>> 16; arr[6] = add(arr[6], arr[3]); arr[4] = add(arr[4], arr[5]); arr[4] ^= arr[5] << 10; arr[7] = add(arr[7], arr[4]); arr[5] = add(arr[5], arr[6]); arr[5] ^= arr[6] >>> 4; arr[0] = add(arr[0], arr[5]); arr[6] = add(arr[6], arr[7]); arr[6] ^= arr[7] << 8; arr[1] = add(arr[1], arr[6]); arr[7] = add(arr[7], arr[0]); arr[7] ^= arr[0] >>> 9; arr[2] = add(arr[2], arr[7]); arr[0] = add(arr[0], arr[1]); return arr; } // src/Isaac/index.ts var Isaac = class { /** * Creates a new Isaac CSPRNG. Note that you must await `seeding` before using the generator. * @param {IsaacSeed} seed Seed to be fed into the generator. * @param {number} runs Number of times to re-run the generator. * @constructor */ constructor(seed, runs) { __publicField(this, "runs"); __publicField(this, "m"); __publicField(this, "acc"); __publicField(this, "brs"); __publicField(this, "cnt"); __publicField(this, "r"); __publicField(this, "gnt"); /** * This promise represents whether the seeding process has been completed. It is recommended that you create the Isaac object as early as possible, do other tasks as needed, and then *await* the promise afterward to ensure that the seeding process has completed. If it is *false*, then the seeding process has completed, and no *await* is necessary. */ __publicField(this, "seeding"); this.runs = runs || 1; this.m = this.r = Array(256); this.acc = this.brs = this.cnt = this.gnt = 0; this.seeding = this.seed(seed); } /** * Batch-generates 256 random numbers, and stores them in the number buffer. * @private */ prng() { let i; let x; let y; let n = this.runs; while (n--) { this.cnt = add(this.cnt, 1); this.brs = add(this.brs, this.cnt); for (i = 0; i < 256; i++) { switch (i & 3) { case 0: this.acc ^= this.acc << 13; break; case 1: this.acc ^= this.acc >>> 6; break; case 2: this.acc ^= this.acc << 2; break; case 3: this.acc ^= this.acc >>> 16; break; } this.acc = add(this.m[i + 128 & 255], this.acc); x = this.m[i]; this.m[i] = y = add(this.m[x >>> 2 & 255], add(this.acc, this.brs)); this.r[i] = this.brs = add(this.m[y >>> 10 & 255], x); } } } /** * Shuffles the given array with the seed array, to ensure even mix. * @param {number[]} arr Array filled with mixed numbers * @param {number[]} s The seed, as an array of numbers * @returns {number[]} The mixed array * @private */ superShuffle(arr, s) { for (let i = 0; i < 256; i += 8) { if (s) for (let j = 0; j < 8; j++) arr[j] = add(arr[j], this.r[i + j]); arr = seed_mix(arr); for (let j = 0; j < 8; j++) this.m[i + j] = arr[j]; } return arr; } /** * Resets the internal state of the generator. * @private */ reset() { this.acc = this.brs = this.cnt = this.gnt = 0; for (let i = 0; i < 256; ++i) this.m[i] = this.r[i] = 0; } /** * Seeds the generator. Note that you must await `seeding` before using the generator, if not manually seeded and awaited. * @param {IsaacSeed} seed Seed to be fed into the generator * @returns {Promise<void>} Promise that resolves when the generator has been seeded * @async * @example ```ts * await isaac.seed(seed); * ``` */ async seed(seed) { let arr = Array.from(crypto.getRandomValues(new Uint32Array(9))); let s = []; let i; if (seed && typeof seed === "string") s = toIntArray(seed); if (seed && typeof seed === "number") s = [seed]; try { s.push( Number( (await (await fetch("https://drand.cloudflare.com/public/latest")).json()).signature ) ); } catch (e) { } s.push(Date.now()); this.reset(); for (i = 0; i < s.length; i++) this.r[i & 255] += typeof s[i] === "number" ? s[i] : 0; for (i = 0; i < 4; i++) arr = seed_mix(arr); arr = this.superShuffle(arr, s); if (s) arr = this.superShuffle(arr, s); this.prng(); this.gnt = 256; this.seeding = false; } /** * Returns a pre-generated random number from the number buffer. * @returns {number} A random number between 0 and 1 * @example ```ts * const num = isaac.rand(); * ``` */ rand() { if (!this.gnt--) { this.prng(); this.gnt = 255; } return 0.5 + this.r[this.gnt] * 23283064365386963e-26; } }; // src/Phonetic.ts function randOf(collection, rand) { return () => collection[Math.floor(rand() * collection.length)]; } var Phonetic = class { /** * Creates a new Phonetic generator. * @param {RandNum} rand Random Number Generator. While it is recommended to use a cryptographically secure random number generator(a la. [Isaac](/classes/Isaac)), this is not required. * @constructor */ constructor(rand) { /** * Random number generator used by Phonetic. Must generate numbers from 0 to 1. */ __publicField(this, "generator"); /** * Returns a random vowel. */ __publicField(this, "vowel"); /** * Returns a random consonant. */ __publicField(this, "consonant"); this.generator = rand; this.vowel = randOf("aeiou", rand); this.consonant = randOf("bcdfghjklmnpqrstvwxyz", rand); } /** * Generates a random phonetic string. * @param {number} length Length of the string to generate. * @returns {string} Random phonetic string. * @example * const phonetic = phonetic.rand(10); */ rand(len = 10) { let id = ""; const start = Math.round(this.generator()); for (let i = 0; i < len; i++) id += i % 2 === start ? this.consonant() : this.vowel(); return id; } }; // src/BetterKV/old.ts function normalizeCacheTtl(cacheTtl) { if (!cacheTtl || cacheTtl <= 60) return 60; return cacheTtl; } var BetterKVOld = class { /** * Creates a new BetterKV instance. * @param {KVNamespace} kv The KV Namespace to use as the primary data store. * @param {ExecutionContext["waitUntil"]} waitUntil The waitUntil function used to asyncronously update the cache. Must be passed in before executing any other methods on every new request. * @param {string} cacheSpace The name utilized to create a dedicated cache for this BetterKV instance. If you have multiple instances of BetterKV running in parallel, make sure each has their own unique cacheSpace. * @example ```ts * const NAMESPACE = new BetterKV(env.KV, "BetterKVNamespace"); * ``` */ constructor(kv, waitUntil, cacheSpace) { /** * Base URL used by BetterKV in Cache Operations. */ __publicField(this, "url", "https://better.kv/"); /** * Root KV instance utilized by BetterKV. */ __publicField(this, "kv"); /** * Utilized to ensure that any operations performed on the cache do not block the main thread. */ __publicField(this, "waitUntil"); /** * The name utilized to create a dedicated cache for this BetterKV instance. If you have multiple instances of BetterKV running in parallel, make sure each has their own unique cacheSpace. */ __publicField(this, "cacheSpace"); /** * Cache instance utilized by BetterKV. */ __publicField(this, "cache"); this.kv = kv; this.waitUntil = waitUntil; this.cacheSpace = cacheSpace; } /** * Retrieves the cache instance utilized by BetterKV. Ensures that the cache is only opened once, and can be shared across multiple runs of BetterKV. If no cacheSpace is provided, the default cache is used. * @private */ async getCache() { if (!this.cache) { if (this.cacheSpace) { this.cache = await caches.open(this.cacheSpace); } else { this.cache = caches.default; } } return this.cache; } /** * Used to update the waitUntil function to the ExecutionContext of the currently executing request. Should be passed in before executing any other methods on every new request. * @param {ExecutionContext["waitUntil"]} waitUntil The waitUntil function used to asyncronously update the cache. */ setWaitUntil(waitUntil) { this.waitUntil = waitUntil; } /** * Retrieves a value from the BetterKV Namespace. * @template K The type of the value. Only used if using the "json" type. * @param {string} key The key to retrieve. * @param {BetterKVGetOptions} options Options for the retrieval. * @returns {Promise<BetterKVGetReturns | null>} The value of the key, or null if the key does not exist. * @example ```ts * const value = await NAMESPACE.get(key); * ``` */ async get(key, options) { const cache = await this.getCache(); const cacheKey = this.url + key; let cacheTTL = 60; let type = "text"; if (options) { if (options.cacheTtl) { cacheTTL = normalizeCacheTtl(options.cacheTtl); } if (options.type) { type = options.type; } } const headers = new Headers({ "Cloudflare-CDN-Cache-Control": `max-age=${cacheTTL}` }); let bodyVal = await cache.match(cacheKey); if (bodyVal) { if (options?.cacheTtl) { bodyVal = new Response(bodyVal.body, bodyVal); bodyVal.headers.set( "Cloudflare-CDN-Cache-Control", `max-age=${cacheTTL}` ); this.waitUntil(cache.put(cacheKey, bodyVal)); } switch (type) { case "json": return await bodyVal.json(); case "arrayBuffer": return await bodyVal.arrayBuffer(); case "stream": return bodyVal.body; default: return await bodyVal.text(); } } switch (type) { case "text": { const textVal = await this.kv.get(key, { type: "text" }); if (!textVal) return null; this.waitUntil(cache.put(cacheKey, new Response(textVal, { headers }))); return { origin: "KV", val: textVal }; } case "json": { const jsonVal = await this.kv.get(key, { type: "json" }); if (!jsonVal) return null; this.waitUntil( cache.put( cacheKey, new Response(JSON.stringify(jsonVal), { headers }) ) ); return jsonVal; } case "arrayBuffer": { const bufVal = await this.kv.get(key, { type: "arrayBuffer" }); if (!bufVal) return null; this.waitUntil(cache.put(cacheKey, new Response(bufVal, { headers }))); return bufVal; } case "stream": { const streamVal = await this.kv.get(key, { type: "stream" }); if (!streamVal) return null; this.waitUntil( cache.put(cacheKey, new Response(streamVal, { headers })) ); return streamVal; } } } /** * Retrieves a value from the BetterKV Namespace, and its associated metadata, if provided. * @template V The type of the value. Only used if using the "json" type. * @template M The type of the metadata. * @param {string} key The key to retrieve. * @param {BetterKVGetOptions} options Options for the retrieval. * @returns {Promise<BetterKVWithMetadata<BetterKVGetReturns, M> | null>} The value of the key, and its associated metadata(if any), or null if the key does not exist. */ async getWithMetadata(key, options) { const cache = await this.getCache(); const cacheKey = this.url + key; let cacheTTL = 60; let type = "text"; if (options) { if (options.cacheTtl) { cacheTTL = normalizeCacheTtl(options.cacheTtl); } if (options.type) { type = options.type; } } let bodyVal = await cache.match(cacheKey); if (bodyVal) { if (options?.cacheTtl) { bodyVal = new Response(bodyVal.body, bodyVal); bodyVal.headers.set( "Cloudflare-CDN-Cache-Control", `max-age=${cacheTTL}` ); this.waitUntil(cache.put(cacheKey, bodyVal)); } const metadata = JSON.parse( bodyVal.headers.get("metadata") ); switch (type) { case "json": return { value: await bodyVal.json(), metadata, cacheStatus: "HIT" }; case "arrayBuffer": return { value: await bodyVal.arrayBuffer(), metadata, cacheStatus: "HIT" }; case "stream": return { value: bodyVal.body, metadata, cacheStatus: "HIT" }; default: return { value: await bodyVal.text(), metadata, cacheStatus: "HIT" }; } } const headers = new Headers({ "Cloudflare-CDN-Cache-Control": `max-age=${cacheTTL}` }); switch (type) { case "text": { const textVal = await this.kv.getWithMetadata(key); if (!textVal) return null; if (textVal.metadata) headers.set("metadata", JSON.stringify(textVal.metadata)); this.waitUntil( cache.put(cacheKey, new Response(textVal.value, { headers })) ); return { ...textVal, cacheStatus: "MISS" }; } case "json": { const jsonVal = await this.kv.getWithMetadata(key, { type: "json" }); if (!jsonVal) return null; if (jsonVal.metadata) headers.set("metadata", JSON.stringify(jsonVal.metadata)); this.waitUntil( cache.put( cacheKey, new Response(JSON.stringify(jsonVal), { headers }) ) ); return { ...jsonVal, cacheStatus: "MISS" }; } case "arrayBuffer": { const bufVal = await this.kv.getWithMetadata(key, { type: "arrayBuffer" }); if (!bufVal) return null; if (bufVal.metadata) headers.set("metadata", JSON.stringify(bufVal.metadata)); this.waitUntil( cache.put(cacheKey, new Response(bufVal.value, { headers })) ); return { ...bufVal, cacheStatus: "MISS" }; } case "stream": { const streamVal = await this.kv.getWithMetadata(key, { type: "stream" }); if (!streamVal) return null; if (streamVal.metadata) headers.set("metadata", JSON.stringify(streamVal.metadata)); this.waitUntil( cache.put(cacheKey, new Response(streamVal.value, { headers })) ); return { ...streamVal, cacheStatus: "MISS" }; } } return null; } /** * Adds a new value to the BetterKV Namespace. Supports CacheTtl. * @param {string} key The key to add. * @param {BetterKVValueOptions} val The value to add. Type is inferred from the value. * @param {BetterKVAddOptions} options Options for the addition. * @example ```ts * await NAMESPACE.put(key, value); * ``` */ async put(key, val, options) { const cache = await this.getCache(); const cacheKey = this.url + key; const cacheTtl = options?.cacheTtl ? normalizeCacheTtl(options.cacheTtl) : 60; const headers = new Headers({ "Cloudflare-CDN-Cache-Control": `max-age=${cacheTtl}` }); if (options?.metadata) headers.set("metadata", JSON.stringify(options.metadata)); if (typeof val === "string" || val instanceof ArrayBuffer) { this.waitUntil(cache.put(cacheKey, new Response(val, { headers }))); await this.kv.put(key, val, options); return; } if (val instanceof ReadableStream) { const a = val.tee(); this.waitUntil(cache.put(cacheKey, new Response(a[0], { headers }))); await this.kv.put(key, a[1], options); return; } if (typeof val !== "object") throw new Error("Invalid Put Type"); this.waitUntil( cache.put(cacheKey, new Response(JSON.stringify(val), { headers })) ); await this.kv.put(key, val, options); } /** * Removes a value from the BetterKV Namespace. * @param {string} key The key to remove. * @example ```ts * await NAMESPACE.delete(key); * ``` */ async delete(key) { const cache = await this.getCache(); this.waitUntil(cache.delete(this.url + key)); await this.kv.delete(key); } /** * Lists keys in the BetterKV Namespace according to the options given. Supports CacheTtl. * @template M The type of the metadata. * @param {BetterKVListOptions} [options] Options for the listing. * @returns {Promise<BetterKVListReturns<M>>} The keys in the namespace, and their associated metadata(if any). * @example ```ts * const {keys, list_complete, cursor} = await NAMESPACE.list(); * ``` */ async list(opts) { const cache = await this.getCache(); const cacheKey = new URL("https://list.better.kv"); let cacheTtl = 60; let limit = 1e3; let prefix = null; let cursor = null; if (opts) { if (opts.cacheTtl) { cacheTtl = normalizeCacheTtl(opts.cacheTtl); } if (opts.limit && opts.limit >= 1 && opts.limit < 1e3) { limit = opts.limit; } if (opts.prefix) { prefix = opts.prefix; cacheKey.searchParams.set("prefix", prefix); } if (opts.limit) { limit = opts.limit; cacheKey.searchParams.set("limit", limit.toString()); } if (opts.cursor) { cursor = opts.cursor; cacheKey.searchParams.append("cursor", cursor); } } let bodyVal = await cache.match(cacheKey.toString()); if (bodyVal) { bodyVal = new Response(bodyVal.body, bodyVal); bodyVal.headers.set( "Cloudflare-CDN-Cache-Control", `max-age=${cacheTtl}` ); this.waitUntil(cache.put(cacheKey.toString(), bodyVal)); const res = await bodyVal.json(); return { ...res, cacheStatus: "HIT" }; } const result = await this.kv.list({ prefix, limit, cursor }); this.waitUntil( cache.put( cacheKey.toString(), new Response(JSON.stringify(result), { headers: { "Cloudflare-CDN-Cache-Control": `max-age=${cacheTtl}` } }) ) ); return { ...result, cacheStatus: "MISS" }; } }; // src/BetterKV/index.ts var BetterKV = class { /** * Creates a new BetterKV instance. * @param {KVNamespace} KV The KV Namespace to use as the primary data store. * @param {ExecutionContext["waitUntil"]} waitUntil The waitUntil function used to asyncronously update the cache. Must be passed in before executing any other methods on every new request. * @param {BetterKVConfig} config Configuration for the BetterKV instance. * @example ```ts * const NAMESPACE = new BetterKV(env.KV, ctx.waitUntil); * ``` */ constructor(KV, waitUntil, config) { /** * Base URL used by BetterKV in Cache Operations. * @private */ __publicField(this, "URL", "https://better.kv/"); /** * Root KV instance utilized by BetterKV. * @private */ __publicField(this, "KV"); /** * Utilized to ensure that any operations performed on the cache do not block the main thread. * @private */ __publicField(this, "waitUntil"); /** * The name utilized to create a dedicated cache for this BetterKV instance. If you have multiple instances of BetterKV running in parallel, make sure each has their own unique cacheSpace. * @private */ __publicField(this, "config"); /** * Cache instance utilized by BetterKV. * @private */ __publicField(this, "cache"); this.KV = KV; this.waitUntil = waitUntil; this.config = { cacheSpace: void 0, probabilityGrowth: 1.28, cacheTtl: 55, kvCacheTtl: 31557600, ...config }; } /** * Retrieves the cache instance utilized by BetterKV. Ensures that the cache is only opened once, and can be shared across multiple runs of BetterKV. If no cacheSpace is provided, the default cache is used. * @private */ async getCache() { if (!this.cache) { if (this.config.cacheSpace) { this.cache = await caches.open(this.config.cacheSpace); } else { this.cache = caches.default; } } return this.cache; } /** * Used to update the waitUntil function to the ExecutionContext of the currently executing request. Should be passed in before executing any other methods on every new request. * @param {ExecutionContext["waitUntil"]} waitUntil The waitUntil function used to asyncronously update the cache. */ setWaitUntil(waitUntil) { this.waitUntil = waitUntil; } /** * Function to handle all GET-ops hitting origin KV. Should not be called manually. * @param {string} key The key to retrieve. * @private */ async getFromOrigin(key) { const baseHeaders = { "cloudflare-cdn-cache-control": `max-age=${this.config.cacheTtl}`, "betterkv-internal-created": Date.now().toString() }; const { value, metadata } = await this.KV.getWithMetadata(key, { type: "stream", cacheTtl: this.config.kvCacheTtl }); if (value === null) return null; return { res: new Response(value, { headers: { ...baseHeaders, "betterkv-internal-meta": metadata ? JSON.stringify(metadata) : "{}", "betterkv-internal-created": Date.now().toString() } }), meta: metadata }; } async get(key, type) { const cache = await this.getCache(); const cacheKey = this.URL + key; const bodyVal = await cache.match(cacheKey); if (bodyVal) { const created = Number(bodyVal.headers.get("betterkv-internal-created")); const probability = Number.isNaN(created) ? 1 : this.config.probabilityGrowth ** (Date.now() - created - this.config.cacheTtl * 1e3); if (Math.random() < probability) { const a = async () => { const newResponse = await this.getFromOrigin(key); if (newResponse) { await cache.put(cacheKey, newResponse.res); } else { await cache.delete(cacheKey); } }; this.waitUntil(a()); } switch (type) { case "json": return await bodyVal.json(); case "arrayBuffer": return await bodyVal.arrayBuffer(); case "stream": return bodyVal.body; default: return await bodyVal.text(); } } const originResponse = await this.getFromOrigin(key); if (!originResponse) return null; this.waitUntil(cache.put(cacheKey, originResponse.res.clone())); switch (type) { case "json": return originResponse.res.json(); case "arrayBuffer": return originResponse.res.arrayBuffer(); case "stream": return originResponse.res.body; default: return originResponse.res.text(); } } async getWithMetadata(key, type) { const cache = await this.getCache(); const cacheKey = this.URL + key; const bodyVal = await cache.match(cacheKey); if (bodyVal) { const created = Number(bodyVal.headers.get("betterkv-internal-created")); const probability = Number.isNaN(created) ? 1 : this.config.probabilityGrowth ** (Date.now() - created - this.config.cacheTtl * 1e3); let revalidated = false; if (Math.random() < probability) { revalidated = true; const a = async () => { const newResponse = await this.getFromOrigin(key); if (newResponse) { await cache.put(cacheKey, newResponse.res); } else { await cache.delete(cacheKey); } }; this.waitUntil(a()); } const rawMeta = bodyVal.headers.get("betterkv-internal-meta"); const metadata = rawMeta ? JSON.parse(rawMeta) : null; switch (type) { case "json": { return { value: await bodyVal.json(), metadata, cacheStatus: revalidated ? "REVALIDATED" : "HIT" }; } case "arrayBuffer": { return { value: await bodyVal.arrayBuffer(), metadata, cacheStatus: revalidated ? "REVALIDATED" : "HIT" }; } case "stream": { return { value: bodyVal.body, metadata, cacheStatus: revalidated ? "REVALIDATED" : "HIT" }; } default: { return { value: await bodyVal.text(), metadata, cacheStatus: revalidated ? "REVALIDATED" : "HIT" }; } } } const originResponse = await this.getFromOrigin(key); if (!originResponse) return null; this.waitUntil(cache.put(cacheKey, originResponse.res.clone())); switch (type) { case "json": { return { value: await originResponse.res.json(), metadata: originResponse.meta, cacheStatus: "MISS" }; } case "arrayBuffer": { return { value: await originResponse.res.arrayBuffer(), metadata: originResponse.meta, cacheStatus: "MISS" }; } case "stream": { return { value: originResponse.res.body, metadata: originResponse.meta, cacheStatus: "MISS" }; } default: { return { value: await originResponse.res.text(), metadata: originResponse.meta, cacheStatus: "MISS" }; } } } /** * Adds a new value to the BetterKV Namespace. Supports CacheTtl. * @param {string} key The key to add. * @param {BetterKVValueOptions} val The value to add. Type is inferred from the value. * @param {BetterKVAddOptions} options Options for the addition. * @example ```ts * await NAMESPACE.put(key, value); * ``` */ async put(key, val, options) { const cache = await this.getCache(); const cacheKey = this.URL + key; let cacheVal; let originVal; if (val instanceof ReadableStream) { const teed = val.tee(); cacheVal = teed[0]; originVal = teed[1]; } else { cacheVal = originVal = val; } this.waitUntil( cache.put( cacheKey, new Response(cacheVal, { headers: { "cloudflare-cdn-cache-control": `max-age=${this.config.cacheTtl}`, "betterkv-internal-created": Date.now().toString(), "betterkv-internal-meta": options?.metadata ? JSON.stringify(options.metadata) : "{}" } }) ) ); await this.KV.put(key, originVal, options); } /** * Removes a value from the BetterKV Namespace. * @param {string} key The key to remove. * @example ```ts * await NAMESPACE.delete(key); * ``` */ async delete(key) { const cache = await this.getCache(); this.waitUntil(cache.delete(this.URL + key)); await this.KV.delete(key); } /** * Lists keys in the BetterKV Namespace according to the options given. Supports CacheTtl. * @template M The type of the metadata. * @param {KVNamespaceListOptions} [opts] Options for the listing. * @returns {Promise<BetterKVListReturns<M>>} The keys in the namespace, and their associated metadata(if any). * @example ```ts * const {keys, list_complete, cursor} = await NAMESPACE.list(); * ``` */ async list(opts) { const cache = await this.getCache(); const cacheKey = new URL("https://list.better.kv"); let limit = 1e3; let prefix = null; let cursor = null; if (opts) { if (opts.limit && opts.limit >= 1 && opts.limit < 1e3) { limit = opts.limit; } if (opts.prefix) { prefix = opts.prefix; cacheKey.searchParams.set("prefix", prefix); } if (opts.limit) { limit = opts.limit; cacheKey.searchParams.set("limit", limit.toString()); } if (opts.cursor) { cursor = opts.cursor; cacheKey.searchParams.append("cursor", cursor); } } const bodyVal = await cache.match(cacheKey.toString()); if (bodyVal) { const created = Number(bodyVal.headers.get("betterkv-internal-created")); const probability = Number.isNaN(created) ? 1 : this.config.probabilityGrowth ** (Date.now() - created - this.config.cacheTtl); let revalidated = false; if (Math.random() < probability) { revalidated = true; const a = async () => { const newResponse = await this.KV.list({ prefix, limit, cursor }); if (newResponse) { await cache.put( cacheKey, new Response(JSON.stringify(newResponse), { headers: { "cloudflare-cdn-cache-control": `max-age=${this.config.cacheTtl}` } }) ); } else { await cache.delete(cacheKey); } }; this.waitUntil(a()); } const res = await bodyVal.json(); return { ...res, cacheStatus: revalidated ? "REVALIDATED" : "HIT" }; } const result = await this.KV.list({ prefix, limit, cursor }); this.waitUntil( cache.put( cacheKey.toString(), new Response(JSON.stringify(result), { headers: { "cloudflare-cdn-cache-control": `max-age=${this.config.cacheTtl}` } }) ) ); return { ...result, cacheStatus: "MISS" }; } }; // src/Temra/index.ts var deleteElement = { element: (e) => { e.remove(); } }; var Temra = class { /** * Initializes a new Temra instance. * @param {string} prefix Default Prefix used for tag names. For example, if the prefix is "Temra", then the tag name "TemraName" will be read as "Name". * @param {boolean} deleteComments Whether comments should be removed. */ constructor(prefix, deleteComments) { /** * Default Prefix used for tag names. */ __publicField(this, "tagPrefix"); /** * Whether comments should be removed. */ __publicField(this, "deleteComments"); /** * Replacers applied to your HTML. */ __publicField(this, "replacers", {}); this.tagPrefix = prefix || ""; this.deleteComments = deleteComments || false; } /** * Add a replacer to the Temra instance. * @param {string} selector Selector used to find elements. * @param {AddOptions} options Options used to configure the replacer. * @returns {Temra} Returns the Temra instance, for chaining. * @example ```ts * temra.add("username", () => "Jay Doe", {removeSelector: true}); * ``` */ add(selector, options) { const replacer = { type: options.selectorType ? options.selectorType : "className" }; if (options.replacerFunction) { replacer.replacerFunction = options.replacerFunction; } if (options.removeSelector) replacer.removeSelector = true; switch (options.selectorType) { case "universal": this.replacers[selector] = replacer; break; case "tagName": this.replacers[`${this.tagPrefix}${selector}`] = replacer; break; case "className": this.replacers[`*.${selector}`] = replacer; break; case "id": this.replacers[`*#${selector}`] = replacer; break; case "attribute": this.replacers[`*[${selector}${options.attributeValue}]`] = replacer; break; default: throw new Error(`Unknown selector type: ${options.selectorType}`); } return this; } /** * Applies the currently added replacer functions to the provided HTML. * @param {Response} body HTML response that will be transformed. * @param {TransformationOptionals<E, O>} [optionals] Optional parameters to pass to the replacer function. * @returns Response * @example ```ts * return temra.transform(await fetch(req), {req, env}); * ``` */ transform(body, optionals) { const rewriter = new HTMLRewriter(); for (const [selector, replacer] of Object.entries(this.replacers)) { if (replacer.delete) { rewriter.on(selector, deleteElement); continue; } const ElementHandler = {}; switch (replacer.type) { case "universal": case "tagName": ElementHandler.element = async (element) => { if (replacer.replacerFunction) { const content = await replacer.replacerFunction( element, optionals ); element.replace(content, { html: replacer.isHTML }); } else { element.remove(); } }; break; case "attribute": ElementHandler.element = async (element) => { if (replacer.replacerFunction) { const content = await replacer.replacerFunction( element, optionals ); element.replace(content, { html: replacer.isHTML }); if (replacer.removeSelector) element.removeAttribute(selector); } else { element.remove(); } }; break; case "className": ElementHandler.element = async (element) => { if (replacer.replacerFunction) { const content = await replacer.replacerFunction( element, optionals ); element.replace(content, { html: replacer.isHTML }); if (replacer.removeSelector) { const allClasses = element.getAttribute("class"); element.setAttribute( "class", allClasses.split(" ").filter((e) => e !== selector).join(" ") ); } } else { element.remove(); } }; break; case "id": ElementHandler.element = async (element) => { if (replacer.replacerFunction) { const content = await replacer.replacerFunction( element, optionals ); element.replace(content, { html: replacer.isHTML }); if (replacer.removeSelector) element.removeAttribute("id"); } else { element.remove(); } }; break; } rewriter.on(selector, ElementHandler); } if (this.deleteComments) rewriter.onDocument({ comments: (c) => { c.remove(); } }); return rewriter.transform(body); } }; // src/MailChannels.ts async function sendMail(opts) { return fetch("https://api.mailchannels.net/tx/v1/send", { headers: { "content-type": "application/json", accept: "application/json" }, method: "POST", body: JSON.stringify(opts) }); } // src/Stubby.ts var Stubby = class { /** * Constructs a Stubby Instance. * @param {DurableObjectNamespace} ns Durable Object Namespace utilized for stubs. * @param {KVNamespace} kv KV Namespace used to store raw DO IDs. * @param {ExecutionContext["waitUntil"]} waitUntil Used to unblock thread, ensuring KV writes are completed without pausing execution of the rest of the script. * @param {string} prefix Used to prefix KV writes, allowing multiple systems to share one KV namespaces without collisions. */ constructor(ns, kv, waitUntil, prefix) { /** * Durable Object Namespace, to which the stubs pertain. */ __publicField(this, "ns"); /** * KV Namespace used to globally cache DO Stub IDs. */ __publicField(this, "kv"); /** * Used to ensure Cache Operations are executed while not blocking script execution. */ __publicField(this, "waitUntil"); /** * Prefix used in KV operations, allows for multiple projects to share one KV namespace. Should be unique. */ __publicField(this, "prefix"); /** * In memory DO Stub Cache. */ __publicField(this, "stubMap", /* @__PURE__ */ new Map()); this.ns = ns; this.kv = kv; this.waitUntil = waitUntil; this.prefix = prefix || ""; } /** * Returns a stub from memory or KV, or generates a new stub if not available. Caches returned stubs to ensure quickest recall. * @param {string} key Used to identify stub for retrieval or creation. * @param {string} [locationHint] Used to hint at the location of the stub, allowing for faster stub creation. Available hints are available on the [DO Docs](https://developers.cloudflare.com/workers/runtime-apis/durable-objects/#providing-a-location-hint). * @returns {Promise<DurableObjectStub>} */ async get(key, locationHint) { let stub = this.stubMap.get(key); if (stub) return stub; const idString = await this.kv.get(this.prefix + key); if (idString) { stub = this.ns.get(this.ns.idFromString(idString)); this.stubMap.set(key, stub); return stub; } const id = this.ns.newUniqueId(); this.waitUntil(this.kv.put(this.prefix + key, id.toString())); stub = this.ns.get(id, { locationHint }); this.stubMap.set(key, stub); return stub; } /** * Removes a singular stub from local and KV storage. Note that this operation is irreversable, and stubs will not be recoverable unless stored elsewhere. * @param {string} key Used to identify stub to remove. * @returns {Promise<void>} */ async remove(key) { this.stubMap.delete(key); await this.kv.delete(key); } /** * Clears all stubs belonging to this Stubby instance. * @returns {Promise<void>} */ async clearAll() { const keys = []; let cursor = ""; for (; ; ) { const listRet = await this.kv.list({ prefix: this.prefix, cursor }); for (const key of listRet.keys) keys.push(this.kv.delete(key.name)); if (listRet.list_complete) { break; } cursor = listRet.cursor; } await Promise.allSettled(keys); this.stubMap.clear(); } }; // src/PromiseQueue.ts var CONCURRENCY = 6; var PromiseQueue = class { constructor() { __publicField(this, "queue"); this.queue = []; } /** * Add a promise to the queue. Always await this function. * @param promise The promise to add to the queue. */ async add(promise) { this.queue.push(promise); if (this.queue.length === CONCURRENCY) { const completed = await Promise.race( this.queue.map(async (e, i) => { await e; return i; }) ); this.queue = this.queue.splice(completed, 1); } } /** * Flush the queue. Always await this function. * @note Run this function when all tasks have been added to the queue. */ async flush() { await Promise.allSettled(this.queue); this.queue = []; } }; // src/Resizer.ts import { nanoid } from "nanoid"; var inputFormats = [ "image/jpeg", "image/png", "image/gif", "image/webp", "image/svg+xml" ]; async function resizeImage(image, config) { if (!inputFormats.includes(config.contentType)) { throw new Error("Invalid content type"); } const bucket = config.storage.bucket; const key = config.storage.prefix + nanoid(); await bucket.put(key, image, { httpMetadata: { contentType: config.contentType } }); const resizedImage = await fetch(`${config.storage.url}/${key}`, { cf: { image: config.transformOpts } }); if (!(resizedImage.ok && resizedImage.body)) { throw { status: resizedImage.status, message: resizedImage.statusText }; } await bucket.delete(key); if (config.returnType === "buffer") { return resizedImage.arrayBuffer(); } return resizedImage.body; } export { BetterKV, BetterKVOld, Isaac, Phonetic, PromiseQueue, Stubby, Temra, resizeImage, sendMail }; //# sourceMappingURL=index.js.map