mirror of https://github.com/vector-im/riot-web
apply changes from review, and other fixes/improvements
parent
753ec9e45a
commit
649ea0d148
|
@ -26,7 +26,7 @@ import {CheckUpdatesPayload} from "./dispatcher/payloads/CheckUpdatesPayload";
|
||||||
import {Action} from "./dispatcher/actions";
|
import {Action} from "./dispatcher/actions";
|
||||||
import {hideToast as hideUpdateToast} from "./toasts/UpdateToast";
|
import {hideToast as hideUpdateToast} from "./toasts/UpdateToast";
|
||||||
import {MatrixClientPeg} from "./MatrixClientPeg";
|
import {MatrixClientPeg} from "./MatrixClientPeg";
|
||||||
import {idbLoad, idbSave, idbDelete} from "./IndexedDB";
|
import {idbLoad, idbSave, idbDelete} from "./utils/StorageManager";
|
||||||
|
|
||||||
export const SSO_HOMESERVER_URL_KEY = "mx_sso_hs_url";
|
export const SSO_HOMESERVER_URL_KEY = "mx_sso_hs_url";
|
||||||
export const SSO_ID_SERVER_URL_KEY = "mx_sso_is_url";
|
export const SSO_ID_SERVER_URL_KEY = "mx_sso_is_url";
|
||||||
|
@ -344,7 +344,11 @@ export default abstract class BasePlatform {
|
||||||
{name: "AES-GCM", iv, additionalData}, cryptoKey, randomArray,
|
{name: "AES-GCM", iv, additionalData}, cryptoKey, randomArray,
|
||||||
);
|
);
|
||||||
|
|
||||||
await idbSave("pickleKey", [userId, deviceId], {encrypted, iv, cryptoKey});
|
try {
|
||||||
|
await idbSave("pickleKey", [userId, deviceId], {encrypted, iv, cryptoKey});
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return encodeUnpaddedBase64(randomArray);
|
return encodeUnpaddedBase64(randomArray);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,92 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* Simple wrapper around IndexedDB.
|
|
||||||
*/
|
|
||||||
|
|
||||||
let idb = null;
|
|
||||||
|
|
||||||
async function idbInit(): Promise<void> {
|
|
||||||
idb = await new Promise((resolve, reject) => {
|
|
||||||
const request = window.indexedDB.open("element", 1);
|
|
||||||
request.onerror = reject;
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore: TS thinks target.result doesn't exist
|
|
||||||
request.onsuccess = (event) => { resolve(event.target.result); };
|
|
||||||
request.onupgradeneeded = (event) => {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore: TS thinks target.result doesn't exist
|
|
||||||
const db = event.target.result;
|
|
||||||
db.createObjectStore("pickleKey");
|
|
||||||
db.createObjectStore("account");
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function idbLoad(
|
|
||||||
table: string,
|
|
||||||
key: string | string[],
|
|
||||||
): Promise<any> {
|
|
||||||
if (!idb) {
|
|
||||||
await idbInit();
|
|
||||||
}
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const txn = idb.transaction([table], "readonly");
|
|
||||||
txn.onerror = reject;
|
|
||||||
|
|
||||||
const objectStore = txn.objectStore(table);
|
|
||||||
const request = objectStore.get(key);
|
|
||||||
request.onerror = reject;
|
|
||||||
request.onsuccess = (event) => { resolve(request.result); };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function idbSave(
|
|
||||||
table: string,
|
|
||||||
key: string | string[],
|
|
||||||
data: any,
|
|
||||||
): Promise<void> {
|
|
||||||
if (!idb) {
|
|
||||||
await idbInit();
|
|
||||||
}
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const txn = idb.transaction([table], "readwrite");
|
|
||||||
txn.onerror = reject;
|
|
||||||
|
|
||||||
const objectStore = txn.objectStore(table);
|
|
||||||
const request = objectStore.put(data, key);
|
|
||||||
request.onerror = reject;
|
|
||||||
request.onsuccess = (event) => { resolve(); };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function idbDelete(
|
|
||||||
table: string,
|
|
||||||
key: string | string[],
|
|
||||||
): Promise<void> {
|
|
||||||
if (!idb) {
|
|
||||||
await idbInit();
|
|
||||||
}
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const txn = idb.transaction([table], "readwrite");
|
|
||||||
txn.onerror = reject;
|
|
||||||
|
|
||||||
const objectStore = txn.objectStore(table);
|
|
||||||
const request = objectStore.delete(key);
|
|
||||||
request.onerror = reject;
|
|
||||||
request.onsuccess = (event) => { resolve(); };
|
|
||||||
});
|
|
||||||
}
|
|
115
src/Lifecycle.ts
115
src/Lifecycle.ts
|
@ -51,7 +51,6 @@ import ThreepidInviteStore from "./stores/ThreepidInviteStore";
|
||||||
import CountlyAnalytics from "./CountlyAnalytics";
|
import CountlyAnalytics from "./CountlyAnalytics";
|
||||||
import CallHandler from './CallHandler';
|
import CallHandler from './CallHandler';
|
||||||
import LifecycleCustomisations from "./customisations/Lifecycle";
|
import LifecycleCustomisations from "./customisations/Lifecycle";
|
||||||
import {idbLoad, idbSave, idbDelete} from "./IndexedDB";
|
|
||||||
|
|
||||||
const HOMESERVER_URL_KEY = "mx_hs_url";
|
const HOMESERVER_URL_KEY = "mx_hs_url";
|
||||||
const ID_SERVER_URL_KEY = "mx_is_url";
|
const ID_SERVER_URL_KEY = "mx_is_url";
|
||||||
|
@ -154,8 +153,8 @@ export async function loadSession(opts: ILoadSessionOpts = {}): Promise<boolean>
|
||||||
* return [null, null].
|
* return [null, null].
|
||||||
*/
|
*/
|
||||||
export async function getStoredSessionOwner(): Promise<[string, boolean]> {
|
export async function getStoredSessionOwner(): Promise<[string, boolean]> {
|
||||||
const {hsUrl, userId, accessToken, isGuest} = await getLocalStorageSessionVars();
|
const {hsUrl, userId, hasAccessToken, isGuest} = await getStoredSessionVars();
|
||||||
return hsUrl && userId && accessToken ? [userId, isGuest] : [null, null];
|
return hsUrl && userId && hasAccessToken ? [userId, isGuest] : [null, null];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -193,7 +192,7 @@ export function attemptTokenLogin(
|
||||||
).then(function(creds) {
|
).then(function(creds) {
|
||||||
console.log("Logged in with token");
|
console.log("Logged in with token");
|
||||||
return clearStorage().then(async () => {
|
return clearStorage().then(async () => {
|
||||||
await persistCredentialsToLocalStorage(creds);
|
await persistCredentials(creds);
|
||||||
// remember that we just logged in
|
// remember that we just logged in
|
||||||
sessionStorage.setItem("mx_fresh_login", String(true));
|
sessionStorage.setItem("mx_fresh_login", String(true));
|
||||||
return true;
|
return true;
|
||||||
|
@ -271,31 +270,42 @@ function registerAsGuest(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ILocalStorageSession {
|
export interface IStoredSession {
|
||||||
hsUrl: string;
|
hsUrl: string;
|
||||||
isUrl: string;
|
isUrl: string;
|
||||||
accessToken: string;
|
hasAccessToken: boolean;
|
||||||
|
accessToken: string | object;
|
||||||
userId: string;
|
userId: string;
|
||||||
deviceId: string;
|
deviceId: string;
|
||||||
isGuest: boolean;
|
isGuest: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves information about the stored session in localstorage. The session
|
* Retrieves information about the stored session from the browser's storage. The session
|
||||||
* may not be valid, as it is not tested for consistency here.
|
* may not be valid, as it is not tested for consistency here.
|
||||||
* @returns {Object} Information about the session - see implementation for variables.
|
* @returns {Object} Information about the session - see implementation for variables.
|
||||||
*/
|
*/
|
||||||
export async function getLocalStorageSessionVars(): Promise<ILocalStorageSession> {
|
export async function getStoredSessionVars(): Promise<IStoredSession> {
|
||||||
const hsUrl = localStorage.getItem(HOMESERVER_URL_KEY);
|
const hsUrl = localStorage.getItem(HOMESERVER_URL_KEY);
|
||||||
const isUrl = localStorage.getItem(ID_SERVER_URL_KEY);
|
const isUrl = localStorage.getItem(ID_SERVER_URL_KEY);
|
||||||
let accessToken = await idbLoad("account", "mx_access_token");
|
let accessToken;
|
||||||
|
try {
|
||||||
|
accessToken = await StorageManager.idbLoad("account", "mx_access_token");
|
||||||
|
} catch (e) {}
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
accessToken = localStorage.getItem("mx_access_token");
|
accessToken = localStorage.getItem("mx_access_token");
|
||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
await idbSave("account", "mx_access_token", accessToken);
|
try {
|
||||||
localStorage.removeItem("mx_access_token");
|
// try to migrate access token to IndexedDB if we can
|
||||||
|
await StorageManager.idbSave("account", "mx_access_token", accessToken);
|
||||||
|
localStorage.removeItem("mx_access_token");
|
||||||
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// if we pre-date storing "mx_has_access_token", but we retrieved an access
|
||||||
|
// token, then we should say we have an access token
|
||||||
|
const hasAccessToken =
|
||||||
|
(localStorage.getItem("mx_has_access_token") === "true") || !!accessToken;
|
||||||
const userId = localStorage.getItem("mx_user_id");
|
const userId = localStorage.getItem("mx_user_id");
|
||||||
const deviceId = localStorage.getItem("mx_device_id");
|
const deviceId = localStorage.getItem("mx_device_id");
|
||||||
|
|
||||||
|
@ -307,7 +317,7 @@ export async function getLocalStorageSessionVars(): Promise<ILocalStorageSession
|
||||||
isGuest = localStorage.getItem("matrix-is-guest") === "true";
|
isGuest = localStorage.getItem("matrix-is-guest") === "true";
|
||||||
}
|
}
|
||||||
|
|
||||||
return {hsUrl, isUrl, accessToken, userId, deviceId, isGuest};
|
return {hsUrl, isUrl, hasAccessToken, accessToken, userId, deviceId, isGuest};
|
||||||
}
|
}
|
||||||
|
|
||||||
// The pickle key is a string of unspecified length and format. For AES, we
|
// The pickle key is a string of unspecified length and format. For AES, we
|
||||||
|
@ -334,6 +344,18 @@ async function pickleKeyToAesKey(pickleKey: string): Promise<Uint8Array> {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function abortLogin() {
|
||||||
|
const signOut = await showStorageEvictedDialog();
|
||||||
|
if (signOut) {
|
||||||
|
await clearStorage();
|
||||||
|
// This error feels a bit clunky, but we want to make sure we don't go any
|
||||||
|
// further and instead head back to sign in.
|
||||||
|
throw new AbortLoginAndRebuildStorage(
|
||||||
|
"Aborting login in progress because of storage inconsistency",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// returns a promise which resolves to true if a session is found in
|
// returns a promise which resolves to true if a session is found in
|
||||||
// localstorage
|
// localstorage
|
||||||
//
|
//
|
||||||
|
@ -351,7 +373,11 @@ async function restoreFromLocalStorage(opts?: { ignoreGuest?: boolean }): Promis
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {hsUrl, isUrl, accessToken, userId, deviceId, isGuest} = await getLocalStorageSessionVars();
|
const {hsUrl, isUrl, hasAccessToken, accessToken, userId, deviceId, isGuest} = await getStoredSessionVars();
|
||||||
|
|
||||||
|
if (hasAccessToken && !accessToken) {
|
||||||
|
abortLogin();
|
||||||
|
}
|
||||||
|
|
||||||
if (accessToken && userId && hsUrl) {
|
if (accessToken && userId && hsUrl) {
|
||||||
if (ignoreGuest && isGuest) {
|
if (ignoreGuest && isGuest) {
|
||||||
|
@ -379,7 +405,7 @@ async function restoreFromLocalStorage(opts?: { ignoreGuest?: boolean }): Promis
|
||||||
await doSetLoggedIn({
|
await doSetLoggedIn({
|
||||||
userId: userId,
|
userId: userId,
|
||||||
deviceId: deviceId,
|
deviceId: deviceId,
|
||||||
accessToken: decryptedAccessToken,
|
accessToken: decryptedAccessToken as string,
|
||||||
homeserverUrl: hsUrl,
|
homeserverUrl: hsUrl,
|
||||||
identityServerUrl: isUrl,
|
identityServerUrl: isUrl,
|
||||||
guest: isGuest,
|
guest: isGuest,
|
||||||
|
@ -518,15 +544,7 @@ async function doSetLoggedIn(
|
||||||
// crypto store, we'll be generally confused when handling encrypted data.
|
// crypto store, we'll be generally confused when handling encrypted data.
|
||||||
// Show a modal recommending a full reset of storage.
|
// Show a modal recommending a full reset of storage.
|
||||||
if (results.dataInLocalStorage && results.cryptoInited && !results.dataInCryptoStore) {
|
if (results.dataInLocalStorage && results.cryptoInited && !results.dataInCryptoStore) {
|
||||||
const signOut = await showStorageEvictedDialog();
|
await abortLogin();
|
||||||
if (signOut) {
|
|
||||||
await clearStorage();
|
|
||||||
// This error feels a bit clunky, but we want to make sure we don't go any
|
|
||||||
// further and instead head back to sign in.
|
|
||||||
throw new AbortLoginAndRebuildStorage(
|
|
||||||
"Aborting login in progress because of storage inconsistency",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Analytics.setLoggedIn(credentials.guest, credentials.homeserverUrl);
|
Analytics.setLoggedIn(credentials.guest, credentials.homeserverUrl);
|
||||||
|
@ -548,7 +566,7 @@ async function doSetLoggedIn(
|
||||||
|
|
||||||
if (localStorage) {
|
if (localStorage) {
|
||||||
try {
|
try {
|
||||||
await persistCredentialsToLocalStorage(credentials);
|
await persistCredentials(credentials);
|
||||||
// make sure we don't think that it's a fresh login any more
|
// make sure we don't think that it's a fresh login any more
|
||||||
sessionStorage.removeItem("mx_fresh_login");
|
sessionStorage.removeItem("mx_fresh_login");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -577,7 +595,7 @@ function showStorageEvictedDialog(): Promise<boolean> {
|
||||||
// `instanceof`. Babel 7 supports this natively in their class handling.
|
// `instanceof`. Babel 7 supports this natively in their class handling.
|
||||||
class AbortLoginAndRebuildStorage extends Error { }
|
class AbortLoginAndRebuildStorage extends Error { }
|
||||||
|
|
||||||
async function persistCredentialsToLocalStorage(credentials: IMatrixClientCreds): Promise<void> {
|
async function persistCredentials(credentials: IMatrixClientCreds): Promise<void> {
|
||||||
localStorage.setItem(HOMESERVER_URL_KEY, credentials.homeserverUrl);
|
localStorage.setItem(HOMESERVER_URL_KEY, credentials.homeserverUrl);
|
||||||
if (credentials.identityServerUrl) {
|
if (credentials.identityServerUrl) {
|
||||||
localStorage.setItem(ID_SERVER_URL_KEY, credentials.identityServerUrl);
|
localStorage.setItem(ID_SERVER_URL_KEY, credentials.identityServerUrl);
|
||||||
|
@ -585,14 +603,47 @@ async function persistCredentialsToLocalStorage(credentials: IMatrixClientCreds)
|
||||||
localStorage.setItem("mx_user_id", credentials.userId);
|
localStorage.setItem("mx_user_id", credentials.userId);
|
||||||
localStorage.setItem("mx_is_guest", JSON.stringify(credentials.guest));
|
localStorage.setItem("mx_is_guest", JSON.stringify(credentials.guest));
|
||||||
|
|
||||||
|
// store whether we expect to find an access token, to detect the case
|
||||||
|
// where IndexedDB is blown away
|
||||||
|
if (credentials.accessToken) {
|
||||||
|
localStorage.setItem("mx_has_access_token", "true");
|
||||||
|
} else {
|
||||||
|
localStorage.deleteItem("mx_has_access_token");
|
||||||
|
}
|
||||||
|
|
||||||
if (credentials.pickleKey) {
|
if (credentials.pickleKey) {
|
||||||
const encrKey = await pickleKeyToAesKey(credentials.pickleKey);
|
let encryptedAccessToken;
|
||||||
const encryptedAccessToken = await encryptAES(credentials.accessToken, encrKey, "access_token");
|
try {
|
||||||
encrKey.fill(0);
|
// try to encrypt the access token using the pickle key
|
||||||
await idbSave("account", "mx_access_token", encryptedAccessToken);
|
const encrKey = await pickleKeyToAesKey(credentials.pickleKey);
|
||||||
|
encryptedAccessToken = await encryptAES(credentials.accessToken, encrKey, "access_token");
|
||||||
|
encrKey.fill(0);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Could not encrypt access token", e);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// save either the encrypted access token, or the plain access
|
||||||
|
// token if we were unable to encrypt (e.g. if the browser doesn't
|
||||||
|
// have WebCrypto).
|
||||||
|
await StorageManager.idbSave(
|
||||||
|
"account", "mx_access_token",
|
||||||
|
encryptedAccessToken || credentials.accessToken,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
// if we couldn't save to indexedDB, fall back to localStorage. We
|
||||||
|
// store the access token unencrypted since localStorage only saves
|
||||||
|
// strings.
|
||||||
|
localStorage.setItem("mx_access_token", credentials.accessToken);
|
||||||
|
}
|
||||||
localStorage.setItem("mx_has_pickle_key", String(true));
|
localStorage.setItem("mx_has_pickle_key", String(true));
|
||||||
} else {
|
} else {
|
||||||
await idbSave("account", "mx_access_token", credentials.accessToken);
|
try {
|
||||||
|
await StorageManager.idbSave(
|
||||||
|
"account", "mx_access_token", credentials.accessToken,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
localStorage.setItem("mx_access_token", credentials.accessToken);
|
||||||
|
}
|
||||||
if (localStorage.getItem("mx_has_pickle_key")) {
|
if (localStorage.getItem("mx_has_pickle_key")) {
|
||||||
console.error("Expected a pickle key, but none provided. Encryption may not work.");
|
console.error("Expected a pickle key, but none provided. Encryption may not work.");
|
||||||
}
|
}
|
||||||
|
@ -769,7 +820,9 @@ async function clearStorage(opts?: { deleteEverything?: boolean }): Promise<void
|
||||||
|
|
||||||
window.localStorage.clear();
|
window.localStorage.clear();
|
||||||
|
|
||||||
await idbDelete("account", "mx_access_token");
|
try {
|
||||||
|
await StorageManager.idbDelete("account", "mx_access_token");
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
// now restore those invites
|
// now restore those invites
|
||||||
if (!opts?.deleteEverything) {
|
if (!opts?.deleteEverything) {
|
||||||
|
|
|
@ -190,3 +190,79 @@ export function trackStores(client) {
|
||||||
export function setCryptoInitialised(cryptoInited) {
|
export function setCryptoInitialised(cryptoInited) {
|
||||||
localStorage.setItem("mx_crypto_initialised", cryptoInited);
|
localStorage.setItem("mx_crypto_initialised", cryptoInited);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Simple wrapper functions around IndexedDB.
|
||||||
|
*/
|
||||||
|
|
||||||
|
let idb = null;
|
||||||
|
|
||||||
|
async function idbInit(): Promise<void> {
|
||||||
|
if (!indexedDB) {
|
||||||
|
throw new Error("IndexedDB not available");
|
||||||
|
}
|
||||||
|
idb = await new Promise((resolve, reject) => {
|
||||||
|
const request = indexedDB.open("matrix-react-sdk", 1);
|
||||||
|
request.onerror = reject;
|
||||||
|
request.onsuccess = (event) => { resolve(request.result); };
|
||||||
|
request.onupgradeneeded = (event) => {
|
||||||
|
const db = request.result;
|
||||||
|
db.createObjectStore("pickleKey");
|
||||||
|
db.createObjectStore("account");
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function idbLoad(
|
||||||
|
table: string,
|
||||||
|
key: string | string[],
|
||||||
|
): Promise<any> {
|
||||||
|
if (!idb) {
|
||||||
|
await idbInit();
|
||||||
|
}
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const txn = idb.transaction([table], "readonly");
|
||||||
|
txn.onerror = reject;
|
||||||
|
|
||||||
|
const objectStore = txn.objectStore(table);
|
||||||
|
const request = objectStore.get(key);
|
||||||
|
request.onerror = reject;
|
||||||
|
request.onsuccess = (event) => { resolve(request.result); };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function idbSave(
|
||||||
|
table: string,
|
||||||
|
key: string | string[],
|
||||||
|
data: any,
|
||||||
|
): Promise<void> {
|
||||||
|
if (!idb) {
|
||||||
|
await idbInit();
|
||||||
|
}
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const txn = idb.transaction([table], "readwrite");
|
||||||
|
txn.onerror = reject;
|
||||||
|
|
||||||
|
const objectStore = txn.objectStore(table);
|
||||||
|
const request = objectStore.put(data, key);
|
||||||
|
request.onerror = reject;
|
||||||
|
request.onsuccess = (event) => { resolve(); };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function idbDelete(
|
||||||
|
table: string,
|
||||||
|
key: string | string[],
|
||||||
|
): Promise<void> {
|
||||||
|
if (!idb) {
|
||||||
|
await idbInit();
|
||||||
|
}
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const txn = idb.transaction([table], "readwrite");
|
||||||
|
txn.onerror = reject;
|
||||||
|
|
||||||
|
const objectStore = txn.objectStore(table);
|
||||||
|
const request = objectStore.delete(key);
|
||||||
|
request.onerror = reject;
|
||||||
|
request.onsuccess = (event) => { resolve(); };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue