@winkjs/composer 0.0.2 → 0.4.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/LICENSE +1 -1
- package/README.md +42 -103
- package/package.json +53 -29
- package/src/composer.js +75 -0
- package/src/core/batch/aoo-encoder.js +148 -0
- package/src/core/batch/index.js +12 -0
- package/src/core/batch/sliding-window.js +91 -0
- package/src/core/codec/index.js +137 -0
- package/src/core/emitter-manager/mqtt/constants.js +102 -0
- package/src/core/emitter-manager/mqtt/emitter.js +780 -0
- package/src/core/emitter-manager/mqtt/index.js +190 -0
- package/src/core/emitter-manager/mqtt/mqtt-store.js +538 -0
- package/src/core/emitter-manager/terminal/assert-columns.js +98 -0
- package/src/core/emitter-manager/terminal/formatters.js +160 -0
- package/src/core/emitter-manager/terminal/index.js +341 -0
- package/src/core/env-vars.js +245 -0
- package/src/core/mqtt-protocol.js +47 -0
- package/src/core/partition-manager/index.js +7 -0
- package/src/core/partition-manager/init.js +44 -0
- package/src/core/partition-manager/introspect.js +15 -0
- package/src/core/partition-manager/publish-to.js +6 -0
- package/src/core/partition-manager/recompute.js +5 -0
- package/src/core/partition-manager/reset.js +5 -0
- package/src/core/partition-manager/update.js +264 -0
- package/src/core/partition-manager/validate-flow.js +157 -0
- package/src/core/semantics/digest.js +128 -0
- package/src/core/semantics/index.js +83 -0
- package/src/core/semantics/loader.js +571 -0
- package/src/core/semantics/schemas/asset-class-schema.js +218 -0
- package/src/core/semantics/schemas/column-schema.js +651 -0
- package/src/core/semantics/schemas/enum-schema.js +153 -0
- package/src/core/semantics/schemas/index.js +51 -0
- package/src/core/semantics/warnings.js +141 -0
- package/src/core/shutdown-manager/index.js +289 -0
- package/src/core/source-manager/csv/index.js +171 -0
- package/src/core/source-manager/csv/start.js +456 -0
- package/src/core/source-manager/mqtt/client.js +406 -0
- package/src/core/source-manager/mqtt/constants.js +147 -0
- package/src/core/source-manager/mqtt/dedup.js +244 -0
- package/src/core/source-manager/mqtt/index.js +260 -0
- package/src/core/source-manager/mqtt/status.js +546 -0
- package/src/core/source-manager/test-harness/comparator.js +226 -0
- package/src/core/source-manager/test-harness/field-generator.js +85 -0
- package/src/core/source-manager/test-harness/fuzz.js +87 -0
- package/src/core/source-manager/test-harness/index.js +115 -0
- package/src/core/source-manager/test-harness/prng.js +57 -0
- package/src/core/source-manager/test-harness/start.js +232 -0
- package/src/core/source-manager/test-harness/validate.js +213 -0
- package/src/core/storage-manager/questdb/assert-columns.js +120 -0
- package/src/core/storage-manager/questdb/ensure-tables.js +187 -0
- package/src/core/storage-manager/questdb/index.js +1251 -0
- package/src/core/storage-manager/questdb/persist-plan.js +488 -0
- package/src/core/storage-manager/questdb/writers.js +142 -0
- package/src/core/test-utils/sealed-record.js +80 -0
- package/src/core/test-utils/soak-signature.js +149 -0
- package/src/core/test-utils/tcp-proxy.js +106 -0
- package/src/core/tunable/helpers.js +254 -0
- package/src/core/tunable/index.js +87 -0
- package/src/core/utils/console-formatter/index.js +207 -0
- package/src/core/utils/create-smart-store-as/index.js +98 -0
- package/src/core/utils/create-smart-store-as/sanitize-field.js +46 -0
- package/src/core/utils/create-smart-store-as/smart-truncate.js +93 -0
- package/src/core/utils/flow/index.js +5 -0
- package/src/core/utils/flow/make-collision-checker.js +23 -0
- package/src/core/utils/flow/to-kebab.js +20 -0
- package/src/core/utils/half-life/index.js +111 -0
- package/src/core/utils/node/disable.js +11 -0
- package/src/core/utils/node/enable.js +11 -0
- package/src/core/utils/node/execute-triggers.js +40 -0
- package/src/core/utils/node/index.js +10 -0
- package/src/core/utils/node/pause.js +11 -0
- package/src/core/utils/node/populate-predicate-inputs.js +14 -0
- package/src/core/utils/node/publish-nan.js +19 -0
- package/src/core/utils/node/reset-predicate-inputs.js +11 -0
- package/src/core/utils/node/unpause.js +11 -0
- package/src/core/utils/node/validate-spec.js +22 -0
- package/src/core/utils/options/resolve-field-keyed.js +115 -0
- package/src/core/utils/quantize/index.js +75 -0
- package/src/core/utils/stats/formulas.js +67 -0
- package/src/core/utils/stats/index.js +3 -0
- package/src/core/utils/validate/composer-validators.js +141 -0
- package/src/core/utils/validate/helpers.js +206 -0
- package/src/core/utils/validate/index.js +12 -0
- package/src/core/utils/validate/type-validators.js +538 -0
- package/src/core/utils/validate/validate.js +107 -0
- package/src/core/utils/validate/validators.js +177 -0
- package/src/core/wiring/assert-handle.js +57 -0
- package/src/core/wiring/assert-module.js +63 -0
- package/src/core/wiring/index.js +5 -0
- package/src/core/wiring/node-type-to-module.js +17 -0
- package/src/core/wiring/wire-emitters.js +256 -0
- package/src/core/wiring/wire-linear-graph.js +25 -0
- package/src/core/wiring/wire-node.js +157 -0
- package/src/core/wiring/wire-semantics.js +173 -0
- package/src/core/wiring/wire-storages.js +344 -0
- package/src/flow/check-output-collisions.js +93 -0
- package/src/flow/consts.js +9 -0
- package/src/flow/deep-clone-spec.js +64 -0
- package/src/flow/driver.js +171 -0
- package/src/flow/expand-single-input-array.js +76 -0
- package/src/flow/expand-template-spec.js +93 -0
- package/src/flow/flow-api-schemas.js +300 -0
- package/src/flow/flow.js +658 -0
- package/src/flow/for-each.js +225 -0
- package/src/flow/get-signature-pattern.js +31 -0
- package/src/flow/get-target-array.js +46 -0
- package/src/flow/inspect.js +67 -0
- package/src/flow/load-node-module.js +81 -0
- package/src/flow/make-node-method.js +112 -0
- package/src/flow/resolve-pick-by-field.js +77 -0
- package/src/flow/resolve-tunables.js +76 -0
- package/src/flow/run.js +392 -0
- package/src/flow/serialize.js +161 -0
- package/src/flow/signatures-schema.js +222 -0
- package/src/flow/spec-builder.js +65 -0
- package/src/flow/validate-triggers.js +250 -0
- package/src/flow/validate.js +261 -0
- package/src/nodes/accumulate/index.js +21 -0
- package/src/nodes/accumulate/init.js +48 -0
- package/src/nodes/accumulate/introspect.js +123 -0
- package/src/nodes/accumulate/publish-to.js +34 -0
- package/src/nodes/accumulate/recompute.js +27 -0
- package/src/nodes/accumulate/reset.js +25 -0
- package/src/nodes/accumulate/update.js +42 -0
- package/src/nodes/appraise/calibrate.js +64 -0
- package/src/nodes/appraise/decision.js +66 -0
- package/src/nodes/appraise/deviation.js +140 -0
- package/src/nodes/appraise/index.js +23 -0
- package/src/nodes/appraise/init.js +165 -0
- package/src/nodes/appraise/integrate.js +45 -0
- package/src/nodes/appraise/introspect.js +352 -0
- package/src/nodes/appraise/publish-to.js +93 -0
- package/src/nodes/appraise/receptor.js +69 -0
- package/src/nodes/appraise/recompute.js +79 -0
- package/src/nodes/appraise/reset.js +53 -0
- package/src/nodes/appraise/update.js +130 -0
- package/src/nodes/archive/csv/index.js +6 -0
- package/src/nodes/archive/csv/init.js +36 -0
- package/src/nodes/archive/csv/load.js +70 -0
- package/src/nodes/archive/csv/reset.js +14 -0
- package/src/nodes/archive/csv/simple-parser.js +216 -0
- package/src/nodes/archive/csv/test.js +173 -0
- package/src/nodes/archive/csv/update.js +63 -0
- package/src/nodes/butterworth-filter/index.js +18 -0
- package/src/nodes/butterworth-filter/init.js +184 -0
- package/src/nodes/butterworth-filter/introspect.js +228 -0
- package/src/nodes/butterworth-filter/publish-to.js +21 -0
- package/src/nodes/butterworth-filter/recompute.js +13 -0
- package/src/nodes/butterworth-filter/reset.js +30 -0
- package/src/nodes/butterworth-filter/update.js +38 -0
- package/src/nodes/categorize/index.js +19 -0
- package/src/nodes/categorize/init.js +58 -0
- package/src/nodes/categorize/introspect.js +177 -0
- package/src/nodes/categorize/publish-to.js +29 -0
- package/src/nodes/categorize/recompute.js +14 -0
- package/src/nodes/categorize/reset.js +16 -0
- package/src/nodes/categorize/update.js +53 -0
- package/src/nodes/controller/index.js +9 -0
- package/src/nodes/controller/init.js +55 -0
- package/src/nodes/controller/introspect.js +91 -0
- package/src/nodes/controller/publish-to.js +8 -0
- package/src/nodes/controller/recompute.js +9 -0
- package/src/nodes/controller/reset.js +17 -0
- package/src/nodes/controller/update.js +63 -0
- package/src/nodes/diff/index.js +19 -0
- package/src/nodes/diff/init.js +37 -0
- package/src/nodes/diff/introspect.js +132 -0
- package/src/nodes/diff/publish-to.js +21 -0
- package/src/nodes/diff/recompute.js +12 -0
- package/src/nodes/diff/reset.js +12 -0
- package/src/nodes/diff/update.js +37 -0
- package/src/nodes/digest-moments/formulas.js +12 -0
- package/src/nodes/digest-moments/index.js +14 -0
- package/src/nodes/digest-moments/init.js +67 -0
- package/src/nodes/digest-moments/introspect.js +154 -0
- package/src/nodes/digest-moments/publish-to.js +34 -0
- package/src/nodes/digest-moments/recompute.js +14 -0
- package/src/nodes/digest-moments/reset.js +14 -0
- package/src/nodes/digest-moments/update.js +120 -0
- package/src/nodes/dwell-time-tracker/index.js +14 -0
- package/src/nodes/dwell-time-tracker/init.js +59 -0
- package/src/nodes/dwell-time-tracker/introspect.js +137 -0
- package/src/nodes/dwell-time-tracker/publish-to.js +22 -0
- package/src/nodes/dwell-time-tracker/recompute.js +8 -0
- package/src/nodes/dwell-time-tracker/reset.js +23 -0
- package/src/nodes/dwell-time-tracker/update.js +103 -0
- package/src/nodes/emit-if/helpers.js +94 -0
- package/src/nodes/emit-if/index.js +16 -0
- package/src/nodes/emit-if/init.js +67 -0
- package/src/nodes/emit-if/introspect.js +102 -0
- package/src/nodes/emit-if/publish-to.js +13 -0
- package/src/nodes/emit-if/recompute.js +12 -0
- package/src/nodes/emit-if/reset.js +27 -0
- package/src/nodes/emit-if/update.js +93 -0
- package/src/nodes/es-correlation/index.js +19 -0
- package/src/nodes/es-correlation/init.js +73 -0
- package/src/nodes/es-correlation/introspect.js +176 -0
- package/src/nodes/es-correlation/publish-to.js +32 -0
- package/src/nodes/es-correlation/recompute.js +56 -0
- package/src/nodes/es-correlation/reset.js +27 -0
- package/src/nodes/es-correlation/update.js +110 -0
- package/src/nodes/es-mean/index.js +20 -0
- package/src/nodes/es-mean/init.js +58 -0
- package/src/nodes/es-mean/introspect.js +129 -0
- package/src/nodes/es-mean/publish-to.js +19 -0
- package/src/nodes/es-mean/recompute.js +16 -0
- package/src/nodes/es-mean/reset.js +24 -0
- package/src/nodes/es-mean/update.js +69 -0
- package/src/nodes/es-pairwise-correlation/index.js +19 -0
- package/src/nodes/es-pairwise-correlation/init.js +98 -0
- package/src/nodes/es-pairwise-correlation/introspect.js +187 -0
- package/src/nodes/es-pairwise-correlation/publish-to.js +31 -0
- package/src/nodes/es-pairwise-correlation/recompute.js +69 -0
- package/src/nodes/es-pairwise-correlation/reset.js +29 -0
- package/src/nodes/es-pairwise-correlation/update.js +89 -0
- package/src/nodes/es-stats/index.js +21 -0
- package/src/nodes/es-stats/init.js +105 -0
- package/src/nodes/es-stats/introspect.js +157 -0
- package/src/nodes/es-stats/publish-to.js +42 -0
- package/src/nodes/es-stats/recompute.js +74 -0
- package/src/nodes/es-stats/reset.js +49 -0
- package/src/nodes/es-stats/update-envelope.js +44 -0
- package/src/nodes/es-stats/update-welford.js +64 -0
- package/src/nodes/es-stats/update.js +59 -0
- package/src/nodes/index.js +83 -0
- package/src/nodes/invert-flag/index.js +20 -0
- package/src/nodes/invert-flag/init.js +36 -0
- package/src/nodes/invert-flag/introspect.js +115 -0
- package/src/nodes/invert-flag/publish-to.js +23 -0
- package/src/nodes/invert-flag/recompute.js +12 -0
- package/src/nodes/invert-flag/reset.js +12 -0
- package/src/nodes/invert-flag/update.js +32 -0
- package/src/nodes/kalman1d/index.js +20 -0
- package/src/nodes/kalman1d/init.js +103 -0
- package/src/nodes/kalman1d/introspect.js +250 -0
- package/src/nodes/kalman1d/publish-to.js +34 -0
- package/src/nodes/kalman1d/recompute.js +23 -0
- package/src/nodes/kalman1d/reset.js +29 -0
- package/src/nodes/kalman1d/update.js +144 -0
- package/src/nodes/kernel/index.js +18 -0
- package/src/nodes/kernel/init.js +66 -0
- package/src/nodes/kernel/introspect.js +163 -0
- package/src/nodes/kernel/presets.js +74 -0
- package/src/nodes/kernel/publish-to.js +25 -0
- package/src/nodes/kernel/recompute.js +14 -0
- package/src/nodes/kernel/reset.js +20 -0
- package/src/nodes/kernel/update.js +69 -0
- package/src/nodes/lag/index.js +21 -0
- package/src/nodes/lag/init.js +87 -0
- package/src/nodes/lag/introspect.js +202 -0
- package/src/nodes/lag/publish-to.js +64 -0
- package/src/nodes/lag/recompute.js +33 -0
- package/src/nodes/lag/reset.js +49 -0
- package/src/nodes/lag/update.js +133 -0
- package/src/nodes/median3/index.js +19 -0
- package/src/nodes/median3/init.js +40 -0
- package/src/nodes/median3/introspect.js +116 -0
- package/src/nodes/median3/publish-to.js +21 -0
- package/src/nodes/median3/recompute.js +12 -0
- package/src/nodes/median3/reset.js +15 -0
- package/src/nodes/median3/update.js +58 -0
- package/src/nodes/moments-digest/copy-from.js +12 -0
- package/src/nodes/moments-digest/flush.js +17 -0
- package/src/nodes/moments-digest/index.js +132 -0
- package/src/nodes/moments-digest/init.js +84 -0
- package/src/nodes/moments-digest/introspect.js +122 -0
- package/src/nodes/moments-digest/prelude.js +28 -0
- package/src/nodes/moments-digest/publish-to.js +31 -0
- package/src/nodes/moments-digest/recompute.js +9 -0
- package/src/nodes/moments-digest/reset.js +20 -0
- package/src/nodes/moments-digest/update.js +182 -0
- package/src/nodes/node-names.js +67 -0
- package/src/nodes/page-hinkley/index.js +20 -0
- package/src/nodes/page-hinkley/init.js +66 -0
- package/src/nodes/page-hinkley/introspect.js +193 -0
- package/src/nodes/page-hinkley/publish-to.js +28 -0
- package/src/nodes/page-hinkley/recompute.js +14 -0
- package/src/nodes/page-hinkley/reset.js +23 -0
- package/src/nodes/page-hinkley/update.js +105 -0
- package/src/nodes/pass-if/index.js +21 -0
- package/src/nodes/pass-if/init.js +37 -0
- package/src/nodes/pass-if/introspect.js +93 -0
- package/src/nodes/pass-if/publish-to.js +15 -0
- package/src/nodes/pass-if/recompute.js +15 -0
- package/src/nodes/pass-if/reset.js +18 -0
- package/src/nodes/pass-if/update.js +37 -0
- package/src/nodes/persist-if/helpers.js +111 -0
- package/src/nodes/persist-if/index.js +9 -0
- package/src/nodes/persist-if/init.js +98 -0
- package/src/nodes/persist-if/introspect.js +106 -0
- package/src/nodes/persist-if/publish-to.js +21 -0
- package/src/nodes/persist-if/recompute.js +20 -0
- package/src/nodes/persist-if/reset.js +35 -0
- package/src/nodes/persist-if/update.js +112 -0
- package/src/nodes/persistence-check/index.js +14 -0
- package/src/nodes/persistence-check/init.js +38 -0
- package/src/nodes/persistence-check/introspect.js +144 -0
- package/src/nodes/persistence-check/publish-to.js +11 -0
- package/src/nodes/persistence-check/recompute.js +7 -0
- package/src/nodes/persistence-check/reset.js +12 -0
- package/src/nodes/persistence-check/update.js +57 -0
- package/src/nodes/process-index/index.js +15 -0
- package/src/nodes/process-index/init.js +48 -0
- package/src/nodes/process-index/introspect.js +182 -0
- package/src/nodes/process-index/publish-to.js +29 -0
- package/src/nodes/process-index/recompute.js +8 -0
- package/src/nodes/process-index/reset.js +8 -0
- package/src/nodes/process-index/update.js +75 -0
- package/src/nodes/ratio/index.js +14 -0
- package/src/nodes/ratio/init.js +37 -0
- package/src/nodes/ratio/introspect.js +155 -0
- package/src/nodes/ratio/publish-to.js +15 -0
- package/src/nodes/ratio/recompute.js +7 -0
- package/src/nodes/ratio/reset.js +7 -0
- package/src/nodes/ratio/update.js +47 -0
- package/src/nodes/sanitize/index.js +82 -0
- package/src/nodes/sanitize/init.js +82 -0
- package/src/nodes/sanitize/introspect.js +194 -0
- package/src/nodes/sanitize/publish-to.js +15 -0
- package/src/nodes/sanitize/recompute.js +8 -0
- package/src/nodes/sanitize/reset.js +14 -0
- package/src/nodes/sanitize/update.js +102 -0
- package/src/nodes/spike-guard/index.js +14 -0
- package/src/nodes/spike-guard/init.js +47 -0
- package/src/nodes/spike-guard/introspect.js +130 -0
- package/src/nodes/spike-guard/publish-to.js +35 -0
- package/src/nodes/spike-guard/recompute.js +14 -0
- package/src/nodes/spike-guard/reset.js +19 -0
- package/src/nodes/spike-guard/update.js +89 -0
- package/src/nodes/sse-emitter/index.js +7 -0
- package/src/nodes/sse-emitter/init.js +105 -0
- package/src/nodes/sse-emitter/introspect.js +134 -0
- package/src/nodes/sse-emitter/publish-to.js +6 -0
- package/src/nodes/sse-emitter/recompute.js +16 -0
- package/src/nodes/sse-emitter/reset.js +19 -0
- package/src/nodes/sse-emitter/update.js +69 -0
- package/src/nodes/sse-emitter/utils/broadcast-heartbeat.js +36 -0
- package/src/nodes/sse-emitter/utils/build-message.js +31 -0
- package/src/nodes/sse-emitter/utils/connection-manager.js +45 -0
- package/src/nodes/sse-emitter/utils/endpoint-initializer.js +20 -0
- package/src/nodes/sse-emitter/utils/health-monitor.js +38 -0
- package/src/nodes/sse-emitter/utils/qos-handler.js +41 -0
- package/src/nodes/state-change-detector/helpers.js +35 -0
- package/src/nodes/state-change-detector/index.js +12 -0
- package/src/nodes/state-change-detector/init.js +56 -0
- package/src/nodes/state-change-detector/introspect.js +146 -0
- package/src/nodes/state-change-detector/publish-to.js +20 -0
- package/src/nodes/state-change-detector/recompute.js +6 -0
- package/src/nodes/state-change-detector/reset.js +20 -0
- package/src/nodes/state-change-detector/update.js +97 -0
- package/src/nodes/sw-stats/index.js +14 -0
- package/src/nodes/sw-stats/init.js +51 -0
- package/src/nodes/sw-stats/introspect.js +131 -0
- package/src/nodes/sw-stats/publish-to.js +45 -0
- package/src/nodes/sw-stats/recompute.js +33 -0
- package/src/nodes/sw-stats/reset.js +15 -0
- package/src/nodes/sw-stats/update.js +39 -0
- package/src/nodes/swing-watch/index.js +17 -0
- package/src/nodes/swing-watch/init.js +144 -0
- package/src/nodes/swing-watch/introspect.js +188 -0
- package/src/nodes/swing-watch/publish-to.js +53 -0
- package/src/nodes/swing-watch/recompute.js +22 -0
- package/src/nodes/swing-watch/reset.js +55 -0
- package/src/nodes/swing-watch/update.js +767 -0
- package/src/nodes/tally/index.js +15 -0
- package/src/nodes/tally/init.js +32 -0
- package/src/nodes/tally/introspect.js +116 -0
- package/src/nodes/tally/publish-to.js +28 -0
- package/src/nodes/tally/recompute.js +10 -0
- package/src/nodes/tally/reset.js +10 -0
- package/src/nodes/tally/update.js +44 -0
- package/src/nodes/threshold/index.js +14 -0
- package/src/nodes/threshold/init.js +121 -0
- package/src/nodes/threshold/introspect.js +192 -0
- package/src/nodes/threshold/publish-to.js +11 -0
- package/src/nodes/threshold/recompute.js +8 -0
- package/src/nodes/threshold/reset.js +14 -0
- package/src/nodes/threshold/update.js +54 -0
- package/src/nodes/transform/helpers.js +39 -0
- package/src/nodes/transform/index.js +15 -0
- package/src/nodes/transform/init.js +35 -0
- package/src/nodes/transform/introspect.js +118 -0
- package/src/nodes/transform/publish-to.js +23 -0
- package/src/nodes/transform/recompute.js +12 -0
- package/src/nodes/transform/reset.js +11 -0
- package/src/nodes/transform/update.js +32 -0
- package/src/nodes/trend/compute-confidence.js +112 -0
- package/src/nodes/trend/index.js +14 -0
- package/src/nodes/trend/init.js +81 -0
- package/src/nodes/trend/introspect.js +151 -0
- package/src/nodes/trend/publish-to.js +22 -0
- package/src/nodes/trend/recompute.js +15 -0
- package/src/nodes/trend/reset.js +26 -0
- package/src/nodes/trend/update.js +93 -0
- package/src/nodes/tw-stats/copy-from.js +16 -0
- package/src/nodes/tw-stats/flush.js +12 -0
- package/src/nodes/tw-stats/index.js +41 -0
- package/src/nodes/tw-stats/init.js +92 -0
- package/src/nodes/tw-stats/introspect.js +171 -0
- package/src/nodes/tw-stats/publish-to.js +108 -0
- package/src/nodes/tw-stats/recompute.js +9 -0
- package/src/nodes/tw-stats/reset.js +16 -0
- package/src/nodes/tw-stats/update.js +113 -0
- package/src/nodes/unbalance/index.js +15 -0
- package/src/nodes/unbalance/init.js +59 -0
- package/src/nodes/unbalance/introspect.js +167 -0
- package/src/nodes/unbalance/publish-to.js +52 -0
- package/src/nodes/unbalance/recompute.js +11 -0
- package/src/nodes/unbalance/reset.js +10 -0
- package/src/nodes/unbalance/update.js +83 -0
- package/src/nodes/vector-distance/index.js +14 -0
- package/src/nodes/vector-distance/init.js +54 -0
- package/src/nodes/vector-distance/introspect.js +149 -0
- package/src/nodes/vector-distance/publish-to.js +35 -0
- package/src/nodes/vector-distance/recompute.js +11 -0
- package/src/nodes/vector-distance/reset.js +20 -0
- package/src/nodes/vector-distance/update.js +139 -0
- package/src/nodes/winnow/index.js +12 -0
- package/src/nodes/winnow/init.js +82 -0
- package/src/nodes/winnow/introspect.js +253 -0
- package/src/nodes/winnow/publish-to.js +51 -0
- package/src/nodes/winnow/recompute.js +14 -0
- package/src/nodes/winnow/reset.js +34 -0
- package/src/nodes/winnow/update.js +203 -0
- package/src/tools/training/features.js +227 -0
- package/src/tools/training/index.js +21 -0
- package/src/tools/training/logistic.js +238 -0
- package/src/tools/training/metrics.js +126 -0
- package/src/tools/training/scale.js +239 -0
- package/src/tools/training/validate.js +103 -0
- package/src/windowing/count-sliding/create.js +16 -0
- package/src/windowing/count-sliding/get-reverse-chronological-index.js +19 -0
- package/src/windowing/count-sliding/index.js +6 -0
- package/src/windowing/count-sliding/is-not-full.js +5 -0
- package/src/windowing/count-sliding/push.js +15 -0
- package/src/windowing/count-sliding/reset.js +11 -0
- package/src/windowing/count-sliding/size.js +5 -0
- package/src/windowing/time-sliding/create.js +25 -0
- package/src/windowing/time-sliding/push.js +42 -0
- package/CODE_OF_CONDUCT.md +0 -46
- package/CONTRIBUTING.md +0 -132
- package/docs/.nojekyll +0 -0
- package/docs/CONTRIBUTING.md +0 -134
- package/docs/README.md +0 -132
- package/docs/_coverpage.md +0 -26
- package/docs/_media/bg.png +0 -0
- package/docs/_media/wink-logo.png +0 -0
- package/docs/_navbar.md +0 -14
- package/docs/_sidebar.md +0 -2
- package/docs/index.html +0 -44
- package/eslint.config.js +0 -283
package/src/flow/flow.js
ADDED
|
@@ -0,0 +1,658 @@
|
|
|
1
|
+
// flow/flow.js
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview Composable DSL for streaming analytics pipelines.
|
|
5
|
+
*
|
|
6
|
+
* Supports two modes:
|
|
7
|
+
* - Transpilation: `.build()` returns executable JS source code
|
|
8
|
+
* - Direct execution: `.run()` wires and starts the pipeline
|
|
9
|
+
*
|
|
10
|
+
* Features:
|
|
11
|
+
* - Fluent chainable API for node composition
|
|
12
|
+
* - Array sugar for single-input nodes (auto-generates storeAs as ${field}_${label})
|
|
13
|
+
* - Lazy module loading at run time
|
|
14
|
+
* - Switch/case/break syntax for multiple specialized pipelines
|
|
15
|
+
*
|
|
16
|
+
* State Machine:
|
|
17
|
+
* - FLOW (config): Config methods + nodes + .switch() + terminals allowed
|
|
18
|
+
* - FLOW (pipeline): Nodes + .switch() + terminals allowed (config blocked)
|
|
19
|
+
* - SWITCH: Only .case() allowed
|
|
20
|
+
* - CASE: Node methods + .break() allowed
|
|
21
|
+
* - BREAK: .case() + terminals allowed
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import * as nodes from '../nodes/index.js';
|
|
25
|
+
import { makeCollisionChecker } from '../core/utils/flow/index.js';
|
|
26
|
+
import { serializeModule } from './serialize.js';
|
|
27
|
+
import { makeNodeMethod } from './make-node-method.js';
|
|
28
|
+
import { makeForEach } from './for-each.js';
|
|
29
|
+
import { runFlow } from './run.js';
|
|
30
|
+
import { validateFlow } from './validate.js';
|
|
31
|
+
import { inspectFlow } from './inspect.js';
|
|
32
|
+
import { loadNodeModules } from './load-node-module.js';
|
|
33
|
+
import { collectFlowOutputCollisions } from './check-output-collisions.js';
|
|
34
|
+
import {
|
|
35
|
+
assetIdSchema,
|
|
36
|
+
yieldSchema,
|
|
37
|
+
switchSchema,
|
|
38
|
+
groupBySchema,
|
|
39
|
+
validateFlowConfig
|
|
40
|
+
} from './flow-api-schemas.js';
|
|
41
|
+
import { validateWithSchema } from '../core/utils/validate/index.js';
|
|
42
|
+
// Asset-class validation goes through the deep semantics schema directly.
|
|
43
|
+
// A shallow flow-side wrapper used to live in flow-api-schemas.js; it was
|
|
44
|
+
// removed so both entry paths (loadSemantics() and hand-construction via
|
|
45
|
+
// .assetClass()) validate against the same deep schema — a single source
|
|
46
|
+
// of truth for what makes an asset class valid.
|
|
47
|
+
import { assetClassSchema } from '../core/semantics/schemas/index.js';
|
|
48
|
+
import { expandTemplateSpec } from './expand-template-spec.js';
|
|
49
|
+
|
|
50
|
+
const flow = function ( flowName = 'flow1' ) {
|
|
51
|
+
const api = Object.create( null );
|
|
52
|
+
|
|
53
|
+
const flowDefinition = [];
|
|
54
|
+
const importSet = new Set();
|
|
55
|
+
const isNodeNameDuplicate = makeCollisionChecker();
|
|
56
|
+
|
|
57
|
+
// Runtime configuration (for .run() mode)
|
|
58
|
+
const runtime = {
|
|
59
|
+
source: null,
|
|
60
|
+
emitters: Object.create( null ),
|
|
61
|
+
storages: Object.create( null ),
|
|
62
|
+
partitionField: null,
|
|
63
|
+
specializationField: null,
|
|
64
|
+
yieldThreshold: null,
|
|
65
|
+
assetClass: null
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// Switch/case/break state machine
|
|
69
|
+
const switchState = {
|
|
70
|
+
active: false, // true once .switch() is called
|
|
71
|
+
currentCase: null, // key of the case currently being defined
|
|
72
|
+
caseSpecs: Object.create( null ), // { [caseKey]: specArray }
|
|
73
|
+
caseOrder: [], // preserves declaration order of cases
|
|
74
|
+
caseEnded: false // true after .break(), false after .case()
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// GroupBy state machine (syntactic sugar for switch/case)
|
|
78
|
+
const groupByState = {
|
|
79
|
+
active: false, // true after .groupBy(), false after .endGroup()
|
|
80
|
+
field: null, // grouping field (e.g., 'rpmBand')
|
|
81
|
+
groupValues: [], // group values (e.g., ['idle', 'low', 'cruise'])
|
|
82
|
+
templateSpecs: [], // template specs collected before expansion
|
|
83
|
+
templateNodeChecker: null, // local collision checker for template
|
|
84
|
+
expanded: false // true after .endGroup() expansion
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// Track if pipeline phase has started (for config-first enforcement)
|
|
88
|
+
let pipelineStarted = false;
|
|
89
|
+
|
|
90
|
+
// Helper: Enforce config methods come before any nodes or .switch()/.groupBy()
|
|
91
|
+
const enforceConfigFirst = function ( methodName ) {
|
|
92
|
+
if ( pipelineStarted ) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
`WinkComposer/flow: .${methodName}() must be called before any nodes or .switch()/.groupBy()`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// Helper: Validate switch is complete before terminal methods
|
|
100
|
+
const validateSwitchComplete = function () {
|
|
101
|
+
if ( switchState.active ) {
|
|
102
|
+
if ( switchState.caseOrder.length === 0 ) {
|
|
103
|
+
throw new Error( 'WinkComposer/flow: .switch() requires at least one .case()' );
|
|
104
|
+
}
|
|
105
|
+
if ( !switchState.caseEnded ) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`WinkComposer/flow: case '${switchState.currentCase}' must end with .break()`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// Output-field overwrite guard: two nodes on one runtime path must not write the
|
|
114
|
+
// same message field, or the second silently overwrites the first. Collisions are
|
|
115
|
+
// collected per path (the linear flow, or one switch case), so the same field across
|
|
116
|
+
// sibling cases stays allowed - only one case runs per message.
|
|
117
|
+
const assertNoOutputCollisions = function () {
|
|
118
|
+
const collisions = collectFlowOutputCollisions( switchState, flowDefinition );
|
|
119
|
+
if ( collisions.length > 0 ) {
|
|
120
|
+
throw new Error(
|
|
121
|
+
'WinkComposer/flow: output field collision(s) - two nodes on one path ' +
|
|
122
|
+
'write the same message field:\n - ' + collisions.join( '\n - ' )
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// Discover node factories exposing DSL metadata
|
|
128
|
+
const nodeNames = Object.keys( nodes )
|
|
129
|
+
.filter( ( n ) => ( typeof nodes[ n ]?.getDSLMetadata === 'function' ) )
|
|
130
|
+
.sort();
|
|
131
|
+
|
|
132
|
+
// Bind handlers via factory to avoid loop-closure issues
|
|
133
|
+
// Note: switchState, groupByState, and markPipelineStarted are passed for multi-specialization routing
|
|
134
|
+
const markPipelineStarted = function () {
|
|
135
|
+
pipelineStarted = true;
|
|
136
|
+
};
|
|
137
|
+
const flowState = {
|
|
138
|
+
flowDefinition,
|
|
139
|
+
importSet,
|
|
140
|
+
isNodeNameDuplicate,
|
|
141
|
+
switchState,
|
|
142
|
+
groupByState,
|
|
143
|
+
markPipelineStarted
|
|
144
|
+
};
|
|
145
|
+
for ( let i = 0; i < nodeNames.length; i += 1 ) {
|
|
146
|
+
const node = nodeNames[ i ];
|
|
147
|
+
const meta = nodes[ node ].getDSLMetadata();
|
|
148
|
+
api[ node ] = makeNodeMethod( api, node, meta, flowState );
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// The chain fan: fans a multi-node chain across a field list (the multi-node
|
|
152
|
+
// sibling of array sugar). Wired after the per-node methods so it shares the
|
|
153
|
+
// same flowState (definitions, checkers, router).
|
|
154
|
+
api.forEach = makeForEach( api, flowState );
|
|
155
|
+
|
|
156
|
+
// ========================================================================
|
|
157
|
+
// RUNTIME CONFIGURATION METHODS (chainable)
|
|
158
|
+
// ========================================================================
|
|
159
|
+
|
|
160
|
+
api.source = function ( adapter, sourceConfig = {} ) {
|
|
161
|
+
enforceConfigFirst( 'source' );
|
|
162
|
+
if ( typeof adapter !== 'object' || adapter === null ) {
|
|
163
|
+
throw new Error( 'WinkComposer/flow: source adapter must be an imported module' );
|
|
164
|
+
}
|
|
165
|
+
if ( typeof adapter.start !== 'function' ) {
|
|
166
|
+
throw new Error( 'WinkComposer/flow: source adapter must have a start() function' );
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// If the adapter provides a configSchema, we validate config here.
|
|
170
|
+
// This is fail-fast: bad config is rejected when the flow is defined,
|
|
171
|
+
// not later inside adapter.start().
|
|
172
|
+
if ( adapter.configSchema ) {
|
|
173
|
+
const validation = validateWithSchema( adapter.configSchema, sourceConfig, 'source' );
|
|
174
|
+
validation.throwIfInvalid( `flow/source:${adapter.id || 'unknown'}` );
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
runtime.source = { adapter, config: sourceConfig };
|
|
178
|
+
return api;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
api.emitter = function ( adapter, emitterConfig = {} ) {
|
|
182
|
+
enforceConfigFirst( 'emitter' );
|
|
183
|
+
if ( typeof adapter !== 'object' || adapter === null ) {
|
|
184
|
+
throw new Error( 'WinkComposer/flow: emitter adapter must be an imported module' );
|
|
185
|
+
}
|
|
186
|
+
if ( typeof adapter.id !== 'string' || !adapter.id ) {
|
|
187
|
+
throw new Error( 'WinkComposer/flow: emitter adapter must have an id' );
|
|
188
|
+
}
|
|
189
|
+
if ( typeof adapter.createEmitter !== 'function' ) {
|
|
190
|
+
throw new Error( 'WinkComposer/flow: emitter adapter must have a createEmitter() function' );
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// If the adapter provides a configSchema, we validate config here.
|
|
194
|
+
// This is fail-fast: bad config is rejected when the flow is defined,
|
|
195
|
+
// not later inside adapter.createEmitter().
|
|
196
|
+
if ( adapter.configSchema ) {
|
|
197
|
+
const validation = validateWithSchema( adapter.configSchema, emitterConfig, 'emitter' );
|
|
198
|
+
validation.throwIfInvalid( `flow/emitter:${adapter.id}` );
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
runtime.emitters[ adapter.id ] = { adapter, config: emitterConfig };
|
|
202
|
+
return api;
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
api.storage = function ( adapter, storageConfig = {} ) {
|
|
206
|
+
enforceConfigFirst( 'storage' );
|
|
207
|
+
if ( typeof adapter !== 'object' || adapter === null ) {
|
|
208
|
+
throw new Error( 'WinkComposer/flow: storage adapter must be an imported module' );
|
|
209
|
+
}
|
|
210
|
+
if ( typeof adapter.id !== 'string' || !adapter.id ) {
|
|
211
|
+
throw new Error( 'WinkComposer/flow: storage adapter must have an id' );
|
|
212
|
+
}
|
|
213
|
+
if ( typeof adapter.createStorage !== 'function' ) {
|
|
214
|
+
throw new Error( 'WinkComposer/flow: storage adapter must have a createStorage() function' );
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// If the adapter provides a configSchema, we validate config here.
|
|
218
|
+
// This is fail-fast: bad config is rejected when the flow is defined,
|
|
219
|
+
// not later inside adapter.createStorage().
|
|
220
|
+
if ( adapter.configSchema ) {
|
|
221
|
+
const validation = validateWithSchema( adapter.configSchema, storageConfig, 'storage' );
|
|
222
|
+
validation.throwIfInvalid( `flow/storage:${adapter.id}` );
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
runtime.storages[ adapter.id ] = { adapter, config: storageConfig };
|
|
226
|
+
return api;
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
api.assetId = function ( field ) {
|
|
230
|
+
enforceConfigFirst( 'assetId' );
|
|
231
|
+
|
|
232
|
+
if ( typeof field !== 'string' || field.length === 0 ) {
|
|
233
|
+
throw new Error( 'WinkComposer/flow: .assetId() requires a non-empty string field name' );
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Schema-based validation for robust error handling
|
|
237
|
+
const validation = validateFlowConfig( { field }, assetIdSchema, 'assetId' );
|
|
238
|
+
validation.throwIfInvalid();
|
|
239
|
+
|
|
240
|
+
runtime.partitionField = field;
|
|
241
|
+
return api;
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
api.assetClass = function ( assetClassDef ) {
|
|
245
|
+
enforceConfigFirst( 'assetClass' );
|
|
246
|
+
|
|
247
|
+
// Pre-check: must be an object before we hand it to the schema.
|
|
248
|
+
// The deep semantics schema's first-tier validators expect to walk
|
|
249
|
+
// properties (`name`, `columns`, `insightTypes`); a non-object
|
|
250
|
+
// would crash inside the schema with a less helpful message.
|
|
251
|
+
if ( assetClassDef === null || assetClassDef === undefined || typeof assetClassDef !== 'object' ) {
|
|
252
|
+
const actualType = assetClassDef === null ? 'null' : typeof assetClassDef;
|
|
253
|
+
throw new Error(
|
|
254
|
+
`WinkComposer/flow.assetClass: Expected object, got ${actualType}`
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Validate against the deep semantics schema directly.
|
|
259
|
+
// This is the same schema the semantics loader runs against JSON
|
|
260
|
+
// files, so both entry paths — loadSemantics() and hand-construction
|
|
261
|
+
// via .assetClass() — agree on what counts as a valid asset class.
|
|
262
|
+
// Closes the Path 1B silent-fail surface where a hand-constructed
|
|
263
|
+
// asset class missing column types or with malformed columns would
|
|
264
|
+
// previously slip through the shallow flow-side validator and
|
|
265
|
+
// surface much later as silent VARCHAR coercion in QuestDB.
|
|
266
|
+
const validation = validateWithSchema( assetClassSchema, assetClassDef, 'assetClass' );
|
|
267
|
+
if ( !validation.valid ) {
|
|
268
|
+
throw new Error(
|
|
269
|
+
`WinkComposer/flow.assetClass: validation failed:\n - ${validation.errors.join( '\n - ' )}`
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
runtime.assetClass = assetClassDef;
|
|
274
|
+
return api;
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
api.yield = function ( options ) {
|
|
278
|
+
enforceConfigFirst( 'yield' );
|
|
279
|
+
|
|
280
|
+
// Schema-based validation for robust error handling
|
|
281
|
+
const validation = validateFlowConfig( options, yieldSchema, 'yield' );
|
|
282
|
+
validation.throwIfInvalid();
|
|
283
|
+
|
|
284
|
+
runtime.yieldThreshold = options.threshold;
|
|
285
|
+
return api;
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
// ========================================================================
|
|
289
|
+
// SWITCH/CASE/BREAK METHODS (State Machine for Multi-Specialization)
|
|
290
|
+
// ========================================================================
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Enters SWITCH state. Defines the field for specialization routing.
|
|
294
|
+
* After .switch(), only .case() is allowed.
|
|
295
|
+
*
|
|
296
|
+
* @param {string} field - Message field name for specialization routing
|
|
297
|
+
* @returns {Object} API object for chaining (only .case() available)
|
|
298
|
+
*/
|
|
299
|
+
api.switch = function ( field ) {
|
|
300
|
+
if ( typeof field !== 'string' || field.length === 0 ) {
|
|
301
|
+
throw new Error( 'WinkComposer/flow: .switch() requires a non-empty string field name' );
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Schema-based validation: field must be a valid identifier (mirrors .assetId())
|
|
305
|
+
const validation = validateFlowConfig( { field }, switchSchema, 'switch' );
|
|
306
|
+
validation.throwIfInvalid();
|
|
307
|
+
|
|
308
|
+
// Check groupBy before switchState (endGroup sets switchState.active)
|
|
309
|
+
if ( groupByState.active || groupByState.expanded ) {
|
|
310
|
+
throw new Error( 'WinkComposer/flow: .switch() cannot be used with .groupBy()' );
|
|
311
|
+
}
|
|
312
|
+
if ( switchState.active ) {
|
|
313
|
+
throw new Error( 'WinkComposer/flow: .switch() can only be called once per flow' );
|
|
314
|
+
}
|
|
315
|
+
if ( flowDefinition.length > 0 ) {
|
|
316
|
+
throw new Error(
|
|
317
|
+
'WinkComposer/flow: .switch() cannot be called after nodes - ' +
|
|
318
|
+
'use .switch() before defining any nodes, or omit .switch() for single-pipeline flows'
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Validate field conflict with partition
|
|
323
|
+
if ( runtime.partitionField === field ) {
|
|
324
|
+
throw new Error( 'WinkComposer/flow: partition field and switch field must be different' );
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
runtime.specializationField = field;
|
|
328
|
+
switchState.active = true;
|
|
329
|
+
pipelineStarted = true;
|
|
330
|
+
|
|
331
|
+
return api;
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Enters CASE state. Creates a new specialization pipeline.
|
|
336
|
+
* After .case(), node methods and .break() are allowed.
|
|
337
|
+
*
|
|
338
|
+
* @param {string|number} key - Unique identifier for this case
|
|
339
|
+
* @returns {Object} API object for chaining (nodes + .break() available)
|
|
340
|
+
*/
|
|
341
|
+
api.case = function ( key ) {
|
|
342
|
+
if ( !switchState.active ) {
|
|
343
|
+
throw new Error( 'WinkComposer/flow: .case() requires an active .switch()' );
|
|
344
|
+
}
|
|
345
|
+
if ( switchState.currentCase !== null && !switchState.caseEnded ) {
|
|
346
|
+
throw new Error(
|
|
347
|
+
`WinkComposer/flow: previous case '${switchState.currentCase}' must end with .break() before starting a new case`
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
if ( switchState.caseSpecs[ key ] !== undefined ) {
|
|
351
|
+
throw new Error( `WinkComposer/flow: duplicate case key '${key}'` );
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
switchState.currentCase = key;
|
|
355
|
+
switchState.caseEnded = false;
|
|
356
|
+
switchState.caseSpecs[ key ] = [];
|
|
357
|
+
switchState.caseOrder.push( key );
|
|
358
|
+
|
|
359
|
+
return api;
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Exits CASE state, enters BREAK state.
|
|
364
|
+
* After .break(), .case() or terminal methods are allowed.
|
|
365
|
+
*
|
|
366
|
+
* @returns {Object} API object for chaining (.case() + terminals available)
|
|
367
|
+
*/
|
|
368
|
+
api.break = function () {
|
|
369
|
+
if ( !switchState.active ) {
|
|
370
|
+
throw new Error( 'WinkComposer/flow: .break() requires an active .switch()' );
|
|
371
|
+
}
|
|
372
|
+
if ( switchState.currentCase === null || switchState.caseEnded ) {
|
|
373
|
+
throw new Error( 'WinkComposer/flow: .break() must follow a .case() with at least one node' );
|
|
374
|
+
}
|
|
375
|
+
if ( switchState.caseSpecs[ switchState.currentCase ].length === 0 ) {
|
|
376
|
+
throw new Error(
|
|
377
|
+
`WinkComposer/flow: case '${switchState.currentCase}' is empty - add at least one node before .break()`
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
switchState.caseEnded = true;
|
|
382
|
+
|
|
383
|
+
return api;
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
// ========================================================================
|
|
387
|
+
// GROUPBY/ENDGROUP METHODS (Syntactic Sugar for Multi-Specialization)
|
|
388
|
+
// ========================================================================
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Enters GROUPBY state. Collects template nodes for expansion.
|
|
392
|
+
* After .groupBy(), node methods and .endGroup() are allowed.
|
|
393
|
+
*
|
|
394
|
+
* groupBy is pure syntactic sugar that expands to switch/case at build time.
|
|
395
|
+
* It provides a concise way to define identical pipelines with parameter variations.
|
|
396
|
+
*
|
|
397
|
+
* @param {string} field - Message field for grouping (e.g., 'rpmBand')
|
|
398
|
+
* @param {Array<string|number>} values - Group values to expand (e.g., ['idle', 'low', 'cruise'])
|
|
399
|
+
* @returns {Object} API object for chaining (nodes + .endGroup() available)
|
|
400
|
+
*
|
|
401
|
+
* @example
|
|
402
|
+
* .groupBy( 'rpmBand', [ 'idle', 'low', 'cruise' ] )
|
|
403
|
+
* .pageHinkley( 'ph', 'r2', { phShift: 'shift' }, {
|
|
404
|
+
* lambda: lookupByField( 'rpmBand', { idle: 3.4, low: 3.2, cruise: 2.4 } )
|
|
405
|
+
* })
|
|
406
|
+
* .endGroup()
|
|
407
|
+
*/
|
|
408
|
+
api.groupBy = function ( field, values ) {
|
|
409
|
+
// Validation: field must be non-empty string
|
|
410
|
+
if ( typeof field !== 'string' || field.length === 0 ) {
|
|
411
|
+
throw new Error( 'WinkComposer/flow: .groupBy() requires a non-empty string field name' );
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Schema-based validation: field must be a valid identifier (mirrors .assetId())
|
|
415
|
+
const validation = validateFlowConfig( { field }, groupBySchema, 'groupBy' );
|
|
416
|
+
validation.throwIfInvalid();
|
|
417
|
+
|
|
418
|
+
// Validation: values must be array with at least 2 elements
|
|
419
|
+
if ( !Array.isArray( values ) || values.length < 2 ) {
|
|
420
|
+
throw new Error( 'WinkComposer/flow: .groupBy() requires at least 2 group values' );
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Validation: each value must be string or number
|
|
424
|
+
for ( let i = 0; i < values.length; i += 1 ) {
|
|
425
|
+
const v = values[ i ];
|
|
426
|
+
if ( typeof v !== 'string' && typeof v !== 'number' ) {
|
|
427
|
+
throw new Error( `WinkComposer/flow: group value at index ${i} must be a string or number` );
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Validation: no duplicate values
|
|
432
|
+
const seen = Object.create( null );
|
|
433
|
+
for ( let i = 0; i < values.length; i += 1 ) {
|
|
434
|
+
const v = values[ i ];
|
|
435
|
+
if ( seen[ v ] ) {
|
|
436
|
+
throw new Error( `WinkComposer/flow: duplicate group value '${v}' in .groupBy()` );
|
|
437
|
+
}
|
|
438
|
+
seen[ v ] = 1;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Validation: cannot call groupBy twice (check this first - user intent is to reuse groupBy)
|
|
442
|
+
if ( groupByState.active || groupByState.expanded ) {
|
|
443
|
+
throw new Error( 'WinkComposer/flow: .groupBy() can only be called once per flow' );
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Validation: cannot use groupBy if switchState is active (manual switch, not from endGroup)
|
|
447
|
+
if ( switchState.active ) {
|
|
448
|
+
throw new Error( 'WinkComposer/flow: .groupBy() cannot be used inside .switch()' );
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Validation: must come before any nodes
|
|
452
|
+
if ( flowDefinition.length > 0 ) {
|
|
453
|
+
throw new Error(
|
|
454
|
+
'WinkComposer/flow: .groupBy() cannot be called after nodes - ' +
|
|
455
|
+
'use .groupBy() before defining any nodes'
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Validation: groupBy field cannot equal partition field
|
|
460
|
+
if ( runtime.partitionField === field ) {
|
|
461
|
+
throw new Error( 'WinkComposer/flow: partition field and groupBy field must be different' );
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Initialize groupBy state
|
|
465
|
+
groupByState.active = true;
|
|
466
|
+
groupByState.field = field;
|
|
467
|
+
groupByState.groupValues = values.slice(); // defensive copy
|
|
468
|
+
groupByState.templateSpecs = [];
|
|
469
|
+
groupByState.templateNodeChecker = makeCollisionChecker();
|
|
470
|
+
|
|
471
|
+
pipelineStarted = true;
|
|
472
|
+
|
|
473
|
+
return api;
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Exits GROUPBY state. Expands template specs into switch/case structure.
|
|
478
|
+
*
|
|
479
|
+
* For each group value:
|
|
480
|
+
* 1. Deep clone each template spec
|
|
481
|
+
* 2. Prefix node name: 'corr' → 'idle_corr'
|
|
482
|
+
* 3. Resolve matching tunables: lookupByField('rpmBand', {...}) → concrete value
|
|
483
|
+
* 4. Prefix trigger targets: ['corr'] → ['idle_corr']
|
|
484
|
+
*
|
|
485
|
+
* @returns {Object} API object for chaining (terminals available)
|
|
486
|
+
*/
|
|
487
|
+
api.endGroup = function () {
|
|
488
|
+
// Validation: must be in active groupBy state
|
|
489
|
+
if ( !groupByState.active ) {
|
|
490
|
+
throw new Error( 'WinkComposer/flow: .endGroup() requires an active .groupBy()' );
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Validation: must have at least one template node
|
|
494
|
+
if ( groupByState.templateSpecs.length === 0 ) {
|
|
495
|
+
throw new Error( 'WinkComposer/flow: .groupBy() must contain at least one node before .endGroup()' );
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Set specialization field (same as switch does)
|
|
499
|
+
runtime.specializationField = groupByState.field;
|
|
500
|
+
|
|
501
|
+
// Activate switch state (we're producing switch/case structure)
|
|
502
|
+
switchState.active = true;
|
|
503
|
+
|
|
504
|
+
// Expand templates into switch cases
|
|
505
|
+
for ( let i = 0; i < groupByState.groupValues.length; i += 1 ) {
|
|
506
|
+
const groupValue = groupByState.groupValues[ i ];
|
|
507
|
+
|
|
508
|
+
// Initialize case
|
|
509
|
+
switchState.caseSpecs[ groupValue ] = [];
|
|
510
|
+
switchState.caseOrder.push( groupValue );
|
|
511
|
+
|
|
512
|
+
// Expand each template spec for this group
|
|
513
|
+
for ( let j = 0; j < groupByState.templateSpecs.length; j += 1 ) {
|
|
514
|
+
const templateSpec = groupByState.templateSpecs[ j ];
|
|
515
|
+
const expandedSpec = expandTemplateSpec(
|
|
516
|
+
templateSpec,
|
|
517
|
+
groupValue,
|
|
518
|
+
groupByState.field
|
|
519
|
+
);
|
|
520
|
+
switchState.caseSpecs[ groupValue ].push( expandedSpec );
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Mark switch as complete (no more cases allowed)
|
|
525
|
+
switchState.caseEnded = true;
|
|
526
|
+
switchState.currentCase = groupByState.groupValues[ groupByState.groupValues.length - 1 ];
|
|
527
|
+
|
|
528
|
+
// Mark groupBy as complete
|
|
529
|
+
groupByState.active = false;
|
|
530
|
+
groupByState.expanded = true;
|
|
531
|
+
|
|
532
|
+
return api;
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
// ========================================================================
|
|
536
|
+
// TERMINAL METHODS
|
|
537
|
+
// ========================================================================
|
|
538
|
+
|
|
539
|
+
api.build = function () {
|
|
540
|
+
validateSwitchComplete();
|
|
541
|
+
assertNoOutputCollisions();
|
|
542
|
+
|
|
543
|
+
if ( switchState.active ) {
|
|
544
|
+
// Multi-specialization mode
|
|
545
|
+
const imports = Array.from( importSet ).sort();
|
|
546
|
+
return serializeModule( {
|
|
547
|
+
imports,
|
|
548
|
+
flowName,
|
|
549
|
+
specsByCase: switchState.caseSpecs,
|
|
550
|
+
caseOrder: switchState.caseOrder
|
|
551
|
+
} );
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Single-pipeline mode (backward compatible)
|
|
555
|
+
if ( flowDefinition.length === 0 ) {
|
|
556
|
+
throw new Error( 'WinkComposer/flow: Cannot build empty flow - add at least one node' );
|
|
557
|
+
}
|
|
558
|
+
const imports = Array.from( importSet ).sort();
|
|
559
|
+
return serializeModule( { imports, flowName, specs: flowDefinition } );
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
api.validate = async function () {
|
|
563
|
+
validateSwitchComplete();
|
|
564
|
+
|
|
565
|
+
// Output-field overwrites surface as validation errors here; build()/run() throw
|
|
566
|
+
// on the same condition. Collected per path, so cross-case reuse is allowed.
|
|
567
|
+
const outputCollisions = collectFlowOutputCollisions( switchState, flowDefinition );
|
|
568
|
+
|
|
569
|
+
// Load node modules for trigger validation (control method validation)
|
|
570
|
+
const importedNodeNames = Array.from( importSet );
|
|
571
|
+
const nodeModulesMap = await loadNodeModules( importedNodeNames );
|
|
572
|
+
const nodeModules = Object.create( null );
|
|
573
|
+
for ( const [ name, mod ] of nodeModulesMap ) {
|
|
574
|
+
nodeModules[ name ] = mod;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Build registered-adapter maps `{ id: module }` for cross-flow
|
|
578
|
+
// target validation AND capability-driven semanticsRequirement
|
|
579
|
+
// checks. runtime.emitters / runtime.storages each
|
|
580
|
+
// store `{ id: { adapter, config } }`; we extract just the
|
|
581
|
+
// adapter modules here so validate.js doesn't need to know about
|
|
582
|
+
// our `{ adapter, config }` wrapper.
|
|
583
|
+
const registeredEmitters = Object.create( null );
|
|
584
|
+
for ( const [ id, { adapter } ] of Object.entries( runtime.emitters ) ) {
|
|
585
|
+
registeredEmitters[ id ] = adapter;
|
|
586
|
+
}
|
|
587
|
+
const registeredStorages = Object.create( null );
|
|
588
|
+
for ( const [ id, { adapter } ] of Object.entries( runtime.storages ) ) {
|
|
589
|
+
registeredStorages[ id ] = adapter;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if ( switchState.active ) {
|
|
593
|
+
// Validate all specializations in parallel
|
|
594
|
+
const validationPromises = switchState.caseOrder.map( ( key ) =>
|
|
595
|
+
Promise.resolve( validateFlow( `${flowName}:${key}`, switchState.caseSpecs[ key ], nodeModules, registeredEmitters, registeredStorages, runtime.assetClass ) )
|
|
596
|
+
.then( ( result ) => ( { key, result } ) )
|
|
597
|
+
);
|
|
598
|
+
const results = await Promise.all( validationPromises );
|
|
599
|
+
|
|
600
|
+
const allErrors = [];
|
|
601
|
+
for ( let i = 0; i < results.length; i += 1 ) {
|
|
602
|
+
const { key, result } = results[ i ];
|
|
603
|
+
if ( !result.valid ) {
|
|
604
|
+
allErrors.push( ...result.errors.map( ( e ) => `[case '${key}'] ${e}` ) );
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
for ( let i = 0; i < outputCollisions.length; i += 1 ) {
|
|
608
|
+
allErrors.push( outputCollisions[ i ] );
|
|
609
|
+
}
|
|
610
|
+
return {
|
|
611
|
+
valid: allErrors.length === 0,
|
|
612
|
+
errors: allErrors
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Single-pipeline mode
|
|
617
|
+
const result = await validateFlow( flowName, flowDefinition, nodeModules, registeredEmitters, registeredStorages, runtime.assetClass );
|
|
618
|
+
if ( outputCollisions.length === 0 ) {
|
|
619
|
+
return result;
|
|
620
|
+
}
|
|
621
|
+
return {
|
|
622
|
+
valid: false,
|
|
623
|
+
errors: result.errors.concat( outputCollisions )
|
|
624
|
+
};
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
api.inspect = function () {
|
|
628
|
+
return inspectFlow( flowName, flowDefinition, importSet, runtime, switchState );
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
api.run = async function () {
|
|
632
|
+
validateSwitchComplete();
|
|
633
|
+
assertNoOutputCollisions();
|
|
634
|
+
|
|
635
|
+
if ( switchState.active ) {
|
|
636
|
+
// Multi-specialization mode
|
|
637
|
+
const handle = await runFlow(
|
|
638
|
+
flowName,
|
|
639
|
+
switchState.caseSpecs,
|
|
640
|
+
importSet,
|
|
641
|
+
runtime,
|
|
642
|
+
switchState.caseOrder
|
|
643
|
+
);
|
|
644
|
+
return handle;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// Single-pipeline mode (backward compatible)
|
|
648
|
+
if ( flowDefinition.length === 0 ) {
|
|
649
|
+
throw new Error( 'WinkComposer/flow: Cannot run empty flow - add at least one node' );
|
|
650
|
+
}
|
|
651
|
+
const handle = await runFlow( flowName, flowDefinition, importSet, runtime );
|
|
652
|
+
return handle;
|
|
653
|
+
};
|
|
654
|
+
|
|
655
|
+
return api;
|
|
656
|
+
}; // flow()
|
|
657
|
+
|
|
658
|
+
export { flow };
|