@schematichq/schematic-js 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.
@@ -781,7 +781,7 @@ function contextString(context) {
781
781
  }
782
782
 
783
783
  // src/version.ts
784
- var version = "1.2.8";
784
+ var version = "1.2.10";
785
785
 
786
786
  // src/index.ts
787
787
  var anonymousIdKey = "schematicId";
@@ -813,6 +813,8 @@ var Schematic = class {
813
813
  wsReconnectAttempts = 0;
814
814
  wsReconnectTimer = null;
815
815
  wsIntentionalDisconnect = false;
816
+ currentWebSocket = null;
817
+ isConnecting = false;
816
818
  maxEventQueueSize = 100;
817
819
  // Prevent memory issues with very long network outages
818
820
  maxEventRetries = 5;
@@ -822,6 +824,8 @@ var Schematic = class {
822
824
  eventRetryMaxDelay = 3e4;
823
825
  // Maximum retry delay in ms
824
826
  retryTimer = null;
827
+ flagValueDefaults = {};
828
+ flagCheckDefaults = {};
825
829
  constructor(apiKey, options) {
826
830
  this.apiKey = apiKey;
827
831
  this.eventQueue = [];
@@ -892,6 +896,12 @@ var Schematic = class {
892
896
  if (options?.eventRetryMaxDelay !== void 0) {
893
897
  this.eventRetryMaxDelay = options.eventRetryMaxDelay;
894
898
  }
899
+ if (options?.flagValueDefaults !== void 0) {
900
+ this.flagValueDefaults = options.flagValueDefaults;
901
+ }
902
+ if (options?.flagCheckDefaults !== void 0) {
903
+ this.flagCheckDefaults = options.flagCheckDefaults;
904
+ }
895
905
  if (typeof window !== "undefined" && window?.addEventListener) {
896
906
  window.addEventListener("beforeunload", () => {
897
907
  this.flushEventQueue();
@@ -916,6 +926,62 @@ var Schematic = class {
916
926
  this.debug("Initialized with debug mode enabled");
917
927
  }
918
928
  }
929
+ /**
930
+ * Resolve fallback value according to priority order:
931
+ * 1. Callsite fallback value (if provided)
932
+ * 2. Initialization fallback value (flagValueDefaults)
933
+ * 3. Default to false
934
+ */
935
+ resolveFallbackValue(key, callsiteFallback) {
936
+ if (callsiteFallback !== void 0) {
937
+ return callsiteFallback;
938
+ }
939
+ if (key in this.flagValueDefaults) {
940
+ return this.flagValueDefaults[key];
941
+ }
942
+ return false;
943
+ }
944
+ /**
945
+ * Resolve complete CheckFlagReturn object according to priority order:
946
+ * 1. Use callsite fallback for boolean value, construct CheckFlagReturn
947
+ * 2. Use flagCheckDefaults if available for this flag
948
+ * 3. Use flagValueDefaults if available for this flag, construct CheckFlagReturn
949
+ * 4. Default CheckFlagReturn with value: false
950
+ */
951
+ resolveFallbackCheckFlagReturn(key, callsiteFallback, reason = "Fallback value used", error) {
952
+ if (callsiteFallback !== void 0) {
953
+ return {
954
+ flag: key,
955
+ value: callsiteFallback,
956
+ reason,
957
+ error
958
+ };
959
+ }
960
+ if (key in this.flagCheckDefaults) {
961
+ const defaultReturn = this.flagCheckDefaults[key];
962
+ return {
963
+ ...defaultReturn,
964
+ flag: key,
965
+ // Ensure flag matches the requested key
966
+ reason: error !== void 0 ? reason : defaultReturn.reason,
967
+ error
968
+ };
969
+ }
970
+ if (key in this.flagValueDefaults) {
971
+ return {
972
+ flag: key,
973
+ value: this.flagValueDefaults[key],
974
+ reason,
975
+ error
976
+ };
977
+ }
978
+ return {
979
+ flag: key,
980
+ value: false,
981
+ reason,
982
+ error
983
+ };
984
+ }
919
985
  /**
920
986
  * Get value for a single flag.
921
987
  * In WebSocket mode, returns cached values if connection is active, otherwise establishes
@@ -924,16 +990,21 @@ var Schematic = class {
924
990
  * In REST mode, makes an API call for each check.
925
991
  */
926
992
  async checkFlag(options) {
927
- const { fallback = false, key } = options;
993
+ const { fallback, key } = options;
928
994
  const context = options.context || this.context;
929
995
  const contextStr = contextString(context);
930
996
  this.debug(`checkFlag: ${key}`, { context, fallback });
931
997
  if (this.isOffline()) {
998
+ const resolvedFallbackResult = this.resolveFallbackCheckFlagReturn(
999
+ key,
1000
+ fallback,
1001
+ "Offline mode - using initialization defaults"
1002
+ );
932
1003
  this.debug(`checkFlag offline result: ${key}`, {
933
- value: fallback,
1004
+ value: resolvedFallbackResult.value,
934
1005
  offlineMode: true
935
1006
  });
936
- return fallback;
1007
+ return resolvedFallbackResult.value;
937
1008
  }
938
1009
  if (!this.useWebSocket) {
939
1010
  const requestUrl = `${this.apiUrl}/flags/${key}/check`;
@@ -961,14 +1032,14 @@ var Schematic = class {
961
1032
  return result.value;
962
1033
  }).catch((error) => {
963
1034
  console.error("There was a problem with the fetch operation:", error);
964
- const errorResult = {
965
- flag: key,
966
- value: fallback,
967
- reason: "API request failed",
968
- error: error instanceof Error ? error.message : String(error)
969
- };
1035
+ const errorResult = this.resolveFallbackCheckFlagReturn(
1036
+ key,
1037
+ fallback,
1038
+ "API request failed",
1039
+ error instanceof Error ? error.message : String(error)
1040
+ );
970
1041
  this.submitFlagCheckEvent(key, errorResult, context);
971
- return fallback;
1042
+ return errorResult.value;
972
1043
  });
973
1044
  }
974
1045
  try {
@@ -978,7 +1049,7 @@ var Schematic = class {
978
1049
  return existingVals[key].value;
979
1050
  }
980
1051
  if (this.isOffline()) {
981
- return fallback;
1052
+ return this.resolveFallbackValue(key, fallback);
982
1053
  }
983
1054
  try {
984
1055
  await this.setContext(context);
@@ -991,10 +1062,10 @@ var Schematic = class {
991
1062
  }
992
1063
  const contextVals = this.checks[contextStr] ?? {};
993
1064
  const flagCheck = contextVals[key];
994
- const result = flagCheck?.value ?? fallback;
1065
+ const result = flagCheck?.value ?? this.resolveFallbackValue(key, fallback);
995
1066
  this.debug(
996
1067
  `checkFlag WebSocket result: ${key}`,
997
- typeof flagCheck !== "undefined" ? flagCheck : { value: fallback, fallbackUsed: true }
1068
+ typeof flagCheck !== "undefined" ? flagCheck : { value: result, fallbackUsed: true }
998
1069
  );
999
1070
  if (typeof flagCheck !== "undefined") {
1000
1071
  this.submitFlagCheckEvent(key, flagCheck, context);
@@ -1002,14 +1073,14 @@ var Schematic = class {
1002
1073
  return result;
1003
1074
  } catch (error) {
1004
1075
  console.error("Unexpected error in checkFlag:", error);
1005
- const errorResult = {
1006
- flag: key,
1007
- value: fallback,
1008
- reason: "Unexpected error in flag check",
1009
- error: error instanceof Error ? error.message : String(error)
1010
- };
1076
+ const errorResult = this.resolveFallbackCheckFlagReturn(
1077
+ key,
1078
+ fallback,
1079
+ "Unexpected error in flag check",
1080
+ error instanceof Error ? error.message : String(error)
1081
+ );
1011
1082
  this.submitFlagCheckEvent(key, errorResult, context);
1012
- return fallback;
1083
+ return errorResult.value;
1013
1084
  }
1014
1085
  }
1015
1086
  /**
@@ -1021,6 +1092,49 @@ var Schematic = class {
1021
1092
  console.log(`[Schematic] ${message}`, ...args);
1022
1093
  }
1023
1094
  }
1095
+ /**
1096
+ * Create a persistent message handler for websocket flag updates
1097
+ */
1098
+ createPersistentMessageHandler(context) {
1099
+ return (event) => {
1100
+ const message = JSON.parse(event.data);
1101
+ this.debug(`WebSocket persistent message received:`, message);
1102
+ if (!(contextString(context) in this.checks)) {
1103
+ this.checks[contextString(context)] = {};
1104
+ }
1105
+ (message.flags ?? []).forEach((flag) => {
1106
+ const flagCheck = CheckFlagReturnFromJSON(flag);
1107
+ const contextStr = contextString(context);
1108
+ if (this.checks[contextStr] === void 0) {
1109
+ this.checks[contextStr] = {};
1110
+ }
1111
+ this.checks[contextStr][flagCheck.flag] = flagCheck;
1112
+ this.debug(`WebSocket flag update:`, {
1113
+ flag: flagCheck.flag,
1114
+ value: flagCheck.value,
1115
+ flagCheck
1116
+ });
1117
+ if (typeof flagCheck.featureUsageEvent === "string") {
1118
+ this.updateFeatureUsageEventMap(flagCheck);
1119
+ }
1120
+ if ((this.flagCheckListeners[flag.flag]?.size ?? 0) > 0 || (this.flagValueListeners[flag.flag]?.size ?? 0) > 0) {
1121
+ this.submitFlagCheckEvent(flagCheck.flag, flagCheck, context);
1122
+ }
1123
+ this.debug(`About to notify listeners for flag ${flag.flag}`, {
1124
+ flag: flag.flag,
1125
+ value: flagCheck.value
1126
+ });
1127
+ this.notifyFlagCheckListeners(flag.flag, flagCheck);
1128
+ this.notifyFlagValueListeners(flag.flag, flagCheck.value);
1129
+ this.debug(`Finished notifying listeners for flag ${flag.flag}`, {
1130
+ flag: flag.flag,
1131
+ value: flagCheck.value
1132
+ });
1133
+ });
1134
+ this.flushContextDependentEventQueue();
1135
+ this.setIsPending(false);
1136
+ };
1137
+ }
1024
1138
  /**
1025
1139
  * Helper function to check if client is in offline mode
1026
1140
  */
@@ -1052,11 +1166,12 @@ var Schematic = class {
1052
1166
  */
1053
1167
  async fallbackToRest(key, context, fallback) {
1054
1168
  if (this.isOffline()) {
1169
+ const resolvedFallback = this.resolveFallbackValue(key, fallback);
1055
1170
  this.debug(`fallbackToRest offline result: ${key}`, {
1056
- value: fallback,
1171
+ value: resolvedFallback,
1057
1172
  offlineMode: true
1058
1173
  });
1059
- return fallback;
1174
+ return resolvedFallback;
1060
1175
  }
1061
1176
  try {
1062
1177
  const requestUrl = `${this.apiUrl}/flags/${key}/check`;
@@ -1083,14 +1198,14 @@ var Schematic = class {
1083
1198
  return result.value;
1084
1199
  } catch (error) {
1085
1200
  console.error("REST API call failed, using fallback value:", error);
1086
- const errorResult = {
1087
- flag: key,
1088
- value: fallback,
1089
- reason: "API request failed (fallback)",
1090
- error: error instanceof Error ? error.message : String(error)
1091
- };
1201
+ const errorResult = this.resolveFallbackCheckFlagReturn(
1202
+ key,
1203
+ fallback,
1204
+ "API request failed (fallback)",
1205
+ error instanceof Error ? error.message : String(error)
1206
+ );
1092
1207
  this.submitFlagCheckEvent(key, errorResult, context);
1093
- return fallback;
1208
+ return errorResult.value;
1094
1209
  }
1095
1210
  }
1096
1211
  /**
@@ -1171,6 +1286,19 @@ var Schematic = class {
1171
1286
  try {
1172
1287
  this.setIsPending(true);
1173
1288
  if (!this.conn) {
1289
+ if (this.isConnecting) {
1290
+ this.debug(
1291
+ `Connection already in progress, waiting for it to complete`
1292
+ );
1293
+ while (this.isConnecting && this.conn === null) {
1294
+ await new Promise((resolve) => setTimeout(resolve, 10));
1295
+ }
1296
+ if (this.conn !== null) {
1297
+ const socket2 = await this.conn;
1298
+ await this.wsSendMessage(socket2, context);
1299
+ return;
1300
+ }
1301
+ }
1174
1302
  if (this.wsReconnectTimer !== null) {
1175
1303
  this.debug(
1176
1304
  `Cancelling scheduled reconnection, connecting immediately`
@@ -1178,7 +1306,17 @@ var Schematic = class {
1178
1306
  clearTimeout(this.wsReconnectTimer);
1179
1307
  this.wsReconnectTimer = null;
1180
1308
  }
1181
- this.conn = this.wsConnect();
1309
+ this.isConnecting = true;
1310
+ try {
1311
+ this.conn = this.wsConnect();
1312
+ const socket2 = await this.conn;
1313
+ this.isConnecting = false;
1314
+ await this.wsSendMessage(socket2, context);
1315
+ return;
1316
+ } catch (error) {
1317
+ this.isConnecting = false;
1318
+ throw error;
1319
+ }
1182
1320
  }
1183
1321
  const socket = await this.conn;
1184
1322
  await this.wsSendMessage(socket, context);
@@ -1343,10 +1481,14 @@ var Schematic = class {
1343
1481
  }
1344
1482
  }
1345
1483
  if (readyEvents.length === 0) {
1346
- this.debug(`No events ready for retry yet (${notReadyEvents.length} still in backoff)`);
1484
+ this.debug(
1485
+ `No events ready for retry yet (${notReadyEvents.length} still in backoff)`
1486
+ );
1347
1487
  return;
1348
1488
  }
1349
- this.debug(`Flushing event queue: ${readyEvents.length} ready, ${notReadyEvents.length} waiting`);
1489
+ this.debug(
1490
+ `Flushing event queue: ${readyEvents.length} ready, ${notReadyEvents.length} waiting`
1491
+ );
1350
1492
  this.eventQueue = notReadyEvents;
1351
1493
  for (const event of readyEvents) {
1352
1494
  try {
@@ -1411,7 +1553,10 @@ var Schematic = class {
1411
1553
  } catch (error) {
1412
1554
  const retryCount = (event.retry_count ?? 0) + 1;
1413
1555
  if (retryCount <= this.maxEventRetries) {
1414
- this.debug(`Event failed to send (attempt ${retryCount}/${this.maxEventRetries}), queueing for retry:`, error);
1556
+ this.debug(
1557
+ `Event failed to send (attempt ${retryCount}/${this.maxEventRetries}), queueing for retry:`,
1558
+ error
1559
+ );
1415
1560
  const baseDelay = this.eventRetryInitialDelay * Math.pow(2, retryCount - 1);
1416
1561
  const jitterDelay = Math.min(baseDelay, this.eventRetryMaxDelay);
1417
1562
  const nextRetryAt = Date.now() + jitterDelay;
@@ -1422,15 +1567,22 @@ var Schematic = class {
1422
1567
  };
1423
1568
  if (this.eventQueue.length < this.maxEventQueueSize) {
1424
1569
  this.eventQueue.push(retryEvent);
1425
- this.debug(`Event queued for retry in ${jitterDelay}ms (${this.eventQueue.length}/${this.maxEventQueueSize})`);
1570
+ this.debug(
1571
+ `Event queued for retry in ${jitterDelay}ms (${this.eventQueue.length}/${this.maxEventQueueSize})`
1572
+ );
1426
1573
  } else {
1427
- this.debug(`Event queue full (${this.maxEventQueueSize}), dropping oldest event`);
1574
+ this.debug(
1575
+ `Event queue full (${this.maxEventQueueSize}), dropping oldest event`
1576
+ );
1428
1577
  this.eventQueue.shift();
1429
1578
  this.eventQueue.push(retryEvent);
1430
1579
  }
1431
1580
  this.startRetryTimer();
1432
1581
  } else {
1433
- this.debug(`Event failed permanently after ${this.maxEventRetries} attempts, dropping:`, error);
1582
+ this.debug(
1583
+ `Event failed permanently after ${this.maxEventRetries} attempts, dropping:`,
1584
+ error
1585
+ );
1434
1586
  }
1435
1587
  }
1436
1588
  return Promise.resolve();
@@ -1460,11 +1612,17 @@ var Schematic = class {
1460
1612
  if (this.conn) {
1461
1613
  try {
1462
1614
  const socket = await this.conn;
1615
+ if (this.currentWebSocket === socket) {
1616
+ this.debug(`Cleaning up current websocket tracking`);
1617
+ this.currentWebSocket = null;
1618
+ }
1463
1619
  socket.close();
1464
1620
  } catch (error) {
1465
1621
  console.error("Error during cleanup:", error);
1466
1622
  } finally {
1467
1623
  this.conn = null;
1624
+ this.currentWebSocket = null;
1625
+ this.isConnecting = false;
1468
1626
  }
1469
1627
  }
1470
1628
  };
@@ -1489,7 +1647,9 @@ var Schematic = class {
1489
1647
  if (this.conn !== null) {
1490
1648
  try {
1491
1649
  const socket = await this.conn;
1492
- socket.close();
1650
+ if (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING) {
1651
+ socket.close();
1652
+ }
1493
1653
  } catch (error) {
1494
1654
  this.debug("Error closing connection on offline:", error);
1495
1655
  }
@@ -1504,7 +1664,9 @@ var Schematic = class {
1504
1664
  * Handle browser coming back online
1505
1665
  */
1506
1666
  handleNetworkOnline = () => {
1507
- this.debug("Network online, attempting reconnection and flushing queued events");
1667
+ this.debug(
1668
+ "Network online, attempting reconnection and flushing queued events"
1669
+ );
1508
1670
  this.wsReconnectAttempts = 0;
1509
1671
  if (this.wsReconnectTimer !== null) {
1510
1672
  clearTimeout(this.wsReconnectTimer);
@@ -1527,7 +1689,10 @@ var Schematic = class {
1527
1689
  return;
1528
1690
  }
1529
1691
  if (this.wsReconnectTimer !== null) {
1530
- clearTimeout(this.wsReconnectTimer);
1692
+ this.debug(
1693
+ `Reconnection attempt already scheduled, ignoring duplicate request`
1694
+ );
1695
+ return;
1531
1696
  }
1532
1697
  const delay = this.calculateReconnectDelay();
1533
1698
  this.debug(
@@ -1540,23 +1705,57 @@ var Schematic = class {
1540
1705
  `Attempting to reconnect (attempt ${this.wsReconnectAttempts}/${this.webSocketMaxReconnectAttempts})`
1541
1706
  );
1542
1707
  try {
1543
- this.conn = this.wsConnect();
1544
- const socket = await this.conn;
1545
- this.debug(`Reconnection context check:`, {
1546
- hasCompany: this.context.company !== void 0,
1547
- hasUser: this.context.user !== void 0,
1548
- context: this.context
1549
- });
1550
- if (this.context.company !== void 0 || this.context.user !== void 0) {
1551
- this.debug(`Reconnected, force re-sending context`);
1552
- await this.wsSendContextAfterReconnection(socket, this.context);
1553
- } else {
1554
- this.debug(`No context to re-send after reconnection - websocket ready for new context`);
1708
+ if (this.conn !== null) {
1709
+ this.debug(`Cleaning up existing connection before reconnection`);
1710
+ try {
1711
+ const existingSocket = await this.conn;
1712
+ if (this.currentWebSocket === existingSocket) {
1713
+ this.debug(`Existing websocket is current, will be replaced`);
1714
+ this.currentWebSocket = null;
1715
+ }
1716
+ if (existingSocket.readyState === WebSocket.OPEN || existingSocket.readyState === WebSocket.CONNECTING) {
1717
+ existingSocket.close();
1718
+ }
1719
+ } catch (error) {
1720
+ this.debug(`Error cleaning up existing connection:`, error);
1721
+ }
1722
+ this.conn = null;
1723
+ this.currentWebSocket = null;
1724
+ this.isConnecting = false;
1725
+ }
1726
+ this.isConnecting = true;
1727
+ try {
1728
+ this.conn = this.wsConnect();
1729
+ const socket = await this.conn;
1730
+ this.isConnecting = false;
1731
+ this.debug(`Reconnection context check:`, {
1732
+ hasCompany: this.context.company !== void 0,
1733
+ hasUser: this.context.user !== void 0,
1734
+ context: this.context
1735
+ });
1736
+ if (this.context.company !== void 0 || this.context.user !== void 0) {
1737
+ this.debug(`Reconnected, force re-sending context`);
1738
+ await this.wsSendMessage(socket, this.context, true);
1739
+ } else {
1740
+ this.debug(
1741
+ `No context to re-send after reconnection - websocket ready for new context`
1742
+ );
1743
+ this.debug(
1744
+ `Setting up tracking for reconnected websocket (no context to send)`
1745
+ );
1746
+ this.currentWebSocket = socket;
1747
+ }
1748
+ this.flushEventQueue().catch((error) => {
1749
+ this.debug(
1750
+ "Error flushing event queue after websocket reconnection:",
1751
+ error
1752
+ );
1753
+ });
1754
+ this.debug(`Reconnection successful`);
1755
+ } catch (error) {
1756
+ this.isConnecting = false;
1757
+ throw error;
1555
1758
  }
1556
- this.flushEventQueue().catch((error) => {
1557
- this.debug("Error flushing event queue after websocket reconnection:", error);
1558
- });
1559
- this.debug(`Reconnection successful`);
1560
1759
  } catch (error) {
1561
1760
  this.debug(`Reconnection attempt failed:`, error);
1562
1761
  }
@@ -1574,6 +1773,8 @@ var Schematic = class {
1574
1773
  const wsUrl = `${this.webSocketUrl}/flags/bootstrap?apiKey=${this.apiKey}`;
1575
1774
  this.debug(`connecting to WebSocket:`, wsUrl);
1576
1775
  const webSocket = new WebSocket(wsUrl);
1776
+ const connectionId = Math.random().toString(36).substring(7);
1777
+ this.debug(`Creating WebSocket connection ${connectionId} to ${wsUrl}`);
1577
1778
  let timeoutId = null;
1578
1779
  let isResolved = false;
1579
1780
  timeoutId = setTimeout(() => {
@@ -1592,7 +1793,7 @@ var Schematic = class {
1592
1793
  }
1593
1794
  this.wsReconnectAttempts = 0;
1594
1795
  this.wsIntentionalDisconnect = false;
1595
- this.debug(`WebSocket connection opened`);
1796
+ this.debug(`WebSocket connection ${connectionId} opened successfully`);
1596
1797
  resolve(webSocket);
1597
1798
  };
1598
1799
  webSocket.onerror = (error) => {
@@ -1600,7 +1801,7 @@ var Schematic = class {
1600
1801
  if (timeoutId !== null) {
1601
1802
  clearTimeout(timeoutId);
1602
1803
  }
1603
- this.debug(`WebSocket connection error:`, error);
1804
+ this.debug(`WebSocket connection ${connectionId} error:`, error);
1604
1805
  reject(error);
1605
1806
  };
1606
1807
  webSocket.onclose = () => {
@@ -1608,121 +1809,48 @@ var Schematic = class {
1608
1809
  if (timeoutId !== null) {
1609
1810
  clearTimeout(timeoutId);
1610
1811
  }
1611
- this.debug(`WebSocket connection closed`);
1812
+ this.debug(`WebSocket connection ${connectionId} closed`);
1612
1813
  this.conn = null;
1814
+ if (this.currentWebSocket === webSocket) {
1815
+ this.currentWebSocket = null;
1816
+ this.isConnecting = false;
1817
+ }
1613
1818
  if (!this.wsIntentionalDisconnect && this.webSocketReconnect) {
1614
1819
  this.attemptReconnect();
1615
1820
  }
1616
1821
  };
1617
1822
  });
1618
1823
  };
1619
- // Send a message on the websocket after reconnection, forcing the send even if context appears unchanged
1620
- // because the server has lost all state and needs the initial context
1621
- wsSendContextAfterReconnection = (socket, context) => {
1622
- if (this.isOffline()) {
1623
- this.debug("wsSendContextAfterReconnection: skipped (offline mode)");
1624
- this.setIsPending(false);
1625
- return Promise.resolve();
1626
- }
1627
- return new Promise((resolve) => {
1628
- this.debug(`WebSocket force sending context after reconnection:`, context);
1629
- this.context = context;
1630
- const sendMessage = () => {
1631
- let resolved = false;
1632
- const messageHandler = (event) => {
1633
- const message = JSON.parse(event.data);
1634
- this.debug(`WebSocket message received after reconnection:`, message);
1635
- if (!(contextString(context) in this.checks)) {
1636
- this.checks[contextString(context)] = {};
1637
- }
1638
- (message.flags ?? []).forEach((flag) => {
1639
- const flagCheck = CheckFlagReturnFromJSON(flag);
1640
- const contextStr = contextString(context);
1641
- if (this.checks[contextStr] === void 0) {
1642
- this.checks[contextStr] = {};
1643
- }
1644
- this.checks[contextStr][flagCheck.flag] = flagCheck;
1645
- });
1646
- this.useWebSocket = true;
1647
- socket.removeEventListener("message", messageHandler);
1648
- if (!resolved) {
1649
- resolved = true;
1650
- resolve(this.setIsPending(false));
1651
- }
1652
- };
1653
- socket.addEventListener("message", messageHandler);
1654
- const clientVersion = this.additionalHeaders["X-Schematic-Client-Version"] ?? `schematic-js@${version}`;
1655
- const messagePayload = {
1656
- apiKey: this.apiKey,
1657
- clientVersion,
1658
- data: context
1659
- };
1660
- this.debug(`WebSocket sending forced message after reconnection:`, messagePayload);
1661
- socket.send(JSON.stringify(messagePayload));
1662
- };
1663
- if (socket.readyState === WebSocket.OPEN) {
1664
- this.debug(`WebSocket already open, sending forced message after reconnection`);
1665
- sendMessage();
1666
- } else {
1667
- socket.addEventListener("open", () => {
1668
- this.debug(`WebSocket opened, sending forced message after reconnection`);
1669
- sendMessage();
1670
- });
1671
- }
1672
- });
1673
- };
1674
1824
  // Send a message on the websocket indicating interest in a particular evaluation context
1675
1825
  // and wait for the initial set of flag values to be returned
1676
- wsSendMessage = (socket, context) => {
1826
+ wsSendMessage = (socket, context, forceContextSend = false) => {
1677
1827
  if (this.isOffline()) {
1678
1828
  this.debug("wsSendMessage: skipped (offline mode)");
1679
1829
  this.setIsPending(false);
1680
1830
  return Promise.resolve();
1681
1831
  }
1682
1832
  return new Promise((resolve, reject) => {
1683
- if (contextString(context) == contextString(this.context)) {
1833
+ if (!forceContextSend && contextString(context) == contextString(this.context)) {
1684
1834
  this.debug(`WebSocket context unchanged, skipping update`);
1685
1835
  return resolve(this.setIsPending(false));
1686
1836
  }
1687
- this.debug(`WebSocket context updated:`, context);
1837
+ this.debug(
1838
+ forceContextSend ? `WebSocket force sending context (reconnection):` : `WebSocket context updated:`,
1839
+ context
1840
+ );
1688
1841
  this.context = context;
1689
1842
  const sendMessage = () => {
1690
1843
  let resolved = false;
1844
+ const persistentMessageHandler = this.createPersistentMessageHandler(context);
1691
1845
  const messageHandler = (event) => {
1692
- const message = JSON.parse(event.data);
1693
- this.debug(`WebSocket message received:`, message);
1694
- if (!(contextString(context) in this.checks)) {
1695
- this.checks[contextString(context)] = {};
1696
- }
1697
- (message.flags ?? []).forEach((flag) => {
1698
- const flagCheck = CheckFlagReturnFromJSON(flag);
1699
- const contextStr = contextString(context);
1700
- if (this.checks[contextStr] === void 0) {
1701
- this.checks[contextStr] = {};
1702
- }
1703
- this.checks[contextStr][flagCheck.flag] = flagCheck;
1704
- this.debug(`WebSocket flag update:`, {
1705
- flag: flagCheck.flag,
1706
- value: flagCheck.value,
1707
- flagCheck
1708
- });
1709
- if (typeof flagCheck.featureUsageEvent === "string") {
1710
- this.updateFeatureUsageEventMap(flagCheck);
1711
- }
1712
- if (this.flagCheckListeners[flag.flag]?.size > 0 || this.flagValueListeners[flag.flag]?.size > 0) {
1713
- this.submitFlagCheckEvent(flagCheck.flag, flagCheck, context);
1714
- }
1715
- this.notifyFlagCheckListeners(flag.flag, flagCheck);
1716
- this.notifyFlagValueListeners(flag.flag, flagCheck.value);
1717
- });
1718
- this.flushContextDependentEventQueue();
1719
- this.setIsPending(false);
1846
+ persistentMessageHandler(event);
1720
1847
  if (!resolved) {
1721
1848
  resolved = true;
1722
1849
  resolve();
1723
1850
  }
1724
1851
  };
1725
1852
  socket.addEventListener("message", messageHandler);
1853
+ this.currentWebSocket = socket;
1726
1854
  const clientVersion = this.additionalHeaders["X-Schematic-Client-Version"] ?? `schematic-js@${version}`;
1727
1855
  const messagePayload = {
1728
1856
  apiKey: this.apiKey,
@@ -1830,7 +1958,17 @@ var Schematic = class {
1830
1958
  { value }
1831
1959
  );
1832
1960
  }
1833
- listeners.forEach((listener) => notifyFlagValueListener(listener, value));
1961
+ listeners.forEach((listener, index) => {
1962
+ this.debug(`Calling listener ${index} for flag ${flagKey}`, {
1963
+ flagKey,
1964
+ value
1965
+ });
1966
+ notifyFlagValueListener(listener, value);
1967
+ this.debug(`Listener ${index} for flag ${flagKey} completed`, {
1968
+ flagKey,
1969
+ value
1970
+ });
1971
+ });
1834
1972
  };
1835
1973
  };
1836
1974
  var notifyPendingListener = (listener, value) => {