diff --git a/.gitignore b/.gitignore index 34af8da..ef8d6f1 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ pom.xml* /target/ /checkouts/ /logs/ +/wiki/.git diff --git a/FUNDING.yml b/FUNDING.yml index dc3d4d2..964e36a 100644 --- a/FUNDING.yml +++ b/FUNDING.yml @@ -1,2 +1,2 @@ github: ptaoussanis -custom: "https://www.taoensso.com/clojure/backers" +custom: "https://www.taoensso.com/clojure" diff --git a/LICENSE b/LICENSE.txt similarity index 100% rename from LICENSE rename to LICENSE.txt diff --git a/README.md b/README.md index f550752..fecd294 100644 --- a/README.md +++ b/README.md @@ -1,61 +1,81 @@ - -Taoensso open-source +Taoensso open source +[**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**. - + > **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) - +[![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](../../wiki/1-Getting-started#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](../../tree/master/src/taoensso/sente/server_adapters), [easily extended](../../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). + + + +[GitHub releases]: ../../releases +[GitHub issues]: ../../issues +[GitHub wiki]: ../../wiki - -[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 + + + +[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 - -[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 diff --git a/wiki/0-Breaking-changes.md b/wiki/0-Breaking-changes.md new file mode 100644 index 0000000..e75b2d1 --- /dev/null +++ b/wiki/0-Breaking-changes.md @@ -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 ]` +- **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) + +--- \ No newline at end of file diff --git a/wiki/1-Getting-started.md b/wiki/1-Getting-started.md new file mode 100644 index 0000000..b63453a --- /dev/null +++ b/wiki/1-Getting-started.md @@ -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 + ;; + [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 + ;; + + ;;; 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 + ;; + [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 ])` + +**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 | `[ ]`, 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]}` +`` | A _namespaced_ keyword like `:my-app/some-req` +`` | 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 [ ]]` 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 }` +:last-ws-close | `?{:udt _ :ev :clean? _ :code _ :reason _}` +:last-close | `?{:udt _ :reason _}`, with reason e/o `#{nil :requested-disconnect :requested-reconnect :downgrading-ws-to-ajax :unexpected}` \ No newline at end of file diff --git a/wiki/2-Client-and-user-ids.md b/wiki/2-Client-and-user-ids.md new file mode 100644 index 0000000..705d78b --- /dev/null +++ b/wiki/2-Client-and-user-ids.md @@ -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. \ No newline at end of file diff --git a/wiki/3-Example-projects.md b/wiki/3-Example-projects.md new file mode 100644 index 0000000..f354bfe --- /dev/null +++ b/wiki/3-Example-projects.md @@ -0,0 +1,29 @@ +# 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 linked examples, please **contact the relevant authors** to let them know! + +Contributor | Link | Description +:-- | :-- | :-- +[@fiv0](https://github.com/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](https://github.com/dharrigan) | [websockets](https://github.com/dharrigan/websockets) | With [Reitit](https://github.com/metosin/reitit), Jetty 9/10 and [websockets-js](https://github.com/dharrigan/websockets-js) +[@laforge49](https://github.com/laforge49) | [sente-boot](https://github.com/laforge49/sente-boot/) | With Sente v1.11, [Boot](https://github.com/boot-clj/boot), works with Windows +[@laforge49](https://github.com/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](https://github.com/tiensonqin) | [lymchat](https://github.com/tiensonqin/lymchat) | Chat app with [React Native](https://github.com/facebook/react-native) +[@danielsz](https://github.com/danielsz) | [system-websockets](https://github.com/danielsz/system-websockets) | Client-side UI, login and wiring of components +[@timothypratley](https://github.com/timothypratley) | [snakelake](https://github.com/timothypratley/snakelake) | Multiplayer snake game with screencast walkthrough +[@theasp](https://github.com/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](https://github.com/ebellani) | [carpet](https://github.com/ebellani/carpet) | Web+mobile interface for a remmitance application +[@danielsz](https://github.com/danielsz) | [sente-system](https://github.com/danielsz/sente-system) | Ref. example adapted for [system](https://github.com/danielsz/system) +[@danielsz](https://github.com/danielsz) | [sente-boot](https://github.com/danielsz/sente-boot) | Ref. example adapted for [boot](https://github.com/boot-clj/boot) +[@seancorfield](https://github.com/seancorfield) | [om-sente](https://github.com/seancorfield/om-sente) | With [Om](https://github.com/swannodette/om) +[@tfoldi](https://github.com/tfoldi) | [data15-blackjack](https://github.com/tfoldi/data15-blackjack) | Multiplayer blackjack game +[@davidvujic](https://github.com/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) +_ | _ | Your link here? [PRs](../wiki#contributions-welcome) welcome! \ No newline at end of file diff --git a/wiki/3-FAQ.md b/wiki/3-FAQ.md new file mode 100644 index 0000000..6ebd91e --- /dev/null +++ b/wiki/3-FAQ.md @@ -0,0 +1,103 @@ +# What is the `user-id` provided to the server>user push fn? + +For the server to push events, we need a destination. Traditionally we might push to a _client_ (e.g. browser tab). But with modern rich web applications and the increasing use of multiple simultaneous devices (tablets, mobiles, etc.) - the value of a _client_ push is diminishing. You'll often see applications (even by Google) struggling to deal with these cases. + +Sente offers an out-the-box solution by pulling the concept of identity one level higher and dealing with unique _users_ rather than clients. **What constitutes a user is entirely at the discretion of each application**: + + * Each user-id may have zero _or more_ connected clients at any given time + * Each user-id _may_ survive across clients (browser tabs, devices), and sessions + +**To give a user an identity, either set the user's `:uid` Ring session key OR supply a `:user-id-fn` (takes request, returns an identity string) to the `make-channel-socket-server!` constructor.** + +If you want a simple _per-session_ identity, generate a _random uuid_. If you want an identity that persists across sessions, try use something with _semantic meaning_ that you may already have like a database-generated user-id, a login email address, a secure URL fragment, etc. + +> Note that user-ids are used **only** for server>user push. client>server requests don't take a user-id. + +See [here](2-Client-and-user-ids) for more info. + +# How do I integrate Sente with my usual login/auth procedure? + +This should be pretty easy to do, please see one of the [example projects](3-Example-projects) for details! + +# Will Sente work with Reactjs/Reagent/Om/Pedestel/etc.? + +Sure! Sente's just a client<->server comms mechanism so it'll work with any view/rendering approach you'd like. + +I have a strong preference for [Reagent](https://reagent-project.github.io/) myself, so would recommend checking that out first if you're still evaluating options. + +# What if I need to use JSON, XML, raw strings, etc.? + +Sente uses an extensible client<->server serialization mechanism. It uses edn by default since this usu. gives good performance and doesn't require any external dependencies. The [reference example project](3-Example-projects#reference-example) shows how you can plug in an alternative de/serializer. In particular, note that Sente ships with a Transit de/serializer that allows manual or smart (automatic) per-payload format selection. + +# How do I add custom Transit read and write handlers? + +To add custom handlers to the TransitPacker, pass them in as `writer-opts` and `reader-opts` when creating a `TransitPacker`. These arguments are the same as the `opts` map you would pass directly to `transit/writer`. The code sample below shows how you would do this to add a write handler to convert [Joda-Time](https://www.joda.org/joda-time/) `DateTime` objects to Transit `time` objects. + +```clj +(ns my-ns.app + (:require [cognitect.transit :as transit] + [taoensso.sente.packers.transit :as sente-transit]) + (:import [org.joda.time DateTime ReadableInstant])) + +;; From https://increasinglyfunctional.com/2014/09/02/custom-transit-writers-clojure-joda-time.html +(def joda-time-writer + (transit/write-handler + (constantly "m") + (fn [v] (-> ^ReadableInstant v .getMillis)) + (fn [v] (-> ^ReadableInstant v .getMillis .toString)))) + +(def packer (sente-transit/->TransitPacker :json {:handlers {DateTime joda-time-writer}} {})) +``` + +# How do I route client/server events? + +However you like! If you don't have many events, a simple `cond` will probably do. Otherwise a multimethod dispatching against event ids works well (this is the approach taken in the [reference example project](./3-Example-projects#reference-example)). + +# Security: is there HTTPS support? + +Yes, it's automatic for both Ajax and WebSockets. If the page serving your JavaScript (ClojureScript) is running HTTPS, your Sente channel sockets will run over HTTPS and/or the WebSocket equivalent (WSS). + +# Security: CSRF protection? + +**This is important**. Sente has support, and use is **strongly recommended**. You'll need to use middleware like [ring-anti-forgery](https://github.com/ring-clojure/ring-anti-forgery) or [ring-defaults](https://github.com/ring-clojure/ring-defaults) to generate and check CSRF codes. The `ring-ajax-post` handler should be covered (i.e. protected). + +Please see one of the [example projects](./3-Example-projects) for a fully-baked example. + +# Pageload: How do I know when Sente is ready client-side? + +You'll want to listen on the receive channel for a `[:chsk/state [_ {:first-open? true}]]` event. That's the signal that the socket's been established. + +# How can server-side channel socket events modify a user's session? + +Recall that server-side `event-msg`s are of the form `{:ring-req _ :event _ :?reply-fn _}`, so each server-side event is accompanied by the relevant[*] Ring request. + +> For WebSocket events this is the initial Ring HTTP handshake request, for Ajax events it's just the Ring HTTP Ajax request. + +The Ring request's `:session` key is an immutable value, so how do you modify a session in response to an event? You won't be doing this often, but it can be handy (e.g. for login/logout forms). + +You've got two choices: + +1. Write any changes directly to your Ring SessionStore (i.e. the mutable state that's actually backing your sessions). You'll need the relevant user's session key, which you can find under your Ring request's `:cookies` key. This is flexible, but requires that you know how+where your session data is being stored. + +2. Just use regular HTTP Ajax requests for stuff that needs to modify sessions (like login/logout), since these will automatically go through the usual Ring session middleware and let you modify a session with a simple `{:status 200 :session }` response. This is the strategy the reference example takes. + +[@danielsz](https://github.com/danielsz) has kindly provided a detailed example [here](../issues/62#issuecomment-58790741). + +# Lifecycle management (component management/shutdown, etc.) + +Using something like [@stuartsierra/component](https://github.com/stuartsierra/component) or [@palletops/leaven](https://github.com/palletops/leaven)? + +Most of Sente's state is held internally to each channel socket (the map returned from client/server calls to `make-channel-socket!`). The absence of global state makes things like testing, and running multiple concurrent connections easy. It also makes integration with your component management easy. + +The only thing you _may_[1] want to do on component shutdown is stop any router loops that you've created to dispatch events to handlers. The client/server side `start-chsk-router!` fns both return a `(fn stop [])` that you can call to do this. + +> [1] The cost of _not_ doing this is actually negligible (a single parked go thread). + +There's also a couple lifecycle libraries that include Sente components: + + 1. [@danielsz/system](https://github.com/danielsz/system) for use with [@stuartsierra/component](https://github.com/stuartsierra/component) + 2. [@palletops/bakery](https://github.com/palletops/bakery) for use with [@palletops/leaven](https://github.com/palletops/leaven) + +# How to debug/benchmark Sente at the protocol level? + +[@arichiardi](https://github.com/arichiardi) has kindly provided some notes on this [here](./4-Connection-debugging). \ No newline at end of file diff --git a/wiki/4-Connection-debugging.md b/wiki/4-Connection-debugging.md new file mode 100644 index 0000000..9837caf --- /dev/null +++ b/wiki/4-Connection-debugging.md @@ -0,0 +1,36 @@ +Some info is provided here on **protocol-level debugging and profiling** for Sente connections. + +# Ajax connections + +These are easily debugged and profiled via your browser's usual network tools. + +# WebSocket connections + +You can inspect Sente packets using `Wireshark` or similar tools. + +Assuming Sente doesn't degrade to Ajax, the initial [WebSocket upgrade](https://tools.ietf.org/html/rfc6455#section-1.2) handshake will include a `:client-id` parameter: + +``` +GET /chsk?client-id=bd5ee0f2-dc22-47b5-98ab-618711f34b45 HTTP/1.1 +Host: localhost:3000 +Connection: Upgrade +Upgrade: websocket +Origin: http://localhost:3000 +Sec-WebSocket-Version: 13 +... +``` + +This is important if you want to emulate Sente's behavior using benchmarking tools like [tcpkali](https://github.com/machinezone/tcpkali), etc. Without this Sente will throw an exception and the benchmark will fail. + +Afterwards, you'll see a series of TCP packets as per the Websocket protocol and containing the `[ ]` vector encoded according to the selected Packer. For instance with Transit: + +``` +`e@7.KcJam +43C-["~:chsk/handshake",["bd5ee0f2-dc22-47b5-98ab-618711f34b45",null]] +``` + +## What is the `+` character I see attached to my Websocket `?ev-data`? + +This is a normal part of Sente's payload encoding. + +See [here](https://github.com/arichiardi/sente/blob/162149663e63fcda0348fb8d28d5533c4d0004cd/src/taoensso/sente.cljc#L212) for the gory details if you need to reproduce this behaviour. \ No newline at end of file diff --git a/wiki/Home.md b/wiki/Home.md new file mode 100644 index 0000000..af3d591 --- /dev/null +++ b/wiki/Home.md @@ -0,0 +1,8 @@ +See the menu to the right for content 👉 + +# Contributions welcome + +**PRs very welcome** to help improve this documentation! +See the [wiki](../tree/master/wiki) folder in the main repo for the relevant files. + +\- [Peter Taoussanis](https://www.taoensso.com) \ No newline at end of file