Skip to content

Commit

Permalink
fix: use 1 hour caches, rewrite cache
Browse files Browse the repository at this point in the history
  • Loading branch information
amoshydra committed Feb 20, 2024
1 parent c7c9eb1 commit d490033
Show file tree
Hide file tree
Showing 5 changed files with 530 additions and 46 deletions.
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"test": "vitest",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
Expand Down Expand Up @@ -45,7 +46,9 @@
"eslint": "^8.56.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
"time-chainer": "^0.0.3",
"typescript": "^5.2.2",
"vite": "^5.1.0"
"vite": "^5.1.0",
"vitest": "^1.3.1"
}
}
79 changes: 79 additions & 0 deletions src/services/CacheStorage/cache.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import Time from "time-chainer";
import { describe, expect, it, vi } from 'vitest'
import { read, write } from './cache'

const offsetNow = (n: number) => Date.now() + n;

describe("read", () => {
it('should retrieve given cache data', () => {
const storageData = JSON.stringify({
created: offsetNow(0),
caches: [
["key-a", "value-a"],
]
});
const retrieved = read(() => storageData);

expect(retrieved.map.get("key-a")).toBe("value-a");
});

it('should retrieve given cache data if data is retrived before an hour (55 minutes)', () => {
const storageData = JSON.stringify({
created: offsetNow(Time.hours(-1).minutes(+5)),
caches: [
["key-a", "value-a"],
]
});
const retrieved = read(() => storageData);

expect(retrieved.map.get("key-a")).toBe("value-a");
});

it('should reset blank data when 1 hour has elapsed', () => {
const storageData = JSON.stringify({
created: offsetNow(Time.hours(-1)),
caches: [
["key-a", "value-a"],
]
});
const retrieved = read(() => storageData);

expect(retrieved.map.get("key-a")).toBeUndefined();
});

it('should reset blank data when data is corrupted', () => {
const storageData = JSON.stringify({
created: offsetNow(0),
invalidKey: [
["key-a", "value-a"],
]
});
const retrieved = read(() => storageData);

expect(retrieved.map.get("key-a")).toBeUndefined();
});
});

describe("write", () => {
it('should pass stringified data to writer', () => {
const mockWriter = vi.fn();
write(
{
created: +Time.seconds(1),
map: new Map([
["key-a", "value-a"]
]),
},
mockWriter,
);

expect(mockWriter.mock.lastCall[0]).toBe(
JSON.stringify({
created: 1000,
caches: [
["key-a", "value-a"]
]
})
);
});
});
54 changes: 54 additions & 0 deletions src/services/CacheStorage/cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import Time from "time-chainer";

const CACHE_EXPIRY = Time.hours(1);

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type CacheValue = any;

interface CacheStorage {
caches: [string, CacheValue][];
created: number;
}

interface CacheMapInfo {
map: Map<string, CacheValue>;
created: number;
}

const DEFAULT_CACHE = JSON.stringify({
caches: [] as [string, CacheValue][],
created: 0,
});

export const read = (reader: () => string | null): CacheMapInfo => {
try {
// When initializing, we restore the data from `localStorage` into a map.
const { caches, created } = JSON.parse(reader() || DEFAULT_CACHE) as CacheStorage;

if (Date.now() - created < CACHE_EXPIRY) {
// restore if cache was created in less than a day
return {
created: created,
map: new Map(caches),
};
}
} catch (e) {
// using empty cache map
}

return {
created: Date.now(),
map: new Map(),
}
};

export const write = (info: CacheMapInfo, writer: (serializedData: string) => void) => {
writer(
JSON.stringify(
{
created: info.created,
caches: [...info.map.entries()],
}
)
);
};
46 changes: 5 additions & 41 deletions src/services/CacheStorage/index.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,15 @@
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type CacheValue = any;
import { read, write } from "./cache";

interface CacheStorage {
caches: [string, CacheValue][];
created: number;
}
const CACHE_KEY = "app-cache"
const ONE_DAY = (
1 // ms
* 1000 // ss
* 60 // mm
* 60 // hh
* 24
)
const DEFAULT_CACHE = JSON.stringify({
caches: [] as [string, CacheValue][],
created: 0,
});
export function localStorageProvider() {
const working = {
map: new Map<string, CacheValue>(),
created: Date.now(),
};

try {
// When initializing, we restore the data from `localStorage` into a map.
const { caches, created } = JSON.parse(localStorage.getItem(CACHE_KEY) || DEFAULT_CACHE) as CacheStorage;
if (Date.now() - created < ONE_DAY) {
// restore if cache was created in less than a day
working.map = new Map(caches);
working.created = created;
} else {
// using empty cache map
}
} catch (e) {
// using empty cache map
}
export function localStorageProvider() {
const cacheInfo = read(localStorage.getItem.bind(null, CACHE_KEY));

// Before unloading the app, we write back all the data into `localStorage`.
window.addEventListener('beforeunload', () => {
const cacheStorage: CacheStorage = {
caches: [...working.map.entries()],
created: working.created,
};
localStorage.setItem(CACHE_KEY, JSON.stringify(cacheStorage))
write(cacheInfo, localStorage.setItem.bind(null, CACHE_KEY));
})

// We still use the map for write & read for performance.
return working.map;
return cacheInfo.map;
}
Loading

0 comments on commit d490033

Please sign in to comment.