@winkjs/composer 0.5.1 → 0.6.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/package.json +2 -2
- package/src/core/batch/aoo-encoder.js +4 -4
- package/src/core/batch/sliding-window.js +2 -2
- package/src/core/codec/index.js +4 -4
- package/src/core/emitter-manager/mqtt/emitter.js +44 -14
- package/src/core/emitter-manager/terminal/assert-columns.js +1 -1
- package/src/core/emitter-manager/terminal/index.js +4 -4
- package/src/core/env-vars.js +44 -3
- package/src/core/logger/index.js +174 -0
- package/src/core/mqtt-protocol.js +2 -2
- package/src/core/partition-manager/get-stats.js +31 -0
- package/src/core/partition-manager/index.js +1 -0
- package/src/core/partition-manager/init.js +15 -0
- package/src/core/partition-manager/update.js +143 -87
- package/src/core/partition-manager/validate-flow.js +1 -1
- package/src/core/semantics/loader.js +1 -1
- package/src/core/semantics/warnings.js +5 -3
- package/src/core/shutdown-manager/index.js +71 -21
- package/src/core/source-manager/csv/start.js +109 -35
- package/src/core/source-manager/mqtt/client.js +95 -36
- package/src/core/source-manager/mqtt/dedup.js +3 -3
- package/src/core/source-manager/mqtt/index.js +1 -1
- package/src/core/source-manager/mqtt/status.js +67 -14
- package/src/core/source-manager/record-shape.js +55 -0
- package/src/core/source-manager/test-harness/field-generator.js +1 -1
- package/src/core/source-manager/test-harness/start.js +52 -10
- package/src/core/source-manager/test-harness/validate.js +1 -1
- package/src/core/storage-manager/questdb/assert-columns.js +2 -2
- package/src/core/storage-manager/questdb/ensure-tables.js +1 -1
- package/src/core/storage-manager/questdb/index.js +55 -17
- package/src/core/storage-manager/questdb/persist-plan.js +32 -8
- package/src/core/tunable/helpers.js +1 -1
- package/src/core/utils/callback-guard/index.js +155 -0
- package/src/core/utils/half-life/index.js +4 -4
- package/src/core/utils/validate/composer-validators.js +2 -2
- package/src/core/utils/validate/index.js +1 -1
- package/src/core/utils/validate/validate.js +1 -1
- package/src/core/wiring/assert-handle.js +5 -5
- package/src/core/wiring/assert-module.js +2 -2
- package/src/core/wiring/wire-emitters.js +4 -3
- package/src/core/wiring/wire-linear-graph.js +1 -1
- package/src/core/wiring/wire-node.js +6 -6
- package/src/core/wiring/wire-semantics.js +1 -1
- package/src/core/wiring/wire-storages.js +4 -3
- package/src/flow/driver.js +31 -6
- package/src/flow/expand-single-input-array.js +4 -4
- package/src/flow/flow-api-schemas.js +4 -4
- package/src/flow/flow.js +41 -41
- package/src/flow/for-each.js +12 -11
- package/src/flow/get-target-array.js +2 -2
- package/src/flow/load-node-module.js +1 -1
- package/src/flow/make-node-method.js +4 -4
- package/src/flow/resolve-pick-by-field.js +1 -1
- package/src/flow/run.js +176 -38
- package/src/flow/serialize.js +1 -1
- package/src/flow/spec-builder.js +1 -1
- package/src/flow/validate.js +1 -1
- package/src/nodes/butterworth-filter/init.js +5 -4
- package/src/nodes/categorize/update.js +3 -1
- package/src/nodes/controller/update.js +2 -1
- package/src/nodes/dwell-time-tracker/update.js +2 -1
- package/src/nodes/emit-if/helpers.js +57 -11
- package/src/nodes/emit-if/init.js +1 -1
- package/src/nodes/emit-if/update.js +8 -11
- package/src/nodes/page-hinkley/update.js +2 -1
- package/src/nodes/pass-if/update.js +3 -1
- package/src/nodes/persist-if/helpers.js +60 -4
- package/src/nodes/persist-if/init.js +1 -1
- package/src/nodes/persist-if/update.js +8 -16
- package/src/nodes/persistence-check/update.js +2 -1
- package/src/nodes/sanitize/update.js +4 -2
- package/src/nodes/swing-watch/update.js +3 -2
- package/src/nodes/threshold/update.js +2 -1
- package/src/nodes/trend/compute-confidence.js +4 -4
- package/src/nodes/trend/update.js +2 -1
- package/src/nodes/winnow/update.js +3 -1
- package/src/tools/training/features.js +13 -13
- package/src/tools/training/logistic.js +1 -1
- package/src/tools/training/metrics.js +2 -2
- package/src/tools/training/scale.js +2 -2
- package/src/tools/training/validate.js +7 -7
- package/src/core/utils/console-formatter/index.js +0 -207
- package/src/nodes/archive/csv/index.js +0 -6
- package/src/nodes/archive/csv/init.js +0 -36
- package/src/nodes/archive/csv/load.js +0 -70
- package/src/nodes/archive/csv/reset.js +0 -14
- package/src/nodes/archive/csv/simple-parser.js +0 -216
- package/src/nodes/archive/csv/test.js +0 -173
- package/src/nodes/archive/csv/update.js +0 -63
- package/src/nodes/sse-emitter/index.js +0 -7
- package/src/nodes/sse-emitter/init.js +0 -105
- package/src/nodes/sse-emitter/introspect.js +0 -134
- package/src/nodes/sse-emitter/publish-to.js +0 -6
- package/src/nodes/sse-emitter/recompute.js +0 -16
- package/src/nodes/sse-emitter/reset.js +0 -19
- package/src/nodes/sse-emitter/update.js +0 -69
- package/src/nodes/sse-emitter/utils/broadcast-heartbeat.js +0 -36
- package/src/nodes/sse-emitter/utils/build-message.js +0 -31
- package/src/nodes/sse-emitter/utils/connection-manager.js +0 -45
- package/src/nodes/sse-emitter/utils/endpoint-initializer.js +0 -20
- package/src/nodes/sse-emitter/utils/health-monitor.js +0 -38
- package/src/nodes/sse-emitter/utils/qos-handler.js +0 -41
package/src/flow/run.js
CHANGED
|
@@ -17,6 +17,8 @@ import * as partitionManager from '../core/partition-manager/index.js';
|
|
|
17
17
|
import { wireLinearGraph, emitters, storages, assertModuleDurability } from '../core/wiring/index.js';
|
|
18
18
|
import shutdownManager from '../core/shutdown-manager/index.js';
|
|
19
19
|
import { ENV_VARS } from '../core/env-vars.js';
|
|
20
|
+
import { wrapCallback } from '../core/utils/callback-guard/index.js';
|
|
21
|
+
import { logger } from '../core/logger/index.js';
|
|
20
22
|
|
|
21
23
|
/**
|
|
22
24
|
* Wires and starts the pipeline for direct execution.
|
|
@@ -27,7 +29,8 @@ import { ENV_VARS } from '../core/env-vars.js';
|
|
|
27
29
|
* @param {Set} importSet - Set of node names used
|
|
28
30
|
* @param {Object} runtime - Runtime configuration
|
|
29
31
|
* @param {Array} [caseOrder] - Order of case keys (only for multi-specialization)
|
|
30
|
-
* @returns {Promise<Object>} Pipeline handle
|
|
32
|
+
* @returns {Promise<Object>} Pipeline handle: processMessage, shutdown,
|
|
33
|
+
* whenComplete, getStats, flowName, composerState
|
|
31
34
|
*/
|
|
32
35
|
export const runFlow = async function ( flowName, specsOrSpecsByCase, importSet, runtime, caseOrder ) {
|
|
33
36
|
// Determine if this is multi-specialization mode
|
|
@@ -156,10 +159,16 @@ export const runFlow = async function ( flowName, specsOrSpecsByCase, importSet,
|
|
|
156
159
|
// caller that ignores the Promise (a push source such as the MQTT
|
|
157
160
|
// subscriber) can never see messages update a partition out of order.
|
|
158
161
|
//
|
|
159
|
-
// The caller contract (ADR-013) is unchanged: `undefined` on
|
|
160
|
-
// path, a Promise on the yield tick; `await` works with both,
|
|
161
|
-
// Promise never rejects
|
|
162
|
-
//
|
|
162
|
+
// The caller contract (ADR-013) is unchanged in shape: `undefined` on
|
|
163
|
+
// the hot path, a Promise on the yield tick; `await` works with both,
|
|
164
|
+
// and the Promise never rejects. Fault delivery depends on the flow
|
|
165
|
+
// kind (ADR-018 — the runtime owns per-message dispatch failure). With
|
|
166
|
+
// a source present, a pipeline fault is contained here: the message is
|
|
167
|
+
// skipped, one red MESSAGE_HANDLER_FAILED travels the status channel,
|
|
168
|
+
// and a run of consecutive failures stops the flow in the terminal
|
|
169
|
+
// 'errored' phase. A headless flow (no source) has no status channel,
|
|
170
|
+
// so the fault throws synchronously to the caller — the headless
|
|
171
|
+
// driver's catch and `failed` counter are that path's containment.
|
|
163
172
|
//
|
|
164
173
|
// The default threshold comes from YIELD_TIME_THRESHOLD_MS (500 ms);
|
|
165
174
|
// `.yield( { threshold } )` overrides it per flow, and Infinity turns
|
|
@@ -187,26 +196,10 @@ export const runFlow = async function ( flowName, specsOrSpecsByCase, importSet,
|
|
|
187
196
|
|
|
188
197
|
const partitionState = composerState.partitionState;
|
|
189
198
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
// (or null for a dropped message, which never sets the flag).
|
|
195
|
-
if ( graph ) {
|
|
196
|
-
runPipeline( graph, msg );
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// Breathe after (yield tick, rare): hand the awaiting caller a
|
|
200
|
-
// Promise that resolves once the event loop has had a full turn.
|
|
201
|
-
if ( partitionState.yieldPending ) {
|
|
202
|
-
partitionState.yieldPending = false;
|
|
203
|
-
return new Promise( ( resolve ) => {
|
|
204
|
-
setImmediate( resolve );
|
|
205
|
-
} );
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
return undefined;
|
|
209
|
-
};
|
|
199
|
+
// The dispatch function itself is defined AFTER the shutdown
|
|
200
|
+
// closure below — its escalation path drains the flow, so it
|
|
201
|
+
// needs `shutdown` in scope. No message can arrive before then:
|
|
202
|
+
// the source only receives processMessage at start, further down.
|
|
210
203
|
|
|
211
204
|
// 6. Setup the flow's shutdown closure.
|
|
212
205
|
//
|
|
@@ -272,8 +265,8 @@ export const runFlow = async function ( flowName, specsOrSpecsByCase, importSet,
|
|
|
272
265
|
await stage();
|
|
273
266
|
} catch ( err ) {
|
|
274
267
|
stageErrors.push( err );
|
|
275
|
-
|
|
276
|
-
`
|
|
268
|
+
logger.error(
|
|
269
|
+
`winkComposer/flow: ${label} drain stage failed [${( err && err.code ) || 'UNKNOWN'}]: ` +
|
|
277
270
|
`${( err && err.message ) || String( err )} — later drain stages still ran`
|
|
278
271
|
);
|
|
279
272
|
}
|
|
@@ -311,6 +304,93 @@ export const runFlow = async function ( flowName, specsOrSpecsByCase, importSet,
|
|
|
311
304
|
return shutdownPromise;
|
|
312
305
|
};
|
|
313
306
|
|
|
307
|
+
// Run the drain with its rejection observed. Both fire-and-forget
|
|
308
|
+
// call sites use this: the natural-completion trigger and the
|
|
309
|
+
// consecutive-failure escalation. drainAll rethrows the first
|
|
310
|
+
// stage error; with no awaiting caller that rejection would end
|
|
311
|
+
// the process as unhandled. The classified log keeps the loss
|
|
312
|
+
// loud without the crash, and a caller that later awaits
|
|
313
|
+
// `handle.shutdown()` still receives the memoized rejection.
|
|
314
|
+
const observedShutdown = function () {
|
|
315
|
+
shutdown().catch( function ( err ) {
|
|
316
|
+
logger.error(
|
|
317
|
+
`winkComposer/flow '${flowName}': shutdown failed ` +
|
|
318
|
+
`[${( err && err.code ) || 'UNKNOWN'}]: ${( err && err.message ) || String( err )}`
|
|
319
|
+
);
|
|
320
|
+
} );
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
// ── Message dispatch with fault containment (ADR-018: the flow
|
|
324
|
+
// runtime owns per-message dispatch failure at this chokepoint) ──
|
|
325
|
+
// All containment state is flow-scoped policy, kept in this
|
|
326
|
+
// closure beside the shutdown state — never on the handle-exposed
|
|
327
|
+
// composerState. `reportDispatchFailure` stays null for a headless
|
|
328
|
+
// flow (no source); the source block below assigns it before the
|
|
329
|
+
// source ever receives processMessage.
|
|
330
|
+
let consecutiveFailures = 0;
|
|
331
|
+
let errored = false;
|
|
332
|
+
let erroredDropReported = false;
|
|
333
|
+
let reportDispatchFailure = null;
|
|
334
|
+
|
|
335
|
+
const processMessage = function ( msg ) {
|
|
336
|
+
// Terminal 'errored': drop everything. The first drop is
|
|
337
|
+
// reported once, classified; a flood of per-drop reports
|
|
338
|
+
// would bury the one terminal red that matters.
|
|
339
|
+
if ( errored ) {
|
|
340
|
+
if ( !erroredDropReported ) {
|
|
341
|
+
erroredDropReported = true;
|
|
342
|
+
logger.error(
|
|
343
|
+
`winkComposer/flow '${flowName}': flow is in the terminal 'errored' ` +
|
|
344
|
+
'phase [MESSAGE_HANDLER_FAILED] — dropping this and all further messages'
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
return undefined;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Process first — pmUpdate always returns the graph synchronously
|
|
351
|
+
// (or null for a dropped message, which never sets the flag).
|
|
352
|
+
// The guard covers pmUpdate too: a first message for a new asset
|
|
353
|
+
// runs every node's init lazily, so an init throw surfaces here.
|
|
354
|
+
try {
|
|
355
|
+
const graph = partitionManager.update( composerState, msg );
|
|
356
|
+
if ( graph ) {
|
|
357
|
+
runPipeline( graph, msg );
|
|
358
|
+
// Reset on success only — a dropped message is neither
|
|
359
|
+
// a success nor a failure. Branch-guarded so the
|
|
360
|
+
// healthy steady state never writes.
|
|
361
|
+
if ( consecutiveFailures !== 0 ) {
|
|
362
|
+
consecutiveFailures = 0;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
} catch ( err ) {
|
|
366
|
+
if ( reportDispatchFailure === null ) {
|
|
367
|
+
// Headless: the caller is the reporting channel — the
|
|
368
|
+
// driver's catch and `failed` counter own the fault.
|
|
369
|
+
throw err;
|
|
370
|
+
}
|
|
371
|
+
consecutiveFailures += 1;
|
|
372
|
+
const isTerminal = consecutiveFailures >= ENV_VARS.messageFailureThreshold;
|
|
373
|
+
reportDispatchFailure( err, isTerminal );
|
|
374
|
+
if ( isTerminal ) {
|
|
375
|
+
errored = true;
|
|
376
|
+
observedShutdown();
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Breathe after (yield tick, rare): hand the awaiting caller a
|
|
381
|
+
// Promise that resolves once the event loop has had a full turn.
|
|
382
|
+
// Runs on the catch path too — pmUpdate may have set the flag
|
|
383
|
+
// before the pipeline threw, and the breath must not be lost.
|
|
384
|
+
if ( partitionState.yieldPending ) {
|
|
385
|
+
partitionState.yieldPending = false;
|
|
386
|
+
return new Promise( ( resolve ) => {
|
|
387
|
+
setImmediate( resolve );
|
|
388
|
+
} );
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return undefined;
|
|
392
|
+
};
|
|
393
|
+
|
|
314
394
|
// 7. Start source (singleton).
|
|
315
395
|
if ( runtime.source ) {
|
|
316
396
|
const { adapter, config } = runtime.source;
|
|
@@ -321,17 +401,42 @@ export const runFlow = async function ( flowName, specsOrSpecsByCase, importSet,
|
|
|
321
401
|
// too (they bypass the DSL entirely).
|
|
322
402
|
assertModuleDurability( adapter.id || 'source', adapter );
|
|
323
403
|
|
|
404
|
+
// The callback guard below turns a non-function into null
|
|
405
|
+
// (absent). Validate raw first, so a misconfigured handler
|
|
406
|
+
// fails loudly at setup instead of silently vanishing. Placed
|
|
407
|
+
// here for the same reason as the durability check above:
|
|
408
|
+
// direct runFlow() callers bypass the DSL schema.
|
|
409
|
+
if ( config.onStatus !== undefined && config.onStatus !== null && typeof config.onStatus !== 'function' ) {
|
|
410
|
+
const err = new Error( `winkComposer/flow '${flowName}': onStatus must be a function` );
|
|
411
|
+
err.code = 'INVALID_CONFIG';
|
|
412
|
+
throw err;
|
|
413
|
+
}
|
|
414
|
+
|
|
324
415
|
// Wrap user's onStatus so the runtime can spot the natural-
|
|
325
416
|
// completion signal without taking it away from the user.
|
|
326
417
|
// When the source signals `phase: 'complete'`:
|
|
327
418
|
// 1. Resolve the whenComplete Promise so callers can stop
|
|
328
419
|
// waiting for natural completion.
|
|
329
|
-
// 2. Trigger the drain
|
|
330
|
-
//
|
|
331
|
-
|
|
420
|
+
// 2. Trigger the drain via observedShutdown — fire-and-forget
|
|
421
|
+
// with its rejection logged; shutdown is concurrent-safe
|
|
422
|
+
// and stages have their own timeouts.
|
|
423
|
+
// The user's handler is armed by the shared callback guard
|
|
424
|
+
// (ADR-018: a misbehaving user callback never interrupts the
|
|
425
|
+
// runtime that called it). Null when the flow has no handler,
|
|
426
|
+
// so the classified fallback branch below keeps red statuses
|
|
427
|
+
// loud.
|
|
428
|
+
const safeUserOnStatus = wrapCallback( config.onStatus, {
|
|
429
|
+
name: 'onStatus',
|
|
430
|
+
severity: 'red',
|
|
431
|
+
report: function ( severity, name, detail ) {
|
|
432
|
+
logger.error(
|
|
433
|
+
`winkComposer/flow '${flowName}': user callback ${name} failed [CALLBACK_FAILED]: ${detail}`
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
} );
|
|
332
437
|
const wrappedOnStatus = function ( s ) {
|
|
333
|
-
if (
|
|
334
|
-
|
|
438
|
+
if ( safeUserOnStatus ) {
|
|
439
|
+
safeUserOnStatus( s );
|
|
335
440
|
} else if ( s && s.status === 'red' ) {
|
|
336
441
|
// No user handler: log the failure here, classified. The
|
|
337
442
|
// source's own console fallback fires only when NO handler
|
|
@@ -341,13 +446,39 @@ export const runFlow = async function ( flowName, specsOrSpecsByCase, importSet,
|
|
|
341
446
|
const code = ( s.error && s.error.code ) || 'UNKNOWN';
|
|
342
447
|
const message = ( s.error && s.error.message ) ||
|
|
343
448
|
'source reported status red with no error detail';
|
|
344
|
-
|
|
449
|
+
logger.error( `winkComposer/flow '${flowName}': source error [${code}]: ${message}` );
|
|
345
450
|
}
|
|
451
|
+
// Completion bookkeeping runs after — and independently of —
|
|
452
|
+
// the user's reporter. A throwing onStatus must never swallow
|
|
453
|
+
// the flow's own completion: whenComplete waiters would hang
|
|
454
|
+
// and the source would never stop.
|
|
346
455
|
if ( s && s.phase === 'complete' ) {
|
|
347
456
|
resolveWhenComplete();
|
|
348
|
-
|
|
457
|
+
observedShutdown();
|
|
349
458
|
}
|
|
350
459
|
};
|
|
460
|
+
|
|
461
|
+
// The dispatch guard's reporting channel. Routing through
|
|
462
|
+
// wrappedOnStatus keeps the two-party rule: a flow without a
|
|
463
|
+
// user handler still gets the classified console fallback for
|
|
464
|
+
// red statuses. No local guard is needed here: the callback
|
|
465
|
+
// guard above contains a throwing user handler, and every other
|
|
466
|
+
// branch of wrappedOnStatus is throw-free.
|
|
467
|
+
reportDispatchFailure = function ( err, isTerminal ) {
|
|
468
|
+
const payload = {
|
|
469
|
+
status: 'red',
|
|
470
|
+
connected: true,
|
|
471
|
+
phase: isTerminal ? 'errored' : 'running',
|
|
472
|
+
error: {
|
|
473
|
+
code: 'MESSAGE_HANDLER_FAILED',
|
|
474
|
+
message: isTerminal ?
|
|
475
|
+
`${ENV_VARS.messageFailureThreshold} consecutive message failures — ` +
|
|
476
|
+
`flow stopped. Last: ${err.message}` :
|
|
477
|
+
`${err.message} — message skipped`
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
wrappedOnStatus( payload );
|
|
481
|
+
};
|
|
351
482
|
stopSource = adapter.start( {
|
|
352
483
|
...config,
|
|
353
484
|
// Override any source-level auto-shutdown setting. The
|
|
@@ -362,10 +493,12 @@ export const runFlow = async function ( flowName, specsOrSpecsByCase, importSet,
|
|
|
362
493
|
onShutdown: shutdown
|
|
363
494
|
} );
|
|
364
495
|
} else {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
//
|
|
368
|
-
//
|
|
496
|
+
// The headless branch: a flow with no source is a documented
|
|
497
|
+
// DSL path (the headless driver feeds it). No source means
|
|
498
|
+
// "nothing to wait for" — resolve immediately so any
|
|
499
|
+
// `await handle.whenComplete()` is a no-op. And with no
|
|
500
|
+
// status channel, `reportDispatchFailure` stays null, so the
|
|
501
|
+
// dispatch guard rethrows pipeline faults to the caller.
|
|
369
502
|
resolveWhenComplete();
|
|
370
503
|
}
|
|
371
504
|
|
|
@@ -384,6 +517,11 @@ export const runFlow = async function ( flowName, specsOrSpecsByCase, importSet,
|
|
|
384
517
|
composerState,
|
|
385
518
|
shutdown,
|
|
386
519
|
processMessage,
|
|
520
|
+
// Cold-path counter snapshot (drops, partitions). Allocates one
|
|
521
|
+
// small object per call; never called by the pipeline itself.
|
|
522
|
+
getStats: function () {
|
|
523
|
+
return partitionManager.getStats( composerState );
|
|
524
|
+
},
|
|
387
525
|
whenComplete: function () {
|
|
388
526
|
return whenCompletePromise;
|
|
389
527
|
}
|
package/src/flow/serialize.js
CHANGED
|
@@ -59,7 +59,7 @@ const serializeValue = function ( value, indent ) {
|
|
|
59
59
|
|
|
60
60
|
if ( t === 'function' ) {
|
|
61
61
|
const funcStr = value.toString();
|
|
62
|
-
if ( funcStr.includes( '[native code]' ) ) throw Error( '
|
|
62
|
+
if ( funcStr.includes( '[native code]' ) ) throw Error( 'winkComposer/flow: only regular or arrow functions are supported.' );
|
|
63
63
|
return funcStr;
|
|
64
64
|
}
|
|
65
65
|
|
package/src/flow/spec-builder.js
CHANGED
|
@@ -11,7 +11,7 @@ const buildStatsFromOutputs = function ( outputs ) {
|
|
|
11
11
|
// overwrite case, caught at build time by check-output-collisions.js.
|
|
12
12
|
if ( seenStoreAs[ storeAs ] ) {
|
|
13
13
|
throw new Error(
|
|
14
|
-
`
|
|
14
|
+
`winkComposer/flow: Node has duplicate storeAs '${storeAs}' ` +
|
|
15
15
|
`for stats '${seenStoreAs[ storeAs ]}' and '${stat}'`
|
|
16
16
|
);
|
|
17
17
|
}
|
package/src/flow/validate.js
CHANGED
|
@@ -254,7 +254,7 @@ export const validateFlowOrThrow = function ( flowName, specs, nodeModules, regi
|
|
|
254
254
|
if ( !result.valid ) {
|
|
255
255
|
const errorList = result.errors.map( ( e ) => ` - ${e}` ).join( '\n' );
|
|
256
256
|
throw new Error(
|
|
257
|
-
`
|
|
257
|
+
`winkComposer/flow '${flowName}': validation failed:\n${errorList}`
|
|
258
258
|
);
|
|
259
259
|
}
|
|
260
260
|
};
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { validateSpec } from '../../core/utils/node/index.js';
|
|
11
11
|
import * as introspect from './introspect.js';
|
|
12
12
|
import { resolveScalar } from '../../core/utils/options/resolve-field-keyed.js';
|
|
13
|
+
import { logger } from '../../core/logger/index.js';
|
|
13
14
|
|
|
14
15
|
const init = function ( spec ) {
|
|
15
16
|
validateSpec( spec, introspect );
|
|
@@ -74,14 +75,14 @@ const init = function ( spec ) {
|
|
|
74
75
|
// Validate the computed cutoff
|
|
75
76
|
if ( cutoffHz <= 0 || cutoffHz >= nyquistHz ) {
|
|
76
77
|
throw new RangeError(
|
|
77
|
-
`Computed cutoff ${cutoffHz.toFixed(1)}Hz is outside valid range (0, ${nyquistHz}Hz)`
|
|
78
|
+
`winkComposer/butterworthFilter: Computed cutoff ${cutoffHz.toFixed(1)}Hz is outside valid range (0, ${nyquistHz}Hz)`
|
|
78
79
|
);
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
// Warn about potential numerical issues
|
|
82
83
|
const normalizedCutoff = cutoffHz / nyquistHz;
|
|
83
84
|
if ( normalizedCutoff < 0.001 && !state.acceptNumericalRisk ) {
|
|
84
|
-
|
|
85
|
+
logger.warn( `winkComposer/butterworthFilter: Very low cutoff (${cutoffHz.toFixed(3)}Hz) may cause numerical instability` );
|
|
85
86
|
}
|
|
86
87
|
|
|
87
88
|
// Store configuration for reference
|
|
@@ -127,14 +128,14 @@ const init = function ( spec ) {
|
|
|
127
128
|
/* c8 ignore next 5 -- defensive: bilinear transform produces stable coefficients for valid Nyquist-bounded cutoff */
|
|
128
129
|
if ( Math.abs( state.a2 ) >= 1 ) {
|
|
129
130
|
throw new Error(
|
|
130
|
-
`Filter unstable: coefficient a2 (${state.a2.toFixed(4)}) is outside stability bounds`
|
|
131
|
+
`winkComposer/butterworthFilter: Filter unstable: coefficient a2 (${state.a2.toFixed(4)}) is outside stability bounds`
|
|
131
132
|
);
|
|
132
133
|
}
|
|
133
134
|
|
|
134
135
|
/* c8 ignore next 5 -- defensive: same Nyquist guard prevents a1 stability violation */
|
|
135
136
|
if ( Math.abs( state.a1 ) >= 1 + state.a2 ) {
|
|
136
137
|
throw new Error(
|
|
137
|
-
`Filter unstable: coefficient a1 (${state.a1.toFixed(4)}) violates stability condition |a1| < 1 + a2`
|
|
138
|
+
`winkComposer/butterworthFilter: Filter unstable: coefficient a1 (${state.a1.toFixed(4)}) violates stability condition |a1| < 1 + a2`
|
|
138
139
|
);
|
|
139
140
|
}
|
|
140
141
|
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* threshold counts (3–5), linear search outperforms binary search.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { logger } from '../../core/logger/index.js';
|
|
10
|
+
|
|
9
11
|
const update = function ( state, msg ) {
|
|
10
12
|
if ( state.disable || state.pause ) return state;
|
|
11
13
|
const xVal = msg[ state.x ];
|
|
@@ -25,7 +27,7 @@ const update = function ( state, msg ) {
|
|
|
25
27
|
} catch ( error ) {
|
|
26
28
|
if ( !state.tunableErrorLogged ) {
|
|
27
29
|
state.tunableErrorLogged = true;
|
|
28
|
-
|
|
30
|
+
logger.error( `winkComposer/${state.nodeType}: tunable threw: ${error.message}` );
|
|
29
31
|
}
|
|
30
32
|
}
|
|
31
33
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// nodes/controller/update.js
|
|
2
2
|
|
|
3
3
|
import { executeTriggers } from '../../core/utils/node/index.js';
|
|
4
|
+
import { logger } from '../../core/logger/index.js';
|
|
4
5
|
|
|
5
6
|
const update = function ( state, msg ) {
|
|
6
7
|
// Track whether any predicate threw during this pass
|
|
@@ -22,7 +23,7 @@ const update = function ( state, msg ) {
|
|
|
22
23
|
// Log first error per episode; suppress subsequent until recovery
|
|
23
24
|
if ( !state.predicateErrorLogged ) {
|
|
24
25
|
state.predicateErrorLogged = true;
|
|
25
|
-
|
|
26
|
+
logger.error( `winkComposer/controller: predicate threw exception: ${error.message}` );
|
|
26
27
|
}
|
|
27
28
|
// Skip to next condition
|
|
28
29
|
continue; // eslint-disable-line no-continue
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// nodes/dwell-time-tracker/update.js
|
|
2
2
|
|
|
3
3
|
import { executeTriggers } from '../../core/utils/node/index.js';
|
|
4
|
+
import { logger } from '../../core/logger/index.js';
|
|
4
5
|
|
|
5
6
|
const update = function ( state, msg ) {
|
|
6
7
|
if ( state.disable || state.pause ) return state;
|
|
@@ -20,7 +21,7 @@ const update = function ( state, msg ) {
|
|
|
20
21
|
// Log first error per episode; suppress subsequent until recovery
|
|
21
22
|
if ( !state.predicateErrorLogged ) {
|
|
22
23
|
state.predicateErrorLogged = true;
|
|
23
|
-
|
|
24
|
+
logger.error( `winkComposer/dwellTimeTracker: predicate threw exception: ${error.message}` );
|
|
24
25
|
}
|
|
25
26
|
return state;
|
|
26
27
|
}
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* and the status-signal path below.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { logger } from '../../core/logger/index.js';
|
|
10
|
+
|
|
9
11
|
/**
|
|
10
12
|
* Static fallback error the framework substitutes when an adapter breaks
|
|
11
13
|
* the return contract — no result object at all, or `{ ok: false }` with
|
|
@@ -54,10 +56,52 @@ const recordEmissionFailure = function ( state, error ) {
|
|
|
54
56
|
state.emitErrorLogged = true;
|
|
55
57
|
state.firstEmissionError = error.message;
|
|
56
58
|
state.firstEmissionErrorCode = error.code;
|
|
57
|
-
|
|
59
|
+
logger.error( state.emitErrorLogPrefix + error.code + '): ' + error.message );
|
|
58
60
|
}
|
|
59
61
|
}; // recordEmissionFailure()
|
|
60
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Substituted error for an emitter that THREW from publishNow — the
|
|
65
|
+
* other face of a broken return contract (ADR-018: the hot path never
|
|
66
|
+
* throws; a sink answers { ok }). Same code as the malformed-result
|
|
67
|
+
* face: the remediation is identical (fix the non-conforming adapter),
|
|
68
|
+
* and the message names the difference. Allocates on the failure path
|
|
69
|
+
* only.
|
|
70
|
+
*
|
|
71
|
+
* @param {Error} error - The thrown value
|
|
72
|
+
* @returns {Object} `{ code, message }` for the failure episode
|
|
73
|
+
*/
|
|
74
|
+
const thrownSinkError = function ( error ) {
|
|
75
|
+
return {
|
|
76
|
+
code: 'MALFORMED_RESULT',
|
|
77
|
+
message: `emitter publishNow threw instead of returning { ok, error? }: ${error.message}`
|
|
78
|
+
};
|
|
79
|
+
}; // thrownSinkError()
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Delivers one payload to the wired emitter with the sink call fully
|
|
83
|
+
* guarded. A conforming emitter never throws; a throwing one is a
|
|
84
|
+
* broken adapter, recorded in the emission episode — never blamed on
|
|
85
|
+
* the user's predicate, and never escaped into the pipeline where it
|
|
86
|
+
* would cost the whole message. Returns true only on `{ ok: true }`.
|
|
87
|
+
*
|
|
88
|
+
* @param {Object} state - Node state containing emitter and topic
|
|
89
|
+
* @param {Object} data - Payload to publish
|
|
90
|
+
* @returns {boolean} True when the emitter accepted the payload
|
|
91
|
+
*/
|
|
92
|
+
const deliverToEmitter = function ( state, data ) {
|
|
93
|
+
try {
|
|
94
|
+
const result = state.emitter.publishNow( state.topic, data );
|
|
95
|
+
if ( result && result.ok ) {
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
recordEmissionFailure( state, ( result && result.error ) || MALFORMED_RESULT_ERROR );
|
|
99
|
+
} catch ( sinkError ) {
|
|
100
|
+
recordEmissionFailure( state, thrownSinkError( sinkError ) );
|
|
101
|
+
}
|
|
102
|
+
return false;
|
|
103
|
+
}; // deliverToEmitter()
|
|
104
|
+
|
|
61
105
|
/**
|
|
62
106
|
* Emits disable/enable status signal to the wired emitter. Published
|
|
63
107
|
* unconditionally (no connectivity pre-check per ADR-018 — during a
|
|
@@ -80,15 +124,17 @@ const emitStatusSignal = function ( state, disable, reason ) {
|
|
|
80
124
|
$timestamp: Date.now()
|
|
81
125
|
};
|
|
82
126
|
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
if ( result && result.ok ) {
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
recordEmissionFailure( state, ( result && result.error ) || MALFORMED_RESULT_ERROR );
|
|
127
|
+
// Fully guarded delivery: this path can run inside update()'s
|
|
128
|
+
// catch block, so a throwing emitter here would otherwise escape
|
|
129
|
+
// update() and kill the pipeline. deliverToEmitter contains both
|
|
130
|
+
// faces of a broken adapter — a throw and a malformed result.
|
|
131
|
+
deliverToEmitter( state, signal );
|
|
92
132
|
}; // emitStatusSignal()
|
|
93
133
|
|
|
94
|
-
export {
|
|
134
|
+
export {
|
|
135
|
+
assertAnnotateReturn,
|
|
136
|
+
emitStatusSignal,
|
|
137
|
+
recordEmissionFailure,
|
|
138
|
+
deliverToEmitter,
|
|
139
|
+
MALFORMED_RESULT_ERROR
|
|
140
|
+
};
|
|
@@ -59,7 +59,7 @@ const init = function ( spec ) {
|
|
|
59
59
|
state.firstEmissionErrorCode = null;
|
|
60
60
|
// Static log prefix pre-built here per ADR-004 (no string building in
|
|
61
61
|
// update beyond the two runtime fields, and only once per episode).
|
|
62
|
-
state.emitErrorLogPrefix = `
|
|
62
|
+
state.emitErrorLogPrefix = `winkComposer/emitIf: publish failed (node=${state.name}, insightType=${state.insightType}, code=`;
|
|
63
63
|
|
|
64
64
|
return state;
|
|
65
65
|
}; // init()
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
* the two output gates share one failure model).
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import { assertAnnotateReturn, emitStatusSignal,
|
|
17
|
+
import { assertAnnotateReturn, emitStatusSignal, deliverToEmitter } from './helpers.js';
|
|
18
|
+
import { logger } from '../../core/logger/index.js';
|
|
18
19
|
|
|
19
20
|
const update = function ( state, msg ) {
|
|
20
21
|
// Always increment pass count for observability
|
|
@@ -44,19 +45,15 @@ const update = function ( state, msg ) {
|
|
|
44
45
|
// Per the ADR-018 sink return contract, the emitter answers
|
|
45
46
|
// { ok: true } on success or
|
|
46
47
|
// { ok: false, error: { code, message } } on failure.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
// deliverToEmitter guards the call itself too: a THROWING
|
|
49
|
+
// emitter is the other face of a broken contract, recorded
|
|
50
|
+
// in the same adapter episode — a non-conforming adapter
|
|
51
|
+
// can neither fail silently nor throw out of the hot path.
|
|
52
|
+
if ( deliverToEmitter( state, data ) ) {
|
|
50
53
|
state.emissionCount += 1;
|
|
51
54
|
state.lastEmissionTime = Date.now();
|
|
52
55
|
// Recovery: a successful publish closes the failure episode.
|
|
53
56
|
if ( state.emitErrorLogged ) state.emitErrorLogged = false;
|
|
54
|
-
} else {
|
|
55
|
-
// A malformed result (no object at all, or ok:false with no
|
|
56
|
-
// error) counts as a failure with the static fallback error,
|
|
57
|
-
// so a non-conforming adapter can neither fail silently
|
|
58
|
-
// nor throw out of the hot path.
|
|
59
|
-
recordEmissionFailure( state, ( result && result.error ) || MALFORMED_RESULT_ERROR );
|
|
60
57
|
}
|
|
61
58
|
}
|
|
62
59
|
|
|
@@ -76,7 +73,7 @@ const update = function ( state, msg ) {
|
|
|
76
73
|
// Log first error per episode; suppress subsequent until recovery
|
|
77
74
|
if ( !state.predicateErrorLogged ) {
|
|
78
75
|
state.predicateErrorLogged = true;
|
|
79
|
-
|
|
76
|
+
logger.error( `winkComposer/emitIf: predicate threw exception: ${error.message}` );
|
|
80
77
|
}
|
|
81
78
|
state.emissionErrors += 1;
|
|
82
79
|
state.lastEmissionError = error.message;
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
*/
|
|
40
40
|
|
|
41
41
|
import { executeTriggers } from '../../core/utils/node/index.js';
|
|
42
|
+
import { logger } from '../../core/logger/index.js';
|
|
42
43
|
|
|
43
44
|
const update = function ( state, msg ) {
|
|
44
45
|
if ( state.disable || state.pause ) return state;
|
|
@@ -64,7 +65,7 @@ const update = function ( state, msg ) {
|
|
|
64
65
|
} catch ( error ) {
|
|
65
66
|
if ( !state.tunableErrorLogged ) {
|
|
66
67
|
state.tunableErrorLogged = true;
|
|
67
|
-
|
|
68
|
+
logger.error( `winkComposer/${state.nodeType}: tunable threw: ${error.message}` );
|
|
68
69
|
}
|
|
69
70
|
}
|
|
70
71
|
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* is false. Predicate errors are caught and tracked per-episode.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { logger } from '../../core/logger/index.js';
|
|
12
|
+
|
|
11
13
|
const update = function ( state, msg ) {
|
|
12
14
|
if ( state.disable || state.pause ) return state;
|
|
13
15
|
|
|
@@ -26,7 +28,7 @@ const update = function ( state, msg ) {
|
|
|
26
28
|
// Log first error per episode; suppress subsequent until recovery
|
|
27
29
|
if ( !state.predicateErrorLogged ) {
|
|
28
30
|
state.predicateErrorLogged = true;
|
|
29
|
-
|
|
31
|
+
logger.error( `winkComposer/passIf: predicate threw exception: ${error.message}` );
|
|
30
32
|
}
|
|
31
33
|
}
|
|
32
34
|
|