Skip to content

Commit

Permalink
feat: new cache strategy
Browse files Browse the repository at this point in the history
- load data on init
- always return latest successful data
- update data in background on each request
- all without data races!
  • Loading branch information
0x009922 committed Jul 24, 2024
1 parent b948345 commit 3001536
Show file tree
Hide file tree
Showing 3 changed files with 128 additions and 28 deletions.
37 changes: 9 additions & 28 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import Api from "./api.ts";
import { getMatrix, Matrix } from "./aggregate.ts";
import { getMatrix } from "./aggregate.ts";
import * as web from "./web.ts";
import { get as getConfig } from "./config.ts";
import { log, match, P } from "../deps.ts";
import { useFreshData } from "./util.ts";

const CONFIG = {
apiToken: getConfig("ALLURE_API_TOKEN"),
Expand Down Expand Up @@ -40,36 +41,16 @@ const api = new Api({
baseUrl: CONFIG.allureBaseUrl,
});

// 24 hours
const CACHE_TTL = 60_000 * 60 * 24;

class State {
#data: null | { matrix: Matrix; lastUpdated: Date } = null;
#promise: null | Promise<Matrix> = null;

getMatrix(): Promise<Matrix> {
if (this.#promise) return this.#promise;
const now = Date.now();
if (!this.#data || this.#data.lastUpdated.getTime() + CACHE_TTL < now) {
log.debug("No data/cache is stale, reloading matrix");
this.#promise = getMatrix(api).then((matrix) => {
this.#data = { matrix, lastUpdated: new Date() };
return matrix;
}).finally(() => {
this.#promise = null;
});
return this.#promise;
}
log.debug("Using cached data");
return Promise.resolve(this.#data.matrix);
}
}

const state = new State();
const { get: getMatrixWithCache } = useFreshData(async () => {
log.info("Getting matrix");
const data = await getMatrix(api);
log.info("Matrix is ready");
return data;
});

await web.run({
port: CONFIG.port,
provider: {
getMatrix: () => state.getMatrix(),
getMatrix: getMatrixWithCache,
},
});
60 changes: 60 additions & 0 deletions src/util.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Doesn't work with Deno!
// In order to run tests, install vitest globally using some node.js based package manager and then run `vitest`

import { describe, expect, test, vi } from "vitest";
import { useFreshData } from "./util.ts";

describe("useFreshData", () => {
test("fetches data before first get", async () => {
let called = false;

useFreshData(() => {
called = true;
return Promise.resolve(null);
});

await expect.poll(() => called).toBe(true);
});

test("initial call fails, then succeeds", async () => {
const data = vi.fn().mockRejectedValueOnce("foo").mockResolvedValueOnce(
"ok",
);

const { get } = useFreshData(data);

expect(() => get()).rejects.toThrow("foo");
await expect.poll(() => get()).toBe("ok");
});

test("initially ok, then fails, then ok", async () => {
const data = vi.fn().mockResolvedValueOnce(1).mockRejectedValueOnce("foo")
.mockResolvedValueOnce(2);

const { get } = useFreshData(data);

expect(await get()).toBe(1);
// waitUntil disallows errors
vi.waitUntil(async () => {
const value = await get();
return value === 2;
});
});

test("updates data 5 times in a row", async () => {
const { get } = useFreshData(
vi.fn()
.mockResolvedValueOnce(1)
.mockResolvedValueOnce(2)
.mockResolvedValueOnce(3)
.mockResolvedValueOnce(4)
.mockResolvedValueOnce(5),
);

expect(await get()).toBe(1);
expect(await get()).toBe(1);
expect(await get()).toBe(2);
expect(await get()).toBe(3);
expect(await get()).toBe(4);
});
});
59 changes: 59 additions & 0 deletions src/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
export function useFreshData<T>(
fn: () => Promise<T>,
): { get: () => Promise<T> } {
let state: { data: null | { some: T }; promise: null | Promise<T> } = {
data: null,
promise: null,
};

function createPromise(): Promise<T> {
const promise = fn().then((data) => {
if (state.promise === promise) {
state.data = { some: data };
state.promise = null;
}
return data;
}).catch((err) => {
if (state.promise === promise) {
state.promise = null;
}
throw err;
});
return promise;
}

async function get(): Promise<T> {
if (state.data && state.promise) {
return (state.data.some);
}
if (state.data) {
const promise = createPromise();
promise.catch((err) => {
console.error(err);
});
state.promise = promise;
return (state.data.some);
}
if (state.promise) {
const data = await state.promise;
state = { data: { some: data }, promise: null };
return data;
}
const promise = createPromise();
state = { data: null, promise };
try {
const data = await promise;
state = { data: { some: data }, promise: null };
return data;
} catch (err) {
state.promise = null;
throw err;
}
}

get().catch((err) => {
console.error(err);
});

return { get };
}

0 comments on commit 3001536

Please sign in to comment.