master a26869ba2e26 cached
51 files
100.4 KB
25.9k tokens
104 symbols
1 requests
Download .txt
Repository: edvinerikson/relay-subscriptions
Branch: master
Commit: a26869ba2e26
Files: 51
Total size: 100.4 KB

Directory structure:
gitextract_qz37ndd9/

├── .babelrc
├── .eslintignore
├── .eslintrc
├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── docs/
│   ├── API/
│   │   └── Subscription.md
│   └── API.md
├── examples/
│   ├── .eslintrc
│   └── todo/
│       ├── .babelrc
│       ├── README.md
│       ├── data/
│       │   ├── database.js
│       │   ├── schema.graphql
│       │   └── schema.js
│       ├── js/
│       │   ├── NetworkLayer.js
│       │   ├── app.js
│       │   ├── components/
│       │   │   ├── Todo.js
│       │   │   ├── TodoApp.js
│       │   │   ├── TodoList.js
│       │   │   ├── TodoListFooter.js
│       │   │   └── TodoTextInput.js
│       │   ├── mutations/
│       │   │   ├── AddTodoMutation.js
│       │   │   ├── ChangeTodoStatusMutation.js
│       │   │   ├── MarkAllTodosMutation.js
│       │   │   ├── RemoveCompletedTodosMutation.js
│       │   │   ├── RemoveTodoMutation.js
│       │   │   └── RenameTodoMutation.js
│       │   ├── queries/
│       │   │   └── ViewerQueries.js
│       │   └── subscriptions/
│       │       ├── AddTodoSubscription.js
│       │       ├── RemoveTodoSubscription.js
│       │       └── UpdateTodoSubscription.js
│       ├── package.json
│       ├── public/
│       │   ├── base.css
│       │   ├── index.css
│       │   ├── index.html
│       │   └── learn.json
│       ├── server.js
│       └── tools/
│           ├── .eslintrc
│           └── updateSchema.js
├── package.json
└── src/
    ├── .flowconfig
    ├── Environment.js
    ├── Subscription.js
    ├── SubscriptionRequest.js
    ├── __tests__/
    │   └── createContainer.js
    ├── createContainer.js
    ├── createSubscriptionQuery.js
    ├── index.js
    ├── types.js
    └── updateStoreData.js

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

================================================
FILE: .babelrc
================================================
{
  "presets": [
    ["env", { "loose": true }],
    "stage-2",
    "react"
  ],
  "plugins": ["dev-expression", "add-module-exports"]
}


================================================
FILE: .eslintignore
================================================
/examples/*/node_modules/*


================================================
FILE: .eslintrc
================================================
{
  "extends": "airbnb",
  "env": {
    "jest": true,
    "jasmine": true
  },
  "parser": "babel-eslint",
  "plugins": [
    "flowtype"
  ],
  "rules": {
    "no-underscore-dangle": 0,
    "flowtype/define-flow-type": 2
  }
}


================================================
FILE: .gitignore
================================================
# Custom
lib/

# Logs
logs
*.log
npm-debug.log*

# Runtime data
pids
*.pid
*.seed

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# nyc test coverage
.nyc_output

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# node-waf configuration
.lock-wscript

# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules
jspm_packages

# Optional npm cache directory
.npm

# Optional REPL history
.node_repl_history


================================================
FILE: .travis.yml
================================================
sudo: false

language: node_js
node_js:
  - stable

cache: yarn

branches:
  only:
    - master


================================================
FILE: LICENSE
================================================
The MIT License (MIT)

Copyright (c) 2016 Edvin Erikson

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
================================================
# Relay Subscriptions [![npm][npm-badge]][npm]

Subscription support for [Relay Classic](http://facebook.github.io/relay/).

![PoC](http://g.recordit.co/zZfGNmYJTr.gif)

[![Discord][discord-badge]][discord]

## Documentation

- [Guide](#guide)
- [TodoMVC example](examples/todo)
- [API reference](docs/API.md)

## Guide

### Installation

```sh
$ npm i -S react react-relay babel-relay-plugin
$ npm i -S relay-subscriptions
```

### Network layer ([API](docs/API.md#network-layer))

To use Relay Subscriptions, you need to provide a network layer with subscription support. This network layer needs to implement a `sendSubscription` method that takes a subscription request, calls the observer methods on the request when the subscription updates, and returns a disposable for tearing down the subscription.

A simple network layer that uses [Socket.IO](http://socket.io/) as the underlying transport looks like:

```js
import Relay from 'react-relay/classic';
import io from 'socket.io-client';

export default class NetworkLayer extends Relay.DefaultNetworkLayer {
  constructor(...args) {
    super(...args);

    this.socket = io();
    this.requests = Object.create(null);

    this.socket.on('subscription update', ({ id, data, errors }) => {
      const request = this.requests[id];
      if (errors) {
        request.onError(errors);
      } else {
        request.onNext(data);
      }
    });
  }

  sendSubscription(request) {
    const id = request.getClientSubscriptionId();
    this.requests[id] = request;

    this.socket.emit('subscribe', {
      id,
      query: request.getQueryString(),
      variables: request.getVariables(),
    });

    return {
      dispose: () => {
        this.socket.emit('unsubscribe', id);
      },
    };
  }
}
```

For a full example, see [the network layer](examples/todo/js/NetworkLayer.js) in the TodoMVC example.

If your server uses [GraphQL.js](https://github.com/graphql/graphql-js), [graphql-relay-subscription](https://github.com/taion/graphql-relay-subscription) provides helpers for implementing subscriptions. For a basic example, see [the server](examples/todo/server.js) and [the schema](examples/todo/data/schema.js) in the TodoMVC example.

### Environment ([API](docs/API.md#relaysubscriptionsenvironment))

Instead of using a standard `Relay.Environment`, use a `RelaySubscriptions.Environment`. This environment class adds subscription support to the standard Relay environment.

```js
import RelaySubscriptions from 'relay-subscriptions';

import NetworkLayer from './NetworkLayer';

const environment = new RelaySubscriptions.Environment();
environment.injectNetworkLayer(new NetworkLayer());
```

### Subscriptions ([API](docs/API.md#subscription))

Subclass the `Subscription` class to define subscriptions. This base class is similar to `Relay.Mutation`. A basic subscription looks like:

```js
import Relay from 'react-relay/classic';
import { Subscription } from 'relay-subscriptions';

import Widget from '../components/Widget';

export default class WidgetSubscription extends Subscription {
  static fragments = {
    widget: () => Relay.QL`
      fragment on Widget {
        id
      }
    `,
  };

  getSubscription() {
    return Relay.QL`
      subscription {
        updateWidget(input: $input) {
          widget {
            ${Widget.getFragment('widget')}
          }
        }
      }
    `;
  }

  getConfigs() {
    return [{
      type: 'FIELDS_CHANGE',
      fieldIDs: {
        widget: this.props.widget.id,
      },
    }];
  }

  getVariables() {
    return {
      id: this.props.widget.id,
    };
  }
}
```

Due to an open issue ([#12]), for a `RANGE_ADD` subscription, you must manually request the `__typename` field on the edge in the payload.

For full examples, see [the subscriptions](examples/todo/js/subscriptions) in the TodoMVC example.

### Containers ([API](docs/API.md#relaysubscriptionscreatecontainer))

For components with subscriptions, use `RelaySubscriptions.createContainer` instead of `Relay.createContainer`. Define your Relay fragments normally, including the fragments for any subscriptions you need, then define a `subscriptions` array of functions that create the desired subscriptions from the component's props.

```js
import React from 'react';
import Relay from 'react-relay/classic';
import RelaySubscriptions from 'relay-subscriptions';

import WidgetSubscription from '../subscriptions/WidgetSubscription';

class Widget extends React.Component { /* ... */ }

export default RelaySubscriptions.createContainer(Widget, {
  fragments: {
    widget: () => Relay.QL`
      fragment on Widget {
        # ...
        ${WidgetSubscription.getFragment('widget')}
      }
    `,
  },

  subscriptions: [
    ({ widget }) => new WidgetSubscription({ widget }),
  ],
})
```

If you want to manually manage your subscription, the container also adds a `subscribe` method on `props.relay`, which takes a `Subscription` and an optional observer, and returns a disposable for tearing down the subscription.

## TODO

- [ ] Add tests ([#1])
- [ ] Automatically add `__typename` to query for `RANGE_ADD` subscriptions ([#12])

## Credits
Big thanks to [@taion](https://github.com/taion) for cleaning up my mess, creating a really nice API and these amazing docs :tada: 

[#1]: https://github.com/edvinerikson/relay-subscriptions/issues/1
[#12]: https://github.com/edvinerikson/relay-subscriptions/issues/12

[npm-badge]: https://img.shields.io/npm/v/relay-subscriptions.svg
[npm]: https://www.npmjs.org/package/relay-subscriptions

[discord-badge]: https://img.shields.io/badge/Discord-join%20chat%20%E2%86%92-738bd7.svg
[discord]: https://discord.gg/0ZcbPKXt5bX40xsQ


================================================
FILE: docs/API/Subscription.md
================================================
# Subscription
RelaySubscriptions makes use of the `Relay.Mutation` API.
If you are familiar with the mutation api this shouldn't be any new things.
Except the `getSubscription` method which replaced `getMutation`.

# Overview

### Properties
`static fragments`  
_Declare this subscription's data dependencies here_  
`static initialVariables`  
_A default set of variables to make available to this subscription's fragment builders_  
`static prepareVariables`  
_A method to modify the variables based on the runtime environment, previous variables, or the meta route_

### Methods
`constructor(props)`  
`abstract getConfigs()`  
`abstract getSubscription()`  
`abstract getVariables()`  
`static getFragment(fragmentName[, variableMapping])`  

# Properties
## fragments (static property)
```js
static fragments: RelayMutationFragments<$Keys<Tp>>

// Type of RelayMutationFragments
type RelayMutationFragments<Tk> = {
  [key: Tk]: FragmentBuilder;
};

// Type of FragmentBuilder
type FragmentBuilder = (variables: Variables) => RelayConcreteNode;
```
We declare our subscription' data dependencies here, just as we would with a container.

### Example
```js
class UpdateTodoSubscription extends RelaySubscriptions.Subscription {
  static fragments = {
    todo: () => Relay.QL`
      fragment on Todo {
        id
        text
        complete
      }
    `,
  };
}
```

## initialVariables (static property)
`static initialVariables: {[name: string]: mixed};`  
The defaults we specify here will become available to our fragment builders:
### Example
```js
class AddTodoSubscription extends RelaySubscriptions.Subscription {
  static initialVariables = {orderby: 'priority'};
  static fragments = {
    todos: () => Relay.QL`
      # The variable defined above is available here as $orderby
      fragment on Viewer { todos(orderby: $orderby) { ... } }
    `,
  };
  /* ... */
}
```

## prepareVariables (static property)
```js
static prepareVariables: ?(
  prevVariables: {[name: string]: mixed},
  route: RelayMetaRoute,
) => {[name: string]: mixed}

// Type of `route` argument
type RelayMetaRoute = {
  name: string;
}
```
If we provide to a subscription a method that conforms to the signature described above, it will be given the opportunity to modify the fragment builders' variables, based on the previous variables (or the initialVariables if no previous ones exist), the meta route, and the runtime environment. Whatever variables this method returns will become available to this subscription's fragment builders.
### Example
```js
class BuySongSubscription extends RelaySubscriptions.Subscription {
  static initialVariables = {format: 'mp3'};
  static prepareVariables = (prevVariables) => {
    var overrideVariables = {};
    var formatPreference = localStorage.getItem('formatPreference');
    if (formatPreference) {
      overrideVariables.format = formatPreference;  // Lossless, hopefully
    }
    return {...prevVariables, overrideVariables};
  };
  /* ... */
}
```

# Methods
## constructor

Create a subscription instance using the `new` keyword, optionally passing it some props. Note that `this.props` is not available inside the constructor function, but are set for all the methods mentioned below (getConfigs, getVariables, etc). This restriction is due to the fact that subscription props may depend on data from the RelayEnvironment, which isn't known until the subscription is applied with `subscribe` provided by `SubscriptionProvider` and `SubscriptionContainer`.

### Example
```js
const flightsUpdateSub = new FlightsUpdateSubscription({airport: 'yvr'});
this.props.subscriptions.subscribe(flightsUpdateSub);
```

## getConfigs (abstract method)
`abstract getConfigs(): Array<{[key: string]: mixed}>`  
Implement this required method to give Relay instructions on how to use the response payload from each subscription to update the client-side store.

### Example
```js
class LikeStorySubscription extends Subscription {
  getConfigs() {
    return [{
      type: 'FIELDS_CHANGE',
      fieldIDs: {
        story: this.props.story.id,
      },
    }];
  }
}
```

## getSubscription (abstract method)
`abstract getSubscription(): GraphQL.Subscription`  
Implement this required method to return a GraphQL subscription operation that represents the subscription to subscribe to.

### Example
```js
class LikeStorySubscription extends Subscription {
  getSubscription() {
    return Relay.QL`subscription {
      likeStorySubscribe {
        story {
          likes {
            likeSentence
            count
          }
        }
      }
    }`;
  }
}
```

## getVariables (abstract method)
`abstract getVariables(): {[name: string]: mixed}`  
Implement this required method to prepare variables to be used as input to the subscription.

## Example
```js
class DestroyShipSubscription extends RelaySubscriptions.Subscription {
  getVariables() {
    return {
      factionId: this.props.faction.id,
    };
  }
}
```

## getFragment (static method)
```js
static getFragment(
  fragmentName: $Keys<Tp>,
  variableMapping?: Variables
): RelayFragmentReference

// Type of the variableMapping argument
type Variables = {[name: string]: mixed};
```
Gets a fragment reference for use in a parent's query fragment.

### Example
```js
class StoryComponent extends React.Component {
  /* ... */
  static fragments = {
    story: () => Relay.QL`
      fragment on Story {
        id,
        text,
        ${LikeStorySubscription.getFragment('story')},
      }
    `,
  };
}
```
You can also pass variables to the subscription's fragment builder from the outer fragment that contains it.
```js
class Movie extends React.Component {
  /* ... */
  static fragments = {
    movie: () => Relay.QL`
      fragment on Movie {
        posterImage(lang: $lang) { url },
        trailerVideo(format: $format, lang: $lang) { url },
        ${MovieUpdateSubscription.getFragment('movie', {
          format: variables.format,
          lang: variables.lang,
        })},
      }
    `,
  };
}
```


================================================
FILE: docs/API.md
================================================
# API Reference

- [Network layer](#network-layer)
- [RelaySubscriptions.Environment](#relaysubscriptionsenvironment)
- [Subscription](#subscription)
- [RelaySubscriptions.createContainer](#relaysubscriptionscreatecontainer)

## Network layer

You must implement a network layer that connects to a backend with subscription support. This network layer must implement the following additional method:

```js
sendSubscription: (request: SubscriptionRequest) => Disposable
```

The `SubscriptionRequest` object supports:

```js
type SubscriptionRequest {
  getQueryString: () => string;
  getVariables: () => Variables;
  getClientSubscriptionId: () => string;

  onNext: (payload: SubscriptionResult) => void;
  onError: (error: any) => void;
  onCompleted: (value: any) => void;

  getDebugName: () => string;
}
```

The `getQueryString` method returns the GraphQL query string. The `getVariables` method returns the variables for the query. The `getClientSubscriptionId` method returns a client-side ID for the subscription.

Call the `onNext`, `onError`, and `onCompleted` methods when the subscription updates.

The return value is expected to conform to:

```js
type Disposable = {
  dispose: () => void;
}
```

The `dispose` method should tear down the subscription.

## `RelaySubscriptions.Environment`

`RelaySubscriptions.Environment` extends `Relay.Environment` and provides subscription support.

### `subscribe`

This method has the signature:

```js
subscribe: (subscription: Subscription, observer?: Observer) => Disposable
```

This method will make the subscription.

The observer, if provided, is expected to conform to:

```js
type Observer = {
  onNext?: (value: SubscriptionResult) => void;
  onError?: (error: any) => void;
  onCompleted?: (value: any) => void;
}
```

The specified callbacks will be invoked when the subscription updates. The `onNext` callback fires after the store update.

## `Subscription`

Subclass the `Subscription` class to define a subscription. This base class is similar to `Relay.Mutation`, except that you need to implement `getSubscription` instead of `getMutation` and `getFatQuery`.

```js
import { Subscription } from 'relay-subscriptions';

export default class WidgetSubscription extends Subscription {
  /* ... */
}
```

### `constructor`

As with `Relay.Mutation`, you can construct an instance of a subclass of `Subscription` with the `new` keyword and optional props.

```js
new WidgetSubscription({ widget })
```

### Static properties

Define these properties to specify the input data dependencies for the subscription.

#### `fragments`

This static property defines the subscription's data requirements as a object of fragment builders, as with the `fragments` static property on `Relay.Mutation`. These fragments can then be composed elsewhere with `MySubscription.getFragment(fragmentName)`.

```js
static fragments = {
  widget: () => Relay.QL`
    fragment on Widget {
      id
    }
  },
};
```

#### `initialVariables`

If provided, this specifies the default variables for the fragment builders, as with the `initialVariables` static property on `Relay.Mutation`.

#### `prepareVariables`

If provided, this method modifies variables for the fragment builders, as with the `prepareVariables` static method on `Relay.Mutation`.

### Abstract methods

Implement these methods to define the subscription's behavior.

#### `getSubscription`

This method should return the concrete subscription query. The query should use the `$input` variable for the subscription input. Unlike with mutations, this is not a fat query, so it must specify all desired fields. You can compose in fragments from container components here, which can help manage code duplication.

```js
getSubscription() {
  return Relay.QL`
    subscription {
      updateWidget(input: $input) {
        ${Widget.getFragment('widget')}
      }
    }
  `;
}
```

#### `getConfigs`

This method should return the mutation configs, as with the `getConfigs` method on `Relay.Mutation`.

```js
getConfigs() {
  return [{
    type: 'FIELDS_CHANGE',
    fieldIDs: {
      widget: this.props.widget.id,
    },
  }];
}
```

#### `getVariables`

This method should return the subscription input variables, as with the `getVariables` method on `Relay.Mutation`.

```js
getVariables() {
  return {
    id: this.props.widget.id,
  };
}
```

## `RelaySubscriptions.createContainer`

`RelaySubscriptions.createContainer` behaves like `Relay.createContainer`. It provides additional functionality for subscription support.

### Container specification

#### `subscriptions`

The specification for a Relay Subscriptions container accepts an optional `subscriptions` property:

```js
subscriptions?: subscriptionFn[]
```

These subscription functions are expected to have the signature:

```js
type subscriptionFn = (props: Object) => ?Subscription<any>;
```

This function can return a falsy value to indicate that no subscription is desired.

The Relay Subscriptions container will manage these subscriptions. It will establish the subscription after the component mounts, replace any subscriptions that have changed type or variables, and tear down these subscriptions when the component unmounts.

```js
import RelaySubscriptions from 'relay-subscriptions';

/* ... */

export default RelaySubscriptions.createContainer(Widget, {
  fragments: {
    widget: () => Relay.QL`
      fragment on Widget {
        # ...
        ${WidgetSubscription.getFragment('widget')}
      }
    `,
  },

  subscriptions: [
    ({ pending, widget }) => !pending && new WidgetSubscription({ widget }),
  ],
});
```

### `props.relay`

The Relay Subscriptions container injects an augmented `props.relay` to the component with subscription functionality.

#### `subscribe`

This method invokes the `subscribe` method on the Relay Subscriptions environment. It has the same signature of:

```js
subscribe: (subscription: Subscription, observer?: Observer) => Disposable
```

You can use this to manually manage the subscription.

```js
import RelaySubscriptions from 'relay-subscriptions';

/* ... */

class Widget extends React.Component {
  componentDidMount() {
    const { relay, widget } = this.props;
    this.subscription = relay.subscribe(
      new WidgetSubscription({ widget }),
    );
  }

  componentWillUnmount() {
    this.subscription.dispose();
  }

  /* ... */
}

export default RelaySubscriptions.createContainer(Widget, {
  fragments: {
    widget: () => Relay.QL`
      fragment on Widget {
        # ...
        ${WidgetSubscription.getFragment('widget')}
      }
    `,
  },
});
```


================================================
FILE: examples/.eslintrc
================================================
{
  "rules": {
    // Don't fail linting if example dependencies aren't installed.
    "import/no-unresolved": "off"
  }
}


================================================
FILE: examples/todo/.babelrc
================================================
{
  "presets": [
    ["env", { "loose": true }],
    "stage-2",
    "react"
  ],
  "plugins": [
    ["relay", { "schema": "data/schema.graphql" }]
  ]
}


================================================
FILE: examples/todo/README.md
================================================
# Relay TodoMVC

## Installation

```
npm install
```

## Running

Start a local server:

```
npm start
```

## Developing

Any changes you make to files in the `js/` directory will cause the server to
automatically rebuild the app and refresh your browser.

If at any time you make changes to `data/schema.js`, stop the server,
regenerate `data/schema.json`, and restart the server:

```
npm run update-schema
npm start
```

## License

    This file provided by Facebook is for non-commercial testing and evaluation
    purposes only.  Facebook reserves all rights not expressly granted.

    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
    FACEBOOK 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: examples/todo/data/database.js
================================================
/**
 * This file provided by Facebook is for non-commercial testing and evaluation
 * purposes only.  Facebook reserves all rights not expressly granted.
 *
 * 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
 * FACEBOOK 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.
 */

export class Todo {}
export class User {}

// Mock authenticated ID
export const VIEWER_ID = 'me';

// Mock user data
const viewer = new User();
viewer.id = VIEWER_ID;
const usersById = {
  [VIEWER_ID]: viewer,
};

// Mock todo data
const todosById = {};
const todoIdsByUser = {
  [VIEWER_ID]: [],
};

const notifiers = [];

function notifyChange(topic, data) {
  // Delay the change notification to avoid the subscription update hitting the
  // client before the mutation response.
  setTimeout(() => {
    notifiers.forEach(notifier => notifier({ topic, data }));
  }, 100);
}

export function addNotifier(cb) {
  notifiers.push(cb);

  return () => {
    const index = notifiers.indexOf(cb);
    if (index !== -1) {
      notifiers.splice(index, 1);
    }
  };
}

let nextTodoId = 0;

export function addTodo(text, complete) {
  const todo = new Todo();
  todo.complete = !!complete;
  todo.id = `${nextTodoId++}`;
  todo.text = text;
  todosById[todo.id] = todo;
  todoIdsByUser[VIEWER_ID].push(todo.id);
  notifyChange('add_todo', todo);
  return todo.id;
}

addTodo('Taste JavaScript', true);
addTodo('Buy a unicorn', false);

export function getTodo(id) {
  return todosById[id];
}

export function getTodos(status = 'any') {
  const todos = todoIdsByUser[VIEWER_ID].map(id => todosById[id]);
  if (status === 'any') {
    return todos;
  }
  return todos.filter(todo => todo.complete === (status === 'completed'));
}

export function changeTodoStatus(id, complete) {
  const todo = getTodo(id);
  todo.complete = complete;
  notifyChange(`update_todo_${id}`, todo);
}

export function getUser(id) {
  return usersById[id];
}

export function getViewer() {
  return getUser(VIEWER_ID);
}

export function markAllTodos(complete) {
  const changedTodos = [];
  getTodos().forEach(todo => {
    if (todo.complete !== complete) {
      todo.complete = complete; // eslint-disable-line no-param-reassign
      changedTodos.push(todo);
      notifyChange(`update_todo_${todo.id}`, todo);
    }
  });
  return changedTodos.map(todo => todo.id);
}

export function removeTodo(id) {
  const todoIndex = todoIdsByUser[VIEWER_ID].indexOf(id);
  if (todoIndex !== -1) {
    todoIdsByUser[VIEWER_ID].splice(todoIndex, 1);
  }
  notifyChange('delete_todo', { id });
  delete todosById[id];
}

export function removeCompletedTodos() {
  const todosToRemove = getTodos().filter(todo => todo.complete);
  todosToRemove.forEach(todo => removeTodo(todo.id));
  return todosToRemove.map(todo => todo.id);
}

export function renameTodo(id, text) {
  const todo = getTodo(id);
  todo.text = text;
  notifyChange(`update_todo_${id}`, todo);
}


================================================
FILE: examples/todo/data/schema.graphql
================================================
schema {
  query: Root
  mutation: Mutation
  subscription: Subscription
}

input AddTodoInput {
  text: String!
  clientMutationId: String
}

type AddTodoPayload {
  todoEdge: TodoEdge
  viewer: User
  clientMutationId: String
}

input AddTodoSubscriptionInput {
  clientSubscriptionId: String
}

type AddTodoSubscriptionPayload {
  todo: Todo
  todoEdge: TodoEdge
  viewer: User
  clientSubscriptionId: String
}

input ChangeTodoStatusInput {
  complete: Boolean!
  id: ID!
  clientMutationId: String
}

type ChangeTodoStatusPayload {
  todo: Todo
  viewer: User
  clientMutationId: String
}

input MarkAllTodosInput {
  complete: Boolean!
  clientMutationId: String
}

type MarkAllTodosPayload {
  changedTodos: [Todo]
  viewer: User
  clientMutationId: String
}

type Mutation {
  addTodo(input: AddTodoInput!): AddTodoPayload
  changeTodoStatus(input: ChangeTodoStatusInput!): ChangeTodoStatusPayload
  markAllTodos(input: MarkAllTodosInput!): MarkAllTodosPayload
  removeCompletedTodos(input: RemoveCompletedTodosInput!): RemoveCompletedTodosPayload
  removeTodo(input: RemoveTodoInput!): RemoveTodoPayload
  renameTodo(input: RenameTodoInput!): RenameTodoPayload
}

# An object with an ID
interface Node {
  # The id of the object.
  id: ID!
}

# Information about pagination in a connection.
type PageInfo {
  # When paginating forwards, are there more items?
  hasNextPage: Boolean!

  # When paginating backwards, are there more items?
  hasPreviousPage: Boolean!

  # When paginating backwards, the cursor to continue.
  startCursor: String

  # When paginating forwards, the cursor to continue.
  endCursor: String
}

input RemoveCompletedTodosInput {
  clientMutationId: String
}

type RemoveCompletedTodosPayload {
  deletedTodoIds: [String]
  viewer: User
  clientMutationId: String
}

input RemoveTodoInput {
  id: ID!
  clientMutationId: String
}

type RemoveTodoPayload {
  deletedTodoId: ID
  viewer: User
  clientMutationId: String
}

input RemoveTodoSubscriptionInput {
  clientSubscriptionId: String
}

type RemoveTodoSubscriptionPayload {
  deletedTodoId: ID
  viewer: User
  clientSubscriptionId: String
}

input RenameTodoInput {
  id: ID!
  text: String!
  clientMutationId: String
}

type RenameTodoPayload {
  todo: Todo
  clientMutationId: String
}

type Root {
  viewer: User

  # Fetches an object given its ID
  node(
    # The ID of an object
    id: ID!
  ): Node
}

type Subscription {
  addTodoSubscription(input: AddTodoSubscriptionInput!): AddTodoSubscriptionPayload
  removeTodoSubscription(input: RemoveTodoSubscriptionInput!): RemoveTodoSubscriptionPayload
  updateTodoSubscription(input: UpdateTodoSubscriptionInput!): UpdateTodoSubscriptionPayload
}

type Todo implements Node {
  # The ID of an object
  id: ID!
  text: String
  complete: Boolean
}

# A connection to a list of items.
type TodoConnection {
  # Information to aid in pagination.
  pageInfo: PageInfo!

  # A list of edges.
  edges: [TodoEdge]
}

# An edge in a connection.
type TodoEdge {
  # The item at the end of the edge
  node: Todo

  # A cursor for use in pagination
  cursor: String!
}

input UpdateTodoSubscriptionInput {
  id: ID!
  clientSubscriptionId: String
}

type UpdateTodoSubscriptionPayload {
  todo: Todo
  viewer: User
  clientSubscriptionId: String
}

type User implements Node {
  # The ID of an object
  id: ID!
  todos(status: String = "any", after: String, first: Int, before: String, last: Int): TodoConnection
  totalCount: Int
  completedCount: Int
}


================================================
FILE: examples/todo/data/schema.js
================================================
/**
 * This file provided by Facebook is for non-commercial testing and evaluation
 * purposes only.  Facebook reserves all rights not expressly granted.
 *
 * 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
 * FACEBOOK 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.
 */

import {
  GraphQLBoolean,
  GraphQLID,
  GraphQLInt,
  GraphQLList,
  GraphQLNonNull,
  GraphQLObjectType,
  GraphQLSchema,
  GraphQLString,
} from 'graphql';

import {
  connectionArgs,
  connectionDefinitions,
  connectionFromArray,
  cursorForObjectInConnection,
  fromGlobalId,
  globalIdField,
  mutationWithClientMutationId,
  nodeDefinitions,
  toGlobalId,
} from 'graphql-relay';

import { subscriptionWithClientId } from 'graphql-relay-subscription';

import {
  Todo,
  User,
  addTodo,
  changeTodoStatus,
  getTodo,
  getTodos,
  getUser,
  getViewer,
  markAllTodos,
  removeCompletedTodos,
  removeTodo,
  renameTodo,
} from './database';

const { nodeInterface, nodeField } = nodeDefinitions(
  (globalId) => {
    const { type, id } = fromGlobalId(globalId);
    if (type === 'Todo') {
      return getTodo(id);
    } else if (type === 'User') {
      return getUser(id);
    }
    return null;
  },
  (obj) => {
    /* eslint-disable no-use-before-define */
    if (obj instanceof Todo) {
      return GraphQLTodo;
    } else if (obj instanceof User) {
      return GraphQLUser;
    }
    /* eslint-enable no-use-before-define */
    return null;
  }
);

const GraphQLTodo = new GraphQLObjectType({
  name: 'Todo',
  fields: {
    id: globalIdField('Todo'),
    text: {
      type: GraphQLString,
      resolve: (obj) => obj.text,
    },
    complete: {
      type: GraphQLBoolean,
      resolve: (obj) => obj.complete,
    },
  },
  interfaces: [nodeInterface],
});

const {
  connectionType: TodosConnection,
  edgeType: GraphQLTodoEdge,
} = connectionDefinitions({
  name: 'Todo',
  nodeType: GraphQLTodo,
});

const GraphQLUser = new GraphQLObjectType({
  name: 'User',
  fields: {
    id: globalIdField('User'),
    todos: {
      type: TodosConnection,
      args: {
        status: {
          type: GraphQLString,
          defaultValue: 'any',
        },
        ...connectionArgs,
      },
      resolve: (obj, { status, ...args }) =>
        connectionFromArray(getTodos(status), args),
    },
    totalCount: {
      type: GraphQLInt,
      resolve: () => getTodos().length,
    },
    completedCount: {
      type: GraphQLInt,
      resolve: () => getTodos('completed').length,
    },
  },
  interfaces: [nodeInterface],
});

const Root = new GraphQLObjectType({
  name: 'Root',
  fields: {
    viewer: {
      type: GraphQLUser,
      resolve: () => getViewer(),
    },
    node: nodeField,
  },
});

const GraphQLAddTodoMutation = mutationWithClientMutationId({
  name: 'AddTodo',
  inputFields: {
    text: { type: new GraphQLNonNull(GraphQLString) },
  },
  outputFields: {
    todoEdge: {
      type: GraphQLTodoEdge,
      resolve: ({ localTodoId }) => {
        const todo = getTodo(localTodoId);
        return {
          cursor: cursorForObjectInConnection(getTodos(), todo),
          node: todo,
        };
      },
    },
    viewer: {
      type: GraphQLUser,
      resolve: () => getViewer(),
    },
  },
  mutateAndGetPayload: ({ text }) => {
    const localTodoId = addTodo(text);
    return { localTodoId };
  },
});

const GraphQLChangeTodoStatusMutation = mutationWithClientMutationId({
  name: 'ChangeTodoStatus',
  inputFields: {
    complete: { type: new GraphQLNonNull(GraphQLBoolean) },
    id: { type: new GraphQLNonNull(GraphQLID) },
  },
  outputFields: {
    todo: {
      type: GraphQLTodo,
      resolve: ({ localTodoId }) => getTodo(localTodoId),
    },
    viewer: {
      type: GraphQLUser,
      resolve: () => getViewer(),
    },
  },
  mutateAndGetPayload: ({ id, complete }) => {
    const localTodoId = fromGlobalId(id).id;
    changeTodoStatus(localTodoId, complete);
    return { localTodoId };
  },
});

const GraphQLMarkAllTodosMutation = mutationWithClientMutationId({
  name: 'MarkAllTodos',
  inputFields: {
    complete: { type: new GraphQLNonNull(GraphQLBoolean) },
  },
  outputFields: {
    changedTodos: {
      type: new GraphQLList(GraphQLTodo),
      resolve: ({ changedTodoLocalIds }) => changedTodoLocalIds.map(getTodo),
    },
    viewer: {
      type: GraphQLUser,
      resolve: () => getViewer(),
    },
  },
  mutateAndGetPayload: ({ complete }) => {
    const changedTodoLocalIds = markAllTodos(complete);
    return { changedTodoLocalIds };
  },
});

// TODO: Support plural deletes
const GraphQLRemoveCompletedTodosMutation = mutationWithClientMutationId({
  name: 'RemoveCompletedTodos',
  outputFields: {
    deletedTodoIds: {
      type: new GraphQLList(GraphQLString),
      resolve: ({ deletedTodoIds }) => deletedTodoIds,
    },
    viewer: {
      type: GraphQLUser,
      resolve: () => getViewer(),
    },
  },
  mutateAndGetPayload: () => {
    const deletedTodoLocalIds = removeCompletedTodos();
    const deletedTodoIds = deletedTodoLocalIds.map(toGlobalId.bind(null, 'Todo'));
    return { deletedTodoIds };
  },
});

const GraphQLRemoveTodoMutation = mutationWithClientMutationId({
  name: 'RemoveTodo',
  inputFields: {
    id: { type: new GraphQLNonNull(GraphQLID) },
  },
  outputFields: {
    deletedTodoId: {
      type: GraphQLID,
      resolve: ({ id }) => id,
    },
    viewer: {
      type: GraphQLUser,
      resolve: () => getViewer(),
    },
  },
  mutateAndGetPayload: ({ id }) => {
    const localTodoId = fromGlobalId(id).id;
    removeTodo(localTodoId);
    return { id };
  },
});

const GraphQLRenameTodoMutation = mutationWithClientMutationId({
  name: 'RenameTodo',
  inputFields: {
    id: { type: new GraphQLNonNull(GraphQLID) },
    text: { type: new GraphQLNonNull(GraphQLString) },
  },
  outputFields: {
    todo: {
      type: GraphQLTodo,
      resolve: ({ localTodoId }) => getTodo(localTodoId),
    },
  },
  mutateAndGetPayload: ({ id, text }) => {
    const localTodoId = fromGlobalId(id).id;
    renameTodo(localTodoId, text);
    return { localTodoId };
  },
});

const GraphQLAddTodoSubscription = subscriptionWithClientId({
  name: 'AddTodoSubscription',
  outputFields: {
    todo: {
      type: GraphQLTodo,
      resolve: obj => obj,
    },
    todoEdge: {
      type: GraphQLTodoEdge,
      resolve: obj => ({
        cursor: cursorForObjectInConnection(getTodos(), getTodo(obj.id)),
        node: obj,
      }),
    },
    viewer: {
      type: GraphQLUser,
      resolve: () => getViewer(),
    },
  },
  subscribe: (input, context) => (
    context.subscribe('add_todo')
  ),
});


const GraphQLRemoveTodoSubscription = subscriptionWithClientId({
  name: 'RemoveTodoSubscription',
  outputFields: {
    deletedTodoId: {
      type: GraphQLID,
      resolve: ({ id }) => toGlobalId('Todo', id),
    },
    viewer: {
      type: GraphQLUser,
      resolve: () => getViewer(),
    },
  },
  subscribe: (input, context) => (
    context.subscribe('delete_todo')
  ),
});

const GraphQLUpdateTodoSubscription = subscriptionWithClientId({
  name: 'UpdateTodoSubscription',
  inputFields: {
    id: { type: new GraphQLNonNull(GraphQLID) },
  },
  outputFields: {
    todo: {
      type: GraphQLTodo,
      resolve: obj => obj,
    },
    viewer: {
      type: GraphQLUser,
      resolve: () => getViewer(),
    },
  },
  subscribe: ({ id }, context) => (
    context.subscribe(`update_todo_${fromGlobalId(id).id}`)
  ),
});

const Mutation = new GraphQLObjectType({
  name: 'Mutation',
  fields: {
    addTodo: GraphQLAddTodoMutation,
    changeTodoStatus: GraphQLChangeTodoStatusMutation,
    markAllTodos: GraphQLMarkAllTodosMutation,
    removeCompletedTodos: GraphQLRemoveCompletedTodosMutation,
    removeTodo: GraphQLRemoveTodoMutation,
    renameTodo: GraphQLRenameTodoMutation,
  },
});

const Subscription = new GraphQLObjectType({
  name: 'Subscription',
  fields: {
    addTodoSubscription: GraphQLAddTodoSubscription,
    removeTodoSubscription: GraphQLRemoveTodoSubscription,
    updateTodoSubscription: GraphQLUpdateTodoSubscription,
  },
});

export const schema = new GraphQLSchema({
  query: Root,
  mutation: Mutation,
  subscription: Subscription,
});


================================================
FILE: examples/todo/js/NetworkLayer.js
================================================
/* eslint-disable no-console */

import Relay from 'react-relay/classic';
import { SubscriptionClient } from 'subscriptions-transport-ws';

export default class NetworkLayer extends Relay.DefaultNetworkLayer {
  constructor(...args) {
    super(...args);

    this._subscriptionClient = new SubscriptionClient(
      `ws://${global.location.host}/graphql`,
      { reconnect: true },
    );
  }

  sendSubscription(request) {
    const { unsubscribe } = this._subscriptionClient.request({
      query: request.getQueryString(),
      variables: request.getVariables(),
    }).subscribe({
      next: ({ errors, data }) => {
        if (errors) {
          request.onError(errors);
        } else {
          request.onNext(data);
        }
      },
      error: request.onError,
      complete: request.onCompleted,
    });

    return { dispose: unsubscribe };
  }

  disconnect() {
    this._subscriptionClient.close();
  }
}


================================================
FILE: examples/todo/js/app.js
================================================
/**
 * This file provided by Facebook is for non-commercial testing and evaluation
 * purposes only.  Facebook reserves all rights not expressly granted.
 *
 * 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
 * FACEBOOK 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.
 */

import 'todomvc-common';

import { createHashHistory } from 'history';
import React from 'react';
import ReactDOM from 'react-dom';
import {
  applyRouterMiddleware,
  IndexRoute,
  Route,
  Router,
  useRouterHistory,
} from 'react-router';
import useRelay from 'react-router-relay';
import RelaySubscriptions from 'relay-subscriptions';

import NetworkLayer from './NetworkLayer';
import TodoApp from './components/TodoApp';
import TodoList from './components/TodoList';
import ViewerQueries from './queries/ViewerQueries';

const history = useRouterHistory(createHashHistory)({ queryKey: false });

const environment = new RelaySubscriptions.Environment();
environment.injectNetworkLayer(new NetworkLayer('/graphql'));

const mountNode = document.getElementById('root');

ReactDOM.render(
  <Router
    environment={environment}
    history={history}
    render={applyRouterMiddleware(useRelay)}
    forceFetch
  >
    <Route
      path="/"
      component={TodoApp}
      queries={ViewerQueries}
    >
      <IndexRoute
        component={TodoList}
        queries={ViewerQueries}
        prepareParams={() => ({ status: 'any' })}
      />
      <Route
        path=":status"
        component={TodoList}
        queries={ViewerQueries}
      />
    </Route>
  </Router>,
  mountNode
);


================================================
FILE: examples/todo/js/components/Todo.js
================================================
/**
 * This file provided by Facebook is for non-commercial testing and evaluation
 * purposes only.  Facebook reserves all rights not expressly granted.
 *
 * 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
 * FACEBOOK 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.
 */

import classNames from 'classnames';
import React from 'react';
import Relay from 'react-relay/classic';
import RelaySubscriptions from 'relay-subscriptions';

import ChangeTodoStatusMutation from '../mutations/ChangeTodoStatusMutation';
import RemoveTodoMutation from '../mutations/RemoveTodoMutation';
import RenameTodoMutation from '../mutations/RenameTodoMutation';
import UpdateTodoSubscription from '../subscriptions/UpdateTodoSubscription';
import TodoTextInput from './TodoTextInput';

class Todo extends React.Component {
  static propTypes = {
    viewer: React.PropTypes.object.isRequired,
    todo: React.PropTypes.object.isRequired,
    relay: React.PropTypes.object.isRequired,
  };

  state = {
    isEditing: false,
  };

  _handleCompleteChange = (e) => {
    const { relay, todo, viewer } = this.props;
    relay.commitUpdate(
      new ChangeTodoStatusMutation({
        todo,
        viewer,
        complete: e.target.checked,
      }),
    );
  };

  _handleDestroyClick = () => {
    this._removeTodo();
  };

  _handleLabelDoubleClick = () => {
    this._setEditMode(true);
  };

  _handleTextInputCancel = () => {
    this._setEditMode(false);
  };

  _handleTextInputDelete = () => {
    this._setEditMode(false);
    this._removeTodo();
  };

  _handleTextInputSave = (text) => {
    this._setEditMode(false);
    const { relay, todo } = this.props;
    relay.commitUpdate(
      new RenameTodoMutation({ todo, text }),
    );
  };

  _removeTodo() {
    const { relay, todo, viewer } = this.props;
    relay.commitUpdate(
      new RemoveTodoMutation({ todo, viewer }),
    );
  }

  _setEditMode = (shouldEdit) => {
    this.setState({ isEditing: shouldEdit });
  };

  renderTextInput() {
    return (
      <TodoTextInput
        className="edit"
        commitOnBlur
        initialValue={this.props.todo.text}
        onCancel={this._handleTextInputCancel}
        onDelete={this._handleTextInputDelete}
        onSave={this._handleTextInputSave}
      />
    );
  }

  render() {
    return (
      <li
        className={classNames({
          completed: this.props.todo.complete,
          editing: this.state.isEditing,
        })}
      >
        <div className="view">
          <input
            checked={this.props.todo.complete}
            className="toggle"
            onChange={this._handleCompleteChange}
            type="checkbox"
          />
          <label onDoubleClick={this._handleLabelDoubleClick}>
            {this.props.todo.text}
          </label>
          <button
            className="destroy"
            onClick={this._handleDestroyClick}
          />
        </div>
        {this.state.isEditing && this.renderTextInput()}
      </li>
    );
  }
}

export default RelaySubscriptions.createContainer(Todo, {
  fragments: {
    todo: () => Relay.QL`
      fragment on Todo {
        id
        complete
        text
        ${ChangeTodoStatusMutation.getFragment('todo')}
        ${RemoveTodoMutation.getFragment('todo')}
        ${RenameTodoMutation.getFragment('todo')}
        ${UpdateTodoSubscription.getFragment('todo')}
      }
    `,
    viewer: () => Relay.QL`
      fragment on User {
        ${ChangeTodoStatusMutation.getFragment('viewer')}
        ${RemoveTodoMutation.getFragment('viewer')}
      }
    `,
  },

  subscriptions: [
    ({ pending, todo }) => !pending && new UpdateTodoSubscription({ todo }),
  ],
});


================================================
FILE: examples/todo/js/components/TodoApp.js
================================================
/**
 * This file provided by Facebook is for non-commercial testing and evaluation
 * purposes only.  Facebook reserves all rights not expressly granted.
 *
 * 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
 * FACEBOOK 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.
 */

import React from 'react';
import Relay from 'react-relay/classic';
import RelaySubscriptions from 'relay-subscriptions';

import AddTodoMutation from '../mutations/AddTodoMutation';
import AddTodoSubscription from '../subscriptions/AddTodoSubscription';
import RemoveTodoSubscription from '../subscriptions/RemoveTodoSubscription';
import TodoListFooter from './TodoListFooter';
import TodoTextInput from './TodoTextInput';

class TodoApp extends React.Component {
  static propTypes = {
    viewer: React.PropTypes.object.isRequired,
    relay: React.PropTypes.object.isRequired,
    children: React.PropTypes.node.isRequired,
  };

  _handleTextInputSave = (text) => {
    const { relay, viewer } = this.props;
    relay.commitUpdate(
      new AddTodoMutation({ viewer, text }),
    );
  };

  render() {
    const { viewer, children } = this.props;
    const hasTodos = viewer.totalCount > 0;

    return (
      <div>
        <section className="todoapp">
          <header className="header">
            <h1>
              todos
            </h1>
            <TodoTextInput
              autoFocus
              className="new-todo"
              onSave={this._handleTextInputSave}
              placeholder="What needs to be done?"
            />
          </header>

          {children}

          {hasTodos && (
            <TodoListFooter
              todos={viewer.todos}
              viewer={viewer}
            />
          )}
        </section>
        <footer className="info">
          <p>
            Double-click to edit a todo
          </p>
          <p>
            Created by the <a href="https://facebook.github.io/relay/">
              Relay team
            </a>
          </p>
          <p>
            Part of <a href="http://todomvc.com">TodoMVC</a>
          </p>
        </footer>
      </div>
    );
  }
}

export default RelaySubscriptions.createContainer(TodoApp, {
  fragments: {
    viewer: () => Relay.QL`
      fragment on User {
        totalCount
        ${AddTodoMutation.getFragment('viewer')}
        ${TodoListFooter.getFragment('viewer')}
        ${AddTodoSubscription.getFragment('viewer')}
        ${RemoveTodoSubscription.getFragment('viewer')}
      }
    `,
  },

  subscriptions: [
    ({ viewer }) => new AddTodoSubscription({ viewer }),
    ({ viewer }) => new RemoveTodoSubscription({ viewer }),
  ],
});


================================================
FILE: examples/todo/js/components/TodoList.js
================================================
/**
 * This file provided by Facebook is for non-commercial testing and evaluation
 * purposes only.  Facebook reserves all rights not expressly granted.
 *
 * 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
 * FACEBOOK 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.
 */

import React from 'react';
import Relay from 'react-relay/classic';

import MarkAllTodosMutation from '../mutations/MarkAllTodosMutation';
import Todo from './Todo';

class TodoList extends React.Component {
  static propTypes = {
    viewer: React.PropTypes.object.isRequired,
    relay: React.PropTypes.object.isRequired,
  };

  _handleMarkAllChange = (e) => {
    const { relay, viewer } = this.props;
    relay.commitUpdate(
      new MarkAllTodosMutation({
        todos: viewer.todos,
        viewer,
        complete: e.target.checked,
      }),
    );
  };

  render() {
    const { viewer, relay } = this.props;
    const numTodos = viewer.totalCount;
    const numCompletedTodos = viewer.completedCount;

    return (
      <section className="main">
        <input
          checked={numTodos === numCompletedTodos}
          className="toggle-all"
          onChange={this._handleMarkAllChange}
          type="checkbox"
        />
        <label htmlFor="toggle-all">
          Mark all as complete
        </label>
        <ul className="todo-list">
          {viewer.todos.edges.map(edge => (
            <Todo
              key={edge.node.id}
              todo={edge.node}
              viewer={viewer}
              pending={relay.hasOptimisticUpdate(edge)}
            />
          ))}
        </ul>
      </section>
    );
  }
}

export default Relay.createContainer(TodoList, {
  initialVariables: {
    status: null,
  },

  prepareVariables({ status }) {
    let nextStatus;
    if (status === 'active' || status === 'completed') {
      nextStatus = status;
    } else {
      // This matches the Backbone example, which displays all todos on an
      // invalid route.
      nextStatus = 'any';
    }
    return {
      status: nextStatus,
    };
  },

  fragments: {
    viewer: () => Relay.QL`
      fragment on User {
        completedCount
        todos(
          status: $status
          first: 2147483647  # max GraphQLInt
        ) {
          edges {
            node {
              id
              ${Todo.getFragment('todo')}
            }
          }
          ${MarkAllTodosMutation.getFragment('todos')}
        }
        totalCount
        ${MarkAllTodosMutation.getFragment('viewer')}
        ${Todo.getFragment('viewer')}
      }
    `,
  },
});


================================================
FILE: examples/todo/js/components/TodoListFooter.js
================================================
/**
 * This file provided by Facebook is for non-commercial testing and evaluation
 * purposes only.  Facebook reserves all rights not expressly granted.
 *
 * 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
 * FACEBOOK 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.
 */

import React from 'react';
import Relay from 'react-relay/classic';
import { IndexLink, Link } from 'react-router';

import RemoveCompletedTodosMutation
  from '../mutations/RemoveCompletedTodosMutation';

class TodoListFooter extends React.Component {
  static propTypes = {
    viewer: React.PropTypes.object.isRequired,
    relay: React.PropTypes.object.isRequired,
  };

  _handleRemoveCompletedTodosClick = () => {
    const { relay, viewer } = this.props;
    relay.commitUpdate(
      new RemoveCompletedTodosMutation({
        todos: viewer.todos,
        viewer,
      }),
    );
  };

  render() {
    const { viewer } = this.props;
    const numCompletedTodos = viewer.completedCount;
    const numRemainingTodos = viewer.totalCount - numCompletedTodos;

    return (
      <footer className="footer">
        <span className="todo-count">
          <strong>{numRemainingTodos}</strong> item{numRemainingTodos === 1 ? '' : 's'} left
        </span>
        <ul className="filters">
          <li>
            <IndexLink to="/" activeClassName="selected">All</IndexLink>
          </li>
          <li>
            <Link to="/active" activeClassName="selected">Active</Link>
          </li>
          <li>
            <Link to="/completed" activeClassName="selected">Completed</Link>
          </li>
        </ul>
        {numCompletedTodos > 0 && (
          <button
            className="clear-completed"
            onClick={this._handleRemoveCompletedTodosClick}
          >
            Clear completed
          </button>
        )}
      </footer>
    );
  }
}

export default Relay.createContainer(TodoListFooter, {
  fragments: {
    viewer: () => Relay.QL`
      fragment on User {
        completedCount
        todos(
          status: "completed"
          first: 2147483647  # max GraphQLInt
        ) {
          ${RemoveCompletedTodosMutation.getFragment('todos')}
        }
        totalCount
        ${RemoveCompletedTodosMutation.getFragment('viewer')}
      }
    `,
  },
});


================================================
FILE: examples/todo/js/components/TodoTextInput.js
================================================
/**
 * This file provided by Facebook is for non-commercial testing and evaluation
 * purposes only.  Facebook reserves all rights not expressly granted.
 *
 * 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
 * FACEBOOK 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.
 */

import React, { PropTypes } from 'react';
import ReactDOM from 'react-dom';

const ENTER_KEY_CODE = 13;
const ESC_KEY_CODE = 27;

export default class TodoTextInput extends React.Component {
  static propTypes = {
    className: PropTypes.string,
    commitOnBlur: PropTypes.bool.isRequired,
    initialValue: PropTypes.string,
    onCancel: PropTypes.func,
    onDelete: PropTypes.func,
    onSave: PropTypes.func.isRequired,
    placeholder: PropTypes.string,
  };

  static defaultProps = {
    commitOnBlur: false,
  };

  state = {
    isEditing: false,
    text: this.props.initialValue || '',
  };

  componentDidMount() {
    ReactDOM.findDOMNode(this).focus();
  }

  _commitChanges = () => {
    const newText = this.state.text.trim();
    if (this.props.onDelete && newText === '') {
      this.props.onDelete();
    } else if (this.props.onCancel && newText === this.props.initialValue) {
      this.props.onCancel();
    } else if (newText !== '') {
      this.props.onSave(newText);
      this.setState({ text: '' });
    }
  };

  _handleBlur = () => {
    if (this.props.commitOnBlur) {
      this._commitChanges();
    }
  };

  _handleChange = (e) => {
    this.setState({ text: e.target.value });
  };

  _handleKeyDown = (e) => {
    if (this.props.onCancel && e.keyCode === ESC_KEY_CODE) {
      this.props.onCancel();
    } else if (e.keyCode === ENTER_KEY_CODE) {
      this._commitChanges();
    }
  };

  render() {
    return (
      <input
        className={this.props.className}
        onBlur={this._handleBlur}
        onChange={this._handleChange}
        onKeyDown={this._handleKeyDown}
        placeholder={this.props.placeholder}
        value={this.state.text}
      />
    );
  }
}


================================================
FILE: examples/todo/js/mutations/AddTodoMutation.js
================================================
/**
 * This file provided by Facebook is for non-commercial testing and evaluation
 * purposes only.  Facebook reserves all rights not expressly granted.
 *
 * 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
 * FACEBOOK 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.
 */

import Relay from 'react-relay/classic';

export default class AddTodoMutation extends Relay.Mutation {
  static fragments = {
    viewer: () => Relay.QL`
      fragment on User {
        id
        totalCount
      }
    `,
  };

  getMutation() {
    return Relay.QL`mutation { addTodo }`;
  }

  getFatQuery() {
    return Relay.QL`
      fragment on AddTodoPayload @relay(pattern: true) {
        todoEdge
        viewer {
          todos
          totalCount
        }
      }
    `;
  }

  getConfigs() {
    return [{
      type: 'RANGE_ADD',
      parentName: 'viewer',
      parentID: this.props.viewer.id,
      connectionName: 'todos',
      edgeName: 'todoEdge',
      rangeBehaviors: ({ status }) => (
        status === 'completed' ? 'ignore' : 'append'
      ),
    }];
  }

  getVariables() {
    return {
      text: this.props.text,
    };
  }

  getOptimisticResponse() {
    return {
      // FIXME: totalCount gets updated optimistically, but this edge does not
      // get added until the server responds
      todoEdge: {
        node: {
          complete: false,
          text: this.props.text,
        },
      },
      viewer: {
        id: this.props.viewer.id,
        totalCount: this.props.viewer.totalCount + 1,
      },
    };
  }
}


================================================
FILE: examples/todo/js/mutations/ChangeTodoStatusMutation.js
================================================
/**
 * This file provided by Facebook is for non-commercial testing and evaluation
 * purposes only.  Facebook reserves all rights not expressly granted.
 *
 * 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
 * FACEBOOK 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.
 */

import Relay from 'react-relay/classic';

export default class ChangeTodoStatusMutation extends Relay.Mutation {
  static fragments = {
    todo: () => Relay.QL`
      fragment on Todo {
        id,
      }
    `,
    // TODO: Mark completedCount optional
    viewer: () => Relay.QL`
      fragment on User {
        id,
        completedCount,
      }
    `,
  };
  getMutation() {
    return Relay.QL`mutation{changeTodoStatus}`;
  }
  getFatQuery() {
    return Relay.QL`
      fragment on ChangeTodoStatusPayload @relay(pattern: true) {
        todo {
          complete,
        },
        viewer {
          completedCount,
          todos,
        },
      }
    `;
  }
  getConfigs() {
    return [{
      type: 'FIELDS_CHANGE',
      fieldIDs: {
        todo: this.props.todo.id,
        viewer: this.props.viewer.id,
      },
    }];
  }
  getVariables() {
    return {
      complete: this.props.complete,
      id: this.props.todo.id,
    };
  }
  getOptimisticResponse() {
    const viewerPayload = { id: this.props.viewer.id };
    if (this.props.viewer.completedCount != null) {
      viewerPayload.completedCount = this.props.complete ?
        this.props.viewer.completedCount + 1 :
        this.props.viewer.completedCount - 1;
    }
    return {
      todo: {
        complete: this.props.complete,
        id: this.props.todo.id,
      },
      viewer: viewerPayload,
    };
  }
}


================================================
FILE: examples/todo/js/mutations/MarkAllTodosMutation.js
================================================
/**
 * This file provided by Facebook is for non-commercial testing and evaluation
 * purposes only.  Facebook reserves all rights not expressly granted.
 *
 * 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
 * FACEBOOK 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.
 */

import Relay from 'react-relay/classic';

export default class MarkAllTodosMutation extends Relay.Mutation {
  static fragments = {
    // TODO: Mark edges and totalCount optional
    todos: () => Relay.QL`
      fragment on TodoConnection {
        edges {
          node {
            complete,
            id,
          },
        },
      }
    `,
    viewer: () => Relay.QL`
      fragment on User {
        id,
        totalCount,
      }
    `,
  };
  getMutation() {
    return Relay.QL`mutation{markAllTodos}`;
  }
  getFatQuery() {
    return Relay.QL`
      fragment on MarkAllTodosPayload @relay(pattern: true) {
        viewer {
          completedCount,
          todos,
        },
      }
    `;
  }
  getConfigs() {
    return [{
      type: 'FIELDS_CHANGE',
      fieldIDs: {
        viewer: this.props.viewer.id,
      },
    }];
  }
  getVariables() {
    return {
      complete: this.props.complete,
    };
  }
  getOptimisticResponse() {
    const viewerPayload = { id: this.props.viewer.id };
    if (this.props.todos && this.props.todos.edges) {
      viewerPayload.todos = {
        edges: this.props.todos.edges
          .filter(edge => edge.node.complete !== this.props.complete)
          .map(edge => ({
            node: {
              complete: this.props.complete,
              id: edge.node.id,
            },
          })),
      };
    }
    if (this.props.viewer.totalCount != null) {
      viewerPayload.completedCount = this.props.complete ?
        this.props.viewer.totalCount :
        0;
    }
    return {
      viewer: viewerPayload,
    };
  }
}


================================================
FILE: examples/todo/js/mutations/RemoveCompletedTodosMutation.js
================================================
/**
 * This file provided by Facebook is for non-commercial testing and evaluation
 * purposes only.  Facebook reserves all rights not expressly granted.
 *
 * 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
 * FACEBOOK 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.
 */

import Relay from 'react-relay/classic';

export default class RemoveCompletedTodosMutation extends Relay.Mutation {
  static fragments = {
    // TODO: Make completedCount, edges, and totalCount optional
    todos: () => Relay.QL`
      fragment on TodoConnection {
        edges {
          node {
            complete,
            id,
          },
        },
      }
    `,
    viewer: () => Relay.QL`
      fragment on User {
        completedCount,
        id,
        totalCount,
      }
    `,
  };
  getMutation() {
    return Relay.QL`mutation{removeCompletedTodos}`;
  }
  getFatQuery() {
    return Relay.QL`
      fragment on RemoveCompletedTodosPayload @relay(pattern: true) {
        deletedTodoIds,
        viewer {
          completedCount,
          totalCount,
        },
      }
    `;
  }
  getConfigs() {
    return [{
      type: 'NODE_DELETE',
      parentName: 'viewer',
      parentID: this.props.viewer.id,
      connectionName: 'todos',
      deletedIDFieldName: 'deletedTodoIds',
    }];
  }
  getVariables() {
    return {};
  }
  getOptimisticResponse() {
    let deletedTodoIds;
    let newTotalCount;
    if (this.props.todos && this.props.todos.edges) {
      deletedTodoIds = this.props.todos.edges
        .filter(edge => edge.node.complete)
        .map(edge => edge.node.id);
    }
    const { completedCount, totalCount } = this.props.viewer;
    if (completedCount != null && totalCount != null) {
      newTotalCount = totalCount - completedCount;
    }
    return {
      deletedTodoIds,
      viewer: {
        completedCount: 0,
        id: this.props.viewer.id,
        totalCount: newTotalCount,
      },
    };
  }
}


================================================
FILE: examples/todo/js/mutations/RemoveTodoMutation.js
================================================
/**
 * This file provided by Facebook is for non-commercial testing and evaluation
 * purposes only.  Facebook reserves all rights not expressly granted.
 *
 * 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
 * FACEBOOK 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.
 */

import Relay from 'react-relay/classic';

export default class RemoveTodoMutation extends Relay.Mutation {
  static fragments = {
    // TODO: Mark complete as optional
    todo: () => Relay.QL`
      fragment on Todo {
        complete,
        id,
      }
    `,
    // TODO: Mark completedCount and totalCount as optional
    viewer: () => Relay.QL`
      fragment on User {
        completedCount,
        id,
        totalCount,
      }
    `,
  };
  getMutation() {
    return Relay.QL`mutation{removeTodo}`;
  }
  getFatQuery() {
    return Relay.QL`
      fragment on RemoveTodoPayload @relay(pattern: true) {
        deletedTodoId,
        viewer {
          completedCount,
          totalCount,
        },
      }
    `;
  }
  getConfigs() {
    return [{
      type: 'NODE_DELETE',
      parentName: 'viewer',
      parentID: this.props.viewer.id,
      connectionName: 'todos',
      deletedIDFieldName: 'deletedTodoId',
    }];
  }
  getVariables() {
    return {
      id: this.props.todo.id,
    };
  }
  getOptimisticResponse() {
    const viewerPayload = { id: this.props.viewer.id };
    if (this.props.viewer.completedCount != null) {
      viewerPayload.completedCount = this.props.todo.complete === true ?
        this.props.viewer.completedCount - 1 :
        this.props.viewer.completedCount;
    }
    if (this.props.viewer.totalCount != null) {
      viewerPayload.totalCount = this.props.viewer.totalCount - 1;
    }
    return {
      deletedTodoId: this.props.todo.id,
      viewer: viewerPayload,
    };
  }
}


================================================
FILE: examples/todo/js/mutations/RenameTodoMutation.js
================================================
/**
 * This file provided by Facebook is for non-commercial testing and evaluation
 * purposes only.  Facebook reserves all rights not expressly granted.
 *
 * 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
 * FACEBOOK 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.
 */

import Relay from 'react-relay/classic';

export default class RenameTodoMutation extends Relay.Mutation {
  static fragments = {
    todo: () => Relay.QL`
      fragment on Todo {
        id,
      }
    `,
  };
  getMutation() {
    return Relay.QL`mutation{renameTodo}`;
  }
  getFatQuery() {
    return Relay.QL`
      fragment on RenameTodoPayload @relay(pattern: true) {
        todo {
          text,
        }
      }
    `;
  }
  getConfigs() {
    return [{
      type: 'FIELDS_CHANGE',
      fieldIDs: {
        todo: this.props.todo.id,
      },
    }];
  }
  getVariables() {
    return {
      id: this.props.todo.id,
      text: this.props.text,
    };
  }
  getOptimisticResponse() {
    return {
      todo: {
        id: this.props.todo.id,
        text: this.props.text,
      },
    };
  }
}


================================================
FILE: examples/todo/js/queries/ViewerQueries.js
================================================
/**
 * This file provided by Facebook is for non-commercial testing and evaluation
 * purposes only.  Facebook reserves all rights not expressly granted.
 *
 * 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
 * FACEBOOK 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.
 */

import Relay from 'react-relay/classic';

export default {
  viewer: () => Relay.QL`query { viewer }`,
};


================================================
FILE: examples/todo/js/subscriptions/AddTodoSubscription.js
================================================
import Relay from 'react-relay/classic';
import { Subscription } from 'relay-subscriptions';

import Todo from '../components/Todo';

export default class AddTodoSubscription extends Subscription {
  static fragments = {
    viewer: () => Relay.QL`
      fragment on User {
        id
      }
    `,
  };

  getSubscription() {
    return Relay.QL`
      subscription {
        addTodoSubscription(input: $input) {
          todoEdge {
            __typename
            node {
              ${Todo.getFragment('todo')}
            }
          }
          viewer {
            id
            totalCount
          }
        }
      }
    `;
  }

  getConfigs() {
    return [{
      type: 'RANGE_ADD',
      parentName: 'viewer',
      parentID: this.props.viewer.id,
      connectionName: 'todos',
      edgeName: 'todoEdge',
      rangeBehaviors: () => 'append',
    }];
  }

  getVariables() {
    return {};
  }
}


================================================
FILE: examples/todo/js/subscriptions/RemoveTodoSubscription.js
================================================
import Relay from 'react-relay/classic';
import { Subscription } from 'relay-subscriptions';

export default class RemoveTodoSubscription extends Subscription {
  static fragments = {
    viewer: () => Relay.QL`
      fragment on User {
        id
      }
    `,
  };

  getSubscription() {
    return Relay.QL`
      subscription {
        removeTodoSubscription(input: $input) {
          deletedTodoId
          viewer {
            completedCount
            totalCount
          }
        }
      }
    `;
  }

  getConfigs() {
    return [{
      type: 'NODE_DELETE',
      parentName: 'viewer',
      parentID: this.props.viewer.id,
      connectionName: 'todos',
      deletedIDFieldName: 'deletedTodoId',
    }];
  }

  getVariables() {
    return {};
  }
}


================================================
FILE: examples/todo/js/subscriptions/UpdateTodoSubscription.js
================================================
import Relay from 'react-relay/classic';
import { Subscription } from 'relay-subscriptions';

import Todo from '../components/Todo';

export default class UpdateTodoSubscription extends Subscription {
  static fragments = {
    todo: () => Relay.QL`
      fragment on Todo {
        id
      }
    `,
  };

  getSubscription() {
    return Relay.QL`
      subscription {
        updateTodoSubscription(input: $input) {
          todo {
            ${Todo.getFragment('todo')}
          }
          viewer {
            id
            completedCount
          }
        }
      }
    `;
  }

  getConfigs() {
    return [{
      type: 'FIELDS_CHANGE',
      fieldIDs: {
        todo: this.props.todo.id,
      },
    }];
  }

  getVariables() {
    return {
      id: this.props.todo.id,
    };
  }
}


================================================
FILE: examples/todo/package.json
================================================
{
  "private": true,
  "scripts": {
    "start": "babel-node server.js",
    "update-schema": "babel-node tools/updateSchema.js"
  },
  "dependencies": {
    "babel-cli": "^6.26.0",
    "babel-core": "^6.26.0",
    "babel-loader": "^7.1.2",
    "babel-plugin-relay": "^1.4.1",
    "babel-polyfill": "^6.26.0",
    "babel-preset-env": "^1.6.1",
    "babel-preset-react": "^6.24.1",
    "babel-preset-stage-2": "^6.24.1",
    "classnames": "^2.2.5",
    "express": "^4.16.2",
    "express-graphql": "^0.6.11",
    "graphql": "^0.11.7",
    "graphql-relay": "^0.5.3",
    "graphql-relay-subscription": "^0.2.0",
    "history": "^2.1.2",
    "react": "^15.5.4",
    "react-dom": "^15.5.4",
    "react-relay": "^1.4.1",
    "react-router": "^2.8.1",
    "react-router-relay": "^0.14.0",
    "relay-subscriptions": "^2.0.2",
    "subscriptions-transport-ws": "^0.9.4",
    "todomvc-app-css": "^2.1.0",
    "todomvc-common": "^1.0.4",
    "webpack": "^3.8.1",
    "webpack-dev-server": "^2.9.4"
  }
}


================================================
FILE: examples/todo/public/base.css
================================================
hr {
	margin: 20px 0;
	border: 0;
	border-top: 1px dashed #c5c5c5;
	border-bottom: 1px dashed #f7f7f7;
}

.learn a {
	font-weight: normal;
	text-decoration: none;
	color: #b83f45;
}

.learn a:hover {
	text-decoration: underline;
	color: #787e7e;
}

.learn h3,
.learn h4,
.learn h5 {
	margin: 10px 0;
	font-weight: 500;
	line-height: 1.2;
	color: #000;
}

.learn h3 {
	font-size: 24px;
}

.learn h4 {
	font-size: 18px;
}

.learn h5 {
	margin-bottom: 0;
	font-size: 14px;
}

.learn ul {
	padding: 0;
	margin: 0 0 30px 25px;
}

.learn li {
	line-height: 20px;
}

.learn p {
	font-size: 15px;
	font-weight: 300;
	line-height: 1.3;
	margin-top: 0;
	margin-bottom: 0;
}

#issue-count {
	display: none;
}

.quote {
	border: none;
	margin: 20px 0 60px 0;
}

.quote p {
	font-style: italic;
}

.quote p:before {
	content: '“';
	font-size: 50px;
	opacity: .15;
	position: absolute;
	top: -20px;
	left: 3px;
}

.quote p:after {
	content: '”';
	font-size: 50px;
	opacity: .15;
	position: absolute;
	bottom: -42px;
	right: 3px;
}

.quote footer {
	position: absolute;
	bottom: -40px;
	right: 0;
}

.quote footer img {
	border-radius: 3px;
}

.quote footer a {
	margin-left: 5px;
	vertical-align: middle;
}

.speech-bubble {
	position: relative;
	padding: 10px;
	background: rgba(0, 0, 0, .04);
	border-radius: 5px;
}

.speech-bubble:after {
	content: '';
	position: absolute;
	top: 100%;
	right: 30px;
	border: 13px solid transparent;
	border-top-color: rgba(0, 0, 0, .04);
}

.learn-bar > .learn {
	position: absolute;
	width: 272px;
	top: 8px;
	left: -300px;
	padding: 10px;
	border-radius: 5px;
	background-color: rgba(255, 255, 255, .6);
	transition-property: left;
	transition-duration: 500ms;
}

@media (min-width: 899px) {
	.learn-bar {
		width: auto;
		padding-left: 300px;
	}

	.learn-bar > .learn {
		left: 8px;
	}
}


================================================
FILE: examples/todo/public/index.css
================================================
html,
body {
	margin: 0;
	padding: 0;
}

button {
	margin: 0;
	padding: 0;
	border: 0;
	background: none;
	font-size: 100%;
	vertical-align: baseline;
	font-family: inherit;
	font-weight: inherit;
	color: inherit;
	-webkit-appearance: none;
	appearance: none;
	-webkit-font-smoothing: antialiased;
	-moz-osx-font-smoothing: grayscale;
}

body {
	font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
	line-height: 1.4em;
	background: #f5f5f5;
	color: #4d4d4d;
	min-width: 230px;
	max-width: 550px;
	margin: 0 auto;
	-webkit-font-smoothing: antialiased;
	-moz-osx-font-smoothing: grayscale;
	font-weight: 300;
}

:focus {
	outline: 0;
}

.hidden {
	display: none;
}

.todoapp {
	background: #fff;
	margin: 130px 0 40px 0;
	position: relative;
	box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
	            0 25px 50px 0 rgba(0, 0, 0, 0.1);
}

.todoapp input::-webkit-input-placeholder {
	font-style: italic;
	font-weight: 300;
	color: #e6e6e6;
}

.todoapp input::-moz-placeholder {
	font-style: italic;
	font-weight: 300;
	color: #e6e6e6;
}

.todoapp input::input-placeholder {
	font-style: italic;
	font-weight: 300;
	color: #e6e6e6;
}

.todoapp h1 {
	position: absolute;
	top: -155px;
	width: 100%;
	font-size: 100px;
	font-weight: 100;
	text-align: center;
	color: rgba(175, 47, 47, 0.15);
	-webkit-text-rendering: optimizeLegibility;
	-moz-text-rendering: optimizeLegibility;
	text-rendering: optimizeLegibility;
}

.new-todo,
.edit {
	position: relative;
	margin: 0;
	width: 100%;
	font-size: 24px;
	font-family: inherit;
	font-weight: inherit;
	line-height: 1.4em;
	border: 0;
	color: inherit;
	padding: 6px;
	border: 1px solid #999;
	box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
	box-sizing: border-box;
	-webkit-font-smoothing: antialiased;
	-moz-osx-font-smoothing: grayscale;
}

.new-todo {
	padding: 16px 16px 16px 60px;
	border: none;
	background: rgba(0, 0, 0, 0.003);
	box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
}

.main {
	position: relative;
	z-index: 2;
	border-top: 1px solid #e6e6e6;
}

label[for='toggle-all'] {
	display: none;
}

.toggle-all {
	position: absolute;
	top: -55px;
	left: -12px;
	width: 60px;
	height: 34px;
	text-align: center;
	border: none; /* Mobile Safari */
}

.toggle-all:before {
	content: '❯';
	font-size: 22px;
	color: #e6e6e6;
	padding: 10px 27px 10px 27px;
}

.toggle-all:checked:before {
	color: #737373;
}

.todo-list {
	margin: 0;
	padding: 0;
	list-style: none;
}

.todo-list li {
	position: relative;
	font-size: 24px;
	border-bottom: 1px solid #ededed;
}

.todo-list li:last-child {
	border-bottom: none;
}

.todo-list li.editing {
	border-bottom: none;
	padding: 0;
}

.todo-list li.editing .edit {
	display: block;
	width: 506px;
	padding: 12px 16px;
	margin: 0 0 0 43px;
}

.todo-list li.editing .view {
	display: none;
}

.todo-list li .toggle {
	text-align: center;
	width: 40px;
	/* auto, since non-WebKit browsers doesn't support input styling */
	height: auto;
	position: absolute;
	top: 0;
	bottom: 0;
	margin: auto 0;
	border: none; /* Mobile Safari */
	-webkit-appearance: none;
	appearance: none;
}

.todo-list li .toggle:after {
	content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#ededed" stroke-width="3"/></svg>');
}

.todo-list li .toggle:checked:after {
	content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#bddad5" stroke-width="3"/><path fill="#5dc2af" d="M72 25L42 71 27 56l-4 4 20 20 34-52z"/></svg>');
}

.todo-list li label {
	word-break: break-all;
	padding: 15px 60px 15px 15px;
	margin-left: 45px;
	display: block;
	line-height: 1.2;
	transition: color 0.4s;
}

.todo-list li.completed label {
	color: #d9d9d9;
	text-decoration: line-through;
}

.todo-list li .destroy {
	display: none;
	position: absolute;
	top: 0;
	right: 10px;
	bottom: 0;
	width: 40px;
	height: 40px;
	margin: auto 0;
	font-size: 30px;
	color: #cc9a9a;
	margin-bottom: 11px;
	transition: color 0.2s ease-out;
}

.todo-list li .destroy:hover {
	color: #af5b5e;
}

.todo-list li .destroy:after {
	content: '×';
}

.todo-list li:hover .destroy {
	display: block;
}

.todo-list li .edit {
	display: none;
}

.todo-list li.editing:last-child {
	margin-bottom: -1px;
}

.footer {
	color: #777;
	padding: 10px 15px;
	height: 20px;
	text-align: center;
	border-top: 1px solid #e6e6e6;
}

.footer:before {
	content: '';
	position: absolute;
	right: 0;
	bottom: 0;
	left: 0;
	height: 50px;
	overflow: hidden;
	box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
	            0 8px 0 -3px #f6f6f6,
	            0 9px 1px -3px rgba(0, 0, 0, 0.2),
	            0 16px 0 -6px #f6f6f6,
	            0 17px 2px -6px rgba(0, 0, 0, 0.2);
}

.todo-count {
	float: left;
	text-align: left;
}

.todo-count strong {
	font-weight: 300;
}

.filters {
	margin: 0;
	padding: 0;
	list-style: none;
	position: absolute;
	right: 0;
	left: 0;
}

.filters li {
	display: inline;
}

.filters li a {
	color: inherit;
	margin: 3px;
	padding: 3px 7px;
	text-decoration: none;
	border: 1px solid transparent;
	border-radius: 3px;
}

.filters li a:hover {
	border-color: rgba(175, 47, 47, 0.1);
}

.filters li a.selected {
	border-color: rgba(175, 47, 47, 0.2);
}

.clear-completed,
html .clear-completed:active {
	float: right;
	position: relative;
	line-height: 20px;
	text-decoration: none;
	cursor: pointer;
}

.clear-completed:hover {
	text-decoration: underline;
}

.info {
	margin: 65px auto 0;
	color: #bfbfbf;
	font-size: 10px;
	text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
	text-align: center;
}

.info p {
	line-height: 1;
}

.info a {
	color: inherit;
	text-decoration: none;
	font-weight: 400;
}

.info a:hover {
	text-decoration: underline;
}

/*
	Hack to remove background from Mobile Safari.
	Can't use it globally since it destroys checkboxes in Firefox
*/
@media screen and (-webkit-min-device-pixel-ratio:0) {
	.toggle-all,
	.todo-list li .toggle {
		background: none;
	}

	.todo-list li .toggle {
		height: 40px;
	}

	.toggle-all {
		-webkit-transform: rotate(90deg);
		transform: rotate(90deg);
		-webkit-appearance: none;
		appearance: none;
	}
}

@media (max-width: 430px) {
	.footer {
		height: 50px;
	}

	.filters {
		bottom: 10px;
	}
}


================================================
FILE: examples/todo/public/index.html
================================================
<!doctype html>
<html lang="en" data-framework="relay">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Relay • TodoMVC</title>
    <link rel="stylesheet" href="base.css">
    <link rel="stylesheet" href="index.css">
  </head>
  <body>
    <div id="root"></div>
    <script type="text/javascript">
      // Force `fetch` polyfill to workaround Chrome not displaying request body
      // in developer tools for the native `fetch`.
      self.fetch = null;
    </script>
    <script src="http://localhost:3000/webpack-dev-server.js"></script>
    <script src="js/app.js"></script>
  </body>
</html>


================================================
FILE: examples/todo/public/learn.json
================================================
{
  "relay": {
    "name": "Relay",
    "description": "A JavaScript framework for building data-driven React applications",
    "homepage": "facebook.github.io/relay/",
    "examples": [{
      "name": "Relay + express-graphql Example",
      "url": "",
      "source_url": "https://github.com/facebook/relay/tree/master/examples/todo",
      "type": "backend"
    }],
    "link_groups": [{
      "heading": "Official Resources",
      "links": [{
        "name": "Documentation",
        "url": "https://facebook.github.io/relay/docs/getting-started.html"
      }, {
        "name": "API Reference",
        "url": "https://facebook.github.io/relay/docs/api-reference-relay.html"
      }, {
        "name": "Relay on GitHub",
        "url": "https://github.com/facebook/relay"
      }]
    }, {
      "heading": "Community",
      "links": [{
        "name": "Relay on StackOverflow",
        "url": "https://stackoverflow.com/questions/tagged/relayjs"
      }]
    }]
  },
  "templates": {
    "todomvc": "<header> <h3><%= name %></h3> <span class=\"source-links\"> <% if (typeof examples !== 'undefined') { %> <% examples.forEach(function (example) { %> <h5><%= example.name %></h5> <% if (!location.href.match(example.url + '/')) { %> <a class=\"demo-link\" data-type=\"<%= example.type === 'backend' ? 'external' : 'local' %>\" href=\"<%= example.url %>\">Demo</a>, <% } if (example.type === 'backend') { %><a href=\"<%= example.source_url %>\"><% } else { %><a href=\"https://github.com/tastejs/todomvc/tree/gh-pages/<%= example.source_url ? example.source_url : example.url %>\"><% } %>Source</a> <% }); %> <% } %> </span> </header> <hr> <blockquote class=\"quote speech-bubble\"> <p><%= description %></p> <footer> <a href=\"http://<%= homepage %>\"><%= name %></a> </footer> </blockquote> <% if (typeof link_groups !== 'undefined') { %> <hr> <% link_groups.forEach(function (link_group) { %> <h4><%= link_group.heading %></h4> <ul> <% link_group.links.forEach(function (link) { %> <li> <a href=\"<%= link.url %>\"><%= link.name %></a> </li> <% }); %> </ul> <% }); %> <% } %> <footer> <hr> <em>If you have other helpful links to share, or find any of the links above no longer work, please <a href=\"https://github.com/tastejs/todomvc/issues\">let us know</a>.</em> </footer>"
  }
}


================================================
FILE: examples/todo/server.js
================================================
/**
 * This file provided by Facebook is for non-commercial testing and evaluation
 * purposes only.  Facebook reserves all rights not expressly granted.
 *
 * 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
 * FACEBOOK 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.
 */

/* eslint-disable no-console */

import express from 'express';
import graphQLHTTP from 'express-graphql';
import { execute, subscribe } from 'graphql';
import path from 'path';
import { SubscriptionServer } from 'subscriptions-transport-ws';
import webpack from 'webpack';
import WebpackDevServer from 'webpack-dev-server';

import { addNotifier } from './data/database';
import { schema } from './data/schema';

const APP_PORT = 3000;
const GRAPHQL_PORT = 8080;

// Expose a GraphQL endpoint
const graphQLApp = express();
graphQLApp.use('/', graphQLHTTP({ schema, pretty: true, graphiql: true }));

const graphQLServer = graphQLApp.listen(GRAPHQL_PORT, () => {
  console.log(
    `GraphQL Server is now running on http://localhost:${GRAPHQL_PORT}`
  );
});

class AsyncQueue {
  constructor(unsubscribe) {
    this.unsubscribe = unsubscribe;

    this.values = [];
    this.createPromise();

    this.iterable = this.createIterable();
  }

  createPromise() {
    this.promise = new Promise((resolve) => {
      this.resolvePromise = resolve;
    });
  }

  async * createIterable() {
    try {
      while (true) { // eslint-disable-line no-constant-condition
        await this.promise; // eslint-disable-line no-unused-expressions

        for (const value of this.values) {
          yield value;
        }

        this.values.length = 0;
        this.createPromise();
      }
    } finally {
      this.unsubscribe();
    }
  }

  push(value) {
    this.values.push(value);
    this.resolvePromise();
  }
}

SubscriptionServer.create(
  {
    schema,
    execute,
    subscribe,

    onConnect: (payload, socket) => {
      const topics = Object.create(null);

      // eslint-disable-next-line no-param-reassign
      socket.removeNotifier = addNotifier(({ topic, data }) => {
        const topicQueues = topics[topic];
        if (!topicQueues) {
          return;
        }

        topicQueues.forEach((queue) => {
          queue.push(data);
        });
      });

      function unsubscribe(topic, queue) {
        const topicQueues = topics[topic];

        const index = topicQueues.indexOf(queue);
        if (index === -1) {
          return;
        }

        topicQueues.splice(index, 1);
        console.log('removed subscription for %s', topic);
      }

      return {
        subscribe: (topic) => {
          if (!topics[topic]) {
            topics[topic] = [];
          }

          const queue = new AsyncQueue(() => {
            unsubscribe(topic, queue);
          });

          topics[topic].push(queue);
          console.log('added subscription for %s', topic);

          return queue.iterable;
        },
      };
    },

    onDisconnect: (socket) => {
      console.log('socket disconnect');
      socket.removeNotifier();
    },
  },
  {
    server: graphQLServer,
  },
);

// Serve the Relay app.
const compiler = webpack({
  entry: [
    'babel-polyfill',
    './js/app.js',
  ],

  output: {
    path: '/',
    filename: 'app.js',
  },

  module: {
    rules: [
      { test: /\.js$/, exclude: /node_modules/, use: 'babel-loader' },
    ],
  },

  devtool: 'sourcemap',
});

const app = new WebpackDevServer(compiler, {
  contentBase: '/public/',
  proxy: {
    '/graphql': {
      target: `http://localhost:${GRAPHQL_PORT}`,
      ws: true,
    },
  },
  publicPath: '/js/',
  stats: { colors: true },
});

// Serve static resources
app.use('/', express.static(path.join(__dirname, 'public')));
app.listen(APP_PORT, () => {
  console.log(`App is now running on http://localhost:${APP_PORT}`);
});


================================================
FILE: examples/todo/tools/.eslintrc
================================================
{
  "rules": {
    "no-console": 0
  }
}


================================================
FILE: examples/todo/tools/updateSchema.js
================================================
#!/usr/bin/env babel-node
/**
 * This file provided by Facebook is for non-commercial testing and evaluation
 * purposes only.  Facebook reserves all rights not expressly granted.
 *
 * 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
 * FACEBOOK 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.
 */

import fs from 'fs';
import path from 'path';
import { schema } from '../data/schema';
import { printSchema } from 'graphql/utilities';

fs.writeFileSync(
  path.join(__dirname, '../data/schema.graphql'),
  printSchema(schema)
);


================================================
FILE: package.json
================================================
{
  "name": "relay-subscriptions",
  "version": "2.0.2",
  "description": "Subscription support for Relay",
  "main": "lib/index.js",
  "scripts": {
    "build": "rimraf lib && babel src --ignore __tests__ --out-dir lib",
    "check": "flow check src",
    "lint": "eslint examples src",
    "prepublish": "npm run build",
    "tdd": "jest --watch",
    "test": "npm run lint && npm run testonly",
    "testonly": "jest --runInBand --verbose",
    "watch": "npm run build -- --watch"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/edvinerikson/relay-subscriptions.git"
  },
  "files": [
    "lib"
  ],
  "keywords": [
    "relay",
    "react",
    "subscriptions"
  ],
  "author": "Edvin Eriksson <edvinerikson@gmail.com>",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/edvinerikson/relay-subscriptions/issues"
  },
  "homepage": "https://github.com/edvinerikson/relay-subscriptions#readme",
  "dependencies": {
    "invariant": "^2.2.2",
    "lodash": "^4.17.4",
    "prop-types": "^15.5.10",
    "warning": "^3.0.0"
  },
  "peerDependencies": {
    "react": "^0.14.9 || >=15.3.0",
    "react-relay": ">=1.0.0"
  },
  "devDependencies": {
    "babel-cli": "^6.24.1",
    "babel-eslint": "^7.2.3",
    "babel-jest": "^20.0.3",
    "babel-plugin-add-module-exports": "^0.2.1",
    "babel-plugin-dev-expression": "^0.2.1",
    "babel-preset-env": "^1.5.1",
    "babel-preset-react": "^6.24.1",
    "babel-preset-stage-2": "^6.24.1",
    "enzyme": "^2.8.2",
    "eslint": "^2.13.1",
    "eslint-config-airbnb": "^9.0.1",
    "eslint-plugin-flowtype": "^2.34.0",
    "eslint-plugin-import": "^1.14.0",
    "eslint-plugin-jsx-a11y": "^1.5.5",
    "eslint-plugin-react": "^5.2.2",
    "flow-bin": "^0.47.0",
    "jest": "^20.0.4",
    "react": "^15.5.4",
    "react-dom": "^15.5.4",
    "react-relay": "^1.0.0",
    "react-test-renderer": "^15.5.4",
    "rimraf": "^2.6.1"
  }
}


================================================
FILE: src/.flowconfig
================================================
[ignore]

[include]
../node_modules/invariant
../node_modules/lodash
../node_modules/react
../node_modules/react-relay
../node_modules/warning

[libs]

[options]
esproposal.class_static_fields=enable


================================================
FILE: src/Environment.js
================================================
/* @flow */

import invariant from 'invariant';
import Relay from 'react-relay/classic';
import RelayNetworkLayer from 'react-relay/lib/RelayNetworkLayer';
import RelayStoreData from 'react-relay/lib/RelayStoreData';

import createSubscriptionQuery from './createSubscriptionQuery';
import type Subscription from './Subscription';
import SubscriptionRequest from './SubscriptionRequest';
import type { SubscriptionDisposable, SubscriptionObserver } from './types';
import updateStoreData from './updateStoreData';

// Override a few Relay classes to use our own network layer proxy that
// supports sendSubscription.

class NetworkLayer extends RelayNetworkLayer {
  sendSubscription(
    subscriptionRequest: SubscriptionRequest,
  ): SubscriptionDisposable {
    const implementation = this._getImplementation();
    invariant(
      implementation.sendSubscription,
      'NetworkLayer: Network layer implementation does not support ' +
      'subscriptions.',
    );

    return implementation.sendSubscription(subscriptionRequest);
  }
}

class StoreData extends RelayStoreData {
  _networkLayer: NetworkLayer;

  constructor(...args) {
    super(...args);

    this._networkLayer = new NetworkLayer();
  }
}

export default class Environment extends Relay.Environment {
  startSubscription: (
    subscription: Subscription<any>,
    observer?: SubscriptionObserver,
  ) => SubscriptionDisposable;

  constructor(storeData?: StoreData) {
    super(storeData || new StoreData());

    this.startSubscription = this.startSubscription.bind(this);

    this._nextClientSubscriptionId = 0;
  }

  startSubscription(
    subscription: Subscription<any>,
    observer?: SubscriptionObserver,
  ): SubscriptionDisposable {
    const clientSubscriptionId = this._nextClientSubscriptionId.toString(36);
    ++this._nextClientSubscriptionId;

    subscription.bindEnvironment(this);

    const query = createSubscriptionQuery(subscription.getSubscription(), {
      input: {
        ...subscription.getVariables(),
        clientSubscriptionId,
      },
    });

    const onPayload = (payload) => {
      updateStoreData(this, subscription.getConfigs(), query, payload);
    };

    let requestObserver;
    if (observer) {
      const definedObserver = observer; // Placate Flow.
      requestObserver = {
        onNext: (payload) => {
          onPayload(payload);
          if (definedObserver.onNext) {
            definedObserver.onNext(payload);
          }
        },
        onError: (error) => {
          if (definedObserver.onError) {
            definedObserver.onError(error);
          }
        },
        onCompleted: (value) => {
          if (definedObserver.onCompleted) {
            definedObserver.onCompleted(value);
          }
        },
      };
    } else {
      requestObserver = {
        onNext: onPayload,
        onError: () => {},
        onCompleted: () => {},
      };
    }

    return this._storeData.getNetworkLayer().sendSubscription(
      new SubscriptionRequest(query, requestObserver),
    );
  }
}


================================================
FILE: src/Subscription.js
================================================
/* @flow */

import invariant from 'invariant';
import Relay from 'react-relay/classic';
import RelayMetaRoute from 'react-relay/lib/RelayMetaRoute';

import type {
  MutationConfig,
  RelayConcreteNode,
  Variables,
} from './types';

export default class Subscription<Tp: Object> {
  static name: string;
  static initialVariables: Variables;
  static prepareVariables: ?(
    prevVariables: Variables,
    route: RelayMetaRoute
  ) => Variables;

  props: Tp;
  _unresolvedProps: Tp;
  _environment: Relay.Environment;
  _didShowFakeDataWarning: boolean;
  _didValidateConfig: boolean;

  constructor(props: Tp) {
    this._didShowFakeDataWarning = false;
    this._didValidateConfig = false;
    this._unresolvedProps = props;
  }

  static getFragment(fragmentName: string, variableMapping): any {
    return Relay.Mutation.getFragment.call(
      this,
      fragmentName,
      variableMapping
    );
  }

  bindEnvironment(environment: Relay.Environment): void {
    Relay.Mutation.prototype.bindEnvironment.call(this, environment);
  }

  getSubscription(): RelayConcreteNode {
    invariant(
      false,
      '%s: Expected abstract method `getSubscription` to be implemented',
      this.constructor.name
    );
  }

  getConfigs(): Array<MutationConfig> {
    invariant(
      false,
      '%s: Expected abstract method `getConfigs` to be implemented',
      this.constructor.name
    );
  }

  getVariables(): Variables {
    invariant(
      false,
      '%s: Expected abstract method `getVariables` to be implemented',
      this.constructor.name
    );
  }

  _resolveProps(): void {
    Relay.Mutation.prototype._resolveProps.call(this);
  }
}


================================================
FILE: src/SubscriptionRequest.js
================================================
/* @flow */

import printQuery from 'react-relay/lib/printRelayQuery';
import RelayQuery from 'react-relay/lib/RelayQuery';

import type {
  PrintedQuery,
  SubscriptionRequestObserver,
  SubscriptionResult,
  Variables,
} from './types';

export default class SubscriptionRequest {
  _printedQuery: ?PrintedQuery;
  _subscription: RelayQuery.Subscription;
  _observer: SubscriptionRequestObserver;

  constructor(
    subscription: RelayQuery.Subscription,
    observer: SubscriptionRequestObserver,
  ) {
    this._printedQuery = null;
    this._subscription = subscription;
    this._observer = observer;
  }

  getDebugName(): string {
    return this._subscription.getName();
  }

  getVariables(): Variables {
    return this._getPrintedQuery().variables;
  }

  getQueryString(): string {
    return this._getPrintedQuery().text;
  }

  _getPrintedQuery(): PrintedQuery {
    if (!this._printedQuery) {
      this._printedQuery = printQuery(this._subscription);
    }

    return this._printedQuery;
  }

  getClientSubscriptionId(): string {
    return this._subscription.getVariables().input.clientSubscriptionId;
  }

  onNext(payload: SubscriptionResult) {
    this._observer.onNext(payload);
  }

  onError(error: any) {
    this._observer.onError(error);
  }

  onCompleted(value: any) {
    this._observer.onCompleted(value);
  }
}


================================================
FILE: src/__tests__/createContainer.js
================================================
jest.mock('react-relay/lib/RelayContainer', () => ({
  create: (Component) => {
    Component.isRelayContainer = true; // eslint-disable-line no-param-reassign
    return Component;
  },
}));

import { mount } from 'enzyme';
import PropTypes from 'prop-types';
import React from 'react';

import RelaySubscriptions from '..';

describe('createContainer', () => {
  it('should support relay.subscribe', () => {
    const environment = new RelaySubscriptions.Environment();
    spyOn(environment, 'startSubscription');

    const dummySubscription = new RelaySubscriptions.Subscription();

    class Widget extends React.Component {
      static propTypes = {
        relay: PropTypes.object.isRequired,
      };

      componentDidMount() {
        this.props.relay.subscribe(dummySubscription);
      }

      render() {
        return null;
      }
    }

    const WidgetContainer = RelaySubscriptions.createContainer(Widget, {});
    expect(WidgetContainer.isRelayContainer).toBe(true);

    mount(<WidgetContainer relay={{}} />, {
      context: {
        relay: {
          environment,
          variables: {},
        },
      },
    });
    expect(environment.startSubscription).toHaveBeenCalledWith(dummySubscription);
  });
});


================================================
FILE: src/createContainer.js
================================================
/* @flow */

import isEqual from 'lodash/isEqual';
import PropTypes from 'prop-types';
import React from 'react';
import Relay from 'react-relay/classic';
import type { RelayContainerSpec } from 'react-relay/lib/RelayContainer';

import type Subscription from './Subscription';
import type { SubscriptionDisposable } from './types';

type subscriptionFn = (props: Object) => ?Subscription<any>;

type ActiveSubscription = {
  subscription: Subscription<any>,
  disposable: SubscriptionDisposable,
}

function disposeActiveSubscription(activeSubscription) {
  if (!activeSubscription) {
    return;
  }

  activeSubscription.disposable.dispose();
}

function subscribe(
  Component: ReactClass<any>,
  subscriptionsSpec: ?Array<subscriptionFn>,
) {
  const componentName = Component.displayName || Component.name || 'Component';

  return class Subscribe extends React.Component {
    static displayName = `Subscribe(${componentName})`;

    static propTypes = {
      relay: PropTypes.object.isRequired,
    };

    static contextTypes = {
      relay: Relay.PropTypes.ClassicRelay,
    };

    relayProp: mixed;
    activeSubscriptions: Array<?ActiveSubscription>;

    constructor(props, context) {
      super(props, context);

      this.relayProp = this.makeRelayProp(props);
      this.activeSubscriptions = [];
    }

    componentDidMount() {
      if (subscriptionsSpec) {
        subscriptionsSpec.forEach((createSubscription) => {
          this.activeSubscriptions.push(
            this.makeActiveSubscription(createSubscription(this.props)),
          );
        });
      }
    }

    componentWillReceiveProps(nextProps) {
      if (nextProps.relay !== this.props.relay) {
        this.relayProp = this.makeRelayProp(nextProps);
      }

      if (subscriptionsSpec) {
        subscriptionsSpec.forEach((createSubscription, index) => {
          const activeSubscription = this.activeSubscriptions[index];
          const nextSubscription = createSubscription(nextProps);

          if (!this.areSubscriptionsEqual(
            activeSubscription,
            nextSubscription,
          )) {
            disposeActiveSubscription(activeSubscription);
            this.activeSubscriptions[index] =
              this.makeActiveSubscription(nextSubscription);
          }
        });
      }
    }

    componentWillUnmount() {
      if (subscriptionsSpec) {
        this.activeSubscriptions.forEach(disposeActiveSubscription);
      }
    }

    makeRelayProp(props) {
      return {
        ...props.relay,
        subscribe: this.context.relay.environment.startSubscription,
      };
    }

    makeActiveSubscription(subscription) {
      if (!subscription) {
        return null;
      }

      return {
        subscription,
        disposable: this.context.relay.environment.startSubscription(subscription),
      };
    }

    areSubscriptionsEqual(activeSubscription, nextSubscription) {
      if (!nextSubscription && !activeSubscription) {
        // Both old and new are falsy.
        return true;
      }

      if (!nextSubscription || !activeSubscription) {
        // Only one of the pair is falsy.
        return false;
      }

      const subscription = activeSubscription.subscription;

      if (nextSubscription.constructor !== subscription.constructor) {
        // Subscriptions are of different types.
        return false;
      }

      // Need to bind subscription to Relay environment to get variables.
      nextSubscription.bindEnvironment(this.context.relay.environment);

      // Check if variables match.
      return isEqual(
        nextSubscription.getVariables(),
        subscription.getVariables(),
      );
    }

    render() {
      return (
        <Component
          {...this.props}
          relay={this.relayProp}
        />
      );
    }
  };
}

export default function createContainer(
  Component: ReactClass<any>,
  spec: RelayContainerSpec & { subscriptions?: subscriptionFn[] },
) {
  return Relay.createContainer(
    subscribe(Component, spec.subscriptions),
    spec
  );
}


================================================
FILE: src/createSubscriptionQuery.js
================================================
/* @flow */

import RelayMetaRoute from 'react-relay/lib/RelayMetaRoute';
import RelayQuery from 'react-relay/lib/RelayQuery';

import type { RelayConcreteNode, Variables } from './types';

export default function createSubscriptionQuery(
  concreteNode: RelayConcreteNode,
  variables: Variables
): RelayQuery.Subscription {
  return RelayQuery.Subscription.create(
    concreteNode,
    RelayMetaRoute.get('$createSubscriptionQuery'),
    variables
  );
}


================================================
FILE: src/index.js
================================================
/* @flow */

import createContainer from './createContainer';
import Environment from './Environment';
import Subscription from './Subscription';

export default { createContainer, Environment, Subscription };
export { createContainer, Environment, Subscription };


================================================
FILE: src/types.js
================================================
/* @flow */

export type Variables = {[name: string]: mixed};
export type RelayConcreteNode = {[name: string]: mixed};

export type MutationConfig = {
  type: string;
  [name: string]: mixed;
};

export type PrintedQuery = {
  text: string;
  variables: Variables;
};

export type SubscriptionResult = {[name: string]: mixed};

export type SubscriptionObserver = {
  onNext?: (value: SubscriptionResult) => void;
  onError?: (error: any) => void;
  onCompleted?: (value: any) => void;
}

export type SubscriptionRequestObserver = {
  onNext: (value: SubscriptionResult) => void;
  onError: (error: any) => void;
  onCompleted: (value: any) => void;
}

export type SubscriptionDisposable = {
  dispose: () => void;
}


================================================
FILE: src/updateStoreData.js
================================================
/* @flow */

import type Relay from 'react-relay/classic';
import type RelayQuery from 'react-relay/lib/RelayQuery';

import type {
  MutationConfig,
  SubscriptionResult,
} from './types';

export default function updateStoreData(
  environment: Relay.Environment,
  configs: Array<MutationConfig>,
  query: RelayQuery.Operation,
  payload: SubscriptionResult
) {
  const storeData = environment.getStoreData();
  const payloadName = query.getCall().name;

  // FIXME: Applying a RANGE_ADD update requires a clientMutationId. This is a
  // nonce that won't collide with any actual mutation IDs.
  const clientMutationId = Math.random().toString(36);

  storeData.handleUpdatePayload(
    query,
    { ...payload[payloadName], clientMutationId },
    { configs }
  );
}
Download .txt
gitextract_qz37ndd9/

├── .babelrc
├── .eslintignore
├── .eslintrc
├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── docs/
│   ├── API/
│   │   └── Subscription.md
│   └── API.md
├── examples/
│   ├── .eslintrc
│   └── todo/
│       ├── .babelrc
│       ├── README.md
│       ├── data/
│       │   ├── database.js
│       │   ├── schema.graphql
│       │   └── schema.js
│       ├── js/
│       │   ├── NetworkLayer.js
│       │   ├── app.js
│       │   ├── components/
│       │   │   ├── Todo.js
│       │   │   ├── TodoApp.js
│       │   │   ├── TodoList.js
│       │   │   ├── TodoListFooter.js
│       │   │   └── TodoTextInput.js
│       │   ├── mutations/
│       │   │   ├── AddTodoMutation.js
│       │   │   ├── ChangeTodoStatusMutation.js
│       │   │   ├── MarkAllTodosMutation.js
│       │   │   ├── RemoveCompletedTodosMutation.js
│       │   │   ├── RemoveTodoMutation.js
│       │   │   └── RenameTodoMutation.js
│       │   ├── queries/
│       │   │   └── ViewerQueries.js
│       │   └── subscriptions/
│       │       ├── AddTodoSubscription.js
│       │       ├── RemoveTodoSubscription.js
│       │       └── UpdateTodoSubscription.js
│       ├── package.json
│       ├── public/
│       │   ├── base.css
│       │   ├── index.css
│       │   ├── index.html
│       │   └── learn.json
│       ├── server.js
│       └── tools/
│           ├── .eslintrc
│           └── updateSchema.js
├── package.json
└── src/
    ├── .flowconfig
    ├── Environment.js
    ├── Subscription.js
    ├── SubscriptionRequest.js
    ├── __tests__/
    │   └── createContainer.js
    ├── createContainer.js
    ├── createSubscriptionQuery.js
    ├── index.js
    ├── types.js
    └── updateStoreData.js
Download .txt
SYMBOL INDEX (104 symbols across 22 files)

FILE: examples/todo/data/database.js
  class Todo (line 13) | class Todo {}
  class User (line 14) | class User {}
  constant VIEWER_ID (line 17) | const VIEWER_ID = 'me';
  function notifyChange (line 34) | function notifyChange(topic, data) {
  function addNotifier (line 42) | function addNotifier(cb) {
  function addTodo (line 55) | function addTodo(text, complete) {
  function getTodo (line 69) | function getTodo(id) {
  function getTodos (line 73) | function getTodos(status = 'any') {
  function changeTodoStatus (line 81) | function changeTodoStatus(id, complete) {
  function getUser (line 87) | function getUser(id) {
  function getViewer (line 91) | function getViewer() {
  function markAllTodos (line 95) | function markAllTodos(complete) {
  function removeTodo (line 107) | function removeTodo(id) {
  function removeCompletedTodos (line 116) | function removeCompletedTodos() {
  function renameTodo (line 122) | function renameTodo(id, text) {

FILE: examples/todo/js/NetworkLayer.js
  class NetworkLayer (line 6) | class NetworkLayer extends Relay.DefaultNetworkLayer {
    method constructor (line 7) | constructor(...args) {
    method sendSubscription (line 16) | sendSubscription(request) {
    method disconnect (line 35) | disconnect() {

FILE: examples/todo/js/components/Todo.js
  class Todo (line 24) | class Todo extends React.Component {
    method _removeTodo (line 71) | _removeTodo() {
    method renderTextInput (line 82) | renderTextInput() {
    method render (line 95) | render() {

FILE: examples/todo/js/components/TodoApp.js
  class TodoApp (line 23) | class TodoApp extends React.Component {
    method render (line 37) | render() {

FILE: examples/todo/js/components/TodoList.js
  class TodoList (line 19) | class TodoList extends React.Component {
    method render (line 36) | render() {
  method prepareVariables (line 72) | prepareVariables({ status }) {

FILE: examples/todo/js/components/TodoListFooter.js
  class TodoListFooter (line 20) | class TodoListFooter extends React.Component {
    method render (line 36) | render() {

FILE: examples/todo/js/components/TodoTextInput.js
  constant ENTER_KEY_CODE (line 16) | const ENTER_KEY_CODE = 13;
  constant ESC_KEY_CODE (line 17) | const ESC_KEY_CODE = 27;
  class TodoTextInput (line 19) | class TodoTextInput extends React.Component {
    method componentDidMount (line 39) | componentDidMount() {
    method render (line 73) | render() {

FILE: examples/todo/js/mutations/AddTodoMutation.js
  class AddTodoMutation (line 15) | class AddTodoMutation extends Relay.Mutation {
    method getMutation (line 25) | getMutation() {
    method getFatQuery (line 29) | getFatQuery() {
    method getConfigs (line 41) | getConfigs() {
    method getVariables (line 54) | getVariables() {
    method getOptimisticResponse (line 60) | getOptimisticResponse() {

FILE: examples/todo/js/mutations/ChangeTodoStatusMutation.js
  class ChangeTodoStatusMutation (line 15) | class ChangeTodoStatusMutation extends Relay.Mutation {
    method getMutation (line 30) | getMutation() {
    method getFatQuery (line 33) | getFatQuery() {
    method getConfigs (line 46) | getConfigs() {
    method getVariables (line 55) | getVariables() {
    method getOptimisticResponse (line 61) | getOptimisticResponse() {

FILE: examples/todo/js/mutations/MarkAllTodosMutation.js
  class MarkAllTodosMutation (line 15) | class MarkAllTodosMutation extends Relay.Mutation {
    method getMutation (line 35) | getMutation() {
    method getFatQuery (line 38) | getFatQuery() {
    method getConfigs (line 48) | getConfigs() {
    method getVariables (line 56) | getVariables() {
    method getOptimisticResponse (line 61) | getOptimisticResponse() {

FILE: examples/todo/js/mutations/RemoveCompletedTodosMutation.js
  class RemoveCompletedTodosMutation (line 15) | class RemoveCompletedTodosMutation extends Relay.Mutation {
    method getMutation (line 36) | getMutation() {
    method getFatQuery (line 39) | getFatQuery() {
    method getConfigs (line 50) | getConfigs() {
    method getVariables (line 59) | getVariables() {
    method getOptimisticResponse (line 62) | getOptimisticResponse() {

FILE: examples/todo/js/mutations/RemoveTodoMutation.js
  class RemoveTodoMutation (line 15) | class RemoveTodoMutation extends Relay.Mutation {
    method getMutation (line 33) | getMutation() {
    method getFatQuery (line 36) | getFatQuery() {
    method getConfigs (line 47) | getConfigs() {
    method getVariables (line 56) | getVariables() {
    method getOptimisticResponse (line 61) | getOptimisticResponse() {

FILE: examples/todo/js/mutations/RenameTodoMutation.js
  class RenameTodoMutation (line 15) | class RenameTodoMutation extends Relay.Mutation {
    method getMutation (line 23) | getMutation() {
    method getFatQuery (line 26) | getFatQuery() {
    method getConfigs (line 35) | getConfigs() {
    method getVariables (line 43) | getVariables() {
    method getOptimisticResponse (line 49) | getOptimisticResponse() {

FILE: examples/todo/js/subscriptions/AddTodoSubscription.js
  class AddTodoSubscription (line 6) | class AddTodoSubscription extends Subscription {
    method getSubscription (line 15) | getSubscription() {
    method getConfigs (line 34) | getConfigs() {
    method getVariables (line 45) | getVariables() {

FILE: examples/todo/js/subscriptions/RemoveTodoSubscription.js
  class RemoveTodoSubscription (line 4) | class RemoveTodoSubscription extends Subscription {
    method getSubscription (line 13) | getSubscription() {
    method getConfigs (line 27) | getConfigs() {
    method getVariables (line 37) | getVariables() {

FILE: examples/todo/js/subscriptions/UpdateTodoSubscription.js
  class UpdateTodoSubscription (line 6) | class UpdateTodoSubscription extends Subscription {
    method getSubscription (line 15) | getSubscription() {
    method getConfigs (line 31) | getConfigs() {
    method getVariables (line 40) | getVariables() {

FILE: examples/todo/server.js
  constant APP_PORT (line 26) | const APP_PORT = 3000;
  constant GRAPHQL_PORT (line 27) | const GRAPHQL_PORT = 8080;
  class AsyncQueue (line 39) | class AsyncQueue {
    method constructor (line 40) | constructor(unsubscribe) {
    method createPromise (line 49) | createPromise() {
    method createIterable (line 55) | async * createIterable() {
    method push (line 72) | push(value) {
  function unsubscribe (line 99) | function unsubscribe(topic, queue) {

FILE: src/Environment.js
  class NetworkLayer (line 17) | class NetworkLayer extends RelayNetworkLayer {
    method sendSubscription (line 18) | sendSubscription(
  class StoreData (line 32) | class StoreData extends RelayStoreData {
    method constructor (line 35) | constructor(...args) {
  class Environment (line 42) | class Environment extends Relay.Environment {

FILE: src/__tests__/createContainer.js
  class Widget (line 21) | class Widget extends React.Component {
    method componentDidMount (line 26) | componentDidMount() {
    method render (line 30) | render() {

FILE: src/createContainer.js
  function disposeActiveSubscription (line 19) | function disposeActiveSubscription(activeSubscription) {
  function subscribe (line 27) | function subscribe(
  function createContainer (line 149) | function createContainer(

FILE: src/createSubscriptionQuery.js
  function createSubscriptionQuery (line 8) | function createSubscriptionQuery(

FILE: src/updateStoreData.js
  function updateStoreData (line 11) | function updateStoreData(
Condensed preview — 51 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (111K chars).
[
  {
    "path": ".babelrc",
    "chars": 137,
    "preview": "{\n  \"presets\": [\n    [\"env\", { \"loose\": true }],\n    \"stage-2\",\n    \"react\"\n  ],\n  \"plugins\": [\"dev-expression\", \"add-mo"
  },
  {
    "path": ".eslintignore",
    "chars": 27,
    "preview": "/examples/*/node_modules/*\n"
  },
  {
    "path": ".eslintrc",
    "chars": 227,
    "preview": "{\n  \"extends\": \"airbnb\",\n  \"env\": {\n    \"jest\": true,\n    \"jasmine\": true\n  },\n  \"parser\": \"babel-eslint\",\n  \"plugins\": "
  },
  {
    "path": ".gitignore",
    "chars": 593,
    "preview": "# Custom\nlib/\n\n# Logs\nlogs\n*.log\nnpm-debug.log*\n\n# Runtime data\npids\n*.pid\n*.seed\n\n# Directory for instrumented libs gen"
  },
  {
    "path": ".travis.yml",
    "chars": 96,
    "preview": "sudo: false\n\nlanguage: node_js\nnode_js:\n  - stable\n\ncache: yarn\n\nbranches:\n  only:\n    - master\n"
  },
  {
    "path": "LICENSE",
    "chars": 1080,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2016 Edvin Erikson\n\nPermission is hereby granted, free of charge, to any person obt"
  },
  {
    "path": "README.md",
    "chars": 5685,
    "preview": "# Relay Subscriptions [![npm][npm-badge]][npm]\n\nSubscription support for [Relay Classic](http://facebook.github.io/relay"
  },
  {
    "path": "docs/API/Subscription.md",
    "chars": 6020,
    "preview": "# Subscription\nRelaySubscriptions makes use of the `Relay.Mutation` API.\nIf you are familiar with the mutation api this "
  },
  {
    "path": "docs/API.md",
    "chars": 6603,
    "preview": "# API Reference\n\n- [Network layer](#network-layer)\n- [RelaySubscriptions.Environment](#relaysubscriptionsenvironment)\n- "
  },
  {
    "path": "examples/.eslintrc",
    "chars": 123,
    "preview": "{\n  \"rules\": {\n    // Don't fail linting if example dependencies aren't installed.\n    \"import/no-unresolved\": \"off\"\n  }"
  },
  {
    "path": "examples/todo/.babelrc",
    "chars": 153,
    "preview": "{\n  \"presets\": [\n    [\"env\", { \"loose\": true }],\n    \"stage-2\",\n    \"react\"\n  ],\n  \"plugins\": [\n    [\"relay\", { \"schema\""
  },
  {
    "path": "examples/todo/README.md",
    "chars": 1052,
    "preview": "# Relay TodoMVC\n\n## Installation\n\n```\nnpm install\n```\n\n## Running\n\nStart a local server:\n\n```\nnpm start\n```\n\n## Developi"
  },
  {
    "path": "examples/todo/data/database.js",
    "chars": 3245,
    "preview": "/**\n * This file provided by Facebook is for non-commercial testing and evaluation\n * purposes only.  Facebook reserves "
  },
  {
    "path": "examples/todo/data/schema.graphql",
    "chars": 3490,
    "preview": "schema {\n  query: Root\n  mutation: Mutation\n  subscription: Subscription\n}\n\ninput AddTodoInput {\n  text: String!\n  clien"
  },
  {
    "path": "examples/todo/data/schema.js",
    "chars": 8534,
    "preview": "/**\n * This file provided by Facebook is for non-commercial testing and evaluation\n * purposes only.  Facebook reserves "
  },
  {
    "path": "examples/todo/js/NetworkLayer.js",
    "chars": 928,
    "preview": "/* eslint-disable no-console */\n\nimport Relay from 'react-relay/classic';\nimport { SubscriptionClient } from 'subscripti"
  },
  {
    "path": "examples/todo/js/app.js",
    "chars": 1908,
    "preview": "/**\n * This file provided by Facebook is for non-commercial testing and evaluation\n * purposes only.  Facebook reserves "
  },
  {
    "path": "examples/todo/js/components/Todo.js",
    "chars": 4015,
    "preview": "/**\n * This file provided by Facebook is for non-commercial testing and evaluation\n * purposes only.  Facebook reserves "
  },
  {
    "path": "examples/todo/js/components/TodoApp.js",
    "chars": 2982,
    "preview": "/**\n * This file provided by Facebook is for non-commercial testing and evaluation\n * purposes only.  Facebook reserves "
  },
  {
    "path": "examples/todo/js/components/TodoList.js",
    "chars": 2908,
    "preview": "/**\n * This file provided by Facebook is for non-commercial testing and evaluation\n * purposes only.  Facebook reserves "
  },
  {
    "path": "examples/todo/js/components/TodoListFooter.js",
    "chars": 2622,
    "preview": "/**\n * This file provided by Facebook is for non-commercial testing and evaluation\n * purposes only.  Facebook reserves "
  },
  {
    "path": "examples/todo/js/components/TodoTextInput.js",
    "chars": 2336,
    "preview": "/**\n * This file provided by Facebook is for non-commercial testing and evaluation\n * purposes only.  Facebook reserves "
  },
  {
    "path": "examples/todo/js/mutations/AddTodoMutation.js",
    "chars": 1885,
    "preview": "/**\n * This file provided by Facebook is for non-commercial testing and evaluation\n * purposes only.  Facebook reserves "
  },
  {
    "path": "examples/todo/js/mutations/ChangeTodoStatusMutation.js",
    "chars": 2018,
    "preview": "/**\n * This file provided by Facebook is for non-commercial testing and evaluation\n * purposes only.  Facebook reserves "
  },
  {
    "path": "examples/todo/js/mutations/MarkAllTodosMutation.js",
    "chars": 2211,
    "preview": "/**\n * This file provided by Facebook is for non-commercial testing and evaluation\n * purposes only.  Facebook reserves "
  },
  {
    "path": "examples/todo/js/mutations/RemoveCompletedTodosMutation.js",
    "chars": 2280,
    "preview": "/**\n * This file provided by Facebook is for non-commercial testing and evaluation\n * purposes only.  Facebook reserves "
  },
  {
    "path": "examples/todo/js/mutations/RemoveTodoMutation.js",
    "chars": 2157,
    "preview": "/**\n * This file provided by Facebook is for non-commercial testing and evaluation\n * purposes only.  Facebook reserves "
  },
  {
    "path": "examples/todo/js/mutations/RenameTodoMutation.js",
    "chars": 1429,
    "preview": "/**\n * This file provided by Facebook is for non-commercial testing and evaluation\n * purposes only.  Facebook reserves "
  },
  {
    "path": "examples/todo/js/queries/ViewerQueries.js",
    "chars": 723,
    "preview": "/**\n * This file provided by Facebook is for non-commercial testing and evaluation\n * purposes only.  Facebook reserves "
  },
  {
    "path": "examples/todo/js/subscriptions/AddTodoSubscription.js",
    "chars": 917,
    "preview": "import Relay from 'react-relay/classic';\nimport { Subscription } from 'relay-subscriptions';\n\nimport Todo from '../compo"
  },
  {
    "path": "examples/todo/js/subscriptions/RemoveTodoSubscription.js",
    "chars": 767,
    "preview": "import Relay from 'react-relay/classic';\nimport { Subscription } from 'relay-subscriptions';\n\nexport default class Remov"
  },
  {
    "path": "examples/todo/js/subscriptions/UpdateTodoSubscription.js",
    "chars": 800,
    "preview": "import Relay from 'react-relay/classic';\nimport { Subscription } from 'relay-subscriptions';\n\nimport Todo from '../compo"
  },
  {
    "path": "examples/todo/package.json",
    "chars": 994,
    "preview": "{\n  \"private\": true,\n  \"scripts\": {\n    \"start\": \"babel-node server.js\",\n    \"update-schema\": \"babel-node tools/updateSc"
  },
  {
    "path": "examples/todo/public/base.css",
    "chars": 1814,
    "preview": "hr {\n\tmargin: 20px 0;\n\tborder: 0;\n\tborder-top: 1px dashed #c5c5c5;\n\tborder-bottom: 1px dashed #f7f7f7;\n}\n\n.learn a {\n\tfo"
  },
  {
    "path": "examples/todo/public/index.css",
    "chars": 6329,
    "preview": "html,\nbody {\n\tmargin: 0;\n\tpadding: 0;\n}\n\nbutton {\n\tmargin: 0;\n\tpadding: 0;\n\tborder: 0;\n\tbackground: none;\n\tfont-size: 10"
  },
  {
    "path": "examples/todo/public/index.html",
    "chars": 670,
    "preview": "<!doctype html>\n<html lang=\"en\" data-framework=\"relay\">\n  <head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" co"
  },
  {
    "path": "examples/todo/public/learn.json",
    "chars": 2292,
    "preview": "{\n  \"relay\": {\n    \"name\": \"Relay\",\n    \"description\": \"A JavaScript framework for building data-driven React applicatio"
  },
  {
    "path": "examples/todo/server.js",
    "chars": 4160,
    "preview": "/**\n * This file provided by Facebook is for non-commercial testing and evaluation\n * purposes only.  Facebook reserves "
  },
  {
    "path": "examples/todo/tools/.eslintrc",
    "chars": 41,
    "preview": "{\n  \"rules\": {\n    \"no-console\": 0\n  }\n}\n"
  },
  {
    "path": "examples/todo/tools/updateSchema.js",
    "chars": 873,
    "preview": "#!/usr/bin/env babel-node\n/**\n * This file provided by Facebook is for non-commercial testing and evaluation\n * purposes"
  },
  {
    "path": "package.json",
    "chars": 1920,
    "preview": "{\n  \"name\": \"relay-subscriptions\",\n  \"version\": \"2.0.2\",\n  \"description\": \"Subscription support for Relay\",\n  \"main\": \"l"
  },
  {
    "path": "src/.flowconfig",
    "chars": 200,
    "preview": "[ignore]\n\n[include]\n../node_modules/invariant\n../node_modules/lodash\n../node_modules/react\n../node_modules/react-relay\n."
  },
  {
    "path": "src/Environment.js",
    "chars": 3040,
    "preview": "/* @flow */\n\nimport invariant from 'invariant';\nimport Relay from 'react-relay/classic';\nimport RelayNetworkLayer from '"
  },
  {
    "path": "src/Subscription.js",
    "chars": 1662,
    "preview": "/* @flow */\n\nimport invariant from 'invariant';\nimport Relay from 'react-relay/classic';\nimport RelayMetaRoute from 'rea"
  },
  {
    "path": "src/SubscriptionRequest.js",
    "chars": 1346,
    "preview": "/* @flow */\n\nimport printQuery from 'react-relay/lib/printRelayQuery';\nimport RelayQuery from 'react-relay/lib/RelayQuer"
  },
  {
    "path": "src/__tests__/createContainer.js",
    "chars": 1238,
    "preview": "jest.mock('react-relay/lib/RelayContainer', () => ({\n  create: (Component) => {\n    Component.isRelayContainer = true; /"
  },
  {
    "path": "src/createContainer.js",
    "chars": 4050,
    "preview": "/* @flow */\n\nimport isEqual from 'lodash/isEqual';\nimport PropTypes from 'prop-types';\nimport React from 'react';\nimport"
  },
  {
    "path": "src/createSubscriptionQuery.js",
    "chars": 458,
    "preview": "/* @flow */\n\nimport RelayMetaRoute from 'react-relay/lib/RelayMetaRoute';\nimport RelayQuery from 'react-relay/lib/RelayQ"
  },
  {
    "path": "src/index.js",
    "chars": 265,
    "preview": "/* @flow */\n\nimport createContainer from './createContainer';\nimport Environment from './Environment';\nimport Subscripti"
  },
  {
    "path": "src/types.js",
    "chars": 716,
    "preview": "/* @flow */\n\nexport type Variables = {[name: string]: mixed};\nexport type RelayConcreteNode = {[name: string]: mixed};\n\n"
  },
  {
    "path": "src/updateStoreData.js",
    "chars": 771,
    "preview": "/* @flow */\n\nimport type Relay from 'react-relay/classic';\nimport type RelayQuery from 'react-relay/lib/RelayQuery';\n\nim"
  }
]

About this extraction

This page contains the full source code of the edvinerikson/relay-subscriptions GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 51 files (100.4 KB), approximately 25.9k tokens, and a symbol index with 104 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!