Skip to content

Commit 4bf7acd

Browse files
committed
fix discovery monitor behavior and transport option semantics
1 parent 723d2ee commit 4bf7acd

5 files changed

Lines changed: 174 additions & 71 deletions

File tree

docs/clients.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ Discovers printers on the local network using UDP multicast and broadcast.
148148
- **`idleTimeout`** (number): Time to wait after last response (default: 1500ms)
149149
- **`maxRetries`** (number): Maximum retry attempts (default: 3)
150150
- **`useMulticast`** (boolean): Use multicast discovery (default: true)
151-
- **`useBroadcast`** (boolean): Use broadcast discovery (default: true)
151+
- **`useBroadcast`** (boolean): Use subnet broadcast discovery (default: true)
152152
- **`ports`** (number[]): Specific ports to scan (default: [8899, 19000, 48899])
153153

154154
**Returns:** An array of `DiscoveredPrinter` objects with comprehensive printer information.

docs/specs/printer-discovery.md

Lines changed: 6 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1178,53 +1178,14 @@ printers.forEach(printer => {
11781178
| N/A | `eventPort` | New field |
11791179
| N/A | `statusCode` | New field |
11801180

1181-
### Backward Compatibility Layer
1181+
### Backward Compatibility
11821182

1183-
**File:** `src/api/PrinterDiscovery.legacy.ts` (NEW)
1183+
No compatibility shim is provided in this implementation.
11841184

1185-
```typescript
1186-
/**
1187-
* @deprecated Use PrinterDiscovery instead
1188-
*/
1189-
export class FlashForgePrinterDiscovery extends PrinterDiscovery {
1190-
/**
1191-
* @deprecated Use discover() instead
1192-
*/
1193-
public async discoverPrintersAsync(
1194-
timeoutMs = 10000,
1195-
idleTimeoutMs = 1500,
1196-
maxRetries = 3
1197-
): Promise<FlashForgePrinter[]> {
1198-
const printers = await this.discover({
1199-
timeout: timeoutMs,
1200-
idleTimeout: idleTimeoutMs,
1201-
maxRetries
1202-
});
1203-
1204-
// Convert to old format
1205-
return printers.map(toLegacyPrinter);
1206-
}
1207-
}
1208-
1209-
/**
1210-
* @deprecated Use DiscoveredPrinter instead
1211-
*/
1212-
export class FlashForgePrinter {
1213-
public name: string = '';
1214-
public serialNumber: string = '';
1215-
public ipAddress: string = '';
1216-
public isAD5X?: boolean;
1217-
}
1218-
1219-
function toLegacyPrinter(printer: DiscoveredPrinter): FlashForgePrinter {
1220-
const legacy = new FlashForgePrinter();
1221-
legacy.name = printer.name;
1222-
legacy.serialNumber = printer.serialNumber || '';
1223-
legacy.ipAddress = printer.ipAddress;
1224-
legacy.isAD5X = printer.model === PrinterModel.AD5X;
1225-
return legacy;
1226-
}
1227-
```
1185+
This is an intentional breaking change. Consumers must migrate from:
1186+
- `FlashForgePrinterDiscovery` -> `PrinterDiscovery`
1187+
- `FlashForgePrinter` -> `DiscoveredPrinter`
1188+
- `discoverPrintersAsync(timeout, idleTimeout, maxRetries)` -> `discover({ timeout, idleTimeout, maxRetries })`
12281189

12291190
---
12301191

src/api/PrinterDiscovery.test.ts

Lines changed: 119 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
*
44
* Tests protocol parsers (modern 276-byte, legacy 140-byte), model detection,
55
* status mapping, multi-port discovery, timeout handling, deduplication,
6-
* and backward compatibility with legacy API.
6+
* and monitor/event behavior.
77
*/
88
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
9-
import * as dgram from 'node:dgram';
9+
import type * as dgram from 'node:dgram';
1010
import { EventEmitter } from 'node:events';
1111
import { PrinterDiscovery } from './PrinterDiscovery';
1212
import {
@@ -16,7 +16,7 @@ import {
1616
type DiscoveredPrinter,
1717
type DiscoveryOptions,
1818
} from '../models/PrinterDiscovery';
19-
import { InvalidResponseError, SocketCreationError } from './network/DiscoveryErrors';
19+
import { InvalidResponseError } from './network/DiscoveryErrors';
2020

2121
// Suppress logs during tests
2222
const originalConsole = { ...console };
@@ -444,7 +444,7 @@ describe('PrinterDiscovery', () => {
444444

445445
protected async createDiscoverySocket(): Promise<dgram.Socket> {
446446
const mockSocket = new EventEmitter() as dgram.Socket;
447-
mockSocket.bind = vi.fn((port, callback) => {
447+
mockSocket.bind = vi.fn((_port, callback) => {
448448
if (callback) callback();
449449
});
450450
mockSocket.setBroadcast = vi.fn();
@@ -489,7 +489,7 @@ describe('PrinterDiscovery', () => {
489489

490490
protected async createDiscoverySocket(): Promise<dgram.Socket> {
491491
const mockSocket = new EventEmitter() as dgram.Socket;
492-
mockSocket.bind = vi.fn((port, callback) => {
492+
mockSocket.bind = vi.fn((_port, callback) => {
493493
if (callback) callback();
494494
});
495495
mockSocket.setBroadcast = vi.fn();
@@ -540,7 +540,7 @@ describe('PrinterDiscovery', () => {
540540
class TestPrinterDiscovery extends PrinterDiscovery {
541541
protected async createDiscoverySocket(): Promise<dgram.Socket> {
542542
const mockSocket = new EventEmitter() as dgram.Socket;
543-
mockSocket.bind = vi.fn((port, callback) => {
543+
mockSocket.bind = vi.fn((_port, callback) => {
544544
if (callback) callback();
545545
});
546546
mockSocket.setBroadcast = vi.fn();
@@ -567,7 +567,7 @@ describe('PrinterDiscovery', () => {
567567

568568
protected async createDiscoverySocket(): Promise<dgram.Socket> {
569569
const mockSocket = new EventEmitter() as dgram.Socket;
570-
mockSocket.bind = vi.fn((port, callback) => {
570+
mockSocket.bind = vi.fn((_port, callback) => {
571571
if (callback) callback();
572572
});
573573
mockSocket.setBroadcast = vi.fn();
@@ -612,6 +612,96 @@ describe('PrinterDiscovery', () => {
612612

613613
await endPromise;
614614
});
615+
616+
it('should emit end when stopped manually', async () => {
617+
class TestPrinterDiscovery extends PrinterDiscovery {
618+
protected async createDiscoverySocket(): Promise<dgram.Socket> {
619+
const mockSocket = new EventEmitter() as dgram.Socket;
620+
mockSocket.bind = vi.fn((_port, callback) => {
621+
if (callback) callback();
622+
});
623+
mockSocket.setBroadcast = vi.fn();
624+
mockSocket.send = vi.fn();
625+
mockSocket.close = vi.fn();
626+
mockSocket.addMembership = vi.fn();
627+
return mockSocket;
628+
}
629+
}
630+
631+
const discovery = new TestPrinterDiscovery();
632+
const monitor = discovery.monitor({ timeout: 1000, maxRetries: 10 });
633+
634+
const endPromise = new Promise<void>((resolve, reject) => {
635+
const timeoutHandle = setTimeout(() => {
636+
reject(new Error('Timed out waiting for end event'));
637+
}, 500);
638+
639+
monitor.on('error', reject);
640+
monitor.on('end', () => {
641+
clearTimeout(timeoutHandle);
642+
resolve();
643+
});
644+
});
645+
646+
setTimeout(() => {
647+
monitor.stop();
648+
}, 50);
649+
650+
await endPromise;
651+
});
652+
653+
it('should honor idleTimeout after first discovery response', async () => {
654+
class TestPrinterDiscovery extends PrinterDiscovery {
655+
public mockSocket: dgram.Socket | null = null;
656+
657+
protected async createDiscoverySocket(): Promise<dgram.Socket> {
658+
const mockSocket = new EventEmitter() as dgram.Socket;
659+
mockSocket.bind = vi.fn((_port, callback) => {
660+
if (callback) callback();
661+
});
662+
mockSocket.setBroadcast = vi.fn();
663+
mockSocket.send = vi.fn();
664+
mockSocket.close = vi.fn();
665+
mockSocket.addMembership = vi.fn();
666+
this.mockSocket = mockSocket;
667+
return mockSocket;
668+
}
669+
}
670+
671+
const discovery = new TestPrinterDiscovery();
672+
const start = Date.now();
673+
const monitor = discovery.monitor({ timeout: 1000, idleTimeout: 100, maxRetries: 10 });
674+
675+
const endPromise = new Promise<void>((resolve, reject) => {
676+
const timeoutHandle = setTimeout(() => {
677+
reject(new Error('Timed out waiting for idleTimeout end event'));
678+
}, 1000);
679+
680+
monitor.on('error', reject);
681+
monitor.on('end', () => {
682+
clearTimeout(timeoutHandle);
683+
resolve();
684+
});
685+
});
686+
687+
setTimeout(() => {
688+
const mockSocket = discovery.mockSocket;
689+
if (mockSocket) {
690+
const buffer = createModernBuffer({ name: 'Idle Timeout Test' });
691+
mockSocket.emit('message', buffer, {
692+
address: '192.168.1.160',
693+
port: 8899,
694+
family: 'IPv4',
695+
size: 276,
696+
});
697+
}
698+
}, 50);
699+
700+
await endPromise;
701+
const elapsedMs = Date.now() - start;
702+
703+
expect(elapsedMs).toBeLessThan(500);
704+
});
615705
});
616706

617707
describe('Broadcast Address Calculation', () => {
@@ -637,15 +727,18 @@ describe('PrinterDiscovery', () => {
637727
describe('DiscoveryOptions', () => {
638728
// Test discovery class with mocked socket
639729
class TestPrinterDiscovery extends PrinterDiscovery {
730+
public mockSocket: dgram.Socket | null = null;
731+
640732
protected async createDiscoverySocket(): Promise<dgram.Socket> {
641733
const mockSocket = new EventEmitter() as dgram.Socket;
642-
mockSocket.bind = vi.fn((port, callback) => {
734+
mockSocket.bind = vi.fn((_port, callback) => {
643735
if (callback) callback();
644736
});
645737
mockSocket.setBroadcast = vi.fn();
646738
mockSocket.send = vi.fn();
647739
mockSocket.close = vi.fn();
648740
mockSocket.addMembership = vi.fn();
741+
this.mockSocket = mockSocket;
649742
return mockSocket;
650743
}
651744
}
@@ -692,4 +785,22 @@ describe('DiscoveryOptions', () => {
692785
const printers = await discovery.discover(options);
693786
expect(printers).toHaveLength(0);
694787
});
788+
789+
it('should not send packets when multicast and broadcast are both disabled', async () => {
790+
const discovery = new TestPrinterDiscovery();
791+
792+
const options: DiscoveryOptions = {
793+
timeout: 50,
794+
idleTimeout: 25,
795+
maxRetries: 1,
796+
useMulticast: false,
797+
useBroadcast: false,
798+
ports: [8899, 19000, 48899],
799+
};
800+
801+
const printers = await discovery.discover(options);
802+
expect(printers).toHaveLength(0);
803+
expect(discovery.mockSocket).not.toBeNull();
804+
expect((discovery.mockSocket?.send as ReturnType<typeof vi.fn>)).not.toHaveBeenCalled();
805+
});
695806
});

0 commit comments

Comments
 (0)