@randajan/pulse 2.0.0 → 3.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.
@@ -20,20 +20,20 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  var index_exports = {};
21
21
  __export(index_exports, {
22
22
  Pulse: () => Pulse,
23
+ PulseCycle: () => PulseCycle,
24
+ PulseError: () => PulseError,
25
+ PulseExecuteError: () => PulseExecuteError,
26
+ PulseRuntimeError: () => PulseRuntimeError,
27
+ PulseScheduleError: () => PulseScheduleError,
28
+ PulseSession: () => PulseSession,
29
+ PulseStartError: () => PulseStartError,
30
+ PulseStopError: () => PulseStopError,
23
31
  createPulse: () => createPulse,
24
32
  default: () => index_default,
25
- stopAllPulses: () => stopAllPulses
33
+ handleError: () => handleError
26
34
  });
27
35
  module.exports = __toCommonJS(index_exports);
28
36
 
29
- // src/Pulse.js
30
- var import_props3 = require("@randajan/props");
31
-
32
- // src/const.js
33
- var _pulses = /* @__PURE__ */ new Map();
34
- var blankFn = () => {
35
- };
36
-
37
37
  // src/tools.js
38
38
  var is = (type, any) => typeof any === type;
39
39
  var valid = (type, any, req = false, msg = "argument") => {
@@ -41,31 +41,61 @@ var valid = (type, any, req = false, msg = "argument") => {
41
41
  if (!req) {
42
42
  return;
43
43
  }
44
- throw new Error(`${msg} require typeof '${type}'`);
44
+ throw new Error(`[pulse] ${msg} require typeof '${type}'`);
45
45
  }
46
46
  if (is(type, any)) {
47
47
  return any;
48
48
  }
49
- throw new Error(`${msg} is not typeof '${type}'`);
49
+ throw new Error(`[pulse] ${msg} is not typeof '${type}'`);
50
50
  };
51
51
  var validRange = (min, max, any, req = false, msg = "argument") => {
52
52
  const num = valid("number", any, req, msg);
53
53
  if (num == null) {
54
54
  return;
55
55
  }
56
- if (num < min) {
57
- throw new Error(`${msg} must be greater than ${min}`);
56
+ if (min != null && num < min) {
57
+ throw new Error(`[pulse] ${msg} must be >= ${min}`);
58
58
  }
59
- if (num > max) {
60
- throw new Error(`${msg} must be less than ${max}`);
59
+ if (max != null && num > max) {
60
+ throw new Error(`[pulse] ${msg} must be <= ${max}`);
61
61
  }
62
62
  return num;
63
63
  };
64
+ var validRangeFinite = (min, any, req = false, msg = "argument") => {
65
+ const num = validRange(min, null, any, req, msg);
66
+ if (num == null) {
67
+ return;
68
+ }
69
+ if (!Number.isFinite(num)) {
70
+ throw new Error(`[pulse] ${msg} must be finite`);
71
+ }
72
+ return num;
73
+ };
74
+ var validateGetNow = (options) => {
75
+ const custom = valid("function", options.getNow, false, "options.getNow");
76
+ if (!custom) {
77
+ return (_) => Date.now();
78
+ }
79
+ const safe = (_) => validRangeFinite(0, custom(), true, "options.getNow result");
80
+ safe();
81
+ return safe;
82
+ };
64
83
 
65
- // src/static/options.js
84
+ // src/options.js
85
+ var blankFn = () => {
86
+ };
66
87
  var formatOptions = (options) => {
67
88
  const o = {};
68
- o.getNow = valid("function", options.getNow, false, "options.getNow") || ((_) => Date.now());
89
+ if (options.noMeta != null) {
90
+ throw new Error("options.noMeta is obsolete");
91
+ }
92
+ if (options.maxRuns != null) {
93
+ throw new Error("options.maxRuns is obsolete");
94
+ }
95
+ if (options.afterPulse != null) {
96
+ throw new Error("options.afterPulse is obsolete");
97
+ }
98
+ o.getNow = validateGetNow(options);
69
99
  o.align = valid("boolean", options.align, true, "options.align");
70
100
  const minInterval = 10;
71
101
  const maxInterval = 2147483647;
@@ -81,209 +111,407 @@ var formatOptions = (options) => {
81
111
  } else {
82
112
  o.offset = -o.interval;
83
113
  }
84
- o.maxRuns = validRange(0, Infinity, options.maxRuns, false, "options.maxRuns") ?? 0;
85
114
  o.onPulse = valid("function", options.onPulse, true, "options.onPulse");
86
115
  o.onStart = valid("function", options.onStart, false, "options.onStart") || blankFn;
87
116
  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;
117
+ o.onError = valid("function", options.onError, false, "options.onError");
90
118
  o.autoStart = valid("boolean", options.autoStart, false, "options.autoStart") || false;
91
- o.noMeta = valid("boolean", options.noMeta, false, "option.noMeta") || false;
92
119
  return Object.freeze(o);
93
120
  };
94
121
 
95
- // src/static/PulsePrivate.js
96
- var import_props2 = require("@randajan/props");
97
-
98
- // src/static/metadata.js
99
- var import_props = require("@randajan/props");
100
- var createMetaData = (_p, id) => {
101
- const md = {};
102
- const w = [];
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;
122
+ // src/errors.js
123
+ var PulseError = class extends Error {
124
+ constructor(type, name, cause) {
125
+ super(`Pulse ${type} failed`, { cause });
126
+ this.name = name;
127
+ this.pulseErrType = type;
128
+ }
110
129
  };
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;
130
+ var PulseStartError = class extends PulseError {
131
+ constructor(cause) {
132
+ super("start", "PulseStartError", cause);
133
+ }
134
+ };
135
+ var PulseStopError = class extends PulseError {
136
+ constructor(cause) {
137
+ super("stop", "PulseStopError", cause);
138
+ }
139
+ };
140
+ var PulseScheduleError = class extends PulseError {
141
+ constructor(cause) {
142
+ super("schedule", "PulseScheduleError", cause);
143
+ }
144
+ };
145
+ var PulseExecuteError = class extends PulseError {
146
+ constructor(cause) {
147
+ super("execute", "PulseExecuteError", cause);
148
+ }
149
+ };
150
+ var PulseRuntimeError = class extends PulseError {
151
+ constructor(cause) {
152
+ super("runtime", "PulseRuntimeError", cause);
153
+ }
154
+ };
155
+ var getErrType = (err) => {
156
+ if (err instanceof PulseError) {
157
+ return err.pulseErrType;
158
+ }
159
+ return "unknown";
160
+ };
161
+ var handleError = async (pulse, error, onError) => {
162
+ const type = getErrType(error);
163
+ if (!onError) {
164
+ console.error(`[pulse] ${type} error:`, error);
165
+ } else {
166
+ try {
167
+ await onError(pulse, type, error);
168
+ } catch (e2) {
169
+ console.error(`[pulse] options.onError('${type}') failed: `, e2);
125
170
  }
126
- return Math.max(0, fireAt - getNow());
127
171
  }
128
- getPotential(isInit = true) {
129
- const { opt: { align, runOnStart, interval, offset, getNow } } = this;
130
- if (isInit && runOnStart) {
131
- return 0;
172
+ };
173
+
174
+ // src/class/PulseCycle.js
175
+ var PulseCycle = class {
176
+ #pulse = null;
177
+ #session = null;
178
+ #opt = null;
179
+ #warnings = [];
180
+ #ok = true;
181
+ #id = void 0;
182
+ #done = void 0;
183
+ #result = void 0;
184
+ #error = void 0;
185
+ #started = void 0;
186
+ #ended = void 0;
187
+ constructor(pulse, session, opt) {
188
+ this.#pulse = pulse;
189
+ this.#session = session;
190
+ this.#opt = opt;
191
+ }
192
+ get pulse() {
193
+ return this.#pulse;
194
+ }
195
+ get session() {
196
+ return this.#session;
197
+ }
198
+ get signal() {
199
+ return this.#session.signal;
200
+ }
201
+ get id() {
202
+ return this.#id;
203
+ }
204
+ get ok() {
205
+ return this.#ok;
206
+ }
207
+ get done() {
208
+ return this.#done;
209
+ }
210
+ get started() {
211
+ return this.#started;
212
+ }
213
+ get ended() {
214
+ return this.#ended;
215
+ }
216
+ get runtime() {
217
+ return (this.#done ? this.#ended : this.#opt.getNow()) - this.#started;
218
+ }
219
+ get lateBy() {
220
+ return Math.max(0, this.runtime - this.#opt.interval);
221
+ }
222
+ get warnings() {
223
+ return [...this.#warnings];
224
+ }
225
+ get result() {
226
+ return this.#result;
227
+ }
228
+ get error() {
229
+ return this.#error;
230
+ }
231
+ warn(warning) {
232
+ return this.#warnings.push(warning);
233
+ }
234
+ async _execute(id) {
235
+ if (this.#id != null) {
236
+ return false;
132
237
  }
133
- if (!align) {
134
- return isInit ? interval + offset : interval;
238
+ const { getNow, onPulse } = this.#opt;
239
+ this.#id = id;
240
+ this.#started = getNow();
241
+ try {
242
+ this.#result = await onPulse(this);
243
+ } catch (err) {
244
+ this.#error = err;
245
+ this.#ok = false;
135
246
  }
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;
247
+ this.#done = true;
248
+ this.#ended = getNow();
142
249
  return true;
143
250
  }
144
- start(reset = false) {
145
- const { pulse, state, timeoutId, opt: { onStart } } = this;
146
- if (state) {
251
+ toJSON() {
252
+ const { id, ok, done, started, ended, runtime, lateBy, warnings, result, error } = this;
253
+ return { id, ok, done, started, ended, runtime, lateBy, warnings, result, error };
254
+ }
255
+ };
256
+
257
+ // src/class/PulseSession.js
258
+ var PulseSession = class {
259
+ #pulse = null;
260
+ #opt = null;
261
+ #ctrl = new AbortController();
262
+ #state = false;
263
+ #timeoutId;
264
+ #fireAt;
265
+ #current = null;
266
+ #last;
267
+ #consumeNextId;
268
+ constructor(pulse, opt, consumeNextId) {
269
+ this.#pulse = pulse;
270
+ this.#opt = opt;
271
+ this.#consumeNextId = consumeNextId;
272
+ }
273
+ get pulse() {
274
+ return this.#pulse;
275
+ }
276
+ get signal() {
277
+ return this.#ctrl.signal;
278
+ }
279
+ get interval() {
280
+ return this.#opt.interval;
281
+ }
282
+ get offset() {
283
+ return this.#opt.offset;
284
+ }
285
+ get align() {
286
+ return this.#opt.align;
287
+ }
288
+ get state() {
289
+ return this.#state && !this.#ctrl.signal.aborted;
290
+ }
291
+ get current() {
292
+ return this.#current;
293
+ }
294
+ get last() {
295
+ return this.#last;
296
+ }
297
+ get countdown() {
298
+ return this.calculateCountdown();
299
+ }
300
+ get potential() {
301
+ return this.#pulse.potential;
302
+ }
303
+ get nextAt() {
304
+ return this.state ? this.#pulse.potential + this.#opt.getNow() : void 0;
305
+ }
306
+ get nextId() {
307
+ return this.#pulse.nextId;
308
+ }
309
+ calculateCountdown() {
310
+ if (!this.state || this.#fireAt == null) {
311
+ return;
312
+ }
313
+ return Math.max(0, this.#fireAt - this.#opt.getNow());
314
+ }
315
+ start() {
316
+ if (this.#state) {
147
317
  return false;
148
318
  }
149
- onStart(pulse);
150
- if (reset) {
151
- this.reset();
319
+ this.#state = true;
320
+ try {
321
+ this.#opt.onStart(this);
322
+ } catch (err) {
323
+ this.#state = false;
324
+ this.#ctrl.abort(err);
325
+ throw new PulseStartError(err);
152
326
  }
153
- this.state = true;
154
- return this.plan(true);
327
+ return this.#schedule(true);
155
328
  }
156
- stop(reset = false) {
157
- const { pulse, state, timeoutId, opt: { onStop } } = this;
158
- if (!state) {
329
+ stop(reason) {
330
+ if (!this.#state) {
159
331
  return false;
160
332
  }
161
- clearTimeout(timeoutId);
162
- this.state = false;
163
- delete this.fireAt;
164
- if (reset) {
165
- this.reset();
333
+ if (this.#pulse.session === this) {
334
+ return this.#pulse.stop(reason);
335
+ }
336
+ this.#state = false;
337
+ clearTimeout(this.#timeoutId);
338
+ this.#fireAt = void 0;
339
+ this.#current = null;
340
+ this.#ctrl.abort(reason);
341
+ if (reason != null) {
342
+ this.#handleError(reason);
343
+ }
344
+ try {
345
+ this.#opt.onStop(this, reason);
346
+ } catch (err) {
347
+ this.#handleError(new PulseStopError(err));
166
348
  }
167
- onStop(pulse);
168
349
  return true;
169
350
  }
170
- plan(isInit) {
171
- const { state, metaData, nextId, opt: { maxRuns, getNow } } = this;
172
- if (!state || metaData != null) {
351
+ #handleError(error) {
352
+ return handleError(this.pulse, error, this.#opt.onError);
353
+ }
354
+ // Even a temporarily broken clock/configuration must not permanently stall scheduling.
355
+ #schedule(isInit) {
356
+ if (!this.state || this.#current != null) {
173
357
  return false;
174
358
  }
175
- if (maxRuns && maxRuns <= nextId) {
176
- this.stop(true);
359
+ try {
360
+ const fireIn = this.#pulse.calculatePotential(isInit);
361
+ this.#fireAt = this.#opt.getNow() + fireIn;
362
+ this.#timeoutId = setTimeout(this.#tick, fireIn);
363
+ this.#timeoutId.unref?.();
364
+ return true;
365
+ } catch (err) {
366
+ this.stop(new PulseScheduleError(err));
177
367
  return false;
178
368
  }
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
369
  }
185
- async run() {
186
- const { pulse, state, metaData, opt: { noMeta, getNow, onPulse, onError, afterPulse } } = this;
187
- if (!state || metaData) {
370
+ #tick = async () => {
371
+ if (!this.state || this.#current != null) {
188
372
  return;
189
373
  }
190
- const id = this.nextId++;
191
- const md = this.metaData = noMeta ? id : createMetaData(this, id);
192
- let errA, errB, result;
374
+ let cycle;
193
375
  try {
194
- result = await onPulse(pulse, md);
195
- if (!noMeta) {
196
- (0, import_props2.solid)(md, "result", result);
376
+ this.#current = cycle = new PulseCycle(this.#pulse, this, this.#opt);
377
+ await cycle._execute(this.#consumeNextId());
378
+ if (!cycle.ok) {
379
+ this.#handleError(new PulseRuntimeError(cycle.error));
197
380
  }
198
381
  } catch (err) {
199
- errA = err;
200
- if (!noMeta) {
201
- (0, import_props2.solid)(md, "error", errA);
202
- }
382
+ this.stop(new PulseExecuteError(err));
203
383
  }
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;
384
+ if (!this.state) {
385
+ return;
218
386
  }
219
- this.last = noMeta ? result : md;
220
- delete this.metaData;
221
- if (errB != null) {
222
- this.stop();
223
- throw errB;
387
+ if (cycle && this.#current === cycle) {
388
+ this.#last = cycle;
389
+ this.#current = null;
224
390
  }
225
- this.plan(false);
226
- }
227
- restart(reset) {
228
- this.stop(reset);
229
- return this.start(reset);
391
+ this.#schedule(false);
392
+ };
393
+ toJSON() {
394
+ const { interval, offset, align, state, current, last, countdown, potential, nextAt, nextId } = this;
395
+ return { interval, offset, align, state, current, last, countdown, potential, nextAt, nextId };
230
396
  }
231
397
  };
232
398
 
233
- // src/Pulse.js
399
+ // src/class/Pulse.js
234
400
  var Pulse = class {
235
- /**
236
- * Create a new Pulse instance.
237
- * @param {PulseOptions} [options={}] See {@link PulseOptions}.
238
- */
401
+ #opt;
402
+ #session = null;
403
+ #nextId = 0;
404
+ #last;
239
405
  constructor(options = {}) {
240
- const _p = new PulsePrivate(this, options);
241
- const { getNow, align, interval, offset, autoStart } = _p.opt;
242
- (0, import_props3.virtuals)(this, {
243
- state: (_) => _p.state,
244
- last: (_) => _p.last,
245
- countdown: (_) => _p.getCountdown(),
246
- potential: (_) => _p.getCountdown() ?? _p.getPotential(),
247
- nextAt: (_) => _p.state ? this.potential + getNow() : void 0
248
- });
249
- (0, import_props3.solids)(this, {
250
- interval,
251
- offset,
252
- align
253
- });
254
- _pulses.set(this, _p);
255
- if (autoStart) {
406
+ this.#opt = formatOptions(options);
407
+ if (this.#opt.autoStart) {
256
408
  this.start();
257
409
  }
258
410
  }
259
- /**
260
- * Start the pulse loop (no-op if already running).
261
- * @returns {boolean} `true` if it started now, `false` if it was already running.
262
- */
263
- start(reset = false) {
264
- return _pulses.get(this).start(reset);
411
+ get session() {
412
+ return this.#session;
413
+ }
414
+ get state() {
415
+ return this.#session?.state ?? false;
265
416
  }
266
- /**
267
- * Stop the pulse loop (no-op if already stopped).
268
- * @returns {boolean} `true` if it stopped now, `false` if it was already stopped.
269
- */
270
- stop(reset = false) {
271
- return _pulses.get(this).stop(reset);
417
+ get current() {
418
+ return this.#session?.current;
272
419
  }
273
- restart(reset = false) {
274
- return _pulses.get(this).restart(reset);
420
+ get last() {
421
+ return this.#session?.last ?? this.#last;
422
+ }
423
+ get countdown() {
424
+ return this.#session?.countdown;
425
+ }
426
+ get potential() {
427
+ return this.countdown ?? this.calculatePotential();
428
+ }
429
+ get nextAt() {
430
+ return this.state ? this.potential + this.#opt.getNow() : void 0;
431
+ }
432
+ get nextId() {
433
+ return this.#nextId;
434
+ }
435
+ get interval() {
436
+ return this.#opt.interval;
437
+ }
438
+ get offset() {
439
+ return this.#opt.offset;
440
+ }
441
+ get align() {
442
+ return this.#opt.align;
443
+ }
444
+ calculatePotential(isInit = true) {
445
+ const { align, runOnStart, interval, offset, getNow } = this.#opt;
446
+ if (isInit && runOnStart) {
447
+ return 0;
448
+ }
449
+ if (!align) {
450
+ return isInit ? interval + offset : interval;
451
+ }
452
+ const now = getNow() - offset;
453
+ const rest = (now % interval + interval) % interval;
454
+ return rest === 0 ? interval : interval - rest;
455
+ }
456
+ #handleError(error) {
457
+ return handleError(this, error, this.#opt.onError);
458
+ }
459
+ #consumeNextId = () => this.#nextId++;
460
+ /** Start a new session. Returns false if already running. */
461
+ start() {
462
+ if (this.state) {
463
+ return false;
464
+ }
465
+ const session = new PulseSession(this, this.#opt, this.#consumeNextId);
466
+ this.#session = session;
467
+ try {
468
+ return session.start();
469
+ } catch (err) {
470
+ if (this.#session === session) {
471
+ this.#session = null;
472
+ }
473
+ this.#handleError(err);
474
+ throw err;
475
+ }
476
+ }
477
+ /** Stop the current session, without cancelling a newer one. */
478
+ stop(reason) {
479
+ const session = this.#session;
480
+ if (!session?.state) {
481
+ return false;
482
+ }
483
+ this.#session = null;
484
+ this.#last = session.last ?? this.#last;
485
+ return session.stop(reason);
486
+ }
487
+ restart() {
488
+ this.stop();
489
+ return this.start();
490
+ }
491
+ toJSON() {
492
+ const { interval, offset, align, state, current, last, countdown, potential, nextAt, nextId } = this;
493
+ return { interval, offset, align, state, current, last, countdown, potential, nextAt, nextId };
275
494
  }
276
495
  reset() {
277
- return _pulses.get(this).reset();
496
+ throw new Error("[pulse] reset() is obsolete");
278
497
  }
279
498
  };
280
499
 
281
500
  // src/index.js
282
501
  var createPulse = (options = {}) => new Pulse(options);
283
- var stopAllPulses = () => {
284
- for (const [pulse, _pulse] of _pulses.entries()) {
285
- pulse.stop();
286
- }
287
- };
288
502
  var index_default = createPulse;
289
- //# sourceMappingURL=index.js.map
503
+ // Annotate the CommonJS export names for ESM import in node:
504
+ 0 && (module.exports = {
505
+ Pulse,
506
+ PulseCycle,
507
+ PulseError,
508
+ PulseExecuteError,
509
+ PulseRuntimeError,
510
+ PulseScheduleError,
511
+ PulseSession,
512
+ PulseStartError,
513
+ PulseStopError,
514
+ createPulse,
515
+ handleError
516
+ });
517
+ //# sourceMappingURL=index.cjs.map