UNPKG

@daiso-tech/core

Version:

The library offers flexible, framework-agnostic solutions for modern web applications, built on adaptable components that integrate seamlessly with popular frameworks like Next Js.

224 lines 8.16 kB
/** * @module Cache */ import { TypeCacheError, } from "../../../../cache/contracts/_module-exports.js"; import { TimeSpan } from "../../../../utilities/_module-exports.js"; /** * @internal */ export class KyselyCacheAdapter { static filterUnexpiredKeys(keys) { return (eb) => { const hasNoExpiration = eb("cache.expiration", "is", null); const hasExpiration = eb("cache.expiration", "is not", null); const hasNotExpired = eb("cache.expiration", ">", new Date().getTime()); const keysMatch = eb("cache.key", "in", keys); return eb.and([ keysMatch, eb.or([ hasNoExpiration, eb.and([hasExpiration, hasNotExpired]), ]), ]); }; } static filterExpiredKeys(keys) { return (eb) => { const keysMatch = eb("cache.key", "in", keys); const hasExpiration = eb("cache.expiration", "is not", null); const hasExpired = eb("cache.expiration", "<=", new Date().getTime()); return eb.and([keysMatch, hasExpiration, hasExpired]); }; } serde; database; shouldRemoveExpiredKeys; expiredKeysRemovalInterval; timeoutId = null; disableTransaction; constructor(settings) { const { database, serde, disableTransaction = false, expiredKeysRemovalInterval = TimeSpan.fromMinutes(1), shouldRemoveExpiredKeys = true, } = settings; this.disableTransaction = disableTransaction; this.database = database; this.serde = serde; this.expiredKeysRemovalInterval = expiredKeysRemovalInterval; this.shouldRemoveExpiredKeys = shouldRemoveExpiredKeys; } async removeAllExpired() { await this.database .deleteFrom("cache") .where("cache.expiration", "<=", new Date().getTime()) .execute(); } async init() { await this.database.schema .createTable("cache") .ifNotExists() .addColumn("key", "text", (col) => col.primaryKey()) .addColumn("value", "text") .addColumn("expiration", "integer") .execute(); await this.database.schema .createIndex("cache_expiration") .ifNotExists() .on("cache") .columns(["expiration"]) .execute(); if (this.shouldRemoveExpiredKeys && this.timeoutId === null) { this.timeoutId = setTimeout(() => { // eslint-disable-next-line @typescript-eslint/no-floating-promises this.removeAllExpired(); }, this.expiredKeysRemovalInterval.toMilliseconds()); } } async deInit() { if (this.shouldRemoveExpiredKeys && this.timeoutId !== null) { clearTimeout(this.timeoutId); } await this.database.schema .dropIndex("cache_expiration") .ifExists() .on("cache") .execute(); await this.database.schema.dropTable("cache").ifExists().execute(); } async find(key) { const row = await this.database .selectFrom("cache") .where("cache.key", "=", key) .select(["cache.expiration", "cache.value"]) .executeTakeFirst(); if (row === undefined) { return null; } return { value: this.serde.deserialize(row.value), expiration: row.expiration ? new Date(row.expiration) : null, }; } async insert(data) { await this.database .insertInto("cache") .values({ key: data.key, value: this.serde.serialize(data.value), expiration: data.expiration?.getTime() ?? null, }) .executeTakeFirst(); } async transaction(fn) { if (this.disableTransaction) { return fn(this.database); } return await this.database.transaction().execute(fn); } async upsert(data) { const expiration = data.expiration?.getTime() ?? null; return await this.transaction(async (trx) => { const prevRow = await trx .selectFrom("cache") .select(["cache.key", "cache.value", "cache.expiration"]) .where(KyselyCacheAdapter.filterUnexpiredKeys([data.key])) .executeTakeFirst(); try { await trx .insertInto("cache") .values({ key: data.key, value: this.serde.serialize(data.value), expiration, }) .executeTakeFirst(); } catch { await trx .updateTable("cache") .where("cache.key", "=", data.key) .set({ value: this.serde.serialize(data.value), expiration, }) .executeTakeFirst(); } if (prevRow === undefined) { return null; } return { value: this.serde.deserialize(prevRow.value), expiration: prevRow.expiration ? new Date(prevRow.expiration) : null, }; }); } async updateExpired(data) { const updateResult = await this.database .updateTable("cache") .where(KyselyCacheAdapter.filterExpiredKeys([data.key])) .set({ value: this.serde.serialize(data.value), expiration: data.expiration?.getTime() ?? null, }) .executeTakeFirst(); return Number(updateResult.numUpdatedRows); } async updateUnexpired(data) { const updateResult = await this.database .updateTable("cache") .where(KyselyCacheAdapter.filterUnexpiredKeys([data.key])) .set({ value: this.serde.serialize(data.value), }) .executeTakeFirst(); return Number(updateResult.numUpdatedRows); } async incrementUnexpired(data) { return await this.transaction(async (trx) => { const row = await trx .selectFrom("cache") .select(["cache.value"]) .where(KyselyCacheAdapter.filterUnexpiredKeys([data.key])) .executeTakeFirst(); if (row === undefined) { return 0; } const { value: serializedValue } = row; const value = this.serde.deserialize(serializedValue); if (typeof value !== "number") { throw new TypeCacheError(`Unable to increment or decrement none number type key "${data.key}"`); } const updateResult = await trx .updateTable("cache") .where(KyselyCacheAdapter.filterUnexpiredKeys([data.key])) .set({ value: this.serde.serialize(value + data.value), }) .executeTakeFirst(); return Number(updateResult.numUpdatedRows); }); } async removeExpiredMany(keys) { const deleteResult = await this.database .deleteFrom("cache") .where(KyselyCacheAdapter.filterExpiredKeys(keys)) .executeTakeFirst(); return Number(deleteResult.numDeletedRows); } async removeUnexpiredMany(keys) { const deleteResult = await this.database .deleteFrom("cache") .where(KyselyCacheAdapter.filterUnexpiredKeys(keys)) .executeTakeFirst(); return Number(deleteResult.numDeletedRows); } async removeAll() { await this.database.deleteFrom("cache").execute(); } async removeByKeyPrefix(prefix) { await this.database .deleteFrom("cache") .where("cache.key", "like", `${prefix}%`) .execute(); } } //# sourceMappingURL=kysely-cache-adapter.js.map