mirror of
https://github.com/socketio/socket.io.git
synced 2026-01-11 16:08:24 -05:00
Compare commits
106 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ab2509d52 | ||
|
|
d9fb2f64b6 | ||
|
|
2c0a81cd87 | ||
|
|
f8d2644921 | ||
|
|
04640d68cf | ||
|
|
cb6d2e02aa | ||
|
|
80b2c34478 | ||
|
|
0d893196f8 | ||
|
|
df8e70f798 | ||
|
|
8c9ebc30e5 | ||
|
|
efb5c21e85 | ||
|
|
3848280125 | ||
|
|
9a2a83fdd4 | ||
|
|
f6ef267b03 | ||
|
|
1cdf36bfea | ||
|
|
bbf1fdc7a6 | ||
|
|
b4dc83eb9b | ||
|
|
ccbb4c0773 | ||
|
|
d744fda772 | ||
|
|
8259cdac84 | ||
|
|
fd9dd74eee | ||
|
|
c332643ad8 | ||
|
|
3468a197af | ||
|
|
09d45491c4 | ||
|
|
0731c0d2f4 | ||
|
|
03046a64ad | ||
|
|
443e447087 | ||
|
|
2f6cc2fa42 | ||
|
|
00d8ee5b05 | ||
|
|
2dd5fa9dd4 | ||
|
|
a5dff0ac83 | ||
|
|
3035c25982 | ||
|
|
63f181cc12 | ||
|
|
a250e283da | ||
|
|
e5c62cad60 | ||
|
|
01d37624a8 | ||
|
|
faf914c9ab | ||
|
|
15af22fc22 | ||
|
|
d3658944e5 | ||
|
|
12b0de4f52 | ||
|
|
3d44aae381 | ||
|
|
cbf0362476 | ||
|
|
59280da20b | ||
|
|
50a4d37cb8 | ||
|
|
6458b2bef1 | ||
|
|
b56da8a99f | ||
|
|
7952312911 | ||
|
|
0d0a7a22b5 | ||
|
|
2a8565fd1e | ||
|
|
d0b22c6302 | ||
|
|
e71f3d7dbe | ||
|
|
a2e5d1f77f | ||
|
|
d8143cc067 | ||
|
|
b2dd7cf660 | ||
|
|
3734b74b45 | ||
|
|
8aa94991ce | ||
|
|
4e64123862 | ||
|
|
115a9819fd | ||
|
|
0c0eb00163 | ||
|
|
f8640d9451 | ||
|
|
93d446a545 | ||
|
|
184f3cf7af | ||
|
|
5d9220b69a | ||
|
|
129883958a | ||
|
|
6c27b8b0a6 | ||
|
|
f3ada7d8cc | ||
|
|
a21ad88828 | ||
|
|
54d5ee05a6 | ||
|
|
da2b542797 | ||
|
|
b7d54dbe8d | ||
|
|
d4a9b2cdcb | ||
|
|
547c541fb9 | ||
|
|
3b7ced7af7 | ||
|
|
c00bb9564c | ||
|
|
57e5f25e26 | ||
|
|
f4b698418a | ||
|
|
945c84be47 | ||
|
|
d3d0a2d5be | ||
|
|
19b225b0c8 | ||
|
|
8fae95dd18 | ||
|
|
e6f6b906db | ||
|
|
596eb88af7 | ||
|
|
e357daf585 | ||
|
|
10fa4a2690 | ||
|
|
8be95b3bd3 | ||
|
|
ba497ee3eb | ||
|
|
28038715cb | ||
|
|
134226e96c | ||
|
|
9890b036cf | ||
|
|
713a6b451b | ||
|
|
18f3fdab12 | ||
|
|
5ab8289c0a | ||
|
|
30430f0985 | ||
|
|
9b43c9167c | ||
|
|
8ecfcba5c1 | ||
|
|
572133a58d | ||
|
|
6e1bb62982 | ||
|
|
06e6838b18 | ||
|
|
1f03a44d1f | ||
|
|
be3d7f0f1f | ||
|
|
d12aab2d69 | ||
|
|
9f758689f6 | ||
|
|
0b35dc77c0 | ||
|
|
531104d332 | ||
|
|
8b204570a9 | ||
|
|
0b7d70ca42 |
28
.github/workflows/ci.yml
vendored
28
.github/workflows/ci.yml
vendored
@@ -6,22 +6,34 @@ on:
|
||||
schedule:
|
||||
- cron: '0 0 * * 0'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test-node:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [12, 14, 16]
|
||||
node-version:
|
||||
- 16
|
||||
- 20
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: npm ci
|
||||
- run: npm test
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run tests
|
||||
run: npm test
|
||||
env:
|
||||
CI: true
|
||||
|
||||
@@ -37,16 +49,18 @@ jobs:
|
||||
- typescript
|
||||
- webpack-build
|
||||
- webpack-build-server
|
||||
- basic-crud-application/angular-client
|
||||
- basic-crud-application/vue-client
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
|
||||
- name: Use Node.js 20
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
|
||||
- name: Build ${{ matrix.example }}
|
||||
run: |
|
||||
cd examples/${{ matrix.example }}
|
||||
|
||||
662
CHANGELOG.md
662
CHANGELOG.md
@@ -1,3 +1,565 @@
|
||||
# History
|
||||
|
||||
## 2024
|
||||
|
||||
- [4.7.4](#474-2024-01-12) (Jan 2024)
|
||||
- [4.7.3](#473-2024-01-03) (Jan 2024)
|
||||
|
||||
## 2023
|
||||
|
||||
- [4.7.2](#472-2023-08-02) (Aug 2023)
|
||||
- [4.7.1](#471-2023-06-28) (Jun 2023)
|
||||
- [4.7.0](#470-2023-06-22) (Jun 2023)
|
||||
- [4.6.2](#462-2023-05-31) (May 2023)
|
||||
- [4.6.1](#461-2023-02-20) (Feb 2023)
|
||||
- [4.6.0](#460-2023-02-07) (Feb 2023)
|
||||
|
||||
## 2022
|
||||
|
||||
- [4.5.4](#454-2022-11-22) (Nov 2022)
|
||||
- [4.5.3](#453-2022-10-15) (Oct 2022)
|
||||
- [4.5.2](#452-2022-09-02) (Sep 2022)
|
||||
- [2.5.0](#250-2022-06-26) (Jun 2022) (from the [2.x](https://github.com/socketio/socket.io/tree/2.x) branch)
|
||||
- [4.5.1](#451-2022-05-17) (May 2022)
|
||||
- [4.5.0](#450-2022-04-23) (Apr 2022)
|
||||
- [4.4.1](#441-2022-01-06) (Jan 2022)
|
||||
|
||||
## 2021
|
||||
|
||||
- [4.4.0](#440-2021-11-18) (Nov 2021)
|
||||
- [4.3.2](#432-2021-11-08) (Nov 2021)
|
||||
- [4.3.1](#431-2021-10-16) (Oct 2021)
|
||||
- [4.3.0](#430-2021-10-14) (Oct 2021)
|
||||
- [4.2.0](#420-2021-08-30) (Aug 2021)
|
||||
- [4.1.3](#413-2021-07-10) (Jul 2021)
|
||||
- [4.1.2](#412-2021-05-17) (May 2021)
|
||||
- [4.1.1](#411-2021-05-11) (May 2021)
|
||||
- [4.1.0](#410-2021-05-11) (May 2021)
|
||||
- [4.0.2](#402-2021-05-06) (May 2021)
|
||||
- [4.0.1](#401-2021-03-31) (Mar 2021)
|
||||
- [**4.0.0**](#400-2021-03-10) (Mar 2021)
|
||||
- [3.1.2](#312-2021-02-26) (Feb 2021)
|
||||
- [3.1.1](#311-2021-02-03) (Feb 2021)
|
||||
- [3.1.0](#310-2021-01-15) (Jan 2021)
|
||||
- [2.4.1](#241-2021-01-07) (Jan 2021) (from the [2.x](https://github.com/socketio/socket.io/tree/2.x) branch)
|
||||
- [3.0.5](#305-2021-01-05) (Jan 2021)
|
||||
- [2.4.0](#240-2021-01-04) (Jan 2021) (from the [2.x](https://github.com/socketio/socket.io/tree/2.x) branch)
|
||||
|
||||
## 2020
|
||||
|
||||
- [3.0.4](#304-2020-12-07) (Dec 2020)
|
||||
- [3.0.3](#303-2020-11-19) (Nov 2020)
|
||||
- [3.0.2](#302-2020-11-17) (Nov 2020)
|
||||
- [3.0.1](#301-2020-11-09) (Nov 2020)
|
||||
- [**3.0.0**](#300-2020-11-05) (Nov 2020)
|
||||
|
||||
## 2019
|
||||
|
||||
- [2.3.0](#230-2019-09-20) (Sep 2019)
|
||||
|
||||
## 2018
|
||||
|
||||
- [2.2.0](#220-2018-11-29) (Nov 2018)
|
||||
- [2.1.1](#211-2018-05-17) (May 2018)
|
||||
- [2.1.0](#210-2018-03-29) (Mar 2018)
|
||||
|
||||
|
||||
# Release notes
|
||||
|
||||
## [4.7.4](https://github.com/socketio/socket.io/compare/4.7.3...4.7.4) (2024-01-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **typings:** calling io.emit with no arguments incorrectly errored ([cb6d2e0](https://github.com/socketio/socket.io/commit/cb6d2e02aa7ec03c2de1817d35cffa1128b107ef)), closes [#4914](https://github.com/socketio/socket.io/issues/4914)
|
||||
|
||||
|
||||
### Dependencies
|
||||
|
||||
- [`engine.io@~6.5.2`](https://github.com/socketio/engine.io/releases/tag/6.5.2) (no change)
|
||||
- [`ws@~8.11.0`](https://github.com/websockets/ws/releases/tag/8.11.0) (no change)
|
||||
|
||||
|
||||
|
||||
## [4.7.3](https://github.com/socketio/socket.io/compare/4.7.2...4.7.3) (2024-01-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* return the first response when broadcasting to a single socket ([#4878](https://github.com/socketio/socket.io/issues/4878)) ([df8e70f](https://github.com/socketio/socket.io/commit/df8e70f79822e3887b4f21ca718af8a53bbda2c4))
|
||||
* **typings:** allow to bind to a non-secure Http2Server ([#4853](https://github.com/socketio/socket.io/issues/4853)) ([8c9ebc3](https://github.com/socketio/socket.io/commit/8c9ebc30e5452ff9381af5d79f547394fa55633c))
|
||||
|
||||
|
||||
### Dependencies
|
||||
|
||||
- [`engine.io@~6.5.2`](https://github.com/socketio/engine.io/releases/tag/6.5.2) (no change)
|
||||
- [`ws@~8.11.0`](https://github.com/websockets/ws/releases/tag/8.11.0) (no change)
|
||||
|
||||
|
||||
|
||||
## [4.7.2](https://github.com/socketio/socket.io/compare/4.7.1...4.7.2) (2023-08-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* clean up child namespace when client is rejected in middleware ([#4773](https://github.com/socketio/socket.io/issues/4773)) ([0731c0d](https://github.com/socketio/socket.io/commit/0731c0d2f497d5cce596ea1ec32a67c08bcccbcd))
|
||||
* **webtransport:** properly handle WebTransport-only connections ([3468a19](https://github.com/socketio/socket.io/commit/3468a197afe87e65eb0d779fabd347fe683013ab))
|
||||
* **webtransport:** add proper framing ([a306db0](https://github.com/socketio/engine.io/commit/a306db09e8ddb367c7d62f45fec920f979580b7c))
|
||||
|
||||
|
||||
### Dependencies
|
||||
|
||||
- [`engine.io@~6.5.2`](https://github.com/socketio/engine.io/releases/tag/6.5.2) ([diff](https://github.com/socketio/engine.io/compare/6.5.0...6.5.2))
|
||||
- [`ws@~8.11.0`](https://github.com/websockets/ws/releases/tag/8.11.0) (no change)
|
||||
|
||||
|
||||
|
||||
## [4.7.1](https://github.com/socketio/socket.io/compare/4.7.0...4.7.1) (2023-06-28)
|
||||
|
||||
The client bundle contains a few fixes regarding the WebTransport support.
|
||||
|
||||
### Dependencies
|
||||
|
||||
- [`engine.io@~6.5.0`](https://github.com/socketio/engine.io/releases/tag/6.5.0) (no change)
|
||||
- [`ws@~8.11.0`](https://github.com/websockets/ws/releases/tag/8.11.0) (no change)
|
||||
|
||||
|
||||
|
||||
## [4.7.0](https://github.com/socketio/socket.io/compare/4.6.2...4.7.0) (2023-06-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* remove the Partial modifier from the socket.data type ([#4740](https://github.com/socketio/socket.io/issues/4740)) ([e5c62ca](https://github.com/socketio/socket.io/commit/e5c62cad60fc7d16fbb024fd9be1d1880f4e6f5f))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
#### Support for WebTransport
|
||||
|
||||
The Socket.IO server can now use WebTransport as the underlying transport.
|
||||
|
||||
WebTransport is a web API that uses the HTTP/3 protocol as a bidirectional transport. It's intended for two-way communications between a web client and an HTTP/3 server.
|
||||
|
||||
References:
|
||||
|
||||
- https://w3c.github.io/webtransport/
|
||||
- https://developer.mozilla.org/en-US/docs/Web/API/WebTransport
|
||||
- https://developer.chrome.com/articles/webtransport/
|
||||
|
||||
Until WebTransport support lands [in Node.js](https://github.com/nodejs/node/issues/38478), you can use the `@fails-components/webtransport` package:
|
||||
|
||||
```js
|
||||
import { readFileSync } from "fs";
|
||||
import { createServer } from "https";
|
||||
import { Server } from "socket.io";
|
||||
import { Http3Server } from "@fails-components/webtransport";
|
||||
|
||||
// WARNING: the total length of the validity period MUST NOT exceed two weeks (https://w3c.github.io/webtransport/#custom-certificate-requirements)
|
||||
const cert = readFileSync("/path/to/my/cert.pem");
|
||||
const key = readFileSync("/path/to/my/key.pem");
|
||||
|
||||
const httpsServer = createServer({
|
||||
key,
|
||||
cert
|
||||
});
|
||||
|
||||
httpsServer.listen(3000);
|
||||
|
||||
const io = new Server(httpsServer, {
|
||||
transports: ["polling", "websocket", "webtransport"] // WebTransport is not enabled by default
|
||||
});
|
||||
|
||||
const h3Server = new Http3Server({
|
||||
port: 3000,
|
||||
host: "0.0.0.0",
|
||||
secret: "changeit",
|
||||
cert,
|
||||
privKey: key,
|
||||
});
|
||||
|
||||
(async () => {
|
||||
const stream = await h3Server.sessionStream("/socket.io/");
|
||||
const sessionReader = stream.getReader();
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await sessionReader.read();
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
io.engine.onWebTransportSession(value);
|
||||
}
|
||||
})();
|
||||
|
||||
h3Server.startServer();
|
||||
```
|
||||
|
||||
Added in [123b68c](https://github.com/socketio/engine.io/commit/123b68c04f9e971f59b526e0f967a488ee6b0116).
|
||||
|
||||
|
||||
#### Client bundles with CORS headers
|
||||
|
||||
The bundles will now have the right `Access-Control-Allow-xxx` headers.
|
||||
|
||||
Added in [63f181c](https://github.com/socketio/socket.io/commit/63f181cc12cbbbf94ed40eef52d60f36a1214fbe).
|
||||
|
||||
|
||||
### Dependencies
|
||||
|
||||
- [`engine.io@~6.5.0`](https://github.com/socketio/engine.io/releases/tag/6.5.0) ([diff](https://github.com/socketio/engine.io/compare/6.4.2...6.5.0))
|
||||
- [`ws@~8.11.0`](https://github.com/websockets/ws/releases/tag/8.11.0) (no change)
|
||||
|
||||
|
||||
|
||||
## [4.6.2](https://github.com/socketio/socket.io/compare/4.6.1...4.6.2) (2023-05-31)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **exports:** move `types` condition to the top ([#4698](https://github.com/socketio/socket.io/issues/4698)) ([3d44aae](https://github.com/socketio/socket.io/commit/3d44aae381af38349fdb808d510d9f47a0c2507e))
|
||||
|
||||
|
||||
### Dependencies
|
||||
|
||||
- [`engine.io@~6.4.2`](https://github.com/socketio/engine.io/releases/tag/6.4.0) ([diff](https://github.com/socketio/engine.io/compare/6.4.1...6.4.2))
|
||||
- [`ws@~8.11.0`](https://github.com/websockets/ws/releases/tag/8.11.0) (no change)
|
||||
|
||||
|
||||
|
||||
## [4.6.1](https://github.com/socketio/socket.io/compare/4.6.0...4.6.1) (2023-02-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* properly handle manually created dynamic namespaces ([0d0a7a2](https://github.com/socketio/socket.io/commit/0d0a7a22b5ff95f864216c529114b7dd41738d1e))
|
||||
* **types:** fix nodenext module resolution compatibility ([#4625](https://github.com/socketio/socket.io/issues/4625)) ([d0b22c6](https://github.com/socketio/socket.io/commit/d0b22c630208669aceb7ae013180c99ef90279b0))
|
||||
|
||||
|
||||
### Dependencies
|
||||
|
||||
- [`engine.io@~6.4.1`](https://github.com/socketio/engine.io/releases/tag/6.4.1) ([diff](https://github.com/socketio/engine.io/compare/6.4.0...6.4.1))
|
||||
- [`ws@~8.11.0`](https://github.com/websockets/ws/releases/tag/8.11.0) (no change)
|
||||
|
||||
|
||||
|
||||
## [4.6.0](https://github.com/socketio/socket.io/compare/4.5.4...4.6.0) (2023-02-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add timeout method to remote socket ([#4558](https://github.com/socketio/socket.io/issues/4558)) ([0c0eb00](https://github.com/socketio/socket.io/commit/0c0eb0016317218c2be3641e706cfaa9bea39a2d))
|
||||
* **typings:** properly type emits with timeout ([f3ada7d](https://github.com/socketio/socket.io/commit/f3ada7d8ccc02eeced2b9b9ac8e4bc921eb630d2))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
#### Promise-based acknowledgements
|
||||
|
||||
This commit adds some syntactic sugar around acknowledgements:
|
||||
|
||||
- `emitWithAck()`
|
||||
|
||||
```js
|
||||
try {
|
||||
const responses = await io.timeout(1000).emitWithAck("some-event");
|
||||
console.log(responses); // one response per client
|
||||
} catch (e) {
|
||||
// some clients did not acknowledge the event in the given delay
|
||||
}
|
||||
|
||||
io.on("connection", async (socket) => {
|
||||
// without timeout
|
||||
const response = await socket.emitWithAck("hello", "world");
|
||||
|
||||
// with a specific timeout
|
||||
try {
|
||||
const response = await socket.timeout(1000).emitWithAck("hello", "world");
|
||||
} catch (err) {
|
||||
// the client did not acknowledge the event in the given delay
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
- `serverSideEmitWithAck()`
|
||||
|
||||
```js
|
||||
try {
|
||||
const responses = await io.timeout(1000).serverSideEmitWithAck("some-event");
|
||||
console.log(responses); // one response per server (except itself)
|
||||
} catch (e) {
|
||||
// some servers did not acknowledge the event in the given delay
|
||||
}
|
||||
```
|
||||
|
||||
Added in [184f3cf](https://github.com/socketio/socket.io/commit/184f3cf7af57acc4b0948eee307f25f8536eb6c8).
|
||||
|
||||
#### Connection state recovery
|
||||
|
||||
This feature allows a client to reconnect after a temporary disconnection and restore its state:
|
||||
|
||||
- id
|
||||
- rooms
|
||||
- data
|
||||
- missed packets
|
||||
|
||||
Usage:
|
||||
|
||||
```js
|
||||
import { Server } from "socket.io";
|
||||
|
||||
const io = new Server({
|
||||
connectionStateRecovery: {
|
||||
// default values
|
||||
maxDisconnectionDuration: 2 * 60 * 1000,
|
||||
skipMiddlewares: true,
|
||||
},
|
||||
});
|
||||
|
||||
io.on("connection", (socket) => {
|
||||
console.log(socket.recovered); // whether the state was recovered or not
|
||||
});
|
||||
```
|
||||
|
||||
Here's how it works:
|
||||
|
||||
- the server sends a session ID during the handshake (which is different from the current `id` attribute, which is public and can be freely shared)
|
||||
- the server also includes an offset in each packet (added at the end of the data array, for backward compatibility)
|
||||
- upon temporary disconnection, the server stores the client state for a given delay (implemented at the adapter level)
|
||||
- upon reconnection, the client sends both the session ID and the last offset it has processed, and the server tries to restore the state
|
||||
|
||||
The in-memory adapter already supports this feature, and we will soon update the Postgres and MongoDB adapters. We will also create a new adapter based on [Redis Streams](https://redis.io/docs/data-types/streams/), which will support this feature.
|
||||
|
||||
Added in [54d5ee0](https://github.com/socketio/socket.io/commit/54d5ee05a684371191e207b8089f09fc24eb5107).
|
||||
|
||||
#### Compatibility (for real) with Express middlewares
|
||||
|
||||
This feature implements middlewares at the Engine.IO level, because Socket.IO middlewares are meant for namespace authorization and are not executed during a classic HTTP request/response cycle.
|
||||
|
||||
Syntax:
|
||||
|
||||
```js
|
||||
io.engine.use((req, res, next) => {
|
||||
// do something
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
// with express-session
|
||||
import session from "express-session";
|
||||
|
||||
io.engine.use(session({
|
||||
secret: "keyboard cat",
|
||||
resave: false,
|
||||
saveUninitialized: true,
|
||||
cookie: { secure: true }
|
||||
}));
|
||||
|
||||
// with helmet
|
||||
import helmet from "helmet";
|
||||
|
||||
io.engine.use(helmet());
|
||||
```
|
||||
|
||||
A workaround was possible by using the allowRequest option and the "headers" event, but this feels way cleaner and works with upgrade requests too.
|
||||
|
||||
Added in [24786e7](https://github.com/socketio/engine.io/commit/24786e77c5403b1c4b5a2bc84e2af06f9187f74a).
|
||||
|
||||
#### Error details in the disconnecting and disconnect events
|
||||
|
||||
The `disconnect` event will now contain additional details about the disconnection reason.
|
||||
|
||||
```js
|
||||
io.on("connection", (socket) => {
|
||||
socket.on("disconnect", (reason, description) => {
|
||||
console.log(description);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Added in [8aa9499](https://github.com/socketio/socket.io/commit/8aa94991cee5518567d6254eec04b23f81510257).
|
||||
|
||||
#### Automatic removal of empty child namespaces
|
||||
|
||||
This commit adds a new option, "cleanupEmptyChildNamespaces". With this option enabled (disabled by default), when a socket disconnects from a dynamic namespace and if there are no other sockets connected to it then the namespace will be cleaned up and its adapter will be closed.
|
||||
|
||||
```js
|
||||
import { createServer } from "node:http";
|
||||
import { Server } from "socket.io";
|
||||
|
||||
const httpServer = createServer();
|
||||
const io = new Server(httpServer, {
|
||||
cleanupEmptyChildNamespaces: true
|
||||
});
|
||||
```
|
||||
|
||||
Added in [5d9220b](https://github.com/socketio/socket.io/commit/5d9220b69adf73e086c27bbb63a4976b348f7c4c).
|
||||
|
||||
#### A new "addTrailingSlash" option
|
||||
|
||||
The trailing slash which was added by default can now be disabled:
|
||||
|
||||
```js
|
||||
import { createServer } from "node:http";
|
||||
import { Server } from "socket.io";
|
||||
|
||||
const httpServer = createServer();
|
||||
const io = new Server(httpServer, {
|
||||
addTrailingSlash: false
|
||||
});
|
||||
```
|
||||
|
||||
In the example above, the clients can omit the trailing slash and use `/socket.io` instead of `/socket.io/`.
|
||||
|
||||
Added in [d0fd474](https://github.com/socketio/engine.io/commit/d0fd4746afa396297f07bb62e539b0c1c4018d7c).
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* precompute the WebSocket frames when broadcasting ([da2b542](https://github.com/socketio/socket.io/commit/da2b54279749adc5279c9ac4742b01b36c01cff0))
|
||||
|
||||
|
||||
### Dependencies
|
||||
|
||||
- [`engine.io@~6.4.0`](https://github.com/socketio/engine.io/releases/tag/6.4.0) (https://github.com/socketio/engine.io/compare/6.2.1...6.4.0)
|
||||
- [`ws@~8.11.0`](https://github.com/websockets/ws/releases/tag/8.11.0) (https://github.com/websockets/ws/compare/8.2.3...8.11.0)
|
||||
|
||||
|
||||
## [4.5.4](https://github.com/socketio/socket.io/compare/4.5.3...4.5.4) (2022-11-22)
|
||||
|
||||
This release contains a bump of:
|
||||
|
||||
- `engine.io` in order to fix [CVE-2022-41940](https://github.com/socketio/engine.io/security/advisories/GHSA-r7qp-cfhv-p84w)
|
||||
- `socket.io-parser` in order to fix [CVE-2022-2421](https://github.com/advisories/GHSA-qm95-pgcg-qqfq).
|
||||
|
||||
### Dependencies
|
||||
|
||||
- [`engine.io@~6.2.1`](https://github.com/socketio/engine.io/releases/tag/6.2.1) ([diff](https://github.com/socketio/engine.io/compare/6.2.0...6.2.1))
|
||||
- [`ws@~8.2.3`](https://github.com/websockets/ws/releases/tag/8.2.3) (no change)
|
||||
|
||||
|
||||
|
||||
## [4.5.3](https://github.com/socketio/socket.io/compare/4.5.2...4.5.3) (2022-10-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **typings:** accept an HTTP2 server in the constructor ([d3d0a2d](https://github.com/socketio/socket.io/commit/d3d0a2d5beaff51fd145f810bcaf6914213f8a06))
|
||||
* **typings:** apply types to "io.timeout(...).emit()" calls ([e357daf](https://github.com/socketio/socket.io/commit/e357daf5858560bc84e7e50cd36f0278d6721ea1))
|
||||
|
||||
### Dependencies
|
||||
|
||||
- [`engine.io@~6.2.0`](https://github.com/socketio/engine.io/releases/tag/6.2.1) (no change)
|
||||
- [`ws@~8.2.3`](https://github.com/websockets/ws/releases/tag/8.2.3) (no change)
|
||||
|
||||
|
||||
|
||||
## [4.5.2](https://github.com/socketio/socket.io/compare/4.5.1...4.5.2) (2022-09-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* prevent the socket from joining a room after disconnection ([18f3fda](https://github.com/socketio/socket.io/commit/18f3fdab12947a9fee3e9c37cfc1da97027d1473))
|
||||
* **uws:** prevent the server from crashing after upgrade ([ba497ee](https://github.com/socketio/socket.io/commit/ba497ee3eb52c4abf1464380d015d8c788714364))
|
||||
|
||||
### Dependencies
|
||||
|
||||
- [`engine.io@~6.2.0`](https://github.com/socketio/engine.io/releases/tag/6.2.0) (no change)
|
||||
- [`ws@~8.2.3`](https://github.com/websockets/ws/releases/tag/8.2.3) (no change)
|
||||
|
||||
|
||||
|
||||
# [2.5.0](https://github.com/socketio/socket.io/compare/2.4.1...2.5.0) (2022-06-26)
|
||||
|
||||
⚠️ WARNING ⚠️
|
||||
|
||||
The default value of the `maxHttpBufferSize` option has been decreased from 100 MB to 1 MB, in order to prevent attacks by denial of service.
|
||||
|
||||
Security advisory: [GHSA-j4f2-536g-r55m](https://github.com/advisories/GHSA-j4f2-536g-r55m)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix race condition in dynamic namespaces ([05e1278](https://github.com/socketio/socket.io/commit/05e1278cfa99f3ecf3f8f0531ffe57d850e9a05b))
|
||||
* ignore packet received after disconnection ([22d4bdf](https://github.com/socketio/socket.io/commit/22d4bdf00d1a03885dc0171125faddfaef730066))
|
||||
* only set 'connected' to true after middleware execution ([226cc16](https://github.com/socketio/socket.io/commit/226cc16165f9fe60f16ff4d295fb91c8971cde35))
|
||||
* prevent the socket from joining a room after disconnection ([f223178](https://github.com/socketio/socket.io/commit/f223178eb655a7713303b21a78f9ef9e161d6458))
|
||||
|
||||
### Dependencies
|
||||
|
||||
- [`engine.io@~3.6.0`](https://github.com/socketio/engine.io/releases/tag/3.6.0) (https://github.com/socketio/engine.io/compare/3.5.0...3.6.0)
|
||||
- [`ws@~7.4.2`](https://github.com/websockets/ws/releases/tag/7.4.2) (no change)
|
||||
|
||||
|
||||
|
||||
## [4.5.1](https://github.com/socketio/socket.io/compare/4.5.0...4.5.1) (2022-05-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* forward the local flag to the adapter when using fetchSockets() ([30430f0](https://github.com/socketio/socket.io/commit/30430f0985f8e7c49394543d4c84913b6a15df60))
|
||||
* **typings:** add HTTPS server to accepted types ([#4351](https://github.com/socketio/socket.io/issues/4351)) ([9b43c91](https://github.com/socketio/socket.io/commit/9b43c9167cff817c60fa29dbda2ef7cd938aff51))
|
||||
|
||||
### Dependencies
|
||||
|
||||
- [`engine.io@~6.2.0`](https://github.com/socketio/engine.io/releases/tag/6.2.0) (no change)
|
||||
- [`ws@~8.2.3`](https://github.com/websockets/ws/releases/tag/8.2.3) (no change)
|
||||
|
||||
|
||||
|
||||
# [4.5.0](https://github.com/socketio/socket.io/compare/4.4.1...4.5.0) (2022-04-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **typings:** ensure compatibility with TypeScript 3.x ([#4259](https://github.com/socketio/socket.io/issues/4259)) ([02c87a8](https://github.com/socketio/socket.io/commit/02c87a85614e217b8e7b93753f315790ae9d99f6))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
#### Catch-all listeners for outgoing packets
|
||||
|
||||
This is similar to `onAny()`, but for outgoing packets.
|
||||
|
||||
Syntax:
|
||||
|
||||
```js
|
||||
socket.onAnyOutgoing((event, ...args) => {
|
||||
console.log(event);
|
||||
});
|
||||
```
|
||||
|
||||
Added in [531104d](https://github.com/socketio/socket.io/commit/531104d332690138b7aab84d5583d6204132c8b4).
|
||||
|
||||
#### Broadcast and expect multiple acknowledgements
|
||||
|
||||
Syntax:
|
||||
|
||||
```js
|
||||
io.timeout(1000).emit("some-event", (err, responses) => {
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
Added in [8b20457](https://github.com/socketio/socket.io/commit/8b204570a94979bbec307f23ca078f30f5cf07b0).
|
||||
|
||||
#### `maxHttpBufferSize` value negotiation
|
||||
|
||||
A "maxPayload" field is now included in the Engine.IO handshake, so that clients in HTTP long-polling can decide how many packets they have to send to stay under the `maxHttpBufferSize` value.
|
||||
|
||||
This is a backward compatible change which should not mandate a new major revision of the protocol (we stay in v4), as we only add a field in the JSON-encoded handshake data:
|
||||
|
||||
```
|
||||
0{"sid":"lv_VI97HAXpY6yYWAAAC","upgrades":["websocket"],"pingInterval":25000,"pingTimeout":5000,"maxPayload":1000000}
|
||||
```
|
||||
|
||||
Added in [088dcb4](https://github.com/socketio/engine.io/commit/088dcb4dff60df39785df13d0a33d3ceaa1dff38).
|
||||
|
||||
### Dependencies
|
||||
|
||||
- [`engine.io@~6.2.0`](https://github.com/socketio/engine.io/releases/tag/6.2.0) (https://github.com/socketio/engine.io/compare/6.1.0...6.2.0)
|
||||
- [`ws@~8.2.3`](https://github.com/websockets/ws/releases/tag/8.2.3) (no change)
|
||||
|
||||
|
||||
|
||||
## [4.4.1](https://github.com/socketio/socket.io/compare/4.4.0...4.4.1) (2022-01-06)
|
||||
|
||||
|
||||
@@ -178,6 +740,16 @@
|
||||
* allow integers as event names ([1c220dd](https://github.com/socketio/socket.io-parser/commit/1c220ddbf45ea4b44bc8dbf6f9ae245f672ba1b9))
|
||||
|
||||
|
||||
|
||||
## [2.4.1](https://github.com/socketio/socket.io/compare/2.4.0...2.4.1) (2021-01-07)
|
||||
|
||||
|
||||
### Reverts
|
||||
|
||||
* fix(security): do not allow all origins by default ([a169050](https://github.com/socketio/socket.io/commit/a1690509470e9dd5559cec4e60908ca6c23e9ba0))
|
||||
|
||||
|
||||
|
||||
## [3.0.5](https://github.com/socketio/socket.io/compare/3.0.4...3.0.5) (2021-01-05)
|
||||
|
||||
|
||||
@@ -191,6 +763,17 @@
|
||||
* restore the socket middleware functionality ([bf54327](https://github.com/socketio/socket.io/commit/bf5432742158e4d5ba2722cff4a614967dffa5b9))
|
||||
|
||||
|
||||
|
||||
# [2.4.0](https://github.com/socketio/socket.io/compare/2.3.0...2.4.0) (2021-01-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **security:** do not allow all origins by default ([f78a575](https://github.com/socketio/socket.io/commit/f78a575f66ab693c3ea96ea88429ddb1a44c86c7))
|
||||
* properly overwrite the query sent in the handshake ([d33a619](https://github.com/socketio/socket.io/commit/d33a619905a4905c153d4fec337c74da5b533a9e))
|
||||
|
||||
|
||||
|
||||
## [3.0.4](https://github.com/socketio/socket.io/compare/3.0.3...3.0.4) (2020-12-07)
|
||||
|
||||
|
||||
@@ -308,7 +891,7 @@ new Server(3000, {
|
||||
const socket = io("/admin");
|
||||
|
||||
// server-side
|
||||
io.on("connect", socket => {
|
||||
io.on("connection", socket => {
|
||||
// not triggered anymore
|
||||
})
|
||||
|
||||
@@ -459,7 +1042,7 @@ new Server(3000, {
|
||||
const socket = io("/admin");
|
||||
|
||||
// server-side
|
||||
io.on("connect", socket => {
|
||||
io.on("connection", socket => {
|
||||
// not triggered anymore
|
||||
})
|
||||
|
||||
@@ -476,3 +1059,78 @@ io.of("/admin").use((socket, next) => {
|
||||
|
||||
This method was kept for backward-compatibility with pre-1.0 versions.
|
||||
|
||||
|
||||
|
||||
# [2.3.0](https://github.com/socketio/socket.io/compare/2.2.0...2.3.0) (2019-09-20)
|
||||
|
||||
This release mainly contains a bump of the `engine.io` and `ws` packages, but no additional features.
|
||||
|
||||
|
||||
|
||||
# [2.2.0](https://github.com/socketio/socket.io/compare/2.1.1...2.2.0) (2018-11-29)
|
||||
|
||||
### Features
|
||||
|
||||
- add cache-control header when serving the client source ([#2907](https://github.com/socketio/socket.io/pull/2907)) ([b00ae50](https://github.com/socketio/socket.io/commit/b00ae50be65d1bc88fa95145f1c486a6886a6b76))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- throw an error when trying to access the clients of a dynamic namespace ([#3355](https://github.com/socketio/socket.io/pull/3355)) ([a7fbd1a](https://github.com/socketio/socket.io/commit/a7fbd1ac4a47cafd832fc62e371754df924c5903))
|
||||
|
||||
|
||||
|
||||
# [2.1.1](https://github.com/socketio/socket.io/compare/2.1.0...2.1.1) (2018-05-17)
|
||||
|
||||
### Features
|
||||
|
||||
- add local flag to the socket object ([#3129](https://github.com/socketio/socket.io/pull/3219)) ([1decae3](https://github.com/socketio/socket.io/commit/1decae341c80c0417b32d3124ca30c005240b48a))
|
||||
|
||||
```js
|
||||
socket.local.to('room101').emit(/* */);
|
||||
```
|
||||
|
||||
|
||||
# [2.1.0](https://github.com/socketio/socket.io/compare/2.1.1...2.2.0) (2018-03-29)
|
||||
|
||||
### Features
|
||||
|
||||
- add a 'binary' flag ([#3185](https://github.com/socketio/socket.io/pull/3185)) ([f48a06c](https://github.com/socketio/socket.io/commit/f48a06c040280b44f90fd225c888910544fd63b5))
|
||||
|
||||
```js
|
||||
// by default, the object is recursively scanned to check whether it contains some binary data
|
||||
// in the following example, the check is skipped in order to improve performance
|
||||
socket.binary(false).emit('plain-object', object);
|
||||
|
||||
// it also works at the namespace level
|
||||
io.binary(false).emit('plain-object', object);
|
||||
```
|
||||
|
||||
- add support for dynamic namespaces ([#3195](https://github.com/socketio/socket.io/pull/3195)) ([c0c79f0](https://github.com/socketio/socket.io/commit/c0c79f019e7138194e438339f8192705957c8ec3))
|
||||
|
||||
```js
|
||||
io.of(/^\/dynamic-\d+$/).on('connect', (socket) => {
|
||||
// socket.nsp.name = '/dynamic-101'
|
||||
});
|
||||
|
||||
// client-side
|
||||
const client = require('socket.io-client')('/dynamic-101');
|
||||
```
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- properly emit 'connect' when using a custom namespace ([#3197](https://github.com/socketio/socket.io/pull/3197)) ([f4fc517](https://github.com/socketio/socket.io/commit/f4fc517e0fe25866c95b584291487b8cbdff889d))
|
||||
- include the protocol in the origins check ([#3198](https://github.com/socketio/socket.io/pull/3198)) ([1f1d64b](https://github.com/socketio/socket.io/commit/1f1d64bab61a273712a199591a3f76210d8c0959))
|
||||
|
||||
### Important note :warning: from Engine.IO [3.2.0 release](https://github.com/socketio/engine.io/releases/tag/3.2.0)
|
||||
|
||||
There are two non-breaking changes that are somehow quite important:
|
||||
|
||||
- `ws` was reverted as the default wsEngine (https://github.com/socketio/engine.io/pull/550), as there was several blocking issues with `uws`. You can still use `uws` by running `npm install uws --save` in your project and using the `wsEngine` option:
|
||||
```js
|
||||
var engine = require('engine.io');
|
||||
var server = engine.listen(3000, {
|
||||
wsEngine: 'uws'
|
||||
});
|
||||
```
|
||||
|
||||
- `pingTimeout` now defaults to 5 seconds (instead of 60 seconds): https://github.com/socketio/engine.io/pull/551
|
||||
|
||||
@@ -21,6 +21,8 @@ Some implementations in other languages are also available:
|
||||
- [Dart](https://github.com/rikulo/socket.io-client-dart)
|
||||
- [Python](https://github.com/miguelgrinberg/python-socketio)
|
||||
- [.NET](https://github.com/doghappy/socket.io-client-csharp)
|
||||
- [Rust](https://github.com/1c3t3a/rust-socketio)
|
||||
- [PHP](https://github.com/ElephantIO/elephant.io)
|
||||
|
||||
Its main features are:
|
||||
|
||||
@@ -125,7 +127,7 @@ io.listen(3000);
|
||||
|
||||
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
|
||||
to pass the `Server` to `socket.io`, not the express application
|
||||
function. Also make sure to call `.listen` on the `server`, not the `app`.
|
||||
|
||||
```js
|
||||
|
||||
22
SECURITY.md
Normal file
22
SECURITY.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
| Version | Supported |
|
||||
|---------|--------------------|
|
||||
| 4.x | :white_check_mark: |
|
||||
| 3.x | :white_check_mark: |
|
||||
| 2.4.x | :white_check_mark: |
|
||||
| < 2.4.0 | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
To report a security vulnerability in this package, please send an email to [@darrachequesne](https://github.com/darrachequesne) (see address in profile) describing the vulnerability and how to reproduce it.
|
||||
|
||||
We will get back to you as soon as possible and publish a fix if necessary.
|
||||
|
||||
:warning: IMPORTANT :warning: please do not create an issue in this repository, as attackers might take advantage of it. Thank you in advance for your responsible disclosure.
|
||||
|
||||
## History
|
||||
|
||||
No security vulnerability were reported yet.
|
||||
6
client-dist/socket.io.esm.min.js
vendored
6
client-dist/socket.io.esm.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
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
6
client-dist/socket.io.min.js
vendored
6
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
6
client-dist/socket.io.msgpack.min.js
vendored
6
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
@@ -15,7 +15,7 @@ interface Todo {
|
||||
|
||||
let todos: Array<Todo> = [];
|
||||
|
||||
io.on("connect", (socket) => {
|
||||
io.on("connection", (socket) => {
|
||||
socket.emit("todos", todos);
|
||||
|
||||
// note: we could also create a CRUD (create/read/update/delete) service for the todo list
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
|
||||
Please read the related [guide](https://socket.io/get-started/basic-crud-application/).
|
||||
|
||||
This repository contains several implementations of the server:
|
||||
|
||||
| Directory | Language | Database | Cluster? |
|
||||
|----------------------------|------------|------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------|
|
||||
| `server/` | TypeScript | in-memory | No |
|
||||
| `server-postgres-cluster/` | JavaScript | Postgres, with the [Postgres adapter](https://socket.io/docs/v4/postgres-adapter/) | Yes, with the [`@socket.io/sticky`](https://github.com/socketio/socket.io-sticky) module) |
|
||||
|
||||
## Running the frontend
|
||||
|
||||
```
|
||||
|
||||
@@ -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,21 +1,18 @@
|
||||
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# compiled output
|
||||
# Compiled output
|
||||
/dist
|
||||
/tmp
|
||||
/out-tsc
|
||||
# Only exists if Bazel was run
|
||||
/bazel-out
|
||||
|
||||
# dependencies
|
||||
# Node
|
||||
/node_modules
|
||||
|
||||
# profiling files
|
||||
chrome-profiler-events*.json
|
||||
speed-measure-plugin*.json
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.idea/
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
@@ -23,7 +20,7 @@ speed-measure-plugin*.json
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# IDE - VSCode
|
||||
# Visual Studio Code
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
@@ -31,16 +28,15 @@ speed-measure-plugin*.json
|
||||
!.vscode/extensions.json
|
||||
.history/*
|
||||
|
||||
# misc
|
||||
/.sass-cache
|
||||
# Miscellaneous
|
||||
/.angular/cache
|
||||
.sass-cache/
|
||||
/connect.lock
|
||||
/coverage
|
||||
/libpeerconnection.log
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
testem.log
|
||||
/typings
|
||||
|
||||
# System Files
|
||||
# System files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
# Angular TodoMVC + Socket.IO
|
||||
# AngularClient
|
||||
|
||||
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).
|
||||
|
||||

|
||||
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 17.0.2.
|
||||
|
||||
## 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.
|
||||
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files.
|
||||
|
||||
## Code scaffolding
|
||||
|
||||
@@ -16,7 +12,7 @@ Run `ng generate component component-name` to generate a new component. You can
|
||||
|
||||
## 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.
|
||||
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
|
||||
|
||||
## Running unit tests
|
||||
|
||||
@@ -24,7 +20,7 @@ Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.
|
||||
|
||||
## Running end-to-end tests
|
||||
|
||||
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
|
||||
Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.
|
||||
|
||||
## Further help
|
||||
|
||||
|
||||
@@ -3,26 +3,23 @@
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"angular-todomvc": {
|
||||
"angular-client": {
|
||||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@schematics/angular:application": {
|
||||
"strict": true
|
||||
}
|
||||
},
|
||||
"schematics": {},
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"builder": "@angular-devkit/build-angular:application",
|
||||
"options": {
|
||||
"outputPath": "dist/angular-todomvc",
|
||||
"outputPath": "dist/angular-client",
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"browser": "src/main.ts",
|
||||
"polyfills": [
|
||||
"zone.js"
|
||||
],
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"aot": true,
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets"
|
||||
@@ -34,19 +31,6 @@
|
||||
},
|
||||
"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",
|
||||
@@ -58,34 +42,49 @@
|
||||
"maximumWarning": "2kb",
|
||||
"maximumError": "4kb"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
},
|
||||
"development": {
|
||||
"optimization": false,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true,
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.development.ts"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"options": {
|
||||
"browserTarget": "angular-todomvc:build"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "angular-todomvc:build:production"
|
||||
"buildTarget": "angular-client:build:production"
|
||||
},
|
||||
"development": {
|
||||
"buildTarget": "angular-client:build:development"
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"browserTarget": "angular-todomvc:build"
|
||||
"buildTarget": "angular-client:build"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"main": "src/test.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"polyfills": [
|
||||
"zone.js",
|
||||
"zone.js/testing"
|
||||
],
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"karmaConfig": "karma.conf.js",
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets"
|
||||
@@ -95,34 +94,8 @@
|
||||
],
|
||||
"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
|
||||
});
|
||||
};
|
||||
@@ -1,46 +1,40 @@
|
||||
{
|
||||
"name": "angular-todomvc",
|
||||
"name": "angular-client",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"build": "ng build",
|
||||
"test": "ng test",
|
||||
"lint": "ng lint",
|
||||
"e2e": "ng e2e"
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"test": "ng test"
|
||||
},
|
||||
"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-client": "^4.0.0",
|
||||
"tslib": "^2.0.0",
|
||||
"zone.js": "~0.10.2"
|
||||
"@angular/animations": "^17.0.0",
|
||||
"@angular/common": "^17.0.0",
|
||||
"@angular/compiler": "^17.0.0",
|
||||
"@angular/core": "^17.0.0",
|
||||
"@angular/forms": "^17.0.0",
|
||||
"@angular/platform-browser": "^17.0.0",
|
||||
"@angular/platform-browser-dynamic": "^17.0.0",
|
||||
"@angular/router": "^17.0.0",
|
||||
"rxjs": "~7.8.0",
|
||||
"socket.io-client": "^4.7.2",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.14.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"
|
||||
"@angular-devkit/build-angular": "^17.0.2",
|
||||
"@angular/cli": "^17.0.2",
|
||||
"@angular/compiler-cli": "^17.0.0",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
"@types/node": "^20.9.2",
|
||||
"jasmine-core": "~5.1.0",
|
||||
"karma": "~6.4.0",
|
||||
"karma-chrome-launcher": "~3.2.0",
|
||||
"karma-coverage": "~2.2.0",
|
||||
"karma-jasmine": "~5.1.0",
|
||||
"karma-jasmine-html-reporter": "~2.1.0",
|
||||
"typescript": "~5.2.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<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()">
|
||||
<!-- <input class="new-todo" placeholder="What needs to be done?" autofocus="" [(ngModel)]="newTodoText" (keyup.enter)="addTodo()">-->
|
||||
<input class="new-todo" placeholder="What needs to be done?" autofocus="" [formControl]="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)">
|
||||
|
||||
@@ -4,9 +4,7 @@ import { AppComponent } from './app.component';
|
||||
describe('AppComponent', () => {
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
AppComponent
|
||||
],
|
||||
imports: [AppComponent],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
@@ -16,16 +14,16 @@ describe('AppComponent', () => {
|
||||
expect(app).toBeTruthy();
|
||||
});
|
||||
|
||||
it(`should have as title 'angular-todomvc'`, () => {
|
||||
it(`should have the 'angular-client' title`, () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.componentInstance;
|
||||
expect(app.title).toEqual('angular-todomvc');
|
||||
expect(app.title).toEqual('angular-client');
|
||||
});
|
||||
|
||||
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!');
|
||||
const compiled = fixture.nativeElement as HTMLElement;
|
||||
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, angular-client');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { TodoStore, Todo } from './store';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterOutlet } from '@angular/router';
|
||||
import {type Todo, TodoStore} from "./store";
|
||||
import { FormControl, ReactiveFormsModule } from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
standalone: true,
|
||||
imports: [CommonModule, RouterOutlet, ReactiveFormsModule],
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.css']
|
||||
styleUrl: './app.component.css',
|
||||
providers: [TodoStore]
|
||||
})
|
||||
export class AppComponent {
|
||||
todoStore: TodoStore;
|
||||
newTodoText = '';
|
||||
newTodoText = new FormControl('');
|
||||
|
||||
constructor(todoStore: TodoStore) {
|
||||
this.todoStore = todoStore;
|
||||
constructor(readonly todoStore: TodoStore) {
|
||||
}
|
||||
|
||||
stopEditing(todo: Todo, editedTitle: string) {
|
||||
@@ -51,9 +55,9 @@ export class AppComponent {
|
||||
}
|
||||
|
||||
addTodo() {
|
||||
if (this.newTodoText.trim().length) {
|
||||
this.todoStore.add(this.newTodoText);
|
||||
this.newTodoText = '';
|
||||
if (this.newTodoText.value?.trim().length) {
|
||||
this.todoStore.add(this.newTodoText.value!);
|
||||
this.newTodoText.setValue('');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { ApplicationConfig } from '@angular/core';
|
||||
import { provideRouter } from '@angular/router';
|
||||
|
||||
import { routes } from './app.routes';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [provideRouter(routes)]
|
||||
};
|
||||
@@ -1,19 +0,0 @@
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
import { AppComponent } from './app.component';
|
||||
import { TodoStore } from './store';
|
||||
import { FormsModule } from "@angular/forms";
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
FormsModule
|
||||
],
|
||||
providers: [TodoStore],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
export class AppModule { }
|
||||
@@ -0,0 +1,3 @@
|
||||
import { Routes } from '@angular/router';
|
||||
|
||||
export const routes: Routes = [];
|
||||
@@ -1,6 +1,7 @@
|
||||
import { io, Socket } from "socket.io-client";
|
||||
import { ClientEvents, ServerEvents } from "../../../server/lib/events";
|
||||
import { ClientEvents, ServerEvents } from "../../../common/events";
|
||||
import { environment } from '../environments/environment';
|
||||
import {Injectable} from "@angular/core";
|
||||
|
||||
export interface Todo {
|
||||
id: string,
|
||||
@@ -18,6 +19,7 @@ const mapTodo = (todo: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class TodoStore {
|
||||
public todos: Array<Todo> = [];
|
||||
private socket: Socket<ServerEvents, ClientEvents>;
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export const environment = {
|
||||
serverUrl: "http://localhost:3000"
|
||||
};
|
||||
@@ -1,4 +0,0 @@
|
||||
export const environment = {
|
||||
production: true,
|
||||
serverUrl: "https://my-custom-domain.com"
|
||||
};
|
||||
@@ -1,17 +1,3 @@
|
||||
// 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,
|
||||
serverUrl: "http://localhost:3000"
|
||||
serverUrl: "https://my-custom-domain.com"
|
||||
};
|
||||
|
||||
/*
|
||||
* 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 After Width: | Height: | Size: 15 KiB |
@@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Angular Todo MVC</title>
|
||||
<title>AngularClient</title>
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import { enableProdMode } from '@angular/core';
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||
import { bootstrapApplication } from '@angular/platform-browser';
|
||||
import { appConfig } from './app/app.config';
|
||||
import { AppComponent } from './app/app.component';
|
||||
|
||||
import { AppModule } from './app/app.module';
|
||||
import { environment } from './environments/environment';
|
||||
|
||||
if (environment.production) {
|
||||
enableProdMode();
|
||||
}
|
||||
|
||||
platformBrowserDynamic().bootstrapModule(AppModule)
|
||||
.catch(err => console.error(err));
|
||||
bootstrapApplication(AppComponent, appConfig)
|
||||
.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,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);
|
||||
@@ -6,8 +6,7 @@
|
||||
"types": []
|
||||
},
|
||||
"files": [
|
||||
"src/main.ts",
|
||||
"src/polyfills.ts"
|
||||
"src/main.ts"
|
||||
],
|
||||
"include": [
|
||||
"src/**/*.d.ts"
|
||||
|
||||
@@ -2,26 +2,29 @@
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"outDir": "./dist/out-tsc",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"esModuleInterop": true,
|
||||
"sourceMap": true,
|
||||
"declaration": false,
|
||||
"downlevelIteration": true,
|
||||
"experimentalDecorators": true,
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"target": "es2015",
|
||||
"module": "es2020",
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
"useDefineForClassFields": false,
|
||||
"lib": [
|
||||
"es2018",
|
||||
"ES2022",
|
||||
"dom"
|
||||
]
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"strictTemplates": true
|
||||
|
||||
@@ -7,10 +7,6 @@
|
||||
"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,9 +1,18 @@
|
||||
import { Todo, TodoID } from "./todo-management/todo.repository";
|
||||
import { ValidationErrorItem } from "joi";
|
||||
export type TodoID = string;
|
||||
|
||||
export interface Todo {
|
||||
id: TodoID;
|
||||
completed: boolean;
|
||||
title: string;
|
||||
}
|
||||
|
||||
interface Error {
|
||||
error: string;
|
||||
errorDetails?: ValidationErrorItem[];
|
||||
errorDetails?: {
|
||||
message: string;
|
||||
path: Array<string | number>;
|
||||
type: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
interface Success<T> {
|
||||
@@ -0,0 +1,16 @@
|
||||
|
||||
A basic TODO project.
|
||||
|
||||
| Characteristic | |
|
||||
|----------------|-------------------------------------------------------------------------------------------|
|
||||
| Language | plain JavaScript |
|
||||
| Database | Postgres, with the [Postgres adapter](https://socket.io/docs/v4/postgres-adapter/) |
|
||||
| Cluster? | Yes, with the [`@socket.io/sticky`](https://github.com/socketio/socket.io-sticky) module) |
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
$ docker-compose up -d
|
||||
$ npm install
|
||||
$ npm start
|
||||
```
|
||||
@@ -0,0 +1,9 @@
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:12
|
||||
ports:
|
||||
- "5432:5432"
|
||||
environment:
|
||||
POSTGRES_PASSWORD: "changeit"
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Server } from "socket.io";
|
||||
import createTodoHandlers from "./todo-management/todo.handlers.js";
|
||||
import { setupWorker } from "@socket.io/sticky";
|
||||
import { createAdapter } from "@socket.io/postgres-adapter";
|
||||
|
||||
export function createApplication(httpServer, components, serverOptions = {}) {
|
||||
const io = new Server(httpServer, serverOptions);
|
||||
|
||||
const { createTodo, readTodo, updateTodo, deleteTodo, listTodo } =
|
||||
createTodoHandlers(components);
|
||||
|
||||
io.on("connection", (socket) => {
|
||||
socket.on("todo:create", createTodo);
|
||||
socket.on("todo:read", readTodo);
|
||||
socket.on("todo:update", updateTodo);
|
||||
socket.on("todo:delete", deleteTodo);
|
||||
socket.on("todo:list", listTodo);
|
||||
});
|
||||
|
||||
// enable sticky session in the cluster (to remove in standalone mode)
|
||||
setupWorker(io);
|
||||
|
||||
io.adapter(createAdapter(components.connectionPool));
|
||||
|
||||
return io;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import cluster from "cluster";
|
||||
import { createServer } from "http";
|
||||
import { setupMaster } from "@socket.io/sticky";
|
||||
import { cpus } from "os";
|
||||
|
||||
if (cluster.isMaster) {
|
||||
console.log(`Master ${process.pid} is running`);
|
||||
const httpServer = createServer();
|
||||
|
||||
setupMaster(httpServer, {
|
||||
loadBalancingMethod: "least-connection",
|
||||
});
|
||||
|
||||
httpServer.listen(3000);
|
||||
|
||||
for (let i = 0; i < cpus().length; i++) {
|
||||
cluster.fork();
|
||||
}
|
||||
|
||||
cluster.on("exit", (worker) => {
|
||||
console.log(`Worker ${worker.process.pid} died`);
|
||||
cluster.fork();
|
||||
});
|
||||
} else {
|
||||
console.log(`Worker ${process.pid} started`);
|
||||
|
||||
import("./index.js");
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { createServer } from "http";
|
||||
import { createApplication } from "./app.js";
|
||||
import { Sequelize } from "sequelize";
|
||||
import pg from "pg";
|
||||
import { PostgresTodoRepository } from "./todo-management/todo.repository.js";
|
||||
|
||||
const httpServer = createServer();
|
||||
|
||||
const sequelize = new Sequelize("postgres", "postgres", "changeit", {
|
||||
dialect: "postgres",
|
||||
});
|
||||
|
||||
const connectionPool = new pg.Pool({
|
||||
user: "postgres",
|
||||
host: "localhost",
|
||||
database: "postgres",
|
||||
password: "changeit",
|
||||
port: 5432,
|
||||
});
|
||||
|
||||
createApplication(
|
||||
httpServer,
|
||||
{
|
||||
connectionPool,
|
||||
todoRepository: new PostgresTodoRepository(sequelize),
|
||||
},
|
||||
{
|
||||
cors: {
|
||||
origin: ["http://localhost:4200"],
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const main = async () => {
|
||||
// create the tables if they do not exist already
|
||||
await sequelize.sync();
|
||||
|
||||
// create the table needed by the postgres adapter
|
||||
await connectionPool.query(`
|
||||
CREATE TABLE IF NOT EXISTS socket_io_attachments (
|
||||
id bigserial UNIQUE,
|
||||
created_at timestamptz DEFAULT NOW(),
|
||||
payload bytea
|
||||
);
|
||||
`);
|
||||
|
||||
// uncomment when running in standalone mode
|
||||
// httpServer.listen(3000);
|
||||
};
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,140 @@
|
||||
import { Errors, mapErrorDetails, sanitizeErrorMessage } from "../util.js";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import Joi from "joi";
|
||||
|
||||
const idSchema = Joi.string().guid({
|
||||
version: "uuidv4",
|
||||
});
|
||||
|
||||
const todoSchema = Joi.object({
|
||||
id: idSchema.alter({
|
||||
create: (schema) => schema.forbidden(),
|
||||
update: (schema) => schema.required(),
|
||||
}),
|
||||
title: Joi.string().max(256).required(),
|
||||
completed: Joi.boolean().required(),
|
||||
});
|
||||
|
||||
export default function (components) {
|
||||
const { todoRepository } = components;
|
||||
return {
|
||||
createTodo: async function (payload, callback) {
|
||||
const socket = this;
|
||||
|
||||
// validate the payload
|
||||
const { error, value } = todoSchema.tailor("create").validate(payload, {
|
||||
abortEarly: false,
|
||||
stripUnknown: true,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return callback({
|
||||
error: Errors.INVALID_PAYLOAD,
|
||||
errorDetails: mapErrorDetails(error.details),
|
||||
});
|
||||
}
|
||||
|
||||
value.id = uuid();
|
||||
|
||||
// persist the entity
|
||||
try {
|
||||
await todoRepository.save(value);
|
||||
} catch (e) {
|
||||
return callback({
|
||||
error: sanitizeErrorMessage(e),
|
||||
});
|
||||
}
|
||||
|
||||
// acknowledge the creation
|
||||
callback({
|
||||
data: value.id,
|
||||
});
|
||||
|
||||
// notify the other users
|
||||
socket.broadcast.emit("todo:created", value);
|
||||
},
|
||||
|
||||
readTodo: async function (id, callback) {
|
||||
const { error } = idSchema.validate(id);
|
||||
|
||||
if (error) {
|
||||
return callback({
|
||||
error: Errors.ENTITY_NOT_FOUND,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const todo = await todoRepository.findById(id);
|
||||
callback({
|
||||
data: todo,
|
||||
});
|
||||
} catch (e) {
|
||||
callback({
|
||||
error: sanitizeErrorMessage(e),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
updateTodo: async function (payload, callback) {
|
||||
const socket = this;
|
||||
|
||||
const { error, value } = todoSchema.tailor("update").validate(payload, {
|
||||
abortEarly: false,
|
||||
stripUnknown: true,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return callback({
|
||||
error: Errors.INVALID_PAYLOAD,
|
||||
errorDetails: mapErrorDetails(error.details),
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await todoRepository.save(value);
|
||||
} catch (e) {
|
||||
return callback({
|
||||
error: sanitizeErrorMessage(e),
|
||||
});
|
||||
}
|
||||
|
||||
callback();
|
||||
socket.broadcast.emit("todo:updated", value);
|
||||
},
|
||||
|
||||
deleteTodo: async function (id, callback) {
|
||||
const socket = this;
|
||||
|
||||
const { error } = idSchema.validate(id);
|
||||
|
||||
if (error) {
|
||||
return callback({
|
||||
error: Errors.ENTITY_NOT_FOUND,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await todoRepository.deleteById(id);
|
||||
} catch (e) {
|
||||
return callback({
|
||||
error: sanitizeErrorMessage(e),
|
||||
});
|
||||
}
|
||||
|
||||
callback();
|
||||
socket.broadcast.emit("todo:deleted", id);
|
||||
},
|
||||
|
||||
listTodo: async function (callback) {
|
||||
try {
|
||||
callback({
|
||||
data: await todoRepository.findAll(),
|
||||
});
|
||||
} catch (e) {
|
||||
callback({
|
||||
error: sanitizeErrorMessage(e),
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Errors } from "../util.js";
|
||||
import { Model, DataTypes } from "sequelize";
|
||||
|
||||
class CrudRepository {
|
||||
findAll() {}
|
||||
findById(id) {}
|
||||
save(entity) {}
|
||||
deleteById(id) {}
|
||||
}
|
||||
|
||||
export class TodoRepository extends CrudRepository {}
|
||||
|
||||
class Todo extends Model {}
|
||||
|
||||
export class PostgresTodoRepository extends TodoRepository {
|
||||
constructor(sequelize) {
|
||||
super();
|
||||
this.sequelize = sequelize;
|
||||
|
||||
Todo.init(
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.STRING,
|
||||
primaryKey: true,
|
||||
allowNull: false,
|
||||
},
|
||||
title: {
|
||||
type: DataTypes.STRING,
|
||||
},
|
||||
completed: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
},
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
tableName: "todos",
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
findAll() {
|
||||
return this.sequelize.transaction((transaction) => {
|
||||
return Todo.findAll({ transaction });
|
||||
});
|
||||
}
|
||||
|
||||
async findById(id) {
|
||||
return this.sequelize.transaction(async (transaction) => {
|
||||
const todo = await Todo.findByPk(id, { transaction });
|
||||
|
||||
if (!todo) {
|
||||
throw Errors.ENTITY_NOT_FOUND;
|
||||
}
|
||||
|
||||
return todo;
|
||||
});
|
||||
}
|
||||
|
||||
save(entity) {
|
||||
return this.sequelize.transaction((transaction) => {
|
||||
return Todo.upsert(entity, { transaction });
|
||||
});
|
||||
}
|
||||
|
||||
async deleteById(id) {
|
||||
return this.sequelize.transaction(async (transaction) => {
|
||||
const count = await Todo.destroy({ where: { id }, transaction });
|
||||
|
||||
if (count === 0) {
|
||||
throw Errors.ENTITY_NOT_FOUND;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
export const Errors = {
|
||||
ENTITY_NOT_FOUND: "entity not found",
|
||||
INVALID_PAYLOAD: "invalid payload",
|
||||
};
|
||||
|
||||
const errorValues = Object.values(Errors);
|
||||
|
||||
export function sanitizeErrorMessage(message) {
|
||||
if (typeof message === "string" && errorValues.includes(message)) {
|
||||
return message;
|
||||
} else {
|
||||
return "an unknown error has occurred";
|
||||
}
|
||||
}
|
||||
|
||||
export function mapErrorDetails(details) {
|
||||
return details.map((item) => ({
|
||||
message: item.message,
|
||||
path: item.path,
|
||||
type: item.type,
|
||||
}));
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "basic-crud-server",
|
||||
"version": "0.0.1",
|
||||
"description": "Server for the Basic CRUD Socket.IO example (with Postgres and multiple Socket.IO servers)",
|
||||
"main": "lib/cluster.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node lib/cluster.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/socketio/socket.io.git"
|
||||
},
|
||||
"author": "Damien Arrachequesne <damien.arrachequesne@gmail.com>",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/socketio/socket.io/issues"
|
||||
},
|
||||
"homepage": "https://github.com/socketio/socket.io#readme",
|
||||
"dependencies": {
|
||||
"@socket.io/postgres-adapter": "^0.2.0",
|
||||
"@socket.io/sticky": "^1.0.1",
|
||||
"joi": "^17.4.0",
|
||||
"pg": "^8.7.3",
|
||||
"pg-hstore": "^2.3.4",
|
||||
"sequelize": "^6.18.0",
|
||||
"socket.io": "^4.0.1",
|
||||
"uuid": "^8.3.2"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Server as HttpServer } from "http";
|
||||
import { Server, ServerOptions } from "socket.io";
|
||||
import { ClientEvents, ServerEvents } from "./events";
|
||||
import { ClientEvents, ServerEvents } from "../../common/events";
|
||||
import { TodoRepository } from "./todo-management/todo.repository";
|
||||
import createTodoHandlers from "./todo-management/todo.handlers";
|
||||
|
||||
|
||||
@@ -2,8 +2,13 @@ import { Errors, mapErrorDetails, sanitizeErrorMessage } from "../util";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import { Components } from "../app";
|
||||
import Joi = require("joi");
|
||||
import { Todo, TodoID } from "./todo.repository";
|
||||
import { ClientEvents, Response, ServerEvents } from "../events";
|
||||
import {
|
||||
Todo,
|
||||
TodoID,
|
||||
ClientEvents,
|
||||
Response,
|
||||
ServerEvents,
|
||||
} from "../../../common/events";
|
||||
import { Socket } from "socket.io";
|
||||
|
||||
const idSchema = Joi.string().guid({
|
||||
@@ -19,8 +24,7 @@ const todoSchema = Joi.object({
|
||||
completed: Joi.boolean().required(),
|
||||
});
|
||||
|
||||
export default function (components: Components) {
|
||||
const { todoRepository } = components;
|
||||
export default function ({ todoRepository }: Components) {
|
||||
return {
|
||||
createTodo: async function (
|
||||
payload: Omit<Todo, "id">,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Errors } from "../util";
|
||||
import { Todo, TodoID } from "../../../common/events";
|
||||
|
||||
abstract class CrudRepository<T, ID> {
|
||||
abstract findAll(): Promise<T[]>;
|
||||
@@ -7,14 +8,6 @@ abstract class CrudRepository<T, ID> {
|
||||
abstract deleteById(id: ID): Promise<void>;
|
||||
}
|
||||
|
||||
export type TodoID = string;
|
||||
|
||||
export interface Todo {
|
||||
id: TodoID;
|
||||
completed: boolean;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export abstract class TodoRepository extends CrudRepository<Todo, TodoID> {}
|
||||
|
||||
export class InMemoryTodoRepository extends TodoRepository {
|
||||
|
||||
@@ -24,14 +24,14 @@
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mocha": "^8.2.3",
|
||||
"@types/chai": "^4.2.16",
|
||||
"@types/mocha": "^10.0.0",
|
||||
"@types/uuid": "^8.3.0",
|
||||
"chai": "^4.3.4",
|
||||
"mocha": "^8.3.2",
|
||||
"mocha": "^10.0.0",
|
||||
"nyc": "^15.1.0",
|
||||
"socket.io-client": "^4.0.1",
|
||||
"ts-node": "^9.1.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.2.4"
|
||||
}
|
||||
}
|
||||
|
||||
23
examples/basic-crud-application/vue-client/.gitignore
vendored
Normal file
23
examples/basic-crud-application/vue-client/.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
24
examples/basic-crud-application/vue-client/README.md
Normal file
24
examples/basic-crud-application/vue-client/README.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# vue-client
|
||||
|
||||
## Project setup
|
||||
```
|
||||
yarn install
|
||||
```
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
```
|
||||
yarn serve
|
||||
```
|
||||
|
||||
### Compiles and minifies for production
|
||||
```
|
||||
yarn build
|
||||
```
|
||||
|
||||
### Lints and fixes files
|
||||
```
|
||||
yarn lint
|
||||
```
|
||||
|
||||
### Customize configuration
|
||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
||||
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
||||
19
examples/basic-crud-application/vue-client/jsconfig.json
Normal file
19
examples/basic-crud-application/vue-client/jsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"module": "esnext",
|
||||
"baseUrl": "./",
|
||||
"moduleResolution": "node",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
},
|
||||
"lib": [
|
||||
"esnext",
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"scripthost"
|
||||
]
|
||||
}
|
||||
}
|
||||
45
examples/basic-crud-application/vue-client/package.json
Normal file
45
examples/basic-crud-application/vue-client/package.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "vue-client",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve --port 4200",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"core-js": "^3.8.3",
|
||||
"pinia": "^2.1.7",
|
||||
"socket.io-client": "^4.7.2",
|
||||
"vue": "^3.2.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.12.16",
|
||||
"@babel/eslint-parser": "^7.12.16",
|
||||
"@vue/cli-plugin-babel": "~5.0.0",
|
||||
"@vue/cli-plugin-eslint": "~5.0.0",
|
||||
"@vue/cli-service": "~5.0.0",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-plugin-vue": "^8.0.3"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"env": {
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"plugin:vue/vue3-essential",
|
||||
"eslint:recommended"
|
||||
],
|
||||
"parserOptions": {
|
||||
"parser": "@babel/eslint-parser"
|
||||
},
|
||||
"rules": {}
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not dead",
|
||||
"not ie 11"
|
||||
]
|
||||
}
|
||||
BIN
examples/basic-crud-application/vue-client/public/favicon.ico
Normal file
BIN
examples/basic-crud-application/vue-client/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
18
examples/basic-crud-application/vue-client/public/index.html
Normal file
18
examples/basic-crud-application/vue-client/public/index.html
Normal file
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<link href="styles.css" rel="stylesheet" type="text/css" />
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
||||
381
examples/basic-crud-application/vue-client/public/styles.css
Normal file
381
examples/basic-crud-application/vue-client/public/styles.css
Normal file
@@ -0,0 +1,381 @@
|
||||
/* 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;
|
||||
}
|
||||
}
|
||||
123
examples/basic-crud-application/vue-client/src/App.vue
Normal file
123
examples/basic-crud-application/vue-client/src/App.vue
Normal file
@@ -0,0 +1,123 @@
|
||||
<script setup>
|
||||
import { computed, ref } from "vue";
|
||||
import { useTodoStore } from "@/stores/todo";
|
||||
import { socket } from "@/socket";
|
||||
|
||||
const newTodo = ref("");
|
||||
const editedTodo = ref(undefined);
|
||||
const newTitle = ref("");
|
||||
|
||||
const store = useTodoStore();
|
||||
|
||||
// remove any existing listeners (in case of hot reload)
|
||||
socket.off();
|
||||
|
||||
store.bindEvents();
|
||||
|
||||
function addTodo() {
|
||||
const value = newTodo.value && newTodo.value.trim();
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
store.add(value);
|
||||
newTodo.value = "";
|
||||
}
|
||||
|
||||
function editTodo(todo) {
|
||||
editedTodo.value = todo;
|
||||
newTitle.value = todo.title;
|
||||
}
|
||||
|
||||
function doneEdit(todo) {
|
||||
if (newTitle.value) {
|
||||
store.setTitle(todo, newTitle.value);
|
||||
} else {
|
||||
store.delete(todo);
|
||||
}
|
||||
editedTodo.value = undefined;
|
||||
}
|
||||
|
||||
function cancelEdit() {
|
||||
editedTodo.value = undefined;
|
||||
}
|
||||
|
||||
const allDone = computed({
|
||||
get: () => {
|
||||
return store.remaining === 0;
|
||||
},
|
||||
set: (value) => {
|
||||
store.toggleAll(value);
|
||||
},
|
||||
});
|
||||
|
||||
function pluralize(word, count) {
|
||||
return word + (count === 1 ? "" : "s");
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="todoapp" v-cloak>
|
||||
<header class="header">
|
||||
<h1>todos</h1>
|
||||
<input
|
||||
class="new-todo"
|
||||
autofocus
|
||||
autocomplete="off"
|
||||
placeholder="What needs to be done?"
|
||||
v-model="newTodo"
|
||||
@keydown.enter="addTodo"
|
||||
/>
|
||||
</header>
|
||||
<section class="main" v-show="store.todos.length">
|
||||
<input
|
||||
id="toggle-all"
|
||||
class="toggle-all"
|
||||
type="checkbox"
|
||||
v-model="allDone"
|
||||
/>
|
||||
<label for="toggle-all">Mark all as complete</label>
|
||||
<ul class="todo-list">
|
||||
<li
|
||||
class="todo"
|
||||
v-for="todo in store.todos"
|
||||
:key="todo.id"
|
||||
:class="{ completed: todo.completed, editing: todo === editedTodo }"
|
||||
>
|
||||
<div class="view">
|
||||
<input
|
||||
class="toggle"
|
||||
type="checkbox"
|
||||
v-model="todo.completed"
|
||||
@click="store.toggleOne(todo)"
|
||||
/>
|
||||
<label @dblclick="editTodo(todo)">{{ todo.title }}</label>
|
||||
<button class="destroy" @click="store.delete(todo)"></button>
|
||||
</div>
|
||||
<input
|
||||
class="edit"
|
||||
type="text"
|
||||
v-model="newTitle"
|
||||
@blur="doneEdit"
|
||||
@keydown.enter="doneEdit(todo)"
|
||||
@keydown.esc="cancelEdit(todo)"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<footer class="footer" v-show="store.todos.length">
|
||||
<span class="todo-count">
|
||||
<strong v-text="store.remaining"></strong>
|
||||
{{ pluralize("item", store.remaining) }} left
|
||||
</span>
|
||||
<button
|
||||
class="clear-completed"
|
||||
@click="store.deleteCompleted"
|
||||
v-show="store.todos.length > store.remaining"
|
||||
>
|
||||
Clear complete
|
||||
</button>
|
||||
</footer>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
BIN
examples/basic-crud-application/vue-client/src/assets/logo.png
Normal file
BIN
examples/basic-crud-application/vue-client/src/assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.7 KiB |
9
examples/basic-crud-application/vue-client/src/main.js
Normal file
9
examples/basic-crud-application/vue-client/src/main.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import { createApp } from "vue";
|
||||
import { createPinia } from "pinia";
|
||||
import App from "./App.vue";
|
||||
|
||||
const pinia = createPinia();
|
||||
const app = createApp(App);
|
||||
|
||||
app.use(pinia);
|
||||
app.mount("#app");
|
||||
7
examples/basic-crud-application/vue-client/src/socket.js
Normal file
7
examples/basic-crud-application/vue-client/src/socket.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import { io } from "socket.io-client";
|
||||
|
||||
// "undefined" means the URL will be computed from the `window.location` object
|
||||
const URL =
|
||||
process.env.NODE_ENV === "production" ? undefined : "http://localhost:3000";
|
||||
|
||||
export const socket = io(URL);
|
||||
106
examples/basic-crud-application/vue-client/src/stores/todo.js
Normal file
106
examples/basic-crud-application/vue-client/src/stores/todo.js
Normal file
@@ -0,0 +1,106 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { socket } from "@/socket";
|
||||
|
||||
export const useTodoStore = defineStore("todo", {
|
||||
state: () => ({
|
||||
todos: [],
|
||||
}),
|
||||
|
||||
getters: {
|
||||
remaining(state) {
|
||||
let count = 0;
|
||||
state.todos.forEach((todo) => {
|
||||
if (!todo.completed) {
|
||||
count++;
|
||||
}
|
||||
});
|
||||
return count;
|
||||
},
|
||||
},
|
||||
|
||||
actions: {
|
||||
bindEvents() {
|
||||
socket.on("connect", () => {
|
||||
socket.emit("todo:list", (res) => {
|
||||
this.todos = res.data;
|
||||
});
|
||||
});
|
||||
|
||||
socket.on("todo:created", (todo) => {
|
||||
this.todos.push(todo);
|
||||
});
|
||||
|
||||
socket.on("todo:updated", (todo) => {
|
||||
const existingTodo = this.todos.find((t) => {
|
||||
return t.id === todo.id;
|
||||
});
|
||||
if (existingTodo) {
|
||||
existingTodo.title = todo.title;
|
||||
existingTodo.completed = todo.completed;
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("todo:deleted", (id) => {
|
||||
const i = this.todos.findIndex((t) => {
|
||||
return t.id === id;
|
||||
});
|
||||
if (i !== -1) {
|
||||
this.todos.splice(i, 1);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
add(title) {
|
||||
const todo = {
|
||||
id: Date.now(),
|
||||
title,
|
||||
completed: false,
|
||||
};
|
||||
this.todos.push(todo);
|
||||
socket.emit("todo:create", { title, completed: false }, (res) => {
|
||||
todo.id = res.data;
|
||||
});
|
||||
},
|
||||
|
||||
setTitle(todo, title) {
|
||||
todo.title = title;
|
||||
socket.emit("todo:update", todo, () => {});
|
||||
},
|
||||
|
||||
delete(todo) {
|
||||
const i = this.todos.findIndex((t) => {
|
||||
return t.id === todo.id;
|
||||
});
|
||||
|
||||
if (i !== -1) {
|
||||
this.todos.splice(i, 1);
|
||||
socket.emit("todo:delete", todo.id, () => {});
|
||||
}
|
||||
},
|
||||
|
||||
deleteCompleted() {
|
||||
this.todos.forEach((todo) => {
|
||||
if (todo.completed) {
|
||||
socket.emit("todo:delete", todo.id, () => {});
|
||||
}
|
||||
});
|
||||
|
||||
this.todos = this.todos.filter((t) => {
|
||||
return !t.completed;
|
||||
});
|
||||
},
|
||||
|
||||
toggleOne(todo) {
|
||||
todo.completed = !todo.completed;
|
||||
socket.emit("todo:update", todo, () => {});
|
||||
},
|
||||
|
||||
toggleAll(onlyActive) {
|
||||
this.todos.forEach((todo) => {
|
||||
if (!onlyActive || !todo.completed) {
|
||||
this.toggleOne(todo);
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
4
examples/basic-crud-application/vue-client/vue.config.js
Normal file
4
examples/basic-crud-application/vue-client/vue.config.js
Normal file
@@ -0,0 +1,4 @@
|
||||
const { defineConfig } = require('@vue/cli-service')
|
||||
module.exports = defineConfig({
|
||||
transpileDependencies: true
|
||||
})
|
||||
6309
examples/basic-crud-application/vue-client/yarn.lock
Normal file
6309
examples/basic-crud-application/vue-client/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
18
examples/basic-websocket-client/README.md
Normal file
18
examples/basic-websocket-client/README.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# Basic Socket.IO client
|
||||
|
||||
Please check the associated guide: https://socket.io/how-to/build-a-basic-client
|
||||
|
||||
Content:
|
||||
|
||||
```
|
||||
├── bundle
|
||||
│ └── socket.io.min.js
|
||||
├── src
|
||||
│ └── index.js
|
||||
├── test
|
||||
│ └── index.js
|
||||
├── check-bundle-size.js
|
||||
├── package.json
|
||||
├── README.md
|
||||
└── rollup.config.js
|
||||
```
|
||||
1
examples/basic-websocket-client/bundle/socket.io.min.js
vendored
Normal file
1
examples/basic-websocket-client/bundle/socket.io.min.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
class e{#e=new Map;on(e,t){let s=this.#e.get(e);s||this.#e.set(e,s=[]),s.push(t)}emit(e,...t){const s=this.#e.get(e);if(s)for(const e of s)e.apply(null,t)}}const t="0",s="1",n="2",i="3",o="4",r={CONNECT:0,DISCONNECT:1,EVENT:2};function c(){}class a extends e{id;connected=!1;#t;#s;#n;#i;#o;#r=[];#c;#a=!0;constructor(e,t){super(),this.#t=e,this.#s=Object.assign({path:"/socket.io/",reconnectionDelay:2e3},t),this.#h()}#h(){this.#n=new WebSocket(this.#u()),this.#n.onmessage=({data:e})=>this.#p(e),this.#n.onerror=c,this.#n.onclose=()=>this.#l("transport close")}#u(){return`${this.#t.replace(/^http/,"ws")}${this.#s.path}?EIO=4&transport=websocket`}#p(e){if("string"==typeof e)switch(e[0]){case t:this.#d(e);break;case s:this.#l("transport close");break;case n:this.#T(),this.#m(i);break;case o:let c;try{c=function(e){let t=1;const s={type:parseInt(e.charAt(t++),10)};e.charAt(t)&&(s.data=JSON.parse(e.substring(t)));if(!function(e){switch(e.type){case r.CONNECT:return"object"==typeof e.data;case r.DISCONNECT:return void 0===e.data;case r.EVENT:{const t=e.data;return Array.isArray(t)&&t.length>0&&"string"==typeof t[0]}default:return!1}}(s))throw new Error("invalid format");return s}(e)}catch(e){return this.#l("parse error")}this.#f(c);break;default:this.#l("parse error")}}#d(e){let t;try{t=JSON.parse(e.substring(1))}catch(e){return this.#l("parse error")}this.#o=t.pingInterval+t.pingTimeout,this.#T(),this.#C()}#f(e){switch(e.type){case r.CONNECT:this.#g(e);break;case r.DISCONNECT:this.#a=!1,this.#l("io server disconnect");break;case r.EVENT:super.emit.apply(this,e.data);break;default:this.#l("parse error")}}#g(e){this.id=e.data.sid,this.connected=!0,this.#r.forEach((e=>this.#y(e))),this.#r.slice(0),super.emit("connect")}#l(e){this.#n&&(this.#n.onclose=c,this.#n.close()),clearTimeout(this.#i),clearTimeout(this.#c),this.connected?(this.connected=!1,this.id=void 0,super.emit("disconnect",e)):super.emit("connect_error",e),this.#a&&(this.#c=setTimeout((()=>this.#h()),this.#s.reconnectionDelay))}#T(){clearTimeout(this.#i),this.#i=setTimeout((()=>{this.#l("ping timeout")}),this.#o)}#m(e){this.#n.readyState===WebSocket.OPEN&&this.#n.send(e)}#y(e){this.#m(o+function(e){let t=""+e.type;e.data&&(t+=JSON.stringify(e.data));return t}(e))}#C(){this.#y({type:r.CONNECT})}emit(...e){const t={type:r.EVENT,data:e};this.connected?this.#y(t):this.#r.push(t)}disconnect(){this.#a=!1,this.#l("io client disconnect")}}function h(e,t){return"string"!=typeof e&&(t=e,e=location.origin),new a(e,t)}export{h as io};
|
||||
17
examples/basic-websocket-client/check-bundle-size.js
Normal file
17
examples/basic-websocket-client/check-bundle-size.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import { rollup } from "rollup";
|
||||
import terser from "@rollup/plugin-terser";
|
||||
import { brotliCompressSync } from "node:zlib";
|
||||
|
||||
const rollupBuild = await rollup({
|
||||
input: "./src/index.js"
|
||||
});
|
||||
|
||||
const rollupOutput = await rollupBuild.generate({
|
||||
format: "esm",
|
||||
plugins: [terser()],
|
||||
});
|
||||
|
||||
const bundleAsString = rollupOutput.output[0].code;
|
||||
const brotliedBundle = brotliCompressSync(Buffer.from(bundleAsString));
|
||||
|
||||
console.log(`Bundle size: ${brotliedBundle.length} B`);
|
||||
18
examples/basic-websocket-client/package.json
Normal file
18
examples/basic-websocket-client/package.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-terser": "^0.4.0",
|
||||
"chai": "^4.3.7",
|
||||
"mocha": "^10.2.0",
|
||||
"prettier": "^2.8.4",
|
||||
"rollup": "^3.20.2",
|
||||
"socket.io": "^4.6.1",
|
||||
"ws": "^8.13.0"
|
||||
},
|
||||
"scripts": {
|
||||
"bundle": "rollup -c",
|
||||
"check-bundle-size": "node check-bundle-size.js",
|
||||
"format": "prettier -w src/ test/",
|
||||
"test": "mocha"
|
||||
}
|
||||
}
|
||||
10
examples/basic-websocket-client/rollup.config.js
Normal file
10
examples/basic-websocket-client/rollup.config.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import terser from "@rollup/plugin-terser";
|
||||
|
||||
export default {
|
||||
input: "./src/index.js",
|
||||
output: {
|
||||
file: "./bundle/socket.io.min.js",
|
||||
format: "esm",
|
||||
plugins: [terser()],
|
||||
}
|
||||
};
|
||||
273
examples/basic-websocket-client/src/index.js
Normal file
273
examples/basic-websocket-client/src/index.js
Normal file
@@ -0,0 +1,273 @@
|
||||
class EventEmitter {
|
||||
#listeners = new Map();
|
||||
|
||||
on(event, listener) {
|
||||
let listeners = this.#listeners.get(event);
|
||||
if (!listeners) {
|
||||
this.#listeners.set(event, (listeners = []));
|
||||
}
|
||||
listeners.push(listener);
|
||||
}
|
||||
|
||||
emit(event, ...args) {
|
||||
const listeners = this.#listeners.get(event);
|
||||
if (listeners) {
|
||||
for (const listener of listeners) {
|
||||
listener.apply(null, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const EIOPacketType = {
|
||||
OPEN: "0",
|
||||
CLOSE: "1",
|
||||
PING: "2",
|
||||
PONG: "3",
|
||||
MESSAGE: "4",
|
||||
};
|
||||
|
||||
const SIOPacketType = {
|
||||
CONNECT: 0,
|
||||
DISCONNECT: 1,
|
||||
EVENT: 2,
|
||||
};
|
||||
|
||||
function noop() {}
|
||||
|
||||
class Socket extends EventEmitter {
|
||||
id;
|
||||
connected = false;
|
||||
|
||||
#uri;
|
||||
#opts;
|
||||
#ws;
|
||||
#pingTimeoutTimer;
|
||||
#pingTimeoutDelay;
|
||||
#sendBuffer = [];
|
||||
#reconnectTimer;
|
||||
#shouldReconnect = true;
|
||||
|
||||
constructor(uri, opts) {
|
||||
super();
|
||||
this.#uri = uri;
|
||||
this.#opts = Object.assign(
|
||||
{
|
||||
path: "/socket.io/",
|
||||
reconnectionDelay: 2000,
|
||||
},
|
||||
opts
|
||||
);
|
||||
this.#open();
|
||||
}
|
||||
|
||||
#open() {
|
||||
this.#ws = new WebSocket(this.#createUrl());
|
||||
this.#ws.onmessage = ({ data }) => this.#onMessage(data);
|
||||
// dummy handler for Node.js
|
||||
this.#ws.onerror = noop;
|
||||
this.#ws.onclose = () => this.#onClose("transport close");
|
||||
}
|
||||
|
||||
#createUrl() {
|
||||
const uri = this.#uri.replace(/^http/, "ws");
|
||||
const queryParams = "?EIO=4&transport=websocket";
|
||||
return `${uri}${this.#opts.path}${queryParams}`;
|
||||
}
|
||||
|
||||
#onMessage(data) {
|
||||
if (typeof data !== "string") {
|
||||
// TODO handle binary payloads
|
||||
return;
|
||||
}
|
||||
|
||||
switch (data[0]) {
|
||||
case EIOPacketType.OPEN:
|
||||
this.#onOpen(data);
|
||||
break;
|
||||
|
||||
case EIOPacketType.CLOSE:
|
||||
this.#onClose("transport close");
|
||||
break;
|
||||
|
||||
case EIOPacketType.PING:
|
||||
this.#resetPingTimeout();
|
||||
this.#send(EIOPacketType.PONG);
|
||||
break;
|
||||
|
||||
case EIOPacketType.MESSAGE:
|
||||
let packet;
|
||||
try {
|
||||
packet = decode(data);
|
||||
} catch (e) {
|
||||
return this.#onClose("parse error");
|
||||
}
|
||||
this.#onPacket(packet);
|
||||
break;
|
||||
|
||||
default:
|
||||
this.#onClose("parse error");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#onOpen(data) {
|
||||
let handshake;
|
||||
try {
|
||||
handshake = JSON.parse(data.substring(1));
|
||||
} catch (e) {
|
||||
return this.#onClose("parse error");
|
||||
}
|
||||
this.#pingTimeoutDelay = handshake.pingInterval + handshake.pingTimeout;
|
||||
this.#resetPingTimeout();
|
||||
this.#doConnect();
|
||||
}
|
||||
|
||||
#onPacket(packet) {
|
||||
switch (packet.type) {
|
||||
case SIOPacketType.CONNECT:
|
||||
this.#onConnect(packet);
|
||||
break;
|
||||
|
||||
case SIOPacketType.DISCONNECT:
|
||||
this.#shouldReconnect = false;
|
||||
this.#onClose("io server disconnect");
|
||||
break;
|
||||
|
||||
case SIOPacketType.EVENT:
|
||||
super.emit.apply(this, packet.data);
|
||||
break;
|
||||
|
||||
default:
|
||||
this.#onClose("parse error");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#onConnect(packet) {
|
||||
this.id = packet.data.sid;
|
||||
this.connected = true;
|
||||
|
||||
this.#sendBuffer.forEach((packet) => this.#sendPacket(packet));
|
||||
this.#sendBuffer.slice(0);
|
||||
|
||||
super.emit("connect");
|
||||
}
|
||||
|
||||
#onClose(reason) {
|
||||
if (this.#ws) {
|
||||
this.#ws.onclose = noop;
|
||||
this.#ws.close();
|
||||
}
|
||||
|
||||
clearTimeout(this.#pingTimeoutTimer);
|
||||
clearTimeout(this.#reconnectTimer);
|
||||
|
||||
if (this.connected) {
|
||||
this.connected = false;
|
||||
this.id = undefined;
|
||||
super.emit("disconnect", reason);
|
||||
} else {
|
||||
super.emit("connect_error", reason);
|
||||
}
|
||||
|
||||
if (this.#shouldReconnect) {
|
||||
this.#reconnectTimer = setTimeout(
|
||||
() => this.#open(),
|
||||
this.#opts.reconnectionDelay
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#resetPingTimeout() {
|
||||
clearTimeout(this.#pingTimeoutTimer);
|
||||
this.#pingTimeoutTimer = setTimeout(() => {
|
||||
this.#onClose("ping timeout");
|
||||
}, this.#pingTimeoutDelay);
|
||||
}
|
||||
|
||||
#send(data) {
|
||||
if (this.#ws.readyState === WebSocket.OPEN) {
|
||||
this.#ws.send(data);
|
||||
}
|
||||
}
|
||||
|
||||
#sendPacket(packet) {
|
||||
this.#send(EIOPacketType.MESSAGE + encode(packet));
|
||||
}
|
||||
|
||||
#doConnect() {
|
||||
this.#sendPacket({ type: SIOPacketType.CONNECT });
|
||||
}
|
||||
|
||||
emit(...args) {
|
||||
const packet = {
|
||||
type: SIOPacketType.EVENT,
|
||||
data: args,
|
||||
};
|
||||
|
||||
if (this.connected) {
|
||||
this.#sendPacket(packet);
|
||||
} else {
|
||||
this.#sendBuffer.push(packet);
|
||||
}
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.#shouldReconnect = false;
|
||||
this.#onClose("io client disconnect");
|
||||
}
|
||||
}
|
||||
|
||||
function encode(packet) {
|
||||
let output = "" + packet.type;
|
||||
|
||||
if (packet.data) {
|
||||
output += JSON.stringify(packet.data);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
function decode(data) {
|
||||
let i = 1; // skip "4" prefix
|
||||
|
||||
const packet = {
|
||||
type: parseInt(data.charAt(i++), 10),
|
||||
};
|
||||
|
||||
if (data.charAt(i)) {
|
||||
packet.data = JSON.parse(data.substring(i));
|
||||
}
|
||||
|
||||
if (!isPacketValid(packet)) {
|
||||
throw new Error("invalid format");
|
||||
}
|
||||
|
||||
return packet;
|
||||
}
|
||||
|
||||
function isPacketValid(packet) {
|
||||
switch (packet.type) {
|
||||
case SIOPacketType.CONNECT:
|
||||
return typeof packet.data === "object";
|
||||
case SIOPacketType.DISCONNECT:
|
||||
return packet.data === undefined;
|
||||
case SIOPacketType.EVENT: {
|
||||
const args = packet.data;
|
||||
return (
|
||||
Array.isArray(args) && args.length > 0 && typeof args[0] === "string"
|
||||
);
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function io(uri, opts) {
|
||||
if (typeof uri !== "string") {
|
||||
opts = uri;
|
||||
uri = location.origin;
|
||||
}
|
||||
return new Socket(uri, opts);
|
||||
}
|
||||
162
examples/basic-websocket-client/test/index.js
Normal file
162
examples/basic-websocket-client/test/index.js
Normal file
@@ -0,0 +1,162 @@
|
||||
import { createServer } from "node:http";
|
||||
import { io as ioc } from "../src/index.js";
|
||||
import { WebSocket } from "ws";
|
||||
import { Server } from "socket.io";
|
||||
import { expect } from "chai";
|
||||
|
||||
// @ts-ignore for Node.js
|
||||
globalThis.WebSocket = WebSocket;
|
||||
|
||||
function waitFor(emitter, eventName) {
|
||||
return new Promise((resolve) => {
|
||||
emitter.on(eventName, resolve);
|
||||
});
|
||||
}
|
||||
|
||||
function sleep(delay) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, delay);
|
||||
});
|
||||
}
|
||||
|
||||
describe("basic client", () => {
|
||||
let io, port, socket;
|
||||
|
||||
beforeEach(() => {
|
||||
const httpServer = createServer();
|
||||
io = new Server(httpServer);
|
||||
|
||||
httpServer.listen(0);
|
||||
port = httpServer.address().port;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
io.close();
|
||||
socket.disconnect();
|
||||
});
|
||||
|
||||
it("should connect", async () => {
|
||||
socket = ioc(`ws://localhost:${port}`);
|
||||
|
||||
await waitFor(socket, "connect");
|
||||
|
||||
expect(socket.connected).to.eql(true);
|
||||
expect(socket.id).to.be.a("string");
|
||||
});
|
||||
|
||||
it("should connect with 'http://' scheme", async () => {
|
||||
socket = ioc(`http://localhost:${port}`);
|
||||
|
||||
await waitFor(socket, "connect");
|
||||
});
|
||||
|
||||
it("should connect with URL inferred from 'window.location'", async () => {
|
||||
globalThis.location = {
|
||||
origin: `http://localhost:${port}`,
|
||||
};
|
||||
socket = ioc();
|
||||
|
||||
await waitFor(socket, "connect");
|
||||
});
|
||||
|
||||
it("should fail to connect to an invalid URL", async () => {
|
||||
socket = ioc(`http://localhost:4321`);
|
||||
|
||||
await waitFor(socket, "connect_error");
|
||||
});
|
||||
|
||||
it("should receive an event", async () => {
|
||||
io.on("connection", (socket) => {
|
||||
socket.emit("foo", 123);
|
||||
});
|
||||
|
||||
socket = ioc(`ws://localhost:${port}`);
|
||||
|
||||
const value = await waitFor(socket, "foo");
|
||||
|
||||
expect(value).to.eql(123);
|
||||
});
|
||||
|
||||
it("should send an event (not buffered)", async () => {
|
||||
socket = ioc(`ws://localhost:${port}`);
|
||||
|
||||
const [serverSocket] = await Promise.all([
|
||||
waitFor(io, "connection"),
|
||||
waitFor(socket, "connect"),
|
||||
]);
|
||||
|
||||
socket.emit("foo", 456);
|
||||
|
||||
const value = await waitFor(serverSocket, "foo");
|
||||
|
||||
expect(value).to.eql(456);
|
||||
});
|
||||
|
||||
it("should send an event (buffered)", async () => {
|
||||
socket = ioc(`ws://localhost:${port}`);
|
||||
|
||||
socket.emit("foo", 789);
|
||||
|
||||
const [serverSocket] = await Promise.all([
|
||||
waitFor(io, "connection"),
|
||||
waitFor(socket, "connect"),
|
||||
]);
|
||||
|
||||
const value = await waitFor(serverSocket, "foo");
|
||||
|
||||
expect(value).to.eql(789);
|
||||
});
|
||||
|
||||
it("should reconnect", async () => {
|
||||
socket = ioc(`ws://localhost:${port}`, {
|
||||
reconnectionDelay: 50,
|
||||
});
|
||||
|
||||
await waitFor(socket, "connect");
|
||||
|
||||
io.close();
|
||||
|
||||
await waitFor(socket, "disconnect");
|
||||
|
||||
io.listen(port);
|
||||
|
||||
await waitFor(socket, "connect");
|
||||
});
|
||||
|
||||
it("should respond to PING packets", async () => {
|
||||
io.engine.opts.pingInterval = 50;
|
||||
io.engine.opts.pingTimeout = 20;
|
||||
|
||||
socket = ioc(`ws://localhost:${port}`);
|
||||
|
||||
await waitFor(socket, "connect");
|
||||
|
||||
await sleep(500);
|
||||
|
||||
expect(socket.connected).to.eql(true);
|
||||
});
|
||||
|
||||
it("should disconnect (client side)", async () => {
|
||||
socket = ioc(`ws://localhost:${port}`);
|
||||
|
||||
await waitFor(socket, "connect");
|
||||
|
||||
socket.disconnect();
|
||||
|
||||
expect(socket.connected).to.eql(false);
|
||||
expect(socket.id).to.eql(undefined);
|
||||
});
|
||||
|
||||
it("should disconnect (server side)", async () => {
|
||||
socket = ioc(`ws://localhost:${port}`);
|
||||
|
||||
const [serverSocket] = await Promise.all([
|
||||
waitFor(io, "connection"),
|
||||
waitFor(socket, "connect"),
|
||||
]);
|
||||
|
||||
serverSocket.disconnect();
|
||||
|
||||
await waitFor(socket, "disconnect");
|
||||
});
|
||||
});
|
||||
@@ -1,51 +1,53 @@
|
||||
services:
|
||||
haproxy:
|
||||
image: haproxy:1.7-alpine
|
||||
volumes:
|
||||
- ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
|
||||
links:
|
||||
- server-john
|
||||
- server-paul
|
||||
- server-george
|
||||
- server-ringo
|
||||
ports:
|
||||
- "3000:80"
|
||||
|
||||
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-john:
|
||||
build: ./server
|
||||
links:
|
||||
- redis
|
||||
expose:
|
||||
- "3000"
|
||||
environment:
|
||||
- NAME=John
|
||||
server-paul:
|
||||
build: ./server
|
||||
links:
|
||||
- redis
|
||||
expose:
|
||||
- "3000"
|
||||
environment:
|
||||
- NAME=Paul
|
||||
|
||||
server-paul:
|
||||
build: ./server
|
||||
links:
|
||||
- redis
|
||||
expose:
|
||||
- "3000"
|
||||
environment:
|
||||
- NAME=Paul
|
||||
server-george:
|
||||
build: ./server
|
||||
links:
|
||||
- redis
|
||||
expose:
|
||||
- "3000"
|
||||
environment:
|
||||
- NAME=George
|
||||
|
||||
server-george:
|
||||
build: ./server
|
||||
links:
|
||||
- redis
|
||||
expose:
|
||||
- "3000"
|
||||
environment:
|
||||
- NAME=George
|
||||
server-ringo:
|
||||
build: ./server
|
||||
links:
|
||||
- redis
|
||||
expose:
|
||||
- "3000"
|
||||
environment:
|
||||
- NAME=Ringo
|
||||
|
||||
server-ringo:
|
||||
build: ./server
|
||||
links:
|
||||
- redis
|
||||
expose:
|
||||
- "3000"
|
||||
environment:
|
||||
- NAME=Ringo
|
||||
|
||||
redis:
|
||||
image: redis:alpine
|
||||
expose:
|
||||
- "6379"
|
||||
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,51 +1,53 @@
|
||||
services:
|
||||
httpd:
|
||||
image: httpd:2.4-alpine
|
||||
volumes:
|
||||
- ./httpd.conf:/usr/local/apache2/conf/httpd.conf:ro
|
||||
links:
|
||||
- server-john
|
||||
- server-paul
|
||||
- server-george
|
||||
- server-ringo
|
||||
ports:
|
||||
- "3000:80"
|
||||
|
||||
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-john:
|
||||
build: ./server
|
||||
links:
|
||||
- redis
|
||||
expose:
|
||||
- "3000"
|
||||
environment:
|
||||
- NAME=John
|
||||
server-paul:
|
||||
build: ./server
|
||||
links:
|
||||
- redis
|
||||
expose:
|
||||
- "3000"
|
||||
environment:
|
||||
- NAME=Paul
|
||||
|
||||
server-paul:
|
||||
build: ./server
|
||||
links:
|
||||
- redis
|
||||
expose:
|
||||
- "3000"
|
||||
environment:
|
||||
- NAME=Paul
|
||||
server-george:
|
||||
build: ./server
|
||||
links:
|
||||
- redis
|
||||
expose:
|
||||
- "3000"
|
||||
environment:
|
||||
- NAME=George
|
||||
|
||||
server-george:
|
||||
build: ./server
|
||||
links:
|
||||
- redis
|
||||
expose:
|
||||
- "3000"
|
||||
environment:
|
||||
- NAME=George
|
||||
server-ringo:
|
||||
build: ./server
|
||||
links:
|
||||
- redis
|
||||
expose:
|
||||
- "3000"
|
||||
environment:
|
||||
- NAME=Ringo
|
||||
|
||||
server-ringo:
|
||||
build: ./server
|
||||
links:
|
||||
- redis
|
||||
expose:
|
||||
- "3000"
|
||||
environment:
|
||||
- NAME=Ringo
|
||||
|
||||
redis:
|
||||
image: redis:alpine
|
||||
expose:
|
||||
- "6379"
|
||||
redis:
|
||||
image: redis:6
|
||||
expose:
|
||||
- "6379"
|
||||
|
||||
@@ -51,4 +51,5 @@ RewriteRule /(.*) balancer://nodes_ws/$1 [P,L]
|
||||
RewriteCond %{HTTP:Upgrade} !=websocket [NC]
|
||||
RewriteRule /(.*) balancer://nodes_polling/$1 [P,L]
|
||||
|
||||
ProxyTimeout 3
|
||||
# must be bigger than pingInterval (25s by default) + pingTimeout (20s by default)
|
||||
ProxyTimeout 60
|
||||
@@ -1,2 +0,0 @@
|
||||
FROM httpd:2.4-alpine
|
||||
COPY ./httpd.conf /usr/local/apache2/conf/httpd.conf
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM mhart/alpine-node:6
|
||||
FROM node:14-alpine
|
||||
|
||||
# Create app directory
|
||||
RUN mkdir -p /usr/src/app
|
||||
|
||||
@@ -3,18 +3,27 @@ services:
|
||||
image: nginx:alpine
|
||||
volumes:
|
||||
- ./nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
links:
|
||||
- server-john
|
||||
- server-paul
|
||||
- server-george
|
||||
- server-ringo
|
||||
ports:
|
||||
- "3000:80"
|
||||
- "3000:80"
|
||||
|
||||
server-john:
|
||||
build: ./server
|
||||
links:
|
||||
- redis
|
||||
expose:
|
||||
- "3000"
|
||||
- "3000"
|
||||
environment:
|
||||
- NAME=John
|
||||
|
||||
server-paul:
|
||||
build: ./server
|
||||
links:
|
||||
- redis
|
||||
expose:
|
||||
- "3000"
|
||||
environment:
|
||||
@@ -22,6 +31,8 @@ services:
|
||||
|
||||
server-george:
|
||||
build: ./server
|
||||
links:
|
||||
- redis
|
||||
expose:
|
||||
- "3000"
|
||||
environment:
|
||||
@@ -29,6 +40,8 @@ services:
|
||||
|
||||
server-ringo:
|
||||
build: ./server
|
||||
links:
|
||||
- redis
|
||||
expose:
|
||||
- "3000"
|
||||
environment:
|
||||
@@ -36,8 +49,10 @@ services:
|
||||
|
||||
client:
|
||||
build: ./client
|
||||
links:
|
||||
- nginx
|
||||
|
||||
redis:
|
||||
image: redis:alpine
|
||||
image: redis:6
|
||||
expose:
|
||||
- "6379"
|
||||
|
||||
25
examples/connection-state-recovery-example/README.md
Normal file
25
examples/connection-state-recovery-example/README.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Example with connection state recovery
|
||||
|
||||
This example shows how to use the [Connection state recovery feature](https://socket.io/docs/v4/connection-state-recovery).
|
||||
|
||||

|
||||
|
||||
## How to use
|
||||
|
||||
```shell
|
||||
# choose your module syntax (either ES modules or CommonJS)
|
||||
$ cd esm/
|
||||
|
||||
# install the dependencies
|
||||
$ npm i
|
||||
|
||||
# start the server
|
||||
$ node index.js
|
||||
```
|
||||
|
||||
And point your browser to `http://localhost:3000`.
|
||||
|
||||
You can also run this example directly in your browser on:
|
||||
|
||||
- [CodeSandbox](https://codesandbox.io/p/sandbox/github/socketio/socket.io/tree/main/examples/connection-state-recovery-example/esm?file=index.js)
|
||||
- [StackBlitz](https://stackblitz.com/github/socketio/socket.io/tree/main/examples/connection-state-recovery-example/esm?file=index.js)
|
||||
BIN
examples/connection-state-recovery-example/assets/csr.gif
Normal file
BIN
examples/connection-state-recovery-example/assets/csr.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 103 KiB |
@@ -0,0 +1 @@
|
||||
FROM node:20-bullseye
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
// These tasks will run in order when initializing your CodeSandbox project.
|
||||
"setupTasks": [
|
||||
{
|
||||
"name": "Install Dependencies",
|
||||
"command": "npm install"
|
||||
}
|
||||
],
|
||||
|
||||
// These tasks can be run from CodeSandbox. Running one will open a log in the app.
|
||||
"tasks": {
|
||||
"npm start": {
|
||||
"name": "npm start",
|
||||
"command": "npm start",
|
||||
"runAtStart": true
|
||||
}
|
||||
}
|
||||
}
|
||||
49
examples/connection-state-recovery-example/cjs/index.html
Normal file
49
examples/connection-state-recovery-example/cjs/index.html
Normal file
@@ -0,0 +1,49 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Connection state recovery | Socket.IO</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Status: <span id="connectionStatus">disconnected</span></p>
|
||||
<p>Recovered? <span id="recoveryStatus">-</span></p>
|
||||
|
||||
<p>Latest messages:</p>
|
||||
<ul id="messages"></ul>
|
||||
|
||||
<script src="/socket.io/socket.io.js"></script>
|
||||
<script>
|
||||
const $connectionStatus = document.getElementById("connectionStatus");
|
||||
const $recoveryStatus = document.getElementById("recoveryStatus");
|
||||
const $messages = document.getElementById("messages");
|
||||
|
||||
const socket = io({
|
||||
reconnectionDelay: 5000 // 1000 by default
|
||||
});
|
||||
|
||||
socket.on("connect", () => {
|
||||
$connectionStatus.innerText = "connected";
|
||||
$recoveryStatus.innerText = "" + socket.recovered;
|
||||
|
||||
setTimeout(() => {
|
||||
// close the low-level connection and trigger a reconnection
|
||||
socket.io.engine.close();
|
||||
}, Math.random() * 5000 + 1000);
|
||||
});
|
||||
|
||||
socket.on("disconnect", () => {
|
||||
$connectionStatus.innerText = "disconnected";
|
||||
$recoveryStatus.innerText = "-"
|
||||
});
|
||||
|
||||
socket.on("ping", (value) => {
|
||||
const item = document.createElement("li");
|
||||
item.textContent = value;
|
||||
$messages.prepend(item);
|
||||
if ($messages.childElementCount > 10) {
|
||||
$messages.removeChild($messages.lastChild);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
53
examples/connection-state-recovery-example/cjs/index.js
Normal file
53
examples/connection-state-recovery-example/cjs/index.js
Normal file
@@ -0,0 +1,53 @@
|
||||
const { readFile } = require("node:fs/promises");
|
||||
const { createServer } = require("node:http");
|
||||
const { Server } = require("socket.io");
|
||||
|
||||
const httpServer = createServer(async (req, res) => {
|
||||
if (req.url !== "/") {
|
||||
res.writeHead(404);
|
||||
res.end("Not found");
|
||||
return;
|
||||
}
|
||||
// reload the file every time
|
||||
const content = await readFile("index.html");
|
||||
const length = Buffer.byteLength(content);
|
||||
|
||||
res.writeHead(200, {
|
||||
"Content-Type": "text/html",
|
||||
"Content-Length": length,
|
||||
});
|
||||
res.end(content);
|
||||
});
|
||||
|
||||
const io = new Server(httpServer, {
|
||||
connectionStateRecovery: {
|
||||
// the backup duration of the sessions and the packets
|
||||
maxDisconnectionDuration: 2 * 60 * 1000,
|
||||
// whether to skip middlewares upon successful recovery
|
||||
skipMiddlewares: true,
|
||||
},
|
||||
});
|
||||
|
||||
io.on("connection", (socket) => {
|
||||
console.log(`connect ${socket.id}`);
|
||||
|
||||
if (socket.recovered) {
|
||||
console.log("recovered!");
|
||||
console.log("socket.rooms:", socket.rooms);
|
||||
console.log("socket.data:", socket.data);
|
||||
} else {
|
||||
console.log("new connection");
|
||||
socket.join("sample room");
|
||||
socket.data.foo = "bar";
|
||||
}
|
||||
|
||||
socket.on("disconnect", (reason) => {
|
||||
console.log(`disconnect ${socket.id} due to ${reason}`);
|
||||
});
|
||||
});
|
||||
|
||||
setInterval(() => {
|
||||
io.emit("ping", new Date().toISOString());
|
||||
}, 1000);
|
||||
|
||||
httpServer.listen(3000);
|
||||
13
examples/connection-state-recovery-example/cjs/package.json
Normal file
13
examples/connection-state-recovery-example/cjs/package.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "connection-state-recovery-example",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "commonjs",
|
||||
"description": "Example with connection state recovery",
|
||||
"scripts": {
|
||||
"start": "node index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"socket.io": "^4.7.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
FROM node:20-bullseye
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
// These tasks will run in order when initializing your CodeSandbox project.
|
||||
"setupTasks": [
|
||||
{
|
||||
"name": "Install Dependencies",
|
||||
"command": "npm install"
|
||||
}
|
||||
],
|
||||
|
||||
// These tasks can be run from CodeSandbox. Running one will open a log in the app.
|
||||
"tasks": {
|
||||
"npm start": {
|
||||
"name": "npm start",
|
||||
"command": "npm start",
|
||||
"runAtStart": true
|
||||
}
|
||||
}
|
||||
}
|
||||
49
examples/connection-state-recovery-example/esm/index.html
Normal file
49
examples/connection-state-recovery-example/esm/index.html
Normal file
@@ -0,0 +1,49 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Connection state recovery | Socket.IO</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Status: <span id="connectionStatus">disconnected</span></p>
|
||||
<p>Recovered? <span id="recoveryStatus">-</span></p>
|
||||
|
||||
<p>Latest messages:</p>
|
||||
<ul id="messages"></ul>
|
||||
|
||||
<script src="/socket.io/socket.io.js"></script>
|
||||
<script>
|
||||
const $connectionStatus = document.getElementById("connectionStatus");
|
||||
const $recoveryStatus = document.getElementById("recoveryStatus");
|
||||
const $messages = document.getElementById("messages");
|
||||
|
||||
const socket = io({
|
||||
reconnectionDelay: 5000 // 1000 by default
|
||||
});
|
||||
|
||||
socket.on("connect", () => {
|
||||
$connectionStatus.innerText = "connected";
|
||||
$recoveryStatus.innerText = "" + socket.recovered;
|
||||
|
||||
setTimeout(() => {
|
||||
// close the low-level connection and trigger a reconnection
|
||||
socket.io.engine.close();
|
||||
}, Math.random() * 5000 + 1000);
|
||||
});
|
||||
|
||||
socket.on("disconnect", () => {
|
||||
$connectionStatus.innerText = "disconnected";
|
||||
$recoveryStatus.innerText = "-"
|
||||
});
|
||||
|
||||
socket.on("ping", (value) => {
|
||||
const item = document.createElement("li");
|
||||
item.textContent = value;
|
||||
$messages.prepend(item);
|
||||
if ($messages.childElementCount > 10) {
|
||||
$messages.removeChild($messages.lastChild);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
53
examples/connection-state-recovery-example/esm/index.js
Normal file
53
examples/connection-state-recovery-example/esm/index.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { createServer } from "node:http";
|
||||
import { Server } from "socket.io";
|
||||
|
||||
const httpServer = createServer(async (req, res) => {
|
||||
if (req.url !== "/") {
|
||||
res.writeHead(404);
|
||||
res.end("Not found");
|
||||
return;
|
||||
}
|
||||
// reload the file every time
|
||||
const content = await readFile("index.html");
|
||||
const length = Buffer.byteLength(content);
|
||||
|
||||
res.writeHead(200, {
|
||||
"Content-Type": "text/html",
|
||||
"Content-Length": length,
|
||||
});
|
||||
res.end(content);
|
||||
});
|
||||
|
||||
const io = new Server(httpServer, {
|
||||
connectionStateRecovery: {
|
||||
// the backup duration of the sessions and the packets
|
||||
maxDisconnectionDuration: 2 * 60 * 1000,
|
||||
// whether to skip middlewares upon successful recovery
|
||||
skipMiddlewares: true,
|
||||
},
|
||||
});
|
||||
|
||||
io.on("connection", (socket) => {
|
||||
console.log(`connect ${socket.id}`);
|
||||
|
||||
if (socket.recovered) {
|
||||
console.log("recovered!");
|
||||
console.log("socket.rooms:", socket.rooms);
|
||||
console.log("socket.data:", socket.data);
|
||||
} else {
|
||||
console.log("new connection");
|
||||
socket.join("sample room");
|
||||
socket.data.foo = "bar";
|
||||
}
|
||||
|
||||
socket.on("disconnect", (reason) => {
|
||||
console.log(`disconnect ${socket.id} due to ${reason}`);
|
||||
});
|
||||
});
|
||||
|
||||
setInterval(() => {
|
||||
io.emit("ping", new Date().toISOString());
|
||||
}, 1000);
|
||||
|
||||
httpServer.listen(3000);
|
||||
13
examples/connection-state-recovery-example/esm/package.json
Normal file
13
examples/connection-state-recovery-example/esm/package.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "connection-state-recovery-example",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Example with connection state recovery",
|
||||
"scripts": {
|
||||
"start": "node index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"socket.io": "^4.7.2"
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user