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