mqtt-plus 1.4.25 → 1.4.26
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 +24 -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 +1 -1
- package/dst-stage1/mqtt-plus.js +2 -0
- package/dst-stage2/mqtt-plus.cjs.cjs +164 -123
- package/dst-stage2/mqtt-plus.esm.js +154 -122
- package/dst-stage2/mqtt-plus.umd.js +11 -11
- package/etc/stx.conf +3 -2
- 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 +3 -1
- 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/etc/stx.conf
CHANGED
|
@@ -24,10 +24,11 @@
|
|
|
24
24
|
|
|
25
25
|
# make patches
|
|
26
26
|
patch-make
|
|
27
|
-
npm
|
|
27
|
+
npm install --package-lock-only --save && \
|
|
28
|
+
cross-env npm_config_allow_remote=all \
|
|
28
29
|
patch-package --patch-dir package.d \
|
|
29
30
|
"vite" "@typescript-eslint/typescript-estree" && \
|
|
30
|
-
shx rm -f
|
|
31
|
+
shx rm -f package-lock.json
|
|
31
32
|
|
|
32
33
|
# (internal) apply patches
|
|
33
34
|
patch-apply
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
diff --git a/node_modules/@typescript-eslint/typescript-estree/dist/parseSettings/warnAboutTSVersion.js b/node_modules/@typescript-eslint/typescript-estree/dist/parseSettings/warnAboutTSVersion.js
|
|
2
|
+
index 0961c74..b76d468 100644
|
|
3
|
+
--- a/node_modules/@typescript-eslint/typescript-estree/dist/parseSettings/warnAboutTSVersion.js
|
|
4
|
+
+++ b/node_modules/@typescript-eslint/typescript-estree/dist/parseSettings/warnAboutTSVersion.js
|
|
5
|
+
@@ -84,7 +84,6 @@ function handleUnsupportedTSVersion(parseSettings, behavior, passedLoggerFn) {
|
|
6
|
+
// See https://github.com/typescript-eslint/typescript-eslint/issues/7896
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
8
|
+
(typeof process === 'undefined' ? false : process.stdout?.isTTY)) {
|
|
9
|
+
- parseSettings.log(buildUnsupportedTSVersionMessage('warn'));
|
|
10
|
+
}
|
|
11
|
+
warnedAboutTSVersion = true;
|
|
12
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
diff --git a/node_modules/vite/dist/node/chunks/node.js b/node_modules/vite/dist/node/chunks/node.js
|
|
2
|
-
index
|
|
2
|
+
index 6aae7ee..0d52252 100644
|
|
3
3
|
--- a/node_modules/vite/dist/node/chunks/node.js
|
|
4
4
|
+++ b/node_modules/vite/dist/node/chunks/node.js
|
|
5
|
-
@@ -
|
|
5
|
+
@@ -37094,7 +37094,6 @@ async function runConfigHook(config, plugins, configEnv) {
|
|
6
6
|
const res = await getHookHandler(hook).call(context, conf, configEnv);
|
|
7
7
|
if (res && res !== conf) {
|
|
8
8
|
if (hasBothRollupOptionsAndRolldownOptions(res)) context.warn(`Both \`rollupOptions\` and \`rolldownOptions\` were specified by ${JSON.stringify(p.name)} plugin. \`rollupOptions\` specified by that plugin will be ignored.`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mqtt-plus",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.26",
|
|
4
4
|
"description": "MQTT Communication Patterns",
|
|
5
5
|
"keywords": [ "mqtt",
|
|
6
6
|
"event", "emit",
|
|
@@ -33,37 +33,37 @@
|
|
|
33
33
|
"patch-package": "8.0.1",
|
|
34
34
|
"@rse/stx": "1.1.6",
|
|
35
35
|
"shx": "0.4.0",
|
|
36
|
-
"eslint": "10.
|
|
37
|
-
"globals": "17.
|
|
36
|
+
"eslint": "10.8.0",
|
|
37
|
+
"globals": "17.9.0",
|
|
38
38
|
"@eslint/js": "10.0.1",
|
|
39
39
|
"@eslint/eslintrc": "3.3.6",
|
|
40
|
-
"knip": "6.
|
|
40
|
+
"knip": "6.32.0",
|
|
41
41
|
"jiti": "2.7.0",
|
|
42
|
-
"tsx": "4.23.
|
|
43
|
-
"mocha": "11.
|
|
42
|
+
"tsx": "4.23.9",
|
|
43
|
+
"mocha": "11.8.0",
|
|
44
44
|
"chai": "6.2.2",
|
|
45
45
|
"sinon-chai": "4.0.1",
|
|
46
|
-
"sinon": "22.
|
|
47
|
-
"c8": "
|
|
46
|
+
"sinon": "22.1.0",
|
|
47
|
+
"c8": "12.0.0",
|
|
48
48
|
"source-map-support": "0.5.21",
|
|
49
49
|
"typescript": "6.0.3",
|
|
50
|
-
"typescript-eslint": "8.
|
|
51
|
-
"@typescript-eslint/parser": "8.
|
|
50
|
+
"typescript-eslint": "8.66.0",
|
|
51
|
+
"@typescript-eslint/parser": "8.66.0",
|
|
52
52
|
"mqtt": "5.15.2",
|
|
53
53
|
"aedes": "1.1.1",
|
|
54
54
|
"mosquitto": "1.1.0",
|
|
55
55
|
"textframe": "1.2.1",
|
|
56
|
-
"vite": "8.1
|
|
56
|
+
"vite": "8.2.1",
|
|
57
57
|
"vite-plugin-node-polyfills": "0.28.0",
|
|
58
58
|
"@rollup/plugin-replace": "6.0.3",
|
|
59
59
|
"@wroud/vite-plugin-tsc": "0.12.3",
|
|
60
60
|
"@terrastruct/d2": "0.1.33",
|
|
61
61
|
"chokidar-cli": "3.0.0",
|
|
62
|
-
"chalk": "
|
|
62
|
+
"chalk": "6.0.0",
|
|
63
63
|
"open-cli": "9.0.0",
|
|
64
64
|
"cross-env": "10.1.0",
|
|
65
65
|
|
|
66
|
-
"@types/node": "26.1.
|
|
66
|
+
"@types/node": "26.1.2",
|
|
67
67
|
"@types/mocha": "10.0.10",
|
|
68
68
|
"@types/chai": "5.2.3",
|
|
69
69
|
"@types/sinon-chai": "4.0.0",
|
|
@@ -76,14 +76,15 @@
|
|
|
76
76
|
"vite-plugin-node-polyfills": { "vite": ">=7.0.0" }
|
|
77
77
|
},
|
|
78
78
|
"allowScripts": {
|
|
79
|
+
"fsevents": true,
|
|
79
80
|
"esbuild": true
|
|
80
81
|
},
|
|
81
82
|
"dependencies": {
|
|
82
|
-
"nanoid": "
|
|
83
|
+
"nanoid": "6.0.1",
|
|
83
84
|
"cbor2": "2.3.0",
|
|
84
85
|
"p-lazy": "5.0.0",
|
|
85
86
|
"valibot": "1.4.2",
|
|
86
|
-
"jose": "6.2.
|
|
87
|
+
"jose": "6.2.8",
|
|
87
88
|
"@stablelib/pbkdf2": "2.0.1",
|
|
88
89
|
"@stablelib/sha256": "2.0.1"
|
|
89
90
|
},
|
package/src/mqtt-plus-base.ts
CHANGED
|
@@ -40,8 +40,8 @@ import { PLazy } from "./mqtt-plus-util"
|
|
|
40
40
|
|
|
41
41
|
/* MQTTp Base class with shared infrastructure */
|
|
42
42
|
export class BaseTrait<T extends APISchema = APISchema> extends TraceTrait<T> {
|
|
43
|
-
private
|
|
44
|
-
private
|
|
43
|
+
private _mqtt: MqttClient
|
|
44
|
+
private _messageHandler: OnMessageCallback
|
|
45
45
|
|
|
46
46
|
/* lifecycle state */
|
|
47
47
|
protected destroyed = false
|
|
@@ -78,7 +78,7 @@ export class BaseTrait<T extends APISchema = APISchema> extends TraceTrait<T> {
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
/* store MQTT client */
|
|
81
|
-
this.
|
|
81
|
+
this._mqtt = mqtt
|
|
82
82
|
|
|
83
83
|
/* resolve the instance "id": if the user did not provide an
|
|
84
84
|
explicit one, fetch the "clientId" from the underlying MQTT
|
|
@@ -95,7 +95,7 @@ export class BaseTrait<T extends APISchema = APISchema> extends TraceTrait<T> {
|
|
|
95
95
|
|
|
96
96
|
/* hook into the MQTT message processing */
|
|
97
97
|
this.log("info", "hooking into MQTT client")
|
|
98
|
-
this.
|
|
98
|
+
this._messageHandler = (topic, message, packet) => {
|
|
99
99
|
if (this.destroyed)
|
|
100
100
|
return
|
|
101
101
|
|
|
@@ -116,14 +116,14 @@ export class BaseTrait<T extends APISchema = APISchema> extends TraceTrait<T> {
|
|
|
116
116
|
}
|
|
117
117
|
this._onMessage(topic, input, packet)
|
|
118
118
|
}
|
|
119
|
-
this.
|
|
119
|
+
this._mqtt.on("message", this._messageHandler)
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
/* destroy API class */
|
|
123
123
|
async destroy () {
|
|
124
124
|
this.destroyed = true
|
|
125
125
|
this.log("info", "un-hooking from MQTT client")
|
|
126
|
-
this.
|
|
126
|
+
this._mqtt.off("message", this._messageHandler)
|
|
127
127
|
this.onRequest.clear()
|
|
128
128
|
this.onResponse.clear()
|
|
129
129
|
}
|
|
@@ -149,7 +149,7 @@ export class BaseTrait<T extends APISchema = APISchema> extends TraceTrait<T> {
|
|
|
149
149
|
protected async subscribeTopic (topic: string, options: Partial<IClientSubscribeOptions> = {}): Promise<void> {
|
|
150
150
|
this.log("info", `subscribing to MQTT topic "${topic}"`)
|
|
151
151
|
return new Promise<void>((resolve, reject) => {
|
|
152
|
-
this.
|
|
152
|
+
this._mqtt.subscribe(topic, { qos: 2, ...options }, (err: Error | null, granted: any) => {
|
|
153
153
|
if (err) {
|
|
154
154
|
this.error(err, `subscribing to MQTT topic "${topic}" failed`)
|
|
155
155
|
reject(err)
|
|
@@ -172,7 +172,7 @@ export class BaseTrait<T extends APISchema = APISchema> extends TraceTrait<T> {
|
|
|
172
172
|
protected async unsubscribeTopic (topic: string): Promise<void> {
|
|
173
173
|
this.log("info", `unsubscribing from MQTT topic "${topic}"`)
|
|
174
174
|
return new Promise<void>((resolve, reject) => {
|
|
175
|
-
this.
|
|
175
|
+
this._mqtt.unsubscribe(topic, (err?: Error, _packet?: any) => {
|
|
176
176
|
if (err) {
|
|
177
177
|
this.error(err, `unsubscribing from MQTT topic "${topic}" failed`)
|
|
178
178
|
reject(err)
|
|
@@ -215,7 +215,7 @@ export class BaseTrait<T extends APISchema = APISchema> extends TraceTrait<T> {
|
|
|
215
215
|
const messageData = typeof message === "string"
|
|
216
216
|
? message
|
|
217
217
|
: Buffer.from(message.buffer, message.byteOffset, message.byteLength)
|
|
218
|
-
this.
|
|
218
|
+
this._mqtt.publish(topic, messageData, options, (err?: Error) => {
|
|
219
219
|
if (err) {
|
|
220
220
|
this.error(err, `publishing to MQTT topic "${topic}" failed`)
|
|
221
221
|
reject(err)
|
|
@@ -268,12 +268,15 @@ export class BaseTrait<T extends APISchema = APISchema> extends TraceTrait<T> {
|
|
|
268
268
|
}
|
|
269
269
|
this.log("debug", `received from MQTT topic "${topic}"`, { message })
|
|
270
270
|
|
|
271
|
-
/*
|
|
271
|
+
/* drop message on receiver mismatch on direct topics, as it is
|
|
272
|
+
addressed to a foreign peer and hence must not be processed here */
|
|
272
273
|
if (topicMatch.peerId !== undefined
|
|
273
274
|
&& message.receiver !== undefined
|
|
274
|
-
&& message.receiver !== topicMatch.peerId)
|
|
275
|
-
this.log("warning", `receiver mismatch
|
|
275
|
+
&& message.receiver !== topicMatch.peerId) {
|
|
276
|
+
this.log("warning", `receiver mismatch -- dropped message from direct topic "${topic}"` +
|
|
276
277
|
` (expected "${topicMatch.peerId}", got "${message.receiver}")`)
|
|
278
|
+
return
|
|
279
|
+
}
|
|
277
280
|
|
|
278
281
|
/* dispatch MQTT+ message */
|
|
279
282
|
if (this.msg.isRequest(message)) {
|
package/src/mqtt-plus-error.ts
CHANGED
|
@@ -201,11 +201,15 @@ function runUnroll (isAsync: boolean, spool?: Spool): Promise<void> | void {
|
|
|
201
201
|
return result
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
-
/* helper
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
function
|
|
208
|
-
|
|
204
|
+
/* helper type for an error carrying an attached, secondary error */
|
|
205
|
+
type ErrorSuppressing = Error & { suppressed?: Error }
|
|
206
|
+
|
|
207
|
+
/* helper function for settling the epilog of "run": run the finally code
|
|
208
|
+
isolated, unroll the spool, and preserve a pending error by attaching a
|
|
209
|
+
failing finally code to it as a suppressed error instead of discarding it */
|
|
210
|
+
function runSettle (isAsync: false, pending: ErrorSuppressing | undefined, spool?: Spool, onfinally?: () => void, description?: string): void
|
|
211
|
+
function runSettle (isAsync: true, pending: ErrorSuppressing | undefined, spool?: Spool, onfinally?: () => Promise<void> | void, description?: string): Promise<void>
|
|
212
|
+
function runSettle (isAsync: boolean, pending: ErrorSuppressing | undefined, spool?: Spool, onfinally?: () => Promise<void> | void, description?: string): Promise<void> | void {
|
|
209
213
|
if (!isAsync) {
|
|
210
214
|
let failure: Error | undefined
|
|
211
215
|
try {
|
|
@@ -214,10 +218,14 @@ function runSettle (isAsync: boolean, pending: boolean, spool?: Spool, onfinally
|
|
|
214
218
|
catch (arg: unknown) {
|
|
215
219
|
failure = ensureError(arg, description)
|
|
216
220
|
}
|
|
217
|
-
if (pending || failure !== undefined) {
|
|
221
|
+
if (pending !== undefined || failure !== undefined) {
|
|
218
222
|
runUnroll(false, spool)
|
|
219
|
-
if (
|
|
220
|
-
|
|
223
|
+
if (failure !== undefined) {
|
|
224
|
+
if (pending !== undefined)
|
|
225
|
+
pending.suppressed = failure
|
|
226
|
+
else
|
|
227
|
+
throw failure
|
|
228
|
+
}
|
|
221
229
|
}
|
|
222
230
|
}
|
|
223
231
|
else {
|
|
@@ -229,15 +237,41 @@ function runSettle (isAsync: boolean, pending: boolean, spool?: Spool, onfinally
|
|
|
229
237
|
catch (arg: unknown) {
|
|
230
238
|
failure = ensureError(arg, description)
|
|
231
239
|
}
|
|
232
|
-
if (pending || failure !== undefined) {
|
|
240
|
+
if (pending !== undefined || failure !== undefined) {
|
|
233
241
|
await runUnroll(true, spool)
|
|
234
|
-
if (
|
|
235
|
-
|
|
242
|
+
if (failure !== undefined) {
|
|
243
|
+
if (pending !== undefined)
|
|
244
|
+
pending.suppressed = failure
|
|
245
|
+
else
|
|
246
|
+
throw failure
|
|
247
|
+
}
|
|
236
248
|
}
|
|
237
249
|
})()
|
|
238
250
|
}
|
|
239
251
|
}
|
|
240
252
|
|
|
253
|
+
/* helper function for settling the epilog of "run" and then failing with
|
|
254
|
+
a pending error in a still unknown synchronous/asynchronous context:
|
|
255
|
+
operate synchronously as long as possible and continue asynchronously as
|
|
256
|
+
soon as a callback returns a Promise. As the pending error always takes
|
|
257
|
+
precedence, a failing finally code is attached to it as a suppressed error. */
|
|
258
|
+
function runFail (error: ErrorSuppressing, spool?: Spool, onfinally?: () => Promise<void> | void, description?: string): Promise<never> {
|
|
259
|
+
const attach = (arg: unknown) => { error.suppressed = ensureError(arg, description) }
|
|
260
|
+
let result: Promise<void> | void = undefined
|
|
261
|
+
try {
|
|
262
|
+
result = onfinally?.()
|
|
263
|
+
}
|
|
264
|
+
catch (arg: unknown) {
|
|
265
|
+
attach(arg)
|
|
266
|
+
}
|
|
267
|
+
if (result instanceof Promise)
|
|
268
|
+
return result.catch(attach).then(() => spool?.unroll()).then(() => { throw error })
|
|
269
|
+
const unrolled = spool?.unroll()
|
|
270
|
+
if (unrolled instanceof Promise)
|
|
271
|
+
return unrolled.then(() => { throw error })
|
|
272
|
+
throw error
|
|
273
|
+
}
|
|
274
|
+
|
|
241
275
|
/* helper type for ensuring T contains no Promise */
|
|
242
276
|
type RunNoPromise<T> =
|
|
243
277
|
[ T ] extends [ Promise<any> ] ? never : T
|
|
@@ -379,31 +413,29 @@ export function run<T> (
|
|
|
379
413
|
result = action()
|
|
380
414
|
}
|
|
381
415
|
catch (arg: unknown) {
|
|
382
|
-
/* synchronous case (error branch)
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
runSettle(false, true, spool, onfinally, description)
|
|
391
|
-
throw error
|
|
392
|
-
}
|
|
393
|
-
if (spool && oncleanup)
|
|
394
|
-
spool.roll(result, oncleanup as SpoolCleanup<unknown>)
|
|
395
|
-
runSettle(false, false, spool, onfinally, description)
|
|
396
|
-
return result
|
|
416
|
+
/* synchronous case (error branch): as the action failed before
|
|
417
|
+
returning its potentially asynchronous result, the context is
|
|
418
|
+
still unknown here and hence has to be probed */
|
|
419
|
+
const error = ensureError(arg, description)
|
|
420
|
+
if (!oncatch)
|
|
421
|
+
return runFail(error, spool, onfinally, description)
|
|
422
|
+
try {
|
|
423
|
+
result = oncatch(error)
|
|
397
424
|
}
|
|
398
|
-
|
|
399
|
-
|
|
425
|
+
catch (arg: unknown) {
|
|
426
|
+
return runFail(ensureError(arg, description), spool, onfinally, description)
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/* the recovery already ran, so let the common epilog below settle
|
|
430
|
+
its potentially asynchronous result without re-entering it */
|
|
431
|
+
oncatch = undefined
|
|
400
432
|
}
|
|
401
433
|
if (result instanceof Promise) {
|
|
402
434
|
/* asynchronous case (result or error branch) */
|
|
403
435
|
return result.then(async (result) => {
|
|
404
436
|
if (spool && oncleanup)
|
|
405
437
|
spool.roll(result, oncleanup as SpoolCleanup<unknown>)
|
|
406
|
-
await runSettle(true,
|
|
438
|
+
await runSettle(true, undefined, spool, onfinally, description)
|
|
407
439
|
return result
|
|
408
440
|
}, async (arg: unknown) => {
|
|
409
441
|
/* asynchronous case (error branch) */
|
|
@@ -415,15 +447,15 @@ export function run<T> (
|
|
|
415
447
|
}
|
|
416
448
|
catch (arg: unknown) {
|
|
417
449
|
error = ensureError(arg, description)
|
|
418
|
-
await runSettle(true,
|
|
450
|
+
await runSettle(true, error, spool, onfinally, description)
|
|
419
451
|
throw error
|
|
420
452
|
}
|
|
421
453
|
if (spool && oncleanup)
|
|
422
454
|
spool.roll(result, oncleanup as SpoolCleanup<unknown>)
|
|
423
|
-
await runSettle(true,
|
|
455
|
+
await runSettle(true, undefined, spool, onfinally, description)
|
|
424
456
|
return result
|
|
425
457
|
}
|
|
426
|
-
await runSettle(true,
|
|
458
|
+
await runSettle(true, error, spool, onfinally, description)
|
|
427
459
|
throw error
|
|
428
460
|
})
|
|
429
461
|
}
|
|
@@ -431,7 +463,7 @@ export function run<T> (
|
|
|
431
463
|
/* synchronous case (result branch) */
|
|
432
464
|
if (spool && oncleanup)
|
|
433
465
|
spool.roll(result, oncleanup as SpoolCleanup<unknown>)
|
|
434
|
-
runSettle(false,
|
|
466
|
+
runSettle(false, undefined, spool, onfinally, description)
|
|
435
467
|
return result
|
|
436
468
|
}
|
|
437
469
|
}
|
package/src/mqtt-plus-event.ts
CHANGED
|
@@ -28,6 +28,7 @@ import type { IClientPublishOptions,
|
|
|
28
28
|
import { nanoid } from "nanoid"
|
|
29
29
|
|
|
30
30
|
/* internal requirements */
|
|
31
|
+
import { alignHandlerParams } from "./mqtt-plus-util"
|
|
31
32
|
import type { EventEmission } from "./mqtt-plus-msg"
|
|
32
33
|
import type { APISchema, EventKeys,
|
|
33
34
|
Registration } from "./mqtt-plus-api"
|
|
@@ -142,7 +143,7 @@ export class EventTrait<T extends APISchema = APISchema> extends AuthTrait<T> {
|
|
|
142
143
|
` (topic: "${topicName}", payload: "${request.name}")`, { requestId })
|
|
143
144
|
return
|
|
144
145
|
}
|
|
145
|
-
const params = request.params ?? []
|
|
146
|
+
const params = alignHandlerParams(request.params ?? [], callback)
|
|
146
147
|
|
|
147
148
|
/* sanity check request id */
|
|
148
149
|
if (this.eventControllers.has(requestId)) {
|
package/src/mqtt-plus-options.ts
CHANGED
|
@@ -71,5 +71,13 @@ export class OptionsTrait<_T extends APISchema = APISchema> {
|
|
|
71
71
|
},
|
|
72
72
|
...options
|
|
73
73
|
}
|
|
74
|
+
|
|
75
|
+
/* sanity check numeric option ranges */
|
|
76
|
+
if (!(Number.isFinite(this.options.timeout) && this.options.timeout > 0))
|
|
77
|
+
throw new Error("options: \"timeout\" must be a positive number")
|
|
78
|
+
if (!(Number.isInteger(this.options.chunkSize) && this.options.chunkSize > 0))
|
|
79
|
+
throw new Error("options: \"chunkSize\" must be a positive integer")
|
|
80
|
+
if (!(Number.isInteger(this.options.chunkCredit) && this.options.chunkCredit >= 0))
|
|
81
|
+
throw new Error("options: \"chunkCredit\" must be a non-negative integer")
|
|
74
82
|
}
|
|
75
83
|
}
|
package/src/mqtt-plus-service.ts
CHANGED
|
@@ -28,6 +28,7 @@ import type { IClientPublishOptions,
|
|
|
28
28
|
import { nanoid } from "nanoid"
|
|
29
29
|
|
|
30
30
|
/* internal requirements */
|
|
31
|
+
import { alignHandlerParams } from "./mqtt-plus-util"
|
|
31
32
|
import { run, Spool, ensureError } from "./mqtt-plus-error"
|
|
32
33
|
import type { ServiceCallRequest,
|
|
33
34
|
ServiceCallResponse } from "./mqtt-plus-msg"
|
|
@@ -138,7 +139,7 @@ export class ServiceTrait<T extends APISchema = APISchema> extends EventTrait<T>
|
|
|
138
139
|
/* determine request information */
|
|
139
140
|
const requestId = request.id
|
|
140
141
|
const senderId = request.sender
|
|
141
|
-
const params = request.params ?? []
|
|
142
|
+
const params = alignHandlerParams(request.params ?? [], callback)
|
|
142
143
|
if (senderId === undefined || senderId === "") {
|
|
143
144
|
this.error(new Error("invalid request: missing sender"))
|
|
144
145
|
return
|
|
@@ -325,11 +326,13 @@ export class ServiceTrait<T extends APISchema = APISchema> extends EventTrait<T>
|
|
|
325
326
|
return false
|
|
326
327
|
settled = true
|
|
327
328
|
|
|
328
|
-
/* unroll fire-and-forget: the "settled" boolean guard
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
spool.unroll()
|
|
329
|
+
/* unroll fire-and-forget: the "settled" boolean guard protects
|
|
330
|
+
against concurrent/duplicate responses and the async MQTT
|
|
331
|
+
unsubscription is handled by reference-counting/linger, while
|
|
332
|
+
cleanup failures are surfaced as "error" events */
|
|
333
|
+
spool.unroll(false)?.catch((err: unknown) => {
|
|
334
|
+
this.error(ensureError(err, `cleanup after call of service "${name}" failed`))
|
|
335
|
+
})
|
|
333
336
|
return true
|
|
334
337
|
}
|
|
335
338
|
const promise: Promise<ReturnType<T[K]>> = new Promise((resolve, reject) => {
|
package/src/mqtt-plus-sink.ts
CHANGED
|
@@ -33,7 +33,7 @@ import { nanoid } from "nanoid"
|
|
|
33
33
|
/* internal requirements */
|
|
34
34
|
import { CreditGate, ReadableTee,
|
|
35
35
|
sendBufferAsChunks, sendStreamAsChunks,
|
|
36
|
-
makeMutuallyExclusiveFields }
|
|
36
|
+
makeMutuallyExclusiveFields, alignHandlerParams } from "./mqtt-plus-util"
|
|
37
37
|
import { run, Spool, ensureError } from "./mqtt-plus-error"
|
|
38
38
|
import type { SinkPushRequest, SinkPushResponse,
|
|
39
39
|
SinkPushChunk, SinkPushCredit } from "./mqtt-plus-msg"
|
|
@@ -163,7 +163,7 @@ export class SinkTrait<T extends APISchema = APISchema> extends SourceTrait<T> {
|
|
|
163
163
|
|
|
164
164
|
/* determine information */
|
|
165
165
|
const requestId = request.id
|
|
166
|
-
const params = request.params ?? []
|
|
166
|
+
const params = alignHandlerParams(request.params ?? [], callback)
|
|
167
167
|
const sender = request.sender
|
|
168
168
|
const receiver = request.receiver
|
|
169
169
|
|
|
@@ -603,9 +603,14 @@ export class SinkTrait<T extends APISchema = APISchema> extends SourceTrait<T> {
|
|
|
603
603
|
/* ensure stream gets destroyed on abort */
|
|
604
604
|
if (data instanceof Readable) {
|
|
605
605
|
const stream = data
|
|
606
|
+
const noopError = () => {} /* prevent unhandled error exception */
|
|
606
607
|
abortSignal.addEventListener("abort", () => {
|
|
607
|
-
if (!stream.destroyed)
|
|
608
|
+
if (!stream.destroyed) {
|
|
609
|
+
/* guard the caller's stream, as it still carries no "error"
|
|
610
|
+
listener when the abort precedes the chunk sending phase */
|
|
611
|
+
stream.on("error", noopError)
|
|
608
612
|
stream.destroy(ensureError(abortSignal.reason))
|
|
613
|
+
}
|
|
609
614
|
}, { once: true })
|
|
610
615
|
}
|
|
611
616
|
|
|
@@ -645,20 +650,25 @@ export class SinkTrait<T extends APISchema = APISchema> extends SourceTrait<T> {
|
|
|
645
650
|
})
|
|
646
651
|
pushFinalize.catch(() => {}) /* avoid unhandled promise rejection */
|
|
647
652
|
|
|
653
|
+
/* abort the push and settle the pending initial/finalize promises */
|
|
654
|
+
const failPush = (error: Error) => {
|
|
655
|
+
abortController.abort(error)
|
|
656
|
+
if (!pushAcked) {
|
|
657
|
+
pushInitialSettled = true
|
|
658
|
+
pushInitialReject(error)
|
|
659
|
+
}
|
|
660
|
+
else if (!pushFinalized) {
|
|
661
|
+
pushFinalized = true
|
|
662
|
+
pushFinalizeReject(error)
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
648
666
|
/* lock the responder for this communication */
|
|
649
667
|
const lockResponder = (kind: string, sender?: string): boolean => {
|
|
650
668
|
if (sender === undefined || sender === "") {
|
|
651
669
|
const error = new Error(`received ${kind} without sender`)
|
|
652
670
|
remoteErrorObject = error
|
|
653
|
-
|
|
654
|
-
if (!pushAcked) {
|
|
655
|
-
pushInitialSettled = true
|
|
656
|
-
pushInitialReject(error)
|
|
657
|
-
}
|
|
658
|
-
else if (!pushFinalized) {
|
|
659
|
-
pushFinalized = true
|
|
660
|
-
pushFinalizeReject(error)
|
|
661
|
-
}
|
|
671
|
+
failPush(error)
|
|
662
672
|
return false
|
|
663
673
|
}
|
|
664
674
|
if (responderId === undefined)
|
|
@@ -680,15 +690,7 @@ export class SinkTrait<T extends APISchema = APISchema> extends SourceTrait<T> {
|
|
|
680
690
|
if (response.name !== name) {
|
|
681
691
|
const error = new Error(`sink response name mismatch (expected "${name}", got "${response.name}")`)
|
|
682
692
|
remoteErrorObject = error
|
|
683
|
-
|
|
684
|
-
if (!pushAcked) {
|
|
685
|
-
pushInitialSettled = true
|
|
686
|
-
pushInitialReject(error)
|
|
687
|
-
}
|
|
688
|
-
else if (!pushFinalized) {
|
|
689
|
-
pushFinalized = true
|
|
690
|
-
pushFinalizeReject(error)
|
|
691
|
-
}
|
|
693
|
+
failPush(error)
|
|
692
694
|
return
|
|
693
695
|
}
|
|
694
696
|
if (!lockResponder("sink response", response.sender))
|
|
@@ -696,15 +698,7 @@ export class SinkTrait<T extends APISchema = APISchema> extends SourceTrait<T> {
|
|
|
696
698
|
if (response.error) {
|
|
697
699
|
const error = new Error(response.error)
|
|
698
700
|
remoteErrorObject = error
|
|
699
|
-
|
|
700
|
-
if (!pushAcked) {
|
|
701
|
-
pushInitialSettled = true
|
|
702
|
-
pushInitialReject(error)
|
|
703
|
-
}
|
|
704
|
-
else if (!pushFinalized) {
|
|
705
|
-
pushFinalized = true
|
|
706
|
-
pushFinalizeReject(error)
|
|
707
|
-
}
|
|
701
|
+
failPush(error)
|
|
708
702
|
}
|
|
709
703
|
else if (!pushAcked) {
|
|
710
704
|
initialCredit = response.credit
|
|
@@ -730,15 +724,7 @@ export class SinkTrait<T extends APISchema = APISchema> extends SourceTrait<T> {
|
|
|
730
724
|
if (response.name !== name) {
|
|
731
725
|
const error = new Error(`sink credit name mismatch (expected "${name}", got "${response.name}")`)
|
|
732
726
|
remoteErrorObject = error
|
|
733
|
-
|
|
734
|
-
if (!pushAcked) {
|
|
735
|
-
pushInitialSettled = true
|
|
736
|
-
pushInitialReject(error)
|
|
737
|
-
}
|
|
738
|
-
else if (!pushFinalized) {
|
|
739
|
-
pushFinalized = true
|
|
740
|
-
pushFinalizeReject(error)
|
|
741
|
-
}
|
|
727
|
+
failPush(error)
|
|
742
728
|
return
|
|
743
729
|
}
|
|
744
730
|
if (!lockResponder("sink credit", response.sender))
|
|
@@ -747,15 +733,7 @@ export class SinkTrait<T extends APISchema = APISchema> extends SourceTrait<T> {
|
|
|
747
733
|
/* cancel signal from receiver */
|
|
748
734
|
cancelledByReceiver = true
|
|
749
735
|
const error = new Error(`push to sink "${name}" cancelled by receiver`)
|
|
750
|
-
|
|
751
|
-
if (!pushAcked) {
|
|
752
|
-
pushInitialSettled = true
|
|
753
|
-
pushInitialReject(error)
|
|
754
|
-
}
|
|
755
|
-
else if (!pushFinalized) {
|
|
756
|
-
pushFinalized = true
|
|
757
|
-
pushFinalizeReject(error)
|
|
758
|
-
}
|
|
736
|
+
failPush(error)
|
|
759
737
|
return
|
|
760
738
|
}
|
|
761
739
|
if (creditGate !== undefined) {
|
|
@@ -767,11 +745,7 @@ export class SinkTrait<T extends APISchema = APISchema> extends SourceTrait<T> {
|
|
|
767
745
|
not granting initial credit during ack */
|
|
768
746
|
const error = new Error(`push to sink "${name}" received unsolicited credit (credit-flow disabled)`)
|
|
769
747
|
remoteErrorObject = error
|
|
770
|
-
|
|
771
|
-
if (!pushFinalized) {
|
|
772
|
-
pushFinalized = true
|
|
773
|
-
pushFinalizeReject(error)
|
|
774
|
-
}
|
|
748
|
+
failPush(error)
|
|
775
749
|
}
|
|
776
750
|
else
|
|
777
751
|
pendingCredit += response.credit
|
|
@@ -897,7 +871,7 @@ export class SinkTrait<T extends APISchema = APISchema> extends SourceTrait<T> {
|
|
|
897
871
|
response from the receiver to be processed, making the
|
|
898
872
|
error outcome deterministic (only when race is possible) */
|
|
899
873
|
if (pushAcked && !remoteErrorObject)
|
|
900
|
-
await new Promise<void>((resolve) => {
|
|
874
|
+
await new Promise<void>((resolve) => { setTimeout(resolve, 0) })
|
|
901
875
|
|
|
902
876
|
if (remoteErrorObject)
|
|
903
877
|
throw remoteErrorObject
|
package/src/mqtt-plus-source.ts
CHANGED
|
@@ -33,7 +33,7 @@ import { nanoid } from "nanoid"
|
|
|
33
33
|
/* internal requirements */
|
|
34
34
|
import { CreditGate, ReadableTee,
|
|
35
35
|
sendBufferAsChunks, sendStreamAsChunks,
|
|
36
|
-
makeMutuallyExclusiveFields }
|
|
36
|
+
makeMutuallyExclusiveFields, alignHandlerParams } from "./mqtt-plus-util"
|
|
37
37
|
import { run, Spool, ensureError } from "./mqtt-plus-error"
|
|
38
38
|
import type { SourceFetchRequest, SourceFetchResponse,
|
|
39
39
|
SourceFetchChunk, SourceFetchCredit } from "./mqtt-plus-msg"
|
|
@@ -143,7 +143,7 @@ export class SourceTrait<T extends APISchema = APISchema> extends ServiceTrait<T
|
|
|
143
143
|
|
|
144
144
|
/* determine information */
|
|
145
145
|
const requestId = request.id
|
|
146
|
-
const params = request.params ?? []
|
|
146
|
+
const params = alignHandlerParams(request.params ?? [], callback)
|
|
147
147
|
const sender = request.sender
|
|
148
148
|
const receiver = request.receiver
|
|
149
149
|
|
|
@@ -211,16 +211,20 @@ export class SourceTrait<T extends APISchema = APISchema> extends ServiceTrait<T
|
|
|
211
211
|
})
|
|
212
212
|
|
|
213
213
|
/* ensure stream gets destroyed on abort */
|
|
214
|
-
|
|
215
|
-
|
|
214
|
+
const noopError = () => {} /* prevent unhandled error exception */
|
|
215
|
+
const destroyStream = () => {
|
|
216
|
+
if (info.stream instanceof Readable && !info.stream.destroyed) {
|
|
217
|
+
/* guard the handler's stream, as it still carries no "error"
|
|
218
|
+
listener when the abort precedes the chunk sending phase */
|
|
219
|
+
info.stream.on("error", noopError)
|
|
216
220
|
info.stream.destroy(ensureError(abortSignal.reason))
|
|
217
|
-
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
abortSignal.addEventListener("abort", destroyStream, { once: true })
|
|
218
224
|
/* safety net: destroy a stream assigned after the abort event fired */
|
|
219
225
|
reqSpool.roll(() => {
|
|
220
|
-
if (abortSignal.aborted
|
|
221
|
-
|
|
222
|
-
&& !info.stream.destroyed)
|
|
223
|
-
info.stream.destroy(ensureError(abortSignal.reason))
|
|
226
|
+
if (abortSignal.aborted)
|
|
227
|
+
destroyStream()
|
|
224
228
|
})
|
|
225
229
|
let abortReject!: (reason?: any) => void
|
|
226
230
|
const abortPromise = new Promise<never>((_resolve, reject) => {
|