| 1 | import * as BrowserKeyValueStore from "@effect/platform-browser/BrowserKeyValueStore" |
| 2 | import * as IndexedDb from "@effect/platform-browser/IndexedDb" |
| 3 | import { assert, describe, it } from "@effect/vitest" |
| 4 | import { Layer } from "effect" |
| 5 | import { testLayer } from "effect-test/persistence/KeyValueStore.test" |
| 6 | import * as Effect from "effect/Effect" |
| 7 | import * as KeyValueStore from "effect/persistence/KeyValueStore" |
| 8 | import * as Result from "effect/Result" |
| 9 | import { IDBKeyRange, indexedDB } from "fake-indexeddb" |
| 10 | |
| 11 | describe("KeyValueStore / layerLocalStorage", () => testLayer(BrowserKeyValueStore.layerLocalStorage)) |
| 12 | |
| 13 | describe("KeyValueStore / layerSessionStorage", () => testLayer(BrowserKeyValueStore.layerSessionStorage)) |
| 14 | |
| 15 | describe("KeyValueStore / layerIndexedDb", () => { |
| 16 | const layerFakeIndexedDb = Layer.succeed( |
| 17 | IndexedDb.IndexedDb, |
| 18 | IndexedDb.make({ indexedDB, IDBKeyRange }) |
| 19 | ) |
| 20 | |
| 21 | testLayer( |
| 22 | BrowserKeyValueStore.layerIndexedDb({ database: "kvs_test_db" }).pipe( |
| 23 | Layer.provide(layerFakeIndexedDb) |
| 24 | ) |
| 25 | ) |
| 26 | |
| 27 | it.effect("reports a key containing bytes as present", () => { |
| 28 | const layer = BrowserKeyValueStore.layerIndexedDb({ |
| 29 | database: `kvs_binary_has_${crypto.randomUUID()}` |
| 30 | }).pipe(Layer.provide(layerFakeIndexedDb)) |
| 31 | |
| 32 | return Effect.gen(function*() { |
| 33 | const store = yield* KeyValueStore.KeyValueStore |
| 34 | yield* store.set("binary", new Uint8Array([0, 127, 255])) |
| 35 | assert.isTrue(yield* store.has("binary")) |
| 36 | }).pipe(Effect.provide(layer)) |
| 37 | }) |
| 38 | |
| 39 | it.effect("does not report a write before its transaction commits", () => { |
| 40 | const db = { |
| 41 | objectStoreNames: { contains: () => true }, |
| 42 | close() {}, |
| 43 | transaction() { |
| 44 | const transaction = { |
| 45 | error: null as unknown, |
| 46 | onabort: null as null | (() => void), |
| 47 | objectStore() { |
| 48 | return { |
| 49 | put() { |
| 50 | const request = { |
| 51 | readyState: "pending", |
| 52 | result: undefined, |
| 53 | error: null, |
| 54 | onsuccess: null as null | (() => void), |
| 55 | onerror: null as null | (() => void) |
| 56 | } |
| 57 | queueMicrotask(() => { |
| 58 | request.readyState = "done" |
| 59 | request.onsuccess?.() |
| 60 | transaction.error = new DOMException("Commit failed", "AbortError") |
| 61 | transaction.onabort?.() |
| 62 | }) |
| 63 | return request |
| 64 | } |
| 65 | } |
| 66 | } |
| 67 | } |
| 68 | return transaction |
| 69 | } |
| 70 | } |
| 71 | const failingIndexedDb = { |
| 72 | open() { |
| 73 | const request = { |
| 74 | readyState: "pending", |
| 75 | result: undefined as unknown, |
| 76 | error: null, |
| 77 | onsuccess: null as null | (() => void), |
| 78 | onerror: null as null | (() => void), |
| 79 | onupgradeneeded: null as null | (() => void) |
| 80 | } |
| 81 | queueMicrotask(() => { |
| 82 | request.readyState = "done" |
| 83 | request.result = db |
| 84 | request.onsuccess?.() |
| 85 | }) |
| 86 | return request |
| 87 | } |
| 88 | } |
| 89 | const layer = BrowserKeyValueStore.layerIndexedDb({ database: "transaction_repro" }).pipe( |
| 90 | Layer.provide(Layer.succeed( |
| 91 | IndexedDb.IndexedDb, |
| 92 | IndexedDb.make({ indexedDB: failingIndexedDb as unknown as IDBFactory, IDBKeyRange }) |
| 93 | )) |
| 94 | ) |
| 95 | |
| 96 | return Effect.gen(function*() { |
| 97 | const store = yield* KeyValueStore.KeyValueStore |
| 98 | const result = yield* Effect.result(store.set("key", "value")) |
| 99 | yield* Effect.yieldNow |
| 100 | assert.isTrue(Result.isFailure(result), "the aborted transaction was reported as successful") |
| 101 | }).pipe(Effect.provide(layer)) |
| 102 | }) |
| 103 | }) |