@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
|
@@ -606,6 +606,36 @@ function v4(options, buf, offset) {
|
|
|
606
606
|
}
|
|
607
607
|
var v4_default = v4;
|
|
608
608
|
var import_polyfill = __toESM(require_browser_polyfill());
|
|
609
|
+
function EntitlementValueTypeFromJSON(json) {
|
|
610
|
+
return EntitlementValueTypeFromJSONTyped(json, false);
|
|
611
|
+
}
|
|
612
|
+
function EntitlementValueTypeFromJSONTyped(json, ignoreDiscriminator) {
|
|
613
|
+
return json;
|
|
614
|
+
}
|
|
615
|
+
function FeatureEntitlementFromJSON(json) {
|
|
616
|
+
return FeatureEntitlementFromJSONTyped(json, false);
|
|
617
|
+
}
|
|
618
|
+
function FeatureEntitlementFromJSONTyped(json, ignoreDiscriminator) {
|
|
619
|
+
if (json == null) {
|
|
620
|
+
return json;
|
|
621
|
+
}
|
|
622
|
+
return {
|
|
623
|
+
allocation: json["allocation"] == null ? void 0 : json["allocation"],
|
|
624
|
+
creditId: json["credit_id"] == null ? void 0 : json["credit_id"],
|
|
625
|
+
creditRemaining: json["credit_remaining"] == null ? void 0 : json["credit_remaining"],
|
|
626
|
+
creditTotal: json["credit_total"] == null ? void 0 : json["credit_total"],
|
|
627
|
+
creditUsed: json["credit_used"] == null ? void 0 : json["credit_used"],
|
|
628
|
+
eventName: json["event_name"] == null ? void 0 : json["event_name"],
|
|
629
|
+
featureId: json["feature_id"],
|
|
630
|
+
featureKey: json["feature_key"],
|
|
631
|
+
metricPeriod: json["metric_period"] == null ? void 0 : json["metric_period"],
|
|
632
|
+
metricResetAt: json["metric_reset_at"] == null ? void 0 : new Date(json["metric_reset_at"]),
|
|
633
|
+
monthReset: json["month_reset"] == null ? void 0 : json["month_reset"],
|
|
634
|
+
softLimit: json["soft_limit"] == null ? void 0 : json["soft_limit"],
|
|
635
|
+
usage: json["usage"] == null ? void 0 : json["usage"],
|
|
636
|
+
valueType: EntitlementValueTypeFromJSON(json["value_type"])
|
|
637
|
+
};
|
|
638
|
+
}
|
|
609
639
|
function CheckFlagResponseDataFromJSON(json) {
|
|
610
640
|
return CheckFlagResponseDataFromJSONTyped(json, false);
|
|
611
641
|
}
|
|
@@ -615,6 +645,7 @@ function CheckFlagResponseDataFromJSONTyped(json, ignoreDiscriminator) {
|
|
|
615
645
|
}
|
|
616
646
|
return {
|
|
617
647
|
companyId: json["company_id"] == null ? void 0 : json["company_id"],
|
|
648
|
+
entitlement: json["entitlement"] == null ? void 0 : FeatureEntitlementFromJSON(json["entitlement"]),
|
|
618
649
|
error: json["error"] == null ? void 0 : json["error"],
|
|
619
650
|
featureAllocation: json["feature_allocation"] == null ? void 0 : json["feature_allocation"],
|
|
620
651
|
featureUsage: json["feature_usage"] == null ? void 0 : json["feature_usage"],
|
|
@@ -630,26 +661,6 @@ function CheckFlagResponseDataFromJSONTyped(json, ignoreDiscriminator) {
|
|
|
630
661
|
value: json["value"]
|
|
631
662
|
};
|
|
632
663
|
}
|
|
633
|
-
function EventBodyFlagCheckToJSON(json) {
|
|
634
|
-
return EventBodyFlagCheckToJSONTyped(json, false);
|
|
635
|
-
}
|
|
636
|
-
function EventBodyFlagCheckToJSONTyped(value, ignoreDiscriminator = false) {
|
|
637
|
-
if (value == null) {
|
|
638
|
-
return value;
|
|
639
|
-
}
|
|
640
|
-
return {
|
|
641
|
-
company_id: value["companyId"],
|
|
642
|
-
error: value["error"],
|
|
643
|
-
flag_id: value["flagId"],
|
|
644
|
-
flag_key: value["flagKey"],
|
|
645
|
-
reason: value["reason"],
|
|
646
|
-
req_company: value["reqCompany"],
|
|
647
|
-
req_user: value["reqUser"],
|
|
648
|
-
rule_id: value["ruleId"],
|
|
649
|
-
user_id: value["userId"],
|
|
650
|
-
value: value["value"]
|
|
651
|
-
};
|
|
652
|
-
}
|
|
653
664
|
function CheckFlagResponseFromJSON(json) {
|
|
654
665
|
return CheckFlagResponseFromJSONTyped(json, false);
|
|
655
666
|
}
|
|
@@ -662,6 +673,26 @@ function CheckFlagResponseFromJSONTyped(json, ignoreDiscriminator) {
|
|
|
662
673
|
params: json["params"]
|
|
663
674
|
};
|
|
664
675
|
}
|
|
676
|
+
function TrialStatusFromJSON(json) {
|
|
677
|
+
return TrialStatusFromJSONTyped(json, false);
|
|
678
|
+
}
|
|
679
|
+
function TrialStatusFromJSONTyped(json, ignoreDiscriminator) {
|
|
680
|
+
return json;
|
|
681
|
+
}
|
|
682
|
+
function DatastreamCompanyPlanFromJSON(json) {
|
|
683
|
+
return DatastreamCompanyPlanFromJSONTyped(json, false);
|
|
684
|
+
}
|
|
685
|
+
function DatastreamCompanyPlanFromJSONTyped(json, ignoreDiscriminator) {
|
|
686
|
+
if (json == null) {
|
|
687
|
+
return json;
|
|
688
|
+
}
|
|
689
|
+
return {
|
|
690
|
+
id: json["id"],
|
|
691
|
+
name: json["name"],
|
|
692
|
+
trialEndDate: json["trial_end_date"] == null ? void 0 : new Date(json["trial_end_date"]),
|
|
693
|
+
trialStatus: json["trial_status"] == null ? void 0 : TrialStatusFromJSON(json["trial_status"])
|
|
694
|
+
};
|
|
695
|
+
}
|
|
665
696
|
function CheckFlagsResponseDataFromJSON(json) {
|
|
666
697
|
return CheckFlagsResponseDataFromJSONTyped(json, false);
|
|
667
698
|
}
|
|
@@ -670,7 +701,8 @@ function CheckFlagsResponseDataFromJSONTyped(json, ignoreDiscriminator) {
|
|
|
670
701
|
return json;
|
|
671
702
|
}
|
|
672
703
|
return {
|
|
673
|
-
flags: json["flags"].map(CheckFlagResponseDataFromJSON)
|
|
704
|
+
flags: json["flags"].map(CheckFlagResponseDataFromJSON),
|
|
705
|
+
plan: json["plan"] == null ? void 0 : DatastreamCompanyPlanFromJSON(json["plan"])
|
|
674
706
|
};
|
|
675
707
|
}
|
|
676
708
|
function CheckFlagsResponseFromJSON(json) {
|
|
@@ -685,6 +717,26 @@ function CheckFlagsResponseFromJSONTyped(json, ignoreDiscriminator) {
|
|
|
685
717
|
params: json["params"]
|
|
686
718
|
};
|
|
687
719
|
}
|
|
720
|
+
function EventBodyFlagCheckToJSON(json) {
|
|
721
|
+
return EventBodyFlagCheckToJSONTyped(json, false);
|
|
722
|
+
}
|
|
723
|
+
function EventBodyFlagCheckToJSONTyped(value, ignoreDiscriminator = false) {
|
|
724
|
+
if (value == null) {
|
|
725
|
+
return value;
|
|
726
|
+
}
|
|
727
|
+
return {
|
|
728
|
+
company_id: value["companyId"],
|
|
729
|
+
error: value["error"],
|
|
730
|
+
flag_id: value["flagId"],
|
|
731
|
+
flag_key: value["flagKey"],
|
|
732
|
+
reason: value["reason"],
|
|
733
|
+
req_company: value["reqCompany"],
|
|
734
|
+
req_user: value["reqUser"],
|
|
735
|
+
rule_id: value["ruleId"],
|
|
736
|
+
user_id: value["userId"],
|
|
737
|
+
value: value["value"]
|
|
738
|
+
};
|
|
739
|
+
}
|
|
688
740
|
var RuleType = /* @__PURE__ */ ((RuleType2) => {
|
|
689
741
|
RuleType2["GLOBAL_OVERRIDE"] = "global_override";
|
|
690
742
|
RuleType2["COMPANY_OVERRIDE"] = "company_override";
|
|
@@ -705,6 +757,7 @@ var UsagePeriod = /* @__PURE__ */ ((UsagePeriod2) => {
|
|
|
705
757
|
var CheckFlagReturnFromJSON = (json) => {
|
|
706
758
|
const {
|
|
707
759
|
companyId,
|
|
760
|
+
entitlement,
|
|
708
761
|
error,
|
|
709
762
|
featureAllocation,
|
|
710
763
|
featureUsage,
|
|
@@ -722,24 +775,40 @@ var CheckFlagReturnFromJSON = (json) => {
|
|
|
722
775
|
const featureUsageExceeded = !value && // if flag is not false, then we haven't exceeded usage
|
|
723
776
|
(ruleType == "company_override_usage_exceeded" || // if the rule type is one of these, then we have exceeded usage
|
|
724
777
|
ruleType == "plan_entitlement_usage_exceeded");
|
|
778
|
+
const resolvedAllocation = entitlement?.allocation ?? featureAllocation;
|
|
779
|
+
const resolvedUsage = entitlement?.usage ?? featureUsage;
|
|
780
|
+
const resolvedEvent = entitlement?.eventName ?? featureUsageEvent;
|
|
781
|
+
const resolvedPeriod = entitlement?.metricPeriod ?? featureUsagePeriod;
|
|
782
|
+
const resolvedResetAt = entitlement?.metricResetAt ?? featureUsageResetAt;
|
|
725
783
|
return {
|
|
726
784
|
featureUsageExceeded,
|
|
727
785
|
companyId: companyId == null ? void 0 : companyId,
|
|
786
|
+
creditRemaining: entitlement?.creditRemaining == null ? void 0 : entitlement.creditRemaining,
|
|
728
787
|
error: error == null ? void 0 : error,
|
|
729
|
-
featureAllocation:
|
|
730
|
-
featureUsage:
|
|
731
|
-
featureUsageEvent:
|
|
732
|
-
featureUsagePeriod:
|
|
733
|
-
featureUsageResetAt:
|
|
788
|
+
featureAllocation: resolvedAllocation == null ? void 0 : resolvedAllocation,
|
|
789
|
+
featureUsage: resolvedUsage == null ? void 0 : resolvedUsage,
|
|
790
|
+
featureUsageEvent: resolvedEvent == null ? void 0 : resolvedEvent,
|
|
791
|
+
featureUsagePeriod: resolvedPeriod == null ? void 0 : resolvedPeriod,
|
|
792
|
+
featureUsageResetAt: resolvedResetAt == null ? void 0 : resolvedResetAt,
|
|
734
793
|
flag,
|
|
735
794
|
flagId: flagId == null ? void 0 : flagId,
|
|
736
795
|
reason,
|
|
737
796
|
ruleId: ruleId == null ? void 0 : ruleId,
|
|
738
797
|
ruleType: ruleType == null ? void 0 : ruleType,
|
|
798
|
+
softLimit: entitlement?.softLimit == null ? void 0 : entitlement.softLimit,
|
|
739
799
|
userId: userId == null ? void 0 : userId,
|
|
740
800
|
value
|
|
741
801
|
};
|
|
742
802
|
};
|
|
803
|
+
var CheckPlanReturnFromJSON = (json) => {
|
|
804
|
+
const { id, name, trialEndDate, trialStatus } = DatastreamCompanyPlanFromJSON(json);
|
|
805
|
+
return {
|
|
806
|
+
id,
|
|
807
|
+
name,
|
|
808
|
+
trialEndDate: trialEndDate == null ? void 0 : trialEndDate,
|
|
809
|
+
trialStatus: trialStatus == null ? void 0 : trialStatus
|
|
810
|
+
};
|
|
811
|
+
};
|
|
743
812
|
function contextString(context) {
|
|
744
813
|
const sortedContext = Object.keys(context).reduce((acc, key) => {
|
|
745
814
|
const sortedKeys = Object.keys(
|
|
@@ -754,7 +823,7 @@ function contextString(context) {
|
|
|
754
823
|
}, {});
|
|
755
824
|
return JSON.stringify(sortedContext);
|
|
756
825
|
}
|
|
757
|
-
var version = "1.
|
|
826
|
+
var version = "1.4.0";
|
|
758
827
|
var anonymousIdKey = "schematicId";
|
|
759
828
|
var Schematic = class {
|
|
760
829
|
additionalHeaders = {};
|
|
@@ -771,11 +840,38 @@ var Schematic = class {
|
|
|
771
840
|
flagValueListeners = {};
|
|
772
841
|
isPending = true;
|
|
773
842
|
isPendingListeners = /* @__PURE__ */ new Set();
|
|
843
|
+
planListeners = /* @__PURE__ */ new Set();
|
|
774
844
|
storage;
|
|
775
845
|
useWebSocket = false;
|
|
776
846
|
checks = {};
|
|
777
847
|
featureUsageEventMap = {};
|
|
848
|
+
planChecks = {};
|
|
778
849
|
webSocketUrl = "wss://api.schematichq.com";
|
|
850
|
+
webSocketConnectionTimeout = 1e4;
|
|
851
|
+
webSocketReconnect = true;
|
|
852
|
+
webSocketMaxReconnectAttempts = 7;
|
|
853
|
+
// Max attempts after connection disrupted
|
|
854
|
+
webSocketMaxConnectionAttempts = 3;
|
|
855
|
+
// Max attempts for initial connection
|
|
856
|
+
webSocketInitialRetryDelay = 1e3;
|
|
857
|
+
webSocketMaxRetryDelay = 3e4;
|
|
858
|
+
wsReconnectAttempts = 0;
|
|
859
|
+
wsReconnectTimer = null;
|
|
860
|
+
wsIntentionalDisconnect = false;
|
|
861
|
+
currentWebSocket = null;
|
|
862
|
+
isConnecting = false;
|
|
863
|
+
maxEventQueueSize = 100;
|
|
864
|
+
// Prevent memory issues with very long network outages
|
|
865
|
+
maxEventRetries = 5;
|
|
866
|
+
// Maximum retry attempts for failed events
|
|
867
|
+
eventRetryInitialDelay = 1e3;
|
|
868
|
+
// Initial retry delay in ms
|
|
869
|
+
eventRetryMaxDelay = 3e4;
|
|
870
|
+
// Maximum retry delay in ms
|
|
871
|
+
retryTimer = null;
|
|
872
|
+
flagValueDefaults = {};
|
|
873
|
+
flagCheckDefaults = {};
|
|
874
|
+
fallbackCheckCache = {};
|
|
779
875
|
constructor(apiKey, options) {
|
|
780
876
|
this.apiKey = apiKey;
|
|
781
877
|
this.eventQueue = [];
|
|
@@ -807,8 +903,13 @@ var Schematic = class {
|
|
|
807
903
|
};
|
|
808
904
|
if (options?.storage) {
|
|
809
905
|
this.storage = options.storage;
|
|
810
|
-
} else
|
|
811
|
-
|
|
906
|
+
} else {
|
|
907
|
+
try {
|
|
908
|
+
if (typeof localStorage !== "undefined") {
|
|
909
|
+
this.storage = localStorage;
|
|
910
|
+
}
|
|
911
|
+
} catch {
|
|
912
|
+
}
|
|
812
913
|
}
|
|
813
914
|
if (options?.apiUrl !== void 0) {
|
|
814
915
|
this.apiUrl = options.apiUrl;
|
|
@@ -819,11 +920,54 @@ var Schematic = class {
|
|
|
819
920
|
if (options?.webSocketUrl !== void 0) {
|
|
820
921
|
this.webSocketUrl = options.webSocketUrl;
|
|
821
922
|
}
|
|
923
|
+
if (options?.webSocketConnectionTimeout !== void 0) {
|
|
924
|
+
this.webSocketConnectionTimeout = options.webSocketConnectionTimeout;
|
|
925
|
+
}
|
|
926
|
+
if (options?.webSocketReconnect !== void 0) {
|
|
927
|
+
this.webSocketReconnect = options.webSocketReconnect;
|
|
928
|
+
}
|
|
929
|
+
if (options?.webSocketMaxReconnectAttempts !== void 0) {
|
|
930
|
+
this.webSocketMaxReconnectAttempts = options.webSocketMaxReconnectAttempts;
|
|
931
|
+
}
|
|
932
|
+
if (options?.webSocketInitialRetryDelay !== void 0) {
|
|
933
|
+
this.webSocketInitialRetryDelay = options.webSocketInitialRetryDelay;
|
|
934
|
+
}
|
|
935
|
+
if (options?.webSocketMaxRetryDelay !== void 0) {
|
|
936
|
+
this.webSocketMaxRetryDelay = options.webSocketMaxRetryDelay;
|
|
937
|
+
}
|
|
938
|
+
if (options?.maxEventQueueSize !== void 0) {
|
|
939
|
+
this.maxEventQueueSize = options.maxEventQueueSize;
|
|
940
|
+
}
|
|
941
|
+
if (options?.maxEventRetries !== void 0) {
|
|
942
|
+
this.maxEventRetries = options.maxEventRetries;
|
|
943
|
+
}
|
|
944
|
+
if (options?.eventRetryInitialDelay !== void 0) {
|
|
945
|
+
this.eventRetryInitialDelay = options.eventRetryInitialDelay;
|
|
946
|
+
}
|
|
947
|
+
if (options?.eventRetryMaxDelay !== void 0) {
|
|
948
|
+
this.eventRetryMaxDelay = options.eventRetryMaxDelay;
|
|
949
|
+
}
|
|
950
|
+
if (options?.flagValueDefaults !== void 0) {
|
|
951
|
+
this.flagValueDefaults = options.flagValueDefaults;
|
|
952
|
+
}
|
|
953
|
+
if (options?.flagCheckDefaults !== void 0) {
|
|
954
|
+
this.flagCheckDefaults = options.flagCheckDefaults;
|
|
955
|
+
}
|
|
822
956
|
if (typeof window !== "undefined" && window?.addEventListener) {
|
|
823
957
|
window.addEventListener("beforeunload", () => {
|
|
824
958
|
this.flushEventQueue();
|
|
825
959
|
this.flushContextDependentEventQueue();
|
|
826
960
|
});
|
|
961
|
+
if (this.useWebSocket) {
|
|
962
|
+
window.addEventListener("offline", () => {
|
|
963
|
+
this.debug("Browser went offline, closing WebSocket connection");
|
|
964
|
+
this.handleNetworkOffline();
|
|
965
|
+
});
|
|
966
|
+
window.addEventListener("online", () => {
|
|
967
|
+
this.debug("Browser came online, attempting to reconnect WebSocket");
|
|
968
|
+
this.handleNetworkOnline();
|
|
969
|
+
});
|
|
970
|
+
}
|
|
827
971
|
}
|
|
828
972
|
if (this.offlineEnabled) {
|
|
829
973
|
this.debug(
|
|
@@ -833,24 +977,89 @@ var Schematic = class {
|
|
|
833
977
|
this.debug("Initialized with debug mode enabled");
|
|
834
978
|
}
|
|
835
979
|
}
|
|
980
|
+
/**
|
|
981
|
+
* Resolve fallback value according to priority order:
|
|
982
|
+
* 1. Callsite fallback value (if provided)
|
|
983
|
+
* 2. Boolean value from flagCheckDefaults initialization option
|
|
984
|
+
* 3. Boolean value from flagValueDefaults initialization option
|
|
985
|
+
* 4. Default to false
|
|
986
|
+
*/
|
|
987
|
+
resolveFallbackValue(key, callsiteFallback) {
|
|
988
|
+
if (callsiteFallback !== void 0) {
|
|
989
|
+
return callsiteFallback;
|
|
990
|
+
}
|
|
991
|
+
if (key in this.flagCheckDefaults) {
|
|
992
|
+
return this.flagCheckDefaults[key].value;
|
|
993
|
+
}
|
|
994
|
+
if (key in this.flagValueDefaults) {
|
|
995
|
+
return this.flagValueDefaults[key];
|
|
996
|
+
}
|
|
997
|
+
return false;
|
|
998
|
+
}
|
|
999
|
+
/**
|
|
1000
|
+
* Resolve complete CheckFlagReturn object according to priority order:
|
|
1001
|
+
* 1. Use callsite fallback for boolean value, construct CheckFlagReturn
|
|
1002
|
+
* 2. Use flagCheckDefaults if available for this flag
|
|
1003
|
+
* 3. Use flagValueDefaults if available for this flag, construct CheckFlagReturn
|
|
1004
|
+
* 4. Default CheckFlagReturn with value: false
|
|
1005
|
+
*/
|
|
1006
|
+
resolveFallbackCheckFlagReturn(key, callsiteFallback, reason = "Fallback value used", error) {
|
|
1007
|
+
if (callsiteFallback !== void 0) {
|
|
1008
|
+
return {
|
|
1009
|
+
flag: key,
|
|
1010
|
+
value: callsiteFallback,
|
|
1011
|
+
reason,
|
|
1012
|
+
error
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
if (key in this.flagCheckDefaults) {
|
|
1016
|
+
const defaultReturn = this.flagCheckDefaults[key];
|
|
1017
|
+
return {
|
|
1018
|
+
...defaultReturn,
|
|
1019
|
+
flag: key,
|
|
1020
|
+
// Ensure flag matches the requested key
|
|
1021
|
+
reason: error !== void 0 ? reason : defaultReturn.reason,
|
|
1022
|
+
error
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
if (key in this.flagValueDefaults) {
|
|
1026
|
+
return {
|
|
1027
|
+
flag: key,
|
|
1028
|
+
value: this.flagValueDefaults[key],
|
|
1029
|
+
reason,
|
|
1030
|
+
error
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
return {
|
|
1034
|
+
flag: key,
|
|
1035
|
+
value: false,
|
|
1036
|
+
reason,
|
|
1037
|
+
error
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
836
1040
|
/**
|
|
837
1041
|
* Get value for a single flag.
|
|
838
1042
|
* In WebSocket mode, returns cached values if connection is active, otherwise establishes
|
|
839
|
-
* new connection and then returns the
|
|
840
|
-
* connection fails.
|
|
1043
|
+
* new connection and then returns the requested value. Falls back to preconfigured fallback
|
|
1044
|
+
* values if WebSocket connection fails.
|
|
841
1045
|
* In REST mode, makes an API call for each check.
|
|
842
1046
|
*/
|
|
843
1047
|
async checkFlag(options) {
|
|
844
|
-
const { fallback
|
|
1048
|
+
const { fallback, key } = options;
|
|
845
1049
|
const context = options.context || this.context;
|
|
846
1050
|
const contextStr = contextString(context);
|
|
847
1051
|
this.debug(`checkFlag: ${key}`, { context, fallback });
|
|
848
1052
|
if (this.isOffline()) {
|
|
1053
|
+
const resolvedFallbackResult = this.resolveFallbackCheckFlagReturn(
|
|
1054
|
+
key,
|
|
1055
|
+
fallback,
|
|
1056
|
+
"Offline mode - using initialization defaults"
|
|
1057
|
+
);
|
|
849
1058
|
this.debug(`checkFlag offline result: ${key}`, {
|
|
850
|
-
value:
|
|
1059
|
+
value: resolvedFallbackResult.value,
|
|
851
1060
|
offlineMode: true
|
|
852
1061
|
});
|
|
853
|
-
return
|
|
1062
|
+
return resolvedFallbackResult.value;
|
|
854
1063
|
}
|
|
855
1064
|
if (!this.useWebSocket) {
|
|
856
1065
|
const requestUrl = `${this.apiUrl}/flags/${key}/check`;
|
|
@@ -877,15 +1086,15 @@ var Schematic = class {
|
|
|
877
1086
|
this.submitFlagCheckEvent(key, result, context);
|
|
878
1087
|
return result.value;
|
|
879
1088
|
}).catch((error) => {
|
|
880
|
-
console.
|
|
881
|
-
const errorResult =
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
error
|
|
886
|
-
|
|
1089
|
+
console.warn("There was a problem with the fetch operation:", error);
|
|
1090
|
+
const errorResult = this.resolveFallbackCheckFlagReturn(
|
|
1091
|
+
key,
|
|
1092
|
+
fallback,
|
|
1093
|
+
"API request failed",
|
|
1094
|
+
error instanceof Error ? error.message : String(error)
|
|
1095
|
+
);
|
|
887
1096
|
this.submitFlagCheckEvent(key, errorResult, context);
|
|
888
|
-
return
|
|
1097
|
+
return errorResult.value;
|
|
889
1098
|
});
|
|
890
1099
|
}
|
|
891
1100
|
try {
|
|
@@ -895,38 +1104,52 @@ var Schematic = class {
|
|
|
895
1104
|
return existingVals[key].value;
|
|
896
1105
|
}
|
|
897
1106
|
if (this.isOffline()) {
|
|
898
|
-
return fallback;
|
|
1107
|
+
return this.resolveFallbackValue(key, fallback);
|
|
899
1108
|
}
|
|
900
1109
|
try {
|
|
901
1110
|
await this.setContext(context);
|
|
902
1111
|
} catch (error) {
|
|
903
|
-
console.
|
|
904
|
-
"WebSocket connection failed,
|
|
1112
|
+
console.warn(
|
|
1113
|
+
"WebSocket connection failed, using fallback value:",
|
|
905
1114
|
error
|
|
906
1115
|
);
|
|
907
|
-
|
|
1116
|
+
const errorResult = this.resolveFallbackCheckFlagReturn(
|
|
1117
|
+
key,
|
|
1118
|
+
fallback,
|
|
1119
|
+
"WebSocket connection failed",
|
|
1120
|
+
error instanceof Error ? error.message : String(error)
|
|
1121
|
+
);
|
|
1122
|
+
this.submitFlagCheckEvent(key, errorResult, context);
|
|
1123
|
+
return errorResult.value;
|
|
908
1124
|
}
|
|
909
1125
|
const contextVals = this.checks[contextStr] ?? {};
|
|
910
1126
|
const flagCheck = contextVals[key];
|
|
911
|
-
const result = flagCheck?.value ?? fallback;
|
|
1127
|
+
const result = flagCheck?.value ?? this.resolveFallbackValue(key, fallback);
|
|
912
1128
|
this.debug(
|
|
913
1129
|
`checkFlag WebSocket result: ${key}`,
|
|
914
|
-
typeof flagCheck !== "undefined" ? flagCheck : { value:
|
|
1130
|
+
typeof flagCheck !== "undefined" ? flagCheck : { value: result, fallbackUsed: true }
|
|
915
1131
|
);
|
|
916
1132
|
if (typeof flagCheck !== "undefined") {
|
|
917
1133
|
this.submitFlagCheckEvent(key, flagCheck, context);
|
|
1134
|
+
} else {
|
|
1135
|
+
const fallbackResult = this.resolveFallbackCheckFlagReturn(
|
|
1136
|
+
key,
|
|
1137
|
+
fallback,
|
|
1138
|
+
"No flag values available"
|
|
1139
|
+
);
|
|
1140
|
+
this.submitFlagCheckEvent(key, fallbackResult, context);
|
|
918
1141
|
}
|
|
919
1142
|
return result;
|
|
920
1143
|
} catch (error) {
|
|
921
1144
|
console.error("Unexpected error in checkFlag:", error);
|
|
922
|
-
const errorResult =
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
error
|
|
927
|
-
|
|
1145
|
+
const errorResult = this.resolveFallbackCheckFlagReturn(
|
|
1146
|
+
key,
|
|
1147
|
+
fallback,
|
|
1148
|
+
"Unexpected error in flag check",
|
|
1149
|
+
error instanceof Error ? error.message : String(error)
|
|
1150
|
+
);
|
|
928
1151
|
this.submitFlagCheckEvent(key, errorResult, context);
|
|
929
|
-
return
|
|
1152
|
+
return errorResult.value;
|
|
930
1153
|
}
|
|
931
1154
|
}
|
|
932
1155
|
/**
|
|
@@ -938,6 +1161,53 @@ var Schematic = class {
|
|
|
938
1161
|
console.log(`[Schematic] ${message}`, ...args);
|
|
939
1162
|
}
|
|
940
1163
|
}
|
|
1164
|
+
/**
|
|
1165
|
+
* Create a persistent message handler for websocket flag updates
|
|
1166
|
+
*/
|
|
1167
|
+
createPersistentMessageHandler(context) {
|
|
1168
|
+
return (event) => {
|
|
1169
|
+
const message = JSON.parse(event.data);
|
|
1170
|
+
this.debug(`WebSocket persistent message received:`, message);
|
|
1171
|
+
if (!(contextString(context) in this.checks)) {
|
|
1172
|
+
this.checks[contextString(context)] = {};
|
|
1173
|
+
}
|
|
1174
|
+
(message.flags ?? []).forEach((flag) => {
|
|
1175
|
+
const flagCheck = CheckFlagReturnFromJSON(flag);
|
|
1176
|
+
const contextStr = contextString(context);
|
|
1177
|
+
if (this.checks[contextStr] === void 0) {
|
|
1178
|
+
this.checks[contextStr] = {};
|
|
1179
|
+
}
|
|
1180
|
+
this.checks[contextStr][flagCheck.flag] = flagCheck;
|
|
1181
|
+
if (typeof flagCheck.featureUsageEvent === "string") {
|
|
1182
|
+
this.updateFeatureUsageEventMap(flagCheck);
|
|
1183
|
+
}
|
|
1184
|
+
if ((this.flagCheckListeners[flag.flag]?.size ?? 0) > 0 || (this.flagValueListeners[flag.flag]?.size ?? 0) > 0) {
|
|
1185
|
+
this.submitFlagCheckEvent(flagCheck.flag, flagCheck, context);
|
|
1186
|
+
}
|
|
1187
|
+
this.debug(
|
|
1188
|
+
`WebSocket flag update received. Notifying listeners for ${flag.flag}`,
|
|
1189
|
+
{
|
|
1190
|
+
flag: flag.flag,
|
|
1191
|
+
value: flagCheck.value,
|
|
1192
|
+
flagCheck
|
|
1193
|
+
}
|
|
1194
|
+
);
|
|
1195
|
+
this.notifyFlagCheckListeners(flag.flag, flagCheck);
|
|
1196
|
+
this.notifyFlagValueListeners(flag.flag, flagCheck.value);
|
|
1197
|
+
});
|
|
1198
|
+
if (message.plan !== void 0 && message.plan !== null) {
|
|
1199
|
+
const plan = CheckPlanReturnFromJSON(message.plan);
|
|
1200
|
+
const contextStr = contextString(context);
|
|
1201
|
+
this.planChecks[contextStr] = plan;
|
|
1202
|
+
this.debug(`WebSocket plan update received. Notifying listeners`, {
|
|
1203
|
+
plan
|
|
1204
|
+
});
|
|
1205
|
+
this.notifyPlanListeners(plan);
|
|
1206
|
+
}
|
|
1207
|
+
this.flushContextDependentEventQueue();
|
|
1208
|
+
this.setIsPending(false);
|
|
1209
|
+
};
|
|
1210
|
+
}
|
|
941
1211
|
/**
|
|
942
1212
|
* Helper function to check if client is in offline mode
|
|
943
1213
|
*/
|
|
@@ -964,52 +1234,6 @@ var Schematic = class {
|
|
|
964
1234
|
this.debug(`submitting flag check event:`, eventBody);
|
|
965
1235
|
return this.handleEvent("flag_check", EventBodyFlagCheckToJSON(eventBody));
|
|
966
1236
|
}
|
|
967
|
-
/**
|
|
968
|
-
* Helper method for falling back to REST API when WebSocket connection fails
|
|
969
|
-
*/
|
|
970
|
-
async fallbackToRest(key, context, fallback) {
|
|
971
|
-
if (this.isOffline()) {
|
|
972
|
-
this.debug(`fallbackToRest offline result: ${key}`, {
|
|
973
|
-
value: fallback,
|
|
974
|
-
offlineMode: true
|
|
975
|
-
});
|
|
976
|
-
return fallback;
|
|
977
|
-
}
|
|
978
|
-
try {
|
|
979
|
-
const requestUrl = `${this.apiUrl}/flags/${key}/check`;
|
|
980
|
-
const response = await fetch(requestUrl, {
|
|
981
|
-
method: "POST",
|
|
982
|
-
headers: {
|
|
983
|
-
...this.additionalHeaders ?? {},
|
|
984
|
-
"Content-Type": "application/json;charset=UTF-8",
|
|
985
|
-
"X-Schematic-Api-Key": this.apiKey
|
|
986
|
-
},
|
|
987
|
-
body: JSON.stringify(context)
|
|
988
|
-
});
|
|
989
|
-
if (!response.ok) {
|
|
990
|
-
throw new Error("Network response was not ok");
|
|
991
|
-
}
|
|
992
|
-
const responseJson = await response.json();
|
|
993
|
-
const data = CheckFlagResponseFromJSON(responseJson);
|
|
994
|
-
this.debug(`fallbackToRest result: ${key}`, data);
|
|
995
|
-
const result = CheckFlagReturnFromJSON(data.data);
|
|
996
|
-
if (typeof result.featureUsageEvent === "string") {
|
|
997
|
-
this.updateFeatureUsageEventMap(result);
|
|
998
|
-
}
|
|
999
|
-
this.submitFlagCheckEvent(key, result, context);
|
|
1000
|
-
return result.value;
|
|
1001
|
-
} catch (error) {
|
|
1002
|
-
console.error("REST API call failed, using fallback value:", error);
|
|
1003
|
-
const errorResult = {
|
|
1004
|
-
flag: key,
|
|
1005
|
-
value: fallback,
|
|
1006
|
-
reason: "API request failed (fallback)",
|
|
1007
|
-
error: error instanceof Error ? error.message : String(error)
|
|
1008
|
-
};
|
|
1009
|
-
this.submitFlagCheckEvent(key, errorResult, context);
|
|
1010
|
-
return fallback;
|
|
1011
|
-
}
|
|
1012
|
-
}
|
|
1013
1237
|
/**
|
|
1014
1238
|
* Make an API call to fetch all flag values for a given context.
|
|
1015
1239
|
* Recommended for use in REST mode only.
|
|
@@ -1048,7 +1272,7 @@ var Schematic = class {
|
|
|
1048
1272
|
{}
|
|
1049
1273
|
);
|
|
1050
1274
|
}).catch((error) => {
|
|
1051
|
-
console.
|
|
1275
|
+
console.warn("There was a problem with the fetch operation:", error);
|
|
1052
1276
|
return {};
|
|
1053
1277
|
});
|
|
1054
1278
|
};
|
|
@@ -1059,14 +1283,12 @@ var Schematic = class {
|
|
|
1059
1283
|
*/
|
|
1060
1284
|
identify = (body) => {
|
|
1061
1285
|
this.debug(`identify:`, body);
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
}
|
|
1068
|
-
console.error("Error setting context:", error);
|
|
1069
|
-
}
|
|
1286
|
+
this.setContext({
|
|
1287
|
+
company: body.company?.keys,
|
|
1288
|
+
user: body.keys
|
|
1289
|
+
}).catch((error) => {
|
|
1290
|
+
console.warn("Error setting context:", error);
|
|
1291
|
+
});
|
|
1070
1292
|
return this.handleEvent("identify", body);
|
|
1071
1293
|
};
|
|
1072
1294
|
/**
|
|
@@ -1085,16 +1307,52 @@ var Schematic = class {
|
|
|
1085
1307
|
this.setIsPending(false);
|
|
1086
1308
|
return Promise.resolve();
|
|
1087
1309
|
}
|
|
1310
|
+
if (contextString(context) === contextString(this.context) && this.conn !== null && !this.isPending) {
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1088
1313
|
try {
|
|
1089
1314
|
this.setIsPending(true);
|
|
1090
1315
|
if (!this.conn) {
|
|
1091
|
-
|
|
1316
|
+
if (this.isConnecting) {
|
|
1317
|
+
this.debug(
|
|
1318
|
+
`Connection already in progress, waiting for it to complete`
|
|
1319
|
+
);
|
|
1320
|
+
while (this.isConnecting && this.conn === null) {
|
|
1321
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1322
|
+
}
|
|
1323
|
+
if (this.conn !== null) {
|
|
1324
|
+
const socket2 = await this.conn;
|
|
1325
|
+
await this.wsSendMessage(socket2, context);
|
|
1326
|
+
return;
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
if (this.wsReconnectTimer !== null) {
|
|
1330
|
+
this.debug(
|
|
1331
|
+
`Cancelling scheduled reconnection, connecting immediately`
|
|
1332
|
+
);
|
|
1333
|
+
clearTimeout(this.wsReconnectTimer);
|
|
1334
|
+
this.wsReconnectTimer = null;
|
|
1335
|
+
}
|
|
1336
|
+
this.isConnecting = true;
|
|
1337
|
+
try {
|
|
1338
|
+
this.conn = this.wsConnect();
|
|
1339
|
+
const socket2 = await this.conn;
|
|
1340
|
+
this.isConnecting = false;
|
|
1341
|
+
await this.wsSendMessage(socket2, context);
|
|
1342
|
+
return;
|
|
1343
|
+
} catch (error) {
|
|
1344
|
+
this.isConnecting = false;
|
|
1345
|
+
throw error;
|
|
1346
|
+
}
|
|
1092
1347
|
}
|
|
1093
1348
|
const socket = await this.conn;
|
|
1094
1349
|
await this.wsSendMessage(socket, context);
|
|
1095
1350
|
} catch (error) {
|
|
1096
|
-
console.
|
|
1097
|
-
|
|
1351
|
+
console.warn("Failed to establish WebSocket connection:", error);
|
|
1352
|
+
this.context = context;
|
|
1353
|
+
this.flushContextDependentEventQueue();
|
|
1354
|
+
this.setIsPending(false);
|
|
1355
|
+
this.attemptReconnect();
|
|
1098
1356
|
}
|
|
1099
1357
|
};
|
|
1100
1358
|
/**
|
|
@@ -1217,11 +1475,57 @@ var Schematic = class {
|
|
|
1217
1475
|
}
|
|
1218
1476
|
}
|
|
1219
1477
|
};
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1478
|
+
startRetryTimer = () => {
|
|
1479
|
+
if (this.retryTimer !== null) {
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
1482
|
+
this.retryTimer = setInterval(() => {
|
|
1483
|
+
this.flushEventQueue().catch((error) => {
|
|
1484
|
+
this.debug("Error in retry timer flush:", error);
|
|
1485
|
+
});
|
|
1486
|
+
if (this.eventQueue.length === 0) {
|
|
1487
|
+
this.stopRetryTimer();
|
|
1488
|
+
}
|
|
1489
|
+
}, 5e3);
|
|
1490
|
+
this.debug("Started retry timer");
|
|
1491
|
+
};
|
|
1492
|
+
stopRetryTimer = () => {
|
|
1493
|
+
if (this.retryTimer !== null) {
|
|
1494
|
+
clearInterval(this.retryTimer);
|
|
1495
|
+
this.retryTimer = null;
|
|
1496
|
+
this.debug("Stopped retry timer");
|
|
1497
|
+
}
|
|
1498
|
+
};
|
|
1499
|
+
flushEventQueue = async () => {
|
|
1500
|
+
if (this.eventQueue.length === 0) {
|
|
1501
|
+
return;
|
|
1502
|
+
}
|
|
1503
|
+
const now = Date.now();
|
|
1504
|
+
const readyEvents = [];
|
|
1505
|
+
const notReadyEvents = [];
|
|
1506
|
+
for (const event of this.eventQueue) {
|
|
1507
|
+
if (event.next_retry_at === void 0 || event.next_retry_at <= now) {
|
|
1508
|
+
readyEvents.push(event);
|
|
1509
|
+
} else {
|
|
1510
|
+
notReadyEvents.push(event);
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
if (readyEvents.length === 0) {
|
|
1514
|
+
this.debug(
|
|
1515
|
+
`No events ready for retry yet (${notReadyEvents.length} still in backoff)`
|
|
1516
|
+
);
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
this.debug(
|
|
1520
|
+
`Flushing event queue: ${readyEvents.length} ready, ${notReadyEvents.length} waiting`
|
|
1521
|
+
);
|
|
1522
|
+
this.eventQueue = notReadyEvents;
|
|
1523
|
+
for (const event of readyEvents) {
|
|
1524
|
+
try {
|
|
1525
|
+
await this.sendEvent(event);
|
|
1526
|
+
this.debug(`Queued event sent successfully:`, event.type);
|
|
1527
|
+
} catch (error) {
|
|
1528
|
+
this.debug(`Failed to send queued event:`, error);
|
|
1225
1529
|
}
|
|
1226
1530
|
}
|
|
1227
1531
|
};
|
|
@@ -1269,12 +1573,47 @@ var Schematic = class {
|
|
|
1269
1573
|
},
|
|
1270
1574
|
body: payload
|
|
1271
1575
|
});
|
|
1576
|
+
if (!response.ok) {
|
|
1577
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
1578
|
+
}
|
|
1272
1579
|
this.debug(`event sent:`, {
|
|
1273
1580
|
status: response.status,
|
|
1274
1581
|
statusText: response.statusText
|
|
1275
1582
|
});
|
|
1276
1583
|
} catch (error) {
|
|
1277
|
-
|
|
1584
|
+
const retryCount = (event.retry_count ?? 0) + 1;
|
|
1585
|
+
if (retryCount <= this.maxEventRetries) {
|
|
1586
|
+
this.debug(
|
|
1587
|
+
`Event failed to send (attempt ${retryCount}/${this.maxEventRetries}), queueing for retry:`,
|
|
1588
|
+
error
|
|
1589
|
+
);
|
|
1590
|
+
const baseDelay = this.eventRetryInitialDelay * Math.pow(2, retryCount - 1);
|
|
1591
|
+
const jitterDelay = Math.min(baseDelay, this.eventRetryMaxDelay);
|
|
1592
|
+
const nextRetryAt = Date.now() + jitterDelay;
|
|
1593
|
+
const retryEvent = {
|
|
1594
|
+
...event,
|
|
1595
|
+
retry_count: retryCount,
|
|
1596
|
+
next_retry_at: nextRetryAt
|
|
1597
|
+
};
|
|
1598
|
+
if (this.eventQueue.length < this.maxEventQueueSize) {
|
|
1599
|
+
this.eventQueue.push(retryEvent);
|
|
1600
|
+
this.debug(
|
|
1601
|
+
`Event queued for retry in ${jitterDelay}ms (${this.eventQueue.length}/${this.maxEventQueueSize})`
|
|
1602
|
+
);
|
|
1603
|
+
} else {
|
|
1604
|
+
this.debug(
|
|
1605
|
+
`Event queue full (${this.maxEventQueueSize}), dropping oldest event`
|
|
1606
|
+
);
|
|
1607
|
+
this.eventQueue.shift();
|
|
1608
|
+
this.eventQueue.push(retryEvent);
|
|
1609
|
+
}
|
|
1610
|
+
this.startRetryTimer();
|
|
1611
|
+
} else {
|
|
1612
|
+
this.debug(
|
|
1613
|
+
`Event failed permanently after ${this.maxEventRetries} attempts, dropping:`,
|
|
1614
|
+
error
|
|
1615
|
+
);
|
|
1616
|
+
}
|
|
1278
1617
|
}
|
|
1279
1618
|
return Promise.resolve();
|
|
1280
1619
|
};
|
|
@@ -1285,6 +1624,130 @@ var Schematic = class {
|
|
|
1285
1624
|
/**
|
|
1286
1625
|
* Websocket management
|
|
1287
1626
|
*/
|
|
1627
|
+
/**
|
|
1628
|
+
* Force an immediate WebSocket reconnection.
|
|
1629
|
+
* This is useful when the application returns from a background state (e.g., mobile app
|
|
1630
|
+
* coming back to foreground) and wants to immediately re-establish the connection
|
|
1631
|
+
* rather than waiting for the exponential backoff timer.
|
|
1632
|
+
*
|
|
1633
|
+
* This method will:
|
|
1634
|
+
* - Cancel any pending reconnection timer
|
|
1635
|
+
* - Reset the reconnection attempt counter
|
|
1636
|
+
* - Close any existing connection
|
|
1637
|
+
* - Immediately attempt to reconnect
|
|
1638
|
+
* - Re-send the current context to get fresh flag values
|
|
1639
|
+
*
|
|
1640
|
+
* Use this when you need guaranteed fresh values (e.g., after an in-app purchase).
|
|
1641
|
+
*
|
|
1642
|
+
* @example
|
|
1643
|
+
* ```typescript
|
|
1644
|
+
* // React Native example: reconnect when app comes to foreground
|
|
1645
|
+
* useEffect(() => {
|
|
1646
|
+
* const subscription = AppState.addEventListener("change", (state) => {
|
|
1647
|
+
* if (state === "active") {
|
|
1648
|
+
* client.forceReconnect();
|
|
1649
|
+
* }
|
|
1650
|
+
* });
|
|
1651
|
+
* return () => subscription.remove();
|
|
1652
|
+
* }, [client]);
|
|
1653
|
+
* ```
|
|
1654
|
+
*/
|
|
1655
|
+
forceReconnect = async () => {
|
|
1656
|
+
return this.reconnect({ force: true });
|
|
1657
|
+
};
|
|
1658
|
+
/**
|
|
1659
|
+
* Reconnect the WebSocket connection only if the current connection is unhealthy.
|
|
1660
|
+
* This is useful when the application returns from a background state and wants to
|
|
1661
|
+
* ensure a healthy connection exists, but doesn't need to force a reconnection if
|
|
1662
|
+
* the connection is still active.
|
|
1663
|
+
*
|
|
1664
|
+
* This method will:
|
|
1665
|
+
* - Check if an existing connection is healthy (readyState === OPEN)
|
|
1666
|
+
* - If healthy, return immediately without reconnecting
|
|
1667
|
+
* - If unhealthy, perform the same reconnection logic as forceReconnect()
|
|
1668
|
+
*
|
|
1669
|
+
* Use this when you want efficient reconnection that avoids unnecessary disconnects.
|
|
1670
|
+
*
|
|
1671
|
+
* @example
|
|
1672
|
+
* ```typescript
|
|
1673
|
+
* // React Native example: reconnect only if needed when app comes to foreground
|
|
1674
|
+
* useEffect(() => {
|
|
1675
|
+
* const subscription = AppState.addEventListener("change", (state) => {
|
|
1676
|
+
* if (state === "active") {
|
|
1677
|
+
* client.reconnectIfNeeded();
|
|
1678
|
+
* }
|
|
1679
|
+
* });
|
|
1680
|
+
* return () => subscription.remove();
|
|
1681
|
+
* }, [client]);
|
|
1682
|
+
* ```
|
|
1683
|
+
*/
|
|
1684
|
+
reconnectIfNeeded = async () => {
|
|
1685
|
+
return this.reconnect({ force: false });
|
|
1686
|
+
};
|
|
1687
|
+
/**
|
|
1688
|
+
* Internal method to handle reconnection logic for both forceReconnect and reconnectIfNeeded.
|
|
1689
|
+
*/
|
|
1690
|
+
reconnect = async (options) => {
|
|
1691
|
+
const { force } = options;
|
|
1692
|
+
const methodName = force ? "forceReconnect" : "reconnectIfNeeded";
|
|
1693
|
+
if (this.isOffline()) {
|
|
1694
|
+
this.debug(`${methodName}: skipped (offline mode)`);
|
|
1695
|
+
return Promise.resolve();
|
|
1696
|
+
}
|
|
1697
|
+
if (!force && this.conn !== null) {
|
|
1698
|
+
try {
|
|
1699
|
+
const existingSocket = await this.conn;
|
|
1700
|
+
if (existingSocket.readyState === WebSocket.OPEN) {
|
|
1701
|
+
this.debug(`${methodName}: connection is healthy, skipping`);
|
|
1702
|
+
return Promise.resolve();
|
|
1703
|
+
}
|
|
1704
|
+
} catch {
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
this.debug(
|
|
1708
|
+
`${methodName}: ${force ? "forcing immediate reconnection" : "reconnecting"}`
|
|
1709
|
+
);
|
|
1710
|
+
this.wsIntentionalDisconnect = false;
|
|
1711
|
+
if (this.wsReconnectTimer !== null) {
|
|
1712
|
+
this.debug(`${methodName}: cancelling pending reconnection timer`);
|
|
1713
|
+
clearTimeout(this.wsReconnectTimer);
|
|
1714
|
+
this.wsReconnectTimer = null;
|
|
1715
|
+
}
|
|
1716
|
+
this.wsReconnectAttempts = 0;
|
|
1717
|
+
if (this.conn !== null) {
|
|
1718
|
+
this.debug(`${methodName}: closing existing connection`);
|
|
1719
|
+
try {
|
|
1720
|
+
const existingSocket = await this.conn;
|
|
1721
|
+
if (this.currentWebSocket === existingSocket) {
|
|
1722
|
+
this.currentWebSocket = null;
|
|
1723
|
+
}
|
|
1724
|
+
if (existingSocket.readyState === WebSocket.OPEN || existingSocket.readyState === WebSocket.CONNECTING) {
|
|
1725
|
+
existingSocket.close();
|
|
1726
|
+
}
|
|
1727
|
+
} catch (error) {
|
|
1728
|
+
this.debug(`${methodName}: error closing existing connection:`, error);
|
|
1729
|
+
}
|
|
1730
|
+
this.conn = null;
|
|
1731
|
+
this.isConnecting = false;
|
|
1732
|
+
}
|
|
1733
|
+
if (this.context.company !== void 0 || this.context.user !== void 0) {
|
|
1734
|
+
this.debug(`${methodName}: reconnecting with existing context`);
|
|
1735
|
+
try {
|
|
1736
|
+
this.isConnecting = true;
|
|
1737
|
+
this.conn = this.wsConnect();
|
|
1738
|
+
const socket = await this.conn;
|
|
1739
|
+
this.isConnecting = false;
|
|
1740
|
+
await this.wsSendMessage(socket, this.context, true);
|
|
1741
|
+
this.debug(`${methodName}: reconnection successful`);
|
|
1742
|
+
} catch (error) {
|
|
1743
|
+
this.isConnecting = false;
|
|
1744
|
+
this.debug(`${methodName}: reconnection failed:`, error);
|
|
1745
|
+
}
|
|
1746
|
+
} else {
|
|
1747
|
+
this.debug(`${methodName}: no context set, skipping reconnection`);
|
|
1748
|
+
}
|
|
1749
|
+
return Promise.resolve();
|
|
1750
|
+
};
|
|
1288
1751
|
/**
|
|
1289
1752
|
* If using websocket mode, close the connection when done.
|
|
1290
1753
|
* In offline mode, this is a no-op.
|
|
@@ -1294,95 +1757,307 @@ var Schematic = class {
|
|
|
1294
1757
|
this.debug("cleanup: skipped (offline mode)");
|
|
1295
1758
|
return Promise.resolve();
|
|
1296
1759
|
}
|
|
1760
|
+
this.wsIntentionalDisconnect = true;
|
|
1761
|
+
if (this.wsReconnectTimer !== null) {
|
|
1762
|
+
clearTimeout(this.wsReconnectTimer);
|
|
1763
|
+
this.wsReconnectTimer = null;
|
|
1764
|
+
}
|
|
1765
|
+
this.stopRetryTimer();
|
|
1297
1766
|
if (this.conn) {
|
|
1298
1767
|
try {
|
|
1299
1768
|
const socket = await this.conn;
|
|
1769
|
+
if (this.currentWebSocket === socket) {
|
|
1770
|
+
this.debug(`Cleaning up current websocket tracking`);
|
|
1771
|
+
this.currentWebSocket = null;
|
|
1772
|
+
}
|
|
1300
1773
|
socket.close();
|
|
1301
1774
|
} catch (error) {
|
|
1302
|
-
console.
|
|
1775
|
+
console.warn("Error during cleanup:", error);
|
|
1303
1776
|
} finally {
|
|
1304
1777
|
this.conn = null;
|
|
1778
|
+
this.currentWebSocket = null;
|
|
1779
|
+
this.isConnecting = false;
|
|
1305
1780
|
}
|
|
1306
1781
|
}
|
|
1307
1782
|
};
|
|
1308
|
-
|
|
1309
|
-
|
|
1783
|
+
/**
|
|
1784
|
+
* Calculate the delay for the next reconnection attempt using exponential backoff with jitter.
|
|
1785
|
+
* This helps prevent dogpiling when the server recovers from an outage.
|
|
1786
|
+
*/
|
|
1787
|
+
calculateReconnectDelay = () => {
|
|
1788
|
+
const exponentialDelay = this.webSocketInitialRetryDelay * Math.pow(2, this.wsReconnectAttempts);
|
|
1789
|
+
const cappedDelay = Math.min(exponentialDelay, this.webSocketMaxRetryDelay);
|
|
1790
|
+
const jitter = Math.random() * cappedDelay * 0.5;
|
|
1791
|
+
const totalDelay = cappedDelay + jitter;
|
|
1792
|
+
this.debug(
|
|
1793
|
+
`Reconnect delay calculated: ${totalDelay.toFixed(0)}ms (attempt ${this.wsReconnectAttempts + 1}/${this.webSocketMaxReconnectAttempts})`
|
|
1794
|
+
);
|
|
1795
|
+
return totalDelay;
|
|
1796
|
+
};
|
|
1797
|
+
/**
|
|
1798
|
+
* Handle browser going offline
|
|
1799
|
+
*/
|
|
1800
|
+
handleNetworkOffline = async () => {
|
|
1801
|
+
if (this.conn !== null) {
|
|
1802
|
+
try {
|
|
1803
|
+
const socket = await this.conn;
|
|
1804
|
+
if (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING) {
|
|
1805
|
+
socket.close();
|
|
1806
|
+
}
|
|
1807
|
+
} catch (error) {
|
|
1808
|
+
this.debug("Error closing connection on offline:", error);
|
|
1809
|
+
}
|
|
1810
|
+
this.conn = null;
|
|
1811
|
+
}
|
|
1812
|
+
if (this.wsReconnectTimer !== null) {
|
|
1813
|
+
clearTimeout(this.wsReconnectTimer);
|
|
1814
|
+
this.wsReconnectTimer = null;
|
|
1815
|
+
}
|
|
1816
|
+
};
|
|
1817
|
+
/**
|
|
1818
|
+
* Handle browser coming back online
|
|
1819
|
+
*/
|
|
1820
|
+
handleNetworkOnline = () => {
|
|
1821
|
+
this.debug(
|
|
1822
|
+
"Network online, attempting reconnection and flushing queued events"
|
|
1823
|
+
);
|
|
1824
|
+
this.wsReconnectAttempts = 0;
|
|
1825
|
+
if (this.wsReconnectTimer !== null) {
|
|
1826
|
+
clearTimeout(this.wsReconnectTimer);
|
|
1827
|
+
this.wsReconnectTimer = null;
|
|
1828
|
+
}
|
|
1829
|
+
this.flushEventQueue().catch((error) => {
|
|
1830
|
+
this.debug("Error flushing event queue on network online:", error);
|
|
1831
|
+
});
|
|
1832
|
+
this.attemptReconnect();
|
|
1833
|
+
};
|
|
1834
|
+
/**
|
|
1835
|
+
* Attempt to reconnect the WebSocket connection with exponential backoff.
|
|
1836
|
+
* Called automatically when the connection closes unexpectedly.
|
|
1837
|
+
*/
|
|
1838
|
+
attemptReconnect = () => {
|
|
1839
|
+
if (this.wsReconnectAttempts >= this.webSocketMaxReconnectAttempts) {
|
|
1840
|
+
this.debug(
|
|
1841
|
+
`Maximum reconnection attempts (${this.webSocketMaxReconnectAttempts}) reached, giving up`
|
|
1842
|
+
);
|
|
1843
|
+
return;
|
|
1844
|
+
}
|
|
1845
|
+
if (this.wsReconnectTimer !== null) {
|
|
1846
|
+
this.debug(
|
|
1847
|
+
`Reconnection attempt already scheduled, ignoring duplicate request`
|
|
1848
|
+
);
|
|
1849
|
+
return;
|
|
1850
|
+
}
|
|
1851
|
+
const delay = this.calculateReconnectDelay();
|
|
1852
|
+
this.debug(
|
|
1853
|
+
`Scheduling reconnection attempt ${this.wsReconnectAttempts + 1}/${this.webSocketMaxReconnectAttempts} in ${delay.toFixed(0)}ms`
|
|
1854
|
+
);
|
|
1855
|
+
this.wsReconnectTimer = setTimeout(async () => {
|
|
1856
|
+
this.wsReconnectTimer = null;
|
|
1857
|
+
this.wsReconnectAttempts++;
|
|
1858
|
+
this.debug(
|
|
1859
|
+
`Attempting to reconnect (attempt ${this.wsReconnectAttempts}/${this.webSocketMaxReconnectAttempts})`
|
|
1860
|
+
);
|
|
1861
|
+
try {
|
|
1862
|
+
if (this.conn !== null) {
|
|
1863
|
+
this.debug(`Cleaning up existing connection before reconnection`);
|
|
1864
|
+
try {
|
|
1865
|
+
const existingSocket = await this.conn;
|
|
1866
|
+
if (this.currentWebSocket === existingSocket) {
|
|
1867
|
+
this.debug(`Existing websocket is current, will be replaced`);
|
|
1868
|
+
this.currentWebSocket = null;
|
|
1869
|
+
}
|
|
1870
|
+
if (existingSocket.readyState === WebSocket.OPEN || existingSocket.readyState === WebSocket.CONNECTING) {
|
|
1871
|
+
existingSocket.close();
|
|
1872
|
+
}
|
|
1873
|
+
} catch (error) {
|
|
1874
|
+
this.debug(`Error cleaning up existing connection:`, error);
|
|
1875
|
+
}
|
|
1876
|
+
this.conn = null;
|
|
1877
|
+
this.currentWebSocket = null;
|
|
1878
|
+
this.isConnecting = false;
|
|
1879
|
+
}
|
|
1880
|
+
this.isConnecting = true;
|
|
1881
|
+
try {
|
|
1882
|
+
this.conn = this.wsConnect();
|
|
1883
|
+
const socket = await this.conn;
|
|
1884
|
+
this.isConnecting = false;
|
|
1885
|
+
this.debug(`Reconnection context check:`, {
|
|
1886
|
+
hasCompany: this.context.company !== void 0,
|
|
1887
|
+
hasUser: this.context.user !== void 0,
|
|
1888
|
+
context: this.context
|
|
1889
|
+
});
|
|
1890
|
+
if (this.context.company !== void 0 || this.context.user !== void 0) {
|
|
1891
|
+
this.debug(`Reconnected, force re-sending context`);
|
|
1892
|
+
await this.wsSendMessage(socket, this.context, true);
|
|
1893
|
+
} else {
|
|
1894
|
+
this.debug(
|
|
1895
|
+
`No context to re-send after reconnection - websocket ready for new context`
|
|
1896
|
+
);
|
|
1897
|
+
this.debug(
|
|
1898
|
+
`Setting up tracking for reconnected websocket (no context to send)`
|
|
1899
|
+
);
|
|
1900
|
+
this.currentWebSocket = socket;
|
|
1901
|
+
}
|
|
1902
|
+
this.flushEventQueue().catch((error) => {
|
|
1903
|
+
this.debug(
|
|
1904
|
+
"Error flushing event queue after websocket reconnection:",
|
|
1905
|
+
error
|
|
1906
|
+
);
|
|
1907
|
+
});
|
|
1908
|
+
this.debug(`Reconnection successful`);
|
|
1909
|
+
} catch (error) {
|
|
1910
|
+
this.isConnecting = false;
|
|
1911
|
+
throw error;
|
|
1912
|
+
}
|
|
1913
|
+
} catch (error) {
|
|
1914
|
+
this.debug(`Reconnection attempt failed:`, error);
|
|
1915
|
+
}
|
|
1916
|
+
}, delay);
|
|
1917
|
+
};
|
|
1918
|
+
// Open a websocket connection with retry logic for timeouts
|
|
1919
|
+
wsConnect = async () => {
|
|
1310
1920
|
if (this.isOffline()) {
|
|
1311
1921
|
this.debug("wsConnect: skipped (offline mode)");
|
|
1312
|
-
|
|
1313
|
-
new Error("WebSocket connection skipped in offline mode")
|
|
1314
|
-
);
|
|
1922
|
+
throw new Error("WebSocket connection skipped in offline mode");
|
|
1315
1923
|
}
|
|
1924
|
+
let lastError = null;
|
|
1925
|
+
const maxAttempts = this.webSocketMaxConnectionAttempts;
|
|
1926
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
1927
|
+
try {
|
|
1928
|
+
const socket = await this.wsConnectOnce();
|
|
1929
|
+
this.wsReconnectAttempts = 0;
|
|
1930
|
+
return socket;
|
|
1931
|
+
} catch (error) {
|
|
1932
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
1933
|
+
const isTimeout = lastError.message === "WebSocket connection timeout";
|
|
1934
|
+
if (!isTimeout) {
|
|
1935
|
+
this.debug(
|
|
1936
|
+
`WebSocket connection failed with non-timeout error, not retrying:`,
|
|
1937
|
+
lastError.message
|
|
1938
|
+
);
|
|
1939
|
+
throw lastError;
|
|
1940
|
+
}
|
|
1941
|
+
if (attempt < maxAttempts - 1) {
|
|
1942
|
+
const baseDelay = this.webSocketInitialRetryDelay * Math.pow(2, attempt);
|
|
1943
|
+
const cappedDelay = Math.min(baseDelay, this.webSocketMaxRetryDelay);
|
|
1944
|
+
const jitter = cappedDelay * 0.2 * Math.random();
|
|
1945
|
+
const delay = cappedDelay + jitter;
|
|
1946
|
+
this.debug(
|
|
1947
|
+
`WebSocket connection timeout (attempt ${attempt + 1}/${maxAttempts}), retrying in ${delay.toFixed(0)}ms`
|
|
1948
|
+
);
|
|
1949
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1950
|
+
} else {
|
|
1951
|
+
this.debug(
|
|
1952
|
+
`WebSocket connection timeout (attempt ${attempt + 1}/${maxAttempts}), no more retries`
|
|
1953
|
+
);
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
throw lastError ?? new Error("WebSocket connection failed");
|
|
1958
|
+
};
|
|
1959
|
+
// Single attempt to open a websocket connection (no retry logic)
|
|
1960
|
+
wsConnectOnce = () => {
|
|
1316
1961
|
return new Promise((resolve, reject) => {
|
|
1317
1962
|
const wsUrl = `${this.webSocketUrl}/flags/bootstrap?apiKey=${this.apiKey}`;
|
|
1318
1963
|
this.debug(`connecting to WebSocket:`, wsUrl);
|
|
1319
1964
|
const webSocket = new WebSocket(wsUrl);
|
|
1965
|
+
const connectionId = Math.random().toString(36).substring(7);
|
|
1966
|
+
this.debug(`Creating WebSocket connection ${connectionId} to ${wsUrl}`);
|
|
1967
|
+
let timeoutId = null;
|
|
1968
|
+
let isResolved = false;
|
|
1969
|
+
timeoutId = setTimeout(() => {
|
|
1970
|
+
if (!isResolved) {
|
|
1971
|
+
isResolved = true;
|
|
1972
|
+
this.debug(
|
|
1973
|
+
`WebSocket connection timeout after ${this.webSocketConnectionTimeout}ms`
|
|
1974
|
+
);
|
|
1975
|
+
webSocket.close();
|
|
1976
|
+
reject(new Error("WebSocket connection timeout"));
|
|
1977
|
+
}
|
|
1978
|
+
}, this.webSocketConnectionTimeout);
|
|
1320
1979
|
webSocket.onopen = () => {
|
|
1321
|
-
|
|
1980
|
+
if (isResolved) return;
|
|
1981
|
+
isResolved = true;
|
|
1982
|
+
if (timeoutId !== null) {
|
|
1983
|
+
clearTimeout(timeoutId);
|
|
1984
|
+
}
|
|
1985
|
+
this.wsIntentionalDisconnect = false;
|
|
1986
|
+
this.debug(`WebSocket connection ${connectionId} opened successfully`);
|
|
1322
1987
|
resolve(webSocket);
|
|
1323
1988
|
};
|
|
1324
1989
|
webSocket.onerror = (error) => {
|
|
1325
|
-
|
|
1326
|
-
|
|
1990
|
+
if (isResolved) return;
|
|
1991
|
+
isResolved = true;
|
|
1992
|
+
if (timeoutId !== null) {
|
|
1993
|
+
clearTimeout(timeoutId);
|
|
1994
|
+
}
|
|
1995
|
+
this.debug(`WebSocket connection ${connectionId} error:`, error);
|
|
1996
|
+
const wrappedError = new Error(
|
|
1997
|
+
"WebSocket connection failed during handshake"
|
|
1998
|
+
);
|
|
1999
|
+
reject(wrappedError);
|
|
1327
2000
|
};
|
|
1328
2001
|
webSocket.onclose = () => {
|
|
1329
|
-
|
|
2002
|
+
if (timeoutId !== null) {
|
|
2003
|
+
clearTimeout(timeoutId);
|
|
2004
|
+
}
|
|
2005
|
+
this.debug(`WebSocket connection ${connectionId} closed`);
|
|
1330
2006
|
this.conn = null;
|
|
2007
|
+
if (this.currentWebSocket === webSocket) {
|
|
2008
|
+
this.currentWebSocket = null;
|
|
2009
|
+
this.isConnecting = false;
|
|
2010
|
+
}
|
|
2011
|
+
if (!isResolved && !this.wsIntentionalDisconnect && this.webSocketReconnect) {
|
|
2012
|
+
this.attemptReconnect();
|
|
2013
|
+
}
|
|
1331
2014
|
};
|
|
1332
2015
|
});
|
|
1333
2016
|
};
|
|
1334
2017
|
// Send a message on the websocket indicating interest in a particular evaluation context
|
|
1335
2018
|
// and wait for the initial set of flag values to be returned
|
|
1336
|
-
wsSendMessage = (socket, context) => {
|
|
2019
|
+
wsSendMessage = (socket, context, forceContextSend = false) => {
|
|
1337
2020
|
if (this.isOffline()) {
|
|
1338
2021
|
this.debug("wsSendMessage: skipped (offline mode)");
|
|
1339
2022
|
this.setIsPending(false);
|
|
1340
2023
|
return Promise.resolve();
|
|
1341
2024
|
}
|
|
1342
2025
|
return new Promise((resolve, reject) => {
|
|
1343
|
-
if (contextString(context) == contextString(this.context)) {
|
|
2026
|
+
if (!forceContextSend && contextString(context) == contextString(this.context)) {
|
|
1344
2027
|
this.debug(`WebSocket context unchanged, skipping update`);
|
|
1345
2028
|
return resolve(this.setIsPending(false));
|
|
1346
2029
|
}
|
|
1347
|
-
this.debug(
|
|
2030
|
+
this.debug(
|
|
2031
|
+
forceContextSend ? `WebSocket force sending context (reconnection):` : `WebSocket context updated:`,
|
|
2032
|
+
context
|
|
2033
|
+
);
|
|
1348
2034
|
this.context = context;
|
|
1349
2035
|
const sendMessage = () => {
|
|
1350
2036
|
let resolved = false;
|
|
2037
|
+
const persistentMessageHandler = this.createPersistentMessageHandler(context);
|
|
1351
2038
|
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);
|
|
2039
|
+
persistentMessageHandler(event);
|
|
1380
2040
|
if (!resolved) {
|
|
1381
2041
|
resolved = true;
|
|
1382
2042
|
resolve();
|
|
1383
2043
|
}
|
|
1384
2044
|
};
|
|
1385
2045
|
socket.addEventListener("message", messageHandler);
|
|
2046
|
+
socket.addEventListener("close", (event) => {
|
|
2047
|
+
if (!resolved) {
|
|
2048
|
+
resolved = true;
|
|
2049
|
+
if (event.code === 4001) {
|
|
2050
|
+
reject(
|
|
2051
|
+
new Error(
|
|
2052
|
+
`Authentication failed: ${event.reason !== "" ? event.reason : "Invalid API key"}`
|
|
2053
|
+
)
|
|
2054
|
+
);
|
|
2055
|
+
} else {
|
|
2056
|
+
reject(new Error("WebSocket connection closed unexpectedly"));
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
});
|
|
2060
|
+
this.currentWebSocket = socket;
|
|
1386
2061
|
const clientVersion = this.additionalHeaders["X-Schematic-Client-Version"] ?? `schematic-js@${version}`;
|
|
1387
2062
|
const messagePayload = {
|
|
1388
2063
|
apiKey: this.apiKey,
|
|
@@ -1418,21 +2093,49 @@ var Schematic = class {
|
|
|
1418
2093
|
};
|
|
1419
2094
|
};
|
|
1420
2095
|
setIsPending = (isPending) => {
|
|
2096
|
+
if (this.isPending === isPending) return;
|
|
1421
2097
|
this.isPending = isPending;
|
|
1422
2098
|
this.isPendingListeners.forEach(
|
|
1423
2099
|
(listener) => notifyPendingListener(listener, isPending)
|
|
1424
2100
|
);
|
|
1425
2101
|
};
|
|
2102
|
+
getPlan = () => {
|
|
2103
|
+
const contextStr = contextString(this.context);
|
|
2104
|
+
const plan = this.planChecks[contextStr];
|
|
2105
|
+
return plan;
|
|
2106
|
+
};
|
|
1426
2107
|
// flag checks state
|
|
1427
2108
|
getFlagCheck = (flagKey) => {
|
|
1428
2109
|
const contextStr = contextString(this.context);
|
|
1429
2110
|
const checks = this.checks[contextStr] ?? {};
|
|
1430
|
-
|
|
2111
|
+
const check = checks[flagKey];
|
|
2112
|
+
if (check !== void 0) {
|
|
2113
|
+
return check;
|
|
2114
|
+
}
|
|
2115
|
+
if (flagKey in this.flagCheckDefaults || flagKey in this.flagValueDefaults) {
|
|
2116
|
+
if (!(flagKey in this.fallbackCheckCache)) {
|
|
2117
|
+
this.fallbackCheckCache[flagKey] = this.resolveFallbackCheckFlagReturn(
|
|
2118
|
+
flagKey,
|
|
2119
|
+
void 0,
|
|
2120
|
+
"Default value used"
|
|
2121
|
+
);
|
|
2122
|
+
}
|
|
2123
|
+
return this.fallbackCheckCache[flagKey];
|
|
2124
|
+
}
|
|
2125
|
+
return void 0;
|
|
1431
2126
|
};
|
|
1432
2127
|
// flagValues state
|
|
1433
2128
|
getFlagValue = (flagKey) => {
|
|
1434
|
-
const
|
|
1435
|
-
|
|
2129
|
+
const contextStr = contextString(this.context);
|
|
2130
|
+
const checks = this.checks[contextStr] ?? {};
|
|
2131
|
+
const check = checks[flagKey];
|
|
2132
|
+
if (check?.value !== void 0) {
|
|
2133
|
+
return check.value;
|
|
2134
|
+
}
|
|
2135
|
+
if (flagKey in this.flagCheckDefaults || flagKey in this.flagValueDefaults) {
|
|
2136
|
+
return this.resolveFallbackValue(flagKey);
|
|
2137
|
+
}
|
|
2138
|
+
return void 0;
|
|
1436
2139
|
};
|
|
1437
2140
|
/** Register an event listener that will be notified with the boolean value for a given flag when this value changes */
|
|
1438
2141
|
addFlagValueListener = (flagKey, listener) => {
|
|
@@ -1454,6 +2157,12 @@ var Schematic = class {
|
|
|
1454
2157
|
this.flagCheckListeners[flagKey].delete(listener);
|
|
1455
2158
|
};
|
|
1456
2159
|
};
|
|
2160
|
+
addPlanListener = (listener) => {
|
|
2161
|
+
this.planListeners.add(listener);
|
|
2162
|
+
return () => {
|
|
2163
|
+
this.planListeners.delete(listener);
|
|
2164
|
+
};
|
|
2165
|
+
};
|
|
1457
2166
|
notifyFlagCheckListeners = (flagKey, check) => {
|
|
1458
2167
|
const listeners = this.flagCheckListeners?.[flagKey] ?? [];
|
|
1459
2168
|
if (listeners.size > 0) {
|
|
@@ -1490,7 +2199,32 @@ var Schematic = class {
|
|
|
1490
2199
|
{ value }
|
|
1491
2200
|
);
|
|
1492
2201
|
}
|
|
1493
|
-
listeners.forEach((listener) =>
|
|
2202
|
+
listeners.forEach((listener, index) => {
|
|
2203
|
+
this.debug(`Calling listener ${index} for flag ${flagKey}`, {
|
|
2204
|
+
flagKey,
|
|
2205
|
+
value
|
|
2206
|
+
});
|
|
2207
|
+
notifyFlagValueListener(listener, value);
|
|
2208
|
+
this.debug(`Listener ${index} for flag ${flagKey} completed`, {
|
|
2209
|
+
flagKey,
|
|
2210
|
+
value
|
|
2211
|
+
});
|
|
2212
|
+
});
|
|
2213
|
+
};
|
|
2214
|
+
notifyPlanListeners = (value) => {
|
|
2215
|
+
const listeners = this.planListeners ?? [];
|
|
2216
|
+
if (listeners.size > 0) {
|
|
2217
|
+
this.debug(`Notifying ${listeners.size} plan listeners`, { value });
|
|
2218
|
+
}
|
|
2219
|
+
listeners.forEach((listener, index) => {
|
|
2220
|
+
this.debug(`Calling listener ${index} for plan`, {
|
|
2221
|
+
value
|
|
2222
|
+
});
|
|
2223
|
+
notifyPlanListener(listener, value);
|
|
2224
|
+
this.debug(`Listener ${index} for plan completed`, {
|
|
2225
|
+
value
|
|
2226
|
+
});
|
|
2227
|
+
});
|
|
1494
2228
|
};
|
|
1495
2229
|
};
|
|
1496
2230
|
var notifyPendingListener = (listener, value) => {
|
|
@@ -1514,12 +2248,19 @@ var notifyFlagValueListener = (listener, value) => {
|
|
|
1514
2248
|
listener();
|
|
1515
2249
|
}
|
|
1516
2250
|
};
|
|
2251
|
+
var notifyPlanListener = (listener, value) => {
|
|
2252
|
+
if (listener.length > 0) {
|
|
2253
|
+
listener(value);
|
|
2254
|
+
} else {
|
|
2255
|
+
listener();
|
|
2256
|
+
}
|
|
2257
|
+
};
|
|
1517
2258
|
|
|
1518
2259
|
// src/context/schematic.ts
|
|
1519
2260
|
import { inject, onScopeDispose } from "vue";
|
|
1520
2261
|
|
|
1521
2262
|
// src/version.ts
|
|
1522
|
-
var version2 = "
|
|
2263
|
+
var version2 = "1.4.0";
|
|
1523
2264
|
|
|
1524
2265
|
// src/context/schematic.ts
|
|
1525
2266
|
var SchematicSymbol = Symbol("schematic");
|
|
@@ -1635,6 +2376,23 @@ var useSchematicEntitlement = (key, opts) => {
|
|
|
1635
2376
|
userId: computed(() => flagCheck.value.userId)
|
|
1636
2377
|
};
|
|
1637
2378
|
};
|
|
2379
|
+
var useSchematicPlan = (opts) => {
|
|
2380
|
+
const client = useSchematicClient(opts);
|
|
2381
|
+
const fallback = opts?.fallback;
|
|
2382
|
+
const plan = ref(
|
|
2383
|
+
client.getPlan() ?? fallback
|
|
2384
|
+
);
|
|
2385
|
+
let unsubscribe = null;
|
|
2386
|
+
onMounted(() => {
|
|
2387
|
+
unsubscribe = client.addPlanListener((value) => {
|
|
2388
|
+
plan.value = value;
|
|
2389
|
+
});
|
|
2390
|
+
});
|
|
2391
|
+
onScopeDispose2(() => {
|
|
2392
|
+
unsubscribe?.();
|
|
2393
|
+
});
|
|
2394
|
+
return plan;
|
|
2395
|
+
};
|
|
1638
2396
|
var useSchematicIsPending = (opts) => {
|
|
1639
2397
|
const client = useSchematicClient(opts);
|
|
1640
2398
|
const isPending = ref(client.getIsPending());
|
|
@@ -1660,7 +2418,8 @@ export {
|
|
|
1660
2418
|
useSchematicEntitlement,
|
|
1661
2419
|
useSchematicEvents,
|
|
1662
2420
|
useSchematicFlag,
|
|
1663
|
-
useSchematicIsPending
|
|
2421
|
+
useSchematicIsPending,
|
|
2422
|
+
useSchematicPlan
|
|
1664
2423
|
};
|
|
1665
2424
|
/*! Bundled license information:
|
|
1666
2425
|
|