nuxt
Version:
232 lines (231 loc) • 8.09 kB
JavaScript
import { useNuxtApp } from "../nuxt.js";
import { stateDiagnostics } from "../diagnostics/state.js";
import { useRequestEvent } from "./ssr.js";
import { customRef, getCurrentScope, nextTick, onScopeDispose, ref, watch } from "vue";
import { klona } from "klona";
import { cookieStore } from "#build/nuxt.config.mjs";
import { deleteCookie, getCookie, getRequestHeader, setCookie } from "@nuxt/nitro-server/h3";
import { parse, serialize } from "cookie-es";
import { isEqual } from "ohash";
//#region src/app/composables/cookie.ts
function parseCookieValue(value) {
if (value === "undefined") return;
try {
const parsed = JSON.parse(value);
if (typeof parsed === "number" && String(parsed) !== value) return value;
return parsed;
} catch {
return value;
}
}
const CookieDefaults = {
path: "/",
watch: true,
decode: (val) => val ? parseCookieValue(decodeURIComponent(val)) : val,
encode: (val) => {
if (typeof val !== "string" || val === "undefined") return encodeURIComponent(JSON.stringify(val));
try {
if (typeof JSON.parse(val) !== "string") return encodeURIComponent(JSON.stringify(val));
} catch {}
return encodeURIComponent(val);
},
refresh: false
};
const store = import.meta.client && cookieStore ? globalThis.cookieStore : void 0;
function useCookie(name, _opts) {
const opts = {
...CookieDefaults,
..._opts
};
opts.filter ??= (key) => key === name;
const cookies = readRawCookies(opts) || {};
let delay;
if (opts.maxAge !== void 0) delay = opts.maxAge * 1e3;
else if (opts.expires) delay = opts.expires.getTime() - Date.now();
const hasExpired = delay !== void 0 && delay <= 0;
const shouldSetInitialClientCookie = import.meta.client && (hasExpired || cookies[name] === void 0 || cookies[name] === null);
const cookieValue = klona(hasExpired ? void 0 : cookies[name] ?? opts.default?.());
const cookie = import.meta.client && delay && !hasExpired ? cookieRef(cookieValue, delay, opts.watch && opts.watch !== "shallow") : import.meta.server ? cookieServerRef(name, cookieValue) : ref(cookieValue);
if (import.meta.dev && hasExpired) stateDiagnostics.NUXT_E7005({ name });
if (import.meta.client) {
let channel = null;
try {
if (!store && typeof BroadcastChannel !== "undefined") channel = new BroadcastChannel(`nuxt:cookies:${name}`);
} catch {}
const callback = (force = false) => {
if (!force) {
if (opts.readonly || isEqual(cookie.value, cookies[name])) return;
}
writeClientCookie(name, cookie.value === null || cookie.value === void 0 ? void 0 : opts.encode(cookie.value), opts);
cookies[name] = klona(cookie.value);
channel?.postMessage({ value: opts.encode(cookie.value) });
};
const handleChange = (data) => {
const value = data.refresh ? readRawCookies(opts)?.[name] : opts.decode(data.value);
watchPaused = true;
cookie.value = value;
cookies[name] = klona(value);
nextTick(() => {
watchPaused = false;
});
};
let watchPaused = false;
const hasScope = !!getCurrentScope();
if (hasScope) onScopeDispose(() => {
watchPaused = true;
callback();
channel?.close();
});
if (store) {
const changeHandler = (event) => {
const changedCookie = event.changed.find((c) => c.name === name);
const removedCookie = event.deleted.find((c) => c.name === name);
if (changedCookie) handleChange({ value: changedCookie.value });
if (removedCookie) handleChange({ value: null });
};
store.addEventListener("change", changeHandler);
if (hasScope) onScopeDispose(() => store.removeEventListener("change", changeHandler));
} else if (channel) channel.onmessage = ({ data }) => handleChange(data);
if (opts.watch) watch(cookie, () => {
if (watchPaused) return;
callback(opts.refresh);
}, { deep: opts.watch !== "shallow" });
if (shouldSetInitialClientCookie) callback(shouldSetInitialClientCookie);
} else if (import.meta.server) {
const nuxtApp = useNuxtApp();
const writeFinalCookieValue = () => {
const valueIsSame = isEqual(cookie.value, cookies[name]);
if (opts.readonly || valueIsSame && !opts.refresh) return;
nuxtApp._cookiesChanged ||= {};
if (valueIsSame && opts.refresh && !nuxtApp._cookiesChanged[name]) return;
nuxtApp._cookies ||= {};
if (name in nuxtApp._cookies) {
if (isEqual(cookie.value, nuxtApp._cookies[name])) return;
if (import.meta.dev) stateDiagnostics.NUXT_E7006({
name,
previous: opts.encode(nuxtApp._cookies[name]),
next: opts.encode(cookie.value)
});
}
nuxtApp._cookies[name] = cookie.value;
const encoded = cookie.value === null || cookie.value === void 0 ? void 0 : opts.encode(cookie.value);
writeServerCookie(useRequestEvent(nuxtApp), name, encoded, opts);
};
const unhook = nuxtApp.hooks.hookOnce("app:rendered", writeFinalCookieValue);
nuxtApp.hooks.hookOnce("app:error", () => {
unhook();
return writeFinalCookieValue();
});
}
return cookie;
}
/** @since 3.10.0 */
function refreshCookie(name) {
if (import.meta.server || store || typeof BroadcastChannel === "undefined") return;
try {
const channel = new BroadcastChannel(`nuxt:cookies:${name}`);
channel.postMessage({ refresh: true });
channel.close();
} catch {}
}
function readRawCookies(opts = {}) {
if (import.meta.server) return parse(getRequestHeader(useRequestEvent(), "cookie") || "", opts);
else if (import.meta.client) return parse(document.cookie, opts);
}
const identityEncode = (val) => val;
function toSerializeOptions(opts) {
const { encode: _encode, decode: _decode, ...rest } = opts;
return {
...rest,
encode: identityEncode
};
}
function serializeCookie(name, value, opts = {}) {
const serializeOpts = toSerializeOptions(opts);
if (value === void 0) return serialize(name, "", {
...serializeOpts,
maxAge: -1
});
return serialize(name, value, serializeOpts);
}
function writeClientCookie(name, value, opts = {}) {
if (import.meta.client) document.cookie = serializeCookie(name, value, opts);
}
function writeServerCookie(event, name, value, opts = {}) {
if (event) {
const serializeOpts = toSerializeOptions(opts);
if (value !== void 0) return setCookie(event, name, value, serializeOpts);
if (getCookie(event, name) !== void 0) return deleteCookie(event, name, serializeOpts);
}
}
/**
* The maximum value allowed on a timeout delay.
*
* Reference: https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout#maximum_delay_value
*/
const MAX_TIMEOUT_DELAY = 2147483647;
function cookieRef(value, delay, shouldWatch) {
let timeout;
let unsubscribe;
let elapsed = 0;
const internalRef = shouldWatch ? ref(value) : { value };
if (getCurrentScope()) onScopeDispose(() => {
unsubscribe?.();
clearTimeout(timeout);
});
return customRef((track, trigger) => {
if (shouldWatch) unsubscribe = watch(internalRef, trigger);
function scheduleTimeout() {
const timeRemaining = delay - elapsed;
const timeoutLength = timeRemaining < MAX_TIMEOUT_DELAY ? timeRemaining : MAX_TIMEOUT_DELAY;
timeout = setTimeout(() => {
elapsed += timeoutLength;
if (elapsed < delay) return scheduleTimeout();
internalRef.value = void 0;
trigger();
}, timeoutLength);
}
function createExpirationTimeout() {
elapsed = 0;
clearTimeout(timeout);
scheduleTimeout();
}
return {
get() {
track();
return internalRef.value;
},
set(newValue) {
createExpirationTimeout();
internalRef.value = newValue;
trigger();
}
};
});
}
/**
* Custom ref that tracks explicit cookie writes on the server.
*
* This is required for the `refresh` option to ensure the cookie is
* re-written on SSR even when the value remains unchanged.
*/
function cookieServerRef(name, value) {
const internalRef = ref(value);
const nuxtApp = useNuxtApp();
return customRef((track, trigger) => {
return {
get() {
track();
return internalRef.value;
},
set(newValue) {
nuxtApp._cookiesChanged ||= {};
nuxtApp._cookiesChanged[name] = true;
internalRef.value = newValue;
trigger();
}
};
});
}
//#endregion
export { refreshCookie, useCookie };