diff --git a/packages/svelte/src/internal/client/reactivity/cache.js b/packages/svelte/src/internal/client/reactivity/cache.js index 379ae978cb..e61d13b5e2 100644 --- a/packages/svelte/src/internal/client/reactivity/cache.js +++ b/packages/svelte/src/internal/client/reactivity/cache.js @@ -68,8 +68,8 @@ function create_remover(key) { }); } -export class CacheObserver extends BaseCacheObserver { - constructor() { - super(client_cache); - } -} +// export class CacheObserver extends BaseCacheObserver { +// constructor() { +// super(client_cache); +// } +// } diff --git a/packages/svelte/src/internal/server/context.js b/packages/svelte/src/internal/server/context.js index 8783d5522f..eeb7fdaa61 100644 --- a/packages/svelte/src/internal/server/context.js +++ b/packages/svelte/src/internal/server/context.js @@ -3,6 +3,7 @@ /** @import { Transport } from '#shared' */ import { DEV } from 'esm-env'; import * as e from './errors.js'; +import { save_render_context } from './render-context.js'; /** @type {SSRContext | null} */ export var ssr_context = null; @@ -115,139 +116,10 @@ function get_parent_context(ssr_context) { */ export async function save(promise) { var previous_context = ssr_context; - var previous_sync_store = sync_store; - var value = await promise; + const restore_render_context = await save_render_context(promise); return () => { ssr_context = previous_context; - sync_store = previous_sync_store; - return value; + return restore_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} fn - * @param {{ transport?: Transport }} [options] - * @returns {Promise} - */ -/** - * @template T - * @overload - * @param {() => Promise} fn - * @param {{ transport?: Transport }} [options] - * @returns {Promise} - */ -/** - * @template T - * @param {string | (() => Promise)} key_or_fn - * @param {(() => Promise) | { transport?: Transport }} [fn_or_options] - * @param {{ transport?: Transport }} [maybe_options] - * @returns {Promise} - */ -export function hydratable(key_or_fn, fn_or_options = {}, maybe_options = {}) { - // TODO DRY out with #shared - /** @type {string} */ - let key; - /** @type {() => Promise} */ - let fn; - /** @type {{ transport?: Transport }} */ - let options; - - if (typeof key_or_fn === 'string') { - key = key_or_fn; - fn = /** @type {() => Promise} */ (fn_or_options); - options = /** @type {{ transport?: Transport }} */ (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} */ (key_or_fn); - options = /** @type {{ transport?: Transport }} */ (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 | 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} fn - * @returns {Promise} - */ -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; - } -} diff --git a/packages/svelte/src/internal/server/hydratable.js b/packages/svelte/src/internal/server/hydratable.js new file mode 100644 index 0000000000..780f732f58 --- /dev/null +++ b/packages/svelte/src/internal/server/hydratable.js @@ -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} fn + * @param {{ transport?: Transport }} [options] + * @returns {Promise} + */ +/** + * @template T + * @overload + * @param {() => Promise} fn + * @param {{ transport?: Transport }} [options] + * @returns {Promise} + */ +/** + * @template T + * @param {string | (() => Promise)} key_or_fn + * @param {(() => Promise) | { transport?: Transport }} [fn_or_options] + * @param {{ transport?: Transport }} [maybe_options] + * @returns {Promise} + */ +export async function hydratable(key_or_fn, fn_or_options = {}, maybe_options = {}) { + // TODO DRY out with #shared + /** @type {string} */ + let key; + /** @type {() => Promise} */ + let fn; + /** @type {{ transport?: Transport }} */ + let options; + + if (typeof key_or_fn === 'string') { + key = key_or_fn; + fn = /** @type {() => Promise} */ (fn_or_options); + options = /** @type {{ transport?: Transport }} */ (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} */ (key_or_fn); + options = /** @type {{ transport?: Transport }} */ (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; +} diff --git a/packages/svelte/src/internal/server/reactivity/cache.js b/packages/svelte/src/internal/server/reactivity/cache.js index a387570f4d..3e057cf92f 100644 --- a/packages/svelte/src/internal/server/reactivity/cache.js +++ b/packages/svelte/src/internal/server/reactivity/cache.js @@ -1,5 +1,5 @@ -import { BaseCacheObserver } from '../../shared/cache-observer'; -import { get_render_store, set_hydratable_key } from '../context'; +import { set_hydratable_key } from '../hydratable'; +import { get_render_context } from '../render-context'; /** * @template {(...args: any[]) => any} TFn @@ -8,7 +8,7 @@ import { get_render_store, set_hydratable_key } from '../context'; * @returns {ReturnType} */ export function cache(key, fn) { - const cache = get_render_store().cache; + const cache = get_render_context().cache; const entry = cache.get(key); if (entry) { return /** @type {ReturnType} */ (entry); @@ -20,8 +20,9 @@ export function cache(key, fn) { return new_entry; } -export class CacheObserver extends BaseCacheObserver { - constructor() { - super(get_render_store().cache); - } -} +// TODO, has to be async +// export class CacheObserver extends BaseCacheObserver { +// constructor() { +// super(get_render_store().cache); +// } +// } diff --git a/packages/svelte/src/internal/server/reactivity/fetcher.js b/packages/svelte/src/internal/server/reactivity/fetcher.js index 9e4870cf2b..62c3513b43 100644 --- a/packages/svelte/src/internal/server/reactivity/fetcher.js +++ b/packages/svelte/src/internal/server/reactivity/fetcher.js @@ -1,6 +1,6 @@ /** @import { GetRequestInit, Resource } from '#shared' */ import { fetch_json } from '../../shared/utils.js'; -import { hydratable } from '../context.js'; +import { hydratable } from '../hydratable.js'; import { cache } from './cache'; import { resource } from './resource.js'; diff --git a/packages/svelte/src/internal/server/render-context.js b/packages/svelte/src/internal/server/render-context.js new file mode 100644 index 0000000000..e86ee813c7 --- /dev/null +++ b/packages/svelte/src/internal/server/render-context.js @@ -0,0 +1,97 @@ +/** @import { AsyncLocalStorage } from 'node:async_hooks' */ +/** @import { RenderContext } from '#server' */ + +import { deferred } from '../shared/utils'; + +/** @type {Promise | null} */ +let current_render = null; + +/** @type {RenderContext | null} */ +let sync_context = null; + +/** + * @template T + * @param {Promise} 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} fn + * @returns {Promise} + */ +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 | 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; +} diff --git a/packages/svelte/src/internal/server/renderer.js b/packages/svelte/src/internal/server/renderer.js index 598bcd73a3..29d1ea58c0 100644 --- a/packages/svelte/src/internal/server/renderer.js +++ b/packages/svelte/src/internal/server/renderer.js @@ -3,20 +3,12 @@ /** @import { MaybePromise } from '#shared' */ import { async_mode_flag } from '../flags/index.js'; import { abort } from './abort-signal.js'; -import { - get_render_store, - pop, - push, - set_ssr_context, - set_sync_store, - ssr_context, - sync_store, - with_render_store -} from './context.js'; +import { pop, push, set_ssr_context, ssr_context, save } from './context.js'; import * as e from './errors.js'; import { BLOCK_CLOSE, BLOCK_OPEN } from './hydration.js'; import { attributes } from './index.js'; import { uneval } from 'devalue'; +import { get_render_context, with_render_context, init_render_context } from './render-context.js'; /** @typedef {'head' | 'body'} RendererType */ /** @typedef {{ [key in RendererType]: string }} AccumulatedContent */ @@ -375,8 +367,8 @@ export class Renderer { }); return Promise.resolve(user_result); } - async ??= with_render_store({ hydratables: new Map(), cache: new Map() }, () => - Renderer.#render_async(component, options) + async ??= init_render_context().then(() => + with_render_context(() => Renderer.#render_async(component, options)) ); return async.then((result) => { Object.defineProperty(result, 'html', { @@ -469,23 +461,24 @@ export class Renderer { * @returns {Promise} */ static async #render_async(component, options) { - var previous_context = ssr_context; - var previous_sync_store = sync_store; - - try { - const renderer = Renderer.#open_render('async', component, options); + const restore = await save( + (async () => { + try { + 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(); - 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); - } + return restore(); } /** @@ -526,7 +519,7 @@ export class Renderer { } async #collect_hydratables() { - const map = get_render_store().hydratables; + const map = get_render_context().hydratables; /** @type {(value: unknown) => string} */ let default_stringify; diff --git a/packages/svelte/src/reactivity/index-client.js b/packages/svelte/src/reactivity/index-client.js index 8b2abd9076..d1c16b1f78 100644 --- a/packages/svelte/src/reactivity/index-client.js +++ b/packages/svelte/src/reactivity/index-client.js @@ -7,7 +7,7 @@ export { SvelteURLSearchParams } from './url-search-params.js'; export { MediaQuery } from './media-query.js'; export { createSubscriber } from './create-subscriber.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'; /** diff --git a/packages/svelte/src/reactivity/index-server.js b/packages/svelte/src/reactivity/index-server.js index 0dcc459e64..99bb469ae2 100644 --- a/packages/svelte/src/reactivity/index-server.js +++ b/packages/svelte/src/reactivity/index-server.js @@ -1,6 +1,6 @@ /** @import { Resource as ResourceType } from '#shared' */ 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 const SvelteDate = globalThis.Date; diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index fd55a7649c..b9db2f9861 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -2422,6 +2422,7 @@ declare module 'svelte/reactivity' { */ export function createSubscriber(start: (update: () => void) => (() => void) | void): () => void; export function resource(fn: () => Promise): Resource_1; + export function cache any>(key: string, fn: TFn): ReturnType; export function fetcher(url: string | URL, init?: GetRequestInit | undefined): Resource_1; type Resource_1 = { then: Promise['then']; @@ -2443,30 +2444,12 @@ declare module 'svelte/reactivity' { ); type GetRequestInit = Omit & { method?: 'GET' }; - export function cache any>(key: string, fn: TFn): ReturnType; - export class CacheObserver extends BaseCacheObserver { - constructor(); - } class ReactiveValue { constructor(fn: () => T, onsubscribe: (update: () => void) => void); get current(): T; #private; } - class BaseCacheObserver implements ReadonlyMap { - - constructor(cache: Map); - get(key: string): any; - has(key: string): boolean; - - get size(): number; - forEach(callbackfn: (value: any, key: string, map: ReadonlyMap) => void, thisArg?: any): void; - entries(): IterableIterator<[string, any]>; - keys(): IterableIterator; - values(): IterableIterator; - [Symbol.iterator](): IterableIterator<[string, any]>; - #private; - } export {}; }