From b8cf3a33b41f13267627079447bceb6ea2806e6e Mon Sep 17 00:00:00 2001 From: hyphenized <28708889+hyphenized@users.noreply.github.com> Date: Mon, 24 Jul 2023 21:29:58 -0500 Subject: [PATCH 1/9] Revert "Revert "Debounce dispatched state diffs"" This reverts commit b8d911bbcc283681e0409e49104e7669bfd13265. --- background/main.ts | 24 ++++++++++++++++++--- patches/webext-redux+2.1.7.patch | 37 ++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/background/main.ts b/background/main.ts index 851e86fe08..b9c9a4b0af 100644 --- a/background/main.ts +++ b/background/main.ts @@ -508,12 +508,30 @@ export default class Main extends BaseService { // Start up the redux store and set it up for proxying. this.store = initializeStore(savedReduxState, this) + const queueUpdate = debounce( + (lastState, newState, updateFn) => { + if (lastState === newState) { + return + } + + const diff = deepDiff(lastState, newState) + + if (diff !== undefined) { + updateFn(newState, [diff]) + } + }, + 30, + { maxWait: 30, trailing: true } + ) + wrapStore(this.store, { serializer: encodeJSON, deserializer: decodeJSON, - diffStrategy: (oldObj, newObj) => { - const diffWrapper = deepDiff(oldObj, newObj) - return diffWrapper === undefined ? [] : [diffWrapper] + diffStrategy: (oldObj, newObj, forceUpdate) => { + queueUpdate(oldObj, newObj, forceUpdate) + + // Return no diffs as we're manually handling these inside `queueUpdate` + return [] }, dispatchResponder: async ( dispatchResult: Promise, diff --git a/patches/webext-redux+2.1.7.patch b/patches/webext-redux+2.1.7.patch index 6f1b70506a..eb5da3a069 100644 --- a/patches/webext-redux+2.1.7.patch +++ b/patches/webext-redux+2.1.7.patch @@ -27,6 +27,18 @@ index 88b6e4f..f16b6dd 100644 @@ -1 +1 @@ -!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e=e||self).WebextRedux={})}(this,(function(e){"use strict";function t(e,t){for(var r=0;t.length>r;r++){var n=t[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}function r(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}function n(e){for(var t=1;arguments.length>t;t++){var n=null!=arguments[t]?arguments[t]:{},o=Object.keys(n);"function"==typeof Object.getOwnPropertySymbols&&(o=o.concat(Object.getOwnPropertySymbols(n).filter((function(e){return Object.getOwnPropertyDescriptor(n,e).enumerable})))),o.forEach((function(t){r(e,t,n[t])}))}return e}function o(e){return function(e){if(Array.isArray(e)){for(var t=0,r=Array(e.length);e.length>t;t++)r[t]=e[t];return r}}(e)||function(e){if(Symbol.iterator in Object(e)||"[object Arguments]"===Object.prototype.toString.call(e))return Array.from(e)}(e)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance")}()}var i=9007199254740991,a="[object Arguments]",s="[object Function]",u="[object GeneratorFunction]",c=/^(?:0|[1-9]\d*)$/;function l(e,t,r){switch(r.length){case 0:return e.call(t);case 1:return e.call(t,r[0]);case 2:return e.call(t,r[0],r[1]);case 3:return e.call(t,r[0],r[1],r[2])}return e.apply(t,r)}var f=Object.prototype,d=f.hasOwnProperty,p=f.toString,h=f.propertyIsEnumerable,y=Math.max;function v(e,t){var r=P(e)||function(e){return function(e){return function(e){return!!e&&"object"==typeof e}(e)&&j(e)}(e)&&d.call(e,"callee")&&(!h.call(e,"callee")||p.call(e)==a)}(e)?function(e,t){for(var r=-1,n=Array(e);++r-1&&e%1==0&&t>e}function w(e,t){return e===t||e!=e&&t!=t}var S,E,x,P=Array.isArray;function j(e){return null!=e&&function(e){return"number"==typeof e&&e>-1&&e%1==0&&i>=e}(e.length)&&!function(e){var t=A(e)?p.call(e):"";return t==s||t==u}(e)}function A(e){var t=typeof e;return!!e&&("object"==t||"function"==t)}var O=(S=function(e,t){!function(e,t,r,n){r||(r={});for(var o=-1,i=t.length;++o1?t[n-1]:void 0,i=n>2?t[2]:void 0;for(o=S.length>3&&"function"==typeof o?(n--,o):void 0,i&&function(e,t,r){if(!A(r))return!1;var n=typeof t;return!!("number"==n?j(r)&&b(t,r.length):"string"==n&&t in r)&&w(r[t],e)}(t[0],t[1],i)&&(o=3>n?void 0:o,n=1),e=Object(e);++r1&&void 0!==arguments[1]?arguments[1]:M;return n({},e,e.payload?{payload:t(e.payload)}:{})},R=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:M,r=arguments.length>2?arguments[2]:void 0;return r?function(n){for(var o=arguments.length,i=Array(o>1?o-1:0),a=1;o>a;a++)i[a-1]=arguments[a];return r.apply(void 0,[n].concat(i))?e.apply(void 0,[N(n,t)].concat(i)):e.apply(void 0,[n].concat(i))}:function(r){for(var n=arguments.length,o=Array(n>1?n-1:0),i=1;n>i;i++)o[i-1]=arguments[i];return e.apply(void 0,[N(r,t)].concat(o))}},I=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:M;return function(t){return function(r,n){return t(R(r,e,n))}}},L=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:M;return function(t){var r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0;return function(){for(var n=arguments.length,o=Array(n),i=0;n>i;i++)o[i]=arguments[i];if(r>=o.length)throw Error("Message in request could not be serialized. "+"Expected message in position ".concat(r," but only received ").concat(o.length," args."));return o[r]=N(o[r],e),t.apply(void 0,o)}}},_="updated",C="removed";function H(){var e;try{e=self.chrome||self.browser||browser}catch(t){e=browser}if(!e)throw Error("Browser API is not present");return e}var q={portName:"chromex.port_name",state:{},extensionId:null,serializer:M,deserializer:M,patchStrategy:function(e,t){var r=Object.assign({},e);return t.forEach((function(e){var t=e.key,n=e.value;switch(e.change){case _:r[t]=n;break;case C:Reflect.deleteProperty(r,t)}})),r}};function D(){for(var e=arguments.length,t=Array(e),r=0;e>r;r++)t[r]=arguments[r];return 0===t.length?function(e){return e}:1===t.length?t[0]:t.reduce((function(e,t){return function(){return e(t.apply(void 0,arguments))}}))}var F={portName:"chromex.port_name",dispatchResponder:function(e,t){Promise.resolve(e).then((function(e){t({error:null,value:e})})).catch((function(e){console.error("error dispatching result:",e),t({error:e.message,value:null})}))},serializer:M,deserializer:M,diffStrategy:function(e,t){var r=[];return Object.keys(t).forEach((function(n){e[n]!==t[n]&&r.push({key:n,value:t[n],change:_})})),Object.keys(e).forEach((function(e){t.hasOwnProperty(e)||r.push({key:e,change:C})})),r}};e.Store=function(){function e(){var t=this,r=arguments.length>0&&void 0!==arguments[0]?arguments[0]:q,n=r.portName,o=void 0===n?q.portName:n,i=r.state,a=void 0===i?q.state:i,s=r.extensionId,u=void 0===s?q.extensionId:s,c=r.serializer,l=void 0===c?q.serializer:c,f=r.deserializer,d=void 0===f?q.deserializer:f,p=r.patchStrategy,h=void 0===p?q.patchStrategy:p;if(function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,e),!o)throw Error("portName is required in options");if("function"!=typeof l)throw Error("serializer must be a function");if("function"!=typeof d)throw Error("deserializer must be a function");if("function"!=typeof h)throw Error("patchStrategy must be one of the included patching strategies or a custom patching function");this.portName=o,this.readyResolved=!1,this.readyPromise=new Promise((function(e){return t.readyResolve=e})),this.browserAPI=H(),this.extensionId=u,this.port=this.browserAPI.runtime.connect(this.extensionId,{name:o}),this.safetyHandler=this.safetyHandler.bind(this),this.browserAPI.runtime.onMessage&&(this.safetyMessage=this.browserAPI.runtime.onMessage.addListener(this.safetyHandler)),this.serializedPortListener=I(d)((function(){var e;return(e=t.port.onMessage).addListener.apply(e,arguments)})),this.serializedMessageSender=L(l)((function(){var e;return(e=t.browserAPI.runtime).sendMessage.apply(e,arguments)}),1),this.listeners=[],this.state=a,this.patchStrategy=h,this.serializedPortListener((function(e){switch(e.type){case k:t.replaceState(e.payload),t.readyResolved||(t.readyResolved=!0,t.readyResolve());break;case z:t.patchState(e.payload)}})),this.dispatch=this.dispatch.bind(this)}var r,n,o;return r=e,(n=[{key:"ready",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:null;return null!==e?this.readyPromise.then(e):this.readyPromise}},{key:"subscribe",value:function(e){var t=this;return this.listeners.push(e),function(){t.listeners=t.listeners.filter((function(t){return t!==e}))}}},{key:"patchState",value:function(e){this.state=this.patchStrategy(this.state,e),this.listeners.forEach((function(e){return e()}))}},{key:"replaceState",value:function(e){this.state=e,this.listeners.forEach((function(e){return e()}))}},{key:"getState",value:function(){return this.state}},{key:"replaceReducer",value:function(){}},{key:"dispatch",value:function(e){var t=this;return new Promise((function(r,n){t.serializedMessageSender(t.extensionId,{type:"chromex.dispatch",portName:t.portName,payload:e},null,(function(e){var t=e.error,o=e.value;if(t){var i=Error("".concat("\nLooks like there is an error in the background page. You might want to inspect your background page for more details.\n").concat(t));n(O(i,t))}else r(o&&o.payload)}))}))}},{key:"safetyHandler",value:function(e){"storeReady"===e.action&&e.portName===this.portName&&(this.browserAPI.runtime.onMessage.removeListener(this.safetyHandler),this.readyResolved||(this.readyResolved=!0,this.readyResolve()))}}])&&t(r.prototype,n),o&&t(r,o),e}(),e.alias=function(e){return function(){return function(t){return function(r){var n=e[r.type];return t(n?n(r):r)}}}},e.applyMiddleware=function(e){for(var t=arguments.length,r=Array(t>1?t-1:0),n=1;t>n;n++)r[n-1]=arguments[n];var i=function(){throw Error("Dispatching while constructing your middleware is not allowed. Other middleware would not be applied to this dispatch.")},a={getState:e.getState.bind(e),dispatch:function(){return i.apply(void 0,arguments)}};return r=(r||[]).map((function(e){return e(a)})),i=D.apply(void 0,o(r))(e.dispatch),e.dispatch=i,e},e.wrapStore=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:F,r=t.portName,n=void 0===r?F.portName:r,o=t.dispatchResponder,i=void 0===o?F.dispatchResponder:o,a=t.serializer,s=void 0===a?F.serializer:a,u=t.deserializer,c=void 0===u?F.deserializer:u,l=t.diffStrategy,f=void 0===l?F.diffStrategy:l;if(!n)throw Error("portName is required in options");if("function"!=typeof s)throw Error("serializer must be a function");if("function"!=typeof c)throw Error("deserializer must be a function");if("function"!=typeof f)throw Error("diffStrategy must be one of the included diffing strategies or a custom diff function");var d=H(),p=function(t,r,o){if("chromex.dispatch"===t.type&&t.portName===n){var a=Object.assign({},t.payload,{_sender:r}),s=null;try{s=e.dispatch(a)}catch(e){s=Promise.reject(e.message),console.error(e)}return i(s,o),!0}},h=function(t){if(t.name===n){var r=L(s)((function(){return t.postMessage.apply(t,arguments)})),o=e.getState(),i=e.subscribe((function(){var t=e.getState(),n=f(o,t);n.length&&(o=t,r({type:z,payload:n}))}));t.onDisconnect.addListener(i),r({type:k,payload:o})}},y=I(c),v=function(e){return"chromex.dispatch"===e.type&&e.portName===n};y((function(){var e;return(e=d.runtime.onMessage).addListener.apply(e,arguments)}))(p,v),d.runtime.onMessageExternal?y((function(){var e;return(e=d.runtime.onMessageExternal).addListener.apply(e,arguments)}))(p,v):console.warn("runtime.onMessageExternal is not supported"),d.runtime.onConnect.addListener(h),d.runtime.onConnectExternal?d.runtime.onConnectExternal.addListener(h):console.warn("runtime.onConnectExternal is not supported"),d.tabs.query({},(function(e){var t=!0,r=!1,o=void 0;try{for(var i,a=e[Symbol.iterator]();!(t=(i=a.next()).done);t=!0){d.tabs.sendMessage(i.value.id,{action:"storeReady",portName:n},(function(){chrome}))}}catch(e){r=!0,o=e}finally{try{t||null==a.return||a.return()}finally{if(r)throw o}}}))},Object.defineProperty(e,"__esModule",{value:!0})})); +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e=e||self).WebextRedux={})}(this,(function(e){"use strict";function t(e,t){for(var r=0;t.length>r;r++){var n=t[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}function r(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}function n(e){for(var t=1;arguments.length>t;t++){var n=null!=arguments[t]?arguments[t]:{},o=Object.keys(n);"function"==typeof Object.getOwnPropertySymbols&&(o=o.concat(Object.getOwnPropertySymbols(n).filter((function(e){return Object.getOwnPropertyDescriptor(n,e).enumerable})))),o.forEach((function(t){r(e,t,n[t])}))}return e}function o(e){return function(e){if(Array.isArray(e)){for(var t=0,r=Array(e.length);e.length>t;t++)r[t]=e[t];return r}}(e)||function(e){if(Symbol.iterator in Object(e)||"[object Arguments]"===Object.prototype.toString.call(e))return Array.from(e)}(e)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance")}()}var i=9007199254740991,a="[object Arguments]",s="[object Function]",u="[object GeneratorFunction]",c=/^(?:0|[1-9]\d*)$/;function l(e,t,r){switch(r.length){case 0:return e.call(t);case 1:return e.call(t,r[0]);case 2:return e.call(t,r[0],r[1]);case 3:return e.call(t,r[0],r[1],r[2])}return e.apply(t,r)}var f=Object.prototype,d=f.hasOwnProperty,p=f.toString,h=f.propertyIsEnumerable,y=Math.max;function v(e,t){var r=P(e)||function(e){return function(e){return function(e){return!!e&&"object"==typeof e}(e)&&j(e)}(e)&&d.call(e,"callee")&&(!h.call(e,"callee")||p.call(e)==a)}(e)?function(e,t){for(var r=-1,n=Array(e);++r-1&&e%1==0&&t>e}function w(e,t){return e===t||e!=e&&t!=t}var S,E,x,P=Array.isArray;function j(e){return null!=e&&function(e){return"number"==typeof e&&e>-1&&e%1==0&&i>=e}(e.length)&&!function(e){var t=A(e)?p.call(e):"";return t==s||t==u}(e)}function A(e){var t=typeof e;return!!e&&("object"==t||"function"==t)}var O=(S=function(e,t){!function(e,t,r,n){r||(r={});for(var o=-1,i=t.length;++o1?t[n-1]:void 0,i=n>2?t[2]:void 0;for(o=S.length>3&&"function"==typeof o?(n--,o):void 0,i&&function(e,t,r){if(!A(r))return!1;var n=typeof t;return!!("number"==n?j(r)&&b(t,r.length):"string"==n&&t in r)&&w(r[t],e)}(t[0],t[1],i)&&(o=3>n?void 0:o,n=1),e=Object(e);++r1&&void 0!==arguments[1]?arguments[1]:M;return n({},e,e.payload?{payload:t(e.payload)}:{})},R=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:M,r=arguments.length>2?arguments[2]:void 0;return r?function(n){for(var o=arguments.length,i=Array(o>1?o-1:0),a=1;o>a;a++)i[a-1]=arguments[a];return r.apply(void 0,[n].concat(i))?e.apply(void 0,[N(n,t)].concat(i)):e.apply(void 0,[n].concat(i))}:function(r){for(var n=arguments.length,o=Array(n>1?n-1:0),i=1;n>i;i++)o[i-1]=arguments[i];return e.apply(void 0,[N(r,t)].concat(o))}},I=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:M;return function(t){return function(r,n){return t(R(r,e,n))}}},L=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:M;return function(t){var r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0;return function(){for(var n=arguments.length,o=Array(n),i=0;n>i;i++)o[i]=arguments[i];if(r>=o.length)throw Error("Message in request could not be serialized. "+"Expected message in position ".concat(r," but only received ").concat(o.length," args."));return o[r]=N(o[r],e),t.apply(void 0,o)}}},_="updated",C="removed";function H(){var e;try{e=self.chrome||self.browser||browser}catch(t){e=browser}if(!e)throw Error("Browser API is not present");return e}var q={portName:"chromex.port_name",state:{},extensionId:null,serializer:M,deserializer:M,patchStrategy:function(e,t){var r=Object.assign({},e);return t.forEach((function(e){var t=e.key,n=e.value;switch(e.change){case _:r[t]=n;break;case C:Reflect.deleteProperty(r,t)}})),r}};function D(){for(var e=arguments.length,t=Array(e),r=0;e>r;r++)t[r]=arguments[r];return 0===t.length?function(e){return e}:1===t.length?t[0]:t.reduce((function(e,t){return function(){return e(t.apply(void 0,arguments))}}))}var F={portName:"chromex.port_name",dispatchResponder:function(e,t){Promise.resolve(e).then((function(e){t({error:null,value:e})})).catch((function(e){console.error("error dispatching result:",e),t({error:e.message,value:null})}))},serializer:M,deserializer:M,diffStrategy:function(e,t){var r=[];return Object.keys(t).forEach((function(n){e[n]!==t[n]&&r.push({key:n,value:t[n],change:_})})),Object.keys(e).forEach((function(e){t.hasOwnProperty(e)||r.push({key:e,change:C})})),r}};e.Store=function(){function e(){var t=this,r=arguments.length>0&&void 0!==arguments[0]?arguments[0]:q,n=r.portName,o=void 0===n?q.portName:n,i=r.state,a=void 0===i?q.state:i,s=r.extensionId,u=void 0===s?q.extensionId:s,c=r.serializer,l=void 0===c?q.serializer:c,f=r.deserializer,d=void 0===f?q.deserializer:f,p=r.patchStrategy,h=void 0===p?q.patchStrategy:p;if(function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,e),!o)throw Error("portName is required in options");if("function"!=typeof l)throw Error("serializer must be a function");if("function"!=typeof d)throw Error("deserializer must be a function");if("function"!=typeof h)throw Error("patchStrategy must be one of the included patching strategies or a custom patching function");this.portName=o,this.readyResolved=!1,this.readyPromise=new Promise((function(e){return t.readyResolve=e})),this.browserAPI=H(),this.extensionId=u,this.port=this.browserAPI.runtime.connect(this.extensionId,{name:o}),this.safetyHandler=this.safetyHandler.bind(this),this.browserAPI.runtime.onMessage&&(this.safetyMessage=this.browserAPI.runtime.onMessage.addListener(this.safetyHandler)),this.serializedPortListener=I(d)((function(){var e;return(e=t.port.onMessage).addListener.apply(e,arguments)})),this.serializedMessageSender=L(l)((function(){var e;return(e=t.browserAPI.runtime).sendMessage.apply(e,arguments)}),1),this.listeners=[],this.state=a,this.patchStrategy=h,this.serializedPortListener((function(e){switch(e.type){case k:t.replaceState(e.payload),t.readyResolved||(t.readyResolved=!0,t.readyResolve());break;case z:t.patchState(e.payload)}})),this.dispatch=this.dispatch.bind(this)}var r,n,o;return r=e,(n=[{key:"ready",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:null;return null!==e?this.readyPromise.then(e):this.readyPromise}},{key:"subscribe",value:function(e){var t=this;return this.listeners.push(e),function(){t.listeners=t.listeners.filter((function(t){return t!==e}))}}},{key:"patchState",value:function(e){this.state=this.patchStrategy(this.state,e),this.listeners.forEach((function(e){return e()}))}},{key:"replaceState",value:function(e){this.state=e,this.listeners.forEach((function(e){return e()}))}},{key:"getState",value:function(){return this.state}},{key:"replaceReducer",value:function(){}},{key:"dispatch",value:function(e){var t=this;return new Promise((function(r,n){t.serializedMessageSender(t.extensionId,{type:"chromex.dispatch",portName:t.portName,payload:e},null,(function(e){var t=e.error,o=e.value;if(t){var i=Error("".concat("\nLooks like there is an error in the background page. You might want to inspect your background page for more details.\n").concat(t));n(O(i,t))}else r(o&&o.payload)}))}))}},{key:"safetyHandler",value:function(e){"storeReady"===e.action&&e.portName===this.portName&&(this.browserAPI.runtime.onMessage.removeListener(this.safetyHandler),this.readyResolved||(this.readyResolved=!0,this.readyResolve()))}}])&&t(r.prototype,n),o&&t(r,o),e}(),e.alias=function(e){return function(){return function(t){return function(r){var n=e[r.type];return t(n?n(r):r)}}}},e.applyMiddleware=function(e){for(var t=arguments.length,r=Array(t>1?t-1:0),n=1;t>n;n++)r[n-1]=arguments[n];var i=function(){throw Error("Dispatching while constructing your middleware is not allowed. Other middleware would not be applied to this dispatch.")},a={getState:e.getState.bind(e),dispatch:function(){return i.apply(void 0,arguments)}};return r=(r||[]).map((function(e){return e(a)})),i=D.apply(void 0,o(r))(e.dispatch),e.dispatch=i,e},e.wrapStore=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:F,r=t.portName,n=void 0===r?F.portName:r,o=t.dispatchResponder,i=void 0===o?F.dispatchResponder:o,a=t.serializer,s=void 0===a?F.serializer:a,u=t.deserializer,c=void 0===u?F.deserializer:u,l=t.diffStrategy,f=void 0===l?F.diffStrategy:l;if(!n)throw Error("portName is required in options");if("function"!=typeof s)throw Error("serializer must be a function");if("function"!=typeof c)throw Error("deserializer must be a function");if("function"!=typeof f)throw Error("diffStrategy must be one of the included diffing strategies or a custom diff function");var d=H(),p=function(t,r,o){if("chromex.dispatch"===t.type&&t.portName===n){var a=Object.assign({},t.payload,{_sender:r}),s=null;try{s=e.dispatch(a)}catch(e){s=Promise.reject(e.message),console.error(e)}return i(s,o),!0}},h=function(t){if(t.name===n){var r=L(s)((function(){return t.postMessage.apply(t,arguments)})),o=e.getState(),i=e.subscribe((function(){var t=e.getState(),n=f(o,t);n.length&&(o=t,r({type:z,payload:n}))}));t.onDisconnect.addListener(i),r({type:k,payload:o})}},y=I(c),v=function(e){return"chromex.dispatch"===e.type&&e.portName===n};y((function(){var e;return(e=d.runtime.onMessage).addListener.apply(e,arguments)}))(p,v),d.runtime.onMessageExternal&&false?y((function(){var e;return(e=d.runtime.onMessageExternal).addListener.apply(e,arguments)}))(p,v):console.warn("runtime.onMessageExternal is not supported"),d.runtime.onConnect.addListener(h),d.runtime.onConnectExternal&&false?d.runtime.onConnectExternal.addListener(h):console.warn("runtime.onConnectExternal is not supported"),d.tabs.query({},(function(e){var t=!0,r=!1,o=void 0;try{for(var i,a=e[Symbol.iterator]();!(t=(i=a.next()).done);t=!0){d.tabs.sendMessage(i.value.id,{action:"storeReady",portName:n},(function(){chrome}))}}catch(e){r=!0,o=e}finally{try{t||null==a.return||a.return()}finally{if(r)throw o}}}))},Object.defineProperty(e,"__esModule",{value:!0})})); +diff --git a/node_modules/webext-redux/index.d.ts b/node_modules/webext-redux/index.d.ts +index 3dd5e29..66eeca6 100644 +--- a/node_modules/webext-redux/index.d.ts ++++ b/node_modules/webext-redux/index.d.ts +@@ -1,6 +1,6 @@ + import * as redux from 'redux'; + +-export type DiffStrategy = (oldObj: any, newObj: any) => any; ++export type DiffStrategy = (oldObj: any, newObj: any, forceUpdate: (newState: any, diff:any[]) => void) => any; + export type PatchStrategy = (oldObj: any, patch: any) => any; + + export class Store { diff --git a/node_modules/webext-redux/lib/store/Store.js b/node_modules/webext-redux/lib/store/Store.js index 8f49d00..869d62e 100644 --- a/node_modules/webext-redux/lib/store/Store.js @@ -53,3 +65,28 @@ index 8f49d00..869d62e 100644 if (error) { var bgErr = new Error("".concat(backgroundErrPrefix).concat(error)); reject((0, _lodash.default)(bgErr, error)); +diff --git a/node_modules/webext-redux/lib/wrap-store/wrapStore.js b/node_modules/webext-redux/lib/wrap-store/wrapStore.js +index 5dc1dea..23e8c77 100644 +--- a/node_modules/webext-redux/lib/wrap-store/wrapStore.js ++++ b/node_modules/webext-redux/lib/wrap-store/wrapStore.js +@@ -114,11 +114,19 @@ var _default = function _default(store) { + var serializedMessagePoster = (0, _serialization.withSerializer)(serializer)(function () { + return port.postMessage.apply(port, arguments); + }); ++ + var prevState = store.getState(); + ++ var forceUpdate = (newState, diff) =>{ ++ prevState = newState ++ serializedMessagePoster({ ++ type: _constants.PATCH_STATE_TYPE, ++ payload: diff ++ })} ++ + var patchState = function patchState() { + var state = store.getState(); +- var diff = diffStrategy(prevState, state); ++ var diff = diffStrategy(prevState, state, forceUpdate); + + if (diff.length) { + prevState = state; From 022b99b46613b41b5d0e8de9203e11f9b9e19742 Mon Sep 17 00:00:00 2001 From: hyphenized <28708889+hyphenized@users.noreply.github.com> Date: Tue, 25 Jul 2023 22:39:54 -0500 Subject: [PATCH 2/9] Lock dispatch responder until aggregated updates are committed Tracks pending store updates to delay async thunk responses until state changes are commited to the UI proxied store. This fixes cases where thunks resolve too early i.e. before the aggregated updates have been sent to the UI. This relies on the subscribed callback from webext-redux for diffing to have executed before the dispatched thunk has finished executing in the background script. --- background/main.ts | 38 +++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/background/main.ts b/background/main.ts index b9c9a4b0af..a9bdf041c4 100644 --- a/background/main.ts +++ b/background/main.ts @@ -508,6 +508,15 @@ export default class Main extends BaseService { // Start up the redux store and set it up for proxying. this.store = initializeStore(savedReduxState, this) + /** + * Tracks pending updates to the redux store. This is used to delay responding + * to dispatched thunks until the store has been updated in the UI. This is + * necessary to prevent race conditions where the UI expects the store to be + * updated before the thunk has finished dispatching. + */ + let storeUpdateLock: Promise = Promise.resolve() + const pendingUpdates: (() => void)[] = [] + const queueUpdate = debounce( (lastState, newState, updateFn) => { if (lastState === newState) { @@ -519,6 +528,13 @@ export default class Main extends BaseService { if (diff !== undefined) { updateFn(newState, [diff]) } + + pendingUpdates.forEach((resolve) => resolve()) + + // Clear all broadcasted updates + while (pendingUpdates.length) { + pendingUpdates.pop() + } }, 30, { maxWait: 30, trailing: true } @@ -528,25 +544,41 @@ export default class Main extends BaseService { serializer: encodeJSON, deserializer: decodeJSON, diffStrategy: (oldObj, newObj, forceUpdate) => { - queueUpdate(oldObj, newObj, forceUpdate) + storeUpdateLock = new Promise((resolve) => { + pendingUpdates.push(resolve) + queueUpdate(oldObj, newObj, forceUpdate) + }) // Return no diffs as we're manually handling these inside `queueUpdate` return [] }, dispatchResponder: async ( - dispatchResult: Promise, + dispatchResult: Promise | unknown, send: (param: { error: string | null; value: unknown | null }) => void ) => { + const isThunk = dispatchResult instanceof Promise try { + // if dispatch is a thunk, wait for the result + const result = await dispatchResult + + // by this time, all pending updates should've been tracked. + // since we're dealing with a thunk, we need to wait for + // the store to be updated + if (isThunk) await storeUpdateLock + send({ error: null, - value: encodeJSON(await dispatchResult), + value: encodeJSON(result), }) } catch (error) { logger.error( "Error awaiting and dispatching redux store result: ", error ) + + // Store could still have been updated if there was an error + if (isThunk) await storeUpdateLock + send({ error: encodeJSON(error), value: null, From 2633420f73c024c5b15bd81ee1a965f4e59f2205 Mon Sep 17 00:00:00 2001 From: Jorge Luis <28708889+hyphenized@users.noreply.github.com> Date: Thu, 20 Jul 2023 17:20:12 -0500 Subject: [PATCH 3/9] Add tests for diffing bigints --- background/tests/differ.test.ts | 65 +++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 background/tests/differ.test.ts diff --git a/background/tests/differ.test.ts b/background/tests/differ.test.ts new file mode 100644 index 0000000000..ecc5066570 --- /dev/null +++ b/background/tests/differ.test.ts @@ -0,0 +1,65 @@ +import assert from "assert" +import { diff, patch } from "../differ" + +describe("Diffing & Patching", () => { + describe("BigInts", () => { + const cases = [ + 0, + "test", + 2n, + { b: 1 }, + [5], + null, + undefined, + true, + false, + "", + NaN, + ] + + test("Creates correct patches when diffing against bigints", () => { + cases.forEach((initial) => { + const expected = 9999n + const delta = diff(initial, expected) + + assert(delta) + expect(patch(initial, delta)).toEqual(expected) + }) + }) + + test("Creates correct patches when diffing from bigints", () => { + cases.forEach((expected) => { + const initial = 9999n + const delta = diff(initial, expected) + + assert(delta) + expect(patch(initial, delta)).toEqual(expected) + }) + }) + + test("Creates correct patches regardless of depth of change", () => { + cases.forEach((expected) => { + const targetFrom = { a: { b: [0, { c: 9999n }] } } + const target = { a: { b: [0, { c: expected }] } } + + const delta = diff(targetFrom, target) + + assert(delta) + + expect(patch(targetFrom, delta)).toEqual(target) + }) + + // against bigints + cases.forEach((initial) => { + const targetFrom = { a: { b: [0, { c: initial }] } } + const target = { a: { b: [0, { c: 9999n }] } } + + const delta = diff(targetFrom, target) + + assert(delta) + + expect(patch(targetFrom, delta)).toEqual(target) + }) + }) + }) +}) From 23563c4f3dfca253aef33939ef14a9727851377b Mon Sep 17 00:00:00 2001 From: Jorge Luis <28708889+hyphenized@users.noreply.github.com> Date: Sat, 29 Jul 2023 20:57:17 -0500 Subject: [PATCH 4/9] Fix added delay when there are no state changes This fixes an issue that caused additional delay on thunks that only return a value, but, still rely on other async operations. --- background/main.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/background/main.ts b/background/main.ts index a9bdf041c4..ad2b6f25ce 100644 --- a/background/main.ts +++ b/background/main.ts @@ -519,7 +519,17 @@ export default class Main extends BaseService { const queueUpdate = debounce( (lastState, newState, updateFn) => { + const clearPendingUpdates = () => { + pendingUpdates.forEach((resolve) => resolve()) + + // Clear all currently tracked updates + while (pendingUpdates.length) { + pendingUpdates.pop() + } + } + if (lastState === newState) { + clearPendingUpdates() return } @@ -529,12 +539,7 @@ export default class Main extends BaseService { updateFn(newState, [diff]) } - pendingUpdates.forEach((resolve) => resolve()) - - // Clear all broadcasted updates - while (pendingUpdates.length) { - pendingUpdates.pop() - } + clearPendingUpdates() }, 30, { maxWait: 30, trailing: true } From 5b169d74975b4f03b425f087be66e1270d7cba9c Mon Sep 17 00:00:00 2001 From: Jorge Luis <28708889+hyphenized@users.noreply.github.com> Date: Sat, 29 Jul 2023 21:04:54 -0500 Subject: [PATCH 5/9] Fix e2e onboarding test assertion --- e2e-tests/utils/onboarding.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/e2e-tests/utils/onboarding.ts b/e2e-tests/utils/onboarding.ts index f9904d35f4..22bc9dc6c0 100644 --- a/e2e-tests/utils/onboarding.ts +++ b/e2e-tests/utils/onboarding.ts @@ -188,6 +188,7 @@ export default class OnboardingHelper { await page .getByTestId("remaining_seed_words") .getByRole("button", { name: word }) + .first() // can be a duplicate word .click() } From 8537e3e2eb3801ba7a7e6a0f155568e9e3548c5b Mon Sep 17 00:00:00 2001 From: Antonio Salazar Cardozo Date: Fri, 4 Aug 2023 17:12:48 -0400 Subject: [PATCH 6/9] Drop ENABLE_UPDATED_DAPP_CONNECTIONS feature flag It is enabled and shall remain so! --- .env.defaults | 1 - background/features.ts | 2 - ui/_locales/en/messages.json | 15 -- ui/components/TopMenu/TopMenu.tsx | 132 +----------------- .../TopMenu/TopMenuConnectedDAppInfo.tsx | 9 +- ui/components/Wallet/WalletDefaultToggle.tsx | 43 ++++++ .../Wallet/WalletToggleDefaultBanner.tsx | 116 --------------- ui/pages/DAppConnect/DAppConnectPage.tsx | 27 +--- ui/pages/DAppConnect/SwitchWallet.tsx | 54 ------- ui/pages/DAppConnect/SwitchWalletPage.tsx | 114 --------------- ui/pages/DAppConnectRequest.tsx | 24 +--- ui/pages/Onboarding/Tabbed/SetPassword.tsx | 2 +- ui/pages/Settings.tsx | 22 --- ui/pages/Wallet.tsx | 20 +-- window-provider/index.ts | 68 +-------- 15 files changed, 65 insertions(+), 584 deletions(-) create mode 100644 ui/components/Wallet/WalletDefaultToggle.tsx delete mode 100644 ui/components/Wallet/WalletToggleDefaultBanner.tsx delete mode 100644 ui/pages/DAppConnect/SwitchWallet.tsx delete mode 100644 ui/pages/DAppConnect/SwitchWalletPage.tsx diff --git a/.env.defaults b/.env.defaults index 5dc8af9712..d2914b14b4 100644 --- a/.env.defaults +++ b/.env.defaults @@ -31,4 +31,3 @@ SUPPORT_SWAP_QUOTE_REFRESH=false SUPPORT_ACHIEVEMENTS_BANNER=false SUPPORT_NFT_SEND=false USE_MAINNET_FORK=false -ENABLE_UPDATED_DAPP_CONNECTIONS=true diff --git a/background/features.ts b/background/features.ts index 62ee5236df..7bf40f3875 100644 --- a/background/features.ts +++ b/background/features.ts @@ -23,8 +23,6 @@ export const RuntimeFlag = { SUPPORT_SWAP_QUOTE_REFRESH: process.env.SUPPORT_SWAP_QUOTE_REFRESH === "true", SUPPORT_CUSTOM_NETWORKS: process.env.SUPPORT_CUSTOM_NETWORKS === "true", SUPPORT_CUSTOM_RPCS: process.env.SUPPORT_CUSTOM_RPCS === "true", - ENABLE_UPDATED_DAPP_CONNECTIONS: - process.env.ENABLE_UPDATED_DAPP_CONNECTIONS === "true", } as const type BuildTimeFlagType = keyof typeof BuildTimeFlag diff --git a/ui/_locales/en/messages.json b/ui/_locales/en/messages.json index 014087e002..fd32234b89 100644 --- a/ui/_locales/en/messages.json +++ b/ui/_locales/en/messages.json @@ -797,15 +797,6 @@ }, "reviewSwap": "Review swap" }, - "switchWallet": { - "title": "Want to use another wallet instead?", - "tooltip": "You are seeing this because Taho is set as default wallet, you can change this option in the main menu.", - "confirmSwitchWallet": "Yes, switch wallet", - "notDefaultWalletMessage": "Taho not Default", - "disableWalletExplainer": "We disabled Taho as the default wallet for you. You can always re-enable it from Menu ☰ at any time.", - "useTahoAsDefaultPrompt": "Use Taho as default wallet", - "closeButton": "Close window" - }, "toggle": { "collapse": "Collapse", "viewAll": "View all" @@ -921,12 +912,6 @@ } }, "dAppConnect": { - "switchWallet": { - "title": "Taho not Default", - "descFirstPart": "We disabled Taho as default wallet for you. You can always enable it back from Settings", - "descSecondPart": "at any time.", - "toggleTitle": "Use Taho as default wallet" - }, "defaultConnectionPopover": { "title": "Connecting with Taho", "activeWallet": "Your active wallet is Taho.", diff --git a/ui/components/TopMenu/TopMenu.tsx b/ui/components/TopMenu/TopMenu.tsx index 965d19c4a2..fdb298b911 100644 --- a/ui/components/TopMenu/TopMenu.tsx +++ b/ui/components/TopMenu/TopMenu.tsx @@ -1,13 +1,5 @@ -import React, { ReactElement, useState, useEffect, useCallback } from "react" -import { browser } from "@tallyho/tally-background" -import { PermissionRequest } from "@tallyho/provider-bridge-shared" -import { selectAllowedPages } from "@tallyho/tally-background/redux-slices/selectors" -import { - FeatureFlags, - isDisabled, - isEnabled, -} from "@tallyho/tally-background/features" -import { denyOrRevokePermission } from "@tallyho/tally-background/redux-slices/dapp" +import React, { ReactElement, useState } from "react" +import { FeatureFlags, isDisabled } from "@tallyho/tally-background/features" import { useTranslation } from "react-i18next" import { setSelectedNetwork } from "@tallyho/tally-background/redux-slices/ui" import TopMenuProtocolSwitcher from "./TopMenuProtocolSwitcher" @@ -16,10 +8,9 @@ import TopMenuProfileButton from "./TopMenuProfileButton" import BonusProgramModal from "../BonusProgram/BonusProgramModal" import AccountsNotificationPanel from "../AccountsNotificationPanel/AccountsNotificationPanel" import SharedSlideUpMenu from "../Shared/SharedSlideUpMenu" -import TopMenuConnectedDAppInfo from "./TopMenuConnectedDAppInfo" import TopMenuProtocolList from "./TopMenuProtocolList" -import { useBackgroundDispatch, useBackgroundSelector } from "../../hooks" +import { useBackgroundDispatch } from "../../hooks" import DAppConnection from "../DAppConnection/DAppConnection" export default function TopMenu(): ReactElement { @@ -29,80 +20,10 @@ export default function TopMenu(): ReactElement { const [isNotificationsOpen, setIsNotificationsOpen] = useState(false) const [isBonusProgramOpen, setIsBonusProgramOpen] = useState(false) - const [isActiveDAppConnectionInfoOpen, setIsActiveDAppConnectionInfoOpen] = - useState(false) - const dispatch = useBackgroundDispatch() - const [currentPermission, setCurrentPermission] = useState( - {} as PermissionRequest - ) - const [isConnectedToDApp, setIsConnectedToDApp] = useState(false) - const allowedPages = useBackgroundSelector((state) => - selectAllowedPages(state) - ) - - const initPermissionAndOrigin = useCallback(async () => { - const { url } = await browser.tabs - .query({ - active: true, - lastFocusedWindow: true, - }) - .then((tabs) => - tabs[0] ? tabs[0] : { url: "", favIconUrl: "", title: "" } - ) - if (!url) return - - const { origin } = new URL(url) - - const allowPermission = allowedPages.find( - (permission) => permission.origin === origin - ) - - if (allowPermission) { - setCurrentPermission(allowPermission) - setIsConnectedToDApp(true) - } else { - setIsConnectedToDApp(false) - } - }, [allowedPages, setCurrentPermission]) - - useEffect(() => { - initPermissionAndOrigin() - }, [initPermissionAndOrigin]) - - const deny = useCallback(async () => { - if (typeof currentPermission !== "undefined") { - // Deletes all permissions corresponding to the currently selected - // account and origin - await Promise.all( - allowedPages.map(async (permission) => { - if (permission.origin === currentPermission.origin) { - return dispatch( - denyOrRevokePermission({ ...permission, state: "deny" }) - ) - } - return undefined - }) - ) - } - }, [dispatch, currentPermission, allowedPages]) - return ( <> - {isDisabled(FeatureFlags.ENABLE_UPDATED_DAPP_CONNECTIONS) && - isActiveDAppConnectionInfoOpen ? ( - { - setIsActiveDAppConnectionInfoOpen(false) - }} - disconnect={deny} - isConnected={isConnectedToDApp} - /> - ) : null} { @@ -135,31 +56,10 @@ export default function TopMenu(): ReactElement { onCurrentAddressChange={() => setIsNotificationsOpen(false)} /> - {isEnabled(FeatureFlags.ENABLE_UPDATED_DAPP_CONNECTIONS) && ( - - )} +