actions.js (5851B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ 4 5 const { loadItemProperties } = require("resource://devtools/client/shared/components/object-inspector/utils/load-properties.js"); 6 const { 7 getPathExpression, 8 getParentFront, 9 getParentGripValue, 10 getValue, 11 nodeIsBucket, 12 getFront, 13 } = require("resource://devtools/client/shared/components/object-inspector/utils/node.js"); 14 const { getLoadedProperties } = require("resource://devtools/client/shared/components/object-inspector/reducer.js"); 15 16 /** 17 * This action is responsible for expanding a given node, which also means that 18 * it will call the action responsible to fetch properties. 19 */ 20 function nodeExpand(node, actor) { 21 return async ({ dispatch }) => { 22 dispatch({ type: "NODE_EXPAND", data: { node } }); 23 dispatch(nodeLoadProperties(node, actor)); 24 }; 25 } 26 27 function nodeCollapse(node) { 28 return { 29 type: "NODE_COLLAPSE", 30 data: { node }, 31 }; 32 } 33 34 /* 35 * This action checks if we need to fetch properties, entries, prototype and 36 * symbols for a given node. If we do, it will call the appropriate ObjectFront 37 * functions. 38 */ 39 function nodeLoadProperties(node, actor) { 40 return async ({ dispatch, client, getState }) => { 41 const state = getState(); 42 const loadedProperties = getLoadedProperties(state); 43 if (loadedProperties.has(node.path)) { 44 return; 45 } 46 47 try { 48 const properties = await loadItemProperties( 49 node, 50 client, 51 loadedProperties 52 ); 53 54 // If the client does not have a releaseActor function, it means the actors are 55 // handled directly by the consumer, so we don't need to track them. 56 if (!client || !client.releaseActor) { 57 actor = null; 58 } 59 60 dispatch(nodePropertiesLoaded(node, actor, properties)); 61 } catch (e) { 62 console.error(e); 63 dispatch(nodeCollapse(node)); 64 } 65 }; 66 } 67 68 function nodePropertiesLoaded(node, actor, properties) { 69 return { 70 type: "NODE_PROPERTIES_LOADED", 71 data: { node, actor, properties }, 72 }; 73 } 74 75 /* 76 * This action adds a property watchpoint to an object 77 */ 78 function addWatchpoint(item, watchpoint) { 79 return async function({ dispatch, client }) { 80 const { parent, name } = item; 81 let object = getValue(parent); 82 83 if (nodeIsBucket(parent)) { 84 object = getValue(parent.parent); 85 } 86 87 if (!object) { 88 return; 89 } 90 91 const path = parent.path; 92 const property = name; 93 const label = getPathExpression(item); 94 const actor = object.actor; 95 96 await client.addWatchpoint(object, property, label, watchpoint); 97 98 dispatch({ 99 type: "SET_WATCHPOINT", 100 data: { path, watchpoint, property, actor }, 101 }); 102 }; 103 } 104 105 /* 106 * This action removes a property watchpoint from an object 107 */ 108 function removeWatchpoint(item) { 109 return async function({ dispatch, client }) { 110 const { parent, name } = item; 111 let object = getValue(parent); 112 113 if (nodeIsBucket(parent)) { 114 object = getValue(parent.parent); 115 } 116 117 const property = name; 118 const path = parent.path; 119 const actor = object.actor; 120 121 await client.removeWatchpoint(object, property); 122 123 dispatch({ 124 type: "REMOVE_WATCHPOINT", 125 data: { path, property, actor }, 126 }); 127 }; 128 } 129 130 function getActorIDs(roots) { 131 if (!roots) { 132 return [] 133 } 134 135 const actorIds = []; 136 for (const root of roots) { 137 const front = getFront(root); 138 if (front?.actorID) { 139 actorIds.push(front.actorID); 140 } 141 } 142 143 return actorIds; 144 } 145 146 function closeObjectInspector(roots) { 147 return ({ client }) => { 148 releaseActors(client, roots); 149 }; 150 } 151 152 /* 153 * This action is dispatched when the `roots` prop, provided by a consumer of 154 * the ObjectInspector (inspector, console, …), is modified. It will clean the 155 * internal state properties (expandedPaths, loadedProperties, …) and release 156 * the actors consumed with the previous roots. 157 * It takes a props argument which reflects what is passed by the upper-level 158 * consumer. 159 */ 160 function rootsChanged(roots, oldRoots, autoReleaseObjectActors) { 161 return ({ dispatch, client }) => { 162 if (autoReleaseObjectActors) { 163 releaseActors(client, oldRoots, roots); 164 } 165 dispatch({ 166 type: "ROOTS_CHANGED", 167 data: roots, 168 }); 169 }; 170 } 171 172 /** 173 * Release any actors we don't need anymore 174 * 175 * @param {object} client: Object with a `releaseActor` method 176 * @param {Array} oldRoots: The roots in which we want to cleanup now-unused actors 177 * @param {Array} newRoots: The current roots (might have item that are also in oldRoots) 178 */ 179 async function releaseActors(client, oldRoots, newRoots = []) { 180 if (!client?.releaseActor ) { 181 return; 182 } 183 184 let actorIdsToRelease = getActorIDs(oldRoots); 185 if (newRoots.length) { 186 const newActorIds = getActorIDs(newRoots); 187 actorIdsToRelease = actorIdsToRelease.filter(id => !newActorIds.includes(id)); 188 } 189 190 if (!actorIdsToRelease.length) { 191 return; 192 } 193 await Promise.all(actorIdsToRelease.map(client.releaseActor)); 194 } 195 196 function invokeGetter(node, receiverId) { 197 return async ({ dispatch, client, _getState }) => { 198 try { 199 const objectFront = 200 getParentFront(node) || 201 client.createObjectFront(getParentGripValue(node)); 202 const getterName = node.propertyName || node.name; 203 204 const result = await objectFront.getPropertyValue(getterName, receiverId); 205 dispatch({ 206 type: "GETTER_INVOKED", 207 data: { 208 node, 209 result, 210 }, 211 }); 212 } catch (e) { 213 console.error(e); 214 } 215 }; 216 } 217 218 module.exports = { 219 closeObjectInspector, 220 invokeGetter, 221 nodeExpand, 222 nodeCollapse, 223 nodeLoadProperties, 224 nodePropertiesLoaded, 225 rootsChanged, 226 addWatchpoint, 227 removeWatchpoint, 228 };