Giter Site home page Giter Site logo

relay-tools / react-relay-network-layer Goto Github PK

View Code? Open in Web Editor NEW
276.0 13.0 47.0 354 KB

ReactRelayNetworkLayer with middlewares and query batching for Relay Classic.

License: MIT License

JavaScript 100.00%
relay relay-network-layer graphql-client batch-request

react-relay-network-layer's Introduction

ReactRelayNetworkLayer (for Relay Classic)

npm Travis Commitizen friendly semantic-release FlowType compatible

For Relay Modern please use react-relay-network-modern package.

The ReactRelayNetworkLayer is a Relay Network Layer with various middlewares which can manipulate requests/responses on the fly (change auth headers, request url or perform some fallback if request fails), batch several relay request by timeout into one http request.

ReactRelayNetworkLayer can be used in browser, react-native or node server for rendering. Under the hood this module uses global fetch method. So if your client is too old, please import explicitly proper polyfill to your code (eg. whatwg-fetch, node-fetch or fetch-everywhere).

yarn add react-relay-network-layer
OR
npm install react-relay-network-layer --save

Migrating from v1 to v2

Changes in v2.0.0:

  • completely rewritten batch logic as middleware, added additional cool options to it batchTimeout, maxBatchSize
  • throw Error object on non-200 response (before thrown response)
  • much more tests

All other parts stay unaffected. So if you use request batching, you should change your config:

import Relay from 'react-relay';
import {
  RelayNetworkLayer,
  urlMiddleware,
+  batchMiddleware,
} from 'react-relay-network-layer';

Relay.injectNetworkLayer(new RelayNetworkLayer([
+  batchMiddleware({
+    batchUrl: (req) => '/graphql/batch',
+  }),
  urlMiddleware({
    url: (req) => '/graphql',
-    batchUrl: (req) => '/graphql/batch',
  }),
- ], { disableBatchQuery: false }));
+ ]));

Big thanks to @brad-decker and @jeanregisser in helping to done this release.

Previous documentation for version 1.x.x can be found here.

Middlewares

Available middlewares:

  • your custom inline middleware - see example below where added credentials and headers to the fetch method.
    • next => req => { /* your modification of 'req' object */ return next(req); }
  • urlMiddleware - for manipulating fetch url on fly via thunk.
    • url - string or function(req) for single request (default: /graphql)
  • batchMiddleware - gather some period of time relay-requests and sends it as one http-request. You server must support batch request, how to setup your server
    • batchUrl - string or function(requestMap). Url of the server endpoint for batch request execution (default: /graphql/batch)
    • batchTimeout - integer in milliseconds, period of time for gathering multiple requests before sending them to the server. Will delay sending of the requests on specified in this option period of time, so be careful and keep this value small. (default: 0)
    • maxBatchSize - integer representing maximum size of request to be sent in a single batch. Once a request hits the provided size in length a new batch request is ran. Actual for hardcoded limit in 100kb per request in express-graphql module. (default: 102400 characters, roughly 100kb for 1-byte characters or 200kb for 2-byte characters)
    • allowMutations - by default batching disabled for mutations, you may enable it passing true (default: false)
  • retryMiddleware - for request retry if the initial request fails.
    • fetchTimeout - number in milliseconds that defines in how much time will request timeout after it has been sent to the server (default: 15000).
    • retryDelays - array of millisecond that defines the values on which retries are based on (default: [1000, 3000]).
    • statusCodes - array of response status codes which will fire up retryMiddleware (default: status < 200 or status > 300).
    • allowMutations - by default retries disabled for mutations, you may allow process retries for them passing true (default: false)
    • forceRetry - function(cb, delay), when request is delayed for next retry, middleware will call this function and pass to it a callback and delay time. When you call this callback, middleware will proceed request immediately (default: false).
  • authMiddleware - for adding auth token, and refreshing it if gets 401 response from server.
    • token - string or function(req) which returns token. If function is provided, then it will be called for every request (so you may change tokens on fly).
    • tokenRefreshPromise: - function(req, err) which must return promise or regular value with a new token. This function is called when server returns 401 status code. After receiving a new token, middleware re-run query to the server with it seamlessly for Relay.
    • allowEmptyToken - allow made a request without Authorization header if token is empty (default: false).
    • prefix - prefix before token (default: 'Bearer ').
    • header - name of the HTTP header to pass the token in (default: 'Authorization').
    • If you use auth middleware with retry, retry must be used before auth. Eg. if token expired when retries apply, then retry can call auth middleware again.
  • loggerMiddleware - for logging requests and responses.
    • logger - log function (default: console.log.bind(console, '[RELAY-NETWORK]'))
    • If you use Relay@^0.9.0 you may turn on relay's internal extended mutation debugger. For this you should open browser console and type __DEV__=true. With webpack you may use webpack.BannerPlugin('__DEV__=true;', {raw: true}) or webpack.DefinePlugin({__DEV__: true}).
    • If you use Relay@^0.8.0 you may turn on internal Relay requests debugger: import RelayNetworkDebug from 'react-relay/lib/RelayNetworkDebug'; RelayNetworkDebug.init();
  • perfMiddleware - simple time measure for network request.
    • logger - log function (default: console.log.bind(console, '[RELAY-NETWORK]'))
  • gqErrorsMiddleware - display errors data to console from graphql response. If you want see stackTrace for errors, you should provide formatError to express-graphql (see example below where graphqlServer accept formatError function).
    • logger - log function (default: console.error.bind(console))
    • prefix - prefix message (default: [RELAY-NETWORK] GRAPHQL SERVER ERROR:)
  • deferMiddleware - experimental Right now deferMiddleware() just set defer as supported option for Relay. So this middleware allow to community play with defer() in cases, which was described by @wincent.

Advanced options (2nd argument after middlewares)

RelayNetworkLayer may accept additional options:

const middlewares = []; // array of middlewares
const options = {}; // optional advanced options
const network = new RelayNetworkLayer(middlewares, options);

Available options:

  • noThrow - EXPERIMENTAL (May be deprecated in the future) set true to not throw when an error response is given by the server, and to instead handle errors in your app code.

Example of injecting NetworkLayer with middlewares on the client side.

import Relay from 'react-relay';
import {
  RelayNetworkLayer,
  urlMiddleware,
  batchMiddleware,
  loggerMiddleware,
  gqErrorsMiddleware,
  perfMiddleware,
  retryMiddleware,
  authMiddleware,
} from 'react-relay-network-layer';

Relay.injectNetworkLayer(new RelayNetworkLayer([
  urlMiddleware({
    url: (req) => '/graphql',
  }),
  batchMiddleware({
    batchUrl: (reqestMap) => '/graphql/batch',
    batchTimeout: 10,
  }),
  loggerMiddleware(),
  gqErrorsMiddleware(),
  perfMiddleware(),
  retryMiddleware({
    fetchTimeout: 15000,
    retryDelays: (attempt) => Math.pow(2, attempt + 4) * 100, // or simple array [3200, 6400, 12800, 25600, 51200, 102400, 204800, 409600],
    forceRetry: (cb, delay) => { window.forceRelayRetry = cb; console.log('call `forceRelayRetry()` for immediately retry! Or wait ' + delay + ' ms.'); },
    statusCodes: [500, 503, 504]
  }),
  authMiddleware({
    token: () => store.get('jwt'),
    tokenRefreshPromise: (req) => {
      console.log('[client.js] resolve token refresh', req);
      return fetch('/jwt/refresh')
        .then(res => res.json())
        .then(json => {
          const token = json.token;
          store.set('jwt', token);
          return token;
        })
        .catch(err => console.log('[client.js] ERROR can not refresh token', err));
    },
  }),

  // example of the custom inline middleware
  next => req => {
    // `req` is an object with settings for `fetch` function. It's not an express request object.
    // Internally works following code:
    //    let { url, ...opts } = req;
    //    fetch(url, opts)
    // So `req` is a fetch options. And into this options, I added `url` prop, which will be extracted as shown above.
    // You have fully control under `fetch` via `req` object.

    req.method = 'GET'; // change default POST request method to GET
    req.headers['X-Request-ID'] = uuid.v4(); // add `X-Request-ID` to request headers
    req.credentials = 'same-origin'; // provide CORS policy to XHR request in fetch method
    return next(req);
  }
]));

How middlewares work internally

Middlewares on bottom layer use fetch method. So req is compliant with a fetch() options. And res can be obtained via resPromise.then(res => ...), which returned by fetch().

Middlewares have 3 phases:

  • setup phase, which runs only once, when middleware added to the NetworkLayer
  • capturing phase, when you may change request object, and pass it down via next(req)
  • bubbling phase, when you may change response promise, made re-request or pass it up unchanged

Basic skeleton of middleware:

export default function skeletonMiddleware(opts = {}) {
  // [SETUP PHASE]: here you can process `opts`, when you create Middleware

  return next => req => {
    // [CAPTURING PHASE]: here you can change `req` object, before it will pass to following middlewares.
    // ...some code which modify `req`

    const resPromise = next(req); // pass request to following middleware and get response promise from it

    // [BUBBLING PHASE]: here you may change response of underlying middlewares, via promise syntax
    // ...some code, which may add `then()` or `catch()` to response promise
    //    resPromise.then(res => { console.log(res); return res; })

    return resPromise; // return response promise to upper middleware
  };
}

Middlewares use LIFO (last in, first out) stack. Or simply put - use compose function. So if you pass such middlewares [M1(opts), M2(opts)] to NetworkLayer it will be work such way:

  • call setup phase of M1 with its opts
  • call setup phase of M2 with its opts
  • for each request
  • call capture phase of M1
  • call capture phase of M2
  • call fetch method
  • call bubbling phase of M2
  • call bubbling phase of M1
  • chain to resPromise.then(res => res.json()) and pass this promise for resolving/rejecting Relay requests.

Batching several requests into one

Joseph Savona wrote: For legacy reasons, Relay splits "plural" root queries into individual queries. In general we want to diff each root value separately, since different fields may be missing for different root values.

Also if you use react-relay-router and have multiple root queries in one route pass, you may notice that default network layer will produce several http requests.

So for avoiding multiple http-requests, the ReactRelayNetworkLayer is the right way to combine it in single http-request.

Example how to enable batching

...on server

Firstly, you should prepare server to proceed the batch request:

import express from 'express';
import graphqlHTTP from 'express-graphql';
import { graphqlBatchHTTPWrapper } from 'react-relay-network-layer';
import bodyParser from 'body-parser';
import myGraphqlSchema from './graphqlSchema';

const port = 3000;
const server = express();

// setup standart `graphqlHTTP` express-middleware
const graphqlServer = graphqlHTTP({
  schema: myGraphqlSchema,
  formatError: (error) => ({ // better errors for development. `stack` used in `gqErrors` middleware
    message: error.message,
    stack: process.env.NODE_ENV === 'development' ? error.stack.split('\n') : null,
  }),
});

// declare route for batch query
server.use('/graphql/batch',
  bodyParser.json(),
  graphqlBatchHTTPWrapper(graphqlServer)
);

// declare standard graphql route
server.use('/graphql',
  graphqlServer
);

server.listen(port, () => {
  console.log(`The server is running at http://localhost:${port}/`);
});

More complex example of how you can use a single DataLoader for all (batched) queries within a one HTTP-request.

If you are on Koa@2, koa-graphql-batch provides the same functionality as graphqlBatchHTTPWrapper (see its docs for usage example).

...on client

And right after server side ready to accept batch queries, you may enable batching on the client:

Relay.injectNetworkLayer(new RelayNetworkLayer([
  batchMiddleware({
    batchUrl: '/graphql/batch', // <--- route for batch queries
  }),
]));

How batching works internally

Internally batching in NetworkLayer prepare list of queries [ {id, query, variables}, ...] sends it to server. And server returns list of responces [ {id, payload}, ...], (where id is the same value as client requested for identifying which data goes with which query, and payload is standard response of GraphQL server: { data, error }).

Recommended modules

  • babel-plugin-transform-relay-hot - Babel 6 plugin for transforming Relay.QL tagged templates via the GraphQL json schema file. Each time when schema file was changed, the wrapper updates instance of standard babelRelayPlugin with new schema without completely restarting dev server.

Contribute

I actively welcome pull requests with code and doc fixes. Also if you made great middleware and want share it within this module, please feel free to open PR.

CHANGELOG

License

MIT

react-relay-network-layer's People

Contributors

alexanderlamb avatar alexxv avatar brad-decker avatar danny-larsen avatar existentialism avatar fracmak avatar giautm avatar golmansax avatar jamesone avatar janmeier avatar jonathanusername avatar kosmikko avatar mattecapu avatar nicholas-l avatar nodkz avatar redjohn avatar tehwalris avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

react-relay-network-layer's Issues

Refresh token executed multiple times, when one time should be enough

Hey,

We have a scenario, where multiple concurrent requests receive HTTP 401,
because authentication token is expired. Every one of those triggers a refreshToken flow, thus we have multiple refreshes concurrently.

I think it should there should be only one refreshToken promise running simultaneously,

What do you think?

If you think it's worth adding, I'll be glad to submit a PR with this.
I've implemented this kind of solution for our app, it's very simple

Alex

Cannot access response header from middleware

I am trying to use JWT for user authentication. The client is a React application with Relay. It is talking to a Rails backend through endpoints served by GraphQL.From the Rails side, I've set up CORS to make sure that I've exposed my header in the response. I've also set my JWT underย Authorization:

class Api::GraphsController < ActionController::Base

def create
  headers['Access-Control-Allow-Origin'] = '*'
  headers['Access-Control-Allow-Methods'] = 'POST, GET, PUT, DELETE, OPTIONS'
  headers['Access-Control-Allow-Headers'] = 'Origin, Content-Type, Accept, Authorization, Token'
  headers['Access-Control-Max-Age'] = "1728000"
  headers['Access-Control-Expose-Headers'] = 'content-length'

  #set a fresh token after every request
  headers['Authorization'] = "WOW.WoW.Wow"

  render json: Schema.execute()
end

On the React side, I've set up the options in my Relay middleware layer. However, this is where I cannot access my header. It just shows Header {} when I print res.headers.

const options = [
  urlMiddleware({
    url: (req) => getEndpoint()
  }),
  retryMiddleware({
    fetchTimeout: 60000,
    retryDelays: (attempt) => [7000],
    forceRetry: (cb, delay) => { window.forceRelayRetry = cb },
    statusCodes: [500, 503, 504]
  }),
  next => req => {
    req.credentials = 'same-origin'; // provide CORS policy to XHR request in fetch method
    const resPromise = next(req);
    resPromise.then(res => {
      let headers = res.headers //Headers is empty
      let user = res.json.data.user //Body is sent properly

      return res;
    })
    .catch((e) => {
      console.log('=============error===========', e);
    })

    return resPromise;
  }
]

I clearly see the Authorization token being set in Chrome Developer Tools:

rrf9u

Any advice on what I'm doing incorrectly here?

Need fetch to send cookies, need to pass option to fetch

In order to implement server side rendering, I've switched from local storage to cookies. Local storage does not work for server rendering because it's a pure JS API and in order for JS to run the page needs to be requested first, which once that has happened you're past the point of server rendering. So, since browsers send cookies natively, cookies are to be used for server rendering so that first request has the session token.

With that said, fetch does not send cookies by default. In order for fetch to send cookies, you need to pass in some additional options.

I need to pass the credentials option to fetch. Looking at your code, I can't quite tell how the opts are defined. Is there an existing way for me to set credentials for fetch? How do you suggest I go about doing this, whether it's a modification to this lib, or some additional network layer middleware, etc?

Thanks!

Throwing errors from middleware

I'm trying to handle errors from mutations similarly to what is described here, and so I'm passing an errors object in my mutation payload. These errors are then handled in the onSuccess callback of the mutation. This presents an issue because to relay it appears as though the mutation completed successfully, and so the relayRequest is resolved rather than rejected. Sometimes this is ok, but in the case of RANGE_ADD type relay mutations, relay proceeds to add an edge when there is no newly created edge.

I'm able to solve this by modifying the mutation function as such:

  return fetchWithMiddleware(req)
    .then(payload => {
      if (payload.hasOwnProperty('errors')) {
        const error = new Error(
          'Server request for mutation `' + relayRequest.getDebugName() + '` ' +
          'failed for the following reasons:\n\n' +
          formatRequestErrors(relayRequest, payload.errors)
        );
        error.source = payload;
        relayRequest.reject(error);
      } else {
        Object.keys(payload.data).forEach(key => {
          const errors = payload.data[key].errors;
          if (errors && errors.length > 0) {
            const error = new Error('Server request failed');
            error.source = errors;
            relayRequest.reject(error);
          } else {
            relayRequest.resolve({ response: payload.data });
          }
        })
      }
    }).catch(
      error => relayRequest.reject(error)
    );

But I realized that rather than modifying this code I should be able to just add a custom middleware, which would throw an error for the mutation function's fetchWithMiddleware to catch:

next => req => {
  const resPromise = next(req);

  if (req.relayReqType === 'mutation') {
    resPromise.then(res => {
      Object.keys(res.json.data).forEach(key => {
        const errors = res.json.data[key].errors;
        if (errors && errors.length > 0) {
          const error = new Error('Server request failed');
          error.source = errors;
          throw error;
        }
      });
    });
  }

  return resPromise;
}

However the fetchWithMiddleware never catches this error, proceeds to resolve the relayRequest, and I get an Unhandled promise rejection error in the console.

Am I maybe misunderstanding the nature of middleware? Is there a better way I might go about doing this?

CORS issue

Your work here is awesome. Everything is working great however i'm having a CORS issue. In order to circumvent this issue in the past, i was passing this option into the Default Network Layer

const networkLayerOptions = {
        credentials: 'same-origin',  // pass cookies when request.
};

like this

Relay.injectNetworkLayer(
    new Relay.DefaultNetworkLayer('/graphql', networkLayerOptions)
  );

How would i could about doing this now?

I tried the following below but am having no such luck.

  Relay.injectNetworkLayer(new RelayNetworkLayer([
    urlMiddleware({
      url: (req) => '/graphql',
      batchUrl: (req) => '/graphql/batch',
    }),
    loggerMiddleware(),
    gqErrorsMiddleware(),
    perfMiddleware(),
    retryMiddleware({
      fetchTimeout: 15000,
      retryDelays: (attempt) => Math.pow(2, attempt + 4) * 100,
      forceRetry: (cb, delay) => {
        window.forceRelayRetry = cb;
        console.log('call `forceRelayRetry()` for immediately retry! Or wait ' + delay + ' ms.');
      },
      statusCodes: [500, 503, 504],
    }),
    // example of the custom inline middleware (add `X-Request-ID` to request headers)
    next => req => {
      req.headers['X-Request-ID'] = uuid.v4();
      req.headers['Access-Control-Allow-Methods'] = 'GET, POST, OPTIONS, PUT, PATCH, DELETE';
      req.headers['Access-Control-Allow-Credentials'] = true;
      req.headers['Access-Control-Allow-Origin'] = '*';
      req.headers['Access-Control-Allow-Headers'] = 'Cache-Control, Pragma, Origin, Authorization, Content-Type, X-Requested-With';
      return next(req);
    }
  ], { disableBatchQuery: false, credentials: 'same-origin' }));

Any help here would be much appreciated.

Batching: make query id optional

I'm trying to make the batching network layer work together with apollo-server-express. I'm using apollo-server instead of graphql-express + the middleware from this module, because it supports query and field tracing.

The batch middleware depends on an id for each batched query to connect requests and responses. However, apollo-server does not pass this id back. This actually turns out to be in line with the graphql spec:

To ensure future changes to the protocol do not break existing servers and clients, the top level response map must not contain any entries other than the three described above. [data, errors, and extensions]
http://facebook.github.io/graphql/draft/#sec-Response-Format

However, the spec also defines that JSON serialization is ordered:

Since the result of evaluating a selection set is ordered, the JSON object serialized should preserve this order by writing the object properties in the same order as those fields were requested as defined by query execution.
http://facebook.github.io/graphql/draft/#sec-JSON-Serialization

So it should be possible to correlate responses to requests without relying on ids. A solution could be to make the ID optional, and fall back to query index when there is no id.

I can implement this change if you think it sounds good, but I would like to get some feedback first.

Uploading a file when using auth middleware fails

Using the auth middleware while sending a mutation that includes a file fails.

It appears that the request headers property isn't defined before the Authorize header is being set in the auth middleware
Possible fix #4

Client is not batching requests

Hey @nodkz

I'm using react-relay-router that has 3 routes being rendered with different root queries for a single path, I noticed that sendQueries is called 3 times with with requests param only containing the specific root query for each call instead of a single sendQueries call with all 3 requests.

here's what the network layer looks like.

export default new RelayNetworkLayer([
  urlMiddleware({
    url: (req) => env.GRAPHQL_ENDPOINT_SECURE,
    batchUrl: (req) => env.GRAPHQL_ENDPOINT_SECURE
  }),
  loggerMiddleware(),
  gqErrorsMiddleware(),
  deferMiddleware(),
  perfMiddleware(),
  retryMiddleware({
    fetchTimeout: 15000,
    retryDelays: (attempt) => Math.pow(2, attempt + 4) * 100, // or simple array [3200, 6400, 12800, 25600, 51200, 102400, 204800, 409600],
    forceRetry: (cb, delay) => {
      window.forceRelayRetry = cb;
      console.log('call `forceRelayRetry()` for immediately retry! Or wait ' + delay + ' ms.');
    },
    statusCodes: [500, 503, 504]
  }),
  homepassAuthMiddleware()
], { disableBatchQuery: false });

Any ideas why this is happening?

Redirect to login page and network layer

I've tried to use network layer to redirect user to user page if status code is 401, but I dont know how to make redirect from callback:

import {
  RelayNetworkLayer,
  authMiddleware,
} from 'react-relay-network-layer';

Relay.injectNetworkLayer(new RelayNetworkLayer([

  authMiddleware({
    token: () => 'testtoken',
    tokenRefreshPromise(){
      // we are here if status code is 401
      // but how can I redirect user to login page here?
    }
  }),
]));

If I just remove token in tokenRefreshPromise and check it out in onEnter hook, user is able to open route because onEnter fires before tokenRefreshPromise.

What is the right way to redirect user to login page?

Thx a lot!

FetchWithRetries PR?

Do you accept PR's for fetchWithRetries middleware, as this is the only huge thing that is missing in this lib and I have some free time to do it?

How to cancel requests

I'm implementing a query buffer to solve #16

I'm able to construct a batch query object after all individual queries have been added to the queue and I only want to execute the final, batch-query and cancel all other requests.

Does react-relay-network-layer support this?

Retry middleware not retrying

I cannot get the retry middleware working.

I am trying to get it to retry on fetch failures, but that does not seem to work. Is it only working for requests that actually return a status code?

EDIT
So after poking around in the code I see that it is only retrying for timeout errors. Is there a reason for that?

if (err === timeoutError) {
var retryDelayMS = retryAfterMs(attempt);
if (retryDelayMS) {
logger('response timeout, retrying after ' + retryDelayMS + ' ms');
return sendTimedRequest(timeout, retryDelayMS);
}
}

Syntax Error GraphQL request (1:1) Unexpected EOF

react native Relay mutation works fine without getFiles(). But if i write getFiles() function my request payload becomes [object Object]

 class RegisterMutation extends Relay.Mutation {
...

  getFiles() {
    console.log('uploading file is ', this.props.icon);
    return {
      icon: {
        fileName:"IMG_0006.JPG",
        fileSize:36867,
        fileType:"image/jpeg",
        name:"photo.jpg",
        type:"image/jpeg",
        uri:"/path/to/image",
      },
    };
  }
...
}

on web my mutation with file is sent as multipart/form-data.

but on react native request header is Content-Type:text/plain;charset=UTF-8

how can i make react native to send file with mutation

Isomorphic rendering and sequential network requests?

Hey @nodkz

I have a very particular use case, and wanted to check if you have any thoughts.

So, I have a react relay isomorphic application which uses this as the network layer both on client and server. On each request I proxy cookies from the frontend to the graphql server using a custom middleware, so it knows who the user is.

However it breaks, when more than once request is made per render cycle on the server. The reason is right now it seems the network layer dispatches request in parallel. This breaks my use case because it results in two users in being created as I'm creating anonymous user like parse does.

Is there a way to dispatch requests sequentially?

Chinese Characters Issue

Hey @nodkz. I'm hoping you can help us. We've been in a month long slice to get our site minimally viable for a Luxury Auction sale to chinese buyers. This included a lot of hard coding of chinese characters in the code base, but now our content team is adding translations into our database. I was able to trace this down by figuring out what page it was that was throwing the issue, here's a recap.

  1. Everything works fine, until you request the page that has the chinese characters in relay response.
  2. After that, every new page request will fail with the error message Server does not return response for request with id q*, (* being a wildcard) when in batching mode. If i remove batch middleware its this: Failed to execute 'fetch' on 'Window': Invalid value
  3. If i remove Relay network layer in favor of default network layer from Relay, it works (but i don't consider it a viable solution unless i have no other choices).

If you have time to look into this i would appreciate it greatly, otherwise if you could point me in the right direction to implement a fix myself that would be fantastic.

Error thrown

I've noticed a difference between this network layer and the default network layer. If there is a network error the render method of the Relay.Renderer is called with the error. In the default network layer nothing else is done, in this network layer the error is then thrown after the render function has been called so you can't silently handle it.

I can't work out exactly where it's being thrown from - is this something you're aware of?

express-graphql 0.5.4 doesn't work with react-relay-network-layer

After upgrading to express-graphql 0.5.4, react-relay-network-layer cannot return data from original graphql queries, in another words - simply whole UI will be broken.

probably because they introduced small fix:

@chentsulin

Use end() to write final data (e01eb2e @chentsulin)

Will maybe look later into react-relay-network-layer code, but if owner can take a look it will be awesome

error: Processing of node_modules/react-relay-network-layer/lib/middleware/gqErrors.js failed. SyntaxError: Unexpected token (80:376)

I'm using Brunch and this function makes compilation fail

function noticeAbsentStack() {
  return '\n    If you using \'express-graphql\', you may get server stack-trace for error.\n    Just tune \'formatError\' to return \'stack\' with stack-trace:\n\n    import graphqlHTTP from \'express-graphql\';\n\n    const graphQLMiddleware = graphqlHTTP({\n      schema: myGraphQLSchema,\n      formatError: (error) => ({\n        message: error.message,\n        stack: process.env.NODE_ENV === \'development\' ? error.stack.split(\'\\n\') : null,\n      })\n    });\n\n    app.use(\'/graphql\', graphQLMiddleware);';
}
18:12:20 - error: Processing of node_modules/react-relay-network-layer/lib/middleware/gqErrors.js failed. SyntaxError: Unexpected token (80:376)
  at Parser.pp$4.raise (/Users/felixdescoteaux/Projects/expenses/node_modules/acorn/dist/acorn.js:2488:13)
  at Parser.pp.unexpected (/Users/felixdescoteaux/Projects/expenses/node_modules/acorn/dist/acorn.js:623:8)
  at Parser.pp.semicolon (/Users/felixdescoteaux/Projects/expenses/node_modules/acorn/dist/acorn.js:600:59)
  at Parser.pp$1.parseReturnStatement (/Users/felixdescoteaux/Projects/expenses/node_modules/acorn/dist/acorn.js:894:55)
  at Parser.pp$1.parseStatement (/Users/felixdescoteaux/Projects/expenses/node_modules/acorn/dist/acorn.js:744:32)
  at Parser.pp$1.parseBlock (/Users/felixdescoteaux/Projects/expenses/node_modules/acorn/dist/acorn.js:1040:23)
  at Parser.pp$3.parseFunctionBody (/Users/felixdescoteaux/Projects/expenses/node_modules/acorn/dist/acorn.js:2362:22)
  at Parser.pp$1.parseFunction (/Users/felixdescoteaux/Projects/expenses/node_modules/acorn/dist/acorn.js:1132:8)
  at Parser.pp$1.parseFunctionStatement (/Users/felixdescoteaux/Projects/expenses/node_modules/acorn/dist/acorn.js:868:15)
  at Parser.pp$1.parseStatement (/Users/felixdescoteaux/Projects/expenses/node_modules/acorn/dist/acorn.js:739:17)
  at Parser.pp$1.parseBlock (/Users/felixdescoteaux/Projects/expenses/node_modules/acorn/dist/acorn.js:1040:23)
  at Parser.pp$3.parseFunctionBody (/Users/felixdescoteaux/Projects/expenses/node_modules/acorn/dist/acorn.js:2362:22)
  at Parser.pp$1.parseFunction (/Users/felixdescoteaux/Projects/expenses/node_modules/acorn/dist/acorn.js:1132:8)
  at Parser.pp$3.parseExprAtom (/Users/felixdescoteaux/Projects/expenses/node_modules/acorn/dist/acorn.js:1999:17)
  at Parser.pp$3.parseExprSubscripts (/Users/felixdescoteaux/Projects/expenses/node_modules/acorn/dist/acorn.js:1872:19)
  at Parser.pp$3.parseMaybeUnary (/Users/felixdescoteaux/Projects/expenses/node_modules/acorn/dist/acorn.js:1849:17)
  at Parser.pp$3.parseExprOps (/Users/felixdescoteaux/Projects/expenses/node_modules/acorn/dist/acorn.js:1791:19)
  at Parser.pp$3.parseMaybeConditional (/Users/felixdescoteaux/Projects/expenses/node_modules/acorn/dist/acorn.js:1774:19)
  at Parser.pp$3.parseMaybeAssign (/Users/felixdescoteaux/Projects/expenses/node_modules/acorn/dist/acorn.js:1750:19)
  at Parser.pp$3.parseParenAndDistinguishExpression (/Users/felixdescoteaux/Projects/expenses/node_modules/acorn/dist/acorn.js:2056:30)
  at Parser.pp$3.parseExprAtom (/Users/felixdescoteaux/Projects/expenses/node_modules/acorn/dist/acorn.js:1978:41)
  at Parser.pp$3.parseExprSubscripts (/Users/felixdescoteaux/Projects/expenses/node_modules/acorn/dist/acorn.js:1872:19)
  at Parser.pp$3.parseMaybeUnary (/Users/felixdescoteaux/Projects/expenses/node_modules/acorn/dist/acorn.js:1849:17)
  at Parser.pp$3.parseExprOps (/Users/felixdescoteaux/Projects/expenses/node_modules/acorn/dist/acorn.js:1791:19)
  at Parser.pp$3.parseMaybeConditional (/Users/felixdescoteaux/Projects/expenses/node_modules/acorn/dist/acorn.js:1774:19)
  at Parser.pp$3.parseMaybeAssign (/Users/felixdescoteaux/Projects/expenses/node_modules/acorn/dist/acorn.js:1750:19)
  at Parser.pp$3.parseExpression (/Users/felixdescoteaux/Projects/expenses/node_modules/acorn/dist/acorn.js:1722:19)
  at Parser.pp$1.parseStatement (/Users/felixdescoteaux/Projects/expenses/node_modules/acorn/dist/acorn.js:777:45)
  at Parser.pp$1.parseBlock (/Users/felixdescoteaux/Projects/expenses/node_modules/acorn/dist/acorn.js:1040:23)
  at Parser.pp$3.parseFunctionBody (/Users/felixdescoteaux/Projects/expenses/node_modules/acorn/dist/acorn.js:2362:22)
  at Parser.pp$1.parseFunction (/Users/felixdescoteaux/Projects/expenses/node_modules/acorn/dist/acorn.js:1132:8)
  at Parser.pp$3.parseExprAtom (/Users/felixdescoteaux/Projects/expenses/node_modules/acorn/dist/acorn.js:1999:17)
  at Parser.pp$3.parseExprSubscripts (/Users/felixdescoteaux/Projects/expenses/node_modules/acorn/dist/acorn.js:1872:19)
  at Parser.pp$3.parseMaybeUnary (/Users/felixdescoteaux/Projects/expenses/node_modules/acorn/dist/acorn.js:1849:17)
  at Parser.pp$3.parseExprOps (/Users/felixdescoteaux/Projects/expenses/node_modules/acorn/dist/acorn.js:1791:19)
  at Parser.pp$3.parseMaybeConditional (/Users/felixdescoteaux/Projects/expenses/node_modules/acorn/dist/acorn.js:1774:19)
  at Parser.pp$3.parseMaybeAssign (/Users/felixdescoteaux/Projects/expenses/node_modules/acorn/dist/acorn.js:1750:19)
  at Parser.pp$3.parseExprList (/Users/felixdescoteaux/Projects/expenses/node_modules/acorn/dist/acorn.js:2418:20)
  at Parser.pp$3.parseSubscripts (/Users/felixdescoteaux/Projects/expenses/node_modules/acorn/dist/acorn.js:1900:29)
  at Parser.pp$3.parseExprSubscripts (/Users/felixdescoteaux/Projects/expenses/node_modules/acorn/dist/acorn.js:1875:21)
  at Parser.pp$3.parseMaybeUnary (/Users/felixdescoteaux/Projects/expenses/node_modules/acorn/dist/acorn.js:1849:17)
  at Parser.pp$3.parseExprOps (/Users/felixdescoteaux/Projects/expenses/node_modules/acorn/dist/acorn.js:1791:19)
  at Parser.pp$3.parseMaybeConditional (/Users/felixdescoteaux/Projects/expenses/node_modules/acorn/dist/acorn.js:1774:19)
  at Parser.pp$3.parseMaybeAssign (/Users/felixdescoteaux/Projects/expenses/node_modules/acorn/dist/acorn.js:1750:19)
  at Parser.pp$3.parseExpression (/Users/felixdescoteaux/Projects/expenses/node_modules/acorn/dist/acorn.js:1722:19)
  at Parser.pp$1.parseStatement (/Users/felixdescoteaux/Projects/expenses/node_modules/acorn/dist/acorn.js:777:45)
  at Parser.pp$1.parseTopLevel (/Users/felixdescoteaux/Projects/expenses/node_modules/acorn/dist/acorn.js:672:23)
  at Parser.parse (/Users/felixdescoteaux/Projects/expenses/node_modules/acorn/dist/acorn.js:529:15)
  at Object.parse (/Users/felixdescoteaux/Projects/expenses/node_modules/acorn/dist/acorn.js:3378:37)
  at parse (/Users/felixdescoteaux/Projects/expenses/node_modules/detective/index.js:9:18)
  at Function.exports.find (/Users/felixdescoteaux/Projects/expenses/node_modules/detective/index.js:44:15)
  at module.exports (/Users/felixdescoteaux/Projects/expenses/node_modules/detective/index.js:23:20)
  at /Users/felixdescoteaux/Projects/expenses/node_modules/deppack/lib/explore.js:61:43
  at sourceFile (/Users/felixdescoteaux/Projects/expenses/node_modules/deppack/lib/explore.js:104:18)

I don't know why... it seems super odd to me, this string seems legit and it was added 1 year ago.
When copy pasting this function in the console, everything works fine

When I replace the string by an empty string my app compiles without any problem.

wtf? thanks! ๐Ÿ˜„

Support max batch size

Description of problem

Our application has a search bar that allows a user to search for items on our site. On click the search bar, relay grabs all data for the items that is searchable. When they select an item they are routed to a page that shows all items but the item that they clicked is focused (modal).

The problem is that because we are requesting more data on these items on this page, relay issues a node query for the missing data for each item (315 node queries). relay-network-layer correctly batches these together into one request but i get a 'payload too large' error (413).

Is there a way to specify a batch size so that i can avoid this issue?

How to pass a single DataLoader instance to all graphql request handlers?

I'm trying to implement DataLoader which batches together underlying database queries within a single request. I'm looking towards react-relay-network-layer to assist with the batching of GraphQL queries into a single HTTP request so that I can use a single DataLoader for all queries within that request.

I notice that graphqlBatchHTTPWrapper calls the graphqlHTTPMiddleware for each query. I need to make accessible to each graphqlHTTPMiddleware call the same instance of my loaders. How do you suggest that I go about this?

Prior to adding react-relay-network-layer, or any batching, just using the default relay network layer, I was simply instantiating my DataLoaders right within the express-graphql middleware function and passing them to the resolvers via rootValue. I can no longer do that there because that'll create new DataLoaders for each query. I need a single DataLoader per request.

Looking for your advice. Thanks!

[Server Side Rendering] `fetch` is not defined, in fetch wrapper

I'm implementing server side rendering, with isomorphic-relay. I also use your react-relay-network-layer.

On the client, after going through a webpack build process during which I can bundle in fetch globally, react-relay-network-layer works great.

On the server, however, there is no webpack build process and there is no global fetch function. I'm seeing errors when trying to renderToString, on the server: fetch is not defined`.

I went and looked at the RelayDefaultNetworkLayer that ships with Relay, and they go ahead and explicitly require("fetch"). That require will work on the client just fine, assuming a webpack build process but it also enable server side rendering because fetch won't be global there.

https://github.com/facebook/relay/blob/master/src/network-layer/default/RelayDefaultNetworkLayer.js#L17

Do you think react-relay-network-layer's fetch wrapper should require("fetch")?

C:\Users\Ryan\Projects\league\node_modules\promise\lib\done.js:10
      throw err;
      ^

ReferenceError: fetch is not defined
    at C:\Users\Ryan\Projects\league\node_modules\react-relay-network-layer\lib\fetchWrapper.js:39:14
    at new Promise (C:\Users\Ryan\Projects\league\node_modules\core-js\modules\es6.promise.js:191:7)
    at Promise.exports.(anonymous function).target.(anonymous function).function.target.(anonymous function).F (C:\Users\Ryan\Projects\league\node_modules\core-js\library\modules\_export.js:35:28)
    at fetchAfterAllWrappers (C:\Users\Ryan\Projects\league\node_modules\react-relay-network-layer\lib\fetchWrapper.js:37:12)
    at C:\Users\Ryan\Projects\league\node_modules\react-relay-network-layer\lib\middleware\auth.js:66:16
    at run (C:\Users\Ryan\Projects\league\node_modules\core-js\modules\es6.promise.js:87:22)
    at C:\Users\Ryan\Projects\league\node_modules\core-js\modules\es6.promise.js:100:28
    at flush (C:\Users\Ryan\Projects\league\node_modules\core-js\modules\_microtask.js:18:9)
    at _combinedTickCallback (internal/process/next_tick.js:67:7)
    at process._tickDomainCallback (internal/process/next_tick.js:122:9)

Refactor into multiple packages

Following #30, I think this module could provide just its core feature (support client-side middlewares for a Relay Network Layer) and then provide the built-ins middlewares as separated packages.
It would be relatively straightforward to implement, improve architecture and bundle size.

I can take care of this too @nodkz

authMiddleware stop working as of 2.0.0!

I noticed that the auth middleware just stopped working when updating to v2.0.0.

Here is the working config that was used before:

export const networkLayer = (store: Store<*, *>): RelayNetworkLayer =>
  new RelayNetworkLayer(
    [
      urlMiddleware({ url: graphQLURL }),
      authMiddleware({
        token: () => store.getState().auth.accessToken,
        tokenRefreshPromise: () => refreshToken(store.dispatch),
      }),
      next =>
        req => {
          req.headers['X-Stats-Timeout'] = statisticsTimeout;
          return next(req);
        },
    ],
    { disableBatchQuery: true },
  );

I then removed the batchQuery config as it was moved to another middleware but the auth middleware stopped working. It crashes on the, recently added, thrown error inside of the fetchWithMiddlewares-function https://github.com/nodkz/react-relay-network-layer/blob/master/src/fetchWithMiddleware.js#L25.

By throwing there the 401 error does not reach the 401 check inside of the authMiddleweare.

If you need more info just let me know ๐Ÿ˜„

Relay Modern equivalent?

Is there an equivalent to this library for Relay Modern? Or are there plans to adapt this library to Relay Modern?

how to update jwt in authMiddleware ?

Hello,

I use react-native and rnrf-relay-renderer with Relay

The networkLayer works fine if I closed the App and reopen it after sign in
but if new user for the app do sign in and redirected to home screen the token not updated and can't execute any queries

My layer code

import Config from './Config';
import LocalStore from 'react-native-simple-store';

import Relay from 'react-relay';
import {
  RelayNetworkLayer, retryMiddleware, urlMiddleware, authMiddleware, loggerMiddleware,
  perfMiddleware, gqErrorsMiddleware
} from 'react-relay-network-layer';

let mytoken = undefined;
 LocalStore.get('LoginData').then((User)=>{
   mytoken = User.authToken
 });

Relay.injectNetworkLayer(new RelayNetworkLayer([
  urlMiddleware({
    url: (req) => Config.GRAPHQL,
  }),
  loggerMiddleware(),
  gqErrorsMiddleware(),
  perfMiddleware(),
  retryMiddleware({
    fetchTimeout: 15000,
    retryDelays: (attempt) => [3200, 6400, 12800, 25600, 51200, 102400, 204800, 409600],
    statusCodes: [500, 503, 504]
  }),
  authMiddleware({
    token: () => mytoken
  }),

], { disableBatchQuery: true }));

could you help me to update token when sign in or sign out ?

__dataID__ unknown field

Hello

I'm using react-relay-network-layer and your relayStore with relay on web and when I post a mutation I got this error

message : "Variable "$input_0" got invalid value {"record":{"username":"admin","fullname":"ahmedAtia","phone":{"__dataID__":"client:-6654687382","phone":"055777098","kind":null,"localext":"123"},"address":{"__dataID__":"client:-6654687383","country":"SA","region":"SA","city":"Riyadh","street":"SA","knownmark":"sa"},"gender":"male"},"filter":{"_id":"58cfafd37b094855997ea271"},"clientMutationId":"0"}. In field "record": In field "phone": In field "__dataID__": Unknown field. In field "record": In field "address": In field "__dataID__": Unknown field."

I did not use the batch for Relay because I have another error with it .

Could you help me to solve my error plz ?

Token not refreshing after user logs out and login again with different user.

Below is the code implemented. It works for a single user. when user login is switched the token is not getting refreshed for the new token received from server. .

On server side:

networkLayer = !isBrowser() ? new Relay.DefaultNetworkLayer(GRAPHQL_URL,  {
      headers: {
        Authorization: store.get('jwt_token') ,
      },
      credentials: 'same-origin', 
    }
  ) : null;

On client side:

environment.injectNetworkLayer(new Relay.DefaultNetworkLayer(CLIENT_GRAPHQL_URL, { 
      headers: {
        Authorization: store.get('jwt_token') ,
      },
      credentials: 'same-origin',
     }));

On logout:

store.clearAll();

I also tried using react-relay-network-layer but facing same issue.

GET for Queries and POST for Mutation

express-graphql supports GET request as well and its great for caching with service worker.

It would be great if this network-layer provides option / flag to do GET instead of POST at least for queries

How do you use authMiddleware?

I want to set the auth token after login/register process using the token received

is it possible? do u have an example?

Ejecting express-middleware to its own module

  1. It doesn't really belong to the network layer. It's just a mean to support the "batching protocol" that should be separated from the rest.
  2. Not everybody use express. I use koa and I made koa-graphql-batch to use this network layer, thus I don't need express-middleware.
  3. If one doesn't bother to do properly minification/tree-shaking, the middleware code will end up client side where it's all but needed.

I could take care of this if @nodkz agrees.

Rendering partial response with error messages

Our GraphQL service relies on a number of downstream services, some of which have occasional outages. We'd like the Relay rendering to proceed when there are errors reported using partial data in the response.

One thing that we're really not clear on is how we might pass the information in the errors to the render. Is this something that anyone here could provide an example for? There is a suggestion in the relay issues, but it doesn't give any hint how one would transmit the error message(s) to the React components.

Flowtype failing on my repo

Hi, I believe 84b3361#diff-98bf0f0a5018817161c912b3742e1b02R17 should make credentials an optional property in Flow.

See the following Flow error I'm getting in my repo. Happy to submit a pull request if you agree with this change.

Error โ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆ node_modules/react-relay-network-layer/lib/middleware/batch.js.flow:162:46

Cannot assign object literal to req because property credentials is missing in object literal [1] but exists in
FetchOpts [2].

     node_modules/react-relay-network-layer/lib/middleware/batch.js.flow
     159โ”‚     // $FlowFixMe
     160โ”‚     const url = isFunction(opts.batchUrl) ? opts.batchUrl(requestMap) : opts.batchUrl;
     161โ”‚
 [1] 162โ”‚     const req: RRNLRequestObjectBatchQuery = {
     163โ”‚       url,
     164โ”‚       relayReqId: `BATCH_QUERY:${ids.join(':')}`,
     165โ”‚       relayReqMap: requestMap,
     166โ”‚       relayReqType: 'batch-query',
     167โ”‚       method: 'POST',
     168โ”‚       headers: {
     169โ”‚         Accept: '*/*',
     170โ”‚         'Content-Type': 'application/json',
     171โ”‚       },
     172โ”‚       body: `[${ids.map(id => requestMap[id].req.body).join(',')}]`,
     173โ”‚     };
     174โ”‚
     175โ”‚     return next(req)
     176โ”‚       .then(batchResponse => {

     node_modules/react-relay-network-layer/lib/definition.js.flow
 [2]  37โ”‚ export type RRNLRequestObjectBatchQuery = FetchOpts & {

tap into each retry

Hello!

I first must say thank you for an awesome library. It has solved many of our problems that we have and also enabled us to test out some of the deferred functionality that is coming.

I have a question about tapping into each retry. We currently have a loadingscreen that we would like to dynamically change after each retry. Basically it will display different messages and potentially write down a countdown until next retry. Is this possible with the current implementation of the library?

My current config looks like this:

const getCustomNetworkLayer = (jwt: string) => {
    return new RelayNetworkLayer([
        urlMiddleware({
            url: (req) => config.graphqlURL,
        }),
        retryMiddleware({
            fetchTimeout: 15000,
            retryDelays: [ 3000, 5000, 10000, 20000 ],
            statusCodes: [500, 503, 504],
        }),
        next => req => {
            req.headers["Authorization"] = `Bearer ${jwt}`;
            return next(req);
        },
    ], { disableBatchQuery: true });
};

Thanks again!

One of the sources for assign has an enumerable key on the prototype chain

I'm getting the following error when trying to use this package on react-native (0.55.4):

ExceptionsManager.js:71 Unhandled JS Exception: One of the sources for assign has an enumerable key on the prototype chain. Are you trying to assign a prototype property? We don't allow it, as this is an edge case that we do not support. This error is a performance optimization and not spec compliant.

The simulator is throwing the following error:

github screenshot 2018-06-05 17 49 09_preview

You can see the simulator is referencing fetchWithMiddleware.js (line: 41-13)

Any ideas why this is happening? I've looked everywhere to try and find a solution, none of them work.

I believe it has something todo with a middleware I added, however, commenting this out doesn't fix the error:

let headers = {
x: 'y',
z: 'x',
};

...otherMiddlewares
next =>
        req => {
          Object.assign(req.headers, headers);
          return next(req);
        },

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.