Code Monkey home page Code Monkey logo

re-graph's Introduction

re-graph

re-graph is a graphql client for Clojure and ClojureScript with bindings for re-frame applications.

Upgrade notice

🔥 Version 0.2.0 was recently released with breaking API changes. Please read the Upgrade guide for more information.

Notes

This library behaves like the popular Apollo client for graphql and as such is compatible with lacinia-pedestal.

Features include:

  • Subscriptions, queries and mutations
  • Supports websocket and HTTP transports
  • Works with Apollo-compatible servers like lacinia-pedestal
  • Queues websocket messages until ready
  • Websocket reconnects on disconnect
  • Simultaneous connection to multiple GraphQL services
  • Handles reauthentication without disruption

Contents

Usage

Add re-graph to your project's dependencies:

Clojars Project

This will also pull in re-graph.hato, a library for using re-graph on the JVM based on hato which requires JDK11. To use earlier JDKs, exclude re-graph.hato and include re-graph.clj-http-gniazdo.

If you are only targeting Javascript you do not need either of these libraries.

Clojars Project Clojars Project

;; For JDK 11+
[re-graph "x.y.z"]

;; For JDK 10-
[re-graph "x.y.z" :exclusions [re-graph.hato]]
[re-graph.clj-http-gniazdo "x.y.z"]

;; For Javascript only
[re-graph "x.y.z" :exclusions [re-graph.hato]]

Vanilla Clojure/Script

Call the init function to bootstrap it and then use subscribe, unsubscribe, query and mutate functions:

(require '[re-graph.core :as re-graph])

;; initialise re-graph, possibly including configuration options (see below)
(re-graph/init {})

(defn on-thing [{:keys [data errors] :as response}]
  ;; do things with data
))

;; start a subscription, with responses sent to the callback-fn provided
(re-graph/subscribe {:id        :my-subscription-id  ;; this id should uniquely identify this subscription
                     :query     "{ things { id } }"  ;; your graphql query
                     :variables {:some "variable"}   ;; arguments map
                     :callback  on-thing})           ;; callback-fn when messages are recieved

;; stop the subscription
(re-graph/unsubscribe {:id :my-subscription-id})

;; perform a query, with the response sent to the callback event provided
(re-graph/query {:query     "{ things { id } }" ;; your graphql query
                 :variables {:some "variable"}  ;; arguments map
                 :callback  on-thing})          ;; callback event when response is recieved

;; shut re-graph down when finished
(re-graph/destroy {})

re-frame users

Dispatch the init event to bootstrap it and then use the :subscribe, :unsubscribe, :query and :mutate events:

(require '[re-graph.core :as re-graph]
         '[re-frame.core :as re-frame])

;; initialise re-graph, possibly including configuration options (see below)
(re-frame/dispatch [::re-graph/init {}])

(re-frame/reg-event-db
  ::on-thing
  [re-frame/unwrap]
  (fn [db {:keys [response]}]
    (let [{:keys [data errors]} response]
      ;; do things with data e.g. write it into the re-frame database
    )))

;; start a subscription, with responses sent to the callback event provided
(re-frame/dispatch [::re-graph/subscribe
                    {:id        :my-subscription-id  ;; this id should uniquely identify this subscription
                     :query     "{ things { id } }"  ;; your graphql query
                     :variables {:some "variable"}   ;; arguments map
                     :callback  [::on-thing]}])      ;; callback event when messages are recieved

;; stop the subscription
(re-frame/dispatch [::re-graph/unsubscribe {:id :my-subscription-id}])

;; perform a query, with the response sent to the callback event provided
(re-frame/dispatch [::re-graph/query
                    {:id        :my-query-id         ;; unique id for this query (optional, used for deduplication)
                     :query     "{ things { id } }"  ;; your graphql query
                     :variables {:some "variable"}   ;; arguments map
                     :callback  [::on-thing]}])      ;; callback event when response is recieved

;; shut re-graph down when finished
(re-frame/dispatch [::re-graph/destroy {}])

Options

Options can be passed to the init event, with the following possibilities:

(re-frame/dispatch
  [::re-graph/init
   {:ws {:url                     "wss://foo.io/graphql-ws" ;; override the websocket url (defaults to /graphql-ws, nil to disable)
         :sub-protocol            "graphql-ws"              ;; override the websocket sub-protocol (defaults to "graphql-ws")
         :reconnect-timeout       5000                      ;; attempt reconnect n milliseconds after disconnect (defaults to 5000, nil to disable)
         :resume-subscriptions?   true                      ;; start existing subscriptions again when websocket is reconnected after a disconnect (defaults to true)
         :connection-init-payload {}                        ;; the payload to send in the connection_init message, sent when a websocket connection is made (defaults to {})
         :impl                    {}                        ;; implementation-specific options (see hato for options, defaults to {}, may be a literal or a function that returns the options)
         :supported-operations    #{:subscribe              ;; declare the operations supported via websocket, defaults to all three
                                    :query                  ;;   if queries/mutations must be done via http set this to #{:subscribe} only
                                    :mutate}
        }

    :http {:url    "http://bar.io/graphql"   ;; override the http url (defaults to /graphql)
           :impl   {}                        ;; implementation-specific options (see clj-http or hato for options, defaults to {}, may be a literal or a function that returns the options)
           :supported-operations #{:query    ;; declare the operations supported via http, defaults to :query and :mutate
                                   :mutate}
          }
   }])

Either :ws or :http can be set to nil to disable the WebSocket or HTTP protocols.

Multiple instances

re-graph now supports multiple instances, allowing you to connect to multiple GraphQL services at the same time. All function/event signatures now take an optional instance-name as the first argument to let you address them separately:

(require '[re-graph.core :as re-graph])

;; initialise re-graph for service A
(re-graph/init {:instance-id :service-a
                :ws {:url "wss://a.com/graphql-ws}})

;; initialise re-graph for service B
(re-graph/init {:instance-id :service-b
                :ws {:url "wss://b.com/api/graphql-ws}})

(defn on-a-thing [{:keys [data errors] :as payload}]
  ;; do things with data from service A
))

;; subscribe to service A, events will be sent to the on-a-thing callback
(re-graph/subscribe {:instance-id :service-a    ;; the instance-name you want to talk to
                     :id :my-subscription-id    ;; this id should uniquely identify this subscription for this service
                     :query "{ things { a } }"
                     :callback on-a-thing})

(defn on-b-thing [{:keys [data errors] :as payload}]
  ;; do things with data from service B
))

;; subscribe to service B, events will be sent to the on-b-thing callback
(re-graph/subscribe {:instance-id :service-b    ;; the instance-name you want to talk to
                     :id :my-subscription-id
                     :query "{ things { a } }"
                     :callback on-b-thing})

;; stop the subscriptions
(re-graph/unsubscribe {:instance-id :service-a
                       :id :my-subscription-id})
(re-graph/unsubscribe {:instance-id :service-b
                       :id :my-subscription-id})

Authentication

There are several methods of authenticating with the server, with various trade-offs. Most complications relate to the websocket connection from the browser, as the usual method of providing an Authorization header is not (currently) possible.

Headers

The most conventional way to authenticate is to use HTTP headers on requests to the server and include an authentication token:

(re-frame/dispatch
  [::re-graph/init
    {:ws {:impl {:headers {:Authorization "my-auth-token"}}}
     :http {:impl {:headers {:Authorization "my-auth-token"}}}}])

This will work for the following cases:

  • JVM for websockets and http (using hato)
  • Browser for http only

Note that it will not work for websocket connections from the browser, for which you will have to choose one of the other methods described below.

Connection init payload

For websocket connections, the de-facto Apollo spec defines a connection_init message which is sent after the websocket connection has been established, but before any GraphQL traffic. This can be used to contain an authentication token which can be associated with the connection, or the connection can be terminated.

(re-frame/dispatch
  [::re-graph/init
    {:ws {:connection-init-payload {:token "my-auth-token"}}}])

Note that for Hasura, and possibly other Apollo server backed instances, your payload may need to look like {:headers {:authorization (str "Bearer " jwt)}}

Cookies

When using re-graph within a browser, site cookies are shared between HTTP and WebSocket connection automatically. There's nothing special that needs to be done.

When using re-graph with Clojure, however, some configuration is necessary to ensure that the same cookie store is used for both HTTP and WebSocket connections.

Before initialising re-graph, create a common HTTP client.

(ns user
  (:require
    [hato.client :as hc]
    [re-graph.core :as re-graph]))

(def http-client (hc/build-http-client {:cookie-policy :all}))

See the hato documentation for all the supported configuration options.

When initialising re-graph, configure both the HTTP and WebSocket connections with this client:

(re-graph/init {:http {:impl {:http-client http-client}}
                :ws   {:impl {:http-client http-client}}})

In the call, you can provide any supported re-graph or hato options. Be careful though; hato convenience options for the HTTP client will be ignored when using the :http-client option.

If you are using lacinia, you probably need to use the :init-context option of the listener-fn-factory to be able to extract the cookie from the underlying webserver request.

Token in query param

You can put a token in the http and websocket urls and use it to authenticate when handling the request.

(re-frame/dispatch
  [::re-graph/init
    {:http {:url "https://my-server.com/graphql?auth=my-auth-token"}
     :ws {:url "wss://my-server.com/graphql-ws?auth=my-auth-token"}}])

Note that query params may be included in the log files of the server.

Basic auth

You can put basic auth in the http and websocket urls and use it to authenticate when handling the request.

(re-frame/dispatch
  [::re-graph/init
    {:http {:url "https://my-user:[email protected]/graphql"}
     :ws {:url "wss://my-user:[email protected]/graphql-ws"}}])

Sub-protocol hack

As mentioned in https://github.com/whatwg/html/issues/3062 it is contentious but possible to smuggle authentication in the websocket sub-protocol, which normally describes the kind of traffic expected over the websocket (the default in re-graph is graphql-ws).

(re-frame/dispatch
  [::re-graph/init
    {:ws {:sub-protocol "graphql-ws;my-auth-token"}}])

Re-initialisation

When initialising re-graph you may have included authorisation tokens e.g.

(re-frame/dispatch [::re-graph/init {:http {:url "http://foo.bar/graph-ql"
                                            :impl {:headers {"Authorization" 123}}}
                                     :ws {:connection-init-payload {:token 123}}}])

If those tokens expire you can refresh them using re-init as follows which allows you to change any parameter provided to re-graph:

(re-frame/dispatch [::re-graph/re-init {:http {:impl {:headers {"Authorization" 456}}}
                                        :ws {:connection-init-payload {:token 456}}}])

The connection-init-payload will be sent again and all future remote calls will contain the updated parameters.

Development

cider-jack-in-clj&cljs

CLJS tests are available at http://localhost:9500/figwheel-extra-main/auto-testing You will need to run (re-graph.integration-server/start!) for the integration tests to pass.

CircleCI

License

Copyright © 2017 oliyh

Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version.

re-graph's People

Contributors

atrox avatar chrisbetz avatar gabrielnau avatar henryw374 avatar iku000888 avatar jmarca avatar loomis avatar madstap avatar oliyh avatar polymeris avatar qiuyin avatar r0man avatar timgilbert avatar viebel 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  avatar  avatar  avatar  avatar

re-graph's Issues

hangs in clojure

Hi, I'm trying to get re-graph working in a clojure command-line app. Following the examples, nothing happens, and the program will not exit, even if I add (shutdown-agents) at the end. E.g.

(defn -main
  [& args]

  (println "entering main")
  (re-graph/init {:ws-url   "ws://localhost:3086/graphql-ws"
                  :http-url "http://localhost:3085/graphql"})
  (println "exiting main")
  (shutdown-agents))

Running it:

$ clojure -M -m mina.repl
entering main
exiting main
...hangs...

Adding the example code to send a query has no effect. What am I doing wrong?

::destroy dispatches ::destroy?

Hi @oliyh !

This has been really useful, thanks!

I noticed that calling re-graph.core/destroy throws a runtime error, and seemed like commenting this line out made the error stop.

:dispatch [::destroy instance-name]}

I have not looked into the internals much, but it would be awesome if you could highlight some intentions.

More than happy to provide more details as well.

Subscription to Lacinia Pedestal back end: Getting just the first event

Hi, first of all, thanks and congratulations for such a nice tool / project !

I am coding a pedagogical project, implementing a full stack GraphQL solution, porting to Clojure/script a full stack GraphQL Apollo tutorial, in order to get started both in Clojure/script and in GraphQL and, by the way, share the experience, assuming it might be useful for others as well.

I have already implemented all the articles there on server-side. Among them, and for this question, the most relevant shows how to implement a couple server-side subscriptions.

I have been able to port to Clojure both those subscriptions: either the one for new links, and the other one, for votes. Both seem to be working fine, and accesible by means of the GraphiQL tools supplied with Lacinia Pedestal.

I have just coded the corresponding front-end article.

The issue I am currently having is:

  1. as I post a new link by means of the GraphiQL tool, it gets pushed right both to the subscription activated in a second instance of GraphiQL, and in the initial links list implemented in my application's front-end
  2. however, as I create further links, although they are pushed right to GraphiQL, they are not reaching my front-end

I have detected an exception thrown on the server-side, in the context of Lacinia Pedestal, as soon as I post the second link creation by GraphiQL, and on, shown in the following attachment:
exception.txt

I have also attached Lacinia Pedestal's logged events, with its log in DEBUG level:
lacinia-pedestal-log-20190429T193306.txt

There, besides the loop corresponding to GraphiQL, identified by single-digit ids, I have detected another two ones:

  1. one for the front-end's subscription, identified by subscribe-to-new-links id, and
  2. another one, identified by ki1m2mj6: it doesn't correspond to any subscription, but to the GraphQL query which loads the links' list. It seems precisely this loop the only one reaching the context in which the exception is thrown:

19:34:57.485 [async-dispatch-29] DEBUG c.w.lacinia.pedestal.subscriptions - {:event :com.walmartlabs.lacinia.pedestal.subscriptions/cleanup-ch, :id "ki1m2mj6", :line 112}

According to the following print screens, re-graph's section in re-frame db seems fine:

Captura de pantalla 2019-04-30 a la(s) 04 46 28

Captura de pantalla 2019-04-30 a la(s) 04 24 45

I guess I am skipping some necessary config related with re-graph, but I have not been able to find out which one...

In the init function in core.cljs, I began with an empty map, {}, then I have added :resume-subscriptions?, set both to true and then false, and then :connection-init-payload {}, even though that is precisely a default value...

(defn ^:export init []
  (routes/app-routes)
  ;; initialise re-graph, including configuration options
  (re-frame/dispatch [::re-graph/init {:resume-subscriptions?   false
                                       :connection-init-payload {}}])
  (re-frame/dispatch-sync [::events/initialize-db])
  (re-frame/dispatch-sync [::events/subscribe-2-new-links])
  (dev-setup)
  (mount-root))

What am I doing wrong?

Thanks in advance and, again, congratulations !

Luis
https://promesante.github.io/
https://github.com/promesante

Access HTTP status codes

Is there any interest in providing access to the HTTP status code of a failed query? It would allow us to differentiate between graphql-level errors and HTTP-level errors (for example, a 403 when a user's session is expired).
I would be willing to craft a PR if there is interest.

Websocket "connection_init" message

Hello,

Thanks for this library, it's really great to not have to interop with Apollo.

I am looking on how to add authorization, and following your discussion I landed on this PR in lacinia-pedestal which didn't seem to have much activity, so I submitted a new one.

In the original PR and in mine too, the connection params are extracted from the payload of the connection_init message, because it seems to be apollo's behaviour.

I'm able to override re-graph's event handler to add the connection_init message but it makes me duplicate the whole handler code: https://gist.github.com/gabrielnau/6817c938754a182a796366c572f7607d

I wonder if you would be interested in adding this in re-graph ? I didn't opened a PR because it changes a lot of details, and you certainly have a strong opinion on how to approach it.

Cheers

Reload website -> re-frame: no :event handler registered for: <graphql-query>

Hi @oliyh ,

Given

  • Using re-graph with re-frame
  • WS connection ready true

When

  • Refresh/Reload page via browser (also on entering the url)
  • {:dispatch [::re-graph/query
    (str "list { list(where: {id: {_eq: "" id ""}})"
    "{id name "
    "listItemsBylistId {id value author}}}")

Then

  • WS connection ready false
  • Error re-frame: no :event handler registered for: list { list(where: {id: {_eq: "cb0caadc-318a-490c-8334-3371487cdf8b"}}){id name listItemsBylistId {id value author}}} occurs

Ok, what i found was in core.cljs line 44
(get-in db [:websocket :ready?]) is false
Therefore core.cljs line 54
{:db (update-in db [:websocket :queue] conj event)} will be executed. The event is the query list {list stuff. And then I think some interceptor search for the event in re-frame and doesn't find it.

If using the http query everything works.
Also if I navigate in the web app itself, it works (with websockets). Problem is only on re-load.

Thanks for your work.

Support servers which don't allow query or mutation on websocket

It turns out that some servers by design don't support queries or mutations over websocket, but only via HTTP.

re-graph currently prefers all operations via websocket when available and treats HTTP as a fallback. An option should be exposed to only allow some operations via a particular transport to support these other servers.

This is my conversation with @gklijs on Slack:

gklijs  6 hours ago
Mutations over websocket is a bit of weird one, just like queries. Some servers and clients support them and other don't. And it's often not clear.

oliy  5 hours ago
oh i didn't realise that. i see no reason why queries and mutations couldn't take place over any transport - http, websocket, channels etc

oliy  5 hours ago
graphql is transport agnostic after all

oliy  5 hours ago
websockets should give you lower latency, avoid duplication of authentication etc

oliy  5 hours ago
but regardless of what should be, if you are telling me that some servers only support subs over websocket but that queries and mutations must go via http, then this might be something that re-graph should support

oliy  5 hours ago
this is something i didn't realise and i've spent a long time trying to tell people their servers are wrong :cry:

gklijs  5 hours ago
Yes, that's my opinion as well. But graphql-java and kotlin-graphql see it differently. The crazy thing is the mutation/query is excecuted but the result is never send to the client.. As far as I've seen usages of the appollo client it also seems to use websockets just for subscriptions.

gklijs  5 hours ago
It took me some time what was going wrong when trying out those server libraries as well. It would be great if in time there was a better spec for graphql over websockets, and with that some tools to test server implementations.

gklijs  5 hours ago
I think re-graph does it right, for those other servers I could easily change the code to support multiple endpoints. The guild is also trying to get ownership of the code that contains the Protocol most use.

gklijs  5 hours ago
It's also pretty easy, to make it work with queries and mutations. Micronaut-graphql does support queries and mutations over websockets.

oliy  5 hours ago
The GraphQL spec says virtually nothing about subscriptions, it seems like the defacto standard is Apollo because they do things first

oliy  5 hours ago
It would be a small change in re-graph to add an option to specify which messages go over which transport

gklijs  1 hour ago
Yes, it would be a nice improvement.

Custom payload for queries, mutations and subscriptions

Some GraphQL servers requires to pass other keys to the payload than query and variables.

For example, this is the subscription query in AWS AppSync:

{
    'id': SUB_ID,
    'payload': {
        'data': GQL_SUBSCRIPTION,
        'extensions': {
            'authorization': {
                'host':HOST,
                'x-api-key':API_KEY
            }
        }
    },
    'type': 'start'
}

Here is the doc.

It doesn't seem possible at the current stage but maybe exposing a lower level event taking a payload map could do the trick in the future?

For information, AppSync also don't support other methods than subscriptions through Websockets as mentioned in #65 .

HTTP library parameters should be adjustable

There seems to be a regression of #13 in that the extra parameters [:http :impl] are now a static map rather than a callback function. This makes it harder to cope with e.g. session tokens timing out and wanting to refresh them without destroying the entire re-graph instance.

This may also help with #70 and and #67 as well.

Add example how to use for Clojure.

I tried to use re-graph from Clojure, but could not get it to work. I tried several things, the latest using the 'plain javascript' style.

  (println "data:" data))

(defn init []
  (re-graph/init {:ws-url   "ws://localhost:8888/graphql-ws"
                  :http-url "http://localhost:8888/graphql"})
  (re-graph/query "{all_last_transactions {iban}}" nil on-thing))```
But there doesn't seem to happen much. When I us a key for the init and query it gives an exception: `Host URL cannot be nil`.

Callback/ event call on init

Hi I may have missed something, but how do I specify an event to happen after the init is successful.

I seem to be getting errors if I dispatch queries before the websocket is up

HTTP headers

Hey @oliyh,

I would like to set some headers on the HTTP requests sent to the server. One use case is set to the the "authorization" header. Another thing I had in mind is receiving transit+json, instead of plain json depending on the "Accept" header. The idea is to convert the response into proper "Clojure" format ("-" instead of "_" in identifiers) and use transit serialization into clojure maps instead of the slower js->clj approach. Not sure how this would work with a websocket connection, though.

What do you think?

Roman

Re-graph testing issue

Hey there, I have been having testing issues recently using re-graph.

Background

  1. lein doo as testing library and chrome-headless as js-env.
  2. [day8.re-frame.test :as rf-test] for re-frame events

Problem

When I run my tests using lein doo chrome-headless test once I will get the output from the console

?[33m08 03 2019 14:41:24.874:WARN [web-server]: ?[39m404: /js/compiled/test/out/goog/deps.js
?[33m08 03 2019 14:41:24.876:WARN [web-server]: ?[39m404: /js/compiled/test/out/cljs_deps.js
HeadlessChrome 72.0.3626 (Windows 10.0.0) WARN: ?[36m're-frame: overwriting', ':event', 'handler for:', {ns: 'slm.olanext.events', name: 'loan-amount.set-in-or-out-of-state', fqn: 'slm.olanext.events/loan-amount.set-in-or-out-of-state', _hash: 1473332321, cljs$lang$protocol_mask$partition0$: 2153775105, cljs$lang$protocol_mask$partition1$: 4096}?[39m


?[1A?[2KHeadlessChrome 72.0.3626 (Windows 10.0.0): Executed 0 of 1?[32m SUCCESS?[39m (0 secs / 0 secs)
?[1A?[2KLOG: ?[36m'Testing slm.olanext.validation-test'?[39m
HeadlessChrome 72.0.3626 (Windows 10.0.0): Executed 0 of 1?[32m SUCCESS?[39m (0 secs / 0 secs)
?[1A?[2KDEBUG: ?[36m'Ignoring graphql-ws event ', {ns: 're-graph.internals', name: 'default', fqn: 're-graph.internals/default', _hash: -1918427164, cljs$lang$protocol_mask$partition0$: 2153775105, cljs$lang$protocol_mask$partition1$: 4096}, ' - ', 'connection_ack'?[39m
HeadlessChrome 72.0.3626 (Windows 10.0.0): Executed 0 of 1?[32m SUCCESS?[39m (0 secs / 0 secs)
?[33m08 03 2019 14:41:55.561:WARN [HeadlessChrome 72.0.3626 (Windows 10.0.0)]: ?[39mDisconnected (0 times), because no message in 30000 ms.
?[1A?[2K?[31mHeadlessChrome 72.0.3626 (Windows 10.0.0) ERROR?[39m
  Disconnected, because no message in 30000 ms.
HeadlessChrome 72.0.3626 (Windows 10.0.0): Executed 0 of 1?[31m DISCONNECTED?[39m (30.015 secs / 0 secs)
?[1A?[2KHeadlessChrome 72.0.3626 (Windows 10.0.0): Executed 0 of 1?[31m DISCONNECTED?[39m (30.015 secs / 0 secs)
Subprocess failed

And in my graph-ql service I get the following stack trace

system=> ERROR com.walmartlabs.lacinia.pedestal.subscriptions - {:event :com.walmartlabs.lacinia.pedestal.subscriptions/error, :line 506}
org.eclipse.jetty.io.EofException: null
        at org.eclipse.jetty.io.ChannelEndPoint.flush(ChannelEndPoint.java:292)
        at org.eclipse.jetty.io.WriteFlusher.flush(WriteFlusher.java:429)
        at org.eclipse.jetty.io.WriteFlusher.write(WriteFlusher.java:322)
        at org.eclipse.jetty.io.AbstractEndPoint.write(AbstractEndPoint.java:372)
        at org.eclipse.jetty.websocket.common.io.FrameFlusher$Flusher.flush(FrameFlusher.java:153)
        at org.eclipse.jetty.websocket.common.io.FrameFlusher$Flusher.process(FrameFlusher.java:217)
        at org.eclipse.jetty.util.IteratingCallback.processing(IteratingCallback.java:241)
        at org.eclipse.jetty.util.IteratingCallback.iterate(IteratingCallback.java:224)
        at org.eclipse.jetty.websocket.common.io.FrameFlusher.enqueue(FrameFlusher.java:382)
        at org.eclipse.jetty.websocket.common.io.AbstractWebSocketConnection.outgoingFrame(AbstractWebSocketConnection.java:614)
        at org.eclipse.jetty.websocket.common.io.AbstractWebSocketConnection.onConnectionStateChange(AbstractWebSocketConnection.java:473)
        at org.eclipse.jetty.websocket.common.io.IOState.notifyStateListeners(IOState.java:184)
        at org.eclipse.jetty.websocket.common.io.IOState.onReadFailure(IOState.java:498)
        at org.eclipse.jetty.websocket.common.io.AbstractWebSocketConnection.readParse(AbstractWebSocketConnection.java:666)
        at org.eclipse.jetty.websocket.common.io.AbstractWebSocketConnection.onFillable(AbstractWebSocketConnection.java:511)
        at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:279)
        at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:112)
        at org.eclipse.jetty.io.ChannelEndPoint$2.run(ChannelEndPoint.java:124)
        at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:672)
        at org.eclipse.jetty.util.thread.QueuedThreadPool$2.run(QueuedThreadPool.java:590)
        at java.lang.Thread.run(Thread.java:748)
Caused by: java.io.IOException: An existing connection was forcibly closed by the remote host
        at sun.nio.ch.SocketDispatcher.writev0(Native Method)
        at sun.nio.ch.SocketDispatcher.writev(SocketDispatcher.java:55)
        at sun.nio.ch.IOUtil.write(IOUtil.java:148)
        at sun.nio.ch.SocketChannelImpl.write(SocketChannelImpl.java:504)
        at org.eclipse.jetty.io.ChannelEndPoint.flush(ChannelEndPoint.java:272)
        ... 20 common frames omitted
ERROR com.walmartlabs.lacinia.pedestal.subscriptions - {:event :com.walmartlabs.lacinia.pedestal.subscriptions/error, :line 506}
org.eclipse.jetty.io.EofException: null
        at org.eclipse.jetty.io.ChannelEndPoint.flush(ChannelEndPoint.java:292)
        at org.eclipse.jetty.io.WriteFlusher.flush(WriteFlusher.java:429)
        at org.eclipse.jetty.io.WriteFlusher.write(WriteFlusher.java:322)
        at org.eclipse.jetty.io.AbstractEndPoint.write(AbstractEndPoint.java:372)
        at org.eclipse.jetty.websocket.common.io.FrameFlusher$Flusher.flush(FrameFlusher.java:153)
        at org.eclipse.jetty.websocket.common.io.FrameFlusher$Flusher.process(FrameFlusher.java:217)
        at org.eclipse.jetty.util.IteratingCallback.processing(IteratingCallback.java:241)
        at org.eclipse.jetty.util.IteratingCallback.iterate(IteratingCallback.java:224)
        at org.eclipse.jetty.websocket.common.io.FrameFlusher.enqueue(FrameFlusher.java:382)
        at org.eclipse.jetty.websocket.common.io.AbstractWebSocketConnection.outgoingFrame(AbstractWebSocketConnection.java:614)
        at org.eclipse.jetty.websocket.common.io.AbstractWebSocketConnection.onConnectionStateChange(AbstractWebSocketConnection.java:473)
        at org.eclipse.jetty.websocket.common.io.IOState.notifyStateListeners(IOState.java:184)
        at org.eclipse.jetty.websocket.common.io.IOState.onReadFailure(IOState.java:498)
        at org.eclipse.jetty.websocket.common.io.AbstractWebSocketConnection.readParse(AbstractWebSocketConnection.java:666)
        at org.eclipse.jetty.websocket.common.io.AbstractWebSocketConnection.onFillable(AbstractWebSocketConnection.java:511)
        at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:279)
        at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:112)
        at org.eclipse.jetty.io.ChannelEndPoint$2.run(ChannelEndPoint.java:124)
        at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:672)
        at org.eclipse.jetty.util.thread.QueuedThreadPool$2.run(QueuedThreadPool.java:590)
        at java.lang.Thread.run(Thread.java:748)
Caused by: java.io.IOException: An existing connection was forcibly closed by the remote host
        at sun.nio.ch.SocketDispatcher.writev0(Native Method)
        at sun.nio.ch.SocketDispatcher.writev(SocketDispatcher.java:55)
        at sun.nio.ch.IOUtil.write(IOUtil.java:148)
        at sun.nio.ch.SocketChannelImpl.write(SocketChannelImpl.java:504)
        at org.eclipse.jetty.io.ChannelEndPoint.flush(ChannelEndPoint.java:272)
        ... 20 common frames omitted

It seems as though the root of this issue lies within calling ::re-graph/init, here is my test

(deftest foo-test
  (rf-test/run-test-async
   (re-frame/dispatch-sync [::events/initialize-db])
   (re-frame/dispatch [::re-graph/init {:ws-url "ws://localhost:8888/graphql-ws"
                                        :http-url "http://localhost:8888/graphql"
                                        :http-parameters {:with-credentials? false
                                                          :oauth-token "Secret"}
                                        :ws-reconnect-timeout 2000
                                        :resume-subscriptions? true
                                        :connection-init-payload {}}])
   (let [sut (re-frame/subscribe [::subs/validation-message :foo :bar :buzz])]
     (re-frame/dispatch [::events/general-info.set-first-name :foo "lipsum"])
     (rf-test/wait-for [::events/on-field-validity]
                       (is (not (empty? @sut)))))))

Any ideas what my be the problem?

Deregister subscription on error?

A bad query will result in an error message on the subscription id. This might mean that no more messages will be sent to that id, meaning it's handler should be deregistered so that it can be tried again.

Doesn't work in react native

The function internals/default-sw-url refers to js/window.location which doesn't exist in react native. I released a fork under a different maven namespace so I could go on working. I just commented out the offending function and callsite.

I'm happy to make a pull request if it is wanted. To keep the current behavior in browsers you could use something like (when (exists? (.-location js/window)) ,,,).

Function wrappers for non-re-framers

Allow the following:

(require [re-graph.core :as re-graph]
         [re-frame.core :as re-frame])

;; initialise re-graph, possibly including configuration options (see below)
(re-graph/init {})

(defn on-thing [thing]
  ...)

;; start a subscription, with responses sent to the callback event provided
(re-graph/subscribe
  :my-subscription-id  ;; this id should uniquely identify this subscription
  "{ things { id } }"  ;; your graphql query
  {:some "variable"}   ;; arguments map
  on-thing)       ;; callback event when messages are recieved

;; stop the subscription
(re-graph/unsubscribe :my-subscription-id)

;; perform a query, with the response sent to the callback event provided
(re-graph/query
  "{ things { id } }"  ;; your graphql query
  {:some "variable"}   ;; arguments map
  on-thing)       ;; callback event when response is recieved

[Feature request]: Aborting queries

Hi. It would be nice if I was able to abort query, e.g. if user changes page before previous one was fully loaded.

I found that the http library seems to support that, but there is no access to the channels from re-graph: https://github.com/r0man/cljs-http/blob/235d45d889f93d3ea79fba772d243eaa122806cc/src/cljs_http/core.cljs#L11

Maybe If I could somehow tag the query and then was able to cancel all queries tagged with the tag?

;; loading dashboard data with 2 queries to not block in case one query will take significantly longer than other
(re-frame/dispatch [::re-graph/query
                    "{ things { id } }"
                    {:some "variable"}
                    [::on-thing]
                    {:tag :dashboard}])
(re-frame/dispatch [::re-graph/query
                    "{ things2 { id } }"
                    {:some "variable"}
                    [::on-thing]
                    {:tag :dashboard}])

;;;
;;; Go to different page
;;;

;; cancel any in-progress requests from previous page
(re-frame/dispatch [::re-graph/cancel-query {:tag #{:dashboard}}])

;; load current page data
(re-frame/dispatch [::re-graph/query
                    "{ user_things { id } }"
                    {:some "variable"}
                    [::on-thing]
                    {:tag :user-list}])

Queries and Mutations: websockets or HTTP?

Hi, Oliver,

After having finished implementing subscriptions in my GraphQL project, queries and mutations implemented beforehand didn't work any longer.

Analyzing the implementation of re-graph, lacinia-pedestal and lacinia, data involved succesfully reached ::send-ws event in internals.cljs. But I have not been able to detect any corresponding event on the server side (lacinia-pedestal).

Then I experimented commenting websocket invocation in ::query and ::mutate events in core.cljs, and the invocation immediately began to work well again.

I have forked re-graph and shared this modification there.

By the way, I have been using re-graph's 0.1.8 version, which I have taken as reference, from which I've performed a branch called 'hn-clj-pedestal-re-frame'.

Your comments and suggestions are more than welcome.

Thanks

Luis
https://promesante.github.io/
https://github.com/promesante

Trying to connect to wrong WebSocket after ::re-graph/init

We dispatch ::re-graph/init with:

{:ws-url "ws://localhost:8888/graphql-ws"
 :http-url "http://localhost:8888/graphql"
 ...}

With re-graph 0.1.6 the re-frame database contains afterwards:

:re-graph {
  {:ws-url "ws://localhost:8888/graphql-ws"
   :http-url "http://localhost:8888/graphql"
   ...}; This is REALLY the key of the following value!
  {:websocket {:url "ws://localhost:9500/graphql-ws" ...}
   :http-url "/graphql"
   ...}
}

With 0.1.4 and otherwise the same code the database would contain:

:re-graph {
  :websocket {:url "ws://localhost:8888/graphql-ws" ...}
  :http-url "http://localhost:8888/graphql"
  ...
}

[Question] caching and re-graph

Does re-graph also caching like the apollo-client?
I know the apollo client has some pretty decent caching, deconstructing the queries and fetching only unknown entities. So, basically:

  1. is re-graph doing query deconstruction?
  2. is re-graph doing any kind of query/entity caching?

thx

difficulty connecting to Elixir/Phoenix/Absinthe server

First of all, I don't really understand the ins and outs of web socket connections, so this bug report isn't very good.

My server side uses Elixir/Phoenix/Absinthe. (I can change that sure, but that means a lot of work)

I can use re-graph no problem to execute all graphql queries/mutations/etc over http.

The problem I'm having is setting up subscriptions, which needs a websocket link. I've tried a number of things, but nothing seems to work. Either I get a silent 400 error from the server, or else if I force a certain path, I can see a connection happening on my server, but with an error:

[debug] CONNECT IN USER SOCKET; connect_info are %{}; params are %{}
[info] CONNECTED TO RouteServerWeb.UserSocket in 203µs
  Transport: :websocket
  Serializer: Phoenix.Socket.V1.JSONSerializer
  Connect Info: %{}
  Parameters: %{}
[error] Ranch ... elixir error message ...exit with reason: {%Phoenix.Socket.InvalidMessageError{message: "missing key \"topic\""}

It is entirely possible that my server is not configured properly, but I am able to get things working with the graphiql client, so I think the server is okay. In graphiql advanced mode, I set the ws url to:

ws://172.18.0.5:4000/api

(although it actually seems to hit 172.18.0.5:4000/api/websocket?vsn=2.0.0)

and then I get this on the server:

[debug] CONNECT IN USER SOCKET; connect_info are %{}; params are %{"vsn" => "2.0.0"}
[info] CONNECTED TO RouteServerWeb.UserSocket in 212µs
  Transport: :websocket
  Serializer: Phoenix.Socket.V2.JSONSerializer
  Connect Info: %{}
  Parameters: %{"vsn" => "2.0.0"}
[info] JOINED __absinthe__:control in 54µs
  Parameters: %{}
[debug] ABSINTHE schema=RouteServerWeb.Schema variables=%{}
---
subscription {routes (veh: 1) {
  uid
  veh
  wkt
}}

The big difference seems to be that the graphiql client produced by Absinthe tacks on a channel topic __absinthe__:control How do I do that in re-graph on connection?

But I'm really grasping at straws here.

GraphQL error for :re-graph.internals/default: argument references undeclared variable

Client

(defn query-countries [query-str callback]
  (re-frame/dispatch
   [::re-graph/query
    "{
      get_countries(query_str: $query_str) {
        countries
      }
    }"
    {:query_str query-str}
    callback]))

Server (lacinia)

...
:queries
 {:get_countries {:type :Countries
                  :description "List of countries"
                  :args {:query_str {:type (non-null String)}}
                  :resolve :query/get-countries}}
...

The following error occurs:

internals.cljs?rel=1546474294888:159 GraphQL error for :re-graph.internals/default - u55kwt3z: Exception applying arguments to field `get_countries': For argument `query_str', argument references undeclared variable `query_str'.

Code works when query_str is hardcoded, but variables per graphql spec are not recognized.

Suggestions?

Parsing exceptions from cljs-http are unhandled

image

I tried putting a try catch block around (a/<! response-chan)] but that error seems to be happening in another async block in the cljs-http library so it doesn't get caught in the try block in my code.

Not sure how can this can be handled in this library ( seems like a bug in cljs-http library), i am opening an issue here to see if i am missing something obvious.

Destroy function

Should close websocket connections (unsubscribe subs first?) and destroy all re-graph state.

No default instance of re-graph found but no valid instance name was provided

I'm using your re-partee with a working lacinia backend, where get_countries query returns the same list as repartee.fake.server.fetch-suggestions. However re-frame/re-graph throws an error

What am I missing?

(re-frame/reg-event-fx
    ::fetch-suggestions
    (fn [_ [_ q callback]]
      (js/setTimeout
       (fn []
         (re-frame/dispatch
          [::re-graph/query
           "{
             get_countries(query_str) {
             countries
            }
            }"
           {:query_str q}
           callback]))
       250)
      {}))

error:

No default instance of re-graph found but no valid instance name was provided. Valid instance names are: null but was provided with {
      get_countries(query_str) {
        countries
      }
    } handling event :re-graph.core/query

   re_frame$interceptor$invoke_interceptor_fn | @ | interceptor.cljc?rel=1546286667700:68
  | re_frame$interceptor$invoke_interceptors | @ | interceptor.cljc?rel=1546286667700:10
...
...
Uncaught Error: Invalid arity: 0
    at Object.G__11767 [as call] (core.cljs:6905)
    at component.cljs?rel=1546286662146:97
    at reagent$impl$component$wrap_render (component.cljs?rel=1546286662146:102)
    at reagent$impl$component$do_render (component.cljs?rel=1546286662146:121)
    at re_frame_10x.cljs?rel=1546286687044:61
    at reagent$ratom$in_context (ratom.cljs?rel=1546286661972:37)
    at reagent$ratom$deref_capture (ratom.cljs?rel=1546286661972:43)
    at reagent$ratom$run_in_reaction (ratom.cljs?rel=1546286661972:504)
    at Object.day8$re_frame_10x$mp_render [as render] (re_frame_10x.cljs?rel=15462866870

It would be helpful if you could provide an example query/subscription/mutation integrated with lacinia, since none of the docs/examples provide an end to end graphql implementation. /

Thanks!

Basic querying not working

I have this simple graphql call:

(re-frame/dispatch [::re-graph/init
                    {:http-url "https://api.spacex.land/graphql"
                     :ws-url nil
                      :http-parameters {:with-credentials? false}}])

(re-frame/dispatch [::re-graph/query
                    "{ launches { id, mission_name } }"  ;; your graphql query
                    [:events/add-launches]])

My event

(re-frame/reg-event-db
 :add-launches
 (fn [db [_ {:keys [data errors] :as payload}]]
   (-> db
       (assoc :data data :errors errors))))

This fails with:

core.cljs:3907 re-frame: expected a vector, but got:  ({:data {…}})
core.cljs:3919 re-frame: no :event handler registered for: undefined

I noticed that even if I remove the callback and just put dispatch to re-graph/query the error persists.

(re-frame/dispatch [::re-graph/query "{ launches { id, mission_name } }" nil])
Same error, any idea what is going on?

Default error handler

Rather than having possibly duplicated error handling logic in every response handler it might be useful to have a catch-all handler

Error when no argument map is wrote

Was getting an error on a query that requires no map:

(re-frame/reg-event-fx
  ::get-comment
  (fn [_ [_ comment]]
    {:dispatch [::re-graph/query
                graph/get-comment
                [::result-get-comments]]}))

TypeError: s.replace is not a function

(re-frame/reg-event-fx
  ::get-comment
  (fn [_ [_ comment]]
    {:dispatch [::re-graph/query
                graph/get-comment
                {}
                [::result-get-comments]]}))

With the empty map it works fine.
I would like to propose a extra check after the query modifier at ::mutate ::query and ::subscribe:

[query (str "query " (string/replace query #"^query\s?" ""))
variables (if (nil? variables) {} variables)]

Is it a good idea?

Error not bubbling to Callback

Recently I've been trying to implement Error Handling with some of the re-graph queries I've implemented. However, for some reason, if there is an error with the call (ex. Query isn't named correctly) the error doesn't bubble to the callback for the query. That being said, the console window does show an error. Any ideas on why this may be?

Cljs code

;; query fn
(defn query-get-foo
  [callback]
  (re-frame/dispatch
    [::re-graph/query
     "query GetFoo {
         get_foo{
           buzz
         }
      }"
     {}
     callback]))

(defn on-get-foo-handler
  "Gets response from GetContent and stores in db"
  [db [_ {:keys [data errors] :as payload}]]
  (println (str "errors::" errors))
  (assoc-in db [:query :foo] (:get_foo data)))

;; callback function
(re-frame/reg-event-db
  ::on-get-foo
  on-get-foo-handler)

(defn fetch-handler
  [_ [_ query-fn]]
  (js/setTimeout
    (fn []
      query-fn)
    200)
  {})

(re-frame/reg-event-fx
  ::fetch
  fetch-handler)

(re-frame/dispatch [::events/fetch (data/query-get-foo [::events/on-get-foo])])

Console Error

GraphQL error for :re-graph.internals/default - ce4678z2: Cannot query field `get_fo' on type `QueryRoot'.

Mutations and Subscriptions

So I'm digging into hn-clj-pedestal-re-frame as I've been trying to understand how subscriptions work.

I've been making edits and fixes as I go here.

My primary experience with using websockets in clojure/script has been sente, however I've been wanting to try integrating lacinia into my application and have been looking for tutorials to get a clear understanding on how it works.

However it seems that some changes were made as hn-clj-pedestal-re-frame is using a fork of re-graph which basically removes it's ability to send mutations over a websocket.
Diff is here: master...promesante:hn-clj-pedestal-re-frame.

I personally would like to understand how this is all supposed to work and it's rather frustrating that an example doesn't really seem to exist.

I did find your historical reply, but unfortunately the example you linked seems to require that I also understand kafka which isn't a thing I want to jump into yet.

  1. hn-clj-pedestal-re-frame is using a mutation to perform a login, by my currently limited understanding of things, this appears to be only advisable to perform over HTTP and not through re-graph as I'm not sure that the mutation being sent can be returned without setting up some sort of subscription, in which case how do you prevent the credentials token response from being sent to all the people who are listening to that subscription?

  2. How complex is this source-stream? Could I just create an atom and have the source-stream be a watcher (using add-watch?) Is it a core-async go block? What would be a minimal example?

Sorry about dropping this here, but I've been spending the last few days trying to make heads or tails of this...

How do I use this with AppSync?

I believe what AWS AppSync does is provide an Apollo compliant client, I'd love to wrap that with re-graph but I didn't see any obvious place to add that in. Am I correct in thinking that this replaces Apollo and thus I would have to write the code to get information from the AppSync sdk?

Really cool library, Id love to add this feature if you can point me in the right direction!

Use with venia

Hi, great library, found it after reading the JUXT article. I'm using it with venia and have run into a small issue. Venia generates a query, mutation, etc with the query, mutation, etc prefix, and re-graph does it as well causing a malformed query. I guess it's relatively simple to fix with another event type or a 'no prefix' option or something. Was going maybe craft a PR, but wanted do it in line with re-graphs api/design goals

Error during WebSocket handshake: Sent non-empty 'Sec-WebSocket-Protocol' header but no response was received

My graphQL API is only found without Sec-WebSocket-Protocol

My request error:
Error during WebSocket handshake: Sent non-empty 'Sec-WebSocket-Protocol' header but no response was received

Error line:

#?(:cljs (let [ws (js/WebSocket. ws-url "graphql-ws")]

Solution:
(js/WebSocket. ws-url "graphql-ws")
to
(js/WebSocket. ws-url)
or make the parameter optional

:resume-subscriptions? on subsequent init

We use JWT auth tokens with expiry time and organise to refresh the JWT token prior to expiry, in turn, when we get the refreshed token, we dispatch ::re-graph/init again, however, despite specifying
:resume-subscriptions? true any existing subscriptions are dropped.

Is this a bug or do you recommend a way to deal with this use case ?

cookie handling with re-frame on clojure

I have a service that uses cookies for managing authn/authz tokens and that does not permit unauthenticated websocket connections. The initial authn is done with http requests; afterwards an authenticated websocket connection can be used. For this to work, the http and websocket clients must share a cookie store. This seems to be impossible (or at least extremely difficult) because clj-http uses the Apache HTTP client and the WebSockets are handled via Jetty.

I've created a fork that uses hato for both HTTP and WebSocket requests. This allows sharing a cookie store.

Is there an easier fix/workaround that I've missed? If not, @oliyh would you be interested in a PR to use hato instead of clj-http/gniazdo? This would unfortunately restrict the code to JDK 11+ only.

With advanced compilation, payload becomes nil

I had some weird behaviour with :optimalizations :advanced, which I didn't had with :optimalizations :simple. After some debugging it seemed like the payload was nil with advanced, as I got warnings for duplicated keys. I then added a effect handler to also send alerts, validating my suspicion. When I look in the chrome devtools the payload looks fine. So it seems the conversion of js to cljs in the on-ws-message function goes wrong.
Let me know if you need more info.
screen shot 2018-02-15 at 08 51 10

Support :errors in response

Hi @oliyh,

I would like to access the :errors key returned by a GraphQL serrver. I think this is not possible in re-graph at the moment, since only the :data key is passed along in the callback event.

https://github.com/oliyh/re-graph/blob/master/src/re_graph/core.cljs#L26

Would you take a PR that adds this functionality. We could either pass the complete payload [event-name {:keys[data errors}] or add it to the event vector like this [event-name data errors]. I think I would prefer the first approach, since you are then free to choose any key from the response (some people may add additional keys to their response).

What do you think?

Roman.

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.