From 1d65dca042796b155350a87890f9602de178cca7 Mon Sep 17 00:00:00 2001 From: Eric Luce <37158449+eluce2@users.noreply.github.com> Date: Tue, 16 Jun 2026 11:34:05 -0500 Subject: [PATCH 1/2] Retry FileMaker webviewer fetches when unavailable --- .changeset/fuzzy-trees-fetch.md | 5 +++ packages/webviewer/src/main.ts | 49 ++++++++++++++++++++++--- packages/webviewer/tests/main.test.ts | 52 +++++++++++++++++++++++++++ 3 files changed, 101 insertions(+), 5 deletions(-) create mode 100644 .changeset/fuzzy-trees-fetch.md create mode 100644 packages/webviewer/tests/main.test.ts diff --git a/.changeset/fuzzy-trees-fetch.md b/.changeset/fuzzy-trees-fetch.md new file mode 100644 index 00000000..3594ea03 --- /dev/null +++ b/.changeset/fuzzy-trees-fetch.md @@ -0,0 +1,5 @@ +--- +"@proofkit/webviewer": patch +--- + +Retry fmFetch when FileMaker bridge is briefly unavailable. diff --git a/packages/webviewer/src/main.ts b/packages/webviewer/src/main.ts index 71ff8812..931d5e38 100644 --- a/packages/webviewer/src/main.ts +++ b/packages/webviewer/src/main.ts @@ -1,6 +1,9 @@ import { v4 } from "uuid"; let webViewerName: string; +const FM_FETCH_FILEMAKER_RETRY_DELAYS_MS = [250, 500, 1000] as const; +const FILEMAKER_UNAVAILABLE_MESSAGE = "'window.FileMaker' was not available"; + /** * @private * set the name of the Web Viewer to use for all fetches @@ -39,12 +42,18 @@ export function fmFetch( ): void; export function fmFetch(scriptName: string, data: string | object, callback?: () => void) { if (callback) { - return _execScript(scriptName, data, callback); + const pendingScript = _execScriptWithFileMakerRetry(scriptName, data, callback); + pendingScript.catch((error: unknown) => { + setTimeout(() => { + throw error; + }, 0); + }); + return; } - return new Promise((resolve) => { - _execScript(scriptName, data, (result) => { + return new Promise((resolve, reject) => { + _execScriptWithFileMakerRetry(scriptName, data, (result) => { resolve(result); - }); + }).catch(reject); }); } @@ -83,7 +92,37 @@ function _execScript(scriptName: string, data: unknown, cb: (arg0?: unknown) => data, callback: { fetchId, fn: "handleFmWVFetchCallback", webViewerName }, }; - callFMScript(scriptName, param); + try { + callFMScript(scriptName, param); + } catch (error) { + delete cbs[fetchId]; + throw error; + } +} + +function isFileMakerUnavailableError(error: unknown): boolean { + return error instanceof Error && error.message.includes(FILEMAKER_UNAVAILABLE_MESSAGE); +} + +function wait(ms: number): Promise { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} + +async function _execScriptWithFileMakerRetry(scriptName: string, data: unknown, cb: (arg0?: unknown) => void) { + for (let attempt = 0; ; attempt++) { + try { + _execScript(scriptName, data, cb); + return; + } catch (error) { + const delay = FM_FETCH_FILEMAKER_RETRY_DELAYS_MS[attempt]; + if (!(delay && isFileMakerUnavailableError(error))) { + throw error; + } + await wait(delay); + } + } } /** diff --git a/packages/webviewer/tests/main.test.ts b/packages/webviewer/tests/main.test.ts new file mode 100644 index 00000000..768bf022 --- /dev/null +++ b/packages/webviewer/tests/main.test.ts @@ -0,0 +1,52 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +describe("fmFetch", () => { + const originalWindow = globalThis.window; + + beforeEach(() => { + vi.useFakeTimers(); + vi.resetModules(); + globalThis.window = {} as Window & typeof globalThis; + }); + + afterEach(() => { + vi.useRealTimers(); + if (typeof originalWindow === "undefined") { + Reflect.deleteProperty(globalThis, "window"); + } else { + globalThis.window = originalWindow; + } + }); + + it("retries when FileMaker is not yet available", async () => { + const { fmFetch } = await import("../src/main.ts"); + const performScript = vi.fn(); + + const result = fmFetch("LoadData", { id: "123" }); + globalThis.window.FileMaker = { + PerformScript: performScript, + PerformScriptWithOption: vi.fn(), + }; + + await vi.advanceTimersByTimeAsync(250); + + expect(performScript).toHaveBeenCalledTimes(1); + const params = JSON.parse(String(performScript.mock.calls[0]?.[1])) as { + callback: { fetchId: string }; + }; + globalThis.window.handleFmWVFetchCallback(JSON.stringify({ ok: true }), params.callback.fetchId); + await vi.advanceTimersByTimeAsync(1); + + await expect(result).resolves.toEqual({ ok: true }); + }); + + it("rejects after three FileMaker availability retries", async () => { + const { fmFetch } = await import("../src/main.ts"); + + const result = fmFetch("LoadData", { id: "123" }); + const rejection = expect(result).rejects.toThrow("'window.FileMaker' was not available"); + await vi.advanceTimersByTimeAsync(1750); + + await rejection; + }); +}); From b352a48754e1101c5b2dd834a340cac2204c3d9f Mon Sep 17 00:00:00 2001 From: Eric Luce <37158449+eluce2@users.noreply.github.com> Date: Tue, 16 Jun 2026 11:49:43 -0500 Subject: [PATCH 2/2] Return fmFetch promise in callback mode --- .changeset/fuzzy-trees-fetch.md | 2 +- packages/webviewer/src/main.ts | 10 ++-------- packages/webviewer/tests/main.test.ts | 10 ++++++++++ 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/.changeset/fuzzy-trees-fetch.md b/.changeset/fuzzy-trees-fetch.md index 3594ea03..4cf5c5df 100644 --- a/.changeset/fuzzy-trees-fetch.md +++ b/.changeset/fuzzy-trees-fetch.md @@ -2,4 +2,4 @@ "@proofkit/webviewer": patch --- -Retry fmFetch when FileMaker bridge is briefly unavailable. +Retry fmFetch when FileMaker bridge is briefly unavailable and keep callback-mode dispatch failures catchable. diff --git a/packages/webviewer/src/main.ts b/packages/webviewer/src/main.ts index 931d5e38..87c10c8d 100644 --- a/packages/webviewer/src/main.ts +++ b/packages/webviewer/src/main.ts @@ -39,16 +39,10 @@ export function fmFetch( * @param cb callback function to call when the script is done */ callback: () => void, -): void; +): Promise; export function fmFetch(scriptName: string, data: string | object, callback?: () => void) { if (callback) { - const pendingScript = _execScriptWithFileMakerRetry(scriptName, data, callback); - pendingScript.catch((error: unknown) => { - setTimeout(() => { - throw error; - }, 0); - }); - return; + return _execScriptWithFileMakerRetry(scriptName, data, callback); } return new Promise((resolve, reject) => { _execScriptWithFileMakerRetry(scriptName, data, (result) => { diff --git a/packages/webviewer/tests/main.test.ts b/packages/webviewer/tests/main.test.ts index 768bf022..d7cef887 100644 --- a/packages/webviewer/tests/main.test.ts +++ b/packages/webviewer/tests/main.test.ts @@ -49,4 +49,14 @@ describe("fmFetch", () => { await rejection; }); + + it("returns a catchable rejection in callback mode after retries", async () => { + const { fmFetch } = await import("../src/main.ts"); + + const result = fmFetch("LoadData", { id: "123" }, vi.fn()); + const rejection = expect(result).rejects.toThrow("'window.FileMaker' was not available"); + await vi.advanceTimersByTimeAsync(1750); + + await rejection; + }); });