rxprotoplex-pingpong
Version:
A ping-pong mechanism using rxprotoplex and RxJS for managing reliable connections.
288 lines (241 loc) • 10.7 kB
JavaScript
import { test, solo } from 'brittle';
import { createPlexPair, destroy } from 'rxprotoplex';
import { plexPingPong } from './lib/plexPingPong.js'; // Adjust the path as needed
test('Ping-Pong Communication between Initiator and Listener', async (t) => {
// Create a pair of plex instances
const [initiatorPlex, listenerPlex] = createPlexPair();
// Start ping-pong on both sides with default behavior (no custom handler)
const initiatorEvents$ = plexPingPong(initiatorPlex, true, { channel: '$PINGPONG$', interval: 500 });
const listenerEvents$ = plexPingPong(listenerPlex, false, { channel: '$PINGPONG$', interval: 500 });
let initiatorErrorCaught = false;
let listenerErrorCaught = false;
// Create separate Promises for initiator and listener errors
const initiatorErrorPromise = new Promise((resolve) => {
initiatorEvents$.subscribe({
next: (event) => {
t.ok(['ping', 'pong'].includes(event.type), `Initiator received event type: ${event.type}`);
},
error: (err) => {
initiatorErrorCaught = true;
t.ok(err, `Initiator encountered expected error: ${err.message}`);
resolve();
},
complete: () => {
// If complete is called without error, resolve the Promise
resolve();
}
});
});
const listenerErrorPromise = new Promise((resolve) => {
listenerEvents$.subscribe({
next: (event) => {
t.ok(['ping', 'pong'].includes(event.type), `Listener received event type: ${event.type}`);
},
error: (err) => {
listenerErrorCaught = true;
t.ok(err, `Listener encountered expected error: ${err.message}`);
resolve();
},
complete: () => {
// If complete is called without error, resolve the Promise
resolve();
}
});
});
// Wait for a few ping-pong cycles
await new Promise((resolve) => setTimeout(resolve, 700));
// Destroy the listener plex to simulate stream closure
destroy(listenerPlex);
// Wait for the disconnection to propagate and errors to be emitted
try {
await Promise.race([
Promise.all([initiatorErrorPromise, listenerErrorPromise]),
new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout waiting for errors')), 2000))
]);
} catch (err) {
t.fail(err.message);
}
// Assert that errors were caught
t.ok(initiatorErrorCaught, 'Initiator caught expected error');
t.ok(listenerErrorCaught, 'Listener caught expected error');
// Pass the test after ensuring errors are caught
t.pass('Ping-Pong communication and disconnection handled correctly');
// Teardown: ensure all plex instances are destroyed
t.teardown(() => {
destroy(initiatorPlex);
destroy(listenerPlex);
});
});
test('Heartbeat Timeout Triggers Disconnection', async (t) => {
// Create a pair of plex instances
const [initiatorPlex, listenerPlex] = createPlexPair();
// Start ping-pong on both sides with a short interval (no custom handler)
const initiatorEvents$ = plexPingPong(initiatorPlex, true, { channel: '$PINGPONG$', interval: 500 });
const listenerEvents$ = plexPingPong(listenerPlex, false, { channel: '$PINGPONG$', interval: 500 });
let initiatorErrorCaught = false;
let listenerErrorCaught = false;
// Create separate Promises for initiator and listener errors
const initiatorErrorPromise = new Promise((resolve) => {
initiatorEvents$.subscribe({
next: (event) => {
t.ok(['ping', 'pong'].includes(event.type), `Initiator received event type: ${event.type}`);
},
error: (err) => {
initiatorErrorCaught = true;
t.ok(err, `Initiator encountered expected error: ${err.message}`);
resolve();
},
complete: () => {
resolve();
}
});
});
const listenerErrorPromise = new Promise((resolve) => {
listenerEvents$.subscribe({
next: (event) => {
t.ok(['ping', 'pong'].includes(event.type), `Listener received event type: ${event.type}`);
},
error: (err) => {
listenerErrorCaught = true;
t.ok(err, `Listener encountered expected error: ${err.message}`);
resolve();
},
complete: () => {
resolve();
}
});
});
// Allow initial exchanges
await new Promise((resolve) => setTimeout(resolve, 750));
// Simulate missing pongs by destroying the listener plex
destroy(listenerPlex);
// Wait for the heartbeat to detect missed pong and trigger disconnection
try {
await Promise.race([
Promise.all([initiatorErrorPromise, listenerErrorPromise]),
new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout waiting for errors')), 2000))
]);
} catch (err) {
t.fail(err.message);
}
// Assert that errors were caught
t.ok(initiatorErrorCaught, 'Initiator caught expected error');
t.ok(listenerErrorCaught, 'Listener caught expected error');
// Pass the test after ensuring errors are caught
t.pass('Heartbeat timeout correctly triggered disconnection');
// Teardown: ensure all plex instances are destroyed
t.teardown(() => {
destroy(initiatorPlex);
destroy(listenerPlex);
});
});
test('Other side doesn\'t support ping pong', async (t) => {
t.comment("The side that does will attempt to reconnect at least 3 times.");
// Create a pair of plex instances
const [initiatorPlex, listenerPlex] = createPlexPair();
// Flags to track destruction
let initiatorDestroyed = false;
let listenerDestroyed = false;
// Mock destroy methods for testing
initiatorPlex.close$.subscribe((error) => {
initiatorDestroyed = true;
t.ok(true, `Initiator Plex destroyed due to: ${error?.message || 'No specific error message'}`);
});
listenerPlex.close$.subscribe(error => {
listenerDestroyed = true;
t.ok(true, `Listener Plex destroyed due to: ${error?.message || 'No specific error message'}`);
})
// Start ping-pong on the initiator only
const initiatorEvents$ = plexPingPong(initiatorPlex, true, {
channel: '$PINGPONG$',
interval: 500,
connectionTimeout: 200,
});
let errorEmitted = false;
// Promise to handle initiator's error
const errorPromise = new Promise((resolve) => {
initiatorEvents$.subscribe({
next: (event) => {
t.ok(['ping', 'pong'].includes(event.type), `Initiator received event type: ${event.type}`);
},
error: (err) => {
errorEmitted = true;
t.ok(err, `Initiator encountered expected error: ${err.message}`);
resolve();
},
complete: () => {
t.fail('Observable completed unexpectedly');
resolve();
},
});
});
// Wait for error to propagate
await errorPromise;
// Validate results
t.ok(errorEmitted, 'Error was emitted due to lack of pong response');
t.ok(initiatorDestroyed, 'Initiator Plex was destroyed as expected');
t.ok(listenerDestroyed, 'Listener Plex was destroyed as expected');
// Teardown
t.teardown(() => {
destroy(initiatorPlex);
destroy(listenerPlex);
});
});
test('Manual Disconnection via Unsubscribe Method', async (t) => {
// Create a pair of plex instances
const [initiatorPlex, listenerPlex] = createPlexPair();
// Start ping-pong on both sides with default behavior
const initiatorEvents$ = plexPingPong(initiatorPlex, true, { channel: '$PINGPONG$', interval: 1000 });
const listenerEvents$ = plexPingPong(listenerPlex, false, { channel: '$PINGPONG$', interval: 1000 });
let initiatorErrorCaught = false;
let listenerErrorCaught = false;
// Promises that will resolve when either error or complete events are emitted
const initiatorErrorPromise = new Promise((resolve) => {
const subscription = initiatorEvents$.subscribe({
next: (event) => t.ok(['ping', 'pong'].includes(event.type), `Initiator received event type: ${event.type}`),
error: (err) => {
initiatorErrorCaught = true;
t.ok(err, `Initiator encountered expected error: ${err.message}`);
resolve();
},
complete: () => {
t.pass('Initiator observable completed');
resolve();
}
});
// Ensure cleanup of subscription in case of any errors
t.teardown(() => subscription.unsubscribe());
});
const listenerErrorPromise = new Promise((resolve) => {
const subscription = listenerEvents$.subscribe({
next: (event) => t.ok(['ping', 'pong'].includes(event.type), `Listener received event type: ${event.type}`),
error: (err) => {
listenerErrorCaught = true;
t.ok(err, `Listener encountered expected error: ${err.message}`);
resolve();
},
complete: () => {
t.pass('Listener observable completed');
resolve();
}
});
// Ensure cleanup of subscription in case of any errors
t.teardown(() => subscription.unsubscribe());
});
// Allow some exchanges before manually disconnecting
await new Promise((resolve) => setTimeout(resolve, 1500));
// Manually destroy both plex instances to simulate disconnection
destroy(initiatorPlex);
destroy(listenerPlex);
// Wait for both promises to resolve on error or complete
await Promise.all([initiatorErrorPromise, listenerErrorPromise]);
// Assert that errors were caught (or completions if no errors occurred)
t.ok(initiatorErrorCaught || true, 'Initiator observable has either errored or completed');
t.ok(listenerErrorCaught || true, 'Listener observable has either errored or completed');
t.pass('Manual disconnection via unsubscribe method works correctly');
// Teardown: ensure all plex instances are destroyed
t.teardown(() => {
destroy(initiatorPlex);
destroy(listenerPlex);
});
});