tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

sources.js (14753B)


      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 /**
      6 * Sources reducer
      7 *
      8 * @module reducers/sources
      9 */
     10 
     11 import { prefs } from "../utils/prefs";
     12 import { createPendingSelectedLocation } from "../utils/location";
     13 
     14 export const UNDEFINED_LOCATION = Symbol("Undefined location");
     15 export const NO_LOCATION = Symbol("No location");
     16 
     17 export function initialSourcesState() {
     18  /* eslint sort-keys: "error" */
     19  return {
     20    /**
     21     * List of all breakpoint positions for all sources (generated and original).
     22     * Map of source id (string) to dictionary object whose keys are line numbers
     23     * and values of array of positions.
     24     * A position is an object made with two attributes:
     25     * location and generatedLocation. Both refering to breakpoint positions
     26     * in original and generated sources.
     27     * In case of generated source, the two location will be the same.
     28     *
     29     * Map(source id => Dictionary(int => array<Position>))
     30     */
     31    mutableBreakpointPositions: new Map(),
     32 
     33    /**
     34     * List of all breakable lines for original sources only.
     35     *
     36     * Map(source id => promise or array<int> : breakable line numbers>)
     37     *
     38     * The value can be a promise to indicate the lines are being loaded.
     39     */
     40    mutableOriginalBreakableLines: new Map(),
     41 
     42    /**
     43     * Map of the source id's to one or more related original source id's
     44     * Only generated sources which have related original sources will be maintained here.
     45     *
     46     * Map(source id => array<Original Source ID>)
     47     */
     48    mutableOriginalSources: new Map(),
     49 
     50    /**
     51     * Mapping of source id's to one or more source-actor's.
     52     * Dictionary whose keys are source id's and values are arrays
     53     * made of all the related source-actor's.
     54     * Note: The source mapped here are only generated sources.
     55     *
     56     * "source" are the objects stored in this reducer, in the `sources` attribute.
     57     * "source-actor" are the objects stored in the "source-actors.js" reducer, in its `sourceActors` attribute.
     58     *
     59     * Map(source id => array<Source Actor object>)
     60     */
     61    mutableSourceActors: new Map(),
     62 
     63    /**
     64     * All currently available sources.
     65     *
     66     * See create.js: `createSourceObject` method for the description of stored objects.
     67     */
     68    mutableSources: new Map(),
     69 
     70    /**
     71     * All sources associated with a given URL. When using source maps, multiple
     72     * sources can have the same URL.
     73     *
     74     * Map(url => array<source>)
     75     */
     76    mutableSourcesPerUrl: new Map(),
     77 
     78    /**
     79     * When we want to select a source that isn't available yet, use this.
     80     * The location object should have a url attribute instead of a sourceId.
     81     *
     82     * See `createPendingSelectedLocation` for the definition of this object.
     83     */
     84    pendingSelectedLocation: prefs.pendingSelectedLocation,
     85 
     86    /**
     87     * The actual currently selected location.
     88     * Only set if the related source is already registered in the sources reducer.
     89     * Otherwise, pendingSelectedLocation should be used. Typically for sources
     90     * which are about to be created.
     91     *
     92     * It also includes line and column information.
     93     *
     94     * See `createLocation` for the definition of this object.
     95     */
     96    selectedLocation: undefined,
     97 
     98    /**
     99     * When selectedLocation refers to a generated source mapping to an original source
    100     * via a source-map, refers to the related original location.
    101     *
    102     * This is UNDEFINED_LOCATION by default and will switch to NO_LOCATION asynchronously after location
    103     * selection if there is no valid original location to map to.
    104     */
    105    selectedOriginalLocation: UNDEFINED_LOCATION,
    106 
    107    /**
    108     * By default, the `selectedLocation` should be highlighted in the editor with a special background.
    109     * On demand, this flag can be set to false in order to prevent this.
    110     * The location will be shown, but not highlighted.
    111     */
    112    shouldHighlightSelectedLocation: true,
    113 
    114    /**
    115     * By default, if we have a source-mapped source, we would automatically try
    116     * to select and show the content of the original source. But, if we explicitly
    117     * select a generated source, we remember this choice. That, until we explicitly
    118     * select an original source.
    119     * Note that selections related to non-source-mapped sources should never
    120     * change this setting.
    121     */
    122    shouldSelectOriginalLocation: true,
    123  };
    124  /* eslint-disable sort-keys */
    125 }
    126 
    127 function update(state = initialSourcesState(), action) {
    128  switch (action.type) {
    129    case "ADD_SOURCES":
    130      return addSources(state, action.sources);
    131 
    132    case "ADD_ORIGINAL_SOURCES":
    133      return addSources(state, action.originalSources);
    134 
    135    case "INSERT_SOURCE_ACTORS":
    136      return insertSourceActors(state, action);
    137 
    138    case "SET_SELECTED_LOCATION": {
    139      let pendingSelectedLocation = null;
    140 
    141      if (action.location.source.url) {
    142        pendingSelectedLocation = createPendingSelectedLocation(
    143          action.location
    144        );
    145        prefs.pendingSelectedLocation = pendingSelectedLocation;
    146      }
    147 
    148      return {
    149        ...state,
    150        selectedLocation: action.location,
    151        selectedOriginalLocation: UNDEFINED_LOCATION,
    152        pendingSelectedLocation,
    153        shouldSelectOriginalLocation: action.shouldSelectOriginalLocation,
    154        shouldHighlightSelectedLocation: action.shouldHighlightSelectedLocation,
    155        shouldScrollToSelectedLocation: action.shouldScrollToSelectedLocation,
    156      };
    157    }
    158 
    159    case "CLEAR_SELECTED_LOCATION": {
    160      const pendingSelectedLocation = { url: "" };
    161      prefs.pendingSelectedLocation = pendingSelectedLocation;
    162 
    163      return {
    164        ...state,
    165        selectedLocation: null,
    166        selectedOriginalLocation: UNDEFINED_LOCATION,
    167        pendingSelectedLocation,
    168      };
    169    }
    170 
    171    case "SET_ORIGINAL_SELECTED_LOCATION": {
    172      if (action.location != state.selectedLocation) {
    173        return state;
    174      }
    175      return {
    176        ...state,
    177        selectedOriginalLocation: action.originalLocation,
    178      };
    179    }
    180 
    181    case "SET_GENERATED_SELECTED_LOCATION": {
    182      if (action.location != state.selectedLocation) {
    183        return state;
    184      }
    185      return {
    186        ...state,
    187        selectedGeneratedLocation: action.generatedLocation,
    188      };
    189    }
    190 
    191    case "SET_DEFAULT_SELECTED_LOCATION": {
    192      if (
    193        state.shouldSelectOriginalLocation ==
    194        action.shouldSelectOriginalLocation
    195      ) {
    196        return state;
    197      }
    198      return {
    199        ...state,
    200        shouldSelectOriginalLocation: action.shouldSelectOriginalLocation,
    201      };
    202    }
    203 
    204    case "SET_PENDING_SELECTED_LOCATION": {
    205      const pendingSelectedLocation = {
    206        url: action.url,
    207        line: action.line,
    208        column: action.column,
    209      };
    210 
    211      prefs.pendingSelectedLocation = pendingSelectedLocation;
    212      return { ...state, pendingSelectedLocation };
    213    }
    214 
    215    case "SET_ORIGINAL_BREAKABLE_LINES": {
    216      state.mutableOriginalBreakableLines.set(
    217        action.source.id,
    218        action.promise || action.breakableLines
    219      );
    220 
    221      return {
    222        ...state,
    223      };
    224    }
    225 
    226    case "ADD_BREAKPOINT_POSITIONS": {
    227      // Merge existing and new reported position if some where already stored
    228      let positions = state.mutableBreakpointPositions.get(action.source.id);
    229      if (positions) {
    230        positions = { ...positions, ...action.positions };
    231      } else {
    232        positions = action.positions;
    233      }
    234 
    235      state.mutableBreakpointPositions.set(action.source.id, positions);
    236 
    237      return {
    238        ...state,
    239      };
    240    }
    241 
    242    case "CLEAR_BREAKPOINT_POSITIONS": {
    243      if (!state.mutableBreakpointPositions.has(action.source.id)) {
    244        return state;
    245      }
    246 
    247      state.mutableBreakpointPositions.delete(action.source.id);
    248 
    249      return {
    250        ...state,
    251      };
    252    }
    253 
    254    case "REMOVE_SOURCES": {
    255      return removeSourcesAndActors(state, action);
    256    }
    257  }
    258 
    259  return state;
    260 }
    261 
    262 /*
    263 * Add sources to the sources store
    264 * - Add the source to the sources store
    265 * - Add the source URL to the source url map
    266 */
    267 function addSources(state, sources) {
    268  for (const source of sources) {
    269    state.mutableSources.set(source.id, source);
    270 
    271    // Update the source url map
    272    const existing = state.mutableSourcesPerUrl.get(source.url);
    273    if (existing) {
    274      // We never return this array from selectors as-is,
    275      // we either return the first entry or lookup for a precise entry
    276      // so we can mutate it.
    277      existing.push(source);
    278    } else {
    279      state.mutableSourcesPerUrl.set(source.url, [source]);
    280    }
    281 
    282    // In case of original source, maintain the mapping of generated source to original sources map.
    283    if (source.isOriginal) {
    284      const generatedSourceId = source.generatedSource.id;
    285      let originalSourceIds =
    286        state.mutableOriginalSources.get(generatedSourceId);
    287      if (!originalSourceIds) {
    288        originalSourceIds = [];
    289        state.mutableOriginalSources.set(generatedSourceId, originalSourceIds);
    290      }
    291      // We never return this array out of selectors, so mutate the list
    292      originalSourceIds.push(source.id);
    293    }
    294  }
    295 
    296  return { ...state };
    297 }
    298 
    299 function removeSourcesAndActors(state, action) {
    300  const {
    301    mutableSourcesPerUrl,
    302    mutableSources,
    303    mutableOriginalSources,
    304    mutableSourceActors,
    305    mutableOriginalBreakableLines,
    306    mutableBreakpointPositions,
    307  } = state;
    308 
    309  const newState = { ...state };
    310 
    311  for (const removedSource of action.sources) {
    312    const sourceId = removedSource.id;
    313 
    314    // Clear the urls Map
    315    const sourceUrl = removedSource.url;
    316    if (sourceUrl) {
    317      const sourcesForSameUrl = (
    318        mutableSourcesPerUrl.get(sourceUrl) || []
    319      ).filter(s => s != removedSource);
    320      if (!sourcesForSameUrl.length) {
    321        // All sources with this URL have been removed
    322        mutableSourcesPerUrl.delete(sourceUrl);
    323      } else {
    324        // There are other sources still alive with the same URL
    325        mutableSourcesPerUrl.set(sourceUrl, sourcesForSameUrl);
    326      }
    327    }
    328 
    329    mutableSources.delete(sourceId);
    330 
    331    // Note that the caller of this method queried the reducer state
    332    // to aggregate the related original sources.
    333    // So if we were having related original sources, they will be
    334    // in `action.sources`.
    335    mutableOriginalSources.delete(sourceId);
    336 
    337    // If a source is removed, immediately remove all its related source actors.
    338    // It can speed-up the following for loop cleaning actors.
    339    mutableSourceActors.delete(sourceId);
    340 
    341    if (removedSource.isOriginal) {
    342      mutableOriginalBreakableLines.delete(sourceId);
    343      // Also ensure removing this original source id in the array specific to its
    344      // generated source
    345      const generatedSourceId = removedSource.generatedSource.id;
    346      let originalSourceIds = mutableOriginalSources.get(generatedSourceId);
    347      if (originalSourceIds) {
    348        originalSourceIds = originalSourceIds.filter(id => id != sourceId);
    349        mutableOriginalSources.set(generatedSourceId, originalSourceIds);
    350      }
    351 
    352      // We should also remove the mapped location from the breakpoint positions
    353      //
    354      // `mutableBreakpointPositions` is a Map keyed per generated source id
    355      //   `generatedBreakpointPositions` is a Array
    356      //     `position` is an object with `location` and `generatedLocation` attributes
    357      const generatedBreakpointPositions =
    358        mutableBreakpointPositions.get(generatedSourceId);
    359      if (generatedBreakpointPositions) {
    360        for (const line in generatedBreakpointPositions) {
    361          for (const position of generatedBreakpointPositions[line]) {
    362            // Only clear the original mapped location if that's a breakpoint
    363            // for the currently removed original source. This generated/bundle source
    364            // may have breakpoints for many original sources.
    365            if (position.location.source == removedSource) {
    366              position.location = position.generatedLocation;
    367            }
    368          }
    369        }
    370      }
    371    }
    372 
    373    mutableBreakpointPositions.delete(sourceId);
    374 
    375    if (
    376      action.resetSelectedLocation &&
    377      newState.selectedLocation?.source == removedSource
    378    ) {
    379      newState.selectedLocation = null;
    380      newState.selectedOriginalLocation = UNDEFINED_LOCATION;
    381    }
    382  }
    383 
    384  for (const removedActor of action.actors) {
    385    const sourceId = removedActor.source;
    386    const actorsForSource = mutableSourceActors.get(sourceId);
    387    // actors may have already been cleared by the previous for..loop
    388    if (!actorsForSource) {
    389      continue;
    390    }
    391    const idx = actorsForSource.indexOf(removedActor);
    392    if (idx != -1) {
    393      actorsForSource.splice(idx, 1);
    394      // While the Map is mutable, we expect new array instance on each new change
    395      mutableSourceActors.set(sourceId, [...actorsForSource]);
    396    }
    397 
    398    // Remove the entry in the Map if there is no more actors for that source
    399    if (!actorsForSource.length) {
    400      mutableSourceActors.delete(sourceId);
    401    }
    402 
    403    if (
    404      action.resetSelectedLocation &&
    405      newState.selectedLocation?.sourceActor == removedActor
    406    ) {
    407      newState.selectedLocation = null;
    408      newState.selectedOriginalLocation = UNDEFINED_LOCATION;
    409    }
    410  }
    411 
    412  return newState;
    413 }
    414 
    415 function insertSourceActors(state, action) {
    416  const { sourceActors } = action;
    417 
    418  const { mutableSourceActors } = state;
    419  // The `sourceActor` objects are defined from `newGeneratedSources` action:
    420  // https://searchfox.org/mozilla-central/rev/4646b826a25d3825cf209db890862b45fa09ffc3/devtools/client/debugger/src/actions/sources/newSources.js#300-314
    421  for (const sourceActor of sourceActors) {
    422    const sourceId = sourceActor.source;
    423    // We always clone the array of source actors as we return it from selectors.
    424    // So the map is mutable, but its values are considered immutable and will change
    425    // anytime there is a new actor added per source ID.
    426    const existing = mutableSourceActors.get(sourceId);
    427    if (existing) {
    428      mutableSourceActors.set(sourceId, [...existing, sourceActor]);
    429    } else {
    430      mutableSourceActors.set(sourceId, [sourceActor]);
    431    }
    432  }
    433 
    434  const scriptActors = sourceActors.filter(
    435    item => item.introductionType === "scriptElement"
    436  );
    437  if (scriptActors.length) {
    438    // If new HTML sources are being added, we need to clear the breakpoint
    439    // positions since the new source is a <script> with new breakpoints.
    440    for (const { source } of scriptActors) {
    441      state.mutableBreakpointPositions.delete(source);
    442    }
    443  }
    444 
    445  return { ...state };
    446 }
    447 
    448 export default update;