Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at c540dae4e7db67031ee5f67feb076927999e364d 163 lines 4.4 kB view raw
1import EventEmitter from 'eventemitter3' 2 3import BroadcastChannel from '#/lib/broadcast' 4import {logger} from '#/logger' 5import { 6 defaults, 7 type Schema, 8 tryParse, 9 tryStringify, 10} from '#/state/persisted/schema' 11import {type PersistedApi} from './types' 12import {normalizeData} from './util' 13 14export type {PersistedAccount, Schema} from '#/state/persisted/schema' 15export {defaults} from '#/state/persisted/schema' 16 17const BSKY_STORAGE = 'BSKY_STORAGE' 18 19const broadcast = new BroadcastChannel('BSKY_BROADCAST_CHANNEL') 20const UPDATE_EVENT = 'BSKY_UPDATE' 21 22let _state: Schema = defaults 23const _emitter = new EventEmitter() 24 25export async function init() { 26 broadcast.onmessage = onBroadcastMessage 27 window.onstorage = onStorage 28 const stored = readFromStorage() 29 if (stored) { 30 _state = stored 31 } 32} 33init satisfies PersistedApi['init'] 34 35export function get<K extends keyof Schema>(key: K): Schema[K] { 36 return _state[key] 37} 38get satisfies PersistedApi['get'] 39 40export async function write<K extends keyof Schema>( 41 key: K, 42 value: Schema[K], 43): Promise<void> { 44 const next = readFromStorage() 45 if (next) { 46 // The storage could have been updated by a different tab before this tab is notified. 47 // Make sure this write is applied on top of the latest data in the storage as long as it's valid. 48 _state = next 49 // Don't fire the update listeners yet to avoid a loop. 50 // If there was a change, we'll receive the broadcast event soon enough which will do that. 51 } 52 try { 53 if (JSON.stringify({v: _state[key]}) === JSON.stringify({v: value})) { 54 // Fast path for updates that are guaranteed to be noops. 55 // This is good mostly because it avoids useless broadcasts to other tabs. 56 return 57 } 58 } catch (e) { 59 // Ignore and go through the normal path. 60 } 61 _state = normalizeData({ 62 ..._state, 63 [key]: value, 64 }) 65 writeToStorage(_state) 66 broadcast.postMessage({event: {type: UPDATE_EVENT, key}}) 67 broadcast.postMessage({event: UPDATE_EVENT}) // Backcompat while upgrading 68} 69write satisfies PersistedApi['write'] 70 71export function onUpdate<K extends keyof Schema>( 72 key: K, 73 cb: (v: Schema[K]) => void, 74): () => void { 75 const listener = () => cb(get(key)) 76 _emitter.addListener('update', listener) // Backcompat while upgrading 77 _emitter.addListener('update:' + key, listener) 78 return () => { 79 _emitter.removeListener('update', listener) // Backcompat while upgrading 80 _emitter.removeListener('update:' + key, listener) 81 } 82} 83onUpdate satisfies PersistedApi['onUpdate'] 84 85export async function clearStorage() { 86 try { 87 localStorage.removeItem(BSKY_STORAGE) 88 } catch (e: any) { 89 // Expected on the web in private mode. 90 } 91} 92clearStorage satisfies PersistedApi['clearStorage'] 93 94function onStorage() { 95 const next = readFromStorage() 96 if (next === _state) { 97 return 98 } 99 if (next) { 100 _state = next 101 _emitter.emit('update') 102 } 103} 104 105async function onBroadcastMessage({data}: MessageEvent) { 106 if ( 107 typeof data === 'object' && 108 (data.event === UPDATE_EVENT || // Backcompat while upgrading 109 data.event?.type === UPDATE_EVENT) 110 ) { 111 // read next state, possibly updated by another tab 112 const next = readFromStorage() 113 if (next === _state) { 114 return 115 } 116 if (next) { 117 _state = next 118 if (typeof data.event.key === 'string') { 119 _emitter.emit('update:' + data.event.key) 120 } else { 121 _emitter.emit('update') // Backcompat while upgrading 122 } 123 } else { 124 logger.error( 125 `persisted state: handled update update from broadcast channel, but found no data`, 126 ) 127 } 128 } 129} 130 131function writeToStorage(value: Schema) { 132 const rawData = tryStringify(value) 133 if (rawData) { 134 try { 135 localStorage.setItem(BSKY_STORAGE, rawData) 136 } catch (e) { 137 // Expected on the web in private mode. 138 } 139 } 140} 141 142let lastRawData: string | undefined 143let lastResult: Schema | undefined 144function readFromStorage(): Schema | undefined { 145 let rawData: string | null = null 146 try { 147 rawData = localStorage.getItem(BSKY_STORAGE) 148 } catch (e) { 149 // Expected on the web in private mode. 150 } 151 if (rawData) { 152 if (rawData === lastRawData) { 153 return lastResult 154 } else { 155 const result = tryParse(rawData) 156 if (result) { 157 lastRawData = rawData 158 lastResult = normalizeData(result) 159 return lastResult 160 } 161 } 162 } 163}