superjs-core 0.3.3 → 0.3.5

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.
@@ -1,5 +1,88 @@
1
1
  import { RetryOptions } from '../core/index.js';
2
2
 
3
+ interface QueueOptions {
4
+ concurrency?: number;
5
+ }
6
+ /**
7
+ * Priority-based async task queue with concurrency control.
8
+ *
9
+ * @example
10
+ * const queue = new Queue({ concurrency: 2 })
11
+ * const result = await queue.add(() => fetch('/api/data'))
12
+ */
13
+ declare class Queue<T = unknown> {
14
+ private _tasks;
15
+ private _running;
16
+ private _paused;
17
+ private _idleResolve;
18
+ private _maxConcurrency;
19
+ constructor(options?: QueueOptions);
20
+ get pending(): number;
21
+ get running(): number;
22
+ add<R>(task: () => Promise<R>, options?: {
23
+ priority?: number;
24
+ }): Promise<R>;
25
+ pause(): void;
26
+ resume(): void;
27
+ clear(): void;
28
+ onIdle(): Promise<void>;
29
+ private _process;
30
+ }
31
+
32
+ /**
33
+ * Semaphore for controlling concurrent access to a resource.
34
+ *
35
+ * @example
36
+ * const sem = new Semaphore(2)
37
+ * await sem.use(async () => {
38
+ * // Only 2 callers at a time
39
+ * await doSomething()
40
+ * })
41
+ */
42
+ declare class Semaphore {
43
+ private _available;
44
+ private _waiting;
45
+ constructor(concurrency: number);
46
+ get available(): number;
47
+ acquire(): Promise<() => void>;
48
+ use<T>(fn: () => Promise<T>): Promise<T>;
49
+ private _release;
50
+ }
51
+
52
+ /**
53
+ * Options for memoizeAsync.
54
+ */
55
+ interface MemoizeAsyncOptions {
56
+ /** Time-to-live in milliseconds (default: 60000) */
57
+ ttl?: number;
58
+ /** Return stale value while fetching fresh data (default: false) */
59
+ staleWhileRevalidate?: boolean;
60
+ /** Maximum number of cached entries (default: 100) */
61
+ maxSize?: number;
62
+ /** Custom cache key resolver. Defaults to JSON.stringify of args. */
63
+ resolver?: (...args: unknown[]) => string;
64
+ }
65
+ interface CacheEntry {
66
+ value: unknown;
67
+ timestamp: number;
68
+ promise?: Promise<unknown>;
69
+ }
70
+ /**
71
+ * Memoizes an async function with TTL, stale-while-revalidate, and bounded cache.
72
+ *
73
+ * @example
74
+ * const fetchData = memoizeAsync(async (id: string) => {
75
+ * return await api.fetch(id)
76
+ * }, { ttl: 5000, staleWhileRevalidate: true })
77
+ *
78
+ * const data = await fetchData('123') // cached for 5s
79
+ * const data2 = await fetchData('123') // returns cached, refreshes in bg
80
+ */
81
+ declare function memoizeAsync<T extends (...args: unknown[]) => Promise<unknown>>(fn: T, options?: MemoizeAsyncOptions): T & {
82
+ cache: Map<string, CacheEntry>;
83
+ clear: () => void;
84
+ };
85
+
3
86
  /**
4
87
  * Delays execution for the given number of milliseconds.
5
88
  */
@@ -41,4 +124,4 @@ interface Deferred<T> {
41
124
  reject: (reason: unknown) => void;
42
125
  }
43
126
 
44
- export { type Deferred, allSettledMap, deferred, parallelMap, pipeline, raceWithTimeout, retryAsync, sleep, timeout };
127
+ export { type Deferred, type MemoizeAsyncOptions, Queue, type QueueOptions, Semaphore, allSettledMap, deferred, memoizeAsync, parallelMap, pipeline, raceWithTimeout, retryAsync, sleep, timeout };
@@ -1,3 +1,151 @@
1
+ // src/async/queue.ts
2
+ var Queue = class {
3
+ _tasks = [];
4
+ _running = 0;
5
+ _paused = false;
6
+ _idleResolve = null;
7
+ _maxConcurrency;
8
+ constructor(options) {
9
+ this._maxConcurrency = options?.concurrency ?? 1;
10
+ if (this._maxConcurrency < 1) this._maxConcurrency = 1;
11
+ }
12
+ get pending() {
13
+ return this._tasks.length;
14
+ }
15
+ get running() {
16
+ return this._running;
17
+ }
18
+ add(task, options) {
19
+ return new Promise((resolve, reject) => {
20
+ this._tasks.push({
21
+ priority: options?.priority ?? 0,
22
+ task,
23
+ resolve,
24
+ reject
25
+ });
26
+ this._tasks.sort((a, b) => b.priority - a.priority);
27
+ this._process();
28
+ });
29
+ }
30
+ pause() {
31
+ this._paused = true;
32
+ }
33
+ resume() {
34
+ this._paused = false;
35
+ this._process();
36
+ }
37
+ clear() {
38
+ const err = new Error("Queue cleared");
39
+ for (const t of this._tasks) t.reject(err);
40
+ this._tasks = [];
41
+ }
42
+ onIdle() {
43
+ if (this._running === 0 && this._tasks.length === 0) return Promise.resolve();
44
+ return new Promise((resolve) => {
45
+ this._idleResolve = resolve;
46
+ });
47
+ }
48
+ _process() {
49
+ if (this._paused) return;
50
+ while (this._running < this._maxConcurrency && this._tasks.length > 0) {
51
+ const item = this._tasks.shift();
52
+ this._running++;
53
+ item.task().then((result) => item.resolve(result)).catch((err) => item.reject(err)).finally(() => {
54
+ this._running--;
55
+ this._process();
56
+ if (this._running === 0 && this._tasks.length === 0 && this._idleResolve) {
57
+ this._idleResolve();
58
+ this._idleResolve = null;
59
+ }
60
+ });
61
+ }
62
+ }
63
+ };
64
+
65
+ // src/async/semaphore.ts
66
+ var Semaphore = class {
67
+ _available;
68
+ _waiting = [];
69
+ constructor(concurrency) {
70
+ if (concurrency < 1) throw new RangeError("Semaphore concurrency must be >= 1");
71
+ this._available = concurrency;
72
+ }
73
+ get available() {
74
+ return this._available;
75
+ }
76
+ acquire() {
77
+ if (this._available > 0) {
78
+ this._available--;
79
+ return Promise.resolve(this._release.bind(this));
80
+ }
81
+ return new Promise((resolve) => {
82
+ this._waiting.push(() => {
83
+ this._available--;
84
+ resolve(this._release.bind(this));
85
+ });
86
+ });
87
+ }
88
+ async use(fn) {
89
+ const release = await this.acquire();
90
+ try {
91
+ return await fn();
92
+ } finally {
93
+ release();
94
+ }
95
+ }
96
+ _release() {
97
+ this._available++;
98
+ if (this._waiting.length > 0) {
99
+ const next = this._waiting.shift();
100
+ if (next) next();
101
+ }
102
+ }
103
+ };
104
+
105
+ // src/async/memoize.ts
106
+ function memoizeAsync(fn, options) {
107
+ const {
108
+ ttl = 6e4,
109
+ staleWhileRevalidate = false,
110
+ maxSize = 100,
111
+ resolver = (...args) => JSON.stringify(args)
112
+ } = options ?? {};
113
+ const cache = /* @__PURE__ */ new Map();
114
+ const memoized = (async (...args) => {
115
+ const key = resolver(...args);
116
+ const now = Date.now();
117
+ const entry = cache.get(key);
118
+ if (entry && now - entry.timestamp < ttl) {
119
+ return entry.value;
120
+ }
121
+ if (entry && staleWhileRevalidate && now - entry.timestamp >= ttl) {
122
+ if (entry.promise) {
123
+ return entry.value;
124
+ }
125
+ entry.promise = fn(...args).then((result2) => {
126
+ entry.value = result2;
127
+ entry.timestamp = now;
128
+ entry.promise = void 0;
129
+ return result2;
130
+ }).catch((err) => {
131
+ entry.promise = void 0;
132
+ throw err;
133
+ });
134
+ return entry.value;
135
+ }
136
+ const result = await fn(...args);
137
+ cache.set(key, { value: result, timestamp: now });
138
+ if (cache.size > maxSize) {
139
+ const firstKey = cache.keys().next().value;
140
+ if (firstKey !== void 0) cache.delete(firstKey);
141
+ }
142
+ return result;
143
+ });
144
+ memoized.cache = cache;
145
+ memoized.clear = () => cache.clear();
146
+ return memoized;
147
+ }
148
+
1
149
  // src/async/index.ts
2
150
  function sleep(ms) {
3
151
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -76,8 +224,11 @@ function deferred() {
76
224
  return { promise, resolve, reject };
77
225
  }
78
226
  export {
227
+ Queue,
228
+ Semaphore,
79
229
  allSettledMap,
80
230
  deferred,
231
+ memoizeAsync,
81
232
  parallelMap,
82
233
  pipeline,
83
234
  raceWithTimeout,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/async/index.ts"],"sourcesContent":["import type { RetryOptions } from '../core/index.js'\r\n\r\n/**\r\n * Delays execution for the given number of milliseconds.\r\n */\r\nexport function sleep(ms: number): Promise<void> {\r\n return new Promise(resolve => setTimeout(resolve, ms))\r\n}\r\n\r\n/**\r\n * Rejects a promise if it does not resolve within the specified timeout.\r\n */\r\nexport async function timeout<T>(promise: Promise<T>, ms: number, errorMessage?: string): Promise<T> {\r\n let timer: ReturnType<typeof setTimeout> | undefined\r\n const timeoutPromise = new Promise<never>((_, reject) => {\r\n timer = setTimeout(() => reject(new Error(errorMessage ?? `Promise timed out after ${ms}ms`)), ms)\r\n })\r\n try {\r\n return await Promise.race([promise, timeoutPromise])\r\n } finally {\r\n if (timer !== undefined) clearTimeout(timer)\r\n }\r\n}\r\n\r\n/**\r\n * Races a promise against a timeout, returning 'timeout' if the timer wins.\r\n */\r\nexport async function raceWithTimeout<T>(promise: Promise<T>, ms: number): Promise<T | 'timeout'> {\r\n let timer: ReturnType<typeof setTimeout> | undefined\r\n const timeoutPromise = new Promise<'timeout'>(resolve => {\r\n timer = setTimeout(() => resolve('timeout'), ms)\r\n })\r\n try {\r\n return await Promise.race([promise, timeoutPromise])\r\n } finally {\r\n if (timer !== undefined) clearTimeout(timer)\r\n }\r\n}\r\n\r\n/**\r\n * Maps over an array with an async function, using Promise.allSettled.\r\n * Returns an array of PromiseSettledResult.\r\n */\r\nexport async function allSettledMap<T, R>(\r\n items: T[],\r\n fn: (item: T) => Promise<R>,\r\n): Promise<PromiseSettledResult<R>[]> {\r\n return await Promise.allSettled(items.map(fn))\r\n}\r\n\r\n/**\r\n * Maps over an array with an async function, limiting concurrency.\r\n *\r\n * @param concurrency - Maximum number of concurrent operations (default Infinity)\r\n */\r\nexport async function parallelMap<T, R>(\r\n items: T[],\r\n fn: (item: T) => Promise<R>,\r\n concurrency = Number.POSITIVE_INFINITY,\r\n): Promise<R[]> {\r\n if (concurrency === Number.POSITIVE_INFINITY) {\r\n return await Promise.all(items.map(fn))\r\n }\r\n\r\n const results: R[] = []\r\n const queue = [...items]\r\n\r\n const worker = async (): Promise<void> => {\r\n while (queue.length > 0) {\r\n const item = queue.shift()!\r\n results.push(await fn(item))\r\n }\r\n }\r\n\r\n const workerCount = Math.min(concurrency, items.length)\r\n const workers = Array.from({ length: workerCount }, () => worker())\r\n await Promise.all(workers)\r\n return results\r\n}\r\n\r\n/**\r\n * Retries an async function with exponential backoff.\r\n */\r\nexport async function retryAsync<T>(fn: () => Promise<T>, options?: RetryOptions): Promise<T> {\r\n const { attempts = 3, baseDelay = 1000, maxDelay = 30000, shouldRetry = () => true } = options ?? {}\r\n let lastError: unknown\r\n for (let attempt = 0; attempt < attempts; attempt++) {\r\n try {\r\n return await fn()\r\n } catch (error) {\r\n lastError = error\r\n if (!shouldRetry(error) || attempt >= attempts - 1) break\r\n const delay = Math.min(baseDelay * 2 ** attempt + Math.random() * baseDelay, maxDelay)\r\n await sleep(delay)\r\n }\r\n }\r\n throw lastError\r\n}\r\n\r\n/**\r\n * Composes async functions, passing the result of each to the next.\r\n */\r\nexport async function pipeline<T>(initial: T, ...fns: Array<(arg: T) => Promise<T>>): Promise<T> {\r\n let result = initial\r\n for (const fn of fns) {\r\n result = await fn(result)\r\n }\r\n return result\r\n}\r\n\r\n/**\r\n * Creates a deferred promise with external resolve and reject methods.\r\n */\r\nexport function deferred<T>(): Deferred<T> {\r\n let resolve!: (value: T) => void\r\n let reject!: (reason: unknown) => void\r\n const promise = new Promise<T>((res, rej) => {\r\n resolve = res\r\n reject = rej\r\n })\r\n return { promise, resolve, reject }\r\n}\r\n\r\nexport interface Deferred<T> {\r\n promise: Promise<T>\r\n resolve: (value: T) => void\r\n reject: (reason: unknown) => void\r\n}\r\n"],"mappings":";AAKO,SAAS,MAAM,IAA2B;AAC/C,SAAO,IAAI,QAAQ,aAAW,WAAW,SAAS,EAAE,CAAC;AACvD;AAKA,eAAsB,QAAW,SAAqB,IAAY,cAAmC;AACnG,MAAI;AACJ,QAAM,iBAAiB,IAAI,QAAe,CAAC,GAAG,WAAW;AACvD,YAAQ,WAAW,MAAM,OAAO,IAAI,MAAM,gBAAgB,2BAA2B,EAAE,IAAI,CAAC,GAAG,EAAE;AAAA,EACnG,CAAC;AACD,MAAI;AACF,WAAO,MAAM,QAAQ,KAAK,CAAC,SAAS,cAAc,CAAC;AAAA,EACrD,UAAE;AACA,QAAI,UAAU,OAAW,cAAa,KAAK;AAAA,EAC7C;AACF;AAKA,eAAsB,gBAAmB,SAAqB,IAAoC;AAChG,MAAI;AACJ,QAAM,iBAAiB,IAAI,QAAmB,aAAW;AACvD,YAAQ,WAAW,MAAM,QAAQ,SAAS,GAAG,EAAE;AAAA,EACjD,CAAC;AACD,MAAI;AACF,WAAO,MAAM,QAAQ,KAAK,CAAC,SAAS,cAAc,CAAC;AAAA,EACrD,UAAE;AACA,QAAI,UAAU,OAAW,cAAa,KAAK;AAAA,EAC7C;AACF;AAMA,eAAsB,cACpB,OACA,IACoC;AACpC,SAAO,MAAM,QAAQ,WAAW,MAAM,IAAI,EAAE,CAAC;AAC/C;AAOA,eAAsB,YACpB,OACA,IACA,cAAc,OAAO,mBACP;AACd,MAAI,gBAAgB,OAAO,mBAAmB;AAC5C,WAAO,MAAM,QAAQ,IAAI,MAAM,IAAI,EAAE,CAAC;AAAA,EACxC;AAEA,QAAM,UAAe,CAAC;AACtB,QAAM,QAAQ,CAAC,GAAG,KAAK;AAEvB,QAAM,SAAS,YAA2B;AACxC,WAAO,MAAM,SAAS,GAAG;AACvB,YAAM,OAAO,MAAM,MAAM;AACzB,cAAQ,KAAK,MAAM,GAAG,IAAI,CAAC;AAAA,IAC7B;AAAA,EACF;AAEA,QAAM,cAAc,KAAK,IAAI,aAAa,MAAM,MAAM;AACtD,QAAM,UAAU,MAAM,KAAK,EAAE,QAAQ,YAAY,GAAG,MAAM,OAAO,CAAC;AAClE,QAAM,QAAQ,IAAI,OAAO;AACzB,SAAO;AACT;AAKA,eAAsB,WAAc,IAAsB,SAAoC;AAC5F,QAAM,EAAE,WAAW,GAAG,YAAY,KAAM,WAAW,KAAO,cAAc,MAAM,KAAK,IAAI,WAAW,CAAC;AACnG,MAAI;AACJ,WAAS,UAAU,GAAG,UAAU,UAAU,WAAW;AACnD,QAAI;AACF,aAAO,MAAM,GAAG;AAAA,IAClB,SAAS,OAAO;AACd,kBAAY;AACZ,UAAI,CAAC,YAAY,KAAK,KAAK,WAAW,WAAW,EAAG;AACpD,YAAM,QAAQ,KAAK,IAAI,YAAY,KAAK,UAAU,KAAK,OAAO,IAAI,WAAW,QAAQ;AACrF,YAAM,MAAM,KAAK;AAAA,IACnB;AAAA,EACF;AACA,QAAM;AACR;AAKA,eAAsB,SAAY,YAAe,KAAgD;AAC/F,MAAI,SAAS;AACb,aAAW,MAAM,KAAK;AACpB,aAAS,MAAM,GAAG,MAAM;AAAA,EAC1B;AACA,SAAO;AACT;AAKO,SAAS,WAA2B;AACzC,MAAI;AACJ,MAAI;AACJ,QAAM,UAAU,IAAI,QAAW,CAAC,KAAK,QAAQ;AAC3C,cAAU;AACV,aAAS;AAAA,EACX,CAAC;AACD,SAAO,EAAE,SAAS,SAAS,OAAO;AACpC;","names":[]}
1
+ {"version":3,"sources":["../../src/async/queue.ts","../../src/async/semaphore.ts","../../src/async/memoize.ts","../../src/async/index.ts"],"sourcesContent":["export interface QueueOptions {\n concurrency?: number\n}\n\n/**\n * Priority-based async task queue with concurrency control.\n *\n * @example\n * const queue = new Queue({ concurrency: 2 })\n * const result = await queue.add(() => fetch('/api/data'))\n */\nexport class Queue<T = unknown> {\n private _tasks: Array<{\n priority: number\n task: () => Promise<T>\n resolve: (value: T) => void\n reject: (reason: unknown) => void\n }> = []\n private _running = 0\n private _paused = false\n private _idleResolve: (() => void) | null = null\n private _maxConcurrency: number\n\n constructor(options?: QueueOptions) {\n this._maxConcurrency = options?.concurrency ?? 1\n if (this._maxConcurrency < 1) this._maxConcurrency = 1\n }\n\n get pending(): number {\n return this._tasks.length\n }\n\n get running(): number {\n return this._running\n }\n\n add<R>(task: () => Promise<R>, options?: { priority?: number }): Promise<R> {\n return new Promise<R>((resolve, reject) => {\n this._tasks.push({\n priority: options?.priority ?? 0,\n task: task as unknown as () => Promise<T>,\n resolve: resolve as (value: T) => void,\n reject,\n })\n this._tasks.sort((a, b) => b.priority - a.priority)\n this._process()\n })\n }\n\n pause(): void {\n this._paused = true\n }\n\n resume(): void {\n this._paused = false\n this._process()\n }\n\n clear(): void {\n const err = new Error('Queue cleared')\n for (const t of this._tasks) t.reject(err)\n this._tasks = []\n }\n\n onIdle(): Promise<void> {\n if (this._running === 0 && this._tasks.length === 0) return Promise.resolve()\n return new Promise<void>((resolve) => {\n this._idleResolve = resolve\n })\n }\n\n private _process(): void {\n if (this._paused) return\n while (this._running < this._maxConcurrency && this._tasks.length > 0) {\n const item = this._tasks.shift()!\n this._running++\n item\n .task()\n .then((result) => item.resolve(result))\n .catch((err) => item.reject(err))\n .finally(() => {\n this._running--\n this._process()\n if (this._running === 0 && this._tasks.length === 0 && this._idleResolve) {\n this._idleResolve()\n this._idleResolve = null\n }\n })\n }\n }\n}\n\nexport default Queue\n","/**\n * Semaphore for controlling concurrent access to a resource.\n *\n * @example\n * const sem = new Semaphore(2)\n * await sem.use(async () => {\n * // Only 2 callers at a time\n * await doSomething()\n * })\n */\nexport class Semaphore {\n private _available: number\n private _waiting: Array<() => void> = []\n\n constructor(concurrency: number) {\n if (concurrency < 1) throw new RangeError('Semaphore concurrency must be >= 1')\n this._available = concurrency\n }\n\n get available(): number {\n return this._available\n }\n\n acquire(): Promise<() => void> {\n if (this._available > 0) {\n this._available--\n return Promise.resolve(this._release.bind(this))\n }\n return new Promise<() => void>((resolve) => {\n this._waiting.push(() => {\n this._available--\n resolve(this._release.bind(this))\n })\n })\n }\n\n async use<T>(fn: () => Promise<T>): Promise<T> {\n const release = await this.acquire()\n try {\n return await fn()\n } finally {\n release()\n }\n }\n\n private _release(): void {\n this._available++\n if (this._waiting.length > 0) {\n const next = this._waiting.shift()\n if (next) next()\n }\n }\n}\n\nexport default Semaphore\n","/**\n * Options for memoizeAsync.\n */\nexport interface MemoizeAsyncOptions {\n /** Time-to-live in milliseconds (default: 60000) */\n ttl?: number\n /** Return stale value while fetching fresh data (default: false) */\n staleWhileRevalidate?: boolean\n /** Maximum number of cached entries (default: 100) */\n maxSize?: number\n /** Custom cache key resolver. Defaults to JSON.stringify of args. */\n resolver?: (...args: unknown[]) => string\n}\n\ninterface CacheEntry {\n value: unknown\n timestamp: number\n promise?: Promise<unknown>\n}\n\n/**\n * Memoizes an async function with TTL, stale-while-revalidate, and bounded cache.\n *\n * @example\n * const fetchData = memoizeAsync(async (id: string) => {\n * return await api.fetch(id)\n * }, { ttl: 5000, staleWhileRevalidate: true })\n *\n * const data = await fetchData('123') // cached for 5s\n * const data2 = await fetchData('123') // returns cached, refreshes in bg\n */\nexport function memoizeAsync<T extends (...args: unknown[]) => Promise<unknown>>(\n fn: T,\n options?: MemoizeAsyncOptions,\n): T & {\n cache: Map<string, CacheEntry>\n clear: () => void\n} {\n const {\n ttl = 60000,\n staleWhileRevalidate = false,\n maxSize = 100,\n resolver = (...args: unknown[]) => JSON.stringify(args),\n } = options ?? {}\n\n const cache = new Map<string, CacheEntry>()\n\n const memoized = (async (...args: unknown[]): Promise<unknown> => {\n const key = resolver(...args)\n const now = Date.now()\n const entry = cache.get(key)\n\n if (entry && now - entry.timestamp < ttl) {\n return entry.value\n }\n\n if (entry && staleWhileRevalidate && now - entry.timestamp >= ttl) {\n if (entry.promise) {\n return entry.value\n }\n entry.promise = fn(...args)\n .then((result) => {\n entry.value = result\n entry.timestamp = now\n entry.promise = undefined\n return result\n })\n .catch((err) => {\n entry.promise = undefined\n throw err\n })\n return entry.value\n }\n\n const result = await fn(...args)\n cache.set(key, { value: result, timestamp: now })\n\n if (cache.size > maxSize) {\n const firstKey = cache.keys().next().value\n if (firstKey !== undefined) cache.delete(firstKey)\n }\n\n return result\n }) as T & {\n cache: Map<string, CacheEntry>\n clear: () => void\n }\n\n memoized.cache = cache\n memoized.clear = () => cache.clear()\n\n return memoized\n}\n\nexport default memoizeAsync\n","import type { RetryOptions } from '../core/index.js'\r\n\r\n/**\r\n * Delays execution for the given number of milliseconds.\r\n */\r\nexport function sleep(ms: number): Promise<void> {\r\n return new Promise(resolve => setTimeout(resolve, ms))\r\n}\r\n\r\n/**\r\n * Rejects a promise if it does not resolve within the specified timeout.\r\n */\r\nexport async function timeout<T>(promise: Promise<T>, ms: number, errorMessage?: string): Promise<T> {\r\n let timer: ReturnType<typeof setTimeout> | undefined\r\n const timeoutPromise = new Promise<never>((_, reject) => {\r\n timer = setTimeout(() => reject(new Error(errorMessage ?? `Promise timed out after ${ms}ms`)), ms)\r\n })\r\n try {\r\n return await Promise.race([promise, timeoutPromise])\r\n } finally {\r\n if (timer !== undefined) clearTimeout(timer)\r\n }\r\n}\r\n\r\n/**\r\n * Races a promise against a timeout, returning 'timeout' if the timer wins.\r\n */\r\nexport async function raceWithTimeout<T>(promise: Promise<T>, ms: number): Promise<T | 'timeout'> {\r\n let timer: ReturnType<typeof setTimeout> | undefined\r\n const timeoutPromise = new Promise<'timeout'>(resolve => {\r\n timer = setTimeout(() => resolve('timeout'), ms)\r\n })\r\n try {\r\n return await Promise.race([promise, timeoutPromise])\r\n } finally {\r\n if (timer !== undefined) clearTimeout(timer)\r\n }\r\n}\r\n\r\n/**\r\n * Maps over an array with an async function, using Promise.allSettled.\r\n * Returns an array of PromiseSettledResult.\r\n */\r\nexport async function allSettledMap<T, R>(\r\n items: T[],\r\n fn: (item: T) => Promise<R>,\r\n): Promise<PromiseSettledResult<R>[]> {\r\n return await Promise.allSettled(items.map(fn))\r\n}\r\n\r\n/**\r\n * Maps over an array with an async function, limiting concurrency.\r\n *\r\n * @param concurrency - Maximum number of concurrent operations (default Infinity)\r\n */\r\nexport async function parallelMap<T, R>(\r\n items: T[],\r\n fn: (item: T) => Promise<R>,\r\n concurrency = Number.POSITIVE_INFINITY,\r\n): Promise<R[]> {\r\n if (concurrency === Number.POSITIVE_INFINITY) {\r\n return await Promise.all(items.map(fn))\r\n }\r\n\r\n const results: R[] = []\r\n const queue = [...items]\r\n\r\n const worker = async (): Promise<void> => {\r\n while (queue.length > 0) {\r\n const item = queue.shift()!\r\n results.push(await fn(item))\r\n }\r\n }\r\n\r\n const workerCount = Math.min(concurrency, items.length)\r\n const workers = Array.from({ length: workerCount }, () => worker())\r\n await Promise.all(workers)\r\n return results\r\n}\r\n\r\n/**\r\n * Retries an async function with exponential backoff.\r\n */\r\nexport async function retryAsync<T>(fn: () => Promise<T>, options?: RetryOptions): Promise<T> {\r\n const { attempts = 3, baseDelay = 1000, maxDelay = 30000, shouldRetry = () => true } = options ?? {}\r\n let lastError: unknown\r\n for (let attempt = 0; attempt < attempts; attempt++) {\r\n try {\r\n return await fn()\r\n } catch (error) {\r\n lastError = error\r\n if (!shouldRetry(error) || attempt >= attempts - 1) break\r\n const delay = Math.min(baseDelay * 2 ** attempt + Math.random() * baseDelay, maxDelay)\r\n await sleep(delay)\r\n }\r\n }\r\n throw lastError\r\n}\r\n\r\n/**\r\n * Composes async functions, passing the result of each to the next.\r\n */\r\nexport async function pipeline<T>(initial: T, ...fns: Array<(arg: T) => Promise<T>>): Promise<T> {\r\n let result = initial\r\n for (const fn of fns) {\r\n result = await fn(result)\r\n }\r\n return result\r\n}\r\n\r\n/**\r\n * Creates a deferred promise with external resolve and reject methods.\r\n */\r\nexport function deferred<T>(): Deferred<T> {\r\n let resolve!: (value: T) => void\r\n let reject!: (reason: unknown) => void\r\n const promise = new Promise<T>((res, rej) => {\r\n resolve = res\r\n reject = rej\r\n })\r\n return { promise, resolve, reject }\r\n}\r\n\r\nexport interface Deferred<T> {\r\n promise: Promise<T>\r\n resolve: (value: T) => void\r\n reject: (reason: unknown) => void\r\n}\r\n\r\n// ─── Queue, Semaphore, memoizeAsync ─────────────────────\r\nexport { Queue } from './queue.js'\r\nexport type { QueueOptions } from './queue.js'\r\nexport { Semaphore } from './semaphore.js'\r\nexport { memoizeAsync } from './memoize.js'\r\nexport type { MemoizeAsyncOptions } from './memoize.js'\r\n"],"mappings":"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","names":["result"]}
@@ -71,5 +71,11 @@ declare function last<T>(items: T[]): T | undefined;
71
71
  */
72
72
  declare function isEmpty(value: unknown): boolean;
73
73
  type SortDirection = 'asc' | 'desc';
74
+ declare function topoSort<T extends {
75
+ id: string;
76
+ dependencies?: string[];
77
+ }>(items: T[]): T[];
78
+ declare function slidingWindows<T>(items: T[], size: number, step?: number): T[][];
79
+ declare function tumblingWindows<T>(items: T[], size: number): T[][];
74
80
 
75
- export { type SortDirection, chunk, first, flatten, groupBy, isEmpty, keyBy, last, omit, orderBy, pick, pluck, sample, sampleSize, shuffle, sortBy, uniq, uniqueBy };
81
+ export { type SortDirection, chunk, first, flatten, groupBy, isEmpty, keyBy, last, omit, orderBy, pick, pluck, sample, sampleSize, shuffle, slidingWindows, sortBy, topoSort, tumblingWindows, uniq, uniqueBy };
@@ -118,6 +118,58 @@ function isEmpty(value) {
118
118
  if (value !== null && typeof value === "object") return Object.keys(value).length === 0;
119
119
  return false;
120
120
  }
121
+ function topoSort(items) {
122
+ const adj = /* @__PURE__ */ new Map();
123
+ const inDegree = /* @__PURE__ */ new Map();
124
+ const itemMap = /* @__PURE__ */ new Map();
125
+ for (const item of items) {
126
+ itemMap.set(item.id, item);
127
+ if (!adj.has(item.id)) adj.set(item.id, []);
128
+ if (!inDegree.has(item.id)) inDegree.set(item.id, 0);
129
+ }
130
+ for (const item of items) {
131
+ if (item.dependencies) {
132
+ for (const depId of item.dependencies) {
133
+ adj.get(depId)?.push(item.id);
134
+ inDegree.set(item.id, (inDegree.get(item.id) ?? 0) + 1);
135
+ }
136
+ }
137
+ }
138
+ const queue = [];
139
+ for (const [id, degree] of inDegree) {
140
+ if (degree === 0) queue.push(id);
141
+ }
142
+ const sorted = [];
143
+ while (queue.length > 0) {
144
+ const id = queue.shift();
145
+ sorted.push(id);
146
+ for (const neighbor of adj.get(id) ?? []) {
147
+ const newDegree = (inDegree.get(neighbor) ?? 1) - 1;
148
+ inDegree.set(neighbor, newDegree);
149
+ if (newDegree === 0) queue.push(neighbor);
150
+ }
151
+ }
152
+ if (sorted.length !== items.length) {
153
+ throw new Error("Circular dependency detected");
154
+ }
155
+ return sorted.map((id) => itemMap.get(id));
156
+ }
157
+ function slidingWindows(items, size, step = 1) {
158
+ if (size <= 0 || items.length === 0 || step <= 0) return [];
159
+ const result = [];
160
+ for (let i = 0; i + size <= items.length; i += step) {
161
+ result.push(items.slice(i, i + size));
162
+ }
163
+ return result;
164
+ }
165
+ function tumblingWindows(items, size) {
166
+ if (size <= 0 || items.length === 0) return [];
167
+ const result = [];
168
+ for (let i = 0; i < items.length; i += size) {
169
+ result.push(items.slice(i, i + size));
170
+ }
171
+ return result;
172
+ }
121
173
  export {
122
174
  chunk,
123
175
  first,
@@ -133,7 +185,10 @@ export {
133
185
  sample,
134
186
  sampleSize,
135
187
  shuffle,
188
+ slidingWindows,
136
189
  sortBy,
190
+ topoSort,
191
+ tumblingWindows,
137
192
  uniq,
138
193
  uniqueBy
139
194
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/collection/index.ts"],"sourcesContent":["/**\r\n * Groups an array of items by a key extracted from each item.\r\n *\r\n * @example groupBy([{ type: 'a' }, { type: 'b' }, { type: 'a' }], x => x.type)\r\n * // => { a: [{ type: 'a' }, { type: 'a' }], b: [{ type: 'b' }] }\r\n */\r\nexport function groupBy<T, K extends string | number | symbol>(items: T[], keyFn: (item: T) => K): Record<K, T[]> {\r\n const result = {} as Record<K, T[]>\r\n for (const item of items) {\r\n const key = keyFn(item)\r\n if (!result[key]) result[key] = []\r\n result[key]!.push(item)\r\n }\r\n return result\r\n}\r\n\r\n/**\r\n * Creates an object keyed by the result of keyFn for each item.\r\n *\r\n * @example keyBy([{ id: 1, name: 'a' }, { id: 2, name: 'b' }], x => x.id)\r\n * // => { 1: { id: 1, name: 'a' }, 2: { id: 2, name: 'b' } }\r\n */\r\nexport function keyBy<T, K extends string | number | symbol>(items: T[], keyFn: (item: T) => K): Record<K, T> {\r\n const result = {} as Record<K, T>\r\n for (const item of items) {\r\n const key = keyFn(item)\r\n result[key] = item\r\n }\r\n return result\r\n}\r\n\r\n/**\r\n * Returns a copy of the object with the specified keys omitted.\r\n */\r\nexport function omit<T extends Record<string, unknown>, K extends keyof T>(obj: T, keys: K[]): Omit<T, K> {\r\n const result = { ...obj } as T\r\n for (const key of keys) {\r\n delete result[key]\r\n }\r\n return result as Omit<T, K>\r\n}\r\n\r\n/**\r\n * Returns a copy of the object with only the specified keys.\r\n */\r\nexport function pick<T extends Record<string, unknown>, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {\r\n const result = {} as Pick<T, K>\r\n for (const key of keys) {\r\n if (key in obj) {\r\n result[key] = obj[key]\r\n }\r\n }\r\n return result\r\n}\r\n\r\n/**\r\n * Extracts a property value from each item in an array.\r\n */\r\nexport function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {\r\n return items.map(item => item[key])\r\n}\r\n\r\n/**\r\n * Randomizes array order in-place using the Fisher-Yates algorithm.\r\n */\r\nexport function shuffle<T>(items: T[]): T[] {\r\n const result = [...items]\r\n for (let i = result.length - 1; i > 0; i--) {\r\n const j = Math.floor(Math.random() * (i + 1))\r\n const temp = result[i]!\r\n result[i] = result[j]!\r\n result[j] = temp\r\n }\r\n return result\r\n}\r\n\r\n/**\r\n * Returns a random element from the array, or undefined if empty.\r\n */\r\nexport function sample<T>(items: T[]): T | undefined {\r\n return items.length > 0 ? items[Math.floor(Math.random() * items.length)] : undefined\r\n}\r\n\r\n/**\r\n * Returns an array of `size` random elements (without duplicates).\r\n */\r\nexport function sampleSize<T>(items: T[], size: number): T[] {\r\n if (size <= 0 || items.length === 0) return []\r\n const pool = shuffle(items)\r\n return pool.slice(0, Math.min(size, items.length))\r\n}\r\n\r\n/**\r\n * Splits an array into chunks of the specified size.\r\n */\r\nexport function chunk<T>(items: T[], size: number): T[][] {\r\n if (size <= 0 || items.length === 0) return []\r\n const result: T[][] = []\r\n for (let i = 0; i < items.length; i += size) {\r\n result.push(items.slice(i, i + size))\r\n }\r\n return result\r\n}\r\n\r\n/**\r\n * Returns a sorted copy of the array using the provided criteria functions.\r\n * Earlier criteria take precedence.\r\n */\r\nfunction compareValues(a: unknown, b: unknown): number {\r\n if (a === b) return 0\r\n if (a == null) return -1\r\n if (b == null) return 1\r\n if (typeof a === 'string' && typeof b === 'string') return a < b ? -1 : 1\r\n if (typeof a === 'number' && typeof b === 'number') return a < b ? -1 : 1\r\n return String(a) < String(b) ? -1 : 1\r\n}\r\n\r\nexport function sortBy<T>(items: T[], ...criteria: Array<(item: T) => unknown>): T[] {\r\n return [...items].sort((a, b) => {\r\n for (const criterion of criteria) {\r\n const cmp = compareValues(criterion(a), criterion(b))\r\n if (cmp !== 0) return cmp\r\n }\r\n return 0\r\n })\r\n}\r\n\r\n/**\r\n * Returns a sorted copy of the array by a single key and direction.\r\n */\r\nexport function orderBy<T>(items: T[], key: (item: T) => unknown, direction: SortDirection = 'asc'): T[] {\r\n return [...items].sort((a, b) => {\r\n const cmp = compareValues(key(a), key(b))\r\n return direction === 'asc' ? cmp : -cmp\r\n })\r\n}\r\n\r\n/**\r\n * Returns unique elements based on the result of keyFn.\r\n */\r\nexport function uniqueBy<T>(items: T[], keyFn: (item: T) => unknown): T[] {\r\n const seen = new Set<unknown>()\r\n return items.filter(item => {\r\n const key = keyFn(item)\r\n if (seen.has(key)) return false\r\n seen.add(key)\r\n return true\r\n })\r\n}\r\n\r\n/**\r\n * Flattens an array of arrays one level.\r\n */\r\nexport function flatten<T>(items: T[][]): T[] {\r\n const result: T[] = []\r\n for (const sub of items) {\r\n for (const item of sub) {\r\n result.push(item)\r\n }\r\n }\r\n return result\r\n}\r\n\r\n/**\r\n * Returns an array with unique primitive values.\r\n */\r\nexport function uniq<T>(items: T[]): T[] {\r\n return [...new Set(items)]\r\n}\r\n\r\n/**\r\n * Returns the first element, or undefined if empty.\r\n */\r\nexport function first<T>(items: T[]): T | undefined {\r\n return items[0]\r\n}\r\n\r\n/**\r\n * Returns the last element, or undefined if empty.\r\n */\r\nexport function last<T>(items: T[]): T | undefined {\r\n return items[items.length - 1]\r\n}\r\n\r\n/**\r\n * Checks if a value is empty.\r\n * Works for arrays, objects, strings, Map, and Set.\r\n */\r\nexport function isEmpty(value: unknown): boolean {\r\n if (Array.isArray(value)) return value.length === 0\r\n if (typeof value === 'string') return value.length === 0\r\n if (value instanceof Map || value instanceof Set) return value.size === 0\r\n if (value !== null && typeof value === 'object') return Object.keys(value as Record<string, unknown>).length === 0\r\n return false\r\n}\r\n\r\nexport type SortDirection = 'asc' | 'desc'\r\n"],"mappings":";AAMO,SAAS,QAA+C,OAAY,OAAuC;AAChH,QAAM,SAAS,CAAC;AAChB,aAAW,QAAQ,OAAO;AACxB,UAAM,MAAM,MAAM,IAAI;AACtB,QAAI,CAAC,OAAO,GAAG,EAAG,QAAO,GAAG,IAAI,CAAC;AACjC,WAAO,GAAG,EAAG,KAAK,IAAI;AAAA,EACxB;AACA,SAAO;AACT;AAQO,SAAS,MAA6C,OAAY,OAAqC;AAC5G,QAAM,SAAS,CAAC;AAChB,aAAW,QAAQ,OAAO;AACxB,UAAM,MAAM,MAAM,IAAI;AACtB,WAAO,GAAG,IAAI;AAAA,EAChB;AACA,SAAO;AACT;AAKO,SAAS,KAA2D,KAAQ,MAAuB;AACxG,QAAM,SAAS,EAAE,GAAG,IAAI;AACxB,aAAW,OAAO,MAAM;AACtB,WAAO,OAAO,GAAG;AAAA,EACnB;AACA,SAAO;AACT;AAKO,SAAS,KAA2D,KAAQ,MAAuB;AACxG,QAAM,SAAS,CAAC;AAChB,aAAW,OAAO,MAAM;AACtB,QAAI,OAAO,KAAK;AACd,aAAO,GAAG,IAAI,IAAI,GAAG;AAAA,IACvB;AAAA,EACF;AACA,SAAO;AACT;AAKO,SAAS,MAA4B,OAAY,KAAgB;AACtE,SAAO,MAAM,IAAI,UAAQ,KAAK,GAAG,CAAC;AACpC;AAKO,SAAS,QAAW,OAAiB;AAC1C,QAAM,SAAS,CAAC,GAAG,KAAK;AACxB,WAAS,IAAI,OAAO,SAAS,GAAG,IAAI,GAAG,KAAK;AAC1C,UAAM,IAAI,KAAK,MAAM,KAAK,OAAO,KAAK,IAAI,EAAE;AAC5C,UAAM,OAAO,OAAO,CAAC;AACrB,WAAO,CAAC,IAAI,OAAO,CAAC;AACpB,WAAO,CAAC,IAAI;AAAA,EACd;AACA,SAAO;AACT;AAKO,SAAS,OAAU,OAA2B;AACnD,SAAO,MAAM,SAAS,IAAI,MAAM,KAAK,MAAM,KAAK,OAAO,IAAI,MAAM,MAAM,CAAC,IAAI;AAC9E;AAKO,SAAS,WAAc,OAAY,MAAmB;AAC3D,MAAI,QAAQ,KAAK,MAAM,WAAW,EAAG,QAAO,CAAC;AAC7C,QAAM,OAAO,QAAQ,KAAK;AAC1B,SAAO,KAAK,MAAM,GAAG,KAAK,IAAI,MAAM,MAAM,MAAM,CAAC;AACnD;AAKO,SAAS,MAAS,OAAY,MAAqB;AACxD,MAAI,QAAQ,KAAK,MAAM,WAAW,EAAG,QAAO,CAAC;AAC7C,QAAM,SAAgB,CAAC;AACvB,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK,MAAM;AAC3C,WAAO,KAAK,MAAM,MAAM,GAAG,IAAI,IAAI,CAAC;AAAA,EACtC;AACA,SAAO;AACT;AAMA,SAAS,cAAc,GAAY,GAAoB;AACrD,MAAI,MAAM,EAAG,QAAO;AACpB,MAAI,KAAK,KAAM,QAAO;AACtB,MAAI,KAAK,KAAM,QAAO;AACtB,MAAI,OAAO,MAAM,YAAY,OAAO,MAAM,SAAU,QAAO,IAAI,IAAI,KAAK;AACxE,MAAI,OAAO,MAAM,YAAY,OAAO,MAAM,SAAU,QAAO,IAAI,IAAI,KAAK;AACxE,SAAO,OAAO,CAAC,IAAI,OAAO,CAAC,IAAI,KAAK;AACtC;AAEO,SAAS,OAAU,UAAe,UAA4C;AACnF,SAAO,CAAC,GAAG,KAAK,EAAE,KAAK,CAAC,GAAG,MAAM;AAC/B,eAAW,aAAa,UAAU;AAChC,YAAM,MAAM,cAAc,UAAU,CAAC,GAAG,UAAU,CAAC,CAAC;AACpD,UAAI,QAAQ,EAAG,QAAO;AAAA,IACxB;AACA,WAAO;AAAA,EACT,CAAC;AACH;AAKO,SAAS,QAAW,OAAY,KAA2B,YAA2B,OAAY;AACvG,SAAO,CAAC,GAAG,KAAK,EAAE,KAAK,CAAC,GAAG,MAAM;AAC/B,UAAM,MAAM,cAAc,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC;AACxC,WAAO,cAAc,QAAQ,MAAM,CAAC;AAAA,EACtC,CAAC;AACH;AAKO,SAAS,SAAY,OAAY,OAAkC;AACxE,QAAM,OAAO,oBAAI,IAAa;AAC9B,SAAO,MAAM,OAAO,UAAQ;AAC1B,UAAM,MAAM,MAAM,IAAI;AACtB,QAAI,KAAK,IAAI,GAAG,EAAG,QAAO;AAC1B,SAAK,IAAI,GAAG;AACZ,WAAO;AAAA,EACT,CAAC;AACH;AAKO,SAAS,QAAW,OAAmB;AAC5C,QAAM,SAAc,CAAC;AACrB,aAAW,OAAO,OAAO;AACvB,eAAW,QAAQ,KAAK;AACtB,aAAO,KAAK,IAAI;AAAA,IAClB;AAAA,EACF;AACA,SAAO;AACT;AAKO,SAAS,KAAQ,OAAiB;AACvC,SAAO,CAAC,GAAG,IAAI,IAAI,KAAK,CAAC;AAC3B;AAKO,SAAS,MAAS,OAA2B;AAClD,SAAO,MAAM,CAAC;AAChB;AAKO,SAAS,KAAQ,OAA2B;AACjD,SAAO,MAAM,MAAM,SAAS,CAAC;AAC/B;AAMO,SAAS,QAAQ,OAAyB;AAC/C,MAAI,MAAM,QAAQ,KAAK,EAAG,QAAO,MAAM,WAAW;AAClD,MAAI,OAAO,UAAU,SAAU,QAAO,MAAM,WAAW;AACvD,MAAI,iBAAiB,OAAO,iBAAiB,IAAK,QAAO,MAAM,SAAS;AACxE,MAAI,UAAU,QAAQ,OAAO,UAAU,SAAU,QAAO,OAAO,KAAK,KAAgC,EAAE,WAAW;AACjH,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../../src/collection/index.ts"],"sourcesContent":["/**\r\n * Groups an array of items by a key extracted from each item.\r\n *\r\n * @example groupBy([{ type: 'a' }, { type: 'b' }, { type: 'a' }], x => x.type)\r\n * // => { a: [{ type: 'a' }, { type: 'a' }], b: [{ type: 'b' }] }\r\n */\r\nexport function groupBy<T, K extends string | number | symbol>(items: T[], keyFn: (item: T) => K): Record<K, T[]> {\r\n const result = {} as Record<K, T[]>\r\n for (const item of items) {\r\n const key = keyFn(item)\r\n if (!result[key]) result[key] = []\r\n result[key]!.push(item)\r\n }\r\n return result\r\n}\r\n\r\n/**\r\n * Creates an object keyed by the result of keyFn for each item.\r\n *\r\n * @example keyBy([{ id: 1, name: 'a' }, { id: 2, name: 'b' }], x => x.id)\r\n * // => { 1: { id: 1, name: 'a' }, 2: { id: 2, name: 'b' } }\r\n */\r\nexport function keyBy<T, K extends string | number | symbol>(items: T[], keyFn: (item: T) => K): Record<K, T> {\r\n const result = {} as Record<K, T>\r\n for (const item of items) {\r\n const key = keyFn(item)\r\n result[key] = item\r\n }\r\n return result\r\n}\r\n\r\n/**\r\n * Returns a copy of the object with the specified keys omitted.\r\n */\r\nexport function omit<T extends Record<string, unknown>, K extends keyof T>(obj: T, keys: K[]): Omit<T, K> {\r\n const result = { ...obj } as T\r\n for (const key of keys) {\r\n delete result[key]\r\n }\r\n return result as Omit<T, K>\r\n}\r\n\r\n/**\r\n * Returns a copy of the object with only the specified keys.\r\n */\r\nexport function pick<T extends Record<string, unknown>, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {\r\n const result = {} as Pick<T, K>\r\n for (const key of keys) {\r\n if (key in obj) {\r\n result[key] = obj[key]\r\n }\r\n }\r\n return result\r\n}\r\n\r\n/**\r\n * Extracts a property value from each item in an array.\r\n */\r\nexport function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {\r\n return items.map(item => item[key])\r\n}\r\n\r\n/**\r\n * Randomizes array order in-place using the Fisher-Yates algorithm.\r\n */\r\nexport function shuffle<T>(items: T[]): T[] {\r\n const result = [...items]\r\n for (let i = result.length - 1; i > 0; i--) {\r\n const j = Math.floor(Math.random() * (i + 1))\r\n const temp = result[i]!\r\n result[i] = result[j]!\r\n result[j] = temp\r\n }\r\n return result\r\n}\r\n\r\n/**\r\n * Returns a random element from the array, or undefined if empty.\r\n */\r\nexport function sample<T>(items: T[]): T | undefined {\r\n return items.length > 0 ? items[Math.floor(Math.random() * items.length)] : undefined\r\n}\r\n\r\n/**\r\n * Returns an array of `size` random elements (without duplicates).\r\n */\r\nexport function sampleSize<T>(items: T[], size: number): T[] {\r\n if (size <= 0 || items.length === 0) return []\r\n const pool = shuffle(items)\r\n return pool.slice(0, Math.min(size, items.length))\r\n}\r\n\r\n/**\r\n * Splits an array into chunks of the specified size.\r\n */\r\nexport function chunk<T>(items: T[], size: number): T[][] {\r\n if (size <= 0 || items.length === 0) return []\r\n const result: T[][] = []\r\n for (let i = 0; i < items.length; i += size) {\r\n result.push(items.slice(i, i + size))\r\n }\r\n return result\r\n}\r\n\r\n/**\r\n * Returns a sorted copy of the array using the provided criteria functions.\r\n * Earlier criteria take precedence.\r\n */\r\nfunction compareValues(a: unknown, b: unknown): number {\r\n if (a === b) return 0\r\n if (a == null) return -1\r\n if (b == null) return 1\r\n if (typeof a === 'string' && typeof b === 'string') return a < b ? -1 : 1\r\n if (typeof a === 'number' && typeof b === 'number') return a < b ? -1 : 1\r\n return String(a) < String(b) ? -1 : 1\r\n}\r\n\r\nexport function sortBy<T>(items: T[], ...criteria: Array<(item: T) => unknown>): T[] {\r\n return [...items].sort((a, b) => {\r\n for (const criterion of criteria) {\r\n const cmp = compareValues(criterion(a), criterion(b))\r\n if (cmp !== 0) return cmp\r\n }\r\n return 0\r\n })\r\n}\r\n\r\n/**\r\n * Returns a sorted copy of the array by a single key and direction.\r\n */\r\nexport function orderBy<T>(items: T[], key: (item: T) => unknown, direction: SortDirection = 'asc'): T[] {\r\n return [...items].sort((a, b) => {\r\n const cmp = compareValues(key(a), key(b))\r\n return direction === 'asc' ? cmp : -cmp\r\n })\r\n}\r\n\r\n/**\r\n * Returns unique elements based on the result of keyFn.\r\n */\r\nexport function uniqueBy<T>(items: T[], keyFn: (item: T) => unknown): T[] {\r\n const seen = new Set<unknown>()\r\n return items.filter(item => {\r\n const key = keyFn(item)\r\n if (seen.has(key)) return false\r\n seen.add(key)\r\n return true\r\n })\r\n}\r\n\r\n/**\r\n * Flattens an array of arrays one level.\r\n */\r\nexport function flatten<T>(items: T[][]): T[] {\r\n const result: T[] = []\r\n for (const sub of items) {\r\n for (const item of sub) {\r\n result.push(item)\r\n }\r\n }\r\n return result\r\n}\r\n\r\n/**\r\n * Returns an array with unique primitive values.\r\n */\r\nexport function uniq<T>(items: T[]): T[] {\r\n return [...new Set(items)]\r\n}\r\n\r\n/**\r\n * Returns the first element, or undefined if empty.\r\n */\r\nexport function first<T>(items: T[]): T | undefined {\r\n return items[0]\r\n}\r\n\r\n/**\r\n * Returns the last element, or undefined if empty.\r\n */\r\nexport function last<T>(items: T[]): T | undefined {\r\n return items[items.length - 1]\r\n}\r\n\r\n/**\r\n * Checks if a value is empty.\r\n * Works for arrays, objects, strings, Map, and Set.\r\n */\r\nexport function isEmpty(value: unknown): boolean {\r\n if (Array.isArray(value)) return value.length === 0\r\n if (typeof value === 'string') return value.length === 0\r\n if (value instanceof Map || value instanceof Set) return value.size === 0\r\n if (value !== null && typeof value === 'object') return Object.keys(value as Record<string, unknown>).length === 0\r\n return false\r\n}\r\n\r\nexport type SortDirection = 'asc' | 'desc'\r\n\r\nexport function topoSort<T extends { id: string; dependencies?: string[] }>(items: T[]): T[] {\r\n const adj = new Map<string, string[]>()\r\n const inDegree = new Map<string, number>()\r\n const itemMap = new Map<string, T>()\r\n\r\n for (const item of items) {\r\n itemMap.set(item.id, item)\r\n if (!adj.has(item.id)) adj.set(item.id, [])\r\n if (!inDegree.has(item.id)) inDegree.set(item.id, 0)\r\n }\r\n\r\n for (const item of items) {\r\n if (item.dependencies) {\r\n for (const depId of item.dependencies) {\r\n adj.get(depId)?.push(item.id)\r\n inDegree.set(item.id, (inDegree.get(item.id) ?? 0) + 1)\r\n }\r\n }\r\n }\r\n\r\n const queue: string[] = []\r\n for (const [id, degree] of inDegree) {\r\n if (degree === 0) queue.push(id)\r\n }\r\n\r\n const sorted: string[] = []\r\n while (queue.length > 0) {\r\n const id = queue.shift()!\r\n sorted.push(id)\r\n for (const neighbor of adj.get(id) ?? []) {\r\n const newDegree = (inDegree.get(neighbor) ?? 1) - 1\r\n inDegree.set(neighbor, newDegree)\r\n if (newDegree === 0) queue.push(neighbor)\r\n }\r\n }\r\n\r\n if (sorted.length !== items.length) {\r\n throw new Error('Circular dependency detected')\r\n }\r\n\r\n return sorted.map(id => itemMap.get(id)!)\r\n}\r\n\r\nexport function slidingWindows<T>(items: T[], size: number, step: number = 1): T[][] {\r\n if (size <= 0 || items.length === 0 || step <= 0) return []\r\n const result: T[][] = []\r\n for (let i = 0; i + size <= items.length; i += step) {\r\n result.push(items.slice(i, i + size))\r\n }\r\n return result\r\n}\r\n\r\nexport function tumblingWindows<T>(items: T[], size: number): T[][] {\r\n if (size <= 0 || items.length === 0) return []\r\n const result: T[][] = []\r\n for (let i = 0; i < items.length; i += size) {\r\n result.push(items.slice(i, i + size))\r\n }\r\n return result\r\n}\r\n"],"mappings":"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","names":[]}
@@ -199,5 +199,75 @@ declare function addBusinessDays(date: Date, days: number): Date;
199
199
  * @throws {InvalidDateError} If the birth date is invalid.
200
200
  */
201
201
  declare function calculateAge(birthDate: Date): number;
202
+ /**
203
+ * Returns a human-readable relative time string (e.g. "5 menit yang lalu").
204
+ *
205
+ * @param date - The past date.
206
+ * @param options - Options with locale ('id' by default, 'en' supported).
207
+ * @returns A relative time string.
208
+ */
209
+ declare function timeAgo(date: Date, options?: {
210
+ locale?: string;
211
+ }): string;
212
+ /**
213
+ * Shows the time remaining until a future date.
214
+ *
215
+ * @param target - The target future date.
216
+ * @param options - Options with locale ('id' by default, 'en' supported).
217
+ * @returns A relative time string.
218
+ */
219
+ declare function timeRemaining(target: Date, options?: {
220
+ locale?: string;
221
+ }): string;
222
+ /**
223
+ * Represents a duration split into calendar and time components.
224
+ */
225
+ interface Duration {
226
+ years: number;
227
+ months: number;
228
+ days: number;
229
+ hours: number;
230
+ minutes: number;
231
+ seconds: number;
232
+ }
233
+ /**
234
+ * Formats a Duration object into a human-readable string.
235
+ *
236
+ * @example
237
+ * formatDuration({ hours: 2, minutes: 30, seconds: 15 }) // "2 jam 30 menit 15 detik"
238
+ * formatDuration({ hours: 2, minutes: 30 }, { locale: 'en' }) // "2 hours 30 minutes"
239
+ *
240
+ * @param duration - The duration to format.
241
+ * @param options - Options with locale ('id' by default, 'en' supported).
242
+ * @returns A formatted duration string.
243
+ */
244
+ declare function formatDuration(duration: Duration, options?: {
245
+ locale?: string;
246
+ }): string;
247
+ /** UTC+7 — Western Indonesia Time (WIB). */
248
+ declare const TIMEZONE_WIB = 7;
249
+ /** UTC+8 — Central Indonesia Time (WITA). */
250
+ declare const TIMEZONE_WITA = 8;
251
+ /** UTC+9 — Eastern Indonesia Time (WIT). */
252
+ declare const TIMEZONE_WIT = 9;
253
+ /**
254
+ * Converts a date to a specific timezone offset by returning a new Date whose
255
+ * local-time getters (getHours, getMinutes etc.) reflect the target timezone.
256
+ *
257
+ * @param date - The source date.
258
+ * @param offsetHours - The timezone offset in hours (e.g. 7 for WIB).
259
+ * @returns A new Date adjusted to the target timezone.
260
+ * @throws {InvalidDateError} If the input date is invalid.
261
+ */
262
+ declare function toTimezone(date: Date, offsetHours: number): Date;
263
+ /**
264
+ * Formats a date in a specific timezone using `formatDate` tokens.
265
+ *
266
+ * @param date - The source date.
267
+ * @param format - The format string (see `formatDate` for supported tokens).
268
+ * @param offsetHours - The timezone offset in hours.
269
+ * @returns The formatted date string.
270
+ */
271
+ declare function formatInTimezone(date: Date, format: string, offsetHours: number): string;
202
272
 
203
- export { type DateDiff, InvalidDateError, addBusinessDays, addDays, addMonths, addYears, calculateAge, dateDiff, endOfDay, endOfMonth, endOfYear, formatDate, isAfter, isBefore, isBetween, isBusinessDay, isLeapYear, isWeekend, parseDate, startOfDay, startOfMonth, startOfYear };
273
+ export { type DateDiff, type Duration, InvalidDateError, TIMEZONE_WIB, TIMEZONE_WIT, TIMEZONE_WITA, addBusinessDays, addDays, addMonths, addYears, calculateAge, dateDiff, endOfDay, endOfMonth, endOfYear, formatDate, formatDuration, formatInTimezone, isAfter, isBefore, isBetween, isBusinessDay, isLeapYear, isWeekend, parseDate, startOfDay, startOfMonth, startOfYear, timeAgo, timeRemaining, toTimezone };
@@ -253,8 +253,123 @@ function calculateAge(birthDate) {
253
253
  }
254
254
  return age;
255
255
  }
256
+ var LOCALE_LABELS = {
257
+ id: {
258
+ years: { single: "tahun", plural: "tahun" },
259
+ months: { single: "bulan", plural: "bulan" },
260
+ weeks: { single: "minggu", plural: "minggu" },
261
+ days: { single: "hari", plural: "hari" },
262
+ hours: { single: "jam", plural: "jam" },
263
+ minutes: { single: "menit", plural: "menit" },
264
+ seconds: { single: "detik", plural: "detik" }
265
+ },
266
+ en: {
267
+ years: { single: "year", plural: "years" },
268
+ months: { single: "month", plural: "months" },
269
+ weeks: { single: "week", plural: "weeks" },
270
+ days: { single: "day", plural: "days" },
271
+ hours: { single: "hour", plural: "hours" },
272
+ minutes: { single: "minute", plural: "minutes" },
273
+ seconds: { single: "second", plural: "seconds" }
274
+ }
275
+ };
276
+ function getSuffix(diffMs, kind, locale) {
277
+ if (diffMs < 0) {
278
+ return locale === "en" ? "ago" : "yang lalu";
279
+ }
280
+ if (kind === "remaining") {
281
+ return locale === "en" ? "remaining" : "lagi";
282
+ }
283
+ return locale === "en" ? "ago" : "yang lalu";
284
+ }
285
+ function formatRelativeTime(absDiffMs, suffix, locale) {
286
+ const labels = LOCALE_LABELS[locale] ?? LOCALE_LABELS.id;
287
+ const seconds = Math.floor(absDiffMs / 1e3);
288
+ const minutes = Math.floor(seconds / 60);
289
+ const hours = Math.floor(minutes / 60);
290
+ const days = Math.floor(hours / 24);
291
+ const weeks = Math.floor(days / 7);
292
+ const months = Math.floor(days / 30.4375);
293
+ const years = Math.floor(days / 365.25);
294
+ let count;
295
+ let unit;
296
+ if (years >= 1) {
297
+ count = years;
298
+ unit = "years";
299
+ } else if (months >= 1) {
300
+ count = months;
301
+ unit = "months";
302
+ } else if (weeks >= 1) {
303
+ count = weeks;
304
+ unit = "weeks";
305
+ } else if (days >= 1) {
306
+ count = days;
307
+ unit = "days";
308
+ } else if (hours >= 1) {
309
+ count = hours;
310
+ unit = "hours";
311
+ } else if (minutes >= 1) {
312
+ count = minutes;
313
+ unit = "minutes";
314
+ } else {
315
+ count = Math.max(1, seconds);
316
+ unit = "seconds";
317
+ }
318
+ const label = count === 1 ? labels[unit].single : labels[unit].plural;
319
+ return `${count} ${label} ${suffix}`;
320
+ }
321
+ function timeAgo(date, options) {
322
+ const diff = Date.now() - date.getTime();
323
+ const locale = options?.locale ?? "id";
324
+ const suffix = getSuffix(diff, "ago", locale);
325
+ return formatRelativeTime(Math.abs(diff), suffix, locale);
326
+ }
327
+ function timeRemaining(target, options) {
328
+ const diff = target.getTime() - Date.now();
329
+ const locale = options?.locale ?? "id";
330
+ const suffix = getSuffix(diff, "remaining", locale);
331
+ return formatRelativeTime(Math.abs(diff), suffix, locale);
332
+ }
333
+ function formatDuration(duration, options) {
334
+ const locale = options?.locale ?? "id";
335
+ const labels = LOCALE_LABELS[locale] ?? LOCALE_LABELS.id;
336
+ const parts = [];
337
+ const entries = [
338
+ ["years", duration.years],
339
+ ["months", duration.months],
340
+ ["days", duration.days],
341
+ ["hours", duration.hours],
342
+ ["minutes", duration.minutes],
343
+ ["seconds", duration.seconds]
344
+ ];
345
+ for (const [key, value] of entries) {
346
+ if (value > 0) {
347
+ const label = value === 1 ? labels[key].single : labels[key].plural;
348
+ parts.push(`${value} ${label}`);
349
+ }
350
+ }
351
+ if (parts.length === 0) {
352
+ const label = labels.seconds.plural;
353
+ return `0 ${label}`;
354
+ }
355
+ return parts.join(" ");
356
+ }
357
+ var TIMEZONE_WIB = 7;
358
+ var TIMEZONE_WITA = 8;
359
+ var TIMEZONE_WIT = 9;
360
+ function toTimezone(date, offsetHours) {
361
+ if (!isValidDate(date)) throw new InvalidDateError(date);
362
+ const utcMs = date.getTime() + date.getTimezoneOffset() * 6e4;
363
+ return new Date(utcMs + offsetHours * 36e5);
364
+ }
365
+ function formatInTimezone(date, format, offsetHours) {
366
+ return formatDate(toTimezone(date, offsetHours), format);
367
+ }
256
368
  export {
257
369
  InvalidDateError,
370
+ TIMEZONE_WIB,
371
+ TIMEZONE_WIT,
372
+ TIMEZONE_WITA,
258
373
  addBusinessDays,
259
374
  addDays,
260
375
  addMonths,
@@ -265,6 +380,8 @@ export {
265
380
  endOfMonth,
266
381
  endOfYear,
267
382
  formatDate,
383
+ formatDuration,
384
+ formatInTimezone,
268
385
  isAfter,
269
386
  isBefore,
270
387
  isBetween,
@@ -274,6 +391,9 @@ export {
274
391
  parseDate,
275
392
  startOfDay,
276
393
  startOfMonth,
277
- startOfYear
394
+ startOfYear,
395
+ timeAgo,
396
+ timeRemaining,
397
+ toTimezone
278
398
  };
279
399
  //# sourceMappingURL=index.js.map