@nexushub/client 0.7.7 → 0.7.9
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 +423 -107
- package/dist/index.cjs +109 -59
- package/dist/index.d.cts +21 -12
- package/dist/index.d.ts +21 -12
- package/dist/index.js +109 -59
- package/dist/react.cjs +109 -59
- package/dist/react.d.cts +21 -12
- package/dist/react.d.ts +21 -12
- package/dist/react.js +109 -59
- package/package.json +1 -1
package/dist/react.d.ts
CHANGED
|
@@ -85,6 +85,7 @@ declare class ContentEngine {
|
|
|
85
85
|
include?: string[];
|
|
86
86
|
revalidate?: number | false;
|
|
87
87
|
forceRefresh?: boolean;
|
|
88
|
+
tags?: string[];
|
|
88
89
|
}): Promise<T>;
|
|
89
90
|
/**
|
|
90
91
|
* Get a single item from a collection (Uses Request Batching)
|
|
@@ -112,24 +113,32 @@ declare class ContentEngine {
|
|
|
112
113
|
prefetch(urls: string[]): Promise<void>;
|
|
113
114
|
/**
|
|
114
115
|
* Robust Real-time Subscriptions (SSE) with Auto-Reconnect
|
|
116
|
+
*
|
|
117
|
+
* ⚠️ SCOPE — READ BEFORE RELYING ON THIS FOR "instant" UPDATES:
|
|
118
|
+
* This connects from the BROWSER TAB it's called in, and on message it
|
|
119
|
+
* only clears THIS SDK INSTANCE's in-memory `memoryCache` (via
|
|
120
|
+
* invalidateCache below) in THAT browser tab's JS heap. In a typical
|
|
121
|
+
* Next.js deployment — and Cloudflare specifically, which is stateless
|
|
122
|
+
* per-request at the edge — that is a different process/isolate than the
|
|
123
|
+
* one that will render the NEXT server request for this content. So:
|
|
124
|
+
* - ✅ Useful for: a client component that reads from `nexus.content`
|
|
125
|
+
* directly in the browser and re-renders in place without a page
|
|
126
|
+
* navigation (e.g. a live-updating dashboard widget).
|
|
127
|
+
* - ❌ NOT sufficient for: "user A edits content in the CMS, user B's
|
|
128
|
+
* already-loaded page updates" via a server-rendered page. That
|
|
129
|
+
* requires the backend's /api/revalidate webhook (see
|
|
130
|
+
* content.service.ts `pingNextRevalidateWebhook`) to have actually
|
|
131
|
+
* cleared the *server's* Data Cache, so the NEXT navigation or
|
|
132
|
+
* server request picks up fresh data. This SSE channel does not
|
|
133
|
+
* replace that — it's a complementary, browser-local optimization.
|
|
134
|
+
* If your symptom was "stale content after editing," fix the webhook
|
|
135
|
+
* wiring first; treat this method as an enhancement layered on top.
|
|
115
136
|
*/
|
|
116
137
|
subscribeToUpdates(callback: (data: any) => void): () => void;
|
|
117
|
-
/**
|
|
118
|
-
* Check all caches in order of speed
|
|
119
|
-
*/
|
|
120
138
|
private checkCaches;
|
|
121
139
|
private writeCache;
|
|
122
|
-
/**
|
|
123
|
-
* Invalidate cache by tags
|
|
124
|
-
*/
|
|
125
140
|
invalidateCache(tags: string[]): void;
|
|
126
|
-
/**
|
|
127
|
-
* Clear all caches
|
|
128
|
-
*/
|
|
129
141
|
clearCache(): void;
|
|
130
|
-
/**
|
|
131
|
-
* Get cache statistics
|
|
132
|
-
*/
|
|
133
142
|
getCacheStats(): {
|
|
134
143
|
memory: {
|
|
135
144
|
size: number;
|
package/dist/react.js
CHANGED
|
@@ -535,7 +535,7 @@ var ContentEngine = class {
|
|
|
535
535
|
});
|
|
536
536
|
this.requestBatcher = new RequestBatcher(20, 50);
|
|
537
537
|
this.circuitBreaker = new CircuitBreaker();
|
|
538
|
-
this.defaultRevalidate = config.revalidateTime
|
|
538
|
+
this.defaultRevalidate = config.revalidateTime ?? false;
|
|
539
539
|
this.cacheStrategy = config.cacheStrategy || "memory";
|
|
540
540
|
if (!this.isServer) {
|
|
541
541
|
window.addEventListener("beforeunload", this.cleanup.bind(this));
|
|
@@ -739,9 +739,16 @@ var ContentEngine = class {
|
|
|
739
739
|
await this.rateLimiter.checkLimit();
|
|
740
740
|
const url = `${this.config.apiUrl}/content/${this.config.projectId}/globals`;
|
|
741
741
|
const params = include.length > 0 ? `?include=${include.join(",")}` : "";
|
|
742
|
+
const fetchTags = [
|
|
743
|
+
CacheTags.project(this.config.projectId),
|
|
744
|
+
CacheTags.global,
|
|
745
|
+
...options.tags || []
|
|
746
|
+
];
|
|
742
747
|
const res = await this.fetchWithTimeout(`${url}${params}`, {
|
|
743
748
|
method: "GET",
|
|
744
|
-
headers: this.getHeaders()
|
|
749
|
+
headers: this.getHeaders(),
|
|
750
|
+
tags: fetchTags,
|
|
751
|
+
revalidate
|
|
745
752
|
});
|
|
746
753
|
if (!res.ok) {
|
|
747
754
|
throw new Error(`API Error ${res.status}: ${res.statusText}`);
|
|
@@ -782,9 +789,20 @@ var ContentEngine = class {
|
|
|
782
789
|
params.append("include", options.include.join(","));
|
|
783
790
|
}
|
|
784
791
|
const url = `${this.config.apiUrl}/content/${this.config.projectId}/collection/${collectionId}/${itemId}?${params}`;
|
|
792
|
+
const fetchTags = [
|
|
793
|
+
CacheTags.project(this.config.projectId),
|
|
794
|
+
CacheTags.collection(collectionId),
|
|
795
|
+
`item_${itemId}`,
|
|
796
|
+
...options.tags || []
|
|
797
|
+
];
|
|
785
798
|
const res = await this.fetchWithTimeout(url, {
|
|
786
799
|
method: "GET",
|
|
787
|
-
headers: this.getHeaders()
|
|
800
|
+
headers: this.getHeaders(),
|
|
801
|
+
tags: fetchTags,
|
|
802
|
+
// ?? not || — see the constructor comment on defaultRevalidate for
|
|
803
|
+
// why: an explicit `revalidate: 0` on this call must not be
|
|
804
|
+
// discarded in favor of the engine's default.
|
|
805
|
+
revalidate: options.revalidate ?? this.defaultRevalidate
|
|
788
806
|
});
|
|
789
807
|
if (!res.ok) {
|
|
790
808
|
throw new Error(`API Error ${res.status}: ${res.statusText}`);
|
|
@@ -792,13 +810,8 @@ var ContentEngine = class {
|
|
|
792
810
|
const json = await res.json();
|
|
793
811
|
const data = json.data || json;
|
|
794
812
|
this.writeCache(cacheKey, data, {
|
|
795
|
-
revalidate: options.revalidate
|
|
796
|
-
tags:
|
|
797
|
-
CacheTags.project(this.config.projectId),
|
|
798
|
-
CacheTags.collection(collectionId),
|
|
799
|
-
`item_${itemId}`,
|
|
800
|
-
...options.tags || []
|
|
801
|
-
]
|
|
813
|
+
revalidate: options.revalidate ?? this.defaultRevalidate,
|
|
814
|
+
tags: fetchTags
|
|
802
815
|
});
|
|
803
816
|
return data;
|
|
804
817
|
});
|
|
@@ -841,19 +854,49 @@ var ContentEngine = class {
|
|
|
841
854
|
}
|
|
842
855
|
/**
|
|
843
856
|
* Robust Real-time Subscriptions (SSE) with Auto-Reconnect
|
|
857
|
+
*
|
|
858
|
+
* ⚠️ SCOPE — READ BEFORE RELYING ON THIS FOR "instant" UPDATES:
|
|
859
|
+
* This connects from the BROWSER TAB it's called in, and on message it
|
|
860
|
+
* only clears THIS SDK INSTANCE's in-memory `memoryCache` (via
|
|
861
|
+
* invalidateCache below) in THAT browser tab's JS heap. In a typical
|
|
862
|
+
* Next.js deployment — and Cloudflare specifically, which is stateless
|
|
863
|
+
* per-request at the edge — that is a different process/isolate than the
|
|
864
|
+
* one that will render the NEXT server request for this content. So:
|
|
865
|
+
* - ✅ Useful for: a client component that reads from `nexus.content`
|
|
866
|
+
* directly in the browser and re-renders in place without a page
|
|
867
|
+
* navigation (e.g. a live-updating dashboard widget).
|
|
868
|
+
* - ❌ NOT sufficient for: "user A edits content in the CMS, user B's
|
|
869
|
+
* already-loaded page updates" via a server-rendered page. That
|
|
870
|
+
* requires the backend's /api/revalidate webhook (see
|
|
871
|
+
* content.service.ts `pingNextRevalidateWebhook`) to have actually
|
|
872
|
+
* cleared the *server's* Data Cache, so the NEXT navigation or
|
|
873
|
+
* server request picks up fresh data. This SSE channel does not
|
|
874
|
+
* replace that — it's a complementary, browser-local optimization.
|
|
875
|
+
* If your symptom was "stale content after editing," fix the webhook
|
|
876
|
+
* wiring first; treat this method as an enhancement layered on top.
|
|
844
877
|
*/
|
|
845
878
|
subscribeToUpdates(callback) {
|
|
846
879
|
if (this.isServer || typeof EventSource === "undefined") {
|
|
847
|
-
|
|
880
|
+
if (this.config.debug) {
|
|
881
|
+
console.warn(
|
|
882
|
+
"[NexusHub] subscribeToUpdates() called in a non-browser environment (server render or SSR pass) \u2014 this is expected and safely skipped; EventSource only makes sense client-side."
|
|
883
|
+
);
|
|
884
|
+
}
|
|
848
885
|
return () => {
|
|
849
886
|
};
|
|
850
887
|
}
|
|
888
|
+
if (!this.config.apiKey) {
|
|
889
|
+
console.warn(
|
|
890
|
+
"[NexusHub] subscribeToUpdates(): no apiKey configured, the SSE connection will likely be rejected by the backend. Set NEXT_PUBLIC_NEXUS_KEY."
|
|
891
|
+
);
|
|
892
|
+
}
|
|
851
893
|
let eventSource = null;
|
|
852
894
|
let retryCount = 0;
|
|
853
895
|
let isClosed = false;
|
|
896
|
+
const MAX_RETRY_DELAY_MS = 3e4;
|
|
854
897
|
const connect = () => {
|
|
855
898
|
if (isClosed) return;
|
|
856
|
-
const url = `${this.config.apiUrl}/content/${this.config.projectId}/updates?key=${this.config.apiKey}`;
|
|
899
|
+
const url = `${this.config.apiUrl}/content/${this.config.projectId}/updates?key=${this.config.apiKey ?? ""}`;
|
|
857
900
|
eventSource = new EventSource(url);
|
|
858
901
|
eventSource.onopen = () => {
|
|
859
902
|
retryCount = 0;
|
|
@@ -861,27 +904,40 @@ var ContentEngine = class {
|
|
|
861
904
|
console.log("[NexusHub] \u{1F7E2} Real-time stream connected");
|
|
862
905
|
};
|
|
863
906
|
eventSource.onmessage = (event) => {
|
|
907
|
+
let data;
|
|
864
908
|
try {
|
|
865
|
-
|
|
866
|
-
if (data.type === "content.updated" && data.slug) {
|
|
867
|
-
this.invalidateCache([CacheTags.content(data.slug)]);
|
|
868
|
-
}
|
|
869
|
-
if (data.type === "collection.updated") {
|
|
870
|
-
this.invalidateCache([CacheTags.collection(data.collectionId)]);
|
|
871
|
-
}
|
|
872
|
-
callback(data);
|
|
909
|
+
data = JSON.parse(event.data);
|
|
873
910
|
} catch (e) {
|
|
874
911
|
console.error("[NexusHub] SSE Parse Error", e);
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
if (!data || typeof data.type !== "string") {
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
if (data.type === "content.updated" && data.slug) {
|
|
918
|
+
this.invalidateCache([CacheTags.content(data.slug)]);
|
|
919
|
+
}
|
|
920
|
+
if (data.type === "collection.updated" && data.collectionId) {
|
|
921
|
+
this.invalidateCache([CacheTags.collection(data.collectionId)]);
|
|
922
|
+
}
|
|
923
|
+
if (data.type === "schema.updated") {
|
|
924
|
+
this.clearCache();
|
|
875
925
|
}
|
|
926
|
+
callback(data);
|
|
876
927
|
};
|
|
877
928
|
eventSource.onerror = () => {
|
|
878
929
|
eventSource?.close();
|
|
879
930
|
if (isClosed) return;
|
|
880
|
-
const timeout = Math.min(
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
`[NexusHub] \u{1F534} Stream disconnected. Reconnecting in ${timeout}ms...`
|
|
931
|
+
const timeout = Math.min(
|
|
932
|
+
1e3 * Math.pow(2, retryCount),
|
|
933
|
+
MAX_RETRY_DELAY_MS
|
|
884
934
|
);
|
|
935
|
+
retryCount++;
|
|
936
|
+
if (this.config.debug) {
|
|
937
|
+
console.warn(
|
|
938
|
+
`[NexusHub] \u{1F534} Stream disconnected. Reconnecting in ${timeout}ms...`
|
|
939
|
+
);
|
|
940
|
+
}
|
|
885
941
|
setTimeout(connect, timeout);
|
|
886
942
|
};
|
|
887
943
|
};
|
|
@@ -892,9 +948,6 @@ var ContentEngine = class {
|
|
|
892
948
|
};
|
|
893
949
|
}
|
|
894
950
|
// --- CACHE MANAGEMENT ---
|
|
895
|
-
/**
|
|
896
|
-
* Check all caches in order of speed
|
|
897
|
-
*/
|
|
898
951
|
async checkCaches(key, forceRefresh, includeMetadata) {
|
|
899
952
|
if (forceRefresh) return null;
|
|
900
953
|
if (this.cacheStrategy === "memory") {
|
|
@@ -951,9 +1004,6 @@ var ContentEngine = class {
|
|
|
951
1004
|
this.browserCache.set(key, data, numericRevalidate * 1e3);
|
|
952
1005
|
}
|
|
953
1006
|
}
|
|
954
|
-
/**
|
|
955
|
-
* Invalidate cache by tags
|
|
956
|
-
*/
|
|
957
1007
|
invalidateCache(tags) {
|
|
958
1008
|
this.memoryCache.invalidateByTags(tags);
|
|
959
1009
|
if (this.config.debug) {
|
|
@@ -962,9 +1012,6 @@ var ContentEngine = class {
|
|
|
962
1012
|
);
|
|
963
1013
|
}
|
|
964
1014
|
}
|
|
965
|
-
/**
|
|
966
|
-
* Clear all caches
|
|
967
|
-
*/
|
|
968
1015
|
clearCache() {
|
|
969
1016
|
this.memoryCache.clear();
|
|
970
1017
|
if (this.browserCache) {
|
|
@@ -974,9 +1021,6 @@ var ContentEngine = class {
|
|
|
974
1021
|
console.log("[NexusHub] \u{1F9F9} Cleared all caches");
|
|
975
1022
|
}
|
|
976
1023
|
}
|
|
977
|
-
/**
|
|
978
|
-
* Get cache statistics
|
|
979
|
-
*/
|
|
980
1024
|
getCacheStats() {
|
|
981
1025
|
const stats = {
|
|
982
1026
|
memory: this.memoryCache.getStats(),
|
|
@@ -990,14 +1034,16 @@ var ContentEngine = class {
|
|
|
990
1034
|
// --- REQUEST METHODS ---
|
|
991
1035
|
async fetchPage(slug, cacheKey, tags, revalidate, forceRefresh) {
|
|
992
1036
|
const url = `${this.config.apiUrl}/content/${this.config.projectId}/page/${slug}`;
|
|
993
|
-
if (this.config.debug) {
|
|
994
|
-
console.log(`[NexusHub] \u{1F310} Fetching: ${url}`);
|
|
995
|
-
}
|
|
996
1037
|
const expiresAt = revalidate === false ? Infinity : Date.now() + revalidate * 1e3;
|
|
1038
|
+
const fetchTags = [
|
|
1039
|
+
CacheTags.project(this.config.projectId),
|
|
1040
|
+
CacheTags.content(slug),
|
|
1041
|
+
...tags
|
|
1042
|
+
];
|
|
997
1043
|
const res = await this.fetchWithTimeout(url, {
|
|
998
1044
|
method: "GET",
|
|
999
1045
|
headers: this.getHeaders(),
|
|
1000
|
-
tags,
|
|
1046
|
+
tags: fetchTags,
|
|
1001
1047
|
revalidate
|
|
1002
1048
|
});
|
|
1003
1049
|
if (!res.ok) {
|
|
@@ -1016,29 +1062,27 @@ var ContentEngine = class {
|
|
|
1016
1062
|
timestamp: Date.now(),
|
|
1017
1063
|
etag: etag || void 0,
|
|
1018
1064
|
expiresAt,
|
|
1019
|
-
tags:
|
|
1065
|
+
tags: fetchTags
|
|
1020
1066
|
}
|
|
1021
1067
|
};
|
|
1022
1068
|
this.writeCache(cacheKey, cacheEntry.data, {
|
|
1023
1069
|
revalidate,
|
|
1024
|
-
tags:
|
|
1025
|
-
CacheTags.project(this.config.projectId),
|
|
1026
|
-
CacheTags.content(slug),
|
|
1027
|
-
...tags
|
|
1028
|
-
]
|
|
1070
|
+
tags: fetchTags
|
|
1029
1071
|
});
|
|
1030
1072
|
return cacheEntry;
|
|
1031
1073
|
}
|
|
1032
1074
|
async fetchCollection(collectionId, query, cacheKey, tags, revalidate) {
|
|
1033
1075
|
const params = buildQueryString(query);
|
|
1034
1076
|
const url = `${this.config.apiUrl}/content/${this.config.projectId}/collection/${collectionId}?${params}`;
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1077
|
+
const fetchTags = [
|
|
1078
|
+
CacheTags.project(this.config.projectId),
|
|
1079
|
+
CacheTags.collection(collectionId),
|
|
1080
|
+
...tags
|
|
1081
|
+
];
|
|
1038
1082
|
const res = await this.fetchWithTimeout(url, {
|
|
1039
1083
|
method: "GET",
|
|
1040
1084
|
headers: this.getHeaders(),
|
|
1041
|
-
tags,
|
|
1085
|
+
tags: fetchTags,
|
|
1042
1086
|
revalidate
|
|
1043
1087
|
});
|
|
1044
1088
|
if (!res.ok) {
|
|
@@ -1053,16 +1097,12 @@ var ContentEngine = class {
|
|
|
1053
1097
|
timestamp: Date.now(),
|
|
1054
1098
|
etag: etag || void 0,
|
|
1055
1099
|
expiresAt,
|
|
1056
|
-
tags:
|
|
1100
|
+
tags: fetchTags
|
|
1057
1101
|
}
|
|
1058
1102
|
};
|
|
1059
1103
|
this.writeCache(cacheKey, cacheEntry.data, {
|
|
1060
1104
|
revalidate,
|
|
1061
|
-
tags:
|
|
1062
|
-
CacheTags.project(this.config.projectId),
|
|
1063
|
-
CacheTags.collection(collectionId),
|
|
1064
|
-
...tags
|
|
1065
|
-
]
|
|
1105
|
+
tags: fetchTags
|
|
1066
1106
|
});
|
|
1067
1107
|
return cacheEntry;
|
|
1068
1108
|
}
|
|
@@ -1122,8 +1162,11 @@ var ContentEngine = class {
|
|
|
1122
1162
|
const controller = new AbortController();
|
|
1123
1163
|
const id = setTimeout(() => controller.abort(), timeout);
|
|
1124
1164
|
const nextConfig = this.isServer ? {
|
|
1125
|
-
cache: "force-cache",
|
|
1126
|
-
next: {
|
|
1165
|
+
cache: revalidate === 0 ? "no-store" : "force-cache",
|
|
1166
|
+
next: {
|
|
1167
|
+
tags,
|
|
1168
|
+
revalidate: revalidate === false ? false : revalidate
|
|
1169
|
+
}
|
|
1127
1170
|
} : {};
|
|
1128
1171
|
try {
|
|
1129
1172
|
const response = await fetch(url, {
|
|
@@ -2146,13 +2189,20 @@ function getSelector(el, depth = 0) {
|
|
|
2146
2189
|
}
|
|
2147
2190
|
|
|
2148
2191
|
// src/client.ts
|
|
2192
|
+
var DEFAULT_REVALIDATE_SECONDS = 60;
|
|
2149
2193
|
var NexusClient = class {
|
|
2150
2194
|
constructor(config) {
|
|
2151
2195
|
const fullConfig = getFullConfig(config);
|
|
2152
2196
|
this.config = {
|
|
2153
2197
|
debug: config?.debug ?? false,
|
|
2154
2198
|
cacheStrategy: config?.cacheStrategy ?? "memory",
|
|
2155
|
-
|
|
2199
|
+
// Only fall through to the safe default when revalidateTime is
|
|
2200
|
+
// genuinely *unset* (undefined). An explicit `false` from the caller
|
|
2201
|
+
// is a deliberate "never revalidate" choice and must be respected,
|
|
2202
|
+
// not silently upgraded — `??` (not `||`) is required here so that
|
|
2203
|
+
// `0` (revalidate on every request) also passes through untouched
|
|
2204
|
+
// instead of being treated as falsy.
|
|
2205
|
+
revalidateTime: config?.revalidateTime ?? DEFAULT_REVALIDATE_SECONDS,
|
|
2156
2206
|
timeout: config?.timeout ?? 1e4,
|
|
2157
2207
|
retries: config?.retries ?? 3,
|
|
2158
2208
|
...fullConfig
|