@pylonsync/functions 0.3.300 → 0.3.302
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/package.json +1 -1
- package/src/ssr-runtime.ts +252 -45
package/package.json
CHANGED
package/src/ssr-runtime.ts
CHANGED
|
@@ -1159,13 +1159,14 @@ const DEV_LIVE_RELOAD_SNIPPET =
|
|
|
1159
1159
|
"}catch(_){}})();</script>";
|
|
1160
1160
|
|
|
1161
1161
|
/**
|
|
1162
|
-
* The dev HUD (a floating bottom-left overlay, dev-only): surfaces the
|
|
1163
|
-
*
|
|
1164
|
-
*
|
|
1165
|
-
*
|
|
1166
|
-
*
|
|
1167
|
-
*
|
|
1168
|
-
*
|
|
1162
|
+
* The dev HUD (a floating bottom-left overlay, dev-only): surfaces the framework
|
|
1163
|
+
* decisions a dev otherwise can't see — the cache verdict + WHY a page isn't
|
|
1164
|
+
* cached, render mode + timing, sync connection + offline-outbox depth, /api
|
|
1165
|
+
* activity + policy denials, and client errors. The cache / api / errors rows
|
|
1166
|
+
* expand a detail drawer (click). Written as a plain function and embedded via
|
|
1167
|
+
* `.toString()` (Bun strips the TS annotations), so it ships as self-contained
|
|
1168
|
+
* browser JS that closes over nothing. Browser globals go through `g` so it
|
|
1169
|
+
* typechecks without a DOM lib. Mirrors Next's dev indicator, tuned to Pylon.
|
|
1169
1170
|
*/
|
|
1170
1171
|
function pylonDevHud() {
|
|
1171
1172
|
const g: any = globalThis;
|
|
@@ -1181,6 +1182,7 @@ function pylonDevHud() {
|
|
|
1181
1182
|
// Marker the sync engine checks before publishing its dev status probe.
|
|
1182
1183
|
g.__PYLON_DEV__ = info;
|
|
1183
1184
|
|
|
1185
|
+
// Client errors.
|
|
1184
1186
|
const errs: string[] = [];
|
|
1185
1187
|
const onErr = (m: any) => {
|
|
1186
1188
|
errs.push(String(m));
|
|
@@ -1191,7 +1193,68 @@ function pylonDevHud() {
|
|
|
1191
1193
|
g.addEventListener("unhandledrejection", (e: any) => onErr((e && e.reason) || e));
|
|
1192
1194
|
}
|
|
1193
1195
|
|
|
1194
|
-
|
|
1196
|
+
// /api activity: wrap fetch ONCE to observe Pylon API calls (method, path,
|
|
1197
|
+
// status, ms, and a policy/error reason on non-2xx). Transparent — returns the
|
|
1198
|
+
// ORIGINAL promise untouched; the body is read from a clone only on errors.
|
|
1199
|
+
const api: any[] = [];
|
|
1200
|
+
if (g.fetch && !g.__pylonFetchWrapped) {
|
|
1201
|
+
g.__pylonFetchWrapped = true;
|
|
1202
|
+
const orig = g.fetch.bind(g);
|
|
1203
|
+
g.fetch = function (input: any, init: any) {
|
|
1204
|
+
let url = "";
|
|
1205
|
+
try {
|
|
1206
|
+
url = typeof input === "string" ? input : (input && input.url) || "";
|
|
1207
|
+
} catch (_e) {}
|
|
1208
|
+
const method = String(
|
|
1209
|
+
(init && init.method) || (input && input.method) || "GET",
|
|
1210
|
+
).toUpperCase();
|
|
1211
|
+
const t0 = g.performance && g.performance.now ? g.performance.now() : Date.now();
|
|
1212
|
+
const p = orig(input, init);
|
|
1213
|
+
if (url.indexOf("/api/") !== -1 && p && p.then) {
|
|
1214
|
+
const path = url.replace(/^https?:\/\/[^/]+/, "").split("?")[0];
|
|
1215
|
+
const done = (status: number, reason: string) => {
|
|
1216
|
+
const t1 = g.performance && g.performance.now ? g.performance.now() : Date.now();
|
|
1217
|
+
api.push({
|
|
1218
|
+
method,
|
|
1219
|
+
path,
|
|
1220
|
+
status,
|
|
1221
|
+
ms: Math.round((t1 - t0) * 10) / 10,
|
|
1222
|
+
reason,
|
|
1223
|
+
denied: status === 403,
|
|
1224
|
+
});
|
|
1225
|
+
if (api.length > 40) api.shift();
|
|
1226
|
+
};
|
|
1227
|
+
p.then(
|
|
1228
|
+
(res: any) => {
|
|
1229
|
+
if (!res.ok && res.clone) {
|
|
1230
|
+
res
|
|
1231
|
+
.clone()
|
|
1232
|
+
.json()
|
|
1233
|
+
.then((b: any) =>
|
|
1234
|
+
done(
|
|
1235
|
+
res.status,
|
|
1236
|
+
(b && b.error && (b.error.message || b.error.code)) || (b && b.message) || "",
|
|
1237
|
+
),
|
|
1238
|
+
)
|
|
1239
|
+
.catch(() => done(res.status, ""));
|
|
1240
|
+
} else {
|
|
1241
|
+
done(res.status, "");
|
|
1242
|
+
}
|
|
1243
|
+
},
|
|
1244
|
+
() => done(0, "network error"),
|
|
1245
|
+
);
|
|
1246
|
+
}
|
|
1247
|
+
return p;
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
const C = {
|
|
1252
|
+
ok: "#3fb950",
|
|
1253
|
+
warn: "#d29922",
|
|
1254
|
+
bad: "#f85149",
|
|
1255
|
+
dim: "#6e7681",
|
|
1256
|
+
txt: "#e6edf3",
|
|
1257
|
+
};
|
|
1195
1258
|
const make = (tag: string, css: string, text?: string) => {
|
|
1196
1259
|
const n = d.createElement(tag);
|
|
1197
1260
|
n.style.cssText = css;
|
|
@@ -1199,7 +1262,11 @@ function pylonDevHud() {
|
|
|
1199
1262
|
return n;
|
|
1200
1263
|
};
|
|
1201
1264
|
const dot = (color: string) =>
|
|
1202
|
-
make(
|
|
1265
|
+
make(
|
|
1266
|
+
"span",
|
|
1267
|
+
"display:inline-block;width:7px;height:7px;border-radius:50%;flex:0 0 auto;background:" +
|
|
1268
|
+
color,
|
|
1269
|
+
);
|
|
1203
1270
|
|
|
1204
1271
|
const box = make(
|
|
1205
1272
|
"div",
|
|
@@ -1207,18 +1274,108 @@ function pylonDevHud() {
|
|
|
1207
1274
|
);
|
|
1208
1275
|
const panel = make(
|
|
1209
1276
|
"div",
|
|
1210
|
-
"background:#0d1117;border:1px solid #30363d;border-radius:8px;padding:9px 11px;margin-bottom:6px;min-width:
|
|
1277
|
+
"background:#0d1117;border:1px solid #30363d;border-radius:8px;padding:9px 11px;margin-bottom:6px;min-width:260px;max-width:380px;box-shadow:0 8px 28px rgba(0,0,0,.45)",
|
|
1211
1278
|
);
|
|
1212
1279
|
const pill = make(
|
|
1213
1280
|
"button",
|
|
1214
1281
|
"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
1282
|
);
|
|
1216
1283
|
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1284
|
+
// The expandable detail area below the rows.
|
|
1285
|
+
let activeDrawer: string | null = null;
|
|
1286
|
+
const drawer = make(
|
|
1287
|
+
"div",
|
|
1288
|
+
"display:none;margin-top:7px;padding-top:7px;border-top:1px solid #21262d;max-height:200px;overflow:auto",
|
|
1289
|
+
);
|
|
1290
|
+
const drawerLine = (label: string, value: string, color?: string) => {
|
|
1291
|
+
const r = make("div", "display:flex;gap:8px;margin:2px 0");
|
|
1292
|
+
r.appendChild(make("span", "color:#8b949e;flex:0 0 110px", label));
|
|
1293
|
+
r.appendChild(make("span", "color:" + (color || C.txt) + ";flex:1;word-break:break-word", value));
|
|
1294
|
+
drawer.appendChild(r);
|
|
1295
|
+
};
|
|
1296
|
+
const renderDrawer = () => {
|
|
1297
|
+
if (!activeDrawer) {
|
|
1298
|
+
drawer.style.display = "none";
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
drawer.style.display = "block";
|
|
1302
|
+
drawer.textContent = "";
|
|
1303
|
+
if (activeDrawer === "cache") {
|
|
1304
|
+
const f = (info.cache && info.cache.flags) || {};
|
|
1305
|
+
const optIn = f.bucketOptIn
|
|
1306
|
+
? "auth-bucketed"
|
|
1307
|
+
: f.revalidateSecs != null
|
|
1308
|
+
? "revalidate=" + f.revalidateSecs + "s"
|
|
1309
|
+
: "none";
|
|
1310
|
+
drawerLine("opt-in", optIn, optIn === "none" ? C.warn : C.ok);
|
|
1311
|
+
drawerLine("props.auth", f.authTouched ? "read ✗" : "untouched ✓", f.authTouched ? C.bad : C.ok);
|
|
1312
|
+
drawerLine(
|
|
1313
|
+
"headers/cookies",
|
|
1314
|
+
f.dynamicTouched ? "read ✗" : "untouched ✓",
|
|
1315
|
+
f.dynamicTouched ? C.bad : C.ok,
|
|
1316
|
+
);
|
|
1317
|
+
drawerLine("props.session", f.sessionTouched ? "read (bucket bit)" : "untouched", C.dim);
|
|
1318
|
+
drawerLine("set-cookie", String(f.cookieCount || 0), f.cookieCount ? C.bad : C.ok);
|
|
1319
|
+
drawerLine("strict policies", f.strictPolicies ? "on ✗" : "off ✓", f.strictPolicies ? C.bad : C.ok);
|
|
1320
|
+
drawerLine("streaming", f.wantsStream ? "yes" : "no", C.dim);
|
|
1321
|
+
drawerLine("status", String(f.status != null ? f.status : "—"), f.status === 200 ? C.ok : C.warn);
|
|
1322
|
+
} else if (activeDrawer === "api") {
|
|
1323
|
+
if (!api.length) {
|
|
1324
|
+
drawerLine("", "no /api calls yet", C.dim);
|
|
1325
|
+
} else {
|
|
1326
|
+
for (let i = api.length - 1; i >= 0; i--) {
|
|
1327
|
+
const a = api[i];
|
|
1328
|
+
const col =
|
|
1329
|
+
a.status === 0 || a.status >= 500 || a.denied
|
|
1330
|
+
? C.bad
|
|
1331
|
+
: a.status >= 400
|
|
1332
|
+
? C.warn
|
|
1333
|
+
: C.ok;
|
|
1334
|
+
drawer.appendChild(
|
|
1335
|
+
make(
|
|
1336
|
+
"div",
|
|
1337
|
+
"margin:2px 0;color:" + col + ";word-break:break-word",
|
|
1338
|
+
a.method +
|
|
1339
|
+
" " +
|
|
1340
|
+
a.path +
|
|
1341
|
+
" " +
|
|
1342
|
+
(a.status || "ERR") +
|
|
1343
|
+
" · " +
|
|
1344
|
+
a.ms +
|
|
1345
|
+
"ms" +
|
|
1346
|
+
(a.reason ? " — " + a.reason : ""),
|
|
1347
|
+
),
|
|
1348
|
+
);
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
} else if (activeDrawer === "errors") {
|
|
1352
|
+
if (!errs.length) {
|
|
1353
|
+
drawerLine("", "no errors", C.dim);
|
|
1354
|
+
} else {
|
|
1355
|
+
for (let i = errs.length - 1; i >= 0; i--) {
|
|
1356
|
+
drawer.appendChild(
|
|
1357
|
+
make("div", "margin:2px 0;color:" + C.bad + ";word-break:break-word", errs[i]),
|
|
1358
|
+
);
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
};
|
|
1363
|
+
|
|
1364
|
+
// A row; pass `key` to make it click-to-expand the matching drawer section.
|
|
1365
|
+
const rowEl = (label: string, key?: string) => {
|
|
1366
|
+
const r = make(
|
|
1367
|
+
"div",
|
|
1368
|
+
"display:flex;align-items:flex-start;gap:8px;margin:3px 0" + (key ? ";cursor:pointer" : ""),
|
|
1369
|
+
);
|
|
1370
|
+
r.appendChild(make("span", "color:#8b949e;flex:0 0 60px", key ? label + " ▸" : label));
|
|
1220
1371
|
const v = make("span", "color:#e6edf3;word-break:break-word;flex:1");
|
|
1221
1372
|
r.appendChild(v);
|
|
1373
|
+
if (key) {
|
|
1374
|
+
r.onclick = () => {
|
|
1375
|
+
activeDrawer = activeDrawer === key ? null : key;
|
|
1376
|
+
renderDrawer();
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1222
1379
|
panel.appendChild(r);
|
|
1223
1380
|
return v;
|
|
1224
1381
|
};
|
|
@@ -1230,13 +1387,15 @@ function pylonDevHud() {
|
|
|
1230
1387
|
: cache.verdict + (cache.secs ? " · " + cache.secs + "s" : "");
|
|
1231
1388
|
rowEl("route").textContent = info.route || "—";
|
|
1232
1389
|
rowEl("page").textContent = info.component || "—";
|
|
1233
|
-
const cacheV = rowEl("cache");
|
|
1390
|
+
const cacheV = rowEl("cache", "cache");
|
|
1234
1391
|
cacheV.textContent = cacheLabel + (cache.reason ? " · " + cache.reason : "");
|
|
1235
1392
|
cacheV.style.color = cache.verdict === "dynamic" ? C.warn : C.ok;
|
|
1236
1393
|
rowEl("render").textContent =
|
|
1237
1394
|
(info.renderMode || "ssr") + (info.renderMs != null ? " · " + info.renderMs + "ms" : "");
|
|
1238
1395
|
const syncV = rowEl("sync");
|
|
1239
|
-
const
|
|
1396
|
+
const apiV = rowEl("api", "api");
|
|
1397
|
+
const errV = rowEl("errors", "errors");
|
|
1398
|
+
panel.appendChild(drawer);
|
|
1240
1399
|
|
|
1241
1400
|
pill.appendChild(dot(cache.verdict === "dynamic" ? C.warn : C.ok));
|
|
1242
1401
|
pill.appendChild(make("span", "font-weight:600;color:#e6edf3", "pylon"));
|
|
@@ -1277,8 +1436,14 @@ function pylonDevHud() {
|
|
|
1277
1436
|
syncV.style.color = C.dim;
|
|
1278
1437
|
pillSyncDot.style.background = online ? C.dim : C.bad;
|
|
1279
1438
|
}
|
|
1280
|
-
|
|
1439
|
+
const failed = api.filter((a) => a.denied || a.status === 0 || a.status >= 400).length;
|
|
1440
|
+
apiV.textContent =
|
|
1441
|
+
api.length === 0 ? "—" : api.length + " calls" + (failed ? " · " + failed + " failed" : "");
|
|
1442
|
+
apiV.style.color = failed ? C.bad : api.length ? C.ok : C.dim;
|
|
1443
|
+
errV.textContent = String(errs.length);
|
|
1281
1444
|
errV.style.color = errs.length ? C.bad : C.dim;
|
|
1445
|
+
// Keep the live sections (api / errors) fresh while open.
|
|
1446
|
+
if (activeDrawer === "api" || activeDrawer === "errors") renderDrawer();
|
|
1282
1447
|
};
|
|
1283
1448
|
|
|
1284
1449
|
box.appendChild(panel);
|
|
@@ -2485,6 +2650,27 @@ export async function handleRenderRoute(
|
|
|
2485
2650
|
wantsStream,
|
|
2486
2651
|
status: responseState.status,
|
|
2487
2652
|
});
|
|
2653
|
+
// Dev diagnostics (the agent + HUD signal): the cache verdict + the single
|
|
2654
|
+
// actionable reason, computed ONCE here and reused for the dev HUD blob, the
|
|
2655
|
+
// `x-pylon-dev` header (→ the host's diagnostics ring + `pylon diagnostics`),
|
|
2656
|
+
// and the structured dev log. Null in prod.
|
|
2657
|
+
const renderMode = wantsStream ? "ssr-streaming" : "ssr-buffered";
|
|
2658
|
+
const devVerdict = isDevMode()
|
|
2659
|
+
? describeCacheVerdict({
|
|
2660
|
+
bucketOptIn,
|
|
2661
|
+
cacheable,
|
|
2662
|
+
bucketable,
|
|
2663
|
+
revalidateSecs,
|
|
2664
|
+
authTouched,
|
|
2665
|
+
dynamicTouched,
|
|
2666
|
+
sessionTouched,
|
|
2667
|
+
cookieCount: responseState.cookies.length,
|
|
2668
|
+
strictPolicies,
|
|
2669
|
+
wantsStream,
|
|
2670
|
+
forceDynamic,
|
|
2671
|
+
status: responseState.status,
|
|
2672
|
+
})
|
|
2673
|
+
: null;
|
|
2488
2674
|
// Serialization view of props built from the RAW `msg` values — the live
|
|
2489
2675
|
// `props` (with its read-tracking proxies) is NEVER mutated back to raw, so
|
|
2490
2676
|
// it can't become a vehicle that leaks a prior request's identity to a later
|
|
@@ -2522,19 +2708,32 @@ export async function handleRenderRoute(
|
|
|
2522
2708
|
type: "response_start",
|
|
2523
2709
|
call_id: msg.call_id,
|
|
2524
2710
|
status: responseState.status,
|
|
2525
|
-
headers: finalizeHeaders(
|
|
2526
|
-
responseState,
|
|
2527
|
-
undefined,
|
|
2711
|
+
headers: finalizeHeaders(responseState, undefined, {
|
|
2528
2712
|
// The #277 anon proof and the Phase 0 bucket proof both ride the TRUSTED
|
|
2529
|
-
// `internal` channel (never stripped
|
|
2530
|
-
//
|
|
2531
|
-
// exclusive (bucketOptIn splits
|
|
2532
|
-
|
|
2713
|
+
// `internal` channel (never stripped here, always stripped by the host
|
|
2714
|
+
// before the client), so userland (page setHeader / route-handler headers
|
|
2715
|
+
// via `extra`) can't forge either. Mutually exclusive (bucketOptIn splits
|
|
2716
|
+
// the two verdicts).
|
|
2717
|
+
...(bucketable
|
|
2533
2718
|
? { "x-pylon-bucket": String(revalidateSecs) }
|
|
2534
2719
|
: cacheable
|
|
2535
2720
|
? { "x-pylon-cacheable": String(revalidateSecs) }
|
|
2536
|
-
:
|
|
2537
|
-
|
|
2721
|
+
: {}),
|
|
2722
|
+
// Dev-only: the host parses this into its diagnostics ring (served at
|
|
2723
|
+
// /_pylon/dev/diagnostics + `pylon diagnostics`). Single-line JSON, no
|
|
2724
|
+
// newlines. Stripped before the client like every x-pylon-* header.
|
|
2725
|
+
...(devVerdict
|
|
2726
|
+
? {
|
|
2727
|
+
"x-pylon-dev": JSON.stringify({
|
|
2728
|
+
verdict: devVerdict.verdict,
|
|
2729
|
+
secs: devVerdict.secs,
|
|
2730
|
+
reason: devVerdict.reason,
|
|
2731
|
+
mode: renderMode,
|
|
2732
|
+
component: msg.component,
|
|
2733
|
+
}),
|
|
2734
|
+
}
|
|
2735
|
+
: {}),
|
|
2736
|
+
}),
|
|
2538
2737
|
});
|
|
2539
2738
|
|
|
2540
2739
|
// Pre-load the manifest BEFORE the React stream starts emitting
|
|
@@ -2692,32 +2891,40 @@ export async function handleRenderRoute(
|
|
|
2692
2891
|
}
|
|
2693
2892
|
|
|
2694
2893
|
// Dev HUD (dev only): append the cache-verdict + render-timing blob + the
|
|
2695
|
-
// floating overlay. After the page tail so
|
|
2696
|
-
// before the deferred client entry boots the
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
revalidateSecs,
|
|
2703
|
-
authTouched,
|
|
2704
|
-
dynamicTouched,
|
|
2705
|
-
sessionTouched,
|
|
2706
|
-
cookieCount: responseState.cookies.length,
|
|
2707
|
-
strictPolicies,
|
|
2708
|
-
wantsStream,
|
|
2709
|
-
forceDynamic,
|
|
2710
|
-
status: responseState.status,
|
|
2711
|
-
});
|
|
2894
|
+
// floating overlay, and emit ONE structured log line. After the page tail so
|
|
2895
|
+
// the HUD marker/probe are in place before the deferred client entry boots the
|
|
2896
|
+
// sync engine. `devVerdict` was computed once above (reused here + in the
|
|
2897
|
+
// x-pylon-dev header). The log rides the runtime's inherited stderr, so an
|
|
2898
|
+
// agent running `pylon dev` sees the verdict without any extra call.
|
|
2899
|
+
if (devVerdict) {
|
|
2900
|
+
const renderMs = Math.round((performance.now() - renderStart) * 10) / 10;
|
|
2712
2901
|
sendChunk(
|
|
2713
2902
|
buildDevHudChunk({
|
|
2714
2903
|
route: msg.url,
|
|
2715
2904
|
component: msg.component,
|
|
2716
|
-
renderMode
|
|
2717
|
-
renderMs
|
|
2718
|
-
|
|
2905
|
+
renderMode,
|
|
2906
|
+
renderMs,
|
|
2907
|
+
// verdict + reason + the raw gate flags, so the HUD's cache drawer can
|
|
2908
|
+
// show the full per-gate breakdown (which check vetoed caching).
|
|
2909
|
+
cache: {
|
|
2910
|
+
...devVerdict,
|
|
2911
|
+
flags: {
|
|
2912
|
+
bucketOptIn,
|
|
2913
|
+
revalidateSecs,
|
|
2914
|
+
authTouched,
|
|
2915
|
+
dynamicTouched,
|
|
2916
|
+
sessionTouched,
|
|
2917
|
+
cookieCount: responseState.cookies.length,
|
|
2918
|
+
strictPolicies,
|
|
2919
|
+
wantsStream,
|
|
2920
|
+
status: responseState.status,
|
|
2921
|
+
},
|
|
2922
|
+
},
|
|
2719
2923
|
}),
|
|
2720
2924
|
);
|
|
2925
|
+
// The structured dev-log line is emitted host-side (Rust tracing) from the
|
|
2926
|
+
// x-pylon-dev header, so it rides the same log stream as every other
|
|
2927
|
+
// [pylon] line — no duplicate console.error here.
|
|
2721
2928
|
}
|
|
2722
2929
|
|
|
2723
2930
|
send({ type: "render_done", call_id: msg.call_id });
|