mirror of https://github.com/sveltejs/svelte
parent
0c4ce5a9ec
commit
a2bff0c0b5
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
Loading…
Reference in new issue