Collection of utility functions for common programming tasks including lazy initialization, disposal patterns, rate limiting, safe JSON parsing, and logging infrastructure.
Helper for lazy-initialized values ensuring single generation and immutability.
/**
* Helper for lazy-initialized values with thread-safe single generation
* Ensures the value is computed only once and becomes immutable thereafter
*/
class Lazy<T> {
/**
* Creates a new Lazy instance
* @param valueGenerator - Function that generates the value when first accessed
*/
constructor(valueGenerator: () => T);
/** Whether the value has been generated yet */
get evaluated(): boolean;
/**
* Gets the lazy value, generating it if this is the first access
* Subsequent calls return the same cached value
*/
get value(): T;
}Usage Examples:
import { Lazy } from "@fluidframework/common-utils";
// Expensive computation that's only done when needed
const expensiveResult = new Lazy(() => {
console.log("Computing expensive result...");
return performExpensiveCalculation();
});
// Value hasn't been computed yet
console.log(expensiveResult.evaluated); // false
// First access triggers computation
const result1 = expensiveResult.value; // Logs "Computing expensive result..."
console.log(expensiveResult.evaluated); // true
// Subsequent accesses return cached value
const result2 = expensiveResult.value; // No logging, returns cached value
console.log(result1 === result2); // true
// Configuration loading
const config = new Lazy(() => {
console.log("Loading configuration...");
return JSON.parse(fs.readFileSync("config.json", "utf8"));
});
// Only loads when first accessed
function getApiUrl(): string {
return config.value.apiUrl;
}
// Database connection
const dbConnection = new Lazy(async () => {
console.log("Establishing database connection...");
return await connectToDatabase();
});
// Can be used with async values too (though the getter itself is synchronous)
// You'd typically wrap this in another async function:
async function getDb() {
return await dbConnection.value;
}
// Factory pattern with lazy initialization
class ServiceFactory {
private static readonly userService = new Lazy(() => new UserService());
private static readonly emailService = new Lazy(() => new EmailService());
private static readonly loggerService = new Lazy(() => new LoggerService());
static getUserService(): UserService {
return this.userService.value;
}
static getEmailService(): EmailService {
return this.emailService.value;
}
static getLogger(): LoggerService {
return this.loggerService.value;
}
}
// Services are only instantiated when first requested
const users = ServiceFactory.getUserService(); // Creates UserService
const sameUsers = ServiceFactory.getUserService(); // Returns same instanceWrapper for functions that only execute if the target object hasn't been disposed.
/**
* Returns a wrapper function that only executes if IDisposable object is not disposed
* @param disposable - Object that implements IDisposable interface
* @returns Function that wraps other functions with disposal checking
* @throws Error if called after disposal
*/
function doIfNotDisposed<TDisposable extends IDisposable>(
disposable: TDisposable
): <TArgs extends any[], TReturn>(
fn: (this: TDisposable, ...args: TArgs) => TReturn
) => (...args: TArgs) => TReturn;
/**
* Standard disposable interface
*/
interface IDisposable {
/** Whether the object has been disposed */
readonly disposed: boolean;
/** Disposes the object and releases resources */
dispose(): void;
}Usage Examples:
import { doIfNotDisposed } from "@fluidframework/common-utils";
// Resource management class
class DatabaseConnection implements IDisposable {
private _disposed = false;
private connection: any;
constructor() {
this.connection = establishConnection();
}
get disposed(): boolean {
return this._disposed;
}
// Wrap methods that shouldn't run after disposal
query = doIfNotDisposed(this)((sql: string) => {
console.log(`Executing query: ${sql}`);
return this.connection.query(sql);
});
transaction = doIfNotDisposed(this)((callback: () => void) => {
console.log("Starting transaction");
this.connection.beginTransaction();
try {
callback();
this.connection.commit();
} catch (error) {
this.connection.rollback();
throw error;
}
});
dispose(): void {
if (!this._disposed) {
this.connection.close();
this._disposed = true;
}
}
}
// Usage
const db = new DatabaseConnection();
// Normal usage
db.query("SELECT * FROM users"); // Works fine
// After disposal
db.dispose();
try {
db.query("SELECT * FROM users"); // Throws error
} catch (error) {
console.error("Operation failed:", error.message);
}
// Event handler cleanup
class EventHandler implements IDisposable {
private _disposed = false;
get disposed(): boolean {
return this._disposed;
}
// Protected event handlers
handleClick = doIfNotDisposed(this)((event: MouseEvent) => {
console.log("Click handled");
this.processClick(event);
});
handleKeypress = doIfNotDisposed(this)((event: KeyboardEvent) => {
console.log("Keypress handled");
this.processKeypress(event);
});
private processClick(event: MouseEvent): void {
// Click processing logic
}
private processKeypress(event: KeyboardEvent): void {
// Keypress processing logic
}
dispose(): void {
this._disposed = true;
// Remove event listeners, etc.
}
}
// Async operation protection
class AsyncService implements IDisposable {
private _disposed = false;
get disposed(): boolean {
return this._disposed;
}
fetchData = doIfNotDisposed(this)(async (url: string) => {
const response = await fetch(url);
return response.json();
});
processData = doIfNotDisposed(this)((data: any) => {
// Processing logic that shouldn't run after disposal
return this.transform(data);
});
private transform(data: any): any {
return { ...data, processed: true };
}
dispose(): void {
this._disposed = true;
}
}Rate limiter for controlling request frequency within time windows.
/**
* Rate limiter for client requests within sliding time windows
* Filters items based on configured maximum count and window size
*/
class RateLimiter {
/**
* Creates a new rate limiter
* @param maxCount - Maximum number of items allowed in the time window
* @param windowSizeInMs - Time window size in milliseconds
*/
constructor(maxCount: number, windowSizeInMs: number);
/**
* Filters items based on rate limiting rules
* Returns only the items that are allowed within the current time window
* @param items - Array of items to filter
* @returns Array containing only the approved items
*/
filter<T>(items: T[]): T[];
}Usage Examples:
import { RateLimiter } from "@fluidframework/common-utils";
// Basic rate limiting
const rateLimiter = new RateLimiter(5, 60000); // 5 requests per minute
// Filter API requests
const requests = [
"request1", "request2", "request3", "request4", "request5",
"request6", "request7", "request8" // These last 3 would be rate limited
];
const allowedRequests = rateLimiter.filter(requests);
console.log(allowedRequests); // ["request1", "request2", "request3", "request4", "request5"]
// Email rate limiting
class EmailService {
private rateLimiter = new RateLimiter(10, 3600000); // 10 emails per hour
async sendEmails(emails: { to: string; subject: string; body: string }[]): Promise<void> {
const allowedEmails = this.rateLimiter.filter(emails);
console.log(`Sending ${allowedEmails.length} of ${emails.length} emails`);
for (const email of allowedEmails) {
await this.sendSingleEmail(email);
}
const rateLimited = emails.length - allowedEmails.length;
if (rateLimited > 0) {
console.log(`${rateLimited} emails were rate limited`);
}
}
private async sendSingleEmail(email: any): Promise<void> {
// Email sending logic
console.log(`Sending email to: ${email.to}`);
}
}
// API endpoint protection
class ApiController {
private rateLimiter = new RateLimiter(100, 60000); // 100 requests per minute
handleBatchRequest(requests: any[]): any[] {
const allowedRequests = this.rateLimiter.filter(requests);
if (allowedRequests.length < requests.length) {
console.warn(`Rate limited ${requests.length - allowedRequests.length} requests`);
}
return allowedRequests.map(req => this.processRequest(req));
}
private processRequest(request: any): any {
return { ...request, processed: true, timestamp: Date.now() };
}
}
// Multi-tier rate limiting
class MultiTierRateLimiter {
private shortTerm = new RateLimiter(10, 60000); // 10 per minute
private longTerm = new RateLimiter(100, 3600000); // 100 per hour
filter<T>(items: T[]): T[] {
// Apply both limits
const shortTermFiltered = this.shortTerm.filter(items);
return this.longTerm.filter(shortTermFiltered);
}
}
const multiLimiter = new MultiTierRateLimiter();
const filteredRequests = multiLimiter.filter(manyRequests);Wrapper for JSON.parse that returns undefined instead of throwing on error.
/**
* Safe wrapper for JSON.parse that returns undefined on parsing errors
* @param json - JSON string to parse
* @returns Parsed object or undefined if parsing fails
*/
function safelyParseJSON(json: string): any;Usage Examples:
import { safelyParseJSON } from "@fluidframework/common-utils";
// Safe parsing without try-catch
const validJson = '{"name": "John", "age": 30}';
const result1 = safelyParseJSON(validJson);
console.log(result1); // { name: "John", age: 30 }
const invalidJson = '{"name": "John", "age": 30'; // Missing closing brace
const result2 = safelyParseJSON(invalidJson);
console.log(result2); // undefined
// Configuration loading with fallback
function loadConfig(configString: string) {
const config = safelyParseJSON(configString);
return config || {
// Default configuration
apiUrl: "http://localhost:3000",
timeout: 5000,
retries: 3
};
}
// API response handling
async function fetchUserData(userId: string) {
const response = await fetch(`/api/users/${userId}`);
const text = await response.text();
const userData = safelyParseJSON(text);
if (!userData) {
console.error("Failed to parse user data response");
return null;
}
return userData;
}
// Batch processing with error resilience
function processJsonItems(jsonStrings: string[]) {
const validItems = jsonStrings
.map(json => safelyParseJSON(json))
.filter(item => item !== undefined);
console.log(`Processed ${validItems.length} of ${jsonStrings.length} items`);
return validItems;
}
// Local storage helper
class SafeLocalStorage {
static getItem<T>(key: string, defaultValue: T): T {
try {
const stored = localStorage.getItem(key);
if (stored === null) return defaultValue;
const parsed = safelyParseJSON(stored);
return parsed !== undefined ? parsed : defaultValue;
} catch {
return defaultValue;
}
}
static setItem<T>(key: string, value: T): boolean {
try {
localStorage.setItem(key, JSON.stringify(value));
return true;
} catch {
return false;
}
}
}
// Usage
const userPrefs = SafeLocalStorage.getItem("userPreferences", {
theme: "light",
language: "en"
});Null logger implementations for telemetry interfaces when logging is disabled.
/**
* Null logger for ITelemetryBaseLogger interface
* Provides no-op implementation for basic telemetry logging
*/
class BaseTelemetryNullLogger {
/** No-op send method that discards all telemetry data */
send(event: any): void;
}
/**
* Null logger for ITelemetryLogger interface
* Provides no-op implementation for all telemetry event types
*/
class TelemetryNullLogger extends BaseTelemetryNullLogger {
/** No-op method for sending generic telemetry events */
sendTelemetryEvent(event: any, properties?: any, measurements?: any): void;
/** No-op method for sending error telemetry events */
sendErrorEvent(event: any, error?: any): void;
/** No-op method for sending performance telemetry events */
sendPerformanceEvent(event: any, measurements?: any): void;
}Usage Examples:
import { TelemetryNullLogger, BaseTelemetryNullLogger } from "@fluidframework/common-utils";
// Conditional telemetry
const logger = process.env.ENABLE_TELEMETRY === "true"
? new RealTelemetryLogger()
: new TelemetryNullLogger();
// Logger works the same way, but null logger discards everything
logger.sendTelemetryEvent("user-action", { action: "click", target: "button" });
logger.sendErrorEvent("api-error", new Error("Network timeout"));
logger.sendPerformanceEvent("page-load", { duration: 1500 });
// Service with optional telemetry
class UserService {
constructor(private logger: ITelemetryLogger = new TelemetryNullLogger()) {}
async createUser(userData: any): Promise<User> {
const startTime = Date.now();
try {
const user = await this.persistUser(userData);
this.logger.sendTelemetryEvent("user-created", {
userId: user.id,
source: "api"
});
this.logger.sendPerformanceEvent("user-creation", {
duration: Date.now() - startTime
});
return user;
} catch (error) {
this.logger.sendErrorEvent("user-creation-failed", error);
throw error;
}
}
private async persistUser(userData: any): Promise<User> {
// User persistence logic
return { id: "123", ...userData };
}
}
// Usage in different environments
const userService = new UserService(
process.env.NODE_ENV === "production"
? new ApplicationInsightsLogger()
: new TelemetryNullLogger()
);import { Lazy, doIfNotDisposed, RateLimiter, safelyParseJSON } from "@fluidframework/common-utils";
// Cached rate-limited service
class CachedApiService implements IDisposable {
private _disposed = false;
private rateLimiter = new RateLimiter(60, 60000); // 60 requests per minute
// Lazy-loaded cache
private cache = new Lazy(() => new Map<string, any>());
get disposed(): boolean {
return this._disposed;
}
// Rate-limited and disposal-protected method
fetchData = doIfNotDisposed(this)(async (urls: string[]) => {
const allowedUrls = this.rateLimiter.filter(urls);
const results = [];
for (const url of allowedUrls) {
// Check cache first
const cached = this.cache.value.get(url);
if (cached) {
results.push(cached);
continue;
}
// Fetch and parse safely
const response = await fetch(url);
const text = await response.text();
const data = safelyParseJSON(text);
if (data) {
this.cache.value.set(url, data);
results.push(data);
}
}
return results;
});
dispose(): void {
this._disposed = true;
this.cache.value.clear();
}
}These utility functions provide essential building blocks for robust, efficient applications with proper resource management and error handling.