@schematichq/schematic-js 1.2.1 → 1.2.3
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/README.md +36 -2
- package/dist/schematic.browser.js +2 -2
- package/dist/schematic.cjs.js +317 -19
- package/dist/schematic.d.ts +133 -4
- package/dist/schematic.esm.js +317 -19
- package/package.json +10 -9
package/dist/schematic.esm.js
CHANGED
|
@@ -628,6 +628,7 @@ function CheckFlagResponseDataFromJSONTyped(json, ignoreDiscriminator) {
|
|
|
628
628
|
error: json["error"] == null ? void 0 : json["error"],
|
|
629
629
|
featureAllocation: json["feature_allocation"] == null ? void 0 : json["feature_allocation"],
|
|
630
630
|
featureUsage: json["feature_usage"] == null ? void 0 : json["feature_usage"],
|
|
631
|
+
featureUsageEvent: json["feature_usage_event"] == null ? void 0 : json["feature_usage_event"],
|
|
631
632
|
featureUsagePeriod: json["feature_usage_period"] == null ? void 0 : json["feature_usage_period"],
|
|
632
633
|
featureUsageResetAt: json["feature_usage_reset_at"] == null ? void 0 : new Date(json["feature_usage_reset_at"]),
|
|
633
634
|
flag: json["flag"],
|
|
@@ -640,6 +641,28 @@ function CheckFlagResponseDataFromJSONTyped(json, ignoreDiscriminator) {
|
|
|
640
641
|
};
|
|
641
642
|
}
|
|
642
643
|
|
|
644
|
+
// src/types/api/models/EventBodyFlagCheck.ts
|
|
645
|
+
function EventBodyFlagCheckToJSON(json) {
|
|
646
|
+
return EventBodyFlagCheckToJSONTyped(json, false);
|
|
647
|
+
}
|
|
648
|
+
function EventBodyFlagCheckToJSONTyped(value, ignoreDiscriminator = false) {
|
|
649
|
+
if (value == null) {
|
|
650
|
+
return value;
|
|
651
|
+
}
|
|
652
|
+
return {
|
|
653
|
+
company_id: value["companyId"],
|
|
654
|
+
error: value["error"],
|
|
655
|
+
flag_id: value["flagId"],
|
|
656
|
+
flag_key: value["flagKey"],
|
|
657
|
+
reason: value["reason"],
|
|
658
|
+
req_company: value["reqCompany"],
|
|
659
|
+
req_user: value["reqUser"],
|
|
660
|
+
rule_id: value["ruleId"],
|
|
661
|
+
user_id: value["userId"],
|
|
662
|
+
value: value["value"]
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
|
|
643
666
|
// src/types/api/models/CheckFlagResponse.ts
|
|
644
667
|
function CheckFlagResponseFromJSON(json) {
|
|
645
668
|
return CheckFlagResponseFromJSONTyped(json, false);
|
|
@@ -705,6 +728,7 @@ var CheckFlagReturnFromJSON = (json) => {
|
|
|
705
728
|
error,
|
|
706
729
|
featureAllocation,
|
|
707
730
|
featureUsage,
|
|
731
|
+
featureUsageEvent,
|
|
708
732
|
featureUsagePeriod,
|
|
709
733
|
featureUsageResetAt,
|
|
710
734
|
flag,
|
|
@@ -724,6 +748,7 @@ var CheckFlagReturnFromJSON = (json) => {
|
|
|
724
748
|
error: error == null ? void 0 : error,
|
|
725
749
|
featureAllocation: featureAllocation == null ? void 0 : featureAllocation,
|
|
726
750
|
featureUsage: featureUsage == null ? void 0 : featureUsage,
|
|
751
|
+
featureUsageEvent: featureUsageEvent === null ? void 0 : featureUsageEvent,
|
|
727
752
|
featureUsagePeriod: featureUsagePeriod == null ? void 0 : featureUsagePeriod,
|
|
728
753
|
featureUsageResetAt: featureUsageResetAt == null ? void 0 : featureUsageResetAt,
|
|
729
754
|
flag,
|
|
@@ -753,7 +778,7 @@ function contextString(context) {
|
|
|
753
778
|
}
|
|
754
779
|
|
|
755
780
|
// src/version.ts
|
|
756
|
-
var version = "1.2.
|
|
781
|
+
var version = "1.2.3";
|
|
757
782
|
|
|
758
783
|
// src/index.ts
|
|
759
784
|
var anonymousIdKey = "schematicId";
|
|
@@ -763,6 +788,8 @@ var Schematic = class {
|
|
|
763
788
|
apiUrl = "https://api.schematichq.com";
|
|
764
789
|
conn = null;
|
|
765
790
|
context = {};
|
|
791
|
+
debugEnabled = false;
|
|
792
|
+
offlineEnabled = false;
|
|
766
793
|
eventQueue;
|
|
767
794
|
eventUrl = "https://c.schematichq.com";
|
|
768
795
|
flagCheckListeners = {};
|
|
@@ -772,11 +799,32 @@ var Schematic = class {
|
|
|
772
799
|
storage;
|
|
773
800
|
useWebSocket = false;
|
|
774
801
|
checks = {};
|
|
802
|
+
featureUsageEventMap = {};
|
|
775
803
|
webSocketUrl = "wss://api.schematichq.com";
|
|
776
804
|
constructor(apiKey, options) {
|
|
777
805
|
this.apiKey = apiKey;
|
|
778
806
|
this.eventQueue = [];
|
|
779
807
|
this.useWebSocket = options?.useWebSocket ?? false;
|
|
808
|
+
this.debugEnabled = options?.debug ?? false;
|
|
809
|
+
this.offlineEnabled = options?.offline ?? false;
|
|
810
|
+
if (typeof window !== "undefined" && typeof window.location !== "undefined") {
|
|
811
|
+
const params = new URLSearchParams(window.location.search);
|
|
812
|
+
const debugParam = params.get("schematic_debug");
|
|
813
|
+
if (debugParam !== null && (debugParam === "" || debugParam === "true" || debugParam === "1")) {
|
|
814
|
+
this.debugEnabled = true;
|
|
815
|
+
}
|
|
816
|
+
const offlineParam = params.get("schematic_offline");
|
|
817
|
+
if (offlineParam !== null && (offlineParam === "" || offlineParam === "true" || offlineParam === "1")) {
|
|
818
|
+
this.offlineEnabled = true;
|
|
819
|
+
this.debugEnabled = true;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
if (this.offlineEnabled && options?.debug !== false) {
|
|
823
|
+
this.debugEnabled = true;
|
|
824
|
+
}
|
|
825
|
+
if (this.offlineEnabled) {
|
|
826
|
+
this.setIsPending(false);
|
|
827
|
+
}
|
|
780
828
|
this.additionalHeaders = {
|
|
781
829
|
"X-Schematic-Client-Version": `schematic-js@${version}`,
|
|
782
830
|
...options?.additionalHeaders ?? {}
|
|
@@ -800,6 +848,13 @@ var Schematic = class {
|
|
|
800
848
|
this.flushEventQueue();
|
|
801
849
|
});
|
|
802
850
|
}
|
|
851
|
+
if (this.offlineEnabled) {
|
|
852
|
+
this.debug(
|
|
853
|
+
"Initialized with offline mode enabled - no network requests will be made"
|
|
854
|
+
);
|
|
855
|
+
} else if (this.debugEnabled) {
|
|
856
|
+
this.debug("Initialized with debug mode enabled");
|
|
857
|
+
}
|
|
803
858
|
}
|
|
804
859
|
/**
|
|
805
860
|
* Get value for a single flag.
|
|
@@ -812,6 +867,14 @@ var Schematic = class {
|
|
|
812
867
|
const { fallback = false, key } = options;
|
|
813
868
|
const context = options.context || this.context;
|
|
814
869
|
const contextStr = contextString(context);
|
|
870
|
+
this.debug(`checkFlag: ${key}`, { context, fallback });
|
|
871
|
+
if (this.isOffline()) {
|
|
872
|
+
this.debug(`checkFlag offline result: ${key}`, {
|
|
873
|
+
value: fallback,
|
|
874
|
+
offlineMode: true
|
|
875
|
+
});
|
|
876
|
+
return fallback;
|
|
877
|
+
}
|
|
815
878
|
if (!this.useWebSocket) {
|
|
816
879
|
const requestUrl = `${this.apiUrl}/flags/${key}/check`;
|
|
817
880
|
return fetch(requestUrl, {
|
|
@@ -828,17 +891,35 @@ var Schematic = class {
|
|
|
828
891
|
}
|
|
829
892
|
return response.json();
|
|
830
893
|
}).then((response) => {
|
|
831
|
-
|
|
894
|
+
const parsedResponse = CheckFlagResponseFromJSON(response);
|
|
895
|
+
this.debug(`checkFlag result: ${key}`, parsedResponse);
|
|
896
|
+
const result = CheckFlagReturnFromJSON(parsedResponse.data);
|
|
897
|
+
if (typeof result.featureUsageEvent === "string") {
|
|
898
|
+
this.updateFeatureUsageEventMap(result);
|
|
899
|
+
}
|
|
900
|
+
this.submitFlagCheckEvent(key, result, context);
|
|
901
|
+
return result.value;
|
|
832
902
|
}).catch((error) => {
|
|
833
903
|
console.error("There was a problem with the fetch operation:", error);
|
|
904
|
+
const errorResult = {
|
|
905
|
+
flag: key,
|
|
906
|
+
value: fallback,
|
|
907
|
+
reason: "API request failed",
|
|
908
|
+
error: error instanceof Error ? error.message : String(error)
|
|
909
|
+
};
|
|
910
|
+
this.submitFlagCheckEvent(key, errorResult, context);
|
|
834
911
|
return fallback;
|
|
835
912
|
});
|
|
836
913
|
}
|
|
837
914
|
try {
|
|
838
915
|
const existingVals = this.checks[contextStr];
|
|
839
|
-
if (this.conn && typeof existingVals !== "undefined" && typeof existingVals[key] !== "undefined") {
|
|
916
|
+
if (this.conn !== null && typeof existingVals !== "undefined" && typeof existingVals[key] !== "undefined") {
|
|
917
|
+
this.debug(`checkFlag cached result: ${key}`, existingVals[key]);
|
|
840
918
|
return existingVals[key].value;
|
|
841
919
|
}
|
|
920
|
+
if (this.isOffline()) {
|
|
921
|
+
return fallback;
|
|
922
|
+
}
|
|
842
923
|
try {
|
|
843
924
|
await this.setContext(context);
|
|
844
925
|
} catch (error) {
|
|
@@ -849,16 +930,74 @@ var Schematic = class {
|
|
|
849
930
|
return this.fallbackToRest(key, context, fallback);
|
|
850
931
|
}
|
|
851
932
|
const contextVals = this.checks[contextStr] ?? {};
|
|
852
|
-
|
|
933
|
+
const flagCheck = contextVals[key];
|
|
934
|
+
const result = flagCheck?.value ?? fallback;
|
|
935
|
+
this.debug(
|
|
936
|
+
`checkFlag WebSocket result: ${key}`,
|
|
937
|
+
typeof flagCheck !== "undefined" ? flagCheck : { value: fallback, fallbackUsed: true }
|
|
938
|
+
);
|
|
939
|
+
if (typeof flagCheck !== "undefined") {
|
|
940
|
+
this.submitFlagCheckEvent(key, flagCheck, context);
|
|
941
|
+
}
|
|
942
|
+
return result;
|
|
853
943
|
} catch (error) {
|
|
854
944
|
console.error("Unexpected error in checkFlag:", error);
|
|
945
|
+
const errorResult = {
|
|
946
|
+
flag: key,
|
|
947
|
+
value: fallback,
|
|
948
|
+
reason: "Unexpected error in flag check",
|
|
949
|
+
error: error instanceof Error ? error.message : String(error)
|
|
950
|
+
};
|
|
951
|
+
this.submitFlagCheckEvent(key, errorResult, context);
|
|
855
952
|
return fallback;
|
|
856
953
|
}
|
|
857
954
|
}
|
|
955
|
+
/**
|
|
956
|
+
* Helper function to log debug messages
|
|
957
|
+
* Only logs if debug mode is enabled
|
|
958
|
+
*/
|
|
959
|
+
debug(message, ...args) {
|
|
960
|
+
if (this.debugEnabled) {
|
|
961
|
+
console.log(`[Schematic] ${message}`, ...args);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* Helper function to check if client is in offline mode
|
|
966
|
+
*/
|
|
967
|
+
isOffline() {
|
|
968
|
+
return this.offlineEnabled;
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* Submit a flag check event
|
|
972
|
+
* Records data about a flag check for analytics
|
|
973
|
+
*/
|
|
974
|
+
submitFlagCheckEvent(flagKey, result, context) {
|
|
975
|
+
const eventBody = {
|
|
976
|
+
flagKey,
|
|
977
|
+
value: result.value,
|
|
978
|
+
reason: result.reason,
|
|
979
|
+
flagId: result.flagId,
|
|
980
|
+
ruleId: result.ruleId,
|
|
981
|
+
companyId: result.companyId,
|
|
982
|
+
userId: result.userId,
|
|
983
|
+
error: result.error,
|
|
984
|
+
reqCompany: context.company,
|
|
985
|
+
reqUser: context.user
|
|
986
|
+
};
|
|
987
|
+
this.debug(`submitting flag check event:`, eventBody);
|
|
988
|
+
return this.handleEvent("flag_check", EventBodyFlagCheckToJSON(eventBody));
|
|
989
|
+
}
|
|
858
990
|
/**
|
|
859
991
|
* Helper method for falling back to REST API when WebSocket connection fails
|
|
860
992
|
*/
|
|
861
993
|
async fallbackToRest(key, context, fallback) {
|
|
994
|
+
if (this.isOffline()) {
|
|
995
|
+
this.debug(`fallbackToRest offline result: ${key}`, {
|
|
996
|
+
value: fallback,
|
|
997
|
+
offlineMode: true
|
|
998
|
+
});
|
|
999
|
+
return fallback;
|
|
1000
|
+
}
|
|
862
1001
|
try {
|
|
863
1002
|
const requestUrl = `${this.apiUrl}/flags/${key}/check`;
|
|
864
1003
|
const response = await fetch(requestUrl, {
|
|
@@ -873,19 +1012,39 @@ var Schematic = class {
|
|
|
873
1012
|
if (!response.ok) {
|
|
874
1013
|
throw new Error("Network response was not ok");
|
|
875
1014
|
}
|
|
876
|
-
const
|
|
877
|
-
|
|
1015
|
+
const responseJson = await response.json();
|
|
1016
|
+
const data = CheckFlagResponseFromJSON(responseJson);
|
|
1017
|
+
this.debug(`fallbackToRest result: ${key}`, data);
|
|
1018
|
+
const result = CheckFlagReturnFromJSON(data.data);
|
|
1019
|
+
if (typeof result.featureUsageEvent === "string") {
|
|
1020
|
+
this.updateFeatureUsageEventMap(result);
|
|
1021
|
+
}
|
|
1022
|
+
this.submitFlagCheckEvent(key, result, context);
|
|
1023
|
+
return result.value;
|
|
878
1024
|
} catch (error) {
|
|
879
1025
|
console.error("REST API call failed, using fallback value:", error);
|
|
1026
|
+
const errorResult = {
|
|
1027
|
+
flag: key,
|
|
1028
|
+
value: fallback,
|
|
1029
|
+
reason: "API request failed (fallback)",
|
|
1030
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1031
|
+
};
|
|
1032
|
+
this.submitFlagCheckEvent(key, errorResult, context);
|
|
880
1033
|
return fallback;
|
|
881
1034
|
}
|
|
882
1035
|
}
|
|
883
1036
|
/**
|
|
884
1037
|
* Make an API call to fetch all flag values for a given context.
|
|
885
1038
|
* Recommended for use in REST mode only.
|
|
1039
|
+
* In offline mode, returns an empty object.
|
|
886
1040
|
*/
|
|
887
1041
|
checkFlags = async (context) => {
|
|
888
1042
|
context = context || this.context;
|
|
1043
|
+
this.debug(`checkFlags`, { context });
|
|
1044
|
+
if (this.isOffline()) {
|
|
1045
|
+
this.debug(`checkFlags offline result: returning empty object`);
|
|
1046
|
+
return {};
|
|
1047
|
+
}
|
|
889
1048
|
const requestUrl = `${this.apiUrl}/flags/check`;
|
|
890
1049
|
const requestBody = JSON.stringify(context);
|
|
891
1050
|
return fetch(requestUrl, {
|
|
@@ -903,6 +1062,7 @@ var Schematic = class {
|
|
|
903
1062
|
return response.json();
|
|
904
1063
|
}).then((responseJson) => {
|
|
905
1064
|
const resp = CheckFlagsResponseFromJSON(responseJson);
|
|
1065
|
+
this.debug(`checkFlags result:`, resp);
|
|
906
1066
|
return (resp?.data?.flags ?? []).reduce(
|
|
907
1067
|
(accum, flag) => {
|
|
908
1068
|
accum[flag.flag] = flag.value;
|
|
@@ -921,6 +1081,7 @@ var Schematic = class {
|
|
|
921
1081
|
* send an identify event to the Schematic API which will upsert a user and company.
|
|
922
1082
|
*/
|
|
923
1083
|
identify = (body) => {
|
|
1084
|
+
this.debug(`identify:`, body);
|
|
924
1085
|
try {
|
|
925
1086
|
this.setContext({
|
|
926
1087
|
company: body.company?.keys,
|
|
@@ -938,10 +1099,12 @@ var Schematic = class {
|
|
|
938
1099
|
* 2. Send the context to the server
|
|
939
1100
|
* 3. Wait for initial flag values to be returned
|
|
940
1101
|
* The promise resolves when initial flag values are received.
|
|
1102
|
+
* In offline mode, this will just set the context locally without connecting.
|
|
941
1103
|
*/
|
|
942
1104
|
setContext = async (context) => {
|
|
943
|
-
if (!this.useWebSocket) {
|
|
1105
|
+
if (this.isOffline() || !this.useWebSocket) {
|
|
944
1106
|
this.context = context;
|
|
1107
|
+
this.setIsPending(false);
|
|
945
1108
|
return Promise.resolve();
|
|
946
1109
|
}
|
|
947
1110
|
try {
|
|
@@ -959,14 +1122,67 @@ var Schematic = class {
|
|
|
959
1122
|
/**
|
|
960
1123
|
* Send a track event
|
|
961
1124
|
* Track usage for a company and/or user.
|
|
1125
|
+
* Optimistically updates feature usage flags if tracking a featureUsageEvent.
|
|
962
1126
|
*/
|
|
963
1127
|
track = (body) => {
|
|
964
|
-
const { company, user, event, traits } = body;
|
|
965
|
-
|
|
1128
|
+
const { company, user, event, traits, quantity = 1 } = body;
|
|
1129
|
+
const trackData = {
|
|
966
1130
|
company: company ?? this.context.company,
|
|
967
1131
|
event,
|
|
968
1132
|
traits: traits ?? {},
|
|
969
|
-
user: user ?? this.context.user
|
|
1133
|
+
user: user ?? this.context.user,
|
|
1134
|
+
quantity
|
|
1135
|
+
};
|
|
1136
|
+
this.debug(`track:`, trackData);
|
|
1137
|
+
if (event in this.featureUsageEventMap) {
|
|
1138
|
+
this.optimisticallyUpdateFeatureUsage(event, quantity);
|
|
1139
|
+
}
|
|
1140
|
+
return this.handleEvent("track", trackData);
|
|
1141
|
+
};
|
|
1142
|
+
/**
|
|
1143
|
+
* Optimistically update feature usage flags associated with a tracked event
|
|
1144
|
+
* This updates flags in memory with updated usage counts and value/featureUsageExceeded flags
|
|
1145
|
+
* before the network request completes
|
|
1146
|
+
*/
|
|
1147
|
+
optimisticallyUpdateFeatureUsage = (eventName, quantity = 1) => {
|
|
1148
|
+
const flagsForEvent = this.featureUsageEventMap[eventName];
|
|
1149
|
+
if (flagsForEvent === void 0 || flagsForEvent === null) return;
|
|
1150
|
+
this.debug(
|
|
1151
|
+
`Optimistically updating feature usage for event: ${eventName}`,
|
|
1152
|
+
{ quantity }
|
|
1153
|
+
);
|
|
1154
|
+
Object.entries(flagsForEvent).forEach(([flagKey, check]) => {
|
|
1155
|
+
if (check === void 0) return;
|
|
1156
|
+
const updatedCheck = { ...check };
|
|
1157
|
+
if (typeof updatedCheck.featureUsage === "number") {
|
|
1158
|
+
updatedCheck.featureUsage += quantity;
|
|
1159
|
+
if (typeof updatedCheck.featureAllocation === "number") {
|
|
1160
|
+
const wasExceeded = updatedCheck.featureUsageExceeded === true;
|
|
1161
|
+
const nowExceeded = updatedCheck.featureUsage >= updatedCheck.featureAllocation;
|
|
1162
|
+
if (nowExceeded !== wasExceeded) {
|
|
1163
|
+
updatedCheck.featureUsageExceeded = nowExceeded;
|
|
1164
|
+
if (nowExceeded) {
|
|
1165
|
+
updatedCheck.value = false;
|
|
1166
|
+
}
|
|
1167
|
+
this.debug(`Usage limit status changed for flag: ${flagKey}`, {
|
|
1168
|
+
was: wasExceeded ? "exceeded" : "within limits",
|
|
1169
|
+
now: nowExceeded ? "exceeded" : "within limits",
|
|
1170
|
+
featureUsage: updatedCheck.featureUsage,
|
|
1171
|
+
featureAllocation: updatedCheck.featureAllocation,
|
|
1172
|
+
value: updatedCheck.value
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
if (this.featureUsageEventMap[eventName] !== void 0) {
|
|
1177
|
+
this.featureUsageEventMap[eventName][flagKey] = updatedCheck;
|
|
1178
|
+
}
|
|
1179
|
+
const contextStr = contextString(this.context);
|
|
1180
|
+
if (this.checks[contextStr] !== void 0 && this.checks[contextStr] !== null) {
|
|
1181
|
+
this.checks[contextStr][flagKey] = updatedCheck;
|
|
1182
|
+
}
|
|
1183
|
+
this.notifyFlagCheckListeners(flagKey, updatedCheck);
|
|
1184
|
+
this.notifyFlagValueListeners(flagKey, updatedCheck.value);
|
|
1185
|
+
}
|
|
970
1186
|
});
|
|
971
1187
|
};
|
|
972
1188
|
/**
|
|
@@ -1010,8 +1226,13 @@ var Schematic = class {
|
|
|
1010
1226
|
sendEvent = async (event) => {
|
|
1011
1227
|
const captureUrl = `${this.eventUrl}/e`;
|
|
1012
1228
|
const payload = JSON.stringify(event);
|
|
1229
|
+
this.debug(`sending event:`, { url: captureUrl, event });
|
|
1230
|
+
if (this.isOffline()) {
|
|
1231
|
+
this.debug(`event not sent (offline mode):`, { event });
|
|
1232
|
+
return Promise.resolve();
|
|
1233
|
+
}
|
|
1013
1234
|
try {
|
|
1014
|
-
await fetch(captureUrl, {
|
|
1235
|
+
const response = await fetch(captureUrl, {
|
|
1015
1236
|
method: "POST",
|
|
1016
1237
|
headers: {
|
|
1017
1238
|
...this.additionalHeaders ?? {},
|
|
@@ -1019,6 +1240,10 @@ var Schematic = class {
|
|
|
1019
1240
|
},
|
|
1020
1241
|
body: payload
|
|
1021
1242
|
});
|
|
1243
|
+
this.debug(`event sent:`, {
|
|
1244
|
+
status: response.status,
|
|
1245
|
+
statusText: response.statusText
|
|
1246
|
+
});
|
|
1022
1247
|
} catch (error) {
|
|
1023
1248
|
console.error("Error sending Schematic event: ", error);
|
|
1024
1249
|
}
|
|
@@ -1033,8 +1258,13 @@ var Schematic = class {
|
|
|
1033
1258
|
*/
|
|
1034
1259
|
/**
|
|
1035
1260
|
* If using websocket mode, close the connection when done.
|
|
1261
|
+
* In offline mode, this is a no-op.
|
|
1036
1262
|
*/
|
|
1037
1263
|
cleanup = async () => {
|
|
1264
|
+
if (this.isOffline()) {
|
|
1265
|
+
this.debug("cleanup: skipped (offline mode)");
|
|
1266
|
+
return Promise.resolve();
|
|
1267
|
+
}
|
|
1038
1268
|
if (this.conn) {
|
|
1039
1269
|
try {
|
|
1040
1270
|
const socket = await this.conn;
|
|
@@ -1048,16 +1278,26 @@ var Schematic = class {
|
|
|
1048
1278
|
};
|
|
1049
1279
|
// Open a websocket connection
|
|
1050
1280
|
wsConnect = () => {
|
|
1281
|
+
if (this.isOffline()) {
|
|
1282
|
+
this.debug("wsConnect: skipped (offline mode)");
|
|
1283
|
+
return Promise.reject(
|
|
1284
|
+
new Error("WebSocket connection skipped in offline mode")
|
|
1285
|
+
);
|
|
1286
|
+
}
|
|
1051
1287
|
return new Promise((resolve, reject) => {
|
|
1052
1288
|
const wsUrl = `${this.webSocketUrl}/flags/bootstrap?apiKey=${this.apiKey}`;
|
|
1289
|
+
this.debug(`connecting to WebSocket:`, wsUrl);
|
|
1053
1290
|
const webSocket = new WebSocket(wsUrl);
|
|
1054
1291
|
webSocket.onopen = () => {
|
|
1292
|
+
this.debug(`WebSocket connection opened`);
|
|
1055
1293
|
resolve(webSocket);
|
|
1056
1294
|
};
|
|
1057
1295
|
webSocket.onerror = (error) => {
|
|
1296
|
+
this.debug(`WebSocket connection error:`, error);
|
|
1058
1297
|
reject(error);
|
|
1059
1298
|
};
|
|
1060
1299
|
webSocket.onclose = () => {
|
|
1300
|
+
this.debug(`WebSocket connection closed`);
|
|
1061
1301
|
this.conn = null;
|
|
1062
1302
|
};
|
|
1063
1303
|
});
|
|
@@ -1065,21 +1305,44 @@ var Schematic = class {
|
|
|
1065
1305
|
// Send a message on the websocket indicating interest in a particular evaluation context
|
|
1066
1306
|
// and wait for the initial set of flag values to be returned
|
|
1067
1307
|
wsSendMessage = (socket, context) => {
|
|
1308
|
+
if (this.isOffline()) {
|
|
1309
|
+
this.debug("wsSendMessage: skipped (offline mode)");
|
|
1310
|
+
this.setIsPending(false);
|
|
1311
|
+
return Promise.resolve();
|
|
1312
|
+
}
|
|
1068
1313
|
return new Promise((resolve, reject) => {
|
|
1069
1314
|
if (contextString(context) == contextString(this.context)) {
|
|
1315
|
+
this.debug(`WebSocket context unchanged, skipping update`);
|
|
1070
1316
|
return resolve(this.setIsPending(false));
|
|
1071
1317
|
}
|
|
1318
|
+
this.debug(`WebSocket context updated:`, context);
|
|
1072
1319
|
this.context = context;
|
|
1073
1320
|
const sendMessage = () => {
|
|
1074
1321
|
let resolved = false;
|
|
1075
1322
|
const messageHandler = (event) => {
|
|
1076
1323
|
const message = JSON.parse(event.data);
|
|
1324
|
+
this.debug(`WebSocket message received:`, message);
|
|
1077
1325
|
if (!(contextString(context) in this.checks)) {
|
|
1078
1326
|
this.checks[contextString(context)] = {};
|
|
1079
1327
|
}
|
|
1080
1328
|
(message.flags ?? []).forEach((flag) => {
|
|
1081
1329
|
const flagCheck = CheckFlagReturnFromJSON(flag);
|
|
1082
|
-
|
|
1330
|
+
const contextStr = contextString(context);
|
|
1331
|
+
if (this.checks[contextStr] === void 0) {
|
|
1332
|
+
this.checks[contextStr] = {};
|
|
1333
|
+
}
|
|
1334
|
+
this.checks[contextStr][flagCheck.flag] = flagCheck;
|
|
1335
|
+
this.debug(`WebSocket flag update:`, {
|
|
1336
|
+
flag: flagCheck.flag,
|
|
1337
|
+
value: flagCheck.value,
|
|
1338
|
+
flagCheck
|
|
1339
|
+
});
|
|
1340
|
+
if (typeof flagCheck.featureUsageEvent === "string") {
|
|
1341
|
+
this.updateFeatureUsageEventMap(flagCheck);
|
|
1342
|
+
}
|
|
1343
|
+
if (this.flagCheckListeners[flag.flag]?.size > 0 || this.flagValueListeners[flag.flag]?.size > 0) {
|
|
1344
|
+
this.submitFlagCheckEvent(flagCheck.flag, flagCheck, context);
|
|
1345
|
+
}
|
|
1083
1346
|
this.notifyFlagCheckListeners(flag.flag, flagCheck);
|
|
1084
1347
|
this.notifyFlagValueListeners(flag.flag, flagCheck.value);
|
|
1085
1348
|
});
|
|
@@ -1090,19 +1353,23 @@ var Schematic = class {
|
|
|
1090
1353
|
}
|
|
1091
1354
|
};
|
|
1092
1355
|
socket.addEventListener("message", messageHandler);
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
);
|
|
1356
|
+
const clientVersion = this.additionalHeaders["X-Schematic-Client-Version"] ?? `schematic-js@${version}`;
|
|
1357
|
+
const messagePayload = {
|
|
1358
|
+
apiKey: this.apiKey,
|
|
1359
|
+
clientVersion,
|
|
1360
|
+
data: context
|
|
1361
|
+
};
|
|
1362
|
+
this.debug(`WebSocket sending message:`, messagePayload);
|
|
1363
|
+
socket.send(JSON.stringify(messagePayload));
|
|
1100
1364
|
};
|
|
1101
1365
|
if (socket.readyState === WebSocket.OPEN) {
|
|
1366
|
+
this.debug(`WebSocket already open, sending message`);
|
|
1102
1367
|
sendMessage();
|
|
1103
1368
|
} else if (socket.readyState === WebSocket.CONNECTING) {
|
|
1369
|
+
this.debug(`WebSocket connecting, waiting for open to send message`);
|
|
1104
1370
|
socket.addEventListener("open", sendMessage);
|
|
1105
1371
|
} else {
|
|
1372
|
+
this.debug(`WebSocket is closed, cannot send message`);
|
|
1106
1373
|
reject("WebSocket is not open or connecting");
|
|
1107
1374
|
}
|
|
1108
1375
|
});
|
|
@@ -1159,10 +1426,40 @@ var Schematic = class {
|
|
|
1159
1426
|
};
|
|
1160
1427
|
notifyFlagCheckListeners = (flagKey, check) => {
|
|
1161
1428
|
const listeners = this.flagCheckListeners?.[flagKey] ?? [];
|
|
1429
|
+
if (listeners.size > 0) {
|
|
1430
|
+
this.debug(
|
|
1431
|
+
`Notifying ${listeners.size} flag check listeners for ${flagKey}`,
|
|
1432
|
+
check
|
|
1433
|
+
);
|
|
1434
|
+
}
|
|
1435
|
+
if (typeof check.featureUsageEvent === "string") {
|
|
1436
|
+
this.updateFeatureUsageEventMap(check);
|
|
1437
|
+
}
|
|
1162
1438
|
listeners.forEach((listener) => notifyFlagCheckListener(listener, check));
|
|
1163
1439
|
};
|
|
1440
|
+
/** Add or update a CheckFlagReturn in the featureUsageEventMap */
|
|
1441
|
+
updateFeatureUsageEventMap = (check) => {
|
|
1442
|
+
if (typeof check.featureUsageEvent !== "string") return;
|
|
1443
|
+
const eventName = check.featureUsageEvent;
|
|
1444
|
+
if (this.featureUsageEventMap[eventName] === void 0 || this.featureUsageEventMap[eventName] === null) {
|
|
1445
|
+
this.featureUsageEventMap[eventName] = {};
|
|
1446
|
+
}
|
|
1447
|
+
if (this.featureUsageEventMap[eventName] !== void 0) {
|
|
1448
|
+
this.featureUsageEventMap[eventName][check.flag] = check;
|
|
1449
|
+
}
|
|
1450
|
+
this.debug(
|
|
1451
|
+
`Updated featureUsageEventMap for event: ${eventName}, flag: ${check.flag}`,
|
|
1452
|
+
check
|
|
1453
|
+
);
|
|
1454
|
+
};
|
|
1164
1455
|
notifyFlagValueListeners = (flagKey, value) => {
|
|
1165
1456
|
const listeners = this.flagValueListeners?.[flagKey] ?? [];
|
|
1457
|
+
if (listeners.size > 0) {
|
|
1458
|
+
this.debug(
|
|
1459
|
+
`Notifying ${listeners.size} flag value listeners for ${flagKey}`,
|
|
1460
|
+
{ value }
|
|
1461
|
+
);
|
|
1462
|
+
}
|
|
1166
1463
|
listeners.forEach((listener) => notifyFlagValueListener(listener, value));
|
|
1167
1464
|
};
|
|
1168
1465
|
};
|
|
@@ -1191,6 +1488,7 @@ export {
|
|
|
1191
1488
|
CheckFlagResponseFromJSON,
|
|
1192
1489
|
CheckFlagReturnFromJSON,
|
|
1193
1490
|
CheckFlagsResponseFromJSON,
|
|
1491
|
+
EventBodyFlagCheckToJSON,
|
|
1194
1492
|
RuleType,
|
|
1195
1493
|
Schematic,
|
|
1196
1494
|
UsagePeriod
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@schematichq/schematic-js",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.3",
|
|
4
4
|
"main": "dist/schematic.cjs.js",
|
|
5
5
|
"module": "dist/schematic.esm.js",
|
|
6
6
|
"types": "dist/schematic.d.ts",
|
|
@@ -33,23 +33,24 @@
|
|
|
33
33
|
"uuid": "^11.0.5"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"@
|
|
37
|
-
"@
|
|
36
|
+
"@eslint/js": "^9.24.0",
|
|
37
|
+
"@microsoft/api-extractor": "^7.52.2",
|
|
38
|
+
"@openapitools/openapi-generator-cli": "^2.18.4",
|
|
38
39
|
"@types/jest": "^29.5.14",
|
|
39
40
|
"@types/uuid": "^10.0.0",
|
|
40
|
-
"
|
|
41
|
-
"@typescript-eslint/parser": "^8.23.0",
|
|
42
|
-
"esbuild": "^0.24.2",
|
|
41
|
+
"esbuild": "^0.25.2",
|
|
43
42
|
"esbuild-jest": "^0.5.0",
|
|
44
|
-
"eslint": "^
|
|
43
|
+
"eslint": "^9.24.0",
|
|
44
|
+
"globals": "^16.0.0",
|
|
45
45
|
"jest": "^29.7.0",
|
|
46
46
|
"jest-environment-jsdom": "^29.7.0",
|
|
47
47
|
"jest-esbuild": "^0.3.0",
|
|
48
48
|
"jest-fetch-mock": "^3.0.3",
|
|
49
49
|
"mock-socket": "^9.3.1",
|
|
50
50
|
"prettier": "^3.4.2",
|
|
51
|
-
"ts-jest": "^29.
|
|
52
|
-
"typescript": "^5.7.3"
|
|
51
|
+
"ts-jest": "^29.3.0",
|
|
52
|
+
"typescript": "^5.7.3",
|
|
53
|
+
"typescript-eslint": "^8.29.1"
|
|
53
54
|
},
|
|
54
55
|
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
|
55
56
|
}
|