diff --git a/packages/idb-cache-app/src/App.tsx b/packages/idb-cache-app/src/App.tsx index 9bd9f38..2f837e3 100644 --- a/packages/idb-cache-app/src/App.tsx +++ b/packages/idb-cache-app/src/App.tsx @@ -1,7 +1,7 @@ import "./App.css"; import { IDBCache } from "@instructure/idb-cache"; import { useCallback, useRef, useState, useEffect } from "react"; -import { deterministicHash, generateTextOfSize } from "./utils"; +import { deterministicHash } from "./utils"; import { Button } from "@instructure/ui-buttons"; import { Metric } from "@instructure/ui-metric"; import { View } from "@instructure/ui-view"; @@ -17,6 +17,7 @@ import { WrappedFlexItem, } from "./components/WrappedFlexItem"; import { Test } from "./components/Test"; +import { generateTextOfSize } from "./fixtures"; // For demonstration/testing purposes. // Do *not* store cacheKey to localStorage in production. @@ -155,22 +156,28 @@ const App = () => { saveContentKey(key); const start1 = performance.now(); - const paragraphs = Array.from({ length: DEFAULT_NUM_ITEMS }, (_, index) => - generateTextOfSize(itemSize, `${key}-${index}`), - ); - const end1 = performance.now(); - setTimeToGenerate(end1 - start1); + try { + const paragraphs = await Promise.all( + Array.from({ length: DEFAULT_NUM_ITEMS }, (_, index) => + generateTextOfSize(itemSize, `${key}-${index}`), + ), + ); + const end1 = performance.now(); + setTimeToGenerate(end1 - start1); + + const start2 = performance.now(); + + for (let i = 0; i < DEFAULT_NUM_ITEMS; i++) { + await cache.setItem(`item-${key}-${i}`, paragraphs[i]); + } - const start2 = performance.now(); + const end2 = performance.now(); + setSetItemTime(end2 - start2); - for (let i = 0; i < DEFAULT_NUM_ITEMS; i++) { - await cache.setItem(`item-${key}-${i}`, paragraphs[i]); + setHash1(deterministicHash(paragraphs.join(""))); + } catch (error) { + console.error("Error during text generation and storage:", error); } - - const end2 = performance.now(); - setSetItemTime(end2 - start2); - - setHash1(deterministicHash(paragraphs.join(""))); }, [itemSize]); const retrieveAndDecrypt = useCallback(async () => { @@ -180,21 +187,25 @@ const App = () => { return; } - const results: Array = []; - const start = performance.now(); + try { + const results: Array = []; + const start = performance.now(); - for (let i = 0; i < DEFAULT_NUM_ITEMS; i++) { - const result = await cache.getItem(`item-${contentKey}-${i}`); - results.push(result); - } + for (let i = 0; i < DEFAULT_NUM_ITEMS; i++) { + const result = await cache.getItem(`item-${contentKey}-${i}`); + results.push(result); + } - const end = performance.now(); - setGetItemTime(end - start); - setHash2( - results.filter((x) => x).length > 0 - ? deterministicHash(results.join("")) - : null, - ); + const end = performance.now(); + setGetItemTime(end - start); + setHash2( + results.filter((x) => x).length > 0 + ? deterministicHash(results.join("")) + : null, + ); + } catch (error) { + console.error("Error during text retrieval and decryption:", error); + } }, [contentKey]); const cleanup = useCallback(async () => { @@ -204,10 +215,14 @@ const App = () => { return; } - const start = performance.now(); - await cache.cleanup(); - const end = performance.now(); - setCleanupTime(end - start); + try { + const start = performance.now(); + await cache.cleanup(); + const end = performance.now(); + setCleanupTime(end - start); + } catch (error) { + console.error("Error during cache cleanup:", error); + } }, []); const count = useCallback(async () => { @@ -217,11 +232,15 @@ const App = () => { return; } - const start = performance.now(); - const count = await cache.count(); - const end = performance.now(); - setCountTime(end - start); - setItemCount(count); + try { + const start = performance.now(); + const count = await cache.count(); + const end = performance.now(); + setCountTime(end - start); + setItemCount(count); + } catch (error) { + console.error("Error during cache count:", error); + } }, []); const clear = useCallback(async () => { @@ -231,11 +250,15 @@ const App = () => { return; } - const start = performance.now(); - await cache.clear(); - localStorage.removeItem("keyCounter"); - const end = performance.now(); - setClearTime(end - start); + try { + const start = performance.now(); + await cache.clear(); + localStorage.removeItem("keyCounter"); + const end = performance.now(); + setClearTime(end - start); + } catch (error) { + console.error("Error during cache clear:", error); + } }, []); return ( diff --git a/packages/idb-cache-app/src/fixtures.ts b/packages/idb-cache-app/src/fixtures.ts new file mode 100644 index 0000000..40a209b --- /dev/null +++ b/packages/idb-cache-app/src/fixtures.ts @@ -0,0 +1,188 @@ +// fixtures.ts + +import { uuid } from "./utils"; + +// Define the types for messages sent to and from the worker +interface WorkerMessage { + requestId: string; + targetSizeInBytes: number; + seed: string; +} + +interface WorkerResponse { + requestId: string; + text: string; +} + +/** + * Generates the Worker code as a string using the Function.prototype.toString() strategy. + * This ensures that the Worker code is self-contained and not transformed by the bundler. + * The worker code is written as a function and then converted to a string. + */ +function generateTextOfSizeWorkerCode(): string { + const workerFunction = () => { + // Define types for internal worker usage + interface WorkerMessage { + requestId: string; + targetSizeInBytes: number; + seed: string; + } + + interface WorkerResponse { + requestId: string; + text: string; + } + + /** + * Utility function to convert a seed string into a numerical hash. + * + * @param str - The seed string to hash. + * @returns A numerical hash derived from the input string. + */ + function hashCode(str: string): number { + let hash = 0; + for (let i = 0; i < str.length; i++) { + hash = (hash * 31 + str.charCodeAt(i)) >>> 0; // Ensure unsigned 32-bit integer + } + return hash; + } + + /** + * Seeded pseudo-random number generator using Linear Congruential Generator (LCG). + * + * @param seed - The seed string to initialize the generator. + * @returns A function that generates a pseudo-random number between 0 (inclusive) and 1 (exclusive). + */ + function seededRandom(seed: string): () => number { + let state: number = hashCode(seed); + const a: number = 1664525; + const c: number = 1013904223; + const m: number = 2 ** 32; + + /** + * Generates the next pseudo-random number in the sequence. + * + * @returns A pseudo-random number between 0 (inclusive) and 1 (exclusive). + */ + function random(): number { + state = (a * state + c) >>> 0; // Update state with LCG formula + return state / m; + } + + return random; + } + + /** + * Calculates the byte size of a string using UTF-8 encoding. + * + * @param str - The string whose byte size is to be calculated. + * @returns The byte size of the input string. + */ + function calculateByteSize(str: string): number { + return new TextEncoder().encode(str).length; + } + + /** + * Listener for messages from the main thread. + * Generates a deterministic random text string based on the provided seed and target size. + */ + self.onmessage = (event: MessageEvent): void => { + const data: WorkerMessage = event.data; + const { requestId, targetSizeInBytes, seed } = data; + + const rand: () => number = seededRandom(seed); + const estimatedChars: number = Math.ceil(targetSizeInBytes); + const charArray: string[] = new Array(estimatedChars); + + for (let i = 0; i < estimatedChars; i++) { + // Generate a random printable ASCII character (codes 33 to 126) + charArray[i] = String.fromCharCode(33 + Math.floor(rand() * 94)); + } + + let result: string = charArray.join(""); + + // Ensure the generated result matches the exact target size + while (calculateByteSize(result) > targetSizeInBytes) { + result = result.slice(0, -1); + } + + const response: WorkerResponse = { requestId, text: result }; + // Send the generated text back to the main thread + postMessage(response); + }; + }; + + // Convert the worker function to a string and invoke it immediately + return `(${workerFunction.toString()})();`; +} + +/** + * Creates a Web Worker from a given code string by converting it to a Blob URL. + * + * @param code The Worker code as a string. + * @returns A new Worker instance. + */ +function createWorkerFromCode(code: string): Worker { + const blob: Blob = new Blob([code], { type: "application/javascript" }); + const blobURL: string = URL.createObjectURL(blob); + return new Worker(blobURL); +} + +/** + * Asynchronously generates a deterministic random text string of a specified byte size + * by offloading the task to a Web Worker. Supports multiple concurrent requests using requestId. + * + * @param targetSizeInBytes The desired byte size of the generated string. + * @param seed Optional seed for the random number generator. Defaults to "default". + * @returns A Promise that resolves to the generated string. + */ +export async function generateTextOfSize( + targetSizeInBytes: number, + seed = "default" +): Promise { + return new Promise((resolve, reject) => { + const requestId: string = uuid(); + + // Generate the worker code and create a new worker + const workerCode: string = generateTextOfSizeWorkerCode(); + const worker: Worker = createWorkerFromCode(workerCode); + + /** + * Handler for messages from the worker. + * Resolves the promise if the response matches the requestId. + */ + const handleMessage = (event: MessageEvent): void => { + const data: WorkerResponse = event.data; + if (data.requestId === requestId) { + resolve(data.text); + cleanup(); + } + }; + + /** + * Handler for errors from the worker. + * Rejects the promise and cleans up the worker. + */ + const handleError = (error: ErrorEvent): void => { + reject(error); + cleanup(); + }; + + /** + * Cleans up event listeners and terminates the worker. + */ + const cleanup = (): void => { + worker.removeEventListener("message", handleMessage); + worker.removeEventListener("error", handleError); + worker.terminate(); + }; + + // Attach event listeners + worker.addEventListener("message", handleMessage); + worker.addEventListener("error", handleError); + + // Send the message with the requestId + const message: WorkerMessage = { requestId, targetSizeInBytes, seed }; + worker.postMessage(message); + }); +} diff --git a/packages/idb-cache-app/src/utils.ts b/packages/idb-cache-app/src/utils.ts index 7235318..d10ed7e 100644 --- a/packages/idb-cache-app/src/utils.ts +++ b/packages/idb-cache-app/src/utils.ts @@ -1,5 +1,3 @@ -import randomSeed from "random-seed"; - export function deterministicHash(str: string): string { let hash = 0; for (let i = 0; i < str.length; i++) { @@ -11,36 +9,21 @@ export function deterministicHash(str: string): string { return Math.abs(hash).toString(36); // Base-36 string for compact representation } -function calculateByteSize(str: string): number { - return new TextEncoder().encode(str).length; -} - -const textCache: Record = {}; - -export function generateTextOfSize( - targetSizeInBytes: number, - seed = "default" -): string { - const cacheKey = `${targetSizeInBytes}-${seed}`; - if (textCache[cacheKey]) { - return textCache[cacheKey]; - } - - const rand = randomSeed.create(seed); - const estimatedChars = Math.ceil(targetSizeInBytes); - const charArray = new Array(estimatedChars); - - for (let i = 0; i < estimatedChars; i++) { - charArray[i] = String.fromCharCode(33 + Math.floor(rand.random() * 94)); // Printable ASCII - } - - let result = charArray.join(""); - - // Ensure the generated result matches the exact target size - while (calculateByteSize(result) > targetSizeInBytes) { - result = result.slice(0, -1); - } - - textCache[cacheKey] = result; - return result; +/** + * Generates a simple UUID (version 4) without using external libraries. + * This function creates a UUID in the format xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx, + * where 'y' is one of [8, 9, A, B]. + * + * @returns A UUID string. + */ +export function uuid(): string { + // Helper function to generate a random hexadecimal digit + const randomHex = (c: string): string => { + const r: number = (Math.random() * 16) | 0; + const v: number = c === "x" ? r : (r & 0x3) | 0x8; + return v.toString(16); + }; + + // Generate the UUID using the helper function + return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, randomHex); } diff --git a/packages/idb-cache-app/tsconfig.json b/packages/idb-cache-app/tsconfig.json index 0f6ef83..2f17813 100644 --- a/packages/idb-cache-app/tsconfig.json +++ b/packages/idb-cache-app/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "noImplicitAny": true, "types": ["node"], "lib": ["DOM", "ES2020"], "jsx": "react-jsx",