Skip to content

Commit

Permalink
Re-open closed database when needed (#6)
Browse files Browse the repository at this point in the history
  • Loading branch information
zoontek authored Jun 2, 2023
1 parent 8608283 commit e5f44f7
Show file tree
Hide file tree
Showing 8 changed files with 201 additions and 205 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@swan-io/indexed-db",
"version": "0.1.1",
"version": "0.2.0",
"license": "MIT",
"description": "A resilient, Future-based key-value store for IndexedDB",
"author": "Mathieu Acthernoene <[email protected]>",
Expand Down
4 changes: 4 additions & 0 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,7 @@ export const rewriteError = (error: DOMException | null): DOMException => {

return error;
};

export const isDatabaseClosedError = (error: DOMException) =>
error.message.indexOf("The database connection is closing") >= 0 ||
error.message.indexOf("Can't start a transaction on a closed database") >= 0;
60 changes: 0 additions & 60 deletions src/factory.ts

This file was deleted.

67 changes: 6 additions & 61 deletions src/futurify.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { Future, Result } from "@swan-io/boxed";
import { rewriteError } from "./errors";

export const futurifyRequest = <T>(
export const futurify = <T>(
request: IDBRequest<T>,
operationName: string,
timeout: number,
): Future<Result<T, DOMException>> =>
Future.make((resolve) => {
Expand All @@ -20,66 +19,12 @@ export const futurifyRequest = <T>(
};

if (transaction != null) {
const resolveAfterAbort = () =>
resolve(
Result.Error(
new DOMException(
`${operationName} IndexedDB request timed out`,
"TimeoutError",
),
),
);

timeoutId = setTimeout(() => {
Result.fromExecution(() => {
transaction.abort();
}).tapError(() => {
resolveAfterAbort();
});
if (request.readyState !== "done") {
// Throws if the transaction has already been committed or aborted.
// Triggers onerror listener with an AbortError DOMException.
Result.fromExecution(() => transaction.abort());
}
}, timeout);

transaction.onabort = () => {
clearTimeout(timeoutId);
resolveAfterAbort();
};
}
});

export const futurifyTransaction = (
transaction: IDBTransaction,
operationName: string,
timeout: number,
): Future<Result<void, DOMException>> =>
Future.make((resolve) => {
transaction.oncomplete = () => {
clearTimeout(timeoutId);
resolve(Result.Ok(undefined));
};
transaction.onerror = () => {
clearTimeout(timeoutId);
resolve(Result.Error(rewriteError(transaction.error)));
};

const resolveAfterAbort = () =>
resolve(
Result.Error(
new DOMException(
`${operationName} IndexedDB transaction timed out`,
"TimeoutError",
),
),
);

const timeoutId = setTimeout(() => {
Result.fromExecution(() => {
transaction.abort();
}).tapError(() => {
resolveAfterAbort();
});
}, timeout);

transaction.onabort = () => {
clearTimeout(timeoutId);
resolveAfterAbort();
};
});
152 changes: 79 additions & 73 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { Dict, Future, Result } from "@swan-io/boxed";
import { rewriteError } from "./errors";
import { getIndexedDBFactory } from "./factory";
import { futurifyRequest, futurifyTransaction } from "./futurify";
import { futurify } from "./futurify";
import { retry, zipToObject } from "./helpers";
import { getInMemoryStore } from "./inMemoryStore";
import { getStore, openDatabase } from "./wrappers";

export const openStore = (
databaseName: string,
Expand All @@ -20,57 +19,52 @@ export const openStore = (
transactionTimeout = 500,
} = options;

const inMemoryStore = getInMemoryStore(databaseName, storeName);

const databaseFuture = getIndexedDBFactory().flatMapOk((factory) => {
const request = factory.open(databaseName);

request.onupgradeneeded = () => {
request.result.createObjectStore(storeName);
};
const databaseFuture = openDatabase(
databaseName,
storeName,
transactionTimeout,
);

return futurifyRequest(request, "openDatabase", transactionTimeout);
});

const getObjectStore = (
transactionMode: IDBTransactionMode,
): Future<Result<IDBObjectStore, DOMException>> =>
databaseFuture.mapOkToResult((database) =>
Result.fromExecution(() =>
database.transaction(storeName, transactionMode).objectStore(storeName),
).mapError((error) =>
rewriteError(error instanceof DOMException ? error : null),
),
);
const inMemoryStore = getInMemoryStore(databaseName, storeName);

return {
getMany: <T extends string>(
keys: T[],
): Future<Result<Record<T, unknown>, DOMException>> => {
return retry(transactionRetries, () =>
getObjectStore("readonly").flatMapOk((store) =>
Future.all(
keys.map((key) =>
futurifyRequest(store.get(key), "getMany", transactionTimeout)
.mapOk((value: unknown) => {
if (!enableInMemoryFallback) {
return value;
}
if (typeof value === "undefined") {
return inMemoryStore.get(key);
}

inMemoryStore.set(key, value);
return value;
})
.mapErrorToResult((error) =>
enableInMemoryFallback
? Result.Ok(inMemoryStore.get(key))
: Result.Error(error),
),
databaseFuture
.flatMapOk((database) =>
getStore(
database,
databaseName,
storeName,
"readonly",
transactionTimeout,
),
).map((results) => Result.all(results)),
),
)
.flatMapOk((store) =>
Future.all(
keys.map((key) =>
futurify(store.get(key), transactionTimeout)
.mapOk((value: unknown) => {
if (!enableInMemoryFallback) {
return value;
}
if (typeof value === "undefined") {
return inMemoryStore.get(key);
}

inMemoryStore.set(key, value);
return value;
})
.mapErrorToResult((error) =>
enableInMemoryFallback
? Result.Ok(inMemoryStore.get(key))
: Result.Error(error),
),
),
).map((results) => Result.all(results)),
),
)
.mapOk((values) => zipToObject(keys, values))
.mapErrorToResult((error) => {
Expand All @@ -85,39 +79,51 @@ export const openStore = (

setMany: (
object: Record<string, unknown>,
): Future<Result<void, DOMException>> => {
): Future<Result<undefined, DOMException>> => {
const entries = Dict.entries(object);

return retry(transactionRetries, () =>
getObjectStore("readwrite").flatMapOk((store) => {
entries.forEach(([key, value]) => store.put(value, key));

return futurifyTransaction(
store.transaction,
"setMany",
transactionTimeout,
);
}),
).tap(() => {
if (enableInMemoryFallback) {
entries.forEach(([key, value]) => {
inMemoryStore.set(key, value);
});
}
});
databaseFuture
.flatMapOk((database) =>
getStore(
database,
databaseName,
storeName,
"readwrite",
transactionTimeout,
),
)
.flatMapOk((store) =>
Future.all(
entries.map(([key, value]) =>
futurify(store.put(value, key), transactionTimeout),
),
).map((results) => Result.all(results)),
),
)
.mapOk(() => undefined)
.tap(() => {
if (enableInMemoryFallback) {
entries.forEach(([key, value]) => {
inMemoryStore.set(key, value);
});
}
});
},

clear: (): Future<Result<void, DOMException>> => {
clear: (): Future<Result<undefined, DOMException>> => {
return retry(transactionRetries, () =>
getObjectStore("readwrite").flatMapOk((store) => {
store.clear();

return futurifyTransaction(
store.transaction,
"clear",
transactionTimeout,
);
}),
databaseFuture
.flatMapOk((database) =>
getStore(
database,
databaseName,
storeName,
"readwrite",
transactionTimeout,
),
)
.flatMapOk((store) => futurify(store.clear(), transactionTimeout)),
).tapOk(() => {
if (enableInMemoryFallback) {
inMemoryStore.clear();
Expand Down
Loading

0 comments on commit e5f44f7

Please sign in to comment.