Skip to content

Commit d36894a

Browse files
it at least basically works
1 parent ef11dae commit d36894a

File tree

12 files changed

+212
-128
lines changed

12 files changed

+212
-128
lines changed

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

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -224,14 +224,60 @@ 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+
227235
/**
228236
* @template T
237+
* @overload
229238
* @param {string} key
230239
* @param {() => T} fn
231240
* @param {{ transport?: Transport<T> }} [options]
232-
* @returns {Promise<T>}
241+
* @returns {Promise<Awaited<T>>}
242+
*/
243+
/**
244+
* @template T
245+
* @overload
246+
* @param {() => T} fn
247+
* @param {{ transport?: Transport<T> }} [options]
248+
* @returns {Promise<Awaited<T>>}
249+
*/
250+
/**
251+
* @template T
252+
* @param {string | (() => T)} key_or_fn
253+
* @param {(() => T) | { transport?: Transport<T> }} [fn_or_options]
254+
* @param {{ transport?: Transport<T> }} [maybe_options]
255+
* @returns {Promise<Awaited<T>>}
233256
*/
234-
export function hydratable(key, fn, { transport } = {}) {
257+
export function hydratable(key_or_fn, fn_or_options = {}, maybe_options = {}) {
258+
/** @type {string} */
259+
let key;
260+
/** @type {() => 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 {() => 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 {() => T} */ (key_or_fn);
278+
options = /** @type {{ transport?: Transport<T> }} */ (fn_or_options);
279+
}
280+
235281
if (!hydrating) {
236282
return Promise.resolve(fn());
237283
}
@@ -245,7 +291,7 @@ export function hydratable(key, fn, { transport } = {}) {
245291
);
246292
}
247293
const entry = /** @type {string} */ (store.get(key));
248-
const parse = transport?.parse ?? ((val) => new Function(`return (${val})`)());
294+
const parse = options.transport?.parse ?? ((val) => new Function(`return (${val})`)());
249295
return Promise.resolve(/** @type {T} */ (parse(entry)));
250296
}
251297

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

Lines changed: 40 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { set_hydratable_key } from '../context.js';
12
import { tick } from '../runtime.js';
23
import { render_effect } from './effects.js';
34

@@ -6,52 +7,49 @@ import { render_effect } from './effects.js';
67
const client_cache = new Map();
78

89
/**
9-
* @template TReturn
10-
* @template {unknown} TArg
11-
* @param {string} name
12-
* @param {(arg: TArg, key: string) => TReturn} fn
13-
* @param {{ hash?: (arg: TArg) => string }} [options]
14-
* @returns {(arg: TArg) => TReturn}
10+
* @template {(...args: any[]) => any} TFn
11+
* @param {string} key
12+
* @param {TFn} fn
13+
* @returns {ReturnType<TFn>}
1514
*/
16-
export function cache(name, fn, { hash = default_hash } = {}) {
17-
return (arg) => {
18-
const key = `${name}::::${hash(arg)}`;
19-
const cached = client_cache.has(key);
20-
const entry = client_cache.get(key);
21-
const maybe_remove = create_remover(key);
22-
23-
let tracking = true;
24-
try {
25-
render_effect(() => {
26-
if (entry) entry.count++;
27-
return () => {
28-
const entry = client_cache.get(key);
29-
if (!entry) return;
30-
entry.count--;
31-
maybe_remove(entry);
32-
};
33-
});
34-
} catch {
35-
tracking = false;
36-
}
15+
export function cache(key, fn) {
16+
const cached = client_cache.has(key);
17+
const entry = client_cache.get(key);
18+
const maybe_remove = create_remover(key);
19+
20+
let tracking = true;
21+
try {
22+
render_effect(() => {
23+
if (entry) entry.count++;
24+
return () => {
25+
const entry = client_cache.get(key);
26+
if (!entry) return;
27+
entry.count--;
28+
maybe_remove(entry);
29+
};
30+
});
31+
} catch {
32+
tracking = false;
33+
}
3734

38-
if (cached) {
39-
return entry?.item;
40-
}
35+
if (cached) {
36+
return entry?.item;
37+
}
4138

42-
const item = fn(arg, key);
43-
const new_entry = {
44-
item,
45-
count: tracking ? 1 : 0
46-
};
47-
client_cache.set(key, new_entry);
48-
49-
Promise.resolve(item).then(
50-
() => maybe_remove(new_entry),
51-
() => maybe_remove(new_entry)
52-
);
53-
return item;
39+
set_hydratable_key(key);
40+
const item = fn();
41+
set_hydratable_key(null);
42+
const new_entry = {
43+
item,
44+
count: tracking ? 1 : 0
5445
};
46+
client_cache.set(key, new_entry);
47+
48+
Promise.resolve(item).then(
49+
() => maybe_remove(new_entry),
50+
() => maybe_remove(new_entry)
51+
);
52+
return item;
5553
}
5654

5755
/**
@@ -124,11 +122,3 @@ const readonly_cache = new ReadonlyCache();
124122
export function get_cache() {
125123
return readonly_cache;
126124
}
127-
128-
/**
129-
* @param {...any} args
130-
* @returns
131-
*/
132-
function default_hash(...args) {
133-
return JSON.stringify(args);
134-
}
Lines changed: 12 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,17 @@
1-
/** @import { StandardSchemaV1 } from '@standard-schema/spec' */
1+
/** @import { GetRequestInit, Resource } from '#shared' */
22
import { cache } from './cache';
3+
import { fetch_json } from '../../shared/utils.js';
4+
import { hydratable } from '../context';
5+
import { resource } from './resource';
36

47
/**
5-
* @template {StandardSchemaV1} TSchema
6-
* @param {{ schema?: TSchema, url: string | URL, init?: RequestInit }} args
7-
* @param {string} [key]
8+
* @template TReturn
9+
* @param {string | URL} url
10+
* @param {GetRequestInit} [init]
11+
* @returns {Resource<TReturn>}
812
*/
9-
async function fetcher_impl({ schema, url, init }, key) {
10-
const response = await fetch(url, init);
11-
if (!response.ok) {
12-
throw new Error(`Fetch error: ${response.status} ${response.statusText}`);
13-
}
14-
if (schema) {
15-
const data = await response.json();
16-
return schema['~standard'].validate(data);
17-
}
18-
return response.json();
19-
}
20-
21-
const cached_fetch = cache('svelte/fetcher', fetcher_impl, {
22-
hash: (arg) => {
23-
return `${typeof arg.url === 'string' ? arg.url : arg.url.toString()}}`;
24-
}
25-
});
26-
27-
/**
28-
* @template {StandardSchemaV1} TSchema
29-
* @overload
30-
* @param {{ schema: TSchema, url: string | URL, init?: RequestInit }} arg
31-
* @returns {Promise<StandardSchemaV1.InferOutput<TSchema>>}
32-
*/
33-
/**
34-
* @overload
35-
* @param {{ schema?: undefined, url: string | URL, init?: RequestInit }} arg
36-
* @returns {Promise<any>}
37-
*/
38-
/**
39-
* @template {StandardSchemaV1} TSchema
40-
* @param {{ schema?: TSchema, url: string | URL, init?: RequestInit }} arg
41-
*/
42-
export function fetcher(arg) {
43-
return cached_fetch(arg);
13+
export function fetcher(url, init) {
14+
return cache(`svelte/fetcher::::${typeof url === 'string' ? url : url.toString()}`, () =>
15+
resource(() => hydratable(() => fetch_json(url, init)))
16+
);
4417
}

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

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,14 +125,60 @@ export async function save(promise) {
125125
};
126126
}
127127

128+
/** @type {string | null} */
129+
export let hydratable_key = null;
130+
131+
/** @param {string | null} key */
132+
export function set_hydratable_key(key) {
133+
hydratable_key = key;
134+
}
135+
128136
/**
129137
* @template T
138+
* @overload
130139
* @param {string} key
131140
* @param {() => T} fn
132141
* @param {{ transport?: Transport<T> }} [options]
133-
* @returns {Promise<T>}
142+
* @returns {Promise<Awaited<T>>}
134143
*/
135-
export function hydratable(key, fn, { transport } = {}) {
144+
/**
145+
* @template T
146+
* @overload
147+
* @param {() => T} fn
148+
* @param {{ transport?: Transport<T> }} [options]
149+
* @returns {Promise<Awaited<T>>}
150+
*/
151+
/**
152+
* @template T
153+
* @param {string | (() => T)} key_or_fn
154+
* @param {(() => T) | { transport?: Transport<T> }} [fn_or_options]
155+
* @param {{ transport?: Transport<T> }} [maybe_options]
156+
* @returns {Promise<Awaited<T>>}
157+
*/
158+
export function hydratable(key_or_fn, fn_or_options = {}, maybe_options = {}) {
159+
// TODO DRY out with #shared
160+
/** @type {string} */
161+
let key;
162+
/** @type {() => T} */
163+
let fn;
164+
/** @type {{ transport?: Transport<T> }} */
165+
let options;
166+
167+
if (typeof key_or_fn === 'string') {
168+
key = key_or_fn;
169+
fn = /** @type {() => T} */ (fn_or_options);
170+
options = /** @type {{ transport?: Transport<T> }} */ (maybe_options);
171+
} else {
172+
if (hydratable_key === null) {
173+
throw new Error(
174+
'TODO error: `hydratable` must be called synchronously within `cache` in order to omit the key'
175+
);
176+
} else {
177+
key = hydratable_key;
178+
}
179+
fn = /** @type {() => T} */ (key_or_fn);
180+
options = /** @type {{ transport?: Transport<T> }} */ (fn_or_options);
181+
}
136182
const store = get_render_store();
137183

138184
if (store.hydratables.has(key)) {
@@ -141,7 +187,7 @@ export function hydratable(key, fn, { transport } = {}) {
141187
}
142188

143189
const result = fn();
144-
store.hydratables.set(key, { value: result, transport });
190+
store.hydratables.set(key, { value: result, transport: options.transport });
145191
return Promise.resolve(result);
146192
}
147193

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

Lines changed: 16 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,22 @@
1-
import { get_render_store } from '../context';
1+
import { get_render_store, set_hydratable_key } from '../context';
22

33
/**
4-
* @template TReturn
5-
* @template {unknown} TArg
6-
* @param {string} name
7-
* @param {(arg: TArg, key: string) => TReturn} fn
8-
* @param {{ hash?: (arg: TArg) => string }} [options]
9-
* @returns {(arg: TArg) => TReturn}
4+
* @template {(...args: any[]) => any} TFn
5+
* @param {string} key
6+
* @param {TFn} fn
7+
* @returns {ReturnType<TFn>}
108
*/
11-
export function cache(name, fn, { hash = default_hash } = {}) {
12-
return (arg) => {
13-
const cache = get_render_store().cache;
14-
const key = `${name}::::${hash(arg)}`;
15-
const entry = cache.get(key);
16-
if (entry) {
17-
return /** @type {TReturn} */ (entry);
18-
}
19-
const new_entry = fn(arg, key);
20-
cache.set(key, new_entry);
21-
return new_entry;
22-
};
23-
}
24-
25-
/**
26-
* @param {any} arg
27-
* @returns {string}
28-
*/
29-
function default_hash(arg) {
30-
return JSON.stringify(arg);
9+
export function cache(key, fn) {
10+
const cache = get_render_store().cache;
11+
const entry = cache.get(key);
12+
if (entry) {
13+
return /** @type {ReturnType<TFn>} */ (entry);
14+
}
15+
set_hydratable_key(key);
16+
const new_entry = fn();
17+
set_hydratable_key(null);
18+
cache.set(key, new_entry);
19+
return new_entry;
3120
}
3221

3322
export function get_cache() {
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/** @import { GetRequestInit, Resource } from '#shared' */
2+
import { fetch_json } from '../../shared/utils.js';
3+
import { hydratable } from '../context.js';
4+
import { cache } from './cache';
5+
import { resource } from './resource.js';
6+
7+
/**
8+
* @template TReturn
9+
* @param {string | URL} url
10+
* @param {GetRequestInit} [init]
11+
* @returns {Resource<TReturn>}
12+
*/
13+
export function fetcher(url, init) {
14+
return cache(`svelte/fetcher::::${typeof url === 'string' ? url : url.toString()}`, () =>
15+
resource(() => hydratable(() => fetch_json(url, init)))
16+
);
17+
}

packages/svelte/src/internal/server/renderer.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -375,7 +375,7 @@ export class Renderer {
375375
});
376376
return Promise.resolve(user_result);
377377
}
378-
async ??= with_render_store({ hydratables: new Map(), resources: new Map() }, () =>
378+
async ??= with_render_store({ hydratables: new Map(), cache: new Map() }, () =>
379379
Renderer.#render_async(component, options)
380380
);
381381
return async.then((result) => {

packages/svelte/src/internal/shared/types.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,5 @@ export type Resource<T> = {
3535
error: any;
3636
}
3737
);
38+
39+
export type GetRequestInit = Omit<RequestInit, 'method' | 'body'> & { method?: 'GET' };

0 commit comments

Comments
 (0)