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