Skip to content

Instantly share code, notes, and snippets.

@ejsmith
Created August 17, 2023 04:08
Show Gist options
  • Save ejsmith/2518bb40b021af02172ba6cfe90ae62f to your computer and use it in GitHub Desktop.
Save ejsmith/2518bb40b021af02172ba6cfe90ae62f to your computer and use it in GitHub Desktop.
Svelte Fetch wrapper
import { writable, derived } from "svelte/store";
import { goto } from "$app/navigation";
function createCount() {
const { subscribe, set, update } = writable(0);
return {
subscribe,
increment: () => update((n) => n + 1),
decrement: () => update((n) => n - 1),
reset: () => set(0)
};
}
export const accessToken = writable<string | null>(null);
export const base = 'api';
type Fetch = typeof globalThis.fetch;
export type RequestOptions = {
params?: Record<string, unknown>;
expectedStatusCodes?: number[];
unauthorizedShouldRedirect?: boolean;
errorCallback?: (error: Response) => void;
}
export class FetchClient {
private accessToken: string | null = null;
constructor(private fetch: Fetch = window.fetch) {
accessToken.subscribe(token => this.accessToken = token);
}
requestCount = createCount();
loading = derived(this.requestCount, $requestCount => $requestCount > 0);
async get(url: string, options?: RequestOptions): Promise<Response> {
const response = await this.fetchInternal(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
}, options);
return response;
}
async getJSON<T>(url: string, options?: RequestOptions): Promise<T> {
const response = await this.get(url, options);
const data = await response.json();
return data as T;
}
async post(url: string, body?: object | string, options?: RequestOptions) : Promise<Response> {
const response = await this.fetchInternal(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: typeof body === 'string' ? body : JSON.stringify(body)
}, options);
return response;
}
async postJSON<T extends object = Response>(url: string, body?: object | string, options?: RequestOptions) : Promise<T> {
const response = await this.post(url, body, options);
const data = await response.json();
return data as T;
}
async put(url: string, body?: object | string, options?: RequestOptions) : Promise<Response> {
const response = await this.fetchInternal(url, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: typeof body === 'string' ? body : JSON.stringify(body)
}, options);
return response;
}
async putJSON<T = object>(url: string, body?: object | string, options?: RequestOptions) : Promise<T> {
const response = await this.put(url, body, options);
const data = await response.json();
return data as T;
}
async patch(url: string, body?: object | string, options?: RequestOptions) : Promise<Response> {
const response = await this.fetchInternal(url, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: typeof body === 'string' ? body : JSON.stringify(body)
}, options);
return response;
}
async patchJSON<T>(url: string, body?: object | string, options?: RequestOptions): Promise<T> {
const response = await this.patch(url, body, options);
const data = await response.json();
return data as T;
}
async delete(url: string, options?: RequestOptions): Promise<Response> {
const response = await this.fetchInternal(url, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
}, options);
return response;
}
private async fetchInternal(url: string, init?: RequestInit, options?: RequestOptions) : Promise<Response> {
url = this.buildUrl(url, options);
this.requestCount.increment();
if (this.accessToken !== null) {
if (!init)
init = {};
if (!init.headers)
init.headers = new Headers();
const headers = init.headers as Headers;
headers.set('Authorization', `Bearer ${this.accessToken}`);
}
const response = await this.fetch(url, init);
this.requestCount.decrement();
this.validateResponse(response, options);
return response;
}
private buildUrl(url: string, options: RequestOptions | undefined) : string {
const isAbsoluteUrl = url.startsWith('http');
if (!url.startsWith('http'))
url = base + '/' + url;
const parsed = new URL(url, window.location.origin);
if (options?.params) {
for (const [key, value] of Object.entries(options?.params)) {
parsed.searchParams.append(key, value as string);
}
url = parsed.toString();
}
if (isAbsoluteUrl)
return url;
return parsed.pathname + parsed.search;
}
private validateResponse(response: Response, options: RequestOptions | undefined) {
if (response.ok)
return;
if (response.status === 401 && options?.unauthorizedShouldRedirect != false) {
const referrer = location.href;
goto('/login?referrer=' + referrer, { replaceState: true });
return;
}
if (options?.expectedStatusCodes && options.expectedStatusCodes.includes(response.status))
return;
if (options?.errorCallback)
options.errorCallback(response);
else
throw new Error(response.status.toString());
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment