Here's some background information: I was working on a Chrome extension called Netflix Hotkeys (you can find it here), and I realized that I needed a persistent storage solution for storing user preferences. Initially, I had a lot of boilerplate code in my storage implementation for each preference, including keys, getters, setters, and more (you can see the old storage implementation here). However, I discovered that I could simplify the process by introducing a lower Storage layer/manager/parent class which will handle all the boilerplate code dynamically and expose simple API to update the data.
Essentially, My goal was to create a super simple API for consumers to interact with Chrome storage. Since it was intended for a simple Chrome extension, efficiency and complexity were not major concerns for me.
Here's the Storage manager:
/**
* A Storage class which creates a super simple API to talk with chrome storage.
*/
class Storage {
/**
* Marks the property as storage field.
*
* @param {Object} props
* @param {any} props.fallback The fallback value of the field if no value is found in chrome storage.
*/
field(props = {}) {
return {
field: true,
fallback: props.fallback ?? null,
}
}
/** Shadow storage which maintains the values of the fields. */
#shadowStorage = {}
/**
* values of the fields.
*/
get values() {
return this.#shadowStorage
}
/** Builds the storage class. */
#build() {
// Populate the shadow storage with the field values.
const fields = Object.keys(this).filter((k) => this[k].field === true)
fields.forEach((f) => (this.#shadowStorage[f] = this[f].fallback ?? null))
const updateFieldAndNotify = (field, newValue) => {
if (newValue === this[field]) {
return
}
this.#shadowStorage[field] = newValue
chrome.storage.local.set({ [field]: newValue })
this.#emit(field)
}
// Initialize values from chrome storage.
chrome.storage.local.get(fields, (result) => {
fields.forEach((field) => {
updateFieldAndNotify(field, result[field] ?? this.#shadowStorage[field])
})
})
// Listen for changes to values.
chrome.storage.onChanged.addListener((changes, namespace) => {
for (const field in changes) {
updateFieldAndNotify(field, changes[field].newValue)
}
})
// Override the getter's and setter's of the field.
fields.forEach((field) => {
Object.defineProperty(this, field, {
get: function () {
return this.#shadowStorage[field]
},
set: function (newValue) {
updateFieldAndNotify(field, newValue)
},
})
})
}
/** The singleton instance */
static #instance = null
/**
* The singleton instance of the Storage.
*
* NOTE: Override in child class to enable Auto Complete and intellisense.
*
* @type {Storage}
*/
static get instance() {
if (!Storage.#instance) {
const subClassStorage = new this()
subClassStorage.#build()
Storage.#instance = subClassStorage
}
return Storage.#instance
}
/**
* Resets the singleton instance.
*
* WARN: ONLY EXPOSED FOR TESTING. SHOULD NOT BE USED IN THE NON-TESTING CODE.
*/
static reset() {
Storage.#instance = null
}
/** Listeners for fields */
#listeners = {}
/**
* Listen to changes of a field.
*
* @param {String} fieldToListen
* @param {Function} callback function to be called when the field changes.
*/
on(fieldToListen, callback) {
if (!this.#listeners[fieldToListen]) {
this.#listeners[fieldToListen] = []
}
this.#listeners[fieldToListen].push(callback)
}
/**
* Remove a listener from a field.
* @param {String} fieldToListen
* @param {Function} callback function to be removed from the listeners.
* @returns
*/
off(fieldToListen, callback) {
if (!this.#listeners[fieldToListen]) {
return
}
this.#listeners[fieldToListen] = this.#listeners[fieldToListen].filter((c) => c !== callback)
}
/** Emit a change to the listeners of a field. */
#emit(changedField) {
if (!this.#listeners[changedField]) {
return
}
for (const callback of this.#listeners[changedField]) {
callback()
}
}
}
It's incredibly easy to use. You just need to subclass it and add the properties that you want to manipulate in Chrome storage. Here's an example:
class Demo extends Storage {
x = this.field({ fallback: 0 })
y = this.field({ fallback: 0 })
}
// Update
Demo.instance.x = 10
// Get
console.log(Demo.instance.x)
// Listen
Demo.instance.on('y', () => {})
Would this solution be suitable for a small project where scalability and efficiency are not major concerns? If so, do you think it would be beneficial to create a separate library for this purpose?
-
\$\begingroup\$ If this class does what you need it to do, then that's great. But it does seem like a lot of boilerplate code (including a shadow administration and a singleton class), just to rewrite the examples that are already there on the google documentation. In the end, this might cause you more work if you need to add something. \$\endgroup\$Kokodoko– Kokodoko2023年06月20日 11:35:58 +00:00Commented Jun 20, 2023 at 11:35
-
\$\begingroup\$ Interesting take \$\endgroup\$Kunal Kamble– Kunal Kamble2023年06月22日 04:22:44 +00:00Commented Jun 22, 2023 at 4:22
You must log in to answer this question.
Explore related questions
See similar questions with these tags.