@schematichq/schematic-react 1.2.16 → 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.16";
802
+ var version = "1.2.17";
803
803
  var anonymousIdKey = "schematicId";
804
804
  var Schematic = class {
805
805
  additionalHeaders = {};
@@ -1013,8 +1013,8 @@ var Schematic = class {
1013
1013
  /**
1014
1014
  * Get value for a single flag.
1015
1015
  * In WebSocket mode, returns cached values if connection is active, otherwise establishes
1016
- * new connection and then returns the requestedvalue. Falls back to REST API if WebSocket
1017
- * connection fails.
1016
+ * new connection and then returns the requested value. Falls back to preconfigured fallback
1017
+ * values if WebSocket connection fails.
1018
1018
  * In REST mode, makes an API call for each check.
1019
1019
  */
1020
1020
  async checkFlag(options) {
@@ -1083,10 +1083,17 @@ var Schematic = class {
1083
1083
  await this.setContext(context);
1084
1084
  } catch (error) {
1085
1085
  console.warn(
1086
- "WebSocket connection failed, falling back to REST:",
1086
+ "WebSocket connection failed, using fallback value:",
1087
1087
  error
1088
1088
  );
1089
- 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;
1090
1097
  }
1091
1098
  const contextVals = this.checks[contextStr] ?? {};
1092
1099
  const flagCheck = contextVals[key];
@@ -1189,53 +1196,6 @@ var Schematic = class {
1189
1196
  this.debug(`submitting flag check event:`, eventBody);
1190
1197
  return this.handleEvent("flag_check", EventBodyFlagCheckToJSON(eventBody));
1191
1198
  }
1192
- /**
1193
- * Helper method for falling back to REST API when WebSocket connection fails
1194
- */
1195
- async fallbackToRest(key, context, fallback) {
1196
- if (this.isOffline()) {
1197
- const resolvedFallback = this.resolveFallbackValue(key, fallback);
1198
- this.debug(`fallbackToRest offline result: ${key}`, {
1199
- value: resolvedFallback,
1200
- offlineMode: true
1201
- });
1202
- return resolvedFallback;
1203
- }
1204
- try {
1205
- const requestUrl = `${this.apiUrl}/flags/${key}/check`;
1206
- const response = await fetch(requestUrl, {
1207
- method: "POST",
1208
- headers: {
1209
- ...this.additionalHeaders ?? {},
1210
- "Content-Type": "application/json;charset=UTF-8",
1211
- "X-Schematic-Api-Key": this.apiKey
1212
- },
1213
- body: JSON.stringify(context)
1214
- });
1215
- if (!response.ok) {
1216
- throw new Error("Network response was not ok");
1217
- }
1218
- const responseJson = await response.json();
1219
- const data = CheckFlagResponseFromJSON(responseJson);
1220
- this.debug(`fallbackToRest result: ${key}`, data);
1221
- const result = CheckFlagReturnFromJSON(data.data);
1222
- if (typeof result.featureUsageEvent === "string") {
1223
- this.updateFeatureUsageEventMap(result);
1224
- }
1225
- this.submitFlagCheckEvent(key, result, context);
1226
- return result.value;
1227
- } catch (error) {
1228
- console.warn("REST API call failed, using fallback value:", error);
1229
- const errorResult = this.resolveFallbackCheckFlagReturn(
1230
- key,
1231
- fallback,
1232
- "API request failed (fallback)",
1233
- error instanceof Error ? error.message : String(error)
1234
- );
1235
- this.submitFlagCheckEvent(key, errorResult, context);
1236
- return errorResult.value;
1237
- }
1238
- }
1239
1199
  /**
1240
1200
  * Make an API call to fetch all flag values for a given context.
1241
1201
  * Recommended for use in REST mode only.
@@ -1989,7 +1949,10 @@ var Schematic = class {
1989
1949
  clearTimeout(timeoutId);
1990
1950
  }
1991
1951
  this.debug(`WebSocket connection ${connectionId} error:`, error);
1992
- reject(error);
1952
+ const wrappedError = new Error(
1953
+ "WebSocket connection failed during handshake"
1954
+ );
1955
+ reject(wrappedError);
1993
1956
  };
1994
1957
  webSocket.onclose = () => {
1995
1958
  if (timeoutId !== null) {
@@ -2036,6 +1999,20 @@ var Schematic = class {
2036
1999
  }
2037
2000
  };
2038
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
+ });
2039
2016
  this.currentWebSocket = socket;
2040
2017
  const clientVersion = this.additionalHeaders["X-Schematic-Client-Version"] ?? `schematic-js@${version}`;
2041
2018
  const messagePayload = {
@@ -2202,7 +2179,7 @@ var notifyFlagValueListener = (listener, value) => {
2202
2179
  var import_react = __toESM(require("react"));
2203
2180
 
2204
2181
  // src/version.ts
2205
- var version2 = "1.2.16";
2182
+ var version2 = "1.2.17";
2206
2183
 
2207
2184
  // src/context/schematic.tsx
2208
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.16";
757
+ var version = "1.2.17";
758
758
  var anonymousIdKey = "schematicId";
759
759
  var Schematic = class {
760
760
  additionalHeaders = {};
@@ -968,8 +968,8 @@ var Schematic = class {
968
968
  /**
969
969
  * Get value for a single flag.
970
970
  * In WebSocket mode, returns cached values if connection is active, otherwise establishes
971
- * new connection and then returns the requestedvalue. Falls back to REST API if WebSocket
972
- * connection fails.
971
+ * new connection and then returns the requested value. Falls back to preconfigured fallback
972
+ * values if WebSocket connection fails.
973
973
  * In REST mode, makes an API call for each check.
974
974
  */
975
975
  async checkFlag(options) {
@@ -1038,10 +1038,17 @@ var Schematic = class {
1038
1038
  await this.setContext(context);
1039
1039
  } catch (error) {
1040
1040
  console.warn(
1041
- "WebSocket connection failed, falling back to REST:",
1041
+ "WebSocket connection failed, using fallback value:",
1042
1042
  error
1043
1043
  );
1044
- 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;
1045
1052
  }
1046
1053
  const contextVals = this.checks[contextStr] ?? {};
1047
1054
  const flagCheck = contextVals[key];
@@ -1144,53 +1151,6 @@ var Schematic = class {
1144
1151
  this.debug(`submitting flag check event:`, eventBody);
1145
1152
  return this.handleEvent("flag_check", EventBodyFlagCheckToJSON(eventBody));
1146
1153
  }
1147
- /**
1148
- * Helper method for falling back to REST API when WebSocket connection fails
1149
- */
1150
- async fallbackToRest(key, context, fallback) {
1151
- if (this.isOffline()) {
1152
- const resolvedFallback = this.resolveFallbackValue(key, fallback);
1153
- this.debug(`fallbackToRest offline result: ${key}`, {
1154
- value: resolvedFallback,
1155
- offlineMode: true
1156
- });
1157
- return resolvedFallback;
1158
- }
1159
- try {
1160
- const requestUrl = `${this.apiUrl}/flags/${key}/check`;
1161
- const response = await fetch(requestUrl, {
1162
- method: "POST",
1163
- headers: {
1164
- ...this.additionalHeaders ?? {},
1165
- "Content-Type": "application/json;charset=UTF-8",
1166
- "X-Schematic-Api-Key": this.apiKey
1167
- },
1168
- body: JSON.stringify(context)
1169
- });
1170
- if (!response.ok) {
1171
- throw new Error("Network response was not ok");
1172
- }
1173
- const responseJson = await response.json();
1174
- const data = CheckFlagResponseFromJSON(responseJson);
1175
- this.debug(`fallbackToRest result: ${key}`, data);
1176
- const result = CheckFlagReturnFromJSON(data.data);
1177
- if (typeof result.featureUsageEvent === "string") {
1178
- this.updateFeatureUsageEventMap(result);
1179
- }
1180
- this.submitFlagCheckEvent(key, result, context);
1181
- return result.value;
1182
- } catch (error) {
1183
- console.warn("REST API call failed, using fallback value:", error);
1184
- const errorResult = this.resolveFallbackCheckFlagReturn(
1185
- key,
1186
- fallback,
1187
- "API request failed (fallback)",
1188
- error instanceof Error ? error.message : String(error)
1189
- );
1190
- this.submitFlagCheckEvent(key, errorResult, context);
1191
- return errorResult.value;
1192
- }
1193
- }
1194
1154
  /**
1195
1155
  * Make an API call to fetch all flag values for a given context.
1196
1156
  * Recommended for use in REST mode only.
@@ -1944,7 +1904,10 @@ var Schematic = class {
1944
1904
  clearTimeout(timeoutId);
1945
1905
  }
1946
1906
  this.debug(`WebSocket connection ${connectionId} error:`, error);
1947
- reject(error);
1907
+ const wrappedError = new Error(
1908
+ "WebSocket connection failed during handshake"
1909
+ );
1910
+ reject(wrappedError);
1948
1911
  };
1949
1912
  webSocket.onclose = () => {
1950
1913
  if (timeoutId !== null) {
@@ -1991,6 +1954,20 @@ var Schematic = class {
1991
1954
  }
1992
1955
  };
1993
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
+ });
1994
1971
  this.currentWebSocket = socket;
1995
1972
  const clientVersion = this.additionalHeaders["X-Schematic-Client-Version"] ?? `schematic-js@${version}`;
1996
1973
  const messagePayload = {
@@ -2157,7 +2134,7 @@ var notifyFlagValueListener = (listener, value) => {
2157
2134
  import React, { createContext, useEffect, useMemo, useRef } from "react";
2158
2135
 
2159
2136
  // src/version.ts
2160
- var version2 = "1.2.16";
2137
+ var version2 = "1.2.17";
2161
2138
 
2162
2139
  // src/context/schematic.tsx
2163
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.16",
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.16"
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.1",
42
- "@types/react": "^19.2.7",
43
- "@vitest/browser": "^4.0.8",
44
- "esbuild": "^0.27.1",
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.3",
55
- "react-dom": "^19.2.3",
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"