@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
|
@@ -0,0 +1,780 @@
|
|
|
1
|
+
// core/emitter-manager/mqtt/emitter.js
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview Production MQTT Emitter — Pure Transport Layer
|
|
5
|
+
*
|
|
6
|
+
* Durability class: **'in-memory'** (ADR-021). Messages buffer in the
|
|
7
|
+
* mqtt.js client's default synchronous memory store; a process crash or
|
|
8
|
+
* power cut loses at most the unacknowledged in-flight window (measured:
|
|
9
|
+
* a few hundred messages at 14k msg/s, at most one burst at edge rates)
|
|
10
|
+
* plus anything buffered during a concurrent broker outage. The
|
|
11
|
+
* disk-backed store was removed because mqtt.js loses QoS-1 messages on
|
|
12
|
+
* EVERY connection acceptance when its outgoing store is asynchronous
|
|
13
|
+
* (the erase-then-rebuild gap — ADR-021 has the full diagnosis
|
|
14
|
+
* and the measured loss-vs-latency curve). The synchronous memory store
|
|
15
|
+
* closes that gap by construction; the composer-owned WAL that restores
|
|
16
|
+
* crash durability is ADR-021's planned successor.
|
|
17
|
+
*
|
|
18
|
+
* DESIGN DECISIONS:
|
|
19
|
+
*
|
|
20
|
+
* 1. FIRE-AND-FORGET WITH QoS 1
|
|
21
|
+
* - Never wait for PUBACK callbacks on the hot path
|
|
22
|
+
* - QoS 1 gives at-least-once delivery while the process lives
|
|
23
|
+
* - Callbacks drive the unacked counter, not delivery decisions
|
|
24
|
+
*
|
|
25
|
+
* 2. COMPOSER-SIDE UNACKED ACCOUNTING (ADR-021)
|
|
26
|
+
* - One counter: +1 when a publish is accepted, -1 when its callback
|
|
27
|
+
* fires (acknowledgment or failure).
|
|
28
|
+
* - The counter IS the pressure gauge, the pre-flight refusal basis,
|
|
29
|
+
* the shutdown drain condition, and the health input. Because
|
|
30
|
+
* "unacknowledged" by definition covers every message sitting in
|
|
31
|
+
* any client-internal queue, no accumulation can be invisible to
|
|
32
|
+
* pressure — bounded memory is arithmetic (cap × message size),
|
|
33
|
+
* not an assumption about library behavior. (The old design read
|
|
34
|
+
* pressure from the LevelDB store while messages piled up unseen
|
|
35
|
+
* inside the client — the 2026-07-08 tier-run OOM.)
|
|
36
|
+
*
|
|
37
|
+
* 3. MESSAGE EXPIRY (MQTT v5)
|
|
38
|
+
* - Configurable per message type
|
|
39
|
+
* - Prevents flooding backend after extended offline periods
|
|
40
|
+
*
|
|
41
|
+
* 4. NO FORCED IDENTITY / OPTIONAL WILL — unchanged transport behavior.
|
|
42
|
+
*
|
|
43
|
+
* 5. STARTUP POSTURE: RECOVERING, WITH A BOUNDED FIRST-CONNACK GRACE
|
|
44
|
+
* - Setup never fails because the broker is unreachable. The factory
|
|
45
|
+
* waits up to `connectGraceMs` (config → MQTT_CONNECT_GRACE_MS env
|
|
46
|
+
* → 500 ms default; 0 disables) for the first connection
|
|
47
|
+
* acknowledgment, then hands back the handle either way. On a
|
|
48
|
+
* reachable broker the real wait is one connack round trip; on an
|
|
49
|
+
* unreachable one the flow starts with `connected: false` and the
|
|
50
|
+
* client keeps retrying in the background.
|
|
51
|
+
* - Return shape: the handle itself at grace 0, otherwise a Promise
|
|
52
|
+
* of it. The wiring layer awaits either (ADR-018).
|
|
53
|
+
* - This replaced the wire-time `sleep(240)` that once papered over
|
|
54
|
+
* the pre-connack loss ADR-021 eliminated.
|
|
55
|
+
*
|
|
56
|
+
* Adapter contract (ADR-018, stream sink):
|
|
57
|
+
* - `publishNow(topic, message, options?)` returns `{ ok: true }` on
|
|
58
|
+
* acceptance or `{ ok: false, error: { code, message } }` on pre-flight
|
|
59
|
+
* rejection. Sync return per ADR-013; never a Promise on the hot path.
|
|
60
|
+
* - Async publish failures route through `onDeliveryFailure(err, ctx)`
|
|
61
|
+
* when supplied (ctx = `{ topic }`); without a handler the adapter
|
|
62
|
+
* surfaces the failure via `Promise.reject(deliveryErr)` — loud failure
|
|
63
|
+
* beats silent loss (parity with QuestDB). `onCritical` is
|
|
64
|
+
* reserved for the high-pressure `QUEUE_CRITICAL` warning (no loss yet).
|
|
65
|
+
* - `getPressure()` returns unacked / maxQueueSize in `[0, 1]` (sync,
|
|
66
|
+
* O(1), allocation-free per ADR-018).
|
|
67
|
+
* - `getHealth()` returns the ADR-018 health floor `{status, connected,
|
|
68
|
+
* pressure}` plus this adapter's own `stats` addition; `stats.unacked`
|
|
69
|
+
* is the live counter.
|
|
70
|
+
*
|
|
71
|
+
* Health-status semantics (uniform across sinks):
|
|
72
|
+
* - `red` if `!state.connected`.
|
|
73
|
+
* - `yellow` if connected AND `pressure >= 0.66` (threshold shared with
|
|
74
|
+
* ADR-020's Draft yield proposal).
|
|
75
|
+
* - `green` otherwise.
|
|
76
|
+
*
|
|
77
|
+
* Backpressure threshold:
|
|
78
|
+
* - Pre-flight reject fires at `pressure >= STORAGE_PRESSURE_LIMIT` (0.9),
|
|
79
|
+
* leaving 10% headroom for in-flight drain on shutdown and a clean band
|
|
80
|
+
* between the yellow-health threshold (0.66) and exhaustion (1.0).
|
|
81
|
+
* - The cap is `maxQueueSize`, clamped to 60,000: every unacknowledged
|
|
82
|
+
* QoS-1 message holds a 16-bit packet id, so one connection can never
|
|
83
|
+
* carry more (see MQTT_INFLIGHT_ID_LIMIT).
|
|
84
|
+
* - The old byte-axis limit (`maxQueueBytes`) died with the disk store;
|
|
85
|
+
* the count cap bounds memory at cap × payload size.
|
|
86
|
+
*
|
|
87
|
+
* `err.code` vocabulary (user-facing; documented in this header as
|
|
88
|
+
* ADR-018 requires):
|
|
89
|
+
* - `STORAGE_FULL` — the unacked cap is reached; pre-flight sync
|
|
90
|
+
* reject. The name is the cross-sink vocabulary word (ADR-018), kept
|
|
91
|
+
* although the "storage" is now the in-memory buffer.
|
|
92
|
+
* - `ENCODE_ERROR` — the codec could not encode the message; sync
|
|
93
|
+
* pre-flight reject, the message was never in flight. The sink-side
|
|
94
|
+
* mirror of the source vocabulary's `DECODE_ERROR`.
|
|
95
|
+
* - `DELIVERY_FAILED` — publish failure. Usually async, routed through
|
|
96
|
+
* `onDeliveryFailure` (default without a handler: `Promise.reject`,
|
|
97
|
+
* an unhandledRejection). Rare sync face: `client.publish` rejecting
|
|
98
|
+
* the call itself returns this code synchronously — the slot is
|
|
99
|
+
* released, nothing is in flight.
|
|
100
|
+
* - `SHUTTING_DOWN` — emitter is mid-shutdown; new publishes are dropped.
|
|
101
|
+
* - `INVALID_CONFIG` — setup-time; missing or malformed config field
|
|
102
|
+
* (brokerUrl, codec.pack, callback type, connectGraceMs). On the
|
|
103
|
+
* thrown TypeError per ADR-018's fail-fast setup rule.
|
|
104
|
+
* - `SHUTDOWN_TIMEOUT` — shutdown closed with unacknowledged messages;
|
|
105
|
+
* carries `dropped: { count }` (the exact counter value). Fires whether
|
|
106
|
+
* connected or not: with no disk store, nothing survives the process,
|
|
107
|
+
* so a disconnected shutdown with pending messages is a real loss and
|
|
108
|
+
* is reported as one (this differs from the wal-backed design, which
|
|
109
|
+
* held them for the next session).
|
|
110
|
+
*
|
|
111
|
+
* @module mqtt-emitter
|
|
112
|
+
*/
|
|
113
|
+
|
|
114
|
+
import mqtt from 'mqtt';
|
|
115
|
+
import crypto from 'crypto';
|
|
116
|
+
import { ENV_VARS } from '../../env-vars.js';
|
|
117
|
+
import {
|
|
118
|
+
QOS,
|
|
119
|
+
MQTT_CONFIG,
|
|
120
|
+
WINK_NAMESPACE,
|
|
121
|
+
QUEUE_CRITICAL_THRESHOLD,
|
|
122
|
+
MESSAGE_EXPIRY,
|
|
123
|
+
DEFAULT_MAX_QUEUE_SIZE,
|
|
124
|
+
MQTT_INFLIGHT_ID_LIMIT
|
|
125
|
+
} from './constants.js';
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Pre-flight reject threshold on store pressure.
|
|
129
|
+
* 0.9 = 10% headroom for in-flight drain on shutdown, plus a clean band
|
|
130
|
+
* between the yellow-health threshold (0.66, shared with the yield level the
|
|
131
|
+
* ADR-020 Draft proposes) and store exhaustion (1.0). At or above this,
|
|
132
|
+
* `publishNow` returns `STORAGE_FULL` synchronously; the message is not buffered.
|
|
133
|
+
* @type {number}
|
|
134
|
+
*/
|
|
135
|
+
const STORAGE_PRESSURE_LIMIT = 0.9;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Pressure threshold above which `getHealth().status` elevates to at least
|
|
139
|
+
* 'yellow' (uniform with Terminal/QuestDB). The value matches the
|
|
140
|
+
* pressure-aware-yield design (ADR-020, still a Draft). That design
|
|
141
|
+
* proposes 0.66 as the pressure level where the flow would start yielding
|
|
142
|
+
* to let sinks drain; a benchmark still has to confirm the number. Today
|
|
143
|
+
* the yield trigger is time-only (ADR-024), so this alignment is
|
|
144
|
+
* forward-looking.
|
|
145
|
+
* @type {number}
|
|
146
|
+
*/
|
|
147
|
+
const HEALTH_PRESSURE_YELLOW_THRESHOLD = 0.66;
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Singleton success result reused on every successful publish. Hot-path zero
|
|
151
|
+
* allocation per ADR-013 / ADR-004. Plain literal — not frozen.
|
|
152
|
+
* @type {{ok: true}}
|
|
153
|
+
*/
|
|
154
|
+
const RESULT_OK = { ok: true };
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Singleton error result for the pre-flight pressure-limit reject. Reused on
|
|
158
|
+
* every occurrence; rare in healthy operation (pressure this close to the
|
|
159
|
+
* limit means delivery has stopped draining), but a defined contract path.
|
|
160
|
+
* @type {{ok: false, error: {code: string, message: string}}}
|
|
161
|
+
*/
|
|
162
|
+
const ERR_STORAGE_FULL = {
|
|
163
|
+
ok: false,
|
|
164
|
+
error: {
|
|
165
|
+
code: 'STORAGE_FULL',
|
|
166
|
+
message: 'Store at or above pressure limit (0.9) — cannot accept message'
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Singleton error result for publishes attempted while shutdown is in progress.
|
|
172
|
+
* @type {{ok: false, error: {code: string, message: string}}}
|
|
173
|
+
*/
|
|
174
|
+
const ERR_SHUTTING_DOWN = {
|
|
175
|
+
ok: false,
|
|
176
|
+
error: {
|
|
177
|
+
code: 'SHUTTING_DOWN',
|
|
178
|
+
message: 'Emitter is shutting down — message dropped'
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Build a config-error TypeError tagged for ADR-018 fail-fast setup routing.
|
|
184
|
+
* @param {string} message
|
|
185
|
+
* @returns {TypeError}
|
|
186
|
+
*/
|
|
187
|
+
const invalidConfig = function ( message ) {
|
|
188
|
+
const err = new TypeError( message );
|
|
189
|
+
err.code = 'INVALID_CONFIG';
|
|
190
|
+
return err;
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Throw classified when an optional callback option is set to a non-function.
|
|
195
|
+
* @param {*} value - the configured option value
|
|
196
|
+
* @param {string} name - the option name for the error message
|
|
197
|
+
*/
|
|
198
|
+
const assertOptionalCallback = function ( value, name ) {
|
|
199
|
+
if ( value !== undefined && typeof value !== 'function' ) {
|
|
200
|
+
throw invalidConfig( `${name} must be a function` );
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Validate and build the mqtt.js will (last-will testament) options.
|
|
206
|
+
*
|
|
207
|
+
* Direct createEmitter callers bypass the DSL schema, so the factory
|
|
208
|
+
* checks the will shape itself, and the payload encoding runs behind
|
|
209
|
+
* the same classified-throw rule as every other setup failure
|
|
210
|
+
* (ADR-018, fail-fast setup) — a raw codec TypeError would reach the
|
|
211
|
+
* caller unclassified.
|
|
212
|
+
*
|
|
213
|
+
* @param {Object} will - config.will as supplied
|
|
214
|
+
* @param {Object} codec - config.codec (pack + contentType)
|
|
215
|
+
* @returns {Object} the mqtt.js connect-options `will` object
|
|
216
|
+
*/
|
|
217
|
+
const buildWillOptions = function ( will, codec ) {
|
|
218
|
+
if ( typeof will.topic !== 'string' || will.topic.length === 0 ) {
|
|
219
|
+
throw invalidConfig( 'will.topic is required — a non-empty string' );
|
|
220
|
+
}
|
|
221
|
+
if ( will.message === undefined || will.message === null ) {
|
|
222
|
+
throw invalidConfig( 'will.message is required' );
|
|
223
|
+
}
|
|
224
|
+
let willPayload;
|
|
225
|
+
try {
|
|
226
|
+
willPayload = Buffer.from( codec.pack( will.message ) );
|
|
227
|
+
} catch ( packErr ) {
|
|
228
|
+
throw invalidConfig( `will.message could not be encoded by the codec: ${packErr.message}` );
|
|
229
|
+
}
|
|
230
|
+
const willOptions = {
|
|
231
|
+
topic: will.topic,
|
|
232
|
+
payload: willPayload,
|
|
233
|
+
// `??`, not `||`: 0 is a valid QoS.
|
|
234
|
+
qos: will.qos ?? QOS,
|
|
235
|
+
retain: will.retain !== false, // Default true
|
|
236
|
+
properties: {
|
|
237
|
+
contentType: codec.contentType
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
if ( codec.payloadFormatIndicator === 1 ) {
|
|
241
|
+
willOptions.properties.payloadFormatIndicator = true;
|
|
242
|
+
}
|
|
243
|
+
return willOptions;
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Bounded wait for the client's first 'connect' event (the broker's
|
|
248
|
+
* connection acknowledgment). Resolves when the event fires or when
|
|
249
|
+
* graceMs elapses, whichever comes first. Expiry is NOT an error —
|
|
250
|
+
* the startup posture stays 'recovering' (ADR-018): the caller gets
|
|
251
|
+
* a working handle that reports `connected: false` while mqtt.js
|
|
252
|
+
* retries in the background. The one-shot listener is removed on
|
|
253
|
+
* expiry so a late connack reaches only the permanent state handler;
|
|
254
|
+
* the timer is cleared on connect so nothing pins the event loop.
|
|
255
|
+
*
|
|
256
|
+
* @param {Object} client - mqtt.js client (EventEmitter surface)
|
|
257
|
+
* @param {number} graceMs - positive wait budget, milliseconds
|
|
258
|
+
* @returns {Promise<void>} resolves on first connect or expiry; never rejects
|
|
259
|
+
*/
|
|
260
|
+
const waitForFirstConnect = function ( client, graceMs ) {
|
|
261
|
+
return new Promise( ( resolve ) => {
|
|
262
|
+
let timer = null;
|
|
263
|
+
const onConnect = function () {
|
|
264
|
+
clearTimeout( timer );
|
|
265
|
+
resolve();
|
|
266
|
+
};
|
|
267
|
+
timer = setTimeout( () => {
|
|
268
|
+
client.removeListener( 'connect', onConnect );
|
|
269
|
+
resolve();
|
|
270
|
+
}, graceMs );
|
|
271
|
+
client.once( 'connect', onConnect );
|
|
272
|
+
} );
|
|
273
|
+
}; // waitForFirstConnect()
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Create production MQTT emitter.
|
|
277
|
+
*
|
|
278
|
+
* Validation is synchronous — bad config throws before any side
|
|
279
|
+
* effect. The return shape depends on the effective grace:
|
|
280
|
+
* `connectGraceMs === 0` returns the handle directly (today's callers
|
|
281
|
+
* that need synchronous creation pass 0); otherwise returns a Promise
|
|
282
|
+
* resolving to the handle after the first connack or the grace budget.
|
|
283
|
+
* The wiring layer awaits either shape (ADR-018).
|
|
284
|
+
*
|
|
285
|
+
* @param {Object} config - emitter config (see configSchema in index.js)
|
|
286
|
+
* @returns {Object|Promise<Object>} the emitter handle, or a Promise of it
|
|
287
|
+
*/
|
|
288
|
+
export const createEmitter = function ( config ) {
|
|
289
|
+
// Per ADR-018, setup-time throws carry classified err.code.
|
|
290
|
+
//
|
|
291
|
+
// brokerUrl: `??` so explicit '' is the user's choice (not "fall back
|
|
292
|
+
// to env"); `.trim()` rejects whitespace-only. Symmetric with QuestDB,
|
|
293
|
+
// which throws on `ilpUrl: ''`.
|
|
294
|
+
const rawBrokerUrl = config.brokerUrl ?? ENV_VARS.mqttBrokerUrl;
|
|
295
|
+
const brokerUrl = typeof rawBrokerUrl === 'string' ? rawBrokerUrl.trim() : '';
|
|
296
|
+
if ( !brokerUrl ) {
|
|
297
|
+
throw invalidConfig( 'brokerUrl required — set a non-empty string in .emitter() config or MQTT_BROKER_URL env var' );
|
|
298
|
+
}
|
|
299
|
+
if ( !config.codec ) {
|
|
300
|
+
throw invalidConfig( 'config.codec is required' );
|
|
301
|
+
}
|
|
302
|
+
if ( typeof config.codec.pack !== 'function' ) {
|
|
303
|
+
throw invalidConfig( 'config.codec must have a pack() function' );
|
|
304
|
+
}
|
|
305
|
+
assertOptionalCallback( config.onDeliveryFailure, 'onDeliveryFailure' );
|
|
306
|
+
assertOptionalCallback( config.onCritical, 'onCritical' );
|
|
307
|
+
assertOptionalCallback( config.onBackpressure, 'onBackpressure' );
|
|
308
|
+
|
|
309
|
+
// First-connack grace: explicit config → MQTT_CONNECT_GRACE_MS env
|
|
310
|
+
// fallback → 500 ms default (ADR-018 precedence; the fallback lives
|
|
311
|
+
// here in the factory body, never as a schema sigil). `??` keeps an
|
|
312
|
+
// explicit 0 — "hand the handle back immediately" — from falling
|
|
313
|
+
// through to the env value. Number.isInteger also rejects Infinity,
|
|
314
|
+
// so the wait is bounded by construction. Validated here, before
|
|
315
|
+
// mqtt.connect opens anything (fail-fast setup per ADR-018).
|
|
316
|
+
const connectGraceMs = config.connectGraceMs ?? ENV_VARS.mqttConnectGraceMs;
|
|
317
|
+
if ( !Number.isInteger( connectGraceMs ) || connectGraceMs < 0 ) {
|
|
318
|
+
throw invalidConfig( 'connectGraceMs must be a non-negative integer (milliseconds); 0 disables the first-connect wait' );
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Core state. `unacked` is THE counter (ADR-021): +1 on accepted
|
|
322
|
+
// publish, -1 when the publish callback fires. Pressure, refusal,
|
|
323
|
+
// drain, and health all read it.
|
|
324
|
+
const state = {
|
|
325
|
+
connected: false,
|
|
326
|
+
hasConnectedOnce: false,
|
|
327
|
+
shuttingDown: false,
|
|
328
|
+
unacked: 0,
|
|
329
|
+
stats: {
|
|
330
|
+
published: 0,
|
|
331
|
+
publishErrors: 0,
|
|
332
|
+
encodeErrors: 0,
|
|
333
|
+
errors: 0,
|
|
334
|
+
reconnects: 0
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
// Broker URLs may carry credentials (mqtt://user:pass@host). Debug
|
|
339
|
+
// logs print this redacted form, so a debug flag flipped on in
|
|
340
|
+
// production cannot leak the password into whatever collects stdout.
|
|
341
|
+
const redactedBrokerUrl = brokerUrl.replace( /\/\/[^@/]*@/, '//***@' );
|
|
342
|
+
|
|
343
|
+
// Generate unique client ID if not provided
|
|
344
|
+
const clientId = config.clientId || `wink-${Date.now()}-${Math.random().toString( 36 ).slice( 2, 9 )}`;
|
|
345
|
+
|
|
346
|
+
// The unacked cap. Clamped to the 16-bit packet-id space: every
|
|
347
|
+
// unacknowledged QoS-1 message holds a packet id, so one connection
|
|
348
|
+
// can never carry more than 65,535; 60,000 leaves working headroom
|
|
349
|
+
// (same clamp the LevelDB store applied, kept with the same warning).
|
|
350
|
+
const requestedQueueSize = config.maxQueueSize || DEFAULT_MAX_QUEUE_SIZE;
|
|
351
|
+
if ( requestedQueueSize > MQTT_INFLIGHT_ID_LIMIT ) {
|
|
352
|
+
console.warn(
|
|
353
|
+
`WinkComposer/mqtt-emitter: maxQueueSize ${requestedQueueSize} exceeds the MQTT ` +
|
|
354
|
+
`packet-id ceiling — clamped to ${MQTT_INFLIGHT_ID_LIMIT}`
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
const maxQueueSize = Math.min( requestedQueueSize, MQTT_INFLIGHT_ID_LIMIT );
|
|
358
|
+
|
|
359
|
+
// Build MQTT options.
|
|
360
|
+
//
|
|
361
|
+
// NO outgoingStore: the client runs its default SYNCHRONOUS memory
|
|
362
|
+
// store (ADR-021). mqtt.js erases its packet-id bookkeeping on every
|
|
363
|
+
// connack and rebuilds it from a store snapshot asynchronously; with
|
|
364
|
+
// an asynchronous store, writes still in flight at that instant are
|
|
365
|
+
// invisible to the snapshot and get overwritten by id reuse — real
|
|
366
|
+
// QoS-1 loss, measured scaling with store write latency. The
|
|
367
|
+
// synchronous store makes the gap zero-width (validated 2026-07-09,
|
|
368
|
+
// nine-run matrix incl. reconnects at 45k in flight).
|
|
369
|
+
//
|
|
370
|
+
// messageIdProvider: the client's default provider cycles the 16-bit
|
|
371
|
+
// id space with NO in-use check — at 14 k msg/s it wraps every ~5 s,
|
|
372
|
+
// and an id whose PUBACK never arrived gets reassigned, overwriting
|
|
373
|
+
// the unacked packet's memory-store entry (same key). Measured at
|
|
374
|
+
// about one lost publish per 700,000 under sustained load.
|
|
375
|
+
// UniqueMessageIdProvider never reissues an in-use id.
|
|
376
|
+
const mqttOptions = {
|
|
377
|
+
...MQTT_CONFIG,
|
|
378
|
+
clientId,
|
|
379
|
+
messageIdProvider: new mqtt.UniqueMessageIdProvider()
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
// Only add will if explicitly provided (validated + encoded by the
|
|
383
|
+
// helper above; classified throws per ADR-018)
|
|
384
|
+
if ( config.will ) {
|
|
385
|
+
mqttOptions.will = buildWillOptions( config.will, config.codec );
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Create MQTT client (injectable for testing). mqtt.connect throws
|
|
389
|
+
// its own raw TypeError on a malformed url; classify it like every
|
|
390
|
+
// other setup failure. The error text uses the redacted url — a
|
|
391
|
+
// malformed url can still carry credentials.
|
|
392
|
+
const connect = config.mqttConnectFn || mqtt.connect;
|
|
393
|
+
let client;
|
|
394
|
+
try {
|
|
395
|
+
client = connect( brokerUrl, mqttOptions );
|
|
396
|
+
} catch ( connectErr ) {
|
|
397
|
+
throw invalidConfig( `brokerUrl rejected by the MQTT client ('${redactedBrokerUrl}'): ${connectErr.message}` );
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Fill ratio of the unacked window: unacked / maxQueueSize, capped
|
|
402
|
+
* at 1. Sync, O(1), allocation-free (ADR-018).
|
|
403
|
+
* @returns {number}
|
|
404
|
+
*/
|
|
405
|
+
const getPressure = function () {
|
|
406
|
+
const ratio = state.unacked / maxQueueSize;
|
|
407
|
+
return ratio > 1 ? 1 : ratio;
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Check and signal backpressure
|
|
412
|
+
*/
|
|
413
|
+
const checkBackpressure = function () {
|
|
414
|
+
const pressure = getPressure();
|
|
415
|
+
|
|
416
|
+
if ( ( pressure > QUEUE_CRITICAL_THRESHOLD ) && config.onCritical ) {
|
|
417
|
+
config.onCritical( 'QUEUE_CRITICAL', pressure );
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if ( config.onBackpressure ) {
|
|
421
|
+
config.onBackpressure( pressure );
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Publish message immediately (fire-and-forget).
|
|
427
|
+
*
|
|
428
|
+
* Sync return per the ADR-018 sink contract:
|
|
429
|
+
* - `{ ok: true }` on successful buffer.
|
|
430
|
+
* - `{ ok: false, error: { code: 'STORAGE_FULL', message } }` if pre-flight
|
|
431
|
+
* pressure check rejects (pressure >= STORAGE_PRESSURE_LIMIT).
|
|
432
|
+
* - `{ ok: false, error: { code: 'SHUTTING_DOWN', message } }` if called
|
|
433
|
+
* while shutdown is in progress.
|
|
434
|
+
*
|
|
435
|
+
* Async store-write or publish failures (after the message was buffered)
|
|
436
|
+
* route through `onDeliveryFailure(err, { topic })` when supplied; without
|
|
437
|
+
* a handler the adapter surfaces the failure via `Promise.reject`,
|
|
438
|
+
* which Node logs as unhandledRejection (and v15+ exits on). Loud failure
|
|
439
|
+
* beats silent loss.
|
|
440
|
+
*
|
|
441
|
+
* @param {string} topic - MQTT topic
|
|
442
|
+
* @param {*} message - Message payload (will be encoded via config.codec)
|
|
443
|
+
* @param {Object} [options] - Per-message options
|
|
444
|
+
* @param {string} [options.type] - Message-type key for MESSAGE_EXPIRY lookup
|
|
445
|
+
* @returns {{ok: true} | {ok: false, error: {code: string, message: string}}}
|
|
446
|
+
*/
|
|
447
|
+
const publishNow = function ( topic, message, options ) {
|
|
448
|
+
if ( state.shuttingDown ) {
|
|
449
|
+
return ERR_SHUTTING_DOWN;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Pre-flight pressure check — reject above STORAGE_PRESSURE_LIMIT to
|
|
453
|
+
// leave headroom for in-flight drain and to surface backpressure to
|
|
454
|
+
// producers before the store is literally full.
|
|
455
|
+
if ( getPressure() >= STORAGE_PRESSURE_LIMIT ) {
|
|
456
|
+
return ERR_STORAGE_FULL;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Encode BEFORE the counter moves. A message the codec cannot
|
|
460
|
+
// encode was never in flight, so it must not occupy a slot: a
|
|
461
|
+
// leaked slot never drains, pressure ratchets up, and the
|
|
462
|
+
// emitter ends up refusing everything. The
|
|
463
|
+
// refusal is synchronous and classified; building it allocates,
|
|
464
|
+
// which is fine on an error path this rare.
|
|
465
|
+
let payload;
|
|
466
|
+
try {
|
|
467
|
+
payload = config.codec.pack( message );
|
|
468
|
+
} catch ( packErr ) {
|
|
469
|
+
state.stats.encodeErrors += 1;
|
|
470
|
+
return {
|
|
471
|
+
ok: false,
|
|
472
|
+
error: {
|
|
473
|
+
code: 'ENCODE_ERROR',
|
|
474
|
+
message: `Message could not be encoded (topic=${topic}): ${packErr.message}`
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const dedupId = crypto.randomUUID();
|
|
480
|
+
const messageType = ( options && options.type ) || 'default';
|
|
481
|
+
const expiry = MESSAGE_EXPIRY[ messageType ] || MESSAGE_EXPIRY.default;
|
|
482
|
+
|
|
483
|
+
// Per-message allocations from here to the publish call (the
|
|
484
|
+
// uuid, the properties object, the callback closure) cannot be
|
|
485
|
+
// pre-allocated and reused: mqtt.js keeps the packet — properties
|
|
486
|
+
// included — until PUBACK so it can retransmit, and a shared
|
|
487
|
+
// mutated object would corrupt every queued retransmission (the
|
|
488
|
+
// unavoidable-residual justification ADR-018 asks for).
|
|
489
|
+
const properties = {
|
|
490
|
+
messageExpiryInterval: expiry,
|
|
491
|
+
userProperties: {
|
|
492
|
+
[ WINK_NAMESPACE.dedupId ]: dedupId,
|
|
493
|
+
[ WINK_NAMESPACE.timestamp ]: Date.now().toString(),
|
|
494
|
+
[ WINK_NAMESPACE.version ]: '1.0'
|
|
495
|
+
},
|
|
496
|
+
contentType: config.codec.contentType
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
if ( config.codec.payloadFormatIndicator === 1 ) {
|
|
500
|
+
properties.payloadFormatIndicator = true;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Accept: the message is now in flight. The counter rises HERE,
|
|
504
|
+
// synchronously with the accept decision, so the very next
|
|
505
|
+
// publishNow call sees the true pressure (the old design's
|
|
506
|
+
// optimistic-increment lesson, kept).
|
|
507
|
+
state.unacked += 1;
|
|
508
|
+
|
|
509
|
+
// Fire-and-forget publish with QoS 1. Outcome of this async callback is
|
|
510
|
+
// observability-only — the publishNow caller has already received a
|
|
511
|
+
// synchronous result. The callback fires exactly once per publish
|
|
512
|
+
// (acknowledgment or failure), so the decrement below is the
|
|
513
|
+
// counter's single exit — paired with the catch, which releases
|
|
514
|
+
// the slot when the client rejects the call before accepting it.
|
|
515
|
+
try {
|
|
516
|
+
client.publish(
|
|
517
|
+
topic,
|
|
518
|
+
payload,
|
|
519
|
+
{ qos: QOS, properties },
|
|
520
|
+
( err ) => {
|
|
521
|
+
state.unacked -= 1;
|
|
522
|
+
if ( err ) {
|
|
523
|
+
state.stats.publishErrors += 1;
|
|
524
|
+
const deliveryErr = new Error(
|
|
525
|
+
`MQTT publish failed (topic=${topic}): ${err.message || err.code || 'unknown'}`
|
|
526
|
+
);
|
|
527
|
+
deliveryErr.code = 'DELIVERY_FAILED';
|
|
528
|
+
deliveryErr.cause = err;
|
|
529
|
+
if ( config.onDeliveryFailure ) {
|
|
530
|
+
config.onDeliveryFailure( deliveryErr, { topic } );
|
|
531
|
+
} else {
|
|
532
|
+
// No handler — surface as unhandledRejection so the
|
|
533
|
+
// process logs loudly and (Node 15+) exits. Mirrors
|
|
534
|
+
// QuestDB's persist-plan.js catch-and-throw: both
|
|
535
|
+
// adapters route undeclared async failures
|
|
536
|
+
// through the same Node-level escape hatch.
|
|
537
|
+
Promise.reject( deliveryErr );
|
|
538
|
+
}
|
|
539
|
+
} else {
|
|
540
|
+
state.stats.published += 1;
|
|
541
|
+
}
|
|
542
|
+
checkBackpressure();
|
|
543
|
+
}
|
|
544
|
+
);
|
|
545
|
+
} catch ( publishErr ) {
|
|
546
|
+
// The client threw before accepting the message; its callback
|
|
547
|
+
// will never fire, so the slot is released here — the sync
|
|
548
|
+
// face of DELIVERY_FAILED (see the header vocabulary).
|
|
549
|
+
state.unacked -= 1;
|
|
550
|
+
state.stats.publishErrors += 1;
|
|
551
|
+
return {
|
|
552
|
+
ok: false,
|
|
553
|
+
error: {
|
|
554
|
+
code: 'DELIVERY_FAILED',
|
|
555
|
+
message: `MQTT publish rejected (topic=${topic}): ${publishErr.message}`
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
return RESULT_OK;
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
// Shutdown outcome, latched on the first call. A second
|
|
564
|
+
// caller used to get an instant clean resolve — mid-drain, or even
|
|
565
|
+
// after a shutdown that recorded dropped messages. Every caller now
|
|
566
|
+
// receives the first call's promise.
|
|
567
|
+
let shutdownPromise = null;
|
|
568
|
+
|
|
569
|
+
const DEFAULT_SHUTDOWN_TIMEOUT_MS = 5000;
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* The real shutdown body. `shutdown` below latches its promise so
|
|
573
|
+
* every caller receives this one outcome.
|
|
574
|
+
*
|
|
575
|
+
* **Drain-then-close** (a sustained-load finding, kept through the
|
|
576
|
+
* ADR-021 rework): mqtt.js's own `end()` does not reliably drain
|
|
577
|
+
* in-flight publishes, so the emitter waits on its own counter.
|
|
578
|
+
*
|
|
579
|
+
* 1. Sets `state.shuttingDown` so `publishNow` rejects new work
|
|
580
|
+
* (returns `SHUTTING_DOWN` immediately).
|
|
581
|
+
* 2. **Polls the unacked counter until zero**, using the whole
|
|
582
|
+
* `timeout` budget — no early give-up. The wait runs whether or
|
|
583
|
+
* not the connection is up at the moment of the call: mqtt.js
|
|
584
|
+
* auto-reconnects, so a mid-drain reconnect can still deliver
|
|
585
|
+
* the backlog inside the budget.
|
|
586
|
+
* 3. Once drained (or budget expired), calls `client.end(...)`
|
|
587
|
+
* with the remaining time as the broker-DISCONNECT budget.
|
|
588
|
+
* 4. Force-closes if the broker hangs the DISCONNECT.
|
|
589
|
+
*
|
|
590
|
+
* The old design's shutdown re-drive (re-sending stragglers through
|
|
591
|
+
* a store stream scan) died with the disk store: with the memory
|
|
592
|
+
* store there is no store to scan, and a message whose PUBACK was
|
|
593
|
+
* lost in-session is re-sent by the client itself on the next
|
|
594
|
+
* reconnect. If it is still unacknowledged at the deadline, the
|
|
595
|
+
* loss report counts it.
|
|
596
|
+
*
|
|
597
|
+
* A clean resolve is a delivery statement (ADR-018). When the
|
|
598
|
+
* drain could not complete, shutdown rejects with classified
|
|
599
|
+
* `SHUTDOWN_TIMEOUT` and `dropped: { count }` — the counter's exact
|
|
600
|
+
* value — after the close has already happened (teardown first,
|
|
601
|
+
* then the report). This fires CONNECTED OR NOT: with no disk
|
|
602
|
+
* store, nothing survives the process, so a disconnected shutdown
|
|
603
|
+
* with pending messages is a real loss and is reported as one. (The
|
|
604
|
+
* wal-backed design resolved that case clean on purpose — its disk
|
|
605
|
+
* store held the messages for the next session. ADR-021 traded that
|
|
606
|
+
* away and says so.)
|
|
607
|
+
*/
|
|
608
|
+
const doShutdown = async function ( timeout ) {
|
|
609
|
+
state.shuttingDown = true;
|
|
610
|
+
|
|
611
|
+
// A non-finite or non-positive timeout collapses the drain
|
|
612
|
+
// (Infinity overflows setTimeout, which clamps it to ~1 ms;
|
|
613
|
+
// NaN fails every deadline comparison), so it falls back to
|
|
614
|
+
// the default budget.
|
|
615
|
+
const budget = ( Number.isFinite( timeout ) && timeout > 0 ) ? timeout : DEFAULT_SHUTDOWN_TIMEOUT_MS;
|
|
616
|
+
const deadline = Date.now() + budget;
|
|
617
|
+
const drainPollIntervalMs = 25;
|
|
618
|
+
|
|
619
|
+
// Drain wait — one cheap counter read per tick; sequential
|
|
620
|
+
// awaits are the wait-for-condition pattern.
|
|
621
|
+
while ( Date.now() < deadline && state.unacked > 0 ) {
|
|
622
|
+
// eslint-disable-next-line no-await-in-loop
|
|
623
|
+
await new Promise( ( r ) => setTimeout( r, drainPollIntervalMs ) );
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Whatever is still unacknowledged goes undelivered — publishNow
|
|
627
|
+
// stopped accepting work at step 1, so the counter has settled
|
|
628
|
+
// into its final value. It becomes the `dropped` payload below.
|
|
629
|
+
const undeliveredCount = state.unacked;
|
|
630
|
+
const remaining = Date.now() - deadline;
|
|
631
|
+
const closeBudget = remaining < 0 ? Math.abs( remaining ) : 1;
|
|
632
|
+
|
|
633
|
+
await new Promise( ( resolve ) => {
|
|
634
|
+
const forceTimer = setTimeout( () => {
|
|
635
|
+
client.end( true );
|
|
636
|
+
resolve();
|
|
637
|
+
}, closeBudget );
|
|
638
|
+
|
|
639
|
+
if ( state.connected ) {
|
|
640
|
+
client.end( false, {}, () => {
|
|
641
|
+
clearTimeout( forceTimer );
|
|
642
|
+
resolve();
|
|
643
|
+
} );
|
|
644
|
+
} else {
|
|
645
|
+
clearTimeout( forceTimer );
|
|
646
|
+
client.end( true );
|
|
647
|
+
resolve();
|
|
648
|
+
}
|
|
649
|
+
} );
|
|
650
|
+
|
|
651
|
+
// Teardown first, then the report: the close has already happened;
|
|
652
|
+
// an incomplete drain must not read as a clean one (ADR-018).
|
|
653
|
+
if ( undeliveredCount > 0 ) {
|
|
654
|
+
const err = new Error(
|
|
655
|
+
`WinkComposer/mqtt-emitter: shutdown closed with ${undeliveredCount} message(s) unacknowledged`
|
|
656
|
+
);
|
|
657
|
+
err.code = 'SHUTDOWN_TIMEOUT';
|
|
658
|
+
err.dropped = { count: undeliveredCount };
|
|
659
|
+
throw err;
|
|
660
|
+
}
|
|
661
|
+
return undefined;
|
|
662
|
+
}; // doShutdown()
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Graceful shutdown — drain semantics with a timeout floor.
|
|
666
|
+
*
|
|
667
|
+
* Accepts the ADR-018 shutdown-contract shape `{ timeout }` (defaults to
|
|
668
|
+
* 5000ms). The destructure with `= {}` lets callers invoke
|
|
669
|
+
* `shutdown()` with no argument, `shutdown( {} )`, or
|
|
670
|
+
* `shutdown( { timeout: N } )`.
|
|
671
|
+
*
|
|
672
|
+
* The outcome is latched: the first call runs the shutdown, every
|
|
673
|
+
* later call returns the same promise. A caller
|
|
674
|
+
* arriving mid-drain waits for the real teardown; a caller arriving
|
|
675
|
+
* after a lossy shutdown sees the same classified rejection, never
|
|
676
|
+
* a contradicting clean resolve. One consequence: the first
|
|
677
|
+
* caller's `{ timeout }` governs; a later caller's is ignored.
|
|
678
|
+
*
|
|
679
|
+
* @param {{timeout?: number}} [options]
|
|
680
|
+
* @returns {Promise<void>}
|
|
681
|
+
*/
|
|
682
|
+
const shutdown = function ( { timeout = DEFAULT_SHUTDOWN_TIMEOUT_MS } = {} ) {
|
|
683
|
+
if ( !shutdownPromise ) {
|
|
684
|
+
shutdownPromise = doShutdown( timeout );
|
|
685
|
+
}
|
|
686
|
+
return shutdownPromise;
|
|
687
|
+
}; // shutdown()
|
|
688
|
+
|
|
689
|
+
// Connection handlers - minimal, no automatic status
|
|
690
|
+
client.on( 'connect', () => {
|
|
691
|
+
state.connected = true;
|
|
692
|
+
// mqtt.js fires 'connect' on every connack, including mid-run
|
|
693
|
+
// reconnects. Counting the later ones gives operators — and the
|
|
694
|
+
// release-soak signature policy — an observable "a reconnect
|
|
695
|
+
// happened during this run" fact via getHealth().stats.
|
|
696
|
+
if ( state.hasConnectedOnce ) {
|
|
697
|
+
state.stats.reconnects += 1;
|
|
698
|
+
}
|
|
699
|
+
state.hasConnectedOnce = true;
|
|
700
|
+
if ( config.debug ) {
|
|
701
|
+
console.log( `Connected to ${redactedBrokerUrl}` );
|
|
702
|
+
}
|
|
703
|
+
} );
|
|
704
|
+
|
|
705
|
+
client.on( 'offline', () => {
|
|
706
|
+
state.connected = false;
|
|
707
|
+
if ( config.debug ) {
|
|
708
|
+
console.log( `Offline - ${state.unacked} messages in flight` );
|
|
709
|
+
}
|
|
710
|
+
} );
|
|
711
|
+
|
|
712
|
+
client.on( 'error', ( err ) => {
|
|
713
|
+
state.stats.errors += 1;
|
|
714
|
+
if ( config.debug ) {
|
|
715
|
+
console.error( `MQTT error: ${err.message}` );
|
|
716
|
+
}
|
|
717
|
+
} );
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* Health snapshot for operator monitoring (uniform across sinks).
|
|
721
|
+
*
|
|
722
|
+
* Returns the ADR-018 health floor `{status, connected, pressure}` plus
|
|
723
|
+
* this adapter's own `stats` addition. `stats.unacked` is the live
|
|
724
|
+
* counter — the number of messages
|
|
725
|
+
* accepted but not yet acknowledged by the broker; exactly what a
|
|
726
|
+
* process crash at this instant would cost (ADR-021).
|
|
727
|
+
*
|
|
728
|
+
* Status derivation (uniform with Terminal/QuestDB; see file header):
|
|
729
|
+
* - `red` when `!connected`
|
|
730
|
+
* - `yellow` when connected AND `pressure >= HEALTH_PRESSURE_YELLOW_THRESHOLD`
|
|
731
|
+
* - `green` otherwise
|
|
732
|
+
*
|
|
733
|
+
* The wal-backed design's `storeHealth`, `circuitState`, and `metrics`
|
|
734
|
+
* diagnostics died with the LevelDB store (ADR-021).
|
|
735
|
+
*
|
|
736
|
+
* @returns {Object} Health snapshot — required floor + live counter
|
|
737
|
+
*/
|
|
738
|
+
const getHealth = function () {
|
|
739
|
+
const pressure = getPressure();
|
|
740
|
+
|
|
741
|
+
let status;
|
|
742
|
+
if ( !state.connected ) {
|
|
743
|
+
status = 'red';
|
|
744
|
+
} else if ( pressure >= HEALTH_PRESSURE_YELLOW_THRESHOLD ) {
|
|
745
|
+
status = 'yellow';
|
|
746
|
+
} else {
|
|
747
|
+
status = 'green';
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
return {
|
|
751
|
+
// Required floor (ADR-018)
|
|
752
|
+
status,
|
|
753
|
+
connected: state.connected,
|
|
754
|
+
pressure,
|
|
755
|
+
// This adapter's addition beyond the floor
|
|
756
|
+
stats: { ...state.stats, unacked: state.unacked }
|
|
757
|
+
};
|
|
758
|
+
};
|
|
759
|
+
|
|
760
|
+
const handle = {
|
|
761
|
+
publishNow,
|
|
762
|
+
getHealth,
|
|
763
|
+
shutdown,
|
|
764
|
+
getPressure
|
|
765
|
+
};
|
|
766
|
+
|
|
767
|
+
// First-connack grace (bounded). With a grace, the return is a
|
|
768
|
+
// Promise the wiring layer awaits (ADR-018 allows sync or async
|
|
769
|
+
// factories): by the time wire() hands the emitter to the flow,
|
|
770
|
+
// the client has either seen its first connack or spent the
|
|
771
|
+
// budget. Expiry is not an error — the posture stays 'recovering'.
|
|
772
|
+
// With connectGraceMs 0 the handle returns synchronously, exactly
|
|
773
|
+
// the pre-grace behavior. The permanent 'connect' handler above is
|
|
774
|
+
// attached before the wait's one-shot listener, so a handle
|
|
775
|
+
// resolved via connack already reports `connected: true`.
|
|
776
|
+
if ( connectGraceMs === 0 ) {
|
|
777
|
+
return handle;
|
|
778
|
+
}
|
|
779
|
+
return waitForFirstConnect( client, connectGraceMs ).then( () => handle );
|
|
780
|
+
}; // createMQTTEmitter()
|