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
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ To avoid a request waterfall, you can start fetching the configuration early in
lifecycle, before the React SDK or `reforge.init()` is called.

```javascript
import { prefetchReforgeConfig, Context } from "@reforge-com/javascript";
import { prefetchReforgeConfig, Context } from "@reforge-com/javascript/prefetch";

prefetchReforgeConfig({
sdkKey: "1234",
Expand All @@ -83,7 +83,8 @@ prefetchReforgeConfig({
});
```

When you later call `reforge.init()`, it will automatically use the prefetched promise if available.
This lightweight import (~3KB) is ideal for early loading. When you later call `reforge.init()`, it
will automatically use the prefetched promise if available.

## Client API

Expand Down
7 changes: 5 additions & 2 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ import {
import { Config } from "./src/config";
import Context from "./src/context";
import { LogLevel, getLogLevelSeverity, shouldLogAtLevel } from "./src/logger";
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { version } = require("./package.json");

/* eslint-disable no-underscore-dangle */
declare const __SDK_VERSION__: string;
const version = __SDK_VERSION__;
/* eslint-enable no-underscore-dangle */

export {
reforge,
Expand Down
7 changes: 6 additions & 1 deletion jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@

import type { Config } from "jest";

// eslint-disable-next-line @typescript-eslint/no-var-requires
const packageJson = require("./package.json");

const config: Config = {
// All imported modules in your tests should be mocked automatically
// automock: false,
Expand Down Expand Up @@ -67,7 +70,9 @@ const config: Config = {
// globalTeardown: undefined,

// A set of global variables that need to be available in all test environments
// globals: {},
globals: {
__SDK_VERSION__: packageJson.version,
},

// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
// maxWorkers: "50%",
Expand Down
12 changes: 7 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
"@types/eslint-plugin-jsx-a11y": "^6",
"@types/express": "^4.17.13",
"@types/jest": "^28.1.6",
"@types/uuid": "^9.0.5",
"@typescript-eslint/eslint-plugin": "^5.33.0",
"@typescript-eslint/parser": "^5.33.0",
"esbuild": "^0.25.11",
Expand Down Expand Up @@ -55,14 +54,17 @@
"url": "https://github.com/ReforgeHQ/sdk-javascript/issues"
},
"homepage": "https://github.com/ReforgeHQ/sdk-javascript#readme",
"dependencies": {
"uuid": "^9.0.1"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
},
"./prefetch": {
"types": "./dist/src/prefetch.d.ts",
"import": "./dist/src/prefetch.mjs",
"require": "./dist/src/prefetch.cjs"
}
}
},
"sideEffects": false
}
58 changes: 56 additions & 2 deletions src/prefetch.test.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
/**
* @jest-environment jsdom
*/
import { prefetchReforgeConfig, Reforge } from "./reforge";
import Context from "./context";
import { prefetchReforgeConfig, Context } from "./prefetch";
import { Reforge } from "./reforge";

describe("prefetchReforgeConfig", () => {
const sdkKey = "test-sdk-key";
const context = new Context({ user: { id: "123" } });
let consoleWarnSpy: jest.SpyInstance;

beforeEach(() => {
// Reset window object
(window as any).REFORGE_SDK_PREFETCH_PROMISE = undefined;
jest.clearAllMocks();
consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
});

afterEach(() => {
consoleWarnSpy.mockRestore();
});

it("should set REFORGE_SDK_PREFETCH_PROMISE on window", () => {
Expand Down Expand Up @@ -73,4 +79,52 @@ describe("prefetchReforgeConfig", () => {
// Verify window global was cleared (as per loader logic)
expect((window as any).REFORGE_SDK_PREFETCH_PROMISE).toBeUndefined();
});

it("should not warn when calling get after data is loaded", async () => {
// Mock fetch with actual config data
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: () =>
Promise.resolve({
evaluations: {
"test-flag": {
value: { bool: true },
configEvaluationMetadata: {
configRowIndex: "0",
conditionalValueIndex: "0",
type: "bool",
id: "123",
},
},
},
}),
} as Response)
);

// Start prefetch
prefetchReforgeConfig({ sdkKey, context });

// Initialize Reforge and wait for it to load
const reforgeInstance = new Reforge();
await reforgeInstance.init({ sdkKey, context });

// Call get after data is loaded - should NOT warn
const value = reforgeInstance.get("test-flag");

expect(value).toBe(true);
expect(consoleWarnSpy).not.toHaveBeenCalled();
});

it("should warn when calling get before data is loaded", () => {
const reforgeInstance = new Reforge();

// Call get before init - should warn
const value = reforgeInstance.get("some-flag");

expect(value).toBeUndefined();
expect(consoleWarnSpy).toHaveBeenCalledWith(
expect.stringContaining("The client has not finished loading data yet")
);
});
});
45 changes: 45 additions & 0 deletions src/prefetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import Context from "./context";
import Loader, { CollectContextModeType } from "./loader";

// Re-export Context so consumers can create contexts for prefetch
export { Context };

/* eslint-disable no-underscore-dangle */
declare const __SDK_VERSION__: string;
const version = __SDK_VERSION__;
/* eslint-enable no-underscore-dangle */

export type PrefetchParams = {
sdkKey: string;
context: Context;
endpoints?: string[] | undefined;
timeout?: number;
collectContextMode?: CollectContextModeType;
clientNameString?: string;
clientVersionString?: string;
};

export function prefetchReforgeConfig({
sdkKey,
context,
endpoints = undefined,
timeout = undefined,
collectContextMode = "PERIODIC_EXAMPLE",
clientNameString = "sdk-javascript",
clientVersionString = version,
}: PrefetchParams) {
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;
44 changes: 16 additions & 28 deletions src/reforge.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
/* eslint-disable max-classes-per-file */
import { v4 as uuid } from "uuid";

import { Config, EvaluationPayload, RawConfigWithoutTypes } from "./config";
import type {
Duration,
Expand All @@ -20,8 +18,19 @@ import {
} from "./logger";
import TelemetryUploader from "./telemetryUploader";
import { LoggerAggregator } from "./loggerAggregator";
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { version } = require("../package.json");

/* eslint-disable no-underscore-dangle */
declare const __SDK_VERSION__: string;
const version = __SDK_VERSION__;
/* eslint-enable no-underscore-dangle */

function uuid() {
if (typeof crypto !== "undefined") {
return crypto.randomUUID();
}

return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think this isn't a uuid.

maybe

function uuid() {
  if (typeof crypto !== "undefined" && crypto.randomUUID) {
    return crypto.randomUUID();
  }

  // Fallback: not cryptographically secure, but at least a valid UUIDv4 format
  return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
    const r = (Math.random() * 16) | 0;
    const v = c === "x" ? r : (r & 0x3) | 0x8;
    return v.toString(16);
  });
}

}

type EvaluationCallback = <K extends keyof TypedFrontEndConfigurationRaw>(
key: K,
Expand Down Expand Up @@ -470,27 +479,6 @@ 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;
// Re-export prefetchReforgeConfig for backwards compatibility
export { prefetchReforgeConfig } from "./prefetch";
export type { PrefetchParams } from "./prefetch";
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */

/* Language and Environment */
"target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
"target": "es2020" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
Expand Down
14 changes: 10 additions & 4 deletions tsup.config.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
import { defineConfig } from "tsup";
import { execSync } from "child_process";

// eslint-disable-next-line @typescript-eslint/no-var-requires
const packageJson = require("./package.json");

export default defineConfig({
entry: ["index.ts"],
entry: ["index.ts", "src/prefetch.ts"],
format: ["cjs", "esm"], // Build both CommonJS and ESM versions
dts: true, // Generate declaration files
splitting: false,
splitting: true, // Enable code splitting for smaller imports
sourcemap: true,
clean: true, // Clean output directory before build
minify: false,
external: ["uuid"], // Don't bundle uuid
minify: true,
external: [],
noExternal: [],
outDir: "dist",
define: {
__SDK_VERSION__: JSON.stringify(packageJson.version),
},
outExtension: ({ format }) => ({
js: format === "cjs" ? ".cjs" : ".mjs",
}),
Expand Down
18 changes: 0 additions & 18 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1295,7 +1295,6 @@ __metadata:
"@types/eslint-plugin-jsx-a11y": "npm:^6"
"@types/express": "npm:^4.17.13"
"@types/jest": "npm:^28.1.6"
"@types/uuid": "npm:^9.0.5"
"@typescript-eslint/eslint-plugin": "npm:^5.33.0"
"@typescript-eslint/parser": "npm:^5.33.0"
esbuild: "npm:^0.25.11"
Expand All @@ -1315,7 +1314,6 @@ __metadata:
ts-node: "npm:^10.9.1"
tsup: "npm:^8.0.2"
typescript: "npm:^5.1.6"
uuid: "npm:^9.0.1"
languageName: unknown
linkType: soft

Expand Down Expand Up @@ -1813,13 +1811,6 @@ __metadata:
languageName: node
linkType: hard

"@types/uuid@npm:^9.0.5":
version: 9.0.8
resolution: "@types/uuid@npm:9.0.8"
checksum: 10c0/b411b93054cb1d4361919579ef3508a1f12bf15b5fdd97337d3d351bece6c921b52b6daeef89b62340fd73fd60da407878432a1af777f40648cbe53a01723489
languageName: node
linkType: hard

"@types/yargs-parser@npm:*":
version: 21.0.3
resolution: "@types/yargs-parser@npm:21.0.3"
Expand Down Expand Up @@ -7580,15 +7571,6 @@ __metadata:
languageName: node
linkType: hard

"uuid@npm:^9.0.1":
version: 9.0.1
resolution: "uuid@npm:9.0.1"
bin:
uuid: dist/bin/uuid
checksum: 10c0/1607dd32ac7fc22f2d8f77051e6a64845c9bce5cd3dd8aa0070c074ec73e666a1f63c7b4e0f4bf2bc8b9d59dc85a15e17807446d9d2b17c8485fbc2147b27f9b
languageName: node
linkType: hard

"v8-compile-cache-lib@npm:^3.0.1":
version: 3.0.1
resolution: "v8-compile-cache-lib@npm:3.0.1"
Expand Down