@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
|
@@ -25,6 +25,11 @@
|
|
|
25
25
|
* malformed, transform-dropped, or
|
|
26
26
|
* transform-failed. For in-range rows,
|
|
27
27
|
* count + skipped covers every data row read.
|
|
28
|
+
* `count` means handed to the flow: a row the
|
|
29
|
+
* pipeline later failed on still counts here.
|
|
30
|
+
* Failure visibility belongs to the flow's
|
|
31
|
+
* MESSAGE_HANDLER_FAILED reports (ADR-018),
|
|
32
|
+
* not to this counter.
|
|
28
33
|
* `phase: 'errored'` — terminal red: the run loop failed and the
|
|
29
34
|
* stream is dead (the ADR-018 two-tier rule).
|
|
30
35
|
* `phase: 'stopped'` — forced stop exceeded its time budget
|
|
@@ -40,19 +45,45 @@
|
|
|
40
45
|
* stream continues — per-record skip-classify-continue (ADR-018).
|
|
41
46
|
* A bad field VALUE in a parseable row is NOT a decode error;
|
|
42
47
|
* it passes through for the pipeline (`sanitize`) to judge.
|
|
43
|
-
* - `CALLBACK_FAILED` —
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
48
|
+
* - `CALLBACK_FAILED` — a piece of user code failed. It covers two
|
|
49
|
+
* cases. Case one: the user's `transform` threw on one row, or
|
|
50
|
+
* returned a scalar or array where a record object was needed. The
|
|
51
|
+
* row is skipped (counted in `skipped`), the fault is reported per
|
|
52
|
+
* record with `status: 'yellow'`, and the stream continues. User
|
|
53
|
+
* code is never reported as a stream failure. A null/undefined
|
|
54
|
+
* return is NOT this case: it is the documented intentional drop.
|
|
55
|
+
* Fix the transform function; the report names the data row.
|
|
56
|
+
* Uniform with the MQTT source (transform contract, 2026-07-11).
|
|
57
|
+
* Case two: the user's `onStatus` itself threw or rejected. The
|
|
58
|
+
* shared callback guard contains that fault (ADR-018). The replay
|
|
59
|
+
* continues, and each fault becomes one classified console line
|
|
60
|
+
* that carries the detail.
|
|
49
61
|
* - `READ_ERROR` — stream read failed mid-stream (encoding error,
|
|
50
62
|
* truncation, decoder failure, etc.). Catch-all for non-open failures.
|
|
63
|
+
* Narrowed by the flow's dispatch guard (ADR-018): in a flow, a
|
|
64
|
+
* pipeline fault is contained at the flow's own chokepoint and
|
|
65
|
+
* reported as MESSAGE_HANDLER_FAILED, so it never surfaces here and
|
|
66
|
+
* never ends the replay. The code remains live for genuine stream
|
|
67
|
+
* failures, and for direct callers of start() whose own onMessage
|
|
68
|
+
* throws.
|
|
51
69
|
*/
|
|
52
70
|
|
|
53
71
|
import fs from 'node:fs';
|
|
54
72
|
import readline from 'node:readline';
|
|
55
73
|
|
|
74
|
+
import { isUsableRecord, describeShape } from '../record-shape.js';
|
|
75
|
+
import { wrapCallback, wrapTransform, TRANSFORM_THREW } from '../../utils/callback-guard/index.js';
|
|
76
|
+
import { logger } from '../../logger/index.js';
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Console channel for the callback guard: one classified line in this
|
|
80
|
+
* source's family. Receives an already-safe detail string, never the
|
|
81
|
+
* raw thrown value.
|
|
82
|
+
*/
|
|
83
|
+
const reportCallbackFault = function ( severity, name, detail ) {
|
|
84
|
+
logger.error( `winkComposer/csvSource: user callback ${name} failed [CALLBACK_FAILED]: ${detail}` );
|
|
85
|
+
}; // reportCallbackFault()
|
|
86
|
+
|
|
56
87
|
/**
|
|
57
88
|
* Detects delimiter from header line.
|
|
58
89
|
* @param {string} headerLine - First line of CSV
|
|
@@ -187,7 +218,8 @@ const checkRange = function ( rangeKey, wasInRange, startMsgId, endMsgId ) {
|
|
|
187
218
|
* @param {boolean} [config.dynamicTyping=true] - Auto-cast values
|
|
188
219
|
* @param {Function} [config.transform] - Optional row transform:
|
|
189
220
|
* ( row ) => row; return null/undefined to drop (counted in skipped);
|
|
190
|
-
* a throw skips the row (CALLBACK_FAILED)
|
|
221
|
+
* a throw or a scalar/array return skips the row (CALLBACK_FAILED)
|
|
222
|
+
* and the stream continues
|
|
191
223
|
* @param {Function} [config.onStatus] - Status callback; completion arrives
|
|
192
224
|
* here as `{phase: 'complete', count, skipped}` (per ADR-018 there is
|
|
193
225
|
* no onComplete callback)
|
|
@@ -213,12 +245,30 @@ export const start = function ( config ) {
|
|
|
213
245
|
endMsgId = null
|
|
214
246
|
} = config;
|
|
215
247
|
|
|
248
|
+
// The guard below turns a non-function into null (absent). So a
|
|
249
|
+
// misconfigured handler must be rejected here, loudly, before the
|
|
250
|
+
// wrap can silently erase it. Null stays legal as "no handler".
|
|
251
|
+
if ( onStatus !== null && typeof onStatus !== 'function' ) {
|
|
252
|
+
const err = new Error( 'winkComposer/csvSource: onStatus must be a function' );
|
|
253
|
+
err.code = 'INVALID_CONFIG';
|
|
254
|
+
throw err;
|
|
255
|
+
}
|
|
256
|
+
|
|
216
257
|
let stopped = false;
|
|
217
258
|
let rowCount = 0;
|
|
218
259
|
let rowIndex = 0;
|
|
219
260
|
let skippedCount = 0;
|
|
220
261
|
let inRange = ( startMsgId === null ); // Start immediately if no startMsgId
|
|
221
262
|
|
|
263
|
+
// The user's onStatus is armed once by the shared callback guard
|
|
264
|
+
// (ADR-018). A throw or rejection inside it becomes one classified
|
|
265
|
+
// CALLBACK_FAILED console line, and the replay continues. Absent
|
|
266
|
+
// stays null, so every no-handler console fallback below keeps
|
|
267
|
+
// its exact meaning.
|
|
268
|
+
const safeOnStatus = wrapCallback( onStatus, {
|
|
269
|
+
name: 'onStatus', severity: 'red', report: reportCallbackFault
|
|
270
|
+
} );
|
|
271
|
+
|
|
222
272
|
// Per-record skip-classify-continue (ADR-018): a structurally
|
|
223
273
|
// malformed row is skipped and signalled — never a silent drop. When
|
|
224
274
|
// the caller owns reporting (onStatus supplied) the framework stays
|
|
@@ -226,50 +276,73 @@ export const start = function ( config ) {
|
|
|
226
276
|
// visible (ADR-018's two-party rule).
|
|
227
277
|
const reportDecodeError = function ( dataRowIndex, fault ) {
|
|
228
278
|
const message = `data row ${dataRowIndex}: ${fault} — row skipped`;
|
|
229
|
-
if (
|
|
230
|
-
|
|
279
|
+
if ( safeOnStatus ) {
|
|
280
|
+
safeOnStatus( {
|
|
231
281
|
status: 'yellow',
|
|
232
282
|
connected: true,
|
|
233
283
|
phase: 'running',
|
|
234
284
|
error: { code: 'DECODE_ERROR', message }
|
|
235
285
|
} );
|
|
236
286
|
} else {
|
|
237
|
-
|
|
287
|
+
logger.error( `winkComposer/csvSource: decode failed [DECODE_ERROR]: ${message}` );
|
|
238
288
|
}
|
|
239
289
|
};
|
|
240
290
|
|
|
241
|
-
// A
|
|
291
|
+
// A faulty transform is user code, never a stream failure: the
|
|
242
292
|
// row is skipped with a per-record CALLBACK_FAILED report and the
|
|
243
293
|
// stream continues (transform contract, uniform with the MQTT
|
|
244
294
|
// source, 2026-07-11). Same two-party reporting rule as decode
|
|
245
|
-
// errors above.
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
295
|
+
// errors above. `detail` names the fault (a throw, or an
|
|
296
|
+
// unusable return shape).
|
|
297
|
+
const reportCallbackError = function ( dataRowIndex, detail ) {
|
|
298
|
+
const message = `data row ${dataRowIndex}: ${detail} — row skipped`;
|
|
299
|
+
if ( safeOnStatus ) {
|
|
300
|
+
safeOnStatus( {
|
|
250
301
|
status: 'yellow',
|
|
251
302
|
connected: true,
|
|
252
303
|
phase: 'running',
|
|
253
304
|
error: { code: 'CALLBACK_FAILED', message }
|
|
254
305
|
} );
|
|
255
306
|
} else {
|
|
256
|
-
|
|
307
|
+
logger.error( `winkComposer/csvSource: source error [CALLBACK_FAILED]: ${message}` );
|
|
257
308
|
}
|
|
258
309
|
};
|
|
259
310
|
|
|
311
|
+
// The user's transform is pre-armed by the shared callback guard,
|
|
312
|
+
// once here — never per row (ADR-018). A throw inside it becomes
|
|
313
|
+
// the sentinel plus one per-record CALLBACK_FAILED report. The
|
|
314
|
+
// row index travels as the guard's per-call context, so the
|
|
315
|
+
// success path allocates nothing.
|
|
316
|
+
const reportTransformFault = function ( detail, dataRowIndex ) {
|
|
317
|
+
reportCallbackError( dataRowIndex, `transform threw: ${detail}` );
|
|
318
|
+
};
|
|
319
|
+
const guardedTransform = transform ? wrapTransform( transform, reportTransformFault ) : null;
|
|
320
|
+
|
|
260
321
|
// Run the user's transform under guard. Returns the transformed
|
|
261
|
-
// row; null when the
|
|
262
|
-
//
|
|
322
|
+
// row; null when the row must be skipped — the caller counts
|
|
323
|
+
// every null/undefined result as skipped. Three skip faces: a
|
|
324
|
+
// throw (reported), a scalar or array return (reported — an
|
|
325
|
+
// unusable record), and a null/undefined return (the documented
|
|
326
|
+
// intentional drop, counted but never reported).
|
|
263
327
|
const applyTransform = function ( row, dataRowIndex ) {
|
|
264
|
-
if ( !
|
|
328
|
+
if ( !guardedTransform ) {
|
|
265
329
|
return row;
|
|
266
330
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
331
|
+
const out = guardedTransform( row, dataRowIndex );
|
|
332
|
+
// The sentinel check runs FIRST: the sentinel is a plain
|
|
333
|
+
// object, so a later isUsableRecord check would wave it
|
|
334
|
+
// through as a row.
|
|
335
|
+
if ( out === TRANSFORM_THREW ) {
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
if ( out === null || out === undefined ) {
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
if ( !isUsableRecord( out ) ) {
|
|
342
|
+
reportCallbackError( dataRowIndex, `transform returned ${describeShape( out )} — a record object (or null/undefined to drop) is required` );
|
|
271
343
|
return null;
|
|
272
344
|
}
|
|
345
|
+
return out;
|
|
273
346
|
};
|
|
274
347
|
|
|
275
348
|
// Holds the file stream so the stop function can close it if the
|
|
@@ -292,7 +365,7 @@ export const start = function ( config ) {
|
|
|
292
365
|
let headers = null;
|
|
293
366
|
let delimiter = ',';
|
|
294
367
|
|
|
295
|
-
if (
|
|
368
|
+
if ( safeOnStatus ) safeOnStatus( {
|
|
296
369
|
status: 'green',
|
|
297
370
|
connected: true,
|
|
298
371
|
phase: 'starting',
|
|
@@ -308,7 +381,7 @@ export const start = function ( config ) {
|
|
|
308
381
|
if ( !headers ) {
|
|
309
382
|
delimiter = detectDelimiter( line );
|
|
310
383
|
headers = parseLine( line, delimiter ).values;
|
|
311
|
-
if (
|
|
384
|
+
if ( safeOnStatus ) safeOnStatus( {
|
|
312
385
|
status: 'green',
|
|
313
386
|
connected: true,
|
|
314
387
|
phase: 'headers',
|
|
@@ -340,9 +413,10 @@ export const start = function ( config ) {
|
|
|
340
413
|
if ( inRange ) {
|
|
341
414
|
const msg = applyTransform( row, rowIndex );
|
|
342
415
|
|
|
343
|
-
//
|
|
344
|
-
//
|
|
345
|
-
//
|
|
416
|
+
// applyTransform maps every skip face (throw, bad
|
|
417
|
+
// shape, intentional drop) to null/undefined; the
|
|
418
|
+
// caller only counts (transform contract, uniform
|
|
419
|
+
// with the MQTT source, 2026-07-11).
|
|
346
420
|
if ( msg !== null && msg !== undefined ) {
|
|
347
421
|
await onMessage( msg );
|
|
348
422
|
rowCount += 1;
|
|
@@ -365,7 +439,7 @@ export const start = function ( config ) {
|
|
|
365
439
|
|
|
366
440
|
// Completion travels onStatus with the uniform `count` field
|
|
367
441
|
// (per ADR-018 there is no onComplete callback).
|
|
368
|
-
if (
|
|
442
|
+
if ( safeOnStatus ) safeOnStatus( {
|
|
369
443
|
status: 'green',
|
|
370
444
|
connected: false,
|
|
371
445
|
phase: 'complete',
|
|
@@ -391,17 +465,17 @@ export const start = function ( config ) {
|
|
|
391
465
|
'SOURCE_UNREACHABLE' :
|
|
392
466
|
'READ_ERROR';
|
|
393
467
|
const message = ( err && err.message ) ? err.message : String( err );
|
|
394
|
-
if (
|
|
468
|
+
if ( safeOnStatus ) {
|
|
395
469
|
// Terminal red: the stream is dead. Uniform payload with
|
|
396
470
|
// phase 'errored' per the ADR-018 two-tier rule.
|
|
397
|
-
|
|
471
|
+
safeOnStatus( {
|
|
398
472
|
status: 'red',
|
|
399
473
|
connected: false,
|
|
400
474
|
phase: 'errored',
|
|
401
475
|
error: { code, message }
|
|
402
476
|
} );
|
|
403
477
|
} else {
|
|
404
|
-
|
|
478
|
+
logger.error( `winkComposer/csvSource: source error [${code}]: ${message}` );
|
|
405
479
|
}
|
|
406
480
|
} ).finally( () => {
|
|
407
481
|
resolveFinished();
|
|
@@ -438,8 +512,8 @@ export const start = function ( config ) {
|
|
|
438
512
|
};
|
|
439
513
|
forceTimer = setTimeout( () => {
|
|
440
514
|
if ( activeStream ) activeStream.destroy();
|
|
441
|
-
if (
|
|
442
|
-
|
|
515
|
+
if ( safeOnStatus ) {
|
|
516
|
+
safeOnStatus( {
|
|
443
517
|
status: 'yellow',
|
|
444
518
|
connected: false,
|
|
445
519
|
phase: 'stopped',
|