about summary refs log tree commit diff
path: root/src/state/persisted
diff options
context:
space:
mode:
Diffstat (limited to 'src/state/persisted')
-rw-r--r--src/state/persisted/index.ts5
-rw-r--r--src/state/persisted/index.web.ts41
-rw-r--r--src/state/persisted/types.ts5
3 files changed, 43 insertions, 8 deletions
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>
 }