diff --git a/README.md b/README.md index db63de2..4d01e4e 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,26 @@ if (reforge.isEnabled('cool-feature') { setTimeout(ping, reforge.get('ping-delay')); ``` +## Prefetching + +To avoid a request waterfall, you can start fetching the configuration early in your app's +lifecycle, before the React SDK or `reforge.init()` is called. + +```javascript +import { prefetchReforgeConfig, Context } from "@reforge-com/javascript"; + +prefetchReforgeConfig({ + sdkKey: "1234", + context: new Context({ + user: { + email: "test@example.com", + }, + }), +}); +``` + +When you later call `reforge.init()`, it will automatically use the prefetched promise if available. + ## Client API | property | example | purpose | diff --git a/index.ts b/index.ts index e55db04..9373cec 100644 --- a/index.ts +++ b/index.ts @@ -1,4 +1,10 @@ -import { reforge, Reforge, ReforgeInitParams, ReforgeBootstrap } from "./src/reforge"; +import { + reforge, + Reforge, + ReforgeInitParams, + ReforgeBootstrap, + prefetchReforgeConfig, +} from "./src/reforge"; import { Config } from "./src/config"; import Context from "./src/context"; import { LogLevel, getLogLevelSeverity, shouldLogAtLevel } from "./src/logger"; @@ -15,6 +21,7 @@ export { getLogLevelSeverity, shouldLogAtLevel, version, + prefetchReforgeConfig, }; export { ReforgeBootstrap }; diff --git a/src/loader.ts b/src/loader.ts index d6cb002..9488205 100644 --- a/src/loader.ts +++ b/src/loader.ts @@ -130,6 +130,19 @@ export default class Loader { headers: headers(this.sdkKey, this.clientVersion), }; + const prefetchPromise = (window as any).REFORGE_SDK_PREFETCH_PROMISE; + + if (prefetchPromise && prefetchPromise instanceof Promise) { + (window as any).REFORGE_SDK_PREFETCH_PROMISE = undefined; + return prefetchPromise.catch( + () => + // If the prefetch failed, we should try to load from the endpoints + new Promise((resolve, reject) => { + this.loadFromEndpoint(0, options, resolve, reject); + }) + ); + } + const promise = new Promise((resolve, reject) => { this.loadFromEndpoint(0, options, resolve, reject); }); diff --git a/src/prefetch.test.ts b/src/prefetch.test.ts new file mode 100644 index 0000000..cfd42f4 --- /dev/null +++ b/src/prefetch.test.ts @@ -0,0 +1,73 @@ +import { prefetchReforgeConfig, Reforge } from "./reforge"; +import Context from "./context"; + +describe("prefetchReforgeConfig", () => { + const sdkKey = "test-sdk-key"; + const context = new Context({ user: { id: "123" } }); + + beforeEach(() => { + // Reset window object + (window as any).REFORGE_SDK_PREFETCH_PROMISE = undefined; + jest.clearAllMocks(); + }); + + it("should set REFORGE_SDK_PREFETCH_PROMISE on window", () => { + // Mock fetch to return a promise + global.fetch = jest.fn(() => + Promise.resolve({ + ok: true, + json: () => Promise.resolve({ evaluations: {} }), + } as Response) + ); + + prefetchReforgeConfig({ sdkKey, context }); + + expect((window as any).REFORGE_SDK_PREFETCH_PROMISE).toBeDefined(); + expect((window as any).REFORGE_SDK_PREFETCH_PROMISE).toBeInstanceOf(Promise); + }); + + it("should pass correct parameters to loader", async () => { + global.fetch = jest.fn(() => + Promise.resolve({ + ok: true, + json: () => Promise.resolve({ evaluations: {} }), + } as Response) + ); + + prefetchReforgeConfig({ sdkKey, context }); + + await (window as any).REFORGE_SDK_PREFETCH_PROMISE; + + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining(context.encode()), + expect.anything() + ); + }); + + it("should not make a second API call when initializing Reforge with prefetch", async () => { + // 1. Mock fetch + global.fetch = jest.fn(() => + Promise.resolve({ + ok: true, + json: () => Promise.resolve({ evaluations: {} }), + } as Response) + ); + + // 2. Start prefetch + prefetchReforgeConfig({ sdkKey, context }); + + // Verify prefetch started + expect(global.fetch).toHaveBeenCalledTimes(1); + + // 3. Initialize Reforge + const reforgeInstance = new Reforge(); + + await reforgeInstance.init({ sdkKey, context }); + + // 4. Verify fetch was NOT called again + expect(global.fetch).toHaveBeenCalledTimes(1); + + // Verify window global was cleared (as per loader logic) + expect((window as any).REFORGE_SDK_PREFETCH_PROMISE).toBeUndefined(); + }); +}); diff --git a/src/reforge.ts b/src/reforge.ts index 0646d88..443a683 100644 --- a/src/reforge.ts +++ b/src/reforge.ts @@ -469,3 +469,28 @@ export class Reforge { } export const reforge = new Reforge(); + +export function prefetchReforgeConfig({ + sdkKey, + context, + endpoints = undefined, + timeout = undefined, + collectContextMode = "PERIODIC_EXAMPLE", + clientNameString = "sdk-javascript", + clientVersionString = version, +}: ReforgeInitParams) { + const clientNameAndVersionString = `${clientNameString}-${clientVersionString}`; + + const loader = new Loader({ + sdkKey, + context, + endpoints, + timeout, + collectContextMode, + clientVersion: clientNameAndVersionString, + }); + + (window as any).REFORGE_SDK_PREFETCH_PROMISE = loader.load(); +} + +export default prefetchReforgeConfig;