I have a class that loads indexedDB. Before methods in the class can access it, I need to have indexedDB loaded beforehand. Currently I'm using an init() method prior to any other method that does not have this.db initialized.
I'm looking for a cleaner way to implement what I have, which definitely isn't DRY. Essentially every method is currently implemented with the same code pattern below.
Problem points are:
The requirement of another method init() in order to properly
handle the intialization of indexedDB.
The if (!this.db) { segment that ends up repeating itself later.
export default class Persist {
constructor(storage) {
if (storage) {
this.storage = storage;
}
else {
throw new Error('A storage object must be passed to new Persist()');
}
}
// needed to ensure that indexedDB is initialized before other methods can access it.
init () {
// initialize indexedDB:
const DBOpenRequest = this.storage.open('db', 1);
DBOpenRequest.onupgradeneeded = () => {
const db = DBOpenRequest.result;
db.createObjectStore('db', { keyPath: 'id', autoIncrement: true });
};
return new Promise((resolve, reject) => {
DBOpenRequest.onerror = event => {
reject(event);
};
DBOpenRequest.onsuccess = event => {
console.log(`IndexedDB successfully opened: ${event.target.result}`);
resolve(event.result);
this.db = DBOpenRequest.result;
};
});
}
toStorage(session) {
if (!this.db) {
return this.init().then(() => {
const db = this.db;
const tx = db.transaction('db', 'readwrite');
const store = tx.objectStore('db');
const putData = store.put(session.toJS());
return new Promise((resolve, reject) => {
putData.onsuccess = () => {
resolve(putData.result);
};
putData.onerror = () => {
reject(putData.error);
};
});
});
}
// basically a repeat of above
const db = this.db;
const tx = db.transaction('db', 'readwrite');
const store = tx.objectStore('db');
const putData = store.put(session.toJS());
return new Promise((resolve, reject) => {
putData.onsuccess = () => {
resolve(putData.result);
};
putData.onerror = () => {
reject(putData.error);
};
});
}
indexedDB provides asynchronous functions. indexedDB.open is an asynchronous function. It looks like you are trying to work with indexedDB in a non-asynchronous manner. Instead of storing the IDBDatabase variable as a property of the instance of your class, just return it as the resolve value and manage it external to the class.
function connect(name, version) {
return new Promise((resolve, reject) => {
const request = indexedDB.open(name, version);
request.onupgradeneeded = myUpgradeHandlerFunction;
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
request.onblocked = () => { console.log('blocked'); };
});
}
function doStuffWithConn(conn, value) {
return new Promise((resolve, reject) => {
const tx = conn.transaction(...);
const store = tx.objectStore(...);
const request = store.put(value);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async function putValue(value) {
let conn;
try {
conn = await connect(...);
await doStuffWithConn(conn, value);
} catch(exception) {
console.error(exception);
} finally {
if(conn)
conn.close();
}
}
Related
context: Two javascript classes in separate files, each integrating a different external service and being called in a express.js router.
See "problematic code" below:
route
routes.post('/aws', upload.single('file'), async (req, res) => {
const transcribeParams = JSON.parse(req.body.options)
const bucket = 'bucket-name'
const data = await ( await ( await awsTranscribe.Upload(req.file, bucket)).CreateJob(transcribeParams)).GetJob()
res.send(data)
})
S3 class
class AmazonS3 {
constructor() {
this.Upload = this.Upload
}
async Upload(file, bucket) {
const uploadParams = {
Bucket: bucket,
Body: fs.createReadStream(file.path),
Key: file.filename,
}
this.data = await s3.upload(uploadParams).promise()
return this
}
}
Transcribe class
class Transcribe extends AwsS3 {
constructor() {
super()
this.CreateJob = this.CreateJob
this.GetJob = this.GetJob
}
async CreateJob(params) {
if(this.data?.Location) {
params.Media = { ...params.Media, MediaFileUri: this.data.Location }
}
this.data = await transcribeService.startTranscriptionJob(params).promise()
return this
}
async GetJob(jobName) {
if(this.data?.TranscriptionJob?.TranscriptionJobName) {
jobName = this.data.TranscriptionJob.TranscriptionJobName
}
this.data = await transcribeService.getTranscriptionJob({TranscriptionJobName: jobName}).promise()
return this
}
}
problem: the problem is with the chained awaits in the router file:
await ( await ( await awsTranscribe.Upload...
Yes, it does work, but it would be horrible for another person to maintain this code in the future.
How can i make so it would be just
awsTranscribe.Upload(req.file, bucket).CreateJob(transcribeParams).GetJob() without the .then?
The problem is with the chained awaits in the router file: await ( await ( await awsTranscribe.Upload...
No, that's fine. In particular it would be trivial to refactor it to separate lines:
routes.post('/aws', upload.single('file'), async (req, res) => {
const transcribeParams = JSON.parse(req.body.options)
const bucket = 'bucket-name'
const a = await awsTranscribe.Upload(req.file, bucket);
const b = await b.CreateJob(transcribeParams);
const c = await b.GetJob();
res.send(c);
});
Your actual problem is that a, b, and c all refer to the same object awsTranscribe. Your code would also "work" if it was written
routes.post('/aws', upload.single('file'), async (req, res) => {
const transcribeParams = JSON.parse(req.body.options)
const bucket = 'bucket-name'
await awsTranscribe.Upload(req.file, bucket);
await awsTranscribe.CreateJob(transcribeParams);
await awsTranscribe.GetJob();
res.send(awsTranscribe);
});
The horrible thing is that you are passing your data between these methods through the mutable awsTranscribe.data property - even storing different kinds of data in it at different times! One could change the order of method calls and it would completely break in non-obvious and hard-to-debug ways.
Also it seems that multiple requests share the same awsTranscribe instance. This will not work with concurrent requests. Anything is possible from just "not working" to responding with the job data from a different user (request)! You absolutely need to fix that, then look at ugly syntax later.
What you really should do is get rid of the classes. There's no reason to use stateful objects here, this is plain procedural code. Write simple functions, taking parameters and returning values:
export async function uploadFile(file, bucket) {
const uploadParams = {
Bucket: bucket,
Body: fs.createReadStream(file.path),
Key: file.filename,
};
const data = s3.upload(uploadParams).promise();
return data.Location;
}
export async function createTranscriptionJob(location, params) {
params = {
...params,
Media: {
...params.Media,
MediaFileUri: location,
},
};
const data = await transcribeService.startTranscriptionJob(params).promise();
return data.TranscriptionJob;
}
async function getTranscriptionJob(job) {
const jobName = job.TranscriptionJobName;
return transcribeService.getTranscriptionJob({TranscriptionJobName: jobName}).promise();
}
Then you can import and call them as
routes.post('/aws', upload.single('file'), async (req, res) => {
const transcribeParams = JSON.parse(req.body.options)
const bucket = 'bucket-name'
const location = await uploadFile(req.file, bucket);
const job = await createTranscriptionJob(location, transcribeParams);
const data = await getTranscriptionJob(job);
res.send(c);
});
I got interested in whether it was possible to take an object with several async methods and somehow make them automatically chainable. Well, you can:
function chain(obj, methodsArray) {
if (!methodsArray || !methodsArray.length) {
throw new Error("methodsArray argument must be array of chainable method names");
}
const methods = new Set(methodsArray);
let lastPromise = Promise.resolve();
const proxy = new Proxy(obj, {
get(target, prop, receiver) {
if (prop === "_promise") {
return function() {
return lastPromise;
}
}
const val = Reflect.get(target, prop, receiver);
if (typeof val !== "function" || !methods.has(prop)) {
// no chaining if it's not a function
// or it's not listed as a chainable method
return val;
} else {
// return a stub function
return function(...args) {
// chain a function call
lastPromise = lastPromise.then(() => {
return val.apply(obj, args);
//return Reflect.apply(val, obj, ...args);
});
return proxy;
}
}
}
});
return proxy;
}
function delay(t) {
return new Promise(resolve => {
setTimeout(resolve, t);
});
}
function log(...args) {
if (!log.start) {
log.start = Date.now();
}
const delta = Date.now() - log.start;
const deltaPad = (delta + "").padStart(6, "0");
console.log(`${deltaPad}: `, ...args)
}
class Transcribe {
constructor() {
this.greeting = "Hello";
}
async createJob(params) {
log(`createJob: ${this.greeting}`);
return delay(200);
}
async getJob(jobName) {
log(`getJob: ${this.greeting}`);
return delay(100);
}
}
const t = new Transcribe();
const obj = chain(t, ["getJob", "createJob"]);
log("begin");
obj.createJob().getJob()._promise().then(() => {
log("end");
});
There's a placeholder for your Transcribe class that has two asynchronous methods that return a promise.
Then, there's a chain() function that returns a proxy to an object that makes a set of passed in method names be chainable which allows you to then do something like this:
const t = new Transcribe();
// make chainable proxy
const obj = chain(t, ["getJob", "createJob"]);
obj.createJob().getJob()
or
await obj.createJob().getJob()._promise()
I wouldn't necessarily say this is production-ready code, but it is an interesting feasibility demonstration and (for me) a chance to learn more about a Javascript proxy object.
Here's a different approach that (instead of the proxy object) adds method stubs to a promise to make things chainable:
function chain(orig, methodsArray) {
let masterP = Promise.resolve();
function addMethods(dest) {
for (const m of methodsArray) {
dest[m] = function(...args) {
// chain onto master promise to force sequencing
masterP = masterP.then(result => {
return orig[m].apply(orig, ...args);
});
// add methods to the latest promise befor returning it
addMethods(masterP);
return masterP;
}
}
}
// add method to our returned promise
addMethods(masterP);
return masterP;
}
function delay(t) {
return new Promise(resolve => {
setTimeout(resolve, t);
});
}
function log(...args) {
if (!log.start) {
log.start = Date.now();
}
const delta = Date.now() - log.start;
const deltaPad = (delta + "").padStart(6, "0");
console.log(`${deltaPad}: `, ...args)
}
class Transcribe {
constructor() {
this.greeting = "Hello";
this.cntr = 0;
}
async createJob(params) {
log(`createJob: ${this.greeting}`);
++this.cntr;
return delay(200);
}
async getJob(jobName) {
log(`getJob: ${this.greeting}`);
++this.cntr;
return delay(100);
}
}
const t = new Transcribe();
log("begin");
chain(t, ["getJob", "createJob"]).createJob().getJob().then(() => {
log(`cntr = ${t.cntr}`);
log("end");
});
Since this returns an actual promise (with additional methods attached), you can directly use .then() or await with it without the separate ._promise() that the first implementation required.
So, you can now do something like this:
const t = new Transcribe();
chain(t, ["getJob", "createJob"]).createJob().getJob().then(() => {
log(`cntr = ${t.cntr}`);
});
or:
const t = new Transcribe();
await chain(t, ["getJob", "createJob"]).createJob().getJob();
log(`cntr = ${t.cntr}`);
And, here's a third version where it creates a thenable object (a pseudo-promise) with the added methods on it (if it bothers you to add methods to an existing promise):
function chain(orig, methodsArray) {
if (!methodsArray || !methodsArray.length) {
throw new Error("methodsArray argument must be array of chainable method names");
}
let masterP = Promise.resolve();
function makeThenable() {
let obj = {};
for (const m of methodsArray) {
obj[m] = function(...args) {
// chain onto master promise to force sequencing
masterP = masterP.then(result => {
return orig[m].apply(orig, ...args);
});
return makeThenable();
}
}
obj.then = function(onFulfill, onReject) {
return masterP.then(onFulfill, onReject);
}
obj.catch = function(onReject) {
return masterP.catch(onReject);
}
obj.finally = function(onFinally) {
return masterP.finally(onFinally);
}
return obj;
}
return makeThenable();
}
function delay(t) {
return new Promise(resolve => {
setTimeout(resolve, t);
});
}
function log(...args) {
if (!log.start) {
log.start = Date.now();
}
const delta = Date.now() - log.start;
const deltaPad = (delta + "").padStart(6, "0");
console.log(`${deltaPad}: `, ...args)
}
class Transcribe {
constructor() {
this.greeting = "Hello";
this.cntr = 0;
}
async createJob(params) {
log(`createJob: ${this.greeting}`);
++this.cntr;
return delay(200);
}
async getJob(jobName) {
log(`getJob: ${this.greeting}`);
++this.cntr;
return delay(100);
}
}
const t = new Transcribe();
log("begin");
chain(t, ["getJob", "createJob"]).createJob().getJob().then(() => {
log(`cntr = ${t.cntr}`);
log("end");
});
Hello I am stuck with indexedDB. When I try to store an array of links, it fails with no errors or exceptions.
I have two code examples. This one works fine:
export const IndexedDB = {
initDB(): Promise<string> {
return new Promise((resolve, reject) => {
const open = indexedDB.open("MyDB", 1);
open.onupgradeneeded = function () {
open.result.createObjectStore("store", { keyPath: "id" });
resolve("Database initialized successfully...");
};
open.onerror = function (err) {
reject(err);
};
open.onsuccess = function () {
resolve("Database initialized successfully...");
};
});
},
getAll(): Promise<Values> {
return new Promise((resolve, reject) => {
const open = indexedDB.open("MyDB", 1);
open.onerror = function (err) {
reject(err);
};
open.onsuccess = function () {
const tx = open.result.transaction("store", "readonly");
const store = tx.objectStore("store");
const getFromStore = store.getAll();
getFromStore.onerror = function (err) {
reject(err);
};
getFromStore.onsuccess = function () {
resolve(getFromStore.result);
};
};
});
},
putMany(values: Values): Promise<void> {
return new Promise((resolve, reject) => {
const open = indexedDB.open("MyDB", 1);
open.onerror = function (err) {
reject(err);
};
open.onsuccess = function () {
values.forEach((value: Value) => {
const tx = open.result.transaction("store", "readwrite");
const store = tx.objectStore("store");
const putInStore = store.put(value);
putInStore.onerror = function (err) {
reject(err);
};
});
resolve();
};
});
},
};
App.tsx:
export const saveValues = (values: Values): Promise<void> => {
IndexedDB.putMany(values);
};
export const App: React.FunctionComponent = () => {
const valuesForTest: Values = [
{
id: Math.random(),
text: "Hello world!"
},
{
id: Math.random(),
text: "Hello world!"
},
{
id: Math.random(),
text: "Hello world!"
}
];
useEffect(() => {
saveValuesToDB(valuesForTest);
}, []);
return (
<SomeComponent/>
);
};
And this one always fails. No errors or exceptions at all. The data just isn't saved.
The main difference is just data types. The first example uses the "Values" type, and the second uses the "Links" type.
export const IndexedDB = {
initDB(): Promise<string> {
return new Promise((resolve, reject) => {
const open = indexedDB.open("MyDB", 1);
open.onupgradeneeded = function () {
open.result.createObjectStore("store", { keyPath: "href" });
resolve("Database initialized successfully...");
};
open.onerror = function (err) {
reject(err);
};
open.onsuccess = function () {
resolve("Database initialized successfully...");
};
});
},
getAll(): Promise<Links> {
return new Promise((resolve, reject) => {
const open = indexedDB.open("MyDB", 1);
open.onerror = function (err) {
reject(err);
};
open.onsuccess = function () {
const tx = open.result.transaction("store", "readonly");
const store = tx.objectStore("store");
const getFromStore = store.getAll();
getFromStore.onerror = function (err) {
reject(err);
};
getFromStore.onsuccess = function () {
resolve(getFromStore.result);
};
};
});
},
putMany(values: Links): Promise<void> {
return new Promise((resolve, reject) => {
const open = indexedDB.open("MyDB", 1);
open.onerror = function (err) {
reject(err);
};
open.onsuccess = function () {
values.forEach((value: Link) => {
const tx = open.result.transaction("store", "readwrite");
const store = tx.objectStore("store");
const putInStore = store.put(value);
putInStore.onerror = function (err) {
reject(err);
};
});
resolve();
};
});
},
};
App.tsx:
export const saveValues = (values: Links): Promise<void> => {
IndexedDB.putMany(values);
};
export const App: React.FunctionComponent = () => {
const valuesForTest: Links = [
{
href: "https://somelink1.com/",
name: "Link"
},
{
href: "https://somelink2.com/",
name: "Link"
},
{
href: "https://somelink3.com/",
name: "Link"
},
];
useEffect(() => {
saveValuesToDB(valuesForTest);
}, []);
return (
<SomeComponent/>
);
};
Can you help me?
If you created the database with the first code snippets, your "store" object store will be using id as keyPath. If you want to change this to href as in your second snippet, delete the database first (in the browser devtools), then run your second snippet.
Kindly note that if you want to make schema changes to an existing database (without deleting it), you will have to pass a higher version number to indexedDB.open to trigger your onupgradeneeded callback: indexedDB.open("MyDB", 2);. If the database exists with the same version, that callback will (obviously) not be executed because it doesn't need upgrading.
Also note that if you want to use both your code snippets to store different things (values and links), use separate object stores for them: createObjectStore("values", { keyPath: "id" }) and createObjectStore("links", { keyPath: "href" })
Last but not least, for easier IndexedDB usage I can really recommend to use AceBase (fullblown realtime database) or Dexie (simple IndexedDB wrapper) instead. Those are easy to use and handle all IndexedDB plumbing!
EDIT: In your putMany method I also see you call resolve in the open.onsuccess callback, instead of waiting until all of your put operations have succeeded. That's why you don't get an error, reject will be called after resolve so that does nothing.
I am trying to export the value with instrument variable. however data is returning as [object Promise] than object. How can I assign module variable with the final result rather than the promise object.
var instruments = {
data: async () => {
return new Promise((resolve, reject) => {
/// Respond after retrieving the data
resolve({result : "...." }
);
}
}
var symbols = async () => {
const res = await instruments.data();
return res;
}
module.exports.instrument = symbols().then((data) => {
console.log('data');
return data;
}).catch((e) => {
console.log('error');
return {}
});
It looks like you want a singleton cache. Here is a basic implementation
cache.js
let data = {}
module.exports = {
getData: () => {
return data
},
setData: newData => {
data = newData
return
},
}
No need for async here. I would separate this code with the code that retrieves data.
fetchData.js
const cache = require('./cache')
const fetchData = () => {} // fetch data code here
fetchData().then(data => {
cache.setData(data)
})
try this
var instruments = {
data: () => {
return new Promise((resolve, reject) => {
/// Respond after retrieving the data
resolve({result : "...." });
}
}
var symbols = async () => {
const res = await instruments.data();
return res;
}
module.exports.instrument = symbols;
then import instrument method to call and then call
const instrument = require("./filepath");
instrument().then((data) => {
console.log('data');
}).catch((e) => {
console.log(e);
});
If your async function instruments.data() called, it'll await return Promise.
just append await at return for your expected result.
var instruments = {
data: async () => {
return await new Promise((resolve, reject) => {
// Respond after retrieving the data
resolve({result : "...." });
}
}
or remove async. it's same as above.
var instruments = {
data: () => {
return new Promise((resolve, reject) => {
// Respond after retrieving the data
resolve({result : "...." });
}
}
I am using node-serialport to communicate with a piece of hardware. It just writes a command and receives a response.
https://serialport.io/docs/en/api-parsers-overview
The following code works:
const port = new SerialPort(path);
const parser = port.pipe(new Readline({ delimiter: '\r', encoding: 'ascii' }));
const requestArray = [];
parser.on('data', (data) => {
// get first item in array
const request = requestArray[0];
// remove first item
requestArray.shift();
// resolve promise
request.promise.resolve(data);
});
export const getFirmwareVersion = async () => {
let resolvePromise;
let rejectPromise;
const promise = new Promise((resolve, reject) => {
resolvePromise = resolve;
rejectPromise = reject;
});
const title = 'getFirmwareVersion';
const cmd = 'V\r';
requestArray.push({
title,
cmd,
promise: {
resolve: resolvePromise,
reject: rejectPromise
}
});
await v2Port.write(cmd);
return promise;
};
Then from my app (which is written in electron/react) I can call the function:
<Button onClick={() => {
let data = await _api.getFirmwareVersion();
console.log('done waiting...');
console.log(data);
}>
Click Me
</Button>
Is there anyway I can refactor this code to make it more succinct?
Is there a way to get the Promise from the async function, rather than having to make a new Promise?
Is there a way to tap into the Transform Stream that already exists and pipe the Promise in there somehow?
I'm also new to async/await, and wanted to avoid using callbacks, especially in the React/Redux side of things.
I aim to have a lot of these endpoints for the api (i.e. getFirmwareVersion, getTemperature, etc...). So I want to make the code as concise as possible. I don't want the UI to have any underlying knowledge of how the API is getting the data. It just needs to request it like any other API and wait for a response.
Oh, I think I get it. The parser is receiving data constantly. So when a request comes, you wait for the next data and send it when it arrives. I suggest you to write an intermediate class.
Like this:
const SerialPort = require('serialport')
const Readline = require('#serialport/parser-readline')
const { EventEmitter } = require('events');
class SerialPortListener extends EventEmitter {
constructor(path) {
super();
this.serialPortPath = path;
}
init() {
this.serialPort = new SerialPort(this.serialPortPath);
const parser = this.serialPort.pipe(new Readline({ delimiter: '\r', encoding: 'ascii' }));
parser.on('data', data => this.emit('data', data));
}
}
Then you could modify the getFirmwareVersion like this:
const serialPortListener = new SerialPortListener(path);
serialPortListener.init();
export const getFirmwareVersion = () => {
return new Promise((resolve, reject) => {
serialPortListener.once('data', async (data) => {
try {
const cmd = 'V\r';
await v2Port.write(cmd);
resolve(data);
} catch (ex) {
reject(ex);
}
});
});
};
Based on help from Mehmet, here is what I ended up with:
const _port = new SerialPort(path);
const _parser = _port.pipe(new Readline({ delimiter: '\r', encoding: 'ascii' }));
const waitForData = async () => {
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => reject('Write Timeout'), 500);
_parser.once('data', (data) => {
clearTimeout(timeoutId);
resolve(data);
});
});
};
const createAPIFunction = (cmdTemplate, validationString) => {
return async (config) => {
try {
// replace {key} in template with config[key] props
const cmd = cmdTemplate.replace(/{(\w+)}/g, (_, key) => {
return config[key];
});
_port.write(cmd + '\r');
const data = await waitForData();
// validate data
if (data.startsWith(validationString)) {
// is valid
return data;
} else {
// invalid data
throw new Error('Invalid Data Returned');
}
} catch (err) {
throw err;
}
};
};
export const getFirmwareVersion = createAPIFunction('V', 'V1');
export const enableSampling = createAPIFunction('G1{scope}', 'G11');
I am trying to write a API to handle my indexedDB functionality. I am having trouble storing the db object in a class, because I have to wait for the .onsuccess events from the .open() request to fire.
So I wrote a method to initialize the db:
async initializeDB() {
return new Promise((resolve, reject) => {
const {
dbVersion,
databaseName,
fieldsObjectStoreName,
filedObjectStoreKeyName
} = IndexDbParams;
// Open a connection to indexDB
const DbOpenRequest = window.indexedDB.open(databaseName, dbVersion);
DbOpenRequest.onsuccess = e => {
const db = DbOpenRequest.result;
// Create data stores if none exist
if (db.objectStoreNames.length < 1) {
if (db.objectStoreNames.indexOf(fieldsObjectStoreName) < 0) {
db.createObjectStore(fieldsObjectStoreName, {
keyPath: filedObjectStoreKeyName
});
}
}
// return db object, will come hore from onupgradeneeded as well
resolve(db);
};
// If we need to upgrade db version
DbOpenRequest.onupgradeneeded = e => {
const db = event.target.result;
const objectStore = db.createObjectStore(fieldsObjectStoreName, {
keyPath: filedObjectStoreKeyName
});
};
});
}
Which I would then call at the beginning of all my other methods, for example:
async getData() {
this.initializeDB().then(db => {
// do stuff with the db object
})
}
My question is - is this wasting a lot more resources than calling .open() once, and then storing it in global state? What (if any) could be the possible consequences of this approach?
Definitely not wasteful.
All my projects look something like this:
function openDb() {
return new Promise(function(resolve, reject) {
var request = indexedDB.open();
request.onsuccess = () => resolve(request.result);
});
}
function doSomeDbAction() {
openDb().then(function(db) {
// do stuff with db
}).catch(console.warn).finally(function(db) {
if(db) db.close();
});
}
function doAnotherDbAction() {
openDb().then(function(db) {
// do more stuff with db
}).catch(console.warn).finally(function(db) {
if(db) db.close();
});
}