-
Notifications
You must be signed in to change notification settings - Fork 4.9k
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
MV3: add retry logic to actions #15337
Changes from 42 commits
620c4ea
617610a
68d0aae
949a95d
12b1504
1b6233d
581ccfa
b81a060
0b4ddd9
3ef2439
5ffe662
4688316
4342f4a
3b4a007
220c9e4
fbd2b7c
29d8a0a
fc9d189
30094ba
33f1a1d
9a76a87
e48a245
acd5f2e
5c7da1f
85e5bb1
721957d
23ed652
080cd36
baa8ef0
7000616
3b11f94
0c19e3e
8161b91
16022ad
11388b4
19af2f4
b67442c
678f4af
85c7075
825205b
650a94c
a17b32a
cebe284
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
import * as actions from '../../ui/store/actions'; | ||
import { _setBackgroundConnection } from '../../ui/store/action-queue'; | ||
|
||
export const setBackgroundConnection = (backgroundConnection = {}) => { | ||
actions._setBackgroundConnection(backgroundConnection); | ||
_setBackgroundConnection(backgroundConnection); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,200 @@ | ||
import pify from 'pify'; | ||
naugtur marked this conversation as resolved.
Show resolved
Hide resolved
|
||
import { isManifestV3 } from '../../../shared/modules/mv3.utils'; | ||
|
||
// // A simplified pify maybe? | ||
// function pify(apiObject) { | ||
// return Object.keys(apiObject).reduce((promisifiedAPI, key) => { | ||
// if (apiObject[key].apply) { // depending on our browser support we might use a nicer check for functions here | ||
// promisifiedAPI[key] = function (...args) { | ||
// return new Promise((resolve, reject) => { | ||
// return apiObject[key]( | ||
// ...args, | ||
// (err, result) => { | ||
// if (err) { | ||
// reject(err); | ||
// } else { | ||
// resolve(result); | ||
// } | ||
// }, | ||
// ); | ||
// }); | ||
// }; | ||
// } | ||
// return promisifiedAPI; | ||
// }, {}); | ||
// } | ||
|
||
let background = null; | ||
let promisifiedBackground = null; | ||
|
||
const actionRetryQueue = []; | ||
|
||
function failQueue() { | ||
actionRetryQueue.forEach(({ reject }) => | ||
reject( | ||
Error('Background operation cancelled while waiting for connection.'), | ||
), | ||
); | ||
} | ||
|
||
/** | ||
* Drops the entire actions queue. Rejects all actions in the queue unless silently==true | ||
* Does not affect the single action that is currently being processed. | ||
* | ||
* @param {boolean} [silently] | ||
*/ | ||
export function dropQueue(silently) { | ||
if (!silently) { | ||
failQueue(); | ||
} | ||
actionRetryQueue.length = 0; | ||
} | ||
|
||
// add action to queue | ||
const executeActionOrAddToRetryQueue = (item) => { | ||
if (actionRetryQueue.some((act) => act.actionId === item.actionId)) { | ||
return; | ||
} | ||
|
||
if (background.connectionStream.readable) { | ||
executeAction({ | ||
action: item, | ||
disconnectSideeffect: () => actionRetryQueue.push(item), | ||
}); | ||
} else { | ||
actionRetryQueue.push(item); | ||
} | ||
}; | ||
|
||
/** | ||
* Promise-style call to background method | ||
* In MV2: invokes promisifiedBackground method directly. | ||
* In MV3: action is added to retry queue, along with resolve handler to be executed on completion, | ||
* the queue is then immediately processed if background connection is available. | ||
* On completion (successful or error) the action is removed from the retry queue. | ||
* | ||
* @param {string} method - name of the background method | ||
* @param {Array} [args] - arguments to that method, if any | ||
* @param {any} [actionId] - if an action with the === same id is submitted, it'll be ignored if already in queue waiting for a retry. | ||
* @returns {Promise} | ||
*/ | ||
export function submitRequestToBackground( | ||
method, | ||
args = [], | ||
actionId = Date.now() + Math.random(), // current date is not guaranteed to be unique | ||
digiwand marked this conversation as resolved.
Show resolved
Hide resolved
|
||
) { | ||
if (isManifestV3) { | ||
return new Promise((resolve, reject) => { | ||
executeActionOrAddToRetryQueue({ | ||
actionId, | ||
request: { method, args }, | ||
resolve, | ||
reject, | ||
}); | ||
}); | ||
} | ||
return promisifiedBackground[method](...args); | ||
} | ||
|
||
/** | ||
* Callback-style call to background method | ||
* In MV2: invokes promisifiedBackground method directly. | ||
* In MV3: action is added to retry queue, along with resolve handler to be executed on completion, | ||
* the queue is then immediately processed if background connection is available. | ||
* On completion (successful or error) the action is removed from the retry queue. | ||
* | ||
* @param {string} method - name of the background method | ||
* @param {Array} [args] - arguments to that method, if any | ||
* @param callback - Node style (error, result) callback for finishing the operation | ||
* @param {any} [actionId] - if an action with the === same id is submitted, it'll be ignored if already in queue. | ||
*/ | ||
export const callBackgroundMethod = ( | ||
method, | ||
args = [], | ||
callback, | ||
actionId = Date.now() + Math.random(), // current date is not guaranteed to be unique | ||
digiwand marked this conversation as resolved.
Show resolved
Hide resolved
|
||
) => { | ||
if (isManifestV3) { | ||
const resolve = (value) => callback(null, value); | ||
const reject = (err) => callback(err); | ||
executeActionOrAddToRetryQueue({ | ||
actionId, | ||
request: { method, args }, | ||
resolve, | ||
reject, | ||
}); | ||
} else { | ||
background[method](...args, callback); | ||
} | ||
}; | ||
|
||
async function executeAction({ action, disconnectSideeffect }) { | ||
const { | ||
request: { method, args }, | ||
resolve, | ||
reject, | ||
} = action; | ||
try { | ||
resolve(await promisifiedBackground[method](...args)); | ||
} catch (err) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @naugtur , I have added actionId here as the last argument, it is useful to make action call idempotent in some scenario. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It crosses abstraction boundaries. Now all
IMHO, if we want to add it, it'd need to be the first argument and be made mandatory, otherwise it'll hurt us later. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I kept it last argument so that methods deal with it only if they need to. If we do not want to pass it to all background methods, I can pass it specifically where needed. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Intuition tells me it's a footgun. Current change visible here sends it or doesn't send it depending on the current state - you only added it in the queue processing but it's not defined if connection was available. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I changed it to pass actionId only when needed. |
||
if ( | ||
background.DisconnectError && // necessary to not break compatibility with background stubs or non-default implementations | ||
err instanceof background.DisconnectError | ||
) { | ||
disconnectSideeffect(action); | ||
} else { | ||
reject(err); | ||
} | ||
} | ||
} | ||
|
||
let processingQueue = false; | ||
|
||
// Clears list of pending action in actionRetryQueue | ||
// The results of background calls are wired up to the original promises that's been returned | ||
// The first method on the queue gets called synchronously to make testing and reasoning about | ||
// a single request to an open connection easier. | ||
async function processActionRetryQueue() { | ||
if (processingQueue) { | ||
return; | ||
} | ||
processingQueue = true; | ||
try { | ||
while ( | ||
background.connectionStream.readable && | ||
actionRetryQueue.length > 0 | ||
) { | ||
// If background disconnects and fails the action, the next one will not be taken off the queue. | ||
// Retrying an action that failed because of connection loss while it was processing is not supported. | ||
const item = actionRetryQueue.shift(); | ||
await executeAction({ | ||
action: item, | ||
disconnectSideeffect: () => actionRetryQueue.unshift(item), | ||
}); | ||
} | ||
} catch (e) { | ||
// error in the queue mechanism itself, the action was malformed | ||
console.error(e); | ||
} | ||
processingQueue = false; | ||
} | ||
|
||
/** | ||
* Sets/replaces the background connection reference | ||
* Under MV3 it also triggers queue processing if the new background is connected | ||
* | ||
* @param {*} backgroundConnection | ||
*/ | ||
export async function _setBackgroundConnection(backgroundConnection) { | ||
background = backgroundConnection; | ||
promisifiedBackground = pify(background); | ||
if (isManifestV3) { | ||
if (processingQueue) { | ||
console.warn( | ||
'_setBackgroundConnection called while a queue was processing and not disconnected yet', | ||
); | ||
} | ||
// Process all actions collected while connection stream was not available. | ||
processActionRetryQueue(); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NIT: my brain sees underscore before a variable/method and it thinks
private
variable/method.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would avoid changing existing method in this PR.