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.
Files changed (39) hide show
  1. package/.ase/service.log +10386 -0
  2. package/.ase/service.yaml +1 -1
  3. package/.ase/task/TASK-P18.md +47 -0
  4. package/.ase/task/TASK-P2.md +76 -0
  5. package/.ase/task/TASK-P21.md +75 -0
  6. package/.claude/settings.local.json +7 -0
  7. package/CHANGELOG.md +23 -0
  8. package/README.md +4 -4
  9. package/dst-stage1/mqtt-plus-api.d.ts +6 -6
  10. package/dst-stage1/mqtt-plus-base.js +21 -4
  11. package/dst-stage1/mqtt-plus-codec.js +15 -6
  12. package/dst-stage1/mqtt-plus-error.js +47 -14
  13. package/dst-stage1/mqtt-plus-event.js +1 -1
  14. package/dst-stage1/mqtt-plus-service.js +1 -1
  15. package/dst-stage1/mqtt-plus-sink.js +10 -2
  16. package/dst-stage1/mqtt-plus-source.d.ts +1 -0
  17. package/dst-stage1/mqtt-plus-source.js +11 -3
  18. package/dst-stage1/mqtt-plus-subscription.js +7 -2
  19. package/dst-stage1/mqtt-plus-timer.js +4 -2
  20. package/dst-stage2/mqtt-plus.cjs.cjs +90 -36
  21. package/dst-stage2/mqtt-plus.esm.js +90 -36
  22. package/dst-stage2/mqtt-plus.umd.js +11 -11
  23. package/package.d/{vite+8.0.13.patch → vite+8.1.4.patch} +2 -2
  24. package/package.json +22 -24
  25. package/src/mqtt-plus-api.ts +5 -5
  26. package/src/mqtt-plus-base.ts +21 -4
  27. package/src/mqtt-plus-codec.ts +15 -8
  28. package/src/mqtt-plus-error.ts +52 -14
  29. package/src/mqtt-plus-event.ts +1 -1
  30. package/src/mqtt-plus-service.ts +1 -1
  31. package/src/mqtt-plus-sink.ts +12 -2
  32. package/src/mqtt-plus-source.ts +13 -3
  33. package/src/mqtt-plus-subscription.ts +7 -2
  34. package/src/mqtt-plus-timer.ts +4 -2
  35. package/tst/mqtt-plus-4-sink.spec.ts +43 -0
  36. package/tst/mqtt-plus-5-source.spec.ts +35 -0
  37. package/tst/mqtt-plus-6-misc.spec.ts +82 -5
  38. package/tst/mqtt-plus-8-run.spec.ts +107 -0
  39. /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 = sinon.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
- const apiJsonS = new MQTTp<API>(ctx.mqttS, { id: "json-server", codec: "json", timeout: 500 })
120
- const apiJsonC = new MQTTp<API>(ctx.mqttC, { id: "json-client", codec: "json", timeout: 500 })
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 handler */
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 event via JSON codec */
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