├── .github └── workflows │ ├── graal-tests.yml │ └── main-tests.yml ├── .gitignore ├── CHANGELOG.md ├── FUNDING.yml ├── LICENSE.txt ├── README.md ├── SECURITY.md ├── bb.edn ├── bb └── graal_tests.clj ├── doc └── cljdoc.edn ├── example-project ├── .gitignore ├── README.md ├── project.clj ├── resources │ └── public │ │ └── empty └── src │ └── example │ ├── client.cljs │ └── server.clj ├── hero.jpg ├── project.clj ├── src └── taoensso │ ├── sente.cljc │ └── sente │ ├── interfaces.cljc │ ├── packers │ └── transit.cljc │ └── server_adapters │ ├── community │ ├── aleph.clj │ ├── dogfort.cljs │ ├── express.cljs │ ├── generic_node.cljs │ ├── immutant.clj │ ├── jetty.clj │ ├── macchiato.cljs │ ├── nginx_clojure.clj │ └── undertow.clj │ └── http_kit.clj ├── test └── taoensso │ ├── graal_tests.clj │ └── sente_tests.cljc └── wiki ├── .gitignore ├── 0-Breaking-changes.md ├── 1-Getting-started.md ├── 2-Client-and-user-ids.md ├── 3-Example-projects.md ├── 3-FAQ.md ├── 4-Connection-debugging.md ├── Home.md └── README.md /.github/workflows/graal-tests.yml: -------------------------------------------------------------------------------- 1 | name: Graal tests 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | test: 6 | strategy: 7 | matrix: 8 | java: ['17'] 9 | os: [ubuntu-latest, macOS-latest, windows-latest] 10 | 11 | runs-on: ${{ matrix.os }} 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: graalvm/setup-graalvm@v1 15 | with: 16 | version: 'latest' 17 | java-version: ${{ matrix.java }} 18 | components: 'native-image' 19 | github-token: ${{ secrets.GITHUB_TOKEN }} 20 | 21 | - uses: DeLaGuardo/setup-clojure@12.5 22 | with: 23 | lein: latest 24 | bb: latest 25 | 26 | - uses: actions/cache@v4 27 | with: 28 | path: ~/.m2/repository 29 | key: deps-${{ hashFiles('deps.edn') }} 30 | restore-keys: deps- 31 | 32 | - run: bb graal-tests 33 | -------------------------------------------------------------------------------- /.github/workflows/main-tests.yml: -------------------------------------------------------------------------------- 1 | name: Main tests 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | tests: 6 | strategy: 7 | matrix: 8 | java: ['17', '18', '19'] 9 | os: [ubuntu-latest] 10 | 11 | runs-on: ${{ matrix.os }} 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: actions/setup-java@v4 15 | with: 16 | distribution: 'corretto' 17 | java-version: ${{ matrix.java }} 18 | 19 | - uses: DeLaGuardo/setup-clojure@12.5 20 | with: 21 | lein: latest 22 | 23 | - uses: actions/cache@v4 24 | id: cache-deps 25 | with: 26 | path: ~/.m2/repository 27 | key: deps-${{ hashFiles('project.clj') }} 28 | restore-keys: deps- 29 | 30 | - run: lein test-all 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pom.xml* 2 | .lein* 3 | .nrepl-port 4 | *.jar 5 | *.class 6 | *.js 7 | .env 8 | .DS_Store 9 | /lib/ 10 | /classes/ 11 | /target/ 12 | /checkouts/ 13 | /logs/ 14 | /wiki/.git 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | This project uses [**Break Versioning**](https://www.taoensso.com/break-versioning). 2 | 3 | --- 4 | 5 | # `v1.20.0` (2024-12-31) 6 | 7 | - **Dependency**: [on Clojars](https://clojars.org/com.taoensso/sente/versions/1.20.0) 8 | - **Versioning**: [Break Versioning](https://www.taoensso.com/break-versioning) 9 | 10 | This is a major **non-breaking maintenance and feature release**. As always, **please report any unexpected problems** 🙏 - [Peter Taoussanis](https://www.taoensso.com) 11 | 12 | Happy holidays everyone! 🎄🫶 13 | 14 | ## Since `v1.20.0-RC1` (2024-10-28) 15 | 16 | > No breaking changes intended 17 | 18 | * **\[fix]** [#458] Fix React Native build: catch invalid call \[4e3f16c] 19 | * **\[new]** [#447] [Community adapters] Support both Jetty 11 and 12 (@stefanroex) \[79c784d] 20 | * **\[new]** [#447] [Community adapters] Improve error message on Ajax read timeouts \[9da662c] 21 | * **\[doc]** [Community adapters] Improve constructor docstrings \[1c7a93c] 22 | 23 | ## Since `v1.19.2` (2023-08-30) 24 | 25 | > No breaking changes intended 26 | 27 | ### Changes 28 | 29 | * **\[mod]** [#440] Decrease log level of noisy ws-ping events (@jwr) \[4241e6c] 30 | * **\[mod]** Tune send backoff time \[84e8b2a] 31 | 32 | ### Fixes 33 | 34 | * **\[fix]** [#448] [#453] Fix NodeJS build: don't add `beforeunload` event listener (@theasp) \[dc6b34e] 35 | * **\[fix]** [#458] Fix React Native build: catch invalid call \[4e3f16c] 36 | * **\[fix]** [#445] [#444] [Community adapters] Undertow: remove invalid option (@danielsz) \[55167f5] 37 | 38 | ### New 39 | 40 | * **\[new]** [#447] [Community adapters] Add Jetty 11/12 adapter (@alexandergunnarson) \[8ecb2d9] 41 | * **\[doc]** [Community adapters] Improve constructor docstrings \[1c7a93c] 42 | * **\[doc]** [#439] Add guidance on large transfers \[513a42d] 43 | 44 | --- 45 | 46 | # `v1.20.0-RC1` (2024-10-28) 47 | 48 | - **Dependency**: [on Clojars](https://clojars.org/com.taoensso/sente/versions/1.20.0-RC1) 49 | - **Versioning**: [Break Versioning](https://www.taoensso.com/break-versioning) 50 | 51 | This is a major **non-breaking maintenance and feature release**. As always, **please report any unexpected problems** 🙏 - [Peter Taoussanis](https://www.taoensso.com) 52 | 53 | ## Changes since `v1.19.2` (2023-08-30) 54 | 55 | * \[mod] [#440] Decrease log level of noisy ws-ping events (@jwr) [4241e6c] 56 | * \[mod] Tune send backoff time [84e8b2a] 57 | 58 | ## Fixes since `v1.19.2` (2023-08-30) 59 | 60 | * \[fix] [#448] [#453] Don't add `beforeunload` event listener when running inside NodeJS (@theasp) [dc6b34e] 61 | * \[fix] [#445] [#444] [Community adapters] Undertow: remove invalid option (@danielsz) [55167f5] 62 | 63 | ## New since `v1.19.2` (2023-08-30) 64 | 65 | * \[new] [#447] [Community adapters] Add Jetty 11 adapter (@alexandergunnarson) [8ecb2d9] 66 | * \[doc] [#439] Add guidance on large transfers [513a42d] 67 | * Update several dependencies 68 | 69 | --- 70 | 71 | # `v1.19.2` (2023-08-30) 72 | 73 | > 📦 [Available on Clojars](https://clojars.org/com.taoensso/sente/versions/1.19.2) 74 | 75 | Identical to `v1.19.1`, but includes a hotfix (dbb798a) for [#434] to remove the unnecessary logging of potentially sensitive Ring request info when connecting to a server without a client id. 76 | 77 | This should be a safe update for users of `v1.19.x`. 78 | 79 | --- 80 | 81 | # `v1.19.1` (2023-07-18) 82 | 83 | > 📦 [Available on Clojars](https://clojars.org/com.taoensso/sente/versions/1.19.1) 84 | 85 | Identical to `v1.19.0`, but synchronizes Encore dependency with my recent library releases (Timbre, Tufte, Sente, Carmine, etc.) to prevent confusion caused by dependency conflicts. 86 | 87 | This is a safe update for users of `v1.19.0`. 88 | 89 | --- 90 | 91 | # `v1.19.0` (2023-07-13) 92 | 93 | > 📦 [Available on Clojars](https://clojars.org/com.taoensso/sente/versions/1.19.0) 94 | 95 | This is intended as a **non-breaking maintenance release**, but it touches a lot of code so **please keep an eye out** for (and let me know about) any unexpected problems - thank you! 🙏 96 | 97 | **Tip**: the [reference example](https://github.com/taoensso/sente/tree/master/example-project) includes a number of tools to help test Sente in your environment. 98 | 99 | ## Fixes since `v1.18.1` 100 | 101 | * 0dc8a12 [fix] [#431] Some disconnected user-ids not removed from `connected-uids` 102 | 103 | ## New since `v1.18.1` 104 | 105 | * e330ef2 [new] Allow WebSocket constructors to delay connection 106 | * 6021258 [new] [example] Misc improvements to example project 107 | * d0fd918 [new] Alias client option: `:ws-kalive-ping-timeout-ms` -> `:ws-ping-timeout-ms` 108 | * GraalVM compatibility is now tested during build 109 | 110 | --- 111 | 112 | # `1.18.1` (2023-07-04) 113 | 114 | > 📦 [Available on Clojars](https://clojars.org/com.taoensso/sente/versions/1.18.1) 115 | 116 | This is an important **hotfix release**, please update if you're using `v1.18.0`. 117 | 118 | ## Fixes since `v1.18.0` 119 | 120 | * ad62f1e [fix] Ajax poll not properly timing out 121 | * 1d15fe5 [fix] [#430] `[:chsk/uidport-close]` server event not firing 122 | 123 | ## New since `v1.18.0` 124 | 125 | * 5c0f4ad [new] [example] Add example server-side uidport event handlers 126 | 127 | --- 128 | 129 | # `v1.18.0` (2023-06-30) 130 | 131 | > 📦 [Available on Clojars](https://clojars.org/com.taoensso/sente/versions/1.18.0) 132 | 133 | Same as `v1.18.0-RC1`, except for: 134 | 135 | * 7889a0b [fix] [#429] Bump deps, fix possible broken cljs builds 136 | 137 | --- 138 | 139 | # Earlier releases 140 | 141 | See [here](https://github.com/taoensso/sente/releases) for earlier releases. 142 | -------------------------------------------------------------------------------- /FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: ptaoussanis 2 | custom: "https://www.taoensso.com/clojure" 3 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Eclipse Public License - v 1.0 2 | 3 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC 4 | LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM 5 | CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 6 | 7 | 1. DEFINITIONS 8 | 9 | "Contribution" means: 10 | 11 | a) in the case of the initial Contributor, the initial code and documentation 12 | distributed under this Agreement, and 13 | b) in the case of each subsequent Contributor: 14 | i) changes to the Program, and 15 | ii) additions to the Program; 16 | 17 | where such changes and/or additions to the Program originate from and are 18 | distributed by that particular Contributor. A Contribution 'originates' 19 | from a Contributor if it was added to the Program by such Contributor 20 | itself or anyone acting on such Contributor's behalf. Contributions do not 21 | include additions to the Program which: (i) are separate modules of 22 | software distributed in conjunction with the Program under their own 23 | license agreement, and (ii) are not derivative works of the Program. 24 | 25 | "Contributor" means any person or entity that distributes the Program. 26 | 27 | "Licensed Patents" mean patent claims licensable by a Contributor which are 28 | necessarily infringed by the use or sale of its Contribution alone or when 29 | combined with the Program. 30 | 31 | "Program" means the Contributions distributed in accordance with this 32 | Agreement. 33 | 34 | "Recipient" means anyone who receives the Program under this Agreement, 35 | including all Contributors. 36 | 37 | 2. GRANT OF RIGHTS 38 | a) Subject to the terms of this Agreement, each Contributor hereby grants 39 | Recipient a non-exclusive, worldwide, royalty-free copyright license to 40 | reproduce, prepare derivative works of, publicly display, publicly 41 | perform, distribute and sublicense the Contribution of such Contributor, 42 | if any, and such derivative works, in source code and object code form. 43 | b) Subject to the terms of this Agreement, each Contributor hereby grants 44 | Recipient a non-exclusive, worldwide, royalty-free patent license under 45 | Licensed Patents to make, use, sell, offer to sell, import and otherwise 46 | transfer the Contribution of such Contributor, if any, in source code and 47 | object code form. This patent license shall apply to the combination of 48 | the Contribution and the Program if, at the time the Contribution is 49 | added by the Contributor, such addition of the Contribution causes such 50 | combination to be covered by the Licensed Patents. The patent license 51 | shall not apply to any other combinations which include the Contribution. 52 | No hardware per se is licensed hereunder. 53 | c) Recipient understands that although each Contributor grants the licenses 54 | to its Contributions set forth herein, no assurances are provided by any 55 | Contributor that the Program does not infringe the patent or other 56 | intellectual property rights of any other entity. Each Contributor 57 | disclaims any liability to Recipient for claims brought by any other 58 | entity based on infringement of intellectual property rights or 59 | otherwise. As a condition to exercising the rights and licenses granted 60 | hereunder, each Recipient hereby assumes sole responsibility to secure 61 | any other intellectual property rights needed, if any. For example, if a 62 | third party patent license is required to allow Recipient to distribute 63 | the Program, it is Recipient's responsibility to acquire that license 64 | before distributing the Program. 65 | d) Each Contributor represents that to its knowledge it has sufficient 66 | copyright rights in its Contribution, if any, to grant the copyright 67 | license set forth in this Agreement. 68 | 69 | 3. REQUIREMENTS 70 | 71 | A Contributor may choose to distribute the Program in object code form under 72 | its own license agreement, provided that: 73 | 74 | a) it complies with the terms and conditions of this Agreement; and 75 | b) its license agreement: 76 | i) effectively disclaims on behalf of all Contributors all warranties 77 | and conditions, express and implied, including warranties or 78 | conditions of title and non-infringement, and implied warranties or 79 | conditions of merchantability and fitness for a particular purpose; 80 | ii) effectively excludes on behalf of all Contributors all liability for 81 | damages, including direct, indirect, special, incidental and 82 | consequential damages, such as lost profits; 83 | iii) states that any provisions which differ from this Agreement are 84 | offered by that Contributor alone and not by any other party; and 85 | iv) states that source code for the Program is available from such 86 | Contributor, and informs licensees how to obtain it in a reasonable 87 | manner on or through a medium customarily used for software exchange. 88 | 89 | When the Program is made available in source code form: 90 | 91 | a) it must be made available under this Agreement; and 92 | b) a copy of this Agreement must be included with each copy of the Program. 93 | Contributors may not remove or alter any copyright notices contained 94 | within the Program. 95 | 96 | Each Contributor must identify itself as the originator of its Contribution, 97 | if 98 | any, in a manner that reasonably allows subsequent Recipients to identify the 99 | originator of the Contribution. 100 | 101 | 4. COMMERCIAL DISTRIBUTION 102 | 103 | Commercial distributors of software may accept certain responsibilities with 104 | respect to end users, business partners and the like. While this license is 105 | intended to facilitate the commercial use of the Program, the Contributor who 106 | includes the Program in a commercial product offering should do so in a manner 107 | which does not create potential liability for other Contributors. Therefore, 108 | if a Contributor includes the Program in a commercial product offering, such 109 | Contributor ("Commercial Contributor") hereby agrees to defend and indemnify 110 | every other Contributor ("Indemnified Contributor") against any losses, 111 | damages and costs (collectively "Losses") arising from claims, lawsuits and 112 | other legal actions brought by a third party against the Indemnified 113 | Contributor to the extent caused by the acts or omissions of such Commercial 114 | Contributor in connection with its distribution of the Program in a commercial 115 | product offering. The obligations in this section do not apply to any claims 116 | or Losses relating to any actual or alleged intellectual property 117 | infringement. In order to qualify, an Indemnified Contributor must: 118 | a) promptly notify the Commercial Contributor in writing of such claim, and 119 | b) allow the Commercial Contributor to control, and cooperate with the 120 | Commercial Contributor in, the defense and any related settlement 121 | negotiations. The Indemnified Contributor may participate in any such claim at 122 | its own expense. 123 | 124 | For example, a Contributor might include the Program in a commercial product 125 | offering, Product X. That Contributor is then a Commercial Contributor. If 126 | that Commercial Contributor then makes performance claims, or offers 127 | warranties related to Product X, those performance claims and warranties are 128 | such Commercial Contributor's responsibility alone. Under this section, the 129 | Commercial Contributor would have to defend claims against the other 130 | Contributors related to those performance claims and warranties, and if a 131 | court requires any other Contributor to pay any damages as a result, the 132 | Commercial Contributor must pay those damages. 133 | 134 | 5. NO WARRANTY 135 | 136 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN 137 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR 138 | IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, 139 | NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each 140 | Recipient is solely responsible for determining the appropriateness of using 141 | and distributing the Program and assumes all risks associated with its 142 | exercise of rights under this Agreement , including but not limited to the 143 | risks and costs of program errors, compliance with applicable laws, damage to 144 | or loss of data, programs or equipment, and unavailability or interruption of 145 | operations. 146 | 147 | 6. DISCLAIMER OF LIABILITY 148 | 149 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY 150 | CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, 151 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION 152 | LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 153 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 154 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 155 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY 156 | OF SUCH DAMAGES. 157 | 158 | 7. GENERAL 159 | 160 | If any provision of this Agreement is invalid or unenforceable under 161 | applicable law, it shall not affect the validity or enforceability of the 162 | remainder of the terms of this Agreement, and without further action by the 163 | parties hereto, such provision shall be reformed to the minimum extent 164 | necessary to make such provision valid and enforceable. 165 | 166 | If Recipient institutes patent litigation against any entity (including a 167 | cross-claim or counterclaim in a lawsuit) alleging that the Program itself 168 | (excluding combinations of the Program with other software or hardware) 169 | infringes such Recipient's patent(s), then such Recipient's rights granted 170 | under Section 2(b) shall terminate as of the date such litigation is filed. 171 | 172 | All Recipient's rights under this Agreement shall terminate if it fails to 173 | comply with any of the material terms or conditions of this Agreement and does 174 | not cure such failure in a reasonable period of time after becoming aware of 175 | such noncompliance. If all Recipient's rights under this Agreement terminate, 176 | Recipient agrees to cease use and distribution of the Program as soon as 177 | reasonably practicable. However, Recipient's obligations under this Agreement 178 | and any licenses granted by Recipient relating to the Program shall continue 179 | and survive. 180 | 181 | Everyone is permitted to copy and distribute copies of this Agreement, but in 182 | order to avoid inconsistency the Agreement is copyrighted and may only be 183 | modified in the following manner. The Agreement Steward reserves the right to 184 | publish new versions (including revisions) of this Agreement from time to 185 | time. No one other than the Agreement Steward has the right to modify this 186 | Agreement. The Eclipse Foundation is the initial Agreement Steward. The 187 | Eclipse Foundation may assign the responsibility to serve as the Agreement 188 | Steward to a suitable separate entity. Each new version of the Agreement will 189 | be given a distinguishing version number. The Program (including 190 | Contributions) may always be distributed subject to the version of the 191 | Agreement under which it was received. In addition, after a new version of the 192 | Agreement is published, Contributor may elect to distribute the Program 193 | (including its Contributions) under the new version. Except as expressly 194 | stated in Sections 2(a) and 2(b) above, Recipient receives no rights or 195 | licenses to the intellectual property of any Contributor under this Agreement, 196 | whether expressly, by implication, estoppel or otherwise. All rights in the 197 | Program not expressly granted under this Agreement are reserved. 198 | 199 | This Agreement is governed by the laws of the State of New York and the 200 | intellectual property laws of the United States of America. No party to this 201 | Agreement will bring a legal action under this Agreement more than one year 202 | after the cause of action arose. Each party waives its rights to a jury trial in 203 | any resulting litigation. 204 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Taoensso open source 2 | [**API**][cljdoc docs] | [**Wiki**][GitHub wiki] | [Latest releases](#latest-releases) | [Slack channel][] 3 | 4 | # Sente 5 | 6 | ### Realtime web comms library for Clojure/Script 7 | 8 | **Sente** is a small client+server library that makes it easy to build **realtime web applications** with Clojure + ClojureScript. 9 | 10 | 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**. 11 | 12 | 13 | 14 | > **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. 15 | 16 | ## Latest release/s 17 | 18 | - `2024-12-31` `v1.20.0` (dev): [release info](../../releases/tag/v1.20.0) 19 | 20 | [![Main tests][Main tests SVG]][Main tests URL] 21 | [![Graal tests][Graal tests SVG]][Graal tests URL] 22 | 23 | See [here][GitHub releases] for earlier releases. 24 | 25 | ## Why Sente? 26 | 27 | - **Bidirectional a/sync comms** over **WebSockets** with **auto Ajax fallback** 28 | - **It just works**: auto keep-alive, buffering, protocol selection, reconnects 29 | - **Efficient design** with auto event batching for low-bandwidth use, even over Ajax 30 | - Send **arbitrary Clojure vals** over [edn](https://github.com/edn-format/edn) or [Transit](https://github.com/cognitect/transit-clj) (JSON, MessagePack, etc.) 31 | - Tiny, easy-to-use [API](../../wiki/1-Getting-started#usage) 32 | - Support for users simultaneously connected with **multiple clients** and/or devices 33 | - Realtime info on **which users are connected**, and over which protocols 34 | - Standard **Ring security model**: auth as you like, HTTPS when available, CSRF support, etc. 35 | - 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. 36 | 37 | ### Capabilities 38 | 39 | Protocol | client>server | client>server + ack/reply | server>user push 40 | ---------- | -------------- | ------------------------- | ---------------- 41 | WebSockets | ✓ (native) | ✓ (emulated) | ✓ (native) 42 | Ajax | ✓ (emulated) | ✓ (native) | ✓ (emulated) 43 | 44 | 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). 45 | 46 | ## Documentation 47 | 48 | - [Wiki][GitHub wiki] (getting started, usage, etc.) 49 | - API reference: [cljdoc][cljdoc docs], [Codox][Codox docs] 50 | 51 | ## Funding 52 | 53 | You can [help support][sponsor] continued work on this project, thank you!! 🙏 54 | 55 | ## License 56 | 57 | Copyright © 2012-2024 [Peter Taoussanis][]. 58 | Licensed under [EPL 1.0](LICENSE.txt) (same as Clojure). 59 | 60 | 61 | 62 | [GitHub releases]: ../../releases 63 | [GitHub issues]: ../../issues 64 | [GitHub wiki]: ../../wiki 65 | [Slack channel]: https://www.taoensso.com/sente/slack 66 | 67 | [Peter Taoussanis]: https://www.taoensso.com 68 | [sponsor]: https://www.taoensso.com/sponsor 69 | 70 | 71 | 72 | [Codox docs]: https://taoensso.github.io/sente/ 73 | [cljdoc docs]: https://cljdoc.org/d/com.taoensso/sente/CURRENT/api/taoensso.sente 74 | 75 | [Clojars SVG]: https://img.shields.io/clojars/v/com.taoensso/sente.svg 76 | [Clojars URL]: https://clojars.org/com.taoensso/sente 77 | 78 | [Main tests SVG]: https://github.com/taoensso/sente/actions/workflows/main-tests.yml/badge.svg 79 | [Main tests URL]: https://github.com/taoensso/sente/actions/workflows/main-tests.yml 80 | [Graal tests SVG]: https://github.com/taoensso/sente/actions/workflows/graal-tests.yml/badge.svg 81 | [Graal tests URL]: https://github.com/taoensso/sente/actions/workflows/graal-tests.yml 82 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security policy 2 | 3 | ## Advisories 4 | 5 | All security advisories will be posted [on GitHub](https://github.com/taoensso/sente/security/advisories). 6 | 7 | ## Reporting a vulnerability 8 | 9 | Please report possible security vulnerabilities [via GitHub](https://github.com/taoensso/sente/security/advisories), or by emailing me at `my first name at taoensso.com`. You may encrypt emails with [my public PGP/GPG key](https://www.taoensso.com/pgp). 10 | 11 | Thank you! 12 | 13 | \- [Peter Taoussanis](https://www.taoensso.com) 14 | -------------------------------------------------------------------------------- /bb.edn: -------------------------------------------------------------------------------- 1 | {:paths ["bb"] 2 | :tasks 3 | {:requires ([graal-tests]) 4 | graal-tests 5 | {:doc "Run Graal native-image tests" 6 | :task 7 | (do 8 | (graal-tests/uberjar) 9 | (graal-tests/native-image) 10 | (graal-tests/run-tests))}}} 11 | -------------------------------------------------------------------------------- /bb/graal_tests.clj: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bb 2 | 3 | (ns graal-tests 4 | (:require 5 | [clojure.string :as str] 6 | [babashka.fs :as fs] 7 | [babashka.process :refer [shell]])) 8 | 9 | (defn uberjar [] 10 | (let [command "lein with-profiles +graal-tests uberjar" 11 | command 12 | (if (fs/windows?) 13 | (if (fs/which "lein") 14 | command 15 | ;; Assume PowerShell powershell module 16 | (str "powershell.exe -command " (pr-str command))) 17 | command)] 18 | 19 | (shell command))) 20 | 21 | (defn executable [dir name] 22 | (-> (fs/glob dir (if (fs/windows?) (str name ".{exe,bat,cmd}") name)) 23 | first 24 | fs/canonicalize 25 | str)) 26 | 27 | (defn native-image [] 28 | (let [graalvm-home (System/getenv "GRAALVM_HOME") 29 | bin-dir (str (fs/file graalvm-home "bin"))] 30 | (shell (executable bin-dir "gu") "install" "native-image") 31 | (shell (executable bin-dir "native-image") 32 | "--features=clj_easy.graal_build_time.InitClojureClasses" 33 | "--no-fallback" "-jar" "target/graal-tests.jar" "graal_tests"))) 34 | 35 | (defn run-tests [] 36 | (let [{:keys [out]} (shell {:out :string} (executable "." "graal_tests"))] 37 | (assert (str/includes? out "loaded") out) 38 | (println "Native image works!"))) 39 | -------------------------------------------------------------------------------- /doc/cljdoc.edn: -------------------------------------------------------------------------------- 1 | {:cljdoc/docstring-format :plaintext} 2 | 3 | -------------------------------------------------------------------------------- /example-project/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /lib 3 | /classes 4 | /checkouts 5 | /logs 6 | /docs 7 | /doc 8 | *.jar 9 | *.class 10 | .lein* 11 | pom.xml* 12 | .env 13 | -------------------------------------------------------------------------------- /example-project/README.md: -------------------------------------------------------------------------------- 1 | # Official Sente reference example 2 | 3 | This example dives into Sente's full functionality pretty quickly; it's probably more useful as a reference than a tutorial. 4 | 5 | Please see Sente's [top-level README](https://github.com/taoensso/sente) for a gentler introduction to Sente. 6 | 7 | ## Instructions 8 | 9 | ### Without REPL 10 | 11 | 1. Call `lein start` at your terminal. 12 | 2. This will start a local HTTP server and auto-open a test page in your web browser. 13 | 3. Follow the instructions from that page. 14 | 15 | ### With REPL 16 | 17 | 1. Call `lein start-dev` at your terminal. 18 | 2. This will start a local [nREPL server](https://nrepl.org/nrepl/index.html) and print the server's details, e.g.: 19 | 20 | > nREPL server started on port 61332 on host 127.0.0.1 - nrepl://127.0.0.1:61332 21 | 2. This will start a local HTTP server and auto-open a test page in your web browser. 22 | 3. Follow the instructions from that page. 23 | 24 | 3. Connect your dev environment to that nREPL server, e.g. `(cider-connect)` from Emacs. 25 | 4. Open the example's [`server.clj`](https://github.com/taoensso/sente/blob/master/example-project/src/example/server.clj) file in your dev environment. 26 | 5. Eval `(example.server/start!)` to start a local HTTP server and auto-open a test page in your web browser. 27 | 6. Follow the instructions from that page. -------------------------------------------------------------------------------- /example-project/project.clj: -------------------------------------------------------------------------------- 1 | (defproject com.taoensso.examples/sente "1.19.2" 2 | :description "Sente, reference web-app example project" 3 | :url "https://github.com/ptaoussanis/sente" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html" 6 | :distribution :repo 7 | :comments "Same as Clojure"} 8 | :min-lein-version "2.3.3" 9 | :global-vars {*warn-on-reflection* true 10 | *assert* true} 11 | 12 | :dependencies 13 | [[org.clojure/clojure "1.11.1"] 14 | [org.clojure/clojurescript "1.11.60"] 15 | [org.clojure/core.async "1.6.673"] 16 | [nrepl "1.0.0"] ; Optional, for Cider 17 | 18 | [com.taoensso/sente "1.19.2"] ; <--- Sente 19 | [com.taoensso/timbre "6.2.2"] 20 | 21 | ;;; TODO Choose (uncomment) a supported web server ----------------------- 22 | [http-kit "2.7.0"] ; Default 23 | ;; [org.immutant/web "x.y.z" 24 | ;; :exclusions [ring/ring-core]] 25 | ;; [nginx-clojure/nginx-clojure-embed "x.y.z"] ; Needs v0.4.2+ 26 | ;; [aleph "x.y.z"] 27 | ;; [info.sunng/ring-jetty9-adapter "x.y.z"] 28 | ;; ----------------------------------------------------------------------- 29 | 30 | [ring "1.10.0"] 31 | [ring/ring-defaults "0.3.4"] ; Includes `ring-anti-forgery`, etc. 32 | ;; [ring-anti-forgery "x.y.z"] 33 | 34 | [compojure "1.7.0"] ; Or routing lib of your choice 35 | [hiccup "1.0.5"] ; Optional, just for HTML 36 | 37 | ;;; Transit deps optional; may be used to aid perf. of larger data payloads 38 | ;;; (see reference example for details): 39 | [com.cognitect/transit-clj "1.0.333"] 40 | [com.cognitect/transit-cljs "0.8.280"]] 41 | 42 | :plugins 43 | [[lein-pprint "1.3.2"] 44 | [lein-ancient "0.7.0"] 45 | [lein-cljsbuild "1.1.8"] 46 | [cider/cider-nrepl "0.31.0"]] ; Optional, for use with Emacs 47 | 48 | :cljsbuild 49 | {:builds 50 | [{:id :cljs-client 51 | :source-paths ["src"] 52 | :compiler {:output-to "resources/public/main.js" 53 | :optimizations :whitespace #_:advanced 54 | :pretty-print true}}]} 55 | 56 | :main example.server 57 | 58 | :clean-targets ^{:protect false} ["resources/public/main.js"] 59 | 60 | ;; Call `lein start-dev` to get a (headless) development repl that you can 61 | ;; connect to with Cider+emacs or your IDE of choice: 62 | :aliases 63 | {"start-dev" ["do" "clean," "cljsbuild" "once," "repl" ":headless"] 64 | "start" ["do" "clean," "cljsbuild" "once," "run"]} 65 | 66 | :repositories 67 | {"sonatype-oss-public" "https://oss.sonatype.org/content/groups/public/"}) 68 | -------------------------------------------------------------------------------- /example-project/resources/public/empty: -------------------------------------------------------------------------------- 1 | empty 2 | -------------------------------------------------------------------------------- /example-project/src/example/client.cljs: -------------------------------------------------------------------------------- 1 | (ns example.client 2 | "Official Sente reference example: client" 3 | {:author "Peter Taoussanis (@ptaoussanis)"} 4 | 5 | (:require 6 | [clojure.string :as str] 7 | [cljs.core.async :as async :refer [! put! chan]] 8 | [taoensso.encore :as encore :refer-macros [have have?]] 9 | [taoensso.timbre :as timbre :refer-macros []] 10 | [taoensso.sente :as sente :refer [cb-success?]] 11 | 12 | ;; Optional, for Transit encoding: 13 | [taoensso.sente.packers.transit :as sente-transit]) 14 | 15 | (:require-macros 16 | [cljs.core.async.macros :as asyncm :refer [go go-loop]])) 17 | 18 | ;;;; Logging config 19 | 20 | (defonce min-log-level_ (atom nil)) 21 | (defn- set-min-log-level! [level] 22 | (sente/set-min-log-level! level) ; Min log level for internal Sente namespaces 23 | (timbre/set-ns-min-level! level) ; Min log level for this namespace 24 | (reset! min-log-level_ level)) 25 | 26 | (when-let [el (.getElementById js/document "sente-min-log-level")] 27 | (let [level (if-let [attr (.getAttribute el "data-level")] 28 | (keyword attr) 29 | :warn)] 30 | (set-min-log-level! level))) 31 | 32 | ;;;; Util for logging output to on-screen console 33 | 34 | (let [output-el (.getElementById js/document "output")] 35 | (defn- ->output!! [x] 36 | (aset output-el "value" (str (.-value output-el) x)) 37 | (aset output-el "scrollTop" (.-scrollHeight output-el)))) 38 | 39 | (defn ->output! 40 | ([ ] (->output!! "\n")) 41 | ([fmt & args] 42 | (let [msg (apply encore/format fmt args)] 43 | (->output!! (str "\n• " msg))))) 44 | 45 | (->output! "ClojureScript has successfully loaded") 46 | (->output! "Sente version: %s" sente/sente-version) 47 | (->output! "Min log level: %s (use toggle button to change)" @min-log-level_) 48 | (->output!) 49 | 50 | ;;;; Define our Sente channel socket (chsk) client 51 | 52 | (def ?csrf-token 53 | (when-let [el (.getElementById js/document "sente-csrf-token")] 54 | (.getAttribute el "data-token"))) 55 | 56 | (if ?csrf-token 57 | (->output! "CSRF token detected in HTML, great!") 58 | (->output! "**IMPORTANT** CSRF token NOT detected in HTML, default Sente config will reject requests!")) 59 | 60 | (def chsk-type 61 | "We'll select a random protocol for this example" 62 | (if (>= (rand) 0.5) :ajax :auto)) 63 | 64 | (->output! "Randomly selected chsk type: %s" chsk-type) 65 | (->output!) 66 | 67 | (let [;; Serializtion format, must use same val for client + server: 68 | packer :edn ; Default packer, a good choice in most cases 69 | ;; (sente-transit/get-transit-packer) ; Needs Transit dep 70 | 71 | {:keys [chsk ch-recv send-fn state]} 72 | (sente/make-channel-socket-client! 73 | "/chsk" ; Must match server Ring routing URL 74 | ?csrf-token 75 | {:type chsk-type 76 | :packer packer})] 77 | 78 | (def chsk chsk) 79 | (def ch-chsk ch-recv) ; ChannelSocket's receive channel 80 | (def chsk-send! send-fn) ; ChannelSocket's send API fn 81 | (def chsk-state state) ; Watchable, read-only atom 82 | ) 83 | 84 | ;;;; Sente event handlers 85 | 86 | (defmulti -event-msg-handler 87 | "Multimethod to handle Sente `event-msg`s" 88 | :id ; Dispatch on event-id 89 | ) 90 | 91 | (defn event-msg-handler 92 | "Wraps `-event-msg-handler` with logging, error catching, etc." 93 | [{:as ev-msg :keys [id ?data event]}] 94 | (-event-msg-handler ev-msg)) 95 | 96 | (defmethod -event-msg-handler 97 | :default ; Default/fallback case (no other matching handler) 98 | [{:as ev-msg :keys [event]}] 99 | (->output! "Unhandled event: %s" event)) 100 | 101 | (defmethod -event-msg-handler :chsk/state 102 | [{:as ev-msg :keys [?data]}] 103 | (let [[old-state-map new-state-map] (have vector? ?data)] 104 | (cond 105 | ;; Tip: look for {:keys [opened? closed? first-open?]} in `new-state-map` to 106 | ;; easily identify these commonly useful state transitions 107 | (:first-open? new-state-map) (->output! "Channel socket FIRST OPENED: %s" new-state-map) 108 | (:opened? new-state-map) (->output! "Channel socket OPENED: %s" new-state-map) 109 | (:closed? new-state-map) (->output! "Channel socket CLOSED: %s" new-state-map) 110 | :else (->output! "Channel socket state changed: %s" new-state-map)))) 111 | 112 | (defmethod -event-msg-handler :chsk/recv 113 | [{:as ev-msg :keys [?data]}] 114 | (->output! "Push event from server: %s" ?data)) 115 | 116 | (defmethod -event-msg-handler :chsk/handshake 117 | [{:as ev-msg :keys [?data]}] 118 | (let [[?uid _ ?handshake-data first-handshake?] ?data] 119 | (->output! "Handshake: %s" ?data))) 120 | 121 | ;; TODO Add your (defmethod -event-msg-handler [ev-msg] )s here... 122 | 123 | ;;;; Sente event router (our `event-msg-handler` loop) 124 | 125 | (defonce router_ (atom nil)) 126 | (defn stop-router! [] (when-let [stop-f @router_] (stop-f))) 127 | (defn start-router! [] 128 | (stop-router!) 129 | (reset! router_ 130 | (sente/start-client-chsk-router! 131 | ch-chsk event-msg-handler))) 132 | 133 | ;;;; UI events 134 | 135 | (when-let [target-el (.getElementById js/document "btn-send-with-reply")] 136 | (.addEventListener target-el "click" 137 | (fn [ev] 138 | (chsk-send! [:example/button2 {:had-a-callback? "indeed"}] 5000 139 | (fn [cb-reply] 140 | (->output! "Callback reply: %s" cb-reply)))))) 141 | 142 | (when-let [target-el (.getElementById js/document "btn-send-wo-reply")] 143 | (.addEventListener target-el "click" 144 | (fn [ev] 145 | (chsk-send! [:example/button1 {:had-a-callback? "nope"}])))) 146 | 147 | (when-let [target-el (.getElementById js/document "btn-test-broadcast")] 148 | (.addEventListener target-el "click" 149 | (fn [ev] 150 | (->output!) 151 | (chsk-send! [:example/test-broadcast])))) 152 | 153 | (when-let [target-el (.getElementById js/document "btn-toggle-broadcast-loop")] 154 | (.addEventListener target-el "click" 155 | (fn [ev] 156 | (chsk-send! [:example/toggle-broadcast-loop] 5000 157 | (fn [cb-reply] 158 | (when (cb-success? cb-reply) 159 | (let [enabled? cb-reply] 160 | (if enabled? 161 | (->output! "Server broadcast loop now ENABLED") 162 | (->output! "Server broadcast loop now DISABLED"))))))))) 163 | 164 | (when-let [target-el (.getElementById js/document "btn-disconnect")] 165 | (.addEventListener target-el "click" 166 | (fn [ev] 167 | (->output!) 168 | (sente/chsk-disconnect! chsk)))) 169 | 170 | (when-let [target-el (.getElementById js/document "btn-reconnect")] 171 | (.addEventListener target-el "click" 172 | (fn [ev] 173 | (->output!) 174 | (sente/chsk-reconnect! chsk)))) 175 | 176 | (when-let [target-el (.getElementById js/document "btn-break-with-close")] 177 | (.addEventListener target-el "click" 178 | (fn [ev] 179 | (->output!) 180 | (sente/chsk-break-connection! chsk {:close-ws? true})))) 181 | 182 | (when-let [target-el (.getElementById js/document "btn-break-wo-close")] 183 | (.addEventListener target-el "click" 184 | (fn [ev] 185 | (->output!) 186 | (sente/chsk-break-connection! chsk {:close-ws? false})))) 187 | 188 | (when-let [target-el (.getElementById js/document "btn-toggle-logging")] 189 | (.addEventListener target-el "click" 190 | (fn [ev] 191 | (chsk-send! [:example/toggle-min-log-level] 5000 192 | (fn [cb-reply] 193 | (if (cb-success? cb-reply) 194 | (let [level cb-reply] 195 | (set-min-log-level! level) 196 | (->output! "New minimum log level (client+server): %s" level)) 197 | (->output! "Request failed: %s" cb-reply))))))) 198 | 199 | (when-let [target-el (.getElementById js/document "btn-toggle-bad-conn-rate")] 200 | (.addEventListener target-el "click" 201 | (fn [ev] 202 | (chsk-send! [:example/toggle-bad-conn-rate] 5000 203 | (fn [cb-reply] 204 | (if (cb-success? cb-reply) 205 | (->output! "New rate: %s" cb-reply) 206 | (->output! "Request failed: %s" cb-reply))))))) 207 | 208 | (when-let [target-el (.getElementById js/document "btn-connected-uids")] 209 | (.addEventListener target-el "click" 210 | (fn [ev] 211 | (chsk-send! [:example/connected-uids] 5000 212 | (fn [cb-reply] 213 | (when (cb-success? cb-reply) 214 | (->output! "Connected uids: %s" cb-reply))))))) 215 | 216 | (when-let [target-el (.getElementById js/document "btn-login")] 217 | (.addEventListener target-el "click" 218 | (fn [ev] 219 | (let [user-id (.-value (.getElementById js/document "input-login"))] 220 | (if (str/blank? user-id) 221 | (js/alert "Please enter a user-id first") 222 | (do 223 | (->output!) 224 | (->output! "Logging in with user-id %s..." user-id) 225 | 226 | ;;; Use any login procedure you'd like. Here we'll trigger an Ajax 227 | ;;; POST request that resets our server-side session. Then we ask 228 | ;;; our channel socket to reconnect, thereby picking up the new 229 | ;;; session. 230 | 231 | (sente/ajax-lite "/login" 232 | {:method :post 233 | :headers {:X-CSRF-Token (:csrf-token @chsk-state)} 234 | :params {:user-id (str user-id)}} 235 | 236 | (fn [ajax-resp] 237 | (->output! "Ajax login response: %s" ajax-resp) 238 | (let [login-successful? true ; Your logic here 239 | ] 240 | (if-not login-successful? 241 | (->output! "Login failed") 242 | (do 243 | (->output! "Login successful") 244 | (sente/chsk-reconnect! chsk)))))))))))) 245 | 246 | (when-let [target-el (.getElementById js/document "btn-repeated-logins")] 247 | (.addEventListener target-el "click" 248 | (fn [ev] 249 | (->output!) 250 | (->output! "Will rapidly change user-id from \"1\" to \"10\"...") 251 | (let [c (async/chan)] 252 | (go-loop [uids (range 11)] 253 | (when-let [[next-uid] uids] 254 | (sente/ajax-lite "/login" 255 | {:method :post 256 | :headers {:X-CSRF-Token (:csrf-token @chsk-state)} 257 | :params {:user-id (str next-uid)}} 258 | (fn [ajax-resp] 259 | (when (:success? ajax-resp) (sente/chsk-reconnect! chsk)) 260 | (put! c :continue))) 261 | (! >!! put! chan go go-loop]] 13 | [taoensso.encore :as encore :refer [have have?]] 14 | [taoensso.timbre :as timbre] 15 | [taoensso.sente :as sente] 16 | 17 | ;;; TODO Choose (uncomment) a supported web server + adapter ------------- 18 | [org.httpkit.server :as http-kit] 19 | [taoensso.sente.server-adapters.http-kit :refer [get-sch-adapter]] 20 | 21 | ;; [immutant.web :as immutant] 22 | ;; [taoensso.sente.server-adapters.immutant :refer [get-sch-adapter]] 23 | 24 | ;; [nginx.clojure.embed :as nginx-clojure] 25 | ;; [taoensso.sente.server-adapters.nginx-clojure :refer [get-sch-adapter]] 26 | 27 | ;; [aleph.http :as aleph] 28 | ;; [taoensso.sente.server-adapters.aleph :refer [get-sch-adapter]] 29 | 30 | ;; [ring.adapter.jetty9.websocket :as jetty9.websocket] 31 | ;; [taoensso.sente.server-adapters.jetty9 :refer [get-sch-adapter]] 32 | ;; 33 | ;; See https://gist.github.com/wavejumper/40c4cbb21d67e4415e20685710b68ea0 34 | ;; for full example using Jetty 9 35 | 36 | ;; ----------------------------------------------------------------------- 37 | 38 | ;; Optional, for Transit encoding: 39 | [taoensso.sente.packers.transit :as sente-transit])) 40 | 41 | ;;;; Logging config 42 | 43 | (defonce min-log-level_ (atom nil)) 44 | (defn- set-min-log-level! [level] 45 | (sente/set-min-log-level! level) ; Min log level for internal Sente namespaces 46 | (timbre/set-ns-min-level! level) ; Min log level for this namespace 47 | (reset! min-log-level_ level)) 48 | 49 | (set-min-log-level! #_:trace :debug #_:info #_:warn) 50 | 51 | ;;;; Define our Sente channel socket (chsk) server 52 | 53 | (let [;; Serialization format, must use same val for client + server: 54 | packer :edn ; Default packer, a good choice in most cases 55 | ;; (sente-transit/get-transit-packer) ; Needs Transit dep 56 | ] 57 | 58 | (defonce chsk-server 59 | (sente/make-channel-socket-server! 60 | (get-sch-adapter) {:packer packer}))) 61 | 62 | (let [{:keys [ch-recv send-fn connected-uids_ private 63 | ajax-post-fn ajax-get-or-ws-handshake-fn]} 64 | chsk-server] 65 | 66 | (defonce ring-ajax-post ajax-post-fn) 67 | (defonce ring-ajax-get-or-ws-handshake ajax-get-or-ws-handshake-fn) 68 | (defonce ch-chsk ch-recv) ; ChannelSocket's receive channel 69 | (defonce chsk-send! send-fn) ; ChannelSocket's send API fn 70 | (defonce connected-uids_ connected-uids_) ; Watchable, read-only atom 71 | (defonce conns_ (:conns_ private)) ; Implementation detail, for debugging! 72 | ) 73 | 74 | ;; We can watch this atom for changes 75 | (add-watch connected-uids_ :connected-uids 76 | (fn [_ _ old new] 77 | (when (not= old new) 78 | (timbre/infof "Connected uids change: %s" new)))) 79 | 80 | ;;;; Ring handlers 81 | 82 | (defn landing-pg-handler [ring-req] 83 | (hiccup/html 84 | (let [csrf-token 85 | ;; (:anti-forgery-token ring-req) ; Also an option 86 | (force anti-forgery/*anti-forgery-token*)] 87 | [:div#sente-csrf-token {:data-token csrf-token}]) 88 | 89 | ;; Convey server's min-log-level to client 90 | [:div#sente-min-log-level {:data-level (name @min-log-level_)}] 91 | 92 | [:h3 "Sente reference example"] 93 | [:p 94 | "A " [:i "random"] " " [:strong [:code ":ajax/:auto"]] 95 | " connection mode has been selected (see " [:strong "client output"] ")." 96 | [:br] 97 | "To " [:strong "re-randomize"] ", hit your browser's reload/refresh button."] 98 | [:ul 99 | [:li [:strong "Server output:"] " → " [:code "*std-out*"]] 100 | [:li [:strong "Client output:"] " → Below textarea and/or browser console"]] 101 | [:textarea#output {:style "width: 100%; height: 200px;" :wrap "off"}] 102 | 103 | [:section 104 | [:h4 "Standard Controls"] 105 | [:p 106 | [:button#btn-send-with-reply {:type "button"} "chsk-send! (with reply)"] " " 107 | [:button#btn-send-wo-reply {:type "button"} "chsk-send! (without reply)"] " "] 108 | [:p 109 | [:button#btn-test-broadcast {:type "button"} "Test broadcast (server>user async push)"] " " 110 | [:button#btn-toggle-broadcast-loop {:type "button"} "Toggle broadcast loop"]] 111 | [:p 112 | [:button#btn-disconnect {:type "button"} "Disconnect"] " " 113 | [:button#btn-reconnect {:type "button"} "Reconnect"]] 114 | [:p 115 | [:button#btn-login {:type "button"} "Log in with user-id →"] " " 116 | [:input#input-login {:type :text :placeholder "user-id"}]] 117 | [:ul {:style "color: #808080; font-size: 0.9em;"} 118 | [:li "Log in with a " [:a {:href "https://github.com/ptaoussanis/sente/wiki/Client-and-user-ids#user-ids" :target :_blank} "user-id"] 119 | " so that the server can directly address that user's connected clients."] 120 | [:li "Open this page with " [:strong "multiple browser windows"] " to simulate multiple clients."] 121 | [:li "Use different browsers and/or " [:strong "Private Browsing / Incognito mode"] " to simulate multiple users."]]] 122 | 123 | [:hr] 124 | 125 | [:section 126 | [:h4 "Debug and Testing Controls"] 127 | [:p 128 | [:button#btn-toggle-logging {:type "button"} "Toggle minimum log level"] " " 129 | [:button#btn-toggle-bad-conn-rate {:type "button"} "Toggle simulated bad conn rate"]] 130 | [:p 131 | [:button#btn-break-with-close {:type "button"} "Simulate broken conn (with on-close)"] " " 132 | [:button#btn-break-wo-close {:type "button"} "Simulate broken conn (w/o on-close)"]] 133 | [:p 134 | [:button#btn-repeated-logins {:type "button"} "Test repeated logins"] " " 135 | [:button#btn-connected-uids {:type "button"} "Print connected uids"]]] 136 | 137 | [:script {:src "main.js"}] ; Include our cljs target 138 | )) 139 | 140 | (defn login-handler 141 | "Here's where you'll add your server-side login/auth procedure (Friend, etc.). 142 | In our simplified example we'll just always successfully authenticate the user 143 | with whatever user-id they provided in the auth request." 144 | [ring-req] 145 | (let [{:keys [session params]} ring-req 146 | {:keys [user-id]} params] 147 | (timbre/debugf "Login request: %s" params) 148 | {:status 200 :session (assoc session :uid user-id)})) 149 | 150 | (defroutes ring-routes 151 | (GET "/" ring-req (landing-pg-handler ring-req)) 152 | (GET "/chsk" ring-req (ring-ajax-get-or-ws-handshake ring-req)) 153 | (POST "/chsk" ring-req (ring-ajax-post ring-req)) 154 | (POST "/login" ring-req (login-handler ring-req)) 155 | (route/resources "/") ; Static files, notably public/main.js (our cljs target) 156 | (route/not-found "

Page not found

")) 157 | 158 | (def main-ring-handler 159 | "**NB**: Sente requires the Ring `wrap-params` + `wrap-keyword-params` 160 | middleware to work. These are included with 161 | `ring.middleware.defaults/wrap-defaults` - but you'll need to ensure 162 | that they're included yourself if you're not using `wrap-defaults`. 163 | 164 | You're also STRONGLY recommended to use `ring.middleware.anti-forgery` 165 | or something similar." 166 | (ring.middleware.defaults/wrap-defaults 167 | ring-routes ring.middleware.defaults/site-defaults)) 168 | 169 | ;;;; Some server>user async push examples 170 | 171 | (defn broadcast! 172 | "Pushes given event to all connected users." 173 | [event] 174 | (let [all-uids (:any @connected-uids_)] 175 | (doseq [uid all-uids] 176 | (timbre/debugf "Broadcasting server>user to %s uids" (count all-uids)) 177 | (chsk-send! uid event)))) 178 | 179 | (defn test-broadcast! 180 | "Quickly broadcasts 100 events to all connected users. 181 | Note that this'll be fast+reliable even over Ajax!" 182 | [] 183 | (doseq [uid (:any @connected-uids_)] 184 | (doseq [i (range 100)] 185 | (chsk-send! uid [:example/broadcast (str {:i i, :uid uid})])))) 186 | 187 | (comment (test-broadcast!)) 188 | 189 | (defonce broadcast-loop?_ (atom true)) 190 | (defonce ^:private auto-loop_ 191 | (delay 192 | (go-loop [i 0] 193 | ( [ev-msg] )s here... 287 | 288 | ;;;; Sente event router (our `event-msg-handler` loop) 289 | 290 | (defonce router_ (atom nil)) 291 | (defn stop-router! [] (when-let [stop-fn @router_] (stop-fn))) 292 | (defn start-router! [] 293 | (stop-router!) 294 | (reset! router_ 295 | (sente/start-server-chsk-router! 296 | ch-chsk event-msg-handler))) 297 | 298 | ;;;; Init stuff 299 | 300 | (defonce web-server_ (atom nil)) ; (fn stop []) 301 | (defn stop-web-server! [] (when-let [stop-fn @web-server_] (stop-fn))) 302 | (defn start-web-server! [& [port]] 303 | (stop-web-server!) 304 | (let [port (or port 0) ; 0 => Choose any available port 305 | ring-handler (var main-ring-handler) 306 | 307 | [port stop-fn] 308 | ;;; TODO Choose (uncomment) a supported web server ------------------ 309 | (let [stop-fn (http-kit/run-server ring-handler {:port port})] 310 | [(:local-port (meta stop-fn)) (fn stop-fn [] (stop-fn :timeout 100))]) 311 | ;; 312 | ;; (let [server (immutant/run ring-handler :port port)] 313 | ;; [(:port server) (fn stop-fn [] (immutant/stop server))]) 314 | ;; 315 | ;; (let [port (nginx-clojure/run-server ring-handler {:port port})] 316 | ;; [port (fn stop-fn [] (nginx-clojure/stop-server))]) 317 | ;; 318 | ;; (let [server (aleph/start-server ring-handler {:port port}) 319 | ;; p (promise)] 320 | ;; (future @p) ; Workaround for Ref. https://goo.gl/kLvced 321 | ;; ;; (aleph.netty/wait-for-close server) 322 | ;; [(aleph.netty/port server) 323 | ;; (fn stop-fn [] (.close ^java.io.Closeable server) (deliver p nil))]) 324 | ;; ------------------------------------------------------------------ 325 | 326 | uri (format "http://localhost:%s/" port)] 327 | 328 | (timbre/infof "HTTP server is running at `%s`" uri) 329 | (try 330 | (.browse (java.awt.Desktop/getDesktop) (java.net.URI. uri)) 331 | (catch Exception _)) 332 | 333 | (reset! web-server_ stop-fn))) 334 | 335 | (defn stop! [] (stop-router!) (stop-web-server!)) 336 | (defn start! [] 337 | (timbre/reportf "Sente version: %s" sente/sente-version) 338 | (timbre/reportf "Min log level: %s" @min-log-level_) 339 | (start-router!) 340 | (let [stop-fn (start-web-server!)] 341 | @auto-loop_ 342 | stop-fn)) 343 | 344 | (defn -main "For `lein run`, etc." [] (start!)) 345 | 346 | (comment 347 | (start!) ; Eval this at REPL to start server via REPL 348 | (test-broadcast!) 349 | 350 | (broadcast! [:example/foo]) 351 | @connected-uids_ 352 | @conns_) 353 | -------------------------------------------------------------------------------- /hero.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taoensso/sente/63b2044ce0a6982f50582080599a604d1ae70946/hero.jpg -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject com.taoensso/sente "1.20.0" 2 | :author "Peter Taoussanis " 3 | :description "Realtime web comms library for Clojure/Script" 4 | :url "https://github.com/taoensso/sente" 5 | 6 | :license 7 | {:name "Eclipse Public License - v 1.0" 8 | :url "https://www.eclipse.org/legal/epl-v10.html"} 9 | 10 | :dependencies 11 | [[org.clojure/core.async "1.7.701"] 12 | [com.taoensso/encore "3.133.0"] 13 | [org.java-websocket/Java-WebSocket "1.6.0"] 14 | [org.clojure/tools.reader "1.5.0"] 15 | [com.taoensso/timbre "6.6.1"]] 16 | 17 | :test-paths ["test" #_"src"] 18 | 19 | :profiles 20 | {;; :default [:base :system :user :provided :dev] 21 | :provided {:dependencies [[org.clojure/clojurescript "1.11.132"] 22 | [org.clojure/clojure "1.11.1"]]} 23 | :c1.12 {:dependencies [[org.clojure/clojure "1.12.0"]]} 24 | :c1.11 {:dependencies [[org.clojure/clojure "1.11.1"]]} 25 | :c1.10 {:dependencies [[org.clojure/clojure "1.10.2"]]} 26 | 27 | :graal-tests 28 | {:source-paths ["test"] 29 | :main taoensso.graal-tests 30 | :aot [taoensso.graal-tests] 31 | :uberjar-name "graal-tests.jar" 32 | :dependencies 33 | [[org.clojure/clojure "1.11.1"] 34 | [com.github.clj-easy/graal-build-time "1.0.5"]]} 35 | 36 | :community 37 | {:dependencies 38 | [[org.immutant/web "2.1.10"] 39 | [nginx-clojure "0.6.0"] 40 | [aleph "0.8.2"] 41 | [macchiato/core "0.2.23"] ; 0.2.24 seems to fail? 42 | [luminus/ring-undertow-adapter "1.4.0"] 43 | [info.sunng/ring-jetty9-adapter "0.36.1"] 44 | [ring/ring-core "1.13.0"] 45 | [ring/ring-jetty-adapter "1.13.0"] 46 | [org.ring-clojure/ring-websocket-protocols "1.13.0"]] 47 | 48 | ;; For nginx-clojure on Java 17+, 49 | ;; Ref. https://github.com/nginx-clojure/nginx-clojure/issues/273 50 | :jvm-opts 51 | ["--add-opens=java.base/java.lang=ALL-UNNAMED" 52 | "--add-opens=java.base/sun.nio.cs=ALL-UNNAMED" 53 | "--add-opens=java.base/sun.nio.ch=ALL-UNNAMED"]} 54 | 55 | :dev [:dev+ :community] 56 | :dev+ 57 | {:jvm-opts ["-server" "-Dtaoensso.elide-deprecated=true"] 58 | :global-vars 59 | {*warn-on-reflection* true 60 | *assert* true 61 | *unchecked-math* false #_:warn-on-boxed} 62 | 63 | :dependencies 64 | [[com.cognitect/transit-clj "1.0.333"] 65 | [com.cognitect/transit-cljs "0.8.280"] 66 | [org.clojure/test.check "1.1.1"] 67 | [http-kit "2.8.0"]] 68 | 69 | :plugins 70 | [[lein-pprint "1.3.2"] 71 | [lein-ancient "0.7.0"] 72 | [lein-cljsbuild "1.1.8"] 73 | [com.taoensso.forks/lein-codox "0.10.11"]] 74 | 75 | :codox 76 | {:language #{:clojure :clojurescript} 77 | :base-language :clojure}}} 78 | 79 | :cljsbuild 80 | {:test-commands {"node" ["node" "target/test.js"]} 81 | :builds 82 | [{:id :main 83 | :source-paths ["src" "test"] 84 | :compiler {:output-to "target/main.js" 85 | :optimizations :advanced 86 | :pretty-print false}} 87 | 88 | {:id :test 89 | :source-paths [#_"src" "test"] 90 | :compiler 91 | {:output-to "target/test.js" 92 | :target :nodejs 93 | :optimizations :simple}}]} 94 | 95 | :aliases 96 | {"start-dev" ["with-profile" "+dev" "repl" ":headless"] 97 | "build-once" ["do" ["clean"] ["cljsbuild" "once"]] 98 | "deploy-lib" ["do" ["build-once"] ["deploy" "clojars"] ["install"]] 99 | 100 | "test-clj" ["with-profile" "+c1.12:+c1.11:+c1.10" "test"] 101 | "test-cljs" ["with-profile" "+c1.12" "cljsbuild" "test"] 102 | "test-all" ["do" ["clean"] ["test-clj"] ["test-cljs"]]}) 103 | -------------------------------------------------------------------------------- /src/taoensso/sente/interfaces.cljc: -------------------------------------------------------------------------------- 1 | (ns taoensso.sente.interfaces 2 | "Alpha, subject to change. 3 | Public interfaces / extension points. 4 | Ref. https://github.com/ptaoussanis/sente/issues/425 for more info." 5 | (:require [taoensso.encore :as enc])) 6 | 7 | ;;;; Web servers 8 | 9 | (defprotocol IServerChanAdapter ; sch-adapter 10 | "For Sente to support a web server, an \"adapter\" for that server 11 | must be provided that implements this protocol." 12 | 13 | (ring-req->server-ch-resp [sch-adapter ring-req callbacks-map] 14 | "Given a Ring request (WebSocket GET handshake or Ajax GET/POST), 15 | returns a Ring response map appropriate for the underlying web server. 16 | 17 | `callbacks-map` contains the following functions that MUST be called as described: 18 | 19 | `:on-open` - (fn [sch websocket?]) 20 | Call exactly once after `sch` is available for sending. 21 | 22 | `:on-close` - (fn [sch websocket? status]) 23 | Call exactly once after `sch` is closed for any cause, incl. an 24 | explicit call to `sch-close!`. `status` arg type is currently undefined. 25 | 26 | `:on-msg` - (fn [sch websocket? msg]) 27 | Call for each `String` or byte[] message received from client. 28 | 29 | `:on-error` - (fn [sch websocket? error]) 30 | Currently unused. 31 | 32 | Note: all `sch` (\"server channel\") args provided above MUST implement 33 | the `IServerChan` protocol. 34 | 35 | `callbacks-map` contains the following functions IFF server is configured to 36 | use 3-arity (async) Ring v1.6+ handlers: 37 | 38 | `:ring-async-resp-fn` - ?(fn [ring-response]) 39 | `:ring-async-raise-fn` - ?(fn [throwable])")) 40 | 41 | (defprotocol IServerChan ; sch 42 | "This protocol must be implemented by the \"server channel\" arguments 43 | provided to callback functions via `ring-req->server-ch-resp`." 44 | 45 | (sch-open? [sch] "Returns true iff the channel is currently open.") 46 | (sch-close! [sch] 47 | "If the channel is open when called: closes the channel and returns true. 48 | Otherwise noops and returns falsey.") 49 | (sch-send! [sch websocket? msg] 50 | "If the channel is open when called: sends a message over channel and 51 | returns true. Otherwise noops and returns falsey.")) 52 | 53 | ;;;; Packers 54 | 55 | (defprotocol IPacker 56 | "Extension pt. for client<->server comms data un/packers: 57 | arbitrary Clojure data <-> serialized payloads. 58 | 59 | NB if dealing with non-string payloads, see also 60 | `taoensso.sente/*write-legacy-pack-format?*`." 61 | 62 | (pack [_ x]) 63 | (unpack [_ x])) 64 | -------------------------------------------------------------------------------- /src/taoensso/sente/packers/transit.cljc: -------------------------------------------------------------------------------- 1 | (ns taoensso.sente.packers.transit 2 | "Alpha - subject to change! 3 | Optional Transit-format[1] IPacker implementation for use with Sente. 4 | [1] https://github.com/cognitect/transit-format." 5 | {:author "Peter Taoussanis, @ckarlsen84"} 6 | (:require 7 | [clojure.string :as str] 8 | [taoensso.encore :as enc :refer [have have! have?]] 9 | [taoensso.timbre :as timbre] 10 | [cognitect.transit :as transit] 11 | [taoensso.sente.interfaces :as interfaces 12 | :refer [pack unpack]]) 13 | 14 | #?(:clj (:import [java.io ByteArrayInputStream ByteArrayOutputStream]))) 15 | 16 | #?(:clj 17 | (defn- get-charset ^String [transit-fmt] 18 | ;; :msgpack appears to need ISO-8859-1 to retain binary data correctly when 19 | ;; string-encoded, all other (non-binary) formats can get UTF-8: 20 | (if (enc/identical-kw? transit-fmt :msgpack) "ISO-8859-1" "UTF-8"))) 21 | 22 | #?(:clj 23 | (def ^:private cache-read-handlers 24 | "reader-opts -> reader-opts with cached read handler map" 25 | (let [cache (enc/fmemoize (fn [m] (transit/read-handler-map m)))] 26 | (fn [reader-opts] 27 | (if-let [m (:handlers reader-opts)] 28 | (assoc reader-opts :handlers (cache m)) 29 | reader-opts))))) 30 | 31 | #?(:clj 32 | (def ^:private cache-write-handlers 33 | "writer-opts -> writer-opts with cached write handler map" 34 | (let [cache (enc/fmemoize (fn [m] (transit/write-handler-map m)))] 35 | (fn [writer-opts] 36 | (if-let [m (:handlers writer-opts)] 37 | (assoc writer-opts :handlers (cache m)) 38 | writer-opts))))) 39 | 40 | #?(:clj 41 | (def ^:private transit-writer-fn-proxy 42 | (enc/thread-local-proxy 43 | (fn [fmt opts] 44 | (let [charset (get-charset fmt) 45 | opts (cache-write-handlers opts) 46 | baos (ByteArrayOutputStream. 64) 47 | writer (transit/writer baos fmt opts)] 48 | (fn [x] 49 | (transit/write writer x) 50 | (let [result (.toString baos charset)] 51 | (.reset baos) 52 | result))))))) 53 | 54 | (def ^:private get-transit-writer-fn 55 | "Returns thread-safe (fn [x-to-write])" 56 | #?(:cljs 57 | (enc/fmemoize 58 | (fn [fmt opts] 59 | (let [writer (transit/writer fmt opts)] 60 | (fn [x] (transit/write writer x))))) 61 | :clj 62 | (fn [fmt opts] 63 | (let [thread-local-transit-writer-fn (.get ^ThreadLocal transit-writer-fn-proxy)] 64 | (thread-local-transit-writer-fn fmt opts))))) 65 | 66 | (def ^:private get-transit-reader-fn 67 | "Returns thread-safe (fn [str-to-read])" 68 | #?(:cljs 69 | (enc/fmemoize 70 | (fn [fmt opts] 71 | (let [reader (transit/reader fmt opts)] 72 | (fn [s] (transit/read reader s))))) 73 | :clj 74 | (fn [fmt opts] 75 | (let [charset (get-charset fmt) 76 | opts (cache-read-handlers opts)] 77 | (fn [^String s] 78 | (let [ba (.getBytes s charset) 79 | bais (ByteArrayInputStream. ba) 80 | reader (transit/reader bais fmt opts)] 81 | (transit/read reader))))))) 82 | 83 | (deftype TransitPacker [transit-fmt writer-opts reader-opts] 84 | taoensso.sente.interfaces/IPacker 85 | (pack [_ x] ((get-transit-writer-fn transit-fmt writer-opts) x)) 86 | (unpack [_ s] ((get-transit-reader-fn transit-fmt reader-opts) s))) 87 | 88 | (defn get-transit-packer "Returns a new TransitPacker" 89 | ([ ] (get-transit-packer :json {} {})) 90 | ([transit-fmt] (get-transit-packer transit-fmt {} {})) 91 | ([transit-fmt writer-opts reader-opts] 92 | ;; No transit-cljs support for msgpack atm 93 | (have? [:el #{:json :json-verbose #_:msgpack}] transit-fmt) 94 | (have? map? writer-opts reader-opts) 95 | (TransitPacker. transit-fmt writer-opts reader-opts))) 96 | 97 | (comment 98 | (def tp (get-transit-packer)) 99 | (enc/qb 1e4 ; [139.36 71.1] 100 | (do (unpack tp (pack tp [:chsk/ws-ping "foo"]))) 101 | (enc/read-edn (enc/pr-edn [:chsk/ws-ping "foo"])))) 102 | -------------------------------------------------------------------------------- /src/taoensso/sente/server_adapters/community/aleph.clj: -------------------------------------------------------------------------------- 1 | (ns taoensso.sente.server-adapters.community.aleph 2 | {:author "Soren Macbeth (@sorenmacbeth)"} 3 | (:require 4 | [taoensso.sente.interfaces :as i] 5 | [clojure.string :as str] 6 | [aleph.http :as aleph] 7 | [manifold.stream :as s] 8 | [manifold.deferred :as d])) 9 | 10 | (extend-type manifold.stream.core.IEventSink 11 | i/IServerChan 12 | (sch-open? [sch] (not (s/closed? sch))) 13 | (sch-close! [sch] (s/close! sch)) 14 | (sch-send! [sch websocket? msg] 15 | (if (s/closed? sch) 16 | false 17 | (let [close-after-send? (if websocket? false true)] 18 | (s/put! sch msg) 19 | (when close-after-send? (s/close! sch)) 20 | true)))) 21 | 22 | (defn- websocket-req? [ring-req] 23 | (when-let [s (get-in ring-req [:headers "upgrade"])] 24 | (= "websocket" (str/lower-case s)))) 25 | 26 | (deftype AlephAsyncNetworkChannelAdapter [opts] 27 | i/IServerChanAdapter 28 | (ring-req->server-ch-resp [sch-adapter ring-req callbacks-map] 29 | (let [{:keys [on-open on-close on-msg _on-error]} callbacks-map 30 | ws? (websocket-req? ring-req)] 31 | (if-let [s (and ws? (try @(aleph/websocket-connection ring-req opts) 32 | (catch Exception e nil)))] 33 | (do 34 | (when on-msg (s/consume (fn [msg] (on-msg s ws? msg)) s)) 35 | (when on-close (s/on-closed s (fn [] (on-close s ws? nil)))) 36 | (when on-open (do (on-open s ws?))) 37 | {:body s}) 38 | 39 | (let [s (s/stream)] ; sch 40 | (when on-close (s/on-closed s (fn [] (on-close s ws? nil)))) 41 | (when on-open (do (on-open s ws?))) 42 | {:body s}))))) 43 | 44 | (defn get-sch-adapter 45 | "Returns a Sente `ServerChan` adapter for `Aleph`, 46 | Ref. . 47 | 48 | Supports websocket-connection options as in `aleph.http`. 49 | If no options map is provided, the default options apply." 50 | ([ ] (AlephAsyncNetworkChannelAdapter. nil)) 51 | ([opts] (AlephAsyncNetworkChannelAdapter. opts))) 52 | -------------------------------------------------------------------------------- /src/taoensso/sente/server_adapters/community/dogfort.cljs: -------------------------------------------------------------------------------- 1 | (ns taoensso.sente.server-adapters.community.dogfort 2 | "Sente server adapter for Node.js with Dog Fort, 3 | Ref. ." 4 | {:author "Matthew Molloy <@whamtet>"} 5 | (:require 6 | [taoensso.encore :as enc :refer-macros ()] 7 | [taoensso.sente.server-adapters.community.generic-node :as generic-node])) 8 | 9 | (defn get-sch-adapter 10 | "Dogfort doesn't need anything special, can just use the `generic-node-ws` 11 | adapter." 12 | [] (generic-node/get-sch-adapter)) 13 | 14 | (enc/deprecated 15 | ;; These are stateful, could be problematic? 16 | (def ^:deprecated ^:no-doc dogfort-adapter (get-sch-adapter)) 17 | (def ^:deprecated ^:no-doc sente-web-server-adapter dogfort-adapter)) 18 | -------------------------------------------------------------------------------- /src/taoensso/sente/server_adapters/community/express.cljs: -------------------------------------------------------------------------------- 1 | (ns taoensso.sente.server-adapters.community.express 2 | "Sente server adapter for Node.js with Express, 3 | Ref. . 4 | 5 | This adapter works differently that the others as Sente is 6 | expecting Ring requests but Express uses http.IncomingMessage. 7 | While most of this adapter could be used for similar 8 | implementations there will be assumptions here that the following 9 | express middleware (or equivalents) are in place: 10 | - cookie-parser 11 | - body-parser 12 | - csurf 13 | - express-session 14 | - express-ws 15 | 16 | See the example project at https://goo.gl/lnkiqS for an 17 | implementation (it's a bit different than something built on Ring)." 18 | {:author "Andrew Phillips <@theasp>"} 19 | (:require 20 | [taoensso.timbre :as timbre] 21 | [taoensso.sente :as sente] 22 | [taoensso.sente.server-adapters.community.generic-node :as generic-node])) 23 | 24 | (defn- obj->map 25 | "Workaround for `TypeError: Cannot convert object to primitive value`s 26 | caused by `(js->clj (.-body exp-req) :keywordize-keys true)` apparently 27 | failing to correctly identify `(.-body exp-req)` as an object. Not sure 28 | what's causing this problem." 29 | [o] 30 | (when-let [ks (js-keys o)] 31 | (into {} (for [k ks] [(keyword k) (str (aget o k))])))) 32 | 33 | (defn- exp-req->ring-req 34 | "Transforms an Express req+resp to a ~standard Ring req map. 35 | `base-ring-req` is a partial Ring req map used to pass in route info." 36 | [base-ring-req exp-req exp-resp] 37 | (let [query-params (obj->map (.-query exp-req)) ; From `express` 38 | form-params (obj->map (.-body exp-req)) ; From `body-parser` 39 | params (merge query-params form-params) 40 | ring-req 41 | (merge base-ring-req 42 | {:response exp-resp 43 | :body exp-req 44 | :query-params query-params 45 | :form-params form-params 46 | :params params})] 47 | 48 | (timbre/tracef "Emulated Ring request: %s" ring-req) 49 | ring-req)) 50 | 51 | (defn- default-csrf-token-fn 52 | "Generates a CSRF token using the `csurf` middleware." 53 | [ring-req] 54 | (.csrfToken (:body ring-req))) 55 | 56 | (defn make-express-channel-socket-server! 57 | "A customized `make-channel-socket-server!` that uses Node.js with 58 | Express as the web server." 59 | [& [opts]] 60 | (timbre/trace "Making Express chsk server") 61 | (let [default-opts {:csrf-token-fn default-csrf-token-fn} 62 | chsk (sente/make-channel-socket-server! 63 | (generic-node/get-sch-adapter) 64 | (merge default-opts opts)) 65 | 66 | {:keys [ajax-get-or-ws-handshake-fn ajax-post-fn]} chsk] 67 | 68 | (merge chsk 69 | {:ajax-get-or-ws-handshake-fn 70 | (fn [req resp & [_ base-ring-req]] 71 | (ajax-get-or-ws-handshake-fn 72 | (exp-req->ring-req base-ring-req req resp))) 73 | 74 | :ajax-post-fn 75 | (fn [req resp & [_ base-ring-req]] 76 | (ajax-post-fn 77 | (exp-req->ring-req base-ring-req req resp)))}))) 78 | -------------------------------------------------------------------------------- /src/taoensso/sente/server_adapters/community/generic_node.cljs: -------------------------------------------------------------------------------- 1 | (ns taoensso.sente.server-adapters.community.generic-node 2 | "Sente server adapter for Node.js using the `ws` and `http` libraries. 3 | Ref. , 4 | , 5 | , 6 | ." 7 | {:author "Andrew Phillips <@theasp>, Matthew Molloy <@whamtet>"} 8 | (:require 9 | [taoensso.encore :as enc] 10 | [taoensso.timbre :as timbre] 11 | [taoensso.sente.interfaces :as i])) 12 | 13 | (defn- ws-open? [ws] (= (.-readyState ws) (.-OPEN ws))) 14 | 15 | (deftype GenericNodeWsAdapter [callbacks-map ws] 16 | i/IServerChan 17 | (sch-open? [sch] (ws-open? ws)) 18 | (sch-close! [sch] (when (ws-open? ws) (.close ws) true)) 19 | (sch-send! [sch websocket? msg] 20 | (when (ws-open? ws) 21 | (let [close-after-send? (if websocket? false true) 22 | sent? 23 | (try 24 | (.send ws msg (fn ack [?error])) 25 | true 26 | (catch :default _ nil))] 27 | (when close-after-send? (.close ws)) 28 | sent?)))) 29 | 30 | (defn- make-ws-chan [callbacks-map ws] 31 | (timbre/trace "Making WebSocket adapter") 32 | (let [chan (GenericNodeWsAdapter. callbacks-map ws) ; sch 33 | {:keys [on-open on-close on-msg _on-error]} callbacks-map 34 | ws? true] 35 | ;; (debugf "WebSocket debug: %s" [(type ws) ws]) 36 | (when on-msg (.on ws "message" (fn [data flags] (on-msg chan ws? data)))) 37 | (when on-close (.on ws "close" (fn [code msg] (on-close chan ws? code)))) 38 | (when on-open (do (on-open chan ws?))) 39 | ws)) 40 | 41 | (deftype GenericNodeAjaxAdapter [resp-open?_ resp] 42 | i/IServerChan 43 | (sch-open? [sch] @resp-open?_) 44 | (sch-close! [sch] 45 | (when (compare-and-set! resp-open?_ true false) 46 | (.end resp) 47 | true)) 48 | 49 | (sch-send! [sch websocket? msg] 50 | (if-let [close-after-send? (if websocket? false true)] 51 | (when (compare-and-set! resp-open?_ true false) 52 | (.end resp msg) 53 | true) 54 | 55 | ;; Currently unused since `close-after-send?` will always 56 | ;; be true for Ajax connections 57 | (when @resp-open?_ 58 | (try 59 | (.write resp msg (fn callback [])) 60 | true 61 | (catch :default _ nil)))))) 62 | 63 | (defn- make-ajax-chan [callbacks-map req resp] 64 | ;; req - IncomingMessage 65 | ;; resp - ServerResponse 66 | (timbre/trace "Making Ajax adapter") 67 | (let [resp-open?_ (atom true) 68 | chan (GenericNodeAjaxAdapter. resp-open?_ resp) ; sch 69 | {:keys [on-open on-close on-msg _on-error]} callbacks-map 70 | ws? false] 71 | 72 | ;; (debugf "Ajax debug: %s" [[(type req) req] [(type resp) resp]]) 73 | ;; (debugf "Ajax request: %s, %s" (aget req "method") (aget req "url")) 74 | 75 | (when on-close 76 | (.on resp "finish" (fn [] (on-close chan ws? nil))) 77 | (.on resp "close" (fn [] (on-close chan ws? nil)))) 78 | (when on-open (on-open chan ws?)) 79 | 80 | ;; If we reply blank for Ajax conns then the route matcher will fail. 81 | ;; Dog Fort will send a 404 resp and close the conn. To keep it open 82 | ;; we just send this instead of a Ring resp. Shouldn't have a bad 83 | ;; effect on other servers. 84 | {:keep-alive true})) 85 | 86 | (deftype GenericNodeServerChanAdapter [] 87 | i/IServerChanAdapter 88 | (ring-req->server-ch-resp [sch-adapter ring-req callbacks-map] 89 | (if-let [ws (:websocket ring-req)] 90 | (make-ws-chan callbacks-map ws) 91 | (make-ajax-chan callbacks-map 92 | (:body ring-req) 93 | (:response ring-req))))) 94 | 95 | (defn get-sch-adapter [] (GenericNodeServerChanAdapter.)) 96 | 97 | (enc/deprecated 98 | ;; These are stateful, could be problematic? 99 | (def ^:deprecated ^:no-doc generic-node-adapter (get-sch-adapter)) 100 | (def ^:deprecated ^:no-doc sente-web-server-adapter generic-node-adapter)) 101 | -------------------------------------------------------------------------------- /src/taoensso/sente/server_adapters/community/immutant.clj: -------------------------------------------------------------------------------- 1 | (ns taoensso.sente.server-adapters.community.immutant 2 | {:author "Toby Crawley (@tobias)"} 3 | (:require 4 | [taoensso.encore :as enc] 5 | [taoensso.sente.interfaces :as i] 6 | [immutant.web.async :as imm])) 7 | 8 | (extend-type org.projectodd.wunderboss.web.async.Channel 9 | i/IServerChan 10 | (sch-open? [sch] (imm/open? sch)) 11 | (sch-close! [sch] (imm/close sch)) 12 | (sch-send! [sch websocket? msg] 13 | (let [close-after-send? (if websocket? false true)] 14 | (imm/send! sch msg {:close? close-after-send?})))) 15 | 16 | (deftype ImmutantServerChanAdapter [] 17 | i/IServerChanAdapter 18 | (ring-req->server-ch-resp [sch-adapter ring-req callbacks-map] 19 | (let [{:keys [on-open on-close on-msg on-error]} callbacks-map 20 | ws? (:websocket? ring-req)] 21 | 22 | ;; Returns {:body ...}: 23 | (imm/as-channel ring-req 24 | :timeout 0 ; Deprecated, Ref. https://goo.gl/t4RolO 25 | :on-open (when on-open (fn [sch ] (on-open sch ws?))) 26 | :on-error (when on-error (fn [sch throwable] (on-error sch ws? throwable))) 27 | :on-message (when on-msg (fn [sch msg ] (on-msg sch ws? msg))) 28 | :on-close (when on-close 29 | (fn [sch {:keys [code reason] :as status-map}] 30 | (on-close sch ws? status-map))))))) 31 | 32 | (defn get-sch-adapter 33 | "Returns a Sente `ServerChan` adapter for `Immutant` v2+, 34 | Ref. ." 35 | [] (ImmutantServerChanAdapter.)) 36 | 37 | (enc/deprecated 38 | (defn ^:deprecated ^:no-doc make-immutant-adapter [_opts] (get-sch-adapter)) 39 | (def ^:deprecated ^:no-doc immutant-adapter (get-sch-adapter)) 40 | (def ^:deprecated ^:no-doc sente-web-server-adapter immutant-adapter)) 41 | -------------------------------------------------------------------------------- /src/taoensso/sente/server_adapters/community/jetty.clj: -------------------------------------------------------------------------------- 1 | (ns taoensso.sente.server-adapters.community.jetty 2 | {:author "Alex Gunnarson (@alexandergunnarson)"} 3 | (:require 4 | [ring.core.protocols :as ring-protocols] 5 | [ring.util.response :as ring-response] 6 | [ring.websocket :as ws] 7 | [ring.websocket.protocols :as ws.protocols] 8 | [taoensso.encore :as enc] 9 | [taoensso.sente.interfaces :as i] 10 | [taoensso.timbre :as timbre]) 11 | 12 | (:import 13 | [ring.websocket.protocols Socket])) 14 | 15 | ;;;; WebSockets 16 | 17 | (enc/compile-if org.eclipse.jetty.websocket.common.WebSocketSession 18 | (extend-type org.eclipse.jetty.websocket.common.WebSocketSession ; Jetty 12 19 | i/IServerChan 20 | (sch-open? [sch] (.isOpen sch)) 21 | (sch-close! [sch] (.close sch)) 22 | (sch-send! [sch-listener _websocket? msg] 23 | (ws.protocols/-send sch-listener msg) 24 | true))) 25 | 26 | (enc/compile-if org.eclipse.jetty.websocket.api.WebSocketListener 27 | (extend-type org.eclipse.jetty.websocket.api.WebSocketListener ; Jetty 11 28 | i/IServerChan 29 | (sch-open? [sch] (.isOpen sch)) 30 | (sch-close! [sch] (.close sch)) 31 | (sch-send! [sch-listener _websocket? msg] 32 | (ws.protocols/-send sch-listener msg) 33 | true))) 34 | 35 | (extend-type Socket 36 | i/IServerChan 37 | (sch-open? [sch] (ws.protocols/-open? sch)) 38 | (sch-close! [sch] (ws.protocols/-close sch nil nil)) 39 | (sch-send! [sch-socket _websocket? msg] 40 | (ws.protocols/-send sch-socket msg) 41 | true)) 42 | 43 | (defn- respond-ws 44 | [{:keys [websocket-subprotocols]} 45 | {:keys [on-close on-error on-msg on-open]}] 46 | 47 | {:ring.websocket/protocol (first websocket-subprotocols) 48 | :ring.websocket/listener 49 | (reify ws.protocols/Listener 50 | (on-close [_ sch status _] (on-close sch true status)) 51 | (on-error [_ sch error] (on-error sch true error)) 52 | (on-message [_ sch msg] (on-msg sch true msg)) 53 | (on-open [_ sch] (on-open sch true)) 54 | (on-pong [_ sch data]))}) 55 | 56 | ;;;; Ajax 57 | 58 | (defprotocol ISenteJettyAjaxChannel 59 | (ajax-read! [sch])) 60 | 61 | (deftype SenteJettyAjaxChannel [resp-promise_ open?_ on-close adapter-opts] 62 | i/IServerChan 63 | (sch-send! [sch _websocket? msg] (deliver resp-promise_ msg) (i/sch-close! sch)) 64 | (sch-open? [sch] @open?_) 65 | (sch-close! [sch] 66 | (when (compare-and-set! open?_ true false) 67 | (deliver resp-promise_ nil) 68 | (when on-close (on-close sch false nil)) 69 | true)) 70 | 71 | ISenteJettyAjaxChannel 72 | (ajax-read! [_sch] 73 | (let [{:keys [ajax-resp-timeout-ms]} adapter-opts 74 | resp 75 | (if ajax-resp-timeout-ms 76 | (deref resp-promise_ ajax-resp-timeout-ms ::timeout) 77 | (deref resp-promise_))] 78 | 79 | (if (= resp ::timeout) 80 | (throw (ex-info "Ajax read timeout" {:timeout-msecs ajax-resp-timeout-ms})) 81 | resp)))) 82 | 83 | (defn- ajax-ch 84 | [{:keys [on-open on-close]} adapter-opts] 85 | (let [open?_ (atom true) 86 | sch (SenteJettyAjaxChannel. (promise) open?_ on-close adapter-opts)] 87 | (when on-open (on-open sch false)) 88 | sch)) 89 | 90 | (extend-protocol ring-protocols/StreamableResponseBody 91 | SenteJettyAjaxChannel 92 | (write-body-to-stream [body _response ^java.io.OutputStream output-stream] 93 | ;; Use `future` because `output-stream` might block the thread, 94 | ;; Ref. 95 | (future 96 | (try 97 | (.write output-stream (.getBytes ^String (ajax-read! body) java.nio.charset.StandardCharsets/UTF_8)) 98 | (.flush output-stream) 99 | (catch Throwable t 100 | (timbre/error t)) 101 | (finally 102 | (.close output-stream)))))) 103 | 104 | ;;;; Adapter 105 | 106 | (deftype JettyServerChanAdapter [adapter-opts] 107 | i/IServerChanAdapter 108 | (ring-req->server-ch-resp [_ request callbacks-map] 109 | (if (ws/upgrade-request? request) 110 | (respond-ws request callbacks-map) 111 | (ring-response/response (ajax-ch callbacks-map adapter-opts))))) 112 | 113 | (defn get-sch-adapter 114 | "Returns a Sente `ServerChan` adapter for `ring-jetty-adapter` [1]. 115 | Supports Jetty 11, 12. 116 | 117 | Options: 118 | `:ajax-resp-timeout-ms` - Max msecs to wait for Ajax responses (default 60 secs), 119 | exception thrown on timeout. 120 | 121 | [1] Ref. ." 122 | ([] (get-sch-adapter nil)) 123 | ([{:as opts 124 | :keys [ajax-resp-timeout-ms] 125 | :or {ajax-resp-timeout-ms (* 60 1000)}}] 126 | 127 | (JettyServerChanAdapter. 128 | (assoc opts 129 | :ajax-resp-timeout-ms ajax-resp-timeout-ms)))) 130 | -------------------------------------------------------------------------------- /src/taoensso/sente/server_adapters/community/macchiato.cljs: -------------------------------------------------------------------------------- 1 | (ns taoensso.sente.server-adapters.community.macchiato 2 | "Sente server adapter for Node.js with the Macchiato Framework, 3 | Ref. ." 4 | {:author "Andrew Phillips <@theasp>"} 5 | (:require 6 | [taoensso.encore :as enc] 7 | [taoensso.timbre :as timbre] 8 | [taoensso.sente :as sente] 9 | [taoensso.sente.server-adapters.community.generic-node :as generic-node] 10 | [macchiato.middleware.anti-forgery :as csrf])) 11 | 12 | (def csrf-path [:session :macchiato.middleware.anti-forgery/anti-forgery-token]) 13 | 14 | (defn wrap-macchiato 15 | "Wraps a generic node Sente handler to work with Macchiato. This 16 | remaps some keys of a Macchiato request to match what Sente and the 17 | generic node adapter are expecting, calling `handler`. The generic 18 | node adapter will call the appropriate methods on the Node.js response 19 | object without using Macchiato's response function." 20 | [handler] 21 | (fn [req res raise] 22 | (-> req 23 | (assoc :response (:node/response req)) 24 | (assoc-in [:session :csrf-token] (get-in req csrf-path)) 25 | (handler)))) 26 | 27 | (defn make-macchiato-channel-socket-server! 28 | "A customized `make-channel-socket-server!` that uses Node.js with 29 | Macchiato as the web server." 30 | [& [opts]] 31 | (timbre/trace "Making Macchiato chsk server") 32 | (-> (generic-node/get-sch-adapter) 33 | (sente/make-channel-socket-server! opts) 34 | (update :ajax-get-or-ws-handshake-fn wrap-macchiato) 35 | (update :ajax-post-fn wrap-macchiato))) 36 | -------------------------------------------------------------------------------- /src/taoensso/sente/server_adapters/community/nginx_clojure.clj: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc taoensso.sente.server-adapters.community.nginx-clojure 2 | ;; `^:no-doc` needed to prevent broken cljdoc build 3 | {:author "Zhang Yuexiang (@xfeep)"} 4 | (:require 5 | [taoensso.encore :as enc] 6 | [taoensso.sente.interfaces :as i] 7 | [nginx.clojure.core :as ncc])) 8 | 9 | (def ^:dynamic *max-message-size* 10 | nginx.clojure.WholeMessageAdapter/DEFAULT_MAX_MESSAGE_SIZE) 11 | 12 | (extend-type nginx.clojure.NginxHttpServerChannel 13 | i/IServerChan 14 | (sch-open? [sch] (not (ncc/closed? sch))) 15 | (sch-close! [sch] (ncc/close! sch)) 16 | (sch-send! [sch websocket? msg] 17 | (if (ncc/closed? sch) 18 | false 19 | (let [close-after-send? (if websocket? false true)] 20 | (ncc/send! sch msg true (boolean close-after-send?)) 21 | true)))) 22 | 23 | (deftype NginxServerChanAdapter [] 24 | i/IServerChanAdapter 25 | (ring-req->server-ch-resp [sch-adapter ring-req callbacks-map] 26 | (let [{:keys [on-open on-close on-msg _on-error]} callbacks-map 27 | sch (ncc/hijack! ring-req true) 28 | ws? (ncc/websocket-upgrade! sch false)] 29 | 30 | ;; Returns {:status 200 :body }: 31 | (when-not ws? ; Send normal header for non-websocket requests 32 | (.setIgnoreFilter sch false) 33 | 34 | ;; For Sente #150, give client a chance to set broken listener. 35 | ;; We could do this via `send-header!` with something like 36 | ;; `(send-header! sch 200, ..., true, false)`. Instead, we're 37 | ;; choosing this approach to match the behaviour of other adapters: 38 | (ncc/send! sch nil true false) 39 | (ncc/send-header! sch 200 {"Content-Type" "text/html"} false false)) 40 | 41 | (ncc/add-aggregated-listener! sch *max-message-size* 42 | {:on-open (when on-open (fn [sch] (on-open sch ws?))) 43 | :on-message (when on-msg (fn [sch msg] (on-msg sch ws? msg))) 44 | :on-close (when on-close (fn [sch reason] (on-close sch ws? reason))) 45 | :on-error nil}) 46 | 47 | {:status 200 :body sch}))) 48 | 49 | (defn get-sch-adapter 50 | "Returns a Sente `ServerChan` adapter for `Nginx-Clojure` v0.4.2+. 51 | Ref. ." 52 | [] (NginxServerChanAdapter.)) 53 | 54 | (enc/deprecated 55 | (def ^:deprecated ^:no-doc nginx-clojure-adapter (get-sch-adapter)) 56 | (def ^:deprecated ^:no-doc sente-web-server-adapter nginx-clojure-adapter)) 57 | -------------------------------------------------------------------------------- /src/taoensso/sente/server_adapters/community/undertow.clj: -------------------------------------------------------------------------------- 1 | (ns taoensso.sente.server-adapters.community.undertow 2 | {:author "Nik Peric"} 3 | (:require 4 | [ring.adapter.undertow.websocket :as websocket] 5 | [ring.adapter.undertow.response :as response] 6 | [taoensso.sente.interfaces :as i]) 7 | (:import 8 | [io.undertow.websockets.core WebSocketChannel] 9 | [io.undertow.server HttpServerExchange] 10 | [io.undertow.websockets 11 | WebSocketConnectionCallback 12 | WebSocketProtocolHandshakeHandler])) 13 | 14 | ;;;; WebSockets 15 | 16 | (extend-type WebSocketChannel 17 | i/IServerChan 18 | (sch-open? [sch] (.isOpen sch)) 19 | (sch-close! [sch] (.sendClose sch)) 20 | (sch-send! [sch websocket? msg] 21 | (websocket/send msg sch) 22 | (i/sch-open? sch))) 23 | 24 | (extend-protocol response/RespondBody 25 | WebSocketConnectionCallback 26 | (respond [body ^HttpServerExchange exchange] 27 | (let [handler (WebSocketProtocolHandshakeHandler. body)] 28 | (.handleRequest handler exchange)))) 29 | 30 | (defn- ws-ch 31 | [{:keys [on-open on-close on-msg on-error]} _adapter-opts] 32 | (websocket/ws-callback 33 | {:on-open (when on-open (fn [{:keys [channel]}] (on-open channel true))) 34 | :on-error (when on-error (fn [{:keys [channel error]}] (on-error channel true error))) 35 | :on-message (when on-msg (fn [{:keys [channel data]}] (on-msg channel true data))) 36 | :on-close-message (when on-close (fn [{:keys [channel message]}] (on-close channel true message)))})) 37 | 38 | ;;;; Ajax 39 | 40 | (defprotocol ISenteUndertowAjaxChannel 41 | (ajax-read! [sch])) 42 | 43 | (deftype SenteUndertowAjaxChannel [resp-promise_ open?_ on-close adapter-opts] 44 | i/IServerChan 45 | (sch-send! [sch websocket? msg] (deliver resp-promise_ msg) (i/sch-close! sch)) 46 | (sch-open? [sch] @open?_) 47 | (sch-close! [sch] 48 | (when (compare-and-set! open?_ true false) 49 | (deliver resp-promise_ nil) 50 | (when on-close (on-close sch false nil)) 51 | true)) 52 | 53 | ISenteUndertowAjaxChannel 54 | (ajax-read! [sch] 55 | (let [{:keys [ajax-resp-timeout-ms]} adapter-opts 56 | resp 57 | (if ajax-resp-timeout-ms 58 | (deref resp-promise_ ajax-resp-timeout-ms ::timeout) 59 | (deref resp-promise_))] 60 | 61 | (if (= resp ::timeout) 62 | (throw (ex-info "Ajax read timeout" {:timeout-msecs ajax-resp-timeout-ms})) 63 | resp)))) 64 | 65 | (defn- ajax-ch [{:keys [on-open on-close]} adapter-opts] 66 | (let [open?_ (atom true) 67 | sch 68 | (SenteUndertowAjaxChannel. (promise) open?_ on-close 69 | adapter-opts)] 70 | 71 | (when on-open (on-open sch false)) 72 | sch)) 73 | 74 | (extend-protocol response/RespondBody 75 | SenteUndertowAjaxChannel 76 | (respond [body ^HttpServerExchange exchange] 77 | (response/respond (ajax-read! body) exchange))) 78 | 79 | ;;;; Adapter 80 | 81 | (deftype UndertowServerChanAdapter [adapter-opts] 82 | i/IServerChanAdapter 83 | (ring-req->server-ch-resp [sch-adapter ring-req callbacks-map] 84 | {:body 85 | (if (:websocket? ring-req) 86 | (ws-ch callbacks-map adapter-opts) 87 | (ajax-ch callbacks-map adapter-opts))})) 88 | 89 | (defn get-sch-adapter 90 | "Returns a Sente `ServerChan` adapter for `ring-undertow-adapter` [1]. 91 | 92 | Options: 93 | `:ajax-resp-timeout-ms` - Max msecs to wait for Ajax responses (default 60 secs), 94 | exception thrown on timeout. 95 | 96 | [1] Ref. ." 97 | ([] (get-sch-adapter nil)) 98 | ([{:as opts 99 | :keys [ajax-resp-timeout-ms] 100 | :or {ajax-resp-timeout-ms (* 60 1000)}}] 101 | 102 | (UndertowServerChanAdapter. 103 | (assoc opts 104 | :ajax-resp-timeout-ms ajax-resp-timeout-ms)))) 105 | -------------------------------------------------------------------------------- /src/taoensso/sente/server_adapters/http_kit.clj: -------------------------------------------------------------------------------- 1 | (ns taoensso.sente.server-adapters.http-kit 2 | "Sente server adapter for http-kit, 3 | Ref. ." 4 | {:author "Peter Taoussanis (@ptaoussanis)"} 5 | (:require 6 | [taoensso.encore :as enc] 7 | [taoensso.sente.interfaces :as i] 8 | [org.httpkit.server :as hk])) 9 | 10 | (extend-type org.httpkit.server.AsyncChannel 11 | i/IServerChan 12 | (sch-open? [sch] (hk/open? sch)) 13 | (sch-close! [sch] (hk/close sch)) 14 | (sch-send! [sch websocket? msg] 15 | (let [close-after-send? (if websocket? false true)] 16 | (hk/send! sch msg close-after-send?)))) 17 | 18 | (deftype HttpKitServerChanAdapter [] 19 | i/IServerChanAdapter 20 | (ring-req->server-ch-resp [sch-adapter ring-req callbacks-map] 21 | (let [{:keys [on-open on-close on-msg _on-error]} callbacks-map 22 | ws? (:websocket? ring-req)] 23 | 24 | ;; Note `as-channel` requires http-kit >= v2.4.0 25 | ;; Returns {:body ...}: 26 | (hk/as-channel ring-req 27 | {:on-close (when on-close (fn [sch status-kw] (on-close sch ws? status-kw))) 28 | :on-receive (when on-msg (fn [sch msg] (on-msg sch ws? msg))) 29 | :on-open (when on-open (fn [sch ] (on-open sch ws?)))})))) 30 | 31 | (defn get-sch-adapter [] (HttpKitServerChanAdapter.)) 32 | 33 | (enc/deprecated 34 | (def ^:deprecated ^:no-doc http-kit-adapter (get-sch-adapter)) 35 | (def ^:deprecated ^:no-doc sente-web-server-adapter http-kit-adapter)) 36 | -------------------------------------------------------------------------------- /test/taoensso/graal_tests.clj: -------------------------------------------------------------------------------- 1 | (ns taoensso.graal-tests 2 | (:require [taoensso.sente :as sente]) 3 | (:gen-class)) 4 | 5 | (defn -main [& args] (println "Namespace loaded successfully")) 6 | -------------------------------------------------------------------------------- /test/taoensso/sente_tests.cljc: -------------------------------------------------------------------------------- 1 | (ns taoensso.sente-tests 2 | "Due to the complexity of automated browser tests, Sente has 3 | traditionally been tested manually. Before each release, a suite 4 | of checks have been done against the reference example project. 5 | 6 | In the hope of eventually doing more of this work automatically, 7 | the current namespace provided as a groundwork for future automated 8 | tests. 9 | 10 | PRs very welcome if you'd like to contribute to this effort!" 11 | 12 | (:require 13 | [clojure.test :as test :refer [deftest testing is]] 14 | ;; [clojure.test.check :as tc] 15 | ;; [clojure.test.check.generators :as tc-gens] 16 | ;; [clojure.test.check.properties :as tc-props] 17 | [clojure.string :as str] 18 | 19 | ;; :cljs cannot compile taoensso.sente under :nodejs target 20 | #?(:clj [taoensso.sente :as sente]))) 21 | 22 | (comment 23 | (remove-ns 'taoensso.sente-tests) 24 | (test/run-tests 'taoensso.sente-tests)) 25 | 26 | ;;;; 27 | 28 | #?(:clj (deftest _test-clj (is (= 1 1)))) 29 | #?(:cljs (deftest _test-cljs (is (= 1 1)))) 30 | 31 | ;;;; 32 | 33 | #?(:cljs 34 | (defmethod test/report [:cljs.test/default :end-run-tests] [m] 35 | (when-not (test/successful? m) 36 | ;; Trigger non-zero `lein test-cljs` exit code for CI 37 | (throw (ex-info "ClojureScript tests failed" {}))))) 38 | 39 | #?(:cljs (test/run-tests)) 40 | -------------------------------------------------------------------------------- /wiki/.gitignore: -------------------------------------------------------------------------------- 1 | README.md 2 | -------------------------------------------------------------------------------- /wiki/0-Breaking-changes.md: -------------------------------------------------------------------------------- 1 | This page details possible **breaking changes and migration instructions** for Sente. 2 | 3 | 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. 4 | 5 | Thanks for your understanding - [Peter Taoussanis](https://www.taoensso.com) 6 | 7 | # Sente `v1.17` to `v1.18` 8 | 9 | This upgrade involves **4 possible breaking changes** detailed below: 10 | 11 | **Change 1/4** 12 | 13 | The default `wrap-recv-evs?` option has changed in [`make-channel-socket-client!`](http://taoensso.github.io/sente/taoensso.sente.html#var-make-channel-socket-client.21). 14 | 15 | - **Old** default behaviour: events from server are **wrapped** with `[:chsk/recv ]` 16 | - **New** default behaviour: events from server are **unwrapped** 17 | 18 | **Motivation for change**: there's no benefit to wrapping events from the server, and this wrapping often causes confusion. 19 | 20 | More info at: [#319](../issues/319) 21 | 22 | --- 23 | 24 | **Change 2/4** 25 | 26 | The default [`*write-legacy-pack-format?*`](http://taoensso.github.io/sente/taoensso.sente.html#var-*write-legacy-pack-format.3F*) value has changed from `true` to `false`. 27 | 28 | This change is only relevant for the small minority of folks that use a custom (non-standard) [`IPacker`](https://github.com/taoensso/sente/blob/f69a5df6d1f3e88d66a148c74e1b5a9084c9c0b9/src/taoensso/sente/interfaces.cljc#L55). 29 | 30 | If you do use a custom (non-standard) `IPacker`, please see the [relevant docstring](http://taoensso.github.io/sente/taoensso.sente.html#var-*write-legacy-pack-format.3F*) for details. 31 | 32 | **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. 33 | 34 | More info at: [#398](../issues/398), [#404](../issues/404) 35 | 36 | --- 37 | 38 | **Change 3/4** 39 | 40 | Unofficial adapters have been moved to `community` dir. 41 | 42 | This change is only relevant for folks using a server other than http-kit. 43 | 44 | If you're using a different server, the adapter's namespace will now include a `.community` part, e.g.: 45 | 46 | - **Old** adapter namespace: `taoensso.sente.server-adapters.undertow` 47 | - **New** adapter namespace: `taoensso.sente.server-adapters.community.undertow` 48 | 49 | **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. 50 | 51 | More info at: [#412](../issues/412) 52 | 53 | --- 54 | 55 | **Change 4/4** 56 | 57 | The `jetty9-ring-adapter` has been removed. 58 | 59 | This change is only relevant for folks using `jetty9-ring-adapter`. 60 | 61 | **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. 62 | 63 | Apologies for this! 64 | 65 | More info at: [#424](../issues/424), [#426](../issues/426) 66 | 67 | --- -------------------------------------------------------------------------------- /wiki/1-Getting-started.md: -------------------------------------------------------------------------------- 1 | > See also [here](./3-Example-projects) for **full example projects** 👈 2 | 3 | # Setup 4 | 5 | ## Dependency 6 | 7 | Add the [relevant dependency](../#latest-releases) to your project: 8 | 9 | ```clojure 10 | Leiningen: [com.taoensso/sente "x-y-z"] ; or 11 | deps.edn: com.taoensso/sente {:mvn/version "x-y-z"} 12 | ``` 13 | 14 | ## Server-side setup 15 | 16 | First make sure that you're using one of the [supported web servers](https://github.com/taoensso/sente/tree/master/src/taoensso/sente/server_adapters) (PRs for additional server adapters welcome!). 17 | 18 | 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: 19 | 20 | ```clojure 21 | (defroutes my-app 22 | (GET "/" req (my-landing-pg-handler req)) 23 | (POST "/submit-form" req (my-form-submit-handler req))) 24 | ``` 25 | 26 | For Sente, we're going to add 2 new URLs and setup their handlers: 27 | 28 | ```clojure 29 | (ns my-server-side-routing-ns ; Usually a .clj file 30 | (:require 31 | ;; 32 | [taoensso.sente :as sente] ; <--- Add this 33 | 34 | [ring.middleware.anti-forgery :refer [wrap-anti-forgery]] ; <--- Recommended 35 | 36 | ;; Uncomment a web-server adapter ---> 37 | ;; [taoensso.sente.server-adapters.http-kit :refer [get-sch-adapter]] 38 | ;; [taoensso.sente.server-adapters.immutant :refer [get-sch-adapter]] 39 | ;; [taoensso.sente.server-adapters.nginx-clojure :refer [get-sch-adapter]] 40 | ;; [taoensso.sente.server-adapters.aleph :refer [get-sch-adapter]] 41 | )) 42 | 43 | ;;; Add this: ---> 44 | (let [{:keys [ch-recv send-fn connected-uids 45 | ajax-post-fn ajax-get-or-ws-handshake-fn]} 46 | (sente/make-channel-socket-server! (get-sch-adapter) {})] 47 | 48 | (def ring-ajax-post ajax-post-fn) 49 | (def ring-ajax-get-or-ws-handshake ajax-get-or-ws-handshake-fn) 50 | (def ch-chsk ch-recv) ; ChannelSocket's receive channel 51 | (def chsk-send! send-fn) ; ChannelSocket's send API fn 52 | (def connected-uids connected-uids) ; Watchable, read-only atom 53 | ) 54 | 55 | (defroutes my-app-routes 56 | ;; 57 | 58 | ;;; Add these 2 entries: ---> 59 | (GET "/chsk" req (ring-ajax-get-or-ws-handshake req)) 60 | (POST "/chsk" req (ring-ajax-post req)) 61 | ) 62 | 63 | (def my-app 64 | (-> my-app-routes 65 | ;; Add necessary Ring middleware: 66 | ring.middleware.keyword-params/wrap-keyword-params 67 | ring.middleware.params/wrap-params 68 | ring.middleware.anti-forgery/wrap-anti-forgery 69 | ring.middleware.session/wrap-session)) 70 | ``` 71 | 72 | > 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. 73 | 74 | Add a CSRF token somewhere in your HTML: 75 | 76 | ``` 77 | (let [csrf-token (force ring.middleware.anti-forgery/*anti-forgery-token*)] 78 | [:div#sente-csrf-token {:data-csrf-token csrf-token}]) 79 | ``` 80 | 81 | ## Client-side setup 82 | 83 | You'll setup something similar on the client side: 84 | 85 | ```clojure 86 | (ns my-client-side-ns ; Usually a .cljs file 87 | (:require-macros 88 | [cljs.core.async.macros :as asyncm :refer (go go-loop)]) 89 | (:require 90 | ;; 91 | [cljs.core.async :as async :refer (! put! chan)] 92 | [taoensso.sente :as sente :refer (cb-success?)] ; <--- Add this 93 | )) 94 | 95 | ;;; Add this: ---> 96 | 97 | (def ?csrf-token 98 | (when-let [el (.getElementById js/document "sente-csrf-token")] 99 | (.getAttribute el "data-csrf-token"))) 100 | 101 | (let [{:keys [chsk ch-recv send-fn state]} 102 | (sente/make-channel-socket-client! 103 | "/chsk" ; Note the same path as before 104 | ?csrf-token 105 | {:type :auto ; e/o #{:auto :ajax :ws} 106 | })] 107 | 108 | (def chsk chsk) 109 | (def ch-chsk ch-recv) ; ChannelSocket's receive channel 110 | (def chsk-send! send-fn) ; ChannelSocket's send API fn 111 | (def chsk-state state) ; Watchable, read-only atom 112 | ) 113 | ``` 114 | 115 | # Usage 116 | 117 | 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. 118 | 119 | **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://taoensso.github.io/sente/) for details. 120 | 121 | ## Client-side API 122 | 123 | * `ch-recv` is a **core.async channel** that'll receive `event-msg`s 124 | * `chsk-send!` is a `(fn [event & [?timeout-ms ?cb-fn]])` for standard **client>server req>resp calls** 125 | 126 | Let's compare some Ajax and Sente code for sending an event from the client to the server: 127 | 128 | ```clojure 129 | (jayq/ajax ; Using the jayq wrapper around jQuery 130 | {:type :post :url "/some-url-on-server/" 131 | :data {:name "Rich Hickey" 132 | :type "Awesome"} 133 | :timeout 8000 134 | :success (fn [content text-status xhr] (do-something! content)) 135 | :error (fn [xhr text-status] (error-handler!))}) 136 | 137 | (chsk-send! ; Using Sente 138 | [:some/request-id {:name "Rich Hickey" :type "Awesome"}] ; Event 139 | 8000 ; Timeout 140 | ;; Optional callback: 141 | (fn [reply] ; Reply is arbitrary Clojure data 142 | (if (sente/cb-success? reply) ; Checks for :chsk/closed, :chsk/timeout, :chsk/error 143 | (do-something! reply) 144 | (error-handler!)))) 145 | ``` 146 | 147 | Note: 148 | 149 | * The Ajax request is slow to initialize, and bulky (HTTP overhead) 150 | * The Sente request is pre-initialized (usu. WebSocket), and lean (edn/Transit protocol) 151 | 152 | ## Server-side API 153 | 154 | * `ch-recv` is a **core.async channel** that'll receive `event-msg`s 155 | * `chsk-send!` is a `(fn [user-id event])` for async **server>user PUSH calls** 156 | 157 | For asynchronously pushing an event from the server to the client: 158 | 159 | * Ajax would require a clumsy long-polling setup, and wouldn't easily support users connected with multiple clients simultaneously 160 | * Sente: `(chsk-send! "destination-user-id" [:some/alert-id ])` 161 | 162 | **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. 163 | 164 | ## Types and terminology 165 | 166 | Term | Form 167 | ---------------- | ---------------------------------------------------------------------- 168 | event | `[ ]`, e.g. `[:my-app/some-req {:data "data"}]` 169 | server event-msg | `{:keys [event id ?data send-fn ?reply-fn uid ring-req client-id]}` 170 | client event-msg | `{:keys [event id ?data send-fn]}` 171 | `` | A _namespaced_ keyword like `:my-app/some-req` 172 | `` | An optional _arbitrary edn value_ like `{:data "data"}` 173 | `:ring-req` | Ring map for Ajax request or WebSocket's initial handshake request 174 | `:?reply-fn` | Present only when client requested a reply 175 | 176 | ## Summary 177 | 178 | * Clients use `chsk-send!` to send `event`s to the server and optionally request a reply with timeout 179 | * 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). 180 | * The server can also use an `event-msg`'s `?reply-fn` to _reply_ to a particular client `event` using an _arbitrary edn value_ 181 | 182 | ## Limitations 183 | 184 | ### Large transfers 185 | 186 | I recommend **not** using Sente to transfer large payloads (> 1MB). 187 | 188 | The reason is that Sente will by default operate over a WebSocket when possible. This is great for realtime bidirectional communications, but it does mean that there's a bottleneck on that socket. 189 | 190 | If a WebSocket connection is saturated dealing with a large transfer, other communications (e.g. notifications) won't be able to get through until the large transfer completes. 191 | 192 | In the worst case (with very large payloads and/or very slow connections), this could even cause the **client to disconnect** due to an apparently unresponsive server. 193 | 194 | Instead, I recommend using Sente **only for small payloads** (<= 1MB) and for **signalling**. For large payloads do the following: 195 | 196 | - **client->server**: the client can just request the large payload over Ajax 197 | - **server->client**: the server can signal the client to request the large payload over Ajax 198 | 199 | (Sente includes a [util](https://taoensso.github.io/sente/taoensso.sente.cljs.html#var-ajax-lite) to make Ajax requests very easy). 200 | 201 | This way you're using the ideal tools for each job: 202 | 203 | - Sente's realtime socket is reserved for realtime purposes 204 | - Dedicated Ajax requests are used for large transfers, and have access to normal browser HTTP caching, etc. 205 | 206 | ## Channel socket state 207 | 208 | 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. 209 | 210 | The event form is `[:chsk/state [ ]]` with the following possible state map keys: 211 | 212 | Key | Value 213 | --------------- | -------------------------------------------------------- 214 | :type | e/o `#{:auto :ws :ajax}` 215 | :open? | Truthy iff chsk appears to be open (connected) now 216 | :ever-opened? | Truthy iff chsk handshake has ever completed successfully 217 | :first-open? | Truthy iff chsk just completed first successful handshake 218 | :uid | User id provided by server on handshake, or nil 219 | :csrf-token | CSRF token provided by server on handshake, or nil 220 | :handshake-data | Arb user data provided by server on handshake 221 | :last-ws-error | `?{:udt _ :ev }` 222 | :last-ws-close | `?{:udt _ :ev :clean? _ :code _ :reason _}` 223 | :last-close | `?{:udt _ :reason _}`, with reason e/o `#{nil :requested-disconnect :requested-reconnect :downgrading-ws-to-ajax :unexpected}` 224 | 225 | # Limitations 226 | 227 | ## Large transfers 228 | 229 | I recommend **not** using Sente to transfer large payloads (> 1MB). 230 | 231 | The reason is that Sente will by default operate over a WebSocket when possible. This is great for realtime bidirectional communications, but: 232 | 233 | 1. WebSockets aren't ideal for large data transfers, and 234 | 2. You'll have a bottleneck on that socket 235 | 236 | If a WebSocket connection is saturated dealing with a large transfer, other communications (e.g. notifications) won't be able to get through until the large transfer completes. 237 | 238 | In the worst case (with very large payloads and/or very slow connections), this could even cause the **client to disconnect** due to an apparently unresponsive server. 239 | 240 | Instead, I recommend using Sente **only for small payloads** (<= 1MB) and for **signalling**. For large payloads do the following: 241 | 242 | - **client->server**: the client can just request the large payload over Ajax 243 | - **server->client**: the server can signal the client to request the large payload over Ajax 244 | 245 | (Sente includes a [util](https://taoensso.github.io/sente/taoensso.sente.cljs.html#var-ajax-lite) to make Ajax requests very easy). 246 | 247 | This way you're using the ideal tool for each job: 248 | 249 | - Sente's realtime socket is reserved for realtime purposes 250 | - Dedicated Ajax requests are used for large transfers, and have access to normal browser HTTP caching, etc. -------------------------------------------------------------------------------- /wiki/2-Client-and-user-ids.md: -------------------------------------------------------------------------------- 1 | Sente uses two types of connection identifiers: **client-ids** and **user-ids**. 2 | 3 | # Client ids 4 | 5 | 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. 6 | 7 | By default, clients generate their own random (uuid) client-id. You can override this in your call to [`make-channel-socket-client!`](http://taoensso.github.io/sente/taoensso.sente.html#var-make-channel-socket-client.21). 8 | 9 | Note: 10 | 1. Each client chooses its _own_ client-id with no input from the server. 11 | 2. By default, each browser tab has its _own_ client-id. 12 | 3. By default, reloading a browser tab (or closing a tab + opening a new one) means a _new_ client-id. 13 | 14 | # User ids 15 | 16 | This is the more important concept in Sente, and is actually the only type of identifier supported by Sente's server>client push API. 17 | 18 | A user-id is a unique application-level identifier associated with >=0 Sente clients (client-ids). 19 | 20 | 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`. 21 | 22 | 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. 23 | 24 | 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://taoensso.github.io/sente/taoensso.sente.html#var-make-channel-socket-server.21). 25 | 26 | Note: 27 | 28 | 1. One user-id may be associated with 0, 1, or _many_ clients (client-ids). 29 | 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. 30 | 31 | # Examples 32 | 33 | ## Per-session persistent user-id 34 | 35 | This is probably a good default choice. 36 | 37 | 1. `:client-id`: use default (random uuid) 38 | 2. `:user-id-fn`: use default, ensure that you sessionize a sensible `:uid` value on user login 39 | 40 | ## Per-tab transient user-id 41 | 42 | I.e. each tab has its own user-id, and reloading a tab means a new user-id. 43 | 44 | 1. `:client-id`: use default (random uuid) 45 | 2. `:user-id-fn`: use `(fn [ring-req] (:client-id ring-req))` 46 | 47 | I.e. we don't use sessions for anything. User-ids are equal to client-ids, which are random per-tab uuids. 48 | 49 | ## Per-tab transient user-id with session security 50 | 51 | I.e. as above, but users must be signed in with a session. 52 | 53 | 1. `:client-id`: leave unchanged. 54 | 2. `:user-id-fn`: `(fn [ring-req] (str (get-in ring-req [:session :base-user-id]) "/" (:client-id ring-req)))` 55 | 56 | 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. -------------------------------------------------------------------------------- /wiki/3-Example-projects.md: -------------------------------------------------------------------------------- 1 | # Reference example 2 | 3 | [This](../tree/master/example-project) is the official example used for testing Sente, and makes a great starting point for the basics. 4 | 5 | It's kept up-to-date and includes basic client+server setup, routing, auth, CSRF protection, etc. 6 | 7 | # Community examples 8 | 9 | Please note that unofficial examples are **provided by the community** and may contain out-of-date or inaccurate information. 10 | 11 | If you spot issues with any linked examples, please **contact the relevant authors** to let them know! 12 | 13 | Contributor | Link | Description 14 | :-- | :-- | :-- 15 | [@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) 16 | [@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) 17 | [@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 18 | [@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) 19 | [@tiensonqin](https://github.com/tiensonqin) | [lymchat](https://github.com/tiensonqin/lymchat) | Chat app with [React Native](https://github.com/facebook/react-native) 20 | [@danielsz](https://github.com/danielsz) | [system-websockets](https://github.com/danielsz/system-websockets) | Client-side UI, login and wiring of components 21 | [@timothypratley](https://github.com/timothypratley) | [snakelake](https://github.com/timothypratley/snakelake) | Multiplayer snake game with screencast walkthrough 22 | [@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 23 | [@ebellani](https://github.com/ebellani) | [carpet](https://github.com/ebellani/carpet) | Web+mobile interface for a remmitance application 24 | [@danielsz](https://github.com/danielsz) | [sente-system](https://github.com/danielsz/sente-system) | Ref. example adapted for [system](https://github.com/danielsz/system) 25 | [@danielsz](https://github.com/danielsz) | [sente-boot](https://github.com/danielsz/sente-boot) | Ref. example adapted for [boot](https://github.com/boot-clj/boot) 26 | [@seancorfield](https://github.com/seancorfield) | [om-sente](https://github.com/seancorfield/om-sente) | With [Om](https://github.com/swannodette/om) 27 | [@tfoldi](https://github.com/tfoldi) | [data15-blackjack](https://github.com/tfoldi/data15-blackjack) | Multiplayer blackjack game 28 | [@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) 29 | _ | _ | Your link here? [PRs](../wiki#contributions-welcome) welcome! -------------------------------------------------------------------------------- /wiki/3-FAQ.md: -------------------------------------------------------------------------------- 1 | # What is the `user-id` provided to the server>user push fn? 2 | 3 | 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. 4 | 5 | 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**: 6 | 7 | * Each user-id may have zero _or more_ connected clients at any given time 8 | * Each user-id _may_ survive across clients (browser tabs, devices), and sessions 9 | 10 | **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.** 11 | 12 | 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. 13 | 14 | > Note that user-ids are used **only** for server>user push. client>server requests don't take a user-id. 15 | 16 | See [here](./2-Client-and-user-ids) for more info. 17 | 18 | # How do I integrate Sente with my usual login/auth procedure? 19 | 20 | This should be pretty easy to do, please see one of the [example projects](./3-Example-projects) for details! 21 | 22 | # Will Sente work with Reactjs/Reagent/Om/Pedestel/etc.? 23 | 24 | Sure! Sente's just a client<->server comms mechanism so it'll work with any view/rendering approach you'd like. 25 | 26 | 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. 27 | 28 | # What if I need to use JSON, XML, raw strings, etc.? 29 | 30 | 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. 31 | 32 | # How do I add custom Transit read and write handlers? 33 | 34 | 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. 35 | 36 | ```clj 37 | (ns my-ns.app 38 | (:require [cognitect.transit :as transit] 39 | [taoensso.sente.packers.transit :as sente-transit]) 40 | (:import [org.joda.time DateTime ReadableInstant])) 41 | 42 | ;; From https://increasinglyfunctional.com/2014/09/02/custom-transit-writers-clojure-joda-time.html 43 | (def joda-time-writer 44 | (transit/write-handler 45 | (constantly "m") 46 | (fn [v] (-> ^ReadableInstant v .getMillis)) 47 | (fn [v] (-> ^ReadableInstant v .getMillis .toString)))) 48 | 49 | (def packer (sente-transit/->TransitPacker :json {:handlers {DateTime joda-time-writer}} {})) 50 | ``` 51 | 52 | # How do I route client/server events? 53 | 54 | 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)). 55 | 56 | # Security: is there HTTPS support? 57 | 58 | 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). 59 | 60 | # Security: CSRF protection? 61 | 62 | **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). 63 | 64 | Please see one of the [example projects](./3-Example-projects) for a fully-baked example. 65 | 66 | # Pageload: How do I know when Sente is ready client-side? 67 | 68 | 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. 69 | 70 | # How can server-side channel socket events modify a user's session? 71 | 72 | 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. 73 | 74 | > For WebSocket events this is the initial Ring HTTP handshake request, for Ajax events it's just the Ring HTTP Ajax request. 75 | 76 | 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). 77 | 78 | You've got two choices: 79 | 80 | 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. 81 | 82 | 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. 83 | 84 | [@danielsz](https://github.com/danielsz) has kindly provided a detailed example [here](../issues/62#issuecomment-58790741). 85 | 86 | # Can I use Sente for large data transfers? 87 | 88 | **No**, Sente shouldn't be used to transfer payloads larger than 1MB. 89 | 90 | The reason (and easy alternative) are explained [here](./1-Getting-started#large-transfers). 91 | 92 | # Lifecycle management (component management/shutdown, etc.) 93 | 94 | Using something like [@stuartsierra/component](https://github.com/stuartsierra/component) or [@palletops/leaven](https://github.com/palletops/leaven)? 95 | 96 | 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. 97 | 98 | 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. 99 | 100 | > [1] The cost of _not_ doing this is actually negligible (a single parked go thread). 101 | 102 | There's also a couple lifecycle libraries that include Sente components: 103 | 104 | 1. [@danielsz/system](https://github.com/danielsz/system) for use with [@stuartsierra/component](https://github.com/stuartsierra/component) 105 | 2. [@palletops/bakery](https://github.com/palletops/bakery) for use with [@palletops/leaven](https://github.com/palletops/leaven) 106 | 107 | # How to debug/benchmark Sente at the protocol level? 108 | 109 | [@arichiardi](https://github.com/arichiardi) has kindly provided some notes on this [here](./4-Connection-debugging). -------------------------------------------------------------------------------- /wiki/4-Connection-debugging.md: -------------------------------------------------------------------------------- 1 | Some info is provided here on **protocol-level debugging and profiling** for Sente connections. 2 | 3 | # Ajax connections 4 | 5 | These are easily debugged and profiled via your browser's usual network tools. 6 | 7 | # WebSocket connections 8 | 9 | You can inspect Sente packets using `Wireshark` or similar tools. 10 | 11 | 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: 12 | 13 | ``` 14 | GET /chsk?client-id=bd5ee0f2-dc22-47b5-98ab-618711f34b45 HTTP/1.1 15 | Host: localhost:3000 16 | Connection: Upgrade 17 | Upgrade: websocket 18 | Origin: http://localhost:3000 19 | Sec-WebSocket-Version: 13 20 | ... 21 | ``` 22 | 23 | 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. 24 | 25 | 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: 26 | 27 | ``` 28 | `e@7.KcJam 29 | 43C-["~:chsk/handshake",["bd5ee0f2-dc22-47b5-98ab-618711f34b45",null]] 30 | ``` 31 | 32 | ## What is the `+` character I see attached to my Websocket `?ev-data`? 33 | 34 | This is a normal part of Sente's payload encoding. 35 | 36 | 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. -------------------------------------------------------------------------------- /wiki/Home.md: -------------------------------------------------------------------------------- 1 | See the **menu to the right** for content 👉 2 | 3 | # Contributions welcome 4 | 5 | **PRs very welcome** to help improve this documentation! 6 | See the [wiki](../tree/master/wiki) folder in the main repo for the relevant files. 7 | 8 | \- [Peter Taoussanis](https://www.taoensso.com) -------------------------------------------------------------------------------- /wiki/README.md: -------------------------------------------------------------------------------- 1 | # Attention! 2 | 3 | This wiki is designed for viewing from [here](../../../wiki)! 4 | 5 | Viewing from GitHub's file browser will result in **broken links**. --------------------------------------------------------------------------------