split stuff out, fix treeshaking

pull/17124/head
Elliott Johnson 3 weeks ago
parent 0c4ce5a9ec
commit a2bff0c0b5

@ -68,8 +68,8 @@ function create_remover(key) {
}); });
} }
export class CacheObserver extends BaseCacheObserver { // export class CacheObserver extends BaseCacheObserver {
constructor() { // constructor() {
super(client_cache); // super(client_cache);
} // }
} // }

@ -3,6 +3,7 @@
/** @import { Transport } from '#shared' */ /** @import { Transport } from '#shared' */
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import * as e from './errors.js'; import * as e from './errors.js';
import { save_render_context } from './render-context.js';
/** @type {SSRContext | null} */ /** @type {SSRContext | null} */
export var ssr_context = null; export var ssr_context = null;
@ -115,139 +116,10 @@ function get_parent_context(ssr_context) {
*/ */
export async function save(promise) { export async function save(promise) {
var previous_context = ssr_context; var previous_context = ssr_context;
var previous_sync_store = sync_store; const restore_render_context = await save_render_context(promise);
var value = await promise;
return () => { return () => {
ssr_context = previous_context; ssr_context = previous_context;
sync_store = previous_sync_store; return restore_render_context();
return value;
}; };
} }
/** @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 {() => Promise<T>} fn
* @param {{ transport?: Transport<T> }} [options]
* @returns {Promise<T>}
*/
/**
* @template T
* @overload
* @param {() => Promise<T>} fn
* @param {{ transport?: Transport<T> }} [options]
* @returns {Promise<T>}
*/
/**
* @template T
* @param {string | (() => Promise<T>)} key_or_fn
* @param {(() => Promise<T>) | { transport?: Transport<T> }} [fn_or_options]
* @param {{ transport?: Transport<T> }} [maybe_options]
* @returns {Promise<T>}
*/
export function hydratable(key_or_fn, fn_or_options = {}, maybe_options = {}) {
// TODO DRY out with #shared
/** @type {string} */
let key;
/** @type {() => Promise<T>} */
let fn;
/** @type {{ transport?: Transport<T> }} */
let options;
if (typeof key_or_fn === 'string') {
key = key_or_fn;
fn = /** @type {() => Promise<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 {() => Promise<T>} */ (key_or_fn);
options = /** @type {{ transport?: Transport<T> }} */ (fn_or_options);
}
const store = get_render_store();
if (store.hydratables.has(key)) {
// TODO error
throw new Error("can't have two hydratables with the same key");
}
const result = fn();
store.hydratables.set(key, { value: result, transport: options.transport });
return Promise.resolve(result);
}
/** @type {RenderContext | null} */
export let sync_store = null;
/** @param {RenderContext | null} store */
export function set_sync_store(store) {
sync_store = store;
}
/** @type {AsyncLocalStorage<RenderContext | null> | null} */
let als = null;
import('node:async_hooks')
.then((hooks) => (als = new hooks.AsyncLocalStorage()))
.catch(() => {
// can't use ALS but can still use manual context preservation
return null;
});
/** @returns {RenderContext | null} */
function try_get_render_store() {
return sync_store ?? als?.getStore() ?? null;
}
/** @returns {RenderContext} */
export function get_render_store() {
const store = try_get_render_store();
if (!store) {
// TODO make this a proper e.error
let message = 'Could not get rendering context.';
if (als) {
message += ' This is an internal error.';
} else {
message +=
' In environments without `AsyncLocalStorage`, `hydratable` must be accessed synchronously, not after an `await`.' +
' If it was accessed synchronously then this is an internal error.';
}
throw new Error(message);
}
return store;
}
/**
* @template T
* @param {RenderContext} store
* @param {() => Promise<T>} fn
* @returns {Promise<T>}
*/
export function with_render_store(store, fn) {
try {
sync_store = store;
const storage = als;
return storage ? storage.run(store, fn) : fn();
} finally {
sync_store = null;
}
}

@ -0,0 +1,69 @@
/** @import { Transport } from '#shared' */
import { get_render_context } from './render-context';
/** @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 {() => Promise<T>} fn
* @param {{ transport?: Transport<T> }} [options]
* @returns {Promise<T>}
*/
/**
* @template T
* @overload
* @param {() => Promise<T>} fn
* @param {{ transport?: Transport<T> }} [options]
* @returns {Promise<T>}
*/
/**
* @template T
* @param {string | (() => Promise<T>)} key_or_fn
* @param {(() => Promise<T>) | { transport?: Transport<T> }} [fn_or_options]
* @param {{ transport?: Transport<T> }} [maybe_options]
* @returns {Promise<T>}
*/
export async function hydratable(key_or_fn, fn_or_options = {}, maybe_options = {}) {
// TODO DRY out with #shared
/** @type {string} */
let key;
/** @type {() => Promise<T>} */
let fn;
/** @type {{ transport?: Transport<T> }} */
let options;
if (typeof key_or_fn === 'string') {
key = key_or_fn;
fn = /** @type {() => Promise<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 {() => Promise<T>} */ (key_or_fn);
options = /** @type {{ transport?: Transport<T> }} */ (fn_or_options);
}
const store = await get_render_context();
if (store.hydratables.has(key)) {
// TODO error
throw new Error("can't have two hydratables with the same key");
}
const result = fn();
store.hydratables.set(key, { value: result, transport: options.transport });
return result;
}

@ -1,5 +1,5 @@
import { BaseCacheObserver } from '../../shared/cache-observer'; import { set_hydratable_key } from '../hydratable';
import { get_render_store, set_hydratable_key } from '../context'; import { get_render_context } from '../render-context';
/** /**
* @template {(...args: any[]) => any} TFn * @template {(...args: any[]) => any} TFn
@ -8,7 +8,7 @@ import { get_render_store, set_hydratable_key } from '../context';
* @returns {ReturnType<TFn>} * @returns {ReturnType<TFn>}
*/ */
export function cache(key, fn) { export function cache(key, fn) {
const cache = get_render_store().cache; const cache = get_render_context().cache;
const entry = cache.get(key); const entry = cache.get(key);
if (entry) { if (entry) {
return /** @type {ReturnType<TFn>} */ (entry); return /** @type {ReturnType<TFn>} */ (entry);
@ -20,8 +20,9 @@ export function cache(key, fn) {
return new_entry; return new_entry;
} }
export class CacheObserver extends BaseCacheObserver { // TODO, has to be async
constructor() { // export class CacheObserver extends BaseCacheObserver {
super(get_render_store().cache); // constructor() {
} // super(get_render_store().cache);
} // }
// }

@ -1,6 +1,6 @@
/** @import { GetRequestInit, Resource } from '#shared' */ /** @import { GetRequestInit, Resource } from '#shared' */
import { fetch_json } from '../../shared/utils.js'; import { fetch_json } from '../../shared/utils.js';
import { hydratable } from '../context.js'; import { hydratable } from '../hydratable.js';
import { cache } from './cache'; import { cache } from './cache';
import { resource } from './resource.js'; import { resource } from './resource.js';

@ -0,0 +1,97 @@
/** @import { AsyncLocalStorage } from 'node:async_hooks' */
/** @import { RenderContext } from '#server' */
import { deferred } from '../shared/utils';
/** @type {Promise<void> | null} */
let current_render = null;
/** @type {RenderContext | null} */
let sync_context = null;
/**
* @template T
* @param {Promise<T>} promise
* @returns {Promise<() => T>}
*/
export async function save_render_context(promise) {
var previous_context = sync_context;
var value = await promise;
return () => {
sync_context = previous_context;
return value;
};
}
/** @returns {RenderContext | null} */
export function try_get_render_context() {
if (sync_context !== null) {
return sync_context;
}
return als?.getStore() ?? null;
}
/** @returns {RenderContext} */
export function get_render_context() {
const store = try_get_render_context();
if (!store) {
// TODO make this a proper e.error
let message = 'Could not get rendering context.';
if (als) {
message += ' You may have called `hydratable` or `cache` outside of the render lifecycle.';
} else {
message +=
' In environments without `AsyncLocalStorage`, `hydratable` must be accessed synchronously, not after an `await`.' +
' If it was accessed synchronously then this is an internal error or you may have called `hydratable` or `cache` outside of the render lifecycle.';
}
throw new Error(message);
}
return store;
}
/**
* @template T
* @param {() => Promise<T>} fn
* @returns {Promise<T>}
*/
export async function with_render_context(fn) {
try {
sync_context = {
hydratables: new Map(),
cache: new Map()
};
if (in_webcontainer()) {
const { promise, resolve } = deferred();
const previous_render = current_render;
current_render = promise;
await previous_render;
return fn().finally(resolve);
}
return als ? als.run(sync_context, fn) : fn();
} finally {
if (!in_webcontainer()) {
sync_context = null;
}
}
}
/** @type {AsyncLocalStorage<RenderContext | null> | null} */
let als = null;
export async function init_render_context() {
if (als !== null) return;
try {
const { AsyncLocalStorage } = await import('node:async_hooks');
als = new AsyncLocalStorage();
} catch {}
}
function in_webcontainer() {
// eslint-disable-next-line n/prefer-global/process
return !!globalThis.process?.versions?.webcontainer;
}

@ -3,20 +3,12 @@
/** @import { MaybePromise } from '#shared' */ /** @import { MaybePromise } from '#shared' */
import { async_mode_flag } from '../flags/index.js'; import { async_mode_flag } from '../flags/index.js';
import { abort } from './abort-signal.js'; import { abort } from './abort-signal.js';
import { import { pop, push, set_ssr_context, ssr_context, save } from './context.js';
get_render_store,
pop,
push,
set_ssr_context,
set_sync_store,
ssr_context,
sync_store,
with_render_store
} from './context.js';
import * as e from './errors.js'; import * as e from './errors.js';
import { BLOCK_CLOSE, BLOCK_OPEN } from './hydration.js'; import { BLOCK_CLOSE, BLOCK_OPEN } from './hydration.js';
import { attributes } from './index.js'; import { attributes } from './index.js';
import { uneval } from 'devalue'; import { uneval } from 'devalue';
import { get_render_context, with_render_context, init_render_context } from './render-context.js';
/** @typedef {'head' | 'body'} RendererType */ /** @typedef {'head' | 'body'} RendererType */
/** @typedef {{ [key in RendererType]: string }} AccumulatedContent */ /** @typedef {{ [key in RendererType]: string }} AccumulatedContent */
@ -375,8 +367,8 @@ export class Renderer {
}); });
return Promise.resolve(user_result); return Promise.resolve(user_result);
} }
async ??= with_render_store({ hydratables: new Map(), cache: new Map() }, () => async ??= init_render_context().then(() =>
Renderer.#render_async(component, options) with_render_context(() => Renderer.#render_async(component, options))
); );
return async.then((result) => { return async.then((result) => {
Object.defineProperty(result, 'html', { Object.defineProperty(result, 'html', {
@ -469,23 +461,24 @@ export class Renderer {
* @returns {Promise<AccumulatedContent>} * @returns {Promise<AccumulatedContent>}
*/ */
static async #render_async(component, options) { static async #render_async(component, options) {
var previous_context = ssr_context; const restore = await save(
var previous_sync_store = sync_store; (async () => {
try {
try { const renderer = Renderer.#open_render('async', component, options);
const renderer = Renderer.#open_render('async', component, options);
const content = await renderer.#collect_content_async();
const hydratables = await renderer.#collect_hydratables();
if (hydratables !== null) {
content.head = hydratables + content.head;
}
return Renderer.#close_render(content, renderer);
} finally {
abort();
}
})()
);
const content = await renderer.#collect_content_async(); return restore();
const hydratables = await renderer.#collect_hydratables();
if (hydratables !== null) {
content.head = hydratables + content.head;
}
return Renderer.#close_render(content, renderer);
} finally {
abort();
set_ssr_context(previous_context);
set_sync_store(previous_sync_store);
}
} }
/** /**
@ -526,7 +519,7 @@ export class Renderer {
} }
async #collect_hydratables() { async #collect_hydratables() {
const map = get_render_store().hydratables; const map = get_render_context().hydratables;
/** @type {(value: unknown) => string} */ /** @type {(value: unknown) => string} */
let default_stringify; let default_stringify;

@ -7,7 +7,7 @@ export { SvelteURLSearchParams } from './url-search-params.js';
export { MediaQuery } from './media-query.js'; export { MediaQuery } from './media-query.js';
export { createSubscriber } from './create-subscriber.js'; export { createSubscriber } from './create-subscriber.js';
export { resource } from '../internal/client/reactivity/resource.js'; export { resource } from '../internal/client/reactivity/resource.js';
export { cache, CacheObserver } from '../internal/client/reactivity/cache.js'; export { cache } from '../internal/client/reactivity/cache.js';
export { fetcher } from '../internal/client/reactivity/fetcher.js'; export { fetcher } from '../internal/client/reactivity/fetcher.js';
/** /**

@ -1,6 +1,6 @@
/** @import { Resource as ResourceType } from '#shared' */ /** @import { Resource as ResourceType } from '#shared' */
export { resource } from '../internal/server/reactivity/resource.js'; export { resource } from '../internal/server/reactivity/resource.js';
export { cache, CacheObserver } from '../internal/server/reactivity/cache.js'; export { cache } from '../internal/server/reactivity/cache.js';
export { fetcher } from '../internal/server/reactivity/fetcher.js'; export { fetcher } from '../internal/server/reactivity/fetcher.js';
export const SvelteDate = globalThis.Date; export const SvelteDate = globalThis.Date;

@ -2422,6 +2422,7 @@ declare module 'svelte/reactivity' {
*/ */
export function createSubscriber(start: (update: () => void) => (() => void) | void): () => void; export function createSubscriber(start: (update: () => void) => (() => void) | void): () => void;
export function resource<T>(fn: () => Promise<T>): Resource_1<T>; export function resource<T>(fn: () => Promise<T>): Resource_1<T>;
export function cache<TFn extends (...args: any[]) => any>(key: string, fn: TFn): ReturnType<TFn>;
export function fetcher<TReturn>(url: string | URL, init?: GetRequestInit | undefined): Resource_1<TReturn>; export function fetcher<TReturn>(url: string | URL, init?: GetRequestInit | undefined): Resource_1<TReturn>;
type Resource_1<T> = { type Resource_1<T> = {
then: Promise<T>['then']; then: Promise<T>['then'];
@ -2443,30 +2444,12 @@ declare module 'svelte/reactivity' {
); );
type GetRequestInit = Omit<RequestInit, 'method' | 'body'> & { method?: 'GET' }; type GetRequestInit = Omit<RequestInit, 'method' | 'body'> & { method?: 'GET' };
export function cache<TFn extends (...args: any[]) => any>(key: string, fn: TFn): ReturnType<TFn>;
export class CacheObserver extends BaseCacheObserver {
constructor();
}
class ReactiveValue<T> { class ReactiveValue<T> {
constructor(fn: () => T, onsubscribe: (update: () => void) => void); constructor(fn: () => T, onsubscribe: (update: () => void) => void);
get current(): T; get current(): T;
#private; #private;
} }
class BaseCacheObserver implements ReadonlyMap<string, any> {
constructor(cache: Map<string, any>);
get(key: string): any;
has(key: string): boolean;
get size(): number;
forEach(callbackfn: (value: any, key: string, map: ReadonlyMap<string, any>) => void, thisArg?: any): void;
entries(): IterableIterator<[string, any]>;
keys(): IterableIterator<string>;
values(): IterableIterator<any>;
[Symbol.iterator](): IterableIterator<[string, any]>;
#private;
}
export {}; export {};
} }

Loading…
Cancel
Save