Skip to content

Commit

Permalink
fix: storage reactivity (#157)
Browse files Browse the repository at this point in the history
Fixes storage reactivity by reimplementing useAsyncStorage from vueuse
and replacing the storage listener on window by a custom one using the
polyfill webextension storage API.
  • Loading branch information
djfhe authored Dec 18, 2023
1 parent 713d7ee commit 0ec9dfe
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 32 deletions.
30 changes: 0 additions & 30 deletions src/composables/useStorageLocal.ts

This file was deleted.

149 changes: 149 additions & 0 deletions src/composables/useWebExtensionStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { StorageSerializers } from '@vueuse/core'
import { toValue, tryOnScopeDispose, watchWithFilter } from '@vueuse/shared'
import { ref, shallowRef } from 'vue-demi'
import { storage } from 'webextension-polyfill'

import type {
StorageLikeAsync,
UseStorageAsyncOptions,
} from '@vueuse/core'
import type { MaybeRefOrGetter, RemovableRef } from '@vueuse/shared'
import type { Ref } from 'vue-demi'
import type { Storage } from 'webextension-polyfill'

export type WebExtensionStorageOptions<T> = UseStorageAsyncOptions<T>

// https://github.com/vueuse/vueuse/blob/658444bf9f8b96118dbd06eba411bb6639e24e88/packages/core/useStorage/guess.ts
export function guessSerializerType<T extends(string | number | boolean | object | null)>(rawInit: T) {
return rawInit == null
? 'any'
: rawInit instanceof Set
? 'set'
: rawInit instanceof Map
? 'map'
: rawInit instanceof Date
? 'date'
: typeof rawInit === 'boolean'
? 'boolean'
: typeof rawInit === 'string'
? 'string'
: typeof rawInit === 'object'
? 'object'
: Number.isNaN(rawInit)
? 'any'
: 'number'
}

const storageInterface: StorageLikeAsync = {
removeItem(key: string) {
return storage.local.remove(key)
},

setItem(key: string, value: string) {
return storage.local.set({ [key]: value })
},

async getItem(key: string) {
const storedData = await storage.local.get(key)

return storedData[key]
},
}

/**
* https://github.com/vueuse/vueuse/blob/658444bf9f8b96118dbd06eba411bb6639e24e88/packages/core/useStorageAsync/index.ts
*
* @param key
* @param initialValue
* @param options
*/
export function useWebExtensionStorage<T extends(string | number | boolean | object | null)>(
key: string,
initialValue: MaybeRefOrGetter<T>,
options: WebExtensionStorageOptions<T> = {},
): RemovableRef<T> {
const {
flush = 'pre',
deep = true,
listenToStorageChanges = true,
writeDefaults = true,
mergeDefaults = false,
shallow,
eventFilter,
onError = (e) => {
console.error(e)
},
} = options

const rawInit: T = toValue(initialValue)
const type = guessSerializerType<T>(rawInit)

const data = (shallow ? shallowRef : ref)(initialValue) as Ref<T>
const serializer = options.serializer ?? StorageSerializers[type]

async function read(event?: { key: string; newValue: string | null }) {
if (event && event.key !== key)
return

try {
const rawValue = event ? event.newValue : await storageInterface.getItem(key)
if (rawValue == null) {
data.value = rawInit
if (writeDefaults && rawInit !== null)
await storageInterface.setItem(key, await serializer.write(rawInit))
}
else if (mergeDefaults) {
const value = await serializer.read(rawValue) as T
if (typeof mergeDefaults === 'function')
data.value = mergeDefaults(value, rawInit)
else if (type === 'object' && !Array.isArray(value))
data.value = { ...(rawInit as Record<keyof unknown, unknown>), ...(value as Record<keyof unknown, unknown>) } as T
else data.value = value
}
else {
data.value = await serializer.read(rawValue) as T
}
}
catch (error) {
onError(error)
}
}

void read()

if (listenToStorageChanges) {
const listener = async (changes: Record<string, Storage.StorageChange>) => {
for (const [key, change] of Object.entries(changes)) {
await read({
key,
newValue: change.newValue as string | null,
})
}
}

storage.onChanged.addListener(listener)

tryOnScopeDispose(() => {
storage.onChanged.removeListener(listener)
})
}

watchWithFilter(
data,
async () => {
try {
await (data.value == null ? storageInterface.removeItem(key) : storageInterface.setItem(key, await serializer.write(data.value)))
}
catch (error) {
onError(error)
}
},
{
flush,
deep,
eventFilter,
},
)

return data as RemovableRef<T>
}
4 changes: 2 additions & 2 deletions src/logic/storage.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { useStorageLocal } from '~/composables/useStorageLocal'
import { useWebExtensionStorage } from '~/composables/useWebExtensionStorage'

export const storageDemo = useStorageLocal('webext-demo', 'Storage Demo')
export const storageDemo = useWebExtensionStorage('webext-demo', 'Storage Demo')

0 comments on commit 0ec9dfe

Please sign in to comment.