Compare commits

..

2 Commits

Author SHA1 Message Date
Maxime Kjaer
f3ea96eb3a test: build examples in the CI (#3856) 2023-10-10 20:00:27 +02:00
abriejenny
c2858e911e docs(examples): update docker-compose.yml (#4594)
Updated docker compose to be in line with current practices.

Reference: https://docs.docker.com/compose/
2023-01-12 09:56:11 +01:00
198 changed files with 15346 additions and 31733 deletions

View File

@@ -6,38 +6,22 @@ on:
schedule:
- cron: '0 0 * * 0'
permissions:
contents: read
jobs:
test-node:
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
matrix:
node-version:
- 16
- 20
node-version: [12, 14, 16]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
run: npm ci
- name: Install TypeScript 4.2
run: npm i typescript@4.2
if: ${{ matrix.node-version == '16' }}
- name: Run tests
run: npm test
- run: npm ci
- run: npm test
env:
CI: true
@@ -53,18 +37,16 @@ jobs:
- typescript
- webpack-build
- webpack-build-server
- basic-crud-application/angular-client
- basic-crud-application/vue-client
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Use Node.js 20
uses: actions/setup-node@v4
uses: actions/setup-node@v3
with:
node-version: 20
- name: Build ${{ matrix.example }}
run: |
cd examples/${{ matrix.example }}

View File

@@ -1,582 +1,3 @@
# History
## 2024
- [4.7.5](#475-2024-03-14) (Mar 2024)
- [4.7.4](#474-2024-01-12) (Jan 2024)
- [4.7.3](#473-2024-01-03) (Jan 2024)
## 2023
- [4.7.2](#472-2023-08-02) (Aug 2023)
- [4.7.1](#471-2023-06-28) (Jun 2023)
- [4.7.0](#470-2023-06-22) (Jun 2023)
- [4.6.2](#462-2023-05-31) (May 2023)
- [4.6.1](#461-2023-02-20) (Feb 2023)
- [4.6.0](#460-2023-02-07) (Feb 2023)
## 2022
- [4.5.4](#454-2022-11-22) (Nov 2022)
- [4.5.3](#453-2022-10-15) (Oct 2022)
- [4.5.2](#452-2022-09-02) (Sep 2022)
- [2.5.0](#250-2022-06-26) (Jun 2022) (from the [2.x](https://github.com/socketio/socket.io/tree/2.x) branch)
- [4.5.1](#451-2022-05-17) (May 2022)
- [4.5.0](#450-2022-04-23) (Apr 2022)
- [4.4.1](#441-2022-01-06) (Jan 2022)
## 2021
- [4.4.0](#440-2021-11-18) (Nov 2021)
- [4.3.2](#432-2021-11-08) (Nov 2021)
- [4.3.1](#431-2021-10-16) (Oct 2021)
- [4.3.0](#430-2021-10-14) (Oct 2021)
- [4.2.0](#420-2021-08-30) (Aug 2021)
- [4.1.3](#413-2021-07-10) (Jul 2021)
- [4.1.2](#412-2021-05-17) (May 2021)
- [4.1.1](#411-2021-05-11) (May 2021)
- [4.1.0](#410-2021-05-11) (May 2021)
- [4.0.2](#402-2021-05-06) (May 2021)
- [4.0.1](#401-2021-03-31) (Mar 2021)
- [**4.0.0**](#400-2021-03-10) (Mar 2021)
- [3.1.2](#312-2021-02-26) (Feb 2021)
- [3.1.1](#311-2021-02-03) (Feb 2021)
- [3.1.0](#310-2021-01-15) (Jan 2021)
- [2.4.1](#241-2021-01-07) (Jan 2021) (from the [2.x](https://github.com/socketio/socket.io/tree/2.x) branch)
- [3.0.5](#305-2021-01-05) (Jan 2021)
- [2.4.0](#240-2021-01-04) (Jan 2021) (from the [2.x](https://github.com/socketio/socket.io/tree/2.x) branch)
## 2020
- [3.0.4](#304-2020-12-07) (Dec 2020)
- [3.0.3](#303-2020-11-19) (Nov 2020)
- [3.0.2](#302-2020-11-17) (Nov 2020)
- [3.0.1](#301-2020-11-09) (Nov 2020)
- [**3.0.0**](#300-2020-11-05) (Nov 2020)
## 2019
- [2.3.0](#230-2019-09-20) (Sep 2019)
## 2018
- [2.2.0](#220-2018-11-29) (Nov 2018)
- [2.1.1](#211-2018-05-17) (May 2018)
- [2.1.0](#210-2018-03-29) (Mar 2018)
# Release notes
## [4.7.5](https://github.com/socketio/socket.io/compare/4.7.4...4.7.5) (2024-03-14)
### Bug Fixes
* close the adapters when the server is closed ([bf64870](https://github.com/socketio/socket.io/commit/bf64870957e626a73e0544716a1a41a4ba5093bb))
* remove duplicate pipeline when serving bundle ([e426f3e](https://github.com/socketio/socket.io/commit/e426f3e8e1bfea5720c32d30a3663303200ee6ad))
### 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.4](https://github.com/socketio/socket.io/compare/4.7.3...4.7.4) (2024-01-12)
### Bug Fixes
* **typings:** calling io.emit with no arguments incorrectly errored ([cb6d2e0](https://github.com/socketio/socket.io/commit/cb6d2e02aa7ec03c2de1817d35cffa1128b107ef)), closes [#4914](https://github.com/socketio/socket.io/issues/4914)
### Dependencies
- [`engine.io@~6.5.2`](https://github.com/socketio/engine.io/releases/tag/6.5.2) (no change)
- [`ws@~8.11.0`](https://github.com/websockets/ws/releases/tag/8.11.0) (no change)
## [4.7.3](https://github.com/socketio/socket.io/compare/4.7.2...4.7.3) (2024-01-03)
### Bug Fixes
* return the first response when broadcasting to a single socket ([#4878](https://github.com/socketio/socket.io/issues/4878)) ([df8e70f](https://github.com/socketio/socket.io/commit/df8e70f79822e3887b4f21ca718af8a53bbda2c4))
* **typings:** allow to bind to a non-secure Http2Server ([#4853](https://github.com/socketio/socket.io/issues/4853)) ([8c9ebc3](https://github.com/socketio/socket.io/commit/8c9ebc30e5452ff9381af5d79f547394fa55633c))
### Dependencies
- [`engine.io@~6.5.2`](https://github.com/socketio/engine.io/releases/tag/6.5.2) (no change)
- [`ws@~8.11.0`](https://github.com/websockets/ws/releases/tag/8.11.0) (no change)
## [4.7.2](https://github.com/socketio/socket.io/compare/4.7.1...4.7.2) (2023-08-02)
### Bug Fixes
* clean up child namespace when client is rejected in middleware ([#4773](https://github.com/socketio/socket.io/issues/4773)) ([0731c0d](https://github.com/socketio/socket.io/commit/0731c0d2f497d5cce596ea1ec32a67c08bcccbcd))
* **webtransport:** properly handle WebTransport-only connections ([3468a19](https://github.com/socketio/socket.io/commit/3468a197afe87e65eb0d779fabd347fe683013ab))
* **webtransport:** add proper framing ([a306db0](https://github.com/socketio/engine.io/commit/a306db09e8ddb367c7d62f45fec920f979580b7c))
### Dependencies
- [`engine.io@~6.5.2`](https://github.com/socketio/engine.io/releases/tag/6.5.2) ([diff](https://github.com/socketio/engine.io/compare/6.5.0...6.5.2))
- [`ws@~8.11.0`](https://github.com/websockets/ws/releases/tag/8.11.0) (no change)
## [4.7.1](https://github.com/socketio/socket.io/compare/4.7.0...4.7.1) (2023-06-28)
The client bundle contains a few fixes regarding the WebTransport support.
### Dependencies
- [`engine.io@~6.5.0`](https://github.com/socketio/engine.io/releases/tag/6.5.0) (no change)
- [`ws@~8.11.0`](https://github.com/websockets/ws/releases/tag/8.11.0) (no change)
## [4.7.0](https://github.com/socketio/socket.io/compare/4.6.2...4.7.0) (2023-06-22)
### Bug Fixes
* remove the Partial modifier from the socket.data type ([#4740](https://github.com/socketio/socket.io/issues/4740)) ([e5c62ca](https://github.com/socketio/socket.io/commit/e5c62cad60fc7d16fbb024fd9be1d1880f4e6f5f))
### Features
#### Support for WebTransport
The Socket.IO server can now use WebTransport as the underlying transport.
WebTransport is a web API that uses the HTTP/3 protocol as a bidirectional transport. It's intended for two-way communications between a web client and an HTTP/3 server.
References:
- https://w3c.github.io/webtransport/
- https://developer.mozilla.org/en-US/docs/Web/API/WebTransport
- https://developer.chrome.com/articles/webtransport/
Until WebTransport support lands [in Node.js](https://github.com/nodejs/node/issues/38478), you can use the `@fails-components/webtransport` package:
```js
import { readFileSync } from "fs";
import { createServer } from "https";
import { Server } from "socket.io";
import { Http3Server } from "@fails-components/webtransport";
// WARNING: the total length of the validity period MUST NOT exceed two weeks (https://w3c.github.io/webtransport/#custom-certificate-requirements)
const cert = readFileSync("/path/to/my/cert.pem");
const key = readFileSync("/path/to/my/key.pem");
const httpsServer = createServer({
key,
cert
});
httpsServer.listen(3000);
const io = new Server(httpsServer, {
transports: ["polling", "websocket", "webtransport"] // WebTransport is not enabled by default
});
const h3Server = new Http3Server({
port: 3000,
host: "0.0.0.0",
secret: "changeit",
cert,
privKey: key,
});
(async () => {
const stream = await h3Server.sessionStream("/socket.io/");
const sessionReader = stream.getReader();
while (true) {
const { done, value } = await sessionReader.read();
if (done) {
break;
}
io.engine.onWebTransportSession(value);
}
})();
h3Server.startServer();
```
Added in [123b68c](https://github.com/socketio/engine.io/commit/123b68c04f9e971f59b526e0f967a488ee6b0116).
#### Client bundles with CORS headers
The bundles will now have the right `Access-Control-Allow-xxx` headers.
Added in [63f181c](https://github.com/socketio/socket.io/commit/63f181cc12cbbbf94ed40eef52d60f36a1214fbe).
### Dependencies
- [`engine.io@~6.5.0`](https://github.com/socketio/engine.io/releases/tag/6.5.0) ([diff](https://github.com/socketio/engine.io/compare/6.4.2...6.5.0))
- [`ws@~8.11.0`](https://github.com/websockets/ws/releases/tag/8.11.0) (no change)
## [4.6.2](https://github.com/socketio/socket.io/compare/4.6.1...4.6.2) (2023-05-31)
### Bug Fixes
* **exports:** move `types` condition to the top ([#4698](https://github.com/socketio/socket.io/issues/4698)) ([3d44aae](https://github.com/socketio/socket.io/commit/3d44aae381af38349fdb808d510d9f47a0c2507e))
### Dependencies
- [`engine.io@~6.4.2`](https://github.com/socketio/engine.io/releases/tag/6.4.0) ([diff](https://github.com/socketio/engine.io/compare/6.4.1...6.4.2))
- [`ws@~8.11.0`](https://github.com/websockets/ws/releases/tag/8.11.0) (no change)
## [4.6.1](https://github.com/socketio/socket.io/compare/4.6.0...4.6.1) (2023-02-20)
### Bug Fixes
* properly handle manually created dynamic namespaces ([0d0a7a2](https://github.com/socketio/socket.io/commit/0d0a7a22b5ff95f864216c529114b7dd41738d1e))
* **types:** fix nodenext module resolution compatibility ([#4625](https://github.com/socketio/socket.io/issues/4625)) ([d0b22c6](https://github.com/socketio/socket.io/commit/d0b22c630208669aceb7ae013180c99ef90279b0))
### Dependencies
- [`engine.io@~6.4.1`](https://github.com/socketio/engine.io/releases/tag/6.4.1) ([diff](https://github.com/socketio/engine.io/compare/6.4.0...6.4.1))
- [`ws@~8.11.0`](https://github.com/websockets/ws/releases/tag/8.11.0) (no change)
## [4.6.0](https://github.com/socketio/socket.io/compare/4.5.4...4.6.0) (2023-02-07)
### Bug Fixes
* add timeout method to remote socket ([#4558](https://github.com/socketio/socket.io/issues/4558)) ([0c0eb00](https://github.com/socketio/socket.io/commit/0c0eb0016317218c2be3641e706cfaa9bea39a2d))
* **typings:** properly type emits with timeout ([f3ada7d](https://github.com/socketio/socket.io/commit/f3ada7d8ccc02eeced2b9b9ac8e4bc921eb630d2))
### Features
#### Promise-based acknowledgements
This commit adds some syntactic sugar around acknowledgements:
- `emitWithAck()`
```js
try {
const responses = await io.timeout(1000).emitWithAck("some-event");
console.log(responses); // one response per client
} catch (e) {
// some clients did not acknowledge the event in the given delay
}
io.on("connection", async (socket) => {
// without timeout
const response = await socket.emitWithAck("hello", "world");
// with a specific timeout
try {
const response = await socket.timeout(1000).emitWithAck("hello", "world");
} catch (err) {
// the client did not acknowledge the event in the given delay
}
});
```
- `serverSideEmitWithAck()`
```js
try {
const responses = await io.timeout(1000).serverSideEmitWithAck("some-event");
console.log(responses); // one response per server (except itself)
} catch (e) {
// some servers did not acknowledge the event in the given delay
}
```
Added in [184f3cf](https://github.com/socketio/socket.io/commit/184f3cf7af57acc4b0948eee307f25f8536eb6c8).
#### Connection state recovery
This feature allows a client to reconnect after a temporary disconnection and restore its state:
- id
- rooms
- data
- missed packets
Usage:
```js
import { Server } from "socket.io";
const io = new Server({
connectionStateRecovery: {
// default values
maxDisconnectionDuration: 2 * 60 * 1000,
skipMiddlewares: true,
},
});
io.on("connection", (socket) => {
console.log(socket.recovered); // whether the state was recovered or not
});
```
Here's how it works:
- the server sends a session ID during the handshake (which is different from the current `id` attribute, which is public and can be freely shared)
- the server also includes an offset in each packet (added at the end of the data array, for backward compatibility)
- upon temporary disconnection, the server stores the client state for a given delay (implemented at the adapter level)
- upon reconnection, the client sends both the session ID and the last offset it has processed, and the server tries to restore the state
The in-memory adapter already supports this feature, and we will soon update the Postgres and MongoDB adapters. We will also create a new adapter based on [Redis Streams](https://redis.io/docs/data-types/streams/), which will support this feature.
Added in [54d5ee0](https://github.com/socketio/socket.io/commit/54d5ee05a684371191e207b8089f09fc24eb5107).
#### Compatibility (for real) with Express middlewares
This feature implements middlewares at the Engine.IO level, because Socket.IO middlewares are meant for namespace authorization and are not executed during a classic HTTP request/response cycle.
Syntax:
```js
io.engine.use((req, res, next) => {
// do something
next();
});
// with express-session
import session from "express-session";
io.engine.use(session({
secret: "keyboard cat",
resave: false,
saveUninitialized: true,
cookie: { secure: true }
}));
// with helmet
import helmet from "helmet";
io.engine.use(helmet());
```
A workaround was possible by using the allowRequest option and the "headers" event, but this feels way cleaner and works with upgrade requests too.
Added in [24786e7](https://github.com/socketio/engine.io/commit/24786e77c5403b1c4b5a2bc84e2af06f9187f74a).
#### Error details in the disconnecting and disconnect events
The `disconnect` event will now contain additional details about the disconnection reason.
```js
io.on("connection", (socket) => {
socket.on("disconnect", (reason, description) => {
console.log(description);
});
});
```
Added in [8aa9499](https://github.com/socketio/socket.io/commit/8aa94991cee5518567d6254eec04b23f81510257).
#### Automatic removal of empty child namespaces
This commit adds a new option, "cleanupEmptyChildNamespaces". With this option enabled (disabled by default), when a socket disconnects from a dynamic namespace and if there are no other sockets connected to it then the namespace will be cleaned up and its adapter will be closed.
```js
import { createServer } from "node:http";
import { Server } from "socket.io";
const httpServer = createServer();
const io = new Server(httpServer, {
cleanupEmptyChildNamespaces: true
});
```
Added in [5d9220b](https://github.com/socketio/socket.io/commit/5d9220b69adf73e086c27bbb63a4976b348f7c4c).
#### A new "addTrailingSlash" option
The trailing slash which was added by default can now be disabled:
```js
import { createServer } from "node:http";
import { Server } from "socket.io";
const httpServer = createServer();
const io = new Server(httpServer, {
addTrailingSlash: false
});
```
In the example above, the clients can omit the trailing slash and use `/socket.io` instead of `/socket.io/`.
Added in [d0fd474](https://github.com/socketio/engine.io/commit/d0fd4746afa396297f07bb62e539b0c1c4018d7c).
### Performance Improvements
* precompute the WebSocket frames when broadcasting ([da2b542](https://github.com/socketio/socket.io/commit/da2b54279749adc5279c9ac4742b01b36c01cff0))
### Dependencies
- [`engine.io@~6.4.0`](https://github.com/socketio/engine.io/releases/tag/6.4.0) (https://github.com/socketio/engine.io/compare/6.2.1...6.4.0)
- [`ws@~8.11.0`](https://github.com/websockets/ws/releases/tag/8.11.0) (https://github.com/websockets/ws/compare/8.2.3...8.11.0)
## [4.5.4](https://github.com/socketio/socket.io/compare/4.5.3...4.5.4) (2022-11-22)
This release contains a bump of:
- `engine.io` in order to fix [CVE-2022-41940](https://github.com/socketio/engine.io/security/advisories/GHSA-r7qp-cfhv-p84w)
- `socket.io-parser` in order to fix [CVE-2022-2421](https://github.com/advisories/GHSA-qm95-pgcg-qqfq).
### Dependencies
- [`engine.io@~6.2.1`](https://github.com/socketio/engine.io/releases/tag/6.2.1) ([diff](https://github.com/socketio/engine.io/compare/6.2.0...6.2.1))
- [`ws@~8.2.3`](https://github.com/websockets/ws/releases/tag/8.2.3) (no change)
## [4.5.3](https://github.com/socketio/socket.io/compare/4.5.2...4.5.3) (2022-10-15)
### Bug Fixes
* **typings:** accept an HTTP2 server in the constructor ([d3d0a2d](https://github.com/socketio/socket.io/commit/d3d0a2d5beaff51fd145f810bcaf6914213f8a06))
* **typings:** apply types to "io.timeout(...).emit()" calls ([e357daf](https://github.com/socketio/socket.io/commit/e357daf5858560bc84e7e50cd36f0278d6721ea1))
### Dependencies
- [`engine.io@~6.2.0`](https://github.com/socketio/engine.io/releases/tag/6.2.1) (no change)
- [`ws@~8.2.3`](https://github.com/websockets/ws/releases/tag/8.2.3) (no change)
## [4.5.2](https://github.com/socketio/socket.io/compare/4.5.1...4.5.2) (2022-09-02)
### Bug Fixes
* prevent the socket from joining a room after disconnection ([18f3fda](https://github.com/socketio/socket.io/commit/18f3fdab12947a9fee3e9c37cfc1da97027d1473))
* **uws:** prevent the server from crashing after upgrade ([ba497ee](https://github.com/socketio/socket.io/commit/ba497ee3eb52c4abf1464380d015d8c788714364))
### Dependencies
- [`engine.io@~6.2.0`](https://github.com/socketio/engine.io/releases/tag/6.2.0) (no change)
- [`ws@~8.2.3`](https://github.com/websockets/ws/releases/tag/8.2.3) (no change)
# [2.5.0](https://github.com/socketio/socket.io/compare/2.4.1...2.5.0) (2022-06-26)
⚠️ WARNING ⚠️
The default value of the `maxHttpBufferSize` option has been decreased from 100 MB to 1 MB, in order to prevent attacks by denial of service.
Security advisory: [GHSA-j4f2-536g-r55m](https://github.com/advisories/GHSA-j4f2-536g-r55m)
### Bug Fixes
* fix race condition in dynamic namespaces ([05e1278](https://github.com/socketio/socket.io/commit/05e1278cfa99f3ecf3f8f0531ffe57d850e9a05b))
* ignore packet received after disconnection ([22d4bdf](https://github.com/socketio/socket.io/commit/22d4bdf00d1a03885dc0171125faddfaef730066))
* only set 'connected' to true after middleware execution ([226cc16](https://github.com/socketio/socket.io/commit/226cc16165f9fe60f16ff4d295fb91c8971cde35))
* prevent the socket from joining a room after disconnection ([f223178](https://github.com/socketio/socket.io/commit/f223178eb655a7713303b21a78f9ef9e161d6458))
### Dependencies
- [`engine.io@~3.6.0`](https://github.com/socketio/engine.io/releases/tag/3.6.0) (https://github.com/socketio/engine.io/compare/3.5.0...3.6.0)
- [`ws@~7.4.2`](https://github.com/websockets/ws/releases/tag/7.4.2) (no change)
## [4.5.1](https://github.com/socketio/socket.io/compare/4.5.0...4.5.1) (2022-05-17)
### Bug Fixes
* forward the local flag to the adapter when using fetchSockets() ([30430f0](https://github.com/socketio/socket.io/commit/30430f0985f8e7c49394543d4c84913b6a15df60))
* **typings:** add HTTPS server to accepted types ([#4351](https://github.com/socketio/socket.io/issues/4351)) ([9b43c91](https://github.com/socketio/socket.io/commit/9b43c9167cff817c60fa29dbda2ef7cd938aff51))
### Dependencies
- [`engine.io@~6.2.0`](https://github.com/socketio/engine.io/releases/tag/6.2.0) (no change)
- [`ws@~8.2.3`](https://github.com/websockets/ws/releases/tag/8.2.3) (no change)
# [4.5.0](https://github.com/socketio/socket.io/compare/4.4.1...4.5.0) (2022-04-23)
### Bug Fixes
* **typings:** ensure compatibility with TypeScript 3.x ([#4259](https://github.com/socketio/socket.io/issues/4259)) ([02c87a8](https://github.com/socketio/socket.io/commit/02c87a85614e217b8e7b93753f315790ae9d99f6))
### Features
#### Catch-all listeners for outgoing packets
This is similar to `onAny()`, but for outgoing packets.
Syntax:
```js
socket.onAnyOutgoing((event, ...args) => {
console.log(event);
});
```
Added in [531104d](https://github.com/socketio/socket.io/commit/531104d332690138b7aab84d5583d6204132c8b4).
#### Broadcast and expect multiple acknowledgements
Syntax:
```js
io.timeout(1000).emit("some-event", (err, responses) => {
// ...
});
```
Added in [8b20457](https://github.com/socketio/socket.io/commit/8b204570a94979bbec307f23ca078f30f5cf07b0).
#### `maxHttpBufferSize` value negotiation
A "maxPayload" field is now included in the Engine.IO handshake, so that clients in HTTP long-polling can decide how many packets they have to send to stay under the `maxHttpBufferSize` value.
This is a backward compatible change which should not mandate a new major revision of the protocol (we stay in v4), as we only add a field in the JSON-encoded handshake data:
```
0{"sid":"lv_VI97HAXpY6yYWAAAC","upgrades":["websocket"],"pingInterval":25000,"pingTimeout":5000,"maxPayload":1000000}
```
Added in [088dcb4](https://github.com/socketio/engine.io/commit/088dcb4dff60df39785df13d0a33d3ceaa1dff38).
### Dependencies
- [`engine.io@~6.2.0`](https://github.com/socketio/engine.io/releases/tag/6.2.0) (https://github.com/socketio/engine.io/compare/6.1.0...6.2.0)
- [`ws@~8.2.3`](https://github.com/websockets/ws/releases/tag/8.2.3) (no change)
## [4.4.1](https://github.com/socketio/socket.io/compare/4.4.0...4.4.1) (2022-01-06)
@@ -757,16 +178,6 @@ Added in [088dcb4](https://github.com/socketio/engine.io/commit/088dcb4dff60df39
* allow integers as event names ([1c220dd](https://github.com/socketio/socket.io-parser/commit/1c220ddbf45ea4b44bc8dbf6f9ae245f672ba1b9))
## [2.4.1](https://github.com/socketio/socket.io/compare/2.4.0...2.4.1) (2021-01-07)
### Reverts
* fix(security): do not allow all origins by default ([a169050](https://github.com/socketio/socket.io/commit/a1690509470e9dd5559cec4e60908ca6c23e9ba0))
## [3.0.5](https://github.com/socketio/socket.io/compare/3.0.4...3.0.5) (2021-01-05)
@@ -780,17 +191,6 @@ Added in [088dcb4](https://github.com/socketio/engine.io/commit/088dcb4dff60df39
* restore the socket middleware functionality ([bf54327](https://github.com/socketio/socket.io/commit/bf5432742158e4d5ba2722cff4a614967dffa5b9))
# [2.4.0](https://github.com/socketio/socket.io/compare/2.3.0...2.4.0) (2021-01-04)
### Bug Fixes
* **security:** do not allow all origins by default ([f78a575](https://github.com/socketio/socket.io/commit/f78a575f66ab693c3ea96ea88429ddb1a44c86c7))
* properly overwrite the query sent in the handshake ([d33a619](https://github.com/socketio/socket.io/commit/d33a619905a4905c153d4fec337c74da5b533a9e))
## [3.0.4](https://github.com/socketio/socket.io/compare/3.0.3...3.0.4) (2020-12-07)
@@ -908,7 +308,7 @@ new Server(3000, {
const socket = io("/admin");
// server-side
io.on("connection", socket => {
io.on("connect", socket => {
// not triggered anymore
})
@@ -1059,7 +459,7 @@ new Server(3000, {
const socket = io("/admin");
// server-side
io.on("connection", socket => {
io.on("connect", socket => {
// not triggered anymore
})
@@ -1076,78 +476,3 @@ io.of("/admin").use((socket, next) => {
This method was kept for backward-compatibility with pre-1.0 versions.
# [2.3.0](https://github.com/socketio/socket.io/compare/2.2.0...2.3.0) (2019-09-20)
This release mainly contains a bump of the `engine.io` and `ws` packages, but no additional features.
# [2.2.0](https://github.com/socketio/socket.io/compare/2.1.1...2.2.0) (2018-11-29)
### Features
- add cache-control header when serving the client source ([#2907](https://github.com/socketio/socket.io/pull/2907)) ([b00ae50](https://github.com/socketio/socket.io/commit/b00ae50be65d1bc88fa95145f1c486a6886a6b76))
### Bug fixes
- throw an error when trying to access the clients of a dynamic namespace ([#3355](https://github.com/socketio/socket.io/pull/3355)) ([a7fbd1a](https://github.com/socketio/socket.io/commit/a7fbd1ac4a47cafd832fc62e371754df924c5903))
# [2.1.1](https://github.com/socketio/socket.io/compare/2.1.0...2.1.1) (2018-05-17)
### Features
- add local flag to the socket object ([#3129](https://github.com/socketio/socket.io/pull/3219)) ([1decae3](https://github.com/socketio/socket.io/commit/1decae341c80c0417b32d3124ca30c005240b48a))
```js
socket.local.to('room101').emit(/* */);
```
# [2.1.0](https://github.com/socketio/socket.io/compare/2.1.1...2.2.0) (2018-03-29)
### Features
- add a 'binary' flag ([#3185](https://github.com/socketio/socket.io/pull/3185)) ([f48a06c](https://github.com/socketio/socket.io/commit/f48a06c040280b44f90fd225c888910544fd63b5))
```js
// by default, the object is recursively scanned to check whether it contains some binary data
// in the following example, the check is skipped in order to improve performance
socket.binary(false).emit('plain-object', object);
// it also works at the namespace level
io.binary(false).emit('plain-object', object);
```
- add support for dynamic namespaces ([#3195](https://github.com/socketio/socket.io/pull/3195)) ([c0c79f0](https://github.com/socketio/socket.io/commit/c0c79f019e7138194e438339f8192705957c8ec3))
```js
io.of(/^\/dynamic-\d+$/).on('connect', (socket) => {
// socket.nsp.name = '/dynamic-101'
});
// client-side
const client = require('socket.io-client')('/dynamic-101');
```
### Bug fixes
- properly emit 'connect' when using a custom namespace ([#3197](https://github.com/socketio/socket.io/pull/3197)) ([f4fc517](https://github.com/socketio/socket.io/commit/f4fc517e0fe25866c95b584291487b8cbdff889d))
- include the protocol in the origins check ([#3198](https://github.com/socketio/socket.io/pull/3198)) ([1f1d64b](https://github.com/socketio/socket.io/commit/1f1d64bab61a273712a199591a3f76210d8c0959))
### Important note :warning: from Engine.IO [3.2.0 release](https://github.com/socketio/engine.io/releases/tag/3.2.0)
There are two non-breaking changes that are somehow quite important:
- `ws` was reverted as the default wsEngine (https://github.com/socketio/engine.io/pull/550), as there was several blocking issues with `uws`. You can still use `uws` by running `npm install uws --save` in your project and using the `wsEngine` option:
```js
var engine = require('engine.io');
var server = engine.listen(3000, {
wsEngine: 'uws'
});
```
- `pingTimeout` now defaults to 5 seconds (instead of 60 seconds): https://github.com/socketio/engine.io/pull/551

View File

@@ -21,8 +21,6 @@ Some implementations in other languages are also available:
- [Dart](https://github.com/rikulo/socket.io-client-dart)
- [Python](https://github.com/miguelgrinberg/python-socketio)
- [.NET](https://github.com/doghappy/socket.io-client-csharp)
- [Rust](https://github.com/1c3t3a/rust-socketio)
- [PHP](https://github.com/ElephantIO/elephant.io)
Its main features are:
@@ -127,7 +125,7 @@ io.listen(3000);
Starting with **3.0**, express applications have become request handler
functions that you pass to `http` or `http` `Server` instances. You need
to pass the `Server` to `socket.io`, not the express application
to pass the `Server` to `socket.io`, and not the express application
function. Also make sure to call `.listen` on the `server`, not the `app`.
```js

View File

@@ -1,22 +0,0 @@
# Security Policy
## Supported Versions
| Version | Supported |
|---------|--------------------|
| 4.x | :white_check_mark: |
| 3.x | :white_check_mark: |
| 2.4.x | :white_check_mark: |
| < 2.4.0 | :x: |
## Reporting a Vulnerability
To report a security vulnerability in this package, please send an email to [@darrachequesne](https://github.com/darrachequesne) (see address in profile) describing the vulnerability and how to reproduce it.
We will get back to you as soon as possible and publish a fix if necessary.
:warning: IMPORTANT :warning: please do not create an issue in this repository, as attackers might take advantage of it. Thank you in advance for your responsible disclosure.
## History
No security vulnerability were reported yet.

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("connection", (socket) => {
io.on("connect", (socket) => {
socket.emit("todos", todos);
// note: we could also create a CRUD (create/read/update/delete) service for the todo list

View File

@@ -2,13 +2,6 @@
Please read the related [guide](https://socket.io/get-started/basic-crud-application/).
This repository contains several implementations of the server:
| Directory | Language | Database | Cluster? |
|----------------------------|------------|------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------|
| `server/` | TypeScript | in-memory | No |
| `server-postgres-cluster/` | JavaScript | Postgres, with the [Postgres adapter](https://socket.io/docs/v4/postgres-adapter/) | Yes, with the [`@socket.io/sticky`](https://github.com/socketio/socket.io-sticky) module) |
## Running the frontend
```

View File

@@ -0,0 +1,17 @@
# 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,18 +1,21 @@
# 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
# Node
# dependencies
/node_modules
npm-debug.log
yarn-error.log
# profiling files
chrome-profiler-events*.json
speed-measure-plugin*.json
# IDEs and editors
.idea/
/.idea
.project
.classpath
.c9/
@@ -20,7 +23,7 @@ yarn-error.log
.settings/
*.sublime-workspace
# Visual Studio Code
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
@@ -28,15 +31,16 @@ yarn-error.log
!.vscode/extensions.json
.history/*
# Miscellaneous
/.angular/cache
.sass-cache/
# misc
/.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,10 +1,14 @@
# AngularClient
# Angular TodoMVC + Socket.IO
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 17.0.2.
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)
## Development server
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.
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.
## Code scaffolding
@@ -12,7 +16,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.
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build.
## Running unit tests
@@ -20,7 +24,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 a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
## Further help

View File

@@ -3,23 +3,26 @@
"version": 1,
"newProjectRoot": "projects",
"projects": {
"angular-client": {
"angular-todomvc": {
"projectType": "application",
"schematics": {},
"schematics": {
"@schematics/angular:application": {
"strict": true
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:application",
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/angular-client",
"outputPath": "dist/angular-todomvc",
"index": "src/index.html",
"browser": "src/main.ts",
"polyfills": [
"zone.js"
],
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"aot": true,
"assets": [
"src/favicon.ico",
"src/assets"
@@ -31,6 +34,19 @@
},
"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",
@@ -42,49 +58,34 @@
"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": {
"buildTarget": "angular-client:build:production"
},
"development": {
"buildTarget": "angular-client:build:development"
"browserTarget": "angular-todomvc:build:production"
}
},
"defaultConfiguration": "development"
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"buildTarget": "angular-client:build"
"browserTarget": "angular-todomvc:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"polyfills": [
"zone.js",
"zone.js/testing"
],
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"assets": [
"src/favicon.ico",
"src/assets"
@@ -94,8 +95,34 @@
],
"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.

After

Width:  |  Height:  |  Size: 205 KiB

View File

@@ -0,0 +1,37 @@
// @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

@@ -0,0 +1,23 @@
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

@@ -0,0 +1,11 @@
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

@@ -0,0 +1,13 @@
/* 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

@@ -0,0 +1,44 @@
// 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,40 +1,46 @@
{
"name": "angular-client",
"name": "angular-todomvc",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test"
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e"
},
"private": true,
"dependencies": {
"@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"
"@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"
},
"devDependencies": {
"@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"
"@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"
}
}

View File

@@ -1,8 +1,7 @@
<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="" [formControl]="newTodoText" (keyup.enter)="addTodo()">
<input class="new-todo" placeholder="What needs to be done?" autofocus="" [(ngModel)]="newTodoText" (keyup.enter)="addTodo()">
</header>
<section class="main" *ngIf="todoStore.todos.length > 0">
<input id="toggle-all" class="toggle-all" type="checkbox" *ngIf="todoStore.todos.length" #toggleall [checked]="todoStore.allCompleted()" (click)="todoStore.setAllTo(toggleall.checked)">

View File

@@ -4,7 +4,9 @@ import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [AppComponent],
declarations: [
AppComponent
],
}).compileComponents();
});
@@ -14,16 +16,16 @@ describe('AppComponent', () => {
expect(app).toBeTruthy();
});
it(`should have the 'angular-client' title`, () => {
it(`should have as title 'angular-todomvc'`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app.title).toEqual('angular-client');
expect(app.title).toEqual('angular-todomvc');
});
it('should render title', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, angular-client');
const compiled = fixture.nativeElement;
expect(compiled.querySelector('.content span').textContent).toContain('angular-todomvc app is running!');
});
});

View File

@@ -1,21 +1,17 @@
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterOutlet } from '@angular/router';
import {type Todo, TodoStore} from "./store";
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { TodoStore, Todo } from './store';
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, RouterOutlet, ReactiveFormsModule],
templateUrl: './app.component.html',
styleUrl: './app.component.css',
providers: [TodoStore]
styleUrls: ['./app.component.css']
})
export class AppComponent {
newTodoText = new FormControl('');
todoStore: TodoStore;
newTodoText = '';
constructor(readonly todoStore: TodoStore) {
constructor(todoStore: TodoStore) {
this.todoStore = todoStore;
}
stopEditing(todo: Todo, editedTitle: string) {
@@ -55,9 +51,9 @@ export class AppComponent {
}
addTodo() {
if (this.newTodoText.value?.trim().length) {
this.todoStore.add(this.newTodoText.value!);
this.newTodoText.setValue('');
if (this.newTodoText.trim().length) {
this.todoStore.add(this.newTodoText);
this.newTodoText = '';
}
}
}

View File

@@ -1,8 +0,0 @@
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [provideRouter(routes)]
};

View File

@@ -0,0 +1,19 @@
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

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

View File

@@ -1,7 +1,6 @@
import { io, Socket } from "socket.io-client";
import { ClientEvents, ServerEvents } from "../../../common/events";
import { ClientEvents, ServerEvents } from "../../../server/lib/events";
import { environment } from '../environments/environment';
import {Injectable} from "@angular/core";
export interface Todo {
id: string,
@@ -19,7 +18,6 @@ const mapTodo = (todo: any) => {
}
}
@Injectable()
export class TodoStore {
public todos: Array<Todo> = [];
private socket: Socket<ServerEvents, ClientEvents>;

View File

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

View File

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

View File

@@ -1,3 +1,17 @@
// 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 = {
serverUrl: "https://my-custom-domain.com"
production: false,
serverUrl: "http://localhost:3000"
};
/*
* 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: 15 KiB

After

Width:  |  Height:  |  Size: 948 B

View File

@@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="utf-8">
<title>AngularClient</title>
<title>Angular Todo MVC</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">

View File

@@ -1,6 +1,12 @@
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
bootstrapApplication(AppComponent, appConfig)
.catch((err) => console.error(err));
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));

View File

@@ -0,0 +1,63 @@
/**
* 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

@@ -0,0 +1,25 @@
// 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,7 +6,8 @@
"types": []
},
"files": [
"src/main.ts"
"src/main.ts",
"src/polyfills.ts"
],
"include": [
"src/**/*.d.ts"

View File

@@ -2,29 +2,26 @@
{
"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": "ES2022",
"module": "ES2022",
"useDefineForClassFields": false,
"target": "es2015",
"module": "es2020",
"lib": [
"ES2022",
"es2018",
"dom"
]
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true

View File

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

View File

@@ -0,0 +1,152 @@
{
"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,16 +0,0 @@
A basic TODO project.
| Characteristic | |
|----------------|-------------------------------------------------------------------------------------------|
| Language | plain JavaScript |
| Database | Postgres, with the [Postgres adapter](https://socket.io/docs/v4/postgres-adapter/) |
| Cluster? | Yes, with the [`@socket.io/sticky`](https://github.com/socketio/socket.io-sticky) module) |
## Usage
```
$ docker-compose up -d
$ npm install
$ npm start
```

View File

@@ -1,9 +0,0 @@
version: "3"
services:
postgres:
image: postgres:12
ports:
- "5432:5432"
environment:
POSTGRES_PASSWORD: "changeit"

View File

@@ -1,26 +0,0 @@
import { Server } from "socket.io";
import createTodoHandlers from "./todo-management/todo.handlers.js";
import { setupWorker } from "@socket.io/sticky";
import { createAdapter } from "@socket.io/postgres-adapter";
export function createApplication(httpServer, components, serverOptions = {}) {
const io = new Server(httpServer, serverOptions);
const { createTodo, readTodo, updateTodo, deleteTodo, listTodo } =
createTodoHandlers(components);
io.on("connection", (socket) => {
socket.on("todo:create", createTodo);
socket.on("todo:read", readTodo);
socket.on("todo:update", updateTodo);
socket.on("todo:delete", deleteTodo);
socket.on("todo:list", listTodo);
});
// enable sticky session in the cluster (to remove in standalone mode)
setupWorker(io);
io.adapter(createAdapter(components.connectionPool));
return io;
}

View File

@@ -1,28 +0,0 @@
import cluster from "cluster";
import { createServer } from "http";
import { setupMaster } from "@socket.io/sticky";
import { cpus } from "os";
if (cluster.isMaster) {
console.log(`Master ${process.pid} is running`);
const httpServer = createServer();
setupMaster(httpServer, {
loadBalancingMethod: "least-connection",
});
httpServer.listen(3000);
for (let i = 0; i < cpus().length; i++) {
cluster.fork();
}
cluster.on("exit", (worker) => {
console.log(`Worker ${worker.process.pid} died`);
cluster.fork();
});
} else {
console.log(`Worker ${process.pid} started`);
import("./index.js");
}

View File

@@ -1,51 +0,0 @@
import { createServer } from "http";
import { createApplication } from "./app.js";
import { Sequelize } from "sequelize";
import pg from "pg";
import { PostgresTodoRepository } from "./todo-management/todo.repository.js";
const httpServer = createServer();
const sequelize = new Sequelize("postgres", "postgres", "changeit", {
dialect: "postgres",
});
const connectionPool = new pg.Pool({
user: "postgres",
host: "localhost",
database: "postgres",
password: "changeit",
port: 5432,
});
createApplication(
httpServer,
{
connectionPool,
todoRepository: new PostgresTodoRepository(sequelize),
},
{
cors: {
origin: ["http://localhost:4200"],
},
}
);
const main = async () => {
// create the tables if they do not exist already
await sequelize.sync();
// create the table needed by the postgres adapter
await connectionPool.query(`
CREATE TABLE IF NOT EXISTS socket_io_attachments (
id bigserial UNIQUE,
created_at timestamptz DEFAULT NOW(),
payload bytea
);
`);
// uncomment when running in standalone mode
// httpServer.listen(3000);
};
main();

View File

@@ -1,140 +0,0 @@
import { Errors, mapErrorDetails, sanitizeErrorMessage } from "../util.js";
import { v4 as uuid } from "uuid";
import Joi from "joi";
const idSchema = Joi.string().guid({
version: "uuidv4",
});
const todoSchema = Joi.object({
id: idSchema.alter({
create: (schema) => schema.forbidden(),
update: (schema) => schema.required(),
}),
title: Joi.string().max(256).required(),
completed: Joi.boolean().required(),
});
export default function (components) {
const { todoRepository } = components;
return {
createTodo: async function (payload, callback) {
const socket = this;
// validate the payload
const { error, value } = todoSchema.tailor("create").validate(payload, {
abortEarly: false,
stripUnknown: true,
});
if (error) {
return callback({
error: Errors.INVALID_PAYLOAD,
errorDetails: mapErrorDetails(error.details),
});
}
value.id = uuid();
// persist the entity
try {
await todoRepository.save(value);
} catch (e) {
return callback({
error: sanitizeErrorMessage(e),
});
}
// acknowledge the creation
callback({
data: value.id,
});
// notify the other users
socket.broadcast.emit("todo:created", value);
},
readTodo: async function (id, callback) {
const { error } = idSchema.validate(id);
if (error) {
return callback({
error: Errors.ENTITY_NOT_FOUND,
});
}
try {
const todo = await todoRepository.findById(id);
callback({
data: todo,
});
} catch (e) {
callback({
error: sanitizeErrorMessage(e),
});
}
},
updateTodo: async function (payload, callback) {
const socket = this;
const { error, value } = todoSchema.tailor("update").validate(payload, {
abortEarly: false,
stripUnknown: true,
});
if (error) {
return callback({
error: Errors.INVALID_PAYLOAD,
errorDetails: mapErrorDetails(error.details),
});
}
try {
await todoRepository.save(value);
} catch (e) {
return callback({
error: sanitizeErrorMessage(e),
});
}
callback();
socket.broadcast.emit("todo:updated", value);
},
deleteTodo: async function (id, callback) {
const socket = this;
const { error } = idSchema.validate(id);
if (error) {
return callback({
error: Errors.ENTITY_NOT_FOUND,
});
}
try {
await todoRepository.deleteById(id);
} catch (e) {
return callback({
error: sanitizeErrorMessage(e),
});
}
callback();
socket.broadcast.emit("todo:deleted", id);
},
listTodo: async function (callback) {
try {
callback({
data: await todoRepository.findAll(),
});
} catch (e) {
callback({
error: sanitizeErrorMessage(e),
});
}
},
};
}

View File

@@ -1,74 +0,0 @@
import { Errors } from "../util.js";
import { Model, DataTypes } from "sequelize";
class CrudRepository {
findAll() {}
findById(id) {}
save(entity) {}
deleteById(id) {}
}
export class TodoRepository extends CrudRepository {}
class Todo extends Model {}
export class PostgresTodoRepository extends TodoRepository {
constructor(sequelize) {
super();
this.sequelize = sequelize;
Todo.init(
{
id: {
type: DataTypes.STRING,
primaryKey: true,
allowNull: false,
},
title: {
type: DataTypes.STRING,
},
completed: {
type: DataTypes.BOOLEAN,
},
},
{
sequelize,
tableName: "todos",
}
);
}
findAll() {
return this.sequelize.transaction((transaction) => {
return Todo.findAll({ transaction });
});
}
async findById(id) {
return this.sequelize.transaction(async (transaction) => {
const todo = await Todo.findByPk(id, { transaction });
if (!todo) {
throw Errors.ENTITY_NOT_FOUND;
}
return todo;
});
}
save(entity) {
return this.sequelize.transaction((transaction) => {
return Todo.upsert(entity, { transaction });
});
}
async deleteById(id) {
return this.sequelize.transaction(async (transaction) => {
const count = await Todo.destroy({ where: { id }, transaction });
if (count === 0) {
throw Errors.ENTITY_NOT_FOUND;
}
});
}
}

View File

@@ -1,22 +0,0 @@
export const Errors = {
ENTITY_NOT_FOUND: "entity not found",
INVALID_PAYLOAD: "invalid payload",
};
const errorValues = Object.values(Errors);
export function sanitizeErrorMessage(message) {
if (typeof message === "string" && errorValues.includes(message)) {
return message;
} else {
return "an unknown error has occurred";
}
}
export function mapErrorDetails(details) {
return details.map((item) => ({
message: item.message,
path: item.path,
type: item.type,
}));
}

View File

@@ -1,30 +0,0 @@
{
"name": "basic-crud-server",
"version": "0.0.1",
"description": "Server for the Basic CRUD Socket.IO example (with Postgres and multiple Socket.IO servers)",
"main": "lib/cluster.js",
"type": "module",
"scripts": {
"start": "node lib/cluster.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/socketio/socket.io.git"
},
"author": "Damien Arrachequesne <damien.arrachequesne@gmail.com>",
"license": "MIT",
"bugs": {
"url": "https://github.com/socketio/socket.io/issues"
},
"homepage": "https://github.com/socketio/socket.io#readme",
"dependencies": {
"@socket.io/postgres-adapter": "^0.2.0",
"@socket.io/sticky": "^1.0.1",
"joi": "^17.4.0",
"pg": "^8.7.3",
"pg-hstore": "^2.3.4",
"sequelize": "^6.18.0",
"socket.io": "^4.0.1",
"uuid": "^8.3.2"
}
}

View File

@@ -1,6 +1,6 @@
import { Server as HttpServer } from "http";
import { Server, ServerOptions } from "socket.io";
import { ClientEvents, ServerEvents } from "../../common/events";
import { ClientEvents, ServerEvents } from "./events";
import { TodoRepository } from "./todo-management/todo.repository";
import createTodoHandlers from "./todo-management/todo.handlers";

View File

@@ -1,18 +1,9 @@
export type TodoID = string;
export interface Todo {
id: TodoID;
completed: boolean;
title: string;
}
import { Todo, TodoID } from "./todo-management/todo.repository";
import { ValidationErrorItem } from "joi";
interface Error {
error: string;
errorDetails?: {
message: string;
path: Array<string | number>;
type: string;
}[];
errorDetails?: ValidationErrorItem[];
}
interface Success<T> {

View File

@@ -2,13 +2,8 @@ import { Errors, mapErrorDetails, sanitizeErrorMessage } from "../util";
import { v4 as uuid } from "uuid";
import { Components } from "../app";
import Joi = require("joi");
import {
Todo,
TodoID,
ClientEvents,
Response,
ServerEvents,
} from "../../../common/events";
import { Todo, TodoID } from "./todo.repository";
import { ClientEvents, Response, ServerEvents } from "../events";
import { Socket } from "socket.io";
const idSchema = Joi.string().guid({
@@ -24,7 +19,8 @@ const todoSchema = Joi.object({
completed: Joi.boolean().required(),
});
export default function ({ todoRepository }: Components) {
export default function (components: Components) {
const { todoRepository } = components;
return {
createTodo: async function (
payload: Omit<Todo, "id">,

View File

@@ -1,5 +1,4 @@
import { Errors } from "../util";
import { Todo, TodoID } from "../../../common/events";
abstract class CrudRepository<T, ID> {
abstract findAll(): Promise<T[]>;
@@ -8,6 +7,14 @@ 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

@@ -24,14 +24,14 @@
"uuid": "^8.3.2"
},
"devDependencies": {
"@types/mocha": "^8.2.3",
"@types/chai": "^4.2.16",
"@types/mocha": "^10.0.0",
"@types/uuid": "^8.3.0",
"chai": "^4.3.4",
"mocha": "^10.0.0",
"mocha": "^8.3.2",
"nyc": "^15.1.0",
"socket.io-client": "^4.0.1",
"ts-node": "^10.9.1",
"ts-node": "^9.1.1",
"typescript": "^4.2.4"
}
}

View File

@@ -1,23 +0,0 @@
.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

@@ -1,24 +0,0 @@
# 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

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

View File

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

View File

@@ -1,45 +0,0 @@
{
"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.

Before

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -1,18 +0,0 @@
<!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

@@ -1,381 +0,0 @@
/* imported from node_modules/todomvc-app-css/index.css */
html,
body {
margin: 0;
padding: 0;
}
button {
margin: 0;
padding: 0;
border: 0;
background: none;
font-size: 100%;
vertical-align: baseline;
font-family: inherit;
font-weight: inherit;
color: inherit;
-webkit-appearance: none;
appearance: none;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
line-height: 1.4em;
background: #f5f5f5;
color: #111111;
min-width: 230px;
max-width: 550px;
margin: 0 auto;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-weight: 300;
}
:focus {
outline: 0;
}
.hidden {
display: none;
}
.todoapp {
background: #fff;
margin: 130px 0 40px 0;
position: relative;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.todoapp input::-webkit-input-placeholder {
font-style: italic;
font-weight: 300;
color: rgba(0, 0, 0, 0.4);
}
.todoapp input::-moz-placeholder {
font-style: italic;
font-weight: 300;
color: rgba(0, 0, 0, 0.4);
}
.todoapp input::input-placeholder {
font-style: italic;
font-weight: 300;
color: rgba(0, 0, 0, 0.4);
}
.todoapp h1 {
position: absolute;
top: -140px;
width: 100%;
font-size: 80px;
font-weight: 200;
text-align: center;
color: #b83f45;
-webkit-text-rendering: optimizeLegibility;
-moz-text-rendering: optimizeLegibility;
text-rendering: optimizeLegibility;
}
.new-todo,
.edit {
position: relative;
margin: 0;
width: 100%;
font-size: 24px;
font-family: inherit;
font-weight: inherit;
line-height: 1.4em;
color: inherit;
padding: 6px;
border: 1px solid #999;
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.new-todo {
padding: 16px 16px 16px 60px;
border: none;
background: rgba(0, 0, 0, 0.003);
box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
}
.main {
position: relative;
z-index: 2;
border-top: 1px solid #e6e6e6;
}
.toggle-all {
width: 1px;
height: 1px;
border: none; /* Mobile Safari */
opacity: 0;
position: absolute;
right: 100%;
bottom: 100%;
}
.toggle-all + label {
width: 60px;
height: 34px;
font-size: 0;
position: absolute;
top: -52px;
left: -13px;
-webkit-transform: rotate(90deg);
transform: rotate(90deg);
}
.toggle-all + label:before {
content: '';
font-size: 22px;
color: #e6e6e6;
padding: 10px 27px 10px 27px;
}
.toggle-all:checked + label:before {
color: #737373;
}
.todo-list {
margin: 0;
padding: 0;
list-style: none;
}
.todo-list li {
position: relative;
font-size: 24px;
border-bottom: 1px solid #ededed;
}
.todo-list li:last-child {
border-bottom: none;
}
.todo-list li.editing {
border-bottom: none;
padding: 0;
}
.todo-list li.editing .edit {
display: block;
width: calc(100% - 43px);
padding: 12px 16px;
margin: 0 0 0 43px;
}
.todo-list li.editing .view {
display: none;
}
.todo-list li .toggle {
text-align: center;
width: 40px;
/* auto, since non-WebKit browsers doesn't support input styling */
height: auto;
position: absolute;
top: 0;
bottom: 0;
margin: auto 0;
border: none; /* Mobile Safari */
-webkit-appearance: none;
appearance: none;
}
.todo-list li .toggle {
opacity: 0;
}
.todo-list li .toggle + label {
/*
Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433
IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/
*/
background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E');
background-repeat: no-repeat;
background-position: center left;
}
.todo-list li .toggle:checked + label {
background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E');
}
.todo-list li label {
word-break: break-all;
padding: 15px 15px 15px 60px;
display: block;
line-height: 1.2;
transition: color 0.4s;
font-weight: 400;
color: #4d4d4d;
}
.todo-list li.completed label {
color: #cdcdcd;
text-decoration: line-through;
}
.todo-list li .destroy {
display: none;
position: absolute;
top: 0;
right: 10px;
bottom: 0;
width: 40px;
height: 40px;
margin: auto 0;
font-size: 30px;
color: #cc9a9a;
margin-bottom: 11px;
transition: color 0.2s ease-out;
}
.todo-list li .destroy:hover {
color: #af5b5e;
}
.todo-list li .destroy:after {
content: '×';
}
.todo-list li:hover .destroy {
display: block;
}
.todo-list li .edit {
display: none;
}
.todo-list li.editing:last-child {
margin-bottom: -1px;
}
.footer {
padding: 10px 15px;
height: 20px;
text-align: center;
font-size: 15px;
border-top: 1px solid #e6e6e6;
}
.footer:before {
content: '';
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 50px;
overflow: hidden;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
0 8px 0 -3px #f6f6f6,
0 9px 1px -3px rgba(0, 0, 0, 0.2),
0 16px 0 -6px #f6f6f6,
0 17px 2px -6px rgba(0, 0, 0, 0.2);
}
.todo-count {
float: left;
text-align: left;
}
.todo-count strong {
font-weight: 300;
}
.filters {
margin: 0;
padding: 0;
list-style: none;
position: absolute;
right: 0;
left: 0;
}
.filters li {
display: inline;
}
.filters li a {
color: inherit;
margin: 3px;
padding: 3px 7px;
text-decoration: none;
border: 1px solid transparent;
border-radius: 3px;
}
.filters li a:hover {
border-color: rgba(175, 47, 47, 0.1);
}
.filters li a.selected {
border-color: rgba(175, 47, 47, 0.2);
}
.clear-completed,
html .clear-completed:active {
float: right;
position: relative;
line-height: 20px;
text-decoration: none;
cursor: pointer;
}
.clear-completed:hover {
text-decoration: underline;
}
.info {
margin: 65px auto 0;
color: #4d4d4d;
font-size: 11px;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
text-align: center;
}
.info p {
line-height: 1;
}
.info a {
color: inherit;
text-decoration: none;
font-weight: 400;
}
.info a:hover {
text-decoration: underline;
}
/*
Hack to remove background from Mobile Safari.
Can't use it globally since it destroys checkboxes in Firefox
*/
@media screen and (-webkit-min-device-pixel-ratio:0) {
.toggle-all,
.todo-list li .toggle {
background: none;
}
.todo-list li .toggle {
height: 40px;
}
}
@media (max-width: 430px) {
.footer {
height: 50px;
}
.filters {
bottom: 10px;
}
}

View File

@@ -1,123 +0,0 @@
<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.

Before

Width:  |  Height:  |  Size: 6.7 KiB

View File

@@ -1,9 +0,0 @@
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

@@ -1,7 +0,0 @@
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

@@ -1,106 +0,0 @@
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

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +0,0 @@
# 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

@@ -1 +0,0 @@
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

@@ -1,17 +0,0 @@
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

@@ -1,18 +0,0 @@
{
"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

@@ -1,10 +0,0 @@
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

@@ -1,273 +0,0 @@
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

@@ -1,162 +0,0 @@
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,53 +1,51 @@
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"
server-john:
build: ./server
links:
- redis
expose:
- "3000"
environment:
- NAME=John
haproxy:
build: ./haproxy
links:
- server-john
- server-paul
- server-george
- server-ringo
ports:
- "3000:80"
server-paul:
build: ./server
links:
- redis
expose:
- "3000"
environment:
- NAME=Paul
server-john:
build: ./server
links:
- redis
expose:
- "3000"
environment:
- NAME=John
server-george:
build: ./server
links:
- redis
expose:
- "3000"
environment:
- NAME=George
server-paul:
build: ./server
links:
- redis
expose:
- "3000"
environment:
- NAME=Paul
server-ringo:
build: ./server
links:
- redis
expose:
- "3000"
environment:
- NAME=Ringo
server-george:
build: ./server
links:
- redis
expose:
- "3000"
environment:
- NAME=George
redis:
image: redis:alpine
expose:
- "6379"
server-ringo:
build: ./server
links:
- redis
expose:
- "3000"
environment:
- NAME=Ringo
redis:
image: redis:alpine
expose:
- "6379"

View File

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

View File

@@ -1,53 +1,51 @@
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"
server-john:
build: ./server
links:
- redis
expose:
- "3000"
environment:
- NAME=John
httpd:
build: ./httpd
links:
- server-john
- server-paul
- server-george
- server-ringo
ports:
- "3000:80"
server-paul:
build: ./server
links:
- redis
expose:
- "3000"
environment:
- NAME=Paul
server-john:
build: ./server
links:
- redis
expose:
- "3000"
environment:
- NAME=John
server-george:
build: ./server
links:
- redis
expose:
- "3000"
environment:
- NAME=George
server-paul:
build: ./server
links:
- redis
expose:
- "3000"
environment:
- NAME=Paul
server-ringo:
build: ./server
links:
- redis
expose:
- "3000"
environment:
- NAME=Ringo
server-george:
build: ./server
links:
- redis
expose:
- "3000"
environment:
- NAME=George
redis:
image: redis:6
expose:
- "6379"
server-ringo:
build: ./server
links:
- redis
expose:
- "3000"
environment:
- NAME=Ringo
redis:
image: redis:alpine
expose:
- "6379"

View File

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

View File

@@ -51,5 +51,4 @@ RewriteRule /(.*) balancer://nodes_ws/$1 [P,L]
RewriteCond %{HTTP:Upgrade} !=websocket [NC]
RewriteRule /(.*) balancer://nodes_polling/$1 [P,L]
# must be bigger than pingInterval (25s by default) + pingTimeout (20s by default)
ProxyTimeout 60
ProxyTimeout 3

View File

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

View File

@@ -3,27 +3,18 @@ services:
image: nginx:alpine
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
links:
- server-john
- server-paul
- server-george
- server-ringo
ports:
- "3000:80"
- "3000:80"
server-john:
build: ./server
links:
- redis
expose:
- "3000"
- "3000"
environment:
- NAME=John
server-paul:
build: ./server
links:
- redis
expose:
- "3000"
environment:
@@ -31,8 +22,6 @@ services:
server-george:
build: ./server
links:
- redis
expose:
- "3000"
environment:
@@ -40,8 +29,6 @@ services:
server-ringo:
build: ./server
links:
- redis
expose:
- "3000"
environment:
@@ -49,10 +36,8 @@ services:
client:
build: ./client
links:
- nginx
redis:
image: redis:6
image: redis:alpine
expose:
- "6379"

View File

@@ -1,25 +0,0 @@
# 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.

Before

Width:  |  Height:  |  Size: 103 KiB

View File

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

View File

@@ -1,18 +0,0 @@
{
// 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

@@ -1,49 +0,0 @@
<!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

@@ -1,53 +0,0 @@
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

@@ -1,13 +0,0 @@
{
"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

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

View File

@@ -1,18 +0,0 @@
{
// 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

@@ -1,49 +0,0 @@
<!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

@@ -1,53 +0,0 @@
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

@@ -1,13 +0,0 @@
{
"name": "connection-state-recovery-example",
"version": "0.0.1",
"private": true,
"type": "module",
"description": "Example with connection state recovery",
"scripts": {
"start": "node index.js"
},
"dependencies": {
"socket.io": "^4.7.2"
}
}

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