mqtt-plus 1.4.23 → 1.4.25
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/service.log +10386 -0
- package/.ase/service.yaml +1 -1
- package/.ase/task/TASK-P18.md +47 -0
- package/.ase/task/TASK-P2.md +76 -0
- package/.ase/task/TASK-P21.md +75 -0
- package/.claude/settings.local.json +7 -0
- package/CHANGELOG.md +23 -0
- package/README.md +4 -4
- package/dst-stage1/mqtt-plus-api.d.ts +6 -6
- package/dst-stage1/mqtt-plus-base.js +21 -4
- package/dst-stage1/mqtt-plus-codec.js +15 -6
- package/dst-stage1/mqtt-plus-error.js +47 -14
- package/dst-stage1/mqtt-plus-event.js +1 -1
- package/dst-stage1/mqtt-plus-service.js +1 -1
- package/dst-stage1/mqtt-plus-sink.js +10 -2
- package/dst-stage1/mqtt-plus-source.d.ts +1 -0
- package/dst-stage1/mqtt-plus-source.js +11 -3
- package/dst-stage1/mqtt-plus-subscription.js +7 -2
- package/dst-stage1/mqtt-plus-timer.js +4 -2
- package/dst-stage2/mqtt-plus.cjs.cjs +90 -36
- package/dst-stage2/mqtt-plus.esm.js +90 -36
- package/dst-stage2/mqtt-plus.umd.js +11 -11
- package/package.d/{vite+8.0.13.patch → vite+8.1.4.patch} +2 -2
- package/package.json +22 -24
- package/src/mqtt-plus-api.ts +5 -5
- package/src/mqtt-plus-base.ts +21 -4
- package/src/mqtt-plus-codec.ts +15 -8
- package/src/mqtt-plus-error.ts +52 -14
- package/src/mqtt-plus-event.ts +1 -1
- package/src/mqtt-plus-service.ts +1 -1
- package/src/mqtt-plus-sink.ts +12 -2
- package/src/mqtt-plus-source.ts +13 -3
- package/src/mqtt-plus-subscription.ts +7 -2
- package/src/mqtt-plus-timer.ts +4 -2
- package/tst/mqtt-plus-4-sink.spec.ts +43 -0
- package/tst/mqtt-plus-5-source.spec.ts +35 -0
- package/tst/mqtt-plus-6-misc.spec.ts +82 -5
- package/tst/mqtt-plus-8-run.spec.ts +107 -0
- /package/package.d/{@typescript-eslint+typescript-estree+8.59.3.patch → @typescript-eslint+typescript-estree+8.63.0.patch} +0 -0
|
@@ -36,6 +36,7 @@ import MQTT from "mqtt"
|
|
|
36
36
|
|
|
37
37
|
/* internal dependencies */
|
|
38
38
|
import MQTTp from "mqtt-plus"
|
|
39
|
+
import type { Event } from "mqtt-plus"
|
|
39
40
|
import { ctx } from "./mqtt-plus-0-fixture"
|
|
40
41
|
import type { API } from "./mqtt-plus-0-fixture"
|
|
41
42
|
import { makeMutuallyExclusiveFields } from "../src/mqtt-plus-util"
|
|
@@ -113,31 +114,76 @@ describe("MQTT+ Miscellaneous", function () {
|
|
|
113
114
|
/* setup */
|
|
114
115
|
this.slow(2000)
|
|
115
116
|
this.timeout(2000)
|
|
116
|
-
const spy
|
|
117
|
+
const spy = sinon.spy()
|
|
118
|
+
const spyB = sinon.spy()
|
|
117
119
|
|
|
118
120
|
/* create JSON codec API instances on existing MQTT connections */
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
+
type APIX = API & { "example/server/binary": Event<(data: Buffer) => void> }
|
|
122
|
+
const apiJsonS = new MQTTp<APIX>(ctx.mqttS, { id: "json-server", codec: "json", timeout: 500 })
|
|
123
|
+
const apiJsonC = new MQTTp<APIX>(ctx.mqttC, { id: "json-client", codec: "json", timeout: 500 })
|
|
121
124
|
|
|
122
|
-
/* register event
|
|
125
|
+
/* register event handlers */
|
|
123
126
|
const registration = await apiJsonS.event("example/server/sample", (str: string, num: number) => {
|
|
124
127
|
spy("event", str, num)
|
|
125
128
|
})
|
|
129
|
+
const registrationB = await apiJsonS.event("example/server/binary", (data: Buffer) => {
|
|
130
|
+
spyB(data)
|
|
131
|
+
})
|
|
126
132
|
|
|
127
|
-
/* emit
|
|
133
|
+
/* emit events via JSON codec */
|
|
128
134
|
apiJsonC.emit("example/server/sample", "hello", 99)
|
|
135
|
+
apiJsonC.emit("example/server/binary", Buffer.from([ 0x01, 0x02, 0x03, 0xff ]))
|
|
129
136
|
await new Promise((resolve) => { setTimeout(resolve, 100) })
|
|
130
137
|
|
|
131
138
|
/* verify round-trip */
|
|
132
139
|
expect(spy.getCalls().length).to.equal(1)
|
|
133
140
|
expect(spy.getCalls()[0].args).to.deep.equal([ "event", "hello", 99 ])
|
|
134
141
|
|
|
142
|
+
/* verify Buffer round-trip */
|
|
143
|
+
expect(spyB.getCalls().length).to.equal(1)
|
|
144
|
+
const data: Buffer = spyB.getCalls()[0].args[0]
|
|
145
|
+
expect(data).to.be.instanceOf(Buffer)
|
|
146
|
+
expect(data.equals(Buffer.from([ 0x01, 0x02, 0x03, 0xff ]))).to.equal(true)
|
|
147
|
+
|
|
135
148
|
/* cleanup */
|
|
136
149
|
await registration.destroy()
|
|
150
|
+
await registrationB.destroy()
|
|
137
151
|
apiJsonS.destroy()
|
|
138
152
|
apiJsonC.destroy()
|
|
139
153
|
})
|
|
140
154
|
|
|
155
|
+
/* test case: Error Event on throwing topicMatch */
|
|
156
|
+
it("MQTT+ Error Event on Throwing topicMatch", async function () {
|
|
157
|
+
/* setup */
|
|
158
|
+
this.slow(2000)
|
|
159
|
+
this.timeout(2000)
|
|
160
|
+
const spy = sinon.spy()
|
|
161
|
+
|
|
162
|
+
/* create API instances with a throwing user-supplied topicMatch on the receiver side */
|
|
163
|
+
const apiThrowS = new MQTTp<API>(ctx.mqttS, { id: "throw-server", timeout: 500,
|
|
164
|
+
topicMatch: () => { throw new Error("intentionally failing topicMatch") } })
|
|
165
|
+
const apiThrowC = new MQTTp<API>(ctx.mqttC, { id: "throw-client", timeout: 500 })
|
|
166
|
+
|
|
167
|
+
/* observe MQTT+ error events */
|
|
168
|
+
apiThrowS.on("error", (err: Error) => { spy(err.message) })
|
|
169
|
+
|
|
170
|
+
/* register event handler to subscribe the topic */
|
|
171
|
+
const registration = await apiThrowS.event("example/server/sample", () => {})
|
|
172
|
+
|
|
173
|
+
/* emit event to trigger the inbound message processing */
|
|
174
|
+
apiThrowC.emit("example/server/sample", "hello", 42)
|
|
175
|
+
await new Promise((resolve) => { setTimeout(resolve, 100) })
|
|
176
|
+
|
|
177
|
+
/* ensure the exception surfaced as MQTT+ "error" event */
|
|
178
|
+
expect(spy.getCalls().length).to.be.at.least(1)
|
|
179
|
+
expect(spy.getCalls()[0].args[0]).to.match(/failed to match MQTT topic/)
|
|
180
|
+
|
|
181
|
+
/* cleanup */
|
|
182
|
+
await registration.destroy()
|
|
183
|
+
apiThrowS.destroy()
|
|
184
|
+
apiThrowC.destroy()
|
|
185
|
+
})
|
|
186
|
+
|
|
141
187
|
/* test case: Authentication */
|
|
142
188
|
it("MQTT+ Authentication", async function () {
|
|
143
189
|
/* setup */
|
|
@@ -222,6 +268,37 @@ describe("MQTT+ Miscellaneous", function () {
|
|
|
222
268
|
await registration2.destroy()
|
|
223
269
|
})
|
|
224
270
|
|
|
271
|
+
/* test case: Authentication with Empty Role */
|
|
272
|
+
it("MQTT+ Authentication with Empty Role", async function () {
|
|
273
|
+
/* setup */
|
|
274
|
+
this.slow(2000)
|
|
275
|
+
this.timeout(2000)
|
|
276
|
+
const spy = sinon.spy()
|
|
277
|
+
|
|
278
|
+
/* server-side: provide hello service with falsy-but-valid empty role */
|
|
279
|
+
const registration = await ctx.apiS.service({
|
|
280
|
+
name: "example/server/hello",
|
|
281
|
+
auth: "",
|
|
282
|
+
callback: (str: string, num: number) => {
|
|
283
|
+
spy("hello")
|
|
284
|
+
return `${str}:${num}`
|
|
285
|
+
}
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
/* call service and expect fail-closed rejection */
|
|
289
|
+
await ctx.apiC.call("example/server/hello", "world", 42).then(async (_result) => {
|
|
290
|
+
spy("call-success")
|
|
291
|
+
}).catch((err: Error) => {
|
|
292
|
+
spy("call-error")
|
|
293
|
+
expect(err.message).to.match(/failed authentication/)
|
|
294
|
+
})
|
|
295
|
+
expect(spy.getCalls().map((call) => call.firstArg))
|
|
296
|
+
.to.be.deep.equal([ "call-error" ])
|
|
297
|
+
|
|
298
|
+
/* cleanup */
|
|
299
|
+
await registration.destroy()
|
|
300
|
+
})
|
|
301
|
+
|
|
225
302
|
/* test case: Unit: arr2buf/buf2arr */
|
|
226
303
|
it("MQTT+ Unit: arr2buf/buf2arr", function () {
|
|
227
304
|
/* create a dry-run MQTTp instance for accessing encode methods */
|
|
@@ -81,6 +81,23 @@ describe("MQTT+ Run", function () {
|
|
|
81
81
|
}
|
|
82
82
|
})
|
|
83
83
|
|
|
84
|
+
/* test case: MQTT+ Run: async oncatch recovers but onfinally fails invokes onfinally once */
|
|
85
|
+
it("MQTT+ Run: async oncatch recovers but onfinally fails invokes onfinally once", async function () {
|
|
86
|
+
let finallyCount = 0
|
|
87
|
+
try {
|
|
88
|
+
await run(
|
|
89
|
+
() => Promise.reject(new Error("async-fail")),
|
|
90
|
+
(_err: Error) => Promise.resolve("async-recovered"),
|
|
91
|
+
async () => { finallyCount++; throw new Error("onfinally-fail") }
|
|
92
|
+
)
|
|
93
|
+
expect.fail("should have thrown")
|
|
94
|
+
}
|
|
95
|
+
catch (err: any) {
|
|
96
|
+
expect(err.message).to.match(/onfinally-fail/)
|
|
97
|
+
}
|
|
98
|
+
expect(finallyCount).to.equal(1)
|
|
99
|
+
})
|
|
100
|
+
|
|
84
101
|
/* test case: MQTT+ Run: sync success with spool and oncleanup */
|
|
85
102
|
it("MQTT+ Run: sync success with spool and oncleanup", function () {
|
|
86
103
|
const spool = new Spool()
|
|
@@ -96,6 +113,36 @@ describe("MQTT+ Run", function () {
|
|
|
96
113
|
expect(cleanups).to.deep.equal([ "resource-val" ])
|
|
97
114
|
})
|
|
98
115
|
|
|
116
|
+
/* test case: MQTT+ Run: sync oncatch recovery with spool and oncleanup */
|
|
117
|
+
it("MQTT+ Run: sync oncatch recovery with spool and oncleanup", function () {
|
|
118
|
+
const spool = new Spool()
|
|
119
|
+
const cleanups: string[] = []
|
|
120
|
+
run<string, string>(
|
|
121
|
+
spool,
|
|
122
|
+
() => { throw new Error("fail") },
|
|
123
|
+
(_err: Error) => "recovered-val",
|
|
124
|
+
undefined,
|
|
125
|
+
(val: string) => { cleanups.push(val) }
|
|
126
|
+
)
|
|
127
|
+
spool.unroll()
|
|
128
|
+
expect(cleanups).to.deep.equal([ "recovered-val" ])
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
/* test case: MQTT+ Run: async oncatch recovery with spool and oncleanup */
|
|
132
|
+
it("MQTT+ Run: async oncatch recovery with spool and oncleanup", async function () {
|
|
133
|
+
const spool = new Spool()
|
|
134
|
+
const cleanups: string[] = []
|
|
135
|
+
await run<string, Promise<string>>(
|
|
136
|
+
spool,
|
|
137
|
+
() => Promise.reject(new Error("async-fail")),
|
|
138
|
+
(_err: Error) => Promise.resolve("async-recovered-val"),
|
|
139
|
+
undefined,
|
|
140
|
+
async (val: string) => { cleanups.push(val) }
|
|
141
|
+
)
|
|
142
|
+
await spool.unroll()
|
|
143
|
+
expect(cleanups).to.deep.equal([ "async-recovered-val" ])
|
|
144
|
+
})
|
|
145
|
+
|
|
99
146
|
/* test case: MQTT+ Run: oncleanup without spool throws */
|
|
100
147
|
it("MQTT+ Run: oncleanup without spool throws", function () {
|
|
101
148
|
try {
|
|
@@ -111,5 +158,65 @@ describe("MQTT+ Run", function () {
|
|
|
111
158
|
expect(err.message).to.equal("oncleanup requires a spool")
|
|
112
159
|
}
|
|
113
160
|
})
|
|
161
|
+
|
|
162
|
+
/* test case: MQTT+ Run: sync error with throwing onfinally still unrolls and preserves error */
|
|
163
|
+
it("MQTT+ Run: sync error with throwing onfinally still unrolls and preserves error", function () {
|
|
164
|
+
const spool = new Spool()
|
|
165
|
+
const cleanups: string[] = []
|
|
166
|
+
spool.roll("res", (val: unknown) => { cleanups.push(val as string) })
|
|
167
|
+
try {
|
|
168
|
+
run<string, string>(
|
|
169
|
+
spool,
|
|
170
|
+
() => { throw new Error("action-fail") },
|
|
171
|
+
undefined,
|
|
172
|
+
() => { throw new Error("finally-fail") }
|
|
173
|
+
)
|
|
174
|
+
expect.fail("should have thrown")
|
|
175
|
+
}
|
|
176
|
+
catch (err: any) {
|
|
177
|
+
expect(err.message).to.match(/action-fail/)
|
|
178
|
+
}
|
|
179
|
+
expect(cleanups).to.deep.equal([ "res" ])
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
/* test case: MQTT+ Run: async error with rejecting onfinally still unrolls and preserves error */
|
|
183
|
+
it("MQTT+ Run: async error with rejecting onfinally still unrolls and preserves error", async function () {
|
|
184
|
+
const spool = new Spool()
|
|
185
|
+
const cleanups: string[] = []
|
|
186
|
+
spool.roll("res", (val: unknown) => { cleanups.push(val as string) })
|
|
187
|
+
try {
|
|
188
|
+
await run<string, Promise<string>>(
|
|
189
|
+
spool,
|
|
190
|
+
() => Promise.reject(new Error("async-action-fail")),
|
|
191
|
+
undefined,
|
|
192
|
+
() => Promise.reject(new Error("async-finally-fail"))
|
|
193
|
+
)
|
|
194
|
+
expect.fail("should have thrown")
|
|
195
|
+
}
|
|
196
|
+
catch (err: any) {
|
|
197
|
+
expect(err.message).to.match(/async-action-fail/)
|
|
198
|
+
}
|
|
199
|
+
expect(cleanups).to.deep.equal([ "res" ])
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
/* test case: MQTT+ Run: async success with rejecting onfinally surfaces onfinally error but still cleans up */
|
|
203
|
+
it("MQTT+ Run: async success with rejecting onfinally surfaces onfinally error but still cleans up", async function () {
|
|
204
|
+
const spool = new Spool()
|
|
205
|
+
const cleanups: string[] = []
|
|
206
|
+
try {
|
|
207
|
+
await run<string, Promise<string>>(
|
|
208
|
+
spool,
|
|
209
|
+
() => Promise.resolve("resource-val"),
|
|
210
|
+
undefined,
|
|
211
|
+
() => Promise.reject(new Error("finally-fail")),
|
|
212
|
+
async (val: string) => { cleanups.push(val) }
|
|
213
|
+
)
|
|
214
|
+
expect.fail("should have thrown")
|
|
215
|
+
}
|
|
216
|
+
catch (err: any) {
|
|
217
|
+
expect(err.message).to.match(/finally-fail/)
|
|
218
|
+
}
|
|
219
|
+
expect(cleanups).to.deep.equal([ "resource-val" ])
|
|
220
|
+
})
|
|
114
221
|
})
|
|
115
222
|
|