mirror of
https://github.com/wukko/cobalt.git
synced 2025-02-08 19:56:34 +01:00
api: implement redis/memory store for cache
This commit is contained in:
parent
5e92b649a3
commit
18acad19b9
6 changed files with 199 additions and 0 deletions
|
@ -24,6 +24,7 @@
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/imputnet/cobalt#readme",
|
"homepage": "https://github.com/imputnet/cobalt#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@datastructures-js/priority-queue": "^6.3.1",
|
||||||
"@imput/version-info": "workspace:^",
|
"@imput/version-info": "workspace:^",
|
||||||
"content-disposition-header": "0.6.0",
|
"content-disposition-header": "0.6.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
|
|
39
api/src/store/base-store.js
Normal file
39
api/src/store/base-store.js
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
const _stores = new Set();
|
||||||
|
|
||||||
|
export class Store {
|
||||||
|
id;
|
||||||
|
|
||||||
|
constructor(name) {
|
||||||
|
name = name.toUpperCase();
|
||||||
|
|
||||||
|
if (_stores.has(name))
|
||||||
|
throw `${name} store already exists`;
|
||||||
|
_stores.add(name);
|
||||||
|
|
||||||
|
this.id = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _get(_key) { throw "needs implementation" }
|
||||||
|
async get(key) {
|
||||||
|
if (typeof key !== 'string') {
|
||||||
|
key = key.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
const val = await this._get(key);
|
||||||
|
if (val === null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _set(_key, _val, _exp_sec = -1) { throw "needs implementation" }
|
||||||
|
set(key, val, exp_sec = -1) {
|
||||||
|
if (typeof key !== 'string') {
|
||||||
|
key = key.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
exp_sec = Math.round(exp_sec);
|
||||||
|
|
||||||
|
return this._set(key, val, exp_sec);
|
||||||
|
}
|
||||||
|
};
|
72
api/src/store/memory-store.js
Normal file
72
api/src/store/memory-store.js
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
import { MinPriorityQueue } from '@datastructures-js/priority-queue'
|
||||||
|
import { Store } from './base-store.js';
|
||||||
|
|
||||||
|
// minimum delay between sweeps to avoid repeatedly
|
||||||
|
// sweeping entries close in proximity one by one.
|
||||||
|
const MIN_THRESHOLD_MS = 2500;
|
||||||
|
|
||||||
|
export default class MemoryStore extends Store {
|
||||||
|
#store = new Map();
|
||||||
|
#timeouts = new MinPriorityQueue/*<{ t: number, k: unknown }>*/((obj) => obj.t);
|
||||||
|
#nextSweep = { id: null, t: null };
|
||||||
|
|
||||||
|
constructor(name) {
|
||||||
|
super(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
async _get(key) {
|
||||||
|
const val = this.#store.get(key);
|
||||||
|
|
||||||
|
return val === undefined ? null : val;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _set(key, val, exp_sec = -1) {
|
||||||
|
if (this.#store.has(key)) {
|
||||||
|
this.#timeouts.remove(o => o.k === key);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exp_sec > 0) {
|
||||||
|
const exp = 1000 * exp_sec;
|
||||||
|
const timeout_at = +new Date() + exp;
|
||||||
|
|
||||||
|
this.#timeouts.enqueue({ k: key, t: timeout_at });
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#store.set(key, val);
|
||||||
|
this.#reschedule();
|
||||||
|
}
|
||||||
|
|
||||||
|
#reschedule() {
|
||||||
|
const current_time = new Date().getTime();
|
||||||
|
const time = this.#timeouts.front()?.t;
|
||||||
|
if (!time) {
|
||||||
|
return;
|
||||||
|
} else if (time < current_time) {
|
||||||
|
return this.#sweepNow();
|
||||||
|
}
|
||||||
|
|
||||||
|
const sweep = this.#nextSweep;
|
||||||
|
if (sweep.id === null || sweep.t > time) {
|
||||||
|
if (sweep.id) {
|
||||||
|
clearTimeout(sweep.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
sweep.t = time;
|
||||||
|
sweep.id = setTimeout(
|
||||||
|
() => this.#sweepNow(),
|
||||||
|
Math.max(MIN_THRESHOLD_MS, time - current_time)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#sweepNow() {
|
||||||
|
while (this.#timeouts.front()?.t < new Date().getTime()) {
|
||||||
|
const item = this.#timeouts.dequeue();
|
||||||
|
this.#store.delete(item.k);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#nextSweep.id = null;
|
||||||
|
this.#nextSweep.t = null;
|
||||||
|
this.#reschedule();
|
||||||
|
}
|
||||||
|
}
|
62
api/src/store/redis-store.js
Normal file
62
api/src/store/redis-store.js
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
import { commandOptions, createClient } from "redis";
|
||||||
|
import { env } from "../config.js";
|
||||||
|
import { Store } from "./base-store.js";
|
||||||
|
|
||||||
|
export default class RedisStore extends Store {
|
||||||
|
#client = createClient({
|
||||||
|
url: env.redisURL,
|
||||||
|
});
|
||||||
|
#connected;
|
||||||
|
|
||||||
|
constructor(name) {
|
||||||
|
super(name);
|
||||||
|
this.#connected = this.#client.connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
#keyOf(key) {
|
||||||
|
return this.id + '_' + key;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _get(key) {
|
||||||
|
await this.#connected;
|
||||||
|
|
||||||
|
const data = await this.#client.hGetAll(
|
||||||
|
commandOptions({ returnBuffers: true }),
|
||||||
|
this.#keyOf(key)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!data.d) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = data.t;
|
||||||
|
if (type && type[0] === 'b'.charCodeAt())
|
||||||
|
return data.d;
|
||||||
|
else
|
||||||
|
return JSON.parse(data.d);
|
||||||
|
}
|
||||||
|
|
||||||
|
async _set(key, val, exp_sec = -1) {
|
||||||
|
await this.#connected;
|
||||||
|
|
||||||
|
const out = { d: val };
|
||||||
|
if (val instanceof Buffer) {
|
||||||
|
out.t = 'b';
|
||||||
|
} else {
|
||||||
|
out.d = JSON.stringify(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.#client.hSet(
|
||||||
|
this.#keyOf(key),
|
||||||
|
out
|
||||||
|
);
|
||||||
|
|
||||||
|
if (exp_sec > 0) {
|
||||||
|
await this.#client.hExpire(
|
||||||
|
this.#keyOf(key),
|
||||||
|
Object.keys(out),
|
||||||
|
exp_sec
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
10
api/src/store/store.js
Normal file
10
api/src/store/store.js
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { env } from '../config.js';
|
||||||
|
|
||||||
|
let _export;
|
||||||
|
if (env.redisURL) {
|
||||||
|
_export = await import('./redis-store.js');
|
||||||
|
} else {
|
||||||
|
_export = await import('./memory-store.js');
|
||||||
|
}
|
||||||
|
|
||||||
|
export default _export.default;
|
|
@ -10,6 +10,9 @@ importers:
|
||||||
|
|
||||||
api:
|
api:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@datastructures-js/priority-queue':
|
||||||
|
specifier: ^6.3.1
|
||||||
|
version: 6.3.1
|
||||||
'@imput/version-info':
|
'@imput/version-info':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
version: link:../packages/version-info
|
version: link:../packages/version-info
|
||||||
|
@ -188,6 +191,12 @@ packages:
|
||||||
'@bufbuild/protobuf@2.1.0':
|
'@bufbuild/protobuf@2.1.0':
|
||||||
resolution: {integrity: sha512-+2Mx67Y3skJ4NCD/qNSdBJNWtu6x6Qr53jeNg+QcwiL6mt0wK+3jwHH2x1p7xaYH6Ve2JKOVn0OxU35WsmqI9A==}
|
resolution: {integrity: sha512-+2Mx67Y3skJ4NCD/qNSdBJNWtu6x6Qr53jeNg+QcwiL6mt0wK+3jwHH2x1p7xaYH6Ve2JKOVn0OxU35WsmqI9A==}
|
||||||
|
|
||||||
|
'@datastructures-js/heap@4.3.3':
|
||||||
|
resolution: {integrity: sha512-UcUu/DLh/aM4W3C8zZfwxxm6/6FIZUlm3mcAXuNOCa6Aj4iizNvNXQyb8DjZQH2jKSQbMRyNlngP6TPimuGjpQ==}
|
||||||
|
|
||||||
|
'@datastructures-js/priority-queue@6.3.1':
|
||||||
|
resolution: {integrity: sha512-eoxkWql/j0VJ0UFMFTpnyJz4KbEEVQ6aZ/JuJUgenu0Im4tYKylAycNGsYCHGXiVNEd7OKGVwfx1Ac3oYkuu7A==}
|
||||||
|
|
||||||
'@derhuerst/http-basic@8.2.4':
|
'@derhuerst/http-basic@8.2.4':
|
||||||
resolution: {integrity: sha512-F9rL9k9Xjf5blCz8HsJRO4diy111cayL2vkY2XE4r4t3n0yPXVYy3KD3nJ1qbrSn9743UWSXH4IwuCa/HWlGFw==}
|
resolution: {integrity: sha512-F9rL9k9Xjf5blCz8HsJRO4diy111cayL2vkY2XE4r4t3n0yPXVYy3KD3nJ1qbrSn9743UWSXH4IwuCa/HWlGFw==}
|
||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
|
@ -2355,6 +2364,12 @@ snapshots:
|
||||||
|
|
||||||
'@bufbuild/protobuf@2.1.0': {}
|
'@bufbuild/protobuf@2.1.0': {}
|
||||||
|
|
||||||
|
'@datastructures-js/heap@4.3.3': {}
|
||||||
|
|
||||||
|
'@datastructures-js/priority-queue@6.3.1':
|
||||||
|
dependencies:
|
||||||
|
'@datastructures-js/heap': 4.3.3
|
||||||
|
|
||||||
'@derhuerst/http-basic@8.2.4':
|
'@derhuerst/http-basic@8.2.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
caseless: 0.12.0
|
caseless: 0.12.0
|
||||||
|
|
Loading…
Reference in a new issue