-
-
Notifications
You must be signed in to change notification settings - Fork 222
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
Improved Storage API (looking for feedback) #228
Comments
I have nothing to add. I like the impl. idea and I'd simplify it even further: const installDate = await storage.get("local:install-date");
storage.watch("session:counter", (event, key) => {
console.log(`${key} ${event}d!`); // session:counter updated/removed!
});
await storage.set("session:counter", 123); I do not think there's need for |
@dvlden If I'm using |
Oh yea, makes sense. 🤣 |
Released in v0.10.0 |
After using impl. of unstorage, I find it weird that in comparison of built-in storage, watcher doesn't return value(s), but only key. Is it possible to hijack the implementation so that it return |
It's definitely not intended, and I didn't explore that. I think I can append an argument to the watch callback without breaking unstorage's interface. I'll add that, because I agree, the vue composable I shared is a little inefficient without it. |
I gave it a try, but it didn't work. Because we define a driver instead of the storage object itself, passing additional arguments into the callback are ignored inside unstorage. So no, this is not possible if using Feel free to contribute a different implementation if this isn't good enough for you. |
Or open a PR on unstorage to add the feature. |
I understand. Was hoping it's possible, but at the moment on the watcher I have to get the current value with the key, so I guess it's not that big of a deal, was just hoping to avoid that extra step. No clues why they decided to go with this specific approach on the watcher, but I guess it's for a good reason. I tried to use the snippet you wrote for me in Vue, previously with two dependencies:
But I failed to make it work with new What can I say man, you did some amazing work in such short time, while I've been testing |
I'll share the entire example, maybe that will help. |
If you get time for it... Not sure I did it right, but looks good to me 👍 |
@dvlden Here's a full example with a working composable: https://github.com/wxt-dev/wxt-examples/blob/main/examples/vue-storage-composable/README.md |
Was using the storage API yesterday, and realized unstorage saves things as strings (at least it saves numbers as strings). await storage.setItem("local:installDate", Date.now());
await storage.getItem("local:installDate"); // "1703427743358" instead of 1703427743358 So that's not acceptable... Will probably implement a custom storage API soon. |
Here's what I'm thinking considering the feedback in this thread, my personal experience, and research into other libraries:
With all that in mind, here's the API I'm proposing for WXT 1.0: // wxt/storage
export const storage: WxtStorage = ...;
export interface WxtStorage {
getItem<T>(key: string): Promise<T | null>;
getMeta<T extends Record<string, unknown>>(key: string): Promise<T>;
getItems(keys: string[]): Promise<Array<{ key: string, value: any }>>;
setItem<T>(key: string, value: T | null): Promise<void>;
setMeta<T extends Record<string, unknown>>(key: string, fields: T | null): Promise<void>;
setItems(values: Array<{ key: string, value: any }>): Promise<void>;
removeItem<T>(key: string): Promise<void>;
removeMeta<T>(key: string, fields?: string[]): Promise<void>;
removeItems<T>(keys: string[]): Promise<void>;
watch<T>(key: string, cb: WatchCallback<T>): Unwatch;
defineItem<T>(key: string, options?: WxtStorageItemOptions): WxtStorageItem<T>;
snapshot(base: string): Promise<any>;
restoreSnapshot(base: string, data: any): Promise<void>;
}
export interface WxtStorageItem<T> {
getItem(): Promise<T>;
setItem(value: T | null): Promise<void>;
setMeta<TMeta extends Record<string, unknown>>(value: TMeta): Promise<void>;
removeItem(): Promise<void>;
removeMeta(fields?: string[]): Promise<void>;
watch(cb: WatchCallback<T>): Unwatch;
};
export interface WxtStorageItemOptions<T> {
defaultValue?: T;
version?: number;
migrations?: Record<number, ((oldValue: any) => any)>;
};
export type WatchCallback<T> = (newValue: T, oldValue: T) => void;
export type Unwatch = () => void; Edit 1: I removed comments since the function names are pretty self-explanatory. I also added the |
New storage API was released in |
Oooh, very interesting. Dropping dependency sounds good, especially cause they did not care about our improvement. Plenty of dangling open PR's. I just updated to v0.13.0 and going through refactor. Got a question tho. I see everywhere literal examples of storing single primitive value per key in the storage. Would you advice to have a single storage item that stores an object instead? Thinking about it, it's the way I used storage with my custom wrapper in the v2 of my extension. So now while developing v3, I do wonder why everyone is showing it as primitive values in storage only? Since you have a lot of knowledge regarding development and extensions, please let me know. Would you advice me to use single source of truth basically or split it into multiple storage items? I love the update! ––– UPDATE ––– Also, how performant is setting watcher per key vs single watcher and switch statement inside per key, as it was with unstorage? |
@dvlden When working with storage, I never use a single key with an object containing multiple fields that need to be set independently for two reasons:
That's not to say you can't store objects. But you should only store objects that are updated as a single "value" all at once instead of updating individual parts of the object. For example, if you have 5 different settings you need to store in your extension, I would store those 5 in separate keys, not a single "preferences" key, because they are updated one at a time. I don't have to get the latest value from storage before updating a single field of that object. I can just set the new value without worrying about that race condition But if you are saving auth tokens for the logged in user, maybe a token and refresh token, those could be stored in a single "auth" key because they're never updated independently.
I decided to change this because I thought the old implementation might be a classic example of premature optimization. I've never measured the performance of the two approaches, so I don't have an answer for that. Extension storage is implemented in native code in all browsers, so my thinking is that it's probably easier to let the native code manage the listeners. If you are experiencing performance issues related to storage, I'll optimize it then. |
I'd love a Migration API. It's something that I had tried to bake into my extension, but didn't quite get it right — tbf I was new to extensions at the time and tried a quick implementation within a week while developing the rest of it. |
@KnightYoshi Try it out in v0.13.0 and let me know what you think :) Here are the docs for setting them up: https://wxt.dev/guide/storage.html#versioning-and-migrations |
Feature Request
The storage APIs are very hard to use, so WXT should provide a standard storage API that's much simpler to use.
Here's an idea for the implementation using
unstorage
:I might try and submit
webExtensionDriver
tounstorage
in a PR. I know the storage listener setup isn't optimized, but it works for a basic setup. I wouldn't add the listener until its needed in WXT's implementation.Usage would looks something like this:
Is your feature request related to a bug?
Related to #227
What are the alternatives?
Here are some alternative storage libraries:
localStorage
-esk APIs for chrome extensionsThe text was updated successfully, but these errors were encountered: