1
- import { Ref , ref , watch } from 'vue'
2
- import { parse , serialize , CookieParseOptions , CookieSerializeOptions } from 'cookie-es'
3
- import { appendHeader } from 'h3'
1
+ import type { Ref } from 'vue'
2
+ import { ref , watch , getCurrentScope , onScopeDispose , customRef , nextTick } from 'vue'
3
+ import { parse , serialize } from 'cookie-es'
4
+ import type { CookieParseOptions , CookieSerializeOptions } from 'cookie-es'
5
+ import { deleteCookie , getCookie , getRequestHeader , setCookie } from 'h3'
4
6
import type { H3Event } from 'h3'
5
7
import destr from 'destr'
8
+ import { isEqual } from 'ohash'
6
9
import { useNuxtApp } from '../nuxt'
7
10
import { useRequestEvent } from './ssr'
8
11
@@ -12,31 +15,92 @@ export interface CookieOptions<T = any> extends _CookieOptions {
12
15
decode ?( value : string ) : T
13
16
encode ?( value : T ) : string
14
17
default ?: ( ) => T
18
+ watch ?: boolean | 'shallow'
19
+ readonly ?: boolean
15
20
}
16
21
17
22
export interface CookieRef < T > extends Ref < T > { }
18
23
19
- const CookieDefaults : CookieOptions < any > = {
24
+ const CookieDefaults = {
20
25
path : '/' ,
26
+ watch : true ,
21
27
decode : val => destr ( decodeURIComponent ( val ) ) ,
22
28
encode : val => encodeURIComponent ( typeof val === 'string' ? val : JSON . stringify ( val ) )
23
- }
29
+ } satisfies CookieOptions < any >
24
30
25
- export function useCookie < T = string > ( name : string , _opts ?: CookieOptions < T > ) : CookieRef < T > {
31
+ export function useCookie < T = string | null | undefined > ( name : string , _opts ?: CookieOptions < T > & { readonly ?: false } ) : CookieRef < T >
32
+ export function useCookie < T = string | null | undefined > ( name : string , _opts : CookieOptions < T > & { readonly : true } ) : Readonly < CookieRef < T > >
33
+ export function useCookie < T = string | null | undefined > ( name : string , _opts ?: CookieOptions < T > ) : CookieRef < T > {
26
34
const opts = { ...CookieDefaults , ..._opts }
27
- const cookies = readRawCookies ( opts )
35
+ const cookies = readRawCookies ( opts ) || { }
36
+
37
+ let delay : number | undefined
38
+
39
+ if ( opts . maxAge !== undefined ) {
40
+ delay = opts . maxAge * 1000 // convert to ms for setTimeout
41
+ } else if ( opts . expires ) {
42
+ // getTime() already returns time in ms
43
+ delay = opts . expires . getTime ( ) - Date . now ( )
44
+ }
28
45
29
- const cookie = ref ( cookies [ name ] ?? opts . default ?.( ) )
46
+ const hasExpired = delay !== undefined && delay <= 0
47
+ const cookieValue = hasExpired ? undefined : ( cookies [ name ] as any ) ?? opts . default ?.( )
48
+
49
+ // use a custom ref to expire the cookie on client side otherwise use basic ref
50
+ const cookie = process . client && delay && ! hasExpired
51
+ ? cookieRef < T | undefined > ( cookieValue , delay )
52
+ : ref < T | undefined > ( cookieValue )
53
+
54
+ if ( process . dev && hasExpired ) {
55
+ console . warn ( `[nuxt] not setting cookie \`${ name } \` as it has already expired.` )
56
+ }
30
57
31
58
if ( process . client ) {
32
- watch ( cookie , ( ) => { writeClientCookie ( name , cookie . value , opts as CookieSerializeOptions ) } )
59
+ const channel = typeof BroadcastChannel === 'undefined' ? null : new BroadcastChannel ( `nuxt:cookies:${ name } ` )
60
+ const callback = ( ) => {
61
+ if ( opts . readonly || isEqual ( cookie . value , cookies [ name ] ) ) { return }
62
+ writeClientCookie ( name , cookie . value , opts as CookieSerializeOptions )
63
+ channel ?. postMessage ( opts . encode ( cookie . value as T ) )
64
+ }
65
+
66
+ let watchPaused = false
67
+
68
+ if ( getCurrentScope ( ) ) {
69
+ onScopeDispose ( ( ) => {
70
+ watchPaused = true
71
+ callback ( )
72
+ channel ?. close ( )
73
+ } )
74
+ }
75
+
76
+ if ( channel ) {
77
+ channel . onmessage = ( event ) => {
78
+ watchPaused = true
79
+ cookies [ name ] = cookie . value = opts . decode ( event . data )
80
+ nextTick ( ( ) => { watchPaused = false } )
81
+ }
82
+ }
83
+
84
+ if ( opts . watch ) {
85
+ watch ( cookie , ( ) => {
86
+ if ( watchPaused ) { return }
87
+ callback ( )
88
+ } ,
89
+ { deep : opts . watch !== 'shallow' } )
90
+ } else {
91
+ callback ( )
92
+ }
33
93
} else if ( process . server ) {
34
- const initialValue = cookie . value
35
94
const nuxtApp = useNuxtApp ( )
36
- nuxtApp . hooks . hookOnce ( 'app:rendered' , ( ) => {
37
- if ( cookie . value !== initialValue ) {
38
- writeServerCookie ( useRequestEvent ( nuxtApp ) , name , cookie . value , opts )
39
- }
95
+ const writeFinalCookieValue = ( ) => {
96
+ if ( opts . readonly || isEqual ( cookie . value , cookies [ name ] ) ) { return }
97
+ writeServerCookie ( useRequestEvent ( nuxtApp ) , name , cookie . value , opts as CookieOptions < any > )
98
+ }
99
+
100
+ const unhook = nuxtApp . hooks . hookOnce ( 'app:rendered' , writeFinalCookieValue )
101
+ nuxtApp . hooks . hookOnce ( 'app:error' , ( ) => {
102
+ unhook ( ) // don't write cookie subsequently when app:rendered is called
103
+ return writeFinalCookieValue ( )
40
104
} )
41
105
}
42
106
@@ -45,7 +109,7 @@ export function useCookie<T = string> (name: string, _opts?: CookieOptions<T>):
45
109
46
110
function readRawCookies ( opts : CookieOptions = { } ) : Record < string , string > {
47
111
if ( process . server ) {
48
- return parse ( useRequestEvent ( ) ?. req . headers . cookie || '' , opts )
112
+ return parse ( getRequestHeader ( useRequestEvent ( ) , ' cookie' ) || '' , opts )
49
113
} else if ( process . client ) {
50
114
return parse ( document . cookie , opts )
51
115
}
@@ -66,7 +130,60 @@ function writeClientCookie (name: string, value: any, opts: CookieSerializeOptio
66
130
67
131
function writeServerCookie ( event : H3Event , name : string , value : any , opts : CookieSerializeOptions = { } ) {
68
132
if ( event ) {
69
- // TODO: Try to smart join with existing Set-Cookie headers
70
- appendHeader ( event , 'Set-Cookie' , serializeCookie ( name , value , opts ) )
133
+ // update if value is set
134
+ if ( value !== null && value !== undefined ) {
135
+ return setCookie ( event , name , value , opts )
136
+ }
137
+
138
+ // delete if cookie exists in browser and value is null/undefined
139
+ if ( getCookie ( event , name ) !== undefined ) {
140
+ return deleteCookie ( event , name , opts )
141
+ }
142
+
143
+ // else ignore if cookie doesn't exist in browser and value is null/undefined
144
+ }
145
+ }
146
+
147
+ /**
148
+ * The maximum value allowed on a timeout delay.
149
+ *
150
+ * Reference: https://developer.mozilla.org/en-US/docs/Web/API/setTimeout#maximum_delay_value
151
+ */
152
+ const MAX_TIMEOUT_DELAY = 2147483647
153
+
154
+ // custom ref that will update the value to undefined if the cookie expires
155
+ function cookieRef < T > ( value : T | undefined , delay : number ) {
156
+ let timeout : NodeJS . Timeout
157
+ let elapsed = 0
158
+ if ( getCurrentScope ( ) ) {
159
+ onScopeDispose ( ( ) => { clearTimeout ( timeout ) } )
71
160
}
161
+
162
+ return customRef ( ( track , trigger ) => {
163
+ function createExpirationTimeout ( ) {
164
+ clearTimeout ( timeout )
165
+ const timeRemaining = delay - elapsed
166
+ const timeoutLength = timeRemaining < MAX_TIMEOUT_DELAY ? timeRemaining : MAX_TIMEOUT_DELAY
167
+ timeout = setTimeout ( ( ) => {
168
+ elapsed += timeoutLength
169
+ if ( elapsed < delay ) { return createExpirationTimeout ( ) }
170
+
171
+ value = undefined
172
+ trigger ( )
173
+ } , timeoutLength )
174
+ }
175
+
176
+ return {
177
+ get ( ) {
178
+ track ( )
179
+ return value
180
+ } ,
181
+ set ( newValue ) {
182
+ createExpirationTimeout ( )
183
+
184
+ value = newValue
185
+ trigger ( )
186
+ }
187
+ }
188
+ } )
72
189
}
0 commit comments