@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
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// core/utils/callback-guard/index.js
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview The shared guard for user-supplied callbacks.
|
|
5
|
+
*
|
|
6
|
+
* Adapters and the flow runtime hand the user's functions to this
|
|
7
|
+
* module once, at setup. The armed version they get back contains
|
|
8
|
+
* every fault the user's code can produce. That covers a plain
|
|
9
|
+
* throw, a rejected promise, and even a `throw null` with no
|
|
10
|
+
* message to read. Each fault becomes one classified
|
|
11
|
+
* CALLBACK_FAILED report. The operation that invoked the callback
|
|
12
|
+
* continues unharmed. This is ADR-018's callback-isolation rule:
|
|
13
|
+
* a misbehaving user callback never reaches transport code and
|
|
14
|
+
* never fails silently. ADR-027 scopes which callbacks are
|
|
15
|
+
* wrapped: notification callbacks only. A callback whose throw
|
|
16
|
+
* carries meaning (QuestDB's strict-mode onWarning) and the
|
|
17
|
+
* flow-guarded onMessage stay unwrapped.
|
|
18
|
+
*
|
|
19
|
+
* Design constraints, in force at every call site:
|
|
20
|
+
* - Wrap once at setup; never per message. The armed closure's
|
|
21
|
+
* success-path cost is one try frame and one typeof check. An
|
|
22
|
+
* async callback additionally costs one derived promise per
|
|
23
|
+
* call — that cost follows the user's own choice of an async
|
|
24
|
+
* handler.
|
|
25
|
+
* - Fixed two-argument signature: every wrapped callback takes at
|
|
26
|
+
* most two arguments, so no rest-array is allocated per call.
|
|
27
|
+
* A one-argument callback observes a trailing undefined; that is
|
|
28
|
+
* deliberate and documented here.
|
|
29
|
+
* - The fault reporter is throw-proof. Every containment guarantee
|
|
30
|
+
* reduces to "reporting a fault cannot itself fault". The report
|
|
31
|
+
* closure receives an already-safe detail string, never the raw
|
|
32
|
+
* thrown value. When the report closure itself fails, the guard
|
|
33
|
+
* falls back to one bare log line with no user-value
|
|
34
|
+
* interpolation.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import { logger } from '../../logger/index.js';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Frozen sentinel `wrapTransform` returns when the transform threw.
|
|
41
|
+
* Distinguishes "threw" from the documented null/undefined drop.
|
|
42
|
+
* Never exported from composer's public surface, so no user value
|
|
43
|
+
* can ever be identical to it.
|
|
44
|
+
*/
|
|
45
|
+
const TRANSFORM_THREW = Object.freeze( {} );
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Renders a thrown value as a safe string. `throw null`, a throwing
|
|
49
|
+
* `message` getter, and an object with no string form all yield a
|
|
50
|
+
* printable result instead of a second throw.
|
|
51
|
+
*
|
|
52
|
+
* @param {*} err - Whatever the user's code threw or rejected with
|
|
53
|
+
* @returns {string} A printable detail, never a throw
|
|
54
|
+
*/
|
|
55
|
+
const describeFault = function ( err ) {
|
|
56
|
+
try {
|
|
57
|
+
if ( err && err.message ) {
|
|
58
|
+
return String( err.message );
|
|
59
|
+
}
|
|
60
|
+
return String( err );
|
|
61
|
+
} catch {
|
|
62
|
+
return 'unprintable error';
|
|
63
|
+
}
|
|
64
|
+
}; // describeFault()
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Last-resort line when the site's own report closure fails. Bare on
|
|
68
|
+
* purpose: interpolating anything user-controlled here could throw
|
|
69
|
+
* again, and this is the floor the whole guard stands on.
|
|
70
|
+
*/
|
|
71
|
+
const reportFallback = function () {
|
|
72
|
+
logger.error( 'winkComposer/callbackGuard: a callback fault report failed [CALLBACK_FAILED]' );
|
|
73
|
+
}; // reportFallback()
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Arms one user callback. Returns null when no function was given,
|
|
77
|
+
* so `if ( onX )` presence checks at the sites keep their meaning
|
|
78
|
+
* (absent stays absent — no-handler fallbacks are untouched).
|
|
79
|
+
*
|
|
80
|
+
* @param {Function} fn - The user's callback, already validated by
|
|
81
|
+
* the site's own config validation (validate raw, then wrap)
|
|
82
|
+
* @param {Object} spec - `{ name, severity, report }`
|
|
83
|
+
* @param {string} spec.name - Callback name for the report
|
|
84
|
+
* @param {string} spec.severity - 'red' or 'yellow' per ADR-018:
|
|
85
|
+
* red when the callback signals something happening now, yellow
|
|
86
|
+
* for post-event reporting
|
|
87
|
+
* @param {Function} spec.report - Site channel: called as
|
|
88
|
+
* `report( severity, name, detail )` with detail already safe
|
|
89
|
+
* @returns {Function|null} The armed callback, or null
|
|
90
|
+
*/
|
|
91
|
+
const wrapCallback = function ( fn, { name, severity, report } ) {
|
|
92
|
+
if ( typeof fn !== 'function' ) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
const faultFn = function ( err ) {
|
|
96
|
+
try {
|
|
97
|
+
report( severity, name, describeFault( err ) );
|
|
98
|
+
} catch {
|
|
99
|
+
reportFallback();
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
return function ( a, b ) {
|
|
103
|
+
try {
|
|
104
|
+
const result = fn( a, b );
|
|
105
|
+
if ( result && typeof result.then === 'function' ) {
|
|
106
|
+
// An async callback costs one derived promise per call.
|
|
107
|
+
// That allocation is proportional to the user's own
|
|
108
|
+
// choice of an async handler; a sync callback allocates
|
|
109
|
+
// nothing here.
|
|
110
|
+
const settled = result.then( undefined, faultFn );
|
|
111
|
+
// A native promise cannot reject past this point, because
|
|
112
|
+
// faultFn cannot throw. A non-conforming thenable can
|
|
113
|
+
// ignore its handlers and RETURN a rejected promise
|
|
114
|
+
// instead; sink that one level so it cannot leak as an
|
|
115
|
+
// unhandled rejection. The guard contains bugs, not
|
|
116
|
+
// in-process adversaries — a deeper chain of hostile
|
|
117
|
+
// thenables stays the caller's own fault.
|
|
118
|
+
if ( !( result instanceof Promise ) && settled && typeof settled.then === 'function' ) {
|
|
119
|
+
settled.then( undefined, faultFn );
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
} catch ( err ) {
|
|
123
|
+
faultFn( err );
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
}; // wrapCallback()
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Arms a value-returning callback (the sources' `transform`). The
|
|
130
|
+
* caller keeps its control flow — a transform's return decides the
|
|
131
|
+
* row's fate — so this face only converts a throw into the sentinel
|
|
132
|
+
* and a safe fault report. Return-shape checks and the null drop
|
|
133
|
+
* stay at the site.
|
|
134
|
+
*
|
|
135
|
+
* @param {Function} fn - The transform, called as `fn( value )`
|
|
136
|
+
* @param {Function} onFault - Hoisted site closure, called as
|
|
137
|
+
* `onFault( detail, context )` on the failure path only
|
|
138
|
+
* @returns {Function} `( value, context ) => result | TRANSFORM_THREW`
|
|
139
|
+
*/
|
|
140
|
+
const wrapTransform = function ( fn, onFault ) {
|
|
141
|
+
return function ( value, context ) {
|
|
142
|
+
try {
|
|
143
|
+
return fn( value );
|
|
144
|
+
} catch ( err ) {
|
|
145
|
+
try {
|
|
146
|
+
onFault( describeFault( err ), context );
|
|
147
|
+
} catch {
|
|
148
|
+
reportFallback();
|
|
149
|
+
}
|
|
150
|
+
return TRANSFORM_THREW;
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
}; // wrapTransform()
|
|
154
|
+
|
|
155
|
+
export { wrapCallback, wrapTransform, TRANSFORM_THREW };
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
export const halfLifeToAlpha = function ( halfLife ) {
|
|
16
16
|
if ( typeof halfLife !== 'number' || !Number.isFinite( halfLife ) || ( halfLife <= 0 ) ) {
|
|
17
|
-
throw new Error( 'Half-life must be a finite number > 0' );
|
|
17
|
+
throw new Error( 'winkComposer/halfLife: Half-life must be a finite number > 0' );
|
|
18
18
|
}
|
|
19
19
|
// alpha = 1 - exp( -ln(2) / halfLife ); use expm1 to avoid cancellation
|
|
20
20
|
let alpha = -Math.expm1( -( Math.LN2 / halfLife ) );
|
|
@@ -37,7 +37,7 @@ export const halfLifeToAlpha = function ( halfLife ) {
|
|
|
37
37
|
*/
|
|
38
38
|
export const alphaToHalfLife = function ( alpha ) {
|
|
39
39
|
if ( typeof alpha !== 'number' || !Number.isFinite( alpha ) || ( alpha <= 0 ) || ( alpha >= 1 ) ) {
|
|
40
|
-
throw new Error( 'Alpha must be a finite number in (0,1)' );
|
|
40
|
+
throw new Error( 'winkComposer/halfLife: Alpha must be a finite number in (0,1)' );
|
|
41
41
|
}
|
|
42
42
|
// halfLife = ln(2) / -ln(1 - alpha) ; use log1p(-alpha) for stability
|
|
43
43
|
return Math.LN2 / ( -Math.log1p( -alpha ) );
|
|
@@ -53,10 +53,10 @@ export const alphaToHalfLife = function ( alpha ) {
|
|
|
53
53
|
*/
|
|
54
54
|
export const halfLifeToWarmupSamples = function ( halfLifeSamples, settledFraction = 0.95 ) {
|
|
55
55
|
if ( typeof halfLifeSamples !== 'number' || !Number.isFinite( halfLifeSamples ) || ( halfLifeSamples <= 0 ) ) {
|
|
56
|
-
throw new Error( 'halfLifeSamples must be a finite number > 0' );
|
|
56
|
+
throw new Error( 'winkComposer/halfLife: halfLifeSamples must be a finite number > 0' );
|
|
57
57
|
}
|
|
58
58
|
if ( typeof settledFraction !== 'number' || !Number.isFinite( settledFraction ) || ( settledFraction <= 0 ) || ( settledFraction >= 1 ) ) {
|
|
59
|
-
throw new Error( 'settledFraction must be a finite number in (0,1)' );
|
|
59
|
+
throw new Error( 'winkComposer/halfLife: settledFraction must be a finite number in (0,1)' );
|
|
60
60
|
}
|
|
61
61
|
// nHL = -log2(1 - settledFraction) = -ln(1 - s)/ln2
|
|
62
62
|
const nHalfLives = ( -Math.log1p( -settledFraction ) ) / Math.LN2;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* winkComposer-specific validators
|
|
3
3
|
* Domain-specific validation functions for streaming analytics nodes
|
|
4
4
|
*/
|
|
5
5
|
|
|
@@ -121,7 +121,7 @@ const noSelfTriggers = {
|
|
|
121
121
|
!trgr.targets.includes( spec.name )
|
|
122
122
|
);
|
|
123
123
|
},
|
|
124
|
-
error: '
|
|
124
|
+
error: 'winkComposer/validation: Node cannot trigger control methods on itself'
|
|
125
125
|
}; // noSelfTriggers
|
|
126
126
|
|
|
127
127
|
// Export all composer validators
|
|
@@ -95,7 +95,7 @@ const validateWithSchema = function ( schema, object, pathPrefix = 'object' ) {
|
|
|
95
95
|
// Throw if invalid - used in init functions
|
|
96
96
|
throwIfInvalid: ( nodeType ) => {
|
|
97
97
|
if ( !valid ) {
|
|
98
|
-
const errorMessage = `
|
|
98
|
+
const errorMessage = `winkComposer/${nodeType}: validation failed:\n ${errors.join( '\n ' )}`;
|
|
99
99
|
throw new TypeError( errorMessage );
|
|
100
100
|
}
|
|
101
101
|
}
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* - Startup runs once per flow; the check costs nothing at runtime.
|
|
18
18
|
*
|
|
19
19
|
* Error message format (same across sinks):
|
|
20
|
-
* `
|
|
20
|
+
* `winkComposer/adapter: '<adapterId>' missing required method '<name>'`
|
|
21
21
|
*
|
|
22
22
|
* @see ADR-018 (sink method surface)
|
|
23
23
|
*/
|
|
@@ -32,23 +32,23 @@
|
|
|
32
32
|
* @param {*} handle - the value returned from the factory
|
|
33
33
|
* @param {string[]} requiredMethods - method names that must be functions
|
|
34
34
|
* on the handle (e.g., [ 'publishNow', 'shutdown', 'getHealth' ])
|
|
35
|
-
* @throws {Error} `
|
|
35
|
+
* @throws {Error} `winkComposer/adapter: '<adapterId>' factory returned non-object handle`
|
|
36
36
|
* if `handle` is null/undefined/non-object.
|
|
37
|
-
* @throws {Error} `
|
|
37
|
+
* @throws {Error} `winkComposer/adapter: '<adapterId>' missing required method '<name>'`
|
|
38
38
|
* on the first missing or non-function method (fails fast — does not
|
|
39
39
|
* enumerate every gap, since one is enough to abort wiring).
|
|
40
40
|
*/
|
|
41
41
|
const assertHandle = function ( adapterId, handle, requiredMethods ) {
|
|
42
42
|
if ( !handle || typeof handle !== 'object' ) {
|
|
43
43
|
throw new Error(
|
|
44
|
-
`
|
|
44
|
+
`winkComposer/adapter: '${adapterId}' factory returned non-object handle`
|
|
45
45
|
);
|
|
46
46
|
}
|
|
47
47
|
for ( let i = 0; i < requiredMethods.length; i += 1 ) {
|
|
48
48
|
const name = requiredMethods[ i ];
|
|
49
49
|
if ( typeof handle[ name ] !== 'function' ) {
|
|
50
50
|
throw new Error(
|
|
51
|
-
`
|
|
51
|
+
`winkComposer/adapter: '${adapterId}' missing required method '${name}'`
|
|
52
52
|
);
|
|
53
53
|
}
|
|
54
54
|
}
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* `src/core/test/adapter-module-surface.specs.js`.
|
|
20
20
|
*
|
|
21
21
|
* Error message format (same shape as assert-handle.js):
|
|
22
|
-
* `
|
|
22
|
+
* `winkComposer/adapter: '<adapterId>' module missing valid
|
|
23
23
|
* 'durabilityClass' export — got <value>; expected one of ...`
|
|
24
24
|
*
|
|
25
25
|
* @see ADR-018 (module surface, durability)
|
|
@@ -52,7 +52,7 @@ const assertModuleDurability = function ( adapterId, module ) {
|
|
|
52
52
|
}
|
|
53
53
|
const got = ( typeof value === 'string' ) ? `'${value}'` : String( value );
|
|
54
54
|
const err = new Error(
|
|
55
|
-
`
|
|
55
|
+
`winkComposer/adapter: '${adapterId}' module missing valid 'durabilityClass' export ` +
|
|
56
56
|
`(ADR-018) — got ${got}; expected one of ` +
|
|
57
57
|
`${DURABILITY_CLASSES.map( ( c ) => `'${c}'` ).join( ' | ' )}`
|
|
58
58
|
);
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
import { assertHandle } from './assert-handle.js';
|
|
46
46
|
import { assertModuleDurability } from './assert-module.js';
|
|
47
47
|
import { applySemanticsRequirement } from './wire-semantics.js';
|
|
48
|
+
import { logger } from '../logger/index.js';
|
|
48
49
|
|
|
49
50
|
/**
|
|
50
51
|
* Methods the framework calls on an emitter handle later, so they have to
|
|
@@ -121,7 +122,7 @@ const emitters = ( function () {
|
|
|
121
122
|
const module = emitterModules[ target ];
|
|
122
123
|
const factory = module && module.createEmitter;
|
|
123
124
|
if ( typeof factory !== 'function' ) {
|
|
124
|
-
throw new Error( `
|
|
125
|
+
throw new Error( `winkComposer/wiring: Invalid emitter module: ${target}` );
|
|
125
126
|
}
|
|
126
127
|
|
|
127
128
|
// ADR-018 module-surface check: the module must say what
|
|
@@ -234,8 +235,8 @@ const emitters = ( function () {
|
|
|
234
235
|
if ( result.status === 'rejected' ) {
|
|
235
236
|
const err = result.reason || {};
|
|
236
237
|
const dropped = err.dropped ? ` dropped=${describeDropped( err.dropped )}` : '';
|
|
237
|
-
|
|
238
|
-
`
|
|
238
|
+
logger.error(
|
|
239
|
+
`winkComposer/wiring: emitter '${names[ i ]}' shutdown failed [${err.code || 'UNKNOWN'}]: ${err.message || String( err )}${dropped}`
|
|
239
240
|
);
|
|
240
241
|
}
|
|
241
242
|
} );
|
|
@@ -11,7 +11,7 @@ const wireLinearGraph = function ( specs, nodeModules, options = {} ) {
|
|
|
11
11
|
const nodeModule = nodeModules[ moduleName ];
|
|
12
12
|
|
|
13
13
|
if (!nodeModule) {
|
|
14
|
-
throw new Error( `
|
|
14
|
+
throw new Error( `winkComposer/wiring: Node module '${moduleName}' not found for nodeType '${spec.nodeType}'` );
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
const nextHops = nextHop ? [ nextHop ] : [];
|
|
@@ -37,27 +37,27 @@ const wireNode = function ( nodeModule, nodeIndex, nextHops = [], options = {} )
|
|
|
37
37
|
const maxRecentErrors = options.maxRecentErrors || 5;
|
|
38
38
|
// Validate required node module interface at wire-time
|
|
39
39
|
if ( !nodeModule || typeof nodeModule.update !== 'function' ) {
|
|
40
|
-
throw new TypeError( `
|
|
40
|
+
throw new TypeError( `winkComposer/wiring: Node module must implement update function; found ${typeof nodeModule.update}` );
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
if ( typeof nodeModule.publishTo !== 'function' ) {
|
|
44
|
-
throw new TypeError( `
|
|
44
|
+
throw new TypeError( `winkComposer/wiring: Node module must implement publishTo function; found ${typeof nodeModule.publishTo}` );
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
// Validate next hops are executable functions
|
|
48
48
|
if ( !Array.isArray( nextHops ) ) {
|
|
49
|
-
throw new TypeError( `
|
|
49
|
+
throw new TypeError( `winkComposer/wiring: Next hops must be an array; found ${typeof nextHops}` );
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
for ( let i = 0; i < nextHops.length; i += 1 ) {
|
|
53
53
|
if ( typeof nextHops[ i ] !== 'function' ) {
|
|
54
|
-
throw new TypeError( `
|
|
54
|
+
throw new TypeError( `winkComposer/wiring: Next hop at index ${i} must be a function; found ${typeof nextHops[ i ]}` );
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
// Validate node index for state store access
|
|
59
59
|
if ( typeof nodeIndex !== 'number' || nodeIndex < 0 ) {
|
|
60
|
-
throw new RangeError( `
|
|
60
|
+
throw new RangeError( `winkComposer/wiring: Node index must be a non-negative number; found ${nodeIndex}` );
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
/**
|
|
@@ -142,7 +142,7 @@ const wireNode = function ( nodeModule, nodeIndex, nextHops = [], options = {} )
|
|
|
142
142
|
|
|
143
143
|
// Provide node-level error isolation with contextual information
|
|
144
144
|
// This prevents individual node failures from crashing the entire flow
|
|
145
|
-
const nodeError = new Error( `Node execution failed at index ${nodeIndex}: ${error.message}` );
|
|
145
|
+
const nodeError = new Error( `winkComposer/wiring: Node execution failed at index ${nodeIndex}: ${error.message}` );
|
|
146
146
|
nodeError.cause = error;
|
|
147
147
|
nodeError.nodeIndex = nodeIndex;
|
|
148
148
|
nodeError.nodeModule = nodeModule.getNodeType ? nodeModule.getNodeType() : 'unknown';
|
|
@@ -151,7 +151,7 @@ const applySemanticsRequirement = function ( adapterId, module, runtimeAssetClas
|
|
|
151
151
|
// QuestDB's existing createStorage guard; this layer
|
|
152
152
|
// catches it earlier in the lifecycle.
|
|
153
153
|
const err = new Error(
|
|
154
|
-
`
|
|
154
|
+
`winkComposer/wiring: adapter '${adapterId}' declares semanticsRequirement.assetClass.required ` +
|
|
155
155
|
'but the flow has no .assetClass() — add .assetClass(assetClassDef) to the flow definition'
|
|
156
156
|
);
|
|
157
157
|
err.code = 'MISSING_ASSET_CLASS';
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
import { assertHandle } from './assert-handle.js';
|
|
49
49
|
import { assertModuleDurability } from './assert-module.js';
|
|
50
50
|
import { applySemanticsRequirement } from './wire-semantics.js';
|
|
51
|
+
import { logger } from '../logger/index.js';
|
|
51
52
|
|
|
52
53
|
/**
|
|
53
54
|
* Methods the framework calls on a storage handle later, so they have to
|
|
@@ -161,7 +162,7 @@ const storages = ( function () {
|
|
|
161
162
|
|
|
162
163
|
const module = storageModules[ storageName ];
|
|
163
164
|
if ( !module?.createStorage ) {
|
|
164
|
-
throw new Error( `
|
|
165
|
+
throw new Error( `winkComposer/wiring: Invalid storage module: ${storageName}` );
|
|
165
166
|
}
|
|
166
167
|
|
|
167
168
|
// ADR-018 module-surface check: the module must say what a
|
|
@@ -322,8 +323,8 @@ const storages = ( function () {
|
|
|
322
323
|
if ( result.status === 'rejected' ) {
|
|
323
324
|
const err = result.reason || {};
|
|
324
325
|
const dropped = err.dropped ? ` dropped=${describeDropped( err.dropped )}` : '';
|
|
325
|
-
|
|
326
|
-
`
|
|
326
|
+
logger.error(
|
|
327
|
+
`winkComposer/wiring: storage '${names[ i ]}' shutdown failed [${err.code || 'UNKNOWN'}]: ${err.message || String( err )}${dropped}`
|
|
327
328
|
);
|
|
328
329
|
}
|
|
329
330
|
} );
|
package/src/flow/driver.js
CHANGED
|
@@ -24,9 +24,18 @@
|
|
|
24
24
|
* `processMessage`, exactly as a success does — so `feedAll` counts it as
|
|
25
25
|
* processed. See `docs/handbook/headless-flow.md`.
|
|
26
26
|
*
|
|
27
|
+
* `err.code` vocabulary (console classification; listed per ADR-018):
|
|
28
|
+
* - `CALLBACK_FAILED` — the user's `onError` itself threw or rejected.
|
|
29
|
+
* The shared callback guard contains the fault (ADR-018): the feed
|
|
30
|
+
* loop continues, counters stay truthful, and each fault becomes one
|
|
31
|
+
* classified console line carrying the detail.
|
|
32
|
+
*
|
|
27
33
|
* @see docs/handbook/headless-flow.md
|
|
28
34
|
*/
|
|
29
35
|
|
|
36
|
+
import { wrapCallback } from '../core/utils/callback-guard/index.js';
|
|
37
|
+
import { logger } from '../core/logger/index.js';
|
|
38
|
+
|
|
30
39
|
/**
|
|
31
40
|
* Default fault handler. Surfaces every fault so none is silent. Allocates only
|
|
32
41
|
* when a fault occurs (rare), never on the success path. Provide your own
|
|
@@ -37,7 +46,7 @@
|
|
|
37
46
|
* @returns {void}
|
|
38
47
|
*/
|
|
39
48
|
const logFault = function ( error ) {
|
|
40
|
-
|
|
49
|
+
logger.error( `winkComposer/headlessDriver: a message failed — ${error.message}` );
|
|
41
50
|
}; // logFault()
|
|
42
51
|
|
|
43
52
|
/**
|
|
@@ -56,18 +65,34 @@ const logFault = function ( error ) {
|
|
|
56
65
|
const headlessDriver = function ( handle, opts = {} ) {
|
|
57
66
|
if ( !handle || typeof handle.processMessage !== 'function' ) {
|
|
58
67
|
throw new TypeError(
|
|
59
|
-
'
|
|
68
|
+
'winkComposer/headlessDriver: handle must be the object returned by ' +
|
|
60
69
|
'flow(...).run() — it has no processMessage() function'
|
|
61
70
|
);
|
|
62
71
|
}
|
|
63
72
|
|
|
64
|
-
const
|
|
65
|
-
if ( typeof
|
|
73
|
+
const userOnError = opts.onError === undefined ? logFault : opts.onError;
|
|
74
|
+
if ( typeof userOnError !== 'function' ) {
|
|
66
75
|
throw new TypeError(
|
|
67
|
-
'
|
|
76
|
+
'winkComposer/headlessDriver: onError must be a function'
|
|
68
77
|
);
|
|
69
78
|
}
|
|
70
79
|
|
|
80
|
+
// Armed once at construction (validate raw first, then wrap). The
|
|
81
|
+
// fault reporter is user code too, so it gets the same containment
|
|
82
|
+
// as the messages it reports on. ADR-018's rule: a misbehaving
|
|
83
|
+
// user callback never interrupts the operation that invoked it.
|
|
84
|
+
// A throwing or rejecting onError costs one classified console
|
|
85
|
+
// line. The feed continues and the counters stay truthful.
|
|
86
|
+
const onError = wrapCallback( userOnError, {
|
|
87
|
+
name: 'onError',
|
|
88
|
+
severity: 'red',
|
|
89
|
+
report: function ( severity, name, detail ) {
|
|
90
|
+
logger.error(
|
|
91
|
+
`winkComposer/headlessDriver: user callback ${name} failed [CALLBACK_FAILED]: ${detail}`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
} );
|
|
95
|
+
|
|
71
96
|
/**
|
|
72
97
|
* Feed one message. Use this for push sources, where an event hands you a
|
|
73
98
|
* message and there is no loop to own.
|
|
@@ -123,7 +148,7 @@ const headlessDriver = function ( handle, opts = {} ) {
|
|
|
123
148
|
const isSync = typeof source?.[ Symbol.iterator ] === 'function';
|
|
124
149
|
if ( !isAsync && !isSync ) {
|
|
125
150
|
throw new TypeError(
|
|
126
|
-
'
|
|
151
|
+
'winkComposer/headlessDriver.feedAll: source must be a sync or async ' +
|
|
127
152
|
'iterable (an array, a generator, or a stream)'
|
|
128
153
|
);
|
|
129
154
|
}
|
|
@@ -12,12 +12,12 @@ const expandXOutputsOptions = function ( meta, nodeArgs, flowState ) {
|
|
|
12
12
|
const param = inputs[ i ];
|
|
13
13
|
|
|
14
14
|
if ( seenX[ param ] === 1 ) {
|
|
15
|
-
throw new Error( `
|
|
15
|
+
throw new Error( `winkComposer/flow: duplicate input found: '${param}'.` );
|
|
16
16
|
}
|
|
17
17
|
seenX[ param ] = 1;
|
|
18
18
|
|
|
19
19
|
const instName = `${baseName}_${param}`;
|
|
20
|
-
if ( isNodeNameDuplicate( instName ) ) throw Error( `
|
|
20
|
+
if ( isNodeNameDuplicate( instName ) ) throw Error( `winkComposer/flow: duplicate node ${instName} found.` );
|
|
21
21
|
const stats = Object.create( null );
|
|
22
22
|
for ( const st in outputs ) {
|
|
23
23
|
if (Object.prototype.hasOwnProperty.call( outputs, st ) ) {
|
|
@@ -47,12 +47,12 @@ const expandXOptions = function ( meta, nodeArgs, flowState ) {
|
|
|
47
47
|
const param = inputs[ i ];
|
|
48
48
|
|
|
49
49
|
if ( seenX[ param ] === 1 ) {
|
|
50
|
-
throw new Error( `
|
|
50
|
+
throw new Error( `winkComposer/flow: duplicate input found: '${param}'.` );
|
|
51
51
|
}
|
|
52
52
|
seenX[ param ] = 1;
|
|
53
53
|
|
|
54
54
|
const instName = `${baseName}_${param}`;
|
|
55
|
-
if ( isNodeNameDuplicate( instName ) ) throw Error( `
|
|
55
|
+
if ( isNodeNameDuplicate( instName ) ) throw Error( `winkComposer/flow: duplicate node ${instName} found.` );
|
|
56
56
|
|
|
57
57
|
const spec = meta.buildSpec( instName, param, ...balanceArgs );
|
|
58
58
|
specs.push( spec );
|
|
@@ -226,7 +226,7 @@ export const validateFlowConfig = function ( config, schema, methodName ) {
|
|
|
226
226
|
valid: false,
|
|
227
227
|
errors,
|
|
228
228
|
throwIfInvalid: () => {
|
|
229
|
-
throw new Error( `
|
|
229
|
+
throw new Error( `winkComposer/flow.${methodName}: ${errors[ 0 ]}` );
|
|
230
230
|
}
|
|
231
231
|
};
|
|
232
232
|
}
|
|
@@ -241,7 +241,7 @@ export const validateFlowConfig = function ( config, schema, methodName ) {
|
|
|
241
241
|
throwIfInvalid: () => {
|
|
242
242
|
if ( !validation.valid ) {
|
|
243
243
|
throw new Error(
|
|
244
|
-
`
|
|
244
|
+
`winkComposer/flow.${methodName}: validation failed:\n - ${validation.errors.join( '\n - ' )}`
|
|
245
245
|
);
|
|
246
246
|
}
|
|
247
247
|
}
|
|
@@ -266,7 +266,7 @@ export const validateFlowArrayConfig = function ( config, schema, methodName ) {
|
|
|
266
266
|
valid: false,
|
|
267
267
|
errors,
|
|
268
268
|
throwIfInvalid: () => {
|
|
269
|
-
throw new Error( `
|
|
269
|
+
throw new Error( `winkComposer/flow.${methodName}: ${errors[ 0 ]}` );
|
|
270
270
|
}
|
|
271
271
|
};
|
|
272
272
|
}
|
|
@@ -280,7 +280,7 @@ export const validateFlowArrayConfig = function ( config, schema, methodName ) {
|
|
|
280
280
|
throwIfInvalid: () => {
|
|
281
281
|
if ( !validation.valid ) {
|
|
282
282
|
throw new Error(
|
|
283
|
-
`
|
|
283
|
+
`winkComposer/flow.${methodName}: validation failed:\n - ${validation.errors.join( '\n - ' )}`
|
|
284
284
|
);
|
|
285
285
|
}
|
|
286
286
|
}
|