This commit is contained in:
Colby McHenry
2026-01-18 16:25:00 -06:00
parent 08ccabb5a9
commit cc6e7a5c89
57 changed files with 23315 additions and 1 deletions
+302
View File
@@ -0,0 +1,302 @@
/**
* CodeGraph Utilities
*
* Common utility functions for memory management, concurrency, and batching.
*
* @module utils
*
* @example
* ```typescript
* import { Mutex, processInBatches, MemoryMonitor } from 'codegraph';
*
* // Use mutex for concurrent safety
* const mutex = new Mutex();
* await mutex.withLock(async () => {
* await performCriticalOperation();
* });
*
* // Process items in batches to manage memory
* const results = await processInBatches(items, 100, async (item) => {
* return await processItem(item);
* });
*
* // Monitor memory usage
* const monitor = new MemoryMonitor(512, (usage) => {
* console.warn(`Memory usage exceeded 512MB: ${usage / 1024 / 1024}MB`);
* });
* monitor.start();
* ```
*/
/**
* Process items in batches to manage memory
*
* @param items - Array of items to process
* @param batchSize - Number of items per batch
* @param processor - Function to process each item
* @param onBatchComplete - Optional callback after each batch
* @returns Array of results
*/
export async function processInBatches<T, R>(
items: T[],
batchSize: number,
processor: (item: T, index: number) => Promise<R>,
onBatchComplete?: (completed: number, total: number) => void
): Promise<R[]> {
const results: R[] = [];
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, Math.min(i + batchSize, items.length));
const batchResults = await Promise.all(
batch.map((item, idx) => processor(item, i + idx))
);
results.push(...batchResults);
if (onBatchComplete) {
onBatchComplete(Math.min(i + batchSize, items.length), items.length);
}
// Allow GC between batches
if (global.gc) {
global.gc();
}
}
return results;
}
/**
* Simple mutex lock for preventing concurrent operations
*/
export class Mutex {
private locked = false;
private waitQueue: Array<() => void> = [];
/**
* Acquire the lock
*
* @returns A release function to call when done
*/
async acquire(): Promise<() => void> {
while (this.locked) {
await new Promise<void>((resolve) => {
this.waitQueue.push(resolve);
});
}
this.locked = true;
return () => {
this.locked = false;
const next = this.waitQueue.shift();
if (next) {
next();
}
};
}
/**
* Execute a function while holding the lock
*/
async withLock<T>(fn: () => Promise<T> | T): Promise<T> {
const release = await this.acquire();
try {
return await fn();
} finally {
release();
}
}
/**
* Check if the lock is currently held
*/
isLocked(): boolean {
return this.locked;
}
}
/**
* Chunked file reader for large files
*
* Reads a file in chunks to avoid loading entire file into memory.
*/
export async function* readFileInChunks(
filePath: string,
chunkSize: number = 64 * 1024
): AsyncGenerator<string, void, undefined> {
const fs = await import('fs');
const fd = fs.openSync(filePath, 'r');
const buffer = Buffer.alloc(chunkSize);
try {
let bytesRead: number;
while ((bytesRead = fs.readSync(fd, buffer, 0, chunkSize, null)) > 0) {
yield buffer.toString('utf-8', 0, bytesRead);
}
} finally {
fs.closeSync(fd);
}
}
/**
* Debounce a function
*
* @param fn - Function to debounce
* @param delay - Delay in milliseconds
* @returns Debounced function
*/
export function debounce<T extends (...args: unknown[]) => unknown>(
fn: T,
delay: number
): (...args: Parameters<T>) => void {
let timeoutId: ReturnType<typeof setTimeout> | null = null;
return (...args: Parameters<T>) => {
if (timeoutId) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(() => {
fn(...args);
timeoutId = null;
}, delay);
};
}
/**
* Throttle a function
*
* @param fn - Function to throttle
* @param limit - Minimum time between calls in milliseconds
* @returns Throttled function
*/
export function throttle<T extends (...args: unknown[]) => unknown>(
fn: T,
limit: number
): (...args: Parameters<T>) => void {
let lastCall = 0;
let timeoutId: ReturnType<typeof setTimeout> | null = null;
return (...args: Parameters<T>) => {
const now = Date.now();
const remaining = limit - (now - lastCall);
if (remaining <= 0) {
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
lastCall = now;
fn(...args);
} else if (!timeoutId) {
timeoutId = setTimeout(() => {
lastCall = Date.now();
timeoutId = null;
fn(...args);
}, remaining);
}
};
}
/**
* Estimate memory usage of an object (rough approximation)
*
* @param obj - Object to measure
* @returns Approximate size in bytes
*/
export function estimateSize(obj: unknown): number {
const seen = new WeakSet();
function sizeOf(value: unknown): number {
if (value === null || value === undefined) {
return 0;
}
switch (typeof value) {
case 'boolean':
return 4;
case 'number':
return 8;
case 'string':
return 2 * (value as string).length;
case 'object':
if (seen.has(value as object)) {
return 0;
}
seen.add(value as object);
if (Array.isArray(value)) {
return value.reduce((acc: number, item) => acc + sizeOf(item), 0);
}
return Object.entries(value as object).reduce(
(acc, [key, val]) => acc + sizeOf(key) + sizeOf(val),
0
);
default:
return 0;
}
}
return sizeOf(obj);
}
/**
* Memory monitor for tracking usage during operations
*/
export class MemoryMonitor {
private checkInterval: ReturnType<typeof setInterval> | null = null;
private peakUsage = 0;
private threshold: number;
private onThresholdExceeded?: (usage: number) => void;
constructor(
thresholdMB: number = 500,
onThresholdExceeded?: (usage: number) => void
) {
this.threshold = thresholdMB * 1024 * 1024;
this.onThresholdExceeded = onThresholdExceeded;
}
/**
* Start monitoring memory usage
*/
start(intervalMs: number = 1000): void {
this.stop();
this.peakUsage = 0;
this.checkInterval = setInterval(() => {
const usage = process.memoryUsage().heapUsed;
if (usage > this.peakUsage) {
this.peakUsage = usage;
}
if (usage > this.threshold && this.onThresholdExceeded) {
this.onThresholdExceeded(usage);
}
}, intervalMs);
}
/**
* Stop monitoring
*/
stop(): void {
if (this.checkInterval) {
clearInterval(this.checkInterval);
this.checkInterval = null;
}
}
/**
* Get peak memory usage in bytes
*/
getPeakUsage(): number {
return this.peakUsage;
}
/**
* Get current memory usage in bytes
*/
getCurrentUsage(): number {
return process.memoryUsage().heapUsed;
}
}