mqtt-client-wasm 0.2.2

MQTT v3.1.1/v5.0 client for browsers using WebSocket (ws/wss) transport, compiled to WebAssembly
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
# mqtt-client-wasm

[![CI](https://github.com/redboltz/mqtt-client-wasm/actions/workflows/ci.yml/badge.svg)](https://github.com/redboltz/mqtt-client-wasm/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/redboltz/mqtt-client-wasm/branch/main/graph/badge.svg)](https://codecov.io/gh/redboltz/mqtt-client-wasm)

MQTT client library compiled to WebAssembly, supporting both browsers and Node.js.

## Supported Transports

| Transport | Browser | Node.js |
|-----------|---------|---------|
| WebSocket (ws://) | Yes | Yes |
| WebSocket Secure (wss://) | Yes | Yes |
| TCP | - | Yes |
| TLS | - | Yes |

## Features

- **MQTT v3.1.1 and v5.0**: Full support for both protocol versions
- **Multiple Transports**: WebSocket for both platforms, TCP/TLS for Node.js
- **Protocol State Machine**: Internal state machine automatically handles protocol behavior based on packet exchange
  - e.g., KeepAlive in CONNECT triggers automatic PINGREQ transmission
  - e.g., MQTT v5.0 properties (receiveMaximum, maximumPacketSize, topicAliasMaximum, etc.) are internally tracked and enforced
  - See [mqtt-protocol-core]https://github.com/redboltz/mqtt-protocol-core for details (Rust)
- **Low-level Endpoint API**: Direct packet send/recv operations for full control
- **Auto Response Options**: Configurable automatic handling of PUBACK, PUBREC, PUBREL, PUBCOMP, and PINGRESP
- **Interactive Client Tool**: Ready-to-use HTML client for testing and debugging (browser)

### Protocol State Machine Details

#### MQTT v3.1.1

**Sending CONNECT Packet:**
- If `keepAlive` is set to a value greater than 0, automatic PINGREQ transmission is enabled. A PINGREQ packet is sent when no other packet has been transmitted within `keepAlive` seconds.
- If `cleanSession` is set to `true`, the endpoint's Session State is cleared.

**Receiving CONNACK Packet:**
- If `sessionPresent` is `false`, the endpoint's Session State is cleared. However, the session state configuration is retained, so subsequent PUBLISH packets with QoS 1 or QoS 2 will be stored.
- If `sessionPresent` is `true`, any stored PUBLISH and PUBREL packets are retransmitted.

#### MQTT v5.0

**Sending CONNECT Packet:**
- If `cleanStart` is set to `true`, the endpoint's Session State is cleared.
- If `topicAliasMaximum` is set, a topic name to topic alias mapping is prepared for outgoing packets.
- If `receiveMaximum` is set, the receive quota for incoming packets is configured.
- If `maximumPacketSize` is set, the maximum packet size for incoming packets is configured.
- If `sessionExpiryInterval` is set to a value greater than 0, session state persistence is enabled.

**Receiving CONNACK Packet:**
- If `sessionPresent` is `false`, the endpoint's Session State is cleared. However, the session state configuration is retained, so subsequent PUBLISH packets with QoS 1 or QoS 2 will be stored.
- If `sessionPresent` is `true`, any stored PUBLISH and PUBREL packets are retransmitted.
- If `topicAliasMaximum` is set, a topic alias to topic name mapping is prepared for incoming packets.
- If `receiveMaximum` is set, the send quota for outgoing packets is configured.
- If `maximumPacketSize` is set, the maximum packet size for outgoing packets is configured.
- If `serverKeepAlive` is set, it overrides the client's `keepAlive` value. A PINGREQ packet is sent when no other packet has been transmitted within `serverKeepAlive` seconds.

#### Topic Alias

Topic Alias is an MQTT v5.0 feature that reduces PUBLISH packet size by mapping topic names to numeric identifiers. The mapping operates independently in two directions: broker-to-client and client-to-broker.

**Capacity Negotiation:**
- **Client to Broker:** The broker declares a `topicAliasMaximum` value in the CONNACK packet, allowing the client to use topic aliases up to that limit when sending PUBLISH packets.
- **Broker to Client:** The client declares a `topicAliasMaximum` value in the CONNECT packet, allowing the broker to use topic aliases up to that limit when sending PUBLISH packets.

**How Topic Alias Works:**

1. **Registering a mapping:** A PUBLISH packet containing both a topic name and a `topicAlias` property establishes the mapping. If the alias was already mapped to a different topic, the mapping is updated.

2. **Using a mapping:** A PUBLISH packet with an empty topic name and a `topicAlias` property signals the receiver to look up the topic name from the established mapping. This reduces packet size, especially for long topic names.

**Automatic Topic Alias Handling:**

The client provides two configuration options to automate topic alias management:

- `autoMapTopicAliasSend: true` - Automatically assigns topic aliases when sending PUBLISH packets. When all available aliases are in use, the least recently used mapping is replaced.

- `autoReplaceTopicAliasSend: true` - Automatically uses existing mappings when sending PUBLISH packets. If a topic name already has an alias registered, the client sends an empty topic name with the alias instead.

These options can be used together for fully automatic topic alias management.

**Receiving PUBLISH with Topic Alias:**

When the client receives a PUBLISH packet with an empty topic name and a topic alias, the library automatically restores the topic name from the mapping. You can check whether the topic name was restored by reading the `topicNameExtracted` property:

```javascript
const pub = client.asPublish(packet);
console.log(`Topic: ${pub.topicName}`);
if (pub.topicNameExtracted) {
    console.log('(topic name was restored from alias mapping)');
}
```

**Important: Topic Alias and Reconnection**

According to the MQTT specification, topic alias mappings are **not** part of Session State. This means all mappings are discarded when the connection closes. However, Session State (which includes unacknowledged QoS 1/2 PUBLISH packets) can persist across connections.

This creates a potential issue: if a QoS 1 or QoS 2 PUBLISH packet was sent with an empty topic name (using a topic alias), and the client reconnects before receiving acknowledgment, the stored packet cannot be resent as-is because the alias mapping no longer exists.

**The library handles this automatically:** When retransmitting stored packets after reconnection, the library reconstructs each packet with the original topic name and removes the topic alias property. This ensures protocol compliance without requiring any user intervention.

---

## Installation

### npm

```bash
npm install @redboltz/mqtt-client-wasm
```

For WebSocket transport in Node.js, also install the `ws` package:

```bash
npm install ws
```

---

## Setup by Platform

### Browser Setup

```javascript
import init, {
    WasmMqttClient,
    WasmMqttConfig,
    WasmPacketType
} from '@redboltz/mqtt-client-wasm';

// Initialize WASM module (required once)
await init();

// Create client
const config = new WasmMqttConfig({
    version: '5.0',           // '3.1.1' or '5.0'
    autoPubResponse: true,    // Auto handle QoS acknowledgments
    autoPingResponse: true,   // Auto respond to PINGREQ
});
const client = new WasmMqttClient(config);

// Connect via WebSocket
await client.connect('wss://broker.example.com:8884/');
```

#### Direct Script Include (No Bundler)

```html
<script type="module">
    import init, { WasmMqttClient, WasmMqttConfig } from './pkg/mqtt_client_wasm.js';
    await init();
    // ... use the client
</script>
```

### Node.js Setup

```javascript
const {
    WasmMqttClient,
    WasmMqttConfig,
    WasmMqttPacket,
    WasmPacketType,
    init,
    createClientWithTransport,
    NodeWebSocketTransport,
    NodeTcpTransport,
    NodeTlsTransport
} = require('@redboltz/mqtt-client-wasm');

// Initialize WASM module (required once)
init();
```

#### Usage

Use `createClientWithTransport()` to create a client with a transport. The WASM client handles state machine, timers, and automatic responses (same API as browser).

```javascript
const fs = require('fs');

// Create transport and client
const transport = new NodeTcpTransport();
const config = new WasmMqttConfig({ version: '5.0' });
const client = createClientWithTransport(config, transport);

// Connect transport AFTER creating client
await transport.connect('broker.example.com', 1883);

// Use the same API as browser
const connectPacket = client.newConnectPacket({
    clientId: 'my-node-client',
    keepAlive: 60,
    cleanSession: true,
});
await client.send(connectPacket);

const connack = await client.recv();
console.log('Connected:', client.asConnack(connack).sessionPresent);

// Subscribe
const packetId = await client.acquirePacketId();
const subPacket = client.newSubscribePacket({
    packetId,
    subscriptions: [{ topic: 'test/#', qos: 1 }],
});
await client.send(subPacket);
const suback = await client.recv();

// Receive messages
while (true) {
    const packet = await client.recv();
    if (packet.packetType() === WasmPacketType.Publish) {
        const pub = client.asPublish(packet);
        console.log(`Received: ${pub.topicName} = ${pub.payload}`);
    }
}
```

#### Transport Types

**TCP Transport:**
```javascript
const transport = new NodeTcpTransport();
await transport.connect('broker.example.com', 1883);
```

**TLS Transport:**
```javascript
const transport = new NodeTlsTransport({
    ca: fs.readFileSync('ca.pem'),
});
await transport.connect('broker.example.com', 8883);
```

**WebSocket Transport (ws://):**
```javascript
const transport = new NodeWebSocketTransport();
await transport.connect('ws://broker.example.com:8080/');
```

**WebSocket Secure Transport (wss://):**
```javascript
const transport = new NodeWebSocketTransport({
    ca: fs.readFileSync('ca.pem'),
});
await transport.connect('wss://broker.example.com:8884/');
```

#### TLS Options (for NodeTlsTransport and NodeWebSocketTransport)

Both `NodeTlsTransport` and `NodeWebSocketTransport` accept a TLS options object in their constructor. These options are passed directly to Node.js TLS module.

| Option | Type | Description |
|--------|------|-------------|
| `ca` | Buffer/string | CA certificate(s) for server verification |
| `cert` | Buffer/string | Client certificate for mutual TLS |
| `key` | Buffer/string | Client private key for mutual TLS |
| `passphrase` | string | Passphrase for encrypted private key |
| `rejectUnauthorized` | boolean | Reject connections with unverified certificates (default: true) |
| `servername` | string | Server name for SNI (Server Name Indication) |
| `minVersion` | string | Minimum TLS version ('TLSv1.2', 'TLSv1.3') |
| `maxVersion` | string | Maximum TLS version |
| `ciphers` | string | Cipher suites to use |

**Example: Mutual TLS Authentication**

```javascript
const transport = new NodeTlsTransport({
    ca: fs.readFileSync('ca.pem'),
    cert: fs.readFileSync('client.crt'),
    key: fs.readFileSync('client.key'),
    passphrase: 'optional-key-passphrase',
});
```

**Example: Custom TLS Configuration**

```javascript
const transport = new NodeWebSocketTransport({
    ca: fs.readFileSync('ca.pem'),
    minVersion: 'TLSv1.2',
    servername: 'broker.example.com',
});
```

---

## Common API (Both Platforms)

The following sections apply to both browser and Node.js environments.

### Configuration Options

```javascript
const config = new WasmMqttConfig({
    version: '5.0',
    autoPubResponse: true,
    autoPingResponse: true,
});
```

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `version` | string | `'3.1.1'` | MQTT version (`'3.1.1'` or `'5.0'`) |
| `pingreqSendIntervalMs` | number | (auto) | Ping interval in ms (omit for auto from keepAlive) |
| `autoPubResponse` | boolean | `true` | Auto handle QoS acknowledgments |
| `autoPingResponse` | boolean | `true` | Auto respond to PINGREQ |
| `autoMapTopicAliasSend` | boolean | `false` | Auto map topic aliases (v5.0) |
| `autoReplaceTopicAliasSend` | boolean | `false` | Auto replace topic with alias (v5.0) |
| `pingrespRecvTimeoutMs` | number | (disabled) | PINGRESP timeout in ms |
| `connectionEstablishTimeoutMs` | number | (disabled) | Connection timeout in ms |
| `shutdownTimeoutMs` | number | (disabled) | Shutdown timeout in ms |

---

## Packet Reference

### Connect

```javascript
const connectPacket = client.newConnectPacket({
    clientId: 'my-client',
    keepAlive: 60,
    cleanSession: true,
    userName: 'user',
    password: 'pass',
    // Will message
    willTopic: 'client/status',
    willPayload: 'offline',
    willQos: 1,
    willRetain: true,
    // v5.0 Properties
    sessionExpiryInterval: 3600,
    receiveMaximum: 65535,
    maximumPacketSize: 1048576,
    topicAliasMaximum: 10,
    requestResponseInformation: true,
    requestProblemInformation: true,
    userProperties: [{ key: 'app', value: 'myapp' }],
    authenticationMethod: 'SCRAM-SHA-256',
    authenticationData: [0x01, 0x02, 0x03],
});

await client.send(connectPacket);
```

#### Connect Options

| Option | Type | Required | Description |
|--------|------|----------|-------------|
| `clientId` | string | Yes | Client identifier |
| `keepAlive` | number | No | Keep alive interval in seconds (default: 0) |
| `cleanSession` | boolean | No | Clean session (v3.1.1) / Clean start (v5.0) |
| `userName` | string | No | Username for authentication |
| `password` | string | No | Password for authentication |
| `willTopic` | string | No | Will message topic |
| `willPayload` | string | No | Will message payload |
| `willQos` | number | No | Will message QoS (0, 1, 2) |
| `willRetain` | boolean | No | Will message retain flag |

#### Connect Properties (v5.0 only)

| Property | Type | Description |
|----------|------|-------------|
| `sessionExpiryInterval` | number | Session expiry interval in seconds |
| `receiveMaximum` | number | Maximum concurrent QoS 1/2 messages |
| `maximumPacketSize` | number | Maximum packet size in bytes |
| `topicAliasMaximum` | number | Maximum topic aliases |
| `requestResponseInformation` | boolean | Request response information from broker |
| `requestProblemInformation` | boolean | Request problem information from broker |
| `userProperties` | array | User properties `[{key, value}, ...]` |
| `authenticationMethod` | string | Authentication method name |
| `authenticationData` | array | Authentication data (byte array) |

---

### Subscribe

```javascript
const packetId = await client.acquirePacketId();
const subscribePacket = client.newSubscribePacket({
    packetId: packetId,
    subscriptions: [{
        topic: 'sensor/#',
        qos: 2,
        // v5.0 options
        noLocal: true,
        retainAsPublished: true,
        retainHandling: 1,
    }],
    // v5.0 Properties
    subscriptionIdentifier: 12345,
    userProperties: [{ key: 'source', value: 'web' }],
});

await client.send(subscribePacket);
```

#### Subscribe Options

| Option | Type | Required | Description |
|--------|------|----------|-------------|
| `packetId` | number | Yes | Packet identifier |
| `subscriptions` | array | Yes | Array of subscription entries |

#### Subscription Entry

| Option | Type | Required | Description |
|--------|------|----------|-------------|
| `topic` | string | Yes | Topic filter |
| `qos` | number | No | Maximum QoS (0, 1, 2, default: 0) |
| `noLocal` | boolean | No | (v5.0) Don't receive own messages |
| `retainAsPublished` | boolean | No | (v5.0) Keep retain flag as published |
| `retainHandling` | number | No | (v5.0) Retain handling: 0=send, 1=send if new, 2=don't send |

#### Subscribe Properties (v5.0 only)

| Property | Type | Description |
|----------|------|-------------|
| `subscriptionIdentifier` | number | Subscription identifier (1-268435455) |
| `userProperties` | array | User properties `[{key, value}, ...]` |

---

### Unsubscribe

```javascript
const packetId = await client.acquirePacketId();
const unsubscribePacket = client.newUnsubscribePacket({
    packetId: packetId,
    topics: ['sensor/#', 'device/+/status'],
    // v5.0 Properties
    userProperties: [{ key: 'reason', value: 'cleanup' }],
});

await client.send(unsubscribePacket);
```

#### Unsubscribe Options

| Option | Type | Required | Description |
|--------|------|----------|-------------|
| `packetId` | number | Yes | Packet identifier |
| `topics` | array | Yes | Array of topic filters to unsubscribe |

#### Unsubscribe Properties (v5.0 only)

| Property | Type | Description |
|----------|------|-------------|
| `userProperties` | array | User properties `[{key, value}, ...]` |

---

### Publish

```javascript
const publishPacket = client.newPublishPacket({
    topicName: 'sensor/temperature',
    payload: '25.5',
    qos: 1,
    retain: false,
    dup: false,
    packetId: await client.acquirePacketId(),  // Required for QoS > 0
    // v5.0 Properties
    payloadFormatIndicator: 1,
    messageExpiryInterval: 3600,
    topicAlias: 1,
    responseTopic: 'response/sensor',
    correlationData: [0x01, 0x02, 0x03],
    contentType: 'application/json',
    userProperties: [{ key: 'unit', value: 'celsius' }],
});

await client.send(publishPacket);
```

#### Publish Options

| Option | Type | Required | Description |
|--------|------|----------|-------------|
| `topicName` | string | Yes | Topic name |
| `payload` | string | No | Payload as UTF-8 string |
| `payloadBytes` | array | No | Payload as byte array (takes precedence over `payload`) |
| `qos` | number | No | QoS level (0, 1, 2, default: 0) |
| `retain` | boolean | No | Retain flag |
| `dup` | boolean | No | Duplicate flag |
| `packetId` | number | No* | Packet identifier (*Required for QoS 1 or 2) |

#### Publish Properties (v5.0 only)

| Property | Type | Description |
|----------|------|-------------|
| `payloadFormatIndicator` | number | Payload format: 0=unspecified, 1=UTF-8 |
| `messageExpiryInterval` | number | Message expiry interval in seconds |
| `topicAlias` | number | Topic alias (1-65535) |
| `responseTopic` | string | Response topic for request/response |
| `correlationData` | array | Correlation data (byte array) |
| `contentType` | string | Content type (MIME type) |
| `userProperties` | array | User properties `[{key, value}, ...]` |

---

### Disconnect

```javascript
const disconnectPacket = client.newDisconnectPacket({
    // v5.0 only options
    reasonCode: 0,
    reasonString: 'Normal disconnection',
    sessionExpiryInterval: 0,
    userProperties: [{ key: 'client', value: 'web' }],
});

await client.send(disconnectPacket);
```

#### Disconnect Options (v5.0 only)

| Option | Type | Description |
|--------|------|-------------|
| `reasonCode` | number | Disconnect reason code (default: 0 = Normal) |
| `reasonString` | string | Human-readable reason string |
| `sessionExpiryInterval` | number | Override session expiry interval |
| `userProperties` | array | User properties `[{key, value}, ...]` |

**Note:** v3.1.1 DISCONNECT has no options - just call `client.newDisconnectPacket({})`.

---

### QoS Response Packets (PUBACK, PUBREC, PUBREL, PUBCOMP)

When `autoPubResponse: false`, you need to manually send QoS responses:

```javascript
// v3.1.1 - only packet_id
const pubackPacket = client.newPubackPacket({ packetId: 1 });

// v5.0 - with optional properties
const pubackPacket = client.newPubackPacket({
    packetId: 1,
    reasonCode: 0,
    reasonString: 'Success',
    userProperties: [{ key: 'info', value: 'processed' }],
});
```

#### QoS Response Options

| Option | Type | Required | Description |
|--------|------|----------|-------------|
| `packetId` | number | Yes | Packet identifier |

#### QoS Response Properties (v5.0 only)

| Property | Type | Description |
|----------|------|-------------|
| `reasonCode` | number | Reason code |
| `reasonString` | string | Human-readable reason |
| `userProperties` | array | User properties `[{key, value}, ...]` |

Available methods:
- `newPubackPacket(options)` - QoS 1 acknowledgment
- `newPubrecPacket(options)` - QoS 2 received
- `newPubrelPacket(options)` - QoS 2 release
- `newPubcompPacket(options)` - QoS 2 complete

---

### Auth (v5.0 only)

```javascript
const authPacket = client.newAuthPacket({
    reasonCode: 0x18,  // Continue authentication
    authenticationMethod: 'SCRAM-SHA-256',
    authenticationData: [0x01, 0x02, 0x03],
    reasonString: 'Continue',
    userProperties: [{ key: 'step', value: '2' }],
});

await client.send(authPacket);
```

#### Auth Options (v5.0 only)

| Option | Type | Description |
|--------|------|-------------|
| `reasonCode` | number | Auth reason code |
| `authenticationMethod` | string | Authentication method name |
| `authenticationData` | array | Authentication data (byte array) |
| `reasonString` | string | Human-readable reason |
| `userProperties` | array | User properties `[{key, value}, ...]` |

---

### Receive Messages

The `client.recv()` API works identically on both browser and Node.js:

```javascript
while (true) {
    const packet = await client.recv();

    switch (packet.packetType()) {
        case WasmPacketType.Connack: {
            const connack = client.asConnack(packet);
            console.log('Connected:', connack.sessionPresent);
            break;
        }
        case WasmPacketType.Publish: {
            const pub = client.asPublish(packet);
            console.log(`Topic: ${pub.topicName}, Payload: ${pub.payload}`);
            break;
        }
        case WasmPacketType.Suback: {
            const suback = client.asSuback(packet);
            console.log(`SUBACK: packetId=${suback.packetId}`);
            break;
        }
        // ... handle other packet types
    }
}
```

---

## Received Packet Fields Reference

When receiving packets via `client.recv()`, you can access fields using the `client.asXxx(packet)` methods. The following sections document all available fields for each packet type.

### Received CONNACK

```javascript
const connack = client.asConnack(packet);
```

| Field | Type | Description |
|-------|------|-------------|
| `sessionPresent` | boolean | Whether a session from a previous connection exists |
| `returnCode` (v3.1.1) | number | Connect return code |
| `reasonCode` (v5.0) | number | Connect reason code |
| `isSuccess()` | boolean | Returns true if connection was accepted |

#### CONNACK Properties (v5.0 only)

| Property | Type | Description |
|----------|------|-------------|
| `sessionExpiryInterval` | number? | Session expiry interval from server |
| `receiveMaximum` | number? | Maximum concurrent QoS 1/2 receives |
| `maximumQos` | number? | Maximum QoS level supported by server |
| `retainAvailable` | boolean? | Whether retain is available |
| `maximumPacketSize` | number? | Maximum packet size |
| `assignedClientIdentifier` | string? | Client ID assigned by server |
| `topicAliasMaximum` | number? | Maximum topic aliases |
| `reasonString` | string? | Human-readable reason |
| `wildcardSubscriptionAvailable` | boolean? | Whether wildcard subscriptions are available |
| `subscriptionIdentifiersAvailable` | boolean? | Whether subscription identifiers are available |
| `sharedSubscriptionAvailable` | boolean? | Whether shared subscriptions are available |
| `serverKeepAlive` | number? | Server-specified keep alive |
| `responseInformation` | string? | Response information |
| `serverReference` | string? | Server reference for redirect |
| `authenticationMethod` | string? | Authentication method |
| `authenticationData` | Uint8Array? | Authentication data |
| `userProperties()` | Array | User properties `[{key, value}, ...]` |

---

### Received PUBLISH

```javascript
const pub = client.asPublish(packet);
```

| Field | Type | Description |
|-------|------|-------------|
| `topicName` | string | Topic name |
| `payload` | string? | Payload as UTF-8 string (null if not valid UTF-8) |
| `payloadBytes()` | Uint8Array | Payload as byte array |
| `qos` | number | QoS level (0, 1, 2) |
| `retain` | boolean | Retain flag |
| `dup` | boolean | Duplicate flag |
| `packetId` | number? | Packet identifier (for QoS > 0) |

#### PUBLISH Properties (v5.0 only)

| Property | Type | Description |
|----------|------|-------------|
| `payloadFormatIndicator` | number? | 0=binary, 1=UTF-8 |
| `messageExpiryInterval` | number? | Message expiry in seconds |
| `topicAlias` | number? | Topic alias used |
| `responseTopic` | string? | Response topic for request/response |
| `correlationData` | Uint8Array? | Correlation data |
| `contentType` | string? | Content type (MIME type) |
| `subscriptionIdentifiers()` | number[] | Subscription identifiers matching this message |
| `userProperties()` | Array | User properties `[{key, value}, ...]` |
| `topicNameExtracted` | boolean | True if topic name was restored from topic alias mapping |

**Note:** `topicNameExtracted` indicates that the received PUBLISH packet had an empty topic name with a topic alias, and the library automatically restored the topic name from the alias mapping.

---

### Received SUBACK

```javascript
const suback = client.asSuback(packet);
```

| Field | Type | Description |
|-------|------|-------------|
| `packetId` | number | Packet identifier |
| `returnCodes()` (v3.1.1) | number[] | Return codes for each subscription |
| `reasonCodes()` (v5.0) | number[] | Reason codes for each subscription |

#### SUBACK Properties (v5.0 only)

| Property | Type | Description |
|----------|------|-------------|
| `reasonString` | string? | Human-readable reason |
| `userProperties()` | Array | User properties `[{key, value}, ...]` |

---

### Received UNSUBACK

```javascript
const unsuback = client.asUnsuback(packet);
```

| Field | Type | Description |
|-------|------|-------------|
| `packetId` | number | Packet identifier |
| `reasonCodes()` (v5.0) | number[] | Reason codes for each topic |

#### UNSUBACK Properties (v5.0 only)

| Property | Type | Description |
|----------|------|-------------|
| `reasonString` | string? | Human-readable reason |
| `userProperties()` | Array | User properties `[{key, value}, ...]` |

---

### Received PUBACK/PUBREC/PUBREL/PUBCOMP

```javascript
const puback = client.asPuback(packet);
const pubrec = client.asPubrec(packet);
const pubrel = client.asPubrel(packet);
const pubcomp = client.asPubcomp(packet);
```

| Field | Type | Description |
|-------|------|-------------|
| `packetId` | number | Packet identifier |
| `reasonCode` (v5.0) | number | Reason code |

#### QoS Response Properties (v5.0 only)

| Property | Type | Description |
|----------|------|-------------|
| `reasonString` | string? | Human-readable reason |
| `userProperties()` | Array | User properties `[{key, value}, ...]` |

---

### Received DISCONNECT (v5.0 only)

```javascript
const disconnect = client.asDisconnect(packet);
```

| Field | Type | Description |
|-------|------|-------------|
| `reasonCode` | number | Disconnect reason code |
| `sessionExpiryInterval` | number? | Session expiry interval |
| `reasonString` | string? | Human-readable reason |
| `serverReference` | string? | Server reference for redirect |
| `userProperties()` | Array | User properties `[{key, value}, ...]` |

---

### Received AUTH (v5.0 only)

```javascript
const auth = client.asAuth(packet);
```

| Field | Type | Description |
|-------|------|-------------|
| `reasonCode` | number | Auth reason code |
| `authenticationMethod` | string? | Authentication method |
| `authenticationData` | Uint8Array? | Authentication data |
| `reasonString` | string? | Human-readable reason |
| `userProperties()` | Array | User properties `[{key, value}, ...]` |

---

## Browser Demo Tools

### 1. Build WASM Package

```bash
wasm-pack build --target web
```

### 2. Start Web Server

```bash
./start_web_server.sh        # Default port 8080
./start_web_server.sh 9000   # Custom port
```

### 3. Open in Browser

Access `http://localhost:8080` to see available tools:

- **Client Tool** (`client.html`): Interactive MQTT client with Connect, Subscribe, Publish UI
- **Sequence Test** (`sequence_test.html`): Automated MQTT packet sequence testing

### Browser Requirements

- Modern browser with WebSocket support
- For `wss://` (secure WebSocket): HTTPS page or localhost
- For `ws://` (plain WebSocket): HTTP page (note: some ports like 10080 are blocked by browsers)

---

## Development

### Build

```bash
# Browser (ES modules)
wasm-pack build --target web

# Node.js (CommonJS)
wasm-pack build --target nodejs --out-dir pkg-nodejs

# Bundlers (npm-style)
wasm-pack build --target bundler
```

### Run Tests

```bash
# Native tests (mock underlying layer)
cargo test --features native

# WASM tests
wasm-pack test --node

# Node.js integration tests
node nodejs/test/integration.test.js

# Full check (fmt, clippy, build, test)
./check.sh
```

---

## License

MIT