Skip to content
Open
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
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
9 changes: 8 additions & 1 deletion index.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -15,6 +21,7 @@ export {
getLogLevelSeverity,
shouldLogAtLevel,
version,
prefetchReforgeConfig,
};

export { ReforgeBootstrap };
Expand Down
13 changes: 13 additions & 0 deletions src/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Expand Down
73 changes: 73 additions & 0 deletions src/prefetch.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
25 changes: 25 additions & 0 deletions src/reforge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;