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