@ogygia/core 0.12.2 → 0.13.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.
Files changed (227) hide show
  1. package/dist/LateIsland.svelte +3 -2
  2. package/dist/LateIslandAwait.svelte +6 -2
  3. package/dist/Region.svelte +22 -33
  4. package/dist/boundary.d.ts +0 -2
  5. package/dist/boundary.js +8 -6
  6. package/dist/compiler/debarrel/barrel.d.ts +15 -8
  7. package/dist/compiler/debarrel/barrel.js +92 -45
  8. package/dist/compiler/debarrel/plugin.js +41 -17
  9. package/dist/compiler/dev/dev-hmr.d.ts +3 -0
  10. package/dist/compiler/dev/dev-hmr.js +3 -0
  11. package/dist/compiler/driver.js +11 -4
  12. package/dist/compiler/flags.js +21 -7
  13. package/dist/compiler/fouc-css.js +5 -1
  14. package/dist/compiler/kit.js +5 -1
  15. package/dist/compiler/link/build-output.js +12 -3
  16. package/dist/compiler/macros/bake.js +18 -4
  17. package/dist/compiler/macros/dollar.js +6 -1
  18. package/dist/compiler/macros/source.js +6 -1
  19. package/dist/compiler/macros/store.js +10 -2
  20. package/dist/compiler/macros/wire.js +6 -1
  21. package/dist/compiler/parse/scan.js +10 -3
  22. package/dist/compiler/region/transform.js +188 -74
  23. package/dist/context-bridge.js +13 -5
  24. package/dist/context.d.ts +9 -0
  25. package/dist/context.js +30 -1
  26. package/dist/document.d.ts +1 -1
  27. package/dist/document.js +7 -1
  28. package/dist/early-holes-head.d.ts +5 -0
  29. package/dist/early-holes-head.js +20 -0
  30. package/dist/early-holes.d.ts +34 -0
  31. package/dist/early-holes.js +51 -0
  32. package/dist/entry-hydrate.js +9 -12
  33. package/dist/events/bus.js +7 -2
  34. package/dist/events/schema.d.ts +2 -2
  35. package/dist/fn-transport.js +4 -3
  36. package/dist/hooks.js +94 -163
  37. package/dist/insights/page-assets.js +11 -3
  38. package/dist/insights/page-insights.d.ts +0 -2
  39. package/dist/insights/page-insights.js +14 -10
  40. package/dist/insights/session-insights.js +2 -2
  41. package/dist/internal-instruments.d.ts +3 -1
  42. package/dist/internal-instruments.js +3 -1
  43. package/dist/internal-server.d.ts +3 -2
  44. package/dist/internal-server.js +3 -2
  45. package/dist/internal.d.ts +1 -2
  46. package/dist/internal.js +1 -2
  47. package/dist/keep-fallback.d.ts +0 -2
  48. package/dist/keep-fallback.js +4 -5
  49. package/dist/live-transport.js +3 -2
  50. package/dist/messages/build.d.ts +585 -0
  51. package/dist/messages/build.js +800 -0
  52. package/dist/messages/core.d.ts +464 -0
  53. package/dist/messages/core.js +843 -0
  54. package/dist/messages/define.d.ts +62 -0
  55. package/dist/messages/define.js +49 -0
  56. package/dist/messages/index.d.ts +6 -0
  57. package/dist/messages/index.js +6 -0
  58. package/dist/messages/island.d.ts +4 -0
  59. package/dist/messages/island.js +34 -0
  60. package/dist/messages/lazy.d.ts +3 -0
  61. package/dist/messages/lazy.js +22 -0
  62. package/dist/messages/prose.d.ts +15 -0
  63. package/dist/messages/prose.js +43 -0
  64. package/dist/messages/server.d.ts +142 -0
  65. package/dist/messages/server.js +246 -0
  66. package/dist/nested.d.ts +3 -0
  67. package/dist/nested.js +8 -1
  68. package/dist/plugin/css.js +14 -13
  69. package/dist/plugin/facts.d.ts +2 -0
  70. package/dist/plugin/facts.js +10 -6
  71. package/dist/plugin/hooks.d.ts +7 -17
  72. package/dist/plugin/hooks.js +32 -4
  73. package/dist/plugin/index.d.ts +9 -2
  74. package/dist/plugin/request-scope.d.ts +20 -0
  75. package/dist/plugin/request-scope.js +149 -0
  76. package/dist/plugin/server.d.ts +20 -2
  77. package/dist/plugin/server.js +98 -28
  78. package/dist/plugin/testing.d.ts +6 -0
  79. package/dist/plugin/testing.js +14 -2
  80. package/dist/policies.d.ts +21 -0
  81. package/dist/policies.js +66 -0
  82. package/dist/preference.js +6 -1
  83. package/dist/preload.js +6 -9
  84. package/dist/ref-watch.d.ts +0 -10
  85. package/dist/ref-watch.js +14 -1
  86. package/dist/ref.js +17 -5
  87. package/dist/region-snippet.js +9 -3
  88. package/dist/region.js +2 -1
  89. package/dist/report-thrown.d.ts +9 -0
  90. package/dist/report-thrown.js +15 -0
  91. package/dist/rules.d.ts +17 -0
  92. package/dist/rules.js +57 -0
  93. package/dist/runtime/beacon.d.ts +0 -4
  94. package/dist/runtime/beacon.js +58 -43
  95. package/dist/runtime/boot-link.js +19 -2
  96. package/dist/runtime/client-hooks.js +2 -1
  97. package/dist/runtime/concurrency.d.ts +16 -5
  98. package/dist/runtime/concurrency.js +123 -7
  99. package/dist/runtime/core.d.ts +3 -1
  100. package/dist/runtime/core.js +718 -384
  101. package/dist/runtime/devalue-lane.d.ts +13 -0
  102. package/dist/runtime/devalue-lane.js +49 -0
  103. package/dist/runtime/disclose-version.d.ts +1 -0
  104. package/dist/runtime/disclose-version.js +14 -0
  105. package/dist/runtime/early-holes.d.ts +4 -0
  106. package/dist/runtime/early-holes.js +40 -0
  107. package/dist/runtime/entry-locations.js +40 -49
  108. package/dist/runtime/foreign-props.d.ts +0 -7
  109. package/dist/runtime/foreign-props.js +2 -1
  110. package/dist/runtime/form-continuity.d.ts +2 -2
  111. package/dist/runtime/form-continuity.js +30 -8
  112. package/dist/runtime/frame-nav.d.ts +7 -1
  113. package/dist/runtime/frame-nav.js +84 -18
  114. package/dist/runtime/frame-store.d.ts +25 -16
  115. package/dist/runtime/frame-store.js +124 -98
  116. package/dist/runtime/frames.js +8 -4
  117. package/dist/runtime/hole-facts.js +6 -7
  118. package/dist/runtime/hole-fetch.d.ts +17 -0
  119. package/dist/runtime/hole-fetch.js +47 -0
  120. package/dist/runtime/hydrate-core.d.ts +4 -4
  121. package/dist/runtime/hydrate-core.js +80 -64
  122. package/dist/runtime/import-again.d.ts +33 -0
  123. package/dist/runtime/import-again.js +217 -0
  124. package/dist/runtime/interaction-replay.d.ts +5 -3
  125. package/dist/runtime/interaction-replay.js +6 -6
  126. package/dist/runtime/interaction.d.ts +14 -3
  127. package/dist/runtime/interaction.js +46 -23
  128. package/dist/runtime/kit-page-thread.svelte.d.ts +2 -1
  129. package/dist/runtime/kit-page-thread.svelte.js +44 -20
  130. package/dist/runtime/lakes.d.ts +1 -1
  131. package/dist/runtime/lakes.js +18 -5
  132. package/dist/runtime/live.d.ts +1 -2
  133. package/dist/runtime/live.js +6 -8
  134. package/dist/runtime/load-scheduler.d.ts +29 -18
  135. package/dist/runtime/load-scheduler.js +236 -136
  136. package/dist/runtime/nav-handle.js +25 -6
  137. package/dist/runtime/navigation.d.ts +24 -0
  138. package/dist/runtime/navigation.js +24 -0
  139. package/dist/runtime/ownership.d.ts +1 -1
  140. package/dist/runtime/page-defer.js +3 -3
  141. package/dist/runtime/parse-html.d.ts +15 -4
  142. package/dist/runtime/parse-html.js +24 -5
  143. package/dist/runtime/pushable-props.svelte.d.ts +21 -0
  144. package/dist/runtime/pushable-props.svelte.js +37 -0
  145. package/dist/runtime/region-endpoint-url.js +4 -6
  146. package/dist/runtime/region-machines.d.ts +111 -0
  147. package/dist/runtime/region-machines.js +97 -0
  148. package/dist/runtime/region-sheets.d.ts +31 -0
  149. package/dist/runtime/region-sheets.js +49 -0
  150. package/dist/runtime/restore-report.d.ts +1 -0
  151. package/dist/runtime/restore-report.js +36 -0
  152. package/dist/runtime/restore.d.ts +5 -2
  153. package/dist/runtime/restore.js +33 -14
  154. package/dist/runtime/router-nav.d.ts +32 -12
  155. package/dist/runtime/router-nav.js +320 -228
  156. package/dist/runtime/router.d.ts +83 -19
  157. package/dist/runtime/router.js +297 -129
  158. package/dist/runtime/rules.d.ts +35 -0
  159. package/dist/runtime/rules.js +137 -0
  160. package/dist/runtime/schedule.d.ts +6 -3
  161. package/dist/runtime/schedule.js +172 -57
  162. package/dist/runtime/seeds.d.ts +5 -1
  163. package/dist/runtime/seeds.js +11 -5
  164. package/dist/runtime/session.d.ts +46 -7
  165. package/dist/runtime/session.js +78 -10
  166. package/dist/runtime/slots.d.ts +53 -20
  167. package/dist/runtime/slots.js +2 -3
  168. package/dist/runtime/soft-refresh.d.ts +16 -0
  169. package/dist/runtime/soft-refresh.js +14 -0
  170. package/dist/runtime/svelte-warnings.d.ts +4 -10
  171. package/dist/runtime/svelte-warnings.js +7 -4
  172. package/dist/runtime/wire-classes.js +21 -8
  173. package/dist/runtime/wire-format.js +6 -3
  174. package/dist/script.js +8 -7
  175. package/dist/seed-ref-reviver.d.ts +0 -9
  176. package/dist/seed-ref-reviver.js +2 -1
  177. package/dist/seed-refs.d.ts +46 -36
  178. package/dist/seed-refs.js +131 -92
  179. package/dist/server/document-tail.d.ts +6 -0
  180. package/dist/server/document-tail.js +7 -1
  181. package/dist/server/early-holes-csp.d.ts +14 -0
  182. package/dist/server/early-holes-csp.js +73 -0
  183. package/dist/server/page-facts.d.ts +4 -1
  184. package/dist/server/page-facts.js +4 -1
  185. package/dist/server/page-seed.d.ts +2 -2
  186. package/dist/server/page-seed.js +104 -11
  187. package/dist/server/page-stream.d.ts +14 -1
  188. package/dist/server/page-stream.js +119 -2
  189. package/dist/server/personal-response.d.ts +0 -5
  190. package/dist/server/personal-response.js +6 -2
  191. package/dist/server/props-wire.js +5 -1
  192. package/dist/server/region-endpoint.js +10 -2
  193. package/dist/server/render-cache.d.ts +24 -20
  194. package/dist/server/render-cache.js +99 -40
  195. package/dist/server/render-page.js +46 -15
  196. package/dist/server/request-stats.d.ts +10 -8
  197. package/dist/server/request-stats.js +20 -12
  198. package/dist/server/rules.d.ts +11 -0
  199. package/dist/server/rules.js +64 -0
  200. package/dist/server/signing-key.js +31 -18
  201. package/dist/server/stream-regions.d.ts +14 -1
  202. package/dist/server/stream-regions.js +38 -2
  203. package/dist/shared-state.js +5 -1
  204. package/dist/shims/app-navigation.js +3 -2
  205. package/dist/shims/page-store.svelte.js +16 -2
  206. package/dist/source.js +7 -2
  207. package/dist/store-transport.js +12 -5
  208. package/dist/transport.js +3 -2
  209. package/dist/vite/index.js +83 -74
  210. package/dist/vite/kit-dirs.js +7 -1
  211. package/dist/vite/options.d.ts +2 -1
  212. package/dist/vite/options.js +11 -20
  213. package/dist/vite/package-files.js +13 -3
  214. package/dist/vite/paths.d.ts +2 -0
  215. package/dist/vite/paths.js +4 -4
  216. package/dist/vite/plugin-api.d.ts +6 -5
  217. package/dist/vite/plugin-api.js +42 -14
  218. package/dist/vite/style-preprocess.js +10 -3
  219. package/package.json +8 -1
  220. package/dist/KeepHost.svelte +0 -19
  221. package/dist/KeepHost.svelte.d.ts +0 -10
  222. package/dist/LiveHost.svelte +0 -19
  223. package/dist/LiveHost.svelte.d.ts +0 -10
  224. package/dist/NestedProvider.svelte +0 -13
  225. package/dist/NestedProvider.svelte.d.ts +0 -8
  226. package/dist/runtime/page-cache.d.ts +0 -23
  227. package/dist/runtime/page-cache.js +0 -74
package/dist/hooks.js CHANGED
@@ -13,32 +13,37 @@ import { region_mac_message, secret_has_min_entropy, sign, verify } from "./serv
13
13
  import { expiry_refusal, note_cache_control } from "./server/shared-cache.js";
14
14
  import { app_lifetime } from "./server/region-endpoint.js";
15
15
  import { cached_render, render_cache_key } from "./server/render-cache.js";
16
+ import { render as render$1 } from "./policies.js";
17
+ import { install_server_rules } from "./server/rules.js";
16
18
  import { B64Url } from "./server/payload.js";
17
19
  import { REF_WIRE_KEY, ref_reviver } from "./ref.js";
18
20
  import { current_facts, facts_of, mark_per_visitor, set_current_request } from "./plugin/facts.js";
19
- import { around, request_contexts, start_plugins } from "./plugin/server.js";
21
+ import { around, end_request, request_contexts, start_app_plugins } from "./plugin/server.js";
22
+ import { end_request_scope, own_response, request_scope } from "./plugin/request-scope.js";
20
23
  import { register_wire_kind } from "./live-transport.js";
21
24
  import { register_derived_kind, register_store_kind } from "./store-transport.js";
22
25
  import { register_snippet_kind } from "./region-snippet.js";
23
26
  import { register_fn_kind } from "./fn-transport.js";
24
27
  import { configure_islands_endpoint, is_region_id, is_region_ttl, islands_endpoint } from "./server/endpoint.js";
25
- import { build_parcel, done_parcel } from "./server/stream-regions.js";
28
+ import { batch_stream } from "./server/stream-regions.js";
26
29
  import { dedupe_head_links, page_declares_dev_hmr_script, page_declares_router_meta, page_declares_runtime_script, page_declares_speculation_rules, runtime_first } from "./server/head-presence.js";
27
30
  import { assemble, locate } from "./server/document-assembly.js";
28
- import { error_route_is_csr_true, route_is_csr_true } from "./context.js";
31
+ import { error_route_is_csr_true, route_is_csr_true, take_early_holes } from "./context.js";
32
+ import { early_holes_head } from "./early-holes-head.js";
33
+ import { add_early_holes_csp, with_early_holes_meta_csp } from "./server/early-holes-csp.js";
29
34
  import { merge_seed_ask, shape_page_data } from "./server/seed-shape.js";
30
35
  import { html_has_kit_bootstrap } from "./runtime/kit-boot.js";
31
36
  import { STAND_IN_CSS, check_script, mark, settle } from "./server/reversible.js";
32
37
  import { hole_head } from "./server/hole-graph.js";
33
38
  import { restore, restore_adopt } from "./runtime/restore.js";
34
39
  import { RateLimiter } from "./server/rate-limit.js";
35
- import { KEEP_FALLBACK_HTML } from "./keep-fallback-marker.js";
36
40
  import { PageSeed } from "./server/page-seed.js";
37
- import { analyze, index_seed, json_culprit, set_measure_memo_reader } from "./seed-refs.js";
41
+ import { SeedIndex, analyze, json_culprit, set_measure_memo_reader } from "./seed-refs.js";
38
42
  import { WIRE_FORMAT_ATTR, WIRE_FORMAT_JSON } from "./server/props-wire.js";
39
- import { page_seed_reducers, resolve_script, settle_deferred, stage_deferred } from "./server/page-stream.js";
40
- import { PAGE_DEFER_BOOTSTRAP, PAGE_DEFER_GLOBAL } from "./page-defer.js";
41
- import { ConcurrencyGate } from "./runtime/concurrency.js";
43
+ import { KEEP_FALLBACK_HTML } from "./keep-fallback-marker.js";
44
+ import { page_seed_reducers, settle_deferred, stage_deferred, stream_page_deferred } from "./server/page-stream.js";
45
+ import { PAGE_DEFER_BOOTSTRAP } from "./page-defer.js";
46
+ import { ConcurrencyGate, is_run_timeout } from "./runtime/concurrency.js";
42
47
  import { serialize_provided_context } from "./context-bridge.js";
43
48
  import { escape_script_text } from "./escape.js";
44
49
  import { hole_copy_key } from "./runtime/hash.js";
@@ -55,6 +60,9 @@ import { drop_dead_kit_resolves } from "./server/dead-kit-resolves.js";
55
60
  import "./events/schema.js";
56
61
  import { DEVTOOLS_COOKIE, SWITCH_COOKIE_OPTIONS, switch_in_url } from "./insights/switch.js";
57
62
  import { mark_personal } from "./server/personal-response.js";
63
+ import { OgygiaError, warn } from "./messages/server.js";
64
+ import { ERRORS, WARNINGS } from "./messages/core.js";
65
+ import { define_messages, dev_or_prerender } from "./messages/define.js";
58
66
  import { ogygiaTransport } from "./transport.js";
59
67
  import { document } from "./document.js";
60
68
  import { liftRegions, restoreRegions, scanRegions } from "./server/split-regions.js";
@@ -95,6 +103,8 @@ import { plugins } from "virtual:ogygia/server-plugins";
95
103
  *
96
104
  * @packageDocumentation
97
105
  */
106
+ globalThis[Symbol.for("ogygia.prerendering")] = building;
107
+ globalThis[Symbol.for("ogygia.messages")] ??= define_messages(import.meta.env?.DEV || import.meta.env?.SSR ? ERRORS : null, import.meta.env?.DEV || import.meta.env?.SSR ? WARNINGS : null, dev_or_prerender);
98
108
  /** Is devtools on for this request's browser (a devtools BUILD): the URL's `?og-devtools=on|off`
99
109
  * wins and is written to the response's cookie, else the cookie (insights/switch.ts). */
100
110
  function devtools_switch(event) {
@@ -109,6 +119,7 @@ function devtools_switch(event) {
109
119
  const MAX_REGION_BODY = 2e6;
110
120
  const DEVTOOLS = typeof __OGYGIA_DEVTOOLS__ !== "undefined" ? __OGYGIA_DEVTOOLS__ : false;
111
121
  const DEVTOOLS_LAZY = typeof __OGYGIA_DEVTOOLS_LAZY__ !== "undefined" ? __OGYGIA_DEVTOOLS_LAZY__ : false;
122
+ if (import.meta.env?.DEV || (typeof __OGYGIA_RULES__ !== "undefined" ? __OGYGIA_RULES__ : false)) install_server_rules();
112
123
  /** High-res clock for server-realm devtools timestamps. */
113
124
  const dt_now = () => typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
114
125
  const bags = /* @__PURE__ */ new WeakMap();
@@ -220,7 +231,7 @@ set_source_recorder((tag) => {
220
231
  current_facts()?.sources?.push(tag);
221
232
  });
222
233
  set_page_route_config(route_config);
223
- const plugins$1 = start_plugins(plugins, {
234
+ const plugins$1 = start_app_plugins(plugins, {
224
235
  sign: (message) => sign(signing_key().secret, message),
225
236
  verify: (message, signature) => verify(signing_key().secret, message, signature),
226
237
  lifetime: app_lifetime,
@@ -242,10 +253,13 @@ if (DEVTOOLS) set_server_devtools_recorder((input) => dt_record(bag_of(), input)
242
253
  /** Cap on a batch POST body before `request.json()` buffers it. 32 endpoints × ~8.5kB (props cap
243
254
  * 8192 + URL overhead) ≈ 270kB; 512kB leaves margin. Rejected up front via `content-length`. */
244
255
  const MAX_BATCH_BODY = 524288;
245
- /** Abort waiting on slow region SSR (work may continue — see INVARIANTS · RENDER-TIMEOUT). */
246
- const RENDER_TIMEOUT_MS = 1e4;
256
+ /** Abort waiting on slow region SSR (work may continue — see INVARIANTS · RENDER-TIMEOUT): the
257
+ * `render` policy's deadline (./policies.ts). */
258
+ const RENDER_TIMEOUT_MS = render$1.deadline;
247
259
  /** Process-local cap on concurrent region SSR (M1 CPU amp under valid MAC). */
248
260
  const render_gate = new ConcurrencyGate(4);
261
+ /** The abandoned renders already told (one render's timeout reaches every call that joined it). */
262
+ const told_abandoned = /* @__PURE__ */ new WeakSet();
249
263
  /** The per-plugin contexts of an app with no plugins (shared: never written to). */
250
264
  const NO_CTXS = [];
251
265
  /**
@@ -385,31 +399,6 @@ function seed_codecs(transport) {
385
399
  }
386
400
  return hit;
387
401
  }
388
- /** `</body>` as bytes (ASCII: the same in every UTF-8 document). */
389
- const BODY_CLOSE_BYTES = [
390
- 60,
391
- 47,
392
- 98,
393
- 111,
394
- 100,
395
- 121,
396
- 62
397
- ];
398
- /** Does `</body>` end in `chunk` — searched backwards from its end, where it sits — or straddle the
399
- * boundary with the previous chunk's last bytes (`carry`, at most 6)? No decode. */
400
- function has_body_close(carry, chunk) {
401
- const at_bytes = (buf, i) => {
402
- for (let k = 0; k < 7; k++) if (buf[i + k] !== BODY_CLOSE_BYTES[k]) return false;
403
- return true;
404
- };
405
- for (let i = chunk.lastIndexOf(60); i !== -1; i = i === 0 ? -1 : chunk.lastIndexOf(60, i - 1)) if (at_bytes(chunk, i)) return true;
406
- if (!carry.length) return false;
407
- const seam = new Uint8Array(carry.length + Math.min(6, chunk.length));
408
- seam.set(carry);
409
- seam.set(chunk.subarray(0, seam.length - carry.length), carry.length);
410
- for (let i = 0; i + 7 <= seam.length; i++) if (at_bytes(seam, i)) return true;
411
- return false;
412
- }
413
402
  function csr_page_of(html, body_end, event, bag) {
414
403
  return event?.route?.id != null ? bag?.page?.error != null ? error_route_is_csr_true(event.route.id) : route_is_csr_true(event.route.id) : body_end !== -1 && html_has_kit_bootstrap(html, body_end);
415
404
  }
@@ -424,6 +413,21 @@ function page_ctx_script(bag) {
424
413
  /** The restorer, inline (runtime/restore.ts — self-contained, so its source runs as is): restores the
425
414
  * document now, and stays as `__og_restore` / `__og_restore_adopt` for hole answers and router swaps. */
426
415
  const RESTORER_SCRIPT = `<script data-og-restore>(function(){var r=${restore.toString()};window.__og_restore=r;window.__og_restore_adopt=${restore_adopt.toString()};r(document)})()<\/script>`;
416
+ /** `inner` with the request's owner around it: made before `inner` runs, ended with its response
417
+ * (or at once when it throws). */
418
+ function owned(inner) {
419
+ return async (input) => {
420
+ const owner = request_scope(input.event.request);
421
+ let response;
422
+ try {
423
+ response = await inner(input);
424
+ } catch (error) {
425
+ end_request_scope(owner);
426
+ throw error;
427
+ }
428
+ return own_response(owner, response, input.event.request);
429
+ };
430
+ }
427
431
  var OgygiaHandle = class {
428
432
  #endpoint;
429
433
  #speculation_rules;
@@ -435,7 +439,7 @@ var OgygiaHandle = class {
435
439
  this.#transform = options.transform;
436
440
  configure_islands_endpoint(options.endpoint);
437
441
  this.#endpoint = islands_endpoint();
438
- if (options.secret?.trim() && !import.meta.env.DEV && !secret_has_min_entropy(options.secret)) throw new Error(`[ogygia] handle({ secret }) is too short for production (need ≥16 UTF-8 bytes).`);
442
+ if (options.secret?.trim() && !import.meta.env.DEV && !secret_has_min_entropy(options.secret)) throw new OgygiaError("handle_secret_too_short", { min: 16 });
439
443
  configure_signing_secret(options.secret);
440
444
  this.#speculation_rules = enabled ? "" : mpaSpeculationRules(this.#endpoint);
441
445
  this.render_rate = new RateLimiter({
@@ -448,7 +452,7 @@ var OgygiaHandle = class {
448
452
  windowMs: rateLimit.windowMs
449
453
  });
450
454
  }
451
- handle = around(plugins$1, (input) => this.#core(input));
455
+ handle = owned(around(plugins$1, (input) => this.#core(input)));
452
456
  #core = async ({ event, resolve }) => {
453
457
  const path = decode_pathname(event.url.pathname);
454
458
  if (path === null) return new Response("Bad Request", { status: 400 });
@@ -462,9 +466,7 @@ var OgygiaHandle = class {
462
466
  }
463
467
  return await this.#route(event, resolve, path, ctxs, facts);
464
468
  } finally {
465
- for (let i = 0; i < plugins$1.length; i++) try {
466
- plugins$1[i].def.done?.(ctxs[i]);
467
- } catch {}
469
+ end_request(plugins$1, ctxs, event.platform);
468
470
  }
469
471
  };
470
472
  /** One signed hole rendered in place with THIS request's event (a plugin's `ctx.renderHole`:
@@ -525,6 +527,9 @@ var OgygiaHandle = class {
525
527
  };
526
528
  const response = await resolve(event, { transformPageChunk: async ({ html }) => this.inject_client_seeds(this.#transform ? await this.#transform_document(html, event, bag) : html, store?.state, event, bag) });
527
529
  if (devtools_personal) mark_personal(response);
530
+ if (bag.early_holes?.length) try {
531
+ add_early_holes_csp(response.headers);
532
+ } catch {}
528
533
  if (this.#transform && (response.headers.get("content-type") ?? "").includes("text/html")) try {
529
534
  response.headers.append("vary", "x-ogygia-spa");
530
535
  } catch {}
@@ -536,7 +541,7 @@ var OgygiaHandle = class {
536
541
  let out = response;
537
542
  for (let i = 0; i < ctxs.length; i++) out = await plugins$1[i].def.response?.(ctxs[i], out) ?? out;
538
543
  if (out !== response) return out;
539
- if (bag.deferred && bag.deferred.length) return this.stream_page_deferred(response, bag.deferred, bag.defer_next_id, bag.seed_reducers ?? void 0);
544
+ if (bag.deferred && bag.deferred.length) return stream_page_deferred(request_scope(event.request), response, bag.deferred, bag.defer_next_id, bag.seed_reducers ?? void 0);
540
545
  if (!bag.kit_client && !response.headers.has("content-length") && (response.headers.get("content-type") ?? "").includes("text/html")) return drop_dead_kit_resolves(response);
541
546
  return response;
542
547
  }
@@ -566,17 +571,7 @@ var OgygiaHandle = class {
566
571
  }
567
572
  if (!Array.isArray(parsed) || parsed.length === 0) return region_response("Bad Request", { status: 400 });
568
573
  const calls = parsed.filter((e) => typeof e === "string").slice(0, MAX_BATCH);
569
- const render = (endpoint) => this.#render_capability(endpoint, event, true);
570
- const encoder = new TextEncoder();
571
- return region_response(new ReadableStream({ async start(controller) {
572
- await Promise.all(calls.map(async (endpoint) => {
573
- const out = await render(endpoint).catch(() => null);
574
- const parcel = out ? build_parcel(out.slot, out.html) : null;
575
- if (parcel) controller.enqueue(encoder.encode(parcel));
576
- }));
577
- controller.enqueue(encoder.encode(done_parcel()));
578
- controller.close();
579
- } }), {
574
+ return region_response(batch_stream(calls, (endpoint, signal) => this.#render_capability(endpoint, event, true, signal), request_scope(event.request)), {
580
575
  status: 200,
581
576
  headers: {
582
577
  "content-type": "text/html; charset=utf-8",
@@ -633,23 +628,27 @@ var OgygiaHandle = class {
633
628
  const t_start = performance.now();
634
629
  const spans = locate(html);
635
630
  const head = spans.head_end === -1 ? null : html.slice(0, spans.head_end);
631
+ const early = head === null || !event ? [] : take_early_holes(event.request);
632
+ if (early.length && bag) bag.early_holes = early;
633
+ const early_head = early_holes_head(early);
634
+ const with_early_csp = (h) => early.length ? with_early_holes_meta_csp(h) : h;
636
635
  const csr_page = csr_page_of(html, spans.body_end, event, bag);
637
636
  if (csr_page && bag) bag.kit_client = true;
638
637
  if (csr_page) {
639
638
  let head_inject = "";
640
639
  if (head !== null) {
641
- head_inject = CSR_META;
640
+ head_inject = CSR_META + early_head;
642
641
  if (devtools_boot_url && !head.includes("data-ogygia-devtools-boot")) head_inject += `<script type="module" data-ogygia-devtools-boot src="${devtools_boot_url}"><\/script>`;
643
642
  if (bag) bag.kit_with_runtime = page_declares_runtime_script(head);
644
643
  }
645
644
  if (head !== null && bag?.transform_head) head_inject = bag.transform_head + head_inject;
646
645
  const tail = spans.body_end !== -1 && bag ? (bag.transform_body ?? "") + bag.tail.render(null) + (bag.kit_with_runtime ? page_ctx_script(bag) : "") : "";
647
646
  if (head === null && !tail) return html;
648
- const ordered = head === null ? null : runtime_first(head, null);
647
+ const ordered = head === null ? null : with_early_csp(runtime_first(head, null));
649
648
  return assemble(html, spans, ordered === head ? null : ordered, head_inject, tail);
650
649
  }
651
650
  let head_out = null;
652
- let head_inject = head !== null ? CSR_FALSE_META : "";
651
+ let head_inject = head !== null ? CSR_FALSE_META + early_head : "";
653
652
  if (head !== null) {
654
653
  const deduped = dedupe_head_links(head);
655
654
  if (deduped !== head) head_out = deduped;
@@ -657,7 +656,7 @@ var OgygiaHandle = class {
657
656
  if (!enabled && !page_declares_speculation_rules(probe)) head_inject += `<script type="speculationrules" data-ogygia-speculate>${this.#speculation_rules}<\/script>`;
658
657
  if (enabled && !page_declares_router_meta(probe)) head_inject += `<meta name="ogygia-router" content="${viewTransitions ? "vt" : "plain"}">`;
659
658
  const runtime_tag = enabled && runtime_url && !page_declares_runtime_script(probe) ? runtime_bootstrap(kit_asset) : null;
660
- const ordered = runtime_first(probe, runtime_tag);
659
+ const ordered = with_early_csp(runtime_first(probe, runtime_tag));
661
660
  if (ordered !== probe) head_out = ordered;
662
661
  if (dev_hmr_url && !page_declares_dev_hmr_script(probe)) {
663
662
  head_inject += `<script type="module" data-ogygia-dev-hmr src="${kit_asset(dev_hmr_url)}"><\/script>`;
@@ -676,9 +675,9 @@ var OgygiaHandle = class {
676
675
  const page_snap = bag.page;
677
676
  const seed_wanted = bag.seed_wanted;
678
677
  const detail = request_stats_detailed();
679
- const tail_html = bag.tail.render(seed_wanted ? index_seed(page_snap.data) : null, detail);
678
+ const tail_html = bag.tail.render(seed_wanted ? SeedIndex.of(page_snap.data) : null, detail);
680
679
  let seed_keys = bag.seed_keys;
681
- if (seed_wanted && seed_keys !== "all" && seed_keys !== null) for (const k of index_seed(page_snap.data).touched) seed_keys.add(k);
680
+ if (seed_wanted && seed_keys !== "all" && seed_keys !== null) for (const k of SeedIndex.of(page_snap.data).touched) seed_keys.add(k);
682
681
  const shaped_data = shape_page_data(page_snap.data, seed_keys);
683
682
  let seed_data = shaped_data;
684
683
  let seed_form = page_snap.form;
@@ -822,88 +821,6 @@ var OgygiaHandle = class {
822
821
  if (DEVTOOLS && dt_buf && dt_buf.events.length) scripts.push(emit_ogygia_script("devtools", escape_script_text(JSON.stringify(dt_buf.events))));
823
822
  }
824
823
  /**
825
- * Stream the captured `$page.data` promises into islands (csr=false, real browser load). The
826
- * document — carrying the pending seed + resolve-global bootstrap — is already built by `resolve`;
827
- * here we (1) forward it, (2) DRAIN Kit's own dead csr=false resolve tail so its
828
- * `__sveltekit_<hash> is not defined` never reaches the browser, and (3) emit one
829
- * `<script>__ogygia_page_resolve(id, ok, value)<\/script>` per promise AS IT SETTLES — completion
830
- * order, non-blocking. Each island's `{#await page.data.x}` flips pending → resolved live.
831
- */
832
- stream_page_deferred(response, deferred, initial_next_id, reducers) {
833
- const source = response.body;
834
- if (!source) return response;
835
- const reader = source.getReader();
836
- const encoder = new TextEncoder();
837
- const stream = new ReadableStream({
838
- async start(controller) {
839
- let carry = /* @__PURE__ */ new Uint8Array(0);
840
- try {
841
- for (;;) {
842
- const { value, done } = await reader.read();
843
- if (done) break;
844
- controller.enqueue(value);
845
- if (has_body_close(carry, value)) break;
846
- carry = value.length >= 6 ? value.subarray(value.length - 6) : value;
847
- }
848
- } catch {}
849
- (async () => {
850
- try {
851
- for (;;) {
852
- const { done } = await reader.read();
853
- if (done) break;
854
- }
855
- } catch {}
856
- })();
857
- let pending = deferred.length;
858
- let next_id = initial_next_id;
859
- let closed = false;
860
- const maybe_close = () => {
861
- if (pending === 0 && !closed) {
862
- closed = true;
863
- try {
864
- controller.close();
865
- } catch {}
866
- }
867
- };
868
- const push = (id, ok, value) => {
869
- try {
870
- controller.enqueue(encoder.encode(resolve_script(PAGE_DEFER_GLOBAL, id, {
871
- ok,
872
- value
873
- }, reducers)));
874
- } catch {}
875
- };
876
- const stream_one = (id, promise) => {
877
- Promise.resolve(promise).then((value) => {
878
- const { staged, deferred: nested, next_id: after } = stage_deferred(value, next_id);
879
- next_id = after;
880
- push(id, true, staged);
881
- pending += nested.length;
882
- for (const n of nested) stream_one(n.id, n.promise);
883
- pending -= 1;
884
- maybe_close();
885
- }, (error) => {
886
- push(id, false, error);
887
- pending -= 1;
888
- maybe_close();
889
- });
890
- };
891
- for (const { id, promise } of deferred) stream_one(id, promise);
892
- maybe_close();
893
- },
894
- cancel() {
895
- reader.cancel().catch(() => {});
896
- }
897
- });
898
- const headers = new Headers(response.headers);
899
- headers.delete("content-length");
900
- return new Response(stream, {
901
- status: response.status,
902
- statusText: response.statusText,
903
- headers
904
- });
905
- }
906
- /**
907
824
  * True when a remote's resolved value (deep) contains a region carrying baked SSR HTML — a
908
825
  * page-sized render, not seed data (see the skip in {@link build_remote_seed_script}). Depth-capped:
909
826
  * a region ticket sits shallow in any sane payload (a DocView's `entry.body` is 2 levels deep).
@@ -934,23 +851,25 @@ var OgygiaHandle = class {
934
851
  * if the render threw or timed out. Shared by the endpoint ({@link render_region}) and the
935
852
  * batch path ({@link #render_capability}).
936
853
  */
937
- async #render_component(load, props, cache, report, timing, base, reads_page_data = false) {
854
+ async #render_component(load, props, cache, report, timing, base, reads_page_data = false, signal) {
938
855
  const render_body = async () => {
939
856
  const mod = await load();
940
- const rendered = render(mod.default, {
857
+ const out = await render(mod.default, {
941
858
  props,
942
859
  context: kit_render_context()
943
860
  });
944
- return await Promise.race([Promise.resolve(rendered).then((out) => {
945
- const body = out.body;
946
- if (body === "<!--ogygia:keep-fallback-->") return body;
947
- return hole_head(out.head, base) + body + kit_page_facts_tail();
948
- }), new Promise((_, rej) => setTimeout(() => rej(/* @__PURE__ */ new Error("region render timeout")), RENDER_TIMEOUT_MS))]);
861
+ const body = out.body;
862
+ if (body === "<!--ogygia:keep-fallback-->") return body;
863
+ return hole_head(out.head, base) + body + kit_page_facts_tail();
949
864
  };
950
865
  try {
951
- return await cached_render(async () => {
866
+ return await cached_render(async (render_signal) => {
867
+ const gated = {
868
+ deadline_ms: RENDER_TIMEOUT_MS,
869
+ signal: render_signal
870
+ };
952
871
  await kit_page_ready(reads_page_data);
953
- if (!timing) return render_gate.run(render_body);
872
+ if (!timing) return render_gate.run(render_body, gated);
954
873
  const asked = performance.now();
955
874
  return render_gate.run(async () => {
956
875
  const entered = performance.now();
@@ -960,19 +879,23 @@ var OgygiaHandle = class {
960
879
  } finally {
961
880
  timing.render_ms = performance.now() - entered;
962
881
  }
963
- });
964
- }, cache, Date.now(), report);
882
+ }, gated);
883
+ }, cache, report, signal);
965
884
  } catch (e) {
966
885
  if (is_keep_fallback(e)) return KEEP_FALLBACK_HTML;
967
- if (import.meta.env.DEV) console.warn("[ogygia] region render failed:", e);
886
+ if (signal?.aborted) return null;
887
+ if (is_run_timeout(e) && render_gate.abandoned === 1 && !told_abandoned.has(e)) {
888
+ told_abandoned.add(e);
889
+ warn("region_render_abandoned", { ms: RENDER_TIMEOUT_MS });
890
+ }
891
+ if (import.meta.env.DEV) warn("region_render_failed", e);
968
892
  return null;
969
893
  }
970
894
  }
971
895
  /** A cached hole's memo key: the call and the visitor's session, and — when its tree reads
972
896
  * `page.data` — the page it renders for (one page's facts never answer another page's hole). */
973
897
  #hole_cache_key(id, payload, event, reads_page_data) {
974
- const session = this.#region_session(event);
975
- return render_cache_key(id, payload, reads_page_data ? session + "\n" + page_url_of(event).href : session);
898
+ return render_cache_key(id, payload, this.#region_session(event), reads_page_data ? page_url_of(event).href : null);
976
899
  }
977
900
  /** The session cookie value sealed into a region capability (empty when no `sessionCookie` is
978
901
  * configured) — the same read the MAC verify uses, and the per-user part of the render-cache key. */
@@ -1031,7 +954,7 @@ var OgygiaHandle = class {
1031
954
  register_derived_kind();
1032
955
  props = devalue.parse(B64Url.decode(payload), { [REF_WIRE_KEY]: ref_reviver(false) });
1033
956
  } catch (e) {
1034
- if (import.meta.env.DEV) console.warn("[ogygia] region endpoint 403: island module load / props decode failed", e);
957
+ if (import.meta.env.DEV) warn("endpoint_props_decode_failed", e);
1035
958
  return null;
1036
959
  }
1037
960
  if (props === null || typeof props !== "object" || Array.isArray(props)) return null;
@@ -1043,9 +966,9 @@ var OgygiaHandle = class {
1043
966
  * bad/expired MAC, unknown id, non-serializable props, render error, oversize — resolves to
1044
967
  * `null` so the hole silently falls back to a client fetch. Never throws.
1045
968
  */
1046
- async #render_capability(endpoint, event, batch = false) {
969
+ async #render_capability(endpoint, event, batch = false, signal) {
1047
970
  const q = endpoint.indexOf("?");
1048
- if (q === -1) return null;
971
+ if (q === -1 || signal?.aborted) return null;
1049
972
  const cap = capability_of(new URLSearchParams(endpoint.slice(q + 1)));
1050
973
  const { id, payload, sig } = cap;
1051
974
  if (this.#capability_precheck(cap)) return null;
@@ -1066,7 +989,7 @@ var OgygiaHandle = class {
1066
989
  const t0 = stats ? performance.now() : 0;
1067
990
  const timing = {};
1068
991
  let outcome = "none";
1069
- const body = await this.#render_component(load, props, cache, stats ? (o) => outcome = o : void 0, stats ? timing : void 0, event.url, reads_page_data);
992
+ const body = await this.#render_component(load, props, cache, stats ? (o) => outcome = o : void 0, stats ? timing : void 0, event.url, reads_page_data, signal);
1070
993
  if (stats) record_batch_hole_stats(event.request, {
1071
994
  kind: "hole",
1072
995
  id,
@@ -1078,7 +1001,7 @@ var OgygiaHandle = class {
1078
1001
  ms: performance.now() - t0,
1079
1002
  status: body === null ? 500 : 200
1080
1003
  });
1081
- if (body === null || body.length > MAX_REGION_BODY) return null;
1004
+ if (body === null || body.length > MAX_REGION_BODY || signal?.aborted) return null;
1082
1005
  if (body === "<!--ogygia:keep-fallback-->") return {
1083
1006
  slot: sig,
1084
1007
  html: KEEP_FALLBACK_HTML
@@ -1116,7 +1039,11 @@ var OgygiaHandle = class {
1116
1039
  const renewal = url.searchParams.get("renew") === "1";
1117
1040
  const refused = this.#capability_precheck(cap, renewal);
1118
1041
  if (refused) {
1119
- if (import.meta.env.DEV && refused === "foreign-key") console.warn(`[ogygia] region endpoint 403: id "${id}" was signed by key ${cap.k}, this server signs with ${signing_key().fp}`);
1042
+ if (import.meta.env.DEV && refused === "foreign-key") warn("endpoint_foreign_key", {
1043
+ id,
1044
+ key: String(cap.k),
1045
+ own: signing_key().fp
1046
+ });
1120
1047
  return region_denied(refused);
1121
1048
  }
1122
1049
  if (this.probe_rate.limited(ip)) return region_response("Too Many Requests", { status: 429 });
@@ -1126,7 +1053,7 @@ var OgygiaHandle = class {
1126
1053
  cap = fresh;
1127
1054
  renewed = `${url.pathname}?id=${encodeURIComponent(id)}${cap.render ? `&r=${cap.render}` : ""}&props=${payload}&exp=${cap.exp}${ttl_raw ? `&ttl=${ttl_raw}` : ""}&sig=${cap.sig}&k=${cap.k}`;
1128
1055
  } else if (!this.#verify_region_mac(cap, event)) {
1129
- if (import.meta.env.DEV) console.warn(`[ogygia] region endpoint 403: bad signature for id "${id}"`);
1056
+ if (import.meta.env.DEV) warn("endpoint_bad_signature", { id });
1130
1057
  return region_denied("sig");
1131
1058
  }
1132
1059
  const cache_control = ttl_raw && !renewed ? `private, max-age=${Number(ttl_raw)}` : "no-store";
@@ -1135,7 +1062,11 @@ var OgygiaHandle = class {
1135
1062
  if (this.render_rate.limited(ip)) return region_response("Too Many Requests", { status: 429 });
1136
1063
  const target = capability_target(cap);
1137
1064
  if (target === null) {
1138
- if (import.meta.env.DEV) console.warn(`[ogygia] region endpoint 403: id "${id}"${cap.render ? ` (component ${cap.render})` : ""} is not in the server manifest (${Object.keys(island_url).length} known)`);
1065
+ if (import.meta.env.DEV) warn("endpoint_unknown_id", {
1066
+ id,
1067
+ component: cap.render || void 0,
1068
+ known: Object.keys(island_url).length
1069
+ });
1139
1070
  return region_denied("unknown-id");
1140
1071
  }
1141
1072
  const load = (await load_islands()).islands[target];
@@ -1,4 +1,5 @@
1
1
  import { find_unscoped } from "../unscoped-css.js";
2
+ import { scope } from "@ogygia/machine";
2
3
  // src/insights/page-assets.ts
3
4
  /**
4
5
  * THE PAGE'S WEIGHT — every file the rendered document makes the browser fetch at start, found in
@@ -315,7 +316,7 @@ async function weigh_assets(opts) {
315
316
  if (w === void 0) {
316
317
  const got = await weigh_one(r, opts, deadline, origin);
317
318
  if ("miss" in got) {
318
- if (got.miss.startsWith("status ")) opts.cache.set(r.url, null);
319
+ if (got.gone) opts.cache.set(r.url, null);
319
320
  miss(r.url, got.miss);
320
321
  continue;
321
322
  }
@@ -564,13 +565,20 @@ function byte_length(s) {
564
565
  }
565
566
  return n;
566
567
  }
568
+ /** A status that says the file is not there for good: a client error, but not a timeout or a
569
+ * rate limit (408, 429), which say "not now", as a 5xx does. */
570
+ const gone_for_good = (status) => status >= 400 && status < 500 && status !== 408 && status !== 429;
567
571
  async function weigh_one(r, opts, deadline, origin) {
568
572
  const left = deadline - Date.now();
569
573
  if (left < 100) return { miss: "budget" };
570
574
  try {
571
- const res = await Promise.race([opts.fetch_url(r.url.startsWith(origin) ? r.url.slice(origin.length) || "/" : r.url), new Promise((ok) => setTimeout(() => ok(null), left))]);
575
+ const wait = scope();
576
+ const res = await Promise.race([opts.fetch_url(r.url.startsWith(origin) ? r.url.slice(origin.length) || "/" : r.url), new Promise((ok) => wait.timeout(() => ok(null), left))]).finally(() => wait.close());
572
577
  if (!res) return { miss: "timeout" };
573
- if (!res.ok) return { miss: `status ${res.status}` };
578
+ if (!res.ok) return gone_for_good(res.status) ? {
579
+ miss: `status ${res.status}`,
580
+ gone: true
581
+ } : { miss: `status ${res.status}` };
574
582
  const enc = res.headers.get("content-encoding");
575
583
  const len = Number(res.headers.get("content-length"));
576
584
  const buf = new Uint8Array(await res.arrayBuffer());
@@ -425,8 +425,6 @@ export interface PreloadMiss {
425
425
  crossorigin: string | null;
426
426
  /** the answer's `Vary` header, when the devtools read it (a font downloaded again) */
427
427
  vary?: string;
428
- /** a hole the runtime asked for: the preload is ogygia's own (on any endpoint path) */
429
- hole?: true;
430
428
  }
431
429
  export interface HoleWait {
432
430
  name: string;
@@ -1197,21 +1197,20 @@ function analyze_page(page, regions, failures = [], now = Infinity, cpu = null)
1197
1197
  };
1198
1198
  const kb = (n) => n >= 1024 ? `${(n / 1024).toFixed(1)} KB` : `${n} B`;
1199
1199
  const safari = !!page.browser?.startsWith("Safari");
1200
- const ours = (m) => m.type === "fetch" && m.crossorigin === "anonymous" && m.hole === true;
1201
1200
  const safari_fetch = (m) => safari && m.type === "fetch" && m.crossorigin !== null;
1202
1201
  const varies = (m) => !!m.vary && m.vary.toLowerCase().split(",").some((v) => v.trim() === "origin" || v.trim() === "*");
1203
1202
  const safari_font = (m) => safari && m.type === "font" && m.crossorigin !== null && varies(m);
1204
- const why = (m) => safari_font(m) ? `its answer says \`Vary: ${m.vary}\`: ${page.browser} does not reuse a font preload that varies by Origin once the @font-face asking for it comes from a stylesheet added after the page parsed${page.dev ? " (on a dev server both are common: it answers every file with Vary: Origin and adds the page's CSS by script)" : ""}` : ours(m) ? `ogygia's own hole preload and its runtime's fetch match (crossorigin="anonymous", credentials same-origin)${safari ? `; ${page.browser} never hands a fetch preload to a later fetch() — whatever its crossorigin or Cache-Control — so it fetched the answer again` : ""}` : safari_fetch(m) ? `${page.browser} never hands a fetch preload to a later fetch(), whatever its crossorigin, credentials or Cache-Control: in Safari it is always a second download (Chromium uses it)` : m.type === "font" && m.crossorigin === null ? "a font always loads in CORS mode: its preload needs `crossorigin`" : m.type === "fetch" && m.crossorigin === null ? "a fetch preload needs `crossorigin` (`anonymous` for a same-origin `fetch()`, `use-credentials` for `credentials: \"include\"`)" : m.type === "fetch" || m.type === "font" ? `its \`crossorigin="${m.crossorigin}"\`${m.crossorigin ? "" : " (anonymous)"} does not match the request's credentials` : m.type === "script" ? "a module script is preloaded with `<link rel=\"modulepreload\">`, not `rel=\"preload\" as=\"script\"`" : "its `as` or `crossorigin` does not match how the page requests the file";
1203
+ const why = (m) => safari_font(m) ? `its answer says \`Vary: ${m.vary}\`: ${page.browser} does not reuse a font preload that varies by Origin once the @font-face asking for it comes from a stylesheet added after the page parsed${page.dev ? " (on a dev server both are common: it answers every file with Vary: Origin and adds the page's CSS by script)" : ""}` : safari_fetch(m) ? `${page.browser} never hands a fetch preload to a later fetch(), whatever its crossorigin, credentials or Cache-Control: in Safari it is always a second download (Chromium uses it)` : m.type === "font" && m.crossorigin === null ? "a font always loads in CORS mode: its preload needs `crossorigin`" : m.type === "fetch" && m.crossorigin === null ? "a fetch preload needs `crossorigin` (`anonymous` for a same-origin `fetch()`, `use-credentials` for `credentials: \"include\"`)" : m.type === "fetch" || m.type === "font" ? `its \`crossorigin="${m.crossorigin}"\`${m.crossorigin ? "" : " (anonymous)"} does not match the request's credentials` : m.type === "script" ? "a module script is preloaded with `<link rel=\"modulepreload\">`, not `rel=\"preload\" as=\"script\"`" : "its `as` or `crossorigin` does not match how the page requests the file";
1205
1204
  const wasted = misses.reduce((a, m) => a + m.bytes, 0);
1206
1205
  findings.push({
1207
1206
  code: "preload-unused",
1208
1207
  severity: "warn",
1209
1208
  message: `${list(misses.map((m) => `${file(m.url)} (${kb(m.bytes)})`))} ${misses.length === 1 ? "was" : "were"} preloaded, then downloaded again: the browser could not use the preload${misses.length === 1 ? "" : "s"}, and the page paid ${kb(wasted)} twice.`,
1210
- fix: `${[...new Set(misses.map(why))].map((w) => w[0].toUpperCase() + w.slice(1)).join(". ")}.` + (misses.every(ours) ? " Nothing in the page's markup to change: it is the hole preload ogygia writes, in a browser that never uses one." : [
1211
- misses.some((m) => !ours(m) && safari_fetch(m)) ? " No attribute changes a fetch preload in Safari: to start the request early there too, begin it in an inline script and hand its promise to the code that reads it — or drop the preload where Safari is most of the traffic." : "",
1209
+ fix: `${[...new Set(misses.map(why))].map((w) => w[0].toUpperCase() + w.slice(1)).join(". ")}.` + [
1210
+ misses.some(safari_fetch) ? " No attribute changes a fetch preload in Safari: to start the request early there too, begin it in an inline script and hand its promise to the code that reads it — or drop the preload where Safari is most of the traffic." : "",
1212
1211
  misses.some(safari_font) ? page.dev ? " For the fonts: a build drops the dev server's habits (its CSS linked, its files answered by the host), so check whether the host also answers fonts with Vary: Origin and whether a stylesheet naming them is added by script." : " For the fonts: answer them without `Vary: Origin` (a font is the same whoever asks; a CORS header alone does not need it), or name them in a stylesheet the HTML links." : "",
1213
- misses.some((m) => !ours(m) && !safari_fetch(m) && !safari_font(m)) ? " The preload and the request must match exactly, or the browser fetches the file again." : ""
1214
- ].join("")),
1212
+ misses.some((m) => !safari_fetch(m) && !safari_font(m)) ? " The preload and the request must match exactly, or the browser fetches the file again." : ""
1213
+ ].join(""),
1215
1214
  fps: []
1216
1215
  });
1217
1216
  }
@@ -1458,18 +1457,23 @@ function lcp_rivals(visit) {
1458
1457
  const below = visit?.images_eager_below;
1459
1458
  const p = visit?.paints;
1460
1459
  if (!visit || !below?.length || typeof p?.lcp !== "number") return null;
1461
- const resources = visit.resources ?? [];
1462
- const res = p.lcp_url ? resources.find((r) => r.url === p.lcp_url) : void 0;
1460
+ const by_url = /* @__PURE__ */ new Map();
1461
+ for (const r of visit.resources ?? []) if (!by_url.has(r.url)) by_url.set(r.url, r);
1462
+ const res = p.lcp_url ? by_url.get(p.lcp_url) : void 0;
1463
1463
  const from = res ? res.req_start ?? res.start : visit.nav?.res_start ?? visit.nav?.res_end ?? 0;
1464
1464
  const to = res ? res.end : p.lcp;
1465
1465
  let bytes = 0;
1466
1466
  const files = [];
1467
+ const named = /* @__PURE__ */ new Set();
1467
1468
  for (const i of below) {
1468
- const r = resources.find((x) => x.url === i.url);
1469
+ const r = by_url.get(i.url);
1469
1470
  if (!r || r.start >= to || r.end <= from) continue;
1470
1471
  bytes += i.bytes;
1471
1472
  const f = i.url.slice(i.url.lastIndexOf("/") + 1).split("?")[0];
1472
- if (!files.includes(f)) files.push(f);
1473
+ if (!named.has(f)) {
1474
+ named.add(f);
1475
+ files.push(f);
1476
+ }
1473
1477
  }
1474
1478
  return bytes ? {
1475
1479
  bytes,
@@ -422,7 +422,7 @@ function analyze_session(s, names, page_url) {
422
422
  else if (e.name === "region.hydrate.conflict") findings.push({
423
423
  code: "hydrate-conflict",
424
424
  severity: "warn",
425
- message: `${name_of(e.fp ?? null)}: ${e.reason ?? "a web component and Svelte wanted the same nodes"}`,
425
+ message: `${name_of(e.fp ?? null)}: ${e.reason ?? "a web component"} changed its own children where Svelte's hydration reads, so the repair removed what it added (render it with {@html}, put it in a lake, or mark it data-ogygia-keep).`,
426
426
  refs: e.fp ? [{
427
427
  kind: "island",
428
428
  fp: e.fp
@@ -444,7 +444,7 @@ function analyze_session(s, names, page_url) {
444
444
  refs: []
445
445
  });
446
446
  }
447
- const errors = s.events.some((e) => e.name === "region.hydrate.failed") ? s.errors.filter((e) => !e.message.startsWith("console.error: [ogygia] hydration failed")) : s.errors;
447
+ const errors = s.events.some((e) => e.name === "region.hydrate.failed") ? s.errors.filter((e) => !(e.message.startsWith("console.error: OgygiaError: [ogygia] hydration failed") || e.message.includes("ogygiajs.dev/e/island_hydrate_failed"))) : s.errors;
448
448
  if (errors.length) findings.push({
449
449
  code: "errors",
450
450
  severity: "error",
@@ -1,5 +1,7 @@
1
1
  import { AttrChange, BeaconPage, beacon_page, beacon_record_cpu, hole_request_times, is_headless } from "./runtime/beacon.js";
2
2
  import { props_sidecar_of } from "./runtime/sidecar.js";
3
+ import { import_again } from "./runtime/import-again.js";
4
+ import { download } from "./policies.js";
3
5
  import { PAGE_SEED_SELECTOR } from "./runtime/seeds.js";
4
6
  import { paint_blocker } from "./runtime/render-blocking.js";
5
7
  import { RegionRefusal } from "./runtime/region-endpoint-url.js";
@@ -10,4 +12,4 @@ import { tool_fetch, tool_made } from "./tool-fetches.js";
10
12
  import { PAGE_DEFER_GLOBAL, PAGE_DEFER_KEY, PAGE_DEFER_REGISTRY_KEY } from "./page-defer.js";
11
13
  import { ISLAND_GRAPH_ATTR, decode_island_graph } from "./island-graph.js";
12
14
  import { UnscopedStyles, find_unscoped, unscoped_finding, unscoped_marker } from "./unscoped-css.js";
13
- export { type AttrChange, type BeaconPage, ISLAND_GRAPH_ATTR, PAGE_DEFER_GLOBAL, PAGE_DEFER_KEY, PAGE_DEFER_REGISTRY_KEY, PAGE_SEED_SELECTOR, REGION_RENDER_CONCURRENCY, type RegionRefusal, type UnscopedStyles, beacon_page, beacon_record_cpu, charge, decode_island_graph, find_unscoped, hole_request_times, is_headless, nearest_attr, own_cost, paint_blocker, props_sidecar_of, tool_fetch, tool_made, unscoped_finding, unscoped_marker };
15
+ export { type AttrChange, type BeaconPage, ISLAND_GRAPH_ATTR, PAGE_DEFER_GLOBAL, PAGE_DEFER_KEY, PAGE_DEFER_REGISTRY_KEY, PAGE_SEED_SELECTOR, REGION_RENDER_CONCURRENCY, type RegionRefusal, type UnscopedStyles, beacon_page, beacon_record_cpu, charge, decode_island_graph, download, find_unscoped, hole_request_times, import_again, is_headless, nearest_attr, own_cost, paint_blocker, props_sidecar_of, tool_fetch, tool_made, unscoped_finding, unscoped_marker };