233 lines
8.0 KiB
JavaScript
233 lines
8.0 KiB
JavaScript
import { createSignal, getListener, getOwner, onCleanup, sharedConfig, startTransition } from "solid-js";
|
|
import { getRequestEvent, isServer } from "solid-js/web";
|
|
import { useNavigate, getIntent, getInPreloadFn } from "../routing.js";
|
|
const LocationHeader = "Location";
|
|
const PRELOAD_TIMEOUT = 5000;
|
|
const CACHE_TIMEOUT = 180000;
|
|
let cacheMap = new Map();
|
|
// cleanup forward/back cache
|
|
if (!isServer) {
|
|
setInterval(() => {
|
|
const now = Date.now();
|
|
for (let [k, v] of cacheMap.entries()) {
|
|
if (!v[4].count && now - v[0] > CACHE_TIMEOUT) {
|
|
cacheMap.delete(k);
|
|
}
|
|
}
|
|
}, 300000);
|
|
}
|
|
function getCache() {
|
|
if (!isServer)
|
|
return cacheMap;
|
|
const req = getRequestEvent();
|
|
if (!req)
|
|
throw new Error("Cannot find cache context");
|
|
return (req.router || (req.router = {})).cache || (req.router.cache = new Map());
|
|
}
|
|
/**
|
|
* Revalidates the given cache entry/entries.
|
|
*/
|
|
export function revalidate(key, force = true) {
|
|
return startTransition(() => {
|
|
const now = Date.now();
|
|
cacheKeyOp(key, entry => {
|
|
force && (entry[0] = 0); //force cache miss
|
|
entry[4][1](now); // retrigger live signals
|
|
});
|
|
});
|
|
}
|
|
export function cacheKeyOp(key, fn) {
|
|
key && !Array.isArray(key) && (key = [key]);
|
|
for (let k of cacheMap.keys()) {
|
|
if (key === undefined || matchKey(k, key))
|
|
fn(cacheMap.get(k));
|
|
}
|
|
}
|
|
export function query(fn, name) {
|
|
// prioritize GET for server functions
|
|
if (fn.GET)
|
|
fn = fn.GET;
|
|
const cachedFn = ((...args) => {
|
|
const cache = getCache();
|
|
const intent = getIntent();
|
|
const inPreloadFn = getInPreloadFn();
|
|
const owner = getOwner();
|
|
const navigate = owner ? useNavigate() : undefined;
|
|
const now = Date.now();
|
|
const key = name + hashKey(args);
|
|
let cached = cache.get(key);
|
|
let tracking;
|
|
if (isServer) {
|
|
const e = getRequestEvent();
|
|
if (e) {
|
|
const dataOnly = (e.router || (e.router = {})).dataOnly;
|
|
if (dataOnly) {
|
|
const data = e && (e.router.data || (e.router.data = {}));
|
|
if (data && key in data)
|
|
return data[key];
|
|
if (Array.isArray(dataOnly) && !matchKey(key, dataOnly)) {
|
|
data[key] = undefined;
|
|
return Promise.resolve();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (getListener() && !isServer) {
|
|
tracking = true;
|
|
onCleanup(() => cached[4].count--);
|
|
}
|
|
if (cached &&
|
|
cached[0] &&
|
|
(isServer ||
|
|
intent === "native" ||
|
|
cached[4].count ||
|
|
Date.now() - cached[0] < PRELOAD_TIMEOUT)) {
|
|
if (tracking) {
|
|
cached[4].count++;
|
|
cached[4][0](); // track
|
|
}
|
|
if (cached[3] === "preload" && intent !== "preload") {
|
|
cached[0] = now;
|
|
}
|
|
let res = cached[1];
|
|
if (intent !== "preload") {
|
|
res =
|
|
"then" in cached[1]
|
|
? cached[1].then(handleResponse(false), handleResponse(true))
|
|
: handleResponse(false)(cached[1]);
|
|
!isServer && intent === "navigate" && startTransition(() => cached[4][1](cached[0])); // update version
|
|
}
|
|
inPreloadFn && "then" in res && res.catch(() => { });
|
|
return res;
|
|
}
|
|
let res;
|
|
if (!isServer && sharedConfig.has && sharedConfig.has(key)) {
|
|
res = sharedConfig.load(key); // hydrating
|
|
// @ts-ignore at least until we add a delete method to sharedConfig
|
|
delete globalThis._$HY.r[key];
|
|
}
|
|
else
|
|
res = fn(...args);
|
|
if (cached) {
|
|
cached[0] = now;
|
|
cached[1] = res;
|
|
cached[3] = intent;
|
|
!isServer && intent === "navigate" && startTransition(() => cached[4][1](cached[0])); // update version
|
|
}
|
|
else {
|
|
cache.set(key, (cached = [now, res, , intent, createSignal(now)]));
|
|
cached[4].count = 0;
|
|
}
|
|
if (tracking) {
|
|
cached[4].count++;
|
|
cached[4][0](); // track
|
|
}
|
|
if (isServer) {
|
|
const e = getRequestEvent();
|
|
if (e && e.router.dataOnly)
|
|
return (e.router.data[key] = res);
|
|
}
|
|
if (intent !== "preload") {
|
|
res =
|
|
"then" in res
|
|
? res.then(handleResponse(false), handleResponse(true))
|
|
: handleResponse(false)(res);
|
|
}
|
|
inPreloadFn && "then" in res && res.catch(() => { });
|
|
// serialize on server
|
|
if (isServer &&
|
|
sharedConfig.context &&
|
|
sharedConfig.context.async &&
|
|
!sharedConfig.context.noHydrate) {
|
|
const e = getRequestEvent();
|
|
(!e || !e.serverOnly) && sharedConfig.context.serialize(key, res);
|
|
}
|
|
return res;
|
|
function handleResponse(error) {
|
|
return async (v) => {
|
|
if (v instanceof Response) {
|
|
const e = getRequestEvent();
|
|
if (e) {
|
|
for (const [key, value] of v.headers) {
|
|
if (key == "set-cookie")
|
|
e.response.headers.append("set-cookie", value);
|
|
else
|
|
e.response.headers.set(key, value);
|
|
}
|
|
}
|
|
const url = v.headers.get(LocationHeader);
|
|
if (url !== null) {
|
|
// client + server relative redirect
|
|
if (navigate && url.startsWith("/"))
|
|
startTransition(() => {
|
|
navigate(url, { replace: true });
|
|
});
|
|
else if (!isServer)
|
|
window.location.href = url;
|
|
else if (e)
|
|
e.response.status = 302;
|
|
return;
|
|
}
|
|
if (v.customBody)
|
|
v = await v.customBody();
|
|
}
|
|
if (error)
|
|
throw v;
|
|
cached[2] = v;
|
|
return v;
|
|
};
|
|
}
|
|
});
|
|
cachedFn.keyFor = (...args) => name + hashKey(args);
|
|
cachedFn.key = name;
|
|
return cachedFn;
|
|
}
|
|
query.get = (key) => {
|
|
const cached = getCache().get(key);
|
|
return cached[2];
|
|
};
|
|
query.set = (key, value) => {
|
|
const cache = getCache();
|
|
const now = Date.now();
|
|
let cached = cache.get(key);
|
|
if (cached) {
|
|
cached[0] = now;
|
|
cached[1] = Promise.resolve(value);
|
|
cached[2] = value;
|
|
cached[3] = "preload";
|
|
}
|
|
else {
|
|
cache.set(key, (cached = [now, Promise.resolve(value), value, "preload", createSignal(now)]));
|
|
cached[4].count = 0;
|
|
}
|
|
};
|
|
query.delete = (key) => getCache().delete(key);
|
|
query.clear = () => getCache().clear();
|
|
/** @deprecated use query instead */
|
|
export const cache = query;
|
|
function matchKey(key, keys) {
|
|
for (let k of keys) {
|
|
if (k && key.startsWith(k))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
// Modified from the amazing Tanstack Query library (MIT)
|
|
// https://github.com/TanStack/query/blob/main/packages/query-core/src/utils.ts#L168
|
|
export function hashKey(args) {
|
|
return JSON.stringify(args, (_, val) => isPlainObject(val)
|
|
? Object.keys(val)
|
|
.sort()
|
|
.reduce((result, key) => {
|
|
result[key] = val[key];
|
|
return result;
|
|
}, {})
|
|
: val);
|
|
}
|
|
function isPlainObject(obj) {
|
|
let proto;
|
|
return (obj != null &&
|
|
typeof obj === "object" &&
|
|
(!(proto = Object.getPrototypeOf(obj)) || proto === Object.prototype));
|
|
}
|