@useless-engineer/utils 0.1.2 → 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
@@ -155,6 +155,78 @@ 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>
@@ -178,11 +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)`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 |
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 |
186
258
 
187
259
  ---
188
260
 
package/dist/index.cjs CHANGED
@@ -108,8 +108,40 @@ function withTimeout(promise, ms, message) {
108
108
  });
109
109
  }
110
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
+
111
141
  exports.TimeoutError = TimeoutError;
112
142
  exports.debounce = debounce;
113
143
  exports.delay = delay;
144
+ exports.safeParseJSON = safeParseJSON;
145
+ exports.safeStringify = safeStringify;
114
146
  exports.throttle = throttle;
115
147
  exports.withTimeout = withTimeout;
package/dist/index.d.cts CHANGED
@@ -49,4 +49,29 @@ declare class TimeoutError extends Error {
49
49
  */
50
50
  declare function withTimeout<T>(promise: PromiseLike<T>, ms: number, message?: string): Promise<T>;
51
51
 
52
- export { type Debounced, type DelayOptions, type ThrottleOptions, type Throttled, TimeoutError, debounce, delay, throttle, withTimeout };
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
@@ -49,4 +49,29 @@ declare class TimeoutError extends Error {
49
49
  */
50
50
  declare function withTimeout<T>(promise: PromiseLike<T>, ms: number, message?: string): Promise<T>;
51
51
 
52
- export { type Debounced, type DelayOptions, type ThrottleOptions, type Throttled, TimeoutError, debounce, delay, throttle, withTimeout };
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
@@ -106,4 +106,34 @@ function withTimeout(promise, ms, message) {
106
106
  });
107
107
  }
108
108
 
109
- export { TimeoutError, debounce, delay, throttle, withTimeout };
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.2",
3
+ "version": "0.1.3",
4
4
  "description": "repo for small, utility functions by useless engineer",
5
5
  "author": "Useless Engineer",
6
6
  "keywords": [