@useless-engineer/utils 0.1.1 → 0.1.3

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
@@ -91,7 +91,7 @@ const padding = isIOS ? 20 : 16;
91
91
  ### Core
92
92
 
93
93
  <details>
94
- <summary><code>debounce(fn, delay)</code> &nbsp; ![live](https://img.shields.io/badge/-live-22c55e?style=flat-square) ![async](https://img.shields.io/badge/-async-6366f1?style=flat-square)</summary>
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
95
 
96
96
  <br>
97
97
 
@@ -117,7 +117,7 @@ save.cancel();
117
117
  </details>
118
118
 
119
119
  <details>
120
- <summary><code>throttle(fn, interval, options?)</code> &nbsp; ![live](https://img.shields.io/badge/-live-22c55e?style=flat-square) ![async](https://img.shields.io/badge/-async-6366f1?style=flat-square)</summary>
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
121
 
122
122
  <br>
123
123
 
@@ -155,10 +155,82 @@ Returns a function with `.cancel()`.
155
155
 
156
156
  </details>
157
157
 
158
+ <details>
159
+ <summary><code>delay(ms, 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>
160
+
161
+ <br>
162
+
163
+ `await`-able sleep that rejects bad input (negative, `NaN`, `Infinity`) instead of silently firing instantly, and can be cancelled.
164
+
165
+ ```ts
166
+ await delay(500);
167
+
168
+ const ctrl = new AbortController();
169
+ delay(5000, { signal: ctrl.signal }).catch(() => {});
170
+ ctrl.abort(); // stops waiting; the promise rejects with the signal's reason
171
+ ```
172
+
173
+ </details>
174
+
175
+ <details>
176
+ <summary><code>withTimeout(promise, ms, message?)</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>
177
+
178
+ <br>
179
+
180
+ Rejects with a `TimeoutError` if the promise takes too long. Clears its timer once the promise settles.
181
+
182
+ ```ts
183
+ try {
184
+ const user = await withTimeout(fetchUser(id), 3000);
185
+ } catch (e) {
186
+ if (e instanceof TimeoutError) showRetry();
187
+ }
188
+ ```
189
+
190
+ > ⚠️ This stops _waiting_, not the work. A timed-out `fetch` keeps running unless you also pass it an `AbortSignal`.
191
+
192
+ </details>
193
+
194
+ <details>
195
+ <summary><code>safeParseJSON(text, fallback?)</code> &nbsp; <img alt="live" src="https://img.shields.io/badge/-live-22c55e?style=flat-square"> <img alt="json" src="https://img.shields.io/badge/-json-f97316?style=flat-square"></summary>
196
+
197
+ <br>
198
+
199
+ `JSON.parse` that never throws. Returns the fallback for invalid JSON or for `null`/`undefined` input (like a missing `AsyncStorage` key).
200
+
201
+ ```ts
202
+ const settings = safeParseJSON(raw, { theme: "light" });
203
+ safeParseJSON("0", 99); // 0, valid falsy values are kept
204
+ safeParseJSON("{oops", 1); // 1
205
+ ```
206
+
207
+ > The type `T` is a promise from you, not a check. Validate untrusted data before relying on its shape.
208
+
209
+ </details>
210
+
211
+ <details>
212
+ <summary><code>safeStringify(value, options?)</code> &nbsp; <img alt="live" src="https://img.shields.io/badge/-live-22c55e?style=flat-square"> <img alt="json" src="https://img.shields.io/badge/-json-f97316?style=flat-square"></summary>
213
+
214
+ <br>
215
+
216
+ `JSON.stringify` that never throws on circular references or `BigInt`.
217
+
218
+ ```ts
219
+ const a = { name: "a" };
220
+ a.self = a;
221
+ safeStringify(a); // '{"name":"a","self":"[Circular]"}'
222
+ safeStringify({ n: 10n }); // '{"n":"10"}'
223
+ safeStringify(data, { space: 2 });
224
+ ```
225
+
226
+ Shared (non-circular) references are kept as they are. Returns `undefined` when there's no JSON form, like `JSON.stringify`.
227
+
228
+ </details>
229
+
158
230
  ### React Native
159
231
 
160
232
  <details>
161
- <summary><code>isIOS</code> / <code>isAndroid</code> &nbsp; ![live](https://img.shields.io/badge/-live-22c55e?style=flat-square) ![react native](https://img.shields.io/badge/-react%20native-61DAFB?style=flat-square&logoColor=black)</summary>
233
+ <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
234
 
163
235
  <br>
164
236
 
@@ -178,12 +250,11 @@ const behavior = isIOS ? "padding" : "height";
178
250
 
179
251
  Released one or two at a time, each matching a post that's already live.
180
252
 
181
- | Next up | Entry | Origin |
182
- | ------------------------------------------------------------------------------------------------------------------------ | ------------ | ----------------------------------------- |
183
- | ![next](https://img.shields.io/badge/-next-f59e0b?style=flat-square) `delay`, `withTimeout` | core | Validated timers, `Promise.race` timeouts |
184
- | ![planned](https://img.shields.io/badge/-planned-64748b?style=flat-square) `safeParseJSON`, `safeStringify`, `deepClone` | core | JSON and shallow-copy gotchas |
185
- | ![planned](https://img.shields.io/badge/-planned-64748b?style=flat-square) `getAccessibilityProps`, `createShadow` | react-native | Accessibility and iOS-only shadows |
186
- | ![planned](https://img.shields.io/badge/-planned-64748b?style=flat-square) `useInterval`, `useCountdown`, `useIsMounted` | react | Stale closures and unmounted updates |
253
+ | Next up | Entry | Origin |
254
+ | ------------------------------------------------------------------------------------------------------------------------ | ------------ | -------------------------------------- |
255
+ | ![next](https://img.shields.io/badge/-next-f59e0b?style=flat-square) `deepClone`, `settle` helpers | core | Shallow-copy and `Promise.all` gotchas |
256
+ | ![planned](https://img.shields.io/badge/-planned-64748b?style=flat-square) `getAccessibilityProps`, `createShadow` | react-native | Accessibility and iOS-only shadows |
257
+ | ![planned](https://img.shields.io/badge/-planned-64748b?style=flat-square) `useInterval`, `useCountdown`, `useIsMounted` | react | Stale closures and unmounted updates |
187
258
 
188
259
  ---
189
260
 
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,92 @@ 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
+ // src/json/safeParseJSON.ts
112
+ function safeParseJSON(text, fallback) {
113
+ if (typeof text !== "string") return fallback;
114
+ try {
115
+ return JSON.parse(text);
116
+ } catch {
117
+ return fallback;
118
+ }
119
+ }
120
+
121
+ // src/json/safeStringify.ts
122
+ function safeStringify(value, { space } = {}) {
123
+ const ancestors = [];
124
+ function replacer(_key, val) {
125
+ if (typeof val === "bigint") return val.toString();
126
+ if (typeof val !== "object" || val === null) return val;
127
+ while (ancestors.length > 0 && ancestors[ancestors.length - 1] !== this) {
128
+ ancestors.pop();
129
+ }
130
+ if (ancestors.includes(val)) return "[Circular]";
131
+ ancestors.push(val);
132
+ return val;
133
+ }
134
+ try {
135
+ return JSON.stringify(value, replacer, space);
136
+ } catch {
137
+ return void 0;
138
+ }
139
+ }
140
+
141
+ exports.TimeoutError = TimeoutError;
59
142
  exports.debounce = debounce;
143
+ exports.delay = delay;
144
+ exports.safeParseJSON = safeParseJSON;
145
+ exports.safeStringify = safeStringify;
60
146
  exports.throttle = throttle;
147
+ exports.withTimeout = withTimeout;
package/dist/index.d.cts CHANGED
@@ -26,4 +26,52 @@ 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
+ /**
53
+ * Parses JSON without throwing. Returns `fallback` (or `undefined`) when the
54
+ * input is not a string or is not valid JSON.
55
+ *
56
+ * The result is NOT validated: `T` is a promise from you, not a check.
57
+ * Validate untrusted data before relying on its shape.
58
+ */
59
+ declare function safeParseJSON<T>(text: string | null | undefined, fallback: T): T;
60
+ declare function safeParseJSON<T = unknown>(text: string | null | undefined): T | undefined;
61
+
62
+ interface SafeStringifyOptions {
63
+ /** Indentation, same as the third argument of JSON.stringify. */
64
+ space?: string | number;
65
+ }
66
+ /**
67
+ * Like JSON.stringify, but never throws:
68
+ * - circular references become "[Circular]" (shared, non-circular references are kept)
69
+ * - BigInt values become strings
70
+ * - anything else that throws (e.g. a failing toJSON) returns `undefined`
71
+ *
72
+ * Like JSON.stringify, returns `undefined` for top-level values with no JSON form
73
+ * (undefined, functions, symbols).
74
+ */
75
+ declare function safeStringify(value: unknown, { space }?: SafeStringifyOptions): string | undefined;
76
+
77
+ export { type Debounced, type DelayOptions, type SafeStringifyOptions, type ThrottleOptions, type Throttled, TimeoutError, debounce, delay, safeParseJSON, safeStringify, throttle, withTimeout };
package/dist/index.d.ts CHANGED
@@ -26,4 +26,52 @@ 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
+ /**
53
+ * Parses JSON without throwing. Returns `fallback` (or `undefined`) when the
54
+ * input is not a string or is not valid JSON.
55
+ *
56
+ * The result is NOT validated: `T` is a promise from you, not a check.
57
+ * Validate untrusted data before relying on its shape.
58
+ */
59
+ declare function safeParseJSON<T>(text: string | null | undefined, fallback: T): T;
60
+ declare function safeParseJSON<T = unknown>(text: string | null | undefined): T | undefined;
61
+
62
+ interface SafeStringifyOptions {
63
+ /** Indentation, same as the third argument of JSON.stringify. */
64
+ space?: string | number;
65
+ }
66
+ /**
67
+ * Like JSON.stringify, but never throws:
68
+ * - circular references become "[Circular]" (shared, non-circular references are kept)
69
+ * - BigInt values become strings
70
+ * - anything else that throws (e.g. a failing toJSON) returns `undefined`
71
+ *
72
+ * Like JSON.stringify, returns `undefined` for top-level values with no JSON form
73
+ * (undefined, functions, symbols).
74
+ */
75
+ declare function safeStringify(value: unknown, { space }?: SafeStringifyOptions): string | undefined;
76
+
77
+ export { type Debounced, type DelayOptions, type SafeStringifyOptions, type ThrottleOptions, type Throttled, TimeoutError, debounce, delay, safeParseJSON, safeStringify, 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,86 @@ 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
+ // src/json/safeParseJSON.ts
110
+ function safeParseJSON(text, fallback) {
111
+ if (typeof text !== "string") return fallback;
112
+ try {
113
+ return JSON.parse(text);
114
+ } catch {
115
+ return fallback;
116
+ }
117
+ }
118
+
119
+ // src/json/safeStringify.ts
120
+ function safeStringify(value, { space } = {}) {
121
+ const ancestors = [];
122
+ function replacer(_key, val) {
123
+ if (typeof val === "bigint") return val.toString();
124
+ if (typeof val !== "object" || val === null) return val;
125
+ while (ancestors.length > 0 && ancestors[ancestors.length - 1] !== this) {
126
+ ancestors.pop();
127
+ }
128
+ if (ancestors.includes(val)) return "[Circular]";
129
+ ancestors.push(val);
130
+ return val;
131
+ }
132
+ try {
133
+ return JSON.stringify(value, replacer, space);
134
+ } catch {
135
+ return void 0;
136
+ }
137
+ }
138
+
139
+ export { TimeoutError, debounce, delay, safeParseJSON, safeStringify, throttle, withTimeout };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@useless-engineer/utils",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "repo for small, utility functions by useless engineer",
5
5
  "author": "Useless Engineer",
6
6
  "keywords": [