diff --git a/packages/browser-sdk/package.json b/packages/browser-sdk/package.json index c9c13870..61577760 100644 --- a/packages/browser-sdk/package.json +++ b/packages/browser-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@reflag/browser-sdk", - "version": "1.4.2", + "version": "1.4.3", "packageManager": "yarn@4.1.1", "license": "MIT", "repository": { diff --git a/packages/browser-sdk/src/flag/flags.ts b/packages/browser-sdk/src/flag/flags.ts index 408d3adc..1a776f6a 100644 --- a/packages/browser-sdk/src/flag/flags.ts +++ b/packages/browser-sdk/src/flag/flags.ts @@ -5,7 +5,7 @@ import { ReflagContext } from "../context"; import { HttpClient } from "../httpClient"; import { Logger, loggerWithPrefix } from "../logger"; import RateLimiter from "../rateLimiter"; -import { getLocalStorageAdapter, StorageAdapter } from "../storage"; +import { getDefaultStorageAdapter, StorageAdapter } from "../storage"; import { createAbortController } from "../utils/abortController"; import { createEventTarget } from "../utils/eventTarget"; @@ -235,7 +235,7 @@ export class FlagsClient { this.logger = loggerWithPrefix(logger, "[Flags]"); this.rateLimiter = rateLimiter ?? new RateLimiter(FLAG_EVENTS_PER_MIN, this.logger); - this.storage = (cache ? undefined : storage) ?? getLocalStorageAdapter(); + this.storage = (cache ? undefined : storage) ?? getDefaultStorageAdapter(); this.cache = cache ?? this.setupCache(this.config.staleTimeMs, this.config.expireTimeMs); diff --git a/packages/browser-sdk/src/storage.ts b/packages/browser-sdk/src/storage.ts index ffe5ab08..b031736b 100644 --- a/packages/browser-sdk/src/storage.ts +++ b/packages/browser-sdk/src/storage.ts @@ -1,9 +1,19 @@ +import { IS_SERVER } from "./config"; + export type StorageAdapter = { getItem(key: string): Promise; setItem(key: string, value: string): Promise; removeItem?(key: string): Promise; }; +export function createNoopStorageAdapter(): StorageAdapter { + return { + getItem: async () => null, + setItem: async () => undefined, + removeItem: async () => undefined, + }; +} + export function getLocalStorageAdapter(): StorageAdapter { if ( typeof localStorage === "undefined" || @@ -24,3 +34,8 @@ export function getLocalStorageAdapter(): StorageAdapter { }, }; } + +export function getDefaultStorageAdapter(): StorageAdapter { + if (IS_SERVER) return createNoopStorageAdapter(); + return getLocalStorageAdapter(); +} diff --git a/packages/browser-sdk/test/client.test.ts b/packages/browser-sdk/test/client.test.ts index 48e2b215..bf1fe228 100644 --- a/packages/browser-sdk/test/client.test.ts +++ b/packages/browser-sdk/test/client.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { ReflagClient } from "../src/client"; import { FlagsClient } from "../src/flag/flags"; @@ -23,6 +23,10 @@ describe("ReflagClient", () => { vi.clearAllMocks(); }); + afterEach(() => { + vi.unstubAllGlobals(); + }); + describe("updateUser", () => { it("should update the user context", async () => { // and send new user data and trigger flag update diff --git a/packages/browser-sdk/test/storage.test.ts b/packages/browser-sdk/test/storage.test.ts new file mode 100644 index 00000000..ffcc5fab --- /dev/null +++ b/packages/browser-sdk/test/storage.test.ts @@ -0,0 +1,45 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +async function loadStorageModule() { + vi.resetModules(); + return import("../src/storage"); +} + +describe("storage adapters", () => { + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it("noop adapter ignores writes", async () => { + const { createNoopStorageAdapter } = await loadStorageModule(); + const adapter = createNoopStorageAdapter(); + + expect(await adapter.getItem("key")).toBeNull(); + await adapter.setItem("key", "value"); + expect(await adapter.getItem("key")).toBeNull(); + + await adapter.removeItem?.("key"); + expect(await adapter.getItem("key")).toBeNull(); + }); + + it("localStorage adapter throws when localStorage is unavailable", async () => { + const { getLocalStorageAdapter } = await loadStorageModule(); + vi.stubGlobal("localStorage", undefined); + expect(() => getLocalStorageAdapter()).toThrowError( + "localStorage is not available. Provide a custom storage adapter.", + ); + }); + + it("default adapter falls back to noop on server runtimes", async () => { + vi.resetModules(); + vi.stubGlobal("window", undefined); + vi.stubGlobal("document", undefined); + vi.stubGlobal("localStorage", undefined); + + const { getDefaultStorageAdapter } = await import("../src/storage"); + const adapter = getDefaultStorageAdapter(); + + await adapter.setItem("fallback", "ok"); + expect(await adapter.getItem("fallback")).toBeNull(); + }); +}); diff --git a/packages/react-native-sdk/package.json b/packages/react-native-sdk/package.json index 543c2032..85f1e2b8 100644 --- a/packages/react-native-sdk/package.json +++ b/packages/react-native-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@reflag/react-native-sdk", - "version": "0.1.2", + "version": "0.1.3", "license": "MIT", "repository": { "type": "git", @@ -32,7 +32,7 @@ }, "dependencies": { "@react-native-async-storage/async-storage": "^2.2.0", - "@reflag/react-sdk": "1.4.2" + "@reflag/react-sdk": "1.4.3" }, "peerDependencies": { "react": "*", diff --git a/packages/react-sdk/package.json b/packages/react-sdk/package.json index 5ff56280..803e2555 100644 --- a/packages/react-sdk/package.json +++ b/packages/react-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@reflag/react-sdk", - "version": "1.4.2", + "version": "1.4.3", "license": "MIT", "repository": { "type": "git", @@ -37,7 +37,7 @@ } }, "dependencies": { - "@reflag/browser-sdk": "1.4.2" + "@reflag/browser-sdk": "1.4.3" }, "peerDependencies": { "react": "*", diff --git a/yarn.lock b/yarn.lock index ee752c53..f19683f8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5036,7 +5036,7 @@ __metadata: languageName: node linkType: hard -"@reflag/browser-sdk@npm:1.4.2, @reflag/browser-sdk@workspace:packages/browser-sdk": +"@reflag/browser-sdk@npm:1.4.3, @reflag/browser-sdk@workspace:packages/browser-sdk": version: 0.0.0-use.local resolution: "@reflag/browser-sdk@workspace:packages/browser-sdk" dependencies: @@ -5223,7 +5223,7 @@ __metadata: dependencies: "@react-native-async-storage/async-storage": "npm:^2.2.0" "@reflag/eslint-config": "npm:^0.0.2" - "@reflag/react-sdk": "npm:1.4.2" + "@reflag/react-sdk": "npm:1.4.3" "@reflag/tsconfig": "npm:^0.0.2" "@types/react": "npm:^19.0.12" eslint: "npm:^9.21.0" @@ -5235,11 +5235,11 @@ __metadata: languageName: unknown linkType: soft -"@reflag/react-sdk@npm:1.4.2, @reflag/react-sdk@workspace:^, @reflag/react-sdk@workspace:packages/react-sdk": +"@reflag/react-sdk@npm:1.4.3, @reflag/react-sdk@workspace:^, @reflag/react-sdk@workspace:packages/react-sdk": version: 0.0.0-use.local resolution: "@reflag/react-sdk@workspace:packages/react-sdk" dependencies: - "@reflag/browser-sdk": "npm:1.4.2" + "@reflag/browser-sdk": "npm:1.4.3" "@reflag/eslint-config": "npm:^0.0.2" "@reflag/tsconfig": "npm:^0.0.2" "@testing-library/react": "npm:^15.0.7"