Giter Club home page Giter Club logo

sente's Introduction

Taoensso open source
Documentation | Latest releases | Get support

Sente

Realtime web comms library for Clojure/Script

Sente is a small client+server library that makes it easy to build realtime web applications with Clojure + ClojureScript.

Loosely inspired by Socket.IO, it uses core.async, WebSockets, and Ajax under the hood to provide a simple high-level API that enables reliable, high-performance, bidirectional communications.

Sen-te (先手) is a Japanese Go term used to describe a play with such an overwhelming follow-up that it demands an immediate response, leaving its player with the initiative.

Latest release/s

Main tests Graal tests

See here for earlier releases.

Why Sente?

  • Bidirectional a/sync comms over WebSockets with auto Ajax fallback
  • It just works: auto keep-alive, buffering, protocol selection, reconnects
  • Efficient design with auto event batching for low-bandwidth use, even over Ajax
  • Send arbitrary Clojure vals over edn or Transit (JSON, MessagePack, etc.)
  • Tiny, easy-to-use API
  • Support for users simultaneously connected with multiple clients and/or devices
  • Realtime info on which users are connected, and over which protocols
  • Standard Ring security model: auth as you like, HTTPS when available, CSRF support, etc.
  • Support for several popular web servers, easily extended to other servers.

Capabilities

Protocol client>server client>server + ack/reply server>user push
WebSockets ✓ (native) ✓ (emulated) ✓ (native)
Ajax ✓ (emulated) ✓ (native) ✓ (emulated)

So you can ignore the underlying protocol and deal directly with Sente's unified API that exposes the best of both WebSockets (bidirectionality + performance) and Ajax (optional ack/reply).

Documentation

Funding

You can help support continued work on this project, thank you!! 🙏

License

Copyright © 2012-2024 Peter Taoussanis.
Licensed under EPL 1.0 (same as Clojure).

sente's People

Contributors

danielcompton avatar danielsz avatar derekchiang avatar domkm avatar ebellani avatar eneroth avatar estsauver avatar hoxu avatar jgrimes avatar jjttjj avatar kajism avatar kaliszad avatar mattford63 avatar matthiasn avatar michaelcameron avatar nfedyashev avatar nikolap avatar p-himik avatar pieterbreed avatar ptaoussanis avatar rkiouak avatar rosejn avatar smichal avatar sritchie avatar surferhalo avatar tfoldi avatar theasp avatar timothypratley avatar tobias avatar viesti 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  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

sente's Issues

Login/logout again

Hi,

I have Sente working with my application. Things are looking very good. Well done!

I've been trying to get my head wrapped around the login thing. The basics are very straight forward, but you might want to move some of the information in issue 17 into the docs.

I think the way things work now is that when you establish a chsk connection you have to know the uid that will be associated with it. So you've established a 1-1 relation between session and chsk connection.

I want to do something a little different than this. I'm building an application where I'd like to open a chsk channel to an anonymous user. There's a fair bit of stuff an anonymous user is allowed to do. At some point, I'd like the user to be able to sign in, so they can do more stuff. Sometime later the user can sign out, but still be able to act as an anonymous user.

One way that I can think of to do this is to create a new channel whenever the user state changes. Easy enough, I can do this by re-loading the page on sign in/off. But this is a single page app. If I do this I'll be effectively starting the application from it's initial state on sign in/off.

Another way would be to have sign in/off trigger disconnect and reconnect. I'm a little leary of this. Is this a 'reliable' option in Sente? Or is Sente depending on a reload to clean stuff up?

Another way is to layer my own channel managemet stuff on top of Sente's. In this case every client would have a unique 'user' id (client id really) that persisted across sign in and out. The new user management layer would do essentially what you're doing already, mapping some 'higher level' user id to the 'lower level' user/client id. As I write this I'm starting to lean towards this way of doing it.

This application is brand new, I can do pretty much anything I want with Sente and user management.

I hope that made sense.

Any suggestions you might have would be greatly appreciated.

Sending params on socket connect

Is there a way to send params from the client when calling make-channel-socket! or chsk-reconnect!? This gets passed to the server's user-id-fn to set the user-id. It's not feasible to store the uid in the session in my case, because I want to support multiple users across different browser windows, and the session persists across all windows.

Running the example project fails

A freshly cloned repository fails the following way:

~/Downloads/git/sente/example-project $ lein start-dev
Reflection warning, cljx/core.clj:73:24 - reference to field getPath on java.lang.Object can't be resolved.
Reflection warning, cljx/core.clj:73:24 - reference to field getPath can't be resolved.
Reflection warning, cljs/closure.clj:142:26 - call to method setDefineToIntegerLiteral on com.google.javascript.jscomp.CompilerOptions can't be resolved (no such method).
Reflection warning, cljs/closure.clj:142:26 - call to method setDefineToIntegerLiteral on com.google.javascript.jscomp.CompilerOptions can't be resolved (no such method).
Reflection warning, cljs/closure.clj:164:17 - call to java.util.zip.ZipFile ctor can't be resolved.
Reflection warning, cljs/closure.clj:165:18 - reference to field getName can't be resolved.
Reflection warning, cljs/closure.clj:229:41 - reference to field getFile can't be resolved.
Reflection warning, cljs/closure.clj:1018:70 - call to method findResources on java.lang.ClassLoader can't be resolved (no such method).
Reflection warning, cljs/repl.clj:96:28 - call to method getBytes can't be resolved (target class is unknown).
Reflection warning, cljs/repl.clj:178:22 - call to java.io.File ctor can't be resolved.
Reflection warning, cljs/repl.clj:180:40 - reference to field getAbsolutePath on java.lang.Object can't be resolved.
Reflection warning, cljs/repl.clj:180:40 - reference to field getAbsolutePath can't be resolved.
Reflection warning, cemerick/austin.clj:50:3 - call to method stop can't be resolved (target class is unknown).
Reflection warning, cemerick/austin.clj:72:3 - reference to field getAddress can't be resolved.
Reflection warning, cemerick/austin.clj:72:3 - reference to field getPort can't be resolved.
Reflection warning, cemerick/austin.clj:77:14 - call to method getBytes can't be resolved (target class is unknown).
Reflection warning, cemerick/austin.clj:82:28 - call to method write on java.io.OutputStream can't be resolved (argument types: unknown).
Reflection warning, cemerick/austin.clj:168:47 - reference to field getRequestHeaders can't be resolved.
Reflection warning, cemerick/austin.clj:191:21 - call to method endsWith can't be resolved (target class is unknown).
Reflection warning, cemerick/austin.clj:429:23 - call to method exec on java.lang.Runtime can't be resolved (argument types: unknown).
Reflection warning, cemerick/austin.clj:439:5 - reference to field destroy on java.lang.Object can't be resolved.
Reflection warning, cljs/repl/rhino.clj:36:5 - call to method evaluateReader on java.lang.Object can't be resolved (no such method).
Reflection warning, cljs/repl/rhino.clj:32:5 - call to method evaluateString on java.lang.Object can't be resolved (no such method).
Reflection warning, cljs/repl/rhino.clj:42:52 - reference to field toString can't be resolved.
Reflection warning, cljs/repl/rhino.clj:42:67 - reference to field getStackTrace can't be resolved.
Reflection warning, cljs/repl/rhino.clj:45:3 - reference to field getScriptStackTrace can't be resolved.
Reflection warning, cljs/repl/rhino.clj:50:3 - reference to field toString can't be resolved.
Reflection warning, cljs/repl/rhino.clj:91:38 - reference to field toString can't be resolved.
Reflection warning, cljs/repl/rhino.clj:105:5 - call to static method putProperty on org.mozilla.javascript.ScriptableObject can't be resolved (argument types: unknown, java.lang.String, java.lang.Object).
Reflection warning, cemerick/piggieback.clj:54:3 - call to static method putProperty on org.mozilla.javascript.ScriptableObject can't be resolved (argument types: unknown, java.lang.String, java.lang.Object).
Reflection warning, alex_and_georges/debug_repl.clj:101:8 - reference to field getCause can't be resolved.
Reflection warning, alex_and_georges/debug_repl.clj:102:8 - reference to field getCause can't be resolved.
Reflection warning, dynapath/defaults.clj:13:52 - reference to field getURLs can't be resolved.
Reflection warning, dynapath/defaults.clj:27:28 - call to method addURL can't be resolved (target class is unknown).
Reflection warning, dynapath/util.clj:30:22 - reference to field getParent can't be resolved.
Reflection warning, alembic/still.clj:33:5 - reference to field close on java.lang.Object can't be resolved.
Reflection warning, com/georgejahad/difform.clj:47:25 - reference to field operation can't be resolved.
Reflection warning, com/georgejahad/difform.clj:48:44 - reference to field text can't be resolved.
Reflection warning, com/georgejahad/difform.clj:48:37 - reference to field trim can't be resolved.
Exception in thread "main" java.lang.ExceptionInInitializerError, compiling:(/private/var/folders/j2/zvt92c1s39d_0kdmhrhbgtkm0000gn/T/form-init7898637281734394767.clj:1:142)
    at clojure.lang.Compiler.load(Compiler.java:7142)
    at clojure.lang.Compiler.loadFile(Compiler.java:7086)
    at clojure.main$load_script.invoke(main.clj:274)
    at clojure.main$init_opt.invoke(main.clj:279)
    at clojure.main$initialize.invoke(main.clj:307)
    at clojure.main$null_opt.invoke(main.clj:342)
    at clojure.main$main.doInvoke(main.clj:420)
    at clojure.lang.RestFn.invoke(RestFn.java:421)
    at clojure.lang.Var.invoke(Var.java:383)
    at clojure.lang.AFn.applyToHelper(AFn.java:156)
    at clojure.lang.Var.applyTo(Var.java:700)
    at clojure.main.main(main.java:37)
Caused by: java.lang.ExceptionInInitializerError
    at java.lang.Class.forName0(Native Method)
    at java.lang.Class.forName(Class.java:270)
    at clojure.lang.RT.loadClassForName(RT.java:2093)
    at clojure.lang.RT.load(RT.java:430)
    at clojure.lang.RT.load(RT.java:411)
    at clojure.core$load$fn__5066.invoke(core.clj:5641)
    at clojure.core$load.doInvoke(core.clj:5640)
    at clojure.lang.RestFn.invoke(RestFn.java:408)
    at clojure.core$load_one.invoke(core.clj:5446)
    at clojure.core$load_lib$fn__5015.invoke(core.clj:5486)
    at clojure.core$load_lib.doInvoke(core.clj:5485)
    at clojure.lang.RestFn.applyTo(RestFn.java:142)
    at clojure.core$apply.invoke(core.clj:626)
    at clojure.core$load_libs.doInvoke(core.clj:5524)
    at clojure.lang.RestFn.applyTo(RestFn.java:137)
    at clojure.core$apply.invoke(core.clj:626)
    at clojure.core$require.doInvoke(core.clj:5607)
    at clojure.lang.RestFn.invoke(RestFn.java:408)
    at clj_ns_browser.sdoc$loading__4920__auto__.invoke(sdoc.clj:9)
    at clj_ns_browser.sdoc__init.load(Unknown Source)
    at clj_ns_browser.sdoc__init.<clinit>(Unknown Source)
    at java.lang.Class.forName0(Native Method)
    at java.lang.Class.forName(Class.java:270)
    at clojure.lang.RT.loadClassForName(RT.java:2093)
    at clojure.lang.RT.load(RT.java:430)
    at clojure.lang.RT.load(RT.java:411)
    at clojure.core$load$fn__5066.invoke(core.clj:5641)
    at clojure.core$load.doInvoke(core.clj:5640)
    at clojure.lang.RestFn.invoke(RestFn.java:408)
    at clojure.core$load_one.invoke(core.clj:5446)
    at clojure.core$load_lib$fn__5015.invoke(core.clj:5486)
    at clojure.core$load_lib.doInvoke(core.clj:5485)
    at clojure.lang.RestFn.applyTo(RestFn.java:142)
    at clojure.core$apply.invoke(core.clj:626)
    at clojure.core$load_libs.doInvoke(core.clj:5524)
    at clojure.lang.RestFn.applyTo(RestFn.java:137)
    at clojure.core$apply.invoke(core.clj:626)
    at clojure.core$require.doInvoke(core.clj:5607)
    at clojure.lang.RestFn.invoke(RestFn.java:512)
    at user$eval4932.invoke(form-init7898637281734394767.clj:1)
    at clojure.lang.Compiler.eval(Compiler.java:6703)
    at clojure.lang.Compiler.eval(Compiler.java:6692)
    at clojure.lang.Compiler.load(Compiler.java:7130)
    ... 11 more
Caused by: java.lang.ExceptionInInitializerError, compiling:(clj_info/doc2map.clj:1:1)
    at clojure.lang.Compiler.load(Compiler.java:7142)
    at clojure.lang.RT.loadResourceScript(RT.java:370)
    at clojure.lang.RT.loadResourceScript(RT.java:361)
    at clojure.lang.RT.load(RT.java:440)
    at clojure.lang.RT.load(RT.java:411)
    at clojure.core$load$fn__5066.invoke(core.clj:5641)
    at clojure.core$load.doInvoke(core.clj:5640)
    at clojure.lang.RestFn.invoke(RestFn.java:408)
    at clojure.core$load_one.invoke(core.clj:5446)
    at clojure.core$load_lib$fn__5015.invoke(core.clj:5486)
    at clojure.core$load_lib.doInvoke(core.clj:5485)
    at clojure.lang.RestFn.applyTo(RestFn.java:142)
    at clojure.core$apply.invoke(core.clj:626)
    at clojure.core$load_libs.doInvoke(core.clj:5524)
    at clojure.lang.RestFn.applyTo(RestFn.java:137)
    at clojure.core$apply.invoke(core.clj:626)
    at clojure.core$require.doInvoke(core.clj:5607)
    at clojure.lang.RestFn.invoke(RestFn.java:2793)
    at clj_ns_browser.browser$loading__4920__auto__.invoke(browser.clj:9)
    at clj_ns_browser.browser__init.load(Unknown Source)
    at clj_ns_browser.browser__init.<clinit>(Unknown Source)
    ... 54 more
Caused by: java.lang.ExceptionInInitializerError
    at hiccup.core__init.load(Unknown Source)
    at hiccup.core__init.<clinit>(Unknown Source)
    at java.lang.Class.forName0(Native Method)
    at java.lang.Class.forName(Class.java:270)
    at clojure.lang.RT.loadClassForName(RT.java:2093)
    at clojure.lang.RT.load(RT.java:430)
    at clojure.lang.RT.load(RT.java:411)
    at clojure.core$load$fn__5066.invoke(core.clj:5641)
    at clojure.core$load.doInvoke(core.clj:5640)
    at clojure.lang.RestFn.invoke(RestFn.java:408)
    at clojure.core$load_one.invoke(core.clj:5446)
    at clojure.core$load_lib$fn__5015.invoke(core.clj:5486)
    at clojure.core$load_lib.doInvoke(core.clj:5485)
    at clojure.lang.RestFn.applyTo(RestFn.java:142)
    at clojure.core$apply.invoke(core.clj:626)
    at clojure.core$load_libs.doInvoke(core.clj:5524)
    at clojure.lang.RestFn.applyTo(RestFn.java:137)
    at clojure.core$apply.invoke(core.clj:626)
    at clojure.core$require.doInvoke(core.clj:5607)
    at clojure.lang.RestFn.invoke(RestFn.java:512)
    at clj_info.doc2map$eval5991$loading__4958__auto____5992.invoke(doc2map.clj:9)
    at clj_info.doc2map$eval5991.invoke(doc2map.clj:9)
    at clojure.lang.Compiler.eval(Compiler.java:6703)
    at clojure.lang.Compiler.eval(Compiler.java:6692)
    at clojure.lang.Compiler.load(Compiler.java:7130)
    ... 74 more
Caused by: java.lang.IllegalStateException: *html-mode* already refers to: #'hiccup.util/*html-mode* in namespace: hiccup.compiler
    at clojure.lang.Namespace.warnOrFailOnReplace(Namespace.java:88)
    at clojure.lang.Namespace.intern(Namespace.java:72)
    at clojure.lang.Var.intern(Var.java:158)
    at clojure.lang.RT.var(RT.java:341)
    at hiccup.core$html.<clinit>(core.clj:7)
    ... 99 more
Error encountered performing task 'repl' with profile(s): 'default,dev'
Subprocess failed

I get pretty much the same stacktrace with lein build-once as well. Appears to be a problem with cljx, so maybe this is a wrong place to submit it. I haven't used cljx before and I'm pretty new to CLJS in general.

Sente and good (functional) programming practice

Sente is an awesome library. As such, it will be used in production-grade applications. It is therefore important to provide guidelines regarding the integration of said library.

Common challenges are:

  • Make it play along a setup and teardown strategy to achieve fast development cycles.
  • Avoid top-level def forms with runtime state. This leads to imperative code, and is prone to cyclic dependencies.

The example project, while extremely useful to introduce the features of Sente, should be complemented with guidelines aiming to address those challenges specifically.

Stuart Sierra has been advocating a workflow that works well and we may want to adopt its principles.

Two key points from the README:

Large applications often consist of many stateful processes which must be started and stopped in a particular order. The component model makes those relationships explicit and declarative, instead of implicit in imperative code.

And:

Instead of having mutable state (atoms, refs, etc.) scattered throughout different namespaces, all the stateful parts of an application can be gathered together.

I would like to open for discussion my implementation of a Sente component. It is working well for me, but I might have overlooked things or, worse, inadvertently introduced new problems.

(ns front-end.framework.components.channel-sockets
  (:require [com.stuartsierra.component :as component]
            [taoensso.sente :as sente]
            [front-end.webapp.handler :refer [event-msg-handler]]))


(defrecord ChannelSockets [ring-ajax-post ring-ajax-get-or-ws-handshake ch-chsk chsk-send! chsk-router]
  component/Lifecycle
  (start [component]
    (let [{:keys [ch-recv send-fn ajax-post-fn ajax-get-or-ws-handshake-fn]}
      (sente/make-channel-socket! {})]
      (assoc component 
        :ring-ajax-post ajax-post-fn 
        :ring-ajax-get-or-ws-handshake ajax-get-or-ws-handshake-fn
        :ch-chsk ch-recv
        :chsk-send! send-fn
        :chsk-router (sente/start-chsk-router-loop! event-msg-handler ch-recv))))
  (stop [component]
    component))

(defn new-channel-sockets
  []
  (map->ChannelSockets {}))

My development system looks like this:

(defn dev-system []
  (component/system-map
   :sente (new-channel-sockets)
   :web (new-web-server (Integer. (env :http-port)) (env :trace-headers))))

One would start the system like this:

(def system)
(alter-var-root #'system (fn [_] (component/start (dev-system))))

As a result, all the relevant sentefunctions are in the system map, allowing us to call chsk-send!, for example, from any namespace.

((:chsk-send! (:sente  (system-map))) (:uid session) [:myapp/account {:status "OK"}])

Following this, the compojure routes look like this;

  (GET  "/chsk" req ((:ring-ajax-get-or-ws-handshake (:sente (system-map))) req))
  (POST "/chsk" req ((:ring-ajax-post (:sente (system-map))) req)))

Thoughts?

Session: writable from within Sente's router loop?

So I know that I can read session properties inside Sente's router loop, but can I write to it as well?

For example, how would I go about writing fn-that-writes-and-persists-a-session-property?

(defn event-msg-handler
  [{:as ev-msg :keys [ring-req event ?reply-fn]} _]
  (let [session (:session ring-req)
        uid     (:uid session)
        [id data :as ev] event]

    (println "Event: %s" ev)
    (match [id data]
           [:myapp/event-x _] (fn-that-writes-and-persists-a-session-property session)
    :else
    (do (println "Unmatched event: %s" ev)
        (when-not (:dummy-reply-fn? (meta ?reply-fn))
          (?reply-fn {:umatched-event-as-echoed-from-from-server ev}))))))

Stack trace in the console (possibly related with connected uid update)

Running Sente 0.12.0-SNAPSHOT, I am seeing this on the console when I reload my app in the browser. It has no adverse effect that I can see, just this stack trace. I thought you might want to know.

Wed Apr 30 07:12:15 IDT 2014 [worker-4] ERROR - on close handler
java.lang.ClassCastException: clojure.lang.PersistentArrayMap cannot be cast to clojure.lang.IPersistentSet
    at clojure.core$disj.invoke(core.clj:1449)
    at taoensso.sente$make_channel_socket_BANG_$upd_connected_uid_BANG___17812.invoke(sente.clj:261)
    at taoensso.sente$make_channel_socket_BANG_$fn__18022$fn__18051.invoke(sente.clj:402)
    at org.httpkit.server.AsyncChannel.onClose(AsyncChannel.java:188)
    at org.httpkit.server.RingHandler$1.run(RingHandler.java:217)
    at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:471)
    at java.util.concurrent.FutureTask.run(FutureTask.java:262)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
    at java.lang.Thread.run(Thread.java:744)

Sente initialization and Safari Password manager

Thanks for a wonderful library. I'm very new to ClojureScript/Javascript world and your library makes things much easier.

Writing a simple single page application I've got stuck with strange issue with Safari browser on OS X. My application works as follows - I have a login page, which redirects to application, if login was successful. When application starts it tries to setup ws/ajax connection to server exactly the same way as shown in sente example project. When I use Chrome/Firefox to access application, everything works as intended. But Safari shows me a popup window offering to save username/password, and application is loaded in background. But if application gets loaded in background, it looks like event-handling loop is not started, though I see on server side, thats connection is established and ws-pings are coming.

Is there a way to figure out why it happens?

Thanks,
Eugene

event handler and blocking APIs

Hi

My server side event handler is doing some blocking API calls and thus making the router loop block other requests.

You see any potential problems with wrapping the handler in another go block inside the router loop? Seems to work fine but haven't tested thoroughly.

@@ -871,11 +871,13 @@
           (let [[v p] (async/alts! [ch ctrl-ch])]
             (if (identical? p ctrl-ch) ::stop
               (let [event-msg v]
-                (try
-                  (timbre/tracef "Event-msg: %s" event-msg)
-                  (do (event-msg-handler event-msg ch) nil)
-                  (catch Throwable t
-                    (timbre/errorf t "Chsk-router-loop handling error: %s" event-msg))))))
+                (go 
+                  (try
+                    (timbre/tracef "Event-msg: %s" event-msg)
+                    (do (event-msg-handler event-msg ch) nil)
+                    (catch Throwable t
+                      (timbre/errorf t "Chsk-router-loop handling error: %s" event-msg))))
+                nil)))

Add support for other Clojure-side web servers (i.e. besides http-kit)

This shouldn't be too hard (there's only a small surface area that actually interacts with http-kit) but will require potential servers to expose a sensible API that we can use.

Don't have a lot of time to look into this myself atm, so assistance would be welcome:

  • What server(s) should we target? This might be a useful starting point?
  • What API does each expose?
  • Suggested abstraction to encapsulate any API differences?

Otherwise any other thoughts/suggestions very welcome! Feel free to ping me with any questions :-)

Avoiding (too) early usage of channels.

I have clojurescript code that runs at pageload time. It might try to use a channel too early.
The console says:

Chsk send against closed chsk. 

How do I avoid that? How do I check when we're all set up and ready to go?

Add optional support for JSON instead of edn

Edn can occasionally be too slow for some applications. In other cases apps may need to interface with pre-existing JSON services, etc.

Edn's a sensible default, but it'd be nice to offer optional JSON chsks.

Pull-requests welcome!

how to broadcast to all connected clients?

Socket.io allows broadcast to every client, or to every client except the sender. I'm using Sente to broadcast race results in realtime to every connected browser - this feels difficult in the current API.

I think what I want is socket.io style channels in addition to the per user channels. I can fake this by using the "uid" as a channel ID for now. Just wanted to gather thoughts.

server>user push sends malformed events

I'm using 0.9 version of sente. When i do:

(chsk-send! uid [:event/my-event some-data])

I'm getting "Malformed events" on the client and the following data in chrome dev tools WS-frames:
[[:event/my-event some-data]]

but other frames contain {:chsk/clj [:normal-event/event-name data]}

Do i need to wrap my events in {:chsk/clj ... } too to push them to client?

Basic auth interoperability

I’d need a way to authenticate Sente workflow with basic auth (I’m using Friend). I can’t think of any clean way to do it, other than maybe digging into the handshake process, I guess.

Possible workaround would be simply use form authentication.

ring-anti-forgery key change

For ring-anti-forgery 1.0.0, I plan on changing the session key from:

"__anti-forgery-token"

to a namespaced keyword:

:ring.middleware.anti-forgery/anti-forgery-token

Decomplect -- see jetty9-websockets-async

At the risk of sounding rude, I want to point out to you the reasons why I will not be using this library. You're a really sharp guy, and rather than keep this feedback to myself, sharing it makes it more likely that we will agree on implementation in the future so we can both benefit. We want the same things, so why not talk about them?

First, you've mandated shipping edn. What if my frontend team writes JS and we want to ship JSON? What if we're high-throughput and are actually shipping BSON? Why can't I pick my serialization mechanism?

Where are my go-loops? You start go-loops on channels as in ch-pull-ajax-hx-chs, but you don't hand them back to me. What if I am trying to test my code and want to verify that the processing has stopped after I have closed my channel?

ajax-post-fn and ajax-get-or-ws-handshake-fn don't belong in the same place. These are different scenarios and should be treated as such.

Why can't I specify my own error handling? Why are you logging for me? I should be able to see successes/failures on channels and be able to make the decision myself.

Why is the cljs client related to the server at all? Reconnect logic in javascript is different than reconnect logic in java and is only relevant on the client side.

You are mandating authentication by using ring sessions. What if I want to generate random urls and use those? In fact, this is what the most recent application I'm working on needs to do.

You explicit recommend placing the resulting channels and functions as top level vars. This is terrible for testing -- where are your tests!

The overarching theme is that you are doing way too many things and making them non-configurable. Contrast this with my library:
https://github.com/ToBeReplaced/jetty9-websockets-async

It's less than a quarter of the size of your code base, yet is entirely configurable in all of the above ways. Moreover, I simply return a javax.http.servlet.HttpServlet. This means that my libarary will work with any java server with servlets. Jetty/Tomcat/whatever. 100% test coverage. There's still a little bit of work to do to open up a few more places (ex. passing the client disconnect message), but the place to do that is clear and won't disrupt anyone by design.

As a final comment, you need to get your dependencies under control -- Why on earth is sente bringing in a redis client? I would not be able to bring this in to a production environment. For reference, lein deps :tree prints out:

WARNING!!! version ranges found for:
[com.keminglabs/cljx "0.3.2"] -> [org.clojars.trptcolin/sjacket "0.1.0.3"] -> [org.clojure/clojure "[1.3.0,)"]
Consider using [com.keminglabs/cljx "0.3.2" :exclusions [org.clojure/clojure]].
[com.keminglabs/cljx "0.3.2"] -> [org.clojars.trptcolin/sjacket "0.1.0.3"] -> [net.cgrand/regex "1.1.0"] -> [org.clojure/clojure "[1.2.0,)"]
Consider using [com.keminglabs/cljx "0.3.2" :exclusions [org.clojure/clojure]].
[com.keminglabs/cljx "0.3.2"] -> [org.clojars.trptcolin/sjacket "0.1.0.3"] -> [net.cgrand/parsley "0.9.1"] -> [org.clojure/clojure "[1.2.0,)"]
Consider using [com.keminglabs/cljx "0.3.2" :exclusions [org.clojure/clojure]].
[com.keminglabs/cljx "0.3.2"] -> [org.clojars.trptcolin/sjacket "0.1.0.3"] -> [net.cgrand/parsley "0.9.1"] -> [net.cgrand/regex "1.1.0"] -> [org.clojure/clojure "[1.2.0,)"]
Consider using [com.keminglabs/cljx "0.3.2" :exclusions [org.clojure/clojure]].
[com.taoensso/timbre "3.1.0"] -> [com.taoensso/encore "0.8.0"] -> [com.keminglabs/cljx "0.3.2"] -> [org.clojars.trptcolin/sjacket "0.1.0.3"] -> [org.clojure/clojure "[1.3.0,)"]
Consider using [com.taoensso/timbre "3.1.0" :exclusions [org.clojure/clojure]].
[com.taoensso/timbre "3.1.0"] -> [com.taoensso/encore "0.8.0"] -> [com.keminglabs/cljx "0.3.2"] -> [org.clojars.trptcolin/sjacket "0.1.0.3"] -> [net.cgrand/regex "1.1.0"] -> [org.clojure/clojure "[1.2.0,)"]
Consider using [com.taoensso/timbre "3.1.0" :exclusions [org.clojure/clojure]].
[com.taoensso/timbre "3.1.0"] -> [com.taoensso/encore "0.8.0"] -> [com.keminglabs/cljx "0.3.2"] -> [org.clojars.trptcolin/sjacket "0.1.0.3"] -> [net.cgrand/parsley "0.9.1"] -> [org.clojure/clojure "[1.2.0,)"]
Consider using [com.taoensso/timbre "3.1.0" :exclusions [org.clojure/clojure]].
[com.taoensso/timbre "3.1.0"] -> [com.taoensso/encore "0.8.0"] -> [com.keminglabs/cljx "0.3.2"] -> [org.clojars.trptcolin/sjacket "0.1.0.3"] -> [net.cgrand/parsley "0.9.1"] -> [net.cgrand/regex "1.1.0"] -> [org.clojure/clojure "[1.2.0,)"]
Consider using [com.taoensso/timbre "3.1.0" :exclusions [org.clojure/clojure]].

Possibly confusing dependencies found:
[com.taoensso/timbre "3.1.0"] -> [com.draines/postal "1.11.1"] -> [commons-codec "1.7"]
 overrides
[com.taoensso/timbre "3.1.0"] -> [com.taoensso/carmine "2.4.6"] -> [commons-codec "1.9"]

Consider using these exclusions:
[com.taoensso/timbre "3.1.0" :exclusions [commons-codec]]

 [clojure-complete "0.2.3" :exclusions [[org.clojure/clojure]]]
 [com.cemerick/austin "0.1.4"]
   [com.cemerick/piggieback "0.1.3"]
 [com.cemerick/clojurescript.test "0.2.2"]
 [com.keminglabs/cljx "0.3.2"]
   [org.clojars.trptcolin/sjacket "0.1.0.3"]
     [net.cgrand/parsley "0.9.1"]
     [net.cgrand/regex "1.1.0"]
   [org.clojure/core.match "0.2.0"]
   [watchtower "0.1.1"]
 [com.taoensso/encore "0.9.2"]
 [com.taoensso/timbre "3.1.0"]
   [com.draines/postal "1.11.1"]
     [commons-codec "1.7"]
     [javax.mail/mail "1.4.4" :exclusions [[javax.activation/activation]]]
   [com.taoensso/carmine "2.4.6"]
     [commons-pool "1.6"]
     [org.clojure/tools.macro "0.1.5"]
   [com.taoensso/nippy "2.5.2"]
     [org.iq80.snappy/snappy "0.3"]
     [org.tukaani/xz "1.4"]
   [io.aviso/pretty "0.1.8"]
   [org.clojure/data.fressian "0.2.0"]
     [org.fressian/fressian "0.6.3"]
   [org.clojure/tools.logging "0.2.6"]
   [org.xerial.snappy/snappy-java "1.1.1-M1"]
 [expectations "1.4.56"]
   [erajure "0.0.3"]
     [org.mockito/mockito-all "1.8.0"]
   [junit "4.8.1"]
 [http-kit "2.1.17"]
 [org.clojure/clojure "1.6.0-beta1"]
 [org.clojure/clojurescript "0.0-2173"]
   [com.google.javascript/closure-compiler "v20131014"]
     [args4j "2.0.16"]
     [com.google.code.findbugs/jsr305 "1.3.9"]
     [com.google.guava/guava "15.0"]
     [com.google.protobuf/protobuf-java "2.4.1"]
     [org.json/json "20090211"]
   [org.clojure/data.json "0.2.3"]
   [org.clojure/google-closure-library "0.0-20130212-95c19e7f0f5f"]
     [org.clojure/google-closure-library-third-party "0.0-20130212-95c19e7f0f5f"]
   [org.mozilla/rhino "1.7R4"]
 [org.clojure/core.async "0.1.278.0-76b25b-alpha"]
 [org.clojure/tools.nrepl "0.2.3" :exclusions [[org.clojure/clojure]]]
 [org.clojure/tools.reader "0.8.3"]
 [reiddraper/simple-check "0.5.6"]

What do the chsk/uidport-close and chsk/uidport-open events mean?

Hi,

I'm really very happy with Sente, thanks. Release 0.13.0 made a huge difference to my application. I upgraded to 0.14.1 today, everything still works so no worries there :-)

This, I suppose, is a documentation issue...

I see you've added two events: chsk/uidport-close and chsk/uidport-open. I put a couple of handlers in place for them and was thinking that they might be handy to clean up some resources when the connection was closed. It's not working out the way I'd expected. If I re-draw the page in the browser I'll get an chsk/uidport-close followed by a chsk/uidport-open with the same session-id. It's almost as though it's dropping then re-establishing the connection. If this is a reconnection then I definitely don't want to clean up after the close.

What do these events actually mean at Sente's level and at the application level? When should they be used? When should they not be used? Your thoughts on this would be greatly appreciated.

bug in connected-uids logic?

I think there's a bug in the connected UID logic.

If a user opens up multiple tabs to the same page (creating multiple websocket connections with the same UID), when any one of the tabs closes, that UID is removed from the connected-uids atom; if you're using connected-uids to broadcast to some user set, the other clients will stop receiving messages.

Internally, sente probably needs to reference count the number of connections.

H12 Timeouts on Heroku with Sente

Hey @ptaoussanis,

I recently upgraded to Sente 0.14.1 (from 0.12.0), and, suspiciously, started seeing a bunch of H12 - Application Timeouts on my Heroku app.

I've also noticed a number of Idle Connection warnings as well.

Could something have changed between those versions wrt the default keep-alive settings? Also, is there some way to tune these settings? I believe that Heroku has a 30 second timeout on connections, and keep-alives need to be sent within 55 seconds. I'm not sure if these errors were coming from websocket connections or long-polling connections.

Anyway, this is out of my wheelhouse, but it's certainly something that just started after a recent batch of upgrades. Would love your advice! Let me know if I can get you any further info.

repl-friendly development workflow

Hi, thanks for making sente.

Could you recommend a workflow that works across REPL re-evaluations? My current solution is wrapping the initialization in a defonce:

(defonce sente-init
  (do (let [{:keys [ch-recv send-fn ajax-post-fn ajax-get-or-ws-handshake-fn
                    connected-uids]}
            (sente/make-channel-socket! {})]
        (def ring-ajax-post                ajax-post-fn)
        (def ring-ajax-get-or-ws-handshake ajax-get-or-ws-handshake-fn)
        (def ch-chsk                       ch-recv) ; ChannelSocket's receive channel
        (def chsk-send!                    send-fn) ; ChannelSocket's send API fn
        (def connected-uids                connected-uids) ; Watchable, read-only atom
        )))

which prevents re-initialization everytime I reload the namespace. Then I use a var instead of the function name for my event handler.

(defonce chsk-router
  (sente/start-chsk-router-loop! #'event-msg-handler ch-chsk))

If this good? I could create a pull request on the example project if that would help.

Csrf protection in 0.14.1

Readme.md
"Server-side: you'll need to use middleware like ring-anti-forgery to generate and check CSRF codes. The ring-ajax-post handler should be covered (i.e. protected).
Client-side: you'll need to pass the page's csrf code to the make-channel-socket! constructor."

Does the client side part still hold, it seems to be delivered from the server on handshake?

Verify that new Ajax impl. is working

Just swapped out the Ajax mechanism to avoid an external dependency. Since I'm running a different version in prod, haven't confirmed yet that this public release behaves as expected under Ajax mode.

Error message from crosspagechannel.js

I’m just getting started with this library so please bear with me if I’m doing something completely wrong.

Following the README instruction I can see handshake event in the browser console, along preceded by CSRF warning, which is ok at this stage. But even before that I see this error message:

TypeError: 'undefined' is not an object (evaluating 'peerUri.setParameterValue') 

Replacing core.async

I'm investigating Sente but don't want to use core.async (it is not particularly efficient for my workload). Instead, I'd like to use or integrate Meltdown.

Any thoughts on if this should be possible/easy, and if I should contribute this to Sente
vs Meltdown itself?

Clarification for use of vars

I've been scratching my head as to why the chsk routes take vars instead of functions. Would you care to explain? Thank you.

(defroutes my-app
  ;; <other stuff>

  ;;; Add these 2 entries: --->
  (GET  "/chsk" req (#'ring-ajax-get-or-ws-handshake req)) ; Note the #'
  (POST "/chsk" req (#'ring-ajax-post                req)) ; ''

  )

Rough getting started experience

I've been trying to wrap my head around your library, but I'm unable to get something useful going.

Here are some problems I ran into:

  • Some examples in the Readme are inconsistent with the current version (such as the send-fn function taking two arguments, not one)
  • There's no full example showing how to communicate between the server and client. When your docs say "what now? You're good to go!" I'm thinking "how?".
  • When creating a socket, I get a bunch of anonymous functions, which leads to errors like "Wrong number of args (1) passed to: sente/make-channel-socket!/fn--14849", which aren't really helpful.
  • The Readme example that desctructures the map of functions gives them all different names, which makes debugging more cumbersome, since I now have to 1) look up what the function was really called, 2) look up how to use that function.

It would be easier to get started with sente if there was one fully contained copy-pastable example in the Readme that got me a client/server talking to each other.

All in all I was very excited about sente upon reading its pitch, but having tried to use it I am frustrated and feel stupid. I hope you can make anything useful out of this feedback.

Optimization: compress edn payloads

Should be easy to automatically+transparently compress edn strings that are long enough to benefit. This'll help cut down on data transfer (esp. useful for mobile clients, etc.)

Server-side compression could be TTL-memoized to get acceptable broadcast performance even for very large payloads.

Relax :uid requirement to Function[Request, UID]

It would be great if we could specify a :uid extraction function, rather than requiring that specific key in the the session. I'd be happy to code this up if you like the feature. Thoughts?

Sente Trips Up When Used In A Browser-Repl

Using a browser-connected repl, on the client side, when sente tries to establish a chsk channel, I experience a broken channel.

I get around this by including a custom chsk-url-fn. The tricky thing, I think, is that the browser-repl uses a different port (i default browser-repl is 9000, ii. austin's browser-repl port changes each session), than the running http-kit server (8090). So when make-channel-socket! calls chsk-url-fn (which is the default-chsk-url-fn), (encore/get-window-location) is returning the URL with the browser-repl's port. I'm not sure how you'd get around that if you're using a browser-repl, vs just running in a plain browser runtime. Seems to come down to the behaviour of (.-location js/window) (in encore/get-window-location). Ie, in my browser-repl, invoking (.-location js/window) gives me "http://172.28.128.5:33283/347/repl/start?...".

cljs.user> (.-location js/window)                                                                                                                                                                                                    

#<http://172.28.128.5:33283/347/repl/start?xpc=%7B%22cn%22%3A%222qJDPo7hur%22%2C%22tp%22%3Anull%2C%22osh%22%3Anull%2C%22ppu%22%3A%22http%3A%2F%2F172.28.128.5%3A8090%2Frobots.txt%22%2C%22lpu%22%3A%22http%3A%2F%2F172.28.128.5%3A33\

283%2Frobots.txt%22%7D>

There's more context in this Google Group thread.

Event loop on client side: optional arguments?

Hi Peter,

I'm initializing Sente's client-side event loop in an Om component, and I would like to pass additional arguments to the event handler.

So I rewrite the event loop like so:

(defn event-loop
  "Handle inbound events."
  [owner]
  (go-loop [] 
    (let [[op arg] (<! ch-chsk)]
      (event-handler op arg owner)
      (recur))))

This seems to work just fine, but I was a bit worried that I might miss something, compared to what you wrote with start-chsk-router-loop! which seems to do more.

(defn start-chsk-router-loop! [event-handler ch]
  (let [ctrl-ch (chan)]
    (go-loop []
      (let [[v p] (async/alts! [ch ctrl-ch])]
        (if (identical? p ctrl-ch) ::stop
          (let [[id data :as event] v]
            ;; Provide ch to handler to allow event injection back into loop:
            (event-handler event ch)  ; Allow errors to throw
            (recur)))))
    (fn stop! [] (async/close! ctrl-ch))))

Is the event-loop I wrote satisfactory or would it be better to enhance the start-chsk-router-loop! function with optional arguments?

Thank you!

Sente in advanced compilation mode

When compiled in advanced mode, Sente doesn't even tries to establish WebSocket connection or long-polling ajax call. It falls back to ajax POST's.

Should i tweak something in cljsbuild configuration?

Logon/logoff example

Hi there!

It's not completely clear to me how to best implement logon/logoff functionality with Sente. I've implemented something, and it works. But it seems somewhat artificial.

So could you please provide us with your recommendation on how to implement logon -
I guess via old-school async AJAX resp-req, then establish websocket via sente/start-chsk-router-loop! ?

And then while you're at it, how to logoff (invalidate session, close socket etc.)?

Thanks for all your great work! :)

Best,
Henrik

Teething troubles

Apologies, probably user error. I cannot get a simple setup working. I've been through the docs for setting up server and client side and cannot get a (chsk-send! ...) from the client working.

(ns bom.core
  (:require-macros
   [cljs.core.match.macros :refer (match)]
   [cljs.core.async.macros :as asyncm :refer (go go-loop)])
  (:require
   [cljs.core.match]
   [cljs.core.async :as async :refer (<! >! put! chan)]
   [taoensso.sente :as sente :refer (cb-success?)]
   ))

(let [{:keys [chsk ch-recv send-fn]}
      (sente/make-channel-socket! "/chsk"
        {} {:type :auto})]
  (def chsk       chsk)
  (def ch-chsk    ch-recv)
  (def chsk-send! send-fn))

(chsk-send!
  [:chsk/ping {:data "data"}]
  8000
  (fn [reply]
    (if (cb-success? reply)
      (.log js/console "Success")
      (.log js/console "Failure"))))

results in:
image

Q: How to close all open sockets / connections?

I'm working on migrating some lifecycle code, where when a server shuts down I shutdown all open sockets from the server side. Is there some message I could send through the channel that would force a disconnection, or is there some other way to do this through the current sente API?

Thanks Peter!

Another example for Light Table users?

Hi, thanks for making sente.

What do you think about providing another example project without cljx?
It is because I am using the Light Table for coding and it doesn't handle cljx project nicely..

If you like this idea, please check my sample code .
This is a port of an ancient app for socket.io (maybe) and has clj and cljx codes separately.

I am thinking It would be great if this can help LT users to try sente!

my project

Graceful shutdown

When I restart http-kit in my development process after having called `sente/make-channel-socket!, I see an error on STDOUT.

ri Mar 28 22:44:12 IDT 2014 [nREPL-worker-5] ERROR - increase :queue-size if this happens often
java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@122c00b3 rejected from java.util.concurrent.ThreadPoolExecutor@6e1f117[Terminated, pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 7]

How should I go about gracefully shutting down all channel sockets with sente?

Graceful shutdown functionality will also be handy when componentizing sente or integrating it in Stuart Sierra's reloaded workflow.

[Perf] Buffer client>server events

This is far less important than buffering for server>user events (which we now have), but it'd still be a big efficiency win - esp. for Ajax + for bandwidth minimization on mobiles.

It's even possible to offer this for events with callbacks if we're prepared to allow actual timeouts to vary by [0,<max-buffer-window-size>] ms.

UPDATE: Have started some work for this on the dev branch. Shouldn't be hard to implement, just short on time atm.

Cnt dcphr chsk

I'm trying to follow the Sente tutorial and can't make much sense of the needlessly brief function names. ch-recv is easy to guess, chsk and ch-chsk are not. Consider using
more descriptive names and documenting what said functions do.

Assertion failed on `receive-buffered-evs!` with ClojureScript 0.0-2261

Description

Running the example project and clicking the chsk-send! (with reply) button throws a validation error on taoensso.sente/receive-buffered-evs!.

Steps to reproduce

  • Update the dependencies in example-project/project.clj for clojurescript to version 0.0-2261 and for cljsbuild to 1.0.3.
  • Follow the instructions on my_app.cljx to build and run the example application
  • Open the console of your browser
  • Click on the button chsk-send! (with reply)

NB I know the above steps could sound insulting to the project maintainer but they’re meant for everybody else. :)

Notes

I’ve tried to dig further into the problem but I’m unable to use the checkouts feature of Leinengen with the example project, so the whole build cycle would be too long for me. I never used this feature before, so maybe I’m doing it wrong.

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.