forked from tsai/budibase
16 changed files with 491 additions and 127 deletions
@ -1,5 +1,5 @@ |
|||
module.exports = { |
|||
Client: require("./src/redis"), |
|||
utils: require("./src/redis/utils"), |
|||
clients: require("./src/redis/authRedis"), |
|||
clients: require("./src/redis/init"), |
|||
} |
|||
|
|||
@ -0,0 +1,92 @@ |
|||
import { getTenantId } from "../../context" |
|||
import redis from "../../redis/init" |
|||
import RedisWrapper from "../../redis" |
|||
|
|||
function generateTenantKey(key: string) { |
|||
const tenantId = getTenantId() |
|||
return `${key}:${tenantId}` |
|||
} |
|||
|
|||
export = class BaseCache { |
|||
client: RedisWrapper | undefined |
|||
|
|||
constructor(client: RedisWrapper | undefined = undefined) { |
|||
this.client = client |
|||
} |
|||
|
|||
async getClient() { |
|||
return !this.client ? await redis.getCacheClient() : this.client |
|||
} |
|||
|
|||
async keys(pattern: string) { |
|||
const client = await this.getClient() |
|||
return client.keys(pattern) |
|||
} |
|||
|
|||
/** |
|||
* Read only from the cache. |
|||
*/ |
|||
async get(key: string, opts = { useTenancy: true }) { |
|||
key = opts.useTenancy ? generateTenantKey(key) : key |
|||
const client = await this.getClient() |
|||
return client.get(key) |
|||
} |
|||
|
|||
/** |
|||
* Write to the cache. |
|||
*/ |
|||
async store( |
|||
key: string, |
|||
value: any, |
|||
ttl: number | null = null, |
|||
opts = { useTenancy: true } |
|||
) { |
|||
key = opts.useTenancy ? generateTenantKey(key) : key |
|||
const client = await this.getClient() |
|||
await client.store(key, value, ttl) |
|||
} |
|||
|
|||
/** |
|||
* Remove from cache. |
|||
*/ |
|||
async delete(key: string, opts = { useTenancy: true }) { |
|||
key = opts.useTenancy ? generateTenantKey(key) : key |
|||
const client = await this.getClient() |
|||
return client.delete(key) |
|||
} |
|||
|
|||
/** |
|||
* Read from the cache. Write to the cache if not exists. |
|||
*/ |
|||
async withCache( |
|||
key: string, |
|||
ttl: number, |
|||
fetchFn: any, |
|||
opts = { useTenancy: true } |
|||
) { |
|||
const cachedValue = await this.get(key, opts) |
|||
if (cachedValue) { |
|||
return cachedValue |
|||
} |
|||
|
|||
try { |
|||
const fetchedValue = await fetchFn() |
|||
|
|||
await this.store(key, fetchedValue, ttl, opts) |
|||
return fetchedValue |
|||
} catch (err) { |
|||
console.error("Error fetching before cache - ", err) |
|||
throw err |
|||
} |
|||
} |
|||
|
|||
async bustCache(key: string, opts = { client: null }) { |
|||
const client = await this.getClient() |
|||
try { |
|||
await client.delete(generateTenantKey(key)) |
|||
} catch (err) { |
|||
console.error("Error busting cache - ", err) |
|||
throw err |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,47 @@ |
|||
require("../../../tests/utilities/TestConfiguration") |
|||
const { get, put } = require("../writethrough") |
|||
const { dangerousGetDB } = require("../../db") |
|||
const tk = require("timekeeper") |
|||
|
|||
const START_DATE = Date.now() |
|||
tk.freeze(START_DATE) |
|||
|
|||
const DELAY = 5000 |
|||
|
|||
const db = dangerousGetDB("test") |
|||
|
|||
describe("writethrough", () => { |
|||
|
|||
describe("put", () => { |
|||
let first |
|||
it("should be able to store, will go to DB", async () => { |
|||
const response = await put(db,{ _id: "test", value: 1 }, DELAY) |
|||
const output = await db.get(response._id) |
|||
first = output |
|||
expect(output.value).toBe(1) |
|||
}) |
|||
|
|||
it("second put shouldn't update DB", async () => { |
|||
const response = await put(db, { ...first, value: 2 }, DELAY) |
|||
const output = await db.get(response._id) |
|||
expect(first._rev).toBe(output._rev) |
|||
expect(output.value).toBe(1) |
|||
}) |
|||
|
|||
it("should put it again after delay period", async () => { |
|||
tk.freeze(START_DATE + DELAY + 1) |
|||
const response = await put(db, { ...first, value: 3 }, DELAY) |
|||
const output = await db.get(response._id) |
|||
expect(response._rev).not.toBe(first._rev) |
|||
expect(output.value).toBe(3) |
|||
}) |
|||
}) |
|||
|
|||
describe("get", () => { |
|||
it("should be able to retrieve", async () => { |
|||
const response = await get(db, "test") |
|||
expect(response.value).toBe(3) |
|||
}) |
|||
}) |
|||
}) |
|||
|
|||
@ -0,0 +1,58 @@ |
|||
import BaseCache from "./base" |
|||
import { getWritethroughClient } from "../redis/init" |
|||
|
|||
const DEFAULT_WRITE_RATE_MS = 10000 |
|||
let CACHE: BaseCache | null = null |
|||
|
|||
interface CacheItem { |
|||
value: any |
|||
lastWrite: number |
|||
} |
|||
|
|||
async function getCache() { |
|||
if (!CACHE) { |
|||
const client = await getWritethroughClient() |
|||
CACHE = new BaseCache(client) |
|||
} |
|||
return CACHE |
|||
} |
|||
|
|||
function makeCacheItem(value: any, lastWrite: number | null = null): CacheItem { |
|||
return { value, lastWrite: lastWrite || Date.now() } |
|||
} |
|||
|
|||
exports.put = async ( |
|||
db: PouchDB.Database, |
|||
value: any, |
|||
writeRateMs: number = DEFAULT_WRITE_RATE_MS |
|||
) => { |
|||
const cache = await getCache() |
|||
const key = value._id |
|||
let cacheItem: CacheItem | undefined = await cache.get(key) |
|||
const updateDb = !cacheItem || cacheItem.lastWrite < Date.now() - writeRateMs |
|||
let output = value |
|||
if (updateDb) { |
|||
// value should contain the _id and _rev
|
|||
const response = await db.put(value) |
|||
output = { |
|||
...value, |
|||
_id: response.id, |
|||
_rev: response.rev, |
|||
} |
|||
} |
|||
// if we are updating the DB then need to set the lastWrite to now
|
|||
cacheItem = makeCacheItem(value, updateDb ? null : cacheItem?.lastWrite) |
|||
await cache.store(key, cacheItem) |
|||
return output |
|||
} |
|||
|
|||
exports.get = async (db: PouchDB.Database, id: string): Promise<any> => { |
|||
const cache = await getCache() |
|||
let cacheItem: CacheItem = await cache.get(id) |
|||
if (!cacheItem) { |
|||
const value = await db.get(id) |
|||
cacheItem = makeCacheItem(value) |
|||
await cache.store(id, cacheItem) |
|||
} |
|||
return cacheItem.value |
|||
} |
|||
Loading…
Reference in new issue