Skip to content

Commit 08a4daa

Browse files
committed
fix(browser-sdk): avoid localStorage crash during SSR
1 parent d7a6993 commit 08a4daa

File tree

4 files changed

+83
-3
lines changed

4 files changed

+83
-3
lines changed

packages/browser-sdk/src/flag/flags.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { ReflagContext } from "../context";
55
import { HttpClient } from "../httpClient";
66
import { Logger, loggerWithPrefix } from "../logger";
77
import RateLimiter from "../rateLimiter";
8-
import { getLocalStorageAdapter, StorageAdapter } from "../storage";
8+
import { getDefaultStorageAdapter, StorageAdapter } from "../storage";
99
import { createAbortController } from "../utils/abortController";
1010
import { createEventTarget } from "../utils/eventTarget";
1111

@@ -235,7 +235,7 @@ export class FlagsClient {
235235
this.logger = loggerWithPrefix(logger, "[Flags]");
236236
this.rateLimiter =
237237
rateLimiter ?? new RateLimiter(FLAG_EVENTS_PER_MIN, this.logger);
238-
this.storage = (cache ? undefined : storage) ?? getLocalStorageAdapter();
238+
this.storage = (cache ? undefined : storage) ?? getDefaultStorageAdapter();
239239
this.cache =
240240
cache ??
241241
this.setupCache(this.config.staleTimeMs, this.config.expireTimeMs);

packages/browser-sdk/src/storage.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,20 @@ export type StorageAdapter = {
44
removeItem?(key: string): Promise<void>;
55
};
66

7+
export function createMemoryStorageAdapter(): StorageAdapter {
8+
const memoryStorage = new Map<string, string>();
9+
10+
return {
11+
getItem: async (key) => memoryStorage.get(key) ?? null,
12+
setItem: async (key, value) => {
13+
memoryStorage.set(key, value);
14+
},
15+
removeItem: async (key) => {
16+
memoryStorage.delete(key);
17+
},
18+
};
19+
}
20+
721
export function getLocalStorageAdapter(): StorageAdapter {
822
if (
923
typeof localStorage === "undefined" ||
@@ -24,3 +38,11 @@ export function getLocalStorageAdapter(): StorageAdapter {
2438
},
2539
};
2640
}
41+
42+
export function getDefaultStorageAdapter(): StorageAdapter {
43+
try {
44+
return getLocalStorageAdapter();
45+
} catch {
46+
return createMemoryStorageAdapter();
47+
}
48+
}

packages/browser-sdk/test/client.test.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { beforeEach, describe, expect, it, vi } from "vitest";
1+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
22

33
import { ReflagClient } from "../src/client";
44
import { FlagsClient } from "../src/flag/flags";
@@ -23,6 +23,25 @@ describe("ReflagClient", () => {
2323
vi.clearAllMocks();
2424
});
2525

26+
afterEach(() => {
27+
vi.unstubAllGlobals();
28+
});
29+
30+
describe("storage runtime compatibility", () => {
31+
it("can be constructed without localStorage", () => {
32+
vi.stubGlobal("localStorage", undefined);
33+
34+
expect(
35+
() =>
36+
new ReflagClient({
37+
publishableKey: "test-key",
38+
user: { id: "user1" },
39+
company: { id: "company1" },
40+
}),
41+
).not.toThrow();
42+
});
43+
});
44+
2645
describe("updateUser", () => {
2746
it("should update the user context", async () => {
2847
// and send new user data and trigger flag update
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { afterEach, describe, expect, it, vi } from "vitest";
2+
3+
import {
4+
createMemoryStorageAdapter,
5+
getDefaultStorageAdapter,
6+
getLocalStorageAdapter,
7+
} from "../src/storage";
8+
9+
describe("storage adapters", () => {
10+
afterEach(() => {
11+
vi.unstubAllGlobals();
12+
});
13+
14+
it("memory adapter stores and retrieves values", async () => {
15+
const adapter = createMemoryStorageAdapter();
16+
17+
expect(await adapter.getItem("key")).toBeNull();
18+
await adapter.setItem("key", "value");
19+
expect(await adapter.getItem("key")).toBe("value");
20+
21+
await adapter.removeItem?.("key");
22+
expect(await adapter.getItem("key")).toBeNull();
23+
});
24+
25+
it("localStorage adapter throws when localStorage is unavailable", () => {
26+
vi.stubGlobal("localStorage", undefined);
27+
expect(() => getLocalStorageAdapter()).toThrowError(
28+
"localStorage is not available. Provide a custom storage adapter.",
29+
);
30+
});
31+
32+
it("default adapter falls back when localStorage is unavailable", async () => {
33+
vi.stubGlobal("localStorage", undefined);
34+
const adapter = getDefaultStorageAdapter();
35+
36+
await adapter.setItem("fallback", "ok");
37+
expect(await adapter.getItem("fallback")).toBe("ok");
38+
});
39+
});

0 commit comments

Comments
 (0)