@ogygia/core 0.12.2 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (227) hide show
  1. package/dist/LateIsland.svelte +3 -2
  2. package/dist/LateIslandAwait.svelte +6 -2
  3. package/dist/Region.svelte +22 -33
  4. package/dist/boundary.d.ts +0 -2
  5. package/dist/boundary.js +8 -6
  6. package/dist/compiler/debarrel/barrel.d.ts +15 -8
  7. package/dist/compiler/debarrel/barrel.js +92 -45
  8. package/dist/compiler/debarrel/plugin.js +41 -17
  9. package/dist/compiler/dev/dev-hmr.d.ts +3 -0
  10. package/dist/compiler/dev/dev-hmr.js +3 -0
  11. package/dist/compiler/driver.js +11 -4
  12. package/dist/compiler/flags.js +21 -7
  13. package/dist/compiler/fouc-css.js +5 -1
  14. package/dist/compiler/kit.js +5 -1
  15. package/dist/compiler/link/build-output.js +12 -3
  16. package/dist/compiler/macros/bake.js +18 -4
  17. package/dist/compiler/macros/dollar.js +6 -1
  18. package/dist/compiler/macros/source.js +6 -1
  19. package/dist/compiler/macros/store.js +10 -2
  20. package/dist/compiler/macros/wire.js +6 -1
  21. package/dist/compiler/parse/scan.js +10 -3
  22. package/dist/compiler/region/transform.js +188 -74
  23. package/dist/context-bridge.js +13 -5
  24. package/dist/context.d.ts +9 -0
  25. package/dist/context.js +30 -1
  26. package/dist/document.d.ts +1 -1
  27. package/dist/document.js +7 -1
  28. package/dist/early-holes-head.d.ts +5 -0
  29. package/dist/early-holes-head.js +20 -0
  30. package/dist/early-holes.d.ts +34 -0
  31. package/dist/early-holes.js +51 -0
  32. package/dist/entry-hydrate.js +9 -12
  33. package/dist/events/bus.js +7 -2
  34. package/dist/events/schema.d.ts +2 -2
  35. package/dist/fn-transport.js +4 -3
  36. package/dist/hooks.js +94 -163
  37. package/dist/insights/page-assets.js +11 -3
  38. package/dist/insights/page-insights.d.ts +0 -2
  39. package/dist/insights/page-insights.js +14 -10
  40. package/dist/insights/session-insights.js +2 -2
  41. package/dist/internal-instruments.d.ts +3 -1
  42. package/dist/internal-instruments.js +3 -1
  43. package/dist/internal-server.d.ts +3 -2
  44. package/dist/internal-server.js +3 -2
  45. package/dist/internal.d.ts +1 -2
  46. package/dist/internal.js +1 -2
  47. package/dist/keep-fallback.d.ts +0 -2
  48. package/dist/keep-fallback.js +4 -5
  49. package/dist/live-transport.js +3 -2
  50. package/dist/messages/build.d.ts +585 -0
  51. package/dist/messages/build.js +800 -0
  52. package/dist/messages/core.d.ts +464 -0
  53. package/dist/messages/core.js +843 -0
  54. package/dist/messages/define.d.ts +62 -0
  55. package/dist/messages/define.js +49 -0
  56. package/dist/messages/index.d.ts +6 -0
  57. package/dist/messages/index.js +6 -0
  58. package/dist/messages/island.d.ts +4 -0
  59. package/dist/messages/island.js +34 -0
  60. package/dist/messages/lazy.d.ts +3 -0
  61. package/dist/messages/lazy.js +22 -0
  62. package/dist/messages/prose.d.ts +15 -0
  63. package/dist/messages/prose.js +43 -0
  64. package/dist/messages/server.d.ts +142 -0
  65. package/dist/messages/server.js +246 -0
  66. package/dist/nested.d.ts +3 -0
  67. package/dist/nested.js +8 -1
  68. package/dist/plugin/css.js +14 -13
  69. package/dist/plugin/facts.d.ts +2 -0
  70. package/dist/plugin/facts.js +10 -6
  71. package/dist/plugin/hooks.d.ts +7 -17
  72. package/dist/plugin/hooks.js +32 -4
  73. package/dist/plugin/index.d.ts +9 -2
  74. package/dist/plugin/request-scope.d.ts +20 -0
  75. package/dist/plugin/request-scope.js +149 -0
  76. package/dist/plugin/server.d.ts +20 -2
  77. package/dist/plugin/server.js +98 -28
  78. package/dist/plugin/testing.d.ts +6 -0
  79. package/dist/plugin/testing.js +14 -2
  80. package/dist/policies.d.ts +21 -0
  81. package/dist/policies.js +66 -0
  82. package/dist/preference.js +6 -1
  83. package/dist/preload.js +6 -9
  84. package/dist/ref-watch.d.ts +0 -10
  85. package/dist/ref-watch.js +14 -1
  86. package/dist/ref.js +17 -5
  87. package/dist/region-snippet.js +9 -3
  88. package/dist/region.js +2 -1
  89. package/dist/report-thrown.d.ts +9 -0
  90. package/dist/report-thrown.js +15 -0
  91. package/dist/rules.d.ts +17 -0
  92. package/dist/rules.js +57 -0
  93. package/dist/runtime/beacon.d.ts +0 -4
  94. package/dist/runtime/beacon.js +58 -43
  95. package/dist/runtime/boot-link.js +19 -2
  96. package/dist/runtime/client-hooks.js +2 -1
  97. package/dist/runtime/concurrency.d.ts +16 -5
  98. package/dist/runtime/concurrency.js +123 -7
  99. package/dist/runtime/core.d.ts +3 -1
  100. package/dist/runtime/core.js +718 -384
  101. package/dist/runtime/devalue-lane.d.ts +13 -0
  102. package/dist/runtime/devalue-lane.js +49 -0
  103. package/dist/runtime/disclose-version.d.ts +1 -0
  104. package/dist/runtime/disclose-version.js +14 -0
  105. package/dist/runtime/early-holes.d.ts +4 -0
  106. package/dist/runtime/early-holes.js +40 -0
  107. package/dist/runtime/entry-locations.js +40 -49
  108. package/dist/runtime/foreign-props.d.ts +0 -7
  109. package/dist/runtime/foreign-props.js +2 -1
  110. package/dist/runtime/form-continuity.d.ts +2 -2
  111. package/dist/runtime/form-continuity.js +30 -8
  112. package/dist/runtime/frame-nav.d.ts +7 -1
  113. package/dist/runtime/frame-nav.js +84 -18
  114. package/dist/runtime/frame-store.d.ts +25 -16
  115. package/dist/runtime/frame-store.js +124 -98
  116. package/dist/runtime/frames.js +8 -4
  117. package/dist/runtime/hole-facts.js +6 -7
  118. package/dist/runtime/hole-fetch.d.ts +17 -0
  119. package/dist/runtime/hole-fetch.js +47 -0
  120. package/dist/runtime/hydrate-core.d.ts +4 -4
  121. package/dist/runtime/hydrate-core.js +80 -64
  122. package/dist/runtime/import-again.d.ts +33 -0
  123. package/dist/runtime/import-again.js +217 -0
  124. package/dist/runtime/interaction-replay.d.ts +5 -3
  125. package/dist/runtime/interaction-replay.js +6 -6
  126. package/dist/runtime/interaction.d.ts +14 -3
  127. package/dist/runtime/interaction.js +46 -23
  128. package/dist/runtime/kit-page-thread.svelte.d.ts +2 -1
  129. package/dist/runtime/kit-page-thread.svelte.js +44 -20
  130. package/dist/runtime/lakes.d.ts +1 -1
  131. package/dist/runtime/lakes.js +18 -5
  132. package/dist/runtime/live.d.ts +1 -2
  133. package/dist/runtime/live.js +6 -8
  134. package/dist/runtime/load-scheduler.d.ts +29 -18
  135. package/dist/runtime/load-scheduler.js +236 -136
  136. package/dist/runtime/nav-handle.js +25 -6
  137. package/dist/runtime/navigation.d.ts +24 -0
  138. package/dist/runtime/navigation.js +24 -0
  139. package/dist/runtime/ownership.d.ts +1 -1
  140. package/dist/runtime/page-defer.js +3 -3
  141. package/dist/runtime/parse-html.d.ts +15 -4
  142. package/dist/runtime/parse-html.js +24 -5
  143. package/dist/runtime/pushable-props.svelte.d.ts +21 -0
  144. package/dist/runtime/pushable-props.svelte.js +37 -0
  145. package/dist/runtime/region-endpoint-url.js +4 -6
  146. package/dist/runtime/region-machines.d.ts +111 -0
  147. package/dist/runtime/region-machines.js +97 -0
  148. package/dist/runtime/region-sheets.d.ts +31 -0
  149. package/dist/runtime/region-sheets.js +49 -0
  150. package/dist/runtime/restore-report.d.ts +1 -0
  151. package/dist/runtime/restore-report.js +36 -0
  152. package/dist/runtime/restore.d.ts +5 -2
  153. package/dist/runtime/restore.js +33 -14
  154. package/dist/runtime/router-nav.d.ts +32 -12
  155. package/dist/runtime/router-nav.js +320 -228
  156. package/dist/runtime/router.d.ts +83 -19
  157. package/dist/runtime/router.js +297 -129
  158. package/dist/runtime/rules.d.ts +35 -0
  159. package/dist/runtime/rules.js +137 -0
  160. package/dist/runtime/schedule.d.ts +6 -3
  161. package/dist/runtime/schedule.js +172 -57
  162. package/dist/runtime/seeds.d.ts +5 -1
  163. package/dist/runtime/seeds.js +11 -5
  164. package/dist/runtime/session.d.ts +46 -7
  165. package/dist/runtime/session.js +78 -10
  166. package/dist/runtime/slots.d.ts +53 -20
  167. package/dist/runtime/slots.js +2 -3
  168. package/dist/runtime/soft-refresh.d.ts +16 -0
  169. package/dist/runtime/soft-refresh.js +14 -0
  170. package/dist/runtime/svelte-warnings.d.ts +4 -10
  171. package/dist/runtime/svelte-warnings.js +7 -4
  172. package/dist/runtime/wire-classes.js +21 -8
  173. package/dist/runtime/wire-format.js +6 -3
  174. package/dist/script.js +8 -7
  175. package/dist/seed-ref-reviver.d.ts +0 -9
  176. package/dist/seed-ref-reviver.js +2 -1
  177. package/dist/seed-refs.d.ts +46 -36
  178. package/dist/seed-refs.js +131 -92
  179. package/dist/server/document-tail.d.ts +6 -0
  180. package/dist/server/document-tail.js +7 -1
  181. package/dist/server/early-holes-csp.d.ts +14 -0
  182. package/dist/server/early-holes-csp.js +73 -0
  183. package/dist/server/page-facts.d.ts +4 -1
  184. package/dist/server/page-facts.js +4 -1
  185. package/dist/server/page-seed.d.ts +2 -2
  186. package/dist/server/page-seed.js +104 -11
  187. package/dist/server/page-stream.d.ts +14 -1
  188. package/dist/server/page-stream.js +119 -2
  189. package/dist/server/personal-response.d.ts +0 -5
  190. package/dist/server/personal-response.js +6 -2
  191. package/dist/server/props-wire.js +5 -1
  192. package/dist/server/region-endpoint.js +10 -2
  193. package/dist/server/render-cache.d.ts +24 -20
  194. package/dist/server/render-cache.js +99 -40
  195. package/dist/server/render-page.js +46 -15
  196. package/dist/server/request-stats.d.ts +10 -8
  197. package/dist/server/request-stats.js +20 -12
  198. package/dist/server/rules.d.ts +11 -0
  199. package/dist/server/rules.js +64 -0
  200. package/dist/server/signing-key.js +31 -18
  201. package/dist/server/stream-regions.d.ts +14 -1
  202. package/dist/server/stream-regions.js +38 -2
  203. package/dist/shared-state.js +5 -1
  204. package/dist/shims/app-navigation.js +3 -2
  205. package/dist/shims/page-store.svelte.js +16 -2
  206. package/dist/source.js +7 -2
  207. package/dist/store-transport.js +12 -5
  208. package/dist/transport.js +3 -2
  209. package/dist/vite/index.js +83 -74
  210. package/dist/vite/kit-dirs.js +7 -1
  211. package/dist/vite/options.d.ts +2 -1
  212. package/dist/vite/options.js +11 -20
  213. package/dist/vite/package-files.js +13 -3
  214. package/dist/vite/paths.d.ts +2 -0
  215. package/dist/vite/paths.js +4 -4
  216. package/dist/vite/plugin-api.d.ts +6 -5
  217. package/dist/vite/plugin-api.js +42 -14
  218. package/dist/vite/style-preprocess.js +10 -3
  219. package/package.json +8 -1
  220. package/dist/KeepHost.svelte +0 -19
  221. package/dist/KeepHost.svelte.d.ts +0 -10
  222. package/dist/LiveHost.svelte +0 -19
  223. package/dist/LiveHost.svelte.d.ts +0 -10
  224. package/dist/NestedProvider.svelte +0 -13
  225. package/dist/NestedProvider.svelte.d.ts +0 -8
  226. package/dist/runtime/page-cache.d.ts +0 -23
  227. package/dist/runtime/page-cache.js +0 -74
@@ -0,0 +1,13 @@
1
+ import { Stale } from "@ogygia/machine";
2
+ type Devalue = typeof import("devalue");
3
+ /** Start (once) and await devalue's import. (Never STALE: nothing drops it.) */
4
+ export declare const load_devalue: () => Promise<Devalue | Stale>;
5
+ /** devalue, already loaded: a devalue-lane read only runs after its load ({@link load_devalue}). */
6
+ export declare function devalue(): Devalue;
7
+ /** Run `fn` with devalue now when it is loaded, else once it is. Nothing else asks again for its
8
+ * caller (a streamed value's resolver runs once), so a failed download is asked for again later,
9
+ * as `devalue_download` says (`tried`: the loads that failed so far). */
10
+ export declare function with_devalue(fn: (d: Devalue) => void, tried?: number): void;
11
+ /** Does this document (or fragment) hold a script only devalue can read? */
12
+ export declare function needs_devalue(root: ParentNode): boolean;
13
+ export {};
@@ -0,0 +1,49 @@
1
+ import { import_again } from "./import-again.js";
2
+ import { devalue_download, download } from "../policies.js";
3
+ import { STALE, backoffAfter, flights } from "@ogygia/machine";
4
+ // src/runtime/devalue-lane.ts
5
+ /**
6
+ * DEVALUE, LOADED WHEN A PAGE NEEDS IT. A side-channel script crosses in one of two lanes
7
+ * (server/props-wire.ts): plain JSON under `data-og-format="json"` (the common props and seed: a
8
+ * payload with nothing devalue exists for), devalue otherwise. A page whose scripts are all JSON never
9
+ * reads devalue, so it never downloads it: the first read that needs it starts its import beside the
10
+ * load that leads there (an island's wake starts it with the hydrate core; a navigation, before its
11
+ * preflight), never after it. The parse itself stays synchronous: by then the module is in.
12
+ *
13
+ * A leaf: island-side code (the page-defer resolver) imports it, so it imports nothing of the boot.
14
+ *
15
+ * A download that fails is tried again at once, and a failure is never kept: the next read loads it
16
+ * afresh (one dropped request must not leave every later wake, and every streamed value, unreadable).
17
+ */
18
+ let lib = null;
19
+ const import_devalue = import_again(() => import("devalue"));
20
+ const devalue_load = flights({
21
+ name: "devalue",
22
+ run: () => import_devalue().then((m) => lib = m),
23
+ retry: download
24
+ });
25
+ /** Start (once) and await devalue's import. (Never STALE: nothing drops it.) */
26
+ const load_devalue = () => devalue_load.get();
27
+ /** devalue, already loaded: a devalue-lane read only runs after its load ({@link load_devalue}). */
28
+ function devalue() {
29
+ if (!lib) throw new Error("[ogygia] https://ogygiajs.dev/e/devalue_not_loaded");
30
+ return lib;
31
+ }
32
+ /** Run `fn` with devalue now when it is loaded, else once it is. Nothing else asks again for its
33
+ * caller (a streamed value's resolver runs once), so a failed download is asked for again later,
34
+ * as `devalue_download` says (`tried`: the loads that failed so far). */
35
+ function with_devalue(fn, tried = 0) {
36
+ if (lib) fn(lib);
37
+ else load_devalue().then((d) => d !== STALE && fn(d), () => {
38
+ const failed = tried + 1;
39
+ if (failed >= devalue_download.attempts) return;
40
+ setTimeout(() => with_devalue(fn, failed), backoffAfter(devalue_download, failed));
41
+ });
42
+ }
43
+ /** Every side-channel a read may parse with devalue: props sidecars, the page seed, page facts. */
44
+ const DEVALUE_LANE_SELECTOR = "script[data-ogygia-props]:not([data-og-format=\"json\"]),script[type=\"application/ogygia-page\"]:not([data-og-format=\"json\"]),script[type=\"application/ogygia-page-facts\"]";
45
+ /** Does this document (or fragment) hold a script only devalue can read? */
46
+ function needs_devalue(root) {
47
+ return root.querySelector(DEVALUE_LANE_SELECTOR) !== null;
48
+ }
49
+ export { devalue, load_devalue, needs_devalue, with_devalue };
@@ -0,0 +1 @@
1
+ export declare function disclose_version(): void;
@@ -0,0 +1,14 @@
1
+ // src/runtime/disclose-version.ts
2
+ /**
3
+ * WHICH OGYGIA IS THIS PAGE ON: the runtime adds its version to `window.__ogygia.v`, a Set, as
4
+ * Svelte does with `window.__svelte.v`. A Set because a page can run more than one ogygia (a
5
+ * federated fragment brings its own app's runtime). The full version, not just the major: telling
6
+ * one release line from another is the point. The plugin defines it from the installed package.
7
+ */
8
+ const VERSION = typeof __OGYGIA_VERSION__ !== "undefined" ? __OGYGIA_VERSION__ : null;
9
+ function disclose_version() {
10
+ if (typeof window === "undefined" || !VERSION) return;
11
+ const own = window;
12
+ ((own.__ogygia ??= {}).v ??= /* @__PURE__ */ new Set()).add(VERSION);
13
+ }
14
+ export { disclose_version };
@@ -0,0 +1,4 @@
1
+ import { Scope } from "@ogygia/machine";
2
+ /** Take the parked requests over, once (the global is emptied): each the flight at its address,
3
+ * with `visit` its first caller. */
4
+ export declare function adopt_early_holes(visit: Scope): void;
@@ -0,0 +1,40 @@
1
+ import { EARLY_HOLES_KEY } from "../early-holes.js";
2
+ import { frameAddress } from "../frame.js";
3
+ import { ensure, hold_refusal, renew } from "./frame-store.js";
4
+ import { read_hole } from "./hole-fetch.js";
5
+ import { scope } from "@ogygia/machine";
6
+ // src/runtime/early-holes.ts
7
+ /**
8
+ * THE PAGE'S EARLY HOLE REQUESTS, TAKEN OVER (../early-holes.ts). The head started each `load`
9
+ * hole's request during the parse and parked it on one global; at boot each becomes the flight at
10
+ * its frame-store address, its response read the one way every hole answer is (./hole-fetch.ts
11
+ * `read_hole`). The hole's own wake joins that flight, a batch finds the address in flight and
12
+ * leaves it out, and the response is read once. The flight's first caller is the page visit, and
13
+ * the flight's signal cancels the parked request: one nobody joined is cancelled with the visit.
14
+ * Part of the frames feature (./frames.ts): no hole, no code.
15
+ */
16
+ const PARKED = Symbol.for(EARLY_HOLES_KEY);
17
+ /** Take the parked requests over, once (the global is emptied): each the flight at its address,
18
+ * with `visit` its first caller. */
19
+ function adopt_early_holes(visit) {
20
+ const host = globalThis;
21
+ const parked = host[PARKED];
22
+ if (!parked) return;
23
+ delete host[PARKED];
24
+ for (const { url, response, controller } of parked) {
25
+ const address = frameAddress(url);
26
+ ensure(address, async (signal) => {
27
+ const tie = scope();
28
+ tie.on(signal, "abort", () => controller.abort(signal.reason));
29
+ try {
30
+ return await read_hole(url, response, {
31
+ signal,
32
+ renewed: (fresh) => renew(address, fresh)
33
+ });
34
+ } finally {
35
+ tie.close();
36
+ }
37
+ }, { signal: visit.signal }).catch((err) => hold_refusal(address, err));
38
+ }
39
+ }
40
+ export { adopt_early_holes };
@@ -1,3 +1,6 @@
1
+ import { import_file } from "./import-again.js";
2
+ import { rollover } from "../policies.js";
3
+ import { STALE, backoffAfter, flights, scope } from "@ogygia/machine";
1
4
  // src/runtime/entry-locations.ts
2
5
  /**
3
6
  * ENTRY IDENTITY → LOCATION, the browser's half (server/entry-location.ts is the server's).
@@ -54,7 +57,19 @@ function entry_location(entry, base) {
54
57
  * is not kept: the next identity-only load asks again (a blip must not cost the page every later one).
55
58
  */
56
59
  const BUILD_MAP_ATTR = "data-og-locations";
57
- let build_map = null;
60
+ /** The map's read: whether it filled the locations. One read at a time, kept once it did. */
61
+ const build_map = flights({
62
+ name: "build location map",
63
+ run: (url) => fetch(url).then((res) => res.ok ? res.json() : null).catch(() => null).then((map) => {
64
+ if (!map) return false;
65
+ for (const stable in map) {
66
+ const identity = absolute(stable, url);
67
+ if (!locations.has(identity)) locations.set(identity, absolute(map[stable], url));
68
+ }
69
+ return true;
70
+ }),
71
+ keep: (filled) => filled ? Infinity : 0
72
+ });
58
73
  /**
59
74
  * Pin the build map's address to the page that wrote it, as `pin_region_urls` pins a region's: with
60
75
  * Kit's default `paths.relative` it is written relative to that page, and it is read lazily — after a
@@ -68,22 +83,10 @@ function pin_build_map() {
68
83
  if (script && href) script.setAttribute(BUILD_MAP_ATTR, absolute(href));
69
84
  }
70
85
  function read_build_map() {
71
- if (build_map) return build_map;
72
86
  if (typeof document === "undefined") return null;
73
87
  const href = document.querySelector(`script[${BUILD_MAP_ATTR}]`)?.getAttribute(BUILD_MAP_ATTR);
74
88
  if (!href) return null;
75
- const url = absolute(href);
76
- const read = fetch(url).then((res) => res.ok ? res.json() : null).catch(() => null).then((map) => {
77
- if (!map) {
78
- if (build_map === read) build_map = null;
79
- return;
80
- }
81
- for (const stable in map) {
82
- const identity = absolute(stable, url);
83
- if (!locations.has(identity)) locations.set(identity, absolute(map[stable], url));
84
- }
85
- });
86
- return build_map = read;
89
+ return build_map.get(absolute(href));
87
90
  }
88
91
  /** The region attributes that hold a URL: its identity, its location, its hole's endpoint. */
89
92
  const REGION_URL_ATTRS = [
@@ -129,7 +132,11 @@ function island_entry_of(el) {
129
132
  * that kept it anyway), read once per document per app. Only an ogygia identity has one: the dev
130
133
  * server's module URLs never ask.
131
134
  */
132
- const current_maps = /* @__PURE__ */ new Map();
135
+ const current_maps = flights({
136
+ name: "current build map",
137
+ run: (file) => fetch(file, { cache: "no-cache" }).then((res) => res.ok ? res.json() : null).catch(() => null),
138
+ keep: (map) => map ? Infinity : 0
139
+ });
133
140
  /** A build's island identity (`…/<appDir>/ogygia/og-region.<id>.js`): a name, no file. */
134
141
  function is_build_identity(entry) {
135
142
  return absolute(entry).includes("/ogygia/og-region.");
@@ -145,22 +152,11 @@ async function current_location(entry) {
145
152
  const identity = absolute(entry);
146
153
  const file = current_map_file(identity);
147
154
  if (!file) return null;
148
- let map = current_maps.get(file);
149
- if (!map) {
150
- map = fetch(file, { cache: "no-cache" }).then((res) => res.ok ? res.json() : null).catch(() => null);
151
- current_maps.set(file, map);
152
- }
153
- const pairs = await map;
154
- if (!pairs) {
155
- forget_current_map(file, map);
156
- return;
157
- }
155
+ const pairs = await current_maps.get(file);
156
+ if (!pairs || pairs === STALE) return void 0;
158
157
  for (const name in pairs) if (absolute(name, file) === identity) return absolute(pairs[name], file);
159
158
  return null;
160
159
  }
161
- function forget_current_map(file, map = current_maps.get(file)) {
162
- if (current_maps.get(file) === map) current_maps.delete(file);
163
- }
164
160
  /** The host says this file is not there (it does not say so of a file that failed another way). */
165
161
  function is_gone(url) {
166
162
  return fetch(url, {
@@ -168,13 +164,6 @@ function is_gone(url) {
168
164
  cache: "no-store"
169
165
  }).then((res) => res.status === 404 || res.status === 410, () => false);
170
166
  }
171
- /** How long a recovery waits before each ask of the current map: a rollover settles in seconds. */
172
- const RECOVERY_WAITS = [
173
- 0,
174
- 1e3,
175
- 3e3,
176
- 9e3
177
- ];
178
167
  /**
179
168
  * The current build's file for a build's identity whose file just failed to load. A deploy's rollover
180
169
  * is exactly when this is asked, so an answer that cannot be the current one is asked again while it
@@ -184,12 +173,20 @@ const RECOVERY_WAITS = [
184
173
  */
185
174
  async function recover_location(entry, failed) {
186
175
  let current;
187
- for (const wait of RECOVERY_WAITS) {
188
- if (wait) await new Promise((resolve) => setTimeout(resolve, wait));
189
- current = await current_location(entry);
190
- if (current === void 0) continue;
191
- if (current !== failed || !await is_gone(failed)) break;
192
- forget_current_map(current_map_file(absolute(entry)));
176
+ const waits = scope();
177
+ try {
178
+ for (let n = 1; n <= rollover.attempts; n++) {
179
+ if (n > 1) {
180
+ const wait = backoffAfter(rollover, n - 1);
181
+ await waits.task(() => new Promise((resolve) => waits.timeout(resolve, wait)));
182
+ }
183
+ current = await current_location(entry);
184
+ if (current === void 0) continue;
185
+ if (current !== failed || !await is_gone(failed)) break;
186
+ current_maps.drop(current_map_file(absolute(entry)));
187
+ }
188
+ } finally {
189
+ waits.close();
193
190
  }
194
191
  return current ?? null;
195
192
  }
@@ -207,17 +204,11 @@ function import_entry(entry, url = entry_location(entry) ?? entry) {
207
204
  });
208
205
  }
209
206
  function load_entry(entry, url) {
210
- return import(
211
- /* @vite-ignore */
212
- url
213
- ).catch(async (error) => {
207
+ return import_file(url).catch(async (error) => {
214
208
  const failed = absolute(url);
215
209
  const current = is_build_identity(entry) ? await recover_location(entry, failed) : absolute(entry);
216
210
  if (!current || current === failed) throw error;
217
- const retry = import(
218
- /* @vite-ignore */
219
- current
220
- );
211
+ const retry = import_file(current);
221
212
  const report = (recovered) => fallback_reporter?.({
222
213
  entry,
223
214
  src: url,
@@ -1,8 +1 @@
1
- /**
2
- * THE MEMBRANE for foreign islands (fragment federation) — a pure module (no DOM, no virtual
3
- * imports) so it is unit-testable and safe in any bundle. A FOREIGN-origin island's props parse
4
- * with wire revivers OFF: reviving a wired ref would construct a class instance from THIS
5
- * build's codec and hand it to ANOTHER build's svelte — the exact live-value crossing the
6
- * boundary laws forbid. A ref in a foreign sidecar fails HERE, loudly, at the boundary.
7
- */
8
1
  export declare function foreign_region_prop_revivers(): Record<string, (d: never) => unknown>;
@@ -1,3 +1,4 @@
1
+ import { OgygiaError } from "../messages/lazy.js";
1
2
  // src/runtime/foreign-props.ts
2
3
  /**
3
4
  * THE MEMBRANE for foreign islands (fragment federation) — a pure module (no DOM, no virtual
@@ -8,7 +9,7 @@
8
9
  */
9
10
  function foreign_region_prop_revivers() {
10
11
  return { OgygiaRef: () => {
11
- throw new Error("[ogygia] a wired class crossed a build boundary: foreign (stitched-fragment) island props must be plain data — wired live objects cannot reunite across two svelte runtimes. Pass the underlying data instead.");
12
+ throw new OgygiaError("wired_class_foreign");
12
13
  } };
13
14
  }
14
15
  export { foreign_region_prop_revivers };
@@ -4,7 +4,7 @@ export declare function install(): void;
4
4
  export declare function snapshot_forms(root: ParentNode, pathKey: string): void;
5
5
  /**
6
6
  * Arm restoration for `pathKey` on the freshly-connected page. Restores already-hydrated islands
7
- * immediately and hooks `ogygia:hydrated` for those still waking. Auto-detaches after a short
8
- * window (the page's islands have all hydrated by then, or never will).
7
+ * immediately and hooks `ogygia:hydrated` for those still waking, for this page's restore window
8
+ * (see `restore_window`).
9
9
  */
10
10
  export declare function arm_form_restore(pathKey: string): void;
@@ -1,5 +1,6 @@
1
1
  import { element_address, resolve_address } from "./interaction.js";
2
2
  import { slots } from "./slots.js";
3
+ import { machine } from "@ogygia/machine";
3
4
  // src/runtime/form-continuity.ts
4
5
  /**
5
6
  * CONTINUITY — ambient island form-field survival across SPA navigation.
@@ -25,9 +26,32 @@ const VALUE_SELECTOR = "input, textarea, select";
25
26
  const MAX_PAGES = 20;
26
27
  /** pathKey → the fields a visitor changed on that page (in this tab session). */
27
28
  const store = /* @__PURE__ */ new Map();
29
+ /**
30
+ * THE RESTORE WINDOW, one per page visit: `open` while a page's saved fields wait for its islands
31
+ * to hydrate. It closes on its own after RESTORE_WINDOW_MS, when the page is left (the next
32
+ * navigation's snapshot), or when another page's restore opens; leaving `open` removes its
33
+ * listeners, so a page's values never reach the next page's islands.
34
+ */
35
+ const restore_window = machine()({
36
+ name: "form-restore",
37
+ initial: "idle",
38
+ steps: {
39
+ idle: ["open"],
40
+ open: ["idle"]
41
+ },
42
+ after: { open: {
43
+ ms: 5e3,
44
+ to: "idle"
45
+ } }
46
+ }).create();
47
+ /** The page being left (or another page's restore opening) ends the open window. */
48
+ function close_restore() {
49
+ if (restore_window.now.kind === "open") restore_window.go("idle");
50
+ }
28
51
  const is_checkable = (f) => f.type === "checkbox" || f.type === "radio";
29
52
  /** Snapshot changed island fields in `root` under `pathKey`. Call BEFORE the body is swapped out. */
30
53
  function snapshot_forms(root, pathKey) {
54
+ close_restore();
31
55
  const regions = Array.from(root.querySelectorAll("ogygia-region"));
32
56
  const recs = [];
33
57
  regions.forEach((region, ri) => {
@@ -76,10 +100,11 @@ function apply_region(region, ri, recs) {
76
100
  }
77
101
  /**
78
102
  * Arm restoration for `pathKey` on the freshly-connected page. Restores already-hydrated islands
79
- * immediately and hooks `ogygia:hydrated` for those still waking. Auto-detaches after a short
80
- * window (the page's islands have all hydrated by then, or never will).
103
+ * immediately and hooks `ogygia:hydrated` for those still waking, for this page's restore window
104
+ * (see `restore_window`).
81
105
  */
82
106
  function arm_form_restore(pathKey) {
107
+ close_restore();
83
108
  const recs = store.get(pathKey);
84
109
  if (!recs) return;
85
110
  let regions = Array.from(document.querySelectorAll("ogygia-region"));
@@ -97,11 +122,8 @@ function arm_form_restore(pathKey) {
97
122
  if (ri >= 0) apply_region(region, ri, recs);
98
123
  };
99
124
  const on_answer = () => regions = Array.from(document.querySelectorAll("ogygia-region"));
100
- document.addEventListener("ogygia:hydrated", on_hydrated, true);
101
- document.addEventListener("ogygia:server", on_answer, true);
102
- setTimeout(() => {
103
- document.removeEventListener("ogygia:hydrated", on_hydrated, true);
104
- document.removeEventListener("ogygia:server", on_answer, true);
105
- }, 5e3);
125
+ restore_window.go("open");
126
+ restore_window.scope.on(document, "ogygia:hydrated", on_hydrated, true);
127
+ restore_window.scope.on(document, "ogygia:server", on_answer, true);
106
128
  }
107
129
  export { arm_form_restore, install, snapshot_forms };
@@ -1,3 +1,4 @@
1
+ import { Scope } from "@ogygia/machine";
1
2
  /** How the batch that carried `endpoint` timed it, or undefined (it rode no batch). */
2
3
  export declare function batch_times(endpoint: string): {
3
4
  left: number;
@@ -20,8 +21,13 @@ export declare function read_parcel(buf: string, from: number): Parcel | null;
20
21
  * do — all are minted against the app's one islands endpoint, `handle({ endpoint })`, read here off
21
22
  * the first URL). Frames apply as they land; the promise resolves when
22
23
  * the response ends.
24
+ *
25
+ * `visit`: the scope of the page whose holes these are (the page on screen by default; a navigation
26
+ * hands the next page's, runtime/session.ts `PageVisit`). The batch is its: the visit ending stops
27
+ * the request and lets go of its body. Every other end lets go of the body too (the end mark, a
28
+ * stall, a refusal), and the holes it did not carry fetch on their own.
23
29
  */
24
- export declare function streamFrames(endpoints: string[], kind?: "page" | "nav"): Promise<void>;
30
+ export declare function streamFrames(endpoints: string[], kind?: "page" | "nav", visit?: Scope): Promise<void>;
25
31
  /**
26
32
  * THE PAGE'S HOLES, ONE REQUEST. Holes that start fetching together (the above-the-fold `visible`
27
33
  * ones fire in one observer callback; `idle` and media ones in one task) join here
@@ -1,7 +1,10 @@
1
1
  import { frameAddress } from "../frame.js";
2
+ import { stream } from "../policies.js";
2
3
  import { is_idle, release, reserve, ticket, write } from "./frame-store.js";
4
+ import { runtime_session } from "./session.js";
3
5
  import { emit } from "../events/bus.js";
4
6
  import { beacon_hole_batch_missed } from "./beacon.js";
7
+ import { STALE, machine } from "@ogygia/machine";
5
8
  // src/runtime/frame-nav.ts
6
9
  const DEVTOOLS = typeof __OGYGIA_DEVTOOLS__ !== "undefined" ? __OGYGIA_DEVTOOLS__ : false;
7
10
  const BEACON = typeof __OGYGIA_BEACON__ !== "undefined" ? __OGYGIA_BEACON__ : true;
@@ -55,6 +58,43 @@ function read_parcel(buf, from) {
55
58
  end
56
59
  };
57
60
  }
61
+ /** A batch stream quiet this long is given up, and the holes still waiting on it fetch on their own:
62
+ * the `stream` policy's deadline (../policies.ts, where its reason lives). */
63
+ const STALL_MS = stream.deadline;
64
+ /**
65
+ * A BATCH STREAM'S LIFE, one machine: `sending` (the POST, until its answer starts), `reading` (each
66
+ * read that brings bytes is a new visit, so the quiet time starts again), `ended`. Quiet for
67
+ * STALL_MS in either, it steps to `stalled` on its own: the request is aborted and every reservation
68
+ * it did not fill is released, as at any other end.
69
+ */
70
+ const batch_stream = machine()({
71
+ name: "batch-stream",
72
+ initial: "sending",
73
+ steps: {
74
+ sending: [
75
+ "reading",
76
+ "stalled",
77
+ "ended"
78
+ ],
79
+ reading: [
80
+ "reading",
81
+ "stalled",
82
+ "ended"
83
+ ],
84
+ stalled: [],
85
+ ended: []
86
+ },
87
+ after: {
88
+ sending: {
89
+ ms: STALL_MS,
90
+ to: "stalled"
91
+ },
92
+ reading: {
93
+ ms: STALL_MS,
94
+ to: "stalled"
95
+ }
96
+ }
97
+ });
58
98
  function sigOf(endpoint) {
59
99
  const q = endpoint.indexOf("?");
60
100
  return q === -1 ? null : new URLSearchParams(endpoint.slice(q + 1)).get("sig");
@@ -64,8 +104,13 @@ function sigOf(endpoint) {
64
104
  * do — all are minted against the app's one islands endpoint, `handle({ endpoint })`, read here off
65
105
  * the first URL). Frames apply as they land; the promise resolves when
66
106
  * the response ends.
107
+ *
108
+ * `visit`: the scope of the page whose holes these are (the page on screen by default; a navigation
109
+ * hands the next page's, runtime/session.ts `PageVisit`). The batch is its: the visit ending stops
110
+ * the request and lets go of its body. Every other end lets go of the body too (the end mark, a
111
+ * stall, a refusal), and the holes it did not carry fetch on their own.
67
112
  */
68
- async function streamFrames(endpoints, kind = "nav") {
113
+ async function streamFrames(endpoints, kind = "nav", visit = runtime_session.visit.scope) {
69
114
  if (!endpoints.length) return;
70
115
  const first = endpoints[0];
71
116
  const q = first.indexOf("?");
@@ -96,31 +141,45 @@ async function streamFrames(endpoints, kind = "nav") {
96
141
  let status = 0;
97
142
  let refused;
98
143
  let final_url;
144
+ const stream = batch_stream.create();
145
+ const batch_scope = visit.child();
146
+ stream.wait("stalled").then((s) => s !== STALE && batch_scope.close());
99
147
  try {
100
148
  let res;
101
149
  try {
102
- res = await fetch(path, {
150
+ res = await stream.scope.task(() => fetch(path, {
103
151
  method: "POST",
104
152
  credentials: "same-origin",
105
153
  headers: { "content-type": "application/json" },
106
- body: JSON.stringify(endpoints)
107
- });
154
+ body: JSON.stringify(endpoints),
155
+ signal: batch_scope.signal
156
+ }));
108
157
  } catch {
109
158
  return;
110
159
  }
160
+ if (res === STALE) return;
161
+ stream.go("reading");
111
162
  status = res.status;
112
163
  if ((DEVTOOLS || BEACON) && res.redirected) {
113
164
  refused = "redirected";
114
165
  final_url = res.url;
115
166
  }
116
- if (!res.ok || !res.body) return;
167
+ if (!res.ok || !res.body) {
168
+ res.body?.cancel().catch(() => {});
169
+ return;
170
+ }
117
171
  const reader = res.body.getReader();
172
+ batch_scope.defer(() => void reader.cancel().catch(() => {}));
118
173
  const decoder = new TextDecoder();
119
174
  let buffer = "";
120
175
  let parcels = false;
121
176
  for (;;) {
122
- const { done, value } = await reader.read();
177
+ const read = await stream.scope.task(() => reader.read());
178
+ if (read === STALE) return;
179
+ const { done, value } = read;
180
+ if (batch_scope.closed) return;
123
181
  if (done) break;
182
+ stream.go("reading");
124
183
  buffer += decoder.decode(value, { stream: true });
125
184
  let consumed = 0;
126
185
  for (let p = read_parcel(buffer, 0); p; p = read_parcel(buffer, consumed)) {
@@ -160,8 +219,10 @@ async function streamFrames(endpoints, kind = "nav") {
160
219
  final_url = res.url;
161
220
  }
162
221
  } finally {
222
+ stream.stop();
223
+ batch_scope.close();
163
224
  for (const a of reserved) release(a);
164
- if (BEACON && delivered < reserved.length) beacon_hole_batch_missed({
225
+ if (BEACON && delivered < reserved.length && !visit.closed) beacon_hole_batch_missed({
165
226
  sent: reserved.length,
166
227
  delivered,
167
228
  status,
@@ -205,21 +266,26 @@ function join_batch(endpoint) {
205
266
  flushed = new Promise((resolve) => queueMicrotask(() => {
206
267
  const list = queued;
207
268
  queued = null;
208
- const fresh = [];
209
- const seen = /* @__PURE__ */ new Set();
210
- const hinted = /* @__PURE__ */ new Set();
211
- for (const l of document.querySelectorAll("link[rel=\"preload\"][as=\"fetch\"]")) if (l.href) hinted.add(l.href);
212
- for (const e of list) {
213
- const a = frameAddress(e);
214
- if (seen.has(a) || hinted.size && hinted.has(new URL(e, document.baseURI).href) || !is_idle(a)) continue;
215
- seen.add(a);
216
- fresh.push(e);
269
+ try {
270
+ gather(list);
271
+ } finally {
272
+ resolve();
217
273
  }
218
- if (fresh.length >= 2) for (let i = 0; i < fresh.length; i += MAX_BATCH) streamFrames(fresh.slice(i, i + MAX_BATCH), "page");
219
- resolve();
220
274
  }));
221
275
  }
222
276
  queued.push(endpoint);
223
277
  return flushed;
224
278
  }
279
+ /** The holes that joined in one task: the ones a batch can carry go out in batches of MAX_BATCH. */
280
+ function gather(list) {
281
+ const fresh = [];
282
+ const seen = /* @__PURE__ */ new Set();
283
+ for (const e of list) {
284
+ const a = frameAddress(e);
285
+ if (seen.has(a) || !is_idle(a)) continue;
286
+ seen.add(a);
287
+ fresh.push(e);
288
+ }
289
+ if (fresh.length >= 2) for (let i = 0; i < fresh.length; i += MAX_BATCH) streamFrames(fresh.slice(i, i + MAX_BATCH), "page");
290
+ }
225
291
  export { batch_times, join_batch, read_parcel, streamFrames };
@@ -1,10 +1,18 @@
1
1
  import { Frame } from "../frame.js";
2
+ import { Stale } from "@ogygia/machine";
2
3
  export type FrameFetcher = (signal: AbortSignal) => Promise<string>;
4
+ /** A refused answer nobody asked for yet (the page's early start: ./early-holes.ts), held at its
5
+ * address for the first caller that asks there (see `Entry.refused`). Any other failure is not. */
6
+ export declare function hold_refusal(a: string, err: unknown): void;
3
7
  /** Applied content for an address, if any. */
4
8
  export declare function peek(a: string): {
5
9
  v: number;
6
10
  html: string;
7
11
  } | null;
12
+ /** A renewal at this address answered `fresh`, the capability its elements fetch with from now on. */
13
+ export declare function renew(a: string, fresh: string): void;
14
+ /** The fresh capability a renewal at this address answered, if any. */
15
+ export declare function renewal(a: string): string | null;
8
16
  /** Nothing applied and nothing in flight at this address (a batch may take it on). */
9
17
  export declare function is_idle(a: string): boolean;
10
18
  /** Next request ticket for an address. External channels (streaming, mutations) use this too. */
@@ -16,16 +24,17 @@ export declare function ticket(a: string): number;
16
24
  export declare function write(f: Frame): boolean;
17
25
  /**
18
26
  * Reserve an address whose content is arriving via an EXTERNAL batch stream (single-flight navigation). A region
19
- * binder that mounts before its frame lands calls `ensure()`, sees this reservation, and JOINS it
27
+ * binder that mounts before its frame lands calls `ensure()`, finds the batch's flight there, and JOINS it
20
28
  * instead of starting its own fetch — so a navigation that pulls N regions makes ONE batch request,
21
- * not N. `write()` fulfils the reservation when the frame arrives; `release()` fails it if the batch
22
- * ends without one (the binder then falls back to its own fetch). No-op if content or a fetch is
23
- * already present.
29
+ * not N. `write()` fulfils it when the frame arrives; `release()` fails it if the batch ends without
30
+ * one (the binder then falls back to its own fetch). The flight is the batch's: a binder leaving never
31
+ * ends it. No-op if content or a fetch is already present.
24
32
  */
25
33
  export declare function reserve(a: string): void;
26
34
  /**
27
- * Fail an unfulfilled reservation (the batch ended and no frame carried this address). Joined binders
28
- * reject out of `ensure()` and retry with their own fetch. No-op once content has arrived.
35
+ * Fail an unfulfilled batch part (the batch ended and no frame was written at this address, by it or
36
+ * by anyone). Joined binders reject out of `ensure()` and retry with their own fetch. No-op once a
37
+ * frame has landed (it settled the part).
29
38
  */
30
39
  export declare function release(a: string): void;
31
40
  /** The error a joined reservation rejects with when its batch ended without its frame: the binder's
@@ -42,21 +51,21 @@ export declare function subscribe(a: string, cb: (f: Frame) => void): () => void
42
51
  * Ensure an address has content, deduping concurrent callers onto one fetch.
43
52
  *
44
53
  * - Content already applied (and not `force`): resolves immediately with it.
45
- * - A fetch is in flight (and not `force`): joins it — N regions, ONE request.
54
+ * - A fetch (or a batch's part) is in flight (and not `force`): joins it — N regions, ONE request.
46
55
  * - Otherwise starts `fetcher` with a fresh ticket; on success the response is WRITTEN (subject to
47
56
  * the staleness check) and the resolved value is whatever the store now holds.
48
- * - `force` (SWR revalidate) always starts a new fetch; the ticket ordering still guarantees an
49
- * older in-flight response can't overwrite it.
57
+ * - `force` (SWR revalidate) always starts a new fetch, and ends the one out before it: whoever
58
+ * waited on that one joins the new one. The ticket ordering still guarantees an older response
59
+ * can't overwrite a newer one.
50
60
  *
51
- * Rejections propagate to every joined caller; retry policy stays with the caller (the element).
61
+ * `signal` is the caller's: when it aborts, this caller alone gets `STALE` at once, and the shared
62
+ * fetch is aborted only once its LAST caller left — one unmounting element must not kill a fetch
63
+ * its twin still awaits. Rejections propagate to every joined caller; retry policy stays with the
64
+ * caller (the element).
52
65
  */
53
66
  export declare function ensure(a: string, fetcher: FrameFetcher, opts?: {
54
67
  force?: boolean;
55
- }): Promise<string>;
56
- /**
57
- * A waiter is leaving (its element disconnected). Aborts the shared fetch only when the LAST
58
- * waiter leaves — one unmounting element must not kill a fetch its twin still awaits.
59
- */
60
- export declare function abandon(a: string): void;
68
+ signal?: AbortSignal;
69
+ }): Promise<string | Stale>;
61
70
  /** Test/HMR hook — drop everything. */
62
71
  export declare function _reset(): void;