Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: storage reactivity #157

Merged
merged 1 commit into from
Dec 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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')