Skip to content

Instantly share code, notes, and snippets.

@nandordudas
Created June 26, 2025 04:43
Show Gist options
  • Save nandordudas/cd59a8fda46ba22d72152a686873d1f1 to your computer and use it in GitHub Desktop.
Save nandordudas/cd59a8fda46ba22d72152a686873d1f1 to your computer and use it in GitHub Desktop.
// =============================================================================
// 1. MODERN MUTEX IMPLEMENTATION WITH NATIVE PRIVATE FIELDS & Promise.withResolvers
// =============================================================================
class Mutex {
#locked = false;
#waitQueue: Array<() => void> = [];
async lock(): Promise<void> {
if (!this.#locked) {
this.#locked = true;
return;
}
const { promise, resolve } = Promise.withResolvers<void>();
this.#waitQueue.push(resolve);
return promise;
}
unlock(): void {
if (this.#waitQueue.length > 0) {
const next = this.#waitQueue.shift()!;
next();
} else {
this.#locked = false;
}
}
async withLock<T>(fn: () => Promise<T>): Promise<T> {
await this.lock();
try {
return await fn();
} finally {
this.unlock();
}
}
get isLocked(): boolean {
return this.#locked;
}
get queueLength(): number {
return this.#waitQueue.length;
}
}
// =============================================================================
// 1.1. ADVANCED MUTEX MANAGER USING MAP FOR MULTIPLE NAMED LOCKS
// =============================================================================
class MutexManager {
#mutexes = new Map<string, Mutex>();
getMutex(key: string): Mutex {
if (!this.#mutexes.has(key)) {
this.#mutexes.set(key, new Mutex());
}
return this.#mutexes.get(key)!;
}
async withLock<T>(key: string, fn: () => Promise<T>): Promise<T> {
const mutex = this.getMutex(key);
return await mutex.withLock(fn);
}
deleteMutex(key: string): boolean {
return this.#mutexes.delete(key);
}
clear(): void {
this.#mutexes.clear();
}
get size(): number {
return this.#mutexes.size;
}
keys(): IterableIterator<string> {
return this.#mutexes.keys();
}
}
// =============================================================================
// 1.2. RESOURCE-BASED MUTEX USING WEAKMAP FOR AUTOMATIC CLEANUP
// =============================================================================
class ResourceMutex {
static #mutexMap = new WeakMap<object, Mutex>();
static async withLock<T>(resource: object, fn: () => Promise<T>): Promise<T> {
let mutex = ResourceMutex.#mutexMap.get(resource);
if (!mutex) {
mutex = new Mutex();
ResourceMutex.#mutexMap.set(resource, mutex);
}
return await mutex.withLock(fn);
}
static getMutex(resource: object): Mutex {
let mutex = ResourceMutex.#mutexMap.get(resource);
if (!mutex) {
mutex = new Mutex();
ResourceMutex.#mutexMap.set(resource, mutex);
}
return mutex;
}
}
// =============================================================================
// 2. RACE CONDITION EXAMPLE (PROBLEMATIC CODE)
// =============================================================================
class BankAccount {
private balance = 0;
// PROBLEMATIC: Race condition possible
async withdrawUnsafe(amount: number): Promise<boolean> {
console.log(`Attempting to withdraw $${amount}, current balance: $${this.balance}`);
// Simulate async operation (database check, etc.)
await new Promise(resolve => setTimeout(resolve, 10));
if (this.balance >= amount) {
// Another withdrawal could happen here before we subtract!
await new Promise(resolve => setTimeout(resolve, 5));
this.balance -= amount;
console.log(`Withdrawal successful. New balance: $${this.balance}`);
return true;
}
console.log(`Insufficient funds. Balance: $${this.balance}`);
return false;
}
deposit(amount: number): void {
this.balance += amount;
}
getBalance(): number {
return this.balance;
}
}
// =============================================================================
// 3. RACE CONDITION DEMONSTRATION
// =============================================================================
async function demonstrateRaceCondition() {
console.log("=== RACE CONDITION DEMO ===");
const account = new BankAccount();
account.deposit(100);
console.log(`Initial balance: $${account.getBalance()}`);
// Two concurrent withdrawals of $60 each
// Both might see balance of $100 and both might succeed!
const withdrawal1 = account.withdrawUnsafe(60);
const withdrawal2 = account.withdrawUnsafe(60);
const [result1, result2] = await Promise.all([withdrawal1, withdrawal2]);
console.log(`Withdrawal 1 result: ${result1}`);
console.log(`Withdrawal 2 result: ${result2}`);
console.log(`Final balance: $${account.getBalance()}`);
console.log("Problem: Balance might be negative!\n");
}
// =============================================================================
// 4. THREAD-SAFE VERSION USING MUTEX
// =============================================================================
class SafeBankAccount {
#balance = 0;
#mutex = new Mutex();
// SAFE: Using mutex to prevent race conditions
async withdraw(amount: number): Promise<boolean> {
return await this.#mutex.withLock(async () => {
console.log(`Attempting to withdraw $${amount}, current balance: $${this.#balance}`);
// Simulate async operation
await new Promise(resolve => setTimeout(resolve, 10));
if (this.#balance >= amount) {
await new Promise(resolve => setTimeout(resolve, 5));
this.#balance -= amount;
console.log(`Withdrawal successful. New balance: $${this.#balance}`);
return true;
}
console.log(`Insufficient funds. Balance: $${this.#balance}`);
return false;
});
}
async deposit(amount: number): Promise<void> {
await this.#mutex.withLock(async () => {
this.#balance += amount;
console.log(`Deposited $${amount}. New balance: $${this.#balance}`);
});
}
getBalance(): number {
return this.#balance;
}
}
// =============================================================================
// 5. SAFE VERSION DEMONSTRATION
// =============================================================================
async function demonstrateMutexSolution() {
console.log("=== MUTEX SOLUTION DEMO ===");
const account = new SafeBankAccount();
await account.deposit(100);
console.log(`Initial balance: $${account.getBalance()}`);
// Two concurrent withdrawals - mutex ensures they happen sequentially
const withdrawal1 = account.withdraw(60);
const withdrawal2 = account.withdraw(60);
const [result1, result2] = await Promise.all([withdrawal1, withdrawal2]);
console.log(`Withdrawal 1 result: ${result1}`);
console.log(`Withdrawal 2 result: ${result2}`);
console.log(`Final balance: $${account.getBalance()}`);
console.log("Solution: Only one withdrawal succeeded!\n");
}
// =============================================================================
// 6. ADVANCED TIMED MUTEX WITH MODERN FEATURES
// =============================================================================
class TimedMutex {
#locked = false;
#waitQueue = new Map<number, {
resolve: () => void;
reject: (error: Error) => void;
timeout: NodeJS.Timeout;
}>();
#nextId = 0;
async lock(timeoutMs = 5000): Promise<void> {
if (!this.#locked) {
this.#locked = true;
return;
}
const { promise, resolve, reject } = Promise.withResolvers<void>();
const id = this.#nextId++;
const timeout = setTimeout(() => {
if (this.#waitQueue.has(id)) {
this.#waitQueue.delete(id);
reject(new Error(`Mutex lock timeout after ${timeoutMs}ms`));
}
}, timeoutMs);
this.#waitQueue.set(id, { resolve, reject, timeout });
return promise;
}
unlock(): void {
if (this.#waitQueue.size > 0) {
const [id, waiter] = this.#waitQueue.entries().next().value;
this.#waitQueue.delete(id);
clearTimeout(waiter.timeout);
this.#locked = true; // Keep locked for next waiter
waiter.resolve();
} else {
this.#locked = false;
}
}
async withLock<T>(fn: () => Promise<T>, timeoutMs = 5000): Promise<T> {
await this.lock(timeoutMs);
try {
return await fn();
} finally {
this.unlock();
}
}
get isLocked(): boolean {
return this.#locked;
}
get queueLength(): number {
return this.#waitQueue.size;
}
}
// =============================================================================
// 7. REAL-WORLD EXAMPLE: COUNTER WITH CONCURRENT INCREMENTS
// =============================================================================
class CounterExample {
#count = 0;
#mutex = new Mutex();
// Unsafe increment
async incrementUnsafe(): Promise<number> {
const current = this.#count;
// Simulate some async work
await new Promise(resolve => setTimeout(resolve, 1));
this.#count = current + 1;
return this.#count;
}
// Safe increment using mutex
async incrementSafe(): Promise<number> {
return await this.#mutex.withLock(async () => {
const current = this.#count;
await new Promise(resolve => setTimeout(resolve, 1));
this.#count = current + 1;
return this.#count;
});
}
getCount(): number {
return this.#count;
}
reset(): void {
this.#count = 0;
}
}
// =============================================================================
// 8. DEMONSTRATION FUNCTIONS
// =============================================================================
async function demonstrateCounterRaceCondition() {
console.log("=== COUNTER RACE CONDITION ===");
const counter = new CounterExample();
// Launch 10 concurrent unsafe increments
const promises = Array.from({ length: 10 }, () => counter.incrementUnsafe());
await Promise.all(promises);
console.log(`Expected count: 10, Actual count: ${counter.getCount()}`);
console.log("Race condition likely caused lost updates!\n");
}
async function demonstrateCounterMutexSolution() {
console.log("=== COUNTER MUTEX SOLUTION ===");
const counter = new CounterExample();
// Launch 10 concurrent safe increments
const promises = Array.from({ length: 10 }, () => counter.incrementSafe());
await Promise.all(promises);
console.log(`Expected count: 10, Actual count: ${counter.getCount()}`);
console.log("Mutex ensures all increments are applied!\n");
}
async function demonstrateMutexManager() {
console.log("=== MUTEX MANAGER WITH MAP ===");
const manager = new MutexManager();
let sharedCounter = 0;
// Multiple operations on different named locks
const operations = [
manager.withLock("counter-a", async () => {
const temp = sharedCounter;
await new Promise(resolve => setTimeout(resolve, 10));
sharedCounter = temp + 1;
console.log(`Lock A: counter = ${sharedCounter}`);
}),
manager.withLock("counter-b", async () => {
console.log(`Lock B: independent operation, counter = ${sharedCounter}`);
}),
manager.withLock("counter-a", async () => {
const temp = sharedCounter;
await new Promise(resolve => setTimeout(resolve, 5));
sharedCounter = temp + 1;
console.log(`Lock A: counter = ${sharedCounter}`);
})
];
await Promise.all(operations);
console.log(`Manager has ${manager.size} active mutexes`);
console.log(`Keys: ${Array.from(manager.keys()).join(', ')}\n`);
}
async function demonstrateResourceMutex() {
console.log("=== RESOURCE MUTEX WITH WEAKMAP ===");
const resourceA = { name: "Resource A" };
const resourceB = { name: "Resource B" };
let globalCounter = 0;
// Operations on different resources can run concurrently
// Operations on same resource are serialized
const operations = [
ResourceMutex.withLock(resourceA, async () => {
const temp = globalCounter;
await new Promise(resolve => setTimeout(resolve, 10));
globalCounter = temp + 1;
console.log(`Resource A operation 1: counter = ${globalCounter}`);
}),
ResourceMutex.withLock(resourceB, async () => {
console.log(`Resource B operation: independent, counter = ${globalCounter}`);
}),
ResourceMutex.withLock(resourceA, async () => {
const temp = globalCounter;
await new Promise(resolve => setTimeout(resolve, 5));
globalCounter = temp + 1;
console.log(`Resource A operation 2: counter = ${globalCounter}`);
})
];
await Promise.all(operations);
console.log("WeakMap automatically cleans up when resources are garbage collected\n");
}
async function demonstrateTimedMutex() {
console.log("=== TIMED MUTEX DEMO ===");
const mutex = new TimedMutex();
try {
// First operation gets the lock
const operation1 = mutex.withLock(async () => {
console.log("Operation 1: Got lock, working for 2 seconds...");
await new Promise(resolve => setTimeout(resolve, 2000));
console.log("Operation 1: Finished");
}, 3000);
// Second operation should timeout after 1 second
const operation2 = mutex.withLock(async () => {
console.log("Operation 2: This shouldn't run");
}, 1000);
await Promise.all([operation1, operation2]);
} catch (error) {
console.log(`Expected timeout error: ${error.message}`);
}
console.log("");
}
// =============================================================================
// 9. USAGE EXAMPLES
// =============================================================================
async function runAllExamples() {
await demonstrateRaceCondition();
await demonstrateMutexSolution();
await demonstrateCounterRaceCondition();
await demonstrateCounterMutexSolution();
await demonstrateMutexManager();
await demonstrateResourceMutex();
await demonstrateTimedMutex();
}
// =============================================================================
// 10. ALTERNATIVE APPROACHES
// =============================================================================
// Using Atomic-like operations with proper async patterns
class AtomicCounter {
#count = 0;
#pending = Promise.resolve();
async increment(): Promise<number> {
this.#pending = this.#pending.then(async () => {
await new Promise(resolve => setTimeout(resolve, 1));
return ++this.#count;
});
return this.#pending;
}
getCount(): number {
return this.#count;
}
}
// Using semaphore for limiting concurrent access
class Semaphore {
#permits: number;
#waitQueue: Array<() => void> = [];
constructor(permits: number) {
this.#permits = permits;
}
async acquire(): Promise<void> {
if (this.#permits > 0) {
this.#permits--;
return;
}
const { promise, resolve } = Promise.withResolvers<void>();
this.#waitQueue.push(resolve);
return promise;
}
release(): void {
if (this.#waitQueue.length > 0) {
const next = this.#waitQueue.shift()!;
next();
} else {
this.#permits++;
}
}
get availablePermits(): number {
return this.#permits;
}
get queueLength(): number {
return this.#waitQueue.length;
}
}
// Export for use
export {
Mutex,
TimedMutex,
BankAccount,
SafeBankAccount,
CounterExample,
AtomicCounter,
Semaphore,
runAllExamples
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment