Skip to content

Commit

Permalink
improve types, make axios dependency optional
Browse files Browse the repository at this point in the history
  • Loading branch information
Alex Lohr committed Jul 5, 2022
1 parent a70a12d commit 54e380c
Show file tree
Hide file tree
Showing 7 changed files with 266 additions and 218 deletions.
5 changes: 4 additions & 1 deletion packages/fetch/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@
"primitives"
],
"devDependencies": {
"axios": "^0.27.2",
"@types/node": "^17.0.45",
"jsdom": "^19.0.0",
"prettier": "^2.7.1",
Expand All @@ -63,10 +62,14 @@
"vite-plugin-solid": "2.2.6"
},
"peerDependencies": {
"axios": ">=0.20.0",
"node-fetch": ">=2.0.0",
"solid-js": ">=1.3.0"
},
"peerDependenciesMeta": {
"axios": {
"optional": true
},
"node-fetch": {
"optional": true
}
Expand Down
52 changes: 29 additions & 23 deletions packages/fetch/src/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,42 +20,48 @@ export const defaultCacheOptions: CacheOptions = {
cache: {}
};

export const serializeRequest = (requestData: [info: RequestInfo, init?: RequestInit]): string =>
export const serializeRequest = <FetcherArgs extends any[]>(requestData: FetcherArgs): string =>
JSON.stringify({
...(typeof requestData[0] === "string" ? { url: requestData[0] } : requestData[0]),
...requestData[1]
});

export const withCache: RequestModifier =
<T>(options: CacheOptions = defaultCacheOptions) =>
(requestContext: RequestContext<T>) => {
<Result extends unknown, FetcherArgs extends any[]>(
options: CacheOptions = defaultCacheOptions
) =>
(requestContext: RequestContext<Result, FetcherArgs>) => {
requestContext.cache = requestContext.cache || options.cache;
const isExpired = (entry: CacheEntry) =>
typeof options.expires === "number"
? entry.ts + options.expires > new Date().getTime()
: options.expires(entry);
wrapFetcher<T>(requestContext, <T>(originalFetcher: any) => (requestData, info) => {
const serializedRequest = serializeRequest(requestData);
const cached: CacheEntry | undefined = requestContext.cache[serializedRequest];
const shouldRead = requestContext.readCache?.(cached) !== false;
if (cached && !isExpired(cached) && shouldRead) {
return Promise.resolve<T>(cached.data);
}
return originalFetcher(requestData, info).then((data: T) => {
requestContext.writeCache?.(
serializedRequest,
(requestContext.cache[serializedRequest] = {
ts: new Date().getTime(),
requestData: requestData,
data
})
);
return data;
});
});
wrapFetcher<Result, FetcherArgs>(
requestContext,
<T>(originalFetcher: any) =>
(requestData, info) => {
const serializedRequest = serializeRequest(requestData);
const cached: CacheEntry | undefined = requestContext.cache[serializedRequest];
const shouldRead = requestContext.readCache?.(cached) !== false;
if (cached && !isExpired(cached) && shouldRead) {
return Promise.resolve<T>(cached.data);
}
return originalFetcher(requestData, info).then((data: T) => {
requestContext.writeCache?.(
serializedRequest,
(requestContext.cache[serializedRequest] = {
ts: new Date().getTime(),
requestData: requestData,
data
})
);
return data;
});
}
);
requestContext.wrapResource();
Object.assign(requestContext.resource![1], {
invalidate: (requestData: [info: RequestInfo, init?: RequestInit]) => {
invalidate: (requestData: FetcherArgs) => {
try {
delete requestContext.cache[serializeRequest(requestData)];
} catch (e) {
Expand Down
202 changes: 108 additions & 94 deletions packages/fetch/src/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,36 +6,35 @@ import {
ResourceReturn
} from "solid-js";
import { RequestModifier } from "./modifiers";
import { fetchRequest } from "./request";
import { fetchRequest, Request } from "./request";

export type RequestContext<T> = {
urlAccessor: Accessor<[info: RequestInfo, init?: RequestInit] | undefined>;
wrapResource: () => ResourceReturn<T>;
fetcher?: <T>(
requestData: [info: RequestInfo, init?: RequestInit],
info: ResourceFetcherInfo<T>
) => Promise<T>;
response?: Response;
resource?: ResourceReturn<T>;
abortController?: AbortController;
responseHandler?: (response: Response) => T;
[key: string]: any;
};
export type FetchArgs = [info: RequestInfo] | [info: RequestInfo, init?: RequestInit];

export type FetchOptions<I> = I extends undefined
export type DistributeFetcherArgs<
FetcherArgs extends any[],
ExtraArgs extends any[]
> = FetcherArgs extends any
? ExtraArgs extends any
? | [...FetcherArgs, ...ExtraArgs]
| [Accessor<FetcherArgs | undefined>, ...ExtraArgs]
| [...{ [n in keyof FetcherArgs]: Accessor<FetcherArgs[n] | undefined> }, ...ExtraArgs]
: never
: never;

export type FetchOptions<Result, InitialValue, FetcherArgs> = InitialValue extends undefined
? {
initialValue?: I;
initialValue?: InitialValue;
name?: string;
fetch?: typeof fetch;
request?: <T>(requestContext: RequestContext<T>) => void;
request?: (requestContext: RequestContext<Result, FetcherArgs>) => void;
responseHandler?: (response: Response) => any;
disable?: boolean;
}
: {
initialValue: I;
initialValue: InitialValue;
name?: string;
fetch?: typeof fetch;
request?: <T>(requestContext: RequestContext<T>) => void;
request?: (requestContext: RequestContext<Result, FetcherArgs>) => void;
responseHandler?: (response: Response) => any;
disable?: boolean;
};
Expand All @@ -59,16 +58,27 @@ export type FetchReturn<T, I> = [
}
];

const isOptions = <I>(prop: any): prop is FetchOptions<I> =>
export type RequestContext<Result, FetcherArgs> = {
urlAccessor: Accessor<FetcherArgs | undefined>;
wrapResource: () => ResourceReturn<Result>;
fetcher?: (requestData: FetcherArgs, info: ResourceFetcherInfo<Result>) => Promise<Result>;
response?: Response;
resource?: ResourceReturn<Result>;
abortController?: AbortController;
responseHandler?: (response: Response) => Result;
[key: string]: any;
};

const isOptions = <Result, InitialValue, FetcherArgs>(
prop: any
): prop is FetchOptions<Result, InitialValue, FetcherArgs> =>
typeof prop === "object" && ["name", "initialValue", "fetch", "request"].some(key => key in prop);

/* we want to be able to overload our functions */
/* eslint no-redeclare:off */
/**
* Creates a fetch resource with lightweight modifications
*
* ```typescript
* createFetch<T>(
* createFetch<Result, InitialValue, FetcherArgs>(
* requestInfo: RequestInfo,
* requestInit?: RequestInit,
* options?: {
Expand All @@ -78,7 +88,7 @@ const isOptions = <I>(prop: any): prop is FetchOptions<I> =>
* // disable fetching, e.g. in SSR situations (use `isServer`)
* disabled?: boolean
* },
* modifiers: (withAbort() | withCache() | ...)[]
* modifiers?: (withAbort() | withCache() | ...)[]
* ): [
* Resource<T> & {
* status: number | null,
Expand All @@ -102,97 +112,101 @@ const isOptions = <I>(prop: any): prop is FetchOptions<I> =>
* ## Available Modifiers:
* * withAbort() - makes request abortable
* * withTimeout(ms) - adds a request timeout (works with abort)
* // TODO:
* * withRetry(num) - retries request *num* times
* * withCache(options) - caches requests
* * withCatchAll() - catches all errors so you don't need a boundary
*
* You can even add your own modifiers.
* ```
*/
export function createFetch<T, I = undefined>(
requestInfo: Accessor<RequestInfo | undefined> | RequestInfo,
modifiers?: RequestModifier[]
): FetchReturn<T, I>;
export function createFetch<T, I = undefined>(
requestInfo: Accessor<RequestInfo | undefined> | RequestInfo,
requestInit: Accessor<RequestInit | undefined> | RequestInit,
modifiers?: RequestModifier[]
): FetchReturn<T, I>;
export function createFetch<T, I = undefined>(
requestInfo: Accessor<RequestInfo | undefined> | RequestInfo,
options: FetchOptions<I>,
modifiers?: RequestModifier[]
): FetchReturn<T, I>;
export function createFetch<T, I>(
requestInfo: Accessor<RequestInfo | undefined> | RequestInfo,
options: FetchOptions<I>,
modifiers?: RequestModifier[]
): FetchReturn<T, I>;
export function createFetch<T, I>(
requestInfo: Accessor<RequestInfo | undefined> | RequestInfo,
requestInit: Accessor<RequestInit | undefined> | RequestInit,
options: FetchOptions<I>,
modifiers?: RequestModifier[]
): FetchReturn<T, I>;
export function createFetch<T, I>(
...args:
| [
requestInfo: Accessor<RequestInfo | undefined> | RequestInfo,
requestInit?: Accessor<RequestInit | undefined> | RequestInit | FetchOptions<I>,
options?: FetchOptions<I>,
modifiers?: RequestModifier[]
]
| [
requestInfo: Accessor<RequestInfo | undefined> | RequestInfo,
requestInit?: Accessor<RequestInit | undefined> | RequestInit | FetchOptions<I>,
modifiers?: RequestModifier[]
]
| [
requestInfo: Accessor<RequestInfo | undefined> | RequestInfo,
options?: FetchOptions<I>,
modifiers?: RequestModifier[]
]
| [requestInfo: Accessor<RequestInfo | undefined> | RequestInfo, modifiers?: RequestModifier[]]
): FetchReturn<T, I> {
const options: FetchOptions<T> = ([args[2], args[1]].find(isOptions) || {}) as FetchOptions<T>;
const urlAccessor: Accessor<[info: RequestInfo, init?: RequestInit] | undefined> = createMemo(
() => {
if (options.disable) {
return undefined;
}
const info: RequestInfo | undefined = typeof args[0] === "function" ? args[0]() : args[0];
if (!info) {
return undefined;
export function createFetch<
Result,
InitialValue = undefined,
FetcherArgs extends any[] = FetchArgs
>(...fetcherArgs: FetcherArgs): FetchReturn<Result, InitialValue>;
export function createFetch<
Result,
InitialValue = undefined,
FetcherArgs extends any[] = FetchArgs
>(
...args: DistributeFetcherArgs<FetcherArgs, [modifiers: RequestModifier[] | [options: FetchOptions<Result, InitialValue, FetcherArgs> & { request: never, initialValue: never }, modifiers?: RequestModifier[]]]>
): FetchReturn<Result, InitialValue>;
export function createFetch<
Result,
InitialValue,
FetcherArgs extends any[] = FetchArgs
>(
...args: DistributeFetcherArgs<FetcherArgs, [options: FetchOptions<Result, InitialValue, FetcherArgs> & { request: never, initialValue: InitialValue }, modifiers?: RequestModifier[]]>
): FetchReturn<Result, InitialValue>;
export function createFetch<
Result,
InitialValue,
FetcherArgs extends any[],
>(
...args: DistributeFetcherArgs<FetcherArgs, [modifiers: RequestModifier[] | [options: FetchOptions<Result, InitialValue, FetcherArgs> & { initialValue?: undefined }, modifiers?: RequestModifier[]]]>
): FetcherArgs extends FetchArgs ? never : FetchReturn<Result, InitialValue>;
export function createFetch<
Result,
InitialValue,
FetcherArgs extends any[]
>(
...args: DistributeFetcherArgs<FetcherArgs, [options: FetchOptions<Result, InitialValue, FetcherArgs>] | [options: FetchOptions<Result, InitialValue, FetcherArgs>, modifiers?: RequestModifier[]]>
): FetchReturn<Result, InitialValue>;
export function createFetch<
Result,
InitialValue extends Result,
FetcherArgs extends any[] = [info: RequestInfo, init?: RequestInit]
>(...args: any[]): FetchReturn<Result, InitialValue> {
const options = ([args[2], args[1]].find(isOptions) || {}) as FetchOptions<
Result,
InitialValue,
FetcherArgs
>;
const urlAccessor: Accessor<FetcherArgs | undefined> = createMemo(() => {
if (options.disable) {
return undefined;
}
const info: RequestInfo | undefined =
typeof args[0] === "function"
? (args[0] as Accessor<FetcherArgs | FetcherArgs[0]>)()
: args[0];
if (!info) {
return undefined;
}
const init =
typeof args[1] === "function"
? (args[1] as Accessor<FetcherArgs[1]>)()
: isOptions(args[1])
? undefined
: (args[1] as RequestInit);
return [info, init] as FetcherArgs;
});
const modifiers: (Request<FetcherArgs> | RequestModifier)[] = ((): RequestModifier[] => {
for (let l = args.length - 1; l >= 1; l--) {
if (Array.isArray(args[l])) {
return args[l];
}
const init =
typeof args[1] === "function"
? args[1]()
: isOptions(args[1])
? undefined
: (args[1] as RequestInit);
return [info, init] as [info: RequestInfo, init?: RequestInit];
}
);
const modifiers = args.slice(1).find(Array.isArray) || [];
modifiers.unshift(options.request || fetchRequest(options.fetch));
return [];
})();
modifiers.unshift((options.request || fetchRequest(options.fetch)) as Request<FetcherArgs>);
let index = 0;
const fetchContext: RequestContext<T> = {
const fetchContext: RequestContext<Result, FetcherArgs> = {
urlAccessor,
responseHandler: options.responseHandler,
wrapResource: () => {
const modifier = modifiers[index++];
typeof modifier === "function" && modifier(fetchContext);
typeof modifier === "function" && (modifier as RequestModifier)(fetchContext);
if (!fetchContext.resource) {
fetchContext.resource = createResource(
fetchContext.urlAccessor,
fetchContext.fetcher!,
options as any
) as ResourceReturn<T>;
) as ResourceReturn<Result>;
}
return fetchContext.resource!;
}
};
fetchContext.wrapResource();
return fetchContext.resource as unknown as FetchReturn<T, I>;
}
return fetchContext.resource as unknown as FetchReturn<Result, InitialValue>;
};
Loading

0 comments on commit 54e380c

Please sign in to comment.