@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/index.js
CHANGED
|
@@ -890,7 +890,7 @@ var ContentEngine = class {
|
|
|
890
890
|
});
|
|
891
891
|
this.requestBatcher = new RequestBatcher(20, 50);
|
|
892
892
|
this.circuitBreaker = new CircuitBreaker();
|
|
893
|
-
this.defaultRevalidate = config.revalidateTime
|
|
893
|
+
this.defaultRevalidate = config.revalidateTime ?? false;
|
|
894
894
|
this.cacheStrategy = config.cacheStrategy || "memory";
|
|
895
895
|
if (!this.isServer) {
|
|
896
896
|
window.addEventListener("beforeunload", this.cleanup.bind(this));
|
|
@@ -1094,9 +1094,16 @@ var ContentEngine = class {
|
|
|
1094
1094
|
await this.rateLimiter.checkLimit();
|
|
1095
1095
|
const url = `${this.config.apiUrl}/content/${this.config.projectId}/globals`;
|
|
1096
1096
|
const params = include.length > 0 ? `?include=${include.join(",")}` : "";
|
|
1097
|
+
const fetchTags = [
|
|
1098
|
+
CacheTags.project(this.config.projectId),
|
|
1099
|
+
CacheTags.global,
|
|
1100
|
+
...options.tags || []
|
|
1101
|
+
];
|
|
1097
1102
|
const res = await this.fetchWithTimeout(`${url}${params}`, {
|
|
1098
1103
|
method: "GET",
|
|
1099
|
-
headers: this.getHeaders()
|
|
1104
|
+
headers: this.getHeaders(),
|
|
1105
|
+
tags: fetchTags,
|
|
1106
|
+
revalidate
|
|
1100
1107
|
});
|
|
1101
1108
|
if (!res.ok) {
|
|
1102
1109
|
throw new Error(`API Error ${res.status}: ${res.statusText}`);
|
|
@@ -1137,9 +1144,20 @@ var ContentEngine = class {
|
|
|
1137
1144
|
params.append("include", options.include.join(","));
|
|
1138
1145
|
}
|
|
1139
1146
|
const url = `${this.config.apiUrl}/content/${this.config.projectId}/collection/${collectionId}/${itemId}?${params}`;
|
|
1147
|
+
const fetchTags = [
|
|
1148
|
+
CacheTags.project(this.config.projectId),
|
|
1149
|
+
CacheTags.collection(collectionId),
|
|
1150
|
+
`item_${itemId}`,
|
|
1151
|
+
...options.tags || []
|
|
1152
|
+
];
|
|
1140
1153
|
const res = await this.fetchWithTimeout(url, {
|
|
1141
1154
|
method: "GET",
|
|
1142
|
-
headers: this.getHeaders()
|
|
1155
|
+
headers: this.getHeaders(),
|
|
1156
|
+
tags: fetchTags,
|
|
1157
|
+
// ?? not || — see the constructor comment on defaultRevalidate for
|
|
1158
|
+
// why: an explicit `revalidate: 0` on this call must not be
|
|
1159
|
+
// discarded in favor of the engine's default.
|
|
1160
|
+
revalidate: options.revalidate ?? this.defaultRevalidate
|
|
1143
1161
|
});
|
|
1144
1162
|
if (!res.ok) {
|
|
1145
1163
|
throw new Error(`API Error ${res.status}: ${res.statusText}`);
|
|
@@ -1147,13 +1165,8 @@ var ContentEngine = class {
|
|
|
1147
1165
|
const json = await res.json();
|
|
1148
1166
|
const data = json.data || json;
|
|
1149
1167
|
this.writeCache(cacheKey, data, {
|
|
1150
|
-
revalidate: options.revalidate
|
|
1151
|
-
tags:
|
|
1152
|
-
CacheTags.project(this.config.projectId),
|
|
1153
|
-
CacheTags.collection(collectionId),
|
|
1154
|
-
`item_${itemId}`,
|
|
1155
|
-
...options.tags || []
|
|
1156
|
-
]
|
|
1168
|
+
revalidate: options.revalidate ?? this.defaultRevalidate,
|
|
1169
|
+
tags: fetchTags
|
|
1157
1170
|
});
|
|
1158
1171
|
return data;
|
|
1159
1172
|
});
|
|
@@ -1196,19 +1209,49 @@ var ContentEngine = class {
|
|
|
1196
1209
|
}
|
|
1197
1210
|
/**
|
|
1198
1211
|
* Robust Real-time Subscriptions (SSE) with Auto-Reconnect
|
|
1212
|
+
*
|
|
1213
|
+
* ⚠️ SCOPE — READ BEFORE RELYING ON THIS FOR "instant" UPDATES:
|
|
1214
|
+
* This connects from the BROWSER TAB it's called in, and on message it
|
|
1215
|
+
* only clears THIS SDK INSTANCE's in-memory `memoryCache` (via
|
|
1216
|
+
* invalidateCache below) in THAT browser tab's JS heap. In a typical
|
|
1217
|
+
* Next.js deployment — and Cloudflare specifically, which is stateless
|
|
1218
|
+
* per-request at the edge — that is a different process/isolate than the
|
|
1219
|
+
* one that will render the NEXT server request for this content. So:
|
|
1220
|
+
* - ✅ Useful for: a client component that reads from `nexus.content`
|
|
1221
|
+
* directly in the browser and re-renders in place without a page
|
|
1222
|
+
* navigation (e.g. a live-updating dashboard widget).
|
|
1223
|
+
* - ❌ NOT sufficient for: "user A edits content in the CMS, user B's
|
|
1224
|
+
* already-loaded page updates" via a server-rendered page. That
|
|
1225
|
+
* requires the backend's /api/revalidate webhook (see
|
|
1226
|
+
* content.service.ts `pingNextRevalidateWebhook`) to have actually
|
|
1227
|
+
* cleared the *server's* Data Cache, so the NEXT navigation or
|
|
1228
|
+
* server request picks up fresh data. This SSE channel does not
|
|
1229
|
+
* replace that — it's a complementary, browser-local optimization.
|
|
1230
|
+
* If your symptom was "stale content after editing," fix the webhook
|
|
1231
|
+
* wiring first; treat this method as an enhancement layered on top.
|
|
1199
1232
|
*/
|
|
1200
1233
|
subscribeToUpdates(callback) {
|
|
1201
1234
|
if (this.isServer || typeof EventSource === "undefined") {
|
|
1202
|
-
|
|
1235
|
+
if (this.config.debug) {
|
|
1236
|
+
console.warn(
|
|
1237
|
+
"[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."
|
|
1238
|
+
);
|
|
1239
|
+
}
|
|
1203
1240
|
return () => {
|
|
1204
1241
|
};
|
|
1205
1242
|
}
|
|
1243
|
+
if (!this.config.apiKey) {
|
|
1244
|
+
console.warn(
|
|
1245
|
+
"[NexusHub] subscribeToUpdates(): no apiKey configured, the SSE connection will likely be rejected by the backend. Set NEXT_PUBLIC_NEXUS_KEY."
|
|
1246
|
+
);
|
|
1247
|
+
}
|
|
1206
1248
|
let eventSource = null;
|
|
1207
1249
|
let retryCount = 0;
|
|
1208
1250
|
let isClosed = false;
|
|
1251
|
+
const MAX_RETRY_DELAY_MS = 3e4;
|
|
1209
1252
|
const connect = () => {
|
|
1210
1253
|
if (isClosed) return;
|
|
1211
|
-
const url = `${this.config.apiUrl}/content/${this.config.projectId}/updates?key=${this.config.apiKey}`;
|
|
1254
|
+
const url = `${this.config.apiUrl}/content/${this.config.projectId}/updates?key=${this.config.apiKey ?? ""}`;
|
|
1212
1255
|
eventSource = new EventSource(url);
|
|
1213
1256
|
eventSource.onopen = () => {
|
|
1214
1257
|
retryCount = 0;
|
|
@@ -1216,27 +1259,40 @@ var ContentEngine = class {
|
|
|
1216
1259
|
console.log("[NexusHub] \u{1F7E2} Real-time stream connected");
|
|
1217
1260
|
};
|
|
1218
1261
|
eventSource.onmessage = (event) => {
|
|
1262
|
+
let data;
|
|
1219
1263
|
try {
|
|
1220
|
-
|
|
1221
|
-
if (data.type === "content.updated" && data.slug) {
|
|
1222
|
-
this.invalidateCache([CacheTags.content(data.slug)]);
|
|
1223
|
-
}
|
|
1224
|
-
if (data.type === "collection.updated") {
|
|
1225
|
-
this.invalidateCache([CacheTags.collection(data.collectionId)]);
|
|
1226
|
-
}
|
|
1227
|
-
callback(data);
|
|
1264
|
+
data = JSON.parse(event.data);
|
|
1228
1265
|
} catch (e) {
|
|
1229
1266
|
console.error("[NexusHub] SSE Parse Error", e);
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
if (!data || typeof data.type !== "string") {
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
if (data.type === "content.updated" && data.slug) {
|
|
1273
|
+
this.invalidateCache([CacheTags.content(data.slug)]);
|
|
1274
|
+
}
|
|
1275
|
+
if (data.type === "collection.updated" && data.collectionId) {
|
|
1276
|
+
this.invalidateCache([CacheTags.collection(data.collectionId)]);
|
|
1230
1277
|
}
|
|
1278
|
+
if (data.type === "schema.updated") {
|
|
1279
|
+
this.clearCache();
|
|
1280
|
+
}
|
|
1281
|
+
callback(data);
|
|
1231
1282
|
};
|
|
1232
1283
|
eventSource.onerror = () => {
|
|
1233
1284
|
eventSource?.close();
|
|
1234
1285
|
if (isClosed) return;
|
|
1235
|
-
const timeout = Math.min(
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
`[NexusHub] \u{1F534} Stream disconnected. Reconnecting in ${timeout}ms...`
|
|
1286
|
+
const timeout = Math.min(
|
|
1287
|
+
1e3 * Math.pow(2, retryCount),
|
|
1288
|
+
MAX_RETRY_DELAY_MS
|
|
1239
1289
|
);
|
|
1290
|
+
retryCount++;
|
|
1291
|
+
if (this.config.debug) {
|
|
1292
|
+
console.warn(
|
|
1293
|
+
`[NexusHub] \u{1F534} Stream disconnected. Reconnecting in ${timeout}ms...`
|
|
1294
|
+
);
|
|
1295
|
+
}
|
|
1240
1296
|
setTimeout(connect, timeout);
|
|
1241
1297
|
};
|
|
1242
1298
|
};
|
|
@@ -1247,9 +1303,6 @@ var ContentEngine = class {
|
|
|
1247
1303
|
};
|
|
1248
1304
|
}
|
|
1249
1305
|
// --- CACHE MANAGEMENT ---
|
|
1250
|
-
/**
|
|
1251
|
-
* Check all caches in order of speed
|
|
1252
|
-
*/
|
|
1253
1306
|
async checkCaches(key, forceRefresh, includeMetadata) {
|
|
1254
1307
|
if (forceRefresh) return null;
|
|
1255
1308
|
if (this.cacheStrategy === "memory") {
|
|
@@ -1306,9 +1359,6 @@ var ContentEngine = class {
|
|
|
1306
1359
|
this.browserCache.set(key, data, numericRevalidate * 1e3);
|
|
1307
1360
|
}
|
|
1308
1361
|
}
|
|
1309
|
-
/**
|
|
1310
|
-
* Invalidate cache by tags
|
|
1311
|
-
*/
|
|
1312
1362
|
invalidateCache(tags) {
|
|
1313
1363
|
this.memoryCache.invalidateByTags(tags);
|
|
1314
1364
|
if (this.config.debug) {
|
|
@@ -1317,9 +1367,6 @@ var ContentEngine = class {
|
|
|
1317
1367
|
);
|
|
1318
1368
|
}
|
|
1319
1369
|
}
|
|
1320
|
-
/**
|
|
1321
|
-
* Clear all caches
|
|
1322
|
-
*/
|
|
1323
1370
|
clearCache() {
|
|
1324
1371
|
this.memoryCache.clear();
|
|
1325
1372
|
if (this.browserCache) {
|
|
@@ -1329,9 +1376,6 @@ var ContentEngine = class {
|
|
|
1329
1376
|
console.log("[NexusHub] \u{1F9F9} Cleared all caches");
|
|
1330
1377
|
}
|
|
1331
1378
|
}
|
|
1332
|
-
/**
|
|
1333
|
-
* Get cache statistics
|
|
1334
|
-
*/
|
|
1335
1379
|
getCacheStats() {
|
|
1336
1380
|
const stats = {
|
|
1337
1381
|
memory: this.memoryCache.getStats(),
|
|
@@ -1345,14 +1389,16 @@ var ContentEngine = class {
|
|
|
1345
1389
|
// --- REQUEST METHODS ---
|
|
1346
1390
|
async fetchPage(slug, cacheKey, tags, revalidate, forceRefresh) {
|
|
1347
1391
|
const url = `${this.config.apiUrl}/content/${this.config.projectId}/page/${slug}`;
|
|
1348
|
-
if (this.config.debug) {
|
|
1349
|
-
console.log(`[NexusHub] \u{1F310} Fetching: ${url}`);
|
|
1350
|
-
}
|
|
1351
1392
|
const expiresAt = revalidate === false ? Infinity : Date.now() + revalidate * 1e3;
|
|
1393
|
+
const fetchTags = [
|
|
1394
|
+
CacheTags.project(this.config.projectId),
|
|
1395
|
+
CacheTags.content(slug),
|
|
1396
|
+
...tags
|
|
1397
|
+
];
|
|
1352
1398
|
const res = await this.fetchWithTimeout(url, {
|
|
1353
1399
|
method: "GET",
|
|
1354
1400
|
headers: this.getHeaders(),
|
|
1355
|
-
tags,
|
|
1401
|
+
tags: fetchTags,
|
|
1356
1402
|
revalidate
|
|
1357
1403
|
});
|
|
1358
1404
|
if (!res.ok) {
|
|
@@ -1371,29 +1417,27 @@ var ContentEngine = class {
|
|
|
1371
1417
|
timestamp: Date.now(),
|
|
1372
1418
|
etag: etag || void 0,
|
|
1373
1419
|
expiresAt,
|
|
1374
|
-
tags:
|
|
1420
|
+
tags: fetchTags
|
|
1375
1421
|
}
|
|
1376
1422
|
};
|
|
1377
1423
|
this.writeCache(cacheKey, cacheEntry.data, {
|
|
1378
1424
|
revalidate,
|
|
1379
|
-
tags:
|
|
1380
|
-
CacheTags.project(this.config.projectId),
|
|
1381
|
-
CacheTags.content(slug),
|
|
1382
|
-
...tags
|
|
1383
|
-
]
|
|
1425
|
+
tags: fetchTags
|
|
1384
1426
|
});
|
|
1385
1427
|
return cacheEntry;
|
|
1386
1428
|
}
|
|
1387
1429
|
async fetchCollection(collectionId, query, cacheKey, tags, revalidate) {
|
|
1388
1430
|
const params = buildQueryString(query);
|
|
1389
1431
|
const url = `${this.config.apiUrl}/content/${this.config.projectId}/collection/${collectionId}?${params}`;
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1432
|
+
const fetchTags = [
|
|
1433
|
+
CacheTags.project(this.config.projectId),
|
|
1434
|
+
CacheTags.collection(collectionId),
|
|
1435
|
+
...tags
|
|
1436
|
+
];
|
|
1393
1437
|
const res = await this.fetchWithTimeout(url, {
|
|
1394
1438
|
method: "GET",
|
|
1395
1439
|
headers: this.getHeaders(),
|
|
1396
|
-
tags,
|
|
1440
|
+
tags: fetchTags,
|
|
1397
1441
|
revalidate
|
|
1398
1442
|
});
|
|
1399
1443
|
if (!res.ok) {
|
|
@@ -1408,16 +1452,12 @@ var ContentEngine = class {
|
|
|
1408
1452
|
timestamp: Date.now(),
|
|
1409
1453
|
etag: etag || void 0,
|
|
1410
1454
|
expiresAt,
|
|
1411
|
-
tags:
|
|
1455
|
+
tags: fetchTags
|
|
1412
1456
|
}
|
|
1413
1457
|
};
|
|
1414
1458
|
this.writeCache(cacheKey, cacheEntry.data, {
|
|
1415
1459
|
revalidate,
|
|
1416
|
-
tags:
|
|
1417
|
-
CacheTags.project(this.config.projectId),
|
|
1418
|
-
CacheTags.collection(collectionId),
|
|
1419
|
-
...tags
|
|
1420
|
-
]
|
|
1460
|
+
tags: fetchTags
|
|
1421
1461
|
});
|
|
1422
1462
|
return cacheEntry;
|
|
1423
1463
|
}
|
|
@@ -1477,8 +1517,11 @@ var ContentEngine = class {
|
|
|
1477
1517
|
const controller = new AbortController();
|
|
1478
1518
|
const id = setTimeout(() => controller.abort(), timeout);
|
|
1479
1519
|
const nextConfig = this.isServer ? {
|
|
1480
|
-
cache: "force-cache",
|
|
1481
|
-
next: {
|
|
1520
|
+
cache: revalidate === 0 ? "no-store" : "force-cache",
|
|
1521
|
+
next: {
|
|
1522
|
+
tags,
|
|
1523
|
+
revalidate: revalidate === false ? false : revalidate
|
|
1524
|
+
}
|
|
1482
1525
|
} : {};
|
|
1483
1526
|
try {
|
|
1484
1527
|
const response = await fetch(url, {
|
|
@@ -2501,13 +2544,20 @@ function getSelector(el, depth = 0) {
|
|
|
2501
2544
|
}
|
|
2502
2545
|
|
|
2503
2546
|
// src/client.ts
|
|
2547
|
+
var DEFAULT_REVALIDATE_SECONDS = 60;
|
|
2504
2548
|
var NexusClient = class {
|
|
2505
2549
|
constructor(config) {
|
|
2506
2550
|
const fullConfig = getFullConfig(config);
|
|
2507
2551
|
this.config = {
|
|
2508
2552
|
debug: config?.debug ?? false,
|
|
2509
2553
|
cacheStrategy: config?.cacheStrategy ?? "memory",
|
|
2510
|
-
|
|
2554
|
+
// Only fall through to the safe default when revalidateTime is
|
|
2555
|
+
// genuinely *unset* (undefined). An explicit `false` from the caller
|
|
2556
|
+
// is a deliberate "never revalidate" choice and must be respected,
|
|
2557
|
+
// not silently upgraded — `??` (not `||`) is required here so that
|
|
2558
|
+
// `0` (revalidate on every request) also passes through untouched
|
|
2559
|
+
// instead of being treated as falsy.
|
|
2560
|
+
revalidateTime: config?.revalidateTime ?? DEFAULT_REVALIDATE_SECONDS,
|
|
2511
2561
|
timeout: config?.timeout ?? 1e4,
|
|
2512
2562
|
retries: config?.retries ?? 3,
|
|
2513
2563
|
...fullConfig
|
package/dist/react.cjs
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 = _nullishCoalesce(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: _nullishCoalesce(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: _nullishCoalesce(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=${_nullishCoalesce(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
|
_optionalChain([eventSource, 'optionalAccess', _24 => _24.close, 'call', _25 => _25()]);
|
|
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 = _chunkWZ47UVA2cjs.getFullConfig.call(void 0, config);
|
|
2152
2196
|
this.config = {
|
|
2153
2197
|
debug: _nullishCoalesce(_optionalChain([config, 'optionalAccess', _41 => _41.debug]), () => ( false)),
|
|
2154
2198
|
cacheStrategy: _nullishCoalesce(_optionalChain([config, 'optionalAccess', _42 => _42.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: _nullishCoalesce(_optionalChain([config, 'optionalAccess', _43 => _43.revalidateTime]), () => ( DEFAULT_REVALIDATE_SECONDS)),
|
|
2156
2206
|
timeout: _nullishCoalesce(_optionalChain([config, 'optionalAccess', _44 => _44.timeout]), () => ( 1e4)),
|
|
2157
2207
|
retries: _nullishCoalesce(_optionalChain([config, 'optionalAccess', _45 => _45.retries]), () => ( 3)),
|
|
2158
2208
|
...fullConfig
|
package/dist/react.d.cts
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;
|