@schematichq/schematic-react 1.2.15 → 1.2.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -136,6 +136,66 @@ const MyComponent = () => {
136
136
 
137
137
  *Note: `useSchematicIsPending` is checking if entitlement data has been loaded, typically via `identify`. It should, therefore, be used to wrap flag and entitlement checks, but never the initial call to `identify`.*
138
138
 
139
+ ## Fallback Behavior
140
+
141
+ The SDK includes built-in fallback behavior you can use to ensure your application continues to function even when unable to reach Schematic (e.g., during service disruptions or network issues).
142
+
143
+ ### Flag Check Fallbacks
144
+
145
+ When flag checks cannot reach Schematic, they use fallback values in the following priority order:
146
+
147
+ 1. Callsite fallback - fallback values can be provided directly in the hook options
148
+ 2. Initialization defaults - fallback values configured via `flagCheckDefaults` or `flagValueDefaults` options when initializing the provider
149
+ 3. Default value - Returns `false` if no fallback is configured
150
+
151
+ ```tsx
152
+ // Provide a fallback value at the callsite
153
+ import { useSchematicFlag } from "@schematichq/schematic-react";
154
+
155
+ const MyComponent = () => {
156
+ const isFeatureEnabled = useSchematicFlag("feature-flag", {
157
+ fallback: true, // Used if API request fails
158
+ });
159
+
160
+ return isFeatureEnabled ? <Feature /> : <Fallback />;
161
+ };
162
+
163
+ // Or configure defaults at initialization
164
+ import { SchematicProvider } from "@schematichq/schematic-react";
165
+
166
+ ReactDOM.render(
167
+ <SchematicProvider
168
+ publishableKey="your-publishable-key"
169
+ flagValueDefaults={{
170
+ "feature-flag": true, // Used if API request fails and no callsite fallback
171
+ }}
172
+ flagCheckDefaults={{
173
+ "another-flag": {
174
+ flag: "another-flag",
175
+ value: true,
176
+ reason: "Default value",
177
+ },
178
+ }}
179
+ >
180
+ <App />
181
+ </SchematicProvider>,
182
+ document.getElementById("root"),
183
+ );
184
+ ```
185
+
186
+ ### Event Queueing and Retry
187
+
188
+ When events (track, identify) cannot be sent due to network issues, they are automatically queued and retried:
189
+
190
+ - Events are queued in memory (up to 100 events by default, configurable via `maxEventQueueSize`)
191
+ - Failed events are retried with exponential backoff (up to 5 attempts by default, configurable via `maxEventRetries`)
192
+ - Events are automatically flushed when the network connection is restored
193
+ - Events queued when the page is hidden are sent when the page becomes visible
194
+
195
+ ### WebSocket Fallback
196
+
197
+ In WebSocket mode, if the WebSocket connection fails, the SDK will provide the last known value or the configured fallback values as [outlined above](/#flag-check-fallbacks). The WebSocket will also automatically attempt to re-establish it's connection with Schematic using an exponential backoff.
198
+
139
199
  ## React Native
140
200
 
141
201
  ### Handling app background/foreground
@@ -217,7 +277,6 @@ ReactDOM.render(
217
277
 
218
278
  Offline mode automatically enables debug mode to help with troubleshooting.
219
279
 
220
-
221
280
  ## License
222
281
 
223
282
  MIT
@@ -799,7 +799,7 @@ function contextString(context) {
799
799
  }, {});
800
800
  return JSON.stringify(sortedContext);
801
801
  }
802
- var version = "1.2.15";
802
+ var version = "1.2.17";
803
803
  var anonymousIdKey = "schematicId";
804
804
  var Schematic = class {
805
805
  additionalHeaders = {};
@@ -876,8 +876,13 @@ var Schematic = class {
876
876
  };
877
877
  if (options?.storage) {
878
878
  this.storage = options.storage;
879
- } else if (typeof localStorage !== "undefined") {
880
- this.storage = localStorage;
879
+ } else {
880
+ try {
881
+ if (typeof localStorage !== "undefined") {
882
+ this.storage = localStorage;
883
+ }
884
+ } catch {
885
+ }
881
886
  }
882
887
  if (options?.apiUrl !== void 0) {
883
888
  this.apiUrl = options.apiUrl;
@@ -1008,8 +1013,8 @@ var Schematic = class {
1008
1013
  /**
1009
1014
  * Get value for a single flag.
1010
1015
  * In WebSocket mode, returns cached values if connection is active, otherwise establishes
1011
- * new connection and then returns the requestedvalue. Falls back to REST API if WebSocket
1012
- * connection fails.
1016
+ * new connection and then returns the requested value. Falls back to preconfigured fallback
1017
+ * values if WebSocket connection fails.
1013
1018
  * In REST mode, makes an API call for each check.
1014
1019
  */
1015
1020
  async checkFlag(options) {
@@ -1078,10 +1083,17 @@ var Schematic = class {
1078
1083
  await this.setContext(context);
1079
1084
  } catch (error) {
1080
1085
  console.warn(
1081
- "WebSocket connection failed, falling back to REST:",
1086
+ "WebSocket connection failed, using fallback value:",
1082
1087
  error
1083
1088
  );
1084
- return this.fallbackToRest(key, context, fallback);
1089
+ const errorResult = this.resolveFallbackCheckFlagReturn(
1090
+ key,
1091
+ fallback,
1092
+ "WebSocket connection failed",
1093
+ error instanceof Error ? error.message : String(error)
1094
+ );
1095
+ this.submitFlagCheckEvent(key, errorResult, context);
1096
+ return errorResult.value;
1085
1097
  }
1086
1098
  const contextVals = this.checks[contextStr] ?? {};
1087
1099
  const flagCheck = contextVals[key];
@@ -1184,53 +1196,6 @@ var Schematic = class {
1184
1196
  this.debug(`submitting flag check event:`, eventBody);
1185
1197
  return this.handleEvent("flag_check", EventBodyFlagCheckToJSON(eventBody));
1186
1198
  }
1187
- /**
1188
- * Helper method for falling back to REST API when WebSocket connection fails
1189
- */
1190
- async fallbackToRest(key, context, fallback) {
1191
- if (this.isOffline()) {
1192
- const resolvedFallback = this.resolveFallbackValue(key, fallback);
1193
- this.debug(`fallbackToRest offline result: ${key}`, {
1194
- value: resolvedFallback,
1195
- offlineMode: true
1196
- });
1197
- return resolvedFallback;
1198
- }
1199
- try {
1200
- const requestUrl = `${this.apiUrl}/flags/${key}/check`;
1201
- const response = await fetch(requestUrl, {
1202
- method: "POST",
1203
- headers: {
1204
- ...this.additionalHeaders ?? {},
1205
- "Content-Type": "application/json;charset=UTF-8",
1206
- "X-Schematic-Api-Key": this.apiKey
1207
- },
1208
- body: JSON.stringify(context)
1209
- });
1210
- if (!response.ok) {
1211
- throw new Error("Network response was not ok");
1212
- }
1213
- const responseJson = await response.json();
1214
- const data = CheckFlagResponseFromJSON(responseJson);
1215
- this.debug(`fallbackToRest result: ${key}`, data);
1216
- const result = CheckFlagReturnFromJSON(data.data);
1217
- if (typeof result.featureUsageEvent === "string") {
1218
- this.updateFeatureUsageEventMap(result);
1219
- }
1220
- this.submitFlagCheckEvent(key, result, context);
1221
- return result.value;
1222
- } catch (error) {
1223
- console.warn("REST API call failed, using fallback value:", error);
1224
- const errorResult = this.resolveFallbackCheckFlagReturn(
1225
- key,
1226
- fallback,
1227
- "API request failed (fallback)",
1228
- error instanceof Error ? error.message : String(error)
1229
- );
1230
- this.submitFlagCheckEvent(key, errorResult, context);
1231
- return errorResult.value;
1232
- }
1233
- }
1234
1199
  /**
1235
1200
  * Make an API call to fetch all flag values for a given context.
1236
1201
  * Recommended for use in REST mode only.
@@ -1984,7 +1949,10 @@ var Schematic = class {
1984
1949
  clearTimeout(timeoutId);
1985
1950
  }
1986
1951
  this.debug(`WebSocket connection ${connectionId} error:`, error);
1987
- reject(error);
1952
+ const wrappedError = new Error(
1953
+ "WebSocket connection failed during handshake"
1954
+ );
1955
+ reject(wrappedError);
1988
1956
  };
1989
1957
  webSocket.onclose = () => {
1990
1958
  if (timeoutId !== null) {
@@ -2031,6 +1999,20 @@ var Schematic = class {
2031
1999
  }
2032
2000
  };
2033
2001
  socket.addEventListener("message", messageHandler);
2002
+ socket.addEventListener("close", (event) => {
2003
+ if (!resolved) {
2004
+ resolved = true;
2005
+ if (event.code === 4001) {
2006
+ reject(
2007
+ new Error(
2008
+ `Authentication failed: ${event.reason !== "" ? event.reason : "Invalid API key"}`
2009
+ )
2010
+ );
2011
+ } else {
2012
+ reject(new Error("WebSocket connection closed unexpectedly"));
2013
+ }
2014
+ }
2015
+ });
2034
2016
  this.currentWebSocket = socket;
2035
2017
  const clientVersion = this.additionalHeaders["X-Schematic-Client-Version"] ?? `schematic-js@${version}`;
2036
2018
  const messagePayload = {
@@ -2197,7 +2179,7 @@ var notifyFlagValueListener = (listener, value) => {
2197
2179
  var import_react = __toESM(require("react"));
2198
2180
 
2199
2181
  // src/version.ts
2200
- var version2 = "1.2.15";
2182
+ var version2 = "1.2.17";
2201
2183
 
2202
2184
  // src/context/schematic.tsx
2203
2185
  var import_jsx_runtime = require("react/jsx-runtime");
@@ -754,7 +754,7 @@ function contextString(context) {
754
754
  }, {});
755
755
  return JSON.stringify(sortedContext);
756
756
  }
757
- var version = "1.2.15";
757
+ var version = "1.2.17";
758
758
  var anonymousIdKey = "schematicId";
759
759
  var Schematic = class {
760
760
  additionalHeaders = {};
@@ -831,8 +831,13 @@ var Schematic = class {
831
831
  };
832
832
  if (options?.storage) {
833
833
  this.storage = options.storage;
834
- } else if (typeof localStorage !== "undefined") {
835
- this.storage = localStorage;
834
+ } else {
835
+ try {
836
+ if (typeof localStorage !== "undefined") {
837
+ this.storage = localStorage;
838
+ }
839
+ } catch {
840
+ }
836
841
  }
837
842
  if (options?.apiUrl !== void 0) {
838
843
  this.apiUrl = options.apiUrl;
@@ -963,8 +968,8 @@ var Schematic = class {
963
968
  /**
964
969
  * Get value for a single flag.
965
970
  * In WebSocket mode, returns cached values if connection is active, otherwise establishes
966
- * new connection and then returns the requestedvalue. Falls back to REST API if WebSocket
967
- * connection fails.
971
+ * new connection and then returns the requested value. Falls back to preconfigured fallback
972
+ * values if WebSocket connection fails.
968
973
  * In REST mode, makes an API call for each check.
969
974
  */
970
975
  async checkFlag(options) {
@@ -1033,10 +1038,17 @@ var Schematic = class {
1033
1038
  await this.setContext(context);
1034
1039
  } catch (error) {
1035
1040
  console.warn(
1036
- "WebSocket connection failed, falling back to REST:",
1041
+ "WebSocket connection failed, using fallback value:",
1037
1042
  error
1038
1043
  );
1039
- return this.fallbackToRest(key, context, fallback);
1044
+ const errorResult = this.resolveFallbackCheckFlagReturn(
1045
+ key,
1046
+ fallback,
1047
+ "WebSocket connection failed",
1048
+ error instanceof Error ? error.message : String(error)
1049
+ );
1050
+ this.submitFlagCheckEvent(key, errorResult, context);
1051
+ return errorResult.value;
1040
1052
  }
1041
1053
  const contextVals = this.checks[contextStr] ?? {};
1042
1054
  const flagCheck = contextVals[key];
@@ -1139,53 +1151,6 @@ var Schematic = class {
1139
1151
  this.debug(`submitting flag check event:`, eventBody);
1140
1152
  return this.handleEvent("flag_check", EventBodyFlagCheckToJSON(eventBody));
1141
1153
  }
1142
- /**
1143
- * Helper method for falling back to REST API when WebSocket connection fails
1144
- */
1145
- async fallbackToRest(key, context, fallback) {
1146
- if (this.isOffline()) {
1147
- const resolvedFallback = this.resolveFallbackValue(key, fallback);
1148
- this.debug(`fallbackToRest offline result: ${key}`, {
1149
- value: resolvedFallback,
1150
- offlineMode: true
1151
- });
1152
- return resolvedFallback;
1153
- }
1154
- try {
1155
- const requestUrl = `${this.apiUrl}/flags/${key}/check`;
1156
- const response = await fetch(requestUrl, {
1157
- method: "POST",
1158
- headers: {
1159
- ...this.additionalHeaders ?? {},
1160
- "Content-Type": "application/json;charset=UTF-8",
1161
- "X-Schematic-Api-Key": this.apiKey
1162
- },
1163
- body: JSON.stringify(context)
1164
- });
1165
- if (!response.ok) {
1166
- throw new Error("Network response was not ok");
1167
- }
1168
- const responseJson = await response.json();
1169
- const data = CheckFlagResponseFromJSON(responseJson);
1170
- this.debug(`fallbackToRest result: ${key}`, data);
1171
- const result = CheckFlagReturnFromJSON(data.data);
1172
- if (typeof result.featureUsageEvent === "string") {
1173
- this.updateFeatureUsageEventMap(result);
1174
- }
1175
- this.submitFlagCheckEvent(key, result, context);
1176
- return result.value;
1177
- } catch (error) {
1178
- console.warn("REST API call failed, using fallback value:", error);
1179
- const errorResult = this.resolveFallbackCheckFlagReturn(
1180
- key,
1181
- fallback,
1182
- "API request failed (fallback)",
1183
- error instanceof Error ? error.message : String(error)
1184
- );
1185
- this.submitFlagCheckEvent(key, errorResult, context);
1186
- return errorResult.value;
1187
- }
1188
- }
1189
1154
  /**
1190
1155
  * Make an API call to fetch all flag values for a given context.
1191
1156
  * Recommended for use in REST mode only.
@@ -1939,7 +1904,10 @@ var Schematic = class {
1939
1904
  clearTimeout(timeoutId);
1940
1905
  }
1941
1906
  this.debug(`WebSocket connection ${connectionId} error:`, error);
1942
- reject(error);
1907
+ const wrappedError = new Error(
1908
+ "WebSocket connection failed during handshake"
1909
+ );
1910
+ reject(wrappedError);
1943
1911
  };
1944
1912
  webSocket.onclose = () => {
1945
1913
  if (timeoutId !== null) {
@@ -1986,6 +1954,20 @@ var Schematic = class {
1986
1954
  }
1987
1955
  };
1988
1956
  socket.addEventListener("message", messageHandler);
1957
+ socket.addEventListener("close", (event) => {
1958
+ if (!resolved) {
1959
+ resolved = true;
1960
+ if (event.code === 4001) {
1961
+ reject(
1962
+ new Error(
1963
+ `Authentication failed: ${event.reason !== "" ? event.reason : "Invalid API key"}`
1964
+ )
1965
+ );
1966
+ } else {
1967
+ reject(new Error("WebSocket connection closed unexpectedly"));
1968
+ }
1969
+ }
1970
+ });
1989
1971
  this.currentWebSocket = socket;
1990
1972
  const clientVersion = this.additionalHeaders["X-Schematic-Client-Version"] ?? `schematic-js@${version}`;
1991
1973
  const messagePayload = {
@@ -2152,7 +2134,7 @@ var notifyFlagValueListener = (listener, value) => {
2152
2134
  import React, { createContext, useEffect, useMemo, useRef } from "react";
2153
2135
 
2154
2136
  // src/version.ts
2155
- var version2 = "1.2.15";
2137
+ var version2 = "1.2.17";
2156
2138
 
2157
2139
  // src/context/schematic.tsx
2158
2140
  import { jsx } from "react/jsx-runtime";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@schematichq/schematic-react",
3
- "version": "1.2.15",
3
+ "version": "1.2.17",
4
4
  "main": "dist/schematic-react.cjs.js",
5
5
  "module": "dist/schematic-react.esm.js",
6
6
  "types": "dist/schematic-react.d.ts",
@@ -31,31 +31,31 @@
31
31
  "prepare": "husky"
32
32
  },
33
33
  "dependencies": {
34
- "@schematichq/schematic-js": "^1.2.15"
34
+ "@schematichq/schematic-js": "^1.2.17"
35
35
  },
36
36
  "devDependencies": {
37
- "@eslint/js": "^9.39.2",
38
- "@microsoft/api-extractor": "^7.55.0",
37
+ "@eslint/js": "^10.0.1",
38
+ "@microsoft/api-extractor": "^7.56.3",
39
39
  "@testing-library/dom": "^10.4.1",
40
40
  "@testing-library/jest-dom": "^6.9.1",
41
- "@testing-library/react": "^16.3.0",
42
- "@types/react": "^19.2.6",
43
- "@vitest/browser": "^4.0.8",
44
- "esbuild": "^0.27.0",
41
+ "@testing-library/react": "^16.3.2",
42
+ "@types/react": "^19.2.14",
43
+ "@vitest/browser": "^4.0.18",
44
+ "esbuild": "^0.27.3",
45
45
  "eslint": "^9.39.2",
46
46
  "eslint-plugin-import": "^2.32.0",
47
47
  "eslint-plugin-react": "^7.37.5",
48
48
  "eslint-plugin-react-hooks": "^7.0.1",
49
- "globals": "^16.5.0",
50
- "happy-dom": "^20.0.10",
49
+ "globals": "^17.3.0",
50
+ "happy-dom": "^20.6.1",
51
51
  "husky": "^9.1.7",
52
- "jsdom": "^27.2.0",
53
- "prettier": "^3.7.4",
54
- "react": "^19.2.0",
55
- "react-dom": "^19.2.0",
52
+ "jsdom": "^28.0.0",
53
+ "prettier": "^3.8.1",
54
+ "react": "^19.2.4",
55
+ "react-dom": "^19.2.4",
56
56
  "typescript": "^5.9.3",
57
- "typescript-eslint": "^8.47.0",
58
- "vitest": "^4.0.8"
57
+ "typescript-eslint": "^8.55.0",
58
+ "vitest": "^4.0.18"
59
59
  },
60
60
  "peerDependencies": {
61
61
  "react": ">=18"