UNPKG

@ugo-code/streamline.js

Version:

A utility module which provides straight-forward, powerful functions for working with asynchronous JavaScript

120 lines (119 loc) 5.66 kB
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; /** * Creates a SHA-256 hash of the given data. This is a robust way to generate a * consistent key from complex data types like objects or arrays. * @param data The data to hash. * @returns A promise that resolves to the hex-encoded hash string. * @internal */ function getHash(data) { return __awaiter(this, void 0, void 0, function* () { let dataString; // Ensure consistent string representation for hashing. if (typeof data === "string") { dataString = data; } else if (data !== null && typeof data === "object") { // Handles arrays and plain objects. May throw on circular references. dataString = JSON.stringify(data); } else { // Handles primitives like number, boolean, null, undefined. dataString = String(data); } const encoder = new TextEncoder(); const encodedData = encoder.encode(dataString); const hashBuffer = yield crypto.subtle.digest("SHA-256", encodedData); const hashArray = Array.from(new Uint8Array(hashBuffer)); // Convert each byte to a 2-character hex string. return hashArray.map((b) => b.toString(16).padStart(2, "0")).join(""); }); } /** * A map to store promises of currently active (in-flight) tasks. * The key is a unique hash, and the value is the promise returned by the task. */ const activeRequests = new Map(); /** * Ensures that an asynchronous task is only executed once at a time for a given key. * * If this function is called while a task with the same key is already running, * it will return the promise of the existing task instead of starting a new one. * This is useful for preventing duplicate network requests or other expensive * operations. Once a task is complete (either resolves or rejects), its promise is * removed, and the next call with the same key will trigger a new execution. * * @template TResult The expected result type of the asynchronous task. * @param {() => Promise<TResult>} taskFn The asynchronous function to execute. * @param {SerializableKey} [key] An optional unique identifier for the task. * If it's an object or array, it will be JSON-stringified and hashed. * If not provided, the task function's source code (`taskFn.toString()`) is * hashed to generate a key. Note that this may not be unique for different * function instances with identical source code. * @returns {Promise<TResult>} A promise that resolves or rejects with the result of the task. * @example * ```ts * // Example 1: Basic deduplication with a string key * async function fetchUser(userId: string) { * // This function will only be executed once, even if called multiple times in parallel. * return singleExecution( * () => { * console.log(`Fetching user ${userId}...`); * return api.fetch(`/users/${userId}`); * }, * `user-${userId}` // A simple, descriptive key * ); * } * * Promise.all([fetchUser('123'), fetchUser('123')]); // "Fetching user 123..." is logged only once. * * // Example 2: Using an object as a key * async function searchProducts(filters: object) { * return singleExecution( * () => api.post('/products/search', filters), * filters // The filters object is hashed to create a unique key * ); * } * * // Example 3: No key provided (hashes the function's source) * const fetchConfig = () => singleExecution(() => api.fetch('/config')); * Promise.all([fetchConfig(), fetchConfig()]); // The config is fetched only once. * ``` */ export function singleExecution(taskFn, key) { return __awaiter(this, void 0, void 0, function* () { // If a key is provided, use it; otherwise, use the function's string representation. const keySource = key !== undefined ? key : taskFn.toString(); const hashedKey = yield getHash(keySource); const existingPromise = activeRequests.get(hashedKey); // If a request with the same key is already pending, return its promise. if (existingPromise) { return existingPromise; } // --- This is the first call for this key --- // Create the promise and store it in the map immediately to handle race conditions. const newPromise = taskFn(); activeRequests.set(hashedKey, newPromise); try { // Await the task's completion. return yield newPromise; } finally { // IMPORTANT: Once the promise settles (resolves or rejects), immediately // remove it from the map. This ensures the next call re-executes the task. // We check if the promise in the map is still the one we created, // preventing a race condition where a new task started before this one finished. if (activeRequests.get(hashedKey) === newPromise) { activeRequests.delete(hashedKey); } } }); }