@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 CHANGED
@@ -1,11 +1,209 @@
1
- # @useless-engineer/utils
1
+ <div align="center">
2
2
 
3
- Small, typed utilities from [@useless_engineer.dev](https://www.instagram.com/useless_engineer.dev/).
4
- One package, multiple entry points, so React Native code never pulls in what it doesn't use.
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
+ [![npm version](https://img.shields.io/npm/v/@useless-engineer/utils?style=for-the-badge&logo=npm&color=CB3837&labelColor=1a1a1a)](https://www.npmjs.com/package/@useless-engineer/utils)
9
+ [![npm downloads](https://img.shields.io/npm/dm/@useless-engineer/utils?style=for-the-badge&color=7c3aed&labelColor=1a1a1a)](https://www.npmjs.com/package/@useless-engineer/utils)
10
+ [![license](https://img.shields.io/npm/l/@useless-engineer/utils?style=for-the-badge&color=22c55e&labelColor=1a1a1a)](./LICENSE)
11
+
12
+ ![TypeScript](https://img.shields.io/badge/TypeScript-ready-3178C6?style=flat-square&logo=typescript&logoColor=white)
13
+ ![Tree-shakeable](https://img.shields.io/badge/tree--shakeable-yes-10b981?style=flat-square)
14
+ ![Zero deps](https://img.shields.io/badge/dependencies-0-f59e0b?style=flat-square)
15
+ ![ESM + CJS](https://img.shields.io/badge/ESM%20%2B%20CJS-both-0ea5e9?style=flat-square)
16
+ ![React Native](https://img.shields.io/badge/React%20Native-supported-61DAFB?style=flat-square&logo=react&logoColor=black)
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 | ![live](https://img.shields.io/badge/-live-22c55e?style=flat-square) |
84
+ | `@useless-engineer/utils/react-native` | React Native helpers | ![live](https://img.shields.io/badge/-live-22c55e?style=flat-square) |
85
+ | `@useless-engineer/utils/react` | React hooks | ![soon](https://img.shields.io/badge/-coming%20soon-f59e0b?style=flat-square) |
86
+
87
+ ---
88
+
89
+ ## πŸ“š API
90
+
91
+ ### Core
92
+
93
+ <details>
94
+ <summary><code>debounce(fn, delay)</code> &nbsp; <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> &nbsp; <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> &nbsp; <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
+ | ![next](https://img.shields.io/badge/-next-f59e0b?style=flat-square)`safeParseJSON`, `safeStringify`, `deepClone` | core | JSON and shallow-copy gotchas |
184
+ | ![planned](https://img.shields.io/badge/-planned-64748b?style=flat-square) `getAccessibilityProps`, `createShadow` | react-native | Accessibility and iOS-only shadows |
185
+ | ![planned](https://img.shields.io/badge/-planned-64748b?style=flat-square) `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
- `react` and `react-native` are optional peer dependencies.
199
+ ---
200
+
201
+ <div align="center">
202
+
203
+ **Built from real bugs, one post at a time.**
204
+
205
+ [![Instagram](https://img.shields.io/badge/Follow-@useless__engineer.dev-E4405F?style=for-the-badge&logo=instagram&logoColor=white)](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, delay) {
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
- }, delay);
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
- export { type Debounced, type ThrottleOptions, type Throttled, debounce, throttle };
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
- export { type Debounced, type ThrottleOptions, type Throttled, debounce, throttle };
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, delay) {
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
- }, delay);
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
- export { debounce, throttle };
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@useless-engineer/utils",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "repo for small, utility functions by useless engineer",
5
5
  "author": "Useless Engineer",
6
6
  "keywords": [