Compare commits

..

21 Commits
4.7.2 ... 4.7.4

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

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

- CommonJS
- ES modules
- TypeScript

Related: https://github.com/socketio/socket.io/pull/4787
2023-09-13 15:56:15 +02:00
Damien Arrachequesne
8259cdac84 docs: use io.engine.use() with express-session
Related: https://github.com/socketio/socket.io/discussions/4819
2023-09-13 12:13:03 +02:00
Damien Arrachequesne
fd9dd74eee docs: use "connection" instead of "connect"
"connect" and "connection" have the same meaning, but "connection" is
the preferred version.
2023-08-12 10:10:55 +02:00
98 changed files with 8862 additions and 1014 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,5 +1,10 @@
# History
## 2024
- [4.7.4](#474-2024-01-12) (Jan 2024)
- [4.7.3](#473-2024-01-03) (Jan 2024)
## 2023
- [4.7.2](#472-2023-08-02) (Aug 2023)
@@ -61,6 +66,37 @@
# Release notes
## [4.7.4](https://github.com/socketio/socket.io/compare/4.7.3...4.7.4) (2024-01-12)
### Bug Fixes
* **typings:** calling io.emit with no arguments incorrectly errored ([cb6d2e0](https://github.com/socketio/socket.io/commit/cb6d2e02aa7ec03c2de1817d35cffa1128b107ef)), closes [#4914](https://github.com/socketio/socket.io/issues/4914)
### Dependencies
- [`engine.io@~6.5.2`](https://github.com/socketio/engine.io/releases/tag/6.5.2) (no change)
- [`ws@~8.11.0`](https://github.com/websockets/ws/releases/tag/8.11.0) (no change)
## [4.7.3](https://github.com/socketio/socket.io/compare/4.7.2...4.7.3) (2024-01-03)
### Bug Fixes
* return the first response when broadcasting to a single socket ([#4878](https://github.com/socketio/socket.io/issues/4878)) ([df8e70f](https://github.com/socketio/socket.io/commit/df8e70f79822e3887b4f21ca718af8a53bbda2c4))
* **typings:** allow to bind to a non-secure Http2Server ([#4853](https://github.com/socketio/socket.io/issues/4853)) ([8c9ebc3](https://github.com/socketio/socket.io/commit/8c9ebc30e5452ff9381af5d79f547394fa55633c))
### Dependencies
- [`engine.io@~6.5.2`](https://github.com/socketio/engine.io/releases/tag/6.5.2) (no change)
- [`ws@~8.11.0`](https://github.com/websockets/ws/releases/tag/8.11.0) (no change)
## [4.7.2](https://github.com/socketio/socket.io/compare/4.7.1...4.7.2) (2023-08-02)
@@ -855,7 +891,7 @@ new Server(3000, {
const socket = io("/admin");
// server-side
io.on("connect", socket => {
io.on("connection", socket => {
// not triggered anymore
})
@@ -1006,7 +1042,7 @@ new Server(3000, {
const socket = io("/admin");
// server-side
io.on("connect", socket => {
io.on("connection", socket => {
// not triggered anymore
})

View File

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

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
/*!
* Socket.IO v4.7.2
* (c) 2014-2023 Guillermo Rauch
* Socket.IO v4.7.4
* (c) 2014-2024 Guillermo Rauch
* Released under the MIT License.
*/
(function (global, factory) {

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) => {

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";
@@ -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) => {

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,64 @@ 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 (Parameters<Map[K]> extends never[] ? K : never)
? 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 +235,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.
*

103
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "socket.io",
"version": "4.7.1",
"version": "4.7.3",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "socket.io",
"version": "4.7.1",
"version": "4.7.3",
"license": "MIT",
"dependencies": {
"accepts": "~1.3.4",
@@ -24,12 +24,12 @@
"nyc": "^15.1.0",
"prettier": "^2.3.2",
"rimraf": "^3.0.2",
"socket.io-client": "4.7.2",
"socket.io-client": "4.7.4",
"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"
},
@@ -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",
@@ -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,9 +3478,9 @@
}
},
"node_modules/socket.io-client": {
"version": "4.7.2",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.2.tgz",
"integrity": "sha512-vtA0uD4ibrYD793SOIAwlo8cj6haOeMHrGvwPxJsxH7CeIksqJ+3Zc06RvWTIFgiSqx4A3sOnTXpfAEE2Zyz6w==",
"version": "4.7.4",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.4.tgz",
"integrity": "sha512-wh+OkeF0rAVCrABWQBaEjLfb7DVPotMbu0cgWgyR0v6eA4EoVnAwcIeIbcdTE3GT/H3kbdLl7OoH2+asoDRIIg==",
"dev": true,
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
@@ -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": {
@@ -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,9 +6942,9 @@
}
},
"socket.io-client": {
"version": "4.7.2",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.2.tgz",
"integrity": "sha512-vtA0uD4ibrYD793SOIAwlo8cj6haOeMHrGvwPxJsxH7CeIksqJ+3Zc06RvWTIFgiSqx4A3sOnTXpfAEE2Zyz6w==",
"version": "4.7.4",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.4.tgz",
"integrity": "sha512-wh+OkeF0rAVCrABWQBaEjLfb7DVPotMbu0cgWgyR0v6eA4EoVnAwcIeIbcdTE3GT/H3kbdLl7OoH2+asoDRIIg==",
"dev": true,
"requires": {
"@socket.io/component-emitter": "~3.1.0",
@@ -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.2",
"version": "4.7.4",
"description": "node.js realtime framework server",
"keywords": [
"realtime",
@@ -61,12 +61,12 @@
"nyc": "^15.1.0",
"prettier": "^2.3.2",
"rimraf": "^3.0.2",
"socket.io-client": "4.7.2",
"socket.io-client": "4.7.4",
"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"
},

View File

@@ -222,11 +222,11 @@ describe("connection state recovery", () => {
class DummyAdapter extends Adapter {
override persistSession(session) {
expect.fail();
expect().fail();
}
override restoreSession(pid, offset) {
expect.fail();
expect().fail();
return Promise.reject("should not happen");
}
}

View File

@@ -526,7 +526,7 @@ describe("messaging many", () => {
]).then(async () => {
try {
await io.timeout(200).emitWithAck("some event");
expect.fail();
expect().fail();
} catch (err) {
expect(err).to.be.an(Error);
// @ts-ignore

View File

@@ -6,6 +6,7 @@ import {
createClient,
successFn,
createPartialDone,
assert,
} from "./support/util";
describe("namespaces", () => {
@@ -62,7 +63,7 @@ describe("namespaces", () => {
const io = new Server(0);
const clientSocket = createClient(io);
io.on("connect", (socket) => {
io.on("connection", (socket) => {
expect(socket).to.be.a(Socket);
success(done, io, clientSocket);
});
@@ -417,6 +418,7 @@ describe("namespaces", () => {
socket1.on("a", successFn(done, io, socket1, socket2));
socket2.on("connect", () => {
assert(socket2.id);
io.except(socket2.id).emit("a");
});
});
@@ -435,6 +437,7 @@ describe("namespaces", () => {
socket1.on("a", successFn(done, io, socket1, socket2));
socket2.on("connect", () => {
assert(socket2.id);
nsp.except(socket2.id).emit("a");
});
});

View File

@@ -62,7 +62,7 @@ describe("timeout", () => {
io.on("connection", async (socket) => {
try {
await socket.timeout(50).emitWithAck("unknown");
expect.fail();
expect().fail();
} catch (err) {
expect(err).to.be.an(Error);
success(done, io, client);

View File

@@ -1,10 +1,14 @@
"use strict";
import { Namespace, Server, Socket } from "..";
import type { DefaultEventsMap } from "../lib/typed-events";
import { createServer } from "http";
import { expectError, expectType } from "tsd";
import { Adapter } from "socket.io-adapter";
import type { DisconnectReason } from "../lib/socket";
import { expectType } from "tsd";
import {
BroadcastOperator,
Server,
Socket,
type DisconnectReason,
} from "../lib/index";
import type { DefaultEventsMap, EventsMap } from "../lib/typed-events";
// This file is run by tsd, not mocha.
@@ -24,7 +28,7 @@ describe("server", () => {
expectType<DisconnectReason>(reason);
});
});
sio.on("connect", (s) => {
sio.on("connection", (s) => {
expectType<Socket<DefaultEventsMap, DefaultEventsMap>>(s);
});
done();
@@ -61,8 +65,7 @@ describe("server", () => {
}
sio.on(Events.CONNECTION, (socket) => {
// TODO(#3833): Make this expect `Socket<DefaultEventsMap, DefaultEventsMap>`
expectType<any>(socket);
expectType<Socket<DefaultEventsMap, DefaultEventsMap>>(socket);
socket.on("test", (a, b, c) => {
expectType<any>(a);
@@ -85,6 +88,8 @@ describe("server", () => {
const srv = createServer();
const sio = new Server(srv);
srv.listen(() => {
sio.emit("random", 1, "2", [3]);
sio.emit("no parameters");
sio.on("connection", (s) => {
s.emit("random", 1, "2", [3]);
s.emit("no parameters");
@@ -92,6 +97,17 @@ describe("server", () => {
});
});
});
describe("send", () => {
it("accepts any parameters", () => {
const srv = createServer();
const sio = new Server(srv);
const nio = sio.of("/test");
sio.send(1, "2", [3]);
sio.send();
nio.send(1, "2", [3]);
nio.send();
});
});
describe("emitWithAck", () => {
it("accepts any parameters", () => {
@@ -144,79 +160,363 @@ describe("server", () => {
const sio = new Server<BidirectionalEvents, BidirectionalEvents, {}>(
srv
);
expectError(sio.on("random", (a, b, c) => {}));
// @ts-expect-error - shouldn't accept arguments of the wrong types
sio.on("random", (a, b, c) => {});
srv.listen(() => {
expectError(sio.on("wrong name", (s) => {}));
// @ts-expect-error - shouldn't accept arguments of the wrong types
sio.on("wrong name", (s) => {});
sio.on("connection", (s) => {
s.on("random", (a, b, c) => {});
expectError(s.on("random"));
expectError(s.on("random", (a, b, c, d) => {}));
expectError(s.on(2, 3));
});
});
});
});
describe("emit", () => {
it("accepts arguments of the correct types", () => {
const srv = createServer();
const sio = new Server<BidirectionalEvents>(srv);
srv.listen(() => {
sio.on("connection", (s) => {
s.emit("random", 1, "2", [3]);
});
});
});
it("does not accept arguments of the wrong types", () => {
const srv = createServer();
const sio = new Server<BidirectionalEvents>(srv);
srv.listen(() => {
sio.on("connection", (s) => {
expectError(s.emit("noParameter", 2));
expectError(s.emit("oneParameter"));
expectError(s.emit("random"));
expectError(s.emit("oneParameter", 2, 3));
expectError(s.emit("random", (a, b, c) => {}));
expectError(s.emit("wrong name", () => {}));
expectError(s.emit("complicated name with spaces", 2));
// @ts-expect-error - shouldn't accept arguments of the wrong types
s.on("random");
// @ts-expect-error - shouldn't accept arguments of the wrong types
s.on("random", (a, b, c, d) => {});
// @ts-expect-error - shouldn't accept arguments of the wrong types
s.on(2, 3);
});
});
});
});
});
type ToEmit<Map extends EventsMap, Ev extends keyof Map = keyof Map> = (
ev: Ev,
...args: Parameters<Map[Ev]>
) => boolean;
type ToEmitWithAck<
Map extends EventsMap,
Ev extends keyof Map = keyof Map
> = (ev: Ev, ...args: Parameters<Map[Ev]>) => ReturnType<Map[Ev]>;
interface ClientToServerEvents {
noArgs: () => void;
helloFromClient: (message: string) => void;
ackFromClient: (
a: string,
b: number,
ack: (c: string, d: number) => void
) => void;
}
interface ServerToClientEvents {
noArgs: () => void;
helloFromServer: (message: string, x: number) => void;
ackFromServer: (
a: boolean,
b: string,
ack: (c: boolean, d: string) => void
) => void;
ackFromServerSingleArg: (
a: boolean,
b: string,
ack: (c: string) => void
) => void;
onlyCallback: (a: () => void) => void;
}
// While these could be generated using the types from typed-events,
// it's likely better to just write them out, so that both the types and this are tested properly
interface ServerToClientEventsNoAck {
noArgs: () => void;
helloFromServer: (message: string, x: number) => void;
ackFromServer: never;
ackFromServerSingleArg: never;
onlyCallback: never;
}
interface ServerToClientEventsWithError {
noArgs: () => void;
helloFromServer: (message: string, x: number) => void;
ackFromServer: (
a: boolean,
b: string,
ack: (err: Error, c: boolean, d: string) => void
) => void;
ackFromServerSingleArg: (
a: boolean,
b: string,
ack: (err: Error, c: string) => void
) => void;
onlyCallback: (a: (err: Error) => void) => void;
}
interface ServerToClientEventsWithMultiple {
noArgs: () => void;
helloFromServer: (message: string, x: number) => void;
ackFromServer: (a: boolean, b: string, ack: (c: boolean[]) => void) => void;
ackFromServerSingleArg: (
a: boolean,
b: string,
ack: (c: string[]) => void
) => void;
onlyCallback: (a: () => void) => void;
}
interface ServerToClientEventsWithMultipleAndError {
noArgs: () => void;
helloFromServer: (message: string, x: number) => void;
ackFromServer: (
a: boolean,
b: string,
ack: (err: Error, c: boolean[]) => void
) => void;
ackFromServerSingleArg: (
a: boolean,
b: string,
ack: (err: Error, c: string[]) => void
) => void;
onlyCallback: (a: (err: Error) => void) => void;
}
interface ServerToClientEventsWithMultipleWithAck {
ackFromServer: (a: boolean, b: string) => Promise<boolean[]>;
ackFromServerSingleArg: (a: boolean, b: string) => Promise<string[]>;
// This should technically be `undefined[]`, but this doesn't work currently *only* with emitWithAck
// you can use an empty callback with emit, but not emitWithAck
onlyCallback: () => Promise<undefined>;
}
interface ServerToClientEventsWithAck {
ackFromServer: (a: boolean, b: string) => Promise<boolean>;
ackFromServerSingleArg: (a: boolean, b: string) => Promise<string>;
// This doesn't work currently *only* with emitWithAck
// you can use an empty callback with emit, but not emitWithAck
onlyCallback: () => Promise<undefined>;
}
describe("Emitting Types", () => {
describe("send", () => {
it("prevents arguments if EmitEvents doesn't have message", () => {
const sio = new Server<ClientToServerEvents, ServerToClientEvents>();
const nio = sio.of("/test");
// @ts-expect-error - ServerToClientEvents doesn't have a message event
sio.send(1, "2", [3]);
// @ts-expect-error - ServerToClientEvents doesn't have a message event
nio.send(1, "2", [3]);
// This correctly becomes an error in TS 5.3.2, so when updating typescript, this should expect-error
sio.send();
nio.send();
});
it("has the correct types", () => {
const sio = new Server<
{},
{ message: (a: number, b: string, c: number[]) => void }
>();
const nio = sio.of("/test");
sio.send(1, "2", [3]);
nio.send(1, "2", [3]);
// @ts-expect-error - message requires arguments
sio.send();
// @ts-expect-error - message requires arguments
nio.send();
// @ts-expect-error - message requires the correct arguments
sio.send(1, 2, [3]);
// @ts-expect-error - message requires the correct arguments
nio.send(1, 2, [3]);
});
});
describe("Broadcast Operator", () => {
it("works untyped", () => {
const untyped = new Server();
untyped.emit("random", 1, 2, Function, Boolean);
untyped.of("/").emit("random2", 2, "string", Server);
expectType<Promise<any>>(untyped.to("1").emitWithAck("random", "test"));
expectType<(ev: string, ...args: any[]) => Promise<any>>(
untyped.to("1").emitWithAck<string>
);
});
it("has the correct types", () => {
// Ensuring that all paths to BroadcastOperator have the correct types
// means that we only need one set of tests for emitting once the
// socket/namespace/server becomes a broadcast emitter
const sio = new Server<ClientToServerEvents, ServerToClientEvents>();
const nio = sio.of("/");
for (const emitter of [sio, nio]) {
expectType<BroadcastOperator<ServerToClientEventsWithMultiple, any>>(
emitter.to("1")
);
expectType<BroadcastOperator<ServerToClientEventsWithMultiple, any>>(
emitter.in("1")
);
expectType<BroadcastOperator<ServerToClientEventsWithMultiple, any>>(
emitter.except("1")
);
expectType<BroadcastOperator<ServerToClientEventsWithMultiple, any>>(
emitter.except("1")
);
expectType<BroadcastOperator<ServerToClientEventsWithMultiple, any>>(
emitter.compress(true)
);
expectType<BroadcastOperator<ServerToClientEventsWithMultiple, any>>(
emitter.volatile
);
expectType<BroadcastOperator<ServerToClientEventsWithMultiple, any>>(
emitter.local
);
expectType<
BroadcastOperator<ServerToClientEventsWithMultipleAndError, any>
>(emitter.timeout(0));
expectType<
BroadcastOperator<ServerToClientEventsWithMultipleAndError, any>
>(emitter.timeout(0).timeout(0));
}
sio.on("connection", (s) => {
expectType<
Socket<
ClientToServerEvents,
ServerToClientEventsWithError,
DefaultEventsMap,
any
>
>(s.timeout(0));
expectType<
BroadcastOperator<ServerToClientEventsWithMultipleAndError, any>
>(s.timeout(0).broadcast);
// ensure that turning socket to a broadcast works correctly
expectType<BroadcastOperator<ServerToClientEventsWithMultiple, any>>(
s.broadcast
);
expectType<BroadcastOperator<ServerToClientEventsWithMultiple, any>>(
s.in("1")
);
expectType<BroadcastOperator<ServerToClientEventsWithMultiple, any>>(
s.except("1")
);
expectType<BroadcastOperator<ServerToClientEventsWithMultiple, any>>(
s.to("1")
);
// Ensure that adding a timeout to a broadcast works after the fact
expectType<
BroadcastOperator<ServerToClientEventsWithMultipleAndError, any>
>(s.broadcast.timeout(0));
// Ensure that adding a timeout to a broadcast works after the fact
expectType<
BroadcastOperator<ServerToClientEventsWithMultipleAndError, any>
>(s.broadcast.timeout(0).timeout(0));
});
});
it("has the correct types for `emit`", () => {
const sio = new Server<ClientToServerEvents, ServerToClientEvents>();
expectType<ToEmit<ServerToClientEventsWithMultipleAndError, "noArgs">>(
sio.timeout(0).emit<"noArgs">
);
expectType<
ToEmit<ServerToClientEventsWithMultipleAndError, "helloFromServer">
>(sio.timeout(0).emit<"helloFromServer">);
expectType<
ToEmit<
ServerToClientEventsWithMultipleAndError,
"ackFromServerSingleArg"
>
>(sio.timeout(0).emit<"ackFromServerSingleArg">);
expectType<
ToEmit<ServerToClientEventsWithMultipleAndError, "ackFromServer">
>(sio.timeout(0).emit<"ackFromServer">);
expectType<
ToEmit<ServerToClientEventsWithMultipleAndError, "onlyCallback">
>(sio.timeout(0).emit<"onlyCallback">);
});
it("has the correct types for `emitWithAck`", () => {
const sio = new Server<ClientToServerEvents, ServerToClientEvents>();
const sansTimeout = sio.in("1");
// Without timeout, `emitWithAck` shouldn't accept any events
expectType<never>(
undefined as Parameters<typeof sansTimeout["emitWithAck"]>[0]
);
// @ts-expect-error - "noArgs" doesn't have a callback and is thus excluded
sio.timeout(0).emitWithAck("noArgs");
// @ts-expect-error - "helloFromServer" doesn't have a callback and is thus excluded
sio.timeout(0).emitWithAck("helloFromServer");
// @ts-expect-error - "onlyCallback" doesn't have a callback and is thus excluded
sio.timeout(0).emitWithAck("onlyCallback");
expectType<
ToEmitWithAck<
ServerToClientEventsWithMultipleWithAck,
"ackFromServerSingleArg"
>
>(sio.timeout(0).emitWithAck<"ackFromServerSingleArg">);
expectType<
ToEmitWithAck<
ServerToClientEventsWithMultipleWithAck,
"ackFromServer"
>
>(sio.timeout(0).emitWithAck<"ackFromServer">);
});
});
describe("emit", () => {
it("Infers correct types", () => {
const sio = new Server<ClientToServerEvents, ServerToClientEvents>();
const nio = sio.of("/test");
expectType<ToEmit<ServerToClientEventsNoAck, "noArgs">>(
sio.emit<"noArgs">
);
expectType<ToEmit<ServerToClientEventsNoAck, "noArgs">>(
nio.emit<"noArgs">
);
expectType<ToEmit<ServerToClientEventsNoAck, "helloFromServer">>(
// These errors will dissapear once the TS version is updated from 4.7.4
// the TSD instance is using a newer version of TS than the workspace version
// to enable the ability to compare against `any`
sio.emit<"helloFromServer">
);
expectType<ToEmit<ServerToClientEventsNoAck, "helloFromServer">>(
nio.emit<"helloFromServer">
);
sio.on("connection", (s) => {
expectType<ToEmit<ServerToClientEvents, "noArgs">>(s.emit<"noArgs">);
expectType<ToEmit<ServerToClientEvents, "helloFromServer">>(
s.emit<"helloFromServer">
);
expectType<ToEmit<ServerToClientEvents, "ackFromServerSingleArg">>(
s.emit<"ackFromServerSingleArg">
);
expectType<ToEmit<ServerToClientEvents, "ackFromServer">>(
s.emit<"ackFromServer">
);
expectType<ToEmit<ServerToClientEvents, "onlyCallback">>(
s.emit<"onlyCallback">
);
});
});
it("does not allow events with acks", () => {
const sio = new Server<ClientToServerEvents, ServerToClientEvents>();
const nio = sio.of("/test");
// @ts-expect-error - "ackFromServerSingleArg" has a callback and is thus excluded
sio.emit<"ackFromServerSingleArg">;
// @ts-expect-error - "ackFromServer" has a callback and is thus excluded
sio.emit<"ackFromServer">;
// @ts-expect-error - "onlyCallback" has a callback and is thus excluded
sio.emit<"onlyCallback">;
// @ts-expect-error - "ackFromServerSingleArg" has a callback and is thus excluded
nio.emit<"ackFromServerSingleArg">;
// @ts-expect-error - "ackFromServer" has a callback and is thus excluded
nio.emit<"ackFromServer">;
// @ts-expect-error - "onlyCallback" has a callback and is thus excluded
nio.emit<"onlyCallback">;
});
});
describe("emitWithAck", () => {
it("Infers correct types", () => {
const sio = new Server<ClientToServerEvents, ServerToClientEvents>();
sio.on("connection", (s) => {
// @ts-expect-error - "noArgs" doesn't have a callback and is thus excluded
s.emitWithAck("noArgs");
// @ts-expect-error - "helloFromServer" doesn't have a callback and is thus excluded
s.emitWithAck("helloFromServer");
// @ts-expect-error - "onlyCallback" doesn't have a callback and is thus excluded
s.emitWithAck("onlyCallback");
// @ts-expect-error - "onlyCallback" doesn't have a callback and is thus excluded
s.timeout(0).emitWithAck("onlyCallback");
expectType<
ToEmitWithAck<ServerToClientEventsWithAck, "ackFromServerSingleArg">
>(s.emitWithAck<"ackFromServerSingleArg">);
expectType<
ToEmitWithAck<ServerToClientEventsWithAck, "ackFromServer">
>(s.emitWithAck<"ackFromServer">);
expectType<
ToEmitWithAck<ServerToClientEventsWithAck, "ackFromServerSingleArg">
>(s.timeout(0).emitWithAck<"ackFromServerSingleArg">);
expectType<
ToEmitWithAck<ServerToClientEventsWithAck, "ackFromServer">
>(s.timeout(0).emitWithAck<"ackFromServer">);
});
});
});
});
describe("listen and emit event maps", () => {
interface ClientToServerEvents {
helloFromClient: (message: string) => void;
ackFromClient: (
a: string,
b: number,
ack: (c: string, d: number) => void
) => void;
}
interface ServerToClientEvents {
helloFromServer: (message: string, x: number) => void;
ackFromServer: (
a: boolean,
b: string,
ack: (c: boolean, d: string) => void
) => void;
ackFromServerSingleArg: (
a: boolean,
b: string,
ack: (c: string) => void
) => void;
multipleAckFromServer: (
a: boolean,
b: string,
ack: (c: string) => void
) => void;
}
describe("on", () => {
it("infers correct types for listener parameters", (done) => {
const srv = createServer();
@@ -225,6 +525,10 @@ describe("server", () => {
srv.listen(() => {
sio.on("connection", (s) => {
expectType<Socket<ClientToServerEvents, ServerToClientEvents>>(s);
s.on("noArgs", (...args) => {
expectType<[]>(args);
done();
});
s.on("helloFromClient", (message) => {
expectType<string>(message);
done();
@@ -245,117 +549,14 @@ describe("server", () => {
const sio = new Server<ClientToServerEvents, ServerToClientEvents>(srv);
srv.listen(() => {
sio.on("connection", (s) => {
expectError(
s.on("helloFromServer", (message, number) => {
done();
})
);
});
});
});
});
describe("emit", () => {
it("accepts arguments of the correct types", (done) => {
const srv = createServer();
const sio = new Server<ClientToServerEvents, ServerToClientEvents>(srv);
srv.listen(() => {
sio.emit("helloFromServer", "hi", 1);
sio.to("room").emit("helloFromServer", "hi", 1);
sio.timeout(1000).emit("helloFromServer", "hi", 1);
sio
.timeout(1000)
.emit("multipleAckFromServer", true, "123", (err, c) => {
expectType<Error>(err);
expectType<string[]>(c);
// @ts-expect-error - shouldn't accept emit events
s.on("noArgs", (message, number) => {
done();
});
sio.on("connection", (s) => {
s.emit("helloFromServer", "hi", 10);
s.emit("ackFromServer", true, "123", (c, d) => {
expectType<boolean>(c);
expectType<string>(d);
// @ts-expect-error - shouldn't accept emit events
s.on("helloFromServer", (message, number) => {
done();
});
s.timeout(1000).emit("ackFromServer", true, "123", (err, c, d) => {
expectType<Error>(err);
expectType<boolean>(c);
expectType<string>(d);
});
s.timeout(1000)
.to("room")
.emit("multipleAckFromServer", true, "123", (err, c) => {
expectType<Error>(err);
expectType<string[]>(c);
});
s.to("room")
.timeout(1000)
.emit("multipleAckFromServer", true, "123", (err, c) => {
expectType<Error>(err);
expectType<string[]>(c);
});
done();
});
});
});
it("does not accept arguments of wrong types", (done) => {
const srv = createServer();
const sio = new Server<ClientToServerEvents, ServerToClientEvents>(srv);
srv.listen(() => {
expectError(sio.emit("helloFromClient"));
expectError(sio.to("room").emit("helloFromClient"));
expectError(sio.timeout(1000).to("room").emit("helloFromClient"));
sio.on("connection", (s) => {
expectError(s.emit("helloFromClient", "hi"));
expectError(s.emit("helloFromServer", "hi", 10, "10"));
expectError(s.emit("helloFromServer", "hi", "10"));
expectError(s.emit("helloFromServer", 0, 0));
expectError(s.emit("wrong name", 10));
expectError(s.emit("wrong name"));
done();
});
});
});
});
describe("emitWithAck", () => {
it("accepts arguments of the correct types", (done) => {
const srv = createServer();
const sio = new Server<ClientToServerEvents, ServerToClientEvents>(srv);
srv.listen(async () => {
const value = await sio
.timeout(1000)
.emitWithAck("multipleAckFromServer", true, "123");
expectType<string[]>(value);
sio.on("connection", async (s) => {
const value1 = await s
.timeout(1000)
.to("room")
.emitWithAck("multipleAckFromServer", true, "123");
expectType<string[]>(value1);
const value2 = await s
.to("room")
.timeout(1000)
.emitWithAck("multipleAckFromServer", true, "123");
expectType<string[]>(value2);
const value3 = await s.emitWithAck(
"ackFromServerSingleArg",
true,
"123"
);
expectType<string>(value3);
done();
});
});
});
@@ -364,14 +565,17 @@ describe("server", () => {
describe("listen and emit event maps for the serverSideEmit method", () => {
interface ClientToServerEvents {
noArgs: () => void;
helloFromClient: (message: string) => void;
}
interface ServerToClientEvents {
noArgs: () => void;
helloFromServer: (message: string, x: number) => void;
}
interface InterServerEvents {
noArgs: () => void;
helloFromServerToServer: (message: string, x: number) => void;
ackFromServerToServer: (foo: string, cb: (bar: number) => void) => void;
}
@@ -389,20 +593,36 @@ describe("server", () => {
Server<ClientToServerEvents, ServerToClientEvents, InterServerEvents>
>(sio);
srv.listen(async () => {
sio.serverSideEmit("noArgs");
sio.serverSideEmit("helloFromServerToServer", "hello", 10);
sio
.of("/test")
.serverSideEmit("helloFromServerToServer", "hello", 10);
sio.on("noArgs", (...args) => {
expectType<[]>(args);
});
sio.on("helloFromServerToServer", (message, x) => {
expectType<string>(message);
expectType<number>(x);
});
sio.of("/test").on("noArgs", (...args) => {
expectType<[]>(args);
});
sio.of("/test").on("helloFromServerToServer", (message, x) => {
expectType<string>(message);
expectType<number>(x);
});
//@ts-expect-error - "helloFromServerToServer" does not have a callback
sio.serverSideEmitWithAck("noArgs");
//@ts-expect-error - "helloFromServerToServer" does not have a callback
sio.serverSideEmitWithAck("helloFromServerToServer", "hello");
sio.on("ackFromServerToServer", (...args) => {
expectType<[string, (bar: number) => void]>(args);
});
sio.serverSideEmit("ackFromServerToServer", "foo", (err, bar) => {
expectType<Error>(err);
expectType<number[]>(bar);
@@ -440,7 +660,8 @@ describe("server", () => {
it("does not accept arguments of wrong types", () => {
const io = new Server();
expectError(io.adapter((nsp) => "nope"));
// @ts-expect-error - shouldn't accept arguments of the wrong types
io.adapter((nsp) => "nope");
});
});
});

View File

@@ -606,9 +606,11 @@ describe("socket", () => {
});
it("should emit an event and wait for the acknowledgement", (done) => {
const io = new Server(0);
const socket = createClient(io);
type Events = {
hi: (a: number, b: number, cb: (c: number) => void) => void;
};
const io = new Server<{}, Events>(0);
const socket = createClient<Events, Events>(io);
io.on("connection", async (s) => {
socket.on("hi", (a, b, fn) => {
expect(a).to.be(1);

221
test/support/expectjs.d.ts vendored Normal file
View File

@@ -0,0 +1,221 @@
declare function expect(target?: any): Expect.Root;
declare namespace Expect {
interface Assertion {
/**
* Check if the value is truthy
*/
ok(): void;
/**
* Creates an anonymous function which calls fn with arguments.
*/
withArgs(...args: any[]): Root;
/**
* Assert that the function throws.
*
* @param fn callback to match error string against
*/
throwError(fn?: (exception: any) => void): void;
/**
* Assert that the function throws.
*
* @param fn callback to match error string against
*/
throwException(fn?: (exception: any) => void): void;
/**
* Assert that the function throws.
*
* @param regexp regexp to match error string against
*/
throwError(regexp: RegExp): void;
/**
* Assert that the function throws.
*
* @param fn callback to match error string against
*/
throwException(regexp: RegExp): void;
/**
* Checks if the array is empty.
*/
empty(): Assertion;
/**
* Checks if the obj exactly equals another.
*/
equal(obj: any): Assertion;
/**
* Checks if the obj sortof equals another.
*/
eql(obj: any): Assertion;
/**
* Assert within start to finish (inclusive).
*
* @param start
* @param finish
*/
within(start: number, finish: number): Assertion;
/**
* Assert typeof.
*/
a(type: string): Assertion;
/**
* Assert instanceof.
*/
a(type: Function): Assertion;
/**
* Assert typeof / instanceof.
*/
an: An;
/**
* Assert numeric value above n.
*/
greaterThan(n: number): Assertion;
/**
* Assert numeric value above n.
*/
above(n: number): Assertion;
/**
* Assert numeric value below n.
*/
lessThan(n: number): Assertion;
/**
* Assert numeric value below n.
*/
below(n: number): Assertion;
/**
* Assert string value matches regexp.
*
* @param regexp
*/
match(regexp: RegExp): Assertion;
/**
* Assert property "length" exists and has value of n.
*
* @param n
*/
length(n: number): Assertion;
/**
* Assert property name exists, with optional val.
*
* @param name
* @param val
*/
property(name: string, val?: any): Assertion;
/**
* Assert that string contains str.
*/
contain(...strings: string[]): Assertion;
string(str: string): Assertion;
/**
* Assert that the array contains obj.
*/
contain(...objs: any[]): Assertion;
string(obj: any): Assertion;
/**
* Assert exact keys or inclusion of keys by using the `.own` modifier.
*/
key(keys: string[]): Assertion;
/**
* Assert exact keys or inclusion of keys by using the `.own` modifier.
*/
key(...keys: string[]): Assertion;
/**
* Assert exact keys or inclusion of keys by using the `.own` modifier.
*/
keys(keys: string[]): Assertion;
/**
* Assert exact keys or inclusion of keys by using the `.own` modifier.
*/
keys(...keys: string[]): Assertion;
/**
* Assert a failure.
*/
fail(message?: string): Assertion;
}
interface Root extends Assertion {
not: Not;
to: To;
only: Only;
have: Have;
be: Be;
}
interface Be extends Assertion {
/**
* Checks if the obj exactly equals another.
*/
(obj: any): Assertion;
an: An;
}
interface An extends Assertion {
/**
* Assert typeof.
*/
(type: string): Assertion;
/**
* Assert instanceof.
*/
(type: Function): Assertion;
}
interface Not extends Expect.NotBase {
to: Expect.ToBase;
}
interface NotBase extends Assertion {
be: Be;
have: Have;
include: Assertion;
only: Only;
}
interface To extends Expect.ToBase {
not: Expect.NotBase;
}
interface ToBase extends Assertion {
be: Be;
have: Have;
include: Assertion;
only: Only;
}
interface Only extends Assertion {
have: Have;
}
interface Have extends Assertion {
own: Assertion;
}
}
declare module "expect.js" {
//@ts-ignore
export = expect;
}

View File

@@ -1,3 +1,4 @@
/// <reference types="./expectjs" />
import type { Server } from "../..";
import {
io as ioc,
@@ -6,6 +7,7 @@ import {
SocketOptions,
} from "socket.io-client";
import request from "supertest";
import type { DefaultEventsMap, EventsMap } from "../../lib/typed-events";
const expect = require("expect.js");
const i = expect.stringify;
@@ -30,11 +32,14 @@ expect.Assertion.prototype.contain = function (...args) {
return contain.apply(this, args);
};
export function createClient(
export function createClient<
CTS extends EventsMap = DefaultEventsMap,
STC extends EventsMap = DefaultEventsMap
>(
io: Server,
nsp: string = "/",
opts?: Partial<ManagerOptions & SocketOptions>
): ClientSocket {
): ClientSocket<STC, CTS> {
// @ts-ignore
const port = io.httpServer.address().port;
return ioc(`http://localhost:${port}${nsp}`, opts);
@@ -58,6 +63,16 @@ export function successFn(
return () => success(done, sio, ...clientSockets);
}
/**
* Asserts a condition so that typescript will recognize the assertion!
*
* Uses expect's `ok` check on condition
* @param condition
*/
export function assert(condition: any): asserts condition {
expect(condition).to.be.ok();
}
export function getPort(io: Server): number {
// @ts-ignore
return io.httpServer.address().port;

View File

@@ -7,6 +7,7 @@ import { Server } from "..";
import { io as ioc, Socket as ClientSocket } from "socket.io-client";
import request from "supertest";
import expect from "expect.js";
import { assert } from "./support/util";
const createPartialDone = (done: (err?: Error) => void, count: number) => {
let i = 0;
@@ -134,6 +135,8 @@ describe("socket.io with uWebSocket.js-based engine", () => {
clientWSOnly.on("hello", partialDone);
clientPollingOnly.on("hello", partialDone);
clientCustomNamespace.on("hello", shouldNotHappen(done));
assert(clientWSOnly.id);
assert(clientPollingOnly.id);
io.of("/").sockets.get(clientWSOnly.id)!.join("room1");
io.of("/").sockets.get(clientPollingOnly.id)!.join("room1");
@@ -148,6 +151,8 @@ describe("socket.io with uWebSocket.js-based engine", () => {
clientWSOnly.on("hello", partialDone);
clientPollingOnly.on("hello", partialDone);
clientCustomNamespace.on("hello", shouldNotHappen(done));
assert(clientWSOnly.id);
assert(clientPollingOnly.id);
io.of("/").sockets.get(clientWSOnly.id)!.join("room1");
io.of("/").sockets.get(clientPollingOnly.id)!.join("room2");
@@ -163,6 +168,8 @@ describe("socket.io with uWebSocket.js-based engine", () => {
clientPollingOnly.on("hello", shouldNotHappen(done));
clientCustomNamespace.on("hello", shouldNotHappen(done));
assert(clientWSOnly.id);
assert(clientPollingOnly.id);
io.of("/").sockets.get(clientWSOnly.id)!.join("room1");
io.of("/").sockets.get(clientPollingOnly.id)!.join("room2");
@@ -177,6 +184,9 @@ describe("socket.io with uWebSocket.js-based engine", () => {
clientPollingOnly.on("hello", partialDone);
clientCustomNamespace.on("hello", shouldNotHappen(done));
assert(client.id);
assert(clientWSOnly.id);
assert(clientPollingOnly.id);
io.of("/").sockets.get(client.id)!.join("room1");
io.of("/").sockets.get(clientPollingOnly.id)!.join("room1");
@@ -189,6 +199,7 @@ describe("socket.io with uWebSocket.js-based engine", () => {
it("should not crash when socket is disconnected before the upgrade", (done) => {
client.on("disconnect", () => done());
assert(client.id);
io.of("/").sockets.get(client.id)!.disconnect();
});