@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
@@ -1,8 +1,16 @@
1
+ import { import_again } from "./import-again.js";
1
2
  import { kit_hydrates_page } from "./kit-boot.js";
3
+ import { create_navigation } from "./navigation.js";
2
4
  import { slots } from "./slots.js";
3
5
  import { publish_nav } from "./nav-handle.js";
4
6
  import { nearest_attr } from "./nearest-attr.js";
5
7
  import { record_server_body } from "./ownership.js";
8
+ import { runtime_session } from "./session.js";
9
+ import { OgygiaError, warn } from "../messages/core.js";
10
+ import { report_thrown } from "../report-thrown.js";
11
+ import { download } from "../policies.js";
12
+ import { watch_router } from "./rules.js";
13
+ import { STALE, flights } from "@ogygia/machine";
6
14
  // src/runtime/router.ts
7
15
  /**
8
16
  * Minimal SPA router for ogygia (Astro's ClientRouter equivalent).
@@ -17,6 +25,7 @@ import { record_server_body } from "./ownership.js";
17
25
  * merge, body reconcile, seeds, a11y) is ./router-nav.ts, loaded on the first prefetch or
18
26
  * intercepted click. A visitor who never leaves the page never downloads it.
19
27
  */
28
+ const RULES = import.meta.env?.DEV || (typeof __OGYGIA_RULES__ !== "undefined" ? __OGYGIA_RULES__ : false);
20
29
  const WS = /\s+/;
21
30
  /**
22
31
  * The `<a>` a pointer event is about — through SHADOW ROOTS. A click inside a web component's
@@ -133,48 +142,159 @@ function jump_to_hash(hash) {
133
142
  html_el.style.scrollBehavior = prev;
134
143
  }
135
144
  }
136
- /** Run `fn` when the browser is idle (bounded), else soon. */
137
- function on_idle(fn, timeout = 1e3) {
138
- if (typeof requestIdleCallback === "function") requestIdleCallback(fn, { timeout });
139
- else setTimeout(fn, 50);
145
+ /**
146
+ * THE ROUTER'S HISTORY, one record (#310). The router keeps the visitor's entries it made, each with
147
+ * its address and where its page was left, and reads the rest from it: the page on screen
148
+ * (`current_url`, its document key), the scroll a back or forward step puts back. `history.state`
149
+ * holds only an entry's id. It once kept the same facts in several places (the address bar written
150
+ * before the fetch, `current_url`, `doc_key`, a scroll saved on push only), and they drifted.
151
+ *
152
+ * - An entry is written when a page goes in (the navigation's commit), never before its fetch: a
153
+ * download or a 204 adds none (#297), and the address bar is the page on screen.
154
+ * - Leaving an entry keeps where its page was, on every move: a click, a back or forward step (#270).
155
+ * - An id this document never made (its state names another document's entry) is not this
156
+ * document's to show: the browser loads it whole. An entry with no id (the browser's own fragment
157
+ * jump, an app's own `pushState`) is taken over when the router meets it.
158
+ */
159
+ /** The state of the entry the browser is on, named by `id`: the id goes beside what the app (or
160
+ * Kit) keeps there, never over it (#375). */
161
+ function named(id) {
162
+ const kept = history.state;
163
+ return kept !== null && typeof kept === "object" ? {
164
+ ...kept,
165
+ ogygia: id
166
+ } : { ogygia: id };
167
+ }
168
+ var RouterHistory = class {
169
+ /** Names this document's entries apart from another document's (a reload's, say). */
170
+ #doc = Math.random().toString(36).slice(2, 10);
171
+ #n = 0;
172
+ #entries = /* @__PURE__ */ new Map();
173
+ #current = null;
174
+ /** The entry of the page on screen. */
175
+ get current() {
176
+ return this.#current ??= this.#add(new URL(location.href));
177
+ }
178
+ /** Takes over the entry the browser is on as the router starts: its state names it now. */
179
+ start() {
180
+ replace_state(named(this.current.id));
181
+ }
182
+ /** The entry a history state names: this model's, `'foreign'` (another document's), or null
183
+ * (no id: an entry the browser or the app made). */
184
+ find(state) {
185
+ const id = state?.ogygia;
186
+ if (typeof id !== "string") return null;
187
+ return this.#entries.get(id) ?? (id.startsWith(this.#doc + ".") ? null : "foreign");
188
+ }
189
+ /** The page on screen is being left: keep where it is. */
190
+ leave() {
191
+ const entry = this.current;
192
+ entry.scroll = {
193
+ x: scrollX,
194
+ y: scrollY
195
+ };
196
+ const active = typeof document === "undefined" ? null : document.activeElement;
197
+ entry.focus = active && active !== document.body && active.id ? active.id : null;
198
+ }
199
+ /** A page goes in at `url` (the browser's entry is written now). `push`: a new entry after the
200
+ * one left; `replace`: the current one at a new address; `pop`: `to`, the entry a back or
201
+ * forward step is on. */
202
+ commit(url, move, to) {
203
+ if (move === "push") {
204
+ this.leave();
205
+ const entry = this.#add(url);
206
+ push_state({ ogygia: entry.id }, url.href);
207
+ return this.#current = entry;
208
+ }
209
+ const entry = move === "pop" && to ? to : this.current;
210
+ entry.url = url;
211
+ this.#current = entry;
212
+ if (location.href !== url.href || history.state?.ogygia !== entry.id) replace_state(named(entry.id), url.href);
213
+ return entry;
214
+ }
215
+ /** The entry the browser stepped to, which no id names: this model's from now on. */
216
+ adopt(url) {
217
+ const entry = this.#add(url);
218
+ replace_state(named(entry.id));
219
+ return entry;
220
+ }
221
+ #add(url) {
222
+ const entry = {
223
+ id: this.#doc + "." + ++this.#n,
224
+ url,
225
+ scroll: null,
226
+ focus: null
227
+ };
228
+ this.#entries.set(entry.id, entry);
229
+ return entry;
230
+ }
231
+ };
232
+ /** Run `fn` once, when the browser is idle or after `cap` ms, whichever is first, unless `page` (a
233
+ * page visit's scope) ends first. */
234
+ function on_idle(page, fn, cap = 1e3) {
235
+ const run = () => {
236
+ idle();
237
+ capped();
238
+ fn();
239
+ };
240
+ const idle = page.timeout(run, "idle");
241
+ const capped = page.timeout(run, cap);
140
242
  }
141
- let nav_promise = null;
142
243
  let loaded_nav = null;
244
+ const load_nav = flights({
245
+ name: "navigation",
246
+ run: import_again(() => import("./router-nav.js")),
247
+ retry: download
248
+ });
249
+ /** (Never STALE: nothing drops the navigation's load; a caller treats it as one that failed.) */
143
250
  function nav() {
144
- slots.router_link ??= {
145
- document_key,
146
- jump_to_hash,
147
- push_state,
148
- replace_state
149
- };
150
- if (!nav_promise) nav_promise = import("./router-nav.js").then((m) => loaded_nav = m, (err) => {
151
- nav_promise = null;
152
- throw err;
153
- });
154
- return nav_promise;
251
+ slots.router_link ??= { jump_to_hash };
252
+ return load_nav.get().then((m) => m === STALE ? m : loaded_nav = m);
155
253
  }
156
254
  var SpaRouter = class {
157
255
  #started = false;
158
256
  #before_hooks = /* @__PURE__ */ new Set();
159
257
  #after_hooks = /* @__PURE__ */ new Set();
160
- /** href of the navigation in flight (set before the fetch, cleared when applied or aborted) */
161
- nav_target = null;
162
- /** The in-flight navigation's cancel handle: router-nav bumps `nav_gen` and sets `nav_abort` for
163
- * each navigation it starts, and stops at its next step once `nav_gen` moved on. */
164
- nav_gen = 0;
165
- nav_abort = null;
166
- #viewport_io = null;
167
- #viewport_seen = /* @__PURE__ */ new WeakSet();
168
- /** pathname+search of the document currently in the DOM (hash ignored). */
169
- doc_key = "";
170
258
  /**
171
- * Full URL of the document currently displayed in the DOM. `navigate()` uses this as its `from`
259
+ * THE NAVIGATION, one machine (./navigation.ts): idle, claimed (the click, while the navigation
260
+ * chunk loads), loading (its fetch through its swap), settling (swapped: the last steps run). A
261
+ * new click is a new visit, so whatever the old one still had in flight sees it is not current
262
+ * (`nav.now !== mine`) and stops, and its fetch is aborted. It was three fields kept by hand; two
263
+ * clicks during the first chunk load once held the same generation and the last one was lost.
264
+ */
265
+ nav = create_navigation();
266
+ /** href of the router's navigation in flight (claimed or loading; cleared once it applied,
267
+ * ended, or went to the browser: `hand_off`) */
268
+ get nav_target() {
269
+ const s = this.nav.now;
270
+ return s.kind === "claimed" || s.kind === "loading" ? s.href : null;
271
+ }
272
+ /**
273
+ * The anchors whose click the router took (prevented, and navigated), and the address it took each
274
+ * to. A link component clicks its inner `<a>` again after the visitor's click, and that second
275
+ * click may never reach the router's `document` listener: a click event that is not composed
276
+ * stays in the component's shadow tree, and an anchor the swap took out of the page is in no
277
+ * document. The browser then followed the link itself, a full load after a click the router had
278
+ * already prevented (#263). So an anchor the router took a click from listens itself, for the
279
+ * clicks `document` will not hear (`#on_anchor_click`).
280
+ */
281
+ #taken = /* @__PURE__ */ new WeakMap();
282
+ /** The visitor's history, as the router keeps it (`RouterHistory`): the rest is read from it. */
283
+ history = new RouterHistory();
284
+ /**
285
+ * Full URL of the page on screen: its history entry's. `navigate()` uses this as its `from`
172
286
  * instead of `location.href`, because on a `popstate` (back/forward) the browser has ALREADY
173
287
  * changed `location` to the target — so `location.href` would equal the target and the
174
288
  * same-document guard would wrongly bail into the hash-only branch, never swapping the body
175
289
  * (browser back left the old page's DOM in place). POP-FROM.
176
290
  */
177
- current_url = null;
291
+ get current_url() {
292
+ return this.history.current.url;
293
+ }
294
+ /** pathname+search of the page on screen (hash ignored). */
295
+ get doc_key() {
296
+ return document_key(this.current_url);
297
+ }
178
298
  beforeNavigate(fn) {
179
299
  this.#before_hooks.add(fn);
180
300
  return () => this.#before_hooks.delete(fn);
@@ -188,7 +308,9 @@ var SpaRouter = class {
188
308
  type: "enter",
189
309
  willUnload: false
190
310
  });
191
- } catch {}
311
+ } catch (error) {
312
+ report_thrown(error);
313
+ }
192
314
  return () => this.#after_hooks.delete(fn);
193
315
  }
194
316
  /**
@@ -199,62 +321,80 @@ var SpaRouter = class {
199
321
  * swapping left the URL on the old page and `/plain` in the DOM).
200
322
  */
201
323
  supersede() {
202
- if (this.nav_target === null && this.nav_abort === null) return;
203
- this.nav_gen++;
204
- this.nav_abort?.abort();
205
- this.nav_abort = null;
206
- this.nav_target = null;
324
+ if (this.nav.now.kind !== "idle") this.nav.go("idle");
325
+ }
326
+ /**
327
+ * GIVE `visit`'s NAVIGATION TO THE BROWSER — a page the router will not render (a csr=true Kit
328
+ * page, a page that is not ogygia's, another build), or one it could not fetch or load its code
329
+ * for: a document navigation to `url`, from the document on screen (`from`).
330
+ *
331
+ * It ENDS the visit, on every path: from here on the navigation is the browser's, never the
332
+ * router's in flight. The browser may well keep this page on screen (a file download, a 204, a
333
+ * body it does not show), and then nothing else ends it: left `loading`, every later click on the
334
+ * link joined it and was swallowed (#266). A visit already left (a newer click took over, or back
335
+ * to the page on screen) hands nothing off: the browser never goes where the visitor no longer is.
336
+ *
337
+ * The router writes an entry only when a page goes in, so a click handed off has none: the
338
+ * browser's navigation is a new entry of its own, and one it keeps the page on screen for adds
339
+ * none (#297). A back or forward step is already on its entry, and a refresh on the one on
340
+ * screen: the browser's navigation REPLACES it, so its document holds the entry alone, never
341
+ * shares it with the router's page (back from a csr=true page then loads the router page, #259).
342
+ * So does a commit that failed after it wrote the entry (the address is already `url`).
343
+ *
344
+ * `error`: the navigation failed part way (its devalue did not load, its swap threw, #276). It
345
+ * is said (the page's error handler, as an uncaught throw would be), and the browser loads the
346
+ * page whole.
347
+ */
348
+ hand_off(url, visit, move, error) {
349
+ if (this.nav.now !== visit) return;
350
+ this.nav.go("idle");
351
+ if (error !== void 0) report_thrown(error);
352
+ if (move === "push" && location.href !== url.href) location.assign(url.href);
353
+ else location.replace(url.href);
207
354
  }
208
355
  bust_page_cache() {
209
356
  loaded_nav?.bust_page_cache();
210
357
  }
211
- _page_cache_size() {
212
- return loaded_nav?.page_cache_size() ?? 0;
213
- }
358
+ /** The page at `href`, or `null` when there is none to have: the fetch failed (./router-nav.ts
359
+ * `fetch_page`), or the navigation code itself did not load. The prefetches (a hover, a
360
+ * viewport link, `preloadData`) start it and never wait on it: a rejection here was uncaught. */
214
361
  fetch_page(href, signal, purpose) {
215
- return nav().then((n) => n.fetch_page(href, signal, purpose));
362
+ return nav().then((n) => n === STALE ? null : n.fetch_page(href, signal, purpose), () => null);
216
363
  }
217
364
  async navigate(url, opts = {}) {
218
365
  const { push = true, type = "link", replace = false } = opts;
219
- const from = this.current_url ?? new URL(location.href);
366
+ const from = this.current_url;
367
+ const move = opts.pop ? "pop" : push && !replace ? "push" : "replace";
220
368
  if (same_document(url, from) && !replace) {
221
369
  if (!this.run_before(from, url, type)) return;
222
370
  this.supersede();
223
- if (push && url.href !== location.href) push_state({
224
- ...history.state,
225
- ogygia: true
226
- }, url.href);
227
- else if (url.href !== location.href) replace_state({
228
- ...history.state,
229
- ogygia: true
230
- }, url.href);
371
+ if (url.href !== location.href) this.history.commit(url, move);
231
372
  jump_to_hash(url.hash);
232
- this.current_url = url;
233
373
  this.run_after(from, url, type);
234
374
  return;
235
375
  }
236
- this.nav_target = url.href;
237
- const gen = this.nav_gen;
376
+ if (!this.run_before(from, url, type)) return;
377
+ this.nav.go("claimed", { href: url.href });
378
+ const mine = this.nav.now;
238
379
  let n;
239
380
  try {
240
381
  n = await nav();
241
382
  } catch {
242
- if (this.nav_target === url.href) this.nav_target = null;
243
- location.href = url.href;
244
- return;
383
+ return this.hand_off(url, mine, move);
245
384
  }
246
- if (gen !== this.nav_gen) return;
247
- return n.navigate(this, url, from, opts);
385
+ if (n === STALE) return this.hand_off(url, mine, move);
386
+ if (this.nav.now !== mine) return;
387
+ return n.navigate(this, url, from, opts, mine);
248
388
  }
249
389
  goto(url, opts = {}) {
250
390
  const target = new URL(url, location.href);
251
- if (target.protocol !== "http:" && target.protocol !== "https:") throw new Error("[ogygia] goto() only supports http(s) URLs");
391
+ if (target.protocol !== "http:" && target.protocol !== "https:") throw new OgygiaError("goto_not_http");
252
392
  if (target.origin !== location.origin) {
253
393
  if (opts.external) {
254
394
  location.assign(target.href);
255
395
  return Promise.resolve();
256
396
  }
257
- throw new Error("[ogygia] goto() only supports same-origin URLs (pass { external: true } to leave)");
397
+ throw new OgygiaError("goto_cross_origin");
258
398
  }
259
399
  return this.navigate(target, {
260
400
  push: !opts.replaceState,
@@ -263,7 +403,7 @@ var SpaRouter = class {
263
403
  });
264
404
  }
265
405
  invalidateAll() {
266
- return nav().then((n) => n.invalidate_all());
406
+ return nav().then((n) => n === STALE ? void 0 : n.invalidate_all(this.current_url));
267
407
  }
268
408
  invalidate() {
269
409
  return this.invalidateAll();
@@ -286,75 +426,110 @@ var SpaRouter = class {
286
426
  return Promise.resolve();
287
427
  }
288
428
  disableScrollHandling() {
289
- if (typeof console !== "undefined") console.warn("[ogygia] disableScrollHandling() is a no-op in the islands SPA router.");
429
+ if (typeof console !== "undefined") warn("scroll_handling_noop");
290
430
  }
291
431
  pushState() {
292
- console.warn("[ogygia] pushState() shallow routing is not supported; use goto().");
432
+ warn("shallow_routing_unsupported", { method: "pushState" });
293
433
  }
294
434
  replaceState() {
295
- console.warn("[ogygia] replaceState() shallow routing is not supported; use goto().");
435
+ warn("shallow_routing_unsupported", { method: "replaceState" });
296
436
  }
297
437
  start() {
298
438
  if (this.#started || typeof document === "undefined") return;
299
439
  if (!document.querySelector("meta[name=\"ogygia-router\"]")) return;
300
440
  if (kit_hydrates_page()) return;
301
441
  this.#started = true;
302
- this.doc_key = document_key(new URL(location.href));
303
- this.current_url = new URL(location.href);
442
+ this.history.start();
443
+ if (RULES) watch_router(this);
304
444
  const body = document.body;
305
445
  if (body) record_server_body(body, body, Array.from(body.childNodes));
306
- document.addEventListener("click", (event) => {
307
- const anchor = anchor_of(event);
308
- const url = this.#should_intercept(event, anchor);
309
- if (!url) return;
310
- if (this.nav_target === url.href) {
311
- event.preventDefault();
312
- return;
313
- }
314
- if (same_document(url, new URL(location.href))) {
315
- const kind = same_document_link(url, new URL(location.href), this.nav_target);
316
- if (kind === "hash") {
317
- if (!hash_target_exists(url.hash)) {
318
- event.preventDefault();
319
- if (location.hash !== url.hash) {
320
- push_state(history.state || {}, url.href);
321
- window.dispatchEvent(new HashChangeEvent("hashchange", {
322
- oldURL: location.href,
323
- newURL: url.href
324
- }));
325
- }
326
- }
327
- return;
328
- }
329
- event.preventDefault();
330
- if (kind === "refresh") this.navigate(url, {
331
- push: false,
332
- replace: true
333
- });
334
- return;
335
- }
336
- event.preventDefault();
337
- this.navigate(url, { push: true });
338
- });
446
+ document.addEventListener("click", (event) => this.#on_click(event, anchor_of(event)));
339
447
  this.#install_prefetch();
340
448
  window.addEventListener("popstate", () => {
341
449
  const url = new URL(location.href);
450
+ const found = this.history.find(history.state);
451
+ if (found === "foreign") {
452
+ this.supersede();
453
+ location.reload();
454
+ return;
455
+ }
456
+ this.history.leave();
457
+ const entry = found ?? this.history.adopt(url);
342
458
  if (document_key(url) === this.doc_key) {
343
459
  this.supersede();
344
- jump_to_hash(url.hash);
460
+ this.history.commit(url, "pop", entry);
345
461
  return;
346
462
  }
347
- const pop_scroll = history.state?.scroll || null;
348
463
  this.navigate(url, {
349
464
  push: false,
350
- pop_scroll,
465
+ pop: entry,
351
466
  type: "popstate"
352
467
  });
353
468
  });
354
- replace_state({
355
- ...history.state,
356
- ogygia: true
357
- });
469
+ }
470
+ /** A click on `anchor`: the router's, or the browser's. A click it prevents always ends in a
471
+ * navigation, the one it starts or the one it joins. */
472
+ #on_click(event, anchor) {
473
+ const url = this.#should_intercept(event, anchor);
474
+ if (!url || !anchor) return;
475
+ if (this.#joins(event, anchor, url)) {
476
+ event.preventDefault();
477
+ return;
478
+ }
479
+ if (same_document(url, new URL(location.href))) {
480
+ const kind = same_document_link(url, new URL(location.href), this.nav_target);
481
+ if (kind === "hash") {
482
+ if (!hash_target_exists(url.hash)) {
483
+ event.preventDefault();
484
+ if (location.hash !== url.hash) {
485
+ const old = location.href;
486
+ this.history.commit(url, "push");
487
+ window.dispatchEvent(new HashChangeEvent("hashchange", {
488
+ oldURL: old,
489
+ newURL: url.href
490
+ }));
491
+ }
492
+ }
493
+ return;
494
+ }
495
+ event.preventDefault();
496
+ this.#take(anchor, url);
497
+ if (kind === "refresh") this.navigate(url, {
498
+ push: false,
499
+ replace: true
500
+ });
501
+ return;
502
+ }
503
+ event.preventDefault();
504
+ this.#take(anchor, url);
505
+ this.navigate(url, { push: true });
506
+ }
507
+ /**
508
+ * Does this click join a navigation the router already has? Its own in flight to the click's
509
+ * address (a double click, or a link component re-dispatching the visitor's click on its inner
510
+ * `<a>`): never one it gave the browser (`hand_off` ends that visit), so a link to a download is
511
+ * a new navigation each time; or
512
+ * the one this very anchor started, to the page now on screen, when the click is a re-dispatch
513
+ * (untrusted: the component's, never the visitor's — a visitor's click on a link to the page they
514
+ * are on refreshes it).
515
+ */
516
+ #joins(event, anchor, url) {
517
+ if (this.nav_target === url.href) return true;
518
+ return !event.isTrusted && this.#taken.get(anchor) === url.href && same_document(url, this.current_url);
519
+ }
520
+ /** The router took a click on `anchor` to `url`: from now on the anchor hears its own clicks too. */
521
+ #take(anchor, url) {
522
+ if (!this.#taken.has(anchor)) anchor.addEventListener("click", (event) => this.#on_anchor_click(event, anchor));
523
+ this.#taken.set(anchor, url.href);
524
+ }
525
+ /**
526
+ * A click on an anchor the router took a click from, heard on the anchor itself. One that reaches
527
+ * `document` is the document listener's (a component on the way may still prevent it); one that
528
+ * cannot — not composed and inside a shadow tree, or on an anchor that is in no document — is
529
+ * handled here, the same way.
530
+ */
531
+ #on_anchor_click(event, anchor) {
532
+ if (!(anchor.isConnected && (event.composed || anchor.getRootNode() === document))) this.#on_click(event, anchor);
358
533
  }
359
534
  #should_intercept(event, anchor) {
360
535
  if (event.defaultPrevented) return false;
@@ -381,7 +556,9 @@ var SpaRouter = class {
381
556
  };
382
557
  for (const fn of this.#before_hooks) try {
383
558
  fn(nav);
384
- } catch {}
559
+ } catch (error) {
560
+ report_thrown(error);
561
+ }
385
562
  return !cancelled;
386
563
  }
387
564
  run_after(from, to, type) {
@@ -392,7 +569,9 @@ var SpaRouter = class {
392
569
  type,
393
570
  willUnload: false
394
571
  });
395
- } catch {}
572
+ } catch (error) {
573
+ report_thrown(error);
574
+ }
396
575
  }
397
576
  #install_prefetch() {
398
577
  document.addEventListener("mouseover", (event) => {
@@ -407,34 +586,27 @@ var SpaRouter = class {
407
586
  document.addEventListener("touchstart", on_press, { passive: true });
408
587
  this.scan_preload_links();
409
588
  }
410
- /** After start + every navigation: drop detached IO targets, then re-observe the live body. */
411
- #reset_viewport_io() {
412
- this.#viewport_io?.disconnect();
413
- this.#viewport_seen = /* @__PURE__ */ new WeakSet();
414
- this.#viewport_io = new IntersectionObserver((entries) => {
415
- for (const e of entries) if (e.isIntersecting) {
416
- this.#viewport_io.unobserve(e.target);
417
- this.#warm_anchor(e.target);
418
- }
419
- }, { rootMargin: "0px" });
420
- }
421
589
  /**
422
590
  * After start + every navigation, in IDLE: eager links warm now; viewport links get observed.
423
591
  * Only anchors under a preload-marked element are read — an unmarked anchor has no trigger. Idle,
424
592
  * because the scan competes with the freshly swapped page's own islands for the main thread and
425
- * a warm is speculation by definition.
593
+ * a warm is speculation by definition. The scan and its observer are the page visit's
594
+ * (./session.ts `PageVisit`): the next page's swap disconnects it, so the anchors of a page left
595
+ * behind are never held, and a scan still waiting for idle time when its page is left never runs.
426
596
  */
427
597
  scan_preload_links() {
428
- on_idle(() => {
429
- this.#reset_viewport_io();
430
- if (!this.#viewport_io) return;
598
+ const page = runtime_session.visit.scope;
599
+ on_idle(page, () => {
600
+ const io = page.watch(new IntersectionObserver((entries) => {
601
+ for (const e of entries) if (e.isIntersecting) {
602
+ io.unobserve(e.target);
603
+ this.#warm_anchor(e.target);
604
+ }
605
+ }, { rootMargin: "0px" }));
431
606
  for (const anchor of Array.from(document.querySelectorAll(PRELOAD_MARKED_ANCHORS))) {
432
607
  const rank = this.#preload_rank(anchor);
433
608
  if (rank === PRELOAD_RANK.eager) this.#warm_anchor(anchor);
434
- else if (rank === PRELOAD_RANK.viewport && !this.#viewport_seen.has(anchor)) {
435
- this.#viewport_seen.add(anchor);
436
- this.#viewport_io.observe(anchor);
437
- }
609
+ else if (rank === PRELOAD_RANK.viewport) io.observe(anchor);
438
610
  }
439
611
  });
440
612
  }
@@ -493,10 +665,6 @@ function afterNavigate(fn) {
493
665
  function bust_page_cache() {
494
666
  spa.bust_page_cache();
495
667
  }
496
- /** Test helper — current warmed entry count. */
497
- function _page_cache_size() {
498
- return spa._page_cache_size();
499
- }
500
668
  /**
501
669
  * Programmatic same-origin navigation. Mirrors Kit's `goto()` subset.
502
670
  * @param url - Absolute or relative URL (http(s) only).
@@ -593,4 +761,4 @@ function install() {
593
761
  if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", start, { once: true });
594
762
  else start();
595
763
  }
596
- export { SpaRouter, _page_cache_size, afterNavigate, anchor_of, beforeNavigate, bust_page_cache, disableScrollHandling, document_key, goto, install, invalidate, invalidateAll, jump_to_hash, preloadCode, preloadData, pushState, push_state, reload_opt_out, replaceState, replace_state, same_document, same_document_link, startRouter };
764
+ export { RouterHistory, SpaRouter, afterNavigate, anchor_of, beforeNavigate, bust_page_cache, disableScrollHandling, document_key, goto, install, invalidate, invalidateAll, jump_to_hash, preloadCode, preloadData, pushState, push_state, reload_opt_out, replaceState, replace_state, same_document, same_document_link, startRouter };
@@ -0,0 +1,35 @@
1
+ import { Rules } from "../rules.js";
2
+ type RuntimeRule = "rule_region_mounted_twice" | "rule_address_bar_drift" | "rule_old_page_open" | "rule_failure_uncoded" | "rule_parse_upgraded";
3
+ /** The runtime's rules. */
4
+ export declare const runtime_rules: Rules<RuntimeRule>;
5
+ /** A region element mounted its island. */
6
+ export declare function note_mount(region: Element): void;
7
+ /** A region element let its island go. */
8
+ export declare function note_unmount(region: Element): void;
9
+ type Visit = {
10
+ readonly scope: {
11
+ readonly closed: boolean;
12
+ };
13
+ };
14
+ /** A page visit was made (`current`: it is the page on screen's now). */
15
+ export declare function note_visit(visit: Visit, is_current: boolean): void;
16
+ /** What the rule reads of the router: its navigation, and its history model's page on screen. */
17
+ interface RouterView {
18
+ readonly nav: {
19
+ readonly now: {
20
+ readonly kind: string;
21
+ };
22
+ };
23
+ readonly history: {
24
+ readonly current: {
25
+ readonly id: string;
26
+ readonly url: URL;
27
+ };
28
+ };
29
+ }
30
+ /** The router started: its history model is checked against the address bar. */
31
+ export declare function watch_router(router: RouterView): void;
32
+ /** Rules on: `globalThis.__ogygia_rules()` gives what is broken now (for e2e); in dev they are also
33
+ * checked after machine moves, and what broke is said with its code. */
34
+ export declare function install_rules(dev: boolean): void;
35
+ export {};