Comprehensive async utilities including promises, delays, timers, and promise caching with expiration support. Essential tools for managing asynchronous operations in Fluid Framework applications.
Simple promise-based delay utility for introducing pauses in async operations.
/**
* Returns a Promise that resolves after the specified number of milliseconds
* @param timeMs - Delay time in milliseconds
* @returns Promise that resolves after the delay
* @deprecated Moved to @fluidframework/core-utils
*/
function delay(timeMs: number): Promise<void>;Usage Examples:
import { delay } from "@fluidframework/common-utils";
// Simple delay
await delay(1000); // Wait 1 second
// Retry with delay
async function retryWithDelay<T>(fn: () => Promise<T>, maxRetries: number = 3): Promise<T> {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
if (i === maxRetries - 1) throw error;
await delay(1000 * Math.pow(2, i)); // Exponential backoff
}
}
throw new Error("Max retries exceeded");
}
// Rate limiting
async function processItems<T>(items: T[], processFn: (item: T) => Promise<void>) {
for (const item of items) {
await processFn(item);
await delay(100); // 100ms between items
}
}Promise with external resolve/reject control, useful for bridging callback-based APIs with async/await.
/**
* Promise with external resolve/reject control
* Provides access to resolve and reject functions outside the Promise constructor
*/
class Deferred<T> {
/** The underlying Promise */
readonly promise: Promise<T>;
/** Whether the promise has been resolved or rejected */
readonly isCompleted: boolean;
/**
* Resolves the promise with the given value
* @param value - Value to resolve with (can be T or PromiseLike<T>)
*/
resolve(value: T | PromiseLike<T>): void;
/**
* Rejects the promise with the given reason
* @param error - Rejection reason (typically an Error)
*/
reject(error: any): void;
}Usage Examples:
import { Deferred } from "@fluidframework/common-utils";
// Bridge callback to Promise
function readFileAsync(filename: string): Promise<string> {
const deferred = new Deferred<string>();
fs.readFile(filename, 'utf8', (err, data) => {
if (err) {
deferred.reject(err);
} else {
deferred.resolve(data);
}
});
return deferred.promise;
}
// Manual promise control
const completionSignal = new Deferred<void>();
// Start some async work
processLongRunningTask().then(() => {
completionSignal.resolve();
}).catch(error => {
completionSignal.reject(error);
});
// Wait for completion elsewhere
await completionSignal.promise;
console.log("Task completed!");
// Check completion status
if (!completionSignal.isCompleted) {
console.log("Still waiting...");
}Promise that delays execution until the promise is actually used (then/catch/await).
/**
* Lazy-evaluated Promise implementation
* The executor function is only called when the Promise is first accessed
* Implements the full Promise interface
*/
class LazyPromise<T> implements Promise<T> {
/**
* Creates a new LazyPromise
* @param executor - Function that returns a Promise when called
*/
constructor(executor: () => Promise<T>);
// Promise interface methods
then<TResult1 = T, TResult2 = never>(
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null
): Promise<TResult1 | TResult2>;
catch<TResult = never>(
onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null
): Promise<T | TResult>;
finally(onfinally?: (() => void) | null): Promise<T>;
readonly [Symbol.toStringTag]: string;
}Usage Examples:
import { LazyPromise } from "@fluidframework/common-utils";
// Expensive operation that's only executed when needed
const expensiveData = new LazyPromise(async () => {
console.log("Starting expensive calculation..."); // Only logs when accessed
await delay(5000);
return performExpensiveCalculation();
});
// The calculation hasn't started yet
console.log("LazyPromise created");
// Now the calculation starts
const result = await expensiveData;
console.log("Got result:", result);
// Factory pattern with lazy evaluation
function createLazyResource<T>(factory: () => Promise<T>): LazyPromise<T> {
return new LazyPromise(factory);
}
const lazyDatabase = createLazyResource(async () => {
console.log("Connecting to database...");
return await connectToDatabase();
});
// Database connection only happens when first accessed
const users = await lazyDatabase.then(db => db.getUsers());Specialized cache for async work with expiration and error handling capabilities.
/**
* Cache expiry policy types
* @deprecated Moved to @fluidframework/core-utils
* @internal
*/
type PromiseCacheExpiry =
| { policy: "indefinite" }
| { policy: "absolute"; durationMs: number }
| { policy: "sliding"; durationMs: number };
/**
* Promise cache configuration options
* @deprecated Moved to @fluidframework/core-utils
* @internal
*/
interface PromiseCacheOptions {
/** Common expiration policy for all items added to this cache */
expiry?: PromiseCacheExpiry;
/** If the stored Promise is rejected with a particular error, should the given key be removed? */
removeOnError?: (error: any) => boolean;
}
/**
* Specialized cache for async work with expiration and error handling
* Caches both successful results and failed promises based on configuration
* @deprecated Moved to @fluidframework/core-utils
* @internal
*/
class PromiseCache<TKey, TResult> {
/**
* Create the PromiseCache with the given options, with the following defaults:
* expiry: indefinite, removeOnError: true for all errors
* @param options - Cache configuration options
*/
constructor(options?: PromiseCacheOptions);
/**
* Check if there's anything cached at the given key
* @param key - Cache key
* @returns True if key exists in cache
*/
has(key: TKey): boolean;
/**
* Get the Promise for the given key, or undefined if it's not found.
* Extend expiry if applicable.
* @param key - Cache key
* @returns Cached promise or undefined if not found/expired
*/
get(key: TKey): Promise<TResult> | undefined;
/**
* Remove the Promise for the given key, returning true if it was found and removed
* @param key - Cache key
* @returns True if key was found and removed
*/
remove(key: TKey): boolean;
/**
* Try to add the result of the given asyncFn, without overwriting an existing cache entry at that key.
* Returns false if the cache already contained an entry at that key, and true otherwise.
* @param key - Cache key where to store the async work
* @param asyncWorkFn - The async work to do and store, if not already in progress under the given key
* @returns False if cache already contained entry at key, true otherwise
*/
add(key: TKey, asyncWorkFn: () => Promise<TResult>): boolean;
/**
* Try to add the result of the given asyncFn, without overwriting an existing cache entry at that key.
* Returns a Promise for the added or existing async work being done at that key.
* @param key - Cache key name where to store the async work
* @param asyncWorkFn - The async work to do and store, if not already in progress under the given key
* @returns Promise for the added or existing async work
*/
addOrGet(key: TKey, asyncWorkFn: () => Promise<TResult>): Promise<TResult>;
/**
* Try to add the given value, without overwriting an existing cache entry at that key.
* Returns false if the cache already contained an entry at that key, and true otherwise.
* @param key - Cache key name where to store the value
* @param value - Value to store
* @returns False if cache already contained entry at key, true otherwise
*/
addValue(key: TKey, value: TResult): boolean;
/**
* Try to add the given value, without overwriting an existing cache entry at that key.
* Returns a Promise for the added or existing async work being done at that key.
* @param key - Cache key name where to store the async work
* @param value - Value to store
* @returns Promise for the added or existing async work
*/
addValueOrGet(key: TKey, value: TResult): Promise<TResult>;
}Usage Examples:
import { PromiseCache } from "@fluidframework/common-utils";
// Create cache with error handling
const apiCache = new PromiseCache<string, any>(
(failedKey) => {
console.log(`Removing failed request for: ${failedKey}`);
},
{
expiry: { policy: "absolute", durationMs: 5 * 60 * 1000 } // 5 minutes
}
);
// Cache API calls
async function fetchUserData(userId: string): Promise<User> {
return apiCache.addOrGet(userId, async () => {
console.log(`Fetching user ${userId} from API...`);
const response = await fetch(`/api/users/${userId}`);
return response.json();
});
}
// First call hits the API
const user1 = await fetchUserData("123");
// Second call uses cache
const user1Again = await fetchUserData("123"); // No API call
// Sliding window cache for frequently accessed data
const slideCache = new PromiseCache<string, string>(
(key) => console.log(`Evicted: ${key}`),
{ expiry: { policy: "sliding", durationMs: 30000 } } // 30 seconds
);
// Cache with indefinite expiry (manual removal only)
const permanentCache = new PromiseCache<string, Config>(
(key) => console.log(`Config error: ${key}`),
{ expiry: { policy: "indefinite" } }
);
// Pre-populate cache with known values
await permanentCache.addValue("app-config", getDefaultConfig());
// Complex caching scenario
class DataService {
private cache = new PromiseCache<string, any>(
(key) => this.handleCacheError(key),
{ expiry: { policy: "absolute", durationMs: 60000 } }
);
async getData(key: string): Promise<any> {
return this.cache.addOrGet(key, () => this.fetchFromSource(key));
}
private async fetchFromSource(key: string): Promise<any> {
// Expensive data fetching logic
return { data: `Fetched data for ${key}`, timestamp: Date.now() };
}
private handleCacheError(key: string): void {
console.error(`Cache error for key: ${key}`);
// Could implement retry logic, metrics, etc.
}
}Timer classes and utility functions for handling timeouts with support for very long durations.
/**
* Sets timeouts like the setTimeout function allowing timeouts to exceed the setTimeout's max timeout limit.
* @param timeoutFn - Executed when the timeout expires
* @param timeoutMs - Duration of the timeout in milliseconds
* @param setTimeoutIdFn - Executed to update the timeout if multiple timeouts are required when timeoutMs greater than maxTimeout
* @returns The initial timeout
* @deprecated Moved to @fluidframework/core-utils
* @internal
*/
function setLongTimeout(
timeoutFn: () => void,
timeoutMs: number,
setTimeoutIdFn?: (timeoutId: ReturnType<typeof setTimeout>) => void
): ReturnType<typeof setTimeout>;
/**
* Timer interface for basic timer operations
* @deprecated Moved to @fluidframework/core-utils
* @internal
*/
interface ITimer {
/** True if timer is currently running */
readonly hasTimer: boolean;
/** Starts the timer */
start(): void;
/** Cancels the timer if already running */
clear(): void;
}
/**
* Result interface for promise-based timers
* @deprecated Moved to @fluid-private/client-utils
* @internal
*/
interface IPromiseTimerResult {
timerResult: "timeout" | "cancel";
}
/**
* Timer interface that returns promises for completion
* @deprecated Moved to @fluid-private/client-utils
* @internal
*/
interface IPromiseTimer extends ITimer {
/** Starts the timer and returns a promise that resolves when the timer times out or is canceled */
start(): Promise<IPromiseTimerResult>;
}
/**
* Timer class for managing timeouts with support for long durations
* @deprecated Moved to @fluidframework/core-utils
* @internal
*/
class Timer implements ITimer {
constructor(
defaultTimeout: number,
defaultHandler: () => void,
getCurrentTick?: () => number
);
readonly hasTimer: boolean;
start(ms?: number, handler?: () => void): void;
clear(): void;
restart(ms?: number, handler?: () => void): void;
}
/**
* Promise-based timer that resolves when timeout completes or is canceled
* @deprecated Moved to @fluid-private/client-utils
* @internal
*/
class PromiseTimer implements IPromiseTimer {
constructor(defaultTimeout: number, defaultHandler: () => void);
readonly hasTimer: boolean;
start(ms?: number, handler?: () => void): Promise<IPromiseTimerResult>;
clear(): void;
}Usage Examples:
import { Timer, PromiseTimer, setLongTimeout } from "@fluidframework/common-utils";
// Basic timer with long timeout support
const timer = new Timer(5000, () => console.log("Timer fired!"));
timer.start();
// Override default timeout and handler
timer.start(10000, () => console.log("Custom handler"));
// Promise-based timer
const promiseTimer = new PromiseTimer(3000, () => console.log("Background task"));
const result = await promiseTimer.start();
console.log(`Timer completed with: ${result.timerResult}`);
// Long timeout (exceeds setTimeout's 24.8 day limit)
setLongTimeout(
() => console.log("Very long timeout completed"),
30 * 24 * 60 * 60 * 1000, // 30 days
(timeoutId) => console.log(`Timeout scheduled: ${timeoutId}`)
);
// Timer-based retry mechanism
class RetryTimer {
private timer = new Timer(1000, () => this.retry());
private retries = 0;
private maxRetries = 3;
async executeWithRetry<T>(fn: () => Promise<T>): Promise<T> {
try {
return await fn();
} catch (error) {
if (this.retries < this.maxRetries) {
console.log(`Attempt ${this.retries + 1} failed, retrying...`);
this.retries++;
this.timer.start(1000 * Math.pow(2, this.retries)); // Exponential backoff
throw error; // Re-throw to handle in calling code
} else {
throw new Error(`Failed after ${this.maxRetries} retries`);
}
}
}
private retry(): void {
console.log("Retrying operation...");
}
}import { Deferred, LazyPromise, PromiseCache, delay } from "@fluidframework/common-utils";
// Circuit breaker pattern with cache
class CircuitBreaker<TKey, TResult> {
private cache = new PromiseCache<TKey, TResult>(
(key) => this.onFailure(key),
{ expiry: { policy: "sliding", durationMs: 30000 } }
);
private failureCount = 0;
private lastFailure: number = 0;
async execute(key: TKey, fn: () => Promise<TResult>): Promise<TResult> {
if (this.isCircuitOpen()) {
throw new Error("Circuit breaker is open");
}
return this.cache.addOrGet(key, async () => {
try {
const result = await fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure(key);
throw error;
}
});
}
private isCircuitOpen(): boolean {
return this.failureCount >= 5 && (Date.now() - this.lastFailure) < 60000;
}
private onSuccess(): void {
this.failureCount = 0;
}
private onFailure(key: TKey): void {
this.failureCount++;
this.lastFailure = Date.now();
}
}
// Coordinated async operations
async function coordinatedStartup(): Promise<void> {
const dbReady = new Deferred<void>();
const cacheReady = new Deferred<void>();
// Start services concurrently
const dbInit = new LazyPromise(async () => {
await initializeDatabase();
dbReady.resolve();
});
const cacheInit = new LazyPromise(async () => {
await initializeCache();
cacheReady.resolve();
});
// Start initialization
void dbInit;
void cacheInit;
// Wait for both to complete
await Promise.all([dbReady.promise, cacheReady.promise]);
console.log("All services ready!");
}These async utilities provide the foundation for building robust, scalable asynchronous operations in Fluid Framework applications.