Skip to content

Commit

Permalink
[nop] Update README template
Browse files Browse the repository at this point in the history
  • Loading branch information
ptaoussanis committed Aug 4, 2023
1 parent 787eaa1 commit f8145b8
Show file tree
Hide file tree
Showing 11 changed files with 557 additions and 37 deletions.
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -11,3 +11,4 @@ pom.xml*
/target/
/checkouts/
/logs/
/wiki/.git
2 changes: 1 addition & 1 deletion FUNDING.yml
@@ -1,2 +1,2 @@
github: ptaoussanis
custom: "https://www.taoensso.com/clojure/backers"
custom: "https://www.taoensso.com/clojure"
File renamed without changes.
92 changes: 56 additions & 36 deletions README.md
@@ -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="../../blob/master/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](../../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 &copy; 2014-2023 [Peter Taoussanis][], licensed under [EPL 1.0][] (same as Clojure).
Copyright &copy; 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
67 changes: 67 additions & 0 deletions 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 <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)

---
200 changes: 200 additions & 0 deletions 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
;; <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}`

0 comments on commit f8145b8

Please sign in to comment.