Full Code of oslabs-beta/protostar-relay for AI

master ec442a7c2a9f cached
66 files
142.3 KB
37.4k tokens
56 symbols
1 requests
Download .txt
Repository: oslabs-beta/protostar-relay
Branch: master
Commit: ec442a7c2a9f
Files: 66
Total size: 142.3 KB

Directory structure:
gitextract_ezpa51h4/

├── .eslintignore
├── .flowconfig
├── .gitignore
├── .gitmodules
├── .travis.yml
├── .yarnrc
├── Dockerfile
├── LICENSE
├── README.md
├── __tests__/
│   ├── DevTools.spec.js
│   ├── Global.spec.js
│   ├── NetworkDisplayer.spec.js
│   ├── Record.spec.js
│   ├── StoreDisplayer.spec.js
│   ├── StoreTimeline.spec.js
│   ├── __mocks__/
│   │   └── styleMock.js
│   ├── __snapshots__/
│   │   ├── DevTools.spec.js.snap
│   │   ├── NetworkDisplayer.spec.js.snap
│   │   ├── StoreDisplayer.spec.js.snap
│   │   └── StoreTimeline.spec.js.snap
│   ├── bridge.spec.js
│   ├── global-setup.js
│   └── store.spec.js
├── babel.config.js
├── docker-compose.yml
├── flow.js
├── package.json
├── shells/
│   ├── browser/
│   │   ├── chrome/
│   │   │   ├── build.js
│   │   │   ├── manifest.json
│   │   │   ├── nottest.js
│   │   │   ├── now.json
│   │   │   └── watch.js
│   │   └── shared/
│   │       ├── build.js
│   │       ├── index.html
│   │       ├── main.html
│   │       ├── src/
│   │       │   ├── backend.js
│   │       │   ├── background.js
│   │       │   ├── contentScript.js
│   │       │   ├── inject.js
│   │       │   ├── injectGlobalHook.js
│   │       │   ├── main.js
│   │       │   └── utils.js
│   │       ├── view/
│   │       │   ├── App.jsx
│   │       │   ├── index.js
│   │       │   └── styles.scss
│   │       ├── webpack.backend.js
│   │       └── webpack.config.js
│   └── utils.js
└── src/
    ├── backend/
    │   ├── EnvironmentWrapper.js
    │   ├── agent.js
    │   ├── index.js
    │   ├── types.js
    │   └── utils.js
    ├── bridge.js
    ├── devtools/
    │   ├── DevTools.js
    │   ├── context.js
    │   ├── store.js
    │   ├── utils.js
    │   └── view/
    │       ├── Components/
    │       │   ├── EnvironmentSelector.js
    │       │   ├── Record.js
    │       │   └── SnapshotLinks.js
    │       ├── NetworkDisplayer.js
    │       ├── StoreDisplayer.js
    │       └── StoreTimeline.js
    ├── hook.js
    └── types.js

================================================
FILE CONTENTS
================================================

================================================
FILE: .eslintignore
================================================
node_modules

shells/browser/chrome/build
shells/browser/firefox/build
shells/browser/shared/build
shells/dev/dist
vendor
*.js.snap

package-lock.json
yarn.lock


================================================
FILE: .flowconfig
================================================
[ignore]
shells/browser/chrome/build/*
shells/browser/firefox/build/*
shells/dev/build/*

[declarations]
<PROJECT_ROOT>/node_modules/graphql

[include]

[libs]
/flow-typed/
./flow.js

[lints]

[options]
esproposal.class_instance_fields=enable
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue
suppress_comment=\\(.\\|\n\\)*\\$FlowIgnore
module.name_mapper='^src' ->'<PROJECT_ROOT>/src'
esproposal.optional_chaining=enable

[strict]

[version]
^0.113.0


================================================
FILE: .gitignore
================================================
/shells/browser/chrome/*.crx
/shells/browser/chrome/*.pem
/shells/browser/firefox/*.xpi
/shells/browser/firefox/*.pem
/shells/browser/shared/build
/packages/relay-devtools-core/dist
/shells/dev/dist
build
node_modules
npm-debug.log
yarn-error.log
.DS_Store
yarn-error.log
.vscode
.idea
*.pem
dist
package-lock.json

================================================
FILE: .gitmodules
================================================
[submodule "relay-examples"]
	path = relay-examples
	url = https://github.com/relayjs/relay-examples.git


================================================
FILE: .travis.yml
================================================
services:
  - docker
dist: xenial
script:
  - docker-compose up --abort-on-container-exit

================================================
FILE: .yarnrc
================================================
yarn-offline-mirror false


================================================
FILE: Dockerfile
================================================
FROM node:12.18.3
WORKDIR /usr/src/app
COPY . /usr/src/app
RUN npm install
CMD npm run test


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) Facebook, Inc. and its affiliates.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================

<div align="center">
  <img width="50%" src='./assets/protologo.png'></img>
</div>

<h1>Proto Relay</h1>
Proto Relay is a Chrome extension devtool for React Relay based off the official devtool. It is designed to be light-weight, performant, and easy-to-use. 

## Features
- [x] Preview Relay store content from the Chrome devtools panel
- [x] View store content over time with included snapshots
- [x] View store mutations and network queries

## Installation
1. Fork and clone this repository onto your local computer
2. Install dependencies and run a build using either the 'Yarn' or 'NPM' commands below:
```node
# Yarn
yarn install
yarn build

# NPM
npm run install
npm run build
```
3. Access the Chome extensions within the browser
4. Access [Chrome extensions](chrome://extensions/) within the browser
5. Click on "Load Unpacked"
6. Navigate and select the folder: protostar-relay  > Shells > browser > chrome > build > unpacked
7. Go to a website built with Relay and open the "proto*" panel. Websites that use Relay include:
   - [facebook.com](https://www.facebook.com/)
   - [artsy.com](https://www.artsy.net/)
   - [oculus.com](https://www.oculus.com/)
      

## How to Use
<img width="100%" src='./assets/protostar-records-filter.gif'></img>
- Example view of interacting with the Relay store.

<img width="100%" src='./assets/protostar-mutations.gif'></img>
- Example of snapshot functionality and viewing mutations.

## Contributing
Protostar-relay is currently in beta release. We encourage you to submit issues for any bugs or ideas for enhancements. Also feel free to fork this repo and submit pull requests to contribute as well. Below are some features we would like to add as we iterate on this project:
- Optimistic updates:
  - Visual representation.
  - List of all optimistic updates with pending/resolved status.
  - Control data flow.
  
## Google Chrome Web Store
Get it on the Chrome Extension Store: [coming soon]().

## Contributors
[Aryeh Kobrinsky](https://github.com/akobrinsky), 
[Liz Lotto](https://github.com/elizlotto), 
[Marc Burnie](https://github.com/marcburnie), 
[Qwen Ballard](https://github.com/qwenballard)


## License
This project is licensed under the MIT License- see the [LICENSE.md](https://github.com/oslabs-beta/protostar-relay/blob/master/LICENSE) for more details.

* Inspired by [Facebook's Relay Devtool](https://github.com/relayjs/relay-devtools)


================================================
FILE: __tests__/DevTools.spec.js
================================================
import React from 'react';
import { configure, shallow, render } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import renderer from 'react-test-renderer';
import DevTools from '../src/devtools/DevTools';

configure({ adapter: new Adapter() });

describe('DevTools', () => {
  let wrapper;
  const RealDate = Date;
  const names = { 1: 'first', 2: 'second', 3: 'third' };
  const props = {
    store: {
      getEnvironmentIDs: () => [1, 2, 3],
      getEnvironmentName: id => names[id]
    },
    bridge: 'hi'
  };

  beforeAll(() => {
    wrapper = shallow(<DevTools {...props} />);
  });

  it('Passes the bridge to provider', () => {
    expect(wrapper.prop('value')).toEqual(props.bridge);
  });

  it('Passes the store to provider', () => {
    expect(
      wrapper
        .children()
        .first()
        .prop('value')
    ).toEqual(props.store);
  });

  it('Has a dropdown select element for environmentID with an onChange method', () => {
    expect(wrapper.find('select').length).toEqual(1);
    expect(wrapper.find('select').prop('onChange')).not.toEqual(undefined);
  });

  it('Lists environments in dropdown selector', () => {
    const option = wrapper.find('option');
    expect(option.length).toEqual(3);
    expect(option.at(0).text()).toEqual(names[1]);
    expect(option.at(1).text()).toEqual(names[2]);
    expect(option.at(2).text()).toEqual(names[3]);
    expect(option.at(0).prop('value')).toEqual(1);
    expect(option.at(1).prop('value')).toEqual(2);
    expect(option.at(2).prop('value')).toEqual(3);
  });

  it('Has a StoreTimeline component', () => {
    expect(wrapper.find('StoreTimeline').length).toEqual(1);
  });

  it('Has a NetworkDisplayer component', () => {
    expect(wrapper.find('NetworkDisplayer').length).toEqual(1);
  });

  it('Passes the current environment to a currentEnvID prop on StoreTimeline and defaults to the first ID', () => {
    expect(wrapper.find('StoreTimeline').prop('currentEnvID')).toEqual(1);
  });

  it('Can select between different environments and pass the current environment to a currentEnvID prop on StoreTimeline', () => {
    wrapper.find('select').simulate('change', { target: { value: 2 } });
    expect(wrapper.find('StoreTimeline').prop('currentEnvID')).toEqual(2);
    wrapper.find('select').simulate('change', { target: { value: 3 } });
    expect(wrapper.find('StoreTimeline').prop('currentEnvID')).toEqual(3);
    wrapper.find('select').simulate('change', { target: { value: 1 } });
    expect(wrapper.find('StoreTimeline').prop('currentEnvID')).toEqual(1);
  });

  it('Has network hidden by default and store is visible', () => {
    expect(
      wrapper
        .find('StoreTimeline')
        .parent()
        .hasClass('is-hidden')
    ).toEqual(false);
    expect(
      wrapper
        .find('NetworkDisplayer')
        .parent()
        .hasClass('is-hidden')
    ).toEqual(true);
  });

  it('Allows user to select between store and network view', () => {
    const networkSelector = wrapper.find('#networkSelector');
    expect(networkSelector.length).toEqual(1);
    expect(networkSelector.prop('onClick')).not.toEqual(undefined);
    networkSelector.simulate('click');
    expect(
      wrapper
        .find('StoreTimeline')
        .parent()
        .hasClass('is-hidden')
    ).toEqual(true);
    expect(
      wrapper
        .find('NetworkDisplayer')
        .parent()
        .hasClass('is-hidden')
    ).toEqual(false);

    const storeSelector = wrapper.find('#storeSelector');
    expect(storeSelector.length).toEqual(1);
    expect(storeSelector.prop('onClick')).not.toEqual(undefined);
    storeSelector.simulate('click');
    expect(
      wrapper
        .find('StoreTimeline')
        .parent()
        .hasClass('is-hidden')
    ).toEqual(false);
    expect(
      wrapper
        .find('NetworkDisplayer')
        .parent()
        .hasClass('is-hidden')
    ).toEqual(true);
  });

  it('Renders correctly', () => {
    const date = new Date(Date.UTC(2020));
    global.Date = jest.fn(() => date);
    const tree = renderer.create(<DevTools {...props} />).toJSON();
    expect(tree).toMatchSnapshot();
    jest.clearAllMocks();
  });
});


================================================
FILE: __tests__/Global.spec.js
================================================
describe('Timezones', () => {
  it('should always be UTC', () => {
    expect(new Date().getTimezoneOffset()).toBe(0);
  });
});


================================================
FILE: __tests__/NetworkDisplayer.spec.js
================================================
import React from 'react';
import { configure, shallow, render } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import renderer from 'react-test-renderer';
import NetworkDisplayer from '../src/devtools/view/NetworkDisplayer';

configure({ adapter: new Adapter() });

describe('NetworkDisplayer', () => {
  let wrapper;
  const props = {};

  beforeAll(() => {
    wrapper = shallow(<NetworkDisplayer {...props} />);
  });

  it('My Test Case', () => {
    expect(true).toEqual(true);
  });

  it('My Test Case', () => {
    expect(wrapper.find('Record').length).toEqual(0);
  });

  it('Renders correctly', () => {
    const tree = renderer.create(<NetworkDisplayer {...props} />).toJSON();
    expect(tree).toMatchSnapshot();
  });
});


================================================
FILE: __tests__/Record.spec.js
================================================
import React from 'react';
import { configure, shallow, render } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
// import toJson from 'enzyme-to-json';

import renderer from 'react-test-renderer';

import Record from '../src/devtools/view/Components/Record';

configure({ adapter: new Adapter() });

describe('Record', () => {
  let wrapper;
  let children; //alternative to .next, not always required.
  const props = {
    //hardcode in what to pass into component
    hi: true,
    nested: { this: 'that' }
  };

  beforeAll(() => {
    wrapper = shallow(<Record {...props} />);
    children = wrapper.children();
  });

  it('Renders a <div> tag with a className of "records"', () => {
    //we are using methods from enzyme library so look at the docs
    expect(wrapper.type()).toEqual('div');
    expect(wrapper.hasClass('records')).toEqual(true);
  });

  it('Has two children divs: one with className objectProperty and the other with className nestedObject', () => {
    expect(children.length).toEqual(2);
    expect(children.first().hasClass('objectProperty')).toEqual(true);
    expect(children.last().hasClass('nestedObject')).toEqual(true);
  });

  it('Has a object property child with two spans, first has class of key and second has class of value. And has text values for the key and stringifies boolean values.', () => {
    const rootChildren = children.first().children();
    expect(rootChildren.length).toEqual(2);
    expect(rootChildren.first().hasClass('key')).toEqual(true);
    expect(rootChildren.first().text()).toEqual('hi: ');
    expect(rootChildren.last().hasClass('value')).toEqual(true);
    expect(rootChildren.last().text()).toEqual('true');
  });

  it('has a nested object child that has a span with class of key and a div with class of records. It also has a first child that has text of "nested: "', () => {
    const rootChildren = children.last().children();
    expect(rootChildren.first().text()).toEqual('nested: ');
    expect(rootChildren.length).toEqual(2);
    expect(rootChildren.first().hasClass('key')).toEqual(true);
    expect(rootChildren.last().find(Record).length).toEqual(1);
  });
});


================================================
FILE: __tests__/StoreDisplayer.spec.js
================================================
import React from 'react';
import { configure, shallow, render } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import renderer from 'react-test-renderer';
import StoreDisplayer from '../src/devtools/view/StoreDisplayer';

configure({ adapter: new Adapter() });

describe('StoreDisplayer', () => {

  let wrapper;
  let useEffect;
  const store = {
    '1': {
      '__id': '1',
      '__typename': 'User',
      'name': 'Marc'
    },
    '2': {
      '__id': '2',
      '__typename': 'User',
      'name': 'Aryeh'
    },
    '3': {
      '__id': '3',
      '__typename': 'User',
      'name': 'Liz'
    },
    '4': {
      '__id': '4',
      '__typename': 'User',
      'name': 'Qwen'
    },
    '5': {
      '__id': '5',
      '__typename': 'Post',
      'text': 'Hi'
    }
  }

  const mockUseEffect = () => {
    useEffect.mockImplementationOnce(f => f());
  };


  beforeEach(() => {
    useEffect = jest.spyOn(React, "useEffect");
    mockUseEffect();
    wrapper = shallow(<StoreDisplayer store={store} />);
  });

  it("Has a Record component with the filtered records passed as props", () => {
    expect(wrapper.find('Record').length).toEqual(1);
    expect(wrapper.find('Record').props()).toEqual(store);
  })

  it("Has a menu", () => {
    expect(wrapper.find('.menu').length).toEqual(1);
  })

  describe("Menu", () => {
    let menu;

    beforeEach(() => {
      menu = wrapper.find('.menu');
    })

    it("Generates a list of menu items from the store object", () => {
      expect(menu.find("#type-User").length).toEqual(1);
      expect(menu.find("#type-Post").length).toEqual(1);
      Object.keys(store).forEach(k => {
        expect(menu.find(`#id-${k}`).length).toEqual(1);
      })
    })

    it("Has menu items with an onClick event that filters the results displayed on the screen based on ID", () => {
      Object.keys(store).forEach(k => {
        menu.find(`#id-${k}`).simulate('click');
        expect(wrapper.find("Record").props()).toEqual({ [k]: store[k] })
      })
    })

    it("Has menu items with an onClick event that filters the results displayed on the screen based on type", () => {
      menu.find(`#type-User`).simulate('click');
      expect(wrapper.find("Record").props()).toEqual({
        '1': {
          '__id': '1',
          '__typename': 'User',
          'name': 'Marc'
        },
        '2': {
          '__id': '2',
          '__typename': 'User',
          'name': 'Aryeh'
        },
        '3': {
          '__id': '3',
          '__typename': 'User',
          'name': 'Liz'
        },
        '4': {
          '__id': '4',
          '__typename': 'User',
          'name': 'Qwen'
        }
      })
    })

    it("Adds an 'is-active' class to the currently selected menu item", () => {
      Object.keys(store).forEach(k => {
        let menuItem = wrapper.find(`#id-${k}`)
        expect(menuItem.length).toEqual(1)
        menuItem.props().onClick();
        menuItem = wrapper.find(`#id-${k}`)
        expect(menuItem.hasClass('is-active')).toEqual(true)
        expect(menuItem.hasClass('is-active')).toEqual(true)
        menu.find("a").forEach(el => {
          if (el !== menuItem) expect(el.hasClass('is-active')).toEqual(false)
        })
      })

      let menuItem = wrapper.find(`#type-User`)
      expect(menuItem.length).toEqual(1)
      menuItem.simulate('click');
      menuItem = wrapper.find(`#type-User`)
      expect(menuItem.hasClass('is-active')).toEqual(true)
      menu.find("a").forEach(el => {
        if (el !== menuItem) expect(el.hasClass('is-active')).toEqual(false)
      })
      menuItem = wrapper.find(`#type-Post`)
      expect(menuItem.length).toEqual(1)
      menuItem.simulate('click');
      menuItem = wrapper.find(`#type-Post`)
      expect(menuItem.hasClass('is-active')).toEqual(true)
      menu.find("a").forEach(el => {
        if (el !== menuItem) expect(el.hasClass('is-active')).toEqual(false)
      })
    })

    it("Removes the 'is-active' class when the reset button is clicked", () => {
      let menuItem = menu.find(`#type-User`)
      expect(menuItem.length).toEqual(1)
      menuItem.simulate('click');
      expect(wrapper.find('.menu').find(".is-active").length).toEqual(1);
      wrapper.find('button').simulate('click');
      expect(wrapper.find('.menu').find(".is-active").length).toEqual(0);
    })

    it('Has a search input with an onChange property', () => {
      expect(wrapper.find('input').length).toEqual(1);
      expect(wrapper.find('input').prop('onChange')).not.toBe(undefined);
    });

    describe('Search Box', () => {
      let search;

      beforeEach(() => {
        search = wrapper.find('input');
      })

      it("Filters the menu items", () => {
        search.prop('onChange')({ target: { value: 'Marc' } });
        jest.runAllTimers();
        expect(wrapper.find(`#id-1`).length).toEqual(1);
        Object.keys(store).forEach(k => {
          if (k !== '1') expect(wrapper.find(`#id-${k}`).length).toEqual(0);
        })
      })

      it("Debounces the input", () => {
        search.prop('onChange')({ target: { value: 'Marc' } });
        Object.keys(store).forEach(k => {
          if (k !== '1') expect(wrapper.find(`#id-${k}`).length).toEqual(1);
        })
        jest.runAllTimers();
        Object.keys(store).forEach(k => {
          if (k !== '1') expect(wrapper.find(`#id-${k}`).length).toEqual(0);
        })
      })
    });

    it('Has a Reset Button with an onClick property', () => {
      expect(wrapper.find('button').length).toEqual(1);
      expect(wrapper.find('button').prop('onClick')).not.toBe(undefined);
    });

    describe('Reset Button', () => {
      it("Has a reset button that removes any selectors", () => {
        menu.find(`#type-User`).simulate('click');
        expect(wrapper.find('Record').props()).not.toEqual(store)
        wrapper.find('button').simulate('click');
        expect(wrapper.find('Record').props()).toEqual(store)
      })
    });

  })

  it('Renders correctly', () => {
    const tree = renderer.create(<StoreDisplayer store={store} />).toJSON();
    expect(tree).toMatchSnapshot();
  })

});

================================================
FILE: __tests__/StoreTimeline.spec.js
================================================
import React from 'react';
import { configure, shallow, render } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import renderer from 'react-test-renderer';
import StoreTimeline from '../src/devtools/view/StoreTimeline';

configure({ adapter: new Adapter() });

describe('StoreTimeline', () => {
  let wrapper;
  const props = {
    currentEnvID: 1
  };

  beforeEach(() => {
    wrapper = shallow(<StoreTimeline {...props} />);
  });

  it('Renders a StoreDisplayer component and passes store as a prop', () => {
    expect(wrapper.find('StoreDisplayer').length).toEqual(1);
  });

  // it("Passes the store based on the currentEnvID", () => {
  // })

  // describe("Snapshots", () => {

  //   it("Takes a snapshot at startup", () => {

  //   })

  //   it("Has a snapshot button that takes and saves a snapshot", () => {

  //   })

  //   it("Defaults to displaying the latest store value when a snapshot is taken", () => {

  //   })

  //   it("Remembers snapshots when switching between environments", () => {

  //   })

  //   it("Has a snapshot text input", () => {

  //   })

  //   it("Has a previous buttons to move to the previous snapshot", () => {

  //   })

  //   it("Has a next button to move to the previous snapshot", () => {

  //   })

  //   it("Has a current button that shows the current store value", () => {

  //   })

  //   it("Has a slider that updates when a new snapshot is taken and when switching between environments", () => {

  //   })

  // })

  it('Renders correctly', () => {
    const date = new Date(Date.UTC(2020));
    global.Date = jest.fn(() => date);
    const tree = renderer.create(<StoreTimeline {...props} />).toJSON();
    expect(tree).toMatchSnapshot();
    jest.clearAllMocks();
  });
});


================================================
FILE: __tests__/__mocks__/styleMock.js
================================================
module.exports = {};

================================================
FILE: __tests__/__snapshots__/DevTools.spec.js.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`DevTools Renders correctly 1`] = `
Array [
  <div
    className="navigation"
  >
    <form
      className="env-select select is-small is-pulled-left"
    >
      <select
        className="env-select"
        onChange={[Function]}
      >
        <option
          value={1}
        >
          first
        </option>
        <option
          value={2}
        >
          second
        </option>
        <option
          value={3}
        >
          third
        </option>
      </select>
    </form>
    <div
      className="tabs is-toggle is-small is-pulled-left"
    >
      <ul>
        <li
          className="is-active"
        >
          <a
            id="storeSelector"
            onClick={[Function]}
          >
            <span
              className="icon is-small"
            >
              <i
                className="fas fa-database"
              />
            </span>
            <span>
              Store
            </span>
          </a>
        </li>
        <li
          className={false}
        >
          <a
            id="networkSelector"
            onClick={[Function]}
          >
            <span
              className="icon is-small"
            >
              <i
                className="fas fa-network-wired"
              />
            </span>
            <span>
              Network
            </span>
          </a>
        </li>
      </ul>
    </div>
    <div
      className="logo is-pulled-right"
    >
      <a
        href="https://github.com/oslabs-beta/protostar-relay"
        target="_blank"
      >
        <img
          src="../../assets/protorelay.png"
        />
      </a>
    </div>
  </div>,
  <div
    className="columns mb-0 is-multiline is-mobile"
  >
    <div
      className="column is-full-mobile is-one-quarter-desktop"
    >
      <div
        className="display-box"
      >
        <div
          className="snapshot-wrapper is-flex ml-2"
        >
          <input
            className="input is-small snapshot-btn is-primary"
            onChange={[Function]}
            placeholder="take a store snapshot"
            type="text"
            value=""
          />
          <button
            className="button is-small is-link"
            onClick={[Function]}
          >
            Snapshot
          </button>
        </div>
      </div>
      <div
        className="snapshots"
      >
        <div
          className="timeline-nav column is-full-desktop is-flex-mobile"
          id="timeline-mini-col"
        >
          <div
            aria-disabled={false}
            className="input-range"
            onKeyDown={[Function]}
            onKeyUp={[Function]}
            onMouseDown={[Function]}
            onTouchStart={[Function]}
          >
            <span
              className="input-range__label input-range__label--min"
            >
              <span
                className="input-range__label-container"
              >
                0
              </span>
            </span>
            <div
              className="input-range__track input-range__track--background"
              onMouseDown={[Function]}
              onTouchStart={[Function]}
            >
              <div
                className="input-range__track input-range__track--active"
                style={
                  Object {
                    "left": "0%",
                    "width": "0%",
                  }
                }
              />
              <span
                className="input-range__slider-container"
                style={
                  Object {
                    "left": "0%",
                    "position": "absolute",
                  }
                }
              >
                <span
                  className="input-range__label input-range__label--value"
                >
                  <span
                    className="input-range__label-container"
                  >
                    0
                  </span>
                </span>
                <div
                  aria-valuemax={1}
                  aria-valuemin={0}
                  aria-valuenow={0}
                  className="input-range__slider"
                  draggable="false"
                  onKeyDown={[Function]}
                  onMouseDown={[Function]}
                  onTouchStart={[Function]}
                  role="slider"
                  tabIndex="0"
                />
              </span>
            </div>
            <span
              className="input-range__label input-range__label--max"
            >
              <span
                className="input-range__label-container"
              >
                1
              </span>
            </span>
          </div>
          <div
            className="snapshot-nav has-text-centered has-text-right-mobile"
          >
            <button
              class="button is-small is-info is-light"
              onClick={[Function]}
            >
              <span
                className="icon is-medium"
              >
                <i
                  className="fas fa-fast-backward"
                />
              </span>
            </button>
            <button
              class="button is-small is-info is-light"
              onClick={[Function]}
            >
              Current
            </button>
            <button
              class="button is-small is-info is-light"
              onClick={[Function]}
            >
              <span
                className="icon is-medium"
              >
                <i
                  className="fas fa-fast-forward"
                />
              </span>
            </button>
          </div>
        </div>
        <div
          className="snapshot-info is-size-7 column is-full-desktop pt-0"
          id="snapshot-info-col"
        >
          <div>
            <aside
              className="menu"
            >
              <ul
                className="menu-list"
              >
                <li
                  onClick={[Function]}
                >
                  <a
                    className={false}
                    href="#"
                  >
                    12:00:00 AM
                    : 
                    at startup
                  </a>
                </li>
              </ul>
            </aside>
          </div>
        </div>
      </div>
    </div>
    <div
      className="column is-half-mobile scrollable"
    >
      <p
        class="control has-icons-left is-flex"
      >
        <input
          className="input is-small is-primary"
          onChange={[Function]}
          placeholder="Search"
          type="text"
        />
        <button
          className="button is-small is-link"
          onClick={[Function]}
        >
          Reset
        </button>
        <span
          class="icon is-left"
        >
          <i
            class="fas fa-search"
          />
        </span>
      </p>
      <aside
        className="menu"
      >
        <p
          className="menu-label mt-1"
        >
          Record List
        </p>
        <ul
          className="menu-list"
        />
      </aside>
    </div>
    <div
      className="column is-half-mobile scrollable"
    >
      <div
        className="display-box"
      >
        <div
          className="records"
        />
      </div>
    </div>
  </div>,
  <div
    className="is-hidden"
  >
    <div
      className="column is-one-third scrollable"
    >
      <p
        class="control has-icons-left is-flex ml-2"
      >
        <input
          className="input is-small is-primary mt-2"
          onChange={[Function]}
          placeholder="Search"
          type="text"
        />
        <button
          className="button is-small is-link my-2"
          onClick={[Function]}
        >
          Reset
        </button>
        <span
          class="icon is-left mt-2"
        >
          <i
            class="fas fa-search"
          />
        </span>
      </p>
      <aside
        className="menu"
      >
        <p
          className="menu-label ml-2"
        >
          Event List
        </p>
        <ul
          className="menu-list"
        />
      </aside>
    </div>
    <div
      className="column scrollable"
    >
      <div
        className="display-box"
      />
    </div>
  </div>,
]
`;


================================================
FILE: __tests__/__snapshots__/NetworkDisplayer.spec.js.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`NetworkDisplayer Renders correctly 1`] = `
Array [
  <div
    className="column is-one-third scrollable"
  >
    <p
      class="control has-icons-left is-flex ml-2"
    >
      <input
        className="input is-small is-primary mt-2"
        onChange={[Function]}
        placeholder="Search"
        type="text"
      />
      <button
        className="button is-small is-link my-2"
        onClick={[Function]}
      >
        Reset
      </button>
      <span
        class="icon is-left mt-2"
      >
        <i
          class="fas fa-search"
        />
      </span>
    </p>
    <aside
      className="menu"
    >
      <p
        className="menu-label ml-2"
      >
        Event List
      </p>
      <ul
        className="menu-list"
      />
    </aside>
  </div>,
  <div
    className="column scrollable"
  >
    <div
      className="display-box"
    />
  </div>,
]
`;


================================================
FILE: __tests__/__snapshots__/StoreDisplayer.spec.js.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`StoreDisplayer Renders correctly 1`] = `
Array [
  <div
    className="column is-half-mobile scrollable"
  >
    <p
      class="control has-icons-left is-flex"
    >
      <input
        className="input is-small is-primary"
        onChange={[Function]}
        placeholder="Search"
        type="text"
      />
      <button
        className="button is-small is-link"
        onClick={[Function]}
      >
        Reset
      </button>
      <span
        class="icon is-left"
      >
        <i
          class="fas fa-search"
        />
      </span>
    </p>
    <aside
      className="menu"
    >
      <p
        className="menu-label mt-1"
      >
        Record List
      </p>
      <ul
        className="menu-list"
      >
        <li>
          <a
            className={false}
            id="type-User"
            onClick={[Function]}
          >
            User
          </a>
          <ul>
            <li>
              <a
                className={false}
                id="id-1"
                onClick={[Function]}
              >
                1
              </a>
            </li>
            <li>
              <a
                className={false}
                id="id-2"
                onClick={[Function]}
              >
                2
              </a>
            </li>
            <li>
              <a
                className={false}
                id="id-3"
                onClick={[Function]}
              >
                3
              </a>
            </li>
            <li>
              <a
                className={false}
                id="id-4"
                onClick={[Function]}
              >
                4
              </a>
            </li>
          </ul>
        </li>
        <li>
          <a
            className={false}
            id="type-Post"
            onClick={[Function]}
          >
            Post
          </a>
          <ul>
            <li>
              <a
                className={false}
                id="id-5"
                onClick={[Function]}
              >
                5
              </a>
            </li>
          </ul>
        </li>
      </ul>
    </aside>
  </div>,
  <div
    className="column is-half-mobile scrollable"
  >
    <div
      className="display-box"
    >
      <div
        className="records"
      />
    </div>
  </div>,
]
`;


================================================
FILE: __tests__/__snapshots__/StoreTimeline.spec.js.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`StoreTimeline Renders correctly 1`] = `
Array [
  <div
    className="column is-full-mobile is-one-quarter-desktop"
  >
    <div
      className="display-box"
    >
      <div
        className="snapshot-wrapper is-flex ml-2"
      >
        <input
          className="input is-small snapshot-btn is-primary"
          onChange={[Function]}
          placeholder="take a store snapshot"
          type="text"
          value=""
        />
        <button
          className="button is-small is-link"
          onClick={[Function]}
        >
          Snapshot
        </button>
      </div>
    </div>
    <div
      className="snapshots"
    >
      <div
        className="timeline-nav column is-full-desktop is-flex-mobile"
        id="timeline-mini-col"
      >
        <div
          aria-disabled={false}
          className="input-range"
          onKeyDown={[Function]}
          onKeyUp={[Function]}
          onMouseDown={[Function]}
          onTouchStart={[Function]}
        >
          <span
            className="input-range__label input-range__label--min"
          >
            <span
              className="input-range__label-container"
            >
              0
            </span>
          </span>
          <div
            className="input-range__track input-range__track--background"
            onMouseDown={[Function]}
            onTouchStart={[Function]}
          >
            <div
              className="input-range__track input-range__track--active"
              style={
                Object {
                  "left": "0%",
                  "width": "0%",
                }
              }
            />
            <span
              className="input-range__slider-container"
              style={
                Object {
                  "left": "0%",
                  "position": "absolute",
                }
              }
            >
              <span
                className="input-range__label input-range__label--value"
              >
                <span
                  className="input-range__label-container"
                >
                  0
                </span>
              </span>
              <div
                aria-valuemax={1}
                aria-valuemin={0}
                aria-valuenow={0}
                className="input-range__slider"
                draggable="false"
                onKeyDown={[Function]}
                onMouseDown={[Function]}
                onTouchStart={[Function]}
                role="slider"
                tabIndex="0"
              />
            </span>
          </div>
          <span
            className="input-range__label input-range__label--max"
          >
            <span
              className="input-range__label-container"
            >
              1
            </span>
          </span>
        </div>
        <div
          className="snapshot-nav has-text-centered has-text-right-mobile"
        >
          <button
            class="button is-small is-info is-light"
            onClick={[Function]}
          >
            <span
              className="icon is-medium"
            >
              <i
                className="fas fa-fast-backward"
              />
            </span>
          </button>
          <button
            class="button is-small is-info is-light"
            onClick={[Function]}
          >
            Current
          </button>
          <button
            class="button is-small is-info is-light"
            onClick={[Function]}
          >
            <span
              className="icon is-medium"
            >
              <i
                className="fas fa-fast-forward"
              />
            </span>
          </button>
        </div>
      </div>
      <div
        className="snapshot-info is-size-7 column is-full-desktop pt-0"
        id="snapshot-info-col"
      >
        <div>
          <aside
            className="menu"
          >
            <ul
              className="menu-list"
            >
              <li
                onClick={[Function]}
              >
                <a
                  className={false}
                  href="#"
                >
                  12:00:00 AM
                  : 
                  at startup
                </a>
              </li>
            </ul>
          </aside>
        </div>
      </div>
    </div>
  </div>,
  <div
    className="column is-half-mobile scrollable"
  >
    <p
      class="control has-icons-left is-flex"
    >
      <input
        className="input is-small is-primary"
        onChange={[Function]}
        placeholder="Search"
        type="text"
      />
      <button
        className="button is-small is-link"
        onClick={[Function]}
      >
        Reset
      </button>
      <span
        class="icon is-left"
      >
        <i
          class="fas fa-search"
        />
      </span>
    </p>
    <aside
      className="menu"
    >
      <p
        className="menu-label mt-1"
      >
        Record List
      </p>
      <ul
        className="menu-list"
      />
    </aside>
  </div>,
  <div
    className="column is-half-mobile scrollable"
  >
    <div
      className="display-box"
    >
      <div
        className="records"
      />
    </div>
  </div>,
]
`;


================================================
FILE: __tests__/bridge.spec.js
================================================
/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 */

describe('Bridge', () => {
  let Bridge;

  beforeEach(() => {
    Bridge = require('../src/bridge').default;
  });

  it('should shutdown properly', () => {
    const wall = {
      listen: jest.fn(() => () => {}),
      send: jest.fn()
    };
    const bridge = new Bridge(wall);

    // Check that we're wired up correctly.
    bridge.send('init');
    jest.runAllTimers();
    expect(wall.send).toHaveBeenCalledWith('init', undefined, undefined);

    // Should flush pending messages and then shut down.
    wall.send.mockClear();
    bridge.send('update', '1');
    bridge.send('update', '2');
    bridge.shutdown();
    jest.runAllTimers();
    expect(wall.send).toHaveBeenCalledWith('update', '1', undefined);
    expect(wall.send).toHaveBeenCalledWith('update', '2', undefined);
    expect(wall.send).toHaveBeenCalledWith('shutdown', undefined, undefined);

    // Verify that the Bridge doesn't send messages after shutdown.
    spyOn(console, 'warn');
    wall.send.mockClear();
    bridge.send('should not send');
    jest.runAllTimers();
    expect(wall.send).not.toHaveBeenCalled();
    expect(console.warn).toHaveBeenCalledWith(
      'Cannot send message "should not send" through a Bridge that has been shutdown.'
    );
  });
});


================================================
FILE: __tests__/global-setup.js
================================================
module.exports = async () => {
  process.env.TZ = 'UTC';
};


================================================
FILE: __tests__/store.spec.js
================================================
/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 */

describe('Store', () => {
  let Store;
  let Bridge;

  beforeEach(() => {
    Bridge = require('../src/bridge').default;
    Store = require('../src/devtools/store').default;
  });

  it('should delete individual records correctly', () => {
    const wall = {
      listen: jest.fn(() => () => {}),
      send: jest.fn()
    };
    const bridge = new Bridge(wall);
    const store = new Store(bridge);

    store.mergeRecords(1, {
      Bob: {
        __id: 'Bob',
        __typename: 'User',
        profile_pic: 'a_different_url'
      },
      Lisa: {
        __id: 'Lisa',
        __typename: 'User',
        profile_pic: 'a_different_url'
      },
      user: {
        __id: 'user',
        __typename: 'User',
        profile_pic: 'new_url'
      }
    });

    expect(store.getRecords(1)).toEqual({
      Bob: {
        __id: 'Bob',
        __typename: 'User',
        profile_pic: 'a_different_url'
      },
      Lisa: {
        __id: 'Lisa',
        __typename: 'User',
        profile_pic: 'a_different_url'
      },
      user: {
        __id: 'user',
        __typename: 'User',
        profile_pic: 'new_url'
      }
    });

    store.removeRecord(1, 'Lisa');
    store.removeRecord(1, 'Bob');

    expect(store.getRecords(1)).toEqual({
      user: {
        __id: 'user',
        __typename: 'User',
        profile_pic: 'new_url'
      }
    });
  });

  it('should merge records correctly', () => {
    const wall = {
      listen: jest.fn(() => () => {}),
      send: jest.fn()
    };
    const bridge = new Bridge(wall);
    const store = new Store(bridge);

    // Testing case when oldRecords is null and we just set the map to the newRecords
    store.mergeRecords(1, { user: { __id: 'user', __typename: 'User' } });

    expect(store.getRecords(1)).toEqual({
      user: { __id: 'user', __typename: 'User' }
    });

    // Testing case when newRecords is null/undefined and we don't change anything
    store.mergeRecords(1, null);

    expect(store.getRecords(1)).toEqual({
      user: { __id: 'user', __typename: 'User' }
    });

    store.mergeRecords(1, undefined);

    expect(store.getRecords(1)).toEqual({
      user: { __id: 'user', __typename: 'User' }
    });

    // Testing multiple environments
    store.mergeRecords(2, { user: { __id: 'user', __typename: 'User' } });

    expect(store.getRecords(1)).toEqual({
      user: { __id: 'user', __typename: 'User' }
    });

    expect(store.getRecords(2)).toEqual({
      user: { __id: 'user', __typename: 'User' }
    });

    // Testing multiple records
    store.mergeRecords(1, {
      Jonathan: {
        __id: 'Jonathan',
        __typename: 'User',
        profile_pic: 'some_url'
      }
    });

    expect(store.getRecords(1)).toEqual({
      Jonathan: {
        __id: 'Jonathan',
        __typename: 'User',
        profile_pic: 'some_url'
      },
      user: { __id: 'user', __typename: 'User' }
    });

    //Testing overwriting a record
    store.mergeRecords(1, {
      Jonathan: {
        __id: 'Jonathan',
        __typename: 'User',
        profile_pic: 'a_different_url'
      }
    });

    expect(store.getRecords(1)).toEqual({
      Jonathan: {
        __id: 'Jonathan',
        __typename: 'User',
        profile_pic: 'a_different_url'
      },
      user: { __id: 'user', __typename: 'User' }
    });

    store.mergeRecords(1, {
      Bob: {
        __id: 'Jonathan',
        __typename: 'User',
        profile_pic: 'a_different_url'
      },
      Lisa: {
        __id: 'Lisa',
        __typename: 'User',
        profile_pic: 'a_different_url'
      },
      user: {
        __id: 'user',
        __typename: 'User',
        profile_pic: 'new_url'
      }
    });

    expect(store.getRecords(1)).toEqual({
      Jonathan: {
        __id: 'Jonathan',
        __typename: 'User',
        profile_pic: 'a_different_url'
      },
      Bob: {
        __id: 'Jonathan',
        __typename: 'User',
        profile_pic: 'a_different_url'
      },
      Lisa: {
        __id: 'Lisa',
        __typename: 'User',
        profile_pic: 'a_different_url'
      },
      user: {
        __id: 'user',
        __typename: 'User',
        profile_pic: 'new_url'
      }
    });

    expect(store.getRecords(2)).toEqual({
      user: { __id: 'user', __typename: 'User' }
    });

    store.mergeRecords(1, {
      Jonathan: {
        __id: 'Jonathan',
        __typename: 'User',
        nickname: 'Zuck'
      },
      Bob: {
        __id: 'Jonathan',
        __typename: 'User',
        profile_pic: 'a_different_url'
      },
      Lisa: {
        __id: 'Lisa',
        __typename: 'User',
        profile_pic: 'a_different_url'
      },
      user: {
        __id: 'user',
        __typename: 'User',
        profile_pic: 'new_url'
      }
    });

    expect(store.getRecords(1)).toEqual({
      Jonathan: {
        __id: 'Jonathan',
        __typename: 'User',
        profile_pic: 'a_different_url',
        nickname: 'Zuck'
      },
      Bob: {
        __id: 'Jonathan',
        __typename: 'User',
        profile_pic: 'a_different_url'
      },
      Lisa: {
        __id: 'Lisa',
        __typename: 'User',
        profile_pic: 'a_different_url'
      },
      user: {
        __id: 'user',
        __typename: 'User',
        profile_pic: 'new_url'
      }
    });

    expect(store.getRecords(2)).toEqual({
      user: { __id: 'user', __typename: 'User' }
    });

    // Deleting records
    store.mergeRecords(1, {
      Bob: null,
      Lisa: null,
      user: {
        __id: 'user',
        __typename: 'User',
        profile_pic: 'new_url'
      }
    });

    expect(store.getRecords(1)).toEqual({
      Jonathan: {
        __id: 'Jonathan',
        __typename: 'User',
        profile_pic: 'a_different_url',
        nickname: 'Zuck'
      },
      user: {
        __id: 'user',
        __typename: 'User',
        profile_pic: 'new_url'
      }
    });
  });

  it('should merge optimistic updates correctly', () => {
    const wall = {
      listen: jest.fn(() => () => {}),
      send: jest.fn()
    };
    const bridge = new Bridge(wall);
    const store = new Store(bridge);

    // Testing with a real optimistic source
    // Testing case when oldRecords is null and we just set the map to the newRecords
    store.mergeOptimisticRecords(1, {
      'Ym9va21hcms6MTAwMDAxNzg1MzU1MDU0OjY0NDcxNTQ0NTY1MDkyNDoyNTAxMDA4NjU3MDg1NDU60g==': {
        'unread_count(bookmark_render_location:"COMET_LEFT_NAV")': 0,
        'unread_count(bookmark_render_location:"COMET_TOP_TAB")': 0,
        'unread_count_string(bookmark_render_location:"COMET_LEFT_NAV")': null,
        __id: 'Ym9va21hcms6MTAwMDAxNzg1MzU1MDU0OjY0NDcxNTQ0NTY1MDkyNDoyNTAxMDA4NjU3MDg1NDU60g==',
        __typename: 'Bookmark'
      }
    });

    expect(store.getOptimisticUpdates(1)).toEqual({
      'Ym9va21hcms6MTAwMDAxNzg1MzU1MDU0OjY0NDcxNTQ0NTY1MDkyNDoyNTAxMDA4NjU3MDg1NDU60g==': {
        'unread_count(bookmark_render_location:"COMET_LEFT_NAV")': 0,
        'unread_count(bookmark_render_location:"COMET_TOP_TAB")': 0,
        'unread_count_string(bookmark_render_location:"COMET_LEFT_NAV")': null,
        __id: 'Ym9va21hcms6MTAwMDAxNzg1MzU1MDU0OjY0NDcxNTQ0NTY1MDkyNDoyNTAxMDA4NjU3MDg1NDU60g==',
        __typename: 'Bookmark'
      }
    });

    // Testing case when newRecords is null/undefined and we don't change anything
    store.mergeOptimisticRecords(1, null);
    jest.runAllTimers();

    expect(store.getOptimisticUpdates(1)).toEqual({
      'Ym9va21hcms6MTAwMDAxNzg1MzU1MDU0OjY0NDcxNTQ0NTY1MDkyNDoyNTAxMDA4NjU3MDg1NDU60g==': {
        'unread_count(bookmark_render_location:"COMET_LEFT_NAV")': 0,
        'unread_count(bookmark_render_location:"COMET_TOP_TAB")': 0,
        'unread_count_string(bookmark_render_location:"COMET_LEFT_NAV")': null,
        __id: 'Ym9va21hcms6MTAwMDAxNzg1MzU1MDU0OjY0NDcxNTQ0NTY1MDkyNDoyNTAxMDA4NjU3MDg1NDU60g==',
        __typename: 'Bookmark'
      }
    });

    store.mergeOptimisticRecords(1, undefined);
    jest.runAllTimers();

    expect(store.getOptimisticUpdates(1)).toEqual({
      'Ym9va21hcms6MTAwMDAxNzg1MzU1MDU0OjY0NDcxNTQ0NTY1MDkyNDoyNTAxMDA4NjU3MDg1NDU60g==': {
        'unread_count(bookmark_render_location:"COMET_LEFT_NAV")': 0,
        'unread_count(bookmark_render_location:"COMET_TOP_TAB")': 0,
        'unread_count_string(bookmark_render_location:"COMET_LEFT_NAV")': null,
        __id: 'Ym9va21hcms6MTAwMDAxNzg1MzU1MDU0OjY0NDcxNTQ0NTY1MDkyNDoyNTAxMDA4NjU3MDg1NDU60g==',
        __typename: 'Bookmark'
      }
    });

    // Removing all optimistic updates
    // Simulating the store.restore event

    store.clearOptimisticUpdates(1);
    jest.runAllTimers();

    expect(store.getRecords(1)).toEqual(undefined);

    // Testing multiple records
    store.mergeOptimisticRecords(1, {
      Jonathan: {
        __id: 'Jonathan',
        __typename: 'User',
        profile_pic: 'some_url'
      },
      Lilly: {
        __id: 'Lilly',
        __typename: 'User',
        profile_pic: 'url'
      }
    });
    jest.runAllTimers();

    expect(store.getOptimisticUpdates(1)).toEqual({
      Jonathan: {
        __id: 'Jonathan',
        __typename: 'User',
        profile_pic: 'some_url'
      },
      Lilly: {
        __id: 'Lilly',
        __typename: 'User',
        profile_pic: 'url'
      }
    });

    //Testing overwriting a record
    store.mergeOptimisticRecords(1, {
      Jonathan: {
        __id: 'Jonathan',
        __typename: 'User',
        profile_pic: 'a_different_url'
      }
    });
    jest.runAllTimers();

    expect(store.getOptimisticUpdates(1)).toEqual({
      Jonathan: {
        __id: 'Jonathan',
        __typename: 'User',
        profile_pic: 'a_different_url'
      },
      Lilly: {
        __id: 'Lilly',
        __typename: 'User',
        profile_pic: 'url'
      }
    });

    store.mergeOptimisticRecords(1, {
      Jonathan: {
        __id: 'Jonathan',
        __typename: 'User',
        nickname: 'Zuck'
      },
      Bob: {
        __id: 'Jonathan',
        __typename: 'User',
        profile_pic: 'a_different_url'
      },
      user: {
        __id: 'user',
        __typename: 'User',
        profile_pic: 'new_url'
      }
    });
    jest.runAllTimers();

    expect(store.getOptimisticUpdates(1)).toEqual({
      Jonathan: {
        __id: 'Jonathan',
        __typename: 'User',
        profile_pic: 'a_different_url',
        nickname: 'Zuck'
      },
      Bob: {
        __id: 'Jonathan',
        __typename: 'User',
        profile_pic: 'a_different_url'
      },
      Lilly: {
        __id: 'Lilly',
        __typename: 'User',
        profile_pic: 'url'
      },
      user: {
        __id: 'user',
        __typename: 'User',
        profile_pic: 'new_url'
      }
    });
  });
});


================================================
FILE: babel.config.js
================================================
/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

const chromeManifest = require('./shells/browser/chrome/manifest.json');


const minChromeVersion = parseInt(chromeManifest.minimum_chrome_version, 10);

validateVersion(minChromeVersion);


function validateVersion(version) {
  if (version > 0 && version < 200) {
    return;
  }
  throw new Error('Suspicious browser version in manifest: ' + version);
}

module.exports = api => {
  const isTest = api.env('test');
  const targets = {};
  if (isTest) {
    targets.node = 'current';
  } else {
    targets.chrome = minChromeVersion.toString();

    // This targets RN/Hermes.
    targets.ie = '11';
  }
  const plugins = [
    ['relay'],
    ['@babel/plugin-proposal-optional-chaining'],
    ['@babel/plugin-transform-flow-strip-types'],
    ['@babel/plugin-proposal-class-properties', { loose: false }],
  ];
  if (process.env.NODE_ENV !== 'production') {
    plugins.push(['@babel/plugin-transform-react-jsx-source']);
  }
  return {
    plugins,
    presets: [
      ['@babel/preset-env', { targets }],
      '@babel/preset-react',
      '@babel/preset-flow',
    ],
  };
};


================================================
FILE: docker-compose.yml
================================================
version: '3.0'
services:
  test:
    image: 'protorelay/protostar'
    container_name: 'protostar-test'
    volumes: 
      - .:/usr/src/app
      - node_modules:/usr/src/app/node_modules
    command: npm run test
volumes:
  node_modules: {}

================================================
FILE: flow.js
================================================
/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

declare module 'events' {
  declare class EventEmitter<Events: Object> {
    addListener<Event: $Keys<Events>>(
      event: Event,
      listener: (...$ElementType<Events, Event>) => any
    ): void;
    emit: <Event: $Keys<Events>>(
      event: Event,
      ...$ElementType<Events, Event>
    ) => void;
    removeListener(event: $Keys<Events>, listener: Function): void;
    removeAllListeners(event?: $Keys<Events>): void;
  }

  declare export default typeof EventEmitter;
}

declare var __DEV__: boolean;

declare var jasmine: {|
  getEnv: () => {|
    afterEach: (callback: Function) => void,
    beforeEach: (callback: Function) => void,
  |},
|};


================================================
FILE: package.json
================================================
{
  "version": "1.0.0",
  "name": "protostar-relay",
  "repository": "oslabs-beta/protostar-relay",
  "license": "MIT",
  "private": true,
  "workspaces": [
    "packages/*"
  ],
  "scripts": {
    "build": "cross-env NODE_ENV=production node ./shells/browser/chrome/build",
    "test": "cross-env TZ=\"UTC\" jest",
    "install-app": "npm i --prefix ./relay-examples/todo/",
    "start-test-app": "npm start --prefix ./relay-examples/todo/",
    "watch:chrome:frontend": "cross-env NODE_ENV=development node ./shells/browser/chrome/watch",
    "docker-test": "docker-compose up"
  },
  "jest": {
    "verbose": true,
    "testRegex": "((\\.|/*.)(spec))\\.js?$",
    "moduleNameMapper": {
      "\\.(css|less)$": "<rootDir>/__tests__/__mocks__/styleMock.js"
    },
    "timers": "fake",
    "globalSetup": "<rootDir>/__tests__/global-setup.js"
  },
  "devEngines": {
    "node": "10.x || 11.x"
  },
  "lint-staged": {
    "{shells,src}/**/*.{js,json,css}": [
      "prettier --write",
      "git add"
    ],
    "**/*.js": "eslint --max-warnings 0"
  },
  "devDependencies": {
    "@babel/core": "^7.7.5",
    "@babel/plugin-proposal-class-properties": "^7.7.4",
    "@babel/plugin-proposal-optional-chaining": "^7.7.5",
    "@babel/plugin-transform-flow-strip-types": "^7.7.4",
    "@babel/plugin-transform-react-jsx-source": "^7.7.4",
    "@babel/preset-env": "^7.7.6",
    "@babel/preset-flow": "^7.7.4",
    "@babel/preset-react": "^7.7.4",
    "@reach/menu-button": "^0.5.4",
    "@reach/tooltip": "^0.5.4",
    "archiver": "^3.0.0",
    "babel-core": "^7.0.0-bridge",
    "babel-eslint": "^10.0.3",
    "babel-jest": "^24.9.0",
    "babel-loader": "^8.0.6",
    "babel-plugin-relay": "master",
    "chance": "^1.0.18",
    "child-process-promise": "^2.2.1",
    "chrome-launch": "^1.1.4",
    "classnames": "2.2.1",
    "clipboard-js": "^0.3.6",
    "cross-env": "^6.0.3",
    "crx": "^5.0.0",
    "css-loader": "^1.0.1",
    "es6-symbol": "3.0.2",
    "eslint": "^6.6.0",
    "eslint-config-prettier": "^6.5.0",
    "eslint-config-react-app": "^5.0.2",
    "eslint-plugin-flowtype": "^4.3.0",
    "eslint-plugin-import": "^2.18.2",
    "eslint-plugin-jsx-a11y": "^6.2.3",
    "eslint-plugin-prettier": "^3.1.1",
    "eslint-plugin-react": "^7.16.0",
    "eslint-plugin-react-hooks": "^2.2.0",
    "events": "^3.0.0",
    "flow-bin": "^0.113.0",
    "fs-extra": "^3.0.1",
    "graphql": "^14.4.2",
    "jest": "^24.9.0",
    "lint-staged": "^7.0.5",
    "local-storage-fallback": "^4.1.1",
    "lodash.throttle": "^4.1.1",
    "log-update": "^2.0.0",
    "lru-cache": "^4.1.3",
    "nullthrows": "^1.0.0",
    "object-assign": "4.0.1",
    "opener": "^1.5.1",
    "prettier": "^1.19.1",
    "prop-types": "^15.7.2",
    "react": "^0.0.0-50b50c26f",
    "react-dom": "^0.0.0-50b50c26f",
    "react-relay": "master",
    "react-test-renderer": "0.0.0-fec00a869",
    "react-virtualized-auto-sizer": "^1.0.2",
    "relay-compiler": "master",
    "relay-config": "master",
    "rimraf": "^2.6.3",
    "sass-loader": "^10.0.2",
    "scheduler": "^0.0.0-50b50c26f",
    "style-loader": "^0.23.1",
    "web-ext": "^3.0.0",
    "webpack": "^4.41.3",
    "webpack-cli": "^3.1.2",
    "webpack-dev-server": "^3.10.0",
    "yargs": "^14.2.0"
  },
  "dependencies": {
    "bulma": "^0.9.0",
    "enzyme": "^3.11.0",
    "enzyme-adapter-react-16": "^1.15.4",
    "node-sass": "^4.14.1",
    "react-input-range": "^1.3.0",
    "sass": "^1.26.10"
  }
}


================================================
FILE: shells/browser/chrome/build.js
================================================
#!/usr/bin/env node
/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

const chalk = require('chalk');
const { execSync } = require('child_process');
const { existsSync } = require('fs');
const { isAbsolute, join, relative } = require('path');
const { argv } = require('yargs');
const build = require('../shared/build');

const main = async () => {
  const { crx, keyPath } = argv;

  if (crx) {
    if (!keyPath || !existsSync(keyPath)) {
      console.error('Must specify a key file (.pem) to build CRX');
      process.exit(1);
    }
  }

  await build('chrome');

  if (crx) {
    const cwd = join(__dirname, 'build');

    let safeKeyPath = keyPath;
    if (!isAbsolute(keyPath)) {
      safeKeyPath = join(relative(cwd, process.cwd()), keyPath);
    }

    execSync(`crx pack ./unpacked -o RelayDevTools.crx -p ${safeKeyPath}`, {
      cwd,
    });
  }

  console.log(chalk.green('\nThe Chrome extension has been built!'));
  console.log(chalk.green('You can test this build by running:'));
  console.log(chalk.gray('\n# From the relay-devtools root directory:'));
  console.log('yarn run test:chrome');
};

main();


================================================
FILE: shells/browser/chrome/manifest.json
================================================
{
  "manifest_version": 2,
  "name": "Proto Relay",
  "description": "Adds Relay debugging tools to the Chrome DevTool panel",
  "version": "1.0.0",
  "version_name": "1.0.0",
  "update_url": "https://github.com/oslabs-beta/protostar-relay",
  "minimum_chrome_version": "78",
  "icons": {
    "16": "assets/proto16.png",
    "32": "assets/proto32.png",
    "48": "assets/proto48.png",
    "128": "assets/proto128.png"
  },
  "browser_action": {
    "default_icon": {
      "16": "assets/proto16.png",
      "32": "assets/proto32.png",
      "48": "assets/proto48.png",
      "128": "assets/proto128.png"
    },
    "default_popup": "popups/disabled.html"
  },
  "devtools_page": "main.html",
  "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
  "web_accessible_resources": [
    "main.html",
    "build/backend.js",
    "build/renderer.js",
    "assets/protorelay.png"
  ],
  "background": {
    "scripts": [
      "build/background.js"
    ],
    "persistent": false
  },
  "permissions": [
    "file:///*",
    "http://*/*",
    "https://*/*",
    "webNavigation"
  ],
  "content_scripts": [
    {
      "matches": [
        "<all_urls>"
      ],
      "js": [
        "build/injectGlobalHook.js"
      ],
      "run_at": "document_start"
    }
  ]
}

================================================
FILE: shells/browser/chrome/nottest.js
================================================
#!/usr/bin/env node
/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

const chromeLaunch = require('chrome-launch'); // eslint-disable-line import/no-extraneous-dependencies
const { resolve } = require('path');

const EXTENSION_PATH = resolve('shells/browser/chrome/build/unpacked');
const START_URL = 'https://facebook.github.io/react/';

chromeLaunch(START_URL, {
  args: [`--load-extension=${EXTENSION_PATH}`],
});


================================================
FILE: shells/browser/chrome/now.json
================================================
{
  "name": "relay-devtools-experimental-chrome",
  "alias": ["relay-devtools-experimental-chrome"],
  "files": ["index.html", "RelayDevTools.zip"]
}


================================================
FILE: shells/browser/chrome/watch.js
================================================
#!/usr/bin/env node
/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

const { execSync } = require('child_process');
const { join } = require('path');


const webpackPath = join(
  __dirname,
  '..',
  '..',
  '..',
  'node_modules',
  '.bin',
  'webpack'
);
const binPath = join(__dirname, 'build', 'unpacked', 'build');
execSync(
  `${webpackPath} --config ../shared/webpack.config.js --output-path ${binPath} --watch`,
  {
    cwd: join(__dirname, '..', 'shared'),
    env: process.env,
    stdio: 'inherit',
  }
);


================================================
FILE: shells/browser/shared/build.js
================================================
#!/usr/bin/env node
/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

const archiver = require('archiver');
const { execSync } = require('child_process');
const { readFileSync, writeFileSync, createWriteStream } = require('fs');
const { copy, ensureDir, move, remove } = require('fs-extra');
const { join } = require('path');
const { getCommit } = require('../../utils');

// These files are copied along with Webpack-bundled files
// to produce the final web extension
const STATIC_FILES = ['assets', 'main.html', 'index.html'];

const preProcess = async (destinationPath, tempPath) => {
  await remove(destinationPath); // Clean up from previously completed builds
  await remove(tempPath); // Clean up from any previously failed builds
  await ensureDir(tempPath); // Create temp dir for this new build
};

const build = async (tempPath, manifestPath) => {
  const binPath = join(tempPath, 'bin');
  const zipPath = join(tempPath, 'zip');

  const webpackPath = join(__dirname, '..', '..', '..', 'node_modules', '.bin', 'webpack');
  execSync(`${webpackPath} --config webpack.config.js --output-path ${binPath}`, {
    cwd: __dirname,
    env: process.env,
    stdio: 'inherit'
  });
  execSync(`${webpackPath} --config webpack.backend.js --output-path ${binPath}`, {
    cwd: __dirname,
    env: process.env,
    stdio: 'inherit'
  });

  // Make temp dir
  await ensureDir(zipPath);

  const copiedManifestPath = join(zipPath, 'manifest.json');

  // Copy unbuilt source files to zip dir to be packaged:
  await copy(binPath, join(zipPath, 'build'));
  await copy(manifestPath, copiedManifestPath);
  await Promise.all(STATIC_FILES.map(file => copy(join(__dirname, file), join(zipPath, file))));

  const commit = getCommit();
  const versionDateString = `${commit} (${new Date().toLocaleDateString()})`;

  const manifest = JSON.parse(readFileSync(copiedManifestPath).toString());
  if (manifest.version_name) {
    manifest.version_name = versionDateString;
  } else {
    manifest.description += `\n\nCreated from revision ${versionDateString}`;
  }

  writeFileSync(copiedManifestPath, JSON.stringify(manifest, null, 2));

  // Pack the extension
  const archive = archiver('zip', { zlib: { level: 9 } });
  const zipStream = createWriteStream(join(tempPath, 'RelayDevTools.zip'));
  await new Promise((resolve, reject) => {
    archive
      .directory(zipPath, false)
      .on('error', err => reject(err))
      .pipe(zipStream);
    archive.finalize();
    zipStream.on('close', () => resolve());
  });
};

const postProcess = async (tempPath, destinationPath) => {
  const unpackedSourcePath = join(tempPath, 'zip');
  const packedSourcePath = join(tempPath, 'RelayDevTools.zip');
  const packedDestPath = join(destinationPath, 'RelayDevTools.zip');
  const unpackedDestPath = join(destinationPath, 'unpacked');

  await move(unpackedSourcePath, unpackedDestPath); // Copy built files to destination
  await move(packedSourcePath, packedDestPath); // Copy built files to destination
  await remove(tempPath); // Clean up temp directory and files
};

const main = async buildId => {
  const root = join(__dirname, '..', buildId);
  const manifestPath = join(root, 'manifest.json');
  const destinationPath = join(root, 'build');

  try {
    const tempPath = join(__dirname, 'build', buildId);
    await preProcess(destinationPath, tempPath);
    await build(tempPath, manifestPath);

    const builtUnpackedPath = join(destinationPath, 'unpacked');
    await postProcess(tempPath, destinationPath);

    return builtUnpackedPath;
  } catch (error) {
    console.error(error);
    process.exit(1);
  }

  return null;
};

module.exports = main;


================================================
FILE: shells/browser/shared/index.html
================================================
<!-- @format -->

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.0/css/bulma.min.css" />
    <link
      rel="stylesheet"
      href="https://use.fontawesome.com/releases/v5.14.0/css/all.css"
      integrity="sha384-HzLeBuhoNPvSl5KYnjx0BT+WB0QEEqLprO+NBkkk5gbc67FTaL7XIGa2w1L0Xbgc"
      crossorigin="anonymous"
    />
    <link rel="stylesheet" type="text/css" href="./styles.scss" />
    <title>Document</title>
  </head>
  <body>
    <div id="root"></div>
    <div id="container">Unable to find Relay on the page.</div>
    <script src="./build/index.js"></script>
  </body>
</html>


================================================
FILE: shells/browser/shared/main.html
================================================
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <script src="./build/main.js"></script>
  </head>
  <body></body>
</html>


================================================
FILE: shells/browser/shared/src/backend.js
================================================
/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 */

// Do not use imports or top-level requires here!
// Running module factories is intentionally delayed until we know the hook exists.
// This is to avoid issues like: https://github.com/facebook/react-devtools/issues/1039

function welcome(event) {
  if (event.source !== window || event.data.source !== 'relay-devtools-content-script') {
    return;
  }

  window.removeEventListener('message', welcome);

  setup(window.__RELAY_DEVTOOLS_HOOK__);
}

window.addEventListener('message', welcome);

function setup(hook) {
  const Agent = require('src/backend/agent').default;
  const Bridge = require('src/bridge').default;
  const { initBackend } = require('src/backend');

  const bridge = new Bridge({
    listen(fn) {
      const listener = event => {
        if (
          event.source !== window ||
          !event.data ||
          event.data.source !== 'relay-devtools-content-script' ||
          !event.data.payload
        ) {
          return;
        }
        fn(event.data.payload);
      };
      window.addEventListener('message', listener);
      return () => {
        window.removeEventListener('message', listener);
      };
    },
    send(event: string, payload: any, transferable?: Array<any>) {
      window.postMessage(
        {
          source: 'relay-devtools-bridge',
          payload: { event, payload: JSON.parse(JSON.stringify(payload)) }
        },
        '*',
        transferable
      );
    }
  });

  const agent = new Agent(bridge);
  agent.addListener('shutdown', () => {
    // If we received 'shutdown' from `agent`, we assume the `bridge` is already shutting down,
    // and that caused the 'shutdown' event on the `agent`, so we don't need to call `bridge.shutdown()` here.
    hook.emit('shutdown');
  });

  initBackend(hook, agent, window);
}


================================================
FILE: shells/browser/shared/src/background.js
================================================
/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 */

/* global chrome */

const ports = {};

chrome.runtime.onConnect.addListener(function(port) {
  let tab = null;
  let name = null;
  if (isNumeric(port.name)) {
    tab = port.name;
    name = 'devtools';
    installContentScript(+port.name);
  } else {
    tab = port.sender.tab.id;
    name = 'content-script';
  }

  if (!ports[tab]) {
    ports[tab] = {
      devtools: null,
      'content-script': null
    };
  }
  ports[tab][name] = port;

  if (ports[tab].devtools && ports[tab]['content-script']) {
    doublePipe(ports[tab].devtools, ports[tab]['content-script']);
  }
});

function isNumeric(str: string): boolean {
  return +str + '' === str;
}

function installContentScript(tabId: number) {
  chrome.tabs.executeScript(tabId, { file: '/build/contentScript.js' }, function() {});
}

function doublePipe(one, two) {
  one.onMessage.addListener(lOne);
  function lOne(message) {
    two.postMessage(message);
  }
  two.onMessage.addListener(lTwo);
  function lTwo(message) {
    one.postMessage(message);
  }
  function shutdown() {
    one.onMessage.removeListener(lOne);
    two.onMessage.removeListener(lTwo);
    one.disconnect();
    two.disconnect();
  }
  one.onDisconnect.addListener(shutdown);
  two.onDisconnect.addListener(shutdown);
}


================================================
FILE: shells/browser/shared/src/contentScript.js
================================================
/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 */

/* global chrome */

let backendDisconnected: boolean = false;
let backendInitialized: boolean = false;

function sayHelloToBackend() {
  window.postMessage(
    {
      source: 'relay-devtools-content-script',
      hello: true
    },
    '*'
  );
}

function handleMessageFromDevtools(message) {
  window.postMessage(
    {
      source: 'relay-devtools-content-script',
      payload: message
    },
    '*'
  );
}

function handleMessageFromPage(evt) {
  if (evt.source === window && evt.data && evt.data.source === 'relay-devtools-bridge') {
    backendInitialized = true;

    port.postMessage(evt.data.payload);
  }
}

function handleDisconnect() {
  backendDisconnected = true;

  window.removeEventListener('message', handleMessageFromPage);

  window.postMessage(
    {
      source: 'relay-devtools-content-script',
      payload: {
        type: 'event',
        event: 'shutdown'
      }
    },
    '*'
  );
}

// proxy from main page to devtools (via the background page)
var port = chrome.runtime.connect({
  name: 'content-script'
});
port.onMessage.addListener(handleMessageFromDevtools);
port.onDisconnect.addListener(handleDisconnect);

window.addEventListener('message', handleMessageFromPage);

sayHelloToBackend();

// The backend waits to install the global hook until notified by the content script.
// In the event of a page reload, the content script might be loaded before the backend is injected.
// Because of this we need to poll the backend until it has been initialized.
if (!backendInitialized) {
  const intervalID = setInterval(() => {
    if (backendInitialized || backendDisconnected) {
      clearInterval(intervalID);
    } else {
      sayHelloToBackend();
    }
  }, 500);
}


================================================
FILE: shells/browser/shared/src/inject.js
================================================
/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

/* global chrome */

export default function inject(scriptName: string, done: ?Function) {
  const source = `
  // the prototype stuff is in case document.createElement has been modified
  (function () {
    var script = document.constructor.prototype.createElement.call(document, 'script');
    script.src = "${scriptName}";
    script.charset = "utf-8";
    document.documentElement.appendChild(script);
    script.parentNode.removeChild(script);
  })()
  `;

  chrome.devtools.inspectedWindow.eval(source, function(response, error) {
    if (error) {
      console.log(error);
    }

    if (typeof done === 'function') {
      done();
    }
  });
}


================================================
FILE: shells/browser/shared/src/injectGlobalHook.js
================================================
/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 */

/* global chrome */

import nullthrows from 'nullthrows';
import { installHook } from 'src/hook';

function injectCode(code) {
  const script = document.createElement('script');
  script.textContent = code;

  // This script runs before the <head> element is created,
  // so we add the script to <html> instead.
  nullthrows(document.documentElement).appendChild(script);
  nullthrows(script.parentNode).removeChild(script);
}

let lastDetectionResult;

// We want to detect when a renderer attaches, and notify the "background page"
// (which is shared between tabs and can highlight the React icon).
// Currently we are in "content script" context, so we can't listen to the hook directly
// (it will be injected directly into the page).
// So instead, the hook will use postMessage() to pass message to us here.
// And when this happens, we'll send a message to the "background page".
window.addEventListener('message', function(evt) {
  if (evt.source === window && evt.data && evt.data.source === 'relay-devtools-detector') {
    lastDetectionResult = {
      hasDetectedReact: true
    };
    chrome.runtime.sendMessage(lastDetectionResult);
  }
});

// NOTE: Firefox WebExtensions content scripts are still alive and not re-injected
// while navigating the history to a document that has not been destroyed yet,
// replay the last detection result if the content script is active and the
// document has been hidden and shown again.
window.addEventListener('pageshow', function(evt) {
  if (!lastDetectionResult || evt.target !== window.document) {
    return;
  }
  chrome.runtime.sendMessage(lastDetectionResult);
});

const detectRelay = `
window.__RELAY_DEVTOOLS_HOOK__.on('environment', function(evt) {
  window.postMessage({
    source: 'relay-devtools-detector',
  }, '*');
});
`;

// Inject a `__RELAY_DEVTOOLS_HOOK__` global so that Relay can detect that the
// devtools are installed (and skip its suggestion to install the devtools).
injectCode(';(' + installHook.toString() + '(window))' + detectRelay);


================================================
FILE: shells/browser/shared/src/main.js
================================================
/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

/* global chrome */

import { createElement } from 'react';
import { unstable_createRoot as createRoot, flushSync } from 'react-dom';
import Bridge from 'src/bridge';
import Store from 'src/devtools/store';
import inject from './inject';
import { createViewElementSource } from './utils';
import DevTools from 'src/devtools/DevTools';

let panelCreated = false;

function createPanelIfReactLoaded() {
  if (panelCreated) {
    return;
  }

  chrome.devtools.inspectedWindow.eval(
    'window.__RELAY_DEVTOOLS_HOOK__ && window.__RELAY_DEVTOOLS_HOOK__.environments.size > 0',
    (pageHasRelay, error) => {
      if (!pageHasRelay || panelCreated) {
        return;
      }

      panelCreated = true;

      clearInterval(loadCheckInterval);

      let bridge = null;
      let store = null;

      let cloneStyleTags = null;
      let render = null;
      let root = null;
      let currentPanel = null;

      const tabId = chrome.devtools.inspectedWindow.tabId;

      function initBridgeAndStore() {
        const port = chrome.runtime.connect({
          name: '' + tabId
        });
        // Looks like `port.onDisconnect` does not trigger on in-tab navigation like new URL or back/forward navigation,
        // so it makes no sense to handle it here.

        bridge = new Bridge({
          listen(fn) {
            const listener = message => fn(message);
            // Store the reference so that we unsubscribe from the same object.
            const portOnMessage = port.onMessage;
            portOnMessage.addListener(listener);
            return () => {
              portOnMessage.removeListener(listener);
            };
          },
          send(event: string, payload: any, transferable?: Array<any>) {
            port.postMessage({ event, payload }, transferable);
          }
        });

        store = new Store(bridge);

        // Initialize the backend only once the Store has been initialized.
        // Otherwise the Store may miss important initial tree op codes.
        inject(chrome.runtime.getURL('build/backend.js'));

        const viewElementSourceFunction = createViewElementSource(bridge, store);

        render = () => {
          console.log('Rendering...');
          if (root) {
            root.render(
              createElement(DevTools, {
                bridge,
                // showTabBar: true,
                store,
                // viewElementSourceFunction,
                rootContainer: currentPanel.container
              })
            );
          }
        };

        render();
      }

      cloneStyleTags = () => {
        const linkTags = [];
        for (const linkTag of document.getElementsByTagName('link')) {
          if (linkTag.rel === 'stylesheet') {
            const newLinkTag = document.createElement('link');
            for (const attribute of linkTag.attributes) {
              newLinkTag.setAttribute(attribute.nodeName, attribute.nodeValue);
            }
            linkTags.push(newLinkTag);
          }
        }
        return linkTags;
      };

      initBridgeAndStore();

      function ensureInitialHTMLIsCleared(container) {
        if (container._hasInitialHTMLBeenCleared) {
          return;
        }
        container.innerHTML = '';
        container._hasInitialHTMLBeenCleared = true;
      }

      chrome.devtools.panels.create('proto*', '', 'index.html', panel => {
        panel.onShown.addListener(listenPanel => {
          if (currentPanel === listenPanel) {
            return;
          }
          currentPanel = listenPanel;

          if (listenPanel.container != null) {
            listenPanel.injectStyles(cloneStyleTags);
            ensureInitialHTMLIsCleared(listenPanel.container);
            root = createRoot(listenPanel.container);
            render();
          }
        });
        panel.onHidden.addListener(() => {
          // TODO: Stop highlighting and stuff.
        });
      });

      chrome.devtools.network.onNavigated.removeListener(checkPageForReact);

      // Shutdown bridge before a new page is loaded.
      chrome.webNavigation.onBeforeNavigate.addListener(function onBeforeNavigate(details) {
        // Ignore navigation events from other tabs (or from within frames).
        if (details.tabId !== tabId || details.frameId !== 0) {
          return;
        }

        // `bridge.shutdown()` will remove all listeners we added, so we don't have to.
        bridge.shutdown();
      });

      // Re-initialize DevTools panel when a new page is loaded.
      chrome.devtools.network.onNavigated.addListener(function onNavigated() {
        // It's easiest to recreate the DevTools panel (to clean up potential stale state).
        // We can revisit this in the future as a small optimization.
        flushSync(() => {
          root.unmount(() => {
            initBridgeAndStore();
          });
        });
      });
    }
  );
}

// Load (or reload) the DevTools extension when the user navigates to a new page.
function checkPageForReact() {
  createPanelIfReactLoaded();
}

chrome.devtools.network.onNavigated.addListener(checkPageForReact);

// Check to see if React has loaded once per second in case React is added
// after page load
const loadCheckInterval = setInterval(function() {
  createPanelIfReactLoaded();
}, 1000);

createPanelIfReactLoaded();


================================================
FILE: shells/browser/shared/src/utils.js
================================================
/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

/* global chrome */

export function createViewElementSource(bridge: Bridge, store: Store) {
  return function viewElementSource(id) {
    const rendererID = store.getRendererIDForElement(id);
    if (rendererID != null) {
      // Ask the renderer interface to determine the component function,
      // and store it as a global variable on the window
      bridge.send('viewElementSource', { id, rendererID });

      setTimeout(() => {
        // Ask Chrome to display the location of the component function,
        // assuming the renderer found one.
        chrome.devtools.inspectedWindow.eval(`
          if (window.$type != null) {
            inspect(window.$type);
          }
        `);
      }, 100);
    }
  };
}


================================================
FILE: shells/browser/shared/view/App.jsx
================================================
/** @format */
import React, { useEffect, useState } from 'react';

const port = chrome.runtime.connect({ name: 'test' });

const App = () => {
  const [tree, setTree] = useState();
  // const [history, setHistory] = useState([]);
  // const [count, setCount] = useState(1);

  // function is receiving fibernode state changes from backend and is saving that data to tree hook
  useEffect(() => {
    port.postMessage({
      name: 'connect',
      tabID: chrome.devtools.inspectedWindow.tabId
    });

    port.onMessage.addListener(message => {
      if (message.length === 3) {
        setTree(message);
      }
    });
  }, []);
  return <div> </div>;
};

export default App;


================================================
FILE: shells/browser/shared/view/index.js
================================================
/** @format */

import React from 'react';
import { render } from 'react-dom';

import App from './App.jsx';
import styles from './styles.scss';

/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

// Portal target container.
window.container = document.getElementById('container');

let hasInjectedStyles = false;

// DevTools styles are injected into the top-level document head (where the main React app is rendered).
// This method copies those styles to the child window where each panel (e.g. Elements, Profiler) is portaled.
window.injectStyles = getLinkTags => {
  if (!hasInjectedStyles) {
    hasInjectedStyles = true;

    const linkTags = getLinkTags();

    for (const linkTag of linkTags) {
      document.head.appendChild(linkTag);
    }
  }
};

render(<App />, document.getElementById('root'));


================================================
FILE: shells/browser/shared/view/styles.scss
================================================
/** @format */

//***********************************
//*********   VARIABLES   ***********
//***********************************

//***********************************
//**********   GENERAL   ************
//***********************************

.button {
  border-radius: 5px;
  border-color: gray;
  border-width: 3px;
}
.snapshot-nav button {
  margin-right: 12px;
}

#container {
  padding: 10px 0 0 10px;
}
.navigation {
  padding: 0;
  overflow: auto;
}
.navigation .tabs {
  margin: 0 0.75em;
}
button.button.is-small.is-link {
  margin-left: 10px;
}
.column {
  border-right: 1px solid #e4e0e0;
  height: 90vh;
}

#timeline-mini-col {
  border-right: none !important;
}

#snapshot-info-col {
  border-right: none !important;
}

.scrollable {
  overflow: scroll;
}

//***********************************
//**********    STORE    ************
//***********************************

.slider-textcolor {
  color: #060606;
  font-weight: bold;
  margin: 20px 0;
}

//***** MENU *****
.type {
  background: lightblue;
}

.menu-list {
  width: 100%;
  font-size: 10px;
}
.menu-list a {
  word-break: break-all;
}
.records {
  width: 100%;
  margin-left: 2em;
}

.records:first-child {
  margin-left: 0em;
  border-bottom: 1px grey solid;
}

//***** STORE DISPLAY *****
.display-box {
  border-radius: 5px;
  width: 100%;
  font-size: 10px;
}

.key {
  font-weight: bold;
  word-wrap: break-word;
}

.logo img {
  height: 30px;
}

.value {
  word-wrap: break-word;
}

.snapshots {
  padding: 0 10px;
}
.snapshot-nav {
  margin-top: 30px;
}
.input-range {
  margin: 35px 0px;
}
.tabs.is-toggle li.is-active a {
  background-color: #00d1b2;
  border-color: #00d1b2;
  color: #fff;
  z-index: 1;
}
.input-range__slider {
  appearance: none;
  background: #00d1b2;
  border: 1px solid #0bc3a8;
  border-radius: 100%;
  cursor: pointer;
  display: block;
  height: 1rem;
  margin-left: -0.5rem;
  margin-top: -0.65rem;
  outline: none;
  position: absolute;
  top: 50%;
  transition: transform 0.3s ease-out, box-shadow 0.3s ease-out;
  width: 1rem;
}
.input-range__slider:active {
  transform: scale(1.3);
}
.input-range__slider:focus {
  box-shadow: 0 0 0 5px rgba(63, 81, 181, 0.2);
}
.input-range--disabled .input-range__slider {
  background: #cccccc;
  border: 1px solid #cccccc;
  box-shadow: none;
  transform: none;
}

.input-range__slider-container {
  transition: left 0.3s ease-out;
}

.input-range__label {
  color: #aaaaaa;
  font-family: "Helvetica Neue", san-serif;
  font-size: 0.8rem;
  transform: translateZ(0);
  white-space: nowrap;
}

.input-range__label--min,
.input-range__label--max {
  bottom: -1.4rem;
  position: absolute;
}

.input-range__label--min {
  left: 0;
}

.input-range__label--max {
  right: 0;
}

.input-range__label--value {
  position: absolute;
  top: -1.8rem;
}

.input-range__label-container {
  left: -50%;
  position: relative;
}
.input-range__label--max .input-range__label-container {
  left: 50%;
}

.input-range__track {
  background: #eeeeee;
  border-radius: 0.3rem;
  cursor: pointer;
  display: block;
  height: 0.3rem;
  position: relative;
  transition: left 0.3s ease-out, width 0.3s ease-out;
}
.input-range--disabled .input-range__track {
  background: #eeeeee;
}

.input-range__track--background {
  left: 0;
  margin-top: -0.15rem;
  position: absolute;
  right: 0;
  top: 50%;
}

.input-range__track--active {
  background: #3f51b5;
}

.input-range {
  height: 1rem;
  position: relative;
  width: 100%;
}
.type {
  background: lightblue;
}

.menu-list {
  width: 100%;
}

.record-line {
  border-bottom: 1px grey solid;
}

.records {
  width: 100%;
  margin-left: 2em;
}

.records:first-child {
  margin-left: 0em;
}

.snapshots .column {
  height: auto;
}

@media screen and (max-width: 768px) {
  .column {
    height: auto;
  }
  .column.is-half-mobile {
    padding-top: 0;
  }
  .snapshots {
    .column {
      align-items: center;
      height: auto;
    }
  }
  .snapshot-nav {
    margin-top: 0;
    width: 100%;
  }
  .input-range {
    width: 70%;
    margin: 25px 0;
  }
}

//768 - 1020 beside snapshots get rid of columns and multi-line// added

//issue now between 712ish 988
// @media screen and (min-width: 769px) and (max-width: 1020px) {
//   .column {
//     height: auto;
//   }
//   .column.is-half-mobile {
//     padding-top: 0;
//   }
//   .snapshots {
//     .column {
//       align-items: center;
//       height: auto;
//     }
//   }
//   .snapshot-nav {
//     margin-top: 0;
//     width: 100%;
//   }
//   .input-range {
//     width: 70%;
//     margin: 25px 0;
//   }
// }


================================================
FILE: shells/browser/shared/webpack.backend.js
================================================
/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

const { resolve } = require('path');
const { DefinePlugin } = require('webpack');
const {
  getGitHubIssuesURL,
  getGitHubURL,
  getInternalDevToolsFeedbackGroup,
  getVersionString
} = require('../../utils');

const NODE_ENV = process.env.NODE_ENV;
if (!NODE_ENV) {
  console.error('NODE_ENV not set');
  process.exit(1);
}

const __DEV__ = NODE_ENV === 'development';

const GITHUB_URL = getGitHubURL();
const DEVTOOLS_VERSION = getVersionString();
const GITHUB_ISSUES_URL = getGitHubIssuesURL();
const DEVTOOLS_FEEDBACK_GROUP = getInternalDevToolsFeedbackGroup();

module.exports = {
  mode: __DEV__ ? 'development' : 'production',
  devtool: __DEV__ ? 'cheap-module-eval-source-map' : false,
  entry: {
    backend: './src/backend.js'
  },
  output: {
    path: __dirname + '/build',
    filename: '[name].js'
  },
  resolve: {
    alias: {
      src: resolve(__dirname, '../../../src')
    }
  },
  plugins: [
    new DefinePlugin({
      __DEV__: true,
      'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
      'process.env.GITHUB_URL': `"${GITHUB_URL}"`,
      'process.env.GITHUB_ISSUES_URL': `"${GITHUB_ISSUES_URL}"`,
      'process.env.DEVTOOLS_FEEDBACK_GROUP': `"${DEVTOOLS_FEEDBACK_GROUP}"`
    })
  ],
  module: {
    rules: [
      {
        test: /\.js$/,
        loader: 'babel-loader',
        options: {
          configFile: resolve(__dirname, '../../../babel.config.js')
        }
      },
      {
        test: /.(css|scss)$/,
        use: ['style-loader', 'css-loader', 'sass-loader']
      }
    ]
  }
};


================================================
FILE: shells/browser/shared/webpack.config.js
================================================
/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

const { resolve } = require('path');
const { DefinePlugin } = require('webpack');
const {
  getGitHubIssuesURL,
  getGitHubURL,
  getInternalDevToolsFeedbackGroup,
  getVersionString
} = require('../../utils');

const NODE_ENV = process.env.NODE_ENV;
if (!NODE_ENV) {
  console.error('NODE_ENV not set');
  process.exit(1);
}

const __DEV__ = NODE_ENV === 'development';

const GITHUB_URL = getGitHubURL();
const DEVTOOLS_VERSION = getVersionString();
const GITHUB_ISSUES_URL = getGitHubIssuesURL();
const DEVTOOLS_FEEDBACK_GROUP = getInternalDevToolsFeedbackGroup();

module.exports = {
  mode: __DEV__ ? 'development' : 'production',
  devtool: __DEV__ ? 'cheap-module-eval-source-map' : false,
  entry: {
    background: './src/background.js',
    contentScript: './src/contentScript.js',
    injectGlobalHook: './src/injectGlobalHook.js',
    index: './view/index.js',
    main: './src/main.js'
  },
  output: {
    path: __dirname + '/build',
    filename: '[name].js'
  },
  resolve: {
    alias: {
      src: resolve(__dirname, '../../../src')
    }
  },
  plugins: [
    new DefinePlugin({
      __DEV__: false,
      'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
      'process.env.GITHUB_URL': `"${GITHUB_URL}"`,
      'process.env.GITHUB_ISSUES_URL': `"${GITHUB_ISSUES_URL}"`,
      'process.env.DEVTOOLS_FEEDBACK_GROUP': `"${DEVTOOLS_FEEDBACK_GROUP}"`
    })
  ],
  module: {
    rules: [
      {
        test: /.jsx?$/,
        exclude: /node_modules/,
        loader: 'babel-loader',
        options: {
          configFile: resolve(__dirname, '../../../babel.config.js')
        }
      },
      {
        test: /.(css|scss)$/,
        use: ['style-loader', 'css-loader', 'sass-loader']
      }
    ]
  }
};


================================================
FILE: shells/utils.js
================================================
/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

const { execSync } = require('child_process');
const { readFileSync, existsSync } = require('fs');
const { resolve } = require('path');

function getCommit() {
  if (existsSync(resolve(__dirname, '../.git'))) {
    return execSync('git show -s --format=%h')
      .toString()
      .trim();
  }
  return execSync('hg id -i')
    .toString()
    .trim();
}

function getGitHubURL() {
  return 'https://github.com/relayjs/relay-devtools';
}

function getGitHubIssuesURL() {
  return 'https://github.com/relayjs/relay-devtools/issues/new';
}

function getInternalDevToolsFeedbackGroup() {
  return 'https://fburl.com/ieftwi8l';
}

function getVersionString() {
  const packageVersion = JSON.parse(readFileSync(resolve(__dirname, '../package.json'))).version;

  const commit = getCommit();

  return `${packageVersion}-${commit}`;
}

module.exports = {
  getCommit,
  getGitHubIssuesURL,
  getGitHubURL,
  getInternalDevToolsFeedbackGroup,
  getVersionString
};


================================================
FILE: src/backend/EnvironmentWrapper.js
================================================
/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 */

import type { DevToolsHook, RelayEnvironment, EnvironmentWrapper } from './types';

export function attach(
  hook: DevToolsHook,
  rendererID: number,
  environment: RelayEnvironment,
  global: Object
): EnvironmentWrapper {
  let pendingEventsQueue = [];
  const store = environment.getStore();

  const originalLog = environment.__log;
  environment.__log = event => {
    originalLog(event);
    // TODO(damassart): Make this a modular function
    if (pendingEventsQueue !== null) {
      pendingEventsQueue.push(event);
    } else {
      hook.emit('environment.event', {
        id: rendererID,
        data: event,
        eventType: 'environment'
      });
    }
  };

  const storeOriginalLog = store.__log;
  // TODO(damassart): Make this cleaner
  store.__log = event => {
    if (storeOriginalLog !== null) {
      storeOriginalLog(event);
    }
    switch (event.name) {
      case 'store.gc':
        // references is a Set, but we can't serialize Sets,
        // so we convert references to an Array
        event.references = Array.from(event.references);
        hook.emit('environment.event', {
          id: rendererID,
          data: event,
          eventType: 'store'
        });
        break;
      case 'store.notify.complete':
        event.invalidatedRecordIDs = Array.from(event.invalidatedRecordIDs);
        hook.emit('environment.event', {
          id: rendererID,
          data: event,
          eventType: 'store'
        });
        break;
      default:
        hook.emit('environment.event', {
          id: rendererID,
          data: event,
          eventType: 'store'
        });
        break;
    }
  };

  function cleanup() {
    // We don't patch any methods so there is no cleanup.
    environment.__log = originalLog;
    store.__log = storeOriginalLog;
  }

  function sendStoreRecords() {
    const records = store.getSource().toJSON();
    hook.emit('environment.store', {
      name: 'refresh.store',
      id: rendererID,
      records
    });
  }

  function flushInitialOperations() {
    // TODO(damassart): Make this a modular function
    if (pendingEventsQueue != null) {
      pendingEventsQueue.forEach(pendingEvent => {
        hook.emit('environment.event', {
          id: rendererID,
          envName: environment.configName,
          data: pendingEvent,
          eventType: 'environment'
        });
      });
      pendingEventsQueue = null;
    }
    this.sendStoreRecords();
  }

  return {
    cleanup,
    sendStoreRecords,
    flushInitialOperations
  };
}


================================================
FILE: src/backend/agent.js
================================================
/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 */

import EventEmitter from 'events';
import type { BackendBridge } from 'src/bridge';

import type { EnvironmentID, EnvironmentWrapper } from './types';

export default class Agent extends EventEmitter<{|
  shutdown: [],
  refreshStore: []
|}> {
  _bridge: BackendBridge;
  _recordChangeDescriptions: boolean = false;
  _environmentWrappers: {
    [key: EnvironmentID]: EnvironmentWrapper
  } = {};

  constructor(bridge: BackendBridge) {
    super();

    this._bridge = bridge;

    bridge.addListener('shutdown', this.shutdown);
    bridge.addListener('refreshStore', this.refreshStore);
  }

  get environmentWrappers(): {
    [key: EnvironmentID]: EnvironmentWrapper
  } {
    return this._environmentWrappers;
  }

  shutdown = () => {
    // Clean up the overlay if visible, and associated events.
    this.emit('shutdown');
  };

  refreshStore = (id: EnvironmentID) => {
    const wrapper = this._environmentWrappers[id];
    wrapper && wrapper.sendStoreRecords();
  };

  onEnvironmentInitialized = (data: mixed) => {
    this._bridge.send('environmentInitialized', [data]);
  };

  setEnvironmentWrapper = (id: number, environmentWrapper: EnvironmentWrapper) => {
    this._environmentWrappers[id] = environmentWrapper;
  };

  onStoreData = (data: mixed) => {
    this._bridge.send('storeRecords', [data]);
  };

  onEnvironmentEvent = (data: mixed) => {
    this._bridge.send('events', [data]);
  };
}


================================================
FILE: src/backend/index.js
================================================
/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 */

import type { DevToolsHook, RelayEnvironment, EnvironmentWrapper } from './types';
import type Agent from './agent';

import { attach } from './EnvironmentWrapper';

export function initBackend(hook: DevToolsHook, agent: Agent, global: Object): () => void {
  const subs = [
    hook.sub('environment.event', data => {
      agent.onEnvironmentEvent(data);
    }),
    hook.sub('environment.store', data => {
      agent.onStoreData(data);
    }),
    hook.sub(
      'environment-attached',
      ({
        id,
        environment,
        environmentWrapper
      }: {
        id: number,
        environment: RelayEnvironment,
        environmentWrapper: EnvironmentWrapper
      }) => {
        agent.setEnvironmentWrapper(id, environmentWrapper);
        agent.onEnvironmentInitialized({
          id: id,
          environmentName: environment.configName
        });
        // Now that the Store and the renderer interface are connected,
        // it's time to flush the pending operation codes to the frontend.
        environmentWrapper.flushInitialOperations();
      }
    )
  ];

  const attachEnvironment = (id: number, environment: RelayEnvironment) => {
    let environmentWrapper = hook.environmentWrappers.get(id);

    // Inject any not-yet-injected renderers (if we didn't reload-and-profile)
    if (!environmentWrapper) {
      environmentWrapper = attach(hook, id, environment, global);
      hook.environmentWrappers.set(id, environmentWrapper);
    }

    // Notify the DevTools frontend about new renderers.
    hook.emit('environment-attached', {
      id,
      environment,
      environmentWrapper
    });
  };

  // Connect renderers that have already injected themselves.
  hook.environments.forEach((environment, id) => {
    attachEnvironment(id, environment);
  });

  // Connect any new renderers that injected themselves.
  subs.push(
    hook.sub(
      'environment',
      ({ id, environment }: { id: number, environment: RelayEnvironment }) => {
        attachEnvironment(id, environment);
      }
    )
  );

  return () => {
    subs.forEach(fn => fn());
  };
}


================================================
FILE: src/backend/types.js
================================================
/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 */

export type EnvironmentID = number;

export type RelayRecordSource = {
  getRecordIDs: () => string,
  get: (id: string) => any,
  toJSON: () => any
};

export type RelayStore = {
  getSource: () => RelayRecordSource,
  __log: (event: Object) => void
};

export type RelayEnvironment = {
  execute: (options: any) => any,
  configName: ?string,
  getStore: () => RelayStore,
  __log: (event: Object) => void
};

export type EnvironmentWrapper = {
  flushInitialOperations: () => void,
  sendStoreRecords: () => void,
  cleanup: () => void
};

export type Handler = (data: any) => void;

export type DevToolsHook = {
  registerEnvironment: (env: RelayEnvironment) => number | null,
  // listeners: { [key: string]: Array<Handler> },
  environmentWrappers: Map<EnvironmentID, EnvironmentWrapper>,
  environments: Map<EnvironmentID, RelayEnvironment>,

  emit: (event: string, data: any) => void,
  on: (event: string, handler: Handler) => void,
  off: (event: string, handler: Handler) => void,
  // reactDevtoolsAgent?: ?Object,
  sub: (event: string, handler: Handler) => () => void
};


================================================
FILE: src/backend/utils.js
================================================
/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 */

export function copyWithSet(
  obj: Object | Array<any>,
  path: Array<number | string>,
  value: any,
  index: number = 0
): Object | Array<any> {
  console.log('[utils] copyWithSet()', obj, path, index, value);
  if (index >= path.length) {
    return value;
  }
  const key = parseInt(path[index]);
  const updated = Array.isArray(obj) ? obj.slice() : { ...obj };
  updated[key] = copyWithSet(obj[key], path, value, index + 1);
  return updated;
}


================================================
FILE: src/bridge.js
================================================
/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 */

import EventEmitter from 'events';

import type { EnvironmentInfo, EventData, StoreData, Wall } from './types';

const BATCH_DURATION = 100;

type Message = {|
  event: string,
  payload: any
|};

type BackendEvents = {|
  events: [Array<EventData>],
  shutdown: [],
  environmentInitialized: [Array<EnvironmentInfo>],
  storeRecords: [Array<StoreData>]
|};

type FrontendEvents = {|
  refreshStore: [number]
|};
class Bridge<OutgoingEvents: Object, IncomingEvents: Object> extends EventEmitter<{|
  ...IncomingEvents,
  ...OutgoingEvents
|}> {
  _isShutdown: boolean = false;
  _messageQueue: Array<any> = [];
  _timeoutID: TimeoutID | null = null;
  _wall: Wall;
  _wallUnlisten: Function | null = null;

  constructor(wall: Wall) {
    super();

    this._wall = wall;

    this._wallUnlisten =
      wall.listen((message: Message) => {
        (this: any).emit(message.event, message.payload);
      }) || null;
  }

  send(event: string, payload: any, transferable?: Array<any>) {
    if (this._isShutdown) {
      console.warn(`Cannot send message "${event}" through a Bridge that has been shutdown.`);
      return;
    }

    // When we receive a message:
    // - we add it to our queue of messages to be sent
    // - if there hasn't been a message recently, we set a timer for 0 ms in
    //   the future, allowing all messages created in the same tick to be sent
    //   together
    // - if there *has* been a message flushed in the last BATCH_DURATION ms
    //   (or we're waiting for our setTimeout-0 to fire), then _timeoutID will
    //   be set, and we'll simply add to the queue and wait for that
    this._messageQueue.push(event, payload, transferable);
    if (!this._timeoutID) {
      this._timeoutID = setTimeout(this._flush, 0);
    }
  }

  shutdown() {
    if (this._isShutdown) {
      console.warn('Bridge was already shutdown.');
      return;
    }

    // Queue the shutdown outgoing message for subscribers.
    this.send('shutdown');

    // Mark this bridge as destroyed, i.e. disable its public API.
    this._isShutdown = true;

    // Disable the API inherited from EventEmitter that can add more listeners and send more messages.
    (this: any).addListener = function() {};
    this.emit = function() {};
    // NOTE: There's also EventEmitter API like `on` and `prependListener` that we didn't add to our Flow type of EventEmitter.

    // Unsubscribe this bridge incoming message listeners to be sure, and so they don't have to do that.
    this.removeAllListeners();

    // Stop accepting and emitting incoming messages from the wall.
    const wallUnlisten = this._wallUnlisten;
    if (wallUnlisten) {
      wallUnlisten();
    }

    // Synchronously flush all queued outgoing messages.
    // At this step the subscribers' code may run in this call stack.
    do {
      this._flush();
    } while (this._messageQueue.length);

    // Make sure once again that there is no dangling timer.
    clearTimeout(this._timeoutID);
    this._timeoutID = null;
  }

  _flush = () => {
    // This method is used after the bridge is marked as destroyed in shutdown sequence,
    // so we do not bail out if the bridge marked as destroyed.
    // It is a private method that the bridge ensures is only called at the right times.

    clearTimeout(this._timeoutID);
    this._timeoutID = null;

    if (this._messageQueue.length) {
      for (let i = 0; i < this._messageQueue.length; i += 3) {
        this._wall.send(
          this._messageQueue[i],
          this._messageQueue[i + 1],
          this._messageQueue[i + 2]
        );
      }
      this._messageQueue.length = 0;

      // Check again for queued messages in BATCH_DURATION ms. This will keep
      // flushing in a loop as long as messages continue to be added. Once no
      // more are, the timer expires.
      this._timeoutID = setTimeout(this._flush, BATCH_DURATION);
    }
  };
}

export type BackendBridge = Bridge<BackendEvents, FrontendEvents>;
export type FrontendBridge = Bridge<FrontendEvents, BackendEvents>;

export default Bridge;


================================================
FILE: src/devtools/DevTools.js
================================================
/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 */

// Reach styles need to come before any component styles.
// This makes overridding the styles simpler.

import React, { useState, useCallback, useEffect } from 'react';
import type { FrontendBridge } from 'src/bridge';
import Store from './store';
import { BridgeContext, StoreContext } from './context';
import NetworkDisplayer from './view/NetworkDisplayer';
import StoreTimeline from './view/StoreTimeline';

// export type TabID = 'network' | 'settings' | 'store-inspector';
export type ViewElementSource = (id: number) => void;

export type Props = {|
  bridge: FrontendBridge,
  // defaultTab?: TabID,
  // showTabBar?: boolean,
  store: Store,
  viewElementSourceFunction?: ?ViewElementSource,
  viewElementSourceRequiresFileLocation?: boolean,

  // This property is used only by the web extension target.
  // The built-in tab UI is hidden in that case, in favor of the browser's own panel tabs.
  // This is done to save space within the app.
  // Because of this, the extension needs to be able to change which tab is active/rendered.
  // overrideTab?: TabID,

  // TODO: Cleanup multi-tabs in webextensions
  // To avoid potential multi-root trickiness, the web extension uses portals to render tabs.
  // The root <DevTools> app is rendered in the top-level extension window,
  // but individual tabs (e.g. Components, Profiling) can be rendered into portals within their browser panels.
  rootContainer?: Element,
  // networkPortalContainer?: Element,
  settingsPortalContainer?: Element,
  storeInspectorPortalContainer?: Element
|};

const networkTab = {
  id: ('network': TabID),
  icon: 'network',
  label: 'Network',
  title: 'Relay Network'
};
const storeInspectorTab = {
  id: ('store-inspector': TabID),
  icon: 'store-inspector',
  label: 'Store',
  title: 'Relay Store'
};

const tabs = [networkTab, storeInspectorTab];

export default function DevTools({
  bridge,
  rootContainer,
  networkPortalContainer,
  storeInspectorPortalContainer,
  settingsPortalContainer,
  store,
  viewElementSourceFunction,
  viewElementSourceRequiresFileLocation = false
}: Props) {
  const [environmentIDs, setEnvironmentIDs] = useState(store.getEnvironmentIDs());
  const [currentEnvID, setCurrentEnvID] = useState(environmentIDs[0]);
  const [selector, setSelector] = useState('Store');

  const setEnv = useCallback(() => {
    const ids = store.getEnvironmentIDs();
    if (currentEnvID === undefined) {
      const firstKey = ids[0];
      setCurrentEnvID(firstKey);
    }
    setEnvironmentIDs(ids);
  }, [store, currentEnvID]);

  useEffect(() => {
    setEnv();
    store.addListener('environmentInitialized', setEnv);
    return () => {
      store.removeListener('environmentInitialized', setEnv);
    };
  }, [store, setEnv]);

  function handleTabClick(e, tab) {
    setSelector(tab);
  }

  const handleChange = useCallback(e => {
    setCurrentEnvID(parseInt(e.target.value));
  }, []);

  console.log('currentenvid before render', currentEnvID);

  return (
    <BridgeContext.Provider value={bridge}>
      <StoreContext.Provider value={store}>
        <div className="navigation">
          <form className="env-select select is-small is-pulled-left">
            <select className="env-select" onChange={handleChange}>
              {environmentIDs.map(id => {
                return (
                  <option key={id} value={id}>
                    {store.getEnvironmentName(id) || id}
                  </option>
                );
              })}
            </select>
          </form>
          <div className="tabs is-toggle is-small is-pulled-left">
            <ul>
              <li className={selector === 'Store' && 'is-active'}>
                <a id="storeSelector" onClick={e => handleTabClick(e, 'Store')}>
                  <span className="icon is-small">
                    <i className="fas fa-database"></i>
                  </span>
                  <span>Store</span>
                </a>
              </li>
              <li className={selector === 'Network' && 'is-active'}>
                <a
                  id="networkSelector"
                  onClick={e => {
                    handleTabClick(e, 'Network');
                  }}
                >
                  <span className="icon is-small">
                    <i className="fas fa-network-wired"></i>
                  </span>
                  <span>Network</span>
                </a>
              </li>
            </ul>
          </div>
          <div className="logo is-pulled-right">
            <a href="https://github.com/oslabs-beta/protostar-relay" target="_blank">
              <img src="../../assets/protorelay.png"></img>
            </a>
          </div>
        </div>
        <div className={selector === 'Store' ? 'columns mb-0 is-multiline is-mobile' : 'is-hidden'}>
          {currentEnvID && (
            <StoreTimeline
              currentEnvID={currentEnvID}
              portalContainer={storeInspectorPortalContainer}
            />
          )}
        </div>
        <div className={selector === 'Network' ? 'columns mb-0 is-mobile' : 'is-hidden'}>
          {currentEnvID && <NetworkDisplayer currentEnvID={currentEnvID} />}
        </div>
      </StoreContext.Provider>
    </BridgeContext.Provider>
  );
}


================================================
FILE: src/devtools/context.js
================================================
/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 */

import { createContext } from 'react';

import type { FrontendBridge } from 'src/bridge';
import type Store from 'store';

export const BridgeContext = createContext<FrontendBridge>(((null: any): FrontendBridge));
BridgeContext.displayName = 'BridgeContext';

export const StoreContext = createContext<Store>(((null: any): Store));
StoreContext.displayName = 'StoreContext';


================================================
FILE: src/devtools/store.js
================================================
/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 */
const __DEBUG__ = true;

import EventEmitter from 'events';
import type { FrontendBridge } from 'src/bridge';
import type {
  DataID,
  LogEvent,
  EventData,
  EnvironmentInfo,
  StoreData,
  StoreRecords,
  Record
} from '../types';

const debug = (methodName, ...args) => {
  if (__DEBUG__) {
    console.log(
      `%cStore %c${methodName}`,
      'color: green; font-weight: bold;',
      'font-weight: bold;',
      ...args
    );
  }
};

const storeEventNames = [
  'queryresource.fetch',
  'store.publish',
  'store.restore',
  'store.gc',
  'store.snapshot',
  'store.notify.complete',
  'store.notify.start'
];

/**
 * The store is the single source of truth for updates from the backend.
 * ContextProviders can subscribe to the Store for specific things they want to provide.
 */
export default class Store extends EventEmitter<{|
  collapseNodesByDefault: [],
  componentFilters: [],
  environmentInitialized: [],
  mutated: [],
  storeDataReceived: [],
  allEventsReceived: [],
  recordChangeDescriptions: [],
  roots: []
|}> {
  _bridge: FrontendBridge;

  _environmentEventsMap: Map<number, Array<LogEvent>> = new Map();
  _environmentNames: Map<number, string> = new Map();
  _environmentStoreData: Map<number, StoreRecords> = new Map();
  _environmentStoreOptimisticData: Map<number, StoreRecords> = new Map();
  _environmentAllEvents: Map<number, Array<LogEvent>> = new Map();
  _recordedRequests: Map<number, Map<number, LogEvent>> = new Map();
  _isRecording: boolean = false;
  _importEnvID: ?number = null;

  constructor(bridge: FrontendBridge) {
    super();
    this._bridge = bridge;
    bridge.addListener('events', this.onBridgeEvents);
    bridge.addListener('shutdown', this.onBridgeShutdown);
    bridge.addListener('environmentInitialized', this.onBridgeEnvironmentInit);
    bridge.addListener('storeRecords', this.onBridgeStoreSnapshot);
    bridge.addListener('mutationComplete', this.setEnvironmentEvents);
    bridge.addListener('all', this.setEnvironmentEvents);
  }

  getAllEventsArray(): $ReadOnlyArray<LogEvent> {
    const allEvents = [];
    this._environmentAllEvents.forEach((value, _) => allEvents.push(...value));
    return allEvents;
  }

  setAllEventsMap(environmentID: number, events: Array<LogEvent>) {
    this._environmentAllEvents.set(environmentID, events);
    this.emit('allEventsReceived');
  }

  getAllEventsMap(): Map<number, Array<LogEvent>> {
    return this._environmentAllEvents;
  }

  getEvents(environmentID: number): ?$ReadOnlyArray<LogEvent> {
    return this._environmentAllEvents.get(environmentID);
  }

  getAllEnvironmentEvents(): $ReadOnlyArray<LogEvent> {
    const allEnvironmentEvents = [];
    this._environmentEventsMap.forEach((value, _) => allEnvironmentEvents.push(...value));
    return allEnvironmentEvents;
  }

  getEnvironmentEvents(environmentID: number): ?$ReadOnlyArray<LogEvent> {
    return this._environmentEventsMap.get(environmentID);
  }

  getEnvironmentIDs(): $ReadOnlyArray<number> {
    return Array.from(this._environmentNames.keys());
  }

  getImportEnvID(): ?number {
    return this._importEnvID;
  }

  setImportEnvID(envID: ?number) {
    this._importEnvID = envID;
    this.emit('allEventsReceived');
  }

  getEnvironmentName(environmentID: number): ?string {
    return this._environmentNames.get(environmentID);
  }

  getRecords(environmentID: number): ?StoreRecords {
    return this._environmentStoreData.get(environmentID);
  }

  getRecordIDs(environmentID: number): ?$ReadOnlyArray<string> {
    const storeRecords = this._environmentStoreData.get(environmentID);
    return storeRecords ? Object.keys(storeRecords) : null;
  }

  removeRecord(environmentID: number, recordID: string) {
    const storeRecords = this._environmentStoreData.get(environmentID);
    if (storeRecords != null) {
      delete storeRecords[recordID];
    }
  }

  getAllRecords(): ?$ReadOnlyArray<StoreRecords> {
    return Array.from(this._environmentStoreData.values());
  }

  getOptimisticUpdates(environmentID: number): ?StoreRecords {
    return this._environmentStoreOptimisticData.get(environmentID);
  }

  mergeRecords(id: number, newRecords: ?StoreRecords) {
    if (newRecords == null) {
      return;
    }
    const oldRecords = this._environmentStoreData.get(id);
    if (oldRecords == null) {
      this._environmentStoreData.set(id, newRecords);
      return;
    }
    const dataIDs = Object.keys(newRecords);

    for (let ii = 0; ii < dataIDs.length; ii++) {
      const dataID = dataIDs[ii];
      const oldRecord = oldRecords[dataID];
      const newRecord = newRecords[dataID];
      if (oldRecord && newRecord) {
        let updated: Record | null = null;
        const keys = Object.keys(newRecord);
        for (let iii = 0; iii < keys.length; iii++) {
          const key = keys[iii];
          if (updated || oldRecord[key] !== newRecord[key]) {
            updated = updated !== null ? updated : { ...oldRecord };
            updated[key] = newRecord[key];
          }
        }
        updated = updated !== null ? updated : oldRecord;
        if (updated !== newRecord) {
          oldRecords[dataID] = updated;
        }
      } else if (oldRecord == null) {
        oldRecords[dataID] = newRecord;
      } else if (newRecord == null) {
        delete oldRecords[dataID];
      }
    }
    this._environmentStoreData.set(id, oldRecords);
  }

  mergeOptimisticRecords(id: number, newRecords: ?StoreRecords) {
    if (newRecords == null) {
      return;
    }
    const oldRecords = this._environmentStoreOptimisticData.get(id);
    if (oldRecords == null) {
      this._environmentStoreOptimisticData.set(id, newRecords);
      return;
    }
    const dataIDs = Object.keys(newRecords);

    for (let ii = 0; ii < dataIDs.length; ii++) {
      const dataID = dataIDs[ii];
      const oldRecord = oldRecords[dataID];
      const newRecord = newRecords[dataID];
      if (oldRecord && newRecord) {
        let updated: Record | null = null;
        const keys = Object.keys(newRecord);
        for (let iii = 0; iii < keys.length; iii++) {
          const key = keys[iii];
          if (updated || oldRecord[key] !== newRecord[key]) {
            updated = updated !== null ? updated : { ...oldRecord };
            updated[key] = newRecord[key];
          }
        }
        updated = updated !== null ? updated : oldRecord;
        if (updated !== newRecord) {
          oldRecords[dataID] = updated;
        }
      } else if (oldRecord == null) {
        oldRecords[dataID] = newRecord;
      } else if (newRecord == null) {
        delete oldRecords[dataID];
      }
    }
    this._environmentStoreOptimisticData.set(id, oldRecords);
  }

  onBridgeStoreSnapshot = (data: Array<StoreData>) => {
    for (const { id, records } of data) {
      this._environmentStoreData.set(id, records);
      this.emit('storeDataReceived');
    }
  };

  setStoreEvents = (id: number, data: LogEvent) => {
    switch (data.name) {
      case 'store.publish':
        this.mergeRecords(id, data.source);
        if (data.optimistic) {
          this.mergeOptimisticRecords(id, data.source);
        }
        break;
      case 'store.restore':
        this.clearOptimisticUpdates(id);
        break;
      case 'store.gc':
        this.garbageCollectRecords(id, data.references);
        break;
      default:
        break;
    }
    this.emit('storeDataReceived');
  };

  setEnvironmentEvents = (id: number, data: LogEvent) => {
    const arr = this._environmentEventsMap.get(id);
    if (arr) {
      arr.push(data);
    } else {
      this._environmentEventsMap.set(id, [data]);
    }
    this.emit('mutated');
    if (data.name === 'execute.complete') {
      this.emit('mutationComplete');
    }
  };

  appendInformationToRequest = (id: number, data: LogEvent) => {
    switch (data.name) {
      case 'execute.start':
        const requestArr = this._recordedRequests.get(id);
        if (requestArr) {
          requestArr.set(data.transactionID, data);
        } else {
          const newRequest = new Map<number, LogEvent>();
          newRequest.set(data.transactionID, data);
          this._recordedRequests.set(id, newRequest);
        }
        break;
      case 'execute.next':
      case 'execute.info':
      case 'execute.complete':
      case 'execute.error':
      case 'execute.unsubscribe':
        const requests = this._recordedRequests.get(id);
        if (requests) {
          const request = requests.get(data.transactionID);
          if (request && request.name === 'execute.start') {
            data.params = request.params;
            data.variables = request.variables;
          }
        }
        break;
      default:
        break;
    }
  };

  startRecording = () => {
    this._isRecording = true;
    this.clearAllEvents();
  };

  stopRecording = () => {
    this._isRecording = false;
  };

  onBridgeEvents = (events: Array<EventData>) => {
    for (const { id, data, eventType } of events) {
      if (this._isRecording) {
        const allEvents = this._environmentAllEvents.get(id);
        if (allEvents) {
          if (data.name === 'store.gc') {
            const records = this.getRecords(id);
            if (records != null) {
              data.gcRecords = {};
              data.references = Object.keys(records)
                .filter(recID => recID != null && !data.references.includes(recID))
                .map(recID => {
                  data.gcRecords[recID] = records[recID];
                  return recID;
                });
            }
          } else if (data.name === 'store.notify.complete') {
            const records = this.getRecords(id);
            if (records != null) {
              data.invalidatedRecords = {};
              data.updatedRecords = {};
              Object.keys(data.updatedRecordIDs).forEach(recID => {
                data.updatedRecords[recID] = { ...records[recID] };
              });
              data.invalidatedRecordIDs.forEach(
                recID => (data.invalidatedRecords[recID] = { ...records[recID] })
              );
            }
          } else if (data.name.startsWith('execute')) {
            this.appendInformationToRequest(id, data);
          }
          allEvents.push(data);
        } else {
          this._environmentAllEvents.set(id, [data]);
        }
        this.emit('allEventsReceived');
      }
      if (eventType === 'store') {
        this.setStoreEvents(id, data);
      } else if (eventType === 'environment') {
        this.setEnvironmentEvents(id, data);
      }
    }
  };

  onBridgeEnvironmentInit = (data: Array<EnvironmentInfo>) => {
    for (const { id, environmentName } of data) {
      this._environmentNames.set(id, environmentName);
    }
    this.emit('environmentInitialized');
  };

  clearOptimisticUpdates = (envID: number) => {
    this._environmentStoreOptimisticData.delete(envID);
  };

  garbageCollectRecords = (envID: number, references: $ReadOnlyArray<DataID>) => {
    if (references.length === 0) {
      this._environmentStoreData.delete(envID);
    } else {
      const storeIDs = this.getRecordIDs(envID);
      if (storeIDs == null) {
        return;
      }
      for (const dataID of storeIDs) {
        if (!references.includes(dataID)) {
          this.removeRecord(envID, dataID);
        }
      }
    }
  };

  clearAllEvents = () => {
    this._environmentAllEvents.forEach((_, key) => this.clearEvents(key));
    this.emit('allEventsReceived');
  };

  clearEvents = (environmentID: number) => {
    this._environmentAllEvents.delete(environmentID);
  };

  clearAllNetworkEvents = () => {
    this._environmentEventsMap.forEach((_, key) => this.clearNetworkEvents(key));
    this.emit('mutated');
  };

  clearNetworkEvents = (environmentID: number) => {
    const completed = new Set();
    let networkEventArray = this._environmentEventsMap.get(environmentID);
    if (networkEventArray !== undefined && networkEventArray.length > 0) {
      for (const event of networkEventArray) {
        if (
          event.name === 'execute.complete' ||
          event.name === 'execute.error' ||
          event.name === 'execute.unsubscribe'
        ) {
          completed.add(event.transactionID);
        }
      }
      networkEventArray = networkEventArray.filter(
        event =>
          storeEventNames.includes(event.name) &&
          event.transactionID != null &&
          !completed.has(event.transactionID)
      );
      this._environmentEventsMap.set(environmentID, networkEventArray);
      this.emit('mutated');
    }
  };

  onBridgeShutdown = () => {
    if (__DEBUG__) {
      debug('onBridgeShutdown', 'unsubscribing from Bridge');
    }

    this._bridge.removeListener('events', this.onBridgeEvents);
    this._bridge.removeListener('shutdown', this.onBridgeShutdown);
    this._bridge.removeListener('environmentInitialized', this.onBridgeEnvironmentInit);
    this._bridge.removeListener('storeRecords', this.onBridgeStoreSnapshot);
  };
}


================================================
FILE: src/devtools/utils.js
================================================
/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 */

export function deepCopyFunction(inObject: any) {
  if (typeof inObject !== 'object' || inObject === null) {
    return inObject;
  }

  if (Array.isArray(inObject)) {
    const outObject = [];
    for (let i = 0; i < inObject.length; i++) {
      const value = inObject[i];
      outObject[i] = deepCopyFunction(value);
    }
    return outObject;
  } else if (inObject instanceof Map) {
    const outObject = new Map<mixed, mixed>();
    inObject.forEach((val, key) => {
      outObject.set(key, deepCopyFunction(val));
    });
    return outObject;
  } else {
    const outObject = {};
    for (const key in inObject) {
      const value = inObject[key];
      if (typeof key === 'string' && key != null) {
        outObject[key] = deepCopyFunction(value);
      }
    }
    return outObject;
  }
}

export function debounce(func, wait) {
  let timeout = null;
  return function() {
    const newfunc = () => {
      timeout = null;
      func.apply(this, arguments);
    };
    clearTimeout(timeout);
    timeout = setTimeout(newfunc, wait);
  };
}


================================================
FILE: src/devtools/view/Components/EnvironmentSelector.js
================================================
import React, { useState, useCallback, useEffect } from 'react';

function EnvironmentSelector(props) {
  const [selectEnv, setSelectEnv] = useState('');

  return (
    <form className="env-select">
      <select name="environment">{dropdownEnv}</select>
    </form>
  );
}

export default EnvironmentSelector;


================================================
FILE: src/devtools/view/Components/Record.js
================================================
import React from 'react';

function Record(props) {
  /* Maps through array and recursively calls component if the props[value] is an object, 
  otherwise, it will store a key/value pair */
  const records = Object.keys(props).map(key => {
    return typeof props[key] === 'object' ? (
      <div className="nestedObject" key={key}>
        <span className="key">{key}: </span>
        <Record {...props[key]} />
      </div>
    ) : (
      <div className="objectProperty" key={key}>
        <span className="key">{key}: </span>
        <span className="value">{JSON.stringify(props[key])}</span>
      </div>
    );
  });

  return <div className="records">{records}</div>;
}

export default Record;


================================================
FILE: src/devtools/view/Components/SnapshotLinks.js
================================================
import React, { useState } from 'react';

const SnapshotLinks = ({ timeline, currentEnvID, handleSnapshot }) => {
  const [active, setActive] = useState(null);
  // Create links with date and label of snapshot; rendered in the left snapshot column using Bulma menu-list. Active state is used to toggle active link.
  return (
    <div>
      <aside className="menu">
        <ul className="menu-list">
          {timeline[currentEnvID].map((item, i) => (
            <li
              key={i}
              onClick={() => {
                handleSnapshot(i);
                setActive(i);
              }}
            >
              <a href="#" key={i} className={active === i && 'is-active'}>
                {item.date.toLocaleTimeString()}: {item.label}
              </a>
            </li>
          ))}
        </ul>
      </aside>
    </div>
  );
};

export default SnapshotLinks;


================================================
FILE: src/devtools/view/NetworkDisplayer.js
================================================
import React, { useState, useEffect, useContext } from 'react';
import { StoreContext } from '../context';
import Record from './Components/Record';
import { execute } from 'graphql';
import { debounce } from '../utils';

//iterates over each event and joins events based on transactionID and sorts by type
const combineEvents = events => {
  const combinedEvents = {};
  const eventTypes = {};
  //join events by transactionID
  events.forEach(event => {
    const tempObj = {};
    if (event.name === 'execute.start') {
      tempObj.request = event.params;
      tempObj.variables = event.variables;
    } else if (event.name === 'execute.info') {
      tempObj.info = event.info;
    } else if (event.name === 'execute.next') {
      tempObj.response = event.response;
    } else if (event.name === 'execute.complete') {
      // tempObj.complete = true
    }
    combinedEvents[event.transactionID]
      ? (combinedEvents[event.transactionID] = Object.assign(
          combinedEvents[event.transactionID],
          tempObj
        ))
      : (combinedEvents[event.transactionID] = tempObj);
  });

  //sort by type
  Object.keys(combinedEvents).forEach(transactionID => {
    const op = combinedEvents[transactionID].request.operationKind;
    eventTypes[op]
      ? (eventTypes[op] = Object.assign(eventTypes[op], {
          [transactionID]: combinedEvents[transactionID]
        }))
      : (eventTypes[op] = { [transactionID]: combinedEvents[transactionID] });
  });

  return eventTypes;
};

//generates a list of elements for the menu and the events listing
const generateElementList = (events, searchResults, selection, handleMenuClick) => {
  const eventMenu = [];
  const eventsList = [];

  //for each event - add to menu list
  for (let type in events) {
    //creates an array of menu items for all events belonging to a given type
    const typeList = [];
    for (let id in events[type]) {
      //filter out results based on search input
      if (new RegExp(searchResults, 'i').test(JSON.stringify(events[type][id]))) {
        typeList.push(
          <li>
            <a
              id={id}
              className={selection === id && 'is-active'}
              onClick={e => {
                handleMenuClick(e, id);
              }}
            >
              {events[type][id].request.name}
            </a>
          </li>
        );
        //creates an array of elements for all events
        eventsList.push(
          <div
            id={id}
            className={`${
              selection !== id && selection !== type && selection !== ''
                ? 'is-hidden'
                : 'record-line'
            }`}
          >
            <Record {...events[type][id]} />
          </div>
        );
      }
    }

    //pushes the new type element with child events to the typeList component array
    eventMenu.push(
      <li>
        <a
          id={type}
          className={selection === type && 'is-active'}
          onClick={e => {
            handleMenuClick(e, type);
          }}
        >
          {type}
        </a>
        <ul>{typeList}</ul>
      </li>
    );
  }
  return { eventMenu, eventsList };
};

const NetworkDisplayer = ({ currentEnvID }) => {
  const [selection, setSelection] = useState('');
  const [events, setEvents] = useState([]);
  const [searchResults, setSearchResults] = useState('');
  const store = useContext(StoreContext);

  useEffect(() => {
    //on mutation all store events are pulled and processed with events state updated
    const onMutated = () => {
      setEvents(combineEvents(store._environmentEventsMap.get(currentEnvID) || []));
    };
    store.addListener('mutated', onMutated);

    return () => {
      store.removeListener('mutated', onMutated);
    };
  }, [store]);

  //handle type menu click events
  function handleMenuClick(e, id) {
    //set new selection
    setSelection(id);
  }

  //shows you the entire network
  function handleReset(e) {
    //remove selection;
    setSelection('');
  }

  //updates search results
  const debounced = debounce(val => setSearchResults(val), 300);
  function handleSearch(e) {
    //debounce search
    debounced(e.target.value);
  }

  //generate menu list and events list
  const { eventMenu, eventsList } = generateElementList(
    events,
    searchResults,
    selection,
    handleMenuClick
  );

  return (
    <React.Fragment>
      <div className="column is-one-third scrollable">
        <p class="control has-icons-left is-flex ml-2">
          <input
            className="input is-small is-primary mt-2"
            type="text"
            placeholder="Search"
            onChange={e => {
              handleSearch(e);
            }}
          ></input>
          <button
            className="button is-small is-link my-2"
            onClick={e => {
              handleReset(e);
            }}
          >
            Reset
          </button>
          <span class="icon is-left mt-2">
            <i class="fas fa-search"></i>
          </span>
        </p>
        <aside className="menu">
          <p className="menu-label ml-2">Event List</p>
          <ul className="menu-list">{eventMenu}</ul>
        </aside>
      </div>
      <div className="column scrollable">
        <div className="display-box">{eventsList}</div>
      </div>
    </React.Fragment>
  );
};

export default NetworkDisplayer;


================================================
FILE: src/devtools/view/StoreDisplayer.js
================================================
import React, { useState } from 'react';
import Record from './Components/Record';
import { debounce } from '../utils';

//update record list to current selection
function updateRecords(store, selection) {
  if (store) {
    if (selection === '') {
      return store;
      //id selected - filter out everything except selected id
    } else if (selection[0] === 'i') {
      const id = selection.slice(3);
      return Object.keys(store).reduce((newRL, key) => {
        if (store[key].__id === id) newRL[key] = store[key];
        return newRL;
      }, {});
      //type selected - filter out everything except selected type
    } else {
      const type = selection.slice(5);
      return Object.keys(store).reduce((newRL, key) => {
        if (store[key].__typename === type) newRL[key] = store[key];
        return newRL;
      }, {});
    }
  }
}

//generate list of menu elements
function generateComponentsList(store, searchResults, recordsList, selection, handleMenuClick) {
  //create menu list of all types
  const menuList = {};
  const typeList = [];

  for (let id in store) {
    const record = store[id];
    menuList[record.__typename]
      ? menuList[record.__typename].push(record.__id)
      : (menuList[record.__typename] = [record.__id]);
  }
  //loop through each type and generate menu item

  for (let type in menuList) {
    //creates an array of elements for all ids belonging to a given type

    const idList = menuList[type]
      .filter(id => new RegExp(searchResults, 'i').test(JSON.stringify(recordsList[id])))
      .map(id => {
        return (
          <li key={id}>
            <a
              id={'id-' + id}
              className={selection === 'id-' + id && 'is-active'}
              onClick={() => {
                handleMenuClick('id-' + id);
              }}
            >
              {id}
            </a>
          </li>
        );
      });
    //pushes the new type element with child ids to the typeList component array
    if (idList.length !== 0) {
      typeList.push(
        <li key={type}>
          <a
            id={'type-' + type}
            className={selection === 'type-' + type && 'is-active'}
            onClick={() => {
              handleMenuClick('type-' + type);
            }}
          >
            {type}
          </a>
          <ul>{idList}</ul>
        </li>
      );
    }
  }
  return typeList;
}

const StoreDisplayer = ({ store }) => {
  const [recordsList, setRecordsList] = useState({});
  const [selection, setSelection] = useState('');
  const [searchResults, setSearchResults] = useState('');

  React.useEffect(() => {
    //initialize store
    setRecordsList(store);
  }, [store]);

  //handle menu click events
  function handleMenuClick(selection) {
    //set new selection
    setSelection(selection);
    //update display with current selection
    setRecordsList(updateRecords(store, selection));
  }

  //shows you the entire store
  function handleReset(e) {
    //remove selection
    setSelection('');
    //reset back to original store
    setRecordsList(store);
  }

  //updates search results
  const debounced = debounce(val => {
    setSelection('');
    setRecordsList(store);
    setSearchResults(val);
  }, 300);
  function handleSearch(e) {
    //debounce search
    debounced(e.target.value);
  }
  //generates the menu element list

  //verify recordsList is not undefined and then generate list of components
  const typeList =
    recordsList === undefined
      ? []
      : generateComponentsList(store, searchResults, recordsList, selection, handleMenuClick);

  return (
    <React.Fragment>
      <div className="column is-half-mobile scrollable">
        <p class="control has-icons-left is-flex">
          <input
            className="input is-small is-primary"
            type="text"
            placeholder="Search"
            onChange={e => {
              handleSearch(e);
            }}
          ></input>
          <button
            className="button is-small is-link"
            onClick={e => {
              handleReset(e);
            }}
          >
            Reset
          </button>
          <span class="icon is-left">
            <i class="fas fa-search"></i>
          </span>
        </p>
        <aside className="menu">
          <p className="menu-label mt-1">Record List</p>
          <ul className="menu-list">{typeList}</ul>
        </aside>
      </div>
      <div className="column is-half-mobile scrollable">
        <div className="display-box">
          <Record {...recordsList} />
        </div>
      </div>
    </React.Fragment>
  );
};

export default StoreDisplayer;


================================================
FILE: src/devtools/view/StoreTimeline.js
================================================
import React, { useState, useContext, useEffect } from 'react';
import InputRange from 'react-input-range';
import { BridgeContext, StoreContext } from '../context';
import StoreDisplayer from './StoreDisplayer';
import SnapshotLinks from './Components/SnapshotLinks';

const StoreTimeline = ({ currentEnvID }) => {
  const store = useContext(StoreContext);
  const bridge = useContext(BridgeContext);
  const [snapshotIndex, setSnapshotIndex] = useState(0);
  const [timelineLabel, setTimelineLabel] = useState('');
  const [liveStore, setLiveStore] = useState({});
  // Each envId has an array of orbject built up for loading snapshots via the handleClick
  const [timeline, setTimeline] = useState({
    [currentEnvID]: [
      {
        label: 'at startup',
        date: new Date(),
        storage: liveStore
      }
    ]
  });

  // build snapshot object and insert into timeline
  const handleClick = e => {
    e.preventDefault();
    const timelineInsert = {};
    const timeStamp = new Date();
    timelineInsert.label = timelineLabel;
    timelineInsert.date = timeStamp;
    timelineInsert.storage = liveStore;
    const newTimeline = timeline[currentEnvID].concat([timelineInsert]);
    setTimeline({ ...timeline, [currentEnvID]: newTimeline });
    setTimelineLabel('');
    setSnapshotIndex(newTimeline.length);
  };

  const handleSnapshot = index => {
    setSnapshotIndex(index);
  };

  const updateStoreHelper = storeObj => {
    setLiveStore(storeObj);
  };

  // triggering refresh of store on completed mutation
  React.useEffect(() => {
    const refreshLiveStore = () => {
      bridge.send('refreshStore', currentEnvID);
    };
    const refreshEvents = () => {
      const allRecords = store.getRecords(currentEnvID);
      updateStoreHelper(allRecords);
    };

    store.addListener('storeDataReceived', refreshEvents);
    store.addListener('allEventsReceived', refreshEvents);
    store.addListener('mutationComplete', refreshLiveStore);

    return () => {
      store.removeListener('mutationComplete', refreshLiveStore);
      store.removeListener('storeDataReceived', refreshEvents);
      store.removeListener('allEventsReceived', refreshEvents);
    };
  }, [store]);

  React.useEffect(() => {
    const allRecords = store.getRecords(currentEnvID);
    setLiveStore(allRecords);

    if (!timeline[currentEnvID]) {
      const newTimeline = {
        ...timeline,
        [currentEnvID]: [
          {
            label: 'current',
            date: new Date(),
            storage: allRecords
          }
        ]
      };
      setTimeline(newTimeline);
      setSnapshotIndex(1);
    } else {
      setSnapshotIndex(timeline[currentEnvID].length);
    }
  }, [currentEnvID]);

  console.log(
    'showing livestore',
    !timeline[currentEnvID] ||
      !timeline[currentEnvID][snapshotIndex] ||
      snapshotIndex === timeline[currentEnvID].length
  );

  return (
    <React.Fragment>
      <div className="column is-full-mobile is-one-quarter-desktop">
        <div className="display-box">
          <div className="snapshot-wrapper is-flex ml-2">
            <input
              type="text"
              className="input is-small snapshot-btn is-primary"
              value={timelineLabel}
              onChange={e => setTimelineLabel(e.target.value)}
              placeholder="take a store snapshot"
            ></input>
            <button className="button is-small is-link" onClick={e => handleClick(e)}>
              Snapshot
            </button>
          </div>
        </div>
        <div className="snapshots">
          <div
            className="timeline-nav column is-full-desktop is-flex-mobile"
            id="timeline-mini-col"
          >
            <InputRange
              maxValue={timeline[currentEnvID] ? timeline[currentEnvID].length : 0}
              minValue={0}
              value={snapshotIndex}
              onChange={value => setSnapshotIndex(value)}
            />
            <div className="snapshot-nav has-text-centered has-text-right-mobile">
              <button
                class="button is-small is-info is-light"
                onClick={() => {
                  if (snapshotIndex !== 0) setSnapshotIndex(snapshotIndex - 1);
                }}
              >
                <span className="icon is-medium">
                  <i className="fas fa-fast-backward"></i>
                </span>
              </button>
              <button
                class="button is-small is-info is-light"
                onClick={() => setSnapshotIndex(timeline[currentEnvID].length)}
              >
                Current
              </button>
              <button
                class="button is-small is-info is-light"
                onClick={() => {
                  if (snapshotIndex !== timeline[currentEnvID].length)
                    setSnapshotIndex(snapshotIndex + 1);
                }}
              >
                <span className="icon is-medium">
                  <i className="fas fa-fast-forward"></i>
                </span>
              </button>
            </div>
          </div>
          <div
            className="snapshot-info is-size-7 column is-full-desktop pt-0"
            id="snapshot-info-col"
          >
            {timeline[currentEnvID] && (
              <SnapshotLinks
                currentEnvID={currentEnvID}
                handleSnapshot={handleSnapshot}
                timeline={timeline}
              />
            )}
          </div>
        </div>
      </div>
      <StoreDisplayer
        store={
          !timeline[currentEnvID] ||
          !timeline[currentEnvID][snapshotIndex] ||
          snapshotIndex === timeline[currentEnvID].length
            ? liveStore
            : timeline[currentEnvID][snapshotIndex].storage
        }
      />
    </React.Fragment>
  );
};

export default StoreTimeline;


================================================
FILE: src/hook.js
================================================
/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 */

/**
 * Install the hook on window, which is an event emitter.
 * Note because Chrome content scripts cannot directly modify the window object,
 * we are evaling this function by inserting a script tag.
 * That's why we have to inline the whole event emitter implementation here.
 */

import type { DevToolsHook } from 'src/backend/types';

declare var window: any;

export function installHook(target: any): DevToolsHook | null {
  if (target.hasOwnProperty('__RELAY_DEVTOOLS_HOOK__')) {
    return null;
  }
  const listeners = {};
  const environments = new Map();

  let uidCounter = 0;

  function registerEnvironment(environment) {
    const id = ++uidCounter;
    environments.set(id, environment);

    hook.emit('environment', { id, environment });

    return id;
  }

  function sub(event, fn) {
    hook.on(event, fn);
    return () => hook.off(event, fn);
  }

  function on(event, fn) {
    if (!listeners[event]) {
      listeners[event] = [];
    }
    listeners[event].push(fn);
  }

  function off(event, fn) {
    if (!listeners[event]) {
      return;
    }
    const index = listeners[event].indexOf(fn);
    if (index !== -1) {
      listeners[event].splice(index, 1);
    }
    if (!listeners[event].length) {
      delete listeners[event];
    }
  }

  function emit(event, data) {
    if (listeners[event]) {
      listeners[event].map(fn => fn(data));
    }
  }

  const environmentWrappers = new Map();

  const hook: DevToolsHook = {
    registerEnvironment,
    environmentWrappers,
    // listeners,
    environments,

    emit,
    // inject,
    on,
    off,
    sub
  };

  Object.defineProperty(
    target,
    '__RELAY_DEVTOOLS_HOOK__',
    ({
      // This property needs to be configurable for the test environment,
      // else we won't be able to delete and recreate it beween tests.
      configurable: __DEV__,
      enumerable: false,
      get() {
        return hook;
      }
    }: Object)
  );

  return hook;
}


================================================
FILE: src/types.js
================================================
/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 */

export type Wall = {|
  // `listen` returns the "unlisten" function.
  listen: (fn: Function) => Function,
  send: (event: string, payload: any, transferable?: Array<any>) => void
|};

export type Record = { [key: string]: mixed, ... };
export type DataID = string;
export type UpdatedRecords = { [dataID: DataID]: boolean, ... };

export type StoreRecords = { [DataID]: ?Record, ... };

// Copied from relay
export type LogEvent =
  | {|
      +name: 'queryresource.fetch',
      +operation: $FlowFixMe,
      // FetchPolicy from relay-experimental
      +fetchPolicy: string,
      // RenderPolicy from relay-experimental
      +renderPolicy: string,
      +hasFullQuery: boolean,
      +shouldFetch: boolean
    |}
  | {|
      +name: 'store.publish',
      +source: any,
      +optimistic: boolean
    |}
  | {|
      +name: 'store.gc',
      references: Array<DataID>,
      gcRecords: StoreRecords
    |}
  | {|
      +name: 'store.restore'
    |}
  | {|
      +name: 'store.snapshot'
    |}
  | {|
      +name: 'store.notify.start'
    |}
  | {|
      +name: 'store.notify.complete',
      +updatedRecordIDs: UpdatedRecords,
      +invalidatedRecordIDs: Array<DataID>,
      updatedRecords: StoreRecords,
      invalidatedRecords: StoreRecords
    |}
  | {|
      +name: 'execute.info',
      +transactionID: number,
      +info: mixed,
      params: $FlowFixMe,
      variables: $FlowFixMe
    |}
  | {|
      +name: 'execute.start',
      +transactionID: number,
      +params: $FlowFixMe,
      +variables: $FlowFixMe
    |}
  | {|
      +name: 'execute.next',
      +transactionID: number,
      +response: $FlowFixMe,
      params: $FlowFixMe,
      variables: $FlowFixMe
    |}
  | {|
      +name: 'execute.error',
      +transactionID: number,
      +error: Error,
      params: $FlowFixMe,
      variables: $FlowFixMe
    |}
  | {|
      +name: 'execute.complete',
      +transactionID: number,
      params: $FlowFixMe,
      variables: $FlowFixMe
    |}
  | {|
      +name: 'execute.unsubscribe',
      +transactionID: number,
      params: $FlowFixMe,
      variables: $FlowFixMe
    |};

export type EventData = {|
  +id: number,
  +data: LogEvent,
  +source: StoreRecords,
  +eventType: string
|};

export type StoreData = {|
  +name: string,
  +id: number,
  +records: StoreRecords
|};

export type EnvironmentInfo = {|
  +id: number,
  +environmentName: string
|};
Download .txt
gitextract_ezpa51h4/

├── .eslintignore
├── .flowconfig
├── .gitignore
├── .gitmodules
├── .travis.yml
├── .yarnrc
├── Dockerfile
├── LICENSE
├── README.md
├── __tests__/
│   ├── DevTools.spec.js
│   ├── Global.spec.js
│   ├── NetworkDisplayer.spec.js
│   ├── Record.spec.js
│   ├── StoreDisplayer.spec.js
│   ├── StoreTimeline.spec.js
│   ├── __mocks__/
│   │   └── styleMock.js
│   ├── __snapshots__/
│   │   ├── DevTools.spec.js.snap
│   │   ├── NetworkDisplayer.spec.js.snap
│   │   ├── StoreDisplayer.spec.js.snap
│   │   └── StoreTimeline.spec.js.snap
│   ├── bridge.spec.js
│   ├── global-setup.js
│   └── store.spec.js
├── babel.config.js
├── docker-compose.yml
├── flow.js
├── package.json
├── shells/
│   ├── browser/
│   │   ├── chrome/
│   │   │   ├── build.js
│   │   │   ├── manifest.json
│   │   │   ├── nottest.js
│   │   │   ├── now.json
│   │   │   └── watch.js
│   │   └── shared/
│   │       ├── build.js
│   │       ├── index.html
│   │       ├── main.html
│   │       ├── src/
│   │       │   ├── backend.js
│   │       │   ├── background.js
│   │       │   ├── contentScript.js
│   │       │   ├── inject.js
│   │       │   ├── injectGlobalHook.js
│   │       │   ├── main.js
│   │       │   └── utils.js
│   │       ├── view/
│   │       │   ├── App.jsx
│   │       │   ├── index.js
│   │       │   └── styles.scss
│   │       ├── webpack.backend.js
│   │       └── webpack.config.js
│   └── utils.js
└── src/
    ├── backend/
    │   ├── EnvironmentWrapper.js
    │   ├── agent.js
    │   ├── index.js
    │   ├── types.js
    │   └── utils.js
    ├── bridge.js
    ├── devtools/
    │   ├── DevTools.js
    │   ├── context.js
    │   ├── store.js
    │   ├── utils.js
    │   └── view/
    │       ├── Components/
    │       │   ├── EnvironmentSelector.js
    │       │   ├── Record.js
    │       │   └── SnapshotLinks.js
    │       ├── NetworkDisplayer.js
    │       ├── StoreDisplayer.js
    │       └── StoreTimeline.js
    ├── hook.js
    └── types.js
Download .txt
SYMBOL INDEX (56 symbols across 23 files)

FILE: babel.config.js
  function validateVersion (line 16) | function validateVersion(version) {

FILE: shells/browser/chrome/nottest.js
  constant EXTENSION_PATH (line 12) | const EXTENSION_PATH = resolve('shells/browser/chrome/build/unpacked');
  constant START_URL (line 13) | const START_URL = 'https://facebook.github.io/react/';

FILE: shells/browser/shared/build.js
  constant STATIC_FILES (line 18) | const STATIC_FILES = ['assets', 'main.html', 'index.html'];

FILE: shells/browser/shared/src/backend.js
  function welcome (line 14) | function welcome(event) {
  function setup (line 26) | function setup(hook) {

FILE: shells/browser/shared/src/background.js
  function isNumeric (line 39) | function isNumeric(str: string): boolean {
  function installContentScript (line 43) | function installContentScript(tabId: number) {
  function doublePipe (line 47) | function doublePipe(one, two) {

FILE: shells/browser/shared/src/contentScript.js
  function sayHelloToBackend (line 15) | function sayHelloToBackend() {
  function handleMessageFromDevtools (line 25) | function handleMessageFromDevtools(message) {
  function handleMessageFromPage (line 35) | function handleMessageFromPage(evt) {
  function handleDisconnect (line 43) | function handleDisconnect() {

FILE: shells/browser/shared/src/injectGlobalHook.js
  function injectCode (line 15) | function injectCode(code) {

FILE: shells/browser/shared/src/main.js
  function createPanelIfReactLoaded (line 20) | function createPanelIfReactLoaded() {
  function checkPageForReact (line 165) | function checkPageForReact() {

FILE: shells/browser/shared/src/utils.js
  function createViewElementSource (line 10) | function createViewElementSource(bridge: Bridge, store: Store) {

FILE: shells/browser/shared/webpack.backend.js
  constant NODE_ENV (line 17) | const NODE_ENV = process.env.NODE_ENV;
  constant GITHUB_URL (line 25) | const GITHUB_URL = getGitHubURL();
  constant DEVTOOLS_VERSION (line 26) | const DEVTOOLS_VERSION = getVersionString();
  constant GITHUB_ISSUES_URL (line 27) | const GITHUB_ISSUES_URL = getGitHubIssuesURL();
  constant DEVTOOLS_FEEDBACK_GROUP (line 28) | const DEVTOOLS_FEEDBACK_GROUP = getInternalDevToolsFeedbackGroup();

FILE: shells/browser/shared/webpack.config.js
  constant NODE_ENV (line 17) | const NODE_ENV = process.env.NODE_ENV;
  constant GITHUB_URL (line 25) | const GITHUB_URL = getGitHubURL();
  constant DEVTOOLS_VERSION (line 26) | const DEVTOOLS_VERSION = getVersionString();
  constant GITHUB_ISSUES_URL (line 27) | const GITHUB_ISSUES_URL = getGitHubIssuesURL();
  constant DEVTOOLS_FEEDBACK_GROUP (line 28) | const DEVTOOLS_FEEDBACK_GROUP = getInternalDevToolsFeedbackGroup();

FILE: shells/utils.js
  function getCommit (line 12) | function getCommit() {
  function getGitHubURL (line 23) | function getGitHubURL() {
  function getGitHubIssuesURL (line 27) | function getGitHubIssuesURL() {
  function getInternalDevToolsFeedbackGroup (line 31) | function getInternalDevToolsFeedbackGroup() {
  function getVersionString (line 35) | function getVersionString() {

FILE: src/backend/EnvironmentWrapper.js
  function attach (line 12) | function attach(

FILE: src/backend/agent.js
  class Agent (line 15) | class Agent extends EventEmitter<{|

FILE: src/bridge.js
  constant BATCH_DURATION (line 14) | const BATCH_DURATION = 100;
  method for (line 118) | for (let i = 0; i < this._messageQueue.length; i += 3) {

FILE: src/devtools/DevTools.js
  function DevTools (line 62) | function DevTools({

FILE: src/devtools/store.js
  method if (line 151) | if (newRecords == null) {

FILE: src/devtools/utils.js
  function deepCopyFunction (line 10) | function deepCopyFunction(inObject: any) {
  function debounce (line 40) | function debounce(func, wait) {

FILE: src/devtools/view/Components/EnvironmentSelector.js
  function EnvironmentSelector (line 3) | function EnvironmentSelector(props) {

FILE: src/devtools/view/Components/Record.js
  function Record (line 3) | function Record(props) {

FILE: src/devtools/view/NetworkDisplayer.js
  function handleMenuClick (line 124) | function handleMenuClick(e, id) {
  function handleReset (line 130) | function handleReset(e) {
  function handleSearch (line 137) | function handleSearch(e) {

FILE: src/devtools/view/StoreDisplayer.js
  function updateRecords (line 6) | function updateRecords(store, selection) {
  function generateComponentsList (line 29) | function generateComponentsList(store, searchResults, recordsList, selec...
  function handleMenuClick (line 94) | function handleMenuClick(selection) {
  function handleReset (line 102) | function handleReset(e) {
  function handleSearch (line 115) | function handleSearch(e) {

FILE: src/hook.js
  function registerEnvironment (line 30) | function registerEnvironment(environment) {
  function sub (line 39) | function sub(event, fn) {
  function on (line 44) | function on(event, fn) {
  function off (line 51) | function off(event, fn) {
  function emit (line 64) | function emit(event, data) {
  method get (line 93) | get() {
Condensed preview — 66 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (156K chars).
[
  {
    "path": ".eslintignore",
    "chars": 161,
    "preview": "node_modules\n\nshells/browser/chrome/build\nshells/browser/firefox/build\nshells/browser/shared/build\nshells/dev/dist\nvendo"
  },
  {
    "path": ".flowconfig",
    "chars": 488,
    "preview": "[ignore]\nshells/browser/chrome/build/*\nshells/browser/firefox/build/*\nshells/dev/build/*\n\n[declarations]\n<PROJECT_ROOT>/"
  },
  {
    "path": ".gitignore",
    "chars": 314,
    "preview": "/shells/browser/chrome/*.crx\n/shells/browser/chrome/*.pem\n/shells/browser/firefox/*.xpi\n/shells/browser/firefox/*.pem\n/s"
  },
  {
    "path": ".gitmodules",
    "chars": 105,
    "preview": "[submodule \"relay-examples\"]\n\tpath = relay-examples\n\turl = https://github.com/relayjs/relay-examples.git\n"
  },
  {
    "path": ".travis.yml",
    "chars": 89,
    "preview": "services:\n  - docker\ndist: xenial\nscript:\n  - docker-compose up --abort-on-container-exit"
  },
  {
    "path": ".yarnrc",
    "chars": 26,
    "preview": "yarn-offline-mirror false\n"
  },
  {
    "path": "Dockerfile",
    "chars": 92,
    "preview": "FROM node:12.18.3\nWORKDIR /usr/src/app\nCOPY . /usr/src/app\nRUN npm install\nCMD npm run test\n"
  },
  {
    "path": "LICENSE",
    "chars": 1086,
    "preview": "MIT License\n\nCopyright (c) Facebook, Inc. and its affiliates.\n\nPermission is hereby granted, free of charge, to any pers"
  },
  {
    "path": "README.md",
    "chars": 2409,
    "preview": "\n<div align=\"center\">\n  <img width=\"50%\" src='./assets/protologo.png'></img>\n</div>\n\n<h1>Proto Relay</h1>\nProto Relay is"
  },
  {
    "path": "__tests__/DevTools.spec.js",
    "chars": 4175,
    "preview": "import React from 'react';\nimport { configure, shallow, render } from 'enzyme';\nimport Adapter from 'enzyme-adapter-reac"
  },
  {
    "path": "__tests__/Global.spec.js",
    "chars": 129,
    "preview": "describe('Timezones', () => {\n  it('should always be UTC', () => {\n    expect(new Date().getTimezoneOffset()).toBe(0);\n "
  },
  {
    "path": "__tests__/NetworkDisplayer.spec.js",
    "chars": 752,
    "preview": "import React from 'react';\nimport { configure, shallow, render } from 'enzyme';\nimport Adapter from 'enzyme-adapter-reac"
  },
  {
    "path": "__tests__/Record.spec.js",
    "chars": 2162,
    "preview": "import React from 'react';\nimport { configure, shallow, render } from 'enzyme';\nimport Adapter from 'enzyme-adapter-reac"
  },
  {
    "path": "__tests__/StoreDisplayer.spec.js",
    "chars": 6135,
    "preview": "import React from 'react';\nimport { configure, shallow, render } from 'enzyme';\nimport Adapter from 'enzyme-adapter-reac"
  },
  {
    "path": "__tests__/StoreTimeline.spec.js",
    "chars": 1764,
    "preview": "import React from 'react';\nimport { configure, shallow, render } from 'enzyme';\nimport Adapter from 'enzyme-adapter-reac"
  },
  {
    "path": "__tests__/__mocks__/styleMock.js",
    "chars": 20,
    "preview": "module.exports = {};"
  },
  {
    "path": "__tests__/__snapshots__/DevTools.spec.js.snap",
    "chars": 8451,
    "preview": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`DevTools Renders correctly 1`] = `\nArray [\n  <div\n    className=\"na"
  },
  {
    "path": "__tests__/__snapshots__/NetworkDisplayer.spec.js.snap",
    "chars": 939,
    "preview": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`NetworkDisplayer Renders correctly 1`] = `\nArray [\n  <div\n    class"
  },
  {
    "path": "__tests__/__snapshots__/StoreDisplayer.spec.js.snap",
    "chars": 2424,
    "preview": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`StoreDisplayer Renders correctly 1`] = `\nArray [\n  <div\n    classNa"
  },
  {
    "path": "__tests__/__snapshots__/StoreTimeline.spec.js.snap",
    "chars": 5347,
    "preview": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`StoreTimeline Renders correctly 1`] = `\nArray [\n  <div\n    classNam"
  },
  {
    "path": "__tests__/bridge.spec.js",
    "chars": 1450,
    "preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
  },
  {
    "path": "__tests__/global-setup.js",
    "chars": 60,
    "preview": "module.exports = async () => {\n  process.env.TZ = 'UTC';\n};\n"
  },
  {
    "path": "__tests__/store.spec.js",
    "chars": 10922,
    "preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
  },
  {
    "path": "babel.config.js",
    "chars": 1270,
    "preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
  },
  {
    "path": "docker-compose.yml",
    "chars": 241,
    "preview": "version: '3.0'\nservices:\n  test:\n    image: 'protorelay/protostar'\n    container_name: 'protostar-test'\n    volumes: \n  "
  },
  {
    "path": "flow.js",
    "chars": 847,
    "preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
  },
  {
    "path": "package.json",
    "chars": 3444,
    "preview": "{\n  \"version\": \"1.0.0\",\n  \"name\": \"protostar-relay\",\n  \"repository\": \"oslabs-beta/protostar-relay\",\n  \"license\": \"MIT\",\n"
  },
  {
    "path": "shells/browser/chrome/build.js",
    "chars": 1261,
    "preview": "#!/usr/bin/env node\n/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the"
  },
  {
    "path": "shells/browser/chrome/manifest.json",
    "chars": 1284,
    "preview": "{\n  \"manifest_version\": 2,\n  \"name\": \"Proto Relay\",\n  \"description\": \"Adds Relay debugging tools to the Chrome DevTool p"
  },
  {
    "path": "shells/browser/chrome/nottest.js",
    "chars": 558,
    "preview": "#!/usr/bin/env node\n/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the"
  },
  {
    "path": "shells/browser/chrome/now.json",
    "chars": 150,
    "preview": "{\n  \"name\": \"relay-devtools-experimental-chrome\",\n  \"alias\": [\"relay-devtools-experimental-chrome\"],\n  \"files\": [\"index."
  },
  {
    "path": "shells/browser/chrome/watch.js",
    "chars": 659,
    "preview": "#!/usr/bin/env node\n/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the"
  },
  {
    "path": "shells/browser/shared/build.js",
    "chars": 3798,
    "preview": "#!/usr/bin/env node\n/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the"
  },
  {
    "path": "shells/browser/shared/index.html",
    "chars": 759,
    "preview": "<!-- @format -->\n\n<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" conte"
  },
  {
    "path": "shells/browser/shared/main.html",
    "chars": 139,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\" />\n    <script src=\"./build/main.js\"></script>\n  </head>\n  <bo"
  },
  {
    "path": "shells/browser/shared/src/backend.js",
    "chars": 1997,
    "preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
  },
  {
    "path": "shells/browser/shared/src/background.js",
    "chars": 1461,
    "preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
  },
  {
    "path": "shells/browser/shared/src/contentScript.js",
    "chars": 1918,
    "preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
  },
  {
    "path": "shells/browser/shared/src/inject.js",
    "chars": 843,
    "preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
  },
  {
    "path": "shells/browser/shared/src/injectGlobalHook.js",
    "chars": 2226,
    "preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
  },
  {
    "path": "shells/browser/shared/src/main.js",
    "chars": 5519,
    "preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
  },
  {
    "path": "shells/browser/shared/src/utils.js",
    "chars": 918,
    "preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
  },
  {
    "path": "shells/browser/shared/view/App.jsx",
    "chars": 680,
    "preview": "/** @format */\nimport React, { useEffect, useState } from 'react';\n\nconst port = chrome.runtime.connect({ name: 'test' }"
  },
  {
    "path": "shells/browser/shared/view/index.js",
    "chars": 949,
    "preview": "/** @format */\n\nimport React from 'react';\nimport { render } from 'react-dom';\n\nimport App from './App.jsx';\nimport styl"
  },
  {
    "path": "shells/browser/shared/view/styles.scss",
    "chars": 4566,
    "preview": "/** @format */\n\n//***********************************\n//*********   VARIABLES   ***********\n//**************************"
  },
  {
    "path": "shells/browser/shared/webpack.backend.js",
    "chars": 1730,
    "preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
  },
  {
    "path": "shells/browser/shared/webpack.config.js",
    "chars": 1924,
    "preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
  },
  {
    "path": "shells/utils.js",
    "chars": 1149,
    "preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
  },
  {
    "path": "src/backend/EnvironmentWrapper.js",
    "chars": 2738,
    "preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
  },
  {
    "path": "src/backend/agent.js",
    "chars": 1615,
    "preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
  },
  {
    "path": "src/backend/index.js",
    "chars": 2308,
    "preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
  },
  {
    "path": "src/backend/types.js",
    "chars": 1288,
    "preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
  },
  {
    "path": "src/backend/utils.js",
    "chars": 653,
    "preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
  },
  {
    "path": "src/bridge.js",
    "chars": 4256,
    "preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
  },
  {
    "path": "src/devtools/DevTools.js",
    "chars": 5471,
    "preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
  },
  {
    "path": "src/devtools/context.js",
    "chars": 577,
    "preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
  },
  {
    "path": "src/devtools/store.js",
    "chars": 13183,
    "preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
  },
  {
    "path": "src/devtools/utils.js",
    "chars": 1255,
    "preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
  },
  {
    "path": "src/devtools/view/Components/EnvironmentSelector.js",
    "chars": 312,
    "preview": "import React, { useState, useCallback, useEffect } from 'react';\n\nfunction EnvironmentSelector(props) {\n  const [selectE"
  },
  {
    "path": "src/devtools/view/Components/Record.js",
    "chars": 703,
    "preview": "import React from 'react';\n\nfunction Record(props) {\n  /* Maps through array and recursively calls component if the prop"
  },
  {
    "path": "src/devtools/view/Components/SnapshotLinks.js",
    "chars": 888,
    "preview": "import React, { useState } from 'react';\n\nconst SnapshotLinks = ({ timeline, currentEnvID, handleSnapshot }) => {\n  cons"
  },
  {
    "path": "src/devtools/view/NetworkDisplayer.js",
    "chars": 5384,
    "preview": "import React, { useState, useEffect, useContext } from 'react';\nimport { StoreContext } from '../context';\nimport Record"
  },
  {
    "path": "src/devtools/view/StoreDisplayer.js",
    "chars": 4641,
    "preview": "import React, { useState } from 'react';\nimport Record from './Components/Record';\nimport { debounce } from '../utils';\n"
  },
  {
    "path": "src/devtools/view/StoreTimeline.js",
    "chars": 5878,
    "preview": "import React, { useState, useContext, useEffect } from 'react';\nimport InputRange from 'react-input-range';\nimport { Bri"
  },
  {
    "path": "src/hook.js",
    "chars": 2161,
    "preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
  },
  {
    "path": "src/types.js",
    "chars": 2589,
    "preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
  }
]

About this extraction

This page contains the full source code of the oslabs-beta/protostar-relay GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 66 files (142.3 KB), approximately 37.4k tokens, and a symbol index with 56 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!