@ts-utilities/core 1.0.4 → 1.0.5

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/dist/index.cjs CHANGED
@@ -1 +1,784 @@
1
- const e=e=>!e,t=e=>e==null,n=e=>{if(e==null)return!0;switch(typeof e){case`string`:case`number`:case`bigint`:case`boolean`:case`symbol`:return!0;default:return!1}};function r(e){if(typeof e!=`object`||!e||Object.prototype.toString.call(e)!==`[object Object]`)return!1;let t=Object.getPrototypeOf(e);return t===null||t===Object.prototype}function i(e,...t){let n,i={},a=t[t.length-1];a&&typeof a==`object`&&!Array.isArray(a)&&(a.arrayMerge!==void 0||a.clone!==void 0||a.customMerge!==void 0||a.functionMerge!==void 0||a.maxDepth!==void 0)?(i={...i,...a},n=t.slice(0,-1)):n=t;let{arrayMerge:o=`replace`,clone:s=!0,functionMerge:c=`replace`,maxDepth:l=100,customMerge:u}=i,d=new WeakMap;return f(e,n,0);function f(e,t,n){if(n>=l)return console.warn(`[deepmerge] Maximum depth ${l} exceeded. Returning target as-is.`),e;if(!r(e)&&!Array.isArray(e)){for(let n of t)if(n!==void 0){if(u){let t=u(``,e,n);if(t!==void 0)return t}return typeof e==`function`&&typeof n==`function`&&c===`compose`?(...t)=>{e(...t),n(...t)}:n}return e}let i=s?Array.isArray(e)?[...e]:{...e}:e;for(let e of t)if(e!=null&&!d.has(e))if(d.set(e,i),Array.isArray(i)&&Array.isArray(e))i=p(i,e,o);else if(r(i)&&r(e)){let t=new Set([...Object.keys(i),...Object.keys(e),...Object.getOwnPropertySymbols(i),...Object.getOwnPropertySymbols(e)]);for(let a of t){let t=i[a],s=e[a];u&&u(a,t,s)!==void 0?i[a]=u(a,t,s):typeof t==`function`&&typeof s==`function`?c===`compose`?i[a]=(...e)=>{t(...e),s(...e)}:i[a]=s:r(t)&&r(s)?i[a]=f(t,[s],n+1):Array.isArray(t)&&Array.isArray(s)?i[a]=p(t,s,o):s!==void 0&&(i[a]=s)}}else i=e;return i}function p(e,t,n){if(typeof n==`function`)return n(e,t);switch(n){case`concat`:return[...e,...t];case`merge`:let n=Math.max(e.length,t.length),i=[];for(let a=0;a<n;a++)a<e.length&&a<t.length?r(e[a])&&r(t[a])?i[a]=f(e[a],[t[a]],0):i[a]=t[a]:a<e.length?i[a]=e[a]:i[a]=t[a];return i;case`replace`:default:return[...t]}}}function a(e){return o(e)}function o(e){if(e!==null){if(typeof e!=`object`||!e)return e;if(Array.isArray(e))return e.map(o);if(r(e)){let t={};for(let n in e)t[n]=o(e[n]);return t}return e}}function s(e,t,n){if(typeof t!=`string`&&!Array.isArray(t))return n;let r=(()=>Array.isArray(t)?t:t===``?[]:String(t).split(`.`).filter(e=>e!==``))();if(!Array.isArray(r))return n;let i=e;for(let e of r){if(i==null)return n;let t=typeof e==`string`&&Array.isArray(i)&&/^\d+$/.test(e)?Number.parseInt(e,10):e;i=i[t]}return i===void 0?n:i}function c(e,t){return e==null?e:Object.assign(e,t)}function l(e){return(e.split(`.`).pop()||e).replace(/([a-z])([A-Z])/g,`$1 $2`).split(/[-_|�\s]+/).map(e=>e.charAt(0).toUpperCase()+e.slice(1)).join(` `)}const u=`àáãäâèéëêìíïîòóöôùúüûñç·/_,:;`,d=`aaaaaeeeeiiiioooouuuunc------`,f=e=>{if(typeof e!=`string`)throw TypeError(`Input must be a string`);let t=e.trim().toLowerCase(),n={};for(let e=0;e<29;e++)n[u.charAt(e)]=`aaaaaeeeeiiiioooouuuunc------`.charAt(e);return t=t.replace(RegExp(`[${u}]`,`g`),e=>n[e]||e),t.replace(/[^a-z0-9 -]/g,``).replace(/\s+/g,`-`).replace(/-+/g,`-`).replace(/^-+/,``).replace(/-+$/,``)||``},p=(e=1e3,t)=>new Promise(n=>{if(t?.aborted)return n();let r=setTimeout(()=>{a(),n()},e);function i(){clearTimeout(r),a(),n()}function a(){t?.removeEventListener(`abort`,i)}t&&t.addEventListener(`abort`,i,{once:!0})});function m(e,t=100,n){if(typeof e!=`function`)throw TypeError(`Expected the first parameter to be a function, got \`${typeof e}\`.`);if(t<0)throw RangeError("`wait` must not be negative.");let r=n?.immediate??!1,i,a,o,s;function c(){return s=e.apply(o,a),a=void 0,o=void 0,s}let l=function(...e){return a=e,o=this,i===void 0&&r&&(s=c.call(this)),i!==void 0&&clearTimeout(i),i=setTimeout(c.bind(this),t),s};return Object.defineProperty(l,`isPending`,{get(){return i!==void 0}}),l}function h(e,t=100,n){if(typeof e!=`function`)throw TypeError(`Expected the first parameter to be a function, got \`${typeof e}\`.`);if(t<0)throw RangeError("`wait` must not be negative.");let r=n?.leading??!0,i=n?.trailing??!0,a,o,s,c,l;function u(){c=Date.now(),l=e.apply(s,o),o=void 0,s=void 0}function d(){a=void 0,i&&o&&u()}let f=function(...e){let n=c?Date.now()-c:1/0;return o=e,s=this,n>=t?r?u():a=setTimeout(d,t):!a&&i&&(a=setTimeout(d,t-n)),l};return Object.defineProperty(f,`isPending`,{get(){return a!==void 0}}),f}function g(e,...t){let n=t.length===1&&Array.isArray(t[0])?t[0]:t,r=0;return e.replace(/%s/g,()=>{let e=n[r++];return e===void 0?``:String(e)})}function _(e){return e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`)}function v(e,t={}){if(!e)return``;let{lowercase:n=!0,removeAccents:r=!0,removeNonAlphanumeric:i=!0}=t,a=e.normalize(`NFC`);return r&&(a=a.normalize(`NFD`).replace(/\p{M}/gu,``)),i&&(a=a.replace(/^[^\p{L}\p{N}]*|[^\p{L}\p{N}]*$/gu,``)),n&&(a=a.toLocaleLowerCase()),a}async function y(e,{interval:t=5e3,timeout:n=300*1e3,jitter:r=!0,signal:i}={}){let a=Date.now(),o=i?.aborted??!1,s=()=>{o=!0};i?.addEventListener(`abort`,s,{once:!0});try{for(let s=0;;s++){if(o)throw Error(`Polling aborted`);let s=await e();if(s)return s;if(Date.now()-a>=n)throw Error(`Polling timed out`,{cause:`Polling timed out after ${n}ms`});await p(r?t+(Math.random()-.5)*t*.2:t,i)}}catch(e){throw e}finally{i?.removeEventListener(`abort`,s)}}function b(e,t={}){let{retry:n=0,delay:r=0}=t,i=Date.now(),a=async t=>{try{await e();let t=Date.now()-i;console.log(`⚡[schedule.ts] Completed in ${t}ms`)}catch(e){if(console.log(`⚡[schedule.ts] err:`,e),t>0)console.log(`⚡[schedule.ts] Retrying in ${r}ms...`),setTimeout(()=>a(t-1),r);else{let e=Date.now()-i;console.log(`⚡[schedule.ts] Failed after ${e}ms`)}}};setTimeout(()=>a(n),0)}function x(e){if(e instanceof Promise)return e.then(e=>[null,e]).catch(e=>[e,null]);try{return[null,e()]}catch(t){return console.log(`\x1b[31m🛡 [shield]\x1b[0m ${e.name} failed →`,t),[t,null]}}exports.convertToNormalCase=l,exports.convertToSlug=f,exports.debounce=m,exports.deepmerge=i,exports.escapeRegExp=_,exports.extendProps=c,exports.getObjectValue=s,exports.hydrate=a,exports.isFalsy=e,exports.isNullish=t,exports.isPlainObject=r,exports.isPrimitive=n,exports.normalizeText=v,exports.poll=y,exports.printf=g,exports.schedule=b,exports.shield=x,exports.sleep=p,exports.throttle=h;
1
+
2
+ //#region src/types/guards.ts
3
+ /**
4
+ * Type guard that checks if a value is falsy.
5
+ *
6
+ * @param val - The value to check
7
+ * @returns True if the value is falsy, false otherwise
8
+ *
9
+ * @example
10
+ * if (isFalsy(value)) {
11
+ * console.log('Value is falsy');
12
+ * }
13
+ */
14
+ const isFalsy = (val) => !val;
15
+ /**
16
+ * Type guard that checks if a value is null or undefined.
17
+ *
18
+ * @param val - The value to check
19
+ * @returns True if the value is null or undefined, false otherwise
20
+ *
21
+ * @example
22
+ * if (isNullish(value)) {
23
+ * console.log('Value is null or undefined');
24
+ * }
25
+ */
26
+ const isNullish = (val) => val == null;
27
+ /**
28
+ * Type guard that checks if a value is a primitive type.
29
+ *
30
+ * @param val - The value to check
31
+ * @returns True if the value is a primitive, false otherwise
32
+ *
33
+ * @example
34
+ * if (isPrimitive(value)) {
35
+ * console.log('Value is a primitive type');
36
+ * }
37
+ */
38
+ const isPrimitive = (val) => {
39
+ if (val === null || val === void 0) return true;
40
+ switch (typeof val) {
41
+ case "string":
42
+ case "number":
43
+ case "bigint":
44
+ case "boolean":
45
+ case "symbol": return true;
46
+ default: return false;
47
+ }
48
+ };
49
+ /**
50
+ * Type guard that checks if a value is a plain object (not an array, function, etc.).
51
+ *
52
+ * @param value - The value to check
53
+ * @returns True if the value is a plain object, false otherwise
54
+ *
55
+ * @example
56
+ * if (isPlainObject(value)) {
57
+ * console.log('Value is a plain object');
58
+ * }
59
+ */
60
+ function isPlainObject(value) {
61
+ if (typeof value !== "object" || value === null) return false;
62
+ if (Object.prototype.toString.call(value) !== "[object Object]") return false;
63
+ const proto = Object.getPrototypeOf(value);
64
+ return proto === null || proto === Object.prototype;
65
+ }
66
+
67
+ //#endregion
68
+ //#region src/functions/deepmerge.ts
69
+ function deepmerge(target, ...args) {
70
+ let sources;
71
+ let options = {};
72
+ const lastArg = args[args.length - 1];
73
+ if (lastArg && typeof lastArg === "object" && !Array.isArray(lastArg) && (lastArg.arrayMerge !== void 0 || lastArg.clone !== void 0 || lastArg.customMerge !== void 0 || lastArg.functionMerge !== void 0 || lastArg.maxDepth !== void 0)) {
74
+ options = {
75
+ ...options,
76
+ ...lastArg
77
+ };
78
+ sources = args.slice(0, -1);
79
+ } else sources = args;
80
+ const { arrayMerge = "replace", clone = true, functionMerge = "replace", maxDepth = 100, customMerge } = options;
81
+ const visited = /* @__PURE__ */ new WeakMap();
82
+ return mergeObjects(target, sources, 0);
83
+ function mergeObjects(target$1, sources$1, depth) {
84
+ if (depth >= maxDepth) {
85
+ console.warn(`[deepmerge] Maximum depth ${maxDepth} exceeded. Returning target as-is.`);
86
+ return target$1;
87
+ }
88
+ if (!isPlainObject(target$1) && !Array.isArray(target$1)) {
89
+ for (const source of sources$1) if (source !== void 0) {
90
+ if (customMerge) {
91
+ const merged = customMerge("", target$1, source);
92
+ if (merged !== void 0) return merged;
93
+ }
94
+ if (typeof target$1 === "function" && typeof source === "function") if (functionMerge === "compose") return (...args$1) => {
95
+ target$1(...args$1);
96
+ source(...args$1);
97
+ };
98
+ else return source;
99
+ return source;
100
+ }
101
+ return target$1;
102
+ }
103
+ let result = clone ? Array.isArray(target$1) ? [...target$1] : { ...target$1 } : target$1;
104
+ for (const source of sources$1) {
105
+ if (source == null) continue;
106
+ if (visited.has(source)) continue;
107
+ visited.set(source, result);
108
+ if (Array.isArray(result) && Array.isArray(source)) result = mergeArrays(result, source, arrayMerge);
109
+ else if (isPlainObject(result) && isPlainObject(source)) {
110
+ const keys = new Set([
111
+ ...Object.keys(result),
112
+ ...Object.keys(source),
113
+ ...Object.getOwnPropertySymbols(result),
114
+ ...Object.getOwnPropertySymbols(source)
115
+ ]);
116
+ for (const key of keys) {
117
+ const targetValue = result[key];
118
+ const sourceValue = source[key];
119
+ if (customMerge && customMerge(key, targetValue, sourceValue) !== void 0) result[key] = customMerge(key, targetValue, sourceValue);
120
+ else if (typeof targetValue === "function" && typeof sourceValue === "function") if (functionMerge === "compose") result[key] = (...args$1) => {
121
+ targetValue(...args$1);
122
+ sourceValue(...args$1);
123
+ };
124
+ else result[key] = sourceValue;
125
+ else if (isPlainObject(targetValue) && isPlainObject(sourceValue)) result[key] = mergeObjects(targetValue, [sourceValue], depth + 1);
126
+ else if (Array.isArray(targetValue) && Array.isArray(sourceValue)) result[key] = mergeArrays(targetValue, sourceValue, arrayMerge);
127
+ else if (sourceValue !== void 0) result[key] = sourceValue;
128
+ }
129
+ } else result = source;
130
+ }
131
+ return result;
132
+ }
133
+ function mergeArrays(target$1, source, strategy) {
134
+ if (typeof strategy === "function") return strategy(target$1, source);
135
+ switch (strategy) {
136
+ case "concat": return [...target$1, ...source];
137
+ case "merge":
138
+ const maxLength = Math.max(target$1.length, source.length);
139
+ const merged = [];
140
+ for (let i = 0; i < maxLength; i++) if (i < target$1.length && i < source.length) if (isPlainObject(target$1[i]) && isPlainObject(source[i])) merged[i] = mergeObjects(target$1[i], [source[i]], 0);
141
+ else merged[i] = source[i];
142
+ else if (i < target$1.length) merged[i] = target$1[i];
143
+ else merged[i] = source[i];
144
+ return merged;
145
+ case "replace":
146
+ default: return [...source];
147
+ }
148
+ }
149
+ }
150
+
151
+ //#endregion
152
+ //#region src/functions/hydrate.ts
153
+ /**
154
+ * Converts all `null` values to `undefined` in the data structure recursively.
155
+ *
156
+ * @param data - Any input data (object, array, primitive)
157
+ * @returns Same type as input, but with all nulls replaced by undefined
158
+ *
159
+ * @example
160
+ * ```ts
161
+ * // Basic object hydration
162
+ * hydrate({ name: null, age: 25 }) // { name: undefined, age: 25 }
163
+ *
164
+ * // Nested object hydration
165
+ * hydrate({
166
+ * user: { email: null, profile: { avatar: null } },
167
+ * settings: { theme: 'dark' }
168
+ * })
169
+ * // { user: { email: undefined, profile: { avatar: undefined } }, settings: { theme: 'dark' } }
170
+ *
171
+ * // Array hydration
172
+ * hydrate([null, 'hello', null, 42]) // [undefined, 'hello', undefined, 42]
173
+ *
174
+ * // Mixed data structures
175
+ * hydrate({
176
+ * posts: [null, { title: 'Hello', content: null }],
177
+ * metadata: { published: null, tags: ['react', null] }
178
+ * })
179
+ * ```
180
+ *
181
+ * @example
182
+ * ```ts
183
+ * // API response normalization
184
+ * const apiResponse = await fetch('/api/user');
185
+ * const rawData = await apiResponse.json(); // May contain null values
186
+ * const normalizedData = hydrate(rawData); // Convert nulls to undefined
187
+ *
188
+ * // Database result processing
189
+ * const dbResult = query('SELECT * FROM users'); // Some fields may be NULL
190
+ * const cleanData = hydrate(dbResult); // Normalize for consistent handling
191
+ *
192
+ * // Form data sanitization
193
+ * const formData = getFormValues(); // May have null values from empty fields
194
+ * const sanitizedData = hydrate(formData); // Prepare for validation/state
195
+ * ```
196
+ */
197
+ function hydrate(data) {
198
+ return convertNulls(data);
199
+ }
200
+ function convertNulls(value) {
201
+ if (value === null) return void 0;
202
+ if (typeof value !== "object" || value === null) return value;
203
+ if (Array.isArray(value)) return value.map(convertNulls);
204
+ if (isPlainObject(value)) {
205
+ const result = {};
206
+ for (const key in value) result[key] = convertNulls(value[key]);
207
+ return result;
208
+ }
209
+ return value;
210
+ }
211
+
212
+ //#endregion
213
+ //#region src/functions/object.ts
214
+ /**
215
+ * Core implementation of getObjectValue with runtime type checking.
216
+ *
217
+ * Handles both dot-notation strings and array paths, with support for nested objects and arrays.
218
+ * Performs validation and safe navigation to prevent runtime errors.
219
+ *
220
+ * @param obj - The source object to traverse
221
+ * @param path - Path as string (dot-separated) or array of keys/indices
222
+ * @param defaultValue - Value to return if path doesn't exist
223
+ * @returns The value at the specified path, or defaultValue if not found
224
+ *
225
+ * @example
226
+ * ```ts
227
+ * getObjectValue({ a: { b: 1 } }, 'a.b') // 1
228
+ * getObjectValue({ a: [1, 2] }, ['a', 0]) // 1
229
+ * getObjectValue({}, 'missing.path', 'default') // 'default'
230
+ * ```
231
+ */
232
+ function getObjectValue(obj, path, defaultValue) {
233
+ if (typeof path !== "string" && !Array.isArray(path)) return defaultValue;
234
+ const pathArray = (() => {
235
+ if (Array.isArray(path)) return path;
236
+ if (path === "") return [];
237
+ return String(path).split(".").filter((segment) => segment !== "");
238
+ })();
239
+ if (!Array.isArray(pathArray)) return defaultValue;
240
+ let current = obj;
241
+ for (const key of pathArray) {
242
+ if (current === null || current === void 0) return defaultValue;
243
+ const actualKey = typeof key === "string" && Array.isArray(current) && /^\d+$/.test(key) ? Number.parseInt(key, 10) : key;
244
+ current = current[actualKey];
245
+ }
246
+ return current !== void 0 ? current : defaultValue;
247
+ }
248
+ /**
249
+ * Extend an object or function with additional properties while
250
+ * preserving the original type information.
251
+ *
252
+ * Works with both plain objects and callable functions since
253
+ * functions in JavaScript are objects too. Also handles nullable types.
254
+ *
255
+ * @template T The base object or function type (can be null/undefined)
256
+ * @template P The additional properties type
257
+ *
258
+ * @param base - The object or function to extend (can be null/undefined)
259
+ * @param props - An object containing properties to attach
260
+ *
261
+ * @returns The same object/function, augmented with the given properties, or the original value if null/undefined
262
+ *
263
+ * @example
264
+ * ```ts
265
+ * // Extend a plain object
266
+ * const config = extendProps({ apiUrl: '/api' }, { timeout: 5000 });
267
+ * // config has both apiUrl and timeout properties
268
+ *
269
+ * // Extend a function with metadata
270
+ * const fetchData = (url: string) => fetch(url).then(r => r.json());
271
+ * const enhancedFetch = extendProps(fetchData, {
272
+ * description: 'Data fetching utility',
273
+ * version: '1.0'
274
+ * });
275
+ * // enhancedFetch is callable and has description/version properties
276
+ *
277
+ * // Create plugin system
278
+ * const basePlugin = { name: 'base', enabled: true };
279
+ * const authPlugin = extendProps(basePlugin, {
280
+ * authenticate: (token: string) => validateToken(token)
281
+ * });
282
+ *
283
+ * // Build configuration objects
284
+ * const defaultSettings = { theme: 'light', lang: 'en' };
285
+ * const userSettings = extendProps(defaultSettings, {
286
+ * theme: 'dark',
287
+ * notifications: true
288
+ * });
289
+ *
290
+ * // Handle nullable types (e.g., Supabase Session | null)
291
+ * const session: Session | null = getSession();
292
+ * const extendedSession = extendProps(session, { customProp: 'value' });
293
+ * // extendedSession is (Session & { customProp: string }) | null
294
+ * ```
295
+ */
296
+ function extendProps(base, props) {
297
+ if (base == null) return base;
298
+ return Object.assign(base, props);
299
+ }
300
+
301
+ //#endregion
302
+ //#region src/functions/utils-core.ts
303
+ /**
304
+ * Converts various case styles (camelCase, PascalCase, kebab-case, snake_case) into readable normal case.
305
+ *
306
+ * Transforms technical naming conventions into human-readable titles by:
307
+ * - Adding spaces between words
308
+ * - Capitalizing the first letter of each word
309
+ * - Handling common separators (-, _, camelCase boundaries)
310
+ *
311
+ * @param inputString - The string to convert (supports camelCase, PascalCase, kebab-case, snake_case).
312
+ * @returns The converted string in normal case (title case).
313
+ *
314
+ * @example
315
+ * ```ts
316
+ * convertToNormalCase('camelCase') // 'Camel Case'
317
+ * convertToNormalCase('kebab-case') // 'Kebab Case'
318
+ * convertToNormalCase('snake_case') // 'Snake Case'
319
+ * convertToNormalCase('PascalCase') // 'Pascal Case'
320
+ * ```
321
+ */
322
+ function convertToNormalCase(inputString) {
323
+ return (inputString.split(".").pop() || inputString).replace(/([a-z])([A-Z])/g, "$1 $2").split(/[-_|�\s]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
324
+ }
325
+ const from = "àáãäâèéëêìíïîòóöôùúüûñç·/_,:;";
326
+ const to = "aaaaaeeeeiiiioooouuuunc------";
327
+ /**
328
+ * Converts a string to a URL-friendly slug by trimming, converting to lowercase,
329
+ * replacing diacritics, removing invalid characters, and replacing spaces with hyphens.
330
+ * @param {string} [str] - The input string to convert.
331
+ * @returns {string} The generated slug.
332
+ * @example
333
+ * convertToSlug("Hello World!"); // "hello-world"
334
+ * convertToSlug("Déjà Vu"); // "deja-vu"
335
+ */
336
+ const convertToSlug = (str) => {
337
+ if (typeof str !== "string") throw new TypeError("Input must be a string");
338
+ let slug = str.trim().toLowerCase();
339
+ const charMap = {};
340
+ for (let i = 0; i < 29; i++) charMap[from.charAt(i)] = to.charAt(i);
341
+ slug = slug.replace(new RegExp(`[${from}]`, "g"), (match) => charMap[match] || match);
342
+ return slug.replace(/[^a-z0-9 -]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-+/, "").replace(/-+$/, "") || "";
343
+ };
344
+ /**
345
+ * Pauses execution for the specified time.
346
+ *
347
+ * `signal` allows cancelling the sleep via AbortSignal.
348
+ *
349
+ * @param time - Time in milliseconds to sleep (default is 1000ms)
350
+ * @param signal - Optional AbortSignal to cancel the sleep early
351
+ * @returns - A Promise that resolves after the specified time or when aborted
352
+ */
353
+ const sleep = (time = 1e3, signal) => new Promise((resolve) => {
354
+ if (signal?.aborted) return resolve();
355
+ const id = setTimeout(() => {
356
+ cleanup();
357
+ resolve();
358
+ }, time);
359
+ function onAbort() {
360
+ clearTimeout(id);
361
+ cleanup();
362
+ resolve();
363
+ }
364
+ function cleanup() {
365
+ signal?.removeEventListener("abort", onAbort);
366
+ }
367
+ if (signal) signal.addEventListener("abort", onAbort, { once: true });
368
+ });
369
+ /**
370
+ * Creates a debounced function that delays invoking the provided function until
371
+ * after the specified `wait` time has elapsed since the last invocation.
372
+ *
373
+ * If the `immediate` option is set to `true`, the function will be invoked immediately
374
+ * on the leading edge of the wait interval. Subsequent calls during the wait interval
375
+ * will reset the timer but not invoke the function until the interval elapses again.
376
+ *
377
+ * The returned function includes the `isPending` property to check if the debounce
378
+ * timer is currently active.
379
+ *
380
+ * @typeParam F - The type of the function to debounce.
381
+ *
382
+ * @param function_ - The function to debounce.
383
+ * @param wait - The number of milliseconds to delay (default is 100ms).
384
+ * @param options - An optional object with the following properties:
385
+ * - `immediate` (boolean): If `true`, invokes the function on the leading edge
386
+ * of the wait interval instead of the trailing edge.
387
+ *
388
+ * @returns A debounced version of the provided function, enhanced with the `isPending` property.
389
+ *
390
+ * @throws {TypeError} If the first parameter is not a function.
391
+ * @throws {RangeError} If the `wait` parameter is negative.
392
+ *
393
+ * @example
394
+ * ```ts
395
+ * // Basic debouncing
396
+ * const log = debounce((message: string) => console.log(message), 200);
397
+ * log('Hello'); // Logs "Hello" after 200ms if no other call is made.
398
+ * console.log(log.isPending); // true if the timer is active.
399
+ *
400
+ * // Immediate execution
401
+ * const save = debounce(() => saveToServer(), 500, { immediate: true });
402
+ * save(); // Executes immediately, then waits 500ms for subsequent calls
403
+ *
404
+ * // Check pending state
405
+ * const debouncedSearch = debounce(searchAPI, 300);
406
+ * debouncedSearch('query');
407
+ * if (debouncedSearch.isPending) {
408
+ * showLoadingIndicator();
409
+ * }
410
+ * ```
411
+ */
412
+ function debounce(function_, wait = 100, options) {
413
+ if (typeof function_ !== "function") throw new TypeError(`Expected the first parameter to be a function, got \`${typeof function_}\`.`);
414
+ if (wait < 0) throw new RangeError("`wait` must not be negative.");
415
+ const immediate = options?.immediate ?? false;
416
+ let timeoutId;
417
+ let lastArgs;
418
+ let lastContext;
419
+ let result;
420
+ function run() {
421
+ result = function_.apply(lastContext, lastArgs);
422
+ lastArgs = void 0;
423
+ lastContext = void 0;
424
+ return result;
425
+ }
426
+ const debounced = function(...args) {
427
+ lastArgs = args;
428
+ lastContext = this;
429
+ if (timeoutId === void 0 && immediate) result = run.call(this);
430
+ if (timeoutId !== void 0) clearTimeout(timeoutId);
431
+ timeoutId = setTimeout(run.bind(this), wait);
432
+ return result;
433
+ };
434
+ Object.defineProperty(debounced, "isPending", { get() {
435
+ return timeoutId !== void 0;
436
+ } });
437
+ return debounced;
438
+ }
439
+ /**
440
+ * Creates a throttled function that invokes the provided function at most once
441
+ * every `wait` milliseconds.
442
+ *
443
+ * If the `leading` option is set to `true`, the function will be invoked immediately
444
+ * on the leading edge of the throttle interval. If the `trailing` option is set to `true`,
445
+ * the function will also be invoked at the end of the throttle interval if additional
446
+ * calls were made during the interval.
447
+ *
448
+ * The returned function includes the `isPending` property to check if the throttle
449
+ * timer is currently active.
450
+ *
451
+ * @typeParam F - The type of the function to throttle.
452
+ *
453
+ * @param function_ - The function to throttle.
454
+ * @param wait - The number of milliseconds to wait between invocations (default is 100ms).
455
+ * @param options - An optional object with the following properties:
456
+ * - `leading` (boolean): If `true`, invokes the function on the leading edge of the interval.
457
+ * - `trailing` (boolean): If `true`, invokes the function on the trailing edge of the interval.
458
+ *
459
+ * @returns A throttled version of the provided function, enhanced with the `isPending` property.
460
+ *
461
+ * @throws {TypeError} If the first parameter is not a function.
462
+ * @throws {RangeError} If the `wait` parameter is negative.
463
+ *
464
+ * @example
465
+ * ```ts
466
+ * // Basic throttling (leading edge by default)
467
+ * const log = throttle((message: string) => console.log(message), 200);
468
+ * log('Hello'); // Logs "Hello" immediately
469
+ * log('World'); // Ignored for 200ms
470
+ * console.log(log.isPending); // true if within throttle window
471
+ *
472
+ * // Trailing edge only
473
+ * const trailingLog = throttle(() => console.log('trailing'), 200, {
474
+ * leading: false,
475
+ * trailing: true
476
+ * });
477
+ * trailingLog(); // No immediate execution
478
+ * // After 200ms: logs "trailing"
479
+ *
480
+ * // Both edges
481
+ * const bothLog = throttle(() => console.log('both'), 200, {
482
+ * leading: true,
483
+ * trailing: true
484
+ * });
485
+ * bothLog(); // Immediate execution
486
+ * // After 200ms: executes again if called during window
487
+ * ```
488
+ */
489
+ function throttle(function_, wait = 100, options) {
490
+ if (typeof function_ !== "function") throw new TypeError(`Expected the first parameter to be a function, got \`${typeof function_}\`.`);
491
+ if (wait < 0) throw new RangeError("`wait` must not be negative.");
492
+ const leading = options?.leading ?? true;
493
+ const trailing = options?.trailing ?? true;
494
+ let timeoutId;
495
+ let lastArgs;
496
+ let lastContext;
497
+ let lastCallTime;
498
+ let result;
499
+ function invoke() {
500
+ lastCallTime = Date.now();
501
+ result = function_.apply(lastContext, lastArgs);
502
+ lastArgs = void 0;
503
+ lastContext = void 0;
504
+ }
505
+ function later() {
506
+ timeoutId = void 0;
507
+ if (trailing && lastArgs) invoke();
508
+ }
509
+ const throttled = function(...args) {
510
+ const timeSinceLastCall = lastCallTime ? Date.now() - lastCallTime : Number.POSITIVE_INFINITY;
511
+ lastArgs = args;
512
+ lastContext = this;
513
+ if (timeSinceLastCall >= wait) if (leading) invoke();
514
+ else timeoutId = setTimeout(later, wait);
515
+ else if (!timeoutId && trailing) timeoutId = setTimeout(later, wait - timeSinceLastCall);
516
+ return result;
517
+ };
518
+ Object.defineProperty(throttled, "isPending", { get() {
519
+ return timeoutId !== void 0;
520
+ } });
521
+ return throttled;
522
+ }
523
+ /**
524
+ * Formats a string by replacing each '%s' placeholder with the corresponding argument.
525
+ *
526
+ * Mimics the basic behavior of C's printf for %s substitution. Supports both
527
+ * variadic arguments and array-based argument passing. Extra placeholders
528
+ * are left as-is, missing arguments result in empty strings.
529
+ *
530
+ * @param format - The format string containing '%s' placeholders.
531
+ * @param args - The values to substitute, either as separate arguments or a single array.
532
+ * @returns The formatted string with placeholders replaced by arguments.
533
+ *
534
+ * @example
535
+ * ```ts
536
+ * // Basic usage with separate arguments
537
+ * printf("%s love %s", "I", "Bangladesh") // "I love Bangladesh"
538
+ *
539
+ * // Using array of arguments
540
+ * printf("%s love %s", ["I", "Bangladesh"]) // "I love Bangladesh"
541
+ *
542
+ * // Extra placeholders remain unchanged
543
+ * printf("%s %s %s", "Hello", "World") // "Hello World %s"
544
+ *
545
+ * // Missing arguments become empty strings
546
+ * printf("%s and %s", "this") // "this and "
547
+ *
548
+ * // Multiple occurrences
549
+ * printf("%s %s %s", "repeat", "repeat", "repeat") // "repeat repeat repeat"
550
+ * ```
551
+ */
552
+ function printf(format, ...args) {
553
+ const replacements = args.length === 1 && Array.isArray(args[0]) ? args[0] : args;
554
+ let idx = 0;
555
+ return format.replace(/%s/g, () => {
556
+ const arg = replacements[idx++];
557
+ return arg === void 0 ? "" : String(arg);
558
+ });
559
+ }
560
+ /**
561
+ * Escapes a string for use in a regular expression.
562
+ *
563
+ * @param str - The string to escape
564
+ * @returns - The escaped string safe for use in RegExp constructor
565
+ *
566
+ * @example
567
+ * ```ts
568
+ * const escapedString = escapeRegExp('Hello, world!');
569
+ * // escapedString === 'Hello\\, world!'
570
+ *
571
+ * const regex = new RegExp(escapeRegExp(userInput));
572
+ * ```
573
+ */
574
+ function escapeRegExp(str) {
575
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
576
+ }
577
+ /**
578
+ * Normalizes a string by:
579
+ * - Applying Unicode normalization (NFC)
580
+ * - Optionally removing diacritic marks (accents)
581
+ * - Optionally trimming leading/trailing non-alphanumeric characters
582
+ * - Optionally converting to lowercase
583
+ *
584
+ * @param str - The string to normalize
585
+ * @param options - Normalization options
586
+ * @param options.lowercase - Whether to convert the result to lowercase (default: true)
587
+ * @param options.removeAccents - Whether to remove diacritic marks like accents (default: true)
588
+ * @param options.removeNonAlphanumeric - Whether to trim non-alphanumeric characters from the edges (default: true)
589
+ * @returns The normalized string
590
+ *
591
+ * @example
592
+ * ```ts
593
+ * normalizeText('Café') // 'cafe'
594
+ * normalizeText(' Hello! ') // 'hello'
595
+ * normalizeText('José', { removeAccents: false }) // 'josé'
596
+ * ```
597
+ */
598
+ function normalizeText(str, options = {}) {
599
+ if (!str) return "";
600
+ const { lowercase = true, removeAccents = true, removeNonAlphanumeric = true } = options;
601
+ let result = str.normalize("NFC");
602
+ if (removeAccents) result = result.normalize("NFD").replace(/\p{M}/gu, "");
603
+ if (removeNonAlphanumeric) result = result.replace(/^[^\p{L}\p{N}]*|[^\p{L}\p{N}]*$/gu, "");
604
+ if (lowercase) result = result.toLocaleLowerCase();
605
+ return result;
606
+ }
607
+
608
+ //#endregion
609
+ //#region src/functions/poll.ts
610
+ /**
611
+ * Repeatedly polls an async `cond` function UNTIL it returns a TRUTHY value,
612
+ * or until the operation times out or is aborted.
613
+ *
614
+ * Designed for waiting on async jobs, external state, or delayed availability.
615
+ *
616
+ * @template T The type of the successful result.
617
+ *
618
+ * @param cond
619
+ * A function returning a Promise that resolves to:
620
+ * - a truthy value `T` → stop polling and return it
621
+ * - falsy/null/undefined → continue polling
622
+ *
623
+ * @param options
624
+ * Configuration options:
625
+ * - `interval` (number) — Time between polls in ms (default: 5000 ms)
626
+ * - `timeout` (number) — Max total duration before failing (default: 5 min)
627
+ * - `jitter` (boolean) — Add small random offset (±10%) to intervals to avoid sync bursts (default: true)
628
+ * - `signal` (AbortSignal) — Optional abort signal to cancel polling
629
+ *
630
+ * @returns
631
+ * Resolves with the truthy value `T` when successful.
632
+ * Throws `AbortError` if aborted
633
+ *
634
+ * @example
635
+ * ```ts
636
+ * // Poll for job completion
637
+ * const job = await poll(async () => {
638
+ * const status = await getJobStatus();
639
+ * return status === 'done' ? status : null;
640
+ * }, { interval: 3000, timeout: 60000 });
641
+ * ```
642
+ *
643
+ * @example
644
+ * ```ts
645
+ * // Wait for API endpoint to be ready
646
+ * const apiReady = await poll(async () => {
647
+ * try {
648
+ * await fetch('/api/health');
649
+ * return true;
650
+ * } catch {
651
+ * return null;
652
+ * }
653
+ * }, { interval: 1000, timeout: 30000 });
654
+ * ```
655
+ *
656
+ * @example
657
+ * ```ts
658
+ * // Poll with abort signal for cancellation
659
+ * const controller = new AbortController();
660
+ * setTimeout(() => controller.abort(), 10000); // Cancel after 10s
661
+ *
662
+ * try {
663
+ * const result = await poll(
664
+ * () => checkExternalService(),
665
+ * { interval: 2000, signal: controller.signal }
666
+ * );
667
+ * } catch (err) {
668
+ * if (err.name === 'AbortError') {
669
+ * console.log('Polling was cancelled');
670
+ * }
671
+ * }
672
+ * ```
673
+ *
674
+ * @example
675
+ * ```ts
676
+ * // Poll for user action completion
677
+ * const userConfirmed = await poll(async () => {
678
+ * const confirmations = await getPendingConfirmations();
679
+ * return confirmations.length > 0 ? confirmations[0] : null;
680
+ * }, { interval: 5000, timeout: 300000 }); // 5 min timeout
681
+ * ```
682
+ */
683
+ async function poll(cond, { interval = 5e3, timeout = 300 * 1e3, jitter = true, signal } = {}) {
684
+ const start = Date.now();
685
+ let aborted = signal?.aborted ?? false;
686
+ const abortListener = () => {
687
+ aborted = true;
688
+ };
689
+ signal?.addEventListener("abort", abortListener, { once: true });
690
+ try {
691
+ for (let attempt = 0;; attempt++) {
692
+ if (aborted) throw new Error("Polling aborted");
693
+ const result = await cond();
694
+ if (result) return result;
695
+ if (Date.now() - start >= timeout) throw new Error("Polling timed out", { cause: `Polling timed out after ${timeout}ms` });
696
+ await sleep(jitter ? interval + (Math.random() - .5) * interval * .2 : interval, signal);
697
+ }
698
+ } catch (err) {
699
+ throw err;
700
+ } finally {
701
+ signal?.removeEventListener("abort", abortListener);
702
+ }
703
+ }
704
+
705
+ //#endregion
706
+ //#region src/functions/schedule.ts
707
+ /**
708
+ * Runs a function asynchronously in the background without blocking the main thread.
709
+ *
710
+ * Executes the task immediately using setTimeout, with optional retry logic on failure.
711
+ * Useful for non-critical operations like analytics, logging, or background processing.
712
+ * Logs execution time and retry attempts to the console.
713
+ *
714
+ * @param task - The function to execute asynchronously
715
+ * @param options - Configuration options for retries and timing
716
+ *
717
+ * @example
718
+ * ```ts
719
+ * // Simple background task
720
+ * schedule(() => {
721
+ * console.log('Background work done');
722
+ * });
723
+ *
724
+ * // Task with retry on failure
725
+ * schedule(
726
+ * () => sendAnalytics(),
727
+ * { retry: 3, delay: 1000 }
728
+ * );
729
+ * ```
730
+ */
731
+ function schedule(task, options = {}) {
732
+ const { retry = 0, delay = 0 } = options;
733
+ const start = Date.now();
734
+ const attempt = async (triesLeft) => {
735
+ try {
736
+ await task();
737
+ const total = Date.now() - start;
738
+ console.log(`⚡[schedule.ts] Completed in ${total}ms`);
739
+ } catch (err) {
740
+ console.log("⚡[schedule.ts] err:", err);
741
+ if (triesLeft > 0) {
742
+ console.log(`⚡[schedule.ts] Retrying in ${delay}ms...`);
743
+ setTimeout(() => attempt(triesLeft - 1), delay);
744
+ } else {
745
+ const total = Date.now() - start;
746
+ console.log(`⚡[schedule.ts] Failed after ${total}ms`);
747
+ }
748
+ }
749
+ };
750
+ setTimeout(() => attempt(retry), 0);
751
+ }
752
+
753
+ //#endregion
754
+ //#region src/functions/shield.ts
755
+ function shield(operation) {
756
+ if (operation instanceof Promise) return operation.then((value) => [null, value]).catch((error) => [error, null]);
757
+ try {
758
+ return [null, operation()];
759
+ } catch (error) {
760
+ console.log(`\x1b[31m🛡 [shield]\x1b[0m ${operation.name} failed →`, error);
761
+ return [error, null];
762
+ }
763
+ }
764
+
765
+ //#endregion
766
+ exports.convertToNormalCase = convertToNormalCase;
767
+ exports.convertToSlug = convertToSlug;
768
+ exports.debounce = debounce;
769
+ exports.deepmerge = deepmerge;
770
+ exports.escapeRegExp = escapeRegExp;
771
+ exports.extendProps = extendProps;
772
+ exports.getObjectValue = getObjectValue;
773
+ exports.hydrate = hydrate;
774
+ exports.isFalsy = isFalsy;
775
+ exports.isNullish = isNullish;
776
+ exports.isPlainObject = isPlainObject;
777
+ exports.isPrimitive = isPrimitive;
778
+ exports.normalizeText = normalizeText;
779
+ exports.poll = poll;
780
+ exports.printf = printf;
781
+ exports.schedule = schedule;
782
+ exports.shield = shield;
783
+ exports.sleep = sleep;
784
+ exports.throttle = throttle;