Last active
April 8, 2023 13:24
-
-
Save phistuck/0d7a501b0704841f5109671327676e0e to your computer and use it in GitHub Desktop.
A simplistic Promise-based API for IndexedDB
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
// License - no author warranty/responsibility/guarantee public domain where allowed. Otherwise - | |
// License - no author warranty/responsibility/guarantee, can do whatever you want with the code where allowed. Otherwise - | |
// License - MIT. | |
(function () | |
{ | |
// Let the fun begin. | |
"use strict"; | |
const | |
/** @const {Object<string, string>} */ | |
Name = {DB: "stuff", DETAILS_TABLE: "stuff-details", ID_KEY: "id"}, | |
/** @const {Object<string, string>} */ | |
Acts = {READWRITE: "readwrite"}, | |
/** @const {Object<string, Function>} */ | |
Actions = | |
{ | |
THROW() | |
{ | |
console.error.apply(console, arguments); | |
throw new Error(); | |
}, | |
LOG: console.log.bind(console) | |
}; | |
class Store | |
{ | |
/** @param {string} databaseName | |
@param {string} storeName | |
@param {string} indexKeyName */ | |
constructor(databaseName, storeName, indexKeyName) | |
{ | |
/** @type {IDBDatabase} */ | |
this.database = null; | |
/** @type {string} */ | |
this.databaseName = databaseName; | |
/** @type {string} */ | |
this.storeName = storeName; | |
/** @type {string} */ | |
this.indexKeyName = indexKeyName; | |
} | |
close() | |
{ | |
if (!this.database) | |
{ | |
return; | |
} | |
this.database.close(); | |
this.database = null; | |
} | |
openDatabase(resolve, reject) | |
{ | |
let | |
/** @type {IDBOpenDBRequest} */ | |
database = indexedDB.open(this.databaseName, 1), | |
/** @type {boolean} */ | |
waitForUpgrade = false; | |
database.onerror = () => | |
{ | |
reject(); | |
}; | |
database.onsuccess = () => | |
{ | |
if (waitForUpgrade) | |
{ | |
return; | |
} | |
this.database = /** @type {IDBDatabase} */ (database.result); | |
resolve(this); | |
}; | |
database.onupgradeneeded = () => { | |
waitForUpgrade = true; | |
this.database = /** @type {IDBDatabase} */ (database.result); | |
this.database | |
.createObjectStore(this.storeName, {keyPath: this.indexKeyName}) | |
.transaction.oncomplete = () => | |
{ | |
resolve(); | |
}; | |
}; | |
} | |
getObjectsByListUsingGetAll(acts, keys) | |
{ | |
return new Promise((resolve, reject) => | |
{ | |
const actGetAll = acts.getAll(); | |
actGetAll.onsuccess = () => | |
{ | |
const KEY_NAME = Name.ID_KEY; | |
resolve(actGetAll.result.reduce((objects, item) => | |
{ | |
const id = item[KEY_NAME]; | |
if (keys.includes(id)) | |
{ | |
objects[id] = item; | |
} | |
return objects; | |
}, {})); | |
}; | |
actGetAll.onerror = () => | |
{ | |
this.close(); | |
reject(actGetAll.error); | |
}; | |
}); | |
} | |
getObjectsByListUsingCursor(acts, keys) | |
{ | |
return new Promise((resolve, reject) => | |
{ | |
let | |
/** @type {IDBRequest<IDBCursorWithValue>} */ | |
actCursor = acts.openCursor(), | |
/** @type {Object.<number, Object<string, ?>>} */ | |
objects = {}; | |
actCursor.onerror = () => | |
{ | |
this.close(); | |
reject(actCursor.error); | |
} | |
actCursor.onsuccess = () => | |
{ | |
let result = actCursor.result; | |
if (result) | |
{ | |
let data = result.value, | |
id = data[this.indexKeyName]; | |
if (keys.indexOf(id) !== -1) | |
{ | |
objects[id] = data; | |
} | |
result.continue(); | |
} | |
else | |
{ | |
this.close(); | |
resolve(objects); | |
} | |
} | |
}); | |
} | |
getObjectsByList(keys) | |
{ | |
return new Promise((resolve, reject) => | |
{ | |
this.getActs().then(acts => | |
{ | |
if (acts.getAll) | |
{ | |
this.getObjectsByListUsingGetAll(acts, keys).then(resolve, reject); | |
} | |
else | |
{ | |
this.getObjectsByListUsingCursor(acts, keys).then(resolve, reject); | |
} | |
}, | |
reject); | |
}); | |
} | |
/** @private | |
@param {string=} scope | |
@returns {Promise<IDBObjectStore>} */ | |
getActs(scope) | |
{ | |
let get = () => | |
{ | |
var parameters = [this.storeName]; | |
if (scope) | |
{ | |
parameters.push(scope); | |
} | |
return this.database.transaction.apply(this.database, parameters).objectStore(this.storeName); | |
}; | |
if (!this.database) | |
{ | |
return this.initialize().then(get); | |
} | |
return Promise.resolve(get()); | |
} | |
/** @param {number} id */ | |
getObjectByKey(id) | |
{ | |
return new Promise((resolve, reject) => | |
{ | |
this.getActs().then(acts => | |
{ | |
let actGet = acts.get(id); | |
actGet.onerror = () => reject(actGet.error); | |
actGet.onsuccess = () => | |
{ | |
if (actGet.result) | |
{ | |
resolve(actGet.result); | |
} | |
else | |
{ | |
reject(new ReferenceError(`The key ${id} did not yield and object.`)); | |
} | |
} | |
}); | |
}); | |
} | |
addOrUpdateObject(data) | |
{ | |
return new Promise((resolve, reject) => | |
{ | |
this.getActs(Acts.READWRITE).then(acts => | |
{ | |
let addOrUpdateAct = acts.put(data); | |
addOrUpdateAct.onerror = () => | |
{ | |
this.close(); | |
reject(addOrUpdateAct.error); | |
} | |
addOrUpdateAct.onsuccess = () => | |
{ | |
this.close(); | |
resolve(data); | |
}; | |
}, | |
reject); | |
}); | |
} | |
initialize() | |
{ | |
return new Promise(this.openDatabase.bind(this)); | |
} | |
} | |
// Usage | |
const store = new Store(Name.DB, Name.DETAILS_TABLE, Name.ID_KEY); | |
await store.addOrUpdateObject({[Name.ID_KEY]: 1, stuff: 'fun'}); | |
await store.getObjectByKey(1); | |
await store.getObjectsByList([1, 2, 3, 4, 5]); | |
}()); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment