Skip to content

Commit

Permalink
fix(nuxt): watch custom cookieRef values deeply (#26151)
Browse files Browse the repository at this point in the history
  • Loading branch information
danielroe committed Mar 8, 2024
1 parent 5c284ff commit 6407cea
Show file tree
Hide file tree
Showing 2 changed files with 44 additions and 10 deletions.
23 changes: 15 additions & 8 deletions packages/nuxt/src/app/composables/cookie.ts
Expand Up @@ -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
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)

if (import.meta.dev && hasExpired) {
Expand Down Expand Up @@ -123,9 +123,9 @@ export function useCookie<T = string | null | undefined> (name: string, _opts?:
return cookie as CookieRef<T>
}
/** @since 3.10.0 */
export function refreshCookie(name: string) {
export function refreshCookie (name: string) {
if (store || typeof BroadcastChannel === 'undefined') return

new BroadcastChannel(`nuxt:cookies:${name}`)?.postMessage({ refresh: true })
}

Expand Down Expand Up @@ -174,14 +174,21 @@ function writeServerCookie (event: H3Event, name: string, value: any, opts: Cook
const MAX_TIMEOUT_DELAY = 2_147_483_647

// 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 unsubscribe: (() => void) | undefined
let elapsed = 0
const internalRef = shouldWatch ? ref(value) : { value }
if (getCurrentScope()) {
onScopeDispose(() => { clearTimeout(timeout) })
onScopeDispose(() => {
unsubscribe?.()
clearTimeout(timeout)
})
}

return customRef((track, trigger) => {
if (shouldWatch) { unsubscribe = watch(internalRef, trigger) }

function createExpirationTimeout () {
clearTimeout(timeout)
const timeRemaining = delay - elapsed
Expand All @@ -190,20 +197,20 @@ function cookieRef<T> (value: T | undefined, delay: number) {
elapsed += timeoutLength
if (elapsed < delay) { return createExpirationTimeout() }

value = undefined
internalRef.value = undefined
trigger()
}, timeoutLength)
}

return {
get () {
track()
return value
return internalRef.value
},
set (newValue) {
createExpirationTimeout()

value = newValue
internalRef.value = newValue
trigger()
}
}
Expand Down
31 changes: 29 additions & 2 deletions test/nuxt/composables.test.ts
Expand Up @@ -97,6 +97,7 @@ describe('composables', () => {
'useRequestFetch',
'isPrerendered',
'useRequestHeaders',
'useCookie',
'clearNuxtState',
'useState',
'useRequestURL',
Expand All @@ -121,7 +122,6 @@ describe('composables', () => {
'preloadRouteComponents',
'reloadNuxtApp',
'refreshCookie',
'useCookie',
'useFetch',
'useHead',
'useLazyFetch',
Expand Down Expand Up @@ -628,6 +628,33 @@ describe('defineNuxtComponent', () => {
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', () => {
it('should only call composable once', async () => {
const fn = vi.fn()
Expand All @@ -643,7 +670,7 @@ describe('callOnce', () => {
await Promise.all([execute(), execute(), execute()])
expect(fn).toHaveBeenCalledTimes(1)

const fnSync = vi.fn().mockImplementation(() => { })
const fnSync = vi.fn().mockImplementation(() => {})
const executeSync = () => callOnce(fnSync)
await Promise.all([executeSync(), executeSync(), executeSync()])
expect(fnSync).toHaveBeenCalledTimes(1)
Expand Down

0 comments on commit 6407cea

Please sign in to comment.