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