mirror of
https://github.com/socketio/socket.io.git
synced 2026-01-11 16:08:24 -05:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7926d96739 | ||
|
|
54871bcc53 | ||
|
|
bf114ddf2e | ||
|
|
300ee104bf | ||
|
|
d742345480 | ||
|
|
d9e60e334d | ||
|
|
2cd0dd7080 | ||
|
|
9e467381f3 |
61
.github/ISSUE_TEMPLATE/bug_report.md
vendored
61
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,61 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: 'bug'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
|
||||
Please fill the following code example:
|
||||
|
||||
Socket.IO server version: `x.y.z`
|
||||
|
||||
*Server*
|
||||
|
||||
```js
|
||||
import { Server } from "socket.io";
|
||||
|
||||
const io = new Server(3000, {});
|
||||
|
||||
io.on("connection", (socket) => {
|
||||
console.log(`connect ${socket.id}`);
|
||||
|
||||
socket.on("disconnect", () => {
|
||||
console.log(`disconnect ${socket.id}`);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Socket.IO client version: `x.y.z`
|
||||
|
||||
*Client*
|
||||
|
||||
```js
|
||||
import { io } from "socket.io-client";
|
||||
|
||||
const socket = io("ws://localhost:3000/", {});
|
||||
|
||||
socket.on("connect", () => {
|
||||
console.log(`connect ${socket.id}`);
|
||||
});
|
||||
|
||||
socket.on("disconnect", () => {
|
||||
console.log("disconnect");
|
||||
});
|
||||
```
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Platform:**
|
||||
- Device: [e.g. Samsung S8]
|
||||
- OS: [e.g. Android 9.2]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
5
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,5 +0,0 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Ask a Question
|
||||
url: https://github.com/socketio/socket.io/discussions/new?category=q-a
|
||||
about: Ask the community for help
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,20 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: 'enhancement'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
18
.github/PULL_REQUEST_TEMPLATE.md
vendored
18
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,18 +0,0 @@
|
||||
|
||||
### The kind of change this PR does introduce
|
||||
|
||||
* [x] a bug fix
|
||||
* [ ] a new feature
|
||||
* [ ] an update to the documentation
|
||||
* [ ] a code change that improves performance
|
||||
* [ ] other
|
||||
|
||||
### Current behavior
|
||||
|
||||
|
||||
### New behavior
|
||||
|
||||
|
||||
### Other information (e.g. related issues)
|
||||
|
||||
|
||||
26
.github/workflows/ci.yml
vendored
26
.github/workflows/ci.yml
vendored
@@ -1,26 +0,0 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
schedule:
|
||||
- cron: '0 0 * * 0'
|
||||
|
||||
jobs:
|
||||
test-node:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [10.x, 12.x, 14.x, 15.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: npm ci
|
||||
- run: npm test
|
||||
env:
|
||||
CI: true
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -8,7 +8,3 @@ lib-cov
|
||||
*.pid
|
||||
benchmarks/*.png
|
||||
node_modules
|
||||
coverage
|
||||
.idea
|
||||
.nyc_output
|
||||
dist/
|
||||
|
||||
3
.npmignore
Normal file
3
.npmignore
Normal file
@@ -0,0 +1,3 @@
|
||||
support
|
||||
test
|
||||
examples
|
||||
7
.travis.yml
Normal file
7
.travis.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- 0.4
|
||||
- 0.6
|
||||
|
||||
notifications:
|
||||
irc: "irc.freenode.org#socket.io"
|
||||
348
CHANGELOG.md
348
CHANGELOG.md
@@ -1,348 +0,0 @@
|
||||
# [4.0.0](https://github.com/socketio/socket.io/compare/3.1.2...4.0.0) (2021-03-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* make io.to(...) immutable ([ac9e8ca](https://github.com/socketio/socket.io/commit/ac9e8ca6c71e00d4af45ee03f590fe56f3951186))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add some utility methods ([b25495c](https://github.com/socketio/socket.io/commit/b25495c069031674da08e19aed68922c7c7a0e28))
|
||||
* add support for typed events ([#3822](https://github.com/socketio/socket.io/issues/3822)) ([0107510](https://github.com/socketio/socket.io/commit/0107510ba8a0f148c78029d8be8919b350feb633))
|
||||
* allow to exclude specific rooms when broadcasting ([#3789](https://github.com/socketio/socket.io/issues/3789)) ([7de2e87](https://github.com/socketio/socket.io/commit/7de2e87e888d849eb2dfc5e362af4c9e86044701))
|
||||
* allow to pass an array to io.to(...) ([085d1de](https://github.com/socketio/socket.io/commit/085d1de9df909651de8b313cc6f9f253374b702e))
|
||||
|
||||
|
||||
## [3.1.2](https://github.com/socketio/socket.io/compare/3.1.1...3.1.2) (2021-02-26)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* ignore packets received after disconnection ([494c64e](https://github.com/socketio/socket.io/commit/494c64e44f645cbd24c645f1186d203789e84af0))
|
||||
|
||||
|
||||
## [3.1.1](https://github.com/socketio/socket.io/compare/3.1.0...3.1.1) (2021-02-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* properly parse the CONNECT packet in v2 compatibility mode ([6f4bd7f](https://github.com/socketio/socket.io/commit/6f4bd7f8e7c41a075a8014565330a77c38b03a8d))
|
||||
* **typings:** add return types and general-case overload signatures ([#3776](https://github.com/socketio/socket.io/issues/3776)) ([9e8f288](https://github.com/socketio/socket.io/commit/9e8f288ca9f14f91064b8d3cce5946f7d23d407c))
|
||||
* **typings:** update the types of "query", "auth" and "headers" ([4f2e9a7](https://github.com/socketio/socket.io/commit/4f2e9a716d9835b550c8fd9a9b429ebf069c2895))
|
||||
|
||||
|
||||
# [3.1.0](https://github.com/socketio/socket.io/compare/3.0.5...3.1.0) (2021-01-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* confirm a weak but matching ETag ([#3485](https://github.com/socketio/socket.io/issues/3485)) ([161091d](https://github.com/socketio/socket.io/commit/161091dd4c9e1b1610ac3d45d964195e63d92b94))
|
||||
* **esm:** export the Namespace and Socket class ([#3699](https://github.com/socketio/socket.io/issues/3699)) ([233650c](https://github.com/socketio/socket.io/commit/233650c22209708b5fccc4349c38d2fa1b465d8f))
|
||||
* add support for Socket.IO v2 clients ([9925746](https://github.com/socketio/socket.io/commit/9925746c8ee3a6522bd640b5d586c83f04f2f1ba))
|
||||
* add room events ([155fa63](https://github.com/socketio/socket.io-adapter/commit/155fa6333a504036e99a33667dc0397f6aede25e))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* allow integers as event names ([1c220dd](https://github.com/socketio/socket.io-parser/commit/1c220ddbf45ea4b44bc8dbf6f9ae245f672ba1b9))
|
||||
|
||||
|
||||
## [3.0.5](https://github.com/socketio/socket.io/compare/3.0.4...3.0.5) (2021-01-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* properly clear timeout on connection failure ([170b739](https://github.com/socketio/socket.io/commit/170b739f147cb6c92b423729b877e242e376927d))
|
||||
|
||||
|
||||
### Reverts
|
||||
|
||||
* restore the socket middleware functionality ([bf54327](https://github.com/socketio/socket.io/commit/bf5432742158e4d5ba2722cff4a614967dffa5b9))
|
||||
|
||||
|
||||
## [3.0.4](https://github.com/socketio/socket.io/compare/3.0.3...3.0.4) (2020-12-07)
|
||||
|
||||
|
||||
## [3.0.3](https://github.com/socketio/socket.io/compare/3.0.2...3.0.3) (2020-11-19)
|
||||
|
||||
|
||||
## [3.0.2](https://github.com/socketio/socket.io/compare/3.0.1...3.0.2) (2020-11-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* merge Engine.IO options ([43705d7](https://github.com/socketio/socket.io/commit/43705d7a9149833afc69edc937ea7f8c9aabfeef))
|
||||
|
||||
|
||||
## [3.0.1](https://github.com/socketio/socket.io/compare/3.0.0...3.0.1) (2020-11-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* export ServerOptions and Namespace types ([#3684](https://github.com/socketio/socket.io/issues/3684)) ([f62f180](https://github.com/socketio/socket.io/commit/f62f180edafdd56d8a8a277e092bc66df0c5f07f))
|
||||
* **typings:** update the signature of the emit method ([50671d9](https://github.com/socketio/socket.io/commit/50671d984a81535a6a15c704546ca7465e2ea295))
|
||||
|
||||
|
||||
# [3.0.0](https://github.com/socketio/socket.io/compare/2.3.0...3.0.0) (2020-11-05)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* close clients with no namespace ([91cd255](https://github.com/socketio/socket.io/commit/91cd255ba76ff6a780c62740f9f5cd3a76f5d7c7))
|
||||
|
||||
### Features
|
||||
|
||||
* emit an Error object upon middleware error ([54bf4a4](https://github.com/socketio/socket.io/commit/54bf4a44e9e896dfb64764ee7bd4e8823eb7dc7b))
|
||||
* serve msgpack bundle ([aa7574f](https://github.com/socketio/socket.io/commit/aa7574f88471aa30ae472a5cddf1000a8baa70fd))
|
||||
* add support for catch-all listeners ([5c73733](https://github.com/socketio/socket.io/commit/5c737339858d59eab4b5ee2dd6feff0e82c4fe5a))
|
||||
* make Socket#join() and Socket#leave() synchronous ([129c641](https://github.com/socketio/socket.io/commit/129c6417bd818bc8b4e1b831644323876e627c13))
|
||||
* remove prod dependency to socket.io-client ([7603da7](https://github.com/socketio/socket.io/commit/7603da71a535481e3fc60e38b013abf78516d322))
|
||||
* move binary detection back to the parser ([669592d](https://github.com/socketio/socket.io/commit/669592d120409a5cf00f128070dee6d22259ba4f))
|
||||
* add ES6 module export ([8b6b100](https://github.com/socketio/socket.io/commit/8b6b100c284ccce7d85e55659e3397f533916847))
|
||||
* do not reuse the Engine.IO id ([2875d2c](https://github.com/socketio/socket.io/commit/2875d2cfdfa463e64cb520099749f543bbc4eb15))
|
||||
* remove Server#set() method ([029f478](https://github.com/socketio/socket.io/commit/029f478992f59b1eb5226453db46363a570eea46))
|
||||
* remove Socket#rooms object ([1507b41](https://github.com/socketio/socket.io/commit/1507b416d584381554d1ed23c9aaf3b650540071))
|
||||
* remove the 'origins' option ([a8c0600](https://github.com/socketio/socket.io/commit/a8c06006098b512ba1b8b8df82777349db486f41))
|
||||
* remove the implicit connection to the default namespace ([3289f7e](https://github.com/socketio/socket.io/commit/3289f7ec376e9ec88c2f90e2735c8ca8d01c0e97))
|
||||
* throw upon reserved event names ([4bd5b23](https://github.com/socketio/socket.io/commit/4bd5b2339a66a5a675e20f689fff2e70ff12d236))
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* the Socket#use() method is removed (see [5c73733](https://github.com/socketio/socket.io/commit/5c737339858d59eab4b5ee2dd6feff0e82c4fe5a))
|
||||
|
||||
* Socket#join() and Socket#leave() do not accept a callback argument anymore.
|
||||
|
||||
Before:
|
||||
|
||||
```js
|
||||
socket.join("room1", () => {
|
||||
io.to("room1").emit("hello");
|
||||
});
|
||||
```
|
||||
|
||||
After:
|
||||
|
||||
```js
|
||||
socket.join("room1");
|
||||
io.to("room1").emit("hello");
|
||||
// or await socket.join("room1"); for custom adapters
|
||||
```
|
||||
|
||||
* the "connected" map is renamed to "sockets"
|
||||
* the Socket#binary() method is removed, as this use case is now covered by the ability to provide your own parser.
|
||||
* the 'origins' option is removed
|
||||
|
||||
Before:
|
||||
|
||||
```js
|
||||
new Server(3000, {
|
||||
origins: ["https://example.com"]
|
||||
});
|
||||
```
|
||||
|
||||
The 'origins' option was used in the allowRequest method, in order to
|
||||
determine whether the request should pass or not. And the Engine.IO
|
||||
server would implicitly add the necessary Access-Control-Allow-xxx
|
||||
headers.
|
||||
|
||||
After:
|
||||
|
||||
```js
|
||||
new Server(3000, {
|
||||
cors: {
|
||||
origin: "https://example.com",
|
||||
methods: ["GET", "POST"],
|
||||
allowedHeaders: ["content-type"]
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
The already existing 'allowRequest' option can be used for validation:
|
||||
|
||||
```js
|
||||
new Server(3000, {
|
||||
allowRequest: (req, callback) => {
|
||||
callback(null, req.headers.referer.startsWith("https://example.com"));
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
* Socket#rooms is now a Set instead of an object
|
||||
|
||||
* Namespace#connected is now a Map instead of an object
|
||||
|
||||
* there is no more implicit connection to the default namespace:
|
||||
|
||||
```js
|
||||
// client-side
|
||||
const socket = io("/admin");
|
||||
|
||||
// server-side
|
||||
io.on("connect", socket => {
|
||||
// not triggered anymore
|
||||
})
|
||||
|
||||
io.use((socket, next) => {
|
||||
// not triggered anymore
|
||||
});
|
||||
|
||||
io.of("/admin").use((socket, next) => {
|
||||
// triggered
|
||||
});
|
||||
```
|
||||
|
||||
* the Server#set() method was removed
|
||||
|
||||
This method was kept for backward-compatibility with pre-1.0 versions.
|
||||
|
||||
|
||||
# [3.0.0-rc4](https://github.com/socketio/socket.io/compare/3.0.0-rc3...3.0.0-rc4) (2020-10-30)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* emit an Error object upon middleware error ([54bf4a4](https://github.com/socketio/socket.io/commit/54bf4a44e9e896dfb64764ee7bd4e8823eb7dc7b))
|
||||
* serve msgpack bundle ([aa7574f](https://github.com/socketio/socket.io/commit/aa7574f88471aa30ae472a5cddf1000a8baa70fd))
|
||||
|
||||
|
||||
|
||||
# [3.0.0-rc3](https://github.com/socketio/socket.io/compare/3.0.0-rc2...3.0.0-rc3) (2020-10-26)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add support for catch-all listeners ([5c73733](https://github.com/socketio/socket.io/commit/5c737339858d59eab4b5ee2dd6feff0e82c4fe5a))
|
||||
* make Socket#join() and Socket#leave() synchronous ([129c641](https://github.com/socketio/socket.io/commit/129c6417bd818bc8b4e1b831644323876e627c13))
|
||||
* remove prod dependency to socket.io-client ([7603da7](https://github.com/socketio/socket.io/commit/7603da71a535481e3fc60e38b013abf78516d322))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* the Socket#use() method is removed (see [5c73733](https://github.com/socketio/socket.io/commit/5c737339858d59eab4b5ee2dd6feff0e82c4fe5a))
|
||||
|
||||
* Socket#join() and Socket#leave() do not accept a callback argument anymore.
|
||||
|
||||
Before:
|
||||
|
||||
```js
|
||||
socket.join("room1", () => {
|
||||
io.to("room1").emit("hello");
|
||||
});
|
||||
```
|
||||
|
||||
After:
|
||||
|
||||
```js
|
||||
socket.join("room1");
|
||||
io.to("room1").emit("hello");
|
||||
// or await socket.join("room1"); for custom adapters
|
||||
```
|
||||
|
||||
|
||||
|
||||
# [3.0.0-rc2](https://github.com/socketio/socket.io/compare/3.0.0-rc1...3.0.0-rc2) (2020-10-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* close clients with no namespace ([91cd255](https://github.com/socketio/socket.io/commit/91cd255ba76ff6a780c62740f9f5cd3a76f5d7c7))
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* remove duplicate _sockets map ([8a5db7f](https://github.com/socketio/socket.io/commit/8a5db7fa36a075da75cde43cd4fb6382b7659953))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* move binary detection back to the parser ([669592d](https://github.com/socketio/socket.io/commit/669592d120409a5cf00f128070dee6d22259ba4f))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* the "connected" map is renamed to "sockets"
|
||||
* the Socket#binary() method is removed, as this use case is now covered by the ability to provide your own parser.
|
||||
|
||||
|
||||
|
||||
# [3.0.0-rc1](https://github.com/socketio/socket.io/compare/2.3.0...3.0.0-rc1) (2020-10-13)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add ES6 module export ([8b6b100](https://github.com/socketio/socket.io/commit/8b6b100c284ccce7d85e55659e3397f533916847))
|
||||
* do not reuse the Engine.IO id ([2875d2c](https://github.com/socketio/socket.io/commit/2875d2cfdfa463e64cb520099749f543bbc4eb15))
|
||||
* remove Server#set() method ([029f478](https://github.com/socketio/socket.io/commit/029f478992f59b1eb5226453db46363a570eea46))
|
||||
* remove Socket#rooms object ([1507b41](https://github.com/socketio/socket.io/commit/1507b416d584381554d1ed23c9aaf3b650540071))
|
||||
* remove the 'origins' option ([a8c0600](https://github.com/socketio/socket.io/commit/a8c06006098b512ba1b8b8df82777349db486f41))
|
||||
* remove the implicit connection to the default namespace ([3289f7e](https://github.com/socketio/socket.io/commit/3289f7ec376e9ec88c2f90e2735c8ca8d01c0e97))
|
||||
* throw upon reserved event names ([4bd5b23](https://github.com/socketio/socket.io/commit/4bd5b2339a66a5a675e20f689fff2e70ff12d236))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* the 'origins' option is removed
|
||||
|
||||
Before:
|
||||
|
||||
```js
|
||||
new Server(3000, {
|
||||
origins: ["https://example.com"]
|
||||
});
|
||||
```
|
||||
|
||||
The 'origins' option was used in the allowRequest method, in order to
|
||||
determine whether the request should pass or not. And the Engine.IO
|
||||
server would implicitly add the necessary Access-Control-Allow-xxx
|
||||
headers.
|
||||
|
||||
After:
|
||||
|
||||
```js
|
||||
new Server(3000, {
|
||||
cors: {
|
||||
origin: "https://example.com",
|
||||
methods: ["GET", "POST"],
|
||||
allowedHeaders: ["content-type"]
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
The already existing 'allowRequest' option can be used for validation:
|
||||
|
||||
```js
|
||||
new Server(3000, {
|
||||
allowRequest: (req, callback) => {
|
||||
callback(null, req.headers.referer.startsWith("https://example.com"));
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
* Socket#rooms is now a Set instead of an object
|
||||
|
||||
* Namespace#connected is now a Map instead of an object
|
||||
|
||||
* there is no more implicit connection to the default namespace:
|
||||
|
||||
```js
|
||||
// client-side
|
||||
const socket = io("/admin");
|
||||
|
||||
// server-side
|
||||
io.on("connect", socket => {
|
||||
// not triggered anymore
|
||||
})
|
||||
|
||||
io.use((socket, next) => {
|
||||
// not triggered anymore
|
||||
});
|
||||
|
||||
io.of("/admin").use((socket, next) => {
|
||||
// triggered
|
||||
});
|
||||
```
|
||||
|
||||
* the Server#set() method was removed
|
||||
|
||||
This method was kept for backward-compatibility with pre-1.0 versions.
|
||||
|
||||
187
History.md
Normal file
187
History.md
Normal file
@@ -0,0 +1,187 @@
|
||||
|
||||
0.8.7 / 2011-11-05
|
||||
==================
|
||||
|
||||
* Fixed memory leaks in closed clients.
|
||||
* Fixed memory leaks in namespaces.
|
||||
* Fixed websocket handling for malformed requests from proxies. [einaros]
|
||||
* Node 0.6 compatibility. [einaros] [3rd-Eden]
|
||||
* Adapted tests and examples.
|
||||
|
||||
0.8.6 / 2011-10-27
|
||||
==================
|
||||
|
||||
* Added JSON decoding on jsonp-polling transport.
|
||||
* Fixed README example.
|
||||
* Major speed optimizations [3rd-Eden] [einaros] [visionmedia]
|
||||
* Added decode/encode benchmarks [visionmedia]
|
||||
* Added support for black-listing client sent events.
|
||||
* Fixed logging options, closes #540 [3rd-Eden]
|
||||
* Added vary header for gzip [3rd-Eden]
|
||||
* Properly cleaned up async websocket / flashsocket tests, after patching node-websocket-client
|
||||
* Patched to properly shut down when a finishClose call is made during connection establishment
|
||||
* Added support for socket.io version on url and far-future Expires [3rd-Eden] [getify]
|
||||
* Began IE10 compatibility [einaros] [tbranyen]
|
||||
* Misc WebSocket fixes [einaros]
|
||||
* Added UTF8 to respone headers for htmlfile [3rd-Eden]
|
||||
|
||||
0.8.5 / 2011-10-07
|
||||
==================
|
||||
|
||||
* Added websocket draft HyBi-16 support. [einaros]
|
||||
* Fixed websocket continuation bugs. [einaros]
|
||||
* Fixed flashsocket transport name.
|
||||
* Fixed websocket tests.
|
||||
* Ensured `parser#decodePayload` doesn't choke.
|
||||
* Added http referrer verification to manager verifyOrigin.
|
||||
* Added access control for cross domain xhr handshakes [3rd-Eden]
|
||||
* Added support for automatic generation of socket.io files [3rd-Eden]
|
||||
* Added websocket binary support [einaros]
|
||||
* Added gzip support for socket.io.js [3rd-Eden]
|
||||
* Expose socket.transport [3rd-Eden]
|
||||
* Updated client.
|
||||
|
||||
0.8.4 / 2011-09-06
|
||||
==================
|
||||
|
||||
* Client build
|
||||
|
||||
0.8.3 / 2011-09-03
|
||||
==================
|
||||
|
||||
* Fixed `\n` parsing for non-JSON packets (fixes #479).
|
||||
* Fixed parsing of certain unicode characters (fixes #451).
|
||||
* Fixed transport message packet logging.
|
||||
* Fixed emission of `error` event resulting in an uncaught exception if unhandled (fixes #476).
|
||||
* Fixed; allow for falsy values as the configuration value of `log level` (fixes #491).
|
||||
* Fixed repository URI in `package.json`. Fixes #504.
|
||||
* Added text/plain content-type to handshake responses [einaros]
|
||||
* Improved single byte writes [einaros]
|
||||
* Updated socket.io-flashsocket default port from 843 to 10843 [3rd-Eden]
|
||||
* Updated client.
|
||||
|
||||
0.8.2 / 2011-08-29
|
||||
==================
|
||||
|
||||
* Updated client.
|
||||
|
||||
0.8.1 / 2011-08-29
|
||||
==================
|
||||
|
||||
* Fixed utf8 bug in send framing in websocket [einaros]
|
||||
* Fixed typo in docs [Znarkus]
|
||||
* Fixed bug in send framing for over 64kB of data in websocket [einaros]
|
||||
* Corrected ping handling in websocket transport [einaros]
|
||||
|
||||
0.8.0 / 2011-08-28
|
||||
==================
|
||||
|
||||
* Updated to work with two-level websocket versioning. [einaros]
|
||||
* Added hybi07 support. [einaros]
|
||||
* Added hybi10 support. [einaros]
|
||||
* Added http referrer verification to manager.js verifyOrigin. [einaors]
|
||||
|
||||
0.7.11 / 2011-08-27
|
||||
===================
|
||||
|
||||
* Updated socket.io-client.
|
||||
|
||||
0.7.10 / 2011-08-27
|
||||
===================
|
||||
|
||||
* Updated socket.io-client.
|
||||
|
||||
0.7.9 / 2011-08-12
|
||||
==================
|
||||
|
||||
* Updated socket.io-client.
|
||||
* Make sure we only do garbage collection when the server we receive is actually run.
|
||||
|
||||
0.7.8 / 2011-08-08
|
||||
==================
|
||||
|
||||
* Changed; make sure sio#listen passes options to both HTTP server and socket.io manager.
|
||||
* Added docs for sio#listen.
|
||||
* Added options parameter support for Manager constructor.
|
||||
* Added memory leaks tests and test-leaks Makefile task.
|
||||
* Removed auto npm-linking from make test.
|
||||
* Make sure that you can disable heartbeats. [3rd-Eden]
|
||||
* Fixed rooms memory leak [3rd-Eden]
|
||||
* Send response once we got all POST data, not immediately [Pita]
|
||||
* Fixed onLeave behavior with missing clientsk [3rd-Eden]
|
||||
* Prevent duplicate references in rooms.
|
||||
* Added alias for `to` to `in` and `in` to `to`.
|
||||
* Fixed roomClients definition.
|
||||
* Removed dependency on redis for installation without npm [3rd-Eden]
|
||||
* Expose path and querystring in handshakeData [3rd-Eden]
|
||||
|
||||
0.7.7 / 2011-07-12
|
||||
==================
|
||||
|
||||
* Fixed double dispatch handling with emit to closed clients.
|
||||
* Added test for emitting to closed clients to prevent regression.
|
||||
* Fixed race condition in redis test.
|
||||
* Changed Transport#end instrumentation.
|
||||
* Leveraged $emit instead of emit internally.
|
||||
* Made tests faster.
|
||||
* Fixed double disconnect events.
|
||||
* Fixed disconnect logic
|
||||
* Simplified remote events handling in Socket.
|
||||
* Increased testcase timeout.
|
||||
* Fixed unknown room emitting (GH-291). [3rd-Eden]
|
||||
* Fixed `address` in handshakeData. [3rd-Eden]
|
||||
* Removed transports definition in chat example.
|
||||
* Fixed room cleanup
|
||||
* Fixed; make sure the client is cleaned up after booting.
|
||||
* Make sure to mark the client as non-open if the connection is closed.
|
||||
* Removed unneeded `buffer` declarations.
|
||||
* Fixed; make sure to clear socket handlers and subscriptions upon transport close.
|
||||
|
||||
0.7.6 / 2011-06-30
|
||||
==================
|
||||
|
||||
* Fixed general dispatching when a client has closed.
|
||||
|
||||
0.7.5 / 2011-06-30
|
||||
==================
|
||||
|
||||
* Fixed dispatching to clients that are disconnected.
|
||||
|
||||
0.7.4 / 2011-06-30
|
||||
==================
|
||||
|
||||
* Fixed; only clear handlers if they were set. [level09]
|
||||
|
||||
0.7.3 / 2011-06-30
|
||||
==================
|
||||
|
||||
* Exposed handshake data to clients.
|
||||
* Refactored dispatcher interface.
|
||||
* Changed; Moved id generation method into the manager.
|
||||
* Added sub-namespace authorization. [3rd-Eden]
|
||||
* Changed; normalized SocketNamespace local eventing [dvv]
|
||||
* Changed; Use packet.reason or default to 'packet' [3rd-Eden]
|
||||
* Changed console.error to console.log.
|
||||
* Fixed; bind both servers at the same time do that the test never times out.
|
||||
* Added 304 support.
|
||||
* Removed `Transport#name` for abstract interface.
|
||||
* Changed; lazily require http and https module only when needed. [3rd-Eden]
|
||||
|
||||
0.7.2 / 2011-06-22
|
||||
==================
|
||||
|
||||
* Make sure to write a packet (of type `noop`) when closing a poll.
|
||||
This solves a problem with cross-domain requests being flagged as aborted and
|
||||
reconnection being triggered.
|
||||
* Added `noop` message type.
|
||||
|
||||
0.7.1 / 2011-06-21
|
||||
==================
|
||||
|
||||
* Fixed cross-domain XHR.
|
||||
* Added CORS test to xhr-polling suite.
|
||||
|
||||
0.7.0 / 2010-06-21
|
||||
==================
|
||||
|
||||
* http://socket.io/announcement.html
|
||||
22
LICENSE
22
LICENSE
@@ -1,22 +0,0 @@
|
||||
(The MIT License)
|
||||
|
||||
Copyright (c) 2014-2018 Automattic <dev@cloudup.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
'Software'), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
18
Makefile
Normal file
18
Makefile
Normal file
@@ -0,0 +1,18 @@
|
||||
|
||||
TESTS = test/*.js
|
||||
BENCHMARKS = $(shell find bench -type f ! -name 'runner.js')
|
||||
REPORTER = dot
|
||||
|
||||
test:
|
||||
@./node_modules/.bin/mocha \
|
||||
--require test/common \
|
||||
--reporter $(REPORTER) \
|
||||
--slow 500ms \
|
||||
--bail \
|
||||
--growl \
|
||||
$(TESTS)
|
||||
|
||||
bench:
|
||||
@node $(PROFILEFLAGS) bench/runner.js $(BENCHMARKS)
|
||||
|
||||
.PHONY: test bench
|
||||
426
Readme.md
426
Readme.md
@@ -1,263 +1,277 @@
|
||||
# socket.io
|
||||
[](https://repl.it/github/socketio/socket.io)
|
||||
[](#backers) [](#sponsors)
|
||||
[](https://github.com/socketio/socket.io/actions)
|
||||
[](https://david-dm.org/socketio/socket.io)
|
||||
[](https://david-dm.org/socketio/socket.io#info=devDependencies)
|
||||
[](https://www.npmjs.com/package/socket.io)
|
||||

|
||||
[](https://slackin-socketio.now.sh)
|
||||
# Socket.IO
|
||||
|
||||
## Features
|
||||
Socket.IO is a Node.JS framework for making realtime applications.
|
||||
|
||||
Socket.IO enables real-time bidirectional event-based communication. It consists of:
|
||||
It brings the best of WebSockets and other data transport mechanisms for
|
||||
blazing fast realtime data exchange in web browsers, mobile devices and
|
||||
servers.
|
||||
|
||||
- a Node.js server (this repository)
|
||||
- a [Javascript client library](https://github.com/socketio/socket.io-client) for the browser (or a Node.js client)
|
||||
## How to Install
|
||||
|
||||
Some implementations in other languages are also available:
|
||||
|
||||
- [Java](https://github.com/socketio/socket.io-client-java)
|
||||
- [C++](https://github.com/socketio/socket.io-client-cpp)
|
||||
- [Swift](https://github.com/socketio/socket.io-client-swift)
|
||||
- [Dart](https://github.com/rikulo/socket.io-client-dart)
|
||||
- [Python](https://github.com/miguelgrinberg/python-socketio)
|
||||
- [.Net](https://github.com/Quobject/SocketIoClientDotNet)
|
||||
|
||||
Its main features are:
|
||||
|
||||
#### Reliability
|
||||
|
||||
Connections are established even in the presence of:
|
||||
- proxies and load balancers.
|
||||
- personal firewall and antivirus software.
|
||||
|
||||
For this purpose, it relies on [Engine.IO](https://github.com/socketio/engine.io), which first establishes a long-polling connection, then tries to upgrade to better transports that are "tested" on the side, like WebSocket. Please see the [Goals](https://github.com/socketio/engine.io#goals) section for more information.
|
||||
|
||||
#### Auto-reconnection support
|
||||
|
||||
Unless instructed otherwise a disconnected client will try to reconnect forever, until the server is available again. Please see the available reconnection options [here](https://socket.io/docs/v3/client-api/#new-Manager-url-options).
|
||||
|
||||
#### Disconnection detection
|
||||
|
||||
A heartbeat mechanism is implemented at the Engine.IO level, allowing both the server and the client to know when the other one is not responding anymore.
|
||||
|
||||
That functionality is achieved with timers set on both the server and the client, with timeout values (the `pingInterval` and `pingTimeout` parameters) shared during the connection handshake. Those timers require any subsequent client calls to be directed to the same server, hence the `sticky-session` requirement when using multiples nodes.
|
||||
|
||||
#### Binary support
|
||||
|
||||
Any serializable data structures can be emitted, including:
|
||||
|
||||
- [ArrayBuffer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer) and [Blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob) in the browser
|
||||
- [ArrayBuffer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer) and [Buffer](https://nodejs.org/api/buffer.html) in Node.js
|
||||
|
||||
#### Simple and convenient API
|
||||
|
||||
Sample code:
|
||||
|
||||
```js
|
||||
io.on('connection', socket => {
|
||||
socket.emit('request', /* … */); // emit an event to the socket
|
||||
io.emit('broadcast', /* … */); // emit an event to all connected sockets
|
||||
socket.on('reply', () => { /* … */ }); // listen to the event
|
||||
});
|
||||
```
|
||||
|
||||
#### Cross-browser
|
||||
|
||||
Browser support is tested in Sauce Labs:
|
||||
|
||||
[](https://saucelabs.com/u/socket)
|
||||
|
||||
#### Multiplexing support
|
||||
|
||||
In order to create separation of concerns within your application (for example per module, or based on permissions), Socket.IO allows you to create several `Namespaces`, which will act as separate communication channels but will share the same underlying connection.
|
||||
|
||||
#### Room support
|
||||
|
||||
Within each `Namespace`, you can define arbitrary channels, called `Rooms`, that sockets can join and leave. You can then broadcast to any given room, reaching every socket that has joined it.
|
||||
|
||||
This is a useful feature to send notifications to a group of users, or to a given user connected on several devices for example.
|
||||
|
||||
|
||||
**Note:** Socket.IO is not a WebSocket implementation. Although Socket.IO indeed uses WebSocket as a transport when possible, it adds some metadata to each packet: the packet type, the namespace and the ack id when a message acknowledgement is needed. That is why a WebSocket client will not be able to successfully connect to a Socket.IO server, and a Socket.IO client will not be able to connect to a WebSocket server (like `ws://echo.websocket.org`) either. Please see the protocol specification [here](https://github.com/socketio/socket.io-protocol).
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
// with npm
|
||||
npm install socket.io
|
||||
|
||||
// with yarn
|
||||
yarn add socket.io
|
||||
```
|
||||
|
||||
## How to use
|
||||
|
||||
The following example attaches socket.io to a plain Node.JS
|
||||
HTTP server listening on port `3000`.
|
||||
First, require `socket.io` and attach it to a HTTP/HTTPS server:
|
||||
|
||||
```js
|
||||
const server = require('http').createServer();
|
||||
const io = require('socket.io')(server);
|
||||
io.on('connection', client => {
|
||||
client.on('event', data => { /* … */ });
|
||||
client.on('disconnect', () => { /* … */ });
|
||||
var app = express.createServer()
|
||||
, io = require('socket.io')(app);
|
||||
|
||||
app.listen(80);
|
||||
|
||||
io.on('connection', function (socket) {
|
||||
socket.emit('news', { hello: 'world' });
|
||||
socket.on('my other event', function (data) {
|
||||
console.log(data);
|
||||
});
|
||||
});
|
||||
server.listen(3000);
|
||||
```
|
||||
|
||||
### Standalone
|
||||
Finally, load it from the client side code:
|
||||
|
||||
```html
|
||||
<script src="/socket.io/socket.io.js"></script>
|
||||
<script>
|
||||
var socket = io.connect('http://localhost');
|
||||
socket.on('news', function (data) {
|
||||
console.log(data);
|
||||
socket.emit('my other event', { my: 'data' });
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
For more thorough examples, look at the `examples/` directory.
|
||||
|
||||
## Short recipes
|
||||
|
||||
### Sending and receiving events.
|
||||
|
||||
Socket.IO allows you to emit and receive custom events.
|
||||
Besides `connect`, `message` and `disconnect`, you can emit custom events:
|
||||
|
||||
```js
|
||||
const io = require('socket.io')();
|
||||
io.on('connection', client => { ... });
|
||||
io.listen(3000);
|
||||
// note, io(<port>) will create a http server for you
|
||||
var io = require('socket.io')(80);
|
||||
|
||||
io.on('connection', function (socket) {
|
||||
io.emit('this', { will: 'be received by everyone' });
|
||||
|
||||
socket.on('private message', function (from, msg) {
|
||||
console.log('I received a private message by ', from, ' saying ', msg);
|
||||
});
|
||||
|
||||
socket.on('disconnect', function () {
|
||||
io.emit('user disconnected');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### In conjunction with Express
|
||||
### Storing data associated to a client
|
||||
|
||||
Starting with **3.0**, express applications have become request handler
|
||||
functions that you pass to `http` or `http` `Server` instances. You need
|
||||
to pass the `Server` to `socket.io`, and not the express application
|
||||
function. Also make sure to call `.listen` on the `server`, not the `app`.
|
||||
Sometimes it's necessary to store data associated with a client that's
|
||||
necessary for the duration of the session.
|
||||
|
||||
#### Server side
|
||||
|
||||
```js
|
||||
const app = require('express')();
|
||||
const server = require('http').createServer(app);
|
||||
const io = require('socket.io')(server);
|
||||
io.on('connection', () => { /* … */ });
|
||||
server.listen(3000);
|
||||
var io = require('socket.io')(80);
|
||||
|
||||
io.on('connection', function (socket) {
|
||||
socket.on('set nickname', function (name) {
|
||||
socket.set('nickname', name, function () { socket.emit('ready'); });
|
||||
});
|
||||
|
||||
socket.on('msg', function () {
|
||||
socket.get('nickname', function (err, name) {
|
||||
console.log('Chat message by ', name);
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### In conjunction with Koa
|
||||
#### Client side
|
||||
|
||||
Like Express.JS, Koa works by exposing an application as a request
|
||||
handler function, but only by calling the `callback` method.
|
||||
```html
|
||||
<script>
|
||||
var socket = io.connect('http://localhost');
|
||||
|
||||
socket.on('connect', function () {
|
||||
socket.emit('set nickname', confirm('What is your nickname?'));
|
||||
socket.on('ready', function () {
|
||||
console.log('Connected !');
|
||||
socket.emit('msg', confirm('What is your message?'));
|
||||
});
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
#### Client side
|
||||
|
||||
In the client side, messages are received the same way whether they're volatile
|
||||
or not.
|
||||
|
||||
### Getting acknowledgements
|
||||
|
||||
Sometimes, you might want to get a callback when the client confirmed the message
|
||||
reception.
|
||||
|
||||
To do this, simply pass a function as the last parameter of `.emit`.
|
||||
What's more, when you use `.emit`, the acknowledgement is done by you, which
|
||||
means you can also pass data along:
|
||||
|
||||
#### Server side
|
||||
|
||||
```js
|
||||
const app = require('koa')();
|
||||
const server = require('http').createServer(app.callback());
|
||||
const io = require('socket.io')(server);
|
||||
io.on('connection', () => { /* … */ });
|
||||
server.listen(3000);
|
||||
var io = require('socket.io')(80);
|
||||
|
||||
io.on('connection', function (socket) {
|
||||
socket.on('ferret', function (name, fn) {
|
||||
fn('woot');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### In conjunction with Fastify
|
||||
#### Client side
|
||||
|
||||
To integrate Socket.io in your Fastify application you just need to
|
||||
register `fastify-socket.io` plugin. It will create a `decorator`
|
||||
called `io`.
|
||||
```html
|
||||
<script>
|
||||
var socket = io.connect(); // TIP: .connect with no args does auto-discovery
|
||||
socket.on('connect', function () { // TIP: you can avoid listening on `connect` and listen on events directly too!
|
||||
socket.emit('ferret', 'tobi', function (data) {
|
||||
console.log(data); // data will be 'woot'
|
||||
});
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
### Broadcasting messages
|
||||
|
||||
To broadcast, simply add a `broadcast` flag to `emit` and `send` method calls.
|
||||
Broadcasting means sending a message to everyone else except for the socket
|
||||
that starts it.
|
||||
|
||||
#### Server side
|
||||
|
||||
```js
|
||||
const app = require('fastify')();
|
||||
app.register(require('fastify-socket.io'));
|
||||
app.io.on('connection', () => { /* … */ });
|
||||
app.listen(3000);
|
||||
var io = require('socket.io')(80);
|
||||
|
||||
io.on('connection', function (socket) {
|
||||
socket.broadcast.emit('user connected');
|
||||
});
|
||||
```
|
||||
|
||||
## Documentation
|
||||
### Rooms
|
||||
|
||||
Please see the documentation [here](https://socket.io/docs/).
|
||||
Sometimes you want to put certain sockets in the same room, so that it's easy
|
||||
to broadcast to all of them together.
|
||||
|
||||
The source code of the website can be found [here](https://github.com/socketio/socket.io-website). Contributions are welcome!
|
||||
Think of this as built-in channels for sockets. Sockets `join` and `leave`
|
||||
rooms in each socket.
|
||||
|
||||
## Debug / logging
|
||||
#### Server side
|
||||
|
||||
Socket.IO is powered by [debug](https://github.com/visionmedia/debug).
|
||||
In order to see all the debug output, run your app with the environment variable
|
||||
`DEBUG` including the desired scope.
|
||||
```js
|
||||
var io = require('socket.io')(80);
|
||||
|
||||
To see the output from all of Socket.IO's debugging scopes you can use:
|
||||
|
||||
```
|
||||
DEBUG=socket.io* node myapp
|
||||
io.on('connection', function (socket) {
|
||||
socket.join('justin bieber fans');
|
||||
socket.broadcast.to('justin bieber fans').emit('new fan');
|
||||
io.to('rammstein fans').emit('new non-fan');
|
||||
});
|
||||
```
|
||||
|
||||
## Testing
|
||||
### Using it just as a cross-browser WebSocket
|
||||
|
||||
If you just want the WebSocket semantics, you can do that too.
|
||||
Simply leverage `send` and listen on the `message` event:
|
||||
|
||||
#### Server side
|
||||
|
||||
```js
|
||||
var io = require('socket.io')(80);
|
||||
|
||||
io.on('connection', function (socket) {
|
||||
socket.on('message', function () { });
|
||||
socket.on('disconnect', function () { });
|
||||
});
|
||||
```
|
||||
npm test
|
||||
|
||||
#### Client side
|
||||
|
||||
```html
|
||||
<script>
|
||||
var socket = io.connect('http://localhost/');
|
||||
socket.on('connect', function () {
|
||||
socket.send('hi');
|
||||
|
||||
socket.on('message', function (msg) {
|
||||
// my msg
|
||||
});
|
||||
});
|
||||
</script>
|
||||
```
|
||||
This runs the `gulp` task `test`. By default the test will be run with the source code in `lib` directory.
|
||||
|
||||
Set the environmental variable `TEST_VERSION` to `compat` to test the transpiled es5-compat version of the code.
|
||||
### Messaging between clients
|
||||
|
||||
The `gulp` task `test` will always transpile the source code into es5 and export to `dist` first before running the test.
|
||||
Each client joins its own group, identified by the client id, which means you
|
||||
can leverage `to` to message a particular client:
|
||||
|
||||
```js
|
||||
var io = require('socket.io')(80);
|
||||
io.on('connection', function (socket) {
|
||||
socket.on('salutate', function (id) {
|
||||
io.to(id)
|
||||
.emit('hi!')
|
||||
.on('disconnect', function () {
|
||||
socket.emit('friend disconnected');
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Backers
|
||||
### Changing configuration
|
||||
|
||||
Support us with a monthly donation and help us continue our activities. [[Become a backer](https://opencollective.com/socketio#backer)]
|
||||
Configuration in socket.io is TJ-style:
|
||||
|
||||
<a href="https://opencollective.com/socketio/backer/0/website" target="_blank"><img src="https://opencollective.com/socketio/backer/0/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/socketio/backer/1/website" target="_blank"><img src="https://opencollective.com/socketio/backer/1/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/socketio/backer/2/website" target="_blank"><img src="https://opencollective.com/socketio/backer/2/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/socketio/backer/3/website" target="_blank"><img src="https://opencollective.com/socketio/backer/3/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/socketio/backer/4/website" target="_blank"><img src="https://opencollective.com/socketio/backer/4/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/socketio/backer/5/website" target="_blank"><img src="https://opencollective.com/socketio/backer/5/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/socketio/backer/6/website" target="_blank"><img src="https://opencollective.com/socketio/backer/6/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/socketio/backer/7/website" target="_blank"><img src="https://opencollective.com/socketio/backer/7/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/socketio/backer/8/website" target="_blank"><img src="https://opencollective.com/socketio/backer/8/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/socketio/backer/9/website" target="_blank"><img src="https://opencollective.com/socketio/backer/9/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/socketio/backer/10/website" target="_blank"><img src="https://opencollective.com/socketio/backer/10/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/socketio/backer/11/website" target="_blank"><img src="https://opencollective.com/socketio/backer/11/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/socketio/backer/12/website" target="_blank"><img src="https://opencollective.com/socketio/backer/12/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/socketio/backer/13/website" target="_blank"><img src="https://opencollective.com/socketio/backer/13/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/socketio/backer/14/website" target="_blank"><img src="https://opencollective.com/socketio/backer/14/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/socketio/backer/15/website" target="_blank"><img src="https://opencollective.com/socketio/backer/15/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/socketio/backer/16/website" target="_blank"><img src="https://opencollective.com/socketio/backer/16/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/socketio/backer/17/website" target="_blank"><img src="https://opencollective.com/socketio/backer/17/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/socketio/backer/18/website" target="_blank"><img src="https://opencollective.com/socketio/backer/18/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/socketio/backer/19/website" target="_blank"><img src="https://opencollective.com/socketio/backer/19/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/socketio/backer/20/website" target="_blank"><img src="https://opencollective.com/socketio/backer/20/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/socketio/backer/21/website" target="_blank"><img src="https://opencollective.com/socketio/backer/21/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/socketio/backer/22/website" target="_blank"><img src="https://opencollective.com/socketio/backer/22/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/socketio/backer/23/website" target="_blank"><img src="https://opencollective.com/socketio/backer/23/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/socketio/backer/24/website" target="_blank"><img src="https://opencollective.com/socketio/backer/24/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/socketio/backer/25/website" target="_blank"><img src="https://opencollective.com/socketio/backer/25/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/socketio/backer/26/website" target="_blank"><img src="https://opencollective.com/socketio/backer/26/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/socketio/backer/27/website" target="_blank"><img src="https://opencollective.com/socketio/backer/27/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/socketio/backer/28/website" target="_blank"><img src="https://opencollective.com/socketio/backer/28/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/socketio/backer/29/website" target="_blank"><img src="https://opencollective.com/socketio/backer/29/avatar.svg"></a>
|
||||
#### Server side
|
||||
|
||||
```js
|
||||
var io = require('socket.io')(80);
|
||||
|
||||
## Sponsors
|
||||
io.configure(function () {
|
||||
io.set('transports', ['websocket', 'flashsocket', 'xhr-polling']);
|
||||
});
|
||||
|
||||
Become a sponsor and get your logo on our README on Github with a link to your site. [[Become a sponsor](https://opencollective.com/socketio#sponsor)]
|
||||
io.configure('development', function () {
|
||||
io.set('transports', ['websocket', 'xhr-polling']);
|
||||
io.enable('log');
|
||||
});
|
||||
```
|
||||
|
||||
<a href="https://opencollective.com/socketio/sponsor/0/website" target="_blank"><img src="https://opencollective.com/socketio/sponsor/0/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/socketio/sponsor/1/website" target="_blank"><img src="https://opencollective.com/socketio/sponsor/1/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/socketio/sponsor/2/website" target="_blank"><img src="https://opencollective.com/socketio/sponsor/2/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/socketio/sponsor/3/website" target="_blank"><img src="https://opencollective.com/socketio/sponsor/3/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/socketio/sponsor/4/website" target="_blank"><img src="https://opencollective.com/socketio/sponsor/4/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/socketio/sponsor/5/website" target="_blank"><img src="https://opencollective.com/socketio/sponsor/5/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/socketio/sponsor/6/website" target="_blank"><img src="https://opencollective.com/socketio/sponsor/6/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/socketio/sponsor/7/website" target="_blank"><img src="https://opencollective.com/socketio/sponsor/7/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/socketio/sponsor/8/website" target="_blank"><img src="https://opencollective.com/socketio/sponsor/8/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/socketio/sponsor/9/website" target="_blank"><img src="https://opencollective.com/socketio/sponsor/9/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/socketio/sponsor/10/website" target="_blank"><img src="https://opencollective.com/socketio/sponsor/10/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/socketio/sponsor/11/website" target="_blank"><img src="https://opencollective.com/socketio/sponsor/11/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/socketio/sponsor/12/website" target="_blank"><img src="https://opencollective.com/socketio/sponsor/12/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/socketio/sponsor/13/website" target="_blank"><img src="https://opencollective.com/socketio/sponsor/13/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/socketio/sponsor/14/website" target="_blank"><img src="https://opencollective.com/socketio/sponsor/14/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/socketio/sponsor/15/website" target="_blank"><img src="https://opencollective.com/socketio/sponsor/15/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/socketio/sponsor/16/website" target="_blank"><img src="https://opencollective.com/socketio/sponsor/16/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/socketio/sponsor/17/website" target="_blank"><img src="https://opencollective.com/socketio/sponsor/17/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/socketio/sponsor/18/website" target="_blank"><img src="https://opencollective.com/socketio/sponsor/18/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/socketio/sponsor/19/website" target="_blank"><img src="https://opencollective.com/socketio/sponsor/19/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/socketio/sponsor/20/website" target="_blank"><img src="https://opencollective.com/socketio/sponsor/20/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/socketio/sponsor/21/website" target="_blank"><img src="https://opencollective.com/socketio/sponsor/21/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/socketio/sponsor/22/website" target="_blank"><img src="https://opencollective.com/socketio/sponsor/22/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/socketio/sponsor/23/website" target="_blank"><img src="https://opencollective.com/socketio/sponsor/23/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/socketio/sponsor/24/website" target="_blank"><img src="https://opencollective.com/socketio/sponsor/24/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/socketio/sponsor/25/website" target="_blank"><img src="https://opencollective.com/socketio/sponsor/25/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/socketio/sponsor/26/website" target="_blank"><img src="https://opencollective.com/socketio/sponsor/26/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/socketio/sponsor/27/website" target="_blank"><img src="https://opencollective.com/socketio/sponsor/27/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/socketio/sponsor/28/website" target="_blank"><img src="https://opencollective.com/socketio/sponsor/28/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/socketio/sponsor/29/website" target="_blank"><img src="https://opencollective.com/socketio/sponsor/29/avatar.svg"></a>
|
||||
## License
|
||||
|
||||
(The MIT License)
|
||||
|
||||
## License
|
||||
Copyright (c) 2011 Guillermo Rauch <guillermo@learnboost.com>
|
||||
|
||||
[MIT](LICENSE)
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
'Software'), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
7
client-dist/socket.io.min.js
vendored
7
client-dist/socket.io.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
7
client-dist/socket.io.msgpack.min.js
vendored
7
client-dist/socket.io.msgpack.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,2 +0,0 @@
|
||||
|
||||
The documentation has been moved to the website [here](https://socket.io/docs/).
|
||||
@@ -1,17 +0,0 @@
|
||||
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
|
||||
# For additional information regarding the format and rule options, please see:
|
||||
# https://github.com/browserslist/browserslist#queries
|
||||
|
||||
# For the full list of supported browsers by the Angular framework, please see:
|
||||
# https://angular.io/guide/browser-support
|
||||
|
||||
# You can see what browsers were selected by your queries by running:
|
||||
# npx browserslist
|
||||
|
||||
last 1 Chrome version
|
||||
last 1 Firefox version
|
||||
last 2 Edge major versions
|
||||
last 2 Safari major versions
|
||||
last 2 iOS major versions
|
||||
Firefox ESR
|
||||
not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line.
|
||||
@@ -1,16 +0,0 @@
|
||||
# Editor configuration, see https://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.ts]
|
||||
quote_type = single
|
||||
|
||||
[*.md]
|
||||
max_line_length = off
|
||||
trim_trailing_whitespace = false
|
||||
46
examples/angular-todomvc/.gitignore
vendored
46
examples/angular-todomvc/.gitignore
vendored
@@ -1,46 +0,0 @@
|
||||
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# compiled output
|
||||
/dist
|
||||
/tmp
|
||||
/out-tsc
|
||||
# Only exists if Bazel was run
|
||||
/bazel-out
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# profiling files
|
||||
chrome-profiler-events*.json
|
||||
speed-measure-plugin*.json
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
.history/*
|
||||
|
||||
# misc
|
||||
/.sass-cache
|
||||
/connect.lock
|
||||
/coverage
|
||||
/libpeerconnection.log
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
testem.log
|
||||
/typings
|
||||
|
||||
# System Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
@@ -1,35 +0,0 @@
|
||||
# Angular TodoMVC + Socket.IO
|
||||
|
||||
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 11.0.4.
|
||||
|
||||
Inspired from the [TodoMVC](http://todomvc.com/) [angular example](https://github.com/tastejs/todomvc/tree/master/examples/angular2).
|
||||
|
||||

|
||||
|
||||
## Development server
|
||||
|
||||
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
|
||||
|
||||
## Socket.IO server
|
||||
|
||||
Run `npm run start:server` to start the Socket.IO server.
|
||||
|
||||
## Code scaffolding
|
||||
|
||||
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
|
||||
|
||||
## Build
|
||||
|
||||
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build.
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
|
||||
|
||||
## Running end-to-end tests
|
||||
|
||||
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
|
||||
|
||||
## Further help
|
||||
|
||||
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.
|
||||
@@ -1,128 +0,0 @@
|
||||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"angular-todomvc": {
|
||||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@schematics/angular:application": {
|
||||
"strict": true
|
||||
}
|
||||
},
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"options": {
|
||||
"outputPath": "dist/angular-todomvc",
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"aot": true,
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets"
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.css"
|
||||
],
|
||||
"scripts": []
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.prod.ts"
|
||||
}
|
||||
],
|
||||
"optimization": true,
|
||||
"outputHashing": "all",
|
||||
"sourceMap": false,
|
||||
"namedChunks": false,
|
||||
"extractLicenses": true,
|
||||
"vendorChunk": false,
|
||||
"buildOptimizer": true,
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "500kb",
|
||||
"maximumError": "1mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "2kb",
|
||||
"maximumError": "4kb"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"options": {
|
||||
"browserTarget": "angular-todomvc:build"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "angular-todomvc:build:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"browserTarget": "angular-todomvc:build"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"main": "src/test.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"karmaConfig": "karma.conf.js",
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets"
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.css"
|
||||
],
|
||||
"scripts": []
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"options": {
|
||||
"tsConfig": [
|
||||
"tsconfig.app.json",
|
||||
"tsconfig.spec.json",
|
||||
"e2e/tsconfig.json"
|
||||
],
|
||||
"exclude": [
|
||||
"**/node_modules/**"
|
||||
]
|
||||
}
|
||||
},
|
||||
"e2e": {
|
||||
"builder": "@angular-devkit/build-angular:protractor",
|
||||
"options": {
|
||||
"protractorConfig": "e2e/protractor.conf.js",
|
||||
"devServerTarget": "angular-todomvc:serve"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"devServerTarget": "angular-todomvc:serve:production"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultProject": "angular-todomvc"
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 205 KiB |
@@ -1,37 +0,0 @@
|
||||
// @ts-check
|
||||
// Protractor configuration file, see link for more information
|
||||
// https://github.com/angular/protractor/blob/master/lib/config.ts
|
||||
|
||||
const { SpecReporter, StacktraceOption } = require('jasmine-spec-reporter');
|
||||
|
||||
/**
|
||||
* @type { import("protractor").Config }
|
||||
*/
|
||||
exports.config = {
|
||||
allScriptsTimeout: 11000,
|
||||
specs: [
|
||||
'./src/**/*.e2e-spec.ts'
|
||||
],
|
||||
capabilities: {
|
||||
browserName: 'chrome'
|
||||
},
|
||||
directConnect: true,
|
||||
SELENIUM_PROMISE_MANAGER: false,
|
||||
baseUrl: 'http://localhost:4200/',
|
||||
framework: 'jasmine',
|
||||
jasmineNodeOpts: {
|
||||
showColors: true,
|
||||
defaultTimeoutInterval: 30000,
|
||||
print: function() {}
|
||||
},
|
||||
onPrepare() {
|
||||
require('ts-node').register({
|
||||
project: require('path').join(__dirname, './tsconfig.json')
|
||||
});
|
||||
jasmine.getEnv().addReporter(new SpecReporter({
|
||||
spec: {
|
||||
displayStacktrace: StacktraceOption.PRETTY
|
||||
}
|
||||
}));
|
||||
}
|
||||
};
|
||||
@@ -1,23 +0,0 @@
|
||||
import { AppPage } from './app.po';
|
||||
import { browser, logging } from 'protractor';
|
||||
|
||||
describe('workspace-project App', () => {
|
||||
let page: AppPage;
|
||||
|
||||
beforeEach(() => {
|
||||
page = new AppPage();
|
||||
});
|
||||
|
||||
it('should display welcome message', async () => {
|
||||
await page.navigateTo();
|
||||
expect(await page.getTitleText()).toEqual('angular-todomvc app is running!');
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// Assert that there are no errors emitted from the browser
|
||||
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
|
||||
expect(logs).not.toContain(jasmine.objectContaining({
|
||||
level: logging.Level.SEVERE,
|
||||
} as logging.Entry));
|
||||
});
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import { browser, by, element } from 'protractor';
|
||||
|
||||
export class AppPage {
|
||||
async navigateTo(): Promise<unknown> {
|
||||
return browser.get(browser.baseUrl);
|
||||
}
|
||||
|
||||
async getTitleText(): Promise<string> {
|
||||
return element(by.css('app-root .content span')).getText();
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../out-tsc/e2e",
|
||||
"module": "commonjs",
|
||||
"target": "es2018",
|
||||
"types": [
|
||||
"jasmine",
|
||||
"node"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
// Karma configuration file, see link for more information
|
||||
// https://karma-runner.github.io/1.0/config/configuration-file.html
|
||||
|
||||
module.exports = function (config) {
|
||||
config.set({
|
||||
basePath: '',
|
||||
frameworks: ['jasmine', '@angular-devkit/build-angular'],
|
||||
plugins: [
|
||||
require('karma-jasmine'),
|
||||
require('karma-chrome-launcher'),
|
||||
require('karma-jasmine-html-reporter'),
|
||||
require('karma-coverage'),
|
||||
require('@angular-devkit/build-angular/plugins/karma')
|
||||
],
|
||||
client: {
|
||||
jasmine: {
|
||||
// you can add configuration options for Jasmine here
|
||||
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
|
||||
// for example, you can disable the random execution with `random: false`
|
||||
// or set a specific seed with `seed: 4321`
|
||||
},
|
||||
clearContext: false // leave Jasmine Spec Runner output visible in browser
|
||||
},
|
||||
jasmineHtmlReporter: {
|
||||
suppressAll: true // removes the duplicated traces
|
||||
},
|
||||
coverageReporter: {
|
||||
dir: require('path').join(__dirname, './coverage/angular-todomvc'),
|
||||
subdir: '.',
|
||||
reporters: [
|
||||
{ type: 'html' },
|
||||
{ type: 'text-summary' }
|
||||
]
|
||||
},
|
||||
reporters: ['progress', 'kjhtml'],
|
||||
port: 9876,
|
||||
colors: true,
|
||||
logLevel: config.LOG_INFO,
|
||||
autoWatch: true,
|
||||
browsers: ['Chrome'],
|
||||
singleRun: false,
|
||||
restartOnFileChange: true
|
||||
});
|
||||
};
|
||||
13694
examples/angular-todomvc/package-lock.json
generated
13694
examples/angular-todomvc/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,48 +0,0 @@
|
||||
{
|
||||
"name": "angular-todomvc",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"build": "ng build",
|
||||
"test": "ng test",
|
||||
"lint": "ng lint",
|
||||
"e2e": "ng e2e",
|
||||
"start:server": "ts-node -O '{\"module\":\"commonjs\"}' server.ts"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "~11.0.4",
|
||||
"@angular/common": "~11.0.4",
|
||||
"@angular/compiler": "~11.0.4",
|
||||
"@angular/core": "~11.0.4",
|
||||
"@angular/forms": "~11.0.4",
|
||||
"@angular/platform-browser": "~11.0.4",
|
||||
"@angular/platform-browser-dynamic": "~11.0.4",
|
||||
"@angular/router": "~11.0.4",
|
||||
"rxjs": "~6.6.0",
|
||||
"socket.io": "^3.0.4",
|
||||
"socket.io-client": "^3.0.4",
|
||||
"tslib": "^2.0.0",
|
||||
"zone.js": "~0.10.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "~0.1100.4",
|
||||
"@angular/cli": "~11.0.4",
|
||||
"@angular/compiler-cli": "~11.0.4",
|
||||
"@types/jasmine": "~3.6.0",
|
||||
"@types/node": "^12.11.1",
|
||||
"codelyzer": "^6.0.0",
|
||||
"jasmine-core": "~3.6.0",
|
||||
"jasmine-spec-reporter": "~5.0.0",
|
||||
"karma": "~5.1.0",
|
||||
"karma-chrome-launcher": "~3.1.0",
|
||||
"karma-coverage": "~2.0.3",
|
||||
"karma-jasmine": "~4.0.0",
|
||||
"karma-jasmine-html-reporter": "^1.5.0",
|
||||
"protractor": "~7.0.0",
|
||||
"ts-node": "~8.3.0",
|
||||
"tslint": "~6.1.0",
|
||||
"typescript": "~4.0.2"
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import { Server, Socket } from "socket.io";
|
||||
|
||||
const io = new Server(8080, {
|
||||
cors: {
|
||||
origin: "http://localhost:4200",
|
||||
methods: ["GET", "POST"]
|
||||
}
|
||||
});
|
||||
|
||||
interface Todo {
|
||||
completed: boolean;
|
||||
editing: boolean;
|
||||
title: string;
|
||||
}
|
||||
|
||||
let todos: Array<Todo> = [];
|
||||
|
||||
io.on("connect", (socket: Socket) => {
|
||||
socket.emit("todos", todos);
|
||||
|
||||
// note: we could also create a CRUD (create/read/update/delete) service for the todo list
|
||||
socket.on("update-store", (updatedTodos) => {
|
||||
// store it locally
|
||||
todos = updatedTodos;
|
||||
// broadcast to everyone but the sender
|
||||
socket.broadcast.emit("todos", todos);
|
||||
});
|
||||
});
|
||||
@@ -1,23 +0,0 @@
|
||||
<section class="todoapp">
|
||||
<header class="header">
|
||||
<h1>todos</h1>
|
||||
<input class="new-todo" placeholder="What needs to be done?" autofocus="" [(ngModel)]="newTodoText" (keyup.enter)="addTodo()">
|
||||
</header>
|
||||
<section class="main" *ngIf="todoStore.todos.length > 0">
|
||||
<input id="toggle-all" class="toggle-all" type="checkbox" *ngIf="todoStore.todos.length" #toggleall [checked]="todoStore.allCompleted()" (click)="todoStore.setAllTo(toggleall.checked)">
|
||||
<ul class="todo-list">
|
||||
<li *ngFor="let todo of todoStore.todos" [class.completed]="todo.completed" [class.editing]="todo.editing">
|
||||
<div class="view">
|
||||
<input class="toggle" type="checkbox" (click)="toggleCompletion(todo)" [checked]="todo.completed">
|
||||
<label (dblclick)="editTodo(todo)">{{todo.title}}</label>
|
||||
<button class="destroy" (click)="remove(todo)"></button>
|
||||
</div>
|
||||
<input class="edit" *ngIf="todo.editing" [value]="todo.title" #editedtodo (blur)="stopEditing(todo, editedtodo.value)" (keyup.enter)="updateEditingTodo(todo, editedtodo.value)" (keyup.escape)="cancelEditingTodo(todo)">
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<footer class="footer" *ngIf="todoStore.todos.length > 0">
|
||||
<span class="todo-count"><strong>{{todoStore.getRemaining().length}}</strong> {{todoStore.getRemaining().length == 1 ? 'item' : 'items'}} left</span>
|
||||
<button class="clear-completed" *ngIf="todoStore.getCompleted().length > 0" (click)="removeCompleted()">Clear completed</button>
|
||||
</footer>
|
||||
</section>
|
||||
@@ -1,31 +0,0 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { AppComponent } from './app.component';
|
||||
|
||||
describe('AppComponent', () => {
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
AppComponent
|
||||
],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
it('should create the app', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.componentInstance;
|
||||
expect(app).toBeTruthy();
|
||||
});
|
||||
|
||||
it(`should have as title 'angular-todomvc'`, () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.componentInstance;
|
||||
expect(app.title).toEqual('angular-todomvc');
|
||||
});
|
||||
|
||||
it('should render title', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
fixture.detectChanges();
|
||||
const compiled = fixture.nativeElement;
|
||||
expect(compiled.querySelector('.content span').textContent).toContain('angular-todomvc app is running!');
|
||||
});
|
||||
});
|
||||
@@ -1,59 +0,0 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { RemoteTodoStore, Todo } from './store';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.css']
|
||||
})
|
||||
export class AppComponent {
|
||||
todoStore: RemoteTodoStore;
|
||||
newTodoText = '';
|
||||
|
||||
constructor(todoStore: RemoteTodoStore) {
|
||||
this.todoStore = todoStore;
|
||||
}
|
||||
|
||||
stopEditing(todo: Todo, editedTitle: string) {
|
||||
todo.title = editedTitle;
|
||||
todo.editing = false;
|
||||
}
|
||||
|
||||
cancelEditingTodo(todo: Todo) {
|
||||
todo.editing = false;
|
||||
}
|
||||
|
||||
updateEditingTodo(todo: Todo, editedTitle: string) {
|
||||
editedTitle = editedTitle.trim();
|
||||
todo.editing = false;
|
||||
|
||||
if (editedTitle.length === 0) {
|
||||
return this.todoStore.remove(todo);
|
||||
}
|
||||
|
||||
todo.title = editedTitle;
|
||||
}
|
||||
|
||||
editTodo(todo: Todo) {
|
||||
todo.editing = true;
|
||||
}
|
||||
|
||||
removeCompleted() {
|
||||
this.todoStore.removeCompleted();
|
||||
}
|
||||
|
||||
toggleCompletion(todo: Todo) {
|
||||
this.todoStore.toggleCompletion(todo);
|
||||
}
|
||||
|
||||
remove(todo: Todo){
|
||||
this.todoStore.remove(todo);
|
||||
}
|
||||
|
||||
addTodo() {
|
||||
if (this.newTodoText.trim().length) {
|
||||
this.todoStore.add(this.newTodoText);
|
||||
this.newTodoText = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
import { AppComponent } from './app.component';
|
||||
import { RemoteTodoStore } from './store';
|
||||
import { FormsModule } from "@angular/forms";
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
FormsModule
|
||||
],
|
||||
providers: [RemoteTodoStore],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
export class AppModule { }
|
||||
@@ -1,95 +0,0 @@
|
||||
import { io, Socket } from "socket.io-client";
|
||||
|
||||
export class Todo {
|
||||
completed: boolean;
|
||||
editing: boolean;
|
||||
|
||||
private _title: String = "";
|
||||
get title() {
|
||||
return this._title;
|
||||
}
|
||||
set title(value: String) {
|
||||
this._title = value.trim();
|
||||
}
|
||||
|
||||
constructor(title: String) {
|
||||
this.completed = false;
|
||||
this.editing = false;
|
||||
this.title = title.trim();
|
||||
}
|
||||
}
|
||||
|
||||
export class TodoStore {
|
||||
todos: Array<Todo>;
|
||||
|
||||
constructor() {
|
||||
let persistedTodos = JSON.parse(localStorage.getItem('angular2-todos') || '[]');
|
||||
// Normalize back into classes
|
||||
this.todos = persistedTodos.map( (todo: {_title: String, completed: boolean}) => {
|
||||
let ret = new Todo(todo._title);
|
||||
ret.completed = todo.completed;
|
||||
return ret;
|
||||
});
|
||||
}
|
||||
|
||||
protected updateStore() {
|
||||
localStorage.setItem('angular2-todos', JSON.stringify(this.todos));
|
||||
}
|
||||
|
||||
private getWithCompleted(completed: boolean) {
|
||||
return this.todos.filter((todo: Todo) => todo.completed === completed);
|
||||
}
|
||||
|
||||
allCompleted() {
|
||||
return this.todos.length === this.getCompleted().length;
|
||||
}
|
||||
|
||||
setAllTo(completed: boolean) {
|
||||
this.todos.forEach((t: Todo) => t.completed = completed);
|
||||
this.updateStore();
|
||||
}
|
||||
|
||||
removeCompleted() {
|
||||
this.todos = this.getWithCompleted(false);
|
||||
this.updateStore();
|
||||
}
|
||||
|
||||
getRemaining() {
|
||||
return this.getWithCompleted(false);
|
||||
}
|
||||
|
||||
getCompleted() {
|
||||
return this.getWithCompleted(true);
|
||||
}
|
||||
|
||||
toggleCompletion(todo: Todo) {
|
||||
todo.completed = !todo.completed;
|
||||
this.updateStore();
|
||||
}
|
||||
|
||||
remove(todo: Todo) {
|
||||
this.todos.splice(this.todos.indexOf(todo), 1);
|
||||
this.updateStore();
|
||||
}
|
||||
|
||||
add(title: String) {
|
||||
this.todos.push(new Todo(title));
|
||||
this.updateStore();
|
||||
}
|
||||
}
|
||||
|
||||
export class RemoteTodoStore extends TodoStore {
|
||||
private socket: Socket;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.socket = io("http://localhost:8080");
|
||||
this.socket.on("todos", (updatedTodos: Array<Todo>) => {
|
||||
this.todos = updatedTodos;
|
||||
});
|
||||
}
|
||||
|
||||
protected updateStore() {
|
||||
this.socket.emit("update-store", this.todos.map(({ title, editing, completed }) => ({ title, editing, completed })));
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export const environment = {
|
||||
production: true
|
||||
};
|
||||
@@ -1,16 +0,0 @@
|
||||
// This file can be replaced during build by using the `fileReplacements` array.
|
||||
// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
|
||||
// The list of file replacements can be found in `angular.json`.
|
||||
|
||||
export const environment = {
|
||||
production: false
|
||||
};
|
||||
|
||||
/*
|
||||
* For easier debugging in development mode, you can import the following file
|
||||
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
|
||||
*
|
||||
* This import should be commented out in production mode because it will have a negative impact
|
||||
* on performance if an error is thrown.
|
||||
*/
|
||||
// import 'zone.js/dist/zone-error'; // Included with Angular CLI.
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 948 B |
@@ -1,13 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Angular Todo MVC</title>
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
</head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,12 +0,0 @@
|
||||
import { enableProdMode } from '@angular/core';
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||
|
||||
import { AppModule } from './app/app.module';
|
||||
import { environment } from './environments/environment';
|
||||
|
||||
if (environment.production) {
|
||||
enableProdMode();
|
||||
}
|
||||
|
||||
platformBrowserDynamic().bootstrapModule(AppModule)
|
||||
.catch(err => console.error(err));
|
||||
@@ -1,63 +0,0 @@
|
||||
/**
|
||||
* This file includes polyfills needed by Angular and is loaded before the app.
|
||||
* You can add your own extra polyfills to this file.
|
||||
*
|
||||
* This file is divided into 2 sections:
|
||||
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
|
||||
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
|
||||
* file.
|
||||
*
|
||||
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
|
||||
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
|
||||
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
|
||||
*
|
||||
* Learn more in https://angular.io/guide/browser-support
|
||||
*/
|
||||
|
||||
/***************************************************************************************************
|
||||
* BROWSER POLYFILLS
|
||||
*/
|
||||
|
||||
/** IE11 requires the following for NgClass support on SVG elements */
|
||||
// import 'classlist.js'; // Run `npm install --save classlist.js`.
|
||||
|
||||
/**
|
||||
* Web Animations `@angular/platform-browser/animations`
|
||||
* Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
|
||||
* Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
|
||||
*/
|
||||
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
|
||||
|
||||
/**
|
||||
* By default, zone.js will patch all possible macroTask and DomEvents
|
||||
* user can disable parts of macroTask/DomEvents patch by setting following flags
|
||||
* because those flags need to be set before `zone.js` being loaded, and webpack
|
||||
* will put import in the top of bundle, so user need to create a separate file
|
||||
* in this directory (for example: zone-flags.ts), and put the following flags
|
||||
* into that file, and then add the following code before importing zone.js.
|
||||
* import './zone-flags';
|
||||
*
|
||||
* The flags allowed in zone-flags.ts are listed here.
|
||||
*
|
||||
* The following flags will work for all browsers.
|
||||
*
|
||||
* (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
|
||||
* (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
|
||||
* (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
|
||||
*
|
||||
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
|
||||
* with the following flag, it will bypass `zone.js` patch for IE/Edge
|
||||
*
|
||||
* (window as any).__Zone_enable_cross_context_check = true;
|
||||
*
|
||||
*/
|
||||
|
||||
/***************************************************************************************************
|
||||
* Zone JS is required by default for Angular itself.
|
||||
*/
|
||||
import 'zone.js/dist/zone'; // Included with Angular CLI.
|
||||
|
||||
|
||||
/***************************************************************************************************
|
||||
* APPLICATION IMPORTS
|
||||
*/
|
||||
@@ -1,381 +0,0 @@
|
||||
/* imported from node_modules/todomvc-app-css/index.css */
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: none;
|
||||
font-size: 100%;
|
||||
vertical-align: baseline;
|
||||
font-family: inherit;
|
||||
font-weight: inherit;
|
||||
color: inherit;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body {
|
||||
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
line-height: 1.4em;
|
||||
background: #f5f5f5;
|
||||
color: #111111;
|
||||
min-width: 230px;
|
||||
max-width: 550px;
|
||||
margin: 0 auto;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.todoapp {
|
||||
background: #fff;
|
||||
margin: 130px 0 40px 0;
|
||||
position: relative;
|
||||
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
|
||||
0 25px 50px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.todoapp input::-webkit-input-placeholder {
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.todoapp input::-moz-placeholder {
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.todoapp input::input-placeholder {
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.todoapp h1 {
|
||||
position: absolute;
|
||||
top: -140px;
|
||||
width: 100%;
|
||||
font-size: 80px;
|
||||
font-weight: 200;
|
||||
text-align: center;
|
||||
color: #b83f45;
|
||||
-webkit-text-rendering: optimizeLegibility;
|
||||
-moz-text-rendering: optimizeLegibility;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
.new-todo,
|
||||
.edit {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
font-size: 24px;
|
||||
font-family: inherit;
|
||||
font-weight: inherit;
|
||||
line-height: 1.4em;
|
||||
color: inherit;
|
||||
padding: 6px;
|
||||
border: 1px solid #999;
|
||||
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
|
||||
box-sizing: border-box;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.new-todo {
|
||||
padding: 16px 16px 16px 60px;
|
||||
border: none;
|
||||
background: rgba(0, 0, 0, 0.003);
|
||||
box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
|
||||
}
|
||||
|
||||
.main {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
border-top: 1px solid #e6e6e6;
|
||||
}
|
||||
|
||||
.toggle-all {
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
border: none; /* Mobile Safari */
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
right: 100%;
|
||||
bottom: 100%;
|
||||
}
|
||||
|
||||
.toggle-all + label {
|
||||
width: 60px;
|
||||
height: 34px;
|
||||
font-size: 0;
|
||||
position: absolute;
|
||||
top: -52px;
|
||||
left: -13px;
|
||||
-webkit-transform: rotate(90deg);
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.toggle-all + label:before {
|
||||
content: '❯';
|
||||
font-size: 22px;
|
||||
color: #e6e6e6;
|
||||
padding: 10px 27px 10px 27px;
|
||||
}
|
||||
|
||||
.toggle-all:checked + label:before {
|
||||
color: #737373;
|
||||
}
|
||||
|
||||
.todo-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.todo-list li {
|
||||
position: relative;
|
||||
font-size: 24px;
|
||||
border-bottom: 1px solid #ededed;
|
||||
}
|
||||
|
||||
.todo-list li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.todo-list li.editing {
|
||||
border-bottom: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.todo-list li.editing .edit {
|
||||
display: block;
|
||||
width: calc(100% - 43px);
|
||||
padding: 12px 16px;
|
||||
margin: 0 0 0 43px;
|
||||
}
|
||||
|
||||
.todo-list li.editing .view {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.todo-list li .toggle {
|
||||
text-align: center;
|
||||
width: 40px;
|
||||
/* auto, since non-WebKit browsers doesn't support input styling */
|
||||
height: auto;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
margin: auto 0;
|
||||
border: none; /* Mobile Safari */
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
.todo-list li .toggle {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.todo-list li .toggle + label {
|
||||
/*
|
||||
Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433
|
||||
IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/
|
||||
*/
|
||||
background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E');
|
||||
background-repeat: no-repeat;
|
||||
background-position: center left;
|
||||
}
|
||||
|
||||
.todo-list li .toggle:checked + label {
|
||||
background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E');
|
||||
}
|
||||
|
||||
.todo-list li label {
|
||||
word-break: break-all;
|
||||
padding: 15px 15px 15px 60px;
|
||||
display: block;
|
||||
line-height: 1.2;
|
||||
transition: color 0.4s;
|
||||
font-weight: 400;
|
||||
color: #4d4d4d;
|
||||
}
|
||||
|
||||
.todo-list li.completed label {
|
||||
color: #cdcdcd;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.todo-list li .destroy {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 10px;
|
||||
bottom: 0;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin: auto 0;
|
||||
font-size: 30px;
|
||||
color: #cc9a9a;
|
||||
margin-bottom: 11px;
|
||||
transition: color 0.2s ease-out;
|
||||
}
|
||||
|
||||
.todo-list li .destroy:hover {
|
||||
color: #af5b5e;
|
||||
}
|
||||
|
||||
.todo-list li .destroy:after {
|
||||
content: '×';
|
||||
}
|
||||
|
||||
.todo-list li:hover .destroy {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.todo-list li .edit {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.todo-list li.editing:last-child {
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding: 10px 15px;
|
||||
height: 20px;
|
||||
text-align: center;
|
||||
font-size: 15px;
|
||||
border-top: 1px solid #e6e6e6;
|
||||
}
|
||||
|
||||
.footer:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 50px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
|
||||
0 8px 0 -3px #f6f6f6,
|
||||
0 9px 1px -3px rgba(0, 0, 0, 0.2),
|
||||
0 16px 0 -6px #f6f6f6,
|
||||
0 17px 2px -6px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.todo-count {
|
||||
float: left;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.todo-count strong {
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.filters {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.filters li {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.filters li a {
|
||||
color: inherit;
|
||||
margin: 3px;
|
||||
padding: 3px 7px;
|
||||
text-decoration: none;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.filters li a:hover {
|
||||
border-color: rgba(175, 47, 47, 0.1);
|
||||
}
|
||||
|
||||
.filters li a.selected {
|
||||
border-color: rgba(175, 47, 47, 0.2);
|
||||
}
|
||||
|
||||
.clear-completed,
|
||||
html .clear-completed:active {
|
||||
float: right;
|
||||
position: relative;
|
||||
line-height: 20px;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.clear-completed:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.info {
|
||||
margin: 65px auto 0;
|
||||
color: #4d4d4d;
|
||||
font-size: 11px;
|
||||
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.info p {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.info a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.info a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/*
|
||||
Hack to remove background from Mobile Safari.
|
||||
Can't use it globally since it destroys checkboxes in Firefox
|
||||
*/
|
||||
@media screen and (-webkit-min-device-pixel-ratio:0) {
|
||||
.toggle-all,
|
||||
.todo-list li .toggle {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.todo-list li .toggle {
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 430px) {
|
||||
.footer {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.filters {
|
||||
bottom: 10px;
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
|
||||
|
||||
import 'zone.js/dist/zone-testing';
|
||||
import { getTestBed } from '@angular/core/testing';
|
||||
import {
|
||||
BrowserDynamicTestingModule,
|
||||
platformBrowserDynamicTesting
|
||||
} from '@angular/platform-browser-dynamic/testing';
|
||||
|
||||
declare const require: {
|
||||
context(path: string, deep?: boolean, filter?: RegExp): {
|
||||
keys(): string[];
|
||||
<T>(id: string): T;
|
||||
};
|
||||
};
|
||||
|
||||
// First, initialize the Angular testing environment.
|
||||
getTestBed().initTestEnvironment(
|
||||
BrowserDynamicTestingModule,
|
||||
platformBrowserDynamicTesting()
|
||||
);
|
||||
// Then we find all the tests.
|
||||
const context = require.context('./', true, /\.spec\.ts$/);
|
||||
// And load the modules.
|
||||
context.keys().map(context);
|
||||
@@ -1,15 +0,0 @@
|
||||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/app",
|
||||
"types": []
|
||||
},
|
||||
"files": [
|
||||
"src/main.ts",
|
||||
"src/polyfills.ts"
|
||||
],
|
||||
"include": [
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"outDir": "./dist/out-tsc",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"sourceMap": true,
|
||||
"declaration": false,
|
||||
"downlevelIteration": true,
|
||||
"experimentalDecorators": true,
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"target": "es2015",
|
||||
"module": "es2020",
|
||||
"lib": [
|
||||
"es2018",
|
||||
"dom"
|
||||
]
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"strictTemplates": true
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/spec",
|
||||
"types": [
|
||||
"jasmine"
|
||||
]
|
||||
},
|
||||
"files": [
|
||||
"src/test.ts",
|
||||
"src/polyfills.ts"
|
||||
],
|
||||
"include": [
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
{
|
||||
"extends": "tslint:recommended",
|
||||
"rulesDirectory": [
|
||||
"codelyzer"
|
||||
],
|
||||
"rules": {
|
||||
"align": {
|
||||
"options": [
|
||||
"parameters",
|
||||
"statements"
|
||||
]
|
||||
},
|
||||
"array-type": false,
|
||||
"arrow-return-shorthand": true,
|
||||
"curly": true,
|
||||
"deprecation": {
|
||||
"severity": "warning"
|
||||
},
|
||||
"eofline": true,
|
||||
"import-blacklist": [
|
||||
true,
|
||||
"rxjs/Rx"
|
||||
],
|
||||
"import-spacing": true,
|
||||
"indent": {
|
||||
"options": [
|
||||
"spaces"
|
||||
]
|
||||
},
|
||||
"max-classes-per-file": false,
|
||||
"max-line-length": [
|
||||
true,
|
||||
140
|
||||
],
|
||||
"member-ordering": [
|
||||
true,
|
||||
{
|
||||
"order": [
|
||||
"static-field",
|
||||
"instance-field",
|
||||
"static-method",
|
||||
"instance-method"
|
||||
]
|
||||
}
|
||||
],
|
||||
"no-console": [
|
||||
true,
|
||||
"debug",
|
||||
"info",
|
||||
"time",
|
||||
"timeEnd",
|
||||
"trace"
|
||||
],
|
||||
"no-empty": false,
|
||||
"no-inferrable-types": [
|
||||
true,
|
||||
"ignore-params"
|
||||
],
|
||||
"no-non-null-assertion": true,
|
||||
"no-redundant-jsdoc": true,
|
||||
"no-switch-case-fall-through": true,
|
||||
"no-var-requires": false,
|
||||
"object-literal-key-quotes": [
|
||||
true,
|
||||
"as-needed"
|
||||
],
|
||||
"quotemark": [
|
||||
true,
|
||||
"single"
|
||||
],
|
||||
"semicolon": {
|
||||
"options": [
|
||||
"always"
|
||||
]
|
||||
},
|
||||
"space-before-function-paren": {
|
||||
"options": {
|
||||
"anonymous": "never",
|
||||
"asyncArrow": "always",
|
||||
"constructor": "never",
|
||||
"method": "never",
|
||||
"named": "never"
|
||||
}
|
||||
},
|
||||
"typedef": [
|
||||
true,
|
||||
"call-signature"
|
||||
],
|
||||
"typedef-whitespace": {
|
||||
"options": [
|
||||
{
|
||||
"call-signature": "nospace",
|
||||
"index-signature": "nospace",
|
||||
"parameter": "nospace",
|
||||
"property-declaration": "nospace",
|
||||
"variable-declaration": "nospace"
|
||||
},
|
||||
{
|
||||
"call-signature": "onespace",
|
||||
"index-signature": "onespace",
|
||||
"parameter": "onespace",
|
||||
"property-declaration": "onespace",
|
||||
"variable-declaration": "onespace"
|
||||
}
|
||||
]
|
||||
},
|
||||
"variable-name": {
|
||||
"options": [
|
||||
"ban-keywords",
|
||||
"check-format",
|
||||
"allow-pascal-case"
|
||||
]
|
||||
},
|
||||
"whitespace": {
|
||||
"options": [
|
||||
"check-branch",
|
||||
"check-decl",
|
||||
"check-operator",
|
||||
"check-separator",
|
||||
"check-type",
|
||||
"check-typecast"
|
||||
]
|
||||
},
|
||||
"component-class-suffix": true,
|
||||
"contextual-lifecycle": true,
|
||||
"directive-class-suffix": true,
|
||||
"no-conflicting-lifecycle": true,
|
||||
"no-host-metadata-property": true,
|
||||
"no-input-rename": true,
|
||||
"no-inputs-metadata-property": true,
|
||||
"no-output-native": true,
|
||||
"no-output-on-prefix": true,
|
||||
"no-output-rename": true,
|
||||
"no-outputs-metadata-property": true,
|
||||
"template-banana-in-box": true,
|
||||
"template-no-negated-async": true,
|
||||
"use-lifecycle-interface": true,
|
||||
"use-pipe-transform-interface": true,
|
||||
"directive-selector": [
|
||||
true,
|
||||
"attribute",
|
||||
"app",
|
||||
"camelCase"
|
||||
],
|
||||
"component-selector": [
|
||||
true,
|
||||
"element",
|
||||
"app",
|
||||
"kebab-case"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
|
||||
# Socket.IO Chat
|
||||
|
||||
A simple chat demo for Socket.IO
|
||||
|
||||
## How to use
|
||||
|
||||
```
|
||||
$ npm ci
|
||||
$ npm start
|
||||
```
|
||||
|
||||
And point your browser to `http://localhost:3000`. Optionally, specify
|
||||
a port by supplying the `PORT` env variable.
|
||||
|
||||
## Features
|
||||
|
||||
- Multiple users can join a chat room by each entering a unique username
|
||||
on website load.
|
||||
- Users can type chat messages to the chat room.
|
||||
- A notification is sent to all users when a user joins or leaves
|
||||
the chatroom.
|
||||
80
examples/chat/app.js
Normal file
80
examples/chat/app.js
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
var express = require('express')
|
||||
, stylus = require('stylus')
|
||||
, nib = require('nib')
|
||||
, sio = require('../../lib/socket.io');
|
||||
|
||||
/**
|
||||
* App.
|
||||
*/
|
||||
|
||||
var app = express.createServer();
|
||||
|
||||
/**
|
||||
* App configuration.
|
||||
*/
|
||||
|
||||
app.configure(function () {
|
||||
app.use(stylus.middleware({ src: __dirname + '/public', compile: compile }));
|
||||
app.use(express.static(__dirname + '/public'));
|
||||
app.set('views', __dirname);
|
||||
app.set('view engine', 'jade');
|
||||
|
||||
function compile (str, path) {
|
||||
return stylus(str)
|
||||
.set('filename', path)
|
||||
.use(nib());
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* App routes.
|
||||
*/
|
||||
|
||||
app.get('/', function (req, res) {
|
||||
res.render('index', { layout: false });
|
||||
});
|
||||
|
||||
/**
|
||||
* App listen.
|
||||
*/
|
||||
|
||||
app.listen(3000, function () {
|
||||
var addr = app.address();
|
||||
console.log(' app listening on http://' + addr.address + ':' + addr.port);
|
||||
});
|
||||
|
||||
/**
|
||||
* Socket.IO server (single process only)
|
||||
*/
|
||||
|
||||
var io = sio(app)
|
||||
, nicknames = {};
|
||||
|
||||
io.on('connection', function (socket) {
|
||||
socket.on('user message', function (msg) {
|
||||
socket.broadcast.emit('user message', socket.nickname, msg);
|
||||
});
|
||||
|
||||
socket.on('nickname', function (nick, fn) {
|
||||
if (nicknames[nick]) {
|
||||
fn(true);
|
||||
} else {
|
||||
fn(false);
|
||||
nicknames[nick] = socket.nickname = nick;
|
||||
socket.broadcast.emit('announcement', nick + ' connected');
|
||||
io.emit('nicknames', nicknames);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('disconnect', function () {
|
||||
if (!socket.nickname) return;
|
||||
|
||||
delete nicknames[socket.nickname];
|
||||
socket.broadcast.emit('announcement', socket.nickname + ' disconnected');
|
||||
socket.broadcast.emit('nicknames', nicknames);
|
||||
});
|
||||
});
|
||||
83
examples/chat/index.jade
Normal file
83
examples/chat/index.jade
Normal file
@@ -0,0 +1,83 @@
|
||||
doctype 5
|
||||
html
|
||||
head
|
||||
link(href='/stylesheets/style.css', rel='stylesheet')
|
||||
script(src='http://code.jquery.com/jquery-1.6.1.min.js')
|
||||
script(src='/socket.io/socket.io.js')
|
||||
script
|
||||
// socket.io specific code
|
||||
var socket = io.connect();
|
||||
|
||||
socket.on('connect', function () {
|
||||
$('#chat').addClass('connected');
|
||||
});
|
||||
|
||||
socket.on('announcement', function (msg) {
|
||||
$('#lines').append($('<p>').append($('<em>').text(msg)));
|
||||
});
|
||||
|
||||
socket.on('nicknames', function (nicknames) {
|
||||
$('#nicknames').empty().append($('<span>Online: </span>'));
|
||||
for (var i in nicknames) {
|
||||
$('#nicknames').append($('<b>').text(nicknames[i]));
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('user message', message);
|
||||
socket.on('reconnect', function () {
|
||||
$('#lines').remove();
|
||||
message('System', 'Reconnected to the server');
|
||||
});
|
||||
|
||||
socket.on('reconnecting', function () {
|
||||
message('System', 'Attempting to re-connect to the server');
|
||||
});
|
||||
|
||||
socket.on('error', function (e) {
|
||||
message('System', e ? e : 'A unknown error occurred');
|
||||
});
|
||||
|
||||
function message (from, msg) {
|
||||
$('#lines').append($('<p>').append($('<b>').text(from), msg));
|
||||
}
|
||||
|
||||
// dom manipulation
|
||||
$(function () {
|
||||
$('#set-nickname').submit(function (ev) {
|
||||
socket.emit('nickname', $('#nick').val(), function (set) {
|
||||
if (!set) {
|
||||
clear();
|
||||
return $('#chat').addClass('nickname-set');
|
||||
}
|
||||
$('#nickname-err').css('visibility', 'visible');
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
$('#send-message').submit(function () {
|
||||
message('me', $('#message').val());
|
||||
socket.emit('user message', $('#message').val());
|
||||
clear();
|
||||
$('#lines').get(0).scrollTop = 10000000;
|
||||
return false;
|
||||
});
|
||||
|
||||
function clear () {
|
||||
$('#message').val('').focus();
|
||||
};
|
||||
});
|
||||
body
|
||||
#chat
|
||||
#nickname
|
||||
form.wrap#set-nickname
|
||||
p Please type in your nickname and press enter.
|
||||
input#nick
|
||||
p#nickname-err Nickname already in use
|
||||
#connecting
|
||||
.wrap Connecting to socket.io server
|
||||
#messages
|
||||
#nicknames
|
||||
#lines
|
||||
form#send-message
|
||||
input#message
|
||||
button Send
|
||||
@@ -1,76 +0,0 @@
|
||||
// Setup basic express server
|
||||
const express = require('express');
|
||||
const app = express();
|
||||
const path = require('path');
|
||||
const server = require('http').createServer(app);
|
||||
const io = require('socket.io')(server);
|
||||
const port = process.env.PORT || 3000;
|
||||
|
||||
server.listen(port, () => {
|
||||
console.log('Server listening at port %d', port);
|
||||
});
|
||||
|
||||
// Routing
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
// Chatroom
|
||||
|
||||
let numUsers = 0;
|
||||
|
||||
io.on('connection', (socket) => {
|
||||
let addedUser = false;
|
||||
|
||||
// when the client emits 'new message', this listens and executes
|
||||
socket.on('new message', (data) => {
|
||||
// we tell the client to execute 'new message'
|
||||
socket.broadcast.emit('new message', {
|
||||
username: socket.username,
|
||||
message: data
|
||||
});
|
||||
});
|
||||
|
||||
// when the client emits 'add user', this listens and executes
|
||||
socket.on('add user', (username) => {
|
||||
if (addedUser) return;
|
||||
|
||||
// we store the username in the socket session for this client
|
||||
socket.username = username;
|
||||
++numUsers;
|
||||
addedUser = true;
|
||||
socket.emit('login', {
|
||||
numUsers: numUsers
|
||||
});
|
||||
// echo globally (all clients) that a person has connected
|
||||
socket.broadcast.emit('user joined', {
|
||||
username: socket.username,
|
||||
numUsers: numUsers
|
||||
});
|
||||
});
|
||||
|
||||
// when the client emits 'typing', we broadcast it to others
|
||||
socket.on('typing', () => {
|
||||
socket.broadcast.emit('typing', {
|
||||
username: socket.username
|
||||
});
|
||||
});
|
||||
|
||||
// when the client emits 'stop typing', we broadcast it to others
|
||||
socket.on('stop typing', () => {
|
||||
socket.broadcast.emit('stop typing', {
|
||||
username: socket.username
|
||||
});
|
||||
});
|
||||
|
||||
// when the user disconnects.. perform this
|
||||
socket.on('disconnect', () => {
|
||||
if (addedUser) {
|
||||
--numUsers;
|
||||
|
||||
// echo globally that this client has left
|
||||
socket.broadcast.emit('user left', {
|
||||
username: socket.username,
|
||||
numUsers: numUsers
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
499
examples/chat/package-lock.json
generated
499
examples/chat/package-lock.json
generated
@@ -1,499 +0,0 @@
|
||||
{
|
||||
"name": "socket.io-chat",
|
||||
"version": "0.0.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"accepts": {
|
||||
"version": "1.3.7",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
|
||||
"integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
|
||||
"requires": {
|
||||
"mime-types": "~2.1.24",
|
||||
"negotiator": "0.6.2"
|
||||
}
|
||||
},
|
||||
"array-flatten": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||
"integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
|
||||
},
|
||||
"base64id": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
|
||||
"integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="
|
||||
},
|
||||
"body-parser": {
|
||||
"version": "1.19.0",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
|
||||
"integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==",
|
||||
"requires": {
|
||||
"bytes": "3.1.0",
|
||||
"content-type": "~1.0.4",
|
||||
"debug": "2.6.9",
|
||||
"depd": "~1.1.2",
|
||||
"http-errors": "1.7.2",
|
||||
"iconv-lite": "0.4.24",
|
||||
"on-finished": "~2.3.0",
|
||||
"qs": "6.7.0",
|
||||
"raw-body": "2.4.0",
|
||||
"type-is": "~1.6.17"
|
||||
}
|
||||
},
|
||||
"bytes": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
|
||||
"integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="
|
||||
},
|
||||
"component-emitter": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
|
||||
"integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg=="
|
||||
},
|
||||
"content-disposition": {
|
||||
"version": "0.5.3",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz",
|
||||
"integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==",
|
||||
"requires": {
|
||||
"safe-buffer": "5.1.2"
|
||||
}
|
||||
},
|
||||
"content-type": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
|
||||
"integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
|
||||
},
|
||||
"cookie": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
|
||||
"integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg=="
|
||||
},
|
||||
"cookie-signature": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
||||
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
|
||||
},
|
||||
"cors": {
|
||||
"version": "2.8.5",
|
||||
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
|
||||
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
|
||||
"requires": {
|
||||
"object-assign": "^4",
|
||||
"vary": "^1"
|
||||
}
|
||||
},
|
||||
"debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"requires": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"depd": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
|
||||
"integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
|
||||
},
|
||||
"destroy": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
|
||||
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
|
||||
},
|
||||
"ee-first": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
|
||||
},
|
||||
"encodeurl": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
|
||||
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
|
||||
},
|
||||
"engine.io": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-4.0.1.tgz",
|
||||
"integrity": "sha512-6EaSBxasBUwxRdf6B68SEYpD3tcrG80J4YTzHl/D+9Q+vM0AMHZabfYcc2WdnvEaQxZjX/UZsa+UdGoM0qQQkQ==",
|
||||
"requires": {
|
||||
"accepts": "~1.3.4",
|
||||
"base64id": "2.0.0",
|
||||
"cookie": "~0.4.1",
|
||||
"cors": "~2.8.5",
|
||||
"debug": "~4.1.0",
|
||||
"engine.io-parser": "~4.0.0",
|
||||
"ws": "^7.1.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"cookie": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
|
||||
"integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA=="
|
||||
},
|
||||
"debug": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
|
||||
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
|
||||
"requires": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"engine.io-parser": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-4.0.1.tgz",
|
||||
"integrity": "sha512-v5aZK1hlckcJDGmHz3W8xvI3NUHYc9t8QtTbqdR5OaH3S9iJZilPubauOm+vLWOMMWzpE3hiq92l9lTAHamRCg=="
|
||||
},
|
||||
"escape-html": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||
"integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
|
||||
},
|
||||
"etag": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
||||
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
|
||||
},
|
||||
"express": {
|
||||
"version": "4.17.1",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz",
|
||||
"integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==",
|
||||
"requires": {
|
||||
"accepts": "~1.3.7",
|
||||
"array-flatten": "1.1.1",
|
||||
"body-parser": "1.19.0",
|
||||
"content-disposition": "0.5.3",
|
||||
"content-type": "~1.0.4",
|
||||
"cookie": "0.4.0",
|
||||
"cookie-signature": "1.0.6",
|
||||
"debug": "2.6.9",
|
||||
"depd": "~1.1.2",
|
||||
"encodeurl": "~1.0.2",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"finalhandler": "~1.1.2",
|
||||
"fresh": "0.5.2",
|
||||
"merge-descriptors": "1.0.1",
|
||||
"methods": "~1.1.2",
|
||||
"on-finished": "~2.3.0",
|
||||
"parseurl": "~1.3.3",
|
||||
"path-to-regexp": "0.1.7",
|
||||
"proxy-addr": "~2.0.5",
|
||||
"qs": "6.7.0",
|
||||
"range-parser": "~1.2.1",
|
||||
"safe-buffer": "5.1.2",
|
||||
"send": "0.17.1",
|
||||
"serve-static": "1.14.1",
|
||||
"setprototypeof": "1.1.1",
|
||||
"statuses": "~1.5.0",
|
||||
"type-is": "~1.6.18",
|
||||
"utils-merge": "1.0.1",
|
||||
"vary": "~1.1.2"
|
||||
}
|
||||
},
|
||||
"finalhandler": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
|
||||
"integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
|
||||
"requires": {
|
||||
"debug": "2.6.9",
|
||||
"encodeurl": "~1.0.2",
|
||||
"escape-html": "~1.0.3",
|
||||
"on-finished": "~2.3.0",
|
||||
"parseurl": "~1.3.3",
|
||||
"statuses": "~1.5.0",
|
||||
"unpipe": "~1.0.0"
|
||||
}
|
||||
},
|
||||
"forwarded": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
|
||||
"integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ="
|
||||
},
|
||||
"fresh": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
||||
"integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
|
||||
},
|
||||
"http-errors": {
|
||||
"version": "1.7.2",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
|
||||
"integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==",
|
||||
"requires": {
|
||||
"depd": "~1.1.2",
|
||||
"inherits": "2.0.3",
|
||||
"setprototypeof": "1.1.1",
|
||||
"statuses": ">= 1.5.0 < 2",
|
||||
"toidentifier": "1.0.0"
|
||||
}
|
||||
},
|
||||
"iconv-lite": {
|
||||
"version": "0.4.24",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
||||
"requires": {
|
||||
"safer-buffer": ">= 2.1.2 < 3"
|
||||
}
|
||||
},
|
||||
"inherits": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
|
||||
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
|
||||
},
|
||||
"ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="
|
||||
},
|
||||
"media-typer": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
|
||||
},
|
||||
"merge-descriptors": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
|
||||
"integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
|
||||
},
|
||||
"methods": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
||||
"integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
|
||||
},
|
||||
"mime": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
||||
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
|
||||
},
|
||||
"mime-db": {
|
||||
"version": "1.43.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz",
|
||||
"integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ=="
|
||||
},
|
||||
"mime-types": {
|
||||
"version": "2.1.26",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz",
|
||||
"integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==",
|
||||
"requires": {
|
||||
"mime-db": "1.43.0"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
||||
},
|
||||
"negotiator": {
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
|
||||
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
|
||||
},
|
||||
"object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
|
||||
},
|
||||
"on-finished": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
|
||||
"integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
|
||||
"requires": {
|
||||
"ee-first": "1.1.1"
|
||||
}
|
||||
},
|
||||
"parseurl": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
|
||||
},
|
||||
"path-to-regexp": {
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
|
||||
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
|
||||
},
|
||||
"proxy-addr": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz",
|
||||
"integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==",
|
||||
"requires": {
|
||||
"forwarded": "~0.1.2",
|
||||
"ipaddr.js": "1.9.1"
|
||||
}
|
||||
},
|
||||
"qs": {
|
||||
"version": "6.7.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
|
||||
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
|
||||
},
|
||||
"range-parser": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
|
||||
},
|
||||
"raw-body": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz",
|
||||
"integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==",
|
||||
"requires": {
|
||||
"bytes": "3.1.0",
|
||||
"http-errors": "1.7.2",
|
||||
"iconv-lite": "0.4.24",
|
||||
"unpipe": "1.0.0"
|
||||
}
|
||||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
||||
},
|
||||
"safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||
},
|
||||
"send": {
|
||||
"version": "0.17.1",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz",
|
||||
"integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==",
|
||||
"requires": {
|
||||
"debug": "2.6.9",
|
||||
"depd": "~1.1.2",
|
||||
"destroy": "~1.0.4",
|
||||
"encodeurl": "~1.0.2",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"fresh": "0.5.2",
|
||||
"http-errors": "~1.7.2",
|
||||
"mime": "1.6.0",
|
||||
"ms": "2.1.1",
|
||||
"on-finished": "~2.3.0",
|
||||
"range-parser": "~1.2.1",
|
||||
"statuses": "~1.5.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ms": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
|
||||
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"serve-static": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz",
|
||||
"integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==",
|
||||
"requires": {
|
||||
"encodeurl": "~1.0.2",
|
||||
"escape-html": "~1.0.3",
|
||||
"parseurl": "~1.3.3",
|
||||
"send": "0.17.1"
|
||||
}
|
||||
},
|
||||
"setprototypeof": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
|
||||
"integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
|
||||
},
|
||||
"socket.io": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-3.0.0.tgz",
|
||||
"integrity": "sha512-arLQtd+UoJ08NXBRBGUJDyQ9B+cc9WwD67hc5s1WQcs2DyAkYzI5HWg4U0CrFtK00kjyAWxBGhLwVbfOeMqz1A==",
|
||||
"requires": {
|
||||
"accepts": "~1.3.4",
|
||||
"base64id": "~2.0.0",
|
||||
"debug": "~4.1.0",
|
||||
"engine.io": "~4.0.0",
|
||||
"socket.io-adapter": "~2.0.3",
|
||||
"socket.io-parser": "~4.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
|
||||
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
|
||||
"requires": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"socket.io-adapter": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.0.3.tgz",
|
||||
"integrity": "sha512-2wo4EXgxOGSFueqvHAdnmi5JLZzWqMArjuP4nqC26AtLh5PoCPsaRbRdah2xhcwTAMooZfjYiNVNkkmmSMaxOQ=="
|
||||
},
|
||||
"socket.io-parser": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.1.tgz",
|
||||
"integrity": "sha512-5JfNykYptCwU2lkOI0ieoePWm+6stEhkZ2UnLDjqnE1YEjUlXXLd1lpxPZ+g+h3rtaytwWkWrLQCaJULlGqjOg==",
|
||||
"requires": {
|
||||
"component-emitter": "~1.3.0",
|
||||
"debug": "~4.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
|
||||
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
|
||||
"requires": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"statuses": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
|
||||
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
|
||||
},
|
||||
"toidentifier": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
|
||||
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
|
||||
},
|
||||
"type-is": {
|
||||
"version": "1.6.18",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
||||
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
|
||||
"requires": {
|
||||
"media-typer": "0.3.0",
|
||||
"mime-types": "~2.1.24"
|
||||
}
|
||||
},
|
||||
"unpipe": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||
"integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
|
||||
},
|
||||
"utils-merge": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
|
||||
},
|
||||
"vary": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
|
||||
},
|
||||
"ws": {
|
||||
"version": "7.4.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.0.tgz",
|
||||
"integrity": "sha512-kyFwXuV/5ymf+IXhS6f0+eAFvydbaBW3zjpT6hUdAh/hbVjTIB5EHBGi0bPoCLSK2wcuz3BrEkB9LrYv1Nm4NQ=="
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,11 @@
|
||||
{
|
||||
"name": "socket.io-chat",
|
||||
"version": "0.0.0",
|
||||
"description": "A simple chat client using socket.io",
|
||||
"main": "index.js",
|
||||
"author": "Grant Timmerman",
|
||||
"private": true,
|
||||
"license": "BSD",
|
||||
"dependencies": {
|
||||
"express": "~4.17.1",
|
||||
"socket.io": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node index.js"
|
||||
}
|
||||
"name": "chat.io"
|
||||
, "description": "example chat application with socket.io"
|
||||
, "version": "0.0.1"
|
||||
, "dependencies": {
|
||||
"express": "2.5.5"
|
||||
, "jade": "0.16.4"
|
||||
, "stylus": "0.19.0"
|
||||
, "nib": "0.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Socket.IO Chat Example</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<ul class="pages">
|
||||
<li class="chat page">
|
||||
<div class="chatArea">
|
||||
<ul class="messages"></ul>
|
||||
</div>
|
||||
<input class="inputMessage" placeholder="Type here..."/>
|
||||
</li>
|
||||
<li class="login page">
|
||||
<div class="form">
|
||||
<h3 class="title">What's your nickname?</h3>
|
||||
<input class="usernameInput" type="text" maxlength="14" />
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<script src="https://code.jquery.com/jquery-1.10.2.min.js"></script>
|
||||
<script src="/socket.io/socket.io.js"></script>
|
||||
<script src="/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,278 +0,0 @@
|
||||
$(function() {
|
||||
const FADE_TIME = 150; // ms
|
||||
const TYPING_TIMER_LENGTH = 400; // ms
|
||||
const COLORS = [
|
||||
'#e21400', '#91580f', '#f8a700', '#f78b00',
|
||||
'#58dc00', '#287b00', '#a8f07a', '#4ae8c4',
|
||||
'#3b88eb', '#3824aa', '#a700ff', '#d300e7'
|
||||
];
|
||||
|
||||
// Initialize variables
|
||||
const $window = $(window);
|
||||
const $usernameInput = $('.usernameInput'); // Input for username
|
||||
const $messages = $('.messages'); // Messages area
|
||||
const $inputMessage = $('.inputMessage'); // Input message input box
|
||||
|
||||
const $loginPage = $('.login.page'); // The login page
|
||||
const $chatPage = $('.chat.page'); // The chatroom page
|
||||
|
||||
const socket = io();
|
||||
|
||||
// Prompt for setting a username
|
||||
let username;
|
||||
let connected = false;
|
||||
let typing = false;
|
||||
let lastTypingTime;
|
||||
let $currentInput = $usernameInput.focus();
|
||||
|
||||
const addParticipantsMessage = (data) => {
|
||||
let message = '';
|
||||
if (data.numUsers === 1) {
|
||||
message += `there's 1 participant`;
|
||||
} else {
|
||||
message += `there are ${data.numUsers} participants`;
|
||||
}
|
||||
log(message);
|
||||
}
|
||||
|
||||
// Sets the client's username
|
||||
const setUsername = () => {
|
||||
username = cleanInput($usernameInput.val().trim());
|
||||
|
||||
// If the username is valid
|
||||
if (username) {
|
||||
$loginPage.fadeOut();
|
||||
$chatPage.show();
|
||||
$loginPage.off('click');
|
||||
$currentInput = $inputMessage.focus();
|
||||
|
||||
// Tell the server your username
|
||||
socket.emit('add user', username);
|
||||
}
|
||||
}
|
||||
|
||||
// Sends a chat message
|
||||
const sendMessage = () => {
|
||||
let message = $inputMessage.val();
|
||||
// Prevent markup from being injected into the message
|
||||
message = cleanInput(message);
|
||||
// if there is a non-empty message and a socket connection
|
||||
if (message && connected) {
|
||||
$inputMessage.val('');
|
||||
addChatMessage({ username, message });
|
||||
// tell server to execute 'new message' and send along one parameter
|
||||
socket.emit('new message', message);
|
||||
}
|
||||
}
|
||||
|
||||
// Log a message
|
||||
const log = (message, options) => {
|
||||
const $el = $('<li>').addClass('log').text(message);
|
||||
addMessageElement($el, options);
|
||||
}
|
||||
|
||||
// Adds the visual chat message to the message list
|
||||
const addChatMessage = (data, options) => {
|
||||
// Don't fade the message in if there is an 'X was typing'
|
||||
const $typingMessages = getTypingMessages(data);
|
||||
if ($typingMessages.length !== 0) {
|
||||
options.fade = false;
|
||||
$typingMessages.remove();
|
||||
}
|
||||
|
||||
const $usernameDiv = $('<span class="username"/>')
|
||||
.text(data.username)
|
||||
.css('color', getUsernameColor(data.username));
|
||||
const $messageBodyDiv = $('<span class="messageBody">')
|
||||
.text(data.message);
|
||||
|
||||
const typingClass = data.typing ? 'typing' : '';
|
||||
const $messageDiv = $('<li class="message"/>')
|
||||
.data('username', data.username)
|
||||
.addClass(typingClass)
|
||||
.append($usernameDiv, $messageBodyDiv);
|
||||
|
||||
addMessageElement($messageDiv, options);
|
||||
}
|
||||
|
||||
// Adds the visual chat typing message
|
||||
const addChatTyping = (data) => {
|
||||
data.typing = true;
|
||||
data.message = 'is typing';
|
||||
addChatMessage(data);
|
||||
}
|
||||
|
||||
// Removes the visual chat typing message
|
||||
const removeChatTyping = (data) => {
|
||||
getTypingMessages(data).fadeOut(function () {
|
||||
$(this).remove();
|
||||
});
|
||||
}
|
||||
|
||||
// Adds a message element to the messages and scrolls to the bottom
|
||||
// el - The element to add as a message
|
||||
// options.fade - If the element should fade-in (default = true)
|
||||
// options.prepend - If the element should prepend
|
||||
// all other messages (default = false)
|
||||
const addMessageElement = (el, options) => {
|
||||
const $el = $(el);
|
||||
// Setup default options
|
||||
if (!options) {
|
||||
options = {};
|
||||
}
|
||||
if (typeof options.fade === 'undefined') {
|
||||
options.fade = true;
|
||||
}
|
||||
if (typeof options.prepend === 'undefined') {
|
||||
options.prepend = false;
|
||||
}
|
||||
|
||||
// Apply options
|
||||
if (options.fade) {
|
||||
$el.hide().fadeIn(FADE_TIME);
|
||||
}
|
||||
if (options.prepend) {
|
||||
$messages.prepend($el);
|
||||
} else {
|
||||
$messages.append($el);
|
||||
}
|
||||
|
||||
$messages[0].scrollTop = $messages[0].scrollHeight;
|
||||
}
|
||||
|
||||
// Prevents input from having injected markup
|
||||
const cleanInput = (input) => {
|
||||
return $('<div/>').text(input).html();
|
||||
}
|
||||
|
||||
// Updates the typing event
|
||||
const updateTyping = () => {
|
||||
if (connected) {
|
||||
if (!typing) {
|
||||
typing = true;
|
||||
socket.emit('typing');
|
||||
}
|
||||
lastTypingTime = (new Date()).getTime();
|
||||
|
||||
setTimeout(() => {
|
||||
const typingTimer = (new Date()).getTime();
|
||||
const timeDiff = typingTimer - lastTypingTime;
|
||||
if (timeDiff >= TYPING_TIMER_LENGTH && typing) {
|
||||
socket.emit('stop typing');
|
||||
typing = false;
|
||||
}
|
||||
}, TYPING_TIMER_LENGTH);
|
||||
}
|
||||
}
|
||||
|
||||
// Gets the 'X is typing' messages of a user
|
||||
const getTypingMessages = (data) => {
|
||||
return $('.typing.message').filter(function (i) {
|
||||
return $(this).data('username') === data.username;
|
||||
});
|
||||
}
|
||||
|
||||
// Gets the color of a username through our hash function
|
||||
const getUsernameColor = (username) => {
|
||||
// Compute hash code
|
||||
let hash = 7;
|
||||
for (let i = 0; i < username.length; i++) {
|
||||
hash = username.charCodeAt(i) + (hash << 5) - hash;
|
||||
}
|
||||
// Calculate color
|
||||
const index = Math.abs(hash % COLORS.length);
|
||||
return COLORS[index];
|
||||
}
|
||||
|
||||
// Keyboard events
|
||||
|
||||
$window.keydown(event => {
|
||||
// Auto-focus the current input when a key is typed
|
||||
if (!(event.ctrlKey || event.metaKey || event.altKey)) {
|
||||
$currentInput.focus();
|
||||
}
|
||||
// When the client hits ENTER on their keyboard
|
||||
if (event.which === 13) {
|
||||
if (username) {
|
||||
sendMessage();
|
||||
socket.emit('stop typing');
|
||||
typing = false;
|
||||
} else {
|
||||
setUsername();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$inputMessage.on('input', () => {
|
||||
updateTyping();
|
||||
});
|
||||
|
||||
// Click events
|
||||
|
||||
// Focus input when clicking anywhere on login page
|
||||
$loginPage.click(() => {
|
||||
$currentInput.focus();
|
||||
});
|
||||
|
||||
// Focus input when clicking on the message input's border
|
||||
$inputMessage.click(() => {
|
||||
$inputMessage.focus();
|
||||
});
|
||||
|
||||
// Socket events
|
||||
|
||||
// Whenever the server emits 'login', log the login message
|
||||
socket.on('login', (data) => {
|
||||
connected = true;
|
||||
// Display the welcome message
|
||||
const message = 'Welcome to Socket.IO Chat – ';
|
||||
log(message, {
|
||||
prepend: true
|
||||
});
|
||||
addParticipantsMessage(data);
|
||||
});
|
||||
|
||||
// Whenever the server emits 'new message', update the chat body
|
||||
socket.on('new message', (data) => {
|
||||
addChatMessage(data);
|
||||
});
|
||||
|
||||
// Whenever the server emits 'user joined', log it in the chat body
|
||||
socket.on('user joined', (data) => {
|
||||
log(`${data.username} joined`);
|
||||
addParticipantsMessage(data);
|
||||
});
|
||||
|
||||
// Whenever the server emits 'user left', log it in the chat body
|
||||
socket.on('user left', (data) => {
|
||||
log(`${data.username} left`);
|
||||
addParticipantsMessage(data);
|
||||
removeChatTyping(data);
|
||||
});
|
||||
|
||||
// Whenever the server emits 'typing', show the typing message
|
||||
socket.on('typing', (data) => {
|
||||
addChatTyping(data);
|
||||
});
|
||||
|
||||
// Whenever the server emits 'stop typing', kill the typing message
|
||||
socket.on('stop typing', (data) => {
|
||||
removeChatTyping(data);
|
||||
});
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
log('you have been disconnected');
|
||||
});
|
||||
|
||||
socket.on('reconnect', () => {
|
||||
log('you have been reconnected');
|
||||
if (username) {
|
||||
socket.emit('add user', username);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('reconnect_error', () => {
|
||||
log('attempt to reconnect has failed');
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,149 +0,0 @@
|
||||
/* Fix user-agent */
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
font-weight: 300;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
html, input {
|
||||
font-family:
|
||||
"HelveticaNeue-Light",
|
||||
"Helvetica Neue Light",
|
||||
"Helvetica Neue",
|
||||
Helvetica,
|
||||
Arial,
|
||||
"Lucida Grande",
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
/* Pages */
|
||||
|
||||
.pages {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.page {
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Login Page */
|
||||
|
||||
.login.page {
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
.login.page .form {
|
||||
height: 100px;
|
||||
margin-top: -100px;
|
||||
position: absolute;
|
||||
|
||||
text-align: center;
|
||||
top: 50%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.login.page .form .usernameInput {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
border-bottom: 2px solid #fff;
|
||||
outline: none;
|
||||
padding-bottom: 15px;
|
||||
text-align: center;
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
.login.page .title {
|
||||
font-size: 200%;
|
||||
}
|
||||
|
||||
.login.page .usernameInput {
|
||||
font-size: 200%;
|
||||
letter-spacing: 3px;
|
||||
}
|
||||
|
||||
.login.page .title, .login.page .usernameInput {
|
||||
color: #fff;
|
||||
font-weight: 100;
|
||||
}
|
||||
|
||||
/* Chat page */
|
||||
|
||||
.chat.page {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Font */
|
||||
|
||||
.messages {
|
||||
font-size: 150%;
|
||||
}
|
||||
|
||||
.inputMessage {
|
||||
font-size: 100%;
|
||||
}
|
||||
|
||||
.log {
|
||||
color: gray;
|
||||
font-size: 70%;
|
||||
margin: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Messages */
|
||||
|
||||
.chatArea {
|
||||
height: 100%;
|
||||
padding-bottom: 60px;
|
||||
}
|
||||
|
||||
.messages {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
overflow-y: scroll;
|
||||
padding: 10px 20px 10px 20px;
|
||||
}
|
||||
|
||||
.message.typing .messageBody {
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.username {
|
||||
font-weight: 700;
|
||||
overflow: hidden;
|
||||
padding-right: 15px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* Input */
|
||||
|
||||
.inputMessage {
|
||||
border: 10px solid #000;
|
||||
bottom: 0;
|
||||
height: 60px;
|
||||
left: 0;
|
||||
outline: none;
|
||||
padding-left: 10px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
}
|
||||
96
examples/chat/public/stylesheets/mixins.styl
Normal file
96
examples/chat/public/stylesheets/mixins.styl
Normal file
@@ -0,0 +1,96 @@
|
||||
border-radius(n)
|
||||
-webkit-border-radius n
|
||||
-moz-border-radius n
|
||||
border-radius n
|
||||
|
||||
// replace str with val
|
||||
|
||||
replace(expr, str, val)
|
||||
expr = clone(expr)
|
||||
for e, i in expr
|
||||
if str == e
|
||||
expr[i] = val
|
||||
expr
|
||||
|
||||
// normalize gradient point (webkit)
|
||||
|
||||
grad-point(pos)
|
||||
if length(pos) == 1
|
||||
return left pos if pos in (top bottom)
|
||||
return pos top if pos in (left right)
|
||||
else if pos[0] in (top bottom)
|
||||
pos[1] pos[0]
|
||||
else
|
||||
pos
|
||||
|
||||
// implicit color stop position
|
||||
|
||||
pos-in-stops(i, stops)
|
||||
len = length(stops)
|
||||
if len - 1 == i
|
||||
100%
|
||||
else if i
|
||||
unit(i / len * 100, '%')
|
||||
else
|
||||
0%
|
||||
|
||||
// normalize color stops
|
||||
// - (color pos) -> (pos color)
|
||||
// - (color) -> (implied-pos color)
|
||||
|
||||
normalize-stops(stops)
|
||||
stops = clone(stops)
|
||||
for stop, i in stops
|
||||
if length(stop) == 1
|
||||
color = stop[0]
|
||||
stop[0] = pos-in-stops(i, stops)
|
||||
stop[1] = color
|
||||
else if typeof(stop[1]) == 'unit'
|
||||
pos = stop[1]
|
||||
stop[1] = stop[0]
|
||||
stop[0] = pos
|
||||
stops
|
||||
|
||||
// join color stops with the given translation function
|
||||
|
||||
join-stops(stops, translate)
|
||||
str = ''
|
||||
len = length(stops)
|
||||
for stop, i in stops
|
||||
str += ', ' if i
|
||||
pos = stop[0]
|
||||
color = stop[1]
|
||||
str += translate(color, pos)
|
||||
unquote(str)
|
||||
|
||||
// webkit translation function
|
||||
|
||||
webkit-stop(color, pos)
|
||||
s('color-stop(%d, %s)', pos / 100, color)
|
||||
|
||||
// mozilla translation function
|
||||
|
||||
moz-stop(color, pos)
|
||||
s('%s %s', color, pos)
|
||||
|
||||
// create a linear gradient with the given start
|
||||
// position, followed by color stops
|
||||
|
||||
linear-gradient(start, stops...)
|
||||
error('color stops required') unless length(stops)
|
||||
prop = current-property[0]
|
||||
val = current-property[1]
|
||||
stops = normalize-stops(stops)
|
||||
|
||||
// webkit
|
||||
end = grad-point(opposite-position(start))
|
||||
webkit = s('-webkit-gradient(linear, %s, %s, %s)', grad-point(start), end, join-stops(stops, webkit-stop))
|
||||
add-property(prop, replace(val, '__CALL__', webkit))
|
||||
|
||||
// moz
|
||||
stops = join-stops(stops, moz-stop)
|
||||
moz = s('-moz-linear-gradient(%s, %s)', start, stops)
|
||||
add-property(prop, replace(val, '__CALL__', moz))
|
||||
|
||||
// literal
|
||||
s('linear-gradient(%s, %s)', start, stops)
|
||||
188
examples/chat/public/stylesheets/style.css
Normal file
188
examples/chat/public/stylesheets/style.css
Normal file
@@ -0,0 +1,188 @@
|
||||
#chat,
|
||||
#nickname,
|
||||
#messages {
|
||||
width: 600px;
|
||||
}
|
||||
#chat {
|
||||
position: relative;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
#nickname,
|
||||
#connecting {
|
||||
position: absolute;
|
||||
height: 410px;
|
||||
z-index: 100;
|
||||
left: 0;
|
||||
top: 0;
|
||||
background: #fff;
|
||||
text-align: center;
|
||||
width: 600px;
|
||||
font: 15px Georgia;
|
||||
color: #666;
|
||||
display: block;
|
||||
}
|
||||
#nickname .wrap,
|
||||
#connecting .wrap {
|
||||
padding-top: 150px;
|
||||
}
|
||||
#nickname input {
|
||||
border: 1px solid #ccc;
|
||||
padding: 10px;
|
||||
}
|
||||
#nickname input:focus {
|
||||
border-color: #999;
|
||||
outline: 0;
|
||||
}
|
||||
#nickname #nickname-err {
|
||||
color: #8b0000;
|
||||
font-size: 12px;
|
||||
visibility: hidden;
|
||||
}
|
||||
.connected #connecting {
|
||||
display: none;
|
||||
}
|
||||
.nickname-set #nickname {
|
||||
display: none;
|
||||
}
|
||||
#messages {
|
||||
height: 380px;
|
||||
background: #eee;
|
||||
}
|
||||
#messages em {
|
||||
text-shadow: 0 1px 0 #fff;
|
||||
color: #999;
|
||||
}
|
||||
#messages p {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font: 12px Helvetica, Arial;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
#messages p b {
|
||||
display: inline-block;
|
||||
padding-right: 10px;
|
||||
}
|
||||
#messages p:nth-child(even) {
|
||||
background: #fafafa;
|
||||
}
|
||||
#messages #nicknames {
|
||||
background: #ccc;
|
||||
padding: 2px 4px 4px;
|
||||
font: 11px Helvetica;
|
||||
}
|
||||
#messages #nicknames span {
|
||||
color: #000;
|
||||
}
|
||||
#messages #nicknames b {
|
||||
display: inline-block;
|
||||
color: #fff;
|
||||
background: #999;
|
||||
padding: 3px 6px;
|
||||
margin-right: 5px;
|
||||
-webkit-border-radius: 10px;
|
||||
-moz-border-radius: 10px;
|
||||
border-radius: 10px;
|
||||
text-shadow: 0 1px 0 #666;
|
||||
}
|
||||
#messages #lines {
|
||||
height: 355px;
|
||||
overflow: auto;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
#messages #lines::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
#messages #lines::-webkit-scrollbar-button:start:decrement,
|
||||
#messages #lines ::-webkit-scrollbar-button:end:increment {
|
||||
display: block;
|
||||
height: 10px;
|
||||
}
|
||||
#messages #lines::-webkit-scrollbar-button:vertical:increment {
|
||||
background-color: #fff;
|
||||
}
|
||||
#messages #lines::-webkit-scrollbar-track-piece {
|
||||
background-color: #fff;
|
||||
-webkit-border-radius: 3px;
|
||||
}
|
||||
#messages #lines::-webkit-scrollbar-thumb:vertical {
|
||||
height: 50px;
|
||||
background-color: #ccc;
|
||||
-webkit-border-radius: 3px;
|
||||
}
|
||||
#messages #lines::-webkit-scrollbar-thumb:horizontal {
|
||||
width: 50px;
|
||||
background-color: #fff;
|
||||
-webkit-border-radius: 3px;
|
||||
}
|
||||
#send-message {
|
||||
background: #fff;
|
||||
position: relative;
|
||||
}
|
||||
#send-message input {
|
||||
border: none;
|
||||
height: 30px;
|
||||
padding: 0 10px;
|
||||
line-height: 30px;
|
||||
vertical-align: middle;
|
||||
width: 580px;
|
||||
}
|
||||
#send-message input:focus {
|
||||
outline: 0;
|
||||
}
|
||||
#send-message button {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
}
|
||||
button {
|
||||
margin: 0;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
user-select: none;
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
background: #43a1f7;
|
||||
background: -webkit-gradient(linear, left top, left bottom, color-stop(0, #43a1f7), color-stop(1, #377ad0));
|
||||
background: -webkit-linear-gradient(top, #43a1f7 0%, #377ad0 100%);
|
||||
background: -moz-linear-gradient(top, #43a1f7 0%, #377ad0 100%);
|
||||
background: linear-gradient(top, #43a1f7 0%, #377ad0 100%);
|
||||
border: 1px solid #2e70c4;
|
||||
-webkit-border-radius: 16px;
|
||||
-moz-border-radius: 16px;
|
||||
border-radius: 16px;
|
||||
color: #fff;
|
||||
font-family: "lucida grande", sans-serif;
|
||||
font-size: 11px;
|
||||
font-weight: normal;
|
||||
line-height: 1;
|
||||
padding: 3px 10px 5px 10px;
|
||||
text-align: center;
|
||||
text-shadow: 0 -1px 1px #2d6dc0;
|
||||
}
|
||||
button:hover,
|
||||
button.hover {
|
||||
background: darker;
|
||||
background: -webkit-gradient(linear, left top, left bottom, color-stop(0, #43a1f7), color-stop(1, #2e70c4));
|
||||
background: -webkit-linear-gradient(top, #43a1f7 0%, #2e70c4 100%);
|
||||
background: -moz-linear-gradient(top, #43a1f7 0%, #2e70c4 100%);
|
||||
background: linear-gradient(top, #43a1f7 0%, #2e70c4 100%);
|
||||
border: 1px solid #2e70c4;
|
||||
cursor: pointer;
|
||||
text-shadow: 0 -1px 1px #2c6bbb;
|
||||
}
|
||||
button:active,
|
||||
button.active {
|
||||
background: #2e70c4;
|
||||
border: 1px solid #2e70c4;
|
||||
border-bottom: 1px solid #2861aa;
|
||||
text-shadow: 0 -1px 1px #2b67b5;
|
||||
}
|
||||
button:focus,
|
||||
button.focus {
|
||||
outline: none;
|
||||
-webkit-box-shadow: 0 1px 0 0 rgba(255,255,255,0.4), 0 0 4px 0 #377ad0;
|
||||
-moz-box-shadow: 0 1px 0 0 rgba(255,255,255,0.4), 0 0 4px 0 #377ad0;
|
||||
box-shadow: 0 1px 0 0 rgba(255,255,255,0.4), 0 0 4px 0 #377ad0;
|
||||
}
|
||||
118
examples/chat/public/stylesheets/style.styl
Normal file
118
examples/chat/public/stylesheets/style.styl
Normal file
@@ -0,0 +1,118 @@
|
||||
@import 'nib'
|
||||
|
||||
#chat, #nickname, #messages
|
||||
width 600px
|
||||
|
||||
#chat
|
||||
position relative
|
||||
border 1px solid #ccc
|
||||
|
||||
#nickname, #connecting
|
||||
position absolute
|
||||
height 410px
|
||||
z-index 100
|
||||
left 0
|
||||
top 0
|
||||
background #fff
|
||||
text-align center
|
||||
width 600px
|
||||
font 15px Georgia
|
||||
color #666
|
||||
display block
|
||||
.wrap
|
||||
padding-top 150px
|
||||
|
||||
#nickname
|
||||
input
|
||||
border 1px solid #ccc
|
||||
padding 10px
|
||||
&:focus
|
||||
border-color #999
|
||||
outline 0
|
||||
#nickname-err
|
||||
color darkred
|
||||
font-size 12px
|
||||
visibility hidden
|
||||
|
||||
.connected
|
||||
#connecting
|
||||
display none
|
||||
|
||||
.nickname-set
|
||||
#nickname
|
||||
display none
|
||||
|
||||
#messages
|
||||
height 380px
|
||||
background #eee
|
||||
em
|
||||
text-shadow 0 1px 0 #fff
|
||||
color #999
|
||||
p
|
||||
padding 0
|
||||
margin 0
|
||||
font 12px Helvetica, Arial
|
||||
padding 5px 10px
|
||||
b
|
||||
display inline-block
|
||||
padding-right 10px
|
||||
p:nth-child(even)
|
||||
background #fafafa
|
||||
#nicknames
|
||||
background #ccc
|
||||
padding 2px 4px 4px
|
||||
font 11px Helvetica
|
||||
span
|
||||
color black
|
||||
b
|
||||
display inline-block
|
||||
color #fff
|
||||
background #999
|
||||
padding 3px 6px
|
||||
margin-right 5px
|
||||
border-radius 10px
|
||||
text-shadow 0 1px 0 #666
|
||||
#lines
|
||||
height 355px
|
||||
overflow auto
|
||||
overflow-x hidden
|
||||
overflow-y auto
|
||||
&::-webkit-scrollbar
|
||||
width 6px
|
||||
height 6px
|
||||
&::-webkit-scrollbar-button:start:decrement, ::-webkit-scrollbar-button:end:increment
|
||||
display block
|
||||
height 10px
|
||||
&::-webkit-scrollbar-button:vertical:increment
|
||||
background-color #fff
|
||||
&::-webkit-scrollbar-track-piece
|
||||
background-color #fff
|
||||
-webkit-border-radius 3px
|
||||
&::-webkit-scrollbar-thumb:vertical
|
||||
height 50px
|
||||
background-color #ccc
|
||||
-webkit-border-radius 3px
|
||||
&::-webkit-scrollbar-thumb:horizontal
|
||||
width 50px
|
||||
background-color #fff
|
||||
-webkit-border-radius 3px
|
||||
|
||||
#send-message
|
||||
background #fff
|
||||
position relative
|
||||
input
|
||||
border none
|
||||
height 30px
|
||||
padding 0 10px
|
||||
line-height 30px
|
||||
vertical-align middle
|
||||
width 580px
|
||||
&:focus
|
||||
outline 0
|
||||
button
|
||||
position absolute
|
||||
top 5px
|
||||
right 5px
|
||||
|
||||
button
|
||||
download-button()
|
||||
@@ -1,31 +0,0 @@
|
||||
|
||||
# Socket.IO Chat with haproxy & redis
|
||||
|
||||
A simple chat demo for socket.io
|
||||
|
||||
## How to use
|
||||
|
||||
Install [Docker Compose](https://docs.docker.com/compose/install/), then:
|
||||
|
||||
```
|
||||
$ docker-compose up -d
|
||||
```
|
||||
|
||||
And then point your browser to `http://localhost:3000`.
|
||||
|
||||
This will start four Socket.IO nodes, behind a haproxy instance which will loadbalance the requests (using a cookie for sticky sessions, see [cookie](https://cbonte.github.io/haproxy-dconv/1.7/configuration.html#4.2-cookie)).
|
||||
|
||||
Each node connects to the redis backend, which will enable to broadcast to every client, no matter which node it is currently connected to.
|
||||
|
||||
```
|
||||
# you can kill a given node, the client should reconnect to another node
|
||||
$ docker-compose stop server-george
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- Multiple users can join a chat room by each entering a unique username
|
||||
on website load.
|
||||
- Users can type chat messages to the chat room.
|
||||
- A notification is sent to all users when a user joins or leaves
|
||||
the chatroom.
|
||||
@@ -1,51 +0,0 @@
|
||||
|
||||
haproxy:
|
||||
build: ./haproxy
|
||||
links:
|
||||
- server-john
|
||||
- server-paul
|
||||
- server-george
|
||||
- server-ringo
|
||||
ports:
|
||||
- "3000:80"
|
||||
|
||||
server-john:
|
||||
build: ./server
|
||||
links:
|
||||
- redis
|
||||
expose:
|
||||
- "3000"
|
||||
environment:
|
||||
- NAME=John
|
||||
|
||||
server-paul:
|
||||
build: ./server
|
||||
links:
|
||||
- redis
|
||||
expose:
|
||||
- "3000"
|
||||
environment:
|
||||
- NAME=Paul
|
||||
|
||||
server-george:
|
||||
build: ./server
|
||||
links:
|
||||
- redis
|
||||
expose:
|
||||
- "3000"
|
||||
environment:
|
||||
- NAME=George
|
||||
|
||||
server-ringo:
|
||||
build: ./server
|
||||
links:
|
||||
- redis
|
||||
expose:
|
||||
- "3000"
|
||||
environment:
|
||||
- NAME=Ringo
|
||||
|
||||
redis:
|
||||
image: redis:alpine
|
||||
expose:
|
||||
- "6379"
|
||||
@@ -1,2 +0,0 @@
|
||||
FROM haproxy:1.7-alpine
|
||||
COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg
|
||||
@@ -1,31 +0,0 @@
|
||||
# Reference: http://blog.haproxy.com/2012/11/07/websockets-load-balancing-with-haproxy/
|
||||
|
||||
global
|
||||
daemon
|
||||
maxconn 4096
|
||||
nbproc 2
|
||||
|
||||
defaults
|
||||
mode http
|
||||
balance roundrobin
|
||||
option http-server-close
|
||||
timeout connect 5s
|
||||
timeout client 30s
|
||||
timeout client-fin 30s
|
||||
timeout server 30s
|
||||
timeout tunnel 1h
|
||||
default-server inter 1s rise 2 fall 1 on-marked-down shutdown-sessions
|
||||
option forwardfor
|
||||
|
||||
listen chat
|
||||
bind *:80
|
||||
default_backend nodes
|
||||
|
||||
backend nodes
|
||||
option httpchk HEAD /health
|
||||
http-check expect status 200
|
||||
cookie serverid insert
|
||||
server john server-john:3000 cookie john check
|
||||
server paul server-paul:3000 cookie paul check
|
||||
server george server-george:3000 cookie george check
|
||||
server ringo server-ringo:3000 cookie ringo check
|
||||
@@ -1,15 +0,0 @@
|
||||
FROM node:14-alpine
|
||||
|
||||
# Create app directory
|
||||
RUN mkdir -p /usr/src/app
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# Install app dependencies
|
||||
COPY package.json /usr/src/app/
|
||||
RUN npm install --prod
|
||||
|
||||
# Bundle app source
|
||||
COPY . /usr/src/app
|
||||
|
||||
EXPOSE 3000
|
||||
CMD [ "npm", "start" ]
|
||||
@@ -1,87 +0,0 @@
|
||||
// Setup basic express server
|
||||
var express = require('express');
|
||||
var app = express();
|
||||
var server = require('http').createServer(app);
|
||||
var io = require('socket.io')(server);
|
||||
var redis = require('socket.io-redis');
|
||||
var port = process.env.PORT || 3000;
|
||||
var serverName = process.env.NAME || 'Unknown';
|
||||
|
||||
io.adapter(redis({ host: 'redis', port: 6379 }));
|
||||
|
||||
server.listen(port, function () {
|
||||
console.log('Server listening at port %d', port);
|
||||
console.log('Hello, I\'m %s, how can I help?', serverName);
|
||||
});
|
||||
|
||||
// Routing
|
||||
app.use(express.static(__dirname + '/public'));
|
||||
|
||||
// Health check
|
||||
app.head('/health', function (req, res) {
|
||||
res.sendStatus(200);
|
||||
});
|
||||
|
||||
// Chatroom
|
||||
|
||||
var numUsers = 0;
|
||||
|
||||
io.on('connection', function (socket) {
|
||||
socket.emit('my-name-is', serverName);
|
||||
|
||||
var addedUser = false;
|
||||
|
||||
// when the client emits 'new message', this listens and executes
|
||||
socket.on('new message', function (data) {
|
||||
// we tell the client to execute 'new message'
|
||||
socket.broadcast.emit('new message', {
|
||||
username: socket.username,
|
||||
message: data
|
||||
});
|
||||
});
|
||||
|
||||
// when the client emits 'add user', this listens and executes
|
||||
socket.on('add user', function (username) {
|
||||
if (addedUser) return;
|
||||
|
||||
// we store the username in the socket session for this client
|
||||
socket.username = username;
|
||||
++numUsers;
|
||||
addedUser = true;
|
||||
socket.emit('login', {
|
||||
numUsers: numUsers
|
||||
});
|
||||
// echo globally (all clients) that a person has connected
|
||||
socket.broadcast.emit('user joined', {
|
||||
username: socket.username,
|
||||
numUsers: numUsers
|
||||
});
|
||||
});
|
||||
|
||||
// when the client emits 'typing', we broadcast it to others
|
||||
socket.on('typing', function () {
|
||||
socket.broadcast.emit('typing', {
|
||||
username: socket.username
|
||||
});
|
||||
});
|
||||
|
||||
// when the client emits 'stop typing', we broadcast it to others
|
||||
socket.on('stop typing', function () {
|
||||
socket.broadcast.emit('stop typing', {
|
||||
username: socket.username
|
||||
});
|
||||
});
|
||||
|
||||
// when the user disconnects.. perform this
|
||||
socket.on('disconnect', function () {
|
||||
if (addedUser) {
|
||||
--numUsers;
|
||||
|
||||
// echo globally that this client has left
|
||||
socket.broadcast.emit('user left', {
|
||||
username: socket.username,
|
||||
numUsers: numUsers
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"name": "socket.io-chat",
|
||||
"version": "0.0.0",
|
||||
"description": "A simple chat client using socket.io",
|
||||
"main": "index.js",
|
||||
"author": "Grant Timmerman",
|
||||
"private": true,
|
||||
"license": "BSD",
|
||||
"dependencies": {
|
||||
"express": "4.13.4",
|
||||
"socket.io": "^3.1.0",
|
||||
"socket.io-redis": "^6.0.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node index.js"
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Socket.IO Chat Example</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<ul class="pages">
|
||||
<li class="chat page">
|
||||
<div class="chatArea">
|
||||
<ul class="messages"></ul>
|
||||
</div>
|
||||
<input class="inputMessage" placeholder="Type here..."/>
|
||||
</li>
|
||||
<li class="login page">
|
||||
<div class="form">
|
||||
<h3 class="title">What's your nickname?</h3>
|
||||
<input class="usernameInput" type="text" maxlength="14" />
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<script src="https://code.jquery.com/jquery-1.10.2.min.js"></script>
|
||||
<script src="/socket.io/socket.io.js"></script>
|
||||
<script src="/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,286 +0,0 @@
|
||||
$(function() {
|
||||
var FADE_TIME = 150; // ms
|
||||
var TYPING_TIMER_LENGTH = 400; // ms
|
||||
var COLORS = [
|
||||
'#e21400', '#91580f', '#f8a700', '#f78b00',
|
||||
'#58dc00', '#287b00', '#a8f07a', '#4ae8c4',
|
||||
'#3b88eb', '#3824aa', '#a700ff', '#d300e7'
|
||||
];
|
||||
|
||||
// Initialize variables
|
||||
var $window = $(window);
|
||||
var $usernameInput = $('.usernameInput'); // Input for username
|
||||
var $messages = $('.messages'); // Messages area
|
||||
var $inputMessage = $('.inputMessage'); // Input message input box
|
||||
|
||||
var $loginPage = $('.login.page'); // The login page
|
||||
var $chatPage = $('.chat.page'); // The chatroom page
|
||||
|
||||
// Prompt for setting a username
|
||||
var username;
|
||||
var connected = false;
|
||||
var typing = false;
|
||||
var lastTypingTime;
|
||||
var $currentInput = $usernameInput.focus();
|
||||
|
||||
var socket = io();
|
||||
|
||||
function addParticipantsMessage (data) {
|
||||
var message = '';
|
||||
if (data.numUsers === 1) {
|
||||
message += "there's 1 participant";
|
||||
} else {
|
||||
message += "there are " + data.numUsers + " participants";
|
||||
}
|
||||
log(message);
|
||||
}
|
||||
|
||||
// Sets the client's username
|
||||
function setUsername () {
|
||||
username = cleanInput($usernameInput.val().trim());
|
||||
|
||||
// If the username is valid
|
||||
if (username) {
|
||||
$loginPage.fadeOut();
|
||||
$chatPage.show();
|
||||
$loginPage.off('click');
|
||||
$currentInput = $inputMessage.focus();
|
||||
|
||||
// Tell the server your username
|
||||
socket.emit('add user', username);
|
||||
}
|
||||
}
|
||||
|
||||
// Sends a chat message
|
||||
function sendMessage () {
|
||||
var message = $inputMessage.val();
|
||||
// Prevent markup from being injected into the message
|
||||
message = cleanInput(message);
|
||||
// if there is a non-empty message and a socket connection
|
||||
if (message && connected) {
|
||||
$inputMessage.val('');
|
||||
addChatMessage({
|
||||
username: username,
|
||||
message: message
|
||||
});
|
||||
// tell server to execute 'new message' and send along one parameter
|
||||
socket.emit('new message', message);
|
||||
}
|
||||
}
|
||||
|
||||
// Log a message
|
||||
function log (message, options) {
|
||||
var $el = $('<li>').addClass('log').text(message);
|
||||
addMessageElement($el, options);
|
||||
}
|
||||
|
||||
// Adds the visual chat message to the message list
|
||||
function addChatMessage (data, options) {
|
||||
// Don't fade the message in if there is an 'X was typing'
|
||||
var $typingMessages = getTypingMessages(data);
|
||||
options = options || {};
|
||||
if ($typingMessages.length !== 0) {
|
||||
options.fade = false;
|
||||
$typingMessages.remove();
|
||||
}
|
||||
|
||||
var $usernameDiv = $('<span class="username"/>')
|
||||
.text(data.username)
|
||||
.css('color', getUsernameColor(data.username));
|
||||
var $messageBodyDiv = $('<span class="messageBody">')
|
||||
.text(data.message);
|
||||
|
||||
var typingClass = data.typing ? 'typing' : '';
|
||||
var $messageDiv = $('<li class="message"/>')
|
||||
.data('username', data.username)
|
||||
.addClass(typingClass)
|
||||
.append($usernameDiv, $messageBodyDiv);
|
||||
|
||||
addMessageElement($messageDiv, options);
|
||||
}
|
||||
|
||||
// Adds the visual chat typing message
|
||||
function addChatTyping (data) {
|
||||
data.typing = true;
|
||||
data.message = 'is typing';
|
||||
addChatMessage(data);
|
||||
}
|
||||
|
||||
// Removes the visual chat typing message
|
||||
function removeChatTyping (data) {
|
||||
getTypingMessages(data).fadeOut(function () {
|
||||
$(this).remove();
|
||||
});
|
||||
}
|
||||
|
||||
// Adds a message element to the messages and scrolls to the bottom
|
||||
// el - The element to add as a message
|
||||
// options.fade - If the element should fade-in (default = true)
|
||||
// options.prepend - If the element should prepend
|
||||
// all other messages (default = false)
|
||||
function addMessageElement (el, options) {
|
||||
var $el = $(el);
|
||||
|
||||
// Setup default options
|
||||
if (!options) {
|
||||
options = {};
|
||||
}
|
||||
if (typeof options.fade === 'undefined') {
|
||||
options.fade = true;
|
||||
}
|
||||
if (typeof options.prepend === 'undefined') {
|
||||
options.prepend = false;
|
||||
}
|
||||
|
||||
// Apply options
|
||||
if (options.fade) {
|
||||
$el.hide().fadeIn(FADE_TIME);
|
||||
}
|
||||
if (options.prepend) {
|
||||
$messages.prepend($el);
|
||||
} else {
|
||||
$messages.append($el);
|
||||
}
|
||||
$messages[0].scrollTop = $messages[0].scrollHeight;
|
||||
}
|
||||
|
||||
// Prevents input from having injected markup
|
||||
function cleanInput (input) {
|
||||
return $('<div/>').text(input).text();
|
||||
}
|
||||
|
||||
// Updates the typing event
|
||||
function updateTyping () {
|
||||
if (connected) {
|
||||
if (!typing) {
|
||||
typing = true;
|
||||
socket.emit('typing');
|
||||
}
|
||||
lastTypingTime = (new Date()).getTime();
|
||||
|
||||
setTimeout(function () {
|
||||
var typingTimer = (new Date()).getTime();
|
||||
var timeDiff = typingTimer - lastTypingTime;
|
||||
if (timeDiff >= TYPING_TIMER_LENGTH && typing) {
|
||||
socket.emit('stop typing');
|
||||
typing = false;
|
||||
}
|
||||
}, TYPING_TIMER_LENGTH);
|
||||
}
|
||||
}
|
||||
|
||||
// Gets the 'X is typing' messages of a user
|
||||
function getTypingMessages (data) {
|
||||
return $('.typing.message').filter(function (i) {
|
||||
return $(this).data('username') === data.username;
|
||||
});
|
||||
}
|
||||
|
||||
// Gets the color of a username through our hash function
|
||||
function getUsernameColor (username) {
|
||||
// Compute hash code
|
||||
var hash = 7;
|
||||
for (var i = 0; i < username.length; i++) {
|
||||
hash = username.charCodeAt(i) + (hash << 5) - hash;
|
||||
}
|
||||
// Calculate color
|
||||
var index = Math.abs(hash % COLORS.length);
|
||||
return COLORS[index];
|
||||
}
|
||||
|
||||
// Keyboard events
|
||||
|
||||
$window.keydown(function (event) {
|
||||
// Auto-focus the current input when a key is typed
|
||||
if (!(event.ctrlKey || event.metaKey || event.altKey)) {
|
||||
$currentInput.focus();
|
||||
}
|
||||
// When the client hits ENTER on their keyboard
|
||||
if (event.which === 13) {
|
||||
if (username) {
|
||||
sendMessage();
|
||||
socket.emit('stop typing');
|
||||
typing = false;
|
||||
} else {
|
||||
setUsername();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$inputMessage.on('input', function() {
|
||||
updateTyping();
|
||||
});
|
||||
|
||||
// Click events
|
||||
|
||||
// Focus input when clicking anywhere on login page
|
||||
$loginPage.click(function () {
|
||||
$currentInput.focus();
|
||||
});
|
||||
|
||||
// Focus input when clicking on the message input's border
|
||||
$inputMessage.click(function () {
|
||||
$inputMessage.focus();
|
||||
});
|
||||
|
||||
// Socket events
|
||||
|
||||
// Whenever the server emits 'login', log the login message
|
||||
socket.on('login', function (data) {
|
||||
connected = true;
|
||||
// Display the welcome message
|
||||
var message = "Welcome to Socket.IO Chat – ";
|
||||
log(message, {
|
||||
prepend: true
|
||||
});
|
||||
addParticipantsMessage(data);
|
||||
});
|
||||
|
||||
// Whenever the server emits 'new message', update the chat body
|
||||
socket.on('new message', function (data) {
|
||||
addChatMessage(data);
|
||||
});
|
||||
|
||||
// Whenever the server emits 'user joined', log it in the chat body
|
||||
socket.on('user joined', function (data) {
|
||||
log(data.username + ' joined');
|
||||
addParticipantsMessage(data);
|
||||
});
|
||||
|
||||
// Whenever the server emits 'user left', log it in the chat body
|
||||
socket.on('user left', function (data) {
|
||||
log(data.username + ' left');
|
||||
addParticipantsMessage(data);
|
||||
removeChatTyping(data);
|
||||
});
|
||||
|
||||
// Whenever the server emits 'typing', show the typing message
|
||||
socket.on('typing', function (data) {
|
||||
addChatTyping(data);
|
||||
});
|
||||
|
||||
// Whenever the server emits 'stop typing', kill the typing message
|
||||
socket.on('stop typing', function (data) {
|
||||
removeChatTyping(data);
|
||||
});
|
||||
|
||||
socket.on('disconnect', function () {
|
||||
log('you have been disconnected');
|
||||
});
|
||||
|
||||
socket.on('reconnect', function () {
|
||||
log('you have been reconnected');
|
||||
if (username) {
|
||||
socket.emit('add user', username);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('reconnect_error', function () {
|
||||
log('attempt to reconnect has failed');
|
||||
});
|
||||
|
||||
socket.on('my-name-is', function (serverName) {
|
||||
log('host is now ' + serverName);
|
||||
})
|
||||
|
||||
});
|
||||
@@ -1,149 +0,0 @@
|
||||
/* Fix user-agent */
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
font-weight: 300;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
html, input {
|
||||
font-family:
|
||||
"HelveticaNeue-Light",
|
||||
"Helvetica Neue Light",
|
||||
"Helvetica Neue",
|
||||
Helvetica,
|
||||
Arial,
|
||||
"Lucida Grande",
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
/* Pages */
|
||||
|
||||
.pages {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.page {
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Login Page */
|
||||
|
||||
.login.page {
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
.login.page .form {
|
||||
height: 100px;
|
||||
margin-top: -100px;
|
||||
position: absolute;
|
||||
|
||||
text-align: center;
|
||||
top: 50%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.login.page .form .usernameInput {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
border-bottom: 2px solid #fff;
|
||||
outline: none;
|
||||
padding-bottom: 15px;
|
||||
text-align: center;
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
.login.page .title {
|
||||
font-size: 200%;
|
||||
}
|
||||
|
||||
.login.page .usernameInput {
|
||||
font-size: 200%;
|
||||
letter-spacing: 3px;
|
||||
}
|
||||
|
||||
.login.page .title, .login.page .usernameInput {
|
||||
color: #fff;
|
||||
font-weight: 100;
|
||||
}
|
||||
|
||||
/* Chat page */
|
||||
|
||||
.chat.page {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Font */
|
||||
|
||||
.messages {
|
||||
font-size: 150%;
|
||||
}
|
||||
|
||||
.inputMessage {
|
||||
font-size: 100%;
|
||||
}
|
||||
|
||||
.log {
|
||||
color: gray;
|
||||
font-size: 70%;
|
||||
margin: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Messages */
|
||||
|
||||
.chatArea {
|
||||
height: 100%;
|
||||
padding-bottom: 60px;
|
||||
}
|
||||
|
||||
.messages {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
overflow-y: scroll;
|
||||
padding: 10px 20px 10px 20px;
|
||||
}
|
||||
|
||||
.message.typing .messageBody {
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.username {
|
||||
font-weight: 700;
|
||||
overflow: hidden;
|
||||
padding-right: 15px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* Input */
|
||||
|
||||
.inputMessage {
|
||||
border: 10px solid #000;
|
||||
bottom: 0;
|
||||
height: 60px;
|
||||
left: 0;
|
||||
outline: none;
|
||||
padding-left: 10px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
|
||||
# Socket.IO Chat with httpd & redis
|
||||
|
||||
A simple chat demo for socket.io
|
||||
|
||||
## How to use
|
||||
|
||||
Install [Docker Compose](https://docs.docker.com/compose/install/), then:
|
||||
|
||||
```
|
||||
$ docker-compose up -d
|
||||
```
|
||||
|
||||
And then point your browser to `http://localhost:3000`.
|
||||
|
||||
This will start four Socket.IO nodes, behind a httpd proxy which will loadbalance the requests (using a cookie for sticky sessions, see [cookie](http://httpd.apache.org/docs/2.4/fr/mod/mod_proxy_balancer.html)).
|
||||
|
||||
Each node connects to the redis backend, which will enable to broadcast to every client, no matter which node it is currently connected to.
|
||||
|
||||
```
|
||||
# you can kill a given node, the client should reconnect to another node
|
||||
$ docker-compose stop server-george
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- Multiple users can join a chat room by each entering a unique username
|
||||
on website load.
|
||||
- Users can type chat messages to the chat room.
|
||||
- A notification is sent to all users when a user joins or leaves
|
||||
the chatroom.
|
||||
@@ -1,51 +0,0 @@
|
||||
|
||||
httpd:
|
||||
build: ./httpd
|
||||
links:
|
||||
- server-john
|
||||
- server-paul
|
||||
- server-george
|
||||
- server-ringo
|
||||
ports:
|
||||
- "3000:80"
|
||||
|
||||
server-john:
|
||||
build: ./server
|
||||
links:
|
||||
- redis
|
||||
expose:
|
||||
- "3000"
|
||||
environment:
|
||||
- NAME=John
|
||||
|
||||
server-paul:
|
||||
build: ./server
|
||||
links:
|
||||
- redis
|
||||
expose:
|
||||
- "3000"
|
||||
environment:
|
||||
- NAME=Paul
|
||||
|
||||
server-george:
|
||||
build: ./server
|
||||
links:
|
||||
- redis
|
||||
expose:
|
||||
- "3000"
|
||||
environment:
|
||||
- NAME=George
|
||||
|
||||
server-ringo:
|
||||
build: ./server
|
||||
links:
|
||||
- redis
|
||||
expose:
|
||||
- "3000"
|
||||
environment:
|
||||
- NAME=Ringo
|
||||
|
||||
redis:
|
||||
image: redis:alpine
|
||||
expose:
|
||||
- "6379"
|
||||
@@ -1,2 +0,0 @@
|
||||
FROM httpd:2.4-alpine
|
||||
COPY ./httpd.conf /usr/local/apache2/conf/httpd.conf
|
||||
@@ -1,54 +0,0 @@
|
||||
|
||||
Listen 80
|
||||
|
||||
ServerName localhost
|
||||
|
||||
LoadModule mpm_event_module modules/mod_mpm_event.so
|
||||
|
||||
LoadModule authn_file_module modules/mod_authn_file.so
|
||||
LoadModule authn_core_module modules/mod_authn_core.so
|
||||
LoadModule authz_host_module modules/mod_authz_host.so
|
||||
LoadModule authz_groupfile_module modules/mod_authz_groupfile.so
|
||||
LoadModule authz_user_module modules/mod_authz_user.so
|
||||
LoadModule authz_core_module modules/mod_authz_core.so
|
||||
|
||||
LoadModule headers_module modules/mod_headers.so
|
||||
LoadModule lbmethod_byrequests_module modules/mod_lbmethod_byrequests.so
|
||||
LoadModule proxy_module modules/mod_proxy.so
|
||||
LoadModule proxy_balancer_module modules/mod_proxy_balancer.so
|
||||
LoadModule proxy_http_module modules/mod_proxy_http.so
|
||||
LoadModule proxy_wstunnel_module modules/mod_proxy_wstunnel.so
|
||||
LoadModule rewrite_module modules/mod_rewrite.so
|
||||
LoadModule slotmem_shm_module modules/mod_slotmem_shm.so
|
||||
LoadModule unixd_module modules/mod_unixd.so
|
||||
|
||||
User daemon
|
||||
Group daemon
|
||||
|
||||
ErrorLog /proc/self/fd/2
|
||||
|
||||
Header add Set-Cookie "SERVERID=sticky.%{BALANCER_WORKER_ROUTE}e; path=/" env=BALANCER_ROUTE_CHANGED
|
||||
|
||||
<Proxy "balancer://nodes_polling">
|
||||
BalancerMember "http://server-john:3000" route=john
|
||||
BalancerMember "http://server-paul:3000" route=paul
|
||||
BalancerMember "http://server-george:3000" route=george
|
||||
BalancerMember "http://server-ringo:3000" route=ringo
|
||||
ProxySet stickysession=SERVERID
|
||||
</Proxy>
|
||||
|
||||
<Proxy "balancer://nodes_ws">
|
||||
BalancerMember "ws://server-john:3000" route=john
|
||||
BalancerMember "ws://server-paul:3000" route=paul
|
||||
BalancerMember "ws://server-george:3000" route=george
|
||||
BalancerMember "ws://server-ringo:3000" route=ringo
|
||||
ProxySet stickysession=SERVERID
|
||||
</Proxy>
|
||||
|
||||
RewriteEngine On
|
||||
RewriteCond %{HTTP:Upgrade} =websocket [NC]
|
||||
RewriteRule /(.*) balancer://nodes_ws/$1 [P,L]
|
||||
RewriteCond %{HTTP:Upgrade} !=websocket [NC]
|
||||
RewriteRule /(.*) balancer://nodes_polling/$1 [P,L]
|
||||
|
||||
ProxyTimeout 3
|
||||
@@ -1,15 +0,0 @@
|
||||
FROM mhart/alpine-node:6
|
||||
|
||||
# Create app directory
|
||||
RUN mkdir -p /usr/src/app
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# Install app dependencies
|
||||
COPY package.json /usr/src/app/
|
||||
RUN npm install --prod
|
||||
|
||||
# Bundle app source
|
||||
COPY . /usr/src/app
|
||||
|
||||
EXPOSE 3000
|
||||
CMD [ "npm", "start" ]
|
||||
@@ -1,82 +0,0 @@
|
||||
// Setup basic express server
|
||||
var express = require('express');
|
||||
var app = express();
|
||||
var server = require('http').createServer(app);
|
||||
var io = require('socket.io')(server);
|
||||
var redis = require('socket.io-redis');
|
||||
var port = process.env.PORT || 3000;
|
||||
var serverName = process.env.NAME || 'Unknown';
|
||||
|
||||
io.adapter(redis({ host: 'redis', port: 6379 }));
|
||||
|
||||
server.listen(port, function () {
|
||||
console.log('Server listening at port %d', port);
|
||||
console.log('Hello, I\'m %s, how can I help?', serverName);
|
||||
});
|
||||
|
||||
// Routing
|
||||
app.use(express.static(__dirname + '/public'));
|
||||
|
||||
// Chatroom
|
||||
|
||||
var numUsers = 0;
|
||||
|
||||
io.on('connection', function (socket) {
|
||||
socket.emit('my-name-is', serverName);
|
||||
|
||||
var addedUser = false;
|
||||
|
||||
// when the client emits 'new message', this listens and executes
|
||||
socket.on('new message', function (data) {
|
||||
// we tell the client to execute 'new message'
|
||||
socket.broadcast.emit('new message', {
|
||||
username: socket.username,
|
||||
message: data
|
||||
});
|
||||
});
|
||||
|
||||
// when the client emits 'add user', this listens and executes
|
||||
socket.on('add user', function (username) {
|
||||
if (addedUser) return;
|
||||
|
||||
// we store the username in the socket session for this client
|
||||
socket.username = username;
|
||||
++numUsers;
|
||||
addedUser = true;
|
||||
socket.emit('login', {
|
||||
numUsers: numUsers
|
||||
});
|
||||
// echo globally (all clients) that a person has connected
|
||||
socket.broadcast.emit('user joined', {
|
||||
username: socket.username,
|
||||
numUsers: numUsers
|
||||
});
|
||||
});
|
||||
|
||||
// when the client emits 'typing', we broadcast it to others
|
||||
socket.on('typing', function () {
|
||||
socket.broadcast.emit('typing', {
|
||||
username: socket.username
|
||||
});
|
||||
});
|
||||
|
||||
// when the client emits 'stop typing', we broadcast it to others
|
||||
socket.on('stop typing', function () {
|
||||
socket.broadcast.emit('stop typing', {
|
||||
username: socket.username
|
||||
});
|
||||
});
|
||||
|
||||
// when the user disconnects.. perform this
|
||||
socket.on('disconnect', function () {
|
||||
if (addedUser) {
|
||||
--numUsers;
|
||||
|
||||
// echo globally that this client has left
|
||||
socket.broadcast.emit('user left', {
|
||||
username: socket.username,
|
||||
numUsers: numUsers
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"name": "socket.io-chat",
|
||||
"version": "0.0.0",
|
||||
"description": "A simple chat client using socket.io",
|
||||
"main": "index.js",
|
||||
"author": "Grant Timmerman",
|
||||
"private": true,
|
||||
"license": "BSD",
|
||||
"dependencies": {
|
||||
"express": "4.13.4",
|
||||
"socket.io": "^3.1.0",
|
||||
"socket.io-redis": "^6.0.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node index.js"
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Socket.IO Chat Example</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<ul class="pages">
|
||||
<li class="chat page">
|
||||
<div class="chatArea">
|
||||
<ul class="messages"></ul>
|
||||
</div>
|
||||
<input class="inputMessage" placeholder="Type here..."/>
|
||||
</li>
|
||||
<li class="login page">
|
||||
<div class="form">
|
||||
<h3 class="title">What's your nickname?</h3>
|
||||
<input class="usernameInput" type="text" maxlength="14" />
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<script src="https://code.jquery.com/jquery-1.10.2.min.js"></script>
|
||||
<script src="/socket.io/socket.io.js"></script>
|
||||
<script src="/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,286 +0,0 @@
|
||||
$(function() {
|
||||
var FADE_TIME = 150; // ms
|
||||
var TYPING_TIMER_LENGTH = 400; // ms
|
||||
var COLORS = [
|
||||
'#e21400', '#91580f', '#f8a700', '#f78b00',
|
||||
'#58dc00', '#287b00', '#a8f07a', '#4ae8c4',
|
||||
'#3b88eb', '#3824aa', '#a700ff', '#d300e7'
|
||||
];
|
||||
|
||||
// Initialize variables
|
||||
var $window = $(window);
|
||||
var $usernameInput = $('.usernameInput'); // Input for username
|
||||
var $messages = $('.messages'); // Messages area
|
||||
var $inputMessage = $('.inputMessage'); // Input message input box
|
||||
|
||||
var $loginPage = $('.login.page'); // The login page
|
||||
var $chatPage = $('.chat.page'); // The chatroom page
|
||||
|
||||
// Prompt for setting a username
|
||||
var username;
|
||||
var connected = false;
|
||||
var typing = false;
|
||||
var lastTypingTime;
|
||||
var $currentInput = $usernameInput.focus();
|
||||
|
||||
var socket = io();
|
||||
|
||||
function addParticipantsMessage (data) {
|
||||
var message = '';
|
||||
if (data.numUsers === 1) {
|
||||
message += "there's 1 participant";
|
||||
} else {
|
||||
message += "there are " + data.numUsers + " participants";
|
||||
}
|
||||
log(message);
|
||||
}
|
||||
|
||||
// Sets the client's username
|
||||
function setUsername () {
|
||||
username = cleanInput($usernameInput.val().trim());
|
||||
|
||||
// If the username is valid
|
||||
if (username) {
|
||||
$loginPage.fadeOut();
|
||||
$chatPage.show();
|
||||
$loginPage.off('click');
|
||||
$currentInput = $inputMessage.focus();
|
||||
|
||||
// Tell the server your username
|
||||
socket.emit('add user', username);
|
||||
}
|
||||
}
|
||||
|
||||
// Sends a chat message
|
||||
function sendMessage () {
|
||||
var message = $inputMessage.val();
|
||||
// Prevent markup from being injected into the message
|
||||
message = cleanInput(message);
|
||||
// if there is a non-empty message and a socket connection
|
||||
if (message && connected) {
|
||||
$inputMessage.val('');
|
||||
addChatMessage({
|
||||
username: username,
|
||||
message: message
|
||||
});
|
||||
// tell server to execute 'new message' and send along one parameter
|
||||
socket.emit('new message', message);
|
||||
}
|
||||
}
|
||||
|
||||
// Log a message
|
||||
function log (message, options) {
|
||||
var $el = $('<li>').addClass('log').text(message);
|
||||
addMessageElement($el, options);
|
||||
}
|
||||
|
||||
// Adds the visual chat message to the message list
|
||||
function addChatMessage (data, options) {
|
||||
// Don't fade the message in if there is an 'X was typing'
|
||||
var $typingMessages = getTypingMessages(data);
|
||||
options = options || {};
|
||||
if ($typingMessages.length !== 0) {
|
||||
options.fade = false;
|
||||
$typingMessages.remove();
|
||||
}
|
||||
|
||||
var $usernameDiv = $('<span class="username"/>')
|
||||
.text(data.username)
|
||||
.css('color', getUsernameColor(data.username));
|
||||
var $messageBodyDiv = $('<span class="messageBody">')
|
||||
.text(data.message);
|
||||
|
||||
var typingClass = data.typing ? 'typing' : '';
|
||||
var $messageDiv = $('<li class="message"/>')
|
||||
.data('username', data.username)
|
||||
.addClass(typingClass)
|
||||
.append($usernameDiv, $messageBodyDiv);
|
||||
|
||||
addMessageElement($messageDiv, options);
|
||||
}
|
||||
|
||||
// Adds the visual chat typing message
|
||||
function addChatTyping (data) {
|
||||
data.typing = true;
|
||||
data.message = 'is typing';
|
||||
addChatMessage(data);
|
||||
}
|
||||
|
||||
// Removes the visual chat typing message
|
||||
function removeChatTyping (data) {
|
||||
getTypingMessages(data).fadeOut(function () {
|
||||
$(this).remove();
|
||||
});
|
||||
}
|
||||
|
||||
// Adds a message element to the messages and scrolls to the bottom
|
||||
// el - The element to add as a message
|
||||
// options.fade - If the element should fade-in (default = true)
|
||||
// options.prepend - If the element should prepend
|
||||
// all other messages (default = false)
|
||||
function addMessageElement (el, options) {
|
||||
var $el = $(el);
|
||||
|
||||
// Setup default options
|
||||
if (!options) {
|
||||
options = {};
|
||||
}
|
||||
if (typeof options.fade === 'undefined') {
|
||||
options.fade = true;
|
||||
}
|
||||
if (typeof options.prepend === 'undefined') {
|
||||
options.prepend = false;
|
||||
}
|
||||
|
||||
// Apply options
|
||||
if (options.fade) {
|
||||
$el.hide().fadeIn(FADE_TIME);
|
||||
}
|
||||
if (options.prepend) {
|
||||
$messages.prepend($el);
|
||||
} else {
|
||||
$messages.append($el);
|
||||
}
|
||||
$messages[0].scrollTop = $messages[0].scrollHeight;
|
||||
}
|
||||
|
||||
// Prevents input from having injected markup
|
||||
function cleanInput (input) {
|
||||
return $('<div/>').text(input).text();
|
||||
}
|
||||
|
||||
// Updates the typing event
|
||||
function updateTyping () {
|
||||
if (connected) {
|
||||
if (!typing) {
|
||||
typing = true;
|
||||
socket.emit('typing');
|
||||
}
|
||||
lastTypingTime = (new Date()).getTime();
|
||||
|
||||
setTimeout(function () {
|
||||
var typingTimer = (new Date()).getTime();
|
||||
var timeDiff = typingTimer - lastTypingTime;
|
||||
if (timeDiff >= TYPING_TIMER_LENGTH && typing) {
|
||||
socket.emit('stop typing');
|
||||
typing = false;
|
||||
}
|
||||
}, TYPING_TIMER_LENGTH);
|
||||
}
|
||||
}
|
||||
|
||||
// Gets the 'X is typing' messages of a user
|
||||
function getTypingMessages (data) {
|
||||
return $('.typing.message').filter(function (i) {
|
||||
return $(this).data('username') === data.username;
|
||||
});
|
||||
}
|
||||
|
||||
// Gets the color of a username through our hash function
|
||||
function getUsernameColor (username) {
|
||||
// Compute hash code
|
||||
var hash = 7;
|
||||
for (var i = 0; i < username.length; i++) {
|
||||
hash = username.charCodeAt(i) + (hash << 5) - hash;
|
||||
}
|
||||
// Calculate color
|
||||
var index = Math.abs(hash % COLORS.length);
|
||||
return COLORS[index];
|
||||
}
|
||||
|
||||
// Keyboard events
|
||||
|
||||
$window.keydown(function (event) {
|
||||
// Auto-focus the current input when a key is typed
|
||||
if (!(event.ctrlKey || event.metaKey || event.altKey)) {
|
||||
$currentInput.focus();
|
||||
}
|
||||
// When the client hits ENTER on their keyboard
|
||||
if (event.which === 13) {
|
||||
if (username) {
|
||||
sendMessage();
|
||||
socket.emit('stop typing');
|
||||
typing = false;
|
||||
} else {
|
||||
setUsername();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$inputMessage.on('input', function() {
|
||||
updateTyping();
|
||||
});
|
||||
|
||||
// Click events
|
||||
|
||||
// Focus input when clicking anywhere on login page
|
||||
$loginPage.click(function () {
|
||||
$currentInput.focus();
|
||||
});
|
||||
|
||||
// Focus input when clicking on the message input's border
|
||||
$inputMessage.click(function () {
|
||||
$inputMessage.focus();
|
||||
});
|
||||
|
||||
// Socket events
|
||||
|
||||
// Whenever the server emits 'login', log the login message
|
||||
socket.on('login', function (data) {
|
||||
connected = true;
|
||||
// Display the welcome message
|
||||
var message = "Welcome to Socket.IO Chat – ";
|
||||
log(message, {
|
||||
prepend: true
|
||||
});
|
||||
addParticipantsMessage(data);
|
||||
});
|
||||
|
||||
// Whenever the server emits 'new message', update the chat body
|
||||
socket.on('new message', function (data) {
|
||||
addChatMessage(data);
|
||||
});
|
||||
|
||||
// Whenever the server emits 'user joined', log it in the chat body
|
||||
socket.on('user joined', function (data) {
|
||||
log(data.username + ' joined');
|
||||
addParticipantsMessage(data);
|
||||
});
|
||||
|
||||
// Whenever the server emits 'user left', log it in the chat body
|
||||
socket.on('user left', function (data) {
|
||||
log(data.username + ' left');
|
||||
addParticipantsMessage(data);
|
||||
removeChatTyping(data);
|
||||
});
|
||||
|
||||
// Whenever the server emits 'typing', show the typing message
|
||||
socket.on('typing', function (data) {
|
||||
addChatTyping(data);
|
||||
});
|
||||
|
||||
// Whenever the server emits 'stop typing', kill the typing message
|
||||
socket.on('stop typing', function (data) {
|
||||
removeChatTyping(data);
|
||||
});
|
||||
|
||||
socket.on('disconnect', function () {
|
||||
log('you have been disconnected');
|
||||
});
|
||||
|
||||
socket.on('reconnect', function () {
|
||||
log('you have been reconnected');
|
||||
if (username) {
|
||||
socket.emit('add user', username);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('reconnect_error', function () {
|
||||
log('attempt to reconnect has failed');
|
||||
});
|
||||
|
||||
socket.on('my-name-is', function (serverName) {
|
||||
log('host is now ' + serverName);
|
||||
})
|
||||
|
||||
});
|
||||
@@ -1,149 +0,0 @@
|
||||
/* Fix user-agent */
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
font-weight: 300;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
html, input {
|
||||
font-family:
|
||||
"HelveticaNeue-Light",
|
||||
"Helvetica Neue Light",
|
||||
"Helvetica Neue",
|
||||
Helvetica,
|
||||
Arial,
|
||||
"Lucida Grande",
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
/* Pages */
|
||||
|
||||
.pages {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.page {
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Login Page */
|
||||
|
||||
.login.page {
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
.login.page .form {
|
||||
height: 100px;
|
||||
margin-top: -100px;
|
||||
position: absolute;
|
||||
|
||||
text-align: center;
|
||||
top: 50%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.login.page .form .usernameInput {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
border-bottom: 2px solid #fff;
|
||||
outline: none;
|
||||
padding-bottom: 15px;
|
||||
text-align: center;
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
.login.page .title {
|
||||
font-size: 200%;
|
||||
}
|
||||
|
||||
.login.page .usernameInput {
|
||||
font-size: 200%;
|
||||
letter-spacing: 3px;
|
||||
}
|
||||
|
||||
.login.page .title, .login.page .usernameInput {
|
||||
color: #fff;
|
||||
font-weight: 100;
|
||||
}
|
||||
|
||||
/* Chat page */
|
||||
|
||||
.chat.page {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Font */
|
||||
|
||||
.messages {
|
||||
font-size: 150%;
|
||||
}
|
||||
|
||||
.inputMessage {
|
||||
font-size: 100%;
|
||||
}
|
||||
|
||||
.log {
|
||||
color: gray;
|
||||
font-size: 70%;
|
||||
margin: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Messages */
|
||||
|
||||
.chatArea {
|
||||
height: 100%;
|
||||
padding-bottom: 60px;
|
||||
}
|
||||
|
||||
.messages {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
overflow-y: scroll;
|
||||
padding: 10px 20px 10px 20px;
|
||||
}
|
||||
|
||||
.message.typing .messageBody {
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.username {
|
||||
font-weight: 700;
|
||||
overflow: hidden;
|
||||
padding-right: 15px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* Input */
|
||||
|
||||
.inputMessage {
|
||||
border: 10px solid #000;
|
||||
bottom: 0;
|
||||
height: 60px;
|
||||
left: 0;
|
||||
outline: none;
|
||||
padding-left: 10px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
|
||||
# Socket.IO Chat with nginx & redis
|
||||
|
||||
A simple chat demo for socket.io
|
||||
|
||||
## How to use
|
||||
|
||||
Install [Docker Compose](https://docs.docker.com/compose/install/), then:
|
||||
|
||||
```
|
||||
$ docker-compose up -d
|
||||
```
|
||||
|
||||
And then point your browser to `http://localhost:3000`.
|
||||
|
||||
This will start four Socket.IO nodes, behind a nginx proxy which will loadbalance the requests (using the IP of the client, see [ip_hash](http://nginx.org/en/docs/http/ngx_http_upstream_module.html#ip_hash)).
|
||||
|
||||
Each node connects to the redis backend, which will enable to broadcast to every client, no matter which node it is currently connected to.
|
||||
|
||||
```
|
||||
# you can kill a given node, the client should reconnect to another node
|
||||
$ docker-compose stop server-george
|
||||
```
|
||||
|
||||
A `client` container is included in the `docker-compose.yml` file, in order to test the routing.
|
||||
|
||||
You can create additional `client` containers with:
|
||||
|
||||
```
|
||||
$ docker-compose up -d --scale=client=10 client
|
||||
# and then
|
||||
$ docker-compose logs client
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- Multiple users can join a chat room by each entering a unique username
|
||||
on website load.
|
||||
- Users can type chat messages to the chat room.
|
||||
- A notification is sent to all users when a user joins or leaves
|
||||
the chatroom.
|
||||
@@ -1,15 +0,0 @@
|
||||
FROM node:14-alpine
|
||||
|
||||
# Create app directory
|
||||
RUN mkdir -p /usr/src/app
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# Install app dependencies
|
||||
COPY package.json /usr/src/app/
|
||||
RUN npm install --prod
|
||||
|
||||
# Bundle app source
|
||||
COPY . /usr/src/app
|
||||
|
||||
EXPOSE 3000
|
||||
CMD [ "npm", "start" ]
|
||||
@@ -1,13 +0,0 @@
|
||||
const socket = require('socket.io-client')('ws://nginx');
|
||||
|
||||
socket.on('connect', () => {
|
||||
console.log('connected');
|
||||
});
|
||||
|
||||
socket.on('my-name-is', (serverName) => {
|
||||
console.log(`connected to ${serverName}`);
|
||||
});
|
||||
|
||||
socket.on('disconnect', (reason) => {
|
||||
console.log(`disconnected due to ${reason}`);
|
||||
});
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"name": "socket.io-chat",
|
||||
"version": "0.0.0",
|
||||
"description": "A simple chat client using socket.io",
|
||||
"main": "index.js",
|
||||
"author": "Grant Timmerman",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"socket.io-client": "^3.1.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node index.js"
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
|
||||
nginx:
|
||||
build: ./nginx
|
||||
links:
|
||||
- server-john
|
||||
- server-paul
|
||||
- server-george
|
||||
- server-ringo
|
||||
ports:
|
||||
- "3000:80"
|
||||
|
||||
server-john:
|
||||
build: ./server
|
||||
links:
|
||||
- redis
|
||||
expose:
|
||||
- "3000"
|
||||
environment:
|
||||
- NAME=John
|
||||
|
||||
server-paul:
|
||||
build: ./server
|
||||
links:
|
||||
- redis
|
||||
expose:
|
||||
- "3000"
|
||||
environment:
|
||||
- NAME=Paul
|
||||
|
||||
server-george:
|
||||
build: ./server
|
||||
links:
|
||||
- redis
|
||||
expose:
|
||||
- "3000"
|
||||
environment:
|
||||
- NAME=George
|
||||
|
||||
server-ringo:
|
||||
build: ./server
|
||||
links:
|
||||
- redis
|
||||
expose:
|
||||
- "3000"
|
||||
environment:
|
||||
- NAME=Ringo
|
||||
|
||||
client:
|
||||
build: ./client
|
||||
links:
|
||||
- nginx
|
||||
|
||||
redis:
|
||||
image: redis:alpine
|
||||
expose:
|
||||
- "6379"
|
||||
@@ -1,3 +0,0 @@
|
||||
|
||||
FROM nginx:alpine
|
||||
COPY nginx.conf /etc/nginx/nginx.conf
|
||||
@@ -1,39 +0,0 @@
|
||||
# Reference: https://www.nginx.com/resources/wiki/start/topics/examples/full/
|
||||
|
||||
worker_processes 4;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
server {
|
||||
listen 80;
|
||||
|
||||
location / {
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Host $host;
|
||||
|
||||
proxy_pass http://nodes;
|
||||
|
||||
# enable WebSockets
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
}
|
||||
|
||||
upstream nodes {
|
||||
# enable sticky session with either "hash" (uses the complete IP address)
|
||||
hash $remote_addr consistent;
|
||||
# or "ip_hash" (uses the first three octets of the client IPv4 address, or the entire IPv6 address)
|
||||
# ip_hash;
|
||||
# or "sticky" (needs commercial subscription)
|
||||
# sticky cookie srv_id expires=1h domain=.example.com path=/;
|
||||
|
||||
server server-john:3000;
|
||||
server server-paul:3000;
|
||||
server server-george:3000;
|
||||
server server-ringo:3000;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
FROM node:14-alpine
|
||||
|
||||
# Create app directory
|
||||
RUN mkdir -p /usr/src/app
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# Install app dependencies
|
||||
COPY package.json /usr/src/app/
|
||||
RUN npm install --prod
|
||||
|
||||
# Bundle app source
|
||||
COPY . /usr/src/app
|
||||
|
||||
EXPOSE 3000
|
||||
CMD [ "npm", "start" ]
|
||||
@@ -1,82 +0,0 @@
|
||||
// Setup basic express server
|
||||
var express = require('express');
|
||||
var app = express();
|
||||
var server = require('http').createServer(app);
|
||||
var io = require('socket.io')(server);
|
||||
var redis = require('socket.io-redis');
|
||||
var port = process.env.PORT || 3000;
|
||||
var serverName = process.env.NAME || 'Unknown';
|
||||
|
||||
io.adapter(redis({ host: 'redis', port: 6379 }));
|
||||
|
||||
server.listen(port, function () {
|
||||
console.log('Server listening at port %d', port);
|
||||
console.log('Hello, I\'m %s, how can I help?', serverName);
|
||||
});
|
||||
|
||||
// Routing
|
||||
app.use(express.static(__dirname + '/public'));
|
||||
|
||||
// Chatroom
|
||||
|
||||
var numUsers = 0;
|
||||
|
||||
io.on('connection', function (socket) {
|
||||
socket.emit('my-name-is', serverName);
|
||||
|
||||
var addedUser = false;
|
||||
|
||||
// when the client emits 'new message', this listens and executes
|
||||
socket.on('new message', function (data) {
|
||||
// we tell the client to execute 'new message'
|
||||
socket.broadcast.emit('new message', {
|
||||
username: socket.username,
|
||||
message: data
|
||||
});
|
||||
});
|
||||
|
||||
// when the client emits 'add user', this listens and executes
|
||||
socket.on('add user', function (username) {
|
||||
if (addedUser) return;
|
||||
|
||||
// we store the username in the socket session for this client
|
||||
socket.username = username;
|
||||
++numUsers;
|
||||
addedUser = true;
|
||||
socket.emit('login', {
|
||||
numUsers: numUsers
|
||||
});
|
||||
// echo globally (all clients) that a person has connected
|
||||
socket.broadcast.emit('user joined', {
|
||||
username: socket.username,
|
||||
numUsers: numUsers
|
||||
});
|
||||
});
|
||||
|
||||
// when the client emits 'typing', we broadcast it to others
|
||||
socket.on('typing', function () {
|
||||
socket.broadcast.emit('typing', {
|
||||
username: socket.username
|
||||
});
|
||||
});
|
||||
|
||||
// when the client emits 'stop typing', we broadcast it to others
|
||||
socket.on('stop typing', function () {
|
||||
socket.broadcast.emit('stop typing', {
|
||||
username: socket.username
|
||||
});
|
||||
});
|
||||
|
||||
// when the user disconnects.. perform this
|
||||
socket.on('disconnect', function () {
|
||||
if (addedUser) {
|
||||
--numUsers;
|
||||
|
||||
// echo globally that this client has left
|
||||
socket.broadcast.emit('user left', {
|
||||
username: socket.username,
|
||||
numUsers: numUsers
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"name": "socket.io-chat",
|
||||
"version": "0.0.0",
|
||||
"description": "A simple chat client using socket.io",
|
||||
"main": "index.js",
|
||||
"author": "Grant Timmerman",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"express": "4.13.4",
|
||||
"socket.io": "^3.1.0",
|
||||
"socket.io-redis": "^6.0.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node index.js"
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Socket.IO Chat Example</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<ul class="pages">
|
||||
<li class="chat page">
|
||||
<div class="chatArea">
|
||||
<ul class="messages"></ul>
|
||||
</div>
|
||||
<input class="inputMessage" placeholder="Type here..."/>
|
||||
</li>
|
||||
<li class="login page">
|
||||
<div class="form">
|
||||
<h3 class="title">What's your nickname?</h3>
|
||||
<input class="usernameInput" type="text" maxlength="14" />
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<script src="https://code.jquery.com/jquery-1.10.2.min.js"></script>
|
||||
<script src="/socket.io/socket.io.js"></script>
|
||||
<script src="/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,286 +0,0 @@
|
||||
$(function() {
|
||||
var FADE_TIME = 150; // ms
|
||||
var TYPING_TIMER_LENGTH = 400; // ms
|
||||
var COLORS = [
|
||||
'#e21400', '#91580f', '#f8a700', '#f78b00',
|
||||
'#58dc00', '#287b00', '#a8f07a', '#4ae8c4',
|
||||
'#3b88eb', '#3824aa', '#a700ff', '#d300e7'
|
||||
];
|
||||
|
||||
// Initialize variables
|
||||
var $window = $(window);
|
||||
var $usernameInput = $('.usernameInput'); // Input for username
|
||||
var $messages = $('.messages'); // Messages area
|
||||
var $inputMessage = $('.inputMessage'); // Input message input box
|
||||
|
||||
var $loginPage = $('.login.page'); // The login page
|
||||
var $chatPage = $('.chat.page'); // The chatroom page
|
||||
|
||||
// Prompt for setting a username
|
||||
var username;
|
||||
var connected = false;
|
||||
var typing = false;
|
||||
var lastTypingTime;
|
||||
var $currentInput = $usernameInput.focus();
|
||||
|
||||
var socket = io();
|
||||
|
||||
function addParticipantsMessage (data) {
|
||||
var message = '';
|
||||
if (data.numUsers === 1) {
|
||||
message += "there's 1 participant";
|
||||
} else {
|
||||
message += "there are " + data.numUsers + " participants";
|
||||
}
|
||||
log(message);
|
||||
}
|
||||
|
||||
// Sets the client's username
|
||||
function setUsername () {
|
||||
username = cleanInput($usernameInput.val().trim());
|
||||
|
||||
// If the username is valid
|
||||
if (username) {
|
||||
$loginPage.fadeOut();
|
||||
$chatPage.show();
|
||||
$loginPage.off('click');
|
||||
$currentInput = $inputMessage.focus();
|
||||
|
||||
// Tell the server your username
|
||||
socket.emit('add user', username);
|
||||
}
|
||||
}
|
||||
|
||||
// Sends a chat message
|
||||
function sendMessage () {
|
||||
var message = $inputMessage.val();
|
||||
// Prevent markup from being injected into the message
|
||||
message = cleanInput(message);
|
||||
// if there is a non-empty message and a socket connection
|
||||
if (message && connected) {
|
||||
$inputMessage.val('');
|
||||
addChatMessage({
|
||||
username: username,
|
||||
message: message
|
||||
});
|
||||
// tell server to execute 'new message' and send along one parameter
|
||||
socket.emit('new message', message);
|
||||
}
|
||||
}
|
||||
|
||||
// Log a message
|
||||
function log (message, options) {
|
||||
var $el = $('<li>').addClass('log').text(message);
|
||||
addMessageElement($el, options);
|
||||
}
|
||||
|
||||
// Adds the visual chat message to the message list
|
||||
function addChatMessage (data, options) {
|
||||
// Don't fade the message in if there is an 'X was typing'
|
||||
var $typingMessages = getTypingMessages(data);
|
||||
options = options || {};
|
||||
if ($typingMessages.length !== 0) {
|
||||
options.fade = false;
|
||||
$typingMessages.remove();
|
||||
}
|
||||
|
||||
var $usernameDiv = $('<span class="username"/>')
|
||||
.text(data.username)
|
||||
.css('color', getUsernameColor(data.username));
|
||||
var $messageBodyDiv = $('<span class="messageBody">')
|
||||
.text(data.message);
|
||||
|
||||
var typingClass = data.typing ? 'typing' : '';
|
||||
var $messageDiv = $('<li class="message"/>')
|
||||
.data('username', data.username)
|
||||
.addClass(typingClass)
|
||||
.append($usernameDiv, $messageBodyDiv);
|
||||
|
||||
addMessageElement($messageDiv, options);
|
||||
}
|
||||
|
||||
// Adds the visual chat typing message
|
||||
function addChatTyping (data) {
|
||||
data.typing = true;
|
||||
data.message = 'is typing';
|
||||
addChatMessage(data);
|
||||
}
|
||||
|
||||
// Removes the visual chat typing message
|
||||
function removeChatTyping (data) {
|
||||
getTypingMessages(data).fadeOut(function () {
|
||||
$(this).remove();
|
||||
});
|
||||
}
|
||||
|
||||
// Adds a message element to the messages and scrolls to the bottom
|
||||
// el - The element to add as a message
|
||||
// options.fade - If the element should fade-in (default = true)
|
||||
// options.prepend - If the element should prepend
|
||||
// all other messages (default = false)
|
||||
function addMessageElement (el, options) {
|
||||
var $el = $(el);
|
||||
|
||||
// Setup default options
|
||||
if (!options) {
|
||||
options = {};
|
||||
}
|
||||
if (typeof options.fade === 'undefined') {
|
||||
options.fade = true;
|
||||
}
|
||||
if (typeof options.prepend === 'undefined') {
|
||||
options.prepend = false;
|
||||
}
|
||||
|
||||
// Apply options
|
||||
if (options.fade) {
|
||||
$el.hide().fadeIn(FADE_TIME);
|
||||
}
|
||||
if (options.prepend) {
|
||||
$messages.prepend($el);
|
||||
} else {
|
||||
$messages.append($el);
|
||||
}
|
||||
$messages[0].scrollTop = $messages[0].scrollHeight;
|
||||
}
|
||||
|
||||
// Prevents input from having injected markup
|
||||
function cleanInput (input) {
|
||||
return $('<div/>').text(input).text();
|
||||
}
|
||||
|
||||
// Updates the typing event
|
||||
function updateTyping () {
|
||||
if (connected) {
|
||||
if (!typing) {
|
||||
typing = true;
|
||||
socket.emit('typing');
|
||||
}
|
||||
lastTypingTime = (new Date()).getTime();
|
||||
|
||||
setTimeout(function () {
|
||||
var typingTimer = (new Date()).getTime();
|
||||
var timeDiff = typingTimer - lastTypingTime;
|
||||
if (timeDiff >= TYPING_TIMER_LENGTH && typing) {
|
||||
socket.emit('stop typing');
|
||||
typing = false;
|
||||
}
|
||||
}, TYPING_TIMER_LENGTH);
|
||||
}
|
||||
}
|
||||
|
||||
// Gets the 'X is typing' messages of a user
|
||||
function getTypingMessages (data) {
|
||||
return $('.typing.message').filter(function (i) {
|
||||
return $(this).data('username') === data.username;
|
||||
});
|
||||
}
|
||||
|
||||
// Gets the color of a username through our hash function
|
||||
function getUsernameColor (username) {
|
||||
// Compute hash code
|
||||
var hash = 7;
|
||||
for (var i = 0; i < username.length; i++) {
|
||||
hash = username.charCodeAt(i) + (hash << 5) - hash;
|
||||
}
|
||||
// Calculate color
|
||||
var index = Math.abs(hash % COLORS.length);
|
||||
return COLORS[index];
|
||||
}
|
||||
|
||||
// Keyboard events
|
||||
|
||||
$window.keydown(function (event) {
|
||||
// Auto-focus the current input when a key is typed
|
||||
if (!(event.ctrlKey || event.metaKey || event.altKey)) {
|
||||
$currentInput.focus();
|
||||
}
|
||||
// When the client hits ENTER on their keyboard
|
||||
if (event.which === 13) {
|
||||
if (username) {
|
||||
sendMessage();
|
||||
socket.emit('stop typing');
|
||||
typing = false;
|
||||
} else {
|
||||
setUsername();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$inputMessage.on('input', function() {
|
||||
updateTyping();
|
||||
});
|
||||
|
||||
// Click events
|
||||
|
||||
// Focus input when clicking anywhere on login page
|
||||
$loginPage.click(function () {
|
||||
$currentInput.focus();
|
||||
});
|
||||
|
||||
// Focus input when clicking on the message input's border
|
||||
$inputMessage.click(function () {
|
||||
$inputMessage.focus();
|
||||
});
|
||||
|
||||
// Socket events
|
||||
|
||||
// Whenever the server emits 'login', log the login message
|
||||
socket.on('login', function (data) {
|
||||
connected = true;
|
||||
// Display the welcome message
|
||||
var message = "Welcome to Socket.IO Chat – ";
|
||||
log(message, {
|
||||
prepend: true
|
||||
});
|
||||
addParticipantsMessage(data);
|
||||
});
|
||||
|
||||
// Whenever the server emits 'new message', update the chat body
|
||||
socket.on('new message', function (data) {
|
||||
addChatMessage(data);
|
||||
});
|
||||
|
||||
// Whenever the server emits 'user joined', log it in the chat body
|
||||
socket.on('user joined', function (data) {
|
||||
log(data.username + ' joined');
|
||||
addParticipantsMessage(data);
|
||||
});
|
||||
|
||||
// Whenever the server emits 'user left', log it in the chat body
|
||||
socket.on('user left', function (data) {
|
||||
log(data.username + ' left');
|
||||
addParticipantsMessage(data);
|
||||
removeChatTyping(data);
|
||||
});
|
||||
|
||||
// Whenever the server emits 'typing', show the typing message
|
||||
socket.on('typing', function (data) {
|
||||
addChatTyping(data);
|
||||
});
|
||||
|
||||
// Whenever the server emits 'stop typing', kill the typing message
|
||||
socket.on('stop typing', function (data) {
|
||||
removeChatTyping(data);
|
||||
});
|
||||
|
||||
socket.on('disconnect', function () {
|
||||
log('you have been disconnected');
|
||||
});
|
||||
|
||||
socket.on('reconnect', function () {
|
||||
log('you have been reconnected');
|
||||
if (username) {
|
||||
socket.emit('add user', username);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('reconnect_error', function () {
|
||||
log('attempt to reconnect has failed');
|
||||
});
|
||||
|
||||
socket.on('my-name-is', function (serverName) {
|
||||
log('host is now ' + serverName);
|
||||
})
|
||||
|
||||
});
|
||||
@@ -1,149 +0,0 @@
|
||||
/* Fix user-agent */
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
font-weight: 300;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
html, input {
|
||||
font-family:
|
||||
"HelveticaNeue-Light",
|
||||
"Helvetica Neue Light",
|
||||
"Helvetica Neue",
|
||||
Helvetica,
|
||||
Arial,
|
||||
"Lucida Grande",
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
/* Pages */
|
||||
|
||||
.pages {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.page {
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Login Page */
|
||||
|
||||
.login.page {
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
.login.page .form {
|
||||
height: 100px;
|
||||
margin-top: -100px;
|
||||
position: absolute;
|
||||
|
||||
text-align: center;
|
||||
top: 50%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.login.page .form .usernameInput {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
border-bottom: 2px solid #fff;
|
||||
outline: none;
|
||||
padding-bottom: 15px;
|
||||
text-align: center;
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
.login.page .title {
|
||||
font-size: 200%;
|
||||
}
|
||||
|
||||
.login.page .usernameInput {
|
||||
font-size: 200%;
|
||||
letter-spacing: 3px;
|
||||
}
|
||||
|
||||
.login.page .title, .login.page .usernameInput {
|
||||
color: #fff;
|
||||
font-weight: 100;
|
||||
}
|
||||
|
||||
/* Chat page */
|
||||
|
||||
.chat.page {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Font */
|
||||
|
||||
.messages {
|
||||
font-size: 150%;
|
||||
}
|
||||
|
||||
.inputMessage {
|
||||
font-size: 100%;
|
||||
}
|
||||
|
||||
.log {
|
||||
color: gray;
|
||||
font-size: 70%;
|
||||
margin: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Messages */
|
||||
|
||||
.chatArea {
|
||||
height: 100%;
|
||||
padding-bottom: 60px;
|
||||
}
|
||||
|
||||
.messages {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
overflow-y: scroll;
|
||||
padding: 10px 20px 10px 20px;
|
||||
}
|
||||
|
||||
.message.typing .messageBody {
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.username {
|
||||
font-weight: 700;
|
||||
overflow: hidden;
|
||||
padding-right: 15px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* Input */
|
||||
|
||||
.inputMessage {
|
||||
border: 10px solid #000;
|
||||
bottom: 0;
|
||||
height: 60px;
|
||||
left: 0;
|
||||
outline: none;
|
||||
padding-left: 10px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
|
||||
# Socket.IO Chat with traefik & [redis](https://redis.io/)
|
||||
|
||||
A simple chat demo for Socket.IO
|
||||
|
||||
## How to use
|
||||
|
||||
Install [Docker Compose](https://docs.docker.com/compose/install/), then:
|
||||
|
||||
```
|
||||
$ docker-compose up -d
|
||||
```
|
||||
|
||||
And then point your browser to `http://localhost:3000`.
|
||||
|
||||
You can then scale the server to multiple instances:
|
||||
|
||||
```
|
||||
$ docker-compose up -d --scale=server=7
|
||||
```
|
||||
|
||||
The session stickiness, which is [required](https://socket.io/docs/v3/using-multiple-nodes/) when using multiple Socket.IO server instances, is achieved with a cookie. More information [here](https://doc.traefik.io/traefik/v2.0/routing/services/#sticky-sessions).
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user