mqtt-plus 1.4.25 → 1.4.27
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/.ase/config.yaml +1 -0
- package/.ase/service.log +34 -0
- package/.ase/service.yaml +1 -1
- package/.ase/task/TASK-P18.md +1 -1
- package/.ase/task/TASK-P21.md +1 -1
- package/.claude/settings.local.json +4 -1
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +29 -5
- package/README.md +2 -2
- package/doc/mqtt-plus-broker-setup.md +1 -1
- package/doc/mqtt-plus-comm.md +3 -3
- package/doc/mqtt-plus-internals.md +6 -0
- package/dst-stage1/mqtt-plus-base.d.ts +2 -2
- package/dst-stage1/mqtt-plus-base.js +13 -10
- package/dst-stage1/mqtt-plus-error.js +54 -28
- package/dst-stage1/mqtt-plus-event.js +3 -1
- package/dst-stage1/mqtt-plus-options.js +7 -0
- package/dst-stage1/mqtt-plus-service.js +9 -6
- package/dst-stage1/mqtt-plus-sink.js +27 -54
- package/dst-stage1/mqtt-plus-source.js +13 -9
- package/dst-stage1/mqtt-plus-util.d.ts +1 -0
- package/dst-stage1/mqtt-plus-util.js +25 -4
- package/dst-stage1/mqtt-plus.d.ts +5 -1
- package/dst-stage1/mqtt-plus.js +10 -1
- package/dst-stage2/mqtt-plus.cjs.cjs +169 -123
- package/dst-stage2/mqtt-plus.esm.js +168 -122
- package/dst-stage2/mqtt-plus.umd.js +11 -11
- package/etc/stx.conf +3 -2
- package/etc/vite.mts +1 -1
- package/package.d/@typescript-eslint+typescript-estree+8.66.0.patch +12 -0
- package/package.d/{vite+8.1.4.patch → vite+8.2.1.patch} +2 -2
- package/package.json +16 -15
- package/src/mqtt-plus-base.ts +15 -12
- package/src/mqtt-plus-error.ts +65 -33
- package/src/mqtt-plus-event.ts +2 -1
- package/src/mqtt-plus-options.ts +8 -0
- package/src/mqtt-plus-service.ts +9 -6
- package/src/mqtt-plus-sink.ts +28 -54
- package/src/mqtt-plus-source.ts +13 -9
- package/src/mqtt-plus-util.ts +27 -4
- package/src/mqtt-plus.ts +14 -3
- package/tst/mqtt-plus-0-fixture.ts +1 -0
- package/tst/mqtt-plus-2-event.spec.ts +25 -0
- package/tst/mqtt-plus-4-sink.spec.ts +23 -0
- package/tst/mqtt-plus-5-source.spec.ts +26 -0
- package/tst/mqtt-plus-6-misc.spec.ts +58 -0
- package/tst/mqtt-plus-8-run.spec.ts +60 -0
- package/dst-stage1/mqtt-plus-api.js.map +0 -1
- package/dst-stage1/mqtt-plus-auth.js.map +0 -1
- package/dst-stage1/mqtt-plus-base.js.map +0 -1
- package/dst-stage1/mqtt-plus-codec.js.map +0 -1
- package/dst-stage1/mqtt-plus-encode.js.map +0 -1
- package/dst-stage1/mqtt-plus-error.js.map +0 -1
- package/dst-stage1/mqtt-plus-event.js.map +0 -1
- package/dst-stage1/mqtt-plus-info.js.map +0 -1
- package/dst-stage1/mqtt-plus-meta.js.map +0 -1
- package/dst-stage1/mqtt-plus-msg.js.map +0 -1
- package/dst-stage1/mqtt-plus-options.js.map +0 -1
- package/dst-stage1/mqtt-plus-service.js.map +0 -1
- package/dst-stage1/mqtt-plus-sink.js.map +0 -1
- package/dst-stage1/mqtt-plus-source.js.map +0 -1
- package/dst-stage1/mqtt-plus-subscription.js.map +0 -1
- package/dst-stage1/mqtt-plus-timer.js.map +0 -1
- package/dst-stage1/mqtt-plus-trace.js.map +0 -1
- package/dst-stage1/mqtt-plus-util.js.map +0 -1
- package/dst-stage1/mqtt-plus-version.js.map +0 -1
- package/dst-stage1/mqtt-plus.js.map +0 -1
- package/package.d/@typescript-eslint+typescript-estree+8.63.0.patch +0 -12
package/.ase/config.yaml
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
null
|
package/.ase/service.log
CHANGED
|
@@ -10433,3 +10433,37 @@ ase: [2026-07-11 12:45:57.471]: [INFO]: mcp: POST /mcp [initialize]
|
|
|
10433
10433
|
ase: [2026-07-11 12:45:57.487]: [INFO]: mcp: POST /mcp [tools/list]
|
|
10434
10434
|
ase: [2026-07-11 12:45:57.494]: [INFO]: mcp: POST /mcp [notifications/initialized]
|
|
10435
10435
|
ase: [2026-07-11 12:45:57.499]: [INFO]: mcp: GET /mcp
|
|
10436
|
+
ase: [2026-07-11 13:19:57.333]: [INFO]: service: idle timeout reached, stopping
|
|
10437
|
+
ase: [2026-08-04 08:46:25.126]: [INFO]: service: listening on port 42029
|
|
10438
|
+
ase: [2026-08-04 08:47:07.892]: [INFO]: mcp: POST /mcp [tools/call] ase_getopt {"name":"ase-code-lint","spec":"--help|-h --auto|-a --severity|-S=(LOW|MEDIUM|HIGH) --include|-i=(none|A01|A02|A03|A04|A05|A06|A07|A08|A09|A10|A11|A12|A13|A14|A15|A16|A17|A18|A19|A20)... --exclude|-e=…
|
|
10439
|
+
ase: [2026-08-04 08:47:20.549]: [INFO]: mcp: POST /mcp [tools/call] ase_getopt {"name":"ase-code-analyze","spec":"--help|-h --performance|-p --security|-s --severity|-S=(LOW|MEDIUM|HIGH) --prefix|-P=","args":"-S MEDIUM @src"}
|
|
10440
|
+
ase: [2026-08-04 08:49:58.001]: [INFO]: mcp: POST /mcp [tools/call] ase_config_set {"key":"agent.skill","val":"ase-docs-proofread","scope":"session:c4b8a1d3-1262-4f52-a20b-5e3caf67f11b"}
|
|
10441
|
+
ase: [2026-08-04 08:55:13.606]: [INFO]: mcp: POST /mcp [tools/call] ase_config_set {"key":"agent.skill","val":"ase-code-lint","scope":"session:991dfac7-3ec1-45df-b605-b85076f1133f"}
|
|
10442
|
+
ase: [2026-08-04 08:59:33.029]: [INFO]: mcp: POST /mcp [tools/call] ase_kv_batch {"transactional":true,"commands":[{"command":"clear","prefix":"ase-issue-"},{"command":"set","key":"ase-issue-P1","val":"Abort destroys the caller's un-consumed `data` stream with an error, causing an…
|
|
10443
|
+
ase: [2026-08-04 09:34:45.196]: [INFO]: mcp: POST /mcp [tools/call] ase_getopt {"name":"ase-docs-proofread","spec":"--help|-h --auto|-a","args":"-a @."}
|
|
10444
|
+
ase: [2026-08-04 09:36:55.088]: [INFO]: mcp: POST /mcp [tools/call] ase_config_set {"key":"agent.skill","val":"ase-docs-proofread","scope":"session:b97f5e3e-1ebd-424b-836f-c36be1120f90"}
|
|
10445
|
+
ase: [2026-08-04 09:37:42.361]: [INFO]: mcp: POST /mcp [tools/call] ase_config_delete {"key":"agent.skill","scope":"session:b97f5e3e-1ebd-424b-836f-c36be1120f90"}
|
|
10446
|
+
ase: [2026-08-06 16:50:29.391]: [INFO]: service: idle timeout reached, stopping
|
|
10447
|
+
ase: [2026-08-06 18:58:21.557]: [INFO]: service: listening on port 43270
|
|
10448
|
+
ase: [2026-08-06 18:58:38.339]: [INFO]: mcp: POST /mcp [tools/call] ase_getopt {"name":"ase-code-resolve","spec":"--help|-h --auto|-a --dry|-d --direct|-D --quick|-Q --next|-n=(none|DONE|EDIT|GRILL|PREFLIGHT|IMPLEMENT)...","args":"-D P1"}
|
|
10449
|
+
ase: [2026-08-06 18:58:56.412]: [INFO]: mcp: POST /mcp [tools/call] ase_kv_get {"key":"ase-issue-P1"}
|
|
10450
|
+
ase: [2026-08-06 18:59:39.600]: [INFO]: mcp: POST /mcp [tools/call] ase_getopt {"name":"ase-code-resolve","spec":"--help|-h --auto|-a --dry|-d --direct|-D --quick|-Q --next|-n=(none|DONE|EDIT|GRILL|PREFLIGHT|IMPLEMENT)...","args":"-D PROBLEM (Severity: HIGH): P1: Abort destroys …
|
|
10451
|
+
ase: [2026-08-06 18:59:58.741]: [INFO]: mcp: POST /mcp [tools/call] ase_task_id {"id":"stream-abort","session":"0e6fbbe6-4403-4b63-a1ec-9d993421b5a3"}
|
|
10452
|
+
ase: [2026-08-06 19:00:12.928]: [INFO]: mcp: POST /mcp [tools/call] ase_getopt {"name":"ase-code-resolve","spec":"--help|-h --auto|-a --dry|-d --direct|-D --quick|-Q --next|-n=(none|DONE|EDIT|GRILL|PREFLIGHT|IMPLEMENT)...","args":"-D PROBLEM (Severity: MEDIUM): P3: WithInfo appe…
|
|
10453
|
+
ase: [2026-08-06 19:00:13.609]: [INFO]: mcp: POST /mcp [tools/call] ase_getopt {"name":"ase-code-resolve","spec":"--help|-h --auto|-a --dry|-d --direct|-D --quick|-Q --next|-n=(none|DONE|EDIT|GRILL|PREFLIGHT|IMPLEMENT)...","args":"-D PROBLEM (Severity: MEDIUM): P2: Sync-throwing…
|
|
10454
|
+
ase: [2026-08-06 19:00:19.055]: [INFO]: mcp: POST /mcp [tools/call] ase_getopt {"name":"ase-code-resolve","spec":"--help|-h --auto|-a --dry|-d --direct|-D --quick|-Q --next|-n=(none|DONE|EDIT|GRILL|PREFLIGHT|IMPLEMENT)...","args":"-D PROBLEM (Severity: MEDIUM): P4: Abort destroy…
|
|
10455
|
+
ase: [2026-08-06 19:00:29.204]: [INFO]: mcp: POST /mcp [tools/call] ase_task_id {"id":"sync-throw","session":"6a89a55a-84c0-4a5f-9724-368cc7b50b3f"}
|
|
10456
|
+
ase: [2026-08-06 19:00:29.627]: [INFO]: mcp: POST /mcp [tools/call] ase_getopt {"name":"ase-code-resolve","spec":"--help|-h --auto|-a --dry|-d --direct|-D --quick|-Q --next|-n=(none|DONE|EDIT|GRILL|PREFLIGHT|IMPLEMENT)...","args":"-D PROBLEM (Severity: MEDIUM): P5: MIN_VERSION c…
|
|
10457
|
+
ase: [2026-08-06 19:00:30.442]: [INFO]: mcp: POST /mcp [tools/call] ase_task_id {"id":"info-positional","session":"8a3e2d56-af15-48dd-8026-b6d41ee9a33a"}
|
|
10458
|
+
ase: [2026-08-06 19:00:53.301]: [INFO]: mcp: POST /mcp [tools/call] ase_task_id {"id":"version-compat","session":"8369a65d-3b10-4294-8870-4327cc269b79"}
|
|
10459
|
+
ase: [2026-08-06 19:00:55.371]: [INFO]: mcp: POST /mcp [tools/call] ase_getopt {"name":"ase-code-resolve","spec":"--help|-h --auto|-a --dry|-d --direct|-D --quick|-Q --next|-n=(none|DONE|EDIT|GRILL|PREFLIGHT|IMPLEMENT)...","args":"-D PROBLEM (Severity: ACCEPTED): P6: Receiver mi…
|
|
10460
|
+
ase: [2026-08-06 19:00:56.036]: [INFO]: mcp: POST /mcp [tools/call] ase_task_id {"id":"stream-abort","session":"56995021-07c8-4b45-8916-4ab8a78d0a8b"}
|
|
10461
|
+
ase: [2026-08-06 19:01:07.667]: [INFO]: mcp: POST /mcp [tools/call] ase_task_id {"id":"receiver-mismatch","session":"9eb01878-9acf-43dd-9554-00066ecc7300"}
|
|
10462
|
+
ase: [2026-08-06 19:01:16.395]: [INFO]: mcp: POST /mcp [tools/call] ase_getopt {"name":"ase-code-resolve","spec":"--help|-h --auto|-a --dry|-d --direct|-D --quick|-Q --next|-n=(none|DONE|EDIT|GRILL|PREFLIGHT|IMPLEMENT)...","args":"-D PROBLEM (Severity: ACCEPTED): P7: runSettle()…
|
|
10463
|
+
ase: [2026-08-06 19:01:20.616]: [INFO]: mcp: POST /mcp [tools/call] ase_getopt {"name":"ase-code-resolve","spec":"--help|-h --auto|-a --dry|-d --direct|-D --quick|-Q --next|-n=(none|DONE|EDIT|GRILL|PREFLIGHT|IMPLEMENT)...","args":"-D PROBLEM (Severity: ACCEPTED): P8: Fire-and-fo…
|
|
10464
|
+
ase: [2026-08-06 19:01:34.810]: [INFO]: mcp: POST /mcp [tools/call] ase_getopt {"name":"ase-code-resolve","spec":"--help|-h --auto|-a --dry|-d --direct|-D --quick|-Q --next|-n=(none|DONE|EDIT|GRILL|PREFLIGHT|IMPLEMENT)...","args":"-D PROBLEM (Severity: ACCEPTED): P9: Enumerating…
|
|
10465
|
+
ase: [2026-08-06 19:01:34.828]: [INFO]: mcp: POST /mcp [tools/call] ase_task_id {"id":"spool-unroll","session":"3ae9d8dc-2ca4-400f-bd32-633859ec1f8a"}
|
|
10466
|
+
ase: [2026-08-06 19:01:36.333]: [INFO]: mcp: POST /mcp [tools/call] ase_task_id {"id":"onfinally-error","session":"e164c52c-d0df-4cc7-a9bc-972012577cec"}
|
|
10467
|
+
ase: [2026-08-06 19:01:47.858]: [INFO]: mcp: POST /mcp [tools/call] ase_task_id {"id":"exclusive-fields","session":"7bcafa3d-0cb1-4c2b-8fdb-17e7a84dc176"}
|
|
10468
|
+
ase: [2026-08-06 19:23:19.610]: [INFO]: mcp: POST /mcp [tools/call] ase_timestamp {"format":"yyyy-LL-dd"}
|
|
10469
|
+
ase: [2026-08-06 19:47:45.209]: [INFO]: mcp: POST /mcp [tools/call] ase_artifact_list {"kind":["spec","arch","code","docs","infr","othr"]}
|
package/.ase/service.yaml
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
port:
|
|
1
|
+
port: 43270
|
package/.ase/task/TASK-P18.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
- **WHY**: When publishing the push request fails (`src/mqtt-plus-sink.ts:795-796`), the `catch`
|
|
14
14
|
block aborts (`src/mqtt-plus-sink.ts:866`) and `onAbort` (`src/mqtt-plus-sink.ts:773-783`) calls
|
|
15
|
-
`pushInitialReject(error)` before `await pushInitial` (`src/mqtt-plus-sink.ts:797`) ever
|
|
15
|
+
`pushInitialReject(error)` before `await pushInitial` (`src/mqtt-plus-sink.ts:797`) ever attaches
|
|
16
16
|
a handler — an *unhandled promise rejection*, fatal by default in Node.js ≥ 15. The same hazard
|
|
17
17
|
exists for early rejections from the response/credit handlers while the publish is still pending.
|
|
18
18
|
|
package/.ase/task/TASK-P21.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
- **WHY**: `destroy()` cannot reach the fetcher-side closure state (`src/mqtt-plus-source.ts:457`),
|
|
13
13
|
so pending `fetch()` consumers hang forever on `stream`/`buffer`/`meta` (the timeout timer is
|
|
14
|
-
cleared and neutralized
|
|
14
|
+
cleared and neutralized and the `onResponse` handlers are dropped); aborting settles all three with
|
|
15
15
|
*"instance destroyed"*, analogous to the service-caller `pendingCalls` pattern
|
|
16
16
|
(`src/mqtt-plus-service.ts:51-54`).
|
|
17
17
|
|
package/AGENTS.md
CHANGED
|
@@ -50,7 +50,7 @@ Configuration lives in `etc/`: `tsc.json`, `vite.mts`, `eslint.mts`,
|
|
|
50
50
|
`knip.jsonc`, `stx.conf`, `d2.mts`, `d2.theme.d2`, `logo.ai`,
|
|
51
51
|
`logo.svg`.
|
|
52
52
|
|
|
53
|
-
Tests require an MQTT broker
|
|
53
|
+
Tests require an MQTT broker at run-time; the test suite starts/stops
|
|
54
54
|
one automatically. If Docker is available, a Mosquitto broker is used;
|
|
55
55
|
otherwise, the Aedes in-process broker serves as the fallback.
|
|
56
56
|
For regression testing always use the all-in-one command `npm start build test`.
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,30 @@
|
|
|
2
2
|
ChangeLog
|
|
3
3
|
=========
|
|
4
4
|
|
|
5
|
+
1.4.27 (2026-08-05)
|
|
6
|
+
-------------------
|
|
7
|
+
|
|
8
|
+
- BUGFIX [code]: expose the version information as static members of the MQTTp class instead of re-exporting the "mqtt-plus-version" module
|
|
9
|
+
|
|
10
|
+
1.4.26 (2026-08-06)
|
|
11
|
+
-------------------
|
|
12
|
+
|
|
13
|
+
- IMPROVEMENT [code]: sanity check the numeric option ranges of "timeout", "chunkSize" and "chunkCredit" at construction time and guard the chunk sending helpers against an invalid chunk size
|
|
14
|
+
- IMPROVEMENT [infr]: upgrade the NPM patch generation procedure to use "npm install --package-lock-only" instead of "npm shrinkwrap"
|
|
15
|
+
- BUGFIX [code, othr]: guard the handler-supplied source fetch stream and the caller-supplied sink push stream with a no-op "error" listener before destroying them on abort, as an abort before the chunk sending phase (field validation, nak, early cancel, ack timeout) otherwise raised an unhandled "error" event and crashed the process
|
|
16
|
+
- BUGFIX [code, docs]: drop instead of just warn about a message received on a direct topic whose "receiver" field does not match the peer id of the topic, as such a mislabeled message is addressed to a foreign peer and was still dispatched to the response handlers
|
|
17
|
+
- BUGFIX [code, othr]: settle the epilog of run() adaptively when the action throws synchronously, as an asynchronous onfinally, oncleanup or spool cleanup was previously mistaken for a non-async context and replaced the original error
|
|
18
|
+
- BUGFIX [code, othr]: attach a failing onfinally callback of run() as a "suppressed" error to the propagated pending error instead of discarding it entirely
|
|
19
|
+
- BUGFIX [code, othr]: align received parameters to the handler arity in the event/service/source/sink traits, as an endpoint with trailing optional parameters otherwise shifted the trailing "info" argument into a data parameter slot
|
|
20
|
+
- BUGFIX [code, othr]: define the mutually exclusive "stream"/"buffer" fields as non-enumerable and assignable, as a mere object spread, Object.values() or JSON.stringify() otherwise consumed one field and then threw on the other
|
|
21
|
+
- BUGFIX [code]: surface the cleanup failures of the fire-and-forget spool unrolling in the service call settle path as "error" events, as they were previously silently suppressed
|
|
22
|
+
- BUGFIX [code]: re-export the "mqtt-plus-version" module with "export *" instead of "export type *", as its version utility is a run-time value and was stripped from the public API
|
|
23
|
+
- BUGFIX [code]: settle the sink push race window with setTimeout() instead of setImmediate(), as setImmediate() is unavailable in browser environments
|
|
24
|
+
- UPDATE [infr, othr]: upgrade NPM dependencies
|
|
25
|
+
- CLEANUP [code]: prefix the private members of BaseTrait with an underscore
|
|
26
|
+
- CLEANUP [docs, infr, othr]: fix spelling in the documentation
|
|
27
|
+
- REFACTOR [code]: factor out the repeated push abort and settle logic in the sink trait into a "failPush" helper
|
|
28
|
+
|
|
5
29
|
1.4.25 (2026-07-11)
|
|
6
30
|
-------------------
|
|
7
31
|
|
|
@@ -79,12 +103,12 @@ ChangeLog
|
|
|
79
103
|
- IMPROVEMENT: bump minimum Node version to 20 for ES2022
|
|
80
104
|
- BUGFIX: Spool.unroll() silently skipped remaining cleanups on first async failure
|
|
81
105
|
- BUGFIX: improve semantics of info.authenticated field for event/service/sink/source in case of optional authentication
|
|
82
|
-
- BUGFIX: in the ReadableTee class, do not run read() twice: once
|
|
106
|
+
- BUGFIX: in the ReadableTee class, do not run read() twice: once ourselves and once via the base class
|
|
83
107
|
- BUGFIX: correctly propagate description in run() also to finally callback
|
|
84
108
|
- BUGFIX: fix resource handling in source trait
|
|
85
109
|
- BUGFIX: avoid race conditions and unhandled promise rejections in async processing
|
|
86
110
|
- BUGFIX: fix cleanup and error handling across sink/source traits
|
|
87
|
-
- BUGFIX: fix
|
|
111
|
+
- BUGFIX: fix Mosquitto ACL
|
|
88
112
|
- UPDATE: upgrade NPM dependencies
|
|
89
113
|
- CLEANUP: various code cleanups (callback handling, settle code, destroy handling, termination, subscriptions)
|
|
90
114
|
- CLEANUP: align with ensureError code and fix typos
|
|
@@ -286,14 +310,14 @@ ChangeLog
|
|
|
286
310
|
- CLEANUP: simplify typing and remove unused code
|
|
287
311
|
- CLEANUP: rename variables and align code formatting
|
|
288
312
|
- CLEANUP: various code cleanups
|
|
289
|
-
- CLEANUP:
|
|
313
|
+
- CLEANUP: split documentation from README.md into doc/*.md
|
|
290
314
|
- UPDATE: upgrade NPM dependencies
|
|
291
315
|
|
|
292
316
|
1.3.0 (2026-02-07)
|
|
293
317
|
------------------
|
|
294
318
|
|
|
295
319
|
- IMPROVEMENT: add credit-based flow control to sink/source facility
|
|
296
|
-
- IMPROVEMENT: make "buffer" and "stream" fields always
|
|
320
|
+
- IMPROVEMENT: make "buffer" and "stream" fields always mutually exclusive
|
|
297
321
|
- IMPROVEMENT: provide version field in protocol messages
|
|
298
322
|
|
|
299
323
|
1.2.1 (2026-02-07)
|
|
@@ -368,7 +392,7 @@ ChangeLog
|
|
|
368
392
|
0.9.17 (2026-01-25)
|
|
369
393
|
-------------------
|
|
370
394
|
|
|
371
|
-
- IMPROVEMENT: add Dry-Run mode
|
|
395
|
+
- IMPROVEMENT: add Dry-Run mode with null MQTT client
|
|
372
396
|
- IMPROVEMENT: add Dry-Run mode for emit() to generate last-will message
|
|
373
397
|
|
|
374
398
|
0.9.16 (2026-01-24)
|
package/README.md
CHANGED
|
@@ -55,7 +55,7 @@ The following is a simple but self-contained example usage of
|
|
|
55
55
|
and an MQTT infrastructure. It can be found in the file
|
|
56
56
|
[sample.ts](sample/sample.ts) and can be executed from the **MQTT+**
|
|
57
57
|
source tree via `npm start sample` (assuming the prerequisite *Docker* is
|
|
58
|
-
available for the underlying *Mosquitto* broker
|
|
58
|
+
available for the underlying *Mosquitto* broker-based infrastructure):
|
|
59
59
|
|
|
60
60
|
```ts
|
|
61
61
|
import { Readable } from "node:stream"
|
|
@@ -175,7 +175,7 @@ Notice
|
|
|
175
175
|
------
|
|
176
176
|
|
|
177
177
|
> [!Note]
|
|
178
|
-
> **MQTT+** and its peer dependency **MQTT.js** provide
|
|
178
|
+
> **MQTT+** and its peer dependency **MQTT.js** provide powerful
|
|
179
179
|
> functionality, but are not small in size. **MQTT+** is 4,000 LoC
|
|
180
180
|
> and 75 KB in size (ESM and CJS format). When bundled with all its
|
|
181
181
|
> dependencies, it is 220 KB in size (UMD format). Its peer dependency
|
|
@@ -5,7 +5,7 @@ MQTT+ Broker Setup
|
|
|
5
5
|
**MQTT+** can be used with an arbitrary MQTT broker. One popular
|
|
6
6
|
MQTT broker is [Mosquitto](https://mosquitto.org/).
|
|
7
7
|
For establishing your own permanent MQTT environment, you can install the
|
|
8
|
-
[Mosquitto](https://mosquitto.org/) MQTT broker yourself and
|
|
8
|
+
[Mosquitto](https://mosquitto.org/) MQTT broker yourself and set up
|
|
9
9
|
a `mosquitto.conf` file like...
|
|
10
10
|
|
|
11
11
|
```
|
package/doc/mqtt-plus-comm.md
CHANGED
|
@@ -13,7 +13,7 @@ case of a directed event emission) or *all* receivers are called and
|
|
|
13
13
|
receive the parameters as extra information.
|
|
14
14
|
|
|
15
15
|
> In contrast to the regular MQTT message publish/subscribe, this
|
|
16
|
-
> pattern allows
|
|
16
|
+
> pattern allows directing the event to particular receivers,
|
|
17
17
|
> provides optional information about the sender and receiver to
|
|
18
18
|
> receivers, supports authentication and meta-data, etc.
|
|
19
19
|
|
|
@@ -48,7 +48,7 @@ sink push) or *one* arbitrary sink is called and receives the data
|
|
|
48
48
|
chunks as a stream with arguments.
|
|
49
49
|
|
|
50
50
|
> In contrast to the regular MQTT message publish/subscribe, this
|
|
51
|
-
> pattern allows
|
|
51
|
+
> pattern allows transferring arbitrary amounts of arbitrary data by
|
|
52
52
|
> chunking the data via a stream. Additionally, it supports authentication
|
|
53
53
|
> and meta-data, provides an `AbortSignal` to the sink handler for
|
|
54
54
|
> cooperative cancellation, and allows the receiver to cancel an
|
|
@@ -67,7 +67,7 @@ directed source fetch) or *one* arbitrary source is called and sends the
|
|
|
67
67
|
data chunks as a stream with arguments.
|
|
68
68
|
|
|
69
69
|
> In contrast to the regular MQTT message publish/subscribe, this
|
|
70
|
-
> pattern allows
|
|
70
|
+
> pattern allows transferring arbitrary amounts of arbitrary data by
|
|
71
71
|
> chunking the data via a stream. Additionally, it supports
|
|
72
72
|
> authentication and meta-data, and provides an `AbortSignal` to the
|
|
73
73
|
> source handler for cooperative cancellation, etc.
|
|
@@ -328,6 +328,12 @@ the configured credential and required roles:
|
|
|
328
328
|
Message Dispatching
|
|
329
329
|
-------------------
|
|
330
330
|
|
|
331
|
+
### Receiver Validation
|
|
332
|
+
|
|
333
|
+
A message received on a direct topic (i.e. a topic carrying a concrete
|
|
334
|
+
peer id instead of `any`) is dropped if its `receiver` field does not
|
|
335
|
+
match that peer id, as it is addressed to a foreign peer.
|
|
336
|
+
|
|
331
337
|
### Request Dispatching
|
|
332
338
|
|
|
333
339
|
Incoming request messages are dispatched based on the combination of
|
|
@@ -4,8 +4,8 @@ import type { APIOptions } from "./mqtt-plus-options";
|
|
|
4
4
|
import { TraceTrait } from "./mqtt-plus-trace";
|
|
5
5
|
import { Spool } from "./mqtt-plus-error";
|
|
6
6
|
export declare class BaseTrait<T extends APISchema = APISchema> extends TraceTrait<T> {
|
|
7
|
-
private
|
|
8
|
-
private
|
|
7
|
+
private _mqtt;
|
|
8
|
+
private _messageHandler;
|
|
9
9
|
protected destroyed: boolean;
|
|
10
10
|
protected onRequest: Map<string, (message: any, topicName: string) => void | Promise<void>>;
|
|
11
11
|
protected onResponse: Map<string, (message: any, topicName: string) => void | Promise<void>>;
|
|
@@ -55,7 +55,7 @@ export class BaseTrait extends TraceTrait {
|
|
|
55
55
|
});
|
|
56
56
|
}
|
|
57
57
|
/* store MQTT client */
|
|
58
|
-
this.
|
|
58
|
+
this._mqtt = mqtt;
|
|
59
59
|
/* resolve the instance "id": if the user did not provide an
|
|
60
60
|
explicit one, fetch the "clientId" from the underlying MQTT
|
|
61
61
|
client, and only as a last resort fall back to a generated id
|
|
@@ -70,7 +70,7 @@ export class BaseTrait extends TraceTrait {
|
|
|
70
70
|
}
|
|
71
71
|
/* hook into the MQTT message processing */
|
|
72
72
|
this.log("info", "hooking into MQTT client");
|
|
73
|
-
this.
|
|
73
|
+
this._messageHandler = (topic, message, packet) => {
|
|
74
74
|
if (this.destroyed)
|
|
75
75
|
return;
|
|
76
76
|
/* convert message to codec-specific input format
|
|
@@ -90,13 +90,13 @@ export class BaseTrait extends TraceTrait {
|
|
|
90
90
|
}
|
|
91
91
|
this._onMessage(topic, input, packet);
|
|
92
92
|
};
|
|
93
|
-
this.
|
|
93
|
+
this._mqtt.on("message", this._messageHandler);
|
|
94
94
|
}
|
|
95
95
|
/* destroy API class */
|
|
96
96
|
async destroy() {
|
|
97
97
|
this.destroyed = true;
|
|
98
98
|
this.log("info", "un-hooking from MQTT client");
|
|
99
|
-
this.
|
|
99
|
+
this._mqtt.off("message", this._messageHandler);
|
|
100
100
|
this.onRequest.clear();
|
|
101
101
|
this.onResponse.clear();
|
|
102
102
|
}
|
|
@@ -120,7 +120,7 @@ export class BaseTrait extends TraceTrait {
|
|
|
120
120
|
async subscribeTopic(topic, options = {}) {
|
|
121
121
|
this.log("info", `subscribing to MQTT topic "${topic}"`);
|
|
122
122
|
return new Promise((resolve, reject) => {
|
|
123
|
-
this.
|
|
123
|
+
this._mqtt.subscribe(topic, { qos: 2, ...options }, (err, granted) => {
|
|
124
124
|
if (err) {
|
|
125
125
|
this.error(err, `subscribing to MQTT topic "${topic}" failed`);
|
|
126
126
|
reject(err);
|
|
@@ -142,7 +142,7 @@ export class BaseTrait extends TraceTrait {
|
|
|
142
142
|
async unsubscribeTopic(topic) {
|
|
143
143
|
this.log("info", `unsubscribing from MQTT topic "${topic}"`);
|
|
144
144
|
return new Promise((resolve, reject) => {
|
|
145
|
-
this.
|
|
145
|
+
this._mqtt.unsubscribe(topic, (err, _packet) => {
|
|
146
146
|
if (err) {
|
|
147
147
|
this.error(err, `unsubscribing from MQTT topic "${topic}" failed`);
|
|
148
148
|
reject(err);
|
|
@@ -178,7 +178,7 @@ export class BaseTrait extends TraceTrait {
|
|
|
178
178
|
const messageData = typeof message === "string"
|
|
179
179
|
? message
|
|
180
180
|
: Buffer.from(message.buffer, message.byteOffset, message.byteLength);
|
|
181
|
-
this.
|
|
181
|
+
this._mqtt.publish(topic, messageData, options, (err) => {
|
|
182
182
|
if (err) {
|
|
183
183
|
this.error(err, `publishing to MQTT topic "${topic}" failed`);
|
|
184
184
|
reject(err);
|
|
@@ -226,12 +226,15 @@ export class BaseTrait extends TraceTrait {
|
|
|
226
226
|
return;
|
|
227
227
|
}
|
|
228
228
|
this.log("debug", `received from MQTT topic "${topic}"`, { message });
|
|
229
|
-
/*
|
|
229
|
+
/* drop message on receiver mismatch on direct topics, as it is
|
|
230
|
+
addressed to a foreign peer and hence must not be processed here */
|
|
230
231
|
if (topicMatch.peerId !== undefined
|
|
231
232
|
&& message.receiver !== undefined
|
|
232
|
-
&& message.receiver !== topicMatch.peerId)
|
|
233
|
-
this.log("warning", `receiver mismatch
|
|
233
|
+
&& message.receiver !== topicMatch.peerId) {
|
|
234
|
+
this.log("warning", `receiver mismatch -- dropped message from direct topic "${topic}"` +
|
|
234
235
|
` (expected "${topicMatch.peerId}", got "${message.receiver}")`);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
235
238
|
/* dispatch MQTT+ message */
|
|
236
239
|
if (this.msg.isRequest(message)) {
|
|
237
240
|
/* dispatch request message */
|
|
@@ -188,10 +188,14 @@ function runSettle(isAsync, pending, spool, onfinally, description) {
|
|
|
188
188
|
catch (arg) {
|
|
189
189
|
failure = ensureError(arg, description);
|
|
190
190
|
}
|
|
191
|
-
if (pending || failure !== undefined) {
|
|
191
|
+
if (pending !== undefined || failure !== undefined) {
|
|
192
192
|
runUnroll(false, spool);
|
|
193
|
-
if (
|
|
194
|
-
|
|
193
|
+
if (failure !== undefined) {
|
|
194
|
+
if (pending !== undefined)
|
|
195
|
+
pending.suppressed = failure;
|
|
196
|
+
else
|
|
197
|
+
throw failure;
|
|
198
|
+
}
|
|
195
199
|
}
|
|
196
200
|
}
|
|
197
201
|
else {
|
|
@@ -203,14 +207,39 @@ function runSettle(isAsync, pending, spool, onfinally, description) {
|
|
|
203
207
|
catch (arg) {
|
|
204
208
|
failure = ensureError(arg, description);
|
|
205
209
|
}
|
|
206
|
-
if (pending || failure !== undefined) {
|
|
210
|
+
if (pending !== undefined || failure !== undefined) {
|
|
207
211
|
await runUnroll(true, spool);
|
|
208
|
-
if (
|
|
209
|
-
|
|
212
|
+
if (failure !== undefined) {
|
|
213
|
+
if (pending !== undefined)
|
|
214
|
+
pending.suppressed = failure;
|
|
215
|
+
else
|
|
216
|
+
throw failure;
|
|
217
|
+
}
|
|
210
218
|
}
|
|
211
219
|
})();
|
|
212
220
|
}
|
|
213
221
|
}
|
|
222
|
+
/* helper function for settling the epilog of "run" and then failing with
|
|
223
|
+
a pending error in a still unknown synchronous/asynchronous context:
|
|
224
|
+
operate synchronously as long as possible and continue asynchronously as
|
|
225
|
+
soon as a callback returns a Promise. As the pending error always takes
|
|
226
|
+
precedence, a failing finally code is attached to it as a suppressed error. */
|
|
227
|
+
function runFail(error, spool, onfinally, description) {
|
|
228
|
+
const attach = (arg) => { error.suppressed = ensureError(arg, description); };
|
|
229
|
+
let result = undefined;
|
|
230
|
+
try {
|
|
231
|
+
result = onfinally?.();
|
|
232
|
+
}
|
|
233
|
+
catch (arg) {
|
|
234
|
+
attach(arg);
|
|
235
|
+
}
|
|
236
|
+
if (result instanceof Promise)
|
|
237
|
+
return result.catch(attach).then(() => spool?.unroll()).then(() => { throw error; });
|
|
238
|
+
const unrolled = spool?.unroll();
|
|
239
|
+
if (unrolled instanceof Promise)
|
|
240
|
+
return unrolled.then(() => { throw error; });
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
214
243
|
export function run(...args) {
|
|
215
244
|
/* support overloaded signatures */
|
|
216
245
|
let description;
|
|
@@ -267,31 +296,28 @@ export function run(...args) {
|
|
|
267
296
|
result = action();
|
|
268
297
|
}
|
|
269
298
|
catch (arg) {
|
|
270
|
-
/* synchronous case (error branch)
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
if (spool && oncleanup)
|
|
282
|
-
spool.roll(result, oncleanup);
|
|
283
|
-
runSettle(false, false, spool, onfinally, description);
|
|
284
|
-
return result;
|
|
299
|
+
/* synchronous case (error branch): as the action failed before
|
|
300
|
+
returning its potentially asynchronous result, the context is
|
|
301
|
+
still unknown here and hence has to be probed */
|
|
302
|
+
const error = ensureError(arg, description);
|
|
303
|
+
if (!oncatch)
|
|
304
|
+
return runFail(error, spool, onfinally, description);
|
|
305
|
+
try {
|
|
306
|
+
result = oncatch(error);
|
|
307
|
+
}
|
|
308
|
+
catch (arg) {
|
|
309
|
+
return runFail(ensureError(arg, description), spool, onfinally, description);
|
|
285
310
|
}
|
|
286
|
-
|
|
287
|
-
|
|
311
|
+
/* the recovery already ran, so let the common epilog below settle
|
|
312
|
+
its potentially asynchronous result without re-entering it */
|
|
313
|
+
oncatch = undefined;
|
|
288
314
|
}
|
|
289
315
|
if (result instanceof Promise) {
|
|
290
316
|
/* asynchronous case (result or error branch) */
|
|
291
317
|
return result.then(async (result) => {
|
|
292
318
|
if (spool && oncleanup)
|
|
293
319
|
spool.roll(result, oncleanup);
|
|
294
|
-
await runSettle(true,
|
|
320
|
+
await runSettle(true, undefined, spool, onfinally, description);
|
|
295
321
|
return result;
|
|
296
322
|
}, async (arg) => {
|
|
297
323
|
/* asynchronous case (error branch) */
|
|
@@ -303,15 +329,15 @@ export function run(...args) {
|
|
|
303
329
|
}
|
|
304
330
|
catch (arg) {
|
|
305
331
|
error = ensureError(arg, description);
|
|
306
|
-
await runSettle(true,
|
|
332
|
+
await runSettle(true, error, spool, onfinally, description);
|
|
307
333
|
throw error;
|
|
308
334
|
}
|
|
309
335
|
if (spool && oncleanup)
|
|
310
336
|
spool.roll(result, oncleanup);
|
|
311
|
-
await runSettle(true,
|
|
337
|
+
await runSettle(true, undefined, spool, onfinally, description);
|
|
312
338
|
return result;
|
|
313
339
|
}
|
|
314
|
-
await runSettle(true,
|
|
340
|
+
await runSettle(true, error, spool, onfinally, description);
|
|
315
341
|
throw error;
|
|
316
342
|
});
|
|
317
343
|
}
|
|
@@ -319,7 +345,7 @@ export function run(...args) {
|
|
|
319
345
|
/* synchronous case (result branch) */
|
|
320
346
|
if (spool && oncleanup)
|
|
321
347
|
spool.roll(result, oncleanup);
|
|
322
|
-
runSettle(false,
|
|
348
|
+
runSettle(false, undefined, spool, onfinally, description);
|
|
323
349
|
return result;
|
|
324
350
|
}
|
|
325
351
|
}
|
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
** SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
23
23
|
*/
|
|
24
24
|
import { nanoid } from "nanoid";
|
|
25
|
+
/* internal requirements */
|
|
26
|
+
import { alignHandlerParams } from "./mqtt-plus-util";
|
|
25
27
|
import { AuthTrait } from "./mqtt-plus-auth";
|
|
26
28
|
import { Spool, ensureError, run } from "./mqtt-plus-error";
|
|
27
29
|
/* Event Emission Trait */
|
|
@@ -100,7 +102,7 @@ export class EventTrait extends AuthTrait {
|
|
|
100
102
|
` (topic: "${topicName}", payload: "${request.name}")`, { requestId });
|
|
101
103
|
return;
|
|
102
104
|
}
|
|
103
|
-
const params = request.params ?? [];
|
|
105
|
+
const params = alignHandlerParams(request.params ?? [], callback);
|
|
104
106
|
/* sanity check request id */
|
|
105
107
|
if (this.eventControllers.has(requestId)) {
|
|
106
108
|
this.log("warning", `duplicate event request id -- dropped request for event "${name}"`, { requestId });
|
|
@@ -46,5 +46,12 @@ export class OptionsTrait {
|
|
|
46
46
|
},
|
|
47
47
|
...options
|
|
48
48
|
};
|
|
49
|
+
/* sanity check numeric option ranges */
|
|
50
|
+
if (!(Number.isFinite(this.options.timeout) && this.options.timeout > 0))
|
|
51
|
+
throw new Error("options: \"timeout\" must be a positive number");
|
|
52
|
+
if (!(Number.isInteger(this.options.chunkSize) && this.options.chunkSize > 0))
|
|
53
|
+
throw new Error("options: \"chunkSize\" must be a positive integer");
|
|
54
|
+
if (!(Number.isInteger(this.options.chunkCredit) && this.options.chunkCredit >= 0))
|
|
55
|
+
throw new Error("options: \"chunkCredit\" must be a non-negative integer");
|
|
49
56
|
}
|
|
50
57
|
}
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import { nanoid } from "nanoid";
|
|
25
25
|
/* internal requirements */
|
|
26
|
+
import { alignHandlerParams } from "./mqtt-plus-util";
|
|
26
27
|
import { run, Spool, ensureError } from "./mqtt-plus-error";
|
|
27
28
|
import { EventTrait } from "./mqtt-plus-event";
|
|
28
29
|
/* Service Call Trait */
|
|
@@ -96,7 +97,7 @@ export class ServiceTrait extends EventTrait {
|
|
|
96
97
|
/* determine request information */
|
|
97
98
|
const requestId = request.id;
|
|
98
99
|
const senderId = request.sender;
|
|
99
|
-
const params = request.params ?? [];
|
|
100
|
+
const params = alignHandlerParams(request.params ?? [], callback);
|
|
100
101
|
if (senderId === undefined || senderId === "") {
|
|
101
102
|
this.error(new Error("invalid request: missing sender"));
|
|
102
103
|
return;
|
|
@@ -237,11 +238,13 @@ export class ServiceTrait extends EventTrait {
|
|
|
237
238
|
if (settled)
|
|
238
239
|
return false;
|
|
239
240
|
settled = true;
|
|
240
|
-
/* unroll fire-and-forget: the "settled" boolean guard
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
spool.unroll()
|
|
241
|
+
/* unroll fire-and-forget: the "settled" boolean guard protects
|
|
242
|
+
against concurrent/duplicate responses and the async MQTT
|
|
243
|
+
unsubscription is handled by reference-counting/linger, while
|
|
244
|
+
cleanup failures are surfaced as "error" events */
|
|
245
|
+
spool.unroll(false)?.catch((err) => {
|
|
246
|
+
this.error(ensureError(err, `cleanup after call of service "${name}" failed`));
|
|
247
|
+
});
|
|
245
248
|
return true;
|
|
246
249
|
};
|
|
247
250
|
const promise = new Promise((resolve, reject) => {
|