antfu / vitesse-webext

⚡️ WebExtension Vite Starter Template

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

should use storage.local over window.localStorage

privatenumber opened this issue · comments

Problem

This template currently uses localStorage(1, 2).

MDN advises against using local storage because of persistence issues:

Although this API is similar to Window.localStorage it is recommended that you don't use Window.localStorage in extension code. Firefox will clear data stored by extensions using the localStorage API in various scenarios where users clear their browsing history and data for privacy reasons, while data saved using the storage.local API will be correctly persisted in these scenarios.

Proposal

Use storage.local via webextension-polyfill.

FYI VueUse useStorage accepts a 3rd parameter to accept a StoreLike object, so perhaps a wrapper around storage.local could have been passed in. However, storage.local APIs return promises so it cannot.

Because of storage.local's async nature, it might be best to create another composable function that returns an isLoaded state.

commented

I'm also facing that issue, for now I will continue to use my custom helpers for storage.local.

If anyone's interested, here's the composition "use" function I ended up with to work with storage.local:

import { storage } from 'webextension-polyfill';

const get = async (key: string) => {
	const data = await storage.local.get(key);
	return data[key];
};

export function useBrowserStorage<T>(
	key: string,
	initialValue: T,
) {
	const data = reactive({
		value: initialValue,
		isLoaded: false,
	});

	// First data read from storage
	get(key).then(
		(keyDataRaw) => {
			if (keyDataRaw) {
				data.value = JSON.parse(keyDataRaw);
			}
			data.isLoaded = true;
		},
		(error) => {
			throw error;
		},
	);

	// Update local reactive state
	storage.onChanged.addListener((changes) => {
		data.value = JSON.parse(changes[key].newValue);
	});

	// Update storage
	watch(
		data,
		async () => {
			const { value } = data;
			if (value === null || value === undefined) {
				await storage.local.remove(key);
			} else {
				const serializedValue = JSON.stringify(value);
				const currentValue = await get(key);

				if (serializedValue !== currentValue) {
					await storage.local.set({
						[key]: serializedValue,
					});
				}
			}
		},
		{
			deep: true,
		},
	);

	return data;
}

It returns an object { value: T, isLoaded: boolean }. The return type is similar to a ref, but with an isLoaded property because storage.local API is async.

@antfu I wasn't sure if this belongs in VueUse because it relies on webextension-polyfill, but happy to PR this in somewhere if you'd like.

I wasn't sure if this belongs in VueUse

@privatenumber I guess we are missing a composable to support async storage. I imagine we could have useAsyncStorage in VueUse, and convert the storage interface from webextension-polyfill in this template.

I ended up using this:

import { useStorageAsync, StorageLikeAsync } from '@vueuse/core';
import { storage } from 'webextension-polyfill';

const browserStorageLocal: StorageLikeAsync = {
  removeItem(key: string) {
    return storage.local.remove(key);
  },
  
  setItem(key: string, value: string) {
    return storage.local.set({ [key]: value });
  },
  
  async getItem(key: string) {
    return (await storage.local.get(key))[key];
  }
};

const useBrowserStorageLocal = <T>(key: string, initialValue: T) => {
  return useStorageAsync(key, initialValue, browserStorageLocal);
};

export default useBrowserStorageLocal;

Nice! Looks like @antfu added useStorageAsync shortly after: vueuse/vueuse@169b02b

Do you want to PR that in @hankolsen ?