qznt 2.1.1 → 2.2.1

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/dist/index.cjs CHANGED
@@ -63,6 +63,7 @@ __export(index_exports, {
63
63
  isSortedArray: () => isSortedArray,
64
64
  isString: () => isString,
65
65
  isToday: () => isToday,
66
+ keyedQueue: () => keyedQueue,
66
67
  lerp: () => lerp,
67
68
  memoize: () => memoize,
68
69
  merge: () => merge,
@@ -604,6 +605,24 @@ function unique(array, key) {
604
605
  }
605
606
 
606
607
  // src/async.ts
608
+ function keyedQueue() {
609
+ const queues = /* @__PURE__ */ new Map();
610
+ return {
611
+ run(key, work) {
612
+ const previous = queues.get(key) ?? Promise.resolve();
613
+ const result = previous.then(work);
614
+ const release = () => {
615
+ if (queues.get(key) === tail) queues.delete(key);
616
+ };
617
+ const tail = result.then(release, release);
618
+ queues.set(key, tail);
619
+ return result;
620
+ },
621
+ get size() {
622
+ return queues.size;
623
+ }
624
+ };
625
+ }
607
626
  async function retryPromise(fn, options = {}) {
608
627
  const { attempts = 3, delay = 500, timeout, signal } = options;
609
628
  if (signal?.aborted) throw new Error("Operation aborted");
@@ -719,9 +738,28 @@ function throttle(fn, limit) {
719
738
  function memoize(fn, options = {}) {
720
739
  const cache = /* @__PURE__ */ new Map();
721
740
  const { resolver, maxAge } = options;
741
+ let lastSweep = Date.now();
742
+ const getKey = (...args) => resolver ? resolver(...args) : JSON.stringify(args);
743
+ const sweep = (now) => {
744
+ if (!maxAge || now - lastSweep < maxAge) return;
745
+ lastSweep = now;
746
+ for (const [key, entry] of cache) {
747
+ if (now - entry.timestamp >= maxAge) cache.delete(key);
748
+ }
749
+ };
750
+ const cacheValue = (key, value, timestamp) => {
751
+ const entry = { value, timestamp };
752
+ cache.set(key, entry);
753
+ if (value !== null && (typeof value === "object" || typeof value === "function") && "then" in value && typeof value.then === "function") {
754
+ void Promise.resolve(value).catch(() => {
755
+ if (cache.get(key) === entry) cache.delete(key);
756
+ });
757
+ }
758
+ };
722
759
  const memoized = function(...args) {
723
- const key = resolver ? resolver(...args) : JSON.stringify(args);
760
+ const key = getKey(...args);
724
761
  const now = Date.now();
762
+ sweep(now);
725
763
  const entry = cache.get(key);
726
764
  if (entry) {
727
765
  if (!maxAge || now - entry.timestamp < maxAge) {
@@ -730,10 +768,17 @@ function memoize(fn, options = {}) {
730
768
  cache.delete(key);
731
769
  }
732
770
  const result = fn.apply(this, args);
733
- cache.set(key, { value: result, timestamp: now });
771
+ cacheValue(key, result, now);
734
772
  return result;
735
773
  };
736
774
  memoized.clear = () => cache.clear();
775
+ memoized.delete = (...args) => cache.delete(getKey(...args));
776
+ memoized.set = (value, ...args) => {
777
+ const key = getKey(...args);
778
+ const now = Date.now();
779
+ sweep(now);
780
+ cacheValue(key, value, now);
781
+ };
737
782
  return memoized;
738
783
  }
739
784
 
@@ -784,7 +829,7 @@ function formatDuration(target, style = "hms", options = {}) {
784
829
  if (diff === 0) return style === "digital" ? "00:00" : "now";
785
830
  const s = Math.floor(diff / 1e3) % 60;
786
831
  const m = Math.floor(diff / 6e4) % 60;
787
- const h = Math.floor(diff / 36e5) % 24;
832
+ const h = style === "hms" ? Math.floor(diff / 36e5) : Math.floor(diff / 36e5) % 24;
788
833
  const d = Math.floor(diff / 864e5);
789
834
  if (style === "digital") {
790
835
  const parts = [m, s].map((v) => String(v).padStart(2, "0"));
@@ -1080,6 +1125,7 @@ function toTitleCase(str, smart = true) {
1080
1125
  isSortedArray,
1081
1126
  isString,
1082
1127
  isToday,
1128
+ keyedQueue,
1083
1129
  lerp,
1084
1130
  memoize,
1085
1131
  merge,
package/dist/index.d.cts CHANGED
@@ -208,6 +208,20 @@ interface RetryOptions {
208
208
  /** An AbortSignal to cancel the entire operation. */
209
209
  signal?: AbortSignal;
210
210
  }
211
+ /**
212
+ * Creates a queue that runs tasks in call order for each key, while different keys run concurrently.
213
+ * Task errors reach their callers without blocking later tasks. Drained keys are removed automatically.
214
+ * @example
215
+ * const queue = keyedQueue();
216
+ * const first = queue.run("user-1", () => saveInventory("user-1"));
217
+ * const second = queue.run("user-1", () => readInventory("user-1"));
218
+ * await Promise.all([first, second]); // The read runs after the save finishes
219
+ * console.log(queue.size); // 0 active keys
220
+ */
221
+ declare function keyedQueue(): {
222
+ run<T>(key: string | number, work: () => T | Promise<T>): Promise<T>;
223
+ readonly size: number;
224
+ };
211
225
  /**
212
226
  * Retries an async function until the maximum number of attempts is reached.
213
227
  *
@@ -275,17 +289,23 @@ declare function debounce<T extends (...args: unknown[]) => unknown>(fn: T, wait
275
289
  */
276
290
  declare function throttle<T extends (...args: unknown[]) => unknown>(fn: T, limit: number): (...args: Parameters<T>) => void;
277
291
 
278
- interface MemoizedFunction<T extends (...args: any[]) => any> {
292
+ interface MemoizedFunction<T extends (...args: never[]) => unknown> {
279
293
  (...args: Parameters<T>): ReturnType<T>;
280
294
  /** Clears the cache. */
281
295
  clear: () => void;
296
+ /** Removes the entry for these arguments, returning whether it existed. */
297
+ delete: (...args: Parameters<T>) => boolean;
298
+ /** Caches a value for these arguments, starting a fresh TTL. */
299
+ set: (value: ReturnType<T>, ...args: Parameters<T>) => void;
282
300
  }
283
301
  /**
284
302
  * Memoizes a function by caching its results based on the input arguments.
285
303
  * A resolver function can be provided to customize the key generation.
286
304
  * If no resolver is provided, the key will be generated by JSON stringify-ing the input arguments.
305
+ * Rejected promises are evicted. Expired entries are swept lazily on calls and writes,
306
+ * at most once per maxAge interval; no cleanup timers are created.
287
307
  * @param fn The function to memoize
288
- * @param resolver An optional resolver function to customize the key generation
308
+ * @param options Optional key resolver and cache lifetime in milliseconds
289
309
  * @example
290
310
  * // 1. Basic usage
291
311
  * const heavyCalc = memoize((n: number) => {
@@ -306,8 +326,14 @@ interface MemoizedFunction<T extends (...args: any[]) => any> {
306
326
  * @example
307
327
  * // 4. Manual cache clearing
308
328
  * heavyCalc.clear();
329
+ * @example
330
+ * // 5. Seed or invalidate one entry using the original arguments
331
+ * const getFlag = memoize(async (id: string) => readFlag(id));
332
+ * await writeFlag("drops", true);
333
+ * getFlag.set(Promise.resolve(true), "drops");
334
+ * getFlag.delete("drops");
309
335
  */
310
- declare function memoize<T extends (...args: any[]) => any>(fn: T, options?: {
336
+ declare function memoize<T extends (...args: never[]) => unknown>(fn: T, options?: {
311
337
  /** Function to generate a cache key from the input arguments. */
312
338
  resolver?: (...args: Parameters<T>) => string;
313
339
  /** The time to wait in milliseconds before expiring a cache entry. */
@@ -368,9 +394,9 @@ declare function FormatNumberCompact(num: number, locale?: Intl.LocalesArgument)
368
394
  * Duration formatter.
369
395
  *
370
396
  * Available styles:
371
- * - Digital (00:00)
372
- * - HMS
373
- * - YMDHMS.
397
+ * - "digital": MM:SS, HH:MM:SS, or D:HH:MM:SS.
398
+ * - "hms": hours, minutes, and seconds, with total hours that can exceed 23.
399
+ * - "ymdhms": days, hours, minutes, and seconds, with hours capped at 23.
374
400
  * @param target The target time to calculate from
375
401
  * @param style The output style to use
376
402
  * @param options Formatting options
@@ -673,4 +699,4 @@ declare function pluralize(count: number, singular: string, plural?: string): st
673
699
  */
674
700
  declare function toTitleCase(str: string, smart?: boolean): string;
675
701
 
676
- export { type AlphaCasing, type AnyFunc, Cache, type DebouncedFunction, type DeepPartial, type FormatDurationOptions, FormatNumberCompact, Loop, type LoopEvents, type MemoizedFunction, type ParseTimeOptions, Pipe, type ReadDirOptions, type RetryOptions, type RndArrayOptions, type RndStrOptions, type SequentialMapContext, Storage, type TypedEmitter, chunk, chunkAdj, clampNumber, cluster, compactArray, createSampler, debounce, escapeRegex, forceArray, formatCurrency, formatDuration, formatETA, formatMemory, formatNumber, formatOrdinal, getAge, getFlag, getProp, hasFlag, hasProp, inverseLerp, isDefined, isEmpty, isInRange, isObject, isSortedArray, isString, isToday, lerp, memoize, merge, msToSecs, omit, parseTime, percent, pick, pluralize, prng, readDir, remap, retryPromise, rndChance, rndChoice, rndFloat, rndIndex, rndInt, rndString, searchSorted, secsToMs, seqMap, setProp, shuffle, sortBy, sum, throttle, toTitleCase, unique, wait, weightedRnd, wrap };
702
+ export { type AlphaCasing, type AnyFunc, Cache, type DebouncedFunction, type DeepPartial, type FormatDurationOptions, FormatNumberCompact, Loop, type LoopEvents, type MemoizedFunction, type ParseTimeOptions, Pipe, type ReadDirOptions, type RetryOptions, type RndArrayOptions, type RndStrOptions, type SequentialMapContext, Storage, type TypedEmitter, chunk, chunkAdj, clampNumber, cluster, compactArray, createSampler, debounce, escapeRegex, forceArray, formatCurrency, formatDuration, formatETA, formatMemory, formatNumber, formatOrdinal, getAge, getFlag, getProp, hasFlag, hasProp, inverseLerp, isDefined, isEmpty, isInRange, isObject, isSortedArray, isString, isToday, keyedQueue, lerp, memoize, merge, msToSecs, omit, parseTime, percent, pick, pluralize, prng, readDir, remap, retryPromise, rndChance, rndChoice, rndFloat, rndIndex, rndInt, rndString, searchSorted, secsToMs, seqMap, setProp, shuffle, sortBy, sum, throttle, toTitleCase, unique, wait, weightedRnd, wrap };
package/dist/index.d.ts CHANGED
@@ -208,6 +208,20 @@ interface RetryOptions {
208
208
  /** An AbortSignal to cancel the entire operation. */
209
209
  signal?: AbortSignal;
210
210
  }
211
+ /**
212
+ * Creates a queue that runs tasks in call order for each key, while different keys run concurrently.
213
+ * Task errors reach their callers without blocking later tasks. Drained keys are removed automatically.
214
+ * @example
215
+ * const queue = keyedQueue();
216
+ * const first = queue.run("user-1", () => saveInventory("user-1"));
217
+ * const second = queue.run("user-1", () => readInventory("user-1"));
218
+ * await Promise.all([first, second]); // The read runs after the save finishes
219
+ * console.log(queue.size); // 0 active keys
220
+ */
221
+ declare function keyedQueue(): {
222
+ run<T>(key: string | number, work: () => T | Promise<T>): Promise<T>;
223
+ readonly size: number;
224
+ };
211
225
  /**
212
226
  * Retries an async function until the maximum number of attempts is reached.
213
227
  *
@@ -275,17 +289,23 @@ declare function debounce<T extends (...args: unknown[]) => unknown>(fn: T, wait
275
289
  */
276
290
  declare function throttle<T extends (...args: unknown[]) => unknown>(fn: T, limit: number): (...args: Parameters<T>) => void;
277
291
 
278
- interface MemoizedFunction<T extends (...args: any[]) => any> {
292
+ interface MemoizedFunction<T extends (...args: never[]) => unknown> {
279
293
  (...args: Parameters<T>): ReturnType<T>;
280
294
  /** Clears the cache. */
281
295
  clear: () => void;
296
+ /** Removes the entry for these arguments, returning whether it existed. */
297
+ delete: (...args: Parameters<T>) => boolean;
298
+ /** Caches a value for these arguments, starting a fresh TTL. */
299
+ set: (value: ReturnType<T>, ...args: Parameters<T>) => void;
282
300
  }
283
301
  /**
284
302
  * Memoizes a function by caching its results based on the input arguments.
285
303
  * A resolver function can be provided to customize the key generation.
286
304
  * If no resolver is provided, the key will be generated by JSON stringify-ing the input arguments.
305
+ * Rejected promises are evicted. Expired entries are swept lazily on calls and writes,
306
+ * at most once per maxAge interval; no cleanup timers are created.
287
307
  * @param fn The function to memoize
288
- * @param resolver An optional resolver function to customize the key generation
308
+ * @param options Optional key resolver and cache lifetime in milliseconds
289
309
  * @example
290
310
  * // 1. Basic usage
291
311
  * const heavyCalc = memoize((n: number) => {
@@ -306,8 +326,14 @@ interface MemoizedFunction<T extends (...args: any[]) => any> {
306
326
  * @example
307
327
  * // 4. Manual cache clearing
308
328
  * heavyCalc.clear();
329
+ * @example
330
+ * // 5. Seed or invalidate one entry using the original arguments
331
+ * const getFlag = memoize(async (id: string) => readFlag(id));
332
+ * await writeFlag("drops", true);
333
+ * getFlag.set(Promise.resolve(true), "drops");
334
+ * getFlag.delete("drops");
309
335
  */
310
- declare function memoize<T extends (...args: any[]) => any>(fn: T, options?: {
336
+ declare function memoize<T extends (...args: never[]) => unknown>(fn: T, options?: {
311
337
  /** Function to generate a cache key from the input arguments. */
312
338
  resolver?: (...args: Parameters<T>) => string;
313
339
  /** The time to wait in milliseconds before expiring a cache entry. */
@@ -368,9 +394,9 @@ declare function FormatNumberCompact(num: number, locale?: Intl.LocalesArgument)
368
394
  * Duration formatter.
369
395
  *
370
396
  * Available styles:
371
- * - Digital (00:00)
372
- * - HMS
373
- * - YMDHMS.
397
+ * - "digital": MM:SS, HH:MM:SS, or D:HH:MM:SS.
398
+ * - "hms": hours, minutes, and seconds, with total hours that can exceed 23.
399
+ * - "ymdhms": days, hours, minutes, and seconds, with hours capped at 23.
374
400
  * @param target The target time to calculate from
375
401
  * @param style The output style to use
376
402
  * @param options Formatting options
@@ -673,4 +699,4 @@ declare function pluralize(count: number, singular: string, plural?: string): st
673
699
  */
674
700
  declare function toTitleCase(str: string, smart?: boolean): string;
675
701
 
676
- export { type AlphaCasing, type AnyFunc, Cache, type DebouncedFunction, type DeepPartial, type FormatDurationOptions, FormatNumberCompact, Loop, type LoopEvents, type MemoizedFunction, type ParseTimeOptions, Pipe, type ReadDirOptions, type RetryOptions, type RndArrayOptions, type RndStrOptions, type SequentialMapContext, Storage, type TypedEmitter, chunk, chunkAdj, clampNumber, cluster, compactArray, createSampler, debounce, escapeRegex, forceArray, formatCurrency, formatDuration, formatETA, formatMemory, formatNumber, formatOrdinal, getAge, getFlag, getProp, hasFlag, hasProp, inverseLerp, isDefined, isEmpty, isInRange, isObject, isSortedArray, isString, isToday, lerp, memoize, merge, msToSecs, omit, parseTime, percent, pick, pluralize, prng, readDir, remap, retryPromise, rndChance, rndChoice, rndFloat, rndIndex, rndInt, rndString, searchSorted, secsToMs, seqMap, setProp, shuffle, sortBy, sum, throttle, toTitleCase, unique, wait, weightedRnd, wrap };
702
+ export { type AlphaCasing, type AnyFunc, Cache, type DebouncedFunction, type DeepPartial, type FormatDurationOptions, FormatNumberCompact, Loop, type LoopEvents, type MemoizedFunction, type ParseTimeOptions, Pipe, type ReadDirOptions, type RetryOptions, type RndArrayOptions, type RndStrOptions, type SequentialMapContext, Storage, type TypedEmitter, chunk, chunkAdj, clampNumber, cluster, compactArray, createSampler, debounce, escapeRegex, forceArray, formatCurrency, formatDuration, formatETA, formatMemory, formatNumber, formatOrdinal, getAge, getFlag, getProp, hasFlag, hasProp, inverseLerp, isDefined, isEmpty, isInRange, isObject, isSortedArray, isString, isToday, keyedQueue, lerp, memoize, merge, msToSecs, omit, parseTime, percent, pick, pluralize, prng, readDir, remap, retryPromise, rndChance, rndChoice, rndFloat, rndIndex, rndInt, rndString, searchSorted, secsToMs, seqMap, setProp, shuffle, sortBy, sum, throttle, toTitleCase, unique, wait, weightedRnd, wrap };
package/dist/index.js CHANGED
@@ -504,6 +504,24 @@ function unique(array, key) {
504
504
  }
505
505
 
506
506
  // src/async.ts
507
+ function keyedQueue() {
508
+ const queues = /* @__PURE__ */ new Map();
509
+ return {
510
+ run(key, work) {
511
+ const previous = queues.get(key) ?? Promise.resolve();
512
+ const result = previous.then(work);
513
+ const release = () => {
514
+ if (queues.get(key) === tail) queues.delete(key);
515
+ };
516
+ const tail = result.then(release, release);
517
+ queues.set(key, tail);
518
+ return result;
519
+ },
520
+ get size() {
521
+ return queues.size;
522
+ }
523
+ };
524
+ }
507
525
  async function retryPromise(fn, options = {}) {
508
526
  const { attempts = 3, delay = 500, timeout, signal } = options;
509
527
  if (signal?.aborted) throw new Error("Operation aborted");
@@ -619,9 +637,28 @@ function throttle(fn, limit) {
619
637
  function memoize(fn, options = {}) {
620
638
  const cache = /* @__PURE__ */ new Map();
621
639
  const { resolver, maxAge } = options;
640
+ let lastSweep = Date.now();
641
+ const getKey = (...args) => resolver ? resolver(...args) : JSON.stringify(args);
642
+ const sweep = (now) => {
643
+ if (!maxAge || now - lastSweep < maxAge) return;
644
+ lastSweep = now;
645
+ for (const [key, entry] of cache) {
646
+ if (now - entry.timestamp >= maxAge) cache.delete(key);
647
+ }
648
+ };
649
+ const cacheValue = (key, value, timestamp) => {
650
+ const entry = { value, timestamp };
651
+ cache.set(key, entry);
652
+ if (value !== null && (typeof value === "object" || typeof value === "function") && "then" in value && typeof value.then === "function") {
653
+ void Promise.resolve(value).catch(() => {
654
+ if (cache.get(key) === entry) cache.delete(key);
655
+ });
656
+ }
657
+ };
622
658
  const memoized = function(...args) {
623
- const key = resolver ? resolver(...args) : JSON.stringify(args);
659
+ const key = getKey(...args);
624
660
  const now = Date.now();
661
+ sweep(now);
625
662
  const entry = cache.get(key);
626
663
  if (entry) {
627
664
  if (!maxAge || now - entry.timestamp < maxAge) {
@@ -630,10 +667,17 @@ function memoize(fn, options = {}) {
630
667
  cache.delete(key);
631
668
  }
632
669
  const result = fn.apply(this, args);
633
- cache.set(key, { value: result, timestamp: now });
670
+ cacheValue(key, result, now);
634
671
  return result;
635
672
  };
636
673
  memoized.clear = () => cache.clear();
674
+ memoized.delete = (...args) => cache.delete(getKey(...args));
675
+ memoized.set = (value, ...args) => {
676
+ const key = getKey(...args);
677
+ const now = Date.now();
678
+ sweep(now);
679
+ cacheValue(key, value, now);
680
+ };
637
681
  return memoized;
638
682
  }
639
683
 
@@ -684,7 +728,7 @@ function formatDuration(target, style = "hms", options = {}) {
684
728
  if (diff === 0) return style === "digital" ? "00:00" : "now";
685
729
  const s = Math.floor(diff / 1e3) % 60;
686
730
  const m = Math.floor(diff / 6e4) % 60;
687
- const h = Math.floor(diff / 36e5) % 24;
731
+ const h = style === "hms" ? Math.floor(diff / 36e5) : Math.floor(diff / 36e5) % 24;
688
732
  const d = Math.floor(diff / 864e5);
689
733
  if (style === "digital") {
690
734
  const parts = [m, s].map((v) => String(v).padStart(2, "0"));
@@ -979,6 +1023,7 @@ export {
979
1023
  isSortedArray,
980
1024
  isString,
981
1025
  isToday,
1026
+ keyedQueue,
982
1027
  lerp,
983
1028
  memoize,
984
1029
  merge,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qznt",
3
- "version": "2.1.1",
3
+ "version": "2.2.1",
4
4
  "description": "A lightweight typed utility toolkit for modern TypeScript environments.",
5
5
  "license": "MIT",
6
6
  "author": "xsqu1znt",
@@ -27,6 +27,7 @@
27
27
  },
28
28
  "scripts": {
29
29
  "build": "tsup",
30
+ "test": "pnpm build && node --test tests/*.test.mjs",
30
31
  "check": "tsc --noEmit",
31
32
  "format": "prettier --write \"./**/*.{ts,json}\""
32
33
  }