fix(correctness): call-invite clock skew, forceState, upload cancel (COR-3/5/6)
COR-3 (CallEmbedProvider): the incoming-call lifetime guard distrusted a caller's sender_ts only when it was >20s AHEAD of the server ts. A caller clock that ran SLOW left sender_ts in the past, so the ring auto-dismissed/never showed for a fresh invite. Trust sender_ts only within ±20s of the server ts, else fall back to it (also fixes a NaN path when sender_ts is missing). COR-6 (CallControl): forceState rebuilt CallControlState with 5 args, silently defaulting screenshareAudioMuted to false; pass this.screenshareAudioMuted. COR-5 (uploadContent + useBindUploadAtom): cancelling during the retry back-off was a no-op (mx.cancelUpload only aborts an in-flight request), so the upload resurrected on the next attempt. Thread an AbortSignal: the back-off sleep resolves early on abort and the loop stops with an abort error; the hook aborts a per-upload AbortController on cancel (alongside mx.cancelUpload for the in-flight case). All verified by two review passes (no double-settle / no resurrection); includes their suggested abort-listener cleanup on normal sleep resolution. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+33
-3
@@ -144,6 +144,9 @@ export type ContentUploadOptions = {
|
||||
onProgress?: (progress: UploadProgress) => void;
|
||||
onSuccess: (mxc: string) => void;
|
||||
onError: (error: MatrixError) => void;
|
||||
// Aborts the retry loop, including while it's sleeping between attempts (a
|
||||
// plain mx.cancelUpload only aborts an in-flight request, not the back-off).
|
||||
signal?: AbortSignal;
|
||||
};
|
||||
|
||||
// Build a MatrixError defensively from an unexpected upload response.
|
||||
@@ -192,16 +195,38 @@ export const uploadContent = async (
|
||||
file: TUploadContent,
|
||||
options: ContentUploadOptions,
|
||||
) => {
|
||||
const { name, fileType, hideFilename, onProgress, onPromise, onSuccess, onError } = options;
|
||||
const { name, fileType, hideFilename, onProgress, onPromise, onSuccess, onError, signal } =
|
||||
options;
|
||||
|
||||
// Resolves after `ms`, or early if `signal` aborts (so a cancel during the
|
||||
// back-off is honored immediately instead of waiting out the delay).
|
||||
const sleepForMs = (ms: number) =>
|
||||
new Promise((resolve) => {
|
||||
setTimeout(resolve, ms);
|
||||
new Promise<void>((resolve) => {
|
||||
if (signal?.aborted) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
const onAbort = () => {
|
||||
clearTimeout(timeout);
|
||||
resolve();
|
||||
};
|
||||
const timeout = setTimeout(() => {
|
||||
signal?.removeEventListener('abort', onAbort);
|
||||
resolve();
|
||||
}, ms);
|
||||
signal?.addEventListener('abort', onAbort, { once: true });
|
||||
});
|
||||
|
||||
const abortError = () =>
|
||||
matrixErrorFromUnknown(new DOMException('Upload cancelled', 'AbortError'));
|
||||
|
||||
let lastError: MatrixError | undefined;
|
||||
|
||||
for (let retryCount = 0; retryCount <= UPLOAD_MAX_RETRY_COUNT; retryCount += 1) {
|
||||
if (signal?.aborted) {
|
||||
onError(abortError());
|
||||
return;
|
||||
}
|
||||
const uploadPromise = mx.uploadContent(file, {
|
||||
name,
|
||||
type: fileType,
|
||||
@@ -236,6 +261,11 @@ export const uploadContent = async (
|
||||
Math.min(1000 * 2 ** retryCount, 30_000);
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await sleepForMs(waitMS);
|
||||
// Cancelled during the back-off — stop instead of resurrecting the upload.
|
||||
if (signal?.aborted) {
|
||||
onError(abortError());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user