smart-home-engine 2.0.1 → 3.0.0-beta.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/dist/web/assets/index-BZWF5ORr.js +394 -0
- package/dist/web/assets/{index-Bu5o4KXc.css → index-CdLKmsX4.css} +1 -1
- package/dist/web/assets/{tsMode-20i8zomj.js → tsMode-BquNtbO5.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/src/config.js +21 -1
- package/src/index.js +532 -86
- package/src/lib/call-site.js +51 -0
- package/src/lib/command-topics.js +162 -0
- package/src/lib/command.js +188 -0
- package/src/lib/holidays.js +188 -0
- package/src/lib/publish-changed.js +45 -0
- package/src/lib/runtime.js +350 -0
- package/src/lib/script-timers.js +49 -7
- package/src/lib/time.js +402 -0
- package/src/lib/timing.js +421 -0
- package/src/lib/update-channel.js +152 -0
- package/src/sandbox/stdlib.js +14 -0
- package/src/web/ai-tools.js +7 -3
- package/src/web/config-api.js +4 -3
- package/src/web/log-ws.js +49 -2
- package/src/web/prompts/api-ref-compact.md +6 -3
- package/src/web/prompts/api-ref.md +45 -4
- package/src/web/runtime-api.js +66 -0
- package/src/web/server.js +51 -19
- package/dist/web/assets/index-CqYTIaIG.js +0 -306
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* The daemon's runtime model: what every script has registered — subscriptions, variable listeners,
|
|
4
|
+
* schedules, sun events, timers (grouped by kind, name and the line that created them) and pending
|
|
5
|
+
* commands — with per-handler statistics, the timers pending under each handler, and a ring buffer
|
|
6
|
+
* of the daemon's gauges for the last hour.
|
|
7
|
+
*
|
|
8
|
+
* The Runtime page reads the whole model about once a second over the WebSocket while it is open;
|
|
9
|
+
* the script editor's Runtime panel reads one script's part of it. Neither changes the model: the
|
|
10
|
+
* actions (cancel a timer, run it now, run a schedule once) go through the closures the
|
|
11
|
+
* registration site stored on the entry, so the model never reaches into the sandbox itself.
|
|
12
|
+
*
|
|
13
|
+
* Statistics are kept since the script's load and as a sliding five-minute window of thirty 10 s
|
|
14
|
+
* buckets, so a handler firing a hundred times a second costs thirty small objects, not a list of
|
|
15
|
+
* every fire.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const WINDOW_MS = 5 * 60 * 1000;
|
|
19
|
+
const BUCKET_MS = 10_000;
|
|
20
|
+
const BUCKETS = WINDOW_MS / BUCKET_MS;
|
|
21
|
+
|
|
22
|
+
/** Fires, errors and time of one handler: totals since load and the last five minutes. */
|
|
23
|
+
class HandlerStats {
|
|
24
|
+
constructor() {
|
|
25
|
+
this.fires = 0;
|
|
26
|
+
this.errors = 0;
|
|
27
|
+
this.totalMs = 0;
|
|
28
|
+
this.maxMs = 0;
|
|
29
|
+
this.lastFire = null;
|
|
30
|
+
this.lastMs = null;
|
|
31
|
+
this.lastError = null;
|
|
32
|
+
this._buckets = [];
|
|
33
|
+
for (let i = 0; i < BUCKETS; i++) this._buckets.push({ start: 0, fires: 0, errors: 0, ms: 0 });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
_bucket(now) {
|
|
37
|
+
const start = Math.floor(now / BUCKET_MS) * BUCKET_MS;
|
|
38
|
+
const b = this._buckets[(start / BUCKET_MS) % BUCKETS];
|
|
39
|
+
if (b.start !== start) {
|
|
40
|
+
b.start = start;
|
|
41
|
+
b.fires = 0;
|
|
42
|
+
b.errors = 0;
|
|
43
|
+
b.ms = 0;
|
|
44
|
+
}
|
|
45
|
+
return b;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* One callback run.
|
|
50
|
+
* @param {number} ms how long it took
|
|
51
|
+
* @param {boolean} [failed] it threw or rejected
|
|
52
|
+
* @param {number} [now]
|
|
53
|
+
*/
|
|
54
|
+
record(ms, failed = false, now = Date.now()) {
|
|
55
|
+
this.fires++;
|
|
56
|
+
this.totalMs += ms;
|
|
57
|
+
if (ms > this.maxMs) this.maxMs = ms;
|
|
58
|
+
this.lastFire = now;
|
|
59
|
+
this.lastMs = ms;
|
|
60
|
+
const b = this._bucket(now);
|
|
61
|
+
b.fires++;
|
|
62
|
+
b.ms += ms;
|
|
63
|
+
if (failed) {
|
|
64
|
+
this.errors++;
|
|
65
|
+
this.lastError = now;
|
|
66
|
+
b.errors++;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** An error that showed up after the run was recorded (a rejected promise). */
|
|
71
|
+
fail(now = Date.now()) {
|
|
72
|
+
this.errors++;
|
|
73
|
+
this.lastError = now;
|
|
74
|
+
this._bucket(now).errors++;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The last five minutes. */
|
|
78
|
+
recent(now = Date.now()) {
|
|
79
|
+
const since = now - WINDOW_MS;
|
|
80
|
+
const r = { fires: 0, errors: 0, totalMs: 0 };
|
|
81
|
+
for (const b of this._buckets) {
|
|
82
|
+
if (b.start <= since || b.start > now) continue;
|
|
83
|
+
r.fires += b.fires;
|
|
84
|
+
r.errors += b.errors;
|
|
85
|
+
r.totalMs += b.ms;
|
|
86
|
+
}
|
|
87
|
+
return r;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
toJSON(now = Date.now()) {
|
|
91
|
+
return {
|
|
92
|
+
fires: this.fires,
|
|
93
|
+
errors: this.errors,
|
|
94
|
+
totalMs: Math.round(this.totalMs * 100) / 100,
|
|
95
|
+
maxMs: Math.round(this.maxMs * 100) / 100,
|
|
96
|
+
lastFire: this.lastFire,
|
|
97
|
+
lastMs: this.lastMs === null ? null : Math.round(this.lastMs * 100) / 100,
|
|
98
|
+
lastError: this.lastError,
|
|
99
|
+
recent: (() => {
|
|
100
|
+
const r = this.recent(now);
|
|
101
|
+
r.totalMs = Math.round(r.totalMs * 100) / 100;
|
|
102
|
+
return r;
|
|
103
|
+
})(),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The daemon's gauges, one sample per interval, for the last hour. */
|
|
109
|
+
class GaugeHistory {
|
|
110
|
+
/**
|
|
111
|
+
* @param {object} [opts]
|
|
112
|
+
* @param {number} [opts.intervalMs=10000] how often a sample is kept
|
|
113
|
+
* @param {number} [opts.size=360] samples kept (360 × 10 s = one hour)
|
|
114
|
+
*/
|
|
115
|
+
constructor(opts = {}) {
|
|
116
|
+
this.intervalMs = opts.intervalMs || 10_000;
|
|
117
|
+
this.size = opts.size || 360;
|
|
118
|
+
this._samples = [];
|
|
119
|
+
this._lastKept = null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Offer a sample; it is kept when the interval since the last kept one has passed.
|
|
124
|
+
* @param {object} sample must carry `ts`
|
|
125
|
+
* @returns {boolean} kept
|
|
126
|
+
*/
|
|
127
|
+
push(sample) {
|
|
128
|
+
if (this._lastKept !== null && sample.ts - this._lastKept < this.intervalMs) return false;
|
|
129
|
+
this._lastKept = sample.ts;
|
|
130
|
+
this._samples.push(sample);
|
|
131
|
+
if (this._samples.length > this.size) this._samples.shift();
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
list() {
|
|
136
|
+
return this._samples.slice();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
class NotFoundError extends Error {
|
|
141
|
+
constructor(id) {
|
|
142
|
+
super(`unknown runtime id ${id} — the script may have been reloaded`);
|
|
143
|
+
this.code = 'ENOENT';
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The registry of handlers and their pending timers.
|
|
149
|
+
*/
|
|
150
|
+
class Runtime {
|
|
151
|
+
constructor() {
|
|
152
|
+
this._nextId = 1;
|
|
153
|
+
/** @type {Map<string, object>} handler id → handler */
|
|
154
|
+
this.handlers = new Map();
|
|
155
|
+
/** @type {Map<string, object>} pending id → pending (timer, command) */
|
|
156
|
+
this.pending = new Map();
|
|
157
|
+
/** @type {Map<string, Set<object>>} script file → its handlers */
|
|
158
|
+
this.byScript = new Map();
|
|
159
|
+
/** @type {Map<string, object>} `${script}\0${group key}` → timer group handler */
|
|
160
|
+
this._groups = new Map();
|
|
161
|
+
this.gauges = new GaugeHistory();
|
|
162
|
+
this.latestGauges = null;
|
|
163
|
+
this.commandsFailed = 0;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Register a handler.
|
|
168
|
+
* @param {string} script the script file
|
|
169
|
+
* @param {object} info
|
|
170
|
+
* @param {string} info.kind `sub`, `var`, `schedule`, `sun`, or a timer kind
|
|
171
|
+
* @param {string} [info.name] what the UI shows: the topic, the pattern, the timer's name
|
|
172
|
+
* @param {string} [info.detail] a second line: the options, the other topics
|
|
173
|
+
* @param {number|null} [info.line] the script line that registered it
|
|
174
|
+
* @param {Function} [info.run] runs the handler once now (a schedule, an interval)
|
|
175
|
+
* @param {Function} [info.cancel] removes the handler (an interval)
|
|
176
|
+
* @param {Function} [info.next] → ms timestamp of the next run, or null
|
|
177
|
+
* @returns {object} the handler
|
|
178
|
+
*/
|
|
179
|
+
handler(script, info) {
|
|
180
|
+
const h = {
|
|
181
|
+
id: 'h' + this._nextId++,
|
|
182
|
+
script,
|
|
183
|
+
kind: info.kind,
|
|
184
|
+
name: info.name ?? null,
|
|
185
|
+
detail: info.detail ?? null,
|
|
186
|
+
line: info.line ?? null,
|
|
187
|
+
stats: new HandlerStats(),
|
|
188
|
+
pending: new Map(),
|
|
189
|
+
run: info.run || null,
|
|
190
|
+
cancel: info.cancel || null,
|
|
191
|
+
next: info.next || null,
|
|
192
|
+
groupKey: info.groupKey || null,
|
|
193
|
+
};
|
|
194
|
+
this.handlers.set(h.id, h);
|
|
195
|
+
let set = this.byScript.get(script);
|
|
196
|
+
if (!set) {
|
|
197
|
+
set = new Set();
|
|
198
|
+
this.byScript.set(script, set);
|
|
199
|
+
}
|
|
200
|
+
set.add(h);
|
|
201
|
+
return h;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* The group handler for timers of one kind, name and line in a script — found or created. Its
|
|
206
|
+
* statistics accumulate over every timer that fired under it; the timers pending right now are
|
|
207
|
+
* its `pending`.
|
|
208
|
+
* @param {string} script
|
|
209
|
+
* @param {{kind: string, name?: string, line?: number|null}} meta
|
|
210
|
+
*/
|
|
211
|
+
timerGroup(script, meta) {
|
|
212
|
+
const key = `${meta.kind}\0${meta.name ?? ''}\0${meta.line ?? ''}`;
|
|
213
|
+
const mapKey = `${script}\0${key}`;
|
|
214
|
+
let h = this._groups.get(mapKey);
|
|
215
|
+
if (!h) {
|
|
216
|
+
h = this.handler(script, { kind: meta.kind, name: meta.name, line: meta.line, groupKey: key });
|
|
217
|
+
this._groups.set(mapKey, h);
|
|
218
|
+
}
|
|
219
|
+
return h;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* A timer (or command) pending under a handler.
|
|
224
|
+
* @param {object} handler
|
|
225
|
+
* @param {object} info
|
|
226
|
+
* @param {number} info.due ms timestamp when it fires
|
|
227
|
+
* @param {number} [info.start] ms timestamp when it was armed (default now)
|
|
228
|
+
* @param {number} [info.every] an interval's period
|
|
229
|
+
* @param {Function} [info.fire] fires it now (removes it, runs the callback)
|
|
230
|
+
* @param {Function} [info.cancel] removes it without running
|
|
231
|
+
* @param {object} [info.extra] shown as is (a command's topic, payload, tries)
|
|
232
|
+
* @returns {object} the pending entry
|
|
233
|
+
*/
|
|
234
|
+
addPending(handler, info) {
|
|
235
|
+
const p = {
|
|
236
|
+
id: 't' + this._nextId++,
|
|
237
|
+
handler,
|
|
238
|
+
due: info.due,
|
|
239
|
+
start: info.start ?? Date.now(),
|
|
240
|
+
every: info.every ?? null,
|
|
241
|
+
fire: info.fire || null,
|
|
242
|
+
cancel: info.cancel || null,
|
|
243
|
+
extra: info.extra || null,
|
|
244
|
+
};
|
|
245
|
+
handler.pending.set(p.id, p);
|
|
246
|
+
this.pending.set(p.id, p);
|
|
247
|
+
return p;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** A pending entry fired or was cleared. */
|
|
251
|
+
removePending(p) {
|
|
252
|
+
if (!p) return;
|
|
253
|
+
p.handler.pending.delete(p.id);
|
|
254
|
+
this.pending.delete(p.id);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** A handler is gone (the subscription removed, the interval cleared). */
|
|
258
|
+
removeHandler(h) {
|
|
259
|
+
if (!h || !this.handlers.has(h.id)) return;
|
|
260
|
+
for (const p of h.pending.values()) this.pending.delete(p.id);
|
|
261
|
+
h.pending.clear();
|
|
262
|
+
this.handlers.delete(h.id);
|
|
263
|
+
this.byScript.get(h.script)?.delete(h);
|
|
264
|
+
if (h.groupKey) this._groups.delete(`${h.script}\0${h.groupKey}`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Everything a script registered — on unload. */
|
|
268
|
+
removeScript(script) {
|
|
269
|
+
const set = this.byScript.get(script);
|
|
270
|
+
if (!set) return;
|
|
271
|
+
for (const h of [...set]) this.removeHandler(h);
|
|
272
|
+
this.byScript.delete(script);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* An action from the editor: `cancel` or `run` on a pending timer, `run` (once) or `cancel` on a
|
|
277
|
+
* handler that offers it.
|
|
278
|
+
* @param {string} id
|
|
279
|
+
* @param {'cancel'|'run'} action
|
|
280
|
+
* @returns {{script: string, kind: string, name: string|null, what: 'timer'|'handler'}}
|
|
281
|
+
*/
|
|
282
|
+
action(id, action) {
|
|
283
|
+
if (action !== 'cancel' && action !== 'run') throw new TypeError(`unknown action ${action}`);
|
|
284
|
+
const p = this.pending.get(id);
|
|
285
|
+
if (p) {
|
|
286
|
+
const fn = action === 'run' ? p.fire : p.cancel;
|
|
287
|
+
if (!fn) throw new TypeError(`${action} is not possible for a pending ${p.handler.kind}`);
|
|
288
|
+
fn();
|
|
289
|
+
return { script: p.handler.script, kind: p.handler.kind, name: p.handler.name, what: 'timer' };
|
|
290
|
+
}
|
|
291
|
+
const h = this.handlers.get(id);
|
|
292
|
+
if (h) {
|
|
293
|
+
const fn = action === 'run' ? h.run : h.cancel;
|
|
294
|
+
if (!fn) throw new TypeError(`${action} is not possible for a ${h.kind}`);
|
|
295
|
+
fn();
|
|
296
|
+
return { script: h.script, kind: h.kind, name: h.name, what: 'handler' };
|
|
297
|
+
}
|
|
298
|
+
throw new NotFoundError(id);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* The model as JSON.
|
|
303
|
+
* @param {object} [opts]
|
|
304
|
+
* @param {string} [opts.script] only this script file
|
|
305
|
+
* @param {Function} [opts.label] script file → label shown in the UI
|
|
306
|
+
* @param {Function} [opts.origin] script file → `builtin` | `user`
|
|
307
|
+
* @param {number} [opts.now]
|
|
308
|
+
*/
|
|
309
|
+
snapshot(opts = {}) {
|
|
310
|
+
const now = opts.now ?? Date.now();
|
|
311
|
+
const label = opts.label || ((f) => f);
|
|
312
|
+
const origin = opts.origin || (() => 'user');
|
|
313
|
+
const scripts = [];
|
|
314
|
+
for (const [file, set] of this.byScript) {
|
|
315
|
+
if (opts.script !== undefined && file !== opts.script) continue;
|
|
316
|
+
const handlers = [];
|
|
317
|
+
for (const h of set) {
|
|
318
|
+
let next = null;
|
|
319
|
+
if (h.next) {
|
|
320
|
+
try {
|
|
321
|
+
next = h.next();
|
|
322
|
+
} catch {
|
|
323
|
+
next = null;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
const pending = [];
|
|
327
|
+
for (const p of h.pending.values()) {
|
|
328
|
+
pending.push({ id: p.id, due: p.due, start: p.start, every: p.every, ...(p.extra || {}) });
|
|
329
|
+
}
|
|
330
|
+
pending.sort((a, b) => a.due - b.due);
|
|
331
|
+
handlers.push({
|
|
332
|
+
id: h.id,
|
|
333
|
+
kind: h.kind,
|
|
334
|
+
name: h.name,
|
|
335
|
+
detail: h.detail,
|
|
336
|
+
line: h.line,
|
|
337
|
+
next,
|
|
338
|
+
actions: { run: !!h.run, cancel: !!h.cancel },
|
|
339
|
+
...h.stats.toJSON(now),
|
|
340
|
+
pending,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
scripts.push({ file, label: label(file), origin: origin(file), handlers });
|
|
344
|
+
}
|
|
345
|
+
scripts.sort((a, b) => a.label.localeCompare(b.label));
|
|
346
|
+
return { ts: now, gauges: this.latestGauges, commandsFailed: this.commandsFailed, scripts };
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
module.exports = { Runtime, HandlerStats, GaugeHistory, NotFoundError, WINDOW_MS, BUCKET_MS };
|
package/src/lib/script-timers.js
CHANGED
|
@@ -7,21 +7,63 @@
|
|
|
7
7
|
* every timeout that ran normally stayed in the set for the life of the daemon — with its callback
|
|
8
8
|
* and everything the callback captured. A script that starts a "switch off after N minutes" timer
|
|
9
9
|
* per event leaked hundreds of them an hour, and the heap ran full after about twelve days.
|
|
10
|
-
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A delay a script passed to a timer API: a finite number of milliseconds, not negative.
|
|
14
|
+
* Node coerces anything else to NaN, warns once per process without naming the caller and fires
|
|
15
|
+
* after 1 ms — a swapped argument order or an undefined config field became a silent 1 ms timer.
|
|
16
|
+
* @param {*} delay
|
|
17
|
+
* @returns {boolean}
|
|
18
|
+
*/
|
|
19
|
+
function isValidDelay(delay) {
|
|
20
|
+
return typeof delay === 'number' && Number.isFinite(delay) && delay >= 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* How an invalid delay is described in the warning: `undefined`, `NaN`, `"5m"`, `an object`.
|
|
25
|
+
* @param {*} delay
|
|
26
|
+
* @returns {string}
|
|
27
|
+
*/
|
|
28
|
+
function describeDelay(delay) {
|
|
29
|
+
if (typeof delay === 'string') return JSON.stringify(delay);
|
|
30
|
+
if (typeof delay === 'function') return 'a function';
|
|
31
|
+
if (delay !== null && typeof delay === 'object') return Array.isArray(delay) ? 'an array' : 'an object';
|
|
32
|
+
return String(delay);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
11
36
|
* @param {Set} timers the script's timer set
|
|
12
37
|
* @param {Function} fn called when the timer fires
|
|
13
|
-
* @param {number} delay ms
|
|
14
|
-
* @param {Function} [
|
|
15
|
-
* @
|
|
38
|
+
* @param {number} delay ms; an invalid delay is reported through `opts.warn` and runs as 0
|
|
39
|
+
* @param {object|Function} [opts] options, or (legacy) the setTimeout function to use
|
|
40
|
+
* @param {Function} [opts.setTimeoutFn] for tests
|
|
41
|
+
* @param {Function} [opts.warn] called with a message when the delay is not a number (the caller
|
|
42
|
+
* adds the script's label); without it an invalid delay is silently 0
|
|
43
|
+
* @param {string} [opts.api] the API name for the warning, default `setTimeout`
|
|
44
|
+
* @param {string} [opts.kind] shown in the timer list: `debounce`, `throttle`, `timeout`, `retry`
|
|
45
|
+
* @param {string} [opts.name] shown in the timer list next to the kind
|
|
46
|
+
* @returns the timer id (what clearTimeout takes); carries `due`, and `kind`/`name` when given
|
|
16
47
|
*/
|
|
17
|
-
function trackTimeout(timers, fn, delay,
|
|
48
|
+
function trackTimeout(timers, fn, delay, opts = {}) {
|
|
49
|
+
if (typeof opts === 'function') opts = { setTimeoutFn: opts };
|
|
50
|
+
const setTimeoutFn = opts.setTimeoutFn || setTimeout;
|
|
51
|
+
if (!isValidDelay(delay)) {
|
|
52
|
+
if (typeof opts.warn === 'function') opts.warn(`${opts.api || 'setTimeout'}: delay is not a number of milliseconds (got ${describeDelay(delay)}), using 0`);
|
|
53
|
+
delay = 0;
|
|
54
|
+
}
|
|
18
55
|
const id = setTimeoutFn(() => {
|
|
19
56
|
timers.delete(id);
|
|
20
57
|
fn();
|
|
21
58
|
}, delay);
|
|
22
|
-
if (id && typeof id === 'object')
|
|
59
|
+
if (id && typeof id === 'object') {
|
|
60
|
+
id.start = Date.now(); // when it was armed — the progress bar of the timer list runs from here
|
|
61
|
+
id.due = id.start + delay; // when it fires (roadmap I28: list_timers)
|
|
62
|
+
if (opts.kind) id.kind = opts.kind;
|
|
63
|
+
if (opts.name) id.name = opts.name;
|
|
64
|
+
}
|
|
23
65
|
timers.add(id);
|
|
24
66
|
return id;
|
|
25
67
|
}
|
|
26
68
|
|
|
27
|
-
module.exports = { trackTimeout };
|
|
69
|
+
module.exports = { trackTimeout, isValidDelay, describeDelay };
|