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