Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fuzzy-trees-fetch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@proofkit/webviewer": patch
---

Retry fmFetch when FileMaker bridge is briefly unavailable and keep callback-mode dispatch failures catchable.
45 changes: 39 additions & 6 deletions packages/webviewer/src/main.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -36,15 +39,15 @@ export function fmFetch(
* @param cb callback function to call when the script is done
*/
callback: () => void,
): void;
): Promise<void>;
export function fmFetch(scriptName: string, data: string | object, callback?: () => void) {
if (callback) {
return _execScript(scriptName, data, callback);
return _execScriptWithFileMakerRetry(scriptName, data, callback);
}
return new Promise((resolve) => {
_execScript(scriptName, data, (result) => {
return new Promise((resolve, reject) => {
_execScriptWithFileMakerRetry(scriptName, data, (result) => {
resolve(result);
});
}).catch(reject);
});
}

Expand Down Expand Up @@ -83,7 +86,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<void> {
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);
}
}
}

/**
Expand Down
62 changes: 62 additions & 0 deletions packages/webviewer/tests/main.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
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;
});

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;
});
});
Loading