Skip to content

Commit 1a42197

Browse files
committed
don't throw, just emit error
1 parent 9ab33d2 commit 1a42197

File tree

2 files changed

+68
-121
lines changed

2 files changed

+68
-121
lines changed

src/robot/__tests__/client.spec.ts

Lines changed: 66 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -386,12 +386,7 @@ describe('RobotClient', () => {
386386
});
387387

388388
describe('dial error handling', () => {
389-
interface DisconnectedEventCapture {
390-
events: unknown[];
391-
setupListener: (client: RobotClient) => void;
392-
}
393-
394-
const captureDisconnectedEvents = (): DisconnectedEventCapture => {
389+
const captureDisconnectedEvents = () => {
395390
const events: unknown[] = [];
396391
const setupListener = (client: RobotClient) => {
397392
client.on('disconnected', (event) => {
@@ -421,22 +416,38 @@ describe('RobotClient', () => {
421416
});
422417
};
423418

424-
it('should throw an error when both WebRTC and gRPC connections fail', async () => {
419+
it('should return client instance when WebRTC connection succeeds', async () => {
420+
// Arrange
421+
const client = setupClientMocks();
422+
423+
// Act
424+
const result = await client.dial({
425+
...baseDialConfig,
426+
noReconnect: true,
427+
});
428+
429+
// Assert
430+
expect(result).toBe(client);
431+
});
432+
433+
it('should return client instance even when both WebRTC and gRPC connections fail', async () => {
425434
// Arrange
426435
const client = new RobotClient();
427436
const webrtcError = new Error('WebRTC connection failed');
428-
const grpcError = new Error('gRPC connection failed');
437+
const { events, setupListener } = captureDisconnectedEvents();
429438

430439
vi.mocked(rpcModule.dialWebRTC).mockRejectedValue(webrtcError);
431-
vi.mocked(rpcModule.dialDirect).mockRejectedValue(grpcError);
440+
setupListener(client);
432441

433-
// Act & Assert
434-
await expect(
435-
client.dial({
436-
...baseDialConfig,
437-
noReconnect: true,
438-
})
439-
).rejects.toThrow('Failed to connect via all methods');
442+
// Act
443+
const result = await client.dial({
444+
...baseDialConfig,
445+
noReconnect: true,
446+
});
447+
448+
// Assert
449+
expect(result).toBe(client);
450+
expect(events.length).toBeGreaterThanOrEqual(2);
440451
});
441452

442453
it('should emit DISCONNECTED event with error when WebRTC fails', async () => {
@@ -449,14 +460,10 @@ describe('RobotClient', () => {
449460
setupListener(client);
450461

451462
// Act
452-
try {
453-
await client.dial({
454-
...baseDialConfig,
455-
noReconnect: true,
456-
});
457-
} catch {
458-
// Expected to throw
459-
}
463+
await client.dial({
464+
...baseDialConfig,
465+
noReconnect: true,
466+
});
460467

461468
// Assert
462469
expect(events.length).toBeGreaterThanOrEqual(2);
@@ -476,14 +483,10 @@ describe('RobotClient', () => {
476483
setupListener(client);
477484

478485
// Act
479-
try {
480-
await client.dial({
481-
host: TEST_HOST,
482-
noReconnect: true,
483-
});
484-
} catch {
485-
// Expected to throw
486-
}
486+
await client.dial({
487+
host: TEST_HOST,
488+
noReconnect: true,
489+
});
487490

488491
// Assert
489492
expect(events.length).toBeGreaterThanOrEqual(1);
@@ -492,83 +495,39 @@ describe('RobotClient', () => {
492495
expect((errorEvent as { error: Error }).error).toBeInstanceOf(Error);
493496
});
494497

495-
it('should emit DISCONNECTED events even for non-Error objects', async () => {
498+
it('should emit both errors as DISCONNECTED events when both connections fail', async () => {
496499
// Arrange
497500
const client = new RobotClient();
498-
const webrtcError = 'string error';
501+
const webrtcError = new Error('WebRTC connection failed');
499502
const { events, setupListener } = captureDisconnectedEvents();
500503

501504
vi.mocked(rpcModule.dialWebRTC).mockRejectedValue(webrtcError);
502505
setupListener(client);
503506

504507
// Act
505-
try {
506-
await client.dial({
507-
...baseDialConfig,
508-
noReconnect: true,
509-
});
510-
} catch {
511-
// Expected to throw
512-
}
508+
await client.dial({
509+
...baseDialConfig,
510+
noReconnect: true,
511+
});
513512

514513
// Assert
515514
expect(events.length).toBeGreaterThanOrEqual(2);
516-
const errorEvent = findEventWithError(events);
517-
expect(errorEvent).toBeDefined();
518-
expect((errorEvent as { error: Error }).error).toBeInstanceOf(Error);
519-
});
520-
521-
it('should include both errors in the thrown error cause', async () => {
522-
// Arrange
523-
const client = new RobotClient();
524-
const webrtcError = new Error('WebRTC connection failed');
525-
526-
vi.mocked(rpcModule.dialWebRTC).mockRejectedValue(webrtcError);
527-
528-
// Act
529-
let caughtError: Error | undefined;
530-
try {
531-
await client.dial({
532-
...baseDialConfig,
533-
noReconnect: true,
534-
});
535-
} catch (error) {
536-
caughtError = error as Error;
537-
}
538-
539-
// Assert
540-
expect(caughtError).toBeDefined();
541-
expect(caughtError).toBeInstanceOf(Error);
542-
expect(caughtError!.message).toBe('Failed to connect via all methods');
543-
expect(caughtError!.cause).toBeDefined();
544-
expect(Array.isArray(caughtError!.cause)).toBe(true);
545-
const causes = caughtError!.cause as Error[];
546-
expect(causes).toHaveLength(2);
547-
expect(causes[0]).toBe(webrtcError);
548-
expect(causes[1]).toBeInstanceOf(Error);
515+
const webrtcEvent = findEventWithError(
516+
events,
517+
'WebRTC connection failed'
518+
);
519+
expect(webrtcEvent).toBeDefined();
520+
expect(webrtcEvent).toMatchObject({ error: webrtcError });
549521
});
550522

551-
it('should handle non-Error objects thrown from dial methods', async () => {
523+
it('should convert non-Error objects to Errors and emit them', async () => {
552524
// Arrange
553525
const client = new RobotClient();
554526
const webrtcError = 'string error';
555-
const grpcError = { message: 'object error' };
527+
const { events, setupListener } = captureDisconnectedEvents();
556528

557529
vi.mocked(rpcModule.dialWebRTC).mockRejectedValue(webrtcError);
558-
vi.mocked(rpcModule.dialDirect).mockRejectedValue(grpcError);
559-
560-
// Act & Assert
561-
await expect(
562-
client.dial({
563-
...baseDialConfig,
564-
noReconnect: true,
565-
})
566-
).rejects.toThrow('Failed to connect via all methods');
567-
});
568-
569-
it('should not throw when WebRTC succeeds', async () => {
570-
// Arrange
571-
const client = setupClientMocks();
530+
setupListener(client);
572531

573532
// Act
574533
const result = await client.dial({
@@ -578,45 +537,42 @@ describe('RobotClient', () => {
578537

579538
// Assert
580539
expect(result).toBe(client);
540+
expect(events.length).toBeGreaterThanOrEqual(1);
541+
const errorEvent = findEventWithError(events);
542+
expect(errorEvent).toBeDefined();
543+
expect((errorEvent as { error: Error }).error).toBeInstanceOf(Error);
544+
expect((errorEvent as { error: Error }).error.message).toBe(
545+
'string error'
546+
);
581547
});
582548

583-
it('should not throw when gRPC succeeds after WebRTC fails', async () => {
549+
it('should fallback to gRPC when WebRTC fails and emit WebRTC error', async () => {
584550
// Arrange
585551
const client = new RobotClient();
586552
const webrtcError = new Error('WebRTC connection failed');
553+
const { events, setupListener } = captureDisconnectedEvents();
587554

555+
setupListener(client);
588556
vi.mocked(rpcModule.dialWebRTC).mockRejectedValue(webrtcError);
589557
vi.mocked(rpcModule.dialDirect).mockResolvedValue(
590558
createMockRobotServiceTransport()
591559
);
592560

593561
// Act
594-
// Use a local host so dialDirect validation passes
595562
const result = await client.dial({
563+
...baseDialConfig,
596564
host: TEST_LOCAL_HOST,
597565
noReconnect: true,
598566
});
599567

600568
// Assert
601569
expect(result).toBe(client);
602-
});
603-
604-
it('should not throw when only gRPC dial is attempted (no WebRTC config)', async () => {
605-
// Arrange
606-
const client = new RobotClient();
607-
vi.mocked(rpcModule.dialDirect).mockResolvedValue(
608-
createMockRobotServiceTransport()
570+
expect(events.length).toBeGreaterThanOrEqual(1);
571+
const webrtcEvent = findEventWithError(
572+
events,
573+
'WebRTC connection failed'
609574
);
610-
611-
// Act
612-
// Use a local host so dialDirect validation passes
613-
const result = await client.dial({
614-
host: TEST_LOCAL_HOST,
615-
noReconnect: true,
616-
});
617-
618-
// Assert
619-
expect(result).toBe(client);
575+
expect(webrtcEvent).toBeDefined();
620576
});
621577
});
622578
});

src/robot/client.ts

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -632,15 +632,12 @@ export class RobotClient extends EventDispatcher implements Robot {
632632

633633
this.currentRetryAttempt = 0;
634634

635-
let dialWebRTCError: Error | undefined;
636-
let dialDirectError: Error | undefined;
637-
638635
// Try to dial via WebRTC first.
639636
if (isDialWebRTCConf(conf) && !conf.reconnectAbortSignal?.abort) {
640637
try {
641638
return await backOff(async () => this.dialWebRTC(conf), backOffOpts);
642639
} catch (error) {
643-
dialWebRTCError =
640+
const dialWebRTCError =
644641
error instanceof Error ? error : new Error(String(error));
645642
// eslint-disable-next-line no-console
646643
console.debug('Failed to connect via WebRTC', dialWebRTCError);
@@ -656,7 +653,7 @@ export class RobotClient extends EventDispatcher implements Robot {
656653
try {
657654
return await backOff(async () => this.dialDirect(conf), backOffOpts);
658655
} catch (error) {
659-
dialDirectError =
656+
const dialDirectError =
660657
error instanceof Error ? error : new Error(String(error));
661658
// eslint-disable-next-line no-console
662659
console.debug('Failed to connect via gRPC', dialDirectError);
@@ -666,12 +663,6 @@ export class RobotClient extends EventDispatcher implements Robot {
666663
}
667664
}
668665

669-
if (dialWebRTCError && dialDirectError) {
670-
throw new Error('Failed to connect via all methods', {
671-
cause: [dialWebRTCError, dialDirectError],
672-
});
673-
}
674-
675666
return this;
676667
}
677668

0 commit comments

Comments
 (0)