├── .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 |
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**.
--------------------------------------------------------------------------------