Features
Persistent storage

Persistent storage

You can create a persistent storage and use it only where you want.

Creating a storage

Create a storage either at top-level...

import { Storage } from "@hazae41/storage"
 
const storage = IDBStorage.create("mydb")
 
function getHelloSchema(storage?: Storage) {
  return getSchema<Hello>("/api/hello", fetchAsJson, { storage: { storage } })
}
 
function useHelloMix() {
  const query = useSchema(getHelloSchema, [storage])
  useAutoFetchMixture(query)
  return query
}

...or using a hook.

import { Storage } from "@hazae41/storage"
 
function getHelloSchema(storage?: Storage) {
  return getSchema("/api/hello", fetchAsJson, { storage: { storage } })
}
 
function useHelloMix() {
  const storage = useIDBStorage("mydb")
  const query = useSchema(getHelloSchema, [storage])
  useAutoFetchMixture(query)
  return query
}

Types of storage

IDBStorage

  • Constructor: IDBStorage.create(name)
  • Hook (static): useIDBStorage(name)

This storage is asynchronous and uses the browser IndexedDB.

IDBStorage is the best storage you can use:

  • it's compatible with SSR
  • it can store very large objects with complex graphs
  • it doesn't need a serializer

Warning: Being asynchronous allows the use of SSR, but it won't display data on first render.

If you want to display data on first render, you can do so by using either:

  • SyncLocalStorage
  • useFallback()

AsyncLocalStorage

  • Constructor: AsyncLocalStorage.create(prefix?, serializer?)
  • Hook (static): useAsyncLocalStorage(prefix?, serializer?)

This storage is asynchronous and uses the browser LocalStorage.

Being asynchronous allows the use of SSR, but it won't display data on first render.

If you want to display data on first render, you can do so by using either:

  • SyncLocalStorage
  • useFallback()

You can set a custom prefix for all keys, by default it's xswr:.

You can pass a custom serializer (default is JSON) for serializing values (keys are already serialized by the Params serializer).

SyncLocalStorage

  • Constructor: SyncLocalStorage.create(prefix?, serializer?)
  • Hook (static): useSyncLocalStorage(prefix?, serializer?)

This storage is synchronous and uses the browser LocalStorage.

Being synchronous allows displaying data on first render, but it won't work with SSR.

You can set a custom prefix for all keys, by default it's xswr:.

You can pass a custom serializer (default is JSON) for serializing values (keys are already serialized by the Params serializer).

Hashing and encryption

In order to hide sensitive data from being seen in the storage, you can hash keys and encrypt values.

Using a password

You can easily create a PBKDF2 from a password, with recommended parameters.

import { PBKDF2 } from "@hazae41/xswr" 
 
const pbkdf2 = await PBKDF2.from(password)

Hashing keys

You can define a hash function as the key serializer, if the keys contain sensitive data.

From a HMAC key

import { HmacEncoder } from "@hazae41/xswr" 
 
const storage = IDBStorage.create("cache")
const keySerializer = new HmacEncoder(key)
 
return { storage, keySerializer }

From PBKDF2

You have to generate a public, random, and static salt (Uint8Array), see example below.

import { HmacEncoder } from "@hazae41/xswr" 
 
const storage = IDBStorage.create("cache")
const keySerializer = await HmacEncoder.fromPBKDF2(pbkdf2, salt)
 
return { storage, keySerializer }

Encrypting values

You can define an encryption function as the value serializer, if the values contain sensitive data.

From an AES-256 key

import { AesGcmCoder } from "@hazae41/xswr" 
 
const storage = IDBStorage.create("cache")
const valueSerializer = new AesGcmCoder(key)
 
return { storage, valueSerializer }

From PBKDF2

You have to generate a public, random, and static salt (Uint8Array), see example below.

import { AesGcmCoder } from "@hazae41/xswr" 
 
const storage = IDBStorage.create("cache")
const valueSerializer = await AesGcmCoder.fromPBKDF2(pbkdf2, salt)
 
return { storage, valueSerializer }

Full example of hashing and encryption

This examples use a user-given password and two random salts to store sensitive data, keys are hashed using HMAC-SHA-256 and values are encrypted using AES-256-GCM.

  • We use a user-given password as a PBKDF2 seed, it will create hashing and encryption keys from that password.
  • We generate two random salts and we store them, one is for the keys, the other is for the values.
  • We store "/api/hello" in a IndexedDB storage using the hashing and encryption.
/**
 * Load the salt from localStorage if it exists, else generate a random salt and store it
 **/
function getOrCreateSalt(key: string): Uint8Array {
  const item = localStorage.getItem(key)
 
  if (item)
    return Bytes.fromBase64(item)
 
  const salt = Bytes.random(16)
  localStorage.setItem(key, Bytes.toBase64(salt))
  return salt
}
 
function useStorage(password?: string) {
  return useMemo(() => {
    if (!password) return
 
    const storage = IDBStorage.create("cache")
    const pbkdf2 = await PBKDF2.from(password)
 
    const keySalt = getOrCreateSalt("keySalt")
    const valueSalt = getOrCreateSalt("valueSalt")
 
    const keySerializer = await HmacEncoder.fromPBKDF2(pbkdf2, keySalt)
    const valueSerializer = await AesGcmCoder.fromPBKDF2(pbkdf2, valueSalt)
 
    return { storage, keySerializer, valueSerializer }
  }, [password])
}
 
function getHello(storage?: StorageQueryParams<any>) {
  return getSchema<Hello>("/api/hello", fetchAsJson, { storage })
}
 
function useHello(storage?: StorageQueryParams<any>) {
  const query = useSchema(getHello, [storage])
  useFetch(query)
  return query
}
 
function Page() {
  const [password, setPassword] = useState<string>()
 
  const storage = useStorage(password)
  const { data, error } = useHello(storage)
 
  ...
}