Skip to content

Commit

Permalink
Initial implementation (#2)
Browse files Browse the repository at this point in the history
* 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
zoontek and bloodyowl authored May 9, 2023
1 parent e2730e5 commit f3deee7
Show file tree
Hide file tree
Showing 22 changed files with 1,835 additions and 353 deletions.
6 changes: 3 additions & 3 deletions .eslintignore
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
9 changes: 9 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@ module.exports = {
es2022: true,
},

overrides: [
{
files: ["*.d.ts"],
rules: {
"@typescript-eslint/consistent-type-definitions": "off",
},
},
],

rules: {
curly: "error",
"no-implicit-coercion": "error",
Expand Down
9 changes: 2 additions & 7 deletions .github/workflows/test.yml → .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: End-to-end tests
name: Tests

on:
push:
Expand All @@ -8,6 +8,7 @@ on:

jobs:
test:
name: Tests
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
Expand All @@ -21,9 +22,3 @@ jobs:
run: yarn test:install-deps
- name: Run tests
run: yarn test:ci
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

# testing
/coverage
/playwright-report

# production
/dist
Expand Down
85 changes: 84 additions & 1 deletion README.md
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)
8 changes: 0 additions & 8 deletions __tests__/title.spec.ts

This file was deleted.

25 changes: 16 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,12 @@
"typecheck": "tsc --noEmit",
"lint": "eslint --ext ts ./src",
"test:install-deps": "playwright install-deps chromium firefox webkit",
"test": "playwright test",
"test:ci": "CI=true playwright test",
"prepack": "yarn typecheck && yarn lint && yarn test && yarn build"
"test:chromium": "BROWSER=chromium vitest run",
"test:firefox": "BROWSER=firefox vitest run",
"test:webkit": "BROWSER=webkit vitest run",
"test": "yarn test:chromium && yarn test:firefox && yarn test:webkit",
"test:ci": "CI=true yarn test",
"prepack": "yarn typecheck && yarn lint && yarn test:chromium && yarn build"
},
"browserslist": [
">0.2%",
Expand All @@ -52,15 +55,19 @@
"prettier": {
"trailingComma": "all"
},
"dependencies": {
"@swan-io/boxed": "^1.0.1"
},
"devDependencies": {
"@playwright/test": "^1.32.3",
"@typescript-eslint/eslint-plugin": "^5.59.0",
"@typescript-eslint/parser": "^5.59.0",
"eslint": "^8.39.0",
"@typescript-eslint/eslint-plugin": "^5.59.5",
"@typescript-eslint/parser": "^5.59.5",
"@vitest/browser": "^0.31.0",
"eslint": "^8.40.0",
"microbundle": "^0.15.1",
"playwright": "^1.32.3",
"playwright": "^1.33.0",
"prettier": "^2.8.8",
"prettier-plugin-organize-imports": "^3.2.2",
"typescript": "^5.0.4"
"typescript": "^5.0.4",
"vitest": "^0.31.0"
}
}
34 changes: 0 additions & 34 deletions playwright.config.ts

This file was deleted.

59 changes: 59 additions & 0 deletions src/errors.ts
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;
};
60 changes: 60 additions & 0 deletions src/factory.ts
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();
});
};
Loading

0 comments on commit f3deee7

Please sign in to comment.