From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- devtools/client/shared/redux/create-store.js | 90 +++++++++++++++++++ .../client/shared/redux/middleware/debounce.js | 100 +++++++++++++++++++++ devtools/client/shared/redux/middleware/ignore.js | 38 ++++++++ devtools/client/shared/redux/middleware/log.js | 31 +++++++ devtools/client/shared/redux/middleware/moz.build | 18 ++++ .../shared/redux/middleware/performance-marker.js | 68 ++++++++++++++ devtools/client/shared/redux/middleware/promise.js | 69 ++++++++++++++ devtools/client/shared/redux/middleware/task.js | 38 ++++++++ devtools/client/shared/redux/middleware/thunk.js | 23 +++++ .../client/shared/redux/middleware/wait-service.js | 64 +++++++++++++ .../shared/redux/middleware/xpcshell/.eslintrc.js | 10 +++ .../shared/redux/middleware/xpcshell/head.js | 26 ++++++ .../middleware/xpcshell/test_middleware-task-01.js | 66 ++++++++++++++ .../middleware/xpcshell/test_middleware-task-02.js | 86 ++++++++++++++++++ .../middleware/xpcshell/test_middleware-task-03.js | 50 +++++++++++ .../shared/redux/middleware/xpcshell/xpcshell.toml | 11 +++ devtools/client/shared/redux/moz.build | 15 ++++ devtools/client/shared/redux/subscriber.js | 16 ++++ .../shared/redux/visibility-handler-connect.js | 35 ++++++++ 19 files changed, 854 insertions(+) create mode 100644 devtools/client/shared/redux/create-store.js create mode 100644 devtools/client/shared/redux/middleware/debounce.js create mode 100644 devtools/client/shared/redux/middleware/ignore.js create mode 100644 devtools/client/shared/redux/middleware/log.js create mode 100644 devtools/client/shared/redux/middleware/moz.build create mode 100644 devtools/client/shared/redux/middleware/performance-marker.js create mode 100644 devtools/client/shared/redux/middleware/promise.js create mode 100644 devtools/client/shared/redux/middleware/task.js create mode 100644 devtools/client/shared/redux/middleware/thunk.js create mode 100644 devtools/client/shared/redux/middleware/wait-service.js create mode 100644 devtools/client/shared/redux/middleware/xpcshell/.eslintrc.js create mode 100644 devtools/client/shared/redux/middleware/xpcshell/head.js create mode 100644 devtools/client/shared/redux/middleware/xpcshell/test_middleware-task-01.js create mode 100644 devtools/client/shared/redux/middleware/xpcshell/test_middleware-task-02.js create mode 100644 devtools/client/shared/redux/middleware/xpcshell/test_middleware-task-03.js create mode 100644 devtools/client/shared/redux/middleware/xpcshell/xpcshell.toml create mode 100644 devtools/client/shared/redux/moz.build create mode 100644 devtools/client/shared/redux/subscriber.js create mode 100644 devtools/client/shared/redux/visibility-handler-connect.js (limited to 'devtools/client/shared/redux') diff --git a/devtools/client/shared/redux/create-store.js b/devtools/client/shared/redux/create-store.js new file mode 100644 index 0000000000..fb348d14dc --- /dev/null +++ b/devtools/client/shared/redux/create-store.js @@ -0,0 +1,90 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const { + combineReducers, + createStore, + applyMiddleware, +} = require("resource://devtools/client/shared/vendor/redux.js"); +const { + thunk, +} = require("resource://devtools/client/shared/redux/middleware/thunk.js"); +const { + waitUntilService, +} = require("resource://devtools/client/shared/redux/middleware/wait-service.js"); +const { + task, +} = require("resource://devtools/client/shared/redux/middleware/task.js"); +const { + promise, +} = require("resource://devtools/client/shared/redux/middleware/promise.js"); +const flags = require("resource://devtools/shared/flags.js"); + +loader.lazyRequireGetter( + this, + "log", + "resource://devtools/client/shared/redux/middleware/log.js", + true +); + +/** + * This creates a dispatcher with all the standard middleware in place + * that all code requires. It can also be optionally configured in + * various ways, such as logging and recording. + * + * @param {object} opts: + * - enableTaskMiddleware: if true, include the task middleware + * - log: log all dispatched actions to console + * - middleware: array of middleware to be included in the redux store + * - thunkOptions: object that will be spread within a {dispatch, getState} object, + * that will be passed in each thunk action. + */ +const createStoreWithMiddleware = (opts = {}) => { + const middleware = []; + if (opts.enableTaskMiddleware) { + middleware.push(task); + } + middleware.push( + thunk(opts.thunkOptions), + promise, + + // Order is important: services must go last as they always + // operate on "already transformed" actions. Actions going through + // them shouldn't have any special fields like promises, they + // should just be normal JSON objects. + waitUntilService + ); + + if (opts.middleware) { + opts.middleware.forEach(fn => middleware.push(fn)); + } + + if (opts.log) { + middleware.push(log); + } + + return applyMiddleware(...middleware)(createStore); +}; + +module.exports = ( + reducers, + { + shouldLog = false, + initialState = undefined, + thunkOptions, + enableTaskMiddleware = false, + } = {} +) => { + const reducer = + typeof reducers === "function" ? reducers : combineReducers(reducers); + + const store = createStoreWithMiddleware({ + enableTaskMiddleware, + log: flags.testing && shouldLog, + thunkOptions, + })(reducer, initialState); + + return store; +}; diff --git a/devtools/client/shared/redux/middleware/debounce.js b/devtools/client/shared/redux/middleware/debounce.js new file mode 100644 index 0000000000..fc5625a0fe --- /dev/null +++ b/devtools/client/shared/redux/middleware/debounce.js @@ -0,0 +1,100 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +/** + * Redux middleware for debouncing actions. + * + * Schedules actions with { meta: { debounce: true } } to be delayed + * by wait milliseconds. If another action is fired during this + * time-frame both actions are inserted into a queue and delayed. + * Maximum delay is defined by maxWait argument. + * + * Handling more actions at once results in better performance since + * components need to be re-rendered less often. + * + * @param string wait Wait for specified amount of milliseconds + * before executing an action. The time is used + * to collect more actions and handle them all + * at once. + * @param string maxWait Max waiting time. It's used in case of + * a long stream of actions. + */ +function debounceActions(wait, maxWait) { + let queuedActions = []; + + return store => next => { + const debounced = debounce( + () => { + next(batchActions(queuedActions)); + queuedActions = []; + }, + wait, + maxWait + ); + + return action => { + if (!action.meta || !action.meta.debounce) { + return next(action); + } + + if (!wait || !maxWait) { + return next(action); + } + + if (action.type == BATCH_ACTIONS) { + queuedActions.push(...action.actions); + } else { + queuedActions.push(action); + } + + return debounced(); + }; + }; +} + +function debounce(cb, wait, maxWait) { + let timeout, maxTimeout; + const doFunction = () => { + clearTimeout(timeout); + clearTimeout(maxTimeout); + timeout = maxTimeout = null; + cb(); + }; + + return () => { + return new Promise(resolve => { + const onTimeout = () => { + doFunction(); + resolve(); + }; + + clearTimeout(timeout); + + timeout = setTimeout(onTimeout, wait); + if (!maxTimeout) { + maxTimeout = setTimeout(onTimeout, maxWait); + } + }); + }; +} + +const BATCH_ACTIONS = Symbol("BATCH_ACTIONS"); + +/** + * Action creator for action-batching. + */ +function batchActions(batchedActions, debounceFlag = true) { + return { + type: BATCH_ACTIONS, + meta: { debounce: debounceFlag }, + actions: batchedActions, + }; +} + +module.exports = { + BATCH_ACTIONS, + batchActions, + debounceActions, +}; diff --git a/devtools/client/shared/redux/middleware/ignore.js b/devtools/client/shared/redux/middleware/ignore.js new file mode 100644 index 0000000000..d2e8cd9e24 --- /dev/null +++ b/devtools/client/shared/redux/middleware/ignore.js @@ -0,0 +1,38 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const IGNORING = Symbol("IGNORING"); +const START_IGNORE_ACTION = "START_IGNORE_ACTION"; + +/** + * A middleware that prevents any action of being called once it is activated. + * This is useful to apply while destroying a given panel, as it will ignore all calls + * to actions, where we usually make our client -> server communications. + * This middleware should be declared before any other middleware to to effectively + * ignore every actions. + */ +function ignore({ getState }) { + return next => action => { + if (action.type === START_IGNORE_ACTION) { + getState()[IGNORING] = true; + return null; + } + + if (getState()[IGNORING]) { + // We only print the action type, and not the whole action object, as it can holds + // very complex data that would clutter stdout logs and make them impossible + // to parse for treeherder. + console.warn("IGNORED REDUX ACTION:", action.type); + return null; + } + + return next(action); + }; +} + +module.exports = { + ignore, + START_IGNORE_ACTION: { type: START_IGNORE_ACTION }, +}; diff --git a/devtools/client/shared/redux/middleware/log.js b/devtools/client/shared/redux/middleware/log.js new file mode 100644 index 0000000000..4ea09491ca --- /dev/null +++ b/devtools/client/shared/redux/middleware/log.js @@ -0,0 +1,31 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +/** + * A middleware that logs all actions coming through the system + * to the console. + */ +function log({ dispatch, getState }) { + return next => action => { + try { + // Only print the action type, rather than printing the whole object + console.log("[DISPATCH] action type:", action.type); + /* + * USE WITH CAUTION!! This will output everything from an action object, + * and these can be quite large. Printing out large objects will slow + * down tests and cause test failures + * + * console.log("[DISPATCH]", JSON.stringify(action, null, 2)); + */ + } catch (e) { + // this occurs if JSON.stringify throws. + console.warn(e); + console.log("[DISPATCH]", action); + } + next(action); + }; +} + +exports.log = log; diff --git a/devtools/client/shared/redux/middleware/moz.build b/devtools/client/shared/redux/middleware/moz.build new file mode 100644 index 0000000000..82de1417e6 --- /dev/null +++ b/devtools/client/shared/redux/middleware/moz.build @@ -0,0 +1,18 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DevToolsModules( + "debounce.js", + "ignore.js", + "log.js", + "performance-marker.js", + "promise.js", + "task.js", + "thunk.js", + "wait-service.js", +) + +XPCSHELL_TESTS_MANIFESTS += ["xpcshell/xpcshell.toml"] diff --git a/devtools/client/shared/redux/middleware/performance-marker.js b/devtools/client/shared/redux/middleware/performance-marker.js new file mode 100644 index 0000000000..c829dffe3e --- /dev/null +++ b/devtools/client/shared/redux/middleware/performance-marker.js @@ -0,0 +1,68 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +/** + * This function returns a middleware, which is responsible for adding markers that will + * be visible in performance profiles, and may help investigate performance issues. + * + * Example usage, adding a marker when console messages are added, and when they are cleared: + * + * return createPerformanceMarkerMiddleware({ + * "MESSAGES_ADD": { + * label: "WebconsoleAddMessages", + * sessionId: 12345, + * getMarkerDescription: function({ action, state }) { + * const { messages } = action; + * const totalMessageCount = state.messages.mutableMessagesById.size; + * return `${messages.length} messages handled, store now has ${totalMessageCount} messages`; + * }, + * }, + * "MESSAGES_CLEARED": { + * label: "WebconsoleClearMessages", + * sessionId: 12345 + * }, + * }); + * + * @param {Object} cases: An object, keyed by action type, that will determine if a + * given action will add a marker. + * @param {String} cases.{actionType} - The type of the action that will trigger the + * marker creation. + * @param {String} cases.{actionType}.label - The marker label + * @param {Integer} cases.{actionType}.sessionId - The telemetry sessionId. This is used + * to be able to distinguish markers coming from different toolboxes. + * @param {Function} [cases.{actionType}.getMarkerDescription] - An optional function that + * will be called when adding the marker to populate its description. The function + * is called with an object holding the action and the state + */ +function createPerformanceMarkerMiddleware(cases) { + return function (store) { + return next => action => { + const condition = cases[action.type]; + const shouldAddProfileMarker = !!condition; + + // Start the marker timer before calling next(action). + const startTime = shouldAddProfileMarker ? Cu.now() : null; + const newState = next(action); + + if (shouldAddProfileMarker) { + ChromeUtils.addProfilerMarker( + `${condition.label} ${condition.sessionId}`, + startTime, + condition.getMarkerDescription + ? condition.getMarkerDescription({ + action, + state: store.getState(), + }) + : "" + ); + } + return newState; + }; + }; +} + +module.exports = { + createPerformanceMarkerMiddleware, +}; diff --git a/devtools/client/shared/redux/middleware/promise.js b/devtools/client/shared/redux/middleware/promise.js new file mode 100644 index 0000000000..7f88651a61 --- /dev/null +++ b/devtools/client/shared/redux/middleware/promise.js @@ -0,0 +1,69 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +loader.lazyRequireGetter( + this, + "generateUUID", + "resource://devtools/shared/generate-uuid.js", + true +); +loader.lazyRequireGetter( + this, + ["entries", "executeSoon", "toObject"], + "resource://devtools/shared/DevToolsUtils.js", + true +); + +const PROMISE = (exports.PROMISE = "@@dispatch/promise"); + +function promiseMiddleware({ dispatch, getState }) { + return next => action => { + if (!(PROMISE in action)) { + return next(action); + } + // Return the promise so action creators can still compose if they + // want to. + return new Promise((resolve, reject) => { + const promiseInst = action[PROMISE]; + const seqId = generateUUID().toString(); + + // Create a new action that doesn't have the promise field and has + // the `seqId` field that represents the sequence id + action = Object.assign( + toObject(entries(action).filter(pair => pair[0] !== PROMISE)), + { seqId } + ); + + dispatch(Object.assign({}, action, { status: "start" })); + + promiseInst.then( + value => { + executeSoon(() => { + dispatch( + Object.assign({}, action, { + status: "done", + value, + }) + ); + resolve(value); + }); + }, + error => { + executeSoon(() => { + dispatch( + Object.assign({}, action, { + status: "error", + error: error.message || error, + }) + ); + reject(error); + }); + } + ); + }); + }; +} + +exports.promise = promiseMiddleware; diff --git a/devtools/client/shared/redux/middleware/task.js b/devtools/client/shared/redux/middleware/task.js new file mode 100644 index 0000000000..1ffb033589 --- /dev/null +++ b/devtools/client/shared/redux/middleware/task.js @@ -0,0 +1,38 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +loader.lazyRequireGetter( + this, + ["executeSoon", "isAsyncFunction", "reportException"], + "resource://devtools/shared/DevToolsUtils.js", + true +); + +const ERROR_TYPE = (exports.ERROR_TYPE = "@@redux/middleware/task#error"); + +/** + * A middleware that allows async thunks (async functions) to be dispatched. + * The middleware is called "task" for historical reasons. TODO: rename? + */ + +function task({ dispatch, getState }) { + return next => action => { + if (isAsyncFunction(action)) { + return action({ dispatch, getState }).catch( + handleError.bind(null, dispatch) + ); + } + return next(action); + }; +} + +function handleError(dispatch, error) { + executeSoon(() => { + reportException(ERROR_TYPE, error); + dispatch({ type: ERROR_TYPE, error }); + }); +} + +exports.task = task; diff --git a/devtools/client/shared/redux/middleware/thunk.js b/devtools/client/shared/redux/middleware/thunk.js new file mode 100644 index 0000000000..adcca77ede --- /dev/null +++ b/devtools/client/shared/redux/middleware/thunk.js @@ -0,0 +1,23 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +/** + * A middleware that allows thunks (functions) to be dispatched + * If it's a thunk, it is called with an argument that will be an object + * containing `dispatch` and `getState` properties, plus any additional properties + * defined in the `options` parameters. + * This allows the action to create multiple actions (most likely asynchronously). + */ +function thunk(options = {}) { + return function ({ dispatch, getState }) { + return next => action => { + return typeof action === "function" + ? action({ dispatch, getState, ...options }) + : next(action); + }; + }; +} + +exports.thunk = thunk; diff --git a/devtools/client/shared/redux/middleware/wait-service.js b/devtools/client/shared/redux/middleware/wait-service.js new file mode 100644 index 0000000000..9416fd22dd --- /dev/null +++ b/devtools/client/shared/redux/middleware/wait-service.js @@ -0,0 +1,64 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +/** + * A middleware which acts like a service, because it is stateful + * and "long-running" in the background. It provides the ability + * for actions to install a function to be run once when a specific + * condition is met by an action coming through the system. Think of + * it as a thunk that blocks until the condition is met. Example: + * + * ```js + * const services = { WAIT_UNTIL: require('wait-service').NAME }; + * + * { type: services.WAIT_UNTIL, + * predicate: action => action.type === constants.ADD_ITEM, + * run: (dispatch, getState, action) => { + * // Do anything here. You only need to accept the arguments + * // if you need them. `action` is the action that satisfied + * // the predicate. + * } + * } + * ``` + */ +const NAME = (exports.NAME = "@@service/waitUntil"); + +function waitUntilService({ dispatch, getState }) { + let pending = []; + + function checkPending(action) { + const readyRequests = []; + const stillPending = []; + + // Find the pending requests whose predicates are satisfied with + // this action. Wait to run the requests until after we update the + // pending queue because the request handler may synchronously + // dispatch again and run this service (that use case is + // completely valid). + for (const request of pending) { + if (request.predicate(action)) { + readyRequests.push(request); + } else { + stillPending.push(request); + } + } + + pending = stillPending; + for (const request of readyRequests) { + request.run(dispatch, getState, action); + } + } + + return next => action => { + if (action.type === NAME) { + pending.push(action); + return null; + } + const result = next(action); + checkPending(action); + return result; + }; +} +exports.waitUntilService = waitUntilService; diff --git a/devtools/client/shared/redux/middleware/xpcshell/.eslintrc.js b/devtools/client/shared/redux/middleware/xpcshell/.eslintrc.js new file mode 100644 index 0000000000..f69336917a --- /dev/null +++ b/devtools/client/shared/redux/middleware/xpcshell/.eslintrc.js @@ -0,0 +1,10 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +module.exports = { + // Extend from the shared list of defined globals for mochitests. + extends: "../../../../../.eslintrc.xpcshell.js", +}; diff --git a/devtools/client/shared/redux/middleware/xpcshell/head.js b/devtools/client/shared/redux/middleware/xpcshell/head.js new file mode 100644 index 0000000000..9f4ba4392a --- /dev/null +++ b/devtools/client/shared/redux/middleware/xpcshell/head.js @@ -0,0 +1,26 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* exported waitUntilState */ + +"use strict"; + +const { require } = ChromeUtils.importESModule( + "resource://devtools/shared/loader/Loader.sys.mjs" +); + +function waitUntilState(store, predicate) { + return new Promise(resolve => { + const unsubscribe = store.subscribe(check); + function check() { + if (predicate(store.getState())) { + unsubscribe(); + resolve(); + } + } + + // Fire the check immediately incase the action has already occurred + check(); + }); +} diff --git a/devtools/client/shared/redux/middleware/xpcshell/test_middleware-task-01.js b/devtools/client/shared/redux/middleware/xpcshell/test_middleware-task-01.js new file mode 100644 index 0000000000..d8516feecc --- /dev/null +++ b/devtools/client/shared/redux/middleware/xpcshell/test_middleware-task-01.js @@ -0,0 +1,66 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + createStore, + applyMiddleware, +} = require("resource://devtools/client/shared/vendor/redux.js"); +const { + task, +} = require("resource://devtools/client/shared/redux/middleware/task.js"); + +/** + * Tests that task middleware allows dispatching generators, promises and objects + * that return actions; + */ +add_task(async function () { + const store = applyMiddleware(task)(createStore)(reducer); + + store.dispatch(fetch1("generator")); + await waitUntilState(store, () => store.getState().length === 1); + equal( + store.getState()[0].data, + "generator", + "task middleware async dispatches an action via generator" + ); + + store.dispatch(fetch2("sync")); + await waitUntilState(store, () => store.getState().length === 2); + equal( + store.getState()[1].data, + "sync", + "task middleware sync dispatches an action via sync" + ); +}); + +function fetch1(data) { + return async function ({ dispatch, getState }) { + equal( + getState().length, + 0, + "`getState` is accessible in a generator action" + ); + let moreData = await new Promise(resolve => resolve(data)); + // Ensure it handles more than one yield + moreData = await new Promise(resolve => resolve(data)); + dispatch({ type: "fetch1", data: moreData }); + }; +} + +function fetch2(data) { + return { + type: "fetch2", + data, + }; +} + +function reducer(state = [], action) { + info("Action called: " + action.type); + if (["fetch1", "fetch2"].includes(action.type)) { + state.push(action); + } + return [...state]; +} diff --git a/devtools/client/shared/redux/middleware/xpcshell/test_middleware-task-02.js b/devtools/client/shared/redux/middleware/xpcshell/test_middleware-task-02.js new file mode 100644 index 0000000000..eaa573a8ae --- /dev/null +++ b/devtools/client/shared/redux/middleware/xpcshell/test_middleware-task-02.js @@ -0,0 +1,86 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * Tests that task middleware allows dispatching generators that dispatch + * additional sync and async actions. + */ + +const { + createStore, + applyMiddleware, +} = require("resource://devtools/client/shared/vendor/redux.js"); +const { + task, +} = require("resource://devtools/client/shared/redux/middleware/task.js"); + +add_task(async function () { + const store = applyMiddleware(task)(createStore)(reducer); + + store.dispatch(comboAction()); + await waitUntilState(store, () => store.getState().length === 4); + + equal( + store.getState()[0].type, + "fetchAsync-start", + "Async dispatched actions in a generator task are fired" + ); + equal( + store.getState()[1].type, + "fetchAsync-end", + "Async dispatched actions in a generator task are fired" + ); + equal( + store.getState()[2].type, + "fetchSync", + "Return values of yielded sync dispatched actions are correct" + ); + equal( + store.getState()[3].type, + "fetch-done", + "Return values of yielded async dispatched actions are correct" + ); + equal( + store.getState()[3].data.sync.data, + "sync", + "Return values of dispatched sync values are correct" + ); + equal( + store.getState()[3].data.async, + "async", + "Return values of dispatched async values are correct" + ); +}); + +function comboAction() { + return async function ({ dispatch, getState }) { + const data = {}; + data.async = await dispatch(fetchAsync("async")); + data.sync = await dispatch(fetchSync("sync")); + dispatch({ type: "fetch-done", data }); + }; +} + +function fetchSync(data) { + return { type: "fetchSync", data }; +} + +function fetchAsync(data) { + return async function ({ dispatch }) { + dispatch({ type: "fetchAsync-start" }); + const val = await new Promise(resolve => resolve(data)); + dispatch({ type: "fetchAsync-end" }); + return val; + }; +} + +function reducer(state = [], action) { + info("Action called: " + action.type); + if (/fetch/.test(action.type)) { + state.push(action); + } + return [...state]; +} diff --git a/devtools/client/shared/redux/middleware/xpcshell/test_middleware-task-03.js b/devtools/client/shared/redux/middleware/xpcshell/test_middleware-task-03.js new file mode 100644 index 0000000000..94087e31de --- /dev/null +++ b/devtools/client/shared/redux/middleware/xpcshell/test_middleware-task-03.js @@ -0,0 +1,50 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + createStore, + applyMiddleware, +} = require("resource://devtools/client/shared/vendor/redux.js"); +const { + task, + ERROR_TYPE, +} = require("resource://devtools/client/shared/redux/middleware/task.js"); + +/** + * Tests that the middleware handles errors thrown in tasks, and rejected promises. + */ + +add_task(async function () { + const store = applyMiddleware(task)(createStore)(reducer); + + store.dispatch(asyncError()); + await waitUntilState(store, () => store.getState().length === 1); + equal( + store.getState()[0].type, + ERROR_TYPE, + "generator errors dispatch ERROR_TYPE actions" + ); + equal( + store.getState()[0].error, + "task-middleware-error-generator", + "generator errors dispatch ERROR_TYPE actions with error" + ); +}); + +function asyncError() { + return async ({ dispatch, getState }) => { + const error = "task-middleware-error-generator"; + throw error; + }; +} + +function reducer(state = [], action) { + info("Action called: " + action.type); + if (action.type === ERROR_TYPE) { + state.push(action); + } + return [...state]; +} diff --git a/devtools/client/shared/redux/middleware/xpcshell/xpcshell.toml b/devtools/client/shared/redux/middleware/xpcshell/xpcshell.toml new file mode 100644 index 0000000000..d1bb2822d3 --- /dev/null +++ b/devtools/client/shared/redux/middleware/xpcshell/xpcshell.toml @@ -0,0 +1,11 @@ +[DEFAULT] +tags = "devtools" +head = "head.js" +firefox-appdir = "browser" +skip-if = ["os == 'android'"] + +["test_middleware-task-01.js"] + +["test_middleware-task-02.js"] + +["test_middleware-task-03.js"] diff --git a/devtools/client/shared/redux/moz.build b/devtools/client/shared/redux/moz.build new file mode 100644 index 0000000000..d4c7bc503b --- /dev/null +++ b/devtools/client/shared/redux/moz.build @@ -0,0 +1,15 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DIRS += [ + "middleware", +] + +DevToolsModules( + "create-store.js", + "subscriber.js", + "visibility-handler-connect.js", +) diff --git a/devtools/client/shared/redux/subscriber.js b/devtools/client/shared/redux/subscriber.js new file mode 100644 index 0000000000..39b0823770 --- /dev/null +++ b/devtools/client/shared/redux/subscriber.js @@ -0,0 +1,16 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at . */ + +"use strict"; + +function registerStoreObserver(store, subscriber) { + let oldState = store.getState(); + store.subscribe(() => { + const state = store.getState(); + subscriber(state, oldState); + oldState = state; + }); +} + +exports.registerStoreObserver = registerStoreObserver; diff --git a/devtools/client/shared/redux/visibility-handler-connect.js b/devtools/client/shared/redux/visibility-handler-connect.js new file mode 100644 index 0000000000..85835a1862 --- /dev/null +++ b/devtools/client/shared/redux/visibility-handler-connect.js @@ -0,0 +1,35 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + createFactory, + createElement, +} = require("resource://devtools/client/shared/vendor/react.js"); +const VisibilityHandler = createFactory( + require("resource://devtools/client/shared/components/VisibilityHandler.js") +); +const { + connect, +} = require("resource://devtools/client/shared/vendor/react-redux.js"); + +/** + * This helper is wrapping Redux's connect() method and applying + * HOC (VisibilityHandler component) on whatever component is + * originally passed in. The HOC is responsible for not causing + * rendering if the owner panel runs in the background. + */ +function visibilityHandlerConnect() { + const args = [].slice.call(arguments); + return component => { + return connect(...args)(props => { + return VisibilityHandler(null, createElement(component, props)); + }); + }; +} + +module.exports = { + connect: visibilityHandlerConnect, +}; -- cgit v1.2.3