11/**
22 * @license
3- * Copyright 2019 Google LLC
3+ * Copyright 2025 Google LLC
44 *
55 * Licensed under the Apache License, Version 2.0 (the "License");
66 * you may not use this file except in compliance with the License.
1616 */
1717
1818import { Persistence } from '../../model/public_types' ;
19+ import type { CookieChangeEvent } from 'cookie-store' ;
20+
21+ const POLLING_INTERVAL_MS = 1_000 ;
1922
2023import {
2124 PersistenceInternal ,
@@ -24,58 +27,116 @@ import {
2427 StorageEventListener
2528} from '../../core/persistence' ;
2629
30+ const getDocumentCookie = ( name : string ) : string | null => {
31+ const escapedName = name . replace ( / [ \\ ^ $ . * + ? ( ) [ \] { } | ] / g, '\\$&' ) ;
32+ const matcher = RegExp ( `${ escapedName } =([^;]+)` ) ;
33+ return document . cookie . match ( matcher ) ?. [ 1 ] ?? null ;
34+ } ;
35+
2736export class CookiePersistence implements PersistenceInternal {
2837 static type : 'COOKIE' = 'COOKIE' ;
2938 readonly type = PersistenceType . COOKIE ;
30- listeners : Map < StorageEventListener , ( e : any ) => void > = new Map ( ) ;
39+ cookieStoreListeners : Map <
40+ StorageEventListener ,
41+ ( event : CookieChangeEvent ) => void
42+ > = new Map ( ) ;
43+ cookiePollingIntervals : Map < StorageEventListener , NodeJS . Timeout > = new Map ( ) ;
3144
3245 async _isAvailable ( ) : Promise < boolean > {
33- return navigator . hasOwnProperty ( 'cookieEnabled' ) ?
34- navigator . cookieEnabled :
35- true ;
46+ if ( typeof navigator === 'undefined' || typeof document === 'undefined' ) {
47+ return false ;
48+ }
49+ return navigator . cookieEnabled ?? true ;
3650 }
3751
3852 async _set ( _key : string , _value : PersistenceValue ) : Promise < void > {
3953 return ;
4054 }
4155
4256 async _get < T extends PersistenceValue > ( key : string ) : Promise < T | null > {
43- const cookie = await ( window as any ) . cookieStore . get ( key ) ;
44- return cookie ?. value ;
57+ if ( ! this . _isAvailable ( ) ) {
58+ return null ;
59+ }
60+ if ( window . cookieStore ) {
61+ const cookie = await window . cookieStore . get ( key ) ;
62+ return cookie ?. value as T ;
63+ } else {
64+ return getDocumentCookie ( key ) as T ;
65+ }
4566 }
4667
4768 async _remove ( key : string ) : Promise < void > {
48- const cookie = await ( window as any ) . cookieStore . get ( key ) ;
49- if ( ! cookie ) {
69+ if ( ! this . _isAvailable ( ) ) {
5070 return ;
5171 }
52- await ( window as any ) . cookieStore . set ( { ...cookie , value : "" } ) ;
72+ if ( window . cookieStore ) {
73+ const cookie = await window . cookieStore . get ( key ) ;
74+ if ( ! cookie ) {
75+ return ;
76+ }
77+ await window . cookieStore . delete ( cookie ) ;
78+ } else {
79+ // TODO how do I get the cookie properties?
80+ document . cookie = `${ key } =;Max-Age=34560000;Partitioned;Secure;SameSite=Strict;Path=/` ;
81+ }
5382 await fetch ( `/__cookies__` , { method : 'DELETE' } ) . catch ( ( ) => undefined ) ;
5483 }
5584
56- _addListener ( _key : string , _listener : StorageEventListener ) : void {
57- // TODO fallback to polling if cookieStore is not available
58- const cb = ( event : any ) => {
59- const cookie = event . changed . find ( ( change : any ) => change . name === _key ) ;
60- if ( cookie ) {
61- _listener ( cookie . value ) ;
62- }
63- } ;
64- this . listeners . set ( _listener , cb ) ;
65- ( window as any ) . cookieStore . addEventListener ( 'change' , cb ) ;
85+ _addListener ( key : string , listener : StorageEventListener ) : void {
86+ if ( ! this . _isAvailable ( ) ) {
87+ return ;
88+ }
89+ if ( window . cookieStore ) {
90+ const cb = ( event : CookieChangeEvent ) : void => {
91+ const changedCookie = event . changed . find ( change => change . name === key ) ;
92+ if ( changedCookie ) {
93+ listener ( changedCookie . value as PersistenceValue ) ;
94+ }
95+ const deletedCookie = event . deleted . find ( change => change . name === key ) ;
96+ if ( deletedCookie ) {
97+ listener ( null ) ;
98+ }
99+ } ;
100+ this . cookieStoreListeners . set ( listener , cb ) ;
101+ window . cookieStore . addEventListener ( 'change' , cb as EventListener ) ;
102+ } else {
103+ let lastValue = getDocumentCookie ( key ) ;
104+ const interval = setInterval ( ( ) => {
105+ const currentValue = getDocumentCookie ( key ) ;
106+ if ( currentValue !== lastValue ) {
107+ listener ( currentValue as PersistenceValue | null ) ;
108+ lastValue = currentValue ;
109+ }
110+ } , POLLING_INTERVAL_MS ) ;
111+ this . cookiePollingIntervals . set ( listener , interval ) ;
112+ }
66113 }
67114
68- _removeListener ( _key : string , _listener : StorageEventListener ) : void {
69- const cb = this . listeners . get ( _listener ) ;
70- if ( ! cb ) {
115+ // TODO can we tidy this logic up into a single unsubscribe function? () => void;
116+ _removeListener ( _key : string , listener : StorageEventListener ) : void {
117+ if ( ! this . _isAvailable ( ) ) {
71118 return ;
72119 }
73- ( window as any ) . cookieStore . removeEventListener ( 'change' , cb ) ;
120+ if ( window . cookieStore ) {
121+ const cb = this . cookieStoreListeners . get ( listener ) ;
122+ if ( ! cb ) {
123+ return ;
124+ }
125+ window . cookieStore . removeEventListener ( 'change' , cb as EventListener ) ;
126+ this . cookieStoreListeners . delete ( listener ) ;
127+ } else {
128+ const interval = this . cookiePollingIntervals . get ( listener ) ;
129+ if ( ! interval ) {
130+ return ;
131+ }
132+ clearInterval ( interval ) ;
133+ this . cookiePollingIntervals . delete ( listener ) ;
134+ }
74135 }
75136}
76137
77138/**
78- * An implementation of {@link Persistence} of type 'NONE '.
139+ * An implementation of {@link Persistence} of type 'COOKIE '.
79140 *
80141 * @public
81142 */
0 commit comments