Skip to content

Commit 8b37ddf

Browse files
committed
refactor: lru cache; utils; vitest
1 parent db6cc1f commit 8b37ddf

File tree

9 files changed

+934
-21
lines changed

9 files changed

+934
-21
lines changed

.github/workflows/deploy.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Build and Deploy to GitHub Pages
1+
name: "Build and Deploy to GitHub Pages"
22

33
on:
44
push:
@@ -7,6 +7,7 @@ on:
77

88
jobs:
99
build-and-deploy:
10+
name: "GitHub Pages"
1011
runs-on: ubuntu-22.04
1112
strategy:
1213
matrix:

.github/workflows/playwright.yml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: "Playwright Tests"
1+
name: "Playwright"
22

33
on:
44
pull_request:
@@ -10,7 +10,7 @@ on:
1010

1111
jobs:
1212
test-linux:
13-
name: Playwright tests on Linux (headless=${{ matrix.headless }}, browser=${{ matrix.browser }})
13+
name: Playwright (headless=${{ matrix.headless }}, browser=${{ matrix.browser }})
1414
runs-on: ubuntu-22.04
1515

1616
strategy:
@@ -31,6 +31,7 @@ jobs:
3131

3232
# 3. Setup pnpm
3333
- name: Setup pnpm
34+
id: setup-pnpm
3435
uses: pnpm/action-setup@v2
3536
with:
3637
version: 7.x
@@ -42,8 +43,9 @@ jobs:
4243
path: |
4344
~/.pnpm-store
4445
node_modules
45-
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
46+
key: ${{ runner.os }}-pnpm-store-${{ steps.setup-pnpm.outputs.pnpm-version }}-${{ hashFiles('**/pnpm-lock.yaml') }}
4647
restore-keys: |
48+
${{ runner.os }}-pnpm-store-${{ steps.setup-pnpm.outputs.pnpm-version }}-
4749
${{ runner.os }}-pnpm-store-
4850
4951
# 5. Cache Playwright browsers

.github/workflows/typescript.yml

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: TypeScript Check
1+
name: "TypeScript"
22

33
on:
44
push:
@@ -13,26 +13,22 @@ on:
1313

1414
jobs:
1515
type-check:
16-
name: Run TypeScript Check for idb-cache
16+
name: Typescript
1717
runs-on: ubuntu-22.04
1818

1919
steps:
20-
# 1. Checkout the repository
2120
- uses: actions/checkout@v3
2221

23-
# 2. Setup Node.js
2422
- name: Setup Node.js
2523
uses: actions/setup-node@v3
2624
with:
2725
node-version: '20'
2826

29-
# 3. Setup pnpm
3027
- name: Setup pnpm
3128
uses: pnpm/action-setup@v2
3229
with:
3330
version: 7.x
3431

35-
# 4: Cache pnpm dependencies (pnpm store and node_modules)
3632
- name: Cache pnpm dependencies
3733
uses: actions/cache@v3
3834
with:
@@ -43,17 +39,14 @@ jobs:
4339
restore-keys: |
4440
${{ runner.os }}-pnpm-store-
4541
46-
# 5: Install dependencies
4742
- name: Install dependencies
4843
run: pnpm install
4944

50-
# 6: Run TypeScript Check and Save Logs
5145
- name: Run TypeScript Check
5246
run: |
5347
mkdir -p logs
5448
pnpm --filter packages/idb-cache typescript:check > logs/typescript-errors.log 2>&1 || true
5549
56-
# 7: Upload TypeScript Logs (on failure)
5750
- name: Upload TypeScript Logs
5851
if: failure()
5952
uses: actions/upload-artifact@v3

.github/workflows/vitest.yml

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
name: "Vitest"
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
- 'releases/*'
8+
pull_request:
9+
branches:
10+
- main
11+
- 'releases/*'
12+
workflow_dispatch:
13+
14+
jobs:
15+
test-idb-cache:
16+
name: Vitest
17+
runs-on: ubuntu-22.04
18+
19+
steps:
20+
# 1. Checkout the repository
21+
- uses: actions/checkout@v3
22+
23+
# 2. Setup Node.js
24+
- name: Setup Node.js
25+
uses: actions/setup-node@v3
26+
with:
27+
node-version: '20'
28+
29+
# 3. Setup pnpm
30+
- name: Setup pnpm
31+
id: setup-pnpm
32+
uses: pnpm/action-setup@v2
33+
with:
34+
version: 7.x
35+
36+
# 4. Cache pnpm dependencies (pnpm store and node_modules)
37+
- name: Cache pnpm dependencies
38+
uses: actions/cache@v3
39+
with:
40+
path: |
41+
~/.pnpm-store
42+
packages/idb-cache/node_modules
43+
key: ${{ runner.os }}-pnpm-store-idb-cache-${{ hashFiles('**/pnpm-lock.yaml') }}-pnpm-${{ steps.setup-pnpm.outputs.pnpm-version }}
44+
restore-keys: |
45+
${{ runner.os }}-pnpm-store-idb-cache-
46+
47+
# 5. Install dependencies
48+
- name: Install dependencies
49+
run: pnpm install
50+
51+
# 6. Run Tests and Save Logs
52+
- name: Run Tests for idb-cache
53+
run: |
54+
mkdir -p logs
55+
pnpm --filter packages/idb-cache test > logs/test-output.log 2>&1 || true
56+
57+
# 7. Upload Test Logs (on failure)
58+
- name: Upload Test Logs
59+
if: failure()
60+
uses: actions/upload-artifact@v3
61+
with:
62+
name: idb-cache-test-logs
63+
path: logs/test-output.log

packages/idb-cache/package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
"build": "rslib build",
2525
"dev": "rslib build --watch",
2626
"biome:check": "biome check . --write",
27-
"typescript:check": "tsc --noEmit"
27+
"typescript:check": "tsc --noEmit",
28+
"test": "vitest"
2829
},
2930
"peerDependencies": {
3031
"@rslib/core": "^0.0.15"
@@ -33,6 +34,7 @@
3334
"@rslib/core": "^0.0.15",
3435
"@types/node": "^22.9.0",
3536
"idb": "^8.0.0",
36-
"typescript": "^5.6.3"
37+
"typescript": "^5.6.3",
38+
"vitest": "^2.1.5"
3739
}
3840
}

packages/idb-cache/src/LRUCache.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Least recently used (LRU) cache
2+
export class LRUCache<K, V> {
3+
private maxSize: number;
4+
private cache: Map<K, V>;
5+
6+
constructor(maxSize = 10000) {
7+
this.maxSize = maxSize;
8+
this.cache = new Map();
9+
}
10+
11+
get(key: K): V | undefined {
12+
if (!this.cache.has(key)) return undefined;
13+
const value = this.cache.get(key);
14+
if (value === undefined) return undefined;
15+
this.cache.delete(key);
16+
this.cache.set(key, value);
17+
return value;
18+
}
19+
20+
set(key: K, value: V): void {
21+
if (this.cache.has(key)) {
22+
this.cache.delete(key);
23+
} else if (this.cache.size >= this.maxSize) {
24+
// Remove least recently used
25+
const firstKey = this.cache.keys().next().value;
26+
if (firstKey !== undefined) {
27+
this.cache.delete(firstKey);
28+
}
29+
}
30+
this.cache.set(key, value);
31+
}
32+
33+
has(key: K): boolean {
34+
return this.cache.has(key);
35+
}
36+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { expect, test, describe } from "vitest";
2+
import { deterministicUUID, generateUUIDFromHash } from "./utils";
3+
4+
const hash1 =
5+
"ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff";
6+
const hash2 =
7+
"aa26b0aa4af7a749aa1a1aa3c10aa9923f611910772a473f1119a5a4940a0ab27ac115f1a0a1a5f14f11bc117fa67b143732c304cc5fa9aa1a6f57f50021a1ff";
8+
9+
describe("generateUUIDFromHash", () => {
10+
test("generates valid UUID v4 format", () => {
11+
const uuid = generateUUIDFromHash(hash1);
12+
13+
// Check UUID format (8-4-4-4-12 characters)
14+
expect(uuid).toMatch(
15+
/^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/
16+
);
17+
});
18+
19+
test("generates consistent UUIDs for same input", () => {
20+
const uuid1 = generateUUIDFromHash(hash1);
21+
const uuid2 = generateUUIDFromHash(hash1);
22+
23+
expect(uuid1).toBe(uuid2);
24+
});
25+
26+
test("sets correct version (5) in UUID", () => {
27+
const uuid = generateUUIDFromHash(hash1);
28+
expect(uuid.charAt(14)).toBe("5");
29+
});
30+
31+
test("sets correct variant bits in UUID", () => {
32+
const uuid = generateUUIDFromHash(hash1);
33+
34+
// The 19th character should be 8, 9, a, or b
35+
expect(uuid.charAt(19)).toMatch(/[89ab]/);
36+
});
37+
38+
test("generates different UUIDs for different inputs", () => {
39+
const uuid1 = generateUUIDFromHash(hash1);
40+
const uuid2 = generateUUIDFromHash(hash2);
41+
42+
expect(uuid1).not.toBe(uuid2);
43+
});
44+
45+
test("throws error for invalid hash length", () => {
46+
expect(() => generateUUIDFromHash("123")).toThrowError();
47+
});
48+
49+
test("throws error for non-hex characters", () => {
50+
const invalidHash =
51+
"qe26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff"; // Contains non-hex chars
52+
53+
expect(() => {
54+
generateUUIDFromHash(invalidHash);
55+
}).toThrowError();
56+
});
57+
});
58+
59+
describe("deterministicUUID", () => {
60+
test("generates consistent UUID for the same key", async () => {
61+
const key = "test-key";
62+
const uuid1 = await deterministicUUID(key);
63+
const uuid2 = await deterministicUUID(key);
64+
expect(uuid1).toBe(uuid2);
65+
});
66+
67+
test("generates different UUIDs for different keys", async () => {
68+
const uuid1 = await deterministicUUID("test");
69+
const uuid2 = await deterministicUUID("test2");
70+
expect(uuid1).not.toBe(uuid2);
71+
});
72+
});

packages/idb-cache/src/utils.ts

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,33 @@ import {
66
openDB,
77
} from "idb";
88
import type { IDBCacheSchema, STORE } from "./types";
9+
import { LRUCache } from "./LRUCache";
910

10-
const uuidCache = new Map<string, string>();
11+
const uuidCache = new LRUCache<string, string>(1000);
1112

13+
function isValidHex(hex: string): boolean {
14+
return /^[0-9a-fA-F]{128}$/.test(hex);
15+
}
16+
17+
function bufferToHex(buffer: ArrayBuffer): string {
18+
const byteArray = new Uint8Array(buffer);
19+
let hex = "";
20+
for (const byte of byteArray) {
21+
hex += byte.toString(16).padStart(2, "0");
22+
}
23+
return hex;
24+
}
25+
26+
// version 5
1227
export function generateUUIDFromHash(hashHex: string): string {
28+
if (!isValidHex(hashHex)) {
29+
throw new Error("Invalid hash: Must be a 128-character hexadecimal string");
30+
}
31+
1332
return [
1433
hashHex.slice(0, 8),
1534
hashHex.slice(8, 12),
16-
`4${hashHex.slice(13, 16)}`,
35+
`5${hashHex.slice(13, 16)}`,
1736
((Number.parseInt(hashHex.slice(16, 17), 16) & 0x3) | 0x8).toString(16) +
1837
hashHex.slice(17, 20),
1938
hashHex.slice(20, 32),
@@ -37,10 +56,7 @@ export async function deterministicUUID(key: string): Promise<string> {
3756
const encoder = new TextEncoder();
3857
const data = encoder.encode(key);
3958
const hashBuffer = await crypto.subtle.digest("SHA-512", data);
40-
const hashArray = Array.from(new Uint8Array(hashBuffer));
41-
const hashHex = hashArray
42-
.map((b) => b.toString(16).padStart(2, "0"))
43-
.join("");
59+
const hashHex = bufferToHex(hashBuffer);
4460

4561
const uuid = generateUUIDFromHash(hashHex);
4662
uuidCache.set(key, uuid);

0 commit comments

Comments
 (0)