@schematichq/schematic-js 1.2.9 → 1.2.11
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/dist/schematic.browser.js +2 -2
- package/dist/schematic.cjs.js +262 -117
- package/dist/schematic.d.ts +36 -2
- package/dist/schematic.esm.js +262 -117
- package/package.json +2 -2
package/dist/schematic.cjs.js
CHANGED
|
@@ -800,7 +800,7 @@ function contextString(context) {
|
|
|
800
800
|
}
|
|
801
801
|
|
|
802
802
|
// src/version.ts
|
|
803
|
-
var version = "1.2.
|
|
803
|
+
var version = "1.2.11";
|
|
804
804
|
|
|
805
805
|
// src/index.ts
|
|
806
806
|
var anonymousIdKey = "schematicId";
|
|
@@ -832,6 +832,8 @@ var Schematic = class {
|
|
|
832
832
|
wsReconnectAttempts = 0;
|
|
833
833
|
wsReconnectTimer = null;
|
|
834
834
|
wsIntentionalDisconnect = false;
|
|
835
|
+
currentWebSocket = null;
|
|
836
|
+
isConnecting = false;
|
|
835
837
|
maxEventQueueSize = 100;
|
|
836
838
|
// Prevent memory issues with very long network outages
|
|
837
839
|
maxEventRetries = 5;
|
|
@@ -1109,6 +1111,49 @@ var Schematic = class {
|
|
|
1109
1111
|
console.log(`[Schematic] ${message}`, ...args);
|
|
1110
1112
|
}
|
|
1111
1113
|
}
|
|
1114
|
+
/**
|
|
1115
|
+
* Create a persistent message handler for websocket flag updates
|
|
1116
|
+
*/
|
|
1117
|
+
createPersistentMessageHandler(context) {
|
|
1118
|
+
return (event) => {
|
|
1119
|
+
const message = JSON.parse(event.data);
|
|
1120
|
+
this.debug(`WebSocket persistent message received:`, message);
|
|
1121
|
+
if (!(contextString(context) in this.checks)) {
|
|
1122
|
+
this.checks[contextString(context)] = {};
|
|
1123
|
+
}
|
|
1124
|
+
(message.flags ?? []).forEach((flag) => {
|
|
1125
|
+
const flagCheck = CheckFlagReturnFromJSON(flag);
|
|
1126
|
+
const contextStr = contextString(context);
|
|
1127
|
+
if (this.checks[contextStr] === void 0) {
|
|
1128
|
+
this.checks[contextStr] = {};
|
|
1129
|
+
}
|
|
1130
|
+
this.checks[contextStr][flagCheck.flag] = flagCheck;
|
|
1131
|
+
this.debug(`WebSocket flag update:`, {
|
|
1132
|
+
flag: flagCheck.flag,
|
|
1133
|
+
value: flagCheck.value,
|
|
1134
|
+
flagCheck
|
|
1135
|
+
});
|
|
1136
|
+
if (typeof flagCheck.featureUsageEvent === "string") {
|
|
1137
|
+
this.updateFeatureUsageEventMap(flagCheck);
|
|
1138
|
+
}
|
|
1139
|
+
if ((this.flagCheckListeners[flag.flag]?.size ?? 0) > 0 || (this.flagValueListeners[flag.flag]?.size ?? 0) > 0) {
|
|
1140
|
+
this.submitFlagCheckEvent(flagCheck.flag, flagCheck, context);
|
|
1141
|
+
}
|
|
1142
|
+
this.debug(`About to notify listeners for flag ${flag.flag}`, {
|
|
1143
|
+
flag: flag.flag,
|
|
1144
|
+
value: flagCheck.value
|
|
1145
|
+
});
|
|
1146
|
+
this.notifyFlagCheckListeners(flag.flag, flagCheck);
|
|
1147
|
+
this.notifyFlagValueListeners(flag.flag, flagCheck.value);
|
|
1148
|
+
this.debug(`Finished notifying listeners for flag ${flag.flag}`, {
|
|
1149
|
+
flag: flag.flag,
|
|
1150
|
+
value: flagCheck.value
|
|
1151
|
+
});
|
|
1152
|
+
});
|
|
1153
|
+
this.flushContextDependentEventQueue();
|
|
1154
|
+
this.setIsPending(false);
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1112
1157
|
/**
|
|
1113
1158
|
* Helper function to check if client is in offline mode
|
|
1114
1159
|
*/
|
|
@@ -1260,6 +1305,19 @@ var Schematic = class {
|
|
|
1260
1305
|
try {
|
|
1261
1306
|
this.setIsPending(true);
|
|
1262
1307
|
if (!this.conn) {
|
|
1308
|
+
if (this.isConnecting) {
|
|
1309
|
+
this.debug(
|
|
1310
|
+
`Connection already in progress, waiting for it to complete`
|
|
1311
|
+
);
|
|
1312
|
+
while (this.isConnecting && this.conn === null) {
|
|
1313
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1314
|
+
}
|
|
1315
|
+
if (this.conn !== null) {
|
|
1316
|
+
const socket2 = await this.conn;
|
|
1317
|
+
await this.wsSendMessage(socket2, context);
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1263
1321
|
if (this.wsReconnectTimer !== null) {
|
|
1264
1322
|
this.debug(
|
|
1265
1323
|
`Cancelling scheduled reconnection, connecting immediately`
|
|
@@ -1267,7 +1325,17 @@ var Schematic = class {
|
|
|
1267
1325
|
clearTimeout(this.wsReconnectTimer);
|
|
1268
1326
|
this.wsReconnectTimer = null;
|
|
1269
1327
|
}
|
|
1270
|
-
this.
|
|
1328
|
+
this.isConnecting = true;
|
|
1329
|
+
try {
|
|
1330
|
+
this.conn = this.wsConnect();
|
|
1331
|
+
const socket2 = await this.conn;
|
|
1332
|
+
this.isConnecting = false;
|
|
1333
|
+
await this.wsSendMessage(socket2, context);
|
|
1334
|
+
return;
|
|
1335
|
+
} catch (error) {
|
|
1336
|
+
this.isConnecting = false;
|
|
1337
|
+
throw error;
|
|
1338
|
+
}
|
|
1271
1339
|
}
|
|
1272
1340
|
const socket = await this.conn;
|
|
1273
1341
|
await this.wsSendMessage(socket, context);
|
|
@@ -1432,10 +1500,14 @@ var Schematic = class {
|
|
|
1432
1500
|
}
|
|
1433
1501
|
}
|
|
1434
1502
|
if (readyEvents.length === 0) {
|
|
1435
|
-
this.debug(
|
|
1503
|
+
this.debug(
|
|
1504
|
+
`No events ready for retry yet (${notReadyEvents.length} still in backoff)`
|
|
1505
|
+
);
|
|
1436
1506
|
return;
|
|
1437
1507
|
}
|
|
1438
|
-
this.debug(
|
|
1508
|
+
this.debug(
|
|
1509
|
+
`Flushing event queue: ${readyEvents.length} ready, ${notReadyEvents.length} waiting`
|
|
1510
|
+
);
|
|
1439
1511
|
this.eventQueue = notReadyEvents;
|
|
1440
1512
|
for (const event of readyEvents) {
|
|
1441
1513
|
try {
|
|
@@ -1500,7 +1572,10 @@ var Schematic = class {
|
|
|
1500
1572
|
} catch (error) {
|
|
1501
1573
|
const retryCount = (event.retry_count ?? 0) + 1;
|
|
1502
1574
|
if (retryCount <= this.maxEventRetries) {
|
|
1503
|
-
this.debug(
|
|
1575
|
+
this.debug(
|
|
1576
|
+
`Event failed to send (attempt ${retryCount}/${this.maxEventRetries}), queueing for retry:`,
|
|
1577
|
+
error
|
|
1578
|
+
);
|
|
1504
1579
|
const baseDelay = this.eventRetryInitialDelay * Math.pow(2, retryCount - 1);
|
|
1505
1580
|
const jitterDelay = Math.min(baseDelay, this.eventRetryMaxDelay);
|
|
1506
1581
|
const nextRetryAt = Date.now() + jitterDelay;
|
|
@@ -1511,15 +1586,22 @@ var Schematic = class {
|
|
|
1511
1586
|
};
|
|
1512
1587
|
if (this.eventQueue.length < this.maxEventQueueSize) {
|
|
1513
1588
|
this.eventQueue.push(retryEvent);
|
|
1514
|
-
this.debug(
|
|
1589
|
+
this.debug(
|
|
1590
|
+
`Event queued for retry in ${jitterDelay}ms (${this.eventQueue.length}/${this.maxEventQueueSize})`
|
|
1591
|
+
);
|
|
1515
1592
|
} else {
|
|
1516
|
-
this.debug(
|
|
1593
|
+
this.debug(
|
|
1594
|
+
`Event queue full (${this.maxEventQueueSize}), dropping oldest event`
|
|
1595
|
+
);
|
|
1517
1596
|
this.eventQueue.shift();
|
|
1518
1597
|
this.eventQueue.push(retryEvent);
|
|
1519
1598
|
}
|
|
1520
1599
|
this.startRetryTimer();
|
|
1521
1600
|
} else {
|
|
1522
|
-
this.debug(
|
|
1601
|
+
this.debug(
|
|
1602
|
+
`Event failed permanently after ${this.maxEventRetries} attempts, dropping:`,
|
|
1603
|
+
error
|
|
1604
|
+
);
|
|
1523
1605
|
}
|
|
1524
1606
|
}
|
|
1525
1607
|
return Promise.resolve();
|
|
@@ -1549,12 +1631,95 @@ var Schematic = class {
|
|
|
1549
1631
|
if (this.conn) {
|
|
1550
1632
|
try {
|
|
1551
1633
|
const socket = await this.conn;
|
|
1634
|
+
if (this.currentWebSocket === socket) {
|
|
1635
|
+
this.debug(`Cleaning up current websocket tracking`);
|
|
1636
|
+
this.currentWebSocket = null;
|
|
1637
|
+
}
|
|
1552
1638
|
socket.close();
|
|
1553
1639
|
} catch (error) {
|
|
1554
1640
|
console.error("Error during cleanup:", error);
|
|
1555
1641
|
} finally {
|
|
1556
1642
|
this.conn = null;
|
|
1643
|
+
this.currentWebSocket = null;
|
|
1644
|
+
this.isConnecting = false;
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
};
|
|
1648
|
+
/**
|
|
1649
|
+
* Force an immediate WebSocket reconnection.
|
|
1650
|
+
* This is useful when the application returns from a background state (e.g., mobile app
|
|
1651
|
+
* coming back to foreground) and wants to immediately re-establish the connection
|
|
1652
|
+
* rather than waiting for the exponential backoff timer.
|
|
1653
|
+
*
|
|
1654
|
+
* This method will:
|
|
1655
|
+
* - Cancel any pending reconnection timer
|
|
1656
|
+
* - Reset the reconnection attempt counter
|
|
1657
|
+
* - Immediately attempt to reconnect
|
|
1658
|
+
* - Re-send the current context to get fresh flag values
|
|
1659
|
+
*
|
|
1660
|
+
* If a connection is already active and healthy, this will close it and create a new one.
|
|
1661
|
+
* If you just want to ensure a connection exists, check the connection state first.
|
|
1662
|
+
*
|
|
1663
|
+
* @example
|
|
1664
|
+
* ```typescript
|
|
1665
|
+
* // React Native example: reconnect when app comes to foreground
|
|
1666
|
+
* useEffect(() => {
|
|
1667
|
+
* const subscription = AppState.addEventListener("change", (state) => {
|
|
1668
|
+
* if (state === "active") {
|
|
1669
|
+
* client.forceReconnect();
|
|
1670
|
+
* }
|
|
1671
|
+
* });
|
|
1672
|
+
* return () => subscription.remove();
|
|
1673
|
+
* }, [client]);
|
|
1674
|
+
* ```
|
|
1675
|
+
*/
|
|
1676
|
+
forceReconnect = async () => {
|
|
1677
|
+
if (this.isOffline()) {
|
|
1678
|
+
this.debug("forceReconnect: skipped (offline mode)");
|
|
1679
|
+
return Promise.resolve();
|
|
1680
|
+
}
|
|
1681
|
+
this.debug("forceReconnect: forcing immediate reconnection");
|
|
1682
|
+
this.wsIntentionalDisconnect = false;
|
|
1683
|
+
if (this.wsReconnectTimer !== null) {
|
|
1684
|
+
this.debug("forceReconnect: cancelling pending reconnection timer");
|
|
1685
|
+
clearTimeout(this.wsReconnectTimer);
|
|
1686
|
+
this.wsReconnectTimer = null;
|
|
1687
|
+
}
|
|
1688
|
+
this.wsReconnectAttempts = 0;
|
|
1689
|
+
if (this.conn !== null) {
|
|
1690
|
+
this.debug("forceReconnect: closing existing connection");
|
|
1691
|
+
try {
|
|
1692
|
+
const existingSocket = await this.conn;
|
|
1693
|
+
if (this.currentWebSocket === existingSocket) {
|
|
1694
|
+
this.currentWebSocket = null;
|
|
1695
|
+
}
|
|
1696
|
+
if (existingSocket.readyState === WebSocket.OPEN || existingSocket.readyState === WebSocket.CONNECTING) {
|
|
1697
|
+
existingSocket.close();
|
|
1698
|
+
}
|
|
1699
|
+
} catch (error) {
|
|
1700
|
+
this.debug("forceReconnect: error closing existing connection:", error);
|
|
1701
|
+
}
|
|
1702
|
+
this.conn = null;
|
|
1703
|
+
this.isConnecting = false;
|
|
1704
|
+
}
|
|
1705
|
+
if (this.context.company !== void 0 || this.context.user !== void 0) {
|
|
1706
|
+
this.debug("forceReconnect: reconnecting with existing context");
|
|
1707
|
+
try {
|
|
1708
|
+
this.isConnecting = true;
|
|
1709
|
+
this.conn = this.wsConnect();
|
|
1710
|
+
const socket = await this.conn;
|
|
1711
|
+
this.isConnecting = false;
|
|
1712
|
+
await this.wsSendMessage(socket, this.context, true);
|
|
1713
|
+
this.debug("forceReconnect: reconnection successful");
|
|
1714
|
+
} catch (error) {
|
|
1715
|
+
this.isConnecting = false;
|
|
1716
|
+
this.debug("forceReconnect: reconnection failed:", error);
|
|
1717
|
+
this.attemptReconnect();
|
|
1557
1718
|
}
|
|
1719
|
+
} else {
|
|
1720
|
+
this.debug(
|
|
1721
|
+
"forceReconnect: no context available, websocket will connect when context is set"
|
|
1722
|
+
);
|
|
1558
1723
|
}
|
|
1559
1724
|
};
|
|
1560
1725
|
/**
|
|
@@ -1578,7 +1743,9 @@ var Schematic = class {
|
|
|
1578
1743
|
if (this.conn !== null) {
|
|
1579
1744
|
try {
|
|
1580
1745
|
const socket = await this.conn;
|
|
1581
|
-
socket.
|
|
1746
|
+
if (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING) {
|
|
1747
|
+
socket.close();
|
|
1748
|
+
}
|
|
1582
1749
|
} catch (error) {
|
|
1583
1750
|
this.debug("Error closing connection on offline:", error);
|
|
1584
1751
|
}
|
|
@@ -1593,7 +1760,9 @@ var Schematic = class {
|
|
|
1593
1760
|
* Handle browser coming back online
|
|
1594
1761
|
*/
|
|
1595
1762
|
handleNetworkOnline = () => {
|
|
1596
|
-
this.debug(
|
|
1763
|
+
this.debug(
|
|
1764
|
+
"Network online, attempting reconnection and flushing queued events"
|
|
1765
|
+
);
|
|
1597
1766
|
this.wsReconnectAttempts = 0;
|
|
1598
1767
|
if (this.wsReconnectTimer !== null) {
|
|
1599
1768
|
clearTimeout(this.wsReconnectTimer);
|
|
@@ -1616,7 +1785,10 @@ var Schematic = class {
|
|
|
1616
1785
|
return;
|
|
1617
1786
|
}
|
|
1618
1787
|
if (this.wsReconnectTimer !== null) {
|
|
1619
|
-
|
|
1788
|
+
this.debug(
|
|
1789
|
+
`Reconnection attempt already scheduled, ignoring duplicate request`
|
|
1790
|
+
);
|
|
1791
|
+
return;
|
|
1620
1792
|
}
|
|
1621
1793
|
const delay = this.calculateReconnectDelay();
|
|
1622
1794
|
this.debug(
|
|
@@ -1629,23 +1801,57 @@ var Schematic = class {
|
|
|
1629
1801
|
`Attempting to reconnect (attempt ${this.wsReconnectAttempts}/${this.webSocketMaxReconnectAttempts})`
|
|
1630
1802
|
);
|
|
1631
1803
|
try {
|
|
1632
|
-
this.conn
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1804
|
+
if (this.conn !== null) {
|
|
1805
|
+
this.debug(`Cleaning up existing connection before reconnection`);
|
|
1806
|
+
try {
|
|
1807
|
+
const existingSocket = await this.conn;
|
|
1808
|
+
if (this.currentWebSocket === existingSocket) {
|
|
1809
|
+
this.debug(`Existing websocket is current, will be replaced`);
|
|
1810
|
+
this.currentWebSocket = null;
|
|
1811
|
+
}
|
|
1812
|
+
if (existingSocket.readyState === WebSocket.OPEN || existingSocket.readyState === WebSocket.CONNECTING) {
|
|
1813
|
+
existingSocket.close();
|
|
1814
|
+
}
|
|
1815
|
+
} catch (error) {
|
|
1816
|
+
this.debug(`Error cleaning up existing connection:`, error);
|
|
1817
|
+
}
|
|
1818
|
+
this.conn = null;
|
|
1819
|
+
this.currentWebSocket = null;
|
|
1820
|
+
this.isConnecting = false;
|
|
1821
|
+
}
|
|
1822
|
+
this.isConnecting = true;
|
|
1823
|
+
try {
|
|
1824
|
+
this.conn = this.wsConnect();
|
|
1825
|
+
const socket = await this.conn;
|
|
1826
|
+
this.isConnecting = false;
|
|
1827
|
+
this.debug(`Reconnection context check:`, {
|
|
1828
|
+
hasCompany: this.context.company !== void 0,
|
|
1829
|
+
hasUser: this.context.user !== void 0,
|
|
1830
|
+
context: this.context
|
|
1831
|
+
});
|
|
1832
|
+
if (this.context.company !== void 0 || this.context.user !== void 0) {
|
|
1833
|
+
this.debug(`Reconnected, force re-sending context`);
|
|
1834
|
+
await this.wsSendMessage(socket, this.context, true);
|
|
1835
|
+
} else {
|
|
1836
|
+
this.debug(
|
|
1837
|
+
`No context to re-send after reconnection - websocket ready for new context`
|
|
1838
|
+
);
|
|
1839
|
+
this.debug(
|
|
1840
|
+
`Setting up tracking for reconnected websocket (no context to send)`
|
|
1841
|
+
);
|
|
1842
|
+
this.currentWebSocket = socket;
|
|
1843
|
+
}
|
|
1844
|
+
this.flushEventQueue().catch((error) => {
|
|
1845
|
+
this.debug(
|
|
1846
|
+
"Error flushing event queue after websocket reconnection:",
|
|
1847
|
+
error
|
|
1848
|
+
);
|
|
1849
|
+
});
|
|
1850
|
+
this.debug(`Reconnection successful`);
|
|
1851
|
+
} catch (error) {
|
|
1852
|
+
this.isConnecting = false;
|
|
1853
|
+
throw error;
|
|
1644
1854
|
}
|
|
1645
|
-
this.flushEventQueue().catch((error) => {
|
|
1646
|
-
this.debug("Error flushing event queue after websocket reconnection:", error);
|
|
1647
|
-
});
|
|
1648
|
-
this.debug(`Reconnection successful`);
|
|
1649
1855
|
} catch (error) {
|
|
1650
1856
|
this.debug(`Reconnection attempt failed:`, error);
|
|
1651
1857
|
}
|
|
@@ -1663,6 +1869,8 @@ var Schematic = class {
|
|
|
1663
1869
|
const wsUrl = `${this.webSocketUrl}/flags/bootstrap?apiKey=${this.apiKey}`;
|
|
1664
1870
|
this.debug(`connecting to WebSocket:`, wsUrl);
|
|
1665
1871
|
const webSocket = new WebSocket(wsUrl);
|
|
1872
|
+
const connectionId = Math.random().toString(36).substring(7);
|
|
1873
|
+
this.debug(`Creating WebSocket connection ${connectionId} to ${wsUrl}`);
|
|
1666
1874
|
let timeoutId = null;
|
|
1667
1875
|
let isResolved = false;
|
|
1668
1876
|
timeoutId = setTimeout(() => {
|
|
@@ -1681,7 +1889,7 @@ var Schematic = class {
|
|
|
1681
1889
|
}
|
|
1682
1890
|
this.wsReconnectAttempts = 0;
|
|
1683
1891
|
this.wsIntentionalDisconnect = false;
|
|
1684
|
-
this.debug(`WebSocket connection opened`);
|
|
1892
|
+
this.debug(`WebSocket connection ${connectionId} opened successfully`);
|
|
1685
1893
|
resolve(webSocket);
|
|
1686
1894
|
};
|
|
1687
1895
|
webSocket.onerror = (error) => {
|
|
@@ -1689,7 +1897,7 @@ var Schematic = class {
|
|
|
1689
1897
|
if (timeoutId !== null) {
|
|
1690
1898
|
clearTimeout(timeoutId);
|
|
1691
1899
|
}
|
|
1692
|
-
this.debug(`WebSocket connection error:`, error);
|
|
1900
|
+
this.debug(`WebSocket connection ${connectionId} error:`, error);
|
|
1693
1901
|
reject(error);
|
|
1694
1902
|
};
|
|
1695
1903
|
webSocket.onclose = () => {
|
|
@@ -1697,121 +1905,48 @@ var Schematic = class {
|
|
|
1697
1905
|
if (timeoutId !== null) {
|
|
1698
1906
|
clearTimeout(timeoutId);
|
|
1699
1907
|
}
|
|
1700
|
-
this.debug(`WebSocket connection closed`);
|
|
1908
|
+
this.debug(`WebSocket connection ${connectionId} closed`);
|
|
1701
1909
|
this.conn = null;
|
|
1910
|
+
if (this.currentWebSocket === webSocket) {
|
|
1911
|
+
this.currentWebSocket = null;
|
|
1912
|
+
this.isConnecting = false;
|
|
1913
|
+
}
|
|
1702
1914
|
if (!this.wsIntentionalDisconnect && this.webSocketReconnect) {
|
|
1703
1915
|
this.attemptReconnect();
|
|
1704
1916
|
}
|
|
1705
1917
|
};
|
|
1706
1918
|
});
|
|
1707
1919
|
};
|
|
1708
|
-
// Send a message on the websocket after reconnection, forcing the send even if context appears unchanged
|
|
1709
|
-
// because the server has lost all state and needs the initial context
|
|
1710
|
-
wsSendContextAfterReconnection = (socket, context) => {
|
|
1711
|
-
if (this.isOffline()) {
|
|
1712
|
-
this.debug("wsSendContextAfterReconnection: skipped (offline mode)");
|
|
1713
|
-
this.setIsPending(false);
|
|
1714
|
-
return Promise.resolve();
|
|
1715
|
-
}
|
|
1716
|
-
return new Promise((resolve) => {
|
|
1717
|
-
this.debug(`WebSocket force sending context after reconnection:`, context);
|
|
1718
|
-
this.context = context;
|
|
1719
|
-
const sendMessage = () => {
|
|
1720
|
-
let resolved = false;
|
|
1721
|
-
const messageHandler = (event) => {
|
|
1722
|
-
const message = JSON.parse(event.data);
|
|
1723
|
-
this.debug(`WebSocket message received after reconnection:`, message);
|
|
1724
|
-
if (!(contextString(context) in this.checks)) {
|
|
1725
|
-
this.checks[contextString(context)] = {};
|
|
1726
|
-
}
|
|
1727
|
-
(message.flags ?? []).forEach((flag) => {
|
|
1728
|
-
const flagCheck = CheckFlagReturnFromJSON(flag);
|
|
1729
|
-
const contextStr = contextString(context);
|
|
1730
|
-
if (this.checks[contextStr] === void 0) {
|
|
1731
|
-
this.checks[contextStr] = {};
|
|
1732
|
-
}
|
|
1733
|
-
this.checks[contextStr][flagCheck.flag] = flagCheck;
|
|
1734
|
-
});
|
|
1735
|
-
this.useWebSocket = true;
|
|
1736
|
-
socket.removeEventListener("message", messageHandler);
|
|
1737
|
-
if (!resolved) {
|
|
1738
|
-
resolved = true;
|
|
1739
|
-
resolve(this.setIsPending(false));
|
|
1740
|
-
}
|
|
1741
|
-
};
|
|
1742
|
-
socket.addEventListener("message", messageHandler);
|
|
1743
|
-
const clientVersion = this.additionalHeaders["X-Schematic-Client-Version"] ?? `schematic-js@${version}`;
|
|
1744
|
-
const messagePayload = {
|
|
1745
|
-
apiKey: this.apiKey,
|
|
1746
|
-
clientVersion,
|
|
1747
|
-
data: context
|
|
1748
|
-
};
|
|
1749
|
-
this.debug(`WebSocket sending forced message after reconnection:`, messagePayload);
|
|
1750
|
-
socket.send(JSON.stringify(messagePayload));
|
|
1751
|
-
};
|
|
1752
|
-
if (socket.readyState === WebSocket.OPEN) {
|
|
1753
|
-
this.debug(`WebSocket already open, sending forced message after reconnection`);
|
|
1754
|
-
sendMessage();
|
|
1755
|
-
} else {
|
|
1756
|
-
socket.addEventListener("open", () => {
|
|
1757
|
-
this.debug(`WebSocket opened, sending forced message after reconnection`);
|
|
1758
|
-
sendMessage();
|
|
1759
|
-
});
|
|
1760
|
-
}
|
|
1761
|
-
});
|
|
1762
|
-
};
|
|
1763
1920
|
// Send a message on the websocket indicating interest in a particular evaluation context
|
|
1764
1921
|
// and wait for the initial set of flag values to be returned
|
|
1765
|
-
wsSendMessage = (socket, context) => {
|
|
1922
|
+
wsSendMessage = (socket, context, forceContextSend = false) => {
|
|
1766
1923
|
if (this.isOffline()) {
|
|
1767
1924
|
this.debug("wsSendMessage: skipped (offline mode)");
|
|
1768
1925
|
this.setIsPending(false);
|
|
1769
1926
|
return Promise.resolve();
|
|
1770
1927
|
}
|
|
1771
1928
|
return new Promise((resolve, reject) => {
|
|
1772
|
-
if (contextString(context) == contextString(this.context)) {
|
|
1929
|
+
if (!forceContextSend && contextString(context) == contextString(this.context)) {
|
|
1773
1930
|
this.debug(`WebSocket context unchanged, skipping update`);
|
|
1774
1931
|
return resolve(this.setIsPending(false));
|
|
1775
1932
|
}
|
|
1776
|
-
this.debug(
|
|
1933
|
+
this.debug(
|
|
1934
|
+
forceContextSend ? `WebSocket force sending context (reconnection):` : `WebSocket context updated:`,
|
|
1935
|
+
context
|
|
1936
|
+
);
|
|
1777
1937
|
this.context = context;
|
|
1778
1938
|
const sendMessage = () => {
|
|
1779
1939
|
let resolved = false;
|
|
1940
|
+
const persistentMessageHandler = this.createPersistentMessageHandler(context);
|
|
1780
1941
|
const messageHandler = (event) => {
|
|
1781
|
-
|
|
1782
|
-
this.debug(`WebSocket message received:`, message);
|
|
1783
|
-
if (!(contextString(context) in this.checks)) {
|
|
1784
|
-
this.checks[contextString(context)] = {};
|
|
1785
|
-
}
|
|
1786
|
-
(message.flags ?? []).forEach((flag) => {
|
|
1787
|
-
const flagCheck = CheckFlagReturnFromJSON(flag);
|
|
1788
|
-
const contextStr = contextString(context);
|
|
1789
|
-
if (this.checks[contextStr] === void 0) {
|
|
1790
|
-
this.checks[contextStr] = {};
|
|
1791
|
-
}
|
|
1792
|
-
this.checks[contextStr][flagCheck.flag] = flagCheck;
|
|
1793
|
-
this.debug(`WebSocket flag update:`, {
|
|
1794
|
-
flag: flagCheck.flag,
|
|
1795
|
-
value: flagCheck.value,
|
|
1796
|
-
flagCheck
|
|
1797
|
-
});
|
|
1798
|
-
if (typeof flagCheck.featureUsageEvent === "string") {
|
|
1799
|
-
this.updateFeatureUsageEventMap(flagCheck);
|
|
1800
|
-
}
|
|
1801
|
-
if (this.flagCheckListeners[flag.flag]?.size > 0 || this.flagValueListeners[flag.flag]?.size > 0) {
|
|
1802
|
-
this.submitFlagCheckEvent(flagCheck.flag, flagCheck, context);
|
|
1803
|
-
}
|
|
1804
|
-
this.notifyFlagCheckListeners(flag.flag, flagCheck);
|
|
1805
|
-
this.notifyFlagValueListeners(flag.flag, flagCheck.value);
|
|
1806
|
-
});
|
|
1807
|
-
this.flushContextDependentEventQueue();
|
|
1808
|
-
this.setIsPending(false);
|
|
1942
|
+
persistentMessageHandler(event);
|
|
1809
1943
|
if (!resolved) {
|
|
1810
1944
|
resolved = true;
|
|
1811
1945
|
resolve();
|
|
1812
1946
|
}
|
|
1813
1947
|
};
|
|
1814
1948
|
socket.addEventListener("message", messageHandler);
|
|
1949
|
+
this.currentWebSocket = socket;
|
|
1815
1950
|
const clientVersion = this.additionalHeaders["X-Schematic-Client-Version"] ?? `schematic-js@${version}`;
|
|
1816
1951
|
const messagePayload = {
|
|
1817
1952
|
apiKey: this.apiKey,
|
|
@@ -1919,7 +2054,17 @@ var Schematic = class {
|
|
|
1919
2054
|
{ value }
|
|
1920
2055
|
);
|
|
1921
2056
|
}
|
|
1922
|
-
listeners.forEach((listener) =>
|
|
2057
|
+
listeners.forEach((listener, index) => {
|
|
2058
|
+
this.debug(`Calling listener ${index} for flag ${flagKey}`, {
|
|
2059
|
+
flagKey,
|
|
2060
|
+
value
|
|
2061
|
+
});
|
|
2062
|
+
notifyFlagValueListener(listener, value);
|
|
2063
|
+
this.debug(`Listener ${index} for flag ${flagKey} completed`, {
|
|
2064
|
+
flagKey,
|
|
2065
|
+
value
|
|
2066
|
+
});
|
|
2067
|
+
});
|
|
1923
2068
|
};
|
|
1924
2069
|
};
|
|
1925
2070
|
var notifyPendingListener = (listener, value) => {
|
package/dist/schematic.d.ts
CHANGED
|
@@ -378,6 +378,8 @@ export declare class Schematic {
|
|
|
378
378
|
private wsReconnectAttempts;
|
|
379
379
|
private wsReconnectTimer;
|
|
380
380
|
private wsIntentionalDisconnect;
|
|
381
|
+
private currentWebSocket;
|
|
382
|
+
private isConnecting;
|
|
381
383
|
private maxEventQueueSize;
|
|
382
384
|
private maxEventRetries;
|
|
383
385
|
private eventRetryInitialDelay;
|
|
@@ -413,7 +415,11 @@ export declare class Schematic {
|
|
|
413
415
|
* Helper function to log debug messages
|
|
414
416
|
* Only logs if debug mode is enabled
|
|
415
417
|
*/
|
|
416
|
-
|
|
418
|
+
debug(message: string, ...args: unknown[]): void;
|
|
419
|
+
/**
|
|
420
|
+
* Create a persistent message handler for websocket flag updates
|
|
421
|
+
*/
|
|
422
|
+
private createPersistentMessageHandler;
|
|
417
423
|
/**
|
|
418
424
|
* Helper function to check if client is in offline mode
|
|
419
425
|
*/
|
|
@@ -481,6 +487,35 @@ export declare class Schematic {
|
|
|
481
487
|
* In offline mode, this is a no-op.
|
|
482
488
|
*/
|
|
483
489
|
cleanup: () => Promise<void>;
|
|
490
|
+
/**
|
|
491
|
+
* Force an immediate WebSocket reconnection.
|
|
492
|
+
* This is useful when the application returns from a background state (e.g., mobile app
|
|
493
|
+
* coming back to foreground) and wants to immediately re-establish the connection
|
|
494
|
+
* rather than waiting for the exponential backoff timer.
|
|
495
|
+
*
|
|
496
|
+
* This method will:
|
|
497
|
+
* - Cancel any pending reconnection timer
|
|
498
|
+
* - Reset the reconnection attempt counter
|
|
499
|
+
* - Immediately attempt to reconnect
|
|
500
|
+
* - Re-send the current context to get fresh flag values
|
|
501
|
+
*
|
|
502
|
+
* If a connection is already active and healthy, this will close it and create a new one.
|
|
503
|
+
* If you just want to ensure a connection exists, check the connection state first.
|
|
504
|
+
*
|
|
505
|
+
* @example
|
|
506
|
+
* ```typescript
|
|
507
|
+
* // React Native example: reconnect when app comes to foreground
|
|
508
|
+
* useEffect(() => {
|
|
509
|
+
* const subscription = AppState.addEventListener("change", (state) => {
|
|
510
|
+
* if (state === "active") {
|
|
511
|
+
* client.forceReconnect();
|
|
512
|
+
* }
|
|
513
|
+
* });
|
|
514
|
+
* return () => subscription.remove();
|
|
515
|
+
* }, [client]);
|
|
516
|
+
* ```
|
|
517
|
+
*/
|
|
518
|
+
forceReconnect: () => Promise<void>;
|
|
484
519
|
/**
|
|
485
520
|
* Calculate the delay for the next reconnection attempt using exponential backoff with jitter.
|
|
486
521
|
* This helps prevent dogpiling when the server recovers from an outage.
|
|
@@ -500,7 +535,6 @@ export declare class Schematic {
|
|
|
500
535
|
*/
|
|
501
536
|
private attemptReconnect;
|
|
502
537
|
private wsConnect;
|
|
503
|
-
private wsSendContextAfterReconnection;
|
|
504
538
|
private wsSendMessage;
|
|
505
539
|
/**
|
|
506
540
|
* State management
|