@nexushub/client 1.1.6 → 1.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +192 -287
- package/dist/index.d.cts +68 -112
- package/dist/index.d.ts +68 -112
- package/dist/index.js +192 -287
- package/dist/react.cjs +207 -302
- package/dist/react.d.cts +12 -48
- package/dist/react.d.ts +12 -48
- package/dist/react.js +192 -287
- package/package.json +1 -1
package/dist/react.js
CHANGED
|
@@ -829,14 +829,14 @@ var ContentEngine = class {
|
|
|
829
829
|
this.localCache = new LocalCacheProxy();
|
|
830
830
|
this.memoryCache = new MemoryCache({
|
|
831
831
|
maxSize: 500,
|
|
832
|
-
ttl: 5 *
|
|
833
|
-
// 5
|
|
832
|
+
ttl: 5 * 1e3
|
|
833
|
+
// 👈 5 seconds
|
|
834
834
|
});
|
|
835
835
|
if (!this.isServer) {
|
|
836
836
|
this.browserCache = new BrowserCache(config.projectId);
|
|
837
837
|
}
|
|
838
838
|
this.rateLimiter = new RateLimiter({
|
|
839
|
-
maxRequests: config.debug ?
|
|
839
|
+
maxRequests: config.debug ? 200 : 100,
|
|
840
840
|
timeWindow: 6e4
|
|
841
841
|
});
|
|
842
842
|
this.backoff = new ExponentialBackoff({
|
|
@@ -853,15 +853,11 @@ var ContentEngine = class {
|
|
|
853
853
|
window.addEventListener("beforeunload", this.cleanup.bind(this));
|
|
854
854
|
}
|
|
855
855
|
}
|
|
856
|
-
/** Update runtime configuration without exposing internal mutation. */
|
|
857
856
|
updateConfig(config) {
|
|
858
857
|
this.config = config;
|
|
859
858
|
this.defaultRevalidate = config.revalidateTime ?? false;
|
|
860
859
|
this.cacheStrategy = config.cacheStrategy || "memory";
|
|
861
860
|
}
|
|
862
|
-
/**
|
|
863
|
-
* Fetch a Single Page with full strategy pipeline
|
|
864
|
-
*/
|
|
865
861
|
async getPage(slug, options = {}) {
|
|
866
862
|
const { result, duration } = measurePerformance(
|
|
867
863
|
`getPage("${slug}")`,
|
|
@@ -883,66 +879,66 @@ var ContentEngine = class {
|
|
|
883
879
|
includeMetadata = false
|
|
884
880
|
} = options;
|
|
885
881
|
const cacheKey = `page:${slug}`;
|
|
886
|
-
|
|
887
|
-
cacheKey
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
882
|
+
if (!this.isServer && !forceRefresh && this.cacheStrategy === "memory") {
|
|
883
|
+
const cached = this.memoryCache.get(cacheKey);
|
|
884
|
+
if (cached && this.isCacheValid(cached.metadata)) {
|
|
885
|
+
return includeMetadata ? cached : cached.data;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
if (!forceRefresh && process.env.NODE_ENV === "development") {
|
|
889
|
+
try {
|
|
890
|
+
const local = await this.localCache.getPage(slug);
|
|
891
|
+
if (local) {
|
|
892
|
+
return includeMetadata ? {
|
|
893
|
+
data: local,
|
|
894
|
+
metadata: {
|
|
895
|
+
timestamp: Date.now(),
|
|
896
|
+
expiresAt: Infinity,
|
|
897
|
+
tags: []
|
|
898
|
+
}
|
|
899
|
+
} : local;
|
|
900
|
+
}
|
|
901
|
+
} catch {
|
|
902
|
+
}
|
|
894
903
|
}
|
|
895
904
|
if (this.circuitBreaker.isOpen()) {
|
|
896
|
-
throw new Error(
|
|
905
|
+
throw new Error("[NexusHub] Circuit open. API is unavailable.");
|
|
897
906
|
}
|
|
898
907
|
try {
|
|
899
908
|
await this.rateLimiter.checkLimit();
|
|
900
|
-
const data = await this.backoff.execute(
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
},
|
|
910
|
-
(attempt, delay, error) => {
|
|
911
|
-
if (this.config.debug) {
|
|
912
|
-
console.log(
|
|
913
|
-
`[NexusHub] \u{1F504} Retry ${attempt} for '${slug}' after ${delay}ms: ${error.message}`
|
|
914
|
-
);
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
|
-
);
|
|
909
|
+
const data = await this.backoff.execute(async () => {
|
|
910
|
+
return this.fetchPage(
|
|
911
|
+
slug,
|
|
912
|
+
cacheKey,
|
|
913
|
+
tags,
|
|
914
|
+
revalidate,
|
|
915
|
+
forceRefresh
|
|
916
|
+
);
|
|
917
|
+
});
|
|
918
918
|
this.circuitBreaker.recordSuccess();
|
|
919
919
|
return includeMetadata ? data : data.data;
|
|
920
920
|
} catch (error) {
|
|
921
921
|
this.circuitBreaker.recordFailure();
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
922
|
+
if (process.env.NODE_ENV === "development") {
|
|
923
|
+
try {
|
|
924
|
+
const local = await this.localCache.getPage(slug);
|
|
925
|
+
if (local) {
|
|
926
|
+
return includeMetadata ? {
|
|
927
|
+
data: local,
|
|
928
|
+
metadata: {
|
|
929
|
+
timestamp: Date.now(),
|
|
930
|
+
expiresAt: Infinity,
|
|
931
|
+
tags: []
|
|
932
|
+
}
|
|
933
|
+
} : local;
|
|
934
|
+
}
|
|
935
|
+
} catch {
|
|
936
|
+
}
|
|
926
937
|
}
|
|
927
938
|
throw this.normalizeError(error, `Failed to fetch page '${slug}'`);
|
|
928
939
|
}
|
|
929
940
|
}
|
|
930
|
-
/**
|
|
931
|
-
* Fetch a Collection (Optimized)
|
|
932
|
-
*/
|
|
933
941
|
async getCollection(collectionId, query = {}, options = {}) {
|
|
934
|
-
const { result, duration } = measurePerformance(
|
|
935
|
-
`getCollection("${collectionId}")`,
|
|
936
|
-
() => this._getCollection(collectionId, query, options)
|
|
937
|
-
);
|
|
938
|
-
if (this.config.debug && duration > 100) {
|
|
939
|
-
console.warn(
|
|
940
|
-
`[NexusHub] \u26A0\uFE0F getCollection("${collectionId}") took ${duration.toFixed(2)}ms`
|
|
941
|
-
);
|
|
942
|
-
}
|
|
943
|
-
return result;
|
|
944
|
-
}
|
|
945
|
-
async _getCollection(collectionId, query = {}, options = {}) {
|
|
946
942
|
const normalizedQuery = normalizeQuery(query);
|
|
947
943
|
const {
|
|
948
944
|
revalidate = this.defaultRevalidate,
|
|
@@ -952,20 +948,13 @@ var ContentEngine = class {
|
|
|
952
948
|
} = options;
|
|
953
949
|
const queryString = buildQueryString(normalizedQuery);
|
|
954
950
|
const cacheKey = `collection:${collectionId}:${queryString}`;
|
|
955
|
-
|
|
956
|
-
cacheKey
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
);
|
|
960
|
-
if (cached) {
|
|
961
|
-
if (this.config.debug) {
|
|
962
|
-
console.log(
|
|
963
|
-
`[NexusHub] \u26A1 Served collection '${collectionId}' from memory cache.`
|
|
964
|
-
);
|
|
951
|
+
if (!this.isServer && !forceRefresh && this.cacheStrategy === "memory") {
|
|
952
|
+
const cached = this.memoryCache.get(cacheKey);
|
|
953
|
+
if (cached && this.isCacheValid(cached.metadata)) {
|
|
954
|
+
return includeMetadata ? cached : cached.data;
|
|
965
955
|
}
|
|
966
|
-
return includeMetadata ? cached : cached.data;
|
|
967
956
|
}
|
|
968
|
-
if (!forceRefresh) {
|
|
957
|
+
if (!forceRefresh && process.env.NODE_ENV === "development") {
|
|
969
958
|
try {
|
|
970
959
|
const localCollection = await this.localCache.getCollection(collectionId);
|
|
971
960
|
if (localCollection) {
|
|
@@ -973,21 +962,14 @@ var ContentEngine = class {
|
|
|
973
962
|
localCollection,
|
|
974
963
|
normalizedQuery
|
|
975
964
|
);
|
|
976
|
-
if (this.config.debug) {
|
|
977
|
-
console.log(
|
|
978
|
-
`[NexusHub] \u{1F4C1} Compiled collection '${collectionId}' from local offline binaries.`
|
|
979
|
-
);
|
|
980
|
-
}
|
|
981
|
-
this.memoryCache.set(cacheKey, result, {
|
|
982
|
-
tags: [...tags, CacheTags.collection(collectionId)],
|
|
983
|
-
revalidate: revalidate === false ? void 0 : revalidate
|
|
984
|
-
});
|
|
985
965
|
return result;
|
|
986
966
|
}
|
|
987
967
|
} catch {
|
|
988
968
|
}
|
|
989
969
|
}
|
|
990
|
-
if (this.circuitBreaker.isOpen())
|
|
970
|
+
if (this.circuitBreaker.isOpen()) {
|
|
971
|
+
throw new Error("[NexusHub] Circuit open. API is unavailable.");
|
|
972
|
+
}
|
|
991
973
|
try {
|
|
992
974
|
await this.rateLimiter.checkLimit();
|
|
993
975
|
const result = await this.backoff.execute(async () => {
|
|
@@ -996,19 +978,25 @@ var ContentEngine = class {
|
|
|
996
978
|
normalizedQuery,
|
|
997
979
|
cacheKey,
|
|
998
980
|
tags,
|
|
999
|
-
revalidate
|
|
981
|
+
revalidate,
|
|
982
|
+
forceRefresh
|
|
1000
983
|
);
|
|
1001
984
|
});
|
|
1002
985
|
this.circuitBreaker.recordSuccess();
|
|
1003
986
|
return includeMetadata ? result : result.data;
|
|
1004
987
|
} catch (error) {
|
|
1005
988
|
this.circuitBreaker.recordFailure();
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
989
|
+
if (process.env.NODE_ENV === "development") {
|
|
990
|
+
try {
|
|
991
|
+
const localCollection = await this.localCache.getCollection(collectionId);
|
|
992
|
+
if (localCollection) {
|
|
993
|
+
return this.applyLocalQuery(
|
|
994
|
+
localCollection,
|
|
995
|
+
normalizedQuery
|
|
996
|
+
);
|
|
997
|
+
}
|
|
998
|
+
} catch {
|
|
999
|
+
}
|
|
1012
1000
|
}
|
|
1013
1001
|
throw this.normalizeError(
|
|
1014
1002
|
error,
|
|
@@ -1016,9 +1004,6 @@ var ContentEngine = class {
|
|
|
1016
1004
|
);
|
|
1017
1005
|
}
|
|
1018
1006
|
}
|
|
1019
|
-
/**
|
|
1020
|
-
* Fetch Global Settings
|
|
1021
|
-
*/
|
|
1022
1007
|
async getGlobals(options = {}) {
|
|
1023
1008
|
const {
|
|
1024
1009
|
include = [],
|
|
@@ -1026,28 +1011,16 @@ var ContentEngine = class {
|
|
|
1026
1011
|
forceRefresh = false
|
|
1027
1012
|
} = options;
|
|
1028
1013
|
const cacheKey = `globals:${include.join(",")}`;
|
|
1029
|
-
if (!forceRefresh && this.cacheStrategy === "memory") {
|
|
1014
|
+
if (!this.isServer && !forceRefresh && this.cacheStrategy === "memory") {
|
|
1030
1015
|
const cached = this.memoryCache.get(cacheKey);
|
|
1031
1016
|
if (cached && this.isCacheValid(cached.metadata)) {
|
|
1032
|
-
if (this.config.debug)
|
|
1033
|
-
console.log("[NexusHub] \u26A1 Served globals from memory cache.");
|
|
1034
1017
|
return cached.data;
|
|
1035
1018
|
}
|
|
1036
1019
|
}
|
|
1037
|
-
if (!forceRefresh &&
|
|
1038
|
-
const cached = this.browserCache.get(cacheKey);
|
|
1039
|
-
if (cached) {
|
|
1040
|
-
return cached;
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1043
|
-
if (!forceRefresh) {
|
|
1020
|
+
if (!forceRefresh && process.env.NODE_ENV === "development") {
|
|
1044
1021
|
try {
|
|
1045
1022
|
const localGlobals = await this.localCache.getGlobals();
|
|
1046
1023
|
if (localGlobals) {
|
|
1047
|
-
this.memoryCache.set(cacheKey, localGlobals, {
|
|
1048
|
-
tags: [CacheTags.global],
|
|
1049
|
-
revalidate: revalidate === false ? void 0 : revalidate
|
|
1050
|
-
});
|
|
1051
1024
|
return localGlobals;
|
|
1052
1025
|
}
|
|
1053
1026
|
} catch {
|
|
@@ -1066,80 +1039,79 @@ var ContentEngine = class {
|
|
|
1066
1039
|
method: "GET",
|
|
1067
1040
|
headers: this.getHeaders(),
|
|
1068
1041
|
tags: fetchTags,
|
|
1069
|
-
revalidate
|
|
1042
|
+
revalidate,
|
|
1043
|
+
forceRefresh
|
|
1070
1044
|
});
|
|
1071
1045
|
if (!res.ok) {
|
|
1072
|
-
if (res.status === 408) {
|
|
1073
|
-
throw new Error("Request timeout");
|
|
1074
|
-
}
|
|
1075
1046
|
throw new Error(`API Error ${res.status}: ${res.statusText}`);
|
|
1076
1047
|
}
|
|
1077
1048
|
const json = await res.json();
|
|
1078
1049
|
const data = json.data || json;
|
|
1079
|
-
this.
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1050
|
+
if (!this.isServer) {
|
|
1051
|
+
this.writeCache(cacheKey, data, {
|
|
1052
|
+
revalidate,
|
|
1053
|
+
tags: fetchTags
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1083
1056
|
return data;
|
|
1084
1057
|
} catch (error) {
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1058
|
+
if (process.env.NODE_ENV === "development") {
|
|
1059
|
+
try {
|
|
1060
|
+
const localGlobals = await this.localCache.getGlobals();
|
|
1061
|
+
if (localGlobals) {
|
|
1062
|
+
return localGlobals;
|
|
1063
|
+
}
|
|
1064
|
+
} catch {
|
|
1065
|
+
}
|
|
1089
1066
|
}
|
|
1090
1067
|
throw this.normalizeError(error, "Failed to fetch globals");
|
|
1091
1068
|
}
|
|
1092
1069
|
}
|
|
1093
|
-
/**
|
|
1094
|
-
* Get a single item from a collection (Uses Request Batching)
|
|
1095
|
-
*/
|
|
1096
1070
|
async getItem(collectionId, itemId, options = {}) {
|
|
1071
|
+
const { forceRefresh = false } = options;
|
|
1097
1072
|
const cacheKey = `item:${collectionId}:${itemId}`;
|
|
1098
|
-
if (this.cacheStrategy === "memory") {
|
|
1073
|
+
if (!this.isServer && !forceRefresh && this.cacheStrategy === "memory") {
|
|
1099
1074
|
const cached = this.memoryCache.get(cacheKey);
|
|
1100
1075
|
if (cached && this.isCacheValid(cached.metadata)) {
|
|
1101
|
-
if (this.config.debug) {
|
|
1102
|
-
console.log(`[NexusHub] \u26A1 Served item '${itemId}' from cache.`);
|
|
1103
|
-
}
|
|
1104
1076
|
return cached.data;
|
|
1105
1077
|
}
|
|
1106
1078
|
}
|
|
1107
|
-
return this.requestBatcher.schedule(
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
params
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
const fetchTags = [
|
|
1114
|
-
CacheTags.project(this.config.projectId),
|
|
1115
|
-
CacheTags.collection(collectionId),
|
|
1116
|
-
`item_${itemId}`,
|
|
1117
|
-
...options.tags || []
|
|
1118
|
-
];
|
|
1119
|
-
const res = await this.fetchWithTimeout(url, {
|
|
1120
|
-
method: "GET",
|
|
1121
|
-
headers: this.getHeaders(),
|
|
1122
|
-
tags: fetchTags,
|
|
1123
|
-
revalidate: options.revalidate ?? this.defaultRevalidate
|
|
1124
|
-
});
|
|
1125
|
-
if (!res.ok) {
|
|
1126
|
-
if (res.status === 408) {
|
|
1127
|
-
throw new Error("Request timeout");
|
|
1079
|
+
return this.requestBatcher.schedule(
|
|
1080
|
+
forceRefresh ? `${cacheKey}:refresh:${Date.now()}` : cacheKey,
|
|
1081
|
+
async () => {
|
|
1082
|
+
const params = new URLSearchParams();
|
|
1083
|
+
if (options.include?.length) {
|
|
1084
|
+
params.append("include", options.include.join(","));
|
|
1128
1085
|
}
|
|
1129
|
-
|
|
1086
|
+
const url = `${this.config.apiUrl}/content/${this.config.projectId}/collection/${collectionId}/${itemId}?${params}`;
|
|
1087
|
+
const fetchTags = [
|
|
1088
|
+
CacheTags.project(this.config.projectId),
|
|
1089
|
+
CacheTags.collection(collectionId),
|
|
1090
|
+
`item_${itemId}`,
|
|
1091
|
+
...options.tags || []
|
|
1092
|
+
];
|
|
1093
|
+
const res = await this.fetchWithTimeout(url, {
|
|
1094
|
+
method: "GET",
|
|
1095
|
+
headers: this.getHeaders(),
|
|
1096
|
+
tags: fetchTags,
|
|
1097
|
+
revalidate: options.revalidate ?? this.defaultRevalidate,
|
|
1098
|
+
forceRefresh
|
|
1099
|
+
});
|
|
1100
|
+
if (!res.ok) {
|
|
1101
|
+
throw new Error(`API Error ${res.status}: ${res.statusText}`);
|
|
1102
|
+
}
|
|
1103
|
+
const json = await res.json();
|
|
1104
|
+
const data = json.data || json;
|
|
1105
|
+
if (!this.isServer) {
|
|
1106
|
+
this.writeCache(cacheKey, data, {
|
|
1107
|
+
revalidate: options.revalidate ?? this.defaultRevalidate,
|
|
1108
|
+
tags: fetchTags
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
1111
|
+
return data;
|
|
1130
1112
|
}
|
|
1131
|
-
|
|
1132
|
-
const data = json.data || json;
|
|
1133
|
-
this.writeCache(cacheKey, data, {
|
|
1134
|
-
revalidate: options.revalidate ?? this.defaultRevalidate,
|
|
1135
|
-
tags: fetchTags
|
|
1136
|
-
});
|
|
1137
|
-
return data;
|
|
1138
|
-
});
|
|
1113
|
+
);
|
|
1139
1114
|
}
|
|
1140
|
-
/**
|
|
1141
|
-
* Search across collections
|
|
1142
|
-
*/
|
|
1143
1115
|
async search(query, options = {}) {
|
|
1144
1116
|
const params = new URLSearchParams({
|
|
1145
1117
|
q: query,
|
|
@@ -1152,65 +1124,61 @@ var ContentEngine = class {
|
|
|
1152
1124
|
params.append("fields", options.fields.join(","));
|
|
1153
1125
|
}
|
|
1154
1126
|
const url = `${this.config.apiUrl}/content/${this.config.projectId}/search?${params}`;
|
|
1127
|
+
const fetchTags = [
|
|
1128
|
+
CacheTags.project(this.config.projectId),
|
|
1129
|
+
...(options.collections || []).map(
|
|
1130
|
+
(collection) => CacheTags.collection(collection)
|
|
1131
|
+
)
|
|
1132
|
+
];
|
|
1155
1133
|
const res = await this.fetchWithTimeout(url, {
|
|
1156
1134
|
method: "GET",
|
|
1157
|
-
headers: this.getHeaders()
|
|
1135
|
+
headers: this.getHeaders(),
|
|
1136
|
+
tags: fetchTags,
|
|
1137
|
+
revalidate: options.revalidate ?? this.defaultRevalidate,
|
|
1138
|
+
forceRefresh: options.forceRefresh ?? false
|
|
1158
1139
|
});
|
|
1159
1140
|
if (!res.ok) {
|
|
1160
|
-
if (res.status === 408) {
|
|
1161
|
-
throw new Error("Request timeout");
|
|
1162
|
-
}
|
|
1163
1141
|
throw new Error(`API Error ${res.status}: ${res.statusText}`);
|
|
1164
1142
|
}
|
|
1165
1143
|
return await res.json();
|
|
1166
1144
|
}
|
|
1167
|
-
/**
|
|
1168
|
-
* Prefetch content
|
|
1169
|
-
*/
|
|
1170
1145
|
async prefetch(urls) {
|
|
1171
1146
|
if (typeof window !== "undefined" && "requestIdleCallback" in window) {
|
|
1172
1147
|
requestIdleCallback(async () => {
|
|
1173
1148
|
await Promise.allSettled(
|
|
1174
|
-
urls.map(
|
|
1149
|
+
urls.map(
|
|
1150
|
+
(url) => fetch(url, {
|
|
1151
|
+
priority: "low"
|
|
1152
|
+
})
|
|
1153
|
+
)
|
|
1175
1154
|
);
|
|
1176
1155
|
});
|
|
1177
1156
|
}
|
|
1178
1157
|
}
|
|
1179
|
-
/**
|
|
1180
|
-
* Robust Real-time Subscriptions (SSE) with Auto-Reconnect
|
|
1181
|
-
*/
|
|
1182
1158
|
subscribeToUpdates(callback) {
|
|
1183
1159
|
if (this.isServer || typeof EventSource === "undefined") {
|
|
1184
|
-
if (this.config.debug) {
|
|
1185
|
-
console.warn(
|
|
1186
|
-
"[NexusHub] subscribeToUpdates() called in a non-browser environment."
|
|
1187
|
-
);
|
|
1188
|
-
}
|
|
1189
1160
|
return () => {
|
|
1190
1161
|
};
|
|
1191
1162
|
}
|
|
1192
1163
|
if (!this.config.apiKey) {
|
|
1193
|
-
|
|
1164
|
+
return () => {
|
|
1165
|
+
};
|
|
1194
1166
|
}
|
|
1195
1167
|
let eventSource = null;
|
|
1196
1168
|
let retryCount = 0;
|
|
1197
1169
|
let isClosed = false;
|
|
1198
|
-
const MAX_RETRY_DELAY_MS = 3e4;
|
|
1199
1170
|
const connect = () => {
|
|
1200
1171
|
if (isClosed) return;
|
|
1201
1172
|
const url = `${this.config.apiUrl}/content/${this.config.projectId}/updates?key=${this.config.apiKey ?? ""}`;
|
|
1202
1173
|
eventSource = new EventSource(url);
|
|
1203
1174
|
eventSource.onopen = () => {
|
|
1204
1175
|
retryCount = 0;
|
|
1205
|
-
if (this.config.debug)
|
|
1206
|
-
console.log("[NexusHub] \u{1F7E2} Real-time stream connected");
|
|
1207
1176
|
};
|
|
1208
1177
|
eventSource.onmessage = (event) => {
|
|
1209
1178
|
let data;
|
|
1210
1179
|
try {
|
|
1211
1180
|
data = JSON.parse(event.data);
|
|
1212
|
-
} catch
|
|
1213
|
-
console.error("[NexusHub] SSE Parse Error", e);
|
|
1181
|
+
} catch {
|
|
1214
1182
|
return;
|
|
1215
1183
|
}
|
|
1216
1184
|
if (!data || typeof data.type !== "string") {
|
|
@@ -1222,6 +1190,9 @@ var ContentEngine = class {
|
|
|
1222
1190
|
if (data.type === "collection.updated" && data.collectionId) {
|
|
1223
1191
|
this.invalidateCache([CacheTags.collection(data.collectionId)]);
|
|
1224
1192
|
}
|
|
1193
|
+
if (data.type === "globals.updated") {
|
|
1194
|
+
this.invalidateCache([CacheTags.global]);
|
|
1195
|
+
}
|
|
1225
1196
|
if (data.type === "schema.updated") {
|
|
1226
1197
|
this.clearCache();
|
|
1227
1198
|
}
|
|
@@ -1230,16 +1201,8 @@ var ContentEngine = class {
|
|
|
1230
1201
|
eventSource.onerror = () => {
|
|
1231
1202
|
eventSource?.close();
|
|
1232
1203
|
if (isClosed) return;
|
|
1233
|
-
const timeout = Math.min(
|
|
1234
|
-
1e3 * Math.pow(2, retryCount),
|
|
1235
|
-
MAX_RETRY_DELAY_MS
|
|
1236
|
-
);
|
|
1204
|
+
const timeout = Math.min(1e3 * Math.pow(2, retryCount), 3e4);
|
|
1237
1205
|
retryCount++;
|
|
1238
|
-
if (this.config.debug) {
|
|
1239
|
-
console.warn(
|
|
1240
|
-
`[NexusHub] \u{1F534} Stream disconnected. Reconnecting in ${timeout}ms...`
|
|
1241
|
-
);
|
|
1242
|
-
}
|
|
1243
1206
|
setTimeout(connect, timeout);
|
|
1244
1207
|
};
|
|
1245
1208
|
};
|
|
@@ -1249,70 +1212,19 @@ var ContentEngine = class {
|
|
|
1249
1212
|
eventSource?.close();
|
|
1250
1213
|
};
|
|
1251
1214
|
}
|
|
1252
|
-
// --- CACHE MANAGEMENT ---
|
|
1253
|
-
async checkCaches(key, forceRefresh, includeMetadata) {
|
|
1254
|
-
if (forceRefresh) return null;
|
|
1255
|
-
if (this.cacheStrategy === "memory") {
|
|
1256
|
-
const cached = this.memoryCache.get(key);
|
|
1257
|
-
if (cached && this.isCacheValid(cached.metadata)) {
|
|
1258
|
-
return cached;
|
|
1259
|
-
}
|
|
1260
|
-
}
|
|
1261
|
-
if (!this.isServer && this.cacheStrategy === "localStorage" && this.browserCache) {
|
|
1262
|
-
const cached = this.browserCache.get(key);
|
|
1263
|
-
if (cached) {
|
|
1264
|
-
return {
|
|
1265
|
-
data: cached,
|
|
1266
|
-
metadata: {
|
|
1267
|
-
timestamp: 0,
|
|
1268
|
-
expiresAt: Infinity,
|
|
1269
|
-
tags: [],
|
|
1270
|
-
etag: void 0
|
|
1271
|
-
}
|
|
1272
|
-
};
|
|
1273
|
-
}
|
|
1274
|
-
}
|
|
1275
|
-
if (key.startsWith("page:")) {
|
|
1276
|
-
const slug = key.split(":")[1];
|
|
1277
|
-
try {
|
|
1278
|
-
const local = await this.localCache.getPage(slug);
|
|
1279
|
-
if (local && (process.env.NODE_ENV === "development" || !this.config.apiKey)) {
|
|
1280
|
-
return {
|
|
1281
|
-
data: local,
|
|
1282
|
-
metadata: {
|
|
1283
|
-
timestamp: Date.now(),
|
|
1284
|
-
expiresAt: Infinity,
|
|
1285
|
-
tags: [
|
|
1286
|
-
CacheTags.content(slug),
|
|
1287
|
-
CacheTags.project(this.config.projectId)
|
|
1288
|
-
],
|
|
1289
|
-
etag: void 0
|
|
1290
|
-
}
|
|
1291
|
-
};
|
|
1292
|
-
}
|
|
1293
|
-
} catch {
|
|
1294
|
-
}
|
|
1295
|
-
}
|
|
1296
|
-
return null;
|
|
1297
|
-
}
|
|
1298
1215
|
writeCache(key, data, options) {
|
|
1299
|
-
const
|
|
1216
|
+
const ttlSeconds = options.revalidate === false ? 60 : Math.max(1, options.revalidate);
|
|
1300
1217
|
if (this.cacheStrategy === "memory") {
|
|
1301
1218
|
this.memoryCache.set(key, data, {
|
|
1302
1219
|
tags: options.tags,
|
|
1303
|
-
|
|
1220
|
+
ttl: ttlSeconds * 1e3
|
|
1304
1221
|
});
|
|
1305
1222
|
}
|
|
1306
|
-
if (!this.isServer && this.cacheStrategy === "localStorage" && this.browserCache) {
|
|
1307
|
-
this.browserCache.set(key, data, numericRevalidate * 1e3);
|
|
1308
|
-
}
|
|
1309
1223
|
}
|
|
1310
1224
|
invalidateCache(tags) {
|
|
1311
1225
|
this.memoryCache.invalidateByTags(tags);
|
|
1312
|
-
if (this.
|
|
1313
|
-
|
|
1314
|
-
`[NexusHub] \u267B\uFE0F Invalidated cache for tags: ${tags.join(", ")}`
|
|
1315
|
-
);
|
|
1226
|
+
if (this.browserCache) {
|
|
1227
|
+
this.browserCache.clear();
|
|
1316
1228
|
}
|
|
1317
1229
|
}
|
|
1318
1230
|
clearCache() {
|
|
@@ -1320,24 +1232,17 @@ var ContentEngine = class {
|
|
|
1320
1232
|
if (this.browserCache) {
|
|
1321
1233
|
this.browserCache.clear();
|
|
1322
1234
|
}
|
|
1323
|
-
if (this.config.debug) {
|
|
1324
|
-
console.log("[NexusHub] \u{1F9F9} Cleared all caches");
|
|
1325
|
-
}
|
|
1326
1235
|
}
|
|
1327
1236
|
getCacheStats() {
|
|
1328
|
-
|
|
1237
|
+
return {
|
|
1329
1238
|
memory: this.memoryCache.getStats(),
|
|
1330
|
-
local: {
|
|
1239
|
+
local: {
|
|
1240
|
+
loaded: this.localCache.isLoaded()
|
|
1241
|
+
}
|
|
1331
1242
|
};
|
|
1332
|
-
if (this.browserCache) {
|
|
1333
|
-
stats.browser = { size: 0 };
|
|
1334
|
-
}
|
|
1335
|
-
return stats;
|
|
1336
1243
|
}
|
|
1337
|
-
// --- REQUEST METHODS ---
|
|
1338
1244
|
async fetchPage(slug, cacheKey, tags, revalidate, forceRefresh) {
|
|
1339
1245
|
const url = `${this.config.apiUrl}/content/${this.config.projectId}/page/${slug}`;
|
|
1340
|
-
const expiresAt = revalidate === false ? Infinity : Date.now() + revalidate * 1e3;
|
|
1341
1246
|
const fetchTags = [
|
|
1342
1247
|
CacheTags.project(this.config.projectId),
|
|
1343
1248
|
CacheTags.content(slug),
|
|
@@ -1347,13 +1252,12 @@ var ContentEngine = class {
|
|
|
1347
1252
|
method: "GET",
|
|
1348
1253
|
headers: this.getHeaders(),
|
|
1349
1254
|
tags: fetchTags,
|
|
1350
|
-
revalidate
|
|
1255
|
+
revalidate,
|
|
1256
|
+
forceRefresh
|
|
1351
1257
|
});
|
|
1352
1258
|
if (!res.ok) {
|
|
1353
1259
|
if (res.status === 404) {
|
|
1354
|
-
throw new Error(
|
|
1355
|
-
`Page '${slug}' not found. Check your Dashboard or Seed data.`
|
|
1356
|
-
);
|
|
1260
|
+
throw new Error(`Page '${slug}' not found.`);
|
|
1357
1261
|
}
|
|
1358
1262
|
if (res.status === 408) {
|
|
1359
1263
|
throw new Error("Request timeout");
|
|
@@ -1367,17 +1271,19 @@ var ContentEngine = class {
|
|
|
1367
1271
|
metadata: {
|
|
1368
1272
|
timestamp: Date.now(),
|
|
1369
1273
|
etag: etag || void 0,
|
|
1370
|
-
expiresAt,
|
|
1274
|
+
expiresAt: revalidate === false ? Infinity : Date.now() + revalidate * 1e3,
|
|
1371
1275
|
tags: fetchTags
|
|
1372
1276
|
}
|
|
1373
1277
|
};
|
|
1374
|
-
this.
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1278
|
+
if (!this.isServer) {
|
|
1279
|
+
this.writeCache(cacheKey, cacheEntry.data, {
|
|
1280
|
+
revalidate,
|
|
1281
|
+
tags: fetchTags
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1378
1284
|
return cacheEntry;
|
|
1379
1285
|
}
|
|
1380
|
-
async fetchCollection(collectionId, query, cacheKey, tags, revalidate) {
|
|
1286
|
+
async fetchCollection(collectionId, query, cacheKey, tags, revalidate, forceRefresh) {
|
|
1381
1287
|
const params = buildQueryString(query);
|
|
1382
1288
|
const url = `${this.config.apiUrl}/content/${this.config.projectId}/collection/${collectionId}?${params}`;
|
|
1383
1289
|
const fetchTags = [
|
|
@@ -1389,7 +1295,8 @@ var ContentEngine = class {
|
|
|
1389
1295
|
method: "GET",
|
|
1390
1296
|
headers: this.getHeaders(),
|
|
1391
1297
|
tags: fetchTags,
|
|
1392
|
-
revalidate
|
|
1298
|
+
revalidate,
|
|
1299
|
+
forceRefresh
|
|
1393
1300
|
});
|
|
1394
1301
|
if (!res.ok) {
|
|
1395
1302
|
if (res.status === 408) {
|
|
@@ -1399,23 +1306,23 @@ var ContentEngine = class {
|
|
|
1399
1306
|
}
|
|
1400
1307
|
const json = await res.json();
|
|
1401
1308
|
const etag = res.headers.get("etag");
|
|
1402
|
-
const expiresAt = revalidate === false ? Infinity : Date.now() + revalidate * 1e3;
|
|
1403
1309
|
const cacheEntry = {
|
|
1404
1310
|
data: json,
|
|
1405
1311
|
metadata: {
|
|
1406
1312
|
timestamp: Date.now(),
|
|
1407
1313
|
etag: etag || void 0,
|
|
1408
|
-
expiresAt,
|
|
1314
|
+
expiresAt: revalidate === false ? Infinity : Date.now() + revalidate * 1e3,
|
|
1409
1315
|
tags: fetchTags
|
|
1410
1316
|
}
|
|
1411
1317
|
};
|
|
1412
|
-
this.
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1318
|
+
if (!this.isServer) {
|
|
1319
|
+
this.writeCache(cacheKey, cacheEntry.data, {
|
|
1320
|
+
revalidate,
|
|
1321
|
+
tags: fetchTags
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1416
1324
|
return cacheEntry;
|
|
1417
1325
|
}
|
|
1418
|
-
// --- HELPER METHODS ---
|
|
1419
1326
|
applyLocalQuery(items, query) {
|
|
1420
1327
|
let filtered = [...items];
|
|
1421
1328
|
if (query.search) {
|
|
@@ -1428,7 +1335,9 @@ var ContentEngine = class {
|
|
|
1428
1335
|
filtered = filtered.filter((item) => {
|
|
1429
1336
|
return Object.entries(query.filter).every(([key, value]) => {
|
|
1430
1337
|
const itemValue = item[key];
|
|
1431
|
-
if (itemValue === void 0)
|
|
1338
|
+
if (itemValue === void 0) {
|
|
1339
|
+
return false;
|
|
1340
|
+
}
|
|
1432
1341
|
if (Array.isArray(value)) {
|
|
1433
1342
|
return value.includes(itemValue);
|
|
1434
1343
|
}
|
|
@@ -1441,8 +1350,12 @@ var ContentEngine = class {
|
|
|
1441
1350
|
const aVal = a[query.sort];
|
|
1442
1351
|
const bVal = b[query.sort];
|
|
1443
1352
|
const order = query.order === "asc" ? 1 : -1;
|
|
1444
|
-
if (aVal < bVal)
|
|
1445
|
-
|
|
1353
|
+
if (aVal < bVal) {
|
|
1354
|
+
return -1 * order;
|
|
1355
|
+
}
|
|
1356
|
+
if (aVal > bVal) {
|
|
1357
|
+
return 1 * order;
|
|
1358
|
+
}
|
|
1446
1359
|
return 0;
|
|
1447
1360
|
});
|
|
1448
1361
|
}
|
|
@@ -1466,15 +1379,18 @@ var ContentEngine = class {
|
|
|
1466
1379
|
timeout = this.config.timeout || 1e4,
|
|
1467
1380
|
tags = [],
|
|
1468
1381
|
revalidate,
|
|
1382
|
+
forceRefresh = false,
|
|
1469
1383
|
...fetchOptions
|
|
1470
1384
|
} = options;
|
|
1471
1385
|
const controller = new AbortController();
|
|
1472
1386
|
const id = setTimeout(() => controller.abort(), timeout);
|
|
1473
1387
|
const nextConfig = this.isServer ? {
|
|
1474
|
-
cache: revalidate === 0 ? "no-store" : "force-cache",
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1388
|
+
cache: forceRefresh || revalidate === 0 ? "no-store" : "force-cache",
|
|
1389
|
+
...forceRefresh ? {} : {
|
|
1390
|
+
next: {
|
|
1391
|
+
tags,
|
|
1392
|
+
revalidate: revalidate === false ? false : revalidate
|
|
1393
|
+
}
|
|
1478
1394
|
}
|
|
1479
1395
|
} : {};
|
|
1480
1396
|
try {
|
|
@@ -1493,9 +1409,9 @@ var ContentEngine = class {
|
|
|
1493
1409
|
getHeaders() {
|
|
1494
1410
|
const headers = {
|
|
1495
1411
|
"Content-Type": "application/json",
|
|
1496
|
-
"X-
|
|
1497
|
-
"X-
|
|
1498
|
-
"X-
|
|
1412
|
+
"X-GN-Apex-Client": `gnapex-sdk/${this.config.sdkVersion ?? "1.1.6"}`,
|
|
1413
|
+
"X-GN-Apex-Request-ID": typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : `nx_${Date.now()}_${Math.random().toString(36).slice(2)}`,
|
|
1414
|
+
"X-GN-Apex-Project": this.config.projectId
|
|
1499
1415
|
};
|
|
1500
1416
|
if (this.config.apiKey) {
|
|
1501
1417
|
headers["Authorization"] = `Bearer ${this.config.apiKey}`;
|
|
@@ -1505,10 +1421,6 @@ var ContentEngine = class {
|
|
|
1505
1421
|
isCacheValid(metadata) {
|
|
1506
1422
|
return Date.now() < metadata.expiresAt;
|
|
1507
1423
|
}
|
|
1508
|
-
/**
|
|
1509
|
-
* 🛡️ FIX: Normalizes timeout errors (AbortError, 408, or Timeout messages)
|
|
1510
|
-
* into clean, standardized "Request timeout" errors.
|
|
1511
|
-
*/
|
|
1512
1424
|
normalizeError(error, context) {
|
|
1513
1425
|
if (error instanceof Error) {
|
|
1514
1426
|
const msg = error.message.toLowerCase();
|
|
@@ -1523,14 +1435,7 @@ var ContentEngine = class {
|
|
|
1523
1435
|
}
|
|
1524
1436
|
return new Error(`${context}: ${String(error)}`);
|
|
1525
1437
|
}
|
|
1526
|
-
cancelRequests() {
|
|
1527
|
-
if (this.abortController) {
|
|
1528
|
-
this.abortController.abort();
|
|
1529
|
-
this.abortController = new AbortController();
|
|
1530
|
-
}
|
|
1531
|
-
}
|
|
1532
1438
|
cleanup() {
|
|
1533
|
-
this.cancelRequests();
|
|
1534
1439
|
if (!this.isServer) {
|
|
1535
1440
|
window.removeEventListener("beforeunload", this.cleanup.bind(this));
|
|
1536
1441
|
}
|