diff --git a/extensions/matrix/src/matrix/monitor/status.ts b/extensions/matrix/src/matrix/monitor/status.ts index 3f101a62f00..6e5630a845e 100644 --- a/extensions/matrix/src/matrix/monitor/status.ts +++ b/extensions/matrix/src/matrix/monitor/status.ts @@ -90,6 +90,8 @@ export function createMatrixMonitorStatusController(params: { noteDisconnected({ state, at, error }); return; } + // Unknown future SDK states inherit the current connectivity bit until the + // SDK classifies them as ready or disconnected. Avoid guessing here. status.lastEventAt = at; status.healthState = state.toLowerCase(); emit(); diff --git a/extensions/matrix/src/matrix/monitor/sync-lifecycle.test.ts b/extensions/matrix/src/matrix/monitor/sync-lifecycle.test.ts index 4c1a5f15a84..cd2d9e47f0e 100644 --- a/extensions/matrix/src/matrix/monitor/sync-lifecycle.test.ts +++ b/extensions/matrix/src/matrix/monitor/sync-lifecycle.test.ts @@ -62,4 +62,50 @@ describe("createMatrixMonitorSyncLifecycle", () => { }), ); }); + + it("ignores unexpected sync errors emitted during intentional shutdown", async () => { + const client = createClientEmitter(); + const setStatus = vi.fn(); + let stopping = false; + const lifecycle = createMatrixMonitorSyncLifecycle({ + client: client as never, + statusController: createMatrixMonitorStatusController({ + accountId: "default", + statusSink: setStatus, + }), + isStopping: () => stopping, + }); + + const waitPromise = lifecycle.waitForFatalStop(); + stopping = true; + client.emit("sync.unexpected_error", new Error("shutdown noise")); + lifecycle.dispose(); + + await expect(waitPromise).resolves.toBeUndefined(); + expect(setStatus).not.toHaveBeenCalledWith( + expect.objectContaining({ + accountId: "default", + healthState: "error", + }), + ); + }); + + it("rejects a second concurrent fatal-stop waiter", async () => { + const client = createClientEmitter(); + const lifecycle = createMatrixMonitorSyncLifecycle({ + client: client as never, + statusController: createMatrixMonitorStatusController({ + accountId: "default", + }), + }); + + const firstWait = lifecycle.waitForFatalStop(); + + await expect(lifecycle.waitForFatalStop()).rejects.toThrow( + "Matrix fatal-stop wait already in progress", + ); + + lifecycle.dispose(); + await expect(firstWait).resolves.toBeUndefined(); + }); }); diff --git a/extensions/matrix/src/matrix/monitor/sync-lifecycle.ts b/extensions/matrix/src/matrix/monitor/sync-lifecycle.ts index 36f60021989..13cc27d1db5 100644 --- a/extensions/matrix/src/matrix/monitor/sync-lifecycle.ts +++ b/extensions/matrix/src/matrix/monitor/sync-lifecycle.ts @@ -43,10 +43,11 @@ export function createMatrixMonitorSyncLifecycle(params: { }; const onUnexpectedError = (error: Error) => { - params.statusController.noteUnexpectedError(error); - if (!params.isStopping?.()) { - settleFatal(error); + if (params.isStopping?.()) { + return; } + params.statusController.noteUnexpectedError(error); + settleFatal(error); }; params.client.on("sync.state", onSyncState); @@ -57,6 +58,9 @@ export function createMatrixMonitorSyncLifecycle(params: { if (fatalError) { throw fatalError; } + if (resolveFatalWait || rejectFatalWait) { + throw new Error("Matrix fatal-stop wait already in progress"); + } await new Promise((resolve, reject) => { resolveFatalWait = resolve; rejectFatalWait = (error) => reject(error); diff --git a/extensions/matrix/src/matrix/sdk.test.ts b/extensions/matrix/src/matrix/sdk.test.ts index 81c3cfc1ab4..8dca0eec27e 100644 --- a/extensions/matrix/src/matrix/sdk.test.ts +++ b/extensions/matrix/src/matrix/sdk.test.ts @@ -1070,6 +1070,36 @@ describe("MatrixClient event bridge", () => { await startExpectation; }); + it("clears stale sync state before a restarted sync session waits for fresh readiness", async () => { + matrixJsClient.startClient = vi + .fn(async () => { + queueMicrotask(() => { + matrixJsClient.emit("sync", "PREPARED", null, undefined); + }); + }) + .mockImplementationOnce(async () => { + queueMicrotask(() => { + matrixJsClient.emit("sync", "PREPARED", null, undefined); + }); + }) + .mockImplementationOnce(async () => {}); + + const client = new MatrixClient("https://matrix.example.org", "token"); + + await client.start(); + client.stopSyncWithoutPersist(); + + vi.useFakeTimers(); + const restartPromise = client.start(); + const restartExpectation = expect(restartPromise).rejects.toThrow( + "Matrix client did not reach a ready sync state within 30000ms", + ); + + await vi.advanceTimersByTimeAsync(30_000); + + await restartExpectation; + }); + it("replays outstanding invite rooms at startup", async () => { matrixJsClient.getRooms = vi.fn(() => [ { diff --git a/extensions/matrix/src/matrix/sdk.ts b/extensions/matrix/src/matrix/sdk.ts index 4415f736405..eaceecef873 100644 --- a/extensions/matrix/src/matrix/sdk.ts +++ b/extensions/matrix/src/matrix/sdk.ts @@ -536,6 +536,7 @@ export class MatrixClient { clearInterval(this.idbPersistTimer); this.idbPersistTimer = null; } + this.currentSyncState = null; this.client.stopClient(); this.started = false; }