0
# Events and Lifecycle Management
1
2
Event-driven architecture for handling device state changes, connection management, and cleanup operations. The Transport class extends EventEmitter to provide real-time notifications about device status and connection changes.
3
4
## Capabilities
5
6
### Event Listening
7
8
Add event listeners to monitor transport and device state changes.
9
10
```typescript { .api }
11
/**
12
* Listen to an event on an instance of transport
13
* Transport implementation can have specific events. Common events:
14
* - "disconnect": triggered if Transport is disconnected
15
* - "unresponsive": triggered when device becomes unresponsive
16
* - "responsive": triggered when device becomes responsive again
17
* @param eventName Name of the event to listen for
18
* @param cb Callback function to handle the event
19
*/
20
on(eventName: string, cb: Function): void;
21
```
22
23
**Usage Example:**
24
25
```javascript
26
import Transport from "@ledgerhq/hw-transport";
27
28
const transport = await MyTransport.create();
29
30
// Listen for disconnect events
31
transport.on("disconnect", () => {
32
console.log("Device disconnected unexpectedly");
33
// Clean up resources, notify user, attempt reconnection
34
handleDisconnection();
35
});
36
37
// Listen for device responsiveness
38
transport.on("unresponsive", () => {
39
console.warn("Device is not responding - please check your device");
40
showUserWarning("Device unresponsive");
41
});
42
43
transport.on("responsive", () => {
44
console.log("Device is responding again");
45
hideUserWarning();
46
});
47
48
// Custom events from specific transport implementations
49
transport.on("device-locked", () => {
50
console.log("Device is locked - user needs to unlock");
51
});
52
```
53
54
### Event Removal
55
56
Remove specific event listeners to prevent memory leaks and unwanted callbacks.
57
58
```typescript { .api }
59
/**
60
* Stop listening to an event on an instance of transport
61
* @param eventName Name of the event to stop listening for
62
* @param cb The same callback function that was passed to on()
63
*/
64
off(eventName: string, cb: Function): void;
65
```
66
67
**Usage Example:**
68
69
```javascript
70
const transport = await MyTransport.create();
71
72
// Define event handler
73
const disconnectHandler = () => {
74
console.log("Device disconnected");
75
// Handle disconnection
76
};
77
78
// Add listener
79
transport.on("disconnect", disconnectHandler);
80
81
// Later, remove the specific listener
82
transport.off("disconnect", disconnectHandler);
83
84
// Remove all listeners for an event (not part of public API, but possible)
85
transport._events.removeAllListeners("disconnect");
86
```
87
88
### Event Emission (Internal)
89
90
Emit events from transport implementations. This method is used internally by transport implementations and should not be called directly by applications.
91
92
```typescript { .api }
93
/**
94
* Emit an event to all registered listeners (internal use)
95
* @param event Name of the event to emit
96
* @param args Arguments to pass to event listeners
97
*/
98
emit(event: string, ...args: any): void;
99
```
100
101
### Connection Cleanup
102
103
Properly close the transport connection and clean up resources.
104
105
```typescript { .api }
106
/**
107
* Close the exchange with the device
108
* @returns Promise that resolves when the transport is closed
109
*/
110
close(): Promise<void>;
111
```
112
113
**Usage Example:**
114
115
```javascript
116
const transport = await MyTransport.create();
117
118
try {
119
// Use the transport
120
transport.setScrambleKey("BTC");
121
const response = await transport.send(0xB0, 0x01, 0x00, 0x00);
122
123
} finally {
124
// Always close the transport
125
await transport.close();
126
console.log("Transport closed successfully");
127
}
128
```
129
130
## Common Events
131
132
### Standard Transport Events
133
134
These events are supported by the base Transport class:
135
136
#### disconnect
137
Emitted when the device is unexpectedly disconnected or becomes unavailable.
138
139
```javascript
140
transport.on("disconnect", () => {
141
// Device was unplugged or connection lost
142
// Stop ongoing operations and clean up
143
});
144
```
145
146
#### unresponsive
147
Emitted when a device stops responding during an operation (after `unresponsiveTimeout`).
148
149
```javascript
150
transport.on("unresponsive", () => {
151
// Device is not responding to commands
152
// Show user feedback, but don't disconnect yet
153
});
154
```
155
156
#### responsive
157
Emitted when a previously unresponsive device starts responding again.
158
159
```javascript
160
transport.on("responsive", () => {
161
// Device is responding normally again
162
// Hide unresponsive warnings
163
});
164
```
165
166
### Transport-Specific Events
167
168
Different transport implementations may emit additional events:
169
170
```javascript
171
// WebUSB transport might emit:
172
transport.on("device-selected", (device) => {
173
console.log("User selected device:", device.productName);
174
});
175
176
// Bluetooth transport might emit:
177
transport.on("pairing-request", () => {
178
console.log("Device is requesting pairing");
179
});
180
181
transport.on("battery-low", (level) => {
182
console.log("Device battery low:", level + "%");
183
});
184
```
185
186
## Lifecycle Management Patterns
187
188
### Basic Connection Lifecycle
189
190
```javascript
191
async function performDeviceOperation() {
192
let transport = null;
193
194
try {
195
// 1. Create connection
196
transport = await MyTransport.create();
197
198
// 2. Set up event handlers
199
transport.on("disconnect", () => {
200
console.log("Connection lost during operation");
201
});
202
203
// 3. Configure transport
204
transport.setScrambleKey("BTC");
205
transport.setExchangeTimeout(30000);
206
207
// 4. Perform operations
208
const result = await transport.send(0xE0, 0x40, 0x00, 0x00);
209
210
return result;
211
212
} finally {
213
// 5. Always clean up
214
if (transport) {
215
await transport.close();
216
}
217
}
218
}
219
```
220
221
### Persistent Connection Management
222
223
```javascript
224
class LedgerDeviceManager {
225
constructor() {
226
this.transport = null;
227
this.isConnected = false;
228
}
229
230
async connect() {
231
if (this.transport) {
232
await this.disconnect();
233
}
234
235
this.transport = await MyTransport.create();
236
this.setupEventHandlers();
237
this.isConnected = true;
238
239
console.log("Connected to device");
240
}
241
242
setupEventHandlers() {
243
this.transport.on("disconnect", () => {
244
this.isConnected = false;
245
this.transport = null;
246
console.log("Device disconnected");
247
248
// Attempt reconnection after delay
249
setTimeout(() => this.attemptReconnection(), 2000);
250
});
251
252
this.transport.on("unresponsive", () => {
253
console.warn("Device unresponsive");
254
// Don't reconnect immediately, wait for responsive event
255
});
256
257
this.transport.on("responsive", () => {
258
console.log("Device responsive again");
259
});
260
}
261
262
async attemptReconnection() {
263
if (this.isConnected) return;
264
265
try {
266
await this.connect();
267
console.log("Reconnection successful");
268
} catch (error) {
269
console.error("Reconnection failed:", error);
270
// Try again after longer delay
271
setTimeout(() => this.attemptReconnection(), 5000);
272
}
273
}
274
275
async disconnect() {
276
if (this.transport) {
277
await this.transport.close();
278
this.transport = null;
279
}
280
this.isConnected = false;
281
}
282
283
async executeCommand(cla, ins, p1, p2, data) {
284
if (!this.isConnected || !this.transport) {
285
throw new Error("Device not connected");
286
}
287
288
return await this.transport.send(cla, ins, p1, p2, data);
289
}
290
}
291
```
292
293
### Resource Management with Timeout
294
295
```javascript
296
async function withTransportTimeout(operation, timeoutMs = 30000) {
297
let transport = null;
298
299
const operationPromise = async () => {
300
transport = await MyTransport.create();
301
return await operation(transport);
302
};
303
304
const timeoutPromise = new Promise((_, reject) => {
305
setTimeout(() => reject(new Error("Operation timeout")), timeoutMs);
306
});
307
308
try {
309
return await Promise.race([operationPromise(), timeoutPromise]);
310
} finally {
311
if (transport) {
312
await transport.close();
313
}
314
}
315
}
316
317
// Usage
318
const result = await withTransportTimeout(async (transport) => {
319
transport.setScrambleKey("ETH");
320
return await transport.send(0xE0, 0x02, 0x00, 0x00);
321
}, 15000);
322
```
323
324
### Event-Driven Application Architecture
325
326
```javascript
327
class LedgerEventManager extends EventEmitter {
328
constructor() {
329
super();
330
this.transport = null;
331
}
332
333
async init() {
334
this.transport = await MyTransport.create();
335
336
// Forward transport events to application
337
this.transport.on("disconnect", () => {
338
this.emit("device-disconnected");
339
});
340
341
this.transport.on("unresponsive", () => {
342
this.emit("device-unresponsive");
343
});
344
345
this.transport.on("responsive", () => {
346
this.emit("device-responsive");
347
});
348
349
this.emit("device-connected", this.transport.deviceModel);
350
}
351
}
352
353
// Application usage
354
const ledger = new LedgerEventManager();
355
356
ledger.on("device-connected", (deviceModel) => {
357
console.log("Ledger connected:", deviceModel.productName);
358
updateUI({ connected: true, device: deviceModel });
359
});
360
361
ledger.on("device-disconnected", () => {
362
console.log("Ledger disconnected");
363
updateUI({ connected: false });
364
});
365
366
ledger.on("device-unresponsive", () => {
367
showWarning("Device is not responding. Please check your device.");
368
});
369
370
ledger.on("device-responsive", () => {
371
hideWarning();
372
});
373
374
await ledger.init();
375
```
376
377
## Error Handling in Lifecycle
378
379
### Graceful Error Recovery
380
381
```javascript
382
async function robustDeviceOperation(operation) {
383
const maxRetries = 3;
384
let attempt = 0;
385
386
while (attempt < maxRetries) {
387
let transport = null;
388
389
try {
390
transport = await MyTransport.create();
391
return await operation(transport);
392
393
} catch (error) {
394
attempt++;
395
396
if (error.name === "DisconnectedDevice") {
397
console.log(`Attempt ${attempt}: Device disconnected, retrying...`);
398
} else if (error.name === "TransportRaceCondition") {
399
console.log(`Attempt ${attempt}: Race condition, retrying...`);
400
} else {
401
// Non-recoverable error
402
throw error;
403
}
404
405
// Wait before retry
406
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
407
408
} finally {
409
if (transport) {
410
try {
411
await transport.close();
412
} catch (closeError) {
413
console.warn("Error closing transport:", closeError);
414
}
415
}
416
}
417
}
418
419
throw new Error(`Operation failed after ${maxRetries} attempts`);
420
}
421
```