mirror of
https://github.com/luxfi/explore.git
synced 2026-09-24 00:28:18 +00:00
The FE connected realtime via Phoenix Channels (wss .../socket/v2), but the
Go backend serves no Phoenix endpoint — its realtime is SSE at
/v1/base/realtime (envelope {event,chain,data}). Result: repeating ws 404 ->
'Live updates temporarily delayed'.
lib/socket/sse.ts reimplements the slice of the Phoenix Socket/Channel API the
27 realtime consumers use, backed by one EventSource, behind the existing
SocketProvider/useSocketChannel/useSocketMessage seam — no consumer changes.
Envelope routes via an optional topic field or a hub-channel translation table
(blocks -> blocks:new_block/new_block today). CT harness becomes a tiny SSE
server with the same signatures. 12 vitest cases cover it.
Frontend-only; chain/indexer untouched. Data freshness remains gated on the
separate C-Chain-rollback/indexer-reorg owner items.
102 lines
5.6 KiB
TypeScript
102 lines
5.6 KiB
TypeScript
import type { TestFixture, Page } from '@playwright/test';
|
|
import http from 'node:http';
|
|
|
|
import type { AddressCoinBalanceHistoryItem, AddressTokensBalancesSocketMessage } from 'types/api/address';
|
|
import type { NewBlockSocketResponse } from 'types/api/block';
|
|
import type { SmartContractVerificationResponse } from 'types/api/contract';
|
|
import type { TokenInstanceMetadataSocketMessage } from 'types/api/token';
|
|
import type { TokenTransfer } from 'types/api/tokenTransfer';
|
|
import type { Transaction } from 'types/api/transaction';
|
|
|
|
import { port as socketPort } from '../utils/socket';
|
|
|
|
// Component-test realtime harness. The app speaks Server-Sent Events to the
|
|
// Lux explorer backend's multiplexed `/v1/base/realtime` endpoint (see
|
|
// lib/socket/sse.ts), so this fixture is a minimal SSE server rather than a
|
|
// Phoenix WebSocket mock. `createSocket` / `joinChannel` / `sendMessage`
|
|
// keep their signatures so the consumer specs are unchanged; `sendMessage`
|
|
// emits the backend's `{ event, topic, data }` envelope, addressing a
|
|
// channel directly via the optional `topic` field the adapter routes off.
|
|
|
|
// SSEStream is the server side of one open EventSource stream. Tests
|
|
// push frames into it with sendMessage().
|
|
export class SSEStream {
|
|
constructor(private readonly res: http.ServerResponse) {}
|
|
|
|
send(frame: string) {
|
|
this.res.write(frame);
|
|
}
|
|
}
|
|
|
|
export type CreateSocketFixture = () => Promise<SSEStream>;
|
|
|
|
type Channel = [string, string, string];
|
|
|
|
export interface SocketServerFixture {
|
|
createSocket: CreateSocketFixture;
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
export const createSocket: TestFixture<CreateSocketFixture, { page: Page }> = async({ page }, use) => {
|
|
const openResponses = new Set<http.ServerResponse>();
|
|
let resolveConnection: (conn: SSEStream) => void;
|
|
const connectionPromise = new Promise<SSEStream>((resolve) => {
|
|
resolveConnection = resolve;
|
|
});
|
|
|
|
const server = http.createServer((req, res) => {
|
|
if (req.method === 'HEAD') {
|
|
res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Access-Control-Allow-Origin': '*' });
|
|
res.end();
|
|
return;
|
|
}
|
|
|
|
res.writeHead(200, {
|
|
'Content-Type': 'text/event-stream',
|
|
'Cache-Control': 'no-cache',
|
|
Connection: 'keep-alive',
|
|
'Access-Control-Allow-Origin': '*',
|
|
});
|
|
res.write('event: CONNECT\ndata: {}\n\n');
|
|
|
|
openResponses.add(res);
|
|
res.on('close', () => openResponses.delete(res));
|
|
resolveConnection(new SSEStream(res));
|
|
});
|
|
|
|
await new Promise<void>((resolve) => server.listen(socketPort, resolve));
|
|
|
|
await use(() => connectionPromise);
|
|
|
|
openResponses.forEach((res) => res.end());
|
|
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
};
|
|
|
|
// SSE is a single read-only multiplexed stream — there is no per-topic join
|
|
// handshake. `socket` is kept for call-site compatibility; the returned token
|
|
// is what sendMessage() addresses.
|
|
export const joinChannel = (socket: SSEStream, channelName: string): Promise<Channel> => {
|
|
return Promise.resolve([ '', '', channelName ]);
|
|
};
|
|
|
|
export function sendMessage(socket: SSEStream, channel: Channel, msg: 'coin_balance', payload: { coin_balance: AddressCoinBalanceHistoryItem }): void;
|
|
export function sendMessage(socket: SSEStream, channel: Channel, msg: 'token_balance', payload: { block_number: number }): void;
|
|
export function sendMessage(socket: SSEStream, channel: Channel, msg: `updated_token_balances_${ string }`, payload: AddressTokensBalancesSocketMessage): void;
|
|
export function sendMessage(socket: SSEStream, channel: Channel, msg: 'transaction', payload: { transaction: number }): void;
|
|
export function sendMessage(socket: SSEStream, channel: Channel, msg: 'transaction', payload: { transactions: Array<Transaction> }): void;
|
|
export function sendMessage(socket: SSEStream, channel: Channel, msg: 'pending_transaction', payload: { pending_transaction: number }): void;
|
|
export function sendMessage(socket: SSEStream, channel: Channel, msg: 'pending_transaction', payload: { pending_transactions: Array<Transaction> }): void;
|
|
export function sendMessage(socket: SSEStream, channel: Channel, msg: 'new_block', payload: NewBlockSocketResponse): void;
|
|
export function sendMessage(socket: SSEStream, channel: Channel, msg: 'verification_result', payload: SmartContractVerificationResponse): void;
|
|
export function sendMessage(socket: SSEStream, channel: Channel, msg: 'total_supply', payload: { total_supply: number }): void;
|
|
export function sendMessage(socket: SSEStream, channel: Channel, msg: 'changed_bytecode', payload: Record<string, never>): void;
|
|
export function sendMessage(socket: SSEStream, channel: Channel, msg: 'fetched_bytecode', payload: { fetched_bytecode: string }): void;
|
|
export function sendMessage(socket: SSEStream, channel: Channel, msg: 'eth_bytecode_db_lookup_started', payload: Record<string, never>): void;
|
|
export function sendMessage(socket: SSEStream, channel: Channel, msg: 'smart_contract_was_verified', payload: Record<string, never>): void;
|
|
export function sendMessage(socket: SSEStream, channel: Channel, msg: 'smart_contract_was_not_verified', payload: Record<string, never>): void;
|
|
export function sendMessage(socket: SSEStream, channel: Channel, msg: 'token_transfer', payload: { token_transfers: Array<TokenTransfer> }): void;
|
|
export function sendMessage(socket: SSEStream, channel: Channel, msg: 'fetched_token_instance_metadata', payload: TokenInstanceMetadataSocketMessage): void;
|
|
export function sendMessage(socket: SSEStream, channel: Channel, msg: string, payload: unknown): void {
|
|
socket.send(`data: ${ JSON.stringify({ event: msg, topic: channel[2], data: payload }) }\n\n`);
|
|
}
|