@schematichq/schematic-react 1.2.10 → 1.2.13
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 +37 -0
- package/dist/schematic-react.cjs.js +181 -21
- package/dist/schematic-react.d.ts +1 -1
- package/dist/schematic-react.esm.js +181 -21
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -136,6 +136,43 @@ 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
|
+
## React Native
|
|
140
|
+
|
|
141
|
+
### Handling app background/foreground
|
|
142
|
+
|
|
143
|
+
When a React Native app is backgrounded for an extended period, the WebSocket connection may be closed by the OS. When the app returns to the foreground, the connection will automatically reconnect, but this happens on an exponential backoff timer which may cause a delay before fresh flag values are available.
|
|
144
|
+
|
|
145
|
+
For cases where you need immediate flag updates when returning to the foreground (e.g., after an in-app purchase), you can use one of these methods to re-establish the connection:
|
|
146
|
+
|
|
147
|
+
- `forceReconnect()`: Always closes and re-establishes the WebSocket connection, guaranteeing fresh values
|
|
148
|
+
- `reconnectIfNeeded()`: Only reconnects if the current connection is unhealthy (more efficient for frequent foreground events)
|
|
149
|
+
|
|
150
|
+
```tsx
|
|
151
|
+
import { useSchematic } from "@schematichq/schematic-react";
|
|
152
|
+
import { useEffect } from "react";
|
|
153
|
+
import { AppState } from "react-native";
|
|
154
|
+
|
|
155
|
+
const SchematicAppStateHandler = () => {
|
|
156
|
+
const { client } = useSchematic();
|
|
157
|
+
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
const subscription = AppState.addEventListener("change", (state) => {
|
|
160
|
+
if (state === "active") {
|
|
161
|
+
// Use forceReconnect() for guaranteed fresh values
|
|
162
|
+
client.forceReconnect();
|
|
163
|
+
// Or use reconnectIfNeeded() to skip if connection is healthy
|
|
164
|
+
// client.reconnectIfNeeded();
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
return () => subscription.remove();
|
|
168
|
+
}, [client]);
|
|
169
|
+
|
|
170
|
+
return null;
|
|
171
|
+
};
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Render this inside your `SchematicProvider`.
|
|
175
|
+
|
|
139
176
|
## Troubleshooting
|
|
140
177
|
|
|
141
178
|
For debugging and development, Schematic supports two special modes:
|
|
@@ -799,7 +799,7 @@ function contextString(context) {
|
|
|
799
799
|
}, {});
|
|
800
800
|
return JSON.stringify(sortedContext);
|
|
801
801
|
}
|
|
802
|
-
var version = "1.2.
|
|
802
|
+
var version = "1.2.13";
|
|
803
803
|
var anonymousIdKey = "schematicId";
|
|
804
804
|
var Schematic = class {
|
|
805
805
|
additionalHeaders = {};
|
|
@@ -824,6 +824,9 @@ var Schematic = class {
|
|
|
824
824
|
webSocketConnectionTimeout = 1e4;
|
|
825
825
|
webSocketReconnect = true;
|
|
826
826
|
webSocketMaxReconnectAttempts = 7;
|
|
827
|
+
// Max attempts after connection disrupted
|
|
828
|
+
webSocketMaxConnectionAttempts = 3;
|
|
829
|
+
// Max attempts for initial connection
|
|
827
830
|
webSocketInitialRetryDelay = 1e3;
|
|
828
831
|
webSocketMaxRetryDelay = 3e4;
|
|
829
832
|
wsReconnectAttempts = 0;
|
|
@@ -1047,7 +1050,7 @@ var Schematic = class {
|
|
|
1047
1050
|
this.submitFlagCheckEvent(key, result, context);
|
|
1048
1051
|
return result.value;
|
|
1049
1052
|
}).catch((error) => {
|
|
1050
|
-
console.
|
|
1053
|
+
console.warn("There was a problem with the fetch operation:", error);
|
|
1051
1054
|
const errorResult = this.resolveFallbackCheckFlagReturn(
|
|
1052
1055
|
key,
|
|
1053
1056
|
fallback,
|
|
@@ -1070,10 +1073,7 @@ var Schematic = class {
|
|
|
1070
1073
|
try {
|
|
1071
1074
|
await this.setContext(context);
|
|
1072
1075
|
} catch (error) {
|
|
1073
|
-
console.error
|
|
1074
|
-
"WebSocket connection failed, falling back to REST:",
|
|
1075
|
-
error
|
|
1076
|
-
);
|
|
1076
|
+
console.warn("WebSocket connection failed, falling back to REST:", error);
|
|
1077
1077
|
return this.fallbackToRest(key, context, fallback);
|
|
1078
1078
|
}
|
|
1079
1079
|
const contextVals = this.checks[contextStr] ?? {};
|
|
@@ -1213,7 +1213,7 @@ var Schematic = class {
|
|
|
1213
1213
|
this.submitFlagCheckEvent(key, result, context);
|
|
1214
1214
|
return result.value;
|
|
1215
1215
|
} catch (error) {
|
|
1216
|
-
console.
|
|
1216
|
+
console.warn("REST API call failed, using fallback value:", error);
|
|
1217
1217
|
const errorResult = this.resolveFallbackCheckFlagReturn(
|
|
1218
1218
|
key,
|
|
1219
1219
|
fallback,
|
|
@@ -1262,7 +1262,7 @@ var Schematic = class {
|
|
|
1262
1262
|
{}
|
|
1263
1263
|
);
|
|
1264
1264
|
}).catch((error) => {
|
|
1265
|
-
console.
|
|
1265
|
+
console.warn("There was a problem with the fetch operation:", error);
|
|
1266
1266
|
return {};
|
|
1267
1267
|
});
|
|
1268
1268
|
};
|
|
@@ -1279,7 +1279,7 @@ var Schematic = class {
|
|
|
1279
1279
|
user: body.keys
|
|
1280
1280
|
});
|
|
1281
1281
|
} catch (error) {
|
|
1282
|
-
console.
|
|
1282
|
+
console.warn("Error setting context:", error);
|
|
1283
1283
|
}
|
|
1284
1284
|
return this.handleEvent("identify", body);
|
|
1285
1285
|
};
|
|
@@ -1337,7 +1337,7 @@ var Schematic = class {
|
|
|
1337
1337
|
const socket = await this.conn;
|
|
1338
1338
|
await this.wsSendMessage(socket, context);
|
|
1339
1339
|
} catch (error) {
|
|
1340
|
-
console.
|
|
1340
|
+
console.warn("Failed to establish WebSocket connection:", error);
|
|
1341
1341
|
throw error;
|
|
1342
1342
|
}
|
|
1343
1343
|
};
|
|
@@ -1610,6 +1610,130 @@ var Schematic = class {
|
|
|
1610
1610
|
/**
|
|
1611
1611
|
* Websocket management
|
|
1612
1612
|
*/
|
|
1613
|
+
/**
|
|
1614
|
+
* Force an immediate WebSocket reconnection.
|
|
1615
|
+
* This is useful when the application returns from a background state (e.g., mobile app
|
|
1616
|
+
* coming back to foreground) and wants to immediately re-establish the connection
|
|
1617
|
+
* rather than waiting for the exponential backoff timer.
|
|
1618
|
+
*
|
|
1619
|
+
* This method will:
|
|
1620
|
+
* - Cancel any pending reconnection timer
|
|
1621
|
+
* - Reset the reconnection attempt counter
|
|
1622
|
+
* - Close any existing connection
|
|
1623
|
+
* - Immediately attempt to reconnect
|
|
1624
|
+
* - Re-send the current context to get fresh flag values
|
|
1625
|
+
*
|
|
1626
|
+
* Use this when you need guaranteed fresh values (e.g., after an in-app purchase).
|
|
1627
|
+
*
|
|
1628
|
+
* @example
|
|
1629
|
+
* ```typescript
|
|
1630
|
+
* // React Native example: reconnect when app comes to foreground
|
|
1631
|
+
* useEffect(() => {
|
|
1632
|
+
* const subscription = AppState.addEventListener("change", (state) => {
|
|
1633
|
+
* if (state === "active") {
|
|
1634
|
+
* client.forceReconnect();
|
|
1635
|
+
* }
|
|
1636
|
+
* });
|
|
1637
|
+
* return () => subscription.remove();
|
|
1638
|
+
* }, [client]);
|
|
1639
|
+
* ```
|
|
1640
|
+
*/
|
|
1641
|
+
forceReconnect = async () => {
|
|
1642
|
+
return this.reconnect({ force: true });
|
|
1643
|
+
};
|
|
1644
|
+
/**
|
|
1645
|
+
* Reconnect the WebSocket connection only if the current connection is unhealthy.
|
|
1646
|
+
* This is useful when the application returns from a background state and wants to
|
|
1647
|
+
* ensure a healthy connection exists, but doesn't need to force a reconnection if
|
|
1648
|
+
* the connection is still active.
|
|
1649
|
+
*
|
|
1650
|
+
* This method will:
|
|
1651
|
+
* - Check if an existing connection is healthy (readyState === OPEN)
|
|
1652
|
+
* - If healthy, return immediately without reconnecting
|
|
1653
|
+
* - If unhealthy, perform the same reconnection logic as forceReconnect()
|
|
1654
|
+
*
|
|
1655
|
+
* Use this when you want efficient reconnection that avoids unnecessary disconnects.
|
|
1656
|
+
*
|
|
1657
|
+
* @example
|
|
1658
|
+
* ```typescript
|
|
1659
|
+
* // React Native example: reconnect only if needed when app comes to foreground
|
|
1660
|
+
* useEffect(() => {
|
|
1661
|
+
* const subscription = AppState.addEventListener("change", (state) => {
|
|
1662
|
+
* if (state === "active") {
|
|
1663
|
+
* client.reconnectIfNeeded();
|
|
1664
|
+
* }
|
|
1665
|
+
* });
|
|
1666
|
+
* return () => subscription.remove();
|
|
1667
|
+
* }, [client]);
|
|
1668
|
+
* ```
|
|
1669
|
+
*/
|
|
1670
|
+
reconnectIfNeeded = async () => {
|
|
1671
|
+
return this.reconnect({ force: false });
|
|
1672
|
+
};
|
|
1673
|
+
/**
|
|
1674
|
+
* Internal method to handle reconnection logic for both forceReconnect and reconnectIfNeeded.
|
|
1675
|
+
*/
|
|
1676
|
+
reconnect = async (options) => {
|
|
1677
|
+
const { force } = options;
|
|
1678
|
+
const methodName = force ? "forceReconnect" : "reconnectIfNeeded";
|
|
1679
|
+
if (this.isOffline()) {
|
|
1680
|
+
this.debug(`${methodName}: skipped (offline mode)`);
|
|
1681
|
+
return Promise.resolve();
|
|
1682
|
+
}
|
|
1683
|
+
if (!force && this.conn !== null) {
|
|
1684
|
+
try {
|
|
1685
|
+
const existingSocket = await this.conn;
|
|
1686
|
+
if (existingSocket.readyState === WebSocket.OPEN) {
|
|
1687
|
+
this.debug(`${methodName}: connection is healthy, skipping`);
|
|
1688
|
+
return Promise.resolve();
|
|
1689
|
+
}
|
|
1690
|
+
} catch {
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
this.debug(
|
|
1694
|
+
`${methodName}: ${force ? "forcing immediate reconnection" : "reconnecting"}`
|
|
1695
|
+
);
|
|
1696
|
+
this.wsIntentionalDisconnect = false;
|
|
1697
|
+
if (this.wsReconnectTimer !== null) {
|
|
1698
|
+
this.debug(`${methodName}: cancelling pending reconnection timer`);
|
|
1699
|
+
clearTimeout(this.wsReconnectTimer);
|
|
1700
|
+
this.wsReconnectTimer = null;
|
|
1701
|
+
}
|
|
1702
|
+
this.wsReconnectAttempts = 0;
|
|
1703
|
+
if (this.conn !== null) {
|
|
1704
|
+
this.debug(`${methodName}: closing existing connection`);
|
|
1705
|
+
try {
|
|
1706
|
+
const existingSocket = await this.conn;
|
|
1707
|
+
if (this.currentWebSocket === existingSocket) {
|
|
1708
|
+
this.currentWebSocket = null;
|
|
1709
|
+
}
|
|
1710
|
+
if (existingSocket.readyState === WebSocket.OPEN || existingSocket.readyState === WebSocket.CONNECTING) {
|
|
1711
|
+
existingSocket.close();
|
|
1712
|
+
}
|
|
1713
|
+
} catch (error) {
|
|
1714
|
+
this.debug(`${methodName}: error closing existing connection:`, error);
|
|
1715
|
+
}
|
|
1716
|
+
this.conn = null;
|
|
1717
|
+
this.isConnecting = false;
|
|
1718
|
+
}
|
|
1719
|
+
if (this.context.company !== void 0 || this.context.user !== void 0) {
|
|
1720
|
+
this.debug(`${methodName}: reconnecting with existing context`);
|
|
1721
|
+
try {
|
|
1722
|
+
this.isConnecting = true;
|
|
1723
|
+
this.conn = this.wsConnect();
|
|
1724
|
+
const socket = await this.conn;
|
|
1725
|
+
this.isConnecting = false;
|
|
1726
|
+
await this.wsSendMessage(socket, this.context, true);
|
|
1727
|
+
this.debug(`${methodName}: reconnection successful`);
|
|
1728
|
+
} catch (error) {
|
|
1729
|
+
this.isConnecting = false;
|
|
1730
|
+
this.debug(`${methodName}: reconnection failed:`, error);
|
|
1731
|
+
}
|
|
1732
|
+
} else {
|
|
1733
|
+
this.debug(`${methodName}: no context set, skipping reconnection`);
|
|
1734
|
+
}
|
|
1735
|
+
return Promise.resolve();
|
|
1736
|
+
};
|
|
1613
1737
|
/**
|
|
1614
1738
|
* If using websocket mode, close the connection when done.
|
|
1615
1739
|
* In offline mode, this is a no-op.
|
|
@@ -1634,7 +1758,7 @@ var Schematic = class {
|
|
|
1634
1758
|
}
|
|
1635
1759
|
socket.close();
|
|
1636
1760
|
} catch (error) {
|
|
1637
|
-
console.
|
|
1761
|
+
console.warn("Error during cleanup:", error);
|
|
1638
1762
|
} finally {
|
|
1639
1763
|
this.conn = null;
|
|
1640
1764
|
this.currentWebSocket = null;
|
|
@@ -1777,14 +1901,49 @@ var Schematic = class {
|
|
|
1777
1901
|
}
|
|
1778
1902
|
}, delay);
|
|
1779
1903
|
};
|
|
1780
|
-
// Open a websocket connection
|
|
1781
|
-
wsConnect = () => {
|
|
1904
|
+
// Open a websocket connection with retry logic for timeouts
|
|
1905
|
+
wsConnect = async () => {
|
|
1782
1906
|
if (this.isOffline()) {
|
|
1783
1907
|
this.debug("wsConnect: skipped (offline mode)");
|
|
1784
|
-
|
|
1785
|
-
new Error("WebSocket connection skipped in offline mode")
|
|
1786
|
-
);
|
|
1908
|
+
throw new Error("WebSocket connection skipped in offline mode");
|
|
1787
1909
|
}
|
|
1910
|
+
let lastError = null;
|
|
1911
|
+
const maxAttempts = this.webSocketMaxConnectionAttempts;
|
|
1912
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
1913
|
+
try {
|
|
1914
|
+
const socket = await this.wsConnectOnce();
|
|
1915
|
+
this.wsReconnectAttempts = 0;
|
|
1916
|
+
return socket;
|
|
1917
|
+
} catch (error) {
|
|
1918
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
1919
|
+
const isTimeout = lastError.message === "WebSocket connection timeout";
|
|
1920
|
+
if (!isTimeout) {
|
|
1921
|
+
this.debug(
|
|
1922
|
+
`WebSocket connection failed with non-timeout error, not retrying:`,
|
|
1923
|
+
lastError.message
|
|
1924
|
+
);
|
|
1925
|
+
throw lastError;
|
|
1926
|
+
}
|
|
1927
|
+
if (attempt < maxAttempts - 1) {
|
|
1928
|
+
const baseDelay = this.webSocketInitialRetryDelay * Math.pow(2, attempt);
|
|
1929
|
+
const cappedDelay = Math.min(baseDelay, this.webSocketMaxRetryDelay);
|
|
1930
|
+
const jitter = cappedDelay * 0.2 * Math.random();
|
|
1931
|
+
const delay = cappedDelay + jitter;
|
|
1932
|
+
this.debug(
|
|
1933
|
+
`WebSocket connection timeout (attempt ${attempt + 1}/${maxAttempts}), retrying in ${delay.toFixed(0)}ms`
|
|
1934
|
+
);
|
|
1935
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1936
|
+
} else {
|
|
1937
|
+
this.debug(
|
|
1938
|
+
`WebSocket connection timeout (attempt ${attempt + 1}/${maxAttempts}), no more retries`
|
|
1939
|
+
);
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
throw lastError ?? new Error("WebSocket connection failed");
|
|
1944
|
+
};
|
|
1945
|
+
// Single attempt to open a websocket connection (no retry logic)
|
|
1946
|
+
wsConnectOnce = () => {
|
|
1788
1947
|
return new Promise((resolve, reject) => {
|
|
1789
1948
|
const wsUrl = `${this.webSocketUrl}/flags/bootstrap?apiKey=${this.apiKey}`;
|
|
1790
1949
|
this.debug(`connecting to WebSocket:`, wsUrl);
|
|
@@ -1795,6 +1954,7 @@ var Schematic = class {
|
|
|
1795
1954
|
let isResolved = false;
|
|
1796
1955
|
timeoutId = setTimeout(() => {
|
|
1797
1956
|
if (!isResolved) {
|
|
1957
|
+
isResolved = true;
|
|
1798
1958
|
this.debug(
|
|
1799
1959
|
`WebSocket connection timeout after ${this.webSocketConnectionTimeout}ms`
|
|
1800
1960
|
);
|
|
@@ -1803,16 +1963,17 @@ var Schematic = class {
|
|
|
1803
1963
|
}
|
|
1804
1964
|
}, this.webSocketConnectionTimeout);
|
|
1805
1965
|
webSocket.onopen = () => {
|
|
1966
|
+
if (isResolved) return;
|
|
1806
1967
|
isResolved = true;
|
|
1807
1968
|
if (timeoutId !== null) {
|
|
1808
1969
|
clearTimeout(timeoutId);
|
|
1809
1970
|
}
|
|
1810
|
-
this.wsReconnectAttempts = 0;
|
|
1811
1971
|
this.wsIntentionalDisconnect = false;
|
|
1812
1972
|
this.debug(`WebSocket connection ${connectionId} opened successfully`);
|
|
1813
1973
|
resolve(webSocket);
|
|
1814
1974
|
};
|
|
1815
1975
|
webSocket.onerror = (error) => {
|
|
1976
|
+
if (isResolved) return;
|
|
1816
1977
|
isResolved = true;
|
|
1817
1978
|
if (timeoutId !== null) {
|
|
1818
1979
|
clearTimeout(timeoutId);
|
|
@@ -1821,7 +1982,6 @@ var Schematic = class {
|
|
|
1821
1982
|
reject(error);
|
|
1822
1983
|
};
|
|
1823
1984
|
webSocket.onclose = () => {
|
|
1824
|
-
isResolved = true;
|
|
1825
1985
|
if (timeoutId !== null) {
|
|
1826
1986
|
clearTimeout(timeoutId);
|
|
1827
1987
|
}
|
|
@@ -1831,7 +1991,7 @@ var Schematic = class {
|
|
|
1831
1991
|
this.currentWebSocket = null;
|
|
1832
1992
|
this.isConnecting = false;
|
|
1833
1993
|
}
|
|
1834
|
-
if (!this.wsIntentionalDisconnect && this.webSocketReconnect) {
|
|
1994
|
+
if (!isResolved && !this.wsIntentionalDisconnect && this.webSocketReconnect) {
|
|
1835
1995
|
this.attemptReconnect();
|
|
1836
1996
|
}
|
|
1837
1997
|
};
|
|
@@ -2013,7 +2173,7 @@ var notifyFlagValueListener = (listener, value) => {
|
|
|
2013
2173
|
var import_react = __toESM(require("react"));
|
|
2014
2174
|
|
|
2015
2175
|
// src/version.ts
|
|
2016
|
-
var version2 = "1.2.
|
|
2176
|
+
var version2 = "1.2.13";
|
|
2017
2177
|
|
|
2018
2178
|
// src/context/schematic.tsx
|
|
2019
2179
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
@@ -2028,7 +2188,7 @@ var SchematicProvider = ({
|
|
|
2028
2188
|
}) => {
|
|
2029
2189
|
const initialOptsRef = (0, import_react.useRef)({
|
|
2030
2190
|
publishableKey,
|
|
2031
|
-
useWebSocket:
|
|
2191
|
+
useWebSocket: true,
|
|
2032
2192
|
additionalHeaders: {
|
|
2033
2193
|
"X-Schematic-Client-Version": `schematic-react@${version2}`
|
|
2034
2194
|
},
|
|
@@ -15,7 +15,7 @@ import { StoragePersister } from '@schematichq/schematic-js';
|
|
|
15
15
|
import { Traits } from '@schematichq/schematic-js';
|
|
16
16
|
import { UsagePeriod } from '@schematichq/schematic-js';
|
|
17
17
|
|
|
18
|
-
declare type BaseSchematicProviderProps = Omit<SchematicJS.SchematicOptions, "client" | "publishableKey"> & {
|
|
18
|
+
declare type BaseSchematicProviderProps = Omit<SchematicJS.SchematicOptions, "client" | "publishableKey" | "useWebSocket"> & {
|
|
19
19
|
children: React_2.ReactNode;
|
|
20
20
|
};
|
|
21
21
|
|
|
@@ -754,7 +754,7 @@ function contextString(context) {
|
|
|
754
754
|
}, {});
|
|
755
755
|
return JSON.stringify(sortedContext);
|
|
756
756
|
}
|
|
757
|
-
var version = "1.2.
|
|
757
|
+
var version = "1.2.13";
|
|
758
758
|
var anonymousIdKey = "schematicId";
|
|
759
759
|
var Schematic = class {
|
|
760
760
|
additionalHeaders = {};
|
|
@@ -779,6 +779,9 @@ var Schematic = class {
|
|
|
779
779
|
webSocketConnectionTimeout = 1e4;
|
|
780
780
|
webSocketReconnect = true;
|
|
781
781
|
webSocketMaxReconnectAttempts = 7;
|
|
782
|
+
// Max attempts after connection disrupted
|
|
783
|
+
webSocketMaxConnectionAttempts = 3;
|
|
784
|
+
// Max attempts for initial connection
|
|
782
785
|
webSocketInitialRetryDelay = 1e3;
|
|
783
786
|
webSocketMaxRetryDelay = 3e4;
|
|
784
787
|
wsReconnectAttempts = 0;
|
|
@@ -1002,7 +1005,7 @@ var Schematic = class {
|
|
|
1002
1005
|
this.submitFlagCheckEvent(key, result, context);
|
|
1003
1006
|
return result.value;
|
|
1004
1007
|
}).catch((error) => {
|
|
1005
|
-
console.
|
|
1008
|
+
console.warn("There was a problem with the fetch operation:", error);
|
|
1006
1009
|
const errorResult = this.resolveFallbackCheckFlagReturn(
|
|
1007
1010
|
key,
|
|
1008
1011
|
fallback,
|
|
@@ -1025,10 +1028,7 @@ var Schematic = class {
|
|
|
1025
1028
|
try {
|
|
1026
1029
|
await this.setContext(context);
|
|
1027
1030
|
} catch (error) {
|
|
1028
|
-
console.error
|
|
1029
|
-
"WebSocket connection failed, falling back to REST:",
|
|
1030
|
-
error
|
|
1031
|
-
);
|
|
1031
|
+
console.warn("WebSocket connection failed, falling back to REST:", error);
|
|
1032
1032
|
return this.fallbackToRest(key, context, fallback);
|
|
1033
1033
|
}
|
|
1034
1034
|
const contextVals = this.checks[contextStr] ?? {};
|
|
@@ -1168,7 +1168,7 @@ var Schematic = class {
|
|
|
1168
1168
|
this.submitFlagCheckEvent(key, result, context);
|
|
1169
1169
|
return result.value;
|
|
1170
1170
|
} catch (error) {
|
|
1171
|
-
console.
|
|
1171
|
+
console.warn("REST API call failed, using fallback value:", error);
|
|
1172
1172
|
const errorResult = this.resolveFallbackCheckFlagReturn(
|
|
1173
1173
|
key,
|
|
1174
1174
|
fallback,
|
|
@@ -1217,7 +1217,7 @@ var Schematic = class {
|
|
|
1217
1217
|
{}
|
|
1218
1218
|
);
|
|
1219
1219
|
}).catch((error) => {
|
|
1220
|
-
console.
|
|
1220
|
+
console.warn("There was a problem with the fetch operation:", error);
|
|
1221
1221
|
return {};
|
|
1222
1222
|
});
|
|
1223
1223
|
};
|
|
@@ -1234,7 +1234,7 @@ var Schematic = class {
|
|
|
1234
1234
|
user: body.keys
|
|
1235
1235
|
});
|
|
1236
1236
|
} catch (error) {
|
|
1237
|
-
console.
|
|
1237
|
+
console.warn("Error setting context:", error);
|
|
1238
1238
|
}
|
|
1239
1239
|
return this.handleEvent("identify", body);
|
|
1240
1240
|
};
|
|
@@ -1292,7 +1292,7 @@ var Schematic = class {
|
|
|
1292
1292
|
const socket = await this.conn;
|
|
1293
1293
|
await this.wsSendMessage(socket, context);
|
|
1294
1294
|
} catch (error) {
|
|
1295
|
-
console.
|
|
1295
|
+
console.warn("Failed to establish WebSocket connection:", error);
|
|
1296
1296
|
throw error;
|
|
1297
1297
|
}
|
|
1298
1298
|
};
|
|
@@ -1565,6 +1565,130 @@ var Schematic = class {
|
|
|
1565
1565
|
/**
|
|
1566
1566
|
* Websocket management
|
|
1567
1567
|
*/
|
|
1568
|
+
/**
|
|
1569
|
+
* Force an immediate WebSocket reconnection.
|
|
1570
|
+
* This is useful when the application returns from a background state (e.g., mobile app
|
|
1571
|
+
* coming back to foreground) and wants to immediately re-establish the connection
|
|
1572
|
+
* rather than waiting for the exponential backoff timer.
|
|
1573
|
+
*
|
|
1574
|
+
* This method will:
|
|
1575
|
+
* - Cancel any pending reconnection timer
|
|
1576
|
+
* - Reset the reconnection attempt counter
|
|
1577
|
+
* - Close any existing connection
|
|
1578
|
+
* - Immediately attempt to reconnect
|
|
1579
|
+
* - Re-send the current context to get fresh flag values
|
|
1580
|
+
*
|
|
1581
|
+
* Use this when you need guaranteed fresh values (e.g., after an in-app purchase).
|
|
1582
|
+
*
|
|
1583
|
+
* @example
|
|
1584
|
+
* ```typescript
|
|
1585
|
+
* // React Native example: reconnect when app comes to foreground
|
|
1586
|
+
* useEffect(() => {
|
|
1587
|
+
* const subscription = AppState.addEventListener("change", (state) => {
|
|
1588
|
+
* if (state === "active") {
|
|
1589
|
+
* client.forceReconnect();
|
|
1590
|
+
* }
|
|
1591
|
+
* });
|
|
1592
|
+
* return () => subscription.remove();
|
|
1593
|
+
* }, [client]);
|
|
1594
|
+
* ```
|
|
1595
|
+
*/
|
|
1596
|
+
forceReconnect = async () => {
|
|
1597
|
+
return this.reconnect({ force: true });
|
|
1598
|
+
};
|
|
1599
|
+
/**
|
|
1600
|
+
* Reconnect the WebSocket connection only if the current connection is unhealthy.
|
|
1601
|
+
* This is useful when the application returns from a background state and wants to
|
|
1602
|
+
* ensure a healthy connection exists, but doesn't need to force a reconnection if
|
|
1603
|
+
* the connection is still active.
|
|
1604
|
+
*
|
|
1605
|
+
* This method will:
|
|
1606
|
+
* - Check if an existing connection is healthy (readyState === OPEN)
|
|
1607
|
+
* - If healthy, return immediately without reconnecting
|
|
1608
|
+
* - If unhealthy, perform the same reconnection logic as forceReconnect()
|
|
1609
|
+
*
|
|
1610
|
+
* Use this when you want efficient reconnection that avoids unnecessary disconnects.
|
|
1611
|
+
*
|
|
1612
|
+
* @example
|
|
1613
|
+
* ```typescript
|
|
1614
|
+
* // React Native example: reconnect only if needed when app comes to foreground
|
|
1615
|
+
* useEffect(() => {
|
|
1616
|
+
* const subscription = AppState.addEventListener("change", (state) => {
|
|
1617
|
+
* if (state === "active") {
|
|
1618
|
+
* client.reconnectIfNeeded();
|
|
1619
|
+
* }
|
|
1620
|
+
* });
|
|
1621
|
+
* return () => subscription.remove();
|
|
1622
|
+
* }, [client]);
|
|
1623
|
+
* ```
|
|
1624
|
+
*/
|
|
1625
|
+
reconnectIfNeeded = async () => {
|
|
1626
|
+
return this.reconnect({ force: false });
|
|
1627
|
+
};
|
|
1628
|
+
/**
|
|
1629
|
+
* Internal method to handle reconnection logic for both forceReconnect and reconnectIfNeeded.
|
|
1630
|
+
*/
|
|
1631
|
+
reconnect = async (options) => {
|
|
1632
|
+
const { force } = options;
|
|
1633
|
+
const methodName = force ? "forceReconnect" : "reconnectIfNeeded";
|
|
1634
|
+
if (this.isOffline()) {
|
|
1635
|
+
this.debug(`${methodName}: skipped (offline mode)`);
|
|
1636
|
+
return Promise.resolve();
|
|
1637
|
+
}
|
|
1638
|
+
if (!force && this.conn !== null) {
|
|
1639
|
+
try {
|
|
1640
|
+
const existingSocket = await this.conn;
|
|
1641
|
+
if (existingSocket.readyState === WebSocket.OPEN) {
|
|
1642
|
+
this.debug(`${methodName}: connection is healthy, skipping`);
|
|
1643
|
+
return Promise.resolve();
|
|
1644
|
+
}
|
|
1645
|
+
} catch {
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
this.debug(
|
|
1649
|
+
`${methodName}: ${force ? "forcing immediate reconnection" : "reconnecting"}`
|
|
1650
|
+
);
|
|
1651
|
+
this.wsIntentionalDisconnect = false;
|
|
1652
|
+
if (this.wsReconnectTimer !== null) {
|
|
1653
|
+
this.debug(`${methodName}: cancelling pending reconnection timer`);
|
|
1654
|
+
clearTimeout(this.wsReconnectTimer);
|
|
1655
|
+
this.wsReconnectTimer = null;
|
|
1656
|
+
}
|
|
1657
|
+
this.wsReconnectAttempts = 0;
|
|
1658
|
+
if (this.conn !== null) {
|
|
1659
|
+
this.debug(`${methodName}: closing existing connection`);
|
|
1660
|
+
try {
|
|
1661
|
+
const existingSocket = await this.conn;
|
|
1662
|
+
if (this.currentWebSocket === existingSocket) {
|
|
1663
|
+
this.currentWebSocket = null;
|
|
1664
|
+
}
|
|
1665
|
+
if (existingSocket.readyState === WebSocket.OPEN || existingSocket.readyState === WebSocket.CONNECTING) {
|
|
1666
|
+
existingSocket.close();
|
|
1667
|
+
}
|
|
1668
|
+
} catch (error) {
|
|
1669
|
+
this.debug(`${methodName}: error closing existing connection:`, error);
|
|
1670
|
+
}
|
|
1671
|
+
this.conn = null;
|
|
1672
|
+
this.isConnecting = false;
|
|
1673
|
+
}
|
|
1674
|
+
if (this.context.company !== void 0 || this.context.user !== void 0) {
|
|
1675
|
+
this.debug(`${methodName}: reconnecting with existing context`);
|
|
1676
|
+
try {
|
|
1677
|
+
this.isConnecting = true;
|
|
1678
|
+
this.conn = this.wsConnect();
|
|
1679
|
+
const socket = await this.conn;
|
|
1680
|
+
this.isConnecting = false;
|
|
1681
|
+
await this.wsSendMessage(socket, this.context, true);
|
|
1682
|
+
this.debug(`${methodName}: reconnection successful`);
|
|
1683
|
+
} catch (error) {
|
|
1684
|
+
this.isConnecting = false;
|
|
1685
|
+
this.debug(`${methodName}: reconnection failed:`, error);
|
|
1686
|
+
}
|
|
1687
|
+
} else {
|
|
1688
|
+
this.debug(`${methodName}: no context set, skipping reconnection`);
|
|
1689
|
+
}
|
|
1690
|
+
return Promise.resolve();
|
|
1691
|
+
};
|
|
1568
1692
|
/**
|
|
1569
1693
|
* If using websocket mode, close the connection when done.
|
|
1570
1694
|
* In offline mode, this is a no-op.
|
|
@@ -1589,7 +1713,7 @@ var Schematic = class {
|
|
|
1589
1713
|
}
|
|
1590
1714
|
socket.close();
|
|
1591
1715
|
} catch (error) {
|
|
1592
|
-
console.
|
|
1716
|
+
console.warn("Error during cleanup:", error);
|
|
1593
1717
|
} finally {
|
|
1594
1718
|
this.conn = null;
|
|
1595
1719
|
this.currentWebSocket = null;
|
|
@@ -1732,14 +1856,49 @@ var Schematic = class {
|
|
|
1732
1856
|
}
|
|
1733
1857
|
}, delay);
|
|
1734
1858
|
};
|
|
1735
|
-
// Open a websocket connection
|
|
1736
|
-
wsConnect = () => {
|
|
1859
|
+
// Open a websocket connection with retry logic for timeouts
|
|
1860
|
+
wsConnect = async () => {
|
|
1737
1861
|
if (this.isOffline()) {
|
|
1738
1862
|
this.debug("wsConnect: skipped (offline mode)");
|
|
1739
|
-
|
|
1740
|
-
new Error("WebSocket connection skipped in offline mode")
|
|
1741
|
-
);
|
|
1863
|
+
throw new Error("WebSocket connection skipped in offline mode");
|
|
1742
1864
|
}
|
|
1865
|
+
let lastError = null;
|
|
1866
|
+
const maxAttempts = this.webSocketMaxConnectionAttempts;
|
|
1867
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
1868
|
+
try {
|
|
1869
|
+
const socket = await this.wsConnectOnce();
|
|
1870
|
+
this.wsReconnectAttempts = 0;
|
|
1871
|
+
return socket;
|
|
1872
|
+
} catch (error) {
|
|
1873
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
1874
|
+
const isTimeout = lastError.message === "WebSocket connection timeout";
|
|
1875
|
+
if (!isTimeout) {
|
|
1876
|
+
this.debug(
|
|
1877
|
+
`WebSocket connection failed with non-timeout error, not retrying:`,
|
|
1878
|
+
lastError.message
|
|
1879
|
+
);
|
|
1880
|
+
throw lastError;
|
|
1881
|
+
}
|
|
1882
|
+
if (attempt < maxAttempts - 1) {
|
|
1883
|
+
const baseDelay = this.webSocketInitialRetryDelay * Math.pow(2, attempt);
|
|
1884
|
+
const cappedDelay = Math.min(baseDelay, this.webSocketMaxRetryDelay);
|
|
1885
|
+
const jitter = cappedDelay * 0.2 * Math.random();
|
|
1886
|
+
const delay = cappedDelay + jitter;
|
|
1887
|
+
this.debug(
|
|
1888
|
+
`WebSocket connection timeout (attempt ${attempt + 1}/${maxAttempts}), retrying in ${delay.toFixed(0)}ms`
|
|
1889
|
+
);
|
|
1890
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1891
|
+
} else {
|
|
1892
|
+
this.debug(
|
|
1893
|
+
`WebSocket connection timeout (attempt ${attempt + 1}/${maxAttempts}), no more retries`
|
|
1894
|
+
);
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
throw lastError ?? new Error("WebSocket connection failed");
|
|
1899
|
+
};
|
|
1900
|
+
// Single attempt to open a websocket connection (no retry logic)
|
|
1901
|
+
wsConnectOnce = () => {
|
|
1743
1902
|
return new Promise((resolve, reject) => {
|
|
1744
1903
|
const wsUrl = `${this.webSocketUrl}/flags/bootstrap?apiKey=${this.apiKey}`;
|
|
1745
1904
|
this.debug(`connecting to WebSocket:`, wsUrl);
|
|
@@ -1750,6 +1909,7 @@ var Schematic = class {
|
|
|
1750
1909
|
let isResolved = false;
|
|
1751
1910
|
timeoutId = setTimeout(() => {
|
|
1752
1911
|
if (!isResolved) {
|
|
1912
|
+
isResolved = true;
|
|
1753
1913
|
this.debug(
|
|
1754
1914
|
`WebSocket connection timeout after ${this.webSocketConnectionTimeout}ms`
|
|
1755
1915
|
);
|
|
@@ -1758,16 +1918,17 @@ var Schematic = class {
|
|
|
1758
1918
|
}
|
|
1759
1919
|
}, this.webSocketConnectionTimeout);
|
|
1760
1920
|
webSocket.onopen = () => {
|
|
1921
|
+
if (isResolved) return;
|
|
1761
1922
|
isResolved = true;
|
|
1762
1923
|
if (timeoutId !== null) {
|
|
1763
1924
|
clearTimeout(timeoutId);
|
|
1764
1925
|
}
|
|
1765
|
-
this.wsReconnectAttempts = 0;
|
|
1766
1926
|
this.wsIntentionalDisconnect = false;
|
|
1767
1927
|
this.debug(`WebSocket connection ${connectionId} opened successfully`);
|
|
1768
1928
|
resolve(webSocket);
|
|
1769
1929
|
};
|
|
1770
1930
|
webSocket.onerror = (error) => {
|
|
1931
|
+
if (isResolved) return;
|
|
1771
1932
|
isResolved = true;
|
|
1772
1933
|
if (timeoutId !== null) {
|
|
1773
1934
|
clearTimeout(timeoutId);
|
|
@@ -1776,7 +1937,6 @@ var Schematic = class {
|
|
|
1776
1937
|
reject(error);
|
|
1777
1938
|
};
|
|
1778
1939
|
webSocket.onclose = () => {
|
|
1779
|
-
isResolved = true;
|
|
1780
1940
|
if (timeoutId !== null) {
|
|
1781
1941
|
clearTimeout(timeoutId);
|
|
1782
1942
|
}
|
|
@@ -1786,7 +1946,7 @@ var Schematic = class {
|
|
|
1786
1946
|
this.currentWebSocket = null;
|
|
1787
1947
|
this.isConnecting = false;
|
|
1788
1948
|
}
|
|
1789
|
-
if (!this.wsIntentionalDisconnect && this.webSocketReconnect) {
|
|
1949
|
+
if (!isResolved && !this.wsIntentionalDisconnect && this.webSocketReconnect) {
|
|
1790
1950
|
this.attemptReconnect();
|
|
1791
1951
|
}
|
|
1792
1952
|
};
|
|
@@ -1968,7 +2128,7 @@ var notifyFlagValueListener = (listener, value) => {
|
|
|
1968
2128
|
import React, { createContext, useEffect, useMemo, useRef } from "react";
|
|
1969
2129
|
|
|
1970
2130
|
// src/version.ts
|
|
1971
|
-
var version2 = "1.2.
|
|
2131
|
+
var version2 = "1.2.13";
|
|
1972
2132
|
|
|
1973
2133
|
// src/context/schematic.tsx
|
|
1974
2134
|
import { jsx } from "react/jsx-runtime";
|
|
@@ -1983,7 +2143,7 @@ var SchematicProvider = ({
|
|
|
1983
2143
|
}) => {
|
|
1984
2144
|
const initialOptsRef = useRef({
|
|
1985
2145
|
publishableKey,
|
|
1986
|
-
useWebSocket:
|
|
2146
|
+
useWebSocket: true,
|
|
1987
2147
|
additionalHeaders: {
|
|
1988
2148
|
"X-Schematic-Client-Version": `schematic-react@${version2}`
|
|
1989
2149
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@schematichq/schematic-react",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.13",
|
|
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,7 +31,7 @@
|
|
|
31
31
|
"prepare": "husky"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@schematichq/schematic-js": "^1.2.
|
|
34
|
+
"@schematichq/schematic-js": "^1.2.13"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@eslint/js": "^9.39.1",
|