-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Ignore test-results directory * Add boxed * WIP * WIP retry * WIP * Try to open the database 5 times * Cleanup Safari fix * Extract userAgent tests * Rename transformError * Extract futurify* functions * Rename safari file to userAgent * Add InvalidStateError handling * Add retry on all operations * Use IDBObjectStore.getAll, as support is good enough * Add timeout on all requests / transactions * Add request / transaction * Add inMemoryStore usage * Prefer Result<void, Error> * Add skipLibCheck * Rename variable * Remove unused file * Rollback exactOptionalPropertyTypes change * Remove eslint overrides * Use Dict * Make getMany return an object * Setup vitest for unit tests (#3) * Rename .spec.ts -> .test.ts * test -> tests * Handle cancellation * Remove function * Improve wording * Switch to safari-14-idb-fix logic * Add cancelPropagation * Update boxed (#4) * Remove skipLibCheck * Update boxed * Remove cancellation handling * Remove aboting from outside * Remove newline * Use futurifyRequest on DB open * Update deps * Add retry unit tests * Copy original error stack * Switch to DOMException + add errors unit tests * Add indexedDBReady tests * Don't setup indexedDB by default in tests * Re-order stubs * Replace indexedDBReady with getIndexedDBFactory * Add basic in-memory store test * Don't use getAll as it doesn't achieve we want * Don't use Option as get returns undefined when the data doesn't exists (and we don't want to add an extra call to determine value existence) * Once a write fail, we should only use inMemoryStore * Delete database when something wrong happen in last session * WIP database cleaning * Rename helper * Clear store instead of deleting database * Rename useInMemoryStore variable * readFromMemoryStore -> readFromInMemoryStore * Rename fns * Move retry to helpers * Update dependencies * A basic inMemoryStore test * Fix test * Switch to inMemory if database open failed * Once from the in-memory store once one read failed * Don't put item multiple times in localStorage * Only read when clear fail * Create an inMemory store per databaseName + storeName * Simplify in memory database / store creation logic * Add test * Add onError option * Rename variable * Switch to inMemoryStore when database cannot open * Check that store is flag as clearable * Implement review feedbacks * Remove eslint-disable-line no-empty usage * Add missing onError * Only use in-memory store on failed get * Add allowInMemoryFallback option * Update vitest * Switch to vitest in browser mode * Add Github workflow name * Fix badge url * Improve inMemoryFallback logic * Add basic API config * Add acknowledgements * Make retries and timeout configurable * Remove fake-indexeddb * Fix typo * Update databaseName / storeName * Add mention about multiple names * Update eslint --------- Co-authored-by: Matthias Le Brun <[email protected]>
- Loading branch information
Showing
22 changed files
with
1,835 additions
and
353 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
__tests__/ | ||
dist/ | ||
playwright.config.ts | ||
.eslintrc.js | ||
dist/ | ||
tests/ | ||
vite.config.ts |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,7 +5,6 @@ | |
|
||
# testing | ||
/coverage | ||
/playwright-report | ||
|
||
# production | ||
/dist | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,86 @@ | ||
# @swan-io/indexed-db | ||
|
||
[![End-to-end tests](https://github.com/swan-io/indexed-db/actions/workflows/test.yml/badge.svg)](https://github.com/swan-io/indexed-db/actions/workflows/test.yml) | ||
[![Tests](https://github.com/swan-io/indexed-db/actions/workflows/tests.yml/badge.svg)](https://github.com/swan-io/indexed-db/actions/workflows/tests.yml) | ||
[![mit licence](https://img.shields.io/dub/l/vibe-d.svg)](https://github.com/swan-io/indexed-db/blob/main/LICENSE) | ||
[![npm version](https://img.shields.io/npm/v/@swan-io/indexed-db)](https://www.npmjs.org/package/@swan-io/indexed-db) | ||
[![bundlephobia](https://img.shields.io/bundlephobia/minzip/@swan-io/indexed-db?label=size)](https://bundlephobia.com/result?p=@swan-io/indexed-db) | ||
|
||
A resilient, [Future](https://swan-io.github.io/boxed/future)-based key-value store for IndexedDB. | ||
|
||
## Installation | ||
|
||
```sh | ||
$ yarn add @swan-io/indexed-db | ||
# --- or --- | ||
$ npm install --save @swan-io/indexed-db | ||
``` | ||
|
||
## Quickstart | ||
|
||
```ts | ||
const store = openStore("myDatabaseName", "myStoreName"); | ||
|
||
store | ||
.setMany({ | ||
firstName: "Mathieu", | ||
lastName: "Breton", | ||
}) | ||
.flatMapOk(() => store.getMany(["firstName", "lastName"])) | ||
.flatMapOk(({ firstName, lastName }) => { | ||
console.log({ | ||
firstName, | ||
lastName, | ||
}); | ||
|
||
return store.clear(); | ||
}) | ||
.tapOk(() => { | ||
console.log("✅"); | ||
}); | ||
``` | ||
|
||
## API | ||
|
||
Open a database, create a store if needed and returns methods to manipulate it.<br/> | ||
Note that you can open multiple databases / stores, with different names. | ||
|
||
```ts | ||
const store = await openStore("myDatabaseName", "myStoreName", { | ||
enableInMemoryFallback: true, // keep data in-memory in cases of read failures (default: false) | ||
transactionRetries: 3, // retry failed transactions (default: 3) | ||
transactionTimeout: 300, // timeout a transaction when it takes too long (default: 300ms) | ||
}); | ||
``` | ||
|
||
### store.getMany | ||
|
||
Get many values at once. Resolves with a record. | ||
|
||
```ts | ||
store | ||
.getMany(["firstName", "lastName"]) | ||
.mapOk(({ firstName, lastName }) => console.log({ firstName, lastName })); | ||
``` | ||
|
||
### store.setMany | ||
|
||
Get many key-value pairs at once. | ||
|
||
```ts | ||
store | ||
.setMany({ firstName: "Mathieu", lastName: "Breton" }) | ||
.tapOk(() => console.log("✅")); | ||
``` | ||
|
||
### store.clear | ||
|
||
Clear all values in the store. | ||
|
||
```ts | ||
store.clear().tapOk(() => console.log("✅")); | ||
``` | ||
|
||
## 🙌 Acknowledgements | ||
|
||
- [firebase-js-sdk](https://github.com/firebase/firebase-js-sdk) by [@firebase](https://github.com/firebase) | ||
- [idb-keyval](https://github.com/jakearchibald/idb-keyval) and [safari-14-idb-fix](https://github.com/jakearchibald/safari-14-idb-fix) by [@jakearchibald](https://github.com/jakearchibald) |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import { Lazy } from "@swan-io/boxed"; | ||
|
||
// https://github.com/firebase/firebase-js-sdk/blob/firebase%409.20.0/packages/firestore/src/local/simple_db.ts#L241 | ||
const iOSVersion = Lazy(() => { | ||
const versionMatch = navigator.userAgent.match( | ||
/i(?:phone|pad|pod) os ([\d_]+)/i, | ||
)?.[1]; | ||
|
||
return versionMatch != null | ||
? Number(versionMatch.split("_").slice(0, 2).join(".")) | ||
: -1; | ||
}); | ||
|
||
const deriveError = ( | ||
originalError: DOMException, | ||
newMessage: string, | ||
): DOMException => { | ||
const newError = new DOMException(newMessage, originalError.name); | ||
|
||
if (originalError.stack != null) { | ||
newError.stack = originalError.stack; | ||
} | ||
|
||
return newError; | ||
}; | ||
|
||
export const rewriteError = (error: DOMException | null): DOMException => { | ||
if (error == null) { | ||
return new DOMException("Unknown IndexedDB error", "UnknownError"); | ||
} | ||
|
||
// https://github.com/firebase/firebase-js-sdk/blob/firebase%409.20.0/packages/firestore/src/local/simple_db.ts#L915 | ||
if (iOSVersion.get() >= 12.2 && iOSVersion.get() < 13) { | ||
const IOS_ERROR = | ||
"An internal error was encountered in the Indexed Database server"; | ||
|
||
if (error.message.indexOf(IOS_ERROR) >= 0) { | ||
return deriveError( | ||
error, | ||
`IndexedDB has thrown '${IOS_ERROR}'. ` + | ||
`This is likely due to an unavoidable bug in iOS ` + | ||
`(https://bugs.webkit.org/show_bug.cgi?id=197050).`, | ||
); | ||
} | ||
} | ||
|
||
// https://github.com/firebase/firebase-js-sdk/blob/firebase%409.20.0/packages/firestore/src/local/simple_db.ts#L335 | ||
if (error.name === "InvalidStateError") { | ||
return deriveError( | ||
error, | ||
`Unable to open an IndexedDB connection. ` + | ||
`This could be due to running in a private browsing ` + | ||
`session on a browser whose private browsing ` + | ||
`sessions do not support IndexedDB: ${error.message}`, | ||
); | ||
} | ||
|
||
return error; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import { Future, Result } from "@swan-io/boxed"; | ||
|
||
/** | ||
* Safari has a horrible bug where IndexedDB requests can hang forever. | ||
* We resolve this future with error after 100ms if it seems to happen. | ||
* @see https://bugs.webkit.org/show_bug.cgi?id=226547 | ||
* @see https://github.com/jakearchibald/safari-14-idb-fix | ||
*/ | ||
export const getIndexedDBFactory = (): Future< | ||
Result<IDBFactory, DOMException> | ||
> => { | ||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition | ||
if (indexedDB == null) { | ||
return Future.value( | ||
Result.Error( | ||
new DOMException("indexedDB global doesn't exist", "UnknownError"), | ||
), | ||
); | ||
} | ||
|
||
const isSafari = | ||
!navigator.userAgentData && | ||
/Safari\//.test(navigator.userAgent) && | ||
!/Chrom(e|ium)\//.test(navigator.userAgent); | ||
|
||
// No point putting other browsers or older versions of Safari through this mess. | ||
if (!isSafari || !("databases" in indexedDB)) { | ||
return Future.value(Result.Ok(indexedDB)); | ||
} | ||
|
||
let intervalId: NodeJS.Timer; | ||
let remainingAttempts = 10; | ||
|
||
return Future.make((resolve) => { | ||
const tryToAccessIndexedDB = () => { | ||
remainingAttempts = remainingAttempts - 1; | ||
|
||
if (remainingAttempts > 0) { | ||
indexedDB.databases().finally(() => { | ||
clearInterval(intervalId); | ||
resolve(Result.Ok(indexedDB)); | ||
}); | ||
} else { | ||
clearInterval(intervalId); | ||
|
||
resolve( | ||
Result.Error( | ||
new DOMException( | ||
"Couldn't list IndexedDB databases", | ||
"TimeoutError", | ||
), | ||
), | ||
); | ||
} | ||
}; | ||
|
||
intervalId = setInterval(tryToAccessIndexedDB, 100); | ||
tryToAccessIndexedDB(); | ||
}); | ||
}; |
Oops, something went wrong.