178 lines
6.4 KiB
TypeScript
178 lines
6.4 KiB
TypeScript
/**
|
|
* Browser-backed `KeyValueStore` layers for client-side Effect programs.
|
|
*
|
|
* This module provides browser implementations of the unstable persistence
|
|
* `KeyValueStore` service. Use {@link layerLocalStorage} for small
|
|
* origin-scoped values that should survive reloads and browser restarts, use
|
|
* {@link layerSessionStorage} for tab / page-session state, and use
|
|
* {@link layerIndexedDb} when the store should be asynchronous and backed by
|
|
* IndexedDB. The IndexedDB layer requires the browser `IndexedDb` service and
|
|
* accepts an optional database name.
|
|
*
|
|
* @since 4.0.0
|
|
*/
|
|
import * as Effect from "effect/Effect"
|
|
import * as Layer from "effect/Layer"
|
|
import * as KeyValueStore from "effect/unstable/persistence/KeyValueStore"
|
|
import { IndexedDb } from "./IndexedDb.ts"
|
|
|
|
/**
|
|
* Creates a `KeyValueStore` layer that uses the browser's `localStorage` API and stores values between browser sessions.
|
|
*
|
|
* @category layers
|
|
* @since 4.0.0
|
|
*/
|
|
export const layerLocalStorage: Layer.Layer<KeyValueStore.KeyValueStore> = KeyValueStore.layerStorage(() =>
|
|
globalThis.localStorage
|
|
)
|
|
|
|
/**
|
|
* Creates a `KeyValueStore` layer that uses the browser's `sessionStorage` API and stores values only for the current session.
|
|
*
|
|
* @category layers
|
|
* @since 4.0.0
|
|
*/
|
|
export const layerSessionStorage: Layer.Layer<KeyValueStore.KeyValueStore> = KeyValueStore.layerStorage(() =>
|
|
globalThis.sessionStorage
|
|
)
|
|
|
|
/**
|
|
* Creates a `KeyValueStore` layer backed by IndexedDB.
|
|
*
|
|
* **When to use**
|
|
*
|
|
* Use when you need persistent asynchronous IndexedDB storage for a browser
|
|
* `KeyValueStore` instead of the synchronous Web Storage APIs.
|
|
*
|
|
* **Details**
|
|
*
|
|
* The database name defaults to `"effect_key_value_store"`. The layer requires
|
|
* the `IndexedDb` service and stores string and `Uint8Array` values in the same
|
|
* backing object store.
|
|
*
|
|
* **Gotchas**
|
|
*
|
|
* IndexedDB may be unavailable or blocked by browser settings, private browsing,
|
|
* quota limits, or restricted contexts. The string and `Uint8Array` accessors do
|
|
* not coerce values stored with the other representation.
|
|
*
|
|
* @see {@link layerLocalStorage} for synchronous persistent Web Storage
|
|
* @see {@link layerSessionStorage} for synchronous tab-session Web Storage
|
|
*
|
|
* @category layers
|
|
* @since 4.0.0
|
|
*/
|
|
export const layerIndexedDb = (options?: {
|
|
readonly database?: string | undefined
|
|
}): Layer.Layer<KeyValueStore.KeyValueStore, never, IndexedDb> =>
|
|
Layer.effect(KeyValueStore.KeyValueStore)(
|
|
Effect.gen(function*() {
|
|
const db = yield* Effect.acquireRelease(
|
|
openDatabase(options?.database ?? "effect_key_value_store"),
|
|
(db) => Effect.sync(() => db.close())
|
|
).pipe(Effect.orDie)
|
|
|
|
return KeyValueStore.make({
|
|
clear: Effect.suspend(() => {
|
|
const store = getKvsEntriesStore(db, "readwrite")
|
|
return idbRequest({ method: "clear", message: "Failed to clear backing store" }, () => store.clear())
|
|
}),
|
|
get: (key: string) =>
|
|
Effect.map(
|
|
Effect.suspend(() => {
|
|
const store = getKvsEntriesStore(db, "readonly")
|
|
return idbRequest<{ key: string; value: string } | undefined>({
|
|
method: "get",
|
|
message: "Failed to get value from backing store",
|
|
key
|
|
}, () => store.get(key))
|
|
}),
|
|
(found) => typeof found?.value === "string" ? found.value : undefined
|
|
),
|
|
getUint8Array: (key: string) =>
|
|
Effect.map(
|
|
Effect.suspend(() => {
|
|
const store = getKvsEntriesStore(db, "readonly")
|
|
return idbRequest<{ key: string; value: Uint8Array } | undefined>({
|
|
method: "getUint8Array",
|
|
message: "Failed to get value from backing store",
|
|
key
|
|
}, () => store.get(key))
|
|
}),
|
|
(found) => found?.value && found.value instanceof Uint8Array ? found.value : undefined
|
|
),
|
|
set: (key: string, value: string | Uint8Array) =>
|
|
Effect.asVoid(Effect.suspend(() => {
|
|
const store = getKvsEntriesStore(db, "readwrite")
|
|
return idbRequest(
|
|
{ method: "set", message: "Failed to set value in backing store", key },
|
|
() => store.put({ key, value })
|
|
)
|
|
})),
|
|
size: Effect.suspend(() => {
|
|
const store = getKvsEntriesStore(db, "readonly")
|
|
return idbRequest<number>(
|
|
{ method: "size", message: "Failed to get backing store size" },
|
|
() => store.count()
|
|
)
|
|
}),
|
|
remove: (key: string) =>
|
|
Effect.asVoid(Effect.suspend(() => {
|
|
const store = getKvsEntriesStore(db, "readwrite")
|
|
return idbRequest(
|
|
{ method: "remove", message: "Failed to remove value from backing store", key },
|
|
() => store.delete(key)
|
|
)
|
|
}))
|
|
})
|
|
})
|
|
)
|
|
|
|
const databaseVersion = 1
|
|
const entriesStoreName = "entries"
|
|
const openDatabase = Effect.fnUntraced(function*(database: string) {
|
|
const idb = (yield* IndexedDb).indexedDB
|
|
const openRequest = yield* Effect.try({
|
|
try: () => idb.open(database, databaseVersion),
|
|
catch: (cause) =>
|
|
new KeyValueStore.KeyValueStoreError({
|
|
method: "open",
|
|
message: "Failed to open backing store database",
|
|
cause
|
|
})
|
|
})
|
|
openRequest.onupgradeneeded = () => {
|
|
const db = openRequest.result
|
|
if (!db.objectStoreNames.contains(entriesStoreName)) {
|
|
db.createObjectStore(entriesStoreName, { keyPath: "key" })
|
|
}
|
|
}
|
|
return yield* idbRequest({ method: "open", message: "Failed to open backing store database" }, () => openRequest)
|
|
})
|
|
|
|
const idbRequest = <A>(
|
|
failArgs: { method: string; message: string; key?: string },
|
|
evaluate: () => IDBRequest<A>
|
|
): Effect.Effect<A, KeyValueStore.KeyValueStoreError> =>
|
|
Effect.callback<A, KeyValueStore.KeyValueStoreError>((resume) => {
|
|
const request = evaluate()
|
|
if (request.readyState === "done") {
|
|
return resume(Effect.succeed(request.result))
|
|
}
|
|
request.onsuccess = () => {
|
|
resume(Effect.succeed(request.result))
|
|
}
|
|
request.onerror = () =>
|
|
resume(Effect.fail(
|
|
new KeyValueStore.KeyValueStoreError({
|
|
...failArgs,
|
|
cause: request.error
|
|
})
|
|
))
|
|
})
|
|
|
|
const getKvsEntriesStore = (db: IDBDatabase, mode: IDBTransactionMode) => {
|
|
const transaction = db.transaction(entriesStoreName, mode)
|
|
return transaction.objectStore(entriesStoreName)
|
|
}
|