|
| 1 | +# Critical Section |
| 2 | + |
| 3 | +[](https://github.com/knowledgecode/critical-section/actions/workflows/ci.yml) |
| 4 | +[](https://www.npmjs.com/package/critical-section) |
| 5 | + |
| 6 | +A lightweight TypeScript/JavaScript library for **object-based mutual exclusion** that treats your domain objects as natural lock identifiers. |
| 7 | + |
| 8 | +## Why Critical Section? |
| 9 | + |
| 10 | +Turn any object into a critical section lock - no string keys, no separate mutex instances to manage: |
| 11 | + |
| 12 | +```typescript |
| 13 | +// Use your existing objects directly as locks |
| 14 | +const user = { id: 123, name: 'John' }; |
| 15 | +await criticalSection.enter(user); |
| 16 | + |
| 17 | +// Global objects work perfectly too |
| 18 | +await criticalSection.enter(document); |
| 19 | +await criticalSection.enter(window); |
| 20 | +``` |
| 21 | + |
| 22 | +## Key Features |
| 23 | + |
| 24 | +- **🎯 Object-centric design**: Your domain objects become the locks themselves |
| 25 | +- **🧠 Intuitive mental model**: One object = one critical section, naturally aligned with OOP |
| 26 | +- **♻️ Automatic cleanup**: WeakMap prevents memory leaks through garbage collection |
| 27 | +- **⚡ Lightweight**: Just 3 methods - `enter()`, `tryEnter()`, `leave()` |
| 28 | +- **🌐 Universal**: Works in Node.js, browsers, and all modern JavaScript environments |
| 29 | +- **🔒 Type-safe**: Full TypeScript support with strict type checking |
| 30 | +- **📦 Zero dependencies**: No external runtime dependencies |
| 31 | + |
| 32 | +## Installation |
| 33 | + |
| 34 | +```bash |
| 35 | +npm install critical-section |
| 36 | +``` |
| 37 | + |
| 38 | +## Quick Start |
| 39 | + |
| 40 | +```typescript |
| 41 | +import { criticalSection } from 'critical-section'; |
| 42 | + |
| 43 | +// Any object becomes a critical section lock |
| 44 | +const database = { host: 'localhost', db: 'users' }; |
| 45 | +const userRecord = { id: 123, email: 'user@example.com' }; |
| 46 | + |
| 47 | +async function updateUser() { |
| 48 | + // Lock the specific user record to prevent race conditions |
| 49 | + const entered = await criticalSection.enter(userRecord); |
| 50 | + |
| 51 | + if (entered) { |
| 52 | + try { |
| 53 | + console.log('Updating user record...'); |
| 54 | + await validateUserData(userRecord); |
| 55 | + await performDatabaseUpdate(userRecord); |
| 56 | + await updateSearchIndex(userRecord); |
| 57 | + // All 3 operations complete atomically - no partial updates |
| 58 | + } finally { |
| 59 | + // Always release the lock |
| 60 | + criticalSection.leave(userRecord); |
| 61 | + } |
| 62 | + } |
| 63 | +} |
| 64 | + |
| 65 | +// Try immediate access without waiting |
| 66 | +async function quickUserCheck() { |
| 67 | + if (criticalSection.tryEnter(userRecord)) { |
| 68 | + try { |
| 69 | + console.log('Quick read operation'); |
| 70 | + const userData = await readUserData(userRecord); |
| 71 | + return userData; |
| 72 | + } finally { |
| 73 | + criticalSection.leave(userRecord); |
| 74 | + } |
| 75 | + } else { |
| 76 | + console.log('User record is busy, using cache...'); |
| 77 | + return await getCachedUserData(userRecord.id); |
| 78 | + } |
| 79 | +} |
| 80 | + |
| 81 | +// Multiple objects = independent locks |
| 82 | +await Promise.all([ |
| 83 | + updateUser(), // Uses userRecord lock |
| 84 | + cleanupDatabase(), // Uses database lock - runs concurrently! |
| 85 | +]); |
| 86 | +``` |
| 87 | + |
| 88 | +## API |
| 89 | + |
| 90 | +### `criticalSection.enter(obj: object, timeout?: number): Promise<boolean>` |
| 91 | + |
| 92 | +Enters a critical section for the given object. If the critical section is already occupied, the promise will wait until it becomes available or times out. |
| 93 | + |
| 94 | +**Parameters:** |
| 95 | + |
| 96 | +- `obj` - Any object to use as the critical section identifier |
| 97 | +- `timeout` (optional) - Maximum time to wait in milliseconds. If not provided, waits indefinitely |
| 98 | + |
| 99 | +**Returns:** |
| 100 | + |
| 101 | +- `Promise<boolean>` - Resolves to `true` when successfully entered, `false` if timeout occurs |
| 102 | + |
| 103 | +**Examples:** |
| 104 | + |
| 105 | +```typescript |
| 106 | +// Wait indefinitely |
| 107 | +const success = await criticalSection.enter(myResource); |
| 108 | +if (success) { |
| 109 | + // Critical section is now entered |
| 110 | +} |
| 111 | + |
| 112 | +// Wait with timeout |
| 113 | +const entered = await criticalSection.enter(myResource, 5000); |
| 114 | +if (entered) { |
| 115 | + // Got access within 5 seconds |
| 116 | +} else { |
| 117 | + // Timeout occurred |
| 118 | +} |
| 119 | +``` |
| 120 | + |
| 121 | +### `criticalSection.tryEnter(obj: object): boolean` |
| 122 | + |
| 123 | +Attempts to enter a critical section immediately without waiting. |
| 124 | + |
| 125 | +**Parameters:** |
| 126 | + |
| 127 | +- `obj` - Any object to use as the critical section identifier |
| 128 | + |
| 129 | +**Returns:** |
| 130 | + |
| 131 | +- `boolean` - `true` if successfully entered, `false` if already occupied |
| 132 | + |
| 133 | +**Example:** |
| 134 | + |
| 135 | +```typescript |
| 136 | +if (criticalSection.tryEnter(myResource)) { |
| 137 | + // Got immediate access |
| 138 | + console.log('Entered critical section'); |
| 139 | +} else { |
| 140 | + // Resource is busy |
| 141 | + console.log('Resource unavailable'); |
| 142 | +} |
| 143 | +``` |
| 144 | + |
| 145 | +### `criticalSection.leave(obj: object): void` |
| 146 | + |
| 147 | +Leaves the critical section for the given object, allowing queued entries to proceed. |
| 148 | + |
| 149 | +**Parameters:** |
| 150 | + |
| 151 | +- `obj` - The object to leave the critical section for |
| 152 | + |
| 153 | +**Example:** |
| 154 | + |
| 155 | +```typescript |
| 156 | +criticalSection.leave(myResource); |
| 157 | +// Critical section is now available for others |
| 158 | +``` |
| 159 | + |
| 160 | +## Usage Patterns |
| 161 | + |
| 162 | +### Protecting Async Operations |
| 163 | + |
| 164 | +```typescript |
| 165 | +const database = { connection: 'db-pool' }; |
| 166 | + |
| 167 | +async function updateUser(userId: string, data: object) { |
| 168 | + const entered = await criticalSection.enter(database); |
| 169 | + |
| 170 | + if (entered) { |
| 171 | + try { |
| 172 | + const user = await db.findUser(userId); |
| 173 | + user.update(data); |
| 174 | + await user.save(); |
| 175 | + } finally { |
| 176 | + criticalSection.leave(database); |
| 177 | + } |
| 178 | + } |
| 179 | +} |
| 180 | +``` |
| 181 | + |
| 182 | +### Browser: Preventing Double-Click Issues |
| 183 | + |
| 184 | +```typescript |
| 185 | +// Problem: Double-clicks can cause duplicate form submissions, |
| 186 | +// leading to duplicate orders, payments, or data corruption |
| 187 | +const submitButton = document.getElementById('submit-btn'); |
| 188 | + |
| 189 | +submitButton.addEventListener('click', async (event) => { |
| 190 | + if (criticalSection.tryEnter(submitButton)) { |
| 191 | + try { |
| 192 | + // These operations must complete as a unit |
| 193 | + await submitForm(); |
| 194 | + showSuccessMessage(); |
| 195 | + } finally { |
| 196 | + criticalSection.leave(submitButton); |
| 197 | + } |
| 198 | + } else { |
| 199 | + // Already processing - prevents duplicate submission |
| 200 | + console.log('Form submission already in progress'); |
| 201 | + } |
| 202 | +}); |
| 203 | +``` |
| 204 | + |
| 205 | +### Browser: Global Object Synchronization |
| 206 | + |
| 207 | +```typescript |
| 208 | +// Use document/window as locks for global operations |
| 209 | +async function updateGlobalTheme(newTheme: string) { |
| 210 | + const entered = await criticalSection.enter(document, 1000); |
| 211 | + if (entered) { |
| 212 | + try { |
| 213 | + document.documentElement.setAttribute('data-theme', newTheme); |
| 214 | + await saveThemeToStorage(newTheme); |
| 215 | + } finally { |
| 216 | + criticalSection.leave(document); |
| 217 | + } |
| 218 | + } |
| 219 | +} |
| 220 | + |
| 221 | +// Prevent overlapping resize operations |
| 222 | +// Without protection: rapid resize events could cause inconsistent UI state |
| 223 | +// (e.g., canvas size updated but layout calculation still pending) |
| 224 | +async function handleWindowResize() { |
| 225 | + if (criticalSection.tryEnter(window)) { |
| 226 | + try { |
| 227 | + // These 3 operations must complete atomically |
| 228 | + await recalculateLayout(); |
| 229 | + await updateCanvasSize(); |
| 230 | + await triggerRedraw(); |
| 231 | + } finally { |
| 232 | + criticalSection.leave(window); |
| 233 | + } |
| 234 | + } |
| 235 | + // Skip if already processing - prevents UI corruption |
| 236 | +} |
| 237 | +``` |
| 238 | + |
| 239 | +### Server: Rate Limiting with tryEnter |
| 240 | + |
| 241 | +```typescript |
| 242 | +const rateLimiter = { endpoint: '/api/heavy-operation' }; |
| 243 | + |
| 244 | +async function handleRequest(req, res) { |
| 245 | + if (criticalSection.tryEnter(rateLimiter)) { |
| 246 | + try { |
| 247 | + // Process the request |
| 248 | + await processHeavyOperation(req, res); |
| 249 | + } finally { |
| 250 | + criticalSection.leave(rateLimiter); |
| 251 | + } |
| 252 | + } else { |
| 253 | + // Too many requests |
| 254 | + res.status(429).send('Rate limit exceeded'); |
| 255 | + } |
| 256 | +} |
| 257 | +``` |
| 258 | + |
| 259 | +### Timeout-based Operations |
| 260 | + |
| 261 | +```typescript |
| 262 | +const slowResource = { name: 'slow-service' }; |
| 263 | + |
| 264 | +async function processWithTimeout() { |
| 265 | + const entered = await criticalSection.enter(slowResource, 3000); |
| 266 | + |
| 267 | + if (entered) { |
| 268 | + try { |
| 269 | + await performSlowOperation(); |
| 270 | + } finally { |
| 271 | + criticalSection.leave(slowResource); |
| 272 | + } |
| 273 | + } else { |
| 274 | + // Handle timeout |
| 275 | + await fallbackOperation(); |
| 276 | + } |
| 277 | +} |
| 278 | +``` |
| 279 | + |
| 280 | +### Queue Management |
| 281 | + |
| 282 | +```typescript |
| 283 | +const printQueue = { printer: 'office-printer' }; |
| 284 | + |
| 285 | +async function printDocument(document: string) { |
| 286 | + console.log(`Queuing document: ${document}`); |
| 287 | + |
| 288 | + // This will wait in line if printer is busy |
| 289 | + const entered = await criticalSection.enter(printQueue); |
| 290 | + |
| 291 | + if (entered) { |
| 292 | + try { |
| 293 | + console.log(`Printing: ${document}`); |
| 294 | + await simulatePrinting(document); |
| 295 | + console.log(`Finished: ${document}`); |
| 296 | + } finally { |
| 297 | + criticalSection.leave(printQueue); |
| 298 | + } |
| 299 | + } |
| 300 | +} |
| 301 | + |
| 302 | +// Multiple documents will print in order |
| 303 | +printDocument('Report A'); |
| 304 | +printDocument('Report B'); |
| 305 | +printDocument('Report C'); |
| 306 | +``` |
| 307 | + |
| 308 | +## How It Works |
| 309 | + |
| 310 | +Uses a `WeakMap` to associate objects with their critical section state. Different objects = independent locks. When objects are garbage collected, their critical section state is automatically cleaned up. |
| 311 | + |
| 312 | +```typescript |
| 313 | +const fileA = { path: '/tmp/fileA.txt' }; |
| 314 | +const fileB = { path: '/tmp/fileB.txt' }; |
| 315 | + |
| 316 | +await criticalSection.enter(fileA); // ✅ Acquired |
| 317 | +await criticalSection.enter(fileB); // ✅ Also acquired (different object) |
| 318 | +``` |
| 319 | + |
| 320 | +## Compatibility |
| 321 | + |
| 322 | +- **TypeScript**: Full type definitions included |
| 323 | +- **Browsers**: Chrome 36+, Firefox 6+, Safari 7.1+, Edge 12+ |
| 324 | +- **Node.js**: 12.0+ |
| 325 | +- **Modules**: ES modules, CommonJS, all modern bundlers |
| 326 | + |
| 327 | +## License |
| 328 | + |
| 329 | +MIT |
| 330 | + |
| 331 | +## Contributing |
| 332 | + |
| 333 | +Contributions are welcome! Please feel free to submit a Pull Request. |
0 commit comments