@ogygia/core 0.12.3 → 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 (226) 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 +17 -10
  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.js +42 -14
  217. package/dist/vite/style-preprocess.js +10 -3
  218. package/package.json +8 -1
  219. package/dist/KeepHost.svelte +0 -19
  220. package/dist/KeepHost.svelte.d.ts +0 -10
  221. package/dist/LiveHost.svelte +0 -19
  222. package/dist/LiveHost.svelte.d.ts +0 -10
  223. package/dist/NestedProvider.svelte +0 -13
  224. package/dist/NestedProvider.svelte.d.ts +0 -8
  225. package/dist/runtime/page-cache.d.ts +0 -23
  226. package/dist/runtime/page-cache.js +0 -74
@@ -0,0 +1,137 @@
1
+ import { Rules } from "../rules.js";
2
+ import { warn } from "../messages/core.js";
3
+ import { STALE, observe } from "@ogygia/machine";
4
+ // src/runtime/rules.ts
5
+ /**
6
+ * THE RUNTIME'S RULES (#313, ../rules.ts): what is always true of a page, checked in dev a task
7
+ * after machine moves (a navigation is the navigation machine's) and by e2e at the end of a test.
8
+ *
9
+ * - `rule_region_mounted_twice`: one mount per region. A region element holds one island at a time,
10
+ * and an island whose element left the page is let go with it.
11
+ * - `rule_address_bar_drift`: the address bar and the router's history model (whose current entry
12
+ * is the page on screen's) agree once no navigation is out.
13
+ * - `rule_old_page_open`: nothing of a page that was left is still open (its visit's scope closed:
14
+ * its listeners, timers, streams).
15
+ * - `rule_failure_uncoded`: no failure leaves ogygia without a code: an uncaught ogygia error names
16
+ * its code's page, and a `STALE` never escapes as a rejection.
17
+ * - `rule_parse_upgraded`: a parsed answer is inert (./parse-html.ts, #382): no element of it was
18
+ * built by a component's constructor before the restore reached it.
19
+ *
20
+ * Each area tells this module what it holds (`note_mount`, `note_visit`, `watch_router`) only when
21
+ * rules are on (`RULES`): every call site folds out of a build without them.
22
+ */
23
+ /** The runtime's rules. */
24
+ const runtime_rules = /* @__PURE__ */ new Rules();
25
+ /** The region elements that hold an island now, and how many. */
26
+ const mounts = /* @__PURE__ */ new Map();
27
+ /** A region element mounted its island. */
28
+ function note_mount(region) {
29
+ mounts.set(region, (mounts.get(region) ?? 0) + 1);
30
+ }
31
+ /** A region element let its island go. */
32
+ function note_unmount(region) {
33
+ const n = (mounts.get(region) ?? 0) - 1;
34
+ if (n > 0) mounts.set(region, n);
35
+ else mounts.delete(region);
36
+ }
37
+ const entry_of = (region) => region.getAttribute("entry") ?? region.tagName;
38
+ function one_mount_per_region() {
39
+ for (const [region, n] of mounts) {
40
+ if (n > 1) return `${entry_of(region)} holds ${n} islands at once`;
41
+ if (!region.isConnected) return `${entry_of(region)} left the page with its island still mounted`;
42
+ }
43
+ return null;
44
+ }
45
+ /** Each visit made, in the order made: one older than the page on screen's is over. */
46
+ const visits = [];
47
+ let visit_n = 0;
48
+ let current = null;
49
+ /** A page visit was made (`current`: it is the page on screen's now). */
50
+ function note_visit(visit, is_current) {
51
+ if (!visits.some((v) => v.visit === visit)) visits.push({
52
+ visit,
53
+ n: ++visit_n
54
+ });
55
+ if (is_current) current = visit;
56
+ }
57
+ function no_old_page_open() {
58
+ const now = visits.find((v) => v.visit === current);
59
+ if (now === void 0) return null;
60
+ for (let i = visits.length - 1; i >= 0; i--) if (visits[i].visit.scope.closed) visits.splice(i, 1);
61
+ const open = visits.filter((v) => v.n < now.n).length;
62
+ return open === 0 ? null : `${open} page visit(s) before the one on screen still open`;
63
+ }
64
+ const no_hash = (href) => href.split("#")[0];
65
+ /** The router started: its history model is checked against the address bar. */
66
+ function watch_router(router) {
67
+ runtime_rules.add("rule_address_bar_drift", () => {
68
+ if (router.nav.now.kind !== "idle") return null;
69
+ const id = history.state?.ogygia;
70
+ if (typeof id !== "string") return null;
71
+ const page = router.history.current;
72
+ if (id !== page.id) return `the address bar is on entry ${id}, the history model on ${page.id}`;
73
+ const bar = no_hash(location.href);
74
+ const model = no_hash(page.url.href);
75
+ return bar === model ? null : `the address bar is ${bar}, the history model's page ${model}`;
76
+ });
77
+ }
78
+ const CODE_LINK_RE = /ogygiajs\.dev\/e\/[a-z_]+/;
79
+ /** The failures that left ogygia without a code, as found (a few). */
80
+ const uncoded = [];
81
+ function failure(reason) {
82
+ if (uncoded.length >= 10) return;
83
+ if (reason === STALE) {
84
+ uncoded.push("a STALE escaped as a rejection");
85
+ return;
86
+ }
87
+ const message = reason instanceof Error ? reason.message : typeof reason === "string" ? reason : "";
88
+ if (message.startsWith("[ogygia]") && !CODE_LINK_RE.test(message)) uncoded.push("an ogygia error with no code: " + message.split("\n")[0].slice(0, 160));
89
+ }
90
+ const no_failure_uncoded = () => uncoded.length === 0 ? null : uncoded.join("; ");
91
+ /** What parses built that they should not have, as found (a few). */
92
+ const parsed_built = [];
93
+ /** The rule's ear on every parse (./parse-html.ts calls it): an element of the parsed fragment that
94
+ * a component's constructor built (a defined name, an instance of its definition). */
95
+ function note_parse(fragment) {
96
+ if (parsed_built.length >= 10 || typeof customElements === "undefined") return;
97
+ const roots = [fragment];
98
+ for (let q = 0; q < roots.length; q++) {
99
+ const walk = document.createTreeWalker(roots[q], 1);
100
+ for (let n = walk.nextNode(); n; n = walk.nextNode()) {
101
+ if (n.shadowRoot) roots.push(n.shadowRoot);
102
+ const ctor = customElements.get(n.localName);
103
+ if (ctor !== void 0 && n instanceof ctor) {
104
+ parsed_built.push(`<${n.localName}> was built while its answer parsed`);
105
+ return;
106
+ }
107
+ }
108
+ }
109
+ }
110
+ const no_parse_upgraded = () => parsed_built.length === 0 ? null : parsed_built.join("; ");
111
+ let installed = false;
112
+ let pending = false;
113
+ /** Checks every rule a task from now (once, however many moves ask), and says what broke. */
114
+ function check_soon() {
115
+ if (pending) return;
116
+ pending = true;
117
+ setTimeout(() => {
118
+ pending = false;
119
+ runtime_rules.say((code, detail) => warn(code, { detail }));
120
+ }, 0);
121
+ }
122
+ /** Rules on: `globalThis.__ogygia_rules()` gives what is broken now (for e2e); in dev they are also
123
+ * checked after machine moves, and what broke is said with its code. */
124
+ function install_rules(dev) {
125
+ if (installed || typeof window === "undefined") return;
126
+ installed = true;
127
+ runtime_rules.add("rule_region_mounted_twice", one_mount_per_region);
128
+ runtime_rules.add("rule_old_page_open", no_old_page_open);
129
+ runtime_rules.add("rule_failure_uncoded", no_failure_uncoded);
130
+ runtime_rules.add("rule_parse_upgraded", no_parse_upgraded);
131
+ globalThis.__og_parsed = note_parse;
132
+ globalThis.__ogygia_rules = () => runtime_rules.broken();
133
+ window.addEventListener("unhandledrejection", (event) => failure(event.reason));
134
+ window.addEventListener("error", (event) => failure(event.error ?? event.message));
135
+ if (dev) observe(check_soon);
136
+ }
137
+ export { install_rules, note_mount, note_unmount, note_visit, runtime_rules, watch_router };
@@ -1,3 +1,4 @@
1
+ import { Scope, Stale } from "@ogygia/machine";
1
2
  export declare function yield_task(): Promise<void>;
2
3
  /**
3
4
  * Run `fn` at background priority, off the current task: `scheduler.postTask` at 'background' where
@@ -21,14 +22,16 @@ export declare function background_start(fn: () => void): void;
21
22
  * so it waits one task instead. Once resolved it stays resolved: a late region (SPA swap, a hole's
22
23
  * answer) passes straight through.
23
24
  */
24
- export declare function after_document_painted(): Promise<void>;
25
+ export declare function after_document_painted(): Promise<void | Stale>;
25
26
  /** Test seam: forget the resolved gate (a test that fakes a fresh document). */
26
27
  export declare function reset_document_painted(): void;
27
28
  /**
28
29
  * Register a region with the scheduler at connect: stamp its order, start its viewport snapshot.
29
- * Cheap on purpose (a stamp and an `observe`) — connect runs for every region in one task.
30
+ * Cheap on purpose (a stamp and an `observe`) — connect runs for every region in one task. The
31
+ * snapshot is `owner`'s (the region's lifetime in the page): it stops at the region's turn, or when
32
+ * the owner ends.
30
33
  */
31
- export declare function register_region(el: Element): void;
34
+ export declare function register_region(el: Element, owner: Scope): void;
32
35
  /**
33
36
  * Drop a region from the scheduler (disconnect): its snapshot stops and any pending turn is handed
34
37
  * out at once — the waiting hydrate step then sees `isConnected === false` and stands down (or, if
@@ -1,4 +1,5 @@
1
- import { observe } from "./observe.js";
1
+ import { observe as observe$1 } from "./observe.js";
2
+ import { flights, machine } from "@ogygia/machine";
2
3
  // src/runtime/schedule.ts
3
4
  /**
4
5
  * THE HYDRATION SCHEDULER — one island per task, viewport first.
@@ -28,17 +29,100 @@ import { observe } from "./observe.js";
28
29
  const SNAPSHOT_WAIT_MS = 48;
29
30
  /** How long a ready below-the-fold island holds for a viewport island still loading its module. */
30
31
  const HOLD_FOR_VIEWPORT_MS = 1e3;
31
- const pending = [];
32
- let draining = false;
32
+ let pending_count = 0;
33
+ let next_seq = 0;
34
+ const heap_vis = [];
35
+ const heap_rest = [];
36
+ const turns_of = /* @__PURE__ */ new Map();
37
+ const unsnapped = [];
38
+ let unsnapped_head = 0;
39
+ /**
40
+ * The drain, as a machine:
41
+ *
42
+ * idle ─ queued ─ idle a drain waits for its task (`yield_task`), then runs
43
+ * ╲ waiting{at} ╱ a drain waits for a time: the snapshot, or a viewport island
44
+ *
45
+ * `waiting` owns its timer (its visit's scope), so leaving it drops the timer: a drain queued
46
+ * meanwhile runs sooner and works out the waits again, and a sooner wait replaces a later one
47
+ * (#236). One state in place of the `draining` flag beside a `wait_timer` and its `wait_at`.
48
+ */
49
+ const drainer = machine()({
50
+ name: "hydrate-drain",
51
+ initial: "idle",
52
+ steps: {
53
+ idle: ["queued", "waiting"],
54
+ queued: ["idle"],
55
+ waiting: ["queued", "waiting"]
56
+ }
57
+ }).create();
33
58
  /** Connect-order stamp per region — document order at upgrade, insertion order after a morph. */
34
59
  const order_of = /* @__PURE__ */ new WeakMap();
35
60
  let next_order = 0;
36
61
  /** The snapshot: intersecting now? Absent = the observer has not reported yet. */
37
62
  const in_viewport = /* @__PURE__ */ new WeakMap();
38
- const unobserve_of = /* @__PURE__ */ new WeakMap();
63
+ /** Each region's viewport snapshot while it runs (closing it stops the snapshot). */
64
+ const snapshot_of = /* @__PURE__ */ new WeakMap();
39
65
  /** Regions whose hydrate has STARTED (module in flight) and not yet taken its turn. */
40
66
  const loading = /* @__PURE__ */ new Set();
41
- let wait_timer = null;
67
+ /** The loading regions the snapshot says intersect the viewport (what a hold waits for). */
68
+ const loading_vis = /* @__PURE__ */ new Set();
69
+ /** Keep `loading_vis` in step after `el`'s loading state or snapshot changed. */
70
+ function sync_loading(el) {
71
+ if (loading.has(el) && in_viewport.get(el) === true) loading_vis.add(el);
72
+ else loading_vis.delete(el);
73
+ }
74
+ function stop_loading(el) {
75
+ loading_vis.delete(el);
76
+ return loading.delete(el);
77
+ }
78
+ /** Connect order, then request order: the pick order within one heap. */
79
+ const before = (a, b) => a.order < b.order || a.order === b.order && a.seq < b.seq;
80
+ function heap_push(h, t) {
81
+ let i = h.push(t) - 1;
82
+ while (i > 0) {
83
+ const p = i - 1 >> 1;
84
+ if (!before(h[i], h[p])) break;
85
+ [h[i], h[p]] = [h[p], h[i]];
86
+ i = p;
87
+ }
88
+ }
89
+ function heap_pop(h) {
90
+ const last = h.pop();
91
+ if (!h.length) return;
92
+ h[0] = last;
93
+ for (let i = 0;;) {
94
+ const l = 2 * i + 1;
95
+ const r = l + 1;
96
+ let m = i;
97
+ if (l < h.length && before(h[l], h[m])) m = l;
98
+ if (r < h.length && before(h[r], h[m])) m = r;
99
+ if (m === i) return;
100
+ [h[i], h[m]] = [h[m], h[i]];
101
+ i = m;
102
+ }
103
+ }
104
+ /** The heap's first live turn (stale entries on top are dropped on the way). */
105
+ function heap_top(h, vis) {
106
+ while (h.length && (h[0].done || h[0].vis !== vis)) heap_pop(h);
107
+ return h[0];
108
+ }
109
+ /** File each pending turn of `el` in the heap its snapshot now says. */
110
+ function refile(el) {
111
+ const vis = in_viewport.get(el) === true;
112
+ for (const t of turns_of.get(el) ?? []) {
113
+ if (t.vis === vis) continue;
114
+ t.vis = vis;
115
+ heap_push(vis ? heap_vis : heap_rest, t);
116
+ }
117
+ }
118
+ /** The turn leaves the queue (taken, or its region went away). */
119
+ function retire(t) {
120
+ t.done = true;
121
+ pending_count--;
122
+ const list = turns_of.get(t.el);
123
+ list.splice(list.indexOf(t), 1);
124
+ if (!list.length) turns_of.delete(t.el);
125
+ }
42
126
  /**
43
127
  * Yield to the event loop: resolves in a NEW task, so whatever ran before is a finished task and
44
128
  * the browser may render and handle input in between. `scheduler.yield()` where it exists (it
@@ -99,7 +183,11 @@ function post_background(fn) {
99
183
  } catch {}
100
184
  setTimeout(fn, 0);
101
185
  }
102
- let document_painted = null;
186
+ /** The gate below, once for the document (a test's reset drops it). */
187
+ const document_painted = flights({
188
+ name: "document painted",
189
+ run: () => until_painted()
190
+ });
103
191
  /**
104
192
  * Resolves once the document is parsed AND painted: after `DOMContentLoaded`, then one rendered
105
193
  * frame. This is the moment SvelteKit starts hydrating (its start runs from a deferred module
@@ -115,7 +203,10 @@ let document_painted = null;
115
203
  * answer) passes straight through.
116
204
  */
117
205
  function after_document_painted() {
118
- return document_painted ??= new Promise((resolve) => {
206
+ return document_painted.get();
207
+ }
208
+ function until_painted() {
209
+ return new Promise((resolve) => {
119
210
  if (typeof document === "undefined") return resolve();
120
211
  const painted = () => {
121
212
  if (document.visibilityState === "hidden" || typeof requestAnimationFrame !== "function") setTimeout(resolve, 0);
@@ -142,19 +233,27 @@ function dom_content_loaded() {
142
233
  }
143
234
  /** Test seam: forget the resolved gate (a test that fakes a fresh document). */
144
235
  function reset_document_painted() {
145
- document_painted = null;
236
+ document_painted.drop(() => true);
146
237
  }
147
238
  /**
148
239
  * Register a region with the scheduler at connect: stamp its order, start its viewport snapshot.
149
- * Cheap on purpose (a stamp and an `observe`) — connect runs for every region in one task.
240
+ * Cheap on purpose (a stamp and an `observe`) — connect runs for every region in one task. The
241
+ * snapshot is `owner`'s (the region's lifetime in the page): it stops at the region's turn, or when
242
+ * the owner ends.
150
243
  */
151
- function register_region(el) {
244
+ function register_region(el, owner) {
152
245
  if (!order_of.has(el)) order_of.set(el, next_order++);
153
- if (unobserve_of.has(el)) return;
154
- unobserve_of.set(el, observe(el, "0px", (intersecting) => {
246
+ if (snapshot_of.has(el) || owner.closed) return;
247
+ const stop = observe$1(el, "0px", (intersecting) => {
155
248
  in_viewport.set(el, intersecting);
156
- if (pending.length) schedule_drain();
157
- }));
249
+ sync_loading(el);
250
+ refile(el);
251
+ if (pending_count) schedule_drain();
252
+ });
253
+ snapshot_of.set(el, stop);
254
+ owner.defer(() => {
255
+ if (snapshot_of.get(el) === stop) stop_snapshot(el);
256
+ });
158
257
  }
159
258
  /**
160
259
  * Drop a region from the scheduler (disconnect): its snapshot stops and any pending turn is handed
@@ -164,26 +263,29 @@ function register_region(el) {
164
263
  function unregister_region(el) {
165
264
  stop_snapshot(el);
166
265
  in_viewport.delete(el);
167
- loading.delete(el);
168
- for (let i = pending.length - 1; i >= 0; i--) {
169
- if (pending[i].el !== el) continue;
170
- const [turn] = pending.splice(i, 1);
266
+ stop_loading(el);
267
+ const list = turns_of.get(el);
268
+ if (list) for (const turn of list.slice().reverse()) {
269
+ retire(turn);
171
270
  turn.resolve();
172
271
  }
173
- if (pending.length) schedule_drain();
272
+ if (pending_count) schedule_drain();
174
273
  }
175
274
  function stop_snapshot(el) {
176
- unobserve_of.get(el)?.();
177
- unobserve_of.delete(el);
275
+ const stop = snapshot_of.get(el);
276
+ if (stop === void 0) return;
277
+ snapshot_of.delete(el);
278
+ stop();
178
279
  }
179
280
  /** The island's module is in flight: a viewport island here holds ready islands below the fold. */
180
281
  function hydrate_started(el) {
181
282
  loading.add(el);
283
+ sync_loading(el);
182
284
  }
183
285
  /** The hydrate attempt is over (turn taken, or it never got there — no entry, a failed import). */
184
286
  function hydrate_settled(el) {
185
- if (!loading.delete(el)) return;
186
- if (pending.length) schedule_drain();
287
+ if (!stop_loading(el)) return;
288
+ if (pending_count) schedule_drain();
187
289
  }
188
290
  /**
189
291
  * Wait for this region's turn. Resolves in a task of its own, after every region ahead of it in
@@ -192,12 +294,21 @@ function hydrate_settled(el) {
192
294
  */
193
295
  function hydrate_turn(el) {
194
296
  return new Promise((resolve) => {
195
- pending.push({
297
+ const turn = {
196
298
  el,
197
299
  order: order_of.get(el) ?? (order_of.set(el, next_order), next_order++),
300
+ seq: next_seq++,
198
301
  queued_at: now(),
199
- resolve
200
- });
302
+ resolve,
303
+ vis: in_viewport.get(el) === true,
304
+ done: false
305
+ };
306
+ pending_count++;
307
+ heap_push(turn.vis ? heap_vis : heap_rest, turn);
308
+ const list = turns_of.get(el);
309
+ if (list) list.push(turn);
310
+ else turns_of.set(el, [turn]);
311
+ if (!in_viewport.has(el)) unsnapped.push(turn);
201
312
  schedule_drain();
202
313
  });
203
314
  }
@@ -205,52 +316,56 @@ function now() {
205
316
  return typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
206
317
  }
207
318
  function schedule_drain() {
208
- if (draining) return;
209
- draining = true;
319
+ if (drainer.now.kind === "queued") return;
320
+ drainer.go("queued");
210
321
  yield_task().then(drain);
211
322
  }
212
- /** Re-drain after `ms` (one timer at a time — the earliest wait wins, later ones ride it). */
323
+ /** Re-drain after `ms`. One wait at a time, and it is the EARLIEST: a sooner wait replaces a later
324
+ * one (a short snapshot wait set during a long hold), a later one rides the sooner. */
213
325
  function drain_after(ms) {
214
- if (wait_timer) return;
215
- wait_timer = setTimeout(() => {
216
- wait_timer = null;
217
- schedule_drain();
218
- }, Math.max(1, ms));
326
+ const at = now() + Math.max(1, ms);
327
+ const s = drainer.now;
328
+ if (s.kind === "waiting" && s.at <= at) return;
329
+ drainer.go("waiting", { at });
330
+ drainer.scope.timeout(schedule_drain, Math.max(1, ms));
331
+ }
332
+ /**
333
+ * The earliest-requested pending turn still waiting for its snapshot within its wait, or none.
334
+ * Turns are requested in time order, so the ones ahead of it that reported, went, or ran out of
335
+ * wait are behind for good (the clock only moves on) and leave the queue.
336
+ */
337
+ function first_unsnapped(t) {
338
+ while (unsnapped_head < unsnapped.length) {
339
+ const p = unsnapped[unsnapped_head];
340
+ if (!p.done && !in_viewport.has(p.el) && SNAPSHOT_WAIT_MS - (t - p.queued_at) > 0) return p;
341
+ unsnapped_head++;
342
+ }
343
+ unsnapped.length = unsnapped_head = 0;
219
344
  }
220
345
  /** One turn per call: pick the best pending region, hand it its task; the next pick after a yield. */
221
346
  function drain() {
222
- draining = false;
223
- if (!pending.length) return;
347
+ drainer.go("idle");
348
+ if (!pending_count) return;
224
349
  const t = now();
225
350
  const hidden = typeof document !== "undefined" && document.visibilityState === "hidden";
226
- if (!hidden) for (const p of pending) {
227
- if (in_viewport.has(p.el)) continue;
228
- const left = SNAPSHOT_WAIT_MS - (t - p.queued_at);
229
- if (left > 0) return drain_after(left);
351
+ if (!hidden) {
352
+ const p = first_unsnapped(t);
353
+ if (p) return drain_after(SNAPSHOT_WAIT_MS - (t - p.queued_at));
230
354
  }
231
- let best = 0;
232
- for (let i = 1; i < pending.length; i++) {
233
- const a = pending[best];
234
- const b = pending[i];
235
- const va = in_viewport.get(a.el) === true;
236
- const vb = in_viewport.get(b.el) === true;
237
- if (vb !== va ? vb : b.order < a.order) best = i;
238
- }
239
- const turn = pending[best];
240
- if (!hidden && in_viewport.get(turn.el) !== true) {
355
+ const turn = heap_top(heap_vis, true) ?? heap_top(heap_rest, false);
356
+ if (!hidden && !turn.vis && loading_vis.size) {
241
357
  const left = HOLD_FOR_VIEWPORT_MS - (t - turn.queued_at);
242
- if (left > 0) {
243
- for (const el of loading) if (in_viewport.get(el) === true) return drain_after(left);
244
- }
358
+ if (left > 0) return drain_after(left);
245
359
  }
246
- pending.splice(best, 1);
247
- loading.delete(turn.el);
360
+ heap_pop(turn.vis ? heap_vis : heap_rest);
361
+ retire(turn);
362
+ stop_loading(turn.el);
248
363
  stop_snapshot(turn.el);
249
364
  turn.resolve();
250
- if (pending.length) schedule_drain();
365
+ if (pending_count) schedule_drain();
251
366
  }
252
367
  /** Test seam: is a hydration turn queued or being drained right now? */
253
368
  function schedule_idle() {
254
- return pending.length === 0 && !draining;
369
+ return pending_count === 0 && drainer.now.kind !== "queued";
255
370
  }
256
371
  export { after_document_painted, background_start, hydrate_settled, hydrate_started, hydrate_turn, register_region, reset_document_painted, schedule_idle, unregister_region, yield_task };
@@ -1,3 +1,4 @@
1
+ import { PageVisit } from "./session.js";
1
2
  import { parse_sidecar_text } from "./wire-format.js";
2
3
  import { PageSnapshot } from "../shims/page-store.svelte.js";
3
4
  export declare const PAGE_SEED_SELECTOR = "script[type=\"application/ogygia-page\"]";
@@ -44,8 +45,11 @@ export declare function apply_soft_invalidate_doc(doc: Document): void;
44
45
  * Do **not** clear `query_map` / `live_query_map` here — old islands are still mounted, and Kit's
45
46
  * LiveQueryProxy throws if its cache entry vanishes mid-render. Instance sweep happens in
46
47
  * {@link finish_spa_document} after the swap.
48
+ *
49
+ * `next`: the visit of the page going in, made by its navigation (runtime/session.ts `PageVisit`):
50
+ * from here on it is the page's, and the visit of the page going out ends.
47
51
  */
48
- export declare function prepare_spa_document(): void;
52
+ export declare function prepare_spa_document(next?: PageVisit): void;
49
53
  /**
50
54
  * After the body swap: old islands have disconnected; new ones have only queued their hydrate.
51
55
  * Sweep Kit query/live instance maps so the next page cannot reuse a LiveQuery whose `#start` is
@@ -5,6 +5,7 @@ import { install_page_defer, page_defer_revivers } from "./page-defer.js";
5
5
  import { transport_decoders } from "./app-transport.js";
6
6
  import { kit_page_thread } from "./kit-page-thread.svelte.js";
7
7
  import { boot_link, slots } from "./slots.js";
8
+ import { warn } from "../messages/lazy.js";
8
9
  // src/runtime/seeds.ts
9
10
  /**
10
11
  * THE PAGE SEED on the client — parsed ONCE per document, kept in ONE place.
@@ -125,7 +126,7 @@ function seed_page_once() {
125
126
  const el = document.querySelector(PAGE_SEED_SELECTOR);
126
127
  if (el) apply_page_seed(parse_page_seed(el));
127
128
  }
128
- if (session().page_facts.length) absorb_page_facts();
129
+ if (session().visit.facts.length) absorb_page_facts();
129
130
  }
130
131
  /**
131
132
  * ONE PAGE, NOT TWO (server/render-page.ts): an answer rendered outside its page — a hole, a region a
@@ -134,9 +135,11 @@ function seed_page_once() {
134
135
  * values their HTML was rendered from, and every island reading the same keys sees them too (the
135
136
  * page's data changed: a locale saved, a sign-in). On a Kit-hydrated page Kit owns `page.data`: the
136
137
  * page's data is older than the answer's, and dev says to refresh it (`invalidateAll()`) first.
138
+ * Only the page's own: the facts queued on its visit (runtime/session.ts `PageVisit`), never the
139
+ * ones a page left before them brought (#269).
137
140
  */
138
141
  function absorb_page_facts() {
139
- const queued = session().page_facts.splice(0);
142
+ const queued = session().visit.facts.splice(0);
140
143
  let merged = null;
141
144
  for (const text of queued) {
142
145
  let facts;
@@ -154,7 +157,7 @@ function absorb_page_facts() {
154
157
  if (import.meta.env.DEV) {
155
158
  const data = kit.page.data ?? {};
156
159
  const older = Object.keys(merged).filter((k) => data[k] !== merged[k] && JSON.stringify(data[k]) !== JSON.stringify(merged[k]));
157
- if (older.length) console.warn(`[ogygia] a region came back rendered with newer page.data (${older.join(", ")}) than this Kit page holds. Refresh the page's data (invalidateAll()) before refreshing the region, or the islands inside it hydrate against the older values.`);
160
+ if (older.length) warn("region_newer_page_data", { keys: older.join(", ") });
158
161
  }
159
162
  return;
160
163
  }
@@ -195,9 +198,12 @@ function apply_soft_invalidate_doc(doc) {
195
198
  * Do **not** clear `query_map` / `live_query_map` here — old islands are still mounted, and Kit's
196
199
  * LiveQueryProxy throws if its cache entry vanishes mid-render. Instance sweep happens in
197
200
  * {@link finish_spa_document} after the swap.
201
+ *
202
+ * `next`: the visit of the page going in, made by its navigation (runtime/session.ts `PageVisit`):
203
+ * from here on it is the page's, and the visit of the page going out ends.
198
204
  */
199
- function prepare_spa_document() {
200
- session().reset();
205
+ function prepare_spa_document(next) {
206
+ session().reset(next);
201
207
  slots.remoteSeeds?.clear_remote_seeds();
202
208
  reset_page();
203
209
  }
@@ -1,4 +1,5 @@
1
1
  import { ConcurrencyGate } from "./concurrency.js";
2
+ import { Scope } from "@ogygia/machine";
2
3
  /** Cached SSR DOM (+ optional SWR endpoint) for `{#if}` remount of hydrate:none regions.
3
4
  * Keyed by lake entry id — size is O(unique lakes on the page), not O(toggles).
4
5
  * Cleared on SPA body swap via `reset()`. SWR refresh replaces the entry (does not grow). */
@@ -11,6 +12,33 @@ export type LakeCacheEntry = {
11
12
  /** Client TTL in ms; `0` = no expiry. */
12
13
  maxAgeMs: number;
13
14
  };
15
+ /**
16
+ * THE PAGE VISIT: one per page the visitor is on, the document load, then each page a navigation
17
+ * swaps in. It ends when the next page's swap takes over (`RuntimeSession.reset`). What the page
18
+ * holds is its own and goes with it: on `scope`, its listeners, timers, observers and streams (the
19
+ * router's link scan, the batch of its holes, the waits for the sheets its answers bring); in
20
+ * `facts`, what its answers brought for its next hydrate.
21
+ *
22
+ * A navigation makes the visit of the page it brings before its swap (`RuntimeSession.next_visit`):
23
+ * what it starts for that page early, the batch of its holes, is that visit's. A navigation that
24
+ * ends without its swap (called off, taken over, given to the browser) ends that visit, never the
25
+ * page's (./router-nav.ts `navigate`).
26
+ */
27
+ export type PageVisit = {
28
+ /** Closed when the visit ends. */
29
+ readonly scope: Scope;
30
+ /** The page facts answers rendered with (server/render-page.ts), queued at the answer seam
31
+ * (`region_fragment`) for the next hydrate to merge into the page store (seeds.ts). This page's
32
+ * only: the next page's visit starts with none (#269). */
33
+ readonly facts: string[];
34
+ };
35
+ /** What one island's hydrate step put in `<head>` ({@link RuntimeSession.mark_head}). */
36
+ export interface HeadMark {
37
+ /** The head's elements now that were not its children when the mark was made, in head order. */
38
+ added(): Element[];
39
+ /** Stop noting (a step that ended without a record). */
40
+ stop(): void;
41
+ }
14
42
  export declare class RuntimeSession {
15
43
  #private;
16
44
  /** Page-owned head nodes (see above). Survives `reset()`: it is the document's, not a page's. */
@@ -21,9 +49,16 @@ export declare class RuntimeSession {
21
49
  claim_page_head(node: Node): void;
22
50
  /** At boot: everything already in <head> came from the server — the page owns it. */
23
51
  adopt_document_head(): void;
24
- /** After an island's hydrate: the head elements it added (not in `before`, not page-owned) are
25
- * that island's; then retire the page's copies of them. */
26
- record_island_head(before: ReadonlySet<Node>): void;
52
+ /**
53
+ * Before an island's hydrate step: start noting what lands in `<head>`. A MutationObserver on the
54
+ * head's own children, read synchronously when the step ends, so the record costs what the island
55
+ * added, not the whole head (a copy of the head per island grew as islands × head). Without
56
+ * MutationObserver: the head's elements, copied.
57
+ */
58
+ mark_head(): HeadMark;
59
+ /** After an island's hydrate: the head elements it added (not there before the step, not
60
+ * page-owned) are that island's; then retire the page's copies of them. */
61
+ record_island_head(mark: HeadMark): void;
27
62
  /**
28
63
  * Remove every PAGE-owned head element that duplicates a live island-owned one (same markup), and
29
64
  * the page's `<title>` once an island renders its own (the browser honours the first title, and
@@ -34,9 +69,11 @@ export declare class RuntimeSession {
34
69
  readonly settled_lakes: WeakSet<Element>;
35
70
  readonly initialized_lakes: Set<string>;
36
71
  readonly server_gate: ConcurrencyGate;
37
- /** The page facts answers rendered with (server/render-page.ts), queued at the answer seam
38
- * (`region_fragment`) for the next hydrate to merge into the page store (seeds.ts). */
39
- readonly page_facts: string[];
72
+ /** The visit of the page on screen (see {@link PageVisit}). */
73
+ get visit(): PageVisit;
74
+ /** The visit of the page a navigation brings, made before its swap: the navigation's (it ends
75
+ * it when there is no swap) until `reset(next)` makes it the page. */
76
+ next_visit(): PageVisit;
40
77
  get remote_seeded(): boolean;
41
78
  mark_remote_seeded(): void;
42
79
  get page_seeded(): boolean;
@@ -46,7 +83,9 @@ export declare class RuntimeSession {
46
83
  /** Insert/replace a lake cache entry; evict oldest if over LAKE_CACHE_MAX. */
47
84
  set_lake_cache(id: string, entry: LakeCacheEntry): void;
48
85
  settle_lakes_in(root: ParentNode): void;
49
- reset(): void;
86
+ /** The next page takes over (its visit, made by its navigation; a new one when none is given),
87
+ * and the visit of the page it replaces ends. */
88
+ reset(next?: PageVisit): void;
50
89
  }
51
90
  /** Process-wide session for the islands runtime (one client module graph). */
52
91
  export declare const runtime_session: RuntimeSession;