or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

apdu-communication.mdconfiguration.mddevice-management.mderror-handling.mdevents-lifecycle.mdindex.md
tile.json

events-lifecycle.mddocs/

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

```