Search code examples
javascriptes6-classproxy-pattern

How to write a proper ES6 wrapper for something like sessionStorage


Hello I am wondering how to write a proper wrapper for something like sessionStorage.

I could implement a class of my own and proxy through method invocation to the sessionStorage. For example with some quick pseudocode:

class customStorage {
    set(key, value) {
        sessionStorage.setItem(key, value);
    }
}

I would use it like so:

const customStorage = new customStorage();
customStorage.set('my-key', 'hello there');

All fine and dandy but I would like for a user to have the freedom to use other native sessionStorage methods on my proxy that I might not implement myself in my proxy.

for something like sessionStorage it would be do-able to write them all yourself even though all they might do is proxy through to sessionStorage without any intervention.

For something larger where I would only manipulate 5 methods out of 20 or something this does not seem viable.

Overwriting native functionality with prototype also seems like a deathtrap leading to many wtfs-per-minute.

So far what I have read from 'proxy patterns' in Javascript they all implemented all of the methods from the original Object. Am I forced to do this?

Is there some sort of way to create an ES6 class and set the prototype of that very class to sessionStorage in the constructor or something?


Solution

  • I would like for a user to have the freedom to use other native sessionStorage methods on my proxy that I might not implement myself in my proxy.

    I would rather give the user the freedom to just use the native sessionStorage directly if he intends to do that. Your implementation does have its own, separate functionality, which does use sessionStorage internally but is not the sessionStorage. There's no reason to implement its interface on your object. (See also composition over inheritance).

    Is there some sort of way to create an ES6 class and set the prototype of that very class to sessionStorage in the constructor or something?

    No. Even when you want to implement that interface, your objects are not really SessionStorage instances. Also in this particular case, sessionStorage is a singleton and you cannot instantiate a second SessionStorage, so inheritance does absolutely not work here.

    There are three ways around this (I'll write code for the generic case with instantiation from arbitrary objects to be wrapped, you likely want a static singleton-like one for your custom storage):

    • Mixins to decorate the object. Don't create another instance, just overwrite the properties on the original. (This is likely out of the question for builtin objects)

      function custom(orig) {
          orig.get = function() { … };
          return orig;
      }
      
    • Parasitical inheritance, creating a complete wrapper using reflection on the object.

      function custom(orig) {
          const obj = {
              get() { … };
          };
          for (const p in orig) { // assuming everything is enumerable - alternatively use
                                  // for (const p of Object.getOwnPropertyNames(…))
                                  // or even incorporating the prototype chain
              obj[p] = typeof orig[p] == "function"
                ? (...args) => orig[p](...args)
                : orig[p];
          }
          return obj;
      }
      
    • A literal Proxy with a suitable handler:

      const customMethods = {
          get() { … }
      };
      const handler = {
          has(target, name) {
              return name in customMethods || name in target;
          },
          get(target, name) {
              if (name in customMethods) return customMethods[name];
              else return target[name];
              // if its a native object with methods that rely on `this`, you'll need to
              // return target[name].bind(target)
              // for function properties
          }
      }
      
      function custom(orig) {
          return new Proxy(orig, handler);
      }