@schematichq/schematic-react 1.2.8 → 1.2.10

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.
@@ -799,7 +799,7 @@ function contextString(context) {
799
799
  }, {});
800
800
  return JSON.stringify(sortedContext);
801
801
  }
802
- var version = "1.2.8";
802
+ var version = "1.2.10";
803
803
  var anonymousIdKey = "schematicId";
804
804
  var Schematic = class {
805
805
  additionalHeaders = {};
@@ -829,6 +829,8 @@ var Schematic = class {
829
829
  wsReconnectAttempts = 0;
830
830
  wsReconnectTimer = null;
831
831
  wsIntentionalDisconnect = false;
832
+ currentWebSocket = null;
833
+ isConnecting = false;
832
834
  maxEventQueueSize = 100;
833
835
  // Prevent memory issues with very long network outages
834
836
  maxEventRetries = 5;
@@ -838,6 +840,8 @@ var Schematic = class {
838
840
  eventRetryMaxDelay = 3e4;
839
841
  // Maximum retry delay in ms
840
842
  retryTimer = null;
843
+ flagValueDefaults = {};
844
+ flagCheckDefaults = {};
841
845
  constructor(apiKey, options) {
842
846
  this.apiKey = apiKey;
843
847
  this.eventQueue = [];
@@ -908,6 +912,12 @@ var Schematic = class {
908
912
  if (options?.eventRetryMaxDelay !== void 0) {
909
913
  this.eventRetryMaxDelay = options.eventRetryMaxDelay;
910
914
  }
915
+ if (options?.flagValueDefaults !== void 0) {
916
+ this.flagValueDefaults = options.flagValueDefaults;
917
+ }
918
+ if (options?.flagCheckDefaults !== void 0) {
919
+ this.flagCheckDefaults = options.flagCheckDefaults;
920
+ }
911
921
  if (typeof window !== "undefined" && window?.addEventListener) {
912
922
  window.addEventListener("beforeunload", () => {
913
923
  this.flushEventQueue();
@@ -932,6 +942,62 @@ var Schematic = class {
932
942
  this.debug("Initialized with debug mode enabled");
933
943
  }
934
944
  }
945
+ /**
946
+ * Resolve fallback value according to priority order:
947
+ * 1. Callsite fallback value (if provided)
948
+ * 2. Initialization fallback value (flagValueDefaults)
949
+ * 3. Default to false
950
+ */
951
+ resolveFallbackValue(key, callsiteFallback) {
952
+ if (callsiteFallback !== void 0) {
953
+ return callsiteFallback;
954
+ }
955
+ if (key in this.flagValueDefaults) {
956
+ return this.flagValueDefaults[key];
957
+ }
958
+ return false;
959
+ }
960
+ /**
961
+ * Resolve complete CheckFlagReturn object according to priority order:
962
+ * 1. Use callsite fallback for boolean value, construct CheckFlagReturn
963
+ * 2. Use flagCheckDefaults if available for this flag
964
+ * 3. Use flagValueDefaults if available for this flag, construct CheckFlagReturn
965
+ * 4. Default CheckFlagReturn with value: false
966
+ */
967
+ resolveFallbackCheckFlagReturn(key, callsiteFallback, reason = "Fallback value used", error) {
968
+ if (callsiteFallback !== void 0) {
969
+ return {
970
+ flag: key,
971
+ value: callsiteFallback,
972
+ reason,
973
+ error
974
+ };
975
+ }
976
+ if (key in this.flagCheckDefaults) {
977
+ const defaultReturn = this.flagCheckDefaults[key];
978
+ return {
979
+ ...defaultReturn,
980
+ flag: key,
981
+ // Ensure flag matches the requested key
982
+ reason: error !== void 0 ? reason : defaultReturn.reason,
983
+ error
984
+ };
985
+ }
986
+ if (key in this.flagValueDefaults) {
987
+ return {
988
+ flag: key,
989
+ value: this.flagValueDefaults[key],
990
+ reason,
991
+ error
992
+ };
993
+ }
994
+ return {
995
+ flag: key,
996
+ value: false,
997
+ reason,
998
+ error
999
+ };
1000
+ }
935
1001
  /**
936
1002
  * Get value for a single flag.
937
1003
  * In WebSocket mode, returns cached values if connection is active, otherwise establishes
@@ -940,16 +1006,21 @@ var Schematic = class {
940
1006
  * In REST mode, makes an API call for each check.
941
1007
  */
942
1008
  async checkFlag(options) {
943
- const { fallback = false, key } = options;
1009
+ const { fallback, key } = options;
944
1010
  const context = options.context || this.context;
945
1011
  const contextStr = contextString(context);
946
1012
  this.debug(`checkFlag: ${key}`, { context, fallback });
947
1013
  if (this.isOffline()) {
1014
+ const resolvedFallbackResult = this.resolveFallbackCheckFlagReturn(
1015
+ key,
1016
+ fallback,
1017
+ "Offline mode - using initialization defaults"
1018
+ );
948
1019
  this.debug(`checkFlag offline result: ${key}`, {
949
- value: fallback,
1020
+ value: resolvedFallbackResult.value,
950
1021
  offlineMode: true
951
1022
  });
952
- return fallback;
1023
+ return resolvedFallbackResult.value;
953
1024
  }
954
1025
  if (!this.useWebSocket) {
955
1026
  const requestUrl = `${this.apiUrl}/flags/${key}/check`;
@@ -977,14 +1048,14 @@ var Schematic = class {
977
1048
  return result.value;
978
1049
  }).catch((error) => {
979
1050
  console.error("There was a problem with the fetch operation:", error);
980
- const errorResult = {
981
- flag: key,
982
- value: fallback,
983
- reason: "API request failed",
984
- error: error instanceof Error ? error.message : String(error)
985
- };
1051
+ const errorResult = this.resolveFallbackCheckFlagReturn(
1052
+ key,
1053
+ fallback,
1054
+ "API request failed",
1055
+ error instanceof Error ? error.message : String(error)
1056
+ );
986
1057
  this.submitFlagCheckEvent(key, errorResult, context);
987
- return fallback;
1058
+ return errorResult.value;
988
1059
  });
989
1060
  }
990
1061
  try {
@@ -994,7 +1065,7 @@ var Schematic = class {
994
1065
  return existingVals[key].value;
995
1066
  }
996
1067
  if (this.isOffline()) {
997
- return fallback;
1068
+ return this.resolveFallbackValue(key, fallback);
998
1069
  }
999
1070
  try {
1000
1071
  await this.setContext(context);
@@ -1007,10 +1078,10 @@ var Schematic = class {
1007
1078
  }
1008
1079
  const contextVals = this.checks[contextStr] ?? {};
1009
1080
  const flagCheck = contextVals[key];
1010
- const result = flagCheck?.value ?? fallback;
1081
+ const result = flagCheck?.value ?? this.resolveFallbackValue(key, fallback);
1011
1082
  this.debug(
1012
1083
  `checkFlag WebSocket result: ${key}`,
1013
- typeof flagCheck !== "undefined" ? flagCheck : { value: fallback, fallbackUsed: true }
1084
+ typeof flagCheck !== "undefined" ? flagCheck : { value: result, fallbackUsed: true }
1014
1085
  );
1015
1086
  if (typeof flagCheck !== "undefined") {
1016
1087
  this.submitFlagCheckEvent(key, flagCheck, context);
@@ -1018,14 +1089,14 @@ var Schematic = class {
1018
1089
  return result;
1019
1090
  } catch (error) {
1020
1091
  console.error("Unexpected error in checkFlag:", error);
1021
- const errorResult = {
1022
- flag: key,
1023
- value: fallback,
1024
- reason: "Unexpected error in flag check",
1025
- error: error instanceof Error ? error.message : String(error)
1026
- };
1092
+ const errorResult = this.resolveFallbackCheckFlagReturn(
1093
+ key,
1094
+ fallback,
1095
+ "Unexpected error in flag check",
1096
+ error instanceof Error ? error.message : String(error)
1097
+ );
1027
1098
  this.submitFlagCheckEvent(key, errorResult, context);
1028
- return fallback;
1099
+ return errorResult.value;
1029
1100
  }
1030
1101
  }
1031
1102
  /**
@@ -1037,6 +1108,49 @@ var Schematic = class {
1037
1108
  console.log(`[Schematic] ${message}`, ...args);
1038
1109
  }
1039
1110
  }
1111
+ /**
1112
+ * Create a persistent message handler for websocket flag updates
1113
+ */
1114
+ createPersistentMessageHandler(context) {
1115
+ return (event) => {
1116
+ const message = JSON.parse(event.data);
1117
+ this.debug(`WebSocket persistent message received:`, message);
1118
+ if (!(contextString(context) in this.checks)) {
1119
+ this.checks[contextString(context)] = {};
1120
+ }
1121
+ (message.flags ?? []).forEach((flag) => {
1122
+ const flagCheck = CheckFlagReturnFromJSON(flag);
1123
+ const contextStr = contextString(context);
1124
+ if (this.checks[contextStr] === void 0) {
1125
+ this.checks[contextStr] = {};
1126
+ }
1127
+ this.checks[contextStr][flagCheck.flag] = flagCheck;
1128
+ this.debug(`WebSocket flag update:`, {
1129
+ flag: flagCheck.flag,
1130
+ value: flagCheck.value,
1131
+ flagCheck
1132
+ });
1133
+ if (typeof flagCheck.featureUsageEvent === "string") {
1134
+ this.updateFeatureUsageEventMap(flagCheck);
1135
+ }
1136
+ if ((this.flagCheckListeners[flag.flag]?.size ?? 0) > 0 || (this.flagValueListeners[flag.flag]?.size ?? 0) > 0) {
1137
+ this.submitFlagCheckEvent(flagCheck.flag, flagCheck, context);
1138
+ }
1139
+ this.debug(`About to notify listeners for flag ${flag.flag}`, {
1140
+ flag: flag.flag,
1141
+ value: flagCheck.value
1142
+ });
1143
+ this.notifyFlagCheckListeners(flag.flag, flagCheck);
1144
+ this.notifyFlagValueListeners(flag.flag, flagCheck.value);
1145
+ this.debug(`Finished notifying listeners for flag ${flag.flag}`, {
1146
+ flag: flag.flag,
1147
+ value: flagCheck.value
1148
+ });
1149
+ });
1150
+ this.flushContextDependentEventQueue();
1151
+ this.setIsPending(false);
1152
+ };
1153
+ }
1040
1154
  /**
1041
1155
  * Helper function to check if client is in offline mode
1042
1156
  */
@@ -1068,11 +1182,12 @@ var Schematic = class {
1068
1182
  */
1069
1183
  async fallbackToRest(key, context, fallback) {
1070
1184
  if (this.isOffline()) {
1185
+ const resolvedFallback = this.resolveFallbackValue(key, fallback);
1071
1186
  this.debug(`fallbackToRest offline result: ${key}`, {
1072
- value: fallback,
1187
+ value: resolvedFallback,
1073
1188
  offlineMode: true
1074
1189
  });
1075
- return fallback;
1190
+ return resolvedFallback;
1076
1191
  }
1077
1192
  try {
1078
1193
  const requestUrl = `${this.apiUrl}/flags/${key}/check`;
@@ -1099,14 +1214,14 @@ var Schematic = class {
1099
1214
  return result.value;
1100
1215
  } catch (error) {
1101
1216
  console.error("REST API call failed, using fallback value:", error);
1102
- const errorResult = {
1103
- flag: key,
1104
- value: fallback,
1105
- reason: "API request failed (fallback)",
1106
- error: error instanceof Error ? error.message : String(error)
1107
- };
1217
+ const errorResult = this.resolveFallbackCheckFlagReturn(
1218
+ key,
1219
+ fallback,
1220
+ "API request failed (fallback)",
1221
+ error instanceof Error ? error.message : String(error)
1222
+ );
1108
1223
  this.submitFlagCheckEvent(key, errorResult, context);
1109
- return fallback;
1224
+ return errorResult.value;
1110
1225
  }
1111
1226
  }
1112
1227
  /**
@@ -1187,6 +1302,19 @@ var Schematic = class {
1187
1302
  try {
1188
1303
  this.setIsPending(true);
1189
1304
  if (!this.conn) {
1305
+ if (this.isConnecting) {
1306
+ this.debug(
1307
+ `Connection already in progress, waiting for it to complete`
1308
+ );
1309
+ while (this.isConnecting && this.conn === null) {
1310
+ await new Promise((resolve) => setTimeout(resolve, 10));
1311
+ }
1312
+ if (this.conn !== null) {
1313
+ const socket2 = await this.conn;
1314
+ await this.wsSendMessage(socket2, context);
1315
+ return;
1316
+ }
1317
+ }
1190
1318
  if (this.wsReconnectTimer !== null) {
1191
1319
  this.debug(
1192
1320
  `Cancelling scheduled reconnection, connecting immediately`
@@ -1194,7 +1322,17 @@ var Schematic = class {
1194
1322
  clearTimeout(this.wsReconnectTimer);
1195
1323
  this.wsReconnectTimer = null;
1196
1324
  }
1197
- this.conn = this.wsConnect();
1325
+ this.isConnecting = true;
1326
+ try {
1327
+ this.conn = this.wsConnect();
1328
+ const socket2 = await this.conn;
1329
+ this.isConnecting = false;
1330
+ await this.wsSendMessage(socket2, context);
1331
+ return;
1332
+ } catch (error) {
1333
+ this.isConnecting = false;
1334
+ throw error;
1335
+ }
1198
1336
  }
1199
1337
  const socket = await this.conn;
1200
1338
  await this.wsSendMessage(socket, context);
@@ -1359,10 +1497,14 @@ var Schematic = class {
1359
1497
  }
1360
1498
  }
1361
1499
  if (readyEvents.length === 0) {
1362
- this.debug(`No events ready for retry yet (${notReadyEvents.length} still in backoff)`);
1500
+ this.debug(
1501
+ `No events ready for retry yet (${notReadyEvents.length} still in backoff)`
1502
+ );
1363
1503
  return;
1364
1504
  }
1365
- this.debug(`Flushing event queue: ${readyEvents.length} ready, ${notReadyEvents.length} waiting`);
1505
+ this.debug(
1506
+ `Flushing event queue: ${readyEvents.length} ready, ${notReadyEvents.length} waiting`
1507
+ );
1366
1508
  this.eventQueue = notReadyEvents;
1367
1509
  for (const event of readyEvents) {
1368
1510
  try {
@@ -1427,7 +1569,10 @@ var Schematic = class {
1427
1569
  } catch (error) {
1428
1570
  const retryCount = (event.retry_count ?? 0) + 1;
1429
1571
  if (retryCount <= this.maxEventRetries) {
1430
- this.debug(`Event failed to send (attempt ${retryCount}/${this.maxEventRetries}), queueing for retry:`, error);
1572
+ this.debug(
1573
+ `Event failed to send (attempt ${retryCount}/${this.maxEventRetries}), queueing for retry:`,
1574
+ error
1575
+ );
1431
1576
  const baseDelay = this.eventRetryInitialDelay * Math.pow(2, retryCount - 1);
1432
1577
  const jitterDelay = Math.min(baseDelay, this.eventRetryMaxDelay);
1433
1578
  const nextRetryAt = Date.now() + jitterDelay;
@@ -1438,15 +1583,22 @@ var Schematic = class {
1438
1583
  };
1439
1584
  if (this.eventQueue.length < this.maxEventQueueSize) {
1440
1585
  this.eventQueue.push(retryEvent);
1441
- this.debug(`Event queued for retry in ${jitterDelay}ms (${this.eventQueue.length}/${this.maxEventQueueSize})`);
1586
+ this.debug(
1587
+ `Event queued for retry in ${jitterDelay}ms (${this.eventQueue.length}/${this.maxEventQueueSize})`
1588
+ );
1442
1589
  } else {
1443
- this.debug(`Event queue full (${this.maxEventQueueSize}), dropping oldest event`);
1590
+ this.debug(
1591
+ `Event queue full (${this.maxEventQueueSize}), dropping oldest event`
1592
+ );
1444
1593
  this.eventQueue.shift();
1445
1594
  this.eventQueue.push(retryEvent);
1446
1595
  }
1447
1596
  this.startRetryTimer();
1448
1597
  } else {
1449
- this.debug(`Event failed permanently after ${this.maxEventRetries} attempts, dropping:`, error);
1598
+ this.debug(
1599
+ `Event failed permanently after ${this.maxEventRetries} attempts, dropping:`,
1600
+ error
1601
+ );
1450
1602
  }
1451
1603
  }
1452
1604
  return Promise.resolve();
@@ -1476,11 +1628,17 @@ var Schematic = class {
1476
1628
  if (this.conn) {
1477
1629
  try {
1478
1630
  const socket = await this.conn;
1631
+ if (this.currentWebSocket === socket) {
1632
+ this.debug(`Cleaning up current websocket tracking`);
1633
+ this.currentWebSocket = null;
1634
+ }
1479
1635
  socket.close();
1480
1636
  } catch (error) {
1481
1637
  console.error("Error during cleanup:", error);
1482
1638
  } finally {
1483
1639
  this.conn = null;
1640
+ this.currentWebSocket = null;
1641
+ this.isConnecting = false;
1484
1642
  }
1485
1643
  }
1486
1644
  };
@@ -1505,7 +1663,9 @@ var Schematic = class {
1505
1663
  if (this.conn !== null) {
1506
1664
  try {
1507
1665
  const socket = await this.conn;
1508
- socket.close();
1666
+ if (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING) {
1667
+ socket.close();
1668
+ }
1509
1669
  } catch (error) {
1510
1670
  this.debug("Error closing connection on offline:", error);
1511
1671
  }
@@ -1520,7 +1680,9 @@ var Schematic = class {
1520
1680
  * Handle browser coming back online
1521
1681
  */
1522
1682
  handleNetworkOnline = () => {
1523
- this.debug("Network online, attempting reconnection and flushing queued events");
1683
+ this.debug(
1684
+ "Network online, attempting reconnection and flushing queued events"
1685
+ );
1524
1686
  this.wsReconnectAttempts = 0;
1525
1687
  if (this.wsReconnectTimer !== null) {
1526
1688
  clearTimeout(this.wsReconnectTimer);
@@ -1543,7 +1705,10 @@ var Schematic = class {
1543
1705
  return;
1544
1706
  }
1545
1707
  if (this.wsReconnectTimer !== null) {
1546
- clearTimeout(this.wsReconnectTimer);
1708
+ this.debug(
1709
+ `Reconnection attempt already scheduled, ignoring duplicate request`
1710
+ );
1711
+ return;
1547
1712
  }
1548
1713
  const delay = this.calculateReconnectDelay();
1549
1714
  this.debug(
@@ -1556,23 +1721,57 @@ var Schematic = class {
1556
1721
  `Attempting to reconnect (attempt ${this.wsReconnectAttempts}/${this.webSocketMaxReconnectAttempts})`
1557
1722
  );
1558
1723
  try {
1559
- this.conn = this.wsConnect();
1560
- const socket = await this.conn;
1561
- this.debug(`Reconnection context check:`, {
1562
- hasCompany: this.context.company !== void 0,
1563
- hasUser: this.context.user !== void 0,
1564
- context: this.context
1565
- });
1566
- if (this.context.company !== void 0 || this.context.user !== void 0) {
1567
- this.debug(`Reconnected, force re-sending context`);
1568
- await this.wsSendContextAfterReconnection(socket, this.context);
1569
- } else {
1570
- this.debug(`No context to re-send after reconnection - websocket ready for new context`);
1724
+ if (this.conn !== null) {
1725
+ this.debug(`Cleaning up existing connection before reconnection`);
1726
+ try {
1727
+ const existingSocket = await this.conn;
1728
+ if (this.currentWebSocket === existingSocket) {
1729
+ this.debug(`Existing websocket is current, will be replaced`);
1730
+ this.currentWebSocket = null;
1731
+ }
1732
+ if (existingSocket.readyState === WebSocket.OPEN || existingSocket.readyState === WebSocket.CONNECTING) {
1733
+ existingSocket.close();
1734
+ }
1735
+ } catch (error) {
1736
+ this.debug(`Error cleaning up existing connection:`, error);
1737
+ }
1738
+ this.conn = null;
1739
+ this.currentWebSocket = null;
1740
+ this.isConnecting = false;
1741
+ }
1742
+ this.isConnecting = true;
1743
+ try {
1744
+ this.conn = this.wsConnect();
1745
+ const socket = await this.conn;
1746
+ this.isConnecting = false;
1747
+ this.debug(`Reconnection context check:`, {
1748
+ hasCompany: this.context.company !== void 0,
1749
+ hasUser: this.context.user !== void 0,
1750
+ context: this.context
1751
+ });
1752
+ if (this.context.company !== void 0 || this.context.user !== void 0) {
1753
+ this.debug(`Reconnected, force re-sending context`);
1754
+ await this.wsSendMessage(socket, this.context, true);
1755
+ } else {
1756
+ this.debug(
1757
+ `No context to re-send after reconnection - websocket ready for new context`
1758
+ );
1759
+ this.debug(
1760
+ `Setting up tracking for reconnected websocket (no context to send)`
1761
+ );
1762
+ this.currentWebSocket = socket;
1763
+ }
1764
+ this.flushEventQueue().catch((error) => {
1765
+ this.debug(
1766
+ "Error flushing event queue after websocket reconnection:",
1767
+ error
1768
+ );
1769
+ });
1770
+ this.debug(`Reconnection successful`);
1771
+ } catch (error) {
1772
+ this.isConnecting = false;
1773
+ throw error;
1571
1774
  }
1572
- this.flushEventQueue().catch((error) => {
1573
- this.debug("Error flushing event queue after websocket reconnection:", error);
1574
- });
1575
- this.debug(`Reconnection successful`);
1576
1775
  } catch (error) {
1577
1776
  this.debug(`Reconnection attempt failed:`, error);
1578
1777
  }
@@ -1590,6 +1789,8 @@ var Schematic = class {
1590
1789
  const wsUrl = `${this.webSocketUrl}/flags/bootstrap?apiKey=${this.apiKey}`;
1591
1790
  this.debug(`connecting to WebSocket:`, wsUrl);
1592
1791
  const webSocket = new WebSocket(wsUrl);
1792
+ const connectionId = Math.random().toString(36).substring(7);
1793
+ this.debug(`Creating WebSocket connection ${connectionId} to ${wsUrl}`);
1593
1794
  let timeoutId = null;
1594
1795
  let isResolved = false;
1595
1796
  timeoutId = setTimeout(() => {
@@ -1608,7 +1809,7 @@ var Schematic = class {
1608
1809
  }
1609
1810
  this.wsReconnectAttempts = 0;
1610
1811
  this.wsIntentionalDisconnect = false;
1611
- this.debug(`WebSocket connection opened`);
1812
+ this.debug(`WebSocket connection ${connectionId} opened successfully`);
1612
1813
  resolve(webSocket);
1613
1814
  };
1614
1815
  webSocket.onerror = (error) => {
@@ -1616,7 +1817,7 @@ var Schematic = class {
1616
1817
  if (timeoutId !== null) {
1617
1818
  clearTimeout(timeoutId);
1618
1819
  }
1619
- this.debug(`WebSocket connection error:`, error);
1820
+ this.debug(`WebSocket connection ${connectionId} error:`, error);
1620
1821
  reject(error);
1621
1822
  };
1622
1823
  webSocket.onclose = () => {
@@ -1624,121 +1825,48 @@ var Schematic = class {
1624
1825
  if (timeoutId !== null) {
1625
1826
  clearTimeout(timeoutId);
1626
1827
  }
1627
- this.debug(`WebSocket connection closed`);
1828
+ this.debug(`WebSocket connection ${connectionId} closed`);
1628
1829
  this.conn = null;
1830
+ if (this.currentWebSocket === webSocket) {
1831
+ this.currentWebSocket = null;
1832
+ this.isConnecting = false;
1833
+ }
1629
1834
  if (!this.wsIntentionalDisconnect && this.webSocketReconnect) {
1630
1835
  this.attemptReconnect();
1631
1836
  }
1632
1837
  };
1633
1838
  });
1634
1839
  };
1635
- // Send a message on the websocket after reconnection, forcing the send even if context appears unchanged
1636
- // because the server has lost all state and needs the initial context
1637
- wsSendContextAfterReconnection = (socket, context) => {
1638
- if (this.isOffline()) {
1639
- this.debug("wsSendContextAfterReconnection: skipped (offline mode)");
1640
- this.setIsPending(false);
1641
- return Promise.resolve();
1642
- }
1643
- return new Promise((resolve) => {
1644
- this.debug(`WebSocket force sending context after reconnection:`, context);
1645
- this.context = context;
1646
- const sendMessage = () => {
1647
- let resolved = false;
1648
- const messageHandler = (event) => {
1649
- const message = JSON.parse(event.data);
1650
- this.debug(`WebSocket message received after reconnection:`, message);
1651
- if (!(contextString(context) in this.checks)) {
1652
- this.checks[contextString(context)] = {};
1653
- }
1654
- (message.flags ?? []).forEach((flag) => {
1655
- const flagCheck = CheckFlagReturnFromJSON(flag);
1656
- const contextStr = contextString(context);
1657
- if (this.checks[contextStr] === void 0) {
1658
- this.checks[contextStr] = {};
1659
- }
1660
- this.checks[contextStr][flagCheck.flag] = flagCheck;
1661
- });
1662
- this.useWebSocket = true;
1663
- socket.removeEventListener("message", messageHandler);
1664
- if (!resolved) {
1665
- resolved = true;
1666
- resolve(this.setIsPending(false));
1667
- }
1668
- };
1669
- socket.addEventListener("message", messageHandler);
1670
- const clientVersion = this.additionalHeaders["X-Schematic-Client-Version"] ?? `schematic-js@${version}`;
1671
- const messagePayload = {
1672
- apiKey: this.apiKey,
1673
- clientVersion,
1674
- data: context
1675
- };
1676
- this.debug(`WebSocket sending forced message after reconnection:`, messagePayload);
1677
- socket.send(JSON.stringify(messagePayload));
1678
- };
1679
- if (socket.readyState === WebSocket.OPEN) {
1680
- this.debug(`WebSocket already open, sending forced message after reconnection`);
1681
- sendMessage();
1682
- } else {
1683
- socket.addEventListener("open", () => {
1684
- this.debug(`WebSocket opened, sending forced message after reconnection`);
1685
- sendMessage();
1686
- });
1687
- }
1688
- });
1689
- };
1690
1840
  // Send a message on the websocket indicating interest in a particular evaluation context
1691
1841
  // and wait for the initial set of flag values to be returned
1692
- wsSendMessage = (socket, context) => {
1842
+ wsSendMessage = (socket, context, forceContextSend = false) => {
1693
1843
  if (this.isOffline()) {
1694
1844
  this.debug("wsSendMessage: skipped (offline mode)");
1695
1845
  this.setIsPending(false);
1696
1846
  return Promise.resolve();
1697
1847
  }
1698
1848
  return new Promise((resolve, reject) => {
1699
- if (contextString(context) == contextString(this.context)) {
1849
+ if (!forceContextSend && contextString(context) == contextString(this.context)) {
1700
1850
  this.debug(`WebSocket context unchanged, skipping update`);
1701
1851
  return resolve(this.setIsPending(false));
1702
1852
  }
1703
- this.debug(`WebSocket context updated:`, context);
1853
+ this.debug(
1854
+ forceContextSend ? `WebSocket force sending context (reconnection):` : `WebSocket context updated:`,
1855
+ context
1856
+ );
1704
1857
  this.context = context;
1705
1858
  const sendMessage = () => {
1706
1859
  let resolved = false;
1860
+ const persistentMessageHandler = this.createPersistentMessageHandler(context);
1707
1861
  const messageHandler = (event) => {
1708
- const message = JSON.parse(event.data);
1709
- this.debug(`WebSocket message received:`, message);
1710
- if (!(contextString(context) in this.checks)) {
1711
- this.checks[contextString(context)] = {};
1712
- }
1713
- (message.flags ?? []).forEach((flag) => {
1714
- const flagCheck = CheckFlagReturnFromJSON(flag);
1715
- const contextStr = contextString(context);
1716
- if (this.checks[contextStr] === void 0) {
1717
- this.checks[contextStr] = {};
1718
- }
1719
- this.checks[contextStr][flagCheck.flag] = flagCheck;
1720
- this.debug(`WebSocket flag update:`, {
1721
- flag: flagCheck.flag,
1722
- value: flagCheck.value,
1723
- flagCheck
1724
- });
1725
- if (typeof flagCheck.featureUsageEvent === "string") {
1726
- this.updateFeatureUsageEventMap(flagCheck);
1727
- }
1728
- if (this.flagCheckListeners[flag.flag]?.size > 0 || this.flagValueListeners[flag.flag]?.size > 0) {
1729
- this.submitFlagCheckEvent(flagCheck.flag, flagCheck, context);
1730
- }
1731
- this.notifyFlagCheckListeners(flag.flag, flagCheck);
1732
- this.notifyFlagValueListeners(flag.flag, flagCheck.value);
1733
- });
1734
- this.flushContextDependentEventQueue();
1735
- this.setIsPending(false);
1862
+ persistentMessageHandler(event);
1736
1863
  if (!resolved) {
1737
1864
  resolved = true;
1738
1865
  resolve();
1739
1866
  }
1740
1867
  };
1741
1868
  socket.addEventListener("message", messageHandler);
1869
+ this.currentWebSocket = socket;
1742
1870
  const clientVersion = this.additionalHeaders["X-Schematic-Client-Version"] ?? `schematic-js@${version}`;
1743
1871
  const messagePayload = {
1744
1872
  apiKey: this.apiKey,
@@ -1846,7 +1974,17 @@ var Schematic = class {
1846
1974
  { value }
1847
1975
  );
1848
1976
  }
1849
- listeners.forEach((listener) => notifyFlagValueListener(listener, value));
1977
+ listeners.forEach((listener, index) => {
1978
+ this.debug(`Calling listener ${index} for flag ${flagKey}`, {
1979
+ flagKey,
1980
+ value
1981
+ });
1982
+ notifyFlagValueListener(listener, value);
1983
+ this.debug(`Listener ${index} for flag ${flagKey} completed`, {
1984
+ flagKey,
1985
+ value
1986
+ });
1987
+ });
1850
1988
  };
1851
1989
  };
1852
1990
  var notifyPendingListener = (listener, value) => {
@@ -1875,7 +2013,7 @@ var notifyFlagValueListener = (listener, value) => {
1875
2013
  var import_react = __toESM(require("react"));
1876
2014
 
1877
2015
  // src/version.ts
1878
- var version2 = "1.2.8";
2016
+ var version2 = "1.2.10";
1879
2017
 
1880
2018
  // src/context/schematic.tsx
1881
2019
  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.8";
757
+ var version = "1.2.10";
758
758
  var anonymousIdKey = "schematicId";
759
759
  var Schematic = class {
760
760
  additionalHeaders = {};
@@ -784,6 +784,8 @@ var Schematic = class {
784
784
  wsReconnectAttempts = 0;
785
785
  wsReconnectTimer = null;
786
786
  wsIntentionalDisconnect = false;
787
+ currentWebSocket = null;
788
+ isConnecting = false;
787
789
  maxEventQueueSize = 100;
788
790
  // Prevent memory issues with very long network outages
789
791
  maxEventRetries = 5;
@@ -793,6 +795,8 @@ var Schematic = class {
793
795
  eventRetryMaxDelay = 3e4;
794
796
  // Maximum retry delay in ms
795
797
  retryTimer = null;
798
+ flagValueDefaults = {};
799
+ flagCheckDefaults = {};
796
800
  constructor(apiKey, options) {
797
801
  this.apiKey = apiKey;
798
802
  this.eventQueue = [];
@@ -863,6 +867,12 @@ var Schematic = class {
863
867
  if (options?.eventRetryMaxDelay !== void 0) {
864
868
  this.eventRetryMaxDelay = options.eventRetryMaxDelay;
865
869
  }
870
+ if (options?.flagValueDefaults !== void 0) {
871
+ this.flagValueDefaults = options.flagValueDefaults;
872
+ }
873
+ if (options?.flagCheckDefaults !== void 0) {
874
+ this.flagCheckDefaults = options.flagCheckDefaults;
875
+ }
866
876
  if (typeof window !== "undefined" && window?.addEventListener) {
867
877
  window.addEventListener("beforeunload", () => {
868
878
  this.flushEventQueue();
@@ -887,6 +897,62 @@ var Schematic = class {
887
897
  this.debug("Initialized with debug mode enabled");
888
898
  }
889
899
  }
900
+ /**
901
+ * Resolve fallback value according to priority order:
902
+ * 1. Callsite fallback value (if provided)
903
+ * 2. Initialization fallback value (flagValueDefaults)
904
+ * 3. Default to false
905
+ */
906
+ resolveFallbackValue(key, callsiteFallback) {
907
+ if (callsiteFallback !== void 0) {
908
+ return callsiteFallback;
909
+ }
910
+ if (key in this.flagValueDefaults) {
911
+ return this.flagValueDefaults[key];
912
+ }
913
+ return false;
914
+ }
915
+ /**
916
+ * Resolve complete CheckFlagReturn object according to priority order:
917
+ * 1. Use callsite fallback for boolean value, construct CheckFlagReturn
918
+ * 2. Use flagCheckDefaults if available for this flag
919
+ * 3. Use flagValueDefaults if available for this flag, construct CheckFlagReturn
920
+ * 4. Default CheckFlagReturn with value: false
921
+ */
922
+ resolveFallbackCheckFlagReturn(key, callsiteFallback, reason = "Fallback value used", error) {
923
+ if (callsiteFallback !== void 0) {
924
+ return {
925
+ flag: key,
926
+ value: callsiteFallback,
927
+ reason,
928
+ error
929
+ };
930
+ }
931
+ if (key in this.flagCheckDefaults) {
932
+ const defaultReturn = this.flagCheckDefaults[key];
933
+ return {
934
+ ...defaultReturn,
935
+ flag: key,
936
+ // Ensure flag matches the requested key
937
+ reason: error !== void 0 ? reason : defaultReturn.reason,
938
+ error
939
+ };
940
+ }
941
+ if (key in this.flagValueDefaults) {
942
+ return {
943
+ flag: key,
944
+ value: this.flagValueDefaults[key],
945
+ reason,
946
+ error
947
+ };
948
+ }
949
+ return {
950
+ flag: key,
951
+ value: false,
952
+ reason,
953
+ error
954
+ };
955
+ }
890
956
  /**
891
957
  * Get value for a single flag.
892
958
  * In WebSocket mode, returns cached values if connection is active, otherwise establishes
@@ -895,16 +961,21 @@ var Schematic = class {
895
961
  * In REST mode, makes an API call for each check.
896
962
  */
897
963
  async checkFlag(options) {
898
- const { fallback = false, key } = options;
964
+ const { fallback, key } = options;
899
965
  const context = options.context || this.context;
900
966
  const contextStr = contextString(context);
901
967
  this.debug(`checkFlag: ${key}`, { context, fallback });
902
968
  if (this.isOffline()) {
969
+ const resolvedFallbackResult = this.resolveFallbackCheckFlagReturn(
970
+ key,
971
+ fallback,
972
+ "Offline mode - using initialization defaults"
973
+ );
903
974
  this.debug(`checkFlag offline result: ${key}`, {
904
- value: fallback,
975
+ value: resolvedFallbackResult.value,
905
976
  offlineMode: true
906
977
  });
907
- return fallback;
978
+ return resolvedFallbackResult.value;
908
979
  }
909
980
  if (!this.useWebSocket) {
910
981
  const requestUrl = `${this.apiUrl}/flags/${key}/check`;
@@ -932,14 +1003,14 @@ var Schematic = class {
932
1003
  return result.value;
933
1004
  }).catch((error) => {
934
1005
  console.error("There was a problem with the fetch operation:", error);
935
- const errorResult = {
936
- flag: key,
937
- value: fallback,
938
- reason: "API request failed",
939
- error: error instanceof Error ? error.message : String(error)
940
- };
1006
+ const errorResult = this.resolveFallbackCheckFlagReturn(
1007
+ key,
1008
+ fallback,
1009
+ "API request failed",
1010
+ error instanceof Error ? error.message : String(error)
1011
+ );
941
1012
  this.submitFlagCheckEvent(key, errorResult, context);
942
- return fallback;
1013
+ return errorResult.value;
943
1014
  });
944
1015
  }
945
1016
  try {
@@ -949,7 +1020,7 @@ var Schematic = class {
949
1020
  return existingVals[key].value;
950
1021
  }
951
1022
  if (this.isOffline()) {
952
- return fallback;
1023
+ return this.resolveFallbackValue(key, fallback);
953
1024
  }
954
1025
  try {
955
1026
  await this.setContext(context);
@@ -962,10 +1033,10 @@ var Schematic = class {
962
1033
  }
963
1034
  const contextVals = this.checks[contextStr] ?? {};
964
1035
  const flagCheck = contextVals[key];
965
- const result = flagCheck?.value ?? fallback;
1036
+ const result = flagCheck?.value ?? this.resolveFallbackValue(key, fallback);
966
1037
  this.debug(
967
1038
  `checkFlag WebSocket result: ${key}`,
968
- typeof flagCheck !== "undefined" ? flagCheck : { value: fallback, fallbackUsed: true }
1039
+ typeof flagCheck !== "undefined" ? flagCheck : { value: result, fallbackUsed: true }
969
1040
  );
970
1041
  if (typeof flagCheck !== "undefined") {
971
1042
  this.submitFlagCheckEvent(key, flagCheck, context);
@@ -973,14 +1044,14 @@ var Schematic = class {
973
1044
  return result;
974
1045
  } catch (error) {
975
1046
  console.error("Unexpected error in checkFlag:", error);
976
- const errorResult = {
977
- flag: key,
978
- value: fallback,
979
- reason: "Unexpected error in flag check",
980
- error: error instanceof Error ? error.message : String(error)
981
- };
1047
+ const errorResult = this.resolveFallbackCheckFlagReturn(
1048
+ key,
1049
+ fallback,
1050
+ "Unexpected error in flag check",
1051
+ error instanceof Error ? error.message : String(error)
1052
+ );
982
1053
  this.submitFlagCheckEvent(key, errorResult, context);
983
- return fallback;
1054
+ return errorResult.value;
984
1055
  }
985
1056
  }
986
1057
  /**
@@ -992,6 +1063,49 @@ var Schematic = class {
992
1063
  console.log(`[Schematic] ${message}`, ...args);
993
1064
  }
994
1065
  }
1066
+ /**
1067
+ * Create a persistent message handler for websocket flag updates
1068
+ */
1069
+ createPersistentMessageHandler(context) {
1070
+ return (event) => {
1071
+ const message = JSON.parse(event.data);
1072
+ this.debug(`WebSocket persistent message received:`, message);
1073
+ if (!(contextString(context) in this.checks)) {
1074
+ this.checks[contextString(context)] = {};
1075
+ }
1076
+ (message.flags ?? []).forEach((flag) => {
1077
+ const flagCheck = CheckFlagReturnFromJSON(flag);
1078
+ const contextStr = contextString(context);
1079
+ if (this.checks[contextStr] === void 0) {
1080
+ this.checks[contextStr] = {};
1081
+ }
1082
+ this.checks[contextStr][flagCheck.flag] = flagCheck;
1083
+ this.debug(`WebSocket flag update:`, {
1084
+ flag: flagCheck.flag,
1085
+ value: flagCheck.value,
1086
+ flagCheck
1087
+ });
1088
+ if (typeof flagCheck.featureUsageEvent === "string") {
1089
+ this.updateFeatureUsageEventMap(flagCheck);
1090
+ }
1091
+ if ((this.flagCheckListeners[flag.flag]?.size ?? 0) > 0 || (this.flagValueListeners[flag.flag]?.size ?? 0) > 0) {
1092
+ this.submitFlagCheckEvent(flagCheck.flag, flagCheck, context);
1093
+ }
1094
+ this.debug(`About to notify listeners for flag ${flag.flag}`, {
1095
+ flag: flag.flag,
1096
+ value: flagCheck.value
1097
+ });
1098
+ this.notifyFlagCheckListeners(flag.flag, flagCheck);
1099
+ this.notifyFlagValueListeners(flag.flag, flagCheck.value);
1100
+ this.debug(`Finished notifying listeners for flag ${flag.flag}`, {
1101
+ flag: flag.flag,
1102
+ value: flagCheck.value
1103
+ });
1104
+ });
1105
+ this.flushContextDependentEventQueue();
1106
+ this.setIsPending(false);
1107
+ };
1108
+ }
995
1109
  /**
996
1110
  * Helper function to check if client is in offline mode
997
1111
  */
@@ -1023,11 +1137,12 @@ var Schematic = class {
1023
1137
  */
1024
1138
  async fallbackToRest(key, context, fallback) {
1025
1139
  if (this.isOffline()) {
1140
+ const resolvedFallback = this.resolveFallbackValue(key, fallback);
1026
1141
  this.debug(`fallbackToRest offline result: ${key}`, {
1027
- value: fallback,
1142
+ value: resolvedFallback,
1028
1143
  offlineMode: true
1029
1144
  });
1030
- return fallback;
1145
+ return resolvedFallback;
1031
1146
  }
1032
1147
  try {
1033
1148
  const requestUrl = `${this.apiUrl}/flags/${key}/check`;
@@ -1054,14 +1169,14 @@ var Schematic = class {
1054
1169
  return result.value;
1055
1170
  } catch (error) {
1056
1171
  console.error("REST API call failed, using fallback value:", error);
1057
- const errorResult = {
1058
- flag: key,
1059
- value: fallback,
1060
- reason: "API request failed (fallback)",
1061
- error: error instanceof Error ? error.message : String(error)
1062
- };
1172
+ const errorResult = this.resolveFallbackCheckFlagReturn(
1173
+ key,
1174
+ fallback,
1175
+ "API request failed (fallback)",
1176
+ error instanceof Error ? error.message : String(error)
1177
+ );
1063
1178
  this.submitFlagCheckEvent(key, errorResult, context);
1064
- return fallback;
1179
+ return errorResult.value;
1065
1180
  }
1066
1181
  }
1067
1182
  /**
@@ -1142,6 +1257,19 @@ var Schematic = class {
1142
1257
  try {
1143
1258
  this.setIsPending(true);
1144
1259
  if (!this.conn) {
1260
+ if (this.isConnecting) {
1261
+ this.debug(
1262
+ `Connection already in progress, waiting for it to complete`
1263
+ );
1264
+ while (this.isConnecting && this.conn === null) {
1265
+ await new Promise((resolve) => setTimeout(resolve, 10));
1266
+ }
1267
+ if (this.conn !== null) {
1268
+ const socket2 = await this.conn;
1269
+ await this.wsSendMessage(socket2, context);
1270
+ return;
1271
+ }
1272
+ }
1145
1273
  if (this.wsReconnectTimer !== null) {
1146
1274
  this.debug(
1147
1275
  `Cancelling scheduled reconnection, connecting immediately`
@@ -1149,7 +1277,17 @@ var Schematic = class {
1149
1277
  clearTimeout(this.wsReconnectTimer);
1150
1278
  this.wsReconnectTimer = null;
1151
1279
  }
1152
- this.conn = this.wsConnect();
1280
+ this.isConnecting = true;
1281
+ try {
1282
+ this.conn = this.wsConnect();
1283
+ const socket2 = await this.conn;
1284
+ this.isConnecting = false;
1285
+ await this.wsSendMessage(socket2, context);
1286
+ return;
1287
+ } catch (error) {
1288
+ this.isConnecting = false;
1289
+ throw error;
1290
+ }
1153
1291
  }
1154
1292
  const socket = await this.conn;
1155
1293
  await this.wsSendMessage(socket, context);
@@ -1314,10 +1452,14 @@ var Schematic = class {
1314
1452
  }
1315
1453
  }
1316
1454
  if (readyEvents.length === 0) {
1317
- this.debug(`No events ready for retry yet (${notReadyEvents.length} still in backoff)`);
1455
+ this.debug(
1456
+ `No events ready for retry yet (${notReadyEvents.length} still in backoff)`
1457
+ );
1318
1458
  return;
1319
1459
  }
1320
- this.debug(`Flushing event queue: ${readyEvents.length} ready, ${notReadyEvents.length} waiting`);
1460
+ this.debug(
1461
+ `Flushing event queue: ${readyEvents.length} ready, ${notReadyEvents.length} waiting`
1462
+ );
1321
1463
  this.eventQueue = notReadyEvents;
1322
1464
  for (const event of readyEvents) {
1323
1465
  try {
@@ -1382,7 +1524,10 @@ var Schematic = class {
1382
1524
  } catch (error) {
1383
1525
  const retryCount = (event.retry_count ?? 0) + 1;
1384
1526
  if (retryCount <= this.maxEventRetries) {
1385
- this.debug(`Event failed to send (attempt ${retryCount}/${this.maxEventRetries}), queueing for retry:`, error);
1527
+ this.debug(
1528
+ `Event failed to send (attempt ${retryCount}/${this.maxEventRetries}), queueing for retry:`,
1529
+ error
1530
+ );
1386
1531
  const baseDelay = this.eventRetryInitialDelay * Math.pow(2, retryCount - 1);
1387
1532
  const jitterDelay = Math.min(baseDelay, this.eventRetryMaxDelay);
1388
1533
  const nextRetryAt = Date.now() + jitterDelay;
@@ -1393,15 +1538,22 @@ var Schematic = class {
1393
1538
  };
1394
1539
  if (this.eventQueue.length < this.maxEventQueueSize) {
1395
1540
  this.eventQueue.push(retryEvent);
1396
- this.debug(`Event queued for retry in ${jitterDelay}ms (${this.eventQueue.length}/${this.maxEventQueueSize})`);
1541
+ this.debug(
1542
+ `Event queued for retry in ${jitterDelay}ms (${this.eventQueue.length}/${this.maxEventQueueSize})`
1543
+ );
1397
1544
  } else {
1398
- this.debug(`Event queue full (${this.maxEventQueueSize}), dropping oldest event`);
1545
+ this.debug(
1546
+ `Event queue full (${this.maxEventQueueSize}), dropping oldest event`
1547
+ );
1399
1548
  this.eventQueue.shift();
1400
1549
  this.eventQueue.push(retryEvent);
1401
1550
  }
1402
1551
  this.startRetryTimer();
1403
1552
  } else {
1404
- this.debug(`Event failed permanently after ${this.maxEventRetries} attempts, dropping:`, error);
1553
+ this.debug(
1554
+ `Event failed permanently after ${this.maxEventRetries} attempts, dropping:`,
1555
+ error
1556
+ );
1405
1557
  }
1406
1558
  }
1407
1559
  return Promise.resolve();
@@ -1431,11 +1583,17 @@ var Schematic = class {
1431
1583
  if (this.conn) {
1432
1584
  try {
1433
1585
  const socket = await this.conn;
1586
+ if (this.currentWebSocket === socket) {
1587
+ this.debug(`Cleaning up current websocket tracking`);
1588
+ this.currentWebSocket = null;
1589
+ }
1434
1590
  socket.close();
1435
1591
  } catch (error) {
1436
1592
  console.error("Error during cleanup:", error);
1437
1593
  } finally {
1438
1594
  this.conn = null;
1595
+ this.currentWebSocket = null;
1596
+ this.isConnecting = false;
1439
1597
  }
1440
1598
  }
1441
1599
  };
@@ -1460,7 +1618,9 @@ var Schematic = class {
1460
1618
  if (this.conn !== null) {
1461
1619
  try {
1462
1620
  const socket = await this.conn;
1463
- socket.close();
1621
+ if (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING) {
1622
+ socket.close();
1623
+ }
1464
1624
  } catch (error) {
1465
1625
  this.debug("Error closing connection on offline:", error);
1466
1626
  }
@@ -1475,7 +1635,9 @@ var Schematic = class {
1475
1635
  * Handle browser coming back online
1476
1636
  */
1477
1637
  handleNetworkOnline = () => {
1478
- this.debug("Network online, attempting reconnection and flushing queued events");
1638
+ this.debug(
1639
+ "Network online, attempting reconnection and flushing queued events"
1640
+ );
1479
1641
  this.wsReconnectAttempts = 0;
1480
1642
  if (this.wsReconnectTimer !== null) {
1481
1643
  clearTimeout(this.wsReconnectTimer);
@@ -1498,7 +1660,10 @@ var Schematic = class {
1498
1660
  return;
1499
1661
  }
1500
1662
  if (this.wsReconnectTimer !== null) {
1501
- clearTimeout(this.wsReconnectTimer);
1663
+ this.debug(
1664
+ `Reconnection attempt already scheduled, ignoring duplicate request`
1665
+ );
1666
+ return;
1502
1667
  }
1503
1668
  const delay = this.calculateReconnectDelay();
1504
1669
  this.debug(
@@ -1511,23 +1676,57 @@ var Schematic = class {
1511
1676
  `Attempting to reconnect (attempt ${this.wsReconnectAttempts}/${this.webSocketMaxReconnectAttempts})`
1512
1677
  );
1513
1678
  try {
1514
- this.conn = this.wsConnect();
1515
- const socket = await this.conn;
1516
- this.debug(`Reconnection context check:`, {
1517
- hasCompany: this.context.company !== void 0,
1518
- hasUser: this.context.user !== void 0,
1519
- context: this.context
1520
- });
1521
- if (this.context.company !== void 0 || this.context.user !== void 0) {
1522
- this.debug(`Reconnected, force re-sending context`);
1523
- await this.wsSendContextAfterReconnection(socket, this.context);
1524
- } else {
1525
- this.debug(`No context to re-send after reconnection - websocket ready for new context`);
1679
+ if (this.conn !== null) {
1680
+ this.debug(`Cleaning up existing connection before reconnection`);
1681
+ try {
1682
+ const existingSocket = await this.conn;
1683
+ if (this.currentWebSocket === existingSocket) {
1684
+ this.debug(`Existing websocket is current, will be replaced`);
1685
+ this.currentWebSocket = null;
1686
+ }
1687
+ if (existingSocket.readyState === WebSocket.OPEN || existingSocket.readyState === WebSocket.CONNECTING) {
1688
+ existingSocket.close();
1689
+ }
1690
+ } catch (error) {
1691
+ this.debug(`Error cleaning up existing connection:`, error);
1692
+ }
1693
+ this.conn = null;
1694
+ this.currentWebSocket = null;
1695
+ this.isConnecting = false;
1696
+ }
1697
+ this.isConnecting = true;
1698
+ try {
1699
+ this.conn = this.wsConnect();
1700
+ const socket = await this.conn;
1701
+ this.isConnecting = false;
1702
+ this.debug(`Reconnection context check:`, {
1703
+ hasCompany: this.context.company !== void 0,
1704
+ hasUser: this.context.user !== void 0,
1705
+ context: this.context
1706
+ });
1707
+ if (this.context.company !== void 0 || this.context.user !== void 0) {
1708
+ this.debug(`Reconnected, force re-sending context`);
1709
+ await this.wsSendMessage(socket, this.context, true);
1710
+ } else {
1711
+ this.debug(
1712
+ `No context to re-send after reconnection - websocket ready for new context`
1713
+ );
1714
+ this.debug(
1715
+ `Setting up tracking for reconnected websocket (no context to send)`
1716
+ );
1717
+ this.currentWebSocket = socket;
1718
+ }
1719
+ this.flushEventQueue().catch((error) => {
1720
+ this.debug(
1721
+ "Error flushing event queue after websocket reconnection:",
1722
+ error
1723
+ );
1724
+ });
1725
+ this.debug(`Reconnection successful`);
1726
+ } catch (error) {
1727
+ this.isConnecting = false;
1728
+ throw error;
1526
1729
  }
1527
- this.flushEventQueue().catch((error) => {
1528
- this.debug("Error flushing event queue after websocket reconnection:", error);
1529
- });
1530
- this.debug(`Reconnection successful`);
1531
1730
  } catch (error) {
1532
1731
  this.debug(`Reconnection attempt failed:`, error);
1533
1732
  }
@@ -1545,6 +1744,8 @@ var Schematic = class {
1545
1744
  const wsUrl = `${this.webSocketUrl}/flags/bootstrap?apiKey=${this.apiKey}`;
1546
1745
  this.debug(`connecting to WebSocket:`, wsUrl);
1547
1746
  const webSocket = new WebSocket(wsUrl);
1747
+ const connectionId = Math.random().toString(36).substring(7);
1748
+ this.debug(`Creating WebSocket connection ${connectionId} to ${wsUrl}`);
1548
1749
  let timeoutId = null;
1549
1750
  let isResolved = false;
1550
1751
  timeoutId = setTimeout(() => {
@@ -1563,7 +1764,7 @@ var Schematic = class {
1563
1764
  }
1564
1765
  this.wsReconnectAttempts = 0;
1565
1766
  this.wsIntentionalDisconnect = false;
1566
- this.debug(`WebSocket connection opened`);
1767
+ this.debug(`WebSocket connection ${connectionId} opened successfully`);
1567
1768
  resolve(webSocket);
1568
1769
  };
1569
1770
  webSocket.onerror = (error) => {
@@ -1571,7 +1772,7 @@ var Schematic = class {
1571
1772
  if (timeoutId !== null) {
1572
1773
  clearTimeout(timeoutId);
1573
1774
  }
1574
- this.debug(`WebSocket connection error:`, error);
1775
+ this.debug(`WebSocket connection ${connectionId} error:`, error);
1575
1776
  reject(error);
1576
1777
  };
1577
1778
  webSocket.onclose = () => {
@@ -1579,121 +1780,48 @@ var Schematic = class {
1579
1780
  if (timeoutId !== null) {
1580
1781
  clearTimeout(timeoutId);
1581
1782
  }
1582
- this.debug(`WebSocket connection closed`);
1783
+ this.debug(`WebSocket connection ${connectionId} closed`);
1583
1784
  this.conn = null;
1785
+ if (this.currentWebSocket === webSocket) {
1786
+ this.currentWebSocket = null;
1787
+ this.isConnecting = false;
1788
+ }
1584
1789
  if (!this.wsIntentionalDisconnect && this.webSocketReconnect) {
1585
1790
  this.attemptReconnect();
1586
1791
  }
1587
1792
  };
1588
1793
  });
1589
1794
  };
1590
- // Send a message on the websocket after reconnection, forcing the send even if context appears unchanged
1591
- // because the server has lost all state and needs the initial context
1592
- wsSendContextAfterReconnection = (socket, context) => {
1593
- if (this.isOffline()) {
1594
- this.debug("wsSendContextAfterReconnection: skipped (offline mode)");
1595
- this.setIsPending(false);
1596
- return Promise.resolve();
1597
- }
1598
- return new Promise((resolve) => {
1599
- this.debug(`WebSocket force sending context after reconnection:`, context);
1600
- this.context = context;
1601
- const sendMessage = () => {
1602
- let resolved = false;
1603
- const messageHandler = (event) => {
1604
- const message = JSON.parse(event.data);
1605
- this.debug(`WebSocket message received after reconnection:`, message);
1606
- if (!(contextString(context) in this.checks)) {
1607
- this.checks[contextString(context)] = {};
1608
- }
1609
- (message.flags ?? []).forEach((flag) => {
1610
- const flagCheck = CheckFlagReturnFromJSON(flag);
1611
- const contextStr = contextString(context);
1612
- if (this.checks[contextStr] === void 0) {
1613
- this.checks[contextStr] = {};
1614
- }
1615
- this.checks[contextStr][flagCheck.flag] = flagCheck;
1616
- });
1617
- this.useWebSocket = true;
1618
- socket.removeEventListener("message", messageHandler);
1619
- if (!resolved) {
1620
- resolved = true;
1621
- resolve(this.setIsPending(false));
1622
- }
1623
- };
1624
- socket.addEventListener("message", messageHandler);
1625
- const clientVersion = this.additionalHeaders["X-Schematic-Client-Version"] ?? `schematic-js@${version}`;
1626
- const messagePayload = {
1627
- apiKey: this.apiKey,
1628
- clientVersion,
1629
- data: context
1630
- };
1631
- this.debug(`WebSocket sending forced message after reconnection:`, messagePayload);
1632
- socket.send(JSON.stringify(messagePayload));
1633
- };
1634
- if (socket.readyState === WebSocket.OPEN) {
1635
- this.debug(`WebSocket already open, sending forced message after reconnection`);
1636
- sendMessage();
1637
- } else {
1638
- socket.addEventListener("open", () => {
1639
- this.debug(`WebSocket opened, sending forced message after reconnection`);
1640
- sendMessage();
1641
- });
1642
- }
1643
- });
1644
- };
1645
1795
  // Send a message on the websocket indicating interest in a particular evaluation context
1646
1796
  // and wait for the initial set of flag values to be returned
1647
- wsSendMessage = (socket, context) => {
1797
+ wsSendMessage = (socket, context, forceContextSend = false) => {
1648
1798
  if (this.isOffline()) {
1649
1799
  this.debug("wsSendMessage: skipped (offline mode)");
1650
1800
  this.setIsPending(false);
1651
1801
  return Promise.resolve();
1652
1802
  }
1653
1803
  return new Promise((resolve, reject) => {
1654
- if (contextString(context) == contextString(this.context)) {
1804
+ if (!forceContextSend && contextString(context) == contextString(this.context)) {
1655
1805
  this.debug(`WebSocket context unchanged, skipping update`);
1656
1806
  return resolve(this.setIsPending(false));
1657
1807
  }
1658
- this.debug(`WebSocket context updated:`, context);
1808
+ this.debug(
1809
+ forceContextSend ? `WebSocket force sending context (reconnection):` : `WebSocket context updated:`,
1810
+ context
1811
+ );
1659
1812
  this.context = context;
1660
1813
  const sendMessage = () => {
1661
1814
  let resolved = false;
1815
+ const persistentMessageHandler = this.createPersistentMessageHandler(context);
1662
1816
  const messageHandler = (event) => {
1663
- const message = JSON.parse(event.data);
1664
- this.debug(`WebSocket message received:`, message);
1665
- if (!(contextString(context) in this.checks)) {
1666
- this.checks[contextString(context)] = {};
1667
- }
1668
- (message.flags ?? []).forEach((flag) => {
1669
- const flagCheck = CheckFlagReturnFromJSON(flag);
1670
- const contextStr = contextString(context);
1671
- if (this.checks[contextStr] === void 0) {
1672
- this.checks[contextStr] = {};
1673
- }
1674
- this.checks[contextStr][flagCheck.flag] = flagCheck;
1675
- this.debug(`WebSocket flag update:`, {
1676
- flag: flagCheck.flag,
1677
- value: flagCheck.value,
1678
- flagCheck
1679
- });
1680
- if (typeof flagCheck.featureUsageEvent === "string") {
1681
- this.updateFeatureUsageEventMap(flagCheck);
1682
- }
1683
- if (this.flagCheckListeners[flag.flag]?.size > 0 || this.flagValueListeners[flag.flag]?.size > 0) {
1684
- this.submitFlagCheckEvent(flagCheck.flag, flagCheck, context);
1685
- }
1686
- this.notifyFlagCheckListeners(flag.flag, flagCheck);
1687
- this.notifyFlagValueListeners(flag.flag, flagCheck.value);
1688
- });
1689
- this.flushContextDependentEventQueue();
1690
- this.setIsPending(false);
1817
+ persistentMessageHandler(event);
1691
1818
  if (!resolved) {
1692
1819
  resolved = true;
1693
1820
  resolve();
1694
1821
  }
1695
1822
  };
1696
1823
  socket.addEventListener("message", messageHandler);
1824
+ this.currentWebSocket = socket;
1697
1825
  const clientVersion = this.additionalHeaders["X-Schematic-Client-Version"] ?? `schematic-js@${version}`;
1698
1826
  const messagePayload = {
1699
1827
  apiKey: this.apiKey,
@@ -1801,7 +1929,17 @@ var Schematic = class {
1801
1929
  { value }
1802
1930
  );
1803
1931
  }
1804
- listeners.forEach((listener) => notifyFlagValueListener(listener, value));
1932
+ listeners.forEach((listener, index) => {
1933
+ this.debug(`Calling listener ${index} for flag ${flagKey}`, {
1934
+ flagKey,
1935
+ value
1936
+ });
1937
+ notifyFlagValueListener(listener, value);
1938
+ this.debug(`Listener ${index} for flag ${flagKey} completed`, {
1939
+ flagKey,
1940
+ value
1941
+ });
1942
+ });
1805
1943
  };
1806
1944
  };
1807
1945
  var notifyPendingListener = (listener, value) => {
@@ -1830,7 +1968,7 @@ var notifyFlagValueListener = (listener, value) => {
1830
1968
  import React, { createContext, useEffect, useMemo, useRef } from "react";
1831
1969
 
1832
1970
  // src/version.ts
1833
- var version2 = "1.2.8";
1971
+ var version2 = "1.2.10";
1834
1972
 
1835
1973
  // src/context/schematic.tsx
1836
1974
  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.8",
3
+ "version": "1.2.10",
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.8"
34
+ "@schematichq/schematic-js": "^1.2.10"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@eslint/js": "^9.39.1",