state.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501
  1. package app
  2. import (
  3. "encoding/json"
  4. "reflect"
  5. "sync"
  6. "time"
  7. "github.com/google/uuid"
  8. "github.com/maxence-charriere/go-app/v9/pkg/errors"
  9. )
  10. // Observer is an observer that observes changes for a given state.
  11. type Observer interface {
  12. // Defines a condition that reports whether the observer keeps observing the
  13. // associated state. Multiple conditions can be defined by successively
  14. // calling While().
  15. While(condition func() bool) Observer
  16. // Executes the given function on the UI goroutine when the observed value
  17. // changes. Multiple functions can be executed by successively calling
  18. // OnChange().
  19. OnChange(fn func()) Observer
  20. // Stores the value associated with the observed state into the given
  21. // receiver. Panics when the receiver is not a pointer or nil.
  22. //
  23. // The receiver is updated each time the associated state changes. It is
  24. // unchanged when its pointed value has a different type than the associated
  25. // state value.
  26. Value(recv any)
  27. }
  28. // A state represents an observable value available across the app.
  29. type State struct {
  30. // Reports whether the state is persisted in local storage.
  31. IsPersistent bool
  32. // Reports whether the state is encrypted before being persisted in local
  33. // storage.
  34. IsEncrypted bool
  35. // The time when the state expires. The state never expires when zero value.
  36. ExpiresAt time.Time
  37. // Reports whether a state is broadcasted to other browser tabs and windows.
  38. IsBroadcasted bool
  39. value any
  40. observers map[*observer]struct{}
  41. }
  42. func (s *State) isExpired(now time.Time) bool {
  43. return s.ExpiresAt != time.Time{} && now.After(s.ExpiresAt)
  44. }
  45. // StateOption represents an option applied when a state is set.
  46. type StateOption func(*State)
  47. // Persist is a state option that persists a state in local storage.
  48. //
  49. // Be mindful to not use this option as a cache since local storage is limited
  50. // to 5MB in a lot of web browsers.
  51. func Persist(s *State) {
  52. s.IsPersistent = true
  53. }
  54. // Encrypt is a state option that encrypts a state before persisting it in local
  55. // storage. Encryption is performed only when the Persist option is also set.
  56. func Encrypt(s *State) {
  57. s.IsEncrypted = true
  58. }
  59. // ExpiresIn returns a state option that sets a state value to its zero value
  60. // after the given duration.
  61. //
  62. // Values persisted to local storage with the Persist option are removed from
  63. // it.
  64. func ExpiresIn(d time.Duration) StateOption {
  65. return ExpiresAt(time.Now().Add(d))
  66. }
  67. // ExpiresAt returns a state option that sets a state value to its zero value at
  68. // the given time.
  69. //
  70. // Values persisted to local storage with the Persist option are removed from
  71. // it.
  72. func ExpiresAt(t time.Time) StateOption {
  73. return func(s *State) {
  74. s.ExpiresAt = t
  75. }
  76. }
  77. // Broadcast is a state option that broadcasts a state to other browser tabs and
  78. // windows from the same origin.
  79. func Broadcast(s *State) {
  80. s.IsBroadcasted = true
  81. }
  82. type observer struct {
  83. element UI
  84. subscribe func(*observer)
  85. conditions []func() bool
  86. onChanges []func()
  87. receiver any
  88. }
  89. func newObserver(elem UI, subscribe func(*observer)) *observer {
  90. return &observer{
  91. element: elem,
  92. subscribe: subscribe,
  93. }
  94. }
  95. func (o *observer) While(fn func() bool) Observer {
  96. o.conditions = append(o.conditions, fn)
  97. return o
  98. }
  99. func (o *observer) OnChange(fn func()) Observer {
  100. o.onChanges = append(o.onChanges, fn)
  101. return o
  102. }
  103. func (o *observer) Value(recv any) {
  104. if reflect.ValueOf(recv).Kind() != reflect.Ptr {
  105. panic(errors.New("observer value receiver is not a pointer"))
  106. }
  107. o.receiver = recv
  108. o.subscribe(o)
  109. }
  110. func (o *observer) isObserving() bool {
  111. if !o.element.Mounted() {
  112. return false
  113. }
  114. for _, c := range o.conditions {
  115. if !c() {
  116. return false
  117. }
  118. }
  119. return true
  120. }
  121. type store struct {
  122. mutex sync.Mutex
  123. id string
  124. states map[string]State
  125. disp Dispatcher
  126. broadcastChannel Value
  127. onBroadcastClose func()
  128. }
  129. func newStore(d Dispatcher) *store {
  130. s := &store{
  131. id: uuid.NewString(),
  132. states: make(map[string]State),
  133. disp: d,
  134. }
  135. s.initBroadcast()
  136. return s
  137. }
  138. func (s *store) Set(key string, v any, opts ...StateOption) {
  139. s.mutex.Lock()
  140. defer s.mutex.Unlock()
  141. state := s.states[key]
  142. state.value = v
  143. for _, o := range opts {
  144. o(&state)
  145. }
  146. s.states[key] = state
  147. if state.IsPersistent {
  148. if err := s.setPersistent(key, state.IsEncrypted, state.ExpiresAt, v); err != nil {
  149. Log(errors.New("persisting state failed").
  150. Tag("state", key).
  151. Wrap(err))
  152. return
  153. }
  154. }
  155. if state.isExpired(time.Now()) {
  156. state = s.expire(key, state)
  157. s.states[key] = state
  158. return
  159. }
  160. if state.IsBroadcasted {
  161. if err := s.broadcast(key, v); err != nil {
  162. Log(errors.New("broadcasting state failed").
  163. Tag("state", key).
  164. Wrap(err))
  165. return
  166. }
  167. }
  168. for obs := range state.observers {
  169. o := obs
  170. if !o.element.Mounted() {
  171. delete(state.observers, o)
  172. continue
  173. }
  174. s.disp.Dispatch(Dispatch{
  175. Mode: Update,
  176. Source: o.element,
  177. Function: func(ctx Context) {
  178. if !o.isObserving() {
  179. s.mutex.Lock()
  180. delete(state.observers, o)
  181. s.mutex.Unlock()
  182. return
  183. }
  184. if err := storeValue(o.receiver, v); err != nil {
  185. Log(errors.New("notifying observer failed").
  186. Tag("state", key).
  187. Tag("element", reflect.TypeOf(o.element)).
  188. Wrap(err))
  189. return
  190. }
  191. for _, fn := range o.onChanges {
  192. fn()
  193. }
  194. },
  195. })
  196. }
  197. }
  198. func (s *store) Get(key string, recv any) {
  199. s.mutex.Lock()
  200. defer s.mutex.Unlock()
  201. var err error
  202. state := s.states[key]
  203. if state.isExpired(time.Now()) {
  204. state = s.expire(key, state)
  205. s.states[key] = state
  206. }
  207. if state.value != nil {
  208. err = storeValue(recv, state.value)
  209. } else {
  210. err = s.getPersistent(key, recv)
  211. }
  212. if err != nil {
  213. Log(errors.New("getting state value failed").
  214. Tag("state", key).
  215. Wrap(err))
  216. }
  217. }
  218. func (s *store) Del(key string) {
  219. s.mutex.Lock()
  220. defer s.mutex.Unlock()
  221. delete(s.states, key)
  222. s.disp.getLocalStorage().Del(key)
  223. }
  224. func (s *store) Observe(key string, elem UI) Observer {
  225. return newObserver(elem, func(o *observer) {
  226. s.mutex.Lock()
  227. defer s.mutex.Unlock()
  228. if err := s.subscribe(key, o); err != nil {
  229. Log(errors.New("notifying observer failed").
  230. Tag("state", key).
  231. Tag("element", reflect.TypeOf(elem)).
  232. Wrap(err))
  233. }
  234. })
  235. }
  236. func (s *store) Cleanup() {
  237. s.mutex.Lock()
  238. defer s.mutex.Unlock()
  239. s.removeUnusedObservers()
  240. s.expireExpiredValues()
  241. }
  242. func (s *store) Close() {
  243. if s.broadcastChannel != nil {
  244. s.broadcastChannel.Call("close")
  245. s.broadcastChannel = nil
  246. }
  247. s.onBroadcastClose()
  248. }
  249. func (s *store) subscribe(key string, o *observer) error {
  250. state := s.states[key]
  251. if state.observers == nil {
  252. state.observers = make(map[*observer]struct{})
  253. }
  254. state.observers[o] = struct{}{}
  255. if state.isExpired(time.Now()) {
  256. state = s.expire(key, state)
  257. }
  258. s.states[key] = state
  259. if state.value != nil {
  260. return storeValue(o.receiver, state.value)
  261. }
  262. return s.getPersistent(key, o.receiver)
  263. }
  264. func (s *store) removeUnusedObservers() {
  265. for _, state := range s.states {
  266. for o := range state.observers {
  267. if !o.isObserving() {
  268. delete(state.observers, o)
  269. }
  270. }
  271. }
  272. }
  273. func (s *store) getPersistent(key string, recv any) error {
  274. var state persistentState
  275. s.disp.getLocalStorage().Get(key, &state)
  276. if state.EncryptedValue == nil && state.Value == nil && state.ExpiresAt == (time.Time{}) {
  277. return nil
  278. }
  279. if state.isExpired(time.Now()) {
  280. s.disp.getLocalStorage().Del(key)
  281. return nil
  282. }
  283. if len(state.EncryptedValue) == 0 {
  284. return json.Unmarshal(state.Value, recv)
  285. }
  286. return s.disp.Context().Decrypt(state.EncryptedValue, recv)
  287. }
  288. func (s *store) setPersistent(key string, encrypt bool, expiresAt time.Time, v any) error {
  289. var err error
  290. state := persistentState{
  291. ExpiresAt: expiresAt,
  292. }
  293. if encrypt {
  294. state.EncryptedValue, err = s.disp.Context().Encrypt(v)
  295. } else {
  296. state.Value, err = json.Marshal(v)
  297. }
  298. if err != nil {
  299. return err
  300. }
  301. return s.disp.getLocalStorage().Set(key, state)
  302. }
  303. func (s *store) expireExpiredValues() {
  304. now := time.Now()
  305. for k, state := range s.states {
  306. if state.isExpired(now) {
  307. state = s.expire(k, state)
  308. s.states[k] = state
  309. }
  310. }
  311. }
  312. func (s *store) expire(key string, state State) State {
  313. s.disp.getLocalStorage().Del(key)
  314. state.value = nil
  315. return state
  316. }
  317. func (s *store) initBroadcast() {
  318. broadcastChannel := Window().Get("BroadcastChannel")
  319. if !broadcastChannel.Truthy() {
  320. s.onBroadcastClose = func() {}
  321. return
  322. }
  323. broadcastChannel = broadcastChannel.New("go-app-broadcast-states")
  324. s.broadcastChannel = broadcastChannel
  325. onBroadcast := FuncOf(func(this Value, args []Value) any {
  326. s.onBroadcast(args[0].Get("data"))
  327. return nil
  328. })
  329. s.onBroadcastClose = onBroadcast.Release
  330. broadcastChannel.Set("onmessage", onBroadcast)
  331. }
  332. func (s *store) broadcast(key string, v any) error {
  333. b, err := json.Marshal(v)
  334. if err != nil {
  335. return err
  336. }
  337. if s.broadcastChannel != nil {
  338. s.broadcastChannel.Call("postMessage", map[string]any{
  339. "StoreID": s.id,
  340. "State": key,
  341. "Value": string(b),
  342. })
  343. }
  344. return nil
  345. }
  346. func (s *store) onBroadcast(event Value) {
  347. if storeID := event.Get("StoreID").String(); storeID == "" || storeID == s.id {
  348. return
  349. }
  350. s.mutex.Lock()
  351. defer s.mutex.Unlock()
  352. key := event.Get("State").String()
  353. v := []byte(event.Get("Value").String())
  354. state := s.states[key]
  355. for obs := range state.observers {
  356. o := obs
  357. if !o.element.Mounted() {
  358. delete(state.observers, o)
  359. continue
  360. }
  361. s.disp.Dispatch(Dispatch{
  362. Mode: Update,
  363. Source: o.element,
  364. Function: func(ctx Context) {
  365. if !o.isObserving() {
  366. s.mutex.Lock()
  367. delete(state.observers, o)
  368. s.mutex.Unlock()
  369. return
  370. }
  371. if err := json.Unmarshal(v, o.receiver); err != nil {
  372. Log(errors.New("notifying observer failed").
  373. Tag("state", key).
  374. Tag("element", reflect.TypeOf(o.element)).
  375. Wrap(err))
  376. return
  377. }
  378. for _, fn := range o.onChanges {
  379. fn()
  380. }
  381. },
  382. })
  383. }
  384. }
  385. func storeValue(recv, v any) error {
  386. dst := reflect.ValueOf(recv)
  387. if dst.Kind() != reflect.Ptr {
  388. return errors.New("receiver is not a pointer")
  389. }
  390. dst = dst.Elem()
  391. src := reflect.ValueOf(v)
  392. switch {
  393. case src == reflect.Value{}:
  394. dst.Set(reflect.Zero(dst.Type()))
  395. return nil
  396. case src.Kind() == reflect.Ptr:
  397. src = src.Elem()
  398. }
  399. if src.Type() != dst.Type() {
  400. return errors.New("value and receiver are not of the same type").
  401. Tag("value", src.Type()).
  402. Tag("receiver", dst.Type())
  403. }
  404. dst.Set(src)
  405. return nil
  406. }
  407. type persistentState struct {
  408. Value json.RawMessage `json:",omitempty"`
  409. EncryptedValue []byte `json:",omitempty"`
  410. ExpiresAt time.Time `json:",omitempty"`
  411. }
  412. func (s *persistentState) isExpired(now time.Time) bool {
  413. return s.ExpiresAt != time.Time{} && now.After(s.ExpiresAt)
  414. }
  415. type broadcastState struct {
  416. StoreID string `json:""`
  417. Value json.RawMessage `json:",omitempty"`
  418. }