@pylonsync/functions 0.3.299 → 0.3.300
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/ssr-runtime.d.ts +32 -0
- package/package.json +1 -1
- package/src/ssr-runtime.ts +245 -0
- package/src/ssr-streaming.test.ts +56 -0
package/dist/ssr-runtime.d.ts
CHANGED
|
@@ -335,6 +335,13 @@ export declare function isSafeRedirect(url: string, opts: {
|
|
|
335
335
|
* into a page's metadata. Explicit `metadata.icons.*` wins. */
|
|
336
336
|
export declare function applyAutoIcons(component: string, metadata: SsrMetadata | undefined): SsrMetadata | undefined;
|
|
337
337
|
export declare function applyAutoSocialImages(component: string, headers: Record<string, string> | undefined, metadata: SsrMetadata | undefined): SsrMetadata | undefined;
|
|
338
|
+
/**
|
|
339
|
+
* Dev-only tail chunk: the `__PYLON_DEV__` info blob (cache verdict, render
|
|
340
|
+
* mode/timing, route) + the HUD bootstrap. Embedded after the page tail so the
|
|
341
|
+
* marker + probe are in place before the deferred client entry boots the sync
|
|
342
|
+
* engine. `<` is escaped so the JSON can't break out of the script.
|
|
343
|
+
*/
|
|
344
|
+
export declare function buildDevHudChunk(devInfo: Record<string, unknown>): string;
|
|
338
345
|
/**
|
|
339
346
|
* Build the hydration tail appended after React's stream EOFs: the
|
|
340
347
|
* `__PYLON_DATA__` JSON blob (props + ssrData) + the per-route entry
|
|
@@ -444,6 +451,31 @@ export declare function computeBucketVerdict(args: {
|
|
|
444
451
|
wantsStream: boolean;
|
|
445
452
|
status: number;
|
|
446
453
|
}): boolean;
|
|
454
|
+
/**
|
|
455
|
+
* Dev HUD: explain the cache verdict for a finished render in human terms — the
|
|
456
|
+
* verdict, its TTL, and (when dynamic) the SINGLE reason it isn't cached. Pure so
|
|
457
|
+
* it's unit-tested and matches `computeCacheVerdict`/`computeBucketVerdict`
|
|
458
|
+
* exactly. The "why isn't my page caching?" answer the framework otherwise throws
|
|
459
|
+
* away.
|
|
460
|
+
*/
|
|
461
|
+
export declare function describeCacheVerdict(v: {
|
|
462
|
+
bucketOptIn: boolean;
|
|
463
|
+
cacheable: boolean;
|
|
464
|
+
bucketable: boolean;
|
|
465
|
+
revalidateSecs: number | null;
|
|
466
|
+
authTouched: boolean;
|
|
467
|
+
dynamicTouched: boolean;
|
|
468
|
+
sessionTouched: boolean;
|
|
469
|
+
cookieCount: number;
|
|
470
|
+
strictPolicies: boolean;
|
|
471
|
+
wantsStream: boolean;
|
|
472
|
+
forceDynamic: boolean;
|
|
473
|
+
status: number;
|
|
474
|
+
}): {
|
|
475
|
+
verdict: "bucketed" | "cacheable" | "dynamic";
|
|
476
|
+
secs: number | null;
|
|
477
|
+
reason: string;
|
|
478
|
+
};
|
|
447
479
|
/**
|
|
448
480
|
* #278: diff the response head committed at `response_start` against the final
|
|
449
481
|
* state after EOF, to catch a late response.* mutation from a suspended subtree
|
package/package.json
CHANGED
package/src/ssr-runtime.ts
CHANGED
|
@@ -1158,6 +1158,155 @@ const DEV_LIVE_RELOAD_SNIPPET =
|
|
|
1158
1158
|
"if(b!==null&&e.data!==b){s.close();location.reload();return;}b=e.data;});" +
|
|
1159
1159
|
"}catch(_){}})();</script>";
|
|
1160
1160
|
|
|
1161
|
+
/**
|
|
1162
|
+
* The dev HUD (a floating bottom-left overlay, dev-only): surfaces the
|
|
1163
|
+
* framework decisions a dev otherwise can't see — the cache verdict + WHY a page
|
|
1164
|
+
* isn't cached, render mode + timing, sync connection + offline-outbox depth, and
|
|
1165
|
+
* client error count. Written as a plain function and embedded via `.toString()`
|
|
1166
|
+
* (Bun strips the TS annotations), so it ships as self-contained browser JS that
|
|
1167
|
+
* closes over nothing. Browser globals go through `g` so it typechecks without a
|
|
1168
|
+
* DOM lib. Mirrors Next's dev indicator, tuned to Pylon's hidden state.
|
|
1169
|
+
*/
|
|
1170
|
+
function pylonDevHud() {
|
|
1171
|
+
const g: any = globalThis;
|
|
1172
|
+
const d: any = g.document;
|
|
1173
|
+
if (!d || g.__pylonHudMounted) return;
|
|
1174
|
+
g.__pylonHudMounted = true;
|
|
1175
|
+
|
|
1176
|
+
let info: any = {};
|
|
1177
|
+
try {
|
|
1178
|
+
const el = d.getElementById("__PYLON_DEV__");
|
|
1179
|
+
if (el) info = JSON.parse(el.textContent || "{}");
|
|
1180
|
+
} catch (_e) {}
|
|
1181
|
+
// Marker the sync engine checks before publishing its dev status probe.
|
|
1182
|
+
g.__PYLON_DEV__ = info;
|
|
1183
|
+
|
|
1184
|
+
const errs: string[] = [];
|
|
1185
|
+
const onErr = (m: any) => {
|
|
1186
|
+
errs.push(String(m));
|
|
1187
|
+
if (errs.length > 50) errs.shift();
|
|
1188
|
+
};
|
|
1189
|
+
if (g.addEventListener) {
|
|
1190
|
+
g.addEventListener("error", (e: any) => onErr((e && (e.message || e.error)) || e));
|
|
1191
|
+
g.addEventListener("unhandledrejection", (e: any) => onErr((e && e.reason) || e));
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
const C = { ok: "#3fb950", warn: "#d29922", bad: "#f85149", dim: "#6e7681" };
|
|
1195
|
+
const make = (tag: string, css: string, text?: string) => {
|
|
1196
|
+
const n = d.createElement(tag);
|
|
1197
|
+
n.style.cssText = css;
|
|
1198
|
+
if (text != null) n.textContent = text;
|
|
1199
|
+
return n;
|
|
1200
|
+
};
|
|
1201
|
+
const dot = (color: string) =>
|
|
1202
|
+
make("span", "display:inline-block;width:7px;height:7px;border-radius:50%;flex:0 0 auto;background:" + color);
|
|
1203
|
+
|
|
1204
|
+
const box = make(
|
|
1205
|
+
"div",
|
|
1206
|
+
"position:fixed;left:12px;bottom:12px;z-index:2147483646;font:11px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace",
|
|
1207
|
+
);
|
|
1208
|
+
const panel = make(
|
|
1209
|
+
"div",
|
|
1210
|
+
"background:#0d1117;border:1px solid #30363d;border-radius:8px;padding:9px 11px;margin-bottom:6px;min-width:240px;max-width:360px;box-shadow:0 8px 28px rgba(0,0,0,.45)",
|
|
1211
|
+
);
|
|
1212
|
+
const pill = make(
|
|
1213
|
+
"button",
|
|
1214
|
+
"display:flex;align-items:center;gap:6px;background:#161b22;border:1px solid #30363d;border-radius:999px;padding:5px 11px;color:#e6edf3;cursor:pointer;font:inherit;box-shadow:0 2px 10px rgba(0,0,0,.35)",
|
|
1215
|
+
);
|
|
1216
|
+
|
|
1217
|
+
const rowEl = (label: string) => {
|
|
1218
|
+
const r = make("div", "display:flex;align-items:flex-start;gap:8px;margin:3px 0");
|
|
1219
|
+
r.appendChild(make("span", "color:#8b949e;flex:0 0 60px", label));
|
|
1220
|
+
const v = make("span", "color:#e6edf3;word-break:break-word;flex:1");
|
|
1221
|
+
r.appendChild(v);
|
|
1222
|
+
panel.appendChild(r);
|
|
1223
|
+
return v;
|
|
1224
|
+
};
|
|
1225
|
+
|
|
1226
|
+
const cache = info.cache || {};
|
|
1227
|
+
const cacheLabel =
|
|
1228
|
+
cache.verdict === "dynamic"
|
|
1229
|
+
? "dynamic"
|
|
1230
|
+
: cache.verdict + (cache.secs ? " · " + cache.secs + "s" : "");
|
|
1231
|
+
rowEl("route").textContent = info.route || "—";
|
|
1232
|
+
rowEl("page").textContent = info.component || "—";
|
|
1233
|
+
const cacheV = rowEl("cache");
|
|
1234
|
+
cacheV.textContent = cacheLabel + (cache.reason ? " · " + cache.reason : "");
|
|
1235
|
+
cacheV.style.color = cache.verdict === "dynamic" ? C.warn : C.ok;
|
|
1236
|
+
rowEl("render").textContent =
|
|
1237
|
+
(info.renderMode || "ssr") + (info.renderMs != null ? " · " + info.renderMs + "ms" : "");
|
|
1238
|
+
const syncV = rowEl("sync");
|
|
1239
|
+
const errV = rowEl("errors");
|
|
1240
|
+
|
|
1241
|
+
pill.appendChild(dot(cache.verdict === "dynamic" ? C.warn : C.ok));
|
|
1242
|
+
pill.appendChild(make("span", "font-weight:600;color:#e6edf3", "pylon"));
|
|
1243
|
+
const pillSyncDot = dot(C.dim);
|
|
1244
|
+
pill.appendChild(pillSyncDot);
|
|
1245
|
+
|
|
1246
|
+
let open = false;
|
|
1247
|
+
try {
|
|
1248
|
+
open = g.localStorage && g.localStorage.getItem("pylon.hud.open") === "1";
|
|
1249
|
+
} catch (_e) {}
|
|
1250
|
+
panel.style.display = open ? "block" : "none";
|
|
1251
|
+
pill.onclick = () => {
|
|
1252
|
+
open = !open;
|
|
1253
|
+
panel.style.display = open ? "block" : "none";
|
|
1254
|
+
try {
|
|
1255
|
+
g.localStorage && g.localStorage.setItem("pylon.hud.open", open ? "1" : "0");
|
|
1256
|
+
} catch (_e) {}
|
|
1257
|
+
};
|
|
1258
|
+
|
|
1259
|
+
const refresh = () => {
|
|
1260
|
+
const s = g.__pylonDevSync;
|
|
1261
|
+
if (s) {
|
|
1262
|
+
let st = "?";
|
|
1263
|
+
let pend = 0;
|
|
1264
|
+
let rows = 0;
|
|
1265
|
+
try {
|
|
1266
|
+
st = s.status();
|
|
1267
|
+
pend = s.pending();
|
|
1268
|
+
rows = s.rows();
|
|
1269
|
+
} catch (_e) {}
|
|
1270
|
+
syncV.textContent = st + " · " + pend + " pending · " + rows + " rows";
|
|
1271
|
+
const col = st === "connected" ? C.ok : st === "offline" ? C.bad : C.warn;
|
|
1272
|
+
syncV.style.color = col;
|
|
1273
|
+
pillSyncDot.style.background = col;
|
|
1274
|
+
} else {
|
|
1275
|
+
const online = g.navigator ? g.navigator.onLine : true;
|
|
1276
|
+
syncV.textContent = "no sync engine · " + (online ? "online" : "offline");
|
|
1277
|
+
syncV.style.color = C.dim;
|
|
1278
|
+
pillSyncDot.style.background = online ? C.dim : C.bad;
|
|
1279
|
+
}
|
|
1280
|
+
errV.textContent = errs.length === 0 ? "0" : errs.length + " · " + errs[errs.length - 1];
|
|
1281
|
+
errV.style.color = errs.length ? C.bad : C.dim;
|
|
1282
|
+
};
|
|
1283
|
+
|
|
1284
|
+
box.appendChild(panel);
|
|
1285
|
+
box.appendChild(pill);
|
|
1286
|
+
const mount = () => (d.body || d.documentElement).appendChild(box);
|
|
1287
|
+
if (d.body) mount();
|
|
1288
|
+
else if (g.addEventListener) g.addEventListener("DOMContentLoaded", mount);
|
|
1289
|
+
refresh();
|
|
1290
|
+
if (g.setInterval) g.setInterval(refresh, 1000);
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
/**
|
|
1294
|
+
* Dev-only tail chunk: the `__PYLON_DEV__` info blob (cache verdict, render
|
|
1295
|
+
* mode/timing, route) + the HUD bootstrap. Embedded after the page tail so the
|
|
1296
|
+
* marker + probe are in place before the deferred client entry boots the sync
|
|
1297
|
+
* engine. `<` is escaped so the JSON can't break out of the script.
|
|
1298
|
+
*/
|
|
1299
|
+
export function buildDevHudChunk(devInfo: Record<string, unknown>): string {
|
|
1300
|
+
const json = JSON.stringify(devInfo)
|
|
1301
|
+
.replace(/</g, "\\u003c")
|
|
1302
|
+
.replace(/\u2028/g, "\\u2028")
|
|
1303
|
+
.replace(/\u2029/g, "\\u2029");
|
|
1304
|
+
return (
|
|
1305
|
+
`<script id="__PYLON_DEV__" type="application/json">${json}</script>` +
|
|
1306
|
+
`<script>(${pylonDevHud.toString()})();</script>`
|
|
1307
|
+
);
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1161
1310
|
/**
|
|
1162
1311
|
* Build the <head> blob for a boundary render: the union of every route's
|
|
1163
1312
|
* stylesheet links from the client build manifest. Boundary modules aren't
|
|
@@ -1705,6 +1854,69 @@ export function computeBucketVerdict(args: {
|
|
|
1705
1854
|
);
|
|
1706
1855
|
}
|
|
1707
1856
|
|
|
1857
|
+
/**
|
|
1858
|
+
* Dev HUD: explain the cache verdict for a finished render in human terms — the
|
|
1859
|
+
* verdict, its TTL, and (when dynamic) the SINGLE reason it isn't cached. Pure so
|
|
1860
|
+
* it's unit-tested and matches `computeCacheVerdict`/`computeBucketVerdict`
|
|
1861
|
+
* exactly. The "why isn't my page caching?" answer the framework otherwise throws
|
|
1862
|
+
* away.
|
|
1863
|
+
*/
|
|
1864
|
+
export function describeCacheVerdict(v: {
|
|
1865
|
+
bucketOptIn: boolean;
|
|
1866
|
+
cacheable: boolean;
|
|
1867
|
+
bucketable: boolean;
|
|
1868
|
+
revalidateSecs: number | null;
|
|
1869
|
+
authTouched: boolean;
|
|
1870
|
+
dynamicTouched: boolean;
|
|
1871
|
+
sessionTouched: boolean;
|
|
1872
|
+
cookieCount: number;
|
|
1873
|
+
strictPolicies: boolean;
|
|
1874
|
+
wantsStream: boolean;
|
|
1875
|
+
forceDynamic: boolean;
|
|
1876
|
+
status: number;
|
|
1877
|
+
}): { verdict: "bucketed" | "cacheable" | "dynamic"; secs: number | null; reason: string } {
|
|
1878
|
+
if (v.bucketable) {
|
|
1879
|
+
return {
|
|
1880
|
+
verdict: "bucketed",
|
|
1881
|
+
secs: v.revalidateSecs,
|
|
1882
|
+
reason: "shared cache, keyed on session presence",
|
|
1883
|
+
};
|
|
1884
|
+
}
|
|
1885
|
+
if (v.cacheable) {
|
|
1886
|
+
return {
|
|
1887
|
+
verdict: "cacheable",
|
|
1888
|
+
secs: v.revalidateSecs,
|
|
1889
|
+
reason: "anonymous shared cache",
|
|
1890
|
+
};
|
|
1891
|
+
}
|
|
1892
|
+
// Dynamic — surface the single most useful reason (mirrors the verdict order).
|
|
1893
|
+
let reason: string;
|
|
1894
|
+
if (!v.bucketOptIn && v.revalidateSecs == null) {
|
|
1895
|
+
reason = "not opted in — add `export const revalidate = N` (or `cache = \"auth-bucketed\"`)";
|
|
1896
|
+
} else if (v.status !== 200) {
|
|
1897
|
+
reason = `status ${v.status} (only 200 is cacheable)`;
|
|
1898
|
+
} else if (v.forceDynamic) {
|
|
1899
|
+
reason = "dynamic = \"force-dynamic\"";
|
|
1900
|
+
} else if (v.wantsStream) {
|
|
1901
|
+
reason = "streaming render (loading.tsx / streaming = true)";
|
|
1902
|
+
} else if (v.cookieCount > 0) {
|
|
1903
|
+
reason = "the render set a cookie";
|
|
1904
|
+
} else if (v.strictPolicies) {
|
|
1905
|
+
reason = "PYLON_STRICT_FN_POLICIES (serverData reads are auth-filtered)";
|
|
1906
|
+
} else if (v.authTouched) {
|
|
1907
|
+
reason = "read props.auth (identity-specific)";
|
|
1908
|
+
} else if (v.dynamicTouched) {
|
|
1909
|
+
reason = "read request headers/cookies";
|
|
1910
|
+
} else if (v.sessionTouched) {
|
|
1911
|
+
reason = v.bucketOptIn
|
|
1912
|
+
? "read props.session"
|
|
1913
|
+
: "read props.session — add `export const cache = \"auth-bucketed\"` to bucket-cache it";
|
|
1914
|
+
} else {
|
|
1915
|
+
reason = "not cacheable";
|
|
1916
|
+
}
|
|
1917
|
+
return { verdict: "dynamic", secs: null, reason };
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1708
1920
|
/**
|
|
1709
1921
|
* #278: diff the response head committed at `response_start` against the final
|
|
1710
1922
|
* state after EOF, to catch a late response.* mutation from a suspended subtree
|
|
@@ -1902,6 +2114,10 @@ export async function handleRenderRoute(
|
|
|
1902
2114
|
: null;
|
|
1903
2115
|
if (dataKind) return handleDataRoute(msg, dataKind, send);
|
|
1904
2116
|
|
|
2117
|
+
// Dev HUD: wall-clock for the whole render, surfaced in the dev overlay's
|
|
2118
|
+
// render row. `performance.now()` is monotonic; harmless in prod (unused).
|
|
2119
|
+
const renderStart = isDevMode() ? performance.now() : 0;
|
|
2120
|
+
|
|
1905
2121
|
// Declared OUTSIDE the try so the catch can read page-set status/
|
|
1906
2122
|
// cookies when turning a redirect()/notFound() throw into a response.
|
|
1907
2123
|
const responseState: ResponseState = {
|
|
@@ -2475,6 +2691,35 @@ export async function handleRenderRoute(
|
|
|
2475
2691
|
sendChunk(tail);
|
|
2476
2692
|
}
|
|
2477
2693
|
|
|
2694
|
+
// Dev HUD (dev only): append the cache-verdict + render-timing blob + the
|
|
2695
|
+
// floating overlay. After the page tail so its marker/probe are in place
|
|
2696
|
+
// before the deferred client entry boots the sync engine.
|
|
2697
|
+
if (isDevMode()) {
|
|
2698
|
+
const verdict = describeCacheVerdict({
|
|
2699
|
+
bucketOptIn,
|
|
2700
|
+
cacheable,
|
|
2701
|
+
bucketable,
|
|
2702
|
+
revalidateSecs,
|
|
2703
|
+
authTouched,
|
|
2704
|
+
dynamicTouched,
|
|
2705
|
+
sessionTouched,
|
|
2706
|
+
cookieCount: responseState.cookies.length,
|
|
2707
|
+
strictPolicies,
|
|
2708
|
+
wantsStream,
|
|
2709
|
+
forceDynamic,
|
|
2710
|
+
status: responseState.status,
|
|
2711
|
+
});
|
|
2712
|
+
sendChunk(
|
|
2713
|
+
buildDevHudChunk({
|
|
2714
|
+
route: msg.url,
|
|
2715
|
+
component: msg.component,
|
|
2716
|
+
renderMode: wantsStream ? "ssr-streaming" : "ssr-buffered",
|
|
2717
|
+
renderMs: Math.round((performance.now() - renderStart) * 10) / 10,
|
|
2718
|
+
cache: verdict,
|
|
2719
|
+
}),
|
|
2720
|
+
);
|
|
2721
|
+
}
|
|
2722
|
+
|
|
2478
2723
|
send({ type: "render_done", call_id: msg.call_id });
|
|
2479
2724
|
} catch (err: any) {
|
|
2480
2725
|
// A page/layout called response.redirect()/response.notFound(), or
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
computeCacheVerdict,
|
|
23
23
|
computeRevalidateSecs,
|
|
24
24
|
computeWantsStream,
|
|
25
|
+
describeCacheVerdict,
|
|
25
26
|
diffCommittedResponse,
|
|
26
27
|
isDevMode,
|
|
27
28
|
} from "./ssr-runtime";
|
|
@@ -222,6 +223,61 @@ describe("computeBucketVerdict (PPR Phase 0 — session-presence shared cache)",
|
|
|
222
223
|
});
|
|
223
224
|
});
|
|
224
225
|
|
|
226
|
+
describe("describeCacheVerdict (dev HUD — why isn't my page cached?)", () => {
|
|
227
|
+
const base = {
|
|
228
|
+
bucketOptIn: false,
|
|
229
|
+
cacheable: false,
|
|
230
|
+
bucketable: false,
|
|
231
|
+
revalidateSecs: null as number | null,
|
|
232
|
+
authTouched: false,
|
|
233
|
+
dynamicTouched: false,
|
|
234
|
+
sessionTouched: false,
|
|
235
|
+
cookieCount: 0,
|
|
236
|
+
strictPolicies: false,
|
|
237
|
+
wantsStream: false,
|
|
238
|
+
forceDynamic: false,
|
|
239
|
+
status: 200,
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
test("cacheable / bucketed verdicts carry the TTL", () => {
|
|
243
|
+
expect(describeCacheVerdict({ ...base, cacheable: true, revalidateSecs: 60 })).toEqual({
|
|
244
|
+
verdict: "cacheable",
|
|
245
|
+
secs: 60,
|
|
246
|
+
reason: "anonymous shared cache",
|
|
247
|
+
});
|
|
248
|
+
const b = describeCacheVerdict({
|
|
249
|
+
...base,
|
|
250
|
+
bucketOptIn: true,
|
|
251
|
+
bucketable: true,
|
|
252
|
+
revalidateSecs: 30,
|
|
253
|
+
});
|
|
254
|
+
expect(b.verdict).toBe("bucketed");
|
|
255
|
+
expect(b.secs).toBe(30);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("dynamic verdict reports the SINGLE actionable reason", () => {
|
|
259
|
+
const why = (over: any) =>
|
|
260
|
+
describeCacheVerdict({ ...base, ...over }).reason;
|
|
261
|
+
// Not opted in at all.
|
|
262
|
+
expect(why({})).toContain("not opted in");
|
|
263
|
+
// Opted in (revalidate set) but each veto wins in priority order.
|
|
264
|
+
const opted = { revalidateSecs: 60 };
|
|
265
|
+
expect(why({ ...opted, status: 404 })).toContain("status 404");
|
|
266
|
+
expect(why({ ...opted, forceDynamic: true })).toContain("force-dynamic");
|
|
267
|
+
expect(why({ ...opted, wantsStream: true })).toContain("streaming");
|
|
268
|
+
expect(why({ ...opted, cookieCount: 1 })).toContain("set a cookie");
|
|
269
|
+
expect(why({ ...opted, strictPolicies: true })).toContain("STRICT");
|
|
270
|
+
expect(why({ ...opted, authTouched: true })).toContain("props.auth");
|
|
271
|
+
expect(why({ ...opted, dynamicTouched: true })).toContain("headers/cookies");
|
|
272
|
+
// Reading session on a non-bucket page → the actionable hint to opt into buckets.
|
|
273
|
+
expect(why({ ...opted, sessionTouched: true })).toContain("auth-bucketed");
|
|
274
|
+
// …but on a bucket-opted page that didn't bucket for another reason, no hint.
|
|
275
|
+
expect(why({ bucketOptIn: true, revalidateSecs: 60, sessionTouched: true })).not.toContain(
|
|
276
|
+
"auth-bucketed",
|
|
277
|
+
);
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
225
281
|
describe("diffCommittedResponse (#278 late-response.* drop detector)", () => {
|
|
226
282
|
const snap = (over: any = {}) => ({
|
|
227
283
|
status: 200,
|