@winkjs/composer 0.0.2 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (451) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +106 -80
  3. package/package.json +56 -28
  4. package/src/composer.js +75 -0
  5. package/src/core/batch/aoo-encoder.js +148 -0
  6. package/src/core/batch/index.js +12 -0
  7. package/src/core/batch/sliding-window.js +91 -0
  8. package/src/core/codec/index.js +137 -0
  9. package/src/core/emitter-manager/mqtt/constants.js +102 -0
  10. package/src/core/emitter-manager/mqtt/emitter.js +780 -0
  11. package/src/core/emitter-manager/mqtt/index.js +190 -0
  12. package/src/core/emitter-manager/mqtt/mqtt-store.js +538 -0
  13. package/src/core/emitter-manager/terminal/assert-columns.js +98 -0
  14. package/src/core/emitter-manager/terminal/formatters.js +160 -0
  15. package/src/core/emitter-manager/terminal/index.js +341 -0
  16. package/src/core/env-vars.js +245 -0
  17. package/src/core/mqtt-protocol.js +47 -0
  18. package/src/core/partition-manager/index.js +7 -0
  19. package/src/core/partition-manager/init.js +44 -0
  20. package/src/core/partition-manager/introspect.js +15 -0
  21. package/src/core/partition-manager/publish-to.js +6 -0
  22. package/src/core/partition-manager/recompute.js +5 -0
  23. package/src/core/partition-manager/reset.js +5 -0
  24. package/src/core/partition-manager/update.js +264 -0
  25. package/src/core/partition-manager/validate-flow.js +157 -0
  26. package/src/core/semantics/digest.js +128 -0
  27. package/src/core/semantics/index.js +83 -0
  28. package/src/core/semantics/loader.js +571 -0
  29. package/src/core/semantics/schemas/asset-class-schema.js +218 -0
  30. package/src/core/semantics/schemas/column-schema.js +651 -0
  31. package/src/core/semantics/schemas/enum-schema.js +153 -0
  32. package/src/core/semantics/schemas/index.js +51 -0
  33. package/src/core/semantics/warnings.js +141 -0
  34. package/src/core/shutdown-manager/index.js +289 -0
  35. package/src/core/source-manager/csv/index.js +171 -0
  36. package/src/core/source-manager/csv/start.js +456 -0
  37. package/src/core/source-manager/mqtt/client.js +406 -0
  38. package/src/core/source-manager/mqtt/constants.js +147 -0
  39. package/src/core/source-manager/mqtt/dedup.js +244 -0
  40. package/src/core/source-manager/mqtt/index.js +260 -0
  41. package/src/core/source-manager/mqtt/status.js +546 -0
  42. package/src/core/source-manager/test-harness/comparator.js +226 -0
  43. package/src/core/source-manager/test-harness/field-generator.js +85 -0
  44. package/src/core/source-manager/test-harness/fuzz.js +87 -0
  45. package/src/core/source-manager/test-harness/index.js +115 -0
  46. package/src/core/source-manager/test-harness/prng.js +57 -0
  47. package/src/core/source-manager/test-harness/start.js +232 -0
  48. package/src/core/source-manager/test-harness/validate.js +213 -0
  49. package/src/core/storage-manager/questdb/assert-columns.js +120 -0
  50. package/src/core/storage-manager/questdb/ensure-tables.js +187 -0
  51. package/src/core/storage-manager/questdb/index.js +1251 -0
  52. package/src/core/storage-manager/questdb/persist-plan.js +488 -0
  53. package/src/core/storage-manager/questdb/writers.js +142 -0
  54. package/src/core/test-utils/sealed-record.js +80 -0
  55. package/src/core/test-utils/soak-signature.js +149 -0
  56. package/src/core/test-utils/tcp-proxy.js +106 -0
  57. package/src/core/tunable/helpers.js +254 -0
  58. package/src/core/tunable/index.js +87 -0
  59. package/src/core/utils/console-formatter/index.js +207 -0
  60. package/src/core/utils/create-smart-store-as/index.js +98 -0
  61. package/src/core/utils/create-smart-store-as/sanitize-field.js +46 -0
  62. package/src/core/utils/create-smart-store-as/smart-truncate.js +93 -0
  63. package/src/core/utils/flow/index.js +5 -0
  64. package/src/core/utils/flow/make-collision-checker.js +23 -0
  65. package/src/core/utils/flow/to-kebab.js +20 -0
  66. package/src/core/utils/half-life/index.js +111 -0
  67. package/src/core/utils/node/disable.js +11 -0
  68. package/src/core/utils/node/enable.js +11 -0
  69. package/src/core/utils/node/execute-triggers.js +40 -0
  70. package/src/core/utils/node/index.js +10 -0
  71. package/src/core/utils/node/pause.js +11 -0
  72. package/src/core/utils/node/populate-predicate-inputs.js +14 -0
  73. package/src/core/utils/node/publish-nan.js +19 -0
  74. package/src/core/utils/node/reset-predicate-inputs.js +11 -0
  75. package/src/core/utils/node/unpause.js +11 -0
  76. package/src/core/utils/node/validate-spec.js +22 -0
  77. package/src/core/utils/options/resolve-field-keyed.js +115 -0
  78. package/src/core/utils/quantize/index.js +75 -0
  79. package/src/core/utils/stats/formulas.js +67 -0
  80. package/src/core/utils/stats/index.js +3 -0
  81. package/src/core/utils/validate/composer-validators.js +141 -0
  82. package/src/core/utils/validate/helpers.js +206 -0
  83. package/src/core/utils/validate/index.js +12 -0
  84. package/src/core/utils/validate/type-validators.js +538 -0
  85. package/src/core/utils/validate/validate.js +107 -0
  86. package/src/core/utils/validate/validators.js +177 -0
  87. package/src/core/wiring/assert-handle.js +57 -0
  88. package/src/core/wiring/assert-module.js +63 -0
  89. package/src/core/wiring/index.js +5 -0
  90. package/src/core/wiring/node-type-to-module.js +17 -0
  91. package/src/core/wiring/wire-emitters.js +256 -0
  92. package/src/core/wiring/wire-linear-graph.js +25 -0
  93. package/src/core/wiring/wire-node.js +157 -0
  94. package/src/core/wiring/wire-semantics.js +173 -0
  95. package/src/core/wiring/wire-storages.js +344 -0
  96. package/src/flow/check-output-collisions.js +93 -0
  97. package/src/flow/consts.js +9 -0
  98. package/src/flow/deep-clone-spec.js +64 -0
  99. package/src/flow/driver.js +171 -0
  100. package/src/flow/expand-single-input-array.js +76 -0
  101. package/src/flow/expand-template-spec.js +93 -0
  102. package/src/flow/flow-api-schemas.js +300 -0
  103. package/src/flow/flow.js +658 -0
  104. package/src/flow/for-each.js +225 -0
  105. package/src/flow/get-signature-pattern.js +31 -0
  106. package/src/flow/get-target-array.js +46 -0
  107. package/src/flow/inspect.js +67 -0
  108. package/src/flow/load-node-module.js +81 -0
  109. package/src/flow/make-node-method.js +112 -0
  110. package/src/flow/resolve-pick-by-field.js +77 -0
  111. package/src/flow/resolve-tunables.js +76 -0
  112. package/src/flow/run.js +392 -0
  113. package/src/flow/serialize.js +161 -0
  114. package/src/flow/signatures-schema.js +222 -0
  115. package/src/flow/spec-builder.js +65 -0
  116. package/src/flow/validate-triggers.js +250 -0
  117. package/src/flow/validate.js +261 -0
  118. package/src/nodes/accumulate/index.js +21 -0
  119. package/src/nodes/accumulate/init.js +48 -0
  120. package/src/nodes/accumulate/introspect.js +123 -0
  121. package/src/nodes/accumulate/publish-to.js +34 -0
  122. package/src/nodes/accumulate/recompute.js +27 -0
  123. package/src/nodes/accumulate/reset.js +25 -0
  124. package/src/nodes/accumulate/update.js +42 -0
  125. package/src/nodes/appraise/calibrate.js +64 -0
  126. package/src/nodes/appraise/decision.js +66 -0
  127. package/src/nodes/appraise/deviation.js +140 -0
  128. package/src/nodes/appraise/index.js +23 -0
  129. package/src/nodes/appraise/init.js +165 -0
  130. package/src/nodes/appraise/integrate.js +45 -0
  131. package/src/nodes/appraise/introspect.js +352 -0
  132. package/src/nodes/appraise/publish-to.js +93 -0
  133. package/src/nodes/appraise/receptor.js +69 -0
  134. package/src/nodes/appraise/recompute.js +79 -0
  135. package/src/nodes/appraise/reset.js +53 -0
  136. package/src/nodes/appraise/update.js +130 -0
  137. package/src/nodes/archive/csv/index.js +6 -0
  138. package/src/nodes/archive/csv/init.js +36 -0
  139. package/src/nodes/archive/csv/load.js +70 -0
  140. package/src/nodes/archive/csv/reset.js +14 -0
  141. package/src/nodes/archive/csv/simple-parser.js +216 -0
  142. package/src/nodes/archive/csv/test.js +173 -0
  143. package/src/nodes/archive/csv/update.js +63 -0
  144. package/src/nodes/butterworth-filter/index.js +18 -0
  145. package/src/nodes/butterworth-filter/init.js +184 -0
  146. package/src/nodes/butterworth-filter/introspect.js +228 -0
  147. package/src/nodes/butterworth-filter/publish-to.js +21 -0
  148. package/src/nodes/butterworth-filter/recompute.js +13 -0
  149. package/src/nodes/butterworth-filter/reset.js +30 -0
  150. package/src/nodes/butterworth-filter/update.js +38 -0
  151. package/src/nodes/categorize/index.js +19 -0
  152. package/src/nodes/categorize/init.js +58 -0
  153. package/src/nodes/categorize/introspect.js +177 -0
  154. package/src/nodes/categorize/publish-to.js +29 -0
  155. package/src/nodes/categorize/recompute.js +14 -0
  156. package/src/nodes/categorize/reset.js +16 -0
  157. package/src/nodes/categorize/update.js +53 -0
  158. package/src/nodes/controller/index.js +9 -0
  159. package/src/nodes/controller/init.js +55 -0
  160. package/src/nodes/controller/introspect.js +91 -0
  161. package/src/nodes/controller/publish-to.js +8 -0
  162. package/src/nodes/controller/recompute.js +9 -0
  163. package/src/nodes/controller/reset.js +17 -0
  164. package/src/nodes/controller/update.js +63 -0
  165. package/src/nodes/diff/index.js +19 -0
  166. package/src/nodes/diff/init.js +37 -0
  167. package/src/nodes/diff/introspect.js +132 -0
  168. package/src/nodes/diff/publish-to.js +21 -0
  169. package/src/nodes/diff/recompute.js +12 -0
  170. package/src/nodes/diff/reset.js +12 -0
  171. package/src/nodes/diff/update.js +37 -0
  172. package/src/nodes/digest-moments/formulas.js +12 -0
  173. package/src/nodes/digest-moments/index.js +14 -0
  174. package/src/nodes/digest-moments/init.js +67 -0
  175. package/src/nodes/digest-moments/introspect.js +154 -0
  176. package/src/nodes/digest-moments/publish-to.js +34 -0
  177. package/src/nodes/digest-moments/recompute.js +14 -0
  178. package/src/nodes/digest-moments/reset.js +14 -0
  179. package/src/nodes/digest-moments/update.js +120 -0
  180. package/src/nodes/dwell-time-tracker/index.js +14 -0
  181. package/src/nodes/dwell-time-tracker/init.js +59 -0
  182. package/src/nodes/dwell-time-tracker/introspect.js +137 -0
  183. package/src/nodes/dwell-time-tracker/publish-to.js +22 -0
  184. package/src/nodes/dwell-time-tracker/recompute.js +8 -0
  185. package/src/nodes/dwell-time-tracker/reset.js +23 -0
  186. package/src/nodes/dwell-time-tracker/update.js +103 -0
  187. package/src/nodes/emit-if/helpers.js +94 -0
  188. package/src/nodes/emit-if/index.js +16 -0
  189. package/src/nodes/emit-if/init.js +67 -0
  190. package/src/nodes/emit-if/introspect.js +102 -0
  191. package/src/nodes/emit-if/publish-to.js +13 -0
  192. package/src/nodes/emit-if/recompute.js +12 -0
  193. package/src/nodes/emit-if/reset.js +27 -0
  194. package/src/nodes/emit-if/update.js +93 -0
  195. package/src/nodes/es-correlation/index.js +19 -0
  196. package/src/nodes/es-correlation/init.js +73 -0
  197. package/src/nodes/es-correlation/introspect.js +176 -0
  198. package/src/nodes/es-correlation/publish-to.js +32 -0
  199. package/src/nodes/es-correlation/recompute.js +56 -0
  200. package/src/nodes/es-correlation/reset.js +27 -0
  201. package/src/nodes/es-correlation/update.js +110 -0
  202. package/src/nodes/es-mean/index.js +20 -0
  203. package/src/nodes/es-mean/init.js +58 -0
  204. package/src/nodes/es-mean/introspect.js +129 -0
  205. package/src/nodes/es-mean/publish-to.js +19 -0
  206. package/src/nodes/es-mean/recompute.js +16 -0
  207. package/src/nodes/es-mean/reset.js +24 -0
  208. package/src/nodes/es-mean/update.js +69 -0
  209. package/src/nodes/es-pairwise-correlation/index.js +19 -0
  210. package/src/nodes/es-pairwise-correlation/init.js +98 -0
  211. package/src/nodes/es-pairwise-correlation/introspect.js +187 -0
  212. package/src/nodes/es-pairwise-correlation/publish-to.js +31 -0
  213. package/src/nodes/es-pairwise-correlation/recompute.js +69 -0
  214. package/src/nodes/es-pairwise-correlation/reset.js +29 -0
  215. package/src/nodes/es-pairwise-correlation/update.js +89 -0
  216. package/src/nodes/es-stats/index.js +21 -0
  217. package/src/nodes/es-stats/init.js +105 -0
  218. package/src/nodes/es-stats/introspect.js +157 -0
  219. package/src/nodes/es-stats/publish-to.js +42 -0
  220. package/src/nodes/es-stats/recompute.js +74 -0
  221. package/src/nodes/es-stats/reset.js +49 -0
  222. package/src/nodes/es-stats/update-envelope.js +44 -0
  223. package/src/nodes/es-stats/update-welford.js +64 -0
  224. package/src/nodes/es-stats/update.js +59 -0
  225. package/src/nodes/index.js +83 -0
  226. package/src/nodes/invert-flag/index.js +20 -0
  227. package/src/nodes/invert-flag/init.js +36 -0
  228. package/src/nodes/invert-flag/introspect.js +115 -0
  229. package/src/nodes/invert-flag/publish-to.js +23 -0
  230. package/src/nodes/invert-flag/recompute.js +12 -0
  231. package/src/nodes/invert-flag/reset.js +12 -0
  232. package/src/nodes/invert-flag/update.js +32 -0
  233. package/src/nodes/kalman1d/index.js +20 -0
  234. package/src/nodes/kalman1d/init.js +103 -0
  235. package/src/nodes/kalman1d/introspect.js +250 -0
  236. package/src/nodes/kalman1d/publish-to.js +34 -0
  237. package/src/nodes/kalman1d/recompute.js +23 -0
  238. package/src/nodes/kalman1d/reset.js +29 -0
  239. package/src/nodes/kalman1d/update.js +144 -0
  240. package/src/nodes/kernel/index.js +18 -0
  241. package/src/nodes/kernel/init.js +66 -0
  242. package/src/nodes/kernel/introspect.js +163 -0
  243. package/src/nodes/kernel/presets.js +74 -0
  244. package/src/nodes/kernel/publish-to.js +25 -0
  245. package/src/nodes/kernel/recompute.js +14 -0
  246. package/src/nodes/kernel/reset.js +20 -0
  247. package/src/nodes/kernel/update.js +69 -0
  248. package/src/nodes/lag/index.js +21 -0
  249. package/src/nodes/lag/init.js +87 -0
  250. package/src/nodes/lag/introspect.js +202 -0
  251. package/src/nodes/lag/publish-to.js +64 -0
  252. package/src/nodes/lag/recompute.js +33 -0
  253. package/src/nodes/lag/reset.js +49 -0
  254. package/src/nodes/lag/update.js +133 -0
  255. package/src/nodes/median3/index.js +19 -0
  256. package/src/nodes/median3/init.js +40 -0
  257. package/src/nodes/median3/introspect.js +116 -0
  258. package/src/nodes/median3/publish-to.js +21 -0
  259. package/src/nodes/median3/recompute.js +12 -0
  260. package/src/nodes/median3/reset.js +15 -0
  261. package/src/nodes/median3/update.js +58 -0
  262. package/src/nodes/moments-digest/copy-from.js +12 -0
  263. package/src/nodes/moments-digest/flush.js +17 -0
  264. package/src/nodes/moments-digest/index.js +132 -0
  265. package/src/nodes/moments-digest/init.js +84 -0
  266. package/src/nodes/moments-digest/introspect.js +122 -0
  267. package/src/nodes/moments-digest/prelude.js +28 -0
  268. package/src/nodes/moments-digest/publish-to.js +31 -0
  269. package/src/nodes/moments-digest/recompute.js +9 -0
  270. package/src/nodes/moments-digest/reset.js +20 -0
  271. package/src/nodes/moments-digest/update.js +182 -0
  272. package/src/nodes/node-names.js +67 -0
  273. package/src/nodes/page-hinkley/index.js +20 -0
  274. package/src/nodes/page-hinkley/init.js +66 -0
  275. package/src/nodes/page-hinkley/introspect.js +193 -0
  276. package/src/nodes/page-hinkley/publish-to.js +28 -0
  277. package/src/nodes/page-hinkley/recompute.js +14 -0
  278. package/src/nodes/page-hinkley/reset.js +23 -0
  279. package/src/nodes/page-hinkley/update.js +105 -0
  280. package/src/nodes/pass-if/index.js +21 -0
  281. package/src/nodes/pass-if/init.js +37 -0
  282. package/src/nodes/pass-if/introspect.js +93 -0
  283. package/src/nodes/pass-if/publish-to.js +15 -0
  284. package/src/nodes/pass-if/recompute.js +15 -0
  285. package/src/nodes/pass-if/reset.js +18 -0
  286. package/src/nodes/pass-if/update.js +37 -0
  287. package/src/nodes/persist-if/helpers.js +111 -0
  288. package/src/nodes/persist-if/index.js +9 -0
  289. package/src/nodes/persist-if/init.js +98 -0
  290. package/src/nodes/persist-if/introspect.js +106 -0
  291. package/src/nodes/persist-if/publish-to.js +21 -0
  292. package/src/nodes/persist-if/recompute.js +20 -0
  293. package/src/nodes/persist-if/reset.js +35 -0
  294. package/src/nodes/persist-if/update.js +112 -0
  295. package/src/nodes/persistence-check/index.js +14 -0
  296. package/src/nodes/persistence-check/init.js +38 -0
  297. package/src/nodes/persistence-check/introspect.js +144 -0
  298. package/src/nodes/persistence-check/publish-to.js +11 -0
  299. package/src/nodes/persistence-check/recompute.js +7 -0
  300. package/src/nodes/persistence-check/reset.js +12 -0
  301. package/src/nodes/persistence-check/update.js +57 -0
  302. package/src/nodes/process-index/index.js +15 -0
  303. package/src/nodes/process-index/init.js +48 -0
  304. package/src/nodes/process-index/introspect.js +182 -0
  305. package/src/nodes/process-index/publish-to.js +29 -0
  306. package/src/nodes/process-index/recompute.js +8 -0
  307. package/src/nodes/process-index/reset.js +8 -0
  308. package/src/nodes/process-index/update.js +75 -0
  309. package/src/nodes/ratio/index.js +14 -0
  310. package/src/nodes/ratio/init.js +37 -0
  311. package/src/nodes/ratio/introspect.js +155 -0
  312. package/src/nodes/ratio/publish-to.js +15 -0
  313. package/src/nodes/ratio/recompute.js +7 -0
  314. package/src/nodes/ratio/reset.js +7 -0
  315. package/src/nodes/ratio/update.js +47 -0
  316. package/src/nodes/sanitize/index.js +82 -0
  317. package/src/nodes/sanitize/init.js +82 -0
  318. package/src/nodes/sanitize/introspect.js +194 -0
  319. package/src/nodes/sanitize/publish-to.js +15 -0
  320. package/src/nodes/sanitize/recompute.js +8 -0
  321. package/src/nodes/sanitize/reset.js +14 -0
  322. package/src/nodes/sanitize/update.js +102 -0
  323. package/src/nodes/spike-guard/index.js +14 -0
  324. package/src/nodes/spike-guard/init.js +47 -0
  325. package/src/nodes/spike-guard/introspect.js +130 -0
  326. package/src/nodes/spike-guard/publish-to.js +35 -0
  327. package/src/nodes/spike-guard/recompute.js +14 -0
  328. package/src/nodes/spike-guard/reset.js +19 -0
  329. package/src/nodes/spike-guard/update.js +89 -0
  330. package/src/nodes/sse-emitter/index.js +7 -0
  331. package/src/nodes/sse-emitter/init.js +105 -0
  332. package/src/nodes/sse-emitter/introspect.js +134 -0
  333. package/src/nodes/sse-emitter/publish-to.js +6 -0
  334. package/src/nodes/sse-emitter/recompute.js +16 -0
  335. package/src/nodes/sse-emitter/reset.js +19 -0
  336. package/src/nodes/sse-emitter/update.js +69 -0
  337. package/src/nodes/sse-emitter/utils/broadcast-heartbeat.js +36 -0
  338. package/src/nodes/sse-emitter/utils/build-message.js +31 -0
  339. package/src/nodes/sse-emitter/utils/connection-manager.js +45 -0
  340. package/src/nodes/sse-emitter/utils/endpoint-initializer.js +20 -0
  341. package/src/nodes/sse-emitter/utils/health-monitor.js +38 -0
  342. package/src/nodes/sse-emitter/utils/qos-handler.js +41 -0
  343. package/src/nodes/state-change-detector/helpers.js +35 -0
  344. package/src/nodes/state-change-detector/index.js +12 -0
  345. package/src/nodes/state-change-detector/init.js +56 -0
  346. package/src/nodes/state-change-detector/introspect.js +146 -0
  347. package/src/nodes/state-change-detector/publish-to.js +20 -0
  348. package/src/nodes/state-change-detector/recompute.js +6 -0
  349. package/src/nodes/state-change-detector/reset.js +20 -0
  350. package/src/nodes/state-change-detector/update.js +97 -0
  351. package/src/nodes/sw-stats/index.js +14 -0
  352. package/src/nodes/sw-stats/init.js +51 -0
  353. package/src/nodes/sw-stats/introspect.js +131 -0
  354. package/src/nodes/sw-stats/publish-to.js +45 -0
  355. package/src/nodes/sw-stats/recompute.js +33 -0
  356. package/src/nodes/sw-stats/reset.js +15 -0
  357. package/src/nodes/sw-stats/update.js +39 -0
  358. package/src/nodes/swing-watch/index.js +17 -0
  359. package/src/nodes/swing-watch/init.js +144 -0
  360. package/src/nodes/swing-watch/introspect.js +188 -0
  361. package/src/nodes/swing-watch/publish-to.js +53 -0
  362. package/src/nodes/swing-watch/recompute.js +22 -0
  363. package/src/nodes/swing-watch/reset.js +55 -0
  364. package/src/nodes/swing-watch/update.js +767 -0
  365. package/src/nodes/tally/index.js +15 -0
  366. package/src/nodes/tally/init.js +32 -0
  367. package/src/nodes/tally/introspect.js +116 -0
  368. package/src/nodes/tally/publish-to.js +28 -0
  369. package/src/nodes/tally/recompute.js +10 -0
  370. package/src/nodes/tally/reset.js +10 -0
  371. package/src/nodes/tally/update.js +44 -0
  372. package/src/nodes/threshold/index.js +14 -0
  373. package/src/nodes/threshold/init.js +121 -0
  374. package/src/nodes/threshold/introspect.js +192 -0
  375. package/src/nodes/threshold/publish-to.js +11 -0
  376. package/src/nodes/threshold/recompute.js +8 -0
  377. package/src/nodes/threshold/reset.js +14 -0
  378. package/src/nodes/threshold/update.js +54 -0
  379. package/src/nodes/transform/helpers.js +39 -0
  380. package/src/nodes/transform/index.js +15 -0
  381. package/src/nodes/transform/init.js +35 -0
  382. package/src/nodes/transform/introspect.js +118 -0
  383. package/src/nodes/transform/publish-to.js +23 -0
  384. package/src/nodes/transform/recompute.js +12 -0
  385. package/src/nodes/transform/reset.js +11 -0
  386. package/src/nodes/transform/update.js +32 -0
  387. package/src/nodes/trend/compute-confidence.js +112 -0
  388. package/src/nodes/trend/index.js +14 -0
  389. package/src/nodes/trend/init.js +81 -0
  390. package/src/nodes/trend/introspect.js +151 -0
  391. package/src/nodes/trend/publish-to.js +22 -0
  392. package/src/nodes/trend/recompute.js +15 -0
  393. package/src/nodes/trend/reset.js +26 -0
  394. package/src/nodes/trend/update.js +93 -0
  395. package/src/nodes/tw-stats/copy-from.js +16 -0
  396. package/src/nodes/tw-stats/flush.js +12 -0
  397. package/src/nodes/tw-stats/index.js +41 -0
  398. package/src/nodes/tw-stats/init.js +92 -0
  399. package/src/nodes/tw-stats/introspect.js +171 -0
  400. package/src/nodes/tw-stats/publish-to.js +108 -0
  401. package/src/nodes/tw-stats/recompute.js +9 -0
  402. package/src/nodes/tw-stats/reset.js +16 -0
  403. package/src/nodes/tw-stats/update.js +113 -0
  404. package/src/nodes/unbalance/index.js +15 -0
  405. package/src/nodes/unbalance/init.js +59 -0
  406. package/src/nodes/unbalance/introspect.js +167 -0
  407. package/src/nodes/unbalance/publish-to.js +52 -0
  408. package/src/nodes/unbalance/recompute.js +11 -0
  409. package/src/nodes/unbalance/reset.js +10 -0
  410. package/src/nodes/unbalance/update.js +83 -0
  411. package/src/nodes/vector-distance/index.js +14 -0
  412. package/src/nodes/vector-distance/init.js +54 -0
  413. package/src/nodes/vector-distance/introspect.js +149 -0
  414. package/src/nodes/vector-distance/publish-to.js +35 -0
  415. package/src/nodes/vector-distance/recompute.js +11 -0
  416. package/src/nodes/vector-distance/reset.js +20 -0
  417. package/src/nodes/vector-distance/update.js +139 -0
  418. package/src/nodes/winnow/index.js +12 -0
  419. package/src/nodes/winnow/init.js +82 -0
  420. package/src/nodes/winnow/introspect.js +253 -0
  421. package/src/nodes/winnow/publish-to.js +51 -0
  422. package/src/nodes/winnow/recompute.js +14 -0
  423. package/src/nodes/winnow/reset.js +34 -0
  424. package/src/nodes/winnow/update.js +203 -0
  425. package/src/tools/training/features.js +227 -0
  426. package/src/tools/training/index.js +21 -0
  427. package/src/tools/training/logistic.js +238 -0
  428. package/src/tools/training/metrics.js +126 -0
  429. package/src/tools/training/scale.js +239 -0
  430. package/src/tools/training/validate.js +103 -0
  431. package/src/windowing/count-sliding/create.js +16 -0
  432. package/src/windowing/count-sliding/get-reverse-chronological-index.js +19 -0
  433. package/src/windowing/count-sliding/index.js +6 -0
  434. package/src/windowing/count-sliding/is-not-full.js +5 -0
  435. package/src/windowing/count-sliding/push.js +15 -0
  436. package/src/windowing/count-sliding/reset.js +11 -0
  437. package/src/windowing/count-sliding/size.js +5 -0
  438. package/src/windowing/time-sliding/create.js +25 -0
  439. package/src/windowing/time-sliding/push.js +42 -0
  440. package/CODE_OF_CONDUCT.md +0 -46
  441. package/CONTRIBUTING.md +0 -132
  442. package/docs/.nojekyll +0 -0
  443. package/docs/CONTRIBUTING.md +0 -134
  444. package/docs/README.md +0 -132
  445. package/docs/_coverpage.md +0 -26
  446. package/docs/_media/bg.png +0 -0
  447. package/docs/_media/wink-logo.png +0 -0
  448. package/docs/_navbar.md +0 -14
  449. package/docs/_sidebar.md +0 -2
  450. package/docs/index.html +0 -44
  451. 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()