Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions packages/svelte/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -168,12 +168,14 @@
"dependencies": {
"@jridgewell/remapping": "^2.3.4",
"@jridgewell/sourcemap-codec": "^1.5.0",
"@standard-schema/spec": "^1.0.0",
"@sveltejs/acorn-typescript": "^1.0.5",
"@types/estree": "^1.0.5",
"acorn": "^8.12.1",
"aria-query": "^5.3.1",
"axobject-query": "^4.1.0",
"clsx": "^2.1.1",
"devalue": "^5.4.1",
"esm-env": "^1.2.1",
"esrap": "^2.1.0",
"is-reference": "^3.0.3",
Expand Down
3 changes: 2 additions & 1 deletion packages/svelte/src/index-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,8 @@ export {
getContext,
getAllContexts,
hasContext,
setContext
setContext,
hydratable
} from './internal/client/context.js';
export { hydrate, mount, unmount } from './internal/client/render.js';
export { tick, untrack, settled } from './internal/client/runtime.js';
Expand Down
3 changes: 2 additions & 1 deletion packages/svelte/src/index-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ export {
getAllContexts,
getContext,
hasContext,
setContext
setContext,
hydratable
} from './internal/server/context.js';

export { createRawSnippet } from './internal/server/blocks/snippet.js';
73 changes: 73 additions & 0 deletions packages/svelte/src/internal/client/context.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
/** @import { ComponentContext, DevStackEntry, Effect } from '#client' */
/** @import { Transport } from '#shared' */
import { DEV } from 'esm-env';
import * as e from './errors.js';
import { active_effect, active_reaction } from './runtime.js';
import { create_user_effect } from './reactivity/effects.js';
import { async_mode_flag, legacy_mode_flag } from '../flags/index.js';
import { FILENAME } from '../../constants.js';
import { BRANCH_EFFECT, EFFECT_RAN } from './constants.js';
import { hydrating } from './dom/hydration.js';

/** @type {ComponentContext | null} */
export let component_context = null;
Expand Down Expand Up @@ -222,6 +224,77 @@ export function is_runes() {
return !legacy_mode_flag || (component_context !== null && component_context.l === null);
}

/** @type {string | null} */
export let hydratable_key = null;

/** @param {string | null} key */
export function set_hydratable_key(key) {
hydratable_key = key;
}

/**
* @template T
* @overload
* @param {string} key
* @param {() => T} fn
* @param {{ transport?: Transport<T> }} [options]
* @returns {Promise<Awaited<T>>}
*/
/**
* @template T
* @overload
* @param {() => T} fn
* @param {{ transport?: Transport<T> }} [options]
* @returns {Promise<Awaited<T>>}
*/
/**
* @template T
* @param {string | (() => T)} key_or_fn
* @param {(() => T) | { transport?: Transport<T> }} [fn_or_options]
* @param {{ transport?: Transport<T> }} [maybe_options]
* @returns {Promise<Awaited<T>>}
*/
export function hydratable(key_or_fn, fn_or_options = {}, maybe_options = {}) {
/** @type {string} */
let key;
/** @type {() => T} */
let fn;
/** @type {{ transport?: Transport<T> }} */
let options;

if (typeof key_or_fn === 'string') {
key = key_or_fn;
fn = /** @type {() => T} */ (fn_or_options);
options = /** @type {{ transport?: Transport<T> }} */ (maybe_options);
} else {
if (hydratable_key === null) {
throw new Error(
'TODO error: `hydratable` must be called synchronously within `cache` in order to omit the key'
);
} else {
key = hydratable_key;
}
fn = /** @type {() => T} */ (key_or_fn);
options = /** @type {{ transport?: Transport<T> }} */ (fn_or_options);
}

if (!hydrating) {
return Promise.resolve(fn());
}
var store = window.__svelte?.h;
if (store === undefined) {
throw new Error('TODO this should be impossible?');
}
if (!store.has(key)) {
throw new Error(
`TODO Expected hydratable key "${key}" to exist during hydration, but it does not`
);
}
const entry = /** @type {string} */ (store.get(key));
const parse = options.transport?.parse ?? ((val) => new Function(`return (${val})`)());
return Promise.resolve(/** @type {T} */ (parse(entry)));
}

/**
* @param {string} name
* @returns {Map<unknown, unknown>}
Expand Down
124 changes: 124 additions & 0 deletions packages/svelte/src/internal/client/reactivity/cache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { set_hydratable_key } from '../context.js';
import { tick } from '../runtime.js';
import { render_effect } from './effects.js';

/** @typedef {{ count: number, item: any }} Entry */
/** @type {Map<string, Entry>} */
const client_cache = new Map();

/**
* @template {(...args: any[]) => any} TFn
* @param {string} key
* @param {TFn} fn
* @returns {ReturnType<TFn>}
*/
export function cache(key, fn) {
const cached = client_cache.has(key);
const entry = client_cache.get(key);
const maybe_remove = create_remover(key);

let tracking = true;
try {
render_effect(() => {
if (entry) entry.count++;
return () => {
const entry = client_cache.get(key);
if (!entry) return;
entry.count--;
maybe_remove(entry);
};
});
} catch {
tracking = false;
}

if (cached) {
return entry?.item;
}

set_hydratable_key(key);
const item = fn();
set_hydratable_key(null);
const new_entry = {
item,
count: tracking ? 1 : 0
};
client_cache.set(key, new_entry);

Promise.resolve(item).then(
() => maybe_remove(new_entry),
() => maybe_remove(new_entry)
);
return item;
}

/**
* @param {string} key
*/
function create_remover(key) {
/**
* @param {Entry | undefined} entry
*/
return (entry) =>
tick().then(() => {
if (!entry?.count && entry === client_cache.get(key)) {
client_cache.delete(key);
}
});
}

/** @implements {ReadonlyMap<string, any>} */
class ReadonlyCache {
/** @type {ReadonlyMap<string, any>['get']} */
get(key) {
const entry = client_cache.get(key);
return entry?.item;
}

/** @type {ReadonlyMap<string, any>['has']} */
has(key) {
return client_cache.has(key);
}

/** @type {ReadonlyMap<string, any>['size']} */
get size() {
return client_cache.size;
}

/** @type {ReadonlyMap<string, any>['forEach']} */
forEach(cb) {
client_cache.forEach((entry, key) => cb(entry.item, key, this));
}

/** @type {ReadonlyMap<string, any>['entries']} */
*entries() {
for (const [key, entry] of client_cache.entries()) {
yield [key, entry.item];
}
}

/** @type {ReadonlyMap<string, any>['keys']} */
*keys() {
for (const key of client_cache.keys()) {
yield key;
}
}

/** @type {ReadonlyMap<string, any>['values']} */
*values() {
for (const entry of client_cache.values()) {
yield entry.item;
}
}

[Symbol.iterator]() {
return this.entries();
}
}

const readonly_cache = new ReadonlyCache();

/** @returns {ReadonlyMap<string, any>} */
export function get_cache() {
return readonly_cache;
}
17 changes: 17 additions & 0 deletions packages/svelte/src/internal/client/reactivity/fetcher.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/** @import { GetRequestInit, Resource } from '#shared' */
import { cache } from './cache';
import { fetch_json } from '../../shared/utils.js';
import { hydratable } from '../context';
import { resource } from './resource';

/**
* @template TReturn
* @param {string | URL} url
* @param {GetRequestInit} [init]
* @returns {Resource<TReturn>}
*/
export function fetcher(url, init) {
return cache(`svelte/fetcher::::${typeof url === 'string' ? url : url.toString()}`, () =>
resource(() => hydratable(() => fetch_json(url, init)))
);
}
Loading
Loading