Compare commits

...

46 Commits
4.6.1 ... 4.7.4

Author SHA1 Message Date
Damien Arrachequesne
6ab2509d52 chore(release): 4.7.4
Diff: https://github.com/socketio/socket.io/compare/4.7.3...4.7.4
2024-01-12 11:09:14 +01:00
Zachary Soare
d9fb2f64b6 chore(tests): add a test for noArgs in a namespace 2024-01-08 06:38:57 +01:00
Zachary Soare
2c0a81cd87 chore(tests): fix issues due to client#id type change 2024-01-08 06:38:57 +01:00
Zachary Soare
f8d2644921 chore(tests): add type defs for expectjs and fix invalid expectations 2024-01-08 06:38:57 +01:00
Zachary Soare
04640d68cf chore(tests): indicate a future ts error with version 2024-01-08 06:38:57 +01:00
Zachary Soare
cb6d2e02aa fix(typings): calling io.emit with no arguments incorrectly errored
Refs: #4914
2024-01-08 06:38:57 +01:00
Damien Arrachequesne
80b2c34478 chore: bump socket.io-client version 2024-01-03 21:37:25 +01:00
Damien Arrachequesne
0d893196f8 chore(release): 4.7.3
Diff: https://github.com/socketio/socket.io/compare/4.7.2...4.7.3
2024-01-03 21:33:29 +01:00
BCCSTeam
df8e70f798 fix: return the first response when broadcasting to a single socket (#4878) 2024-01-02 17:43:10 +01:00
Xì Gà
8c9ebc30e5 fix(typings): allow to bind to a non-secure Http2Server (#4853) 2023-11-22 17:48:59 +01:00
Damien Arrachequesne
efb5c21e85 docs(examples): add Vue client with CRUD example 2023-11-22 10:12:17 +01:00
Damien Arrachequesne
3848280125 docs(examples): upgrade basic-crud-application to Angular v17
Related: https://github.com/socketio/socket.io/issues/4875
2023-11-21 14:15:50 +01:00
Damien Arrachequesne
9a2a83fdd4 refactor: cleanup after merge 2023-10-11 10:45:59 +02:00
Zachary Haber
f6ef267b03 refactor(typings): improve emit types (#4817)
This commit fixes several issues with emit types:

- calling `emit()` without calling `timeout()` first is now only available for events without acknowledgement
- calling `emit()` after calling `timeout()` is now only available for events with an acknowledgement
- calling `emitWithAck()` is now only available for events with an acknowledgement
- `timeout()` must be called before calling `emitWithAck()`
2023-10-11 10:37:13 +02:00
Maxime Kjaer
1cdf36bfea test: build examples in the CI (#3856) 2023-10-10 20:02:52 +02:00
Toha
bbf1fdc7a6 docs: add Elephant.IO as PHP client library (#4779) 2023-10-10 17:32:19 +02:00
Damien Arrachequesne
b4dc83eb9b docs(examples): add codesandbox configuration 2023-09-20 12:57:37 +02:00
Damien Arrachequesne
ccbb4c0773 docs: add example with connection state recovery 2023-09-20 12:45:04 +02:00
Damien Arrachequesne
d744fda772 docs: improve example with express-session
The example is now available with different syntaxes:

- CommonJS
- ES modules
- TypeScript

Related: https://github.com/socketio/socket.io/pull/4787
2023-09-13 15:56:15 +02:00
Damien Arrachequesne
8259cdac84 docs: use io.engine.use() with express-session
Related: https://github.com/socketio/socket.io/discussions/4819
2023-09-13 12:13:03 +02:00
Damien Arrachequesne
fd9dd74eee docs: use "connection" instead of "connect"
"connect" and "connection" have the same meaning, but "connection" is
the preferred version.
2023-08-12 10:10:55 +02:00
Damien Arrachequesne
c332643ad8 chore(release): 4.7.2
Diff: https://github.com/socketio/socket.io/compare/4.7.1...4.7.2
2023-08-03 01:51:04 +02:00
Damien Arrachequesne
3468a197af fix(webtransport): properly handle WebTransport-only connections
A WebTransport-only connection has no `request` attribute, so we need
to handle that case.
2023-08-03 01:45:21 +02:00
Damien Arrachequesne
09d45491c4 chore: bump engine.io to version 6.5.2
Diff: https://github.com/socketio/engine.io/compare/6.5.0...6.5.2
Release notes: https://github.com/socketio/engine.io/releases/tag/6.5.2
2023-08-03 00:39:46 +02:00
Jaro
0731c0d2f4 fix: clean up child namespace when client is rejected in middleware (#4773)
Related: https://github.com/socketio/socket.io/issues/4772
2023-07-21 08:33:46 +02:00
Damien Arrachequesne
03046a64ad docs: update the list of supported Node.js versions
The Engine.IO server uses `timeout.refresh()` (see [1]), which was
added in Node.js 10.2.0.

Reference: https://nodejs.org/api/timers.html#timeoutrefresh

Related: https://github.com/socketio/engine.io/issues/686

[1]: 37474c7e67
2023-07-09 10:14:49 +02:00
Damien Arrachequesne
443e447087 docs(examples): add example with WebTransport 2023-06-29 11:21:27 +02:00
Damien Arrachequesne
2f6cc2fa42 chore(release): 4.7.1
Diff: https://github.com/socketio/socket.io/compare/4.7.0...4.7.1
2023-06-28 09:32:32 +02:00
Damien Arrachequesne
00d8ee5b05 chore(release): 4.7.0
Diff: https://github.com/socketio/socket.io/compare/4.6.2...4.7.0
2023-06-22 11:27:45 +02:00
Damien Arrachequesne
2dd5fa9dd4 ci: add Node.js 20 in the test matrix
Reference: https://github.com/nodejs/Release
2023-06-22 07:55:32 +02:00
Damien Arrachequesne
a5dff0ac83 docs(examples): increase httpd ProxyTimeout value (2) 2023-06-21 00:07:44 +02:00
Damien Arrachequesne
3035c25982 docs(examples): increase httpd ProxyTimeout value
With a value that is too small, the HTTP long-polling request receives
an HTTP 502 response code and the connection gets closed.
2023-06-20 23:57:46 +02:00
Damien Arrachequesne
63f181cc12 feat: serve client bundles with CORS headers
The version of the `cors` package matches the one used by `engine.io`.

Related: https://github.com/socketio/socket.io/issues/3552
2023-06-20 14:32:59 +02:00
Damien Arrachequesne
a250e283da chore: bump engine.io to version 6.5.0
Diff: https://github.com/socketio/engine.io/compare/6.4.2...6.5.0
Release notes: https://github.com/socketio/engine.io/releases/tag/6.5.0
2023-06-20 09:17:10 +02:00
SyedTayyabUlMazhar
e5c62cad60 fix: remove the Partial modifier from the socket.data type (#4740)
Wrapping SocketData with Partial causes issues when reading data even
if you've made sure to pass all values. If someone want to make their
type Partial or make only a few properties optional, they can do so in
their own type instead.

Related: https://github.com/socketio/socket.io/issues/4537
2023-06-20 07:49:02 +02:00
Damien Arrachequesne
01d37624a8 docs(changelog): update the version range of the engine.io dependency 2023-05-31 11:28:00 +02:00
Damien Arrachequesne
faf914c9ab chore(release): 4.6.2
Diff: https://github.com/socketio/socket.io/compare/4.6.1...4.6.2
2023-05-31 11:15:41 +02:00
Damien Arrachequesne
15af22fc22 refactor: add a noop handler for the error event
We should reduce the scope of the "event" error in the next major
version, as it is overloaded today:

- it can be sent by the client (`socket.emit("error")`, which is a perfectly valid event name)
- it can be emitted when the connection encounters an error (an invalid packet for example)
- it can be emitted when a packet is rejected in a middleware (`socket.use()`)

Related: https://github.com/socketio/socket.io/issues/2047
2023-05-24 10:47:52 +02:00
Damien Arrachequesne
d3658944e5 chore: bump socket.io-parser to version 4.2.3
Reference: https://github.com/advisories/GHSA-cqmj-92xf-r6r9
2023-05-24 07:27:12 +02:00
Damien Arrachequesne
12b0de4f52 chore: bump engine.io to version 6.4.2
Reference: https://github.com/advisories/GHSA-q9mw-68c2-j6m5

Related: https://github.com/socketio/socket.io/issues/4711
2023-05-10 10:20:42 +02:00
Mateusz Burzyński
3d44aae381 fix(exports): move types condition to the top (#4698)
Related: https://github.com/microsoft/TypeScript/issues/50762
2023-05-04 07:27:09 +02:00
Damien Arrachequesne
cbf0362476 docs(examples): bump dependencies for the private messaging example
Related: https://github.com/socketio/socket.io/issues/4681
2023-05-02 18:07:07 +02:00
Damien Arrachequesne
59280da20b docs(examples): update examples to docker compose v2
Reference: https://docs.docker.com/compose/

Related: https://github.com/socketio/socket.io/discussions/4669
2023-04-07 15:57:20 +02:00
Damien Arrachequesne
50a4d37cb8 docs(changelog): add version of transitive dependencies 2023-03-27 17:35:42 +02:00
Damien Arrachequesne
6458b2bef1 docs(example): basic WebSocket-only client 2023-03-24 11:17:29 +01:00
Damien Arrachequesne
b56da8a99f docs(examples): upgrade to React 18
Reference: https://reactjs.org/blog/2022/03/08/react-18-upgrade-guide.html
2023-03-07 08:31:21 +01:00
130 changed files with 16947 additions and 10413 deletions

View File

@@ -16,7 +16,9 @@ jobs:
strategy:
matrix:
node-version: [12, 14, 16]
node-version:
- 16
- 20
steps:
- name: Checkout repository
@@ -34,3 +36,33 @@ jobs:
run: npm test
env:
CI: true
build-examples:
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
example:
- custom-parsers
- 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 }}
npm install
npm run build

View File

@@ -1,7 +1,16 @@
# 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)
@@ -57,6 +66,166 @@
# 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)
@@ -68,7 +237,7 @@
### Dependencies
- [`engine.io@~6.4.0`](https://github.com/socketio/engine.io/releases/tag/6.4.0) (no change)
- [`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)
@@ -250,8 +419,8 @@ Added in [d0fd474](https://github.com/socketio/engine.io/commit/d0fd4746afa39629
### Dependencies
- [`engine.io@~6.4.0`](https://github.com/socketio/engine.io/releases/tag/6.4.0) ([diff](https://github.com/socketio/engine.io/compare/6.2.0...6.2.1))
- [`ws@~8.11.0`](https://github.com/websockets/ws/releases/tag/8.11.0) ([diff](https://github.com/websockets/ws/compare/8.2.3...8.11.0))
- [`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)
@@ -276,6 +445,11 @@ This release contains a bump of:
* **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)
@@ -286,13 +460,18 @@ This release contains a bump of:
* 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.
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)
@@ -304,6 +483,11 @@ Security advisory: [GHSA-j4f2-536g-r55m](https://github.com/advisories/GHSA-j4f2
* 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)
@@ -314,6 +498,11 @@ Security advisory: [GHSA-j4f2-536g-r55m](https://github.com/advisories/GHSA-j4f2
* 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)
@@ -326,7 +515,7 @@ Security advisory: [GHSA-j4f2-536g-r55m](https://github.com/advisories/GHSA-j4f2
### Features
* add support for catch-all listeners for outgoing packets ([531104d](https://github.com/socketio/socket.io/commit/531104d332690138b7aab84d5583d6204132c8b4))
#### Catch-all listeners for outgoing packets
This is similar to `onAny()`, but for outgoing packets.
@@ -338,7 +527,9 @@ socket.onAnyOutgoing((event, ...args) => {
});
```
* broadcast and expect multiple acks ([8b20457](https://github.com/socketio/socket.io/commit/8b204570a94979bbec307f23ca078f30f5cf07b0))
Added in [531104d](https://github.com/socketio/socket.io/commit/531104d332690138b7aab84d5583d6204132c8b4).
#### Broadcast and expect multiple acknowledgements
Syntax:
@@ -348,18 +539,25 @@ io.timeout(1000).emit("some-event", (err, responses) => {
});
```
* add the "maxPayload" field in the handshake details ([088dcb4](https://github.com/socketio/engine.io/commit/088dcb4dff60df39785df13d0a33d3ceaa1dff38))
Added in [8b20457](https://github.com/socketio/socket.io/commit/8b204570a94979bbec307f23ca078f30f5cf07b0).
So that clients in HTTP long-polling can decide how many packets they have to send to stay under the maxHttpBufferSize
value.
#### `maxHttpBufferSize` value negotiation
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:
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)
@@ -693,7 +891,7 @@ new Server(3000, {
const socket = io("/admin");
// server-side
io.on("connect", socket => {
io.on("connection", socket => {
// not triggered anymore
})
@@ -844,7 +1042,7 @@ new Server(3000, {
const socket = io("/admin");
// server-side
io.on("connect", socket => {
io.on("connection", socket => {
// not triggered anymore
})

View File

@@ -22,6 +22,7 @@ Some implementations in other languages are also available:
- [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:

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

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 one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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).
![demo](assets/demo.gif)
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

View File

@@ -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

View File

@@ -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
}
}));
}
};

View File

@@ -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));
});
});

View File

@@ -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();
}
}

View File

@@ -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"
]
}
}

View File

@@ -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
});
};

View File

@@ -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"
}
}

View File

@@ -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)">

View File

@@ -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');
});
});

View File

@@ -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('');
}
}
}

View File

@@ -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)]
};

View File

@@ -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 { }

View File

@@ -0,0 +1,3 @@
import { Routes } from '@angular/router';
export const routes: Routes = [];

View File

@@ -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>;

View File

@@ -0,0 +1,3 @@
export const environment = {
serverUrl: "http://localhost:3000"
};

View File

@@ -1,4 +0,0 @@
export const environment = {
production: true,
serverUrl: "https://my-custom-domain.com"
};

View File

@@ -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

View File

@@ -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">

View File

@@ -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));

View File

@@ -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
*/

View File

@@ -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);

View File

@@ -6,8 +6,7 @@
"types": []
},
"files": [
"src/main.ts",
"src/polyfills.ts"
"src/main.ts"
],
"include": [
"src/**/*.d.ts"

View File

@@ -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

View File

@@ -7,10 +7,6 @@
"jasmine"
]
},
"files": [
"src/test.ts",
"src/polyfills.ts"
],
"include": [
"src/**/*.spec.ts",
"src/**/*.d.ts"

View File

@@ -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"
]
}
}

View File

@@ -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> {

View File

@@ -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";

View File

@@ -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">,

View File

@@ -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 {

View 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?

View 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/).

View File

@@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

View File

@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "es5",
"module": "esnext",
"baseUrl": "./",
"moduleResolution": "node",
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
}
}

View 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"
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View 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>

View 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;
}
}

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View 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");

View 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);

View 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);
}
});
},
},
});

View File

@@ -0,0 +1,4 @@
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true
})

File diff suppressed because it is too large Load Diff

View 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
```

View 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};

View 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`);

View 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"
}
}

View 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()],
}
};

View 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);
}

View 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");
});
});

View File

@@ -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"

View File

@@ -1,2 +0,0 @@
FROM haproxy:1.7-alpine
COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg

View File

@@ -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"

View File

@@ -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

View File

@@ -1,2 +0,0 @@
FROM httpd:2.4-alpine
COPY ./httpd.conf /usr/local/apache2/conf/httpd.conf

View File

@@ -1,4 +1,4 @@
FROM mhart/alpine-node:6
FROM node:14-alpine
# Create app directory
RUN mkdir -p /usr/src/app

View File

@@ -1,58 +1,58 @@
services:
nginx:
image: nginx:alpine
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
links:
- server-john
- server-paul
- server-george
- server-ringo
ports:
- "3000:80"
nginx:
image: nginx:alpine
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
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
client:
build: ./client
links:
- nginx
client:
build: ./client
links:
- nginx
redis:
image: redis:alpine
expose:
- "6379"
redis:
image: redis:6
expose:
- "6379"

View 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).
![Video of the example](assets/csr.gif)
## 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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

View File

@@ -0,0 +1 @@
FROM node:20-bullseye

View File

@@ -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
}
}
}

View 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>

View 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);

View 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"
}
}

View File

@@ -0,0 +1 @@
FROM node:20-bullseye

View File

@@ -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
}
}
}

View 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>

View 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);

View 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"
}
}

View File

@@ -3,14 +3,14 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-scripts": "3.4.1",
"socket.io": "4",
"socket.io-client": "4"
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.4.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "^5.0.1",
"socket.io": "^4.6.1",
"socket.io-client": "^4.6.1"
},
"scripts": {
"start": "react-scripts start",

View File

@@ -1,14 +1,15 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { createRoot } from 'react-dom/client';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
ReactDOM.render(
const container = document.getElementById('root');
const root = createRoot(container)
root.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
</React.StrictMode>
);
// If you want your app to work offline and load faster, you can change

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@ import { Server } from "socket.io";
const io = new Server(8080);
io.on("connect", (socket) => {
io.on("connection", (socket) => {
console.log(`connect ${socket.id}`);
socket.on("ping", (cb) => {

View File

@@ -52,6 +52,10 @@
socket.on("disconnect", () => {
ioStatus.innerText = "disconnected";
});
socket.on("current count", (count) => {
ioCount.innerText = count;
});
</script>
</body>
</html>

View File

@@ -0,0 +1,66 @@
const express = require("express");
const { createServer } = require("node:http");
const { join } = require("node:path");
const { Server } = require("socket.io");
const session = require("express-session");
const port = process.env.PORT || 3000;
const app = express();
const httpServer = createServer(app);
const sessionMiddleware = session({
secret: "changeit",
resave: true,
saveUninitialized: true,
});
app.use(sessionMiddleware);
app.get("/", (req, res) => {
res.sendFile(join(__dirname, "index.html"));
});
app.post("/incr", (req, res) => {
const session = req.session;
session.count = (session.count || 0) + 1;
res.status(200).end("" + session.count);
io.to(session.id).emit("current count", session.count);
});
app.post("/logout", (req, res) => {
const sessionId = req.session.id;
req.session.destroy(() => {
// disconnect all Socket.IO connections linked to this session ID
io.to(sessionId).disconnectSockets();
res.status(204).end();
});
});
const io = new Server(httpServer);
io.engine.use(sessionMiddleware);
io.on("connection", (socket) => {
const req = socket.request;
socket.join(req.session.id);
socket.on("incr", (cb) => {
req.session.reload((err) => {
if (err) {
// session has expired
return socket.disconnect();
}
req.session.count = (req.session.count || 0) + 1;
req.session.save(() => {
cb(req.session.count);
});
});
});
});
httpServer.listen(port, () => {
console.log(`application is running at: http://localhost:${port}`);
});

View File

@@ -0,0 +1,15 @@
{
"name": "express-session-example",
"version": "0.0.1",
"private": true,
"type": "commonjs",
"description": "Example with express-session (https://github.com/expressjs/session)",
"scripts": {
"start": "node index.js"
},
"dependencies": {
"express": "~4.17.3",
"express-session": "~1.17.2",
"socket.io": "^4.7.2"
}
}

View File

@@ -0,0 +1,61 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Example with express-session</title>
</head>
<body>
<button onclick="incrementWithFetch()">Increment with fetch()</button>
<button onclick="logout()">Logout</button>
<p>Count: <span id="httpCount">0</span></p>
<button onclick="incrementWithEmit()">
Increment with Socket.IO emit()
</button>
<p>Status: <span id="ioStatus">disconnected</span></p>
<p>Count: <span id="ioCount">0</span></p>
<script src="/socket.io/socket.io.js"></script>
<script>
const httpCount = document.getElementById("httpCount");
const ioStatus = document.getElementById("ioStatus");
const ioCount = document.getElementById("ioCount");
const socket = io({
// with WebSocket only
// transports: ["websocket"],
});
async function incrementWithFetch() {
const response = await fetch("/incr", {
method: "post",
});
httpCount.innerText = await response.text();
}
function logout() {
fetch("/logout", {
method: "post",
});
}
async function incrementWithEmit() {
socket.emit("incr", (count) => {
ioCount.innerText = count;
});
}
socket.on("connect", () => {
ioStatus.innerText = "connected";
});
socket.on("disconnect", () => {
ioStatus.innerText = "disconnected";
});
socket.on("current count", (count) => {
ioCount.innerText = count;
});
</script>
</body>
</html>

View File

@@ -1,5 +1,5 @@
import express from "express";
import { createServer } from "http";
import { createServer } from "node:http";
import { Server } from "socket.io";
import session from "express-session";
@@ -17,13 +17,15 @@ const sessionMiddleware = session({
app.use(sessionMiddleware);
app.get("/", (req, res) => {
res.sendFile("./index.html", { root: process.cwd() });
res.sendFile(new URL("./index.html", import.meta.url).pathname);
});
app.post("/incr", (req, res) => {
const session = req.session;
session.count = (session.count || 0) + 1;
res.status(200).end("" + session.count);
io.to(session.id).emit("current count", session.count);
});
app.post("/logout", (req, res) => {
@@ -35,39 +37,11 @@ app.post("/logout", (req, res) => {
});
});
const io = new Server(httpServer, {
allowRequest: (req, callback) => {
// with HTTP long-polling, we have access to the HTTP response here, but this is not
// the case with WebSocket, so we provide a dummy response object
const fakeRes = {
getHeader() {
return [];
},
setHeader(key, values) {
req.cookieHolder = values[0];
},
writeHead() {},
};
sessionMiddleware(req, fakeRes, () => {
if (req.session) {
// trigger the setHeader() above
fakeRes.writeHead();
// manually save the session (normally triggered by res.end())
req.session.save();
}
callback(null, true);
});
},
});
const io = new Server(httpServer);
io.engine.on("initial_headers", (headers, req) => {
if (req.cookieHolder) {
headers["set-cookie"] = req.cookieHolder;
delete req.cookieHolder;
}
});
io.engine.use(sessionMiddleware);
io.on("connect", (socket) => {
io.on("connection", (socket) => {
const req = socket.request;
socket.join(req.session.id);

View File

@@ -10,6 +10,6 @@
"dependencies": {
"express": "~4.17.3",
"express-session": "~1.17.2",
"socket.io": "~4.4.1"
"socket.io": "^4.7.2"
}
}

View File

@@ -0,0 +1,61 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Example with express-session</title>
</head>
<body>
<button onclick="incrementWithFetch()">Increment with fetch()</button>
<button onclick="logout()">Logout</button>
<p>Count: <span id="httpCount">0</span></p>
<button onclick="incrementWithEmit()">
Increment with Socket.IO emit()
</button>
<p>Status: <span id="ioStatus">disconnected</span></p>
<p>Count: <span id="ioCount">0</span></p>
<script src="/socket.io/socket.io.js"></script>
<script>
const httpCount = document.getElementById("httpCount");
const ioStatus = document.getElementById("ioStatus");
const ioCount = document.getElementById("ioCount");
const socket = io({
// with WebSocket only
// transports: ["websocket"],
});
async function incrementWithFetch() {
const response = await fetch("/incr", {
method: "post",
});
httpCount.innerText = await response.text();
}
function logout() {
fetch("/logout", {
method: "post",
});
}
async function incrementWithEmit() {
socket.emit("incr", (count) => {
ioCount.innerText = count;
});
}
socket.on("connect", () => {
ioStatus.innerText = "connected";
});
socket.on("disconnect", () => {
ioStatus.innerText = "disconnected";
});
socket.on("current count", (count) => {
ioCount.innerText = count;
});
</script>
</body>
</html>

View File

@@ -0,0 +1,72 @@
import express = require("express");
import { createServer } from "http";
import { Server } from "socket.io";
import session from "express-session";
import { type Request } from "express";
declare module "express-session" {
interface SessionData {
count: number;
}
}
const port = process.env.PORT || 3000;
const app = express();
const httpServer = createServer(app);
const sessionMiddleware = session({
secret: "changeit",
resave: true,
saveUninitialized: true,
});
app.use(sessionMiddleware);
app.get("/", (req, res) => {
res.sendFile(new URL("./index.html", import.meta.url).pathname);
});
app.post("/incr", (req, res) => {
const session = req.session;
session.count = (session.count || 0) + 1;
res.status(200).end("" + session.count);
io.to(session.id).emit("current count", session.count);
});
app.post("/logout", (req, res) => {
const sessionId = req.session.id;
req.session.destroy(() => {
// disconnect all Socket.IO connections linked to this session ID
io.to(sessionId).disconnectSockets();
res.status(204).end();
});
});
const io = new Server(httpServer);
io.engine.use(sessionMiddleware);
io.on("connection", (socket) => {
const req = socket.request as Request;
socket.join(req.session.id);
socket.on("incr", (cb) => {
req.session.reload((err) => {
if (err) {
// session has expired
return socket.disconnect();
}
req.session.count = (req.session.count || 0) + 1;
req.session.save(() => {
cb(req.session.count);
});
});
});
});
httpServer.listen(port, () => {
console.log(`application is running at: http://localhost:${port}`);
});

Some files were not shown because too many files have changed in this diff Show More