mirror of https://github.com/sveltejs/svelte
parent
da3260f99a
commit
4d766f87b9
@ -0,0 +1,66 @@
|
||||
/** @import { Resource } from './resource.js' */
|
||||
/** @import { StandardSchemaV1 } from '@standard-schema/spec' */
|
||||
|
||||
import { compile } from 'path-to-regexp';
|
||||
import { create_resource } from './create-resource.js';
|
||||
|
||||
/**
|
||||
* @template {Record<string, unknown>} TPathParams
|
||||
* @typedef {{ searchParams?: ConstructorParameters<typeof URLSearchParams>[0], pathParams?: TPathParams } & RequestInit} FetcherInit
|
||||
*/
|
||||
/**
|
||||
* @template TReturn
|
||||
* @template {Record<string, unknown>} TPathParams
|
||||
* @typedef {(init: FetcherInit<TPathParams>) => Resource<TReturn>} Fetcher
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template {Record<string, unknown>} TPathParams
|
||||
* @template {typeof Resource} TResource
|
||||
* @overload
|
||||
* @param {string} url
|
||||
* @param {{ Resource?: TResource, schema?: undefined }} [options] - TODO what options should we support?
|
||||
* @returns {Fetcher<string | number | Record<string, unknown> | 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<string, unknown>} TPathParams
|
||||
* @template {StandardSchemaV1} TSchema
|
||||
* @template {typeof Resource} TResource
|
||||
* @overload
|
||||
* @param {string} url
|
||||
* @param {{ Resource?: TResource, schema: StandardSchemaV1 }} options
|
||||
* @returns {Fetcher<StandardSchemaV1.InferOutput<TSchema>, 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<string, unknown>} 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<Fetcher<any, any>>[0]} args
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
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);
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
import { hydratable } from '../../context.js';
|
||||
import { tick } from '../../runtime';
|
||||
import { render_effect } from '../effects';
|
||||
import { Resource } from './resource.js';
|
||||
|
||||
/** @typedef {{ count: number, resource: Resource<any> }} Entry */
|
||||
/** @type {Map<string, Entry>} */
|
||||
const cache = new Map();
|
||||
|
||||
/**
|
||||
* @template TReturn
|
||||
* @template {unknown[]} [TArgs=[]]
|
||||
* @template {typeof Resource} [TResource=typeof Resource]
|
||||
* @param {string} name
|
||||
* @param {(...args: TArgs) => Promise<TReturn>} fn
|
||||
* @param {{ Resource?: TResource }} [options]
|
||||
* @returns {(...args: TArgs) => Resource<TReturn>}
|
||||
*/
|
||||
export function create_resource(name, fn, options) {
|
||||
const ResolvedResource = options?.Resource ?? Resource;
|
||||
return (...args) => {
|
||||
const stringified_args = JSON.stringify(args);
|
||||
const cache_key = `${name}:${stringified_args}`;
|
||||
let entry = cache.get(cache_key);
|
||||
const maybe_remove = create_remover(cache_key);
|
||||
|
||||
let tracking = true;
|
||||
try {
|
||||
render_effect(() => {
|
||||
if (entry) entry.count++;
|
||||
return () => {
|
||||
const entry = cache.get(cache_key);
|
||||
if (!entry) return;
|
||||
entry.count--;
|
||||
maybe_remove(entry, cache);
|
||||
};
|
||||
});
|
||||
} catch {
|
||||
tracking = false;
|
||||
}
|
||||
|
||||
let resource = entry?.resource;
|
||||
if (!resource) {
|
||||
resource = new ResolvedResource(() => hydratable(cache_key, () => fn(...args)));
|
||||
const entry = {
|
||||
resource,
|
||||
count: tracking ? 1 : 0
|
||||
};
|
||||
cache.set(cache_key, entry);
|
||||
|
||||
resource.then(
|
||||
() => maybe_remove(entry, cache),
|
||||
() => maybe_remove(entry, cache)
|
||||
);
|
||||
}
|
||||
|
||||
return resource;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
*/
|
||||
function create_remover(key) {
|
||||
/**
|
||||
* @param {Entry | undefined} entry
|
||||
* @param {Map<string, Entry>} cache
|
||||
*/
|
||||
return (entry, cache) =>
|
||||
tick().then(() => {
|
||||
if (!entry?.count && entry === cache.get(key)) {
|
||||
cache.delete(key);
|
||||
}
|
||||
});
|
||||
}
|
Loading…
Reference in new issue