diff options
author | dan <dan.abramov@gmail.com> | 2024-08-06 01:30:52 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-08-06 01:30:52 +0100 |
commit | 686d5ebb535710dd8c96aa694b4cd1f7913ff3fa (patch) | |
tree | 35ca7e7646d455468c97520b8cb972e8555fc3f2 /src | |
parent | 966f6c511fff510fc011aa5c426c6b7eaf4f21ac (diff) | |
download | voidsky-686d5ebb535710dd8c96aa694b4cd1f7913ff3fa.tar.zst |
[Persisted] Make broadcast subscriptions granular by key (#4874)
* Add fast path for guaranteed noop updates * Change persisted.onUpdate() API to take a key * Implement granular broadcast listeners
Diffstat (limited to 'src')
-rw-r--r-- | src/state/invites.tsx | 5 | ||||
-rw-r--r-- | src/state/persisted/index.ts | 5 | ||||
-rw-r--r-- | src/state/persisted/index.web.ts | 41 | ||||
-rw-r--r-- | src/state/persisted/types.ts | 5 | ||||
-rw-r--r-- | src/state/preferences/alt-text-required.tsx | 9 | ||||
-rw-r--r-- | src/state/preferences/autoplay.tsx | 4 | ||||
-rw-r--r-- | src/state/preferences/disable-haptics.tsx | 4 | ||||
-rw-r--r-- | src/state/preferences/external-embeds-prefs.tsx | 4 | ||||
-rw-r--r-- | src/state/preferences/hidden-posts.tsx | 4 | ||||
-rw-r--r-- | src/state/preferences/in-app-browser.tsx | 4 | ||||
-rw-r--r-- | src/state/preferences/kawaii.tsx | 4 | ||||
-rw-r--r-- | src/state/preferences/languages.tsx | 4 | ||||
-rw-r--r-- | src/state/preferences/large-alt-badge.tsx | 9 | ||||
-rw-r--r-- | src/state/preferences/used-starter-packs.tsx | 9 | ||||
-rw-r--r-- | src/state/session/index.tsx | 4 | ||||
-rw-r--r-- | src/state/shell/color-mode.tsx | 13 | ||||
-rw-r--r-- | src/state/shell/onboarding.tsx | 9 |
17 files changed, 95 insertions, 42 deletions
diff --git a/src/state/invites.tsx b/src/state/invites.tsx index 6a0d1b590..0d40caf25 100644 --- a/src/state/invites.tsx +++ b/src/state/invites.tsx @@ -1,4 +1,5 @@ import React from 'react' + import * as persisted from '#/state/persisted' type StateContext = persisted.Schema['invites'] @@ -35,8 +36,8 @@ export function Provider({children}: React.PropsWithChildren<{}>) { ) React.useEffect(() => { - return persisted.onUpdate(() => { - setState(persisted.get('invites')) + return persisted.onUpdate('invites', nextInvites => { + setState(nextInvites) }) }, [setState]) diff --git a/src/state/persisted/index.ts b/src/state/persisted/index.ts index 95f814850..6f4beae2c 100644 --- a/src/state/persisted/index.ts +++ b/src/state/persisted/index.ts @@ -41,7 +41,10 @@ export async function write<K extends keyof Schema>( } write satisfies PersistedApi['write'] -export function onUpdate(_cb: () => void): () => void { +export function onUpdate<K extends keyof Schema>( + _key: K, + _cb: (v: Schema[K]) => void, +): () => void { return () => {} } onUpdate satisfies PersistedApi['onUpdate'] diff --git a/src/state/persisted/index.web.ts b/src/state/persisted/index.web.ts index d71b59096..7521776bc 100644 --- a/src/state/persisted/index.web.ts +++ b/src/state/persisted/index.web.ts @@ -47,18 +47,36 @@ export async function write<K extends keyof Schema>( // Don't fire the update listeners yet to avoid a loop. // If there was a change, we'll receive the broadcast event soon enough which will do that. } + try { + if (JSON.stringify({v: _state[key]}) === JSON.stringify({v: value})) { + // Fast path for updates that are guaranteed to be noops. + // This is good mostly because it avoids useless broadcasts to other tabs. + return + } + } catch (e) { + // Ignore and go through the normal path. + } _state = { ..._state, [key]: value, } writeToStorage(_state) - broadcast.postMessage({event: UPDATE_EVENT}) + broadcast.postMessage({event: {type: UPDATE_EVENT, key}}) + broadcast.postMessage({event: UPDATE_EVENT}) // Backcompat while upgrading } write satisfies PersistedApi['write'] -export function onUpdate(cb: () => void): () => void { - _emitter.addListener('update', cb) - return () => _emitter.removeListener('update', cb) +export function onUpdate<K extends keyof Schema>( + key: K, + cb: (v: Schema[K]) => void, +): () => void { + const listener = () => cb(get(key)) + _emitter.addListener('update', listener) // Backcompat while upgrading + _emitter.addListener('update:' + key, listener) + return () => { + _emitter.removeListener('update', listener) // Backcompat while upgrading + _emitter.removeListener('update:' + key, listener) + } } onUpdate satisfies PersistedApi['onUpdate'] @@ -72,12 +90,23 @@ export async function clearStorage() { clearStorage satisfies PersistedApi['clearStorage'] async function onBroadcastMessage({data}: MessageEvent) { - if (typeof data === 'object' && data.event === UPDATE_EVENT) { + if ( + typeof data === 'object' && + (data.event === UPDATE_EVENT || // Backcompat while upgrading + data.event?.type === UPDATE_EVENT) + ) { // read next state, possibly updated by another tab const next = readFromStorage() + if (next === _state) { + return + } if (next) { _state = next - _emitter.emit('update') + if (typeof data.event.key === 'string') { + _emitter.emit('update:' + data.event.key) + } else { + _emitter.emit('update') // Backcompat while upgrading + } } else { logger.error( `persisted state: handled update update from broadcast channel, but found no data`, diff --git a/src/state/persisted/types.ts b/src/state/persisted/types.ts index 95852f796..fd39079bf 100644 --- a/src/state/persisted/types.ts +++ b/src/state/persisted/types.ts @@ -4,6 +4,9 @@ export type PersistedApi = { init(): Promise<void> get<K extends keyof Schema>(key: K): Schema[K] write<K extends keyof Schema>(key: K, value: Schema[K]): Promise<void> - onUpdate(_cb: () => void): () => void + onUpdate<K extends keyof Schema>( + key: K, + cb: (v: Schema[K]) => void, + ): () => void clearStorage: () => Promise<void> } diff --git a/src/state/preferences/alt-text-required.tsx b/src/state/preferences/alt-text-required.tsx index 642e790fb..0ddc173ea 100644 --- a/src/state/preferences/alt-text-required.tsx +++ b/src/state/preferences/alt-text-required.tsx @@ -26,9 +26,12 @@ export function Provider({children}: React.PropsWithChildren<{}>) { ) React.useEffect(() => { - return persisted.onUpdate(() => { - setState(persisted.get('requireAltTextEnabled')) - }) + return persisted.onUpdate( + 'requireAltTextEnabled', + nextRequireAltTextEnabled => { + setState(nextRequireAltTextEnabled) + }, + ) }, [setStateWrapped]) return ( diff --git a/src/state/preferences/autoplay.tsx b/src/state/preferences/autoplay.tsx index d5aa049f3..141c8161e 100644 --- a/src/state/preferences/autoplay.tsx +++ b/src/state/preferences/autoplay.tsx @@ -24,8 +24,8 @@ export function Provider({children}: {children: React.ReactNode}) { ) React.useEffect(() => { - return persisted.onUpdate(() => { - setState(Boolean(persisted.get('disableAutoplay'))) + return persisted.onUpdate('disableAutoplay', nextDisableAutoplay => { + setState(Boolean(nextDisableAutoplay)) }) }, [setStateWrapped]) diff --git a/src/state/preferences/disable-haptics.tsx b/src/state/preferences/disable-haptics.tsx index af2c55a18..367d4f7db 100644 --- a/src/state/preferences/disable-haptics.tsx +++ b/src/state/preferences/disable-haptics.tsx @@ -24,8 +24,8 @@ export function Provider({children}: {children: React.ReactNode}) { ) React.useEffect(() => { - return persisted.onUpdate(() => { - setState(Boolean(persisted.get('disableHaptics'))) + return persisted.onUpdate('disableHaptics', nextDisableHaptics => { + setState(Boolean(nextDisableHaptics)) }) }, [setStateWrapped]) diff --git a/src/state/preferences/external-embeds-prefs.tsx b/src/state/preferences/external-embeds-prefs.tsx index 9ace5d940..04afb89dd 100644 --- a/src/state/preferences/external-embeds-prefs.tsx +++ b/src/state/preferences/external-embeds-prefs.tsx @@ -35,8 +35,8 @@ export function Provider({children}: React.PropsWithChildren<{}>) { ) React.useEffect(() => { - return persisted.onUpdate(() => { - setState(persisted.get('externalEmbeds')) + return persisted.onUpdate('externalEmbeds', nextExternalEmbeds => { + setState(nextExternalEmbeds) }) }, [setStateWrapped]) diff --git a/src/state/preferences/hidden-posts.tsx b/src/state/preferences/hidden-posts.tsx index 2c6a373e1..510af713d 100644 --- a/src/state/preferences/hidden-posts.tsx +++ b/src/state/preferences/hidden-posts.tsx @@ -44,8 +44,8 @@ export function Provider({children}: React.PropsWithChildren<{}>) { ) React.useEffect(() => { - return persisted.onUpdate(() => { - setState(persisted.get('hiddenPosts')) + return persisted.onUpdate('hiddenPosts', nextHiddenPosts => { + setState(nextHiddenPosts) }) }, [setStateWrapped]) diff --git a/src/state/preferences/in-app-browser.tsx b/src/state/preferences/in-app-browser.tsx index 73c4bbbe7..76c854105 100644 --- a/src/state/preferences/in-app-browser.tsx +++ b/src/state/preferences/in-app-browser.tsx @@ -34,8 +34,8 @@ export function Provider({children}: React.PropsWithChildren<{}>) { ) React.useEffect(() => { - return persisted.onUpdate(() => { - setState(persisted.get('useInAppBrowser')) + return persisted.onUpdate('useInAppBrowser', nextUseInAppBrowser => { + setState(nextUseInAppBrowser) }) }, [setStateWrapped]) diff --git a/src/state/preferences/kawaii.tsx b/src/state/preferences/kawaii.tsx index 4aa95ef8b..421689164 100644 --- a/src/state/preferences/kawaii.tsx +++ b/src/state/preferences/kawaii.tsx @@ -21,8 +21,8 @@ export function Provider({children}: React.PropsWithChildren<{}>) { ) React.useEffect(() => { - return persisted.onUpdate(() => { - setState(persisted.get('kawaii')) + return persisted.onUpdate('kawaii', nextKawaii => { + setState(nextKawaii) }) }, [setStateWrapped]) diff --git a/src/state/preferences/languages.tsx b/src/state/preferences/languages.tsx index b7494c1f9..5093cd725 100644 --- a/src/state/preferences/languages.tsx +++ b/src/state/preferences/languages.tsx @@ -43,8 +43,8 @@ export function Provider({children}: React.PropsWithChildren<{}>) { ) React.useEffect(() => { - return persisted.onUpdate(() => { - setState(persisted.get('languagePrefs')) + return persisted.onUpdate('languagePrefs', nextLanguagePrefs => { + setState(nextLanguagePrefs) }) }, [setStateWrapped]) diff --git a/src/state/preferences/large-alt-badge.tsx b/src/state/preferences/large-alt-badge.tsx index b3d597c5c..9d2c9fa54 100644 --- a/src/state/preferences/large-alt-badge.tsx +++ b/src/state/preferences/large-alt-badge.tsx @@ -26,9 +26,12 @@ export function Provider({children}: React.PropsWithChildren<{}>) { ) React.useEffect(() => { - return persisted.onUpdate(() => { - setState(persisted.get('largeAltBadgeEnabled')) - }) + return persisted.onUpdate( + 'largeAltBadgeEnabled', + nextLargeAltBadgeEnabled => { + setState(nextLargeAltBadgeEnabled) + }, + ) }, [setStateWrapped]) return ( diff --git a/src/state/preferences/used-starter-packs.tsx b/src/state/preferences/used-starter-packs.tsx index 8d5d9e828..e4de479d5 100644 --- a/src/state/preferences/used-starter-packs.tsx +++ b/src/state/preferences/used-starter-packs.tsx @@ -19,9 +19,12 @@ export function Provider({children}: {children: React.ReactNode}) { } React.useEffect(() => { - return persisted.onUpdate(() => { - setState(persisted.get('hasCheckedForStarterPack')) - }) + return persisted.onUpdate( + 'hasCheckedForStarterPack', + nextHasCheckedForStarterPack => { + setState(nextHasCheckedForStarterPack) + }, + ) }, []) return ( diff --git a/src/state/session/index.tsx b/src/state/session/index.tsx index 3aac19025..09fcf8664 100644 --- a/src/state/session/index.tsx +++ b/src/state/session/index.tsx @@ -185,8 +185,8 @@ export function Provider({children}: React.PropsWithChildren<{}>) { }, [state]) React.useEffect(() => { - return persisted.onUpdate(() => { - const synced = persisted.get('session') + return persisted.onUpdate('session', nextSession => { + const synced = nextSession addSessionDebugLog({type: 'persisted:receive', data: synced}) dispatch({ type: 'synced-accounts', diff --git a/src/state/shell/color-mode.tsx b/src/state/shell/color-mode.tsx index f3339d240..47b936c0b 100644 --- a/src/state/shell/color-mode.tsx +++ b/src/state/shell/color-mode.tsx @@ -1,4 +1,5 @@ import React from 'react' + import * as persisted from '#/state/persisted' type StateContext = { @@ -43,10 +44,16 @@ export function Provider({children}: React.PropsWithChildren<{}>) { ) React.useEffect(() => { - return persisted.onUpdate(() => { - setColorMode(persisted.get('colorMode')) - setDarkTheme(persisted.get('darkTheme')) + const unsub1 = persisted.onUpdate('darkTheme', nextDarkTheme => { + setDarkTheme(nextDarkTheme) + }) + const unsub2 = persisted.onUpdate('colorMode', nextColorMode => { + setColorMode(nextColorMode) }) + return () => { + unsub1() + unsub2() + } }, []) return ( diff --git a/src/state/shell/onboarding.tsx b/src/state/shell/onboarding.tsx index 6a18b461f..d3a8fec46 100644 --- a/src/state/shell/onboarding.tsx +++ b/src/state/shell/onboarding.tsx @@ -1,6 +1,7 @@ import React from 'react' -import * as persisted from '#/state/persisted' + import {track} from '#/lib/analytics/analytics' +import * as persisted from '#/state/persisted' export const OnboardingScreenSteps = { Welcome: 'Welcome', @@ -81,13 +82,13 @@ export function Provider({children}: React.PropsWithChildren<{}>) { ) React.useEffect(() => { - return persisted.onUpdate(() => { - const next = persisted.get('onboarding').step + return persisted.onUpdate('onboarding', nextOnboarding => { + const next = nextOnboarding.step // TODO we've introduced a footgun if (state.step !== next) { dispatch({ type: 'set', - step: persisted.get('onboarding').step as OnboardingStep, + step: nextOnboarding.step as OnboardingStep, }) } }) |