@endgame45/nodescript 1.1.0 → 1.4.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,95 @@
1
+ # nodescript
2
+
3
+ Lightweight Node.js logging, styling and scheduling helpers.
4
+
5
+ Features
6
+ - Styled logging with sensible level colors: nsc.log / nsc.info / nsc.warn / nsc.error / nsc.debug
7
+ - Namespaced instances: nsc.create(namespace, opts)
8
+ - Auto color detection (respects NO_COLOR and TTY)
9
+ - Timestamping (ISO, local, or custom formatter)
10
+ - Color parsing: hex, rgb/rgba, hsl/hsla, CSS keywords (alpha blended)
11
+ - Theme presets and tagged-template styling
12
+ - Flow-control utilities: debounce, throttle, retry/backoff, pLimit
13
+ - Loop helpers with concurrency and scheduling
14
+ - Simple file sink (rotating by size) and writable stream support
15
+ - TypeScript declarations included
16
+
17
+ Installation
18
+
19
+ npm install nodescript
20
+
21
+ Quick start
22
+
23
+ ```javascript
24
+ const nsc = require('nodescript');
25
+
26
+ // basic
27
+ nsc.info('Hello %s', 'world');
28
+ nsc.warn('Low disk space: %d%%', 12);
29
+
30
+ // change global level
31
+ nsc.level = 'debug';
32
+
33
+ // namespaced logger
34
+ const db = nsc.create('db', { textColor: 'cyan', level: 'debug', timestamp: { enabled: true } });
35
+ db.debug('Connected to %s', 'postgres://...');
36
+
37
+ // themes
38
+ nsc.registerTheme('errorTheme', { textColor: '#ff4444', font: 'bold', level: 'error' });
39
+ const err = nsc.useTheme('errorTheme');
40
+ err.error('Fatal: %s', 'something broke');
41
+
42
+ // tagged-template
43
+ const name = 'Alice';
44
+ console.log(nsc.style`Hello, ${name}!`);
45
+ // or log via tag
46
+ nsc.tagLog`Processed ${42} items for ${() => name}`;
47
+
48
+ // fluent style shortcuts
49
+ console.log(nsc.red.bold('Important: %s', 'value'));
50
+ nsc.red.bold.log('Important message');
51
+
52
+ // file sink
53
+ const sink = nsc.logToFile('./logs/app.log', { maxBytes: 5 * 1024 * 1024 });
54
+ nsc.outStream = sink; // attach sink to instance
55
+ nsc.info('This goes to the log file');
56
+
57
+ // utilities
58
+ const deb = nsc.debounce(() => console.log('debounced'), 200);
59
+ deb();
60
+
61
+ const limited = nsc.pLimit(3);
62
+ // concurrency-controlled calls: limited(fn)(...)
63
+
64
+ // loops with concurrency
65
+ await nsc.loop.duration('200ms').call(asyncTask).concurrency(5).times(50);
66
+
67
+ // start/stop controller
68
+ const ctl = nsc.loop.duration('1s').call(task).concurrency(2).start();
69
+ ctl.stop();
70
+ await ctl.done;
71
+ ```
72
+
73
+ API summary
74
+
75
+ - nsc.create(namespace, opts) -> namespaced instance
76
+ - nsc.info/warn/error/debug(...args)
77
+ - nsc.level = 'error'|'warn'|'info'|'debug'
78
+ - nsc.timestamp = { enabled: boolean, format: 'iso'|'local'|fn }
79
+ - nsc.registerTheme(name, opts) / nsc.useTheme(name) / nsc.applyTheme(name)
80
+ - nsc.style`...` and nsc.tagLog`...` (tagged templates)
81
+ - nsc.red/.green/.blue etc. fluent proxies with .bold/.underline chaining
82
+ - Utilities: nsc.debounce, nsc.throttle, nsc.retry, nsc.pLimit
83
+ - nsc.logToFile(path, { maxBytes }) -> sink with write/close
84
+
85
+ TypeScript
86
+
87
+ Types are included at types/nsc.d.ts. Import with:
88
+
89
+ ```ts
90
+ import nsc = require('nodescript');
91
+ ```
92
+
93
+ License
94
+
95
+ MIT — see LICENSE
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@endgame45/nodescript",
3
- "version": "1.1.0",
3
+ "version": "1.4.0",
4
4
  "main": "src/nodescript.js",
5
+ "types": "types/nsc.d.ts",
5
6
  "files": [
6
7
  "src"
7
8
  ],
@@ -29,5 +30,5 @@
29
30
  "url": "https://github.com/horizon-std/nodescript"
30
31
  },
31
32
  "bugs": "https://github.com/horizon-std/nodescript/issues",
32
- "readme": ""
33
+ "readme": "README.md"
33
34
  }
package/src/nodescript.js CHANGED
@@ -1,5 +1,6 @@
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'
@@ -97,6 +98,53 @@ function buildStyler(config) {
97
98
  return styler;
98
99
  }
99
100
 
101
+ // Fluent style proxy helper: returns a callable that produces styled strings
102
+ function makeStyleProxy(baseConfig = {}) {
103
+ const factory = function (...args) {
104
+ // when called, format like util.format and return styled string
105
+ const txt = util.format(...args);
106
+ const styler = buildStyler(baseConfig);
107
+ return styler(txt);
108
+ };
109
+
110
+ const handler = {
111
+ get(_, prop) {
112
+ const p = String(prop);
113
+ // allow access to .log/.info/.warn/.error which will print using the styled text
114
+ if (p === 'log') return (...a) => { console.log(factory(...a)); };
115
+ if (p === 'info') return (...a) => { console.log(factory(...a)); };
116
+ if (p === 'warn') return (...a) => { console.warn(factory(...a)); };
117
+ if (p === 'error') return (...a) => { console.error(factory(...a)); };
118
+ if (SUPPORTED_FONTS.has(p)) {
119
+ // return new proxy with font added
120
+ const next = Object.assign({}, baseConfig);
121
+ next.font = next.font ? (next.font + ' ' + p) : p;
122
+ return makeStyleProxy(next);
123
+ }
124
+ // treat other props as colors (e.g., .red)
125
+ const next = Object.assign({}, baseConfig);
126
+ next.textColor = p;
127
+ return makeStyleProxy(next);
128
+ }
129
+ };
130
+
131
+ return new Proxy(factory, handler);
132
+ }
133
+
134
+ // Create a style object from config that supports .log/.info chaining
135
+ function styleConfig(config = {}) {
136
+ const inst = Object.create(nsc);
137
+ if (config.textColor) inst.textColor = config.textColor;
138
+ if (config.bgColor) inst.bgColor = config.bgColor;
139
+ if (config.font) inst.font = config.font;
140
+ // bind convenience
141
+ inst.log = inst.log.bind(inst);
142
+ inst.info = inst.info.bind(inst);
143
+ inst.warn = inst.warn.bind(inst);
144
+ inst.error = inst.error.bind(inst);
145
+ return inst;
146
+ }
147
+
100
148
  /**
101
149
  * Parse duration input into milliseconds.
102
150
  * Supported inputs:
@@ -161,6 +209,7 @@ class LoopBuilder {
161
209
  this.fn = null;
162
210
  this.fnArgs = [];
163
211
  this._stopped = false;
212
+ this.concurrency = 1;
164
213
  }
165
214
 
166
215
  // set duration (accepts same inputs as parseDuration)
@@ -170,6 +219,12 @@ class LoopBuilder {
170
219
  }
171
220
  every(d) { return this.duration(d); }
172
221
 
222
+ // set concurrency for parallel execution (for times/start)
223
+ concurrency(n) {
224
+ this.concurrency = Math.max(1, Math.floor(Number(n) || 1));
225
+ return this;
226
+ }
227
+
173
228
  // set target function to call; fn can be sync or async
174
229
  call(fn, ...args) {
175
230
  if (typeof fn !== 'function') {
@@ -196,12 +251,41 @@ class LoopBuilder {
196
251
  if (!Number.isFinite(n) || n < 0) throw new TypeError('times(n) expects a non-negative number');
197
252
 
198
253
  this._stopped = false;
199
- for (let i = 0; i < n; i++) {
200
- if (this._stopped) break;
201
- const res = this.fn(...this.fnArgs);
202
- if (res && typeof res.then === 'function') await res;
203
- if (i < n - 1 && this.durationMs > 0) await sleep(this.durationMs);
254
+ if (!this.concurrency || this.concurrency <= 1) {
255
+ for (let i = 0; i < n; i++) {
256
+ if (this._stopped) break;
257
+ const res = this.fn(...this.fnArgs);
258
+ if (res && typeof res.then === 'function') await res;
259
+ if (i < n - 1 && this.durationMs > 0) await sleep(this.durationMs);
260
+ }
261
+ return this;
204
262
  }
263
+
264
+ // concurrency > 1: launch up to concurrency parallel runners
265
+ let launched = 0;
266
+ const errors = [];
267
+
268
+ const runner = async () => {
269
+ while (!this._stopped) {
270
+ const idx = launched++;
271
+ if (idx >= n) break;
272
+ try {
273
+ const res = this.fn(...this.fnArgs);
274
+ if (res && typeof res.then === 'function') await res;
275
+ } catch (err) {
276
+ errors.push(err);
277
+ this._stopped = true;
278
+ break;
279
+ }
280
+ if (this.durationMs > 0 && launched < n) await sleep(this.durationMs);
281
+ }
282
+ };
283
+
284
+ const runners = [];
285
+ const concurrency = Math.min(this.concurrency, n);
286
+ for (let i = 0; i < concurrency; i++) runners.push(runner());
287
+ await Promise.all(runners);
288
+ if (errors.length) throw errors[0];
205
289
  return this;
206
290
  }
207
291
 
@@ -249,9 +333,10 @@ class LoopBuilder {
249
333
  start() {
250
334
  if (!this.fn) throw new Error('No function set. Use .call(fn) before .start()');
251
335
  this._stopped = false;
252
- const done = (async () => {
253
- try {
254
- // run forever until stopped
336
+ const runners = [];
337
+ const concurrency = Math.max(1, this.concurrency || 1);
338
+ for (let i = 0; i < concurrency; i++) {
339
+ const p = (async () => {
255
340
  while (!this._stopped) {
256
341
  const res = this.fn(...this.fnArgs);
257
342
  if (res && typeof res.then === 'function') await res;
@@ -259,14 +344,12 @@ class LoopBuilder {
259
344
  await Promise.race([sleep(this.durationMs), new Promise(r => { this._stopResolve = r; })]);
260
345
  }
261
346
  }
262
- } finally {
263
- // noop
264
- }
265
- })();
266
- return {
267
- stop: () => this.stop(),
268
- done
269
- };
347
+ })();
348
+ runners.push(p);
349
+ }
350
+
351
+ const done = Promise.all(runners).then(() => undefined);
352
+ return { stop: () => this.stop(), done };
270
353
  }
271
354
  }
272
355
 
@@ -362,7 +445,12 @@ const nsc = {
362
445
  });
363
446
  }
364
447
  const out = styler(text);
365
- console.log(out);
448
+ // if an outStream is configured on this instance, write to it; otherwise use console
449
+ if (this.outStream && typeof this.outStream.write === 'function') {
450
+ this.outStream.write(String(out) + (this.outStream._autoNewline === false ? '' : '\n'));
451
+ } else {
452
+ console.log(out);
453
+ }
366
454
  },
367
455
 
368
456
  // low-level logging method used by level helpers
@@ -481,6 +569,62 @@ const nsc = {
481
569
  console.log(s);
482
570
  },
483
571
 
572
+ // file sink: simple rotating file writer
573
+ logToFile(filename, opts = {}) {
574
+ const fs = require('fs');
575
+ const maxBytes = Number(opts.maxBytes || 0);
576
+ // create write stream
577
+ let stream = fs.createWriteStream(filename, { flags: 'a', encoding: 'utf8' });
578
+
579
+ const rotate = () => {
580
+ try {
581
+ stream.end();
582
+ } catch (e) {
583
+ // ignore errors when closing stream
584
+ }
585
+ const backup = filename + '.' + Date.now();
586
+ try {
587
+ fs.renameSync(filename, backup);
588
+ } catch (e) {
589
+ // ignore rotation errors
590
+ }
591
+ stream = fs.createWriteStream(filename, { flags: 'a', encoding: 'utf8' });
592
+ };
593
+
594
+ const writer = (text) => {
595
+ if (maxBytes > 0) {
596
+ try {
597
+ const st = fs.existsSync(filename) ? fs.statSync(filename) : null;
598
+ if (st && st.size >= maxBytes) rotate();
599
+ } catch (e) {
600
+ // ignore stat errors
601
+ }
602
+ }
603
+ stream.write(String(text) + '\n');
604
+ };
605
+
606
+ // return a simple sink object and attach to this instance if desired
607
+ return { write: writer, close: () => stream.end() };
608
+ },
609
+
610
+ // utilities: debounce/throttle/retry/pLimit
611
+ debounce: utils.debounce,
612
+ throttle: utils.throttle,
613
+ retry: utils.retry,
614
+ pLimit: utils.pLimit,
615
+ // Fluent style factory and color shortcuts
616
+ styleConfig,
617
+ // common color shortcuts (red, green, blue, yellow, cyan, magenta, white, gray, black)
618
+ red: makeStyleProxy({ textColor: 'red' }),
619
+ green: makeStyleProxy({ textColor: 'green' }),
620
+ blue: makeStyleProxy({ textColor: 'blue' }),
621
+ yellow: makeStyleProxy({ textColor: 'yellow' }),
622
+ cyan: makeStyleProxy({ textColor: 'cyan' }),
623
+ magenta: makeStyleProxy({ textColor: 'magenta' }),
624
+ white: makeStyleProxy({ textColor: 'white' }),
625
+ gray: makeStyleProxy({ textColor: 'gray' }),
626
+ black: makeStyleProxy({ textColor: 'black' }),
627
+
484
628
  /**
485
629
  * Wait for the given duration and resolve the returned Promise.
486
630
  * - 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
+ };