SnowLuma v2.4.2 source snapshot
Dev Build / cleanup (push) Waiting to run
Dev Build / typecheck (push) Waiting to run
Publish MCP to npm / Build, test, and publish MCP (push) Waiting to run
Publish SDK to npm / Build, test, and publish SDK (push) Waiting to run
Sync action catalog to docs / sync (push) Waiting to run
Publish UI to npm / Build and publish UI (push) Waiting to run
Build and Release / preflight (push) Waiting to run
Dev Build / cleanup (push) Waiting to run
Dev Build / typecheck (push) Waiting to run
Publish MCP to npm / Build, test, and publish MCP (push) Waiting to run
Publish SDK to npm / Build, test, and publish SDK (push) Waiting to run
Sync action catalog to docs / sync (push) Waiting to run
Publish UI to npm / Build and publish UI (push) Waiting to run
Build and Release / preflight (push) Waiting to run
This commit is contained in:
commit
fa9c73363d
1367 files changed
+355735
No files matched your search
@@ -0,0 +1,185 @@
|
||||
// Issue #208 (server side): symmetric transport keepalive for the reverse-WS
|
||||
// SERVER. Each attached client is pinged; a client that goes fully silent for
|
||||
// maxMissed consecutive intervals is terminated (and drops out of the
|
||||
// connection set) so a half-open client can't wedge forever. A healthy or
|
||||
// actively-messaging client must never be reaped — the same false-positive
|
||||
// guard as the client adapter.
|
||||
//
|
||||
// Mock `@snowluma/websocket` (native addon isn't built in a plain checkout),
|
||||
// mirroring network-bootstrap-meta.test.ts. Timers are faked and driven.
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const { FakeWebSocket, FakeWebSocketServer, servers } = vi.hoisted(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const { EventEmitter } = require('node:events') as typeof import('node:events');
|
||||
const servers: FakeWebSocketServer[] = [];
|
||||
|
||||
class FakeWebSocket extends EventEmitter {
|
||||
public readyState = 1; // OPEN
|
||||
public pings = 0;
|
||||
public terminated = false;
|
||||
send(_payload: string, cb?: (err?: Error | null) => void): void {
|
||||
cb?.(null);
|
||||
}
|
||||
ping(): void {
|
||||
this.pings += 1;
|
||||
}
|
||||
terminate(): void {
|
||||
if (this.readyState === 3) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.terminated = true;
|
||||
this.readyState = 3;
|
||||
this.emit('close');
|
||||
}
|
||||
close(): void {
|
||||
if (this.readyState < 2) {
|
||||
this.readyState = 2;
|
||||
}
|
||||
}
|
||||
simulatePong(): void {
|
||||
this.emit('pong', Buffer.alloc(0));
|
||||
}
|
||||
simulateMessage(): void {
|
||||
this.emit('message', Buffer.from('{}'), false);
|
||||
}
|
||||
}
|
||||
|
||||
class FakeWebSocketServer extends EventEmitter {
|
||||
public closed = false;
|
||||
constructor(_opts: unknown) {
|
||||
super();
|
||||
servers.push(this);
|
||||
}
|
||||
close(): void {
|
||||
this.closed = true;
|
||||
}
|
||||
simulateConnection(socket: FakeWebSocket, request: unknown): void {
|
||||
this.emit('connection', socket, request);
|
||||
}
|
||||
}
|
||||
|
||||
return { FakeWebSocket, FakeWebSocketServer, servers };
|
||||
});
|
||||
|
||||
vi.mock('@snowluma/websocket', () => ({
|
||||
WebSocket: FakeWebSocket,
|
||||
WebSocketServer: FakeWebSocketServer,
|
||||
}));
|
||||
|
||||
import { WsServerAdapter } from '../src/network/ws-server-adapter';
|
||||
import type { NetworkAdapterContext } from '../src/network/adapter';
|
||||
import type { WsServerNetwork } from '../src/types';
|
||||
|
||||
const INTERVAL = 30_000;
|
||||
const MAX_MISSED = 2;
|
||||
|
||||
function ctx(): NetworkAdapterContext {
|
||||
return {
|
||||
uin: '10001',
|
||||
api: { processStreamRequest: async () => {} } as never,
|
||||
buildLifecycleEvent: () => ({}),
|
||||
buildHeartbeatEvent: () => ({}),
|
||||
};
|
||||
}
|
||||
|
||||
function cfg(over: Partial<WsServerNetwork> = {}): WsServerNetwork {
|
||||
return {
|
||||
enabled: true,
|
||||
host: '0.0.0.0',
|
||||
port: 8080,
|
||||
path: '/',
|
||||
role: 'Universal',
|
||||
...over,
|
||||
} as WsServerNetwork;
|
||||
}
|
||||
|
||||
const REQUEST = { url: '/', headers: {} } as unknown;
|
||||
|
||||
function connect(): { adapter: WsServerAdapter; socket: InstanceType<typeof FakeWebSocket> } {
|
||||
const adapter = new WsServerAdapter('ws', cfg(), ctx());
|
||||
adapter.open();
|
||||
const server = servers[servers.length - 1];
|
||||
server.emit('listening'); // adapter marks itself listening → status 'ok'
|
||||
const socket = new FakeWebSocket();
|
||||
server.simulateConnection(socket, REQUEST);
|
||||
return { adapter, socket };
|
||||
}
|
||||
|
||||
describe('WsServerAdapter — transport heartbeat (issue #208)', () => {
|
||||
beforeEach(() => {
|
||||
servers.length = 0;
|
||||
});
|
||||
|
||||
it('reaps a silent client only after maxMissed intervals, then drops it', () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
try {
|
||||
const { adapter, socket } = connect();
|
||||
expect(adapter.describeStatus().detail).toBe('1 个客户端');
|
||||
|
||||
vi.advanceTimersByTime(INTERVAL * MAX_MISSED); // tolerated
|
||||
expect(socket.terminated).toBe(false);
|
||||
|
||||
vi.advanceTimersByTime(INTERVAL); // threshold crossed
|
||||
expect(socket.terminated).toBe(true);
|
||||
expect(adapter.describeStatus().detail).toBe('0 个客户端');
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it('never reaps a client that keeps ponging', () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
try {
|
||||
const { adapter, socket } = connect();
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
vi.advanceTimersByTime(INTERVAL);
|
||||
socket.simulatePong();
|
||||
}
|
||||
|
||||
expect(socket.terminated).toBe(false);
|
||||
expect(adapter.describeStatus().detail).toBe('1 个客户端');
|
||||
expect(socket.pings).toBeGreaterThan(0);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it('treats inbound messages as liveness', () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
try {
|
||||
const { adapter, socket } = connect();
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
vi.advanceTimersByTime(INTERVAL);
|
||||
socket.simulateMessage();
|
||||
}
|
||||
|
||||
expect(socket.terminated).toBe(false);
|
||||
expect(adapter.describeStatus().detail).toBe('1 个客户端');
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it('stops all client heartbeats on adapter close — no lingering pings', () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
try {
|
||||
const { adapter, socket } = connect();
|
||||
adapter.close();
|
||||
const pingsAtClose = socket.pings;
|
||||
vi.advanceTimersByTime(INTERVAL * 5);
|
||||
expect(socket.pings).toBe(pingsAtClose);
|
||||
expect(socket.terminated).toBe(false); // closed cleanly, not reaped
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user