@schematichq/schematic-vue 0.1.0 → 1.4.0
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/LICENSE +1 -2
- package/README.md +98 -0
- package/dist/schematic-vue.cjs.js +925 -166
- package/dist/schematic-vue.d.ts +26 -0
- package/dist/schematic-vue.esm.js +925 -166
- package/package.json +6 -5
|
@@ -30,7 +30,8 @@ __export(index_exports, {
|
|
|
30
30
|
useSchematicEntitlement: () => useSchematicEntitlement,
|
|
31
31
|
useSchematicEvents: () => useSchematicEvents,
|
|
32
32
|
useSchematicFlag: () => useSchematicFlag,
|
|
33
|
-
useSchematicIsPending: () => useSchematicIsPending
|
|
33
|
+
useSchematicIsPending: () => useSchematicIsPending,
|
|
34
|
+
useSchematicPlan: () => useSchematicPlan
|
|
34
35
|
});
|
|
35
36
|
module.exports = __toCommonJS(index_exports);
|
|
36
37
|
|
|
@@ -642,6 +643,36 @@ function v4(options, buf, offset) {
|
|
|
642
643
|
}
|
|
643
644
|
var v4_default = v4;
|
|
644
645
|
var import_polyfill = __toESM(require_browser_polyfill());
|
|
646
|
+
function EntitlementValueTypeFromJSON(json) {
|
|
647
|
+
return EntitlementValueTypeFromJSONTyped(json, false);
|
|
648
|
+
}
|
|
649
|
+
function EntitlementValueTypeFromJSONTyped(json, ignoreDiscriminator) {
|
|
650
|
+
return json;
|
|
651
|
+
}
|
|
652
|
+
function FeatureEntitlementFromJSON(json) {
|
|
653
|
+
return FeatureEntitlementFromJSONTyped(json, false);
|
|
654
|
+
}
|
|
655
|
+
function FeatureEntitlementFromJSONTyped(json, ignoreDiscriminator) {
|
|
656
|
+
if (json == null) {
|
|
657
|
+
return json;
|
|
658
|
+
}
|
|
659
|
+
return {
|
|
660
|
+
allocation: json["allocation"] == null ? void 0 : json["allocation"],
|
|
661
|
+
creditId: json["credit_id"] == null ? void 0 : json["credit_id"],
|
|
662
|
+
creditRemaining: json["credit_remaining"] == null ? void 0 : json["credit_remaining"],
|
|
663
|
+
creditTotal: json["credit_total"] == null ? void 0 : json["credit_total"],
|
|
664
|
+
creditUsed: json["credit_used"] == null ? void 0 : json["credit_used"],
|
|
665
|
+
eventName: json["event_name"] == null ? void 0 : json["event_name"],
|
|
666
|
+
featureId: json["feature_id"],
|
|
667
|
+
featureKey: json["feature_key"],
|
|
668
|
+
metricPeriod: json["metric_period"] == null ? void 0 : json["metric_period"],
|
|
669
|
+
metricResetAt: json["metric_reset_at"] == null ? void 0 : new Date(json["metric_reset_at"]),
|
|
670
|
+
monthReset: json["month_reset"] == null ? void 0 : json["month_reset"],
|
|
671
|
+
softLimit: json["soft_limit"] == null ? void 0 : json["soft_limit"],
|
|
672
|
+
usage: json["usage"] == null ? void 0 : json["usage"],
|
|
673
|
+
valueType: EntitlementValueTypeFromJSON(json["value_type"])
|
|
674
|
+
};
|
|
675
|
+
}
|
|
645
676
|
function CheckFlagResponseDataFromJSON(json) {
|
|
646
677
|
return CheckFlagResponseDataFromJSONTyped(json, false);
|
|
647
678
|
}
|
|
@@ -651,6 +682,7 @@ function CheckFlagResponseDataFromJSONTyped(json, ignoreDiscriminator) {
|
|
|
651
682
|
}
|
|
652
683
|
return {
|
|
653
684
|
companyId: json["company_id"] == null ? void 0 : json["company_id"],
|
|
685
|
+
entitlement: json["entitlement"] == null ? void 0 : FeatureEntitlementFromJSON(json["entitlement"]),
|
|
654
686
|
error: json["error"] == null ? void 0 : json["error"],
|
|
655
687
|
featureAllocation: json["feature_allocation"] == null ? void 0 : json["feature_allocation"],
|
|
656
688
|
featureUsage: json["feature_usage"] == null ? void 0 : json["feature_usage"],
|
|
@@ -666,26 +698,6 @@ function CheckFlagResponseDataFromJSONTyped(json, ignoreDiscriminator) {
|
|
|
666
698
|
value: json["value"]
|
|
667
699
|
};
|
|
668
700
|
}
|
|
669
|
-
function EventBodyFlagCheckToJSON(json) {
|
|
670
|
-
return EventBodyFlagCheckToJSONTyped(json, false);
|
|
671
|
-
}
|
|
672
|
-
function EventBodyFlagCheckToJSONTyped(value, ignoreDiscriminator = false) {
|
|
673
|
-
if (value == null) {
|
|
674
|
-
return value;
|
|
675
|
-
}
|
|
676
|
-
return {
|
|
677
|
-
company_id: value["companyId"],
|
|
678
|
-
error: value["error"],
|
|
679
|
-
flag_id: value["flagId"],
|
|
680
|
-
flag_key: value["flagKey"],
|
|
681
|
-
reason: value["reason"],
|
|
682
|
-
req_company: value["reqCompany"],
|
|
683
|
-
req_user: value["reqUser"],
|
|
684
|
-
rule_id: value["ruleId"],
|
|
685
|
-
user_id: value["userId"],
|
|
686
|
-
value: value["value"]
|
|
687
|
-
};
|
|
688
|
-
}
|
|
689
701
|
function CheckFlagResponseFromJSON(json) {
|
|
690
702
|
return CheckFlagResponseFromJSONTyped(json, false);
|
|
691
703
|
}
|
|
@@ -698,6 +710,26 @@ function CheckFlagResponseFromJSONTyped(json, ignoreDiscriminator) {
|
|
|
698
710
|
params: json["params"]
|
|
699
711
|
};
|
|
700
712
|
}
|
|
713
|
+
function TrialStatusFromJSON(json) {
|
|
714
|
+
return TrialStatusFromJSONTyped(json, false);
|
|
715
|
+
}
|
|
716
|
+
function TrialStatusFromJSONTyped(json, ignoreDiscriminator) {
|
|
717
|
+
return json;
|
|
718
|
+
}
|
|
719
|
+
function DatastreamCompanyPlanFromJSON(json) {
|
|
720
|
+
return DatastreamCompanyPlanFromJSONTyped(json, false);
|
|
721
|
+
}
|
|
722
|
+
function DatastreamCompanyPlanFromJSONTyped(json, ignoreDiscriminator) {
|
|
723
|
+
if (json == null) {
|
|
724
|
+
return json;
|
|
725
|
+
}
|
|
726
|
+
return {
|
|
727
|
+
id: json["id"],
|
|
728
|
+
name: json["name"],
|
|
729
|
+
trialEndDate: json["trial_end_date"] == null ? void 0 : new Date(json["trial_end_date"]),
|
|
730
|
+
trialStatus: json["trial_status"] == null ? void 0 : TrialStatusFromJSON(json["trial_status"])
|
|
731
|
+
};
|
|
732
|
+
}
|
|
701
733
|
function CheckFlagsResponseDataFromJSON(json) {
|
|
702
734
|
return CheckFlagsResponseDataFromJSONTyped(json, false);
|
|
703
735
|
}
|
|
@@ -706,7 +738,8 @@ function CheckFlagsResponseDataFromJSONTyped(json, ignoreDiscriminator) {
|
|
|
706
738
|
return json;
|
|
707
739
|
}
|
|
708
740
|
return {
|
|
709
|
-
flags: json["flags"].map(CheckFlagResponseDataFromJSON)
|
|
741
|
+
flags: json["flags"].map(CheckFlagResponseDataFromJSON),
|
|
742
|
+
plan: json["plan"] == null ? void 0 : DatastreamCompanyPlanFromJSON(json["plan"])
|
|
710
743
|
};
|
|
711
744
|
}
|
|
712
745
|
function CheckFlagsResponseFromJSON(json) {
|
|
@@ -721,6 +754,26 @@ function CheckFlagsResponseFromJSONTyped(json, ignoreDiscriminator) {
|
|
|
721
754
|
params: json["params"]
|
|
722
755
|
};
|
|
723
756
|
}
|
|
757
|
+
function EventBodyFlagCheckToJSON(json) {
|
|
758
|
+
return EventBodyFlagCheckToJSONTyped(json, false);
|
|
759
|
+
}
|
|
760
|
+
function EventBodyFlagCheckToJSONTyped(value, ignoreDiscriminator = false) {
|
|
761
|
+
if (value == null) {
|
|
762
|
+
return value;
|
|
763
|
+
}
|
|
764
|
+
return {
|
|
765
|
+
company_id: value["companyId"],
|
|
766
|
+
error: value["error"],
|
|
767
|
+
flag_id: value["flagId"],
|
|
768
|
+
flag_key: value["flagKey"],
|
|
769
|
+
reason: value["reason"],
|
|
770
|
+
req_company: value["reqCompany"],
|
|
771
|
+
req_user: value["reqUser"],
|
|
772
|
+
rule_id: value["ruleId"],
|
|
773
|
+
user_id: value["userId"],
|
|
774
|
+
value: value["value"]
|
|
775
|
+
};
|
|
776
|
+
}
|
|
724
777
|
var RuleType = /* @__PURE__ */ ((RuleType2) => {
|
|
725
778
|
RuleType2["GLOBAL_OVERRIDE"] = "global_override";
|
|
726
779
|
RuleType2["COMPANY_OVERRIDE"] = "company_override";
|
|
@@ -741,6 +794,7 @@ var UsagePeriod = /* @__PURE__ */ ((UsagePeriod2) => {
|
|
|
741
794
|
var CheckFlagReturnFromJSON = (json) => {
|
|
742
795
|
const {
|
|
743
796
|
companyId,
|
|
797
|
+
entitlement,
|
|
744
798
|
error,
|
|
745
799
|
featureAllocation,
|
|
746
800
|
featureUsage,
|
|
@@ -758,24 +812,40 @@ var CheckFlagReturnFromJSON = (json) => {
|
|
|
758
812
|
const featureUsageExceeded = !value && // if flag is not false, then we haven't exceeded usage
|
|
759
813
|
(ruleType == "company_override_usage_exceeded" || // if the rule type is one of these, then we have exceeded usage
|
|
760
814
|
ruleType == "plan_entitlement_usage_exceeded");
|
|
815
|
+
const resolvedAllocation = entitlement?.allocation ?? featureAllocation;
|
|
816
|
+
const resolvedUsage = entitlement?.usage ?? featureUsage;
|
|
817
|
+
const resolvedEvent = entitlement?.eventName ?? featureUsageEvent;
|
|
818
|
+
const resolvedPeriod = entitlement?.metricPeriod ?? featureUsagePeriod;
|
|
819
|
+
const resolvedResetAt = entitlement?.metricResetAt ?? featureUsageResetAt;
|
|
761
820
|
return {
|
|
762
821
|
featureUsageExceeded,
|
|
763
822
|
companyId: companyId == null ? void 0 : companyId,
|
|
823
|
+
creditRemaining: entitlement?.creditRemaining == null ? void 0 : entitlement.creditRemaining,
|
|
764
824
|
error: error == null ? void 0 : error,
|
|
765
|
-
featureAllocation:
|
|
766
|
-
featureUsage:
|
|
767
|
-
featureUsageEvent:
|
|
768
|
-
featureUsagePeriod:
|
|
769
|
-
featureUsageResetAt:
|
|
825
|
+
featureAllocation: resolvedAllocation == null ? void 0 : resolvedAllocation,
|
|
826
|
+
featureUsage: resolvedUsage == null ? void 0 : resolvedUsage,
|
|
827
|
+
featureUsageEvent: resolvedEvent == null ? void 0 : resolvedEvent,
|
|
828
|
+
featureUsagePeriod: resolvedPeriod == null ? void 0 : resolvedPeriod,
|
|
829
|
+
featureUsageResetAt: resolvedResetAt == null ? void 0 : resolvedResetAt,
|
|
770
830
|
flag,
|
|
771
831
|
flagId: flagId == null ? void 0 : flagId,
|
|
772
832
|
reason,
|
|
773
833
|
ruleId: ruleId == null ? void 0 : ruleId,
|
|
774
834
|
ruleType: ruleType == null ? void 0 : ruleType,
|
|
835
|
+
softLimit: entitlement?.softLimit == null ? void 0 : entitlement.softLimit,
|
|
775
836
|
userId: userId == null ? void 0 : userId,
|
|
776
837
|
value
|
|
777
838
|
};
|
|
778
839
|
};
|
|
840
|
+
var CheckPlanReturnFromJSON = (json) => {
|
|
841
|
+
const { id, name, trialEndDate, trialStatus } = DatastreamCompanyPlanFromJSON(json);
|
|
842
|
+
return {
|
|
843
|
+
id,
|
|
844
|
+
name,
|
|
845
|
+
trialEndDate: trialEndDate == null ? void 0 : trialEndDate,
|
|
846
|
+
trialStatus: trialStatus == null ? void 0 : trialStatus
|
|
847
|
+
};
|
|
848
|
+
};
|
|
779
849
|
function contextString(context) {
|
|
780
850
|
const sortedContext = Object.keys(context).reduce((acc, key) => {
|
|
781
851
|
const sortedKeys = Object.keys(
|
|
@@ -790,7 +860,7 @@ function contextString(context) {
|
|
|
790
860
|
}, {});
|
|
791
861
|
return JSON.stringify(sortedContext);
|
|
792
862
|
}
|
|
793
|
-
var version = "1.
|
|
863
|
+
var version = "1.4.0";
|
|
794
864
|
var anonymousIdKey = "schematicId";
|
|
795
865
|
var Schematic = class {
|
|
796
866
|
additionalHeaders = {};
|
|
@@ -807,11 +877,38 @@ var Schematic = class {
|
|
|
807
877
|
flagValueListeners = {};
|
|
808
878
|
isPending = true;
|
|
809
879
|
isPendingListeners = /* @__PURE__ */ new Set();
|
|
880
|
+
planListeners = /* @__PURE__ */ new Set();
|
|
810
881
|
storage;
|
|
811
882
|
useWebSocket = false;
|
|
812
883
|
checks = {};
|
|
813
884
|
featureUsageEventMap = {};
|
|
885
|
+
planChecks = {};
|
|
814
886
|
webSocketUrl = "wss://api.schematichq.com";
|
|
887
|
+
webSocketConnectionTimeout = 1e4;
|
|
888
|
+
webSocketReconnect = true;
|
|
889
|
+
webSocketMaxReconnectAttempts = 7;
|
|
890
|
+
// Max attempts after connection disrupted
|
|
891
|
+
webSocketMaxConnectionAttempts = 3;
|
|
892
|
+
// Max attempts for initial connection
|
|
893
|
+
webSocketInitialRetryDelay = 1e3;
|
|
894
|
+
webSocketMaxRetryDelay = 3e4;
|
|
895
|
+
wsReconnectAttempts = 0;
|
|
896
|
+
wsReconnectTimer = null;
|
|
897
|
+
wsIntentionalDisconnect = false;
|
|
898
|
+
currentWebSocket = null;
|
|
899
|
+
isConnecting = false;
|
|
900
|
+
maxEventQueueSize = 100;
|
|
901
|
+
// Prevent memory issues with very long network outages
|
|
902
|
+
maxEventRetries = 5;
|
|
903
|
+
// Maximum retry attempts for failed events
|
|
904
|
+
eventRetryInitialDelay = 1e3;
|
|
905
|
+
// Initial retry delay in ms
|
|
906
|
+
eventRetryMaxDelay = 3e4;
|
|
907
|
+
// Maximum retry delay in ms
|
|
908
|
+
retryTimer = null;
|
|
909
|
+
flagValueDefaults = {};
|
|
910
|
+
flagCheckDefaults = {};
|
|
911
|
+
fallbackCheckCache = {};
|
|
815
912
|
constructor(apiKey, options) {
|
|
816
913
|
this.apiKey = apiKey;
|
|
817
914
|
this.eventQueue = [];
|
|
@@ -843,8 +940,13 @@ var Schematic = class {
|
|
|
843
940
|
};
|
|
844
941
|
if (options?.storage) {
|
|
845
942
|
this.storage = options.storage;
|
|
846
|
-
} else
|
|
847
|
-
|
|
943
|
+
} else {
|
|
944
|
+
try {
|
|
945
|
+
if (typeof localStorage !== "undefined") {
|
|
946
|
+
this.storage = localStorage;
|
|
947
|
+
}
|
|
948
|
+
} catch {
|
|
949
|
+
}
|
|
848
950
|
}
|
|
849
951
|
if (options?.apiUrl !== void 0) {
|
|
850
952
|
this.apiUrl = options.apiUrl;
|
|
@@ -855,11 +957,54 @@ var Schematic = class {
|
|
|
855
957
|
if (options?.webSocketUrl !== void 0) {
|
|
856
958
|
this.webSocketUrl = options.webSocketUrl;
|
|
857
959
|
}
|
|
960
|
+
if (options?.webSocketConnectionTimeout !== void 0) {
|
|
961
|
+
this.webSocketConnectionTimeout = options.webSocketConnectionTimeout;
|
|
962
|
+
}
|
|
963
|
+
if (options?.webSocketReconnect !== void 0) {
|
|
964
|
+
this.webSocketReconnect = options.webSocketReconnect;
|
|
965
|
+
}
|
|
966
|
+
if (options?.webSocketMaxReconnectAttempts !== void 0) {
|
|
967
|
+
this.webSocketMaxReconnectAttempts = options.webSocketMaxReconnectAttempts;
|
|
968
|
+
}
|
|
969
|
+
if (options?.webSocketInitialRetryDelay !== void 0) {
|
|
970
|
+
this.webSocketInitialRetryDelay = options.webSocketInitialRetryDelay;
|
|
971
|
+
}
|
|
972
|
+
if (options?.webSocketMaxRetryDelay !== void 0) {
|
|
973
|
+
this.webSocketMaxRetryDelay = options.webSocketMaxRetryDelay;
|
|
974
|
+
}
|
|
975
|
+
if (options?.maxEventQueueSize !== void 0) {
|
|
976
|
+
this.maxEventQueueSize = options.maxEventQueueSize;
|
|
977
|
+
}
|
|
978
|
+
if (options?.maxEventRetries !== void 0) {
|
|
979
|
+
this.maxEventRetries = options.maxEventRetries;
|
|
980
|
+
}
|
|
981
|
+
if (options?.eventRetryInitialDelay !== void 0) {
|
|
982
|
+
this.eventRetryInitialDelay = options.eventRetryInitialDelay;
|
|
983
|
+
}
|
|
984
|
+
if (options?.eventRetryMaxDelay !== void 0) {
|
|
985
|
+
this.eventRetryMaxDelay = options.eventRetryMaxDelay;
|
|
986
|
+
}
|
|
987
|
+
if (options?.flagValueDefaults !== void 0) {
|
|
988
|
+
this.flagValueDefaults = options.flagValueDefaults;
|
|
989
|
+
}
|
|
990
|
+
if (options?.flagCheckDefaults !== void 0) {
|
|
991
|
+
this.flagCheckDefaults = options.flagCheckDefaults;
|
|
992
|
+
}
|
|
858
993
|
if (typeof window !== "undefined" && window?.addEventListener) {
|
|
859
994
|
window.addEventListener("beforeunload", () => {
|
|
860
995
|
this.flushEventQueue();
|
|
861
996
|
this.flushContextDependentEventQueue();
|
|
862
997
|
});
|
|
998
|
+
if (this.useWebSocket) {
|
|
999
|
+
window.addEventListener("offline", () => {
|
|
1000
|
+
this.debug("Browser went offline, closing WebSocket connection");
|
|
1001
|
+
this.handleNetworkOffline();
|
|
1002
|
+
});
|
|
1003
|
+
window.addEventListener("online", () => {
|
|
1004
|
+
this.debug("Browser came online, attempting to reconnect WebSocket");
|
|
1005
|
+
this.handleNetworkOnline();
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
863
1008
|
}
|
|
864
1009
|
if (this.offlineEnabled) {
|
|
865
1010
|
this.debug(
|
|
@@ -869,24 +1014,89 @@ var Schematic = class {
|
|
|
869
1014
|
this.debug("Initialized with debug mode enabled");
|
|
870
1015
|
}
|
|
871
1016
|
}
|
|
1017
|
+
/**
|
|
1018
|
+
* Resolve fallback value according to priority order:
|
|
1019
|
+
* 1. Callsite fallback value (if provided)
|
|
1020
|
+
* 2. Boolean value from flagCheckDefaults initialization option
|
|
1021
|
+
* 3. Boolean value from flagValueDefaults initialization option
|
|
1022
|
+
* 4. Default to false
|
|
1023
|
+
*/
|
|
1024
|
+
resolveFallbackValue(key, callsiteFallback) {
|
|
1025
|
+
if (callsiteFallback !== void 0) {
|
|
1026
|
+
return callsiteFallback;
|
|
1027
|
+
}
|
|
1028
|
+
if (key in this.flagCheckDefaults) {
|
|
1029
|
+
return this.flagCheckDefaults[key].value;
|
|
1030
|
+
}
|
|
1031
|
+
if (key in this.flagValueDefaults) {
|
|
1032
|
+
return this.flagValueDefaults[key];
|
|
1033
|
+
}
|
|
1034
|
+
return false;
|
|
1035
|
+
}
|
|
1036
|
+
/**
|
|
1037
|
+
* Resolve complete CheckFlagReturn object according to priority order:
|
|
1038
|
+
* 1. Use callsite fallback for boolean value, construct CheckFlagReturn
|
|
1039
|
+
* 2. Use flagCheckDefaults if available for this flag
|
|
1040
|
+
* 3. Use flagValueDefaults if available for this flag, construct CheckFlagReturn
|
|
1041
|
+
* 4. Default CheckFlagReturn with value: false
|
|
1042
|
+
*/
|
|
1043
|
+
resolveFallbackCheckFlagReturn(key, callsiteFallback, reason = "Fallback value used", error) {
|
|
1044
|
+
if (callsiteFallback !== void 0) {
|
|
1045
|
+
return {
|
|
1046
|
+
flag: key,
|
|
1047
|
+
value: callsiteFallback,
|
|
1048
|
+
reason,
|
|
1049
|
+
error
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
if (key in this.flagCheckDefaults) {
|
|
1053
|
+
const defaultReturn = this.flagCheckDefaults[key];
|
|
1054
|
+
return {
|
|
1055
|
+
...defaultReturn,
|
|
1056
|
+
flag: key,
|
|
1057
|
+
// Ensure flag matches the requested key
|
|
1058
|
+
reason: error !== void 0 ? reason : defaultReturn.reason,
|
|
1059
|
+
error
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
if (key in this.flagValueDefaults) {
|
|
1063
|
+
return {
|
|
1064
|
+
flag: key,
|
|
1065
|
+
value: this.flagValueDefaults[key],
|
|
1066
|
+
reason,
|
|
1067
|
+
error
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
return {
|
|
1071
|
+
flag: key,
|
|
1072
|
+
value: false,
|
|
1073
|
+
reason,
|
|
1074
|
+
error
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
872
1077
|
/**
|
|
873
1078
|
* Get value for a single flag.
|
|
874
1079
|
* In WebSocket mode, returns cached values if connection is active, otherwise establishes
|
|
875
|
-
* new connection and then returns the
|
|
876
|
-
* connection fails.
|
|
1080
|
+
* new connection and then returns the requested value. Falls back to preconfigured fallback
|
|
1081
|
+
* values if WebSocket connection fails.
|
|
877
1082
|
* In REST mode, makes an API call for each check.
|
|
878
1083
|
*/
|
|
879
1084
|
async checkFlag(options) {
|
|
880
|
-
const { fallback
|
|
1085
|
+
const { fallback, key } = options;
|
|
881
1086
|
const context = options.context || this.context;
|
|
882
1087
|
const contextStr = contextString(context);
|
|
883
1088
|
this.debug(`checkFlag: ${key}`, { context, fallback });
|
|
884
1089
|
if (this.isOffline()) {
|
|
1090
|
+
const resolvedFallbackResult = this.resolveFallbackCheckFlagReturn(
|
|
1091
|
+
key,
|
|
1092
|
+
fallback,
|
|
1093
|
+
"Offline mode - using initialization defaults"
|
|
1094
|
+
);
|
|
885
1095
|
this.debug(`checkFlag offline result: ${key}`, {
|
|
886
|
-
value:
|
|
1096
|
+
value: resolvedFallbackResult.value,
|
|
887
1097
|
offlineMode: true
|
|
888
1098
|
});
|
|
889
|
-
return
|
|
1099
|
+
return resolvedFallbackResult.value;
|
|
890
1100
|
}
|
|
891
1101
|
if (!this.useWebSocket) {
|
|
892
1102
|
const requestUrl = `${this.apiUrl}/flags/${key}/check`;
|
|
@@ -913,15 +1123,15 @@ var Schematic = class {
|
|
|
913
1123
|
this.submitFlagCheckEvent(key, result, context);
|
|
914
1124
|
return result.value;
|
|
915
1125
|
}).catch((error) => {
|
|
916
|
-
console.
|
|
917
|
-
const errorResult =
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
error
|
|
922
|
-
|
|
1126
|
+
console.warn("There was a problem with the fetch operation:", error);
|
|
1127
|
+
const errorResult = this.resolveFallbackCheckFlagReturn(
|
|
1128
|
+
key,
|
|
1129
|
+
fallback,
|
|
1130
|
+
"API request failed",
|
|
1131
|
+
error instanceof Error ? error.message : String(error)
|
|
1132
|
+
);
|
|
923
1133
|
this.submitFlagCheckEvent(key, errorResult, context);
|
|
924
|
-
return
|
|
1134
|
+
return errorResult.value;
|
|
925
1135
|
});
|
|
926
1136
|
}
|
|
927
1137
|
try {
|
|
@@ -931,38 +1141,52 @@ var Schematic = class {
|
|
|
931
1141
|
return existingVals[key].value;
|
|
932
1142
|
}
|
|
933
1143
|
if (this.isOffline()) {
|
|
934
|
-
return fallback;
|
|
1144
|
+
return this.resolveFallbackValue(key, fallback);
|
|
935
1145
|
}
|
|
936
1146
|
try {
|
|
937
1147
|
await this.setContext(context);
|
|
938
1148
|
} catch (error) {
|
|
939
|
-
console.
|
|
940
|
-
"WebSocket connection failed,
|
|
1149
|
+
console.warn(
|
|
1150
|
+
"WebSocket connection failed, using fallback value:",
|
|
941
1151
|
error
|
|
942
1152
|
);
|
|
943
|
-
|
|
1153
|
+
const errorResult = this.resolveFallbackCheckFlagReturn(
|
|
1154
|
+
key,
|
|
1155
|
+
fallback,
|
|
1156
|
+
"WebSocket connection failed",
|
|
1157
|
+
error instanceof Error ? error.message : String(error)
|
|
1158
|
+
);
|
|
1159
|
+
this.submitFlagCheckEvent(key, errorResult, context);
|
|
1160
|
+
return errorResult.value;
|
|
944
1161
|
}
|
|
945
1162
|
const contextVals = this.checks[contextStr] ?? {};
|
|
946
1163
|
const flagCheck = contextVals[key];
|
|
947
|
-
const result = flagCheck?.value ?? fallback;
|
|
1164
|
+
const result = flagCheck?.value ?? this.resolveFallbackValue(key, fallback);
|
|
948
1165
|
this.debug(
|
|
949
1166
|
`checkFlag WebSocket result: ${key}`,
|
|
950
|
-
typeof flagCheck !== "undefined" ? flagCheck : { value:
|
|
1167
|
+
typeof flagCheck !== "undefined" ? flagCheck : { value: result, fallbackUsed: true }
|
|
951
1168
|
);
|
|
952
1169
|
if (typeof flagCheck !== "undefined") {
|
|
953
1170
|
this.submitFlagCheckEvent(key, flagCheck, context);
|
|
1171
|
+
} else {
|
|
1172
|
+
const fallbackResult = this.resolveFallbackCheckFlagReturn(
|
|
1173
|
+
key,
|
|
1174
|
+
fallback,
|
|
1175
|
+
"No flag values available"
|
|
1176
|
+
);
|
|
1177
|
+
this.submitFlagCheckEvent(key, fallbackResult, context);
|
|
954
1178
|
}
|
|
955
1179
|
return result;
|
|
956
1180
|
} catch (error) {
|
|
957
1181
|
console.error("Unexpected error in checkFlag:", error);
|
|
958
|
-
const errorResult =
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
error
|
|
963
|
-
|
|
1182
|
+
const errorResult = this.resolveFallbackCheckFlagReturn(
|
|
1183
|
+
key,
|
|
1184
|
+
fallback,
|
|
1185
|
+
"Unexpected error in flag check",
|
|
1186
|
+
error instanceof Error ? error.message : String(error)
|
|
1187
|
+
);
|
|
964
1188
|
this.submitFlagCheckEvent(key, errorResult, context);
|
|
965
|
-
return
|
|
1189
|
+
return errorResult.value;
|
|
966
1190
|
}
|
|
967
1191
|
}
|
|
968
1192
|
/**
|
|
@@ -974,6 +1198,53 @@ var Schematic = class {
|
|
|
974
1198
|
console.log(`[Schematic] ${message}`, ...args);
|
|
975
1199
|
}
|
|
976
1200
|
}
|
|
1201
|
+
/**
|
|
1202
|
+
* Create a persistent message handler for websocket flag updates
|
|
1203
|
+
*/
|
|
1204
|
+
createPersistentMessageHandler(context) {
|
|
1205
|
+
return (event) => {
|
|
1206
|
+
const message = JSON.parse(event.data);
|
|
1207
|
+
this.debug(`WebSocket persistent message received:`, message);
|
|
1208
|
+
if (!(contextString(context) in this.checks)) {
|
|
1209
|
+
this.checks[contextString(context)] = {};
|
|
1210
|
+
}
|
|
1211
|
+
(message.flags ?? []).forEach((flag) => {
|
|
1212
|
+
const flagCheck = CheckFlagReturnFromJSON(flag);
|
|
1213
|
+
const contextStr = contextString(context);
|
|
1214
|
+
if (this.checks[contextStr] === void 0) {
|
|
1215
|
+
this.checks[contextStr] = {};
|
|
1216
|
+
}
|
|
1217
|
+
this.checks[contextStr][flagCheck.flag] = flagCheck;
|
|
1218
|
+
if (typeof flagCheck.featureUsageEvent === "string") {
|
|
1219
|
+
this.updateFeatureUsageEventMap(flagCheck);
|
|
1220
|
+
}
|
|
1221
|
+
if ((this.flagCheckListeners[flag.flag]?.size ?? 0) > 0 || (this.flagValueListeners[flag.flag]?.size ?? 0) > 0) {
|
|
1222
|
+
this.submitFlagCheckEvent(flagCheck.flag, flagCheck, context);
|
|
1223
|
+
}
|
|
1224
|
+
this.debug(
|
|
1225
|
+
`WebSocket flag update received. Notifying listeners for ${flag.flag}`,
|
|
1226
|
+
{
|
|
1227
|
+
flag: flag.flag,
|
|
1228
|
+
value: flagCheck.value,
|
|
1229
|
+
flagCheck
|
|
1230
|
+
}
|
|
1231
|
+
);
|
|
1232
|
+
this.notifyFlagCheckListeners(flag.flag, flagCheck);
|
|
1233
|
+
this.notifyFlagValueListeners(flag.flag, flagCheck.value);
|
|
1234
|
+
});
|
|
1235
|
+
if (message.plan !== void 0 && message.plan !== null) {
|
|
1236
|
+
const plan = CheckPlanReturnFromJSON(message.plan);
|
|
1237
|
+
const contextStr = contextString(context);
|
|
1238
|
+
this.planChecks[contextStr] = plan;
|
|
1239
|
+
this.debug(`WebSocket plan update received. Notifying listeners`, {
|
|
1240
|
+
plan
|
|
1241
|
+
});
|
|
1242
|
+
this.notifyPlanListeners(plan);
|
|
1243
|
+
}
|
|
1244
|
+
this.flushContextDependentEventQueue();
|
|
1245
|
+
this.setIsPending(false);
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
977
1248
|
/**
|
|
978
1249
|
* Helper function to check if client is in offline mode
|
|
979
1250
|
*/
|
|
@@ -1000,52 +1271,6 @@ var Schematic = class {
|
|
|
1000
1271
|
this.debug(`submitting flag check event:`, eventBody);
|
|
1001
1272
|
return this.handleEvent("flag_check", EventBodyFlagCheckToJSON(eventBody));
|
|
1002
1273
|
}
|
|
1003
|
-
/**
|
|
1004
|
-
* Helper method for falling back to REST API when WebSocket connection fails
|
|
1005
|
-
*/
|
|
1006
|
-
async fallbackToRest(key, context, fallback) {
|
|
1007
|
-
if (this.isOffline()) {
|
|
1008
|
-
this.debug(`fallbackToRest offline result: ${key}`, {
|
|
1009
|
-
value: fallback,
|
|
1010
|
-
offlineMode: true
|
|
1011
|
-
});
|
|
1012
|
-
return fallback;
|
|
1013
|
-
}
|
|
1014
|
-
try {
|
|
1015
|
-
const requestUrl = `${this.apiUrl}/flags/${key}/check`;
|
|
1016
|
-
const response = await fetch(requestUrl, {
|
|
1017
|
-
method: "POST",
|
|
1018
|
-
headers: {
|
|
1019
|
-
...this.additionalHeaders ?? {},
|
|
1020
|
-
"Content-Type": "application/json;charset=UTF-8",
|
|
1021
|
-
"X-Schematic-Api-Key": this.apiKey
|
|
1022
|
-
},
|
|
1023
|
-
body: JSON.stringify(context)
|
|
1024
|
-
});
|
|
1025
|
-
if (!response.ok) {
|
|
1026
|
-
throw new Error("Network response was not ok");
|
|
1027
|
-
}
|
|
1028
|
-
const responseJson = await response.json();
|
|
1029
|
-
const data = CheckFlagResponseFromJSON(responseJson);
|
|
1030
|
-
this.debug(`fallbackToRest result: ${key}`, data);
|
|
1031
|
-
const result = CheckFlagReturnFromJSON(data.data);
|
|
1032
|
-
if (typeof result.featureUsageEvent === "string") {
|
|
1033
|
-
this.updateFeatureUsageEventMap(result);
|
|
1034
|
-
}
|
|
1035
|
-
this.submitFlagCheckEvent(key, result, context);
|
|
1036
|
-
return result.value;
|
|
1037
|
-
} catch (error) {
|
|
1038
|
-
console.error("REST API call failed, using fallback value:", error);
|
|
1039
|
-
const errorResult = {
|
|
1040
|
-
flag: key,
|
|
1041
|
-
value: fallback,
|
|
1042
|
-
reason: "API request failed (fallback)",
|
|
1043
|
-
error: error instanceof Error ? error.message : String(error)
|
|
1044
|
-
};
|
|
1045
|
-
this.submitFlagCheckEvent(key, errorResult, context);
|
|
1046
|
-
return fallback;
|
|
1047
|
-
}
|
|
1048
|
-
}
|
|
1049
1274
|
/**
|
|
1050
1275
|
* Make an API call to fetch all flag values for a given context.
|
|
1051
1276
|
* Recommended for use in REST mode only.
|
|
@@ -1084,7 +1309,7 @@ var Schematic = class {
|
|
|
1084
1309
|
{}
|
|
1085
1310
|
);
|
|
1086
1311
|
}).catch((error) => {
|
|
1087
|
-
console.
|
|
1312
|
+
console.warn("There was a problem with the fetch operation:", error);
|
|
1088
1313
|
return {};
|
|
1089
1314
|
});
|
|
1090
1315
|
};
|
|
@@ -1095,14 +1320,12 @@ var Schematic = class {
|
|
|
1095
1320
|
*/
|
|
1096
1321
|
identify = (body) => {
|
|
1097
1322
|
this.debug(`identify:`, body);
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
}
|
|
1104
|
-
console.error("Error setting context:", error);
|
|
1105
|
-
}
|
|
1323
|
+
this.setContext({
|
|
1324
|
+
company: body.company?.keys,
|
|
1325
|
+
user: body.keys
|
|
1326
|
+
}).catch((error) => {
|
|
1327
|
+
console.warn("Error setting context:", error);
|
|
1328
|
+
});
|
|
1106
1329
|
return this.handleEvent("identify", body);
|
|
1107
1330
|
};
|
|
1108
1331
|
/**
|
|
@@ -1121,16 +1344,52 @@ var Schematic = class {
|
|
|
1121
1344
|
this.setIsPending(false);
|
|
1122
1345
|
return Promise.resolve();
|
|
1123
1346
|
}
|
|
1347
|
+
if (contextString(context) === contextString(this.context) && this.conn !== null && !this.isPending) {
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1124
1350
|
try {
|
|
1125
1351
|
this.setIsPending(true);
|
|
1126
1352
|
if (!this.conn) {
|
|
1127
|
-
|
|
1353
|
+
if (this.isConnecting) {
|
|
1354
|
+
this.debug(
|
|
1355
|
+
`Connection already in progress, waiting for it to complete`
|
|
1356
|
+
);
|
|
1357
|
+
while (this.isConnecting && this.conn === null) {
|
|
1358
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1359
|
+
}
|
|
1360
|
+
if (this.conn !== null) {
|
|
1361
|
+
const socket2 = await this.conn;
|
|
1362
|
+
await this.wsSendMessage(socket2, context);
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
if (this.wsReconnectTimer !== null) {
|
|
1367
|
+
this.debug(
|
|
1368
|
+
`Cancelling scheduled reconnection, connecting immediately`
|
|
1369
|
+
);
|
|
1370
|
+
clearTimeout(this.wsReconnectTimer);
|
|
1371
|
+
this.wsReconnectTimer = null;
|
|
1372
|
+
}
|
|
1373
|
+
this.isConnecting = true;
|
|
1374
|
+
try {
|
|
1375
|
+
this.conn = this.wsConnect();
|
|
1376
|
+
const socket2 = await this.conn;
|
|
1377
|
+
this.isConnecting = false;
|
|
1378
|
+
await this.wsSendMessage(socket2, context);
|
|
1379
|
+
return;
|
|
1380
|
+
} catch (error) {
|
|
1381
|
+
this.isConnecting = false;
|
|
1382
|
+
throw error;
|
|
1383
|
+
}
|
|
1128
1384
|
}
|
|
1129
1385
|
const socket = await this.conn;
|
|
1130
1386
|
await this.wsSendMessage(socket, context);
|
|
1131
1387
|
} catch (error) {
|
|
1132
|
-
console.
|
|
1133
|
-
|
|
1388
|
+
console.warn("Failed to establish WebSocket connection:", error);
|
|
1389
|
+
this.context = context;
|
|
1390
|
+
this.flushContextDependentEventQueue();
|
|
1391
|
+
this.setIsPending(false);
|
|
1392
|
+
this.attemptReconnect();
|
|
1134
1393
|
}
|
|
1135
1394
|
};
|
|
1136
1395
|
/**
|
|
@@ -1253,11 +1512,57 @@ var Schematic = class {
|
|
|
1253
1512
|
}
|
|
1254
1513
|
}
|
|
1255
1514
|
};
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1515
|
+
startRetryTimer = () => {
|
|
1516
|
+
if (this.retryTimer !== null) {
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
this.retryTimer = setInterval(() => {
|
|
1520
|
+
this.flushEventQueue().catch((error) => {
|
|
1521
|
+
this.debug("Error in retry timer flush:", error);
|
|
1522
|
+
});
|
|
1523
|
+
if (this.eventQueue.length === 0) {
|
|
1524
|
+
this.stopRetryTimer();
|
|
1525
|
+
}
|
|
1526
|
+
}, 5e3);
|
|
1527
|
+
this.debug("Started retry timer");
|
|
1528
|
+
};
|
|
1529
|
+
stopRetryTimer = () => {
|
|
1530
|
+
if (this.retryTimer !== null) {
|
|
1531
|
+
clearInterval(this.retryTimer);
|
|
1532
|
+
this.retryTimer = null;
|
|
1533
|
+
this.debug("Stopped retry timer");
|
|
1534
|
+
}
|
|
1535
|
+
};
|
|
1536
|
+
flushEventQueue = async () => {
|
|
1537
|
+
if (this.eventQueue.length === 0) {
|
|
1538
|
+
return;
|
|
1539
|
+
}
|
|
1540
|
+
const now = Date.now();
|
|
1541
|
+
const readyEvents = [];
|
|
1542
|
+
const notReadyEvents = [];
|
|
1543
|
+
for (const event of this.eventQueue) {
|
|
1544
|
+
if (event.next_retry_at === void 0 || event.next_retry_at <= now) {
|
|
1545
|
+
readyEvents.push(event);
|
|
1546
|
+
} else {
|
|
1547
|
+
notReadyEvents.push(event);
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
if (readyEvents.length === 0) {
|
|
1551
|
+
this.debug(
|
|
1552
|
+
`No events ready for retry yet (${notReadyEvents.length} still in backoff)`
|
|
1553
|
+
);
|
|
1554
|
+
return;
|
|
1555
|
+
}
|
|
1556
|
+
this.debug(
|
|
1557
|
+
`Flushing event queue: ${readyEvents.length} ready, ${notReadyEvents.length} waiting`
|
|
1558
|
+
);
|
|
1559
|
+
this.eventQueue = notReadyEvents;
|
|
1560
|
+
for (const event of readyEvents) {
|
|
1561
|
+
try {
|
|
1562
|
+
await this.sendEvent(event);
|
|
1563
|
+
this.debug(`Queued event sent successfully:`, event.type);
|
|
1564
|
+
} catch (error) {
|
|
1565
|
+
this.debug(`Failed to send queued event:`, error);
|
|
1261
1566
|
}
|
|
1262
1567
|
}
|
|
1263
1568
|
};
|
|
@@ -1305,12 +1610,47 @@ var Schematic = class {
|
|
|
1305
1610
|
},
|
|
1306
1611
|
body: payload
|
|
1307
1612
|
});
|
|
1613
|
+
if (!response.ok) {
|
|
1614
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
1615
|
+
}
|
|
1308
1616
|
this.debug(`event sent:`, {
|
|
1309
1617
|
status: response.status,
|
|
1310
1618
|
statusText: response.statusText
|
|
1311
1619
|
});
|
|
1312
1620
|
} catch (error) {
|
|
1313
|
-
|
|
1621
|
+
const retryCount = (event.retry_count ?? 0) + 1;
|
|
1622
|
+
if (retryCount <= this.maxEventRetries) {
|
|
1623
|
+
this.debug(
|
|
1624
|
+
`Event failed to send (attempt ${retryCount}/${this.maxEventRetries}), queueing for retry:`,
|
|
1625
|
+
error
|
|
1626
|
+
);
|
|
1627
|
+
const baseDelay = this.eventRetryInitialDelay * Math.pow(2, retryCount - 1);
|
|
1628
|
+
const jitterDelay = Math.min(baseDelay, this.eventRetryMaxDelay);
|
|
1629
|
+
const nextRetryAt = Date.now() + jitterDelay;
|
|
1630
|
+
const retryEvent = {
|
|
1631
|
+
...event,
|
|
1632
|
+
retry_count: retryCount,
|
|
1633
|
+
next_retry_at: nextRetryAt
|
|
1634
|
+
};
|
|
1635
|
+
if (this.eventQueue.length < this.maxEventQueueSize) {
|
|
1636
|
+
this.eventQueue.push(retryEvent);
|
|
1637
|
+
this.debug(
|
|
1638
|
+
`Event queued for retry in ${jitterDelay}ms (${this.eventQueue.length}/${this.maxEventQueueSize})`
|
|
1639
|
+
);
|
|
1640
|
+
} else {
|
|
1641
|
+
this.debug(
|
|
1642
|
+
`Event queue full (${this.maxEventQueueSize}), dropping oldest event`
|
|
1643
|
+
);
|
|
1644
|
+
this.eventQueue.shift();
|
|
1645
|
+
this.eventQueue.push(retryEvent);
|
|
1646
|
+
}
|
|
1647
|
+
this.startRetryTimer();
|
|
1648
|
+
} else {
|
|
1649
|
+
this.debug(
|
|
1650
|
+
`Event failed permanently after ${this.maxEventRetries} attempts, dropping:`,
|
|
1651
|
+
error
|
|
1652
|
+
);
|
|
1653
|
+
}
|
|
1314
1654
|
}
|
|
1315
1655
|
return Promise.resolve();
|
|
1316
1656
|
};
|
|
@@ -1321,6 +1661,130 @@ var Schematic = class {
|
|
|
1321
1661
|
/**
|
|
1322
1662
|
* Websocket management
|
|
1323
1663
|
*/
|
|
1664
|
+
/**
|
|
1665
|
+
* Force an immediate WebSocket reconnection.
|
|
1666
|
+
* This is useful when the application returns from a background state (e.g., mobile app
|
|
1667
|
+
* coming back to foreground) and wants to immediately re-establish the connection
|
|
1668
|
+
* rather than waiting for the exponential backoff timer.
|
|
1669
|
+
*
|
|
1670
|
+
* This method will:
|
|
1671
|
+
* - Cancel any pending reconnection timer
|
|
1672
|
+
* - Reset the reconnection attempt counter
|
|
1673
|
+
* - Close any existing connection
|
|
1674
|
+
* - Immediately attempt to reconnect
|
|
1675
|
+
* - Re-send the current context to get fresh flag values
|
|
1676
|
+
*
|
|
1677
|
+
* Use this when you need guaranteed fresh values (e.g., after an in-app purchase).
|
|
1678
|
+
*
|
|
1679
|
+
* @example
|
|
1680
|
+
* ```typescript
|
|
1681
|
+
* // React Native example: reconnect when app comes to foreground
|
|
1682
|
+
* useEffect(() => {
|
|
1683
|
+
* const subscription = AppState.addEventListener("change", (state) => {
|
|
1684
|
+
* if (state === "active") {
|
|
1685
|
+
* client.forceReconnect();
|
|
1686
|
+
* }
|
|
1687
|
+
* });
|
|
1688
|
+
* return () => subscription.remove();
|
|
1689
|
+
* }, [client]);
|
|
1690
|
+
* ```
|
|
1691
|
+
*/
|
|
1692
|
+
forceReconnect = async () => {
|
|
1693
|
+
return this.reconnect({ force: true });
|
|
1694
|
+
};
|
|
1695
|
+
/**
|
|
1696
|
+
* Reconnect the WebSocket connection only if the current connection is unhealthy.
|
|
1697
|
+
* This is useful when the application returns from a background state and wants to
|
|
1698
|
+
* ensure a healthy connection exists, but doesn't need to force a reconnection if
|
|
1699
|
+
* the connection is still active.
|
|
1700
|
+
*
|
|
1701
|
+
* This method will:
|
|
1702
|
+
* - Check if an existing connection is healthy (readyState === OPEN)
|
|
1703
|
+
* - If healthy, return immediately without reconnecting
|
|
1704
|
+
* - If unhealthy, perform the same reconnection logic as forceReconnect()
|
|
1705
|
+
*
|
|
1706
|
+
* Use this when you want efficient reconnection that avoids unnecessary disconnects.
|
|
1707
|
+
*
|
|
1708
|
+
* @example
|
|
1709
|
+
* ```typescript
|
|
1710
|
+
* // React Native example: reconnect only if needed when app comes to foreground
|
|
1711
|
+
* useEffect(() => {
|
|
1712
|
+
* const subscription = AppState.addEventListener("change", (state) => {
|
|
1713
|
+
* if (state === "active") {
|
|
1714
|
+
* client.reconnectIfNeeded();
|
|
1715
|
+
* }
|
|
1716
|
+
* });
|
|
1717
|
+
* return () => subscription.remove();
|
|
1718
|
+
* }, [client]);
|
|
1719
|
+
* ```
|
|
1720
|
+
*/
|
|
1721
|
+
reconnectIfNeeded = async () => {
|
|
1722
|
+
return this.reconnect({ force: false });
|
|
1723
|
+
};
|
|
1724
|
+
/**
|
|
1725
|
+
* Internal method to handle reconnection logic for both forceReconnect and reconnectIfNeeded.
|
|
1726
|
+
*/
|
|
1727
|
+
reconnect = async (options) => {
|
|
1728
|
+
const { force } = options;
|
|
1729
|
+
const methodName = force ? "forceReconnect" : "reconnectIfNeeded";
|
|
1730
|
+
if (this.isOffline()) {
|
|
1731
|
+
this.debug(`${methodName}: skipped (offline mode)`);
|
|
1732
|
+
return Promise.resolve();
|
|
1733
|
+
}
|
|
1734
|
+
if (!force && this.conn !== null) {
|
|
1735
|
+
try {
|
|
1736
|
+
const existingSocket = await this.conn;
|
|
1737
|
+
if (existingSocket.readyState === WebSocket.OPEN) {
|
|
1738
|
+
this.debug(`${methodName}: connection is healthy, skipping`);
|
|
1739
|
+
return Promise.resolve();
|
|
1740
|
+
}
|
|
1741
|
+
} catch {
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
this.debug(
|
|
1745
|
+
`${methodName}: ${force ? "forcing immediate reconnection" : "reconnecting"}`
|
|
1746
|
+
);
|
|
1747
|
+
this.wsIntentionalDisconnect = false;
|
|
1748
|
+
if (this.wsReconnectTimer !== null) {
|
|
1749
|
+
this.debug(`${methodName}: cancelling pending reconnection timer`);
|
|
1750
|
+
clearTimeout(this.wsReconnectTimer);
|
|
1751
|
+
this.wsReconnectTimer = null;
|
|
1752
|
+
}
|
|
1753
|
+
this.wsReconnectAttempts = 0;
|
|
1754
|
+
if (this.conn !== null) {
|
|
1755
|
+
this.debug(`${methodName}: closing existing connection`);
|
|
1756
|
+
try {
|
|
1757
|
+
const existingSocket = await this.conn;
|
|
1758
|
+
if (this.currentWebSocket === existingSocket) {
|
|
1759
|
+
this.currentWebSocket = null;
|
|
1760
|
+
}
|
|
1761
|
+
if (existingSocket.readyState === WebSocket.OPEN || existingSocket.readyState === WebSocket.CONNECTING) {
|
|
1762
|
+
existingSocket.close();
|
|
1763
|
+
}
|
|
1764
|
+
} catch (error) {
|
|
1765
|
+
this.debug(`${methodName}: error closing existing connection:`, error);
|
|
1766
|
+
}
|
|
1767
|
+
this.conn = null;
|
|
1768
|
+
this.isConnecting = false;
|
|
1769
|
+
}
|
|
1770
|
+
if (this.context.company !== void 0 || this.context.user !== void 0) {
|
|
1771
|
+
this.debug(`${methodName}: reconnecting with existing context`);
|
|
1772
|
+
try {
|
|
1773
|
+
this.isConnecting = true;
|
|
1774
|
+
this.conn = this.wsConnect();
|
|
1775
|
+
const socket = await this.conn;
|
|
1776
|
+
this.isConnecting = false;
|
|
1777
|
+
await this.wsSendMessage(socket, this.context, true);
|
|
1778
|
+
this.debug(`${methodName}: reconnection successful`);
|
|
1779
|
+
} catch (error) {
|
|
1780
|
+
this.isConnecting = false;
|
|
1781
|
+
this.debug(`${methodName}: reconnection failed:`, error);
|
|
1782
|
+
}
|
|
1783
|
+
} else {
|
|
1784
|
+
this.debug(`${methodName}: no context set, skipping reconnection`);
|
|
1785
|
+
}
|
|
1786
|
+
return Promise.resolve();
|
|
1787
|
+
};
|
|
1324
1788
|
/**
|
|
1325
1789
|
* If using websocket mode, close the connection when done.
|
|
1326
1790
|
* In offline mode, this is a no-op.
|
|
@@ -1330,95 +1794,307 @@ var Schematic = class {
|
|
|
1330
1794
|
this.debug("cleanup: skipped (offline mode)");
|
|
1331
1795
|
return Promise.resolve();
|
|
1332
1796
|
}
|
|
1797
|
+
this.wsIntentionalDisconnect = true;
|
|
1798
|
+
if (this.wsReconnectTimer !== null) {
|
|
1799
|
+
clearTimeout(this.wsReconnectTimer);
|
|
1800
|
+
this.wsReconnectTimer = null;
|
|
1801
|
+
}
|
|
1802
|
+
this.stopRetryTimer();
|
|
1333
1803
|
if (this.conn) {
|
|
1334
1804
|
try {
|
|
1335
1805
|
const socket = await this.conn;
|
|
1806
|
+
if (this.currentWebSocket === socket) {
|
|
1807
|
+
this.debug(`Cleaning up current websocket tracking`);
|
|
1808
|
+
this.currentWebSocket = null;
|
|
1809
|
+
}
|
|
1336
1810
|
socket.close();
|
|
1337
1811
|
} catch (error) {
|
|
1338
|
-
console.
|
|
1812
|
+
console.warn("Error during cleanup:", error);
|
|
1339
1813
|
} finally {
|
|
1340
1814
|
this.conn = null;
|
|
1815
|
+
this.currentWebSocket = null;
|
|
1816
|
+
this.isConnecting = false;
|
|
1341
1817
|
}
|
|
1342
1818
|
}
|
|
1343
1819
|
};
|
|
1344
|
-
|
|
1345
|
-
|
|
1820
|
+
/**
|
|
1821
|
+
* Calculate the delay for the next reconnection attempt using exponential backoff with jitter.
|
|
1822
|
+
* This helps prevent dogpiling when the server recovers from an outage.
|
|
1823
|
+
*/
|
|
1824
|
+
calculateReconnectDelay = () => {
|
|
1825
|
+
const exponentialDelay = this.webSocketInitialRetryDelay * Math.pow(2, this.wsReconnectAttempts);
|
|
1826
|
+
const cappedDelay = Math.min(exponentialDelay, this.webSocketMaxRetryDelay);
|
|
1827
|
+
const jitter = Math.random() * cappedDelay * 0.5;
|
|
1828
|
+
const totalDelay = cappedDelay + jitter;
|
|
1829
|
+
this.debug(
|
|
1830
|
+
`Reconnect delay calculated: ${totalDelay.toFixed(0)}ms (attempt ${this.wsReconnectAttempts + 1}/${this.webSocketMaxReconnectAttempts})`
|
|
1831
|
+
);
|
|
1832
|
+
return totalDelay;
|
|
1833
|
+
};
|
|
1834
|
+
/**
|
|
1835
|
+
* Handle browser going offline
|
|
1836
|
+
*/
|
|
1837
|
+
handleNetworkOffline = async () => {
|
|
1838
|
+
if (this.conn !== null) {
|
|
1839
|
+
try {
|
|
1840
|
+
const socket = await this.conn;
|
|
1841
|
+
if (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING) {
|
|
1842
|
+
socket.close();
|
|
1843
|
+
}
|
|
1844
|
+
} catch (error) {
|
|
1845
|
+
this.debug("Error closing connection on offline:", error);
|
|
1846
|
+
}
|
|
1847
|
+
this.conn = null;
|
|
1848
|
+
}
|
|
1849
|
+
if (this.wsReconnectTimer !== null) {
|
|
1850
|
+
clearTimeout(this.wsReconnectTimer);
|
|
1851
|
+
this.wsReconnectTimer = null;
|
|
1852
|
+
}
|
|
1853
|
+
};
|
|
1854
|
+
/**
|
|
1855
|
+
* Handle browser coming back online
|
|
1856
|
+
*/
|
|
1857
|
+
handleNetworkOnline = () => {
|
|
1858
|
+
this.debug(
|
|
1859
|
+
"Network online, attempting reconnection and flushing queued events"
|
|
1860
|
+
);
|
|
1861
|
+
this.wsReconnectAttempts = 0;
|
|
1862
|
+
if (this.wsReconnectTimer !== null) {
|
|
1863
|
+
clearTimeout(this.wsReconnectTimer);
|
|
1864
|
+
this.wsReconnectTimer = null;
|
|
1865
|
+
}
|
|
1866
|
+
this.flushEventQueue().catch((error) => {
|
|
1867
|
+
this.debug("Error flushing event queue on network online:", error);
|
|
1868
|
+
});
|
|
1869
|
+
this.attemptReconnect();
|
|
1870
|
+
};
|
|
1871
|
+
/**
|
|
1872
|
+
* Attempt to reconnect the WebSocket connection with exponential backoff.
|
|
1873
|
+
* Called automatically when the connection closes unexpectedly.
|
|
1874
|
+
*/
|
|
1875
|
+
attemptReconnect = () => {
|
|
1876
|
+
if (this.wsReconnectAttempts >= this.webSocketMaxReconnectAttempts) {
|
|
1877
|
+
this.debug(
|
|
1878
|
+
`Maximum reconnection attempts (${this.webSocketMaxReconnectAttempts}) reached, giving up`
|
|
1879
|
+
);
|
|
1880
|
+
return;
|
|
1881
|
+
}
|
|
1882
|
+
if (this.wsReconnectTimer !== null) {
|
|
1883
|
+
this.debug(
|
|
1884
|
+
`Reconnection attempt already scheduled, ignoring duplicate request`
|
|
1885
|
+
);
|
|
1886
|
+
return;
|
|
1887
|
+
}
|
|
1888
|
+
const delay = this.calculateReconnectDelay();
|
|
1889
|
+
this.debug(
|
|
1890
|
+
`Scheduling reconnection attempt ${this.wsReconnectAttempts + 1}/${this.webSocketMaxReconnectAttempts} in ${delay.toFixed(0)}ms`
|
|
1891
|
+
);
|
|
1892
|
+
this.wsReconnectTimer = setTimeout(async () => {
|
|
1893
|
+
this.wsReconnectTimer = null;
|
|
1894
|
+
this.wsReconnectAttempts++;
|
|
1895
|
+
this.debug(
|
|
1896
|
+
`Attempting to reconnect (attempt ${this.wsReconnectAttempts}/${this.webSocketMaxReconnectAttempts})`
|
|
1897
|
+
);
|
|
1898
|
+
try {
|
|
1899
|
+
if (this.conn !== null) {
|
|
1900
|
+
this.debug(`Cleaning up existing connection before reconnection`);
|
|
1901
|
+
try {
|
|
1902
|
+
const existingSocket = await this.conn;
|
|
1903
|
+
if (this.currentWebSocket === existingSocket) {
|
|
1904
|
+
this.debug(`Existing websocket is current, will be replaced`);
|
|
1905
|
+
this.currentWebSocket = null;
|
|
1906
|
+
}
|
|
1907
|
+
if (existingSocket.readyState === WebSocket.OPEN || existingSocket.readyState === WebSocket.CONNECTING) {
|
|
1908
|
+
existingSocket.close();
|
|
1909
|
+
}
|
|
1910
|
+
} catch (error) {
|
|
1911
|
+
this.debug(`Error cleaning up existing connection:`, error);
|
|
1912
|
+
}
|
|
1913
|
+
this.conn = null;
|
|
1914
|
+
this.currentWebSocket = null;
|
|
1915
|
+
this.isConnecting = false;
|
|
1916
|
+
}
|
|
1917
|
+
this.isConnecting = true;
|
|
1918
|
+
try {
|
|
1919
|
+
this.conn = this.wsConnect();
|
|
1920
|
+
const socket = await this.conn;
|
|
1921
|
+
this.isConnecting = false;
|
|
1922
|
+
this.debug(`Reconnection context check:`, {
|
|
1923
|
+
hasCompany: this.context.company !== void 0,
|
|
1924
|
+
hasUser: this.context.user !== void 0,
|
|
1925
|
+
context: this.context
|
|
1926
|
+
});
|
|
1927
|
+
if (this.context.company !== void 0 || this.context.user !== void 0) {
|
|
1928
|
+
this.debug(`Reconnected, force re-sending context`);
|
|
1929
|
+
await this.wsSendMessage(socket, this.context, true);
|
|
1930
|
+
} else {
|
|
1931
|
+
this.debug(
|
|
1932
|
+
`No context to re-send after reconnection - websocket ready for new context`
|
|
1933
|
+
);
|
|
1934
|
+
this.debug(
|
|
1935
|
+
`Setting up tracking for reconnected websocket (no context to send)`
|
|
1936
|
+
);
|
|
1937
|
+
this.currentWebSocket = socket;
|
|
1938
|
+
}
|
|
1939
|
+
this.flushEventQueue().catch((error) => {
|
|
1940
|
+
this.debug(
|
|
1941
|
+
"Error flushing event queue after websocket reconnection:",
|
|
1942
|
+
error
|
|
1943
|
+
);
|
|
1944
|
+
});
|
|
1945
|
+
this.debug(`Reconnection successful`);
|
|
1946
|
+
} catch (error) {
|
|
1947
|
+
this.isConnecting = false;
|
|
1948
|
+
throw error;
|
|
1949
|
+
}
|
|
1950
|
+
} catch (error) {
|
|
1951
|
+
this.debug(`Reconnection attempt failed:`, error);
|
|
1952
|
+
}
|
|
1953
|
+
}, delay);
|
|
1954
|
+
};
|
|
1955
|
+
// Open a websocket connection with retry logic for timeouts
|
|
1956
|
+
wsConnect = async () => {
|
|
1346
1957
|
if (this.isOffline()) {
|
|
1347
1958
|
this.debug("wsConnect: skipped (offline mode)");
|
|
1348
|
-
|
|
1349
|
-
new Error("WebSocket connection skipped in offline mode")
|
|
1350
|
-
);
|
|
1959
|
+
throw new Error("WebSocket connection skipped in offline mode");
|
|
1351
1960
|
}
|
|
1961
|
+
let lastError = null;
|
|
1962
|
+
const maxAttempts = this.webSocketMaxConnectionAttempts;
|
|
1963
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
1964
|
+
try {
|
|
1965
|
+
const socket = await this.wsConnectOnce();
|
|
1966
|
+
this.wsReconnectAttempts = 0;
|
|
1967
|
+
return socket;
|
|
1968
|
+
} catch (error) {
|
|
1969
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
1970
|
+
const isTimeout = lastError.message === "WebSocket connection timeout";
|
|
1971
|
+
if (!isTimeout) {
|
|
1972
|
+
this.debug(
|
|
1973
|
+
`WebSocket connection failed with non-timeout error, not retrying:`,
|
|
1974
|
+
lastError.message
|
|
1975
|
+
);
|
|
1976
|
+
throw lastError;
|
|
1977
|
+
}
|
|
1978
|
+
if (attempt < maxAttempts - 1) {
|
|
1979
|
+
const baseDelay = this.webSocketInitialRetryDelay * Math.pow(2, attempt);
|
|
1980
|
+
const cappedDelay = Math.min(baseDelay, this.webSocketMaxRetryDelay);
|
|
1981
|
+
const jitter = cappedDelay * 0.2 * Math.random();
|
|
1982
|
+
const delay = cappedDelay + jitter;
|
|
1983
|
+
this.debug(
|
|
1984
|
+
`WebSocket connection timeout (attempt ${attempt + 1}/${maxAttempts}), retrying in ${delay.toFixed(0)}ms`
|
|
1985
|
+
);
|
|
1986
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1987
|
+
} else {
|
|
1988
|
+
this.debug(
|
|
1989
|
+
`WebSocket connection timeout (attempt ${attempt + 1}/${maxAttempts}), no more retries`
|
|
1990
|
+
);
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
throw lastError ?? new Error("WebSocket connection failed");
|
|
1995
|
+
};
|
|
1996
|
+
// Single attempt to open a websocket connection (no retry logic)
|
|
1997
|
+
wsConnectOnce = () => {
|
|
1352
1998
|
return new Promise((resolve, reject) => {
|
|
1353
1999
|
const wsUrl = `${this.webSocketUrl}/flags/bootstrap?apiKey=${this.apiKey}`;
|
|
1354
2000
|
this.debug(`connecting to WebSocket:`, wsUrl);
|
|
1355
2001
|
const webSocket = new WebSocket(wsUrl);
|
|
2002
|
+
const connectionId = Math.random().toString(36).substring(7);
|
|
2003
|
+
this.debug(`Creating WebSocket connection ${connectionId} to ${wsUrl}`);
|
|
2004
|
+
let timeoutId = null;
|
|
2005
|
+
let isResolved = false;
|
|
2006
|
+
timeoutId = setTimeout(() => {
|
|
2007
|
+
if (!isResolved) {
|
|
2008
|
+
isResolved = true;
|
|
2009
|
+
this.debug(
|
|
2010
|
+
`WebSocket connection timeout after ${this.webSocketConnectionTimeout}ms`
|
|
2011
|
+
);
|
|
2012
|
+
webSocket.close();
|
|
2013
|
+
reject(new Error("WebSocket connection timeout"));
|
|
2014
|
+
}
|
|
2015
|
+
}, this.webSocketConnectionTimeout);
|
|
1356
2016
|
webSocket.onopen = () => {
|
|
1357
|
-
|
|
2017
|
+
if (isResolved) return;
|
|
2018
|
+
isResolved = true;
|
|
2019
|
+
if (timeoutId !== null) {
|
|
2020
|
+
clearTimeout(timeoutId);
|
|
2021
|
+
}
|
|
2022
|
+
this.wsIntentionalDisconnect = false;
|
|
2023
|
+
this.debug(`WebSocket connection ${connectionId} opened successfully`);
|
|
1358
2024
|
resolve(webSocket);
|
|
1359
2025
|
};
|
|
1360
2026
|
webSocket.onerror = (error) => {
|
|
1361
|
-
|
|
1362
|
-
|
|
2027
|
+
if (isResolved) return;
|
|
2028
|
+
isResolved = true;
|
|
2029
|
+
if (timeoutId !== null) {
|
|
2030
|
+
clearTimeout(timeoutId);
|
|
2031
|
+
}
|
|
2032
|
+
this.debug(`WebSocket connection ${connectionId} error:`, error);
|
|
2033
|
+
const wrappedError = new Error(
|
|
2034
|
+
"WebSocket connection failed during handshake"
|
|
2035
|
+
);
|
|
2036
|
+
reject(wrappedError);
|
|
1363
2037
|
};
|
|
1364
2038
|
webSocket.onclose = () => {
|
|
1365
|
-
|
|
2039
|
+
if (timeoutId !== null) {
|
|
2040
|
+
clearTimeout(timeoutId);
|
|
2041
|
+
}
|
|
2042
|
+
this.debug(`WebSocket connection ${connectionId} closed`);
|
|
1366
2043
|
this.conn = null;
|
|
2044
|
+
if (this.currentWebSocket === webSocket) {
|
|
2045
|
+
this.currentWebSocket = null;
|
|
2046
|
+
this.isConnecting = false;
|
|
2047
|
+
}
|
|
2048
|
+
if (!isResolved && !this.wsIntentionalDisconnect && this.webSocketReconnect) {
|
|
2049
|
+
this.attemptReconnect();
|
|
2050
|
+
}
|
|
1367
2051
|
};
|
|
1368
2052
|
});
|
|
1369
2053
|
};
|
|
1370
2054
|
// Send a message on the websocket indicating interest in a particular evaluation context
|
|
1371
2055
|
// and wait for the initial set of flag values to be returned
|
|
1372
|
-
wsSendMessage = (socket, context) => {
|
|
2056
|
+
wsSendMessage = (socket, context, forceContextSend = false) => {
|
|
1373
2057
|
if (this.isOffline()) {
|
|
1374
2058
|
this.debug("wsSendMessage: skipped (offline mode)");
|
|
1375
2059
|
this.setIsPending(false);
|
|
1376
2060
|
return Promise.resolve();
|
|
1377
2061
|
}
|
|
1378
2062
|
return new Promise((resolve, reject) => {
|
|
1379
|
-
if (contextString(context) == contextString(this.context)) {
|
|
2063
|
+
if (!forceContextSend && contextString(context) == contextString(this.context)) {
|
|
1380
2064
|
this.debug(`WebSocket context unchanged, skipping update`);
|
|
1381
2065
|
return resolve(this.setIsPending(false));
|
|
1382
2066
|
}
|
|
1383
|
-
this.debug(
|
|
2067
|
+
this.debug(
|
|
2068
|
+
forceContextSend ? `WebSocket force sending context (reconnection):` : `WebSocket context updated:`,
|
|
2069
|
+
context
|
|
2070
|
+
);
|
|
1384
2071
|
this.context = context;
|
|
1385
2072
|
const sendMessage = () => {
|
|
1386
2073
|
let resolved = false;
|
|
2074
|
+
const persistentMessageHandler = this.createPersistentMessageHandler(context);
|
|
1387
2075
|
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);
|
|
2076
|
+
persistentMessageHandler(event);
|
|
1416
2077
|
if (!resolved) {
|
|
1417
2078
|
resolved = true;
|
|
1418
2079
|
resolve();
|
|
1419
2080
|
}
|
|
1420
2081
|
};
|
|
1421
2082
|
socket.addEventListener("message", messageHandler);
|
|
2083
|
+
socket.addEventListener("close", (event) => {
|
|
2084
|
+
if (!resolved) {
|
|
2085
|
+
resolved = true;
|
|
2086
|
+
if (event.code === 4001) {
|
|
2087
|
+
reject(
|
|
2088
|
+
new Error(
|
|
2089
|
+
`Authentication failed: ${event.reason !== "" ? event.reason : "Invalid API key"}`
|
|
2090
|
+
)
|
|
2091
|
+
);
|
|
2092
|
+
} else {
|
|
2093
|
+
reject(new Error("WebSocket connection closed unexpectedly"));
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
});
|
|
2097
|
+
this.currentWebSocket = socket;
|
|
1422
2098
|
const clientVersion = this.additionalHeaders["X-Schematic-Client-Version"] ?? `schematic-js@${version}`;
|
|
1423
2099
|
const messagePayload = {
|
|
1424
2100
|
apiKey: this.apiKey,
|
|
@@ -1454,21 +2130,49 @@ var Schematic = class {
|
|
|
1454
2130
|
};
|
|
1455
2131
|
};
|
|
1456
2132
|
setIsPending = (isPending) => {
|
|
2133
|
+
if (this.isPending === isPending) return;
|
|
1457
2134
|
this.isPending = isPending;
|
|
1458
2135
|
this.isPendingListeners.forEach(
|
|
1459
2136
|
(listener) => notifyPendingListener(listener, isPending)
|
|
1460
2137
|
);
|
|
1461
2138
|
};
|
|
2139
|
+
getPlan = () => {
|
|
2140
|
+
const contextStr = contextString(this.context);
|
|
2141
|
+
const plan = this.planChecks[contextStr];
|
|
2142
|
+
return plan;
|
|
2143
|
+
};
|
|
1462
2144
|
// flag checks state
|
|
1463
2145
|
getFlagCheck = (flagKey) => {
|
|
1464
2146
|
const contextStr = contextString(this.context);
|
|
1465
2147
|
const checks = this.checks[contextStr] ?? {};
|
|
1466
|
-
|
|
2148
|
+
const check = checks[flagKey];
|
|
2149
|
+
if (check !== void 0) {
|
|
2150
|
+
return check;
|
|
2151
|
+
}
|
|
2152
|
+
if (flagKey in this.flagCheckDefaults || flagKey in this.flagValueDefaults) {
|
|
2153
|
+
if (!(flagKey in this.fallbackCheckCache)) {
|
|
2154
|
+
this.fallbackCheckCache[flagKey] = this.resolveFallbackCheckFlagReturn(
|
|
2155
|
+
flagKey,
|
|
2156
|
+
void 0,
|
|
2157
|
+
"Default value used"
|
|
2158
|
+
);
|
|
2159
|
+
}
|
|
2160
|
+
return this.fallbackCheckCache[flagKey];
|
|
2161
|
+
}
|
|
2162
|
+
return void 0;
|
|
1467
2163
|
};
|
|
1468
2164
|
// flagValues state
|
|
1469
2165
|
getFlagValue = (flagKey) => {
|
|
1470
|
-
const
|
|
1471
|
-
|
|
2166
|
+
const contextStr = contextString(this.context);
|
|
2167
|
+
const checks = this.checks[contextStr] ?? {};
|
|
2168
|
+
const check = checks[flagKey];
|
|
2169
|
+
if (check?.value !== void 0) {
|
|
2170
|
+
return check.value;
|
|
2171
|
+
}
|
|
2172
|
+
if (flagKey in this.flagCheckDefaults || flagKey in this.flagValueDefaults) {
|
|
2173
|
+
return this.resolveFallbackValue(flagKey);
|
|
2174
|
+
}
|
|
2175
|
+
return void 0;
|
|
1472
2176
|
};
|
|
1473
2177
|
/** Register an event listener that will be notified with the boolean value for a given flag when this value changes */
|
|
1474
2178
|
addFlagValueListener = (flagKey, listener) => {
|
|
@@ -1490,6 +2194,12 @@ var Schematic = class {
|
|
|
1490
2194
|
this.flagCheckListeners[flagKey].delete(listener);
|
|
1491
2195
|
};
|
|
1492
2196
|
};
|
|
2197
|
+
addPlanListener = (listener) => {
|
|
2198
|
+
this.planListeners.add(listener);
|
|
2199
|
+
return () => {
|
|
2200
|
+
this.planListeners.delete(listener);
|
|
2201
|
+
};
|
|
2202
|
+
};
|
|
1493
2203
|
notifyFlagCheckListeners = (flagKey, check) => {
|
|
1494
2204
|
const listeners = this.flagCheckListeners?.[flagKey] ?? [];
|
|
1495
2205
|
if (listeners.size > 0) {
|
|
@@ -1526,7 +2236,32 @@ var Schematic = class {
|
|
|
1526
2236
|
{ value }
|
|
1527
2237
|
);
|
|
1528
2238
|
}
|
|
1529
|
-
listeners.forEach((listener) =>
|
|
2239
|
+
listeners.forEach((listener, index) => {
|
|
2240
|
+
this.debug(`Calling listener ${index} for flag ${flagKey}`, {
|
|
2241
|
+
flagKey,
|
|
2242
|
+
value
|
|
2243
|
+
});
|
|
2244
|
+
notifyFlagValueListener(listener, value);
|
|
2245
|
+
this.debug(`Listener ${index} for flag ${flagKey} completed`, {
|
|
2246
|
+
flagKey,
|
|
2247
|
+
value
|
|
2248
|
+
});
|
|
2249
|
+
});
|
|
2250
|
+
};
|
|
2251
|
+
notifyPlanListeners = (value) => {
|
|
2252
|
+
const listeners = this.planListeners ?? [];
|
|
2253
|
+
if (listeners.size > 0) {
|
|
2254
|
+
this.debug(`Notifying ${listeners.size} plan listeners`, { value });
|
|
2255
|
+
}
|
|
2256
|
+
listeners.forEach((listener, index) => {
|
|
2257
|
+
this.debug(`Calling listener ${index} for plan`, {
|
|
2258
|
+
value
|
|
2259
|
+
});
|
|
2260
|
+
notifyPlanListener(listener, value);
|
|
2261
|
+
this.debug(`Listener ${index} for plan completed`, {
|
|
2262
|
+
value
|
|
2263
|
+
});
|
|
2264
|
+
});
|
|
1530
2265
|
};
|
|
1531
2266
|
};
|
|
1532
2267
|
var notifyPendingListener = (listener, value) => {
|
|
@@ -1550,12 +2285,19 @@ var notifyFlagValueListener = (listener, value) => {
|
|
|
1550
2285
|
listener();
|
|
1551
2286
|
}
|
|
1552
2287
|
};
|
|
2288
|
+
var notifyPlanListener = (listener, value) => {
|
|
2289
|
+
if (listener.length > 0) {
|
|
2290
|
+
listener(value);
|
|
2291
|
+
} else {
|
|
2292
|
+
listener();
|
|
2293
|
+
}
|
|
2294
|
+
};
|
|
1553
2295
|
|
|
1554
2296
|
// src/context/schematic.ts
|
|
1555
2297
|
var import_vue = require("vue");
|
|
1556
2298
|
|
|
1557
2299
|
// src/version.ts
|
|
1558
|
-
var version2 = "
|
|
2300
|
+
var version2 = "1.4.0";
|
|
1559
2301
|
|
|
1560
2302
|
// src/context/schematic.ts
|
|
1561
2303
|
var SchematicSymbol = Symbol("schematic");
|
|
@@ -1671,6 +2413,23 @@ var useSchematicEntitlement = (key, opts) => {
|
|
|
1671
2413
|
userId: (0, import_vue2.computed)(() => flagCheck.value.userId)
|
|
1672
2414
|
};
|
|
1673
2415
|
};
|
|
2416
|
+
var useSchematicPlan = (opts) => {
|
|
2417
|
+
const client = useSchematicClient(opts);
|
|
2418
|
+
const fallback = opts?.fallback;
|
|
2419
|
+
const plan = (0, import_vue2.ref)(
|
|
2420
|
+
client.getPlan() ?? fallback
|
|
2421
|
+
);
|
|
2422
|
+
let unsubscribe = null;
|
|
2423
|
+
(0, import_vue2.onMounted)(() => {
|
|
2424
|
+
unsubscribe = client.addPlanListener((value) => {
|
|
2425
|
+
plan.value = value;
|
|
2426
|
+
});
|
|
2427
|
+
});
|
|
2428
|
+
(0, import_vue2.onScopeDispose)(() => {
|
|
2429
|
+
unsubscribe?.();
|
|
2430
|
+
});
|
|
2431
|
+
return plan;
|
|
2432
|
+
};
|
|
1674
2433
|
var useSchematicIsPending = (opts) => {
|
|
1675
2434
|
const client = useSchematicClient(opts);
|
|
1676
2435
|
const isPending = (0, import_vue2.ref)(client.getIsPending());
|