@endgame45/nodescript 1.0.1 → 1.2.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 horizonstudios
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,85 @@
1
+ # nodescript
2
+
3
+ Node.js logging and scheduling utility with easy chalk styling.
4
+
5
+ Features
6
+ - Simple styled logging: nsc.log, nsc.info, nsc.warn, nsc.error, nsc.debug
7
+ - Namespaced loggers: nsc.create(namespace, opts)
8
+ - Auto color detection (respects NO_COLOR and TTY)
9
+ - Timestamping (ISO/local/custom)
10
+ - Color parsing: hex, rgb/rgba, hsl/hsla, CSS keywords (alpha blended)
11
+ - Themes and tagged-template styling
12
+ - Flow control utilities: debounce, throttle, retry/backoff, pLimit
13
+ - Looping helpers with concurrency
14
+
15
+ Installation
16
+
17
+ npm install nodescript
18
+
19
+ Quick examples
20
+
21
+ Basic logging
22
+
23
+ ```javascript
24
+ const nsc = require('nodescript');
25
+ nsc.info('Hello %s', 'world');
26
+ nsc.warn('This is a warning');
27
+ nsc.error('Oops: %s', 'failure');
28
+
29
+ // change global level
30
+ nsc.level = 'debug';
31
+ ```
32
+
33
+ Namespaced logger
34
+
35
+ ```javascript
36
+ const db = nsc.create('db', { textColor: 'cyan', level: 'debug', timestamp: { enabled: true } });
37
+ db.debug('Connected to %s', 'postgres://...');
38
+ ```
39
+
40
+ Themes
41
+
42
+ ```javascript
43
+ nsc.registerTheme('errorTheme', { textColor: '#ff4444', font: 'bold', level: 'error' });
44
+ const err = nsc.useTheme('errorTheme');
45
+ err.error('Fatal: %s', 'something broke');
46
+ ```
47
+
48
+ Tagged template styling
49
+
50
+ ```javascript
51
+ const name = 'Alice';
52
+ console.log(nsc.style`Hello, ${name}!`);
53
+ nsc.tagLog`Processed ${42} items for ${() => name}`;
54
+ ```
55
+
56
+ Looping and concurrency
57
+
58
+ ```javascript
59
+ // run task 50 times with up to 5 concurrent executions
60
+ await nsc.loop.duration('200ms').call(asyncTask).concurrency(5).times(50);
61
+
62
+ // start infinite loop with 2 workers
63
+ const ctl = nsc.loop.duration('1s').call(task).concurrency(2).start();
64
+ // stop
65
+ ctl.stop();
66
+ await ctl.done;
67
+ ```
68
+
69
+ Utilities
70
+
71
+ ```javascript
72
+ const deb = nsc.debounce(() => console.log('debounced'), 200);
73
+ deb();
74
+
75
+ const safe = await nsc.retry(async () => fetchSomething(), { retries: 5 });
76
+ await safe();
77
+
78
+ const limit = nsc.pLimit(3);
79
+ const limited = limit(async (id) => fetch(id));
80
+ await Promise.all(ids.map(id => limited(id)));
81
+ ```
82
+
83
+ License
84
+
85
+ MIT — see LICENSE file.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@endgame45/nodescript",
3
- "version": "1.0.1",
3
+ "version": "1.2.0",
4
4
  "main": "src/nodescript.js",
5
5
  "files": [
6
6
  "src"
@@ -24,8 +24,10 @@
24
24
  "eslint": "^7.32.0"
25
25
  },
26
26
  "url": {
27
- "repository": "npmjs.com/package/@endgame45/nodescript",
27
+ "repository": "https://npmjs.com/package/@endgame45/nodescript",
28
28
  "type": "git",
29
- "url": "github.com/horizon-std/nodescript"
30
- }
29
+ "url": "https://github.com/horizon-std/nodescript"
30
+ },
31
+ "bugs": "https://github.com/horizon-std/nodescript/issues",
32
+ "readme": ""
31
33
  }
package/src/nodescript.js CHANGED
@@ -1,29 +1,80 @@
1
1
  const util = require('util');
2
2
  const chalk = require('chalk');
3
+ const utils = require('./utils');
3
4
 
4
5
  const SUPPORTED_FONTS = new Set([
5
6
  'bold', 'dim', 'italic', 'underline', 'inverse', 'hidden', 'strikethrough'
6
7
  ]);
7
8
 
9
+ function clamp(v, a = 0, b = 255) {
10
+ return Math.min(b, Math.max(a, Math.round(Number(v) || 0)));
11
+ }
12
+
13
+ function hslToRgb(h, s, l) {
14
+ // h: 0-360, s,l: 0-1
15
+ h = ((h % 360) + 360) % 360;
16
+ s = Math.max(0, Math.min(1, s));
17
+ l = Math.max(0, Math.min(1, l));
18
+ const c = (1 - Math.abs(2 * l - 1)) * s;
19
+ const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
20
+ const m = l - c / 2;
21
+ let r1 = 0; let g1 = 0; let b1 = 0;
22
+ if (h < 60) { r1 = c; g1 = x; b1 = 0; }
23
+ else if (h < 120) { r1 = x; g1 = c; b1 = 0; }
24
+ else if (h < 180) { r1 = 0; g1 = c; b1 = x; }
25
+ else if (h < 240) { r1 = 0; g1 = x; b1 = c; }
26
+ else if (h < 300) { r1 = x; g1 = 0; b1 = c; }
27
+ else { r1 = c; g1 = 0; b1 = x; }
28
+ return [ clamp((r1 + m) * 255), clamp((g1 + m) * 255), clamp((b1 + m) * 255) ];
29
+ }
30
+
31
+ function blendOverBg(r, g, b, a, bgR = 0, bgG = 0, bgB = 0) {
32
+ // alpha in 0..1
33
+ const alpha = Math.max(0, Math.min(1, Number(a) || 0));
34
+ const rr = Math.round((1 - alpha) * bgR + alpha * r);
35
+ const gg = Math.round((1 - alpha) * bgG + alpha * g);
36
+ const bb = Math.round((1 - alpha) * bgB + alpha * b);
37
+ return [clamp(rr), clamp(gg), clamp(bb)];
38
+ }
39
+
8
40
  function parseColor(styler, color, isBg = false) {
9
41
  if (!color) return styler;
10
42
  const c = String(color).trim();
43
+ // hex
44
+ if (/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(c)) {
45
+ return isBg ? styler.bgHex(c) : styler.hex(c);
46
+ }
47
+ // rgb() or rgba()
48
+ const rgbMatch = c.match(/^rgba?\s*\(([^)]+)\)$/i);
49
+ if (rgbMatch) {
50
+ const parts = rgbMatch[1].split(',').map(s => s.trim());
51
+ const r = parseInt(parts[0], 10) || 0;
52
+ const g = parseInt(parts[1], 10) || 0;
53
+ const b = parseInt(parts[2], 10) || 0;
54
+ const a = parts[3] !== undefined ? parseFloat(parts[3]) : 1;
55
+ if (a >= 1) return isBg ? styler.bgRgb(r, g, b) : styler.rgb(r, g, b);
56
+ // blend alpha over background if provided via isBg===false and styler has bgColor; we don't know bg, assume black
57
+ const [rr, gg, bb] = blendOverBg(r, g, b, a, 0, 0, 0);
58
+ return isBg ? styler.bgRgb(rr, gg, bb) : styler.rgb(rr, gg, bb);
59
+ }
60
+ // hsl() or hsla()
61
+ const hslMatch = c.match(/^hsla?\s*\(([^)]+)\)$/i);
62
+ if (hslMatch) {
63
+ const parts = hslMatch[1].split(',').map(s => s.trim());
64
+ const h = parseFloat(parts[0]) || 0;
65
+ const s = (String(parts[1]).replace('%', '') || 0) / 100;
66
+ const l = (String(parts[2]).replace('%', '') || 0) / 100;
67
+ const a = parts[3] !== undefined ? parseFloat(parts[3]) : 1;
68
+ const [r, g, b] = hslToRgb(h, s, l);
69
+ if (a >= 1) return isBg ? styler.bgRgb(r, g, b) : styler.rgb(r, g, b);
70
+ const [rr, gg, bb] = blendOverBg(r, g, b, a, 0, 0, 0);
71
+ return isBg ? styler.bgRgb(rr, gg, bb) : styler.rgb(rr, gg, bb);
72
+ }
73
+ // fallback to CSS keyword (named color)
11
74
  try {
12
- if (/^#/.test(c)) {
13
- return isBg ? styler.bgHex(c) : styler.hex(c);
14
- }
15
- if (/^rgb\(/i.test(c)) {
16
- const nums = c
17
- .replace(/^[^(]*\(/, '')
18
- .replace(/\).*/, '')
19
- .split(',')
20
- .map(n => parseInt(n, 10) || 0);
21
- return isBg ? styler.bgRgb(nums[0], nums[1], nums[2]) : styler.rgb(nums[0], nums[1], nums[2]);
22
- }
23
- // fallback to CSS keyword (named color)
24
75
  return isBg ? styler.bgKeyword(c) : styler.keyword(c);
25
76
  } catch (e) {
26
- return styler; // if chalk call fails, return base styler
77
+ return styler;
27
78
  }
28
79
  }
29
80
 
@@ -111,6 +162,7 @@ class LoopBuilder {
111
162
  this.fn = null;
112
163
  this.fnArgs = [];
113
164
  this._stopped = false;
165
+ this.concurrency = 1;
114
166
  }
115
167
 
116
168
  // set duration (accepts same inputs as parseDuration)
@@ -120,6 +172,12 @@ class LoopBuilder {
120
172
  }
121
173
  every(d) { return this.duration(d); }
122
174
 
175
+ // set concurrency for parallel execution (for times/start)
176
+ concurrency(n) {
177
+ this.concurrency = Math.max(1, Math.floor(Number(n) || 1));
178
+ return this;
179
+ }
180
+
123
181
  // set target function to call; fn can be sync or async
124
182
  call(fn, ...args) {
125
183
  if (typeof fn !== 'function') {
@@ -146,12 +204,41 @@ class LoopBuilder {
146
204
  if (!Number.isFinite(n) || n < 0) throw new TypeError('times(n) expects a non-negative number');
147
205
 
148
206
  this._stopped = false;
149
- for (let i = 0; i < n; i++) {
150
- if (this._stopped) break;
151
- const res = this.fn(...this.fnArgs);
152
- if (res && typeof res.then === 'function') await res;
153
- if (i < n - 1 && this.durationMs > 0) await sleep(this.durationMs);
207
+ if (!this.concurrency || this.concurrency <= 1) {
208
+ for (let i = 0; i < n; i++) {
209
+ if (this._stopped) break;
210
+ const res = this.fn(...this.fnArgs);
211
+ if (res && typeof res.then === 'function') await res;
212
+ if (i < n - 1 && this.durationMs > 0) await sleep(this.durationMs);
213
+ }
214
+ return this;
154
215
  }
216
+
217
+ // concurrency > 1: launch up to concurrency parallel runners
218
+ let launched = 0;
219
+ const errors = [];
220
+
221
+ const runner = async () => {
222
+ while (!this._stopped) {
223
+ const idx = launched++;
224
+ if (idx >= n) break;
225
+ try {
226
+ const res = this.fn(...this.fnArgs);
227
+ if (res && typeof res.then === 'function') await res;
228
+ } catch (err) {
229
+ errors.push(err);
230
+ this._stopped = true;
231
+ break;
232
+ }
233
+ if (this.durationMs > 0 && launched < n) await sleep(this.durationMs);
234
+ }
235
+ };
236
+
237
+ const runners = [];
238
+ const concurrency = Math.min(this.concurrency, n);
239
+ for (let i = 0; i < concurrency; i++) runners.push(runner());
240
+ await Promise.all(runners);
241
+ if (errors.length) throw errors[0];
155
242
  return this;
156
243
  }
157
244
 
@@ -199,9 +286,10 @@ class LoopBuilder {
199
286
  start() {
200
287
  if (!this.fn) throw new Error('No function set. Use .call(fn) before .start()');
201
288
  this._stopped = false;
202
- const done = (async () => {
203
- try {
204
- // run forever until stopped
289
+ const runners = [];
290
+ const concurrency = Math.max(1, this.concurrency || 1);
291
+ for (let i = 0; i < concurrency; i++) {
292
+ const p = (async () => {
205
293
  while (!this._stopped) {
206
294
  const res = this.fn(...this.fnArgs);
207
295
  if (res && typeof res.then === 'function') await res;
@@ -209,14 +297,12 @@ class LoopBuilder {
209
297
  await Promise.race([sleep(this.durationMs), new Promise(r => { this._stopResolve = r; })]);
210
298
  }
211
299
  }
212
- } finally {
213
- // noop
214
- }
215
- })();
216
- return {
217
- stop: () => this.stop(),
218
- done
219
- };
300
+ })();
301
+ runners.push(p);
302
+ }
303
+
304
+ const done = Promise.all(runners).then(() => undefined);
305
+ return { stop: () => this.stop(), done };
220
306
  }
221
307
  }
222
308
 
@@ -225,8 +311,35 @@ const nsc = {
225
311
  textColor: null,
226
312
  bgColor: null,
227
313
  font: null,
314
+ namespace: null,
315
+ // logging level: error, warn, info, debug
316
+ level: 'info',
317
+ levels: { error: 0, warn: 1, info: 2, debug: 3 },
318
+ // auto color detection (respects NO_COLOR and TTY)
319
+ autoColor: true,
320
+ // timestamping options: { enabled: false, format: 'iso'|'local'|function }
321
+ timestamp: { enabled: false, format: 'iso' },
322
+
323
+ _colorEnabled() {
324
+ if (!this.autoColor) return false;
325
+ if (process && process.env && process.env.NO_COLOR) return false;
326
+ return Boolean(process && process.stdout && process.stdout.isTTY);
327
+ },
228
328
 
229
329
  _getStyler() {
330
+ if (!this._colorEnabled()) {
331
+ // minimal no-color styler that mimics chalk chaining API
332
+ const noColor = (s) => String(s);
333
+ const fonts = ['bold', 'dim', 'italic', 'underline', 'inverse', 'hidden', 'strikethrough'];
334
+ for (const f of fonts) noColor[f] = noColor;
335
+ noColor.hex = () => noColor;
336
+ noColor.bgHex = () => noColor;
337
+ noColor.rgb = () => noColor;
338
+ noColor.bgRgb = () => noColor;
339
+ noColor.keyword = () => noColor;
340
+ noColor.bgKeyword = () => noColor;
341
+ return noColor;
342
+ }
230
343
  return buildStyler({
231
344
  textColor: this.textColor,
232
345
  bgColor: this.bgColor,
@@ -234,36 +347,81 @@ const nsc = {
234
347
  });
235
348
  },
236
349
 
237
- // nsc.log can be used in two ways:
238
- // 1) nsc.log('a', 'b', 123)
239
- // 2) nsc.log(() => { console.log('inside will be styled') }) <-- temporary override of console.log
240
- log(...args) {
241
- // callback-mode: override console.log while invoking the function
350
+ _formatPrefix(level) {
351
+ const parts = [];
352
+ if (this.timestamp && this.timestamp.enabled) {
353
+ const fmt = this.timestamp.format;
354
+ let ts = '';
355
+ if (typeof fmt === 'function') ts = String(fmt());
356
+ else if (fmt === 'local') ts = new Date().toLocaleString();
357
+ else ts = new Date().toISOString();
358
+ parts.push(ts);
359
+ }
360
+ if (this.namespace) parts.push(this.namespace);
361
+ if (level) parts.push(level.toUpperCase());
362
+ return parts.length ? '[' + parts.join('] [') + '] ' : '';
363
+ },
364
+
365
+ _shouldLog(level) {
366
+ const cur = this.levels[this.level] ?? 2;
367
+ const lvl = this.levels[level] ?? 2;
368
+ return lvl <= cur;
369
+ },
370
+
371
+ _printStyled(text, level) {
372
+ // choose effective color without mutating instance
373
+ let defaultColor;
374
+ switch (level) {
375
+ case 'error': defaultColor = 'red'; break;
376
+ case 'warn': defaultColor = 'yellow'; break;
377
+ case 'info': defaultColor = 'cyan'; break;
378
+ case 'debug': defaultColor = 'magenta'; break;
379
+ default: defaultColor = null; break;
380
+ }
381
+ let styler;
382
+ if (!this._colorEnabled()) {
383
+ const noColor = (s) => String(s);
384
+ const fonts = ['bold', 'dim', 'italic', 'underline', 'inverse', 'hidden', 'strikethrough'];
385
+ for (const f of fonts) noColor[f] = noColor;
386
+ noColor.hex = () => noColor;
387
+ noColor.bgHex = () => noColor;
388
+ noColor.rgb = () => noColor;
389
+ noColor.bgRgb = () => noColor;
390
+ noColor.keyword = () => noColor;
391
+ noColor.bgKeyword = () => noColor;
392
+ styler = noColor;
393
+ } else {
394
+ styler = buildStyler({
395
+ textColor: this.textColor || defaultColor,
396
+ bgColor: this.bgColor,
397
+ font: this.font
398
+ });
399
+ }
400
+ const out = styler(text);
401
+ console.log(out);
402
+ },
403
+
404
+ // low-level logging method used by level helpers
405
+ _log(level, ...args) {
406
+ if (level && !this._shouldLog(level)) return;
407
+ // support callback-mode where a function temporarily overrides console.log
242
408
  if (args.length === 1 && typeof args[0] === 'function') {
243
409
  const fn = args[0];
244
410
  const originalLog = console.log;
411
+ const prefix = this._formatPrefix(level);
245
412
  const styler = this._getStyler();
246
413
 
247
414
  console.log = (...innerArgs) => {
248
415
  const out = util.format(...innerArgs);
249
- originalLog(styler(out));
416
+ originalLog(prefix + styler(out));
250
417
  };
251
418
 
252
419
  try {
253
420
  const result = fn();
254
- // support async callbacks (promises)
255
421
  if (result && typeof result.then === 'function') {
256
- return result
257
- .then(res => {
258
- console.log = originalLog;
259
- return res;
260
- })
261
- .catch(err => {
262
- console.log = originalLog;
263
- throw err;
264
- });
422
+ return result.then((res) => { console.log = originalLog; return res; })
423
+ .catch((err) => { console.log = originalLog; throw err; });
265
424
  }
266
- // sync
267
425
  console.log = originalLog;
268
426
  return result;
269
427
  } catch (err) {
@@ -272,12 +430,99 @@ const nsc = {
272
430
  }
273
431
  }
274
432
 
275
- // normal-mode: format args and print styled
433
+ const prefix = this._formatPrefix(level);
434
+ const message = util.format(...args);
435
+ // combine prefix and message
436
+ const full = prefix + message;
437
+ this._printStyled(full, level);
438
+ },
439
+
440
+ // public API: default log (info-level)
441
+ log(...args) { return this._log('info', ...args); },
442
+
443
+ info(...args) { return this._log('info', ...args); },
444
+ warn(...args) { return this._log('warn', ...args); },
445
+ error(...args) { return this._log('error', ...args); },
446
+ debug(...args) { return this._log('debug', ...args); },
447
+
448
+ // create a namespaced logger instance with optional defaults
449
+ create(namespace, opts = {}) {
450
+ const inst = Object.create(this);
451
+ inst.namespace = namespace || null;
452
+ if (opts.textColor) inst.textColor = opts.textColor;
453
+ if (opts.bgColor) inst.bgColor = opts.bgColor;
454
+ if (opts.font) inst.font = opts.font;
455
+ if (opts.level) inst.level = opts.level;
456
+ if (typeof opts.autoColor === 'boolean') inst.autoColor = opts.autoColor;
457
+ if (opts.timestamp) inst.timestamp = Object.assign({}, inst.timestamp, opts.timestamp);
458
+ // bind methods to instance for convenience
459
+ inst.log = inst.log.bind(inst);
460
+ inst.info = inst.info.bind(inst);
461
+ inst.warn = inst.warn.bind(inst);
462
+ inst.error = inst.error.bind(inst);
463
+ inst.debug = inst.debug.bind(inst);
464
+ inst.create = inst.create.bind(inst);
465
+ return inst;
466
+ },
467
+ // theme registry
468
+ _themes: new Map(),
469
+ registerTheme(name, opts) {
470
+ if (!name || typeof name !== 'string') throw new TypeError('theme name must be a string');
471
+ if (!opts || typeof opts !== 'object') throw new TypeError('theme opts must be an object');
472
+ // store a shallow copy
473
+ this._themes.set(name, Object.assign({}, opts));
474
+ return this;
475
+ },
476
+ getTheme(name) {
477
+ return this._themes.get(name) || null;
478
+ },
479
+ // apply theme to this instance (mutates)
480
+ applyTheme(name) {
481
+ const t = this.getTheme(name);
482
+ if (!t) throw new Error('unknown theme: ' + name);
483
+ if (t.textColor) this.textColor = t.textColor;
484
+ if (t.bgColor) this.bgColor = t.bgColor;
485
+ if (t.font) this.font = t.font;
486
+ if (t.level) this.level = t.level;
487
+ if (t.timestamp) this.timestamp = Object.assign({}, this.timestamp, t.timestamp);
488
+ return this;
489
+ },
490
+ // return a new instance pre-configured with theme
491
+ useTheme(name) {
492
+ const t = this.getTheme(name);
493
+ if (!t) throw new Error('unknown theme: ' + name);
494
+ return this.create(null, t);
495
+ },
496
+
497
+ // tagged template helper: returns styled string (does not log)
498
+ style(strings, ...values) {
499
+ // combine template parts, evaluating functions
500
+ const parts = [];
501
+ for (let i = 0; i < strings.length; i++) {
502
+ parts.push(strings[i]);
503
+ if (i < values.length) {
504
+ const v = values[i];
505
+ if (typeof v === 'function') parts.push(String(v()));
506
+ else parts.push(String(v));
507
+ }
508
+ }
509
+ const full = parts.join('');
276
510
  const styler = this._getStyler();
277
- const out = util.format(...args);
278
- console.log(styler(out));
511
+ return styler(full);
279
512
  },
280
513
 
514
+ // convenience: styled log via template tag
515
+ tagLog(strings, ...values) {
516
+ const s = this.style(strings, ...values);
517
+ console.log(s);
518
+ },
519
+
520
+ // utilities: debounce/throttle/retry/pLimit
521
+ debounce: utils.debounce,
522
+ throttle: utils.throttle,
523
+ retry: utils.retry,
524
+ pLimit: utils.pLimit,
525
+
281
526
  /**
282
527
  * Wait for the given duration and resolve the returned Promise.
283
528
  * - numeric values are treated as seconds
package/src/utils.js ADDED
@@ -0,0 +1,135 @@
1
+ // Utility helpers: debounce, throttle, retry/backoff, pLimit
2
+ 'use strict';
3
+
4
+ function debounce(fn, wait = 0, options = {}) {
5
+ let timer = null;
6
+ let lastArgs = null;
7
+ let lastThis = null;
8
+ let resolveFlush = null;
9
+
10
+ const debounced = function (...args) {
11
+ lastArgs = args;
12
+ lastThis = this;
13
+ if (timer) clearTimeout(timer);
14
+ if (options.leading && !timer) {
15
+ fn.apply(lastThis, lastArgs);
16
+ }
17
+ return new Promise((resolve) => {
18
+ resolveFlush = resolve;
19
+ timer = setTimeout(() => {
20
+ timer = null;
21
+ if (!options.leading) fn.apply(lastThis, lastArgs);
22
+ if (resolveFlush) {
23
+ resolveFlush();
24
+ resolveFlush = null;
25
+ }
26
+ }, wait);
27
+ });
28
+ };
29
+ debounced.cancel = () => {
30
+ if (timer) clearTimeout(timer);
31
+ timer = null;
32
+ lastArgs = null;
33
+ lastThis = null;
34
+ if (resolveFlush) { resolveFlush(); resolveFlush = null; }
35
+ };
36
+ debounced.flush = () => {
37
+ if (timer) {
38
+ clearTimeout(timer);
39
+ timer = null;
40
+ if (!options.leading && lastArgs) fn.apply(lastThis, lastArgs);
41
+ if (resolveFlush) { resolveFlush(); resolveFlush = null; }
42
+ }
43
+ };
44
+ return debounced;
45
+ }
46
+
47
+ function throttle(fn, wait = 0, options = {}) {
48
+ let lastCall = 0;
49
+ let timer = null;
50
+ let lastArgs = null;
51
+ let lastThis = null;
52
+
53
+ const throttled = function (...args) {
54
+ const now = Date.now();
55
+ const remaining = wait - (now - lastCall);
56
+ lastArgs = args;
57
+ lastThis = this;
58
+ if (remaining <= 0 || remaining > wait) {
59
+ if (timer) { clearTimeout(timer); timer = null; }
60
+ lastCall = now;
61
+ fn.apply(lastThis, lastArgs);
62
+ } else if (!timer && options.trailing !== false) {
63
+ timer = setTimeout(() => {
64
+ lastCall = Date.now();
65
+ timer = null;
66
+ fn.apply(lastThis, lastArgs);
67
+ }, remaining);
68
+ }
69
+ };
70
+ throttled.cancel = () => { if (timer) clearTimeout(timer); timer = null; };
71
+ return throttled;
72
+ }
73
+
74
+ async function retry(fn, opts = {}) {
75
+ const retries = Number(opts.retries == null ? 3 : opts.retries);
76
+ const minDelay = Number(opts.minDelay == null ? 100 : opts.minDelay);
77
+ const maxDelay = Number(opts.maxDelay == null ? 1000 : opts.maxDelay);
78
+ const factor = Number(opts.factor == null ? 2 : opts.factor);
79
+ const jitter = opts.jitter !== false;
80
+
81
+ let attempt = 0;
82
+ const exec = async (...args) => {
83
+ let lastErr;
84
+ while (attempt <= retries) {
85
+ try {
86
+ return await fn(...args);
87
+ } catch (err) {
88
+ lastErr = err;
89
+ if (attempt === retries) break;
90
+ attempt += 1;
91
+ let delay = Math.min(maxDelay, minDelay * Math.pow(factor, attempt - 1));
92
+ if (jitter) delay = Math.random() * delay;
93
+ await new Promise(r => setTimeout(r, delay));
94
+ }
95
+ }
96
+ throw lastErr;
97
+ };
98
+ return exec;
99
+ }
100
+
101
+ function pLimit(concurrency = 1) {
102
+ const queue = [];
103
+ let activeCount = 0;
104
+
105
+ const next = () => {
106
+ if (queue.length === 0) return;
107
+ if (activeCount >= concurrency) return;
108
+ activeCount += 1;
109
+ const item = queue.shift();
110
+ const { fn, resolve, reject } = item;
111
+ (async () => {
112
+ try {
113
+ const result = await fn();
114
+ resolve(result);
115
+ } catch (e) {
116
+ reject(e);
117
+ } finally {
118
+ activeCount -= 1;
119
+ next();
120
+ }
121
+ })();
122
+ };
123
+
124
+ return (fn) => (...args) => new Promise((resolve, reject) => {
125
+ queue.push({ fn: () => fn(...args), resolve, reject });
126
+ next();
127
+ });
128
+ }
129
+
130
+ module.exports = {
131
+ debounce,
132
+ throttle,
133
+ retry,
134
+ pLimit
135
+ };