@nexushub/client 0.7.8 → 0.8.0

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));
@@ -1200,7 +1200,10 @@ var ContentEngine = class {
1200
1200
  method: "GET",
1201
1201
  headers: this.getHeaders(),
1202
1202
  tags: fetchTags,
1203
- revalidate: options.revalidate || this.defaultRevalidate
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
1204
1207
  });
1205
1208
  if (!res.ok) {
1206
1209
  throw new Error(`API Error ${res.status}: ${res.statusText}`);
@@ -1208,7 +1211,7 @@ var ContentEngine = class {
1208
1211
  const json = await res.json();
1209
1212
  const data = json.data || json;
1210
1213
  this.writeCache(cacheKey, data, {
1211
- revalidate: options.revalidate || this.defaultRevalidate,
1214
+ revalidate: options.revalidate ?? this.defaultRevalidate,
1212
1215
  tags: fetchTags
1213
1216
  });
1214
1217
  return data;
@@ -1252,19 +1255,49 @@ var ContentEngine = class {
1252
1255
  }
1253
1256
  /**
1254
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.
1255
1278
  */
1256
1279
  subscribeToUpdates(callback) {
1257
1280
  if (this.isServer || typeof EventSource === "undefined") {
1258
- 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
+ }
1259
1286
  return () => {
1260
1287
  };
1261
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
+ }
1262
1294
  let eventSource = null;
1263
1295
  let retryCount = 0;
1264
1296
  let isClosed = false;
1297
+ const MAX_RETRY_DELAY_MS = 3e4;
1265
1298
  const connect = () => {
1266
1299
  if (isClosed) return;
1267
- 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 ?? ""}`;
1268
1301
  eventSource = new EventSource(url);
1269
1302
  eventSource.onopen = () => {
1270
1303
  retryCount = 0;
@@ -1272,27 +1305,40 @@ var ContentEngine = class {
1272
1305
  console.log("[NexusHub] \u{1F7E2} Real-time stream connected");
1273
1306
  };
1274
1307
  eventSource.onmessage = (event) => {
1308
+ let data;
1275
1309
  try {
1276
- const data = JSON.parse(event.data);
1277
- if (data.type === "content.updated" && data.slug) {
1278
- this.invalidateCache([CacheTags.content(data.slug)]);
1279
- }
1280
- if (data.type === "collection.updated") {
1281
- this.invalidateCache([CacheTags.collection(data.collectionId)]);
1282
- }
1283
- callback(data);
1310
+ data = JSON.parse(event.data);
1284
1311
  } catch (e) {
1285
1312
  console.error("[NexusHub] SSE Parse Error", e);
1313
+ return;
1314
+ }
1315
+ if (!data || typeof data.type !== "string") {
1316
+ return;
1286
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)]);
1323
+ }
1324
+ if (data.type === "schema.updated") {
1325
+ this.clearCache();
1326
+ }
1327
+ callback(data);
1287
1328
  };
1288
1329
  eventSource.onerror = () => {
1289
1330
  eventSource?.close();
1290
1331
  if (isClosed) return;
1291
- const timeout = Math.min(1e3 * Math.pow(2, retryCount), 1e4);
1292
- retryCount++;
1293
- console.warn(
1294
- `[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
1295
1335
  );
1336
+ retryCount++;
1337
+ if (this.config.debug) {
1338
+ console.warn(
1339
+ `[NexusHub] \u{1F534} Stream disconnected. Reconnecting in ${timeout}ms...`
1340
+ );
1341
+ }
1296
1342
  setTimeout(connect, timeout);
1297
1343
  };
1298
1344
  };
@@ -1517,8 +1563,11 @@ var ContentEngine = class {
1517
1563
  const controller = new AbortController();
1518
1564
  const id = setTimeout(() => controller.abort(), timeout);
1519
1565
  const nextConfig = this.isServer ? {
1520
- cache: "force-cache",
1521
- 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
+ }
1522
1571
  } : {};
1523
1572
  try {
1524
1573
  const response = await fetch(url, {
@@ -2541,13 +2590,20 @@ function getSelector(el, depth = 0) {
2541
2590
  }
2542
2591
 
2543
2592
  // src/client.ts
2593
+ var DEFAULT_REVALIDATE_SECONDS = 60;
2544
2594
  var NexusClient = class {
2545
2595
  constructor(config) {
2546
2596
  const fullConfig = getFullConfig(config);
2547
2597
  this.config = {
2548
2598
  debug: config?.debug ?? false,
2549
2599
  cacheStrategy: config?.cacheStrategy ?? "memory",
2550
- 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,
2551
2607
  timeout: config?.timeout ?? 1e4,
2552
2608
  retries: config?.retries ?? 3,
2553
2609
  ...fullConfig
package/dist/index.d.cts CHANGED
@@ -147,6 +147,26 @@ declare class ContentEngine {
147
147
  prefetch(urls: string[]): Promise<void>;
148
148
  /**
149
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.
150
170
  */
151
171
  subscribeToUpdates(callback: (data: any) => void): () => void;
152
172
  private checkCaches;
package/dist/index.d.ts CHANGED
@@ -147,6 +147,26 @@ declare class ContentEngine {
147
147
  prefetch(urls: string[]): Promise<void>;
148
148
  /**
149
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.
150
170
  */
151
171
  subscribeToUpdates(callback: (data: any) => void): () => void;
152
172
  private checkCaches;
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 || false;
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));
@@ -1154,7 +1154,10 @@ var ContentEngine = class {
1154
1154
  method: "GET",
1155
1155
  headers: this.getHeaders(),
1156
1156
  tags: fetchTags,
1157
- revalidate: options.revalidate || this.defaultRevalidate
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
1158
1161
  });
1159
1162
  if (!res.ok) {
1160
1163
  throw new Error(`API Error ${res.status}: ${res.statusText}`);
@@ -1162,7 +1165,7 @@ var ContentEngine = class {
1162
1165
  const json = await res.json();
1163
1166
  const data = json.data || json;
1164
1167
  this.writeCache(cacheKey, data, {
1165
- revalidate: options.revalidate || this.defaultRevalidate,
1168
+ revalidate: options.revalidate ?? this.defaultRevalidate,
1166
1169
  tags: fetchTags
1167
1170
  });
1168
1171
  return data;
@@ -1206,19 +1209,49 @@ var ContentEngine = class {
1206
1209
  }
1207
1210
  /**
1208
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.
1209
1232
  */
1210
1233
  subscribeToUpdates(callback) {
1211
1234
  if (this.isServer || typeof EventSource === "undefined") {
1212
- console.warn("[NexusHub] EventSource not supported in this environment");
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
+ }
1213
1240
  return () => {
1214
1241
  };
1215
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
+ }
1216
1248
  let eventSource = null;
1217
1249
  let retryCount = 0;
1218
1250
  let isClosed = false;
1251
+ const MAX_RETRY_DELAY_MS = 3e4;
1219
1252
  const connect = () => {
1220
1253
  if (isClosed) return;
1221
- 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 ?? ""}`;
1222
1255
  eventSource = new EventSource(url);
1223
1256
  eventSource.onopen = () => {
1224
1257
  retryCount = 0;
@@ -1226,27 +1259,40 @@ var ContentEngine = class {
1226
1259
  console.log("[NexusHub] \u{1F7E2} Real-time stream connected");
1227
1260
  };
1228
1261
  eventSource.onmessage = (event) => {
1262
+ let data;
1229
1263
  try {
1230
- const data = JSON.parse(event.data);
1231
- if (data.type === "content.updated" && data.slug) {
1232
- this.invalidateCache([CacheTags.content(data.slug)]);
1233
- }
1234
- if (data.type === "collection.updated") {
1235
- this.invalidateCache([CacheTags.collection(data.collectionId)]);
1236
- }
1237
- callback(data);
1264
+ data = JSON.parse(event.data);
1238
1265
  } catch (e) {
1239
1266
  console.error("[NexusHub] SSE Parse Error", e);
1267
+ return;
1268
+ }
1269
+ if (!data || typeof data.type !== "string") {
1270
+ return;
1240
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)]);
1277
+ }
1278
+ if (data.type === "schema.updated") {
1279
+ this.clearCache();
1280
+ }
1281
+ callback(data);
1241
1282
  };
1242
1283
  eventSource.onerror = () => {
1243
1284
  eventSource?.close();
1244
1285
  if (isClosed) return;
1245
- const timeout = Math.min(1e3 * Math.pow(2, retryCount), 1e4);
1246
- retryCount++;
1247
- console.warn(
1248
- `[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
1249
1289
  );
1290
+ retryCount++;
1291
+ if (this.config.debug) {
1292
+ console.warn(
1293
+ `[NexusHub] \u{1F534} Stream disconnected. Reconnecting in ${timeout}ms...`
1294
+ );
1295
+ }
1250
1296
  setTimeout(connect, timeout);
1251
1297
  };
1252
1298
  };
@@ -1471,8 +1517,11 @@ var ContentEngine = class {
1471
1517
  const controller = new AbortController();
1472
1518
  const id = setTimeout(() => controller.abort(), timeout);
1473
1519
  const nextConfig = this.isServer ? {
1474
- cache: "force-cache",
1475
- next: { tags, revalidate: revalidate === false ? false : revalidate }
1520
+ cache: revalidate === 0 ? "no-store" : "force-cache",
1521
+ next: {
1522
+ tags,
1523
+ revalidate: revalidate === false ? false : revalidate
1524
+ }
1476
1525
  } : {};
1477
1526
  try {
1478
1527
  const response = await fetch(url, {
@@ -2495,13 +2544,20 @@ function getSelector(el, depth = 0) {
2495
2544
  }
2496
2545
 
2497
2546
  // src/client.ts
2547
+ var DEFAULT_REVALIDATE_SECONDS = 60;
2498
2548
  var NexusClient = class {
2499
2549
  constructor(config) {
2500
2550
  const fullConfig = getFullConfig(config);
2501
2551
  this.config = {
2502
2552
  debug: config?.debug ?? false,
2503
2553
  cacheStrategy: config?.cacheStrategy ?? "memory",
2504
- revalidateTime: config?.revalidateTime ?? false,
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,
2505
2561
  timeout: config?.timeout ?? 1e4,
2506
2562
  retries: config?.retries ?? 3,
2507
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 || false;
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));
@@ -799,7 +799,10 @@ var ContentEngine = class {
799
799
  method: "GET",
800
800
  headers: this.getHeaders(),
801
801
  tags: fetchTags,
802
- revalidate: options.revalidate || this.defaultRevalidate
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))
803
806
  });
804
807
  if (!res.ok) {
805
808
  throw new Error(`API Error ${res.status}: ${res.statusText}`);
@@ -807,7 +810,7 @@ var ContentEngine = class {
807
810
  const json = await res.json();
808
811
  const data = json.data || json;
809
812
  this.writeCache(cacheKey, data, {
810
- revalidate: options.revalidate || this.defaultRevalidate,
813
+ revalidate: _nullishCoalesce(options.revalidate, () => ( this.defaultRevalidate)),
811
814
  tags: fetchTags
812
815
  });
813
816
  return data;
@@ -851,19 +854,49 @@ var ContentEngine = class {
851
854
  }
852
855
  /**
853
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.
854
877
  */
855
878
  subscribeToUpdates(callback) {
856
879
  if (this.isServer || typeof EventSource === "undefined") {
857
- console.warn("[NexusHub] EventSource not supported in this environment");
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
+ }
858
885
  return () => {
859
886
  };
860
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
+ }
861
893
  let eventSource = null;
862
894
  let retryCount = 0;
863
895
  let isClosed = false;
896
+ const MAX_RETRY_DELAY_MS = 3e4;
864
897
  const connect = () => {
865
898
  if (isClosed) return;
866
- 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, () => ( ""))}`;
867
900
  eventSource = new EventSource(url);
868
901
  eventSource.onopen = () => {
869
902
  retryCount = 0;
@@ -871,27 +904,40 @@ var ContentEngine = class {
871
904
  console.log("[NexusHub] \u{1F7E2} Real-time stream connected");
872
905
  };
873
906
  eventSource.onmessage = (event) => {
907
+ let data;
874
908
  try {
875
- const data = JSON.parse(event.data);
876
- if (data.type === "content.updated" && data.slug) {
877
- this.invalidateCache([CacheTags.content(data.slug)]);
878
- }
879
- if (data.type === "collection.updated") {
880
- this.invalidateCache([CacheTags.collection(data.collectionId)]);
881
- }
882
- callback(data);
909
+ data = JSON.parse(event.data);
883
910
  } catch (e) {
884
911
  console.error("[NexusHub] SSE Parse Error", e);
912
+ return;
913
+ }
914
+ if (!data || typeof data.type !== "string") {
915
+ return;
885
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();
925
+ }
926
+ callback(data);
886
927
  };
887
928
  eventSource.onerror = () => {
888
929
  _optionalChain([eventSource, 'optionalAccess', _24 => _24.close, 'call', _25 => _25()]);
889
930
  if (isClosed) return;
890
- const timeout = Math.min(1e3 * Math.pow(2, retryCount), 1e4);
891
- retryCount++;
892
- console.warn(
893
- `[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
894
934
  );
935
+ retryCount++;
936
+ if (this.config.debug) {
937
+ console.warn(
938
+ `[NexusHub] \u{1F534} Stream disconnected. Reconnecting in ${timeout}ms...`
939
+ );
940
+ }
895
941
  setTimeout(connect, timeout);
896
942
  };
897
943
  };
@@ -1116,8 +1162,11 @@ var ContentEngine = class {
1116
1162
  const controller = new AbortController();
1117
1163
  const id = setTimeout(() => controller.abort(), timeout);
1118
1164
  const nextConfig = this.isServer ? {
1119
- cache: "force-cache",
1120
- next: { tags, revalidate: revalidate === false ? false : revalidate }
1165
+ cache: revalidate === 0 ? "no-store" : "force-cache",
1166
+ next: {
1167
+ tags,
1168
+ revalidate: revalidate === false ? false : revalidate
1169
+ }
1121
1170
  } : {};
1122
1171
  try {
1123
1172
  const response = await fetch(url, {
@@ -2140,13 +2189,20 @@ function getSelector(el, depth = 0) {
2140
2189
  }
2141
2190
 
2142
2191
  // src/client.ts
2192
+ var DEFAULT_REVALIDATE_SECONDS = 60;
2143
2193
  var NexusClient = class {
2144
2194
  constructor(config) {
2145
2195
  const fullConfig = _chunkWZ47UVA2cjs.getFullConfig.call(void 0, config);
2146
2196
  this.config = {
2147
2197
  debug: _nullishCoalesce(_optionalChain([config, 'optionalAccess', _41 => _41.debug]), () => ( false)),
2148
2198
  cacheStrategy: _nullishCoalesce(_optionalChain([config, 'optionalAccess', _42 => _42.cacheStrategy]), () => ( "memory")),
2149
- revalidateTime: _nullishCoalesce(_optionalChain([config, 'optionalAccess', _43 => _43.revalidateTime]), () => ( false)),
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)),
2150
2206
  timeout: _nullishCoalesce(_optionalChain([config, 'optionalAccess', _44 => _44.timeout]), () => ( 1e4)),
2151
2207
  retries: _nullishCoalesce(_optionalChain([config, 'optionalAccess', _45 => _45.retries]), () => ( 3)),
2152
2208
  ...fullConfig
package/dist/react.d.cts CHANGED
@@ -113,6 +113,26 @@ declare class ContentEngine {
113
113
  prefetch(urls: string[]): Promise<void>;
114
114
  /**
115
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.
116
136
  */
117
137
  subscribeToUpdates(callback: (data: any) => void): () => void;
118
138
  private checkCaches;
package/dist/react.d.ts CHANGED
@@ -113,6 +113,26 @@ declare class ContentEngine {
113
113
  prefetch(urls: string[]): Promise<void>;
114
114
  /**
115
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.
116
136
  */
117
137
  subscribeToUpdates(callback: (data: any) => void): () => void;
118
138
  private checkCaches;