@randajan/pulse 1.1.4 → 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,17 +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.countdown` | `number` | number of ms that remains to the next pulse |
92
- | `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. |
93
142
 
94
143
  ---
95
144
 
96
145
  ## Why not `setInterval`?
97
146
 
98
- `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.
99
148
 
100
149
  ---
101
150
 
@@ -107,8 +156,9 @@ JavaScript timers accept at most **2 147 483 647 ms**. To wait longer, ch
107
156
 
108
157
  ```js
109
158
  createPulse({
159
+ align: false,
110
160
  interval: 2_147_483_647,
111
- onPulse: async meta => {
161
+ onPulse: async (pulse, meta) => {
112
162
  if (stillWaiting()) return; // keep sleeping…
113
163
  // …real logic here
114
164
  },
@@ -122,4 +172,3 @@ createPulse({
122
172
  ## License
123
173
 
124
174
  MIT © [randajan](https://github.com/randajan)
125
-
@@ -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,78 +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
- _p.timeoutId = setTimeout(_p.run, this.countdown);
107
- };
108
- _p.run = async (_) => {
109
- if (!_p.state || _p.metaData) {
110
- return;
111
- }
112
- const id = _p.nextId++;
113
- const md = _p.metaData = noMeta ? id : createMetaData(id, getNow);
114
- let errA, errB, result;
115
- try {
116
- result = await onPulse(this, md);
117
- if (!noMeta) {
118
- (0, import_props2.solid)(md, "result", result);
119
- }
120
- } catch (err) {
121
- errA = err;
122
- if (!noMeta) {
123
- (0, import_props2.solid)(md, "error", errA);
124
- }
125
- }
126
- if (!noMeta) {
127
- (0, import_props2.solid)(md, "ended", getNow());
128
- }
129
- if (errA != null) {
130
- if (noMeta) {
131
- onError(this, errA, md);
132
- } else {
133
- onError(this, md);
134
- }
135
- }
136
- try {
137
- await afterPulse(this, md);
138
- } catch (err) {
139
- errB = err;
140
- }
141
- _p.last = noMeta ? result : md;
142
- delete _p.metaData;
143
- if (errB != null) {
144
- _p.state = false;
145
- throw errB;
146
- }
147
- _p.plan();
148
- };
149
- (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, {
150
243
  state: (_) => _p.state,
151
244
  last: (_) => _p.last,
152
- countdown: (_) => interval - getNow() % interval + offset
245
+ countdown: (_) => _p.getCountdown(),
246
+ potential: (_) => _p.getCountdown() ?? _p.getPotential(),
247
+ nextAt: (_) => _p.state ? this.potential + getNow() : void 0
153
248
  });
154
- (0, import_props2.solids)(this, {
249
+ (0, import_props3.solids)(this, {
155
250
  interval,
156
- offset
251
+ offset,
252
+ align
157
253
  });
158
254
  _pulses.set(this, _p);
159
255
  if (autoStart) {
@@ -165,55 +261,29 @@ var Pulse = class {
165
261
  * @returns {boolean} `true` if it started now, `false` if it was already running.
166
262
  */
167
263
  start(reset = false) {
168
- const { state } = this;
169
- if (state) {
170
- return false;
171
- }
172
- const _p = _pulses.get(this);
173
- _p.onStart(this);
174
- if (reset) {
175
- _p.reset();
176
- }
177
- _p.state = true;
178
- _p.plan();
179
- return true;
264
+ return _pulses.get(this).start(reset);
180
265
  }
181
266
  /**
182
267
  * Stop the pulse loop (no-op if already stopped).
183
268
  * @returns {boolean} `true` if it stopped now, `false` if it was already stopped.
184
269
  */
185
270
  stop(reset = false) {
186
- const { state } = this;
187
- if (!state) {
188
- return false;
189
- }
190
- const _p = _pulses.get(this);
191
- clearTimeout(_p.timeoutId);
192
- _p.state = false;
193
- if (reset) {
194
- _p.reset();
195
- }
196
- _p.onStop(this);
197
- return true;
198
- }
199
- reset() {
200
- _pulses.get(this).reset();
201
- return true;
271
+ return _pulses.get(this).stop(reset);
202
272
  }
203
273
  restart(reset = false) {
204
- this.stop(reset);
205
- return this.start(reset);
274
+ return _pulses.get(this).restart(reset);
275
+ }
276
+ reset() {
277
+ return _pulses.get(this).reset();
206
278
  }
207
279
  };
208
280
 
209
- // src/static.js
281
+ // src/index.js
210
282
  var createPulse = (options = {}) => new Pulse(options);
211
283
  var stopAllPulses = () => {
212
284
  for (const [pulse, _pulse] of _pulses.entries()) {
213
285
  pulse.stop();
214
286
  }
215
287
  };
216
-
217
- // src/index.js
218
288
  var index_default = createPulse;
219
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 _p.timeoutId = setTimeout(_p.run, this.countdown);\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 countdown:_=>((interval - getNow() % interval) + offset)\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,78 +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
- _p.timeoutId = setTimeout(_p.run, this.countdown);
79
- };
80
- _p.run = async (_) => {
81
- if (!_p.state || _p.metaData) {
82
- return;
83
- }
84
- const id = _p.nextId++;
85
- const md = _p.metaData = noMeta ? id : createMetaData(id, getNow);
86
- let errA, errB, result;
87
- try {
88
- result = await onPulse(this, md);
89
- if (!noMeta) {
90
- solid2(md, "result", result);
91
- }
92
- } catch (err) {
93
- errA = err;
94
- if (!noMeta) {
95
- solid2(md, "error", errA);
96
- }
97
- }
98
- if (!noMeta) {
99
- solid2(md, "ended", getNow());
100
- }
101
- if (errA != null) {
102
- if (noMeta) {
103
- onError(this, errA, md);
104
- } else {
105
- onError(this, md);
106
- }
107
- }
108
- try {
109
- await afterPulse(this, md);
110
- } catch (err) {
111
- errB = err;
112
- }
113
- _p.last = noMeta ? result : md;
114
- delete _p.metaData;
115
- if (errB != null) {
116
- _p.state = false;
117
- throw errB;
118
- }
119
- _p.plan();
120
- };
212
+ const _p = new PulsePrivate(this, options);
213
+ const { getNow, align, interval, offset, autoStart } = _p.opt;
121
214
  virtuals(this, {
122
215
  state: (_) => _p.state,
123
216
  last: (_) => _p.last,
124
- countdown: (_) => interval - getNow() % interval + offset
217
+ countdown: (_) => _p.getCountdown(),
218
+ potential: (_) => _p.getCountdown() ?? _p.getPotential(),
219
+ nextAt: (_) => _p.state ? this.potential + getNow() : void 0
125
220
  });
126
221
  solids(this, {
127
222
  interval,
128
- offset
223
+ offset,
224
+ align
129
225
  });
130
226
  _pulses.set(this, _p);
131
227
  if (autoStart) {
@@ -137,56 +233,30 @@ var Pulse = class {
137
233
  * @returns {boolean} `true` if it started now, `false` if it was already running.
138
234
  */
139
235
  start(reset = false) {
140
- const { state } = this;
141
- if (state) {
142
- return false;
143
- }
144
- const _p = _pulses.get(this);
145
- _p.onStart(this);
146
- if (reset) {
147
- _p.reset();
148
- }
149
- _p.state = true;
150
- _p.plan();
151
- return true;
236
+ return _pulses.get(this).start(reset);
152
237
  }
153
238
  /**
154
239
  * Stop the pulse loop (no-op if already stopped).
155
240
  * @returns {boolean} `true` if it stopped now, `false` if it was already stopped.
156
241
  */
157
242
  stop(reset = false) {
158
- const { state } = this;
159
- if (!state) {
160
- return false;
161
- }
162
- const _p = _pulses.get(this);
163
- clearTimeout(_p.timeoutId);
164
- _p.state = false;
165
- if (reset) {
166
- _p.reset();
167
- }
168
- _p.onStop(this);
169
- return true;
170
- }
171
- reset() {
172
- _pulses.get(this).reset();
173
- return true;
243
+ return _pulses.get(this).stop(reset);
174
244
  }
175
245
  restart(reset = false) {
176
- this.stop(reset);
177
- return this.start(reset);
246
+ return _pulses.get(this).restart(reset);
247
+ }
248
+ reset() {
249
+ return _pulses.get(this).reset();
178
250
  }
179
251
  };
180
252
 
181
- // src/static.js
253
+ // src/index.js
182
254
  var createPulse = (options = {}) => new Pulse(options);
183
255
  var stopAllPulses = () => {
184
256
  for (const [pulse, _pulse] of _pulses.entries()) {
185
257
  pulse.stop();
186
258
  }
187
259
  };
188
-
189
- // src/index.js
190
260
  var index_default = createPulse;
191
261
  export {
192
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 _p.timeoutId = setTimeout(_p.run, this.countdown);\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 countdown:_=>((interval - getNow() % interval) + offset)\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": "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",
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.4",
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"