@@ -32,6 +32,7 @@ async function setup({
3232 const timers = new Map ( ) ;
3333 const firstHydrates = [ ] ;
3434 const updates = [ ] ;
35+ const emits = [ ] ;
3536 const consoleErrors = [ ] ;
3637 let connectErrors = [ ] ;
3738 const errorUpdates = [ ] ;
@@ -47,6 +48,9 @@ async function setup({
4748 getItem ( key ) {
4849 return this . items . get ( key ) ?? null ;
4950 } ,
51+ setItem ( key , value ) {
52+ this . items . set ( key , String ( value ) ) ;
53+ } ,
5054 } ;
5155 const document = new EventTarget ( ) ;
5256 document . cookie = disabled ? "backend-enabled=false" : "" ;
@@ -107,6 +111,9 @@ async function setup({
107111 on ( name , handler ) {
108112 handlers . set ( name , handler ) ;
109113 } ,
114+ emit ( name , payload ) {
115+ emits . push ( payload ) ;
116+ } ,
110117 connect ( ) {
111118 // A ready transport can deliver these synchronously on namespace connect.
112119 for ( const name of [
@@ -226,6 +233,7 @@ async function setup({
226233 localStorage,
227234 socket,
228235 updates,
236+ emits,
229237 consoleErrors,
230238 get connectErrors ( ) {
231239 // Like React with pending work, apply queued updaters only on render.
@@ -238,13 +246,13 @@ async function setup({
238246 module,
239247 dispose,
240248 flush : ( ) => microtasks . splice ( 0 ) . forEach ( ( fn ) => fn ( ) ) ,
241- connect : ( transports = [ "websocket" ] ) =>
249+ connect : ( transports = [ "websocket" ] , storage = { } ) =>
242250 module . namespace . connect (
243251 socket ,
244252 { child : ( delta ) => updates . push ( delta . count ) } ,
245253 transports ,
246254 ( update ) => errorUpdates . push ( update ) ,
247- { } ,
255+ storage ,
248256 ( ) => { } ,
249257 { current : { } } ,
250258 ) ,
@@ -724,3 +732,162 @@ test("storage names that are Object.prototype properties sync like any other", a
724732 JSON . stringify ( { third : null } ) ,
725733 ) ;
726734} ) ;
735+
736+ test ( "a synced storage write syncs the writing tab's sibling vars" , async ( ) => {
737+ const clientStorage = {
738+ local_storage : {
739+ "child.first" : { name : "shared" , sync : true } ,
740+ "child.second" : { name : "shared" , sync : true } ,
741+ "child.third" : { name : "shared" , sync : true } ,
742+ "child.unsynced" : { name : "shared" } ,
743+ } ,
744+ } ;
745+ const app = await setup ( { clientStorage } ) ;
746+ app . connect ( [ "websocket" ] , clientStorage ) ;
747+ app . flush ( ) ;
748+ const sock = app . sockets [ 0 ] ;
749+ const syncs = ( ) =>
750+ app . emits . filter ( ( e ) => e ?. name ?. endsWith ?. ( "update_vars_internal" ) ) ;
751+
752+ // The backend applies a write to one synced var.
753+ sock . handlers . get ( "event" ) ( { delta : { child : { first : "from-cs0" } } } ) ;
754+ app . flush ( ) ;
755+ await new Promise ( ( resolve ) => setImmediate ( resolve ) ) ;
756+
757+ // The storage event only fires in other tabs, so the writing tab syncs its
758+ // own sibling vars sharing the name.
759+ assert . equal ( app . localStorage . items . get ( "shared" ) , "from-cs0" ) ;
760+ assert . equal ( syncs ( ) . length , 1 , JSON . stringify ( app . emits ) ) ;
761+ assert . equal (
762+ JSON . stringify ( syncs ( ) [ 0 ] . payload . vars ) ,
763+ JSON . stringify ( { "child.second" : "from-cs0" , "child.third" : "from-cs0" } ) ,
764+ ) ;
765+
766+ // The echoed sibling updates neither write again nor queue another sync.
767+ sock . handlers . get ( "event" ) ( {
768+ delta : { child : { second : "from-cs0" , third : "from-cs0" } } ,
769+ } ) ;
770+ app . flush ( ) ;
771+ await new Promise ( ( resolve ) => setImmediate ( resolve ) ) ;
772+ assert . equal ( syncs ( ) . length , 1 , JSON . stringify ( app . emits ) ) ;
773+ } ) ;
774+
775+ test ( "an unsynced write still syncs its synced siblings in the writing tab" , async ( ) => {
776+ const clientStorage = {
777+ local_storage : {
778+ "child.first" : { name : "shared" , sync : true } ,
779+ "child.second" : { name : "shared" } ,
780+ } ,
781+ } ;
782+ const app = await setup ( { clientStorage } ) ;
783+ app . connect ( [ "websocket" ] , clientStorage ) ;
784+ app . flush ( ) ;
785+ const sock = app . sockets [ 0 ] ;
786+ const syncs = ( ) =>
787+ app . emits . filter ( ( e ) => e ?. name ?. endsWith ?. ( "update_vars_internal" ) ) ;
788+
789+ // Storage writes raise the cross-tab event whether or not the written var
790+ // syncs, so the writing tab syncs its synced siblings either way.
791+ sock . handlers . get ( "event" ) ( { delta : { child : { second : "plain" } } } ) ;
792+ app . flush ( ) ;
793+ await new Promise ( ( resolve ) => setImmediate ( resolve ) ) ;
794+
795+ assert . equal ( app . localStorage . items . get ( "shared" ) , "plain" ) ;
796+ assert . equal ( syncs ( ) . length , 1 , JSON . stringify ( app . emits ) ) ;
797+ assert . equal (
798+ JSON . stringify ( syncs ( ) [ 0 ] . payload . vars ) ,
799+ JSON . stringify ( { "child.first" : "plain" } ) ,
800+ ) ;
801+ } ) ;
802+
803+ test ( "two same-name vars in one delta queue one sync per storage name" , async ( ) => {
804+ const clientStorage = {
805+ local_storage : {
806+ "child.first" : { name : "shared" , sync : true } ,
807+ "child.second" : { name : "shared" , sync : true } ,
808+ "child.third" : { name : "other" , sync : true } ,
809+ "child.fourth" : { name : "other" , sync : true } ,
810+ } ,
811+ } ;
812+ const app = await setup ( { clientStorage } ) ;
813+ app . connect ( [ "websocket" ] , clientStorage ) ;
814+ app . flush ( ) ;
815+ const sock = app . sockets [ 0 ] ;
816+ const syncs = ( ) =>
817+ app . emits . filter ( ( e ) => e ?. name ?. endsWith ?. ( "update_vars_internal" ) ) ;
818+
819+ sock . handlers . get ( "event" ) ( {
820+ delta : { child : { first : "one" , second : "two" , third : "three" } } ,
821+ } ) ;
822+ app . flush ( ) ;
823+ await new Promise ( ( resolve ) => setImmediate ( resolve ) ) ;
824+
825+ // One sync per storage name, reading the name's latest value when sent.
826+ assert . equal ( syncs ( ) . length , 2 , JSON . stringify ( app . emits ) ) ;
827+ const byVars = syncs ( ) . map ( ( e ) => JSON . stringify ( e . payload . vars ) ) ;
828+ assert . ok ( byVars . includes ( JSON . stringify ( { "child.second" : "two" } ) ) ) ;
829+ assert . ok ( byVars . includes ( JSON . stringify ( { "child.fourth" : "three" } ) ) ) ;
830+ } ) ;
831+
832+ test ( "an unhydrated delta writes and syncs no client storage" , async ( ) => {
833+ const clientStorage = {
834+ local_storage : {
835+ "child.first" : { name : "shared" , sync : true } ,
836+ "child.second" : { name : "shared" , sync : true } ,
837+ } ,
838+ } ;
839+ const app = await setup ( { clientStorage } ) ;
840+ app . connect ( [ "websocket" ] , clientStorage ) ;
841+ app . flush ( ) ;
842+ const sock = app . sockets [ 0 ] ;
843+
844+ sock . handlers . get ( "event" ) ( {
845+ delta : { child : { is_hydrated_rx_state_ : false , first : "early" } } ,
846+ } ) ;
847+ app . flush ( ) ;
848+ await new Promise ( ( resolve ) => setImmediate ( resolve ) ) ;
849+
850+ assert . equal ( app . localStorage . items . get ( "shared" ) , undefined ) ;
851+ assert . equal (
852+ app . emits . filter ( ( e ) => e ?. name ?. endsWith ?. ( "update_vars_internal" ) ) . length ,
853+ 0 ,
854+ JSON . stringify ( app . emits ) ,
855+ ) ;
856+ } ) ;
857+
858+ test ( "a newer backend value re-syncs once, then the echo terminates it" , async ( ) => {
859+ const clientStorage = {
860+ local_storage : {
861+ "child.first" : { name : "shared" , sync : true } ,
862+ "child.second" : { name : "shared" , sync : true } ,
863+ } ,
864+ } ;
865+ const app = await setup ( { clientStorage } ) ;
866+ app . connect ( [ "websocket" ] , clientStorage ) ;
867+ app . flush ( ) ;
868+ const sock = app . sockets [ 0 ] ;
869+ const syncs = ( ) =>
870+ app . emits . filter ( ( e ) => e ?. name ?. endsWith ?. ( "update_vars_internal" ) ) ;
871+ const deliver = async ( delta ) => {
872+ sock . handlers . get ( "event" ) ( { delta } ) ;
873+ app . flush ( ) ;
874+ await new Promise ( ( resolve ) => setImmediate ( resolve ) ) ;
875+ } ;
876+
877+ await deliver ( { child : { first : "v1" } } ) ;
878+ assert . equal ( syncs ( ) . length , 1 , JSON . stringify ( app . emits ) ) ;
879+
880+ // A backend value that matches no sent value overrides storage and syncs
881+ // the siblings once more.
882+ await deliver ( { child : { second : "v2" } } ) ;
883+ assert . equal ( app . localStorage . items . get ( "shared" ) , "v2" ) ;
884+ assert . equal ( syncs ( ) . length , 2 , JSON . stringify ( app . emits ) ) ;
885+ assert . equal (
886+ JSON . stringify ( syncs ( ) [ 1 ] . payload . vars ) ,
887+ JSON . stringify ( { "child.first" : "v2" } ) ,
888+ ) ;
889+
890+ // That update's echo is recognized: no write, no third sync. No ping-pong.
891+ await deliver ( { child : { first : "v2" } } ) ;
892+ assert . equal ( syncs ( ) . length , 2 , JSON . stringify ( app . emits ) ) ;
893+ } ) ;
0 commit comments