@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.
@@ -800,7 +800,7 @@ function contextString(context) {
800
800
  }
801
801
 
802
802
  // src/version.ts
803
- var version = "1.2.8";
803
+ var version = "1.2.10";
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;
@@ -841,6 +843,8 @@ var Schematic = class {
841
843
  eventRetryMaxDelay = 3e4;
842
844
  // Maximum retry delay in ms
843
845
  retryTimer = null;
846
+ flagValueDefaults = {};
847
+ flagCheckDefaults = {};
844
848
  constructor(apiKey, options) {
845
849
  this.apiKey = apiKey;
846
850
  this.eventQueue = [];
@@ -911,6 +915,12 @@ var Schematic = class {
911
915
  if (options?.eventRetryMaxDelay !== void 0) {
912
916
  this.eventRetryMaxDelay = options.eventRetryMaxDelay;
913
917
  }
918
+ if (options?.flagValueDefaults !== void 0) {
919
+ this.flagValueDefaults = options.flagValueDefaults;
920
+ }
921
+ if (options?.flagCheckDefaults !== void 0) {
922
+ this.flagCheckDefaults = options.flagCheckDefaults;
923
+ }
914
924
  if (typeof window !== "undefined" && window?.addEventListener) {
915
925
  window.addEventListener("beforeunload", () => {
916
926
  this.flushEventQueue();
@@ -935,6 +945,62 @@ var Schematic = class {
935
945
  this.debug("Initialized with debug mode enabled");
936
946
  }
937
947
  }
948
+ /**
949
+ * Resolve fallback value according to priority order:
950
+ * 1. Callsite fallback value (if provided)
951
+ * 2. Initialization fallback value (flagValueDefaults)
952
+ * 3. Default to false
953
+ */
954
+ resolveFallbackValue(key, callsiteFallback) {
955
+ if (callsiteFallback !== void 0) {
956
+ return callsiteFallback;
957
+ }
958
+ if (key in this.flagValueDefaults) {
959
+ return this.flagValueDefaults[key];
960
+ }
961
+ return false;
962
+ }
963
+ /**
964
+ * Resolve complete CheckFlagReturn object according to priority order:
965
+ * 1. Use callsite fallback for boolean value, construct CheckFlagReturn
966
+ * 2. Use flagCheckDefaults if available for this flag
967
+ * 3. Use flagValueDefaults if available for this flag, construct CheckFlagReturn
968
+ * 4. Default CheckFlagReturn with value: false
969
+ */
970
+ resolveFallbackCheckFlagReturn(key, callsiteFallback, reason = "Fallback value used", error) {
971
+ if (callsiteFallback !== void 0) {
972
+ return {
973
+ flag: key,
974
+ value: callsiteFallback,
975
+ reason,
976
+ error
977
+ };
978
+ }
979
+ if (key in this.flagCheckDefaults) {
980
+ const defaultReturn = this.flagCheckDefaults[key];
981
+ return {
982
+ ...defaultReturn,
983
+ flag: key,
984
+ // Ensure flag matches the requested key
985
+ reason: error !== void 0 ? reason : defaultReturn.reason,
986
+ error
987
+ };
988
+ }
989
+ if (key in this.flagValueDefaults) {
990
+ return {
991
+ flag: key,
992
+ value: this.flagValueDefaults[key],
993
+ reason,
994
+ error
995
+ };
996
+ }
997
+ return {
998
+ flag: key,
999
+ value: false,
1000
+ reason,
1001
+ error
1002
+ };
1003
+ }
938
1004
  /**
939
1005
  * Get value for a single flag.
940
1006
  * In WebSocket mode, returns cached values if connection is active, otherwise establishes
@@ -943,16 +1009,21 @@ var Schematic = class {
943
1009
  * In REST mode, makes an API call for each check.
944
1010
  */
945
1011
  async checkFlag(options) {
946
- const { fallback = false, key } = options;
1012
+ const { fallback, key } = options;
947
1013
  const context = options.context || this.context;
948
1014
  const contextStr = contextString(context);
949
1015
  this.debug(`checkFlag: ${key}`, { context, fallback });
950
1016
  if (this.isOffline()) {
1017
+ const resolvedFallbackResult = this.resolveFallbackCheckFlagReturn(
1018
+ key,
1019
+ fallback,
1020
+ "Offline mode - using initialization defaults"
1021
+ );
951
1022
  this.debug(`checkFlag offline result: ${key}`, {
952
- value: fallback,
1023
+ value: resolvedFallbackResult.value,
953
1024
  offlineMode: true
954
1025
  });
955
- return fallback;
1026
+ return resolvedFallbackResult.value;
956
1027
  }
957
1028
  if (!this.useWebSocket) {
958
1029
  const requestUrl = `${this.apiUrl}/flags/${key}/check`;
@@ -980,14 +1051,14 @@ var Schematic = class {
980
1051
  return result.value;
981
1052
  }).catch((error) => {
982
1053
  console.error("There was a problem with the fetch operation:", error);
983
- const errorResult = {
984
- flag: key,
985
- value: fallback,
986
- reason: "API request failed",
987
- error: error instanceof Error ? error.message : String(error)
988
- };
1054
+ const errorResult = this.resolveFallbackCheckFlagReturn(
1055
+ key,
1056
+ fallback,
1057
+ "API request failed",
1058
+ error instanceof Error ? error.message : String(error)
1059
+ );
989
1060
  this.submitFlagCheckEvent(key, errorResult, context);
990
- return fallback;
1061
+ return errorResult.value;
991
1062
  });
992
1063
  }
993
1064
  try {
@@ -997,7 +1068,7 @@ var Schematic = class {
997
1068
  return existingVals[key].value;
998
1069
  }
999
1070
  if (this.isOffline()) {
1000
- return fallback;
1071
+ return this.resolveFallbackValue(key, fallback);
1001
1072
  }
1002
1073
  try {
1003
1074
  await this.setContext(context);
@@ -1010,10 +1081,10 @@ var Schematic = class {
1010
1081
  }
1011
1082
  const contextVals = this.checks[contextStr] ?? {};
1012
1083
  const flagCheck = contextVals[key];
1013
- const result = flagCheck?.value ?? fallback;
1084
+ const result = flagCheck?.value ?? this.resolveFallbackValue(key, fallback);
1014
1085
  this.debug(
1015
1086
  `checkFlag WebSocket result: ${key}`,
1016
- typeof flagCheck !== "undefined" ? flagCheck : { value: fallback, fallbackUsed: true }
1087
+ typeof flagCheck !== "undefined" ? flagCheck : { value: result, fallbackUsed: true }
1017
1088
  );
1018
1089
  if (typeof flagCheck !== "undefined") {
1019
1090
  this.submitFlagCheckEvent(key, flagCheck, context);
@@ -1021,14 +1092,14 @@ var Schematic = class {
1021
1092
  return result;
1022
1093
  } catch (error) {
1023
1094
  console.error("Unexpected error in checkFlag:", error);
1024
- const errorResult = {
1025
- flag: key,
1026
- value: fallback,
1027
- reason: "Unexpected error in flag check",
1028
- error: error instanceof Error ? error.message : String(error)
1029
- };
1095
+ const errorResult = this.resolveFallbackCheckFlagReturn(
1096
+ key,
1097
+ fallback,
1098
+ "Unexpected error in flag check",
1099
+ error instanceof Error ? error.message : String(error)
1100
+ );
1030
1101
  this.submitFlagCheckEvent(key, errorResult, context);
1031
- return fallback;
1102
+ return errorResult.value;
1032
1103
  }
1033
1104
  }
1034
1105
  /**
@@ -1040,6 +1111,49 @@ var Schematic = class {
1040
1111
  console.log(`[Schematic] ${message}`, ...args);
1041
1112
  }
1042
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
+ }
1043
1157
  /**
1044
1158
  * Helper function to check if client is in offline mode
1045
1159
  */
@@ -1071,11 +1185,12 @@ var Schematic = class {
1071
1185
  */
1072
1186
  async fallbackToRest(key, context, fallback) {
1073
1187
  if (this.isOffline()) {
1188
+ const resolvedFallback = this.resolveFallbackValue(key, fallback);
1074
1189
  this.debug(`fallbackToRest offline result: ${key}`, {
1075
- value: fallback,
1190
+ value: resolvedFallback,
1076
1191
  offlineMode: true
1077
1192
  });
1078
- return fallback;
1193
+ return resolvedFallback;
1079
1194
  }
1080
1195
  try {
1081
1196
  const requestUrl = `${this.apiUrl}/flags/${key}/check`;
@@ -1102,14 +1217,14 @@ var Schematic = class {
1102
1217
  return result.value;
1103
1218
  } catch (error) {
1104
1219
  console.error("REST API call failed, using fallback value:", error);
1105
- const errorResult = {
1106
- flag: key,
1107
- value: fallback,
1108
- reason: "API request failed (fallback)",
1109
- error: error instanceof Error ? error.message : String(error)
1110
- };
1220
+ const errorResult = this.resolveFallbackCheckFlagReturn(
1221
+ key,
1222
+ fallback,
1223
+ "API request failed (fallback)",
1224
+ error instanceof Error ? error.message : String(error)
1225
+ );
1111
1226
  this.submitFlagCheckEvent(key, errorResult, context);
1112
- return fallback;
1227
+ return errorResult.value;
1113
1228
  }
1114
1229
  }
1115
1230
  /**
@@ -1190,6 +1305,19 @@ var Schematic = class {
1190
1305
  try {
1191
1306
  this.setIsPending(true);
1192
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
+ }
1193
1321
  if (this.wsReconnectTimer !== null) {
1194
1322
  this.debug(
1195
1323
  `Cancelling scheduled reconnection, connecting immediately`
@@ -1197,7 +1325,17 @@ var Schematic = class {
1197
1325
  clearTimeout(this.wsReconnectTimer);
1198
1326
  this.wsReconnectTimer = null;
1199
1327
  }
1200
- this.conn = this.wsConnect();
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
+ }
1201
1339
  }
1202
1340
  const socket = await this.conn;
1203
1341
  await this.wsSendMessage(socket, context);
@@ -1362,10 +1500,14 @@ var Schematic = class {
1362
1500
  }
1363
1501
  }
1364
1502
  if (readyEvents.length === 0) {
1365
- this.debug(`No events ready for retry yet (${notReadyEvents.length} still in backoff)`);
1503
+ this.debug(
1504
+ `No events ready for retry yet (${notReadyEvents.length} still in backoff)`
1505
+ );
1366
1506
  return;
1367
1507
  }
1368
- this.debug(`Flushing event queue: ${readyEvents.length} ready, ${notReadyEvents.length} waiting`);
1508
+ this.debug(
1509
+ `Flushing event queue: ${readyEvents.length} ready, ${notReadyEvents.length} waiting`
1510
+ );
1369
1511
  this.eventQueue = notReadyEvents;
1370
1512
  for (const event of readyEvents) {
1371
1513
  try {
@@ -1430,7 +1572,10 @@ var Schematic = class {
1430
1572
  } catch (error) {
1431
1573
  const retryCount = (event.retry_count ?? 0) + 1;
1432
1574
  if (retryCount <= this.maxEventRetries) {
1433
- this.debug(`Event failed to send (attempt ${retryCount}/${this.maxEventRetries}), queueing for retry:`, error);
1575
+ this.debug(
1576
+ `Event failed to send (attempt ${retryCount}/${this.maxEventRetries}), queueing for retry:`,
1577
+ error
1578
+ );
1434
1579
  const baseDelay = this.eventRetryInitialDelay * Math.pow(2, retryCount - 1);
1435
1580
  const jitterDelay = Math.min(baseDelay, this.eventRetryMaxDelay);
1436
1581
  const nextRetryAt = Date.now() + jitterDelay;
@@ -1441,15 +1586,22 @@ var Schematic = class {
1441
1586
  };
1442
1587
  if (this.eventQueue.length < this.maxEventQueueSize) {
1443
1588
  this.eventQueue.push(retryEvent);
1444
- this.debug(`Event queued for retry in ${jitterDelay}ms (${this.eventQueue.length}/${this.maxEventQueueSize})`);
1589
+ this.debug(
1590
+ `Event queued for retry in ${jitterDelay}ms (${this.eventQueue.length}/${this.maxEventQueueSize})`
1591
+ );
1445
1592
  } else {
1446
- this.debug(`Event queue full (${this.maxEventQueueSize}), dropping oldest event`);
1593
+ this.debug(
1594
+ `Event queue full (${this.maxEventQueueSize}), dropping oldest event`
1595
+ );
1447
1596
  this.eventQueue.shift();
1448
1597
  this.eventQueue.push(retryEvent);
1449
1598
  }
1450
1599
  this.startRetryTimer();
1451
1600
  } else {
1452
- this.debug(`Event failed permanently after ${this.maxEventRetries} attempts, dropping:`, error);
1601
+ this.debug(
1602
+ `Event failed permanently after ${this.maxEventRetries} attempts, dropping:`,
1603
+ error
1604
+ );
1453
1605
  }
1454
1606
  }
1455
1607
  return Promise.resolve();
@@ -1479,11 +1631,17 @@ var Schematic = class {
1479
1631
  if (this.conn) {
1480
1632
  try {
1481
1633
  const socket = await this.conn;
1634
+ if (this.currentWebSocket === socket) {
1635
+ this.debug(`Cleaning up current websocket tracking`);
1636
+ this.currentWebSocket = null;
1637
+ }
1482
1638
  socket.close();
1483
1639
  } catch (error) {
1484
1640
  console.error("Error during cleanup:", error);
1485
1641
  } finally {
1486
1642
  this.conn = null;
1643
+ this.currentWebSocket = null;
1644
+ this.isConnecting = false;
1487
1645
  }
1488
1646
  }
1489
1647
  };
@@ -1508,7 +1666,9 @@ var Schematic = class {
1508
1666
  if (this.conn !== null) {
1509
1667
  try {
1510
1668
  const socket = await this.conn;
1511
- socket.close();
1669
+ if (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING) {
1670
+ socket.close();
1671
+ }
1512
1672
  } catch (error) {
1513
1673
  this.debug("Error closing connection on offline:", error);
1514
1674
  }
@@ -1523,7 +1683,9 @@ var Schematic = class {
1523
1683
  * Handle browser coming back online
1524
1684
  */
1525
1685
  handleNetworkOnline = () => {
1526
- this.debug("Network online, attempting reconnection and flushing queued events");
1686
+ this.debug(
1687
+ "Network online, attempting reconnection and flushing queued events"
1688
+ );
1527
1689
  this.wsReconnectAttempts = 0;
1528
1690
  if (this.wsReconnectTimer !== null) {
1529
1691
  clearTimeout(this.wsReconnectTimer);
@@ -1546,7 +1708,10 @@ var Schematic = class {
1546
1708
  return;
1547
1709
  }
1548
1710
  if (this.wsReconnectTimer !== null) {
1549
- clearTimeout(this.wsReconnectTimer);
1711
+ this.debug(
1712
+ `Reconnection attempt already scheduled, ignoring duplicate request`
1713
+ );
1714
+ return;
1550
1715
  }
1551
1716
  const delay = this.calculateReconnectDelay();
1552
1717
  this.debug(
@@ -1559,23 +1724,57 @@ var Schematic = class {
1559
1724
  `Attempting to reconnect (attempt ${this.wsReconnectAttempts}/${this.webSocketMaxReconnectAttempts})`
1560
1725
  );
1561
1726
  try {
1562
- this.conn = this.wsConnect();
1563
- const socket = await this.conn;
1564
- this.debug(`Reconnection context check:`, {
1565
- hasCompany: this.context.company !== void 0,
1566
- hasUser: this.context.user !== void 0,
1567
- context: this.context
1568
- });
1569
- if (this.context.company !== void 0 || this.context.user !== void 0) {
1570
- this.debug(`Reconnected, force re-sending context`);
1571
- await this.wsSendContextAfterReconnection(socket, this.context);
1572
- } else {
1573
- this.debug(`No context to re-send after reconnection - websocket ready for new context`);
1727
+ if (this.conn !== null) {
1728
+ this.debug(`Cleaning up existing connection before reconnection`);
1729
+ try {
1730
+ const existingSocket = await this.conn;
1731
+ if (this.currentWebSocket === existingSocket) {
1732
+ this.debug(`Existing websocket is current, will be replaced`);
1733
+ this.currentWebSocket = null;
1734
+ }
1735
+ if (existingSocket.readyState === WebSocket.OPEN || existingSocket.readyState === WebSocket.CONNECTING) {
1736
+ existingSocket.close();
1737
+ }
1738
+ } catch (error) {
1739
+ this.debug(`Error cleaning up existing connection:`, error);
1740
+ }
1741
+ this.conn = null;
1742
+ this.currentWebSocket = null;
1743
+ this.isConnecting = false;
1744
+ }
1745
+ this.isConnecting = true;
1746
+ try {
1747
+ this.conn = this.wsConnect();
1748
+ const socket = await this.conn;
1749
+ this.isConnecting = false;
1750
+ this.debug(`Reconnection context check:`, {
1751
+ hasCompany: this.context.company !== void 0,
1752
+ hasUser: this.context.user !== void 0,
1753
+ context: this.context
1754
+ });
1755
+ if (this.context.company !== void 0 || this.context.user !== void 0) {
1756
+ this.debug(`Reconnected, force re-sending context`);
1757
+ await this.wsSendMessage(socket, this.context, true);
1758
+ } else {
1759
+ this.debug(
1760
+ `No context to re-send after reconnection - websocket ready for new context`
1761
+ );
1762
+ this.debug(
1763
+ `Setting up tracking for reconnected websocket (no context to send)`
1764
+ );
1765
+ this.currentWebSocket = socket;
1766
+ }
1767
+ this.flushEventQueue().catch((error) => {
1768
+ this.debug(
1769
+ "Error flushing event queue after websocket reconnection:",
1770
+ error
1771
+ );
1772
+ });
1773
+ this.debug(`Reconnection successful`);
1774
+ } catch (error) {
1775
+ this.isConnecting = false;
1776
+ throw error;
1574
1777
  }
1575
- this.flushEventQueue().catch((error) => {
1576
- this.debug("Error flushing event queue after websocket reconnection:", error);
1577
- });
1578
- this.debug(`Reconnection successful`);
1579
1778
  } catch (error) {
1580
1779
  this.debug(`Reconnection attempt failed:`, error);
1581
1780
  }
@@ -1593,6 +1792,8 @@ var Schematic = class {
1593
1792
  const wsUrl = `${this.webSocketUrl}/flags/bootstrap?apiKey=${this.apiKey}`;
1594
1793
  this.debug(`connecting to WebSocket:`, wsUrl);
1595
1794
  const webSocket = new WebSocket(wsUrl);
1795
+ const connectionId = Math.random().toString(36).substring(7);
1796
+ this.debug(`Creating WebSocket connection ${connectionId} to ${wsUrl}`);
1596
1797
  let timeoutId = null;
1597
1798
  let isResolved = false;
1598
1799
  timeoutId = setTimeout(() => {
@@ -1611,7 +1812,7 @@ var Schematic = class {
1611
1812
  }
1612
1813
  this.wsReconnectAttempts = 0;
1613
1814
  this.wsIntentionalDisconnect = false;
1614
- this.debug(`WebSocket connection opened`);
1815
+ this.debug(`WebSocket connection ${connectionId} opened successfully`);
1615
1816
  resolve(webSocket);
1616
1817
  };
1617
1818
  webSocket.onerror = (error) => {
@@ -1619,7 +1820,7 @@ var Schematic = class {
1619
1820
  if (timeoutId !== null) {
1620
1821
  clearTimeout(timeoutId);
1621
1822
  }
1622
- this.debug(`WebSocket connection error:`, error);
1823
+ this.debug(`WebSocket connection ${connectionId} error:`, error);
1623
1824
  reject(error);
1624
1825
  };
1625
1826
  webSocket.onclose = () => {
@@ -1627,121 +1828,48 @@ var Schematic = class {
1627
1828
  if (timeoutId !== null) {
1628
1829
  clearTimeout(timeoutId);
1629
1830
  }
1630
- this.debug(`WebSocket connection closed`);
1831
+ this.debug(`WebSocket connection ${connectionId} closed`);
1631
1832
  this.conn = null;
1833
+ if (this.currentWebSocket === webSocket) {
1834
+ this.currentWebSocket = null;
1835
+ this.isConnecting = false;
1836
+ }
1632
1837
  if (!this.wsIntentionalDisconnect && this.webSocketReconnect) {
1633
1838
  this.attemptReconnect();
1634
1839
  }
1635
1840
  };
1636
1841
  });
1637
1842
  };
1638
- // Send a message on the websocket after reconnection, forcing the send even if context appears unchanged
1639
- // because the server has lost all state and needs the initial context
1640
- wsSendContextAfterReconnection = (socket, context) => {
1641
- if (this.isOffline()) {
1642
- this.debug("wsSendContextAfterReconnection: skipped (offline mode)");
1643
- this.setIsPending(false);
1644
- return Promise.resolve();
1645
- }
1646
- return new Promise((resolve) => {
1647
- this.debug(`WebSocket force sending context after reconnection:`, context);
1648
- this.context = context;
1649
- const sendMessage = () => {
1650
- let resolved = false;
1651
- const messageHandler = (event) => {
1652
- const message = JSON.parse(event.data);
1653
- this.debug(`WebSocket message received after reconnection:`, message);
1654
- if (!(contextString(context) in this.checks)) {
1655
- this.checks[contextString(context)] = {};
1656
- }
1657
- (message.flags ?? []).forEach((flag) => {
1658
- const flagCheck = CheckFlagReturnFromJSON(flag);
1659
- const contextStr = contextString(context);
1660
- if (this.checks[contextStr] === void 0) {
1661
- this.checks[contextStr] = {};
1662
- }
1663
- this.checks[contextStr][flagCheck.flag] = flagCheck;
1664
- });
1665
- this.useWebSocket = true;
1666
- socket.removeEventListener("message", messageHandler);
1667
- if (!resolved) {
1668
- resolved = true;
1669
- resolve(this.setIsPending(false));
1670
- }
1671
- };
1672
- socket.addEventListener("message", messageHandler);
1673
- const clientVersion = this.additionalHeaders["X-Schematic-Client-Version"] ?? `schematic-js@${version}`;
1674
- const messagePayload = {
1675
- apiKey: this.apiKey,
1676
- clientVersion,
1677
- data: context
1678
- };
1679
- this.debug(`WebSocket sending forced message after reconnection:`, messagePayload);
1680
- socket.send(JSON.stringify(messagePayload));
1681
- };
1682
- if (socket.readyState === WebSocket.OPEN) {
1683
- this.debug(`WebSocket already open, sending forced message after reconnection`);
1684
- sendMessage();
1685
- } else {
1686
- socket.addEventListener("open", () => {
1687
- this.debug(`WebSocket opened, sending forced message after reconnection`);
1688
- sendMessage();
1689
- });
1690
- }
1691
- });
1692
- };
1693
1843
  // Send a message on the websocket indicating interest in a particular evaluation context
1694
1844
  // and wait for the initial set of flag values to be returned
1695
- wsSendMessage = (socket, context) => {
1845
+ wsSendMessage = (socket, context, forceContextSend = false) => {
1696
1846
  if (this.isOffline()) {
1697
1847
  this.debug("wsSendMessage: skipped (offline mode)");
1698
1848
  this.setIsPending(false);
1699
1849
  return Promise.resolve();
1700
1850
  }
1701
1851
  return new Promise((resolve, reject) => {
1702
- if (contextString(context) == contextString(this.context)) {
1852
+ if (!forceContextSend && contextString(context) == contextString(this.context)) {
1703
1853
  this.debug(`WebSocket context unchanged, skipping update`);
1704
1854
  return resolve(this.setIsPending(false));
1705
1855
  }
1706
- this.debug(`WebSocket context updated:`, context);
1856
+ this.debug(
1857
+ forceContextSend ? `WebSocket force sending context (reconnection):` : `WebSocket context updated:`,
1858
+ context
1859
+ );
1707
1860
  this.context = context;
1708
1861
  const sendMessage = () => {
1709
1862
  let resolved = false;
1863
+ const persistentMessageHandler = this.createPersistentMessageHandler(context);
1710
1864
  const messageHandler = (event) => {
1711
- const message = JSON.parse(event.data);
1712
- this.debug(`WebSocket message received:`, message);
1713
- if (!(contextString(context) in this.checks)) {
1714
- this.checks[contextString(context)] = {};
1715
- }
1716
- (message.flags ?? []).forEach((flag) => {
1717
- const flagCheck = CheckFlagReturnFromJSON(flag);
1718
- const contextStr = contextString(context);
1719
- if (this.checks[contextStr] === void 0) {
1720
- this.checks[contextStr] = {};
1721
- }
1722
- this.checks[contextStr][flagCheck.flag] = flagCheck;
1723
- this.debug(`WebSocket flag update:`, {
1724
- flag: flagCheck.flag,
1725
- value: flagCheck.value,
1726
- flagCheck
1727
- });
1728
- if (typeof flagCheck.featureUsageEvent === "string") {
1729
- this.updateFeatureUsageEventMap(flagCheck);
1730
- }
1731
- if (this.flagCheckListeners[flag.flag]?.size > 0 || this.flagValueListeners[flag.flag]?.size > 0) {
1732
- this.submitFlagCheckEvent(flagCheck.flag, flagCheck, context);
1733
- }
1734
- this.notifyFlagCheckListeners(flag.flag, flagCheck);
1735
- this.notifyFlagValueListeners(flag.flag, flagCheck.value);
1736
- });
1737
- this.flushContextDependentEventQueue();
1738
- this.setIsPending(false);
1865
+ persistentMessageHandler(event);
1739
1866
  if (!resolved) {
1740
1867
  resolved = true;
1741
1868
  resolve();
1742
1869
  }
1743
1870
  };
1744
1871
  socket.addEventListener("message", messageHandler);
1872
+ this.currentWebSocket = socket;
1745
1873
  const clientVersion = this.additionalHeaders["X-Schematic-Client-Version"] ?? `schematic-js@${version}`;
1746
1874
  const messagePayload = {
1747
1875
  apiKey: this.apiKey,
@@ -1849,7 +1977,17 @@ var Schematic = class {
1849
1977
  { value }
1850
1978
  );
1851
1979
  }
1852
- listeners.forEach((listener) => notifyFlagValueListener(listener, value));
1980
+ listeners.forEach((listener, index) => {
1981
+ this.debug(`Calling listener ${index} for flag ${flagKey}`, {
1982
+ flagKey,
1983
+ value
1984
+ });
1985
+ notifyFlagValueListener(listener, value);
1986
+ this.debug(`Listener ${index} for flag ${flagKey} completed`, {
1987
+ flagKey,
1988
+ value
1989
+ });
1990
+ });
1853
1991
  };
1854
1992
  };
1855
1993
  var notifyPendingListener = (listener, value) => {