diff --git a/packages/svelte/src/internal/client/reactivity/resources/create-fetcher.js b/packages/svelte/src/internal/client/reactivity/resources/create-fetcher.js deleted file mode 100644 index bba3d46529..0000000000 --- a/packages/svelte/src/internal/client/reactivity/resources/create-fetcher.js +++ /dev/null @@ -1,66 +0,0 @@ -/** @import { Resource } from './resource.js' */ -/** @import { StandardSchemaV1 } from '@standard-schema/spec' */ - -import { compile } from 'path-to-regexp'; -import { create_resource } from './define-resource.js'; - -/** - * @template {Record} TPathParams - * @typedef {{ searchParams?: ConstructorParameters[0], pathParams?: TPathParams } & RequestInit} FetcherInit - */ -/** - * @template TReturn - * @template {Record} TPathParams - * @typedef {(init: FetcherInit) => Resource} Fetcher - */ - -/** - * @template {Record} TPathParams - * @template {typeof Resource} TResource - * @overload - * @param {string} url - * @param {{ Resource?: TResource, schema?: undefined }} [options] - TODO what options should we support? - * @returns {Fetcher | unknown[] | boolean | null, TPathParams>} - TODO this return type has to be gnarly unless we do schema validation, as it could be any JSON value (including string, number, etc) - */ -/** - * @template {Record} TPathParams - * @template {StandardSchemaV1} TSchema - * @template {typeof Resource} TResource - * @overload - * @param {string} url - * @param {{ Resource?: TResource, schema: StandardSchemaV1 }} options - * @returns {Fetcher, TPathParams>} - TODO this return type has to be gnarly unless we do schema validation, as it could be any JSON value (including string, number, etc) - */ -/** - * @template {Record} TPathParams - * @template {typeof Resource} TResource - * @template {StandardSchemaV1} TSchema - * @param {string} url - * @param {{ Resource?: TResource, schema?: StandardSchemaV1 }} [options] - */ -export function create_fetcher(url, options) { - const raw_pathname = url.split('//')[1].match(/(\/[^?#]*)/)?.[1] ?? ''; - const populate_path = compile(raw_pathname); - /** - * @param {Parameters>[0]} args - * @returns {Promise} - */ - const fn = async (args) => { - const cloned_url = new URL(url); - const new_params = new URLSearchParams(args.searchParams); - const combined_params = new URLSearchParams([...cloned_url.searchParams, ...new_params]); - cloned_url.search = combined_params.toString(); - cloned_url.pathname = populate_path(args.pathParams ?? {}); // TODO we definitely should get rid of this lib for launch, I just wanted to play with the API - // TODO how to populate path params - const resp = await fetch(cloned_url, args); - if (!resp.ok) { - throw new Error(`Fetch error: ${resp.status} ${resp.statusText}`); - } - const json = await resp.json(); - if (options?.schema) { - return options.schema['~standard'].validate(json); - } - return json; - }; - return create_resource(url.toString(), fn, options); -} diff --git a/packages/svelte/src/internal/server/context.js b/packages/svelte/src/internal/server/context.js index 91299b12c1..f9addbb24b 100644 --- a/packages/svelte/src/internal/server/context.js +++ b/packages/svelte/src/internal/server/context.js @@ -132,8 +132,8 @@ export async function save(promise) { * @param {{ transport?: Transport }} [options] * @returns {Promise} */ -export async function hydratable(key, fn, { transport } = {}) { - const store = await get_render_store(); +export function hydratable(key, fn, { transport } = {}) { + const store = get_render_store(); if (store.hydratables.has(key)) { // TODO error @@ -142,7 +142,7 @@ export async function hydratable(key, fn, { transport } = {}) { const result = fn(); store.hydratables.set(key, { value: result, transport }); - return result; + return Promise.resolve(result); } /** @type {ALSContext | null} */ @@ -153,28 +153,30 @@ export function set_sync_store(store) { sync_store = store; } -/** @type {Promise | null>} */ -const als = import('node:async_hooks') - .then((hooks) => new hooks.AsyncLocalStorage()) +/** @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 {Promise} */ -async function try_get_render_store() { - return sync_store ?? (await als)?.getStore() ?? null; +/** @returns {ALSContext | null} */ +function try_get_render_store() { + return sync_store ?? als?.getStore() ?? null; } -/** @returns {Promise} */ -export async function get_render_store() { - const store = await try_get_render_store(); +/** @returns {ALSContext} */ +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 (await als) { + if (als) { message += ' This is an internal error.'; } else { message += @@ -194,10 +196,10 @@ export async function get_render_store() { * @param {() => Promise} fn * @returns {Promise} */ -export async function with_render_store(store, fn) { +export function with_render_store(store, fn) { try { sync_store = store; - const storage = await als; + const storage = als; return storage ? storage.run(store, fn) : fn(); } finally { sync_store = null; diff --git a/packages/svelte/src/internal/server/renderer.js b/packages/svelte/src/internal/server/renderer.js index 7dc2583379..3d6e8d6cb0 100644 --- a/packages/svelte/src/internal/server/renderer.js +++ b/packages/svelte/src/internal/server/renderer.js @@ -375,7 +375,7 @@ export class Renderer { }); return Promise.resolve(user_result); } - async ??= with_render_store({ hydratables: new Map() }, () => + async ??= with_render_store({ hydratables: new Map(), resources: new Map() }, () => Renderer.#render_async(component, options) ); return async.then((result) => { @@ -523,7 +523,7 @@ export class Renderer { } async #collect_hydratables() { - const map = (await get_render_store()).hydratables; + const map = get_render_store().hydratables; /** @type {(value: unknown) => string} */ let default_stringify; diff --git a/packages/svelte/src/internal/server/types.d.ts b/packages/svelte/src/internal/server/types.d.ts index 0c45c0adc5..8840c329bd 100644 --- a/packages/svelte/src/internal/server/types.d.ts +++ b/packages/svelte/src/internal/server/types.d.ts @@ -1,4 +1,5 @@ import type { MaybePromise, Transport } from '#shared'; +import type { Resource } from '../../reactivity/index-server'; import type { Element } from './dev'; import type { Renderer } from './renderer'; @@ -23,6 +24,7 @@ export interface ALSContext { transport: Transport | undefined; } >; + resources: Map>; } export interface SyncRenderOutput { diff --git a/packages/svelte/src/reactivity/index-client.js b/packages/svelte/src/reactivity/index-client.js index fe5318dae3..9b8f4da827 100644 --- a/packages/svelte/src/reactivity/index-client.js +++ b/packages/svelte/src/reactivity/index-client.js @@ -7,4 +7,3 @@ export { MediaQuery } from './media-query.js'; export { createSubscriber } from './create-subscriber.js'; export { Resource } from '../internal/client/reactivity/resources/resource.js'; export { define_resource as defineResource } from '../internal/client/reactivity/resources/define-resource.js'; -export { create_fetcher as createFetcher } from '../internal/client/reactivity/resources/create-fetcher.js'; diff --git a/packages/svelte/src/reactivity/index-server.js b/packages/svelte/src/reactivity/index-server.js index 1d0d5c8226..b088874e8a 100644 --- a/packages/svelte/src/reactivity/index-server.js +++ b/packages/svelte/src/reactivity/index-server.js @@ -1,7 +1,7 @@ /** @import { StandardSchemaV1 } from '@standard-schema/spec' */ /** @import { Transport } from '#shared' */ import { uneval } from 'devalue'; -import { hydratable } from '../internal/server/context.js'; +import { get_render_store, hydratable } from '../internal/server/context.js'; export const SvelteDate = globalThis.Date; export const SvelteSet = globalThis.Set; @@ -114,10 +114,6 @@ export class Resource { }; } -/** @type {Map>} */ -// TODO scope to render, clear after render -const cache = new Map(); - /** * @template TReturn * @template {unknown[]} [TArgs=[]] @@ -130,6 +126,7 @@ const cache = new Map(); export function defineResource(name, fn, options = {}) { const ResolvedResource = options?.Resource ?? Resource; return (...args) => { + const cache = get_render_store().resources; const stringified_args = (options.transport?.stringify ?? JSON.stringify)(args); const cache_key = `${name}:${stringified_args}`; const entry = cache.get(cache_key); @@ -143,64 +140,3 @@ export function defineResource(name, fn, options = {}) { return resource; }; } - -/** - * @template {Record} TPathParams - * @typedef {{ searchParams?: ConstructorParameters[0], pathParams?: TPathParams } & RequestInit} FetcherInit - */ -/** - * @template TReturn - * @template {Record} TPathParams - * @typedef {(init: FetcherInit) => Resource} Fetcher - */ - -/** - * @template {Record} TPathParams - * @template {typeof Resource} TResource - * @overload - * @param {string} url - * @param {{ Resource?: TResource, schema?: undefined }} [options] - TODO what options should we support? - * @returns {Fetcher | unknown[] | boolean | null, TPathParams>} - TODO this return type has to be gnarly unless we do schema validation, as it could be any JSON value (including string, number, etc) - */ -/** - * @template {Record} TPathParams - * @template {StandardSchemaV1} TSchema - * @template {typeof Resource} TResource - * @overload - * @param {string} url - * @param {{ Resource?: TResource, schema: StandardSchemaV1 }} options - * @returns {Fetcher, TPathParams>} - TODO this return type has to be gnarly unless we do schema validation, as it could be any JSON value (including string, number, etc) - */ -/** - * @template {Record} TPathParams - * @template {typeof Resource} TResource - * @template {StandardSchemaV1} TSchema - * @param {string} url - * @param {{ Resource?: TResource, schema?: StandardSchemaV1 }} [options] - */ -export function createFetcher(url, options) { - const raw_pathname = url.split('//')[1].match(/(\/[^?#]*)/)?.[1] ?? ''; - const populate_path = compile(raw_pathname); - /** - * @param {Parameters>[0]} args - * @returns {Promise} - */ - const fn = async (args) => { - const cloned_url = new URL(url); - const new_params = new URLSearchParams(args.searchParams); - const combined_params = new URLSearchParams([...cloned_url.searchParams, ...new_params]); - cloned_url.search = combined_params.toString(); - cloned_url.pathname = populate_path(args.pathParams ?? {}); // TODO we definitely should get rid of this lib for launch, I just wanted to play with the API - // TODO how to populate path params - const resp = await fetch(cloned_url, args); - if (!resp.ok) { - throw new Error(`Fetch error: ${resp.status} ${resp.statusText}`); - } - const json = await resp.json(); - if (options?.schema) { - return options.schema['~standard'].validate(json); - } - return json; - }; - return createResource(url.toString(), fn, options); -} diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 0531d750a6..2c8912d440 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -448,8 +448,6 @@ declare module 'svelte' { }): Snippet; /** Anything except a function */ type NotFunction = T extends Function ? never : T; - - type MaybePromise = T | Promise; /** * Returns a `[get, set]` pair of functions for working with context in a type-safe way. * @@ -491,7 +489,9 @@ declare module 'svelte' { * */ export function getAllContexts = Map>(): T; - export function hydratable(key: string, fn: () => T): MaybePromise; + export function hydratable(key: string, fn: () => T, { transport }?: { + transport?: Transport; + } | undefined): Promise; /** * Mounts a component to the given target and returns the exports and potentially the props (if compiled with `accessors: true`) of the component. * Transitions will play during the initial render unless the `intro` option is set to `false`. @@ -565,6 +565,11 @@ declare module 'svelte' { [K in keyof T]: () => T[K]; }; + type Transport = { + stringify: (value: unknown) => string; + parse: (value: string) => unknown; + }; + export {}; } @@ -2143,7 +2148,6 @@ declare module 'svelte/motion' { } declare module 'svelte/reactivity' { - import type { StandardSchemaV1 } from '@standard-schema/spec'; /** * A reactive version of the built-in [`Date`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) object. * Reading the date (whether with methods like `date.getTime()` or `date.toString()`, or via things like [`Intl.DateTimeFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat)) @@ -2429,29 +2433,20 @@ declare module 'svelte/reactivity' { set: (value: T) => void; #private; } - export function createResource(name: string, fn: (...args: TArgs) => Promise, options?: { + export function defineResource(name: string, fn: (...args: TArgs) => TReturn, options?: { Resource?: TResource; + transport?: Transport; } | undefined): (...args: TArgs) => Resource; - export function createFetcher, TResource extends typeof Resource>(url: string, options?: { - Resource?: TResource; - schema?: undefined; - } | undefined): Fetcher | unknown[] | boolean | null, TPathParams>; - - export function createFetcher, TSchema extends StandardSchemaV1, TResource extends typeof Resource>(url: string, options: { - Resource?: TResource; - schema: StandardSchemaV1; - }): Fetcher, TPathParams>; - type FetcherInit> = { - searchParams?: ConstructorParameters[0]; - pathParams?: TPathParams; - } & RequestInit; - type Fetcher> = (init: FetcherInit) => Resource; class ReactiveValue { constructor(fn: () => T, onsubscribe: (update: () => void) => void); get current(): T; #private; } + type Transport = { + stringify: (value: unknown) => string; + parse: (value: string) => unknown; + }; export {}; }