INIT
This commit is contained in:
+1
@@ -0,0 +1 @@
|
||||
export * from "./resource.svelte.js";
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./resource.svelte.js";
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
export type ResponseData = {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
};
|
||||
export type SearchResponseData = {
|
||||
results: {
|
||||
id: number;
|
||||
title: string;
|
||||
}[];
|
||||
page: number;
|
||||
total: number;
|
||||
};
|
||||
export declare const handlers: import("msw").HttpHandler[];
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import { http, delay, HttpResponse } from "msw";
|
||||
export const handlers = [
|
||||
// Basic user endpoint
|
||||
http.get("https://api.example.com/users/:id", async ({ params }) => {
|
||||
await delay(50);
|
||||
return HttpResponse.json({
|
||||
id: Number(params.id),
|
||||
name: `User ${params.id}`,
|
||||
email: `user${params.id}@example.com`,
|
||||
});
|
||||
}),
|
||||
// Search endpoint with query params
|
||||
http.get("https://api.example.com/search", ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
const query = url.searchParams.get("q");
|
||||
const page = Number(url.searchParams.get("page")) || 1;
|
||||
return HttpResponse.json({
|
||||
results: [
|
||||
{ id: page * 1, title: `Result 1 for ${query}` },
|
||||
{ id: page * 2, title: `Result 2 for ${query}` },
|
||||
],
|
||||
page,
|
||||
total: 10,
|
||||
});
|
||||
}),
|
||||
// Endpoint that can fail
|
||||
http.get("https://api.example.com/error-prone", () => {
|
||||
return new HttpResponse(null, { status: 500 });
|
||||
}),
|
||||
];
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
import type { Getter } from "../../internal/types.js";
|
||||
/**
|
||||
* Configuration options for the resource function
|
||||
*/
|
||||
export type ResourceOptions<Data> = {
|
||||
/** Skip initial fetch when true */
|
||||
lazy?: boolean;
|
||||
/** Only fetch once when true */
|
||||
once?: boolean;
|
||||
/** Initial value for the resource */
|
||||
initialValue?: Data;
|
||||
/** Debounce time in milliseconds */
|
||||
debounce?: number;
|
||||
/** Throttle time in milliseconds */
|
||||
throttle?: number;
|
||||
};
|
||||
/**
|
||||
* Core state of a resource
|
||||
*/
|
||||
export type ResourceState<Data, HasInitialValue extends boolean = false> = {
|
||||
/** Current value of the resource */
|
||||
current: HasInitialValue extends true ? Data : Data | undefined;
|
||||
/** Whether the resource is currently loading */
|
||||
loading: boolean;
|
||||
/** Error if the fetch failed */
|
||||
error: Error | undefined;
|
||||
};
|
||||
/**
|
||||
* Return type of the resource function, extends ResourceState with additional methods
|
||||
*/
|
||||
export type ResourceReturn<Data, RefetchInfo = unknown, HasInitialValue extends boolean = false> = ResourceState<Data, HasInitialValue> & {
|
||||
/** Update the resource value directly */
|
||||
mutate: (value: Data) => void;
|
||||
/** Re-run the fetcher with current values */
|
||||
refetch: (info?: RefetchInfo) => Promise<Data | undefined>;
|
||||
};
|
||||
export type ResourceFetcherRefetchInfo<Data, RefetchInfo = unknown> = {
|
||||
/** Previous data return from fetcher */
|
||||
data: Data | undefined;
|
||||
/** Whether the fetcher is currently refetching or it can be the value you passed to refetch. */
|
||||
refetching: RefetchInfo | boolean;
|
||||
/** A cleanup function that will be called when the source is invalidated and the fetcher is about to re-run */
|
||||
onCleanup: (fn: () => void) => void;
|
||||
/** AbortSignal for cancelling fetch requests */
|
||||
signal: AbortSignal;
|
||||
};
|
||||
export type ResourceFetcher<Source, Data, RefetchInfo = unknown> = (
|
||||
/** Current value of the source */
|
||||
value: Source extends Array<unknown> ? {
|
||||
[K in keyof Source]: Source[K];
|
||||
} : Source,
|
||||
/** Previous value of the source */
|
||||
previousValue: Source extends Array<unknown> ? {
|
||||
[K in keyof Source]: Source[K];
|
||||
} : Source | undefined, info: ResourceFetcherRefetchInfo<Data, RefetchInfo>) => Promise<Data>;
|
||||
/**
|
||||
* Creates a reactive resource that combines reactive dependency tracking with async data fetching.
|
||||
* This version uses watch under the hood and runs after render.
|
||||
* For pre-render execution, use resource.pre().
|
||||
*
|
||||
* Features:
|
||||
* - Automatic request cancellation when dependencies change
|
||||
* - Built-in loading and error states
|
||||
* - Support for initial values and lazy loading
|
||||
* - Type-safe reactive dependencies
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Basic usage with automatic request cancellation
|
||||
* const userResource = resource(
|
||||
* () => userId,
|
||||
* async (newId, prevId, { signal }) => {
|
||||
* const res = await fetch(`/api/users/${newId}`, { signal });
|
||||
* return res.json();
|
||||
* }
|
||||
* );
|
||||
*
|
||||
* // Multiple dependencies
|
||||
* const searchResource = resource(
|
||||
* [() => query, () => page],
|
||||
* async ([query, page], [prevQuery, prevPage], { signal }) => {
|
||||
* const res = await fetch(
|
||||
* `/api/search?q=${query}&page=${page}`,
|
||||
* { signal }
|
||||
* );
|
||||
* return res.json();
|
||||
* },
|
||||
* { lazy: true }
|
||||
* );
|
||||
*
|
||||
* // Custom cleanup with built-in request cancellation
|
||||
* const streamResource = resource(
|
||||
* () => streamId,
|
||||
* async (id, prevId, { signal, onCleanup }) => {
|
||||
* const eventSource = new EventSource(`/api/stream/${id}`);
|
||||
* onCleanup(() => eventSource.close());
|
||||
*
|
||||
* const res = await fetch(`/api/stream/${id}/init`, { signal });
|
||||
* return res.json();
|
||||
* }
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
export declare function resource<Source, RefetchInfo = unknown, Fetcher extends ResourceFetcher<Source, Awaited<ReturnType<Fetcher>>, RefetchInfo> = ResourceFetcher<Source, any, RefetchInfo>>(source: Getter<Source>, fetcher: Fetcher, options: ResourceOptions<Awaited<ReturnType<Fetcher>>> & {
|
||||
initialValue: Awaited<ReturnType<Fetcher>>;
|
||||
}): ResourceReturn<Awaited<ReturnType<Fetcher>>, RefetchInfo, true>;
|
||||
export declare function resource<Source, RefetchInfo = unknown, Fetcher extends ResourceFetcher<Source, Awaited<ReturnType<Fetcher>>, RefetchInfo> = ResourceFetcher<Source, any, RefetchInfo>>(source: Getter<Source>, fetcher: Fetcher, options?: Omit<ResourceOptions<Awaited<ReturnType<Fetcher>>>, "initialValue">): ResourceReturn<Awaited<ReturnType<Fetcher>>, RefetchInfo, false>;
|
||||
export declare function resource<Sources extends Array<unknown>, RefetchInfo = unknown, Fetcher extends ResourceFetcher<Sources, Awaited<ReturnType<Fetcher>>, RefetchInfo> = ResourceFetcher<Sources, any, RefetchInfo>>(sources: {
|
||||
[K in keyof Sources]: Getter<Sources[K]>;
|
||||
}, fetcher: Fetcher, options: ResourceOptions<Awaited<ReturnType<Fetcher>>> & {
|
||||
initialValue: Awaited<ReturnType<Fetcher>>;
|
||||
}): ResourceReturn<Awaited<ReturnType<Fetcher>>, RefetchInfo, true>;
|
||||
export declare function resource<Sources extends Array<unknown>, RefetchInfo = unknown, Fetcher extends ResourceFetcher<Sources, Awaited<ReturnType<Fetcher>>, RefetchInfo> = ResourceFetcher<Sources, any, RefetchInfo>>(sources: {
|
||||
[K in keyof Sources]: Getter<Sources[K]>;
|
||||
}, fetcher: Fetcher, options?: Omit<ResourceOptions<Awaited<ReturnType<Fetcher>>>, "initialValue">): ResourceReturn<Awaited<ReturnType<Fetcher>>, RefetchInfo, false>;
|
||||
export declare namespace resource {
|
||||
var pre: typeof resourcePre;
|
||||
}
|
||||
/**
|
||||
* Helper function to create a resource with pre-effect (runs before render).
|
||||
* Uses watch.pre internally instead of watch for pre-render execution.
|
||||
* Includes all features of the standard resource including automatic request cancellation.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Pre-render resource with automatic cancellation
|
||||
* const data = resource.pre(
|
||||
* () => query,
|
||||
* async (newQuery, prevQuery, { signal }) => {
|
||||
* const res = await fetch(`/api/search?q=${newQuery}`, { signal });
|
||||
* return res.json();
|
||||
* }
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
export declare function resourcePre<Source, RefetchInfo = unknown, Fetcher extends ResourceFetcher<Source, Awaited<ReturnType<Fetcher>>, RefetchInfo> = ResourceFetcher<Source, any, RefetchInfo>>(source: Getter<Source>, fetcher: Fetcher, options: ResourceOptions<Awaited<ReturnType<Fetcher>>> & {
|
||||
initialValue: Awaited<ReturnType<Fetcher>>;
|
||||
}): ResourceReturn<Awaited<ReturnType<Fetcher>>, RefetchInfo, true>;
|
||||
export declare function resourcePre<Source, RefetchInfo = unknown, Fetcher extends ResourceFetcher<Source, Awaited<ReturnType<Fetcher>>, RefetchInfo> = ResourceFetcher<Source, any, RefetchInfo>>(source: Getter<Source>, fetcher: Fetcher, options?: Omit<ResourceOptions<Awaited<ReturnType<Fetcher>>>, "initialValue">): ResourceReturn<Awaited<ReturnType<Fetcher>>, RefetchInfo, false>;
|
||||
export declare function resourcePre<Sources extends Array<unknown>, RefetchInfo = unknown, Fetcher extends ResourceFetcher<Sources, Awaited<ReturnType<Fetcher>>, RefetchInfo> = ResourceFetcher<Sources, any, RefetchInfo>>(sources: {
|
||||
[K in keyof Sources]: Getter<Sources[K]>;
|
||||
}, fetcher: Fetcher, options: ResourceOptions<Awaited<ReturnType<Fetcher>>> & {
|
||||
initialValue: Awaited<ReturnType<Fetcher>>;
|
||||
}): ResourceReturn<Awaited<ReturnType<Fetcher>>, RefetchInfo, true>;
|
||||
export declare function resourcePre<Sources extends Array<unknown>, RefetchInfo = unknown, Fetcher extends ResourceFetcher<Sources, Awaited<ReturnType<Fetcher>>, RefetchInfo> = ResourceFetcher<Sources, any, RefetchInfo>>(sources: {
|
||||
[K in keyof Sources]: Getter<Sources[K]>;
|
||||
}, fetcher: Fetcher, options?: Omit<ResourceOptions<Awaited<ReturnType<Fetcher>>>, "initialValue">): ResourceReturn<Awaited<ReturnType<Fetcher>>, RefetchInfo, false>;
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
import { watch } from "../watch/index.js";
|
||||
// Helper functions for debounce and throttle
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function debounce(fn, delay) {
|
||||
let timeoutId;
|
||||
let lastResolve = null;
|
||||
return (...args) => {
|
||||
return new Promise((resolve) => {
|
||||
if (lastResolve) {
|
||||
lastResolve(undefined);
|
||||
}
|
||||
lastResolve = resolve;
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(async () => {
|
||||
const result = await fn(...args);
|
||||
if (lastResolve) {
|
||||
lastResolve(result);
|
||||
lastResolve = null;
|
||||
}
|
||||
}, delay);
|
||||
});
|
||||
};
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function throttle(fn, delay) {
|
||||
let lastRun = 0;
|
||||
let lastPromise = null;
|
||||
return (...args) => {
|
||||
const now = Date.now();
|
||||
if (lastRun && now - lastRun < delay) {
|
||||
return lastPromise ?? Promise.resolve(undefined);
|
||||
}
|
||||
lastRun = now;
|
||||
lastPromise = fn(...args);
|
||||
return lastPromise;
|
||||
};
|
||||
}
|
||||
function runResource(source, fetcher, options = {}, effectFn) {
|
||||
const { lazy = false, once = false, initialValue, debounce: debounceTime, throttle: throttleTime, } = options;
|
||||
// Create state
|
||||
let current = $state(initialValue);
|
||||
let loading = $state(false);
|
||||
let error = $state(undefined);
|
||||
let cleanupFns = $state([]);
|
||||
// Helper function to run cleanup functions
|
||||
const runCleanup = () => {
|
||||
cleanupFns.forEach((fn) => fn());
|
||||
cleanupFns = [];
|
||||
};
|
||||
// Helper function to register cleanup
|
||||
const onCleanup = (fn) => {
|
||||
cleanupFns = [...cleanupFns, fn];
|
||||
};
|
||||
// Create the base fetcher function
|
||||
const baseFetcher = async (value, previousValue, refetching = false) => {
|
||||
try {
|
||||
loading = true;
|
||||
error = undefined;
|
||||
runCleanup();
|
||||
// Create new AbortController for this fetch
|
||||
const controller = new AbortController();
|
||||
onCleanup(() => controller.abort());
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const result = await fetcher(value, previousValue, {
|
||||
data: current,
|
||||
refetching,
|
||||
onCleanup,
|
||||
signal: controller.signal,
|
||||
});
|
||||
current = result;
|
||||
return result;
|
||||
}
|
||||
catch (e) {
|
||||
if (!(e instanceof DOMException && e.name === "AbortError")) {
|
||||
error = e;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
finally {
|
||||
loading = false;
|
||||
}
|
||||
};
|
||||
// Apply debounce or throttle if specified
|
||||
const runFetcher = debounceTime
|
||||
? debounce(baseFetcher, debounceTime)
|
||||
: throttleTime
|
||||
? throttle(baseFetcher, throttleTime)
|
||||
: baseFetcher;
|
||||
// Setup effect
|
||||
const sources = Array.isArray(source) ? source : [source];
|
||||
let prevValues;
|
||||
effectFn((values, previousValues) => {
|
||||
// Skip if once and already ran
|
||||
if (once && prevValues) {
|
||||
return;
|
||||
}
|
||||
prevValues = values;
|
||||
runFetcher(Array.isArray(source) ? values : values[0], Array.isArray(source) ? previousValues : previousValues?.[0]);
|
||||
}, { lazy });
|
||||
return {
|
||||
get current() {
|
||||
return current;
|
||||
},
|
||||
get loading() {
|
||||
return loading;
|
||||
},
|
||||
get error() {
|
||||
return error;
|
||||
},
|
||||
mutate: (value) => {
|
||||
current = value;
|
||||
},
|
||||
refetch: (info) => {
|
||||
const values = sources.map((s) => s());
|
||||
return runFetcher(Array.isArray(source) ? values : values[0], Array.isArray(source) ? values : values[0], info ?? true);
|
||||
},
|
||||
};
|
||||
}
|
||||
// Implementation
|
||||
export function resource(source, fetcher, options) {
|
||||
return runResource(source, fetcher, options, (fn, options) => {
|
||||
const sources = Array.isArray(source) ? source : [source];
|
||||
const getters = () => sources.map((s) => s());
|
||||
watch(getters, (values, previousValues) => {
|
||||
fn(values, previousValues ?? []);
|
||||
}, options);
|
||||
});
|
||||
}
|
||||
// Implementation
|
||||
export function resourcePre(source, fetcher, options) {
|
||||
return runResource(source, fetcher, options, (fn, options) => {
|
||||
const sources = Array.isArray(source) ? source : [source];
|
||||
const getter = () => sources.map((s) => s());
|
||||
watch.pre(getter, (values, previousValues) => {
|
||||
fn(values, previousValues ?? []);
|
||||
}, options);
|
||||
});
|
||||
}
|
||||
resource.pre = resourcePre;
|
||||
Reference in New Issue
Block a user