@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
package/dist/runtime/core.js
CHANGED
|
@@ -1,31 +1,43 @@
|
|
|
1
1
|
import { frameAddress } from "../frame.js";
|
|
2
|
+
import { import_again, is_failed_import, on_reload_needed } from "./import-again.js";
|
|
2
3
|
import { kit_hydrates_page } from "./kit-boot.js";
|
|
3
4
|
import { init_shadow_registries, parse_region_html, restore_adopt_sheets, restore_markup } from "./parse-html.js";
|
|
4
5
|
import { mark_region_changed, region_changed, unwatch_region, watch_region } from "./drift-watch.js";
|
|
5
6
|
import { runtime_session } from "./session.js";
|
|
6
|
-
import { RegionAnswerRefused, capability_expired,
|
|
7
|
+
import { RegionAnswerRefused, capability_expired, is_allowed_region_endpoint, island_module_url } from "./region-endpoint-url.js";
|
|
8
|
+
import { fetch_hole } from "./hole-fetch.js";
|
|
7
9
|
import { import_entry, island_entry_of, on_entry_fallback, pin_build_map, pin_region_urls } from "./entry-locations.js";
|
|
10
|
+
import { load_devalue, needs_devalue } from "./devalue-lane.js";
|
|
8
11
|
import { is_awake, is_deferred, is_frozen, ours_on_kit_document, phase2_hydrate_schedule, region_hydrate_schedule, region_schedule } from "./region-attrs.js";
|
|
9
12
|
import { slots } from "./slots.js";
|
|
10
|
-
import {
|
|
13
|
+
import { create_answer, create_hole, create_live, create_paint, create_wake } from "./region-machines.js";
|
|
14
|
+
import "../keep-fallback-marker.js";
|
|
15
|
+
import { download, region_fetch } from "../policies.js";
|
|
16
|
+
import { install_rules, note_mount, note_unmount } from "./rules.js";
|
|
11
17
|
import { PAGE_FACTS_SELECTOR } from "../page-facts-script.js";
|
|
12
18
|
import { link_boot } from "./boot-link.js";
|
|
13
19
|
import { background_start, hydrate_settled, hydrate_started, hydrate_turn, register_region, unregister_region, yield_task } from "./schedule.js";
|
|
14
20
|
import { once_visible } from "./observe.js";
|
|
21
|
+
import { sheet_of, use_sheets } from "./region-sheets.js";
|
|
15
22
|
import { preload_island_graph } from "./island-graph-preload.js";
|
|
16
|
-
import {
|
|
23
|
+
import { LoadSlot, fetch_priority_of, viewport_class } from "./load-scheduler.js";
|
|
17
24
|
import { connected_regions } from "./connected.js";
|
|
18
25
|
import { restore_props_sidecar } from "./sidecar.js";
|
|
19
26
|
import { hole_facts_of } from "./hole-facts.js";
|
|
20
|
-
import { beacon_entry_fallback, beacon_failed, beacon_hole_answered, beacon_hole_failed,
|
|
27
|
+
import { beacon_entry_fallback, beacon_failed, beacon_hole_answered, beacon_hole_failed, beacon_hydrated, beacon_warning, beacon_watch } from "./beacon.js";
|
|
21
28
|
import { on_svelte_warning, set_hydrating, tap_svelte_warnings } from "./svelte-warnings.js";
|
|
22
29
|
import { emit } from "../events/bus.js";
|
|
23
30
|
import { ingest_server_events, install_window_sink } from "../events/sinks.js";
|
|
24
31
|
import { CLIENT_PLUGINS, boot_client_plugins, client_region_mount, client_region_unmount } from "./client-plugins.js";
|
|
32
|
+
import { OgygiaError, line, warn } from "../messages/core.js";
|
|
33
|
+
import { report_restores } from "./restore-report.js";
|
|
34
|
+
import { disclose_version } from "./disclose-version.js";
|
|
35
|
+
import { STALE, backoffAfter, flights, mailbox, scope } from "@ogygia/machine";
|
|
25
36
|
import { FRAMES } from "virtual:ogygia/runtime-flags";
|
|
26
37
|
// src/runtime/core.ts
|
|
27
38
|
const DEVTOOLS = typeof __OGYGIA_DEVTOOLS__ !== "undefined" ? __OGYGIA_DEVTOOLS__ : false;
|
|
28
39
|
const BEACON = typeof __OGYGIA_BEACON__ !== "undefined" ? __OGYGIA_BEACON__ : true;
|
|
40
|
+
const RULES = import.meta.env?.DEV || (typeof __OGYGIA_RULES__ !== "undefined" ? __OGYGIA_RULES__ : false);
|
|
29
41
|
/** Above this many characters an island keeps no server copy (its hydration falls back to
|
|
30
42
|
* Svelte's own recovery on a mismatch) — a bound on memory, not a behaviour anyone tunes. */
|
|
31
43
|
const SSR_SNAPSHOT_MAX = 1 << 19;
|
|
@@ -33,9 +45,6 @@ const SSR_SNAPSHOT_MAX = 1 << 19;
|
|
|
33
45
|
* is still disconnected — nothing has upgraded it — so the island repairs against its true server bytes
|
|
34
46
|
* instead of a DOM a swapped-in foreign runtime may have already mutated. See region_fragment. */
|
|
35
47
|
const PRISTINE_SSR = Symbol("ogygia.pristine-ssr");
|
|
36
|
-
/** A dynamic-import failure of the island ENTRY (network/stale), not a hydration throw. The browser
|
|
37
|
-
* message is `Failed to fetch dynamically imported module: <url>` across engines. */
|
|
38
|
-
const ENTRY_FETCH_FAILED_RE = /failed to fetch dynamically imported module|error loading dynamically imported module/i;
|
|
39
48
|
const CSS_ASSET_HREF_RE = /\.css(\?|$)/;
|
|
40
49
|
/** The island's server markup to hydrate against later (#ssr_html), or `null` when there is
|
|
41
50
|
* nothing worth keeping: no children, or more than SSR_SNAPSHOT_MAX characters. */
|
|
@@ -56,7 +65,6 @@ function fragment_markup(frag) {
|
|
|
56
65
|
frag.append(...Array.from(box.childNodes));
|
|
57
66
|
return html.length > SSR_SNAPSHOT_MAX ? null : html;
|
|
58
67
|
}
|
|
59
|
-
const HOLE_WITHOUT_ADDRESS_WARNING = "[ogygia] deferred region %s was rendered in the browser with no server-minted address, and the document records none for it (a client-side navigation mounted it, or its props differ from the server render) — its fallback stands. A hole is server HTML: render it on the server.";
|
|
60
68
|
/** Read the identity fields devtools events correlate on, off a region element. Cheap — attributes
|
|
61
69
|
* already in hand. Only ever called from behind an `if (DEVTOOLS)` guard, so it costs nothing off. */
|
|
62
70
|
function dt_ids(el) {
|
|
@@ -69,17 +77,47 @@ function dt_ids(el) {
|
|
|
69
77
|
function now_ms() {
|
|
70
78
|
return typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
71
79
|
}
|
|
72
|
-
let core_promise = null;
|
|
73
80
|
let loaded_core = null;
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
81
|
+
const import_core = import_again(() => import("./hydrate-core.js"));
|
|
82
|
+
const hydrate_core = flights({
|
|
83
|
+
name: "hydrate core",
|
|
84
|
+
run: () => yield_task().then(import_core).then((m) => loaded_core = m),
|
|
85
|
+
retry: download
|
|
86
|
+
});
|
|
78
87
|
/** An answer just went in carrying the page facts it rendered with (server/render-page.ts): once
|
|
79
88
|
* islands are awake (the hydrate core loaded), they join the page store NOW, so a KEPT island reading
|
|
80
89
|
* those keys shows the new values; before any island woke, the first hydrate absorbs them. */
|
|
81
90
|
function absorb_page_facts_now() {
|
|
82
|
-
if (runtime_session.
|
|
91
|
+
if (runtime_session.visit.facts.length && loaded_core) load_devalue().then(() => loaded_core?.seed_page_once(), () => {});
|
|
92
|
+
}
|
|
93
|
+
/** devalue, when this wake's reads need it (./devalue-lane.ts): a devalue-lane script in the
|
|
94
|
+
* document (a props sidecar, the page seed) or page facts an answer queued. Started beside the
|
|
95
|
+
* hydrate core, so it adds no hop; a page of JSON-lane scripts never downloads it. */
|
|
96
|
+
function devalue_for_wake() {
|
|
97
|
+
if (runtime_session.visit.facts.length || document_needs_devalue()) return load_devalue();
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The document's `needs_devalue` answer (null: ask). One query of the whole document, not one per
|
|
101
|
+
* wake (that was islands × the page's elements). Asked again only after markup came in: a region
|
|
102
|
+
* connecting (every later render root, a hole answer, a router document, a fragment, carries its
|
|
103
|
+
* sidecars beside its regions), a fetched region's HTML parsed, a router swap. A yes stays yes: devalue
|
|
104
|
+
* is loading from then on.
|
|
105
|
+
*/
|
|
106
|
+
let doc_needs_devalue = null;
|
|
107
|
+
let devalue_swap_armed = false;
|
|
108
|
+
function document_needs_devalue() {
|
|
109
|
+
if (doc_needs_devalue === null) {
|
|
110
|
+
if (!devalue_swap_armed) {
|
|
111
|
+
devalue_swap_armed = true;
|
|
112
|
+
document.addEventListener("og:after-swap", devalue_lane_stale);
|
|
113
|
+
}
|
|
114
|
+
doc_needs_devalue = needs_devalue(document);
|
|
115
|
+
}
|
|
116
|
+
return doc_needs_devalue;
|
|
117
|
+
}
|
|
118
|
+
/** New markup may hold a devalue-lane script: the next wake asks the document again. */
|
|
119
|
+
function devalue_lane_stale() {
|
|
120
|
+
if (doc_needs_devalue === false) doc_needs_devalue = null;
|
|
83
121
|
}
|
|
84
122
|
/** What counts as intent for an ON-DEMAND hole (`render: 'deferred'` + `fetch: 'interaction'`).
|
|
85
123
|
* `pointerover` (not `pointerenter`) so it fires on hover AND bubbles through the boxless
|
|
@@ -92,6 +130,8 @@ const ON_DEMAND_EVENTS = [
|
|
|
92
130
|
"keydown"
|
|
93
131
|
];
|
|
94
132
|
const ON_DEMAND_SELECTOR = "ogygia-region[render=\"defer\"][when=\"interaction\"]";
|
|
133
|
+
/** A visitor's wake with nothing to replay (a hole's on-demand fetch). */
|
|
134
|
+
const NO_REPLAY = Promise.resolve();
|
|
95
135
|
/**
|
|
96
136
|
* ON-DEMAND holes, delegated: ONE set of capture listeners on the document for every armed hole
|
|
97
137
|
* (five listeners per hole used to be five hundred on a page of nav-menus). Intent lands anywhere
|
|
@@ -153,26 +193,36 @@ function dom_ready() {
|
|
|
153
193
|
if (typeof document === "undefined" || document.readyState !== "loading") return Promise.resolve();
|
|
154
194
|
return new Promise((r) => document.addEventListener("DOMContentLoaded", r, { once: true }));
|
|
155
195
|
}
|
|
196
|
+
/** What a region element holds before its first connect and after it left the page: nothing.
|
|
197
|
+
* Anything given to it is let go at once. */
|
|
198
|
+
const NOT_CONNECTED = scope();
|
|
199
|
+
NOT_CONNECTED.close();
|
|
200
|
+
/** Its mailbox then: closed (every post dropped, every wait `STALE`). */
|
|
201
|
+
const CLOSED_BOX = mailbox(NOT_CONNECTED);
|
|
202
|
+
/** A download slot (load-scheduler.ts) asked for by a step whose element then left: its turn still
|
|
203
|
+
* comes, and is handed straight back. */
|
|
204
|
+
const hand_back = (release) => release();
|
|
205
|
+
const nothing = () => {};
|
|
156
206
|
/** Where a live snippet's pending hydrate waits on its host region (region-snippet.ts `hold_host`). */
|
|
157
207
|
const SNIPPET_BOOTS = Symbol.for("ogygia.snippet-boots");
|
|
158
208
|
/** A snippet whose entry never arrives must not hold its host's signal forever. */
|
|
159
209
|
const SNIPPET_BOOT_CAP_MS = 1e4;
|
|
160
210
|
/** Settle the live snippets a just-hydrated region renders — and the snippets those render, which
|
|
161
|
-
* register as they boot — or the cap, whichever is first. A failed boot settles too.
|
|
162
|
-
|
|
211
|
+
* register as they boot — or the cap, whichever is first. A failed boot settles too. The wait is
|
|
212
|
+
* `visit`'s (the wake's): it ends when the region leaves, too. */
|
|
213
|
+
async function snippets_settled(region, visit) {
|
|
163
214
|
const host = region;
|
|
164
|
-
|
|
165
|
-
const cap = new Promise((r) =>
|
|
166
|
-
|
|
167
|
-
cap.then(() => capped = true);
|
|
215
|
+
const wait = visit.child();
|
|
216
|
+
const cap = new Promise((r) => wait.defer(r));
|
|
217
|
+
wait.timeout(() => wait.close(), SNIPPET_BOOT_CAP_MS);
|
|
168
218
|
try {
|
|
169
|
-
for (let batch = host[SNIPPET_BOOTS]; batch?.length && !
|
|
219
|
+
for (let batch = host[SNIPPET_BOOTS]; batch?.length && !wait.closed; batch = host[SNIPPET_BOOTS]) {
|
|
170
220
|
host[SNIPPET_BOOTS] = [];
|
|
171
221
|
await Promise.race([Promise.allSettled(batch), cap]);
|
|
172
222
|
await Promise.resolve();
|
|
173
223
|
}
|
|
174
224
|
} finally {
|
|
175
|
-
|
|
225
|
+
wait.close();
|
|
176
226
|
delete host[SNIPPET_BOOTS];
|
|
177
227
|
}
|
|
178
228
|
}
|
|
@@ -181,16 +231,20 @@ async function snippets_settled(region) {
|
|
|
181
231
|
* inlined `<style data-ogygia-region-css="href">`) it carries into `<head>` (deduped by href). A held / server-picked region's component was never
|
|
182
232
|
* imported by the page, so its scoped CSS is in no stylesheet the page loaded; the region response
|
|
183
233
|
* ships the links and the runtime lifts them to the head — where they load once and stick. (A link
|
|
184
|
-
* left in the body would also fail to load inside a `<template>` batch parcel.)
|
|
234
|
+
* left in the body would also fail to load inside a `<template>` batch parcel.) `sheets`: the head
|
|
235
|
+
* nodes the answer uses, for the region that paints it (./region-sheets.ts `use_sheets`).
|
|
185
236
|
*/
|
|
186
237
|
function region_fragment(html) {
|
|
187
238
|
const frag = parse_region_html(html);
|
|
239
|
+
devalue_lane_stale();
|
|
240
|
+
const { facts: queued, scope: page } = runtime_session.visit;
|
|
188
241
|
for (const facts of frag.querySelectorAll(PAGE_FACTS_SELECTOR)) {
|
|
189
242
|
facts.remove();
|
|
190
|
-
|
|
243
|
+
queued.push(facts.textContent ?? "");
|
|
191
244
|
}
|
|
192
245
|
const links = frag.querySelectorAll("link[data-ogygia-region-css]");
|
|
193
246
|
const pending = [];
|
|
247
|
+
const sheets = [];
|
|
194
248
|
if (links.length && import.meta.env.DEV) {
|
|
195
249
|
const seen = /* @__PURE__ */ new Set();
|
|
196
250
|
for (const link of links) {
|
|
@@ -207,55 +261,70 @@ function region_fragment(html) {
|
|
|
207
261
|
}
|
|
208
262
|
} else if (links.length) {
|
|
209
263
|
const existing = new Map(Array.from(document.querySelectorAll("link[rel=\"stylesheet\"]"), (l) => [l.getAttribute("href"), l]));
|
|
210
|
-
const
|
|
211
|
-
l.addEventListener("load", () => resolve(), { once: true });
|
|
212
|
-
l.addEventListener("error", () => resolve(), { once: true });
|
|
213
|
-
if (l.sheet) resolve();
|
|
214
|
-
});
|
|
264
|
+
const until_settled = (sheet) => sheet.wait("loaded").then(() => void 0);
|
|
215
265
|
for (const link of links) {
|
|
216
266
|
const href = link.getAttribute("href");
|
|
217
267
|
link.remove();
|
|
218
268
|
if (!href) continue;
|
|
219
269
|
const present = existing.get(href);
|
|
220
270
|
if (present) {
|
|
221
|
-
|
|
222
|
-
|
|
271
|
+
const sheet = sheet_of(present);
|
|
272
|
+
if (sheet.now.kind === "loading") pending.push(until_settled(sheet));
|
|
273
|
+
if (sheet.now.kind !== "failed") {
|
|
274
|
+
sheets.push(present);
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
present.remove();
|
|
223
278
|
}
|
|
224
279
|
const clone = document.createElement("link");
|
|
225
280
|
clone.rel = "stylesheet";
|
|
226
281
|
clone.href = href;
|
|
227
282
|
clone.setAttribute("data-ogygia-region-css", "");
|
|
228
|
-
pending.push(
|
|
283
|
+
pending.push(until_settled(sheet_of(clone)));
|
|
229
284
|
existing.set(href, clone);
|
|
285
|
+
sheets.push(clone);
|
|
230
286
|
runtime_session.claim_page_head(clone);
|
|
231
287
|
document.head.appendChild(clone);
|
|
232
288
|
}
|
|
233
289
|
}
|
|
234
290
|
const styles = frag.querySelectorAll("style[data-ogygia-region-css]");
|
|
235
291
|
if (styles.length) {
|
|
236
|
-
const present = /* @__PURE__ */ new
|
|
292
|
+
const present = /* @__PURE__ */ new Map();
|
|
237
293
|
for (const n of document.head.querySelectorAll("[data-ogygia-region-css], link[rel=\"stylesheet\"]")) {
|
|
238
294
|
const id = n.tagName === "STYLE" ? n.getAttribute("data-ogygia-region-css") : n.getAttribute("href");
|
|
239
|
-
if (id) present.
|
|
295
|
+
if (id) present.set(id, n);
|
|
240
296
|
}
|
|
241
297
|
for (const style of styles) {
|
|
242
298
|
const id = style.getAttribute("data-ogygia-region-css") || "";
|
|
243
299
|
style.remove();
|
|
244
|
-
if (!id
|
|
245
|
-
present.
|
|
300
|
+
if (!id) continue;
|
|
301
|
+
const found = present.get(id);
|
|
302
|
+
if (found !== void 0) {
|
|
303
|
+
sheets.push(found);
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
246
306
|
const el = document.createElement("style");
|
|
307
|
+
present.set(id, el);
|
|
308
|
+
sheets.push(el);
|
|
247
309
|
el.setAttribute("data-ogygia-region-css", id);
|
|
248
310
|
el.textContent = style.textContent || "";
|
|
249
311
|
runtime_session.claim_page_head(el);
|
|
250
312
|
document.head.insertBefore(el, document.head.firstChild);
|
|
251
313
|
}
|
|
252
314
|
}
|
|
253
|
-
const ready = pending.length ?
|
|
315
|
+
const ready = pending.length ? page.task(() => new Promise((resolve) => {
|
|
316
|
+
const cancel = page.timeout(resolve, 5e3);
|
|
317
|
+
Promise.all(pending).then(() => {
|
|
318
|
+
cancel();
|
|
319
|
+
resolve();
|
|
320
|
+
});
|
|
321
|
+
})).then(() => void 0) : Promise.resolve();
|
|
254
322
|
restore_markup(frag);
|
|
255
323
|
for (const el of frag.querySelectorAll("ogygia-region")) if (el.getAttribute("entry") && !is_deferred(el) && !is_frozen(el)) el[PRISTINE_SSR] = el.innerHTML;
|
|
256
324
|
return {
|
|
257
325
|
frag,
|
|
258
|
-
ready
|
|
326
|
+
ready,
|
|
327
|
+
sheets
|
|
259
328
|
};
|
|
260
329
|
}
|
|
261
330
|
/** TEST: the pristine server markup {@link region_fragment} stashed on a nested island, if any. */
|
|
@@ -270,71 +339,107 @@ var OgygiaRegion = class OgygiaRegion extends HTMLElement {
|
|
|
270
339
|
* The fetch reads this copy and restores the attribute (lakes, devtools and the router's
|
|
271
340
|
* next-page warm all read the DOM). */
|
|
272
341
|
#minted_endpoint = null;
|
|
273
|
-
/** A fresh capability a renewal answered (#frame_fetcher), adopted once the fetch that got it is
|
|
274
|
-
* applied (#adopt_renewed) — moving addresses mid-fetch would orphan that answer. */
|
|
275
|
-
#renewed_endpoint = null;
|
|
276
342
|
/** THE HYDRATION SOURCE OF TRUTH: this island's server markup as it connected (or, for a
|
|
277
343
|
* hydrating hole, as its answer was swapped in). An island can sleep a long time and other
|
|
278
344
|
* scripts edit the page meanwhile; on wake, hydrate-core hydrates against THIS when the live
|
|
279
345
|
* DOM drifted, instead of letting Svelte re-render the island client-side. Dropped once the
|
|
280
346
|
* island is awake. `null` for a nested region (rides its parent) and above SSR_SNAPSHOT_MAX. */
|
|
281
347
|
#ssr_html = null;
|
|
282
|
-
/**
|
|
283
|
-
|
|
348
|
+
/**
|
|
349
|
+
* THE ELEMENT'S LIFETIME IN THE PAGE: made at its connect, closed at its teardown (a move — out and
|
|
350
|
+
* back in one task — is no teardown, so it keeps it). What the element holds across its machines'
|
|
351
|
+
* visits is this scope's: its sleep in drift-watch.ts, its viewport snapshot for the scheduler,
|
|
352
|
+
* its binding to its frame-store address, the frame that replays a visitor's clicks. What one
|
|
353
|
+
* visit holds stays on that visit (`m.scope`). Closed before the first connect and after the
|
|
354
|
+
* teardown: anything given to it then is let go at once.
|
|
355
|
+
*/
|
|
356
|
+
#life = NOT_CONNECTED;
|
|
357
|
+
/**
|
|
358
|
+
* ITS EVENTS, ONE AT A TIME (@ogygia/machine's `mailbox()`, #322): a connect, a schedule firing, a
|
|
359
|
+
* frame landing, a live tick, its fetch's end and its retry, its wake's steps — each a turn, in the
|
|
360
|
+
* order they came, so no two see each other's half-done step, whichever of its machines they drive.
|
|
361
|
+
* Its lifetime's:
|
|
362
|
+
* the element leaving drops what is queued, and a step waiting gets `STALE` at once. Every callback
|
|
363
|
+
* that comes back to the element later posts here (the box of the lifetime it started in).
|
|
364
|
+
*/
|
|
365
|
+
#box = CLOSED_BOX;
|
|
366
|
+
/** The answer it painted last: what a hole put back on the page shows again when the frame store
|
|
367
|
+
* let it go meanwhile (#336). */
|
|
368
|
+
#shown = null;
|
|
369
|
+
/** Frames its binding was handed (a binding that replays one counts it at once). */
|
|
370
|
+
#frames_in = 0;
|
|
371
|
+
/** A live region's last tick: what it goes on with when it is put back on the page (#337). */
|
|
372
|
+
#live_last = null;
|
|
373
|
+
/** While it sleeps with a copy: counted in drift-watch.ts's sleeping islands, until it wakes (or
|
|
374
|
+
* leaves the page). */
|
|
375
|
+
#asleep = null;
|
|
376
|
+
get #watching() {
|
|
377
|
+
return this.#asleep !== null && !this.#asleep.closed;
|
|
378
|
+
}
|
|
284
379
|
#watch() {
|
|
285
380
|
if (this.#watching) return;
|
|
286
|
-
this.#
|
|
381
|
+
this.#asleep = this.#life.child();
|
|
287
382
|
watch_region();
|
|
383
|
+
this.#asleep.defer(unwatch_region);
|
|
288
384
|
}
|
|
289
385
|
#unwatch() {
|
|
290
|
-
|
|
291
|
-
this.#
|
|
292
|
-
unwatch_region();
|
|
386
|
+
this.#asleep?.close();
|
|
387
|
+
this.#asleep = null;
|
|
293
388
|
}
|
|
294
|
-
/**
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
*
|
|
299
|
-
#
|
|
300
|
-
/**
|
|
301
|
-
#
|
|
302
|
-
/** Bounded automatic retries after a failed defer/SWR fetch. */
|
|
303
|
-
#fetch_attempts = 0;
|
|
389
|
+
/** A deferred region's own fetch of its answer: armed, fetching, waiting to retry, given up
|
|
390
|
+
* (./region-machines.ts). Its `armed` state owns the `when` and `prefetch` listeners. */
|
|
391
|
+
#hole = create_hole();
|
|
392
|
+
/** What the hole's answers put on screen: nothing yet, an answer, or the kept fallback. Any
|
|
393
|
+
* answer counts, whoever fetched it (a twin, a batch, a write at its address). */
|
|
394
|
+
#answer = create_answer();
|
|
395
|
+
/** The island's hydration: cold, armed (its schedule's listeners), waking, awake, failed. */
|
|
396
|
+
#wake = create_wake();
|
|
304
397
|
/** when this hole's first fetch started (devtools + profiler: how long its fallback showed) */
|
|
305
398
|
#fetch_started = 0;
|
|
306
|
-
/** Frame-store address (endpoint call) this region is
|
|
399
|
+
/** Frame-store address (endpoint call) this region is bound to. */
|
|
307
400
|
#frame_address = null;
|
|
308
|
-
/**
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
#
|
|
401
|
+
/** Its subscription at that address (a defer region binds to its address), a child of its
|
|
402
|
+
* lifetime in the page: closing it unsubscribes. */
|
|
403
|
+
#binding = null;
|
|
404
|
+
/** A visitor's gesture woke this region (`interaction`, or the capture on a scheduled island): its
|
|
405
|
+
* downloads are the `user` class, and this is what replays their input (the capture's replay half,
|
|
406
|
+
* loading) — its island goes live once it is in, so the input lands AT wake (#260). */
|
|
407
|
+
#visitor = null;
|
|
315
408
|
/** The hydrated island (hydrate core handle): dispose, and for a kept island, props push. */
|
|
316
409
|
#app = null;
|
|
317
|
-
/** Stops the shared `visible` observation (set while armed, cold). */
|
|
318
|
-
#stop_visible = null;
|
|
319
|
-
/** Removes the `wake="interaction"` wake listeners (set while armed, cold). */
|
|
320
|
-
#disarm_interaction = null;
|
|
321
|
-
#mql = null;
|
|
322
|
-
/** The hole answered `keepFallback()`: the page's fallback stands, no phase-2 wake. */
|
|
323
|
-
#kept = false;
|
|
324
|
-
/** Abort in-flight region HTML fetch on disconnect (P-ABORT). */
|
|
325
|
-
#fetch_abort = null;
|
|
326
|
-
/** Cancel idle schedule when disconnected. */
|
|
327
|
-
#idle_handle = null;
|
|
328
410
|
/** Disconnected, teardown waiting a microtask: a reconnect before it is a move. */
|
|
329
411
|
#leaving = false;
|
|
330
412
|
/** Live region (`<ogygia-region live>`): driven imperatively by Region.svelte's applyLive. */
|
|
331
413
|
#live_ready = false;
|
|
332
|
-
|
|
333
|
-
#
|
|
414
|
+
/** Its mode, tick by tick: static (subscribed to the store), hydrating, live (./region-machines.ts). */
|
|
415
|
+
#live = create_live();
|
|
416
|
+
/** The frame that paints next: each frame waits for its sheets in a visit of its own, and only
|
|
417
|
+
* the newest paints (./region-machines.ts). */
|
|
418
|
+
#paint = create_paint();
|
|
419
|
+
/** The props of the latest same-module tick that came while its island hydrated: pushed once live. */
|
|
420
|
+
#live_props = null;
|
|
421
|
+
/** A static live region's subscription to its address, while it is static. */
|
|
422
|
+
#live_unsub = null;
|
|
334
423
|
#hydrate_run = null;
|
|
335
|
-
#fire_hydrate = () =>
|
|
336
|
-
|
|
337
|
-
|
|
424
|
+
#fire_hydrate = () => {
|
|
425
|
+
const held = this.#hydrate_run;
|
|
426
|
+
if (held && (held.visit === this.#wake.now || this.#wake.now.kind === "waking")) return held.run;
|
|
427
|
+
const run = this.#box.ask((turn) => this.#hydrate(turn)).then(nothing);
|
|
428
|
+
const entry = {
|
|
429
|
+
visit: this.#wake.now,
|
|
430
|
+
run
|
|
431
|
+
};
|
|
432
|
+
this.#hydrate_run = entry;
|
|
433
|
+
const ran = () => this.#hydrate_ran(entry);
|
|
434
|
+
run.then(ran, ran);
|
|
435
|
+
return run;
|
|
436
|
+
};
|
|
437
|
+
/** A hydration run ended: forget it, unless a newer one took its place. */
|
|
438
|
+
#hydrate_ran(entry) {
|
|
439
|
+
if (this.#hydrate_run === entry) this.#hydrate_run = null;
|
|
440
|
+
}
|
|
441
|
+
#fire_server = () => this.#box.post((turn) => this.#server(turn));
|
|
442
|
+
#fire_prefetch = () => this.#box.post(() => this.#prefetch_html());
|
|
338
443
|
/**
|
|
339
444
|
* CONTINUITY: this persisted island is relocating onto `next` (the incoming page's SSR region).
|
|
340
445
|
* Push the new page's props into the live app so a `persist`ed component reflects the new route
|
|
@@ -349,9 +454,18 @@ var OgygiaRegion = class OgygiaRegion extends HTMLElement {
|
|
|
349
454
|
} catch {}
|
|
350
455
|
}
|
|
351
456
|
connectedCallback() {
|
|
457
|
+
if (this.#life.closed && this.isConnected) {
|
|
458
|
+
this.#life = scope();
|
|
459
|
+
this.#box = mailbox(this.#life);
|
|
460
|
+
}
|
|
352
461
|
connected_regions.add(this);
|
|
462
|
+
devalue_lane_stale();
|
|
353
463
|
pin_region_urls(this);
|
|
354
|
-
|
|
464
|
+
this.#box.post((turn) => this.#connect(turn));
|
|
465
|
+
}
|
|
466
|
+
/** The connect, in its turn: where the region stands, and what it arms. */
|
|
467
|
+
#connect(turn) {
|
|
468
|
+
if (this.hasAttribute("live")) return this.#live_put_back(turn);
|
|
355
469
|
if (FRAMES && this.#minted_endpoint === null && is_deferred(this)) {
|
|
356
470
|
const minted = this.getAttribute("endpoint");
|
|
357
471
|
if (minted) this.#minted_endpoint = minted;
|
|
@@ -363,7 +477,7 @@ var OgygiaRegion = class OgygiaRegion extends HTMLElement {
|
|
|
363
477
|
pin_region_urls(this);
|
|
364
478
|
this.#minted_endpoint = this.getAttribute("endpoint");
|
|
365
479
|
if (facts.sidecar) restore_props_sidecar(this, facts.sidecar.cloneNode(true));
|
|
366
|
-
} else if (import.meta.env.DEV && identity)
|
|
480
|
+
} else if (import.meta.env.DEV && identity) warn("hole_without_address", { region: this.getAttribute("entry") || identity });
|
|
367
481
|
}
|
|
368
482
|
}
|
|
369
483
|
if (is_frozen(this) && slots.lakes.on_frozen_connect(this, this.#lake_arm())) return;
|
|
@@ -383,7 +497,10 @@ var OgygiaRegion = class OgygiaRegion extends HTMLElement {
|
|
|
383
497
|
deferred: false,
|
|
384
498
|
nested: true
|
|
385
499
|
});
|
|
386
|
-
if (import.meta.env.DEV)
|
|
500
|
+
if (import.meta.env.DEV) warn("nested_region_rides_parent", {
|
|
501
|
+
entry: this.getAttribute("entry") || "",
|
|
502
|
+
wake: this.getAttribute("wake") || "load"
|
|
503
|
+
});
|
|
387
504
|
return;
|
|
388
505
|
}
|
|
389
506
|
const deferred = is_deferred(this);
|
|
@@ -395,6 +512,10 @@ var OgygiaRegion = class OgygiaRegion extends HTMLElement {
|
|
|
395
512
|
if (this.#ssr_html !== null) this.#watch();
|
|
396
513
|
if (slots.lakes.wait_for_boundary(this, boundary)) return;
|
|
397
514
|
this.#scheduled = true;
|
|
515
|
+
if (deferred && this.#answer.now.kind !== "none") {
|
|
516
|
+
this.#put_back();
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
398
519
|
const when = region_schedule(this);
|
|
399
520
|
if (DEVTOOLS) {
|
|
400
521
|
emit({
|
|
@@ -412,13 +533,44 @@ var OgygiaRegion = class OgygiaRegion extends HTMLElement {
|
|
|
412
533
|
when
|
|
413
534
|
});
|
|
414
535
|
}
|
|
415
|
-
if (!deferred) register_region(this);
|
|
416
|
-
if (deferred)
|
|
417
|
-
|
|
418
|
-
|
|
536
|
+
if (!deferred) register_region(this, this.#life);
|
|
537
|
+
if (!deferred) {
|
|
538
|
+
if (this.#wake.now.kind === "cold") this.#wake.go("armed");
|
|
539
|
+
this.#arm(when, this.#fire_hydrate);
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
if (!this.#hole_armed()) return;
|
|
543
|
+
this.#arm(when, this.#fire_server);
|
|
544
|
+
const prefetch = this.getAttribute("prefetch");
|
|
419
545
|
if (prefetch && prefetch !== when && prefetch !== "interaction") this.#arm(prefetch, this.#fire_prefetch, this.getAttribute("margin") || void 0);
|
|
420
546
|
}
|
|
421
547
|
/**
|
|
548
|
+
* Put the hole's fetch in `armed`, the state its schedules' listeners belong to: from idle, from a
|
|
549
|
+
* retry it waited for, or after it gave up (a lake's or a connect's new try). A fetch in flight
|
|
550
|
+
* arms nothing — it is already doing what a schedule would start.
|
|
551
|
+
*/
|
|
552
|
+
#hole_armed() {
|
|
553
|
+
const s = this.#hole.now;
|
|
554
|
+
if (s.kind === "armed") return true;
|
|
555
|
+
if (s.kind === "fetching") return false;
|
|
556
|
+
return this.#hole.go("armed", { attempts: s.kind === "retrying" ? s.attempts : 0 });
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* A hole that had its answer is back on the page (#336): it binds to its address again, and the
|
|
560
|
+
* store hands it the frame there, as it does any late binder — its island wakes on it, whoever's
|
|
561
|
+
* the answer. A minute off the page with nothing bound, the store let the frame go: the hole's own
|
|
562
|
+
* last answer is painted again (its island, unmounted as it left, took its markup with it). Still
|
|
563
|
+
* bound (a move, a failed refresh), it has nothing to catch up on.
|
|
564
|
+
*/
|
|
565
|
+
#put_back() {
|
|
566
|
+
const endpoint = this.#endpoint();
|
|
567
|
+
if (!endpoint || this.#bound()) return;
|
|
568
|
+
const handed = this.#frames_in;
|
|
569
|
+
this.#bind(endpoint);
|
|
570
|
+
const shown = this.#shown;
|
|
571
|
+
if (this.#frames_in === handed && shown !== null && this.#answer.now.kind === "answered") this.#box.post((turn) => this.#apply(shown, turn));
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
422
574
|
* Warm this hole's HTML into the frame store ahead of its `when` — the `prefetch` schedule. No
|
|
423
575
|
* subscription is taken, so nothing is applied now; the later wake (`#server`) subscribes, the
|
|
424
576
|
* store replays the warm frame at once, and its `ensure` joins instead of fetching. A failed
|
|
@@ -427,7 +579,7 @@ var OgygiaRegion = class OgygiaRegion extends HTMLElement {
|
|
|
427
579
|
*/
|
|
428
580
|
#prefetch_html() {
|
|
429
581
|
if (!FRAMES) return;
|
|
430
|
-
if (this.#
|
|
582
|
+
if (this.#answer.now.kind !== "none" || this.#hole.now.kind === "fetching" || !this.isConnected) return;
|
|
431
583
|
const endpoint = this.#endpoint();
|
|
432
584
|
if (!endpoint || !is_allowed_region_endpoint(endpoint)) return;
|
|
433
585
|
const address = frameAddress(endpoint);
|
|
@@ -447,7 +599,7 @@ var OgygiaRegion = class OgygiaRegion extends HTMLElement {
|
|
|
447
599
|
#frame_fetcher(endpoint, revalidate, cls) {
|
|
448
600
|
const class_now = typeof cls === "function" ? cls : () => cls;
|
|
449
601
|
return async (signal) => {
|
|
450
|
-
const release = await
|
|
602
|
+
const release = await new LoadSlot({
|
|
451
603
|
kind: "content",
|
|
452
604
|
cls,
|
|
453
605
|
label: endpoint
|
|
@@ -460,45 +612,41 @@ var OgygiaRegion = class OgygiaRegion extends HTMLElement {
|
|
|
460
612
|
};
|
|
461
613
|
}
|
|
462
614
|
#fetch_region(endpoint, revalidate, priority, signal) {
|
|
463
|
-
return runtime_session.server_gate.run(
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
cache: revalidate ? "no-store" : "default",
|
|
615
|
+
return runtime_session.server_gate.run(() => {
|
|
616
|
+
const address = frameAddress(endpoint);
|
|
617
|
+
return fetch_hole(endpoint, {
|
|
618
|
+
revalidate,
|
|
468
619
|
priority,
|
|
469
|
-
signal
|
|
620
|
+
signal,
|
|
621
|
+
renewed: (fresh) => slots.frames?.renew(address, fresh)
|
|
470
622
|
});
|
|
471
|
-
if (res.status === 403 && capability_expired(endpoint)) {
|
|
472
|
-
res = await fetch(renewal_url(endpoint), {
|
|
473
|
-
credentials: "same-origin",
|
|
474
|
-
cache: "no-store",
|
|
475
|
-
signal
|
|
476
|
-
});
|
|
477
|
-
const fresh = res.ok ? res.headers.get("x-ogygia-capability") : null;
|
|
478
|
-
if (fresh && is_allowed_region_endpoint(fresh)) this.#renewed_endpoint = fresh;
|
|
479
|
-
}
|
|
480
|
-
if (!is_same_origin_response(res)) throw new Error("cross-origin redirect");
|
|
481
|
-
if (is_redirected_answer(res)) throw new RegionAnswerRefused("redirected", res.url);
|
|
482
|
-
if (res.status === 403) throw new RegionAnswerRefused("forbidden", res.url || endpoint, deny_of(res, endpoint));
|
|
483
|
-
if (!res.ok) throw new Error("status " + res.status);
|
|
484
|
-
if (res.status === 204) return KEEP_FALLBACK_HTML;
|
|
485
|
-
const text = await res.text();
|
|
486
|
-
if (is_document_answer(text)) throw new RegionAnswerRefused("document", res.url);
|
|
487
|
-
return text;
|
|
488
623
|
});
|
|
489
624
|
}
|
|
490
|
-
/** The schedule hooks a frozen region (lake) drives its revalidation through
|
|
625
|
+
/** The schedule hooks a frozen region (lake) drives its revalidation through: their listeners
|
|
626
|
+
* belong to the hole's `armed` state, like a hole's own schedule's. */
|
|
491
627
|
#lake_arm() {
|
|
492
628
|
return {
|
|
493
|
-
idle: (fire) =>
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
629
|
+
idle: (fire) => {
|
|
630
|
+
if (this.#hole_armed()) this.#on_idle(fire, this.#hole);
|
|
631
|
+
},
|
|
632
|
+
visible: (fire, margin) => {
|
|
633
|
+
if (this.#hole_armed()) this.#on_visible(fire, margin, this.#hole);
|
|
634
|
+
},
|
|
635
|
+
media: (when, fire) => {
|
|
636
|
+
if (this.#hole_armed()) this.#on_media(when, fire, this.#hole);
|
|
637
|
+
},
|
|
638
|
+
fetch_revalidate: () => this.#box.post((turn) => this.#fetch_html(turn, true)),
|
|
497
639
|
wake_children: () => this.#wake_waiting_regions()
|
|
498
640
|
};
|
|
499
641
|
}
|
|
500
|
-
/**
|
|
642
|
+
/**
|
|
643
|
+
* Arm idle / visible / load / interaction / media for a schedule callback. The listeners belong to
|
|
644
|
+
* the `armed` state of the machine the callback drives — the island's wake, or the hole's fetch —
|
|
645
|
+
* which must be in it: leaving it (the schedule fired, an answer came another way, the region left
|
|
646
|
+
* the page) drops every one, however many schedules armed.
|
|
647
|
+
*/
|
|
501
648
|
#arm(when, fire, visible_margin) {
|
|
649
|
+
const owner = fire === this.#fire_hydrate ? this.#wake : this.#hole;
|
|
502
650
|
if (fire === this.#fire_hydrate && when !== "interaction") this.#catch_early_input(fire);
|
|
503
651
|
if (DEVTOOLS) {
|
|
504
652
|
const raw = fire;
|
|
@@ -512,18 +660,20 @@ var OgygiaRegion = class OgygiaRegion extends HTMLElement {
|
|
|
512
660
|
return raw();
|
|
513
661
|
};
|
|
514
662
|
}
|
|
515
|
-
const
|
|
516
|
-
|
|
517
|
-
|
|
663
|
+
const armed = owner.scope;
|
|
664
|
+
const start = () => background_start(() => void (armed.closed || fire()));
|
|
665
|
+
if (when === "idle") this.#on_idle(fire, owner);
|
|
666
|
+
else if (when === "visible") this.#on_visible(start, visible_margin, owner);
|
|
518
667
|
else if (when === "load") start();
|
|
519
668
|
else if (when === "interaction") {
|
|
520
|
-
|
|
521
|
-
this
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
669
|
+
if (owner === this.#hole) {
|
|
670
|
+
arm_on_demand(this, () => {
|
|
671
|
+
this.#visitor = NO_REPLAY;
|
|
672
|
+
return fire();
|
|
673
|
+
});
|
|
674
|
+
owner.scope.defer(() => on_demand_armed.delete(this));
|
|
675
|
+
} else this.#on_interaction(fire);
|
|
676
|
+
} else this.#on_media(when, start, owner);
|
|
527
677
|
}
|
|
528
678
|
/**
|
|
529
679
|
* `wake="interaction"`: sleep until a pointer/key/focus/click lands inside the region, then
|
|
@@ -543,8 +693,7 @@ var OgygiaRegion = class OgygiaRegion extends HTMLElement {
|
|
|
543
693
|
fire();
|
|
544
694
|
return;
|
|
545
695
|
}
|
|
546
|
-
|
|
547
|
-
this.#disarm_interaction = typeof disarm === "function" ? disarm : null;
|
|
696
|
+
this.#hold_capture(arm(this, this.#by_visitor(fire), this.#life));
|
|
548
697
|
}
|
|
549
698
|
/**
|
|
550
699
|
* A scheduled island (`load` / `idle` / `visible` / media) also catches input until it is live — the
|
|
@@ -555,28 +704,81 @@ var OgygiaRegion = class OgygiaRegion extends HTMLElement {
|
|
|
555
704
|
#catch_early_input(fire) {
|
|
556
705
|
const arm = slots.interaction;
|
|
557
706
|
if (!arm || this.#app || kit_hydrates_page() && !ours_on_kit_document(this)) return;
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
707
|
+
this.#hold_capture(arm(this, this.#by_visitor(fire), this.#life));
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* The wake as the interaction capture awaits it: a visitor's gesture (its downloads are the `user`
|
|
711
|
+
* class from here on), and a promise that REJECTS when the island failed to wake — the capture then
|
|
712
|
+
* replays its clicks onto the server markup and says so (interaction.ts). Only the capture gets this:
|
|
713
|
+
* a schedule's own wake has nobody to tell.
|
|
714
|
+
*/
|
|
715
|
+
#by_visitor(fire) {
|
|
716
|
+
return (replayed) => {
|
|
717
|
+
this.#visitor = replayed;
|
|
718
|
+
return Promise.resolve(fire()).then(() => {
|
|
719
|
+
const s = this.#wake.now;
|
|
720
|
+
if (s.kind === "failed") throw s.error;
|
|
721
|
+
});
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* The capture outlives the schedule it was armed with: it stays while the island wakes (a click
|
|
726
|
+
* meanwhile is queued, and replayed once the island is live), so leaving `armed` for `waking` hands
|
|
727
|
+
* its disarm on to `waking`, and leaving that (awake, failed, or the region gone) disarms it. Gone
|
|
728
|
+
* (out of the document), its visit is over: the clicks it queued go with it.
|
|
729
|
+
*/
|
|
730
|
+
#hold_capture(disarm) {
|
|
731
|
+
if (typeof disarm !== "function") return;
|
|
732
|
+
const wake = this.#wake;
|
|
733
|
+
const hold = () => {
|
|
734
|
+
if (wake.now.kind === "waking") wake.scope.defer(hold);
|
|
735
|
+
else disarm(!this.isConnected);
|
|
736
|
+
};
|
|
737
|
+
wake.scope.defer(hold);
|
|
563
738
|
}
|
|
564
739
|
/**
|
|
565
|
-
* Move this hole onto the fresh capability a renewal
|
|
566
|
-
*
|
|
567
|
-
*
|
|
740
|
+
* Move this hole onto the fresh capability a renewal at its address answered (frame-store
|
|
741
|
+
* `renew`, whichever copy's fetch got it): the attribute (every DOM reader — lakes, devtools, the
|
|
742
|
+
* router's warm — agrees), the minted copy, and, should the address differ, the frame-store
|
|
743
|
+
* address its later wakes and revalidates fetch and apply at. Called once an answer landed — moving
|
|
744
|
+
* addresses mid-fetch would orphan that answer. A copy whose own capability has not expired keeps
|
|
745
|
+
* it (it can be newer than the renewal: a page rendered since).
|
|
568
746
|
*/
|
|
569
747
|
#adopt_renewed() {
|
|
570
|
-
const
|
|
571
|
-
|
|
572
|
-
|
|
748
|
+
const address = this.#frame_address;
|
|
749
|
+
const fresh = address ? slots.frames?.renewal(address) : null;
|
|
750
|
+
const current = this.getAttribute("endpoint");
|
|
751
|
+
if (!fresh || fresh === current || current && !capability_expired(current)) return;
|
|
573
752
|
this.#minted_endpoint = fresh;
|
|
574
753
|
this.setAttribute("endpoint", fresh);
|
|
575
|
-
if (this.#
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
754
|
+
if (this.#bound()) this.#bind(fresh);
|
|
755
|
+
}
|
|
756
|
+
/**
|
|
757
|
+
* Bind to the store at `endpoint`'s address: this region applies whatever frame lands there (#apply)
|
|
758
|
+
* — from its own fetch, a twin's, a navigation batch stream, or a mutation. subscribe() replays
|
|
759
|
+
* current content immediately, so a late-mounting twin catches up free. A re-minted endpoint (new
|
|
760
|
+
* props, new page facts) moves the binding to its new address. Returns the address.
|
|
761
|
+
*/
|
|
762
|
+
#bind(endpoint) {
|
|
763
|
+
const address = frameAddress(endpoint);
|
|
764
|
+
if (this.#bound() && this.#frame_address === address) return address;
|
|
765
|
+
this.#binding?.close();
|
|
766
|
+
const binding = this.#binding = this.#life.child();
|
|
767
|
+
this.#frame_address = address;
|
|
768
|
+
binding.defer(() => {
|
|
769
|
+
if (this.#binding === binding) this.#frame_address = null;
|
|
770
|
+
});
|
|
771
|
+
const box = this.#box;
|
|
772
|
+
const off = slots.frames?.subscribe(address, (f) => {
|
|
773
|
+
this.#frames_in++;
|
|
774
|
+
box.post((turn) => this.#apply(f.html, turn));
|
|
775
|
+
});
|
|
776
|
+
if (off) binding.defer(off);
|
|
777
|
+
return address;
|
|
778
|
+
}
|
|
779
|
+
/** Bound to a frame-store address now. */
|
|
780
|
+
#bound() {
|
|
781
|
+
return this.#binding !== null && !this.#binding.closed;
|
|
580
782
|
}
|
|
581
783
|
/** The hole's endpoint: the attribute, or the server-minted copy when Kit's hydration wiped it
|
|
582
784
|
* (restored on the element so every other DOM reader agrees). */
|
|
@@ -588,28 +790,53 @@ var OgygiaRegion = class OgygiaRegion extends HTMLElement {
|
|
|
588
790
|
return this.#minted_endpoint;
|
|
589
791
|
}
|
|
590
792
|
/**
|
|
591
|
-
* Deferred hole:
|
|
592
|
-
* island
|
|
793
|
+
* Deferred hole, its `when` fired: bind to its address and get its HTML. The swap — and, for a
|
|
794
|
+
* deferred client island, the phase-2 wake — happen where every answer lands (#apply), whoever
|
|
795
|
+
* fetched it.
|
|
593
796
|
*/
|
|
594
|
-
|
|
797
|
+
#server(turn) {
|
|
595
798
|
if (!FRAMES) return;
|
|
596
799
|
const endpoint = this.#endpoint();
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
800
|
+
const handed = this.#frames_in;
|
|
801
|
+
if (endpoint) this.#bind(endpoint);
|
|
802
|
+
const run = this.#frames_in === handed ? this.#fetch_html(turn) : void 0;
|
|
803
|
+
if (this.#hole.now.kind === "armed") this.#hole.go("idle");
|
|
804
|
+
return run;
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* A deferred client island's answer is in: arm its phase-2 wake — coalescing a schedule that
|
|
808
|
+
* matches the hole's own `when` to an immediate load. Once: an island already armed, waking, awake
|
|
809
|
+
* or failed is not armed again (a refresh morphs a live island in place).
|
|
810
|
+
*/
|
|
811
|
+
#arm_phase2() {
|
|
812
|
+
if (this.#wake.now.kind !== "cold" || this.#app || !this.isConnected) return;
|
|
605
813
|
const hydrate = region_hydrate_schedule(this);
|
|
606
814
|
if (!hydrate) return;
|
|
607
815
|
const defer_when = this.getAttribute("when") || "load";
|
|
608
816
|
const phase2 = phase2_hydrate_schedule(defer_when, hydrate);
|
|
609
817
|
const margin = phase2 === "visible" ? this.getAttribute("hydrate-margin") || this.getAttribute("margin") || void 0 : void 0;
|
|
610
|
-
register_region(this);
|
|
818
|
+
register_region(this, this.#life);
|
|
819
|
+
this.#wake.go("armed");
|
|
611
820
|
this.#arm(phase2, this.#fire_hydrate, margin);
|
|
612
821
|
}
|
|
822
|
+
/** An answer landed: the hole's own fetch has nothing left to wait for — a schedule still armed
|
|
823
|
+
* (a retry's) or a retry it waited for is called off. A fetch in flight runs to its end. */
|
|
824
|
+
#answered(kind) {
|
|
825
|
+
if (this.#answer.now.kind !== kind) this.#answer.go(kind);
|
|
826
|
+
const hole = this.#hole.now.kind;
|
|
827
|
+
if (hole === "armed" || hole === "retrying" || hole === "failed") this.#hole.go("idle");
|
|
828
|
+
}
|
|
829
|
+
/** A frame landed: it starts its paint visit (a frame still waiting for its sheets is older, and
|
|
830
|
+
* its visit is over). Returns the visit, to check after the wait. */
|
|
831
|
+
#paint_next() {
|
|
832
|
+
this.#paint.go("painting");
|
|
833
|
+
return this.#paint.now;
|
|
834
|
+
}
|
|
835
|
+
/** The frame of visit `painting` paints now: nothing is waiting any more, unless a newer frame
|
|
836
|
+
* already started (a listener of this paint wrote one). */
|
|
837
|
+
#painted(painting) {
|
|
838
|
+
if (this.#paint.now === painting) this.#paint.go("idle");
|
|
839
|
+
}
|
|
613
840
|
/**
|
|
614
841
|
* THE single apply path. A frame's HTML lands here from any source (own fetch, stream parcel,
|
|
615
842
|
* navigation batch, mutation). First arrival hydrates the hole; a later arrival at a newer
|
|
@@ -617,22 +844,25 @@ var OgygiaRegion = class OgygiaRegion extends HTMLElement {
|
|
|
617
844
|
* offline before custom elements connect, then swap, mark, event. HOLE-TRUST: the HTML is our own
|
|
618
845
|
* signed same-origin SSR.
|
|
619
846
|
*/
|
|
620
|
-
async #apply(html) {
|
|
847
|
+
async #apply(html, turn) {
|
|
621
848
|
if (!this.isConnected) return;
|
|
849
|
+
const painting = this.#paint_next();
|
|
622
850
|
if (html === "<!--ogygia:keep-fallback-->") {
|
|
623
|
-
this.#
|
|
624
|
-
this
|
|
625
|
-
this.#kept
|
|
851
|
+
this.#painted(painting);
|
|
852
|
+
use_sheets(this, []);
|
|
853
|
+
this.#answered("kept");
|
|
626
854
|
if (!is_awake(this)) this.setAttribute("data-hydrated", "");
|
|
627
855
|
this.setAttribute("data-og-kept", "");
|
|
856
|
+
this.#adopt_renewed();
|
|
628
857
|
this.dispatchEvent(new CustomEvent("ogygia:server", { bubbles: true }));
|
|
629
858
|
return;
|
|
630
859
|
}
|
|
631
|
-
const revalidate = this
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
860
|
+
const revalidate = is_frozen(this) || this.#answer.now.kind === "answered";
|
|
861
|
+
const { frag, ready, sheets } = region_fragment(html);
|
|
862
|
+
if (await turn.wait(ready) === STALE || this.#paint.now !== painting) return;
|
|
863
|
+
this.#painted(painting);
|
|
864
|
+
use_sheets(this, sheets);
|
|
865
|
+
this.#shown = html;
|
|
636
866
|
slots.lakes.settle_in(frag);
|
|
637
867
|
slots.lakes.mark_frozen_settled(this);
|
|
638
868
|
const morph = slots.morph;
|
|
@@ -645,10 +875,11 @@ var OgygiaRegion = class OgygiaRegion extends HTMLElement {
|
|
|
645
875
|
restore_adopt_sheets();
|
|
646
876
|
init_shadow_registries(this);
|
|
647
877
|
absorb_page_facts_now();
|
|
648
|
-
this.#
|
|
878
|
+
this.#answered("answered");
|
|
879
|
+
this.removeAttribute("data-og-kept");
|
|
649
880
|
if (revalidate) this.setAttribute("data-revalidated", "");
|
|
650
881
|
else if (!is_awake(this)) this.setAttribute("data-hydrated", "");
|
|
651
|
-
slots.lakes.after_html_swap(this, { revalidate });
|
|
882
|
+
slots.lakes.after_html_swap(this, { revalidate }, () => this.#wake_waiting_regions());
|
|
652
883
|
if (BEACON && !revalidate && this.#fetch_started) {
|
|
653
884
|
const ep = this.getAttribute("endpoint");
|
|
654
885
|
beacon_hole_answered(this, this.#fetch_started, ep ? slots.frames?.batch_times?.(ep) : void 0);
|
|
@@ -662,7 +893,9 @@ var OgygiaRegion = class OgygiaRegion extends HTMLElement {
|
|
|
662
893
|
revalidate,
|
|
663
894
|
...!revalidate && this.#fetch_started ? { wait_ms: Math.round(performance.now() - this.#fetch_started) } : {}
|
|
664
895
|
});
|
|
896
|
+
this.#adopt_renewed();
|
|
665
897
|
this.dispatchEvent(new CustomEvent("ogygia:server", { bubbles: true }));
|
|
898
|
+
this.#arm_phase2();
|
|
666
899
|
}
|
|
667
900
|
/**
|
|
668
901
|
* Store applicator for a STATIC live/held region (`<ogygia-region live>` with no interactive
|
|
@@ -671,7 +904,7 @@ var OgygiaRegion = class OgygiaRegion extends HTMLElement {
|
|
|
671
904
|
* survives — the breathing update live partials rely on). Interactive live regions never reach
|
|
672
905
|
* here: they keep the imperative keep-alive path in {@link applyLive}.
|
|
673
906
|
*/
|
|
674
|
-
async #morph_live(html) {
|
|
907
|
+
async #morph_live(html, turn) {
|
|
675
908
|
if (!this.isConnected) return;
|
|
676
909
|
if (!this.#live_ready && this.hasAttribute("data-og-baked")) {
|
|
677
910
|
this.#live_ready = true;
|
|
@@ -679,9 +912,11 @@ var OgygiaRegion = class OgygiaRegion extends HTMLElement {
|
|
|
679
912
|
this.dispatchEvent(new CustomEvent("ogygia:live", { bubbles: true }));
|
|
680
913
|
return;
|
|
681
914
|
}
|
|
682
|
-
const
|
|
683
|
-
|
|
684
|
-
if (
|
|
915
|
+
const painting = this.#paint_next();
|
|
916
|
+
const { frag, ready, sheets } = region_fragment(html);
|
|
917
|
+
if (await turn.wait(ready) === STALE || this.#paint.now !== painting) return;
|
|
918
|
+
this.#painted(painting);
|
|
919
|
+
use_sheets(this, sheets);
|
|
685
920
|
slots.lakes.settle_in(frag);
|
|
686
921
|
if (!this.#live_ready) {
|
|
687
922
|
this.replaceChildren(frag);
|
|
@@ -692,7 +927,7 @@ var OgygiaRegion = class OgygiaRegion extends HTMLElement {
|
|
|
692
927
|
const morph = slots.morph;
|
|
693
928
|
if (morph) morph(this, nodes);
|
|
694
929
|
else this.replaceChildren(...nodes);
|
|
695
|
-
for (const el of this.querySelectorAll("ogygia-region")) if (el instanceof OgygiaRegion) el.#renew();
|
|
930
|
+
for (const el of this.querySelectorAll("ogygia-region")) if (el instanceof OgygiaRegion) el.#box.post((t) => el.#renew(t));
|
|
696
931
|
}
|
|
697
932
|
restore_adopt_sheets();
|
|
698
933
|
init_shadow_registries(this);
|
|
@@ -704,154 +939,188 @@ var OgygiaRegion = class OgygiaRegion extends HTMLElement {
|
|
|
704
939
|
* new page facts (`pv`) — it fetches its new answer, the current one on screen until that lands.
|
|
705
940
|
* A hole that never fetched yet fetches the new address on its own schedule.
|
|
706
941
|
*/
|
|
707
|
-
#renew() {
|
|
942
|
+
#renew(turn) {
|
|
708
943
|
if (!this.#frame_address || !is_deferred(this)) return;
|
|
709
944
|
const endpoint = this.getAttribute("endpoint");
|
|
710
945
|
if (!endpoint || frameAddress(endpoint) === this.#frame_address) return;
|
|
711
|
-
this.#fetch_html(
|
|
946
|
+
return this.#fetch_html(turn, true);
|
|
712
947
|
}
|
|
713
948
|
/**
|
|
714
|
-
* Fetch `endpoint` HTML
|
|
715
|
-
*
|
|
716
|
-
*
|
|
717
|
-
*
|
|
949
|
+
* Fetch `endpoint` HTML into the store; the store's write swaps it in (#apply). One visit to the
|
|
950
|
+
* hole's `fetching`: it ends when the fetch does (idle), when it failed (`retrying`, `failed`), or
|
|
951
|
+
* when the region leaves the page (it leaves the shared fetch). A failure leaves the
|
|
952
|
+
* answer as it was, so a reconnect, a remount or the retry can try again — a transient network
|
|
953
|
+
* error never kills the hole for good.
|
|
718
954
|
* A deferred hole reuses the browser's `<link rel="preload">` response; an swr REVALIDATE must
|
|
719
955
|
* not (the endpoint answers `cache-control: private, max-age=30`, and stale is the whole point).
|
|
720
956
|
*/
|
|
721
|
-
async #fetch_html(
|
|
957
|
+
async #fetch_html(turn, revalidate = false) {
|
|
722
958
|
if (!FRAMES) return;
|
|
723
|
-
|
|
959
|
+
const s = this.#hole.now;
|
|
960
|
+
if (s.kind === "fetching" || !revalidate && this.#answer.now.kind !== "none") return;
|
|
724
961
|
const endpoint = this.#endpoint();
|
|
725
962
|
if (!endpoint) return;
|
|
726
963
|
if (!is_allowed_region_endpoint(endpoint)) {
|
|
727
|
-
if (import.meta.env.DEV)
|
|
964
|
+
if (import.meta.env.DEV) warn("region_endpoint_foreign", { endpoint });
|
|
728
965
|
return;
|
|
729
966
|
}
|
|
730
|
-
|
|
967
|
+
const frames = slots.frames;
|
|
968
|
+
if (!frames) return;
|
|
731
969
|
if (!this.#fetch_started) this.#fetch_started = performance.now();
|
|
732
|
-
this.#
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
970
|
+
const address = this.#bind(endpoint);
|
|
971
|
+
const attempts = s.kind === "armed" || s.kind === "retrying" ? s.attempts : 0;
|
|
972
|
+
this.#hole.go("fetching", {
|
|
973
|
+
attempts,
|
|
974
|
+
revalidate
|
|
975
|
+
});
|
|
976
|
+
const mine = this.#hole.now;
|
|
977
|
+
let html;
|
|
978
|
+
try {
|
|
979
|
+
html = await turn.wait(this.#hole.scope.task((signal) => this.#ensure_frame(frames, endpoint, address, revalidate, signal)));
|
|
980
|
+
} catch (err) {
|
|
981
|
+
if (this.#hole.now !== mine) return;
|
|
982
|
+
return this.#fetch_failed(turn, err, endpoint, revalidate, attempts + 1);
|
|
740
983
|
}
|
|
741
|
-
if (
|
|
742
|
-
|
|
984
|
+
if (html !== STALE && this.#hole.now === mine) this.#hole.go("idle");
|
|
985
|
+
}
|
|
986
|
+
/**
|
|
987
|
+
* The network side of one visit to `fetching`, through the frame store: N regions with the same
|
|
988
|
+
* address ⇒ one request, and a stale response can't overwrite a newer one (the store tickets at
|
|
989
|
+
* request time). `signal` is the visit's: leaving it (the region left the page) lets THIS region
|
|
990
|
+
* go (`STALE`), never a twin — the shared fetch is aborted only once its last caller left.
|
|
991
|
+
*/
|
|
992
|
+
async #ensure_frame(frames, endpoint, address, revalidate, signal) {
|
|
993
|
+
if (!revalidate && frames.join && !endpoint.includes("&ttl=") && !capability_expired(endpoint)) await frames.join(endpoint);
|
|
994
|
+
const fetcher = this.#frame_fetcher(endpoint, revalidate, () => this.#visitor ? "user" : viewport_class(this));
|
|
995
|
+
const run = () => frames.ensure(address, fetcher, {
|
|
996
|
+
force: revalidate,
|
|
997
|
+
signal
|
|
998
|
+
});
|
|
743
999
|
try {
|
|
744
|
-
|
|
745
|
-
if (!opts.revalidate && frames?.join && !endpoint.includes("&ttl=") && !capability_expired(endpoint)) await frames.join(endpoint);
|
|
746
|
-
const run = () => frames?.ensure(address, this.#frame_fetcher(endpoint, !!opts.revalidate, () => this.#user_woken ? "user" : viewport_class(this)), { force: opts.revalidate });
|
|
747
|
-
try {
|
|
748
|
-
await run();
|
|
749
|
-
} catch (err) {
|
|
750
|
-
if (err?.name !== "OgygiaBatchMiss" || outer.aborted) throw err;
|
|
751
|
-
await run();
|
|
752
|
-
}
|
|
753
|
-
this.#adopt_renewed();
|
|
1000
|
+
return await run();
|
|
754
1001
|
} catch (err) {
|
|
755
|
-
if (err?.name
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
id = new URL(endpoint, location.href).searchParams.get("id") ?? "";
|
|
770
|
-
if (refused) final_path = new URL(err.final_url, location.href).pathname;
|
|
771
|
-
} catch {}
|
|
772
|
-
if (BEACON && id) beacon_hole_failed({
|
|
773
|
-
id,
|
|
774
|
-
reason: refused ? err.reason : "error",
|
|
775
|
-
...final_path ? { final_path } : {},
|
|
776
|
-
...refused ? {} : { message: String(err?.message ?? err).split("\n")[0].slice(0, 200) },
|
|
777
|
-
...deny !== void 0 ? { message: deny.slice(0, 200) } : {},
|
|
778
|
-
attempts: this.#fetch_attempts,
|
|
779
|
-
t: Math.round(performance.now())
|
|
780
|
-
});
|
|
781
|
-
}
|
|
782
|
-
if (DEVTOOLS) emit({
|
|
783
|
-
domain: "runtime",
|
|
784
|
-
name: "region.server.failed",
|
|
785
|
-
entry: this.getAttribute("entry") || void 0,
|
|
1002
|
+
if (err?.name !== "OgygiaBatchMiss" || signal.aborted) throw err;
|
|
1003
|
+
return await run();
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
/**
|
|
1007
|
+
* A fetch of this hole failed (never its own leaving: that is no failure). Said in dev, told to the
|
|
1008
|
+
* profiler and the devtools; then a retry waits in `retrying` (called off by an answer that comes
|
|
1009
|
+
* another way, a new schedule, or the region leaving), or, refused or out of tries, it gives up.
|
|
1010
|
+
*/
|
|
1011
|
+
async #fetch_failed(turn, err, endpoint, revalidate, attempts) {
|
|
1012
|
+
const refused = err instanceof RegionAnswerRefused;
|
|
1013
|
+
const deny = refused && err.reason === "forbidden" ? err.deny || "no reason given" : void 0;
|
|
1014
|
+
if (import.meta.env.DEV) {
|
|
1015
|
+
if (deny !== void 0) warn("region_answer_denied", {
|
|
786
1016
|
endpoint,
|
|
1017
|
+
deny
|
|
1018
|
+
});
|
|
1019
|
+
else if (refused) warn("region_answer_redirected", {
|
|
1020
|
+
endpoint,
|
|
1021
|
+
how: err.reason === "redirected" ? "redirected" : "document",
|
|
1022
|
+
final_url: err.final_url
|
|
1023
|
+
});
|
|
1024
|
+
else warn("region_fetch_failed", { endpoint }, err);
|
|
1025
|
+
}
|
|
1026
|
+
const final = refused || !this.isConnected || attempts >= region_fetch.attempts;
|
|
1027
|
+
if (final) {
|
|
1028
|
+
let id = "";
|
|
1029
|
+
let final_path;
|
|
1030
|
+
try {
|
|
1031
|
+
id = new URL(endpoint, location.href).searchParams.get("id") ?? "";
|
|
1032
|
+
if (refused) final_path = new URL(err.final_url, location.href).pathname;
|
|
1033
|
+
} catch {}
|
|
1034
|
+
if (BEACON && id) beacon_hole_failed({
|
|
1035
|
+
id,
|
|
787
1036
|
reason: refused ? err.reason : "error",
|
|
788
|
-
...
|
|
1037
|
+
...final_path ? { final_path } : {},
|
|
1038
|
+
...refused ? {} : { message: String(err?.message ?? err).split("\n")[0].slice(0, 200) },
|
|
789
1039
|
...deny !== void 0 ? { message: deny.slice(0, 200) } : {},
|
|
790
|
-
|
|
791
|
-
|
|
1040
|
+
attempts,
|
|
1041
|
+
t: Math.round(performance.now())
|
|
792
1042
|
});
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
1043
|
+
}
|
|
1044
|
+
if (DEVTOOLS) emit({
|
|
1045
|
+
domain: "runtime",
|
|
1046
|
+
name: "region.server.failed",
|
|
1047
|
+
entry: this.getAttribute("entry") || void 0,
|
|
1048
|
+
endpoint,
|
|
1049
|
+
reason: refused ? err.reason : "error",
|
|
1050
|
+
...refused ? { final_url: err.final_url } : { message: String(err?.message ?? err).split("\n")[0].slice(0, 200) },
|
|
1051
|
+
...deny !== void 0 ? { message: deny.slice(0, 200) } : {},
|
|
1052
|
+
attempt: attempts,
|
|
1053
|
+
final
|
|
1054
|
+
});
|
|
1055
|
+
this.#scheduled = false;
|
|
1056
|
+
if (!final) {
|
|
1057
|
+
if (this.#answer.now.kind !== "none") {
|
|
1058
|
+
this.#hole.go("idle");
|
|
801
1059
|
return;
|
|
802
1060
|
}
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
1061
|
+
this.#hole.go("retrying", {
|
|
1062
|
+
attempts,
|
|
1063
|
+
revalidate
|
|
1064
|
+
});
|
|
1065
|
+
const retrying = this.#hole.scope;
|
|
1066
|
+
const wait = backoffAfter(region_fetch, attempts);
|
|
1067
|
+
if (await turn.wait(retrying.task(() => new Promise((resolve) => retrying.timeout(resolve, wait)))) === STALE) return;
|
|
1068
|
+
if (revalidate) return this.#fetch_html(turn, true);
|
|
1069
|
+
return this.#connect(turn);
|
|
806
1070
|
}
|
|
1071
|
+
this.#hole.go("failed");
|
|
1072
|
+
slots.lakes.after_fetch_exhausted(this, { revalidate }, () => this.#wake_waiting_regions());
|
|
807
1073
|
}
|
|
808
1074
|
/** Re-enter connectedCallback for descendants that early-returned on an unsettled lake. */
|
|
809
1075
|
#wake_waiting_regions() {
|
|
810
1076
|
for (const el of this.querySelectorAll("ogygia-region")) if (el instanceof OgygiaRegion) el.connectedCallback();
|
|
811
1077
|
}
|
|
812
|
-
#on_idle(fire) {
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
fire();
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
1078
|
+
#on_idle(fire, owner) {
|
|
1079
|
+
const s = owner.scope;
|
|
1080
|
+
if (!("requestIdleCallback" in window)) {
|
|
1081
|
+
s.timeout(() => fire(), 200);
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
const first = () => {
|
|
1085
|
+
off_idle();
|
|
1086
|
+
off_cap();
|
|
819
1087
|
fire();
|
|
820
|
-
}
|
|
1088
|
+
};
|
|
1089
|
+
const off_idle = s.timeout(first, "idle");
|
|
1090
|
+
const off_cap = s.timeout(first, 2e3);
|
|
821
1091
|
}
|
|
822
1092
|
/** `visible`: the shared observer for this margin (observe.ts) — fires once, on first entry. */
|
|
823
|
-
#on_visible(fire, root_margin) {
|
|
1093
|
+
#on_visible(fire, root_margin, owner) {
|
|
824
1094
|
const rootMargin = root_margin || this.getAttribute("margin") || "0px";
|
|
825
|
-
|
|
826
|
-
this.#stop_visible = null;
|
|
827
|
-
fire();
|
|
828
|
-
});
|
|
1095
|
+
owner.scope.defer(once_visible(this, rootMargin, () => void fire()));
|
|
829
1096
|
}
|
|
830
|
-
#on_media(q, fire) {
|
|
831
|
-
if (!q) return fire();
|
|
1097
|
+
#on_media(q, fire, owner) {
|
|
1098
|
+
if (!q) return void fire();
|
|
832
1099
|
const mql = matchMedia(q);
|
|
833
|
-
if (mql.matches) return fire();
|
|
834
|
-
const
|
|
835
|
-
if (e.matches)
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
}
|
|
840
|
-
};
|
|
841
|
-
mql.addEventListener("change", on);
|
|
842
|
-
this.#mql = {
|
|
843
|
-
mql,
|
|
844
|
-
on
|
|
845
|
-
};
|
|
1100
|
+
if (mql.matches) return void fire();
|
|
1101
|
+
const off = owner.scope.on(mql, "change", (e) => {
|
|
1102
|
+
if (!e.matches) return;
|
|
1103
|
+
off();
|
|
1104
|
+
fire();
|
|
1105
|
+
});
|
|
846
1106
|
}
|
|
847
1107
|
/**
|
|
848
1108
|
* Wake this island: load its module and the hydrate core in parallel, then take a SCHEDULER
|
|
849
1109
|
* TURN (one island per task, viewport first — schedule.ts) and run the synchronous hydrate step
|
|
850
1110
|
* (hydrate-core.ts) inside it.
|
|
851
1111
|
*/
|
|
852
|
-
async #hydrate() {
|
|
853
|
-
|
|
854
|
-
this.#
|
|
1112
|
+
async #hydrate(turn) {
|
|
1113
|
+
const from = this.#wake.now.kind;
|
|
1114
|
+
if (this.#app || from !== "cold" && from !== "armed") return;
|
|
1115
|
+
this.#wake.go("waking");
|
|
1116
|
+
const mine = this.#wake.now;
|
|
1117
|
+
/** What this wake waits on is its visit's: leaving `waking` ends it. */
|
|
1118
|
+
const visit = this.#wake.scope;
|
|
1119
|
+
/** After each wait, back in its turn: the region left the page (`STALE`: its mailbox closed, the
|
|
1120
|
+
* one it came back with wakes it), or is on its way out (its teardown a microtask away), or the
|
|
1121
|
+
* visit is over. Checked after every wait before the turn. */
|
|
1122
|
+
const gone = (back) => back === STALE || !this.isConnected || this.#wake.now !== mine;
|
|
1123
|
+
let failure = null;
|
|
855
1124
|
const t0 = now_ms();
|
|
856
1125
|
const dt_t0 = DEVTOOLS ? t0 : 0;
|
|
857
1126
|
let t_loaded = t0;
|
|
@@ -864,49 +1133,60 @@ var OgygiaRegion = class OgygiaRegion extends HTMLElement {
|
|
|
864
1133
|
});
|
|
865
1134
|
if (CLIENT_PLUGINS) client_region_mount(this);
|
|
866
1135
|
try {
|
|
867
|
-
if (typeof document !== "undefined" && document.readyState === "loading")
|
|
868
|
-
|
|
1136
|
+
if (typeof document !== "undefined" && document.readyState === "loading") {
|
|
1137
|
+
if (gone(await turn.wait(dom_ready()))) return;
|
|
1138
|
+
}
|
|
1139
|
+
if (gone()) return;
|
|
869
1140
|
const entry = island_entry_of(this);
|
|
870
1141
|
if (!entry) return;
|
|
871
|
-
const cls = () => this.#
|
|
872
|
-
const
|
|
1142
|
+
const cls = () => this.#visitor ? "user" : viewport_class(this);
|
|
1143
|
+
const slot = new LoadSlot({
|
|
873
1144
|
kind: "code",
|
|
874
1145
|
cls,
|
|
875
1146
|
label: entry
|
|
876
|
-
})
|
|
1147
|
+
});
|
|
1148
|
+
const release = await turn.wait(slot.ready);
|
|
1149
|
+
if (release === STALE) return void slot.ready.then(hand_back);
|
|
877
1150
|
let mod, core;
|
|
878
1151
|
try {
|
|
879
|
-
if (
|
|
1152
|
+
if (gone()) return;
|
|
880
1153
|
hydrate_started(this);
|
|
881
|
-
|
|
1154
|
+
const loaded = await turn.wait(Promise.all([
|
|
882
1155
|
load_island(entry, cls()),
|
|
883
|
-
hydrate_core(),
|
|
884
|
-
slots.wire_classes?.(this)
|
|
885
|
-
|
|
1156
|
+
hydrate_core.get(),
|
|
1157
|
+
slots.wire_classes?.(this),
|
|
1158
|
+
devalue_for_wake()
|
|
1159
|
+
]));
|
|
1160
|
+
if (loaded === STALE) return;
|
|
1161
|
+
[mod, core] = loaded;
|
|
886
1162
|
} finally {
|
|
887
1163
|
release();
|
|
888
1164
|
}
|
|
889
1165
|
t_loaded = now_ms();
|
|
890
|
-
if (
|
|
891
|
-
await this.#kit_page_ready(core);
|
|
892
|
-
if (!this.isConnected) return;
|
|
1166
|
+
if (gone()) return;
|
|
1167
|
+
if (gone(await turn.wait(this.#kit_page_ready(core, visit)))) return;
|
|
893
1168
|
core.seed_remote_once();
|
|
894
1169
|
core.seed_page_once();
|
|
895
|
-
|
|
896
|
-
|
|
1170
|
+
let visitor;
|
|
1171
|
+
do {
|
|
1172
|
+
visitor = this.#visitor;
|
|
1173
|
+
if (visitor && gone(await turn.wait(visitor.catch(nothing)))) return;
|
|
1174
|
+
if (gone()) return;
|
|
1175
|
+
if (gone(await turn.wait(hydrate_turn(this))) || this.#app) return;
|
|
1176
|
+
} while (this.#visitor !== visitor);
|
|
897
1177
|
t_turn = now_ms();
|
|
898
1178
|
const ssr_html = this.#ssr_html;
|
|
899
1179
|
const touched = ssr_html === null || region_changed(this);
|
|
900
|
-
if (import.meta.env.DEV) set_hydrating(this);
|
|
1180
|
+
if (import.meta.env.DEV) set_hydrating(this, this.#life);
|
|
901
1181
|
this.#app = core.hydrate_island(this, entry, mod, ssr_html, touched);
|
|
902
1182
|
this.#ssr_html = null;
|
|
903
1183
|
this.#unwatch();
|
|
904
1184
|
if (!this.#app) return;
|
|
1185
|
+
if (RULES) note_mount(this);
|
|
905
1186
|
const t_done = now_ms();
|
|
906
1187
|
await Promise.resolve();
|
|
907
1188
|
const t_mounted = now_ms();
|
|
908
|
-
await snippets_settled(this);
|
|
909
|
-
if (!this.isConnected || !this.#app) return;
|
|
1189
|
+
if (gone(await turn.wait(snippets_settled(this, visit))) || !this.#app) return;
|
|
910
1190
|
this.setAttribute("data-hydrated", "");
|
|
911
1191
|
if (BEACON) beacon_hydrated(this, t0, t_loaded, t_done, ssr_html, t_turn);
|
|
912
1192
|
if (DEVTOOLS) emit({
|
|
@@ -917,6 +1197,8 @@ var OgygiaRegion = class OgygiaRegion extends HTMLElement {
|
|
|
917
1197
|
});
|
|
918
1198
|
this.dispatchEvent(new CustomEvent("ogygia:hydrated", { bubbles: true }));
|
|
919
1199
|
} catch (err) {
|
|
1200
|
+
if (this.#wake.now !== mine) return;
|
|
1201
|
+
failure = { error: err };
|
|
920
1202
|
if (this.#watching) mark_region_changed(this);
|
|
921
1203
|
this.#unwatch();
|
|
922
1204
|
if (BEACON) beacon_failed(this, err?.message ?? String(err), t0, t_turn);
|
|
@@ -927,19 +1209,19 @@ var OgygiaRegion = class OgygiaRegion extends HTMLElement {
|
|
|
927
1209
|
message: err?.message ?? String(err),
|
|
928
1210
|
...t_turn !== void 0 ? { turn: t_turn } : {}
|
|
929
1211
|
});
|
|
930
|
-
const msg = err?.message ?? "";
|
|
931
1212
|
const entry = this.getAttribute("entry");
|
|
932
|
-
if (
|
|
1213
|
+
if (is_failed_import(err)) {
|
|
933
1214
|
if (import.meta.env.DEV) {
|
|
934
|
-
console.error(
|
|
1215
|
+
console.error(new OgygiaError("island_load_failed_stale_deps", { entry }), err);
|
|
935
1216
|
recover_from_stale_deps(entry);
|
|
936
|
-
} else console.error("
|
|
937
|
-
} else console.error(
|
|
1217
|
+
} else console.error(new OgygiaError("island_load_failed", { entry }), err);
|
|
1218
|
+
} else console.error(new OgygiaError("island_hydrate_failed", { entry }), err);
|
|
938
1219
|
} finally {
|
|
939
|
-
this.#
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
1220
|
+
if (this.#wake.now === mine) {
|
|
1221
|
+
hydrate_settled(this);
|
|
1222
|
+
if (failure) this.#wake.go("failed", failure);
|
|
1223
|
+
else this.#wake.go(this.#app ? "awake" : "cold");
|
|
1224
|
+
}
|
|
943
1225
|
}
|
|
944
1226
|
}
|
|
945
1227
|
/**
|
|
@@ -947,7 +1229,7 @@ var OgygiaRegion = class OgygiaRegion extends HTMLElement {
|
|
|
947
1229
|
* carries server-rendered HTML). No fetch — the HTML is already here.
|
|
948
1230
|
*
|
|
949
1231
|
* - **First tick:** parse + swap the HTML in. Interactive (`hydrate` + `module`) → hydrate a
|
|
950
|
-
*
|
|
1232
|
+
* pushable props so later ticks can push them. Static → done (no JS ever ships).
|
|
951
1233
|
* - **Later tick, same interactive module:** keep-alive — push the new props to the mounted
|
|
952
1234
|
* component (focus + local state survive; Svelte reconciles). No swap, no re-hydrate.
|
|
953
1235
|
* - **Later tick, static:** morph the new HTML in place (see {@link morph_children}).
|
|
@@ -955,82 +1237,145 @@ var OgygiaRegion = class OgygiaRegion extends HTMLElement {
|
|
|
955
1237
|
*/
|
|
956
1238
|
async applyLive(desc) {
|
|
957
1239
|
if (!FRAMES) return;
|
|
1240
|
+
this.#live_last = desc;
|
|
1241
|
+
await this.#box.ask((turn) => this.#apply_live(desc, turn));
|
|
1242
|
+
}
|
|
1243
|
+
/**
|
|
1244
|
+
* A live region put back on the page (#337) goes on with its last tick: leaving it disposed its
|
|
1245
|
+
* island or dropped its subscription. A static one binds to its address again, where the store
|
|
1246
|
+
* hands it the newest frame (or, when it let that go, the last tick's HTML is painted again); an
|
|
1247
|
+
* interactive one swaps and hydrates its island again. Still in its mode (a move), it has nothing
|
|
1248
|
+
* to catch up on.
|
|
1249
|
+
*/
|
|
1250
|
+
#live_put_back(turn) {
|
|
1251
|
+
const last = this.#live_last;
|
|
1252
|
+
if (last === null || this.#live.now.kind !== "idle") return;
|
|
1253
|
+
return this.#apply_live(last, turn, true);
|
|
1254
|
+
}
|
|
1255
|
+
/** One live tick, in its turn. `again`: its last tick, gone on with as it is put back. */
|
|
1256
|
+
async #apply_live(desc, turn, again = false) {
|
|
958
1257
|
this.setAttribute("render", "defer");
|
|
959
1258
|
this.setAttribute("entry", desc.module || desc.id);
|
|
960
1259
|
if (desc.url) this.setAttribute("endpoint", desc.url);
|
|
961
1260
|
if (desc.hydrate) this.setAttribute("wake", desc.hydrate);
|
|
962
1261
|
else this.removeAttribute("wake");
|
|
963
1262
|
if (desc.hydrateMargin) this.setAttribute("hydrate-margin", desc.hydrateMargin);
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
1263
|
+
if (!(!!desc.hydrate && !!desc.module)) {
|
|
1264
|
+
if (this.#live.now.kind !== "static") this.#live.go("static");
|
|
1265
|
+
let replayed = false;
|
|
1266
|
+
if (desc.url && !this.#live_unsub) {
|
|
1267
|
+
const box = this.#box;
|
|
1268
|
+
let binding = true;
|
|
1269
|
+
const off = slots.frames?.subscribe(frameAddress(desc.url), (f) => {
|
|
1270
|
+
if (binding && !again) return;
|
|
1271
|
+
replayed ||= binding;
|
|
1272
|
+
box.post((t) => this.#morph_live(f.html, t));
|
|
1273
|
+
});
|
|
1274
|
+
binding = false;
|
|
1275
|
+
if (off) {
|
|
1276
|
+
this.#live_unsub = off;
|
|
1277
|
+
this.#live.scope.defer(() => {
|
|
1278
|
+
off();
|
|
1279
|
+
this.#live_unsub = null;
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
969
1282
|
}
|
|
970
|
-
this.#morph_live(desc.html);
|
|
1283
|
+
if (!replayed) await this.#morph_live(desc.html, turn);
|
|
971
1284
|
return;
|
|
972
1285
|
}
|
|
973
|
-
|
|
974
|
-
|
|
1286
|
+
const s = this.#live.now;
|
|
1287
|
+
if (s.kind === "live" && s.module === desc.module) {
|
|
1288
|
+
s.app.set_props?.(desc.props);
|
|
975
1289
|
this.dispatchEvent(new CustomEvent("ogygia:live", { bubbles: true }));
|
|
976
1290
|
return;
|
|
977
1291
|
}
|
|
978
|
-
if (
|
|
979
|
-
this.#
|
|
980
|
-
|
|
1292
|
+
if (s.kind === "hydrating" && s.module === desc.module) {
|
|
1293
|
+
this.#live_props = desc.props;
|
|
1294
|
+
return;
|
|
981
1295
|
}
|
|
1296
|
+
this.#live.go("hydrating", { module: desc.module });
|
|
1297
|
+
const mine = this.#live.now;
|
|
1298
|
+
const painting = this.#paint_next();
|
|
1299
|
+
this.#live_props = null;
|
|
982
1300
|
this.#live_ready = true;
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1301
|
+
const { frag, ready, sheets } = region_fragment(desc.html);
|
|
1302
|
+
if (await turn.wait(ready) === STALE || this.#live.now !== mine) return;
|
|
1303
|
+
this.#painted(painting);
|
|
1304
|
+
use_sheets(this, sheets);
|
|
987
1305
|
slots.lakes.settle_in(frag);
|
|
988
1306
|
this.replaceChildren(frag);
|
|
989
1307
|
restore_adopt_sheets();
|
|
990
1308
|
init_shadow_registries(this);
|
|
991
|
-
|
|
992
|
-
else {
|
|
993
|
-
this.setAttribute("data-hydrated", "");
|
|
994
|
-
this.dispatchEvent(new CustomEvent("ogygia:live", { bubbles: true }));
|
|
995
|
-
}
|
|
1309
|
+
await this.#live_hydrate(desc.props, mine, desc.module, turn);
|
|
996
1310
|
}
|
|
997
1311
|
/**
|
|
998
1312
|
* On a Kit-hydrated document, a region of ours (a lake's inside, a hole's answer) waits for Kit to
|
|
999
1313
|
* apply its page before it hydrates — `null` (no await) once applied. The wait lives in the lazy
|
|
1000
1314
|
* hydrate core (it observes Kit's reactive page, i.e. it needs Svelte), so it never reaches the boot.
|
|
1315
|
+
* It waits for `visit` (the wake's, or the live tick's): a region that left stops waiting, and the
|
|
1316
|
+
* wait ends with the last region in it.
|
|
1001
1317
|
*/
|
|
1002
|
-
#kit_page_ready(core) {
|
|
1318
|
+
#kit_page_ready(core, visit) {
|
|
1003
1319
|
if (!kit_hydrates_page() || !ours_on_kit_document(this)) return null;
|
|
1004
|
-
return core.kit_page_thread();
|
|
1320
|
+
return core.kit_page_thread(visit.signal);
|
|
1005
1321
|
}
|
|
1006
|
-
/**
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
label: entry
|
|
1017
|
-
}).ready;
|
|
1018
|
-
let mod, core;
|
|
1322
|
+
/**
|
|
1323
|
+
* Hydrate a live region's swapped-in HTML through the hydrate core's live path — for the visit to
|
|
1324
|
+
* `hydrating` that swapped it (`mine`): a later tick that took over (another module, a static
|
|
1325
|
+
* tick) or the region leaving ends it before it mounts. One that could not mount goes back to
|
|
1326
|
+
* `idle`, so the next tick starts over.
|
|
1327
|
+
*/
|
|
1328
|
+
async #live_hydrate(props, mine, module, turn) {
|
|
1329
|
+
/** Back in its turn after a wait: the region left the page (`STALE`), is on its way out, or
|
|
1330
|
+
* another tick took over. */
|
|
1331
|
+
const ours = (back) => back !== STALE && this.isConnected && this.#live.now === mine;
|
|
1019
1332
|
try {
|
|
1020
|
-
if (!
|
|
1021
|
-
|
|
1333
|
+
if (!ours(await turn.wait(dom_ready()))) return;
|
|
1334
|
+
const entry = island_entry_of(this);
|
|
1335
|
+
if (!entry) return;
|
|
1336
|
+
const cls = () => viewport_class(this);
|
|
1337
|
+
const slot = new LoadSlot({
|
|
1338
|
+
kind: "code",
|
|
1339
|
+
cls,
|
|
1340
|
+
label: entry
|
|
1341
|
+
});
|
|
1342
|
+
const release = await turn.wait(slot.ready);
|
|
1343
|
+
if (release === STALE) return void slot.ready.then(hand_back);
|
|
1344
|
+
let mod, core;
|
|
1345
|
+
try {
|
|
1346
|
+
if (!ours()) return;
|
|
1347
|
+
const loaded = await turn.wait(Promise.all([
|
|
1348
|
+
load_island(entry, cls()),
|
|
1349
|
+
hydrate_core.get(),
|
|
1350
|
+
devalue_for_wake()
|
|
1351
|
+
]));
|
|
1352
|
+
if (loaded === STALE) return;
|
|
1353
|
+
[mod, core] = loaded;
|
|
1354
|
+
} finally {
|
|
1355
|
+
release();
|
|
1356
|
+
}
|
|
1357
|
+
if (!ours()) return;
|
|
1358
|
+
if (!ours(await turn.wait(this.#kit_page_ready(core, this.#live.scope)))) return;
|
|
1359
|
+
if (CLIENT_PLUGINS) client_region_mount(this);
|
|
1360
|
+
const app = core.hydrate_live(this, entry, mod, props);
|
|
1361
|
+
if (!app) return;
|
|
1362
|
+
this.#live.go("live", {
|
|
1363
|
+
module,
|
|
1364
|
+
app
|
|
1365
|
+
});
|
|
1366
|
+
this.#live.scope.defer(() => {
|
|
1367
|
+
if (CLIENT_PLUGINS) client_region_unmount(this, this.getAttribute("entry") ?? "");
|
|
1368
|
+
app.dispose();
|
|
1369
|
+
});
|
|
1370
|
+
const pending = this.#live_props;
|
|
1371
|
+
this.#live_props = null;
|
|
1372
|
+
if (pending) app.set_props?.(pending);
|
|
1373
|
+
this.setAttribute("data-hydrated", "");
|
|
1374
|
+
this.dispatchEvent(new CustomEvent("ogygia:hydrated", { bubbles: true }));
|
|
1375
|
+
this.dispatchEvent(new CustomEvent("ogygia:live", { bubbles: true }));
|
|
1022
1376
|
} finally {
|
|
1023
|
-
|
|
1377
|
+
if (this.#live.now === mine) this.#live.go("idle");
|
|
1024
1378
|
}
|
|
1025
|
-
if (!this.isConnected) return;
|
|
1026
|
-
await this.#kit_page_ready(core);
|
|
1027
|
-
if (!this.isConnected) return;
|
|
1028
|
-
if (CLIENT_PLUGINS) client_region_mount(this);
|
|
1029
|
-
this.#live_app = core.hydrate_live(this, entry, mod, props);
|
|
1030
|
-
if (!this.#live_app) return;
|
|
1031
|
-
this.setAttribute("data-hydrated", "");
|
|
1032
|
-
this.dispatchEvent(new CustomEvent("ogygia:hydrated", { bubbles: true }));
|
|
1033
|
-
this.dispatchEvent(new CustomEvent("ogygia:live", { bubbles: true }));
|
|
1034
1379
|
}
|
|
1035
1380
|
/**
|
|
1036
1381
|
* A MOVE IS NOT A REMOVAL. Another script relocating the element (a custom element that, at its
|
|
@@ -1054,37 +1399,17 @@ var OgygiaRegion = class OgygiaRegion extends HTMLElement {
|
|
|
1054
1399
|
connected_regions.delete(this);
|
|
1055
1400
|
unregister_region(this);
|
|
1056
1401
|
if (this.#watching) mark_region_changed(this);
|
|
1057
|
-
this.#
|
|
1058
|
-
if (
|
|
1059
|
-
if (this.#
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
this.#
|
|
1064
|
-
this.#fetch_abort = null;
|
|
1065
|
-
this.#frame_unsub?.();
|
|
1066
|
-
this.#frame_unsub = null;
|
|
1067
|
-
if (this.#frame_address) {
|
|
1068
|
-
slots.frames?.abandon(this.#frame_address);
|
|
1069
|
-
this.#frame_address = null;
|
|
1070
|
-
}
|
|
1071
|
-
if (this.#idle_handle != null) {
|
|
1072
|
-
if ("cancelIdleCallback" in window) cancelIdleCallback(this.#idle_handle);
|
|
1073
|
-
else clearTimeout(this.#idle_handle);
|
|
1074
|
-
this.#idle_handle = null;
|
|
1075
|
-
}
|
|
1076
|
-
this.#stop_visible?.();
|
|
1077
|
-
this.#stop_visible = null;
|
|
1078
|
-
on_demand_armed.delete(this);
|
|
1079
|
-
this.#disarm_interaction?.();
|
|
1080
|
-
this.#disarm_interaction = null;
|
|
1081
|
-
if (this.#mql) {
|
|
1082
|
-
this.#mql.mql.removeEventListener("change", this.#mql.on);
|
|
1083
|
-
this.#mql = null;
|
|
1084
|
-
}
|
|
1402
|
+
if (CLIENT_PLUGINS && this.#app) client_region_unmount(this, this.getAttribute("entry") ?? "");
|
|
1403
|
+
if (this.#live.now.kind !== "idle") this.#live.go("idle");
|
|
1404
|
+
if (this.#hole.now.kind !== "idle") this.#hole.go("idle");
|
|
1405
|
+
if (this.#wake.now.kind !== "cold") this.#wake.go("cold");
|
|
1406
|
+
this.#life.close();
|
|
1407
|
+
this.#life = NOT_CONNECTED;
|
|
1408
|
+
this.#box = CLOSED_BOX;
|
|
1085
1409
|
if (this.#app) {
|
|
1086
1410
|
this.#app.dispose();
|
|
1087
1411
|
this.#app = null;
|
|
1412
|
+
if (RULES) note_unmount(this);
|
|
1088
1413
|
}
|
|
1089
1414
|
this.#scheduled = false;
|
|
1090
1415
|
}
|
|
@@ -1125,7 +1450,15 @@ function apply_dev_head_region_css() {
|
|
|
1125
1450
|
});
|
|
1126
1451
|
}
|
|
1127
1452
|
function boot(installers = [], client_plugins = []) {
|
|
1453
|
+
disclose_version();
|
|
1454
|
+
if (RULES) install_rules(import.meta.env.DEV);
|
|
1128
1455
|
runtime_session.adopt_document_head();
|
|
1456
|
+
globalThis[Symbol.for("ogygia.messages")] ??= {
|
|
1457
|
+
OgygiaError,
|
|
1458
|
+
warn,
|
|
1459
|
+
line
|
|
1460
|
+
};
|
|
1461
|
+
report_restores();
|
|
1129
1462
|
link_boot();
|
|
1130
1463
|
for (const install of installers) install();
|
|
1131
1464
|
pin_build_map();
|
|
@@ -1137,6 +1470,7 @@ function boot(installers = [], client_plugins = []) {
|
|
|
1137
1470
|
});
|
|
1138
1471
|
if (BEACON) beacon_entry_fallback(report);
|
|
1139
1472
|
});
|
|
1473
|
+
on_reload_needed((url, error) => console.error(new OgygiaError("chunk_reload_needed", { url }), error));
|
|
1140
1474
|
if (import.meta.env.DEV) apply_dev_head_region_css();
|
|
1141
1475
|
if (BEACON) beacon_watch();
|
|
1142
1476
|
if (import.meta.env.DEV) {
|