@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.
- package/dist/schematic.browser.js +2 -2
- package/dist/schematic.cjs.js +284 -146
- package/dist/schematic.d.ts +28 -2
- package/dist/schematic.esm.js +284 -146
- package/package.json +4 -4
package/dist/schematic.cjs.js
CHANGED
|
@@ -800,7 +800,7 @@ function contextString(context) {
|
|
|
800
800
|
}
|
|
801
801
|
|
|
802
802
|
// src/version.ts
|
|
803
|
-
var version = "1.2.
|
|
803
|
+
var version = "1.2.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
|
|
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:
|
|
1023
|
+
value: resolvedFallbackResult.value,
|
|
953
1024
|
offlineMode: true
|
|
954
1025
|
});
|
|
955
|
-
return
|
|
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
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
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
|
|
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:
|
|
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
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
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
|
|
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:
|
|
1190
|
+
value: resolvedFallback,
|
|
1076
1191
|
offlineMode: true
|
|
1077
1192
|
});
|
|
1078
|
-
return
|
|
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
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
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
|
|
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.
|
|
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(
|
|
1503
|
+
this.debug(
|
|
1504
|
+
`No events ready for retry yet (${notReadyEvents.length} still in backoff)`
|
|
1505
|
+
);
|
|
1366
1506
|
return;
|
|
1367
1507
|
}
|
|
1368
|
-
this.debug(
|
|
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(
|
|
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(
|
|
1589
|
+
this.debug(
|
|
1590
|
+
`Event queued for retry in ${jitterDelay}ms (${this.eventQueue.length}/${this.maxEventQueueSize})`
|
|
1591
|
+
);
|
|
1445
1592
|
} else {
|
|
1446
|
-
this.debug(
|
|
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(
|
|
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.
|
|
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(
|
|
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
|
-
|
|
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
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
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(
|
|
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
|
-
|
|
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) =>
|
|
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) => {
|