bosia 0.8.6 → 0.8.7
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.
- package/package.json +1 -1
- package/src/core/cache.ts +7 -5
- package/src/core/client/App.svelte +24 -3
- package/src/core/client/router.svelte.ts +43 -1
- package/src/core/dev.ts +1 -1
- package/src/core/hooks.ts +30 -0
- package/src/core/html.ts +5 -3
- package/src/core/renderer.ts +13 -6
- package/src/core/routeTypes.ts +8 -0
- package/src/core/server.ts +8 -2
- package/src/lib/client.ts +3 -0
package/package.json
CHANGED
package/src/core/cache.ts
CHANGED
|
@@ -350,18 +350,20 @@ export function coalesceMiss(
|
|
|
350
350
|
|
|
351
351
|
export function serveCached(entry: CacheEntry, req: Request): Response {
|
|
352
352
|
const accept = req.headers.get("accept-encoding") ?? "";
|
|
353
|
+
// Base keys lowercased so lowercased extraHeaders (e.g. loader setHeaders)
|
|
354
|
+
// override them instead of getting comma-joined by Headers.
|
|
353
355
|
const headers: Record<string, string> = {
|
|
354
|
-
"
|
|
355
|
-
|
|
356
|
-
"
|
|
356
|
+
"content-type": entry.contentType,
|
|
357
|
+
vary: "Accept-Encoding",
|
|
358
|
+
"x-bosia-cache": "HIT",
|
|
357
359
|
...entry.extraHeaders,
|
|
358
360
|
};
|
|
359
361
|
if (entry.brotli && accept.includes("br")) {
|
|
360
|
-
headers["
|
|
362
|
+
headers["content-encoding"] = "br";
|
|
361
363
|
return new Response(entry.brotli, { status: entry.status, headers });
|
|
362
364
|
}
|
|
363
365
|
if (entry.gzip && accept.includes("gzip")) {
|
|
364
|
-
headers["
|
|
366
|
+
headers["content-encoding"] = "gzip";
|
|
365
367
|
return new Response(entry.gzip, { status: entry.status, headers });
|
|
366
368
|
}
|
|
367
369
|
return new Response(entry.raw, { status: entry.status, headers });
|
|
@@ -34,6 +34,24 @@
|
|
|
34
34
|
|
|
35
35
|
let PageComponent = $state<any>(ssrPageComponent);
|
|
36
36
|
let layoutComponents = $state<any[]>(ssrLayoutComponents ?? []);
|
|
37
|
+
// Mounted page instance — the two <PageComponent> render spots are mutually
|
|
38
|
+
// exclusive branches, so one ref suffices. Exposes `export const snapshot`.
|
|
39
|
+
let pageInstance = $state<any>(null);
|
|
40
|
+
if (!ssrMode) router.getPageSnapshot = () => pageInstance?.snapshot;
|
|
41
|
+
|
|
42
|
+
// Call the landed-on page's snapshot.restore() once it's mounted (post-tick).
|
|
43
|
+
function settleSnapshot() {
|
|
44
|
+
const snap = router.pendingSnapshot;
|
|
45
|
+
if (snap !== undefined)
|
|
46
|
+
tick().then(() => {
|
|
47
|
+
try {
|
|
48
|
+
pageInstance?.snapshot?.restore?.(snap);
|
|
49
|
+
} catch {
|
|
50
|
+
// user restore() threw — never break navigation
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
router.pendingSnapshot = undefined;
|
|
54
|
+
}
|
|
37
55
|
// Network protocol still ships `params` merged into pageData (see renderer.ts).
|
|
38
56
|
// Strip it at the component boundary so consumers receive `params` only via
|
|
39
57
|
// the dedicated prop, matching SvelteKit's surface.
|
|
@@ -75,12 +93,13 @@
|
|
|
75
93
|
});
|
|
76
94
|
} else if (!router.isPush) {
|
|
77
95
|
const pos = router.pendingScroll;
|
|
78
|
-
//
|
|
96
|
+
// single post-tick restore; content that loads later (images
|
|
79
97
|
// without dimensions) can still shift it. Revisit only if reported.
|
|
80
98
|
if (pos) tick().then(() => window.scrollTo(pos.x, pos.y));
|
|
81
99
|
}
|
|
82
100
|
router.pendingScroll = null;
|
|
83
101
|
router.suppressScroll = false;
|
|
102
|
+
settleSnapshot();
|
|
84
103
|
}
|
|
85
104
|
|
|
86
105
|
$effect(() => {
|
|
@@ -102,6 +121,8 @@
|
|
|
102
121
|
if (isFirst) {
|
|
103
122
|
currentLayoutPaths = (match.route as any).layoutPaths ?? [];
|
|
104
123
|
lastSettledPath = pathname;
|
|
124
|
+
// Restore-after-reload: router.init() staged this entry's snapshot.
|
|
125
|
+
settleSnapshot();
|
|
105
126
|
return; // Initial hydration — data already in SSR props, no fetch needed
|
|
106
127
|
}
|
|
107
128
|
|
|
@@ -404,7 +425,7 @@
|
|
|
404
425
|
{:else if layoutComponents.length > 0}
|
|
405
426
|
{@render renderLayout(0, layoutComponents.length)}
|
|
406
427
|
{:else if PageComponent}
|
|
407
|
-
<PageComponent data={pageData} {params} form={formData} />
|
|
428
|
+
<PageComponent bind:this={pageInstance} data={pageData} {params} form={formData} />
|
|
408
429
|
{:else}
|
|
409
430
|
<p>Loading...</p>
|
|
410
431
|
{/if}
|
|
@@ -434,7 +455,7 @@
|
|
|
434
455
|
{#if ErrorComponent}
|
|
435
456
|
<ErrorComponent {...errorProps ?? {}} />
|
|
436
457
|
{:else if PageComponent}
|
|
437
|
-
<PageComponent data={pageData} {params} form={formData} />
|
|
458
|
+
<PageComponent bind:this={pageInstance} data={pageData} {params} form={formData} />
|
|
438
459
|
{:else}
|
|
439
460
|
<p>Loading...</p>
|
|
440
461
|
{/if}
|
|
@@ -30,6 +30,24 @@ function saveScroll() {
|
|
|
30
30
|
scrollPositions[historyIndex] = { x: window.scrollX, y: window.scrollY };
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
// ─── Page snapshots ───
|
|
34
|
+
// `export const snapshot = { capture, restore }` in +page.svelte. Captured
|
|
35
|
+
// alongside scroll on every history mutation, keyed by the same bosiaIndex,
|
|
36
|
+
// restored post-tick by App.svelte. Values must be JSON-serializable — raw in
|
|
37
|
+
// memory, stringified only at the unload flush.
|
|
38
|
+
// page-only; layout snapshots need per-depth instance refs — add if asked.
|
|
39
|
+
const SNAPSHOT_KEY = "bosia:snapshot";
|
|
40
|
+
let pageSnapshots: Record<number, unknown> = {};
|
|
41
|
+
|
|
42
|
+
function savePageSnapshot() {
|
|
43
|
+
try {
|
|
44
|
+
const s = router.getPageSnapshot?.();
|
|
45
|
+
if (s?.capture) pageSnapshots[historyIndex] = s.capture();
|
|
46
|
+
} catch {
|
|
47
|
+
// user capture() threw — never break navigation
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
33
51
|
export function scrollToHash(hash: string): boolean {
|
|
34
52
|
if (typeof document === "undefined" || !hash) return false;
|
|
35
53
|
const raw = hash.startsWith("#") ? hash.slice(1) : hash;
|
|
@@ -61,6 +79,12 @@ export const router = new (class Router {
|
|
|
61
79
|
suppressScroll = false;
|
|
62
80
|
/** Saved scroll position for the entry a popstate landed on; consumed once by App.svelte after the nav settles. */
|
|
63
81
|
pendingScroll: { x: number; y: number } | null = null;
|
|
82
|
+
/** Set by App.svelte; returns the mounted page's `export const snapshot` (if any) at capture time. */
|
|
83
|
+
getPageSnapshot:
|
|
84
|
+
| (() => { capture: () => unknown; restore: (v: any) => void } | undefined)
|
|
85
|
+
| null = null;
|
|
86
|
+
/** Captured snapshot for the entry a popstate/reload landed on; consumed once by App.svelte. `undefined` = nothing to restore. */
|
|
87
|
+
pendingSnapshot: unknown = undefined;
|
|
64
88
|
|
|
65
89
|
navigate(path: string, opts: { replace?: boolean; source?: NavType } = {}) {
|
|
66
90
|
if (this.currentRoute === path) return;
|
|
@@ -91,12 +115,14 @@ export const router = new (class Router {
|
|
|
91
115
|
this.lastNavType = navType;
|
|
92
116
|
this.isPush = true;
|
|
93
117
|
this.pendingScroll = null;
|
|
118
|
+
this.pendingSnapshot = undefined;
|
|
94
119
|
this.currentRoute = finalPath;
|
|
95
120
|
if (typeof history !== "undefined") {
|
|
96
121
|
if (opts.replace) {
|
|
97
122
|
history.replaceState({ bosiaIndex: historyIndex }, "", finalPath);
|
|
98
123
|
} else {
|
|
99
124
|
saveScroll();
|
|
125
|
+
savePageSnapshot();
|
|
100
126
|
historyIndex++;
|
|
101
127
|
history.pushState({ bosiaIndex: historyIndex }, "", finalPath);
|
|
102
128
|
}
|
|
@@ -112,8 +138,14 @@ export const router = new (class Router {
|
|
|
112
138
|
} catch {
|
|
113
139
|
scrollPositions = {};
|
|
114
140
|
}
|
|
141
|
+
try {
|
|
142
|
+
pageSnapshots = JSON.parse(sessionStorage.getItem(SNAPSHOT_KEY) ?? "{}");
|
|
143
|
+
} catch {
|
|
144
|
+
pageSnapshots = {};
|
|
145
|
+
}
|
|
115
146
|
const stamped = history.state?.bosiaIndex != null;
|
|
116
147
|
historyIndex = stamped ? history.state.bosiaIndex : 0;
|
|
148
|
+
this.pendingSnapshot = stamped ? pageSnapshots[historyIndex] : undefined;
|
|
117
149
|
history.replaceState({ ...history.state, bosiaIndex: historyIndex }, "");
|
|
118
150
|
// Restore after a reload — manual mode means the browser won't. Only for
|
|
119
151
|
// entries we stamped before (a fresh visit has no bosiaIndex in state).
|
|
@@ -146,6 +178,7 @@ export const router = new (class Router {
|
|
|
146
178
|
const finalPath = anchor.pathname + anchor.search + anchor.hash;
|
|
147
179
|
if (this.currentRoute !== finalPath) {
|
|
148
180
|
saveScroll();
|
|
181
|
+
savePageSnapshot();
|
|
149
182
|
historyIndex++;
|
|
150
183
|
history.pushState({ bosiaIndex: historyIndex }, "", finalPath);
|
|
151
184
|
this.currentRoute = finalPath;
|
|
@@ -164,8 +197,10 @@ export const router = new (class Router {
|
|
|
164
197
|
// Save the position of the entry we're leaving (historyIndex is still
|
|
165
198
|
// the old entry's), then look up where the landed-on entry was.
|
|
166
199
|
saveScroll();
|
|
200
|
+
savePageSnapshot();
|
|
167
201
|
historyIndex = e.state?.bosiaIndex ?? 0;
|
|
168
202
|
this.pendingScroll = scrollPositions[historyIndex] ?? null;
|
|
203
|
+
this.pendingSnapshot = pageSnapshots[historyIndex];
|
|
169
204
|
// Fire beforeNavigate listeners; popstate can't be reliably cancelled
|
|
170
205
|
// (browser history already advanced), so we surface the event for
|
|
171
206
|
// observation only — `cancel()` is a no-op for this source.
|
|
@@ -189,14 +224,21 @@ export const router = new (class Router {
|
|
|
189
224
|
// listeners can warn-on-leave (return value ignored; cancellation here
|
|
190
225
|
// requires `beforeunload`, not in scope).
|
|
191
226
|
window.addEventListener("beforeunload", () => {
|
|
192
|
-
//
|
|
227
|
+
// beforeunload can be skipped on mobile page-discard; add a
|
|
193
228
|
// visibilitychange persist if restore-after-reload proves flaky there.
|
|
194
229
|
saveScroll();
|
|
230
|
+
savePageSnapshot();
|
|
195
231
|
try {
|
|
196
232
|
sessionStorage.setItem(SCROLL_KEY, JSON.stringify(scrollPositions));
|
|
197
233
|
} catch {
|
|
198
234
|
// sessionStorage unavailable (private mode / quota) — skip persistence.
|
|
199
235
|
}
|
|
236
|
+
try {
|
|
237
|
+
// Separate try: a non-JSON-serializable snapshot must not kill scroll persistence.
|
|
238
|
+
sessionStorage.setItem(SNAPSHOT_KEY, JSON.stringify(pageSnapshots));
|
|
239
|
+
} catch {
|
|
240
|
+
// sessionStorage unavailable or snapshot not serializable — skip persistence.
|
|
241
|
+
}
|
|
200
242
|
const fromTarget = buildTarget(this.currentRoute);
|
|
201
243
|
const nav: Navigation = {
|
|
202
244
|
from: fromTarget,
|
package/src/core/dev.ts
CHANGED
|
@@ -265,7 +265,7 @@ let buildPending = false;
|
|
|
265
265
|
async function buildAndRestart(): Promise<boolean> {
|
|
266
266
|
if (building) {
|
|
267
267
|
buildPending = true;
|
|
268
|
-
//
|
|
268
|
+
// coalesced into the in-flight build. Managed mode fires one
|
|
269
269
|
// rebuild per turn so overlap is rare; report optimistic rather than a
|
|
270
270
|
// false build failure to the host.
|
|
271
271
|
return true;
|
package/src/core/hooks.ts
CHANGED
|
@@ -63,8 +63,38 @@ export type LoadEvent = {
|
|
|
63
63
|
* namespaced (e.g. `"app:user"`).
|
|
64
64
|
*/
|
|
65
65
|
depends: (...keys: string[]) => void;
|
|
66
|
+
/**
|
|
67
|
+
* Set response headers for this request. Headers accumulate across
|
|
68
|
+
* layout and page loaders and land on both the SSR HTML response and
|
|
69
|
+
* the client-navigation data response. Setting the same header twice
|
|
70
|
+
* throws, as does `set-cookie` (use the `cookies` API instead).
|
|
71
|
+
* No-op during prerendering (only bodies are written to disk).
|
|
72
|
+
*/
|
|
73
|
+
setHeaders: (headers: Record<string, string>) => void;
|
|
66
74
|
};
|
|
67
75
|
|
|
76
|
+
/**
|
|
77
|
+
* Build a `setHeaders` closure over a shared accumulator. Keys are stored
|
|
78
|
+
* lowercased; duplicates (any casing) and `set-cookie` throw. Shared across
|
|
79
|
+
* all loaders of one request so cross-loader duplicates are caught too.
|
|
80
|
+
*/
|
|
81
|
+
export function makeSetHeaders(
|
|
82
|
+
acc: Record<string, string>,
|
|
83
|
+
): (headers: Record<string, string>) => void {
|
|
84
|
+
return (headers) => {
|
|
85
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
86
|
+
const k = key.toLowerCase();
|
|
87
|
+
if (k === "set-cookie") {
|
|
88
|
+
throw new Error('setHeaders() cannot set "set-cookie" — use the cookies API instead');
|
|
89
|
+
}
|
|
90
|
+
if (k in acc) {
|
|
91
|
+
throw new Error(`setHeaders() called twice for header "${k}"`);
|
|
92
|
+
}
|
|
93
|
+
acc[k] = value;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
68
98
|
/**
|
|
69
99
|
* Tracked dependencies captured for a single loader during one run.
|
|
70
100
|
* Shipped to the client so subsequent client-side navigations can
|
package/src/core/html.ts
CHANGED
|
@@ -373,9 +373,11 @@ export function compress(
|
|
|
373
373
|
status = 200,
|
|
374
374
|
extraHeaders?: Record<string, string>,
|
|
375
375
|
): Response {
|
|
376
|
+
// Base keys lowercased so lowercased extraHeaders (e.g. loader setHeaders)
|
|
377
|
+
// override them instead of getting comma-joined by Headers.
|
|
376
378
|
const headers: Record<string, string> = {
|
|
377
|
-
"
|
|
378
|
-
|
|
379
|
+
"content-type": contentType,
|
|
380
|
+
vary: "Accept-Encoding",
|
|
379
381
|
...extraHeaders,
|
|
380
382
|
};
|
|
381
383
|
const accept = req.headers.get("accept-encoding") ?? "";
|
|
@@ -385,7 +387,7 @@ export function compress(
|
|
|
385
387
|
if (!isDev && bytes.length > GZIP_MIN_BYTES && accept.includes("gzip")) {
|
|
386
388
|
return new Response(Bun.gzipSync(bytes), {
|
|
387
389
|
status,
|
|
388
|
-
headers: { ...headers, "
|
|
390
|
+
headers: { ...headers, "content-encoding": "gzip" },
|
|
389
391
|
});
|
|
390
392
|
}
|
|
391
393
|
return new Response(bytes, { status, headers });
|
package/src/core/renderer.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { render } from "svelte/server";
|
|
|
3
3
|
import { findMatch } from "./matcher.ts";
|
|
4
4
|
import { serverRoutes, errorPage } from "bosia:routes";
|
|
5
5
|
import type { RouteMatch } from "./types.ts";
|
|
6
|
+
import { makeSetHeaders } from "./hooks.ts";
|
|
6
7
|
import type { Cookies, LoaderDeps } from "./hooks.ts";
|
|
7
8
|
import { CSP_ENABLED } from "./csp.ts";
|
|
8
9
|
import {
|
|
@@ -333,6 +334,10 @@ export async function loadRouteData(
|
|
|
333
334
|
const layoutData: (Record<string, any> | null)[] = [];
|
|
334
335
|
const layoutDeps: (LoaderDeps | null)[] = [];
|
|
335
336
|
let parentData: Record<string, any> = {};
|
|
337
|
+
// Shared across layout + page loaders so cross-loader duplicate
|
|
338
|
+
// setHeaders() calls throw. Keys stored lowercased.
|
|
339
|
+
const loaderHeaders: Record<string, string> = {};
|
|
340
|
+
const setHeaders = makeSetHeaders(loaderHeaders);
|
|
336
341
|
|
|
337
342
|
// Run layout server loaders root → leaf, each gets parent() data
|
|
338
343
|
for (const ls of route.layoutServers) {
|
|
@@ -366,6 +371,7 @@ export async function loadRouteData(
|
|
|
366
371
|
fetch: trackedFetch(fetch, origin, deps),
|
|
367
372
|
metadata: null,
|
|
368
373
|
depends: makeDepends(deps),
|
|
374
|
+
setHeaders,
|
|
369
375
|
}),
|
|
370
376
|
LOAD_TIMEOUT,
|
|
371
377
|
`layout load (depth=${ls.depth}, ${url.pathname})`,
|
|
@@ -431,6 +437,7 @@ export async function loadRouteData(
|
|
|
431
437
|
fetch: trackedFetch(fetch, origin, deps),
|
|
432
438
|
metadata: metadataData,
|
|
433
439
|
depends: makeDepends(deps),
|
|
440
|
+
setHeaders,
|
|
434
441
|
}),
|
|
435
442
|
LOAD_TIMEOUT,
|
|
436
443
|
`page load (${url.pathname})`,
|
|
@@ -472,7 +479,7 @@ export async function loadRouteData(
|
|
|
472
479
|
// When pageData is skipped, the client merges its cached pageData with current
|
|
473
480
|
// route params separately, so we keep the `null` sentinel here.
|
|
474
481
|
const pageOut = pageData === null ? null : { ...pageData, params };
|
|
475
|
-
return { pageData: pageOut, layoutData, csr, ssr, pageDeps, layoutDeps };
|
|
482
|
+
return { pageData: pageOut, layoutData, csr, ssr, pageDeps, layoutDeps, loaderHeaders };
|
|
476
483
|
}
|
|
477
484
|
|
|
478
485
|
// ─── Metadata Loader ─────────────────────────────────────
|
|
@@ -690,7 +697,7 @@ export async function renderSSRStream(
|
|
|
690
697
|
appHtmlSegments,
|
|
691
698
|
);
|
|
692
699
|
return new Response(html, {
|
|
693
|
-
headers: { "
|
|
700
|
+
headers: { "content-type": "text/html; charset=utf-8", ...data.loaderHeaders },
|
|
694
701
|
});
|
|
695
702
|
}
|
|
696
703
|
|
|
@@ -773,7 +780,7 @@ export async function renderSSRStream(
|
|
|
773
780
|
brotli,
|
|
774
781
|
contentType: "text/html; charset=utf-8",
|
|
775
782
|
status: 200,
|
|
776
|
-
extraHeaders:
|
|
783
|
+
extraHeaders: data.loaderHeaders,
|
|
777
784
|
tags,
|
|
778
785
|
},
|
|
779
786
|
cookies,
|
|
@@ -812,7 +819,7 @@ export async function renderSSRStream(
|
|
|
812
819
|
});
|
|
813
820
|
|
|
814
821
|
return new Response(stream, {
|
|
815
|
-
headers: { "
|
|
822
|
+
headers: { "content-type": "text/html; charset=utf-8", ...data.loaderHeaders },
|
|
816
823
|
});
|
|
817
824
|
} finally {
|
|
818
825
|
releaseMiss?.();
|
|
@@ -894,7 +901,7 @@ export async function renderPageWithFormData(
|
|
|
894
901
|
undefined,
|
|
895
902
|
appHtmlSegments,
|
|
896
903
|
);
|
|
897
|
-
return compress(html, "text/html; charset=utf-8", req, status);
|
|
904
|
+
return compress(html, "text/html; charset=utf-8", req, status, data.loaderHeaders);
|
|
898
905
|
}
|
|
899
906
|
|
|
900
907
|
const { body, head } = render(App, {
|
|
@@ -923,7 +930,7 @@ export async function renderPageWithFormData(
|
|
|
923
930
|
undefined,
|
|
924
931
|
appHtmlSegments,
|
|
925
932
|
);
|
|
926
|
-
return compress(html, "text/html; charset=utf-8", req, status);
|
|
933
|
+
return compress(html, "text/html; charset=utf-8", req, status, data.loaderHeaders);
|
|
927
934
|
}
|
|
928
935
|
|
|
929
936
|
// ─── Error Page Renderer ──────────────────────────────────
|
package/src/core/routeTypes.ts
CHANGED
|
@@ -86,6 +86,14 @@ export function generateRouteTypes(manifest: RouteManifest): void {
|
|
|
86
86
|
lines.push(` * or +server.ts to opt the route out of the server response cache. */`);
|
|
87
87
|
lines.push(`export type CacheOption = false;`);
|
|
88
88
|
lines.push(``);
|
|
89
|
+
lines.push(
|
|
90
|
+
`/** \`export const snapshot: Snapshot<T>\` in +page.svelte — captured on nav-away,`,
|
|
91
|
+
);
|
|
92
|
+
lines.push(` * restored on back/forward. Values must be JSON-serializable. */`);
|
|
93
|
+
lines.push(
|
|
94
|
+
`export type Snapshot<T = any> = { capture: () => T; restore: (snapshot: T) => void };`,
|
|
95
|
+
);
|
|
96
|
+
lines.push(``);
|
|
89
97
|
lines.push(`type _LoadEvent = Omit<LoadEvent, 'params'> & { params: Params };`);
|
|
90
98
|
lines.push(`type _MetadataEvent = Omit<MetadataEvent, 'params'> & { params: Params };`);
|
|
91
99
|
lines.push(`type _RequestEvent = Omit<RequestEvent, 'params'> & { params: Params };`);
|
package/src/core/server.ts
CHANGED
|
@@ -314,12 +314,18 @@ async function resolve(event: RequestEvent): Promise<Response> {
|
|
|
314
314
|
{ "Cache-Control": cc },
|
|
315
315
|
);
|
|
316
316
|
}
|
|
317
|
+
// loaderHeaders must not leak into the JSON body the client router consumes.
|
|
318
|
+
const { loaderHeaders = {}, ...payload } = result.data;
|
|
319
|
+
const extra: Record<string, string> = { "cache-control": cc, ...loaderHeaders };
|
|
320
|
+
// Privacy beats intent: cookie-derived responses stay private even if
|
|
321
|
+
// a loader set its own cache-control.
|
|
322
|
+
if (cookiesWereAccessed) extra["cache-control"] = cc;
|
|
317
323
|
return compress(
|
|
318
|
-
JSON.stringify({ ...
|
|
324
|
+
JSON.stringify({ ...payload, metadata: result.metadata }),
|
|
319
325
|
"application/json",
|
|
320
326
|
request,
|
|
321
327
|
200,
|
|
322
|
-
|
|
328
|
+
extra,
|
|
323
329
|
);
|
|
324
330
|
} catch (err) {
|
|
325
331
|
if (err instanceof Redirect) {
|
package/src/lib/client.ts
CHANGED
|
@@ -18,4 +18,7 @@ export {
|
|
|
18
18
|
invalidateAll,
|
|
19
19
|
} from "../core/client/navigation.ts";
|
|
20
20
|
export type { GotoOptions, Navigation, NavigationTarget } from "../core/client/navigation.ts";
|
|
21
|
+
/** `export const snapshot: Snapshot<T>` in +page.svelte — captured on nav-away,
|
|
22
|
+
* restored on back/forward. Values must be JSON-serializable. */
|
|
23
|
+
export type Snapshot<T = any> = { capture: () => T; restore: (snapshot: T) => void };
|
|
21
24
|
export { page } from "../core/client/page.svelte.ts";
|