#Best practices for customizing localStorageCollectionOptions in Browser Extensions?

3 messages · Page 1 of 1 (latest)

cedar current
#

Hi everyone! I'm using TanStack DB in a Chrome Extension environment. Since browser.storage.local is asynchronous, I've implemented a custom storage adapter with a memory cache and an event bridge to make it compatible with the synchronous requirements of localStorageCollectionOptions.

Here is my current implementation strategy:

Initial Hydration: I await browser.storage.local.get during initialization to populate a memory cache object.

Synchronous Adapter: The extensionStorage object (passed to the storage option) reads/writes to this cache synchronously while firing fire-and-forget async updates to the real storage.

Event Bridge: I use browser.storage.onChanged to intercept changes from other contexts (background/popup) and manually trigger the handler by mocking a StorageEvent object.

While this works and successfully triggers useLiveQuery across different tabs/popups, mocking StorageEvent and manual cache management feels a bit hacky.

Is there a recommended "best practice" or a more idiomatic way to handle non-window-localStorage environments in TanStack DB? Specifically, I'd love to know if there's a cleaner way to implement storageEventApi without so much type casting (as any) or event mocking.

Thanks in advance!

#
let collection: Collection<Todo, string> | null = null

const cache: Record<string, string | null> = {}

const extensionStorage: StorageApi = {
  getItem: (key: string) => {
    return cache[key] as string | null
  },
  setItem: (key: string, value: string) => {
    cache[key] = value
    browser.storage.local.set({ [key]: value })
  },
  removeItem: (key: string) => {
    delete cache[key]
    browser.storage.local.remove(key)
  }
}

const extensionStorageEventApi = {
  addEventListener: (type: string, handler: (e: any) => void) => {
    if (type !== 'storage') return

    const listener = (changes: Record<string, any>, areaName: string) => {
      if (areaName === 'local' && changes[STORAGE_KEY]) {
        const nextValue = changes[STORAGE_KEY].newValue
        console.log('nextValue', nextValue)
        cache[STORAGE_KEY] = nextValue

        handler({
          key: STORAGE_KEY,
          newValue: nextValue,
          storageArea: extensionStorage
        })
      }
    }

    browser.storage.onChanged.addListener(listener)
    return () => browser.storage.onChanged.removeListener(listener)
  }
}

const initializeCache = async () => {
  const result = await browser.storage.local.get(STORAGE_KEY)
  cache[STORAGE_KEY] = (result[STORAGE_KEY] as string) || null
}

export async function getTodoCollection(): Promise<Collection<Todo, string>> {
  if (collection) return collection

  await initializeCache()

  collection = createCollection(
    localStorageCollectionOptions({
      id: 'todos',
      storageKey: STORAGE_KEY,
      getKey: (item: Todo) => item.id,
      storage: extensionStorage,
      storageEventApi: extensionStorageEventApi as unknown as StorageEventApi
    })
  ) as Collection<Todo, string>

  return collection
}
cunning minnow
#

seems fine to me — why does it feel hacky? localStorageCollectionOptions is meant to be extended like this