feat(websockets): add real-time payment notifications via Socket.IO (fix #430) - #441
feat(websockets): add real-time payment notifications via Socket.IO (fix #430)#441RedheadedProgrammer wants to merge 4 commits into
Conversation
|
@RedheadedProgrammer is attempting to deploy a commit to the Abdulazeem's projects Team on Vercel. A member of the Team first needs to authorize it. |
|
@RedheadedProgrammer Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
|
Fix backend build error |
2034bc3 to
bcabc0a
Compare
|
Fix backend build error |
|
The backend build is failing on one test, The test disconnects the publisher on the line right after publisherSocket.emit('payment', { ... });
publisherSocket.disconnect();Socket.IO opens on HTTP long-polling and upgrades to WebSocket a moment later, so that The --- a/stellar-payment-platform/src/socketManager.js
+++ b/stellar-payment-platform/src/socketManager.js
@@ -18,17 +18,25 @@ function initSocketServer(httpServer, corsOptions = {}) {
io.on('connection', (socket) => {
logger.info(`Socket connected: ${socket.id}`);
- socket.on('authenticate', (payload) => {
+ // The optional ack lets a client wait until it is actually subscribed.
+ // Without it a client that emits `authenticate` and immediately expects
+ // events can miss any payment that arrives before the room join lands.
+ socket.on('authenticate', async (payload, ack) => {
+ let subscribed = false;
try {
const address = payload && typeof payload.address === 'string' ? payload.address : null;
if (address) {
- socket.join(address);
+ await socket.join(address);
socket.address = address;
+ subscribed = true;
logger.info(`Socket ${socket.id} joined room for ${address}`);
}
} catch (err) {
logger.error('Socket authenticate error', err);
}
+ if (typeof ack === 'function') {
+ ack({ subscribed });
+ }
});
socket.on('payment', (payload) => {--- a/stellar-payment-platform/tests/socket.test.js
+++ b/stellar-payment-platform/tests/socket.test.js
@@ -19,6 +19,7 @@ jest.mock('../src/logger', () => ({
describe('Socket.IO real-time notification system', () => {
let server;
let clientSocket;
+ let publisherSocket;
let port;
beforeAll((done) => {
@@ -31,9 +32,6 @@ describe('Socket.IO real-time notification system', () => {
});
afterAll((done) => {
- if (clientSocket && clientSocket.connected) {
- clientSocket.disconnect();
- }
closeSocketServer();
server.close(done);
});
@@ -44,27 +42,54 @@ describe('Socket.IO real-time notification system', () => {
});
afterEach(() => {
- clientSocket.disconnect();
+ for (const socket of [clientSocket, publisherSocket]) {
+ if (socket && socket.connected) socket.disconnect();
+ }
+ publisherSocket = null;
});
test('should authenticate and join the address room and receive payment events', (done) => {
const testAddress = 'GAPUQZH3WZUXHEMUGZN5ZYU4D4GHCFEMOGUINU6MF345GBD2QXNYYIEQ';
- clientSocket.emit('authenticate', { address: testAddress });
- setTimeout(() => {
- clientSocket.on('payment', (paymentData) => {
- expect(paymentData).toEqual({ amount: '100', asset: 'XLM' });
- done();
- });
+ clientSocket.on('payment', (paymentData) => {
+ expect(paymentData).toEqual({ amount: '100', asset: 'XLM' });
+ done();
+ });
- const publisherSocket = ioClient(`http://localhost:${port}`);
+ // The ack fires after the server has joined the room, so the publish below
+ // cannot outrun the subscription.
+ clientSocket.emit('authenticate', { address: testAddress }, () => {
+ publisherSocket = ioClient(`http://localhost:${port}`);
publisherSocket.on('connect', () => {
publisherSocket.emit('payment', {
address: testAddress,
payment: { amount: '100', asset: 'XLM' },
});
- publisherSocket.disconnect();
+ // Disconnecting here would close the transport before the packet is
+ // flushed and drop the event; afterEach tears the socket down instead.
});
- }, 200);
+ });
+ });
+
+ test('does not deliver payments for an address the client did not subscribe to', (done) => {
+ const subscribed = 'GAPUQZH3WZUXHEMUGZN5ZYU4D4GHCFEMOGUINU6MF345GBD2QXNYYIEQ';
+ const other = 'GBDQD3WTQ6W2VQ2W4V74UZ5WYF6B72GZ6EHD7I3L3WYH357Y4K5H3E4W';
+
+ clientSocket.on('payment', () => {
+ done(new Error('received a payment addressed to another account'));
+ });
+
+ clientSocket.emit('authenticate', { address: subscribed }, () => {
+ publisherSocket = ioClient(`http://localhost:${port}`);
+ publisherSocket.on('connect', () => {
+ publisherSocket.emit('payment', {
+ address: other,
+ payment: { amount: '100', asset: 'XLM' },
+ });
+ // Round-trip through the same socket: once this ack returns the server
+ // has already handled the payment above, so nothing is still in flight.
+ publisherSocket.emit('authenticate', { address: other }, () => done());
+ });
+ });
});
});With these the suite is 351/351 across repeated runs. The ack argument is optional, so clients that call |
|
Still the same thing |
|
fix backend build error |
Closes #430
Short description:
How to test locally:
socket.emit('authenticate', { address: '<TEST_STELLAR_ADDRESS>' })
socket.on('payment', (data) => console.log('payment received', data))
Files changed:
Notes: