@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.
Files changed (125) hide show
  1. package/README.md +1 -1
  2. package/package.json +3 -3
  3. package/src/composer.js +1 -1
  4. package/src/core/batch/aoo-encoder.js +4 -4
  5. package/src/core/batch/sliding-window.js +2 -2
  6. package/src/core/codec/index.js +4 -4
  7. package/src/core/emitter-manager/mqtt/connection.js +209 -0
  8. package/src/core/emitter-manager/mqtt/constants.js +43 -29
  9. package/src/core/emitter-manager/mqtt/emitter.js +108 -633
  10. package/src/core/emitter-manager/mqtt/health.js +151 -0
  11. package/src/core/emitter-manager/mqtt/index.js +68 -4
  12. package/src/core/emitter-manager/mqtt/publish.js +292 -0
  13. package/src/core/emitter-manager/mqtt/resolve-config.js +312 -0
  14. package/src/core/emitter-manager/mqtt/shutdown-drain.js +247 -0
  15. package/src/core/emitter-manager/terminal/assert-columns.js +1 -1
  16. package/src/core/emitter-manager/terminal/index.js +20 -5
  17. package/src/core/env-vars.js +143 -16
  18. package/src/core/logger/index.js +174 -0
  19. package/src/core/mqtt-protocol.js +2 -2
  20. package/src/core/partition-manager/get-stats.js +31 -0
  21. package/src/core/partition-manager/index.js +1 -0
  22. package/src/core/partition-manager/init.js +15 -0
  23. package/src/core/partition-manager/update.js +143 -87
  24. package/src/core/partition-manager/validate-flow.js +1 -1
  25. package/src/core/semantics/loader.js +1 -1
  26. package/src/core/semantics/warnings.js +5 -3
  27. package/src/core/shutdown-manager/index.js +71 -21
  28. package/src/core/source-manager/csv/start.js +109 -35
  29. package/src/core/source-manager/mqtt/client.js +276 -83
  30. package/src/core/source-manager/mqtt/dedup.js +19 -14
  31. package/src/core/source-manager/mqtt/index.js +33 -6
  32. package/src/core/source-manager/mqtt/status.js +211 -46
  33. package/src/core/source-manager/record-shape.js +55 -0
  34. package/src/core/source-manager/test-harness/field-generator.js +1 -1
  35. package/src/core/source-manager/test-harness/start.js +52 -10
  36. package/src/core/source-manager/test-harness/validate.js +1 -1
  37. package/src/core/storage-manager/questdb/address-policy.js +264 -0
  38. package/src/core/storage-manager/questdb/assert-columns.js +2 -2
  39. package/src/core/storage-manager/questdb/delivery-gate.js +218 -0
  40. package/src/core/storage-manager/questdb/ensure-tables.js +1 -1
  41. package/src/core/storage-manager/questdb/flush-engine.js +852 -0
  42. package/src/core/storage-manager/questdb/flush-tracker.js +331 -0
  43. package/src/core/storage-manager/questdb/index.js +592 -827
  44. package/src/core/storage-manager/questdb/persist-plan.js +147 -162
  45. package/src/core/storage-manager/questdb/resolve-options.js +403 -0
  46. package/src/core/storage-manager/questdb/shutdown-drain.js +181 -0
  47. package/src/core/storage-manager/questdb/skip-warnings.js +180 -0
  48. package/src/core/test-utils/tcp-proxy.js +194 -1
  49. package/src/core/tunable/helpers.js +1 -1
  50. package/src/core/utils/address/index.js +362 -0
  51. package/src/core/utils/address/probe.js +210 -0
  52. package/src/core/utils/callback-guard/index.js +176 -0
  53. package/src/core/utils/clock/index.js +34 -0
  54. package/src/core/utils/half-life/index.js +4 -4
  55. package/src/core/utils/jitter/index.js +51 -0
  56. package/src/core/utils/line-rate/index.js +104 -0
  57. package/src/core/utils/quantize/index.js +43 -19
  58. package/src/core/utils/validate/composer-validators.js +2 -2
  59. package/src/core/utils/validate/index.js +1 -1
  60. package/src/core/utils/validate/validate.js +1 -1
  61. package/src/core/wiring/assert-handle.js +5 -5
  62. package/src/core/wiring/assert-module.js +2 -2
  63. package/src/core/wiring/wire-emitters.js +7 -4
  64. package/src/core/wiring/wire-linear-graph.js +1 -1
  65. package/src/core/wiring/wire-node.js +6 -6
  66. package/src/core/wiring/wire-semantics.js +1 -1
  67. package/src/core/wiring/wire-storages.js +14 -5
  68. package/src/flow/driver.js +31 -6
  69. package/src/flow/expand-single-input-array.js +4 -4
  70. package/src/flow/flow-api-schemas.js +4 -4
  71. package/src/flow/flow.js +62 -41
  72. package/src/flow/for-each.js +12 -11
  73. package/src/flow/get-target-array.js +2 -2
  74. package/src/flow/load-node-module.js +1 -1
  75. package/src/flow/make-node-method.js +4 -4
  76. package/src/flow/resolve-pick-by-field.js +1 -1
  77. package/src/flow/run.js +200 -40
  78. package/src/flow/serialize.js +1 -1
  79. package/src/flow/spec-builder.js +1 -1
  80. package/src/flow/validate.js +1 -1
  81. package/src/nodes/butterworth-filter/init.js +5 -4
  82. package/src/nodes/categorize/update.js +3 -1
  83. package/src/nodes/controller/update.js +2 -1
  84. package/src/nodes/dwell-time-tracker/update.js +2 -1
  85. package/src/nodes/emit-if/helpers.js +57 -11
  86. package/src/nodes/emit-if/init.js +1 -1
  87. package/src/nodes/emit-if/update.js +8 -11
  88. package/src/nodes/page-hinkley/update.js +2 -1
  89. package/src/nodes/pass-if/update.js +3 -1
  90. package/src/nodes/persist-if/helpers.js +60 -4
  91. package/src/nodes/persist-if/init.js +1 -1
  92. package/src/nodes/persist-if/update.js +8 -16
  93. package/src/nodes/persistence-check/update.js +2 -1
  94. package/src/nodes/sanitize/update.js +4 -2
  95. package/src/nodes/swing-watch/update.js +3 -2
  96. package/src/nodes/threshold/update.js +2 -1
  97. package/src/nodes/trend/compute-confidence.js +4 -4
  98. package/src/nodes/trend/update.js +2 -1
  99. package/src/nodes/winnow/update.js +3 -1
  100. package/src/tools/training/features.js +13 -13
  101. package/src/tools/training/logistic.js +1 -1
  102. package/src/tools/training/metrics.js +2 -2
  103. package/src/tools/training/scale.js +2 -2
  104. package/src/tools/training/validate.js +7 -7
  105. package/src/core/utils/console-formatter/index.js +0 -207
  106. package/src/nodes/archive/csv/index.js +0 -6
  107. package/src/nodes/archive/csv/init.js +0 -36
  108. package/src/nodes/archive/csv/load.js +0 -70
  109. package/src/nodes/archive/csv/reset.js +0 -14
  110. package/src/nodes/archive/csv/simple-parser.js +0 -216
  111. package/src/nodes/archive/csv/test.js +0 -173
  112. package/src/nodes/archive/csv/update.js +0 -63
  113. package/src/nodes/sse-emitter/index.js +0 -7
  114. package/src/nodes/sse-emitter/init.js +0 -105
  115. package/src/nodes/sse-emitter/introspect.js +0 -134
  116. package/src/nodes/sse-emitter/publish-to.js +0 -6
  117. package/src/nodes/sse-emitter/recompute.js +0 -16
  118. package/src/nodes/sse-emitter/reset.js +0 -19
  119. package/src/nodes/sse-emitter/update.js +0 -69
  120. package/src/nodes/sse-emitter/utils/broadcast-heartbeat.js +0 -36
  121. package/src/nodes/sse-emitter/utils/build-message.js +0 -31
  122. package/src/nodes/sse-emitter/utils/connection-manager.js +0 -45
  123. package/src/nodes/sse-emitter/utils/endpoint-initializer.js +0 -20
  124. package/src/nodes/sse-emitter/utils/health-monitor.js +0 -38
  125. 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` — the user's `transform` threw on one row. The
44
- * row is skipped (counted in `skipped`), the throw is reported per
45
- * record with `status: 'yellow'`, and the stream continues — user
46
- * code is never reported as a stream failure. Fix the transform
47
- * function; the report names the data row. Uniform with the MQTT
48
- * source (transform contract, 2026-07-11).
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) and the stream continues
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 ( onStatus ) {
230
- onStatus( {
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
- console.error( `CSV source error [DECODE_ERROR]: ${message}` );
287
+ logger.error( `winkComposer/csvSource: decode failed [DECODE_ERROR]: ${message}` );
238
288
  }
239
289
  };
240
290
 
241
- // A throwing transform is user code, never a stream failure: the
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
- const reportCallbackError = function ( dataRowIndex, err ) {
247
- const message = `data row ${dataRowIndex}: transform threw: ${err.message} — row skipped`;
248
- if ( onStatus ) {
249
- onStatus( {
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
- console.error( `CSV source error [CALLBACK_FAILED]: ${message}` );
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 transform threw (reported above) — the caller
262
- // counts every null/undefined result as skipped.
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 ( !transform ) {
328
+ if ( !guardedTransform ) {
265
329
  return row;
266
330
  }
267
- try {
268
- return transform( row );
269
- } catch ( err ) {
270
- reportCallbackError( dataRowIndex, err );
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 ( onStatus ) onStatus( {
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 ( onStatus ) onStatus( {
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
- // Only null/undefined mean drop — any other return,
344
- // however falsy, is delivered (transform contract,
345
- // uniform with the MQTT source, 2026-07-11).
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 ( onStatus ) onStatus( {
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 ( onStatus ) {
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
- onStatus( {
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
- console.error( `CSV source error [${code}]: ${message}` );
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 ( onStatus ) {
442
- onStatus( {
515
+ if ( safeOnStatus ) {
516
+ safeOnStatus( {
443
517
  status: 'yellow',
444
518
  connected: false,
445
519
  phase: 'stopped',