Skip to content

Commit

Permalink
[new] Add wiki content to main repo
Browse files Browse the repository at this point in the history
  • Loading branch information
ptaoussanis committed Aug 3, 2023
1 parent 337dd5e commit a6e9f31
Show file tree
Hide file tree
Showing 8 changed files with 494 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ pom.xml*
/target/
/checkouts/
/logs/
/wiki/.git
67 changes: 67 additions & 0 deletions wiki/0-Breaking-changes.md
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)

---
199 changes: 199 additions & 0 deletions wiki/1-Getting-started.md
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](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}`
56 changes: 56 additions & 0 deletions wiki/2-Client-and-user-ids.md
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.
26 changes: 26 additions & 0 deletions wiki/3-Example-projects.md
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)
Loading

0 comments on commit a6e9f31

Please sign in to comment.