@azghr/singlet 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +183 -0
- package/package.json +1 -5
package/README.md
CHANGED
|
@@ -342,6 +342,189 @@ wrap(searchUsers, {
|
|
|
342
342
|
});
|
|
343
343
|
```
|
|
344
344
|
|
|
345
|
+
## Real-world examples
|
|
346
|
+
|
|
347
|
+
### React hook for data fetching
|
|
348
|
+
|
|
349
|
+
```ts
|
|
350
|
+
import { wrap } from "@azghr/singlet";
|
|
351
|
+
import { useState, useEffect } from "react";
|
|
352
|
+
|
|
353
|
+
const fetchUser = wrap(
|
|
354
|
+
async (id: string) => {
|
|
355
|
+
const response = await fetch(`/api/users/${id}`);
|
|
356
|
+
if (!response.ok) throw new Error("User not found");
|
|
357
|
+
return response.json();
|
|
358
|
+
},
|
|
359
|
+
{ keyFn: ([id]) => `user:${id}` }
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
function useUser(id: string) {
|
|
363
|
+
const [user, setUser] = useState(null);
|
|
364
|
+
const [loading, setLoading] = useState(false);
|
|
365
|
+
const [error, setError] = useState(null);
|
|
366
|
+
|
|
367
|
+
useEffect(() => {
|
|
368
|
+
let cancelled = false;
|
|
369
|
+
|
|
370
|
+
async function loadUser() {
|
|
371
|
+
setLoading(true);
|
|
372
|
+
setError(null);
|
|
373
|
+
try {
|
|
374
|
+
const data = await fetchUser(id);
|
|
375
|
+
if (!cancelled) setUser(data);
|
|
376
|
+
} catch (err) {
|
|
377
|
+
if (!cancelled) setError(err);
|
|
378
|
+
} finally {
|
|
379
|
+
if (!cancelled) setLoading(false);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
loadUser();
|
|
384
|
+
return () => { cancelled = true; };
|
|
385
|
+
}, [id]);
|
|
386
|
+
|
|
387
|
+
return { user, loading, error };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Multiple components using the same user ID share ONE fetch:
|
|
391
|
+
function UserProfile({ userId }) {
|
|
392
|
+
const { user, loading, error } = useUser(userId);
|
|
393
|
+
if (loading) return <div>Loading...</div>;
|
|
394
|
+
if (error) return <div>Error: {error.message}</div>;
|
|
395
|
+
return <div>{user.name}</div>;
|
|
396
|
+
}
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
### Express middleware for API deduplication
|
|
400
|
+
|
|
401
|
+
```ts
|
|
402
|
+
import { wrap } from "@azghr/singlet";
|
|
403
|
+
import express from "express";
|
|
404
|
+
|
|
405
|
+
const app = express();
|
|
406
|
+
|
|
407
|
+
// Deduplicate database queries
|
|
408
|
+
const fetchUserFromDB = wrap(
|
|
409
|
+
async (id: string) => {
|
|
410
|
+
return await db.query('SELECT * FROM users WHERE id = $1', [id]);
|
|
411
|
+
},
|
|
412
|
+
{ keyFn: ([id]) => `user:db:${id}` }
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
// Middleware for caching expensive API calls
|
|
416
|
+
const externalAPICache = wrap(
|
|
417
|
+
async (endpoint: string) => {
|
|
418
|
+
const response = await fetch(`https://external.api.com${endpoint}`);
|
|
419
|
+
return response.json();
|
|
420
|
+
},
|
|
421
|
+
{ keyFn: ([endpoint]) => `external:${endpoint}` }
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
app.get('/users/:id', async (req, res) => {
|
|
425
|
+
try {
|
|
426
|
+
// If multiple requests hit this endpoint concurrently for the same user,
|
|
427
|
+
// they share ONE database query
|
|
428
|
+
const user = await fetchUserFromDB(req.params.id);
|
|
429
|
+
res.json(user);
|
|
430
|
+
} catch (error) {
|
|
431
|
+
res.status(500).json({ error: 'User not found' });
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
app.get('/proxy/*', async (req, res) => {
|
|
436
|
+
try {
|
|
437
|
+
const endpoint = req.path.replace('/proxy', '');
|
|
438
|
+
const data = await externalAPICache(endpoint);
|
|
439
|
+
res.json(data);
|
|
440
|
+
} catch (error) {
|
|
441
|
+
res.status(502).json({ error: 'Bad gateway' });
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
### Database query coalescing
|
|
447
|
+
|
|
448
|
+
```ts
|
|
449
|
+
import { singlet } from "@azghr/singlet";
|
|
450
|
+
|
|
451
|
+
const flight = singlet();
|
|
452
|
+
|
|
453
|
+
async function getUserWithPosts(userId: string) {
|
|
454
|
+
// These run concurrently but share queries if called multiple times
|
|
455
|
+
const [user, posts] = await Promise.all([
|
|
456
|
+
flight.run(`user:${userId}`, () => db.users.find(userId)),
|
|
457
|
+
flight.run(`posts:user:${userId}`, () => db.posts.findByUser(userId))
|
|
458
|
+
]);
|
|
459
|
+
|
|
460
|
+
return { user, posts };
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Multiple concurrent calls for the same user:
|
|
464
|
+
const [result1, result2, result3] = await Promise.all([
|
|
465
|
+
getUserWithPosts('42'),
|
|
466
|
+
getUserWithPosts('42'),
|
|
467
|
+
getUserWithPosts('42')
|
|
468
|
+
]);
|
|
469
|
+
// Results in only 2 database queries instead of 6
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
### Webhook deduplication
|
|
473
|
+
|
|
474
|
+
```ts
|
|
475
|
+
import { shared, wrapWithKey } from "@azghr/singlet";
|
|
476
|
+
|
|
477
|
+
// Handle duplicate webhook events gracefully
|
|
478
|
+
const processWebhook = wrapWithKey(
|
|
479
|
+
async (event: WebhookEvent) => {
|
|
480
|
+
// Process expensive webhook logic
|
|
481
|
+
await updateDatabase(event);
|
|
482
|
+
await sendNotifications(event);
|
|
483
|
+
await triggerWorkflows(event);
|
|
484
|
+
},
|
|
485
|
+
`webhook:${event.type}:${event.id}`
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
app.post('/webhooks', async (req, res) => {
|
|
489
|
+
const event = req.body;
|
|
490
|
+
|
|
491
|
+
// If duplicate webhooks arrive simultaneously, only process once
|
|
492
|
+
processWebhook(event).catch(err => {
|
|
493
|
+
console.error('Webhook processing failed:', err);
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
res.status(202).json({ received: true });
|
|
497
|
+
});
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
### Configuration loading with fallback
|
|
501
|
+
|
|
502
|
+
```ts
|
|
503
|
+
import { shared } from "@azghr/singlet";
|
|
504
|
+
|
|
505
|
+
async function loadConfigWithFallback() {
|
|
506
|
+
try {
|
|
507
|
+
return await shared.run('app:config', async () => {
|
|
508
|
+
const config = await fetch('/api/config').then(r => r.json());
|
|
509
|
+
return config;
|
|
510
|
+
});
|
|
511
|
+
} catch (error) {
|
|
512
|
+
// If config loading fails, provide defaults
|
|
513
|
+
return {
|
|
514
|
+
theme: 'light',
|
|
515
|
+
language: 'en',
|
|
516
|
+
features: { premium: false }
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Multiple components load config concurrently → ONE fetch
|
|
522
|
+
const [appConfig, adminConfig] = await Promise.all([
|
|
523
|
+
loadConfigWithFallback(),
|
|
524
|
+
loadConfigWithFallback()
|
|
525
|
+
]);
|
|
526
|
+
```
|
|
527
|
+
|
|
345
528
|
## TypeScript
|
|
346
529
|
|
|
347
530
|
Written in strict TypeScript; ships bundled `.d.ts`. All functions infer types from your code:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@azghr/singlet",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Deduplicate concurrent async calls: same key, one in-flight promise. Not a cache.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"singleflight",
|
|
@@ -18,10 +18,6 @@
|
|
|
18
18
|
],
|
|
19
19
|
"license": "MIT",
|
|
20
20
|
"author": "",
|
|
21
|
-
"repository": {
|
|
22
|
-
"type": "git",
|
|
23
|
-
"url": "git+https://github.com/azghr/singlet.git"
|
|
24
|
-
},
|
|
25
21
|
"publishConfig": {
|
|
26
22
|
"access": "public"
|
|
27
23
|
},
|