Detect stale tcp connections to relay
Enable nostr-tools' internal stale tcp connection detection
This commit is contained in:
parent
4bcbef2aae
commit
22601be42d
2 changed files with 33 additions and 3 deletions
|
|
@ -85,6 +85,23 @@ to `now` in `disconnect()`. Any incoming message with `time < lastProcessedTimes
|
||||||
dropped. This prevents the relay from re-delivering messages that were already handled before a
|
dropped. This prevents the relay from re-delivering messages that were already handled before a
|
||||||
disconnect.
|
disconnect.
|
||||||
|
|
||||||
|
### Keepalive
|
||||||
|
|
||||||
|
`RelayClient` creates its `SimplePool` with `enablePing: true`. This enables nostr-tools'
|
||||||
|
built-in heartbeat: every 29 seconds the pool pings each connected relay and expects a
|
||||||
|
response within 20 seconds. In Node.js this uses native WebSocket ping/pong frames; in
|
||||||
|
browsers (where the WebSocket API doesn't expose ping) it falls back to sending a dummy
|
||||||
|
subscription request and waiting for EOSE.
|
||||||
|
|
||||||
|
If a relay fails to respond, nostr-tools closes the WebSocket, which fires the subscription
|
||||||
|
`onclose` callback, which emits `"disconnect"` on `RelayClient`, which triggers the
|
||||||
|
connection manager's existing reconnect loop. This detects "zombie" TCP connections where the
|
||||||
|
socket appears open but the relay is unreachable.
|
||||||
|
|
||||||
|
Additionally, if a `publishMessage()` call fails (all relays reject the event),
|
||||||
|
`RelayClient` emits `"disconnect"` alongside the thrown error. This ensures the reconnect
|
||||||
|
loop starts immediately rather than waiting for the next ping cycle.
|
||||||
|
|
||||||
### Sequence numbers
|
### Sequence numbers
|
||||||
|
|
||||||
`nextSequence()` starts at a random offset in the safe integer range and increments by 2. This
|
`nextSequence()` starts at a random offset in the safe integer range and increments by 2. This
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,8 @@ export class RelayClient extends EventEmitter {
|
||||||
private messageQueue: MessageQueue;
|
private messageQueue: MessageQueue;
|
||||||
private readyTimeoutId: ReturnType<typeof setTimeout> | null = null;
|
private readyTimeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
|
private disconnecting: boolean = false;
|
||||||
|
|
||||||
private sequence: number = Math.floor(
|
private sequence: number = Math.floor(
|
||||||
Math.random() * (Number.MAX_SAFE_INTEGER - 500_000),
|
Math.random() * (Number.MAX_SAFE_INTEGER - 500_000),
|
||||||
);
|
);
|
||||||
|
|
@ -72,7 +74,7 @@ export class RelayClient extends EventEmitter {
|
||||||
logNetworkActivity: true,
|
logNetworkActivity: true,
|
||||||
...config,
|
...config,
|
||||||
};
|
};
|
||||||
this.pool = pool ?? new SimplePool();
|
this.pool = pool ?? new SimplePool({ enablePing: true });
|
||||||
this.sharedPool = pool !== undefined;
|
this.sharedPool = pool !== undefined;
|
||||||
|
|
||||||
this.messageQueue = new MessageQueue({
|
this.messageQueue = new MessageQueue({
|
||||||
|
|
@ -120,11 +122,19 @@ export class RelayClient extends EventEmitter {
|
||||||
return !this.config.pairedPublicKey.every((byte) => byte === 0);
|
return !this.config.pairedPublicKey.every((byte) => byte === 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private emitDisconnect(error: Error): void {
|
||||||
|
if (this.disconnecting) return;
|
||||||
|
this.disconnecting = true;
|
||||||
|
this.emit("disconnect", error);
|
||||||
|
}
|
||||||
|
|
||||||
async connect(): Promise<void> {
|
async connect(): Promise<void> {
|
||||||
if (this.config.logNetworkActivity) {
|
if (this.config.logNetworkActivity) {
|
||||||
debug(Scope.Relay, `Connecting to relay...`);
|
debug(Scope.Relay, `Connecting to relay...`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.disconnecting = false;
|
||||||
|
|
||||||
if (this.lastProcessedTimestamp === 0) {
|
if (this.lastProcessedTimestamp === 0) {
|
||||||
this.lastProcessedTimestamp = Math.floor(Date.now() / 1000) - 2;
|
this.lastProcessedTimestamp = Math.floor(Date.now() / 1000) - 2;
|
||||||
}
|
}
|
||||||
|
|
@ -150,7 +160,7 @@ export class RelayClient extends EventEmitter {
|
||||||
if (this.config.logNetworkActivity) {
|
if (this.config.logNetworkActivity) {
|
||||||
debug(Scope.Relay, `Subscription closed: ${reasons.join(", ")}`);
|
debug(Scope.Relay, `Subscription closed: ${reasons.join(", ")}`);
|
||||||
}
|
}
|
||||||
this.emit("disconnect", new Error("Subscription closed"));
|
this.emitDisconnect(new Error("Subscription closed"));
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
@ -251,6 +261,9 @@ export class RelayClient extends EventEmitter {
|
||||||
error,
|
error,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
this.emitDisconnect(
|
||||||
|
error instanceof Error ? error : new Error(`Publish failed: ${error}`),
|
||||||
|
);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -330,7 +343,7 @@ export class RelayClient extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
isConnected(): boolean {
|
isConnected(): boolean {
|
||||||
return true;
|
return this.subscription !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private netlog(
|
private netlog(
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue