@ogygia/core 0.12.3 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (226) hide show
  1. package/dist/LateIsland.svelte +3 -2
  2. package/dist/LateIslandAwait.svelte +6 -2
  3. package/dist/Region.svelte +22 -33
  4. package/dist/boundary.d.ts +0 -2
  5. package/dist/boundary.js +8 -6
  6. package/dist/compiler/debarrel/barrel.d.ts +15 -8
  7. package/dist/compiler/debarrel/barrel.js +92 -45
  8. package/dist/compiler/debarrel/plugin.js +41 -17
  9. package/dist/compiler/dev/dev-hmr.d.ts +3 -0
  10. package/dist/compiler/dev/dev-hmr.js +3 -0
  11. package/dist/compiler/driver.js +11 -4
  12. package/dist/compiler/flags.js +21 -7
  13. package/dist/compiler/fouc-css.js +5 -1
  14. package/dist/compiler/kit.js +5 -1
  15. package/dist/compiler/link/build-output.js +12 -3
  16. package/dist/compiler/macros/bake.js +18 -4
  17. package/dist/compiler/macros/dollar.js +6 -1
  18. package/dist/compiler/macros/source.js +6 -1
  19. package/dist/compiler/macros/store.js +10 -2
  20. package/dist/compiler/macros/wire.js +6 -1
  21. package/dist/compiler/parse/scan.js +10 -3
  22. package/dist/compiler/region/transform.js +188 -74
  23. package/dist/context-bridge.js +13 -5
  24. package/dist/context.d.ts +9 -0
  25. package/dist/context.js +30 -1
  26. package/dist/document.d.ts +1 -1
  27. package/dist/document.js +7 -1
  28. package/dist/early-holes-head.d.ts +5 -0
  29. package/dist/early-holes-head.js +20 -0
  30. package/dist/early-holes.d.ts +34 -0
  31. package/dist/early-holes.js +51 -0
  32. package/dist/entry-hydrate.js +9 -12
  33. package/dist/events/bus.js +7 -2
  34. package/dist/events/schema.d.ts +2 -2
  35. package/dist/fn-transport.js +4 -3
  36. package/dist/hooks.js +94 -163
  37. package/dist/insights/page-assets.js +11 -3
  38. package/dist/insights/page-insights.d.ts +0 -2
  39. package/dist/insights/page-insights.js +14 -10
  40. package/dist/insights/session-insights.js +2 -2
  41. package/dist/internal-instruments.d.ts +3 -1
  42. package/dist/internal-instruments.js +3 -1
  43. package/dist/internal-server.d.ts +3 -2
  44. package/dist/internal-server.js +3 -2
  45. package/dist/internal.d.ts +1 -2
  46. package/dist/internal.js +1 -2
  47. package/dist/keep-fallback.d.ts +0 -2
  48. package/dist/keep-fallback.js +4 -5
  49. package/dist/live-transport.js +3 -2
  50. package/dist/messages/build.d.ts +585 -0
  51. package/dist/messages/build.js +800 -0
  52. package/dist/messages/core.d.ts +464 -0
  53. package/dist/messages/core.js +843 -0
  54. package/dist/messages/define.d.ts +62 -0
  55. package/dist/messages/define.js +49 -0
  56. package/dist/messages/index.d.ts +6 -0
  57. package/dist/messages/index.js +6 -0
  58. package/dist/messages/island.d.ts +4 -0
  59. package/dist/messages/island.js +34 -0
  60. package/dist/messages/lazy.d.ts +3 -0
  61. package/dist/messages/lazy.js +22 -0
  62. package/dist/messages/prose.d.ts +15 -0
  63. package/dist/messages/prose.js +43 -0
  64. package/dist/messages/server.d.ts +142 -0
  65. package/dist/messages/server.js +246 -0
  66. package/dist/nested.d.ts +3 -0
  67. package/dist/nested.js +8 -1
  68. package/dist/plugin/css.js +14 -13
  69. package/dist/plugin/facts.d.ts +2 -0
  70. package/dist/plugin/facts.js +10 -6
  71. package/dist/plugin/hooks.d.ts +7 -17
  72. package/dist/plugin/hooks.js +32 -4
  73. package/dist/plugin/index.d.ts +9 -2
  74. package/dist/plugin/request-scope.d.ts +20 -0
  75. package/dist/plugin/request-scope.js +149 -0
  76. package/dist/plugin/server.d.ts +20 -2
  77. package/dist/plugin/server.js +98 -28
  78. package/dist/plugin/testing.d.ts +6 -0
  79. package/dist/plugin/testing.js +14 -2
  80. package/dist/policies.d.ts +21 -0
  81. package/dist/policies.js +66 -0
  82. package/dist/preference.js +6 -1
  83. package/dist/preload.js +6 -9
  84. package/dist/ref-watch.d.ts +0 -10
  85. package/dist/ref-watch.js +14 -1
  86. package/dist/ref.js +17 -5
  87. package/dist/region-snippet.js +9 -3
  88. package/dist/region.js +2 -1
  89. package/dist/report-thrown.d.ts +9 -0
  90. package/dist/report-thrown.js +15 -0
  91. package/dist/rules.d.ts +17 -0
  92. package/dist/rules.js +57 -0
  93. package/dist/runtime/beacon.d.ts +0 -4
  94. package/dist/runtime/beacon.js +58 -43
  95. package/dist/runtime/boot-link.js +19 -2
  96. package/dist/runtime/client-hooks.js +2 -1
  97. package/dist/runtime/concurrency.d.ts +16 -5
  98. package/dist/runtime/concurrency.js +123 -7
  99. package/dist/runtime/core.d.ts +3 -1
  100. package/dist/runtime/core.js +718 -384
  101. package/dist/runtime/devalue-lane.d.ts +13 -0
  102. package/dist/runtime/devalue-lane.js +49 -0
  103. package/dist/runtime/disclose-version.d.ts +1 -0
  104. package/dist/runtime/disclose-version.js +14 -0
  105. package/dist/runtime/early-holes.d.ts +4 -0
  106. package/dist/runtime/early-holes.js +40 -0
  107. package/dist/runtime/entry-locations.js +40 -49
  108. package/dist/runtime/foreign-props.d.ts +0 -7
  109. package/dist/runtime/foreign-props.js +2 -1
  110. package/dist/runtime/form-continuity.d.ts +2 -2
  111. package/dist/runtime/form-continuity.js +30 -8
  112. package/dist/runtime/frame-nav.d.ts +7 -1
  113. package/dist/runtime/frame-nav.js +84 -18
  114. package/dist/runtime/frame-store.d.ts +25 -16
  115. package/dist/runtime/frame-store.js +124 -98
  116. package/dist/runtime/frames.js +8 -4
  117. package/dist/runtime/hole-facts.js +6 -7
  118. package/dist/runtime/hole-fetch.d.ts +17 -0
  119. package/dist/runtime/hole-fetch.js +47 -0
  120. package/dist/runtime/hydrate-core.d.ts +4 -4
  121. package/dist/runtime/hydrate-core.js +80 -64
  122. package/dist/runtime/import-again.d.ts +33 -0
  123. package/dist/runtime/import-again.js +217 -0
  124. package/dist/runtime/interaction-replay.d.ts +5 -3
  125. package/dist/runtime/interaction-replay.js +6 -6
  126. package/dist/runtime/interaction.d.ts +14 -3
  127. package/dist/runtime/interaction.js +46 -23
  128. package/dist/runtime/kit-page-thread.svelte.d.ts +2 -1
  129. package/dist/runtime/kit-page-thread.svelte.js +44 -20
  130. package/dist/runtime/lakes.d.ts +1 -1
  131. package/dist/runtime/lakes.js +18 -5
  132. package/dist/runtime/live.d.ts +1 -2
  133. package/dist/runtime/live.js +6 -8
  134. package/dist/runtime/load-scheduler.d.ts +29 -18
  135. package/dist/runtime/load-scheduler.js +236 -136
  136. package/dist/runtime/nav-handle.js +25 -6
  137. package/dist/runtime/navigation.d.ts +24 -0
  138. package/dist/runtime/navigation.js +24 -0
  139. package/dist/runtime/ownership.d.ts +1 -1
  140. package/dist/runtime/page-defer.js +3 -3
  141. package/dist/runtime/parse-html.d.ts +15 -4
  142. package/dist/runtime/parse-html.js +24 -5
  143. package/dist/runtime/pushable-props.svelte.d.ts +21 -0
  144. package/dist/runtime/pushable-props.svelte.js +37 -0
  145. package/dist/runtime/region-endpoint-url.js +4 -6
  146. package/dist/runtime/region-machines.d.ts +111 -0
  147. package/dist/runtime/region-machines.js +97 -0
  148. package/dist/runtime/region-sheets.d.ts +31 -0
  149. package/dist/runtime/region-sheets.js +49 -0
  150. package/dist/runtime/restore-report.d.ts +1 -0
  151. package/dist/runtime/restore-report.js +36 -0
  152. package/dist/runtime/restore.d.ts +5 -2
  153. package/dist/runtime/restore.js +33 -14
  154. package/dist/runtime/router-nav.d.ts +32 -12
  155. package/dist/runtime/router-nav.js +320 -228
  156. package/dist/runtime/router.d.ts +83 -19
  157. package/dist/runtime/router.js +297 -129
  158. package/dist/runtime/rules.d.ts +35 -0
  159. package/dist/runtime/rules.js +137 -0
  160. package/dist/runtime/schedule.d.ts +6 -3
  161. package/dist/runtime/schedule.js +172 -57
  162. package/dist/runtime/seeds.d.ts +5 -1
  163. package/dist/runtime/seeds.js +11 -5
  164. package/dist/runtime/session.d.ts +46 -7
  165. package/dist/runtime/session.js +78 -10
  166. package/dist/runtime/slots.d.ts +53 -20
  167. package/dist/runtime/slots.js +2 -3
  168. package/dist/runtime/soft-refresh.d.ts +16 -0
  169. package/dist/runtime/soft-refresh.js +14 -0
  170. package/dist/runtime/svelte-warnings.d.ts +4 -10
  171. package/dist/runtime/svelte-warnings.js +7 -4
  172. package/dist/runtime/wire-classes.js +21 -8
  173. package/dist/runtime/wire-format.js +6 -3
  174. package/dist/script.js +8 -7
  175. package/dist/seed-ref-reviver.d.ts +0 -9
  176. package/dist/seed-ref-reviver.js +2 -1
  177. package/dist/seed-refs.d.ts +46 -36
  178. package/dist/seed-refs.js +131 -92
  179. package/dist/server/document-tail.d.ts +6 -0
  180. package/dist/server/document-tail.js +7 -1
  181. package/dist/server/early-holes-csp.d.ts +14 -0
  182. package/dist/server/early-holes-csp.js +73 -0
  183. package/dist/server/page-facts.d.ts +4 -1
  184. package/dist/server/page-facts.js +4 -1
  185. package/dist/server/page-seed.d.ts +2 -2
  186. package/dist/server/page-seed.js +104 -11
  187. package/dist/server/page-stream.d.ts +14 -1
  188. package/dist/server/page-stream.js +119 -2
  189. package/dist/server/personal-response.d.ts +0 -5
  190. package/dist/server/personal-response.js +6 -2
  191. package/dist/server/props-wire.js +5 -1
  192. package/dist/server/region-endpoint.js +10 -2
  193. package/dist/server/render-cache.d.ts +24 -20
  194. package/dist/server/render-cache.js +99 -40
  195. package/dist/server/render-page.js +46 -15
  196. package/dist/server/request-stats.d.ts +10 -8
  197. package/dist/server/request-stats.js +20 -12
  198. package/dist/server/rules.d.ts +11 -0
  199. package/dist/server/rules.js +64 -0
  200. package/dist/server/signing-key.js +31 -18
  201. package/dist/server/stream-regions.d.ts +14 -1
  202. package/dist/server/stream-regions.js +38 -2
  203. package/dist/shared-state.js +5 -1
  204. package/dist/shims/app-navigation.js +3 -2
  205. package/dist/shims/page-store.svelte.js +16 -2
  206. package/dist/source.js +7 -2
  207. package/dist/store-transport.js +12 -5
  208. package/dist/transport.js +3 -2
  209. package/dist/vite/index.js +17 -10
  210. package/dist/vite/kit-dirs.js +7 -1
  211. package/dist/vite/options.d.ts +2 -1
  212. package/dist/vite/options.js +11 -20
  213. package/dist/vite/package-files.js +13 -3
  214. package/dist/vite/paths.d.ts +2 -0
  215. package/dist/vite/paths.js +4 -4
  216. package/dist/vite/plugin-api.js +42 -14
  217. package/dist/vite/style-preprocess.js +10 -3
  218. package/package.json +8 -1
  219. package/dist/KeepHost.svelte +0 -19
  220. package/dist/KeepHost.svelte.d.ts +0 -10
  221. package/dist/LiveHost.svelte +0 -19
  222. package/dist/LiveHost.svelte.d.ts +0 -10
  223. package/dist/NestedProvider.svelte +0 -13
  224. package/dist/NestedProvider.svelte.d.ts +0 -8
  225. package/dist/runtime/page-cache.d.ts +0 -23
  226. package/dist/runtime/page-cache.js +0 -74
@@ -1,14 +1,49 @@
1
1
  import { notify, watch, watcher_count } from "../ref-watch.js";
2
+ import { STALE, flights, scope } from "@ogygia/machine";
2
3
  // src/runtime/frame-store.ts
3
4
  /** Frame addresses live in the hub's watcher space under a namespace, so region binders subscribe
4
5
  * through the ONE hub `watch`/`notify` primitive (hub v2 phase W) — the frame store no longer owns
5
- * a parallel subscriber set. Fetch orchestration (ticket, inflight, reserve, evict) stays here. */
6
+ * a parallel subscriber set. */
6
7
  const frame_id = (a) => "frame:" + a;
8
+ /** The name of a refused answer's error (./region-endpoint-url.ts): by name, since the store is one
9
+ * per document across bundle copies, and a copy's class is not another's. */
10
+ const REFUSED = "RegionAnswerRefused";
11
+ /** A refused answer nobody asked for yet (the page's early start: ./early-holes.ts), held at its
12
+ * address for the first caller that asks there (see `Entry.refused`). Any other failure is not. */
13
+ function hold_refusal(a, err) {
14
+ if (err?.name === REFUSED) entry(a).refused = err;
15
+ }
7
16
  /** Entries with no subscribers linger this long (ms) so quick remounts / SPA swaps reuse content. */
8
17
  const EVICT_TTL = 6e4;
9
18
  const GLOBAL_KEY = "__ogygia_frames__";
19
+ const FLIGHTS_KEY = "__ogygia_frame_flights__";
10
20
  const store_host = typeof globalThis !== "undefined" ? globalThis : {};
11
- const entries = store_host[GLOBAL_KEY] ?? (store_host[GLOBAL_KEY] = /* @__PURE__ */ new Map());
21
+ const entries = store_host[GLOBAL_KEY] ??= /* @__PURE__ */ new Map();
22
+ /** The fetches in flight, one per address, shared by every caller of it. Nothing is kept: the
23
+ * versioned store holds what landed. A flight's work aborts once its last caller left. */
24
+ function frame_flights() {
25
+ return store_host[FLIGHTS_KEY] ??= flights.keyed()({
26
+ name: "frames",
27
+ key: ["a"],
28
+ ignore: { work: "one flight per address: a fetch or a batch part out there is joined" },
29
+ keep: 0,
30
+ run: ({ a, work }, signal) => typeof work === "function" ? fetch_frame(a, work, signal) : work
31
+ });
32
+ }
33
+ /** A fetch at an address takes its ticket at its start, and writes what it got (a newer write may
34
+ * have raced past it: the answer is whatever the store then holds). */
35
+ async function fetch_frame(a, fetcher, signal) {
36
+ const t = ticket(a);
37
+ const html = await fetcher(signal);
38
+ if (!signal.aborted) write({
39
+ a,
40
+ v: t,
41
+ html
42
+ });
43
+ return entries.get(a)?.html ?? html;
44
+ }
45
+ /** A fetch or a batch's part is out at this address. */
46
+ const flying = (a) => store_host[FLIGHTS_KEY]?.peek({ a }) !== void 0;
12
47
  function entry(a) {
13
48
  let e = entries.get(a);
14
49
  if (!e) {
@@ -16,28 +51,29 @@ function entry(a) {
16
51
  v: 0,
17
52
  html: null,
18
53
  seq: 0,
19
- inflight: null,
20
- evict: null
54
+ batch: null,
55
+ evict: null,
56
+ renewed: null,
57
+ refused: null
21
58
  };
22
59
  entries.set(a, e);
23
60
  }
24
61
  return e;
25
62
  }
26
63
  /**
27
- * Let an idle entry (no subscribers, no in-flight fetch) linger EVICT_TTL, then drop it. A late binder
64
+ * Let an idle entry (no subscribers, nothing in flight) linger EVICT_TTL, then drop it. A late binder
28
65
  * that mounts inside the window still catches its content (prefetch / batch-stream ahead of the DOM);
29
- * nothing bound means nothing to keep. Idempotent — resets the timer. Both channels that leave an
30
- * entry idle call this: a subscriber unbinding, AND a `write()` that lands where nothing is listening
31
- * (a single-flight mutation for an unmounted region, or a live tick whose address keeps changing).
32
- * Without the write path, those orphan entries would accumulate forever — they never had a subscriber
33
- * to trigger eviction.
66
+ * nothing bound means nothing to keep. Idempotent — resets the timer. Every channel that leaves an
67
+ * entry idle calls this: a subscriber unbinding, a flight that settled, AND a `write()` that lands
68
+ * where nothing is listening (a single-flight mutation for an unmounted region, or a live tick whose
69
+ * address keeps changing). Without the write path, those orphan entries would accumulate forever —
70
+ * they never had a subscriber to trigger eviction.
34
71
  */
35
72
  function schedule_evict(a, e) {
36
- if (watcher_count(frame_id(a)) > 0 || e.inflight) return;
37
- if (e.evict) clearTimeout(e.evict);
38
- e.evict = setTimeout(() => {
39
- const cur = entries.get(a);
40
- if (cur === e && watcher_count(frame_id(a)) === 0 && !cur.inflight) entries.delete(a);
73
+ if (watcher_count(frame_id(a)) > 0 || flying(a)) return;
74
+ e.evict?.close();
75
+ (e.evict = scope()).timeout(() => {
76
+ if (entries.get(a) === e && watcher_count(frame_id(a)) === 0 && !flying(a)) entries.delete(a);
41
77
  }, EVICT_TTL);
42
78
  }
43
79
  /** Applied content for an address, if any. */
@@ -48,10 +84,18 @@ function peek(a) {
48
84
  html: e.html
49
85
  } : null;
50
86
  }
87
+ /** A renewal at this address answered `fresh`, the capability its elements fetch with from now on. */
88
+ function renew(a, fresh) {
89
+ entry(a).renewed = fresh;
90
+ }
91
+ /** The fresh capability a renewal at this address answered, if any. */
92
+ function renewal(a) {
93
+ return entries.get(a)?.renewed ?? null;
94
+ }
51
95
  /** Nothing applied and nothing in flight at this address (a batch may take it on). */
52
96
  function is_idle(a) {
53
97
  const e = entries.get(a);
54
- return !e || e.html == null && !e.inflight;
98
+ return !e || e.html == null && !flying(a);
55
99
  }
56
100
  /** Next request ticket for an address. External channels (streaming, mutations) use this too. */
57
101
  function ticket(a) {
@@ -67,9 +111,10 @@ function write(f) {
67
111
  if (f.v > e.seq) e.seq = f.v;
68
112
  e.v = f.v;
69
113
  e.html = f.html;
70
- const held = e.inflight;
71
- if (held?.settle) {
72
- e.inflight = null;
114
+ e.refused = null;
115
+ const held = e.batch;
116
+ if (held) {
117
+ e.batch = null;
73
118
  held.settle(f.html);
74
119
  }
75
120
  notify(frame_id(f.a), f);
@@ -78,48 +123,43 @@ function write(f) {
78
123
  }
79
124
  /**
80
125
  * Reserve an address whose content is arriving via an EXTERNAL batch stream (single-flight navigation). A region
81
- * binder that mounts before its frame lands calls `ensure()`, sees this reservation, and JOINS it
126
+ * binder that mounts before its frame lands calls `ensure()`, finds the batch's flight there, and JOINS it
82
127
  * instead of starting its own fetch — so a navigation that pulls N regions makes ONE batch request,
83
- * not N. `write()` fulfils the reservation when the frame arrives; `release()` fails it if the batch
84
- * ends without one (the binder then falls back to its own fetch). No-op if content or a fetch is
85
- * already present.
128
+ * not N. `write()` fulfils it when the frame arrives; `release()` fails it if the batch ends without
129
+ * one (the binder then falls back to its own fetch). The flight is the batch's: a binder leaving never
130
+ * ends it. No-op if content or a fetch is already present.
86
131
  */
87
132
  function reserve(a) {
88
133
  const e = entry(a);
89
- if (e.html != null || e.inflight) return;
90
- if (e.evict) {
91
- clearTimeout(e.evict);
92
- e.evict = null;
93
- }
94
- let settle;
95
- let fail;
96
- const promise = new Promise((res, rej) => {
97
- settle = res;
98
- fail = rej;
134
+ if (e.html != null || flying(a)) return;
135
+ e.evict?.close();
136
+ e.evict = null;
137
+ const batch = new Promise((settle, fail) => {
138
+ e.batch = {
139
+ settle,
140
+ fail
141
+ };
99
142
  });
100
- promise.catch(() => {});
101
- e.inflight = {
102
- ticket: ticket(a),
103
- promise,
104
- controller: new AbortController(),
105
- waiters: 0,
106
- settle,
107
- fail
108
- };
143
+ batch.catch(() => {});
144
+ const settled = () => schedule_evict(a, e);
145
+ frame_flights().get({
146
+ a,
147
+ work: batch
148
+ }).then(settled, settled);
109
149
  }
110
150
  /**
111
- * Fail an unfulfilled reservation (the batch ended and no frame carried this address). Joined binders
112
- * reject out of `ensure()` and retry with their own fetch. No-op once content has arrived.
151
+ * Fail an unfulfilled batch part (the batch ended and no frame was written at this address, by it or
152
+ * by anyone). Joined binders reject out of `ensure()` and retry with their own fetch. No-op once a
153
+ * frame has landed (it settled the part).
113
154
  */
114
155
  function release(a) {
115
156
  const e = entries.get(a);
116
- const held = e?.inflight;
117
- if (!e || !held?.fail || e.html != null) return;
118
- e.inflight = null;
157
+ const held = e?.batch;
158
+ if (!e || !held) return;
159
+ e.batch = null;
119
160
  const miss = /* @__PURE__ */ new Error("ogygia batch: no frame for " + a);
120
161
  miss.name = BATCH_MISS;
121
162
  held.fail(miss);
122
- schedule_evict(a, e);
123
163
  }
124
164
  /** The error a joined reservation rejects with when its batch ended without its frame: the binder's
125
165
  * own fetch runs at once (nothing failed — the batch just did not carry it). */
@@ -132,10 +172,8 @@ const BATCH_MISS = "OgygiaBatchMiss";
132
172
  */
133
173
  function subscribe(a, cb) {
134
174
  const e = entry(a);
135
- if (e.evict) {
136
- clearTimeout(e.evict);
137
- e.evict = null;
138
- }
175
+ e.evict?.close();
176
+ e.evict = null;
139
177
  const off = watch(frame_id(a), cb);
140
178
  if (e.html != null) cb({
141
179
  a,
@@ -151,64 +189,52 @@ function subscribe(a, cb) {
151
189
  * Ensure an address has content, deduping concurrent callers onto one fetch.
152
190
  *
153
191
  * - Content already applied (and not `force`): resolves immediately with it.
154
- * - A fetch is in flight (and not `force`): joins it — N regions, ONE request.
192
+ * - A fetch (or a batch's part) is in flight (and not `force`): joins it — N regions, ONE request.
155
193
  * - Otherwise starts `fetcher` with a fresh ticket; on success the response is WRITTEN (subject to
156
194
  * the staleness check) and the resolved value is whatever the store now holds.
157
- * - `force` (SWR revalidate) always starts a new fetch; the ticket ordering still guarantees an
158
- * older in-flight response can't overwrite it.
195
+ * - `force` (SWR revalidate) always starts a new fetch, and ends the one out before it: whoever
196
+ * waited on that one joins the new one. The ticket ordering still guarantees an older response
197
+ * can't overwrite a newer one.
159
198
  *
160
- * Rejections propagate to every joined caller; retry policy stays with the caller (the element).
199
+ * `signal` is the caller's: when it aborts, this caller alone gets `STALE` at once, and the shared
200
+ * fetch is aborted only once its LAST caller left — one unmounting element must not kill a fetch
201
+ * its twin still awaits. Rejections propagate to every joined caller; retry policy stays with the
202
+ * caller (the element).
161
203
  */
162
- function ensure(a, fetcher, opts = {}) {
204
+ async function ensure(a, fetcher, opts = {}) {
205
+ const { signal } = opts;
206
+ const f = frame_flights();
163
207
  const e = entry(a);
164
- if (!opts.force) {
165
- if (e.html != null) return Promise.resolve(e.html);
166
- if (e.inflight) {
167
- e.inflight.waiters++;
168
- return e.inflight.promise;
169
- }
170
- }
171
- const controller = new AbortController();
172
- const t = ticket(a);
173
- const inflight = {
174
- ticket: t,
175
- controller,
176
- waiters: 1,
177
- promise: fetcher(controller.signal).then((html) => {
178
- if (e.inflight === inflight) e.inflight = null;
179
- write({
208
+ let fresh = opts.force === true;
209
+ try {
210
+ for (;;) {
211
+ if (!fresh && e.html != null) return e.html;
212
+ const refused = e.refused;
213
+ e.refused = null;
214
+ if (!fresh && refused !== null) throw refused;
215
+ const html = await f.get({
180
216
  a,
181
- v: t,
182
- html
217
+ work: fetcher
218
+ }, {
219
+ signal,
220
+ fresh
183
221
  });
184
- return e.html ?? html;
185
- }, (err) => {
186
- if (e.inflight === inflight) e.inflight = null;
187
- throw err;
188
- })
189
- };
190
- e.inflight = inflight;
191
- return inflight.promise;
192
- }
193
- /**
194
- * A waiter is leaving (its element disconnected). Aborts the shared fetch only when the LAST
195
- * waiter leaves — one unmounting element must not kill a fetch its twin still awaits.
196
- */
197
- function abandon(a) {
198
- const e = entries.get(a);
199
- if (!e?.inflight) return;
200
- if (--e.inflight.waiters <= 0) {
201
- e.inflight.controller.abort();
202
- e.inflight = null;
222
+ if (html !== STALE || signal?.aborted || frame_flights() !== f) return html;
223
+ fresh = false;
224
+ }
225
+ } finally {
226
+ schedule_evict(a, e);
203
227
  }
204
228
  }
205
229
  /** Test/HMR hook — drop everything. */
206
230
  function _reset() {
207
231
  for (const e of entries.values()) {
208
- if (e.evict) clearTimeout(e.evict);
209
- e.inflight?.fail?.(/* @__PURE__ */ new Error("reset"));
210
- e.inflight?.controller.abort();
232
+ e.evict?.close();
233
+ e.batch?.fail(/* @__PURE__ */ new Error("reset"));
211
234
  }
235
+ const old = store_host[FLIGHTS_KEY];
236
+ store_host[FLIGHTS_KEY] = void 0;
237
+ old?.drop(() => true);
212
238
  entries.clear();
213
239
  }
214
- export { BATCH_MISS, _reset, abandon, ensure, is_idle, peek, release, reserve, subscribe, ticket, write };
240
+ export { BATCH_MISS, _reset, ensure, hold_refusal, is_idle, peek, release, renew, renewal, reserve, subscribe, ticket, write };
@@ -1,23 +1,27 @@
1
1
  import { slots } from "./slots.js";
2
- import { abandon, ensure, subscribe } from "./frame-store.js";
2
+ import { ensure, renew, renewal, subscribe } from "./frame-store.js";
3
3
  import { batch_times, join_batch, streamFrames } from "./frame-nav.js";
4
+ import { adopt_early_holes } from "./early-holes.js";
5
+ import { runtime_session } from "./session.js";
4
6
  // src/runtime/frames.ts
5
7
  /**
6
8
  * frames feature: hand the client frame store to core through the {@link ./slots.js slots} seam.
7
9
  *
8
10
  * Core drives deferred / live / SWR-lake regions off this store (subscribe → apply, ensure →
9
- * single-flight fetch, abandon → cleanup), and the router single-flights a route's deferred holes through
11
+ * single-flight fetch, a caller's signal → its leaving), and the router single-flights a route's deferred holes through
10
12
  * `stream` — but NEITHER may statically import the store, or the ~10 kB store + nav-stream graph
11
13
  * lands in every build, including a plain router app with no such region. The generated runtime entry
12
14
  * installs this feature only when the app actually ships a deferred / live / lake region, so a
13
15
  * static-islands app (and a router app without deferred regions) tree-shakes the whole graph away.
14
16
  */
15
17
  function install() {
18
+ adopt_early_holes(runtime_session.visit.scope);
16
19
  slots.frames = {
17
20
  subscribe,
18
21
  ensure,
19
- abandon,
20
- stream: streamFrames,
22
+ renew,
23
+ renewal,
24
+ stream: (endpoints, visit) => streamFrames(endpoints, "nav", visit),
21
25
  join: join_batch,
22
26
  batch_times
23
27
  };
@@ -1,4 +1,5 @@
1
1
  import { HOLES_SCRIPT_TYPE } from "../holes-record.js";
2
+ import { parse_region_html } from "./parse-html.js";
2
3
  // src/runtime/hole-facts.ts
3
4
  /**
4
5
  * THE HOLES RECORD — what the server minted for each deferred hole on a KIT-HYDRATED document,
@@ -32,17 +33,15 @@ function parse_record(doc) {
32
33
  if (!entry || typeof entry.endpoint !== "string" || !entry.endpoint) continue;
33
34
  map.set(identity, {
34
35
  endpoint: entry.endpoint,
35
- sidecar: typeof entry.sidecar === "string" && entry.sidecar ? sidecar_element(doc, entry.sidecar) : null
36
+ sidecar: typeof entry.sidecar === "string" && entry.sidecar ? sidecar_element(entry.sidecar) : null
36
37
  });
37
38
  }
38
39
  return map;
39
40
  }
40
- /** The recorded sidecar HTML back into a script element (off the DOM; a `<template>` parses it
41
- * without running anything — the sidecar is a data script, never executable). */
42
- function sidecar_element(doc, html) {
43
- const holder = doc.createElement("template");
44
- holder.innerHTML = html;
45
- const el = holder.content.firstElementChild;
41
+ /** The recorded sidecar HTML back into a script element, through the one parser (./parse-html.ts:
42
+ * inert, nothing runs — the sidecar is a data script, never executable). */
43
+ function sidecar_element(html) {
44
+ const el = parse_region_html(html).firstElementChild;
46
45
  return el && el.tagName === "SCRIPT" ? el : null;
47
46
  }
48
47
  /** The server-minted facts of the hole with this identity on `doc`, or `null`. */
@@ -0,0 +1,17 @@
1
+ export type HoleFetch = {
2
+ /** Past the browser cache: the endpoint may answer `private, max-age`, and stale is the whole
3
+ * point of revalidating. */
4
+ revalidate?: boolean;
5
+ priority?: "high" | "auto" | "low";
6
+ signal?: AbortSignal;
7
+ /** A renewal answered this fresh capability for the hole. */
8
+ renewed?: (fresh: string) => void;
9
+ };
10
+ /** A hole's fetch: its request started ({@link start_hole}), its answer read ({@link read_hole}). */
11
+ export declare function fetch_hole(endpoint: string, opts?: HoleFetch): Promise<string>;
12
+ /** The request for a hole's HTML (the page's head starts a `load` hole's the same way:
13
+ * ../early-holes.ts). */
14
+ export declare function start_hole(endpoint: string, { revalidate, priority, signal }?: HoleFetch): Promise<Response>;
15
+ /** A hole's answer, read: whatever lands at an address is read this one way, whoever started its
16
+ * request. */
17
+ export declare function read_hole(endpoint: string, response: Promise<Response>, opts?: Pick<HoleFetch, "signal" | "renewed">): Promise<string>;
@@ -0,0 +1,47 @@
1
+ import { KEEP_FALLBACK_HTML } from "../keep-fallback-marker.js";
2
+ import { RegionAnswerRefused, capability_expired, deny_of, is_allowed_region_endpoint, is_document_answer, is_redirected_answer, is_same_origin_response, renewal_url } from "./region-endpoint-url.js";
3
+ // src/runtime/hole-fetch.ts
4
+ /**
5
+ * THE ONE NETWORK FETCH OF A HOLE'S HTML. The element's own wake, its `prefetch` warm and `preload()`
6
+ * all fetch a hole through here, so whatever lands in the frame store at its address was read one
7
+ * way: the endpoint's text, the keep-fallback marker on a 204 (`keepFallback()` on the server), or a
8
+ * refusal when the answer is not the region's. An expired capability renews once, and the fresh one
9
+ * is handed to `renewed` (the frame store keeps it for the address). No DOM, no store, no scheduler:
10
+ * the caller decides when it may go out.
11
+ */
12
+ /** A hole's fetch: its request started ({@link start_hole}), its answer read ({@link read_hole}). */
13
+ async function fetch_hole(endpoint, opts = {}) {
14
+ return read_hole(endpoint, start_hole(endpoint, opts), opts);
15
+ }
16
+ /** The request for a hole's HTML (the page's head starts a `load` hole's the same way:
17
+ * ../early-holes.ts). */
18
+ function start_hole(endpoint, { revalidate = false, priority = "auto", signal } = {}) {
19
+ return fetch(endpoint, {
20
+ credentials: "same-origin",
21
+ cache: revalidate ? "no-store" : "default",
22
+ priority,
23
+ signal
24
+ });
25
+ }
26
+ /** A hole's answer, read: whatever lands at an address is read this one way, whoever started its
27
+ * request. */
28
+ async function read_hole(endpoint, response, opts = {}) {
29
+ let res = await response;
30
+ if (res.status === 403 && capability_expired(endpoint)) {
31
+ res = await start_hole(renewal_url(endpoint), {
32
+ revalidate: true,
33
+ signal: opts.signal
34
+ });
35
+ const fresh = res.ok ? res.headers.get("x-ogygia-capability") : null;
36
+ if (fresh && is_allowed_region_endpoint(fresh)) opts.renewed?.(fresh);
37
+ }
38
+ if (!is_same_origin_response(res)) throw new Error("cross-origin redirect");
39
+ if (is_redirected_answer(res)) throw new RegionAnswerRefused("redirected", res.url);
40
+ if (res.status === 403) throw new RegionAnswerRefused("forbidden", res.url || endpoint, deny_of(res, endpoint));
41
+ if (!res.ok) throw new Error("status " + res.status);
42
+ if (res.status === 204) return KEEP_FALLBACK_HTML;
43
+ const text = await res.text();
44
+ if (is_document_answer(text)) throw new RegionAnswerRefused("document", res.url);
45
+ return text;
46
+ }
47
+ export { fetch_hole, read_hole, start_hole };
@@ -1,7 +1,7 @@
1
1
  import { Component } from "svelte";
2
2
  export declare const seed_page_once: () => void;
3
3
  export declare const seed_remote_once: () => void;
4
- export declare const kit_page_thread: () => Promise<void> | null;
4
+ export declare const kit_page_thread: (signal: AbortSignal) => Promise<unknown> | null;
5
5
  /** A hydrated island as the element holds it: how to tear it down, and (keep / live hosts) how to
6
6
  * push the next page's props into it. */
7
7
  export type IslandHandle = {
@@ -94,8 +94,8 @@ export declare function read_region_props(region: Element, foreign?: boolean): R
94
94
  */
95
95
  export declare function hydrate_island(region: HTMLElement, entry: string, mod: IslandModule, ssr_html?: string | null, touched?: boolean): IslandHandle | null;
96
96
  /**
97
- * Hydrate a live region's swapped-in HTML through {@link LiveHost} (props-pushable). A live region's
98
- * HTML comes from svelte `render()` (both envelope layers) — same as the deferred swap path, so it
99
- * is NOT wrapped in extra `[..]` hydration comments. `null` when the live feature is absent.
97
+ * Hydrate a live region's swapped-in HTML with pushable props, so a later tick reaches the mounted
98
+ * island in place. Its HTML is svelte `render()`'s (the root anchor pair already there) — the
99
+ * deferred swap's shape, so it is never wrapped again. `null` when the live feature is absent.
100
100
  */
101
101
  export declare function hydrate_live(region: HTMLElement, entry: string, mod: IslandModule, props: Record<string, unknown>): IslandHandle | null;