Skip to content

Commit 3d9e03a

Browse files
Rich-Harristrueadmdummdidumm
authored
feat: allow serialization/deserialization of custom data types (alternative API) (#13149)
* feat: allow serialization/deserialization of custom data types * feat: allow serialization/deserialization of custom data types * feat: allow serialization/deserialization of custom data types * feat: allow serialization/deserialization of custom data types * feat: allow serialization/deserialization of custom data types * add test * fix bugs * lint * improve test name * lint * alternative approach * tweak * added more tests and moved to basics * lint * fix typo * fix test * address feedback * comment * make it work * Update packages/kit/src/core/sync/write_client_manifest.js Co-authored-by: Rich Harris <[email protected]> * use universal transport hook * fix * Update packages/kit/src/core/sync/write_client_manifest.js * tweaks * add types, rename to encode/decode * docs * regenerate * changeset --------- Co-authored-by: Dominic Gannaway <[email protected]> Co-authored-by: Simon H <[email protected]> Co-authored-by: Dominic Gannaway <[email protected]>
1 parent 47890e0 commit 3d9e03a

File tree

25 files changed

+292
-27
lines changed

25 files changed

+292
-27
lines changed

.changeset/fast-dragons-own.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': minor
3+
---
4+
5+
feat: transport custom types across the server/client boundary

documentation/docs/30-advanced/20-hooks.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,23 @@ The `lang` parameter will be correctly derived from the returned pathname.
290290

291291
Using `reroute` will _not_ change the contents of the browser's address bar, or the value of `event.url`.
292292

293+
### transport
294+
295+
This is a collection of _transporters_, which allow you to pass custom types — returned from `load` and form actions — across the server/client boundary. Each transporter contains an `encode` function, which encodes values on the server (or returns `false` for anything that isn't an instance of the type) and a corresponding `decode` function:
296+
297+
```js
298+
/// file: src/hooks.js
299+
import { Vector } from '$lib/math';
300+
301+
/** @type {import('@sveltejs/kit').Transport} */
302+
export const transport = {
303+
Vector: {
304+
encode: (value) => value instanceof Vector && [value.x, value.y],
305+
decode: ([x, y]) => new Vector(x, y)
306+
}
307+
};
308+
```
309+
293310

294311
## Further reading
295312

packages/kit/src/core/sync/write_client_manifest.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,10 +152,14 @@ export function write_client_manifest(kit, manifest_data, output, metadata) {
152152
client_hooks_file ? 'client_hooks.handleError || ' : ''
153153
}(({ error }) => { console.error(error) }),
154154
${client_hooks_file ? 'init: client_hooks.init,' : ''}
155-
156-
reroute: ${universal_hooks_file ? 'universal_hooks.reroute || ' : ''}(() => {})
155+
reroute: ${universal_hooks_file ? 'universal_hooks.reroute || ' : ''}(() => {}),
156+
transport: ${universal_hooks_file ? 'universal_hooks.transport || ' : ''}{}
157157
};
158158
159+
export const decoders = Object.fromEntries(Object.entries(hooks.transport).map(([k, v]) => [k, v.decode]));
160+
161+
export const decode = (type, value) => decoders[type](value);
162+
159163
export { default as root } from '../root.${isSvelte5Plus() ? 'js' : 'svelte'}';
160164
`
161165
);

packages/kit/src/core/sync/write_server.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,14 +71,16 @@ export async function get_hooks() {
7171
${server_hooks ? `({ handle, handleFetch, handleError, init } = await import(${s(server_hooks)}));` : ''}
7272
7373
let reroute;
74-
${universal_hooks ? `({ reroute } = await import(${s(universal_hooks)}));` : ''}
74+
let transport;
75+
${universal_hooks ? `({ reroute, transport } = await import(${s(universal_hooks)}));` : ''}
7576
7677
return {
7778
handle,
7879
handleFetch,
7980
handleError,
80-
reroute,
8181
init,
82+
reroute,
83+
transport
8284
};
8385
}
8486

packages/kit/src/exports/public.d.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -742,6 +742,43 @@ export type ClientInit = () => MaybePromise<void>;
742742
*/
743743
export type Reroute = (event: { url: URL }) => void | string;
744744

745+
/**
746+
* The [`transport`](https://svelte.dev/docs/kit/hooks#Universal-hooks-transport) hook allows you to transport custom types across the server/client boundary.
747+
*
748+
* Each transporter has a pair of `encode` and `decode` functions. On the server, `encode` determines whether a value is an instance of the custom type and, if so, returns a non-falsy encoding of the value which can be an object or an array (or `false` otherwise).
749+
*
750+
* In the browser, `decode` turns the encoding back into an instance of the custom type.
751+
*
752+
* ```ts
753+
* import type { Transport } from '@sveltejs/kit';
754+
*
755+
* declare class MyCustomType {
756+
* data: any
757+
* }
758+
*
759+
* // hooks.js
760+
* export const transport: Transport = {
761+
* MyCustomType: {
762+
* encode: (value) => value instanceof MyCustomType && [value.data],
763+
* decode: ([data]) => new MyCustomType(data)
764+
* }
765+
* };
766+
* ```
767+
* @since 2.11.0
768+
*/
769+
export type Transport = Record<string, Transporter>;
770+
771+
/**
772+
* A member of the [`transport`](https://svelte.dev/docs/kit/hooks#Universal-hooks-transport) hook.
773+
*/
774+
export interface Transporter<
775+
T = any,
776+
U = Exclude<any, false | 0 | '' | null | undefined | typeof NaN>
777+
> {
778+
encode: (value: T) => false | U;
779+
decode: (data: U) => T;
780+
}
781+
745782
/**
746783
* The generic form of `PageLoad` and `LayoutLoad`. You should import those from `./$types` (see [generated types](https://svelte.dev/docs/kit/types#Generated-types))
747784
* rather than using `Load` directly.

packages/kit/src/runtime/app/forms.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as devalue from 'devalue';
22
import { DEV } from 'esm-env';
33
import { invalidateAll } from './navigation.js';
4-
import { applyAction } from '../client/client.js';
4+
import { app, applyAction } from '../client/client.js';
55

66
export { applyAction };
77

@@ -29,9 +29,11 @@ export { applyAction };
2929
*/
3030
export function deserialize(result) {
3131
const parsed = JSON.parse(result);
32+
3233
if (parsed.data) {
33-
parsed.data = devalue.parse(parsed.data);
34+
parsed.data = devalue.parse(parsed.data, app.decoders);
3435
}
36+
3537
return parsed;
3638
}
3739

packages/kit/src/runtime/client/client.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ let container;
174174
/** @type {HTMLElement} */
175175
let target;
176176
/** @type {import('./types.js').SvelteKitApp} */
177-
let app;
177+
export let app;
178178

179179
/** @type {Array<((url: URL) => boolean)>} */
180180
const invalidated = [];
@@ -2493,6 +2493,7 @@ async function load_data(url, invalid) {
24932493
*/
24942494
function deserialize(data) {
24952495
return devalue.unflatten(data, {
2496+
...app.decoders,
24962497
Promise: (id) => {
24972498
return new Promise((fulfil, reject) => {
24982499
deferreds.set(id, { fulfil, reject });

packages/kit/src/runtime/client/types.d.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ export interface SvelteKitApp {
2626

2727
hooks: ClientHooks;
2828

29+
decode: (type: string, value: any) => any;
30+
31+
decoders: Record<string, (data: any) => any>;
32+
2933
root: typeof SvelteComponent;
3034
}
3135

@@ -54,7 +58,7 @@ export type NavigationFinished = {
5458
state: NavigationState;
5559
props: {
5660
constructors: Array<typeof SvelteComponent>;
57-
components?: Array<SvelteComponent>;
61+
components?: SvelteComponent[];
5862
page: Page;
5963
form?: Record<string, any> | null;
6064
[key: `data_${number}`]: Record<string, any>;

packages/kit/src/runtime/server/data/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,9 @@ export function get_data_json(event, options, nodes) {
197197
const { iterator, push, done } = create_async_iterator();
198198

199199
const reducers = {
200+
...Object.fromEntries(
201+
Object.entries(options.hooks.transport).map(([key, value]) => [key, value.encode])
202+
),
200203
/** @param {any} thing */
201204
Promise: (thing) => {
202205
if (typeof thing?.then === 'function') {

packages/kit/src/runtime/server/index.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,8 @@ export class Server {
7676
handle: module.handle || (({ event, resolve }) => resolve(event)),
7777
handleError: module.handleError || (({ error }) => console.error(error)),
7878
handleFetch: module.handleFetch || (({ request, fetch }) => fetch(request)),
79-
reroute: module.reroute || (() => {})
79+
reroute: module.reroute || (() => {}),
80+
transport: module.transport || {}
8081
};
8182

8283
if (module.init) {
@@ -90,7 +91,8 @@ export class Server {
9091
},
9192
handleError: ({ error }) => console.error(error),
9293
handleFetch: ({ request, fetch }) => fetch(request),
93-
reroute: () => {}
94+
reroute: () => {},
95+
transport: {}
9496
};
9597
} else {
9698
throw error;

0 commit comments

Comments
 (0)