UNPKG

@julesl23/s5js

Version:

Enhanced TypeScript SDK for S5 decentralized storage with path-based API, media processing, and directory utilities

302 lines 11.9 kB
import { describe, test, expect, beforeEach } from "vitest"; import { HAMT } from "../../../src/fs/hamt/hamt.js"; // Mock S5 API class MockS5API { storage = new Map(); async uploadBlob(blob) { const data = new Uint8Array(await blob.arrayBuffer()); const hash = new Uint8Array(32); crypto.getRandomValues(hash); const key = Buffer.from(hash).toString('hex'); this.storage.set(key, data); return { hash, size: blob.size }; } async downloadBlobAsBytes(hash) { const key = Buffer.from(hash).toString('hex'); const data = this.storage.get(key); if (!data) throw new Error("Blob not found"); return data; } } describe("HAMT Iteration", () => { let hamt; let api; beforeEach(() => { api = new MockS5API(); hamt = new HAMT(api); }); describe("Basic iteration", () => { test("should iterate all entries with async iterator", async () => { const entries = new Map(); // Add test entries for (let i = 0; i < 10; i++) { const ref = { hash: new Uint8Array(32).fill(i), size: 100 + i }; const key = `f:iter${i}.txt`; entries.set(key, ref); await hamt.insert(key, ref); } // Iterate and collect const collected = new Map(); for await (const [key, value] of hamt.entries()) { collected.set(key, value); } // Verify all entries were iterated expect(collected.size).toBe(10); for (const [key, ref] of entries) { expect(collected.has(key)).toBe(true); expect(collected.get(key)).toEqual(ref); } }); test("should yield [key, value] tuples", async () => { const fileRef = { hash: new Uint8Array(32).fill(42), size: 1234 }; const dirRef = { link: { type: "fixed_hash_blake3", hash: new Uint8Array(32).fill(43) } }; await hamt.insert("f:test.txt", fileRef); await hamt.insert("d:testdir", dirRef); const results = []; for await (const entry of hamt.entries()) { results.push(entry); } expect(results.length).toBe(2); // Check tuple structure for (const [key, value] of results) { expect(typeof key).toBe("string"); expect(value).toBeDefined(); if (key.startsWith("f:")) { expect(value.size).toBeDefined(); } else if (key.startsWith("d:")) { expect(value.link).toBeDefined(); } } }); test("should handle empty HAMT", async () => { const results = []; for await (const entry of hamt.entries()) { results.push(entry); } expect(results.length).toBe(0); }); test("should traverse leaf and internal nodes correctly", async () => { // Insert enough entries to create internal nodes const entries = new Map(); for (let i = 0; i < 50; i++) { const ref = { hash: new Uint8Array(32).fill(i), size: 1000 + i, media_type: "text/plain" }; const key = `f:traverse${i}.txt`; entries.set(key, ref); await hamt.insert(key, ref); } // Collect all via iteration const collected = new Set(); for await (const [key] of hamt.entries()) { collected.add(key); } // Verify all were found expect(collected.size).toBe(50); for (const key of entries.keys()) { expect(collected.has(key)).toBe(true); } }); }); describe("Cursor support", () => { test("should generate path array with getPathForKey", async () => { // Insert some entries for (let i = 0; i < 20; i++) { const ref = { hash: new Uint8Array(32).fill(i), size: 100 }; await hamt.insert(`f:path${i}.txt`, ref); } // Get path for an existing key const path = await hamt.getPathForKey("f:path10.txt"); expect(Array.isArray(path)).toBe(true); expect(path.length).toBeGreaterThan(0); // Path should contain indices for (const idx of path) { expect(typeof idx).toBe("number"); expect(idx).toBeGreaterThanOrEqual(0); } }); test("should return empty path for non-existent key", async () => { // Insert some entries for (let i = 0; i < 5; i++) { const ref = { hash: new Uint8Array(32).fill(i), size: 100 }; await hamt.insert(`f:exists${i}.txt`, ref); } // Get path for non-existent key const path = await hamt.getPathForKey("f:doesnotexist.txt"); expect(Array.isArray(path)).toBe(true); expect(path.length).toBe(0); }); test("should track child indices in path", async () => { // Insert entries to create some structure for (let i = 0; i < 30; i++) { const ref = { hash: new Uint8Array(32).fill(i), size: 100 }; await hamt.insert(`f:track${i}.txt`, ref); } // Get paths for multiple keys const paths = new Map(); for (let i = 0; i < 5; i++) { const key = `f:track${i * 5}.txt`; const path = await hamt.getPathForKey(key); paths.set(key, path); } // Paths should be unique for different keys (in most cases) const pathStrings = new Set(); for (const path of paths.values()) { pathStrings.add(JSON.stringify(path)); } // At least some paths should be different expect(pathStrings.size).toBeGreaterThan(1); }); }); describe("entriesFrom cursor", () => { test("should resume from exact cursor position", async () => { // Insert ordered entries const allKeys = []; for (let i = 0; i < 20; i++) { const key = `f:cursor${i.toString().padStart(2, '0')}.txt`; allKeys.push(key); const ref = { hash: new Uint8Array(32).fill(i), size: 100 }; await hamt.insert(key, ref); } // Get path for middle entry const middleKey = allKeys[10]; const hamtPath = await hamt.getPathForKey(middleKey); // Resume from cursor const resumedKeys = []; for await (const [key] of hamt.entriesFrom(hamtPath)) { resumedKeys.push(key); if (resumedKeys.length >= 5) break; // Just get a few } // Should start from or after the cursor position expect(resumedKeys.length).toBeGreaterThan(0); // First resumed key should be at or after middle position const firstResumedIdx = allKeys.indexOf(resumedKeys[0]); expect(firstResumedIdx).toBeGreaterThanOrEqual(10); }); test("should skip already-seen entries", async () => { // Insert entries const entries = new Map(); for (let i = 0; i < 30; i++) { const key = `f:skip${i}.txt`; entries.set(key, i); const ref = { hash: new Uint8Array(32).fill(i), size: 100 + i }; await hamt.insert(key, ref); } // First, collect some entries const firstBatch = []; for await (const [key] of hamt.entries()) { firstBatch.push(key); if (firstBatch.length >= 10) break; } // Get cursor for last entry in first batch const lastKey = firstBatch[firstBatch.length - 1]; const cursor = await hamt.getPathForKey(lastKey); // Resume from cursor const secondBatch = []; for await (const [key] of hamt.entriesFrom(cursor)) { secondBatch.push(key); } // No duplicates between batches const firstSet = new Set(firstBatch); for (const key of secondBatch) { expect(firstSet.has(key)).toBe(false); } }); test("should handle cursor at leaf node", async () => { // Create a small HAMT that will have leaf nodes for (let i = 0; i < 5; i++) { const ref = { hash: new Uint8Array(32).fill(i), size: 100 }; await hamt.insert(`f:leaf${i}.txt`, ref); } // Get path to a leaf entry const path = await hamt.getPathForKey("f:leaf2.txt"); // Resume from this leaf position const resumed = []; for await (const [key] of hamt.entriesFrom(path)) { resumed.push(key); } // Should get remaining entries expect(resumed.length).toBeGreaterThan(0); expect(resumed.length).toBeLessThanOrEqual(3); // At most 3 entries after leaf2 }); test("should handle cursor at internal node", async () => { // Insert many entries to ensure internal nodes for (let i = 0; i < 100; i++) { const ref = { hash: new Uint8Array(32).fill(i % 256), size: 1000 + i }; await hamt.insert(`f:internal${i}.txt`, ref); } // Get a path that likely points to internal node const path = await hamt.getPathForKey("f:internal50.txt"); // Truncate path to point to internal node const internalPath = path.slice(0, -1); // Resume from internal node const resumed = []; for await (const [key] of hamt.entriesFrom(internalPath)) { resumed.push(key); if (resumed.length >= 10) break; } expect(resumed.length).toBe(10); }); test("should complete iteration when path exhausted", async () => { // Insert entries const total = 25; for (let i = 0; i < total; i++) { const ref = { hash: new Uint8Array(32).fill(i), size: 100 }; await hamt.insert(`f:exhaust${i}.txt`, ref); } // Get path near the end const nearEndPath = await hamt.getPathForKey("f:exhaust20.txt"); // Count remaining entries let remaining = 0; for await (const _ of hamt.entriesFrom(nearEndPath)) { remaining++; } // Should have some but not all entries expect(remaining).toBeGreaterThan(0); expect(remaining).toBeLessThan(total); }); }); }); //# sourceMappingURL=hamt-iteration.test.js.map