@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
package/dist/seed-refs.js CHANGED
@@ -308,100 +308,141 @@ function path_of(at) {
308
308
  return path;
309
309
  }
310
310
  const EMPTY_NODES = [];
311
- const EMPTY_INDEX = {
312
- by_identity: {
313
- get: () => void 0,
314
- has: () => false
315
- },
316
- candidates: () => EMPTY_NODES,
317
- hash_of: () => null,
318
- size: 0,
319
- touched: /* @__PURE__ */ new Set()
320
- };
321
311
  const index_cache = /* @__PURE__ */ new WeakMap();
322
312
  /**
323
- * Index the seed's `data` for referencing: every referenceable node at or above `min_bytes`, by
313
+ * The seed's `data` indexed for referencing: every referenceable node at or above `min_bytes`, by
324
314
  * identity now and by structural hash on demand (first occurrence wins — the shortest path is not
325
- * guaranteed, the first in key order is, which is deterministic). Cached per `data` object: one
326
- * index per request, however many islands ask. It reads the one measuring walk (`analyze`) and
327
- * prunes: a node below `min_bytes` cannot hold a child at or above it (a child is never larger
328
- * than its parent), so its subtree is skipped BEFORE anything is materialised — on a CMS tree that
329
- * is most of the nodes.
315
+ * guaranteed, the first in key order is, which is deterministic). It reads the one measuring walk
316
+ * (`analyze`) and prunes: a node below `min_bytes` cannot hold a child at or above it (a child is
317
+ * never larger than its parent), so its subtree is skipped BEFORE anything is materialised — on a
318
+ * CMS tree that is most of the nodes. Data that is not a plain object or array indexes nothing.
330
319
  */
331
- function index_seed(data, min_bytes = 96) {
332
- if (!is_plain(data)) return {
333
- ...EMPTY_INDEX,
334
- touched: /* @__PURE__ */ new Set()
335
- };
336
- const hit = index_cache.get(data);
337
- if (hit) return hit;
338
- const memo = analysis(data).memo;
339
- const at = /* @__PURE__ */ new WeakMap();
340
- const nodes = [];
341
- const seen = /* @__PURE__ */ new Set();
342
- const enter = (c) => {
343
- if (!is_plain(c) || seen.has(c)) return false;
344
- const p = memo.get(c);
345
- return p !== void 0 && bytes_of(p) >= min_bytes;
346
- };
347
- const visit = (v, here) => {
348
- seen.add(v);
349
- if (memo.get(v) & F_REF) {
350
- at.set(v, here);
351
- nodes.push(v);
352
- }
353
- if (Array.isArray(v)) for (let i = 0; i < v.length; i++) {
354
- const c = v[i];
355
- if (enter(c)) visit(c, {
356
- parent: here,
357
- key: i,
358
- path: null
359
- });
360
- }
361
- else for (const key in v) {
362
- const c = v[key];
363
- if (enter(c)) visit(c, {
364
- parent: here,
365
- key,
366
- path: null
320
+ var SeedIndex = class SeedIndex {
321
+ /** nodes indexed (for tests / devtools). */
322
+ size;
323
+ /**
324
+ * SEED SHAPING: the top-level `page.data` keys under which a props node was matched
325
+ * (`plan_seed_refs` records the first path segment of every reference it plans). The handle
326
+ * renders the tail against the FULL index, then ships every touched key — a key no island's
327
+ * code reads but whose node an island's props point into still ships, so a reference always
328
+ * has something to point at.
329
+ */
330
+ touched = /* @__PURE__ */ new Set();
331
+ #memo = null;
332
+ #at = /* @__PURE__ */ new WeakMap();
333
+ #nodes = [];
334
+ #by_bytes = null;
335
+ #hash_memo = /* @__PURE__ */ new Map();
336
+ #by_hash = /* @__PURE__ */ new Map();
337
+ /** The index of `data`, cached per `data` object: one index per request, however many islands
338
+ * ask. */
339
+ static of(data, min_bytes = 96) {
340
+ if (!is_plain(data)) return new SeedIndex(data, min_bytes);
341
+ let index = index_cache.get(data);
342
+ if (!index) index_cache.set(data, index = new SeedIndex(data, min_bytes));
343
+ return index;
344
+ }
345
+ constructor(data, min_bytes) {
346
+ if (is_plain(data)) {
347
+ const memo = this.#memo = analysis(data).memo;
348
+ const at = this.#at;
349
+ const nodes = this.#nodes;
350
+ const seen = /* @__PURE__ */ new Set();
351
+ const enter = (c) => {
352
+ if (!is_plain(c) || seen.has(c)) return false;
353
+ const p = memo.get(c);
354
+ return p !== void 0 && bytes_of(p) >= min_bytes;
355
+ };
356
+ const visit = (v, here) => {
357
+ seen.add(v);
358
+ if (memo.get(v) & F_REF) {
359
+ at.set(v, here);
360
+ nodes.push(v);
361
+ }
362
+ if (Array.isArray(v)) for (let i = 0; i < v.length; i++) {
363
+ const c = v[i];
364
+ if (enter(c)) visit(c, {
365
+ parent: here,
366
+ key: i,
367
+ path: null
368
+ });
369
+ }
370
+ else for (const key in v) {
371
+ const c = v[key];
372
+ if (enter(c)) visit(c, {
373
+ parent: here,
374
+ key,
375
+ path: null
376
+ });
377
+ }
378
+ };
379
+ if (enter(data)) visit(data, {
380
+ parent: null,
381
+ key: "",
382
+ path: []
367
383
  });
368
384
  }
369
- };
370
- if (enter(data)) visit(data, {
371
- parent: null,
372
- key: "",
373
- path: []
374
- });
375
- let by_bytes = null;
376
- const buckets = () => {
377
- if (by_bytes === null) {
378
- by_bytes = /* @__PURE__ */ new Map();
379
- for (const n of nodes) {
380
- const b = bytes_of(memo.get(n));
385
+ this.size = this.#nodes.length;
386
+ }
387
+ /** node → its path from `page.data` (identity matches: the app passed the seed's own object).
388
+ * The path is materialised on the first ask — an indexed node that no island references
389
+ * never allocates one. */
390
+ path(node) {
391
+ const here = this.#at.get(node);
392
+ return here === void 0 ? void 0 : path_of(here);
393
+ }
394
+ /** Whether `node` is an indexed seed node (by identity). */
395
+ has(node) {
396
+ return this.#at.has(node);
397
+ }
398
+ /**
399
+ * The seed nodes with exactly this byte estimate — the only ones a props node of that size can
400
+ * be a clone of. Structure matching hashes just those few candidates (memoised per seed node)
401
+ * instead of the whole seed, so an app that passes the seed's own objects never hashes a seed
402
+ * node at all, and a cloning app hashes only what it has to. The buckets are built on the first
403
+ * ask — a page whose islands only pass seed objects by identity never builds them.
404
+ */
405
+ candidates(bytes) {
406
+ if (this.#by_bytes === null) {
407
+ const by_bytes = this.#by_bytes = /* @__PURE__ */ new Map();
408
+ for (const n of this.#nodes) {
409
+ const b = bytes_of(this.#memo.get(n));
381
410
  const bucket = by_bytes.get(b);
382
411
  if (bucket) bucket.push(n);
383
412
  else by_bytes.set(b, [n]);
384
413
  }
385
414
  }
386
- return by_bytes;
387
- };
388
- const hash_memo = /* @__PURE__ */ new Map();
389
- const index = {
390
- by_identity: {
391
- get: (node) => {
392
- const here = at.get(node);
393
- return here === void 0 ? void 0 : path_of(here);
394
- },
395
- has: (node) => at.has(node)
396
- },
397
- candidates: (b) => buckets().get(b) ?? EMPTY_NODES,
398
- hash_of: (node) => hash_subtree(node, hash_memo, /* @__PURE__ */ new Set())?.hash ?? null,
399
- size: nodes.length,
400
- touched: /* @__PURE__ */ new Set()
401
- };
402
- index_cache.set(data, index);
403
- return index;
404
- }
415
+ return this.#by_bytes.get(bytes) ?? EMPTY_NODES;
416
+ }
417
+ /**
418
+ * The first seed node of exactly this byte estimate whose structural hash is `hash` (bucket
419
+ * order, as `candidates(bytes).find(...)` gives it), in one map lookup. The bucket is hashed in
420
+ * order only as far as an ask needs, each hash keeping its first node, so the many same-sized
421
+ * records of a list cost one hash each however many props nodes ask.
422
+ */
423
+ match(bytes, hash) {
424
+ const bucket = this.candidates(bytes);
425
+ if (bucket.length === 0) return void 0;
426
+ let at = this.#by_hash.get(bytes);
427
+ if (at === void 0) this.#by_hash.set(bytes, at = {
428
+ hashed: 0,
429
+ first: /* @__PURE__ */ new Map()
430
+ });
431
+ const hit = at.first.get(hash);
432
+ if (hit !== void 0) return hit;
433
+ while (at.hashed < bucket.length) {
434
+ const node = bucket[at.hashed++];
435
+ const h = this.hash_of(node);
436
+ if (h === null || at.first.has(h)) continue;
437
+ at.first.set(h, node);
438
+ if (h === hash) return node;
439
+ }
440
+ }
441
+ /** Structural hash of an indexed seed node, memoised for the request. */
442
+ hash_of(node) {
443
+ return this.#memo === null ? null : hash_subtree(node, this.#hash_memo, /* @__PURE__ */ new Set())?.hash ?? null;
444
+ }
445
+ };
405
446
  /**
406
447
  * Plan one island's references: walk the props once, top-down, largest matching ancestor first —
407
448
  * identity first (a pointer lookup per node, no hashing), then structure (hash + exact comparison,
@@ -410,6 +451,7 @@ function index_seed(data, min_bytes = 96) {
410
451
  * ride inside the reference), and neither is anything under a node below `min_bytes` (no child
411
452
  * of it can be referenced). The props' own measures come from the request memo, so the plan adds
412
453
  * no walk of its own beyond the pointer lookups.
454
+ * @heisenbench linear in props
413
455
  */
414
456
  function plan_seed_refs(index, props, min_bytes = 96) {
415
457
  const matched = /* @__PURE__ */ new WeakMap();
@@ -428,18 +470,15 @@ function plan_seed_refs(index, props, min_bytes = 96) {
428
470
  const visit = (v) => {
429
471
  if (!is_plain(v) || seen.has(v)) return;
430
472
  seen.add(v);
431
- const by_id = index.by_identity.get(v);
473
+ const by_id = index.path(v);
432
474
  if (by_id) return hit(v, by_id);
433
475
  const p = memo.get(v) ?? measure(v, memo);
434
476
  const bytes = bytes_of(p);
435
477
  if (bytes < min_bytes) return;
436
- if (p & F_REF) {
437
- const cands = index.candidates(bytes);
438
- if (cands.length) {
439
- const h = hash_subtree(v, hashed, /* @__PURE__ */ new Set())?.hash ?? null;
440
- const cand = h ? cands.find((c) => index.hash_of(c) === h) : void 0;
441
- if (cand && deep_equal_plain(cand, v)) return hit(v, index.by_identity.get(cand));
442
- }
478
+ if (p & F_REF && index.candidates(bytes).length) {
479
+ const h = hash_subtree(v, hashed, /* @__PURE__ */ new Set())?.hash ?? null;
480
+ const cand = h ? index.match(bytes, h) : void 0;
481
+ if (cand && deep_equal_plain(cand, v)) return hit(v, index.path(cand));
443
482
  }
444
483
  if (Array.isArray(v)) for (const item of v) visit(item);
445
484
  else for (const key in v) visit(v[key]);
@@ -452,4 +491,4 @@ function plan_seed_refs(index, props, min_bytes = 96) {
452
491
  keys
453
492
  };
454
493
  }
455
- export { SEED_REF_KEY, analyze, deep_equal_plain, index_seed, json_culprit, plan_seed_refs, resolve_seed_ref, seed_ref_reviver, set_measure_memo_reader };
494
+ export { SEED_REF_KEY, SeedIndex, analyze, deep_equal_plain, json_culprit, plan_seed_refs, resolve_seed_ref, seed_ref_reviver, set_measure_memo_reader };
@@ -43,6 +43,12 @@ export declare function modulepreload_tag(href: string): string;
43
43
  * them. `data-ogygia-runtime-dep` keeps them attached to the script when the handle moves it
44
44
  * (head-presence.ts `runtime_first`). `locations`: the build's location map, which the runtime reads
45
45
  * for a load that arrives with an identity alone (runtime/entry-locations.ts).
46
+ *
47
+ * The runtime cannot say that it failed to load: when its file or one of its static imports fails to
48
+ * download, none of it runs, every island stays server markup, and nothing of ogygia's was logged
49
+ * (#262). The script element's own `error` event fires then (a module graph that failed to fetch),
50
+ * so the tag says it: the code's link, the way a production build prints every message. (A strict
51
+ * CSP without inline handlers blocks it, and the browser's own error stands, as before.)
46
52
  */
47
53
  export declare function runtime_bootstrap_tags(src: string, deps: readonly string[], locations?: string | null): string;
48
54
  /**
@@ -35,9 +35,15 @@ function modulepreload_tag(href) {
35
35
  * them. `data-ogygia-runtime-dep` keeps them attached to the script when the handle moves it
36
36
  * (head-presence.ts `runtime_first`). `locations`: the build's location map, which the runtime reads
37
37
  * for a load that arrives with an identity alone (runtime/entry-locations.ts).
38
+ *
39
+ * The runtime cannot say that it failed to load: when its file or one of its static imports fails to
40
+ * download, none of it runs, every island stays server markup, and nothing of ogygia's was logged
41
+ * (#262). The script element's own `error` event fires then (a module graph that failed to fetch),
42
+ * so the tag says it: the code's link, the way a production build prints every message. (A strict
43
+ * CSP without inline handlers blocks it, and the browser's own error stands, as before.)
38
44
  */
39
45
  function runtime_bootstrap_tags(src, deps, locations = null) {
40
- let html = "<script type=\"module\" data-ogygia-runtime" + (locations ? " data-og-locations=\"" + locations + "\"" : "") + " src=\"" + src + "\"><\/script>";
46
+ let html = "<script type=\"module\" data-ogygia-runtime" + (locations ? " data-og-locations=\"" + locations + "\"" : "") + " src=\"" + src + "\" onerror=\"console.error('[ogygia] https://ogygiajs.dev/e/runtime_load_failed')\"><\/script>";
41
47
  for (const dep of deps) html += "<link rel=\"modulepreload\" href=\"" + dep + "\" data-ogygia-runtime-dep>";
42
48
  return html;
43
49
  }
@@ -0,0 +1,14 @@
1
+ /** The text between a tag's `<script…>` and `<\/script>`: what a CSP hash covers. */
2
+ export declare function script_text(tag: string): string;
3
+ /** The script's hash source, as a policy lists it: of the exact text the head carries, once. */
4
+ export declare const EARLY_HOLES_SOURCE: string;
5
+ /**
6
+ * A `Content-Security-Policy` (or `-Report-Only`) header value with the early start's hash where its
7
+ * script would be blocked. A header can carry several policies, comma-separated: each is checked.
8
+ */
9
+ export declare function with_early_holes_csp(header: string): string;
10
+ /** A head's `<meta http-equiv="content-security-policy">` policies, each with the hash where its
11
+ * script would be blocked. A head with none comes back as itself. */
12
+ export declare function with_early_holes_meta_csp(head: string): string;
13
+ /** A response's CSP headers (enforced and report-only), each with the hash where it is needed. */
14
+ export declare function add_early_holes_csp(headers: Headers): void;
@@ -0,0 +1,73 @@
1
+ import { EARLY_HOLES_TAG } from "../early-holes-head.js";
2
+ import { createHash } from "node:crypto";
3
+ // src/server/early-holes-csp.ts
4
+ /**
5
+ * THE EARLY START'S SCRIPT UNDER A CONTENT SECURITY POLICY (../early-holes.ts). Its bytes never
6
+ * change, so its `'sha256-…'` source is computed once, here. A policy that would block an inline
7
+ * script gets that one source added to the directive that rules script elements:
8
+ * `script-src-elem`, else `script-src`, else `default-src`. Nothing else in the policy changes:
9
+ *
10
+ * - no such directive: inline scripts are not restricted, nothing to add;
11
+ * - `'none'`: no script runs, ours neither;
12
+ * - `'unsafe-inline'` with no nonce or hash: inline scripts run already, and adding a hash would
13
+ * switch `'unsafe-inline'` off for every other one;
14
+ * - a nonce or `'strict-dynamic'` policy: a hash is a valid source beside both.
15
+ *
16
+ * Server-only (node:crypto).
17
+ */
18
+ /** The text between a tag's `<script…>` and `<\/script>`: what a CSP hash covers. */
19
+ function script_text(tag) {
20
+ return tag.slice(tag.indexOf(">") + 1, tag.lastIndexOf("<\/script>"));
21
+ }
22
+ /** The script's hash source, as a policy lists it: of the exact text the head carries, once. */
23
+ const EARLY_HOLES_SOURCE = `'sha256-${createHash("sha256").update(script_text(EARLY_HOLES_TAG), "utf8").digest("base64")}'`;
24
+ /** The directives that rule an inline `<script>`, the most specific first. */
25
+ const SCRIPT_DIRECTIVES = [
26
+ "script-src-elem",
27
+ "script-src",
28
+ "default-src"
29
+ ];
30
+ const NONCE_OR_HASH_RE = /^'(?:nonce|sha256|sha384|sha512)-/i;
31
+ const WHITESPACE_RE = /\s+/;
32
+ /** One serialized policy (`directive sources; …`) with the hash where an inline script needs it. */
33
+ function with_hash(policy) {
34
+ const parts = policy.split(";");
35
+ const names = parts.map((p) => p.trim().split(WHITESPACE_RE)[0]?.toLowerCase() ?? "");
36
+ for (const directive of SCRIPT_DIRECTIVES) {
37
+ const at = names.indexOf(directive);
38
+ if (at === -1) continue;
39
+ const sources = parts[at].trim().split(WHITESPACE_RE).slice(1);
40
+ const lower = sources.map((s) => s.toLowerCase());
41
+ if (sources.includes(EARLY_HOLES_SOURCE)) return policy;
42
+ if (lower.length === 1 && lower[0] === "'none'") return policy;
43
+ if (lower.includes("'unsafe-inline'") && !sources.some((s) => NONCE_OR_HASH_RE.test(s))) return policy;
44
+ parts[at] = parts[at].replace(/\s*$/, "") + " " + EARLY_HOLES_SOURCE;
45
+ return parts.join(";");
46
+ }
47
+ return policy;
48
+ }
49
+ /**
50
+ * A `Content-Security-Policy` (or `-Report-Only`) header value with the early start's hash where its
51
+ * script would be blocked. A header can carry several policies, comma-separated: each is checked.
52
+ */
53
+ function with_early_holes_csp(header) {
54
+ if (!header) return header;
55
+ return header.split(",").map(with_hash).join(",");
56
+ }
57
+ const META_CSP_G = /(<meta\b[^>]*\bhttp-equiv\s*=\s*["']?content-security-policy["']?[^>]*\bcontent\s*=\s*")([^"]*)(")/gi;
58
+ /** A head's `<meta http-equiv="content-security-policy">` policies, each with the hash where its
59
+ * script would be blocked. A head with none comes back as itself. */
60
+ function with_early_holes_meta_csp(head) {
61
+ return head.replace(META_CSP_G, (_m, open, policy, close) => open + with_early_holes_csp(policy) + close);
62
+ }
63
+ const CSP_HEADERS = ["content-security-policy", "content-security-policy-report-only"];
64
+ /** A response's CSP headers (enforced and report-only), each with the hash where it is needed. */
65
+ function add_early_holes_csp(headers) {
66
+ for (const name of CSP_HEADERS) {
67
+ const value = headers.get(name);
68
+ if (!value) continue;
69
+ const next = with_early_holes_csp(value);
70
+ if (next !== value) headers.set(name, next);
71
+ }
72
+ }
73
+ export { EARLY_HOLES_SOURCE, add_early_holes_csp, script_text, with_early_holes_csp, with_early_holes_meta_csp };
@@ -1,3 +1,4 @@
1
+ import { Scope } from "@ogygia/machine";
1
2
  /** Marks ogygia's own lookup: the handle answers it without setting up a page render. */
2
3
  export declare const PAGE_FACTS_HEADER = "x-ogygia-page-facts";
3
4
  /** The route id and params Kit matched for the looked-up page (the data alone carries neither). */
@@ -18,8 +19,10 @@ export declare function data_url_of(page: URL): string;
18
19
  /**
19
20
  * The page's facts, or a reason they are not available (a redirect, an error, a load that threw,
20
21
  * not a Kit data answer). `decoders`: the app's `transport` decoders, as Kit's client uses them.
22
+ * `owner`: the request the look-up is for. When it ends, the answer's reader stops (a streamed
23
+ * promise still pending then stays pending).
21
24
  */
22
- export declare function look_up_page(page: URL, fetch: Fetch, decoders: Decoders): Promise<PageFacts | {
25
+ export declare function look_up_page(page: URL, fetch: Fetch, decoders: Decoders, owner?: Scope): Promise<PageFacts | {
23
26
  miss: string;
24
27
  }>;
25
28
  export {};
@@ -31,8 +31,10 @@ function data_url_of(page) {
31
31
  /**
32
32
  * The page's facts, or a reason they are not available (a redirect, an error, a load that threw,
33
33
  * not a Kit data answer). `decoders`: the app's `transport` decoders, as Kit's client uses them.
34
+ * `owner`: the request the look-up is for. When it ends, the answer's reader stops (a streamed
35
+ * promise still pending then stays pending).
34
36
  */
35
- async function look_up_page(page, fetch, decoders) {
37
+ async function look_up_page(page, fetch, decoders, owner) {
36
38
  let res;
37
39
  try {
38
40
  res = await fetch(data_url_of(page), { headers: { [PAGE_FACTS_HEADER]: "1" } });
@@ -44,6 +46,7 @@ async function look_up_page(page, fetch, decoders) {
44
46
  return { miss: `the data request answered ${res.status}` };
45
47
  }
46
48
  const reader = res.body.getReader();
49
+ owner?.defer(() => void reader.cancel().catch(() => {}));
47
50
  const text = new TextDecoder();
48
51
  let buf = "";
49
52
  let nl = -1;
@@ -20,8 +20,8 @@ export interface SeedText {
20
20
  export declare class PageSeed {
21
21
  /**
22
22
  * Public page slice for islands. url/params/route/status are always plain and MUST survive;
23
- * data/form/error may carry a non-serializable leaf (function/store/class) — that ONE field is
24
- * dropped, never the whole seed. Promises should already be resolved by the handle
23
+ * data/form/error may carry a non-serializable leaf (function/store/class) — that leaf is left
24
+ * out where it sits (`keep_serializable`), never its field or the whole seed. Promises should already be resolved by the handle
25
25
  * (`resolve_promises`) before this. See INVARIANTS.md · PAGE-SEED.
26
26
  *
27
27
  * `json`: the caller proved the whole slice JSON-exact (seed-refs.ts `analyze`) — then the seed
@@ -2,11 +2,96 @@ import { escape_script_text } from "../escape.js";
2
2
  import { stringify } from "devalue";
3
3
  // src/server/page-seed.ts
4
4
  /** Serialize a Kit `page` snapshot for a single document-level ogygia side-channel. */
5
+ const DROP = Symbol("drop");
6
+ /**
7
+ * A copy of `value` without the values `stringify_fn` cannot carry (a function, a symbol, a class
8
+ * no reducer encodes): an object's key is left out, an array's slot reads `undefined` (later
9
+ * indexes still point at what they did), a Map or Set loses that entry. Plain objects, arrays,
10
+ * Maps and Sets are copied (one copy per source object, so shared and cyclic references keep their
11
+ * shape); every other value is kept or left out whole, asked of `stringify_fn` itself so the app's
12
+ * reducers decide. The source is not touched. Only for a slice whose whole serialization threw.
13
+ */
14
+ function keep_serializable(value, stringify_fn) {
15
+ const copies = /* @__PURE__ */ new Map();
16
+ const carries = (v) => {
17
+ try {
18
+ stringify_fn(v);
19
+ return true;
20
+ } catch {
21
+ return false;
22
+ }
23
+ };
24
+ const put = (out, key, v) => {
25
+ if (key === "__proto__") Object.defineProperty(out, key, {
26
+ value: v,
27
+ enumerable: true,
28
+ writable: true,
29
+ configurable: true
30
+ });
31
+ else out[key] = v;
32
+ };
33
+ const keep = (v) => {
34
+ if (typeof v === "function" || typeof v === "symbol") return carries(v) ? v : DROP;
35
+ if (v === null || typeof v !== "object") return v;
36
+ if (copies.has(v)) return copies.get(v);
37
+ const proto = Object.getPrototypeOf(v);
38
+ if (Array.isArray(v) && proto === Array.prototype) {
39
+ const out = [];
40
+ out.length = v.length;
41
+ copies.set(v, out);
42
+ for (let i = 0; i < v.length; i++) {
43
+ if (!(i in v)) continue;
44
+ const c = keep(v[i]);
45
+ out[i] = c === DROP ? void 0 : c;
46
+ }
47
+ return out;
48
+ }
49
+ if (proto === Object.prototype || proto === null) {
50
+ const out = Object.create(proto);
51
+ copies.set(v, out);
52
+ for (const key of Object.keys(v)) {
53
+ let e;
54
+ try {
55
+ e = v[key];
56
+ } catch {
57
+ continue;
58
+ }
59
+ const c = keep(e);
60
+ if (c !== DROP) put(out, key, c);
61
+ }
62
+ return out;
63
+ }
64
+ if (proto === Map.prototype) {
65
+ const out = /* @__PURE__ */ new Map();
66
+ copies.set(v, out);
67
+ for (const [k, e] of v) {
68
+ const ck = keep(k);
69
+ const ce = keep(e);
70
+ if (ck !== DROP && ce !== DROP) out.set(ck, ce);
71
+ }
72
+ return out;
73
+ }
74
+ if (proto === Set.prototype) {
75
+ const out = /* @__PURE__ */ new Set();
76
+ copies.set(v, out);
77
+ for (const e of v) {
78
+ const c = keep(e);
79
+ if (c !== DROP) out.add(c);
80
+ }
81
+ return out;
82
+ }
83
+ const kept = carries(v) ? v : DROP;
84
+ copies.set(v, kept);
85
+ return kept;
86
+ };
87
+ const out = keep(value);
88
+ return out === DROP ? void 0 : out;
89
+ }
5
90
  var PageSeed = class {
6
91
  /**
7
92
  * Public page slice for islands. url/params/route/status are always plain and MUST survive;
8
- * data/form/error may carry a non-serializable leaf (function/store/class) — that ONE field is
9
- * dropped, never the whole seed. Promises should already be resolved by the handle
93
+ * data/form/error may carry a non-serializable leaf (function/store/class) — that leaf is left
94
+ * out where it sits (`keep_serializable`), never its field or the whole seed. Promises should already be resolved by the handle
10
95
  * (`resolve_promises`) before this. See INVARIANTS.md · PAGE-SEED.
11
96
  *
12
97
  * `json`: the caller proved the whole slice JSON-exact (seed-refs.ts `analyze`) — then the seed
@@ -37,16 +122,24 @@ var PageSeed = class {
37
122
  try {
38
123
  raw = stringify_fn(full);
39
124
  } catch {
40
- const safe = { ...base };
41
- for (const key of [
42
- "data",
43
- "form",
44
- "error"
45
- ]) try {
46
- stringify_fn(full[key]);
47
- safe[key] = full[key];
125
+ let kept = full;
126
+ try {
127
+ kept = keep_serializable(full, stringify_fn);
48
128
  } catch {}
49
- raw = stringify_fn(safe);
129
+ try {
130
+ raw = stringify_fn(kept);
131
+ } catch {
132
+ const safe = { ...base };
133
+ for (const key of [
134
+ "data",
135
+ "form",
136
+ "error"
137
+ ]) try {
138
+ stringify_fn(kept[key]);
139
+ safe[key] = kept[key];
140
+ } catch {}
141
+ raw = stringify_fn(safe);
142
+ }
50
143
  }
51
144
  return {
52
145
  text: raw,
@@ -1,3 +1,4 @@
1
+ import { Scope } from "@ogygia/machine";
1
2
  /** A THENABLE, not just a native Promise — Kit wraps streamed load promises, so `instanceof Promise`
2
3
  * would miss them. Anything with a callable `.then` we can await. */
3
4
  export declare const is_thenable: (v: unknown) => v is PromiseLike<unknown>;
@@ -52,4 +53,16 @@ export declare function resolve_script(global_name: string, id: number, settled:
52
53
  }, reducers?: Record<string, (v: unknown) => unknown>): string;
53
54
  export declare function normalize_error(err: unknown): {
54
55
  message: string;
55
- };
56
+ };
57
+ /**
58
+ * Stream the captured `$page.data` promises into islands (csr=false, real browser load). The
59
+ * document — carrying the pending seed + resolve-global bootstrap — is already built by `resolve`;
60
+ * here we (1) forward it, (2) DRAIN Kit's own dead csr=false resolve tail so its
61
+ * `__sveltekit_<hash> is not defined` never reaches the browser, and (3) emit one
62
+ * `<script>__ogygia_page_resolve(id, ok, value)<\/script>` per promise AS IT SETTLES — completion
63
+ * order, non-blocking. Each island's `{#await page.data.x}` flips pending → resolved live.
64
+ *
65
+ * The stream is the request's (`request`, its owner): the visitor leaving, or the body cancelled,
66
+ * stops Kit's reader and drops what settles after.
67
+ */
68
+ export declare function stream_page_deferred(request: Scope, response: Response, deferred: Deferred[], initial_next_id: number, reducers?: Record<string, (v: unknown) => unknown>): Response;