From 1fcc375dd539024f56de32792f5fc5b150aafe34 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Fri, 9 May 2025 11:16:35 +0100
Subject: [PATCH] Deprecate utils function `defer` in favour of
 `Promise.withResolvers` (#4829)

* Switch from defer to Promise.withResolvers

As supported by the outgoing LTS version (v22) which has 99% support of ES2024

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* delint

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Deprecate defer instead of killing it

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Knip

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate based on review

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate based on review

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate based on review

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Improve coverage

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 knip.ts                                       |  1 +
 spec/integ/crypto/crypto.spec.ts              |  8 ++--
 spec/integ/crypto/megolm-backup.spec.ts       |  5 +--
 spec/integ/crypto/verification.spec.ts        | 26 ++++++-------
 spec/integ/matrix-client-methods.spec.ts      | 21 ++++++++++
 .../rendezvous/MSC4108SignInWithQR.spec.ts    | 15 ++++----
 spec/integ/sliding-sync-sdk.spec.ts           |  3 +-
 spec/unit/ToDeviceMessageQueue.spec.ts        | 19 +++++-----
 spec/unit/http-api/fetch.spec.ts              | 12 +++---
 spec/unit/matrix-client.spec.ts               |  8 ++--
 spec/unit/matrixrtc/MembershipManager.spec.ts |  3 +-
 .../matrixrtc/ToDeviceKeyTransport.spec.ts    | 12 ++++--
 spec/unit/room-state.spec.ts                  |  7 ++--
 .../OutgoingRequestProcessor.spec.ts          |  7 ++--
 .../OutgoingRequestsManager.spec.ts           | 23 ++++++-----
 .../PerSessionKeyBackupDownloader.spec.ts     | 31 ++++++++-------
 spec/unit/rust-crypto/RoomEncryptor.spec.ts   |  9 ++---
 spec/unit/rust-crypto/rust-crypto.spec.ts     |  7 ++--
 spec/unit/scheduler.spec.ts                   | 21 +++++-----
 spec/unit/secret-storage.spec.ts              |  5 +--
 .../stores/indexeddb-store-worker.spec.ts     |  7 ++--
 spec/unit/stores/indexeddb.spec.ts            | 37 +++++++++---------
 src/client.ts                                 | 18 ++++-----
 src/http-api/index.ts                         | 20 +++++-----
 src/interactive-auth.ts                       |  5 +--
 src/matrixrtc/NewMembershipManager.ts         | 16 ++++----
 src/rust-crypto/OutgoingRequestsManager.ts    | 10 ++---
 src/rust-crypto/verification.ts               |  5 +--
 src/scheduler.ts                              | 16 ++++----
 src/store/indexeddb-remote-backend.ts         |  5 +--
 src/sync.ts                                   | 38 +++++++++----------
 src/utils.ts                                  | 21 +++-------
 32 files changed, 220 insertions(+), 221 deletions(-)

diff --git a/knip.ts b/knip.ts
index 4709fdbc3..afffa74b7 100644
--- a/knip.ts
+++ b/knip.ts
@@ -9,6 +9,7 @@ export default {
         "src/crypto-api/index.ts",
         "src/testing.ts",
         "src/matrix.ts",
+        "src/utils.ts", // not really an entrypoint but we have deprecated `defer` there
         "scripts/**",
         "spec/**",
         // XXX: these look entirely unused
diff --git a/spec/integ/crypto/crypto.spec.ts b/spec/integ/crypto/crypto.spec.ts
index 1594753e5..f292f9ff4 100644
--- a/spec/integ/crypto/crypto.spec.ts
+++ b/spec/integ/crypto/crypto.spec.ts
@@ -59,7 +59,7 @@ import {
 } from "../../../src/matrix";
 import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
 import { type ISyncResponder, SyncResponder } from "../../test-utils/SyncResponder";
-import { defer, escapeRegExp } from "../../../src/utils";
+import { escapeRegExp } from "../../../src/utils";
 import { downloadDeviceToJsDevice } from "../../../src/rust-crypto/device-converter";
 import { flushPromises } from "../../test-utils/flushPromises";
 import {
@@ -1283,13 +1283,13 @@ describe("crypto", () => {
             const inboundGroupSessionPromise = expectSendRoomKey("@bob:xyz", testOlmAccount);
 
             // ... and finally, send the room key. We block the response until `sendRoomMessageDefer` completes.
-            const sendRoomMessageDefer = defer<FetchMock.MockResponse>();
+            const sendRoomMessageResolvers = Promise.withResolvers<FetchMock.MockResponse>();
             const reqProm = new Promise<IContent>((resolve) => {
                 fetchMock.putOnce(
                     new RegExp("/send/m.room.encrypted/"),
                     async (url: string, opts: RequestInit): Promise<FetchMock.MockResponse> => {
                         resolve(JSON.parse(opts.body as string));
-                        return await sendRoomMessageDefer.promise;
+                        return await sendRoomMessageResolvers.promise;
                     },
                     {
                         // append to the list of intercepts on this path (since we have some tests that call
@@ -1318,7 +1318,7 @@ describe("crypto", () => {
 
             // release the send request
             const resp = { event_id: "$event_id" };
-            sendRoomMessageDefer.resolve(resp);
+            sendRoomMessageResolvers.resolve(resp);
             expect(await sendProm).toEqual(resp);
 
             // still pending at this point
diff --git a/spec/integ/crypto/megolm-backup.spec.ts b/spec/integ/crypto/megolm-backup.spec.ts
index 897875094..bbb489b10 100644
--- a/spec/integ/crypto/megolm-backup.spec.ts
+++ b/spec/integ/crypto/megolm-backup.spec.ts
@@ -36,7 +36,6 @@ import { advanceTimersUntil, awaitDecryption, syncPromise } from "../../test-uti
 import * as testData from "../../test-utils/test-data";
 import { type KeyBackupInfo, type KeyBackupSession } from "../../../src/crypto-api/keybackup";
 import { flushPromises } from "../../test-utils/flushPromises";
-import { defer, type IDeferred } from "../../../src/utils";
 import { decodeRecoveryKey, DecryptionFailureCode, CryptoEvent, type CryptoApi } from "../../../src/crypto-api";
 import { type KeyBackup } from "../../../src/rust-crypto/backup.ts";
 
@@ -861,7 +860,7 @@ describe("megolm-keys backup", () => {
         expect(await aliceCrypto.getKeyBackupInfo()).toStrictEqual(testData.SIGNED_BACKUP_DATA);
 
         // Delete the backup and we are expecting the key backup to be disabled
-        const keyBackupStatus = defer<boolean>();
+        const keyBackupStatus = Promise.withResolvers<boolean>();
         aliceClient.once(CryptoEvent.KeyBackupStatus, (enabled) => keyBackupStatus.resolve(enabled));
         await aliceCrypto.deleteKeyBackupVersion(testData.SIGNED_BACKUP_DATA.version!);
         expect(await keyBackupStatus.promise).toBe(false);
@@ -1158,7 +1157,7 @@ describe("megolm-keys backup", () => {
             // A check backup should happen at some point
             await aliceCrypto.checkKeyBackupAndEnable();
 
-            const awaitHasQueriedNewBackup: IDeferred<void> = defer<void>();
+            const awaitHasQueriedNewBackup: PromiseWithResolvers<void> = Promise.withResolvers<void>();
 
             fetchMock.get(
                 "express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id",
diff --git a/spec/integ/crypto/verification.spec.ts b/spec/integ/crypto/verification.spec.ts
index 53e49e1d0..99c646ccd 100644
--- a/spec/integ/crypto/verification.spec.ts
+++ b/spec/integ/crypto/verification.spec.ts
@@ -44,7 +44,7 @@ import {
     type Verifier,
     VerifierEvent,
 } from "../../../src/crypto-api/verification";
-import { defer, escapeRegExp } from "../../../src/utils";
+import { escapeRegExp } from "../../../src/utils";
 import { awaitDecryption, emitPromise, getSyncResponse, syncPromise } from "../../test-utils/test-utils";
 import { SyncResponder } from "../../test-utils/SyncResponder";
 import {
@@ -1540,10 +1540,10 @@ function expectSendToDeviceMessage(msgtype: string): Promise<{ messages: any }>
  *  @returns a map of secret name to promise that will resolve (with the id of the secret request) when the secret is requested.
  */
 function mockSecretRequestAndGetPromises(): Map<string, Promise<string>> {
-    const mskRequestDefer = defer<string>();
-    const sskRequestDefer = defer<string>();
-    const uskRequestDefer = defer<string>();
-    const backupKeyRequestDefer = defer<string>();
+    const mskRequestResolvers = Promise.withResolvers<string>();
+    const sskRequestResolvers = Promise.withResolvers<string>();
+    const uskRequestResolvers = Promise.withResolvers<string>();
+    const backupKeyRequestResolvers = Promise.withResolvers<string>();
 
     fetchMock.put(
         new RegExp(`/_matrix/client/(r0|v3)/sendToDevice/m.secret.request`),
@@ -1555,13 +1555,13 @@ function mockSecretRequestAndGetPromises(): Map<string, Promise<string>> {
                 const name = content.name;
                 const requestId = content.request_id;
                 if (name == "m.cross_signing.user_signing") {
-                    uskRequestDefer.resolve(requestId);
+                    uskRequestResolvers.resolve(requestId);
                 } else if (name == "m.cross_signing.master") {
-                    mskRequestDefer.resolve(requestId);
+                    mskRequestResolvers.resolve(requestId);
                 } else if (name == "m.cross_signing.self_signing") {
-                    sskRequestDefer.resolve(requestId);
+                    sskRequestResolvers.resolve(requestId);
                 } else if (name == "m.megolm_backup.v1") {
-                    backupKeyRequestDefer.resolve(requestId);
+                    backupKeyRequestResolvers.resolve(requestId);
                 }
             }
             return {};
@@ -1570,10 +1570,10 @@ function mockSecretRequestAndGetPromises(): Map<string, Promise<string>> {
     );
 
     const promiseMap = new Map<string, Promise<string>>();
-    promiseMap.set("m.cross_signing.master", mskRequestDefer.promise);
-    promiseMap.set("m.cross_signing.self_signing", sskRequestDefer.promise);
-    promiseMap.set("m.cross_signing.user_signing", uskRequestDefer.promise);
-    promiseMap.set("m.megolm_backup.v1", backupKeyRequestDefer.promise);
+    promiseMap.set("m.cross_signing.master", mskRequestResolvers.promise);
+    promiseMap.set("m.cross_signing.self_signing", sskRequestResolvers.promise);
+    promiseMap.set("m.cross_signing.user_signing", uskRequestResolvers.promise);
+    promiseMap.set("m.megolm_backup.v1", backupKeyRequestResolvers.promise);
     return promiseMap;
 }
 
diff --git a/spec/integ/matrix-client-methods.spec.ts b/spec/integ/matrix-client-methods.spec.ts
index 1964cbe13..f4e207b50 100644
--- a/spec/integ/matrix-client-methods.spec.ts
+++ b/spec/integ/matrix-client-methods.spec.ts
@@ -1796,6 +1796,27 @@ describe("MatrixClient", function () {
             expect(client.getUserIdLocalpart()).toBe("alice");
         });
     });
+
+    describe("setRoomMutePushRule", () => {
+        it("should set room push rule to muted", async () => {
+            const roomId = "!roomId:server";
+            const client = new MatrixClient({
+                baseUrl: "http://localhost",
+                fetchFn: httpBackend.fetchFn as typeof globalThis.fetch,
+            });
+            client.pushRules = {
+                global: {
+                    room: [{ rule_id: roomId, actions: [], default: false, enabled: false }],
+                },
+            };
+
+            const path = `/pushrules/global/room/${encodeURIComponent(roomId)}`;
+            httpBackend.when("DELETE", path).respond(200, {});
+            httpBackend.when("PUT", path).respond(200, {});
+            client.setRoomMutePushRule("global", roomId, true);
+            await httpBackend.flush("");
+        });
+    });
 });
 
 function withThreadId(event: MatrixEvent, newThreadId: string): MatrixEvent {
diff --git a/spec/integ/rendezvous/MSC4108SignInWithQR.spec.ts b/spec/integ/rendezvous/MSC4108SignInWithQR.spec.ts
index dd956bea2..c2c608cb8 100644
--- a/spec/integ/rendezvous/MSC4108SignInWithQR.spec.ts
+++ b/spec/integ/rendezvous/MSC4108SignInWithQR.spec.ts
@@ -26,7 +26,6 @@ import {
     PayloadType,
     RendezvousError,
 } from "../../../src/rendezvous";
-import { defer } from "../../../src/utils";
 import {
     ClientPrefix,
     DEVICE_CODE_SCOPE,
@@ -112,8 +111,8 @@ describe("MSC4108SignInWithQR", () => {
         let opponentLogin: MSC4108SignInWithQR;
 
         beforeEach(async () => {
-            let ourData = defer<string>();
-            let opponentData = defer<string>();
+            let ourData = Promise.withResolvers<string>();
+            let opponentData = Promise.withResolvers<string>();
 
             const ourMockSession = {
                 send: jest.fn(async (newData) => {
@@ -122,7 +121,7 @@ describe("MSC4108SignInWithQR", () => {
                 receive: jest.fn(() => {
                     const prom = opponentData.promise;
                     prom.then(() => {
-                        opponentData = defer();
+                        opponentData = Promise.withResolvers();
                     });
                     return prom;
                 }),
@@ -141,7 +140,7 @@ describe("MSC4108SignInWithQR", () => {
                 receive: jest.fn(() => {
                     const prom = ourData.promise;
                     prom.then(() => {
-                        ourData = defer();
+                        ourData = Promise.withResolvers();
                     });
                     return prom;
                 }),
@@ -334,11 +333,11 @@ describe("MSC4108SignInWithQR", () => {
             // @ts-ignore
             await opponentLogin.receive();
 
-            const deferred = defer<IMyDevice>();
-            mocked(client.getDevice).mockReturnValue(deferred.promise);
+            const deviceResolvers = Promise.withResolvers<IMyDevice>();
+            mocked(client.getDevice).mockReturnValue(deviceResolvers.promise);
 
             ourLogin.cancel(MSC4108FailureReason.UserCancelled).catch(() => {});
-            deferred.resolve({} as IMyDevice);
+            deviceResolvers.resolve({} as IMyDevice);
 
             const secrets = {
                 cross_signing: { master_key: "mk", user_signing_key: "usk", self_signing_key: "ssk" },
diff --git a/spec/integ/sliding-sync-sdk.spec.ts b/spec/integ/sliding-sync-sdk.spec.ts
index cf249b4b4..449090a17 100644
--- a/spec/integ/sliding-sync-sdk.spec.ts
+++ b/spec/integ/sliding-sync-sdk.spec.ts
@@ -48,7 +48,6 @@ import { type SyncApiOptions, SyncState } from "../../src/sync";
 import { type IStoredClientOpts } from "../../src";
 import { logger } from "../../src/logger";
 import { emitPromise } from "../test-utils/test-utils";
-import { defer } from "../../src/utils";
 import { KnownMembership } from "../../src/@types/membership";
 import { type SyncCryptoCallbacks } from "../../src/common-crypto/CryptoBackend";
 
@@ -369,7 +368,7 @@ describe("SlidingSyncSdk", () => {
             });
 
             it("can be created with live events", async () => {
-                const seenLiveEventDeferred = defer<boolean>();
+                const seenLiveEventDeferred = Promise.withResolvers<boolean>();
                 const listener = (
                     ev: MatrixEvent,
                     room?: Room,
diff --git a/spec/unit/ToDeviceMessageQueue.spec.ts b/spec/unit/ToDeviceMessageQueue.spec.ts
index 765d85e4f..a3c86585e 100644
--- a/spec/unit/ToDeviceMessageQueue.spec.ts
+++ b/spec/unit/ToDeviceMessageQueue.spec.ts
@@ -5,7 +5,6 @@ import { getMockClientWithEventEmitter } from "../test-utils/client";
 import { StubStore } from "../../src/store/stub";
 import { type IndexedToDeviceBatch } from "../../src/models/ToDeviceMessage";
 import { SyncState } from "../../src/sync";
-import { defer } from "../../src/utils";
 
 describe("onResumedSync", () => {
     let batch: IndexedToDeviceBatch | null;
@@ -60,7 +59,7 @@ describe("onResumedSync", () => {
     });
 
     it("resends queue after connectivity restored", async () => {
-        const deferred = defer();
+        const successResolvers = Promise.withResolvers<void>();
 
         onSendToDeviceFailure = () => {
             expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(1);
@@ -73,15 +72,15 @@ describe("onResumedSync", () => {
         onSendToDeviceSuccess = () => {
             expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(3);
             expect(store.removeToDeviceBatch).toHaveBeenCalled();
-            deferred.resolve();
+            successResolvers.resolve();
         };
 
         queue.start();
-        return deferred.promise;
+        return successResolvers.promise;
     });
 
     it("does not resend queue if client sync still catching up", async () => {
-        const deferred = defer();
+        const successResolvers = Promise.withResolvers<void>();
 
         onSendToDeviceFailure = () => {
             expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(1);
@@ -89,15 +88,15 @@ describe("onResumedSync", () => {
 
             resumeSync(SyncState.Catchup, SyncState.Catchup);
             expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(1);
-            deferred.resolve();
+            successResolvers.resolve();
         };
 
         queue.start();
-        return deferred.promise;
+        return successResolvers.promise;
     });
 
     it("does not resend queue if connectivity restored after queue stopped", async () => {
-        const deferred = defer();
+        const successResolvers = Promise.withResolvers<void>();
 
         onSendToDeviceFailure = () => {
             expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(1);
@@ -107,10 +106,10 @@ describe("onResumedSync", () => {
 
             resumeSync(SyncState.Syncing, SyncState.Catchup);
             expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(1);
-            deferred.resolve();
+            successResolvers.resolve();
         };
 
         queue.start();
-        return deferred.promise;
+        return successResolvers.promise;
     });
 });
diff --git a/spec/unit/http-api/fetch.spec.ts b/spec/unit/http-api/fetch.spec.ts
index ae4ee8017..1f6355792 100644
--- a/spec/unit/http-api/fetch.spec.ts
+++ b/spec/unit/http-api/fetch.spec.ts
@@ -29,7 +29,7 @@ import {
     Method,
 } from "../../../src";
 import { emitPromise } from "../../test-utils/test-utils";
-import { defer, type QueryDict, sleep } from "../../../src/utils";
+import { type QueryDict, sleep } from "../../../src/utils";
 import { type Logger } from "../../../src/logger";
 
 describe("FetchHttpApi", () => {
@@ -525,8 +525,8 @@ describe("FetchHttpApi", () => {
 
     it("should not log query parameters", async () => {
         jest.useFakeTimers();
-        const deferred = defer<Response>();
-        const fetchFn = jest.fn().mockReturnValue(deferred.promise);
+        const responseResolvers = Promise.withResolvers<Response>();
+        const fetchFn = jest.fn().mockReturnValue(responseResolvers.promise);
         const mockLogger = {
             debug: jest.fn(),
         } as unknown as Mocked<Logger>;
@@ -538,7 +538,7 @@ describe("FetchHttpApi", () => {
         });
         const prom = api.requestOtherUrl(Method.Get, "https://server:8448/some/path?query=param#fragment");
         jest.advanceTimersByTime(1234);
-        deferred.resolve({ ok: true, status: 200, text: () => Promise.resolve("RESPONSE") } as Response);
+        responseResolvers.resolve({ ok: true, status: 200, text: () => Promise.resolve("RESPONSE") } as Response);
         await prom;
         expect(mockLogger.debug).not.toHaveBeenCalledWith("fragment");
         expect(mockLogger.debug).not.toHaveBeenCalledWith("query");
@@ -557,7 +557,7 @@ describe("FetchHttpApi", () => {
     });
 
     it("should not make multiple concurrent refresh token requests", async () => {
-        const deferredTokenRefresh = defer<{ accessToken: string; refreshToken: string }>();
+        const deferredTokenRefresh = Promise.withResolvers<{ accessToken: string; refreshToken: string }>();
         const fetchFn = jest.fn().mockResolvedValue({
             ok: false,
             status: tokenInactiveError.httpStatus,
@@ -612,7 +612,7 @@ describe("FetchHttpApi", () => {
     });
 
     it("should use newly refreshed token if request starts mid-refresh", async () => {
-        const deferredTokenRefresh = defer<{ accessToken: string; refreshToken: string }>();
+        const deferredTokenRefresh = Promise.withResolvers<{ accessToken: string; refreshToken: string }>();
         const fetchFn = jest.fn().mockResolvedValue({
             ok: false,
             status: tokenInactiveError.httpStatus,
diff --git a/spec/unit/matrix-client.spec.ts b/spec/unit/matrix-client.spec.ts
index 671924975..f5299a346 100644
--- a/spec/unit/matrix-client.spec.ts
+++ b/spec/unit/matrix-client.spec.ts
@@ -73,7 +73,7 @@ import {
     PolicyRecommendation,
     PolicyScope,
 } from "../../src/models/invites-ignorer";
-import { defer, type QueryDict } from "../../src/utils";
+import { type QueryDict } from "../../src/utils";
 import { type SyncState } from "../../src/sync";
 import * as featureUtils from "../../src/feature";
 import { StubStore } from "../../src/store/stub";
@@ -1997,8 +1997,8 @@ describe("MatrixClient", function () {
         });
 
         it("should cancel an event which is encrypting", async () => {
-            const encryptEventDefer = defer();
-            mockCrypto.encryptEvent.mockReturnValue(encryptEventDefer.promise);
+            const encryptEventResolvers = Promise.withResolvers<void>();
+            mockCrypto.encryptEvent.mockReturnValue(encryptEventResolvers.promise);
 
             const statusPromise = testUtils.emitPromise(event, "Event.status");
             // @ts-ignore protected method access
@@ -2009,7 +2009,7 @@ describe("MatrixClient", function () {
             assertCancelled();
 
             // now let the encryption complete, and check that the message is not sent.
-            encryptEventDefer.resolve();
+            encryptEventResolvers.resolve();
             await encryptAndSendPromise;
             assertCancelled();
         });
diff --git a/spec/unit/matrixrtc/MembershipManager.spec.ts b/spec/unit/matrixrtc/MembershipManager.spec.ts
index f0173506c..afc339f41 100644
--- a/spec/unit/matrixrtc/MembershipManager.spec.ts
+++ b/spec/unit/matrixrtc/MembershipManager.spec.ts
@@ -30,7 +30,6 @@ import {
 import { LegacyMembershipManager } from "../../../src/matrixrtc/LegacyMembershipManager";
 import { makeMockClient, makeMockRoom, membershipTemplate, mockCallMembership, type MockClient } from "./mocks";
 import { MembershipManager } from "../../../src/matrixrtc/NewMembershipManager";
-import { defer } from "../../../src/utils";
 import { logger } from "../../../src/logger.ts";
 
 function waitForMockCall(method: MockedFunction<any>, returnVal?: Promise<any>) {
@@ -51,7 +50,7 @@ function waitForMockCallOnce(method: MockedFunction<any>, returnVal?: Promise<an
 }
 
 function createAsyncHandle(method: MockedFunction<any>) {
-    const { reject, resolve, promise } = defer();
+    const { reject, resolve, promise } = Promise.withResolvers<void>();
     method.mockImplementation(() => promise);
     return { reject, resolve };
 }
diff --git a/spec/unit/matrixrtc/ToDeviceKeyTransport.spec.ts b/spec/unit/matrixrtc/ToDeviceKeyTransport.spec.ts
index e120eeb88..80ff7fc4e 100644
--- a/spec/unit/matrixrtc/ToDeviceKeyTransport.spec.ts
+++ b/spec/unit/matrixrtc/ToDeviceKeyTransport.spec.ts
@@ -22,7 +22,6 @@ import { ToDeviceKeyTransport } from "../../../src/matrixrtc/ToDeviceKeyTranspor
 import { getMockClientWithEventEmitter } from "../../test-utils/client.ts";
 import { type Statistics } from "../../../src/matrixrtc";
 import { KeyTransportEvents } from "../../../src/matrixrtc/IKeyTransport.ts";
-import { defer } from "../../../src/utils.ts";
 import { type Logger } from "../../../src/logger.ts";
 
 describe("ToDeviceKeyTransport", () => {
@@ -108,9 +107,14 @@ describe("ToDeviceKeyTransport", () => {
     });
 
     it("should emit when a key is received", async () => {
-        const deferred = defer<{ userId: string; deviceId: string; keyBase64Encoded: string; index: number }>();
+        const receivedKeyResolvers = Promise.withResolvers<{
+            userId: string;
+            deviceId: string;
+            keyBase64Encoded: string;
+            index: number;
+        }>();
         transport.on(KeyTransportEvents.ReceivedKeys, (userId, deviceId, keyBase64Encoded, index, timestamp) => {
-            deferred.resolve({ userId, deviceId, keyBase64Encoded, index });
+            receivedKeyResolvers.resolve({ userId, deviceId, keyBase64Encoded, index });
         });
         transport.start();
 
@@ -136,7 +140,7 @@ describe("ToDeviceKeyTransport", () => {
             }),
         );
 
-        const { userId, deviceId, keyBase64Encoded, index } = await deferred.promise;
+        const { userId, deviceId, keyBase64Encoded, index } = await receivedKeyResolvers.promise;
         expect(userId).toBe("@bob:example.org");
         expect(deviceId).toBe("BOBDEVICE");
         expect(keyBase64Encoded).toBe(testEncoded);
diff --git a/spec/unit/room-state.spec.ts b/spec/unit/room-state.spec.ts
index ab8ecda9a..da3f5de6c 100644
--- a/spec/unit/room-state.spec.ts
+++ b/spec/unit/room-state.spec.ts
@@ -25,7 +25,6 @@ import { EventType, RelationType, UNSTABLE_MSC2716_MARKER } from "../../src/@typ
 import { MatrixEvent, MatrixEventEvent } from "../../src/models/event";
 import { M_BEACON } from "../../src/@types/beacon";
 import { type MatrixClient } from "../../src/client";
-import { defer } from "../../src/utils";
 import { Room } from "../../src/models/room";
 import { KnownMembership } from "../../src/@types/membership";
 import { DecryptionFailureCode } from "../../src/crypto-api";
@@ -1084,8 +1083,8 @@ describe("RoomState", function () {
                     timestamp: Date.now() + 1,
                 });
 
-                const deferred = defer<void>();
-                mockClient.decryptEventIfNeeded.mockReturnValue(deferred.promise);
+                const decryptEventResolvers = Promise.withResolvers<void>();
+                mockClient.decryptEventIfNeeded.mockReturnValue(decryptEventResolvers.promise);
 
                 state.setStateEvents([beacon1, beacon2]);
                 const beacon = state.beacons.get(getBeaconInfoIdentifier(beacon1)) as Beacon;
@@ -1095,7 +1094,7 @@ describe("RoomState", function () {
                 // update type after '''decryption'''
                 decryptingRelatedEvent.event.type = M_BEACON.name;
                 decryptingRelatedEvent.event.content = locationEvent.event.content;
-                deferred.resolve();
+                decryptEventResolvers.resolve();
                 await prom;
 
                 expect(addLocationsSpy).toHaveBeenCalledWith([decryptingRelatedEvent]);
diff --git a/spec/unit/rust-crypto/OutgoingRequestProcessor.spec.ts b/spec/unit/rust-crypto/OutgoingRequestProcessor.spec.ts
index e278dfbd8..77a901b45 100644
--- a/spec/unit/rust-crypto/OutgoingRequestProcessor.spec.ts
+++ b/spec/unit/rust-crypto/OutgoingRequestProcessor.spec.ts
@@ -40,7 +40,6 @@ import {
     type UIAuthCallback,
 } from "../../../src";
 import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";
-import { defer } from "../../../src/utils";
 
 describe("OutgoingRequestProcessor", () => {
     /** the OutgoingRequestProcessor implementation under test */
@@ -285,13 +284,13 @@ describe("OutgoingRequestProcessor", () => {
             new RustSdkCryptoJs.DeviceId("TEST_DEVICE"),
         );
 
-        const authRequestResultDefer = defer<string>();
+        const authRequestResultResolvers = Promise.withResolvers<string>();
 
         const authRequestCalledPromise = new Promise<void>((resolve) => {
             const mockHttpApi = {
                 authedRequest: async () => {
                     resolve();
-                    return await authRequestResultDefer.promise;
+                    return await authRequestResultResolvers.promise;
                 },
             } as unknown as Mocked<MatrixHttpApi<IHttpOpts & { onlyData: true }>>;
             processor = new OutgoingRequestProcessor(olmMachine, mockHttpApi);
@@ -308,7 +307,7 @@ describe("OutgoingRequestProcessor", () => {
         olmMachine.close();
 
         // the HTTP request completes...
-        authRequestResultDefer.resolve("{}");
+        authRequestResultResolvers.resolve("{}");
 
         // ... and `makeOutgoingRequest` resolves satisfactorily
         await result;
diff --git a/spec/unit/rust-crypto/OutgoingRequestsManager.spec.ts b/spec/unit/rust-crypto/OutgoingRequestsManager.spec.ts
index 5772efbc2..429df62b8 100644
--- a/spec/unit/rust-crypto/OutgoingRequestsManager.spec.ts
+++ b/spec/unit/rust-crypto/OutgoingRequestsManager.spec.ts
@@ -20,7 +20,6 @@ import { type OutgoingRequest } from "@matrix-org/matrix-sdk-crypto-wasm";
 
 import { type OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";
 import { OutgoingRequestsManager } from "../../../src/rust-crypto/OutgoingRequestsManager";
-import { defer, type IDeferred } from "../../../src/utils";
 import { logger } from "../../../src/logger";
 
 describe("OutgoingRequestsManager", () => {
@@ -70,11 +69,11 @@ describe("OutgoingRequestsManager", () => {
             const request2 = new RustSdkCryptoJs.KeysUploadRequest("foo2", "{}");
             const request3 = new RustSdkCryptoJs.KeysBackupRequest("foo3", "{}", "1");
 
-            const firstOutgoingRequestDefer = defer<OutgoingRequest[]>();
+            const firstOutgoingRequestResolvers = Promise.withResolvers<OutgoingRequest[]>();
 
             olmMachine.outgoingRequests
                 .mockImplementationOnce(async () => {
-                    return firstOutgoingRequestDefer.promise;
+                    return firstOutgoingRequestResolvers.promise;
                 })
                 .mockImplementationOnce(async () => {
                     return [request3];
@@ -87,7 +86,7 @@ describe("OutgoingRequestsManager", () => {
             const thirdRequest = manager.doProcessOutgoingRequests();
 
             // let the first request complete
-            firstOutgoingRequestDefer.resolve([request1, request2]);
+            firstOutgoingRequestResolvers.resolve([request1, request2]);
 
             await firstRequest;
             await secondRequest;
@@ -112,10 +111,10 @@ describe("OutgoingRequestsManager", () => {
             const outgoingRequestCalledPromises: Promise<void>[] = [];
 
             // deferreds which will provide the results of OlmMachine.outgoingRequests
-            const outgoingRequestResultDeferreds: IDeferred<OutgoingRequest[]>[] = [];
+            const outgoingRequestResultDeferreds: PromiseWithResolvers<OutgoingRequest[]>[] = [];
 
             for (let i = 0; i < 3; i++) {
-                const resultDeferred = defer<OutgoingRequest[]>();
+                const resultDeferred = Promise.withResolvers<OutgoingRequest[]>();
                 const calledPromise = new Promise<void>((resolve) => {
                     olmMachine.outgoingRequests.mockImplementationOnce(() => {
                         resolve();
@@ -187,10 +186,10 @@ describe("OutgoingRequestsManager", () => {
         it("When the manager is stopped after outgoingRequests() call, do not make sever requests", async () => {
             const request1 = new RustSdkCryptoJs.KeysQueryRequest("foo", "{}");
 
-            const firstOutgoingRequestDefer = defer<OutgoingRequest[]>();
+            const firstOutgoingRequestResolvers = Promise.withResolvers<OutgoingRequest[]>();
 
             olmMachine.outgoingRequests.mockImplementationOnce(async (): Promise<OutgoingRequest[]> => {
-                return firstOutgoingRequestDefer.promise;
+                return firstOutgoingRequestResolvers.promise;
             });
 
             const firstRequest = manager.doProcessOutgoingRequests();
@@ -199,7 +198,7 @@ describe("OutgoingRequestsManager", () => {
             manager.stop();
 
             // let the first request complete
-            firstOutgoingRequestDefer.resolve([request1]);
+            firstOutgoingRequestResolvers.resolve([request1]);
 
             await firstRequest;
 
@@ -210,7 +209,7 @@ describe("OutgoingRequestsManager", () => {
             const request1 = new RustSdkCryptoJs.KeysQueryRequest("11", "{}");
             const request2 = new RustSdkCryptoJs.KeysUploadRequest("12", "{}");
 
-            const firstRequestDefer = defer<void>();
+            const firstRequestResolvers = Promise.withResolvers<void>();
 
             olmMachine.outgoingRequests.mockImplementationOnce(async (): Promise<OutgoingRequest[]> => {
                 return [request1, request2];
@@ -219,7 +218,7 @@ describe("OutgoingRequestsManager", () => {
             processor.makeOutgoingRequest
                 .mockImplementationOnce(async () => {
                     manager.stop();
-                    return firstRequestDefer.promise;
+                    return firstRequestResolvers.promise;
                 })
                 .mockImplementationOnce(async () => {
                     return;
@@ -227,7 +226,7 @@ describe("OutgoingRequestsManager", () => {
 
             const firstRequest = manager.doProcessOutgoingRequests();
 
-            firstRequestDefer.resolve();
+            firstRequestResolvers.resolve();
 
             await firstRequest;
 
diff --git a/spec/unit/rust-crypto/PerSessionKeyBackupDownloader.spec.ts b/spec/unit/rust-crypto/PerSessionKeyBackupDownloader.spec.ts
index 43cf68798..2691b95e4 100644
--- a/spec/unit/rust-crypto/PerSessionKeyBackupDownloader.spec.ts
+++ b/spec/unit/rust-crypto/PerSessionKeyBackupDownloader.spec.ts
@@ -21,7 +21,6 @@ import fetchMock from "fetch-mock-jest";
 
 import { PerSessionKeyBackupDownloader } from "../../../src/rust-crypto/PerSessionKeyBackupDownloader";
 import { logger } from "../../../src/logger";
-import { defer, type IDeferred } from "../../../src/utils";
 import {
     type RustBackupCryptoEventMap,
     type RustBackupCryptoEvents,
@@ -57,15 +56,15 @@ describe("PerSessionKeyBackupDownloader", () => {
     let mockOlmMachine: Mocked<OlmMachine>;
     let mockBackupDecryptor: Mocked<BackupDecryptor>;
 
-    let expectedSession: { [roomId: string]: { [sessionId: string]: IDeferred<void> } };
+    let expectedSession: { [roomId: string]: { [sessionId: string]: PromiseWithResolvers<void> } };
 
     function expectSessionImported(roomId: string, sessionId: string) {
-        const deferred = defer<void>();
+        const sessionImportedResolvers = Promise.withResolvers<void>();
         if (!expectedSession[roomId]) {
             expectedSession[roomId] = {};
         }
-        expectedSession[roomId][sessionId] = deferred;
-        return deferred.promise;
+        expectedSession[roomId][sessionId] = sessionImportedResolvers;
+        return sessionImportedResolvers.promise;
     }
 
     function mockClearSession(sessionId: string): Mocked<IMegolmSessionData> {
@@ -115,9 +114,9 @@ describe("PerSessionKeyBackupDownloader", () => {
         mockRustBackupManager.importBackedUpRoomKeys.mockImplementation(async (keys) => {
             const roomId = keys[0].room_id;
             const sessionId = keys[0].session_id;
-            const deferred = expectedSession[roomId] && expectedSession[roomId][sessionId];
-            if (deferred) {
-                deferred.resolve();
+            const sessionImportedResolvers = expectedSession[roomId] && expectedSession[roomId][sessionId];
+            if (sessionImportedResolvers) {
+                sessionImportedResolvers.resolve();
             }
         });
 
@@ -143,7 +142,7 @@ describe("PerSessionKeyBackupDownloader", () => {
         });
 
         it("Should download and import a missing key from backup", async () => {
-            const awaitKeyImported = defer<void>();
+            const awaitKeyImported = Promise.withResolvers<void>();
             const roomId = "!roomId";
             const sessionId = "sessionId";
             const expectAPICall = new Promise<void>((resolve) => {
@@ -168,14 +167,14 @@ describe("PerSessionKeyBackupDownloader", () => {
         });
 
         it("Should not hammer the backup if the key is requested repeatedly", async () => {
-            const blockOnServerRequest = defer<void>();
+            const blockOnServerRequest = Promise.withResolvers<void>();
 
             fetchMock.get(`express:/_matrix/client/v3/room_keys/keys/!roomId/:session_id`, async (url, request) => {
                 await blockOnServerRequest.promise;
                 return [mockCipherKey];
             });
 
-            const awaitKey2Imported = defer<void>();
+            const awaitKey2Imported = Promise.withResolvers<void>();
 
             mockRustBackupManager.importBackedUpRoomKeys.mockImplementation(async (keys) => {
                 if (keys[0].session_id === "sessionId2") {
@@ -267,8 +266,8 @@ describe("PerSessionKeyBackupDownloader", () => {
 
         it("Should stop properly", async () => {
             // Simulate a call to stop while request is in flight
-            const blockOnServerRequest = defer<void>();
-            const requestRoomKeyCalled = defer<void>();
+            const blockOnServerRequest = Promise.withResolvers<void>();
+            const requestRoomKeyCalled = Promise.withResolvers<void>();
 
             // Mock the request to block
             fetchMock.get(`express:/_matrix/client/v3/room_keys/keys/:roomId/:sessionId`, async (url, request) => {
@@ -490,7 +489,7 @@ describe("PerSessionKeyBackupDownloader", () => {
 
             // @ts-ignore access to private function
             const keyQuerySpy: SpyInstance = jest.spyOn(downloader, "queryKeyBackup");
-            const rateDeferred = defer<void>();
+            const rateDeferred = Promise.withResolvers<void>();
 
             keyQuerySpy.mockImplementation(
                 // @ts-ignore
@@ -544,7 +543,7 @@ describe("PerSessionKeyBackupDownloader", () => {
 
             // @ts-ignore
             const keyQuerySpy: SpyInstance = jest.spyOn(downloader, "queryKeyBackup");
-            const errorDeferred = defer<void>();
+            const errorDeferred = Promise.withResolvers<void>();
 
             keyQuerySpy.mockImplementation(
                 // @ts-ignore
@@ -588,7 +587,7 @@ describe("PerSessionKeyBackupDownloader", () => {
         });
 
         it("On Unknown error on import skip the key and continue", async () => {
-            const keyImported = defer<void>();
+            const keyImported = Promise.withResolvers<void>();
             mockRustBackupManager.importBackedUpRoomKeys
                 .mockImplementationOnce(async () => {
                     throw new Error("Didn't work");
diff --git a/spec/unit/rust-crypto/RoomEncryptor.spec.ts b/spec/unit/rust-crypto/RoomEncryptor.spec.ts
index b7263ac45..83993e6aa 100644
--- a/spec/unit/rust-crypto/RoomEncryptor.spec.ts
+++ b/spec/unit/rust-crypto/RoomEncryptor.spec.ts
@@ -29,7 +29,6 @@ import { type Mocked } from "jest-mock";
 import { HistoryVisibility, type MatrixEvent, type Room, type RoomMember } from "../../../src";
 import { RoomEncryptor, toRustHistoryVisibility } from "../../../src/rust-crypto/RoomEncryptor";
 import { type KeyClaimManager } from "../../../src/rust-crypto/KeyClaimManager";
-import { defer } from "../../../src/utils";
 import { type OutgoingRequestsManager } from "../../../src/rust-crypto/OutgoingRequestsManager";
 import { KnownMembership } from "../../../src/@types/membership";
 import {
@@ -120,8 +119,8 @@ describe("RoomEncryptor", () => {
         const defaultDevicesIsolationMode = new AllDevicesIsolationMode(false);
 
         it("should ensure that there is only one shareRoomKey at a time", async () => {
-            const deferredShare = defer<void>();
-            const insideOlmShareRoom = defer<void>();
+            const deferredShare = Promise.withResolvers<void>();
+            const insideOlmShareRoom = Promise.withResolvers<void>();
 
             mockOlmMachine.shareRoomKey.mockImplementationOnce(async () => {
                 insideOlmShareRoom.resolve();
@@ -149,8 +148,8 @@ describe("RoomEncryptor", () => {
 
         // Regression test for https://github.com/element-hq/element-web/issues/26684
         it("Should maintain order of encryption requests", async () => {
-            const firstTargetMembers = defer<void>();
-            const secondTargetMembers = defer<void>();
+            const firstTargetMembers = Promise.withResolvers<void>();
+            const secondTargetMembers = Promise.withResolvers<void>();
 
             mockOlmMachine.shareRoomKey.mockResolvedValue([]);
 
diff --git a/spec/unit/rust-crypto/rust-crypto.spec.ts b/spec/unit/rust-crypto/rust-crypto.spec.ts
index 987de13ea..65804d7a1 100644
--- a/spec/unit/rust-crypto/rust-crypto.spec.ts
+++ b/spec/unit/rust-crypto/rust-crypto.spec.ts
@@ -68,7 +68,6 @@ import {
 import * as testData from "../../test-utils/test-data";
 import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
 import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
-import { defer } from "../../../src/utils";
 import { logger } from "../../../src/logger";
 import { OutgoingRequestsManager } from "../../../src/rust-crypto/OutgoingRequestsManager";
 import { ClientEvent, type ClientEventHandlerMap } from "../../../src/client";
@@ -928,8 +927,8 @@ describe("RustCrypto", () => {
 
         it("should go round the loop again if another sync completes while the first `outgoingRequests` is running", async () => {
             // the first call to `outgoingMessages` will return a promise which blocks for a while
-            const firstOutgoingRequestsDefer = defer<Array<any>>();
-            mocked(olmMachine.outgoingRequests).mockReturnValueOnce(firstOutgoingRequestsDefer.promise);
+            const firstOutgoingRequestsResolvers = Promise.withResolvers<Array<any>>();
+            mocked(olmMachine.outgoingRequests).mockReturnValueOnce(firstOutgoingRequestsResolvers.promise);
 
             // the second will return a KeysQueryRequest.
             const testReq = new KeysQueryRequest("1234", "{}");
@@ -946,7 +945,7 @@ describe("RustCrypto", () => {
 
             // the first call now completes, *with an empty result*, which would normally cause us to exit the loop, but
             // we should have a second call queued. It should trigger a call to `makeOutgoingRequest`.
-            firstOutgoingRequestsDefer.resolve([]);
+            firstOutgoingRequestsResolvers.resolve([]);
             await awaitCallToMakeOutgoingRequest();
             expect(olmMachine.outgoingRequests).toHaveBeenCalledTimes(2);
         });
diff --git a/spec/unit/scheduler.spec.ts b/spec/unit/scheduler.spec.ts
index 3972a47a9..0cb16df1e 100644
--- a/spec/unit/scheduler.spec.ts
+++ b/spec/unit/scheduler.spec.ts
@@ -1,7 +1,6 @@
 // This file had a function whose name is all caps, which displeases eslint
 /* eslint new-cap: "off" */
 
-import { defer, type IDeferred } from "../../src/utils";
 import { MatrixError } from "../../src/http-api";
 import { MatrixScheduler } from "../../src/scheduler";
 import * as utils from "../test-utils/test-utils";
@@ -14,7 +13,7 @@ describe("MatrixScheduler", function () {
     let scheduler: MatrixScheduler<Record<string, boolean>>;
     let retryFn: ((event: MatrixEvent | null, attempt: number, err: MatrixError) => number) | null;
     let queueFn: ((event: MatrixEvent) => string | null) | null;
-    let deferred: IDeferred<Record<string, boolean>>;
+    let deferred: PromiseWithResolvers<Record<string, boolean>>;
     const roomId = "!foo:bar";
     const eventA = utils.mkMessage({
         user: "@alice:bar",
@@ -44,7 +43,7 @@ describe("MatrixScheduler", function () {
         );
         retryFn = null;
         queueFn = null;
-        deferred = defer();
+        deferred = Promise.withResolvers();
     });
 
     it("should process events in a queue in a FIFO manner", async function () {
@@ -54,8 +53,8 @@ describe("MatrixScheduler", function () {
         queueFn = function () {
             return "one_big_queue";
         };
-        const deferA = defer<Record<string, boolean>>();
-        const deferB = defer<Record<string, boolean>>();
+        const deferA = Promise.withResolvers<Record<string, boolean>>();
+        const deferB = Promise.withResolvers<Record<string, boolean>>();
         let yieldedA = false;
         scheduler.setProcessFunction(function (event) {
             if (yieldedA) {
@@ -79,9 +78,9 @@ describe("MatrixScheduler", function () {
 
     it("should invoke the retryFn on failure and wait the amount of time specified", async function () {
         const waitTimeMs = 1500;
-        const retryDefer = defer();
+        const retryResolvers = Promise.withResolvers<void>();
         retryFn = function () {
-            retryDefer.resolve();
+            retryResolvers.resolve();
             return waitTimeMs;
         };
         queueFn = function () {
@@ -109,7 +108,7 @@ describe("MatrixScheduler", function () {
         await Promise.resolve();
         expect(procCount).toEqual(1);
         deferred.reject({});
-        await retryDefer.promise;
+        await retryResolvers.promise;
         expect(procCount).toEqual(1);
         jest.advanceTimersByTime(waitTimeMs);
         await Promise.resolve();
@@ -127,8 +126,8 @@ describe("MatrixScheduler", function () {
             return "yep";
         };
 
-        const deferA = defer<Record<string, boolean>>();
-        const deferB = defer<Record<string, boolean>>();
+        const deferA = Promise.withResolvers<Record<string, boolean>>();
+        const deferB = Promise.withResolvers<Record<string, boolean>>();
         let procCount = 0;
         scheduler.setProcessFunction(function (ev) {
             procCount += 1;
@@ -180,7 +179,7 @@ describe("MatrixScheduler", function () {
         };
 
         const expectOrder = [eventA.getId(), eventB.getId(), eventD.getId()];
-        const deferA = defer<Record<string, boolean>>();
+        const deferA = Promise.withResolvers<Record<string, boolean>>();
         const allExpectedEventsSeenInOrderPromise = new Promise((resolve) => {
             scheduler.setProcessFunction(function (event) {
                 const id = expectOrder.shift();
diff --git a/spec/unit/secret-storage.spec.ts b/spec/unit/secret-storage.spec.ts
index 13094bf10..c8780cb09 100644
--- a/spec/unit/secret-storage.spec.ts
+++ b/spec/unit/secret-storage.spec.ts
@@ -30,7 +30,6 @@ import {
 import { secureRandomString } from "../../src/randomstring";
 import { type SecretInfo } from "../../src/secret-storage.ts";
 import { type AccountDataEvents, ClientEvent, MatrixEvent, TypedEventEmitter } from "../../src";
-import { defer, type IDeferred } from "../../src/utils";
 
 declare module "../../src/@types/event" {
     interface SecretStorageAccountDataEvents {
@@ -289,10 +288,10 @@ describe("ServerSideSecretStorageImpl", function () {
     describe("setDefaultKeyId", function () {
         let secretStorage: ServerSideSecretStorage;
         let accountDataAdapter: Mocked<AccountDataClient>;
-        let accountDataPromise: IDeferred<void>;
+        let accountDataPromise: PromiseWithResolvers<void>;
         beforeEach(() => {
             accountDataAdapter = mockAccountDataClient();
-            accountDataPromise = defer();
+            accountDataPromise = Promise.withResolvers();
             accountDataAdapter.setAccountData.mockImplementation(() => {
                 accountDataPromise.resolve();
                 return Promise.resolve({});
diff --git a/spec/unit/stores/indexeddb-store-worker.spec.ts b/spec/unit/stores/indexeddb-store-worker.spec.ts
index af0b345fc..7a683f874 100644
--- a/spec/unit/stores/indexeddb-store-worker.spec.ts
+++ b/spec/unit/stores/indexeddb-store-worker.spec.ts
@@ -18,7 +18,6 @@ import "fake-indexeddb/auto";
 
 import { type LocalIndexedDBStoreBackend } from "../../../src/store/indexeddb-local-backend";
 import { IndexedDBStoreWorker } from "../../../src/store/indexeddb-store-worker";
-import { defer } from "../../../src/utils";
 
 function setupWorker(worker: IndexedDBStoreWorker): void {
     worker.onMessage({ data: { command: "setupWorker", args: [] } } as any);
@@ -27,16 +26,16 @@ function setupWorker(worker: IndexedDBStoreWorker): void {
 
 describe("IndexedDBStore Worker", () => {
     it("should pass 'closed' event via postMessage", async () => {
-        const deferred = defer<void>();
+        const postMessageSuccessResolvers = Promise.withResolvers<void>();
         const postMessage = jest.fn().mockImplementation(({ seq, command }) => {
             if (seq === 1 && command === "cmd_success") {
-                deferred.resolve();
+                postMessageSuccessResolvers.resolve();
             }
         });
         const worker = new IndexedDBStoreWorker(postMessage);
         setupWorker(worker);
 
-        await deferred.promise;
+        await postMessageSuccessResolvers.promise;
 
         // @ts-ignore - private field access
         (worker.backend as LocalIndexedDBStoreBackend).db!.onclose!({} as Event);
diff --git a/spec/unit/stores/indexeddb.spec.ts b/spec/unit/stores/indexeddb.spec.ts
index 7d0108fb7..2672334e4 100644
--- a/spec/unit/stores/indexeddb.spec.ts
+++ b/spec/unit/stores/indexeddb.spec.ts
@@ -21,7 +21,6 @@ import { IDBFactory } from "fake-indexeddb";
 import { IndexedDBStore, type IStateEventWithRoomId, MemoryStore, User, UserEvent } from "../../../src";
 import { emitPromise } from "../../test-utils/test-utils";
 import { type LocalIndexedDBStoreBackend } from "../../../src/store/indexeddb-local-backend";
-import { defer } from "../../../src/utils";
 
 describe("IndexedDBStore", () => {
     afterEach(() => {
@@ -80,7 +79,7 @@ describe("IndexedDBStore", () => {
     it("Should load presence events on startup", async () => {
         // 1. Create idb database
         const indexedDB = new IDBFactory();
-        const setupDefer = defer<Event>();
+        const setupResolvers = Promise.withResolvers<Event>();
         const req = indexedDB.open("matrix-js-sdk:db3", 1);
         let db: IDBDatabase;
         req.onupgradeneeded = () => {
@@ -89,11 +88,11 @@ describe("IndexedDBStore", () => {
             db.createObjectStore("accountData", { keyPath: ["type"] });
             db.createObjectStore("sync", { keyPath: ["clobber"] });
         };
-        req.onsuccess = setupDefer.resolve;
-        await setupDefer.promise;
+        req.onsuccess = setupResolvers.resolve;
+        await setupResolvers.promise;
 
         // 2. Fill in user presence data
-        const writeDefer = defer<Event>();
+        const writeResolvers = Promise.withResolvers<Event>();
         const transaction = db!.transaction(["users"], "readwrite");
         const objectStore = transaction.objectStore("users");
         const request = objectStore.put({
@@ -106,8 +105,8 @@ describe("IndexedDBStore", () => {
                 type: "m.presence",
             },
         });
-        request.onsuccess = writeDefer.resolve;
-        await writeDefer.promise;
+        request.onsuccess = writeResolvers.resolve;
+        await writeResolvers.promise;
 
         // 3. Close database
         req.result.close();
@@ -201,7 +200,7 @@ describe("IndexedDBStore", () => {
     });
 
     it("should resolve isNewlyCreated to false if database existed already but needs upgrade", async () => {
-        const deferred = defer<Event>();
+        const requestSuccessResolvers = Promise.withResolvers<Event>();
         // seed db3 to Version 1 so it forces a migration
         const req = indexedDB.open("matrix-js-sdk:db3", 1);
         req.onupgradeneeded = () => {
@@ -210,8 +209,8 @@ describe("IndexedDBStore", () => {
             db.createObjectStore("accountData", { keyPath: ["type"] });
             db.createObjectStore("sync", { keyPath: ["clobber"] });
         };
-        req.onsuccess = deferred.resolve;
-        await deferred.promise;
+        req.onsuccess = requestSuccessResolvers.resolve;
+        await requestSuccessResolvers.promise;
         req.result.close();
 
         const store = new IndexedDBStore({
@@ -232,20 +231,20 @@ describe("IndexedDBStore", () => {
         });
         await store.startup();
 
-        const deferred = defer<void>();
-        store.on("closed", deferred.resolve);
+        const storeClosedResolvers = Promise.withResolvers<void>();
+        store.on("closed", storeClosedResolvers.resolve);
 
         // @ts-ignore - private field access
         (store.backend as LocalIndexedDBStoreBackend).db!.onclose!({} as Event);
-        await deferred.promise;
+        await storeClosedResolvers.promise;
     });
 
     it("should use remote backend if workerFactory passed", async () => {
-        const deferred = defer<void>();
+        const workerPostMessageResolvers = Promise.withResolvers<void>();
         class MockWorker {
             postMessage(data: any) {
                 if (data.command === "setupWorker") {
-                    deferred.resolve();
+                    workerPostMessageResolvers.resolve();
                 }
             }
         }
@@ -257,7 +256,7 @@ describe("IndexedDBStore", () => {
             workerFactory: () => new MockWorker() as Worker,
         });
         store.startup();
-        await deferred.promise;
+        await workerPostMessageResolvers.promise;
     });
 
     it("remote worker should pass closed event", async () => {
@@ -273,10 +272,10 @@ describe("IndexedDBStore", () => {
         });
         store.startup();
 
-        const deferred = defer<void>();
-        store.on("closed", deferred.resolve);
+        const storeClosedResolvers = Promise.withResolvers<void>();
+        store.on("closed", storeClosedResolvers.resolve);
         (worker as any).onmessage({ data: { command: "closed" } });
-        await deferred.promise;
+        await storeClosedResolvers.promise;
     });
 
     it("remote worker should pass command failures", async () => {
diff --git a/src/client.ts b/src/client.ts
index 6974f35fa..74e86ecab 100644
--- a/src/client.ts
+++ b/src/client.ts
@@ -52,7 +52,7 @@ import {
     type GroupCallEventHandlerEventHandlerMap,
 } from "./webrtc/groupCallEventHandler.ts";
 import * as utils from "./utils.ts";
-import { deepCompare, defer, noUnsafeEventProps, type QueryDict, replaceParam, safeSet, sleep } from "./utils.ts";
+import { deepCompare, noUnsafeEventProps, type QueryDict, replaceParam, safeSet, sleep } from "./utils.ts";
 import { Direction, EventTimeline } from "./models/event-timeline.ts";
 import { type IActionsObject, PushProcessor } from "./pushprocessor.ts";
 import { AutoDiscovery, type AutoDiscoveryAction } from "./autodiscovery.ts";
@@ -2191,7 +2191,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
         if (existingData && deepCompare(existingData.event.content, content)) return {};
 
         // Create a promise which will resolve when the update is received
-        const updatedDefer = defer<void>();
+        const updatedResolvers = Promise.withResolvers<void>();
         function accountDataListener(event: MatrixEvent): void {
             // Note that we cannot safely check that the content matches what we expected, because there is a race:
             //   * We set the new content
@@ -2203,13 +2203,13 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
             //
             // Anyway, what we *shouldn't* do is get stuck in a loop. I think the best we can do is check that the event
             // type matches.
-            if (event.getType() === eventType) updatedDefer.resolve();
+            if (event.getType() === eventType) updatedResolvers.resolve();
         }
         this.addListener(ClientEvent.AccountData, accountDataListener);
 
         try {
             const result = await retryNetworkOperation(5, () => this.setAccountDataRaw(eventType, content));
-            await updatedDefer.promise;
+            await updatedResolvers.promise;
             return result;
         } finally {
             this.removeListener(ClientEvent.AccountData, accountDataListener);
@@ -5173,24 +5173,24 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
             } else if (!hasDontNotifyRule) {
                 // Remove the existing one before setting the mute push rule
                 // This is a workaround to SYN-590 (Push rule update fails)
-                const deferred = utils.defer();
+                const doneResolvers = Promise.withResolvers<void>();
                 this.deletePushRule(scope, PushRuleKind.RoomSpecific, roomPushRule.rule_id)
                     .then(() => {
                         this.addPushRule(scope, PushRuleKind.RoomSpecific, roomId, {
                             actions: [PushRuleActionName.DontNotify],
                         })
                             .then(() => {
-                                deferred.resolve();
+                                doneResolvers.resolve();
                             })
                             .catch((err) => {
-                                deferred.reject(err);
+                                doneResolvers.reject(err);
                             });
                     })
                     .catch((err) => {
-                        deferred.reject(err);
+                        doneResolvers.reject(err);
                     });
 
-                promise = deferred.promise;
+                promise = doneResolvers.promise;
             }
         }
 
diff --git a/src/http-api/index.ts b/src/http-api/index.ts
index cd5db0a86..1474cdde9 100644
--- a/src/http-api/index.ts
+++ b/src/http-api/index.ts
@@ -24,7 +24,7 @@ import {
     type UploadResponse,
 } from "./interface.ts";
 import { MediaPrefix } from "./prefix.ts";
-import { defer, type QueryDict, removeElement } from "../utils.ts";
+import { type QueryDict, removeElement } from "../utils.ts";
 import * as callbacks from "../realtime-callbacks.ts";
 import { Method } from "./method.ts";
 import { ConnectionError } from "./errors.ts";
@@ -65,14 +65,14 @@ export class MatrixHttpApi<O extends IHttpOpts> extends FetchHttpApi<O> {
             total: 0,
             abortController,
         } as Upload;
-        const deferred = defer<UploadResponse>();
+        const uploadResolvers = Promise.withResolvers<UploadResponse>();
 
         if (globalThis.XMLHttpRequest) {
             const xhr = new globalThis.XMLHttpRequest();
 
             const timeoutFn = function (): void {
                 xhr.abort();
-                deferred.reject(new Error("Timeout"));
+                uploadResolvers.reject(new Error("Timeout"));
             };
 
             // set an initial timeout of 30s; we'll advance it each time we get a progress notification
@@ -91,16 +91,16 @@ export class MatrixHttpApi<O extends IHttpOpts> extends FetchHttpApi<O> {
                             }
 
                             if (xhr.status >= 400) {
-                                deferred.reject(parseErrorResponse(xhr, xhr.responseText));
+                                uploadResolvers.reject(parseErrorResponse(xhr, xhr.responseText));
                             } else {
-                                deferred.resolve(JSON.parse(xhr.responseText));
+                                uploadResolvers.resolve(JSON.parse(xhr.responseText));
                             }
                         } catch (err) {
                             if ((<Error>err).name === "AbortError") {
-                                deferred.reject(err);
+                                uploadResolvers.reject(err);
                                 return;
                             }
-                            deferred.reject(new ConnectionError("request failed", <Error>err));
+                            uploadResolvers.reject(new ConnectionError("request failed", <Error>err));
                         }
                         break;
                 }
@@ -153,16 +153,16 @@ export class MatrixHttpApi<O extends IHttpOpts> extends FetchHttpApi<O> {
                 .then((response) => {
                     return this.opts.onlyData ? <UploadResponse>response : response.json();
                 })
-                .then(deferred.resolve, deferred.reject);
+                .then(uploadResolvers.resolve, uploadResolvers.reject);
         }
 
         // remove the upload from the list on completion
-        upload.promise = deferred.promise.finally(() => {
+        upload.promise = uploadResolvers.promise.finally(() => {
             removeElement(this.uploads, (elem) => elem === upload);
         });
         abortController.signal.addEventListener("abort", () => {
             removeElement(this.uploads, (elem) => elem === upload);
-            deferred.reject(new DOMException("Aborted", "AbortError"));
+            uploadResolvers.reject(new DOMException("Aborted", "AbortError"));
         });
         this.uploads.push(upload);
         return upload.promise;
diff --git a/src/interactive-auth.ts b/src/interactive-auth.ts
index 0e02d2903..74aafb6b3 100644
--- a/src/interactive-auth.ts
+++ b/src/interactive-auth.ts
@@ -18,7 +18,6 @@ limitations under the License.
 
 import { logger } from "./logger.ts";
 import { type MatrixClient } from "./client.ts";
-import { defer, type IDeferred } from "./utils.ts";
 import { MatrixError } from "./http-api/index.ts";
 import { type UserIdentifier } from "./@types/auth.ts";
 
@@ -262,7 +261,7 @@ export class InteractiveAuth<T> {
     private data: IAuthData & MatrixError["data"];
     private emailSid?: string;
     private requestingEmailToken = false;
-    private attemptAuthDeferred: IDeferred<T> | null = null;
+    private attemptAuthDeferred: PromiseWithResolvers<T> | null = null;
     private chosenFlow: UIAFlow | null = null;
     private currentStage: string | null = null;
 
@@ -298,7 +297,7 @@ export class InteractiveAuth<T> {
     public async attemptAuth(): Promise<T> {
         // This promise will be quite long-lived and will resolve when the
         // request is authenticated and completes successfully.
-        this.attemptAuthDeferred = defer();
+        this.attemptAuthDeferred = Promise.withResolvers();
         // pluck the promise out now, as doRequest may clear before we return
         const promise = this.attemptAuthDeferred.promise;
 
diff --git a/src/matrixrtc/NewMembershipManager.ts b/src/matrixrtc/NewMembershipManager.ts
index fad78e216..ddc755d53 100644
--- a/src/matrixrtc/NewMembershipManager.ts
+++ b/src/matrixrtc/NewMembershipManager.ts
@@ -169,7 +169,7 @@ export class MembershipManager
         }
         this.fociPreferred = fociPreferred;
         this.focusActive = focusActive;
-        this.leavePromiseDefer = undefined;
+        this.leavePromiseResolvers = undefined;
         this.activated = true;
         this.oldStatus = this.status;
         this.state = MembershipManager.defaultState;
@@ -188,8 +188,8 @@ export class MembershipManager
                     this.emit(MembershipManagerEvent.StatusChanged, this.oldStatus, this.status);
                 }
                 if (!this.scheduler.running) {
-                    this.leavePromiseDefer?.resolve(true);
-                    this.leavePromiseDefer = undefined;
+                    this.leavePromiseResolvers?.resolve(true);
+                    this.leavePromiseResolvers = undefined;
                 }
             });
     }
@@ -207,16 +207,16 @@ export class MembershipManager
 
         // We use the promise to track if we already scheduled a leave event
         // So we do not check scheduler.actions/scheduler.insertions
-        if (!this.leavePromiseDefer) {
+        if (!this.leavePromiseResolvers) {
             // reset scheduled actions so we will not do any new actions.
-            this.leavePromiseDefer = defer<boolean>();
+            this.leavePromiseResolvers = defer<boolean>();
             this.activated = false;
             this.scheduler.initiateLeave();
-            if (timeout) setTimeout(() => this.leavePromiseDefer?.resolve(false), timeout);
+            if (timeout) setTimeout(() => this.leavePromiseResolvers?.resolve(false), timeout);
         }
-        return this.leavePromiseDefer.promise;
+        return this.leavePromiseResolvers.promise;
     }
-    private leavePromiseDefer?: IDeferred<boolean>;
+    private leavePromiseResolvers?: IDeferred<boolean>;
 
     public async onRTCSessionMemberUpdate(memberships: CallMembership[]): Promise<void> {
         const userId = this.client.getUserId();
diff --git a/src/rust-crypto/OutgoingRequestsManager.ts b/src/rust-crypto/OutgoingRequestsManager.ts
index 0e64433b8..a2fdf37a3 100644
--- a/src/rust-crypto/OutgoingRequestsManager.ts
+++ b/src/rust-crypto/OutgoingRequestsManager.ts
@@ -18,7 +18,7 @@ import { type OlmMachine, type OutgoingRequest } from "@matrix-org/matrix-sdk-cr
 
 import { type OutgoingRequestProcessor } from "./OutgoingRequestProcessor.ts";
 import { type Logger } from "../logger.ts";
-import { defer, type IDeferred, logDuration } from "../utils.ts";
+import { logDuration } from "../utils.ts";
 
 /**
  * OutgoingRequestsManager: responsible for processing outgoing requests from the OlmMachine.
@@ -39,7 +39,7 @@ export class OutgoingRequestsManager {
      * will resolve once that next iteration completes. If it is undefined, there have been no new calls
      * to `doProcessOutgoingRequests` since the current iteration started.
      */
-    private nextLoopDeferred?: IDeferred<void>;
+    private nextLoopDeferred?: PromiseWithResolvers<void>;
 
     public constructor(
         private readonly logger: Logger,
@@ -74,7 +74,7 @@ export class OutgoingRequestsManager {
         // In order to circumvent the race, we set a flag which tells the loop to go round once again even if the
         // queue appears to be empty.
         if (!this.nextLoopDeferred) {
-            this.nextLoopDeferred = defer();
+            this.nextLoopDeferred = Promise.withResolvers();
         }
 
         // ... and wait for it to complete.
@@ -99,14 +99,14 @@ export class OutgoingRequestsManager {
         this.outgoingRequestLoopRunning = true;
         try {
             while (!this.stopped && this.nextLoopDeferred) {
-                const deferred = this.nextLoopDeferred;
+                const loopTickResolvers = this.nextLoopDeferred;
 
                 // reset `nextLoopDeferred` so that any future calls to `doProcessOutgoingRequests` are queued
                 // for another additional iteration.
                 this.nextLoopDeferred = undefined;
 
                 // make the requests and feed the results back to the `nextLoopDeferred`
-                await this.processOutgoingRequests().then(deferred.resolve, deferred.reject);
+                await this.processOutgoingRequests().then(loopTickResolvers.resolve, loopTickResolvers.reject);
             }
         } finally {
             this.outgoingRequestLoopRunning = false;
diff --git a/src/rust-crypto/verification.ts b/src/rust-crypto/verification.ts
index f2684d10d..fbe7cc750 100644
--- a/src/rust-crypto/verification.ts
+++ b/src/rust-crypto/verification.ts
@@ -34,7 +34,6 @@ import { type OutgoingRequestProcessor } from "./OutgoingRequestProcessor.ts";
 import { TypedReEmitter } from "../ReEmitter.ts";
 import { type MatrixEvent } from "../models/event.ts";
 import { EventType, MsgType } from "../@types/event.ts";
-import { defer, type IDeferred } from "../utils.ts";
 import { VerificationMethod } from "../types.ts";
 import type { Logger } from "../logger.ts";
 
@@ -478,7 +477,7 @@ abstract class BaseRustVerifer<InnerType extends RustSdkCryptoJs.Qr | RustSdkCry
     VerifierEventHandlerMap & VerificationRequestEventHandlerMap
 > {
     /** A deferred which completes when the verification completes (or rejects when it is cancelled/fails) */
-    protected readonly completionDeferred: IDeferred<void>;
+    protected readonly completionDeferred: PromiseWithResolvers<void>;
 
     public constructor(
         protected inner: InnerType,
@@ -486,7 +485,7 @@ abstract class BaseRustVerifer<InnerType extends RustSdkCryptoJs.Qr | RustSdkCry
     ) {
         super();
 
-        this.completionDeferred = defer();
+        this.completionDeferred = Promise.withResolvers();
 
         // As with RustVerificationRequest, we need to avoid a reference cycle.
         // See the comments in RustVerificationRequest.
diff --git a/src/scheduler.ts b/src/scheduler.ts
index bd5c38a0d..ac4b3dee5 100644
--- a/src/scheduler.ts
+++ b/src/scheduler.ts
@@ -21,7 +21,7 @@ limitations under the License.
 import { logger } from "./logger.ts";
 import { type MatrixEvent } from "./models/event.ts";
 import { EventType } from "./@types/event.ts";
-import { defer, type IDeferred, removeElement } from "./utils.ts";
+import { removeElement } from "./utils.ts";
 import { calculateRetryBackoff, type MatrixError } from "./http-api/index.ts";
 import { type ISendEventResponse } from "./@types/requests.ts";
 
@@ -29,7 +29,7 @@ const DEBUG = false; // set true to enable console logging.
 
 interface IQueueEntry<T> {
     event: MatrixEvent;
-    defer: IDeferred<T>;
+    resolvers: PromiseWithResolvers<T>;
     attempts: number;
 }
 
@@ -70,7 +70,7 @@ export class MatrixScheduler<T = ISendEventResponse> {
 
     // queueName: [{
     //  event: MatrixEvent,  // event to send
-    //  defer: Deferred,  // defer to resolve/reject at the END of the retries
+    //  defer: PromiseWithResolvers,  // defer to resolve/reject at the END of the retries
     //  attempts: Number  // number of times we've called processFn
     // }, ...]
     private readonly queues: Record<string, IQueueEntry<T>[]> = {};
@@ -188,15 +188,15 @@ export class MatrixScheduler<T = ISendEventResponse> {
         if (!this.queues[queueName]) {
             this.queues[queueName] = [];
         }
-        const deferred = defer<T>();
+        const eventResolvers = Promise.withResolvers<T>();
         this.queues[queueName].push({
             event: event,
-            defer: deferred,
+            resolvers: eventResolvers,
             attempts: 0,
         });
         debuglog("Queue algorithm dumped event %s into queue '%s'", event.getId(), queueName);
         this.startProcessingQueues();
-        return deferred.promise;
+        return eventResolvers.promise;
     }
 
     private startProcessingQueues(): void {
@@ -239,7 +239,7 @@ export class MatrixScheduler<T = ISendEventResponse> {
                     // remove this from the queue
                     this.removeNextEvent(queueName);
                     debuglog("Queue '%s' sent event %s", queueName, obj.event.getId());
-                    obj.defer.resolve(res);
+                    obj.resolvers.resolve(res);
                     // keep processing
                     this.processQueue(queueName);
                 },
@@ -279,7 +279,7 @@ export class MatrixScheduler<T = ISendEventResponse> {
         logger.info("clearing queue '%s'", queueName);
         let obj: IQueueEntry<T> | undefined;
         while ((obj = this.removeNextEvent(queueName))) {
-            obj.defer.reject(err);
+            obj.resolvers.reject(err);
         }
         this.disableQueue(queueName);
     }
diff --git a/src/store/indexeddb-remote-backend.ts b/src/store/indexeddb-remote-backend.ts
index 26ec0613d..293953ff7 100644
--- a/src/store/indexeddb-remote-backend.ts
+++ b/src/store/indexeddb-remote-backend.ts
@@ -15,7 +15,6 @@ limitations under the License.
 */
 
 import { logger } from "../logger.ts";
-import { defer, type IDeferred } from "../utils.ts";
 import { type ISavedSync } from "./index.ts";
 import { type IStoredClientOpts } from "../client.ts";
 import { type IStateEventWithRoomId, type ISyncResponse } from "../matrix.ts";
@@ -26,7 +25,7 @@ export class RemoteIndexedDBStoreBackend implements IIndexedDBBackend {
     private worker?: Worker;
     private nextSeq = 0;
     // The currently in-flight requests to the actual backend
-    private inFlight: Record<number, IDeferred<any>> = {}; // seq: promise
+    private inFlight: Record<number, PromiseWithResolvers<any>> = {}; // seq: promise
     // Once we start connecting, we keep the promise and re-use it
     // if we try to connect again
     private startPromise?: Promise<void>;
@@ -164,7 +163,7 @@ export class RemoteIndexedDBStoreBackend implements IIndexedDBBackend {
         // the promise automatically gets rejected
         return Promise.resolve().then(() => {
             const seq = this.nextSeq++;
-            const def = defer<T>();
+            const def = Promise.withResolvers<T>();
 
             this.inFlight[seq] = def;
 
diff --git a/src/sync.ts b/src/sync.ts
index 1419d1ccf..4c9a78fa6 100644
--- a/src/sync.ts
+++ b/src/sync.ts
@@ -28,7 +28,7 @@ import { type Optional } from "matrix-events-sdk";
 import type { SyncCryptoCallbacks } from "./common-crypto/CryptoBackend.ts";
 import { User } from "./models/user.ts";
 import { NotificationCountType, Room, RoomEvent } from "./models/room.ts";
-import { deepCopy, defer, type IDeferred, noUnsafeEventProps, promiseMapSeries, unsafeProp } from "./utils.ts";
+import { deepCopy, noUnsafeEventProps, promiseMapSeries, unsafeProp } from "./utils.ts";
 import { Filter } from "./filter.ts";
 import { EventTimeline } from "./models/event-timeline.ts";
 import { logger } from "./logger.ts";
@@ -220,7 +220,7 @@ export class SyncApi {
     private catchingUp = false;
     private running = false;
     private keepAliveTimer?: ReturnType<typeof setTimeout>;
-    private connectionReturnedDefer?: IDeferred<boolean>;
+    private connectionReturnedResolvers?: PromiseWithResolvers<boolean>;
     private notifEvents: MatrixEvent[] = []; // accumulator of sync events in the current sync response
     private failedSyncCount = 0; // Number of consecutive failed /sync requests
     private storeIsInvalid = false; // flag set if the store needs to be cleared before we can start
@@ -792,7 +792,7 @@ export class SyncApi {
      * @returns True if this resulted in a request being retried.
      */
     public retryImmediately(): boolean {
-        if (!this.connectionReturnedDefer) {
+        if (!this.connectionReturnedResolvers) {
             return false;
         }
         this.startKeepAlives(0);
@@ -916,9 +916,9 @@ export class SyncApi {
 
         if (!this.running) {
             debuglog("Sync no longer running: exiting.");
-            if (this.connectionReturnedDefer) {
-                this.connectionReturnedDefer.reject();
-                this.connectionReturnedDefer = undefined;
+            if (this.connectionReturnedResolvers) {
+                this.connectionReturnedResolvers.reject();
+                this.connectionReturnedResolvers = undefined;
             }
             this.updateSyncState(SyncState.Stopped);
         }
@@ -999,9 +999,9 @@ export class SyncApi {
     private async onSyncError(err: MatrixError): Promise<boolean> {
         if (!this.running) {
             debuglog("Sync no longer running: exiting");
-            if (this.connectionReturnedDefer) {
-                this.connectionReturnedDefer.reject();
-                this.connectionReturnedDefer = undefined;
+            if (this.connectionReturnedResolvers) {
+                this.connectionReturnedResolvers.reject();
+                this.connectionReturnedResolvers = undefined;
             }
             this.updateSyncState(SyncState.Stopped);
             return true; // abort
@@ -1551,10 +1551,10 @@ export class SyncApi {
         } else {
             this.pokeKeepAlive();
         }
-        if (!this.connectionReturnedDefer) {
-            this.connectionReturnedDefer = defer();
+        if (!this.connectionReturnedResolvers) {
+            this.connectionReturnedResolvers = Promise.withResolvers();
         }
-        return this.connectionReturnedDefer.promise;
+        return this.connectionReturnedResolvers.promise;
     }
 
     /**
@@ -1562,7 +1562,7 @@ export class SyncApi {
      * reachable.
      *
      * On failure, schedules a call back to itself. On success, resolves
-     * this.connectionReturnedDefer.
+     * this.connectionReturnedResolvers.
      *
      * @param connDidFail - True if a connectivity failure has been detected. Optional.
      */
@@ -1571,18 +1571,18 @@ export class SyncApi {
             // we are in a keepAlive, retrying to connect, but the syncronization
             // was stopped, so we are stopping the retry.
             clearTimeout(this.keepAliveTimer);
-            if (this.connectionReturnedDefer) {
-                this.connectionReturnedDefer.reject("SyncApi.stop() was called");
-                this.connectionReturnedDefer = undefined;
+            if (this.connectionReturnedResolvers) {
+                this.connectionReturnedResolvers.reject("SyncApi.stop() was called");
+                this.connectionReturnedResolvers = undefined;
             }
             return;
         }
 
         const success = (): void => {
             clearTimeout(this.keepAliveTimer);
-            if (this.connectionReturnedDefer) {
-                this.connectionReturnedDefer.resolve(connDidFail);
-                this.connectionReturnedDefer = undefined;
+            if (this.connectionReturnedResolvers) {
+                this.connectionReturnedResolvers.resolve(connDidFail);
+                this.connectionReturnedResolvers = undefined;
             }
         };
 
diff --git a/src/utils.ts b/src/utils.ts
index 9a822f92f..b5e178bc6 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -436,23 +436,14 @@ export function isNullOrUndefined(val: any): boolean {
     return val === null || val === undefined;
 }
 
-export interface IDeferred<T> {
-    resolve: (value: T | Promise<T>) => void;
-    reject: (reason?: any) => void;
-    promise: Promise<T>;
-}
+export type IDeferred<T> = PromiseWithResolvers<T>;
 
-// Returns a Deferred
+/**
+ * Creates a deferred promise. This is a promise that can be resolved or rejected.
+ * @deprecated use {@link Promise.withResolvers} instead.
+ */
 export function defer<T = void>(): IDeferred<T> {
-    let resolve!: IDeferred<T>["resolve"];
-    let reject!: IDeferred<T>["reject"];
-
-    const promise = new Promise<T>((_resolve, _reject) => {
-        resolve = _resolve;
-        reject = _reject;
-    });
-
-    return { resolve, reject, promise };
+    return Promise.withResolvers<T>();
 }
 
 export async function promiseMapSeries<T>(
-- 
GitLab