@entwico/dash 1.0.0
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/LICENSE +21 -0
- package/README.md +95 -0
- package/dist/async.d.ts +87 -0
- package/dist/async.js +225 -0
- package/dist/async.js.map +1 -0
- package/dist/cn.d.ts +11 -0
- package/dist/cn.js +10 -0
- package/dist/cn.js.map +1 -0
- package/dist/index.d.ts +156 -0
- package/dist/index.js +225 -0
- package/dist/index.js.map +1 -0
- package/dist/match.d.ts +32 -0
- package/dist/match.js +48 -0
- package/dist/match.js.map +1 -0
- package/dist/react.d.ts +28 -0
- package/dist/react.js +62 -0
- package/dist/react.js.map +1 -0
- package/dist/sleep-J71HYu7n.js +19 -0
- package/dist/sleep-J71HYu7n.js.map +1 -0
- package/package.json +87 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 entwico GmbH
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# @entwico/dash
|
|
2
|
+
|
|
3
|
+
Typical reusable utilities shared across projects — the canonical home for the small helpers.
|
|
4
|
+
|
|
5
|
+
- **ESM-only**, `sideEffects: false`, fully tree-shakable
|
|
6
|
+
- **Zero-dependency core** — heavier worlds (React, Tailwind) are isolated behind subpath exports with optional peer dependencies
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
pnpm add @entwico/dash
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Entry points
|
|
15
|
+
|
|
16
|
+
| Entry | Contents | Peer dependencies |
|
|
17
|
+
| --------------------- | ------------------------------------ | ------------------------------ |
|
|
18
|
+
| `@entwico/dash` | zero-dependency utilities and types | — |
|
|
19
|
+
| `@entwico/dash/async` | AsyncIterable primitives and backoff | — |
|
|
20
|
+
| `@entwico/dash/match` | performant string pattern matching | — |
|
|
21
|
+
| `@entwico/dash/cn` | `cn()` class name merging | `clsx` ≥2, `tailwind-merge` ≥2 |
|
|
22
|
+
| `@entwico/dash/react` | React hooks | `react` ≥18 |
|
|
23
|
+
|
|
24
|
+
All peer dependencies are optional — install only what the entry points you use need.
|
|
25
|
+
|
|
26
|
+
## Core (`@entwico/dash`)
|
|
27
|
+
|
|
28
|
+
| Export | Purpose |
|
|
29
|
+
| ------------------------------------------------ | -------------------------------------------------------------------------------------------------------------- |
|
|
30
|
+
| `assert(expr, message?)` | assertion with `asserts expr` narrowing; `message` may be a string or an `Error` |
|
|
31
|
+
| `capitalize(str)` | uppercase the first letter |
|
|
32
|
+
| `createBatcher(onFlush, delayMs)` | batch calls into one flush after a delay |
|
|
33
|
+
| `debounce(fn, wait)` | classic trailing debounce |
|
|
34
|
+
| `deepFreeze(value)` | recursive `Object.freeze`, returns `ReadonlyDeep<T>` |
|
|
35
|
+
| `defined(val)` / `truthy(val)` | type-guard filters for `null` / `undefined` / falsy |
|
|
36
|
+
| `indexBy(array, keyFn, valueFn?)` | index an array into a `Map`; key/value selectors are property names or functions |
|
|
37
|
+
| `mapConcurrent(items, fn, concurrency)` | order-preserving concurrent map with a bounded worker pool |
|
|
38
|
+
| `maybeThen(value, fn)` / `maybeCatch(value, fn)` | chain transformations on `MaybePromise` values without unwrapping |
|
|
39
|
+
| `maybeAll(values)` / `maybeAllSettled(values)` | `Promise.all` / `Promise.allSettled` over `MaybePromise` values, synchronous when all values are |
|
|
40
|
+
| `noop` / `markAsUsed` | no-op functions |
|
|
41
|
+
| `omitUndefined(obj, recursive?)` | strip `undefined` properties (deep by default) |
|
|
42
|
+
| `optionalize(fn, options?)` | lift a function to accept `null` / `undefined`; `defaultValue` / `strategy` (`'nullish'` \| `'falsy'`) options |
|
|
43
|
+
| `retry(fn, options?)` | retry with a fixed delay; `retries` / `delayMs` / `signal` / `onError` options |
|
|
44
|
+
| `sleep(ms, signal?)` | promise-based delay, abortable via `AbortSignal` |
|
|
45
|
+
|
|
46
|
+
Types: `MaybePromise<T>`, `ReadonlyDeep<T>`.
|
|
47
|
+
|
|
48
|
+
## Async (`@entwico/dash/async`)
|
|
49
|
+
|
|
50
|
+
Zero-dependency streaming and retry primitives — the rxjs patterns without rxjs:
|
|
51
|
+
|
|
52
|
+
| Export | Purpose |
|
|
53
|
+
| ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
|
|
54
|
+
| `createAsyncIterableSubject()` / `AsyncIterableSubject<T>` | multicast source with `next` / `error` / `complete`, consumable via `for await` or `subscribe(observerOrNext)` |
|
|
55
|
+
| `firstAsync(iter)` | first yielded value, then disposes the iterator; throws if the iterable completes empty (≈ `firstValueFrom`) |
|
|
56
|
+
| `mapAsync(iter, fn)` / `filterAsync(iter, predicate)` | lazy operators over an `AsyncIterable` |
|
|
57
|
+
| `concatAsync(iter)` | concatenate all yielded arrays into one (plain collection: `Array.fromAsync`) |
|
|
58
|
+
| `exponentialBackoff(attempt, options?)` | jittered exponential delay, abortable; bounds configurable via `maxDelayMs` / `jitterMinMs` / `jitterMaxMs` |
|
|
59
|
+
| `retryWithBackoff(fn, options?)` | retry `fn` with exponential backoff indefinitely until it resolves, abortable via `signal` |
|
|
60
|
+
| `keepalive(factory, options?)` | keep a long-running `AsyncIterable` alive: re-subscribe with backoff on error or completion, stop on `signal` abort |
|
|
61
|
+
|
|
62
|
+
## Match (`@entwico/dash/match`)
|
|
63
|
+
|
|
64
|
+
A pattern union that keeps cheap checks cheap — `startsWith` beats a regex when a prefix is all you need:
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import { type StringPattern, createMatcher } from "@entwico/dash/match";
|
|
68
|
+
|
|
69
|
+
const isExcluded = createMatcher([{ exact: "/favicon.ico" }, { prefix: "/_astro/" }, { suffix: ".map" }, { includes: "/internal/" }, { pattern: /^\/api\/v\d+\// }, (path) => path.length > 2000]);
|
|
70
|
+
|
|
71
|
+
isExcluded("/_astro/chunk.js"); // true
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`createMatcher` buckets patterns by check cost at creation time: all `exact` patterns collapse into a single `Set` lookup, then prefixes / suffixes / substrings, then regexps and functions. For one-shot checks there are `matches(value, pattern)` and `matchesAny(value, patterns)`.
|
|
75
|
+
|
|
76
|
+
## cn (`@entwico/dash/cn`)
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import { type Classable, cn } from "@entwico/dash/cn";
|
|
80
|
+
|
|
81
|
+
cn("p-2", condition && "p-4"); // tailwind conflicts resolved in favor of the last class
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`Classable` is the `{ className?: string }` contract for component props designed to be merged via `cn()`.
|
|
85
|
+
|
|
86
|
+
## React (`@entwico/dash/react`)
|
|
87
|
+
|
|
88
|
+
- `useDebouncedValue(value, delay)` — the value, updated only after it has stayed unchanged for the given delay
|
|
89
|
+
- `useEffectAfterMount(effect, deps?)` — run the effect on updates, skip the initial mount
|
|
90
|
+
- `useEffectOnce(effect)` — run the effect exactly once on mount
|
|
91
|
+
- `useIsMobile()` — whether the viewport is below the mobile breakpoint (768px); `false` during SSR
|
|
92
|
+
|
|
93
|
+
## License
|
|
94
|
+
|
|
95
|
+
MIT
|
package/dist/async.d.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
//#region src/async/async-iterable-subject.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Multicast event source with `next` / `error` / `complete` (Subject semantics) plus an
|
|
4
|
+
* `AsyncIterable` view. Two consumption modes that fan out from the same source:
|
|
5
|
+
*
|
|
6
|
+
* - `for await (const v of subject)` — iteration. Each `[Symbol.asyncIterator]()` call returns
|
|
7
|
+
* a fresh per-iterator queue. Stop by `break`ing the loop. On `.error(err)` the iterator's
|
|
8
|
+
* pending `next()` rejects, propagating into the for-await as a thrown exception.
|
|
9
|
+
*
|
|
10
|
+
* - `subject.subscribe(observerOrNext)` — observer style. Pass either a full/partial observer
|
|
11
|
+
* `{ next?, error?, complete? }` or just an `(value) => …` function as the `next`-only shortcut.
|
|
12
|
+
* Returns an unsubscribe function. Missing handlers default to no-ops.
|
|
13
|
+
*
|
|
14
|
+
* Producer side: call `.next(value)` to emit, `.error(err)` to fail the subject (all consumers
|
|
15
|
+
* see the error and are detached), `.complete()` to close cleanly (all consumers see done and
|
|
16
|
+
* are detached). After either `error` or `complete`, further `.next()` calls are dropped and
|
|
17
|
+
* new `.subscribe(...)` calls register a no-op unsubscribe.
|
|
18
|
+
*
|
|
19
|
+
* Backpressure: unbounded queue per pending iterator. Intended for low-frequency events.
|
|
20
|
+
*/
|
|
21
|
+
type AsyncIterableSubject<T> = AsyncIterable<T> & {
|
|
22
|
+
next: (value: T) => void;
|
|
23
|
+
error: (err: unknown) => void;
|
|
24
|
+
complete: () => void;
|
|
25
|
+
subscribe: (observerOrNext: ((value: T) => void) | {
|
|
26
|
+
next?: (value: T) => void;
|
|
27
|
+
error?: (err: unknown) => void;
|
|
28
|
+
complete?: () => void;
|
|
29
|
+
}) => () => void;
|
|
30
|
+
};
|
|
31
|
+
declare function createAsyncIterableSubject<T>(): AsyncIterableSubject<T>;
|
|
32
|
+
//#endregion
|
|
33
|
+
//#region src/async/backoff.d.ts
|
|
34
|
+
type BackoffOptions = {
|
|
35
|
+
/** upper bound for the exponential delay, before jitter (default 30_000) */
|
|
36
|
+
maxDelayMs?: number | undefined;
|
|
37
|
+
/** lower bound of the random jitter added to every delay (default 300) */
|
|
38
|
+
jitterMinMs?: number | undefined;
|
|
39
|
+
/** upper bound of the random jitter added to every delay (default 3000) */
|
|
40
|
+
jitterMaxMs?: number | undefined;
|
|
41
|
+
/** aborts the backoff wait (and, where applicable, the retry loop) */
|
|
42
|
+
signal?: AbortSignal | undefined;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Exponential backoff with jitter. `attempt` starts at 0 → ~1s+jitter, 1 → ~2s+jitter, etc.,
|
|
46
|
+
* capped at `maxDelayMs`. Returns a Promise that resolves after the delay.
|
|
47
|
+
*/
|
|
48
|
+
declare function exponentialBackoff(attempt: number, options?: BackoffOptions | undefined): Promise<void>;
|
|
49
|
+
/**
|
|
50
|
+
* Runs `fn` with exponential-backoff retry on rejection, indefinitely, resolving with the first
|
|
51
|
+
* successful result. `options.signal` aborts both the in-flight `fn` (if it observes the signal)
|
|
52
|
+
* and the backoff wait.
|
|
53
|
+
*/
|
|
54
|
+
declare function retryWithBackoff<T>(fn: () => Promise<T>, options?: BackoffOptions | undefined): Promise<T>;
|
|
55
|
+
//#endregion
|
|
56
|
+
//#region src/async/concat-async.d.ts
|
|
57
|
+
/**
|
|
58
|
+
* Concatenates all elements of all arrays yielded by `iter` into a single array. Used to support
|
|
59
|
+
* subscriptions that emit chunks of items before completing.
|
|
60
|
+
*/
|
|
61
|
+
declare function concatAsync<T>(iter: AsyncIterable<T[]>): Promise<T[]>;
|
|
62
|
+
//#endregion
|
|
63
|
+
//#region src/async/filter-async.d.ts
|
|
64
|
+
/** Lazy filter over an async iterable. */
|
|
65
|
+
declare function filterAsync<T>(iter: AsyncIterable<T>, predicate: (value: T) => boolean): AsyncIterable<T>;
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region src/async/first-async.d.ts
|
|
68
|
+
/**
|
|
69
|
+
* Returns the first value yielded by `iter`, then disposes the iterator. Throws if the iterable
|
|
70
|
+
* completes without yielding. Equivalent of rxjs's `firstValueFrom` for AsyncIterables.
|
|
71
|
+
*/
|
|
72
|
+
declare function firstAsync<T>(iter: AsyncIterable<T>): Promise<T>;
|
|
73
|
+
//#endregion
|
|
74
|
+
//#region src/async/keepalive.d.ts
|
|
75
|
+
/**
|
|
76
|
+
* Long-running stream wrapper: subscribes via `factory()`, yields every value, and on error or
|
|
77
|
+
* unexpected completion re-subscribes with exponential backoff. Stops only when `options.signal`
|
|
78
|
+
* aborts. Designed for subscriptions that should stay open across reconnects.
|
|
79
|
+
*/
|
|
80
|
+
declare function keepalive<T>(factory: () => AsyncIterable<T>, options?: BackoffOptions | undefined): AsyncIterable<T>;
|
|
81
|
+
//#endregion
|
|
82
|
+
//#region src/async/map-async.d.ts
|
|
83
|
+
/** Lazy map over an async iterable. Cleanup propagates: breaking the outer iter disposes the inner. */
|
|
84
|
+
declare function mapAsync<T, R>(iter: AsyncIterable<T>, fn: (value: T) => R): AsyncIterable<R>;
|
|
85
|
+
//#endregion
|
|
86
|
+
export { AsyncIterableSubject, BackoffOptions, concatAsync, createAsyncIterableSubject, exponentialBackoff, filterAsync, firstAsync, keepalive, mapAsync, retryWithBackoff };
|
|
87
|
+
//# sourceMappingURL=async.d.ts.map
|
package/dist/async.js
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { t as sleep } from "./sleep-J71HYu7n.js";
|
|
2
|
+
//#region src/async/async-iterable-subject.ts
|
|
3
|
+
function createAsyncIterableSubject() {
|
|
4
|
+
const consumers = /* @__PURE__ */ new Set();
|
|
5
|
+
let status = { state: "open" };
|
|
6
|
+
const next = (value) => {
|
|
7
|
+
if (status.state !== "open") return;
|
|
8
|
+
for (const consumer of consumers) if (consumer.kind === "listener") consumer.next?.(value);
|
|
9
|
+
else if (consumer.pending) {
|
|
10
|
+
const p = consumer.pending;
|
|
11
|
+
consumer.pending = void 0;
|
|
12
|
+
p.resolve({
|
|
13
|
+
value,
|
|
14
|
+
done: false
|
|
15
|
+
});
|
|
16
|
+
} else consumer.queue.push(value);
|
|
17
|
+
};
|
|
18
|
+
const error = (err) => {
|
|
19
|
+
if (status.state !== "open") return;
|
|
20
|
+
status = {
|
|
21
|
+
state: "errored",
|
|
22
|
+
err
|
|
23
|
+
};
|
|
24
|
+
for (const consumer of consumers) if (consumer.kind === "listener") consumer.error?.(err);
|
|
25
|
+
else if (consumer.pending) {
|
|
26
|
+
const p = consumer.pending;
|
|
27
|
+
consumer.pending = void 0;
|
|
28
|
+
p.reject(err);
|
|
29
|
+
}
|
|
30
|
+
consumers.clear();
|
|
31
|
+
};
|
|
32
|
+
const complete = () => {
|
|
33
|
+
if (status.state !== "open") return;
|
|
34
|
+
status = { state: "completed" };
|
|
35
|
+
for (const consumer of consumers) if (consumer.kind === "listener") consumer.complete?.();
|
|
36
|
+
else if (consumer.pending) {
|
|
37
|
+
const p = consumer.pending;
|
|
38
|
+
consumer.pending = void 0;
|
|
39
|
+
p.resolve({
|
|
40
|
+
value: void 0,
|
|
41
|
+
done: true
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
consumers.clear();
|
|
45
|
+
};
|
|
46
|
+
const subscribe = (observerOrNext) => {
|
|
47
|
+
if (status.state !== "open") return () => {};
|
|
48
|
+
const consumer = {
|
|
49
|
+
kind: "listener",
|
|
50
|
+
...typeof observerOrNext === "function" ? { next: observerOrNext } : observerOrNext
|
|
51
|
+
};
|
|
52
|
+
consumers.add(consumer);
|
|
53
|
+
return () => {
|
|
54
|
+
consumers.delete(consumer);
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
return Object.assign({ [Symbol.asyncIterator]() {
|
|
58
|
+
if (status.state === "errored") {
|
|
59
|
+
const err = status.err;
|
|
60
|
+
return {
|
|
61
|
+
next: () => Promise.reject(err),
|
|
62
|
+
return: () => Promise.resolve({
|
|
63
|
+
value: void 0,
|
|
64
|
+
done: true
|
|
65
|
+
})
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (status.state === "completed") return {
|
|
69
|
+
next: () => Promise.resolve({
|
|
70
|
+
value: void 0,
|
|
71
|
+
done: true
|
|
72
|
+
}),
|
|
73
|
+
return: () => Promise.resolve({
|
|
74
|
+
value: void 0,
|
|
75
|
+
done: true
|
|
76
|
+
})
|
|
77
|
+
};
|
|
78
|
+
const consumer = {
|
|
79
|
+
kind: "iterator",
|
|
80
|
+
queue: []
|
|
81
|
+
};
|
|
82
|
+
consumers.add(consumer);
|
|
83
|
+
return {
|
|
84
|
+
next() {
|
|
85
|
+
if (consumer.queue.length > 0) return Promise.resolve({
|
|
86
|
+
value: consumer.queue.shift(),
|
|
87
|
+
done: false
|
|
88
|
+
});
|
|
89
|
+
if (status.state === "errored") return Promise.reject(status.err);
|
|
90
|
+
if (status.state === "completed") return Promise.resolve({
|
|
91
|
+
value: void 0,
|
|
92
|
+
done: true
|
|
93
|
+
});
|
|
94
|
+
return new Promise((resolve, reject) => {
|
|
95
|
+
consumer.pending = {
|
|
96
|
+
resolve,
|
|
97
|
+
reject
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
},
|
|
101
|
+
return() {
|
|
102
|
+
consumers.delete(consumer);
|
|
103
|
+
if (consumer.pending) {
|
|
104
|
+
const p = consumer.pending;
|
|
105
|
+
consumer.pending = void 0;
|
|
106
|
+
p.resolve({
|
|
107
|
+
value: void 0,
|
|
108
|
+
done: true
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return Promise.resolve({
|
|
112
|
+
value: void 0,
|
|
113
|
+
done: true
|
|
114
|
+
});
|
|
115
|
+
},
|
|
116
|
+
throw(err) {
|
|
117
|
+
consumers.delete(consumer);
|
|
118
|
+
return Promise.reject(err);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
} }, {
|
|
122
|
+
next,
|
|
123
|
+
error,
|
|
124
|
+
complete,
|
|
125
|
+
subscribe
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
//#endregion
|
|
129
|
+
//#region src/async/backoff.ts
|
|
130
|
+
const DEFAULT_MAX_DELAY_MS = 3e4;
|
|
131
|
+
const DEFAULT_JITTER_MIN_MS = 300;
|
|
132
|
+
const DEFAULT_JITTER_MAX_MS = 3e3;
|
|
133
|
+
/**
|
|
134
|
+
* Exponential backoff with jitter. `attempt` starts at 0 → ~1s+jitter, 1 → ~2s+jitter, etc.,
|
|
135
|
+
* capped at `maxDelayMs`. Returns a Promise that resolves after the delay.
|
|
136
|
+
*/
|
|
137
|
+
function exponentialBackoff(attempt, options) {
|
|
138
|
+
const maxDelayMs = options?.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;
|
|
139
|
+
const jitterMinMs = options?.jitterMinMs ?? DEFAULT_JITTER_MIN_MS;
|
|
140
|
+
const jitterMaxMs = options?.jitterMaxMs ?? DEFAULT_JITTER_MAX_MS;
|
|
141
|
+
return sleep(Math.min(Math.pow(2, attempt) * 1e3, maxDelayMs) + Math.floor(Math.random() * (jitterMaxMs - jitterMinMs) + jitterMinMs), options?.signal);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Runs `fn` with exponential-backoff retry on rejection, indefinitely, resolving with the first
|
|
145
|
+
* successful result. `options.signal` aborts both the in-flight `fn` (if it observes the signal)
|
|
146
|
+
* and the backoff wait.
|
|
147
|
+
*/
|
|
148
|
+
async function retryWithBackoff(fn, options) {
|
|
149
|
+
let attempt = 0;
|
|
150
|
+
for (;;) {
|
|
151
|
+
if (options?.signal?.aborted) throw options.signal.reason;
|
|
152
|
+
try {
|
|
153
|
+
return await fn();
|
|
154
|
+
} catch {
|
|
155
|
+
await exponentialBackoff(attempt, options);
|
|
156
|
+
attempt++;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
//#endregion
|
|
161
|
+
//#region src/async/concat-async.ts
|
|
162
|
+
/**
|
|
163
|
+
* Concatenates all elements of all arrays yielded by `iter` into a single array. Used to support
|
|
164
|
+
* subscriptions that emit chunks of items before completing.
|
|
165
|
+
*/
|
|
166
|
+
async function concatAsync(iter) {
|
|
167
|
+
const out = [];
|
|
168
|
+
for await (const chunk of iter) out.push(...chunk);
|
|
169
|
+
return out;
|
|
170
|
+
}
|
|
171
|
+
//#endregion
|
|
172
|
+
//#region src/async/filter-async.ts
|
|
173
|
+
/** Lazy filter over an async iterable. */
|
|
174
|
+
async function* filterAsync(iter, predicate) {
|
|
175
|
+
for await (const value of iter) if (predicate(value)) yield value;
|
|
176
|
+
}
|
|
177
|
+
//#endregion
|
|
178
|
+
//#region src/async/first-async.ts
|
|
179
|
+
/**
|
|
180
|
+
* Returns the first value yielded by `iter`, then disposes the iterator. Throws if the iterable
|
|
181
|
+
* completes without yielding. Equivalent of rxjs's `firstValueFrom` for AsyncIterables.
|
|
182
|
+
*/
|
|
183
|
+
async function firstAsync(iter) {
|
|
184
|
+
const it = iter[Symbol.asyncIterator]();
|
|
185
|
+
try {
|
|
186
|
+
const result = await it.next();
|
|
187
|
+
if (result.done) throw new Error("firstAsync: async iterable completed without yielding a value");
|
|
188
|
+
return result.value;
|
|
189
|
+
} finally {
|
|
190
|
+
await it.return?.();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
//#endregion
|
|
194
|
+
//#region src/async/keepalive.ts
|
|
195
|
+
/**
|
|
196
|
+
* Long-running stream wrapper: subscribes via `factory()`, yields every value, and on error or
|
|
197
|
+
* unexpected completion re-subscribes with exponential backoff. Stops only when `options.signal`
|
|
198
|
+
* aborts. Designed for subscriptions that should stay open across reconnects.
|
|
199
|
+
*/
|
|
200
|
+
async function* keepalive(factory, options) {
|
|
201
|
+
let attempt = 0;
|
|
202
|
+
const signal = options?.signal;
|
|
203
|
+
while (!signal?.aborted) try {
|
|
204
|
+
for await (const value of factory()) {
|
|
205
|
+
if (signal?.aborted) return;
|
|
206
|
+
yield value;
|
|
207
|
+
attempt = 0;
|
|
208
|
+
}
|
|
209
|
+
attempt = 0;
|
|
210
|
+
} catch (error) {
|
|
211
|
+
if (signal?.aborted) return;
|
|
212
|
+
await exponentialBackoff(attempt, options);
|
|
213
|
+
attempt++;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
//#endregion
|
|
217
|
+
//#region src/async/map-async.ts
|
|
218
|
+
/** Lazy map over an async iterable. Cleanup propagates: breaking the outer iter disposes the inner. */
|
|
219
|
+
async function* mapAsync(iter, fn) {
|
|
220
|
+
for await (const value of iter) yield fn(value);
|
|
221
|
+
}
|
|
222
|
+
//#endregion
|
|
223
|
+
export { concatAsync, createAsyncIterableSubject, exponentialBackoff, filterAsync, firstAsync, keepalive, mapAsync, retryWithBackoff };
|
|
224
|
+
|
|
225
|
+
//# sourceMappingURL=async.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"async.js","names":[],"sources":["../src/async/async-iterable-subject.ts","../src/async/backoff.ts","../src/async/concat-async.ts","../src/async/filter-async.ts","../src/async/first-async.ts","../src/async/keepalive.ts","../src/async/map-async.ts"],"sourcesContent":["/**\n * Multicast event source with `next` / `error` / `complete` (Subject semantics) plus an\n * `AsyncIterable` view. Two consumption modes that fan out from the same source:\n *\n * - `for await (const v of subject)` — iteration. Each `[Symbol.asyncIterator]()` call returns\n * a fresh per-iterator queue. Stop by `break`ing the loop. On `.error(err)` the iterator's\n * pending `next()` rejects, propagating into the for-await as a thrown exception.\n *\n * - `subject.subscribe(observerOrNext)` — observer style. Pass either a full/partial observer\n * `{ next?, error?, complete? }` or just an `(value) => …` function as the `next`-only shortcut.\n * Returns an unsubscribe function. Missing handlers default to no-ops.\n *\n * Producer side: call `.next(value)` to emit, `.error(err)` to fail the subject (all consumers\n * see the error and are detached), `.complete()` to close cleanly (all consumers see done and\n * are detached). After either `error` or `complete`, further `.next()` calls are dropped and\n * new `.subscribe(...)` calls register a no-op unsubscribe.\n *\n * Backpressure: unbounded queue per pending iterator. Intended for low-frequency events.\n */\nexport type AsyncIterableSubject<T> = AsyncIterable<T> & {\n next: (value: T) => void;\n error: (err: unknown) => void;\n complete: () => void;\n subscribe: (\n observerOrNext:\n | ((value: T) => void)\n | {\n next?: (value: T) => void;\n error?: (err: unknown) => void;\n complete?: () => void;\n },\n ) => () => void;\n};\n\ntype IteratorConsumer<T> = {\n kind: 'iterator';\n queue: T[];\n pending?: { resolve: (r: IteratorResult<T>) => void; reject: (e: unknown) => void } | undefined;\n};\n\ntype ListenerConsumer<T> = {\n kind: 'listener';\n next?: (value: T) => void;\n error?: (err: unknown) => void;\n complete?: () => void;\n};\n\ntype Consumer<T> = IteratorConsumer<T> | ListenerConsumer<T>;\n\ntype Status = { state: 'open' } | { state: 'errored'; err: unknown } | { state: 'completed' };\n\nexport function createAsyncIterableSubject<T>(): AsyncIterableSubject<T> {\n const consumers = new Set<Consumer<T>>();\n let status: Status = { state: 'open' };\n\n const next = (value: T): void => {\n if (status.state !== 'open') {\n return;\n }\n\n for (const consumer of consumers) {\n if (consumer.kind === 'listener') {\n consumer.next?.(value);\n } else if (consumer.pending) {\n const p = consumer.pending;\n\n consumer.pending = undefined;\n p.resolve({ value, done: false });\n } else {\n consumer.queue.push(value);\n }\n }\n };\n\n const error = (err: unknown): void => {\n if (status.state !== 'open') {\n return;\n }\n\n status = { state: 'errored', err };\n\n for (const consumer of consumers) {\n if (consumer.kind === 'listener') {\n consumer.error?.(err);\n } else if (consumer.pending) {\n const p = consumer.pending;\n\n consumer.pending = undefined;\n p.reject(err);\n }\n }\n\n consumers.clear();\n };\n\n const complete = (): void => {\n if (status.state !== 'open') {\n return;\n }\n\n status = { state: 'completed' };\n\n for (const consumer of consumers) {\n if (consumer.kind === 'listener') {\n consumer.complete?.();\n } else if (consumer.pending) {\n const p = consumer.pending;\n\n consumer.pending = undefined;\n p.resolve({ value: undefined as unknown as T, done: true });\n }\n }\n\n consumers.clear();\n };\n\n const subscribe = (\n observerOrNext:\n | ((value: T) => void)\n | {\n next?: (value: T) => void;\n error?: (err: unknown) => void;\n complete?: () => void;\n },\n ): (() => void) => {\n if (status.state !== 'open') {\n return () => {};\n }\n\n const observer = typeof observerOrNext === 'function' ? { next: observerOrNext } : observerOrNext;\n const consumer: ListenerConsumer<T> = { kind: 'listener', ...observer };\n\n consumers.add(consumer);\n\n return () => {\n consumers.delete(consumer);\n };\n };\n\n const iterable: AsyncIterable<T> = {\n [Symbol.asyncIterator](): AsyncIterator<T> {\n if (status.state === 'errored') {\n const err = status.err;\n\n return {\n next: () => Promise.reject(err),\n return: () => Promise.resolve({ value: undefined as unknown as T, done: true }),\n };\n }\n\n if (status.state === 'completed') {\n return {\n next: () => Promise.resolve({ value: undefined as unknown as T, done: true }),\n return: () => Promise.resolve({ value: undefined as unknown as T, done: true }),\n };\n }\n\n const consumer: IteratorConsumer<T> = { kind: 'iterator', queue: [] };\n\n consumers.add(consumer);\n\n return {\n next(): Promise<IteratorResult<T>> {\n if (consumer.queue.length > 0) {\n return Promise.resolve({ value: consumer.queue.shift()!, done: false });\n }\n\n if (status.state === 'errored') {\n return Promise.reject(status.err);\n }\n\n if (status.state === 'completed') {\n return Promise.resolve({ value: undefined as unknown as T, done: true });\n }\n\n return new Promise<IteratorResult<T>>((resolve, reject) => {\n consumer.pending = { resolve, reject };\n });\n },\n return(): Promise<IteratorResult<T>> {\n consumers.delete(consumer);\n\n if (consumer.pending) {\n const p = consumer.pending;\n\n consumer.pending = undefined;\n p.resolve({ value: undefined as unknown as T, done: true });\n }\n\n return Promise.resolve({ value: undefined as unknown as T, done: true });\n },\n throw(err): Promise<IteratorResult<T>> {\n consumers.delete(consumer);\n\n return Promise.reject(err);\n },\n };\n },\n };\n\n return Object.assign(iterable, { next, error, complete, subscribe });\n}\n","import { sleep } from '../core/sleep';\n\nconst DEFAULT_MAX_DELAY_MS = 30_000;\nconst DEFAULT_JITTER_MIN_MS = 300;\nconst DEFAULT_JITTER_MAX_MS = 3000;\n\nexport type BackoffOptions = {\n /** upper bound for the exponential delay, before jitter (default 30_000) */\n maxDelayMs?: number | undefined;\n\n /** lower bound of the random jitter added to every delay (default 300) */\n jitterMinMs?: number | undefined;\n\n /** upper bound of the random jitter added to every delay (default 3000) */\n jitterMaxMs?: number | undefined;\n\n /** aborts the backoff wait (and, where applicable, the retry loop) */\n signal?: AbortSignal | undefined;\n};\n\n/**\n * Exponential backoff with jitter. `attempt` starts at 0 → ~1s+jitter, 1 → ~2s+jitter, etc.,\n * capped at `maxDelayMs`. Returns a Promise that resolves after the delay.\n */\nexport function exponentialBackoff(attempt: number, options?: BackoffOptions | undefined): Promise<void> {\n const maxDelayMs = options?.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;\n const jitterMinMs = options?.jitterMinMs ?? DEFAULT_JITTER_MIN_MS;\n const jitterMaxMs = options?.jitterMaxMs ?? DEFAULT_JITTER_MAX_MS;\n\n const base = Math.min(Math.pow(2, attempt) * 1000, maxDelayMs);\n const jitter = Math.floor(Math.random() * (jitterMaxMs - jitterMinMs) + jitterMinMs);\n\n return sleep(base + jitter, options?.signal);\n}\n\n/**\n * Runs `fn` with exponential-backoff retry on rejection, indefinitely, resolving with the first\n * successful result. `options.signal` aborts both the in-flight `fn` (if it observes the signal)\n * and the backoff wait.\n */\nexport async function retryWithBackoff<T>(fn: () => Promise<T>, options?: BackoffOptions | undefined): Promise<T> {\n let attempt = 0;\n\n for (;;) {\n if (options?.signal?.aborted) {\n throw options.signal.reason;\n }\n\n try {\n return await fn();\n } catch {\n await exponentialBackoff(attempt, options);\n attempt++;\n }\n }\n}\n","/**\n * Concatenates all elements of all arrays yielded by `iter` into a single array. Used to support\n * subscriptions that emit chunks of items before completing.\n */\nexport async function concatAsync<T>(iter: AsyncIterable<T[]>): Promise<T[]> {\n const out: T[] = [];\n\n for await (const chunk of iter) {\n out.push(...chunk);\n }\n\n return out;\n}\n","/** Lazy filter over an async iterable. */\nexport async function* filterAsync<T>(iter: AsyncIterable<T>, predicate: (value: T) => boolean): AsyncIterable<T> {\n for await (const value of iter) {\n if (predicate(value)) {\n yield value;\n }\n }\n}\n","/**\n * Returns the first value yielded by `iter`, then disposes the iterator. Throws if the iterable\n * completes without yielding. Equivalent of rxjs's `firstValueFrom` for AsyncIterables.\n */\nexport async function firstAsync<T>(iter: AsyncIterable<T>): Promise<T> {\n const it = iter[Symbol.asyncIterator]();\n\n try {\n const result = await it.next();\n\n if (result.done) {\n throw new Error('firstAsync: async iterable completed without yielding a value');\n }\n\n return result.value;\n } finally {\n await it.return?.();\n }\n}\n","import { type BackoffOptions, exponentialBackoff } from './backoff';\n\n/**\n * Long-running stream wrapper: subscribes via `factory()`, yields every value, and on error or\n * unexpected completion re-subscribes with exponential backoff. Stops only when `options.signal`\n * aborts. Designed for subscriptions that should stay open across reconnects.\n */\nexport async function* keepalive<T>(\n factory: () => AsyncIterable<T>,\n options?: BackoffOptions | undefined,\n): AsyncIterable<T> {\n let attempt = 0;\n const signal = options?.signal;\n\n while (!signal?.aborted) {\n try {\n for await (const value of factory()) {\n if (signal?.aborted) {\n return;\n }\n\n yield value;\n attempt = 0;\n }\n\n // source completed cleanly — re-subscribe immediately\n attempt = 0;\n } catch (error) {\n if (signal?.aborted) {\n return;\n }\n\n // swallow + back off, then retry. callers that want the error to surface should not use keepalive\n void error;\n await exponentialBackoff(attempt, options);\n attempt++;\n }\n }\n}\n","/** Lazy map over an async iterable. Cleanup propagates: breaking the outer iter disposes the inner. */\nexport async function* mapAsync<T, R>(iter: AsyncIterable<T>, fn: (value: T) => R): AsyncIterable<R> {\n for await (const value of iter) {\n yield fn(value);\n }\n}\n"],"mappings":"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"}
|
package/dist/cn.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ClassValue } from "clsx";
|
|
2
|
+
//#region src/cn/classable.d.ts
|
|
3
|
+
type Classable = {
|
|
4
|
+
readonly className?: string | undefined;
|
|
5
|
+
};
|
|
6
|
+
//#endregion
|
|
7
|
+
//#region src/cn/cn.d.ts
|
|
8
|
+
declare function cn(...inputs: ClassValue[]): string;
|
|
9
|
+
//#endregion
|
|
10
|
+
export { Classable, cn };
|
|
11
|
+
//# sourceMappingURL=cn.d.ts.map
|
package/dist/cn.js
ADDED
package/dist/cn.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cn.js","names":[],"sources":["../src/cn/cn.ts"],"sourcesContent":["import { type ClassValue, clsx } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]): string {\n return twMerge(clsx(inputs));\n}\n"],"mappings":";;;AAGA,SAAgB,GAAG,GAAG,QAA8B;CAClD,OAAO,QAAQ,KAAK,MAAM,CAAC;AAC7B"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
//#region src/core/assert.d.ts
|
|
2
|
+
declare function assert<T>(expr: T, message?: string | Error): asserts expr;
|
|
3
|
+
//#endregion
|
|
4
|
+
//#region src/core/capitalize.d.ts
|
|
5
|
+
declare function capitalize(str: string): string;
|
|
6
|
+
//#endregion
|
|
7
|
+
//#region src/core/create-batcher.d.ts
|
|
8
|
+
/**
|
|
9
|
+
* Buffers function calls and executes them together after a specified delay.
|
|
10
|
+
* Useful for batching multiple events into a single operation.
|
|
11
|
+
*
|
|
12
|
+
* @param onFlush - Function to execute with all buffered items
|
|
13
|
+
* @param delayMs - Delay in milliseconds before flushing the buffer
|
|
14
|
+
* @returns Function that adds items to the buffer
|
|
15
|
+
*/
|
|
16
|
+
declare function createBatcher<T>(onFlush: (items: T[]) => void, delayMs: number): (item: T) => void;
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region src/core/debounce.d.ts
|
|
19
|
+
declare const debounce: <A extends unknown[]>(func: (...args: A) => unknown, wait: number) => (...args: A) => void;
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/core/readonly-deep.d.ts
|
|
22
|
+
/**
|
|
23
|
+
* Recursively makes a type and all its nested properties readonly.
|
|
24
|
+
*
|
|
25
|
+
* Adapted from type-fest's `ReadonlyDeep` taken under CC0:
|
|
26
|
+
* https://github.com/sindresorhus/type-fest/blob/main/source/readonly-deep.d.ts
|
|
27
|
+
*/
|
|
28
|
+
type Primitive = null | undefined | string | number | boolean | symbol | bigint;
|
|
29
|
+
type BuiltIns = Primitive | void | Date | RegExp;
|
|
30
|
+
type HasMultipleCallSignatures<T extends (...arguments_: any[]) => unknown> = T extends {
|
|
31
|
+
(...arguments_: infer A): unknown;
|
|
32
|
+
(...arguments_: infer B): unknown;
|
|
33
|
+
} ? B extends A ? A extends B ? false : true : true : false;
|
|
34
|
+
type EmptyObject = {};
|
|
35
|
+
type ReadonlyDeep<T> = T extends BuiltIns ? T : T extends (new (...arguments_: any[]) => unknown) ? T : T extends ((...arguments_: any[]) => unknown) ? EmptyObject extends ReadonlyObjectDeep<T> ? T : HasMultipleCallSignatures<T> extends true ? T : ((...arguments_: Parameters<T>) => ReturnType<T>) & ReadonlyObjectDeep<T> : T extends Readonly<ReadonlyMap<infer KeyType, infer ValueType>> ? ReadonlyMapDeep<KeyType, ValueType> : T extends Readonly<ReadonlySet<infer ItemType>> ? ReadonlySetDeep<ItemType> : T extends readonly [] | readonly [...never[]] ? readonly [] : T extends readonly [infer U, ...infer V] ? readonly [ReadonlyDeep<U>, ...ReadonlyDeep<V>] : T extends readonly [...infer U, infer V] ? readonly [...ReadonlyDeep<U>, ReadonlyDeep<V>] : T extends ReadonlyArray<infer ItemType> ? ReadonlyArray<ReadonlyDeep<ItemType>> : T extends object ? ReadonlyObjectDeep<T> : unknown;
|
|
36
|
+
type ReadonlyMapDeep<KeyType, ValueType> = EmptyObject & Readonly<ReadonlyMap<ReadonlyDeep<KeyType>, ReadonlyDeep<ValueType>>>;
|
|
37
|
+
type ReadonlySetDeep<ItemType> = EmptyObject & Readonly<ReadonlySet<ReadonlyDeep<ItemType>>>;
|
|
38
|
+
type ReadonlyObjectDeep<ObjectType extends object> = { readonly [KeyType in keyof ObjectType]: ReadonlyDeep<ObjectType[KeyType]>; };
|
|
39
|
+
//#endregion
|
|
40
|
+
//#region src/core/deep-freeze.d.ts
|
|
41
|
+
/**
|
|
42
|
+
* Runtime companion of {@link ReadonlyDeep}: recursively Object.freeze()s a value and returns it
|
|
43
|
+
* typed as deeply readonly. Safe on cyclic structures; already-frozen values are assumed to be
|
|
44
|
+
* deep-frozen and are not descended into.
|
|
45
|
+
*
|
|
46
|
+
* Runtime limits (the type still forbids mutation in all these cases): Map / Set contents are
|
|
47
|
+
* frozen, but the containers themselves stay mutable; Date internals cannot be frozen; functions
|
|
48
|
+
* are left untouched.
|
|
49
|
+
*/
|
|
50
|
+
declare function deepFreeze<T>(value: T): ReadonlyDeep<T>;
|
|
51
|
+
//#endregion
|
|
52
|
+
//#region src/core/defined.d.ts
|
|
53
|
+
declare function defined<T>(val: T | undefined | null | void): val is T;
|
|
54
|
+
declare function truthy<T>(val: T | undefined | null | void): val is T;
|
|
55
|
+
//#endregion
|
|
56
|
+
//#region src/core/index-by.d.ts
|
|
57
|
+
declare function indexBy<T>(array: readonly T[], keyFn: keyof T | ((value: T) => string)): Map<string, T>;
|
|
58
|
+
declare function indexBy<T, K extends keyof T>(array: readonly T[], keyFn: keyof T | ((value: T) => string), valueFn: K): Map<string, T[K]>;
|
|
59
|
+
declare function indexBy<T, V>(array: readonly T[], keyFn: keyof T | ((value: T) => string), valueFn: (value: T) => V): Map<string, V>;
|
|
60
|
+
//#endregion
|
|
61
|
+
//#region src/core/map-concurrent.d.ts
|
|
62
|
+
/**
|
|
63
|
+
* Order-preserving concurrent map: runs `fn` over the items with a sliding pool of at most
|
|
64
|
+
* `concurrency` workers, so a slow item never stalls the others. Errors propagate via Promise.all.
|
|
65
|
+
*/
|
|
66
|
+
declare function mapConcurrent<T, R>(items: readonly T[], fn: (item: T, index: number) => Promise<R>, concurrency: number): Promise<R[]>;
|
|
67
|
+
//#endregion
|
|
68
|
+
//#region src/core/maybe-promise.d.ts
|
|
69
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
70
|
+
type MaybeAwaitedAll<T extends readonly MaybePromise<unknown>[]> = { -readonly [K in keyof T]: Awaited<T[K]>; };
|
|
71
|
+
type MaybeSettledAll<T extends readonly MaybePromise<unknown>[]> = { -readonly [K in keyof T]: PromiseSettledResult<Awaited<T[K]>>; };
|
|
72
|
+
/**
|
|
73
|
+
* Chains a transformation on a MaybePromise value.
|
|
74
|
+
* If the value is a Promise, uses .then(). Otherwise, calls fn directly.
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```ts
|
|
78
|
+
* const result = maybeThen(map.get('key'), (value) => value.name);
|
|
79
|
+
* // result is MaybePromise<string>
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
declare function maybeThen<T, R>(value: MaybePromise<T>, fn: (v: T) => MaybePromise<R>): MaybePromise<R>;
|
|
83
|
+
/**
|
|
84
|
+
* Adds error handling to a MaybePromise value.
|
|
85
|
+
* If the value is a Promise, uses .catch(). Otherwise, returns value as-is.
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* ```ts
|
|
89
|
+
* const result = maybeCatch(map.get('key'), (err) => defaultValue);
|
|
90
|
+
* // result is MaybePromise<T>
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
declare function maybeCatch<T>(value: MaybePromise<T>, fn: (e: unknown) => MaybePromise<T>): MaybePromise<T>;
|
|
94
|
+
/**
|
|
95
|
+
* Combines MaybePromise values.
|
|
96
|
+
* If any value is a Promise, behaves like Promise.all(). Otherwise, returns the values synchronously.
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```ts
|
|
100
|
+
* const [user, config] = await maybeAll([cache.get('user'), cache.get('config')]);
|
|
101
|
+
* // no microtask hop when both were cached synchronously
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
declare function maybeAll<T extends readonly MaybePromise<unknown>[]>(values: T): MaybePromise<MaybeAwaitedAll<T>>;
|
|
105
|
+
/**
|
|
106
|
+
* Settles MaybePromise values.
|
|
107
|
+
* If any value is a Promise, behaves like Promise.allSettled(). Otherwise, returns the values
|
|
108
|
+
* synchronously, each wrapped as a fulfilled result.
|
|
109
|
+
*/
|
|
110
|
+
declare function maybeAllSettled<T extends readonly MaybePromise<unknown>[]>(values: T): MaybePromise<MaybeSettledAll<T>>;
|
|
111
|
+
//#endregion
|
|
112
|
+
//#region src/core/noop.d.ts
|
|
113
|
+
declare const noop: (..._: any[]) => void;
|
|
114
|
+
declare const markAsUsed: (..._: any[]) => void;
|
|
115
|
+
//#endregion
|
|
116
|
+
//#region src/core/omit-undefined.d.ts
|
|
117
|
+
type DeepNonUndefined<T> = T extends Record<string, unknown> ? { [P in keyof T as undefined extends T[P] ? never : P]: T[P] extends (infer U)[] ? DeepNonUndefined<U>[] : T[P] extends Record<string, unknown> ? DeepNonUndefined<T[P]> : Exclude<T[P], undefined>; } : Exclude<T, undefined>;
|
|
118
|
+
type ShallowNonUndefined<T> = { [P in keyof T as undefined extends T[P] ? never : P]: Exclude<T[P], undefined>; };
|
|
119
|
+
/**
|
|
120
|
+
* Removes undefined properties from an object.
|
|
121
|
+
* Optionally processes nested objects and arrays recursively.
|
|
122
|
+
*
|
|
123
|
+
* @param obj object to clean
|
|
124
|
+
* @param recursive whether to recursively process nested objects and arrays (default: true)
|
|
125
|
+
*/
|
|
126
|
+
declare function omitUndefined<T extends Record<string, unknown>>(obj: T, recursive?: true): DeepNonUndefined<T>;
|
|
127
|
+
declare function omitUndefined<T extends Record<string, unknown>>(obj: T, recursive: false): ShallowNonUndefined<T>;
|
|
128
|
+
//#endregion
|
|
129
|
+
//#region src/core/optionalize.d.ts
|
|
130
|
+
declare function optionalize<T, U, D = undefined>(fn: (v: T) => U, options?: {
|
|
131
|
+
defaultValue?: D | undefined;
|
|
132
|
+
strategy?: 'falsy' | 'nullish' | undefined;
|
|
133
|
+
} | undefined): (v: T | null | undefined) => U | D;
|
|
134
|
+
//#endregion
|
|
135
|
+
//#region src/core/retry.d.ts
|
|
136
|
+
type RetryOptions = {
|
|
137
|
+
/** total number of attempts before the last error is rethrown (default 3) */
|
|
138
|
+
retries?: number | undefined;
|
|
139
|
+
/** fixed delay between attempts (default 1000) */
|
|
140
|
+
delayMs?: number | undefined;
|
|
141
|
+
/** aborts the delay between attempts */
|
|
142
|
+
signal?: AbortSignal | undefined;
|
|
143
|
+
/** called after every failed attempt with the error and the 1-based attempt number */
|
|
144
|
+
onError?: ((error: unknown, attempt: number) => void) | undefined;
|
|
145
|
+
};
|
|
146
|
+
/**
|
|
147
|
+
* Runs `fn`, retrying with a fixed delay on failure. Resolves with the first successful result;
|
|
148
|
+
* rethrows the last error once the attempts are exhausted.
|
|
149
|
+
*/
|
|
150
|
+
declare function retry<T>(fn: () => T, options?: RetryOptions | undefined): Promise<T>;
|
|
151
|
+
//#endregion
|
|
152
|
+
//#region src/core/sleep.d.ts
|
|
153
|
+
declare function sleep(ms: number, signal?: AbortSignal | undefined): Promise<void>;
|
|
154
|
+
//#endregion
|
|
155
|
+
export { MaybePromise, ReadonlyDeep, RetryOptions, assert, capitalize, createBatcher, debounce, deepFreeze, defined, indexBy, mapConcurrent, markAsUsed, maybeAll, maybeAllSettled, maybeCatch, maybeThen, noop, omitUndefined, optionalize, retry, sleep, truthy };
|
|
156
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { t as sleep } from "./sleep-J71HYu7n.js";
|
|
2
|
+
//#region src/core/assert.ts
|
|
3
|
+
function assert(expr, message) {
|
|
4
|
+
message ||= "unknown assertion error";
|
|
5
|
+
message = typeof message === "string" ? new Error(message) : message;
|
|
6
|
+
if (!expr) throw message;
|
|
7
|
+
}
|
|
8
|
+
//#endregion
|
|
9
|
+
//#region src/core/capitalize.ts
|
|
10
|
+
function capitalize(str) {
|
|
11
|
+
if (!str) return str;
|
|
12
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
13
|
+
}
|
|
14
|
+
//#endregion
|
|
15
|
+
//#region src/core/create-batcher.ts
|
|
16
|
+
/**
|
|
17
|
+
* Buffers function calls and executes them together after a specified delay.
|
|
18
|
+
* Useful for batching multiple events into a single operation.
|
|
19
|
+
*
|
|
20
|
+
* @param onFlush - Function to execute with all buffered items
|
|
21
|
+
* @param delayMs - Delay in milliseconds before flushing the buffer
|
|
22
|
+
* @returns Function that adds items to the buffer
|
|
23
|
+
*/
|
|
24
|
+
function createBatcher(onFlush, delayMs) {
|
|
25
|
+
let buffer = [];
|
|
26
|
+
let timeoutId = null;
|
|
27
|
+
return (item) => {
|
|
28
|
+
buffer.push(item);
|
|
29
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
30
|
+
timeoutId = setTimeout(() => {
|
|
31
|
+
onFlush(buffer);
|
|
32
|
+
buffer = [];
|
|
33
|
+
timeoutId = null;
|
|
34
|
+
}, delayMs);
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
//#endregion
|
|
38
|
+
//#region src/core/debounce.ts
|
|
39
|
+
const debounce = (func, wait) => {
|
|
40
|
+
let timeout;
|
|
41
|
+
return function executedFunction(...args) {
|
|
42
|
+
const later = () => {
|
|
43
|
+
timeout = void 0;
|
|
44
|
+
func(...args);
|
|
45
|
+
};
|
|
46
|
+
clearTimeout(timeout);
|
|
47
|
+
timeout = setTimeout(later, wait);
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
//#endregion
|
|
51
|
+
//#region src/core/deep-freeze.ts
|
|
52
|
+
/**
|
|
53
|
+
* Runtime companion of {@link ReadonlyDeep}: recursively Object.freeze()s a value and returns it
|
|
54
|
+
* typed as deeply readonly. Safe on cyclic structures; already-frozen values are assumed to be
|
|
55
|
+
* deep-frozen and are not descended into.
|
|
56
|
+
*
|
|
57
|
+
* Runtime limits (the type still forbids mutation in all these cases): Map / Set contents are
|
|
58
|
+
* frozen, but the containers themselves stay mutable; Date internals cannot be frozen; functions
|
|
59
|
+
* are left untouched.
|
|
60
|
+
*/
|
|
61
|
+
function deepFreeze(value) {
|
|
62
|
+
freeze(value);
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
function freeze(value) {
|
|
66
|
+
if (value === null || typeof value !== "object" || Object.isFrozen(value)) return;
|
|
67
|
+
if (ArrayBuffer.isView(value)) return;
|
|
68
|
+
Object.freeze(value);
|
|
69
|
+
if (value instanceof Map) {
|
|
70
|
+
for (const [key, item] of value) {
|
|
71
|
+
freeze(key);
|
|
72
|
+
freeze(item);
|
|
73
|
+
}
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (value instanceof Set) {
|
|
77
|
+
for (const item of value) freeze(item);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
for (const key of Reflect.ownKeys(value)) freeze(value[key]);
|
|
81
|
+
}
|
|
82
|
+
//#endregion
|
|
83
|
+
//#region src/core/defined.ts
|
|
84
|
+
function defined(val) {
|
|
85
|
+
return val !== void 0 && val !== null;
|
|
86
|
+
}
|
|
87
|
+
function truthy(val) {
|
|
88
|
+
return !!val;
|
|
89
|
+
}
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region src/core/index-by.ts
|
|
92
|
+
function indexBy(array, keyFn, valueFn) {
|
|
93
|
+
return array.reduce((map, item) => {
|
|
94
|
+
const key = typeof keyFn === "function" ? keyFn(item) : String(item[keyFn]);
|
|
95
|
+
if (valueFn) {
|
|
96
|
+
const value = typeof valueFn === "function" ? valueFn(item) : item[valueFn];
|
|
97
|
+
map.set(key, value);
|
|
98
|
+
} else map.set(key, item);
|
|
99
|
+
return map;
|
|
100
|
+
}, /* @__PURE__ */ new Map());
|
|
101
|
+
}
|
|
102
|
+
//#endregion
|
|
103
|
+
//#region src/core/map-concurrent.ts
|
|
104
|
+
/**
|
|
105
|
+
* Order-preserving concurrent map: runs `fn` over the items with a sliding pool of at most
|
|
106
|
+
* `concurrency` workers, so a slow item never stalls the others. Errors propagate via Promise.all.
|
|
107
|
+
*/
|
|
108
|
+
async function mapConcurrent(items, fn, concurrency) {
|
|
109
|
+
const results = Array.from({ length: items.length });
|
|
110
|
+
let nextIndex = 0;
|
|
111
|
+
async function worker() {
|
|
112
|
+
while (true) {
|
|
113
|
+
const i = nextIndex++;
|
|
114
|
+
if (i >= items.length) return;
|
|
115
|
+
results[i] = await fn(items[i], i);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const workerCount = Math.max(1, Math.min(concurrency, items.length));
|
|
119
|
+
const workers = Array.from({ length: workerCount }, () => worker());
|
|
120
|
+
await Promise.all(workers);
|
|
121
|
+
return results;
|
|
122
|
+
}
|
|
123
|
+
//#endregion
|
|
124
|
+
//#region src/core/maybe-promise.ts
|
|
125
|
+
/**
|
|
126
|
+
* Chains a transformation on a MaybePromise value.
|
|
127
|
+
* If the value is a Promise, uses .then(). Otherwise, calls fn directly.
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* ```ts
|
|
131
|
+
* const result = maybeThen(map.get('key'), (value) => value.name);
|
|
132
|
+
* // result is MaybePromise<string>
|
|
133
|
+
* ```
|
|
134
|
+
*/
|
|
135
|
+
function maybeThen(value, fn) {
|
|
136
|
+
if (value instanceof Promise) return value.then(fn);
|
|
137
|
+
return fn(value);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Adds error handling to a MaybePromise value.
|
|
141
|
+
* If the value is a Promise, uses .catch(). Otherwise, returns value as-is.
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* ```ts
|
|
145
|
+
* const result = maybeCatch(map.get('key'), (err) => defaultValue);
|
|
146
|
+
* // result is MaybePromise<T>
|
|
147
|
+
* ```
|
|
148
|
+
*/
|
|
149
|
+
function maybeCatch(value, fn) {
|
|
150
|
+
if (value instanceof Promise) return value.catch(fn);
|
|
151
|
+
return value;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Combines MaybePromise values.
|
|
155
|
+
* If any value is a Promise, behaves like Promise.all(). Otherwise, returns the values synchronously.
|
|
156
|
+
*
|
|
157
|
+
* @example
|
|
158
|
+
* ```ts
|
|
159
|
+
* const [user, config] = await maybeAll([cache.get('user'), cache.get('config')]);
|
|
160
|
+
* // no microtask hop when both were cached synchronously
|
|
161
|
+
* ```
|
|
162
|
+
*/
|
|
163
|
+
function maybeAll(values) {
|
|
164
|
+
if (values.some((value) => value instanceof Promise)) return Promise.all(values);
|
|
165
|
+
return [...values];
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Settles MaybePromise values.
|
|
169
|
+
* If any value is a Promise, behaves like Promise.allSettled(). Otherwise, returns the values
|
|
170
|
+
* synchronously, each wrapped as a fulfilled result.
|
|
171
|
+
*/
|
|
172
|
+
function maybeAllSettled(values) {
|
|
173
|
+
if (values.some((value) => value instanceof Promise)) return Promise.allSettled(values);
|
|
174
|
+
return values.map((value) => ({
|
|
175
|
+
status: "fulfilled",
|
|
176
|
+
value
|
|
177
|
+
}));
|
|
178
|
+
}
|
|
179
|
+
//#endregion
|
|
180
|
+
//#region src/core/noop.ts
|
|
181
|
+
const noop = (..._) => {};
|
|
182
|
+
const markAsUsed = (..._) => {};
|
|
183
|
+
//#endregion
|
|
184
|
+
//#region src/core/omit-undefined.ts
|
|
185
|
+
function omitUndefined(obj, recursive = true) {
|
|
186
|
+
const result = {};
|
|
187
|
+
for (const [key, value] of Object.entries(obj)) if (value !== void 0) if (recursive && value && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date)) result[key] = omitUndefined(value, recursive);
|
|
188
|
+
else if (recursive && Array.isArray(value)) result[key] = value.filter((item) => item !== void 0).map((item) => {
|
|
189
|
+
if (item && typeof item === "object" && !Array.isArray(item) && !(item instanceof Date)) return omitUndefined(item, recursive);
|
|
190
|
+
return item;
|
|
191
|
+
});
|
|
192
|
+
else result[key] = value;
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
195
|
+
//#endregion
|
|
196
|
+
//#region src/core/optionalize.ts
|
|
197
|
+
function optionalize(fn, options) {
|
|
198
|
+
const defaultValue = options?.defaultValue;
|
|
199
|
+
const emptyMode = options?.strategy ?? "nullish";
|
|
200
|
+
return (v) => {
|
|
201
|
+
if (v === null || v === void 0 || emptyMode === "falsy" && !v) return defaultValue;
|
|
202
|
+
return fn(v);
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
//#endregion
|
|
206
|
+
//#region src/core/retry.ts
|
|
207
|
+
/**
|
|
208
|
+
* Runs `fn`, retrying with a fixed delay on failure. Resolves with the first successful result;
|
|
209
|
+
* rethrows the last error once the attempts are exhausted.
|
|
210
|
+
*/
|
|
211
|
+
async function retry(fn, options) {
|
|
212
|
+
const retries = options?.retries ?? 3;
|
|
213
|
+
const delayMs = options?.delayMs ?? 1e3;
|
|
214
|
+
for (let attempt = 1;; attempt++) try {
|
|
215
|
+
return await fn();
|
|
216
|
+
} catch (error) {
|
|
217
|
+
options?.onError?.(error, attempt);
|
|
218
|
+
if (attempt >= retries) throw error;
|
|
219
|
+
await sleep(delayMs, options?.signal);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
//#endregion
|
|
223
|
+
export { assert, capitalize, createBatcher, debounce, deepFreeze, defined, indexBy, mapConcurrent, markAsUsed, maybeAll, maybeAllSettled, maybeCatch, maybeThen, noop, omitUndefined, optionalize, retry, sleep, truthy };
|
|
224
|
+
|
|
225
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/core/assert.ts","../src/core/capitalize.ts","../src/core/create-batcher.ts","../src/core/debounce.ts","../src/core/deep-freeze.ts","../src/core/defined.ts","../src/core/index-by.ts","../src/core/map-concurrent.ts","../src/core/maybe-promise.ts","../src/core/noop.ts","../src/core/omit-undefined.ts","../src/core/optionalize.ts","../src/core/retry.ts"],"sourcesContent":["export function assert<T>(expr: T, message?: string | Error): asserts expr {\n message ||= 'unknown assertion error';\n message = typeof message === 'string' ? new Error(message) : message;\n\n if (!expr) {\n throw message;\n }\n}\n","export function capitalize(str: string): string {\n if (!str) return str;\n\n return str.charAt(0).toUpperCase() + str.slice(1);\n}\n","/**\n * Buffers function calls and executes them together after a specified delay.\n * Useful for batching multiple events into a single operation.\n *\n * @param onFlush - Function to execute with all buffered items\n * @param delayMs - Delay in milliseconds before flushing the buffer\n * @returns Function that adds items to the buffer\n */\nexport function createBatcher<T>(onFlush: (items: T[]) => void, delayMs: number) {\n let buffer: T[] = [];\n let timeoutId: ReturnType<typeof setTimeout> | null = null;\n\n return (item: T) => {\n buffer.push(item);\n\n if (timeoutId) {\n clearTimeout(timeoutId);\n }\n\n timeoutId = setTimeout(() => {\n onFlush(buffer);\n buffer = [];\n timeoutId = null;\n }, delayMs);\n };\n}\n","export const debounce = <A extends unknown[]>(func: (...args: A) => unknown, wait: number) => {\n let timeout: ReturnType<typeof setTimeout> | undefined;\n\n return function executedFunction(...args: A) {\n const later = () => {\n timeout = undefined;\n func(...args);\n };\n\n clearTimeout(timeout);\n\n timeout = setTimeout(later, wait);\n };\n};\n","import type { ReadonlyDeep } from './readonly-deep';\n\n/**\n * Runtime companion of {@link ReadonlyDeep}: recursively Object.freeze()s a value and returns it\n * typed as deeply readonly. Safe on cyclic structures; already-frozen values are assumed to be\n * deep-frozen and are not descended into.\n *\n * Runtime limits (the type still forbids mutation in all these cases): Map / Set contents are\n * frozen, but the containers themselves stay mutable; Date internals cannot be frozen; functions\n * are left untouched.\n */\nexport function deepFreeze<T>(value: T): ReadonlyDeep<T> {\n freeze(value);\n\n return value as ReadonlyDeep<T>;\n}\n\nfunction freeze(value: unknown): void {\n if (value === null || typeof value !== 'object' || Object.isFrozen(value)) {\n return;\n }\n\n // freezing typed arrays with elements throws\n if (ArrayBuffer.isView(value)) {\n return;\n }\n\n Object.freeze(value);\n\n if (value instanceof Map) {\n for (const [key, item] of value) {\n freeze(key);\n freeze(item);\n }\n\n return;\n }\n\n if (value instanceof Set) {\n for (const item of value) {\n freeze(item);\n }\n\n return;\n }\n\n for (const key of Reflect.ownKeys(value)) {\n freeze((value as Record<PropertyKey, unknown>)[key]);\n }\n}\n","export function defined<T>(val: T | undefined | null | void): val is T {\n return val !== undefined && val !== null;\n}\n\nexport function truthy<T>(val: T | undefined | null | void): val is T {\n return !!val;\n}\n","export function indexBy<T>(array: readonly T[], keyFn: keyof T | ((value: T) => string)): Map<string, T>;\nexport function indexBy<T, K extends keyof T>(\n array: readonly T[],\n keyFn: keyof T | ((value: T) => string),\n valueFn: K,\n): Map<string, T[K]>;\nexport function indexBy<T, V>(\n array: readonly T[],\n keyFn: keyof T | ((value: T) => string),\n valueFn: (value: T) => V,\n): Map<string, V>;\nexport function indexBy<T, V = T>(\n array: readonly T[],\n keyFn: keyof T | ((value: T) => string),\n valueFn?: keyof T | ((value: T) => V),\n): Map<string, V | T> {\n return array.reduce((map, item) => {\n const key = typeof keyFn === 'function' ? keyFn(item) : String(item[keyFn]); // eslint-disable-line unicorn/no-unsafe-property-key\n\n if (valueFn) {\n const value = typeof valueFn === 'function' ? valueFn(item) : item[valueFn]; // eslint-disable-line unicorn/no-unsafe-property-key\n map.set(key, value as V | T);\n } else {\n map.set(key, item as V | T);\n }\n\n return map;\n }, new Map<string, V | T>());\n}\n","/**\n * Order-preserving concurrent map: runs `fn` over the items with a sliding pool of at most\n * `concurrency` workers, so a slow item never stalls the others. Errors propagate via Promise.all.\n */\nexport async function mapConcurrent<T, R>(\n items: readonly T[],\n fn: (item: T, index: number) => Promise<R>,\n concurrency: number,\n): Promise<R[]> {\n const results: R[] = Array.from({ length: items.length });\n let nextIndex = 0;\n\n async function worker(): Promise<void> {\n while (true) {\n const i = nextIndex++;\n\n if (i >= items.length) {\n return;\n }\n\n results[i] = await fn(items[i]!, i);\n }\n }\n\n const workerCount = Math.max(1, Math.min(concurrency, items.length));\n const workers = Array.from({ length: workerCount }, () => worker());\n\n await Promise.all(workers);\n\n return results;\n}\n","export type MaybePromise<T> = T | Promise<T>;\n\ntype MaybeAwaitedAll<T extends readonly MaybePromise<unknown>[]> = { -readonly [K in keyof T]: Awaited<T[K]> };\n\ntype MaybeSettledAll<T extends readonly MaybePromise<unknown>[]> = {\n -readonly [K in keyof T]: PromiseSettledResult<Awaited<T[K]>>;\n};\n\n/**\n * Chains a transformation on a MaybePromise value.\n * If the value is a Promise, uses .then(). Otherwise, calls fn directly.\n *\n * @example\n * ```ts\n * const result = maybeThen(map.get('key'), (value) => value.name);\n * // result is MaybePromise<string>\n * ```\n */\nexport function maybeThen<T, R>(value: MaybePromise<T>, fn: (v: T) => MaybePromise<R>): MaybePromise<R> {\n if (value instanceof Promise) {\n return value.then(fn); // eslint-disable-line unicorn/prefer-await\n }\n\n return fn(value);\n}\n\n/**\n * Adds error handling to a MaybePromise value.\n * If the value is a Promise, uses .catch(). Otherwise, returns value as-is.\n *\n * @example\n * ```ts\n * const result = maybeCatch(map.get('key'), (err) => defaultValue);\n * // result is MaybePromise<T>\n * ```\n */\nexport function maybeCatch<T>(value: MaybePromise<T>, fn: (e: unknown) => MaybePromise<T>): MaybePromise<T> {\n if (value instanceof Promise) {\n return value.catch(fn); // eslint-disable-line unicorn/prefer-await\n }\n\n return value;\n}\n\n/**\n * Combines MaybePromise values.\n * If any value is a Promise, behaves like Promise.all(). Otherwise, returns the values synchronously.\n *\n * @example\n * ```ts\n * const [user, config] = await maybeAll([cache.get('user'), cache.get('config')]);\n * // no microtask hop when both were cached synchronously\n * ```\n */\nexport function maybeAll<T extends readonly MaybePromise<unknown>[]>(values: T): MaybePromise<MaybeAwaitedAll<T>> {\n if (values.some((value) => value instanceof Promise)) {\n return Promise.all(values) as Promise<MaybeAwaitedAll<T>>;\n }\n\n return [...values] as MaybeAwaitedAll<T>;\n}\n\n/**\n * Settles MaybePromise values.\n * If any value is a Promise, behaves like Promise.allSettled(). Otherwise, returns the values\n * synchronously, each wrapped as a fulfilled result.\n */\nexport function maybeAllSettled<T extends readonly MaybePromise<unknown>[]>(\n values: T,\n): MaybePromise<MaybeSettledAll<T>> {\n if (values.some((value) => value instanceof Promise)) {\n return Promise.allSettled(values) as Promise<MaybeSettledAll<T>>;\n }\n\n return values.map((value) => ({ status: 'fulfilled', value })) as MaybeSettledAll<T>;\n}\n","export const noop = (..._: any[]) => {};\n\n// used e.g. because of https://github.com/withastro/astro/issues/14684 — fixed in\n// @astrojs/compiler 3.0.0, but @astrojs/check still bundles the unfixed 2.x line;\n// obsolete once the language server ships with compiler >= 3.0.0\nexport const markAsUsed = (..._: any[]) => {};\n","type DeepNonUndefined<T> =\n T extends Record<string, unknown>\n ? {\n [P in keyof T as undefined extends T[P] ? never : P]: T[P] extends (infer U)[]\n ? DeepNonUndefined<U>[]\n : T[P] extends Record<string, unknown>\n ? DeepNonUndefined<T[P]>\n : Exclude<T[P], undefined>;\n }\n : Exclude<T, undefined>;\n\ntype ShallowNonUndefined<T> = {\n [P in keyof T as undefined extends T[P] ? never : P]: Exclude<T[P], undefined>;\n};\n\n/**\n * Removes undefined properties from an object.\n * Optionally processes nested objects and arrays recursively.\n *\n * @param obj object to clean\n * @param recursive whether to recursively process nested objects and arrays (default: true)\n */\nexport function omitUndefined<T extends Record<string, unknown>>(obj: T, recursive?: true): DeepNonUndefined<T>;\nexport function omitUndefined<T extends Record<string, unknown>>(obj: T, recursive: false): ShallowNonUndefined<T>;\nexport function omitUndefined<T extends Record<string, unknown>>(\n obj: T,\n recursive = true,\n): DeepNonUndefined<T> | ShallowNonUndefined<T> {\n const result = {} as DeepNonUndefined<T> | ShallowNonUndefined<T>;\n\n for (const [key, value] of Object.entries(obj)) {\n if (value !== undefined) {\n // recursively process nested objects\n if (recursive && value && typeof value === 'object' && !Array.isArray(value) && !(value instanceof Date)) {\n (result as Record<string, unknown>)[key] = omitUndefined(value as Record<string, unknown>, recursive);\n } else if (recursive && Array.isArray(value)) {\n // recursively process arrays\n (result as Record<string, unknown>)[key] = value\n .filter((item) => item !== undefined)\n .map((item) => {\n if (item && typeof item === 'object' && !Array.isArray(item) && !(item instanceof Date)) {\n return omitUndefined(item as Record<string, unknown>, recursive);\n }\n return item;\n });\n } else {\n // handle primitive values\n (result as Record<string, unknown>)[key] = value;\n }\n }\n }\n\n return result;\n}\n","export function optionalize<T, U, D = undefined>(\n fn: (v: T) => U,\n options?: { defaultValue?: D | undefined; strategy?: 'falsy' | 'nullish' | undefined } | undefined,\n): (v: T | null | undefined) => U | D {\n const defaultValue = options?.defaultValue;\n const emptyMode = options?.strategy ?? 'nullish';\n\n return (v: T | null | undefined): U | D => {\n if (v === null || v === undefined || (emptyMode === 'falsy' && !v)) {\n return defaultValue as D;\n }\n\n return fn(v);\n };\n}\n","import { sleep } from './sleep';\n\nexport type RetryOptions = {\n /** total number of attempts before the last error is rethrown (default 3) */\n retries?: number | undefined;\n\n /** fixed delay between attempts (default 1000) */\n delayMs?: number | undefined;\n\n /** aborts the delay between attempts */\n signal?: AbortSignal | undefined;\n\n /** called after every failed attempt with the error and the 1-based attempt number */\n onError?: ((error: unknown, attempt: number) => void) | undefined;\n};\n\n/**\n * Runs `fn`, retrying with a fixed delay on failure. Resolves with the first successful result;\n * rethrows the last error once the attempts are exhausted.\n */\nexport async function retry<T>(fn: () => T, options?: RetryOptions | undefined): Promise<T> {\n const retries = options?.retries ?? 3;\n const delayMs = options?.delayMs ?? 1000;\n\n for (let attempt = 1; ; attempt++) {\n try {\n return await fn();\n } catch (error) {\n options?.onError?.(error, attempt);\n\n if (attempt >= retries) {\n throw error;\n }\n\n await sleep(delayMs, options?.signal);\n }\n }\n}\n"],"mappings":"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"}
|
package/dist/match.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
//#region src/match/index.d.ts
|
|
2
|
+
type StringPattern = ((value: string) => boolean) | {
|
|
3
|
+
exact: string;
|
|
4
|
+
} | {
|
|
5
|
+
prefix: string;
|
|
6
|
+
} | {
|
|
7
|
+
suffix: string;
|
|
8
|
+
} | {
|
|
9
|
+
includes: string;
|
|
10
|
+
} | {
|
|
11
|
+
pattern: RegExp;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Checks if a string matches a single pattern.
|
|
15
|
+
*/
|
|
16
|
+
declare function matches(value: string, pattern: StringPattern): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Checks if a string matches any of the patterns.
|
|
19
|
+
* For repeated matching against the same pattern set, prefer {@link createMatcher}.
|
|
20
|
+
*/
|
|
21
|
+
declare function matchesAny(value: string, patterns: readonly StringPattern[]): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Compiles a pattern set into a matcher function.
|
|
24
|
+
*
|
|
25
|
+
* Patterns are bucketed by check cost at creation time: all `exact` patterns collapse
|
|
26
|
+
* into a single Set lookup, then prefixes / suffixes / substrings run as cheap string
|
|
27
|
+
* checks, and regular expressions and matcher functions run last.
|
|
28
|
+
*/
|
|
29
|
+
declare function createMatcher(patterns: readonly StringPattern[]): (value: string) => boolean;
|
|
30
|
+
//#endregion
|
|
31
|
+
export { StringPattern, createMatcher, matches, matchesAny };
|
|
32
|
+
//# sourceMappingURL=match.d.ts.map
|
package/dist/match.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
//#region src/match/index.ts
|
|
2
|
+
/**
|
|
3
|
+
* Checks if a string matches a single pattern.
|
|
4
|
+
*/
|
|
5
|
+
function matches(value, pattern) {
|
|
6
|
+
if (typeof pattern === "function") return pattern(value);
|
|
7
|
+
if ("exact" in pattern) return value === pattern.exact;
|
|
8
|
+
if ("prefix" in pattern) return value.startsWith(pattern.prefix);
|
|
9
|
+
if ("suffix" in pattern) return value.endsWith(pattern.suffix);
|
|
10
|
+
if ("includes" in pattern) return value.includes(pattern.includes);
|
|
11
|
+
return pattern.pattern.test(value);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Checks if a string matches any of the patterns.
|
|
15
|
+
* For repeated matching against the same pattern set, prefer {@link createMatcher}.
|
|
16
|
+
*/
|
|
17
|
+
function matchesAny(value, patterns) {
|
|
18
|
+
return patterns.some((pattern) => matches(value, pattern));
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Compiles a pattern set into a matcher function.
|
|
22
|
+
*
|
|
23
|
+
* Patterns are bucketed by check cost at creation time: all `exact` patterns collapse
|
|
24
|
+
* into a single Set lookup, then prefixes / suffixes / substrings run as cheap string
|
|
25
|
+
* checks, and regular expressions and matcher functions run last.
|
|
26
|
+
*/
|
|
27
|
+
function createMatcher(patterns) {
|
|
28
|
+
const exacts = /* @__PURE__ */ new Set();
|
|
29
|
+
const prefixes = [];
|
|
30
|
+
const suffixes = [];
|
|
31
|
+
const substrings = [];
|
|
32
|
+
const regexps = [];
|
|
33
|
+
const fns = [];
|
|
34
|
+
for (const pattern of patterns) if (typeof pattern === "function") fns.push(pattern);
|
|
35
|
+
else if ("exact" in pattern) exacts.add(pattern.exact);
|
|
36
|
+
else if ("prefix" in pattern) prefixes.push(pattern.prefix);
|
|
37
|
+
else if ("suffix" in pattern) suffixes.push(pattern.suffix);
|
|
38
|
+
else if ("includes" in pattern) substrings.push(pattern.includes);
|
|
39
|
+
else regexps.push(pattern.pattern);
|
|
40
|
+
return (value) => {
|
|
41
|
+
if (exacts.has(value)) return true;
|
|
42
|
+
return prefixes.some((prefix) => value.startsWith(prefix)) || suffixes.some((suffix) => value.endsWith(suffix)) || substrings.some((substring) => value.includes(substring)) || regexps.some((regexp) => regexp.test(value)) || fns.some((fn) => fn(value));
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
export { createMatcher, matches, matchesAny };
|
|
47
|
+
|
|
48
|
+
//# sourceMappingURL=match.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"match.js","names":[],"sources":["../src/match/index.ts"],"sourcesContent":["export type StringPattern =\n | ((value: string) => boolean)\n | { exact: string }\n | { prefix: string }\n | { suffix: string }\n | { includes: string }\n | { pattern: RegExp };\n\n/**\n * Checks if a string matches a single pattern.\n */\nexport function matches(value: string, pattern: StringPattern): boolean {\n if (typeof pattern === 'function') {\n return pattern(value);\n }\n\n if ('exact' in pattern) {\n return value === pattern.exact;\n }\n\n if ('prefix' in pattern) {\n return value.startsWith(pattern.prefix);\n }\n\n if ('suffix' in pattern) {\n return value.endsWith(pattern.suffix);\n }\n\n if ('includes' in pattern) {\n return value.includes(pattern.includes);\n }\n\n return pattern.pattern.test(value);\n}\n\n/**\n * Checks if a string matches any of the patterns.\n * For repeated matching against the same pattern set, prefer {@link createMatcher}.\n */\nexport function matchesAny(value: string, patterns: readonly StringPattern[]): boolean {\n return patterns.some((pattern) => matches(value, pattern));\n}\n\n/**\n * Compiles a pattern set into a matcher function.\n *\n * Patterns are bucketed by check cost at creation time: all `exact` patterns collapse\n * into a single Set lookup, then prefixes / suffixes / substrings run as cheap string\n * checks, and regular expressions and matcher functions run last.\n */\nexport function createMatcher(patterns: readonly StringPattern[]): (value: string) => boolean {\n const exacts = new Set<string>();\n const prefixes: string[] = [];\n const suffixes: string[] = [];\n const substrings: string[] = [];\n const regexps: RegExp[] = [];\n const fns: ((value: string) => boolean)[] = [];\n\n for (const pattern of patterns) {\n if (typeof pattern === 'function') {\n fns.push(pattern);\n } else if ('exact' in pattern) {\n exacts.add(pattern.exact);\n } else if ('prefix' in pattern) {\n prefixes.push(pattern.prefix);\n } else if ('suffix' in pattern) {\n suffixes.push(pattern.suffix);\n } else if ('includes' in pattern) {\n substrings.push(pattern.includes);\n } else {\n regexps.push(pattern.pattern);\n }\n }\n\n return (value) => {\n if (exacts.has(value)) {\n return true;\n }\n\n return (\n prefixes.some((prefix) => value.startsWith(prefix)) ||\n suffixes.some((suffix) => value.endsWith(suffix)) ||\n substrings.some((substring) => value.includes(substring)) ||\n regexps.some((regexp) => regexp.test(value)) ||\n fns.some((fn) => fn(value))\n );\n };\n}\n"],"mappings":";;;;AAWA,SAAgB,QAAQ,OAAe,SAAiC;CACtE,IAAI,OAAO,YAAY,YACrB,OAAO,QAAQ,KAAK;CAGtB,IAAI,WAAW,SACb,OAAO,UAAU,QAAQ;CAG3B,IAAI,YAAY,SACd,OAAO,MAAM,WAAW,QAAQ,MAAM;CAGxC,IAAI,YAAY,SACd,OAAO,MAAM,SAAS,QAAQ,MAAM;CAGtC,IAAI,cAAc,SAChB,OAAO,MAAM,SAAS,QAAQ,QAAQ;CAGxC,OAAO,QAAQ,QAAQ,KAAK,KAAK;AACnC;;;;;AAMA,SAAgB,WAAW,OAAe,UAA6C;CACrF,OAAO,SAAS,MAAM,YAAY,QAAQ,OAAO,OAAO,CAAC;AAC3D;;;;;;;;AASA,SAAgB,cAAc,UAAgE;CAC5F,MAAM,yBAAS,IAAI,IAAY;CAC/B,MAAM,WAAqB,CAAC;CAC5B,MAAM,WAAqB,CAAC;CAC5B,MAAM,aAAuB,CAAC;CAC9B,MAAM,UAAoB,CAAC;CAC3B,MAAM,MAAsC,CAAC;CAE7C,KAAK,MAAM,WAAW,UACpB,IAAI,OAAO,YAAY,YACrB,IAAI,KAAK,OAAO;MACX,IAAI,WAAW,SACpB,OAAO,IAAI,QAAQ,KAAK;MACnB,IAAI,YAAY,SACrB,SAAS,KAAK,QAAQ,MAAM;MACvB,IAAI,YAAY,SACrB,SAAS,KAAK,QAAQ,MAAM;MACvB,IAAI,cAAc,SACvB,WAAW,KAAK,QAAQ,QAAQ;MAEhC,QAAQ,KAAK,QAAQ,OAAO;CAIhC,QAAQ,UAAU;EAChB,IAAI,OAAO,IAAI,KAAK,GAClB,OAAO;EAGT,OACE,SAAS,MAAM,WAAW,MAAM,WAAW,MAAM,CAAC,KAClD,SAAS,MAAM,WAAW,MAAM,SAAS,MAAM,CAAC,KAChD,WAAW,MAAM,cAAc,MAAM,SAAS,SAAS,CAAC,KACxD,QAAQ,MAAM,WAAW,OAAO,KAAK,KAAK,CAAC,KAC3C,IAAI,MAAM,OAAO,GAAG,KAAK,CAAC;CAE9B;AACF"}
|
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { DependencyList, EffectCallback } from "react";
|
|
2
|
+
//#region src/react/use-debounced-value.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Returns the value, updated only after it has stayed unchanged for the given delay.
|
|
5
|
+
*/
|
|
6
|
+
declare function useDebouncedValue<T>(value: T, delay: number): T;
|
|
7
|
+
//#endregion
|
|
8
|
+
//#region src/react/use-effect-after-mount.d.ts
|
|
9
|
+
/**
|
|
10
|
+
* Runs the effect on dependency changes, but skips the initial mount.
|
|
11
|
+
*/
|
|
12
|
+
declare function useEffectAfterMount(effect: EffectCallback, deps?: DependencyList | undefined): void;
|
|
13
|
+
//#endregion
|
|
14
|
+
//#region src/react/use-effect-once.d.ts
|
|
15
|
+
/**
|
|
16
|
+
* Runs the effect exactly once on mount.
|
|
17
|
+
*/
|
|
18
|
+
declare function useEffectOnce(effect: EffectCallback): void;
|
|
19
|
+
//#endregion
|
|
20
|
+
//#region src/react/use-is-mobile.d.ts
|
|
21
|
+
/**
|
|
22
|
+
* Tracks whether the viewport is below the mobile breakpoint (768px).
|
|
23
|
+
* Returns false during SSR.
|
|
24
|
+
*/
|
|
25
|
+
declare function useIsMobile(): boolean;
|
|
26
|
+
//#endregion
|
|
27
|
+
export { useDebouncedValue, useEffectAfterMount, useEffectOnce, useIsMobile };
|
|
28
|
+
//# sourceMappingURL=react.d.ts.map
|
package/dist/react.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { useEffect, useRef, useState, useSyncExternalStore } from "react";
|
|
2
|
+
//#region src/react/use-debounced-value.ts
|
|
3
|
+
/**
|
|
4
|
+
* Returns the value, updated only after it has stayed unchanged for the given delay.
|
|
5
|
+
*/
|
|
6
|
+
function useDebouncedValue(value, delay) {
|
|
7
|
+
const [debouncedValue, setDebouncedValue] = useState(value);
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
const timeout = setTimeout(() => {
|
|
10
|
+
setDebouncedValue(value);
|
|
11
|
+
}, delay);
|
|
12
|
+
return () => clearTimeout(timeout);
|
|
13
|
+
}, [value, delay]);
|
|
14
|
+
return debouncedValue;
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region src/react/use-effect-after-mount.ts
|
|
18
|
+
/**
|
|
19
|
+
* Runs the effect on dependency changes, but skips the initial mount.
|
|
20
|
+
*/
|
|
21
|
+
function useEffectAfterMount(effect, deps) {
|
|
22
|
+
const mountedRef = useRef(false);
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (mountedRef.current) return effect();
|
|
25
|
+
mountedRef.current = true;
|
|
26
|
+
}, deps);
|
|
27
|
+
}
|
|
28
|
+
//#endregion
|
|
29
|
+
//#region src/react/use-effect-once.ts
|
|
30
|
+
/**
|
|
31
|
+
* Runs the effect exactly once on mount.
|
|
32
|
+
*/
|
|
33
|
+
function useEffectOnce(effect) {
|
|
34
|
+
const hasRunRef = useRef(false);
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (hasRunRef.current) return;
|
|
37
|
+
hasRunRef.current = true;
|
|
38
|
+
return effect();
|
|
39
|
+
}, []);
|
|
40
|
+
}
|
|
41
|
+
//#endregion
|
|
42
|
+
//#region src/react/use-is-mobile.ts
|
|
43
|
+
const MOBILE_BREAKPOINT = 768;
|
|
44
|
+
function subscribe(callback) {
|
|
45
|
+
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
|
|
46
|
+
mql.addEventListener("change", callback);
|
|
47
|
+
return () => mql.removeEventListener("change", callback);
|
|
48
|
+
}
|
|
49
|
+
function getSnapshot() {
|
|
50
|
+
return window.innerWidth < MOBILE_BREAKPOINT;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Tracks whether the viewport is below the mobile breakpoint (768px).
|
|
54
|
+
* Returns false during SSR.
|
|
55
|
+
*/
|
|
56
|
+
function useIsMobile() {
|
|
57
|
+
return useSyncExternalStore(subscribe, getSnapshot, () => false);
|
|
58
|
+
}
|
|
59
|
+
//#endregion
|
|
60
|
+
export { useDebouncedValue, useEffectAfterMount, useEffectOnce, useIsMobile };
|
|
61
|
+
|
|
62
|
+
//# sourceMappingURL=react.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"react.js","names":[],"sources":["../src/react/use-debounced-value.ts","../src/react/use-effect-after-mount.ts","../src/react/use-effect-once.ts","../src/react/use-is-mobile.ts"],"sourcesContent":["import { useEffect, useState } from 'react';\n\n/**\n * Returns the value, updated only after it has stayed unchanged for the given delay.\n */\nexport function useDebouncedValue<T>(value: T, delay: number): T {\n const [debouncedValue, setDebouncedValue] = useState(value);\n\n useEffect(() => {\n const timeout = setTimeout(() => {\n setDebouncedValue(value);\n }, delay);\n\n return () => clearTimeout(timeout);\n }, [value, delay]);\n\n return debouncedValue;\n}\n","import { type DependencyList, type EffectCallback, useEffect, useRef } from 'react';\n\n/**\n * Runs the effect on dependency changes, but skips the initial mount.\n */\nexport function useEffectAfterMount(effect: EffectCallback, deps?: DependencyList | undefined) {\n const mountedRef = useRef(false);\n\n useEffect(() => {\n if (mountedRef.current) {\n return effect();\n }\n mountedRef.current = true;\n }, deps); // eslint-disable-line @eslint-react/exhaustive-deps\n}\n","import { type EffectCallback, useEffect, useRef } from 'react';\n\n/**\n * Runs the effect exactly once on mount.\n */\nexport function useEffectOnce(effect: EffectCallback) {\n const hasRunRef = useRef(false);\n\n useEffect(() => {\n if (hasRunRef.current) {\n return;\n }\n\n hasRunRef.current = true;\n\n return effect();\n }, []); // eslint-disable-line @eslint-react/exhaustive-deps\n}\n","import { useSyncExternalStore } from 'react';\n\nconst MOBILE_BREAKPOINT = 768;\n\nfunction subscribe(callback: () => void): () => void {\n const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);\n\n mql.addEventListener('change', callback);\n\n return () => mql.removeEventListener('change', callback);\n}\n\nfunction getSnapshot(): boolean {\n return window.innerWidth < MOBILE_BREAKPOINT;\n}\n\n/**\n * Tracks whether the viewport is below the mobile breakpoint (768px).\n * Returns false during SSR.\n */\nexport function useIsMobile(): boolean {\n return useSyncExternalStore(subscribe, getSnapshot, () => false);\n}\n"],"mappings":";;;;;AAKA,SAAgB,kBAAqB,OAAU,OAAkB;CAC/D,MAAM,CAAC,gBAAgB,qBAAqB,SAAS,KAAK;CAE1D,gBAAgB;EACd,MAAM,UAAU,iBAAiB;GAC/B,kBAAkB,KAAK;EACzB,GAAG,KAAK;EAER,aAAa,aAAa,OAAO;CACnC,GAAG,CAAC,OAAO,KAAK,CAAC;CAEjB,OAAO;AACT;;;;;;ACZA,SAAgB,oBAAoB,QAAwB,MAAmC;CAC7F,MAAM,aAAa,OAAO,KAAK;CAE/B,gBAAgB;EACd,IAAI,WAAW,SACb,OAAO,OAAO;EAEhB,WAAW,UAAU;CACvB,GAAG,IAAI;AACT;;;;;;ACTA,SAAgB,cAAc,QAAwB;CACpD,MAAM,YAAY,OAAO,KAAK;CAE9B,gBAAgB;EACd,IAAI,UAAU,SACZ;EAGF,UAAU,UAAU;EAEpB,OAAO,OAAO;CAChB,GAAG,CAAC,CAAC;AACP;;;ACfA,MAAM,oBAAoB;AAE1B,SAAS,UAAU,UAAkC;CACnD,MAAM,MAAM,OAAO,WAAW,eAAe,oBAAoB,EAAE,IAAI;CAEvE,IAAI,iBAAiB,UAAU,QAAQ;CAEvC,aAAa,IAAI,oBAAoB,UAAU,QAAQ;AACzD;AAEA,SAAS,cAAuB;CAC9B,OAAO,OAAO,aAAa;AAC7B;;;;;AAMA,SAAgB,cAAuB;CACrC,OAAO,qBAAqB,WAAW,mBAAmB,KAAK;AACjE"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
//#region src/core/sleep.ts
|
|
2
|
+
function sleep(ms, signal) {
|
|
3
|
+
if (signal?.aborted) return Promise.reject(signal.reason);
|
|
4
|
+
return new Promise((resolve, reject) => {
|
|
5
|
+
const timer = setTimeout(() => {
|
|
6
|
+
signal?.removeEventListener("abort", onAbort);
|
|
7
|
+
resolve();
|
|
8
|
+
}, ms);
|
|
9
|
+
const onAbort = () => {
|
|
10
|
+
clearTimeout(timer);
|
|
11
|
+
reject(signal.reason);
|
|
12
|
+
};
|
|
13
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
17
|
+
export { sleep as t };
|
|
18
|
+
|
|
19
|
+
//# sourceMappingURL=sleep-J71HYu7n.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sleep-J71HYu7n.js","names":[],"sources":["../src/core/sleep.ts"],"sourcesContent":["export function sleep(ms: number, signal?: AbortSignal | undefined): Promise<void> {\n if (signal?.aborted) {\n return Promise.reject(signal.reason);\n }\n\n return new Promise((resolve, reject) => {\n const timer = setTimeout(() => {\n signal?.removeEventListener('abort', onAbort);\n resolve();\n }, ms);\n\n const onAbort = () => {\n clearTimeout(timer);\n reject(signal!.reason);\n };\n\n signal?.addEventListener('abort', onAbort, { once: true });\n });\n}\n"],"mappings":";AAAA,SAAgB,MAAM,IAAY,QAAiD;CACjF,IAAI,QAAQ,SACV,OAAO,QAAQ,OAAO,OAAO,MAAM;CAGrC,OAAO,IAAI,SAAS,SAAS,WAAW;EACtC,MAAM,QAAQ,iBAAiB;GAC7B,QAAQ,oBAAoB,SAAS,OAAO;GAC5C,QAAQ;EACV,GAAG,EAAE;EAEL,MAAM,gBAAgB;GACpB,aAAa,KAAK;GAClB,OAAO,OAAQ,MAAM;EACvB;EAEA,QAAQ,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;CAC3D,CAAC;AACH"}
|
package/package.json
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@entwico/dash",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Typical reusable utilities shared across projects. Tree-shakable, ESM-only, zero-dependency core.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"author": "admin@entwico.com",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/entwico/dash"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"start": "tsdown --watch",
|
|
15
|
+
"build": "tsdown",
|
|
16
|
+
"test": "vitest",
|
|
17
|
+
"coverage": "vitest run --coverage",
|
|
18
|
+
"clear": "rm -rf dist",
|
|
19
|
+
"lint": "eslint --cache src",
|
|
20
|
+
"lint:fix": "pnpm lint --fix",
|
|
21
|
+
"typecheck": "tsc --noEmit",
|
|
22
|
+
"validate": "pnpm lint && pnpm typecheck && pnpm coverage",
|
|
23
|
+
"prepublishOnly": "pnpm build",
|
|
24
|
+
"ci:version": "changeset version && pnpm install --no-frozen-lockfile",
|
|
25
|
+
"ci:publish": "changeset publish"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"clsx": ">=2",
|
|
29
|
+
"react": ">=18",
|
|
30
|
+
"tailwind-merge": ">=2"
|
|
31
|
+
},
|
|
32
|
+
"peerDependenciesMeta": {
|
|
33
|
+
"clsx": {
|
|
34
|
+
"optional": true
|
|
35
|
+
},
|
|
36
|
+
"react": {
|
|
37
|
+
"optional": true
|
|
38
|
+
},
|
|
39
|
+
"tailwind-merge": {
|
|
40
|
+
"optional": true
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@changesets/cli": "^2.31.0",
|
|
45
|
+
"@entwico/eslint-config": "^2.0.7",
|
|
46
|
+
"@types/node": "^26.1.1",
|
|
47
|
+
"@types/react": "^19.2.17",
|
|
48
|
+
"@vitest/coverage-istanbul": "^4.1.10",
|
|
49
|
+
"clsx": "^2.1.1",
|
|
50
|
+
"eslint": "^10.6.0",
|
|
51
|
+
"react": "^19.2.7",
|
|
52
|
+
"tailwind-merge": "^3.6.0",
|
|
53
|
+
"tsdown": "^0.22.4",
|
|
54
|
+
"typescript": "^6.0.3",
|
|
55
|
+
"vitest": "^4.1.10"
|
|
56
|
+
},
|
|
57
|
+
"files": [
|
|
58
|
+
"dist"
|
|
59
|
+
],
|
|
60
|
+
"exports": {
|
|
61
|
+
".": {
|
|
62
|
+
"types": "./dist/index.d.ts",
|
|
63
|
+
"import": "./dist/index.js"
|
|
64
|
+
},
|
|
65
|
+
"./async": {
|
|
66
|
+
"types": "./dist/async.d.ts",
|
|
67
|
+
"import": "./dist/async.js"
|
|
68
|
+
},
|
|
69
|
+
"./match": {
|
|
70
|
+
"types": "./dist/match.d.ts",
|
|
71
|
+
"import": "./dist/match.js"
|
|
72
|
+
},
|
|
73
|
+
"./cn": {
|
|
74
|
+
"types": "./dist/cn.d.ts",
|
|
75
|
+
"import": "./dist/cn.js"
|
|
76
|
+
},
|
|
77
|
+
"./react": {
|
|
78
|
+
"types": "./dist/react.d.ts",
|
|
79
|
+
"import": "./dist/react.js"
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
"publishConfig": {
|
|
83
|
+
"access": "public",
|
|
84
|
+
"provenance": true
|
|
85
|
+
},
|
|
86
|
+
"packageManager": "pnpm@11.1.3"
|
|
87
|
+
}
|