@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.
- package/dist/LateIsland.svelte +3 -2
- package/dist/LateIslandAwait.svelte +6 -2
- package/dist/Region.svelte +22 -33
- package/dist/boundary.d.ts +0 -2
- package/dist/boundary.js +8 -6
- package/dist/compiler/debarrel/barrel.d.ts +15 -8
- package/dist/compiler/debarrel/barrel.js +92 -45
- package/dist/compiler/debarrel/plugin.js +41 -17
- package/dist/compiler/dev/dev-hmr.d.ts +3 -0
- package/dist/compiler/dev/dev-hmr.js +3 -0
- package/dist/compiler/driver.js +11 -4
- package/dist/compiler/flags.js +21 -7
- package/dist/compiler/fouc-css.js +5 -1
- package/dist/compiler/kit.js +5 -1
- package/dist/compiler/link/build-output.js +12 -3
- package/dist/compiler/macros/bake.js +18 -4
- package/dist/compiler/macros/dollar.js +6 -1
- package/dist/compiler/macros/source.js +6 -1
- package/dist/compiler/macros/store.js +10 -2
- package/dist/compiler/macros/wire.js +6 -1
- package/dist/compiler/parse/scan.js +10 -3
- package/dist/compiler/region/transform.js +188 -74
- package/dist/context-bridge.js +13 -5
- package/dist/context.d.ts +9 -0
- package/dist/context.js +30 -1
- package/dist/document.d.ts +1 -1
- package/dist/document.js +7 -1
- package/dist/early-holes-head.d.ts +5 -0
- package/dist/early-holes-head.js +20 -0
- package/dist/early-holes.d.ts +34 -0
- package/dist/early-holes.js +51 -0
- package/dist/entry-hydrate.js +9 -12
- package/dist/events/bus.js +7 -2
- package/dist/events/schema.d.ts +2 -2
- package/dist/fn-transport.js +4 -3
- package/dist/hooks.js +94 -163
- package/dist/insights/page-assets.js +11 -3
- package/dist/insights/page-insights.d.ts +0 -2
- package/dist/insights/page-insights.js +14 -10
- package/dist/insights/session-insights.js +2 -2
- package/dist/internal-instruments.d.ts +3 -1
- package/dist/internal-instruments.js +3 -1
- package/dist/internal-server.d.ts +3 -2
- package/dist/internal-server.js +3 -2
- package/dist/internal.d.ts +1 -2
- package/dist/internal.js +1 -2
- package/dist/keep-fallback.d.ts +0 -2
- package/dist/keep-fallback.js +4 -5
- package/dist/live-transport.js +3 -2
- package/dist/messages/build.d.ts +585 -0
- package/dist/messages/build.js +800 -0
- package/dist/messages/core.d.ts +464 -0
- package/dist/messages/core.js +843 -0
- package/dist/messages/define.d.ts +62 -0
- package/dist/messages/define.js +49 -0
- package/dist/messages/index.d.ts +6 -0
- package/dist/messages/index.js +6 -0
- package/dist/messages/island.d.ts +4 -0
- package/dist/messages/island.js +34 -0
- package/dist/messages/lazy.d.ts +3 -0
- package/dist/messages/lazy.js +22 -0
- package/dist/messages/prose.d.ts +15 -0
- package/dist/messages/prose.js +43 -0
- package/dist/messages/server.d.ts +142 -0
- package/dist/messages/server.js +246 -0
- package/dist/nested.d.ts +3 -0
- package/dist/nested.js +8 -1
- package/dist/plugin/css.js +14 -13
- package/dist/plugin/facts.d.ts +2 -0
- package/dist/plugin/facts.js +10 -6
- package/dist/plugin/hooks.d.ts +7 -17
- package/dist/plugin/hooks.js +32 -4
- package/dist/plugin/index.d.ts +9 -2
- package/dist/plugin/request-scope.d.ts +20 -0
- package/dist/plugin/request-scope.js +149 -0
- package/dist/plugin/server.d.ts +20 -2
- package/dist/plugin/server.js +98 -28
- package/dist/plugin/testing.d.ts +6 -0
- package/dist/plugin/testing.js +14 -2
- package/dist/policies.d.ts +21 -0
- package/dist/policies.js +66 -0
- package/dist/preference.js +6 -1
- package/dist/preload.js +6 -9
- package/dist/ref-watch.d.ts +0 -10
- package/dist/ref-watch.js +14 -1
- package/dist/ref.js +17 -5
- package/dist/region-snippet.js +9 -3
- package/dist/region.js +2 -1
- package/dist/report-thrown.d.ts +9 -0
- package/dist/report-thrown.js +15 -0
- package/dist/rules.d.ts +17 -0
- package/dist/rules.js +57 -0
- package/dist/runtime/beacon.d.ts +0 -4
- package/dist/runtime/beacon.js +58 -43
- package/dist/runtime/boot-link.js +19 -2
- package/dist/runtime/client-hooks.js +2 -1
- package/dist/runtime/concurrency.d.ts +16 -5
- package/dist/runtime/concurrency.js +123 -7
- package/dist/runtime/core.d.ts +3 -1
- package/dist/runtime/core.js +718 -384
- package/dist/runtime/devalue-lane.d.ts +13 -0
- package/dist/runtime/devalue-lane.js +49 -0
- package/dist/runtime/disclose-version.d.ts +1 -0
- package/dist/runtime/disclose-version.js +14 -0
- package/dist/runtime/early-holes.d.ts +4 -0
- package/dist/runtime/early-holes.js +40 -0
- package/dist/runtime/entry-locations.js +40 -49
- package/dist/runtime/foreign-props.d.ts +0 -7
- package/dist/runtime/foreign-props.js +2 -1
- package/dist/runtime/form-continuity.d.ts +2 -2
- package/dist/runtime/form-continuity.js +30 -8
- package/dist/runtime/frame-nav.d.ts +7 -1
- package/dist/runtime/frame-nav.js +84 -18
- package/dist/runtime/frame-store.d.ts +25 -16
- package/dist/runtime/frame-store.js +124 -98
- package/dist/runtime/frames.js +8 -4
- package/dist/runtime/hole-facts.js +6 -7
- package/dist/runtime/hole-fetch.d.ts +17 -0
- package/dist/runtime/hole-fetch.js +47 -0
- package/dist/runtime/hydrate-core.d.ts +4 -4
- package/dist/runtime/hydrate-core.js +80 -64
- package/dist/runtime/import-again.d.ts +33 -0
- package/dist/runtime/import-again.js +217 -0
- package/dist/runtime/interaction-replay.d.ts +5 -3
- package/dist/runtime/interaction-replay.js +6 -6
- package/dist/runtime/interaction.d.ts +14 -3
- package/dist/runtime/interaction.js +46 -23
- package/dist/runtime/kit-page-thread.svelte.d.ts +2 -1
- package/dist/runtime/kit-page-thread.svelte.js +44 -20
- package/dist/runtime/lakes.d.ts +1 -1
- package/dist/runtime/lakes.js +18 -5
- package/dist/runtime/live.d.ts +1 -2
- package/dist/runtime/live.js +6 -8
- package/dist/runtime/load-scheduler.d.ts +29 -18
- package/dist/runtime/load-scheduler.js +236 -136
- package/dist/runtime/nav-handle.js +25 -6
- package/dist/runtime/navigation.d.ts +24 -0
- package/dist/runtime/navigation.js +24 -0
- package/dist/runtime/ownership.d.ts +1 -1
- package/dist/runtime/page-defer.js +3 -3
- package/dist/runtime/parse-html.d.ts +15 -4
- package/dist/runtime/parse-html.js +24 -5
- package/dist/runtime/pushable-props.svelte.d.ts +21 -0
- package/dist/runtime/pushable-props.svelte.js +37 -0
- package/dist/runtime/region-endpoint-url.js +4 -6
- package/dist/runtime/region-machines.d.ts +111 -0
- package/dist/runtime/region-machines.js +97 -0
- package/dist/runtime/region-sheets.d.ts +31 -0
- package/dist/runtime/region-sheets.js +49 -0
- package/dist/runtime/restore-report.d.ts +1 -0
- package/dist/runtime/restore-report.js +36 -0
- package/dist/runtime/restore.d.ts +5 -2
- package/dist/runtime/restore.js +33 -14
- package/dist/runtime/router-nav.d.ts +32 -12
- package/dist/runtime/router-nav.js +320 -228
- package/dist/runtime/router.d.ts +83 -19
- package/dist/runtime/router.js +297 -129
- package/dist/runtime/rules.d.ts +35 -0
- package/dist/runtime/rules.js +137 -0
- package/dist/runtime/schedule.d.ts +6 -3
- package/dist/runtime/schedule.js +172 -57
- package/dist/runtime/seeds.d.ts +5 -1
- package/dist/runtime/seeds.js +11 -5
- package/dist/runtime/session.d.ts +46 -7
- package/dist/runtime/session.js +78 -10
- package/dist/runtime/slots.d.ts +53 -20
- package/dist/runtime/slots.js +2 -3
- package/dist/runtime/soft-refresh.d.ts +16 -0
- package/dist/runtime/soft-refresh.js +14 -0
- package/dist/runtime/svelte-warnings.d.ts +4 -10
- package/dist/runtime/svelte-warnings.js +7 -4
- package/dist/runtime/wire-classes.js +21 -8
- package/dist/runtime/wire-format.js +6 -3
- package/dist/script.js +8 -7
- package/dist/seed-ref-reviver.d.ts +0 -9
- package/dist/seed-ref-reviver.js +2 -1
- package/dist/seed-refs.d.ts +46 -36
- package/dist/seed-refs.js +131 -92
- package/dist/server/document-tail.d.ts +6 -0
- package/dist/server/document-tail.js +7 -1
- package/dist/server/early-holes-csp.d.ts +14 -0
- package/dist/server/early-holes-csp.js +73 -0
- package/dist/server/page-facts.d.ts +4 -1
- package/dist/server/page-facts.js +4 -1
- package/dist/server/page-seed.d.ts +2 -2
- package/dist/server/page-seed.js +104 -11
- package/dist/server/page-stream.d.ts +14 -1
- package/dist/server/page-stream.js +119 -2
- package/dist/server/personal-response.d.ts +0 -5
- package/dist/server/personal-response.js +6 -2
- package/dist/server/props-wire.js +5 -1
- package/dist/server/region-endpoint.js +10 -2
- package/dist/server/render-cache.d.ts +24 -20
- package/dist/server/render-cache.js +99 -40
- package/dist/server/render-page.js +46 -15
- package/dist/server/request-stats.d.ts +10 -8
- package/dist/server/request-stats.js +20 -12
- package/dist/server/rules.d.ts +11 -0
- package/dist/server/rules.js +64 -0
- package/dist/server/signing-key.js +31 -18
- package/dist/server/stream-regions.d.ts +14 -1
- package/dist/server/stream-regions.js +38 -2
- package/dist/shared-state.js +5 -1
- package/dist/shims/app-navigation.js +3 -2
- package/dist/shims/page-store.svelte.js +16 -2
- package/dist/source.js +7 -2
- package/dist/store-transport.js +12 -5
- package/dist/transport.js +3 -2
- package/dist/vite/index.js +17 -10
- package/dist/vite/kit-dirs.js +7 -1
- package/dist/vite/options.d.ts +2 -1
- package/dist/vite/options.js +11 -20
- package/dist/vite/package-files.js +13 -3
- package/dist/vite/paths.d.ts +2 -0
- package/dist/vite/paths.js +4 -4
- package/dist/vite/plugin-api.js +42 -14
- package/dist/vite/style-preprocess.js +10 -3
- package/package.json +8 -1
- package/dist/KeepHost.svelte +0 -19
- package/dist/KeepHost.svelte.d.ts +0 -10
- package/dist/LiveHost.svelte +0 -19
- package/dist/LiveHost.svelte.d.ts +0 -10
- package/dist/NestedProvider.svelte +0 -13
- package/dist/NestedProvider.svelte.d.ts +0 -8
- package/dist/runtime/page-cache.d.ts +0 -23
- package/dist/runtime/page-cache.js +0 -74
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { PageCache } from "./page-cache.js";
|
|
2
1
|
import { boot_link, router_link, slots } from "./slots.js";
|
|
3
2
|
import { dispose_scope } from "../ref.js";
|
|
4
3
|
import { reconcile_body, stamp_region_keys, swap_body } from "./reconcile.js";
|
|
@@ -12,26 +11,31 @@ import { emit } from "../events/bus.js";
|
|
|
12
11
|
* intercepted click: page fetch + cache, head merge, the body reconcile / morph, seeds, a11y.
|
|
13
12
|
* Nothing here runs at boot; a visitor who never leaves the page never downloads it.
|
|
14
13
|
*
|
|
15
|
-
* A navigation is
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
14
|
+
* A navigation is a STAGED CHANGE (`stage`, @ogygia/machine), in two tasks. The build — this
|
|
15
|
+
* module's `navigate` up to the commit — does everything that needs no live DOM change: fetch and
|
|
16
|
+
* parse the HTML, read the route facts, batch the incoming holes, stamp reconcile keys, parse the
|
|
17
|
+
* incoming page seed, drop hints for modules already warm, load the stylesheets (inert: they style
|
|
18
|
+
* nothing yet). Then it yields. The commit is one synchronous step, the view transition's callback
|
|
19
|
+
* or a plain call, and does ONLY the DOM: head merge, seeds, body morph, title. A navigation left
|
|
20
|
+
* before its commit gives back what it built; one whose commit throws goes to the browser. The old
|
|
21
|
+
* single-task swap ran a whole-document regex, two `'*'` walks and a seed parse inside
|
|
22
|
+
* `startViewTransition`, on the frame the transition captures. The soft refresh is staged the same
|
|
23
|
+
* way.
|
|
22
24
|
*/
|
|
23
25
|
const KitBoot = { document_has: (doc) => boot_link().KitBoot.document_has(doc) };
|
|
24
26
|
const regions_in_shadow = () => boot_link().regions_in_shadow();
|
|
27
|
+
const release_sheets = (unlisted) => boot_link().release_sheets(unlisted);
|
|
25
28
|
const session = () => boot_link().runtime_session;
|
|
29
|
+
/** devalue first, when the fetched page holds a script in its lane (./devalue-lane.ts) */
|
|
30
|
+
const devalue_for = (doc) => boot_link().needs_devalue(doc) ? boot_link().load_devalue() : void 0;
|
|
26
31
|
const is_warmed_module = (entry, base) => boot_link().is_warmed_module(entry, base);
|
|
27
32
|
const warm_island_module = (entry, base) => boot_link().warm_island_module(entry, base);
|
|
28
33
|
const note_entry_location = (entry, src, base) => boot_link().note_entry_location(entry, src, base);
|
|
29
34
|
const register_island_graph = (text, base) => boot_link().register_island_graph(text, base);
|
|
30
35
|
const yield_task = () => boot_link().yield_task();
|
|
31
|
-
|
|
36
|
+
/** What a machine's task answers once its visit is left (the boot's `@ogygia/machine`). */
|
|
37
|
+
const is_stale = (v) => v === boot_link().STALE;
|
|
32
38
|
const jump_to_hash = (hash) => router_link().jump_to_hash(hash);
|
|
33
|
-
const push_state = (state, url) => router_link().push_state(state, url);
|
|
34
|
-
const replace_state = (state, url) => router_link().replace_state(state, url);
|
|
35
39
|
const DEVTOOLS = typeof __OGYGIA_DEVTOOLS__ !== "undefined" ? __OGYGIA_DEVTOOLS__ : false;
|
|
36
40
|
/** High-res clock for devtools nav timings (guarded — dead when off). */
|
|
37
41
|
const dt_now = () => typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
@@ -117,6 +121,8 @@ const PAGE_CACHE_MAX_ENTRIES = 32;
|
|
|
117
121
|
const PAGE_CACHE_MAX_BYTES = 4e6;
|
|
118
122
|
/** Cap on waiting for a destination stylesheet to load before the body swap (cold-cache FOUC guard). */
|
|
119
123
|
const STYLESHEET_WAIT_MS = 2e3;
|
|
124
|
+
/** A `Content-Disposition` that makes a response a file to save. */
|
|
125
|
+
const ATTACHMENT = /^\s*attachment\b/i;
|
|
120
126
|
/** Kit remote-function POSTs live under `…/_app/remote/…` (or custom `appDir`). */
|
|
121
127
|
const REMOTE_MUTATION_PATH = /\/remote(?:\/|$|\?)/;
|
|
122
128
|
/** An `<ogygia-region>` open tag's attributes (shared `g` regex — reset `lastIndex` per scan). */
|
|
@@ -213,20 +219,36 @@ function apply_late_templates(root) {
|
|
|
213
219
|
* linger in the tab order. For `<body>` the attribute is removed immediately (body stays the active
|
|
214
220
|
* element regardless). For a normal element — a `#hash` heading — removing `tabindex` while it is
|
|
215
221
|
* focused would BLUR it back to `<body>`, so the cleanup is deferred to the element's next `blur`.
|
|
222
|
+
*
|
|
223
|
+
* The `tabindex` is LENT until that blur (`lent`), once per element: a second navigation that
|
|
224
|
+
* focuses it meanwhile (a refresh in place at its `#hash`) finds the loan and leaves it, instead of
|
|
225
|
+
* saving the first one's `-1` as the element's own and putting that back after the first cleanup
|
|
226
|
+
* (#278). The loan is the element's, not the page's: an element kept across a navigation (inside a
|
|
227
|
+
* kept island) still gives its `tabindex` back when focus leaves it on the next page.
|
|
216
228
|
*/
|
|
217
229
|
function focus_reset(el) {
|
|
218
230
|
const is_body = el === document.body || el === document.documentElement;
|
|
219
|
-
|
|
231
|
+
if (lent.has(el)) {
|
|
232
|
+
el.focus({ preventScroll: true });
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
220
235
|
const prev = el.getAttribute("tabindex");
|
|
221
236
|
const restore = () => {
|
|
222
|
-
|
|
237
|
+
lent.delete(el);
|
|
238
|
+
if (prev !== null) el.setAttribute("tabindex", prev);
|
|
223
239
|
else el.removeAttribute("tabindex");
|
|
224
240
|
};
|
|
225
241
|
el.tabIndex = -1;
|
|
226
242
|
el.focus({ preventScroll: true });
|
|
227
|
-
if (is_body)
|
|
228
|
-
|
|
243
|
+
if (is_body) {
|
|
244
|
+
restore();
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
lent.set(el, prev);
|
|
248
|
+
el.addEventListener("blur", restore, { once: true });
|
|
229
249
|
}
|
|
250
|
+
/** The elements whose `tabindex` a navigation's focus lent until they blur, with their own value. */
|
|
251
|
+
const lent = /* @__PURE__ */ new WeakMap();
|
|
230
252
|
/** Head nodes that must never be adopted from SPA HTML (rewrite relative fetches / CSP). */
|
|
231
253
|
function is_dangerous_head_node(node) {
|
|
232
254
|
const tag = node.tagName;
|
|
@@ -237,38 +259,44 @@ function is_dangerous_head_node(node) {
|
|
|
237
259
|
}
|
|
238
260
|
return false;
|
|
239
261
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
262
|
+
let page_flights;
|
|
263
|
+
/** The world the cached pages are made in: a mutation moves it (`bust_page_cache`). */
|
|
264
|
+
let page_world;
|
|
265
|
+
/** The page fetches: one flight per address, shared by every navigation and prefetch of it. A
|
|
266
|
+
* cacheable answer is kept for PREFETCH_TTL_MS, within PAGE_CACHE_MAX_BYTES (each entry weighs at
|
|
267
|
+
* least its share of it, so no more than PAGE_CACHE_MAX_ENTRIES small pages are kept). They are made
|
|
268
|
+
* in `page_world`: a move of it drops them all, a fetch in flight is aborted, and a navigation that
|
|
269
|
+
* waited on it fetches again. */
|
|
270
|
+
const pages = () => page_flights ??= boot_link().flights.keyed()({
|
|
271
|
+
name: "router pages",
|
|
272
|
+
key: ["href"],
|
|
273
|
+
ignore: { purpose: "a navigation joins the prefetch out for its address: how, not what" },
|
|
274
|
+
run: (ask, signal) => run_page_fetch(ask, signal),
|
|
275
|
+
world: page_world = boot_link().world(),
|
|
276
|
+
keep: (answer) => answer.page !== null && answer.cacheable ? PREFETCH_TTL_MS : 0,
|
|
277
|
+
max: PAGE_CACHE_MAX_BYTES,
|
|
278
|
+
weigh: (answer) => Math.max((answer.page?.html.length ?? 0) * 2, PAGE_CACHE_MAX_BYTES / PAGE_CACHE_MAX_ENTRIES)
|
|
244
279
|
});
|
|
245
|
-
const inflight = /* @__PURE__ */ new Map();
|
|
246
280
|
/** Prefetches still waiting in the load scheduler's queue, by href (a click promotes its one). */
|
|
247
281
|
const queued_prefetch = /* @__PURE__ */ new Map();
|
|
248
|
-
/** requested href → the FINAL href a fetch landed on after following server redirects, when it
|
|
249
|
-
* differs. One-shot: `navigate()` reads it to correct the address bar (a redirect is invisible to
|
|
250
|
-
* `fetch` beyond `response.url`), then deletes it. See {@link fetch_page}. */
|
|
251
|
-
const final_url = /* @__PURE__ */ new Map();
|
|
252
282
|
/** Hrefs whose prefetched HTML was already scanned for island entries — parse once. (URL-level
|
|
253
283
|
* import dedupe lives in the shared `warm_island_module`.) */
|
|
254
284
|
const warmed_pages = /* @__PURE__ */ new Set();
|
|
255
|
-
/** (Hard SPA navigations
|
|
256
|
-
*
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
let soft_abort = null;
|
|
285
|
+
/** (Hard SPA navigations are the router's own machine, `r.nav` (./navigation.ts), so a "same
|
|
286
|
+
* document" decision there can stop one in flight: SpaRouter.supersede. The soft refresh is its
|
|
287
|
+
* machine too, `boot_link().soft` (./soft-refresh.ts): the boot holds it, as a page without the
|
|
288
|
+
* router refreshes too.) */
|
|
260
289
|
/** The visually-hidden `aria-live` region that announces each navigation to screen readers. Created
|
|
261
290
|
* lazily on the first nav and reused (a live region must persist so assistive tech observes its
|
|
262
291
|
* mutations). It lives in `<body>` but is DETACHED before each body swap so the reconcile never
|
|
263
292
|
* removes it, then re-attached. See {@link announce} / {@link reset_focus}. */
|
|
264
293
|
let announcer = null;
|
|
294
|
+
/** Moves the world the cached pages are made in: what is kept goes, and a fetch that started before
|
|
295
|
+
* the move (a prefetch out when a remote mutation lands) never fills the cache after it with the
|
|
296
|
+
* page as it was (#208), and a navigation that waited on it fetches again. (No pages yet: none to
|
|
297
|
+
* put out of date.) */
|
|
265
298
|
function bust_page_cache() {
|
|
266
|
-
|
|
267
|
-
inflight.clear();
|
|
268
|
-
final_url.clear();
|
|
269
|
-
}
|
|
270
|
-
function page_cache_size() {
|
|
271
|
-
return page_cache.size;
|
|
299
|
+
page_world?.bump();
|
|
272
300
|
}
|
|
273
301
|
/**
|
|
274
302
|
* Bust the SPA HTML cache after any successful Kit remote mutation (command/form POST).
|
|
@@ -292,65 +320,81 @@ function install_remote_mutation_cache_bust() {
|
|
|
292
320
|
};
|
|
293
321
|
}
|
|
294
322
|
install_remote_mutation_cache_bust();
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
323
|
+
/**
|
|
324
|
+
* The page at `href`, as HTML plus the address it belongs to: a server redirect (a Kit `redirect()`
|
|
325
|
+
* in load, a trailing-slash canonicalization) is followed by `fetch` transparently, and
|
|
326
|
+
* `response.url` is its only trace. The landing travels WITH the HTML, through the cache and to the
|
|
327
|
+
* navigation that reads it, so nothing else (a cache clear, another fetch) can take it away.
|
|
328
|
+
*/
|
|
329
|
+
async function fetch_page(href, signal, purpose = "nav") {
|
|
330
|
+
const ask = {
|
|
331
|
+
href,
|
|
332
|
+
purpose
|
|
333
|
+
};
|
|
334
|
+
for (;;) {
|
|
335
|
+
if (purpose !== "prefetch") queued_prefetch.get(href)?.promote("user");
|
|
336
|
+
const answer = await pages().get(ask, { signal });
|
|
337
|
+
if (answer !== boot_link().STALE) {
|
|
338
|
+
if (purpose === "prefetch" && answer.page !== null) warm_modules(href, answer.page.html);
|
|
339
|
+
return answer.page;
|
|
340
|
+
}
|
|
341
|
+
if (signal?.aborted || purpose === "prefetch") return null;
|
|
302
342
|
}
|
|
303
|
-
|
|
343
|
+
}
|
|
344
|
+
/** One page fetch (a flight's work): a prefetch waits for its turn in the load scheduler first. */
|
|
345
|
+
async function run_page_fetch({ href, purpose }, signal) {
|
|
304
346
|
const go = () => fetch(href, {
|
|
305
347
|
signal,
|
|
306
348
|
headers: nav_headers(purpose),
|
|
307
349
|
...purpose === "prefetch" ? { priority: "low" } : {}
|
|
308
350
|
});
|
|
309
|
-
let
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
queued_prefetch.
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
351
|
+
let res;
|
|
352
|
+
try {
|
|
353
|
+
if (purpose === "prefetch") {
|
|
354
|
+
const { LoadSlot } = boot_link();
|
|
355
|
+
const slot = new LoadSlot({
|
|
356
|
+
kind: "content",
|
|
357
|
+
cls: "speculative",
|
|
358
|
+
label: href
|
|
359
|
+
});
|
|
360
|
+
queued_prefetch.set(href, slot);
|
|
361
|
+
const release = await slot.ready;
|
|
362
|
+
if (queued_prefetch.get(href) === slot) queued_prefetch.delete(href);
|
|
363
|
+
try {
|
|
364
|
+
if (signal.aborted) return {
|
|
365
|
+
page: null,
|
|
366
|
+
cacheable: false
|
|
367
|
+
};
|
|
368
|
+
res = await go();
|
|
369
|
+
} finally {
|
|
370
|
+
release();
|
|
371
|
+
}
|
|
372
|
+
} else res = await go();
|
|
323
373
|
if (!(res.headers.get("content-type") || "").includes("text/html")) return {
|
|
324
|
-
|
|
374
|
+
page: null,
|
|
325
375
|
cacheable: false
|
|
326
376
|
};
|
|
327
|
-
if (
|
|
377
|
+
if (ATTACHMENT.test(res.headers.get("content-disposition") || "")) return {
|
|
378
|
+
page: null,
|
|
379
|
+
cacheable: false
|
|
380
|
+
};
|
|
381
|
+
const url = res.redirected && res.url ? res.url : href;
|
|
328
382
|
const html = await res.text();
|
|
329
383
|
const cc = res.headers.get("cache-control") || "";
|
|
330
384
|
return {
|
|
331
|
-
|
|
385
|
+
page: {
|
|
386
|
+
html,
|
|
387
|
+
url
|
|
388
|
+
},
|
|
332
389
|
cacheable: spa_html_cacheable(cc, res.headers.has("set-cookie"))
|
|
333
390
|
};
|
|
334
|
-
}
|
|
391
|
+
} catch (err) {
|
|
335
392
|
if (err && err.name === "AbortError") throw err;
|
|
336
393
|
return {
|
|
337
|
-
|
|
394
|
+
page: null,
|
|
338
395
|
cacheable: false
|
|
339
396
|
};
|
|
340
|
-
});
|
|
341
|
-
const html_p = settled.then((r) => r.html);
|
|
342
|
-
if (!signal) {
|
|
343
|
-
inflight.set(href, html_p);
|
|
344
|
-
html_p.finally(() => {
|
|
345
|
-
if (inflight.get(href) === html_p) inflight.delete(href);
|
|
346
|
-
});
|
|
347
|
-
settled.then((r) => {
|
|
348
|
-
if (r.html == null) return;
|
|
349
|
-
if (r.cacheable) page_cache.set(href, r.html);
|
|
350
|
-
warm_modules(href, r.html);
|
|
351
|
-
}).catch(() => {});
|
|
352
397
|
}
|
|
353
|
-
return html_p;
|
|
354
398
|
}
|
|
355
399
|
/**
|
|
356
400
|
* Kick off import() for every island module the prefetched page will hydrate, so they are in the
|
|
@@ -401,16 +445,18 @@ function build_of(d) {
|
|
|
401
445
|
const file = (v) => v ? v.slice(v.lastIndexOf("/") + 1) : "";
|
|
402
446
|
return file(s.getAttribute("src")) + " " + file(s.getAttribute("data-og-locations"));
|
|
403
447
|
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
448
|
+
/** What matches no screen: a sheet under it loads, and styles nothing. */
|
|
449
|
+
const INERT_MEDIA = "not all";
|
|
450
|
+
/** The next page's sheet goes live: it styles the page as it came. */
|
|
451
|
+
function apply_sheet({ link, media, priority }) {
|
|
452
|
+
if (media === null) link.removeAttribute("media");
|
|
453
|
+
else link.setAttribute("media", media);
|
|
454
|
+
if (priority === null) link.removeAttribute("fetchpriority");
|
|
455
|
+
}
|
|
456
|
+
async function navigate(r, url, from, { push = true, pop, type = "link", replace = false } = {}, claim = r.nav.now) {
|
|
457
|
+
if (r.nav.now !== claim) return;
|
|
409
458
|
const dt_t0 = DEVTOOLS ? dt_now() : 0;
|
|
410
|
-
let dt_reconciled = false;
|
|
411
459
|
const nav_t0 = performance.now();
|
|
412
|
-
let nav_fetched = nav_t0;
|
|
413
|
-
let nav_styled = nav_t0;
|
|
414
460
|
if (DEVTOOLS) emit({
|
|
415
461
|
domain: "nav",
|
|
416
462
|
name: "nav.start",
|
|
@@ -418,93 +464,80 @@ async function navigate(r, url, from, { push = true, pop_scroll = null, type = "
|
|
|
418
464
|
to: url.pathname + url.search,
|
|
419
465
|
type
|
|
420
466
|
});
|
|
421
|
-
r.
|
|
422
|
-
|
|
423
|
-
const
|
|
424
|
-
const
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
if (r.nav_target === url.href) r.nav_target = null;
|
|
467
|
+
r.nav.go("loading", { href: url.href });
|
|
468
|
+
const mine = r.nav.now;
|
|
469
|
+
const attempt = r.nav.scope;
|
|
470
|
+
const next = session().next_visit();
|
|
471
|
+
attempt.defer(() => {
|
|
472
|
+
if (session().visit !== next) next.scope.close();
|
|
428
473
|
});
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
ogygia: true
|
|
432
|
-
}, url.href);
|
|
433
|
-
else if (push) {
|
|
434
|
-
replace_state({
|
|
435
|
-
...history.state,
|
|
436
|
-
scroll: {
|
|
437
|
-
x: scrollX,
|
|
438
|
-
y: scrollY
|
|
439
|
-
}
|
|
440
|
-
});
|
|
441
|
-
push_state({ ogygia: true }, url.href);
|
|
442
|
-
}
|
|
474
|
+
const move = pop ? "pop" : push && !replace ? "push" : "replace";
|
|
475
|
+
const hand_off = (error, written = false) => r.hand_off(url, mine, written ? "replace" : move, error);
|
|
443
476
|
const purpose = type === "popstate" ? "history" : "nav";
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
477
|
+
const build = async ({ signal, discard }) => {
|
|
478
|
+
const page = await fetch_page(url.href, signal, purpose);
|
|
479
|
+
if (page === null || signal.aborted) return null;
|
|
480
|
+
const { html } = page;
|
|
481
|
+
const fetched = performance.now();
|
|
482
|
+
pages().drop(({ href }, entry) => href === url.href && entry.kind === "kept" && !entry.flying);
|
|
483
|
+
let dest = url;
|
|
484
|
+
if (page.url !== url.href) try {
|
|
485
|
+
dest = new URL(page.url, url);
|
|
486
|
+
} catch {
|
|
487
|
+
dest = url;
|
|
488
|
+
}
|
|
489
|
+
if (dest.origin !== location.origin) return null;
|
|
490
|
+
const doc = new DOMParser().parseFromString(html, "text/html");
|
|
491
|
+
if (KitBoot.document_has(doc)) return null;
|
|
492
|
+
const marker = doc.querySelector("meta[name=\"ogygia-router\"]");
|
|
493
|
+
if (!marker) return null;
|
|
494
|
+
const next_build = build_of(doc);
|
|
495
|
+
const this_build = next_build === null ? null : build_of(document);
|
|
496
|
+
if (this_build !== null && next_build !== this_build) return null;
|
|
497
|
+
if (doc.querySelector("meta[name=\"ogygia-first-load\"]")) return null;
|
|
498
|
+
globalThis.__og_restore?.(doc);
|
|
499
|
+
const prefer_reduced_motion = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
500
|
+
const use_vt = marker.getAttribute("content") !== "plain" && !prefer_reduced_motion;
|
|
501
|
+
for (const el of doc.querySelectorAll("ogygia-region")) pin_region_urls(el, dest.href);
|
|
502
|
+
batch_regions(doc, next.scope);
|
|
503
|
+
await devalue_for(doc);
|
|
504
|
+
if (signal.aborted) return null;
|
|
505
|
+
preflight(doc, dest);
|
|
506
|
+
const sheets = [];
|
|
507
|
+
await preload_stylesheets(doc.head, attempt, sheets, discard);
|
|
508
|
+
if (signal.aborted) return null;
|
|
509
|
+
const styled = performance.now();
|
|
510
|
+
await yield_task();
|
|
511
|
+
return {
|
|
512
|
+
doc,
|
|
513
|
+
dest,
|
|
514
|
+
use_vt,
|
|
515
|
+
sheets,
|
|
516
|
+
fetched,
|
|
517
|
+
styled,
|
|
518
|
+
reconciled: false,
|
|
519
|
+
written: false
|
|
520
|
+
};
|
|
469
521
|
};
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
globalThis.__og_restore?.(doc);
|
|
480
|
-
if (dest !== url) replace_state({
|
|
481
|
-
...history.state,
|
|
482
|
-
ogygia: true
|
|
483
|
-
}, dest.href);
|
|
484
|
-
const prefer_reduced_motion = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
485
|
-
const use_vt = marker.getAttribute("content") !== "plain" && !prefer_reduced_motion;
|
|
486
|
-
for (const el of doc.querySelectorAll("ogygia-region")) pin_region_urls(el, dest.href);
|
|
487
|
-
batch_regions(doc, dest.href);
|
|
488
|
-
preflight(doc, dest);
|
|
489
|
-
await preload_stylesheets(doc.head);
|
|
490
|
-
if (gen !== r.nav_gen) return;
|
|
491
|
-
nav_styled = performance.now();
|
|
492
|
-
await yield_task();
|
|
493
|
-
if (gen !== r.nav_gen) return;
|
|
494
|
-
const reconcile = !!slots.morph && !regions_in_shadow() && !document.body.querySelector("[data-og-no-reconcile]") && !doc.body.querySelector("[data-og-no-reconcile]");
|
|
495
|
-
const swap = () => {
|
|
496
|
-
if (gen !== r.nav_gen) return;
|
|
522
|
+
const commit = (built) => {
|
|
523
|
+
if (built === null) return;
|
|
524
|
+
const { doc, dest } = built;
|
|
525
|
+
const { soft } = boot_link();
|
|
526
|
+
if (soft.now.kind === "refreshing") soft.go("idle");
|
|
527
|
+
const left = r.current_url;
|
|
528
|
+
r.history.commit(dest, move, pop);
|
|
529
|
+
built.written = true;
|
|
530
|
+
const reconcile = !!slots.morph && !regions_in_shadow() && !document.body.querySelector("[data-og-no-reconcile]") && !doc.body.querySelector("[data-og-no-reconcile]");
|
|
497
531
|
announcer?.remove();
|
|
498
532
|
document.dispatchEvent(new Event("og:before-swap"));
|
|
499
|
-
merge_head(doc.head);
|
|
500
|
-
|
|
501
|
-
if (slots.forms.enabled
|
|
502
|
-
prepare_spa_document();
|
|
533
|
+
const unlisted_sheets = merge_head(doc.head);
|
|
534
|
+
for (const sheet of built.sheets) apply_sheet(sheet);
|
|
535
|
+
if (slots.forms.enabled) slots.forms.snapshot(document.body, left.pathname);
|
|
536
|
+
prepare_spa_document(next);
|
|
503
537
|
apply_soft_invalidate_doc(doc);
|
|
504
|
-
if (gen !== r.nav_gen) return;
|
|
505
538
|
if (reconcile) {
|
|
506
539
|
reconcile_body(document.body, doc.body, slots.morph);
|
|
507
|
-
|
|
540
|
+
built.reconciled = true;
|
|
508
541
|
document.title = doc.title;
|
|
509
542
|
session().settle_lakes_in(document.body);
|
|
510
543
|
} else {
|
|
@@ -520,29 +553,53 @@ async function navigate(r, url, from, { push = true, pop_scroll = null, type = "
|
|
|
520
553
|
}
|
|
521
554
|
globalThis.__og_restore_adopt?.();
|
|
522
555
|
session().retire_page_head_copies();
|
|
556
|
+
release_sheets(unlisted_sheets);
|
|
523
557
|
apply_late_templates(document.body);
|
|
524
558
|
finish_spa_document();
|
|
525
559
|
if (slots.forms.enabled) slots.forms.restore(url.pathname);
|
|
526
560
|
document.dispatchEvent(new Event("og:after-swap"));
|
|
527
561
|
};
|
|
528
|
-
|
|
529
|
-
|
|
562
|
+
const staged = boot_link().stage(attempt, {
|
|
563
|
+
name: "navigation",
|
|
564
|
+
build,
|
|
565
|
+
commit
|
|
566
|
+
});
|
|
567
|
+
let built;
|
|
568
|
+
try {
|
|
569
|
+
built = await staged.ready;
|
|
570
|
+
} catch (error) {
|
|
571
|
+
return hand_off(error);
|
|
572
|
+
}
|
|
573
|
+
if (is_stale(built) || r.nav.now !== mine) return;
|
|
574
|
+
if (built === null) return hand_off();
|
|
575
|
+
const { dest } = built;
|
|
576
|
+
const out = {};
|
|
577
|
+
const swap = () => {
|
|
578
|
+
try {
|
|
579
|
+
staged.commit();
|
|
580
|
+
} catch (error) {
|
|
581
|
+
out.failed = { error };
|
|
582
|
+
}
|
|
583
|
+
};
|
|
584
|
+
if (built.use_vt && document.startViewTransition) {
|
|
585
|
+
const restore_vt_names = fold_orphan_vt_names(document.body, built.doc.body);
|
|
530
586
|
const t = document.startViewTransition(swap);
|
|
531
587
|
t.ready?.catch(() => {});
|
|
532
588
|
(t.finished ?? Promise.resolve()).then(restore_vt_names, restore_vt_names);
|
|
533
589
|
await t.updateCallbackDone.catch(() => {});
|
|
590
|
+
if (out.failed) t.skipTransition?.();
|
|
534
591
|
} else swap();
|
|
535
|
-
if (
|
|
536
|
-
r.
|
|
537
|
-
r.
|
|
538
|
-
|
|
592
|
+
if (out.failed) return hand_off(out.failed.error, built.written);
|
|
593
|
+
if (r.nav.now !== mine) return;
|
|
594
|
+
r.nav.move("loading", "settling", { href: url.href });
|
|
595
|
+
const settled = r.nav.now;
|
|
539
596
|
boot_link().beacon_nav({
|
|
540
597
|
from: from.pathname + from.search,
|
|
541
598
|
to: dest.pathname + dest.search,
|
|
542
599
|
type,
|
|
543
600
|
t: nav_t0,
|
|
544
|
-
fetched:
|
|
545
|
-
styled:
|
|
601
|
+
fetched: built.fetched,
|
|
602
|
+
styled: built.styled,
|
|
546
603
|
swapped: performance.now(),
|
|
547
604
|
href: url.href
|
|
548
605
|
});
|
|
@@ -551,27 +608,25 @@ async function navigate(r, url, from, { push = true, pop_scroll = null, type = "
|
|
|
551
608
|
name: "nav.finish",
|
|
552
609
|
to: dest.pathname + dest.search,
|
|
553
610
|
ms: dt_now() - dt_t0,
|
|
554
|
-
reconciled:
|
|
555
|
-
vt: !!(use_vt && document.startViewTransition)
|
|
611
|
+
reconciled: built.reconciled,
|
|
612
|
+
vt: !!(built.use_vt && document.startViewTransition)
|
|
556
613
|
});
|
|
557
|
-
|
|
614
|
+
const scroll = pop?.scroll;
|
|
615
|
+
if (replace) {} else if (scroll) {
|
|
558
616
|
const html_el = document.documentElement;
|
|
559
617
|
const prev = html_el.style.scrollBehavior;
|
|
560
618
|
html_el.style.scrollBehavior = "auto";
|
|
561
619
|
try {
|
|
562
|
-
window.scrollTo(
|
|
620
|
+
window.scrollTo(scroll.x, scroll.y);
|
|
563
621
|
} finally {
|
|
564
622
|
html_el.style.scrollBehavior = prev;
|
|
565
623
|
}
|
|
566
624
|
} else jump_to_hash(dest.hash);
|
|
567
625
|
announce(dest);
|
|
568
|
-
reset_focus(dest.hash);
|
|
626
|
+
reset_focus(dest.hash, pop?.focus ?? null);
|
|
569
627
|
r.run_after(from, dest, type);
|
|
570
628
|
document.dispatchEvent(new Event("og:page-load"));
|
|
571
|
-
if (
|
|
572
|
-
r.nav_abort = null;
|
|
573
|
-
if (r.nav_target === url.href) r.nav_target = null;
|
|
574
|
-
}
|
|
629
|
+
if (r.nav.now === settled) r.nav.go("idle");
|
|
575
630
|
r.scan_preload_links();
|
|
576
631
|
}
|
|
577
632
|
/**
|
|
@@ -585,29 +640,44 @@ async function navigate(r, url, from, { push = true, pop_scroll = null, type = "
|
|
|
585
640
|
* that need query updates use `.refresh()`, or `submit().updates(q)` with server
|
|
586
641
|
* `requested(q).refreshAll()` (updates alone does not populate response `q`).
|
|
587
642
|
*
|
|
588
|
-
*
|
|
589
|
-
* an in-flight click navigation
|
|
643
|
+
* Its own machine (`soft`, ./soft-refresh.ts, which the boot holds: a page without the router
|
|
644
|
+
* refreshes too) so soft fetches never cancel an in-flight click navigation; a real navigation's
|
|
645
|
+
* commit ends a soft refresh in flight.
|
|
646
|
+
*
|
|
647
|
+
* It refreshes the PAGE ON SCREEN (`on_screen`: the router's, which knows it; the address bar
|
|
648
|
+
* without the router), never the page a navigation is still bringing (#268). The refreshed page is
|
|
649
|
+
* staged like a navigation's (`stage`): fetched and parsed off to the side, merged in one commit,
|
|
650
|
+
* which a navigation that went in first refuses (its commit ended this refresh). A refresh whose
|
|
651
|
+
* devalue fails to load ends, and says so to its caller.
|
|
590
652
|
*/
|
|
591
|
-
async function invalidate_all() {
|
|
653
|
+
async function invalidate_all(on_screen) {
|
|
592
654
|
bust_page_cache();
|
|
593
|
-
const url = new URL(location.href);
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
const
|
|
597
|
-
const
|
|
598
|
-
|
|
655
|
+
const url = on_screen ?? new URL(location.href);
|
|
656
|
+
const { soft, stage } = boot_link();
|
|
657
|
+
soft.go("refreshing", { href: url.href });
|
|
658
|
+
const mine = soft.now;
|
|
659
|
+
const staged = stage(soft.scope, {
|
|
660
|
+
name: "soft refresh",
|
|
661
|
+
build: async ({ signal }) => {
|
|
662
|
+
const fetched = await fetch_page(url.href, signal);
|
|
663
|
+
if (fetched === null || signal.aborted) return null;
|
|
664
|
+
const doc = new DOMParser().parseFromString(fetched.html, "text/html");
|
|
665
|
+
if (KitBoot.document_has(doc) || !doc.querySelector("meta[name=\"ogygia-router\"]")) return null;
|
|
666
|
+
await devalue_for(doc);
|
|
667
|
+
return doc;
|
|
668
|
+
},
|
|
669
|
+
commit: (doc) => {
|
|
670
|
+
if (doc === null) return;
|
|
671
|
+
release_sheets(merge_head(doc.head));
|
|
672
|
+
apply_soft_invalidate_doc(doc);
|
|
673
|
+
}
|
|
674
|
+
});
|
|
599
675
|
try {
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
676
|
+
if (is_stale(await staged.ready)) return;
|
|
677
|
+
staged.commit();
|
|
678
|
+
} finally {
|
|
679
|
+
if (soft.now === mine) soft.go("idle");
|
|
604
680
|
}
|
|
605
|
-
if (gen !== soft_gen || html == null) return;
|
|
606
|
-
const doc = new DOMParser().parseFromString(html, "text/html");
|
|
607
|
-
if (KitBoot.document_has(doc)) return;
|
|
608
|
-
if (!doc.querySelector("meta[name=\"ogygia-router\"]")) return;
|
|
609
|
-
merge_head(doc.head);
|
|
610
|
-
apply_soft_invalidate_doc(doc);
|
|
611
681
|
}
|
|
612
682
|
/**
|
|
613
683
|
* ACCESSIBILITY — announce the new page. A body swap is invisible to assistive tech: no document
|
|
@@ -636,11 +706,15 @@ function announce(dest) {
|
|
|
636
706
|
* does this natively; a body swap does not). SvelteKit's algorithm: a `[autofocus]` element wins;
|
|
637
707
|
* else a `#hash` target; else focus falls back to `<body>` (made momentarily focusable). The one
|
|
638
708
|
* ogygia twist: a control INSIDE kept/persisted chrome (`data-ogygia-keep` / `data-persist`) that
|
|
639
|
-
* survived the nav is left focused — the visitor is still using that persistent widget.
|
|
709
|
+
* survived the nav is left focused — the visitor is still using that persistent widget. A back or
|
|
710
|
+
* forward step first gives focus back to the element (`kept`, by id) that had it when its entry
|
|
711
|
+
* was left (./router.ts `RouterHistory`).
|
|
640
712
|
*/
|
|
641
|
-
function reset_focus(hash) {
|
|
713
|
+
function reset_focus(hash, kept = null) {
|
|
642
714
|
const active = document.activeElement;
|
|
643
715
|
if (active && active !== document.body && active.isConnected && active.closest("[data-ogygia-keep],[data-persist]")) return;
|
|
716
|
+
const was = kept ? document.getElementById(kept) : null;
|
|
717
|
+
if (was) return focus_reset(was);
|
|
644
718
|
if (hash) {
|
|
645
719
|
const id = decodeURIComponent(hash.slice(1));
|
|
646
720
|
const target = id ? document.getElementById(id) : null;
|
|
@@ -653,7 +727,11 @@ function reset_focus(hash) {
|
|
|
653
727
|
}
|
|
654
728
|
focus_reset(document.body);
|
|
655
729
|
}
|
|
656
|
-
/**
|
|
730
|
+
/**
|
|
731
|
+
* Merge <head>: keep nodes present in both, remove stale, add new. Keeps runtime scripts alive.
|
|
732
|
+
* Returns the region sheets the new head does not list: they are for the regions that use them too
|
|
733
|
+
* (./region-sheets.ts), so they go only after the swap, once it is settled which regions stayed.
|
|
734
|
+
*/
|
|
657
735
|
function merge_head(new_head) {
|
|
658
736
|
const current = document.head;
|
|
659
737
|
const owned = session().page_head;
|
|
@@ -664,36 +742,49 @@ function merge_head(new_head) {
|
|
|
664
742
|
if (is_dangerous_head_node(node)) continue;
|
|
665
743
|
next_keys.add(head_node_key(node));
|
|
666
744
|
}
|
|
745
|
+
const unlisted = [];
|
|
667
746
|
for (const [key, node] of current_nodes) {
|
|
668
747
|
if (next_keys.has(key)) continue;
|
|
669
748
|
if (keep_head_node_across_spa(node)) continue;
|
|
670
|
-
node.
|
|
749
|
+
if (node.hasAttribute("data-ogygia-region-css")) unlisted.push(node);
|
|
750
|
+
else node.remove();
|
|
671
751
|
}
|
|
672
|
-
|
|
752
|
+
let anchor = current.firstChild;
|
|
673
753
|
for (const node of Array.from(new_head.children)) {
|
|
674
754
|
if (is_dangerous_head_node(node)) continue;
|
|
675
755
|
const key = head_node_key(node);
|
|
676
756
|
const existing = current_nodes.get(key);
|
|
677
757
|
if (key === "STYLE:data-sveltekit") {
|
|
678
|
-
existing?.remove();
|
|
679
758
|
install_head_style(node, current, anchor);
|
|
759
|
+
if (existing) {
|
|
760
|
+
if (existing === anchor) anchor = existing.nextSibling;
|
|
761
|
+
existing.remove();
|
|
762
|
+
}
|
|
680
763
|
continue;
|
|
681
764
|
}
|
|
682
765
|
if (existing) continue;
|
|
683
766
|
if (node.tagName === "STYLE") install_head_style(node, current, anchor);
|
|
684
767
|
else session().claim_page_head(current.appendChild(node.cloneNode(true)));
|
|
685
768
|
}
|
|
769
|
+
return unlisted;
|
|
686
770
|
}
|
|
687
771
|
/**
|
|
688
772
|
* Load the destination page's stylesheets into the live `<head>` and resolve once they have
|
|
689
|
-
*
|
|
773
|
+
* loaded — call this BEFORE the body swap. A freshly appended `<link rel="stylesheet">` loads
|
|
690
774
|
* asynchronously, so swapping the body first shows the new route unstyled (e.g. a content column
|
|
691
775
|
* at full width) until the sheet arrives. That window is invisible on a warm cache but flashes on
|
|
692
776
|
* the first visit after a deploy, when the route CSS isn't cached yet. Preloading here closes it;
|
|
693
777
|
* `merge_head` then dedupes these by key so nothing is added twice. Capped so a stalled sheet
|
|
694
778
|
* can't hang navigation.
|
|
779
|
+
*
|
|
780
|
+
* Each loads INERT: under a media no screen matches (at the fetch priority of a sheet that does),
|
|
781
|
+
* so a sheet that arrived first never styles the page still on screen while the navigation waits
|
|
782
|
+
* for the rest (#267). The commit gives each its own media back (`apply_sheet`). The wait is the
|
|
783
|
+
* navigation's (`attempt`: its listeners and its cap go when it ends); the sheets are the stage's
|
|
784
|
+
* (listed in `sheets`, given back by `discard`): a navigation that ends before its commit takes
|
|
785
|
+
* them out again.
|
|
695
786
|
*/
|
|
696
|
-
function preload_stylesheets(new_head) {
|
|
787
|
+
function preload_stylesheets(new_head, attempt, sheets, discard) {
|
|
697
788
|
const present = /* @__PURE__ */ new Set();
|
|
698
789
|
for (const node of Array.from(document.head.children)) if (node.tagName === "LINK" && node.getAttribute("rel") === "stylesheet") present.add(head_node_key(node));
|
|
699
790
|
const pending = [];
|
|
@@ -705,15 +796,30 @@ function preload_stylesheets(new_head) {
|
|
|
705
796
|
if (present.has(key)) continue;
|
|
706
797
|
present.add(key);
|
|
707
798
|
const link = node.cloneNode(true);
|
|
799
|
+
const sheet = {
|
|
800
|
+
link,
|
|
801
|
+
media: link.getAttribute("media"),
|
|
802
|
+
priority: link.getAttribute("fetchpriority")
|
|
803
|
+
};
|
|
804
|
+
link.setAttribute("media", INERT_MEDIA);
|
|
805
|
+
if (sheet.priority === null) link.setAttribute("fetchpriority", "high");
|
|
708
806
|
pending.push(new Promise((resolve) => {
|
|
709
|
-
|
|
710
|
-
|
|
807
|
+
attempt.on(link, "load", () => resolve(), { once: true });
|
|
808
|
+
attempt.on(link, "error", () => resolve(), { once: true });
|
|
711
809
|
}));
|
|
712
810
|
session().claim_page_head(link);
|
|
713
811
|
document.head.insertBefore(link, document.head.firstChild);
|
|
812
|
+
sheets.push(sheet);
|
|
813
|
+
discard(() => link.remove());
|
|
714
814
|
}
|
|
715
815
|
if (!pending.length) return Promise.resolve();
|
|
716
|
-
return
|
|
816
|
+
return new Promise((resolve) => {
|
|
817
|
+
const cancel = attempt.timeout(resolve, STYLESHEET_WAIT_MS);
|
|
818
|
+
Promise.all(pending).then(() => {
|
|
819
|
+
cancel();
|
|
820
|
+
resolve();
|
|
821
|
+
});
|
|
822
|
+
});
|
|
717
823
|
}
|
|
718
824
|
/**
|
|
719
825
|
* SINGLE-FLIGHT NAVIGATION. Collect the incoming page's deferred, load-timed region calls and stream them as
|
|
@@ -722,34 +828,20 @@ function preload_stylesheets(new_head) {
|
|
|
722
828
|
* irrelevant. Only `when="load"` (or unset) is batched: a region scheduled `visible`/`idle`/media
|
|
723
829
|
* stays lazy and fetches on its own trigger, so dynamic schedules are preserved, not eagerly pulled.
|
|
724
830
|
*/
|
|
725
|
-
/** `
|
|
726
|
-
function batch_regions(doc,
|
|
831
|
+
/** `visit`: the scope of the page's visit, the batch's owner (a navigation called off stops it). */
|
|
832
|
+
function batch_regions(doc, visit) {
|
|
727
833
|
const endpoints = [];
|
|
728
|
-
const batched = /* @__PURE__ */ new Set();
|
|
729
834
|
for (const el of Array.from(doc.querySelectorAll("ogygia-region[render=\"defer\"][endpoint]"))) {
|
|
730
835
|
if ((el.getAttribute("when") || "load") !== "load") continue;
|
|
731
836
|
const ep = el.getAttribute("endpoint");
|
|
732
|
-
if (ep)
|
|
733
|
-
endpoints.push(ep);
|
|
734
|
-
batched.add(ep);
|
|
735
|
-
}
|
|
837
|
+
if (ep) endpoints.push(ep);
|
|
736
838
|
}
|
|
737
839
|
if (!endpoints.length) return;
|
|
738
|
-
const same_url = (href) => {
|
|
739
|
-
try {
|
|
740
|
-
const u = new URL(href, base);
|
|
741
|
-
return u.pathname + u.search;
|
|
742
|
-
} catch {
|
|
743
|
-
return href;
|
|
744
|
-
}
|
|
745
|
-
};
|
|
746
|
-
const batched_urls = new Set(Array.from(batched, same_url));
|
|
747
|
-
for (const link of Array.from(doc.querySelectorAll("link[rel=\"preload\"][as=\"fetch\"]"))) if (batched_urls.has(same_url(link.getAttribute("href") || ""))) link.remove();
|
|
748
840
|
if (DEVTOOLS) emit({
|
|
749
841
|
domain: "nav",
|
|
750
842
|
name: "nav.batch",
|
|
751
843
|
count: endpoints.length
|
|
752
844
|
});
|
|
753
|
-
slots.frames?.stream?.(endpoints);
|
|
845
|
+
slots.frames?.stream?.(endpoints, visit);
|
|
754
846
|
}
|
|
755
|
-
export { DOCUMENT_ACCEPT, apply_late_templates, bust_page_cache, fetch_page, head_node_key, install_head_style, invalidate_all, keep_head_node_across_spa, navigate,
|
|
847
|
+
export { DOCUMENT_ACCEPT, apply_late_templates, bust_page_cache, fetch_page, head_node_key, install_head_style, invalidate_all, keep_head_node_across_spa, navigate, tag_attr };
|