@azghr/singlet 0.1.1 → 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.
Files changed (2) hide show
  1. package/README.md +183 -0
  2. package/package.json +1 -1
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.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",