@julesl23/s5js
Version:
Enhanced TypeScript SDK for S5 decentralized storage with path-based API, media processing, and directory utilities
183 lines • 6.88 kB
JavaScript
import { describe, test, expect, beforeEach } from "vitest";
import { FS5 } from "../../../src/fs/fs5.js";
// Mock S5 API
class MockS5API {
storage = new Map();
registry = new Map();
crypto = {
hashBlake3Sync: (data) => {
// Simple mock hash - just use first 32 bytes or pad
const hash = new Uint8Array(32);
for (let i = 0; i < Math.min(data.length, 32); i++) {
hash[i] = data[i];
}
return hash;
},
hashBlake3Blob: async (blob) => {
const data = new Uint8Array(await blob.arrayBuffer());
return MockS5API.prototype.crypto.hashBlake3Sync(data);
},
generateSecureRandomBytes: (size) => {
const bytes = new Uint8Array(size);
crypto.getRandomValues(bytes);
return bytes;
},
newKeyPairEd25519: async (seed) => {
return {
publicKey: seed,
privateKey: seed
};
},
encryptXChaCha20Poly1305: async (key, nonce, plaintext) => {
// Simple mock - just return plaintext with 16-byte tag
return new Uint8Array([...plaintext, ...new Uint8Array(16)]);
},
decryptXChaCha20Poly1305: async (key, nonce, ciphertext) => {
// Simple mock - remove tag
return ciphertext.subarray(0, ciphertext.length - 16);
},
signRawRegistryEntry: async (keyPair, entry) => {
// Simple mock signature
return new Uint8Array(64);
},
signEd25519: async (keyPair, message) => {
// Simple mock signature
return new Uint8Array(64);
}
};
async uploadBlob(blob) {
const data = new Uint8Array(await blob.arrayBuffer());
const hash = new Uint8Array(33); // Include multihash prefix
hash[0] = 0x1e; // MULTIHASH_BLAKE3
crypto.getRandomValues(hash.subarray(1));
// Store by the full hash
const key = Buffer.from(hash).toString('hex');
this.storage.set(key, data);
return { hash, size: blob.size };
}
async downloadBlob(cid) {
const data = await this.downloadBlobAsBytes(cid);
return new Blob([data]);
}
async downloadBlobAsBytes(cid) {
// Try direct lookup first
let key = Buffer.from(cid).toString('hex');
let data = this.storage.get(key);
if (!data && cid.length === 32) {
// Try with MULTIHASH_BLAKE3 prefix
const cidWithPrefix = new Uint8Array(33);
cidWithPrefix[0] = 0x1e;
cidWithPrefix.set(cid, 1);
key = Buffer.from(cidWithPrefix).toString('hex');
data = this.storage.get(key);
}
if (!data)
throw new Error("Blob not found");
return data;
}
async registryGet(publicKey) {
const key = Buffer.from(publicKey).toString('hex');
const entry = this.registry.get(key);
// Return proper registry entry structure
if (!entry) {
return { exists: false, data: null, revision: 0 };
}
return {
exists: true,
data: entry.data,
revision: entry.revision || 1,
signature: entry.signature || new Uint8Array(64)
};
}
async registrySet(entry) {
const key = Buffer.from(entry.pk).toString('hex');
this.registry.set(key, {
data: entry.data,
revision: entry.revision || 1,
signature: entry.signature || new Uint8Array(64)
});
}
registryListen(publicKey) {
// Mock implementation - return empty async iterator
return (async function* () {
// Empty async generator
})();
}
}
// Mock Identity
class MockIdentity {
fsRootKey = new Uint8Array(32).fill(1);
// Add required properties for proper identity initialization
get publicKey() {
return new Uint8Array(32).fill(2);
}
get privateKey() {
return new Uint8Array(64).fill(3);
}
// For registry operations
keyPair = {
publicKey: new Uint8Array(32).fill(2),
privateKey: new Uint8Array(64).fill(3)
};
}
describe("FS5 HAMT Performance", () => {
let fs;
beforeEach(async () => {
// Setup mock API and identity
fs = new FS5(new MockS5API(), new MockIdentity());
try {
// Initialize the filesystem with root directories
await fs.ensureIdentityInitialized();
}
catch (error) {
// Silently handle initialization errors
// Tests will fail appropriately if fs is not properly initialized
}
});
test("should handle 10K entries efficiently", async () => {
const start = Date.now();
// Add 10K files
for (let i = 0; i < 10000; i++) {
await fs.put(`home/perf10k/file${i}.txt`, `content ${i}`);
}
const insertTime = Date.now() - start;
console.log(`Insert 10K entries: ${insertTime}ms`);
// Test random access
const accessStart = Date.now();
for (let i = 0; i < 100; i++) {
const idx = Math.floor(Math.random() * 10000);
const content = await fs.get(`home/perf10k/file${idx}.txt`);
expect(content).toBe(`content ${idx}`);
}
const accessTime = Date.now() - accessStart;
console.log(`100 random accesses: ${accessTime}ms (${accessTime / 100}ms per access)`);
// Should be under 100ms per access
expect(accessTime / 100).toBeLessThan(100);
});
test("should maintain O(log n) performance at scale", async () => {
const sizes = [1000, 5000, 10000];
const accessTimes = [];
for (const size of sizes) {
// Create directory with 'size' entries
for (let i = 0; i < size; i++) {
await fs.put(`home/scale${size}/file${i}.txt`, `content ${i}`);
}
// Measure access time
const start = Date.now();
for (let i = 0; i < 50; i++) {
const idx = Math.floor(Math.random() * size);
await fs.get(`home/scale${size}/file${idx}.txt`);
}
const avgTime = (Date.now() - start) / 50;
accessTimes.push(avgTime);
console.log(`Size ${size}: ${avgTime}ms average access`);
}
// Access time should not grow linearly
// With O(log n), doubling size should add constant time
const growth1 = accessTimes[1] - accessTimes[0];
const growth2 = accessTimes[2] - accessTimes[1];
// Growth should be relatively constant (allowing 50% variance)
expect(growth2).toBeLessThan(growth1 * 1.5);
});
});
//# sourceMappingURL=fs5-hamt-performance.test.js.map