diff --git a/apps/mobile/index.ts b/apps/mobile/index.ts index 1da67e1c5..042b307d8 100644 --- a/apps/mobile/index.ts +++ b/apps/mobile/index.ts @@ -1,4 +1,5 @@ import "./src/platform/installCrypto"; +import "./src/platform/installAbortSignal"; import "./src/background/defineNavigationTask"; import { registerRootComponent } from "expo"; import { App } from "./src/App"; diff --git a/apps/mobile/src/platform/installAbortSignal.test.ts b/apps/mobile/src/platform/installAbortSignal.test.ts new file mode 100644 index 000000000..e6c0f83a1 --- /dev/null +++ b/apps/mobile/src/platform/installAbortSignal.test.ts @@ -0,0 +1,30 @@ +import { installAbortSignal } from "./installAbortSignal"; + +class BareSignal { + aborted = false; + reason?: unknown; +} +type Throwing = BareSignal & { throwIfAborted(): void }; + +describe("installAbortSignal", () => { + it("adds throwIfAborted to a signal class without it", () => { + installAbortSignal(BareSignal); + const signal = new BareSignal() as Throwing; + expect(() => signal.throwIfAborted()).not.toThrow(); + signal.aborted = true; + expect(() => signal.throwIfAborted()).toThrow(expect.objectContaining({ name: "AbortError" })); + const reason = new Error("timeout"); + signal.reason = reason; + expect(() => signal.throwIfAborted()).toThrow(reason); + }); + + it("keeps an existing implementation", () => { + const own = () => undefined; + class Native { + throwIfAborted = own; + } + Object.defineProperty(Native.prototype, "throwIfAborted", { value: own }); + installAbortSignal(Native); + expect(Native.prototype.throwIfAborted).toBe(own); + }); +}); diff --git a/apps/mobile/src/platform/installAbortSignal.ts b/apps/mobile/src/platform/installAbortSignal.ts new file mode 100644 index 000000000..38d0c1c54 --- /dev/null +++ b/apps/mobile/src/platform/installAbortSignal.ts @@ -0,0 +1,28 @@ +/** + * Gives React Native's `AbortSignal` the `throwIfAborted()` method it lacks. + * + * The shared API client calls `signal.throwIfAborted()` after reading a + * response, so without it every directions, reroute and transit refresh + * request from the shell fails with "undefined is not a function". A runtime + * that already has the method keeps it. + */ +export function installAbortSignal( + ctor: { prototype: object } | undefined = globalThis.AbortSignal, +): void { + if (!ctor) return; + const prototype = ctor.prototype as { throwIfAborted?: unknown }; + if (typeof prototype.throwIfAborted === "function") return; + Object.defineProperty(prototype, "throwIfAborted", { + configurable: true, + writable: true, + value(this: { aborted: boolean; reason?: unknown }) { + if (!this.aborted) return; + if (this.reason !== undefined) throw this.reason; + const error = new Error("This operation was aborted"); + error.name = "AbortError"; + throw error; + }, + }); +} + +installAbortSignal();