Skip to content

Commit

Permalink
Specify whether an observer listen to broadcast
Browse files Browse the repository at this point in the history
  • Loading branch information
maxence-charriere committed Mar 21, 2024
1 parent 3aa435a commit 163a0bf
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 35 deletions.
1 change: 0 additions & 1 deletion pkg/app/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,6 @@ func (e *engineX) initBrowser() {
return
}
e.browser.HandleEvents(e.baseContext(), e.notifyComponentEvent)
e.states.InitBroadcast(e.baseContext())
}

func (e *engineX) notifyComponentEvent(event any) {
Expand Down
80 changes: 55 additions & 25 deletions pkg/app/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ func (s State) PersistWithEncryption() State {

// Broadcast signals that changes to the state will be broadcasted to other
// browser tabs and windows sharing the same origin when it is supported.
//
// Using Broadcast creates a BroadcastChannel, which prevents the page from
// being cached. This may impact the Chrome Lighthouse performance score due to
// the additional resources required to manage the broadcast channel.
func (s State) Broadcast() State {
return s.broadcast(s)
}
Expand All @@ -63,9 +67,11 @@ type Observer struct {
receiver any
condition func() bool
changeHandler func()
broadcast bool

state string
setObserver func(Observer) Observer
state string
setObserver func(Observer) Observer
enableBroadcast func()
}

// While sets a condition for the observer, determining whether it observes
Expand All @@ -83,6 +89,21 @@ func (o Observer) OnChange(h func()) Observer {
return o.setObserver(o)
}

// WithBroadcast enables the observer to listen to state changes that are
// broadcasted by other browser tabs or windows. This is useful for s
// ynchronizing state across multiple open instances of a web application within
// the same browser.
//
// Calling WithBroadcast creates a BroadcastChannel, which prevents the page
// from being cached. This may impact the Chrome Lighthouse performance
// score due to the additional resources required to manage the broadcast
// channel.
func (o Observer) WithBroadcast() Observer {
o.enableBroadcast()
o.broadcast = true
return o.setObserver(o)
}

func (o Observer) observing() bool {
if o.source == nil || !o.source.Mounted() {
return false
Expand All @@ -97,23 +118,26 @@ func (o Observer) observing() bool {
// to state values. It supports concurrency-safe operations and provides
// functionality to observe state changes.
type stateManager struct {
mutex sync.RWMutex
states map[string]State
observers map[string]map[UI]Observer
broadcastStoreID string
broadcastChannel Value
mutex sync.RWMutex
states map[string]State
observers map[string]map[UI]Observer
initBroadcastOnce sync.Once
broadcastStoreID string
broadcastChannel Value
}

// Observe initiates observation for a specified state, ensuring the state
// is fetched and set into the given receiver. The returned observer object
// offers methods for advanced observation configurations.
func (m *stateManager) Observe(ctx Context, state string, receiver any) Observer {
m.Get(ctx, state, receiver)

return m.setObserver(Observer{
source: ctx.Src(),
receiver: receiver,
state: state,
setObserver: m.setObserver,
source: ctx.Src(),
receiver: receiver,
state: state,
setObserver: m.setObserver,
enableBroadcast: func() { m.initBroadcast(ctx) },
})
}

Expand All @@ -135,6 +159,7 @@ func (m *stateManager) setObserver(v Observer) Observer {
receiver: v.receiver,
condition: v.condition,
changeHandler: v.changeHandler,
broadcast: v.broadcast,
}

return v
Expand Down Expand Up @@ -296,6 +321,8 @@ func (m *stateManager) broadcast(s State) State {
m.mutex.Lock()
defer m.mutex.Unlock()

m.initBroadcast(s.ctx)

if m.broadcastChannel == nil {
Log(errors.New("broadcast not supported").
WithTag("state", s.name))
Expand All @@ -318,22 +345,22 @@ func (m *stateManager) broadcast(s State) State {
return s
}

// InitBroadcast initializes a broadcast channel to share state changes
// across browser tabs or windows.
func (m *stateManager) InitBroadcast(ctx Context) {
broadcastChannel := Window().Get("BroadcastChannel")
if !broadcastChannel.Truthy() {
return
}
broadcastChannel = broadcastChannel.New("go-app-broadcast-states")
m.broadcastChannel = broadcastChannel
m.broadcastStoreID = uuid.NewString()
func (m *stateManager) initBroadcast(ctx Context) {
m.initBroadcastOnce.Do(func() {
broadcastChannel := Window().Get("BroadcastChannel")
if !broadcastChannel.Truthy() {
return
}
broadcastChannel = broadcastChannel.New("go-app-broadcast-states")
m.broadcastChannel = broadcastChannel
m.broadcastStoreID = uuid.NewString()

handleBroadcast := FuncOf(func(this Value, args []Value) any {
m.handleBroadcast(ctx, args[0].Get("data"))
return nil
handleBroadcast := FuncOf(func(this Value, args []Value) any {
m.handleBroadcast(ctx, args[0].Get("data"))
return nil
})
broadcastChannel.Set("onmessage", handleBroadcast)
})
broadcastChannel.Set("onmessage", handleBroadcast)
}

func (m *stateManager) handleBroadcast(ctx Context, data Value) {
Expand All @@ -349,6 +376,9 @@ func (m *stateManager) handleBroadcast(ctx Context, data Value) {

for _, observer := range m.observers[state] {
o := observer
if !o.broadcast {
continue
}

ctx.sourceElement = o.source
ctx.Dispatch(func(ctx Context) {
Expand Down
31 changes: 22 additions & 9 deletions pkg/app/state_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -597,8 +597,6 @@ func TestStateManagerSet(t *testing.T) {
var m stateManager
ctx := makeTestContext()

e := newTestEngine()
m.InitBroadcast(e.baseContext())
state := m.Set(ctx, stateName, 42).Broadcast()
require.Equal(t, 42, state.value)
require.NotNil(t, state.ctx)
Expand Down Expand Up @@ -626,17 +624,30 @@ func TestStateManagerSet(t *testing.T) {

m1 := newTestEngine()
m2 := newTestEngine()
m3 := newTestEngine()

compo3 := &hello{}
err := m3.Load(compo3)
require.NoError(t, err)
ctx3 := m2.nodes.context(m3.baseContext(), compo3)
var value3 int
broadcasted3 := false
ctx3.ObserveState(stateName, &value3).
OnChange(func() {
broadcasted3 = true
})

compo2 := &hello{}
err := m2.Load(compo2)
err = m2.Load(compo2)
require.NoError(t, err)
ctx2 := m2.nodes.context(m2.baseContext(), compo2)
var value2 int

broadcasted := false
ctx2.ObserveState(stateName, &value2).OnChange(func() {
broadcasted = true
})
broadcasted2 := false
ctx2.ObserveState(stateName, &value2).
WithBroadcast().
OnChange(func() {
broadcasted2 = true
})

compo1 := &hello{}
err = m1.Load(compo1)
Expand All @@ -645,12 +656,14 @@ func TestStateManagerSet(t *testing.T) {
ctx1.SetState(stateName, 42).Broadcast()
m1.ConsumeAll()

for !broadcasted {
for !broadcasted2 {
m2.ConsumeAll()
time.Sleep(time.Millisecond * 5)
}

require.Equal(t, 42, value2)
require.Zero(t, value3)
require.False(t, broadcasted3)
})
}

Expand Down

0 comments on commit 163a0bf

Please sign in to comment.