@schematichq/schematic-vue 0.1.0 → 1.2.16
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-vue.cjs.js +735 -97
- package/dist/schematic-vue.esm.js +735 -97
- package/package.json +2 -2
|
@@ -790,7 +790,7 @@ function contextString(context) {
|
|
|
790
790
|
}, {});
|
|
791
791
|
return JSON.stringify(sortedContext);
|
|
792
792
|
}
|
|
793
|
-
var version = "1.2.
|
|
793
|
+
var version = "1.2.16";
|
|
794
794
|
var anonymousIdKey = "schematicId";
|
|
795
795
|
var Schematic = class {
|
|
796
796
|
additionalHeaders = {};
|
|
@@ -812,6 +812,30 @@ var Schematic = class {
|
|
|
812
812
|
checks = {};
|
|
813
813
|
featureUsageEventMap = {};
|
|
814
814
|
webSocketUrl = "wss://api.schematichq.com";
|
|
815
|
+
webSocketConnectionTimeout = 1e4;
|
|
816
|
+
webSocketReconnect = true;
|
|
817
|
+
webSocketMaxReconnectAttempts = 7;
|
|
818
|
+
// Max attempts after connection disrupted
|
|
819
|
+
webSocketMaxConnectionAttempts = 3;
|
|
820
|
+
// Max attempts for initial connection
|
|
821
|
+
webSocketInitialRetryDelay = 1e3;
|
|
822
|
+
webSocketMaxRetryDelay = 3e4;
|
|
823
|
+
wsReconnectAttempts = 0;
|
|
824
|
+
wsReconnectTimer = null;
|
|
825
|
+
wsIntentionalDisconnect = false;
|
|
826
|
+
currentWebSocket = null;
|
|
827
|
+
isConnecting = false;
|
|
828
|
+
maxEventQueueSize = 100;
|
|
829
|
+
// Prevent memory issues with very long network outages
|
|
830
|
+
maxEventRetries = 5;
|
|
831
|
+
// Maximum retry attempts for failed events
|
|
832
|
+
eventRetryInitialDelay = 1e3;
|
|
833
|
+
// Initial retry delay in ms
|
|
834
|
+
eventRetryMaxDelay = 3e4;
|
|
835
|
+
// Maximum retry delay in ms
|
|
836
|
+
retryTimer = null;
|
|
837
|
+
flagValueDefaults = {};
|
|
838
|
+
flagCheckDefaults = {};
|
|
815
839
|
constructor(apiKey, options) {
|
|
816
840
|
this.apiKey = apiKey;
|
|
817
841
|
this.eventQueue = [];
|
|
@@ -843,8 +867,13 @@ var Schematic = class {
|
|
|
843
867
|
};
|
|
844
868
|
if (options?.storage) {
|
|
845
869
|
this.storage = options.storage;
|
|
846
|
-
} else
|
|
847
|
-
|
|
870
|
+
} else {
|
|
871
|
+
try {
|
|
872
|
+
if (typeof localStorage !== "undefined") {
|
|
873
|
+
this.storage = localStorage;
|
|
874
|
+
}
|
|
875
|
+
} catch {
|
|
876
|
+
}
|
|
848
877
|
}
|
|
849
878
|
if (options?.apiUrl !== void 0) {
|
|
850
879
|
this.apiUrl = options.apiUrl;
|
|
@@ -855,11 +884,54 @@ var Schematic = class {
|
|
|
855
884
|
if (options?.webSocketUrl !== void 0) {
|
|
856
885
|
this.webSocketUrl = options.webSocketUrl;
|
|
857
886
|
}
|
|
887
|
+
if (options?.webSocketConnectionTimeout !== void 0) {
|
|
888
|
+
this.webSocketConnectionTimeout = options.webSocketConnectionTimeout;
|
|
889
|
+
}
|
|
890
|
+
if (options?.webSocketReconnect !== void 0) {
|
|
891
|
+
this.webSocketReconnect = options.webSocketReconnect;
|
|
892
|
+
}
|
|
893
|
+
if (options?.webSocketMaxReconnectAttempts !== void 0) {
|
|
894
|
+
this.webSocketMaxReconnectAttempts = options.webSocketMaxReconnectAttempts;
|
|
895
|
+
}
|
|
896
|
+
if (options?.webSocketInitialRetryDelay !== void 0) {
|
|
897
|
+
this.webSocketInitialRetryDelay = options.webSocketInitialRetryDelay;
|
|
898
|
+
}
|
|
899
|
+
if (options?.webSocketMaxRetryDelay !== void 0) {
|
|
900
|
+
this.webSocketMaxRetryDelay = options.webSocketMaxRetryDelay;
|
|
901
|
+
}
|
|
902
|
+
if (options?.maxEventQueueSize !== void 0) {
|
|
903
|
+
this.maxEventQueueSize = options.maxEventQueueSize;
|
|
904
|
+
}
|
|
905
|
+
if (options?.maxEventRetries !== void 0) {
|
|
906
|
+
this.maxEventRetries = options.maxEventRetries;
|
|
907
|
+
}
|
|
908
|
+
if (options?.eventRetryInitialDelay !== void 0) {
|
|
909
|
+
this.eventRetryInitialDelay = options.eventRetryInitialDelay;
|
|
910
|
+
}
|
|
911
|
+
if (options?.eventRetryMaxDelay !== void 0) {
|
|
912
|
+
this.eventRetryMaxDelay = options.eventRetryMaxDelay;
|
|
913
|
+
}
|
|
914
|
+
if (options?.flagValueDefaults !== void 0) {
|
|
915
|
+
this.flagValueDefaults = options.flagValueDefaults;
|
|
916
|
+
}
|
|
917
|
+
if (options?.flagCheckDefaults !== void 0) {
|
|
918
|
+
this.flagCheckDefaults = options.flagCheckDefaults;
|
|
919
|
+
}
|
|
858
920
|
if (typeof window !== "undefined" && window?.addEventListener) {
|
|
859
921
|
window.addEventListener("beforeunload", () => {
|
|
860
922
|
this.flushEventQueue();
|
|
861
923
|
this.flushContextDependentEventQueue();
|
|
862
924
|
});
|
|
925
|
+
if (this.useWebSocket) {
|
|
926
|
+
window.addEventListener("offline", () => {
|
|
927
|
+
this.debug("Browser went offline, closing WebSocket connection");
|
|
928
|
+
this.handleNetworkOffline();
|
|
929
|
+
});
|
|
930
|
+
window.addEventListener("online", () => {
|
|
931
|
+
this.debug("Browser came online, attempting to reconnect WebSocket");
|
|
932
|
+
this.handleNetworkOnline();
|
|
933
|
+
});
|
|
934
|
+
}
|
|
863
935
|
}
|
|
864
936
|
if (this.offlineEnabled) {
|
|
865
937
|
this.debug(
|
|
@@ -869,6 +941,66 @@ var Schematic = class {
|
|
|
869
941
|
this.debug("Initialized with debug mode enabled");
|
|
870
942
|
}
|
|
871
943
|
}
|
|
944
|
+
/**
|
|
945
|
+
* Resolve fallback value according to priority order:
|
|
946
|
+
* 1. Callsite fallback value (if provided)
|
|
947
|
+
* 2. Boolean value from flagCheckDefaults initialization option
|
|
948
|
+
* 3. Boolean value from flagValueDefaults initialization option
|
|
949
|
+
* 4. Default to false
|
|
950
|
+
*/
|
|
951
|
+
resolveFallbackValue(key, callsiteFallback) {
|
|
952
|
+
if (callsiteFallback !== void 0) {
|
|
953
|
+
return callsiteFallback;
|
|
954
|
+
}
|
|
955
|
+
if (key in this.flagCheckDefaults) {
|
|
956
|
+
return this.flagCheckDefaults[key].value;
|
|
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
|
+
}
|
|
872
1004
|
/**
|
|
873
1005
|
* Get value for a single flag.
|
|
874
1006
|
* In WebSocket mode, returns cached values if connection is active, otherwise establishes
|
|
@@ -877,16 +1009,21 @@ var Schematic = class {
|
|
|
877
1009
|
* In REST mode, makes an API call for each check.
|
|
878
1010
|
*/
|
|
879
1011
|
async checkFlag(options) {
|
|
880
|
-
const { fallback
|
|
1012
|
+
const { fallback, key } = options;
|
|
881
1013
|
const context = options.context || this.context;
|
|
882
1014
|
const contextStr = contextString(context);
|
|
883
1015
|
this.debug(`checkFlag: ${key}`, { context, fallback });
|
|
884
1016
|
if (this.isOffline()) {
|
|
1017
|
+
const resolvedFallbackResult = this.resolveFallbackCheckFlagReturn(
|
|
1018
|
+
key,
|
|
1019
|
+
fallback,
|
|
1020
|
+
"Offline mode - using initialization defaults"
|
|
1021
|
+
);
|
|
885
1022
|
this.debug(`checkFlag offline result: ${key}`, {
|
|
886
|
-
value:
|
|
1023
|
+
value: resolvedFallbackResult.value,
|
|
887
1024
|
offlineMode: true
|
|
888
1025
|
});
|
|
889
|
-
return
|
|
1026
|
+
return resolvedFallbackResult.value;
|
|
890
1027
|
}
|
|
891
1028
|
if (!this.useWebSocket) {
|
|
892
1029
|
const requestUrl = `${this.apiUrl}/flags/${key}/check`;
|
|
@@ -913,15 +1050,15 @@ var Schematic = class {
|
|
|
913
1050
|
this.submitFlagCheckEvent(key, result, context);
|
|
914
1051
|
return result.value;
|
|
915
1052
|
}).catch((error) => {
|
|
916
|
-
console.
|
|
917
|
-
const errorResult =
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
error
|
|
922
|
-
|
|
1053
|
+
console.warn("There was a problem with the fetch operation:", error);
|
|
1054
|
+
const errorResult = this.resolveFallbackCheckFlagReturn(
|
|
1055
|
+
key,
|
|
1056
|
+
fallback,
|
|
1057
|
+
"API request failed",
|
|
1058
|
+
error instanceof Error ? error.message : String(error)
|
|
1059
|
+
);
|
|
923
1060
|
this.submitFlagCheckEvent(key, errorResult, context);
|
|
924
|
-
return
|
|
1061
|
+
return errorResult.value;
|
|
925
1062
|
});
|
|
926
1063
|
}
|
|
927
1064
|
try {
|
|
@@ -931,12 +1068,12 @@ var Schematic = class {
|
|
|
931
1068
|
return existingVals[key].value;
|
|
932
1069
|
}
|
|
933
1070
|
if (this.isOffline()) {
|
|
934
|
-
return fallback;
|
|
1071
|
+
return this.resolveFallbackValue(key, fallback);
|
|
935
1072
|
}
|
|
936
1073
|
try {
|
|
937
1074
|
await this.setContext(context);
|
|
938
1075
|
} catch (error) {
|
|
939
|
-
console.
|
|
1076
|
+
console.warn(
|
|
940
1077
|
"WebSocket connection failed, falling back to REST:",
|
|
941
1078
|
error
|
|
942
1079
|
);
|
|
@@ -944,10 +1081,10 @@ var Schematic = class {
|
|
|
944
1081
|
}
|
|
945
1082
|
const contextVals = this.checks[contextStr] ?? {};
|
|
946
1083
|
const flagCheck = contextVals[key];
|
|
947
|
-
const result = flagCheck?.value ?? fallback;
|
|
1084
|
+
const result = flagCheck?.value ?? this.resolveFallbackValue(key, fallback);
|
|
948
1085
|
this.debug(
|
|
949
1086
|
`checkFlag WebSocket result: ${key}`,
|
|
950
|
-
typeof flagCheck !== "undefined" ? flagCheck : { value:
|
|
1087
|
+
typeof flagCheck !== "undefined" ? flagCheck : { value: result, fallbackUsed: true }
|
|
951
1088
|
);
|
|
952
1089
|
if (typeof flagCheck !== "undefined") {
|
|
953
1090
|
this.submitFlagCheckEvent(key, flagCheck, context);
|
|
@@ -955,14 +1092,14 @@ var Schematic = class {
|
|
|
955
1092
|
return result;
|
|
956
1093
|
} catch (error) {
|
|
957
1094
|
console.error("Unexpected error in checkFlag:", error);
|
|
958
|
-
const errorResult =
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
error
|
|
963
|
-
|
|
1095
|
+
const errorResult = this.resolveFallbackCheckFlagReturn(
|
|
1096
|
+
key,
|
|
1097
|
+
fallback,
|
|
1098
|
+
"Unexpected error in flag check",
|
|
1099
|
+
error instanceof Error ? error.message : String(error)
|
|
1100
|
+
);
|
|
964
1101
|
this.submitFlagCheckEvent(key, errorResult, context);
|
|
965
|
-
return
|
|
1102
|
+
return errorResult.value;
|
|
966
1103
|
}
|
|
967
1104
|
}
|
|
968
1105
|
/**
|
|
@@ -974,6 +1111,49 @@ var Schematic = class {
|
|
|
974
1111
|
console.log(`[Schematic] ${message}`, ...args);
|
|
975
1112
|
}
|
|
976
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
|
+
}
|
|
977
1157
|
/**
|
|
978
1158
|
* Helper function to check if client is in offline mode
|
|
979
1159
|
*/
|
|
@@ -1005,11 +1185,12 @@ var Schematic = class {
|
|
|
1005
1185
|
*/
|
|
1006
1186
|
async fallbackToRest(key, context, fallback) {
|
|
1007
1187
|
if (this.isOffline()) {
|
|
1188
|
+
const resolvedFallback = this.resolveFallbackValue(key, fallback);
|
|
1008
1189
|
this.debug(`fallbackToRest offline result: ${key}`, {
|
|
1009
|
-
value:
|
|
1190
|
+
value: resolvedFallback,
|
|
1010
1191
|
offlineMode: true
|
|
1011
1192
|
});
|
|
1012
|
-
return
|
|
1193
|
+
return resolvedFallback;
|
|
1013
1194
|
}
|
|
1014
1195
|
try {
|
|
1015
1196
|
const requestUrl = `${this.apiUrl}/flags/${key}/check`;
|
|
@@ -1035,15 +1216,15 @@ var Schematic = class {
|
|
|
1035
1216
|
this.submitFlagCheckEvent(key, result, context);
|
|
1036
1217
|
return result.value;
|
|
1037
1218
|
} catch (error) {
|
|
1038
|
-
console.
|
|
1039
|
-
const errorResult =
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
error
|
|
1044
|
-
|
|
1219
|
+
console.warn("REST API call failed, using fallback value:", error);
|
|
1220
|
+
const errorResult = this.resolveFallbackCheckFlagReturn(
|
|
1221
|
+
key,
|
|
1222
|
+
fallback,
|
|
1223
|
+
"API request failed (fallback)",
|
|
1224
|
+
error instanceof Error ? error.message : String(error)
|
|
1225
|
+
);
|
|
1045
1226
|
this.submitFlagCheckEvent(key, errorResult, context);
|
|
1046
|
-
return
|
|
1227
|
+
return errorResult.value;
|
|
1047
1228
|
}
|
|
1048
1229
|
}
|
|
1049
1230
|
/**
|
|
@@ -1084,7 +1265,7 @@ var Schematic = class {
|
|
|
1084
1265
|
{}
|
|
1085
1266
|
);
|
|
1086
1267
|
}).catch((error) => {
|
|
1087
|
-
console.
|
|
1268
|
+
console.warn("There was a problem with the fetch operation:", error);
|
|
1088
1269
|
return {};
|
|
1089
1270
|
});
|
|
1090
1271
|
};
|
|
@@ -1095,14 +1276,12 @@ var Schematic = class {
|
|
|
1095
1276
|
*/
|
|
1096
1277
|
identify = (body) => {
|
|
1097
1278
|
this.debug(`identify:`, body);
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
}
|
|
1104
|
-
console.error("Error setting context:", error);
|
|
1105
|
-
}
|
|
1279
|
+
this.setContext({
|
|
1280
|
+
company: body.company?.keys,
|
|
1281
|
+
user: body.keys
|
|
1282
|
+
}).catch((error) => {
|
|
1283
|
+
console.warn("Error setting context:", error);
|
|
1284
|
+
});
|
|
1106
1285
|
return this.handleEvent("identify", body);
|
|
1107
1286
|
};
|
|
1108
1287
|
/**
|
|
@@ -1124,12 +1303,42 @@ var Schematic = class {
|
|
|
1124
1303
|
try {
|
|
1125
1304
|
this.setIsPending(true);
|
|
1126
1305
|
if (!this.conn) {
|
|
1127
|
-
|
|
1306
|
+
if (this.isConnecting) {
|
|
1307
|
+
this.debug(
|
|
1308
|
+
`Connection already in progress, waiting for it to complete`
|
|
1309
|
+
);
|
|
1310
|
+
while (this.isConnecting && this.conn === null) {
|
|
1311
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1312
|
+
}
|
|
1313
|
+
if (this.conn !== null) {
|
|
1314
|
+
const socket2 = await this.conn;
|
|
1315
|
+
await this.wsSendMessage(socket2, context);
|
|
1316
|
+
return;
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
if (this.wsReconnectTimer !== null) {
|
|
1320
|
+
this.debug(
|
|
1321
|
+
`Cancelling scheduled reconnection, connecting immediately`
|
|
1322
|
+
);
|
|
1323
|
+
clearTimeout(this.wsReconnectTimer);
|
|
1324
|
+
this.wsReconnectTimer = null;
|
|
1325
|
+
}
|
|
1326
|
+
this.isConnecting = true;
|
|
1327
|
+
try {
|
|
1328
|
+
this.conn = this.wsConnect();
|
|
1329
|
+
const socket2 = await this.conn;
|
|
1330
|
+
this.isConnecting = false;
|
|
1331
|
+
await this.wsSendMessage(socket2, context);
|
|
1332
|
+
return;
|
|
1333
|
+
} catch (error) {
|
|
1334
|
+
this.isConnecting = false;
|
|
1335
|
+
throw error;
|
|
1336
|
+
}
|
|
1128
1337
|
}
|
|
1129
1338
|
const socket = await this.conn;
|
|
1130
1339
|
await this.wsSendMessage(socket, context);
|
|
1131
1340
|
} catch (error) {
|
|
1132
|
-
console.
|
|
1341
|
+
console.warn("Failed to establish WebSocket connection:", error);
|
|
1133
1342
|
throw error;
|
|
1134
1343
|
}
|
|
1135
1344
|
};
|
|
@@ -1253,11 +1462,57 @@ var Schematic = class {
|
|
|
1253
1462
|
}
|
|
1254
1463
|
}
|
|
1255
1464
|
};
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1465
|
+
startRetryTimer = () => {
|
|
1466
|
+
if (this.retryTimer !== null) {
|
|
1467
|
+
return;
|
|
1468
|
+
}
|
|
1469
|
+
this.retryTimer = setInterval(() => {
|
|
1470
|
+
this.flushEventQueue().catch((error) => {
|
|
1471
|
+
this.debug("Error in retry timer flush:", error);
|
|
1472
|
+
});
|
|
1473
|
+
if (this.eventQueue.length === 0) {
|
|
1474
|
+
this.stopRetryTimer();
|
|
1475
|
+
}
|
|
1476
|
+
}, 5e3);
|
|
1477
|
+
this.debug("Started retry timer");
|
|
1478
|
+
};
|
|
1479
|
+
stopRetryTimer = () => {
|
|
1480
|
+
if (this.retryTimer !== null) {
|
|
1481
|
+
clearInterval(this.retryTimer);
|
|
1482
|
+
this.retryTimer = null;
|
|
1483
|
+
this.debug("Stopped retry timer");
|
|
1484
|
+
}
|
|
1485
|
+
};
|
|
1486
|
+
flushEventQueue = async () => {
|
|
1487
|
+
if (this.eventQueue.length === 0) {
|
|
1488
|
+
return;
|
|
1489
|
+
}
|
|
1490
|
+
const now = Date.now();
|
|
1491
|
+
const readyEvents = [];
|
|
1492
|
+
const notReadyEvents = [];
|
|
1493
|
+
for (const event of this.eventQueue) {
|
|
1494
|
+
if (event.next_retry_at === void 0 || event.next_retry_at <= now) {
|
|
1495
|
+
readyEvents.push(event);
|
|
1496
|
+
} else {
|
|
1497
|
+
notReadyEvents.push(event);
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
if (readyEvents.length === 0) {
|
|
1501
|
+
this.debug(
|
|
1502
|
+
`No events ready for retry yet (${notReadyEvents.length} still in backoff)`
|
|
1503
|
+
);
|
|
1504
|
+
return;
|
|
1505
|
+
}
|
|
1506
|
+
this.debug(
|
|
1507
|
+
`Flushing event queue: ${readyEvents.length} ready, ${notReadyEvents.length} waiting`
|
|
1508
|
+
);
|
|
1509
|
+
this.eventQueue = notReadyEvents;
|
|
1510
|
+
for (const event of readyEvents) {
|
|
1511
|
+
try {
|
|
1512
|
+
await this.sendEvent(event);
|
|
1513
|
+
this.debug(`Queued event sent successfully:`, event.type);
|
|
1514
|
+
} catch (error) {
|
|
1515
|
+
this.debug(`Failed to send queued event:`, error);
|
|
1261
1516
|
}
|
|
1262
1517
|
}
|
|
1263
1518
|
};
|
|
@@ -1305,12 +1560,47 @@ var Schematic = class {
|
|
|
1305
1560
|
},
|
|
1306
1561
|
body: payload
|
|
1307
1562
|
});
|
|
1563
|
+
if (!response.ok) {
|
|
1564
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
1565
|
+
}
|
|
1308
1566
|
this.debug(`event sent:`, {
|
|
1309
1567
|
status: response.status,
|
|
1310
1568
|
statusText: response.statusText
|
|
1311
1569
|
});
|
|
1312
1570
|
} catch (error) {
|
|
1313
|
-
|
|
1571
|
+
const retryCount = (event.retry_count ?? 0) + 1;
|
|
1572
|
+
if (retryCount <= this.maxEventRetries) {
|
|
1573
|
+
this.debug(
|
|
1574
|
+
`Event failed to send (attempt ${retryCount}/${this.maxEventRetries}), queueing for retry:`,
|
|
1575
|
+
error
|
|
1576
|
+
);
|
|
1577
|
+
const baseDelay = this.eventRetryInitialDelay * Math.pow(2, retryCount - 1);
|
|
1578
|
+
const jitterDelay = Math.min(baseDelay, this.eventRetryMaxDelay);
|
|
1579
|
+
const nextRetryAt = Date.now() + jitterDelay;
|
|
1580
|
+
const retryEvent = {
|
|
1581
|
+
...event,
|
|
1582
|
+
retry_count: retryCount,
|
|
1583
|
+
next_retry_at: nextRetryAt
|
|
1584
|
+
};
|
|
1585
|
+
if (this.eventQueue.length < this.maxEventQueueSize) {
|
|
1586
|
+
this.eventQueue.push(retryEvent);
|
|
1587
|
+
this.debug(
|
|
1588
|
+
`Event queued for retry in ${jitterDelay}ms (${this.eventQueue.length}/${this.maxEventQueueSize})`
|
|
1589
|
+
);
|
|
1590
|
+
} else {
|
|
1591
|
+
this.debug(
|
|
1592
|
+
`Event queue full (${this.maxEventQueueSize}), dropping oldest event`
|
|
1593
|
+
);
|
|
1594
|
+
this.eventQueue.shift();
|
|
1595
|
+
this.eventQueue.push(retryEvent);
|
|
1596
|
+
}
|
|
1597
|
+
this.startRetryTimer();
|
|
1598
|
+
} else {
|
|
1599
|
+
this.debug(
|
|
1600
|
+
`Event failed permanently after ${this.maxEventRetries} attempts, dropping:`,
|
|
1601
|
+
error
|
|
1602
|
+
);
|
|
1603
|
+
}
|
|
1314
1604
|
}
|
|
1315
1605
|
return Promise.resolve();
|
|
1316
1606
|
};
|
|
@@ -1321,6 +1611,130 @@ var Schematic = class {
|
|
|
1321
1611
|
/**
|
|
1322
1612
|
* Websocket management
|
|
1323
1613
|
*/
|
|
1614
|
+
/**
|
|
1615
|
+
* Force an immediate WebSocket reconnection.
|
|
1616
|
+
* This is useful when the application returns from a background state (e.g., mobile app
|
|
1617
|
+
* coming back to foreground) and wants to immediately re-establish the connection
|
|
1618
|
+
* rather than waiting for the exponential backoff timer.
|
|
1619
|
+
*
|
|
1620
|
+
* This method will:
|
|
1621
|
+
* - Cancel any pending reconnection timer
|
|
1622
|
+
* - Reset the reconnection attempt counter
|
|
1623
|
+
* - Close any existing connection
|
|
1624
|
+
* - Immediately attempt to reconnect
|
|
1625
|
+
* - Re-send the current context to get fresh flag values
|
|
1626
|
+
*
|
|
1627
|
+
* Use this when you need guaranteed fresh values (e.g., after an in-app purchase).
|
|
1628
|
+
*
|
|
1629
|
+
* @example
|
|
1630
|
+
* ```typescript
|
|
1631
|
+
* // React Native example: reconnect when app comes to foreground
|
|
1632
|
+
* useEffect(() => {
|
|
1633
|
+
* const subscription = AppState.addEventListener("change", (state) => {
|
|
1634
|
+
* if (state === "active") {
|
|
1635
|
+
* client.forceReconnect();
|
|
1636
|
+
* }
|
|
1637
|
+
* });
|
|
1638
|
+
* return () => subscription.remove();
|
|
1639
|
+
* }, [client]);
|
|
1640
|
+
* ```
|
|
1641
|
+
*/
|
|
1642
|
+
forceReconnect = async () => {
|
|
1643
|
+
return this.reconnect({ force: true });
|
|
1644
|
+
};
|
|
1645
|
+
/**
|
|
1646
|
+
* Reconnect the WebSocket connection only if the current connection is unhealthy.
|
|
1647
|
+
* This is useful when the application returns from a background state and wants to
|
|
1648
|
+
* ensure a healthy connection exists, but doesn't need to force a reconnection if
|
|
1649
|
+
* the connection is still active.
|
|
1650
|
+
*
|
|
1651
|
+
* This method will:
|
|
1652
|
+
* - Check if an existing connection is healthy (readyState === OPEN)
|
|
1653
|
+
* - If healthy, return immediately without reconnecting
|
|
1654
|
+
* - If unhealthy, perform the same reconnection logic as forceReconnect()
|
|
1655
|
+
*
|
|
1656
|
+
* Use this when you want efficient reconnection that avoids unnecessary disconnects.
|
|
1657
|
+
*
|
|
1658
|
+
* @example
|
|
1659
|
+
* ```typescript
|
|
1660
|
+
* // React Native example: reconnect only if needed when app comes to foreground
|
|
1661
|
+
* useEffect(() => {
|
|
1662
|
+
* const subscription = AppState.addEventListener("change", (state) => {
|
|
1663
|
+
* if (state === "active") {
|
|
1664
|
+
* client.reconnectIfNeeded();
|
|
1665
|
+
* }
|
|
1666
|
+
* });
|
|
1667
|
+
* return () => subscription.remove();
|
|
1668
|
+
* }, [client]);
|
|
1669
|
+
* ```
|
|
1670
|
+
*/
|
|
1671
|
+
reconnectIfNeeded = async () => {
|
|
1672
|
+
return this.reconnect({ force: false });
|
|
1673
|
+
};
|
|
1674
|
+
/**
|
|
1675
|
+
* Internal method to handle reconnection logic for both forceReconnect and reconnectIfNeeded.
|
|
1676
|
+
*/
|
|
1677
|
+
reconnect = async (options) => {
|
|
1678
|
+
const { force } = options;
|
|
1679
|
+
const methodName = force ? "forceReconnect" : "reconnectIfNeeded";
|
|
1680
|
+
if (this.isOffline()) {
|
|
1681
|
+
this.debug(`${methodName}: skipped (offline mode)`);
|
|
1682
|
+
return Promise.resolve();
|
|
1683
|
+
}
|
|
1684
|
+
if (!force && this.conn !== null) {
|
|
1685
|
+
try {
|
|
1686
|
+
const existingSocket = await this.conn;
|
|
1687
|
+
if (existingSocket.readyState === WebSocket.OPEN) {
|
|
1688
|
+
this.debug(`${methodName}: connection is healthy, skipping`);
|
|
1689
|
+
return Promise.resolve();
|
|
1690
|
+
}
|
|
1691
|
+
} catch {
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
this.debug(
|
|
1695
|
+
`${methodName}: ${force ? "forcing immediate reconnection" : "reconnecting"}`
|
|
1696
|
+
);
|
|
1697
|
+
this.wsIntentionalDisconnect = false;
|
|
1698
|
+
if (this.wsReconnectTimer !== null) {
|
|
1699
|
+
this.debug(`${methodName}: cancelling pending reconnection timer`);
|
|
1700
|
+
clearTimeout(this.wsReconnectTimer);
|
|
1701
|
+
this.wsReconnectTimer = null;
|
|
1702
|
+
}
|
|
1703
|
+
this.wsReconnectAttempts = 0;
|
|
1704
|
+
if (this.conn !== null) {
|
|
1705
|
+
this.debug(`${methodName}: closing existing connection`);
|
|
1706
|
+
try {
|
|
1707
|
+
const existingSocket = await this.conn;
|
|
1708
|
+
if (this.currentWebSocket === existingSocket) {
|
|
1709
|
+
this.currentWebSocket = null;
|
|
1710
|
+
}
|
|
1711
|
+
if (existingSocket.readyState === WebSocket.OPEN || existingSocket.readyState === WebSocket.CONNECTING) {
|
|
1712
|
+
existingSocket.close();
|
|
1713
|
+
}
|
|
1714
|
+
} catch (error) {
|
|
1715
|
+
this.debug(`${methodName}: error closing existing connection:`, error);
|
|
1716
|
+
}
|
|
1717
|
+
this.conn = null;
|
|
1718
|
+
this.isConnecting = false;
|
|
1719
|
+
}
|
|
1720
|
+
if (this.context.company !== void 0 || this.context.user !== void 0) {
|
|
1721
|
+
this.debug(`${methodName}: reconnecting with existing context`);
|
|
1722
|
+
try {
|
|
1723
|
+
this.isConnecting = true;
|
|
1724
|
+
this.conn = this.wsConnect();
|
|
1725
|
+
const socket = await this.conn;
|
|
1726
|
+
this.isConnecting = false;
|
|
1727
|
+
await this.wsSendMessage(socket, this.context, true);
|
|
1728
|
+
this.debug(`${methodName}: reconnection successful`);
|
|
1729
|
+
} catch (error) {
|
|
1730
|
+
this.isConnecting = false;
|
|
1731
|
+
this.debug(`${methodName}: reconnection failed:`, error);
|
|
1732
|
+
}
|
|
1733
|
+
} else {
|
|
1734
|
+
this.debug(`${methodName}: no context set, skipping reconnection`);
|
|
1735
|
+
}
|
|
1736
|
+
return Promise.resolve();
|
|
1737
|
+
};
|
|
1324
1738
|
/**
|
|
1325
1739
|
* If using websocket mode, close the connection when done.
|
|
1326
1740
|
* In offline mode, this is a no-op.
|
|
@@ -1330,95 +1744,290 @@ var Schematic = class {
|
|
|
1330
1744
|
this.debug("cleanup: skipped (offline mode)");
|
|
1331
1745
|
return Promise.resolve();
|
|
1332
1746
|
}
|
|
1747
|
+
this.wsIntentionalDisconnect = true;
|
|
1748
|
+
if (this.wsReconnectTimer !== null) {
|
|
1749
|
+
clearTimeout(this.wsReconnectTimer);
|
|
1750
|
+
this.wsReconnectTimer = null;
|
|
1751
|
+
}
|
|
1752
|
+
this.stopRetryTimer();
|
|
1333
1753
|
if (this.conn) {
|
|
1334
1754
|
try {
|
|
1335
1755
|
const socket = await this.conn;
|
|
1756
|
+
if (this.currentWebSocket === socket) {
|
|
1757
|
+
this.debug(`Cleaning up current websocket tracking`);
|
|
1758
|
+
this.currentWebSocket = null;
|
|
1759
|
+
}
|
|
1336
1760
|
socket.close();
|
|
1337
1761
|
} catch (error) {
|
|
1338
|
-
console.
|
|
1762
|
+
console.warn("Error during cleanup:", error);
|
|
1339
1763
|
} finally {
|
|
1340
1764
|
this.conn = null;
|
|
1765
|
+
this.currentWebSocket = null;
|
|
1766
|
+
this.isConnecting = false;
|
|
1341
1767
|
}
|
|
1342
1768
|
}
|
|
1343
1769
|
};
|
|
1344
|
-
|
|
1345
|
-
|
|
1770
|
+
/**
|
|
1771
|
+
* Calculate the delay for the next reconnection attempt using exponential backoff with jitter.
|
|
1772
|
+
* This helps prevent dogpiling when the server recovers from an outage.
|
|
1773
|
+
*/
|
|
1774
|
+
calculateReconnectDelay = () => {
|
|
1775
|
+
const exponentialDelay = this.webSocketInitialRetryDelay * Math.pow(2, this.wsReconnectAttempts);
|
|
1776
|
+
const cappedDelay = Math.min(exponentialDelay, this.webSocketMaxRetryDelay);
|
|
1777
|
+
const jitter = Math.random() * cappedDelay * 0.5;
|
|
1778
|
+
const totalDelay = cappedDelay + jitter;
|
|
1779
|
+
this.debug(
|
|
1780
|
+
`Reconnect delay calculated: ${totalDelay.toFixed(0)}ms (attempt ${this.wsReconnectAttempts + 1}/${this.webSocketMaxReconnectAttempts})`
|
|
1781
|
+
);
|
|
1782
|
+
return totalDelay;
|
|
1783
|
+
};
|
|
1784
|
+
/**
|
|
1785
|
+
* Handle browser going offline
|
|
1786
|
+
*/
|
|
1787
|
+
handleNetworkOffline = async () => {
|
|
1788
|
+
if (this.conn !== null) {
|
|
1789
|
+
try {
|
|
1790
|
+
const socket = await this.conn;
|
|
1791
|
+
if (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING) {
|
|
1792
|
+
socket.close();
|
|
1793
|
+
}
|
|
1794
|
+
} catch (error) {
|
|
1795
|
+
this.debug("Error closing connection on offline:", error);
|
|
1796
|
+
}
|
|
1797
|
+
this.conn = null;
|
|
1798
|
+
}
|
|
1799
|
+
if (this.wsReconnectTimer !== null) {
|
|
1800
|
+
clearTimeout(this.wsReconnectTimer);
|
|
1801
|
+
this.wsReconnectTimer = null;
|
|
1802
|
+
}
|
|
1803
|
+
};
|
|
1804
|
+
/**
|
|
1805
|
+
* Handle browser coming back online
|
|
1806
|
+
*/
|
|
1807
|
+
handleNetworkOnline = () => {
|
|
1808
|
+
this.debug(
|
|
1809
|
+
"Network online, attempting reconnection and flushing queued events"
|
|
1810
|
+
);
|
|
1811
|
+
this.wsReconnectAttempts = 0;
|
|
1812
|
+
if (this.wsReconnectTimer !== null) {
|
|
1813
|
+
clearTimeout(this.wsReconnectTimer);
|
|
1814
|
+
this.wsReconnectTimer = null;
|
|
1815
|
+
}
|
|
1816
|
+
this.flushEventQueue().catch((error) => {
|
|
1817
|
+
this.debug("Error flushing event queue on network online:", error);
|
|
1818
|
+
});
|
|
1819
|
+
this.attemptReconnect();
|
|
1820
|
+
};
|
|
1821
|
+
/**
|
|
1822
|
+
* Attempt to reconnect the WebSocket connection with exponential backoff.
|
|
1823
|
+
* Called automatically when the connection closes unexpectedly.
|
|
1824
|
+
*/
|
|
1825
|
+
attemptReconnect = () => {
|
|
1826
|
+
if (this.wsReconnectAttempts >= this.webSocketMaxReconnectAttempts) {
|
|
1827
|
+
this.debug(
|
|
1828
|
+
`Maximum reconnection attempts (${this.webSocketMaxReconnectAttempts}) reached, giving up`
|
|
1829
|
+
);
|
|
1830
|
+
return;
|
|
1831
|
+
}
|
|
1832
|
+
if (this.wsReconnectTimer !== null) {
|
|
1833
|
+
this.debug(
|
|
1834
|
+
`Reconnection attempt already scheduled, ignoring duplicate request`
|
|
1835
|
+
);
|
|
1836
|
+
return;
|
|
1837
|
+
}
|
|
1838
|
+
const delay = this.calculateReconnectDelay();
|
|
1839
|
+
this.debug(
|
|
1840
|
+
`Scheduling reconnection attempt ${this.wsReconnectAttempts + 1}/${this.webSocketMaxReconnectAttempts} in ${delay.toFixed(0)}ms`
|
|
1841
|
+
);
|
|
1842
|
+
this.wsReconnectTimer = setTimeout(async () => {
|
|
1843
|
+
this.wsReconnectTimer = null;
|
|
1844
|
+
this.wsReconnectAttempts++;
|
|
1845
|
+
this.debug(
|
|
1846
|
+
`Attempting to reconnect (attempt ${this.wsReconnectAttempts}/${this.webSocketMaxReconnectAttempts})`
|
|
1847
|
+
);
|
|
1848
|
+
try {
|
|
1849
|
+
if (this.conn !== null) {
|
|
1850
|
+
this.debug(`Cleaning up existing connection before reconnection`);
|
|
1851
|
+
try {
|
|
1852
|
+
const existingSocket = await this.conn;
|
|
1853
|
+
if (this.currentWebSocket === existingSocket) {
|
|
1854
|
+
this.debug(`Existing websocket is current, will be replaced`);
|
|
1855
|
+
this.currentWebSocket = null;
|
|
1856
|
+
}
|
|
1857
|
+
if (existingSocket.readyState === WebSocket.OPEN || existingSocket.readyState === WebSocket.CONNECTING) {
|
|
1858
|
+
existingSocket.close();
|
|
1859
|
+
}
|
|
1860
|
+
} catch (error) {
|
|
1861
|
+
this.debug(`Error cleaning up existing connection:`, error);
|
|
1862
|
+
}
|
|
1863
|
+
this.conn = null;
|
|
1864
|
+
this.currentWebSocket = null;
|
|
1865
|
+
this.isConnecting = false;
|
|
1866
|
+
}
|
|
1867
|
+
this.isConnecting = true;
|
|
1868
|
+
try {
|
|
1869
|
+
this.conn = this.wsConnect();
|
|
1870
|
+
const socket = await this.conn;
|
|
1871
|
+
this.isConnecting = false;
|
|
1872
|
+
this.debug(`Reconnection context check:`, {
|
|
1873
|
+
hasCompany: this.context.company !== void 0,
|
|
1874
|
+
hasUser: this.context.user !== void 0,
|
|
1875
|
+
context: this.context
|
|
1876
|
+
});
|
|
1877
|
+
if (this.context.company !== void 0 || this.context.user !== void 0) {
|
|
1878
|
+
this.debug(`Reconnected, force re-sending context`);
|
|
1879
|
+
await this.wsSendMessage(socket, this.context, true);
|
|
1880
|
+
} else {
|
|
1881
|
+
this.debug(
|
|
1882
|
+
`No context to re-send after reconnection - websocket ready for new context`
|
|
1883
|
+
);
|
|
1884
|
+
this.debug(
|
|
1885
|
+
`Setting up tracking for reconnected websocket (no context to send)`
|
|
1886
|
+
);
|
|
1887
|
+
this.currentWebSocket = socket;
|
|
1888
|
+
}
|
|
1889
|
+
this.flushEventQueue().catch((error) => {
|
|
1890
|
+
this.debug(
|
|
1891
|
+
"Error flushing event queue after websocket reconnection:",
|
|
1892
|
+
error
|
|
1893
|
+
);
|
|
1894
|
+
});
|
|
1895
|
+
this.debug(`Reconnection successful`);
|
|
1896
|
+
} catch (error) {
|
|
1897
|
+
this.isConnecting = false;
|
|
1898
|
+
throw error;
|
|
1899
|
+
}
|
|
1900
|
+
} catch (error) {
|
|
1901
|
+
this.debug(`Reconnection attempt failed:`, error);
|
|
1902
|
+
}
|
|
1903
|
+
}, delay);
|
|
1904
|
+
};
|
|
1905
|
+
// Open a websocket connection with retry logic for timeouts
|
|
1906
|
+
wsConnect = async () => {
|
|
1346
1907
|
if (this.isOffline()) {
|
|
1347
1908
|
this.debug("wsConnect: skipped (offline mode)");
|
|
1348
|
-
|
|
1349
|
-
new Error("WebSocket connection skipped in offline mode")
|
|
1350
|
-
);
|
|
1909
|
+
throw new Error("WebSocket connection skipped in offline mode");
|
|
1351
1910
|
}
|
|
1911
|
+
let lastError = null;
|
|
1912
|
+
const maxAttempts = this.webSocketMaxConnectionAttempts;
|
|
1913
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
1914
|
+
try {
|
|
1915
|
+
const socket = await this.wsConnectOnce();
|
|
1916
|
+
this.wsReconnectAttempts = 0;
|
|
1917
|
+
return socket;
|
|
1918
|
+
} catch (error) {
|
|
1919
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
1920
|
+
const isTimeout = lastError.message === "WebSocket connection timeout";
|
|
1921
|
+
if (!isTimeout) {
|
|
1922
|
+
this.debug(
|
|
1923
|
+
`WebSocket connection failed with non-timeout error, not retrying:`,
|
|
1924
|
+
lastError.message
|
|
1925
|
+
);
|
|
1926
|
+
throw lastError;
|
|
1927
|
+
}
|
|
1928
|
+
if (attempt < maxAttempts - 1) {
|
|
1929
|
+
const baseDelay = this.webSocketInitialRetryDelay * Math.pow(2, attempt);
|
|
1930
|
+
const cappedDelay = Math.min(baseDelay, this.webSocketMaxRetryDelay);
|
|
1931
|
+
const jitter = cappedDelay * 0.2 * Math.random();
|
|
1932
|
+
const delay = cappedDelay + jitter;
|
|
1933
|
+
this.debug(
|
|
1934
|
+
`WebSocket connection timeout (attempt ${attempt + 1}/${maxAttempts}), retrying in ${delay.toFixed(0)}ms`
|
|
1935
|
+
);
|
|
1936
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1937
|
+
} else {
|
|
1938
|
+
this.debug(
|
|
1939
|
+
`WebSocket connection timeout (attempt ${attempt + 1}/${maxAttempts}), no more retries`
|
|
1940
|
+
);
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
throw lastError ?? new Error("WebSocket connection failed");
|
|
1945
|
+
};
|
|
1946
|
+
// Single attempt to open a websocket connection (no retry logic)
|
|
1947
|
+
wsConnectOnce = () => {
|
|
1352
1948
|
return new Promise((resolve, reject) => {
|
|
1353
1949
|
const wsUrl = `${this.webSocketUrl}/flags/bootstrap?apiKey=${this.apiKey}`;
|
|
1354
1950
|
this.debug(`connecting to WebSocket:`, wsUrl);
|
|
1355
1951
|
const webSocket = new WebSocket(wsUrl);
|
|
1952
|
+
const connectionId = Math.random().toString(36).substring(7);
|
|
1953
|
+
this.debug(`Creating WebSocket connection ${connectionId} to ${wsUrl}`);
|
|
1954
|
+
let timeoutId = null;
|
|
1955
|
+
let isResolved = false;
|
|
1956
|
+
timeoutId = setTimeout(() => {
|
|
1957
|
+
if (!isResolved) {
|
|
1958
|
+
isResolved = true;
|
|
1959
|
+
this.debug(
|
|
1960
|
+
`WebSocket connection timeout after ${this.webSocketConnectionTimeout}ms`
|
|
1961
|
+
);
|
|
1962
|
+
webSocket.close();
|
|
1963
|
+
reject(new Error("WebSocket connection timeout"));
|
|
1964
|
+
}
|
|
1965
|
+
}, this.webSocketConnectionTimeout);
|
|
1356
1966
|
webSocket.onopen = () => {
|
|
1357
|
-
|
|
1967
|
+
if (isResolved) return;
|
|
1968
|
+
isResolved = true;
|
|
1969
|
+
if (timeoutId !== null) {
|
|
1970
|
+
clearTimeout(timeoutId);
|
|
1971
|
+
}
|
|
1972
|
+
this.wsIntentionalDisconnect = false;
|
|
1973
|
+
this.debug(`WebSocket connection ${connectionId} opened successfully`);
|
|
1358
1974
|
resolve(webSocket);
|
|
1359
1975
|
};
|
|
1360
1976
|
webSocket.onerror = (error) => {
|
|
1361
|
-
|
|
1977
|
+
if (isResolved) return;
|
|
1978
|
+
isResolved = true;
|
|
1979
|
+
if (timeoutId !== null) {
|
|
1980
|
+
clearTimeout(timeoutId);
|
|
1981
|
+
}
|
|
1982
|
+
this.debug(`WebSocket connection ${connectionId} error:`, error);
|
|
1362
1983
|
reject(error);
|
|
1363
1984
|
};
|
|
1364
1985
|
webSocket.onclose = () => {
|
|
1365
|
-
|
|
1986
|
+
if (timeoutId !== null) {
|
|
1987
|
+
clearTimeout(timeoutId);
|
|
1988
|
+
}
|
|
1989
|
+
this.debug(`WebSocket connection ${connectionId} closed`);
|
|
1366
1990
|
this.conn = null;
|
|
1991
|
+
if (this.currentWebSocket === webSocket) {
|
|
1992
|
+
this.currentWebSocket = null;
|
|
1993
|
+
this.isConnecting = false;
|
|
1994
|
+
}
|
|
1995
|
+
if (!isResolved && !this.wsIntentionalDisconnect && this.webSocketReconnect) {
|
|
1996
|
+
this.attemptReconnect();
|
|
1997
|
+
}
|
|
1367
1998
|
};
|
|
1368
1999
|
});
|
|
1369
2000
|
};
|
|
1370
2001
|
// Send a message on the websocket indicating interest in a particular evaluation context
|
|
1371
2002
|
// and wait for the initial set of flag values to be returned
|
|
1372
|
-
wsSendMessage = (socket, context) => {
|
|
2003
|
+
wsSendMessage = (socket, context, forceContextSend = false) => {
|
|
1373
2004
|
if (this.isOffline()) {
|
|
1374
2005
|
this.debug("wsSendMessage: skipped (offline mode)");
|
|
1375
2006
|
this.setIsPending(false);
|
|
1376
2007
|
return Promise.resolve();
|
|
1377
2008
|
}
|
|
1378
2009
|
return new Promise((resolve, reject) => {
|
|
1379
|
-
if (contextString(context) == contextString(this.context)) {
|
|
2010
|
+
if (!forceContextSend && contextString(context) == contextString(this.context)) {
|
|
1380
2011
|
this.debug(`WebSocket context unchanged, skipping update`);
|
|
1381
2012
|
return resolve(this.setIsPending(false));
|
|
1382
2013
|
}
|
|
1383
|
-
this.debug(
|
|
2014
|
+
this.debug(
|
|
2015
|
+
forceContextSend ? `WebSocket force sending context (reconnection):` : `WebSocket context updated:`,
|
|
2016
|
+
context
|
|
2017
|
+
);
|
|
1384
2018
|
this.context = context;
|
|
1385
2019
|
const sendMessage = () => {
|
|
1386
2020
|
let resolved = false;
|
|
2021
|
+
const persistentMessageHandler = this.createPersistentMessageHandler(context);
|
|
1387
2022
|
const messageHandler = (event) => {
|
|
1388
|
-
|
|
1389
|
-
this.debug(`WebSocket message received:`, message);
|
|
1390
|
-
if (!(contextString(context) in this.checks)) {
|
|
1391
|
-
this.checks[contextString(context)] = {};
|
|
1392
|
-
}
|
|
1393
|
-
(message.flags ?? []).forEach((flag) => {
|
|
1394
|
-
const flagCheck = CheckFlagReturnFromJSON(flag);
|
|
1395
|
-
const contextStr = contextString(context);
|
|
1396
|
-
if (this.checks[contextStr] === void 0) {
|
|
1397
|
-
this.checks[contextStr] = {};
|
|
1398
|
-
}
|
|
1399
|
-
this.checks[contextStr][flagCheck.flag] = flagCheck;
|
|
1400
|
-
this.debug(`WebSocket flag update:`, {
|
|
1401
|
-
flag: flagCheck.flag,
|
|
1402
|
-
value: flagCheck.value,
|
|
1403
|
-
flagCheck
|
|
1404
|
-
});
|
|
1405
|
-
if (typeof flagCheck.featureUsageEvent === "string") {
|
|
1406
|
-
this.updateFeatureUsageEventMap(flagCheck);
|
|
1407
|
-
}
|
|
1408
|
-
if (this.flagCheckListeners[flag.flag]?.size > 0 || this.flagValueListeners[flag.flag]?.size > 0) {
|
|
1409
|
-
this.submitFlagCheckEvent(flagCheck.flag, flagCheck, context);
|
|
1410
|
-
}
|
|
1411
|
-
this.notifyFlagCheckListeners(flag.flag, flagCheck);
|
|
1412
|
-
this.notifyFlagValueListeners(flag.flag, flagCheck.value);
|
|
1413
|
-
});
|
|
1414
|
-
this.flushContextDependentEventQueue();
|
|
1415
|
-
this.setIsPending(false);
|
|
2023
|
+
persistentMessageHandler(event);
|
|
1416
2024
|
if (!resolved) {
|
|
1417
2025
|
resolved = true;
|
|
1418
2026
|
resolve();
|
|
1419
2027
|
}
|
|
1420
2028
|
};
|
|
1421
2029
|
socket.addEventListener("message", messageHandler);
|
|
2030
|
+
this.currentWebSocket = socket;
|
|
1422
2031
|
const clientVersion = this.additionalHeaders["X-Schematic-Client-Version"] ?? `schematic-js@${version}`;
|
|
1423
2032
|
const messagePayload = {
|
|
1424
2033
|
apiKey: this.apiKey,
|
|
@@ -1463,12 +2072,31 @@ var Schematic = class {
|
|
|
1463
2072
|
getFlagCheck = (flagKey) => {
|
|
1464
2073
|
const contextStr = contextString(this.context);
|
|
1465
2074
|
const checks = this.checks[contextStr] ?? {};
|
|
1466
|
-
|
|
2075
|
+
const check = checks[flagKey];
|
|
2076
|
+
if (check !== void 0) {
|
|
2077
|
+
return check;
|
|
2078
|
+
}
|
|
2079
|
+
if (flagKey in this.flagCheckDefaults || flagKey in this.flagValueDefaults) {
|
|
2080
|
+
return this.resolveFallbackCheckFlagReturn(
|
|
2081
|
+
flagKey,
|
|
2082
|
+
void 0,
|
|
2083
|
+
"Default value used"
|
|
2084
|
+
);
|
|
2085
|
+
}
|
|
2086
|
+
return void 0;
|
|
1467
2087
|
};
|
|
1468
2088
|
// flagValues state
|
|
1469
2089
|
getFlagValue = (flagKey) => {
|
|
1470
|
-
const
|
|
1471
|
-
|
|
2090
|
+
const contextStr = contextString(this.context);
|
|
2091
|
+
const checks = this.checks[contextStr] ?? {};
|
|
2092
|
+
const check = checks[flagKey];
|
|
2093
|
+
if (check?.value !== void 0) {
|
|
2094
|
+
return check.value;
|
|
2095
|
+
}
|
|
2096
|
+
if (flagKey in this.flagCheckDefaults || flagKey in this.flagValueDefaults) {
|
|
2097
|
+
return this.resolveFallbackValue(flagKey);
|
|
2098
|
+
}
|
|
2099
|
+
return void 0;
|
|
1472
2100
|
};
|
|
1473
2101
|
/** Register an event listener that will be notified with the boolean value for a given flag when this value changes */
|
|
1474
2102
|
addFlagValueListener = (flagKey, listener) => {
|
|
@@ -1526,7 +2154,17 @@ var Schematic = class {
|
|
|
1526
2154
|
{ value }
|
|
1527
2155
|
);
|
|
1528
2156
|
}
|
|
1529
|
-
listeners.forEach((listener) =>
|
|
2157
|
+
listeners.forEach((listener, index) => {
|
|
2158
|
+
this.debug(`Calling listener ${index} for flag ${flagKey}`, {
|
|
2159
|
+
flagKey,
|
|
2160
|
+
value
|
|
2161
|
+
});
|
|
2162
|
+
notifyFlagValueListener(listener, value);
|
|
2163
|
+
this.debug(`Listener ${index} for flag ${flagKey} completed`, {
|
|
2164
|
+
flagKey,
|
|
2165
|
+
value
|
|
2166
|
+
});
|
|
2167
|
+
});
|
|
1530
2168
|
};
|
|
1531
2169
|
};
|
|
1532
2170
|
var notifyPendingListener = (listener, value) => {
|
|
@@ -1555,7 +2193,7 @@ var notifyFlagValueListener = (listener, value) => {
|
|
|
1555
2193
|
var import_vue = require("vue");
|
|
1556
2194
|
|
|
1557
2195
|
// src/version.ts
|
|
1558
|
-
var version2 = "
|
|
2196
|
+
var version2 = "1.2.16";
|
|
1559
2197
|
|
|
1560
2198
|
// src/context/schematic.ts
|
|
1561
2199
|
var SchematicSymbol = Symbol("schematic");
|