@randajan/pulse 1.1.3 → 2.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 CHANGED
@@ -9,7 +9,7 @@
9
9
 
10
10
  ## Overview
11
11
 
12
- **@randajan/pulse** is a tiny, zero‑dependency scheduler that fires your callback at *wall‑clock‑aligned* intervals. It focuses on precision (no drift), clarity and graceful error handling, so you can drive heart‑beat logic, telemetry or background maintenance tasks with just a few lines of code.
12
+ **@randajan/pulse** is a tiny, zero‑dependency scheduler that fires your callback on aligned or relative intervals. It focuses on precision, clarity and graceful error handling, so you can drive heart‑beat logic, telemetry or background maintenance tasks with just a few lines of code.
13
13
 
14
14
  The package ships dual builds—**ESM** *and* **CommonJS**—and works the same in Node.js and the browser.
15
15
 
@@ -32,10 +32,11 @@ import createPulse from "@randajan/pulse";
32
32
 
33
33
  const pulse = createPulse({
34
34
  interval: 1000, // 1 second
35
- onPulse: meta => {
35
+ align: true, // align to wall-clock time
36
+ onPulse: (pulse, meta) => {
36
37
  console.log("tick", meta.runtime, "ms");
37
38
  },
38
- onError: err => console.error("Pulse error:", err),
39
+ onError: (pulse, meta) => console.error("Pulse error:", meta.error),
39
40
  autoStart: true // start immediately
40
41
  });
41
42
 
@@ -43,12 +44,14 @@ const pulse = createPulse({
43
44
  setTimeout(() => pulse.stop(), 10_000);
44
45
  ```
45
46
 
46
- Every time **onPulse** runs it receives a fresh metadata object:
47
+ Every time **onPulse** runs it receives the pulse instance and a fresh metadata object:
47
48
 
48
49
  ```ts
49
50
  interface PulseMeta {
51
+ readonly id: number;
50
52
  readonly started: Date;
51
53
  readonly runtime: number; // ms since started
54
+ readonly lateBy: number; // ms over interval runtime
52
55
  readonly warnings: string[];
53
56
  warn(w: string | Error): void; // push warning
54
57
  }
@@ -61,17 +64,61 @@ interface PulseMeta {
61
64
  | Option | Type | Required | Default | Description |
62
65
  | ----------- | ---------- | -------- | ------------------ | ------------------------------------------------------------------ |
63
66
  | `interval` | `number` | ✔︎ | — | Period in ms. Min **10 ms**, max **2 147 483 647 ms** (\~24 days). |
64
- | `offset` | `number` |   | `0` | Fixed shift applied to every pulse (0 ≤ offset < interval). |
67
+ | `align` | `boolean` | ✔︎ | — | If `true`, pulse is aligned to wall-clock interval edges. If `false`, pulse runs relatively from start. |
68
+ | `offset` | `number` |   | `0` | Timing offset. Range and meaning depend on `align`; see below. |
69
+ | `maxRuns` | `number` |   | `0` | Maximum number of pulses before automatic stop. `0` means unlimited. |
65
70
  | `getNow` | `function` |   | `() => Date.now()` | Custom clock—handy for deterministic tests or time travel. |
66
71
  | `autoStart` | `boolean` |   | `false` | If `true`, the scheduler starts right after construction. |
67
- | `noMeta` | `boolean` |   | `false` | If `false`, the scheduler creates metadata `PuleMeta` at runtime |
68
- | `onPulse` | `function` | ✔︎ | — | Async/sync callback executed on each pulse. Receives `PulseMeta`. |
69
- | `onError` | `function` |   | `() => {}` | Called when `onPulse` throws or rejects. |
72
+ | `runOnStart` | `boolean` |   | `false` | If `true`, the first pulse is scheduled immediately on start. |
73
+ | `noMeta` | `boolean` |   | `false` | If `true`, callbacks receive the numeric pulse id instead of `PulseMeta`. |
74
+ | `onPulse` | `function` | ✔︎ | — | Async/sync callback executed on each pulse. Receives `(pulse, meta)`. |
75
+ | `onError` | `function` |   | `() => {}` | Called when `onPulse` throws or rejects. Receives `(pulse, meta)`, or `(pulse, error, id)` with `noMeta: true`. |
70
76
  | `onStart` | `function` |   | `() => {}` | Called right before pulse starts. |
71
77
  | `onStop` | `function` |   | `() => {}` | Called right after pulse stops. |
72
- | `afterPulse` | `function` | | `() => {}` | Async/sync callback executed after each pulse even if the onPulse raises an error. Receives `PulseMeta`. |
78
+ | `afterPulse` | `function` | | `() => {}` | Async/sync callback executed after each pulse even if `onPulse` raises an error. Receives `(pulse, meta)`. |
73
79
 
74
80
 
81
+ ### `align` and `offset`
82
+
83
+ With `align: true`, pulses are scheduled on wall-clock interval edges shifted by `offset`. In this mode `offset` must be between `0` and `interval`.
84
+
85
+ ```js
86
+ createPulse({
87
+ align: true,
88
+ interval: 60_000,
89
+ offset: 30_000,
90
+ onPulse: () => console.log("fires around every minute at :30")
91
+ });
92
+ ```
93
+
94
+ With `align: false`, the first pulse is delayed by `interval + offset`; every following pulse uses `interval`. In this mode `offset` can be negative down to `-interval`, or positive as long as `interval + offset` does not exceed the maximum JavaScript timer delay.
95
+
96
+ ```js
97
+ createPulse({
98
+ align: false,
99
+ interval: 5_000,
100
+ offset: -4_000,
101
+ onPulse: () => console.log("first fire after 1s, then every 5s")
102
+ });
103
+ ```
104
+
105
+ In Node.js, Pulse calls `timeout.unref?.()` for its internal timers. A running pulse will not keep an otherwise idle Node process alive.
106
+
107
+ ### `runOnStart`
108
+
109
+ With `runOnStart: true`, the first pulse is scheduled with a `0` ms delay when `start()` is called. The callback still runs asynchronously through the scheduler, not directly inside `start()`.
110
+
111
+ ```js
112
+ createPulse({
113
+ align: false,
114
+ interval: 60_000,
115
+ runOnStart: true,
116
+ onPulse: () => console.log("runs now, then every minute")
117
+ });
118
+ ```
119
+
120
+ `runOnStart` only affects the initial plan after `start()`. After the first pulse completes, the next pulse is scheduled normally: aligned to the wall-clock grid when `align: true`, or after `interval` when `align: false`.
121
+
75
122
  ---
76
123
 
77
124
  ## API
@@ -85,16 +132,19 @@ interface PulseMeta {
85
132
  | `pulse.restart()` | `boolean` | Call pulse.stop() and then pulse.start() |
86
133
  | `pulse.reset()` | `boolean` | Resets nextId counter |
87
134
  | `pulse.state` | `boolean` | `true` = running, `false` = stopped (read-only) |
88
- | `pulse.nextId` | `number` | next pulse id (read-only). |
135
+ | `pulse.align` | `boolean` | Alignment mode (read-only). |
89
136
  | `pulse.interval` | `number` | Interval in ms (read‑only). |
90
137
  | `pulse.offset` | `number` | Offset in ms (read‑only). |
91
- | `pulse.last` | `number` | if noMeta=true there will be last result, else there will be last metadata |
138
+ | `pulse.countdown` | `number\|undefined` | Real ms remaining to the currently scheduled pulse; `undefined` while stopped. |
139
+ | `pulse.potential` | `number` | Real countdown while running, otherwise ms that would be used by the next `start()`; returns `0` when `runOnStart` applies. |
140
+ | `pulse.nextAt` | `number\|undefined` | Timestamp of the next scheduled pulse while running. |
141
+ | `pulse.last` | `any\|PulseMeta` | Last callback result with `noMeta: true`; otherwise last metadata. |
92
142
 
93
143
  ---
94
144
 
95
145
  ## Why not `setInterval`?
96
146
 
97
- `setInterval` drifts: extra work inside the callback pushes the next tick further, eventually desynchronising your schedule. **Pulse** recalculates the next edge *after* each run and keeps the loop tightly aligned to the original wall‑clock grid.
147
+ `setInterval` drifts: extra work inside the callback pushes the next tick further, eventually desynchronising your schedule. With `align: true`, **Pulse** recalculates the next edge *after* each run and keeps the loop tightly aligned to the wall-clock grid. With `align: false`, it runs as a promise-friendly relative scheduler.
98
148
 
99
149
  ---
100
150
 
@@ -106,8 +156,9 @@ JavaScript timers accept at most **2 147 483 647 ms**. To wait longer, ch
106
156
 
107
157
  ```js
108
158
  createPulse({
159
+ align: false,
109
160
  interval: 2_147_483_647,
110
- onPulse: async meta => {
161
+ onPulse: async (pulse, meta) => {
111
162
  if (stillWaiting()) return; // keep sleeping…
112
163
  // …real logic here
113
164
  },
@@ -121,4 +172,3 @@ createPulse({
121
172
  ## License
122
173
 
123
174
  MIT © [randajan](https://github.com/randajan)
124
-
@@ -27,7 +27,7 @@ __export(index_exports, {
27
27
  module.exports = __toCommonJS(index_exports);
28
28
 
29
29
  // src/Pulse.js
30
- var import_props2 = require("@randajan/props");
30
+ var import_props3 = require("@randajan/props");
31
31
 
32
32
  // src/const.js
33
33
  var _pulses = /* @__PURE__ */ new Map();
@@ -62,17 +62,172 @@ var validRange = (min, max, any, req = false, msg = "argument") => {
62
62
  return num;
63
63
  };
64
64
 
65
- // src/metadata.js
65
+ // src/static/options.js
66
+ var formatOptions = (options) => {
67
+ const o = {};
68
+ o.getNow = valid("function", options.getNow, false, "options.getNow") || ((_) => Date.now());
69
+ o.align = valid("boolean", options.align, true, "options.align");
70
+ const minInterval = 10;
71
+ const maxInterval = 2147483647;
72
+ o.interval = validRange(minInterval, maxInterval, options.interval, true, "options.interval");
73
+ o.runOnStart = valid("boolean", options.runOnStart, false, "options.runOnStart") || false;
74
+ const offsetMsg = `options.offset (align:${o.align})`;
75
+ if (o.align) {
76
+ o.offset = validRange(0, o.interval, options.offset, false, offsetMsg) ?? 0;
77
+ } else if (!o.runOnStart) {
78
+ o.offset = validRange(-o.interval, maxInterval - o.interval, options.offset, false, offsetMsg) ?? 0;
79
+ } else if (options.offset != null) {
80
+ throw new Error(`${offsetMsg} can't be set if runOnStart:true`);
81
+ } else {
82
+ o.offset = -o.interval;
83
+ }
84
+ o.maxRuns = validRange(0, Infinity, options.maxRuns, false, "options.maxRuns") ?? 0;
85
+ o.onPulse = valid("function", options.onPulse, true, "options.onPulse");
86
+ o.onStart = valid("function", options.onStart, false, "options.onStart") || blankFn;
87
+ o.onStop = valid("function", options.onStop, false, "options.onStop") || blankFn;
88
+ o.onError = valid("function", options.onError, false, "options.onError") || blankFn;
89
+ o.afterPulse = valid("function", options.afterPulse, false, "options.afterPulse") || blankFn;
90
+ o.autoStart = valid("boolean", options.autoStart, false, "options.autoStart") || false;
91
+ o.noMeta = valid("boolean", options.noMeta, false, "option.noMeta") || false;
92
+ return Object.freeze(o);
93
+ };
94
+
95
+ // src/static/PulsePrivate.js
96
+ var import_props2 = require("@randajan/props");
97
+
98
+ // src/static/metadata.js
66
99
  var import_props = require("@randajan/props");
67
- var createMetaData = (id, getNow) => {
68
- const p = {};
100
+ var createMetaData = (_p, id) => {
101
+ const md = {};
69
102
  const w = [];
70
- (0, import_props.solid)(p, "id", id);
71
- (0, import_props.solid)(p, "started", getNow());
72
- (0, import_props.virtual)(p, "warnings", (_) => [...w]);
73
- (0, import_props.virtual)(p, "runtime", (_) => (p.ended ? p.ended : getNow()) - p.started);
74
- (0, import_props.solid)(p, "warn", (warning) => w.push(warning), false);
75
- return p;
103
+ (0, import_props.solid)(md, "id", id);
104
+ (0, import_props.solid)(md, "started", _p.opt.getNow());
105
+ (0, import_props.virtual)(md, "warnings", (_) => [...w]);
106
+ (0, import_props.virtual)(md, "runtime", (_) => (md.ended ? md.ended : _p.opt.getNow()) - md.started);
107
+ (0, import_props.virtual)(md, "lateBy", (_) => Math.max(0, md.runtime - _p.opt.interval));
108
+ (0, import_props.solid)(md, "warn", (warning) => w.push(warning), false);
109
+ return md;
110
+ };
111
+
112
+ // src/static/PulsePrivate.js
113
+ var PulsePrivate = class {
114
+ constructor(pulse, options) {
115
+ this.pulse = pulse;
116
+ this.state = false;
117
+ this.nextId = 0;
118
+ this.opt = formatOptions(options);
119
+ this.run = this.run.bind(this);
120
+ }
121
+ getCountdown() {
122
+ const { state, fireAt, opt: { getNow } } = this;
123
+ if (!state || fireAt == null) {
124
+ return;
125
+ }
126
+ return Math.max(0, fireAt - getNow());
127
+ }
128
+ getPotential(isInit = true) {
129
+ const { opt: { align, runOnStart, interval, offset, getNow } } = this;
130
+ if (isInit && runOnStart) {
131
+ return 0;
132
+ }
133
+ if (!align) {
134
+ return isInit ? interval + offset : interval;
135
+ }
136
+ const now = getNow() - offset;
137
+ const rest = (now % interval + interval) % interval;
138
+ return rest === 0 ? interval : interval - rest;
139
+ }
140
+ reset() {
141
+ this.nextId = 0;
142
+ return true;
143
+ }
144
+ start(reset = false) {
145
+ const { pulse, state, timeoutId, opt: { onStart } } = this;
146
+ if (state) {
147
+ return false;
148
+ }
149
+ onStart(pulse);
150
+ if (reset) {
151
+ this.reset();
152
+ }
153
+ this.state = true;
154
+ return this.plan(true);
155
+ }
156
+ stop(reset = false) {
157
+ const { pulse, state, timeoutId, opt: { onStop } } = this;
158
+ if (!state) {
159
+ return false;
160
+ }
161
+ clearTimeout(timeoutId);
162
+ this.state = false;
163
+ delete this.fireAt;
164
+ if (reset) {
165
+ this.reset();
166
+ }
167
+ onStop(pulse);
168
+ return true;
169
+ }
170
+ plan(isInit) {
171
+ const { state, metaData, nextId, opt: { maxRuns, getNow } } = this;
172
+ if (!state || metaData != null) {
173
+ return false;
174
+ }
175
+ if (maxRuns && maxRuns <= nextId) {
176
+ this.stop(true);
177
+ return false;
178
+ }
179
+ const fireIn = this.getPotential(isInit);
180
+ this.fireAt = getNow() + fireIn;
181
+ this.timeoutId = setTimeout(this.run, fireIn);
182
+ this.timeoutId.unref?.();
183
+ return true;
184
+ }
185
+ async run() {
186
+ const { pulse, state, metaData, opt: { noMeta, getNow, onPulse, onError, afterPulse } } = this;
187
+ if (!state || metaData) {
188
+ return;
189
+ }
190
+ const id = this.nextId++;
191
+ const md = this.metaData = noMeta ? id : createMetaData(this, id);
192
+ let errA, errB, result;
193
+ try {
194
+ result = await onPulse(pulse, md);
195
+ if (!noMeta) {
196
+ (0, import_props2.solid)(md, "result", result);
197
+ }
198
+ } catch (err) {
199
+ errA = err;
200
+ if (!noMeta) {
201
+ (0, import_props2.solid)(md, "error", errA);
202
+ }
203
+ }
204
+ if (!noMeta) {
205
+ (0, import_props2.solid)(md, "ended", getNow());
206
+ }
207
+ if (errA != null) {
208
+ if (noMeta) {
209
+ onError(pulse, errA, md);
210
+ } else {
211
+ onError(pulse, md);
212
+ }
213
+ }
214
+ try {
215
+ await afterPulse(pulse, md);
216
+ } catch (err) {
217
+ errB = err;
218
+ }
219
+ this.last = noMeta ? result : md;
220
+ delete this.metaData;
221
+ if (errB != null) {
222
+ this.stop();
223
+ throw errB;
224
+ }
225
+ this.plan(false);
226
+ }
227
+ restart(reset) {
228
+ this.stop(reset);
229
+ return this.start(reset);
230
+ }
76
231
  };
77
232
 
78
233
  // src/Pulse.js
@@ -82,79 +237,19 @@ var Pulse = class {
82
237
  * @param {PulseOptions} [options={}] See {@link PulseOptions}.
83
238
  */
84
239
  constructor(options = {}) {
85
- const _p = {
86
- state: false,
87
- nextId: 0
88
- };
89
- const getNow = valid("function", options.getNow, false, "options.getNow") || ((_) => Date.now());
90
- const interval = validRange(10, 2147483647, options.interval, true, "options.interval");
91
- const offset = validRange(0, interval, options.offset, false, "options.offset") || 0;
92
- const onPulse = valid("function", options.onPulse, true, "options.onPulse");
93
- _p.onStart = valid("function", options.onStart, false, "options.onStart") || blankFn;
94
- _p.onStop = valid("function", options.onStop, false, "options.onStop") || blankFn;
95
- const onError = valid("function", options.onError, false, "options.onError") || blankFn;
96
- const afterPulse = valid("function", options.afterPulse, false, "options.afterPulse") || blankFn;
97
- const autoStart = valid("boolean", options.autoStart, false, "options.autoStart") || false;
98
- const noMeta = valid("boolean", options.noMeta, false, "option.noMeta") || false;
99
- _p.reset = (_) => {
100
- _p.nextId = 0;
101
- };
102
- _p.plan = (_) => {
103
- if (!_p.state || _p.metaData != null) {
104
- return;
105
- }
106
- const now = getNow();
107
- const runIn = interval - now % interval + offset;
108
- _p.timeoutId = setTimeout(_p.run, runIn);
109
- };
110
- _p.run = async (_) => {
111
- if (!_p.state || _p.metaData) {
112
- return;
113
- }
114
- const id = _p.nextId++;
115
- const md = _p.metaData = noMeta ? id : createMetaData(id, getNow);
116
- let errA, errB, result;
117
- try {
118
- result = await onPulse(this, md);
119
- if (!noMeta) {
120
- (0, import_props2.solid)(md, "result", result);
121
- }
122
- } catch (err) {
123
- errA = err;
124
- if (!noMeta) {
125
- (0, import_props2.solid)(md, "error", errA);
126
- }
127
- }
128
- if (!noMeta) {
129
- (0, import_props2.solid)(md, "ended", getNow());
130
- }
131
- if (errA != null) {
132
- if (noMeta) {
133
- onError(this, errA, md);
134
- } else {
135
- onError(this, md);
136
- }
137
- }
138
- try {
139
- await afterPulse(this, md);
140
- } catch (err) {
141
- errB = err;
142
- }
143
- _p.last = noMeta ? result : md;
144
- delete _p.metaData;
145
- if (errB != null) {
146
- _p.state = false;
147
- throw errB;
148
- }
149
- _p.plan();
150
- };
151
- (0, import_props2.virtuals)(this, {
240
+ const _p = new PulsePrivate(this, options);
241
+ const { getNow, align, interval, offset, autoStart } = _p.opt;
242
+ (0, import_props3.virtuals)(this, {
152
243
  state: (_) => _p.state,
153
- last: (_) => _p.last
244
+ last: (_) => _p.last,
245
+ countdown: (_) => _p.getCountdown(),
246
+ potential: (_) => _p.getCountdown() ?? _p.getPotential(),
247
+ nextAt: (_) => _p.state ? this.potential + getNow() : void 0
154
248
  });
155
- (0, import_props2.solids)(this, {
249
+ (0, import_props3.solids)(this, {
156
250
  interval,
157
- offset
251
+ offset,
252
+ align
158
253
  });
159
254
  _pulses.set(this, _p);
160
255
  if (autoStart) {
@@ -166,55 +261,29 @@ var Pulse = class {
166
261
  * @returns {boolean} `true` if it started now, `false` if it was already running.
167
262
  */
168
263
  start(reset = false) {
169
- const { state } = this;
170
- if (state) {
171
- return false;
172
- }
173
- const _p = _pulses.get(this);
174
- _p.onStart(this);
175
- if (reset) {
176
- _p.reset();
177
- }
178
- _p.state = true;
179
- _p.plan();
180
- return true;
264
+ return _pulses.get(this).start(reset);
181
265
  }
182
266
  /**
183
267
  * Stop the pulse loop (no-op if already stopped).
184
268
  * @returns {boolean} `true` if it stopped now, `false` if it was already stopped.
185
269
  */
186
270
  stop(reset = false) {
187
- const { state } = this;
188
- if (!state) {
189
- return false;
190
- }
191
- const _p = _pulses.get(this);
192
- clearTimeout(_p.timeoutId);
193
- _p.state = false;
194
- if (reset) {
195
- _p.reset();
196
- }
197
- _p.onStop(this);
198
- return true;
199
- }
200
- reset() {
201
- _pulses.get(this).reset();
202
- return true;
271
+ return _pulses.get(this).stop(reset);
203
272
  }
204
273
  restart(reset = false) {
205
- this.stop(reset);
206
- return this.start(reset);
274
+ return _pulses.get(this).restart(reset);
275
+ }
276
+ reset() {
277
+ return _pulses.get(this).reset();
207
278
  }
208
279
  };
209
280
 
210
- // src/static.js
281
+ // src/index.js
211
282
  var createPulse = (options = {}) => new Pulse(options);
212
283
  var stopAllPulses = () => {
213
284
  for (const [pulse, _pulse] of _pulses.entries()) {
214
285
  pulse.stop();
215
286
  }
216
287
  };
217
-
218
- // src/index.js
219
288
  var index_default = createPulse;
220
289
  //# sourceMappingURL=index.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../../src/index.js", "../../src/Pulse.js", "../../src/const.js", "../../src/tools.js", "../../src/metadata.js", "../../src/static.js"],
4
- "sourcesContent": ["import { Pulse } from \"./Pulse\";\nimport { createPulse, stopAllPulses } from \"./static\";\n\nexport default createPulse;\n\nexport { createPulse, stopAllPulses, Pulse }\n", "import { solid, solids, virtuals } from \"@randajan/props\";\r\nimport { blankFn, _pulses } from \"./const\";\r\nimport { valid, validRange } from \"./tools\";\r\nimport { createMetaData } from \"./metadata\";\r\n\r\n/**\r\n * Periodic scheduler that aligns each callback to exact multiples\r\n * of the given interval (optionally shifted by `offset`).\r\n */\r\nexport class Pulse {\r\n\r\n /**\r\n * Create a new Pulse instance.\r\n * @param {PulseOptions} [options={}] See {@link PulseOptions}.\r\n */\r\n constructor(options = {}) {\r\n const _p = {\r\n state: false,\r\n nextId:0\r\n }\r\n\r\n const getNow = valid(\"function\", options.getNow, false, \"options.getNow\") || (_ => Date.now());\r\n\r\n const interval = validRange(10, 2_147_483_647, options.interval, true, \"options.interval\");\r\n const offset = (validRange(0, interval, options.offset, false, \"options.offset\") || 0);\r\n\r\n const onPulse = valid(\"function\", options.onPulse, true, \"options.onPulse\");\r\n _p.onStart = valid(\"function\", options.onStart, false, \"options.onStart\") || blankFn;\r\n _p.onStop = valid(\"function\", options.onStop, false, \"options.onStop\") || blankFn;\r\n \r\n const onError = valid(\"function\", options.onError, false, \"options.onError\") || blankFn;\r\n \r\n const afterPulse = valid(\"function\", options.afterPulse, false, \"options.afterPulse\") || blankFn;\r\n const autoStart = valid(\"boolean\", options.autoStart, false, \"options.autoStart\") || false;\r\n const noMeta = valid(\"boolean\", options.noMeta, false, \"option.noMeta\") || false;\r\n\r\n _p.reset = _=>{\r\n _p.nextId = 0;\r\n }\r\n\r\n _p.plan = _ => {\r\n if (!_p.state || _p.metaData != null) { return; }\r\n const now = getNow();\r\n const runIn = (interval - now % interval) + offset;\r\n _p.timeoutId = setTimeout(_p.run, runIn);\r\n }\r\n\r\n _p.run = async _ => {\r\n if (!_p.state || _p.metaData) { return; }\r\n\r\n const id = _p.nextId++;\r\n const md = _p.metaData = noMeta ? id : createMetaData(id, getNow);\r\n let errA, errB, result;\r\n\r\n try {\r\n result = await onPulse(this, md);\r\n if (!noMeta) { solid(md, \"result\", result); }\r\n }\r\n catch (err) {\r\n errA = err;\r\n if (!noMeta) { solid(md, \"error\", errA); }\r\n }\r\n\r\n if (!noMeta) { solid(md, \"ended\", getNow()); }\r\n\r\n if (errA != null) {\r\n if (noMeta) { onError(this, errA, md); }\r\n else { onError(this, md); }\r\n }\r\n\r\n try { await afterPulse(this, md); }\r\n catch(err) { errB = err; }\r\n\r\n _p.last = noMeta ? result : md;\r\n delete _p.metaData;\r\n\r\n if (errB != null) {\r\n _p.state = false;\r\n throw errB;\r\n }\r\n \r\n _p.plan();\r\n }\r\n\r\n virtuals(this, {\r\n state:_=>_p.state,\r\n last:_=>_p.last\r\n });\r\n\r\n solids(this, {\r\n interval,\r\n offset\r\n });\r\n\r\n _pulses.set(this, _p);\r\n\r\n if (autoStart) { this.start(); }\r\n }\r\n\r\n /**\r\n * Start the pulse loop (no-op if already running).\r\n * @returns {boolean} `true` if it started now, `false` if it was already running.\r\n */\r\n start(reset=false) {\r\n const { state } = this;\r\n if (state) { return false; }\r\n const _p = _pulses.get(this);\r\n _p.onStart(this);\r\n if (reset) { _p.reset(); }\r\n _p.state = true;\r\n _p.plan();\r\n return true;\r\n }\r\n\r\n /**\r\n * Stop the pulse loop (no-op if already stopped).\r\n * @returns {boolean} `true` if it stopped now, `false` if it was already stopped.\r\n */\r\n stop(reset=false) {\r\n const { state } = this;\r\n if (!state) { return false; }\r\n const _p = _pulses.get(this);\r\n clearTimeout(_p.timeoutId);\r\n _p.state = false;\r\n if (reset) { _p.reset(); }\r\n _p.onStop(this);\r\n return true;\r\n }\r\n\r\n reset() {\r\n _pulses.get(this).reset();\r\n return true;\r\n }\r\n\r\n restart(reset=false) {\r\n this.stop(reset);\r\n return this.start(reset);\r\n }\r\n\r\n\r\n}", "\r\n\r\n\r\nexport const _pulses = new Map();\r\n\r\nexport const blankFn = ()=>{};", "\r\n\r\nexport const is = (type, any)=>typeof any === type;\r\n\r\nexport const valid = (type, any, req=false, msg=\"argument\")=>{\r\n if (any == null) {\r\n if (!req) { return; }\r\n throw new Error(`${msg} require typeof '${type}'`);\r\n }\r\n if (is(type, any)) { return any; }\r\n throw new Error(`${msg} is not typeof '${type}'`);\r\n}\r\n\r\nexport const validRange = (min, max, any, req=false, msg=\"argument\")=>{\r\n const num = valid(\"number\", any, req, msg);\r\n if (num == null) { return; }\r\n if (num < min) { throw new Error(`${msg} must be greater than ${min}`); }\r\n if (num > max) { throw new Error(`${msg} must be less than ${max}`); }\r\n return num;\r\n}", "import { solid, virtual } from \"@randajan/props\";\r\n\r\n\r\n/**\r\n * Metadata object passed to every pulse callback.\r\n * @typedef {Object} PulseMeta\r\n * @property {Date} started Timestamp when this pulse began.\r\n * @property {string[]} warnings Snapshot of all warnings collected so far.\r\n * @property {number} runtime Milliseconds elapsed since `started`.\r\n * @property {(warning: string|Error) => void} warn Push a warning into the internal list.\r\n */\r\nexport const createMetaData = (id, getNow) => {\r\n const p = {};\r\n const w = [];\r\n solid(p, \"id\", id);\r\n solid(p, \"started\", getNow());\r\n virtual(p, \"warnings\", _ => [...w]);\r\n virtual(p, \"runtime\", _ => (p.ended ? p.ended : getNow()) - p.started);\r\n solid(p, \"warn\", warning => w.push(warning), false);\r\n return p;\r\n}\r\n\r\n/**\r\n * Configuration object for {@link Pulse}.\r\n * @typedef {Object} PulseOptions\r\n * @property {(meta: PulseMeta) => (void|Promise<void>)} onPulse **required** \u2013 user callback invoked on every pulse.\r\n * @property {number} interval **required** \u2013 period length in ms (50 \u2013 2 147 483 647).\r\n * @property {number} [offset=0] Shift in ms applied to the start of each interval (0 \u2264 offset < interval).\r\n * @property {() => number} [getNow=() => Date.now()] Custom time source, useful for tests or time travel.\r\n * @property {(err: unknown) => void} [onError=blankFn] Error handler for exceptions or rejected `onPulse` promises.\r\n * @property {boolean} [autoStart=false] If true, the loop starts immediately after construction.\r\n */", "import { _pulses } from \"./const\";\r\nimport { Pulse } from \"./Pulse.js\";\r\n\r\n\r\n/**\r\n * Factory helper \u2013 simply calls `new Pulse(options)`.\r\n * @param {PulseOptions} [options={}] See {@link PulseOptions}.\r\n * @returns {Pulse}\r\n */\r\nexport const createPulse = (options = {}) => new Pulse(options);\r\n\r\n/**\r\n * Process exit helper \u2013 stops all pending pulses.\r\n */\r\n\r\nexport const stopAllPulses = ()=>{\r\n for (const [pulse, _pulse] of _pulses.entries()) {\r\n pulse.stop();\r\n }\r\n}\r\n"],
5
- "mappings": "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",
6
- "names": ["import_props"]
3
+ "sources": ["../../src/index.js", "../../src/Pulse.js", "../../src/const.js", "../../src/tools.js", "../../src/static/options.js", "../../src/static/PulsePrivate.js", "../../src/static/metadata.js"],
4
+ "sourcesContent": ["import { Pulse } from \"./Pulse\";\nimport { _pulses } from \"./const\";\n\n\nexport {\n Pulse\n}\n\n/**\n * Factory helper \u2013 simply calls `new Pulse(options)`.\n * @param {PulseOptions} [options={}] See {@link PulseOptions}.\n * @returns {Pulse}\n */\nexport const createPulse = (options = {}) => new Pulse(options);\n\n\n/**\n * Process exit helper \u2013 stops all pending pulses.\n */\nexport const stopAllPulses = ()=>{\n for (const [pulse, _pulse] of _pulses.entries()) {\n pulse.stop();\n }\n}\n\nexport default createPulse;\n", "import { solid, solids, virtuals } from \"@randajan/props\";\nimport { _pulses } from \"./const\";\nimport { formatOptions } from \"./static/options\";\nimport { PulsePrivate } from \"./static/PulsePrivate\";\n\r\n/**\r\n * Periodic scheduler that aligns each callback to exact multiples\r\n * of the given interval (optionally shifted by `offset`).\r\n */\r\nexport class Pulse {\r\n\r\n /**\r\n * Create a new Pulse instance.\r\n * @param {PulseOptions} [options={}] See {@link PulseOptions}.\r\n */\r\n constructor(options = {}) {\r\n const _p = new PulsePrivate(this, options);\r\n\r\n const { getNow, align, interval, offset, autoStart } = _p.opt;\r\n\r\n virtuals(this, {\r\n state:_=>_p.state,\r\n last:_=>_p.last,\r\n countdown:_=>_p.getCountdown(),\r\n potential:_=>_p.getCountdown() ?? _p.getPotential(),\r\n nextAt:_=>_p.state ? this.potential + getNow() : undefined\r\n });\r\n\r\n solids(this, {\r\n interval,\r\n offset,\r\n align\r\n });\r\n\r\n _pulses.set(this, _p);\r\n\r\n if (autoStart) { this.start(); }\r\n }\r\n\r\n /**\r\n * Start the pulse loop (no-op if already running).\r\n * @returns {boolean} `true` if it started now, `false` if it was already running.\r\n */\r\n start(reset=false) {\r\n return _pulses.get(this).start(reset);\r\n }\r\n\r\n /**\r\n * Stop the pulse loop (no-op if already stopped).\r\n * @returns {boolean} `true` if it stopped now, `false` if it was already stopped.\r\n */\r\n stop(reset=false) {\r\n return _pulses.get(this).stop(reset);\r\n }\r\n\r\n restart(reset=false) {\r\n return _pulses.get(this).restart(reset);\r\n }\r\n\r\n reset() {\r\n return _pulses.get(this).reset();\r\n }\r\n\r\n}\r\n", "\r\n\r\n\r\nexport const _pulses = new Map();\r\n\r\nexport const blankFn = ()=>{};", "\n\nexport const is = (type, any)=>typeof any === type;\n\nexport const valid = (type, any, req=false, msg=\"argument\")=>{\n if (any == null) {\n if (!req) { return; }\n throw new Error(`${msg} require typeof '${type}'`);\n }\n if (is(type, any)) { return any; }\n throw new Error(`${msg} is not typeof '${type}'`);\n}\n\nexport const validRange = (min, max, any, req=false, msg=\"argument\")=>{\n const num = valid(\"number\", any, req, msg);\n if (num == null) { return; }\n if (num < min) { throw new Error(`${msg} must be greater than ${min}`); }\n if (num > max) { throw new Error(`${msg} must be less than ${max}`); }\n return num;\n}", "import { blankFn } from \"../const\";\nimport { valid, validRange } from \"../tools\";\n\n\n\n/**\n * Configuration object for {@link Pulse}.\n * @typedef {Object} PulseOptions\n * @property {(pulse: import(\"../Pulse\").Pulse, meta: import(\"./metadata\").PulseMeta|number) => (*|Promise<*>)} onPulse **required** \u2013 user callback invoked on every pulse.\n * @property {boolean} align **required** \u2013 align to wall-clock interval edges when true, run relatively when false.\n * @property {number} interval **required** \u2013 period length in ms (10 \u2013 2 147 483 647).\n * @property {number} [offset=0] Timing offset. Range and meaning depend on `align` and `runOnStart`.\n * @property {boolean} [runOnStart=false] Schedule the first pulse immediately on start.\n * @property {number} [maxRuns=0] Maximum number of pulses before automatic stop. `0` means unlimited.\n * @property {() => number} [getNow=() => Date.now()] Custom time source, useful for tests or time travel.\n * @property {boolean} [autoStart=false] If true, the loop starts immediately after construction.\n * @property {boolean} [noMeta=false] If true, callbacks receive the numeric pulse id instead of `PulseMeta`.\n * @property {(pulse: import(\"../Pulse\").Pulse) => void} [onStart=blankFn] Called right before the pulse loop starts.\n * @property {(pulse: import(\"../Pulse\").Pulse) => void} [onStop=blankFn] Called right after the pulse loop stops.\n * @property {Function} [onError=blankFn] Error handler for exceptions or rejected `onPulse` promises.\n * @property {(pulse: import(\"../Pulse\").Pulse, meta: import(\"./metadata\").PulseMeta|number) => (void|Promise<void>)} [afterPulse=blankFn] Callback executed after every pulse.\n */\nexport const formatOptions = (options) => {\n const o = {};\r\n\r\n o.getNow = valid(\"function\", options.getNow, false, \"options.getNow\") || (_ => Date.now());\r\n\r\n o.align = valid(\"boolean\", options.align, true, \"options.align\");\r\n\r\n const minInterval = 10;\r\n const maxInterval = 2_147_483_647;\r\n o.interval = validRange(minInterval, maxInterval, options.interval, true, \"options.interval\");\r\n\r\n o.runOnStart = valid(\"boolean\", options.runOnStart, false, \"options.runOnStart\") || false;\r\n\r\n const offsetMsg = `options.offset (align:${o.align})`;\r\n\r\n if (o.align) {\r\n o.offset = (validRange(0, o.interval, options.offset, false, offsetMsg) ?? 0);\r\n } else if (!o.runOnStart) {\r\n o.offset = (validRange(-o.interval, (maxInterval - o.interval), options.offset, false, offsetMsg) ?? 0);\r\n } else if (options.offset != null) {\n throw new Error(`${offsetMsg} can't be set if runOnStart:true`);\n } else {\n o.offset = -o.interval;\n }\r\n\r\n o.maxRuns = validRange(0, Infinity, options.maxRuns, false, \"options.maxRuns\") ?? 0;\r\n\r\n o.onPulse = valid(\"function\", options.onPulse, true, \"options.onPulse\");\r\n o.onStart = valid(\"function\", options.onStart, false, \"options.onStart\") || blankFn;\r\n o.onStop = valid(\"function\", options.onStop, false, \"options.onStop\") || blankFn;\r\n o.onError = valid(\"function\", options.onError, false, \"options.onError\") || blankFn;\r\n\r\n o.afterPulse = valid(\"function\", options.afterPulse, false, \"options.afterPulse\") || blankFn;\r\n o.autoStart = valid(\"boolean\", options.autoStart, false, \"options.autoStart\") || false;\r\n o.noMeta = valid(\"boolean\", options.noMeta, false, \"option.noMeta\") || false;\r\n\r\n return Object.freeze(o);\r\n}\n", "import { solid } from \"@randajan/props\";\r\nimport { createMetaData } from \"./metadata\";\r\nimport { formatOptions } from \"./options\";\r\n\r\n\r\nexport class PulsePrivate {\r\n\r\n constructor(pulse, options) {\r\n this.pulse = pulse;\r\n this.state = false;\r\n this.nextId = 0;\r\n this.opt = formatOptions(options);\r\n\r\n this.run = this.run.bind(this);\r\n }\r\n\r\n getCountdown() {\r\n const { state, fireAt, opt:{ getNow } } = this;\r\n if (!state || fireAt == null) { return; }\r\n return Math.max(0, fireAt - getNow());\r\n }\r\n\r\n getPotential(isInit = true) {\r\n const { opt:{ align, runOnStart, interval, offset, getNow } } = this;\r\n if (isInit && runOnStart) { return 0; }\r\n if (!align) { return isInit ? (interval + offset) : interval; }\r\n const now = getNow() - offset;\r\n const rest = ((now % interval) + interval) % interval;\r\n return rest === 0 ? interval : interval - rest;\r\n }\r\n\r\n reset() {\r\n this.nextId = 0;\r\n return true;\r\n }\r\n\r\n start(reset=false) {\r\n const { pulse, state, timeoutId, opt:{ onStart } } = this;\r\n if (state) { return false; }\r\n onStart(pulse);\r\n if (reset) { this.reset(); }\r\n this.state = true;\r\n return this.plan(true);\r\n }\r\n\r\n stop(reset=false) {\r\n const { pulse, state, timeoutId, opt:{ onStop } } = this;\r\n if (!state) { return false; }\r\n clearTimeout(timeoutId);\r\n this.state = false;\r\n delete this.fireAt;\r\n if (reset) { this.reset(); }\r\n onStop(pulse);\r\n return true;\r\n }\r\n\r\n plan(isInit) {\r\n const { state, metaData, nextId, opt:{ maxRuns, getNow } } = this;\r\n if (!state || metaData != null) { return false; }\r\n if (maxRuns && maxRuns <= nextId) { this.stop(true); return false; }\r\n const fireIn = this.getPotential(isInit);\r\n this.fireAt = getNow() + fireIn;\r\n this.timeoutId = setTimeout(this.run, fireIn);\r\n this.timeoutId.unref?.();\r\n return true;\r\n }\r\n\r\n async run() {\r\n const { pulse, state, metaData, opt:{ noMeta, getNow, onPulse, onError, afterPulse } } = this;\r\n if (!state || metaData) { return; }\r\n\r\n const id = this.nextId++;\r\n const md = this.metaData = noMeta ? id : createMetaData(this, id);\n let errA, errB, result;\r\n\r\n try {\r\n result = await onPulse(pulse, md);\r\n if (!noMeta) { solid(md, \"result\", result); }\r\n }\r\n catch (err) {\r\n errA = err;\r\n if (!noMeta) { solid(md, \"error\", errA); }\r\n }\r\n\r\n if (!noMeta) { solid(md, \"ended\", getNow()); }\r\n\r\n if (errA != null) {\r\n if (noMeta) { onError(pulse, errA, md); }\r\n else { onError(pulse, md); }\r\n }\r\n\r\n try { await afterPulse(pulse, md); }\r\n catch (err) { errB = err; }\r\n\r\n this.last = noMeta ? result : md;\r\n delete this.metaData;\r\n\r\n if (errB != null) {\r\n this.stop();\r\n throw errB;\r\n }\r\n\r\n this.plan(false);\r\n }\r\n\r\n restart(reset) {\r\n this.stop(reset);\r\n return this.start(reset);\r\n }\r\n\r\n}\n", "import { solid, virtual } from \"@randajan/props\";\r\n\r\n\r\n/**\n * Metadata object passed to every pulse callback.\n * @typedef {Object} PulseMeta\n * @property {number} id Sequential pulse id.\n * @property {number} started Timestamp when this pulse began.\n * @property {number} [ended] Timestamp when this pulse finished.\n * @property {number} runtime Milliseconds elapsed since `started`.\n * @property {number} lateBy Milliseconds over the configured interval.\n * @property {string[]|Error[]} warnings Snapshot of all warnings collected so far.\n * @property {*} [result] Value returned by `onPulse`.\n * @property {*} [error] Error thrown or rejected by `onPulse`.\n * @property {(warning: string|Error) => void} warn Push a warning into the internal list.\n */\nexport const createMetaData = (_p, id) => {\n const md = {};\n const w = [];\n solid(md, \"id\", id);\n solid(md, \"started\", _p.opt.getNow());\r\n virtual(md, \"warnings\", _ => [...w]);\r\n virtual(md, \"runtime\", _ => (md.ended ? md.ended : _p.opt.getNow()) - md.started);\r\n virtual(md, \"lateBy\", _=>Math.max(0, md.runtime-_p.opt.interval));\r\n solid(md, \"warn\", warning => w.push(warning), false);\r\n return md;\r\n}\r\n\r\n"],
5
+ "mappings": "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",
6
+ "names": ["import_props", "import_props"]
7
7
  }
@@ -1,5 +1,5 @@
1
1
  // src/Pulse.js
2
- import { solid as solid2, solids, virtuals } from "@randajan/props";
2
+ import { solid as solid3, solids, virtuals } from "@randajan/props";
3
3
 
4
4
  // src/const.js
5
5
  var _pulses = /* @__PURE__ */ new Map();
@@ -34,17 +34,172 @@ var validRange = (min, max, any, req = false, msg = "argument") => {
34
34
  return num;
35
35
  };
36
36
 
37
- // src/metadata.js
37
+ // src/static/options.js
38
+ var formatOptions = (options) => {
39
+ const o = {};
40
+ o.getNow = valid("function", options.getNow, false, "options.getNow") || ((_) => Date.now());
41
+ o.align = valid("boolean", options.align, true, "options.align");
42
+ const minInterval = 10;
43
+ const maxInterval = 2147483647;
44
+ o.interval = validRange(minInterval, maxInterval, options.interval, true, "options.interval");
45
+ o.runOnStart = valid("boolean", options.runOnStart, false, "options.runOnStart") || false;
46
+ const offsetMsg = `options.offset (align:${o.align})`;
47
+ if (o.align) {
48
+ o.offset = validRange(0, o.interval, options.offset, false, offsetMsg) ?? 0;
49
+ } else if (!o.runOnStart) {
50
+ o.offset = validRange(-o.interval, maxInterval - o.interval, options.offset, false, offsetMsg) ?? 0;
51
+ } else if (options.offset != null) {
52
+ throw new Error(`${offsetMsg} can't be set if runOnStart:true`);
53
+ } else {
54
+ o.offset = -o.interval;
55
+ }
56
+ o.maxRuns = validRange(0, Infinity, options.maxRuns, false, "options.maxRuns") ?? 0;
57
+ o.onPulse = valid("function", options.onPulse, true, "options.onPulse");
58
+ o.onStart = valid("function", options.onStart, false, "options.onStart") || blankFn;
59
+ o.onStop = valid("function", options.onStop, false, "options.onStop") || blankFn;
60
+ o.onError = valid("function", options.onError, false, "options.onError") || blankFn;
61
+ o.afterPulse = valid("function", options.afterPulse, false, "options.afterPulse") || blankFn;
62
+ o.autoStart = valid("boolean", options.autoStart, false, "options.autoStart") || false;
63
+ o.noMeta = valid("boolean", options.noMeta, false, "option.noMeta") || false;
64
+ return Object.freeze(o);
65
+ };
66
+
67
+ // src/static/PulsePrivate.js
68
+ import { solid as solid2 } from "@randajan/props";
69
+
70
+ // src/static/metadata.js
38
71
  import { solid, virtual } from "@randajan/props";
39
- var createMetaData = (id, getNow) => {
40
- const p = {};
72
+ var createMetaData = (_p, id) => {
73
+ const md = {};
41
74
  const w = [];
42
- solid(p, "id", id);
43
- solid(p, "started", getNow());
44
- virtual(p, "warnings", (_) => [...w]);
45
- virtual(p, "runtime", (_) => (p.ended ? p.ended : getNow()) - p.started);
46
- solid(p, "warn", (warning) => w.push(warning), false);
47
- return p;
75
+ solid(md, "id", id);
76
+ solid(md, "started", _p.opt.getNow());
77
+ virtual(md, "warnings", (_) => [...w]);
78
+ virtual(md, "runtime", (_) => (md.ended ? md.ended : _p.opt.getNow()) - md.started);
79
+ virtual(md, "lateBy", (_) => Math.max(0, md.runtime - _p.opt.interval));
80
+ solid(md, "warn", (warning) => w.push(warning), false);
81
+ return md;
82
+ };
83
+
84
+ // src/static/PulsePrivate.js
85
+ var PulsePrivate = class {
86
+ constructor(pulse, options) {
87
+ this.pulse = pulse;
88
+ this.state = false;
89
+ this.nextId = 0;
90
+ this.opt = formatOptions(options);
91
+ this.run = this.run.bind(this);
92
+ }
93
+ getCountdown() {
94
+ const { state, fireAt, opt: { getNow } } = this;
95
+ if (!state || fireAt == null) {
96
+ return;
97
+ }
98
+ return Math.max(0, fireAt - getNow());
99
+ }
100
+ getPotential(isInit = true) {
101
+ const { opt: { align, runOnStart, interval, offset, getNow } } = this;
102
+ if (isInit && runOnStart) {
103
+ return 0;
104
+ }
105
+ if (!align) {
106
+ return isInit ? interval + offset : interval;
107
+ }
108
+ const now = getNow() - offset;
109
+ const rest = (now % interval + interval) % interval;
110
+ return rest === 0 ? interval : interval - rest;
111
+ }
112
+ reset() {
113
+ this.nextId = 0;
114
+ return true;
115
+ }
116
+ start(reset = false) {
117
+ const { pulse, state, timeoutId, opt: { onStart } } = this;
118
+ if (state) {
119
+ return false;
120
+ }
121
+ onStart(pulse);
122
+ if (reset) {
123
+ this.reset();
124
+ }
125
+ this.state = true;
126
+ return this.plan(true);
127
+ }
128
+ stop(reset = false) {
129
+ const { pulse, state, timeoutId, opt: { onStop } } = this;
130
+ if (!state) {
131
+ return false;
132
+ }
133
+ clearTimeout(timeoutId);
134
+ this.state = false;
135
+ delete this.fireAt;
136
+ if (reset) {
137
+ this.reset();
138
+ }
139
+ onStop(pulse);
140
+ return true;
141
+ }
142
+ plan(isInit) {
143
+ const { state, metaData, nextId, opt: { maxRuns, getNow } } = this;
144
+ if (!state || metaData != null) {
145
+ return false;
146
+ }
147
+ if (maxRuns && maxRuns <= nextId) {
148
+ this.stop(true);
149
+ return false;
150
+ }
151
+ const fireIn = this.getPotential(isInit);
152
+ this.fireAt = getNow() + fireIn;
153
+ this.timeoutId = setTimeout(this.run, fireIn);
154
+ this.timeoutId.unref?.();
155
+ return true;
156
+ }
157
+ async run() {
158
+ const { pulse, state, metaData, opt: { noMeta, getNow, onPulse, onError, afterPulse } } = this;
159
+ if (!state || metaData) {
160
+ return;
161
+ }
162
+ const id = this.nextId++;
163
+ const md = this.metaData = noMeta ? id : createMetaData(this, id);
164
+ let errA, errB, result;
165
+ try {
166
+ result = await onPulse(pulse, md);
167
+ if (!noMeta) {
168
+ solid2(md, "result", result);
169
+ }
170
+ } catch (err) {
171
+ errA = err;
172
+ if (!noMeta) {
173
+ solid2(md, "error", errA);
174
+ }
175
+ }
176
+ if (!noMeta) {
177
+ solid2(md, "ended", getNow());
178
+ }
179
+ if (errA != null) {
180
+ if (noMeta) {
181
+ onError(pulse, errA, md);
182
+ } else {
183
+ onError(pulse, md);
184
+ }
185
+ }
186
+ try {
187
+ await afterPulse(pulse, md);
188
+ } catch (err) {
189
+ errB = err;
190
+ }
191
+ this.last = noMeta ? result : md;
192
+ delete this.metaData;
193
+ if (errB != null) {
194
+ this.stop();
195
+ throw errB;
196
+ }
197
+ this.plan(false);
198
+ }
199
+ restart(reset) {
200
+ this.stop(reset);
201
+ return this.start(reset);
202
+ }
48
203
  };
49
204
 
50
205
  // src/Pulse.js
@@ -54,79 +209,19 @@ var Pulse = class {
54
209
  * @param {PulseOptions} [options={}] See {@link PulseOptions}.
55
210
  */
56
211
  constructor(options = {}) {
57
- const _p = {
58
- state: false,
59
- nextId: 0
60
- };
61
- const getNow = valid("function", options.getNow, false, "options.getNow") || ((_) => Date.now());
62
- const interval = validRange(10, 2147483647, options.interval, true, "options.interval");
63
- const offset = validRange(0, interval, options.offset, false, "options.offset") || 0;
64
- const onPulse = valid("function", options.onPulse, true, "options.onPulse");
65
- _p.onStart = valid("function", options.onStart, false, "options.onStart") || blankFn;
66
- _p.onStop = valid("function", options.onStop, false, "options.onStop") || blankFn;
67
- const onError = valid("function", options.onError, false, "options.onError") || blankFn;
68
- const afterPulse = valid("function", options.afterPulse, false, "options.afterPulse") || blankFn;
69
- const autoStart = valid("boolean", options.autoStart, false, "options.autoStart") || false;
70
- const noMeta = valid("boolean", options.noMeta, false, "option.noMeta") || false;
71
- _p.reset = (_) => {
72
- _p.nextId = 0;
73
- };
74
- _p.plan = (_) => {
75
- if (!_p.state || _p.metaData != null) {
76
- return;
77
- }
78
- const now = getNow();
79
- const runIn = interval - now % interval + offset;
80
- _p.timeoutId = setTimeout(_p.run, runIn);
81
- };
82
- _p.run = async (_) => {
83
- if (!_p.state || _p.metaData) {
84
- return;
85
- }
86
- const id = _p.nextId++;
87
- const md = _p.metaData = noMeta ? id : createMetaData(id, getNow);
88
- let errA, errB, result;
89
- try {
90
- result = await onPulse(this, md);
91
- if (!noMeta) {
92
- solid2(md, "result", result);
93
- }
94
- } catch (err) {
95
- errA = err;
96
- if (!noMeta) {
97
- solid2(md, "error", errA);
98
- }
99
- }
100
- if (!noMeta) {
101
- solid2(md, "ended", getNow());
102
- }
103
- if (errA != null) {
104
- if (noMeta) {
105
- onError(this, errA, md);
106
- } else {
107
- onError(this, md);
108
- }
109
- }
110
- try {
111
- await afterPulse(this, md);
112
- } catch (err) {
113
- errB = err;
114
- }
115
- _p.last = noMeta ? result : md;
116
- delete _p.metaData;
117
- if (errB != null) {
118
- _p.state = false;
119
- throw errB;
120
- }
121
- _p.plan();
122
- };
212
+ const _p = new PulsePrivate(this, options);
213
+ const { getNow, align, interval, offset, autoStart } = _p.opt;
123
214
  virtuals(this, {
124
215
  state: (_) => _p.state,
125
- last: (_) => _p.last
216
+ last: (_) => _p.last,
217
+ countdown: (_) => _p.getCountdown(),
218
+ potential: (_) => _p.getCountdown() ?? _p.getPotential(),
219
+ nextAt: (_) => _p.state ? this.potential + getNow() : void 0
126
220
  });
127
221
  solids(this, {
128
222
  interval,
129
- offset
223
+ offset,
224
+ align
130
225
  });
131
226
  _pulses.set(this, _p);
132
227
  if (autoStart) {
@@ -138,56 +233,30 @@ var Pulse = class {
138
233
  * @returns {boolean} `true` if it started now, `false` if it was already running.
139
234
  */
140
235
  start(reset = false) {
141
- const { state } = this;
142
- if (state) {
143
- return false;
144
- }
145
- const _p = _pulses.get(this);
146
- _p.onStart(this);
147
- if (reset) {
148
- _p.reset();
149
- }
150
- _p.state = true;
151
- _p.plan();
152
- return true;
236
+ return _pulses.get(this).start(reset);
153
237
  }
154
238
  /**
155
239
  * Stop the pulse loop (no-op if already stopped).
156
240
  * @returns {boolean} `true` if it stopped now, `false` if it was already stopped.
157
241
  */
158
242
  stop(reset = false) {
159
- const { state } = this;
160
- if (!state) {
161
- return false;
162
- }
163
- const _p = _pulses.get(this);
164
- clearTimeout(_p.timeoutId);
165
- _p.state = false;
166
- if (reset) {
167
- _p.reset();
168
- }
169
- _p.onStop(this);
170
- return true;
171
- }
172
- reset() {
173
- _pulses.get(this).reset();
174
- return true;
243
+ return _pulses.get(this).stop(reset);
175
244
  }
176
245
  restart(reset = false) {
177
- this.stop(reset);
178
- return this.start(reset);
246
+ return _pulses.get(this).restart(reset);
247
+ }
248
+ reset() {
249
+ return _pulses.get(this).reset();
179
250
  }
180
251
  };
181
252
 
182
- // src/static.js
253
+ // src/index.js
183
254
  var createPulse = (options = {}) => new Pulse(options);
184
255
  var stopAllPulses = () => {
185
256
  for (const [pulse, _pulse] of _pulses.entries()) {
186
257
  pulse.stop();
187
258
  }
188
259
  };
189
-
190
- // src/index.js
191
260
  var index_default = createPulse;
192
261
  export {
193
262
  Pulse,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../../src/Pulse.js", "../../src/const.js", "../../src/tools.js", "../../src/metadata.js", "../../src/static.js", "../../src/index.js"],
4
- "sourcesContent": ["import { solid, solids, virtuals } from \"@randajan/props\";\r\nimport { blankFn, _pulses } from \"./const\";\r\nimport { valid, validRange } from \"./tools\";\r\nimport { createMetaData } from \"./metadata\";\r\n\r\n/**\r\n * Periodic scheduler that aligns each callback to exact multiples\r\n * of the given interval (optionally shifted by `offset`).\r\n */\r\nexport class Pulse {\r\n\r\n /**\r\n * Create a new Pulse instance.\r\n * @param {PulseOptions} [options={}] See {@link PulseOptions}.\r\n */\r\n constructor(options = {}) {\r\n const _p = {\r\n state: false,\r\n nextId:0\r\n }\r\n\r\n const getNow = valid(\"function\", options.getNow, false, \"options.getNow\") || (_ => Date.now());\r\n\r\n const interval = validRange(10, 2_147_483_647, options.interval, true, \"options.interval\");\r\n const offset = (validRange(0, interval, options.offset, false, \"options.offset\") || 0);\r\n\r\n const onPulse = valid(\"function\", options.onPulse, true, \"options.onPulse\");\r\n _p.onStart = valid(\"function\", options.onStart, false, \"options.onStart\") || blankFn;\r\n _p.onStop = valid(\"function\", options.onStop, false, \"options.onStop\") || blankFn;\r\n \r\n const onError = valid(\"function\", options.onError, false, \"options.onError\") || blankFn;\r\n \r\n const afterPulse = valid(\"function\", options.afterPulse, false, \"options.afterPulse\") || blankFn;\r\n const autoStart = valid(\"boolean\", options.autoStart, false, \"options.autoStart\") || false;\r\n const noMeta = valid(\"boolean\", options.noMeta, false, \"option.noMeta\") || false;\r\n\r\n _p.reset = _=>{\r\n _p.nextId = 0;\r\n }\r\n\r\n _p.plan = _ => {\r\n if (!_p.state || _p.metaData != null) { return; }\r\n const now = getNow();\r\n const runIn = (interval - now % interval) + offset;\r\n _p.timeoutId = setTimeout(_p.run, runIn);\r\n }\r\n\r\n _p.run = async _ => {\r\n if (!_p.state || _p.metaData) { return; }\r\n\r\n const id = _p.nextId++;\r\n const md = _p.metaData = noMeta ? id : createMetaData(id, getNow);\r\n let errA, errB, result;\r\n\r\n try {\r\n result = await onPulse(this, md);\r\n if (!noMeta) { solid(md, \"result\", result); }\r\n }\r\n catch (err) {\r\n errA = err;\r\n if (!noMeta) { solid(md, \"error\", errA); }\r\n }\r\n\r\n if (!noMeta) { solid(md, \"ended\", getNow()); }\r\n\r\n if (errA != null) {\r\n if (noMeta) { onError(this, errA, md); }\r\n else { onError(this, md); }\r\n }\r\n\r\n try { await afterPulse(this, md); }\r\n catch(err) { errB = err; }\r\n\r\n _p.last = noMeta ? result : md;\r\n delete _p.metaData;\r\n\r\n if (errB != null) {\r\n _p.state = false;\r\n throw errB;\r\n }\r\n \r\n _p.plan();\r\n }\r\n\r\n virtuals(this, {\r\n state:_=>_p.state,\r\n last:_=>_p.last\r\n });\r\n\r\n solids(this, {\r\n interval,\r\n offset\r\n });\r\n\r\n _pulses.set(this, _p);\r\n\r\n if (autoStart) { this.start(); }\r\n }\r\n\r\n /**\r\n * Start the pulse loop (no-op if already running).\r\n * @returns {boolean} `true` if it started now, `false` if it was already running.\r\n */\r\n start(reset=false) {\r\n const { state } = this;\r\n if (state) { return false; }\r\n const _p = _pulses.get(this);\r\n _p.onStart(this);\r\n if (reset) { _p.reset(); }\r\n _p.state = true;\r\n _p.plan();\r\n return true;\r\n }\r\n\r\n /**\r\n * Stop the pulse loop (no-op if already stopped).\r\n * @returns {boolean} `true` if it stopped now, `false` if it was already stopped.\r\n */\r\n stop(reset=false) {\r\n const { state } = this;\r\n if (!state) { return false; }\r\n const _p = _pulses.get(this);\r\n clearTimeout(_p.timeoutId);\r\n _p.state = false;\r\n if (reset) { _p.reset(); }\r\n _p.onStop(this);\r\n return true;\r\n }\r\n\r\n reset() {\r\n _pulses.get(this).reset();\r\n return true;\r\n }\r\n\r\n restart(reset=false) {\r\n this.stop(reset);\r\n return this.start(reset);\r\n }\r\n\r\n\r\n}", "\r\n\r\n\r\nexport const _pulses = new Map();\r\n\r\nexport const blankFn = ()=>{};", "\r\n\r\nexport const is = (type, any)=>typeof any === type;\r\n\r\nexport const valid = (type, any, req=false, msg=\"argument\")=>{\r\n if (any == null) {\r\n if (!req) { return; }\r\n throw new Error(`${msg} require typeof '${type}'`);\r\n }\r\n if (is(type, any)) { return any; }\r\n throw new Error(`${msg} is not typeof '${type}'`);\r\n}\r\n\r\nexport const validRange = (min, max, any, req=false, msg=\"argument\")=>{\r\n const num = valid(\"number\", any, req, msg);\r\n if (num == null) { return; }\r\n if (num < min) { throw new Error(`${msg} must be greater than ${min}`); }\r\n if (num > max) { throw new Error(`${msg} must be less than ${max}`); }\r\n return num;\r\n}", "import { solid, virtual } from \"@randajan/props\";\r\n\r\n\r\n/**\r\n * Metadata object passed to every pulse callback.\r\n * @typedef {Object} PulseMeta\r\n * @property {Date} started Timestamp when this pulse began.\r\n * @property {string[]} warnings Snapshot of all warnings collected so far.\r\n * @property {number} runtime Milliseconds elapsed since `started`.\r\n * @property {(warning: string|Error) => void} warn Push a warning into the internal list.\r\n */\r\nexport const createMetaData = (id, getNow) => {\r\n const p = {};\r\n const w = [];\r\n solid(p, \"id\", id);\r\n solid(p, \"started\", getNow());\r\n virtual(p, \"warnings\", _ => [...w]);\r\n virtual(p, \"runtime\", _ => (p.ended ? p.ended : getNow()) - p.started);\r\n solid(p, \"warn\", warning => w.push(warning), false);\r\n return p;\r\n}\r\n\r\n/**\r\n * Configuration object for {@link Pulse}.\r\n * @typedef {Object} PulseOptions\r\n * @property {(meta: PulseMeta) => (void|Promise<void>)} onPulse **required** \u2013 user callback invoked on every pulse.\r\n * @property {number} interval **required** \u2013 period length in ms (50 \u2013 2 147 483 647).\r\n * @property {number} [offset=0] Shift in ms applied to the start of each interval (0 \u2264 offset < interval).\r\n * @property {() => number} [getNow=() => Date.now()] Custom time source, useful for tests or time travel.\r\n * @property {(err: unknown) => void} [onError=blankFn] Error handler for exceptions or rejected `onPulse` promises.\r\n * @property {boolean} [autoStart=false] If true, the loop starts immediately after construction.\r\n */", "import { _pulses } from \"./const\";\r\nimport { Pulse } from \"./Pulse.js\";\r\n\r\n\r\n/**\r\n * Factory helper \u2013 simply calls `new Pulse(options)`.\r\n * @param {PulseOptions} [options={}] See {@link PulseOptions}.\r\n * @returns {Pulse}\r\n */\r\nexport const createPulse = (options = {}) => new Pulse(options);\r\n\r\n/**\r\n * Process exit helper \u2013 stops all pending pulses.\r\n */\r\n\r\nexport const stopAllPulses = ()=>{\r\n for (const [pulse, _pulse] of _pulses.entries()) {\r\n pulse.stop();\r\n }\r\n}\r\n", "import { Pulse } from \"./Pulse\";\nimport { createPulse, stopAllPulses } from \"./static\";\n\nexport default createPulse;\n\nexport { createPulse, stopAllPulses, Pulse }\n"],
5
- "mappings": "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",
6
- "names": ["solid", "solid"]
3
+ "sources": ["../../src/Pulse.js", "../../src/const.js", "../../src/tools.js", "../../src/static/options.js", "../../src/static/PulsePrivate.js", "../../src/static/metadata.js", "../../src/index.js"],
4
+ "sourcesContent": ["import { solid, solids, virtuals } from \"@randajan/props\";\nimport { _pulses } from \"./const\";\nimport { formatOptions } from \"./static/options\";\nimport { PulsePrivate } from \"./static/PulsePrivate\";\n\r\n/**\r\n * Periodic scheduler that aligns each callback to exact multiples\r\n * of the given interval (optionally shifted by `offset`).\r\n */\r\nexport class Pulse {\r\n\r\n /**\r\n * Create a new Pulse instance.\r\n * @param {PulseOptions} [options={}] See {@link PulseOptions}.\r\n */\r\n constructor(options = {}) {\r\n const _p = new PulsePrivate(this, options);\r\n\r\n const { getNow, align, interval, offset, autoStart } = _p.opt;\r\n\r\n virtuals(this, {\r\n state:_=>_p.state,\r\n last:_=>_p.last,\r\n countdown:_=>_p.getCountdown(),\r\n potential:_=>_p.getCountdown() ?? _p.getPotential(),\r\n nextAt:_=>_p.state ? this.potential + getNow() : undefined\r\n });\r\n\r\n solids(this, {\r\n interval,\r\n offset,\r\n align\r\n });\r\n\r\n _pulses.set(this, _p);\r\n\r\n if (autoStart) { this.start(); }\r\n }\r\n\r\n /**\r\n * Start the pulse loop (no-op if already running).\r\n * @returns {boolean} `true` if it started now, `false` if it was already running.\r\n */\r\n start(reset=false) {\r\n return _pulses.get(this).start(reset);\r\n }\r\n\r\n /**\r\n * Stop the pulse loop (no-op if already stopped).\r\n * @returns {boolean} `true` if it stopped now, `false` if it was already stopped.\r\n */\r\n stop(reset=false) {\r\n return _pulses.get(this).stop(reset);\r\n }\r\n\r\n restart(reset=false) {\r\n return _pulses.get(this).restart(reset);\r\n }\r\n\r\n reset() {\r\n return _pulses.get(this).reset();\r\n }\r\n\r\n}\r\n", "\r\n\r\n\r\nexport const _pulses = new Map();\r\n\r\nexport const blankFn = ()=>{};", "\n\nexport const is = (type, any)=>typeof any === type;\n\nexport const valid = (type, any, req=false, msg=\"argument\")=>{\n if (any == null) {\n if (!req) { return; }\n throw new Error(`${msg} require typeof '${type}'`);\n }\n if (is(type, any)) { return any; }\n throw new Error(`${msg} is not typeof '${type}'`);\n}\n\nexport const validRange = (min, max, any, req=false, msg=\"argument\")=>{\n const num = valid(\"number\", any, req, msg);\n if (num == null) { return; }\n if (num < min) { throw new Error(`${msg} must be greater than ${min}`); }\n if (num > max) { throw new Error(`${msg} must be less than ${max}`); }\n return num;\n}", "import { blankFn } from \"../const\";\nimport { valid, validRange } from \"../tools\";\n\n\n\n/**\n * Configuration object for {@link Pulse}.\n * @typedef {Object} PulseOptions\n * @property {(pulse: import(\"../Pulse\").Pulse, meta: import(\"./metadata\").PulseMeta|number) => (*|Promise<*>)} onPulse **required** \u2013 user callback invoked on every pulse.\n * @property {boolean} align **required** \u2013 align to wall-clock interval edges when true, run relatively when false.\n * @property {number} interval **required** \u2013 period length in ms (10 \u2013 2 147 483 647).\n * @property {number} [offset=0] Timing offset. Range and meaning depend on `align` and `runOnStart`.\n * @property {boolean} [runOnStart=false] Schedule the first pulse immediately on start.\n * @property {number} [maxRuns=0] Maximum number of pulses before automatic stop. `0` means unlimited.\n * @property {() => number} [getNow=() => Date.now()] Custom time source, useful for tests or time travel.\n * @property {boolean} [autoStart=false] If true, the loop starts immediately after construction.\n * @property {boolean} [noMeta=false] If true, callbacks receive the numeric pulse id instead of `PulseMeta`.\n * @property {(pulse: import(\"../Pulse\").Pulse) => void} [onStart=blankFn] Called right before the pulse loop starts.\n * @property {(pulse: import(\"../Pulse\").Pulse) => void} [onStop=blankFn] Called right after the pulse loop stops.\n * @property {Function} [onError=blankFn] Error handler for exceptions or rejected `onPulse` promises.\n * @property {(pulse: import(\"../Pulse\").Pulse, meta: import(\"./metadata\").PulseMeta|number) => (void|Promise<void>)} [afterPulse=blankFn] Callback executed after every pulse.\n */\nexport const formatOptions = (options) => {\n const o = {};\r\n\r\n o.getNow = valid(\"function\", options.getNow, false, \"options.getNow\") || (_ => Date.now());\r\n\r\n o.align = valid(\"boolean\", options.align, true, \"options.align\");\r\n\r\n const minInterval = 10;\r\n const maxInterval = 2_147_483_647;\r\n o.interval = validRange(minInterval, maxInterval, options.interval, true, \"options.interval\");\r\n\r\n o.runOnStart = valid(\"boolean\", options.runOnStart, false, \"options.runOnStart\") || false;\r\n\r\n const offsetMsg = `options.offset (align:${o.align})`;\r\n\r\n if (o.align) {\r\n o.offset = (validRange(0, o.interval, options.offset, false, offsetMsg) ?? 0);\r\n } else if (!o.runOnStart) {\r\n o.offset = (validRange(-o.interval, (maxInterval - o.interval), options.offset, false, offsetMsg) ?? 0);\r\n } else if (options.offset != null) {\n throw new Error(`${offsetMsg} can't be set if runOnStart:true`);\n } else {\n o.offset = -o.interval;\n }\r\n\r\n o.maxRuns = validRange(0, Infinity, options.maxRuns, false, \"options.maxRuns\") ?? 0;\r\n\r\n o.onPulse = valid(\"function\", options.onPulse, true, \"options.onPulse\");\r\n o.onStart = valid(\"function\", options.onStart, false, \"options.onStart\") || blankFn;\r\n o.onStop = valid(\"function\", options.onStop, false, \"options.onStop\") || blankFn;\r\n o.onError = valid(\"function\", options.onError, false, \"options.onError\") || blankFn;\r\n\r\n o.afterPulse = valid(\"function\", options.afterPulse, false, \"options.afterPulse\") || blankFn;\r\n o.autoStart = valid(\"boolean\", options.autoStart, false, \"options.autoStart\") || false;\r\n o.noMeta = valid(\"boolean\", options.noMeta, false, \"option.noMeta\") || false;\r\n\r\n return Object.freeze(o);\r\n}\n", "import { solid } from \"@randajan/props\";\r\nimport { createMetaData } from \"./metadata\";\r\nimport { formatOptions } from \"./options\";\r\n\r\n\r\nexport class PulsePrivate {\r\n\r\n constructor(pulse, options) {\r\n this.pulse = pulse;\r\n this.state = false;\r\n this.nextId = 0;\r\n this.opt = formatOptions(options);\r\n\r\n this.run = this.run.bind(this);\r\n }\r\n\r\n getCountdown() {\r\n const { state, fireAt, opt:{ getNow } } = this;\r\n if (!state || fireAt == null) { return; }\r\n return Math.max(0, fireAt - getNow());\r\n }\r\n\r\n getPotential(isInit = true) {\r\n const { opt:{ align, runOnStart, interval, offset, getNow } } = this;\r\n if (isInit && runOnStart) { return 0; }\r\n if (!align) { return isInit ? (interval + offset) : interval; }\r\n const now = getNow() - offset;\r\n const rest = ((now % interval) + interval) % interval;\r\n return rest === 0 ? interval : interval - rest;\r\n }\r\n\r\n reset() {\r\n this.nextId = 0;\r\n return true;\r\n }\r\n\r\n start(reset=false) {\r\n const { pulse, state, timeoutId, opt:{ onStart } } = this;\r\n if (state) { return false; }\r\n onStart(pulse);\r\n if (reset) { this.reset(); }\r\n this.state = true;\r\n return this.plan(true);\r\n }\r\n\r\n stop(reset=false) {\r\n const { pulse, state, timeoutId, opt:{ onStop } } = this;\r\n if (!state) { return false; }\r\n clearTimeout(timeoutId);\r\n this.state = false;\r\n delete this.fireAt;\r\n if (reset) { this.reset(); }\r\n onStop(pulse);\r\n return true;\r\n }\r\n\r\n plan(isInit) {\r\n const { state, metaData, nextId, opt:{ maxRuns, getNow } } = this;\r\n if (!state || metaData != null) { return false; }\r\n if (maxRuns && maxRuns <= nextId) { this.stop(true); return false; }\r\n const fireIn = this.getPotential(isInit);\r\n this.fireAt = getNow() + fireIn;\r\n this.timeoutId = setTimeout(this.run, fireIn);\r\n this.timeoutId.unref?.();\r\n return true;\r\n }\r\n\r\n async run() {\r\n const { pulse, state, metaData, opt:{ noMeta, getNow, onPulse, onError, afterPulse } } = this;\r\n if (!state || metaData) { return; }\r\n\r\n const id = this.nextId++;\r\n const md = this.metaData = noMeta ? id : createMetaData(this, id);\n let errA, errB, result;\r\n\r\n try {\r\n result = await onPulse(pulse, md);\r\n if (!noMeta) { solid(md, \"result\", result); }\r\n }\r\n catch (err) {\r\n errA = err;\r\n if (!noMeta) { solid(md, \"error\", errA); }\r\n }\r\n\r\n if (!noMeta) { solid(md, \"ended\", getNow()); }\r\n\r\n if (errA != null) {\r\n if (noMeta) { onError(pulse, errA, md); }\r\n else { onError(pulse, md); }\r\n }\r\n\r\n try { await afterPulse(pulse, md); }\r\n catch (err) { errB = err; }\r\n\r\n this.last = noMeta ? result : md;\r\n delete this.metaData;\r\n\r\n if (errB != null) {\r\n this.stop();\r\n throw errB;\r\n }\r\n\r\n this.plan(false);\r\n }\r\n\r\n restart(reset) {\r\n this.stop(reset);\r\n return this.start(reset);\r\n }\r\n\r\n}\n", "import { solid, virtual } from \"@randajan/props\";\r\n\r\n\r\n/**\n * Metadata object passed to every pulse callback.\n * @typedef {Object} PulseMeta\n * @property {number} id Sequential pulse id.\n * @property {number} started Timestamp when this pulse began.\n * @property {number} [ended] Timestamp when this pulse finished.\n * @property {number} runtime Milliseconds elapsed since `started`.\n * @property {number} lateBy Milliseconds over the configured interval.\n * @property {string[]|Error[]} warnings Snapshot of all warnings collected so far.\n * @property {*} [result] Value returned by `onPulse`.\n * @property {*} [error] Error thrown or rejected by `onPulse`.\n * @property {(warning: string|Error) => void} warn Push a warning into the internal list.\n */\nexport const createMetaData = (_p, id) => {\n const md = {};\n const w = [];\n solid(md, \"id\", id);\n solid(md, \"started\", _p.opt.getNow());\r\n virtual(md, \"warnings\", _ => [...w]);\r\n virtual(md, \"runtime\", _ => (md.ended ? md.ended : _p.opt.getNow()) - md.started);\r\n virtual(md, \"lateBy\", _=>Math.max(0, md.runtime-_p.opt.interval));\r\n solid(md, \"warn\", warning => w.push(warning), false);\r\n return md;\r\n}\r\n\r\n", "import { Pulse } from \"./Pulse\";\nimport { _pulses } from \"./const\";\n\n\nexport {\n Pulse\n}\n\n/**\n * Factory helper \u2013 simply calls `new Pulse(options)`.\n * @param {PulseOptions} [options={}] See {@link PulseOptions}.\n * @returns {Pulse}\n */\nexport const createPulse = (options = {}) => new Pulse(options);\n\n\n/**\n * Process exit helper \u2013 stops all pending pulses.\n */\nexport const stopAllPulses = ()=>{\n for (const [pulse, _pulse] of _pulses.entries()) {\n pulse.stop();\n }\n}\n\nexport default createPulse;\n"],
5
+ "mappings": ";AAAA,SAAS,SAAAA,QAAO,QAAQ,gBAAgB;;;ACGjC,IAAM,UAAU,oBAAI,IAAI;AAExB,IAAM,UAAU,MAAI;AAAC;;;ACHrB,IAAM,KAAK,CAAC,MAAM,QAAM,OAAO,QAAQ;AAEvC,IAAM,QAAQ,CAAC,MAAM,KAAK,MAAI,OAAO,MAAI,eAAa;AACzD,MAAI,OAAO,MAAM;AACb,QAAI,CAAC,KAAK;AAAE;AAAA,IAAQ;AACpB,UAAM,IAAI,MAAM,GAAG,GAAG,oBAAoB,IAAI,GAAG;AAAA,EACrD;AACA,MAAI,GAAG,MAAM,GAAG,GAAG;AAAE,WAAO;AAAA,EAAK;AACjC,QAAM,IAAI,MAAM,GAAG,GAAG,mBAAmB,IAAI,GAAG;AACpD;AAEO,IAAM,aAAa,CAAC,KAAK,KAAK,KAAK,MAAI,OAAO,MAAI,eAAa;AAClE,QAAM,MAAM,MAAM,UAAU,KAAK,KAAK,GAAG;AACzC,MAAI,OAAO,MAAM;AAAE;AAAA,EAAQ;AAC3B,MAAI,MAAM,KAAK;AAAE,UAAM,IAAI,MAAM,GAAG,GAAG,yBAAyB,GAAG,EAAE;AAAA,EAAG;AACxE,MAAI,MAAM,KAAK;AAAE,UAAM,IAAI,MAAM,GAAG,GAAG,sBAAsB,GAAG,EAAE;AAAA,EAAG;AACrE,SAAO;AACX;;;ACGO,IAAM,gBAAgB,CAAC,YAAY;AACtC,QAAM,IAAI,CAAC;AAEX,IAAE,SAAS,MAAM,YAAY,QAAQ,QAAQ,OAAO,gBAAgB,MAAM,OAAK,KAAK,IAAI;AAExF,IAAE,QAAQ,MAAM,WAAW,QAAQ,OAAO,MAAM,eAAe;AAE/D,QAAM,cAAc;AACpB,QAAM,cAAc;AACpB,IAAE,WAAW,WAAW,aAAa,aAAa,QAAQ,UAAU,MAAM,kBAAkB;AAE5F,IAAE,aAAa,MAAM,WAAW,QAAQ,YAAY,OAAO,oBAAoB,KAAK;AAEpF,QAAM,YAAY,yBAAyB,EAAE,KAAK;AAElD,MAAI,EAAE,OAAO;AACT,MAAE,SAAU,WAAW,GAAG,EAAE,UAAU,QAAQ,QAAQ,OAAO,SAAS,KAAK;AAAA,EAC/E,WAAW,CAAC,EAAE,YAAY;AACtB,MAAE,SAAU,WAAW,CAAC,EAAE,UAAW,cAAc,EAAE,UAAW,QAAQ,QAAQ,OAAO,SAAS,KAAK;AAAA,EACzG,WAAW,QAAQ,UAAU,MAAM;AAC/B,UAAM,IAAI,MAAM,GAAG,SAAS,kCAAkC;AAAA,EAClE,OAAO;AACH,MAAE,SAAS,CAAC,EAAE;AAAA,EAClB;AAEA,IAAE,UAAU,WAAW,GAAG,UAAU,QAAQ,SAAS,OAAO,iBAAiB,KAAK;AAElF,IAAE,UAAU,MAAM,YAAY,QAAQ,SAAS,MAAM,iBAAiB;AACtE,IAAE,UAAU,MAAM,YAAY,QAAQ,SAAS,OAAO,iBAAiB,KAAK;AAC5E,IAAE,SAAS,MAAM,YAAY,QAAQ,QAAQ,OAAO,gBAAgB,KAAK;AACzE,IAAE,UAAU,MAAM,YAAY,QAAQ,SAAS,OAAO,iBAAiB,KAAK;AAE5E,IAAE,aAAa,MAAM,YAAY,QAAQ,YAAY,OAAO,oBAAoB,KAAK;AACrF,IAAE,YAAY,MAAM,WAAW,QAAQ,WAAW,OAAO,mBAAmB,KAAK;AACjF,IAAE,SAAS,MAAM,WAAW,QAAQ,QAAQ,OAAO,eAAe,KAAK;AAEvE,SAAO,OAAO,OAAO,CAAC;AAC1B;;;AC3DA,SAAS,SAAAC,cAAa;;;ACAtB,SAAS,OAAO,eAAe;AAgBxB,IAAM,iBAAiB,CAAC,IAAI,OAAO;AACtC,QAAM,KAAK,CAAC;AACZ,QAAM,IAAI,CAAC;AACX,QAAM,IAAI,MAAM,EAAE;AAClB,QAAM,IAAI,WAAW,GAAG,IAAI,OAAO,CAAC;AACpC,UAAQ,IAAI,YAAY,OAAK,CAAC,GAAG,CAAC,CAAC;AACnC,UAAQ,IAAI,WAAW,QAAM,GAAG,QAAQ,GAAG,QAAQ,GAAG,IAAI,OAAO,KAAK,GAAG,OAAO;AAChF,UAAQ,IAAI,UAAU,OAAG,KAAK,IAAI,GAAG,GAAG,UAAQ,GAAG,IAAI,QAAQ,CAAC;AAChE,QAAM,IAAI,QAAQ,aAAW,EAAE,KAAK,OAAO,GAAG,KAAK;AACnD,SAAO;AACX;;;ADrBO,IAAM,eAAN,MAAmB;AAAA,EAEtB,YAAY,OAAO,SAAS;AACxB,SAAK,QAAQ;AACb,SAAK,QAAQ;AACb,SAAK,SAAS;AACd,SAAK,MAAM,cAAc,OAAO;AAEhC,SAAK,MAAM,KAAK,IAAI,KAAK,IAAI;AAAA,EACjC;AAAA,EAEA,eAAe;AACX,UAAM,EAAE,OAAO,QAAQ,KAAI,EAAE,OAAO,EAAE,IAAI;AAC1C,QAAI,CAAC,SAAS,UAAU,MAAM;AAAE;AAAA,IAAQ;AACxC,WAAO,KAAK,IAAI,GAAG,SAAS,OAAO,CAAC;AAAA,EACxC;AAAA,EAEA,aAAa,SAAS,MAAM;AACxB,UAAM,EAAE,KAAI,EAAE,OAAO,YAAY,UAAU,QAAQ,OAAO,EAAE,IAAI;AAChE,QAAI,UAAU,YAAY;AAAE,aAAO;AAAA,IAAG;AACtC,QAAI,CAAC,OAAO;AAAE,aAAO,SAAU,WAAW,SAAU;AAAA,IAAU;AAC9D,UAAM,MAAM,OAAO,IAAI;AACvB,UAAM,QAAS,MAAM,WAAY,YAAY;AAC7C,WAAO,SAAS,IAAI,WAAW,WAAW;AAAA,EAC9C;AAAA,EAEA,QAAQ;AACJ,SAAK,SAAS;AACd,WAAO;AAAA,EACX;AAAA,EAEA,MAAM,QAAM,OAAO;AACf,UAAM,EAAE,OAAO,OAAO,WAAW,KAAI,EAAE,QAAQ,EAAE,IAAI;AACrD,QAAI,OAAO;AAAE,aAAO;AAAA,IAAO;AAC3B,YAAQ,KAAK;AACb,QAAI,OAAO;AAAE,WAAK,MAAM;AAAA,IAAG;AAC3B,SAAK,QAAQ;AACb,WAAO,KAAK,KAAK,IAAI;AAAA,EACzB;AAAA,EAEA,KAAK,QAAM,OAAO;AACd,UAAM,EAAE,OAAO,OAAO,WAAW,KAAI,EAAE,OAAO,EAAE,IAAI;AACpD,QAAI,CAAC,OAAO;AAAE,aAAO;AAAA,IAAO;AAC5B,iBAAa,SAAS;AACtB,SAAK,QAAQ;AACb,WAAO,KAAK;AACZ,QAAI,OAAO;AAAE,WAAK,MAAM;AAAA,IAAG;AAC3B,WAAO,KAAK;AACZ,WAAO;AAAA,EACX;AAAA,EAEA,KAAK,QAAQ;AACT,UAAM,EAAE,OAAO,UAAU,QAAQ,KAAI,EAAE,SAAS,OAAO,EAAE,IAAI;AAC7D,QAAI,CAAC,SAAS,YAAY,MAAM;AAAE,aAAO;AAAA,IAAO;AAChD,QAAI,WAAW,WAAW,QAAQ;AAAE,WAAK,KAAK,IAAI;AAAG,aAAO;AAAA,IAAO;AACnE,UAAM,SAAS,KAAK,aAAa,MAAM;AACvC,SAAK,SAAS,OAAO,IAAI;AACzB,SAAK,YAAY,WAAW,KAAK,KAAK,MAAM;AAC5C,SAAK,UAAU,QAAQ;AACvB,WAAO;AAAA,EACX;AAAA,EAEA,MAAM,MAAM;AACR,UAAM,EAAE,OAAO,OAAO,UAAU,KAAI,EAAE,QAAQ,QAAQ,SAAS,SAAS,WAAW,EAAE,IAAI;AACzF,QAAI,CAAC,SAAS,UAAU;AAAE;AAAA,IAAQ;AAElC,UAAM,KAAK,KAAK;AAChB,UAAM,KAAK,KAAK,WAAW,SAAS,KAAK,eAAe,MAAM,EAAE;AAChE,QAAI,MAAM,MAAM;AAEhB,QAAI;AACA,eAAS,MAAM,QAAQ,OAAO,EAAE;AAChC,UAAI,CAAC,QAAQ;AAAE,QAAAC,OAAM,IAAI,UAAU,MAAM;AAAA,MAAG;AAAA,IAChD,SACO,KAAK;AACR,aAAO;AACP,UAAI,CAAC,QAAQ;AAAE,QAAAA,OAAM,IAAI,SAAS,IAAI;AAAA,MAAG;AAAA,IAC7C;AAEA,QAAI,CAAC,QAAQ;AAAE,MAAAA,OAAM,IAAI,SAAS,OAAO,CAAC;AAAA,IAAG;AAE7C,QAAI,QAAQ,MAAM;AACd,UAAI,QAAQ;AAAE,gBAAQ,OAAO,MAAM,EAAE;AAAA,MAAG,OACnC;AAAE,gBAAQ,OAAO,EAAE;AAAA,MAAG;AAAA,IAC/B;AAEA,QAAI;AAAE,YAAM,WAAW,OAAO,EAAE;AAAA,IAAG,SAC5B,KAAK;AAAE,aAAO;AAAA,IAAK;AAE1B,SAAK,OAAO,SAAS,SAAS;AAC9B,WAAO,KAAK;AAEZ,QAAI,QAAQ,MAAM;AACd,WAAK,KAAK;AACV,YAAM;AAAA,IACV;AAEA,SAAK,KAAK,KAAK;AAAA,EACnB;AAAA,EAEA,QAAQ,OAAO;AACX,SAAK,KAAK,KAAK;AACf,WAAO,KAAK,MAAM,KAAK;AAAA,EAC3B;AAEJ;;;AJrGO,IAAM,QAAN,MAAY;AAAA;AAAA;AAAA;AAAA;AAAA,EAMf,YAAY,UAAU,CAAC,GAAG;AACtB,UAAM,KAAK,IAAI,aAAa,MAAM,OAAO;AAEzC,UAAM,EAAE,QAAQ,OAAO,UAAU,QAAQ,UAAU,IAAI,GAAG;AAE1D,aAAS,MAAM;AAAA,MACX,OAAM,OAAG,GAAG;AAAA,MACZ,MAAK,OAAG,GAAG;AAAA,MACX,WAAU,OAAG,GAAG,aAAa;AAAA,MAC7B,WAAU,OAAG,GAAG,aAAa,KAAK,GAAG,aAAa;AAAA,MAClD,QAAO,OAAG,GAAG,QAAQ,KAAK,YAAY,OAAO,IAAI;AAAA,IACrD,CAAC;AAED,WAAO,MAAM;AAAA,MACT;AAAA,MACA;AAAA,MACA;AAAA,IACJ,CAAC;AAED,YAAQ,IAAI,MAAM,EAAE;AAEpB,QAAI,WAAW;AAAE,WAAK,MAAM;AAAA,IAAG;AAAA,EACnC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,QAAM,OAAO;AACf,WAAO,QAAQ,IAAI,IAAI,EAAE,MAAM,KAAK;AAAA,EACxC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,KAAK,QAAM,OAAO;AACd,WAAO,QAAQ,IAAI,IAAI,EAAE,KAAK,KAAK;AAAA,EACvC;AAAA,EAEA,QAAQ,QAAM,OAAO;AACjB,WAAO,QAAQ,IAAI,IAAI,EAAE,QAAQ,KAAK;AAAA,EAC1C;AAAA,EAEA,QAAQ;AACJ,WAAO,QAAQ,IAAI,IAAI,EAAE,MAAM;AAAA,EACnC;AAEJ;;;AMlDO,IAAM,cAAc,CAAC,UAAU,CAAC,MAAM,IAAI,MAAM,OAAO;AAMvD,IAAM,gBAAgB,MAAI;AAC7B,aAAW,CAAC,OAAO,MAAM,KAAK,QAAQ,QAAQ,GAAG;AAC7C,UAAM,KAAK;AAAA,EACf;AACJ;AAEA,IAAO,gBAAQ;",
6
+ "names": ["solid", "solid", "solid"]
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@randajan/pulse",
3
- "version": "1.1.3",
3
+ "version": "2.0.0",
4
4
  "description": "Lightweight, promise-friendly scheduler that outperforms classic cron for any interval shorter than one month.",
5
5
  "repository": "randajan/pulse",
6
6
  "type": "module",
@@ -12,6 +12,9 @@
12
12
  "require": "./dist/cjs/index.cjs"
13
13
  }
14
14
  },
15
+ "scripts": {
16
+ "build": "node index.js isBuild=true"
17
+ },
15
18
  "license": "MIT",
16
19
  "devDependencies": {
17
20
  "@randajan/simple-lib": "^3.3.0"