Skip to main content
Module

x/aleph/runtime/vue/data.ts

The Full-stack Framework in Deno.
Very Popular
Go to Latest
File
import { inject, onBeforeUnmount, Ref, ref, toRaw, watch } from "vue";import { FetchError } from "../core/error.ts";import type { SSRContext } from "../../server/types.ts";import { HttpMethod, UpdateStrategy } from "./context.ts";
export type RouteData = { data?: unknown; dataCacheTtl?: number; dataExpires?: number;};
export type DataProviderProps = { dataUrl: string; dataCache: Map<string, RouteData>;};
// fixme: drop unnecessary data fetchingconst createDataProvider = () => { const dataCache: Map<string, RouteData> = inject("dataCache") || new Map(); const ssrContext: SSRContext | undefined = inject("ssrContext"); const url = ssrContext?.url || new URL(window.location?.href); const defaultDataUrl = url.pathname + url.search; const dataUrl: Ref<string> = inject("dataUrl") || ref(defaultDataUrl);
const cached = dataCache?.get(dataUrl.value);
if (cached) { if (cached.data instanceof Error) { throw cached.data; } if (typeof cached.data === "function") { const data = cached.data(); if (data instanceof Promise) { data.then((data) => { cached.data = data; }).catch((error) => { cached.data = error; }); } throw new Error(`Data for ${dataUrl} has invalid type [function].`); } } else { throw new Error(`Data for ${dataUrl} is not found`); }
const _data: Ref<unknown> = ref(cached?.data); const isMutating = ref<HttpMethod | boolean>(false);
const action = async (method: HttpMethod, fetcher: Promise<Response>, update: UpdateStrategy) => { const updateIsObject = update && typeof update === "object" && update !== null; const optimistic = updateIsObject && typeof update.optimisticUpdate === "function"; const replace = update === "replace" || (updateIsObject && !!update.replace);
let rollbackData: unknown = undefined; if (optimistic) { const optimisticUpdate = update.optimisticUpdate!; if (_data.value !== undefined) { rollbackData = toRaw(_data.value); _data.value = optimisticUpdate(shallowClone(toRaw(_data.value))); } }
isMutating.value = method; const res = await fetcher; if (res.status >= 400) { const err = await FetchError.fromResponse(res); const details = err.details as { redirect?: { location: string } }; if (err.status === 501 && typeof details.redirect?.location === "string") { location.href = details.redirect?.location; return res; }
if (optimistic) { if (rollbackData !== undefined) { _data.value = rollbackData; } if (update.onFailure) { update.onFailure(err); } isMutating.value = false; return res; }
throw err; }
if (replace && res.ok) { try { const data = await res.json(); const dataCacheTtl = dataCache.get(dataUrl.value)?.dataCacheTtl; dataCache.set(dataUrl.value, { data, dataCacheTtl, dataExpires: Date.now() + (dataCacheTtl || 1) * 1000 }); _data.value = data; } catch (_) { if (optimistic) { if (rollbackData !== undefined) { _data.value = rollbackData; } if (update.onFailure) { update.onFailure(new FetchError(500, "Data must be valid JSON")); } } } }
isMutating.value = false; return res; };
const reload = async (signal?: AbortSignal) => { try { const res = await fetch(dataUrl.value + (dataUrl.value.includes("?") ? "&" : "?") + "_data_", { signal });
if (!res.ok) { const err = await FetchError.fromResponse(res); const details = err.details as { redirect?: { location: string } }; if (err.status === 501 && typeof details.redirect?.location === "string") { location.href = details.redirect?.location; await new Promise(() => {}); } throw err; }
try { const data = await res.json(); const cc = res.headers.get("Cache-Control"); const dataCacheTtl = cc && cc.includes("max-age=") ? parseInt(cc.split("max-age=")[1]) : undefined; const dataExpires = Date.now() + (dataCacheTtl || 1) * 1000; dataCache.set(dataUrl.value, { data, dataExpires }); _data.value = data; } catch (_e) { throw new FetchError(500, "Data must be valid JSON"); } } catch (error) { throw new Error(`Failed to reload data for ${dataUrl.value}: ${error.message}`); } };
const mutation = { post: (data?: unknown, update?: UpdateStrategy) => { return action("post", send("post", dataUrl.value, data), update ?? "none"); }, put: (data?: unknown, update?: UpdateStrategy) => { return action("put", send("put", dataUrl.value, data), update ?? "none"); }, patch: (data?: unknown, update?: UpdateStrategy) => { return action("patch", send("patch", dataUrl.value, data), update ?? "none"); }, delete: (data?: unknown, update?: UpdateStrategy) => { return action("delete", send("delete", dataUrl.value, data), update ?? "none"); }, };
watch(() => dataUrl.value, () => { const now = Date.now(); const cache = dataCache.get(dataUrl.value); let ac: AbortController | null = null; if (cache === undefined || cache.dataExpires === undefined || cache.dataExpires < now) { ac = new AbortController(); reload(ac.signal).finally(() => { ac = null; }); } else if (cache.data !== undefined) { _data.value = cache.data as never; }
onBeforeUnmount(() => ac?.abort()); });
return { data: _data, isMutating, mutation, reload };};
export const useData = () => { return createDataProvider();};
function send(method: HttpMethod, href: string, data: unknown) { let body: BodyInit | undefined; const headers = new Headers([["Accept", "application/json"]]); if (typeof data === "string") { body = data; } else if (typeof data === "number") { body = data.toString(); } else if (typeof data === "object") { if (data instanceof ArrayBuffer || data instanceof Uint8Array) { body = data; } else if (data instanceof FormData) { body = data; } else if (data instanceof URLSearchParams) { body = data; } else if (data instanceof Blob) { body = data; headers.append("Content-Type", data.type); } else { body = JSON.stringify(data); headers.append("Content-Type", "application/json; charset=utf-8"); } } // NOTE: RFC 2616 section 5.1.1 and RFC 7231 section 4.1 state that the method // token is case-sensitive, and all tokens are by convention all-uppercase. return fetch(href, { method: method.toUpperCase(), body, headers });}
function shallowClone<T>(obj: T): T { if (obj === null || typeof obj !== "object") { return obj; } if (Array.isArray(obj)) { return [...obj] as unknown as T; } return { ...obj };}