@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
@@ -1,19 +1,21 @@
1
1
  import { set_current_region, set_foreign_hydrate } from "../current-region.js";
2
2
  import { capture_region_ids } from "./region-ids.js";
3
+ import { nested_context } from "../nested.js";
3
4
  import { foreign_region_prop_revivers } from "./foreign-props.js";
4
5
  import { SEED_REF_KEY, seed_ref_reviver } from "../seed-ref-reviver.js";
5
6
  import { parse_sidecar_text } from "./wire-format.js";
6
7
  import { is_deferred, ours_on_kit_document, region_ssr_truncated } from "./region-attrs.js";
7
8
  import { boot_link, slots } from "./slots.js";
9
+ import { report_thrown } from "../report-thrown.js";
8
10
  import { keep_host_classes, note_host_classes } from "./host-classes.js";
9
11
  import { emit } from "../events/bus.js";
12
+ import { warn } from "../messages/lazy.js";
10
13
  import { hydrate, unmount } from "svelte";
11
- import NestedProvider from "../NestedProvider.svelte";
12
14
  import { install } from "virtual:ogygia/hydrate-features";
13
15
  // src/runtime/hydrate-core.ts
14
16
  /**
15
17
  * THE HYDRATE CORE — everything an island needs only at the moment it hydrates: Svelte's
16
- * `hydrate`/`unmount`, the `NestedProvider` host, the devalue props parse, the page seed, the
18
+ * `hydrate`/`unmount`, the "inside an island" context, the devalue props parse, the page seed, the
17
19
  * DEV props guard. Loaded LAZILY by the region element on its first `#hydrate` (one dynamic
18
20
  * import, shared by every island), so a page whose islands all wake on `visible` or `interaction`
19
21
  * boots the runtime WITHOUT fetching Svelte's client runtime — that arrives with the first island
@@ -25,7 +27,7 @@ import { install } from "virtual:ogygia/hydrate-features";
25
27
  */
26
28
  const seed_page_once = () => slots.page?.seed_page_once();
27
29
  const seed_remote_once = () => slots.remoteSeeds?.seed_once();
28
- const kit_page_thread = () => slots.page?.kit_page_thread() ?? null;
30
+ const kit_page_thread = (signal) => slots.page?.kit_page_thread(signal) ?? null;
29
31
  install();
30
32
  const kit_hydrates_page = () => boot_link().kit_hydrates_page();
31
33
  const props_sidecar_of = (region) => boot_link().props_sidecar_of(region);
@@ -35,14 +37,30 @@ const is_self_owned = (el) => boot_link().is_self_owned(el);
35
37
  const DEVTOOLS = typeof __OGYGIA_DEVTOOLS__ !== "undefined" ? __OGYGIA_DEVTOOLS__ : false;
36
38
  /** The morph target for hydration repair: the server sequence Svelte's walk binds — see `repair_markup`. */
37
39
  const REPAIR_MORPH = { target: "walk" };
38
- /** The hydration ENVELOPE: `hydrate()` anchors on a top-level `<!--[-->` comment and then expects
39
- * the component's OWN region envelope — but embedded SSR (Region.svelte) emits only the inner
40
- * layer. Idempotent: the second attempt after a failed first one finds it in place. */
40
+ /** The hydration ENVELOPE: `hydrate()` anchors on a top-level `<!--[-->` comment, then walks the
41
+ * component's own nodes. Every server shape carries it — a placed island's dynamic-component pair
42
+ * (Region.svelte), `render()`'s root pair (a hole's answer, a live region) — so this only adds one
43
+ * to markup that has none. Idempotent: the second attempt after a failed first one finds it.
44
+ *
45
+ * A placed island an EARLIER build rendered (a hole's answer from a server still on it, mid rolling
46
+ * deploy) carries one pair more: its host's `{#if}` branch (`<!--[0-->`) around the component's
47
+ * pair. That outer pair goes, so the component's own is the envelope and the island is adopted
48
+ * instead of re-rendered. */
41
49
  function ensure_envelope(region) {
42
50
  if (has_envelope(region)) return;
51
+ const first = region.firstChild;
52
+ const last = region.lastChild;
53
+ if (first && last && first !== last && is_comment(first, "[0") && is_comment(first.nextSibling, "[") && is_comment(last, "]")) {
54
+ first.remove();
55
+ last.remove();
56
+ return;
57
+ }
43
58
  region.insertBefore(document.createComment("["), region.firstChild);
44
59
  region.appendChild(document.createComment("]"));
45
60
  }
61
+ function is_comment(node, data) {
62
+ return !!node && node.nodeType === 8 && node.data === data;
63
+ }
46
64
  function has_envelope(region) {
47
65
  const first = region.firstChild;
48
66
  return !!first && first.nodeType === 8 && first.data === "[";
@@ -227,10 +245,11 @@ function describe_divergence(live, want, where = "the island") {
227
245
  }
228
246
  return null;
229
247
  }
230
- /** The first ownership conflict the current repair met (DEV / devtools read it), or `null`. */
248
+ /** The element of the first ownership conflict the current repair met (DEV / devtools read it:
249
+ * `ownership_conflict`), or `null`. */
231
250
  let conflict = null;
232
251
  function note_conflict(el) {
233
- conflict ??= `${node_label(el)} is a web component that changed its own children in a position Svelte's hydration reads, so the repair had to remove the nodes it added. Render that subtree with {@html} or as static markup (Svelte never reads inside it), put it in a lake (wake: 'none'), or mark the element data-ogygia-keep.`;
252
+ conflict ??= node_label(el);
234
253
  }
235
254
  /** Put the island's light DOM back to the server's node sequence when it drifted while the island
236
255
  * slept. Returns whether a repair was made and, in DEV / devtools, WHY it drifted (the first
@@ -311,7 +330,6 @@ function quietly(fn) {
311
330
  console.warn = warn;
312
331
  }
313
332
  }
314
- const HEALED_WARNING = "[ogygia] island \"%s\" was edited while it slept (its light DOM no longer matched the server render — a design-system runtime, an A/B tool, a translator), so its server HTML was put back and hydrated instead of re-rendering it client-side. Whatever the edit added is gone; the script that made it should leave <ogygia-region> subtrees alone.";
315
333
  /** Is this entry another build's (fragment federation): absolute, different origin. */
316
334
  function is_foreign_entry(entry) {
317
335
  return boot_link().ABSOLUTE_URL_SCHEME.test(entry) && new URL(entry).origin !== location.origin;
@@ -356,7 +374,10 @@ var PropMutationGuard = class {
356
374
  const key = entry + "" + prop_path;
357
375
  if (this.#warned.has(key)) return;
358
376
  this.#warned.add(key);
359
- console.warn(`[ogygia] mutating prop '${prop_path}' inside island ${entry} — island props are a serialized snapshot of the host, or a reference into page.data shared with every island that reads it: the write updates nothing (or leaks into page.data). Move mutable state inside the island component.`);
377
+ warn("prop_mutation", {
378
+ prop: prop_path,
379
+ entry
380
+ });
360
381
  }
361
382
  #guard_map(map, entry, prop_path) {
362
383
  const mutators = /* @__PURE__ */ new Set([
@@ -418,24 +439,23 @@ var PropMutationGuard = class {
418
439
  });
419
440
  }
420
441
  wrap(props, entry) {
421
- if (!(import.meta.env && import.meta.env.DEV)) return props;
422
442
  return this.#guard_value(props, entry, "");
423
443
  }
424
444
  };
425
- const prop_guard = new PropMutationGuard();
445
+ const dev_guard = import.meta.env?.DEV ? new PropMutationGuard() : null;
446
+ const prop_guard = { wrap: (props, entry) => dev_guard ? dev_guard.wrap(props, entry) : props };
426
447
  /** Svelte's `unmount`, typed for the opaque app this module hands around. */
427
448
  const unmount_app = (app) => void unmount(app);
428
- /** A handle over a mounted Svelte app: dispose unmounts, `set_props` pushes through a host. */
429
- function handle(app, dispose_with, entry, host) {
449
+ /** A handle over a mounted Svelte app: dispose unmounts, `set_props` pushes into its pushable props. */
450
+ function handle(app, dispose_with, entry, pushed) {
430
451
  const h = { dispose() {
431
452
  try {
432
453
  dispose_with(app);
433
- } catch {}
454
+ } catch (error) {
455
+ report_thrown(error);
456
+ }
434
457
  } };
435
- if (host) {
436
- const live = app;
437
- h.set_props = (props) => live.setProps?.(prop_guard.wrap(props, entry));
438
- }
458
+ if (pushed) h.set_props = (props) => pushed.push(prop_guard.wrap(props, entry));
439
459
  return h;
440
460
  }
441
461
  /**
@@ -452,15 +472,15 @@ function hydrate_island(region, entry, mod, ssr_html = null, touched = true) {
452
472
  const props = capture_region_ids(region, () => read_region_props(region, foreign));
453
473
  if (kit_hydrates_page() && !ours_on_kit_document(region)) {
454
474
  region.setAttribute("data-kit-hydrated", "");
455
- if (import.meta.env.DEV) console.warn(`[ogygia] island "${entry}" is on a csr=true page; Kit hydrates it, so the island directive is redundant here (it behaves as a normal component).`);
475
+ if (import.meta.env.DEV) warn("island_on_kit_page", { entry });
456
476
  return null;
457
477
  }
458
478
  if (!kit_hydrates_page() && !head_markers_neutralized) {
459
479
  head_markers_neutralized = true;
460
480
  neutralize_head_hydration_markers();
461
481
  }
462
- if (import.meta.env.DEV && !is_deferred(region) && region_ssr_truncated(region)) console.warn(`[ogygia] island "${entry}" rendered a BLOCK element inline inside a <p> (or other phrasing-only context). The browser's HTML parser hoisted that block out of the paragraph before hydration, so this region is empty and a SECOND copy is about to mount here — the server-rendered copy is now an orphaned sibling of the paragraph. Fix: make the component render an inline element (e.g. <span> instead of <div>), or place the island on its own line (block context) rather than inside a sentence.`);
463
- const head_before = new Set(document.head.children);
482
+ if (import.meta.env.DEV && !is_deferred(region) && region_ssr_truncated(region)) warn("island_block_in_inline", { entry });
483
+ let head_mark = null;
464
484
  let lifted = slots.lakes.lift(region);
465
485
  const drift = ssr_html !== null && touched && region.isConnected ? repair_if_drifted(region, ssr_html) : {
466
486
  repaired: false,
@@ -469,23 +489,25 @@ function hydrate_island(region, entry, mod, ssr_html = null, touched = true) {
469
489
  };
470
490
  const repaired = drift.repaired;
471
491
  try {
492
+ head_mark = kit_hydrates_page() ? null : boot_link().runtime_session.mark_head();
472
493
  if (!region.isConnected) return null;
473
494
  const ssr_children = Array.from(region.children);
474
495
  const foreign_delegate = foreign && typeof mod.__og_hydrate === "function";
475
496
  const wrapped = foreign ? props : prop_guard.wrap(props, entry);
476
- const KeepHost = slots.keep ?? slots.live;
477
- const keep = region.hasAttribute("data-ogygia-keep") && !!KeepHost;
478
- if (keep && import.meta.env.DEV && !document.querySelector("meta[name=\"ogygia-router\"]")) console.warn(`[ogygia] island "${entry}" has keep:'${region.getAttribute("data-ogygia-keep")}' but the SPA router is off (ogygia({ router: false })) — keep relies on SPA navigation; a full-page load replaces the DOM, so the attribute is a no-op here.`);
497
+ const keep = region.hasAttribute("data-ogygia-keep") && !!slots.Pushable;
498
+ if (keep && import.meta.env.DEV && !document.querySelector("meta[name=\"ogygia-router\"]")) warn("keep_without_router", {
499
+ entry,
500
+ key: region.getAttribute("data-ogygia-keep") || ""
501
+ });
479
502
  /**
480
503
  * ONE hydrate attempt. `recover: false` makes Svelte THROW on a mismatch (`hydration_failed`)
481
504
  * instead of silently discarding the server DOM and re-rendering — that silent path is what
482
505
  * the self-heal below replaces; `recover: true` is that path, the last resort.
483
506
  *
484
- * Hydration envelope: `render()` (region endpoint / deferred swap) has BOTH anchor layers —
485
- * do not wrap again or hydration mismatches. (Verified against svelte 5.56.) Hydrate through
486
- * NestedProvider so descendants see the "inside a hydrated island" context — any nested
487
- * island wrapper then degrades to a plain inline component (single hydration with this
488
- * parent). The provider adds no DOM, so this matches SSR.
507
+ * The component itself is hydrated, no host around it: every server shape is the component's
508
+ * nodes in one anchor pair (see ensure_envelope; a deferred answer carries `render()`'s own, so
509
+ * it is never wrapped again). Its context marks it "inside a hydrated island", so any nested
510
+ * island wrapper degrades to a plain inline component (one hydration, with this parent).
489
511
  */
490
512
  const attempt = (recover) => {
491
513
  if (!is_deferred(region) && !foreign_delegate) ensure_envelope(region);
@@ -496,28 +518,17 @@ function hydrate_island(region, entry, mod, ssr_html = null, touched = true) {
496
518
  origin: new URL(entry).origin,
497
519
  entry
498
520
  });
499
- return handle(mod.__og_hydrate(region, wrapped, recover ? void 0 : { recover: false }), mod.__og_unmount ?? (() => {}), entry, false);
521
+ return handle(mod.__og_hydrate(region, wrapped, recover ? void 0 : { recover: false }), mod.__og_unmount ?? (() => {}), entry, null);
500
522
  }
501
523
  const provided_ctx = capture_region_ids(region, () => slots.context?.(region));
502
524
  const recovery = recover ? {} : { recover: false };
503
- if (keep) return handle(hydrate(KeepHost, {
504
- target: region,
505
- props: {
506
- component: Component,
507
- initialProps: wrapped
508
- },
509
- ...provided_ctx ? { context: provided_ctx } : {},
510
- ...recovery
511
- }), unmount_app, entry, true);
512
- return handle(hydrate(NestedProvider, {
525
+ const pushed = keep ? new slots.Pushable(wrapped) : null;
526
+ return handle(hydrate(Component, {
513
527
  target: region,
514
- props: {
515
- component: Component,
516
- props: wrapped
517
- },
518
- ...provided_ctx ? { context: provided_ctx } : {},
528
+ props: pushed ? pushed.props : wrapped,
529
+ context: nested_context(provided_ctx),
519
530
  ...recovery
520
- }), unmount_app, entry, false);
531
+ }), unmount_app, entry, pushed);
521
532
  } finally {
522
533
  set_current_region(null);
523
534
  set_foreign_hydrate(null);
@@ -544,7 +555,10 @@ function hydrate_island(region, entry, mod, ssr_html = null, touched = true) {
544
555
  fp: region.getAttribute("data-og-fp") || void 0,
545
556
  reason: drift.reason || void 0
546
557
  });
547
- if (import.meta.env.DEV) console.warn(HEALED_WARNING + (drift.reason ? `\nWhat drifted: ${drift.reason}` : ""), entry);
558
+ if (import.meta.env.DEV) warn("island_healed", {
559
+ entry,
560
+ drift: drift.reason
561
+ });
548
562
  }
549
563
  if (drift.conflict) {
550
564
  if (DEVTOOLS) emit({
@@ -554,7 +568,7 @@ function hydrate_island(region, entry, mod, ssr_html = null, touched = true) {
554
568
  fp: region.getAttribute("data-og-fp") || void 0,
555
569
  reason: drift.conflict
556
570
  });
557
- if (import.meta.env.DEV) console.warn(`[ogygia] ${drift.conflict}`, entry);
571
+ if (import.meta.env.DEV) warn("ownership_conflict", { element: drift.conflict }, entry);
558
572
  }
559
573
  slots.lakes.restore(region, lifted);
560
574
  lifted = null;
@@ -571,38 +585,40 @@ function hydrate_island(region, entry, mod, ssr_html = null, touched = true) {
571
585
  fp: region.getAttribute("data-og-fp") || void 0,
572
586
  reason: drift.reason || void 0
573
587
  });
574
- console.warn(`[ogygia] island "${entry}" discarded its ENTIRE server-rendered DOM during hydration and re-rendered client-side (Svelte hydration-mismatch recovery). Three things cause this. (1) Something changed this region's HTML between SSR and wake — a post-SSR transform (transformPageChunk / an HTML-rewriting middleware), an A/B-testing snippet, or an edge rewriter; whatever it injected (e.g. declarative shadow DOM) was just destroyed, and the swap is timing-dependent, so symptoms look erratic. (2) A component in this island reads a page/layout CONTEXT via plain Svelte setContext — on csr=false that runs only on the server, so getContext returns undefined on the client and the island renders a different tree. (3) A block in this island ({#if} / {:else if} / {#each} / {#await}) took a DIFFERENT branch on the client than on the server — its condition reads something that differs between the two (a browser global, a store's client default, url/page state, a Date). Svelte recovers a branch mismatch SILENTLY (no throw); the walk is then off and the rest of the tree follows.` + (drift.reason ? `\nWhat changed: ${drift.reason}` : "") + (strict_error ? `\nSvelte threw on the strict attempt (after repair): ${strict_error instanceof Error ? strict_error.message : String(strict_error)}` : drift.reason ? `\nSvelte did not throw after the repair — the mismatch is one this walk does not model.` : "\nThe server node sequence was INTACT and Svelte did not throw: nothing mutated this region's DOM, so this is (3) (or (2)). Find the block whose branch differs: in the server markup its anchor reads <!--[N--> where N is the branch the SERVER chose.") + "\nFix: for (1) make the mutation invisible to hydration (mutate only <head>, attributes, or shadow templates — never the region's light DOM), or freeze the foreign-owned subtree with a wake:'none' (lake) boundary; for (2) provide the value with setContext / <Provide> / createContext imported from 'ogygia', read it with the same getContext(key); for (3) make the condition read the same value on both sides (a prop / page.data), or move the browser-only branch behind an $effect / hydratedBy().");
588
+ warn("hydration_discarded", {
589
+ entry,
590
+ drift: drift.reason,
591
+ strict: strict_error === null ? null : strict_error instanceof Error ? strict_error.message : String(strict_error)
592
+ });
575
593
  }
576
- if (!kit_hydrates_page()) boot_link().runtime_session.record_island_head(head_before);
594
+ if (head_mark) boot_link().runtime_session.record_island_head(head_mark);
577
595
  return out;
578
596
  } finally {
597
+ head_mark?.stop();
579
598
  if (lifted) slots.lakes.restore(region, lifted);
580
599
  }
581
600
  }
582
601
  /**
583
- * Hydrate a live region's swapped-in HTML through {@link LiveHost} (props-pushable). A live region's
584
- * HTML comes from svelte `render()` (both envelope layers) — same as the deferred swap path, so it
585
- * is NOT wrapped in extra `[..]` hydration comments. `null` when the live feature is absent.
602
+ * Hydrate a live region's swapped-in HTML with pushable props, so a later tick reaches the mounted
603
+ * island in place. Its HTML is svelte `render()`'s (the root anchor pair already there) — the
604
+ * deferred swap's shape, so it is never wrapped again. `null` when the live feature is absent.
586
605
  */
587
606
  function hydrate_live(region, entry, mod, props) {
588
607
  seed_remote_once();
589
608
  seed_page_once();
590
- const LiveHost = slots.live;
591
- if (!LiveHost) {
592
- if (import.meta.env.DEV) console.warn("[ogygia] live region needs the live feature plugin (LiveHost missing)");
609
+ if (!slots.Pushable) {
610
+ if (import.meta.env.DEV) warn("live_feature_missing");
593
611
  return null;
594
612
  }
595
613
  const provided_ctx = capture_region_ids(region, () => slots.context?.(region));
596
614
  const host_classes = note_host_classes(region);
597
- const app = hydrate(LiveHost, {
615
+ const pushed = new slots.Pushable(prop_guard.wrap(props, entry));
616
+ const app = hydrate(mod.default, {
598
617
  target: region,
599
- props: {
600
- component: mod.default,
601
- initialProps: prop_guard.wrap(props, entry)
602
- },
603
- ...provided_ctx ? { context: provided_ctx } : {}
618
+ props: pushed.props,
619
+ context: nested_context(provided_ctx)
604
620
  });
605
621
  keep_host_classes(host_classes);
606
- return handle(app, unmount_app, entry, true);
622
+ return handle(app, unmount_app, entry, pushed);
607
623
  }
608
624
  export { hydrate_island, hydrate_live, is_foreign_entry, kit_page_thread, neutralize_head_hydration_markers, read_region_props, repair_if_drifted, seed_page_once, seed_remote_once, sequence_differs };
@@ -0,0 +1,33 @@
1
+ /** Whether `error` is a dynamic import that failed to load (any engine's wording), not an error the
2
+ * module threw as it ran. */
3
+ export declare function is_failed_import(error: unknown): boolean;
4
+ /**
5
+ * The URL a failed dynamic import names, if it names one we can load again (http(s), this origin).
6
+ * WebKit names none: there `known`, the file the caller imported, is it (#383).
7
+ */
8
+ export declare function failed_import_url(error: unknown, known?: string): string | null;
9
+ /** `url` with its retry mark: a URL the module map has not seen. */
10
+ export declare function retry_url(url: string, attempt: number): string;
11
+ /** The status of the response the browser's resource timing holds for `url` (0 where the browser does
12
+ * not say), or `null` when none came: a download that failed (or no timing for it). */
13
+ export declare function response_of(url: string): number | null;
14
+ /** The boot hands over its message for a chunk no retry can load. Said once per page (for the first
15
+ * such chunk: one reload loads them all). */
16
+ export declare function on_reload_needed(say: (url: string, error: unknown) => void): void;
17
+ /**
18
+ * `load` (a bundler-resolved `() => import('./x.ts')`), and after it fails to download, the same
19
+ * file under a fresh URL on each later call; once it has loaded, every later call is that load (a
20
+ * fresh URL after a success would be a second copy of the module). A retry whose own file came and
21
+ * still failed is a dependency's failure, which no URL fixes: from then on every call fails at once
22
+ * with its error, asking nothing. Pair it with `flights()` from `@ogygia/machine`, which calls it
23
+ * again on a failure and never keeps one.
24
+ */
25
+ export declare function import_again<T>(load: () => Promise<T>, fresh?: (url: string) => Promise<unknown>, known?: string): () => Promise<T>;
26
+ /**
27
+ * The module at `url` (an island's own file), through {@link import_again}: a download that fails is
28
+ * asked for again at once under a fresh URL, and by every later load of that file. A plain
29
+ * `import()` of it once left an island whose file dropped one download dead for the tab (#261). A
30
+ * file the server answered for (a 404 after a deploy) is not asked for again at once: the caller's
31
+ * own recovery (./entry-locations.ts, the current build's file) is what helps there.
32
+ */
33
+ export declare function import_file<T>(url: string): Promise<T>;
@@ -0,0 +1,217 @@
1
+ import { STALE, flights, machine, scope } from "@ogygia/machine";
2
+ // src/runtime/import-again.ts
3
+ /**
4
+ * A LAZY CHUNK THAT FAILED TO DOWNLOAD, ASKED FOR AGAIN. A browser keeps a module whose download
5
+ * failed in its module map for the life of the page: a second `import()` of the same URL fails at
6
+ * once, without a request (Chromium and Firefox both do; the e2e lazy-chunk-retry spec proves it).
7
+ * Retrying the same `import()` therefore does nothing. What works is the same file under a new URL:
8
+ * the failed import's error names the URL, so the next attempt imports it with an `og-retry` query
9
+ * (a static server ignores it; a module never evaluated, so no second copy of its state exists).
10
+ *
11
+ * Safari's error names no URL: there the retry is the plain import again (it fails the same way),
12
+ * so a dropped chunk stays dropped until the page reloads, as before.
13
+ *
14
+ * One failure no URL fixes: a static dependency of the chunk that failed. The error names the chunk
15
+ * all the same, and the chunk under a fresh URL imports the dependency's own URL, which the browser
16
+ * keeps failed: every retry fails the same way (#262). Told apart by the retry itself — it failed,
17
+ * yet its own file came (the browser's resource timing holds its response): asking again cannot
18
+ * help, so the loader stops asking, and the page says once that it needs a reload.
19
+ *
20
+ * A leaf: the boot and island-side code (devalue's lane, the entry locations) import it, so it
21
+ * imports nothing of the boot (only `@ogygia/machine`, which the boot runs on); the boot hands it
22
+ * its message (`on_reload_needed`).
23
+ */
24
+ /** How long a failed import waits for its file's timing entry before it reads as "no response". */
25
+ const ENTRY_WAIT_MS = 1e3;
26
+ /**
27
+ * The resource-timing entries for `url` once the browser has one, or none after
28
+ * {@link ENTRY_WAIT_MS}. An engine can reject a failed import before the entry lands, and later
29
+ * than one task: Firefox on a loaded machine. Chromium and Firefox give every download an entry (a
30
+ * dropped one too, with a status of 0); WebKit gives a dropped one none, so only there the wait
31
+ * runs out. Its own scope: it ends itself, and no page's visit owns a file's load.
32
+ */
33
+ function entries_landed(url) {
34
+ if (typeof PerformanceObserver === "undefined") return Promise.resolve([]);
35
+ const wait = scope();
36
+ return new Promise((resolve) => {
37
+ const done = (entries) => {
38
+ wait.close();
39
+ resolve(entries);
40
+ };
41
+ wait.watch(new PerformanceObserver((list) => {
42
+ const entries = list.getEntriesByName(url);
43
+ if (entries.length > 0) done(entries);
44
+ })).observe({
45
+ type: "resource",
46
+ buffered: true
47
+ });
48
+ wait.timeout(() => done([]), ENTRY_WAIT_MS);
49
+ });
50
+ }
51
+ /** The URL in a failed dynamic import's message: Chromium "Failed to fetch dynamically imported
52
+ * module: <url>", Firefox "error loading dynamically imported module: <url>". */
53
+ const FAILED_IMPORT_RE = /dynamically imported module:?\s+(\S+)/i;
54
+ /** WebKit's (Safari's) failed import, which names no URL: "Importing a module script failed." */
55
+ const WEBKIT_IMPORT_FAILED_RE = /^importing a module script failed\.?$/i;
56
+ /** Whether `error` is a dynamic import that failed to load (any engine's wording), not an error the
57
+ * module threw as it ran. */
58
+ function is_failed_import(error) {
59
+ const message = error instanceof Error ? error.message : typeof error === "string" ? error : "";
60
+ return FAILED_IMPORT_RE.test(message) || WEBKIT_IMPORT_FAILED_RE.test(message);
61
+ }
62
+ /**
63
+ * The URL a failed dynamic import names, if it names one we can load again (http(s), this origin).
64
+ * WebKit names none: there `known`, the file the caller imported, is it (#383).
65
+ */
66
+ function failed_import_url(error, known) {
67
+ const message = error instanceof Error ? error.message : typeof error === "string" ? error : "";
68
+ const match = FAILED_IMPORT_RE.exec(message);
69
+ const named = match ? match[1] : known !== void 0 && WEBKIT_IMPORT_FAILED_RE.test(message) ? known : null;
70
+ if (named === null) return null;
71
+ try {
72
+ const url = new URL(named, typeof location !== "undefined" ? location.href : void 0);
73
+ if (url.protocol !== "http:" && url.protocol !== "https:") return null;
74
+ if (typeof location !== "undefined" && url.origin !== location.origin) return null;
75
+ return url.href;
76
+ } catch {
77
+ return null;
78
+ }
79
+ }
80
+ /** `url` with its retry mark: a URL the module map has not seen. */
81
+ function retry_url(url, attempt) {
82
+ const u = new URL(url);
83
+ u.searchParams.set("og-retry", String(attempt));
84
+ return u.href;
85
+ }
86
+ /** The status of the response the browser's resource timing holds for `url` (0 where the browser does
87
+ * not say), or `null` when none came: a download that failed (or no timing for it). */
88
+ function response_of(url) {
89
+ if (typeof performance === "undefined" || typeof performance.getEntriesByName !== "function") return null;
90
+ return status_of(performance.getEntriesByName(url, "resource"));
91
+ }
92
+ /** {@link response_of}, read from `entries` (one file's resource-timing entries). */
93
+ function status_of(entries) {
94
+ let status = null;
95
+ for (const e of entries) {
96
+ const t = e;
97
+ if (typeof t.responseStatus === "number") {
98
+ if (t.responseStatus > 0) status = t.responseStatus;
99
+ } else if (t.responseStart > 0) status = 0;
100
+ }
101
+ return status;
102
+ }
103
+ /** What the page says when a chunk cannot load until it reloads: the boot's message (./core.ts). */
104
+ let say_reload_needed = () => {};
105
+ let said = false;
106
+ /** The boot hands over its message for a chunk no retry can load. Said once per page (for the first
107
+ * such chunk: one reload loads them all). */
108
+ function on_reload_needed(say) {
109
+ say_reload_needed = say;
110
+ }
111
+ /**
112
+ * `load` (a bundler-resolved `() => import('./x.ts')`), and after it fails to download, the same
113
+ * file under a fresh URL on each later call; once it has loaded, every later call is that load (a
114
+ * fresh URL after a success would be a second copy of the module). A retry whose own file came and
115
+ * still failed is a dependency's failure, which no URL fixes: from then on every call fails at once
116
+ * with its error, asking nothing. Pair it with `flights()` from `@ogygia/machine`, which calls it
117
+ * again on a failure and never keeps one.
118
+ */
119
+ function import_again(load, fresh = (url) => import(
120
+ /* @vite-ignore */
121
+ url
122
+ ), known) {
123
+ const file = file_definition.create();
124
+ return () => {
125
+ const s = file.now;
126
+ if (s.kind === "loaded") return s.run;
127
+ if (s.kind === "stuck") return Promise.reject(s.error);
128
+ let run;
129
+ if (s.kind === "failed") {
130
+ const attempt = s.attempt + 1;
131
+ file.go("failed", {
132
+ url: s.url,
133
+ attempt
134
+ });
135
+ const url = retry_url(s.url, attempt);
136
+ run = fresh(url).catch((error) => {
137
+ const status = response_of(url);
138
+ if (status !== null && status < 400 && file.move("failed", "stuck", { error })) {
139
+ if (!said) {
140
+ said = true;
141
+ say_reload_needed(url, error);
142
+ }
143
+ }
144
+ throw error;
145
+ });
146
+ } else run = load().catch((error) => {
147
+ const url = failed_import_url(error, known);
148
+ if (url !== null) file.move("fresh", "failed", {
149
+ url,
150
+ attempt: 0
151
+ });
152
+ throw error;
153
+ });
154
+ run.then(() => {
155
+ if (file.now.kind !== "loaded") file.go("loaded", { run });
156
+ }, () => {});
157
+ return run;
158
+ };
159
+ }
160
+ /**
161
+ * One file's loads, as a machine:
162
+ *
163
+ * fresh ─ failed{url, attempt} ⟲ ─ loaded{run}
164
+ * ╲──────────────────────────╱ ╲ stuck{error}
165
+ *
166
+ * `fresh`: nothing failed yet (or a failure named no URL): a call loads the bundler's way. `failed`:
167
+ * a download failed; each call asks under a fresh URL. `loaded`: a load landed, and every later call
168
+ * is that load (a fresh URL after a success would be a second copy of the module). `stuck`: a retry's
169
+ * own file came and it still failed, a dependency's failure: every later call fails at once with it.
170
+ */
171
+ const file_definition = machine()({
172
+ name: "import-again",
173
+ initial: "fresh",
174
+ steps: {
175
+ fresh: ["failed", "loaded"],
176
+ failed: [
177
+ "failed",
178
+ "loaded",
179
+ "stuck"
180
+ ],
181
+ loaded: [],
182
+ stuck: ["loaded"]
183
+ }
184
+ });
185
+ /** Each file's loader, by URL (an island's own file: its load, and the warm ahead of it): what it
186
+ * asks for next (the file, or a fresh URL after a failed download). */
187
+ const file_loaders = /* @__PURE__ */ new Map();
188
+ /** One load of each file at a time, kept once it lands: loads of one file that fail together share
189
+ * one retry, so a failure gets one fresh URL and the module runs once (#271). */
190
+ const file_loads = flights({
191
+ name: "island file",
192
+ run: (url) => {
193
+ let again = file_loaders.get(url);
194
+ if (!again) file_loaders.set(url, again = import_again(() => import(
195
+ /* @vite-ignore */
196
+ url
197
+ ), void 0, url));
198
+ const load = again;
199
+ return load().catch(async (error) => {
200
+ const failed = failed_import_url(error, url);
201
+ if (failed === null) throw error;
202
+ if ((response_of(failed) ?? status_of(await entries_landed(failed))) === null) return load();
203
+ throw error;
204
+ });
205
+ }
206
+ });
207
+ /**
208
+ * The module at `url` (an island's own file), through {@link import_again}: a download that fails is
209
+ * asked for again at once under a fresh URL, and by every later load of that file. A plain
210
+ * `import()` of it once left an island whose file dropped one download dead for the tab (#261). A
211
+ * file the server answered for (a 404 after a deploy) is not asked for again at once: the caller's
212
+ * own recovery (./entry-locations.ts, the current build's file) is what helps there.
213
+ */
214
+ function import_file(url) {
215
+ return file_loads.get(url).then((m) => m === STALE ? import_file(url) : m);
216
+ }
217
+ export { failed_import_url, import_again, import_file, is_failed_import, on_reload_needed, response_of, retry_url };
@@ -1,11 +1,13 @@
1
1
  import { FieldSnapshot, QueuedClick } from "./interaction.js";
2
+ import { Scope } from "@ogygia/machine";
2
3
  /**
3
4
  * Replay the queued clicks in arrival order at each click's address (tag-checked against drift).
4
5
  * One frame later: hydration just (re)bound the island's web-component wiring (a dropdown web
5
6
  * component's `target` element, set by a reactive statement) and many components apply such
6
7
  * changes in their own render tick. A synchronous replay lands before that tick and toggles
7
- * nothing; after a frame the component is settled.
8
+ * nothing; after a frame the component is settled. The frame is `owner`'s (the region's lifetime in
9
+ * the page): a region gone by then replays nothing.
8
10
  */
9
- export declare function replay_clicks(region: Element, queued: QueuedClick[]): void;
11
+ export declare function replay_clicks(region: Element, queued: QueuedClick[], owner: Scope): void;
10
12
  /** The island is live: fields back, focus back, clicks replayed. */
11
- export declare function after_wake(region: Element, fields: FieldSnapshot[], active_addr: number[] | null, queued: QueuedClick[]): void;
13
+ export declare function after_wake(region: Element, fields: FieldSnapshot[], active_addr: number[] | null, queued: QueuedClick[], owner: Scope): void;
@@ -34,9 +34,10 @@ function restore_fields(region, fields) {
34
34
  * One frame later: hydration just (re)bound the island's web-component wiring (a dropdown web
35
35
  * component's `target` element, set by a reactive statement) and many components apply such
36
36
  * changes in their own render tick. A synchronous replay lands before that tick and toggles
37
- * nothing; after a frame the component is settled.
37
+ * nothing; after a frame the component is settled. The frame is `owner`'s (the region's lifetime in
38
+ * the page): a region gone by then replays nothing.
38
39
  */
39
- function replay_clicks(region, queued) {
40
+ function replay_clicks(region, queued, owner) {
40
41
  const replay = () => {
41
42
  for (const q of queued) {
42
43
  const t = resolve_address(region, q.addr);
@@ -46,18 +47,17 @@ function replay_clicks(region, queued) {
46
47
  }
47
48
  queued.length = 0;
48
49
  };
49
- if (typeof requestAnimationFrame === "function") requestAnimationFrame(replay);
50
- else replay();
50
+ owner.timeout(replay, "frame");
51
51
  }
52
52
  /** The island is live: fields back, focus back, clicks replayed. */
53
- function after_wake(region, fields, active_addr, queued) {
53
+ function after_wake(region, fields, active_addr, queued, owner) {
54
54
  restore_fields(region, fields);
55
55
  if (active_addr) {
56
56
  const el = resolve_address(region, active_addr);
57
57
  if (el instanceof HTMLElement && document.activeElement !== el) el.focus();
58
58
  }
59
59
  const replayed = queued.length;
60
- replay_clicks(region, queued);
60
+ replay_clicks(region, queued, owner);
61
61
  if (DEVTOOLS) emit({
62
62
  domain: "runtime",
63
63
  name: "interaction.replay",
@@ -1,3 +1,4 @@
1
+ import { Scope } from "@ogygia/machine";
1
2
  /** Feature entry: fill the `interaction` slot — wake a cold island on first use, warm on hover. */
2
3
  export declare function install(): void;
3
4
  /** Child-index path from `region` down to `el` — survives node replacement by position. */
@@ -13,6 +14,13 @@ export type FieldSnapshot = {
13
14
  addr: number[];
14
15
  tag: string;
15
16
  };
17
+ /**
18
+ * Wake the island for a visitor. `replayed` is what replays their input once it is live (the replay
19
+ * half, loading): the island's wake waits for it before it goes live, so the input lands AT wake. An
20
+ * island that went live first left the replay to come in later, over a slow network, beside the
21
+ * visitor's next click (#260). Resolves when the island is live; rejects when it failed to wake.
22
+ */
23
+ export type WakeFn = (replayed: Promise<unknown>) => Promise<void> | void;
16
24
  export type QueuedClick = {
17
25
  addr: number[];
18
26
  /** The deepest node of the click's composed path when it lies INSIDE a web component's shadow
@@ -27,7 +35,10 @@ export type QueuedClick = {
27
35
  *
28
36
  * @param region the `<ogygia-region>` element
29
37
  * @param warm prefetch the island module (no hydrate) — wired to the pointer entering the region
30
- * @param fire begin hydration; resolves when the island is live
31
- * @returns disarm() — forget the region (disconnected before any interaction)
38
+ * @param fire begin hydration ({@link WakeFn}); resolves when the island is live
39
+ * @param owner the region's lifetime in the page: the frame its clicks replay in is its
40
+ * @returns disarm(over?) — end this arming (the island is live or failed, or the region left
41
+ * before any interaction); `over`: the region left while it was woken, so its queued clicks
42
+ * are dropped instead of replayed
32
43
  */
33
- export declare function arm_interaction(region: Element, warm: () => void, fire: () => Promise<void> | void): () => void;
44
+ export declare function arm_interaction(region: Element, warm: () => void, fire: WakeFn, owner: Scope): (over?: boolean) => void;