Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: catch and log electron-store.set errors #2547

Merged

Conversation

SgtPooki
Copy link
Member

@SgtPooki SgtPooki commented Jul 7, 2023

Technically, this is a partial fix for #2336 because we're not sure which value
or key is being used when store.set was triggering the error in that issue.

  • feat: add store.safeSet function and fix tests
  • feat: use store.safeSet

fixes #2336

@SgtPooki SgtPooki requested review from a team and whizzzkid as code owners July 7, 2023 19:38
@SgtPooki SgtPooki linked an issue Jul 7, 2023 that may be closed by this pull request
@SgtPooki SgtPooki self-assigned this Jul 7, 2023
Copy link
Contributor

@whizzzkid whizzzkid left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

questions and concerns around safeStoreSet

module.exports = async function (key, value, onSuccessFn) {
try {
store.set(key, value)
return onSuccessFn?.()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we absolutely need to pass a callback? I would rather return a success boolean and let the caller decide what needs to happen next.

But if we want to use it, return await is required here (because try-catch) also, can we check typeof onSuccessFn === 'function' or typeof onSuccessFn instanceof Promise before calling it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do think we should have some sort of success identifier, regardless of what it is. Part of the problem with the existing code is that we assume the values we're setting are actually being set even though they may not be.

In general, I am not a fan of putting the burden on consumers for APIs. If we were to return a true/false, then the responsibility of control flow is on the consumer to check whether they set things successfully or not, and then call their own function. If we were going to do that, it would be better to just wrap all store.set() calls in a try/catch where it's used, instead of using safeStoreSet, but I feel like that's a lot for using a simple keyValue store.

return await is required here (because try-catch)

try-catch doesn't necessitate the use of return await. A return await keeps the awaited promise inside the call-stack, instead of throwing separately.

By not awaiting the onSuccessFn, we require consumers to handle errors in their own function. We don't want to have safe-store-set be responsible for catching and logging errors for functions completely unrelated to the store, which is generally what the onSuccessFn are used for. e.g. set new apiUrl, then trigger ipfs-daemon restart.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could await the onSuccessFunction and then throw a separate error there while identifying the key used

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am trying to understand if it's only the try-catch that's providing safety.

Can we not add safety to the existing implementation by using a proxy? Maybe something like:

const store = new Store({
  defaults,
  migrations
})

const patchedImplementations = {
  set: (target) => async (key, value, onSuccessFn) => {
    try {
      target.set(key, value)
      return await onSuccessFn?.()
    } catch (err) {
      logger.error(`Could not set store key '${key}' to '${value}'`, /** @type {Error} */(err))
    }
  }
}

const patchedStore = new Proxy(store, {
  get (target, prop, receiver) {
    if (prop in patchedImplementations) {
      return patchedImplementation[prop](target);
    }
    return Reflect.get(...arguments);
  }
});

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I explicitly didn't use a proxy because we use the store so often, but I don't imagine it would hurt too much, and it would be significantly less code.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked into this and using the proxy is odd because the store is supposed to be electron-store and we'd actually be modifying the API to accept a third parameter, instead of explicitly calling a separate method.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SgtPooki missed your notification, github needs to fix this.

do we need to explicitly pass a callback? maybe the set implementation needs to be just:

const patchedImplementations = {
  set: (target) => (key, value) => {
    try {
      target.set(key, value)
    } catch (err) {
      logger.error(`Could not set store key '${key}' to '${value}'`, /** @type {Error} */(err))
    }
  }
}

// and then wherever you call it:

store.set(key, value) //already safe now this error can be caught?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a follow up when TS might be available, this can still be a proxy, with modified ambient type:

declare interface SafeStore extends Electron.store {
    set: (key, value, optionalFn) => void
}

then use that work with store eventually.

Copy link
Member Author

@SgtPooki SgtPooki left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self review

Comment on lines +126 to +137
error: (errMsg, error) => {
if (errMsg instanceof Error) {
Countly.log_error(errMsg)
logger.error(errMsg)
} else if (error != null && error instanceof Error) {
// errorMessage is not an instance of an error, but error is
Countly.log_error(error)
logger.error(errMsg, error)
} else {
Countly.log_error(new Error(errMsg))
logger.error(errMsg, error)
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix error logging

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked into this and using the proxy is odd because the store is supposed to be electron-store and we'd actually be modifying the API to accept a third parameter, instead of explicitly calling a separate method.

return store
}
function MockElectronStoreConstructor ({ ...options }) {
return new Store({ ...options, migrations: undefined })
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using the real electron-store, but removing any migrations to prevent CI failures.

@SgtPooki SgtPooki requested a review from whizzzkid July 18, 2023 23:43
Copy link
Member Author

@SgtPooki SgtPooki left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self review

Copy link
Contributor

@whizzzkid whizzzkid left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +78 to +108
/**
* @extends {Store<import('./types').DesktopPersistentStore>}
*/
class StoreWrapper extends Store {
constructor (options) {
super(options)

/**
* @template {unknown} R
* @param {string} key
* @param {unknown} value
* @param {() => Promise<R>|R|void} [onSuccessFn]
* @returns {Promise<R|void>}
*/
this.safeSet = async function (key, value, onSuccessFn) {
try {
this.set(key, value)
if (typeof onSuccessFn === 'function') {
try {
return await onSuccessFn()
} catch (err) {
logger.error(`[store.safeSet] Error calling onSuccessFn for '${key}'`, /** @type {Error} */(err))
}
}
} catch (err) {
logger.error(`[store.safeSet] Could not set store key '${key}' to '${value}'`, /** @type {Error} */(err))
}
}
}
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TYSM ❤️

@SgtPooki SgtPooki merged commit 536710e into main Aug 9, 2023
7 checks passed
@SgtPooki SgtPooki deleted the 2336-gui-error-report-typeerror-use-delete-to-clear-values branch August 9, 2023 20:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[gui error report] TypeError: Use delete() to clear values
2 participants