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