@winkjs/composer 0.8.0 → 0.8.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.
package/README.md CHANGED
@@ -38,7 +38,7 @@ The Quick Start below builds one such pipeline end to end.
38
38
 
39
39
  ## Quick Start
40
40
 
41
- Requires Node.js 22 or newer.
41
+ Requires Node.js 22 or newer. Each release is certified on the newest long-term support line of Node.js, 26 today, and tested on 22 and 24.
42
42
 
43
43
  The Quick Start flow replays a CSV feed of pump motor temperatures. It cleans each reading, flags a hot motor, confirms the heat is not a blip, and prints an alert. One command scaffolds it as a runnable project:
44
44
 
@@ -106,15 +106,15 @@ The example above replays a CSV file. A live deployment reads from an MQTT broke
106
106
 
107
107
  The numbers below come from a pure compute benchmark — every step a live message takes through an 8-node flow, from arrival to final output. **Storage and MQTT I/O are excluded** — these are compute ceilings, not full-deployment numbers.
108
108
 
109
- | Configuration | Throughput |
110
- |----------------------|-------------------------|
111
- | Raspberry Pi 5 | ~100K messages/second |
112
- | MacBook Pro (M4 Max) | ~1.1M messages/second |
113
- | Tracking 10K assets | ~500K messages/second |
109
+ | Configuration | Throughput |
110
+ |-------------------------------|-------------------------|
111
+ | Revolution Pi Connect 5 (CM5) | 200K+ messages/second |
112
+ | MacBook Pro (M4 Max) | ~1.1M messages/second |
113
+ | Tracking 10K assets | ~500K messages/second |
114
114
 
115
- Both non-Pi rows were measured on a MacBook Pro (M4 Max) under Node.js 22. Every figure is single-core.
115
+ The first row was measured on a Revolution Pi Connect 5, an industrial computer built on the Raspberry Pi Compute Module 5. The other two rows were measured on a MacBook Pro (M4 Max). All three ran under Node.js 26. Every figure is single-core.
116
116
 
117
- Two configurations produced these numbers. The first two rows interleave 10 asset pipelines in random order — 4.5 million messages (10 pipelines × 900 data points × 500 iterations). The third row runs the same flow with 10,000 assets — 10,000 isolated states alive at once, 9 million messages, under 140 MB of heap. Pipelines are created dynamically as each asset first appears; timing uses `process.hrtime.bigint()`. The same pipeline [runs in your browser](https://composer.winkjs.org/docs/benchmark) — browser performance varies from native Node.js due to JIT differences.
117
+ Two configurations produced these numbers. The first two rows interleave 10 asset pipelines in random order — 4.5 million messages (10 pipelines × 900 data points × 500 iterations). The third row runs the same flow with 10,000 assets — 10,000 isolated states alive at once, 9 million messages, about 150 MB of heap. Pipelines are created dynamically as each asset first appears; timing uses `process.hrtime.bigint()`. The same pipeline [runs in your browser](https://composer.winkjs.org/docs/benchmark) — browser performance varies from native Node.js due to JIT differences.
118
118
 
119
119
  Reproduce them from a repo clone. `node benchmark/compare.js 10 500` reports the median of three rounds. `node benchmark/run-benchmark.js static 10000 1` gives the 10K-asset row.
120
120
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@winkjs/composer",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "Composable Streaming Intelligence — turn streaming data into actionable insights, from edge to cloud",
5
5
  "keywords": [
6
6
  "streaming",
@@ -61,23 +61,25 @@
61
61
  "access": "public"
62
62
  },
63
63
  "devDependencies": {
64
- "c8": "^10.1.3",
64
+ "c8": "^12.0.0",
65
65
  "chai": "^5.2.0",
66
66
  "classic-level": "^3.0.0",
67
67
  "eslint": "^9.21.0",
68
68
  "eslint-plugin-security": "^4.0.0",
69
- "mocha": "^11.1.0",
69
+ "mocha": "^12.0.3",
70
70
  "sinon": "^19.0.2"
71
71
  },
72
72
  "dependencies": {
73
73
  "@questdb/nodejs-client": "~4.2.0",
74
- "glob": "^13.0.6",
75
- "mqtt": "^5.15.1",
74
+ "glob": "^14.0.3",
75
+ "mqtt": "^5.16.1",
76
76
  "msgpackr": "^1.11.10",
77
77
  "pg": "^8.20.0"
78
78
  },
79
79
  "overrides": {
80
80
  "serialize-javascript": "^7.1.0",
81
- "diff": "^8.0.3"
81
+ "diff": "^8.0.3",
82
+ "minimatch": "^11.0.3",
83
+ "glob": "^14.0.3"
82
84
  }
83
85
  }
@@ -61,11 +61,27 @@ export const QUEUE_CRITICAL_THRESHOLD = 0.8;
61
61
  *
62
62
  * The emitter uses the client's UniqueMessageIdProvider, which never
63
63
  * reissues an id that is in use. So the unacked window must never
64
- * hold more messages than the id space has ids. 60,000 leaves margin
65
- * for ids stuck on packets that are never acknowledged.
64
+ * hold more messages than the id space has ids. The id space alone
65
+ * would allow about 60,000.
66
+ *
67
+ * A second, tighter limit sets the value. libuv 1.52 and later (the
68
+ * version inside Node 24 and Node 26) refuses one socket write call
69
+ * that carries more than 1,048,576 buffer pieces. The call fails
70
+ * with `write EINVAL` and the connection drops. Source:
71
+ * `uv__check_before_write` in libuv `src/unix/stream.c`, added by
72
+ * libuv PR #5014. mqtt-packet corks the socket, so every publish
73
+ * accepted inside one event-loop turn leaves in ONE write call. One
74
+ * MQTT 5 publish with this emitter's properties is 29 pieces. The
75
+ * accepted window is 0.9 × maxQueueSize, and the whole window must
76
+ * fit one call: 33,000 × 0.9 × 29 = 861,300 pieces, 82% of the
77
+ * limit. Even the full cap fits: 33,000 × 29 = 957,000, 91%.
78
+ * mqtt.js resends a stored backlog one packet per acknowledgement,
79
+ * but it releases the publishes queued during that resend in one
80
+ * turn, so the window bounds that path too. The constant keeps its
81
+ * original name. The id space is still the first of its two reasons.
66
82
  * @type {number}
67
83
  */
68
- export const MQTT_INFLIGHT_ID_LIMIT = 60000;
84
+ export const MQTT_INFLIGHT_ID_LIMIT = 33000;
69
85
 
70
86
  // Message expiry intervals (seconds), indexed by the caller's
71
87
  // `options.type`. A dictionary indexed by an external key has no
@@ -105,9 +105,10 @@
105
105
  * - Pre-flight reject fires at `pressure >= STORAGE_PRESSURE_LIMIT` (0.9),
106
106
  * leaving 10% headroom for in-flight drain on shutdown and a clean band
107
107
  * between the yellow-health threshold (0.66) and exhaustion (1.0).
108
- * - The cap is `maxQueueSize`, clamped to 60,000: every unacknowledged
109
- * QoS-1 message holds a 16-bit packet id, so one connection can never
110
- * carry more (see MQTT_INFLIGHT_ID_LIMIT).
108
+ * - The cap is `maxQueueSize`, clamped to MQTT_INFLIGHT_ID_LIMIT
109
+ * (33,000). Two limits set that ceiling: the 16-bit packet-id space,
110
+ * and the libuv cap on buffer pieces per socket write. constants.js
111
+ * carries both with their sources.
111
112
  * - The old byte-axis limit (`maxQueueBytes`) died with the disk store;
112
113
  * the count cap bounds memory at cap × payload size.
113
114
  *
@@ -65,7 +65,9 @@
65
65
  *
66
66
  * `brokerUrl` falls back to `MQTT_BROKER_URL`. The reconnect period
67
67
  * gains a random share of up to 20% per client (`utils/jitter`).
68
- * The cap is clamped to 60,000, the packet-id ceiling.
68
+ * The cap is clamped to 33,000, the in-flight ceiling. constants.js
69
+ * carries its two reasons: the packet-id space and the libuv write
70
+ * limit.
69
71
  *
70
72
  * Related decisions beyond ADR-018 and ADR-021. ADR-022: this adapter
71
73
  * writes the `winkDedupId` the source dedups on. ADR-023: `publishNow`
@@ -213,11 +213,11 @@ const resolveGrace = function ( config ) {
213
213
  }; // resolveGrace()
214
214
 
215
215
  /**
216
- * Resolves the unacked cap. Clamped to the 16-bit packet-id space:
217
- * every unacknowledged QoS-1 message holds a packet id, so one
218
- * connection can never carry more than 65,535; 60,000 leaves working
219
- * headroom (same clamp the LevelDB store applied, kept with the same
220
- * warning).
216
+ * Resolves the unacked cap, clamped to MQTT_INFLIGHT_ID_LIMIT. Two
217
+ * limits set that ceiling: the 16-bit packet-id space, and the libuv
218
+ * cap on buffer pieces per socket write. constants.js carries both
219
+ * with their sources. The clamp prints the same warning the LevelDB
220
+ * store applied.
221
221
  *
222
222
  * @param {Object} config - emitter config
223
223
  * @returns {number} the effective cap
@@ -227,7 +227,7 @@ const resolveQueueSize = function ( config ) {
227
227
  if ( requestedQueueSize > MQTT_INFLIGHT_ID_LIMIT ) {
228
228
  logger.warn(
229
229
  `winkComposer/mqttEmitter: maxQueueSize ${requestedQueueSize} exceeds the MQTT ` +
230
- `packet-id ceiling — clamped to ${MQTT_INFLIGHT_ID_LIMIT}`
230
+ `in-flight ceiling — clamped to ${MQTT_INFLIGHT_ID_LIMIT}`
231
231
  );
232
232
  }
233
233
  return Math.min( requestedQueueSize, MQTT_INFLIGHT_ID_LIMIT );