about summary refs log tree commit diff
path: root/src/state/persisted/index.ts
blob: 5fe0f9bd0acece4abc65bc7bdfddce70ccd68e01 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
import EventEmitter from 'eventemitter3'

import BroadcastChannel from '#/lib/broadcast'
import {logger} from '#/logger'
import {migrate} from '#/state/persisted/legacy'
import {defaults, Schema} from '#/state/persisted/schema'
import * as store from '#/state/persisted/store'
export type {PersistedAccount, Schema} from '#/state/persisted/schema'
export {defaults} from '#/state/persisted/schema'

const broadcast = new BroadcastChannel('BSKY_BROADCAST_CHANNEL')
const UPDATE_EVENT = 'BSKY_UPDATE'

let _state: Schema = defaults
const _emitter = new EventEmitter()

/**
 * Initializes and returns persisted data state, so that it can be passed to
 * the Provider.
 */
export async function init() {
  logger.debug('persisted state: initializing')

  broadcast.onmessage = onBroadcastMessage

  try {
    await migrate() // migrate old store
    const stored = await store.read() // check for new store
    if (!stored) {
      logger.debug('persisted state: initializing default storage')
      await store.write(defaults) // opt: init new store
    }
    _state = stored || defaults // return new store
    logger.debug('persisted state: initialized')
  } catch (e) {
    logger.error('persisted state: failed to load root state from storage', {
      message: e,
    })
    // AsyncStorage failure, but we can still continue in memory
    return defaults
  }
}

export function get<K extends keyof Schema>(key: K): Schema[K] {
  return _state[key]
}

export async function write<K extends keyof Schema>(
  key: K,
  value: Schema[K],
): Promise<void> {
  try {
    _state[key] = value
    await store.write(_state)
    // must happen on next tick, otherwise the tab will read stale storage data
    setTimeout(() => broadcast.postMessage({event: UPDATE_EVENT}), 0)
    logger.debug(`persisted state: wrote root state to storage`, {
      updatedKey: key,
    })
  } catch (e) {
    logger.error(`persisted state: failed writing root state to storage`, {
      message: e,
    })
  }
}

export function onUpdate(cb: () => void): () => void {
  _emitter.addListener('update', cb)
  return () => _emitter.removeListener('update', cb)
}

async function onBroadcastMessage({data}: MessageEvent) {
  // validate event
  if (typeof data === 'object' && data.event === UPDATE_EVENT) {
    try {
      // read next state, possibly updated by another tab
      const next = await store.read()

      if (next) {
        logger.debug(`persisted state: handling update from broadcast channel`)
        _state = next
        _emitter.emit('update')
      } else {
        logger.error(
          `persisted state: handled update update from broadcast channel, but found no data`,
        )
      }
    } catch (e) {
      logger.error(
        `persisted state: failed handling update from broadcast channel`,
        {
          message: e,
        },
      )
    }
  }
}