| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501 |
- package app
- import (
- "encoding/json"
- "reflect"
- "sync"
- "time"
- "github.com/google/uuid"
- "github.com/maxence-charriere/go-app/v9/pkg/errors"
- )
- // Observer is an observer that observes changes for a given state.
- type Observer interface {
- // Defines a condition that reports whether the observer keeps observing the
- // associated state. Multiple conditions can be defined by successively
- // calling While().
- While(condition func() bool) Observer
- // Executes the given function on the UI goroutine when the observed value
- // changes. Multiple functions can be executed by successively calling
- // OnChange().
- OnChange(fn func()) Observer
- // Stores the value associated with the observed state into the given
- // receiver. Panics when the receiver is not a pointer or nil.
- //
- // The receiver is updated each time the associated state changes. It is
- // unchanged when its pointed value has a different type than the associated
- // state value.
- Value(recv any)
- }
- // A state represents an observable value available across the app.
- type State struct {
- // Reports whether the state is persisted in local storage.
- IsPersistent bool
- // Reports whether the state is encrypted before being persisted in local
- // storage.
- IsEncrypted bool
- // The time when the state expires. The state never expires when zero value.
- ExpiresAt time.Time
- // Reports whether a state is broadcasted to other browser tabs and windows.
- IsBroadcasted bool
- value any
- observers map[*observer]struct{}
- }
- func (s *State) isExpired(now time.Time) bool {
- return s.ExpiresAt != time.Time{} && now.After(s.ExpiresAt)
- }
- // StateOption represents an option applied when a state is set.
- type StateOption func(*State)
- // Persist is a state option that persists a state in local storage.
- //
- // Be mindful to not use this option as a cache since local storage is limited
- // to 5MB in a lot of web browsers.
- func Persist(s *State) {
- s.IsPersistent = true
- }
- // Encrypt is a state option that encrypts a state before persisting it in local
- // storage. Encryption is performed only when the Persist option is also set.
- func Encrypt(s *State) {
- s.IsEncrypted = true
- }
- // ExpiresIn returns a state option that sets a state value to its zero value
- // after the given duration.
- //
- // Values persisted to local storage with the Persist option are removed from
- // it.
- func ExpiresIn(d time.Duration) StateOption {
- return ExpiresAt(time.Now().Add(d))
- }
- // ExpiresAt returns a state option that sets a state value to its zero value at
- // the given time.
- //
- // Values persisted to local storage with the Persist option are removed from
- // it.
- func ExpiresAt(t time.Time) StateOption {
- return func(s *State) {
- s.ExpiresAt = t
- }
- }
- // Broadcast is a state option that broadcasts a state to other browser tabs and
- // windows from the same origin.
- func Broadcast(s *State) {
- s.IsBroadcasted = true
- }
- type observer struct {
- element UI
- subscribe func(*observer)
- conditions []func() bool
- onChanges []func()
- receiver any
- }
- func newObserver(elem UI, subscribe func(*observer)) *observer {
- return &observer{
- element: elem,
- subscribe: subscribe,
- }
- }
- func (o *observer) While(fn func() bool) Observer {
- o.conditions = append(o.conditions, fn)
- return o
- }
- func (o *observer) OnChange(fn func()) Observer {
- o.onChanges = append(o.onChanges, fn)
- return o
- }
- func (o *observer) Value(recv any) {
- if reflect.ValueOf(recv).Kind() != reflect.Ptr {
- panic(errors.New("observer value receiver is not a pointer"))
- }
- o.receiver = recv
- o.subscribe(o)
- }
- func (o *observer) isObserving() bool {
- if !o.element.Mounted() {
- return false
- }
- for _, c := range o.conditions {
- if !c() {
- return false
- }
- }
- return true
- }
- type store struct {
- mutex sync.Mutex
- id string
- states map[string]State
- disp Dispatcher
- broadcastChannel Value
- onBroadcastClose func()
- }
- func newStore(d Dispatcher) *store {
- s := &store{
- id: uuid.NewString(),
- states: make(map[string]State),
- disp: d,
- }
- s.initBroadcast()
- return s
- }
- func (s *store) Set(key string, v any, opts ...StateOption) {
- s.mutex.Lock()
- defer s.mutex.Unlock()
- state := s.states[key]
- state.value = v
- for _, o := range opts {
- o(&state)
- }
- s.states[key] = state
- if state.IsPersistent {
- if err := s.setPersistent(key, state.IsEncrypted, state.ExpiresAt, v); err != nil {
- Log(errors.New("persisting state failed").
- Tag("state", key).
- Wrap(err))
- return
- }
- }
- if state.isExpired(time.Now()) {
- state = s.expire(key, state)
- s.states[key] = state
- return
- }
- if state.IsBroadcasted {
- if err := s.broadcast(key, v); err != nil {
- Log(errors.New("broadcasting state failed").
- Tag("state", key).
- Wrap(err))
- return
- }
- }
- for obs := range state.observers {
- o := obs
- if !o.element.Mounted() {
- delete(state.observers, o)
- continue
- }
- s.disp.Dispatch(Dispatch{
- Mode: Update,
- Source: o.element,
- Function: func(ctx Context) {
- if !o.isObserving() {
- s.mutex.Lock()
- delete(state.observers, o)
- s.mutex.Unlock()
- return
- }
- if err := storeValue(o.receiver, v); err != nil {
- Log(errors.New("notifying observer failed").
- Tag("state", key).
- Tag("element", reflect.TypeOf(o.element)).
- Wrap(err))
- return
- }
- for _, fn := range o.onChanges {
- fn()
- }
- },
- })
- }
- }
- func (s *store) Get(key string, recv any) {
- s.mutex.Lock()
- defer s.mutex.Unlock()
- var err error
- state := s.states[key]
- if state.isExpired(time.Now()) {
- state = s.expire(key, state)
- s.states[key] = state
- }
- if state.value != nil {
- err = storeValue(recv, state.value)
- } else {
- err = s.getPersistent(key, recv)
- }
- if err != nil {
- Log(errors.New("getting state value failed").
- Tag("state", key).
- Wrap(err))
- }
- }
- func (s *store) Del(key string) {
- s.mutex.Lock()
- defer s.mutex.Unlock()
- delete(s.states, key)
- s.disp.getLocalStorage().Del(key)
- }
- func (s *store) Observe(key string, elem UI) Observer {
- return newObserver(elem, func(o *observer) {
- s.mutex.Lock()
- defer s.mutex.Unlock()
- if err := s.subscribe(key, o); err != nil {
- Log(errors.New("notifying observer failed").
- Tag("state", key).
- Tag("element", reflect.TypeOf(elem)).
- Wrap(err))
- }
- })
- }
- func (s *store) Cleanup() {
- s.mutex.Lock()
- defer s.mutex.Unlock()
- s.removeUnusedObservers()
- s.expireExpiredValues()
- }
- func (s *store) Close() {
- if s.broadcastChannel != nil {
- s.broadcastChannel.Call("close")
- s.broadcastChannel = nil
- }
- s.onBroadcastClose()
- }
- func (s *store) subscribe(key string, o *observer) error {
- state := s.states[key]
- if state.observers == nil {
- state.observers = make(map[*observer]struct{})
- }
- state.observers[o] = struct{}{}
- if state.isExpired(time.Now()) {
- state = s.expire(key, state)
- }
- s.states[key] = state
- if state.value != nil {
- return storeValue(o.receiver, state.value)
- }
- return s.getPersistent(key, o.receiver)
- }
- func (s *store) removeUnusedObservers() {
- for _, state := range s.states {
- for o := range state.observers {
- if !o.isObserving() {
- delete(state.observers, o)
- }
- }
- }
- }
- func (s *store) getPersistent(key string, recv any) error {
- var state persistentState
- s.disp.getLocalStorage().Get(key, &state)
- if state.EncryptedValue == nil && state.Value == nil && state.ExpiresAt == (time.Time{}) {
- return nil
- }
- if state.isExpired(time.Now()) {
- s.disp.getLocalStorage().Del(key)
- return nil
- }
- if len(state.EncryptedValue) == 0 {
- return json.Unmarshal(state.Value, recv)
- }
- return s.disp.Context().Decrypt(state.EncryptedValue, recv)
- }
- func (s *store) setPersistent(key string, encrypt bool, expiresAt time.Time, v any) error {
- var err error
- state := persistentState{
- ExpiresAt: expiresAt,
- }
- if encrypt {
- state.EncryptedValue, err = s.disp.Context().Encrypt(v)
- } else {
- state.Value, err = json.Marshal(v)
- }
- if err != nil {
- return err
- }
- return s.disp.getLocalStorage().Set(key, state)
- }
- func (s *store) expireExpiredValues() {
- now := time.Now()
- for k, state := range s.states {
- if state.isExpired(now) {
- state = s.expire(k, state)
- s.states[k] = state
- }
- }
- }
- func (s *store) expire(key string, state State) State {
- s.disp.getLocalStorage().Del(key)
- state.value = nil
- return state
- }
- func (s *store) initBroadcast() {
- broadcastChannel := Window().Get("BroadcastChannel")
- if !broadcastChannel.Truthy() {
- s.onBroadcastClose = func() {}
- return
- }
- broadcastChannel = broadcastChannel.New("go-app-broadcast-states")
- s.broadcastChannel = broadcastChannel
- onBroadcast := FuncOf(func(this Value, args []Value) any {
- s.onBroadcast(args[0].Get("data"))
- return nil
- })
- s.onBroadcastClose = onBroadcast.Release
- broadcastChannel.Set("onmessage", onBroadcast)
- }
- func (s *store) broadcast(key string, v any) error {
- b, err := json.Marshal(v)
- if err != nil {
- return err
- }
- if s.broadcastChannel != nil {
- s.broadcastChannel.Call("postMessage", map[string]any{
- "StoreID": s.id,
- "State": key,
- "Value": string(b),
- })
- }
- return nil
- }
- func (s *store) onBroadcast(event Value) {
- if storeID := event.Get("StoreID").String(); storeID == "" || storeID == s.id {
- return
- }
- s.mutex.Lock()
- defer s.mutex.Unlock()
- key := event.Get("State").String()
- v := []byte(event.Get("Value").String())
- state := s.states[key]
- for obs := range state.observers {
- o := obs
- if !o.element.Mounted() {
- delete(state.observers, o)
- continue
- }
- s.disp.Dispatch(Dispatch{
- Mode: Update,
- Source: o.element,
- Function: func(ctx Context) {
- if !o.isObserving() {
- s.mutex.Lock()
- delete(state.observers, o)
- s.mutex.Unlock()
- return
- }
- if err := json.Unmarshal(v, o.receiver); err != nil {
- Log(errors.New("notifying observer failed").
- Tag("state", key).
- Tag("element", reflect.TypeOf(o.element)).
- Wrap(err))
- return
- }
- for _, fn := range o.onChanges {
- fn()
- }
- },
- })
- }
- }
- func storeValue(recv, v any) error {
- dst := reflect.ValueOf(recv)
- if dst.Kind() != reflect.Ptr {
- return errors.New("receiver is not a pointer")
- }
- dst = dst.Elem()
- src := reflect.ValueOf(v)
- switch {
- case src == reflect.Value{}:
- dst.Set(reflect.Zero(dst.Type()))
- return nil
- case src.Kind() == reflect.Ptr:
- src = src.Elem()
- }
- if src.Type() != dst.Type() {
- return errors.New("value and receiver are not of the same type").
- Tag("value", src.Type()).
- Tag("receiver", dst.Type())
- }
- dst.Set(src)
- return nil
- }
- type persistentState struct {
- Value json.RawMessage `json:",omitempty"`
- EncryptedValue []byte `json:",omitempty"`
- ExpiresAt time.Time `json:",omitempty"`
- }
- func (s *persistentState) isExpired(now time.Time) bool {
- return s.ExpiresAt != time.Time{} && now.After(s.ExpiresAt)
- }
- type broadcastState struct {
- StoreID string `json:""`
- Value json.RawMessage `json:",omitempty"`
- }
|