-
-
Notifications
You must be signed in to change notification settings - Fork 4.7k
feat: hydratable and friends
#16960
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
feat: hydratable and friends
#16960
Changes from 38 commits
da3260f
4d766f8
7c8c1ad
83643ce
61d02fe
bca87b9
82be388
25210c2
9c7da6c
5de6383
8449ea7
ef11dae
d36894a
0c4ce5a
a2bff0c
2e292b1
7ee0ce8
90b85d1
598dc30
2c08d4f
5adb3f1
8179c6a
6873685
8939bbf
0f6001d
8667dab
200b011
816ddca
7d44a1d
cc094c5
c6da91f
b36ba6d
d6f240a
7d0451e
cd7a71f
0581bb9
5aa7598
08d755b
9a424cd
e28ced7
aaf2eb8
555e950
d5fef8b
3f24dd2
551572e
998510c
f9123f4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| export { | ||
| get_hydratable_value as getHydratableValue, | ||
| has_hydratable_value as hasHydratableValue | ||
| } from '../internal/client/hydratable.js'; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| /** @import { Decode, Transport } from '#shared' */ | ||
| import { hydrating } from './dom/hydration.js'; | ||
|
|
||
| /** | ||
| * @template T | ||
| * @param {string} key | ||
| * @param {() => T} fn | ||
| * @param {Transport<T>} [options] | ||
| * @returns {T} | ||
| */ | ||
| export function hydratable(key, fn, options) { | ||
| if (!hydrating) { | ||
| return fn(); | ||
| } | ||
| var store = window.__svelte?.h; | ||
| const val = store?.get(key); | ||
| if (val === undefined) { | ||
| // TODO this should really be an error or at least a warning because it would be disastrous to expect | ||
| // something to be synchronously hydratable and then have it not be | ||
| return fn(); | ||
| } | ||
| return decode(val, options?.decode); | ||
| } | ||
|
|
||
| /** | ||
| * @template T | ||
| * @param {string} key | ||
| * @param {{ decode?: Decode<T> }} [options] | ||
| * @returns {T | undefined} | ||
| */ | ||
| export function get_hydratable_value(key, options = {}) { | ||
| // TODO probably can DRY this out with the above | ||
| if (!hydrating) { | ||
| return undefined; | ||
| } | ||
|
|
||
| var store = window.__svelte?.h; | ||
| const val = store?.get(key); | ||
| if (val === undefined) { | ||
| return undefined; | ||
| } | ||
|
|
||
| return decode(val, options.decode); | ||
| } | ||
|
|
||
| /** | ||
| * @param {string} key | ||
| * @returns {boolean} | ||
| */ | ||
| export function has_hydratable_value(key) { | ||
| if (!hydrating) { | ||
| return false; | ||
| } | ||
| var store = window.__svelte?.h; | ||
| return store?.has(key) ?? false; | ||
| } | ||
|
|
||
| /** | ||
| * @template T | ||
| * @param {unknown} val | ||
| * @param {Decode<T> | undefined} decode | ||
| * @returns {T} | ||
| */ | ||
| function decode(val, decode) { | ||
| return (decode ?? ((val) => /** @type {T} */ (val)))(val); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| /** @import { CacheEntry } from '#shared' */ | ||
| import { BaseCacheObserver } from '../../shared/cache-observer.js'; | ||
| import { tick } from '../runtime.js'; | ||
| import { get_effect_validation_error_code, render_effect } from './effects.js'; | ||
|
|
||
| /** @typedef {{ count: number, item: any }} Entry */ | ||
| /** @type {Map<string, CacheEntry>} */ | ||
| const client_cache = new Map(); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Given that the intent here is for libraries to provide a prefix if they're going to use this API, should this be a two-tiered cache? Right now, if a library (like SvelteKit) wants to do something to everything it has in the cache, it has to iterate over all of the map entries, skipping the ones that don't start with its prefix, and refresh the things that do start with its prefix. Maybe what we should do is make this two-tiered, where, if you don't provide a prefix, everything gets put into From an API perspective, you can provide a The downside would be if you truly wanted to operate on the entire cache, which would be more complicated... |
||
|
|
||
| /** | ||
| * @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); | ||
|
|
||
| const tracking = get_effect_validation_error_code() === null; | ||
| if (tracking) { | ||
| render_effect(() => { | ||
| if (entry) entry.count++; | ||
| return () => { | ||
| const entry = client_cache.get(key); | ||
| if (!entry) return; | ||
| entry.count--; | ||
| maybe_remove(entry); | ||
| }; | ||
| }); | ||
| } | ||
|
|
||
| if (cached) { | ||
| return entry?.item; | ||
| } | ||
|
|
||
| const item = fn(); | ||
| 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); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * @template T | ||
| * @extends BaseCacheObserver<T> | ||
| */ | ||
| export class CacheObserver extends BaseCacheObserver { | ||
| constructor(prefix = '') { | ||
| super(() => client_cache, prefix); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| /** @import { GetRequestInit, Resource } from '#shared' */ | ||
| import { cache } from './cache'; | ||
| import { fetch_json } from '../../shared/utils.js'; | ||
| import { hydratable } from '../hydratable'; | ||
| import { resource } from './resource'; | ||
|
|
||
| /** | ||
| * @template TReturn | ||
| * @param {string | URL} url | ||
| * @param {GetRequestInit} [init] | ||
| * @returns {Resource<TReturn>} | ||
| */ | ||
| export function fetcher(url, init) { | ||
| const key = `svelte/fetcher/${typeof url === 'string' ? url : url.toString()}`; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Given how simple this is I'm quite tempted to say "nah, this can be an example in the docs" instead of shipping it as a core API... |
||
| return cache(key, () => resource(() => hydratable(key, () => fetch_json(url, init)))); | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.