From e18b66624a984b1a4081ae0e27d21a3eff21ce3f Mon Sep 17 00:00:00 2001 From: Ben Papillon Date: Fri, 21 Nov 2025 10:19:39 -0800 Subject: [PATCH] Offline flag check mode --- js/src/index.spec.ts | 507 ++++++++++++++++++++++++++++++++++++++++++ js/src/index.ts | 88 +++++--- js/src/types/index.ts | 52 +++-- 3 files changed, 597 insertions(+), 50 deletions(-) diff --git a/js/src/index.spec.ts b/js/src/index.spec.ts index 4f7644f20..da70657c9 100644 --- a/js/src/index.spec.ts +++ b/js/src/index.spec.ts @@ -1417,4 +1417,511 @@ describe("WebSocket Fallback Behavior", () => { await schematic.cleanup(); }); }); + + describe("offlineFlagChecks", () => { + describe("Initialization", () => { + it("should initialize with offlineFlagChecks enabled", () => { + const schematic = new Schematic("API_KEY", { + offlineFlagChecks: true, + }); + + expect(schematic).toBeDefined(); + }); + + it("should work with flagValueDefaults when offlineFlagChecks is enabled", () => { + const schematic = new Schematic("API_KEY", { + offlineFlagChecks: true, + flagValueDefaults: { + 'test-flag': true, + }, + }); + + expect(schematic).toBeDefined(); + }); + + it("should work with flagCheckDefaults when offlineFlagChecks is enabled", () => { + const schematic = new Schematic("API_KEY", { + offlineFlagChecks: true, + flagCheckDefaults: { + 'complex-flag': { + flag: 'complex-flag', + value: false, + reason: 'Default from flagCheckDefaults', + }, + }, + }); + + expect(schematic).toBeDefined(); + }); + }); + + describe("Flag Checking Behavior", () => { + it("should return flagValueDefaults immediately without network calls", async () => { + const schematic = new Schematic("API_KEY", { + offlineFlagChecks: true, + flagValueDefaults: { + 'premium-feature': true, + 'beta-feature': false, + }, + }); + + const premiumResult = await schematic.checkFlag({ key: 'premium-feature' }); + const betaResult = await schematic.checkFlag({ key: 'beta-feature' }); + const unknownResult = await schematic.checkFlag({ key: 'unknown-feature' }); + + expect(premiumResult).toBe(true); + expect(betaResult).toBe(false); + expect(unknownResult).toBe(false); // Default fallback + expect(mockFetch).not.toHaveBeenCalled(); // No network calls + }); + + it("should return flagCheckDefaults immediately without network calls", async () => { + const schematic = new Schematic("API_KEY", { + offlineFlagChecks: true, + flagCheckDefaults: { + 'complex-flag': { + flag: 'complex-flag', + value: true, + reason: 'From flagCheckDefaults', + companyId: 'test-company', + }, + }, + }); + + const result = await schematic.checkFlag({ key: 'complex-flag' }); + + expect(result).toBe(true); + expect(mockFetch).not.toHaveBeenCalled(); // No network calls + }); + + it("should prioritize callsite fallback over flagValueDefaults", async () => { + const schematic = new Schematic("API_KEY", { + offlineFlagChecks: true, + flagValueDefaults: { + 'priority-test': false, + }, + }); + + const result = await schematic.checkFlag({ + key: 'priority-test', + fallback: true + }); + + expect(result).toBe(true); // Callsite fallback wins + expect(mockFetch).not.toHaveBeenCalled(); + }); + + it("should prioritize flagCheckDefaults over flagValueDefaults", async () => { + const schematic = new Schematic("API_KEY", { + offlineFlagChecks: true, + flagValueDefaults: { + 'priority-test': true, + }, + flagCheckDefaults: { + 'priority-test': { + flag: 'priority-test', + value: false, + reason: 'flagCheckDefaults wins', + }, + }, + }); + + const result = await schematic.checkFlag({ key: 'priority-test' }); + + expect(result).toBe(false); // flagCheckDefaults wins over flagValueDefaults + expect(mockFetch).not.toHaveBeenCalled(); + }); + }); + + describe("checkFlags method", () => { + it("should return empty object without network calls when offlineFlagChecks is enabled", async () => { + const schematic = new Schematic("API_KEY", { + offlineFlagChecks: true, + }); + + const result = await schematic.checkFlags(); + + expect(result).toEqual({}); + expect(mockFetch).not.toHaveBeenCalled(); // No network calls + }); + }); + + describe("Event Submission", () => { + beforeEach(() => { + mockFetch.mockResolvedValue({ ok: true, status: 200 }); + }); + + it("should send events normally when offlineFlagChecks is enabled", async () => { + const schematic = new Schematic("API_KEY", { + offlineFlagChecks: true, + }); + + await schematic.identify({ + keys: { userId: "test-user" }, + traits: { plan: "premium" }, + }); + + expect(mockFetch).toHaveBeenCalledTimes(1); + expect(mockFetch).toHaveBeenCalledWith( + expect.stringContaining("/e"), // Event URL + expect.objectContaining({ + method: "POST", + headers: expect.objectContaining({ + "Content-Type": "application/json;charset=UTF-8", + }), + }) + ); + }); + + it("should send track events normally when offlineFlagChecks is enabled", async () => { + const schematic = new Schematic("API_KEY", { + offlineFlagChecks: true, + }); + + await schematic.track({ + user: { userId: "test-user" }, + event: "feature_used", + traits: { feature: "premium-feature" }, + }); + + expect(mockFetch).toHaveBeenCalledTimes(1); + expect(mockFetch).toHaveBeenCalledWith( + expect.stringContaining("/e"), // Event URL + expect.objectContaining({ + method: "POST", + }) + ); + }); + }); + + describe("WebSocket Behavior", () => { + it("should not establish WebSocket connection when offlineFlagChecks is enabled", async () => { + const schematic = new Schematic("API_KEY", { + offlineFlagChecks: true, + useWebSocket: true, + webSocketUrl: "ws://localhost:1234", + }); + + await schematic.setContext({ + user: { userId: "test-user" }, + }); + + // Should not attempt WebSocket connection + expect(schematic.getIsPending()).toBe(false); + }); + + it("should not send WebSocket messages when offlineFlagChecks is enabled", async () => { + const schematic = new Schematic("API_KEY", { + offlineFlagChecks: true, + useWebSocket: true, + flagValueDefaults: { + 'ws-test': true, + }, + }); + + // Set context explicitly to test WebSocket avoidance + await schematic.setContext({ user: { userId: "test-user" } }); + + const result = await schematic.checkFlag({ + key: 'ws-test', + }); + + expect(result).toBe(true); // Uses flagValueDefaults + expect(schematic.getIsPending()).toBe(false); + }); + }); + + describe("Comparison with Full Offline Mode", () => { + beforeEach(() => { + mockFetch.mockResolvedValue({ ok: true, status: 200 }); + }); + + it("should behave differently from full offline mode for events", async () => { + const offlineFlagChecksSchematic = new Schematic("API_KEY", { + offlineFlagChecks: true, + }); + + const fullOfflineSchematic = new Schematic("API_KEY", { + offline: true, + }); + + // Both should handle flags the same way + const flagResult1 = await offlineFlagChecksSchematic.checkFlag({ key: 'test-flag' }); + const flagResult2 = await fullOfflineSchematic.checkFlag({ key: 'test-flag' }); + + expect(flagResult1).toBe(flagResult2); // Both return false by default + + // But events should behave differently + await offlineFlagChecksSchematic.identify({ keys: { userId: "test1" } }); + await fullOfflineSchematic.identify({ keys: { userId: "test2" } }); + + // offlineFlagChecks should send events, offline should not + expect(mockFetch).toHaveBeenCalledTimes(1); // Only offlineFlagChecks sent event + }); + + it("should return same flag values but handle events differently", async () => { + const flagDefaults = { + 'premium': true, + 'beta': false, + }; + + const offlineFlagChecksSchematic = new Schematic("API_KEY", { + offlineFlagChecks: true, + flagValueDefaults: flagDefaults, + }); + + const fullOfflineSchematic = new Schematic("API_KEY", { + offline: true, + flagValueDefaults: flagDefaults, + }); + + // Flag values should be identical + const premium1 = await offlineFlagChecksSchematic.checkFlag({ key: 'premium' }); + const premium2 = await fullOfflineSchematic.checkFlag({ key: 'premium' }); + + const beta1 = await offlineFlagChecksSchematic.checkFlag({ key: 'beta' }); + const beta2 = await fullOfflineSchematic.checkFlag({ key: 'beta' }); + + expect(premium1).toBe(premium2); // true + expect(beta1).toBe(beta2); // false + + // Event behavior should differ + mockFetch.mockClear(); + + await offlineFlagChecksSchematic.track({ + user: { userId: "test1" }, + event: "test_event1", + }); + + await fullOfflineSchematic.track({ + user: { userId: "test2" }, + event: "test_event2", + }); + + // Only offlineFlagChecks should send the event + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + }); + }); + + describe("Mode Interactions", () => { + beforeEach(() => { + mockFetch.mockResolvedValue({ ok: true, status: 200 }); + }); + + describe("offline + useWebSocket", () => { + it("should prioritize offline mode over WebSocket for flag checks", async () => { + const schematic = new Schematic("API_KEY", { + offline: true, + useWebSocket: true, // This should be ignored due to offline mode + flagValueDefaults: { + 'test-flag': true, + }, + }); + + const result = await schematic.checkFlag({ key: 'test-flag' }); + + expect(result).toBe(true); // Uses flagValueDefaults + expect(mockFetch).not.toHaveBeenCalled(); // No network calls at all + expect(schematic.getIsPending()).toBe(false); // Should not be pending + }); + + it("should prioritize offline mode over WebSocket for events", async () => { + const schematic = new Schematic("API_KEY", { + offline: true, + useWebSocket: true, + }); + + await schematic.identify({ keys: { userId: "test-user" } }); + + // Should not send event due to offline mode + expect(mockFetch).not.toHaveBeenCalled(); + }); + + it("should prioritize offline mode for setContext", async () => { + const schematic = new Schematic("API_KEY", { + offline: true, + useWebSocket: true, + }); + + await schematic.setContext({ user: { userId: "test-user" } }); + + // Should not attempt WebSocket connection + expect(schematic.getIsPending()).toBe(false); + }); + }); + + describe("offlineFlagChecks + useWebSocket", () => { + it("should disable WebSocket for flag checks but allow events", async () => { + const schematic = new Schematic("API_KEY", { + offlineFlagChecks: true, + useWebSocket: true, // Should be ignored for flags but events should still work + flagValueDefaults: { + 'test-flag': false, + }, + }); + + // Flag check should use defaults without WebSocket + const flagResult = await schematic.checkFlag({ key: 'test-flag' }); + expect(flagResult).toBe(false); + // Note: isPending might be true temporarily due to WebSocket initialization attempt + // but the flag check itself uses defaults immediately + + // Events should still work normally + await schematic.track({ + user: { userId: "test-user" }, + event: "test_event", + }); + + expect(mockFetch).toHaveBeenCalledTimes(1); // Only the event call + expect(mockFetch).toHaveBeenCalledWith( + expect.stringContaining("/e"), // Event URL + expect.objectContaining({ method: "POST" }) + ); + }); + + it("should not establish WebSocket connection when offlineFlagChecks is enabled", async () => { + const schematic = new Schematic("API_KEY", { + offlineFlagChecks: true, + useWebSocket: true, + }); + + // Even with useWebSocket=true, should not connect due to offlineFlagChecks + await schematic.setContext({ user: { userId: "test-user" } }); + + expect(schematic.getIsPending()).toBe(false); + }); + }); + + describe("offline + offlineFlagChecks", () => { + it("should prioritize offline mode when both are enabled", async () => { + const schematic = new Schematic("API_KEY", { + offline: true, + offlineFlagChecks: true, // This should be redundant + flagValueDefaults: { + 'test-flag': true, + }, + }); + + // Flag behavior should be the same + const flagResult = await schematic.checkFlag({ key: 'test-flag' }); + expect(flagResult).toBe(true); + + // Events should be blocked (offline mode wins) + await schematic.track({ + user: { userId: "test-user" }, + event: "test_event", + }); + + expect(mockFetch).not.toHaveBeenCalled(); // No network calls at all + }); + + it("should handle initialization with both modes enabled", () => { + const schematic = new Schematic("API_KEY", { + offline: true, + offlineFlagChecks: true, + }); + + expect(schematic).toBeDefined(); + expect(schematic.getIsPending()).toBe(false); + }); + }); + + describe("complex mode combinations", () => { + it("should handle offline + useWebSocket + offlineFlagChecks", async () => { + const schematic = new Schematic("API_KEY", { + offline: true, // Should override everything + useWebSocket: true, + offlineFlagChecks: true, + flagValueDefaults: { + 'test-flag': false, + }, + }); + + // Flag checks should work with defaults + const flagResult = await schematic.checkFlag({ key: 'test-flag' }); + expect(flagResult).toBe(false); + + // Events should be blocked due to offline mode + await schematic.identify({ keys: { userId: "test-user" } }); + await schematic.track({ user: { userId: "test-user" }, event: "test_event" }); + + expect(mockFetch).not.toHaveBeenCalled(); // No network calls + expect(schematic.getIsPending()).toBe(false); + }); + + it("should handle useWebSocket + offlineFlagChecks without offline", async () => { + const schematic = new Schematic("API_KEY", { + useWebSocket: true, // Should be disabled for flags but events work + offlineFlagChecks: true, + flagValueDefaults: { + 'premium-feature': true, + }, + }); + + // Flags should use defaults immediately + const flagResult = await schematic.checkFlag({ key: 'premium-feature' }); + expect(flagResult).toBe(true); + // Note: isPending might be true due to WebSocket initialization attempt + // but flag evaluation still uses defaults immediately + + // Events should still be sent + await schematic.identify({ keys: { userId: "test-user" } }); + + expect(mockFetch).toHaveBeenCalledTimes(1); // Only event call + expect(mockFetch).toHaveBeenCalledWith( + expect.stringContaining("/e"), + expect.objectContaining({ method: "POST" }) + ); + }); + }); + + describe("mode precedence and conflicts", () => { + it("should establish clear precedence: offline > offlineFlagChecks > useWebSocket", async () => { + // Test that offline mode completely overrides other settings + const fullOfflineSchematic = new Schematic("API_KEY", { + offline: true, + offlineFlagChecks: false, // Should be ignored + useWebSocket: true, // Should be ignored + }); + + await fullOfflineSchematic.identify({ keys: { userId: "test1" } }); + expect(mockFetch).not.toHaveBeenCalled(); + + mockFetch.mockClear(); + + // Test that offlineFlagChecks overrides useWebSocket for flags only + const flagsOfflineSchematic = new Schematic("API_KEY", { + offline: false, + offlineFlagChecks: true, + useWebSocket: true, // Should be ignored for flags + }); + + await flagsOfflineSchematic.checkFlag({ key: 'test-flag' }); + await flagsOfflineSchematic.identify({ keys: { userId: "test2" } }); + + expect(mockFetch).toHaveBeenCalledTimes(1); // Only the identify call + }); + + it("should handle conflicting fallback configurations gracefully", async () => { + const schematic = new Schematic("API_KEY", { + offline: true, + flagValueDefaults: { + 'conflict-flag': true, + }, + flagCheckDefaults: { + 'conflict-flag': { + flag: 'conflict-flag', + value: false, // Conflicts with flagValueDefaults + reason: 'From flagCheckDefaults', + }, + }, + }); + + // Should prioritize flagCheckDefaults over flagValueDefaults + const result = await schematic.checkFlag({ key: 'conflict-flag' }); + expect(result).toBe(false); // flagCheckDefaults wins + }); + }); + }); }); diff --git a/js/src/index.ts b/js/src/index.ts index 281f48ffc..e06929f16 100644 --- a/js/src/index.ts +++ b/js/src/index.ts @@ -39,6 +39,7 @@ export class Schematic { private context: SchematicContext = {}; private debugEnabled: boolean = false; private offlineEnabled: boolean = false; + private offlineFlagChecksEnabled: boolean = false; private eventQueue: Event[]; private contextDependentEventQueue: Event[]; private eventUrl = "https://c.schematichq.com"; @@ -80,6 +81,7 @@ export class Schematic { this.useWebSocket = options?.useWebSocket ?? false; this.debugEnabled = options?.debug ?? false; this.offlineEnabled = options?.offline ?? false; + this.offlineFlagChecksEnabled = options?.offlineFlagChecks ?? false; // Check for debug mode in URL query parameter if ( @@ -212,6 +214,10 @@ export class Schematic { this.debug( "Initialized with offline mode enabled - no network requests will be made", ); + } else if (this.offlineFlagChecksEnabled) { + this.debug( + "Initialized with offline flag checks enabled - flag checks will use defaults, events will still be sent", + ); } else if (this.debugEnabled) { this.debug("Initialized with debug mode enabled"); } @@ -306,17 +312,21 @@ export class Schematic { this.debug(`checkFlag: ${key}`, { context, fallback }); - // If in offline mode, return fallback immediately without making any network request - if (this.isOffline()) { - // In offline mode, use the full fallback resolution including flagCheckDefaults + // If flag checks are offline, return fallback immediately without making any network request + if (this.areFlagChecksOffline()) { + // Use the full fallback resolution including flagCheckDefaults + const reasonText = this.offlineEnabled + ? "Offline mode - using initialization defaults" + : "Flag checks disabled - using initialization defaults"; const resolvedFallbackResult = this.resolveFallbackCheckFlagReturn( key, fallback, - "Offline mode - using initialization defaults", + reasonText, ); this.debug(`checkFlag offline result: ${key}`, { value: resolvedFallbackResult.value, - offlineMode: true, + offlineMode: this.offlineEnabled, + offlineFlagChecks: this.offlineFlagChecksEnabled, }); return resolvedFallbackResult.value; @@ -384,8 +394,8 @@ export class Schematic { return existingVals[key].value; } - // If in offline mode, return fallback immediately - if (this.isOffline()) { + // If flag checks are offline, return fallback immediately + if (this.areFlagChecksOffline()) { return this.resolveFallbackValue(key, fallback); } @@ -451,6 +461,14 @@ export class Schematic { return this.offlineEnabled; } + /** + * Helper function to check if flag checks should be offline + * (either full offline mode or just flag checks disabled) + */ + private areFlagChecksOffline(): boolean { + return this.offlineEnabled || this.offlineFlagChecksEnabled; + } + /** * Submit a flag check event * Records data about a flag check for analytics @@ -486,12 +504,13 @@ export class Schematic { context: SchematicContext, fallback?: boolean, ): Promise { - // If in offline mode, immediately return fallback value - if (this.isOffline()) { + // If flag checks are offline, immediately return fallback value + if (this.areFlagChecksOffline()) { const resolvedFallback = this.resolveFallbackValue(key, fallback); this.debug(`fallbackToRest offline result: ${key}`, { value: resolvedFallback, - offlineMode: true, + offlineMode: this.offlineEnabled, + offlineFlagChecks: this.offlineFlagChecksEnabled, }); return resolvedFallback; @@ -558,9 +577,12 @@ export class Schematic { this.debug(`checkFlags`, { context }); - // If in offline mode, return empty object without making network request - if (this.isOffline()) { - this.debug(`checkFlags offline result: returning empty object`); + // If flag checks are offline, return empty object without making network request + if (this.areFlagChecksOffline()) { + this.debug(`checkFlags offline result: returning empty object`, { + offlineMode: this.offlineEnabled, + offlineFlagChecks: this.offlineFlagChecksEnabled, + }); return {}; } @@ -633,7 +655,7 @@ export class Schematic { * In offline mode, this will just set the context locally without connecting. */ setContext = async (context: SchematicContext): Promise => { - if (this.isOffline() || !this.useWebSocket) { + if (this.areFlagChecksOffline() || !this.useWebSocket) { this.context = context; this.flushContextDependentEventQueue(); this.setIsPending(false); @@ -1036,9 +1058,12 @@ export class Schematic { * In offline mode, this is a no-op. */ cleanup = async (): Promise => { - // In offline mode, no need to clean up connections since none are made - if (this.isOffline()) { - this.debug("cleanup: skipped (offline mode)"); + // If flag checks are offline, no need to clean up connections since none are made + if (this.areFlagChecksOffline()) { + const reason = this.offlineEnabled + ? "offline mode" + : "offline flag checks enabled"; + this.debug(`cleanup: skipped (${reason})`); return Promise.resolve(); } @@ -1214,11 +1239,14 @@ export class Schematic { // Open a websocket connection private wsConnect = (): Promise => { - // If in offline mode, don't actually connect - if (this.isOffline()) { - this.debug("wsConnect: skipped (offline mode)"); + // If flag checks are offline, don't actually connect + if (this.areFlagChecksOffline()) { + const reason = this.offlineEnabled + ? "offline mode" + : "offline flag checks enabled"; + this.debug(`wsConnect: skipped (${reason})`); return Promise.reject( - new Error("WebSocket connection skipped in offline mode"), + new Error(`WebSocket connection skipped in ${reason}`), ); } @@ -1289,9 +1317,12 @@ export class Schematic { socket: WebSocket, context: SchematicContext, ): Promise => { - // If in offline mode, don't send messages - if (this.isOffline()) { - this.debug("wsSendContextAfterReconnection: skipped (offline mode)"); + // If flag checks are offline, don't send messages + if (this.areFlagChecksOffline()) { + const reason = this.offlineEnabled + ? "offline mode" + : "offline flag checks enabled"; + this.debug(`wsSendContextAfterReconnection: skipped (${reason})`); this.setIsPending(false); return Promise.resolve(); } @@ -1373,9 +1404,12 @@ export class Schematic { socket: WebSocket, context: SchematicContext, ): Promise => { - // If in offline mode, don't send messages - if (this.isOffline()) { - this.debug("wsSendMessage: skipped (offline mode)"); + // If flag checks are offline, don't send messages + if (this.areFlagChecksOffline()) { + const reason = this.offlineEnabled + ? "offline mode" + : "offline flag checks enabled"; + this.debug(`wsSendMessage: skipped (${reason})`); this.setIsPending(false); return Promise.resolve(); } diff --git a/js/src/types/index.ts b/js/src/types/index.ts index e8ba3f430..d4f94f788 100644 --- a/js/src/types/index.ts +++ b/js/src/types/index.ts @@ -126,55 +126,61 @@ export type SchematicOptions = { * Can also be enabled at runtime via URL query parameter "schematic_debug=true" */ debug?: boolean; + /** Initial retry delay in milliseconds for failed events (default: 1000) */ + eventRetryInitialDelay?: number; + + /** Maximum retry delay in milliseconds for failed events (default: 30000) */ + eventRetryMaxDelay?: number; + + /** Default CheckFlagReturn objects for flags when Schematic API cannot be reached and no callsite fallback is provided */ + flagCheckDefaults?: Record; + + /** Default boolean values for flags when Schematic API cannot be reached and no callsite fallback is provided */ + flagValueDefaults?: Record; + /** Optionally provide a custom event URL */ eventUrl?: string; + /** Maximum number of events to queue for retry when network is down (default: 100) */ + maxEventQueueSize?: number; + + /** Maximum number of retry attempts for failed events (default: 5) */ + maxEventRetries?: number; + /** Enable offline mode to prevent all network requests. * When enabled, events are only logged not sent, and flag checks return fallback values. * Can also be enabled at runtime via URL query parameter "schematic_offline=true" */ offline?: boolean; + /** Disable networked flag checks while still allowing event submission. + * When enabled, flag checks return fallback values immediately without making network requests, + * but events are still sent normally. This is useful for scenarios where you want to provide + * already-evaluated flag values from your backend. */ + offlineFlagChecks?: boolean; + /** Optionally provide a custom storage persister for client-side storage */ storage?: StoragePersister; /** Use a WebSocket connection for real-time flag checks; if using this, run the cleanup function to close the connection */ useWebSocket?: boolean; - /** Optionally provide a custom WebSocket URL */ - webSocketUrl?: string; - /** WebSocket connection timeout in milliseconds (default: 10000) */ webSocketConnectionTimeout?: number; /** Enable automatic reconnection on WebSocket disconnect (default: true) */ webSocketReconnect?: boolean; - /** Maximum number of reconnection attempts (default: 7, set to Infinity for unlimited) */ - webSocketMaxReconnectAttempts?: number; - /** Initial retry delay in milliseconds for exponential backoff (default: 1000) */ webSocketInitialRetryDelay?: number; + /** Maximum number of reconnection attempts (default: 7, set to Infinity for unlimited) */ + webSocketMaxReconnectAttempts?: number; + /** Maximum retry delay in milliseconds for exponential backoff (default: 30000) */ webSocketMaxRetryDelay?: number; - /** Maximum number of events to queue for retry when network is down (default: 100) */ - maxEventQueueSize?: number; - - /** Maximum number of retry attempts for failed events (default: 5) */ - maxEventRetries?: number; - - /** Initial retry delay in milliseconds for failed events (default: 1000) */ - eventRetryInitialDelay?: number; - - /** Maximum retry delay in milliseconds for failed events (default: 30000) */ - eventRetryMaxDelay?: number; - - /** Default boolean values for flags when Schematic API cannot be reached and no callsite fallback is provided */ - flagValueDefaults?: Record; - - /** Default CheckFlagReturn objects for flags when Schematic API cannot be reached and no callsite fallback is provided */ - flagCheckDefaults?: Record; + /** Optionally provide a custom WebSocket URL */ + webSocketUrl?: string; }; export type CheckOptions = {