Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
333 changes: 207 additions & 126 deletions packages/kit/src/runtime/client/client.js

Large diffs are not rendered by default.

39 changes: 10 additions & 29 deletions packages/kit/src/runtime/client/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
prefetch,
prefetchRoutes
} from '$app/navigation';
import { CSRPageNode, CSRRoute } from 'types';
import { CSRPageNode, CSRPageNodeLoader, CSRRoute, ServerErrorNode, Uses } from 'types';
import { HttpError } from '../../index/private.js';
import { SerializedHttpError } from '../server/page/types.js';

Expand Down Expand Up @@ -65,41 +65,22 @@ export type NavigationFinished = {

export type BranchNode = {
node: CSRPageNode;
loader: CSRPageNodeLoader;
server: DataNode | null;
shared: DataNode | null;
data: Record<string, any> | null;
uses: {
params: Set<string>;
url: boolean; // TODO make more granular?
dependencies: Set<string>;
parent: boolean;
};
};

export interface DataNode {
type: 'data';
data: Record<string, any> | null;
uses: Uses;
}

export type NavigationState = {
branch: Array<BranchNode | undefined>;
error: HttpError | Error | null;
params: Record<string, string>;
session_id: number;
url: URL;
};

export type ServerDataPayload = ServerDataRedirected | ServerDataLoaded;

export interface ServerDataRedirected {
type: 'redirect';
location: string;
}

export interface ServerDataLoaded {
type: 'data';
nodes: Array<{
data?: Record<string, any> | null; // TODO or `-1` to indicate 'reuse cached data'?
status?: number;
message?: string;
error?: {
name: string;
message: string;
stack: string;
[key: string]: any;
};
}>;
}
115 changes: 77 additions & 38 deletions packages/kit/src/runtime/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { negotiate } from '../../utils/http.js';
import { HttpError, Redirect } from '../../index/private.js';
import { load_server_data } from './page/load_data.js';
import { json } from '../../index/index.js';
import { once } from '../../utils/functions.js';

/* global __SVELTEKIT_ADAPTER_NAME__ */

Expand Down Expand Up @@ -254,67 +255,105 @@ export async function respond(request, options, state) {
let response;
if (is_data_request && route.type === 'page') {
try {
/** @type {Redirect | HttpError | Error} */
let error;

// TODO only get the data we need for the navigation
const promises = [...route.layouts, route.leaf].map(async (n, i) => {
try {
if (error) return;

// == because it could be undefined (in dev) or null (in build, because of JSON.stringify)
const node = n == undefined ? n : await options.manifest._.nodes[n]();
return {
// TODO return `uses`, so we can reuse server data effectively
data: await load_server_data({
const node_ids = [...route.layouts, route.leaf];

const invalidated =
request.headers.get('x-sveltekit-invalidated')?.split(',').map(Boolean) ??
node_ids.map(() => true);

let aborted = false;

const functions = node_ids.map((n, i) => {
return once(async () => {
try {
if (aborted) {
return /** @type {import('types').ServerDataSkippedNode} */ ({
type: 'skip'
});
}

// == because it could be undefined (in dev) or null (in build, because of JSON.stringify)
const node = n == undefined ? n : await options.manifest._.nodes[n]();
return load_server_data({
dev: options.dev,
event,
node,
parent: async () => {
/** @type {Record<string, any>} */
const data = {};
for (let j = 0; j < i; j += 1) {
const parent = await promises[j];
if (!parent || parent instanceof HttpError || 'error' in parent) {
return data;
}
const parent = /** @type {import('types').ServerDataNode} */ (
await functions[j]()
);
Object.assign(data, parent.data);
}
return data;
}
})
};
} catch (e) {
error = normalize_error(e);

if (error instanceof Redirect) {
throw error;
}

if (error instanceof HttpError) {
return error; // { status, message }
});
} catch (e) {
aborted = true;
throw e;
}
});
});

options.handle_error(error, event);

return {
error: error_to_pojo(error, options.get_stack)
};
const promises = functions.map(async (fn, i) => {
if (!invalidated[i]) {
return /** @type {import('types').ServerDataSkippedNode} */ ({
type: 'skip'
});
}

return fn();
});

response = json({
let length = promises.length;
const nodes = await Promise.all(
promises.map((p, i) =>
p.catch((e) => {
const error = normalize_error(e);

if (error instanceof Redirect) {
throw error;
}

length = i + 1; // don't include nodes after first error

if (error instanceof HttpError) {
return /** @type {import('types').ServerErrorNode} */ ({
type: 'error',
httperror: { ...error }
});
}

options.handle_error(error, event);

return /** @type {import('types').ServerErrorNode} */ ({
type: 'error',
error: error_to_pojo(error, options.get_stack)
});
})
)
);

/** @type {import('types').ServerData} */
const server_data = {
type: 'data',
nodes: await Promise.all(promises)
});
nodes: nodes.slice(0, length)
};

response = json(server_data);
} catch (e) {
const error = normalize_error(e);

if (error instanceof Redirect) {
response = json({
/** @type {import('types').ServerData} */
const server_data = {
type: 'redirect',
location: error.location
});
};

response = json(server_data);
} else {
response = json(error_to_pojo(error, options.get_stack), { status: 500 });
}
Expand Down
7 changes: 4 additions & 3 deletions packages/kit/src/runtime/server/page/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ export async function render_page(event, route, options, state, resolve_opts) {
/** @type {Error | null} */
let load_error = null;

/** @type {Array<Promise<Record<string, any> | null>>} */
/** @type {Array<Promise<import('types').ServerDataNode | null>>} */
const server_promises = nodes.map((node, i) => {
if (load_error) {
// if an error happens immediately, don't bother with the rest of the nodes
Expand All @@ -168,7 +168,8 @@ export async function render_page(event, route, options, state, resolve_opts) {
/** @type {Record<string, any>} */
const data = {};
for (let j = 0; j < i; j += 1) {
Object.assign(data, await server_promises[j]);
const parent = await server_promises[j];
if (parent) Object.assign(data, await parent.data);
}
return data;
}
Expand Down Expand Up @@ -291,7 +292,7 @@ export async function render_page(event, route, options, state, resolve_opts) {
response: new Response(undefined),
body: JSON.stringify({
type: 'data',
nodes: branch.map((branch_node) => ({ data: branch_node?.server_data }))
nodes: branch.map((branch_node) => branch_node?.server_data)
})
});
}
Expand Down
57 changes: 47 additions & 10 deletions packages/kit/src/runtime/server/page/load_data.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,33 +8,69 @@ import { LoadURL, PrerenderingURL } from '../../../utils/url.js';
* node: import('types').SSRNode | undefined;
* parent: () => Promise<Record<string, any>>;
* }} opts
* @returns {Promise<import('types').ServerDataNode | null>}
*/
export async function load_server_data({ dev, event, node, parent }) {
if (!node?.server) return null;

const server_data = await node.server.load?.call(null, {
const uses = {
dependencies: new Set(),
params: new Set(),
parent: false,
url: false
};

/** @param {string[]} deps */
function depends(...deps) {
for (const dep of deps) {
const { href } = new URL(dep, event.url);
uses.dependencies.add(href);
}
}

const params = new Proxy(event.params, {
get: (target, key) => {
uses.params.add(key);
return target[/** @type {string} */ (key)];
}
});

const result = await node.server.load?.call(null, {
// can't use destructuring here because it will always
// invoke event.clientAddress, which breaks prerendering
get clientAddress() {
return event.clientAddress;
},
depends,
locals: event.locals,
params: event.params,
parent,
params,
parent: async () => {
uses.parent = true;
return parent();
},
platform: event.platform,
request: event.request,
routeId: event.routeId,
setHeaders: event.setHeaders,
url: event.url
});

const result = server_data ? await unwrap_promises(server_data) : null;
const data = result ? await unwrap_promises(result) : null;

if (dev) {
check_serializability(result, /** @type {string} */ (node.server_id), 'data');
check_serializability(data, /** @type {string} */ (node.server_id), 'data');
}

return result;
return {
type: 'data',
data,
uses: {
dependencies: uses.dependencies.size > 0 ? Array.from(uses.dependencies) : undefined,
params: uses.params.size > 0 ? Array.from(uses.params) : undefined,
parent: uses.parent ? 1 : undefined,
url: uses.url ? 1 : undefined
}
};
}

/**
Expand All @@ -44,21 +80,22 @@ export async function load_server_data({ dev, event, node, parent }) {
* fetcher: typeof fetch;
* node: import('types').SSRNode | undefined;
* parent: () => Promise<Record<string, any>>;
* server_data_promise: Promise<Record<string, any> | null>;
* server_data_promise: Promise<import('types').ServerDataNode | null>;
* state: import('types').SSRState;
* }} opts
* @returns {Promise<Record<string, any> | null>}
*/
export async function load_data({ event, fetcher, node, parent, server_data_promise, state }) {
const server_data = await server_data_promise;
const server_data_node = await server_data_promise;

if (!node?.shared?.load) {
return server_data;
return server_data_node?.data ?? null;
}

const load_input = {
url: state.prerendering ? new PrerenderingURL(event.url) : new LoadURL(event.url),
params: event.params,
data: server_data,
data: server_data_node?.data ?? null,
routeId: event.routeId,
fetch: fetcher,
setHeaders: event.setHeaders,
Expand Down
16 changes: 16 additions & 0 deletions packages/kit/src/utils/functions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* @template T
* @param {() => T} fn
*/
export function once(fn) {
let done = false;

/** @type T */
let result;

return () => {
if (done) return result;
done = true;
return (result = fn());
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { increment } from './state.js';

export function load() {
return {
count: increment()
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<!-- TODO remove this file post-https://github.com/sveltejs/kit/issues/5967 -->
<slot />

<ul>
<li><a href="/load/unchanged/reset">/load/unchanged/reset</a></li>
<li><a href="/load/unchanged/isolated/a">/load/unchanged/isolated/a</a></li>
<li><a href="/load/unchanged/isolated/b">/load/unchanged/isolated/b</a></li>
<li><a href="/load/unchanged/uses-parent/a">/load/unchanged/uses-parent/a</a></li>
<li><a href="/load/unchanged/uses-parent/b">/load/unchanged/uses-parent/b</a></li>
</ul>
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<a href="/load/unchanged/uses-parent/a">uses parent</a>
<a href="/load/unchanged/isolated/a">isolated</a>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/** @type {import('./$types').PageServerLoad} */
export function load({ params }) {
return {
slug: params.slug
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<script>
/** @type {import('./$types').PageData} */
export let data;
</script>

<h1>slug: {data.slug}</h1>
<h2>count: {data.count}</h2>
Loading