Compare commits

..

20 Commits
4.7.1 ... 4.7.3

Author SHA1 Message Date
Damien Arrachequesne
0d893196f8 chore(release): 4.7.3
Diff: https://github.com/socketio/socket.io/compare/4.7.2...4.7.3
2024-01-03 21:33:29 +01:00
BCCSTeam
df8e70f798 fix: return the first response when broadcasting to a single socket (#4878) 2024-01-02 17:43:10 +01:00
Xì Gà
8c9ebc30e5 fix(typings): allow to bind to a non-secure Http2Server (#4853) 2023-11-22 17:48:59 +01:00
Damien Arrachequesne
efb5c21e85 docs(examples): add Vue client with CRUD example 2023-11-22 10:12:17 +01:00
Damien Arrachequesne
3848280125 docs(examples): upgrade basic-crud-application to Angular v17
Related: https://github.com/socketio/socket.io/issues/4875
2023-11-21 14:15:50 +01:00
Damien Arrachequesne
9a2a83fdd4 refactor: cleanup after merge 2023-10-11 10:45:59 +02:00
Zachary Haber
f6ef267b03 refactor(typings): improve emit types (#4817)
This commit fixes several issues with emit types:

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

- CommonJS
- ES modules
- TypeScript

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

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

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

[1]: 37474c7e67
2023-07-09 10:14:49 +02:00
Damien Arrachequesne
443e447087 docs(examples): add example with WebTransport 2023-06-29 11:21:27 +02:00
104 changed files with 8990 additions and 1089 deletions

View File

@@ -36,3 +36,33 @@ jobs:
run: npm test
env:
CI: true
build-examples:
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
example:
- custom-parsers
- typescript
- webpack-build
- webpack-build-server
- basic-crud-application/angular-client
- basic-crud-application/vue-client
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Use Node.js 20
uses: actions/setup-node@v3
with:
node-version: 20
- name: Build ${{ matrix.example }}
run: |
cd examples/${{ matrix.example }}
npm install
npm run build

View File

@@ -1,7 +1,12 @@
# History
## 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)
@@ -60,6 +65,39 @@
# Release notes
## [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.
@@ -837,7 +875,7 @@ new Server(3000, {
const socket = io("/admin");
// server-side
io.on("connect", socket => {
io.on("connection", socket => {
// not triggered anymore
})
@@ -988,7 +1026,7 @@ new Server(3000, {
const socket = io("/admin");
// server-side
io.on("connect", socket => {
io.on("connection", socket => {
// not triggered anymore
})

View File

@@ -22,6 +22,7 @@ Some implementations in other languages are also available:
- [Python](https://github.com/miguelgrinberg/python-socketio)
- [.NET](https://github.com/doghappy/socket.io-client-csharp)
- [Rust](https://github.com/1c3t3a/rust-socketio)
- [PHP](https://github.com/ElephantIO/elephant.io)
Its main features are:

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
/*!
* Socket.IO v4.7.1
* (c) 2014-2023 Guillermo Rauch
* Socket.IO v4.7.3
* (c) 2014-2024 Guillermo Rauch
* Released under the MIT License.
*/
(function (global, factory) {
@@ -466,16 +466,124 @@
}
return packets;
};
function createPacketEncoderStream() {
return new TransformStream({
transform: function transform(packet, controller) {
encodePacketToBinary(packet, function (encodedPacket) {
var payloadLength = encodedPacket.length;
var header;
// inspired by the WebSocket format: https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers#decoding_payload_length
if (payloadLength < 126) {
header = new Uint8Array(1);
new DataView(header.buffer).setUint8(0, payloadLength);
} else if (payloadLength < 65536) {
header = new Uint8Array(3);
var view = new DataView(header.buffer);
view.setUint8(0, 126);
view.setUint16(1, payloadLength);
} else {
header = new Uint8Array(9);
var _view = new DataView(header.buffer);
_view.setUint8(0, 127);
_view.setBigUint64(1, BigInt(payloadLength));
}
// first bit indicates whether the payload is plain text (0) or binary (1)
if (packet.data && typeof packet.data !== "string") {
header[0] |= 0x80;
}
controller.enqueue(header);
controller.enqueue(encodedPacket);
});
}
});
}
var TEXT_DECODER;
function decodePacketFromBinary(data, isBinary, binaryType) {
function totalLength(chunks) {
return chunks.reduce(function (acc, chunk) {
return acc + chunk.length;
}, 0);
}
function concatChunks(chunks, size) {
if (chunks[0].length === size) {
return chunks.shift();
}
var buffer = new Uint8Array(size);
var j = 0;
for (var i = 0; i < size; i++) {
buffer[i] = chunks[0][j++];
if (j === chunks[0].length) {
chunks.shift();
j = 0;
}
}
if (chunks.length && j < chunks[0].length) {
chunks[0] = chunks[0].slice(j);
}
return buffer;
}
function createPacketDecoderStream(maxPayload, binaryType) {
if (!TEXT_DECODER) {
// lazily created for compatibility with old browser platforms
TEXT_DECODER = new TextDecoder();
}
// 48 === "0".charCodeAt(0) (OPEN packet type)
// 54 === "6".charCodeAt(0) (NOOP packet type)
var isPlainBinary = isBinary || data[0] < 48 || data[0] > 54;
return decodePacket(isPlainBinary ? data : TEXT_DECODER.decode(data), binaryType);
var chunks = [];
var state = 0 /* READ_HEADER */;
var expectedLength = -1;
var isBinary = false;
return new TransformStream({
transform: function transform(chunk, controller) {
chunks.push(chunk);
while (true) {
if (state === 0 /* READ_HEADER */) {
if (totalLength(chunks) < 1) {
break;
}
var header = concatChunks(chunks, 1);
isBinary = (header[0] & 0x80) === 0x80;
expectedLength = header[0] & 0x7f;
if (expectedLength < 126) {
state = 3 /* READ_PAYLOAD */;
} else if (expectedLength === 126) {
state = 1 /* READ_EXTENDED_LENGTH_16 */;
} else {
state = 2 /* READ_EXTENDED_LENGTH_64 */;
}
} else if (state === 1 /* READ_EXTENDED_LENGTH_16 */) {
if (totalLength(chunks) < 2) {
break;
}
var headerArray = concatChunks(chunks, 2);
expectedLength = new DataView(headerArray.buffer, headerArray.byteOffset, headerArray.length).getUint16(0);
state = 3 /* READ_PAYLOAD */;
} else if (state === 2 /* READ_EXTENDED_LENGTH_64 */) {
if (totalLength(chunks) < 8) {
break;
}
var _headerArray = concatChunks(chunks, 8);
var view = new DataView(_headerArray.buffer, _headerArray.byteOffset, _headerArray.length);
var n = view.getUint32(0);
if (n > Math.pow(2, 53 - 32) - 1) {
// the maximum safe integer in JavaScript is 2^53 - 1
controller.enqueue(ERROR_PACKET);
break;
}
expectedLength = n * Math.pow(2, 32) + view.getUint32(4);
state = 3 /* READ_PAYLOAD */;
} else {
if (totalLength(chunks) < expectedLength) {
break;
}
var data = concatChunks(chunks, expectedLength);
controller.enqueue(decodePacket(isBinary ? data : TEXT_DECODER.decode(data), binaryType));
state = 0 /* READ_HEADER */;
}
if (expectedLength === 0 || expectedLength > maxPayload) {
controller.enqueue(ERROR_PACKET);
break;
}
}
}
});
}
var protocol$1 = 4;
@@ -1455,7 +1563,7 @@
} catch (err) {
return this.emitReserved("error", err);
}
this.ws.binaryType = this.socket.binaryType || defaultBinaryType;
this.ws.binaryType = this.socket.binaryType;
this.addEventListeners();
}
/**
@@ -1565,11 +1673,6 @@
return WS;
}(Transport);
function shouldIncludeBinaryHeader(packet, encoded) {
// 48 === "0".charCodeAt(0) (OPEN packet type)
// 54 === "6".charCodeAt(0) (NOOP packet type)
return packet.type === "message" && typeof packet.data !== "string" && encoded[0] >= 48 && encoded[0] <= 54;
}
var WT = /*#__PURE__*/function (_Transport) {
_inherits(WT, _Transport);
var _super = _createSuper(WT);
@@ -1600,9 +1703,11 @@
// note: we could have used async/await, but that would require some additional polyfills
this.transport.ready.then(function () {
_this.transport.createBidirectionalStream().then(function (stream) {
var reader = stream.readable.getReader();
_this.writer = stream.writable.getWriter();
var binaryFlag;
var decoderStream = createPacketDecoderStream(Number.MAX_SAFE_INTEGER, _this.socket.binaryType);
var reader = stream.readable.pipeThrough(decoderStream).getReader();
var encoderStream = createPacketEncoderStream();
encoderStream.readable.pipeTo(stream.writable);
_this.writer = encoderStream.writable.getWriter();
var read = function read() {
reader.read().then(function (_ref) {
var done = _ref.done,
@@ -1610,19 +1715,18 @@
if (done) {
return;
}
if (!binaryFlag && value.byteLength === 1 && value[0] === 54) {
binaryFlag = true;
} else {
// TODO expose binarytype
_this.onPacket(decodePacketFromBinary(value, binaryFlag, "arraybuffer"));
binaryFlag = false;
}
_this.onPacket(value);
read();
})["catch"](function (err) {});
};
read();
var handshake = _this.query.sid ? "0{\"sid\":\"".concat(_this.query.sid, "\"}") : "0";
_this.writer.write(new TextEncoder().encode(handshake)).then(function () {
var packet = {
type: "open"
};
if (_this.query.sid) {
packet.data = "{\"sid\":\"".concat(_this.query.sid, "\"}");
}
_this.writer.write(packet).then(function () {
return _this.onOpen();
});
});
@@ -1636,18 +1740,13 @@
var _loop = function _loop() {
var packet = packets[i];
var lastPacket = i === packets.length - 1;
encodePacketToBinary(packet, function (data) {
if (shouldIncludeBinaryHeader(packet, data)) {
_this2.writer.write(Uint8Array.of(54));
_this2.writer.write(packet).then(function () {
if (lastPacket) {
nextTick(function () {
_this2.writable = true;
_this2.emitReserved("drain");
}, _this2.setTimeoutFn);
}
_this2.writer.write(data).then(function () {
if (lastPacket) {
nextTick(function () {
_this2.writable = true;
_this2.emitReserved("drain");
}, _this2.setTimeoutFn);
}
});
});
};
for (var i = 0; i < packets.length; i++) {
@@ -1749,6 +1848,7 @@
var opts = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
_classCallCheck(this, Socket);
_this = _super.call(this);
_this.binaryType = defaultBinaryType;
_this.writeBuffer = [];
if (uri && "object" === _typeof(uri)) {
opts = uri;
@@ -2038,12 +2138,12 @@
this.emitReserved("packet", packet);
// Socket is live - any packet counts
this.emitReserved("heartbeat");
this.resetPingTimeout();
switch (packet.type) {
case "open":
this.onHandshake(JSON.parse(packet.data));
break;
case "ping":
this.resetPingTimeout();
this.sendPacket("pong");
this.emitReserved("ping");
this.emitReserved("pong");

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -15,7 +15,7 @@ interface Todo {
let todos: Array<Todo> = [];
io.on("connect", (socket) => {
io.on("connection", (socket) => {
socket.emit("todos", todos);
// note: we could also create a CRUD (create/read/update/delete) service for the todo list

View File

@@ -1,17 +0,0 @@
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
# For the full list of supported browsers by the Angular framework, please see:
# https://angular.io/guide/browser-support
# You can see what browsers were selected by your queries by running:
# npx browserslist
last 1 Chrome version
last 1 Firefox version
last 2 Edge major versions
last 2 Safari major versions
last 2 iOS major versions
Firefox ESR
not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line.

View File

@@ -1,21 +1,18 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
# Compiled output
/dist
/tmp
/out-tsc
# Only exists if Bazel was run
/bazel-out
# dependencies
# Node
/node_modules
# profiling files
chrome-profiler-events*.json
speed-measure-plugin*.json
npm-debug.log
yarn-error.log
# IDEs and editors
/.idea
.idea/
.project
.classpath
.c9/
@@ -23,7 +20,7 @@ speed-measure-plugin*.json
.settings/
*.sublime-workspace
# IDE - VSCode
# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
@@ -31,16 +28,15 @@ speed-measure-plugin*.json
!.vscode/extensions.json
.history/*
# misc
/.sass-cache
# Miscellaneous
/.angular/cache
.sass-cache/
/connect.lock
/coverage
/libpeerconnection.log
npm-debug.log
yarn-error.log
testem.log
/typings
# System Files
# System files
.DS_Store
Thumbs.db

View File

@@ -1,14 +1,10 @@
# Angular TodoMVC + Socket.IO
# AngularClient
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 11.0.4.
Inspired from the [TodoMVC](http://todomvc.com/) [angular example](https://github.com/tastejs/todomvc/tree/master/examples/angular2).
![demo](assets/demo.gif)
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 17.0.2.
## Development server
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files.
## Code scaffolding
@@ -16,7 +12,7 @@ Run `ng generate component component-name` to generate a new component. You can
## Build
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build.
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
## Running unit tests
@@ -24,7 +20,7 @@ Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.
## Running end-to-end tests
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.
## Further help

View File

@@ -3,26 +3,23 @@
"version": 1,
"newProjectRoot": "projects",
"projects": {
"angular-todomvc": {
"angular-client": {
"projectType": "application",
"schematics": {
"@schematics/angular:application": {
"strict": true
}
},
"schematics": {},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"builder": "@angular-devkit/build-angular:application",
"options": {
"outputPath": "dist/angular-todomvc",
"outputPath": "dist/angular-client",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"browser": "src/main.ts",
"polyfills": [
"zone.js"
],
"tsConfig": "tsconfig.app.json",
"aot": true,
"assets": [
"src/favicon.ico",
"src/assets"
@@ -34,19 +31,6 @@
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
@@ -58,34 +42,49 @@
"maximumWarning": "2kb",
"maximumError": "4kb"
}
],
"outputHashing": "all"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true,
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.development.ts"
}
]
}
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "angular-todomvc:build"
},
"configurations": {
"production": {
"browserTarget": "angular-todomvc:build:production"
"buildTarget": "angular-client:build:production"
},
"development": {
"buildTarget": "angular-client:build:development"
}
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "angular-todomvc:build"
"buildTarget": "angular-client:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"polyfills": [
"zone.js",
"zone.js/testing"
],
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"assets": [
"src/favicon.ico",
"src/assets"
@@ -95,34 +94,8 @@
],
"scripts": []
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"tsconfig.app.json",
"tsconfig.spec.json",
"e2e/tsconfig.json"
],
"exclude": [
"**/node_modules/**"
]
}
},
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "e2e/protractor.conf.js",
"devServerTarget": "angular-todomvc:serve"
},
"configurations": {
"production": {
"devServerTarget": "angular-todomvc:serve:production"
}
}
}
}
}
},
"defaultProject": "angular-todomvc"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 205 KiB

View File

@@ -1,37 +0,0 @@
// @ts-check
// Protractor configuration file, see link for more information
// https://github.com/angular/protractor/blob/master/lib/config.ts
const { SpecReporter, StacktraceOption } = require('jasmine-spec-reporter');
/**
* @type { import("protractor").Config }
*/
exports.config = {
allScriptsTimeout: 11000,
specs: [
'./src/**/*.e2e-spec.ts'
],
capabilities: {
browserName: 'chrome'
},
directConnect: true,
SELENIUM_PROMISE_MANAGER: false,
baseUrl: 'http://localhost:4200/',
framework: 'jasmine',
jasmineNodeOpts: {
showColors: true,
defaultTimeoutInterval: 30000,
print: function() {}
},
onPrepare() {
require('ts-node').register({
project: require('path').join(__dirname, './tsconfig.json')
});
jasmine.getEnv().addReporter(new SpecReporter({
spec: {
displayStacktrace: StacktraceOption.PRETTY
}
}));
}
};

View File

@@ -1,23 +0,0 @@
import { AppPage } from './app.po';
import { browser, logging } from 'protractor';
describe('workspace-project App', () => {
let page: AppPage;
beforeEach(() => {
page = new AppPage();
});
it('should display welcome message', async () => {
await page.navigateTo();
expect(await page.getTitleText()).toEqual('angular-todomvc app is running!');
});
afterEach(async () => {
// Assert that there are no errors emitted from the browser
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
expect(logs).not.toContain(jasmine.objectContaining({
level: logging.Level.SEVERE,
} as logging.Entry));
});
});

View File

@@ -1,11 +0,0 @@
import { browser, by, element } from 'protractor';
export class AppPage {
async navigateTo(): Promise<unknown> {
return browser.get(browser.baseUrl);
}
async getTitleText(): Promise<string> {
return element(by.css('app-root .content span')).getText();
}
}

View File

@@ -1,13 +0,0 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/e2e",
"module": "commonjs",
"target": "es2018",
"types": [
"jasmine",
"node"
]
}
}

View File

@@ -1,44 +0,0 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
jasmine: {
// you can add configuration options for Jasmine here
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
// for example, you can disable the random execution with `random: false`
// or set a specific seed with `seed: 4321`
},
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
jasmineHtmlReporter: {
suppressAll: true // removes the duplicated traces
},
coverageReporter: {
dir: require('path').join(__dirname, './coverage/angular-todomvc'),
subdir: '.',
reporters: [
{ type: 'html' },
{ type: 'text-summary' }
]
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
restartOnFileChange: true
});
};

View File

@@ -1,46 +1,40 @@
{
"name": "angular-todomvc",
"name": "angular-client",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e"
"watch": "ng build --watch --configuration development",
"test": "ng test"
},
"private": true,
"dependencies": {
"@angular/animations": "~11.0.4",
"@angular/common": "~11.0.4",
"@angular/compiler": "~11.0.4",
"@angular/core": "~11.0.4",
"@angular/forms": "~11.0.4",
"@angular/platform-browser": "~11.0.4",
"@angular/platform-browser-dynamic": "~11.0.4",
"@angular/router": "~11.0.4",
"rxjs": "~6.6.0",
"socket.io-client": "^4.0.0",
"tslib": "^2.0.0",
"zone.js": "~0.10.2"
"@angular/animations": "^17.0.0",
"@angular/common": "^17.0.0",
"@angular/compiler": "^17.0.0",
"@angular/core": "^17.0.0",
"@angular/forms": "^17.0.0",
"@angular/platform-browser": "^17.0.0",
"@angular/platform-browser-dynamic": "^17.0.0",
"@angular/router": "^17.0.0",
"rxjs": "~7.8.0",
"socket.io-client": "^4.7.2",
"tslib": "^2.3.0",
"zone.js": "~0.14.2"
},
"devDependencies": {
"@angular-devkit/build-angular": "~0.1100.4",
"@angular/cli": "~11.0.4",
"@angular/compiler-cli": "~11.0.4",
"@types/jasmine": "~3.6.0",
"@types/node": "^12.11.1",
"codelyzer": "^6.0.0",
"jasmine-core": "~3.6.0",
"jasmine-spec-reporter": "~5.0.0",
"karma": "~5.1.0",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage": "~2.0.3",
"karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "~1.5.0",
"protractor": "~7.0.0",
"ts-node": "~8.3.0",
"tslint": "~6.1.0",
"typescript": "~4.0.2"
"@angular-devkit/build-angular": "^17.0.2",
"@angular/cli": "^17.0.2",
"@angular/compiler-cli": "^17.0.0",
"@types/jasmine": "~5.1.0",
"@types/node": "^20.9.2",
"jasmine-core": "~5.1.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"typescript": "~5.2.2"
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,19 +0,0 @@
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { TodoStore } from './store';
import { FormsModule } from "@angular/forms";
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
FormsModule
],
providers: [TodoStore],
bootstrap: [AppComponent]
})
export class AppModule { }

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,17 +1,3 @@
// This file can be replaced during build by using the `fileReplacements` array.
// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
// The list of file replacements can be found in `angular.json`.
export const environment = {
production: false,
serverUrl: "http://localhost:3000"
serverUrl: "https://my-custom-domain.com"
};
/*
* For easier debugging in development mode, you can import the following file
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
*
* This import should be commented out in production mode because it will have a negative impact
* on performance if an error is thrown.
*/
// import 'zone.js/dist/zone-error'; // Included with Angular CLI.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 948 B

After

Width:  |  Height:  |  Size: 15 KiB

View File

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

View File

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

View File

@@ -1,63 +0,0 @@
/**
* This file includes polyfills needed by Angular and is loaded before the app.
* You can add your own extra polyfills to this file.
*
* This file is divided into 2 sections:
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
* file.
*
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
*
* Learn more in https://angular.io/guide/browser-support
*/
/***************************************************************************************************
* BROWSER POLYFILLS
*/
/** IE11 requires the following for NgClass support on SVG elements */
// import 'classlist.js'; // Run `npm install --save classlist.js`.
/**
* Web Animations `@angular/platform-browser/animations`
* Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
* Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
*/
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
/**
* By default, zone.js will patch all possible macroTask and DomEvents
* user can disable parts of macroTask/DomEvents patch by setting following flags
* because those flags need to be set before `zone.js` being loaded, and webpack
* will put import in the top of bundle, so user need to create a separate file
* in this directory (for example: zone-flags.ts), and put the following flags
* into that file, and then add the following code before importing zone.js.
* import './zone-flags';
*
* The flags allowed in zone-flags.ts are listed here.
*
* The following flags will work for all browsers.
*
* (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
* (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
* (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
*
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
* with the following flag, it will bypass `zone.js` patch for IE/Edge
*
* (window as any).__Zone_enable_cross_context_check = true;
*
*/
/***************************************************************************************************
* Zone JS is required by default for Angular itself.
*/
import 'zone.js/dist/zone'; // Included with Angular CLI.
/***************************************************************************************************
* APPLICATION IMPORTS
*/

View File

@@ -1,25 +0,0 @@
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
import 'zone.js/dist/zone-testing';
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
declare const require: {
context(path: string, deep?: boolean, filter?: RegExp): {
keys(): string[];
<T>(id: string): T;
};
};
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting()
);
// Then we find all the tests.
const context = require.context('./', true, /\.spec\.ts$/);
// And load the modules.
context.keys().map(context);

View File

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

View File

@@ -2,26 +2,29 @@
{
"compileOnSave": false,
"compilerOptions": {
"baseUrl": "./",
"outDir": "./dist/out-tsc",
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"esModuleInterop": true,
"sourceMap": true,
"declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true,
"moduleResolution": "node",
"importHelpers": true,
"target": "es2015",
"module": "es2020",
"target": "ES2022",
"module": "ES2022",
"useDefineForClassFields": false,
"lib": [
"es2018",
"ES2022",
"dom"
]
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true

View File

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

View File

@@ -1,152 +0,0 @@
{
"extends": "tslint:recommended",
"rulesDirectory": [
"codelyzer"
],
"rules": {
"align": {
"options": [
"parameters",
"statements"
]
},
"array-type": false,
"arrow-return-shorthand": true,
"curly": true,
"deprecation": {
"severity": "warning"
},
"eofline": true,
"import-blacklist": [
true,
"rxjs/Rx"
],
"import-spacing": true,
"indent": {
"options": [
"spaces"
]
},
"max-classes-per-file": false,
"max-line-length": [
true,
140
],
"member-ordering": [
true,
{
"order": [
"static-field",
"instance-field",
"static-method",
"instance-method"
]
}
],
"no-console": [
true,
"debug",
"info",
"time",
"timeEnd",
"trace"
],
"no-empty": false,
"no-inferrable-types": [
true,
"ignore-params"
],
"no-non-null-assertion": true,
"no-redundant-jsdoc": true,
"no-switch-case-fall-through": true,
"no-var-requires": false,
"object-literal-key-quotes": [
true,
"as-needed"
],
"quotemark": [
true,
"single"
],
"semicolon": {
"options": [
"always"
]
},
"space-before-function-paren": {
"options": {
"anonymous": "never",
"asyncArrow": "always",
"constructor": "never",
"method": "never",
"named": "never"
}
},
"typedef": [
true,
"call-signature"
],
"typedef-whitespace": {
"options": [
{
"call-signature": "nospace",
"index-signature": "nospace",
"parameter": "nospace",
"property-declaration": "nospace",
"variable-declaration": "nospace"
},
{
"call-signature": "onespace",
"index-signature": "onespace",
"parameter": "onespace",
"property-declaration": "onespace",
"variable-declaration": "onespace"
}
]
},
"variable-name": {
"options": [
"ban-keywords",
"check-format",
"allow-pascal-case"
]
},
"whitespace": {
"options": [
"check-branch",
"check-decl",
"check-operator",
"check-separator",
"check-type",
"check-typecast"
]
},
"component-class-suffix": true,
"contextual-lifecycle": true,
"directive-class-suffix": true,
"no-conflicting-lifecycle": true,
"no-host-metadata-property": true,
"no-input-rename": true,
"no-inputs-metadata-property": true,
"no-output-native": true,
"no-output-on-prefix": true,
"no-output-rename": true,
"no-outputs-metadata-property": true,
"template-banana-in-box": true,
"template-no-negated-async": true,
"use-lifecycle-interface": true,
"use-pipe-transform-interface": true,
"directive-selector": [
true,
"attribute",
"app",
"camelCase"
],
"component-selector": [
true,
"element",
"app",
"kebab-case"
]
}
}

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
import { Errors } from "../util";
import { Todo, TodoID } from "../../../common/events";
abstract class CrudRepository<T, ID> {
abstract findAll(): Promise<T[]>;
@@ -7,14 +8,6 @@ abstract class CrudRepository<T, ID> {
abstract deleteById(id: ID): Promise<void>;
}
export type TodoID = string;
export interface Todo {
id: TodoID;
completed: boolean;
title: string;
}
export abstract class TodoRepository extends CrudRepository<Todo, TodoID> {}
export class InMemoryTodoRepository extends TodoRepository {

View File

@@ -0,0 +1,23 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -0,0 +1,24 @@
# vue-client
## Project setup
```
yarn install
```
### Compiles and hot-reloads for development
```
yarn serve
```
### Compiles and minifies for production
```
yarn build
```
### Lints and fixes files
```
yarn lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

View File

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

View File

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

View File

@@ -0,0 +1,45 @@
{
"name": "vue-client",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve --port 4200",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"core-js": "^3.8.3",
"pinia": "^2.1.7",
"socket.io-client": "^4.7.2",
"vue": "^3.2.13"
},
"devDependencies": {
"@babel/core": "^7.12.16",
"@babel/eslint-parser": "^7.12.16",
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"eslint": "^7.32.0",
"eslint-plugin-vue": "^8.0.3"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/vue3-essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "@babel/eslint-parser"
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead",
"not ie 11"
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<link href="styles.css" rel="stylesheet" type="text/css" />
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

View File

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

View File

@@ -0,0 +1,123 @@
<script setup>
import { computed, ref } from "vue";
import { useTodoStore } from "@/stores/todo";
import { socket } from "@/socket";
const newTodo = ref("");
const editedTodo = ref(undefined);
const newTitle = ref("");
const store = useTodoStore();
// remove any existing listeners (in case of hot reload)
socket.off();
store.bindEvents();
function addTodo() {
const value = newTodo.value && newTodo.value.trim();
if (!value) {
return;
}
store.add(value);
newTodo.value = "";
}
function editTodo(todo) {
editedTodo.value = todo;
newTitle.value = todo.title;
}
function doneEdit(todo) {
if (newTitle.value) {
store.setTitle(todo, newTitle.value);
} else {
store.delete(todo);
}
editedTodo.value = undefined;
}
function cancelEdit() {
editedTodo.value = undefined;
}
const allDone = computed({
get: () => {
return store.remaining === 0;
},
set: (value) => {
store.toggleAll(value);
},
});
function pluralize(word, count) {
return word + (count === 1 ? "" : "s");
}
</script>
<template>
<section class="todoapp" v-cloak>
<header class="header">
<h1>todos</h1>
<input
class="new-todo"
autofocus
autocomplete="off"
placeholder="What needs to be done?"
v-model="newTodo"
@keydown.enter="addTodo"
/>
</header>
<section class="main" v-show="store.todos.length">
<input
id="toggle-all"
class="toggle-all"
type="checkbox"
v-model="allDone"
/>
<label for="toggle-all">Mark all as complete</label>
<ul class="todo-list">
<li
class="todo"
v-for="todo in store.todos"
:key="todo.id"
:class="{ completed: todo.completed, editing: todo === editedTodo }"
>
<div class="view">
<input
class="toggle"
type="checkbox"
v-model="todo.completed"
@click="store.toggleOne(todo)"
/>
<label @dblclick="editTodo(todo)">{{ todo.title }}</label>
<button class="destroy" @click="store.delete(todo)"></button>
</div>
<input
class="edit"
type="text"
v-model="newTitle"
@blur="doneEdit"
@keydown.enter="doneEdit(todo)"
@keydown.esc="cancelEdit(todo)"
/>
</li>
</ul>
</section>
<footer class="footer" v-show="store.todos.length">
<span class="todo-count">
<strong v-text="store.remaining"></strong>
{{ pluralize("item", store.remaining) }} left
</span>
<button
class="clear-completed"
@click="store.deleteCompleted"
v-show="store.todos.length > store.remaining"
>
Clear complete
</button>
</footer>
</section>
</template>
<style scoped></style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@@ -0,0 +1,9 @@
import { createApp } from "vue";
import { createPinia } from "pinia";
import App from "./App.vue";
const pinia = createPinia();
const app = createApp(App);
app.use(pinia);
app.mount("#app");

View File

@@ -0,0 +1,7 @@
import { io } from "socket.io-client";
// "undefined" means the URL will be computed from the `window.location` object
const URL =
process.env.NODE_ENV === "production" ? undefined : "http://localhost:3000";
export const socket = io(URL);

View File

@@ -0,0 +1,106 @@
import { defineStore } from "pinia";
import { socket } from "@/socket";
export const useTodoStore = defineStore("todo", {
state: () => ({
todos: [],
}),
getters: {
remaining(state) {
let count = 0;
state.todos.forEach((todo) => {
if (!todo.completed) {
count++;
}
});
return count;
},
},
actions: {
bindEvents() {
socket.on("connect", () => {
socket.emit("todo:list", (res) => {
this.todos = res.data;
});
});
socket.on("todo:created", (todo) => {
this.todos.push(todo);
});
socket.on("todo:updated", (todo) => {
const existingTodo = this.todos.find((t) => {
return t.id === todo.id;
});
if (existingTodo) {
existingTodo.title = todo.title;
existingTodo.completed = todo.completed;
}
});
socket.on("todo:deleted", (id) => {
const i = this.todos.findIndex((t) => {
return t.id === id;
});
if (i !== -1) {
this.todos.splice(i, 1);
}
});
},
add(title) {
const todo = {
id: Date.now(),
title,
completed: false,
};
this.todos.push(todo);
socket.emit("todo:create", { title, completed: false }, (res) => {
todo.id = res.data;
});
},
setTitle(todo, title) {
todo.title = title;
socket.emit("todo:update", todo, () => {});
},
delete(todo) {
const i = this.todos.findIndex((t) => {
return t.id === todo.id;
});
if (i !== -1) {
this.todos.splice(i, 1);
socket.emit("todo:delete", todo.id, () => {});
}
},
deleteCompleted() {
this.todos.forEach((todo) => {
if (todo.completed) {
socket.emit("todo:delete", todo.id, () => {});
}
});
this.todos = this.todos.filter((t) => {
return !t.completed;
});
},
toggleOne(todo) {
todo.completed = !todo.completed;
socket.emit("todo:update", todo, () => {});
},
toggleAll(onlyActive) {
this.todos.forEach((todo) => {
if (!onlyActive || !todo.completed) {
this.toggleOne(todo);
}
});
},
},
});

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,25 @@
# Example with connection state recovery
This example shows how to use the [Connection state recovery feature](https://socket.io/docs/v4/connection-state-recovery).
![Video of the example](assets/csr.gif)
## How to use
```shell
# choose your module syntax (either ES modules or CommonJS)
$ cd esm/
# install the dependencies
$ npm i
# start the server
$ node index.js
```
And point your browser to `http://localhost:3000`.
You can also run this example directly in your browser on:
- [CodeSandbox](https://codesandbox.io/p/sandbox/github/socketio/socket.io/tree/main/examples/connection-state-recovery-example/esm?file=index.js)
- [StackBlitz](https://stackblitz.com/github/socketio/socket.io/tree/main/examples/connection-state-recovery-example/esm?file=index.js)

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

View File

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

View File

@@ -0,0 +1,18 @@
{
// These tasks will run in order when initializing your CodeSandbox project.
"setupTasks": [
{
"name": "Install Dependencies",
"command": "npm install"
}
],
// These tasks can be run from CodeSandbox. Running one will open a log in the app.
"tasks": {
"npm start": {
"name": "npm start",
"command": "npm start",
"runAtStart": true
}
}
}

View File

@@ -0,0 +1,49 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Connection state recovery | Socket.IO</title>
</head>
<body>
<p>Status: <span id="connectionStatus">disconnected</span></p>
<p>Recovered? <span id="recoveryStatus">-</span></p>
<p>Latest messages:</p>
<ul id="messages"></ul>
<script src="/socket.io/socket.io.js"></script>
<script>
const $connectionStatus = document.getElementById("connectionStatus");
const $recoveryStatus = document.getElementById("recoveryStatus");
const $messages = document.getElementById("messages");
const socket = io({
reconnectionDelay: 5000 // 1000 by default
});
socket.on("connect", () => {
$connectionStatus.innerText = "connected";
$recoveryStatus.innerText = "" + socket.recovered;
setTimeout(() => {
// close the low-level connection and trigger a reconnection
socket.io.engine.close();
}, Math.random() * 5000 + 1000);
});
socket.on("disconnect", () => {
$connectionStatus.innerText = "disconnected";
$recoveryStatus.innerText = "-"
});
socket.on("ping", (value) => {
const item = document.createElement("li");
item.textContent = value;
$messages.prepend(item);
if ($messages.childElementCount > 10) {
$messages.removeChild($messages.lastChild);
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,53 @@
const { readFile } = require("node:fs/promises");
const { createServer } = require("node:http");
const { Server } = require("socket.io");
const httpServer = createServer(async (req, res) => {
if (req.url !== "/") {
res.writeHead(404);
res.end("Not found");
return;
}
// reload the file every time
const content = await readFile("index.html");
const length = Buffer.byteLength(content);
res.writeHead(200, {
"Content-Type": "text/html",
"Content-Length": length,
});
res.end(content);
});
const io = new Server(httpServer, {
connectionStateRecovery: {
// the backup duration of the sessions and the packets
maxDisconnectionDuration: 2 * 60 * 1000,
// whether to skip middlewares upon successful recovery
skipMiddlewares: true,
},
});
io.on("connection", (socket) => {
console.log(`connect ${socket.id}`);
if (socket.recovered) {
console.log("recovered!");
console.log("socket.rooms:", socket.rooms);
console.log("socket.data:", socket.data);
} else {
console.log("new connection");
socket.join("sample room");
socket.data.foo = "bar";
}
socket.on("disconnect", (reason) => {
console.log(`disconnect ${socket.id} due to ${reason}`);
});
});
setInterval(() => {
io.emit("ping", new Date().toISOString());
}, 1000);
httpServer.listen(3000);

View File

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

View File

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

View File

@@ -0,0 +1,18 @@
{
// These tasks will run in order when initializing your CodeSandbox project.
"setupTasks": [
{
"name": "Install Dependencies",
"command": "npm install"
}
],
// These tasks can be run from CodeSandbox. Running one will open a log in the app.
"tasks": {
"npm start": {
"name": "npm start",
"command": "npm start",
"runAtStart": true
}
}
}

View File

@@ -0,0 +1,49 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Connection state recovery | Socket.IO</title>
</head>
<body>
<p>Status: <span id="connectionStatus">disconnected</span></p>
<p>Recovered? <span id="recoveryStatus">-</span></p>
<p>Latest messages:</p>
<ul id="messages"></ul>
<script src="/socket.io/socket.io.js"></script>
<script>
const $connectionStatus = document.getElementById("connectionStatus");
const $recoveryStatus = document.getElementById("recoveryStatus");
const $messages = document.getElementById("messages");
const socket = io({
reconnectionDelay: 5000 // 1000 by default
});
socket.on("connect", () => {
$connectionStatus.innerText = "connected";
$recoveryStatus.innerText = "" + socket.recovered;
setTimeout(() => {
// close the low-level connection and trigger a reconnection
socket.io.engine.close();
}, Math.random() * 5000 + 1000);
});
socket.on("disconnect", () => {
$connectionStatus.innerText = "disconnected";
$recoveryStatus.innerText = "-"
});
socket.on("ping", (value) => {
const item = document.createElement("li");
item.textContent = value;
$messages.prepend(item);
if ($messages.childElementCount > 10) {
$messages.removeChild($messages.lastChild);
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,53 @@
import { readFile } from "node:fs/promises";
import { createServer } from "node:http";
import { Server } from "socket.io";
const httpServer = createServer(async (req, res) => {
if (req.url !== "/") {
res.writeHead(404);
res.end("Not found");
return;
}
// reload the file every time
const content = await readFile("index.html");
const length = Buffer.byteLength(content);
res.writeHead(200, {
"Content-Type": "text/html",
"Content-Length": length,
});
res.end(content);
});
const io = new Server(httpServer, {
connectionStateRecovery: {
// the backup duration of the sessions and the packets
maxDisconnectionDuration: 2 * 60 * 1000,
// whether to skip middlewares upon successful recovery
skipMiddlewares: true,
},
});
io.on("connection", (socket) => {
console.log(`connect ${socket.id}`);
if (socket.recovered) {
console.log("recovered!");
console.log("socket.rooms:", socket.rooms);
console.log("socket.data:", socket.data);
} else {
console.log("new connection");
socket.join("sample room");
socket.data.foo = "bar";
}
socket.on("disconnect", (reason) => {
console.log(`disconnect ${socket.id} due to ${reason}`);
});
});
setInterval(() => {
io.emit("ping", new Date().toISOString());
}, 1000);
httpServer.listen(3000);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,20 @@
{
"name": "express-session-example",
"version": "0.0.1",
"private": true,
"type": "module",
"description": "Example with express-session (https://github.com/expressjs/session)",
"scripts": {
"start": "ts-node index.ts"
},
"dependencies": {
"@types/express": "^4.17.17",
"@types/express-session": "^1.17.7",
"@types/node": "^20.6.0",
"express": "~4.17.3",
"express-session": "~1.17.2",
"socket.io": "^4.7.2",
"ts-node": "^10.9.1",
"typescript": "^5.2.2"
}
}

View File

@@ -0,0 +1,11 @@
{
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext",
"target": "ES2022",
"strict": true
},
"ts-node": {
"esm": true
}
}

View File

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

1
examples/webtransport/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
*.pem

View File

@@ -0,0 +1,18 @@
# Socket.IO WebTransport example
## How to use
```shell
# generate a self-signed certificate
$ ./generate_cert.sh
# install dependencies
$ npm i
# start the server
$ node index.js
# open a Chrome browser
$ ./open_chrome.sh
```

View File

@@ -0,0 +1,8 @@
#!/bin/bash
openssl req -new -x509 -nodes \
-out cert.pem \
-keyout key.pem \
-newkey ec \
-pkeyopt ec_paramgen_curve:prime256v1 \
-subj '/CN=127.0.0.1' \
-days 14

View File

@@ -0,0 +1,35 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Socket.IO WebTransport exampleqg</title>
</head>
<body>
<script src="/socket.io/socket.io.js"></script>
<script>
const socket = io({
transportOptions: {
webtransport: {
hostname: "127.0.0.1"
}
}
});
socket.on("connect", () => {
console.log(`connect ${socket.id}`);
socket.io.engine.on("upgrade", (transport) => {
console.log(`transport upgraded to ${transport.name}`);
});
});
socket.on("connect_error", (err) => {
console.log(`connect_error due to ${err.message}`);
});
socket.on("disconnect", (reason) => {
console.log(`disconnect due to ${reason}`);
});
</script>
</body>
</html>

View File

@@ -0,0 +1,68 @@
import { readFileSync } from "node:fs";
import { createServer } from "node:https";
import { Server } from "socket.io";
import { Http3Server } from "@fails-components/webtransport";
const key = readFileSync("./key.pem");
const cert = readFileSync("./cert.pem");
const httpsServer = createServer({
key,
cert
}, (req, res) => {
if (req.method === "GET" && req.url === "/") {
const content = readFileSync("./index.html");
res.writeHead(200, {
"content-type": "text/html"
});
res.write(content);
res.end();
} else {
res.writeHead(404).end();
}
});
const io = new Server(httpsServer, {
transports: ["polling", "websocket", "webtransport"]
});
const port = process.env.PORT || 3000;
io.on("connection", (socket) => {
console.log(`connect ${socket.id}`);
socket.conn.on("upgrade", (transport) => {
console.log(`transport upgraded to ${transport.name}`);
});
socket.on("disconnect", (reason) => {
console.log(`disconnect ${socket.id} due to ${reason}`);
});
});
httpsServer.listen(port, () => {
console.log(`server listening at https://localhost:${port}`);
});
const h3Server = new Http3Server({
port,
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();

View File

@@ -0,0 +1,10 @@
#!/bin/bash
HASH=`openssl x509 -pubkey -noout -in cert.pem |
openssl pkey -pubin -outform der |
openssl dgst -sha256 -binary |
base64`
/opt/google/chrome/chrome \
--origin-to-force-quic-on=127.0.0.1:3000 \
--ignore-certificate-errors-spki-list=$HASH \
https://localhost:3000

View File

@@ -0,0 +1,11 @@
{
"name": "webtransport",
"version": "0.0.1",
"description": "",
"private": true,
"type": "module",
"dependencies": {
"@fails-components/webtransport": "^0.1.7",
"socket.io": "^4.7.1"
}
}

View File

@@ -8,10 +8,10 @@ import type {
EventsMap,
TypedEventBroadcaster,
DecorateAcknowledgements,
DecorateAcknowledgementsWithTimeoutAndMultipleResponses,
AllButLast,
Last,
SecondArg,
FirstNonErrorArg,
EventNamesWithError,
} from "./typed-events";
export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
@@ -177,7 +177,7 @@ export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
public timeout(timeout: number) {
const flags = Object.assign({}, this.flags, { timeout });
return new BroadcastOperator<
DecorateAcknowledgementsWithTimeoutAndMultipleResponses<EmitEvents>,
DecorateAcknowledgements<EmitEvents>,
SocketData
>(this.adapter, this.rooms, this.exceptRooms, flags);
}
@@ -254,7 +254,7 @@ export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
clearTimeout(timer);
ack.apply(this, [
null,
this.flags.expectSingleResponse ? null : responses,
this.flags.expectSingleResponse ? responses[0] : responses,
]);
}
};
@@ -300,10 +300,10 @@ export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
*
* @return a Promise that will be fulfilled when all clients have acknowledged the event
*/
public emitWithAck<Ev extends EventNames<EmitEvents>>(
public emitWithAck<Ev extends EventNamesWithError<EmitEvents>>(
ev: Ev,
...args: AllButLast<EventParams<EmitEvents, Ev>>
): Promise<SecondArg<Last<EventParams<EmitEvents, Ev>>>> {
): Promise<FirstNonErrorArg<Last<EventParams<EmitEvents, Ev>>>> {
return new Promise((resolve, reject) => {
args.push((err, responses) => {
if (err) {
@@ -516,11 +516,10 @@ export class RemoteSocket<EmitEvents extends EventsMap, SocketData>
*
* @param timeout
*/
public timeout(timeout: number) {
return this.operator.timeout(timeout) as BroadcastOperator<
DecorateAcknowledgements<EmitEvents>,
SocketData
>;
public timeout(
timeout: number
): BroadcastOperator<DecorateAcknowledgements<EmitEvents>, SocketData> {
return this.operator.timeout(timeout);
}
public emit<Ev extends EventNames<EmitEvents>>(

View File

@@ -1,6 +1,6 @@
import http = require("http");
import type { Server as HTTPSServer } from "https";
import type { Http2SecureServer } from "http2";
import type { Http2SecureServer, Http2Server } from "http2";
import { createReadStream } from "fs";
import { createDeflate, createGzip, createBrotliCompress } from "zlib";
import accepts = require("accepts");
@@ -36,8 +36,9 @@ import {
DecorateAcknowledgementsWithTimeoutAndMultipleResponses,
AllButLast,
Last,
FirstArg,
SecondArg,
RemoveAcknowledgements,
EventNamesWithAck,
FirstNonErrorArg,
} from "./typed-events";
import { patchAdapter, restoreAdapter, serveFile } from "./uws";
import corsMiddleware from "cors";
@@ -55,6 +56,12 @@ type ParentNspNameMatchFn = (
type AdapterConstructor = typeof Adapter | ((nsp: Namespace) => Adapter);
type TServerInstance =
| http.Server
| HTTPSServer
| Http2SecureServer
| Http2Server;
interface ServerOptions extends EngineOptions, AttachOptions {
/**
* name of the path to capture
@@ -140,7 +147,7 @@ export class Server<
SocketData = any
> extends StrictEventEmitter<
ServerSideEvents,
EmitEvents,
RemoveAcknowledgements<EmitEvents>,
ServerReservedEventsMap<
ListenEvents,
EmitEvents,
@@ -202,7 +209,7 @@ export class Server<
* @private
*/
_connectTimeout: number;
private httpServer: http.Server | HTTPSServer | Http2SecureServer;
private httpServer: TServerInstance;
private _corsMiddleware: (
req: http.IncomingMessage,
res: http.ServerResponse,
@@ -216,28 +223,13 @@ export class Server<
* @param [opts]
*/
constructor(opts?: Partial<ServerOptions>);
constructor(srv?: TServerInstance | number, opts?: Partial<ServerOptions>);
constructor(
srv?: http.Server | HTTPSServer | Http2SecureServer | number,
srv: undefined | Partial<ServerOptions> | TServerInstance | number,
opts?: Partial<ServerOptions>
);
constructor(
srv:
| undefined
| Partial<ServerOptions>
| http.Server
| HTTPSServer
| Http2SecureServer
| number,
opts?: Partial<ServerOptions>
);
constructor(
srv:
| undefined
| Partial<ServerOptions>
| http.Server
| HTTPSServer
| Http2SecureServer
| number,
srv: undefined | Partial<ServerOptions> | TServerInstance | number,
opts: Partial<ServerOptions> = {}
) {
super();
@@ -270,9 +262,7 @@ export class Server<
opts.cleanupEmptyChildNamespaces = !!opts.cleanupEmptyChildNamespaces;
this.sockets = this.of("/");
if (srv || typeof srv == "number")
this.attach(
srv as http.Server | HTTPSServer | Http2SecureServer | number
);
this.attach(srv as TServerInstance | number);
if (this.opts.cors) {
this._corsMiddleware = corsMiddleware(this.opts.cors);
@@ -406,7 +396,7 @@ export class Server<
* @return self
*/
public listen(
srv: http.Server | HTTPSServer | Http2SecureServer | number,
srv: TServerInstance | number,
opts: Partial<ServerOptions> = {}
): this {
return this.attach(srv, opts);
@@ -420,7 +410,7 @@ export class Server<
* @return self
*/
public attach(
srv: http.Server | HTTPSServer | Http2SecureServer | number,
srv: TServerInstance | number,
opts: Partial<ServerOptions> = {}
): this {
if ("function" == typeof srv) {
@@ -526,7 +516,7 @@ export class Server<
* @private
*/
private initEngine(
srv: http.Server | HTTPSServer | Http2SecureServer,
srv: TServerInstance,
opts: EngineOptions & AttachOptions
): void {
// initialize engine
@@ -549,9 +539,7 @@ export class Server<
* @param srv http server
* @private
*/
private attachServe(
srv: http.Server | HTTPSServer | Http2SecureServer
): void {
private attachServe(srv: TServerInstance): void {
debug("attaching client serving req handler");
const evs = srv.listeners("request").slice(0);
@@ -846,26 +834,6 @@ export class Server<
return this.sockets.except(room);
}
/**
* Emits an event and waits for an acknowledgement from all clients.
*
* @example
* 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
* }
*
* @return a Promise that will be fulfilled when all clients have acknowledged the event
*/
public emitWithAck<Ev extends EventNames<EmitEvents>>(
ev: Ev,
...args: AllButLast<EventParams<EmitEvents, Ev>>
): Promise<SecondArg<Last<EventParams<EmitEvents, Ev>>>> {
return this.sockets.emitWithAck(ev, ...args);
}
/**
* Sends a `message` event to all clients.
*
@@ -882,7 +850,9 @@ export class Server<
* @return self
*/
public send(...args: EventParams<EmitEvents, "message">): this {
this.sockets.emit("message", ...args);
// This type-cast is needed because EmitEvents likely doesn't have `message` as a key.
// if you specify the EmitEvents, the type of args will be never.
this.sockets.emit("message" as any, ...args);
return this;
}
@@ -892,7 +862,9 @@ export class Server<
* @return self
*/
public write(...args: EventParams<EmitEvents, "message">): this {
this.sockets.emit("message", ...args);
// This type-cast is needed because EmitEvents likely doesn't have `message` as a key.
// if you specify the EmitEvents, the type of args will be never.
this.sockets.emit("message" as any, ...args);
return this;
}
@@ -948,10 +920,10 @@ export class Server<
*
* @return a Promise that will be fulfilled when all servers have acknowledged the event
*/
public serverSideEmitWithAck<Ev extends EventNames<ServerSideEvents>>(
public serverSideEmitWithAck<Ev extends EventNamesWithAck<ServerSideEvents>>(
ev: Ev,
...args: AllButLast<EventParams<ServerSideEvents, Ev>>
): Promise<FirstArg<Last<EventParams<ServerSideEvents, Ev>>>[]> {
): Promise<FirstNonErrorArg<Last<EventParams<ServerSideEvents, Ev>>>[]> {
return this.sockets.serverSideEmitWithAck(ev, ...args);
}

View File

@@ -9,8 +9,12 @@ import {
DecorateAcknowledgementsWithTimeoutAndMultipleResponses,
AllButLast,
Last,
FirstArg,
SecondArg,
DecorateAcknowledgementsWithMultipleResponses,
DecorateAcknowledgements,
RemoveAcknowledgements,
EventNamesWithAck,
FirstNonErrorArg,
EventNamesWithoutAck,
} from "./typed-events";
import type { Client } from "./client";
import debugModule from "debug";
@@ -117,7 +121,7 @@ export class Namespace<
SocketData = any
> extends StrictEventEmitter<
ServerSideEvents,
EmitEvents,
RemoveAcknowledgements<EmitEvents>,
NamespaceReservedEventsMap<
ListenEvents,
EmitEvents,
@@ -252,7 +256,10 @@ export class Namespace<
* @return a new {@link BroadcastOperator} instance for chaining
*/
public to(room: Room | Room[]) {
return new BroadcastOperator<EmitEvents, SocketData>(this.adapter).to(room);
return new BroadcastOperator<
DecorateAcknowledgementsWithMultipleResponses<EmitEvents>,
SocketData
>(this.adapter).to(room);
}
/**
@@ -268,7 +275,10 @@ export class Namespace<
* @return a new {@link BroadcastOperator} instance for chaining
*/
public in(room: Room | Room[]) {
return new BroadcastOperator<EmitEvents, SocketData>(this.adapter).in(room);
return new BroadcastOperator<
DecorateAcknowledgementsWithMultipleResponses<EmitEvents>,
SocketData
>(this.adapter).in(room);
}
/**
@@ -290,9 +300,10 @@ export class Namespace<
* @return a new {@link BroadcastOperator} instance for chaining
*/
public except(room: Room | Room[]) {
return new BroadcastOperator<EmitEvents, SocketData>(this.adapter).except(
room
);
return new BroadcastOperator<
DecorateAcknowledgementsWithMultipleResponses<EmitEvents>,
SocketData
>(this.adapter).except(room);
}
/**
@@ -430,7 +441,7 @@ export class Namespace<
*
* @return Always true
*/
public emit<Ev extends EventNames<EmitEvents>>(
public emit<Ev extends EventNamesWithoutAck<EmitEvents>>(
ev: Ev,
...args: EventParams<EmitEvents, Ev>
): boolean {
@@ -440,30 +451,6 @@ export class Namespace<
);
}
/**
* Emits an event and waits for an acknowledgement from all clients.
*
* @example
* const myNamespace = io.of("/my-namespace");
*
* try {
* const responses = await myNamespace.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
* }
*
* @return a Promise that will be fulfilled when all clients have acknowledged the event
*/
public emitWithAck<Ev extends EventNames<EmitEvents>>(
ev: Ev,
...args: AllButLast<EventParams<EmitEvents, Ev>>
): Promise<SecondArg<Last<EventParams<EmitEvents, Ev>>>> {
return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter
).emitWithAck(ev, ...args);
}
/**
* Sends a `message` event to all clients.
*
@@ -482,7 +469,9 @@ export class Namespace<
* @return self
*/
public send(...args: EventParams<EmitEvents, "message">): this {
this.emit("message", ...args);
// This type-cast is needed because EmitEvents likely doesn't have `message` as a key.
// if you specify the EmitEvents, the type of args will be never.
this.emit("message" as any, ...args);
return this;
}
@@ -492,7 +481,9 @@ export class Namespace<
* @return self
*/
public write(...args: EventParams<EmitEvents, "message">): this {
this.emit("message", ...args);
// This type-cast is needed because EmitEvents likely doesn't have `message` as a key.
// if you specify the EmitEvents, the type of args will be never.
this.emit("message" as any, ...args);
return this;
}
@@ -557,10 +548,10 @@ export class Namespace<
*
* @return a Promise that will be fulfilled when all servers have acknowledged the event
*/
public serverSideEmitWithAck<Ev extends EventNames<ServerSideEvents>>(
public serverSideEmitWithAck<Ev extends EventNamesWithAck<ServerSideEvents>>(
ev: Ev,
...args: AllButLast<EventParams<ServerSideEvents, Ev>>
): Promise<FirstArg<Last<EventParams<ServerSideEvents, Ev>>>[]> {
): Promise<FirstNonErrorArg<Last<EventParams<ServerSideEvents, Ev>>>[]> {
return new Promise((resolve, reject) => {
args.push((err, responses) => {
if (err) {
@@ -612,9 +603,10 @@ export class Namespace<
* @return self
*/
public compress(compress: boolean) {
return new BroadcastOperator<EmitEvents, SocketData>(this.adapter).compress(
compress
);
return new BroadcastOperator<
DecorateAcknowledgementsWithMultipleResponses<EmitEvents>,
SocketData
>(this.adapter).compress(compress);
}
/**
@@ -630,7 +622,10 @@ export class Namespace<
* @return self
*/
public get volatile() {
return new BroadcastOperator<EmitEvents, SocketData>(this.adapter).volatile;
return new BroadcastOperator<
DecorateAcknowledgementsWithMultipleResponses<EmitEvents>,
SocketData
>(this.adapter).volatile;
}
/**
@@ -645,7 +640,10 @@ export class Namespace<
* @return a new {@link BroadcastOperator} instance for chaining
*/
public get local() {
return new BroadcastOperator<EmitEvents, SocketData>(this.adapter).local;
return new BroadcastOperator<
DecorateAcknowledgementsWithMultipleResponses<EmitEvents>,
SocketData
>(this.adapter).local;
}
/**
@@ -665,9 +663,10 @@ export class Namespace<
* @param timeout
*/
public timeout(timeout: number) {
return new BroadcastOperator<EmitEvents, SocketData>(this.adapter).timeout(
timeout
);
return new BroadcastOperator<
DecorateAcknowledgementsWithMultipleResponses<EmitEvents>,
SocketData
>(this.adapter).timeout(timeout);
}
/**

View File

@@ -2,9 +2,9 @@ import { Namespace } from "./namespace";
import type { Server, RemoteSocket } from "./index";
import type {
EventParams,
EventNames,
EventsMap,
DefaultEventsMap,
EventNamesWithoutAck,
} from "./typed-events";
import type { BroadcastOptions } from "socket.io-adapter";
import debugModule from "debug";
@@ -56,7 +56,7 @@ export class ParentNamespace<
this.adapter = { broadcast };
}
public emit<Ev extends EventNames<EmitEvents>>(
public emit<Ev extends EventNamesWithoutAck<EmitEvents>>(
ev: Ev,
...args: EventParams<EmitEvents, Ev>
): boolean {

View File

@@ -7,9 +7,10 @@ import {
DecorateAcknowledgementsWithMultipleResponses,
DefaultEventsMap,
EventNames,
EventNamesWithAck,
EventParams,
EventsMap,
FirstArg,
FirstNonErrorArg,
Last,
StrictEventEmitter,
} from "./typed-events";
@@ -292,16 +293,16 @@ export class Socket<
*/
private buildHandshake(auth: object): Handshake {
return {
headers: this.request.headers,
headers: this.request?.headers || {},
time: new Date() + "",
address: this.conn.remoteAddress,
xdomain: !!this.request.headers.origin,
xdomain: !!this.request?.headers.origin,
// @ts-ignore
secure: !!this.request.connection.encrypted,
secure: !this.request || !!this.request.connection.encrypted,
issued: +new Date(),
url: this.request.url!,
url: this.request?.url!,
// @ts-ignore
query: this.request._query,
query: this.request?._query || {},
auth,
};
}
@@ -383,10 +384,10 @@ export class Socket<
*
* @return a Promise that will be fulfilled when the client acknowledges the event
*/
public emitWithAck<Ev extends EventNames<EmitEvents>>(
public emitWithAck<Ev extends EventNamesWithAck<EmitEvents>>(
ev: Ev,
...args: AllButLast<EventParams<EmitEvents, Ev>>
): Promise<FirstArg<Last<EventParams<EmitEvents, Ev>>>> {
): Promise<FirstNonErrorArg<Last<EventParams<EmitEvents, Ev>>>> {
// the timeout flag is optional
const withErr = this.flags.timeout !== undefined;
return new Promise((resolve, reject) => {
@@ -758,7 +759,6 @@ export class Socket<
}
this._cleanup();
this.nsp._remove(this);
this.client._remove(this);
this.connected = false;
this.emitReserved("disconnect", reason, description);
@@ -772,6 +772,7 @@ export class Socket<
*/
_cleanup() {
this.leaveAll();
this.nsp._remove(this);
this.join = noop;
}

View File

@@ -1,5 +1,4 @@
import { EventEmitter } from "events";
/**
* An events map is an interface that maps event names to their value, which
* represents the type of the `on` listener.
@@ -21,6 +20,62 @@ export interface DefaultEventsMap {
*/
export type EventNames<Map extends EventsMap> = keyof Map & (string | symbol);
/**
* Returns a union type containing all the keys of an event map that have an acknowledgement callback.
*
* That also have *some* data coming in.
*/
export type EventNamesWithAck<
Map extends EventsMap,
K extends EventNames<Map> = EventNames<Map>
> = IfAny<
Last<Parameters<Map[K]>> | Map[K],
K,
K extends (
Last<Parameters<Map[K]>> extends (...args: any[]) => any
? FirstNonErrorArg<Last<Parameters<Map[K]>>> extends void
? never
: K
: never
)
? K
: never
>;
/**
* Returns a union type containing all the keys of an event map that have an acknowledgement callback.
*
* That also have *some* data coming in.
*/
export type EventNamesWithoutAck<
Map extends EventsMap,
K extends EventNames<Map> = EventNames<Map>
> = IfAny<
Last<Parameters<Map[K]>> | Map[K],
K,
K extends (
Last<Parameters<Map[K]>> extends (...args: any[]) => any ? never : K
)
? K
: never
>;
export type RemoveAcknowledgements<E extends EventsMap> = {
[K in EventNamesWithoutAck<E>]: E[K];
};
export type EventNamesWithError<
Map extends EventsMap,
K extends EventNamesWithAck<Map> = EventNamesWithAck<Map>
> = IfAny<
Last<Parameters<Map[K]>> | Map[K],
K,
K extends (
LooseParameters<Last<Parameters<Map[K]>>>[0] extends Error ? K : never
)
? K
: never
>;
/** The tuple type representing the parameters of an event listener */
export type EventParams<
Map extends EventsMap,
@@ -178,33 +233,96 @@ export abstract class StrictEventEmitter<
>[];
}
}
/**
* Returns a boolean for whether the given type is `any`.
*
* @link https://stackoverflow.com/a/49928360/1490091
*
* Useful in type utilities, such as disallowing `any`s to be passed to a function.
*
* @author sindresorhus
* @link https://github.com/sindresorhus/type-fest
*/
type IsAny<T> = 0 extends 1 & T ? true : false;
export type Last<T extends any[]> = T extends [...infer H, infer L] ? L : any;
/**
* An if-else-like type that resolves depending on whether the given type is `any`.
*
* @see {@link IsAny}
*
* @author sindresorhus
* @link https://github.com/sindresorhus/type-fest
*/
type IfAny<T, TypeIfAny = true, TypeIfNotAny = false> = IsAny<T> extends true
? TypeIfAny
: TypeIfNotAny;
/**
* Extracts the type of the last element of an array.
*
* Use-case: Defining the return type of functions that extract the last element of an array, for example [`lodash.last`](https://lodash.com/docs/4.17.15#last).
*
* @author sindresorhus
* @link https://github.com/sindresorhus/type-fest
*/
export type Last<ValueType extends readonly unknown[]> =
ValueType extends readonly [infer ElementType]
? ElementType
: ValueType extends readonly [infer _, ...infer Tail]
? Last<Tail>
: ValueType extends ReadonlyArray<infer ElementType>
? ElementType
: never;
export type FirstNonErrorTuple<T extends unknown[]> = T[0] extends Error
? T[1]
: T[0];
export type AllButLast<T extends any[]> = T extends [...infer H, infer L]
? H
: any[];
export type FirstArg<T> = T extends (arg: infer Param) => infer Result
? Param
: any;
export type SecondArg<T> = T extends (
err: Error,
arg: infer Param
) => infer Result
? Param
: any;
/**
* Like `Parameters<T>`, but doesn't require `T` to be a function ahead of time.
*/
type LooseParameters<T> = T extends (...args: infer P) => any ? P : never;
export type FirstNonErrorArg<T> = T extends (...args: infer Params) => any
? FirstNonErrorTuple<Params>
: any;
type PrependTimeoutError<T extends any[]> = {
[K in keyof T]: T[K] extends (...args: infer Params) => infer Result
? (err: Error, ...args: Params) => Result
? Params[0] extends Error
? T[K]
: (err: Error, ...args: Params) => Result
: T[K];
};
export type MultiplyArray<T extends unknown[]> = {
[K in keyof T]: T[K][];
};
type InferFirstAndPreserveLabel<T extends any[]> = T extends [any, ...infer R]
? T extends [...infer H, ...R]
? H
: never
: never;
/**
* Utility type to decorate the acknowledgement callbacks multiple values
* on the first non error element while removing any elements after
*/
type ExpectMultipleResponses<T extends any[]> = {
[K in keyof T]: T[K] extends (err: Error, arg: infer Param) => infer Result
? (err: Error, arg: Param[]) => Result
[K in keyof T]: T[K] extends (...args: infer Params) => infer Result
? Params extends [Error]
? (err: Error) => Result
: Params extends [Error, ...infer Rest]
? (
err: Error,
...args: InferFirstAndPreserveLabel<MultiplyArray<Rest>>
) => Result
: Params extends []
? () => Result
: (...args: InferFirstAndPreserveLabel<MultiplyArray<Params>>) => Result
: T[K];
};
/**
* Utility type to decorate the acknowledgement callbacks with a timeout error.
*

157
package-lock.json generated
View File

@@ -1,19 +1,19 @@
{
"name": "socket.io",
"version": "4.7.0",
"version": "4.7.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "socket.io",
"version": "4.7.0",
"version": "4.7.2",
"license": "MIT",
"dependencies": {
"accepts": "~1.3.4",
"base64id": "~2.0.0",
"cors": "~2.8.5",
"debug": "~4.3.2",
"engine.io": "~6.5.0",
"engine.io": "~6.5.2",
"socket.io-adapter": "~2.5.2",
"socket.io-parser": "~4.2.4"
},
@@ -24,17 +24,17 @@
"nyc": "^15.1.0",
"prettier": "^2.3.2",
"rimraf": "^3.0.2",
"socket.io-client": "4.7.1",
"socket.io-client": "4.7.2",
"socket.io-client-v2": "npm:socket.io-client@^2.4.0",
"superagent": "^8.0.0",
"supertest": "^6.1.6",
"ts-node": "^10.2.1",
"tsd": "^0.21.0",
"tsd": "^0.27.0",
"typescript": "^4.4.2",
"uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.30.0"
},
"engines": {
"node": ">=10.0.0"
"node": ">=10.2.0"
}
},
"node_modules/@ampproject/remapping": {
@@ -646,14 +646,10 @@
"dev": true
},
"node_modules/@tsd/typescript": {
"version": "4.7.4",
"resolved": "https://registry.npmjs.org/@tsd/typescript/-/typescript-4.7.4.tgz",
"integrity": "sha512-jbtC+RgKZ9Kk65zuRZbKLTACf+tvFW4Rfq0JEMXrlmV3P3yme+Hm+pnb5fJRyt61SjIitcrC810wj7+1tgsEmg==",
"dev": true,
"bin": {
"tsc": "typescript/bin/tsc",
"tsserver": "typescript/bin/tsserver"
}
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/@tsd/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-+UgxOvJUl5rQdPFSSOOwhmSmpThm8DJ3HwHxAOq5XYe7CcmG1LcM2QeqWwILzUIT5tbeMqY8qABiCsRtIjk/2g==",
"dev": true
},
"node_modules/@types/cookie": {
"version": "0.4.1",
@@ -1378,9 +1374,9 @@
"dev": true
},
"node_modules/engine.io": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.0.tgz",
"integrity": "sha512-UlfoK1iD62Hkedw2TmuHdhDsZCGaAyp+LZ/AvnImjYBeWagA3qIEETum90d6shMeFZiDuGT66zVCdx1wKYKGGg==",
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.2.tgz",
"integrity": "sha512-IXsMcGpw/xRfjra46sVZVHiSWo/nJ/3g1337q9KNXtS6YRzbW5yIzTCb9DjhrBe7r3GZQR0I4+nq+4ODk5g/cA==",
"dependencies": {
"@types/cookie": "^0.4.1",
"@types/cors": "^2.8.12",
@@ -1390,30 +1386,30 @@
"cookie": "~0.4.1",
"cors": "~2.8.5",
"debug": "~4.3.1",
"engine.io-parser": "~5.1.0",
"engine.io-parser": "~5.2.1",
"ws": "~8.11.0"
},
"engines": {
"node": ">=10.0.0"
"node": ">=10.2.0"
}
},
"node_modules/engine.io-client": {
"version": "6.5.1",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.1.tgz",
"integrity": "sha512-hE5wKXH8Ru4L19MbM1GgYV/2Qo54JSMh1rlJbfpa40bEWkCKNo3ol2eOtGmowcr+ysgbI7+SGL+by42Q3pt/Ng==",
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.2.tgz",
"integrity": "sha512-CQZqbrpEYnrpGqC07a9dJDz4gePZUgTPMU3NKJPSeQOyw27Tst4Pl3FemKoFGAlHzgZmKjoRmiJvbWfhCXUlIg==",
"dev": true,
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1",
"engine.io-parser": "~5.1.0",
"engine.io-parser": "~5.2.1",
"ws": "~8.11.0",
"xmlhttprequest-ssl": "~2.0.0"
}
},
"node_modules/engine.io-parser": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.1.0.tgz",
"integrity": "sha512-enySgNiK5tyZFynt3z7iqBR+Bto9EVVVvDFuTT0ioHCGbzirZVGDGiQjZzEp8hWl6hd5FSVytJGuScX1C1C35w==",
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz",
"integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==",
"engines": {
"node": ">=10.0.0"
}
@@ -1877,6 +1873,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/hasha/node_modules/type-fest": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
"integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/he": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
@@ -3199,6 +3204,15 @@
"node": ">=8"
}
},
"node_modules/read-pkg-up/node_modules/type-fest": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
"integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/read-pkg/node_modules/hosted-git-info": {
"version": "2.8.9",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
@@ -3464,14 +3478,14 @@
}
},
"node_modules/socket.io-client": {
"version": "4.7.1",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.1.tgz",
"integrity": "sha512-Qk3Xj8ekbnzKu3faejo4wk2MzXA029XppiXtTF/PkbTg+fcwaTw1PlDrTrrrU4mKoYC4dvlApOnSeyLCKwek2w==",
"version": "4.7.2",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.2.tgz",
"integrity": "sha512-vtA0uD4ibrYD793SOIAwlo8cj6haOeMHrGvwPxJsxH7CeIksqJ+3Zc06RvWTIFgiSqx4A3sOnTXpfAEE2Zyz6w==",
"dev": true,
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.2",
"engine.io-client": "~6.5.1",
"engine.io-client": "~6.5.2",
"socket.io-parser": "~4.2.4"
},
"engines": {
@@ -4014,12 +4028,12 @@
}
},
"node_modules/tsd": {
"version": "0.21.0",
"resolved": "https://registry.npmjs.org/tsd/-/tsd-0.21.0.tgz",
"integrity": "sha512-6DugCw1Q4H8HYwDT3itzgALjeDxN4RO3iqu7gRdC/YNVSCRSGXRGQRRasftL1uKDuKxlFffYKHv5j5G7YnKGxQ==",
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/tsd/-/tsd-0.27.0.tgz",
"integrity": "sha512-G/2Sejk9N21TcuWlHwrvVWwIyIl2mpECFPbnJvFMsFN1xQCIbi2QnvG4fkw3VitFhNF6dy38cXxKJ8Paq8kOGQ==",
"dev": true,
"dependencies": {
"@tsd/typescript": "~4.7.3",
"@tsd/typescript": "~4.9.5",
"eslint-formatter-pretty": "^4.1.0",
"globby": "^11.0.1",
"meow": "^9.0.0",
@@ -4030,16 +4044,7 @@
"tsd": "dist/cli.js"
},
"engines": {
"node": ">=12"
}
},
"node_modules/type-fest": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
"integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
"dev": true,
"engines": {
"node": ">=8"
"node": ">=14.16"
}
},
"node_modules/typedarray-to-buffer": {
@@ -4825,9 +4830,9 @@
"dev": true
},
"@tsd/typescript": {
"version": "4.7.4",
"resolved": "https://registry.npmjs.org/@tsd/typescript/-/typescript-4.7.4.tgz",
"integrity": "sha512-jbtC+RgKZ9Kk65zuRZbKLTACf+tvFW4Rfq0JEMXrlmV3P3yme+Hm+pnb5fJRyt61SjIitcrC810wj7+1tgsEmg==",
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/@tsd/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-+UgxOvJUl5rQdPFSSOOwhmSmpThm8DJ3HwHxAOq5XYe7CcmG1LcM2QeqWwILzUIT5tbeMqY8qABiCsRtIjk/2g==",
"dev": true
},
"@types/cookie": {
@@ -5388,9 +5393,9 @@
"dev": true
},
"engine.io": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.0.tgz",
"integrity": "sha512-UlfoK1iD62Hkedw2TmuHdhDsZCGaAyp+LZ/AvnImjYBeWagA3qIEETum90d6shMeFZiDuGT66zVCdx1wKYKGGg==",
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.2.tgz",
"integrity": "sha512-IXsMcGpw/xRfjra46sVZVHiSWo/nJ/3g1337q9KNXtS6YRzbW5yIzTCb9DjhrBe7r3GZQR0I4+nq+4ODk5g/cA==",
"requires": {
"@types/cookie": "^0.4.1",
"@types/cors": "^2.8.12",
@@ -5400,27 +5405,27 @@
"cookie": "~0.4.1",
"cors": "~2.8.5",
"debug": "~4.3.1",
"engine.io-parser": "~5.1.0",
"engine.io-parser": "~5.2.1",
"ws": "~8.11.0"
}
},
"engine.io-client": {
"version": "6.5.1",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.1.tgz",
"integrity": "sha512-hE5wKXH8Ru4L19MbM1GgYV/2Qo54JSMh1rlJbfpa40bEWkCKNo3ol2eOtGmowcr+ysgbI7+SGL+by42Q3pt/Ng==",
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.2.tgz",
"integrity": "sha512-CQZqbrpEYnrpGqC07a9dJDz4gePZUgTPMU3NKJPSeQOyw27Tst4Pl3FemKoFGAlHzgZmKjoRmiJvbWfhCXUlIg==",
"dev": true,
"requires": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1",
"engine.io-parser": "~5.1.0",
"engine.io-parser": "~5.2.1",
"ws": "~8.11.0",
"xmlhttprequest-ssl": "~2.0.0"
}
},
"engine.io-parser": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.1.0.tgz",
"integrity": "sha512-enySgNiK5tyZFynt3z7iqBR+Bto9EVVVvDFuTT0ioHCGbzirZVGDGiQjZzEp8hWl6hd5FSVytJGuScX1C1C35w=="
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz",
"integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ=="
},
"error-ex": {
"version": "1.3.2",
@@ -5754,6 +5759,14 @@
"requires": {
"is-stream": "^2.0.0",
"type-fest": "^0.8.0"
},
"dependencies": {
"type-fest": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
"integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
"dev": true
}
}
},
"he": {
@@ -6754,6 +6767,12 @@
"requires": {
"p-limit": "^2.2.0"
}
},
"type-fest": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
"integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
"dev": true
}
}
},
@@ -6923,14 +6942,14 @@
}
},
"socket.io-client": {
"version": "4.7.1",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.1.tgz",
"integrity": "sha512-Qk3Xj8ekbnzKu3faejo4wk2MzXA029XppiXtTF/PkbTg+fcwaTw1PlDrTrrrU4mKoYC4dvlApOnSeyLCKwek2w==",
"version": "4.7.2",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.2.tgz",
"integrity": "sha512-vtA0uD4ibrYD793SOIAwlo8cj6haOeMHrGvwPxJsxH7CeIksqJ+3Zc06RvWTIFgiSqx4A3sOnTXpfAEE2Zyz6w==",
"dev": true,
"requires": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.2",
"engine.io-client": "~6.5.1",
"engine.io-client": "~6.5.2",
"socket.io-parser": "~4.2.4"
}
},
@@ -7346,12 +7365,12 @@
}
},
"tsd": {
"version": "0.21.0",
"resolved": "https://registry.npmjs.org/tsd/-/tsd-0.21.0.tgz",
"integrity": "sha512-6DugCw1Q4H8HYwDT3itzgALjeDxN4RO3iqu7gRdC/YNVSCRSGXRGQRRasftL1uKDuKxlFffYKHv5j5G7YnKGxQ==",
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/tsd/-/tsd-0.27.0.tgz",
"integrity": "sha512-G/2Sejk9N21TcuWlHwrvVWwIyIl2mpECFPbnJvFMsFN1xQCIbi2QnvG4fkw3VitFhNF6dy38cXxKJ8Paq8kOGQ==",
"dev": true,
"requires": {
"@tsd/typescript": "~4.7.3",
"@tsd/typescript": "~4.9.5",
"eslint-formatter-pretty": "^4.1.0",
"globby": "^11.0.1",
"meow": "^9.0.0",
@@ -7359,12 +7378,6 @@
"read-pkg-up": "^7.0.0"
}
},
"type-fest": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
"integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
"dev": true
},
"typedarray-to-buffer": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "socket.io",
"version": "4.7.1",
"version": "4.7.3",
"description": "node.js realtime framework server",
"keywords": [
"realtime",
@@ -50,7 +50,7 @@
"base64id": "~2.0.0",
"cors": "~2.8.5",
"debug": "~4.3.2",
"engine.io": "~6.5.0",
"engine.io": "~6.5.2",
"socket.io-adapter": "~2.5.2",
"socket.io-parser": "~4.2.4"
},
@@ -61,12 +61,12 @@
"nyc": "^15.1.0",
"prettier": "^2.3.2",
"rimraf": "^3.0.2",
"socket.io-client": "4.7.1",
"socket.io-client": "4.7.2",
"socket.io-client-v2": "npm:socket.io-client@^2.4.0",
"superagent": "^8.0.0",
"supertest": "^6.1.6",
"ts-node": "^10.2.1",
"tsd": "^0.21.0",
"tsd": "^0.27.0",
"typescript": "^4.4.2",
"uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.30.0"
},
@@ -89,7 +89,7 @@
}
],
"engines": {
"node": ">=10.0.0"
"node": ">=10.2.0"
},
"tsd": {
"directory": "test"

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