Skip to content

Commit

Permalink
refactor: lru cache; utils; vitest
Browse files Browse the repository at this point in the history
  • Loading branch information
aaronshaf committed Nov 21, 2024
1 parent db6cc1f commit c5aaa22
Show file tree
Hide file tree
Showing 9 changed files with 928 additions and 12 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Build and Deploy to GitHub Pages
name: "Build and Deploy to GitHub Pages"

on:
push:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ on:

jobs:
test-linux:
name: Playwright tests on Linux (headless=${{ matrix.headless }}, browser=${{ matrix.browser }})
name: playwright (headless=${{ matrix.headless }}, browser=${{ matrix.browser }})
runs-on: ubuntu-22.04

strategy:
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/typescript.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: TypeScript Check
name: "TypeScript Check"

on:
push:
Expand All @@ -13,7 +13,7 @@ on:

jobs:
type-check:
name: Run TypeScript Check for idb-cache
name: typescript
runs-on: ubuntu-22.04

steps:
Expand Down
62 changes: 62 additions & 0 deletions .github/workflows/vitest.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
name: "Vitest Tests"

on:
push:
branches:
- main
- 'releases/*'
pull_request:
branches:
- main
- 'releases/*'
workflow_dispatch:

jobs:
test-idb-cache:
name: vitest
runs-on: ubuntu-22.04

steps:
# 1. Checkout the repository
- uses: actions/checkout@v3

# 2. Setup Node.js
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '20'

# 3. Setup pnpm
- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: 7.x

# 4. Cache pnpm dependencies (pnpm store and node_modules)
- name: Cache pnpm dependencies
uses: actions/cache@v3
with:
path: |
~/.pnpm-store
packages/idb-cache/node_modules
key: ${{ runner.os }}-pnpm-store-idb-cache-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-idb-cache-
# 5. Install dependencies
- name: Install dependencies
run: pnpm install

# 6. Run Tests and Save Logs
- name: Run Tests for idb-cache
run: |
mkdir -p logs
pnpm --filter packages/idb-cache test > logs/test-output.log 2>&1 || true
# 7. Upload Test Logs (on failure)
- name: Upload Test Logs
if: failure()
uses: actions/upload-artifact@v3
with:
name: idb-cache-test-logs
path: logs/test-output.log
6 changes: 4 additions & 2 deletions packages/idb-cache/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
"build": "rslib build",
"dev": "rslib build --watch",
"biome:check": "biome check . --write",
"typescript:check": "tsc --noEmit"
"typescript:check": "tsc --noEmit",
"test": "vitest"
},
"peerDependencies": {
"@rslib/core": "^0.0.15"
Expand All @@ -33,6 +34,7 @@
"@rslib/core": "^0.0.15",
"@types/node": "^22.9.0",
"idb": "^8.0.0",
"typescript": "^5.6.3"
"typescript": "^5.6.3",
"vitest": "^2.1.5"
}
}
36 changes: 36 additions & 0 deletions packages/idb-cache/src/LRUCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Least recently used (LRU) cache
export class LRUCache<K, V> {
private maxSize: number;
private cache: Map<K, V>;

constructor(maxSize = 10000) {
this.maxSize = maxSize;
this.cache = new Map();
}

get(key: K): V | undefined {
if (!this.cache.has(key)) return undefined;
const value = this.cache.get(key);
if (value === undefined) return undefined;
this.cache.delete(key);
this.cache.set(key, value);
return value;
}

set(key: K, value: V): void {
if (this.cache.has(key)) {
this.cache.delete(key);
} else if (this.cache.size >= this.maxSize) {
// Remove least recently used
const firstKey = this.cache.keys().next().value;
if (firstKey !== undefined) {
this.cache.delete(firstKey);
}
}
this.cache.set(key, value);
}

has(key: K): boolean {
return this.cache.has(key);
}
}
72 changes: 72 additions & 0 deletions packages/idb-cache/src/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { expect, test, describe } from "vitest";
import { deterministicUUID, generateUUIDFromHash } from "./utils";

const hash1 =
"ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff";
const hash2 =
"aa26b0aa4af7a749aa1a1aa3c10aa9923f611910772a473f1119a5a4940a0ab27ac115f1a0a1a5f14f11bc117fa67b143732c304cc5fa9aa1a6f57f50021a1ff";

describe("generateUUIDFromHash", () => {
test("generates valid UUID v4 format", () => {
const uuid = generateUUIDFromHash(hash1);

// Check UUID format (8-4-4-4-12 characters)
expect(uuid).toMatch(
/^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/
);
});

test("generates consistent UUIDs for same input", () => {
const uuid1 = generateUUIDFromHash(hash1);
const uuid2 = generateUUIDFromHash(hash1);

expect(uuid1).toBe(uuid2);
});

test("sets correct version (5) in UUID", () => {
const uuid = generateUUIDFromHash(hash1);
expect(uuid.charAt(14)).toBe("5");
});

test("sets correct variant bits in UUID", () => {
const uuid = generateUUIDFromHash(hash1);

// The 19th character should be 8, 9, a, or b
expect(uuid.charAt(19)).toMatch(/[89ab]/);
});

test("generates different UUIDs for different inputs", () => {
const uuid1 = generateUUIDFromHash(hash1);
const uuid2 = generateUUIDFromHash(hash2);

expect(uuid1).not.toBe(uuid2);
});

test("throws error for invalid hash length", () => {
expect(() => generateUUIDFromHash("123")).toThrowError();
});

test("throws error for non-hex characters", () => {
const invalidHash =
"qe26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff"; // Contains non-hex chars

expect(() => {
generateUUIDFromHash(invalidHash);
}).toThrowError();
});
});

describe("deterministicUUID", () => {
test("generates consistent UUID for the same key", async () => {
const key = "test-key";
const uuid1 = await deterministicUUID(key);
const uuid2 = await deterministicUUID(key);
expect(uuid1).toBe(uuid2);
});

test("generates different UUIDs for different keys", async () => {
const uuid1 = await deterministicUUID("test");
const uuid2 = await deterministicUUID("test2");
expect(uuid1).not.toBe(uuid2);
});
});
28 changes: 22 additions & 6 deletions packages/idb-cache/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,33 @@ import {
openDB,
} from "idb";
import type { IDBCacheSchema, STORE } from "./types";
import { LRUCache } from "./LRUCache";

const uuidCache = new Map<string, string>();
const uuidCache = new LRUCache<string, string>(1000);

function isValidHex(hex: string): boolean {
return /^[0-9a-fA-F]{128}$/.test(hex);
}

function bufferToHex(buffer: ArrayBuffer): string {
const byteArray = new Uint8Array(buffer);
let hex = "";
for (const byte of byteArray) {
hex += byte.toString(16).padStart(2, "0");
}
return hex;
}

// version 5
export function generateUUIDFromHash(hashHex: string): string {
if (!isValidHex(hashHex)) {
throw new Error("Invalid hash: Must be a 128-character hexadecimal string");
}

return [
hashHex.slice(0, 8),
hashHex.slice(8, 12),
`4${hashHex.slice(13, 16)}`,
`5${hashHex.slice(13, 16)}`,
((Number.parseInt(hashHex.slice(16, 17), 16) & 0x3) | 0x8).toString(16) +
hashHex.slice(17, 20),
hashHex.slice(20, 32),
Expand All @@ -37,10 +56,7 @@ export async function deterministicUUID(key: string): Promise<string> {
const encoder = new TextEncoder();
const data = encoder.encode(key);
const hashBuffer = await crypto.subtle.digest("SHA-512", data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
const hashHex = bufferToHex(hashBuffer);

const uuid = generateUUIDFromHash(hashHex);
uuidCache.set(key, uuid);
Expand Down
Loading

0 comments on commit c5aaa22

Please sign in to comment.