@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,64 @@
1
+ import { Rules } from "../rules.js";
2
+ import { warn } from "../messages/server.js";
3
+ import { observe } from "@ogygia/machine";
4
+ // src/server/rules.ts
5
+ /**
6
+ * THE SERVER'S RULES (#313, ../rules.ts): what is always true of the server, checked a task after
7
+ * each response ends (in dev, and in a build made with `OGYGIA_RULES=1`), printed with its code.
8
+ *
9
+ * - `rule_request_scope_open`: a request's scope (its timers, streams, fetches) is closed once its
10
+ * response has ended.
11
+ * - `rule_flight_kept_failure`: no flight keeps a failure: an answer kept is never an error.
12
+ *
13
+ * Each area tells this module what it holds (`note_response_end`) only when rules are on: every call
14
+ * site folds out of a build without them.
15
+ */
16
+ /** The server's rules. */
17
+ const server_rules = /* @__PURE__ */ new Rules();
18
+ /** Request scopes whose response has ended, until a check sees them closed. */
19
+ const ended = /* @__PURE__ */ new Set();
20
+ /** A response ended (its body read to the end, cancelled, or none): its request's scope must be
21
+ * closed by the time the next check runs. */
22
+ function note_response_end(owner) {
23
+ if (!installed) return;
24
+ ended.add(owner);
25
+ check_soon();
26
+ }
27
+ function request_scopes_closed() {
28
+ let open = 0;
29
+ for (const owner of ended) if (owner.closed) ended.delete(owner);
30
+ else open++;
31
+ return open === 0 ? null : `${open} request scope(s) still open after their response ended`;
32
+ }
33
+ /** The flights' entries that keep an error as their answer now, each with what it keeps. An entry
34
+ * leaves when it moves on (dropped, or a fresh answer kept): the rule is about what is kept now, so
35
+ * a check (a simulated run, the next response in dev) sees its own breaks, not an earlier one's. */
36
+ const kept_failures = /* @__PURE__ */ new Map();
37
+ function no_kept_failure() {
38
+ if (kept_failures.size === 0) return null;
39
+ return [...kept_failures.values()].slice(0, 10).join("; ");
40
+ }
41
+ let installed = false;
42
+ let pending = false;
43
+ /** Checks every rule a task from now (once, however many responses ask), and says what broke. */
44
+ function check_soon() {
45
+ if (pending) return;
46
+ pending = true;
47
+ setTimeout(() => {
48
+ pending = false;
49
+ server_rules.say((code, detail) => warn(code, { detail }));
50
+ }, 0);
51
+ }
52
+ /** Rules on: checked after each response, and what broke is said with its code. */
53
+ function install_server_rules() {
54
+ if (installed) return;
55
+ installed = true;
56
+ server_rules.add("rule_request_scope_open", request_scopes_closed);
57
+ server_rules.add("rule_flight_kept_failure", no_kept_failure);
58
+ observe((name, _from, to, machine) => {
59
+ const answer = to?.kind === "kept" ? to.answer : void 0;
60
+ if (answer instanceof Error) kept_failures.set(machine, `"${name}" kept ${answer.name}: ${answer.message.slice(0, 120)}`);
61
+ else if (kept_failures.size !== 0) kept_failures.delete(machine);
62
+ });
63
+ }
64
+ export { install_server_rules, note_response_end, server_rules };
@@ -1,3 +1,4 @@
1
+ import { OgygiaError, warn } from "../messages/server.js";
1
2
  import { buildHadSecret, buildKey } from "virtual:ogygia/secret";
2
3
  import { sign } from "virtual:ogygia/sign";
3
4
  import { building } from "$app/env";
@@ -24,23 +25,27 @@ const SOURCE_NAME = {
24
25
  env: "OGYGIA_SECRET",
25
26
  build: "this build's random key"
26
27
  };
28
+ const slots = globalThis;
27
29
  function slot() {
28
- const g = globalThis;
29
- return g[SLOT] ??= {};
30
+ return slots[SLOT] ?? { kind: "undecided" };
30
31
  }
31
32
  function signing_key() {
32
33
  const s = slot();
33
- if (s.key) return s.key;
34
+ if (s.kind === "decided") return s.key;
34
35
  const from_env = typeof process !== "undefined" && process.env?.OGYGIA_SECRET || "";
35
- const [secret, source] = s.option ? [s.option, "option"] : from_env ? [from_env, "env"] : [buildKey, "build"];
36
- s.key = {
36
+ const [secret, source] = s.kind === "secret" ? [s.option, "option"] : from_env ? [from_env, "env"] : [buildKey, "build"];
37
+ const key = {
37
38
  secret,
38
39
  stable: source !== "build",
39
40
  fp: key_fingerprint(secret),
40
41
  source
41
42
  };
42
- if (!s.key.stable && !import.meta.env.DEV) warn_build_key();
43
- return s.key;
43
+ slots[SLOT] = {
44
+ kind: "decided",
45
+ key
46
+ };
47
+ if (!key.stable && !import.meta.env.DEV) warn_build_key();
48
+ return key;
44
49
  }
45
50
  /**
46
51
  * Record the app's `handle({ secret })`. A blank one counts as none (an app can pass a value that may
@@ -50,15 +55,23 @@ function signing_key() {
50
55
  function configure_signing_secret(secret) {
51
56
  if (!secret?.trim()) return;
52
57
  const s = slot();
53
- const decided = s.key?.secret ?? s.option;
54
- if (decided === void 0 || decided === secret) {
55
- s.option = secret;
58
+ if (s.kind === "undecided") {
59
+ slots[SLOT] = {
60
+ kind: "secret",
61
+ option: secret
62
+ };
56
63
  return;
57
64
  }
58
- const from = s.key ? `${SOURCE_NAME[s.key.source]}, decided by an earlier mint or verify` : "another handle({ secret })";
59
- const message = `[ogygia] handle({ secret }) names key ${key_fingerprint(secret)}, but this server already signs with key ${key_fingerprint(decided)} (from ${from}). The first key stays: pass one secret, before anything renders.`;
60
- if (import.meta.env.DEV) throw new Error(message);
61
- console.error(message);
65
+ const decided = s.kind === "decided" ? s.key.secret : s.option;
66
+ if (decided === secret) return;
67
+ const from = s.kind === "decided" ? `${SOURCE_NAME[s.key.source]}, decided by an earlier mint or verify` : "another handle({ secret })";
68
+ const error = new OgygiaError("secret_conflict", {
69
+ key: key_fingerprint(secret),
70
+ decided: key_fingerprint(decided),
71
+ from
72
+ });
73
+ if (import.meta.env.DEV) throw error;
74
+ console.error(error);
62
75
  }
63
76
  function key_fingerprint(secret) {
64
77
  return sign(secret, "ogygia:key-fp").slice(0, 8);
@@ -70,12 +83,12 @@ function warn_build_key() {
70
83
  const K = Symbol.for("ogygia.build-key-warned");
71
84
  if (g[K]) return;
72
85
  g[K] = true;
73
- if (building) console.warn("[ogygia] prerendered page mints region capabilities signed with a per-build random key — they stop verifying after your next deploy while the static HTML lives on. Set a stable OGYGIA_SECRET env var (or pass it to handle({ secret })) so prerendered holes survive redeploys.");
74
- else if (buildHadSecret) console.error("[ogygia] OGYGIA_SECRET was set when this app was built but is not set where it runs: region capabilities are signed with this build's random key, so every hole minted by another deployment (another backend behind the same CDN, the previous deploy) is refused (403, x-ogygia-deny: foreign-key). Set OGYGIA_SECRET in the server's runtime environment, or pass it to handle({ secret }) (e.g. from $env/static/private).");
75
- else console.warn("[ogygia] OGYGIA_SECRET is not set: region capabilities are signed with this build's random key, so a page cached across a deploy, or served by another deployment than its islands endpoint, gets its holes refused (403, x-ogygia-deny: foreign-key). Set a stable OGYGIA_SECRET, or pass one to handle({ secret }).");
86
+ if (building) warn("build_key_prerender");
87
+ else if (buildHadSecret) console.error(new OgygiaError("secret_missing_at_runtime"));
88
+ else warn("secret_missing");
76
89
  }
77
90
  /** Tests: forget the decided key and the app's secret. */
78
91
  function reset_signing_key() {
79
- delete globalThis[SLOT];
92
+ delete slots[SLOT];
80
93
  }
81
94
  export { configure_signing_secret, key_fingerprint, reset_signing_key, signing_key };
@@ -1,5 +1,18 @@
1
+ import { Scope } from "@ogygia/machine";
1
2
  /** Build a parcel `<template data-ogygia-slot="…">…rendered html…</template>` (length-framed when
2
3
  * the html carries a `</template` of its own). */
3
4
  export declare function build_parcel(slot: string, html: string): string;
4
5
  /** The done-sentinel parcel (empty template) the client watches to end the batch. */
5
- export declare function done_parcel(): string;
6
+ export declare function done_parcel(): string;
7
+ /** One region call's answer in a batch: its slot (the capability signature) and its HTML. Null:
8
+ * the call is left out (the client fetches it on its own). */
9
+ export type BatchAnswer = {
10
+ slot: string;
11
+ html: string;
12
+ } | null;
13
+ /**
14
+ * The batch's frame stream: every call renders at once, and each answer goes out as its parcel the
15
+ * moment it settles, whatever the order; the done sentinel ends it. `render` is handed the batch's
16
+ * signal: it aborts when the stream is cancelled, or when `owner` (the request's) ends first.
17
+ */
18
+ export declare function batch_stream(calls: readonly string[], render: (endpoint: string, signal: AbortSignal) => Promise<BatchAnswer>, owner?: Scope): ReadableStream<Uint8Array>;
@@ -1,3 +1,4 @@
1
+ import { STALE, machine } from "@ogygia/machine";
1
2
  // src/server/stream-regions.ts
2
3
  /**
3
4
  * Region parcels for the batch endpoint (single-flight navigation, a page's first-load holes, OOO).
@@ -11,7 +12,7 @@
11
12
  * decoded string counts in — so its content can never close the box or forge a parcel for another
12
13
  * slot, and it no longer falls back to a second request.
13
14
  *
14
- * Pure (no I/O, no Kit) so it stays unit-testable.
15
+ * No I/O and no Kit (the batch stream is handed its render), so it stays unit-testable.
15
16
  */
16
17
  /** Sentinel slot appended once a batch finishes, so waiting regions know it is complete. */
17
18
  const STREAM_DONE_SLOT = "__ogygia_done__";
@@ -33,4 +34,39 @@ function build_parcel(slot, html) {
33
34
  function done_parcel() {
34
35
  return `<template data-ogygia-slot="${STREAM_DONE_SLOT}"></template>`;
35
36
  }
36
- export { build_parcel, done_parcel };
37
+ const batch_def = machine()({
38
+ name: "batch stream",
39
+ initial: "streaming",
40
+ steps: {
41
+ streaming: ["ended", "cancelled"],
42
+ ended: [],
43
+ cancelled: []
44
+ }
45
+ });
46
+ /**
47
+ * The batch's frame stream: every call renders at once, and each answer goes out as its parcel the
48
+ * moment it settles, whatever the order; the done sentinel ends it. `render` is handed the batch's
49
+ * signal: it aborts when the stream is cancelled, or when `owner` (the request's) ends first.
50
+ */
51
+ function batch_stream(calls, render, owner) {
52
+ const encoder = new TextEncoder();
53
+ const batch = batch_def.create();
54
+ owner?.defer(() => {
55
+ batch.move("streaming", "cancelled");
56
+ });
57
+ return new ReadableStream({
58
+ async start(controller) {
59
+ const streaming = batch.now;
60
+ if (await batch.scope.task((signal) => Promise.all(calls.map(async (endpoint) => {
61
+ const out = await render(endpoint, signal).catch(() => null);
62
+ if (out && batch.now === streaming) controller.enqueue(encoder.encode(build_parcel(out.slot, out.html)));
63
+ }))) === STALE || !batch.move("streaming", "ended")) return;
64
+ controller.enqueue(encoder.encode(done_parcel()));
65
+ controller.close();
66
+ },
67
+ cancel() {
68
+ batch.move("streaming", "cancelled");
69
+ }
70
+ });
71
+ }
72
+ export { batch_stream, build_parcel, done_parcel };
@@ -1,3 +1,4 @@
1
+ import { OgygiaError } from "./messages/island.js";
1
2
  import { parse, stringify } from "devalue";
2
3
  import { createSubscriber } from "svelte/reactivity";
3
4
  // src/shared-state.ts
@@ -46,7 +47,10 @@ function snapshot(name, value) {
46
47
  try {
47
48
  return parse(stringify(value));
48
49
  } catch (e) {
49
- throw new Error(`[ogygia] SharedState '${name}': values must be plain data (devalue-serializable — JSON, Date, Map, Set, BigInt, RegExp). Functions, class instances, and DOM nodes do not cross build boundaries — share facts, not live objects. (${e instanceof Error ? e.message : e})`);
50
+ throw new OgygiaError("shared_state_not_plain", {
51
+ name,
52
+ reason: e instanceof Error ? e.message : String(e)
53
+ });
50
54
  }
51
55
  }
52
56
  var SharedState = class {
@@ -1,14 +1,15 @@
1
1
  import { kit_bridge } from "./kit-bridge.js";
2
2
  import { refresh_remote_instances } from "./kit-remote/remote-cache.js";
3
3
  import { bind_to_component } from "../runtime/nav-hooks.js";
4
+ import { OgygiaError } from "../messages/island.js";
4
5
  // src/shims/app-navigation.ts
5
6
  const kit = () => kit_bridge()?.navigation ?? null;
6
7
  const og = () => globalThis[Symbol.for("ogygia.nav")] ?? null;
7
8
  /** No runtime on this document at all (neither Kit's nor ogygia's): the browser navigates. */
8
9
  function browser_goto(url, opts = {}) {
9
10
  const target = new URL(url, location.href);
10
- if (target.protocol !== "http:" && target.protocol !== "https:") throw new Error("[ogygia] goto() only supports http(s) URLs");
11
- if (target.origin !== location.origin && !opts.external) throw new Error("[ogygia] goto() only supports same-origin URLs (pass { external: true } to leave)");
11
+ if (target.protocol !== "http:" && target.protocol !== "https:") throw new OgygiaError("goto_not_http");
12
+ if (target.origin !== location.origin && !opts.external) throw new OgygiaError("goto_cross_origin");
12
13
  if (opts.replaceState) location.replace(target.href);
13
14
  else location.assign(target.href);
14
15
  return Promise.resolve();
@@ -1,4 +1,5 @@
1
1
  import { foreign_hydrate } from "../current-region.js";
2
+ import { warn } from "../messages/island.js";
2
3
  // src/shims/page-store.svelte.ts
3
4
  const FALLBACK = {
4
5
  url: typeof location !== "undefined" ? new URL(location.href) : new URL("http://localhost/"),
@@ -32,7 +33,9 @@ var PageState = class {
32
33
  this.state = s.state ?? {};
33
34
  for (const fn of this.#subscribers) try {
34
35
  fn();
35
- } catch {}
36
+ } catch (error) {
37
+ report_thrown(error);
38
+ }
36
39
  }
37
40
  reset() {
38
41
  this.set({
@@ -51,6 +54,13 @@ var PageState = class {
51
54
  return () => this.#subscribers.delete(fn);
52
55
  }
53
56
  };
57
+ /** report-thrown.ts's `report_thrown`, here in full: island-side code never imports a module the
58
+ * runtime's boot imports (runtime-boot-svelte-free.test.ts), and the boot's ref watchers use it. */
59
+ function report_thrown(error) {
60
+ const report = globalThis.reportError;
61
+ if (typeof report === "function") report(error);
62
+ else console.error(error);
63
+ }
54
64
  const PAGE_STATE_KEY = Symbol.for("ogygia.page-state");
55
65
  const global_scope = globalThis;
56
66
  const page_state = global_scope[PAGE_STATE_KEY] ??= new PageState();
@@ -72,6 +82,10 @@ function warn_foreign_page_read(expr) {
72
82
  const f = foreign_hydrate();
73
83
  if (!f || warned_foreign_entries.has(f.entry)) return;
74
84
  warned_foreign_entries.add(f.entry);
75
- console.warn(`[ogygia] ${expr} was read inside a mounted MFE island (${f.entry}, from ${f.origin}).\nIn the browser this is the SHELL's page, not the MFE's. The HTML you see was rendered with the MFE's own load data, so this island will repaint with different values after hydrate.\nPass the value as a prop, or read it with a remote function.`);
85
+ warn("foreign_page_read", {
86
+ expr,
87
+ entry: f.entry,
88
+ origin: f.origin
89
+ });
76
90
  }
77
91
  export { PageState, page_state, reset_page, set_page, subscribe_page, warn_foreign_page_read };
package/dist/source.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { fnv1a } from "./runtime/hash.js";
2
+ import { report_thrown } from "./report-thrown.js";
2
3
  // src/source.ts
3
4
  /**
4
5
  * DATA SOURCES — `import.meta.og.source(fn)`, the RUNTIME half (the compiler rewrites the macro to
@@ -29,10 +30,14 @@ function set_source_observer(fn) {
29
30
  function record_source_read(tag) {
30
31
  try {
31
32
  slots.recorder?.(tag);
32
- } catch {}
33
+ } catch (error) {
34
+ report_thrown(error);
35
+ }
33
36
  try {
34
37
  slots.observer?.(tag);
35
- } catch {}
38
+ } catch (error) {
39
+ report_thrown(error);
40
+ }
36
41
  }
37
42
  /** The identity stamp — read by `postrender.invalidate(fn, args)`. */
38
43
  const SOURCE_ID = Symbol.for("ogygia.source.id");
@@ -1,5 +1,6 @@
1
1
  import { wire } from "./live-transport.js";
2
2
  import { mint, register_kind, resolve } from "./ref.js";
3
+ import { OgygiaError, warn } from "./messages/island.js";
3
4
  import { derived, get, writable } from "svelte/store";
4
5
  // src/store-transport.ts
5
6
  /**
@@ -81,7 +82,7 @@ function mark_store(store, tag) {
81
82
  enumerable: false
82
83
  });
83
84
  } catch {
84
- if (typeof console !== "undefined") console.warn(`[ogygia] store "${tag}": the store object is not extensible (frozen/sealed), so it cannot carry its factory brand — it will cross boundaries as a plain writable (custom methods unavailable). Brand before freezing, or leave the store extensible.`);
85
+ if (typeof console !== "undefined") warn("store_not_extensible", { tag });
85
86
  }
86
87
  return store;
87
88
  }
@@ -100,12 +101,15 @@ function register_store_kind() {
100
101
  const t = ref.t ?? GENERIC_TAG;
101
102
  const factory = t === GENERIC_TAG ? void 0 : store_registry().factories.get(t);
102
103
  if (t !== GENERIC_TAG && factory === void 0) {
103
- if (typeof console !== "undefined") console.warn(`[ogygia] store "${t}": factory not loaded in this bundle — degraded to a plain writable (custom methods unavailable). Import the factory's module in the island (or its graph) so \`__register_store_factory\` runs there.`);
104
+ if (typeof console !== "undefined") warn("store_factory_missing", { tag: t });
104
105
  return writable(ref.d);
105
106
  }
106
107
  const built = factory ? factory(ref.d) : writable(ref.d);
107
108
  if (built === null || typeof built !== "object") {
108
- if (typeof console !== "undefined") console.warn(`[ogygia] store "${t}": factory returned ${type_of(built)} while rebuilding — degraded to a plain writable (custom methods unavailable). A store factory must return a store on every call; move environment guards inside the factory body.`);
109
+ if (typeof console !== "undefined") warn("store_factory_bad_rebuild", {
110
+ tag: t,
111
+ got: type_of(built)
112
+ });
109
113
  return writable(ref.d);
110
114
  }
111
115
  return built;
@@ -160,7 +164,10 @@ function __og_store(tag, factory) {
160
164
  if (product === null || typeof product !== "object") {
161
165
  if (!warned_unbranded.has(tag) && typeof console !== "undefined") {
162
166
  warned_unbranded.add(tag);
163
- console.warn(`[ogygia] store "${tag}": factory returned ${type_of(product)} — nothing to brand, value passed through as-is. Fine for a client-only store; a store that must cross an island boundary has to exist on the server too — return e.g. writable(seed) under your guard and gate the side effects inside the factory instead.`);
167
+ warn("store_factory_unbranded", {
168
+ tag,
169
+ got: type_of(product)
170
+ });
164
171
  }
165
172
  return product;
166
173
  }
@@ -221,7 +228,7 @@ function register_derived_kind() {
221
228
  },
222
229
  decode(ref) {
223
230
  const d = ref.d;
224
- if (typeof d.f !== "function") throw new Error("[ogygia] cannot resume og_derived: its formula did not cross — mark it with import.meta.og.$ at the og_derived call site so it travels as a fn ref.");
231
+ if (typeof d.f !== "function") throw new OgygiaError("og_derived_formula_missing");
225
232
  return og_derived(d.single ? d.s[0] : d.s, d.f, d.seed);
226
233
  }
227
234
  });
package/dist/transport.js CHANGED
@@ -6,6 +6,7 @@ import { register_wire_kind } from "./live-transport.js";
6
6
  import { register_derived_kind, register_store_kind } from "./store-transport.js";
7
7
  import { register_snippet_kind } from "./region-snippet.js";
8
8
  import { register_fn_kind } from "./fn-transport.js";
9
+ import { OgygiaError } from "./messages/island.js";
9
10
  // src/transport.ts
10
11
  /**
11
12
  * `ogygiaTransport` — the SvelteKit `transport` entry that lets a held region cross the wire.
@@ -38,7 +39,7 @@ function encode_region(value) {
38
39
  return pack(f.id, props, sign(f.id, props), f.module, f.hydrate, f.hydrateMargin, f.html);
39
40
  }
40
41
  if (typeof f.html === "string") return pack("", f.props ?? {}, "", "", void 0, void 0, f.html);
41
- throw new Error("[ogygia] a held region reached the wire but its component is a plain import, so nothing built a chunk for it. `await` the region before returning it from a remote/load — an awaited region bakes its SSR HTML and crosses as an HTML-only ticket. Or import the component `with { region: 'raw' }` (or a `wake:` mark) to make it a signed, fetchable capability. On a csr=false page you can also skip the wire entirely: call `get()` in a universal `+page.ts` load / the component — its data reaches the render by reference.");
42
+ throw new OgygiaError("held_region_plain_import");
42
43
  }
43
44
  }
44
45
  /** The region DECODE law (single-flight frame write for baked HTML + deferred rebuild),
@@ -88,7 +89,7 @@ function register_region_kind() {
88
89
  },
89
90
  encode(value) {
90
91
  const packed = encode_region(value);
91
- if (packed === false) throw new Error("[ogygia] region kind claimed a non-region value");
92
+ if (packed === false) throw new OgygiaError("region_kind_mismatch");
92
93
  return { d: packed };
93
94
  },
94
95
  decode(ref) {