-
-
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
337dd5e
commit a8df561
Showing
9 changed files
with
502 additions
and
0 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 |
---|---|---|
@@ -0,0 +1 @@ | ||
._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 |
---|---|---|
@@ -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,199 @@ | ||
> 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] 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}` |
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,56 @@ | ||
Sente uses two types of connection identifiers: **client-ids** and **user-ids**. | ||
|
||
# Client ids | ||
|
||
A client-id is a unique identifier for one particular Sente client: i.e. one particular invocation of `make-channel-socket-client!`. This typically means **one particular browser tab** on one device. | ||
|
||
By default, clients generate their own random (uuid) client-id. You can override this in your call to [`make-channel-socket-client!`](http://ptaoussanis.github.io/sente/taoensso.sente.html#var-make-channel-socket-client.21). | ||
|
||
Note: | ||
1. Each client chooses its _own_ client-id with no input from the server. | ||
2. By default, each browser tab has its _own_ client-id. | ||
3. By default, reloading a browser tab (or closing a tab + opening a new one) means a _new_ client-id. | ||
|
||
# User ids | ||
|
||
This is the more important concept in Sente, and is actually the only type of identifier supported by Sente's server>client push API. | ||
|
||
A user-id is a unique application-level identifier associated with >=0 Sente clients (client-ids). | ||
|
||
It is determined _server-side_ as a configurable function of each connecting channel socket's Ring request, i.e. `(fn user-id [ring-req]) => ?user-id`. | ||
|
||
Typically, you'll configure Sente's user-id to be something like your application's username: if Alice logs into your application with 6 different browser tabs over 3 different devices - she'll have 6 client-ids associated with her user-id. And when your server sends an event "to Alice", it'll be delivered to all 6 of her connected clients. | ||
|
||
By default, Sente will use `(fn user-id [ring-req] (get-in ring-req [:session :uid]))` as your user-id function. You can override this in your call to [`make-channel-socket-server!`](http://ptaoussanis.github.io/sente/taoensso.sente.html#var-make-channel-socket-server.21). | ||
|
||
Note: | ||
|
||
1. One user-id may be associated with 0, 1, or _many_ clients (client-ids). | ||
2. By default (i.e. with the sessionized `:uid` value), user-ids are persistent and shared among multiple tabs in one browser as a result of the way browser sessions work. | ||
|
||
# Examples | ||
|
||
## Per-session persistent user-id | ||
|
||
This is probably a good default choice. | ||
|
||
1. `:client-id`: use default (random uuid) | ||
2. `:user-id-fn`: use default, ensure that you sessionize a sensible `:uid` value on user login | ||
|
||
## Per-tab transient user-id | ||
|
||
I.e. each tab has its own user-id, and reloading a tab means a new user-id. | ||
|
||
1. `:client-id`: use default (random uuid) | ||
2. `:user-id-fn`: use `(fn [ring-req] (:client-id ring-req))` | ||
|
||
I.e. we don't use sessions for anything. User-ids are equal to client-ids, which are random per-tab uuids. | ||
|
||
## Per-tab transient user-id with session security | ||
|
||
I.e. as above, but users must be signed in with a session. | ||
|
||
1. `:client-id`: leave unchanged. | ||
2. `:user-id-fn`: `(fn [ring-req] (str (get-in ring-req [:session :base-user-id]) "/" (:client-id ring-req)))` | ||
|
||
I.e. sessions (+ some kind of login procedure) are used to determine a `:base-user-id`. That base user-id is then joined with each unique client-id. Each tab therefore retains its own user-id, but each user-id is dependent on a secure login procedure. |
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,26 @@ | ||
# Reference example | ||
|
||
[This](../tree/master/example-project) is the official example used for testing Sente, and makes a great starting point for the basics. | ||
|
||
It's kept up-to-date and includes basic client+server setup, routing, auth, CSRF protection, etc. | ||
|
||
# Community examples | ||
|
||
Please note that unofficial examples are **provided by the community** and may contain out-of-date or inaccurate information. If you spot issues with any of the community examples, please contact the relevant authors to let them know! | ||
|
||
Link | Description | ||
---| --- | ||
[@fiv0/spa-ws-template](https://github.com/FiV0/spa-ws-template) | SPA with [re-frame](https://github.com/day8/re-frame), [http-kit](https://github.com/http-kit/http-kit), [shadow-cljs](https://github.com/thheller/shadow-cljs) | ||
[@dharrigan/websockets](https://github.com/dharrigan/websockets) | With [Reitit](https://github.com/metosin/reitit), Jetty 9/10 and [@dharrigan/websockets-js](https://github.com/dharrigan/websockets-js) | ||
[@laforge49/sente-boot](https://github.com/laforge49/sente-boot/) | With Sente v1.11, [Boot](https://github.com/boot-clj/boot), works with Windows | ||
[@laforge49/sente-boot-reagent](https://github.com/laforge49/sente-boot-reagent) | With Sente v1.11, [Boot](https://github.com/boot-clj/boot), and [Reagent](https://github.com/reagent-project/reagent) | ||
[@tiensonqin/lymchat](https://github.com/tiensonqin/lymchat) | Chat app with [React Native](https://github.com/facebook/react-native) | ||
[@danielsz/system-websockets](https://github.com/danielsz/system-websockets) | Client-side UI, login and wiring of components | ||
[@timothypratley/snakelake](https://github.com/timothypratley/snakelake) | Multiplayer snake game with screencast walkthrough | ||
[@theasp/sente-nodejs-example](https://github.com/theasp/sente-nodejs-example) | Ref. example adapted for Node.js servers ([Express](https://github.com/expressjs/express), [Dog Fort](https://github.com/whamtet/dogfort)), and Node.js client | ||
[@ebellani/carpet](https://github.com/ebellani/carpet) | Web+mobile interface for a remmitance application | ||
[@danielsz/sente-system](https://github.com/danielsz/sente-system) | Ref. example adapted for [@danielsz/system](https://github.com/danielsz/system) | ||
[@danielsz/sente-boot](https://github.com/danielsz/sente-boot) | Ref. example adapted for [boot](https://github.com/boot-clj/boot) | ||
[@seancorfield/om-sente](https://github.com/seancorfield/om-sente) | With [Om](https://github.com/swannodette/om) | ||
[@tfoldi/data15-blackjack](https://github.com/tfoldi/data15-blackjack) | Multiplayer blackjack game | ||
[@davidvujic/sente-with-reagent-and-re-frame](https://github.com/DavidVujic/sente-with-reagent-and-re-frame) | SPA with [re-frame](https://github.com/day8/re-frame) |
Oops, something went wrong.