@winkjs/composer 0.5.1 → 0.7.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/README.md +1 -1
- package/package.json +3 -3
- package/src/composer.js +1 -1
- 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/connection.js +209 -0
- package/src/core/emitter-manager/mqtt/constants.js +43 -29
- package/src/core/emitter-manager/mqtt/emitter.js +108 -633
- package/src/core/emitter-manager/mqtt/health.js +151 -0
- package/src/core/emitter-manager/mqtt/index.js +68 -4
- package/src/core/emitter-manager/mqtt/publish.js +292 -0
- package/src/core/emitter-manager/mqtt/resolve-config.js +312 -0
- package/src/core/emitter-manager/mqtt/shutdown-drain.js +247 -0
- package/src/core/emitter-manager/terminal/assert-columns.js +1 -1
- package/src/core/emitter-manager/terminal/index.js +20 -5
- package/src/core/env-vars.js +143 -16
- 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 +276 -83
- package/src/core/source-manager/mqtt/dedup.js +19 -14
- package/src/core/source-manager/mqtt/index.js +33 -6
- package/src/core/source-manager/mqtt/status.js +211 -46
- 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/address-policy.js +264 -0
- package/src/core/storage-manager/questdb/assert-columns.js +2 -2
- package/src/core/storage-manager/questdb/delivery-gate.js +218 -0
- package/src/core/storage-manager/questdb/ensure-tables.js +1 -1
- package/src/core/storage-manager/questdb/flush-engine.js +852 -0
- package/src/core/storage-manager/questdb/flush-tracker.js +331 -0
- package/src/core/storage-manager/questdb/index.js +592 -827
- package/src/core/storage-manager/questdb/persist-plan.js +147 -162
- package/src/core/storage-manager/questdb/resolve-options.js +403 -0
- package/src/core/storage-manager/questdb/shutdown-drain.js +181 -0
- package/src/core/storage-manager/questdb/skip-warnings.js +180 -0
- package/src/core/test-utils/tcp-proxy.js +194 -1
- package/src/core/tunable/helpers.js +1 -1
- package/src/core/utils/address/index.js +362 -0
- package/src/core/utils/address/probe.js +210 -0
- package/src/core/utils/callback-guard/index.js +176 -0
- package/src/core/utils/clock/index.js +34 -0
- package/src/core/utils/half-life/index.js +4 -4
- package/src/core/utils/jitter/index.js +51 -0
- package/src/core/utils/line-rate/index.js +104 -0
- package/src/core/utils/quantize/index.js +43 -19
- 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 +7 -4
- 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 +14 -5
- 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 +62 -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 +200 -40
- 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
|
@@ -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
|
|
@@ -145,6 +146,13 @@ const storages = ( function () {
|
|
|
145
146
|
// to assetClass.name if not explicitly specified.
|
|
146
147
|
const wire = async function ( specs, storageConfigs, storageModules, assetClass = null ) {
|
|
147
148
|
const pendingStorages = [];
|
|
149
|
+
// Names whose factory has been called in THIS wire() call. The
|
|
150
|
+
// singleton slot is filled only after every pending factory has
|
|
151
|
+
// settled. Without this set, a flow with several persist-if nodes
|
|
152
|
+
// on one storage would call the factory once per node and keep
|
|
153
|
+
// only the last handle. Each orphan would hold a flush timer, an
|
|
154
|
+
// agent, and a sender for the life of the process.
|
|
155
|
+
const pendingNames = new Set();
|
|
148
156
|
|
|
149
157
|
for ( const spec of specs ) {
|
|
150
158
|
// Skip non-persistIf nodes
|
|
@@ -154,14 +162,14 @@ const storages = ( function () {
|
|
|
154
162
|
|
|
155
163
|
const storageName = spec.storageName;
|
|
156
164
|
|
|
157
|
-
// Skip if already in singleton registry
|
|
158
|
-
if ( singletons[ storageName ] ) {
|
|
165
|
+
// Skip if already in singleton registry, or already being built
|
|
166
|
+
if ( singletons[ storageName ] || pendingNames.has( storageName ) ) {
|
|
159
167
|
continue; // eslint-disable-line no-continue
|
|
160
168
|
}
|
|
161
169
|
|
|
162
170
|
const module = storageModules[ storageName ];
|
|
163
171
|
if ( !module?.createStorage ) {
|
|
164
|
-
throw new Error( `
|
|
172
|
+
throw new Error( `winkComposer/wiring: Invalid storage module: ${storageName}` );
|
|
165
173
|
}
|
|
166
174
|
|
|
167
175
|
// ADR-018 module-surface check: the module must say what a
|
|
@@ -195,6 +203,7 @@ const storages = ( function () {
|
|
|
195
203
|
|
|
196
204
|
// createStorage may return Promise (async) or storage directly (sync)
|
|
197
205
|
const storageOrPromise = module.createStorage( effectiveConfig );
|
|
206
|
+
pendingNames.add( storageName );
|
|
198
207
|
pendingStorages.push( { storageName, storageOrPromise } );
|
|
199
208
|
}
|
|
200
209
|
|
|
@@ -322,8 +331,8 @@ const storages = ( function () {
|
|
|
322
331
|
if ( result.status === 'rejected' ) {
|
|
323
332
|
const err = result.reason || {};
|
|
324
333
|
const dropped = err.dropped ? ` dropped=${describeDropped( err.dropped )}` : '';
|
|
325
|
-
|
|
326
|
-
`
|
|
334
|
+
logger.error(
|
|
335
|
+
`winkComposer/wiring: storage '${names[ i ]}' shutdown failed [${err.code || 'UNKNOWN'}]: ${err.message || String( err )}${dropped}`
|
|
327
336
|
);
|
|
328
337
|
}
|
|
329
338
|
} );
|
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
|
}
|
package/src/flow/flow.js
CHANGED
|
@@ -47,6 +47,27 @@ import { validateWithSchema } from '../core/utils/validate/index.js';
|
|
|
47
47
|
import { assetClassSchema } from '../core/semantics/schemas/index.js';
|
|
48
48
|
import { expandTemplateSpec } from './expand-template-spec.js';
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Throws the flow's setup error for an adapter config that the
|
|
52
|
+
* adapter's own schema rejected. The error carries `INVALID_CONFIG`,
|
|
53
|
+
* the ADR-018 setup code, and the ADR-028 bracket. A program matches
|
|
54
|
+
* the code; an operator reads the remedy the schema names.
|
|
55
|
+
*
|
|
56
|
+
* @param {{valid: boolean, errors: string[]}} validation - The schema result
|
|
57
|
+
* @param {string} label - `flow.<method>.<adapterId>`, for the message
|
|
58
|
+
* @throws {Error} INVALID_CONFIG when the config is invalid
|
|
59
|
+
*/
|
|
60
|
+
const throwIfAdapterConfigInvalid = function ( validation, label ) {
|
|
61
|
+
if ( validation.valid ) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const err = new Error(
|
|
65
|
+
`winkComposer/${label}: validation failed [INVALID_CONFIG]:\n - ${validation.errors.join( '\n - ' )}`
|
|
66
|
+
);
|
|
67
|
+
err.code = 'INVALID_CONFIG';
|
|
68
|
+
throw err;
|
|
69
|
+
}; // throwIfAdapterConfigInvalid()
|
|
70
|
+
|
|
50
71
|
const flow = function ( flowName = 'flow1' ) {
|
|
51
72
|
const api = Object.create( null );
|
|
52
73
|
|
|
@@ -91,7 +112,7 @@ const flow = function ( flowName = 'flow1' ) {
|
|
|
91
112
|
const enforceConfigFirst = function ( methodName ) {
|
|
92
113
|
if ( pipelineStarted ) {
|
|
93
114
|
throw new Error(
|
|
94
|
-
`
|
|
115
|
+
`winkComposer/flow: .${methodName}() must be called before any nodes or .switch()/.groupBy()`
|
|
95
116
|
);
|
|
96
117
|
}
|
|
97
118
|
};
|
|
@@ -100,11 +121,11 @@ const flow = function ( flowName = 'flow1' ) {
|
|
|
100
121
|
const validateSwitchComplete = function () {
|
|
101
122
|
if ( switchState.active ) {
|
|
102
123
|
if ( switchState.caseOrder.length === 0 ) {
|
|
103
|
-
throw new Error( '
|
|
124
|
+
throw new Error( 'winkComposer/flow: .switch() requires at least one .case()' );
|
|
104
125
|
}
|
|
105
126
|
if ( !switchState.caseEnded ) {
|
|
106
127
|
throw new Error(
|
|
107
|
-
`
|
|
128
|
+
`winkComposer/flow: case '${switchState.currentCase}' must end with .break()`
|
|
108
129
|
);
|
|
109
130
|
}
|
|
110
131
|
}
|
|
@@ -118,7 +139,7 @@ const flow = function ( flowName = 'flow1' ) {
|
|
|
118
139
|
const collisions = collectFlowOutputCollisions( switchState, flowDefinition );
|
|
119
140
|
if ( collisions.length > 0 ) {
|
|
120
141
|
throw new Error(
|
|
121
|
-
'
|
|
142
|
+
'winkComposer/flow: output field collision(s) - two nodes on one path ' +
|
|
122
143
|
'write the same message field:\n - ' + collisions.join( '\n - ' )
|
|
123
144
|
);
|
|
124
145
|
}
|
|
@@ -160,10 +181,10 @@ const flow = function ( flowName = 'flow1' ) {
|
|
|
160
181
|
api.source = function ( adapter, sourceConfig = {} ) {
|
|
161
182
|
enforceConfigFirst( 'source' );
|
|
162
183
|
if ( typeof adapter !== 'object' || adapter === null ) {
|
|
163
|
-
throw new Error( '
|
|
184
|
+
throw new Error( 'winkComposer/flow: source adapter must be an imported module' );
|
|
164
185
|
}
|
|
165
186
|
if ( typeof adapter.start !== 'function' ) {
|
|
166
|
-
throw new Error( '
|
|
187
|
+
throw new Error( 'winkComposer/flow: source adapter must have a start() function' );
|
|
167
188
|
}
|
|
168
189
|
|
|
169
190
|
// If the adapter provides a configSchema, we validate config here.
|
|
@@ -171,7 +192,7 @@ const flow = function ( flowName = 'flow1' ) {
|
|
|
171
192
|
// not later inside adapter.start().
|
|
172
193
|
if ( adapter.configSchema ) {
|
|
173
194
|
const validation = validateWithSchema( adapter.configSchema, sourceConfig, 'source' );
|
|
174
|
-
|
|
195
|
+
throwIfAdapterConfigInvalid( validation, `flow.source.${adapter.id || 'unknown'}` );
|
|
175
196
|
}
|
|
176
197
|
|
|
177
198
|
runtime.source = { adapter, config: sourceConfig };
|
|
@@ -181,13 +202,13 @@ const flow = function ( flowName = 'flow1' ) {
|
|
|
181
202
|
api.emitter = function ( adapter, emitterConfig = {} ) {
|
|
182
203
|
enforceConfigFirst( 'emitter' );
|
|
183
204
|
if ( typeof adapter !== 'object' || adapter === null ) {
|
|
184
|
-
throw new Error( '
|
|
205
|
+
throw new Error( 'winkComposer/flow: emitter adapter must be an imported module' );
|
|
185
206
|
}
|
|
186
207
|
if ( typeof adapter.id !== 'string' || !adapter.id ) {
|
|
187
|
-
throw new Error( '
|
|
208
|
+
throw new Error( 'winkComposer/flow: emitter adapter must have an id' );
|
|
188
209
|
}
|
|
189
210
|
if ( typeof adapter.createEmitter !== 'function' ) {
|
|
190
|
-
throw new Error( '
|
|
211
|
+
throw new Error( 'winkComposer/flow: emitter adapter must have a createEmitter() function' );
|
|
191
212
|
}
|
|
192
213
|
|
|
193
214
|
// If the adapter provides a configSchema, we validate config here.
|
|
@@ -195,7 +216,7 @@ const flow = function ( flowName = 'flow1' ) {
|
|
|
195
216
|
// not later inside adapter.createEmitter().
|
|
196
217
|
if ( adapter.configSchema ) {
|
|
197
218
|
const validation = validateWithSchema( adapter.configSchema, emitterConfig, 'emitter' );
|
|
198
|
-
|
|
219
|
+
throwIfAdapterConfigInvalid( validation, `flow.emitter.${adapter.id}` );
|
|
199
220
|
}
|
|
200
221
|
|
|
201
222
|
runtime.emitters[ adapter.id ] = { adapter, config: emitterConfig };
|
|
@@ -205,13 +226,13 @@ const flow = function ( flowName = 'flow1' ) {
|
|
|
205
226
|
api.storage = function ( adapter, storageConfig = {} ) {
|
|
206
227
|
enforceConfigFirst( 'storage' );
|
|
207
228
|
if ( typeof adapter !== 'object' || adapter === null ) {
|
|
208
|
-
throw new Error( '
|
|
229
|
+
throw new Error( 'winkComposer/flow: storage adapter must be an imported module' );
|
|
209
230
|
}
|
|
210
231
|
if ( typeof adapter.id !== 'string' || !adapter.id ) {
|
|
211
|
-
throw new Error( '
|
|
232
|
+
throw new Error( 'winkComposer/flow: storage adapter must have an id' );
|
|
212
233
|
}
|
|
213
234
|
if ( typeof adapter.createStorage !== 'function' ) {
|
|
214
|
-
throw new Error( '
|
|
235
|
+
throw new Error( 'winkComposer/flow: storage adapter must have a createStorage() function' );
|
|
215
236
|
}
|
|
216
237
|
|
|
217
238
|
// If the adapter provides a configSchema, we validate config here.
|
|
@@ -219,7 +240,7 @@ const flow = function ( flowName = 'flow1' ) {
|
|
|
219
240
|
// not later inside adapter.createStorage().
|
|
220
241
|
if ( adapter.configSchema ) {
|
|
221
242
|
const validation = validateWithSchema( adapter.configSchema, storageConfig, 'storage' );
|
|
222
|
-
|
|
243
|
+
throwIfAdapterConfigInvalid( validation, `flow.storage.${adapter.id}` );
|
|
223
244
|
}
|
|
224
245
|
|
|
225
246
|
runtime.storages[ adapter.id ] = { adapter, config: storageConfig };
|
|
@@ -230,7 +251,7 @@ const flow = function ( flowName = 'flow1' ) {
|
|
|
230
251
|
enforceConfigFirst( 'assetId' );
|
|
231
252
|
|
|
232
253
|
if ( typeof field !== 'string' || field.length === 0 ) {
|
|
233
|
-
throw new Error( '
|
|
254
|
+
throw new Error( 'winkComposer/flow: .assetId() requires a non-empty string field name' );
|
|
234
255
|
}
|
|
235
256
|
|
|
236
257
|
// Schema-based validation for robust error handling
|
|
@@ -251,7 +272,7 @@ const flow = function ( flowName = 'flow1' ) {
|
|
|
251
272
|
if ( assetClassDef === null || assetClassDef === undefined || typeof assetClassDef !== 'object' ) {
|
|
252
273
|
const actualType = assetClassDef === null ? 'null' : typeof assetClassDef;
|
|
253
274
|
throw new Error(
|
|
254
|
-
`
|
|
275
|
+
`winkComposer/flow.assetClass: Expected object, got ${actualType}`
|
|
255
276
|
);
|
|
256
277
|
}
|
|
257
278
|
|
|
@@ -266,7 +287,7 @@ const flow = function ( flowName = 'flow1' ) {
|
|
|
266
287
|
const validation = validateWithSchema( assetClassSchema, assetClassDef, 'assetClass' );
|
|
267
288
|
if ( !validation.valid ) {
|
|
268
289
|
throw new Error(
|
|
269
|
-
`
|
|
290
|
+
`winkComposer/flow.assetClass: validation failed:\n - ${validation.errors.join( '\n - ' )}`
|
|
270
291
|
);
|
|
271
292
|
}
|
|
272
293
|
|
|
@@ -298,7 +319,7 @@ const flow = function ( flowName = 'flow1' ) {
|
|
|
298
319
|
*/
|
|
299
320
|
api.switch = function ( field ) {
|
|
300
321
|
if ( typeof field !== 'string' || field.length === 0 ) {
|
|
301
|
-
throw new Error( '
|
|
322
|
+
throw new Error( 'winkComposer/flow: .switch() requires a non-empty string field name' );
|
|
302
323
|
}
|
|
303
324
|
|
|
304
325
|
// Schema-based validation: field must be a valid identifier (mirrors .assetId())
|
|
@@ -307,21 +328,21 @@ const flow = function ( flowName = 'flow1' ) {
|
|
|
307
328
|
|
|
308
329
|
// Check groupBy before switchState (endGroup sets switchState.active)
|
|
309
330
|
if ( groupByState.active || groupByState.expanded ) {
|
|
310
|
-
throw new Error( '
|
|
331
|
+
throw new Error( 'winkComposer/flow: .switch() cannot be used with .groupBy()' );
|
|
311
332
|
}
|
|
312
333
|
if ( switchState.active ) {
|
|
313
|
-
throw new Error( '
|
|
334
|
+
throw new Error( 'winkComposer/flow: .switch() can only be called once per flow' );
|
|
314
335
|
}
|
|
315
336
|
if ( flowDefinition.length > 0 ) {
|
|
316
337
|
throw new Error(
|
|
317
|
-
'
|
|
338
|
+
'winkComposer/flow: .switch() cannot be called after nodes - ' +
|
|
318
339
|
'use .switch() before defining any nodes, or omit .switch() for single-pipeline flows'
|
|
319
340
|
);
|
|
320
341
|
}
|
|
321
342
|
|
|
322
343
|
// Validate field conflict with partition
|
|
323
344
|
if ( runtime.partitionField === field ) {
|
|
324
|
-
throw new Error( '
|
|
345
|
+
throw new Error( 'winkComposer/flow: partition field and switch field must be different' );
|
|
325
346
|
}
|
|
326
347
|
|
|
327
348
|
runtime.specializationField = field;
|
|
@@ -340,15 +361,15 @@ const flow = function ( flowName = 'flow1' ) {
|
|
|
340
361
|
*/
|
|
341
362
|
api.case = function ( key ) {
|
|
342
363
|
if ( !switchState.active ) {
|
|
343
|
-
throw new Error( '
|
|
364
|
+
throw new Error( 'winkComposer/flow: .case() requires an active .switch()' );
|
|
344
365
|
}
|
|
345
366
|
if ( switchState.currentCase !== null && !switchState.caseEnded ) {
|
|
346
367
|
throw new Error(
|
|
347
|
-
`
|
|
368
|
+
`winkComposer/flow: previous case '${switchState.currentCase}' must end with .break() before starting a new case`
|
|
348
369
|
);
|
|
349
370
|
}
|
|
350
371
|
if ( switchState.caseSpecs[ key ] !== undefined ) {
|
|
351
|
-
throw new Error( `
|
|
372
|
+
throw new Error( `winkComposer/flow: duplicate case key '${key}'` );
|
|
352
373
|
}
|
|
353
374
|
|
|
354
375
|
switchState.currentCase = key;
|
|
@@ -367,14 +388,14 @@ const flow = function ( flowName = 'flow1' ) {
|
|
|
367
388
|
*/
|
|
368
389
|
api.break = function () {
|
|
369
390
|
if ( !switchState.active ) {
|
|
370
|
-
throw new Error( '
|
|
391
|
+
throw new Error( 'winkComposer/flow: .break() requires an active .switch()' );
|
|
371
392
|
}
|
|
372
393
|
if ( switchState.currentCase === null || switchState.caseEnded ) {
|
|
373
|
-
throw new Error( '
|
|
394
|
+
throw new Error( 'winkComposer/flow: .break() must follow a .case() with at least one node' );
|
|
374
395
|
}
|
|
375
396
|
if ( switchState.caseSpecs[ switchState.currentCase ].length === 0 ) {
|
|
376
397
|
throw new Error(
|
|
377
|
-
`
|
|
398
|
+
`winkComposer/flow: case '${switchState.currentCase}' is empty - add at least one node before .break()`
|
|
378
399
|
);
|
|
379
400
|
}
|
|
380
401
|
|
|
@@ -408,7 +429,7 @@ const flow = function ( flowName = 'flow1' ) {
|
|
|
408
429
|
api.groupBy = function ( field, values ) {
|
|
409
430
|
// Validation: field must be non-empty string
|
|
410
431
|
if ( typeof field !== 'string' || field.length === 0 ) {
|
|
411
|
-
throw new Error( '
|
|
432
|
+
throw new Error( 'winkComposer/flow: .groupBy() requires a non-empty string field name' );
|
|
412
433
|
}
|
|
413
434
|
|
|
414
435
|
// Schema-based validation: field must be a valid identifier (mirrors .assetId())
|
|
@@ -417,14 +438,14 @@ const flow = function ( flowName = 'flow1' ) {
|
|
|
417
438
|
|
|
418
439
|
// Validation: values must be array with at least 2 elements
|
|
419
440
|
if ( !Array.isArray( values ) || values.length < 2 ) {
|
|
420
|
-
throw new Error( '
|
|
441
|
+
throw new Error( 'winkComposer/flow: .groupBy() requires at least 2 group values' );
|
|
421
442
|
}
|
|
422
443
|
|
|
423
444
|
// Validation: each value must be string or number
|
|
424
445
|
for ( let i = 0; i < values.length; i += 1 ) {
|
|
425
446
|
const v = values[ i ];
|
|
426
447
|
if ( typeof v !== 'string' && typeof v !== 'number' ) {
|
|
427
|
-
throw new Error( `
|
|
448
|
+
throw new Error( `winkComposer/flow: group value at index ${i} must be a string or number` );
|
|
428
449
|
}
|
|
429
450
|
}
|
|
430
451
|
|
|
@@ -433,32 +454,32 @@ const flow = function ( flowName = 'flow1' ) {
|
|
|
433
454
|
for ( let i = 0; i < values.length; i += 1 ) {
|
|
434
455
|
const v = values[ i ];
|
|
435
456
|
if ( seen[ v ] ) {
|
|
436
|
-
throw new Error( `
|
|
457
|
+
throw new Error( `winkComposer/flow: duplicate group value '${v}' in .groupBy()` );
|
|
437
458
|
}
|
|
438
459
|
seen[ v ] = 1;
|
|
439
460
|
}
|
|
440
461
|
|
|
441
462
|
// Validation: cannot call groupBy twice (check this first - user intent is to reuse groupBy)
|
|
442
463
|
if ( groupByState.active || groupByState.expanded ) {
|
|
443
|
-
throw new Error( '
|
|
464
|
+
throw new Error( 'winkComposer/flow: .groupBy() can only be called once per flow' );
|
|
444
465
|
}
|
|
445
466
|
|
|
446
467
|
// Validation: cannot use groupBy if switchState is active (manual switch, not from endGroup)
|
|
447
468
|
if ( switchState.active ) {
|
|
448
|
-
throw new Error( '
|
|
469
|
+
throw new Error( 'winkComposer/flow: .groupBy() cannot be used inside .switch()' );
|
|
449
470
|
}
|
|
450
471
|
|
|
451
472
|
// Validation: must come before any nodes
|
|
452
473
|
if ( flowDefinition.length > 0 ) {
|
|
453
474
|
throw new Error(
|
|
454
|
-
'
|
|
475
|
+
'winkComposer/flow: .groupBy() cannot be called after nodes - ' +
|
|
455
476
|
'use .groupBy() before defining any nodes'
|
|
456
477
|
);
|
|
457
478
|
}
|
|
458
479
|
|
|
459
480
|
// Validation: groupBy field cannot equal partition field
|
|
460
481
|
if ( runtime.partitionField === field ) {
|
|
461
|
-
throw new Error( '
|
|
482
|
+
throw new Error( 'winkComposer/flow: partition field and groupBy field must be different' );
|
|
462
483
|
}
|
|
463
484
|
|
|
464
485
|
// Initialize groupBy state
|
|
@@ -487,12 +508,12 @@ const flow = function ( flowName = 'flow1' ) {
|
|
|
487
508
|
api.endGroup = function () {
|
|
488
509
|
// Validation: must be in active groupBy state
|
|
489
510
|
if ( !groupByState.active ) {
|
|
490
|
-
throw new Error( '
|
|
511
|
+
throw new Error( 'winkComposer/flow: .endGroup() requires an active .groupBy()' );
|
|
491
512
|
}
|
|
492
513
|
|
|
493
514
|
// Validation: must have at least one template node
|
|
494
515
|
if ( groupByState.templateSpecs.length === 0 ) {
|
|
495
|
-
throw new Error( '
|
|
516
|
+
throw new Error( 'winkComposer/flow: .groupBy() must contain at least one node before .endGroup()' );
|
|
496
517
|
}
|
|
497
518
|
|
|
498
519
|
// Set specialization field (same as switch does)
|
|
@@ -553,7 +574,7 @@ const flow = function ( flowName = 'flow1' ) {
|
|
|
553
574
|
|
|
554
575
|
// Single-pipeline mode (backward compatible)
|
|
555
576
|
if ( flowDefinition.length === 0 ) {
|
|
556
|
-
throw new Error( '
|
|
577
|
+
throw new Error( 'winkComposer/flow: Cannot build empty flow - add at least one node' );
|
|
557
578
|
}
|
|
558
579
|
const imports = Array.from( importSet ).sort();
|
|
559
580
|
return serializeModule( { imports, flowName, specs: flowDefinition } );
|
|
@@ -646,7 +667,7 @@ const flow = function ( flowName = 'flow1' ) {
|
|
|
646
667
|
|
|
647
668
|
// Single-pipeline mode (backward compatible)
|
|
648
669
|
if ( flowDefinition.length === 0 ) {
|
|
649
|
-
throw new Error( '
|
|
670
|
+
throw new Error( 'winkComposer/flow: Cannot run empty flow - add at least one node' );
|
|
650
671
|
}
|
|
651
672
|
const handle = await runFlow( flowName, flowDefinition, importSet, runtime );
|
|
652
673
|
return handle;
|