@@ -18,6 +18,7 @@ async function setup({
1818 disabled = false ,
1919 hidden = false ,
2020 storedToken = "original-token" ,
21+ clientStorage = { } ,
2122 react = { useCallback ( ) { } , useEffect ( ) { } , useRef ( ) { } , useState ( ) { } } ,
2223} = { } ) {
2324 const sockets = [ ] ;
@@ -32,6 +33,12 @@ async function setup({
3233 getItem : ( key ) => storage . get ( key ) ,
3334 setItem : ( key , value ) => storage . set ( key , value ) ,
3435 } ;
36+ const localStorage = {
37+ items : new Map ( ) ,
38+ getItem ( key ) {
39+ return this . items . get ( key ) ?? null ;
40+ } ,
41+ } ;
3542 const document = new EventTarget ( ) ;
3643 document . cookie = disabled ? "backend-enabled=false" : "" ;
3744 document . visibilityState = hidden ? "hidden" : "visible" ;
@@ -41,6 +48,7 @@ async function setup({
4148 URL ,
4249 URLSearchParams,
4350 performance,
51+ localStorage,
4452 ...( browser ? { window, document } : { } ) ,
4553 queueMicrotask : ( fn ) => microtasks . push ( fn ) ,
4654 setTimeout : ( fn , ms ) => {
@@ -127,6 +135,7 @@ async function setup({
127135 initialState : stateful ? { root : { } , child : { } } : { } ,
128136 onLoadInternalEvent ( ) { } ,
129137 state_name : "root" ,
138+ clientStorage,
130139 exception_state_name : "exception" ,
131140 } ,
132141 eventLoop : { addEvents ( ) { } , connectErrors : [ ] } ,
@@ -164,6 +173,7 @@ async function setup({
164173 timers,
165174 firstHydrates,
166175 window,
176+ localStorage,
167177 socket,
168178 updates,
169179 module,
@@ -314,7 +324,7 @@ for (const config of [
314324}
315325
316326/** Run `useEventLoop` once with minimal hooks, keeping the effects it registers. */
317- async function mountEventLoop ( ) {
327+ async function mountEventLoop ( clientStorage = { } ) {
318328 const effects = [ ] ;
319329 const sent = [ ] ;
320330 const emitted = {
@@ -323,6 +333,7 @@ async function mountEventLoop() {
323333 } ;
324334 let refs = 0 ;
325335 const harness = await setup ( {
336+ clientStorage,
326337 react : {
327338 useCallback : ( fn ) => fn ,
328339 useEffect : ( fn ) => effects . push ( fn ) ,
@@ -334,7 +345,7 @@ async function mountEventLoop() {
334345 harness . module . namespace . useEventLoop (
335346 ( ) => { } ,
336347 ( ) => [ ] ,
337- { } ,
348+ clientStorage ,
338349 ) ;
339350 return { harness, effects, sent } ;
340351}
@@ -393,3 +404,66 @@ test("window.onerror preserves the original Error message", async () => {
393404 ) ;
394405 assert . equal ( event . payload . component_stack , "" ) ;
395406} ) ;
407+
408+ test ( "a storage event syncs every synced var that shares the storage name" , async ( ) => {
409+ const clientStorage = {
410+ local_storage : {
411+ first : { name : "shared" , sync : true } ,
412+ second : { name : "shared" , sync : true } ,
413+ other : { name : "other" , sync : true } ,
414+ unsynced : { name : "shared" } ,
415+ } ,
416+ } ;
417+ const { harness, effects, sent } = await mountEventLoop ( clientStorage ) ;
418+ for ( const effect of effects ) effect ( ) ;
419+ harness . localStorage . items . set ( "shared" , "stored" ) ;
420+
421+ const event = new Event ( "storage" ) ;
422+ event . key = "shared" ;
423+ event . storageArea = harness . localStorage ;
424+ harness . window . dispatchEvent ( event ) ;
425+ await new Promise ( ( resolve ) => setImmediate ( resolve ) ) ;
426+
427+ const updates = sent . filter ( ( e ) => e . name . endsWith ( "update_vars_internal" ) ) ;
428+ assert . equal ( updates . length , 1 , JSON . stringify ( sent ) ) ;
429+ // The vars come from the vm's realm, so compare their serialised form.
430+ assert . equal (
431+ JSON . stringify ( updates [ 0 ] . payload . vars ) ,
432+ JSON . stringify ( { first : "stored" , second : "stored" } ) ,
433+ ) ;
434+ } ) ;
435+
436+ test ( "storage names that are Object.prototype properties sync like any other" , async ( ) => {
437+ const clientStorage = {
438+ local_storage : {
439+ first : { name : "constructor" , sync : true } ,
440+ second : { name : "constructor" , sync : true } ,
441+ third : { name : "__proto__" , sync : true } ,
442+ } ,
443+ } ;
444+ const { harness, effects, sent } = await mountEventLoop ( clientStorage ) ;
445+ assert . doesNotThrow ( ( ) => {
446+ for ( const effect of effects ) effect ( ) ;
447+ } ) ;
448+ harness . localStorage . items . set ( "constructor" , "stored" ) ;
449+
450+ for ( const key of [ "constructor" , "toString" , "__proto__" ] ) {
451+ const event = new Event ( "storage" ) ;
452+ event . key = key ;
453+ event . storageArea = harness . localStorage ;
454+ harness . window . dispatchEvent ( event ) ;
455+ }
456+ await new Promise ( ( resolve ) => setImmediate ( resolve ) ) ;
457+
458+ // Only the names a var declared are synced, and a name's vars all are.
459+ const updates = sent . filter ( ( e ) => e . name . endsWith ( "update_vars_internal" ) ) ;
460+ assert . equal ( updates . length , 2 , JSON . stringify ( sent ) ) ;
461+ assert . equal (
462+ JSON . stringify ( updates [ 0 ] . payload . vars ) ,
463+ JSON . stringify ( { first : "stored" , second : "stored" } ) ,
464+ ) ;
465+ assert . equal (
466+ JSON . stringify ( updates [ 1 ] . payload . vars ) ,
467+ JSON . stringify ( { third : null } ) ,
468+ ) ;
469+ } ) ;
0 commit comments