@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.
249 lines • 8.22 kB
JavaScript
/**
* @module Lock
*/
import { MysqlAdapter } from "kysely";
import { TimeSpan, } from "../../../../utilities/_module-exports.js";
/**
* To utilize the `KyselyLockAdapter`, you must install the [`"kysely"`](https://www.npmjs.com/package/kysely) package and configure a `Kysely` class instance.
*
* Note in order to use `KyselyLockAdapter` correctly, ensure you use a single, consistent database across all server instances.
* The adapter have been tested with `sqlite`, `postgres` and `mysql` databases.
*
* IMPORT_PATH: `"@daiso-tech/core/lock/adapters"`
* @group Adapters
*/
export class KyselyLockAdapter {
kysely;
expiredKeysRemovalInterval;
shouldRemoveExpiredKeys;
timeoutId = null;
currentDate;
setInterval;
isMysql;
/**
* @example
* ```ts
* import { KyselyLockAdapter } from "@daiso-tech/core/lock/adapters";
* import Sqlite from "better-sqlite3";
*
* const lockAdapter = new KyselyLockAdapter({
* kysely: new Kysely({
* dialect: new SqliteDialect({
* database: new Sqlite("local.db"),
* }),
* }),
* });
* // You need initialize the adapter once before using it.
* await lockAdapter.init();
* ```
*/
constructor(settings) {
const { kysely, expiredKeysRemovalInterval = TimeSpan.fromMinutes(1), shouldRemoveExpiredKeys = true, currentDate = () => new Date(), setInterval = globalThis.setInterval, } = settings;
this.expiredKeysRemovalInterval = expiredKeysRemovalInterval;
this.shouldRemoveExpiredKeys = shouldRemoveExpiredKeys;
this.kysely = kysely;
this.isMysql =
this.kysely.getExecutor().adapter instanceof MysqlAdapter;
this.currentDate = currentDate;
this.setInterval = setInterval;
}
async deInit() {
if (this.shouldRemoveExpiredKeys && this.timeoutId !== null) {
clearTimeout(this.timeoutId);
}
// Should throw if the index does not exists thats why the try catch is used.
try {
await this.kysely.schema
.dropIndex("lock_expiration")
.on("lock")
.execute();
}
catch {
/* EMPTY */
}
// Should throw if the table does not exists thats why the try catch is used.
try {
await this.kysely.schema.dropTable("lock").execute();
}
catch {
/* EMPTY */
}
}
async init() {
// Should throw if the table already exists thats why the try catch is used.
try {
await this.kysely.schema
.createTable("lock")
.addColumn("key", "varchar(255)", (col) => col.notNull().primaryKey())
.addColumn("owner", "varchar(255)", (col) => col.notNull())
.addColumn("expiration", "bigint")
.execute();
}
catch {
/* EMPTY */
}
// Should throw if the index already exists thats why the try catch is used.
try {
await this.kysely.schema
.createIndex("lock_expiration")
.on("lock")
.columns(["expiration"])
.execute();
}
catch {
/* EMPTY */
}
if (this.shouldRemoveExpiredKeys) {
this.timeoutId = this.setInterval(() => {
void this.removeAllExpired();
}, this.expiredKeysRemovalInterval.toMilliseconds());
}
}
async removeAllExpired() {
await this.kysely
.deleteFrom("lock")
.where("lock.expiration", "<=", this.currentDate().getTime())
.execute();
}
async insert(key, owner, expiration) {
await this.kysely
.insertInto("lock")
.values({
key,
owner,
expiration: expiration?.getTime() ?? null,
})
.execute();
}
async updateIfExpired(key, owner, expiration) {
const result = await this.kysely
.updateTable("lock")
.where("lock.key", "=", key)
.where((eb) => {
const hasExpiration = eb("lock.expiration", "is not", null);
const hasExpired = eb("lock.expiration", "<=", Date.now());
return eb.and([hasExpiration, hasExpired]);
})
.set({
owner,
expiration: expiration?.getTime() ?? null,
})
.executeTakeFirst();
return Number(result.numUpdatedRows);
}
async remove(key) {
let result;
if (this.isMysql) {
result = await this.kysely
.transaction()
.setIsolationLevel("serializable")
.execute(async (trx) => {
const row = await trx
.selectFrom("lock")
.select("lock.expiration")
.where("lock.key", "=", key)
.executeTakeFirst();
await trx
.deleteFrom("lock")
.where("lock.key", "=", key)
.executeTakeFirst();
return row;
});
}
else {
result = await this.kysely
.deleteFrom("lock")
.where("lock.key", "=", key)
.returning(["lock.expiration"])
.executeTakeFirst();
}
if (result === undefined) {
return null;
}
if (result.expiration === null) {
return {
expiration: null,
};
}
return {
expiration: new Date(Number(result.expiration)),
};
}
async removeIfOwner(key, owner) {
let row;
if (this.isMysql) {
row = await this.kysely
.transaction()
.setIsolationLevel("serializable")
.execute(async (trx) => {
const row = await trx
.selectFrom("lock")
.where("lock.key", "=", key)
.where("lock.owner", "=", owner)
.select(["lock.expiration", "lock.owner"])
.executeTakeFirst();
await trx
.deleteFrom("lock")
.where("lock.key", "=", key)
.where("lock.owner", "=", owner)
.execute();
return row;
});
}
else {
row = await this.kysely
.deleteFrom("lock")
.where("lock.key", "=", key)
.where("lock.owner", "=", owner)
.returning(["lock.expiration", "lock.owner"])
.executeTakeFirst();
}
if (row === undefined) {
return null;
}
const { expiration } = row;
if (expiration === null) {
return {
owner: row.owner,
expiration: null,
};
}
return {
owner: row.owner,
expiration: new Date(Number(expiration)),
};
}
async updateExpirationIfOwner(key, owner, expiration) {
const result = await this.kysely
.updateTable("lock")
.where("lock.key", "=", key)
.where("lock.owner", "=", owner)
.set({
expiration: expiration.getTime(),
})
.executeTakeFirst();
return Number(result.numUpdatedRows);
}
async find(key) {
const row = await this.kysely
.selectFrom("lock")
.select(["lock.owner", "lock.expiration"])
.where("lock.key", "=", key)
.executeTakeFirst();
if (row === undefined) {
return null;
}
const { owner, expiration } = row;
if (expiration === null) {
return {
owner,
expiration: null,
};
}
return {
owner,
expiration: new Date(Number(expiration)),
};
}
}
//# sourceMappingURL=kysely-lock-adapter.js.map