-
-
Notifications
You must be signed in to change notification settings - Fork 196
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
787eaa1
commit 695b773
Showing
11 changed files
with
557 additions
and
37 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,3 +11,4 @@ pom.xml* | |
/target/ | ||
/checkouts/ | ||
/logs/ | ||
/wiki/.git |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,2 @@ | ||
github: ptaoussanis | ||
custom: "https://www.taoensso.com/clojure/backers" | ||
custom: "https://www.taoensso.com/clojure" |
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,61 +1,81 @@ | ||
<a href="https://www.taoensso.com/clojure" title="More stuff by @ptaoussanis at www.taoensso.com"> | ||
<img src="https://www.taoensso.com/taoensso-open-source.png" alt="Taoensso open-source" width="380""/></a> | ||
<a href="https://www.taoensso.com/clojure" title="More stuff by @ptaoussanis at www.taoensso.com"><img src="https://www.taoensso.com/open-source.png" alt="Taoensso open source" width="340"/></a> | ||
[**Documentation**](#documentation) | [Latest releases](#latest-releases) | [Get support][GitHub issues] | ||
|
||
# Sente | ||
|
||
## Realtime web comms for Clojure/Script | ||
### Realtime web comms for Clojure/Script applications | ||
|
||
**Sente** is a small client+server library that makes it easy to build **realtime web applications** with Clojure + ClojureScript. | ||
|
||
Loosely inspired by [Socket.IO](https://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**. | ||
|
||
<img src="https://raw.githubusercontent.com/ptaoussanis/sente/master/hero.jpg" width="600"> | ||
<img width="600" src="hero.jpg"/> | ||
|
||
> **Sen-te** (先手) is a Japanese [Go](https://en.wikipedia.org/wiki/Go_(game)) 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 | ||
## Latest release/s | ||
|
||
- 2023-07-18: `1.19.1` - [release notes](https://github.com/ptaoussanis/sente/releases/tag/v1.19.1) | [Clojars](https://clojars.org/com.taoensso/sente/versions/1.19.1) | ||
- `2023-07-18` `1.19.1`: [changes](../../releases/tag/v1.19.1) | ||
|
||
<!--- [![tests][tests badge]][tests status] --> | ||
[![Main tests][Main tests SVG]][Main tests URL] | ||
[![Graal tests][Graal tests SVG]][Graal tests URL] | ||
|
||
## Resources | ||
1. [Wiki][wiki] - **community docs** (👈 start here) | ||
1. [Release info][] - releases and changes | ||
1. [API docs][] - auto-generated API docs | ||
1. [GitHub issues][] - for support requests, contributions, etc. | ||
See [here][GitHub releases] for earlier releases. | ||
|
||
## Features | ||
## Why Sente? | ||
|
||
* **Bidirectional a/sync comms** over **WebSockets** with **auto Ajax fallback** | ||
* **It just works**: auto keep-alives, buffering, protocol selection, reconnects | ||
* Efficient design with transparent event batching for **low-bandwidth use, even over Ajax** | ||
* Send **arbitrary Clojure vals** over [edn](https://github.com/edn-format/edn | ||
- **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](https://github.com/edn-format/edn | ||
) or [Transit](https://github.com/cognitect/transit-clj) (JSON, MessagePack, etc.) | ||
* **Tiny API** (see the [wiki][] for details) | ||
* Automatic, sensible support for users connected with **multiple clients** and/or devices simultaneously | ||
* Realtime info on **which users are connected** over which protocols | ||
* Standard **Ring security model**: auth as you like, HTTPS when available, CSRF support, etc. | ||
* Support for [several popular web servers](https://github.com/ptaoussanis/sente/tree/master/src/taoensso/sente/server_adapters), [easily extended](https://github.com/ptaoussanis/sente/blob/master/src/taoensso/sente/interfaces.cljc) to other servers. | ||
- Tiny, easy-to-use [API](https://github.com/taoensso/sente/wiki#usage) | ||
- 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](https://github.com/ptaoussanis/sente/tree/master/src/taoensso/sente/server_adapters), [easily extended](https://github.com/ptaoussanis/sente/blob/master/src/taoensso/sente/interfaces.cljc) to other servers. | ||
|
||
## Funding this work | ||
### Capabilities | ||
|
||
Please see [here][funding] if you'd like to help support my continued [open-source work][] (thank you!! 🙏) - Peter | ||
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 | ||
|
||
- [Full documentation][GitHub wiki] (**getting started** and more) | ||
- Auto-generated API reference: [Codox][Codox docs], [clj-doc][clj-doc docs] | ||
|
||
## Funding | ||
|
||
You can [help support][sponsor] continued work on this project, thank you!! 🙏 | ||
|
||
## License | ||
|
||
Copyright © 2014-2023 [Peter Taoussanis][], licensed under [EPL 1.0][] (same as Clojure). | ||
Copyright © 2012-2023 [Peter Taoussanis][]. | ||
Licensed under [EPL 1.0](LICENSE.txt) (same as Clojure). | ||
|
||
<!-- Common --> | ||
|
||
[GitHub releases]: ../../releases | ||
[GitHub issues]: ../../issues | ||
[GitHub wiki]: ../../wiki | ||
|
||
<!--- Common links --> | ||
[wiki]: ../../wiki | ||
[Release info]: ../../releases | ||
[GitHub issues]: ../../issues | ||
[funding]: https://taoensso.com/clojure/backers | ||
[EPL 1.0]: LICENSE | ||
[Peter Taoussanis]: https://www.taoensso.com | ||
[open-source work]: https://www.taoensso.com/clojure | ||
[sponsor]: https://www.taoensso.com/sponsor | ||
|
||
<!-- Project --> | ||
|
||
[Codox docs]: https://taoensso.github.io/sente/ | ||
[clj-doc docs]: https://cljdoc.org/d/com.taoensso/sente/ | ||
|
||
[Clojars SVG]: https://img.shields.io/clojars/v/com.taoensso/sente.svg | ||
[Clojars URL]: https://clojars.org/com.taoensso/sente | ||
|
||
<!--- Repo links --> | ||
[API docs]: http://ptaoussanis.github.io/sente/ | ||
[tests badge]: https://github.com/ptaoussanis/sente/actions/workflows/tests.yml/badge.svg | ||
[tests status]: https://github.com/ptaoussanis/sente/actions/workflows/tests.yml | ||
[Main tests SVG]: https://github.com/taoensso/sente/actions/workflows/main-tests.yml/badge.svg | ||
[Main tests URL]: https://github.com/taoensso/sente/actions/workflows/main-tests.yml | ||
[Graal tests SVG]: https://github.com/taoensso/sente/actions/workflows/graal-tests.yml/badge.svg | ||
[Graal tests URL]: https://github.com/taoensso/sente/actions/workflows/graal-tests.yml |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
This page details possible **breaking changes and migration instructions** for Sente. | ||
|
||
My apologies for the trouble. I'm very mindful of the costs involved in breaking changes, and I try hard to avoid them whenever possible. When there is a very good reason to break, I'll try to batch breaks and to make migration as easy as possible. | ||
|
||
Thanks for your understanding - [Peter Taoussanis](https://www.taoensso.com) | ||
|
||
# Sente `v1.17` to `v1.18` | ||
|
||
This upgrade involves **4 possible breaking changes** detailed below: | ||
|
||
**Change 1/4** | ||
|
||
The default `wrap-recv-evs?` option has changed in [`make-channel-socket-client!`](http://ptaoussanis.github.io/sente/taoensso.sente.html#var-make-channel-socket-client.21). | ||
|
||
- **Old** default behaviour: events from server are **wrapped** with `[:chsk/recv <event>]` | ||
- **New** default behaviour: events from server are **unwrapped** | ||
|
||
**Motivation for change**: there's no benefit to wrapping events from the server, and this wrapping often causes confusion. | ||
|
||
More info at: [#319](../issues/319) | ||
|
||
--- | ||
|
||
**Change 2/4** | ||
|
||
The default [`*write-legacy-pack-format?*`](http://ptaoussanis.github.io/sente/taoensso.sente.html#var-*write-legacy-pack-format.3F*) value has changed from `true` to `false`. | ||
|
||
This change is only relevant for the small minority of folks that use a custom (non-standard) [`IPacker`](https://github.com/ptaoussanis/sente/blob/f69a5df6d1f3e88d66a148c74e1b5a9084c9c0b9/src/taoensso/sente/interfaces.cljc#L55). | ||
|
||
If you do use a custom (non-standard) `IPacker`, please see the [relevant docstring](http://ptaoussanis.github.io/sente/taoensso.sente.html#var-*write-legacy-pack-format.3F*) for details. | ||
|
||
**Motivation for change**: the new default value is part of a phased transition to a new Sente message format that better supports non-string (e.g. binary) payloads. | ||
|
||
More info at: [#398](../issues/398), [#404](../issues/404) | ||
|
||
--- | ||
|
||
**Change 3/4** | ||
|
||
Unofficial adapters have been moved to `community` dir. | ||
|
||
This change is only relevant for folks using a server other than http-kit. | ||
|
||
If you're using a different server, the adapter's namespace will now include a `.community` part, e.g.: | ||
|
||
- **Old** adapter namespace: `taoensso.sente.server-adapters.undertow` | ||
- **New** adapter namespace: `taoensso.sente.server-adapters.community.undertow` | ||
|
||
**Motivation for change**: the new namespace structure is intended to more clearly indicate which adapters are/not officially maintained as part of the core project. | ||
|
||
More info at: [#412](../issues/412) | ||
|
||
--- | ||
|
||
**Change 4/4** | ||
|
||
The `jetty9-ring-adapter` has been removed. | ||
|
||
This change is only relevant for folks using `jetty9-ring-adapter`. | ||
|
||
**Motivation for change**: it looks like the previous adapter may have been broken for some time. And despite [some effort](../issues/426) from the community, a new/fixed adapter isn't currently available. Further investigation is necessary, but it looks like it's _possible_ that the current `jetty9-ring-adapter` API might not support the kind of functionality that Sente needs for its Ajax fallback behaviour. | ||
|
||
Apologies for this! | ||
|
||
More info at: [#424](../issues/424), [#426](../issues/426) | ||
|
||
--- |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,200 @@ | ||
> See also [here](3-Example-projects) for **full example projects** 👈 | ||
# Setup | ||
|
||
## Dependency | ||
|
||
Add the [relevant dependency](../#latest-releases) to your project: | ||
|
||
```clojure | ||
Leiningen: [com.taoensso/sente "x-y-z"] ; or | ||
deps.edn: com.taoensso/sente {:mvn/version "x-y-z"} | ||
``` | ||
|
||
## Server-side setup | ||
|
||
First make sure that you're using one of the [supported web servers](https://github.com/ptaoussanis/sente/tree/master/src/taoensso/sente/server_adapters) (PRs for additional server adapters welcome!). | ||
|
||
Somewhere in your web app's code you'll already have a routing mechanism in place for handling Ring requests by request URL. If you're using [Compojure](https://github.com/weavejester/compojure) for example, you'll have something that looks like this: | ||
|
||
```clojure | ||
(defroutes my-app | ||
(GET "/" req (my-landing-pg-handler req)) | ||
(POST "/submit-form" req (my-form-submit-handler req))) | ||
``` | ||
|
||
For Sente, we're going to add 2 new URLs and setup their handlers: | ||
|
||
```clojure | ||
(ns my-server-side-routing-ns ; Usually a .clj file | ||
(:require | ||
;; <other stuff> | ||
[taoensso.sente :as sente] ; <--- Add this | ||
|
||
[ring.middleware.anti-forgery :refer [wrap-anti-forgery]] ; <--- Recommended | ||
|
||
;; Uncomment a web-server adapter ---> | ||
;; [taoensso.sente.server-adapters.http-kit :refer [get-sch-adapter]] | ||
;; [taoensso.sente.server-adapters.immutant :refer [get-sch-adapter]] | ||
;; [taoensso.sente.server-adapters.nginx-clojure :refer [get-sch-adapter]] | ||
;; [taoensso.sente.server-adapters.aleph :refer [get-sch-adapter]] | ||
)) | ||
|
||
;;; Add this: ---> | ||
(let [{:keys [ch-recv send-fn connected-uids | ||
ajax-post-fn ajax-get-or-ws-handshake-fn]} | ||
(sente/make-channel-socket-server! (get-sch-adapter) {})] | ||
|
||
(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 | ||
) | ||
|
||
(defroutes my-app-routes | ||
;; <other stuff> | ||
|
||
;;; Add these 2 entries: ---> | ||
(GET "/chsk" req (ring-ajax-get-or-ws-handshake req)) | ||
(POST "/chsk" req (ring-ajax-post req)) | ||
) | ||
|
||
(def my-app | ||
(-> my-app-routes | ||
;; Add necessary Ring middleware: | ||
ring.middleware.keyword-params/wrap-keyword-params | ||
ring.middleware.params/wrap-params | ||
ring.middleware.anti-forgery/wrap-anti-forgery | ||
ring.middleware.session/wrap-session)) | ||
``` | ||
|
||
> The `ring-ajax-post` and `ring-ajax-get-or-ws-handshake` fns will automatically handle Ring GET and POST requests to our channel socket URL (`"/chsk"`). Together these take care of the messy details of establishing + maintaining WebSocket or long-polling requests. | ||
Add a CSRF token somewhere in your HTML: | ||
|
||
``` | ||
(let [csrf-token (force ring.middleware.anti-forgery/*anti-forgery-token*)] | ||
[:div#sente-csrf-token {:data-csrf-token csrf-token}]) | ||
``` | ||
|
||
## Client-side setup | ||
|
||
You'll setup something similar on the client side: | ||
|
||
```clojure | ||
(ns my-client-side-ns ; Usually a .cljs file | ||
(:require-macros | ||
[cljs.core.async.macros :as asyncm :refer (go go-loop)]) | ||
(:require | ||
;; <other stuff> | ||
[cljs.core.async :as async :refer (<! >! put! chan)] | ||
[taoensso.sente :as sente :refer (cb-success?)] ; <--- Add this | ||
)) | ||
|
||
;;; Add this: ---> | ||
|
||
(def ?csrf-token | ||
(when-let [el (.getElementById js/document "sente-csrf-token")] | ||
(.getAttribute el "data-csrf-token"))) | ||
|
||
(let [{:keys [chsk ch-recv send-fn state]} | ||
(sente/make-channel-socket-client! | ||
"/chsk" ; Note the same path as before | ||
?csrf-token | ||
{:type :auto ; e/o #{:auto :ajax :ws} | ||
})] | ||
|
||
(def chsk chsk) | ||
(def ch-chsk ch-recv) ; ChannelSocket's receive channel | ||
(def chsk-send! send-fn) ; ChannelSocket's send API fn | ||
(def chsk-state state) ; Watchable, read-only atom | ||
) | ||
``` | ||
|
||
# Usage | ||
|
||
After setup, the client will automatically initiate a WebSocket or repeating long-polling connection to your server. Client<->server events are now ready to transmit over the `ch-chsk` channel. | ||
|
||
**Last step**: you'll want to **hook your own event handlers up to this channel**. Please see one of the [example projects](3-Example-projects) and/or [API docs](http://ptaoussanis.github.io/sente/) for details. | ||
|
||
## Client-side API | ||
|
||
* `ch-recv` is a **core.async channel** that'll receive `event-msg`s | ||
* `chsk-send!` is a `(fn [event & [?timeout-ms ?cb-fn]])` for standard **client>server req>resp calls** | ||
|
||
Let's compare some Ajax and Sente code for sending an event from the client to the server: | ||
|
||
```clojure | ||
(jayq/ajax ; Using the jayq wrapper around jQuery | ||
{:type :post :url "/some-url-on-server/" | ||
:data {:name "Rich Hickey" | ||
:type "Awesome"} | ||
:timeout 8000 | ||
:success (fn [content text-status xhr] (do-something! content)) | ||
:error (fn [xhr text-status] (error-handler!))}) | ||
|
||
(chsk-send! ; Using Sente | ||
[:some/request-id {:name "Rich Hickey" :type "Awesome"}] ; Event | ||
8000 ; Timeout | ||
;; Optional callback: | ||
(fn [reply] ; Reply is arbitrary Clojure data | ||
(if (sente/cb-success? reply) ; Checks for :chsk/closed, :chsk/timeout, :chsk/error | ||
(do-something! reply) | ||
(error-handler!)))) | ||
``` | ||
|
||
Note: | ||
|
||
* The Ajax request is slow to initialize, and bulky (HTTP overhead) | ||
* The Sente request is pre-initialized (usu. WebSocket), and lean (edn/Transit protocol) | ||
|
||
## Server-side API | ||
|
||
* `ch-recv` is a **core.async channel** that'll receive `event-msg`s | ||
* `chsk-send!` is a `(fn [user-id event])` for async **server>user PUSH calls** | ||
|
||
For asynchronously pushing an event from the server to the client: | ||
|
||
* Ajax would require a clumsy long-polling setup, and wouldn't easily support users connected with multiple clients simultaneously | ||
* Sente: `(chsk-send! "destination-user-id" [:some/alert-id <arb-clj-data-payload>])` | ||
|
||
**Important**: note that Sente intentionally offers server to [user](2-Client-and-user-ids) push rather than server>client push. A single user may have >=0 associated clients. | ||
|
||
## Types and terminology | ||
|
||
Term | Form | ||
---------------- | ---------------------------------------------------------------------- | ||
event | `[<ev-id> <?ev-data>]`, e.g. `[:my-app/some-req {:data "data"}]` | ||
server event-msg | `{:keys [event id ?data send-fn ?reply-fn uid ring-req client-id]}` | ||
client event-msg | `{:keys [event id ?data send-fn]}` | ||
`<ev-id>` | A _namespaced_ keyword like `:my-app/some-req` | ||
`<?ev-data>` | An optional _arbitrary edn value_ like `{:data "data"}` | ||
`:ring-req` | Ring map for Ajax request or WebSocket's initial handshake request | ||
`:?reply-fn` | Present only when client requested a reply | ||
|
||
|
||
## Summary | ||
|
||
* Clients use `chsk-send!` to send `event`s to the server and optionally request a reply with timeout | ||
* Server uses `chsk-send!` to send `event`s to _all_ the clients (browser tabs, devices, etc.) of a particular connected user by his/her [user-id](2-Client-and-user-ids). | ||
* The server can also use an `event-msg`'s `?reply-fn` to _reply_ to a particular client `event` using an _arbitrary edn value_ | ||
|
||
## Channel socket state | ||
|
||
Each time the client's channel socket state changes, a client-side `:chsk/state` event will fire that you can watch for and handle like any other event. | ||
|
||
The event form is `[:chsk/state [<old-state-map> <new-state-map>]]` with the following possible state map keys: | ||
|
||
Key | Value | ||
--------------- | -------------------------------------------------------- | ||
:type | e/o `#{:auto :ws :ajax}` | ||
:open? | Truthy iff chsk appears to be open (connected) now | ||
:ever-opened? | Truthy iff chsk handshake has ever completed successfully | ||
:first-open? | Truthy iff chsk just completed first successful handshake | ||
:uid | User id provided by server on handshake, or nil | ||
:csrf-token | CSRF token provided by server on handshake, or nil | ||
:handshake-data | Arb user data provided by server on handshake | ||
:last-ws-error | `?{:udt _ :ev <WebSocket-on-error-event>}` | ||
:last-ws-close | `?{:udt _ :ev <WebSocket-on-close-event> :clean? _ :code _ :reason _}` | ||
:last-close | `?{:udt _ :reason _}`, with reason e/o `#{nil :requested-disconnect :requested-reconnect :downgrading-ws-to-ajax :unexpected}` |
Oops, something went wrong.