1
\$\begingroup\$

I have made an async function in TypeScript that responds to Events and returns an object with methods that return a promise. I would like help making it better and prettier. :)

My interface files:

config.model.ts

export interface IDBUConfigModel {
    version: number;
    dbName: string;
    storeNames: string[];
    keyPath?: string;
}

IDBUtility.model.ts

export interface IDBUtility {
    add: (storeName: string, value: {}) => Promise<string | {}>;
    put: (storeName: string, value: {}) => Promise<string | {}>;
    update: (storeName: string, keyValue: string, value: {}) => Promise<string | {}>;
    get: (storeName: string, keyValue: string) => Promise<any>;
    remove: (storeName: string, keyValue: string) => Promise<{} | void>;
}

My main function:

import { IDBUConfigModel } from '../models/config.model';
import { IDBUtility } from '../models/idb-utility.model';

export async function openIDB(config: IDBUConfigModel): Promise<IDBUtility> {
    if (!window.indexedDB) {
        // console.log("Your browser doesn't support a stable version of IndexedDB. IndexedDB will not be available.");
        return void 0;
    }
    return new Promise<IDBUtility>((resolve, reject) => {
        const request = indexedDB.open(config.dbName, config.version);
        request.onerror = (evt: ErrorEvent | any) => {
            reject(request.result);
        };
        request.onupgradeneeded = (evt: IDBVersionChangeEvent | any): void => {
            const nextDb = evt.target.result;
            if(config.keyPath){
                config.storeNames
                    .forEach((storeName: string) => {
                        nextDb.createObjectStore(
                            storeName,
                            {
                                keyPath: config.keyPath
                            }
                        );
                    });
            } else {
                config.storeNames
                    .forEach((storeName: string) => {
                        nextDb.createObjectStore(
                            storeName,
                            {
                                autoIncrement: true
                            }
                        );
                    });
            }
        };
        request.onsuccess = (evt) => {
            const db = request.result;
            resolve({
                async add(storeName: string, value: {}): Promise<string | {}> {
                    return new Promise((res, rej) => {
                        const request = db.transaction([storeName], 'readwrite')
                            .objectStore(`${storeName}`)
                            .add(value);
                        request.onsuccess = (evt) => {
                            res(request.result);
                        };
                        request.onerror = () => {
                            rej(request.result);
                        };
                    });
                },
                async put(storeName: string, value: {}): Promise<string | {}> {
                    return new Promise((res, rej) => {
                        const request = db.transaction([storeName], 'readwrite')
                            .objectStore(storeName)
                            .put(value);
                        request.onsuccess = () => {
                            res(request.result);
                        };
                        request.onerror = () => {
                            rej(request.result);
                        };
                    });
                },
                async update(storeName: string, key: string, value: ({} | any[])): Promise<string | {}> {
                    return new Promise((res, rej) => {
                        const transaction = db.transaction([storeName], 'readwrite');
                        const getRequest = transaction
                            .objectStore(storeName)
                            .get(key);
                        transaction.onerror = () => {
                            rej(request.result);
                        };
                        getRequest.onsuccess = () => {
                            const currentValue = getRequest.result;
                            const updatedValue = mergeDeep(currentValue, value);
                            const delRequest = transaction
                                .objectStore(storeName)
                                .delete(key);
                            delRequest.onsuccess = () => {
                                const addRequest = transaction
                                    .objectStore(storeName)
                                    .add(updatedValue);
                                addRequest.onsuccess = () => {
                                    res(addRequest.result);
                                };
                            };
                        };
                    });
                },
                async remove(storeName: string, keyValue: string): Promise<any> {
                    return new Promise((res, rej) => {
                        const delRequest = db.transaction([storeName], 'readwrite')
                            .objectStore(storeName)
                            .delete(keyValue);
                        delRequest.onsuccess = () => {
                            res(delRequest.result);
                        };
                        delRequest.onerror = () => {
                            rej(delRequest.result);
                        };
                    });
                },
                async get(storeName: string, key: string): Promise<{}> {
                    return new Promise((res, rej) => {
                        const request = db.transaction([storeName])
                            .objectStore(storeName)
                            .get(key);
                        request.onsuccess = () => {
                            res(request.result);
                        };
                        request.onerror = () => {
                            rej(request.result);
                        };
                    });
                }
            });
        };
    });
}

function mergeDeep (target, source)  {
    if (typeof target == "object" && typeof source == "object") {
        for (const key in source) {
            if (source[key] === null && (target[key] === undefined || target[key] === null)) {
                target[key] = null;
            } else if (source[key] instanceof Array) {
                if (!target[key]) target[key] = [];
                target[key] = target[key].concat(source[key]);
            } else if (typeof source[key] == "object") {
                if (!target[key]) target[key] = {};
                this.mergeDeep(target[key], source[key]);
            } else {
                target[key] = source[key];
            }
        }
    }
    return target;
}
\$\endgroup\$
1
\$\begingroup\$

I consider myself no expert in the field, but I think I can give you some small tips :). Here you go:

  • Writing a function as async makes it return a Promise. As you already wrote the return type Promise<IDBUtility>, declaring the function as async makes it redundant. In Typescript, an async function can include await expressions to simplify the Promise behaviour. MDN

  • I recommend you looking up some NPM packages to simplify your tasks. There is no need to reinvent the wheel. I believe these might be helpful for you: https://www.npmjs.com/package/idb
    https://www.npmjs.com/package/idb-keyval

  • The union types are very useful in Typescript, but doesn't add any information to the type when you join something with any, as the joint type is already covered by any. Therefore, in evt: ErrorEvent | any, evt can be an ErrorEvent or anything else. This makes it not type safe, as you will have to type-cast it anyway. If you need to cover more kind of events than just ErrorEvent, you could maybe use the class it extends (evt: Event) or make your own custom events by extending Event.

    function add(storeName: string, value: {}): Promise<string | {}> {
        return new Promise((res, rej) => {
            const request = db.transaction([storeName], 'readwrite')
                .objectStore(`${storeName}`)
                .add(value);
            request.onsuccess = (evt) => {
                res(request.result);
            };
            request.onerror = () => {
                rej(request.result);
            };
        });
    }
    

    you could rewrite it to something like this:

    async function add(storeName: string, value: any): Promise<any> {
        const request = await db.transaction([storeName], 'readwrite')
            .objectStore(storeName).add(value);
    
        if (request.isValid()) {
            return Promise.resolve(request.result);
        }
    
        return Promise.reject(`Add transaction failed: ${request.error}`);
    }
    

    It's 3 LoC shorter and easier to understand what's going on (although, keep in mind that I made it up!). There are three significant instructions that can be caught in just a glimpse:

    1. Async transaction
    2. Return valid transaction
    3. Return transaction error

    You will probably want to add some catching to make your database connection tolerant to errors. With the approach I'm trying to explain, shouldn't be hard to extend your code and still leave it readable and maintainable.

  • I suggest you to study how to create clean, readable and easy-to-maintain code. The function you provided has way too many indentation levels for a function with that complexity (which shouldn't have either!).

    A function should do one thing, and do it well.

    A 100-line function might be easy to write, but it's hard to understand for a person who hasn't been following your code development (or even yourself after certain amount of time). I strongly suggest Clean Code by Robert C. Martin. Related question.

Good luck! :)

| improve this answer | |
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.