From 6407cea6203fc3cfd887866e8e41cc0ea9797cea Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Fri, 8 Mar 2024 17:03:31 +0000 Subject: [PATCH] fix(nuxt): watch custom `cookieRef` values deeply (#26151) --- packages/nuxt/src/app/composables/cookie.ts | 23 +++++++++------ test/nuxt/composables.test.ts | 31 +++++++++++++++++++-- 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/packages/nuxt/src/app/composables/cookie.ts b/packages/nuxt/src/app/composables/cookie.ts index 09015f854486..9c074a97faa7 100644 --- a/packages/nuxt/src/app/composables/cookie.ts +++ b/packages/nuxt/src/app/composables/cookie.ts @@ -55,7 +55,7 @@ export function useCookie (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(cookieValue, delay) + ? cookieRef(cookieValue, delay, opts.watch && opts.watch !== 'shallow') : ref(cookieValue) if (import.meta.dev && hasExpired) { @@ -123,9 +123,9 @@ export function useCookie (name: string, _opts?: return cookie as CookieRef } /** @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 }) } @@ -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 (value: T | undefined, delay: number) { +function cookieRef (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 @@ -190,7 +197,7 @@ function cookieRef (value: T | undefined, delay: number) { elapsed += timeoutLength if (elapsed < delay) { return createExpirationTimeout() } - value = undefined + internalRef.value = undefined trigger() }, timeoutLength) } @@ -198,12 +205,12 @@ function cookieRef (value: T | undefined, delay: number) { return { get () { track() - return value + return internalRef.value }, set (newValue) { createExpirationTimeout() - value = newValue + internalRef.value = newValue trigger() } } diff --git a/test/nuxt/composables.test.ts b/test/nuxt/composables.test.ts index 8104a1340b20..d0b6e68bcd76 100644 --- a/test/nuxt/composables.test.ts +++ b/test/nuxt/composables.test.ts @@ -97,6 +97,7 @@ describe('composables', () => { 'useRequestFetch', 'isPrerendered', 'useRequestHeaders', + 'useCookie', 'clearNuxtState', 'useState', 'useRequestURL', @@ -121,7 +122,6 @@ describe('composables', () => { 'preloadRouteComponents', 'reloadNuxtApp', 'refreshCookie', - 'useCookie', 'useFetch', 'useHead', 'useLazyFetch', @@ -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() @@ -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)