From 67a49305ac711f6ae38b06e8d737777a241fcab8 Mon Sep 17 00:00:00 2001 From: Rizky Date: Fri, 19 Jul 2024 20:59:02 +0700 Subject: [PATCH] fix --- bun.lockb | Bin 117912 -> 117484 bytes package.json | 1 - pkgs/utils/global.ts | 5 +- pkgs/utils/kv.ts | 430 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 432 insertions(+), 4 deletions(-) create mode 100644 pkgs/utils/kv.ts diff --git a/bun.lockb b/bun.lockb index ba9c6deb7a0c80a88ab64ff73ee3fd0b81c2b974..446281478e404556fb224f8baaf473f865e1a88e 100755 GIT binary patch delta 18183 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 E9}fcmEC2ui delta 18474 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 C<|5z# diff --git a/package.json b/package.json index 9cb8d28..4ce5398 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,6 @@ }, "dependencies": { "brotli-wasm": "^2.0.1", - "bun-sqlite-key-value": "^1.4.5", "exit-hook": "^4.0.0", "firebase-admin": "^12.2.0", "prisma": "^5.17.0" diff --git a/pkgs/utils/global.ts b/pkgs/utils/global.ts index 6bc52c8..49c3976 100644 --- a/pkgs/utils/global.ts +++ b/pkgs/utils/global.ts @@ -3,10 +3,9 @@ import { Logger } from "pino"; import { RadixRouter } from "radix3"; import { PrismaClient } from "../../app/db/db"; -import admin from "firebase-admin"; import { Database } from "bun:sqlite"; -import { prodIndex } from "./prod-index"; -import { BunSqliteKeyValue } from "bun-sqlite-key-value"; +import admin from "firebase-admin"; +import { BunSqliteKeyValue } from "./kv"; type SingleRoute = { url: string; diff --git a/pkgs/utils/kv.ts b/pkgs/utils/kv.ts new file mode 100644 index 0000000..6283d1b --- /dev/null +++ b/pkgs/utils/kv.ts @@ -0,0 +1,430 @@ +import { Database, type Statement } from "bun:sqlite"; +import { serialize, deserialize } from "node:v8"; +import { dirname, resolve } from "node:path"; +import { existsSync, mkdirSync } from "node:fs"; + +export interface Item { + key: string; + value: T | undefined; +} + +interface Record { + key: string; + value: Buffer | null; + expires: number | null; +} + +interface Options { + readonly?: boolean; + create?: boolean; // Defaults to true + readwrite?: boolean; // Defaults to true + ttlMs?: number; // Default TTL milliseconds +} + +interface DbOptions extends Omit { + strict: boolean; +} + +const MIN_UTF8_CHAR: string = String.fromCodePoint(1); +const MAX_UTF8_CHAR: string = String.fromCodePoint(1_114_111); + +export class BunSqliteKeyValue { + db: Database; + ttlMs: number | undefined; + + private deleteExpiredStatement: Statement; + private deleteStatement: Statement; + private clearStatement: Statement; + private countStatement: Statement<{ count: number }>; + private setItemStatement: Statement; + private getItemStatement: Statement>; + private getAllItemsStatement: Statement; + private getItemsStartsWithStatement: Statement; + private getKeyStatement: Statement>; + private getAllKeysStatement: Statement>; + private getKeysStartsWithStatement: Statement>; + private countExpiringStatement: Statement<{ count: number }>; + private deleteExpiringStatement: Statement; + + // - `filename`: The full path of the SQLite database to open. + // Pass an empty string (`""`) or `":memory:"` or undefined for an in-memory database. + // - `options`: + // - ... + // - `ttlMs?: boolean`: Standard time period in milliseconds before + // an entry written to the DB becomes invalid. + constructor(filename?: string, options?: Options) { + // Parse options + const { ttlMs, ...otherOptions } = options ?? {}; + this.ttlMs = ttlMs; + const dbOptions: DbOptions = { + ...otherOptions, + strict: true, + readwrite: otherOptions?.readwrite ?? true, + create: otherOptions?.create ?? true, + }; + + // Create database directory + if ( + filename?.length && + filename.toLowerCase() !== ":memory:" && + dbOptions.create + ) { + const dbDir = dirname(resolve(filename)); + if (!existsSync(dbDir)) { + console.log(`The "${dbDir}" folder is created.`); + mkdirSync(dbDir, { recursive: true }); + } + } + + // Open database + this.db = new Database(filename, dbOptions); + this.db.run("PRAGMA journal_mode = WAL"); + + // Create table and indexes + this.db.run( + "CREATE TABLE IF NOT EXISTS items (key TEXT PRIMARY KEY, value BLOB, expires INT)" + ); + this.db.run( + "CREATE UNIQUE INDEX IF NOT EXISTS ix_items_key ON items (key)" + ); + this.db.run( + "CREATE INDEX IF NOT EXISTS ix_items_expires ON items (expires)" + ); + + // Prepare and cache statements + this.clearStatement = this.db.query("DELETE FROM items"); + this.deleteStatement = this.db.query("DELETE FROM items WHERE key = $key"); + this.deleteExpiredStatement = this.db.query( + "DELETE FROM items WHERE expires < $now" + ); + + this.setItemStatement = this.db.query( + "INSERT OR REPLACE INTO items (key, value, expires) VALUES ($key, $value, $expires)" + ); + this.countStatement = this.db.query("SELECT COUNT(*) AS count FROM items"); + + this.getAllItemsStatement = this.db.query( + "SELECT key, value, expires FROM items" + ); + this.getItemStatement = this.db.query( + "SELECT value, expires FROM items WHERE key = $key" + ); + this.getItemsStartsWithStatement = this.db.query( + "SELECT key, value, expires FROM items WHERE key = $key OR key >= $gte AND key < $lt" + ); + + this.getAllKeysStatement = this.db.query("SELECT key, expires FROM items"); + this.getKeyStatement = this.db.query( + "SELECT expires FROM items WHERE key = $key" + ); + this.getKeysStartsWithStatement = this.db.query( + "SELECT key, expires FROM items WHERE (key = $key OR key >= $gte AND key < $lt)" + ); + + this.countExpiringStatement = this.db.query( + "SELECT COUNT(*) as count FROM items WHERE expires IS NOT NULL" + ); + this.deleteExpiringStatement = this.db.query(` + DELETE FROM items + WHERE key IN ( + SELECT key FROM items + WHERE expires IS NOT NULL + ORDER BY expires ASC + LIMIT $limit + )`); + + // Delete expired items + this.deleteExpired(); + } + + // Delete all expired records + deleteExpired() { + this.deleteExpiredStatement.run({ now: Date.now() }); + } + + // Delete one or multiple items + delete(keyOrKeys?: string | string[]) { + if (typeof keyOrKeys === "string") { + // Delete one + this.deleteStatement.run({ key: keyOrKeys }); + } else if (keyOrKeys?.length) { + // Delete multiple items + this.db.transaction(() => { + keyOrKeys.forEach((key) => { + this.deleteStatement.run({ key }); + }); + })(); + } else { + // Delete all + this.clearStatement.run(); + } + } + + // Delete all items + clear() { + this.delete(); + } + + // Explicitly close database + // Removes .sqlite-shm and .sqlite-wal files + close() { + this.db.close(); + } + + // Returns the number of all items, including those that have already expired. + // First delete the expired items with `deleteExpired()` + // if you want to get the number of items that have not yet expired. + getCount(): number { + return (this.countStatement.get() as { count: number }).count; + } + + // Getter for getCount() + get length() { + return this.getCount(); + } + + // @param ttlMs: + // Time to live in milliseconds. + // Set ttlMs to 0 if you explicitly want to disable expiration. + set(key: string, value: T, ttlMs?: number) { + let expires: number | undefined; + ttlMs = ttlMs ?? this.ttlMs; + if (ttlMs !== undefined && ttlMs > 0) { + expires = Date.now() + ttlMs; + } + this.setItemStatement.run({ key, value: serialize(value), expires }); + } + + // Alias for `set` + setValue = this.set; + put = this.set; + + // Adds a large number of entries to the database and takes only + // a small fraction of the time that `set()` would take individually. + setItems(items: { key: string; value: T; ttlMs?: number }[]) { + this.db.transaction(() => { + items.forEach(({ key, value, ttlMs }) => { + this.set(key, value, ttlMs); + }); + })(); + } + + // Get one value + get(key: string): T | undefined { + const record = this.getItemStatement.get({ key }); + if (!record) return; + const { value, expires } = record; + if (expires) { + if (expires < Date.now()) { + this.delete(key); + return; + } + } + return value ? (deserialize(value) as T) : undefined; + } + + // Alias for `get` + getValue = this.get; + + // Get one item (key, value) + getItem(key: string): Item | undefined { + return { + key, + value: this.get(key), + }; + } + + // Get multiple items (key-value array) + getItems( + startsWithOrKeys?: string | string[] + ): Item[] | undefined { + let records: Record[]; + if (startsWithOrKeys && typeof startsWithOrKeys === "string") { + // Filtered items (startsWith) + // key = "addresses:" + // gte = key + MIN_UTF8_CHAR + // "addresses:aaa" + // "addresses:xxx" + // lt = key + MAX_UTF8_CHAR + const key: string = startsWithOrKeys; + const gte: string = key + MIN_UTF8_CHAR; + const lt: string = key + MAX_UTF8_CHAR; + records = this.getItemsStartsWithStatement.all({ key, gte, lt }); + } else if (startsWithOrKeys) { + // Filtered items (array with keys) + records = this.db.transaction(() => { + return (startsWithOrKeys as string[]).map((key: string) => { + const record = this.getItemStatement.get({ key }); + return { ...record, key }; + }); + })(); + } else { + // All items + records = this.getAllItemsStatement.all(); + } + if (!records.length) return; + const now = Date.now(); + const result: Item[] = []; + for (const record of records) { + const { key, value, expires } = record; + if (expires && expires < now) { + this.delete(key); + } else { + result.push({ + key, + value: value ? (deserialize(value) as T) : undefined, + }); + } + } + if (result.length) { + return result; + } + } + + // Alias for getItems + getItemsArray = this.getItems; + + // Get multiple values as array + getValues( + startsWithOrKeys?: string | string[] + ): (T | undefined)[] | undefined { + return this.getItems(startsWithOrKeys)?.map((result) => result.value); + } + + // Alias for getValues + getValuesArray = this.getValues; + + // Get multiple items as object + getItemsObject( + startsWithOrKeys?: string | string[] + ): { [key: string]: T | undefined } | undefined { + const items = this.getItems(startsWithOrKeys); + if (!items) return; + return Object.fromEntries( + items.map((item) => [item.key, item.value as T | undefined]) + ); + } + + // Get multiple items as Map() + getItemsMap( + startsWithOrKeys?: string | string[] + ): Map | undefined { + const items = this.getItems(startsWithOrKeys); + if (!items) return; + return new Map( + items.map((item) => [item.key, item.value as T | undefined]) + ); + } + + // Get multiple values as Set() + getValuesSet( + startsWithOrKeys?: string | string[] + ): Set | undefined { + const values = this.getValues(startsWithOrKeys); + if (!values) return; + return new Set(values); + } + + // Checks if key exists + has(key: string): boolean { + const record = this.getKeyStatement.get({ key }); + if (!record) return false; + if (record.expires) { + if (record.expires < Date.now()) { + this.delete(key); + return false; + } + } + return true; + } + + // Get multiple keys as array + getKeys(startsWithOrKeys?: string | string[]): string[] | undefined { + let records: (Omit | undefined)[]; + if (startsWithOrKeys && typeof startsWithOrKeys === "string") { + const key: string = startsWithOrKeys; + const gte: string = key + MIN_UTF8_CHAR; + const lt: string = key + MAX_UTF8_CHAR; + records = this.getKeysStartsWithStatement.all({ key, gte, lt }); + } else if (startsWithOrKeys) { + // Filtered items (array with keys) + records = this.db.transaction(() => { + return (startsWithOrKeys as string[]).map((key: string) => { + const record = this.getKeyStatement.get({ key }); + return record ? { ...record, key } : undefined; + }); + })(); + } else { + // All items + records = this.getAllKeysStatement.all(); + } + if (!records?.length) return; + const now = Date.now(); + const result: string[] = []; + for (const record of records) { + if (!record) continue; + const { key, expires } = record; + if (expires && expires < now) { + this.delete(key); + } else { + result.push(key); + } + } + if (result.length) { + return result; + } + } + + getExpiringItemsCount() { + return this.countExpiringStatement.get()!.count; + } + + // If there are more expiring items in the database than `maxExpiringItemsInDb`, + // the oldest items are deleted until there are only `maxExpiringItemsInDb` items with + // an expiration date in the database. + deleteOldExpiringItems(maxExpiringItemsInDb: number) { + const count = this.getExpiringItemsCount(); + if (count <= maxExpiringItemsInDb) return; + + const limit = count - maxExpiringItemsInDb; + this.deleteExpiringStatement.run({ limit }); + } + + // Alias for deleteOldExpiringItems + deleteOldestExpiringItems = this.deleteOldExpiringItems; + + // Proxy + getDataObject(): { [key: string]: any } { + const self = this; + return new Proxy( + {}, + { + get(target, property: string, receiver) { + if (property === "length") { + return self.length; + } else { + return self.get(property); + } + }, + + set(target, property: string, value: any) { + self.set(property, value); + return true; + }, + + has(target, property: string) { + return self.has(property); + }, + + deleteProperty(target: {}, property: string) { + self.delete(property); + return true; + }, + } + ); + } + + get dataObject() { + return this.getDataObject(); + } +}