Skip to content

Commit f71f376

Browse files
committed
Initial commit
1 parent 3d4c252 commit f71f376

12 files changed

Lines changed: 4277 additions & 0 deletions

.github/workflows/ci.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main, develop]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
test:
11+
name: Test
12+
runs-on: ubuntu-latest
13+
14+
steps:
15+
- name: Checkout repository
16+
uses: actions/checkout@v4
17+
18+
- name: Setup Node.js
19+
uses: actions/setup-node@v4
20+
with:
21+
node-version: 'lts/*'
22+
cache: 'npm'
23+
24+
- name: Install dependencies
25+
run: npm ci
26+
27+
- name: Build project
28+
run: npm run build
29+
30+
- name: Run linter
31+
run: npm run lint
32+
33+
- name: Run unit tests
34+
run: npm test

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.DS_Store
2+
CLAUDE.md
3+
dist/
4+
node_modules/

README.md

Lines changed: 333 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,333 @@
1+
# Critical Section
2+
3+
[![CI](https://github.com/knowledgecode/critical-section/actions/workflows/ci.yml/badge.svg)](https://github.com/knowledgecode/critical-section/actions/workflows/ci.yml)
4+
[![npm](https://img.shields.io/npm/v/critical-section)](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

Comments
 (0)