This commit is contained in:
Andras Schmelczer 2025-11-23 20:27:16 +00:00
parent 4b195b070d
commit 18be9f4dd8
19 changed files with 301 additions and 226 deletions

View file

@ -1,3 +1,4 @@
import type { Mock } from "node:test";
import { describe, it, mock, beforeEach, afterEach } from "node:test";
import assert from "node:assert";
import { FetchController } from "./fetch-controller";
@ -6,7 +7,9 @@ import { SyncResetError } from "./sync-reset-error";
import { sleep } from "../utils/sleep";
describe("FetchController", () => {
const createMockFetch = (shouldSleep: boolean) =>
const createMockFetch = (
shouldSleep: boolean
): Mock<() => Promise<Response>> =>
mock.fn(async () => {
if (shouldSleep) {
await sleep(30);

View file

@ -24,16 +24,6 @@ export class FetchController {
createPromise<symbol>();
}
private static getUrlFromInput(input: RequestInfo | URL): string {
if (input instanceof URL) {
return input.href;
}
if (typeof input === "string") {
return input;
}
return input.url;
}
/**
* Whether the fetch implementation can immediately send requests once outside of a reset.
*/
@ -58,6 +48,16 @@ export class FetchController {
}
}
private static getUrlFromInput(input: RequestInfo | URL): string {
if (input instanceof URL) {
return input.href;
}
if (typeof input === "string") {
return input;
}
return input.url;
}
/**
* Starts a reset, causing all ongoing and future fetches to be rejected
* with a SyncResetError until finishReset is called.

View file

@ -82,7 +82,7 @@ export class WebSocketManager {
}
public async stop(): Promise<void> {
const [promise, resolve] = createPromise<void>();
const [promise, resolve] = createPromise();
this.resolveDisconnectingPromise = resolve;
this.isStopped = true;
@ -99,7 +99,7 @@ export class WebSocketManager {
await promise;
}
await awaitAll(this.outstandingPromises).then(() => {});
await awaitAll(this.outstandingPromises);
}
public sendHandshakeMessage(
@ -164,10 +164,25 @@ export class WebSocketManager {
);
};
this.webSocket.onmessage = async (event): Promise<void> => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const message = JSON.parse(event.data) as WebSocketServerMessage;
return this.handleWebSocketMessage(message);
this.webSocket.onmessage = (event): void => {
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const message = JSON.parse(
event.data
) as WebSocketServerMessage;
void this.handleWebSocketMessage(message).catch(
(error: unknown) => {
this.logger.error(
`Error handling WebSocket message: ${String(error)}`
);
}
);
} catch (error) {
this.logger.error(
`Error parsing WebSocket message: ${String(error)}`
);
}
};
this.webSocket.onclose = (event): void => {
@ -194,42 +209,58 @@ export class WebSocketManager {
message: WebSocketServerMessage
): Promise<void> {
if (message.type === "vaultUpdate") {
this.outstandingPromises.push(
...this.remoteVaultUpdateListeners.map(async (listener) => {
const promise = listener(message);
return promise.finally(() => {
if (this.outstandingPromises.includes(promise)) {
this.outstandingPromises.splice(
this.outstandingPromises.indexOf(promise),
1
const promises = this.remoteVaultUpdateListeners.map(
async (listener) => {
const trackedPromise = listener(message)
.catch((error: unknown) => {
this.logger.error(
`Error in vault update listener: ${String(error)}`
);
}
});
})
})
.finally(() => {
const index =
this.outstandingPromises.indexOf(
trackedPromise
);
if (index !== -1) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.outstandingPromises.splice(index, 1);
}
});
await trackedPromise;
}
);
this.outstandingPromises.push(...promises);
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
} else if (message.type === "cursorPositions") {
this.logger.debug(
`Received cursor positions for ${JSON.stringify(message.clients)}`
);
this.outstandingPromises.push(
...this.remoteCursorsUpdateListeners.map(async (listener) => {
const promise = listener(
message.clients.filter(
(client) => client.deviceId !== this.deviceId
)
);
return promise.finally(() => {
if (this.outstandingPromises.includes(promise)) {
this.outstandingPromises.splice(
this.outstandingPromises.indexOf(promise),
1
);
}
});
})
const filteredClients = message.clients.filter(
(client) => client.deviceId !== this.deviceId
);
const promises = this.remoteCursorsUpdateListeners.map(
async (listener) => {
const trackedPromise = listener(filteredClients)
.catch((error: unknown) => {
this.logger.error(
`Error in cursor positions listener: ${String(error)}`
);
})
.finally(() => {
const index =
this.outstandingPromises.indexOf(
trackedPromise
);
if (index !== -1) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.outstandingPromises.splice(index, 1);
}
});
await trackedPromise;
}
);
this.outstandingPromises.push(...promises);
} else {
this.logger.warn(
`Received unknown message type: ${JSON.stringify(message)}`