fix(nuxt): watch custom cookieRef values deeply (#26151)

This commit is contained in:
Daniel Roe 2024-03-08 17:03:31 +00:00 committed by GitHub
parent 5c284ffda8
commit 6407cea620
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 44 additions and 10 deletions

View File

@ -55,7 +55,7 @@ export function useCookie<T = string | null | undefined> (name: string, _opts?:
// use a custom ref to expire the cookie on client side otherwise use basic ref // use a custom ref to expire the cookie on client side otherwise use basic ref
const cookie = import.meta.client && delay && !hasExpired const cookie = import.meta.client && delay && !hasExpired
? cookieRef<T | undefined>(cookieValue, delay) ? cookieRef<T | undefined>(cookieValue, delay, opts.watch && opts.watch !== 'shallow')
: ref<T | undefined>(cookieValue) : ref<T | undefined>(cookieValue)
if (import.meta.dev && hasExpired) { if (import.meta.dev && hasExpired) {
@ -123,9 +123,9 @@ export function useCookie<T = string | null | undefined> (name: string, _opts?:
return cookie as CookieRef<T> return cookie as CookieRef<T>
} }
/** @since 3.10.0 */ /** @since 3.10.0 */
export function refreshCookie(name: string) { export function refreshCookie (name: string) {
if (store || typeof BroadcastChannel === 'undefined') return if (store || typeof BroadcastChannel === 'undefined') return
new BroadcastChannel(`nuxt:cookies:${name}`)?.postMessage({ refresh: true }) new BroadcastChannel(`nuxt:cookies:${name}`)?.postMessage({ refresh: true })
} }
@ -174,14 +174,21 @@ function writeServerCookie (event: H3Event, name: string, value: any, opts: Cook
const MAX_TIMEOUT_DELAY = 2_147_483_647 const MAX_TIMEOUT_DELAY = 2_147_483_647
// custom ref that will update the value to undefined if the cookie expires // custom ref that will update the value to undefined if the cookie expires
function cookieRef<T> (value: T | undefined, delay: number) { function cookieRef<T> (value: T | undefined, delay: number, shouldWatch: boolean) {
let timeout: NodeJS.Timeout let timeout: NodeJS.Timeout
let unsubscribe: (() => void) | undefined
let elapsed = 0 let elapsed = 0
const internalRef = shouldWatch ? ref(value) : { value }
if (getCurrentScope()) { if (getCurrentScope()) {
onScopeDispose(() => { clearTimeout(timeout) }) onScopeDispose(() => {
unsubscribe?.()
clearTimeout(timeout)
})
} }
return customRef((track, trigger) => { return customRef((track, trigger) => {
if (shouldWatch) { unsubscribe = watch(internalRef, trigger) }
function createExpirationTimeout () { function createExpirationTimeout () {
clearTimeout(timeout) clearTimeout(timeout)
const timeRemaining = delay - elapsed const timeRemaining = delay - elapsed
@ -190,7 +197,7 @@ function cookieRef<T> (value: T | undefined, delay: number) {
elapsed += timeoutLength elapsed += timeoutLength
if (elapsed < delay) { return createExpirationTimeout() } if (elapsed < delay) { return createExpirationTimeout() }
value = undefined internalRef.value = undefined
trigger() trigger()
}, timeoutLength) }, timeoutLength)
} }
@ -198,12 +205,12 @@ function cookieRef<T> (value: T | undefined, delay: number) {
return { return {
get () { get () {
track() track()
return value return internalRef.value
}, },
set (newValue) { set (newValue) {
createExpirationTimeout() createExpirationTimeout()
value = newValue internalRef.value = newValue
trigger() trigger()
} }
} }

View File

@ -97,6 +97,7 @@ describe('composables', () => {
'useRequestFetch', 'useRequestFetch',
'isPrerendered', 'isPrerendered',
'useRequestHeaders', 'useRequestHeaders',
'useCookie',
'clearNuxtState', 'clearNuxtState',
'useState', 'useState',
'useRequestURL', 'useRequestURL',
@ -121,7 +122,6 @@ describe('composables', () => {
'preloadRouteComponents', 'preloadRouteComponents',
'reloadNuxtApp', 'reloadNuxtApp',
'refreshCookie', 'refreshCookie',
'useCookie',
'useFetch', 'useFetch',
'useHead', 'useHead',
'useLazyFetch', 'useLazyFetch',
@ -628,6 +628,33 @@ describe('defineNuxtComponent', () => {
it.todo('should support Options API head') it.todo('should support Options API head')
}) })
describe('useCookie', () => {
it('should watch custom cookie refs', () => {
const user = useCookie('userInfo', {
default: () => ({ score: -1 }),
maxAge: 60 * 60,
})
const computedVal = computed(() => user.value.score)
expect(computedVal.value).toBe(-1)
user.value.score++
expect(computedVal.value).toBe(0)
})
it('should not watch custom cookie refs when shallow', () => {
for (const value of ['shallow', false] as const) {
const user = useCookie('shallowUserInfo', {
default: () => ({ score: -1 }),
maxAge: 60 * 60,
watch: value
})
const computedVal = computed(() => user.value.score)
expect(computedVal.value).toBe(-1)
user.value.score++
expect(computedVal.value).toBe(-1)
}
})
})
describe('callOnce', () => { describe('callOnce', () => {
it('should only call composable once', async () => { it('should only call composable once', async () => {
const fn = vi.fn() const fn = vi.fn()
@ -643,7 +670,7 @@ describe('callOnce', () => {
await Promise.all([execute(), execute(), execute()]) await Promise.all([execute(), execute(), execute()])
expect(fn).toHaveBeenCalledTimes(1) expect(fn).toHaveBeenCalledTimes(1)
const fnSync = vi.fn().mockImplementation(() => { }) const fnSync = vi.fn().mockImplementation(() => {})
const executeSync = () => callOnce(fnSync) const executeSync = () => callOnce(fnSync)
await Promise.all([executeSync(), executeSync(), executeSync()]) await Promise.all([executeSync(), executeSync(), executeSync()])
expect(fnSync).toHaveBeenCalledTimes(1) expect(fnSync).toHaveBeenCalledTimes(1)