@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.esm.js
CHANGED
|
@@ -781,7 +781,7 @@ function contextString(context) {
|
|
|
781
781
|
}
|
|
782
782
|
|
|
783
783
|
// src/version.ts
|
|
784
|
-
var version = "1.2.
|
|
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
|
|
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:
|
|
1004
|
+
value: resolvedFallbackResult.value,
|
|
934
1005
|
offlineMode: true
|
|
935
1006
|
});
|
|
936
|
-
return
|
|
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
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
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
|
|
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:
|
|
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
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
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
|
|
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:
|
|
1171
|
+
value: resolvedFallback,
|
|
1057
1172
|
offlineMode: true
|
|
1058
1173
|
});
|
|
1059
|
-
return
|
|
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
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
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
|
|
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.
|
|
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(
|
|
1484
|
+
this.debug(
|
|
1485
|
+
`No events ready for retry yet (${notReadyEvents.length} still in backoff)`
|
|
1486
|
+
);
|
|
1347
1487
|
return;
|
|
1348
1488
|
}
|
|
1349
|
-
this.debug(
|
|
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(
|
|
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(
|
|
1570
|
+
this.debug(
|
|
1571
|
+
`Event queued for retry in ${jitterDelay}ms (${this.eventQueue.length}/${this.maxEventQueueSize})`
|
|
1572
|
+
);
|
|
1426
1573
|
} else {
|
|
1427
|
-
this.debug(
|
|
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(
|
|
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.
|
|
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(
|
|
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
|
-
|
|
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
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
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(
|
|
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
|
-
|
|
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) =>
|
|
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) => {
|