1
\$\begingroup\$

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?

BCdotWEB
11.4k2 gold badges28 silver badges45 bronze badges
asked Jun 17, 2023 at 13:24
\$\endgroup\$
2
  • \$\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\$ Commented Jun 20, 2023 at 11:35
  • \$\begingroup\$ Interesting take \$\endgroup\$ Commented Jun 22, 2023 at 4:22

0

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.