Skip to content

Commit 90b85d1

Browse files
add imperative hydratable API
1 parent 7ee0ce8 commit 90b85d1

File tree

17 files changed

+145
-162
lines changed

17 files changed

+145
-162
lines changed

packages/svelte/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,10 @@
9595
"types": "./types/index.d.ts",
9696
"default": "./src/server/index.js"
9797
},
98+
"./client": {
99+
"types": "./types/index.d.ts",
100+
"default": "./src/client/index.js"
101+
},
98102
"./store": {
99103
"types": "./types/index.d.ts",
100104
"worker": "./src/store/index-server.js",

packages/svelte/scripts/generate-types.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ await createBundle({
4545
[`${pkg.name}/reactivity`]: `${dir}/src/reactivity/index-client.js`,
4646
[`${pkg.name}/reactivity/window`]: `${dir}/src/reactivity/window/index.js`,
4747
[`${pkg.name}/server`]: `${dir}/src/server/index.d.ts`,
48+
[`${pkg.name}/client`]: `${dir}/src/client/index.d.ts`,
4849
[`${pkg.name}/store`]: `${dir}/src/store/public.d.ts`,
4950
[`${pkg.name}/transition`]: `${dir}/src/transition/public.d.ts`,
5051
[`${pkg.name}/events`]: `${dir}/src/events/public.d.ts`,
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { get_hydratable_value as getHydratableValue } from '../internal/client/hydratable.js';

packages/svelte/src/index-client.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -247,9 +247,9 @@ export {
247247
getContext,
248248
getAllContexts,
249249
hasContext,
250-
setContext,
251-
hydratable
250+
setContext
252251
} from './internal/client/context.js';
252+
export { hydratable } from './internal/client/hydratable.js';
253253
export { hydrate, mount, unmount } from './internal/client/render.js';
254254
export { tick, untrack, settled } from './internal/client/runtime.js';
255255
export { createRawSnippet } from './internal/client/dom/blocks/snippet.js';

packages/svelte/src/internal/client/context.js

Lines changed: 0 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -224,77 +224,6 @@ export function is_runes() {
224224
return !legacy_mode_flag || (component_context !== null && component_context.l === null);
225225
}
226226

227-
/** @type {string | null} */
228-
export let hydratable_key = null;
229-
230-
/** @param {string | null} key */
231-
export function set_hydratable_key(key) {
232-
hydratable_key = key;
233-
}
234-
235-
/**
236-
* @template T
237-
* @overload
238-
* @param {string} key
239-
* @param {() => Promise<T>} fn
240-
* @param {{ transport?: Transport<T> }} [options]
241-
* @returns {Promise<T>}
242-
*/
243-
/**
244-
* @template T
245-
* @overload
246-
* @param {() => Promise<T>} fn
247-
* @param {{ transport?: Transport<T> }} [options]
248-
* @returns {Promise<T>}
249-
*/
250-
/**
251-
* @template T
252-
* @param {string | (() => Promise<T>)} key_or_fn
253-
* @param {(() => Promise<T>) | { transport?: Transport<T> }} [fn_or_options]
254-
* @param {{ transport?: Transport<T> }} [maybe_options]
255-
* @returns {Promise<T>}
256-
*/
257-
export function hydratable(key_or_fn, fn_or_options = {}, maybe_options = {}) {
258-
/** @type {string} */
259-
let key;
260-
/** @type {() => Promise<T>} */
261-
let fn;
262-
/** @type {{ transport?: Transport<T> }} */
263-
let options;
264-
265-
if (typeof key_or_fn === 'string') {
266-
key = key_or_fn;
267-
fn = /** @type {() => Promise<T>} */ (fn_or_options);
268-
options = /** @type {{ transport?: Transport<T> }} */ (maybe_options);
269-
} else {
270-
if (hydratable_key === null) {
271-
throw new Error(
272-
'TODO error: `hydratable` must be called synchronously within `cache` in order to omit the key'
273-
);
274-
} else {
275-
key = hydratable_key;
276-
}
277-
fn = /** @type {() => Promise<T>} */ (key_or_fn);
278-
options = /** @type {{ transport?: Transport<T> }} */ (fn_or_options);
279-
}
280-
281-
if (!hydrating) {
282-
return Promise.resolve(fn());
283-
}
284-
var store = window.__svelte?.h;
285-
if (store === undefined) {
286-
throw new Error('TODO this should be impossible?');
287-
}
288-
if (!store.has(key)) {
289-
throw new Error(
290-
`TODO Expected hydratable key "${key}" to exist during hydration, but it does not`
291-
);
292-
}
293-
const entry = /** @type {string} */ (store.get(key));
294-
const parse = options.transport?.parse ?? ((val) => new Function(`return (${val})`)());
295-
return Promise.resolve(/** @type {T} */ (parse(entry)));
296-
}
297-
298227
/**
299228
* @param {string} name
300229
* @returns {Map<unknown, unknown>}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/** @import { Parse, Transport } from '#shared' */
2+
import { hydrating } from './dom/hydration';
3+
4+
/**
5+
* @template T
6+
* @param {string} key
7+
* @param {() => T} fn
8+
* @param {{ transport?: Transport<T> }} [options]
9+
* @returns {T}
10+
*/
11+
export function hydratable(key, fn, options = {}) {
12+
if (!hydrating) {
13+
return fn();
14+
}
15+
var store = window.__svelte?.h;
16+
if (store === undefined) {
17+
throw new Error('TODO this should be impossible?');
18+
}
19+
const val = store.get(key);
20+
if (val === undefined) {
21+
throw new Error(
22+
`TODO Expected hydratable key "${key}" to exist during hydration, but it does not`
23+
);
24+
}
25+
return parse(val, options.transport?.parse);
26+
}
27+
28+
/**
29+
* @template T
30+
* @param {string} key
31+
* @param {{ parse?: Parse<T> }} [options]
32+
* @returns {T | undefined}
33+
*/
34+
export function get_hydratable_value(key, options = {}) {
35+
// TODO probably can DRY this out with the above
36+
if (!hydrating) {
37+
return undefined;
38+
}
39+
40+
var store = window.__svelte?.h;
41+
if (store === undefined) {
42+
throw new Error('TODO this should be impossible?');
43+
}
44+
const val = store.get(key);
45+
if (val === undefined) {
46+
return undefined;
47+
}
48+
49+
return parse(val, options.parse);
50+
}
51+
52+
/**
53+
* @template T
54+
* @param {string} val
55+
* @param {Parse<T> | undefined} parse
56+
* @returns {T}
57+
*/
58+
function parse(val, parse) {
59+
return (parse ?? ((val) => new Function(`return (${val})`)()))(val);
60+
}

packages/svelte/src/internal/client/reactivity/cache.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { BaseCacheObserver } from '../../shared/cache-observer.js';
22
import { ObservableCache } from '../../shared/observable-cache.js';
3-
import { set_hydratable_key } from '../context.js';
43
import { tick } from '../runtime.js';
54
import { render_effect } from './effects.js';
65

@@ -38,9 +37,7 @@ export function cache(key, fn) {
3837
return entry?.item;
3938
}
4039

41-
set_hydratable_key(key);
4240
const item = fn();
43-
set_hydratable_key(null);
4441
const new_entry = {
4542
item,
4643
count: tracking ? 1 : 0
Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/** @import { GetRequestInit, Resource } from '#shared' */
22
import { cache } from './cache';
33
import { fetch_json } from '../../shared/utils.js';
4-
import { hydratable } from '../context';
4+
import { hydratable } from '../hydratable';
55
import { resource } from './resource';
66

77
/**
@@ -11,7 +11,6 @@ import { resource } from './resource';
1111
* @returns {Resource<TReturn>}
1212
*/
1313
export function fetcher(url, init) {
14-
return cache(`svelte/fetcher::::${typeof url === 'string' ? url : url.toString()}`, () =>
15-
resource(() => hydratable(() => fetch_json(url, init)))
16-
);
14+
const key = `svelte/fetcher/${typeof url === 'string' ? url : url.toString()}`;
15+
return cache(key, () => resource(() => hydratable(key, () => fetch_json(url, init))));
1716
}

packages/svelte/src/internal/client/reactivity/resource.js

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,21 @@ import { deferred } from '../../shared/utils.js';
55

66
/**
77
* @template T
8-
* @param {() => Promise<T>} fn
9-
* @returns {ResourceType<T>}
8+
* @param {() => T} fn
9+
* @returns {ResourceType<Awaited<T>>}
1010
*/
1111
export function resource(fn) {
12-
return /** @type {ResourceType<T>} */ (new Resource(fn));
12+
return /** @type {ResourceType<Awaited<T>>} */ (new Resource(fn));
1313
}
1414

1515
/**
1616
* @template T
17-
* @implements {Partial<Promise<T>>}
17+
* @implements {Partial<Promise<Awaited<T>>>}
1818
*/
1919
class Resource {
2020
#init = false;
2121

22-
/** @type {() => Promise<T>} */
22+
/** @type {() => T} */
2323
#fn;
2424

2525
/** @type {Source<boolean>} */
@@ -31,13 +31,13 @@ class Resource {
3131
/** @type {Source<boolean>} */
3232
#ready = state(false);
3333

34-
/** @type {Source<T | undefined>} */
34+
/** @type {Source<Awaited<T> | undefined>} */
3535
#raw = state(undefined);
3636

3737
/** @type {Source<Promise<any>>} */
3838
#promise;
3939

40-
/** @type {Derived<T | undefined>} */
40+
/** @type {Derived<Awaited<T> | undefined>} */
4141
#current = derived(() => {
4242
if (!get(this.#ready)) return undefined;
4343
return get(this.#raw);
@@ -46,7 +46,7 @@ class Resource {
4646
/** {@type Source<any>} */
4747
#error = state(undefined);
4848

49-
/** @type {Derived<Promise<T>['then']>} */
49+
/** @type {Derived<Promise<Awaited<T>>['then']>} */
5050
// @ts-expect-error - I feel this might actually be incorrect but I can't prove it yet.
5151
// we are technically not returning a promise that resolves to the correct type... but it _is_ a promise that resolves at the correct time
5252
#then = derived(() => {
@@ -57,15 +57,15 @@ class Resource {
5757
await p;
5858
await tick();
5959

60-
resolve?.(/** @type {T} */ (get(this.#current)));
60+
resolve?.(/** @type {Awaited<T>} */ (get(this.#current)));
6161
} catch (error) {
6262
reject?.(error);
6363
}
6464
};
6565
});
6666

6767
/**
68-
* @param {() => Promise<T>} fn
68+
* @param {() => T} fn
6969
*/
7070
constructor(fn) {
7171
this.#fn = fn;
@@ -166,7 +166,7 @@ class Resource {
166166
};
167167

168168
/**
169-
* @param {T} value
169+
* @param {Awaited<T>} value
170170
*/
171171
set = (value) => {
172172
set(this.#ready, true);
Lines changed: 24 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/** @import { Transport } from '#shared' */
1+
/** @import { Stringify, Transport } from '#shared' */
22

33
import { get_render_context } from './render-context';
44

@@ -12,58 +12,39 @@ export function set_hydratable_key(key) {
1212

1313
/**
1414
* @template T
15-
* @overload
1615
* @param {string} key
17-
* @param {() => Promise<T>} fn
16+
* @param {() => T} fn
1817
* @param {{ transport?: Transport<T> }} [options]
19-
* @returns {Promise<T>}
20-
*/
21-
/**
22-
* @template T
23-
* @overload
24-
* @param {() => Promise<T>} fn
25-
* @param {{ transport?: Transport<T> }} [options]
26-
* @returns {Promise<T>}
18+
* @returns {T}
2719
*/
20+
export function hydratable(key, fn, options = {}) {
21+
const store = get_render_context();
22+
23+
if (store.hydratables.has(key)) {
24+
// TODO error
25+
throw new Error("can't have two hydratables with the same key");
26+
}
27+
28+
const result = fn();
29+
store.hydratables.set(key, { value: result, stringify: options.transport?.stringify });
30+
return result;
31+
}
2832
/**
2933
* @template T
30-
* @param {string | (() => Promise<T>)} key_or_fn
31-
* @param {(() => Promise<T>) | { transport?: Transport<T> }} [fn_or_options]
32-
* @param {{ transport?: Transport<T> }} [maybe_options]
33-
* @returns {Promise<T>}
34+
* @param {string} key
35+
* @param {T} value
36+
* @param {{ stringify?: Stringify<T> }} [options]
3437
*/
35-
export async function hydratable(key_or_fn, fn_or_options = {}, maybe_options = {}) {
36-
// TODO DRY out with #shared
37-
/** @type {string} */
38-
let key;
39-
/** @type {() => Promise<T>} */
40-
let fn;
41-
/** @type {{ transport?: Transport<T> }} */
42-
let options;
43-
44-
if (typeof key_or_fn === 'string') {
45-
key = key_or_fn;
46-
fn = /** @type {() => Promise<T>} */ (fn_or_options);
47-
options = /** @type {{ transport?: Transport<T> }} */ (maybe_options);
48-
} else {
49-
if (hydratable_key === null) {
50-
throw new Error(
51-
'TODO error: `hydratable` must be called synchronously within `cache` in order to omit the key'
52-
);
53-
} else {
54-
key = hydratable_key;
55-
}
56-
fn = /** @type {() => Promise<T>} */ (key_or_fn);
57-
options = /** @type {{ transport?: Transport<T> }} */ (fn_or_options);
58-
}
59-
const store = await get_render_context();
38+
export function set_hydratable_value(key, value, options = {}) {
39+
const store = get_render_context();
6040

6141
if (store.hydratables.has(key)) {
6242
// TODO error
6343
throw new Error("can't have two hydratables with the same key");
6444
}
6545

66-
const result = fn();
67-
store.hydratables.set(key, { value: result, transport: options.transport });
68-
return result;
46+
store.hydratables.set(key, {
47+
value,
48+
stringify: options.stringify
49+
});
6950
}

0 commit comments

Comments
 (0)