mirror of
https://github.com/socketio/socket.io.git
synced 2026-01-11 16:08:24 -05:00
Compare commits
48 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f11c4745f | ||
|
|
b839a3b400 | ||
|
|
f0ed42f18c | ||
|
|
b7213e71e4 | ||
|
|
2da82103d2 | ||
|
|
02b0f73e2c | ||
|
|
c0d8c5ab23 | ||
|
|
fe8730ca0f | ||
|
|
ed8483da4d | ||
|
|
9d86397243 | ||
|
|
44e20ba5bf | ||
|
|
ccc5ec39a8 | ||
|
|
0ef2a4d02c | ||
|
|
95810aa62d | ||
|
|
60edecb3bd | ||
|
|
eb5fdbd03e | ||
|
|
4974e9077c | ||
|
|
033c5d399a | ||
|
|
7a74b66872 | ||
|
|
dc81fcf461 | ||
|
|
c100b7b61c | ||
|
|
f03eeca39a | ||
|
|
d8cc8aef7e | ||
|
|
ccfd8caba6 | ||
|
|
24fee27ba3 | ||
|
|
310f8557a7 | ||
|
|
dbd2a07cda | ||
|
|
94e27cd072 | ||
|
|
a4dffc6527 | ||
|
|
7c44893d78 | ||
|
|
b833f918c8 | ||
|
|
24d8d1f67f | ||
|
|
6f2a50b932 | ||
|
|
1633150b2b | ||
|
|
0cb6ac95b4 | ||
|
|
a2cf2486c3 | ||
|
|
995f38f4cc | ||
|
|
891b1870e9 | ||
|
|
b84ed1e41c | ||
|
|
fb6b0efec9 | ||
|
|
95d9e4a42f | ||
|
|
499c89250d | ||
|
|
93cce05fb3 | ||
|
|
dc381b72c6 | ||
|
|
9fff03487c | ||
|
|
b81ce4c9d0 | ||
|
|
d65b6ee84c | ||
|
|
3665aada47 |
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -12,12 +12,12 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [10.x, 12.x, 14.x, 15.x]
|
||||
node-version: [12, 14, 16]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: npm ci
|
||||
|
||||
111
CHANGELOG.md
111
CHANGELOG.md
@@ -1,3 +1,114 @@
|
||||
# [4.4.0](https://github.com/socketio/socket.io/compare/4.3.2...4.4.0) (2021-11-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* only set 'connected' to true after middleware execution ([02b0f73](https://github.com/socketio/socket.io/commit/02b0f73e2c64b09c72c5fbf7dc5f059557bdbe50))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add an implementation based on uWebSockets.js ([c0d8c5a](https://github.com/socketio/socket.io/commit/c0d8c5ab234d0d2bef0d0dec472973cc9662f647))
|
||||
* add timeout feature ([f0ed42f](https://github.com/socketio/socket.io/commit/f0ed42f18cabef20ad976aeec37077b6bf3837a5))
|
||||
* add type information to `socket.data` ([#4159](https://github.com/socketio/socket.io/issues/4159)) ([fe8730c](https://github.com/socketio/socket.io/commit/fe8730ca0f15bc92d5de81cf934c89c76d6af329))
|
||||
|
||||
|
||||
|
||||
## [4.3.2](https://github.com/socketio/socket.io/compare/4.3.1...4.3.2) (2021-11-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix race condition in dynamic namespaces ([#4137](https://github.com/socketio/socket.io/issues/4137)) ([9d86397](https://github.com/socketio/socket.io/commit/9d86397243bcbb5775a29d96e5ef03e17148a8e7))
|
||||
|
||||
|
||||
## [4.3.1](https://github.com/socketio/socket.io/compare/4.3.0...4.3.1) (2021-10-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix server attachment ([#4127](https://github.com/socketio/socket.io/issues/4127)) ([0ef2a4d](https://github.com/socketio/socket.io/commit/0ef2a4d02c9350aff163df9cb61aece89c4dac0f))
|
||||
|
||||
|
||||
# [4.3.0](https://github.com/socketio/socket.io/compare/4.2.0...4.3.0) (2021-10-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **typings:** add name field to cookie option ([#4099](https://github.com/socketio/socket.io/issues/4099)) ([033c5d3](https://github.com/socketio/socket.io/commit/033c5d399a2b985afad32c1e4b0c16d764e248cd))
|
||||
* send volatile packets with binary attachments ([dc81fcf](https://github.com/socketio/socket.io/commit/dc81fcf461cfdbb5b34b1a5a96b84373754047d5))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* serve ESM bundle ([60edecb](https://github.com/socketio/socket.io/commit/60edecb3bd33801803cdcba0aefbafa381a2abb3))
|
||||
|
||||
|
||||
# [4.2.0](https://github.com/socketio/socket.io/compare/4.1.3...4.2.0) (2021-08-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **typings:** allow async listener in typed events ([ccfd8ca](https://github.com/socketio/socket.io/commit/ccfd8caba6d38b7ba6c5114bd8179346ed07671c))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* ignore the query string when serving client JavaScript ([#4024](https://github.com/socketio/socket.io/issues/4024)) ([24fee27](https://github.com/socketio/socket.io/commit/24fee27ba36485308f8e995879c10931532c814e))
|
||||
|
||||
|
||||
## [4.1.3](https://github.com/socketio/socket.io/compare/4.1.2...4.1.3) (2021-07-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix io.except() method ([94e27cd](https://github.com/socketio/socket.io/commit/94e27cd072c8a4eeb9636f6ffbb7a21d382f36b0))
|
||||
* remove x-sourcemap header ([a4dffc6](https://github.com/socketio/socket.io/commit/a4dffc6527f412d51a786ae5bf2e9080fe1ca63c))
|
||||
|
||||
|
||||
## [4.1.2](https://github.com/socketio/socket.io/compare/4.1.1...4.1.2) (2021-05-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **typings:** ensure compatibility with TypeScript 3.x ([0cb6ac9](https://github.com/socketio/socket.io/commit/0cb6ac95b49a27483b6f1b6402fa54b35f82e36f))
|
||||
* ensure compatibility with previous versions of the adapter ([a2cf248](https://github.com/socketio/socket.io/commit/a2cf2486c366cb62293101c10520c57f6984a3fc))
|
||||
|
||||
|
||||
## [4.1.1](https://github.com/socketio/socket.io/compare/4.1.0...4.1.1) (2021-05-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **typings:** properly type server-side events ([b84ed1e](https://github.com/socketio/socket.io/commit/b84ed1e41c9053792caf58974c5de9395bfd509f))
|
||||
* **typings:** properly type the adapter attribute ([891b187](https://github.com/socketio/socket.io/commit/891b1870e92d1ec38910f03bb839817e2d6be65a))
|
||||
|
||||
|
||||
# [4.1.0](https://github.com/socketio/socket.io/compare/4.0.2...4.1.0) (2021-05-11)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add support for inter-server communication ([93cce05](https://github.com/socketio/socket.io/commit/93cce05fb3faf91f21fa71212275c776aa161107))
|
||||
* notify upon namespace creation ([499c892](https://github.com/socketio/socket.io/commit/499c89250d2db1ab7725ab2b74840e188c267c46))
|
||||
* add a "connection_error" event ([7096e98](https://github.com/socketio/engine.io/commit/7096e98a02295a62c8ea2aa56461d4875887092d), from `engine.io`)
|
||||
* add the "initial_headers" and "headers" events ([2527543](https://github.com/socketio/engine.io/commit/252754353a0e88eb036ebb3082e9d6a9a5f497db), from `engine.io`)
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* add support for the "wsPreEncoded" writing option ([dc381b7](https://github.com/socketio/socket.io/commit/dc381b72c6b2f8172001dedd84116122e4cc95b3))
|
||||
|
||||
|
||||
## [4.0.2](https://github.com/socketio/socket.io/compare/4.0.1...4.0.2) (2021-05-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **typings:** make "engine" attribute public ([b81ce4c](https://github.com/socketio/socket.io/commit/b81ce4c9d0b00666361498e2ba5e0d007d5860b8))
|
||||
* properly export the Socket class ([d65b6ee](https://github.com/socketio/socket.io/commit/d65b6ee84c8e91deb61c3c1385eb19afa196a909))
|
||||
|
||||
|
||||
## [4.0.1](https://github.com/socketio/socket.io/compare/4.0.0...4.0.1) (2021-03-31)
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# socket.io
|
||||
[](https://repl.it/github/socketio/socket.io)
|
||||
[](https://replit.com/@socketio/socketio-minimal-example)
|
||||
[](#backers) [](#sponsors)
|
||||
[](https://github.com/socketio/socket.io/actions)
|
||||
[](https://david-dm.org/socketio/socket.io)
|
||||
@@ -22,7 +22,7 @@ Some implementations in other languages are also available:
|
||||
- [Swift](https://github.com/socketio/socket.io-client-swift)
|
||||
- [Dart](https://github.com/rikulo/socket.io-client-dart)
|
||||
- [Python](https://github.com/miguelgrinberg/python-socketio)
|
||||
- [.Net](https://github.com/Quobject/SocketIoClientDotNet)
|
||||
- [.NET](https://github.com/doghappy/socket.io-client-csharp)
|
||||
|
||||
Its main features are:
|
||||
|
||||
|
||||
7
client-dist/socket.io.esm.min.js
vendored
Normal file
7
client-dist/socket.io.esm.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
client-dist/socket.io.esm.min.js.map
Normal file
1
client-dist/socket.io.esm.min.js.map
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
6
client-dist/socket.io.min.js
vendored
6
client-dist/socket.io.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
6
client-dist/socket.io.msgpack.min.js
vendored
6
client-dist/socket.io.msgpack.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
19
examples/basic-crud-application/README.md
Normal file
19
examples/basic-crud-application/README.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Basic CRUD application with Socket.IO
|
||||
|
||||
Please read the related [guide](https://socket.io/get-started/basic-crud-application/).
|
||||
|
||||
## Running the frontend
|
||||
|
||||
```
|
||||
cd angular-client
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
|
||||
### Running the server
|
||||
|
||||
```
|
||||
cd server
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
@@ -0,0 +1,17 @@
|
||||
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
|
||||
# For additional information regarding the format and rule options, please see:
|
||||
# https://github.com/browserslist/browserslist#queries
|
||||
|
||||
# For the full list of supported browsers by the Angular framework, please see:
|
||||
# https://angular.io/guide/browser-support
|
||||
|
||||
# You can see what browsers were selected by your queries by running:
|
||||
# npx browserslist
|
||||
|
||||
last 1 Chrome version
|
||||
last 1 Firefox version
|
||||
last 2 Edge major versions
|
||||
last 2 Safari major versions
|
||||
last 2 iOS major versions
|
||||
Firefox ESR
|
||||
not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line.
|
||||
16
examples/basic-crud-application/angular-client/.editorconfig
Normal file
16
examples/basic-crud-application/angular-client/.editorconfig
Normal file
@@ -0,0 +1,16 @@
|
||||
# Editor configuration, see https://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.ts]
|
||||
quote_type = single
|
||||
|
||||
[*.md]
|
||||
max_line_length = off
|
||||
trim_trailing_whitespace = false
|
||||
46
examples/basic-crud-application/angular-client/.gitignore
vendored
Normal file
46
examples/basic-crud-application/angular-client/.gitignore
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# compiled output
|
||||
/dist
|
||||
/tmp
|
||||
/out-tsc
|
||||
# Only exists if Bazel was run
|
||||
/bazel-out
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# profiling files
|
||||
chrome-profiler-events*.json
|
||||
speed-measure-plugin*.json
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
.history/*
|
||||
|
||||
# misc
|
||||
/.sass-cache
|
||||
/connect.lock
|
||||
/coverage
|
||||
/libpeerconnection.log
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
testem.log
|
||||
/typings
|
||||
|
||||
# System Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
31
examples/basic-crud-application/angular-client/README.md
Normal file
31
examples/basic-crud-application/angular-client/README.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Angular TodoMVC + Socket.IO
|
||||
|
||||
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).
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
|
||||
## Code scaffolding
|
||||
|
||||
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
|
||||
|
||||
## 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.
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
|
||||
|
||||
## Running end-to-end tests
|
||||
|
||||
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
|
||||
|
||||
## Further help
|
||||
|
||||
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.
|
||||
128
examples/basic-crud-application/angular-client/angular.json
Normal file
128
examples/basic-crud-application/angular-client/angular.json
Normal file
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"angular-todomvc": {
|
||||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@schematics/angular:application": {
|
||||
"strict": true
|
||||
}
|
||||
},
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"options": {
|
||||
"outputPath": "dist/angular-todomvc",
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"aot": true,
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets"
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.css"
|
||||
],
|
||||
"scripts": []
|
||||
},
|
||||
"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",
|
||||
"maximumWarning": "500kb",
|
||||
"maximumError": "1mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "2kb",
|
||||
"maximumError": "4kb"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"options": {
|
||||
"browserTarget": "angular-todomvc:build"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "angular-todomvc:build:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"browserTarget": "angular-todomvc:build"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"main": "src/test.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"karmaConfig": "karma.conf.js",
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets"
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.css"
|
||||
],
|
||||
"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"
|
||||
}
|
||||
BIN
examples/basic-crud-application/angular-client/assets/demo.gif
Normal file
BIN
examples/basic-crud-application/angular-client/assets/demo.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 205 KiB |
@@ -0,0 +1,37 @@
|
||||
// @ts-check
|
||||
// Protractor configuration file, see link for more information
|
||||
// https://github.com/angular/protractor/blob/master/lib/config.ts
|
||||
|
||||
const { SpecReporter, StacktraceOption } = require('jasmine-spec-reporter');
|
||||
|
||||
/**
|
||||
* @type { import("protractor").Config }
|
||||
*/
|
||||
exports.config = {
|
||||
allScriptsTimeout: 11000,
|
||||
specs: [
|
||||
'./src/**/*.e2e-spec.ts'
|
||||
],
|
||||
capabilities: {
|
||||
browserName: 'chrome'
|
||||
},
|
||||
directConnect: true,
|
||||
SELENIUM_PROMISE_MANAGER: false,
|
||||
baseUrl: 'http://localhost:4200/',
|
||||
framework: 'jasmine',
|
||||
jasmineNodeOpts: {
|
||||
showColors: true,
|
||||
defaultTimeoutInterval: 30000,
|
||||
print: function() {}
|
||||
},
|
||||
onPrepare() {
|
||||
require('ts-node').register({
|
||||
project: require('path').join(__dirname, './tsconfig.json')
|
||||
});
|
||||
jasmine.getEnv().addReporter(new SpecReporter({
|
||||
spec: {
|
||||
displayStacktrace: StacktraceOption.PRETTY
|
||||
}
|
||||
}));
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
import { AppPage } from './app.po';
|
||||
import { browser, logging } from 'protractor';
|
||||
|
||||
describe('workspace-project App', () => {
|
||||
let page: AppPage;
|
||||
|
||||
beforeEach(() => {
|
||||
page = new AppPage();
|
||||
});
|
||||
|
||||
it('should display welcome message', async () => {
|
||||
await page.navigateTo();
|
||||
expect(await page.getTitleText()).toEqual('angular-todomvc app is running!');
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// Assert that there are no errors emitted from the browser
|
||||
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
|
||||
expect(logs).not.toContain(jasmine.objectContaining({
|
||||
level: logging.Level.SEVERE,
|
||||
} as logging.Entry));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { browser, by, element } from 'protractor';
|
||||
|
||||
export class AppPage {
|
||||
async navigateTo(): Promise<unknown> {
|
||||
return browser.get(browser.baseUrl);
|
||||
}
|
||||
|
||||
async getTitleText(): Promise<string> {
|
||||
return element(by.css('app-root .content span')).getText();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../out-tsc/e2e",
|
||||
"module": "commonjs",
|
||||
"target": "es2018",
|
||||
"types": [
|
||||
"jasmine",
|
||||
"node"
|
||||
]
|
||||
}
|
||||
}
|
||||
44
examples/basic-crud-application/angular-client/karma.conf.js
Normal file
44
examples/basic-crud-application/angular-client/karma.conf.js
Normal file
@@ -0,0 +1,44 @@
|
||||
// Karma configuration file, see link for more information
|
||||
// https://karma-runner.github.io/1.0/config/configuration-file.html
|
||||
|
||||
module.exports = function (config) {
|
||||
config.set({
|
||||
basePath: '',
|
||||
frameworks: ['jasmine', '@angular-devkit/build-angular'],
|
||||
plugins: [
|
||||
require('karma-jasmine'),
|
||||
require('karma-chrome-launcher'),
|
||||
require('karma-jasmine-html-reporter'),
|
||||
require('karma-coverage'),
|
||||
require('@angular-devkit/build-angular/plugins/karma')
|
||||
],
|
||||
client: {
|
||||
jasmine: {
|
||||
// you can add configuration options for Jasmine here
|
||||
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
|
||||
// for example, you can disable the random execution with `random: false`
|
||||
// or set a specific seed with `seed: 4321`
|
||||
},
|
||||
clearContext: false // leave Jasmine Spec Runner output visible in browser
|
||||
},
|
||||
jasmineHtmlReporter: {
|
||||
suppressAll: true // removes the duplicated traces
|
||||
},
|
||||
coverageReporter: {
|
||||
dir: require('path').join(__dirname, './coverage/angular-todomvc'),
|
||||
subdir: '.',
|
||||
reporters: [
|
||||
{ type: 'html' },
|
||||
{ type: 'text-summary' }
|
||||
]
|
||||
},
|
||||
reporters: ['progress', 'kjhtml'],
|
||||
port: 9876,
|
||||
colors: true,
|
||||
logLevel: config.LOG_INFO,
|
||||
autoWatch: true,
|
||||
browsers: ['Chrome'],
|
||||
singleRun: false,
|
||||
restartOnFileChange: true
|
||||
});
|
||||
};
|
||||
46
examples/basic-crud-application/angular-client/package.json
Normal file
46
examples/basic-crud-application/angular-client/package.json
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "angular-todomvc",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"build": "ng build",
|
||||
"test": "ng test",
|
||||
"lint": "ng lint",
|
||||
"e2e": "ng e2e"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<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()">
|
||||
</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)">
|
||||
<ul class="todo-list">
|
||||
<li *ngFor="let todo of todoStore.todos" [class.completed]="todo.completed" [class.editing]="todo.editing">
|
||||
<div class="view">
|
||||
<input class="toggle" type="checkbox" (click)="toggleCompletion(todo)" [checked]="todo.completed">
|
||||
<label (dblclick)="editTodo(todo)">{{todo.title}}</label>
|
||||
<button class="destroy" (click)="remove(todo)"></button>
|
||||
</div>
|
||||
<input class="edit" *ngIf="todo.editing" [value]="todo.title" #editedtodo (blur)="stopEditing(todo, editedtodo.value)" (keyup.enter)="updateEditingTodo(todo, editedtodo.value)" (keyup.escape)="cancelEditingTodo(todo)">
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<footer class="footer" *ngIf="todoStore.todos.length > 0">
|
||||
<span class="todo-count"><strong>{{todoStore.getRemaining().length}}</strong> {{todoStore.getRemaining().length == 1 ? 'item' : 'items'}} left</span>
|
||||
<button class="clear-completed" *ngIf="todoStore.getCompleted().length > 0" (click)="removeCompleted()">Clear completed</button>
|
||||
</footer>
|
||||
</section>
|
||||
@@ -0,0 +1,31 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { AppComponent } from './app.component';
|
||||
|
||||
describe('AppComponent', () => {
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
AppComponent
|
||||
],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
it('should create the app', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.componentInstance;
|
||||
expect(app).toBeTruthy();
|
||||
});
|
||||
|
||||
it(`should have as title 'angular-todomvc'`, () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.componentInstance;
|
||||
expect(app.title).toEqual('angular-todomvc');
|
||||
});
|
||||
|
||||
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!');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { TodoStore, Todo } from './store';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.css']
|
||||
})
|
||||
export class AppComponent {
|
||||
todoStore: TodoStore;
|
||||
newTodoText = '';
|
||||
|
||||
constructor(todoStore: TodoStore) {
|
||||
this.todoStore = todoStore;
|
||||
}
|
||||
|
||||
stopEditing(todo: Todo, editedTitle: string) {
|
||||
todo.title = editedTitle;
|
||||
todo.editing = false;
|
||||
}
|
||||
|
||||
cancelEditingTodo(todo: Todo) {
|
||||
todo.editing = false;
|
||||
}
|
||||
|
||||
updateEditingTodo(todo: Todo, editedTitle: string) {
|
||||
editedTitle = editedTitle.trim();
|
||||
todo.editing = false;
|
||||
|
||||
if (editedTitle.length === 0) {
|
||||
return this.todoStore.remove(todo);
|
||||
}
|
||||
|
||||
todo.title = editedTitle;
|
||||
}
|
||||
|
||||
editTodo(todo: Todo) {
|
||||
todo.editing = true;
|
||||
}
|
||||
|
||||
removeCompleted() {
|
||||
this.todoStore.removeCompleted();
|
||||
}
|
||||
|
||||
toggleCompletion(todo: Todo) {
|
||||
this.todoStore.toggleCompletion(todo);
|
||||
}
|
||||
|
||||
remove(todo: Todo){
|
||||
this.todoStore.remove(todo);
|
||||
}
|
||||
|
||||
addTodo() {
|
||||
if (this.newTodoText.trim().length) {
|
||||
this.todoStore.add(this.newTodoText);
|
||||
this.newTodoText = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
import { AppComponent } from './app.component';
|
||||
import { TodoStore } from './store';
|
||||
import { FormsModule } from "@angular/forms";
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
FormsModule
|
||||
],
|
||||
providers: [TodoStore],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
export class AppModule { }
|
||||
140
examples/basic-crud-application/angular-client/src/app/store.ts
Normal file
140
examples/basic-crud-application/angular-client/src/app/store.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import { io, Socket } from "socket.io-client";
|
||||
import { ClientEvents, ServerEvents } from "../../../server/lib/events";
|
||||
import { environment } from '../environments/environment';
|
||||
|
||||
export interface Todo {
|
||||
id: string,
|
||||
title: string,
|
||||
completed: boolean,
|
||||
editing: boolean,
|
||||
synced: boolean
|
||||
}
|
||||
|
||||
const mapTodo = (todo: any) => {
|
||||
return {
|
||||
...todo,
|
||||
editing: false,
|
||||
synced: true
|
||||
}
|
||||
}
|
||||
|
||||
export class TodoStore {
|
||||
public todos: Array<Todo> = [];
|
||||
private socket: Socket<ServerEvents, ClientEvents>;
|
||||
|
||||
constructor() {
|
||||
this.socket = io(environment.serverUrl);
|
||||
|
||||
this.socket.on("connect", () => {
|
||||
this.socket.emit("todo:list", (res) => {
|
||||
if ("error" in res) {
|
||||
// handle the error
|
||||
return;
|
||||
}
|
||||
this.todos = res.data.map(mapTodo);
|
||||
});
|
||||
});
|
||||
|
||||
this.socket.on("todo:created", (todo) => {
|
||||
this.todos.push(mapTodo(todo));
|
||||
});
|
||||
|
||||
this.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;
|
||||
}
|
||||
});
|
||||
|
||||
this.socket.on("todo:deleted", (id) => {
|
||||
const index = this.todos.findIndex(t => {
|
||||
return t.id === id
|
||||
});
|
||||
if (index !== -1) {
|
||||
this.todos.splice(index, 1);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private getWithCompleted(completed: boolean) {
|
||||
return this.todos.filter((todo: Todo) => todo.completed === completed);
|
||||
}
|
||||
|
||||
allCompleted() {
|
||||
return this.todos.length === this.getCompleted().length;
|
||||
}
|
||||
|
||||
setAllTo(completed: boolean) {
|
||||
this.todos.forEach(todo => {
|
||||
todo.completed = completed;
|
||||
todo.synced = false;
|
||||
this.socket.emit("todo:update", todo, (res) => {
|
||||
if (res && "error" in res) {
|
||||
// handle the error
|
||||
return;
|
||||
}
|
||||
todo.synced = true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
removeCompleted() {
|
||||
this.getCompleted().forEach((todo) => {
|
||||
this.socket.emit("todo:delete", todo.id, (res) => {
|
||||
if (res && "error" in res) {
|
||||
// handle the error
|
||||
}
|
||||
});
|
||||
})
|
||||
this.todos = this.getRemaining();
|
||||
}
|
||||
|
||||
getRemaining() {
|
||||
return this.getWithCompleted(false);
|
||||
}
|
||||
|
||||
getCompleted() {
|
||||
return this.getWithCompleted(true);
|
||||
}
|
||||
|
||||
toggleCompletion(todo: Todo) {
|
||||
todo.completed = !todo.completed;
|
||||
todo.synced = false;
|
||||
this.socket.emit("todo:update", todo, (res) => {
|
||||
if (res && "error" in res) {
|
||||
// handle the error
|
||||
return;
|
||||
}
|
||||
todo.synced = true;
|
||||
})
|
||||
}
|
||||
|
||||
remove(todo: Todo) {
|
||||
this.todos.splice(this.todos.indexOf(todo), 1);
|
||||
this.socket.emit("todo:delete", todo.id, (res) => {
|
||||
if (res && "error" in res) {
|
||||
// handle the error
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
add(title: string) {
|
||||
this.socket.emit("todo:create", { title, completed: false }, (res) => {
|
||||
if ("error" in res) {
|
||||
// handle the error
|
||||
return;
|
||||
}
|
||||
this.todos.push({
|
||||
id: res.data,
|
||||
title,
|
||||
completed: false,
|
||||
editing: false,
|
||||
synced: true
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export const environment = {
|
||||
production: true,
|
||||
serverUrl: "https://my-custom-domain.com"
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
// This file can be replaced during build by using the `fileReplacements` array.
|
||||
// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
|
||||
// The list of file replacements can be found in `angular.json`.
|
||||
|
||||
export const environment = {
|
||||
production: false,
|
||||
serverUrl: "http://localhost:3000"
|
||||
};
|
||||
|
||||
/*
|
||||
* For easier debugging in development mode, you can import the following file
|
||||
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
|
||||
*
|
||||
* This import should be commented out in production mode because it will have a negative impact
|
||||
* on performance if an error is thrown.
|
||||
*/
|
||||
// import 'zone.js/dist/zone-error'; // Included with Angular CLI.
|
||||
BIN
examples/basic-crud-application/angular-client/src/favicon.ico
Normal file
BIN
examples/basic-crud-application/angular-client/src/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 948 B |
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Angular Todo MVC</title>
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
</head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
||||
12
examples/basic-crud-application/angular-client/src/main.ts
Normal file
12
examples/basic-crud-application/angular-client/src/main.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { enableProdMode } from '@angular/core';
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||
|
||||
import { AppModule } from './app/app.module';
|
||||
import { environment } from './environments/environment';
|
||||
|
||||
if (environment.production) {
|
||||
enableProdMode();
|
||||
}
|
||||
|
||||
platformBrowserDynamic().bootstrapModule(AppModule)
|
||||
.catch(err => console.error(err));
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* This file includes polyfills needed by Angular and is loaded before the app.
|
||||
* You can add your own extra polyfills to this file.
|
||||
*
|
||||
* This file is divided into 2 sections:
|
||||
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
|
||||
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
|
||||
* file.
|
||||
*
|
||||
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
|
||||
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
|
||||
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
|
||||
*
|
||||
* Learn more in https://angular.io/guide/browser-support
|
||||
*/
|
||||
|
||||
/***************************************************************************************************
|
||||
* BROWSER POLYFILLS
|
||||
*/
|
||||
|
||||
/** IE11 requires the following for NgClass support on SVG elements */
|
||||
// import 'classlist.js'; // Run `npm install --save classlist.js`.
|
||||
|
||||
/**
|
||||
* Web Animations `@angular/platform-browser/animations`
|
||||
* Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
|
||||
* Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
|
||||
*/
|
||||
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
|
||||
|
||||
/**
|
||||
* By default, zone.js will patch all possible macroTask and DomEvents
|
||||
* user can disable parts of macroTask/DomEvents patch by setting following flags
|
||||
* because those flags need to be set before `zone.js` being loaded, and webpack
|
||||
* will put import in the top of bundle, so user need to create a separate file
|
||||
* in this directory (for example: zone-flags.ts), and put the following flags
|
||||
* into that file, and then add the following code before importing zone.js.
|
||||
* import './zone-flags';
|
||||
*
|
||||
* The flags allowed in zone-flags.ts are listed here.
|
||||
*
|
||||
* The following flags will work for all browsers.
|
||||
*
|
||||
* (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
|
||||
* (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
|
||||
* (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
|
||||
*
|
||||
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
|
||||
* with the following flag, it will bypass `zone.js` patch for IE/Edge
|
||||
*
|
||||
* (window as any).__Zone_enable_cross_context_check = true;
|
||||
*
|
||||
*/
|
||||
|
||||
/***************************************************************************************************
|
||||
* Zone JS is required by default for Angular itself.
|
||||
*/
|
||||
import 'zone.js/dist/zone'; // Included with Angular CLI.
|
||||
|
||||
|
||||
/***************************************************************************************************
|
||||
* APPLICATION IMPORTS
|
||||
*/
|
||||
381
examples/basic-crud-application/angular-client/src/styles.css
Normal file
381
examples/basic-crud-application/angular-client/src/styles.css
Normal file
@@ -0,0 +1,381 @@
|
||||
/* imported from node_modules/todomvc-app-css/index.css */
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: none;
|
||||
font-size: 100%;
|
||||
vertical-align: baseline;
|
||||
font-family: inherit;
|
||||
font-weight: inherit;
|
||||
color: inherit;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body {
|
||||
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
line-height: 1.4em;
|
||||
background: #f5f5f5;
|
||||
color: #111111;
|
||||
min-width: 230px;
|
||||
max-width: 550px;
|
||||
margin: 0 auto;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.todoapp {
|
||||
background: #fff;
|
||||
margin: 130px 0 40px 0;
|
||||
position: relative;
|
||||
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
|
||||
0 25px 50px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.todoapp input::-webkit-input-placeholder {
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.todoapp input::-moz-placeholder {
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.todoapp input::input-placeholder {
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.todoapp h1 {
|
||||
position: absolute;
|
||||
top: -140px;
|
||||
width: 100%;
|
||||
font-size: 80px;
|
||||
font-weight: 200;
|
||||
text-align: center;
|
||||
color: #b83f45;
|
||||
-webkit-text-rendering: optimizeLegibility;
|
||||
-moz-text-rendering: optimizeLegibility;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
.new-todo,
|
||||
.edit {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
font-size: 24px;
|
||||
font-family: inherit;
|
||||
font-weight: inherit;
|
||||
line-height: 1.4em;
|
||||
color: inherit;
|
||||
padding: 6px;
|
||||
border: 1px solid #999;
|
||||
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
|
||||
box-sizing: border-box;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.new-todo {
|
||||
padding: 16px 16px 16px 60px;
|
||||
border: none;
|
||||
background: rgba(0, 0, 0, 0.003);
|
||||
box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
|
||||
}
|
||||
|
||||
.main {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
border-top: 1px solid #e6e6e6;
|
||||
}
|
||||
|
||||
.toggle-all {
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
border: none; /* Mobile Safari */
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
right: 100%;
|
||||
bottom: 100%;
|
||||
}
|
||||
|
||||
.toggle-all + label {
|
||||
width: 60px;
|
||||
height: 34px;
|
||||
font-size: 0;
|
||||
position: absolute;
|
||||
top: -52px;
|
||||
left: -13px;
|
||||
-webkit-transform: rotate(90deg);
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.toggle-all + label:before {
|
||||
content: '❯';
|
||||
font-size: 22px;
|
||||
color: #e6e6e6;
|
||||
padding: 10px 27px 10px 27px;
|
||||
}
|
||||
|
||||
.toggle-all:checked + label:before {
|
||||
color: #737373;
|
||||
}
|
||||
|
||||
.todo-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.todo-list li {
|
||||
position: relative;
|
||||
font-size: 24px;
|
||||
border-bottom: 1px solid #ededed;
|
||||
}
|
||||
|
||||
.todo-list li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.todo-list li.editing {
|
||||
border-bottom: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.todo-list li.editing .edit {
|
||||
display: block;
|
||||
width: calc(100% - 43px);
|
||||
padding: 12px 16px;
|
||||
margin: 0 0 0 43px;
|
||||
}
|
||||
|
||||
.todo-list li.editing .view {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.todo-list li .toggle {
|
||||
text-align: center;
|
||||
width: 40px;
|
||||
/* auto, since non-WebKit browsers doesn't support input styling */
|
||||
height: auto;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
margin: auto 0;
|
||||
border: none; /* Mobile Safari */
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
.todo-list li .toggle {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.todo-list li .toggle + label {
|
||||
/*
|
||||
Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433
|
||||
IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/
|
||||
*/
|
||||
background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E');
|
||||
background-repeat: no-repeat;
|
||||
background-position: center left;
|
||||
}
|
||||
|
||||
.todo-list li .toggle:checked + label {
|
||||
background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E');
|
||||
}
|
||||
|
||||
.todo-list li label {
|
||||
word-break: break-all;
|
||||
padding: 15px 15px 15px 60px;
|
||||
display: block;
|
||||
line-height: 1.2;
|
||||
transition: color 0.4s;
|
||||
font-weight: 400;
|
||||
color: #4d4d4d;
|
||||
}
|
||||
|
||||
.todo-list li.completed label {
|
||||
color: #cdcdcd;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.todo-list li .destroy {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 10px;
|
||||
bottom: 0;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin: auto 0;
|
||||
font-size: 30px;
|
||||
color: #cc9a9a;
|
||||
margin-bottom: 11px;
|
||||
transition: color 0.2s ease-out;
|
||||
}
|
||||
|
||||
.todo-list li .destroy:hover {
|
||||
color: #af5b5e;
|
||||
}
|
||||
|
||||
.todo-list li .destroy:after {
|
||||
content: '×';
|
||||
}
|
||||
|
||||
.todo-list li:hover .destroy {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.todo-list li .edit {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.todo-list li.editing:last-child {
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding: 10px 15px;
|
||||
height: 20px;
|
||||
text-align: center;
|
||||
font-size: 15px;
|
||||
border-top: 1px solid #e6e6e6;
|
||||
}
|
||||
|
||||
.footer:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 50px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
|
||||
0 8px 0 -3px #f6f6f6,
|
||||
0 9px 1px -3px rgba(0, 0, 0, 0.2),
|
||||
0 16px 0 -6px #f6f6f6,
|
||||
0 17px 2px -6px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.todo-count {
|
||||
float: left;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.todo-count strong {
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.filters {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.filters li {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.filters li a {
|
||||
color: inherit;
|
||||
margin: 3px;
|
||||
padding: 3px 7px;
|
||||
text-decoration: none;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.filters li a:hover {
|
||||
border-color: rgba(175, 47, 47, 0.1);
|
||||
}
|
||||
|
||||
.filters li a.selected {
|
||||
border-color: rgba(175, 47, 47, 0.2);
|
||||
}
|
||||
|
||||
.clear-completed,
|
||||
html .clear-completed:active {
|
||||
float: right;
|
||||
position: relative;
|
||||
line-height: 20px;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.clear-completed:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.info {
|
||||
margin: 65px auto 0;
|
||||
color: #4d4d4d;
|
||||
font-size: 11px;
|
||||
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.info p {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.info a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.info a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/*
|
||||
Hack to remove background from Mobile Safari.
|
||||
Can't use it globally since it destroys checkboxes in Firefox
|
||||
*/
|
||||
@media screen and (-webkit-min-device-pixel-ratio:0) {
|
||||
.toggle-all,
|
||||
.todo-list li .toggle {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.todo-list li .toggle {
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 430px) {
|
||||
.footer {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.filters {
|
||||
bottom: 10px;
|
||||
}
|
||||
}
|
||||
25
examples/basic-crud-application/angular-client/src/test.ts
Normal file
25
examples/basic-crud-application/angular-client/src/test.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
|
||||
|
||||
import 'zone.js/dist/zone-testing';
|
||||
import { getTestBed } from '@angular/core/testing';
|
||||
import {
|
||||
BrowserDynamicTestingModule,
|
||||
platformBrowserDynamicTesting
|
||||
} from '@angular/platform-browser-dynamic/testing';
|
||||
|
||||
declare const require: {
|
||||
context(path: string, deep?: boolean, filter?: RegExp): {
|
||||
keys(): string[];
|
||||
<T>(id: string): T;
|
||||
};
|
||||
};
|
||||
|
||||
// First, initialize the Angular testing environment.
|
||||
getTestBed().initTestEnvironment(
|
||||
BrowserDynamicTestingModule,
|
||||
platformBrowserDynamicTesting()
|
||||
);
|
||||
// Then we find all the tests.
|
||||
const context = require.context('./', true, /\.spec\.ts$/);
|
||||
// And load the modules.
|
||||
context.keys().map(context);
|
||||
@@ -0,0 +1,15 @@
|
||||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/app",
|
||||
"types": []
|
||||
},
|
||||
"files": [
|
||||
"src/main.ts",
|
||||
"src/polyfills.ts"
|
||||
],
|
||||
"include": [
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
29
examples/basic-crud-application/angular-client/tsconfig.json
Normal file
29
examples/basic-crud-application/angular-client/tsconfig.json
Normal file
@@ -0,0 +1,29 @@
|
||||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"outDir": "./dist/out-tsc",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"sourceMap": true,
|
||||
"declaration": false,
|
||||
"downlevelIteration": true,
|
||||
"experimentalDecorators": true,
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"target": "es2015",
|
||||
"module": "es2020",
|
||||
"lib": [
|
||||
"es2018",
|
||||
"dom"
|
||||
]
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"strictTemplates": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/spec",
|
||||
"types": [
|
||||
"jasmine"
|
||||
]
|
||||
},
|
||||
"files": [
|
||||
"src/test.ts",
|
||||
"src/polyfills.ts"
|
||||
],
|
||||
"include": [
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
152
examples/basic-crud-application/angular-client/tslint.json
Normal file
152
examples/basic-crud-application/angular-client/tslint.json
Normal file
@@ -0,0 +1,152 @@
|
||||
{
|
||||
"extends": "tslint:recommended",
|
||||
"rulesDirectory": [
|
||||
"codelyzer"
|
||||
],
|
||||
"rules": {
|
||||
"align": {
|
||||
"options": [
|
||||
"parameters",
|
||||
"statements"
|
||||
]
|
||||
},
|
||||
"array-type": false,
|
||||
"arrow-return-shorthand": true,
|
||||
"curly": true,
|
||||
"deprecation": {
|
||||
"severity": "warning"
|
||||
},
|
||||
"eofline": true,
|
||||
"import-blacklist": [
|
||||
true,
|
||||
"rxjs/Rx"
|
||||
],
|
||||
"import-spacing": true,
|
||||
"indent": {
|
||||
"options": [
|
||||
"spaces"
|
||||
]
|
||||
},
|
||||
"max-classes-per-file": false,
|
||||
"max-line-length": [
|
||||
true,
|
||||
140
|
||||
],
|
||||
"member-ordering": [
|
||||
true,
|
||||
{
|
||||
"order": [
|
||||
"static-field",
|
||||
"instance-field",
|
||||
"static-method",
|
||||
"instance-method"
|
||||
]
|
||||
}
|
||||
],
|
||||
"no-console": [
|
||||
true,
|
||||
"debug",
|
||||
"info",
|
||||
"time",
|
||||
"timeEnd",
|
||||
"trace"
|
||||
],
|
||||
"no-empty": false,
|
||||
"no-inferrable-types": [
|
||||
true,
|
||||
"ignore-params"
|
||||
],
|
||||
"no-non-null-assertion": true,
|
||||
"no-redundant-jsdoc": true,
|
||||
"no-switch-case-fall-through": true,
|
||||
"no-var-requires": false,
|
||||
"object-literal-key-quotes": [
|
||||
true,
|
||||
"as-needed"
|
||||
],
|
||||
"quotemark": [
|
||||
true,
|
||||
"single"
|
||||
],
|
||||
"semicolon": {
|
||||
"options": [
|
||||
"always"
|
||||
]
|
||||
},
|
||||
"space-before-function-paren": {
|
||||
"options": {
|
||||
"anonymous": "never",
|
||||
"asyncArrow": "always",
|
||||
"constructor": "never",
|
||||
"method": "never",
|
||||
"named": "never"
|
||||
}
|
||||
},
|
||||
"typedef": [
|
||||
true,
|
||||
"call-signature"
|
||||
],
|
||||
"typedef-whitespace": {
|
||||
"options": [
|
||||
{
|
||||
"call-signature": "nospace",
|
||||
"index-signature": "nospace",
|
||||
"parameter": "nospace",
|
||||
"property-declaration": "nospace",
|
||||
"variable-declaration": "nospace"
|
||||
},
|
||||
{
|
||||
"call-signature": "onespace",
|
||||
"index-signature": "onespace",
|
||||
"parameter": "onespace",
|
||||
"property-declaration": "onespace",
|
||||
"variable-declaration": "onespace"
|
||||
}
|
||||
]
|
||||
},
|
||||
"variable-name": {
|
||||
"options": [
|
||||
"ban-keywords",
|
||||
"check-format",
|
||||
"allow-pascal-case"
|
||||
]
|
||||
},
|
||||
"whitespace": {
|
||||
"options": [
|
||||
"check-branch",
|
||||
"check-decl",
|
||||
"check-operator",
|
||||
"check-separator",
|
||||
"check-type",
|
||||
"check-typecast"
|
||||
]
|
||||
},
|
||||
"component-class-suffix": true,
|
||||
"contextual-lifecycle": true,
|
||||
"directive-class-suffix": true,
|
||||
"no-conflicting-lifecycle": true,
|
||||
"no-host-metadata-property": true,
|
||||
"no-input-rename": true,
|
||||
"no-inputs-metadata-property": true,
|
||||
"no-output-native": true,
|
||||
"no-output-on-prefix": true,
|
||||
"no-output-rename": true,
|
||||
"no-outputs-metadata-property": true,
|
||||
"template-banana-in-box": true,
|
||||
"template-no-negated-async": true,
|
||||
"use-lifecycle-interface": true,
|
||||
"use-pipe-transform-interface": true,
|
||||
"directive-selector": [
|
||||
true,
|
||||
"attribute",
|
||||
"app",
|
||||
"camelCase"
|
||||
],
|
||||
"component-selector": [
|
||||
true,
|
||||
"element",
|
||||
"app",
|
||||
"kebab-case"
|
||||
]
|
||||
}
|
||||
}
|
||||
35
examples/basic-crud-application/server/lib/app.ts
Normal file
35
examples/basic-crud-application/server/lib/app.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Server as HttpServer } from "http";
|
||||
import { Server, ServerOptions } from "socket.io";
|
||||
import { ClientEvents, ServerEvents } from "./events";
|
||||
import { TodoRepository } from "./todo-management/todo.repository";
|
||||
import createTodoHandlers from "./todo-management/todo.handlers";
|
||||
|
||||
export interface Components {
|
||||
todoRepository: TodoRepository;
|
||||
}
|
||||
|
||||
export function createApplication(
|
||||
httpServer: HttpServer,
|
||||
components: Components,
|
||||
serverOptions: Partial<ServerOptions> = {}
|
||||
): Server<ClientEvents, ServerEvents> {
|
||||
const io = new Server<ClientEvents, ServerEvents>(httpServer, serverOptions);
|
||||
|
||||
const {
|
||||
createTodo,
|
||||
readTodo,
|
||||
updateTodo,
|
||||
deleteTodo,
|
||||
listTodo,
|
||||
} = createTodoHandlers(components);
|
||||
|
||||
io.on("connection", (socket) => {
|
||||
socket.on("todo:create", createTodo);
|
||||
socket.on("todo:read", readTodo);
|
||||
socket.on("todo:update", updateTodo);
|
||||
socket.on("todo:delete", deleteTodo);
|
||||
socket.on("todo:list", listTodo);
|
||||
});
|
||||
|
||||
return io;
|
||||
}
|
||||
37
examples/basic-crud-application/server/lib/events.ts
Normal file
37
examples/basic-crud-application/server/lib/events.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Todo, TodoID } from "./todo-management/todo.repository";
|
||||
import { ValidationErrorItem } from "joi";
|
||||
|
||||
interface Error {
|
||||
error: string;
|
||||
errorDetails?: ValidationErrorItem[];
|
||||
}
|
||||
|
||||
interface Success<T> {
|
||||
data: T;
|
||||
}
|
||||
|
||||
export type Response<T> = Error | Success<T>;
|
||||
|
||||
export interface ServerEvents {
|
||||
"todo:created": (todo: Todo) => void;
|
||||
"todo:updated": (todo: Todo) => void;
|
||||
"todo:deleted": (id: TodoID) => void;
|
||||
}
|
||||
|
||||
export interface ClientEvents {
|
||||
"todo:list": (callback: (res: Response<Todo[]>) => void) => void;
|
||||
|
||||
"todo:create": (
|
||||
payload: Omit<Todo, "id">,
|
||||
callback: (res: Response<TodoID>) => void
|
||||
) => void;
|
||||
|
||||
"todo:read": (id: TodoID, callback: (res: Response<Todo>) => void) => void;
|
||||
|
||||
"todo:update": (
|
||||
payload: Todo,
|
||||
callback: (res?: Response<void>) => void
|
||||
) => void;
|
||||
|
||||
"todo:delete": (id: TodoID, callback: (res?: Response<void>) => void) => void;
|
||||
}
|
||||
19
examples/basic-crud-application/server/lib/index.ts
Normal file
19
examples/basic-crud-application/server/lib/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { createServer } from "http";
|
||||
import { createApplication } from "./app";
|
||||
import { InMemoryTodoRepository } from "./todo-management/todo.repository";
|
||||
|
||||
const httpServer = createServer();
|
||||
|
||||
createApplication(
|
||||
httpServer,
|
||||
{
|
||||
todoRepository: new InMemoryTodoRepository(),
|
||||
},
|
||||
{
|
||||
cors: {
|
||||
origin: ["http://localhost:4200"],
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
httpServer.listen(3000);
|
||||
@@ -0,0 +1,159 @@
|
||||
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 { Socket } from "socket.io";
|
||||
|
||||
const idSchema = Joi.string().guid({
|
||||
version: "uuidv4",
|
||||
});
|
||||
|
||||
const todoSchema = Joi.object({
|
||||
id: idSchema.alter({
|
||||
create: (schema) => schema.forbidden(),
|
||||
update: (schema) => schema.required(),
|
||||
}),
|
||||
title: Joi.string().max(256).required(),
|
||||
completed: Joi.boolean().required(),
|
||||
});
|
||||
|
||||
export default function (components: Components) {
|
||||
const { todoRepository } = components;
|
||||
return {
|
||||
createTodo: async function (
|
||||
payload: Omit<Todo, "id">,
|
||||
callback: (res: Response<TodoID>) => void
|
||||
) {
|
||||
// @ts-ignore
|
||||
const socket: Socket<ClientEvents, ServerEvents> = this;
|
||||
|
||||
// validate the payload
|
||||
const { error, value } = todoSchema.tailor("create").validate(payload, {
|
||||
abortEarly: false,
|
||||
stripUnknown: true,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return callback({
|
||||
error: Errors.INVALID_PAYLOAD,
|
||||
errorDetails: mapErrorDetails(error.details),
|
||||
});
|
||||
}
|
||||
|
||||
value.id = uuid();
|
||||
|
||||
// persist the entity
|
||||
try {
|
||||
await todoRepository.save(value);
|
||||
} catch (e) {
|
||||
return callback({
|
||||
error: sanitizeErrorMessage(e),
|
||||
});
|
||||
}
|
||||
|
||||
// acknowledge the creation
|
||||
callback({
|
||||
data: value.id,
|
||||
});
|
||||
|
||||
// notify the other users
|
||||
socket.broadcast.emit("todo:created", value);
|
||||
},
|
||||
|
||||
readTodo: async function (
|
||||
id: TodoID,
|
||||
callback: (res: Response<Todo>) => void
|
||||
) {
|
||||
const { error } = idSchema.validate(id);
|
||||
|
||||
if (error) {
|
||||
return callback({
|
||||
error: Errors.ENTITY_NOT_FOUND,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const todo = await todoRepository.findById(id);
|
||||
callback({
|
||||
data: todo,
|
||||
});
|
||||
} catch (e) {
|
||||
callback({
|
||||
error: sanitizeErrorMessage(e),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
updateTodo: async function (
|
||||
payload: Todo,
|
||||
callback: (res?: Response<void>) => void
|
||||
) {
|
||||
// @ts-ignore
|
||||
const socket: Socket<ClientEvents, ServerEvents> = this;
|
||||
|
||||
const { error, value } = todoSchema.tailor("update").validate(payload, {
|
||||
abortEarly: false,
|
||||
stripUnknown: true,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return callback({
|
||||
error: Errors.INVALID_PAYLOAD,
|
||||
errorDetails: mapErrorDetails(error.details),
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await todoRepository.save(value);
|
||||
} catch (e) {
|
||||
return callback({
|
||||
error: sanitizeErrorMessage(e),
|
||||
});
|
||||
}
|
||||
|
||||
callback();
|
||||
socket.broadcast.emit("todo:updated", value);
|
||||
},
|
||||
|
||||
deleteTodo: async function (
|
||||
id: TodoID,
|
||||
callback: (res?: Response<void>) => void
|
||||
) {
|
||||
// @ts-ignore
|
||||
const socket: Socket<ClientEvents, ServerEvents> = this;
|
||||
|
||||
const { error } = idSchema.validate(id);
|
||||
|
||||
if (error) {
|
||||
return callback({
|
||||
error: Errors.ENTITY_NOT_FOUND,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await todoRepository.deleteById(id);
|
||||
} catch (e) {
|
||||
return callback({
|
||||
error: sanitizeErrorMessage(e),
|
||||
});
|
||||
}
|
||||
|
||||
callback();
|
||||
socket.broadcast.emit("todo:deleted", id);
|
||||
},
|
||||
|
||||
listTodo: async function (callback: (res: Response<Todo[]>) => void) {
|
||||
try {
|
||||
callback({
|
||||
data: await todoRepository.findAll(),
|
||||
});
|
||||
} catch (e) {
|
||||
callback({
|
||||
error: sanitizeErrorMessage(e),
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Errors } from "../util";
|
||||
|
||||
abstract class CrudRepository<T, ID> {
|
||||
abstract findAll(): Promise<T[]>;
|
||||
abstract findById(id: ID): Promise<T>;
|
||||
abstract save(entity: T): Promise<void>;
|
||||
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 {
|
||||
private readonly todos: Map<TodoID, Todo> = new Map();
|
||||
|
||||
findAll(): Promise<Todo[]> {
|
||||
const entities = Array.from(this.todos.values());
|
||||
return Promise.resolve(entities);
|
||||
}
|
||||
|
||||
findById(id: TodoID): Promise<Todo> {
|
||||
if (this.todos.has(id)) {
|
||||
return Promise.resolve(this.todos.get(id)!);
|
||||
} else {
|
||||
return Promise.reject(Errors.ENTITY_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
save(entity: Todo): Promise<void> {
|
||||
this.todos.set(entity.id, entity);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
deleteById(id: TodoID): Promise<void> {
|
||||
const deleted = this.todos.delete(id);
|
||||
if (deleted) {
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
return Promise.reject(Errors.ENTITY_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
}
|
||||
24
examples/basic-crud-application/server/lib/util.ts
Normal file
24
examples/basic-crud-application/server/lib/util.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { ValidationErrorItem } from "joi";
|
||||
|
||||
export enum Errors {
|
||||
ENTITY_NOT_FOUND = "entity not found",
|
||||
INVALID_PAYLOAD = "invalid payload",
|
||||
}
|
||||
|
||||
const errorValues: string[] = Object.values(Errors);
|
||||
|
||||
export function sanitizeErrorMessage(message: string) {
|
||||
if (errorValues.includes(message)) {
|
||||
return message;
|
||||
} else {
|
||||
return "an unknown error has occurred";
|
||||
}
|
||||
}
|
||||
|
||||
export function mapErrorDetails(details: ValidationErrorItem[]) {
|
||||
return details.map((item) => ({
|
||||
message: item.message,
|
||||
path: item.path,
|
||||
type: item.type,
|
||||
}));
|
||||
}
|
||||
37
examples/basic-crud-application/server/package.json
Normal file
37
examples/basic-crud-application/server/package.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "basic-crud-server",
|
||||
"version": "0.0.1",
|
||||
"description": "Server for the Basic CRUD Socket.IO example",
|
||||
"main": "dist/lib/index.js",
|
||||
"scripts": {
|
||||
"start": "ts-node lib/index.ts",
|
||||
"build": "tsc",
|
||||
"test": "nyc mocha --require ts-node/register test/**/*.ts"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/socketio/socket.io.git"
|
||||
},
|
||||
"author": "Damien Arrachequesne <damien.arrachequesne@gmail.com>",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/socketio/socket.io/issues"
|
||||
},
|
||||
"homepage": "https://github.com/socketio/socket.io#readme",
|
||||
"dependencies": {
|
||||
"joi": "^17.4.0",
|
||||
"socket.io": "^4.0.1",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mocha": "^8.2.3",
|
||||
"@types/chai": "^4.2.16",
|
||||
"@types/uuid": "^8.3.0",
|
||||
"chai": "^4.3.4",
|
||||
"mocha": "^8.3.2",
|
||||
"nyc": "^15.1.0",
|
||||
"socket.io-client": "^4.0.1",
|
||||
"ts-node": "^9.1.1",
|
||||
"typescript": "^4.2.4"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
import { createApplication } from "../../lib/app";
|
||||
import { createServer, Server } from "http";
|
||||
import {
|
||||
InMemoryTodoRepository,
|
||||
TodoRepository,
|
||||
} from "../../lib/todo-management/todo.repository";
|
||||
import { AddressInfo } from "net";
|
||||
import { io, Socket } from "socket.io-client";
|
||||
import { ClientEvents, ServerEvents } from "../../lib/events";
|
||||
import { expect } from "chai";
|
||||
|
||||
const createPartialDone = (count: number, done: () => void) => {
|
||||
let i = 0;
|
||||
return () => {
|
||||
if (++i === count) {
|
||||
done();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
describe("todo management", () => {
|
||||
let httpServer: Server,
|
||||
socket: Socket<ServerEvents, ClientEvents>,
|
||||
otherSocket: Socket<ServerEvents, ClientEvents>,
|
||||
todoRepository: TodoRepository;
|
||||
|
||||
beforeEach((done) => {
|
||||
const partialDone = createPartialDone(2, done);
|
||||
|
||||
httpServer = createServer();
|
||||
todoRepository = new InMemoryTodoRepository();
|
||||
|
||||
createApplication(httpServer, {
|
||||
todoRepository,
|
||||
});
|
||||
|
||||
httpServer.listen(() => {
|
||||
const port = (httpServer.address() as AddressInfo).port;
|
||||
socket = io(`http://localhost:${port}`);
|
||||
socket.on("connect", partialDone);
|
||||
|
||||
otherSocket = io(`http://localhost:${port}`);
|
||||
otherSocket.on("connect", partialDone);
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
httpServer.close();
|
||||
socket.disconnect();
|
||||
otherSocket.disconnect();
|
||||
});
|
||||
|
||||
describe("create todo", () => {
|
||||
it("should create a todo entity", (done) => {
|
||||
const partialDone = createPartialDone(2, done);
|
||||
|
||||
socket.emit(
|
||||
"todo:create",
|
||||
{
|
||||
title: "lorem ipsum",
|
||||
completed: false,
|
||||
},
|
||||
async (res) => {
|
||||
if ("error" in res) {
|
||||
return done(new Error("should not happen"));
|
||||
}
|
||||
expect(res.data).to.be.a("string");
|
||||
|
||||
const storedEntity = await todoRepository.findById(res.data);
|
||||
expect(storedEntity).to.eql({
|
||||
id: res.data,
|
||||
title: "lorem ipsum",
|
||||
completed: false,
|
||||
});
|
||||
|
||||
partialDone();
|
||||
}
|
||||
);
|
||||
|
||||
otherSocket.on("todo:created", (todo) => {
|
||||
expect(todo.id).to.be.a("string");
|
||||
expect(todo.title).to.eql("lorem ipsum");
|
||||
expect(todo.completed).to.eql(false);
|
||||
partialDone();
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail with an invalid entity", (done) => {
|
||||
const incompleteTodo = {
|
||||
completed: "false",
|
||||
description: true,
|
||||
};
|
||||
// @ts-ignore
|
||||
socket.emit("todo:create", incompleteTodo, (res) => {
|
||||
if (!("error" in res)) {
|
||||
return done(new Error("should not happen"));
|
||||
}
|
||||
expect(res.error).to.eql("invalid payload");
|
||||
expect(res.errorDetails).to.eql([
|
||||
{
|
||||
message: '"title" is required',
|
||||
path: ["title"],
|
||||
type: "any.required",
|
||||
},
|
||||
]);
|
||||
done();
|
||||
});
|
||||
|
||||
otherSocket.on("todo:created", () => {
|
||||
done(new Error("should not happen"));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("read todo", () => {
|
||||
it("should return a todo entity", (done) => {
|
||||
todoRepository.save({
|
||||
id: "254dbf85-f5b9-4675-b913-acab5d600884",
|
||||
title: "lorem ipsum",
|
||||
completed: true,
|
||||
});
|
||||
|
||||
socket.emit(
|
||||
"todo:read",
|
||||
"254dbf85-f5b9-4675-b913-acab5d600884",
|
||||
(res) => {
|
||||
if ("error" in res) {
|
||||
return done(new Error("should not happen"));
|
||||
}
|
||||
expect(res.data.id).to.eql("254dbf85-f5b9-4675-b913-acab5d600884");
|
||||
expect(res.data.title).to.eql("lorem ipsum");
|
||||
expect(res.data.completed).to.eql(true);
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it("should fail with an invalid ID", (done) => {
|
||||
socket.emit("todo:read", "123", (res) => {
|
||||
if ("error" in res) {
|
||||
expect(res.error).to.eql("entity not found");
|
||||
done();
|
||||
} else {
|
||||
done(new Error("should not happen"));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail with an unknown entity", (done) => {
|
||||
socket.emit(
|
||||
"todo:read",
|
||||
"6edcf81e-7049-40e0-8497-9cdd52414f75",
|
||||
(res) => {
|
||||
if ("error" in res) {
|
||||
expect(res.error).to.eql("entity not found");
|
||||
done();
|
||||
} else {
|
||||
done(new Error("should not happen"));
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("update todo", () => {
|
||||
it("should update a todo entity", (done) => {
|
||||
const partialDone = createPartialDone(2, done);
|
||||
|
||||
todoRepository.save({
|
||||
id: "c7790b35-6bbb-45dd-8d67-a281ca407b43",
|
||||
title: "lorem ipsum",
|
||||
completed: true,
|
||||
});
|
||||
|
||||
socket.emit(
|
||||
"todo:update",
|
||||
{
|
||||
id: "c7790b35-6bbb-45dd-8d67-a281ca407b43",
|
||||
title: "dolor sit amet",
|
||||
completed: true,
|
||||
},
|
||||
async () => {
|
||||
const storedEntity = await todoRepository.findById(
|
||||
"c7790b35-6bbb-45dd-8d67-a281ca407b43"
|
||||
);
|
||||
expect(storedEntity).to.eql({
|
||||
id: "c7790b35-6bbb-45dd-8d67-a281ca407b43",
|
||||
title: "dolor sit amet",
|
||||
completed: true,
|
||||
});
|
||||
partialDone();
|
||||
}
|
||||
);
|
||||
|
||||
otherSocket.on("todo:updated", (todo) => {
|
||||
expect(todo.title).to.eql("dolor sit amet");
|
||||
expect(todo.completed).to.eql(true);
|
||||
partialDone();
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail with an invalid entity", (done) => {
|
||||
const incompleteTodo = {
|
||||
id: "123",
|
||||
completed: "false",
|
||||
description: true,
|
||||
};
|
||||
// @ts-ignore
|
||||
socket.emit("todo:update", incompleteTodo, (res) => {
|
||||
if (!(res && "error" in res)) {
|
||||
return done(new Error("should not happen"));
|
||||
}
|
||||
expect(res.error).to.eql("invalid payload");
|
||||
expect(res.errorDetails).to.eql([
|
||||
{
|
||||
message: '"id" must be a valid GUID',
|
||||
path: ["id"],
|
||||
type: "string.guid",
|
||||
},
|
||||
{
|
||||
message: '"title" is required',
|
||||
path: ["title"],
|
||||
type: "any.required",
|
||||
},
|
||||
]);
|
||||
done();
|
||||
});
|
||||
|
||||
otherSocket.on("todo:updated", () => {
|
||||
done(new Error("should not happen"));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("delete todo", () => {
|
||||
it("should delete a todo entity", (done) => {
|
||||
const partialDone = createPartialDone(2, done);
|
||||
const id = "58960ab2-4e78-4ced-8079-134f12179d46";
|
||||
|
||||
todoRepository.save({
|
||||
id,
|
||||
title: "lorem ipsum",
|
||||
completed: true,
|
||||
});
|
||||
|
||||
socket.emit("todo:delete", id, async () => {
|
||||
try {
|
||||
await todoRepository.findById(id);
|
||||
} catch (e) {
|
||||
partialDone();
|
||||
}
|
||||
});
|
||||
|
||||
otherSocket.on("todo:deleted", (id) => {
|
||||
expect(id).to.eql("58960ab2-4e78-4ced-8079-134f12179d46");
|
||||
partialDone();
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail with an invalid ID", (done) => {
|
||||
socket.emit("todo:delete", "123", (res) => {
|
||||
if (!(res && "error" in res)) {
|
||||
return done(new Error("should not happen"));
|
||||
}
|
||||
expect(res.error).to.eql("entity not found");
|
||||
done();
|
||||
});
|
||||
|
||||
otherSocket.on("todo:deleted", () => {
|
||||
done(new Error("should not happen"));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("list todo", () => {
|
||||
it("should return a list of entities", (done) => {
|
||||
todoRepository.save({
|
||||
id: "d445db6d-9d55-4ff2-88ae-bd1f81c299d2",
|
||||
title: "lorem ipsum",
|
||||
completed: false,
|
||||
});
|
||||
|
||||
todoRepository.save({
|
||||
id: "5f56fb59-a887-4984-93bf-eb39b4170a35",
|
||||
title: "dolor sit amet",
|
||||
completed: true,
|
||||
});
|
||||
|
||||
socket.emit("todo:list", (res) => {
|
||||
if ("error" in res) {
|
||||
return done(new Error("should not happen"));
|
||||
}
|
||||
expect(res.data).to.eql([
|
||||
{
|
||||
id: "d445db6d-9d55-4ff2-88ae-bd1f81c299d2",
|
||||
title: "lorem ipsum",
|
||||
completed: false,
|
||||
},
|
||||
{
|
||||
id: "5f56fb59-a887-4984-93bf-eb39b4170a35",
|
||||
title: "dolor sit amet",
|
||||
completed: true,
|
||||
},
|
||||
]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
11
examples/basic-crud-application/server/tsconfig.json
Normal file
11
examples/basic-crud-application/server/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"module": "commonjs",
|
||||
"target": "es2017",
|
||||
"strict": true
|
||||
},
|
||||
"include": [
|
||||
"./lib/**/*"
|
||||
]
|
||||
}
|
||||
20
examples/webpack-build-server/index.js
Normal file
20
examples/webpack-build-server/index.js
Normal file
@@ -0,0 +1,20 @@
|
||||
const { Server } = require("socket.io");
|
||||
|
||||
const clientFile = require("./node_modules/socket.io/client-dist/socket.io.min?raw");
|
||||
const clientMap = require("./node_modules/socket.io/client-dist/socket.io.min.js.map?raw");
|
||||
|
||||
Server.sendFile = (filename, req, res) => {
|
||||
res.end(filename.endsWith(".map") ? clientMap : clientFile);
|
||||
};
|
||||
|
||||
const io = new Server();
|
||||
|
||||
io.on("connection", socket => {
|
||||
console.log(`connect ${socket.id}`);
|
||||
|
||||
socket.on("disconnect", (reason) => {
|
||||
console.log(`disconnect ${socket.id} due to ${reason}`);
|
||||
});
|
||||
});
|
||||
|
||||
io.listen(3000);
|
||||
@@ -1,15 +0,0 @@
|
||||
|
||||
const server = require('http').createServer();
|
||||
const io = require('socket.io')(server, {
|
||||
serveClient: false
|
||||
});
|
||||
const port = process.env.PORT || 3000;
|
||||
|
||||
io.on('connect', onConnect);
|
||||
server.listen(port, () => console.log('server listening on port ' + port));
|
||||
|
||||
function onConnect(socket){
|
||||
console.log('connect ' + socket.id);
|
||||
|
||||
socket.on('disconnect', () => console.log('disconnect ' + socket.id));
|
||||
}
|
||||
@@ -4,13 +4,15 @@
|
||||
"description": "A sample Webpack build (for the server)",
|
||||
"scripts": {
|
||||
"start": "node dist/server.js",
|
||||
"build": "webpack --config ./support/webpack.config.js"
|
||||
"build": "webpack"
|
||||
},
|
||||
"author": "Damien Arrachequesne",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"bufferutil": "^4.0.3",
|
||||
"socket.io": "^4.0.0",
|
||||
"webpack": "~4.43.0",
|
||||
"webpack-cli": "~3.3.11"
|
||||
"utf-8-validate": "^5.0.5",
|
||||
"webpack": "^5.39.0",
|
||||
"webpack-cli": "^4.7.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
|
||||
module.exports = {
|
||||
entry: './lib/index.js',
|
||||
target: 'node',
|
||||
output: {
|
||||
path: require('path').join(__dirname, '../dist'),
|
||||
filename: 'server.js'
|
||||
},
|
||||
mode: 'production'
|
||||
};
|
||||
19
examples/webpack-build-server/webpack.config.js
Normal file
19
examples/webpack-build-server/webpack.config.js
Normal file
@@ -0,0 +1,19 @@
|
||||
const path = require("path");
|
||||
|
||||
module.exports = {
|
||||
entry: "./index.js",
|
||||
target: "node",
|
||||
mode: "production",
|
||||
output: {
|
||||
path: path.resolve(__dirname, "dist"),
|
||||
filename: "index.js",
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
resourceQuery: /raw/,
|
||||
type: "asset/source",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -10,7 +10,8 @@ import type {
|
||||
} from "./typed-events";
|
||||
|
||||
export class BroadcastOperator<EmitEvents extends EventsMap>
|
||||
implements TypedEventBroadcaster<EmitEvents> {
|
||||
implements TypedEventBroadcaster<EmitEvents>
|
||||
{
|
||||
constructor(
|
||||
private readonly adapter: Adapter,
|
||||
private readonly rooms: Set<Room> = new Set<Room>(),
|
||||
@@ -186,7 +187,7 @@ export class BroadcastOperator<EmitEvents extends EventsMap>
|
||||
return sockets.map((socket) => {
|
||||
if (socket instanceof Socket) {
|
||||
// FIXME the TypeScript compiler complains about missing private properties
|
||||
return (socket as unknown) as RemoteSocket<EmitEvents>;
|
||||
return socket as unknown as RemoteSocket<EmitEvents>;
|
||||
} else {
|
||||
return new RemoteSocket(this.adapter, socket as SocketDetails);
|
||||
}
|
||||
@@ -257,7 +258,8 @@ interface SocketDetails {
|
||||
* Expose of subset of the attributes and methods of the Socket class
|
||||
*/
|
||||
export class RemoteSocket<EmitEvents extends EventsMap>
|
||||
implements TypedEventBroadcaster<EmitEvents> {
|
||||
implements TypedEventBroadcaster<EmitEvents>
|
||||
{
|
||||
public readonly id: SocketId;
|
||||
public readonly handshake: Handshake;
|
||||
public readonly rooms: Set<Room>;
|
||||
|
||||
@@ -2,25 +2,47 @@ import { Decoder, Encoder, Packet, PacketType } from "socket.io-parser";
|
||||
import debugModule = require("debug");
|
||||
import url = require("url");
|
||||
import type { IncomingMessage } from "http";
|
||||
import type { Namespace, Server } from "./index";
|
||||
import type { Server } from "./index";
|
||||
import type { Namespace } from "./namespace";
|
||||
import type { EventsMap } from "./typed-events";
|
||||
import type { Socket } from "./socket";
|
||||
import type { SocketId } from "socket.io-adapter";
|
||||
import type { Socket as RawSocket } from "engine.io";
|
||||
|
||||
const debug = debugModule("socket.io:client");
|
||||
|
||||
interface WriteOptions {
|
||||
compress?: boolean;
|
||||
volatile?: boolean;
|
||||
preEncoded?: boolean;
|
||||
wsPreEncoded?: string;
|
||||
}
|
||||
|
||||
export class Client<
|
||||
ListenEvents extends EventsMap,
|
||||
EmitEvents extends EventsMap
|
||||
EmitEvents extends EventsMap,
|
||||
ServerSideEvents extends EventsMap,
|
||||
SocketData = any
|
||||
> {
|
||||
public readonly conn;
|
||||
public readonly conn: RawSocket;
|
||||
|
||||
private readonly id: string;
|
||||
private readonly server: Server<ListenEvents, EmitEvents>;
|
||||
private readonly server: Server<
|
||||
ListenEvents,
|
||||
EmitEvents,
|
||||
ServerSideEvents,
|
||||
SocketData
|
||||
>;
|
||||
private readonly encoder: Encoder;
|
||||
private readonly decoder: Decoder;
|
||||
private sockets: Map<SocketId, Socket<ListenEvents, EmitEvents>> = new Map();
|
||||
private nsps: Map<string, Socket<ListenEvents, EmitEvents>> = new Map();
|
||||
private sockets: Map<
|
||||
SocketId,
|
||||
Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
|
||||
> = new Map();
|
||||
private nsps: Map<
|
||||
string,
|
||||
Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
|
||||
> = new Map();
|
||||
private connectTimeout?: NodeJS.Timeout;
|
||||
|
||||
/**
|
||||
@@ -30,7 +52,10 @@ export class Client<
|
||||
* @param conn
|
||||
* @package
|
||||
*/
|
||||
constructor(server: Server<ListenEvents, EmitEvents>, conn: any) {
|
||||
constructor(
|
||||
server: Server<ListenEvents, EmitEvents, ServerSideEvents, SocketData>,
|
||||
conn: any
|
||||
) {
|
||||
this.server = server;
|
||||
this.conn = conn;
|
||||
this.encoder = server.encoder;
|
||||
@@ -91,9 +116,12 @@ export class Client<
|
||||
this.server._checkNamespace(
|
||||
name,
|
||||
auth,
|
||||
(dynamicNspName: Namespace<ListenEvents, EmitEvents> | false) => {
|
||||
(
|
||||
dynamicNspName:
|
||||
| Namespace<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
|
||||
| false
|
||||
) => {
|
||||
if (dynamicNspName) {
|
||||
debug("dynamic namespace %s was created", dynamicNspName);
|
||||
this.doConnect(name, auth);
|
||||
} else {
|
||||
debug("creation of namespace %s was denied", name);
|
||||
@@ -149,7 +177,9 @@ export class Client<
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_remove(socket: Socket<ListenEvents, EmitEvents>): void {
|
||||
_remove(
|
||||
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
|
||||
): void {
|
||||
if (this.sockets.has(socket.id)) {
|
||||
const nsp = this.sockets.get(socket.id)!.nsp.name;
|
||||
this.sockets.delete(socket.id);
|
||||
@@ -179,30 +209,32 @@ export class Client<
|
||||
* @param {Object} opts
|
||||
* @private
|
||||
*/
|
||||
_packet(packet: Packet, opts?: any): void {
|
||||
opts = opts || {};
|
||||
const self = this;
|
||||
|
||||
// this writes to the actual connection
|
||||
function writeToEngine(encodedPackets: any) {
|
||||
// TODO clarify this.
|
||||
if (opts.volatile && !self.conn.transport.writable) return;
|
||||
for (let i = 0; i < encodedPackets.length; i++) {
|
||||
self.conn.write(encodedPackets[i], { compress: opts.compress });
|
||||
}
|
||||
}
|
||||
|
||||
if ("open" === this.conn.readyState) {
|
||||
debug("writing packet %j", packet);
|
||||
if (!opts.preEncoded) {
|
||||
// not broadcasting, need to encode
|
||||
writeToEngine(this.encoder.encode(packet)); // encode, then write results to engine
|
||||
} else {
|
||||
// a broadcast pre-encodes a packet
|
||||
writeToEngine(packet);
|
||||
}
|
||||
} else {
|
||||
_packet(packet: Packet | any[], opts: WriteOptions = {}): void {
|
||||
if (this.conn.readyState !== "open") {
|
||||
debug("ignoring packet write %j", packet);
|
||||
return;
|
||||
}
|
||||
const encodedPackets = opts.preEncoded
|
||||
? (packet as any[]) // previous versions of the adapter incorrectly used socket.packet() instead of writeToEngine()
|
||||
: this.encoder.encode(packet as Packet);
|
||||
this.writeToEngine(encodedPackets, opts);
|
||||
}
|
||||
|
||||
private writeToEngine(
|
||||
encodedPackets: Array<String | Buffer>,
|
||||
opts: WriteOptions
|
||||
): void {
|
||||
if (opts.volatile && !this.conn.transport.writable) {
|
||||
debug(
|
||||
"volatile packet is discarded since the transport is not currently writable"
|
||||
);
|
||||
return;
|
||||
}
|
||||
const packets = Array.isArray(encodedPackets)
|
||||
? encodedPackets
|
||||
: [encodedPackets];
|
||||
for (const encodedPacket of packets) {
|
||||
this.conn.write(encodedPacket, opts);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
305
lib/index.ts
305
lib/index.ts
@@ -4,143 +4,46 @@ import { createDeflate, createGzip, createBrotliCompress } from "zlib";
|
||||
import accepts = require("accepts");
|
||||
import { pipeline } from "stream";
|
||||
import path = require("path");
|
||||
import engine = require("engine.io");
|
||||
import {
|
||||
attach,
|
||||
Server as Engine,
|
||||
ServerOptions as EngineOptions,
|
||||
AttachOptions,
|
||||
uServer,
|
||||
} from "engine.io";
|
||||
import { Client } from "./client";
|
||||
import { EventEmitter } from "events";
|
||||
import {
|
||||
ExtendedError,
|
||||
Namespace,
|
||||
NamespaceReservedEventsMap,
|
||||
} from "./namespace";
|
||||
import { ExtendedError, Namespace, ServerReservedEventsMap } from "./namespace";
|
||||
import { ParentNamespace } from "./parent-namespace";
|
||||
import { Adapter, Room, SocketId } from "socket.io-adapter";
|
||||
import * as parser from "socket.io-parser";
|
||||
import type { Encoder } from "socket.io-parser";
|
||||
import debugModule from "debug";
|
||||
import { Socket } from "./socket";
|
||||
import type { CookieSerializeOptions } from "cookie";
|
||||
import type { CorsOptions } from "cors";
|
||||
import type { BroadcastOperator, RemoteSocket } from "./broadcast-operator";
|
||||
import {
|
||||
EventsMap,
|
||||
DefaultEventsMap,
|
||||
EventParams,
|
||||
StrictEventEmitter,
|
||||
EventNames,
|
||||
} from "./typed-events";
|
||||
import { patchAdapter, restoreAdapter, serveFile } from "./uws.js";
|
||||
|
||||
const debug = debugModule("socket.io:server");
|
||||
|
||||
const clientVersion = require("../package.json").version;
|
||||
const dotMapRegex = /\.map/;
|
||||
|
||||
type Transport = "polling" | "websocket";
|
||||
type ParentNspNameMatchFn = (
|
||||
name: string,
|
||||
auth: { [key: string]: any },
|
||||
fn: (err: Error | null, success: boolean) => void
|
||||
) => void;
|
||||
|
||||
interface EngineOptions {
|
||||
/**
|
||||
* how many ms without a pong packet to consider the connection closed
|
||||
* @default 20000
|
||||
*/
|
||||
pingTimeout: number;
|
||||
/**
|
||||
* how many ms before sending a new ping packet
|
||||
* @default 25000
|
||||
*/
|
||||
pingInterval: number;
|
||||
/**
|
||||
* how many ms before an uncompleted transport upgrade is cancelled
|
||||
* @default 10000
|
||||
*/
|
||||
upgradeTimeout: number;
|
||||
/**
|
||||
* how many bytes or characters a message can be, before closing the session (to avoid DoS).
|
||||
* @default 1e5 (100 KB)
|
||||
*/
|
||||
maxHttpBufferSize: number;
|
||||
/**
|
||||
* A function that receives a given handshake or upgrade request as its first parameter,
|
||||
* and can decide whether to continue or not. The second argument is a function that needs
|
||||
* to be called with the decided information: fn(err, success), where success is a boolean
|
||||
* value where false means that the request is rejected, and err is an error code.
|
||||
*/
|
||||
allowRequest: (
|
||||
req: http.IncomingMessage,
|
||||
fn: (err: string | null | undefined, success: boolean) => void
|
||||
) => void;
|
||||
/**
|
||||
* the low-level transports that are enabled
|
||||
* @default ["polling", "websocket"]
|
||||
*/
|
||||
transports: Transport[];
|
||||
/**
|
||||
* whether to allow transport upgrades
|
||||
* @default true
|
||||
*/
|
||||
allowUpgrades: boolean;
|
||||
/**
|
||||
* parameters of the WebSocket permessage-deflate extension (see ws module api docs). Set to false to disable.
|
||||
* @default false
|
||||
*/
|
||||
perMessageDeflate: boolean | object;
|
||||
/**
|
||||
* parameters of the http compression for the polling transports (see zlib api docs). Set to false to disable.
|
||||
* @default true
|
||||
*/
|
||||
httpCompression: boolean | object;
|
||||
/**
|
||||
* what WebSocket server implementation to use. Specified module must
|
||||
* conform to the ws interface (see ws module api docs).
|
||||
* An alternative c++ addon is also available by installing eiows module.
|
||||
*
|
||||
* @default `require("ws").Server`
|
||||
*/
|
||||
wsEngine: Function;
|
||||
/**
|
||||
* an optional packet which will be concatenated to the handshake packet emitted by Engine.IO.
|
||||
*/
|
||||
initialPacket: any;
|
||||
/**
|
||||
* configuration of the cookie that contains the client sid to send as part of handshake response headers. This cookie
|
||||
* might be used for sticky-session. Defaults to not sending any cookie.
|
||||
* @default false
|
||||
*/
|
||||
cookie: CookieSerializeOptions | boolean;
|
||||
/**
|
||||
* the options that will be forwarded to the cors module
|
||||
*/
|
||||
cors: CorsOptions;
|
||||
/**
|
||||
* whether to enable compatibility with Socket.IO v2 clients
|
||||
* @default false
|
||||
*/
|
||||
allowEIO3: boolean;
|
||||
}
|
||||
type AdapterConstructor = typeof Adapter | ((nsp: Namespace) => Adapter);
|
||||
|
||||
interface AttachOptions {
|
||||
/**
|
||||
* name of the path to capture
|
||||
* @default "/engine.io"
|
||||
*/
|
||||
path: string;
|
||||
/**
|
||||
* destroy unhandled upgrade requests
|
||||
* @default true
|
||||
*/
|
||||
destroyUpgrade: boolean;
|
||||
/**
|
||||
* milliseconds after which unhandled requests are ended
|
||||
* @default 1000
|
||||
*/
|
||||
destroyUpgradeTimeout: number;
|
||||
}
|
||||
|
||||
interface EngineAttachOptions extends EngineOptions, AttachOptions {}
|
||||
|
||||
interface ServerOptions extends EngineAttachOptions {
|
||||
interface ServerOptions extends EngineOptions, AttachOptions {
|
||||
/**
|
||||
* name of the path to capture
|
||||
* @default "/socket.io"
|
||||
@@ -155,7 +58,7 @@ interface ServerOptions extends EngineAttachOptions {
|
||||
* the adapter to use
|
||||
* @default the in-memory adapter (https://github.com/socketio/socket.io-adapter)
|
||||
*/
|
||||
adapter: any;
|
||||
adapter: AdapterConstructor;
|
||||
/**
|
||||
* the parser to use
|
||||
* @default the default parser (https://github.com/socketio/socket.io-parser)
|
||||
@@ -170,13 +73,36 @@ interface ServerOptions extends EngineAttachOptions {
|
||||
|
||||
export class Server<
|
||||
ListenEvents extends EventsMap = DefaultEventsMap,
|
||||
EmitEvents extends EventsMap = ListenEvents
|
||||
EmitEvents extends EventsMap = ListenEvents,
|
||||
ServerSideEvents extends EventsMap = DefaultEventsMap,
|
||||
SocketData = any
|
||||
> extends StrictEventEmitter<
|
||||
{},
|
||||
ServerSideEvents,
|
||||
EmitEvents,
|
||||
NamespaceReservedEventsMap<ListenEvents, EmitEvents>
|
||||
ServerReservedEventsMap<
|
||||
ListenEvents,
|
||||
EmitEvents,
|
||||
ServerSideEvents,
|
||||
SocketData
|
||||
>
|
||||
> {
|
||||
public readonly sockets: Namespace<ListenEvents, EmitEvents>;
|
||||
public readonly sockets: Namespace<
|
||||
ListenEvents,
|
||||
EmitEvents,
|
||||
ServerSideEvents,
|
||||
SocketData
|
||||
>;
|
||||
/**
|
||||
* A reference to the underlying Engine.IO server.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* <code>
|
||||
* const clientsCount = io.engine.clientsCount;
|
||||
* </code>
|
||||
*
|
||||
*/
|
||||
public engine: any;
|
||||
|
||||
/** @private */
|
||||
readonly _parser: typeof parser;
|
||||
@@ -186,16 +112,16 @@ export class Server<
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_nsps: Map<string, Namespace<ListenEvents, EmitEvents>> = new Map();
|
||||
_nsps: Map<string, Namespace<ListenEvents, EmitEvents, ServerSideEvents>> =
|
||||
new Map();
|
||||
private parentNsps: Map<
|
||||
ParentNspNameMatchFn,
|
||||
ParentNamespace<ListenEvents, EmitEvents>
|
||||
ParentNamespace<ListenEvents, EmitEvents, ServerSideEvents>
|
||||
> = new Map();
|
||||
private _adapter?: typeof Adapter;
|
||||
private _adapter?: AdapterConstructor;
|
||||
private _serveClient: boolean;
|
||||
private opts: Partial<EngineOptions>;
|
||||
private eio;
|
||||
private engine;
|
||||
private eio: Engine;
|
||||
private _path: string;
|
||||
private clientPathRegex: RegExp;
|
||||
|
||||
@@ -239,7 +165,7 @@ export class Server<
|
||||
this.adapter(opts.adapter || Adapter);
|
||||
this.sockets = this.of("/");
|
||||
this.opts = opts;
|
||||
if (srv) this.attach(srv as http.Server);
|
||||
if (srv || typeof srv == "number") this.attach(srv as http.Server | number);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -270,7 +196,9 @@ export class Server<
|
||||
_checkNamespace(
|
||||
name: string,
|
||||
auth: { [key: string]: any },
|
||||
fn: (nsp: Namespace<ListenEvents, EmitEvents> | false) => void
|
||||
fn: (
|
||||
nsp: Namespace<ListenEvents, EmitEvents, ServerSideEvents> | false
|
||||
) => void
|
||||
): void {
|
||||
if (this.parentNsps.size === 0) return fn(false);
|
||||
|
||||
@@ -283,10 +211,18 @@ export class Server<
|
||||
}
|
||||
nextFn.value(name, auth, (err, allow) => {
|
||||
if (err || !allow) {
|
||||
run();
|
||||
} else {
|
||||
fn(this.parentNsps.get(nextFn.value)!.createChild(name));
|
||||
return run();
|
||||
}
|
||||
if (this._nsps.has(name)) {
|
||||
// the namespace was created in the meantime
|
||||
debug("dynamic namespace %s already exists", name);
|
||||
return fn(this._nsps.get(name) as Namespace);
|
||||
}
|
||||
const namespace = this.parentNsps.get(nextFn.value)!.createChild(name);
|
||||
debug("dynamic namespace %s was created", name);
|
||||
// @ts-ignore
|
||||
this.sockets.emitReserved("new_namespace", namespace);
|
||||
fn(namespace);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -312,7 +248,7 @@ export class Server<
|
||||
this.clientPathRegex = new RegExp(
|
||||
"^" +
|
||||
escapedPath +
|
||||
"/socket\\.io(\\.min|\\.msgpack\\.min)?\\.js(\\.map)?$"
|
||||
"/socket\\.io(\\.msgpack|\\.esm)?(\\.min)?\\.js(\\.map)?(?:\\?|$)"
|
||||
);
|
||||
return this;
|
||||
}
|
||||
@@ -338,10 +274,11 @@ export class Server<
|
||||
* @return self when setting or value when getting
|
||||
* @public
|
||||
*/
|
||||
public adapter(): typeof Adapter | undefined;
|
||||
public adapter(v: typeof Adapter): this;
|
||||
public adapter(v?: typeof Adapter): typeof Adapter | undefined | this;
|
||||
public adapter(v?: typeof Adapter): typeof Adapter | undefined | this {
|
||||
public adapter(): AdapterConstructor | undefined;
|
||||
public adapter(v: AdapterConstructor): this;
|
||||
public adapter(
|
||||
v?: AdapterConstructor
|
||||
): AdapterConstructor | undefined | this {
|
||||
if (!arguments.length) return this._adapter;
|
||||
this._adapter = v;
|
||||
for (const nsp of this._nsps.values()) {
|
||||
@@ -409,6 +346,69 @@ export class Server<
|
||||
return this;
|
||||
}
|
||||
|
||||
public attachApp(app /*: TemplatedApp */, opts: Partial<ServerOptions> = {}) {
|
||||
// merge the options passed to the Socket.IO server
|
||||
Object.assign(opts, this.opts);
|
||||
// set engine.io path to `/socket.io`
|
||||
opts.path = opts.path || this._path;
|
||||
|
||||
// initialize engine
|
||||
debug("creating uWebSockets.js-based engine with opts %j", opts);
|
||||
const engine = new uServer(opts);
|
||||
|
||||
engine.attach(app, opts);
|
||||
|
||||
// bind to engine events
|
||||
this.bind(engine);
|
||||
|
||||
if (this._serveClient) {
|
||||
// attach static file serving
|
||||
app.get(`${this._path}/*`, (res, req) => {
|
||||
if (!this.clientPathRegex.test(req.getUrl())) {
|
||||
req.setYield(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const filename = req
|
||||
.getUrl()
|
||||
.replace(this._path, "")
|
||||
.replace(/\?.*$/, "")
|
||||
.replace(/^\//, "");
|
||||
const isMap = dotMapRegex.test(filename);
|
||||
const type = isMap ? "map" : "source";
|
||||
|
||||
// Per the standard, ETags must be quoted:
|
||||
// https://tools.ietf.org/html/rfc7232#section-2.3
|
||||
const expectedEtag = '"' + clientVersion + '"';
|
||||
const weakEtag = "W/" + expectedEtag;
|
||||
|
||||
const etag = req.getHeader("if-none-match");
|
||||
if (etag) {
|
||||
if (expectedEtag === etag || weakEtag === etag) {
|
||||
debug("serve client %s 304", type);
|
||||
res.writeStatus("304 Not Modified");
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
debug("serve client %s", type);
|
||||
|
||||
res.writeHeader("cache-control", "public, max-age=0");
|
||||
res.writeHeader(
|
||||
"content-type",
|
||||
"application/" + (isMap ? "json" : "javascript")
|
||||
);
|
||||
res.writeHeader("etag", expectedEtag);
|
||||
|
||||
const filepath = path.join(__dirname, "../client-dist/", filename);
|
||||
serveFile(res, filepath);
|
||||
});
|
||||
}
|
||||
|
||||
patchAdapter(app);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize engine
|
||||
*
|
||||
@@ -418,11 +418,11 @@ export class Server<
|
||||
*/
|
||||
private initEngine(
|
||||
srv: http.Server,
|
||||
opts: Partial<EngineAttachOptions>
|
||||
opts: EngineOptions & AttachOptions
|
||||
): void {
|
||||
// initialize engine
|
||||
debug("creating engine.io instance with opts %j", opts);
|
||||
this.eio = engine.attach(srv, opts);
|
||||
this.eio = attach(srv, opts);
|
||||
|
||||
// attach static file serving
|
||||
if (this._serveClient) this.attachServe(srv);
|
||||
@@ -446,7 +446,7 @@ export class Server<
|
||||
const evs = srv.listeners("request").slice(0);
|
||||
srv.removeAllListeners("request");
|
||||
srv.on("request", (req, res) => {
|
||||
if (this.clientPathRegex.test(req.url)) {
|
||||
if (this.clientPathRegex.test(req.url!)) {
|
||||
this.serve(req, res);
|
||||
} else {
|
||||
for (let i = 0; i < evs.length; i++) {
|
||||
@@ -464,7 +464,7 @@ export class Server<
|
||||
* @private
|
||||
*/
|
||||
private serve(req: http.IncomingMessage, res: http.ServerResponse): void {
|
||||
const filename = req.url!.replace(this._path, "");
|
||||
const filename = req.url!.replace(this._path, "").replace(/\?.*$/, "");
|
||||
const isMap = dotMapRegex.test(filename);
|
||||
const type = isMap ? "map" : "source";
|
||||
|
||||
@@ -492,9 +492,6 @@ export class Server<
|
||||
);
|
||||
res.setHeader("ETag", expectedEtag);
|
||||
|
||||
if (!isMap) {
|
||||
res.setHeader("X-SourceMap", filename.substring(1) + ".map");
|
||||
}
|
||||
Server.sendFile(filename, req, res);
|
||||
}
|
||||
|
||||
@@ -579,8 +576,10 @@ export class Server<
|
||||
*/
|
||||
public of(
|
||||
name: string | RegExp | ParentNspNameMatchFn,
|
||||
fn?: (socket: Socket<ListenEvents, EmitEvents>) => void
|
||||
): Namespace<ListenEvents, EmitEvents> {
|
||||
fn?: (
|
||||
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
|
||||
) => void
|
||||
): Namespace<ListenEvents, EmitEvents, ServerSideEvents> {
|
||||
if (typeof name === "function" || name instanceof RegExp) {
|
||||
const parentNsp = new ParentNamespace(this);
|
||||
debug("initializing parent namespace %s", parentNsp.name);
|
||||
@@ -606,6 +605,10 @@ export class Server<
|
||||
debug("initializing namespace %s", name);
|
||||
nsp = new Namespace(this, name);
|
||||
this._nsps.set(name, nsp);
|
||||
if (name !== "/") {
|
||||
// @ts-ignore
|
||||
this.sockets.emitReserved("new_namespace", nsp);
|
||||
}
|
||||
}
|
||||
if (fn) nsp.on("connect", fn);
|
||||
return nsp;
|
||||
@@ -624,6 +627,9 @@ export class Server<
|
||||
|
||||
this.engine.close();
|
||||
|
||||
// restore the Adapter prototype
|
||||
restoreAdapter();
|
||||
|
||||
if (this.httpServer) {
|
||||
this.httpServer.close(fn);
|
||||
} else {
|
||||
@@ -639,7 +645,7 @@ export class Server<
|
||||
*/
|
||||
public use(
|
||||
fn: (
|
||||
socket: Socket<ListenEvents, EmitEvents>,
|
||||
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>,
|
||||
next: (err?: ExtendedError) => void
|
||||
) => void
|
||||
): this {
|
||||
@@ -676,9 +682,8 @@ export class Server<
|
||||
* @return self
|
||||
* @public
|
||||
*/
|
||||
public except(name: Room | Room[]): Server<ListenEvents, EmitEvents> {
|
||||
this.sockets.except(name);
|
||||
return this;
|
||||
public except(name: Room | Room[]): BroadcastOperator<EmitEvents> {
|
||||
return this.sockets.except(name);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -703,6 +708,20 @@ export class Server<
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit a packet to other Socket.IO servers
|
||||
*
|
||||
* @param ev - the event name
|
||||
* @param args - an array of arguments, which may include an acknowledgement callback at the end
|
||||
* @public
|
||||
*/
|
||||
public serverSideEmit<Ev extends EventNames<ServerSideEvents>>(
|
||||
ev: Ev,
|
||||
...args: EventParams<ServerSideEvents, Ev>
|
||||
): boolean {
|
||||
return this.sockets.serverSideEmit(ev, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of socket ids.
|
||||
*
|
||||
@@ -803,5 +822,7 @@ emitterMethods.forEach(function (fn) {
|
||||
|
||||
module.exports = (srv?, opts?) => new Server(srv, opts);
|
||||
module.exports.Server = Server;
|
||||
module.exports.Namespace = Namespace;
|
||||
module.exports.Socket = Socket;
|
||||
|
||||
export { Socket, ServerOptions, Namespace, BroadcastOperator, RemoteSocket };
|
||||
|
||||
104
lib/namespace.ts
104
lib/namespace.ts
@@ -20,35 +20,73 @@ export interface ExtendedError extends Error {
|
||||
|
||||
export interface NamespaceReservedEventsMap<
|
||||
ListenEvents extends EventsMap,
|
||||
EmitEvents extends EventsMap
|
||||
EmitEvents extends EventsMap,
|
||||
ServerSideEvents extends EventsMap,
|
||||
SocketData
|
||||
> {
|
||||
connect: (socket: Socket<ListenEvents, EmitEvents>) => void;
|
||||
connection: (socket: Socket<ListenEvents, EmitEvents>) => void;
|
||||
connect: (
|
||||
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
|
||||
) => void;
|
||||
connection: (
|
||||
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
|
||||
) => void;
|
||||
}
|
||||
|
||||
export interface ServerReservedEventsMap<
|
||||
ListenEvents,
|
||||
EmitEvents,
|
||||
ServerSideEvents,
|
||||
SocketData
|
||||
> extends NamespaceReservedEventsMap<
|
||||
ListenEvents,
|
||||
EmitEvents,
|
||||
ServerSideEvents,
|
||||
SocketData
|
||||
> {
|
||||
new_namespace: (
|
||||
namespace: Namespace<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
|
||||
) => void;
|
||||
}
|
||||
|
||||
export const RESERVED_EVENTS: ReadonlySet<string | Symbol> = new Set<
|
||||
keyof ServerReservedEventsMap<never, never, never, never>
|
||||
>(<const>["connect", "connection", "new_namespace"]);
|
||||
|
||||
export class Namespace<
|
||||
ListenEvents extends EventsMap = DefaultEventsMap,
|
||||
EmitEvents extends EventsMap = ListenEvents
|
||||
EmitEvents extends EventsMap = ListenEvents,
|
||||
ServerSideEvents extends EventsMap = DefaultEventsMap,
|
||||
SocketData = any
|
||||
> extends StrictEventEmitter<
|
||||
{},
|
||||
ServerSideEvents,
|
||||
EmitEvents,
|
||||
NamespaceReservedEventsMap<ListenEvents, EmitEvents>
|
||||
NamespaceReservedEventsMap<
|
||||
ListenEvents,
|
||||
EmitEvents,
|
||||
ServerSideEvents,
|
||||
SocketData
|
||||
>
|
||||
> {
|
||||
public readonly name: string;
|
||||
public readonly sockets: Map<
|
||||
SocketId,
|
||||
Socket<ListenEvents, EmitEvents>
|
||||
Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
|
||||
> = new Map();
|
||||
|
||||
public adapter: Adapter;
|
||||
|
||||
/** @private */
|
||||
readonly server: Server<ListenEvents, EmitEvents>;
|
||||
readonly server: Server<
|
||||
ListenEvents,
|
||||
EmitEvents,
|
||||
ServerSideEvents,
|
||||
SocketData
|
||||
>;
|
||||
|
||||
/** @private */
|
||||
_fns: Array<
|
||||
(
|
||||
socket: Socket<ListenEvents, EmitEvents>,
|
||||
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>,
|
||||
next: (err?: ExtendedError) => void
|
||||
) => void
|
||||
> = [];
|
||||
@@ -62,7 +100,10 @@ export class Namespace<
|
||||
* @param server instance
|
||||
* @param name
|
||||
*/
|
||||
constructor(server: Server<ListenEvents, EmitEvents>, name: string) {
|
||||
constructor(
|
||||
server: Server<ListenEvents, EmitEvents, ServerSideEvents, SocketData>,
|
||||
name: string
|
||||
) {
|
||||
super();
|
||||
this.server = server;
|
||||
this.name = name;
|
||||
@@ -77,6 +118,7 @@ export class Namespace<
|
||||
* @private
|
||||
*/
|
||||
_initAdapter(): void {
|
||||
// @ts-ignore
|
||||
this.adapter = new (this.server.adapter()!)(this);
|
||||
}
|
||||
|
||||
@@ -88,7 +130,7 @@ export class Namespace<
|
||||
*/
|
||||
public use(
|
||||
fn: (
|
||||
socket: Socket<ListenEvents, EmitEvents>,
|
||||
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>,
|
||||
next: (err?: ExtendedError) => void
|
||||
) => void
|
||||
): this {
|
||||
@@ -104,7 +146,7 @@ export class Namespace<
|
||||
* @private
|
||||
*/
|
||||
private run(
|
||||
socket: Socket<ListenEvents, EmitEvents>,
|
||||
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>,
|
||||
fn: (err: ExtendedError | null) => void
|
||||
) {
|
||||
const fns = this._fns.slice(0);
|
||||
@@ -166,10 +208,10 @@ export class Namespace<
|
||||
* @private
|
||||
*/
|
||||
_add(
|
||||
client: Client<ListenEvents, EmitEvents>,
|
||||
client: Client<ListenEvents, EmitEvents, ServerSideEvents>,
|
||||
query,
|
||||
fn?: () => void
|
||||
): Socket<ListenEvents, EmitEvents> {
|
||||
): Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData> {
|
||||
debug("adding socket to nsp %s", this.name);
|
||||
const socket = new Socket(this, client, query);
|
||||
this.run(socket, (err) => {
|
||||
@@ -212,7 +254,9 @@ export class Namespace<
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_remove(socket: Socket<ListenEvents, EmitEvents>): void {
|
||||
_remove(
|
||||
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
|
||||
): void {
|
||||
if (this.sockets.has(socket.id)) {
|
||||
this.sockets.delete(socket.id);
|
||||
} else {
|
||||
@@ -255,6 +299,36 @@ export class Namespace<
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit a packet to other Socket.IO servers
|
||||
*
|
||||
* @param ev - the event name
|
||||
* @param args - an array of arguments, which may include an acknowledgement callback at the end
|
||||
* @public
|
||||
*/
|
||||
public serverSideEmit<Ev extends EventNames<ServerSideEvents>>(
|
||||
ev: Ev,
|
||||
...args: EventParams<ServerSideEvents, Ev>
|
||||
): boolean {
|
||||
if (RESERVED_EVENTS.has(ev)) {
|
||||
throw new Error(`"${ev}" is a reserved event name`);
|
||||
}
|
||||
args.unshift(ev);
|
||||
this.adapter.serverSideEmit(args);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a packet is received from another Socket.IO server
|
||||
*
|
||||
* @param args - an array of arguments, which may include an acknowledgement callback at the end
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_onServerSideEmit(args: [string, ...any[]]) {
|
||||
super.emitUntyped.apply(this, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of clients.
|
||||
*
|
||||
|
||||
@@ -10,12 +10,18 @@ import type { BroadcastOptions } from "socket.io-adapter";
|
||||
|
||||
export class ParentNamespace<
|
||||
ListenEvents extends EventsMap = DefaultEventsMap,
|
||||
EmitEvents extends EventsMap = ListenEvents
|
||||
> extends Namespace<ListenEvents, EmitEvents> {
|
||||
EmitEvents extends EventsMap = ListenEvents,
|
||||
ServerSideEvents extends EventsMap = DefaultEventsMap,
|
||||
SocketData = any
|
||||
> extends Namespace<ListenEvents, EmitEvents, ServerSideEvents, SocketData> {
|
||||
private static count: number = 0;
|
||||
private children: Set<Namespace<ListenEvents, EmitEvents>> = new Set();
|
||||
private children: Set<
|
||||
Namespace<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
|
||||
> = new Set();
|
||||
|
||||
constructor(server: Server<ListenEvents, EmitEvents>) {
|
||||
constructor(
|
||||
server: Server<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
|
||||
) {
|
||||
super(server, "/_" + ParentNamespace.count++);
|
||||
}
|
||||
|
||||
@@ -43,7 +49,9 @@ export class ParentNamespace<
|
||||
return true;
|
||||
}
|
||||
|
||||
createChild(name: string): Namespace<ListenEvents, EmitEvents> {
|
||||
createChild(
|
||||
name: string
|
||||
): Namespace<ListenEvents, EmitEvents, ServerSideEvents, SocketData> {
|
||||
const namespace = new Namespace(this.server, name);
|
||||
namespace._fns = this._fns.slice(0);
|
||||
this.listeners("connect").forEach((listener) =>
|
||||
|
||||
106
lib/socket.ts
106
lib/socket.ts
@@ -1,5 +1,4 @@
|
||||
import { Packet, PacketType } from "socket.io-parser";
|
||||
import url = require("url");
|
||||
import debugModule from "debug";
|
||||
import type { Server } from "./index";
|
||||
import {
|
||||
@@ -46,7 +45,7 @@ export interface EventEmitterReservedEventsMap {
|
||||
|
||||
export const RESERVED_EVENTS: ReadonlySet<string | Symbol> = new Set<
|
||||
| ClientReservedEvents
|
||||
| keyof NamespaceReservedEventsMap<never, never>
|
||||
| keyof NamespaceReservedEventsMap<never, never, never, never>
|
||||
| keyof SocketReservedEventsMap
|
||||
| keyof EventEmitterReservedEventsMap
|
||||
>(<const>[
|
||||
@@ -108,9 +107,13 @@ export interface Handshake {
|
||||
auth: { [key: string]: any };
|
||||
}
|
||||
|
||||
type Event = [eventName: string, ...args: any[]];
|
||||
|
||||
export class Socket<
|
||||
ListenEvents extends EventsMap = DefaultEventsMap,
|
||||
EmitEvents extends EventsMap = ListenEvents
|
||||
EmitEvents extends EventsMap = ListenEvents,
|
||||
ServerSideEvents extends EventsMap = DefaultEventsMap,
|
||||
SocketData = any
|
||||
> extends StrictEventEmitter<
|
||||
ListenEvents,
|
||||
EmitEvents,
|
||||
@@ -121,18 +124,20 @@ export class Socket<
|
||||
/**
|
||||
* Additional information that can be attached to the Socket instance and which will be used in the fetchSockets method
|
||||
*/
|
||||
public data: any = {};
|
||||
public data: Partial<SocketData> = {};
|
||||
|
||||
public connected: boolean;
|
||||
public disconnected: boolean;
|
||||
public connected: boolean = false;
|
||||
|
||||
private readonly server: Server<ListenEvents, EmitEvents>;
|
||||
private readonly server: Server<
|
||||
ListenEvents,
|
||||
EmitEvents,
|
||||
ServerSideEvents,
|
||||
SocketData
|
||||
>;
|
||||
private readonly adapter: Adapter;
|
||||
private acks: Map<number, () => void> = new Map();
|
||||
private fns: Array<
|
||||
(event: Array<any>, next: (err?: Error) => void) => void
|
||||
> = [];
|
||||
private flags: BroadcastFlags = {};
|
||||
private fns: Array<(event: Event, next: (err?: Error) => void) => void> = [];
|
||||
private flags: BroadcastFlags & { timeout?: number } = {};
|
||||
private _anyListeners?: Array<(...args: any[]) => void>;
|
||||
|
||||
/**
|
||||
@@ -144,8 +149,8 @@ export class Socket<
|
||||
* @package
|
||||
*/
|
||||
constructor(
|
||||
readonly nsp: Namespace<ListenEvents, EmitEvents>,
|
||||
readonly client: Client<ListenEvents, EmitEvents>,
|
||||
readonly nsp: Namespace<ListenEvents, EmitEvents, ServerSideEvents>,
|
||||
readonly client: Client<ListenEvents, EmitEvents, ServerSideEvents>,
|
||||
auth: object
|
||||
) {
|
||||
super();
|
||||
@@ -157,8 +162,6 @@ export class Socket<
|
||||
} else {
|
||||
this.id = base64id.generateId(); // don't reuse the Engine.IO id because it's sensitive information
|
||||
}
|
||||
this.connected = true;
|
||||
this.disconnected = false;
|
||||
this.handshake = this.buildHandshake(auth);
|
||||
}
|
||||
|
||||
@@ -177,7 +180,8 @@ export class Socket<
|
||||
secure: !!this.request.connection.encrypted,
|
||||
issued: +new Date(),
|
||||
url: this.request.url!,
|
||||
query: url.parse(this.request.url!, true).query,
|
||||
// @ts-ignore
|
||||
query: this.request._query,
|
||||
auth,
|
||||
};
|
||||
}
|
||||
@@ -203,9 +207,11 @@ export class Socket<
|
||||
|
||||
// access last argument to see if it's an ACK callback
|
||||
if (typeof data[data.length - 1] === "function") {
|
||||
debug("emitting packet with ack id %d", this.nsp._ids);
|
||||
this.acks.set(this.nsp._ids, data.pop());
|
||||
packet.id = this.nsp._ids++;
|
||||
const id = this.nsp._ids++;
|
||||
debug("emitting packet with ack id %d", id);
|
||||
|
||||
this.registerAckCallback(id, data.pop());
|
||||
packet.id = id;
|
||||
}
|
||||
|
||||
const flags = Object.assign({}, this.flags);
|
||||
@@ -216,6 +222,28 @@ export class Socket<
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
private registerAckCallback(id: number, ack: (...args: any[]) => void): void {
|
||||
const timeout = this.flags.timeout;
|
||||
if (timeout === undefined) {
|
||||
this.acks.set(id, ack);
|
||||
return;
|
||||
}
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
debug("event with ack id %d has timed out after %d ms", id, timeout);
|
||||
this.acks.delete(id);
|
||||
ack.call(this, new Error("operation has timed out"));
|
||||
}, timeout);
|
||||
|
||||
this.acks.set(id, (...args) => {
|
||||
clearTimeout(timer);
|
||||
ack.apply(this, [null, ...args]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Targets a room when broadcasting.
|
||||
*
|
||||
@@ -335,6 +363,7 @@ export class Socket<
|
||||
*/
|
||||
_onconnect(): void {
|
||||
debug("socket connected - writing packet");
|
||||
this.connected = true;
|
||||
this.join(this.id);
|
||||
if (this.conn.protocol === 3) {
|
||||
this.packet({ type: PacketType.CONNECT });
|
||||
@@ -482,7 +511,6 @@ export class Socket<
|
||||
this.nsp._remove(this);
|
||||
this.client._remove(this);
|
||||
this.connected = false;
|
||||
this.disconnected = true;
|
||||
this.emitReserved("disconnect", reason);
|
||||
return;
|
||||
}
|
||||
@@ -563,13 +591,33 @@ export class Socket<
|
||||
return this.newBroadcastOperator().local;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a modifier for a subsequent event emission that the callback will be called with an error when the
|
||||
* given number of milliseconds have elapsed without an acknowledgement from the client:
|
||||
*
|
||||
* ```
|
||||
* socket.timeout(5000).emit("my-event", (err) => {
|
||||
* if (err) {
|
||||
* // the client did not acknowledge the event in the given delay
|
||||
* }
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @returns self
|
||||
* @public
|
||||
*/
|
||||
public timeout(timeout: number): this {
|
||||
this.flags.timeout = timeout;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch incoming event to socket listeners.
|
||||
*
|
||||
* @param {Array} event - event that will get emitted
|
||||
* @private
|
||||
*/
|
||||
private dispatch(event: [eventName: string, ...args: any[]]): void {
|
||||
private dispatch(event: Event): void {
|
||||
debug("dispatching an event %j", event);
|
||||
this.run(event, (err) => {
|
||||
process.nextTick(() => {
|
||||
@@ -592,9 +640,7 @@ export class Socket<
|
||||
* @return {Socket} self
|
||||
* @public
|
||||
*/
|
||||
public use(
|
||||
fn: (event: Array<any>, next: (err?: Error) => void) => void
|
||||
): this {
|
||||
public use(fn: (event: Event, next: (err?: Error) => void) => void): this {
|
||||
this.fns.push(fn);
|
||||
return this;
|
||||
}
|
||||
@@ -606,10 +652,7 @@ export class Socket<
|
||||
* @param {Function} fn - last fn call in the middleware
|
||||
* @private
|
||||
*/
|
||||
private run(
|
||||
event: [eventName: string, ...args: any[]],
|
||||
fn: (err: Error | null) => void
|
||||
): void {
|
||||
private run(event: Event, fn: (err: Error | null) => void): void {
|
||||
const fns = this.fns.slice(0);
|
||||
if (!fns.length) return fn(null);
|
||||
|
||||
@@ -629,6 +672,13 @@ export class Socket<
|
||||
run(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the socket is currently disconnected
|
||||
*/
|
||||
public get disconnected() {
|
||||
return !this.connected;
|
||||
}
|
||||
|
||||
/**
|
||||
* A reference to the request that originated the underlying Engine.IO Socket.
|
||||
*
|
||||
|
||||
@@ -58,7 +58,7 @@ export type ReservedOrUserListener<
|
||||
* Needed because of https://github.com/microsoft/TypeScript/issues/41778
|
||||
*/
|
||||
type FallbackToUntypedListener<T> = [T] extends [never]
|
||||
? (...args: any[]) => void
|
||||
? (...args: any[]) => void | Promise<void>
|
||||
: T;
|
||||
|
||||
/**
|
||||
@@ -91,7 +91,8 @@ export abstract class StrictEventEmitter<
|
||||
ReservedEvents extends EventsMap = {}
|
||||
>
|
||||
extends EventEmitter
|
||||
implements TypedEventBroadcaster<EmitEvents> {
|
||||
implements TypedEventBroadcaster<EmitEvents>
|
||||
{
|
||||
/**
|
||||
* Adds the `listener` function as an event listener for `ev`.
|
||||
*
|
||||
|
||||
162
lib/uws.ts
Normal file
162
lib/uws.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import { Adapter, Room } from "socket.io-adapter";
|
||||
import type { WebSocket } from "uWebSockets.js";
|
||||
import type { Socket } from "./socket.js";
|
||||
import { createReadStream, statSync } from "fs";
|
||||
import debugModule from "debug";
|
||||
|
||||
const debug = debugModule("socket.io:adapter-uws");
|
||||
|
||||
const SEPARATOR = "\x1f"; // see https://en.wikipedia.org/wiki/Delimiter#ASCII_delimited_text
|
||||
|
||||
const { addAll, del, broadcast } = Adapter.prototype;
|
||||
|
||||
export function patchAdapter(app /* : TemplatedApp */) {
|
||||
Adapter.prototype.addAll = function (id, rooms) {
|
||||
const isNew = !this.sids.has(id);
|
||||
addAll.call(this, id, rooms);
|
||||
const socket: Socket = this.nsp.sockets.get(id);
|
||||
if (!socket) {
|
||||
return;
|
||||
}
|
||||
if (socket.conn.transport.name === "websocket") {
|
||||
subscribe(this.nsp.name, socket, isNew, rooms);
|
||||
return;
|
||||
}
|
||||
if (isNew) {
|
||||
socket.conn.on("upgrade", () => {
|
||||
const rooms = this.sids.get(id);
|
||||
subscribe(this.nsp.name, socket, isNew, rooms);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Adapter.prototype.del = function (id, room) {
|
||||
del.call(this, id, room);
|
||||
const socket: Socket = this.nsp.sockets.get(id);
|
||||
if (socket && socket.conn.transport.name === "websocket") {
|
||||
// @ts-ignore
|
||||
const sessionId = socket.conn.id;
|
||||
// @ts-ignore
|
||||
const websocket: WebSocket = socket.conn.transport.socket;
|
||||
const topic = `${this.nsp.name}${SEPARATOR}${room}`;
|
||||
debug("unsubscribe connection %s from topic %s", sessionId, topic);
|
||||
websocket.unsubscribe(topic);
|
||||
}
|
||||
};
|
||||
|
||||
Adapter.prototype.broadcast = function (packet, opts) {
|
||||
const useFastPublish = opts.rooms.size <= 1 && opts.except!.size === 0;
|
||||
if (!useFastPublish) {
|
||||
broadcast.call(this, packet, opts);
|
||||
return;
|
||||
}
|
||||
|
||||
const flags = opts.flags || {};
|
||||
const basePacketOpts = {
|
||||
preEncoded: true,
|
||||
volatile: flags.volatile,
|
||||
compress: flags.compress,
|
||||
};
|
||||
|
||||
packet.nsp = this.nsp.name;
|
||||
const encodedPackets = this.encoder.encode(packet);
|
||||
|
||||
const topic =
|
||||
opts.rooms.size === 0
|
||||
? this.nsp.name
|
||||
: `${this.nsp.name}${SEPARATOR}${opts.rooms.keys().next().value}`;
|
||||
debug("fast publish to %s", topic);
|
||||
|
||||
// fast publish for clients connected with WebSocket
|
||||
encodedPackets.forEach((encodedPacket) => {
|
||||
const isBinary = typeof encodedPacket !== "string";
|
||||
// "4" being the message type in the Engine.IO protocol, see https://github.com/socketio/engine.io-protocol
|
||||
app.publish(
|
||||
topic,
|
||||
isBinary ? encodedPacket : "4" + encodedPacket,
|
||||
isBinary
|
||||
);
|
||||
});
|
||||
|
||||
this.apply(opts, (socket) => {
|
||||
if (socket.conn.transport.name !== "websocket") {
|
||||
// classic publish for clients connected with HTTP long-polling
|
||||
socket.client.writeToEngine(encodedPackets, basePacketOpts);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function subscribe(
|
||||
namespaceName: string,
|
||||
socket: Socket,
|
||||
isNew: boolean,
|
||||
rooms: Set<Room>
|
||||
) {
|
||||
// @ts-ignore
|
||||
const sessionId = socket.conn.id;
|
||||
// @ts-ignore
|
||||
const websocket: WebSocket = socket.conn.transport.socket;
|
||||
if (isNew) {
|
||||
debug("subscribe connection %s to topic %s", sessionId, namespaceName);
|
||||
websocket.subscribe(namespaceName);
|
||||
}
|
||||
rooms.forEach((room) => {
|
||||
const topic = `${namespaceName}${SEPARATOR}${room}`; // '#' can be used as wildcard
|
||||
debug("subscribe connection %s to topic %s", sessionId, topic);
|
||||
websocket.subscribe(topic);
|
||||
});
|
||||
}
|
||||
|
||||
export function restoreAdapter() {
|
||||
Adapter.prototype.addAll = addAll;
|
||||
Adapter.prototype.del = del;
|
||||
Adapter.prototype.broadcast = broadcast;
|
||||
}
|
||||
|
||||
const toArrayBuffer = (buffer: Buffer) => {
|
||||
const { buffer: arrayBuffer, byteOffset, byteLength } = buffer;
|
||||
return arrayBuffer.slice(byteOffset, byteOffset + byteLength);
|
||||
};
|
||||
|
||||
// imported from https://github.com/kolodziejczak-sz/uwebsocket-serve
|
||||
export function serveFile(res /* : HttpResponse */, filepath: string) {
|
||||
const { size } = statSync(filepath);
|
||||
const readStream = createReadStream(filepath);
|
||||
const destroyReadStream = () => !readStream.destroyed && readStream.destroy();
|
||||
|
||||
const onError = (error: Error) => {
|
||||
destroyReadStream();
|
||||
throw error;
|
||||
};
|
||||
|
||||
const onDataChunk = (chunk: Buffer) => {
|
||||
const arrayBufferChunk = toArrayBuffer(chunk);
|
||||
|
||||
const lastOffset = res.getWriteOffset();
|
||||
const [ok, done] = res.tryEnd(arrayBufferChunk, size);
|
||||
|
||||
if (!done && !ok) {
|
||||
readStream.pause();
|
||||
|
||||
res.onWritable((offset) => {
|
||||
const [ok, done] = res.tryEnd(
|
||||
arrayBufferChunk.slice(offset - lastOffset),
|
||||
size
|
||||
);
|
||||
|
||||
if (!done && ok) {
|
||||
readStream.resume();
|
||||
}
|
||||
|
||||
return ok;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
res.onAborted(destroyReadStream);
|
||||
readStream
|
||||
.on("data", onDataChunk)
|
||||
.on("error", onError)
|
||||
.on("end", destroyReadStream);
|
||||
}
|
||||
1864
package-lock.json
generated
1864
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
31
package.json
31
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "socket.io",
|
||||
"version": "4.0.1",
|
||||
"version": "4.4.0",
|
||||
"description": "node.js realtime framework server",
|
||||
"keywords": [
|
||||
"realtime",
|
||||
@@ -45,33 +45,28 @@
|
||||
"prepack": "npm run compile"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/cookie": "^0.4.0",
|
||||
"@types/cors": "^2.8.8",
|
||||
"@types/node": ">=10.0.0",
|
||||
"accepts": "~1.3.4",
|
||||
"base64id": "~2.0.0",
|
||||
"debug": "~4.3.1",
|
||||
"engine.io": "~5.0.0",
|
||||
"socket.io-adapter": "~2.2.0",
|
||||
"socket.io-parser": "~4.0.3"
|
||||
"debug": "~4.3.2",
|
||||
"engine.io": "~6.1.0",
|
||||
"socket.io-adapter": "~2.3.3",
|
||||
"socket.io-parser": "~4.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mocha": "^8.0.4",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"eslint": "^7.14.0",
|
||||
"eslint-config-prettier": "^6.11.0",
|
||||
"@types/mocha": "^9.0.0",
|
||||
"expect.js": "0.3.1",
|
||||
"mocha": "^3.5.3",
|
||||
"nyc": "^15.1.0",
|
||||
"prettier": "^2.2.0",
|
||||
"prettier": "^2.3.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"socket.io-client": "4.0.1",
|
||||
"socket.io-client": "4.4.0",
|
||||
"socket.io-client-v2": "npm:socket.io-client@^2.4.0",
|
||||
"superagent": "^6.1.0",
|
||||
"supertest": "^6.0.1",
|
||||
"ts-node": "^9.0.0",
|
||||
"tsd": "^0.14.0",
|
||||
"typescript": "^4.1.2"
|
||||
"supertest": "^6.1.6",
|
||||
"ts-node": "^10.2.1",
|
||||
"tsd": "^0.17.0",
|
||||
"typescript": "^4.4.2",
|
||||
"uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.0.0"
|
||||
},
|
||||
"contributors": [
|
||||
{
|
||||
|
||||
2
test/fixtures/server-close.ts
vendored
2
test/fixtures/server-close.ts
vendored
@@ -3,7 +3,7 @@ const ioc = require("socket.io-client");
|
||||
const io = require("../..")(server);
|
||||
|
||||
const srv = server.listen(() => {
|
||||
const socket = ioc("ws://localhost:" + server.address().port);
|
||||
const socket = ioc.connect("ws://localhost:" + server.address().port);
|
||||
socket.on("connect", () => {
|
||||
io.close();
|
||||
socket.close();
|
||||
|
||||
57
test/socket-timeout.ts
Normal file
57
test/socket-timeout.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { Server } from "..";
|
||||
import { createClient, success } from "./support/util";
|
||||
import expect from "expect.js";
|
||||
|
||||
describe("timeout", () => {
|
||||
it("should timeout if the client does not acknowledge the event", (done) => {
|
||||
const io = new Server(0);
|
||||
const client = createClient(io, "/");
|
||||
|
||||
io.on("connection", (socket) => {
|
||||
socket.timeout(50).emit("unknown", (err) => {
|
||||
expect(err).to.be.an(Error);
|
||||
success(done, io, client);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should timeout if the client does not acknowledge the event in time", (done) => {
|
||||
const io = new Server(0);
|
||||
const client = createClient(io, "/");
|
||||
|
||||
client.on("echo", (arg, cb) => {
|
||||
cb(arg);
|
||||
});
|
||||
|
||||
let count = 0;
|
||||
|
||||
io.on("connection", (socket) => {
|
||||
socket.timeout(0).emit("echo", 42, (err) => {
|
||||
expect(err).to.be.an(Error);
|
||||
count++;
|
||||
});
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
expect(count).to.eql(1);
|
||||
success(done, io, client);
|
||||
}, 200);
|
||||
});
|
||||
|
||||
it("should not timeout if the client does acknowledge the event", (done) => {
|
||||
const io = new Server(0);
|
||||
const client = createClient(io, "/");
|
||||
|
||||
client.on("echo", (arg, cb) => {
|
||||
cb(arg);
|
||||
});
|
||||
|
||||
io.on("connection", (socket) => {
|
||||
socket.timeout(50).emit("echo", 42, (err, value) => {
|
||||
expect(err).to.be(null);
|
||||
expect(value).to.be(42);
|
||||
success(done, io, client);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,9 @@
|
||||
"use strict";
|
||||
import { Server, Socket } from "..";
|
||||
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";
|
||||
|
||||
// This file is run by tsd, not mocha.
|
||||
|
||||
@@ -117,7 +118,9 @@ describe("server", () => {
|
||||
|
||||
it("does not accept arguments of wrong types", (done) => {
|
||||
const srv = createServer();
|
||||
const sio = new Server<BidirectionalEvents>(srv);
|
||||
const sio = new Server<BidirectionalEvents, BidirectionalEvents, {}>(
|
||||
srv
|
||||
);
|
||||
expectError(sio.on("random", (a, b, c) => {}));
|
||||
srv.listen(() => {
|
||||
expectError(sio.on("wrong name", (s) => {}));
|
||||
@@ -229,4 +232,69 @@ describe("server", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("listen and emit event maps", () => {
|
||||
interface ClientToServerEvents {
|
||||
helloFromClient: (message: string) => void;
|
||||
}
|
||||
|
||||
interface ServerToClientEvents {
|
||||
helloFromServer: (message: string, x: number) => void;
|
||||
}
|
||||
|
||||
interface InterServerEvents {
|
||||
helloFromServerToServer: (message: string, x: number) => void;
|
||||
}
|
||||
|
||||
describe("on", () => {
|
||||
it("infers correct types for listener parameters", () => {
|
||||
const srv = createServer();
|
||||
const sio = new Server<
|
||||
ClientToServerEvents,
|
||||
ServerToClientEvents,
|
||||
InterServerEvents
|
||||
>(srv);
|
||||
|
||||
expectType<
|
||||
Server<ClientToServerEvents, ServerToClientEvents, InterServerEvents>
|
||||
>(sio);
|
||||
srv.listen(() => {
|
||||
sio.serverSideEmit("helloFromServerToServer", "hello", 10);
|
||||
sio
|
||||
.of("/test")
|
||||
.serverSideEmit("helloFromServerToServer", "hello", 10);
|
||||
|
||||
sio.on("helloFromServerToServer", (message, x) => {
|
||||
expectType<string>(message);
|
||||
expectType<number>(x);
|
||||
});
|
||||
sio.of("/test").on("helloFromServerToServer", (message, x) => {
|
||||
expectType<string>(message);
|
||||
expectType<number>(x);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("adapter", () => {
|
||||
it("accepts arguments of the correct types", () => {
|
||||
const io = new Server({
|
||||
adapter: (nsp) => new Adapter(nsp),
|
||||
});
|
||||
io.adapter(Adapter);
|
||||
|
||||
class MyCustomAdapter extends Adapter {
|
||||
constructor(nsp, readonly opts) {
|
||||
super(nsp);
|
||||
}
|
||||
}
|
||||
io.adapter((nsp) => new MyCustomAdapter(nsp, { test: "123" }));
|
||||
});
|
||||
|
||||
it("does not accept arguments of wrong types", () => {
|
||||
const io = new Server();
|
||||
expectError(io.adapter((nsp) => "nope"));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use strict";
|
||||
|
||||
import { Server, Socket } from "..";
|
||||
import { Server, Socket, Namespace } from "..";
|
||||
import { createServer } from "http";
|
||||
import fs = require("fs");
|
||||
import { join } from "path";
|
||||
@@ -14,6 +14,9 @@ import { io as ioc, Socket as ClientSocket } from "socket.io-client";
|
||||
|
||||
import "./support/util";
|
||||
import "./utility-methods";
|
||||
import "./uws";
|
||||
|
||||
type callback = (err: Error | null, success: boolean) => void;
|
||||
|
||||
// Creates a socket.io client for the given server
|
||||
function client(srv, nsp?: string | object, opts?: object): ClientSocket {
|
||||
@@ -39,6 +42,11 @@ const waitFor = (emitter, event) => {
|
||||
});
|
||||
};
|
||||
|
||||
const getPort = (io: Server): number => {
|
||||
// @ts-ignore
|
||||
return io.httpServer.address().port;
|
||||
};
|
||||
|
||||
describe("socket.io", () => {
|
||||
it("should be the same version as client", () => {
|
||||
const version = require("../package").version;
|
||||
@@ -59,7 +67,7 @@ describe("socket.io", () => {
|
||||
if (err) return done(err);
|
||||
expect(res.headers["content-type"]).to.be("application/javascript");
|
||||
expect(res.headers.etag).to.be('"' + clientVersion + '"');
|
||||
expect(res.headers["x-sourcemap"]).to.be(filename + ".map");
|
||||
expect(res.headers["x-sourcemap"]).to.be(undefined);
|
||||
expect(res.text).to.match(/engine\.io/);
|
||||
expect(res.status).to.be(200);
|
||||
done();
|
||||
@@ -83,6 +91,10 @@ describe("socket.io", () => {
|
||||
};
|
||||
|
||||
it("should serve client", testSource("socket.io.js"));
|
||||
it(
|
||||
"should serve client with query string",
|
||||
testSource("socket.io.js?buster=" + Date.now())
|
||||
);
|
||||
it("should serve source map", testSourceMap("socket.io.js.map"));
|
||||
it("should serve client (min)", testSource("socket.io.min.js"));
|
||||
|
||||
@@ -116,6 +128,13 @@ describe("socket.io", () => {
|
||||
testSourceMap("socket.io.msgpack.min.js.map")
|
||||
);
|
||||
|
||||
it("should serve the ESM bundle", testSource("socket.io.esm.min.js"));
|
||||
|
||||
it(
|
||||
"should serve the source map for the ESM bundle",
|
||||
testSourceMap("socket.io.esm.min.js.map")
|
||||
);
|
||||
|
||||
it("should handle 304", (done) => {
|
||||
const srv = createServer();
|
||||
new Server(srv);
|
||||
@@ -185,29 +204,17 @@ describe("socket.io", () => {
|
||||
|
||||
describe("port", () => {
|
||||
it("should be bound", (done) => {
|
||||
const sockets = new Server(54010);
|
||||
request("http://localhost:54010")
|
||||
.get("/socket.io/socket.io.js")
|
||||
.expect(200, done);
|
||||
});
|
||||
const io = new Server(0);
|
||||
|
||||
it("should be bound as a string", (done) => {
|
||||
const sockets = new Server(54020);
|
||||
request("http://localhost:54020")
|
||||
request(`http://localhost:${getPort(io)}`)
|
||||
.get("/socket.io/socket.io.js")
|
||||
.expect(200, done);
|
||||
});
|
||||
|
||||
it("with listen", (done) => {
|
||||
const sockets = new Server().listen(54011);
|
||||
request("http://localhost:54011")
|
||||
.get("/socket.io/socket.io.js")
|
||||
.expect(200, done);
|
||||
});
|
||||
const io = new Server().listen(0);
|
||||
|
||||
it("as a string", (done) => {
|
||||
const sockets = new Server().listen(54012);
|
||||
request("http://localhost:54012")
|
||||
request(`http://localhost:${getPort(io)}`)
|
||||
.get("/socket.io/socket.io.js")
|
||||
.expect(200, done);
|
||||
});
|
||||
@@ -218,7 +225,7 @@ describe("socket.io", () => {
|
||||
const request = require("superagent");
|
||||
|
||||
it("should send the Access-Control-Allow-xxx headers on OPTIONS request", (done) => {
|
||||
const sockets = new Server(54013, {
|
||||
const io = new Server(0, {
|
||||
cors: {
|
||||
origin: "http://localhost:54023",
|
||||
methods: ["GET", "POST"],
|
||||
@@ -227,7 +234,7 @@ describe("socket.io", () => {
|
||||
},
|
||||
});
|
||||
request
|
||||
.options("http://localhost:54013/socket.io/default/")
|
||||
.options(`http://localhost:${getPort(io)}/socket.io/default/`)
|
||||
.query({ transport: "polling", EIO: 4 })
|
||||
.set("Origin", "http://localhost:54023")
|
||||
.end((err, res) => {
|
||||
@@ -246,7 +253,7 @@ describe("socket.io", () => {
|
||||
});
|
||||
|
||||
it("should send the Access-Control-Allow-xxx headers on GET request", (done) => {
|
||||
const sockets = new Server(54014, {
|
||||
const io = new Server(0, {
|
||||
cors: {
|
||||
origin: "http://localhost:54024",
|
||||
methods: ["GET", "POST"],
|
||||
@@ -255,7 +262,7 @@ describe("socket.io", () => {
|
||||
},
|
||||
});
|
||||
request
|
||||
.get("http://localhost:54014/socket.io/default/")
|
||||
.get(`http://localhost:${getPort(io)}/socket.io/default/`)
|
||||
.query({ transport: "polling", EIO: 4 })
|
||||
.set("Origin", "http://localhost:54024")
|
||||
.end((err, res) => {
|
||||
@@ -270,12 +277,12 @@ describe("socket.io", () => {
|
||||
});
|
||||
|
||||
it("should allow request if custom function in opts.allowRequest returns true", (done) => {
|
||||
const sockets = new Server(createServer().listen(54022), {
|
||||
const io = new Server(0, {
|
||||
allowRequest: (req, callback) => callback(null, true),
|
||||
});
|
||||
|
||||
request
|
||||
.get("http://localhost:54022/socket.io/default/")
|
||||
.get(`http://localhost:${getPort(io)}/socket.io/default/`)
|
||||
.query({ transport: "polling", EIO: 4 })
|
||||
.end((err, res) => {
|
||||
expect(res.status).to.be(200);
|
||||
@@ -284,11 +291,11 @@ describe("socket.io", () => {
|
||||
});
|
||||
|
||||
it("should disallow request if custom function in opts.allowRequest returns false", (done) => {
|
||||
const sockets = new Server(createServer().listen(54023), {
|
||||
const io = new Server(0, {
|
||||
allowRequest: (req, callback) => callback(null, false),
|
||||
});
|
||||
request
|
||||
.get("http://localhost:54023/socket.io/default/")
|
||||
.get(`http://localhost:${getPort(io)}/socket.io/default/`)
|
||||
.set("origin", "http://foo.example")
|
||||
.query({ transport: "polling", EIO: 4 })
|
||||
.end((err, res) => {
|
||||
@@ -300,22 +307,22 @@ describe("socket.io", () => {
|
||||
|
||||
describe("close", () => {
|
||||
it("should be able to close sio sending a srv", (done) => {
|
||||
const PORT = 54018;
|
||||
const srv = createServer().listen(PORT);
|
||||
const sio = new Server(srv);
|
||||
const httpServer = createServer().listen(0);
|
||||
const io = new Server(httpServer);
|
||||
const port = getPort(io);
|
||||
const net = require("net");
|
||||
const server = net.createServer();
|
||||
|
||||
const clientSocket = client(srv, { reconnection: false });
|
||||
const clientSocket = client(httpServer, { reconnection: false });
|
||||
|
||||
clientSocket.on("disconnect", () => {
|
||||
expect(sio.sockets.sockets.size).to.equal(0);
|
||||
server.listen(PORT);
|
||||
expect(io.sockets.sockets.size).to.equal(0);
|
||||
server.listen(port);
|
||||
});
|
||||
|
||||
clientSocket.on("connect", () => {
|
||||
expect(sio.sockets.sockets.size).to.equal(1);
|
||||
sio.close();
|
||||
expect(io.sockets.sockets.size).to.equal(1);
|
||||
io.close();
|
||||
});
|
||||
|
||||
server.once("listening", () => {
|
||||
@@ -327,30 +334,31 @@ describe("socket.io", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should be able to close sio sending a port", () => {
|
||||
const PORT = 54019;
|
||||
const sio = new Server(PORT);
|
||||
it("should be able to close sio sending a srv", (done) => {
|
||||
const io = new Server(0);
|
||||
const port = getPort(io);
|
||||
const net = require("net");
|
||||
const server = net.createServer();
|
||||
|
||||
const clientSocket = ioc("ws://0.0.0.0:" + PORT, {
|
||||
const clientSocket = ioc("ws://0.0.0.0:" + port, {
|
||||
reconnection: false,
|
||||
});
|
||||
|
||||
clientSocket.on("disconnect", () => {
|
||||
expect(Object.keys(sio._nsps["/"].sockets).length).to.equal(0);
|
||||
server.listen(PORT);
|
||||
expect(io.sockets.sockets.size).to.equal(0);
|
||||
server.listen(port);
|
||||
});
|
||||
|
||||
clientSocket.on("connect", () => {
|
||||
expect(Object.keys(sio._nsps["/"].sockets).length).to.equal(1);
|
||||
sio.close();
|
||||
expect(io.sockets.sockets.size).to.equal(1);
|
||||
io.close();
|
||||
});
|
||||
|
||||
server.once("listening", () => {
|
||||
// PORT should be free
|
||||
server.close((error) => {
|
||||
expect(error).to.be(undefined);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -373,9 +381,6 @@ describe("socket.io", () => {
|
||||
});
|
||||
|
||||
describe("namespaces", () => {
|
||||
const { Socket } = require("../dist/socket");
|
||||
const { Namespace } = require("../dist/namespace");
|
||||
|
||||
it("should be accessible through .sockets", () => {
|
||||
const sio = new Server();
|
||||
expect(sio.sockets).to.be.a(Namespace);
|
||||
@@ -805,6 +810,7 @@ describe("socket.io", () => {
|
||||
srv.listen(() => {
|
||||
const socket = client(srv);
|
||||
|
||||
// @ts-ignore
|
||||
socket.io.engine.write = () => {}; // prevent the client from sending a CONNECT packet
|
||||
|
||||
socket.on("disconnect", () => {
|
||||
@@ -815,30 +821,28 @@ describe("socket.io", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should close a client without namespace", (done) => {
|
||||
it("should exclude a specific socket when emitting", (done) => {
|
||||
const srv = createServer();
|
||||
const sio = new Server(srv, {
|
||||
connectTimeout: 100,
|
||||
});
|
||||
|
||||
sio.use((_, next) => {
|
||||
next(new Error("nope"));
|
||||
});
|
||||
const io = new Server(srv);
|
||||
|
||||
srv.listen(() => {
|
||||
const socket = client(srv);
|
||||
const socket1 = client(srv, "/");
|
||||
const socket2 = client(srv, "/");
|
||||
|
||||
const success = () => {
|
||||
socket.close();
|
||||
sio.close();
|
||||
socket2.on("a", () => {
|
||||
done(new Error("should not happen"));
|
||||
});
|
||||
socket1.on("a", () => {
|
||||
done();
|
||||
};
|
||||
});
|
||||
|
||||
socket.on("disconnect", success);
|
||||
socket2.on("connect", () => {
|
||||
io.except(socket2.id).emit("a");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should exclude a specific socket when emitting", (done) => {
|
||||
it("should exclude a specific socket when emitting (in a namespace)", (done) => {
|
||||
const srv = createServer();
|
||||
const sio = new Server(srv);
|
||||
|
||||
@@ -889,6 +893,17 @@ describe("socket.io", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit an 'new_namespace' event", (done) => {
|
||||
const sio = new Server();
|
||||
|
||||
sio.on("new_namespace", (namespace) => {
|
||||
expect(namespace.name).to.eql("/nsp");
|
||||
done();
|
||||
});
|
||||
|
||||
sio.of("/nsp");
|
||||
});
|
||||
|
||||
describe("dynamic namespaces", () => {
|
||||
it("should allow connections to dynamic namespaces with a regex", (done) => {
|
||||
const srv = createServer();
|
||||
@@ -945,6 +960,64 @@ describe("socket.io", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit an 'new_namespace' event for a dynamic namespace", (done) => {
|
||||
const srv = createServer();
|
||||
const sio = new Server(srv);
|
||||
srv.listen(() => {
|
||||
sio.of(/^\/dynamic-\d+$/);
|
||||
|
||||
sio.on("new_namespace", (namespace) => {
|
||||
expect(namespace.name).to.be("/dynamic-101");
|
||||
|
||||
socket.disconnect();
|
||||
srv.close();
|
||||
done();
|
||||
});
|
||||
|
||||
const socket = client(srv, "/dynamic-101");
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle race conditions with dynamic namespaces (#4136)", (done) => {
|
||||
const srv = createServer();
|
||||
const sio = new Server(srv);
|
||||
const counters = {
|
||||
connected: 0,
|
||||
created: 0,
|
||||
events: 0,
|
||||
};
|
||||
const buffer: callback[] = [];
|
||||
sio.on("new_namespace", (namespace) => {
|
||||
counters.created++;
|
||||
});
|
||||
srv.listen(() => {
|
||||
const handler = () => {
|
||||
if (++counters.events === 2) {
|
||||
expect(counters.created).to.equal(1);
|
||||
done();
|
||||
}
|
||||
};
|
||||
|
||||
sio
|
||||
.of((name, query, next) => {
|
||||
buffer.push(next);
|
||||
if (buffer.length === 2) {
|
||||
buffer.forEach((next) => next(null, true));
|
||||
}
|
||||
})
|
||||
.on("connection", (socket) => {
|
||||
if (++counters.connected === 2) {
|
||||
sio.of("/dynamic-101").emit("message");
|
||||
}
|
||||
});
|
||||
|
||||
let one = client(srv, "/dynamic-101");
|
||||
let two = client(srv, "/dynamic-101");
|
||||
one.on("message", handler);
|
||||
two.on("message", handler);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -958,7 +1031,9 @@ describe("socket.io", () => {
|
||||
clientSocket.off("connect", init);
|
||||
clientSocket.io.engine.close();
|
||||
|
||||
clientSocket.connect();
|
||||
process.nextTick(() => {
|
||||
clientSocket.connect();
|
||||
});
|
||||
clientSocket.on("connect", () => {
|
||||
done();
|
||||
});
|
||||
@@ -975,7 +1050,7 @@ describe("socket.io", () => {
|
||||
reconnectionDelay: 100,
|
||||
});
|
||||
clientSocket.on("connect", () => {
|
||||
srv.close();
|
||||
sio.close();
|
||||
});
|
||||
|
||||
clientSocket.io.on("reconnect_failed", () => {
|
||||
@@ -1329,6 +1404,58 @@ describe("socket.io", () => {
|
||||
}, 200);
|
||||
});
|
||||
|
||||
it("should emit only one consecutive volatile event with binary (ws)", (done) => {
|
||||
const srv = createServer();
|
||||
const sio = new Server(srv, { transports: ["websocket"] });
|
||||
|
||||
let counter = 0;
|
||||
srv.listen(() => {
|
||||
sio.on("connection", (s) => {
|
||||
// Wait to make sure there are no packets being sent for opening the connection
|
||||
setTimeout(() => {
|
||||
s.volatile.emit("ev", Buffer.from([1, 2, 3]));
|
||||
s.volatile.emit("ev", Buffer.from([4, 5, 6]));
|
||||
}, 20);
|
||||
});
|
||||
|
||||
const socket = client(srv, { transports: ["websocket"] });
|
||||
socket.on("ev", () => {
|
||||
counter++;
|
||||
});
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
expect(counter).to.be(1);
|
||||
done();
|
||||
}, 200);
|
||||
});
|
||||
|
||||
it("should broadcast only one consecutive volatile event with binary (ws)", (done) => {
|
||||
const srv = createServer();
|
||||
const sio = new Server(srv, { transports: ["websocket"] });
|
||||
|
||||
let counter = 0;
|
||||
srv.listen(() => {
|
||||
sio.on("connection", (s) => {
|
||||
// Wait to make sure there are no packets being sent for opening the connection
|
||||
setTimeout(() => {
|
||||
sio.volatile.emit("ev", Buffer.from([1, 2, 3]));
|
||||
sio.volatile.emit("ev", Buffer.from([4, 5, 6]));
|
||||
}, 20);
|
||||
});
|
||||
|
||||
const socket = client(srv, { transports: ["websocket"] });
|
||||
socket.on("ev", () => {
|
||||
counter++;
|
||||
});
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
expect(counter).to.be(1);
|
||||
done();
|
||||
}, 200);
|
||||
});
|
||||
|
||||
it("should emit regular events after trying a failed volatile event (polling)", (done) => {
|
||||
const srv = createServer();
|
||||
const sio = new Server(srv, { transports: ["polling"] });
|
||||
@@ -1753,6 +1880,7 @@ describe("socket.io", () => {
|
||||
console.log(
|
||||
"\u001b[96mNote: warning expected and normal in test.\u001b[39m"
|
||||
);
|
||||
// @ts-ignore
|
||||
socket.io.engine.write("5woooot");
|
||||
setTimeout(() => {
|
||||
done();
|
||||
@@ -1772,6 +1900,7 @@ describe("socket.io", () => {
|
||||
console.log(
|
||||
"\u001b[96mNote: warning expected and normal in test.\u001b[39m"
|
||||
);
|
||||
// @ts-ignore
|
||||
socket.io.engine.write('44["handle me please"]');
|
||||
setTimeout(() => {
|
||||
done();
|
||||
@@ -1792,6 +1921,7 @@ describe("socket.io", () => {
|
||||
done();
|
||||
});
|
||||
s.conn.on("upgrade", () => {
|
||||
// @ts-ignore
|
||||
socket.io.engine.write("5woooot");
|
||||
});
|
||||
});
|
||||
@@ -1809,6 +1939,7 @@ describe("socket.io", () => {
|
||||
done();
|
||||
});
|
||||
s.conn.on("upgrade", () => {
|
||||
// @ts-ignore
|
||||
socket.io.engine.write("5");
|
||||
});
|
||||
});
|
||||
@@ -2391,8 +2522,6 @@ describe("socket.io", () => {
|
||||
});
|
||||
|
||||
describe("middleware", () => {
|
||||
const { Socket } = require("../dist/socket");
|
||||
|
||||
it("should call functions", (done) => {
|
||||
const srv = createServer();
|
||||
const sio = new Server(srv);
|
||||
@@ -2578,11 +2707,28 @@ describe("socket.io", () => {
|
||||
if (++count === 2) done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should only set `connected` to true after the middleware execution", (done) => {
|
||||
const httpServer = createServer();
|
||||
const io = new Server(httpServer);
|
||||
|
||||
const clientSocket = client(httpServer, "/");
|
||||
|
||||
io.use((socket, next) => {
|
||||
expect(socket.connected).to.be(false);
|
||||
expect(socket.disconnected).to.be(true);
|
||||
next();
|
||||
});
|
||||
|
||||
io.on("connection", (socket) => {
|
||||
expect(socket.connected).to.be(true);
|
||||
expect(socket.disconnected).to.be(false);
|
||||
success(io, clientSocket, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("socket middleware", () => {
|
||||
const { Socket } = require("../dist/socket");
|
||||
|
||||
it("should call functions", (done) => {
|
||||
const srv = createServer();
|
||||
const sio = new Server(srv);
|
||||
@@ -2719,4 +2865,6 @@ describe("socket.io", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
require("./socket-timeout");
|
||||
});
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
import type { Server } from "../..";
|
||||
import {
|
||||
io as ioc,
|
||||
ManagerOptions,
|
||||
Socket as ClientSocket,
|
||||
SocketOptions,
|
||||
} from "socket.io-client";
|
||||
|
||||
const expect = require("expect.js");
|
||||
const i = expect.stringify;
|
||||
|
||||
@@ -20,3 +28,19 @@ expect.Assertion.prototype.contain = function (...args) {
|
||||
}
|
||||
return contain.apply(this, args);
|
||||
};
|
||||
|
||||
export function createClient(
|
||||
io: Server,
|
||||
nsp: string,
|
||||
opts?: ManagerOptions & SocketOptions
|
||||
): ClientSocket {
|
||||
// @ts-ignore
|
||||
const port = io.httpServer.address().port;
|
||||
return ioc(`http://localhost:${port}${nsp}`, opts);
|
||||
}
|
||||
|
||||
export function success(done: Function, io: Server, client: ClientSocket) {
|
||||
io.close();
|
||||
client.disconnect();
|
||||
done();
|
||||
}
|
||||
|
||||
195
test/uws.ts
Normal file
195
test/uws.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
import { App, us_socket_local_port } from "uWebSockets.js";
|
||||
import { Server } from "..";
|
||||
import { io as ioc, Socket as ClientSocket } from "socket.io-client";
|
||||
import request from "supertest";
|
||||
import expect from "expect.js";
|
||||
|
||||
const createPartialDone = (done: (err?: Error) => void, count: number) => {
|
||||
let i = 0;
|
||||
return () => {
|
||||
if (++i === count) {
|
||||
done();
|
||||
} else if (i > count) {
|
||||
done(new Error(`partialDone() called too many times: ${i} > ${count}`));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const shouldNotHappen = (done) => () => done(new Error("should not happen"));
|
||||
|
||||
describe("socket.io with uWebSocket.js-based engine", () => {
|
||||
let io: Server,
|
||||
port: number,
|
||||
client: ClientSocket,
|
||||
clientWSOnly: ClientSocket,
|
||||
clientPollingOnly: ClientSocket,
|
||||
clientCustomNamespace: ClientSocket;
|
||||
|
||||
beforeEach((done) => {
|
||||
const app = App();
|
||||
io = new Server();
|
||||
io.attachApp(app);
|
||||
|
||||
io.of("/custom");
|
||||
|
||||
app.listen(0, (listenSocket) => {
|
||||
port = us_socket_local_port(listenSocket);
|
||||
|
||||
client = ioc(`http://localhost:${port}`);
|
||||
clientWSOnly = ioc(`http://localhost:${port}`, {
|
||||
transports: ["websocket"],
|
||||
});
|
||||
clientPollingOnly = ioc(`http://localhost:${port}`, {
|
||||
transports: ["polling"],
|
||||
});
|
||||
clientCustomNamespace = ioc(`http://localhost:${port}/custom`);
|
||||
});
|
||||
|
||||
const partialDone = createPartialDone(done, 4);
|
||||
io.on("connection", partialDone);
|
||||
io.of("/custom").on("connection", partialDone);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
io.close();
|
||||
client.disconnect();
|
||||
clientWSOnly.disconnect();
|
||||
clientPollingOnly.disconnect();
|
||||
clientCustomNamespace.disconnect();
|
||||
});
|
||||
|
||||
it("should broadcast", (done) => {
|
||||
const partialDone = createPartialDone(done, 3);
|
||||
|
||||
client.on("hello", partialDone);
|
||||
clientWSOnly.on("hello", partialDone);
|
||||
clientPollingOnly.on("hello", partialDone);
|
||||
clientCustomNamespace.on("hello", shouldNotHappen(done));
|
||||
|
||||
io.emit("hello");
|
||||
});
|
||||
|
||||
it("should broadcast in a namespace", (done) => {
|
||||
client.on("hello", shouldNotHappen(done));
|
||||
clientWSOnly.on("hello", shouldNotHappen(done));
|
||||
clientPollingOnly.on("hello", shouldNotHappen(done));
|
||||
clientCustomNamespace.on("hello", done);
|
||||
|
||||
io.of("/custom").emit("hello");
|
||||
});
|
||||
|
||||
it("should broadcast in a dynamic namespace", (done) => {
|
||||
const dynamicNamespace = io.of(/\/dynamic-\d+/);
|
||||
const dynamicClient = clientWSOnly.io.socket("/dynamic-101");
|
||||
|
||||
dynamicClient.on("connect", () => {
|
||||
dynamicNamespace.emit("hello");
|
||||
});
|
||||
|
||||
dynamicClient.on("hello", () => {
|
||||
dynamicClient.disconnect();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should broadcast binary content", (done) => {
|
||||
const partialDone = createPartialDone(done, 3);
|
||||
|
||||
client.on("hello", partialDone);
|
||||
clientWSOnly.on("hello", partialDone);
|
||||
clientPollingOnly.on("hello", partialDone);
|
||||
clientCustomNamespace.on("hello", shouldNotHappen(done));
|
||||
|
||||
io.emit("hello", Buffer.from([1, 2, 3]));
|
||||
});
|
||||
|
||||
it("should broadcast volatile packet with binary content", (done) => {
|
||||
const partialDone = createPartialDone(done, 3);
|
||||
|
||||
client.on("hello", partialDone);
|
||||
clientWSOnly.on("hello", partialDone);
|
||||
clientPollingOnly.on("hello", partialDone);
|
||||
clientCustomNamespace.on("hello", shouldNotHappen(done));
|
||||
|
||||
// wait to make sure there are no packets being sent for opening the connection
|
||||
setTimeout(() => {
|
||||
io.volatile.emit("hello", Buffer.from([1, 2, 3]));
|
||||
}, 20);
|
||||
});
|
||||
|
||||
it("should broadcast in a room", (done) => {
|
||||
const partialDone = createPartialDone(done, 2);
|
||||
|
||||
client.on("hello", shouldNotHappen(done));
|
||||
clientWSOnly.on("hello", partialDone);
|
||||
clientPollingOnly.on("hello", partialDone);
|
||||
clientCustomNamespace.on("hello", shouldNotHappen(done));
|
||||
|
||||
io.of("/").sockets.get(clientWSOnly.id)!.join("room1");
|
||||
io.of("/").sockets.get(clientPollingOnly.id)!.join("room1");
|
||||
|
||||
io.to("room1").emit("hello");
|
||||
});
|
||||
|
||||
it("should broadcast in multiple rooms", (done) => {
|
||||
const partialDone = createPartialDone(done, 2);
|
||||
|
||||
client.on("hello", shouldNotHappen(done));
|
||||
clientWSOnly.on("hello", partialDone);
|
||||
clientPollingOnly.on("hello", partialDone);
|
||||
clientCustomNamespace.on("hello", shouldNotHappen(done));
|
||||
|
||||
io.of("/").sockets.get(clientWSOnly.id)!.join("room1");
|
||||
io.of("/").sockets.get(clientPollingOnly.id)!.join("room2");
|
||||
|
||||
io.to(["room1", "room2"]).emit("hello");
|
||||
});
|
||||
|
||||
it("should broadcast in all but a given room", (done) => {
|
||||
const partialDone = createPartialDone(done, 2);
|
||||
|
||||
client.on("hello", partialDone);
|
||||
clientWSOnly.on("hello", partialDone);
|
||||
clientPollingOnly.on("hello", shouldNotHappen(done));
|
||||
clientCustomNamespace.on("hello", shouldNotHappen(done));
|
||||
|
||||
io.of("/").sockets.get(clientWSOnly.id)!.join("room1");
|
||||
io.of("/").sockets.get(clientPollingOnly.id)!.join("room2");
|
||||
|
||||
io.except("room2").emit("hello");
|
||||
});
|
||||
|
||||
it("should work even after leaving room", (done) => {
|
||||
const partialDone = createPartialDone(done, 2);
|
||||
|
||||
client.on("hello", partialDone);
|
||||
clientWSOnly.on("hello", shouldNotHappen(done));
|
||||
clientPollingOnly.on("hello", partialDone);
|
||||
clientCustomNamespace.on("hello", shouldNotHappen(done));
|
||||
|
||||
io.of("/").sockets.get(client.id)!.join("room1");
|
||||
io.of("/").sockets.get(clientPollingOnly.id)!.join("room1");
|
||||
|
||||
io.of("/").sockets.get(clientWSOnly.id)!.join("room1");
|
||||
io.of("/").sockets.get(clientWSOnly.id)!.leave("room1");
|
||||
|
||||
io.to("room1").emit("hello");
|
||||
});
|
||||
|
||||
it("should serve static files", (done) => {
|
||||
const clientVersion = require("socket.io-client/package.json").version;
|
||||
|
||||
request(`http://localhost:${port}`)
|
||||
.get("/socket.io/socket.io.js")
|
||||
.buffer(true)
|
||||
.end((err, res) => {
|
||||
if (err) return done(err);
|
||||
expect(res.headers["content-type"]).to.be("application/javascript");
|
||||
expect(res.headers.etag).to.be('"' + clientVersion + '"');
|
||||
expect(res.headers["x-sourcemap"]).to.be(undefined);
|
||||
expect(res.text).to.match(/engine\.io/);
|
||||
expect(res.status).to.be(200);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user