2020-08-05 18:38:55 +02:00
|
|
|
/*
|
|
|
|
Copyright 2020 Bruno Windels <bruno@windels.cloud>
|
|
|
|
|
|
|
|
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.
|
|
|
|
*/
|
|
|
|
|
2019-06-26 22:00:50 +02:00
|
|
|
import { StorageError } from "../common.js";
|
|
|
|
|
2020-08-18 17:27:40 +02:00
|
|
|
class WrappedDOMException extends StorageError {
|
|
|
|
constructor(request) {
|
2020-08-19 11:36:43 +02:00
|
|
|
const source = request?.source;
|
|
|
|
const storeName = source?.name || "<unknown store>";
|
|
|
|
const databaseName = source?.transaction?.db?.name || "<unknown db>";
|
2020-08-18 17:27:40 +02:00
|
|
|
super(`Failed IDBRequest on ${databaseName}.${storeName}`, request.error);
|
|
|
|
this.storeName = storeName;
|
|
|
|
this.databaseName = databaseName;
|
|
|
|
}
|
|
|
|
}
|
2019-07-01 10:00:29 +02:00
|
|
|
|
|
|
|
// storage keys are defined to be unsigned 32bit numbers in WebPlatform.js, which is assumed by idb
|
|
|
|
export function encodeUint32(n) {
|
|
|
|
const hex = n.toString(16);
|
|
|
|
return "0".repeat(8 - hex.length) + hex;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function decodeUint32(str) {
|
|
|
|
return parseInt(str, 16);
|
|
|
|
}
|
|
|
|
|
2019-02-07 00:19:14 +01:00
|
|
|
export function openDatabase(name, createObjectStore, version) {
|
2019-02-06 23:06:56 +01:00
|
|
|
const req = window.indexedDB.open(name, version);
|
|
|
|
req.onupgradeneeded = (ev) => {
|
|
|
|
const db = ev.target.result;
|
2020-06-26 23:26:24 +02:00
|
|
|
const txn = ev.target.transaction;
|
2019-02-06 23:06:56 +01:00
|
|
|
const oldVersion = ev.oldVersion;
|
2020-06-26 23:26:24 +02:00
|
|
|
createObjectStore(db, txn, oldVersion, version);
|
2019-02-06 23:06:56 +01:00
|
|
|
};
|
|
|
|
return reqAsPromise(req);
|
2019-01-09 11:06:09 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
export function reqAsPromise(req) {
|
|
|
|
return new Promise((resolve, reject) => {
|
2019-02-06 23:06:33 +01:00
|
|
|
req.addEventListener("success", event => resolve(event.target.result));
|
2020-08-18 17:27:40 +02:00
|
|
|
req.addEventListener("error", event => reject(new WrappedDOMException(event.target)));
|
2019-01-09 11:06:09 +01:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
export function txnAsPromise(txn) {
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
txn.addEventListener("complete", resolve);
|
2020-08-18 17:27:40 +02:00
|
|
|
txn.addEventListener("abort", event => reject(new WrappedDOMException(event.target)));
|
2019-01-09 11:06:09 +01:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2019-09-15 12:23:54 +02:00
|
|
|
export function iterateCursor(cursorRequest, processValue) {
|
2019-02-06 23:06:56 +01:00
|
|
|
// TODO: does cursor already have a value here??
|
2019-01-09 11:06:09 +01:00
|
|
|
return new Promise((resolve, reject) => {
|
2019-09-15 12:23:54 +02:00
|
|
|
cursorRequest.onerror = () => {
|
|
|
|
reject(new StorageError("Query failed", cursorRequest.error));
|
2019-01-09 11:06:09 +01:00
|
|
|
};
|
|
|
|
// collect results
|
2019-09-15 12:23:54 +02:00
|
|
|
cursorRequest.onsuccess = (event) => {
|
2019-01-09 11:06:09 +01:00
|
|
|
const cursor = event.target.result;
|
|
|
|
if (!cursor) {
|
|
|
|
resolve(false);
|
|
|
|
return; // end of results
|
|
|
|
}
|
2020-06-26 23:26:24 +02:00
|
|
|
const result = processValue(cursor.value, cursor.key);
|
|
|
|
const done = result && result.done;
|
|
|
|
const jumpTo = result && result.jumpTo;
|
|
|
|
|
2019-05-11 13:10:31 +02:00
|
|
|
if (done) {
|
2019-02-06 23:06:56 +01:00
|
|
|
resolve(true);
|
2019-06-26 22:02:00 +02:00
|
|
|
} else if(jumpTo) {
|
2019-05-11 13:10:31 +02:00
|
|
|
cursor.continue(jumpTo);
|
2019-06-26 22:02:00 +02:00
|
|
|
} else {
|
|
|
|
cursor.continue();
|
2019-01-09 11:06:09 +01:00
|
|
|
}
|
|
|
|
};
|
2019-06-26 22:00:50 +02:00
|
|
|
}).catch(err => {
|
|
|
|
throw new StorageError("iterateCursor failed", err);
|
2019-01-09 11:06:09 +01:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function fetchResults(cursor, isDone) {
|
|
|
|
const results = [];
|
|
|
|
await iterateCursor(cursor, (value) => {
|
2019-02-06 23:06:56 +01:00
|
|
|
results.push(value);
|
2019-05-11 13:10:31 +02:00
|
|
|
return {done: isDone(results)};
|
2019-01-09 11:06:09 +01:00
|
|
|
});
|
|
|
|
return results;
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function select(db, storeName, toCursor, isDone) {
|
2019-02-06 23:06:56 +01:00
|
|
|
if (!isDone) {
|
|
|
|
isDone = () => false;
|
|
|
|
}
|
|
|
|
if (!toCursor) {
|
|
|
|
toCursor = store => store.openCursor();
|
|
|
|
}
|
|
|
|
const tx = db.transaction([storeName], "readonly");
|
|
|
|
const store = tx.objectStore(storeName);
|
|
|
|
const cursor = toCursor(store);
|
|
|
|
return await fetchResults(cursor, isDone);
|
2019-01-09 11:06:09 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
export async function updateSingletonStore(db, storeName, value) {
|
2019-02-06 23:06:56 +01:00
|
|
|
const tx = db.transaction([storeName], "readwrite");
|
|
|
|
const store = tx.objectStore(storeName);
|
|
|
|
const cursor = await reqAsPromise(store.openCursor());
|
|
|
|
if (cursor) {
|
|
|
|
return reqAsPromise(cursor.update(storeName));
|
|
|
|
} else {
|
|
|
|
return reqAsPromise(store.add(value));
|
|
|
|
}
|
2019-01-09 11:06:09 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
export async function findStoreValue(db, storeName, toCursor, matchesValue) {
|
2019-02-06 23:06:56 +01:00
|
|
|
if (!matchesValue) {
|
|
|
|
matchesValue = () => true;
|
|
|
|
}
|
|
|
|
if (!toCursor) {
|
|
|
|
toCursor = store => store.openCursor();
|
|
|
|
}
|
2019-01-09 11:06:09 +01:00
|
|
|
|
2019-02-06 23:06:56 +01:00
|
|
|
const tx = db.transaction([storeName], "readwrite");
|
|
|
|
const store = tx.objectStore(storeName);
|
|
|
|
const cursor = await reqAsPromise(toCursor(store));
|
|
|
|
let match;
|
|
|
|
const matched = await iterateCursor(cursor, (value) => {
|
|
|
|
if (matchesValue(value)) {
|
|
|
|
match = value;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
if (!matched) {
|
2019-06-26 22:00:50 +02:00
|
|
|
throw new StorageError("Value not found");
|
2019-02-06 23:06:56 +01:00
|
|
|
}
|
|
|
|
return match;
|
2019-05-11 13:10:31 +02:00
|
|
|
}
|