@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,66 @@
1
+ import { policy } from "@ogygia/machine";
2
+ // src/policies.ts
3
+ /**
4
+ * EVERY RETRY, BACKOFF AND DEADLINE OF THE RUNTIME AND THE SERVER, NAMED ONCE (#313). Each is a
5
+ * `policy()` of `@ogygia/machine` named for what it protects, with the reason for its numbers
6
+ * beside it: the work that tries again reads them from here, never a number of its own.
7
+ *
8
+ * These are the numbers the code used before they were named (no behavior moved with them), so
9
+ * tuning one is a change here and nowhere else. Each is marked pure: a bundle keeps only the ones
10
+ * its code reads (the browser runtime does not ship the server's render and stream deadlines).
11
+ */
12
+ /** A lazy chunk of the runtime (the hydrate core, the navigation chunk, devalue): one more try at
13
+ * once when its download fails, under a fresh URL (`import_again`). A dropped download is usually
14
+ * a blip; a second failure is a real outage, and the caller's own recovery takes over. */
15
+ const download = /* @__PURE__ */ policy({
16
+ attempts: 2,
17
+ backoff: 0,
18
+ jitter: 0,
19
+ deadline: Infinity
20
+ });
21
+ /** devalue's download for a read that runs once (a streamed value's resolver): asked for again
22
+ * after 1, 2, 4 and 8 s, then left (the network is down; the reads wait, as before). */
23
+ const devalue_download = /* @__PURE__ */ policy({
24
+ attempts: 5,
25
+ backoff: 1e3,
26
+ jitter: 0,
27
+ deadline: Infinity
28
+ });
29
+ /** A hole's fetch of its region: three tries, 0.5 s then 1 s apart, before it keeps its fallback.
30
+ * Long enough for a server restart's blip, short enough that a broken hole settles fast. */
31
+ const region_fetch = /* @__PURE__ */ policy({
32
+ attempts: 3,
33
+ backoff: [500, 1e3],
34
+ jitter: 0,
35
+ deadline: Infinity
36
+ });
37
+ /** A region's server render, its wait for a render slot included: one try within 10 s (#195). A
38
+ * render stuck on a hung upstream past it is abandoned, so it never holds a slot for good. */
39
+ const render = /* @__PURE__ */ policy({
40
+ attempts: 1,
41
+ backoff: 0,
42
+ jitter: 0,
43
+ deadline: 1e4
44
+ });
45
+ /** A batch stream of holes: given up after 10 s with no bytes (#214). The server sends a hole's part
46
+ * only once it has rendered, so a few quiet seconds are a slow render, not a dead stream; the
47
+ * `render` deadline bounds each render, so a stream quiet for as long is dead. */
48
+ const stream = /* @__PURE__ */ policy({
49
+ attempts: 1,
50
+ backoff: 0,
51
+ jitter: 0,
52
+ deadline: 1e4
53
+ });
54
+ /** An island file's recovery during a deploy's rollover: the current build's map asked at once,
55
+ * then after 1, 3 and 9 s while it settles (a rollover settles in seconds). */
56
+ const rollover = /* @__PURE__ */ policy({
57
+ attempts: 4,
58
+ backoff: [
59
+ 1e3,
60
+ 3e3,
61
+ 9e3
62
+ ],
63
+ jitter: 0,
64
+ deadline: Infinity
65
+ });
66
+ export { devalue_download, download, region_fetch, render, rollover, stream };
@@ -1,4 +1,5 @@
1
1
  import { script } from "./script.js";
2
+ import { OgygiaError } from "./messages/island.js";
2
3
  // src/preference.ts
3
4
  /**
4
5
  * `preference()` — a persisted, site-wide client CHOICE that flips a CSS state, with zero shipped JS
@@ -44,7 +45,11 @@ function preference_switch() {
44
45
  }
45
46
  /** Declare a site-wide client preference. Throws at creation if `default` isn't one of `values`. */
46
47
  function preference_impl(spec) {
47
- if (!spec.values.includes(spec.default)) throw new Error(`[ogygia] preference('${spec.name}'): default '${spec.default}' is not one of values [${spec.values.join(", ")}]`);
48
+ if (!spec.values.includes(spec.default)) throw new OgygiaError("preference_default_invalid", {
49
+ name: spec.name,
50
+ fallback: spec.default,
51
+ values: spec.values.join(", ")
52
+ });
48
53
  const name = spec.name;
49
54
  const values = spec.values;
50
55
  const def = spec.default;
package/dist/preload.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { REGION_BRAND } from "./region-brand.js";
2
2
  import { frameAddress } from "./frame.js";
3
- import { ensure } from "./runtime/frame-store.js";
3
+ import { ensure, renew } from "./runtime/frame-store.js";
4
+ import { fetch_hole } from "./runtime/hole-fetch.js";
4
5
  // src/preload.ts
5
6
  function preload(region) {
6
7
  if (typeof document === "undefined") return;
@@ -9,13 +10,9 @@ function preload(region) {
9
10
  const url = typeof r.url === "string" ? r.url : "";
10
11
  if (!url || r.html != null) return;
11
12
  const address = frameAddress(url);
12
- ensure(address, async (signal) => {
13
- const res = await fetch(url, {
14
- credentials: "same-origin",
15
- signal
16
- });
17
- if (!res.ok) throw new Error("status " + res.status);
18
- return res.text();
19
- }).catch(() => {});
13
+ ensure(address, (signal) => fetch_hole(url, {
14
+ signal,
15
+ renewed: (fresh) => renew(address, fresh)
16
+ })).catch(() => {});
20
17
  }
21
18
  export { preload };
@@ -1,13 +1,3 @@
1
- /**
2
- * THE HUB'S WATCH CHANNEL (hub v2, phases W and B) — split out of ref.ts so the boot, which needs only
3
- * "subscribe to an id / tell its watchers" (runtime/frame-store.ts: region frames), never carries the
4
- * whole identity hub. ref.ts re-exports all of it: one channel either way.
5
- *
6
- * WATCH — subscribe to fresh data settling for a hub id. THE one subscription primitive: region
7
- * frames, streamed page-data, live refresh — every "a value arrives later for this identity" channel
8
- * routes through here instead of owning its own subscriber set. Browser-only in practice (the
9
- * request-scoped server never re-settles an id within one render).
10
- */
11
1
  type Watcher = (live: unknown) => void;
12
2
  export declare function watchers(): Map<string, Set<Watcher>>;
13
3
  /** Subscribe to fresh data settling for `id`. Returns an unsubscribe fn. */
package/dist/ref-watch.js CHANGED
@@ -1,4 +1,15 @@
1
+ import { report_thrown } from "./report-thrown.js";
1
2
  // src/ref-watch.ts
3
+ /**
4
+ * THE HUB'S WATCH CHANNEL (hub v2, phases W and B) — split out of ref.ts so the boot, which needs only
5
+ * "subscribe to an id / tell its watchers" (runtime/frame-store.ts: region frames), never carries the
6
+ * whole identity hub. ref.ts re-exports all of it: one channel either way.
7
+ *
8
+ * WATCH — subscribe to fresh data settling for a hub id. THE one subscription primitive: region
9
+ * frames, streamed page-data, live refresh — every "a value arrives later for this identity" channel
10
+ * routes through here instead of owning its own subscriber set. Browser-only in practice (the
11
+ * request-scoped server never re-settles an id within one render).
12
+ */
2
13
  /** id → callbacks. One per realm (a second bundle evaluating this module shares it). */
3
14
  const WATCHERS_KEY = Symbol.for("ogygia.hub.watchers");
4
15
  function watchers() {
@@ -36,7 +47,9 @@ function notify_now(id, live) {
36
47
  if (set === void 0) return;
37
48
  for (const cb of Array.from(set)) try {
38
49
  cb(live);
39
- } catch {}
50
+ } catch (error) {
51
+ report_thrown(error);
52
+ }
40
53
  }
41
54
  let batch_depth = 0;
42
55
  let batch_pending = null;
package/dist/ref.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { emit } from "./events/bus.js";
2
2
  import { batch, notify, watch, watcher_count, watchers } from "./ref-watch.js";
3
3
  import { resolve_sink, set_resolve_sink } from "./ref-sink.js";
4
+ import { OgygiaError } from "./messages/island.js";
5
+ import { report_thrown } from "./report-thrown.js";
4
6
  // src/ref.ts
5
7
  const DEVTOOLS = typeof __OGYGIA_DEVTOOLS__ !== "undefined" ? __OGYGIA_DEVTOOLS__ : false;
6
8
  const REGISTRY_KEY = Symbol.for("ogygia.hub");
@@ -104,14 +106,18 @@ function dispose_scope(scope) {
104
106
  const kind = reg.kinds.get(reg.instance_kind.get(inst) ?? "");
105
107
  try {
106
108
  kind?.dispose?.(inst);
107
- } catch {}
109
+ } catch (error) {
110
+ report_thrown(error);
111
+ }
108
112
  }
109
113
  b.clear();
110
114
  }
111
115
  const ds = reg.scope_disposers.get(scope);
112
116
  if (ds !== void 0) for (const fn of Array.from(ds)) try {
113
117
  fn();
114
- } catch {}
118
+ } catch (error) {
119
+ report_thrown(error);
120
+ }
115
121
  }
116
122
  /**
117
123
  * DISPOSE specific page-scoped ids (reconciler R3) — the SELECTIVE counterpart to dispose_scope.
@@ -136,7 +142,9 @@ function dispose_ids(ids) {
136
142
  reg.instance_kind.delete(inst);
137
143
  try {
138
144
  kind?.dispose?.(inst);
139
- } catch {}
145
+ } catch (error) {
146
+ report_thrown(error);
147
+ }
140
148
  }
141
149
  if (DEVTOOLS && dt_disposed > 0) emit({
142
150
  domain: "hub",
@@ -192,14 +200,18 @@ function resolve(ref, scope_or_remember) {
192
200
  }
193
201
  }
194
202
  const kind = reg.kinds.get(ref.k);
195
- if (kind === void 0) throw new Error(`[ogygia] cannot resolve a "${ref.k}" ref: no such kind is registered on this side. The module that registers it must be imported as a VALUE in this bundle.`);
203
+ if (kind === void 0) throw new OgygiaError("ref_kind_missing", { kind: String(ref.k) });
196
204
  if (remember) {
197
205
  const name = kind.keep_name?.(ref);
198
206
  if (typeof name === "string") {
199
207
  const tag = ref.t ?? ref.k;
200
208
  const owner = reg.keep_owner.get(name);
201
209
  if (owner === void 0) reg.keep_owner.set(name, tag);
202
- else if (owner !== tag && typeof console !== "undefined") console.error(`[ogygia] two transportables both use continuity id "${name}" ("${owner}" and "${tag}"). A continuity id must be unique per class — rename one, or they will clobber each other in the session Keep.`);
210
+ else if (owner !== tag && typeof console !== "undefined") console.error(new OgygiaError("continuity_id_collision", {
211
+ name,
212
+ owner,
213
+ tag
214
+ }));
203
215
  const kept = bucket(reg, "session").get(name);
204
216
  if (kept !== void 0) {
205
217
  kind.merge?.(kept, ref);
@@ -5,6 +5,7 @@ import { PORTABLE_FORM, with_portable_forms } from "./portable-form.js";
5
5
  import { analyze } from "./seed-refs.js";
6
6
  import { fnv1a, fnv1a32 } from "./runtime/hash.js";
7
7
  import { import_entry } from "./runtime/entry-locations.js";
8
+ import { OgygiaError } from "./messages/island.js";
8
9
  import { createRawSnippet, getAllContexts, hydrate, unmount } from "svelte";
9
10
  import { render } from "svelte/server";
10
11
  import { BROWSER, DEV } from "esm-env";
@@ -168,7 +169,10 @@ function capture_static(snippet, label) {
168
169
  }).body;
169
170
  } catch (e) {
170
171
  const where = label ? `prop \`${label}\`` : "a function prop";
171
- throw new Error(`[ogygia] ${where} is a function that can't cross the island boundary — ${e instanceof Error ? e.message : String(e)}. Island props must be SERIALIZABLE; a function crosses only as a Svelte snippet that freezes to HTML, and this one didn't. Usually one of:\n • it's a PLAIN function (an event handler / callback) — those can't cross into an island; pass serializable data as props and keep the behavior inside the island component;\n • it's a snippet WITH PARAMETERS — declare it as a {#snippet} so it crosses LIVE, or pass its data as props;\n • it's children containing a NESTED island — keep those out of frozen children for now.`);
172
+ throw new OgygiaError("function_prop_cannot_cross", {
173
+ where,
174
+ reason: e instanceof Error ? e.message : String(e)
175
+ });
172
176
  }
173
177
  return make({
174
178
  m: "static",
@@ -204,11 +208,13 @@ function og_portable(native, Entry, props, url) {
204
208
  /** Lift a parameterless snippet, or a raw `{ render, setup?, captures? }`, into a region snippet. */
205
209
  function region_snippet(input) {
206
210
  if (typeof input === "function" && !BROWSER) return capture_static(input);
207
- if (typeof input === "function") throw new Error("[ogygia] region_snippet(fn) freezes a snippet by server-rendering it — call it during SSR, not on the client. Pass a raw { render, setup } region snippet for client-constructed regions.");
211
+ if (typeof input === "function") throw new OgygiaError("region_snippet_on_client");
208
212
  const caps = input.captures ?? [];
209
213
  const snip = createRawSnippet(() => ({
210
214
  render: () => WRAP_OPEN + input.render(...caps) + WRAP_CLOSE,
211
- setup: input.setup ? (el) => void input.setup(el, ...caps) : () => {}
215
+ setup: input.setup ? (el) => {
216
+ input.setup(el, ...caps);
217
+ } : () => {}
212
218
  }));
213
219
  snip.__ogRegion = {
214
220
  m: "static",
package/dist/region.js CHANGED
@@ -3,6 +3,7 @@ import { with_portable_forms } from "./portable-form.js";
3
3
  import { REGION_BRAND } from "./region-brand.js";
4
4
  import { kit_page_facts_tail, kit_page_ready, kit_render_context } from "./server/kit-context.js";
5
5
  import { region_css_tags } from "./server/html-scan.js";
6
+ import { OgygiaError } from "./messages/island.js";
6
7
  import RawHtml from "./RawHtml.svelte";
7
8
  // src/region.ts
8
9
  /** Brand so the transport can recognize a region without false-matching plain objects. */
@@ -28,7 +29,7 @@ function make_region(component, props, opts, content_id) {
28
29
  props: p,
29
30
  ...content_id ? { content_id } : {}
30
31
  });
31
- throw new Error("[ogygia] a `with { region: 'raw' }` component must be turned into a region on the server (the signer lives server-side). Call region() in a load / remote / render context, not in client code.");
32
+ throw new OgygiaError("raw_region_on_client");
32
33
  }
33
34
  const hydrate = o?.wake ?? component.__hydrate;
34
35
  const hydrateMargin = o?.margin ?? component.__hydrateMargin;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * AN APP'S CALLBACK THAT THREW where the loop it ran in must go on (a ref watcher, a scope disposer,
3
+ * a page store subscriber, a navigation hook, a receipt seam, a devtools sink): the others still
4
+ * run, and its error is not dropped. On a page it goes to the page's error handler (`reportError`:
5
+ * the console and `window.onerror`, where error monitoring sees it), exactly as an uncaught throw
6
+ * would, without stopping the loop. Where there is no such handler (a server, a test), it is
7
+ * printed.
8
+ */
9
+ export declare function report_thrown(error: unknown): void;
@@ -0,0 +1,15 @@
1
+ // src/report-thrown.ts
2
+ /**
3
+ * AN APP'S CALLBACK THAT THREW where the loop it ran in must go on (a ref watcher, a scope disposer,
4
+ * a page store subscriber, a navigation hook, a receipt seam, a devtools sink): the others still
5
+ * run, and its error is not dropped. On a page it goes to the page's error handler (`reportError`:
6
+ * the console and `window.onerror`, where error monitoring sees it), exactly as an uncaught throw
7
+ * would, without stopping the loop. Where there is no such handler (a server, a test), it is
8
+ * printed.
9
+ */
10
+ function report_thrown(error) {
11
+ const report = globalThis.reportError;
12
+ if (typeof report === "function") report(error);
13
+ else console.error(error);
14
+ }
15
+ export { report_thrown };
@@ -0,0 +1,17 @@
1
+ /** What a rule found broken (null: it holds). */
2
+ export type RuleCheck = () => string | null;
3
+ /** A rule found broken: its code, and what it found. */
4
+ export interface BrokenRule {
5
+ readonly code: string;
6
+ readonly detail: string;
7
+ }
8
+ /** One area's rules: each a check under its code. */
9
+ export declare class Rules<C extends string> {
10
+ #private;
11
+ /** Adds a rule. */
12
+ add(code: C, check: RuleCheck): void;
13
+ /** Every rule broken now. A check that throws is broken, with its error. */
14
+ broken(): BrokenRule[];
15
+ /** Checks every rule, and says each newly broken one (`say` prints its code). */
16
+ say(say: (code: C, detail: string) => void): void;
17
+ }
package/dist/rules.js ADDED
@@ -0,0 +1,57 @@
1
+ // src/rules.ts
2
+ /**
3
+ * RULES AS CODE (#313). Each "always true" rule of an area is written once, as a plain function over
4
+ * that area's state that names what it found broken (or null: kept), under the message code it
5
+ * breaks with. The same function is checked wherever it is cheap to look:
6
+ *
7
+ * - in dev, after machine moves and navigations (the runtime) and after each response (the server),
8
+ * printing its code once per thing it found;
9
+ * - in e2e, at the end of every test (`globalThis.__ogygia_rules()` in the page, read by the e2e
10
+ * harness's `rules(page)`).
11
+ *
12
+ * The areas keep their rules beside their code (./runtime/rules.ts, ./server/rules.ts) and register
13
+ * them here. Rules exist in dev and in a build made with `OGYGIA_RULES=1` (the e2e suites'); in any
14
+ * other build every call site folds out, and no rule ships (`__OGYGIA_RULES__`).
15
+ */
16
+ const SAID_MAX = 200;
17
+ /** One area's rules: each a check under its code. */
18
+ var Rules = class {
19
+ #checks = [];
20
+ /** What was said already (`code detail`): a rule that stays broken is said once. */
21
+ #said = /* @__PURE__ */ new Set();
22
+ /** Adds a rule. */
23
+ add(code, check) {
24
+ this.#checks.push({
25
+ code,
26
+ check
27
+ });
28
+ }
29
+ /** Every rule broken now. A check that throws is broken, with its error. */
30
+ broken() {
31
+ const out = [];
32
+ for (const { code, check } of this.#checks) {
33
+ let detail;
34
+ try {
35
+ detail = check();
36
+ } catch (error) {
37
+ detail = "the check threw: " + String(error);
38
+ }
39
+ if (detail !== null) out.push({
40
+ code,
41
+ detail
42
+ });
43
+ }
44
+ return out;
45
+ }
46
+ /** Checks every rule, and says each newly broken one (`say` prints its code). */
47
+ say(say) {
48
+ for (const b of this.broken()) {
49
+ const key = b.code + " " + b.detail;
50
+ if (this.#said.has(key)) continue;
51
+ if (this.#said.size >= SAID_MAX) return;
52
+ this.#said.add(key);
53
+ say(b.code, b.detail);
54
+ }
55
+ }
56
+ };
57
+ export { Rules };
@@ -143,10 +143,6 @@ export interface EntryFallback {
143
143
  recovered: boolean;
144
144
  }
145
145
  export declare function beacon_entry_fallback(f: EntryFallback): void;
146
- /** The runtime asks for a hole's HTML (each ask, retries and re-asks included): what lets a
147
- * preload followed by a download read as used-then-asked-again, not as a preload the browser could
148
- * not use. */
149
- export declare function beacon_hole_fetched(endpoint: string): void;
150
146
  /** A hole's first answer landed: the profiler weighs how long its fallback held the first screen.
151
147
  * The element is read (id, place) only while measuring. */
152
148
  export declare function beacon_hole_answered(el: Element, start: number, batch?: {
@@ -4,6 +4,7 @@ import { paint_blocker } from "./render-blocking.js";
4
4
  import { critical_outcome } from "./load-scheduler.js";
5
5
  import { devtools_switched_on } from "../insights/switch.js";
6
6
  import { charge, metered } from "./own-cost.js";
7
+ import { machine } from "@ogygia/machine";
7
8
  // src/runtime/beacon.ts
8
9
  /**
9
10
  * THE BEACON — the browser's half of the profiler. When the document carries
@@ -45,7 +46,6 @@ function owner() {
45
46
  beacon_hole_answered,
46
47
  beacon_hole_batch_missed,
47
48
  beacon_entry_fallback,
48
- beacon_hole_fetched,
49
49
  beacon_nav
50
50
  };
51
51
  const g = globalThis;
@@ -72,7 +72,6 @@ function beacon_mark(name, ms, attrs, t0) {
72
72
  schedule();
73
73
  }
74
74
  let vitals = null;
75
- let vitals_sent = false;
76
75
  const ISLAND_SEL = "ogygia-region[data-og-fp]";
77
76
  const r2 = (n) => Math.round(n * 100) / 100;
78
77
  let visit_islands = [];
@@ -296,20 +295,6 @@ function beacon_entry_fallback(f) {
296
295
  });
297
296
  if (early_visit_done) resend_soon();
298
297
  }
299
- /** each hole URL (absolute) → how many times the runtime asked the network for it */
300
- let hole_fetches = /* @__PURE__ */ new Map();
301
- /** The runtime asks for a hole's HTML (each ask, retries and re-asks included): what lets a
302
- * preload followed by a download read as used-then-asked-again, not as a preload the browser could
303
- * not use. */
304
- function beacon_hole_fetched(endpoint) {
305
- const o = owner();
306
- if (o) return o.beacon_hole_fetched?.(endpoint);
307
- if (!collecting() || hole_fetches.size >= 200) return;
308
- try {
309
- const url = new URL(endpoint, location.href).href;
310
- hole_fetches.set(url, (hole_fetches.get(url) ?? 0) + 1);
311
- } catch {}
312
- }
313
298
  let holes_answered_els = /* @__PURE__ */ new WeakSet();
314
299
  /** A hole's first answer landed: the profiler weighs how long its fallback held the first screen.
315
300
  * The element is read (id, place) only while measuring. */
@@ -352,7 +337,22 @@ let lcp_el = null;
352
337
  let lcp_region = null;
353
338
  /** an island's markup as the server sent it and after it hydrated — the browser store only */
354
339
  let snapshots = [];
355
- let visit_sent = false;
340
+ /**
341
+ * THE VISIT'S STATE. A hide sends the vitals and the visit (`sent`), but the visit is not over:
342
+ * coming back to the tab, or a back/forward cache restore, opens it again (`reopened`), and what
343
+ * happens after (a navigation, a worse interaction, a shift) goes out again, with the next hide.
344
+ * A second hide with no return in between sends nothing more.
345
+ */
346
+ const visit_machine = machine()({
347
+ name: "beacon-visit",
348
+ initial: "open",
349
+ steps: {
350
+ open: ["sent"],
351
+ sent: ["reopened"],
352
+ reopened: ["sent"]
353
+ }
354
+ });
355
+ let visit_state = visit_machine.create();
356
356
  const SNAPSHOT_CAP = 24e3;
357
357
  const MAX_SNAPSHOTS = 40;
358
358
  /** The snapshot of a changed island: whole when both fit, else the window around the change
@@ -690,8 +690,7 @@ function send(body, slim) {
690
690
  post(body, true);
691
691
  }
692
692
  function flush_vitals() {
693
- if (vitals_sent || !vitals || !Object.keys(vitals).length) return;
694
- vitals_sent = true;
693
+ if (!vitals || !Object.keys(vitals).length) return;
695
694
  send(JSON.stringify({
696
695
  page: visit_page(),
697
696
  at: Math.round(performance.timeOrigin),
@@ -1093,17 +1092,14 @@ function build_visit_now() {
1093
1092
  for (const [url, list] of by_url) {
1094
1093
  if (list.length < 2 || preload_misses.length >= 20) continue;
1095
1094
  const pre = list.find((r) => r.initiatorType === "link");
1096
- const downloads = list.filter((r) => r !== pre && r.initiatorType !== "link" && r.encodedBodySize > 0 && r.transferSize >= r.encodedBodySize);
1097
- const again = downloads[0];
1095
+ const again = list.filter((r) => r !== pre && r.initiatorType !== "link" && r.encodedBodySize > 0 && r.transferSize >= r.encodedBodySize)[0];
1098
1096
  if ((pre?.responseStatus ?? 0) >= 400) continue;
1099
- if (downloads.length < (hole_fetches.get(url) ?? 0)) continue;
1100
1097
  if (pre && again) preload_misses.push({
1101
1098
  url: url.slice(0, 500),
1102
1099
  type: type_of(pre),
1103
1100
  bytes: again.transferSize,
1104
1101
  as: link_as.get(url) ?? "",
1105
- crossorigin: link_co.get(url) ?? null,
1106
- ...hole_fetches.has(url) ? { hole: true } : {}
1102
+ crossorigin: link_co.get(url) ?? null
1107
1103
  });
1108
1104
  }
1109
1105
  const reload = nav.type === "reload";
@@ -1276,18 +1272,17 @@ function visit_page() {
1276
1272
  if (landing_page === null && typeof location !== "undefined") landing_page = location.pathname;
1277
1273
  return landing_page ?? "/";
1278
1274
  }
1275
+ /** The visit so far; `final`: the hide's (the page may come back: `visit_state`). Nothing while
1276
+ * the page is hidden after its hide sent it. */
1279
1277
  function flush_visit(final) {
1280
- if (visit_sent) return;
1278
+ if (visit_state.now.kind === "sent") return;
1281
1279
  const visit = build_visit();
1282
1280
  if (!visit) return;
1283
- if (final) {
1284
- visit_sent = true;
1285
- for (const s of snapshots) {
1286
- const el = document.querySelector(`ogygia-region[data-og-fp="${s.fp}"]`);
1287
- if (el) {
1288
- const html = s.from === void 0 ? el.innerHTML : without_runtime_marks(without_comments(el.innerHTML)).slice(s.from, s.from + SNAPSHOT_CAP);
1289
- if (html !== s.hydrated) s.final = html.slice(0, SNAPSHOT_CAP);
1290
- }
1281
+ if (final) for (const s of snapshots) {
1282
+ const el = document.querySelector(`ogygia-region[data-og-fp="${s.fp}"]`);
1283
+ if (el) {
1284
+ const html = s.from === void 0 ? el.innerHTML : without_runtime_marks(without_comments(el.innerHTML)).slice(s.from, s.from + SNAPSHOT_CAP);
1285
+ if (html !== s.hydrated) s.final = html.slice(0, SNAPSHOT_CAP);
1291
1286
  }
1292
1287
  }
1293
1288
  if (!endpoint()) return;
@@ -1856,13 +1851,15 @@ function endpoint() {
1856
1851
  return target = href ? href : null;
1857
1852
  }
1858
1853
  let early_visit_done = false;
1854
+ /** The re-send waiting to go: its cancel. The open visit's timer: the hide (`sent`) or a reset ends
1855
+ * it with the visit. */
1859
1856
  let resend_timer = null;
1860
1857
  let resends = 0;
1861
1858
  const RESEND_MAX = 8;
1862
1859
  function resend_soon() {
1863
- if (!endpoint() || visit_sent || resends >= RESEND_MAX) return;
1864
- if (resend_timer) clearTimeout(resend_timer);
1865
- resend_timer = setTimeout(() => {
1860
+ if (!endpoint() || visit_state.now.kind === "sent" || resends >= RESEND_MAX) return;
1861
+ resend_timer?.();
1862
+ resend_timer = visit_state.scope.timeout(() => {
1866
1863
  resend_timer = null;
1867
1864
  resends++;
1868
1865
  flush_visit(false);
@@ -1901,14 +1898,24 @@ function on_hide() {
1901
1898
  leaving = true;
1902
1899
  try {
1903
1900
  flush();
1904
- flush_vitals();
1905
- flush_visit(true);
1901
+ if (visit_state.now.kind !== "sent") {
1902
+ resend_timer?.();
1903
+ resend_timer = null;
1904
+ flush_vitals();
1905
+ flush_visit(true);
1906
+ visit_state.go("sent");
1907
+ }
1906
1908
  } finally {
1907
1909
  leaving = false;
1908
1910
  }
1909
1911
  flush_cpu(true);
1910
1912
  stop_interaction_cpu(true);
1911
1913
  }
1914
+ /** the page is shown again (back to its tab, or restored from the back/forward cache): the visit
1915
+ * goes on, and the next hide sends it again */
1916
+ function on_show() {
1917
+ visit_state.move("sent", "reopened");
1918
+ }
1912
1919
  function schedule() {
1913
1920
  if (scheduled) return;
1914
1921
  scheduled = true;
@@ -1916,8 +1923,9 @@ function schedule() {
1916
1923
  listening = true;
1917
1924
  observe_vitals();
1918
1925
  start_cpu();
1919
- document.addEventListener("visibilitychange", () => document.visibilityState === "hidden" && on_hide());
1926
+ document.addEventListener("visibilitychange", () => document.visibilityState === "hidden" ? on_hide() : on_show());
1920
1927
  addEventListener("pagehide", on_hide);
1928
+ addEventListener("pageshow", (e) => e.persisted && on_show());
1921
1929
  }
1922
1930
  const idle = globalThis.requestIdleCallback;
1923
1931
  if (idle) idle(flush, { timeout: 2e3 });
@@ -2113,7 +2121,6 @@ function _reset_beacon() {
2113
2121
  marks = [];
2114
2122
  scheduled = false;
2115
2123
  vitals = null;
2116
- vitals_sent = false;
2117
2124
  cpu = null;
2118
2125
  cpu_sent = false;
2119
2126
  cpu_kept = [];
@@ -2156,12 +2163,13 @@ function _reset_beacon() {
2156
2163
  visit_hole_batches = [];
2157
2164
  visit_entry_fallbacks = [];
2158
2165
  holes_answered_els = /* @__PURE__ */ new WeakSet();
2159
- hole_fetches = /* @__PURE__ */ new Map();
2160
2166
  visit_marks = [];
2161
2167
  visit_paints = {};
2162
2168
  lcp_el = lcp_region = null;
2163
2169
  snapshots = [];
2164
- visit_sent = false;
2170
+ visit_state.stop();
2171
+ visit_state = visit_machine.create();
2172
+ resend_timer = null;
2165
2173
  early_visit_done = false;
2166
2174
  resends = 0;
2167
2175
  }
@@ -2187,4 +2195,11 @@ function _beacon_send_leaving(body, slim) {
2187
2195
  }
2188
2196
  }
2189
2197
  if (typeof document !== "undefined" && owner() === null && (devtools_measures() || !!document.querySelector("meta[name=\"ogygia-profiler-beacon\"]")) && !supports("longtask")) watch_frame_gaps();
2190
- export { _beacon_send, _beacon_send_leaving, _beacon_state, _reset_beacon, attr_changes, beacon_entry_fallback, beacon_failed, beacon_hole_answered, beacon_hole_batch_missed, beacon_hole_failed, beacon_hole_fetched, beacon_hydrated, beacon_mark, beacon_nav, beacon_page, beacon_record_cpu, beacon_warning, beacon_watch, hole_request_times, is_headless, resource_type, without_comments, without_runtime_marks };
2198
+ const WAITING_KEY = Symbol.for("ogygia.beacon.waiting");
2199
+ if (typeof document !== "undefined" && owner() === null) {
2200
+ const g = globalThis;
2201
+ const waiting = g[WAITING_KEY];
2202
+ delete g[WAITING_KEY];
2203
+ if (waiting) for (const args of waiting) beacon_mark(...args);
2204
+ }
2205
+ export { _beacon_send, _beacon_send_leaving, _beacon_state, _reset_beacon, attr_changes, beacon_entry_fallback, beacon_failed, beacon_hole_answered, beacon_hole_batch_missed, beacon_hole_failed, beacon_hydrated, beacon_mark, beacon_nav, beacon_page, beacon_record_cpu, beacon_warning, beacon_watch, hole_request_times, is_headless, resource_type, without_comments, without_runtime_marks };
@@ -1,4 +1,5 @@
1
1
  import { slots } from "./slots.js";
2
+ import { create_soft_refresh } from "./soft-refresh.js";
2
3
  import { KitBoot, kit_hydrates_page } from "./kit-boot.js";
3
4
  import { ABSOLUTE_URL_SCHEME, is_warmed_module, warm_island_module } from "./region-endpoint-url.js";
4
5
  import { note_entry_location } from "./entry-locations.js";
@@ -7,10 +8,14 @@ import { props_sidecar_of } from "./sidecar.js";
7
8
  import { parse_region_html } from "./parse-html.js";
8
9
  import { runtime_session } from "./session.js";
9
10
  import { regions_in_shadow } from "./connected.js";
11
+ import { release_sheets } from "./region-sheets.js";
10
12
  import { yield_task } from "./schedule.js";
11
13
  import { beacon_nav } from "./beacon.js";
12
14
  import { is_self_owned, record_server_body, server_body_attributes, server_body_child, walk_enters } from "./ownership.js";
13
- import { load_slot } from "./load-scheduler.js";
15
+ import { LoadSlot } from "./load-scheduler.js";
16
+ import { devalue, load_devalue, needs_devalue, with_devalue } from "./devalue-lane.js";
17
+ import { OgygiaError, warn } from "../messages/core.js";
18
+ import { STALE, flights, stage, world } from "@ogygia/machine";
14
19
  // src/runtime/boot-link.ts
15
20
  /**
16
21
  * Fill `slots.boot` — the boot helpers and session state the runtime's lazy chunks (the hydrate
@@ -34,6 +39,7 @@ function link_boot() {
34
39
  parse_region_html,
35
40
  runtime_session,
36
41
  regions_in_shadow,
42
+ release_sheets,
37
43
  yield_task,
38
44
  beacon_nav: BEACON ? beacon_nav : no_beacon_nav,
39
45
  walk_enters,
@@ -41,7 +47,18 @@ function link_boot() {
41
47
  record_server_body,
42
48
  server_body_attributes,
43
49
  server_body_child,
44
- load_slot
50
+ LoadSlot,
51
+ load_devalue,
52
+ devalue,
53
+ with_devalue,
54
+ needs_devalue,
55
+ OgygiaError,
56
+ warn,
57
+ STALE,
58
+ flights,
59
+ stage,
60
+ world,
61
+ soft: create_soft_refresh()
45
62
  };
46
63
  }
47
64
  export { link_boot };