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