@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/index.cjs CHANGED
@@ -936,7 +936,7 @@ var ContentEngine = class {
936
936
  });
937
937
  this.requestBatcher = new RequestBatcher(20, 50);
938
938
  this.circuitBreaker = new CircuitBreaker();
939
- this.defaultRevalidate = config.revalidateTime || false;
939
+ this.defaultRevalidate = config.revalidateTime ?? false;
940
940
  this.cacheStrategy = config.cacheStrategy || "memory";
941
941
  if (!this.isServer) {
942
942
  window.addEventListener("beforeunload", this.cleanup.bind(this));
@@ -1140,9 +1140,16 @@ var ContentEngine = class {
1140
1140
  await this.rateLimiter.checkLimit();
1141
1141
  const url = `${this.config.apiUrl}/content/${this.config.projectId}/globals`;
1142
1142
  const params = include.length > 0 ? `?include=${include.join(",")}` : "";
1143
+ const fetchTags = [
1144
+ CacheTags.project(this.config.projectId),
1145
+ CacheTags.global,
1146
+ ...options.tags || []
1147
+ ];
1143
1148
  const res = await this.fetchWithTimeout(`${url}${params}`, {
1144
1149
  method: "GET",
1145
- headers: this.getHeaders()
1150
+ headers: this.getHeaders(),
1151
+ tags: fetchTags,
1152
+ revalidate
1146
1153
  });
1147
1154
  if (!res.ok) {
1148
1155
  throw new Error(`API Error ${res.status}: ${res.statusText}`);
@@ -1183,9 +1190,20 @@ var ContentEngine = class {
1183
1190
  params.append("include", options.include.join(","));
1184
1191
  }
1185
1192
  const url = `${this.config.apiUrl}/content/${this.config.projectId}/collection/${collectionId}/${itemId}?${params}`;
1193
+ const fetchTags = [
1194
+ CacheTags.project(this.config.projectId),
1195
+ CacheTags.collection(collectionId),
1196
+ `item_${itemId}`,
1197
+ ...options.tags || []
1198
+ ];
1186
1199
  const res = await this.fetchWithTimeout(url, {
1187
1200
  method: "GET",
1188
- headers: this.getHeaders()
1201
+ headers: this.getHeaders(),
1202
+ tags: fetchTags,
1203
+ // ?? not || — see the constructor comment on defaultRevalidate for
1204
+ // why: an explicit `revalidate: 0` on this call must not be
1205
+ // discarded in favor of the engine's default.
1206
+ revalidate: options.revalidate ?? this.defaultRevalidate
1189
1207
  });
1190
1208
  if (!res.ok) {
1191
1209
  throw new Error(`API Error ${res.status}: ${res.statusText}`);
@@ -1193,13 +1211,8 @@ var ContentEngine = class {
1193
1211
  const json = await res.json();
1194
1212
  const data = json.data || json;
1195
1213
  this.writeCache(cacheKey, data, {
1196
- revalidate: options.revalidate || this.defaultRevalidate,
1197
- tags: [
1198
- CacheTags.project(this.config.projectId),
1199
- CacheTags.collection(collectionId),
1200
- `item_${itemId}`,
1201
- ...options.tags || []
1202
- ]
1214
+ revalidate: options.revalidate ?? this.defaultRevalidate,
1215
+ tags: fetchTags
1203
1216
  });
1204
1217
  return data;
1205
1218
  });
@@ -1242,19 +1255,49 @@ var ContentEngine = class {
1242
1255
  }
1243
1256
  /**
1244
1257
  * Robust Real-time Subscriptions (SSE) with Auto-Reconnect
1258
+ *
1259
+ * ⚠️ SCOPE — READ BEFORE RELYING ON THIS FOR "instant" UPDATES:
1260
+ * This connects from the BROWSER TAB it's called in, and on message it
1261
+ * only clears THIS SDK INSTANCE's in-memory `memoryCache` (via
1262
+ * invalidateCache below) in THAT browser tab's JS heap. In a typical
1263
+ * Next.js deployment — and Cloudflare specifically, which is stateless
1264
+ * per-request at the edge — that is a different process/isolate than the
1265
+ * one that will render the NEXT server request for this content. So:
1266
+ * - ✅ Useful for: a client component that reads from `nexus.content`
1267
+ * directly in the browser and re-renders in place without a page
1268
+ * navigation (e.g. a live-updating dashboard widget).
1269
+ * - ❌ NOT sufficient for: "user A edits content in the CMS, user B's
1270
+ * already-loaded page updates" via a server-rendered page. That
1271
+ * requires the backend's /api/revalidate webhook (see
1272
+ * content.service.ts `pingNextRevalidateWebhook`) to have actually
1273
+ * cleared the *server's* Data Cache, so the NEXT navigation or
1274
+ * server request picks up fresh data. This SSE channel does not
1275
+ * replace that — it's a complementary, browser-local optimization.
1276
+ * If your symptom was "stale content after editing," fix the webhook
1277
+ * wiring first; treat this method as an enhancement layered on top.
1245
1278
  */
1246
1279
  subscribeToUpdates(callback) {
1247
1280
  if (this.isServer || typeof EventSource === "undefined") {
1248
- console.warn("[NexusHub] EventSource not supported in this environment");
1281
+ if (this.config.debug) {
1282
+ console.warn(
1283
+ "[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."
1284
+ );
1285
+ }
1249
1286
  return () => {
1250
1287
  };
1251
1288
  }
1289
+ if (!this.config.apiKey) {
1290
+ console.warn(
1291
+ "[NexusHub] subscribeToUpdates(): no apiKey configured, the SSE connection will likely be rejected by the backend. Set NEXT_PUBLIC_NEXUS_KEY."
1292
+ );
1293
+ }
1252
1294
  let eventSource = null;
1253
1295
  let retryCount = 0;
1254
1296
  let isClosed = false;
1297
+ const MAX_RETRY_DELAY_MS = 3e4;
1255
1298
  const connect = () => {
1256
1299
  if (isClosed) return;
1257
- const url = `${this.config.apiUrl}/content/${this.config.projectId}/updates?key=${this.config.apiKey}`;
1300
+ const url = `${this.config.apiUrl}/content/${this.config.projectId}/updates?key=${this.config.apiKey ?? ""}`;
1258
1301
  eventSource = new EventSource(url);
1259
1302
  eventSource.onopen = () => {
1260
1303
  retryCount = 0;
@@ -1262,27 +1305,40 @@ var ContentEngine = class {
1262
1305
  console.log("[NexusHub] \u{1F7E2} Real-time stream connected");
1263
1306
  };
1264
1307
  eventSource.onmessage = (event) => {
1308
+ let data;
1265
1309
  try {
1266
- const data = JSON.parse(event.data);
1267
- if (data.type === "content.updated" && data.slug) {
1268
- this.invalidateCache([CacheTags.content(data.slug)]);
1269
- }
1270
- if (data.type === "collection.updated") {
1271
- this.invalidateCache([CacheTags.collection(data.collectionId)]);
1272
- }
1273
- callback(data);
1310
+ data = JSON.parse(event.data);
1274
1311
  } catch (e) {
1275
1312
  console.error("[NexusHub] SSE Parse Error", e);
1313
+ return;
1314
+ }
1315
+ if (!data || typeof data.type !== "string") {
1316
+ return;
1317
+ }
1318
+ if (data.type === "content.updated" && data.slug) {
1319
+ this.invalidateCache([CacheTags.content(data.slug)]);
1320
+ }
1321
+ if (data.type === "collection.updated" && data.collectionId) {
1322
+ this.invalidateCache([CacheTags.collection(data.collectionId)]);
1276
1323
  }
1324
+ if (data.type === "schema.updated") {
1325
+ this.clearCache();
1326
+ }
1327
+ callback(data);
1277
1328
  };
1278
1329
  eventSource.onerror = () => {
1279
1330
  eventSource?.close();
1280
1331
  if (isClosed) return;
1281
- const timeout = Math.min(1e3 * Math.pow(2, retryCount), 1e4);
1282
- retryCount++;
1283
- console.warn(
1284
- `[NexusHub] \u{1F534} Stream disconnected. Reconnecting in ${timeout}ms...`
1332
+ const timeout = Math.min(
1333
+ 1e3 * Math.pow(2, retryCount),
1334
+ MAX_RETRY_DELAY_MS
1285
1335
  );
1336
+ retryCount++;
1337
+ if (this.config.debug) {
1338
+ console.warn(
1339
+ `[NexusHub] \u{1F534} Stream disconnected. Reconnecting in ${timeout}ms...`
1340
+ );
1341
+ }
1286
1342
  setTimeout(connect, timeout);
1287
1343
  };
1288
1344
  };
@@ -1293,9 +1349,6 @@ var ContentEngine = class {
1293
1349
  };
1294
1350
  }
1295
1351
  // --- CACHE MANAGEMENT ---
1296
- /**
1297
- * Check all caches in order of speed
1298
- */
1299
1352
  async checkCaches(key, forceRefresh, includeMetadata) {
1300
1353
  if (forceRefresh) return null;
1301
1354
  if (this.cacheStrategy === "memory") {
@@ -1352,9 +1405,6 @@ var ContentEngine = class {
1352
1405
  this.browserCache.set(key, data, numericRevalidate * 1e3);
1353
1406
  }
1354
1407
  }
1355
- /**
1356
- * Invalidate cache by tags
1357
- */
1358
1408
  invalidateCache(tags) {
1359
1409
  this.memoryCache.invalidateByTags(tags);
1360
1410
  if (this.config.debug) {
@@ -1363,9 +1413,6 @@ var ContentEngine = class {
1363
1413
  );
1364
1414
  }
1365
1415
  }
1366
- /**
1367
- * Clear all caches
1368
- */
1369
1416
  clearCache() {
1370
1417
  this.memoryCache.clear();
1371
1418
  if (this.browserCache) {
@@ -1375,9 +1422,6 @@ var ContentEngine = class {
1375
1422
  console.log("[NexusHub] \u{1F9F9} Cleared all caches");
1376
1423
  }
1377
1424
  }
1378
- /**
1379
- * Get cache statistics
1380
- */
1381
1425
  getCacheStats() {
1382
1426
  const stats = {
1383
1427
  memory: this.memoryCache.getStats(),
@@ -1391,14 +1435,16 @@ var ContentEngine = class {
1391
1435
  // --- REQUEST METHODS ---
1392
1436
  async fetchPage(slug, cacheKey, tags, revalidate, forceRefresh) {
1393
1437
  const url = `${this.config.apiUrl}/content/${this.config.projectId}/page/${slug}`;
1394
- if (this.config.debug) {
1395
- console.log(`[NexusHub] \u{1F310} Fetching: ${url}`);
1396
- }
1397
1438
  const expiresAt = revalidate === false ? Infinity : Date.now() + revalidate * 1e3;
1439
+ const fetchTags = [
1440
+ CacheTags.project(this.config.projectId),
1441
+ CacheTags.content(slug),
1442
+ ...tags
1443
+ ];
1398
1444
  const res = await this.fetchWithTimeout(url, {
1399
1445
  method: "GET",
1400
1446
  headers: this.getHeaders(),
1401
- tags,
1447
+ tags: fetchTags,
1402
1448
  revalidate
1403
1449
  });
1404
1450
  if (!res.ok) {
@@ -1417,29 +1463,27 @@ var ContentEngine = class {
1417
1463
  timestamp: Date.now(),
1418
1464
  etag: etag || void 0,
1419
1465
  expiresAt,
1420
- tags: [...tags, CacheTags.content(slug)]
1466
+ tags: fetchTags
1421
1467
  }
1422
1468
  };
1423
1469
  this.writeCache(cacheKey, cacheEntry.data, {
1424
1470
  revalidate,
1425
- tags: [
1426
- CacheTags.project(this.config.projectId),
1427
- CacheTags.content(slug),
1428
- ...tags
1429
- ]
1471
+ tags: fetchTags
1430
1472
  });
1431
1473
  return cacheEntry;
1432
1474
  }
1433
1475
  async fetchCollection(collectionId, query, cacheKey, tags, revalidate) {
1434
1476
  const params = buildQueryString(query);
1435
1477
  const url = `${this.config.apiUrl}/content/${this.config.projectId}/collection/${collectionId}?${params}`;
1436
- if (this.config.debug) {
1437
- console.log(`[NexusHub] \u{1F310} Fetching Collection: ${url}`);
1438
- }
1478
+ const fetchTags = [
1479
+ CacheTags.project(this.config.projectId),
1480
+ CacheTags.collection(collectionId),
1481
+ ...tags
1482
+ ];
1439
1483
  const res = await this.fetchWithTimeout(url, {
1440
1484
  method: "GET",
1441
1485
  headers: this.getHeaders(),
1442
- tags,
1486
+ tags: fetchTags,
1443
1487
  revalidate
1444
1488
  });
1445
1489
  if (!res.ok) {
@@ -1454,16 +1498,12 @@ var ContentEngine = class {
1454
1498
  timestamp: Date.now(),
1455
1499
  etag: etag || void 0,
1456
1500
  expiresAt,
1457
- tags: [...tags, CacheTags.collection(collectionId)]
1501
+ tags: fetchTags
1458
1502
  }
1459
1503
  };
1460
1504
  this.writeCache(cacheKey, cacheEntry.data, {
1461
1505
  revalidate,
1462
- tags: [
1463
- CacheTags.project(this.config.projectId),
1464
- CacheTags.collection(collectionId),
1465
- ...tags
1466
- ]
1506
+ tags: fetchTags
1467
1507
  });
1468
1508
  return cacheEntry;
1469
1509
  }
@@ -1523,8 +1563,11 @@ var ContentEngine = class {
1523
1563
  const controller = new AbortController();
1524
1564
  const id = setTimeout(() => controller.abort(), timeout);
1525
1565
  const nextConfig = this.isServer ? {
1526
- cache: "force-cache",
1527
- next: { tags, revalidate: revalidate === false ? false : revalidate }
1566
+ cache: revalidate === 0 ? "no-store" : "force-cache",
1567
+ next: {
1568
+ tags,
1569
+ revalidate: revalidate === false ? false : revalidate
1570
+ }
1528
1571
  } : {};
1529
1572
  try {
1530
1573
  const response = await fetch(url, {
@@ -2547,13 +2590,20 @@ function getSelector(el, depth = 0) {
2547
2590
  }
2548
2591
 
2549
2592
  // src/client.ts
2593
+ var DEFAULT_REVALIDATE_SECONDS = 60;
2550
2594
  var NexusClient = class {
2551
2595
  constructor(config) {
2552
2596
  const fullConfig = getFullConfig(config);
2553
2597
  this.config = {
2554
2598
  debug: config?.debug ?? false,
2555
2599
  cacheStrategy: config?.cacheStrategy ?? "memory",
2556
- revalidateTime: config?.revalidateTime ?? false,
2600
+ // Only fall through to the safe default when revalidateTime is
2601
+ // genuinely *unset* (undefined). An explicit `false` from the caller
2602
+ // is a deliberate "never revalidate" choice and must be respected,
2603
+ // not silently upgraded — `??` (not `||`) is required here so that
2604
+ // `0` (revalidate on every request) also passes through untouched
2605
+ // instead of being treated as falsy.
2606
+ revalidateTime: config?.revalidateTime ?? DEFAULT_REVALIDATE_SECONDS,
2557
2607
  timeout: config?.timeout ?? 1e4,
2558
2608
  retries: config?.retries ?? 3,
2559
2609
  ...fullConfig
package/dist/index.d.cts CHANGED
@@ -119,6 +119,7 @@ declare class ContentEngine {
119
119
  include?: string[];
120
120
  revalidate?: number | false;
121
121
  forceRefresh?: boolean;
122
+ tags?: string[];
122
123
  }): Promise<T>;
123
124
  /**
124
125
  * Get a single item from a collection (Uses Request Batching)
@@ -146,24 +147,32 @@ declare class ContentEngine {
146
147
  prefetch(urls: string[]): Promise<void>;
147
148
  /**
148
149
  * Robust Real-time Subscriptions (SSE) with Auto-Reconnect
150
+ *
151
+ * ⚠️ SCOPE — READ BEFORE RELYING ON THIS FOR "instant" UPDATES:
152
+ * This connects from the BROWSER TAB it's called in, and on message it
153
+ * only clears THIS SDK INSTANCE's in-memory `memoryCache` (via
154
+ * invalidateCache below) in THAT browser tab's JS heap. In a typical
155
+ * Next.js deployment — and Cloudflare specifically, which is stateless
156
+ * per-request at the edge — that is a different process/isolate than the
157
+ * one that will render the NEXT server request for this content. So:
158
+ * - ✅ Useful for: a client component that reads from `nexus.content`
159
+ * directly in the browser and re-renders in place without a page
160
+ * navigation (e.g. a live-updating dashboard widget).
161
+ * - ❌ NOT sufficient for: "user A edits content in the CMS, user B's
162
+ * already-loaded page updates" via a server-rendered page. That
163
+ * requires the backend's /api/revalidate webhook (see
164
+ * content.service.ts `pingNextRevalidateWebhook`) to have actually
165
+ * cleared the *server's* Data Cache, so the NEXT navigation or
166
+ * server request picks up fresh data. This SSE channel does not
167
+ * replace that — it's a complementary, browser-local optimization.
168
+ * If your symptom was "stale content after editing," fix the webhook
169
+ * wiring first; treat this method as an enhancement layered on top.
149
170
  */
150
171
  subscribeToUpdates(callback: (data: any) => void): () => void;
151
- /**
152
- * Check all caches in order of speed
153
- */
154
172
  private checkCaches;
155
173
  private writeCache;
156
- /**
157
- * Invalidate cache by tags
158
- */
159
174
  invalidateCache(tags: string[]): void;
160
- /**
161
- * Clear all caches
162
- */
163
175
  clearCache(): void;
164
- /**
165
- * Get cache statistics
166
- */
167
176
  getCacheStats(): {
168
177
  memory: {
169
178
  size: number;
package/dist/index.d.ts CHANGED
@@ -119,6 +119,7 @@ declare class ContentEngine {
119
119
  include?: string[];
120
120
  revalidate?: number | false;
121
121
  forceRefresh?: boolean;
122
+ tags?: string[];
122
123
  }): Promise<T>;
123
124
  /**
124
125
  * Get a single item from a collection (Uses Request Batching)
@@ -146,24 +147,32 @@ declare class ContentEngine {
146
147
  prefetch(urls: string[]): Promise<void>;
147
148
  /**
148
149
  * Robust Real-time Subscriptions (SSE) with Auto-Reconnect
150
+ *
151
+ * ⚠️ SCOPE — READ BEFORE RELYING ON THIS FOR "instant" UPDATES:
152
+ * This connects from the BROWSER TAB it's called in, and on message it
153
+ * only clears THIS SDK INSTANCE's in-memory `memoryCache` (via
154
+ * invalidateCache below) in THAT browser tab's JS heap. In a typical
155
+ * Next.js deployment — and Cloudflare specifically, which is stateless
156
+ * per-request at the edge — that is a different process/isolate than the
157
+ * one that will render the NEXT server request for this content. So:
158
+ * - ✅ Useful for: a client component that reads from `nexus.content`
159
+ * directly in the browser and re-renders in place without a page
160
+ * navigation (e.g. a live-updating dashboard widget).
161
+ * - ❌ NOT sufficient for: "user A edits content in the CMS, user B's
162
+ * already-loaded page updates" via a server-rendered page. That
163
+ * requires the backend's /api/revalidate webhook (see
164
+ * content.service.ts `pingNextRevalidateWebhook`) to have actually
165
+ * cleared the *server's* Data Cache, so the NEXT navigation or
166
+ * server request picks up fresh data. This SSE channel does not
167
+ * replace that — it's a complementary, browser-local optimization.
168
+ * If your symptom was "stale content after editing," fix the webhook
169
+ * wiring first; treat this method as an enhancement layered on top.
149
170
  */
150
171
  subscribeToUpdates(callback: (data: any) => void): () => void;
151
- /**
152
- * Check all caches in order of speed
153
- */
154
172
  private checkCaches;
155
173
  private writeCache;
156
- /**
157
- * Invalidate cache by tags
158
- */
159
174
  invalidateCache(tags: string[]): void;
160
- /**
161
- * Clear all caches
162
- */
163
175
  clearCache(): void;
164
- /**
165
- * Get cache statistics
166
- */
167
176
  getCacheStats(): {
168
177
  memory: {
169
178
  size: number;