/ src / composables / useWebExtensionStorage.ts
useWebExtensionStorage.ts
  1  import { StorageSerializers } from '@vueuse/core'
  2  import { pausableWatch, toValue, tryOnScopeDispose } from '@vueuse/shared'
  3  import { ref, shallowRef } from 'vue-demi'
  4  import { storage } from 'webextension-polyfill'
  5  
  6  import type {
  7    StorageLikeAsync,
  8    UseStorageAsyncOptions,
  9  } from '@vueuse/core'
 10  import type { MaybeRefOrGetter, RemovableRef } from '@vueuse/shared'
 11  import type { Ref } from 'vue-demi'
 12  import type { Storage } from 'webextension-polyfill'
 13  
 14  export type WebExtensionStorageOptions<T> = UseStorageAsyncOptions<T>
 15  
 16  // https://github.com/vueuse/vueuse/blob/658444bf9f8b96118dbd06eba411bb6639e24e88/packages/core/useStorage/guess.ts
 17  export function guessSerializerType(rawInit: unknown) {
 18    return rawInit == null
 19      ? 'any'
 20      : rawInit instanceof Set
 21        ? 'set'
 22        : rawInit instanceof Map
 23          ? 'map'
 24          : rawInit instanceof Date
 25            ? 'date'
 26            : typeof rawInit === 'boolean'
 27              ? 'boolean'
 28              : typeof rawInit === 'string'
 29                ? 'string'
 30                : typeof rawInit === 'object'
 31                  ? 'object'
 32                  : Number.isNaN(rawInit)
 33                    ? 'any'
 34                    : 'number'
 35  }
 36  
 37  const storageInterface: StorageLikeAsync = {
 38    removeItem(key: string) {
 39      return storage.local.remove(key)
 40    },
 41  
 42    setItem(key: string, value: string) {
 43      return storage.local.set({ [key]: value })
 44    },
 45  
 46    async getItem(key: string) {
 47      const storedData = await storage.local.get(key)
 48  
 49      return storedData[key] as string
 50    },
 51  }
 52  
 53  /**
 54   * https://github.com/vueuse/vueuse/blob/658444bf9f8b96118dbd06eba411bb6639e24e88/packages/core/useStorageAsync/index.ts
 55   *
 56   * @param key
 57   * @param initialValue
 58   * @param options
 59   */
 60  export function useWebExtensionStorage<T>(
 61    key: string,
 62    initialValue: MaybeRefOrGetter<T>,
 63    options: WebExtensionStorageOptions<T> = {},
 64  ): RemovableRef<T> {
 65    const {
 66      flush = 'pre',
 67      deep = true,
 68      listenToStorageChanges = true,
 69      writeDefaults = true,
 70      mergeDefaults = false,
 71      shallow,
 72      eventFilter,
 73      onError = (e) => {
 74        console.error(e)
 75      },
 76    } = options
 77  
 78    const rawInit: T = toValue(initialValue)
 79    const type = guessSerializerType(rawInit)
 80  
 81    const data = (shallow ? shallowRef : ref)(initialValue) as Ref<T>
 82    const serializer = options.serializer ?? StorageSerializers[type]
 83  
 84    async function read(event?: { key: string, newValue: string | null }) {
 85      if (event && event.key !== key)
 86        return
 87  
 88      try {
 89        const rawValue = event ? event.newValue : await storageInterface.getItem(key)
 90        if (rawValue == null) {
 91          data.value = rawInit
 92          if (writeDefaults && rawInit !== null)
 93            await storageInterface.setItem(key, await serializer.write(rawInit))
 94        }
 95        else if (mergeDefaults) {
 96          const value = await serializer.read(rawValue) as T
 97          if (typeof mergeDefaults === 'function')
 98            data.value = mergeDefaults(value, rawInit)
 99          else if (type === 'object' && !Array.isArray(value))
100            data.value = { ...(rawInit as Record<keyof unknown, unknown>), ...(value as Record<keyof unknown, unknown>) } as T
101          else data.value = value
102        }
103        else {
104          data.value = await serializer.read(rawValue) as T
105        }
106      }
107      catch (error) {
108        onError(error)
109      }
110    }
111  
112    void read()
113  
114    async function write() {
115      try {
116        await (
117          data.value == null
118            ? storageInterface.removeItem(key)
119            : storageInterface.setItem(key, await serializer.write(data.value))
120        )
121      }
122      catch (error) {
123        onError(error)
124      }
125    }
126  
127    const { pause: pauseWatch, resume: resumeWatch } = pausableWatch(
128      data,
129      write,
130      {
131        flush,
132        deep,
133        eventFilter,
134      },
135    )
136  
137    if (listenToStorageChanges) {
138      const listener = async (changes: Record<string, Storage.StorageChange>) => {
139        try {
140          pauseWatch()
141          for (const [key, change] of Object.entries(changes)) {
142            await read({
143              key,
144              newValue: change.newValue as string | null,
145            })
146          }
147        }
148        finally {
149          resumeWatch()
150        }
151      }
152  
153      storage.onChanged.addListener(listener)
154  
155      tryOnScopeDispose(() => {
156        storage.onChanged.removeListener(listener)
157      })
158    }
159  
160    return data as RemovableRef<T>
161  }