Hook up LibSignalWebsocketResource.forceKeepAlive
This commit is contained in:
parent
f7420e0512
commit
ba6e11614e
2 changed files with 100 additions and 44 deletions
|
@ -187,6 +187,7 @@ export class SocketManager extends EventListener {
|
||||||
this.queueOrHandleRequest(req);
|
this.queueOrHandleRequest(req);
|
||||||
},
|
},
|
||||||
receiveStories: !this.hasStoriesDisabled,
|
receiveStories: !this.hasStoriesDisabled,
|
||||||
|
keepalive: { path: '/v1/keepalive' },
|
||||||
})
|
})
|
||||||
: this.connectResource({
|
: this.connectResource({
|
||||||
name: AUTHENTICATED_CHANNEL_NAME,
|
name: AUTHENTICATED_CHANNEL_NAME,
|
||||||
|
@ -628,6 +629,7 @@ export class SocketManager extends EventListener {
|
||||||
process = connectUnauthenticatedLibsignal({
|
process = connectUnauthenticatedLibsignal({
|
||||||
libsignalNet: this.libsignalNet,
|
libsignalNet: this.libsignalNet,
|
||||||
name: UNAUTHENTICATED_CHANNEL_NAME,
|
name: UNAUTHENTICATED_CHANNEL_NAME,
|
||||||
|
keepalive: { path: '/v1/keepalive' },
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
process = this.connectResource({
|
process = this.connectResource({
|
||||||
|
@ -749,6 +751,7 @@ export class SocketManager extends EventListener {
|
||||||
const shadowingConnection = connectUnauthenticatedLibsignal({
|
const shadowingConnection = connectUnauthenticatedLibsignal({
|
||||||
libsignalNet: this.libsignalNet,
|
libsignalNet: this.libsignalNet,
|
||||||
name: options.name,
|
name: options.name,
|
||||||
|
keepalive: options.keepalive ?? {},
|
||||||
});
|
});
|
||||||
const shadowWrapper = async () => {
|
const shadowWrapper = async () => {
|
||||||
// if main connection results in an error,
|
// if main connection results in an error,
|
||||||
|
|
|
@ -326,7 +326,7 @@ export interface IWebSocketResource extends IResource {
|
||||||
|
|
||||||
shutdown(): void;
|
shutdown(): void;
|
||||||
|
|
||||||
close(): void;
|
close(code?: number, reason?: string): void;
|
||||||
|
|
||||||
localPort(): number | undefined;
|
localPort(): number | undefined;
|
||||||
}
|
}
|
||||||
|
@ -340,9 +340,11 @@ const UNEXPECTED_DISCONNECT_CODE = 3001;
|
||||||
export function connectUnauthenticatedLibsignal({
|
export function connectUnauthenticatedLibsignal({
|
||||||
libsignalNet,
|
libsignalNet,
|
||||||
name,
|
name,
|
||||||
|
keepalive,
|
||||||
}: {
|
}: {
|
||||||
libsignalNet: Net.Net;
|
libsignalNet: Net.Net;
|
||||||
name: string;
|
name: string;
|
||||||
|
keepalive: KeepAliveOptionsType;
|
||||||
}): AbortableProcess<LibsignalWebSocketResource> {
|
}): AbortableProcess<LibsignalWebSocketResource> {
|
||||||
const logId = `LibsignalWebSocketResource(${name})`;
|
const logId = `LibsignalWebSocketResource(${name})`;
|
||||||
const listener: LibsignalWebSocketResourceHolder & ConnectionEventsListener =
|
const listener: LibsignalWebSocketResourceHolder & ConnectionEventsListener =
|
||||||
|
@ -360,7 +362,8 @@ export function connectUnauthenticatedLibsignal({
|
||||||
return connectLibsignal(
|
return connectLibsignal(
|
||||||
libsignalNet.newUnauthenticatedChatService(listener),
|
libsignalNet.newUnauthenticatedChatService(listener),
|
||||||
listener,
|
listener,
|
||||||
logId
|
logId,
|
||||||
|
keepalive
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -370,12 +373,14 @@ export function connectAuthenticatedLibsignal({
|
||||||
credentials,
|
credentials,
|
||||||
handler,
|
handler,
|
||||||
receiveStories,
|
receiveStories,
|
||||||
|
keepalive,
|
||||||
}: {
|
}: {
|
||||||
libsignalNet: Net.Net;
|
libsignalNet: Net.Net;
|
||||||
name: string;
|
name: string;
|
||||||
credentials: WebAPICredentials;
|
credentials: WebAPICredentials;
|
||||||
handler: (request: IncomingWebSocketRequest) => void;
|
handler: (request: IncomingWebSocketRequest) => void;
|
||||||
receiveStories: boolean;
|
receiveStories: boolean;
|
||||||
|
keepalive: KeepAliveOptionsType;
|
||||||
}): AbortableProcess<LibsignalWebSocketResource> {
|
}): AbortableProcess<LibsignalWebSocketResource> {
|
||||||
const logId = `LibsignalWebSocketResource(${name})`;
|
const logId = `LibsignalWebSocketResource(${name})`;
|
||||||
const listener: LibsignalWebSocketResourceHolder & ChatServiceListener = {
|
const listener: LibsignalWebSocketResourceHolder & ChatServiceListener = {
|
||||||
|
@ -424,7 +429,8 @@ export function connectAuthenticatedLibsignal({
|
||||||
listener
|
listener
|
||||||
),
|
),
|
||||||
listener,
|
listener,
|
||||||
logId
|
logId,
|
||||||
|
keepalive
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -435,7 +441,8 @@ function logDisconnectedListenerWarn(logId: string, method: string): void {
|
||||||
function connectLibsignal(
|
function connectLibsignal(
|
||||||
chatService: Net.ChatService,
|
chatService: Net.ChatService,
|
||||||
resourceHolder: LibsignalWebSocketResourceHolder,
|
resourceHolder: LibsignalWebSocketResourceHolder,
|
||||||
logId: string
|
logId: string,
|
||||||
|
keepalive: KeepAliveOptionsType
|
||||||
): AbortableProcess<LibsignalWebSocketResource> {
|
): AbortableProcess<LibsignalWebSocketResource> {
|
||||||
const connectAsync = async () => {
|
const connectAsync = async () => {
|
||||||
try {
|
try {
|
||||||
|
@ -444,7 +451,8 @@ function connectLibsignal(
|
||||||
const resource = new LibsignalWebSocketResource(
|
const resource = new LibsignalWebSocketResource(
|
||||||
chatService,
|
chatService,
|
||||||
IpVersion.fromDebugInfoCode(debugInfo.ipType),
|
IpVersion.fromDebugInfoCode(debugInfo.ipType),
|
||||||
logId
|
logId,
|
||||||
|
keepalive
|
||||||
);
|
);
|
||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
resourceHolder.resource = resource;
|
resourceHolder.resource = resource;
|
||||||
|
@ -473,12 +481,21 @@ export class LibsignalWebSocketResource
|
||||||
{
|
{
|
||||||
closed = false;
|
closed = false;
|
||||||
|
|
||||||
|
// Unlike WebSocketResource, libsignal will automatically attempt to keep the
|
||||||
|
// socket alive using websocket pings, so we don't need a timer-based
|
||||||
|
// keepalive mechanism. But we still send one-off keepalive requests when
|
||||||
|
// things change (see forceKeepAlive()).
|
||||||
|
private keepalive: KeepAliveSender;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly chatService: Net.ChatService,
|
private readonly chatService: Net.ChatService,
|
||||||
private readonly socketIpVersion: IpVersion | undefined,
|
private readonly socketIpVersion: IpVersion | undefined,
|
||||||
private readonly logId: string
|
private readonly logId: string,
|
||||||
|
keepalive: KeepAliveOptionsType
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
this.keepalive = new KeepAliveSender(this, this.logId, keepalive);
|
||||||
}
|
}
|
||||||
|
|
||||||
public localPort(): number | undefined {
|
public localPort(): number | undefined {
|
||||||
|
@ -538,8 +555,8 @@ export class LibsignalWebSocketResource
|
||||||
this.dispatchEvent(event);
|
this.dispatchEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
public forceKeepAlive(): void {
|
public forceKeepAlive(timeout?: number): void {
|
||||||
// no-op
|
drop(this.keepalive.send(timeout));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async sendRequest(options: SendRequestOptions): Promise<Response> {
|
public async sendRequest(options: SendRequestOptions): Promise<Response> {
|
||||||
|
@ -654,10 +671,10 @@ export class WebSocketResourceWithShadowing implements IWebSocketResource {
|
||||||
this.main.addEventListener(name, handler);
|
this.main.addEventListener(name, handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
public close(): void {
|
public close(code = NORMAL_DISCONNECT_CODE, reason?: string): void {
|
||||||
this.main.close();
|
this.main.close(code, reason);
|
||||||
if (this.shadowing) {
|
if (this.shadowing) {
|
||||||
this.shadowing.close();
|
this.shadowing.close(code, reason);
|
||||||
this.shadowing = undefined;
|
this.shadowing = undefined;
|
||||||
} else {
|
} else {
|
||||||
this.shadowingConnection.abort();
|
this.shadowingConnection.abort();
|
||||||
|
@ -1088,48 +1105,35 @@ const KEEPALIVE_TIMEOUT_MS = 30 * durations.SECOND;
|
||||||
|
|
||||||
const LOG_KEEPALIVE_AFTER_MS = 500;
|
const LOG_KEEPALIVE_AFTER_MS = 500;
|
||||||
|
|
||||||
class KeepAlive {
|
/**
|
||||||
private keepAliveTimer: Timers.Timeout | undefined;
|
* References an {@link IWebSocketResource} and a request path that should
|
||||||
|
* return promptly to determine whether the connection is still alive.
|
||||||
|
*
|
||||||
|
* The response to the request must have a 2xx status code but is otherwise
|
||||||
|
* ignored. A failing response or a timeout results in the socket being closed
|
||||||
|
* with {@link UNEXPECTED_DISCONNECT_CODE}.
|
||||||
|
*
|
||||||
|
* Use the subclass {@link KeepAlive} if you want to send the request at regular
|
||||||
|
* intervals.
|
||||||
|
*/
|
||||||
|
class KeepAliveSender {
|
||||||
private path: string;
|
private path: string;
|
||||||
|
|
||||||
private wsr: WebSocketResource;
|
protected wsr: IWebSocketResource;
|
||||||
|
|
||||||
private lastAliveAt: number = Date.now();
|
protected logId: string;
|
||||||
|
|
||||||
private logId: string;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
websocketResource: WebSocketResource,
|
websocketResource: IWebSocketResource,
|
||||||
name: string,
|
name: string,
|
||||||
opts: KeepAliveOptionsType = {}
|
opts: KeepAliveOptionsType = {}
|
||||||
) {
|
) {
|
||||||
this.logId = `WebSocketResources.KeepAlive(${name})`;
|
this.logId = `WebSocketResources.KeepAlive(${name})`;
|
||||||
if (websocketResource instanceof WebSocketResource) {
|
this.path = opts.path ?? '/';
|
||||||
this.path = opts.path ?? '/';
|
this.wsr = websocketResource;
|
||||||
this.wsr = websocketResource;
|
|
||||||
} else {
|
|
||||||
throw new TypeError('KeepAlive expected a WebSocketResource');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public stop(): void {
|
public async send(timeout = KEEPALIVE_TIMEOUT_MS): Promise<boolean> {
|
||||||
this.clearTimers();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async send(timeout = KEEPALIVE_TIMEOUT_MS): Promise<void> {
|
|
||||||
this.clearTimers();
|
|
||||||
|
|
||||||
const isStale = isOlderThan(this.lastAliveAt, STALE_THRESHOLD_MS);
|
|
||||||
if (isStale) {
|
|
||||||
log.info(`${this.logId}.send: disconnecting due to stale state`);
|
|
||||||
this.wsr.close(
|
|
||||||
UNEXPECTED_DISCONNECT_CODE,
|
|
||||||
`Last keepalive request was too far in the past: ${this.lastAliveAt}`
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info(`${this.logId}.send: Sending a keepalive message`);
|
log.info(`${this.logId}.send: Sending a keepalive message`);
|
||||||
const sentAt = Date.now();
|
const sentAt = Date.now();
|
||||||
|
|
||||||
|
@ -1148,14 +1152,14 @@ class KeepAlive {
|
||||||
UNEXPECTED_DISCONNECT_CODE,
|
UNEXPECTED_DISCONNECT_CODE,
|
||||||
`keepalive response with ${status} code`
|
`keepalive response with ${status} code`
|
||||||
);
|
);
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.wsr.close(
|
this.wsr.close(
|
||||||
UNEXPECTED_DISCONNECT_CODE,
|
UNEXPECTED_DISCONNECT_CODE,
|
||||||
'No response to keepalive request'
|
'No response to keepalive request'
|
||||||
);
|
);
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const responseTime = Date.now() - sentAt;
|
const responseTime = Date.now() - sentAt;
|
||||||
|
@ -1166,8 +1170,57 @@ class KeepAlive {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages a timer that checks if a particular {@link WebSocketResource} is
|
||||||
|
* still alive.
|
||||||
|
*
|
||||||
|
* The resource must specifically be a {@link WebSocketResource}. Other kinds of
|
||||||
|
* resource are expected to manage their own liveness checks. If you want to
|
||||||
|
* manually send keepalive requests to such resources, use the base class
|
||||||
|
* {@link KeepAliveSender}.
|
||||||
|
*/
|
||||||
|
class KeepAlive extends KeepAliveSender {
|
||||||
|
private keepAliveTimer: Timers.Timeout | undefined;
|
||||||
|
|
||||||
|
private lastAliveAt: number = Date.now();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
websocketResource: WebSocketResource,
|
||||||
|
name: string,
|
||||||
|
opts: KeepAliveOptionsType = {}
|
||||||
|
) {
|
||||||
|
super(websocketResource, name, opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
public stop(): void {
|
||||||
|
this.clearTimers();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async send(timeout = KEEPALIVE_TIMEOUT_MS): Promise<boolean> {
|
||||||
|
this.clearTimers();
|
||||||
|
|
||||||
|
const isStale = isOlderThan(this.lastAliveAt, STALE_THRESHOLD_MS);
|
||||||
|
if (isStale) {
|
||||||
|
log.info(`${this.logId}.send: disconnecting due to stale state`);
|
||||||
|
this.wsr.close(
|
||||||
|
UNEXPECTED_DISCONNECT_CODE,
|
||||||
|
`Last keepalive request was too far in the past: ${this.lastAliveAt}`
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAlive = await super.send(timeout);
|
||||||
|
if (!isAlive) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Successful response on time
|
// Successful response on time
|
||||||
this.reset();
|
this.reset();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public reset(): void {
|
public reset(): void {
|
||||||
|
|
Loading…
Add table
Reference in a new issue