Fixed ttp frfr
This commit is contained in:
parent
24e969a19c
commit
ec5ee847e0
4 changed files with 588 additions and 633 deletions
|
|
@ -5,7 +5,8 @@ import { useStorage } from "@tensamin/storage/context";
|
||||||
import { createTransportClient, READY_STATE, type BoundSendFn } from "./core";
|
import { createTransportClient, READY_STATE, type BoundSendFn } from "./core";
|
||||||
import {
|
import {
|
||||||
PING_INTERVAL,
|
PING_INTERVAL,
|
||||||
RETRY_COUNT,
|
RECONNECT_RESET,
|
||||||
|
RECONNECT_TRIES,
|
||||||
RETRY_INTERVAL,
|
RETRY_INTERVAL,
|
||||||
TRANSPORT_URL,
|
TRANSPORT_URL,
|
||||||
} from "./values";
|
} from "./values";
|
||||||
|
|
@ -224,6 +225,7 @@ export default function Provider(props: { children: React.ReactNode }) {
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
let attempts = 0;
|
let attempts = 0;
|
||||||
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
let reconnectResetTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
let reconnectScheduled = false;
|
let reconnectScheduled = false;
|
||||||
let disposed = false;
|
let disposed = false;
|
||||||
|
|
||||||
|
|
@ -241,6 +243,31 @@ export default function Provider(props: { children: React.ReactNode }) {
|
||||||
reconnectScheduled = false;
|
reconnectScheduled = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the stability timer that resets reconnect attempt counters.
|
||||||
|
* @returns Void.
|
||||||
|
*/
|
||||||
|
const clearReconnectResetTimer = () => {
|
||||||
|
if (!reconnectResetTimer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearTimeout(reconnectResetTimer);
|
||||||
|
reconnectResetTimer = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the stability timer that resets reconnect attempts after uptime.
|
||||||
|
* @returns Void.
|
||||||
|
*/
|
||||||
|
const scheduleReconnectReset = () => {
|
||||||
|
clearReconnectResetTimer();
|
||||||
|
reconnectResetTimer = setTimeout(() => {
|
||||||
|
attempts = 0;
|
||||||
|
reconnectResetTimer = null;
|
||||||
|
}, RECONNECT_RESET * 1_000);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Schedules a delayed reconnect attempt unless retries are exhausted.
|
* Schedules a delayed reconnect attempt unless retries are exhausted.
|
||||||
* @param reason Optional reason for reconnect scheduling.
|
* @param reason Optional reason for reconnect scheduling.
|
||||||
|
|
@ -251,7 +278,7 @@ export default function Provider(props: { children: React.ReactNode }) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attempts >= RETRY_COUNT) {
|
if (attempts >= RECONNECT_TRIES) {
|
||||||
setError("Connection Failed");
|
setError("Connection Failed");
|
||||||
setErrorDescription(
|
setErrorDescription(
|
||||||
"Unable to connect to the server after multiple attempts. Please check your internet connection or try again later.",
|
"Unable to connect to the server after multiple attempts. Please check your internet connection or try again later.",
|
||||||
|
|
@ -275,8 +302,8 @@ export default function Provider(props: { children: React.ReactNode }) {
|
||||||
setReadyState(state);
|
setReadyState(state);
|
||||||
|
|
||||||
if (state === READY_STATE.OPEN) {
|
if (state === READY_STATE.OPEN) {
|
||||||
attempts = 0;
|
|
||||||
clearReconnectTimer();
|
clearReconnectTimer();
|
||||||
|
scheduleReconnectReset();
|
||||||
identificationStartedRef.current = false;
|
identificationStartedRef.current = false;
|
||||||
setConnected(true);
|
setConnected(true);
|
||||||
setIdentified(false);
|
setIdentified(false);
|
||||||
|
|
@ -285,32 +312,24 @@ export default function Provider(props: { children: React.ReactNode }) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearReconnectResetTimer();
|
||||||
identificationStartedRef.current = false;
|
identificationStartedRef.current = false;
|
||||||
setConnected(false);
|
setConnected(false);
|
||||||
setIdentified(false);
|
setIdentified(false);
|
||||||
},
|
},
|
||||||
onClose: ({ error: closeError, intentional }) => {
|
onClose: ({ error: closeError, intentional }) => {
|
||||||
if (isStopSendingError(closeError)) {
|
clearReconnectResetTimer();
|
||||||
clearReconnectTimer();
|
|
||||||
setConnected(false);
|
|
||||||
setIdentified(false);
|
|
||||||
setIdentifying(false);
|
|
||||||
setError("Connection closed");
|
|
||||||
setErrorDescription(
|
|
||||||
"The connection was forcefully closed by the Omikron.",
|
|
||||||
);
|
|
||||||
log(0, "Socket", "red", "Connection closed", closeError);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setConnected(false);
|
setConnected(false);
|
||||||
setIdentified(false);
|
setIdentified(false);
|
||||||
|
setIdentifying(false);
|
||||||
|
|
||||||
if (disposed || intentional) {
|
if (disposed || intentional) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
log(0, "Socket", "red", "Disconnected", closeError);
|
log(0, "Socket", "red", "Disconnected", closeError, {
|
||||||
|
stopSending: isStopSendingError(closeError),
|
||||||
|
});
|
||||||
scheduleReconnect(closeError);
|
scheduleReconnect(closeError);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -333,15 +352,6 @@ export default function Provider(props: { children: React.ReactNode }) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isStopSendingError(connectError)) {
|
|
||||||
clearReconnectTimer();
|
|
||||||
setError("Connection closed");
|
|
||||||
setErrorDescription(
|
|
||||||
"The connection was forcefully closed by the Omikron.",
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
log(0, "Socket", "red", "Connection attempt failed", connectError);
|
log(0, "Socket", "red", "Connection attempt failed", connectError);
|
||||||
scheduleReconnect(connectError);
|
scheduleReconnect(connectError);
|
||||||
}
|
}
|
||||||
|
|
@ -352,6 +362,7 @@ export default function Provider(props: { children: React.ReactNode }) {
|
||||||
return () => {
|
return () => {
|
||||||
disposed = true;
|
disposed = true;
|
||||||
clearReconnectTimer();
|
clearReconnectTimer();
|
||||||
|
clearReconnectResetTimer();
|
||||||
|
|
||||||
if (clientRef.current === transportClient) {
|
if (clientRef.current === transportClient) {
|
||||||
clientRef.current = null;
|
clientRef.current = null;
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -61,6 +61,11 @@ type ActiveConnection = {
|
||||||
streamReader: ReadableStreamDefaultReader<ReadableStream<Uint8Array>> | null;
|
streamReader: ReadableStreamDefaultReader<ReadableStream<Uint8Array>> | null;
|
||||||
intentional: boolean;
|
intentional: boolean;
|
||||||
closeNotified: boolean;
|
closeNotified: boolean;
|
||||||
|
acceptLoopDone: Promise<void> | null;
|
||||||
|
resolveAcceptLoopDone: (() => void) | null;
|
||||||
|
activeIncomingTasks: Set<Promise<void>>;
|
||||||
|
sendStream: WritableStream<Uint8Array> | null;
|
||||||
|
sendWriter: WritableStreamDefaultWriter<Uint8Array> | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -532,32 +537,6 @@ export function createTransportClient<T extends SchemaMap>(
|
||||||
notifyClosed(connection, error);
|
notifyClosed(connection, error);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles STOP_SENDING failures by forcing close and notifying failure.
|
|
||||||
* @param connection Active connection.
|
|
||||||
* @param error Failure reason.
|
|
||||||
* @returns Void.
|
|
||||||
*/
|
|
||||||
const closeFromStopSending = (
|
|
||||||
connection: ActiveConnection,
|
|
||||||
error: unknown,
|
|
||||||
) => {
|
|
||||||
if (!isStopSendingError(error)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
connection.transport.close({
|
|
||||||
closeCode: APPLICATION_CLOSE_CODE,
|
|
||||||
reason: "stop-sending",
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
// Ignore close failures while handling STOP_SENDING.
|
|
||||||
}
|
|
||||||
|
|
||||||
handleConnectionFailure(connection, error);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles decoded incoming messages and resolves request promises or push listeners.
|
* Handles decoded incoming messages and resolves request promises or push listeners.
|
||||||
* @param message Decoded incoming message.
|
* @param message Decoded incoming message.
|
||||||
|
|
@ -680,40 +659,88 @@ export function createTransportClient<T extends SchemaMap>(
|
||||||
const startIncomingLoop = (connection: ActiveConnection) => {
|
const startIncomingLoop = (connection: ActiveConnection) => {
|
||||||
connection.streamReader =
|
connection.streamReader =
|
||||||
connection.transport.incomingUnidirectionalStreams.getReader();
|
connection.transport.incomingUnidirectionalStreams.getReader();
|
||||||
|
connection.acceptLoopDone = new Promise<void>((resolve) => {
|
||||||
|
connection.resolveAcceptLoopDone = resolve;
|
||||||
|
});
|
||||||
|
|
||||||
void (async () => {
|
void (async () => {
|
||||||
try {
|
try {
|
||||||
while (currentConnection === connection && !connection.intentional) {
|
while (!connection.closeNotified) {
|
||||||
const result = await connection.streamReader?.read();
|
const streamReader = connection.streamReader;
|
||||||
|
if (!streamReader) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const readResult = await Promise.race([
|
||||||
|
streamReader.read().then((result) => ({
|
||||||
|
type: "stream" as const,
|
||||||
|
result,
|
||||||
|
})),
|
||||||
|
connection.transport.closed
|
||||||
|
.catch(() => undefined)
|
||||||
|
.then(() => ({ type: "closed" as const })),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (readResult.type !== "stream") {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = readResult.result;
|
||||||
if (!result || result.done) {
|
if (!result || result.done) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const closedByPeer = await processIncomingStream(
|
const shouldDiscardFrames =
|
||||||
result.value,
|
connection.intentional || currentConnection !== connection;
|
||||||
connection,
|
|
||||||
handleIncomingMessage,
|
|
||||||
handleRecoverableDecodeFailure,
|
|
||||||
handleConnectionFailure,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (closedByPeer) {
|
const task = (async () => {
|
||||||
return;
|
try {
|
||||||
}
|
await processIncomingStream(
|
||||||
|
result.value,
|
||||||
|
connection,
|
||||||
|
handleIncomingMessage,
|
||||||
|
handleRecoverableDecodeFailure,
|
||||||
|
handleConnectionFailure,
|
||||||
|
shouldDiscardFrames,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
log(
|
||||||
|
0,
|
||||||
|
"Socket",
|
||||||
|
"red",
|
||||||
|
"Incoming transport stream failed",
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
handleConnectionFailure(connection, error);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
connection.activeIncomingTasks.add(task);
|
||||||
|
void task.finally(() => {
|
||||||
|
connection.activeIncomingTasks.delete(task);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!connection.intentional) {
|
if (
|
||||||
|
!connection.intentional &&
|
||||||
|
!connection.closeNotified &&
|
||||||
|
currentConnection === connection
|
||||||
|
) {
|
||||||
handleConnectionFailure(
|
handleConnectionFailure(
|
||||||
connection,
|
connection,
|
||||||
new Error("Transport stream closed"),
|
new Error("Transport stream closed"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log(0, "Socket", "red", "Incoming transport stream failed", error);
|
log(0, "Socket", "red", "Incoming stream accept loop failed", error);
|
||||||
handleConnectionFailure(connection, error);
|
handleConnectionFailure(connection, error);
|
||||||
} finally {
|
} finally {
|
||||||
connection.streamReader?.releaseLock();
|
connection.streamReader?.releaseLock();
|
||||||
connection.streamReader = null;
|
connection.streamReader = null;
|
||||||
|
|
||||||
|
const resolveAcceptLoopDone = connection.resolveAcceptLoopDone;
|
||||||
|
connection.resolveAcceptLoopDone = null;
|
||||||
|
resolveAcceptLoopDone?.();
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
};
|
};
|
||||||
|
|
@ -756,6 +783,11 @@ export function createTransportClient<T extends SchemaMap>(
|
||||||
streamReader: null,
|
streamReader: null,
|
||||||
intentional: false,
|
intentional: false,
|
||||||
closeNotified: false,
|
closeNotified: false,
|
||||||
|
acceptLoopDone: null,
|
||||||
|
resolveAcceptLoopDone: null,
|
||||||
|
activeIncomingTasks: new Set(),
|
||||||
|
sendStream: null,
|
||||||
|
sendWriter: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
currentConnection = connection;
|
currentConnection = connection;
|
||||||
|
|
@ -793,21 +825,23 @@ export function createTransportClient<T extends SchemaMap>(
|
||||||
|
|
||||||
connection.intentional = true;
|
connection.intentional = true;
|
||||||
setReadyState(READY_STATE.CLOSING);
|
setReadyState(READY_STATE.CLOSING);
|
||||||
|
const acceptLoopDone = connection.acceptLoopDone;
|
||||||
|
|
||||||
rejectPending(new Error("Transport closed"));
|
rejectPending(new Error("Transport closed"));
|
||||||
|
|
||||||
|
try {
|
||||||
|
connection.sendWriter?.releaseLock();
|
||||||
|
await connection.sendStream?.abort();
|
||||||
|
} catch {
|
||||||
|
// Ignore errors during stream abort
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await writeCloseFrame(connection.transport);
|
await writeCloseFrame(connection.transport);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log(1, "Socket", "yellow", "Failed to send close sentinel", error);
|
log(1, "Socket", "yellow", "Failed to send close sentinel", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
connection.streamReader?.cancel().catch(() => undefined);
|
|
||||||
} catch {
|
|
||||||
// Ignore reader cancellation failures during shutdown.
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
connection.transport.close({
|
connection.transport.close({
|
||||||
closeCode: APPLICATION_CLOSE_CODE,
|
closeCode: APPLICATION_CLOSE_CODE,
|
||||||
|
|
@ -819,6 +853,10 @@ export function createTransportClient<T extends SchemaMap>(
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await connection.transport.closed.catch(() => undefined);
|
await connection.transport.closed.catch(() => undefined);
|
||||||
|
await acceptLoopDone;
|
||||||
|
if (connection.activeIncomingTasks.size > 0) {
|
||||||
|
await Promise.allSettled([...connection.activeIncomingTasks]);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
notifyClosed(connection);
|
notifyClosed(connection);
|
||||||
}
|
}
|
||||||
|
|
@ -898,9 +936,9 @@ export function createTransportClient<T extends SchemaMap>(
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!expectsResponse) {
|
if (!expectsResponse) {
|
||||||
return writeMessage(connection.transport, messageBytes).catch(
|
return writeMessageOnPersistentStream(connection, messageBytes).catch(
|
||||||
(error) => {
|
(error) => {
|
||||||
closeFromStopSending(connection, error);
|
handleConnectionFailure(connection, error);
|
||||||
throw error;
|
throw error;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
@ -923,12 +961,14 @@ export function createTransportClient<T extends SchemaMap>(
|
||||||
timeoutId,
|
timeoutId,
|
||||||
});
|
});
|
||||||
|
|
||||||
void writeMessage(connection.transport, messageBytes).catch((error) => {
|
void writeMessageOnPersistentStream(connection, messageBytes).catch(
|
||||||
closeFromStopSending(connection, error);
|
(error) => {
|
||||||
clearTimeout(timeoutId);
|
handleConnectionFailure(connection, error);
|
||||||
pending.delete(requestId);
|
clearTimeout(timeoutId);
|
||||||
reject(error);
|
pending.delete(requestId);
|
||||||
});
|
reject(error);
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
|
|
@ -1055,39 +1095,6 @@ function logBinaryMessage(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Detects whether an error chain includes STOP_SENDING.
|
|
||||||
* @param error Unknown transport error.
|
|
||||||
* @returns True when STOP_SENDING appears in the error chain.
|
|
||||||
*/
|
|
||||||
function isStopSendingError(error: unknown) {
|
|
||||||
if (typeof error === "string") {
|
|
||||||
return error.includes("STOP_SENDING");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error instanceof Error) {
|
|
||||||
if (error.message.includes("STOP_SENDING")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const errorWithCause = error as Error & { cause?: unknown };
|
|
||||||
if (errorWithCause.cause !== undefined) {
|
|
||||||
return isStopSendingError(errorWithCause.cause);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof error === "object" && error !== null) {
|
|
||||||
const maybeMessage = (error as { message?: unknown }).message;
|
|
||||||
if (typeof maybeMessage === "string") {
|
|
||||||
return maybeMessage.includes("STOP_SENDING");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensures outbound message payloads are plain object records.
|
* Ensures outbound message payloads are plain object records.
|
||||||
* @param value Candidate payload.
|
* @param value Candidate payload.
|
||||||
|
|
@ -1170,30 +1177,54 @@ function validateRequestId(id: number, expectsResponse: boolean) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Writes a protocol message payload as a framed unidirectional transport stream.
|
* Writes a protocol message payload as a framed unidirectional transport stream.
|
||||||
* @param transport Active transport instance.
|
* Uses a persistent stream, and retries once if the stream was closed by the receiver.
|
||||||
|
* @param connection Active connection instance.
|
||||||
* @param payload Encoded message payload bytes.
|
* @param payload Encoded message payload bytes.
|
||||||
* @returns Promise that resolves when frame writing is complete.
|
* @returns Promise that resolves when frame writing is complete.
|
||||||
*/
|
*/
|
||||||
async function writeMessage(transport: WebTransportLike, payload: Uint8Array) {
|
async function writeMessageOnPersistentStream(
|
||||||
|
connection: ActiveConnection,
|
||||||
|
payload: Uint8Array,
|
||||||
|
) {
|
||||||
if (payload.byteLength >= CLOSE_FRAME_LEN) {
|
if (payload.byteLength >= CLOSE_FRAME_LEN) {
|
||||||
throw new Error("Message too large for transport frame");
|
throw new Error("Message too large for transport frame");
|
||||||
}
|
}
|
||||||
|
|
||||||
const stream = await transport.createUnidirectionalStream();
|
const frame = new Uint8Array(4 + payload.byteLength);
|
||||||
const writer = stream.getWriter();
|
writeU32(frame, 0, payload.byteLength);
|
||||||
|
frame.set(payload, 4);
|
||||||
|
|
||||||
try {
|
const writeAndCatch = async (): Promise<boolean> => {
|
||||||
const frame = new Uint8Array(4 + payload.byteLength);
|
try {
|
||||||
writeU32(frame, 0, payload.byteLength);
|
if (!connection.sendStream || !connection.sendWriter) {
|
||||||
frame.set(payload, 4);
|
connection.sendStream =
|
||||||
|
await connection.transport.createUnidirectionalStream();
|
||||||
|
connection.sendWriter = connection.sendStream.getWriter();
|
||||||
|
}
|
||||||
|
|
||||||
logBinaryMessage("Outgoing", frame);
|
logBinaryMessage("Outgoing", frame);
|
||||||
|
await connection.sendWriter.write(frame);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
await writer.write(frame);
|
const firstResult = await writeAndCatch();
|
||||||
await writer.close();
|
if (firstResult) return;
|
||||||
} finally {
|
|
||||||
writer.releaseLock();
|
// Retry once
|
||||||
}
|
connection.sendWriter?.releaseLock();
|
||||||
|
connection.sendWriter = null;
|
||||||
|
connection.sendStream = null;
|
||||||
|
|
||||||
|
const secondResult = await writeAndCatch();
|
||||||
|
if (secondResult) return;
|
||||||
|
|
||||||
|
connection.sendWriter = null;
|
||||||
|
connection.sendStream = null;
|
||||||
|
|
||||||
|
throw new Error("Transport stream closed during send");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -1221,6 +1252,7 @@ async function writeCloseFrame(transport: WebTransportLike) {
|
||||||
* @param connection Active connection instance.
|
* @param connection Active connection instance.
|
||||||
* @param handleIncomingFrame Handler for decoded protocol messages.
|
* @param handleIncomingFrame Handler for decoded protocol messages.
|
||||||
* @param handleDecodeFailure Handler for recoverable frame decode failures.
|
* @param handleDecodeFailure Handler for recoverable frame decode failures.
|
||||||
|
* @param discardFrames Whether frames should be drained and discarded.
|
||||||
* @returns True when the peer close sentinel was received.
|
* @returns True when the peer close sentinel was received.
|
||||||
*/
|
*/
|
||||||
async function processIncomingStream(
|
async function processIncomingStream(
|
||||||
|
|
@ -1229,9 +1261,11 @@ async function processIncomingStream(
|
||||||
handleIncomingFrame: (message: TypedMessage) => void,
|
handleIncomingFrame: (message: TypedMessage) => void,
|
||||||
handleDecodeFailure: (error: RecoverableMessageDecodeError) => void,
|
handleDecodeFailure: (error: RecoverableMessageDecodeError) => void,
|
||||||
handleStreamFailure: (connection: ActiveConnection, error?: unknown) => void,
|
handleStreamFailure: (connection: ActiveConnection, error?: unknown) => void,
|
||||||
|
discardFrames: boolean,
|
||||||
) {
|
) {
|
||||||
const reader = stream.getReader();
|
const reader = stream.getReader();
|
||||||
let bufferedBytes = new Uint8Array(0) as Uint8Array<ArrayBufferLike>;
|
let bufferedBytes = new Uint8Array(0) as Uint8Array<ArrayBufferLike>;
|
||||||
|
let peerCloseDetected = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
while (true) {
|
while (true) {
|
||||||
|
|
@ -1242,10 +1276,18 @@ async function processIncomingStream(
|
||||||
|
|
||||||
bufferedBytes = appendBytes(bufferedBytes, value);
|
bufferedBytes = appendBytes(bufferedBytes, value);
|
||||||
|
|
||||||
|
if (discardFrames || peerCloseDetected) {
|
||||||
|
bufferedBytes = new Uint8Array(0) as Uint8Array<ArrayBufferLike>;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
while (bufferedBytes.byteLength >= 4) {
|
while (bufferedBytes.byteLength >= 4) {
|
||||||
const declaredLength = readU32(bufferedBytes, 0);
|
const declaredLength = readU32(bufferedBytes, 0);
|
||||||
|
|
||||||
if (declaredLength === CLOSE_FRAME_LEN) {
|
if (declaredLength === CLOSE_FRAME_LEN) {
|
||||||
|
peerCloseDetected = true;
|
||||||
|
bufferedBytes = new Uint8Array(0) as Uint8Array<ArrayBufferLike>;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
connection.transport.close({
|
connection.transport.close({
|
||||||
closeCode: APPLICATION_CLOSE_CODE,
|
closeCode: APPLICATION_CLOSE_CODE,
|
||||||
|
|
@ -1259,7 +1301,7 @@ async function processIncomingStream(
|
||||||
connection,
|
connection,
|
||||||
new Error("Transport closed by peer"),
|
new Error("Transport closed by peer"),
|
||||||
);
|
);
|
||||||
return true;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const expectedLength = 4 + declaredLength;
|
const expectedLength = 4 + declaredLength;
|
||||||
|
|
@ -1277,20 +1319,24 @@ async function processIncomingStream(
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof RecoverableMessageDecodeError) {
|
if (error instanceof RecoverableMessageDecodeError) {
|
||||||
handleDecodeFailure(error);
|
handleDecodeFailure(error);
|
||||||
continue;
|
} else {
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Just like the backend, drop the stream after receiving exactly one incoming message!
|
||||||
|
return peerCloseDetected;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bufferedBytes.byteLength > 0) {
|
if (!discardFrames && !peerCloseDetected && bufferedBytes.byteLength > 0) {
|
||||||
throw new Error("Received truncated transport frame");
|
throw new Error("Received truncated transport frame");
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return peerCloseDetected;
|
||||||
} finally {
|
} finally {
|
||||||
|
// We cancel the reader to signal the stream is naturally dropped, matching Rust's receiver behavior.
|
||||||
|
reader.cancel().catch(() => {});
|
||||||
reader.releaseLock();
|
reader.releaseLock();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
export const RESPONSE_TIMEOUT = 15_000;
|
export const RESPONSE_TIMEOUT = 15_000;
|
||||||
export const RETRY_COUNT = 10;
|
export const RETRY_COUNT = 10;
|
||||||
export const RETRY_INTERVAL = 3_000;
|
export const RETRY_INTERVAL = 3_000;
|
||||||
export const PING_INTERVAL = 5_000;
|
export const PING_INTERVAL = 3_000;
|
||||||
export const TRANSPORT_URL = "https://methanium.net:959";
|
export const TRANSPORT_URL = "https://methanium.net:959";
|
||||||
|
export const RECONNECT_TRIES = 3;
|
||||||
|
export const RECONNECT_RESET = 6;
|
||||||
Loading…
Reference in a new issue