@useless-engineer/utils 0.1.0 β 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +202 -4
- package/dist/index.cjs +57 -2
- package/dist/index.d.cts +24 -1
- package/dist/index.d.ts +24 -1
- package/dist/index.js +55 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,11 +1,209 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
# π οΈ @useless-engineer/utils
|
|
4
|
+
|
|
5
|
+
**Small, typed utilities distilled from real production bugs.**
|
|
6
|
+
One package. Multiple entry points. Only pay for what you import.
|
|
7
|
+
|
|
8
|
+
[](https://www.npmjs.com/package/@useless-engineer/utils)
|
|
9
|
+
[](https://www.npmjs.com/package/@useless-engineer/utils)
|
|
10
|
+
[](./LICENSE)
|
|
11
|
+
|
|
12
|
+

|
|
13
|
+

|
|
14
|
+

|
|
15
|
+

|
|
16
|
+

|
|
17
|
+
|
|
18
|
+
[**π¦ Install**](#-install) Β·
|
|
19
|
+
[**β‘ Quick start**](#-quick-start) Β·
|
|
20
|
+
[**π API**](#-api) Β·
|
|
21
|
+
[**πΊοΈ Roadmap**](#οΈ-roadmap) Β·
|
|
22
|
+
[**πΈ Follow**](https://www.instagram.com/useless_engineer.dev/)
|
|
23
|
+
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## β¨ Why this exists
|
|
29
|
+
|
|
30
|
+
Every utility here started as an [Instagram post](https://www.instagram.com/useless_engineer.dev/) about a bug that actually shipped. The post explains the problem; this package is the fix, typed and tested, so you never copy-paste it again.
|
|
31
|
+
|
|
32
|
+
| | |
|
|
33
|
+
| ------------------------ | ------------------------------------------------------ |
|
|
34
|
+
| π― **Focused** | Each function does one thing and has tests |
|
|
35
|
+
| π² **Tree-shakeable** | `sideEffects: false`, import only what you use |
|
|
36
|
+
| π§© **Split by platform** | Core, React Native, and more, each its own import path |
|
|
37
|
+
| π **Strictly typed** | Written in TypeScript with strict mode on |
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## π¦ Install
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm install @useless-engineer/utils
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
<details>
|
|
48
|
+
<summary><b>pnpm / yarn / bun</b></summary>
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pnpm add @useless-engineer/utils
|
|
52
|
+
yarn add @useless-engineer/utils
|
|
53
|
+
bun add @useless-engineer/utils
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
</details>
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## β‘ Quick start
|
|
5
61
|
|
|
6
62
|
```ts
|
|
7
63
|
import { debounce, throttle } from "@useless-engineer/utils";
|
|
64
|
+
|
|
65
|
+
const onSearch = debounce((query: string) => fetchResults(query), 300);
|
|
66
|
+
const onScroll = throttle(() => updateHeader(), 100);
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
import { isIOS, isAndroid } from "@useless-engineer/utils/react-native";
|
|
71
|
+
|
|
72
|
+
const padding = isIOS ? 20 : 16;
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
> π‘ `react` and `react-native` are **optional** peer dependencies. If you only use the core entry, you never need to install either.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## π§ Entry points
|
|
80
|
+
|
|
81
|
+
| Import path | For | Status |
|
|
82
|
+
| -------------------------------------- | ------------------------- | ----------------------------------------------------------------------------- |
|
|
83
|
+
| `@useless-engineer/utils` | Platform-agnostic helpers |  |
|
|
84
|
+
| `@useless-engineer/utils/react-native` | React Native helpers |  |
|
|
85
|
+
| `@useless-engineer/utils/react` | React hooks |  |
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## π API
|
|
90
|
+
|
|
91
|
+
### Core
|
|
92
|
+
|
|
93
|
+
<details>
|
|
94
|
+
<summary><code>debounce(fn, delay)</code> <img alt="live" src="https://img.shields.io/badge/-live-22c55e?style=flat-square"> <img alt="async" src="https://img.shields.io/badge/-async-6366f1?style=flat-square"></summary>
|
|
95
|
+
|
|
96
|
+
<br>
|
|
97
|
+
|
|
98
|
+
Waits for quiet. Every call resets the timer, and only the last call fires. Good for search inputs and autosave.
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
const save = debounce((text: string) => api.save(text), 500);
|
|
102
|
+
|
|
103
|
+
save("h");
|
|
104
|
+
save("he");
|
|
105
|
+
save("hello"); // only this one runs, 500ms after the last call
|
|
106
|
+
|
|
107
|
+
// In a React effect cleanup, so nothing fires after unmount:
|
|
108
|
+
save.cancel();
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
| Param | Type | Description |
|
|
112
|
+
| ----------- | ------------------- | ----------------------------------- |
|
|
113
|
+
| `fn` | `(...args) => void` | Function to debounce |
|
|
114
|
+
| `delay` | `number` | Milliseconds of quiet before firing |
|
|
115
|
+
| **returns** | `Debounced` | Callable with a `.cancel()` method |
|
|
116
|
+
|
|
117
|
+
</details>
|
|
118
|
+
|
|
119
|
+
<details>
|
|
120
|
+
<summary><code>throttle(fn, interval, options?)</code> <img alt="live" src="https://img.shields.io/badge/-live-22c55e?style=flat-square"> <img alt="async" src="https://img.shields.io/badge/-async-6366f1?style=flat-square"></summary>
|
|
121
|
+
|
|
122
|
+
<br>
|
|
123
|
+
|
|
124
|
+
Keeps a steady beat. Runs at most once per interval, however often it's called. Good for scroll, drag, and resize.
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
const onScroll = throttle(() => updateHeader(), 100);
|
|
128
|
+
|
|
129
|
+
// Skip the immediate first call:
|
|
130
|
+
throttle(fn, 100, { leading: false });
|
|
131
|
+
|
|
132
|
+
// Skip the final catch-up call:
|
|
133
|
+
throttle(fn, 100, { trailing: false });
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
| Option | Default | Description |
|
|
137
|
+
| ---------- | ------- | --------------------------------------------------- |
|
|
138
|
+
| `leading` | `true` | Fire right away on the first call of a burst |
|
|
139
|
+
| `trailing` | `true` | Fire once more at the end with the latest arguments |
|
|
140
|
+
|
|
141
|
+
Returns a function with `.cancel()`.
|
|
142
|
+
|
|
143
|
+
</details>
|
|
144
|
+
|
|
145
|
+
<details>
|
|
146
|
+
<summary><b>debounce or throttle?</b></summary>
|
|
147
|
+
|
|
148
|
+
<br>
|
|
149
|
+
|
|
150
|
+
| | `debounce` | `throttle` |
|
|
151
|
+
| ------------ | --------------------------------- | -------------------------------- |
|
|
152
|
+
| Fires | Once, after calls **stop** | On a fixed beat **during** calls |
|
|
153
|
+
| Use for | Search box, autosave, validation | Scroll, drag, resize |
|
|
154
|
+
| Ask yourself | "Do I only need the final state?" | "Does it need to stay live?" |
|
|
155
|
+
|
|
156
|
+
</details>
|
|
157
|
+
|
|
158
|
+
### React Native
|
|
159
|
+
|
|
160
|
+
<details>
|
|
161
|
+
<summary><code>isIOS</code> / <code>isAndroid</code> <img alt="live" src="https://img.shields.io/badge/-live-22c55e?style=flat-square"> <img alt="react native" src="https://img.shields.io/badge/-react%20native-61DAFB?style=flat-square&logoColor=black"></summary>
|
|
162
|
+
|
|
163
|
+
<br>
|
|
164
|
+
|
|
165
|
+
Plain booleans derived from `Platform.OS`.
|
|
166
|
+
|
|
167
|
+
```ts
|
|
8
168
|
import { isIOS, isAndroid } from "@useless-engineer/utils/react-native";
|
|
169
|
+
|
|
170
|
+
const behavior = isIOS ? "padding" : "height";
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
</details>
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## πΊοΈ Roadmap
|
|
178
|
+
|
|
179
|
+
Released one or two at a time, each matching a post that's already live.
|
|
180
|
+
|
|
181
|
+
| Next up | Entry | Origin |
|
|
182
|
+
| ------------------------------------------------------------------------------------------------------------------------ | ------------ | ------------------------------------ |
|
|
183
|
+
| `safeParseJSON`, `safeStringify`, `deepClone` | core | JSON and shallow-copy gotchas |
|
|
184
|
+
|  `getAccessibilityProps`, `createShadow` | react-native | Accessibility and iOS-only shadows |
|
|
185
|
+
|  `useInterval`, `useCountdown`, `useIsMounted` | react | Stale closures and unmounted updates |
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## π€ Contributing
|
|
190
|
+
|
|
191
|
+
Found a bug or have a post idea that deserves a utility? [Open an issue](https://github.com/<your-username>/useless-engineer/issues).
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
npm install
|
|
195
|
+
npm test
|
|
196
|
+
npm run build
|
|
9
197
|
```
|
|
10
198
|
|
|
11
|
-
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
<div align="center">
|
|
202
|
+
|
|
203
|
+
**Built from real bugs, one post at a time.**
|
|
204
|
+
|
|
205
|
+
[](https://www.instagram.com/useless_engineer.dev/)
|
|
206
|
+
|
|
207
|
+
MIT Β© Saurabh Bandkar
|
|
208
|
+
|
|
209
|
+
</div>
|
package/dist/index.cjs
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
// src/async/debounce.ts
|
|
4
|
-
function debounce(fn,
|
|
4
|
+
function debounce(fn, delay2) {
|
|
5
5
|
let timer;
|
|
6
6
|
const debounced = (...args) => {
|
|
7
7
|
if (timer !== void 0) clearTimeout(timer);
|
|
8
8
|
timer = setTimeout(() => {
|
|
9
9
|
timer = void 0;
|
|
10
10
|
fn(...args);
|
|
11
|
-
},
|
|
11
|
+
}, delay2);
|
|
12
12
|
};
|
|
13
13
|
debounced.cancel = () => {
|
|
14
14
|
if (timer !== void 0) clearTimeout(timer);
|
|
@@ -56,5 +56,60 @@ function throttle(fn, interval, { leading = true, trailing = true } = {}) {
|
|
|
56
56
|
return throttled;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
// src/async/delay.ts
|
|
60
|
+
function delay(ms, { signal } = {}) {
|
|
61
|
+
if (typeof ms !== "number" || !Number.isFinite(ms) || ms < 0) {
|
|
62
|
+
return Promise.reject(
|
|
63
|
+
new RangeError(`delay: expected a finite, non-negative number of ms, got ${String(ms)}`)
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
if (signal?.aborted) return Promise.reject(signal.reason);
|
|
67
|
+
return new Promise((resolve, reject) => {
|
|
68
|
+
const onAbort = () => {
|
|
69
|
+
clearTimeout(timer);
|
|
70
|
+
reject(signal.reason);
|
|
71
|
+
};
|
|
72
|
+
const timer = setTimeout(() => {
|
|
73
|
+
signal?.removeEventListener("abort", onAbort);
|
|
74
|
+
resolve();
|
|
75
|
+
}, ms);
|
|
76
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// src/async/withTimeout.ts
|
|
81
|
+
var TimeoutError = class extends Error {
|
|
82
|
+
constructor(message = "Operation timed out") {
|
|
83
|
+
super(message);
|
|
84
|
+
this.name = "TimeoutError";
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
function withTimeout(promise, ms, message) {
|
|
88
|
+
if (typeof ms !== "number" || !Number.isFinite(ms) || ms < 0) {
|
|
89
|
+
return Promise.reject(
|
|
90
|
+
new RangeError(`withTimeout: expected a finite, non-negative number of ms, got ${String(ms)}`)
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
return new Promise((resolve, reject) => {
|
|
94
|
+
const timer = setTimeout(
|
|
95
|
+
() => reject(new TimeoutError(message ?? `Timed out after ${ms}ms`)),
|
|
96
|
+
ms
|
|
97
|
+
);
|
|
98
|
+
Promise.resolve(promise).then(
|
|
99
|
+
(value) => {
|
|
100
|
+
clearTimeout(timer);
|
|
101
|
+
resolve(value);
|
|
102
|
+
},
|
|
103
|
+
(error) => {
|
|
104
|
+
clearTimeout(timer);
|
|
105
|
+
reject(error);
|
|
106
|
+
}
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
exports.TimeoutError = TimeoutError;
|
|
59
112
|
exports.debounce = debounce;
|
|
113
|
+
exports.delay = delay;
|
|
60
114
|
exports.throttle = throttle;
|
|
115
|
+
exports.withTimeout = withTimeout;
|
package/dist/index.d.cts
CHANGED
|
@@ -26,4 +26,27 @@ interface Throttled<A extends unknown[]> {
|
|
|
26
26
|
*/
|
|
27
27
|
declare function throttle<A extends unknown[]>(fn: (...args: A) => void, interval: number, { leading, trailing }?: ThrottleOptions): Throttled<A>;
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
interface DelayOptions {
|
|
30
|
+
/** Abort to stop waiting early. The promise rejects with `signal.reason`. */
|
|
31
|
+
signal?: AbortSignal;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Resolves after `ms` milliseconds.
|
|
35
|
+
*
|
|
36
|
+
* Unlike a bare `setTimeout`, it rejects bad input (NaN, negative, Infinity)
|
|
37
|
+
* instead of silently firing immediately, and it can be cancelled.
|
|
38
|
+
*/
|
|
39
|
+
declare function delay(ms: number, { signal }?: DelayOptions): Promise<void>;
|
|
40
|
+
|
|
41
|
+
declare class TimeoutError extends Error {
|
|
42
|
+
constructor(message?: string);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Rejects with `TimeoutError` if `promise` hasn't settled within `ms`.
|
|
46
|
+
*
|
|
47
|
+
* Note: this stops *waiting*, it does not stop the underlying work.
|
|
48
|
+
* To cancel a request too, pass an AbortSignal to it as well.
|
|
49
|
+
*/
|
|
50
|
+
declare function withTimeout<T>(promise: PromiseLike<T>, ms: number, message?: string): Promise<T>;
|
|
51
|
+
|
|
52
|
+
export { type Debounced, type DelayOptions, type ThrottleOptions, type Throttled, TimeoutError, debounce, delay, throttle, withTimeout };
|
package/dist/index.d.ts
CHANGED
|
@@ -26,4 +26,27 @@ interface Throttled<A extends unknown[]> {
|
|
|
26
26
|
*/
|
|
27
27
|
declare function throttle<A extends unknown[]>(fn: (...args: A) => void, interval: number, { leading, trailing }?: ThrottleOptions): Throttled<A>;
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
interface DelayOptions {
|
|
30
|
+
/** Abort to stop waiting early. The promise rejects with `signal.reason`. */
|
|
31
|
+
signal?: AbortSignal;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Resolves after `ms` milliseconds.
|
|
35
|
+
*
|
|
36
|
+
* Unlike a bare `setTimeout`, it rejects bad input (NaN, negative, Infinity)
|
|
37
|
+
* instead of silently firing immediately, and it can be cancelled.
|
|
38
|
+
*/
|
|
39
|
+
declare function delay(ms: number, { signal }?: DelayOptions): Promise<void>;
|
|
40
|
+
|
|
41
|
+
declare class TimeoutError extends Error {
|
|
42
|
+
constructor(message?: string);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Rejects with `TimeoutError` if `promise` hasn't settled within `ms`.
|
|
46
|
+
*
|
|
47
|
+
* Note: this stops *waiting*, it does not stop the underlying work.
|
|
48
|
+
* To cancel a request too, pass an AbortSignal to it as well.
|
|
49
|
+
*/
|
|
50
|
+
declare function withTimeout<T>(promise: PromiseLike<T>, ms: number, message?: string): Promise<T>;
|
|
51
|
+
|
|
52
|
+
export { type Debounced, type DelayOptions, type ThrottleOptions, type Throttled, TimeoutError, debounce, delay, throttle, withTimeout };
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// src/async/debounce.ts
|
|
2
|
-
function debounce(fn,
|
|
2
|
+
function debounce(fn, delay2) {
|
|
3
3
|
let timer;
|
|
4
4
|
const debounced = (...args) => {
|
|
5
5
|
if (timer !== void 0) clearTimeout(timer);
|
|
6
6
|
timer = setTimeout(() => {
|
|
7
7
|
timer = void 0;
|
|
8
8
|
fn(...args);
|
|
9
|
-
},
|
|
9
|
+
}, delay2);
|
|
10
10
|
};
|
|
11
11
|
debounced.cancel = () => {
|
|
12
12
|
if (timer !== void 0) clearTimeout(timer);
|
|
@@ -54,4 +54,56 @@ function throttle(fn, interval, { leading = true, trailing = true } = {}) {
|
|
|
54
54
|
return throttled;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
// src/async/delay.ts
|
|
58
|
+
function delay(ms, { signal } = {}) {
|
|
59
|
+
if (typeof ms !== "number" || !Number.isFinite(ms) || ms < 0) {
|
|
60
|
+
return Promise.reject(
|
|
61
|
+
new RangeError(`delay: expected a finite, non-negative number of ms, got ${String(ms)}`)
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
if (signal?.aborted) return Promise.reject(signal.reason);
|
|
65
|
+
return new Promise((resolve, reject) => {
|
|
66
|
+
const onAbort = () => {
|
|
67
|
+
clearTimeout(timer);
|
|
68
|
+
reject(signal.reason);
|
|
69
|
+
};
|
|
70
|
+
const timer = setTimeout(() => {
|
|
71
|
+
signal?.removeEventListener("abort", onAbort);
|
|
72
|
+
resolve();
|
|
73
|
+
}, ms);
|
|
74
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// src/async/withTimeout.ts
|
|
79
|
+
var TimeoutError = class extends Error {
|
|
80
|
+
constructor(message = "Operation timed out") {
|
|
81
|
+
super(message);
|
|
82
|
+
this.name = "TimeoutError";
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
function withTimeout(promise, ms, message) {
|
|
86
|
+
if (typeof ms !== "number" || !Number.isFinite(ms) || ms < 0) {
|
|
87
|
+
return Promise.reject(
|
|
88
|
+
new RangeError(`withTimeout: expected a finite, non-negative number of ms, got ${String(ms)}`)
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
return new Promise((resolve, reject) => {
|
|
92
|
+
const timer = setTimeout(
|
|
93
|
+
() => reject(new TimeoutError(message ?? `Timed out after ${ms}ms`)),
|
|
94
|
+
ms
|
|
95
|
+
);
|
|
96
|
+
Promise.resolve(promise).then(
|
|
97
|
+
(value) => {
|
|
98
|
+
clearTimeout(timer);
|
|
99
|
+
resolve(value);
|
|
100
|
+
},
|
|
101
|
+
(error) => {
|
|
102
|
+
clearTimeout(timer);
|
|
103
|
+
reject(error);
|
|
104
|
+
}
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export { TimeoutError, debounce, delay, throttle, withTimeout };
|