@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
|
@@ -754,7 +754,7 @@ function contextString(context) {
|
|
|
754
754
|
}, {});
|
|
755
755
|
return JSON.stringify(sortedContext);
|
|
756
756
|
}
|
|
757
|
-
var version = "1.2.
|
|
757
|
+
var version = "1.2.16";
|
|
758
758
|
var anonymousIdKey = "schematicId";
|
|
759
759
|
var Schematic = class {
|
|
760
760
|
additionalHeaders = {};
|
|
@@ -776,6 +776,30 @@ var Schematic = class {
|
|
|
776
776
|
checks = {};
|
|
777
777
|
featureUsageEventMap = {};
|
|
778
778
|
webSocketUrl = "wss://api.schematichq.com";
|
|
779
|
+
webSocketConnectionTimeout = 1e4;
|
|
780
|
+
webSocketReconnect = true;
|
|
781
|
+
webSocketMaxReconnectAttempts = 7;
|
|
782
|
+
// Max attempts after connection disrupted
|
|
783
|
+
webSocketMaxConnectionAttempts = 3;
|
|
784
|
+
// Max attempts for initial connection
|
|
785
|
+
webSocketInitialRetryDelay = 1e3;
|
|
786
|
+
webSocketMaxRetryDelay = 3e4;
|
|
787
|
+
wsReconnectAttempts = 0;
|
|
788
|
+
wsReconnectTimer = null;
|
|
789
|
+
wsIntentionalDisconnect = false;
|
|
790
|
+
currentWebSocket = null;
|
|
791
|
+
isConnecting = false;
|
|
792
|
+
maxEventQueueSize = 100;
|
|
793
|
+
// Prevent memory issues with very long network outages
|
|
794
|
+
maxEventRetries = 5;
|
|
795
|
+
// Maximum retry attempts for failed events
|
|
796
|
+
eventRetryInitialDelay = 1e3;
|
|
797
|
+
// Initial retry delay in ms
|
|
798
|
+
eventRetryMaxDelay = 3e4;
|
|
799
|
+
// Maximum retry delay in ms
|
|
800
|
+
retryTimer = null;
|
|
801
|
+
flagValueDefaults = {};
|
|
802
|
+
flagCheckDefaults = {};
|
|
779
803
|
constructor(apiKey, options) {
|
|
780
804
|
this.apiKey = apiKey;
|
|
781
805
|
this.eventQueue = [];
|
|
@@ -807,8 +831,13 @@ var Schematic = class {
|
|
|
807
831
|
};
|
|
808
832
|
if (options?.storage) {
|
|
809
833
|
this.storage = options.storage;
|
|
810
|
-
} else
|
|
811
|
-
|
|
834
|
+
} else {
|
|
835
|
+
try {
|
|
836
|
+
if (typeof localStorage !== "undefined") {
|
|
837
|
+
this.storage = localStorage;
|
|
838
|
+
}
|
|
839
|
+
} catch {
|
|
840
|
+
}
|
|
812
841
|
}
|
|
813
842
|
if (options?.apiUrl !== void 0) {
|
|
814
843
|
this.apiUrl = options.apiUrl;
|
|
@@ -819,11 +848,54 @@ var Schematic = class {
|
|
|
819
848
|
if (options?.webSocketUrl !== void 0) {
|
|
820
849
|
this.webSocketUrl = options.webSocketUrl;
|
|
821
850
|
}
|
|
851
|
+
if (options?.webSocketConnectionTimeout !== void 0) {
|
|
852
|
+
this.webSocketConnectionTimeout = options.webSocketConnectionTimeout;
|
|
853
|
+
}
|
|
854
|
+
if (options?.webSocketReconnect !== void 0) {
|
|
855
|
+
this.webSocketReconnect = options.webSocketReconnect;
|
|
856
|
+
}
|
|
857
|
+
if (options?.webSocketMaxReconnectAttempts !== void 0) {
|
|
858
|
+
this.webSocketMaxReconnectAttempts = options.webSocketMaxReconnectAttempts;
|
|
859
|
+
}
|
|
860
|
+
if (options?.webSocketInitialRetryDelay !== void 0) {
|
|
861
|
+
this.webSocketInitialRetryDelay = options.webSocketInitialRetryDelay;
|
|
862
|
+
}
|
|
863
|
+
if (options?.webSocketMaxRetryDelay !== void 0) {
|
|
864
|
+
this.webSocketMaxRetryDelay = options.webSocketMaxRetryDelay;
|
|
865
|
+
}
|
|
866
|
+
if (options?.maxEventQueueSize !== void 0) {
|
|
867
|
+
this.maxEventQueueSize = options.maxEventQueueSize;
|
|
868
|
+
}
|
|
869
|
+
if (options?.maxEventRetries !== void 0) {
|
|
870
|
+
this.maxEventRetries = options.maxEventRetries;
|
|
871
|
+
}
|
|
872
|
+
if (options?.eventRetryInitialDelay !== void 0) {
|
|
873
|
+
this.eventRetryInitialDelay = options.eventRetryInitialDelay;
|
|
874
|
+
}
|
|
875
|
+
if (options?.eventRetryMaxDelay !== void 0) {
|
|
876
|
+
this.eventRetryMaxDelay = options.eventRetryMaxDelay;
|
|
877
|
+
}
|
|
878
|
+
if (options?.flagValueDefaults !== void 0) {
|
|
879
|
+
this.flagValueDefaults = options.flagValueDefaults;
|
|
880
|
+
}
|
|
881
|
+
if (options?.flagCheckDefaults !== void 0) {
|
|
882
|
+
this.flagCheckDefaults = options.flagCheckDefaults;
|
|
883
|
+
}
|
|
822
884
|
if (typeof window !== "undefined" && window?.addEventListener) {
|
|
823
885
|
window.addEventListener("beforeunload", () => {
|
|
824
886
|
this.flushEventQueue();
|
|
825
887
|
this.flushContextDependentEventQueue();
|
|
826
888
|
});
|
|
889
|
+
if (this.useWebSocket) {
|
|
890
|
+
window.addEventListener("offline", () => {
|
|
891
|
+
this.debug("Browser went offline, closing WebSocket connection");
|
|
892
|
+
this.handleNetworkOffline();
|
|
893
|
+
});
|
|
894
|
+
window.addEventListener("online", () => {
|
|
895
|
+
this.debug("Browser came online, attempting to reconnect WebSocket");
|
|
896
|
+
this.handleNetworkOnline();
|
|
897
|
+
});
|
|
898
|
+
}
|
|
827
899
|
}
|
|
828
900
|
if (this.offlineEnabled) {
|
|
829
901
|
this.debug(
|
|
@@ -833,6 +905,66 @@ var Schematic = class {
|
|
|
833
905
|
this.debug("Initialized with debug mode enabled");
|
|
834
906
|
}
|
|
835
907
|
}
|
|
908
|
+
/**
|
|
909
|
+
* Resolve fallback value according to priority order:
|
|
910
|
+
* 1. Callsite fallback value (if provided)
|
|
911
|
+
* 2. Boolean value from flagCheckDefaults initialization option
|
|
912
|
+
* 3. Boolean value from flagValueDefaults initialization option
|
|
913
|
+
* 4. Default to false
|
|
914
|
+
*/
|
|
915
|
+
resolveFallbackValue(key, callsiteFallback) {
|
|
916
|
+
if (callsiteFallback !== void 0) {
|
|
917
|
+
return callsiteFallback;
|
|
918
|
+
}
|
|
919
|
+
if (key in this.flagCheckDefaults) {
|
|
920
|
+
return this.flagCheckDefaults[key].value;
|
|
921
|
+
}
|
|
922
|
+
if (key in this.flagValueDefaults) {
|
|
923
|
+
return this.flagValueDefaults[key];
|
|
924
|
+
}
|
|
925
|
+
return false;
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* Resolve complete CheckFlagReturn object according to priority order:
|
|
929
|
+
* 1. Use callsite fallback for boolean value, construct CheckFlagReturn
|
|
930
|
+
* 2. Use flagCheckDefaults if available for this flag
|
|
931
|
+
* 3. Use flagValueDefaults if available for this flag, construct CheckFlagReturn
|
|
932
|
+
* 4. Default CheckFlagReturn with value: false
|
|
933
|
+
*/
|
|
934
|
+
resolveFallbackCheckFlagReturn(key, callsiteFallback, reason = "Fallback value used", error) {
|
|
935
|
+
if (callsiteFallback !== void 0) {
|
|
936
|
+
return {
|
|
937
|
+
flag: key,
|
|
938
|
+
value: callsiteFallback,
|
|
939
|
+
reason,
|
|
940
|
+
error
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
if (key in this.flagCheckDefaults) {
|
|
944
|
+
const defaultReturn = this.flagCheckDefaults[key];
|
|
945
|
+
return {
|
|
946
|
+
...defaultReturn,
|
|
947
|
+
flag: key,
|
|
948
|
+
// Ensure flag matches the requested key
|
|
949
|
+
reason: error !== void 0 ? reason : defaultReturn.reason,
|
|
950
|
+
error
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
if (key in this.flagValueDefaults) {
|
|
954
|
+
return {
|
|
955
|
+
flag: key,
|
|
956
|
+
value: this.flagValueDefaults[key],
|
|
957
|
+
reason,
|
|
958
|
+
error
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
return {
|
|
962
|
+
flag: key,
|
|
963
|
+
value: false,
|
|
964
|
+
reason,
|
|
965
|
+
error
|
|
966
|
+
};
|
|
967
|
+
}
|
|
836
968
|
/**
|
|
837
969
|
* Get value for a single flag.
|
|
838
970
|
* In WebSocket mode, returns cached values if connection is active, otherwise establishes
|
|
@@ -841,16 +973,21 @@ var Schematic = class {
|
|
|
841
973
|
* In REST mode, makes an API call for each check.
|
|
842
974
|
*/
|
|
843
975
|
async checkFlag(options) {
|
|
844
|
-
const { fallback
|
|
976
|
+
const { fallback, key } = options;
|
|
845
977
|
const context = options.context || this.context;
|
|
846
978
|
const contextStr = contextString(context);
|
|
847
979
|
this.debug(`checkFlag: ${key}`, { context, fallback });
|
|
848
980
|
if (this.isOffline()) {
|
|
981
|
+
const resolvedFallbackResult = this.resolveFallbackCheckFlagReturn(
|
|
982
|
+
key,
|
|
983
|
+
fallback,
|
|
984
|
+
"Offline mode - using initialization defaults"
|
|
985
|
+
);
|
|
849
986
|
this.debug(`checkFlag offline result: ${key}`, {
|
|
850
|
-
value:
|
|
987
|
+
value: resolvedFallbackResult.value,
|
|
851
988
|
offlineMode: true
|
|
852
989
|
});
|
|
853
|
-
return
|
|
990
|
+
return resolvedFallbackResult.value;
|
|
854
991
|
}
|
|
855
992
|
if (!this.useWebSocket) {
|
|
856
993
|
const requestUrl = `${this.apiUrl}/flags/${key}/check`;
|
|
@@ -877,15 +1014,15 @@ var Schematic = class {
|
|
|
877
1014
|
this.submitFlagCheckEvent(key, result, context);
|
|
878
1015
|
return result.value;
|
|
879
1016
|
}).catch((error) => {
|
|
880
|
-
console.
|
|
881
|
-
const errorResult =
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
error
|
|
886
|
-
|
|
1017
|
+
console.warn("There was a problem with the fetch operation:", error);
|
|
1018
|
+
const errorResult = this.resolveFallbackCheckFlagReturn(
|
|
1019
|
+
key,
|
|
1020
|
+
fallback,
|
|
1021
|
+
"API request failed",
|
|
1022
|
+
error instanceof Error ? error.message : String(error)
|
|
1023
|
+
);
|
|
887
1024
|
this.submitFlagCheckEvent(key, errorResult, context);
|
|
888
|
-
return
|
|
1025
|
+
return errorResult.value;
|
|
889
1026
|
});
|
|
890
1027
|
}
|
|
891
1028
|
try {
|
|
@@ -895,12 +1032,12 @@ var Schematic = class {
|
|
|
895
1032
|
return existingVals[key].value;
|
|
896
1033
|
}
|
|
897
1034
|
if (this.isOffline()) {
|
|
898
|
-
return fallback;
|
|
1035
|
+
return this.resolveFallbackValue(key, fallback);
|
|
899
1036
|
}
|
|
900
1037
|
try {
|
|
901
1038
|
await this.setContext(context);
|
|
902
1039
|
} catch (error) {
|
|
903
|
-
console.
|
|
1040
|
+
console.warn(
|
|
904
1041
|
"WebSocket connection failed, falling back to REST:",
|
|
905
1042
|
error
|
|
906
1043
|
);
|
|
@@ -908,10 +1045,10 @@ var Schematic = class {
|
|
|
908
1045
|
}
|
|
909
1046
|
const contextVals = this.checks[contextStr] ?? {};
|
|
910
1047
|
const flagCheck = contextVals[key];
|
|
911
|
-
const result = flagCheck?.value ?? fallback;
|
|
1048
|
+
const result = flagCheck?.value ?? this.resolveFallbackValue(key, fallback);
|
|
912
1049
|
this.debug(
|
|
913
1050
|
`checkFlag WebSocket result: ${key}`,
|
|
914
|
-
typeof flagCheck !== "undefined" ? flagCheck : { value:
|
|
1051
|
+
typeof flagCheck !== "undefined" ? flagCheck : { value: result, fallbackUsed: true }
|
|
915
1052
|
);
|
|
916
1053
|
if (typeof flagCheck !== "undefined") {
|
|
917
1054
|
this.submitFlagCheckEvent(key, flagCheck, context);
|
|
@@ -919,14 +1056,14 @@ var Schematic = class {
|
|
|
919
1056
|
return result;
|
|
920
1057
|
} catch (error) {
|
|
921
1058
|
console.error("Unexpected error in checkFlag:", error);
|
|
922
|
-
const errorResult =
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
error
|
|
927
|
-
|
|
1059
|
+
const errorResult = this.resolveFallbackCheckFlagReturn(
|
|
1060
|
+
key,
|
|
1061
|
+
fallback,
|
|
1062
|
+
"Unexpected error in flag check",
|
|
1063
|
+
error instanceof Error ? error.message : String(error)
|
|
1064
|
+
);
|
|
928
1065
|
this.submitFlagCheckEvent(key, errorResult, context);
|
|
929
|
-
return
|
|
1066
|
+
return errorResult.value;
|
|
930
1067
|
}
|
|
931
1068
|
}
|
|
932
1069
|
/**
|
|
@@ -938,6 +1075,49 @@ var Schematic = class {
|
|
|
938
1075
|
console.log(`[Schematic] ${message}`, ...args);
|
|
939
1076
|
}
|
|
940
1077
|
}
|
|
1078
|
+
/**
|
|
1079
|
+
* Create a persistent message handler for websocket flag updates
|
|
1080
|
+
*/
|
|
1081
|
+
createPersistentMessageHandler(context) {
|
|
1082
|
+
return (event) => {
|
|
1083
|
+
const message = JSON.parse(event.data);
|
|
1084
|
+
this.debug(`WebSocket persistent message received:`, message);
|
|
1085
|
+
if (!(contextString(context) in this.checks)) {
|
|
1086
|
+
this.checks[contextString(context)] = {};
|
|
1087
|
+
}
|
|
1088
|
+
(message.flags ?? []).forEach((flag) => {
|
|
1089
|
+
const flagCheck = CheckFlagReturnFromJSON(flag);
|
|
1090
|
+
const contextStr = contextString(context);
|
|
1091
|
+
if (this.checks[contextStr] === void 0) {
|
|
1092
|
+
this.checks[contextStr] = {};
|
|
1093
|
+
}
|
|
1094
|
+
this.checks[contextStr][flagCheck.flag] = flagCheck;
|
|
1095
|
+
this.debug(`WebSocket flag update:`, {
|
|
1096
|
+
flag: flagCheck.flag,
|
|
1097
|
+
value: flagCheck.value,
|
|
1098
|
+
flagCheck
|
|
1099
|
+
});
|
|
1100
|
+
if (typeof flagCheck.featureUsageEvent === "string") {
|
|
1101
|
+
this.updateFeatureUsageEventMap(flagCheck);
|
|
1102
|
+
}
|
|
1103
|
+
if ((this.flagCheckListeners[flag.flag]?.size ?? 0) > 0 || (this.flagValueListeners[flag.flag]?.size ?? 0) > 0) {
|
|
1104
|
+
this.submitFlagCheckEvent(flagCheck.flag, flagCheck, context);
|
|
1105
|
+
}
|
|
1106
|
+
this.debug(`About to notify listeners for flag ${flag.flag}`, {
|
|
1107
|
+
flag: flag.flag,
|
|
1108
|
+
value: flagCheck.value
|
|
1109
|
+
});
|
|
1110
|
+
this.notifyFlagCheckListeners(flag.flag, flagCheck);
|
|
1111
|
+
this.notifyFlagValueListeners(flag.flag, flagCheck.value);
|
|
1112
|
+
this.debug(`Finished notifying listeners for flag ${flag.flag}`, {
|
|
1113
|
+
flag: flag.flag,
|
|
1114
|
+
value: flagCheck.value
|
|
1115
|
+
});
|
|
1116
|
+
});
|
|
1117
|
+
this.flushContextDependentEventQueue();
|
|
1118
|
+
this.setIsPending(false);
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
941
1121
|
/**
|
|
942
1122
|
* Helper function to check if client is in offline mode
|
|
943
1123
|
*/
|
|
@@ -969,11 +1149,12 @@ var Schematic = class {
|
|
|
969
1149
|
*/
|
|
970
1150
|
async fallbackToRest(key, context, fallback) {
|
|
971
1151
|
if (this.isOffline()) {
|
|
1152
|
+
const resolvedFallback = this.resolveFallbackValue(key, fallback);
|
|
972
1153
|
this.debug(`fallbackToRest offline result: ${key}`, {
|
|
973
|
-
value:
|
|
1154
|
+
value: resolvedFallback,
|
|
974
1155
|
offlineMode: true
|
|
975
1156
|
});
|
|
976
|
-
return
|
|
1157
|
+
return resolvedFallback;
|
|
977
1158
|
}
|
|
978
1159
|
try {
|
|
979
1160
|
const requestUrl = `${this.apiUrl}/flags/${key}/check`;
|
|
@@ -999,15 +1180,15 @@ var Schematic = class {
|
|
|
999
1180
|
this.submitFlagCheckEvent(key, result, context);
|
|
1000
1181
|
return result.value;
|
|
1001
1182
|
} catch (error) {
|
|
1002
|
-
console.
|
|
1003
|
-
const errorResult =
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
error
|
|
1008
|
-
|
|
1183
|
+
console.warn("REST API call failed, using fallback value:", error);
|
|
1184
|
+
const errorResult = this.resolveFallbackCheckFlagReturn(
|
|
1185
|
+
key,
|
|
1186
|
+
fallback,
|
|
1187
|
+
"API request failed (fallback)",
|
|
1188
|
+
error instanceof Error ? error.message : String(error)
|
|
1189
|
+
);
|
|
1009
1190
|
this.submitFlagCheckEvent(key, errorResult, context);
|
|
1010
|
-
return
|
|
1191
|
+
return errorResult.value;
|
|
1011
1192
|
}
|
|
1012
1193
|
}
|
|
1013
1194
|
/**
|
|
@@ -1048,7 +1229,7 @@ var Schematic = class {
|
|
|
1048
1229
|
{}
|
|
1049
1230
|
);
|
|
1050
1231
|
}).catch((error) => {
|
|
1051
|
-
console.
|
|
1232
|
+
console.warn("There was a problem with the fetch operation:", error);
|
|
1052
1233
|
return {};
|
|
1053
1234
|
});
|
|
1054
1235
|
};
|
|
@@ -1059,14 +1240,12 @@ var Schematic = class {
|
|
|
1059
1240
|
*/
|
|
1060
1241
|
identify = (body) => {
|
|
1061
1242
|
this.debug(`identify:`, body);
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
}
|
|
1068
|
-
console.error("Error setting context:", error);
|
|
1069
|
-
}
|
|
1243
|
+
this.setContext({
|
|
1244
|
+
company: body.company?.keys,
|
|
1245
|
+
user: body.keys
|
|
1246
|
+
}).catch((error) => {
|
|
1247
|
+
console.warn("Error setting context:", error);
|
|
1248
|
+
});
|
|
1070
1249
|
return this.handleEvent("identify", body);
|
|
1071
1250
|
};
|
|
1072
1251
|
/**
|
|
@@ -1088,12 +1267,42 @@ var Schematic = class {
|
|
|
1088
1267
|
try {
|
|
1089
1268
|
this.setIsPending(true);
|
|
1090
1269
|
if (!this.conn) {
|
|
1091
|
-
|
|
1270
|
+
if (this.isConnecting) {
|
|
1271
|
+
this.debug(
|
|
1272
|
+
`Connection already in progress, waiting for it to complete`
|
|
1273
|
+
);
|
|
1274
|
+
while (this.isConnecting && this.conn === null) {
|
|
1275
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1276
|
+
}
|
|
1277
|
+
if (this.conn !== null) {
|
|
1278
|
+
const socket2 = await this.conn;
|
|
1279
|
+
await this.wsSendMessage(socket2, context);
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
if (this.wsReconnectTimer !== null) {
|
|
1284
|
+
this.debug(
|
|
1285
|
+
`Cancelling scheduled reconnection, connecting immediately`
|
|
1286
|
+
);
|
|
1287
|
+
clearTimeout(this.wsReconnectTimer);
|
|
1288
|
+
this.wsReconnectTimer = null;
|
|
1289
|
+
}
|
|
1290
|
+
this.isConnecting = true;
|
|
1291
|
+
try {
|
|
1292
|
+
this.conn = this.wsConnect();
|
|
1293
|
+
const socket2 = await this.conn;
|
|
1294
|
+
this.isConnecting = false;
|
|
1295
|
+
await this.wsSendMessage(socket2, context);
|
|
1296
|
+
return;
|
|
1297
|
+
} catch (error) {
|
|
1298
|
+
this.isConnecting = false;
|
|
1299
|
+
throw error;
|
|
1300
|
+
}
|
|
1092
1301
|
}
|
|
1093
1302
|
const socket = await this.conn;
|
|
1094
1303
|
await this.wsSendMessage(socket, context);
|
|
1095
1304
|
} catch (error) {
|
|
1096
|
-
console.
|
|
1305
|
+
console.warn("Failed to establish WebSocket connection:", error);
|
|
1097
1306
|
throw error;
|
|
1098
1307
|
}
|
|
1099
1308
|
};
|
|
@@ -1217,11 +1426,57 @@ var Schematic = class {
|
|
|
1217
1426
|
}
|
|
1218
1427
|
}
|
|
1219
1428
|
};
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1429
|
+
startRetryTimer = () => {
|
|
1430
|
+
if (this.retryTimer !== null) {
|
|
1431
|
+
return;
|
|
1432
|
+
}
|
|
1433
|
+
this.retryTimer = setInterval(() => {
|
|
1434
|
+
this.flushEventQueue().catch((error) => {
|
|
1435
|
+
this.debug("Error in retry timer flush:", error);
|
|
1436
|
+
});
|
|
1437
|
+
if (this.eventQueue.length === 0) {
|
|
1438
|
+
this.stopRetryTimer();
|
|
1439
|
+
}
|
|
1440
|
+
}, 5e3);
|
|
1441
|
+
this.debug("Started retry timer");
|
|
1442
|
+
};
|
|
1443
|
+
stopRetryTimer = () => {
|
|
1444
|
+
if (this.retryTimer !== null) {
|
|
1445
|
+
clearInterval(this.retryTimer);
|
|
1446
|
+
this.retryTimer = null;
|
|
1447
|
+
this.debug("Stopped retry timer");
|
|
1448
|
+
}
|
|
1449
|
+
};
|
|
1450
|
+
flushEventQueue = async () => {
|
|
1451
|
+
if (this.eventQueue.length === 0) {
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
const now = Date.now();
|
|
1455
|
+
const readyEvents = [];
|
|
1456
|
+
const notReadyEvents = [];
|
|
1457
|
+
for (const event of this.eventQueue) {
|
|
1458
|
+
if (event.next_retry_at === void 0 || event.next_retry_at <= now) {
|
|
1459
|
+
readyEvents.push(event);
|
|
1460
|
+
} else {
|
|
1461
|
+
notReadyEvents.push(event);
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
if (readyEvents.length === 0) {
|
|
1465
|
+
this.debug(
|
|
1466
|
+
`No events ready for retry yet (${notReadyEvents.length} still in backoff)`
|
|
1467
|
+
);
|
|
1468
|
+
return;
|
|
1469
|
+
}
|
|
1470
|
+
this.debug(
|
|
1471
|
+
`Flushing event queue: ${readyEvents.length} ready, ${notReadyEvents.length} waiting`
|
|
1472
|
+
);
|
|
1473
|
+
this.eventQueue = notReadyEvents;
|
|
1474
|
+
for (const event of readyEvents) {
|
|
1475
|
+
try {
|
|
1476
|
+
await this.sendEvent(event);
|
|
1477
|
+
this.debug(`Queued event sent successfully:`, event.type);
|
|
1478
|
+
} catch (error) {
|
|
1479
|
+
this.debug(`Failed to send queued event:`, error);
|
|
1225
1480
|
}
|
|
1226
1481
|
}
|
|
1227
1482
|
};
|
|
@@ -1269,12 +1524,47 @@ var Schematic = class {
|
|
|
1269
1524
|
},
|
|
1270
1525
|
body: payload
|
|
1271
1526
|
});
|
|
1527
|
+
if (!response.ok) {
|
|
1528
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
1529
|
+
}
|
|
1272
1530
|
this.debug(`event sent:`, {
|
|
1273
1531
|
status: response.status,
|
|
1274
1532
|
statusText: response.statusText
|
|
1275
1533
|
});
|
|
1276
1534
|
} catch (error) {
|
|
1277
|
-
|
|
1535
|
+
const retryCount = (event.retry_count ?? 0) + 1;
|
|
1536
|
+
if (retryCount <= this.maxEventRetries) {
|
|
1537
|
+
this.debug(
|
|
1538
|
+
`Event failed to send (attempt ${retryCount}/${this.maxEventRetries}), queueing for retry:`,
|
|
1539
|
+
error
|
|
1540
|
+
);
|
|
1541
|
+
const baseDelay = this.eventRetryInitialDelay * Math.pow(2, retryCount - 1);
|
|
1542
|
+
const jitterDelay = Math.min(baseDelay, this.eventRetryMaxDelay);
|
|
1543
|
+
const nextRetryAt = Date.now() + jitterDelay;
|
|
1544
|
+
const retryEvent = {
|
|
1545
|
+
...event,
|
|
1546
|
+
retry_count: retryCount,
|
|
1547
|
+
next_retry_at: nextRetryAt
|
|
1548
|
+
};
|
|
1549
|
+
if (this.eventQueue.length < this.maxEventQueueSize) {
|
|
1550
|
+
this.eventQueue.push(retryEvent);
|
|
1551
|
+
this.debug(
|
|
1552
|
+
`Event queued for retry in ${jitterDelay}ms (${this.eventQueue.length}/${this.maxEventQueueSize})`
|
|
1553
|
+
);
|
|
1554
|
+
} else {
|
|
1555
|
+
this.debug(
|
|
1556
|
+
`Event queue full (${this.maxEventQueueSize}), dropping oldest event`
|
|
1557
|
+
);
|
|
1558
|
+
this.eventQueue.shift();
|
|
1559
|
+
this.eventQueue.push(retryEvent);
|
|
1560
|
+
}
|
|
1561
|
+
this.startRetryTimer();
|
|
1562
|
+
} else {
|
|
1563
|
+
this.debug(
|
|
1564
|
+
`Event failed permanently after ${this.maxEventRetries} attempts, dropping:`,
|
|
1565
|
+
error
|
|
1566
|
+
);
|
|
1567
|
+
}
|
|
1278
1568
|
}
|
|
1279
1569
|
return Promise.resolve();
|
|
1280
1570
|
};
|
|
@@ -1285,6 +1575,130 @@ var Schematic = class {
|
|
|
1285
1575
|
/**
|
|
1286
1576
|
* Websocket management
|
|
1287
1577
|
*/
|
|
1578
|
+
/**
|
|
1579
|
+
* Force an immediate WebSocket reconnection.
|
|
1580
|
+
* This is useful when the application returns from a background state (e.g., mobile app
|
|
1581
|
+
* coming back to foreground) and wants to immediately re-establish the connection
|
|
1582
|
+
* rather than waiting for the exponential backoff timer.
|
|
1583
|
+
*
|
|
1584
|
+
* This method will:
|
|
1585
|
+
* - Cancel any pending reconnection timer
|
|
1586
|
+
* - Reset the reconnection attempt counter
|
|
1587
|
+
* - Close any existing connection
|
|
1588
|
+
* - Immediately attempt to reconnect
|
|
1589
|
+
* - Re-send the current context to get fresh flag values
|
|
1590
|
+
*
|
|
1591
|
+
* Use this when you need guaranteed fresh values (e.g., after an in-app purchase).
|
|
1592
|
+
*
|
|
1593
|
+
* @example
|
|
1594
|
+
* ```typescript
|
|
1595
|
+
* // React Native example: reconnect when app comes to foreground
|
|
1596
|
+
* useEffect(() => {
|
|
1597
|
+
* const subscription = AppState.addEventListener("change", (state) => {
|
|
1598
|
+
* if (state === "active") {
|
|
1599
|
+
* client.forceReconnect();
|
|
1600
|
+
* }
|
|
1601
|
+
* });
|
|
1602
|
+
* return () => subscription.remove();
|
|
1603
|
+
* }, [client]);
|
|
1604
|
+
* ```
|
|
1605
|
+
*/
|
|
1606
|
+
forceReconnect = async () => {
|
|
1607
|
+
return this.reconnect({ force: true });
|
|
1608
|
+
};
|
|
1609
|
+
/**
|
|
1610
|
+
* Reconnect the WebSocket connection only if the current connection is unhealthy.
|
|
1611
|
+
* This is useful when the application returns from a background state and wants to
|
|
1612
|
+
* ensure a healthy connection exists, but doesn't need to force a reconnection if
|
|
1613
|
+
* the connection is still active.
|
|
1614
|
+
*
|
|
1615
|
+
* This method will:
|
|
1616
|
+
* - Check if an existing connection is healthy (readyState === OPEN)
|
|
1617
|
+
* - If healthy, return immediately without reconnecting
|
|
1618
|
+
* - If unhealthy, perform the same reconnection logic as forceReconnect()
|
|
1619
|
+
*
|
|
1620
|
+
* Use this when you want efficient reconnection that avoids unnecessary disconnects.
|
|
1621
|
+
*
|
|
1622
|
+
* @example
|
|
1623
|
+
* ```typescript
|
|
1624
|
+
* // React Native example: reconnect only if needed when app comes to foreground
|
|
1625
|
+
* useEffect(() => {
|
|
1626
|
+
* const subscription = AppState.addEventListener("change", (state) => {
|
|
1627
|
+
* if (state === "active") {
|
|
1628
|
+
* client.reconnectIfNeeded();
|
|
1629
|
+
* }
|
|
1630
|
+
* });
|
|
1631
|
+
* return () => subscription.remove();
|
|
1632
|
+
* }, [client]);
|
|
1633
|
+
* ```
|
|
1634
|
+
*/
|
|
1635
|
+
reconnectIfNeeded = async () => {
|
|
1636
|
+
return this.reconnect({ force: false });
|
|
1637
|
+
};
|
|
1638
|
+
/**
|
|
1639
|
+
* Internal method to handle reconnection logic for both forceReconnect and reconnectIfNeeded.
|
|
1640
|
+
*/
|
|
1641
|
+
reconnect = async (options) => {
|
|
1642
|
+
const { force } = options;
|
|
1643
|
+
const methodName = force ? "forceReconnect" : "reconnectIfNeeded";
|
|
1644
|
+
if (this.isOffline()) {
|
|
1645
|
+
this.debug(`${methodName}: skipped (offline mode)`);
|
|
1646
|
+
return Promise.resolve();
|
|
1647
|
+
}
|
|
1648
|
+
if (!force && this.conn !== null) {
|
|
1649
|
+
try {
|
|
1650
|
+
const existingSocket = await this.conn;
|
|
1651
|
+
if (existingSocket.readyState === WebSocket.OPEN) {
|
|
1652
|
+
this.debug(`${methodName}: connection is healthy, skipping`);
|
|
1653
|
+
return Promise.resolve();
|
|
1654
|
+
}
|
|
1655
|
+
} catch {
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
this.debug(
|
|
1659
|
+
`${methodName}: ${force ? "forcing immediate reconnection" : "reconnecting"}`
|
|
1660
|
+
);
|
|
1661
|
+
this.wsIntentionalDisconnect = false;
|
|
1662
|
+
if (this.wsReconnectTimer !== null) {
|
|
1663
|
+
this.debug(`${methodName}: cancelling pending reconnection timer`);
|
|
1664
|
+
clearTimeout(this.wsReconnectTimer);
|
|
1665
|
+
this.wsReconnectTimer = null;
|
|
1666
|
+
}
|
|
1667
|
+
this.wsReconnectAttempts = 0;
|
|
1668
|
+
if (this.conn !== null) {
|
|
1669
|
+
this.debug(`${methodName}: closing existing connection`);
|
|
1670
|
+
try {
|
|
1671
|
+
const existingSocket = await this.conn;
|
|
1672
|
+
if (this.currentWebSocket === existingSocket) {
|
|
1673
|
+
this.currentWebSocket = null;
|
|
1674
|
+
}
|
|
1675
|
+
if (existingSocket.readyState === WebSocket.OPEN || existingSocket.readyState === WebSocket.CONNECTING) {
|
|
1676
|
+
existingSocket.close();
|
|
1677
|
+
}
|
|
1678
|
+
} catch (error) {
|
|
1679
|
+
this.debug(`${methodName}: error closing existing connection:`, error);
|
|
1680
|
+
}
|
|
1681
|
+
this.conn = null;
|
|
1682
|
+
this.isConnecting = false;
|
|
1683
|
+
}
|
|
1684
|
+
if (this.context.company !== void 0 || this.context.user !== void 0) {
|
|
1685
|
+
this.debug(`${methodName}: reconnecting with existing context`);
|
|
1686
|
+
try {
|
|
1687
|
+
this.isConnecting = true;
|
|
1688
|
+
this.conn = this.wsConnect();
|
|
1689
|
+
const socket = await this.conn;
|
|
1690
|
+
this.isConnecting = false;
|
|
1691
|
+
await this.wsSendMessage(socket, this.context, true);
|
|
1692
|
+
this.debug(`${methodName}: reconnection successful`);
|
|
1693
|
+
} catch (error) {
|
|
1694
|
+
this.isConnecting = false;
|
|
1695
|
+
this.debug(`${methodName}: reconnection failed:`, error);
|
|
1696
|
+
}
|
|
1697
|
+
} else {
|
|
1698
|
+
this.debug(`${methodName}: no context set, skipping reconnection`);
|
|
1699
|
+
}
|
|
1700
|
+
return Promise.resolve();
|
|
1701
|
+
};
|
|
1288
1702
|
/**
|
|
1289
1703
|
* If using websocket mode, close the connection when done.
|
|
1290
1704
|
* In offline mode, this is a no-op.
|
|
@@ -1294,95 +1708,290 @@ var Schematic = class {
|
|
|
1294
1708
|
this.debug("cleanup: skipped (offline mode)");
|
|
1295
1709
|
return Promise.resolve();
|
|
1296
1710
|
}
|
|
1711
|
+
this.wsIntentionalDisconnect = true;
|
|
1712
|
+
if (this.wsReconnectTimer !== null) {
|
|
1713
|
+
clearTimeout(this.wsReconnectTimer);
|
|
1714
|
+
this.wsReconnectTimer = null;
|
|
1715
|
+
}
|
|
1716
|
+
this.stopRetryTimer();
|
|
1297
1717
|
if (this.conn) {
|
|
1298
1718
|
try {
|
|
1299
1719
|
const socket = await this.conn;
|
|
1720
|
+
if (this.currentWebSocket === socket) {
|
|
1721
|
+
this.debug(`Cleaning up current websocket tracking`);
|
|
1722
|
+
this.currentWebSocket = null;
|
|
1723
|
+
}
|
|
1300
1724
|
socket.close();
|
|
1301
1725
|
} catch (error) {
|
|
1302
|
-
console.
|
|
1726
|
+
console.warn("Error during cleanup:", error);
|
|
1303
1727
|
} finally {
|
|
1304
1728
|
this.conn = null;
|
|
1729
|
+
this.currentWebSocket = null;
|
|
1730
|
+
this.isConnecting = false;
|
|
1305
1731
|
}
|
|
1306
1732
|
}
|
|
1307
1733
|
};
|
|
1308
|
-
|
|
1309
|
-
|
|
1734
|
+
/**
|
|
1735
|
+
* Calculate the delay for the next reconnection attempt using exponential backoff with jitter.
|
|
1736
|
+
* This helps prevent dogpiling when the server recovers from an outage.
|
|
1737
|
+
*/
|
|
1738
|
+
calculateReconnectDelay = () => {
|
|
1739
|
+
const exponentialDelay = this.webSocketInitialRetryDelay * Math.pow(2, this.wsReconnectAttempts);
|
|
1740
|
+
const cappedDelay = Math.min(exponentialDelay, this.webSocketMaxRetryDelay);
|
|
1741
|
+
const jitter = Math.random() * cappedDelay * 0.5;
|
|
1742
|
+
const totalDelay = cappedDelay + jitter;
|
|
1743
|
+
this.debug(
|
|
1744
|
+
`Reconnect delay calculated: ${totalDelay.toFixed(0)}ms (attempt ${this.wsReconnectAttempts + 1}/${this.webSocketMaxReconnectAttempts})`
|
|
1745
|
+
);
|
|
1746
|
+
return totalDelay;
|
|
1747
|
+
};
|
|
1748
|
+
/**
|
|
1749
|
+
* Handle browser going offline
|
|
1750
|
+
*/
|
|
1751
|
+
handleNetworkOffline = async () => {
|
|
1752
|
+
if (this.conn !== null) {
|
|
1753
|
+
try {
|
|
1754
|
+
const socket = await this.conn;
|
|
1755
|
+
if (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING) {
|
|
1756
|
+
socket.close();
|
|
1757
|
+
}
|
|
1758
|
+
} catch (error) {
|
|
1759
|
+
this.debug("Error closing connection on offline:", error);
|
|
1760
|
+
}
|
|
1761
|
+
this.conn = null;
|
|
1762
|
+
}
|
|
1763
|
+
if (this.wsReconnectTimer !== null) {
|
|
1764
|
+
clearTimeout(this.wsReconnectTimer);
|
|
1765
|
+
this.wsReconnectTimer = null;
|
|
1766
|
+
}
|
|
1767
|
+
};
|
|
1768
|
+
/**
|
|
1769
|
+
* Handle browser coming back online
|
|
1770
|
+
*/
|
|
1771
|
+
handleNetworkOnline = () => {
|
|
1772
|
+
this.debug(
|
|
1773
|
+
"Network online, attempting reconnection and flushing queued events"
|
|
1774
|
+
);
|
|
1775
|
+
this.wsReconnectAttempts = 0;
|
|
1776
|
+
if (this.wsReconnectTimer !== null) {
|
|
1777
|
+
clearTimeout(this.wsReconnectTimer);
|
|
1778
|
+
this.wsReconnectTimer = null;
|
|
1779
|
+
}
|
|
1780
|
+
this.flushEventQueue().catch((error) => {
|
|
1781
|
+
this.debug("Error flushing event queue on network online:", error);
|
|
1782
|
+
});
|
|
1783
|
+
this.attemptReconnect();
|
|
1784
|
+
};
|
|
1785
|
+
/**
|
|
1786
|
+
* Attempt to reconnect the WebSocket connection with exponential backoff.
|
|
1787
|
+
* Called automatically when the connection closes unexpectedly.
|
|
1788
|
+
*/
|
|
1789
|
+
attemptReconnect = () => {
|
|
1790
|
+
if (this.wsReconnectAttempts >= this.webSocketMaxReconnectAttempts) {
|
|
1791
|
+
this.debug(
|
|
1792
|
+
`Maximum reconnection attempts (${this.webSocketMaxReconnectAttempts}) reached, giving up`
|
|
1793
|
+
);
|
|
1794
|
+
return;
|
|
1795
|
+
}
|
|
1796
|
+
if (this.wsReconnectTimer !== null) {
|
|
1797
|
+
this.debug(
|
|
1798
|
+
`Reconnection attempt already scheduled, ignoring duplicate request`
|
|
1799
|
+
);
|
|
1800
|
+
return;
|
|
1801
|
+
}
|
|
1802
|
+
const delay = this.calculateReconnectDelay();
|
|
1803
|
+
this.debug(
|
|
1804
|
+
`Scheduling reconnection attempt ${this.wsReconnectAttempts + 1}/${this.webSocketMaxReconnectAttempts} in ${delay.toFixed(0)}ms`
|
|
1805
|
+
);
|
|
1806
|
+
this.wsReconnectTimer = setTimeout(async () => {
|
|
1807
|
+
this.wsReconnectTimer = null;
|
|
1808
|
+
this.wsReconnectAttempts++;
|
|
1809
|
+
this.debug(
|
|
1810
|
+
`Attempting to reconnect (attempt ${this.wsReconnectAttempts}/${this.webSocketMaxReconnectAttempts})`
|
|
1811
|
+
);
|
|
1812
|
+
try {
|
|
1813
|
+
if (this.conn !== null) {
|
|
1814
|
+
this.debug(`Cleaning up existing connection before reconnection`);
|
|
1815
|
+
try {
|
|
1816
|
+
const existingSocket = await this.conn;
|
|
1817
|
+
if (this.currentWebSocket === existingSocket) {
|
|
1818
|
+
this.debug(`Existing websocket is current, will be replaced`);
|
|
1819
|
+
this.currentWebSocket = null;
|
|
1820
|
+
}
|
|
1821
|
+
if (existingSocket.readyState === WebSocket.OPEN || existingSocket.readyState === WebSocket.CONNECTING) {
|
|
1822
|
+
existingSocket.close();
|
|
1823
|
+
}
|
|
1824
|
+
} catch (error) {
|
|
1825
|
+
this.debug(`Error cleaning up existing connection:`, error);
|
|
1826
|
+
}
|
|
1827
|
+
this.conn = null;
|
|
1828
|
+
this.currentWebSocket = null;
|
|
1829
|
+
this.isConnecting = false;
|
|
1830
|
+
}
|
|
1831
|
+
this.isConnecting = true;
|
|
1832
|
+
try {
|
|
1833
|
+
this.conn = this.wsConnect();
|
|
1834
|
+
const socket = await this.conn;
|
|
1835
|
+
this.isConnecting = false;
|
|
1836
|
+
this.debug(`Reconnection context check:`, {
|
|
1837
|
+
hasCompany: this.context.company !== void 0,
|
|
1838
|
+
hasUser: this.context.user !== void 0,
|
|
1839
|
+
context: this.context
|
|
1840
|
+
});
|
|
1841
|
+
if (this.context.company !== void 0 || this.context.user !== void 0) {
|
|
1842
|
+
this.debug(`Reconnected, force re-sending context`);
|
|
1843
|
+
await this.wsSendMessage(socket, this.context, true);
|
|
1844
|
+
} else {
|
|
1845
|
+
this.debug(
|
|
1846
|
+
`No context to re-send after reconnection - websocket ready for new context`
|
|
1847
|
+
);
|
|
1848
|
+
this.debug(
|
|
1849
|
+
`Setting up tracking for reconnected websocket (no context to send)`
|
|
1850
|
+
);
|
|
1851
|
+
this.currentWebSocket = socket;
|
|
1852
|
+
}
|
|
1853
|
+
this.flushEventQueue().catch((error) => {
|
|
1854
|
+
this.debug(
|
|
1855
|
+
"Error flushing event queue after websocket reconnection:",
|
|
1856
|
+
error
|
|
1857
|
+
);
|
|
1858
|
+
});
|
|
1859
|
+
this.debug(`Reconnection successful`);
|
|
1860
|
+
} catch (error) {
|
|
1861
|
+
this.isConnecting = false;
|
|
1862
|
+
throw error;
|
|
1863
|
+
}
|
|
1864
|
+
} catch (error) {
|
|
1865
|
+
this.debug(`Reconnection attempt failed:`, error);
|
|
1866
|
+
}
|
|
1867
|
+
}, delay);
|
|
1868
|
+
};
|
|
1869
|
+
// Open a websocket connection with retry logic for timeouts
|
|
1870
|
+
wsConnect = async () => {
|
|
1310
1871
|
if (this.isOffline()) {
|
|
1311
1872
|
this.debug("wsConnect: skipped (offline mode)");
|
|
1312
|
-
|
|
1313
|
-
new Error("WebSocket connection skipped in offline mode")
|
|
1314
|
-
);
|
|
1873
|
+
throw new Error("WebSocket connection skipped in offline mode");
|
|
1315
1874
|
}
|
|
1875
|
+
let lastError = null;
|
|
1876
|
+
const maxAttempts = this.webSocketMaxConnectionAttempts;
|
|
1877
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
1878
|
+
try {
|
|
1879
|
+
const socket = await this.wsConnectOnce();
|
|
1880
|
+
this.wsReconnectAttempts = 0;
|
|
1881
|
+
return socket;
|
|
1882
|
+
} catch (error) {
|
|
1883
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
1884
|
+
const isTimeout = lastError.message === "WebSocket connection timeout";
|
|
1885
|
+
if (!isTimeout) {
|
|
1886
|
+
this.debug(
|
|
1887
|
+
`WebSocket connection failed with non-timeout error, not retrying:`,
|
|
1888
|
+
lastError.message
|
|
1889
|
+
);
|
|
1890
|
+
throw lastError;
|
|
1891
|
+
}
|
|
1892
|
+
if (attempt < maxAttempts - 1) {
|
|
1893
|
+
const baseDelay = this.webSocketInitialRetryDelay * Math.pow(2, attempt);
|
|
1894
|
+
const cappedDelay = Math.min(baseDelay, this.webSocketMaxRetryDelay);
|
|
1895
|
+
const jitter = cappedDelay * 0.2 * Math.random();
|
|
1896
|
+
const delay = cappedDelay + jitter;
|
|
1897
|
+
this.debug(
|
|
1898
|
+
`WebSocket connection timeout (attempt ${attempt + 1}/${maxAttempts}), retrying in ${delay.toFixed(0)}ms`
|
|
1899
|
+
);
|
|
1900
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1901
|
+
} else {
|
|
1902
|
+
this.debug(
|
|
1903
|
+
`WebSocket connection timeout (attempt ${attempt + 1}/${maxAttempts}), no more retries`
|
|
1904
|
+
);
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
throw lastError ?? new Error("WebSocket connection failed");
|
|
1909
|
+
};
|
|
1910
|
+
// Single attempt to open a websocket connection (no retry logic)
|
|
1911
|
+
wsConnectOnce = () => {
|
|
1316
1912
|
return new Promise((resolve, reject) => {
|
|
1317
1913
|
const wsUrl = `${this.webSocketUrl}/flags/bootstrap?apiKey=${this.apiKey}`;
|
|
1318
1914
|
this.debug(`connecting to WebSocket:`, wsUrl);
|
|
1319
1915
|
const webSocket = new WebSocket(wsUrl);
|
|
1916
|
+
const connectionId = Math.random().toString(36).substring(7);
|
|
1917
|
+
this.debug(`Creating WebSocket connection ${connectionId} to ${wsUrl}`);
|
|
1918
|
+
let timeoutId = null;
|
|
1919
|
+
let isResolved = false;
|
|
1920
|
+
timeoutId = setTimeout(() => {
|
|
1921
|
+
if (!isResolved) {
|
|
1922
|
+
isResolved = true;
|
|
1923
|
+
this.debug(
|
|
1924
|
+
`WebSocket connection timeout after ${this.webSocketConnectionTimeout}ms`
|
|
1925
|
+
);
|
|
1926
|
+
webSocket.close();
|
|
1927
|
+
reject(new Error("WebSocket connection timeout"));
|
|
1928
|
+
}
|
|
1929
|
+
}, this.webSocketConnectionTimeout);
|
|
1320
1930
|
webSocket.onopen = () => {
|
|
1321
|
-
|
|
1931
|
+
if (isResolved) return;
|
|
1932
|
+
isResolved = true;
|
|
1933
|
+
if (timeoutId !== null) {
|
|
1934
|
+
clearTimeout(timeoutId);
|
|
1935
|
+
}
|
|
1936
|
+
this.wsIntentionalDisconnect = false;
|
|
1937
|
+
this.debug(`WebSocket connection ${connectionId} opened successfully`);
|
|
1322
1938
|
resolve(webSocket);
|
|
1323
1939
|
};
|
|
1324
1940
|
webSocket.onerror = (error) => {
|
|
1325
|
-
|
|
1941
|
+
if (isResolved) return;
|
|
1942
|
+
isResolved = true;
|
|
1943
|
+
if (timeoutId !== null) {
|
|
1944
|
+
clearTimeout(timeoutId);
|
|
1945
|
+
}
|
|
1946
|
+
this.debug(`WebSocket connection ${connectionId} error:`, error);
|
|
1326
1947
|
reject(error);
|
|
1327
1948
|
};
|
|
1328
1949
|
webSocket.onclose = () => {
|
|
1329
|
-
|
|
1950
|
+
if (timeoutId !== null) {
|
|
1951
|
+
clearTimeout(timeoutId);
|
|
1952
|
+
}
|
|
1953
|
+
this.debug(`WebSocket connection ${connectionId} closed`);
|
|
1330
1954
|
this.conn = null;
|
|
1955
|
+
if (this.currentWebSocket === webSocket) {
|
|
1956
|
+
this.currentWebSocket = null;
|
|
1957
|
+
this.isConnecting = false;
|
|
1958
|
+
}
|
|
1959
|
+
if (!isResolved && !this.wsIntentionalDisconnect && this.webSocketReconnect) {
|
|
1960
|
+
this.attemptReconnect();
|
|
1961
|
+
}
|
|
1331
1962
|
};
|
|
1332
1963
|
});
|
|
1333
1964
|
};
|
|
1334
1965
|
// Send a message on the websocket indicating interest in a particular evaluation context
|
|
1335
1966
|
// and wait for the initial set of flag values to be returned
|
|
1336
|
-
wsSendMessage = (socket, context) => {
|
|
1967
|
+
wsSendMessage = (socket, context, forceContextSend = false) => {
|
|
1337
1968
|
if (this.isOffline()) {
|
|
1338
1969
|
this.debug("wsSendMessage: skipped (offline mode)");
|
|
1339
1970
|
this.setIsPending(false);
|
|
1340
1971
|
return Promise.resolve();
|
|
1341
1972
|
}
|
|
1342
1973
|
return new Promise((resolve, reject) => {
|
|
1343
|
-
if (contextString(context) == contextString(this.context)) {
|
|
1974
|
+
if (!forceContextSend && contextString(context) == contextString(this.context)) {
|
|
1344
1975
|
this.debug(`WebSocket context unchanged, skipping update`);
|
|
1345
1976
|
return resolve(this.setIsPending(false));
|
|
1346
1977
|
}
|
|
1347
|
-
this.debug(
|
|
1978
|
+
this.debug(
|
|
1979
|
+
forceContextSend ? `WebSocket force sending context (reconnection):` : `WebSocket context updated:`,
|
|
1980
|
+
context
|
|
1981
|
+
);
|
|
1348
1982
|
this.context = context;
|
|
1349
1983
|
const sendMessage = () => {
|
|
1350
1984
|
let resolved = false;
|
|
1985
|
+
const persistentMessageHandler = this.createPersistentMessageHandler(context);
|
|
1351
1986
|
const messageHandler = (event) => {
|
|
1352
|
-
|
|
1353
|
-
this.debug(`WebSocket message received:`, message);
|
|
1354
|
-
if (!(contextString(context) in this.checks)) {
|
|
1355
|
-
this.checks[contextString(context)] = {};
|
|
1356
|
-
}
|
|
1357
|
-
(message.flags ?? []).forEach((flag) => {
|
|
1358
|
-
const flagCheck = CheckFlagReturnFromJSON(flag);
|
|
1359
|
-
const contextStr = contextString(context);
|
|
1360
|
-
if (this.checks[contextStr] === void 0) {
|
|
1361
|
-
this.checks[contextStr] = {};
|
|
1362
|
-
}
|
|
1363
|
-
this.checks[contextStr][flagCheck.flag] = flagCheck;
|
|
1364
|
-
this.debug(`WebSocket flag update:`, {
|
|
1365
|
-
flag: flagCheck.flag,
|
|
1366
|
-
value: flagCheck.value,
|
|
1367
|
-
flagCheck
|
|
1368
|
-
});
|
|
1369
|
-
if (typeof flagCheck.featureUsageEvent === "string") {
|
|
1370
|
-
this.updateFeatureUsageEventMap(flagCheck);
|
|
1371
|
-
}
|
|
1372
|
-
if (this.flagCheckListeners[flag.flag]?.size > 0 || this.flagValueListeners[flag.flag]?.size > 0) {
|
|
1373
|
-
this.submitFlagCheckEvent(flagCheck.flag, flagCheck, context);
|
|
1374
|
-
}
|
|
1375
|
-
this.notifyFlagCheckListeners(flag.flag, flagCheck);
|
|
1376
|
-
this.notifyFlagValueListeners(flag.flag, flagCheck.value);
|
|
1377
|
-
});
|
|
1378
|
-
this.flushContextDependentEventQueue();
|
|
1379
|
-
this.setIsPending(false);
|
|
1987
|
+
persistentMessageHandler(event);
|
|
1380
1988
|
if (!resolved) {
|
|
1381
1989
|
resolved = true;
|
|
1382
1990
|
resolve();
|
|
1383
1991
|
}
|
|
1384
1992
|
};
|
|
1385
1993
|
socket.addEventListener("message", messageHandler);
|
|
1994
|
+
this.currentWebSocket = socket;
|
|
1386
1995
|
const clientVersion = this.additionalHeaders["X-Schematic-Client-Version"] ?? `schematic-js@${version}`;
|
|
1387
1996
|
const messagePayload = {
|
|
1388
1997
|
apiKey: this.apiKey,
|
|
@@ -1427,12 +2036,31 @@ var Schematic = class {
|
|
|
1427
2036
|
getFlagCheck = (flagKey) => {
|
|
1428
2037
|
const contextStr = contextString(this.context);
|
|
1429
2038
|
const checks = this.checks[contextStr] ?? {};
|
|
1430
|
-
|
|
2039
|
+
const check = checks[flagKey];
|
|
2040
|
+
if (check !== void 0) {
|
|
2041
|
+
return check;
|
|
2042
|
+
}
|
|
2043
|
+
if (flagKey in this.flagCheckDefaults || flagKey in this.flagValueDefaults) {
|
|
2044
|
+
return this.resolveFallbackCheckFlagReturn(
|
|
2045
|
+
flagKey,
|
|
2046
|
+
void 0,
|
|
2047
|
+
"Default value used"
|
|
2048
|
+
);
|
|
2049
|
+
}
|
|
2050
|
+
return void 0;
|
|
1431
2051
|
};
|
|
1432
2052
|
// flagValues state
|
|
1433
2053
|
getFlagValue = (flagKey) => {
|
|
1434
|
-
const
|
|
1435
|
-
|
|
2054
|
+
const contextStr = contextString(this.context);
|
|
2055
|
+
const checks = this.checks[contextStr] ?? {};
|
|
2056
|
+
const check = checks[flagKey];
|
|
2057
|
+
if (check?.value !== void 0) {
|
|
2058
|
+
return check.value;
|
|
2059
|
+
}
|
|
2060
|
+
if (flagKey in this.flagCheckDefaults || flagKey in this.flagValueDefaults) {
|
|
2061
|
+
return this.resolveFallbackValue(flagKey);
|
|
2062
|
+
}
|
|
2063
|
+
return void 0;
|
|
1436
2064
|
};
|
|
1437
2065
|
/** Register an event listener that will be notified with the boolean value for a given flag when this value changes */
|
|
1438
2066
|
addFlagValueListener = (flagKey, listener) => {
|
|
@@ -1490,7 +2118,17 @@ var Schematic = class {
|
|
|
1490
2118
|
{ value }
|
|
1491
2119
|
);
|
|
1492
2120
|
}
|
|
1493
|
-
listeners.forEach((listener) =>
|
|
2121
|
+
listeners.forEach((listener, index) => {
|
|
2122
|
+
this.debug(`Calling listener ${index} for flag ${flagKey}`, {
|
|
2123
|
+
flagKey,
|
|
2124
|
+
value
|
|
2125
|
+
});
|
|
2126
|
+
notifyFlagValueListener(listener, value);
|
|
2127
|
+
this.debug(`Listener ${index} for flag ${flagKey} completed`, {
|
|
2128
|
+
flagKey,
|
|
2129
|
+
value
|
|
2130
|
+
});
|
|
2131
|
+
});
|
|
1494
2132
|
};
|
|
1495
2133
|
};
|
|
1496
2134
|
var notifyPendingListener = (listener, value) => {
|
|
@@ -1519,7 +2157,7 @@ var notifyFlagValueListener = (listener, value) => {
|
|
|
1519
2157
|
import { inject, onScopeDispose } from "vue";
|
|
1520
2158
|
|
|
1521
2159
|
// src/version.ts
|
|
1522
|
-
var version2 = "
|
|
2160
|
+
var version2 = "1.2.16";
|
|
1523
2161
|
|
|
1524
2162
|
// src/context/schematic.ts
|
|
1525
2163
|
var SchematicSymbol = Symbol("schematic");
|