@azghr/singlet 0.1.3 → 0.1.4
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 +214 -0
- package/dist/index.cjs +59 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +78 -1
- package/dist/index.d.ts +78 -1
- package/dist/index.js +56 -0
- package/dist/index.js.map +1 -1
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -96,6 +96,19 @@ import { shared } from "@azghr/singlet";
|
|
|
96
96
|
const config = await shared.run("app:config", loadConfig);
|
|
97
97
|
```
|
|
98
98
|
|
|
99
|
+
### 4. Key generation utilities
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
import { createKey, createKeyScope } from "@azghr/singlet";
|
|
103
|
+
|
|
104
|
+
// Create structured deduplication keys
|
|
105
|
+
const userKey = createKey("user", { id: "42", organization: "acme" });
|
|
106
|
+
const cacheKey = createKeyScope("cache", "posts", "42");
|
|
107
|
+
|
|
108
|
+
// Use with any singlet instance
|
|
109
|
+
await flight.run(userKey, () => fetchUser("42"));
|
|
110
|
+
```
|
|
111
|
+
|
|
99
112
|
## Use
|
|
100
113
|
|
|
101
114
|
### Core API: `singlet()`
|
|
@@ -208,6 +221,52 @@ wasn't in flight.
|
|
|
208
221
|
flight.forget(`user:${userId}`);
|
|
209
222
|
```
|
|
210
223
|
|
|
224
|
+
#### `Singlet.forgetMultiple(keys: string[]): number`
|
|
225
|
+
|
|
226
|
+
Drops multiple in-flight keys so the *next* calls start fresh. Returns the count
|
|
227
|
+
of keys that were actually in flight. Callers already awaiting old promises
|
|
228
|
+
still receive their results.
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
// Batch invalidate multiple user fetches:
|
|
232
|
+
const count = flight.forgetMultiple([`user:1`, `user:2`, `user:3`]);
|
|
233
|
+
console.log(`Forgot ${count} in-flight user fetches`);
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
#### `Singlet.forgetAll(): number`
|
|
237
|
+
|
|
238
|
+
Drops ALL in-flight keys so all *next* calls start fresh. Returns the count
|
|
239
|
+
of keys that were in flight. Useful for cleanup or invalidation scenarios.
|
|
240
|
+
|
|
241
|
+
```ts
|
|
242
|
+
// Clear all pending operations:
|
|
243
|
+
const count = flight.forgetAll();
|
|
244
|
+
console.log(`Cleared ${count} in-flight operations`);
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
#### `Singlet.getInFlightKeys(): string[]`
|
|
248
|
+
|
|
249
|
+
Returns a snapshot of all keys currently in flight. Useful for monitoring,
|
|
250
|
+
debugging, and operational insight.
|
|
251
|
+
|
|
252
|
+
```ts
|
|
253
|
+
const keys = flight.getInFlightKeys();
|
|
254
|
+
console.log(`In-flight operations:`, keys);
|
|
255
|
+
// → ["user:42", "posts:42", "config"]
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
#### `Singlet.getAwaitersCount(key: string): number`
|
|
259
|
+
|
|
260
|
+
Returns the number of concurrent awaiters for a specific key. Returns `0` if
|
|
261
|
+
the key is not in flight. Useful for understanding request multiplicity.
|
|
262
|
+
|
|
263
|
+
```ts
|
|
264
|
+
const awaiters = flight.getAwaitersCount(`user:${id}`);
|
|
265
|
+
if (awaiters > 0) {
|
|
266
|
+
console.log(`${awaiters} callers are waiting for user ${id}`);
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
211
270
|
#### `Singlet.isInFlight(key: string): boolean` · `Singlet.size: number`
|
|
212
271
|
|
|
213
272
|
Introspection for tests, metrics, and debugging.
|
|
@@ -264,6 +323,57 @@ import { shared } from "@azghr/singlet";
|
|
|
264
323
|
await shared.run("config", loadConfig);
|
|
265
324
|
```
|
|
266
325
|
|
|
326
|
+
### Key generation utilities
|
|
327
|
+
|
|
328
|
+
#### `createKey(base: string, params: Record<string, unknown>): string`
|
|
329
|
+
|
|
330
|
+
Create consistent deduplication keys from a base and parameters. Automatically
|
|
331
|
+
handles object serialization with JSON.stringify and skips null/undefined values.
|
|
332
|
+
|
|
333
|
+
```ts
|
|
334
|
+
import { createKey } from "@azghr/singlet";
|
|
335
|
+
|
|
336
|
+
const key = createKey("user", { id: "42" });
|
|
337
|
+
// → "user:id:42"
|
|
338
|
+
|
|
339
|
+
const complexKey = createKey("api", {
|
|
340
|
+
endpoint: "/users",
|
|
341
|
+
method: "GET",
|
|
342
|
+
filter: { active: true }
|
|
343
|
+
});
|
|
344
|
+
// → "api:endpoint:/users:method:GET:filter:{\"active\":true}"
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
#### `createKeyScope(...parts: string[]): string`
|
|
348
|
+
|
|
349
|
+
Create namespaced deduplication keys by joining parts with colons. Useful for
|
|
350
|
+
hierarchical key organization.
|
|
351
|
+
|
|
352
|
+
```ts
|
|
353
|
+
import { createKeyScope } from "@azghr/singlet";
|
|
354
|
+
|
|
355
|
+
const key = createKeyScope("api", "users", "42");
|
|
356
|
+
// → "api:users:42"
|
|
357
|
+
|
|
358
|
+
const scopedKey = createKeyScope("cache", "user", "profile", "settings");
|
|
359
|
+
// → "cache:user:profile:settings"
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
#### `createCompoundKey(base: string, params: Record<string, unknown>): string`
|
|
363
|
+
|
|
364
|
+
Create compound keys from multiple sources (alias for createKey). Useful for
|
|
365
|
+
combining different aspects of a request into one key.
|
|
366
|
+
|
|
367
|
+
```ts
|
|
368
|
+
import { createCompoundKey } from "@azghr/singlet";
|
|
369
|
+
|
|
370
|
+
const key = createCompoundKey("request", {
|
|
371
|
+
userId: "42",
|
|
372
|
+
organizationId: "10"
|
|
373
|
+
});
|
|
374
|
+
// → "request:userId:42:organizationId:10"
|
|
375
|
+
```
|
|
376
|
+
|
|
267
377
|
## Patterns & composition
|
|
268
378
|
|
|
269
379
|
### Deduplication + caching
|
|
@@ -525,6 +635,110 @@ const [appConfig, adminConfig] = await Promise.all([
|
|
|
525
635
|
]);
|
|
526
636
|
```
|
|
527
637
|
|
|
638
|
+
### Batch invalidation with forgetMultiple
|
|
639
|
+
|
|
640
|
+
```ts
|
|
641
|
+
import { singlet } from "@azghr/singlet";
|
|
642
|
+
|
|
643
|
+
const flight = singlet();
|
|
644
|
+
|
|
645
|
+
// Multiple user fetches in flight
|
|
646
|
+
const userPromises = [1, 2, 3, 4, 5].map(id =>
|
|
647
|
+
flight.run(`user:${id}`, () => fetchUser(id))
|
|
648
|
+
);
|
|
649
|
+
|
|
650
|
+
// Invalidate specific users to force fresh fetches
|
|
651
|
+
const invalidated = flight.forgetMultiple(['user:1', 'user:2', 'user:3']);
|
|
652
|
+
console.log(`Invalidated ${invalidated} user fetches`);
|
|
653
|
+
|
|
654
|
+
// Next calls for these users will start fresh
|
|
655
|
+
const freshUsers = await Promise.all([
|
|
656
|
+
flight.run('user:1', () => fetchUser(1)), // Fresh fetch
|
|
657
|
+
flight.run('user:2', () => fetchUser(2)) // Fresh fetch
|
|
658
|
+
]);
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
### Bulk cleanup with forgetAll
|
|
662
|
+
|
|
663
|
+
```ts
|
|
664
|
+
import { singlet, createKey } from "@azghr/singlet";
|
|
665
|
+
|
|
666
|
+
const flight = singlet();
|
|
667
|
+
|
|
668
|
+
// Multiple cache operations in flight
|
|
669
|
+
const cacheKeys = ['posts', 'users', 'comments', 'likes'].map(resource =>
|
|
670
|
+
flight.run(createKey('cache', { resource }), () => loadResource(resource))
|
|
671
|
+
);
|
|
672
|
+
|
|
673
|
+
// Force reset of all cache operations
|
|
674
|
+
const cleared = flight.forgetAll();
|
|
675
|
+
console.log(`Cleared ${cleared} cache operations`);
|
|
676
|
+
|
|
677
|
+
// All subsequent cache calls will be fresh
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
### Monitoring with introspection API
|
|
681
|
+
|
|
682
|
+
```ts
|
|
683
|
+
import { singlet } from "@azghr/singlet";
|
|
684
|
+
|
|
685
|
+
const flight = singlet();
|
|
686
|
+
|
|
687
|
+
// Start some operations
|
|
688
|
+
flight.run('user:42', () => fetchUser(42));
|
|
689
|
+
flight.run('posts:42', () => fetchPosts(42));
|
|
690
|
+
flight.run('config', () => loadConfig());
|
|
691
|
+
|
|
692
|
+
// Monitor what's in flight
|
|
693
|
+
const inFlightKeys = flight.getInFlightKeys();
|
|
694
|
+
console.log('Active operations:', inFlightKeys);
|
|
695
|
+
// → ["user:42", "posts:42", "config"]
|
|
696
|
+
|
|
697
|
+
// Check specific key status
|
|
698
|
+
if (flight.isInFlight('user:42')) {
|
|
699
|
+
const awaiters = flight.getAwaitersCount('user:42');
|
|
700
|
+
console.log(`${awaiters} caller(s) waiting for user:42`);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// Get overall system state
|
|
704
|
+
console.log(`Total in-flight operations: ${flight.size}`);
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
### Dynamic key generation with utilities
|
|
708
|
+
|
|
709
|
+
```ts
|
|
710
|
+
import { singlet, createKey, createKeyScope } from "@azghr/singlet";
|
|
711
|
+
|
|
712
|
+
const flight = singlet();
|
|
713
|
+
|
|
714
|
+
// API endpoint deduplication with structured keys
|
|
715
|
+
async function fetchAPI(endpoint: string, params: Record<string, unknown>) {
|
|
716
|
+
const key = createKey('api', { endpoint, ...params });
|
|
717
|
+
return flight.run(key, () =>
|
|
718
|
+
fetch(`/api/${endpoint}?${new URLSearchParams(params as any)}`).then(r => r.json())
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// Namespaced cache keys
|
|
723
|
+
const userKey = createKeyScope('cache', 'user', '42');
|
|
724
|
+
const postsKey = createKeyScope('cache', 'posts', '42');
|
|
725
|
+
|
|
726
|
+
await Promise.all([
|
|
727
|
+
flight.run(userKey, () => loadUser(42)),
|
|
728
|
+
flight.run(postsKey, () => loadPosts(42))
|
|
729
|
+
]);
|
|
730
|
+
|
|
731
|
+
// Complex query keys
|
|
732
|
+
const searchKey = createKey('search', {
|
|
733
|
+
query: 'typescript',
|
|
734
|
+
filters: { language: 'en', sort: 'relevance' },
|
|
735
|
+
page: 1,
|
|
736
|
+
limit: 20
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
await flight.run(searchKey, () => performSearch(searchKey));
|
|
740
|
+
```
|
|
741
|
+
|
|
528
742
|
## TypeScript
|
|
529
743
|
|
|
530
744
|
Written in strict TypeScript; ships bundled `.d.ts`. All functions infer types from your code:
|
package/dist/index.cjs
CHANGED
|
@@ -20,6 +20,9 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
createCompoundKey: () => createCompoundKey,
|
|
24
|
+
createKey: () => createKey,
|
|
25
|
+
createKeyScope: () => createKeyScope,
|
|
23
26
|
default: () => singlet_default,
|
|
24
27
|
shared: () => shared,
|
|
25
28
|
singlet: () => singlet,
|
|
@@ -48,9 +51,29 @@ function singlet() {
|
|
|
48
51
|
forget(key) {
|
|
49
52
|
return inflight.delete(key);
|
|
50
53
|
},
|
|
54
|
+
forgetMultiple(keys) {
|
|
55
|
+
let count = 0;
|
|
56
|
+
for (const key of keys) {
|
|
57
|
+
if (inflight.delete(key)) {
|
|
58
|
+
count++;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return count;
|
|
62
|
+
},
|
|
63
|
+
forgetAll() {
|
|
64
|
+
const count = inflight.size;
|
|
65
|
+
inflight.clear();
|
|
66
|
+
return count;
|
|
67
|
+
},
|
|
51
68
|
isInFlight(key) {
|
|
52
69
|
return inflight.has(key);
|
|
53
70
|
},
|
|
71
|
+
getInFlightKeys() {
|
|
72
|
+
return Array.from(inflight.keys());
|
|
73
|
+
},
|
|
74
|
+
getAwaitersCount(key) {
|
|
75
|
+
return this.isInFlight(key) ? 1 : 0;
|
|
76
|
+
},
|
|
54
77
|
get size() {
|
|
55
78
|
return inflight.size;
|
|
56
79
|
}
|
|
@@ -74,8 +97,44 @@ function wrapWithKey(fn, key, singlet2) {
|
|
|
74
97
|
}
|
|
75
98
|
return wrap(fn, options);
|
|
76
99
|
}
|
|
100
|
+
|
|
101
|
+
// src/keys.ts
|
|
102
|
+
function createKey(base, params) {
|
|
103
|
+
const parts = [base];
|
|
104
|
+
for (const [key, value] of Object.entries(params)) {
|
|
105
|
+
if (value === void 0 || value === null) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const serializedKey = key;
|
|
109
|
+
let serializedValue = "";
|
|
110
|
+
switch (typeof value) {
|
|
111
|
+
case "object":
|
|
112
|
+
case "function":
|
|
113
|
+
serializedValue = JSON.stringify(value);
|
|
114
|
+
break;
|
|
115
|
+
case "string":
|
|
116
|
+
case "number":
|
|
117
|
+
case "boolean":
|
|
118
|
+
case "bigint":
|
|
119
|
+
case "symbol":
|
|
120
|
+
serializedValue = String(value);
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
parts.push(`${serializedKey}:${serializedValue}`);
|
|
124
|
+
}
|
|
125
|
+
return parts.join(":");
|
|
126
|
+
}
|
|
127
|
+
function createKeyScope(...parts) {
|
|
128
|
+
return parts.join(":");
|
|
129
|
+
}
|
|
130
|
+
function createCompoundKey(base, params) {
|
|
131
|
+
return createKey(base, params);
|
|
132
|
+
}
|
|
77
133
|
// Annotate the CommonJS export names for ESM import in node:
|
|
78
134
|
0 && (module.exports = {
|
|
135
|
+
createCompoundKey,
|
|
136
|
+
createKey,
|
|
137
|
+
createKeyScope,
|
|
79
138
|
shared,
|
|
80
139
|
singlet,
|
|
81
140
|
wrap,
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/singlet.ts","../src/wrap.ts"],"sourcesContent":["/**\n * singlet — deduplicate concurrent async calls.\n *\n * Barrel export: re-exports all public API.\n * No logic here — see src/singlet.ts and src/wrap.ts for implementation.\n */\n\nexport type { Singlet } from \"./singlet.js\";\nexport { singlet, shared } from \"./singlet.js\";\nexport { default } from \"./singlet.js\";\n\nexport type { WrapOptions } from \"./wrap.js\";\nexport { wrap, wrapWithKey } from \"./wrap.js\";\n","/**\n * singlet — deduplicate concurrent async calls.\n *\n * Concurrent calls with the same key share ONE in-flight promise.\n * Once the promise settles, the key is forgotten. singlet is NOT a cache:\n * it stores nothing after completion. One job, done well.\n */\n\nexport interface Singlet {\n /**\n * Run `fn` under `key`. If a call with the same key is already in flight,\n * the existing promise is returned and `fn` is NOT invoked.\n */\n run<T>(key: string, fn: () => T | Promise<T>): Promise<T>;\n\n /**\n * Drop an in-flight key so the NEXT call starts fresh.\n * Callers already awaiting the old promise still receive its result.\n * Returns true if a key was dropped.\n */\n forget(key: string): boolean;\n\n /** Whether a call for `key` is currently in flight. */\n isInFlight(key: string): boolean;\n\n /** Number of distinct keys currently in flight. */\n readonly size: number;\n}\n\n/**\n * Create an isolated singlet instance (its own key namespace).\n */\nexport function singlet(): Singlet {\n const inflight = new Map<string, Promise<unknown>>();\n\n return {\n run<T>(key: string, fn: () => T | Promise<T>): Promise<T> {\n const existing = inflight.get(key);\n if (existing !== undefined) {\n return existing as Promise<T>;\n }\n\n // `fn` is invoked in a microtask via .then(), AFTER the map entry is\n // set, so synchronous throws inside `fn` reject the shared promise\n // instead of escaping to only the first caller. The key is forgotten\n // in `finally`, which runs BEFORE the promise settles for consumers —\n // so by the time any caller's `await` resolves, the key is already\n // gone and a sequential call starts fresh (no caching, not even for\n // one microtask). The identity check guards against `forget()` having\n // replaced the entry with a newer promise for the same key.\n const promise: Promise<T> = Promise.resolve()\n .then(fn)\n .finally(() => {\n if (inflight.get(key) === promise) {\n inflight.delete(key);\n }\n });\n\n inflight.set(key, promise);\n return promise;\n },\n\n forget(key: string): boolean {\n return inflight.delete(key);\n },\n\n isInFlight(key: string): boolean {\n return inflight.has(key);\n },\n\n get size(): number {\n return inflight.size;\n },\n };\n}\n\n/**\n * Shared default instance — convenient for app-wide deduplication.\n * Prefer `singlet()` instances in libraries to avoid key collisions.\n */\nexport const shared: Singlet = singlet();\n\n// Default export for ESM compatibility\nexport default singlet;\n","/**\n * Ergonomic layer over singlet core.\n *\n * This module COMPOSES the core singlet functionality — it never duplicates logic.\n * See src/singlet.ts for the core implementation.\n */\n\nimport type { Singlet } from \"./singlet.js\";\nimport { singlet as createSinglet } from \"./singlet.js\";\n\n/**\n * Options for configuring a wrapped function.\n */\nexport interface WrapOptions<TArgs> {\n /**\n * Extract a deduplication key from function arguments.\n * Return the same key for concurrent calls that should share one promise.\n *\n * @example\n * ```ts\n * // Dedupe by user ID:\n * keyFn: ([id]) => `user:${id}`\n *\n * // Dedupe by multiple args:\n * keyFn: ([method, url]) => `${method}:${url}`\n * ```\n */\n keyFn: (args: TArgs) => string;\n\n /**\n * Optional singlet instance for this wrapped function.\n * Defaults to a new isolated instance.\n * Use this to share a singlet across multiple wrapped functions.\n */\n singlet?: Singlet;\n}\n\n/**\n * Wrap an async function to automatically deduplicate concurrent calls.\n *\n * @example\n * ```ts\n * import { wrap } from 'singlet';\n *\n * const fetchUser = wrap(\n * async (id: string) => fetch(`/api/users/${id}`).then(r => r.json()),\n * { keyFn: ([id]) => `user:${id}` }\n * );\n *\n * // These two concurrent calls share ONE fetch:\n * const [user1, user2] = await Promise.all([\n * fetchUser('42'),\n * fetchUser('42')\n * ]);\n * ```\n *\n * @param fn - The async function to wrap\n * @param options - Configuration for key extraction and singlet instance\n * @returns A wrapped function with automatic deduplication\n */\nexport function wrap<TArgs extends unknown[], TResult>(\n fn: (...args: TArgs) => TResult | Promise<TResult>,\n options: WrapOptions<TArgs>\n): (...args: TArgs) => Promise<TResult> {\n const instance = options.singlet ?? createSinglet();\n\n return function (...args: TArgs): Promise<TResult> {\n const key = options.keyFn(args);\n return instance.run(key, () => fn(...args));\n };\n}\n\n/**\n * Wrap an async function with a simple key string (for single-operation functions).\n *\n * @example\n * ```ts\n * import { wrapWithKey } from 'singlet';\n *\n * const loadConfig = wrapWithKey(\n * async () => fetch('/api/config').then(r => r.json()),\n * 'config'\n * );\n * ```\n *\n * @param fn - The async function to wrap\n * @param key - Fixed key for all calls to this function\n * @param singlet - Optional singlet instance\n * @returns A wrapped function with fixed-key deduplication\n */\nexport function wrapWithKey<TResult>(\n fn: () => TResult | Promise<TResult>,\n key: string,\n singlet?: Singlet\n): () => Promise<TResult> {\n const options: WrapOptions<[]> = { keyFn: () => key };\n if (singlet !== undefined) {\n options.singlet = singlet;\n }\n return wrap(fn, options);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACgCO,SAAS,UAAmB;AACjC,QAAM,WAAW,oBAAI,IAA8B;AAEnD,SAAO;AAAA,IACL,IAAO,KAAa,IAAsC;AACxD,YAAM,WAAW,SAAS,IAAI,GAAG;AACjC,UAAI,aAAa,QAAW;AAC1B,eAAO;AAAA,MACT;AAUA,YAAM,UAAsB,QAAQ,QAAQ,EACzC,KAAK,EAAE,EACP,QAAQ,MAAM;AACb,YAAI,SAAS,IAAI,GAAG,MAAM,SAAS;AACjC,mBAAS,OAAO,GAAG;AAAA,QACrB;AAAA,MACF,CAAC;AAEH,eAAS,IAAI,KAAK,OAAO;AACzB,aAAO;AAAA,IACT;AAAA,IAEA,OAAO,KAAsB;AAC3B,aAAO,SAAS,OAAO,GAAG;AAAA,IAC5B;AAAA,IAEA,WAAW,KAAsB;AAC/B,aAAO,SAAS,IAAI,GAAG;AAAA,IACzB;AAAA,IAEA,IAAI,OAAe;AACjB,aAAO,SAAS;AAAA,IAClB;AAAA,EACF;AACF;AAMO,IAAM,SAAkB,QAAQ;AAGvC,IAAO,kBAAQ;;;ACvBR,SAAS,KACd,IACA,SACsC;AACtC,QAAM,WAAW,QAAQ,WAAW,QAAc;AAElD,SAAO,YAAa,MAA+B;AACjD,UAAM,MAAM,QAAQ,MAAM,IAAI;AAC9B,WAAO,SAAS,IAAI,KAAK,MAAM,GAAG,GAAG,IAAI,CAAC;AAAA,EAC5C;AACF;AAoBO,SAAS,YACd,IACA,KACAA,UACwB;AACxB,QAAM,UAA2B,EAAE,OAAO,MAAM,IAAI;AACpD,MAAIA,aAAY,QAAW;AACzB,YAAQ,UAAUA;AAAA,EACpB;AACA,SAAO,KAAK,IAAI,OAAO;AACzB;","names":["singlet"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/singlet.ts","../src/wrap.ts","../src/keys.ts"],"sourcesContent":["/**\n * singlet — deduplicate concurrent async calls.\n *\n * Barrel export: re-exports all public API.\n * No logic here — see src/singlet.ts, src/wrap.ts, and src/keys.ts for implementation.\n */\n\nexport type { Singlet } from \"./singlet.js\";\nexport { singlet, shared } from \"./singlet.js\";\nexport { default } from \"./singlet.js\";\n\nexport type { WrapOptions } from \"./wrap.js\";\nexport { wrap, wrapWithKey } from \"./wrap.js\";\n\nexport { createKey, createKeyScope, createCompoundKey } from \"./keys.js\";\n","/**\n * singlet — deduplicate concurrent async calls.\n *\n * Concurrent calls with the same key share ONE in-flight promise.\n * Once the promise settles, the key is forgotten. singlet is NOT a cache:\n * it stores nothing after completion. One job, done well.\n */\n\nexport interface Singlet {\n /**\n * Run `fn` under `key`. If a call with the same key is already in flight,\n * the existing promise is returned and `fn` is NOT invoked.\n */\n run<T>(key: string, fn: () => T | Promise<T>): Promise<T>;\n\n /**\n * Drop an in-flight key so the NEXT call starts fresh.\n * Callers already awaiting the old promise still receive its result.\n * Returns true if a key was dropped.\n */\n forget(key: string): boolean;\n\n /**\n * Drop multiple in-flight keys so the NEXT calls start fresh.\n * Callers already awaiting the old promises still receive their results.\n * Returns the number of keys that were in flight.\n */\n forgetMultiple(keys: string[]): number;\n\n /**\n * Drop ALL in-flight keys so all NEXT calls start fresh.\n * Callers already awaiting old promises still receive their results.\n * Returns the number of keys that were in flight.\n */\n forgetAll(): number;\n\n /** Whether a call for `key` is currently in flight. */\n isInFlight(key: string): boolean;\n\n /**\n * Get all keys currently in flight.\n * Returns a snapshot of current keys (may change immediately).\n */\n getInFlightKeys(): string[];\n\n /**\n * Get the number of concurrent callers awaiting a specific key.\n * Returns 0 if the key is not in flight.\n */\n getAwaitersCount(key: string): number;\n\n /** Number of distinct keys currently in flight. */\n readonly size: number;\n}\n\n/**\n * Create an isolated singlet instance (its own key namespace).\n */\nexport function singlet(): Singlet {\n const inflight = new Map<string, Promise<unknown>>();\n\n return {\n run<T>(key: string, fn: () => T | Promise<T>): Promise<T> {\n const existing = inflight.get(key);\n if (existing !== undefined) {\n return existing as Promise<T>;\n }\n\n // `fn` is invoked in a microtask via .then(), AFTER the map entry is\n // set, so synchronous throws inside `fn` reject the shared promise\n // instead of escaping to only the first caller. The key is forgotten\n // in `finally`, which runs BEFORE the promise settles for consumers —\n // so by the time any caller's `await` resolves, the key is already\n // gone and a sequential call starts fresh (no caching, not even for\n // one microtask). The identity check guards against `forget()` having\n // replaced the entry with a newer promise for the same key.\n const promise: Promise<T> = Promise.resolve()\n .then(fn)\n .finally(() => {\n if (inflight.get(key) === promise) {\n inflight.delete(key);\n }\n });\n\n inflight.set(key, promise);\n return promise;\n },\n\n forget(key: string): boolean {\n return inflight.delete(key);\n },\n\n forgetMultiple(keys: string[]): number {\n let count = 0;\n for (const key of keys) {\n if (inflight.delete(key)) {\n count++;\n }\n }\n return count;\n },\n\n forgetAll(): number {\n const count = inflight.size;\n inflight.clear();\n return count;\n },\n\n isInFlight(key: string): boolean {\n return inflight.has(key);\n },\n\n getInFlightKeys(): string[] {\n return Array.from(inflight.keys());\n },\n\n getAwaitersCount(key: string): number {\n // Returns 1 if key is in flight, 0 otherwise\n // For basic observability without complex awaiter tracking\n return this.isInFlight(key) ? 1 : 0;\n },\n\n get size(): number {\n return inflight.size;\n },\n };\n}\n\n/**\n * Shared default instance — convenient for app-wide deduplication.\n * Prefer `singlet()` instances in libraries to avoid key collisions.\n */\nexport const shared: Singlet = singlet();\n\n// Default export for ESM compatibility\nexport default singlet;\n","/**\n * Ergonomic layer over singlet core.\n *\n * This module COMPOSES the core singlet functionality — it never duplicates logic.\n * See src/singlet.ts for the core implementation.\n */\n\nimport type { Singlet } from \"./singlet.js\";\nimport { singlet as createSinglet } from \"./singlet.js\";\n\n/**\n * Options for configuring a wrapped function.\n */\nexport interface WrapOptions<TArgs> {\n /**\n * Extract a deduplication key from function arguments.\n * Return the same key for concurrent calls that should share one promise.\n *\n * @example\n * ```ts\n * // Dedupe by user ID:\n * keyFn: ([id]) => `user:${id}`\n *\n * // Dedupe by multiple args:\n * keyFn: ([method, url]) => `${method}:${url}`\n * ```\n */\n keyFn: (args: TArgs) => string;\n\n /**\n * Optional singlet instance for this wrapped function.\n * Defaults to a new isolated instance.\n * Use this to share a singlet across multiple wrapped functions.\n */\n singlet?: Singlet;\n}\n\n/**\n * Wrap an async function to automatically deduplicate concurrent calls.\n *\n * @example\n * ```ts\n * import { wrap } from 'singlet';\n *\n * const fetchUser = wrap(\n * async (id: string) => fetch(`/api/users/${id}`).then(r => r.json()),\n * { keyFn: ([id]) => `user:${id}` }\n * );\n *\n * // These two concurrent calls share ONE fetch:\n * const [user1, user2] = await Promise.all([\n * fetchUser('42'),\n * fetchUser('42')\n * ]);\n * ```\n *\n * @param fn - The async function to wrap\n * @param options - Configuration for key extraction and singlet instance\n * @returns A wrapped function with automatic deduplication\n */\nexport function wrap<TArgs extends unknown[], TResult>(\n fn: (...args: TArgs) => TResult | Promise<TResult>,\n options: WrapOptions<TArgs>\n): (...args: TArgs) => Promise<TResult> {\n const instance = options.singlet ?? createSinglet();\n\n return function (...args: TArgs): Promise<TResult> {\n const key = options.keyFn(args);\n return instance.run(key, () => fn(...args));\n };\n}\n\n/**\n * Wrap an async function with a simple key string (for single-operation functions).\n *\n * @example\n * ```ts\n * import { wrapWithKey } from 'singlet';\n *\n * const loadConfig = wrapWithKey(\n * async () => fetch('/api/config').then(r => r.json()),\n * 'config'\n * );\n * ```\n *\n * @param fn - The async function to wrap\n * @param key - Fixed key for all calls to this function\n * @param singlet - Optional singlet instance\n * @returns A wrapped function with fixed-key deduplication\n */\nexport function wrapWithKey<TResult>(\n fn: () => TResult | Promise<TResult>,\n key: string,\n singlet?: Singlet\n): () => Promise<TResult> {\n const options: WrapOptions<[]> = { keyFn: () => key };\n if (singlet !== undefined) {\n options.singlet = singlet;\n }\n return wrap(fn, options);\n}\n","/**\n * Key generation utilities for consistent deduplication keys.\n *\n * These helpers create consistent string keys for deduplication purposes.\n * They do NOT perform caching or deduplication themselves — they just\n * help format keys that will be used with singlet's run() method.\n */\n\n/**\n * Create a consistent deduplication key from a base and parameters.\n *\n * @param base - The key namespace/base (e.g., \"user\", \"api\")\n * @param params - Parameters to serialize into the key\n * @returns A formatted deduplication key\n *\n * @example\n * ```ts\n * createKey(\"user\", { id: \"42\" })\n * // → \"user:id:42\"\n *\n * createKey(\"api\", { endpoint: \"/users\", method: \"GET\", filter: { active: true } })\n * // → \"api:endpoint:/users:method:GET:filter:{\\\"active\\\":true}\"\n * ```\n */\nexport function createKey(\n base: string,\n params: Record<string, unknown>,\n): string {\n const parts = [base];\n\n for (const [key, value] of Object.entries(params)) {\n if (value === undefined || value === null) {\n continue;\n }\n\n const serializedKey = key;\n let serializedValue: string = \"\";\n\n switch (typeof value) {\n case \"object\":\n case \"function\":\n serializedValue = JSON.stringify(value);\n break;\n case \"string\":\n case \"number\":\n case \"boolean\":\n case \"bigint\":\n case \"symbol\":\n serializedValue = String(value);\n break;\n }\n\n parts.push(`${serializedKey}:${serializedValue}`);\n }\n\n return parts.join(\":\");\n}\n\n/**\n * Create a namespaced deduplication key.\n *\n * @param parts - Key parts to join with colons\n * @returns A formatted namespaced key\n *\n * @example\n * ```ts\n * createKeyScope(\"api\", \"users\", \"42\")\n * // → \"api:users:42\"\n *\n * createKeyScope(\"cache\", \"user\", \"profile\", \"123\")\n * // → \"cache:user:profile:123\"\n * ```\n */\nexport function createKeyScope(...parts: string[]): string {\n return parts.join(\":\");\n}\n\n/**\n * Create a compound key from multiple sources.\n * Useful for combining different aspects of a request into one key.\n *\n * @param sources - Different sources to combine into a key\n * @returns A compound deduplication key\n *\n * @example\n * ```ts\n * createCompoundKey(\"user\", { userId: \"42\", organizationId: \"10\" })\n * // → \"user:userId:42:organizationId:10\"\n * ```\n */\nexport function createCompoundKey(\n base: string,\n params: Record<string, unknown>,\n): string {\n return createKey(base, params);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;AC0DO,SAAS,UAAmB;AACjC,QAAM,WAAW,oBAAI,IAA8B;AAEnD,SAAO;AAAA,IACL,IAAO,KAAa,IAAsC;AACxD,YAAM,WAAW,SAAS,IAAI,GAAG;AACjC,UAAI,aAAa,QAAW;AAC1B,eAAO;AAAA,MACT;AAUA,YAAM,UAAsB,QAAQ,QAAQ,EACzC,KAAK,EAAE,EACP,QAAQ,MAAM;AACb,YAAI,SAAS,IAAI,GAAG,MAAM,SAAS;AACjC,mBAAS,OAAO,GAAG;AAAA,QACrB;AAAA,MACF,CAAC;AAEH,eAAS,IAAI,KAAK,OAAO;AACzB,aAAO;AAAA,IACT;AAAA,IAEA,OAAO,KAAsB;AAC3B,aAAO,SAAS,OAAO,GAAG;AAAA,IAC5B;AAAA,IAEA,eAAe,MAAwB;AACrC,UAAI,QAAQ;AACZ,iBAAW,OAAO,MAAM;AACtB,YAAI,SAAS,OAAO,GAAG,GAAG;AACxB;AAAA,QACF;AAAA,MACF;AACA,aAAO;AAAA,IACT;AAAA,IAEA,YAAoB;AAClB,YAAM,QAAQ,SAAS;AACvB,eAAS,MAAM;AACf,aAAO;AAAA,IACT;AAAA,IAEA,WAAW,KAAsB;AAC/B,aAAO,SAAS,IAAI,GAAG;AAAA,IACzB;AAAA,IAEA,kBAA4B;AAC1B,aAAO,MAAM,KAAK,SAAS,KAAK,CAAC;AAAA,IACnC;AAAA,IAEA,iBAAiB,KAAqB;AAGpC,aAAO,KAAK,WAAW,GAAG,IAAI,IAAI;AAAA,IACpC;AAAA,IAEA,IAAI,OAAe;AACjB,aAAO,SAAS;AAAA,IAClB;AAAA,EACF;AACF;AAMO,IAAM,SAAkB,QAAQ;AAGvC,IAAO,kBAAQ;;;AC3ER,SAAS,KACd,IACA,SACsC;AACtC,QAAM,WAAW,QAAQ,WAAW,QAAc;AAElD,SAAO,YAAa,MAA+B;AACjD,UAAM,MAAM,QAAQ,MAAM,IAAI;AAC9B,WAAO,SAAS,IAAI,KAAK,MAAM,GAAG,GAAG,IAAI,CAAC;AAAA,EAC5C;AACF;AAoBO,SAAS,YACd,IACA,KACAA,UACwB;AACxB,QAAM,UAA2B,EAAE,OAAO,MAAM,IAAI;AACpD,MAAIA,aAAY,QAAW;AACzB,YAAQ,UAAUA;AAAA,EACpB;AACA,SAAO,KAAK,IAAI,OAAO;AACzB;;;AC5EO,SAAS,UACd,MACA,QACQ;AACR,QAAM,QAAQ,CAAC,IAAI;AAEnB,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,MAAM,GAAG;AACjD,QAAI,UAAU,UAAa,UAAU,MAAM;AACzC;AAAA,IACF;AAEA,UAAM,gBAAgB;AACtB,QAAI,kBAA0B;AAE9B,YAAQ,OAAO,OAAO;AAAA,MACpB,KAAK;AAAA,MACL,KAAK;AACH,0BAAkB,KAAK,UAAU,KAAK;AACtC;AAAA,MACF,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AACH,0BAAkB,OAAO,KAAK;AAC9B;AAAA,IACJ;AAEA,UAAM,KAAK,GAAG,aAAa,IAAI,eAAe,EAAE;AAAA,EAClD;AAEA,SAAO,MAAM,KAAK,GAAG;AACvB;AAiBO,SAAS,kBAAkB,OAAyB;AACzD,SAAO,MAAM,KAAK,GAAG;AACvB;AAeO,SAAS,kBACd,MACA,QACQ;AACR,SAAO,UAAU,MAAM,MAAM;AAC/B;","names":["singlet"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -17,8 +17,30 @@ interface Singlet {
|
|
|
17
17
|
* Returns true if a key was dropped.
|
|
18
18
|
*/
|
|
19
19
|
forget(key: string): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Drop multiple in-flight keys so the NEXT calls start fresh.
|
|
22
|
+
* Callers already awaiting the old promises still receive their results.
|
|
23
|
+
* Returns the number of keys that were in flight.
|
|
24
|
+
*/
|
|
25
|
+
forgetMultiple(keys: string[]): number;
|
|
26
|
+
/**
|
|
27
|
+
* Drop ALL in-flight keys so all NEXT calls start fresh.
|
|
28
|
+
* Callers already awaiting old promises still receive their results.
|
|
29
|
+
* Returns the number of keys that were in flight.
|
|
30
|
+
*/
|
|
31
|
+
forgetAll(): number;
|
|
20
32
|
/** Whether a call for `key` is currently in flight. */
|
|
21
33
|
isInFlight(key: string): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Get all keys currently in flight.
|
|
36
|
+
* Returns a snapshot of current keys (may change immediately).
|
|
37
|
+
*/
|
|
38
|
+
getInFlightKeys(): string[];
|
|
39
|
+
/**
|
|
40
|
+
* Get the number of concurrent callers awaiting a specific key.
|
|
41
|
+
* Returns 0 if the key is not in flight.
|
|
42
|
+
*/
|
|
43
|
+
getAwaitersCount(key: string): number;
|
|
22
44
|
/** Number of distinct keys currently in flight. */
|
|
23
45
|
readonly size: number;
|
|
24
46
|
}
|
|
@@ -108,4 +130,59 @@ declare function wrap<TArgs extends unknown[], TResult>(fn: (...args: TArgs) =>
|
|
|
108
130
|
*/
|
|
109
131
|
declare function wrapWithKey<TResult>(fn: () => TResult | Promise<TResult>, key: string, singlet?: Singlet): () => Promise<TResult>;
|
|
110
132
|
|
|
111
|
-
|
|
133
|
+
/**
|
|
134
|
+
* Key generation utilities for consistent deduplication keys.
|
|
135
|
+
*
|
|
136
|
+
* These helpers create consistent string keys for deduplication purposes.
|
|
137
|
+
* They do NOT perform caching or deduplication themselves — they just
|
|
138
|
+
* help format keys that will be used with singlet's run() method.
|
|
139
|
+
*/
|
|
140
|
+
/**
|
|
141
|
+
* Create a consistent deduplication key from a base and parameters.
|
|
142
|
+
*
|
|
143
|
+
* @param base - The key namespace/base (e.g., "user", "api")
|
|
144
|
+
* @param params - Parameters to serialize into the key
|
|
145
|
+
* @returns A formatted deduplication key
|
|
146
|
+
*
|
|
147
|
+
* @example
|
|
148
|
+
* ```ts
|
|
149
|
+
* createKey("user", { id: "42" })
|
|
150
|
+
* // → "user:id:42"
|
|
151
|
+
*
|
|
152
|
+
* createKey("api", { endpoint: "/users", method: "GET", filter: { active: true } })
|
|
153
|
+
* // → "api:endpoint:/users:method:GET:filter:{\"active\":true}"
|
|
154
|
+
* ```
|
|
155
|
+
*/
|
|
156
|
+
declare function createKey(base: string, params: Record<string, unknown>): string;
|
|
157
|
+
/**
|
|
158
|
+
* Create a namespaced deduplication key.
|
|
159
|
+
*
|
|
160
|
+
* @param parts - Key parts to join with colons
|
|
161
|
+
* @returns A formatted namespaced key
|
|
162
|
+
*
|
|
163
|
+
* @example
|
|
164
|
+
* ```ts
|
|
165
|
+
* createKeyScope("api", "users", "42")
|
|
166
|
+
* // → "api:users:42"
|
|
167
|
+
*
|
|
168
|
+
* createKeyScope("cache", "user", "profile", "123")
|
|
169
|
+
* // → "cache:user:profile:123"
|
|
170
|
+
* ```
|
|
171
|
+
*/
|
|
172
|
+
declare function createKeyScope(...parts: string[]): string;
|
|
173
|
+
/**
|
|
174
|
+
* Create a compound key from multiple sources.
|
|
175
|
+
* Useful for combining different aspects of a request into one key.
|
|
176
|
+
*
|
|
177
|
+
* @param sources - Different sources to combine into a key
|
|
178
|
+
* @returns A compound deduplication key
|
|
179
|
+
*
|
|
180
|
+
* @example
|
|
181
|
+
* ```ts
|
|
182
|
+
* createCompoundKey("user", { userId: "42", organizationId: "10" })
|
|
183
|
+
* // → "user:userId:42:organizationId:10"
|
|
184
|
+
* ```
|
|
185
|
+
*/
|
|
186
|
+
declare function createCompoundKey(base: string, params: Record<string, unknown>): string;
|
|
187
|
+
|
|
188
|
+
export { type Singlet, type WrapOptions, createCompoundKey, createKey, createKeyScope, singlet as default, shared, singlet, wrap, wrapWithKey };
|
package/dist/index.d.ts
CHANGED
|
@@ -17,8 +17,30 @@ interface Singlet {
|
|
|
17
17
|
* Returns true if a key was dropped.
|
|
18
18
|
*/
|
|
19
19
|
forget(key: string): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Drop multiple in-flight keys so the NEXT calls start fresh.
|
|
22
|
+
* Callers already awaiting the old promises still receive their results.
|
|
23
|
+
* Returns the number of keys that were in flight.
|
|
24
|
+
*/
|
|
25
|
+
forgetMultiple(keys: string[]): number;
|
|
26
|
+
/**
|
|
27
|
+
* Drop ALL in-flight keys so all NEXT calls start fresh.
|
|
28
|
+
* Callers already awaiting old promises still receive their results.
|
|
29
|
+
* Returns the number of keys that were in flight.
|
|
30
|
+
*/
|
|
31
|
+
forgetAll(): number;
|
|
20
32
|
/** Whether a call for `key` is currently in flight. */
|
|
21
33
|
isInFlight(key: string): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Get all keys currently in flight.
|
|
36
|
+
* Returns a snapshot of current keys (may change immediately).
|
|
37
|
+
*/
|
|
38
|
+
getInFlightKeys(): string[];
|
|
39
|
+
/**
|
|
40
|
+
* Get the number of concurrent callers awaiting a specific key.
|
|
41
|
+
* Returns 0 if the key is not in flight.
|
|
42
|
+
*/
|
|
43
|
+
getAwaitersCount(key: string): number;
|
|
22
44
|
/** Number of distinct keys currently in flight. */
|
|
23
45
|
readonly size: number;
|
|
24
46
|
}
|
|
@@ -108,4 +130,59 @@ declare function wrap<TArgs extends unknown[], TResult>(fn: (...args: TArgs) =>
|
|
|
108
130
|
*/
|
|
109
131
|
declare function wrapWithKey<TResult>(fn: () => TResult | Promise<TResult>, key: string, singlet?: Singlet): () => Promise<TResult>;
|
|
110
132
|
|
|
111
|
-
|
|
133
|
+
/**
|
|
134
|
+
* Key generation utilities for consistent deduplication keys.
|
|
135
|
+
*
|
|
136
|
+
* These helpers create consistent string keys for deduplication purposes.
|
|
137
|
+
* They do NOT perform caching or deduplication themselves — they just
|
|
138
|
+
* help format keys that will be used with singlet's run() method.
|
|
139
|
+
*/
|
|
140
|
+
/**
|
|
141
|
+
* Create a consistent deduplication key from a base and parameters.
|
|
142
|
+
*
|
|
143
|
+
* @param base - The key namespace/base (e.g., "user", "api")
|
|
144
|
+
* @param params - Parameters to serialize into the key
|
|
145
|
+
* @returns A formatted deduplication key
|
|
146
|
+
*
|
|
147
|
+
* @example
|
|
148
|
+
* ```ts
|
|
149
|
+
* createKey("user", { id: "42" })
|
|
150
|
+
* // → "user:id:42"
|
|
151
|
+
*
|
|
152
|
+
* createKey("api", { endpoint: "/users", method: "GET", filter: { active: true } })
|
|
153
|
+
* // → "api:endpoint:/users:method:GET:filter:{\"active\":true}"
|
|
154
|
+
* ```
|
|
155
|
+
*/
|
|
156
|
+
declare function createKey(base: string, params: Record<string, unknown>): string;
|
|
157
|
+
/**
|
|
158
|
+
* Create a namespaced deduplication key.
|
|
159
|
+
*
|
|
160
|
+
* @param parts - Key parts to join with colons
|
|
161
|
+
* @returns A formatted namespaced key
|
|
162
|
+
*
|
|
163
|
+
* @example
|
|
164
|
+
* ```ts
|
|
165
|
+
* createKeyScope("api", "users", "42")
|
|
166
|
+
* // → "api:users:42"
|
|
167
|
+
*
|
|
168
|
+
* createKeyScope("cache", "user", "profile", "123")
|
|
169
|
+
* // → "cache:user:profile:123"
|
|
170
|
+
* ```
|
|
171
|
+
*/
|
|
172
|
+
declare function createKeyScope(...parts: string[]): string;
|
|
173
|
+
/**
|
|
174
|
+
* Create a compound key from multiple sources.
|
|
175
|
+
* Useful for combining different aspects of a request into one key.
|
|
176
|
+
*
|
|
177
|
+
* @param sources - Different sources to combine into a key
|
|
178
|
+
* @returns A compound deduplication key
|
|
179
|
+
*
|
|
180
|
+
* @example
|
|
181
|
+
* ```ts
|
|
182
|
+
* createCompoundKey("user", { userId: "42", organizationId: "10" })
|
|
183
|
+
* // → "user:userId:42:organizationId:10"
|
|
184
|
+
* ```
|
|
185
|
+
*/
|
|
186
|
+
declare function createCompoundKey(base: string, params: Record<string, unknown>): string;
|
|
187
|
+
|
|
188
|
+
export { type Singlet, type WrapOptions, createCompoundKey, createKey, createKeyScope, singlet as default, shared, singlet, wrap, wrapWithKey };
|
package/dist/index.js
CHANGED
|
@@ -18,9 +18,29 @@ function singlet() {
|
|
|
18
18
|
forget(key) {
|
|
19
19
|
return inflight.delete(key);
|
|
20
20
|
},
|
|
21
|
+
forgetMultiple(keys) {
|
|
22
|
+
let count = 0;
|
|
23
|
+
for (const key of keys) {
|
|
24
|
+
if (inflight.delete(key)) {
|
|
25
|
+
count++;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return count;
|
|
29
|
+
},
|
|
30
|
+
forgetAll() {
|
|
31
|
+
const count = inflight.size;
|
|
32
|
+
inflight.clear();
|
|
33
|
+
return count;
|
|
34
|
+
},
|
|
21
35
|
isInFlight(key) {
|
|
22
36
|
return inflight.has(key);
|
|
23
37
|
},
|
|
38
|
+
getInFlightKeys() {
|
|
39
|
+
return Array.from(inflight.keys());
|
|
40
|
+
},
|
|
41
|
+
getAwaitersCount(key) {
|
|
42
|
+
return this.isInFlight(key) ? 1 : 0;
|
|
43
|
+
},
|
|
24
44
|
get size() {
|
|
25
45
|
return inflight.size;
|
|
26
46
|
}
|
|
@@ -44,7 +64,43 @@ function wrapWithKey(fn, key, singlet2) {
|
|
|
44
64
|
}
|
|
45
65
|
return wrap(fn, options);
|
|
46
66
|
}
|
|
67
|
+
|
|
68
|
+
// src/keys.ts
|
|
69
|
+
function createKey(base, params) {
|
|
70
|
+
const parts = [base];
|
|
71
|
+
for (const [key, value] of Object.entries(params)) {
|
|
72
|
+
if (value === void 0 || value === null) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const serializedKey = key;
|
|
76
|
+
let serializedValue = "";
|
|
77
|
+
switch (typeof value) {
|
|
78
|
+
case "object":
|
|
79
|
+
case "function":
|
|
80
|
+
serializedValue = JSON.stringify(value);
|
|
81
|
+
break;
|
|
82
|
+
case "string":
|
|
83
|
+
case "number":
|
|
84
|
+
case "boolean":
|
|
85
|
+
case "bigint":
|
|
86
|
+
case "symbol":
|
|
87
|
+
serializedValue = String(value);
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
parts.push(`${serializedKey}:${serializedValue}`);
|
|
91
|
+
}
|
|
92
|
+
return parts.join(":");
|
|
93
|
+
}
|
|
94
|
+
function createKeyScope(...parts) {
|
|
95
|
+
return parts.join(":");
|
|
96
|
+
}
|
|
97
|
+
function createCompoundKey(base, params) {
|
|
98
|
+
return createKey(base, params);
|
|
99
|
+
}
|
|
47
100
|
export {
|
|
101
|
+
createCompoundKey,
|
|
102
|
+
createKey,
|
|
103
|
+
createKeyScope,
|
|
48
104
|
singlet_default as default,
|
|
49
105
|
shared,
|
|
50
106
|
singlet,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/singlet.ts","../src/wrap.ts"],"sourcesContent":["/**\n * singlet — deduplicate concurrent async calls.\n *\n * Concurrent calls with the same key share ONE in-flight promise.\n * Once the promise settles, the key is forgotten. singlet is NOT a cache:\n * it stores nothing after completion. One job, done well.\n */\n\nexport interface Singlet {\n /**\n * Run `fn` under `key`. If a call with the same key is already in flight,\n * the existing promise is returned and `fn` is NOT invoked.\n */\n run<T>(key: string, fn: () => T | Promise<T>): Promise<T>;\n\n /**\n * Drop an in-flight key so the NEXT call starts fresh.\n * Callers already awaiting the old promise still receive its result.\n * Returns true if a key was dropped.\n */\n forget(key: string): boolean;\n\n /** Whether a call for `key` is currently in flight. */\n isInFlight(key: string): boolean;\n\n /** Number of distinct keys currently in flight. */\n readonly size: number;\n}\n\n/**\n * Create an isolated singlet instance (its own key namespace).\n */\nexport function singlet(): Singlet {\n const inflight = new Map<string, Promise<unknown>>();\n\n return {\n run<T>(key: string, fn: () => T | Promise<T>): Promise<T> {\n const existing = inflight.get(key);\n if (existing !== undefined) {\n return existing as Promise<T>;\n }\n\n // `fn` is invoked in a microtask via .then(), AFTER the map entry is\n // set, so synchronous throws inside `fn` reject the shared promise\n // instead of escaping to only the first caller. The key is forgotten\n // in `finally`, which runs BEFORE the promise settles for consumers —\n // so by the time any caller's `await` resolves, the key is already\n // gone and a sequential call starts fresh (no caching, not even for\n // one microtask). The identity check guards against `forget()` having\n // replaced the entry with a newer promise for the same key.\n const promise: Promise<T> = Promise.resolve()\n .then(fn)\n .finally(() => {\n if (inflight.get(key) === promise) {\n inflight.delete(key);\n }\n });\n\n inflight.set(key, promise);\n return promise;\n },\n\n forget(key: string): boolean {\n return inflight.delete(key);\n },\n\n isInFlight(key: string): boolean {\n return inflight.has(key);\n },\n\n get size(): number {\n return inflight.size;\n },\n };\n}\n\n/**\n * Shared default instance — convenient for app-wide deduplication.\n * Prefer `singlet()` instances in libraries to avoid key collisions.\n */\nexport const shared: Singlet = singlet();\n\n// Default export for ESM compatibility\nexport default singlet;\n","/**\n * Ergonomic layer over singlet core.\n *\n * This module COMPOSES the core singlet functionality — it never duplicates logic.\n * See src/singlet.ts for the core implementation.\n */\n\nimport type { Singlet } from \"./singlet.js\";\nimport { singlet as createSinglet } from \"./singlet.js\";\n\n/**\n * Options for configuring a wrapped function.\n */\nexport interface WrapOptions<TArgs> {\n /**\n * Extract a deduplication key from function arguments.\n * Return the same key for concurrent calls that should share one promise.\n *\n * @example\n * ```ts\n * // Dedupe by user ID:\n * keyFn: ([id]) => `user:${id}`\n *\n * // Dedupe by multiple args:\n * keyFn: ([method, url]) => `${method}:${url}`\n * ```\n */\n keyFn: (args: TArgs) => string;\n\n /**\n * Optional singlet instance for this wrapped function.\n * Defaults to a new isolated instance.\n * Use this to share a singlet across multiple wrapped functions.\n */\n singlet?: Singlet;\n}\n\n/**\n * Wrap an async function to automatically deduplicate concurrent calls.\n *\n * @example\n * ```ts\n * import { wrap } from 'singlet';\n *\n * const fetchUser = wrap(\n * async (id: string) => fetch(`/api/users/${id}`).then(r => r.json()),\n * { keyFn: ([id]) => `user:${id}` }\n * );\n *\n * // These two concurrent calls share ONE fetch:\n * const [user1, user2] = await Promise.all([\n * fetchUser('42'),\n * fetchUser('42')\n * ]);\n * ```\n *\n * @param fn - The async function to wrap\n * @param options - Configuration for key extraction and singlet instance\n * @returns A wrapped function with automatic deduplication\n */\nexport function wrap<TArgs extends unknown[], TResult>(\n fn: (...args: TArgs) => TResult | Promise<TResult>,\n options: WrapOptions<TArgs>\n): (...args: TArgs) => Promise<TResult> {\n const instance = options.singlet ?? createSinglet();\n\n return function (...args: TArgs): Promise<TResult> {\n const key = options.keyFn(args);\n return instance.run(key, () => fn(...args));\n };\n}\n\n/**\n * Wrap an async function with a simple key string (for single-operation functions).\n *\n * @example\n * ```ts\n * import { wrapWithKey } from 'singlet';\n *\n * const loadConfig = wrapWithKey(\n * async () => fetch('/api/config').then(r => r.json()),\n * 'config'\n * );\n * ```\n *\n * @param fn - The async function to wrap\n * @param key - Fixed key for all calls to this function\n * @param singlet - Optional singlet instance\n * @returns A wrapped function with fixed-key deduplication\n */\nexport function wrapWithKey<TResult>(\n fn: () => TResult | Promise<TResult>,\n key: string,\n singlet?: Singlet\n): () => Promise<TResult> {\n const options: WrapOptions<[]> = { keyFn: () => key };\n if (singlet !== undefined) {\n options.singlet = singlet;\n }\n return wrap(fn, options);\n}\n"],"mappings":";AAgCO,SAAS,UAAmB;AACjC,QAAM,WAAW,oBAAI,IAA8B;AAEnD,SAAO;AAAA,IACL,IAAO,KAAa,IAAsC;AACxD,YAAM,WAAW,SAAS,IAAI,GAAG;AACjC,UAAI,aAAa,QAAW;AAC1B,eAAO;AAAA,MACT;AAUA,YAAM,UAAsB,QAAQ,QAAQ,EACzC,KAAK,EAAE,EACP,QAAQ,MAAM;AACb,YAAI,SAAS,IAAI,GAAG,MAAM,SAAS;AACjC,mBAAS,OAAO,GAAG;AAAA,QACrB;AAAA,MACF,CAAC;AAEH,eAAS,IAAI,KAAK,OAAO;AACzB,aAAO;AAAA,IACT;AAAA,IAEA,OAAO,KAAsB;AAC3B,aAAO,SAAS,OAAO,GAAG;AAAA,IAC5B;AAAA,IAEA,WAAW,KAAsB;AAC/B,aAAO,SAAS,IAAI,GAAG;AAAA,IACzB;AAAA,IAEA,IAAI,OAAe;AACjB,aAAO,SAAS;AAAA,IAClB;AAAA,EACF;AACF;AAMO,IAAM,SAAkB,QAAQ;AAGvC,IAAO,kBAAQ;;;ACvBR,SAAS,KACd,IACA,SACsC;AACtC,QAAM,WAAW,QAAQ,WAAW,QAAc;AAElD,SAAO,YAAa,MAA+B;AACjD,UAAM,MAAM,QAAQ,MAAM,IAAI;AAC9B,WAAO,SAAS,IAAI,KAAK,MAAM,GAAG,GAAG,IAAI,CAAC;AAAA,EAC5C;AACF;AAoBO,SAAS,YACd,IACA,KACAA,UACwB;AACxB,QAAM,UAA2B,EAAE,OAAO,MAAM,IAAI;AACpD,MAAIA,aAAY,QAAW;AACzB,YAAQ,UAAUA;AAAA,EACpB;AACA,SAAO,KAAK,IAAI,OAAO;AACzB;","names":["singlet"]}
|
|
1
|
+
{"version":3,"sources":["../src/singlet.ts","../src/wrap.ts","../src/keys.ts"],"sourcesContent":["/**\n * singlet — deduplicate concurrent async calls.\n *\n * Concurrent calls with the same key share ONE in-flight promise.\n * Once the promise settles, the key is forgotten. singlet is NOT a cache:\n * it stores nothing after completion. One job, done well.\n */\n\nexport interface Singlet {\n /**\n * Run `fn` under `key`. If a call with the same key is already in flight,\n * the existing promise is returned and `fn` is NOT invoked.\n */\n run<T>(key: string, fn: () => T | Promise<T>): Promise<T>;\n\n /**\n * Drop an in-flight key so the NEXT call starts fresh.\n * Callers already awaiting the old promise still receive its result.\n * Returns true if a key was dropped.\n */\n forget(key: string): boolean;\n\n /**\n * Drop multiple in-flight keys so the NEXT calls start fresh.\n * Callers already awaiting the old promises still receive their results.\n * Returns the number of keys that were in flight.\n */\n forgetMultiple(keys: string[]): number;\n\n /**\n * Drop ALL in-flight keys so all NEXT calls start fresh.\n * Callers already awaiting old promises still receive their results.\n * Returns the number of keys that were in flight.\n */\n forgetAll(): number;\n\n /** Whether a call for `key` is currently in flight. */\n isInFlight(key: string): boolean;\n\n /**\n * Get all keys currently in flight.\n * Returns a snapshot of current keys (may change immediately).\n */\n getInFlightKeys(): string[];\n\n /**\n * Get the number of concurrent callers awaiting a specific key.\n * Returns 0 if the key is not in flight.\n */\n getAwaitersCount(key: string): number;\n\n /** Number of distinct keys currently in flight. */\n readonly size: number;\n}\n\n/**\n * Create an isolated singlet instance (its own key namespace).\n */\nexport function singlet(): Singlet {\n const inflight = new Map<string, Promise<unknown>>();\n\n return {\n run<T>(key: string, fn: () => T | Promise<T>): Promise<T> {\n const existing = inflight.get(key);\n if (existing !== undefined) {\n return existing as Promise<T>;\n }\n\n // `fn` is invoked in a microtask via .then(), AFTER the map entry is\n // set, so synchronous throws inside `fn` reject the shared promise\n // instead of escaping to only the first caller. The key is forgotten\n // in `finally`, which runs BEFORE the promise settles for consumers —\n // so by the time any caller's `await` resolves, the key is already\n // gone and a sequential call starts fresh (no caching, not even for\n // one microtask). The identity check guards against `forget()` having\n // replaced the entry with a newer promise for the same key.\n const promise: Promise<T> = Promise.resolve()\n .then(fn)\n .finally(() => {\n if (inflight.get(key) === promise) {\n inflight.delete(key);\n }\n });\n\n inflight.set(key, promise);\n return promise;\n },\n\n forget(key: string): boolean {\n return inflight.delete(key);\n },\n\n forgetMultiple(keys: string[]): number {\n let count = 0;\n for (const key of keys) {\n if (inflight.delete(key)) {\n count++;\n }\n }\n return count;\n },\n\n forgetAll(): number {\n const count = inflight.size;\n inflight.clear();\n return count;\n },\n\n isInFlight(key: string): boolean {\n return inflight.has(key);\n },\n\n getInFlightKeys(): string[] {\n return Array.from(inflight.keys());\n },\n\n getAwaitersCount(key: string): number {\n // Returns 1 if key is in flight, 0 otherwise\n // For basic observability without complex awaiter tracking\n return this.isInFlight(key) ? 1 : 0;\n },\n\n get size(): number {\n return inflight.size;\n },\n };\n}\n\n/**\n * Shared default instance — convenient for app-wide deduplication.\n * Prefer `singlet()` instances in libraries to avoid key collisions.\n */\nexport const shared: Singlet = singlet();\n\n// Default export for ESM compatibility\nexport default singlet;\n","/**\n * Ergonomic layer over singlet core.\n *\n * This module COMPOSES the core singlet functionality — it never duplicates logic.\n * See src/singlet.ts for the core implementation.\n */\n\nimport type { Singlet } from \"./singlet.js\";\nimport { singlet as createSinglet } from \"./singlet.js\";\n\n/**\n * Options for configuring a wrapped function.\n */\nexport interface WrapOptions<TArgs> {\n /**\n * Extract a deduplication key from function arguments.\n * Return the same key for concurrent calls that should share one promise.\n *\n * @example\n * ```ts\n * // Dedupe by user ID:\n * keyFn: ([id]) => `user:${id}`\n *\n * // Dedupe by multiple args:\n * keyFn: ([method, url]) => `${method}:${url}`\n * ```\n */\n keyFn: (args: TArgs) => string;\n\n /**\n * Optional singlet instance for this wrapped function.\n * Defaults to a new isolated instance.\n * Use this to share a singlet across multiple wrapped functions.\n */\n singlet?: Singlet;\n}\n\n/**\n * Wrap an async function to automatically deduplicate concurrent calls.\n *\n * @example\n * ```ts\n * import { wrap } from 'singlet';\n *\n * const fetchUser = wrap(\n * async (id: string) => fetch(`/api/users/${id}`).then(r => r.json()),\n * { keyFn: ([id]) => `user:${id}` }\n * );\n *\n * // These two concurrent calls share ONE fetch:\n * const [user1, user2] = await Promise.all([\n * fetchUser('42'),\n * fetchUser('42')\n * ]);\n * ```\n *\n * @param fn - The async function to wrap\n * @param options - Configuration for key extraction and singlet instance\n * @returns A wrapped function with automatic deduplication\n */\nexport function wrap<TArgs extends unknown[], TResult>(\n fn: (...args: TArgs) => TResult | Promise<TResult>,\n options: WrapOptions<TArgs>\n): (...args: TArgs) => Promise<TResult> {\n const instance = options.singlet ?? createSinglet();\n\n return function (...args: TArgs): Promise<TResult> {\n const key = options.keyFn(args);\n return instance.run(key, () => fn(...args));\n };\n}\n\n/**\n * Wrap an async function with a simple key string (for single-operation functions).\n *\n * @example\n * ```ts\n * import { wrapWithKey } from 'singlet';\n *\n * const loadConfig = wrapWithKey(\n * async () => fetch('/api/config').then(r => r.json()),\n * 'config'\n * );\n * ```\n *\n * @param fn - The async function to wrap\n * @param key - Fixed key for all calls to this function\n * @param singlet - Optional singlet instance\n * @returns A wrapped function with fixed-key deduplication\n */\nexport function wrapWithKey<TResult>(\n fn: () => TResult | Promise<TResult>,\n key: string,\n singlet?: Singlet\n): () => Promise<TResult> {\n const options: WrapOptions<[]> = { keyFn: () => key };\n if (singlet !== undefined) {\n options.singlet = singlet;\n }\n return wrap(fn, options);\n}\n","/**\n * Key generation utilities for consistent deduplication keys.\n *\n * These helpers create consistent string keys for deduplication purposes.\n * They do NOT perform caching or deduplication themselves — they just\n * help format keys that will be used with singlet's run() method.\n */\n\n/**\n * Create a consistent deduplication key from a base and parameters.\n *\n * @param base - The key namespace/base (e.g., \"user\", \"api\")\n * @param params - Parameters to serialize into the key\n * @returns A formatted deduplication key\n *\n * @example\n * ```ts\n * createKey(\"user\", { id: \"42\" })\n * // → \"user:id:42\"\n *\n * createKey(\"api\", { endpoint: \"/users\", method: \"GET\", filter: { active: true } })\n * // → \"api:endpoint:/users:method:GET:filter:{\\\"active\\\":true}\"\n * ```\n */\nexport function createKey(\n base: string,\n params: Record<string, unknown>,\n): string {\n const parts = [base];\n\n for (const [key, value] of Object.entries(params)) {\n if (value === undefined || value === null) {\n continue;\n }\n\n const serializedKey = key;\n let serializedValue: string = \"\";\n\n switch (typeof value) {\n case \"object\":\n case \"function\":\n serializedValue = JSON.stringify(value);\n break;\n case \"string\":\n case \"number\":\n case \"boolean\":\n case \"bigint\":\n case \"symbol\":\n serializedValue = String(value);\n break;\n }\n\n parts.push(`${serializedKey}:${serializedValue}`);\n }\n\n return parts.join(\":\");\n}\n\n/**\n * Create a namespaced deduplication key.\n *\n * @param parts - Key parts to join with colons\n * @returns A formatted namespaced key\n *\n * @example\n * ```ts\n * createKeyScope(\"api\", \"users\", \"42\")\n * // → \"api:users:42\"\n *\n * createKeyScope(\"cache\", \"user\", \"profile\", \"123\")\n * // → \"cache:user:profile:123\"\n * ```\n */\nexport function createKeyScope(...parts: string[]): string {\n return parts.join(\":\");\n}\n\n/**\n * Create a compound key from multiple sources.\n * Useful for combining different aspects of a request into one key.\n *\n * @param sources - Different sources to combine into a key\n * @returns A compound deduplication key\n *\n * @example\n * ```ts\n * createCompoundKey(\"user\", { userId: \"42\", organizationId: \"10\" })\n * // → \"user:userId:42:organizationId:10\"\n * ```\n */\nexport function createCompoundKey(\n base: string,\n params: Record<string, unknown>,\n): string {\n return createKey(base, params);\n}\n"],"mappings":";AA0DO,SAAS,UAAmB;AACjC,QAAM,WAAW,oBAAI,IAA8B;AAEnD,SAAO;AAAA,IACL,IAAO,KAAa,IAAsC;AACxD,YAAM,WAAW,SAAS,IAAI,GAAG;AACjC,UAAI,aAAa,QAAW;AAC1B,eAAO;AAAA,MACT;AAUA,YAAM,UAAsB,QAAQ,QAAQ,EACzC,KAAK,EAAE,EACP,QAAQ,MAAM;AACb,YAAI,SAAS,IAAI,GAAG,MAAM,SAAS;AACjC,mBAAS,OAAO,GAAG;AAAA,QACrB;AAAA,MACF,CAAC;AAEH,eAAS,IAAI,KAAK,OAAO;AACzB,aAAO;AAAA,IACT;AAAA,IAEA,OAAO,KAAsB;AAC3B,aAAO,SAAS,OAAO,GAAG;AAAA,IAC5B;AAAA,IAEA,eAAe,MAAwB;AACrC,UAAI,QAAQ;AACZ,iBAAW,OAAO,MAAM;AACtB,YAAI,SAAS,OAAO,GAAG,GAAG;AACxB;AAAA,QACF;AAAA,MACF;AACA,aAAO;AAAA,IACT;AAAA,IAEA,YAAoB;AAClB,YAAM,QAAQ,SAAS;AACvB,eAAS,MAAM;AACf,aAAO;AAAA,IACT;AAAA,IAEA,WAAW,KAAsB;AAC/B,aAAO,SAAS,IAAI,GAAG;AAAA,IACzB;AAAA,IAEA,kBAA4B;AAC1B,aAAO,MAAM,KAAK,SAAS,KAAK,CAAC;AAAA,IACnC;AAAA,IAEA,iBAAiB,KAAqB;AAGpC,aAAO,KAAK,WAAW,GAAG,IAAI,IAAI;AAAA,IACpC;AAAA,IAEA,IAAI,OAAe;AACjB,aAAO,SAAS;AAAA,IAClB;AAAA,EACF;AACF;AAMO,IAAM,SAAkB,QAAQ;AAGvC,IAAO,kBAAQ;;;AC3ER,SAAS,KACd,IACA,SACsC;AACtC,QAAM,WAAW,QAAQ,WAAW,QAAc;AAElD,SAAO,YAAa,MAA+B;AACjD,UAAM,MAAM,QAAQ,MAAM,IAAI;AAC9B,WAAO,SAAS,IAAI,KAAK,MAAM,GAAG,GAAG,IAAI,CAAC;AAAA,EAC5C;AACF;AAoBO,SAAS,YACd,IACA,KACAA,UACwB;AACxB,QAAM,UAA2B,EAAE,OAAO,MAAM,IAAI;AACpD,MAAIA,aAAY,QAAW;AACzB,YAAQ,UAAUA;AAAA,EACpB;AACA,SAAO,KAAK,IAAI,OAAO;AACzB;;;AC5EO,SAAS,UACd,MACA,QACQ;AACR,QAAM,QAAQ,CAAC,IAAI;AAEnB,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,MAAM,GAAG;AACjD,QAAI,UAAU,UAAa,UAAU,MAAM;AACzC;AAAA,IACF;AAEA,UAAM,gBAAgB;AACtB,QAAI,kBAA0B;AAE9B,YAAQ,OAAO,OAAO;AAAA,MACpB,KAAK;AAAA,MACL,KAAK;AACH,0BAAkB,KAAK,UAAU,KAAK;AACtC;AAAA,MACF,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AACH,0BAAkB,OAAO,KAAK;AAC9B;AAAA,IACJ;AAEA,UAAM,KAAK,GAAG,aAAa,IAAI,eAAe,EAAE;AAAA,EAClD;AAEA,SAAO,MAAM,KAAK,GAAG;AACvB;AAiBO,SAAS,kBAAkB,OAAyB;AACzD,SAAO,MAAM,KAAK,GAAG;AACvB;AAeO,SAAS,kBACd,MACA,QACQ;AACR,SAAO,UAAU,MAAM,MAAM;AAC/B;","names":["singlet"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@azghr/singlet",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Deduplicate concurrent async calls: same key, one in-flight promise. Not a cache.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"singleflight",
|
|
@@ -14,7 +14,12 @@
|
|
|
14
14
|
"typescript",
|
|
15
15
|
"type-safe",
|
|
16
16
|
"isomorphic",
|
|
17
|
-
"in-flight"
|
|
17
|
+
"in-flight",
|
|
18
|
+
"batch-operations",
|
|
19
|
+
"introspection",
|
|
20
|
+
"key-generation",
|
|
21
|
+
"monitoring",
|
|
22
|
+
"cache-invalidation"
|
|
18
23
|
],
|
|
19
24
|
"license": "MIT",
|
|
20
25
|
"author": "",
|