ripeworks / local-storage-fallback

Check and use appropriate storage adapter for browser (localStorage, sessionStorage, cookies, memory)
MIT License
109 stars 19 forks source link

Emit StorageEvents from CookieStorage and MemoryStorage #37

Open Sjlver opened 6 months ago

Sjlver commented 6 months ago

When modifying the browser's LocalStorage or SessionStorage, the browser emits a storage event. Other tabs viewing the same page can use this event to update their state.

It would be great if CookieStorage and MemoryStorage could emit the same event. That would make them more compatible with the storage providers that they are emulating.

More info: https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event

tamagokun commented 1 month ago

I'd like to do this, but it seems like the event only fires in other browser contexts, and not the one where the change originated. I'm not exactly sure how to implement that.

Note: This won't work on the same browsing context that is making the changes — it is really a way for other browsing contexts on the domain using the storage to sync any changes that are made. Browsing contexts on other domains can't access the same storage objects.

Sjlver commented 1 month ago

Yes, that's true. I figure that an extra event is less harmful than a missing event though.

For what it's worth, here is the fallback that we've ended up using in our project. It just sends events for all changes.

// Our global singleton storage. This will point either to window.localStorage
// or a MemoryStorage instance.
let storage: Storage | undefined = undefined;

// Returns window.localStorage if available, and an in-memory fallback
// otherwise.
export const getStorage = () => {
  if (storage !== undefined) {
    return storage;
  }

  if (hasLocalStorage()) {
    storage = window.localStorage;
    return storage;
  }

  storage = new MemoryStorage();
  return storage;
};

// Local storage check, taken from Modernizr:
// https://github.com/Modernizr/Modernizr/blob/master/feature-detects/storage/localstorage.js
const hasLocalStorage = () => {
  const key = "x";
  try {
    localStorage.setItem(key, key);
    localStorage.removeItem(key);
    return true;
  } catch (e) {
    return false;
  }
};

// MemoryStorage is a fallback for window.localStorage.
class MemoryStorage implements Storage {
  private data: Record<string, string> = {};

  get length(): number {
    return Object.keys(this.data).length;
  }

  key(index: number): string | null {
    return Object.keys(this.data)[index] || null;
  }

  getItem(key: string): string | null {
    // biome-ignore lint/suspicious/noPrototypeBuiltins: Object.hasOwn would be preferred, but is not supported by old browsers
    return this.data.hasOwnProperty(key) ? this.data[key] : null;
  }

  setItem(key: string, value: string): void {
    const oldValue = this.getItem(key);
    if (value === oldValue) return;

    this.data[key] = value;
    window.dispatchEvent(
      // Emit a storage event. In principle, this should have a `storageArea:
      // this` property, but adding that property causes an error in Firefox.
      new StorageEvent("storage", {
        key: key,
        newValue: value,
        oldValue: oldValue,
        url: window.location.href,
      }),
    );
  }

  removeItem(key: string): void {
    const oldValue = this.getItem(key);
    if (oldValue === null) return;

    delete this.data[key];
    window.dispatchEvent(
      new StorageEvent("storage", {
        key: key,
        newValue: null,
        oldValue: oldValue,
        url: window.location.href,
      }),
    );
  }

  clear(): void {
    this.data = {};
    window.dispatchEvent(
      new StorageEvent("storage", {
        key: null,
        newValue: null,
        oldValue: null,
        url: window.location.href,
      }),
    );
  }
}

To use this, write something like const foo = getStorage().getItem("foo")

tamagokun commented 1 month ago

yeah, I think that's fair. Also, I read that sessionStorage does emit a storage event in the current browsing context. I'll get this added in.