Compare commits

...

143 Commits

Author SHA1 Message Date
Damien Arrachequesne
968277cef8 chore(release): socket.io-adapter@2.5.6
Diff: https://github.com/socketio/socket.io/compare/socket.io-adapter@2.5.5...socket.io-adapter@2.5.6
2025-12-23 12:18:53 +01:00
Damien Arrachequesne
2bf16bd214 chore(release): engine.io-client@6.6.4
Diff: https://github.com/socketio/socket.io/compare/engine.io-client@6.6.3...engine.io-client@6.6.4
2025-12-23 12:03:43 +01:00
Damien Arrachequesne
ad616070b8 docs(eio): fix link in the release notes
[skip ci]
2025-12-22 17:53:09 +01:00
Damien Arrachequesne
dd71792455 chore(release): socket.io@4.8.2
Diff: https://github.com/socketio/socket.io/compare/socket.io@4.8.1...socket.io@4.8.2
2025-12-22 17:42:41 +01:00
Ihor Machuzhak
bb0b480d2a fix(sio): improve io.close() function (#5344)
Before this change, `await io.close();` would resolve before the HTTP server was properly shut down.

Related: https://github.com/socketio/socket.io/pull/4971
2025-12-22 17:37:24 +01:00
Damien Arrachequesne
161be91975 test(sio): pin version of the client bundle in the tests 2025-12-22 17:35:35 +01:00
Damien Arrachequesne
fd9d4cab5e chore(release): socket.io-client@4.8.2
Diff: https://github.com/socketio/socket.io/compare/socket.io-client@4.8.1...socket.io-client@4.8.2
2025-12-22 16:52:21 +01:00
Damien Arrachequesne
0a99ac44a2 chore(release): engine.io@6.6.5
Diff: https://github.com/socketio/socket.io/compare/engine.io@6.6.4...engine.io@6.6.5
2025-12-22 16:27:31 +01:00
Damien Arrachequesne
4338f47336 ci(publish): use Node.js 24
Trusted publishing requires npm CLI version 11.5.1 or later.

Reference: https://docs.npmjs.com/trusted-publishers#for-github-actions
2025-12-22 16:27:30 +01:00
Damien Arrachequesne
9199156758 test(eio): fix flaky test 2025-12-22 15:27:48 +01:00
Damien Arrachequesne
594841617d test(redis-streams-emitter): migrate to Node.js test runner
We should eventually be able to replace:

- mocha and nyc with Node.js built-in test runner (`node:test`)
- expect.js with Node.js built-in assertion library (`node:assert`)
2025-12-22 14:48:29 +01:00
Denis Barbaron
84e7253e57 refactor(sio): add package.json entrypoint (#5239) 2025-12-22 14:33:12 +01:00
Damien Arrachequesne
30ec4a136a test(sio-client): reactivate all tests 2025-12-22 13:46:15 +01:00
Damien Arrachequesne
e08293bc37 refactor(eio): use URL constructor instead of url.parse() 2025-12-22 13:45:52 +01:00
Damien Arrachequesne
b837949479 ci: use Node.js 24
Reference: https://github.com/nodejs/Release
2025-12-22 13:45:36 +01:00
Damien Arrachequesne
118ef41b94 test: use tsx instead of ts-node 2025-12-22 10:39:57 +01:00
Suraj Rana
d19928e8d8 fix(sio-client): drain queue before emitting "connect" (#5259)
When the `retries` option was enabled, an event emitted in the "connect" handler would be sent twice.

Related: https://github.com/socketio/socket.io/issues/5258
2025-12-22 10:39:39 +01:00
Damien Arrachequesne
cdae01983a fix(sio-client): do not mangle the "_placeholder" attribute (bis)
The "_placeholder" attribute is used when sending binary data, and was
incorrectly mangled (converted to a random short property, like "it",
to reduce the bundle size).

Related:

- ca9e994815
- https://github.com/socketio/socket.io/issues/5215

[skip ci]
2025-12-19 15:48:28 +01:00
Damien Arrachequesne
39bb72039d docs: add release steps
[skip ci]
2025-12-19 15:43:14 +01:00
Valentin Rault
98741e15e9 refactor(sio-client): export DisconnectDescription type (#5392)
Related: https://github.com/socketio/socket.io/issues/4556
2025-12-19 14:51:39 +01:00
Damien Arrachequesne
8af70195bb refactor(sio): use URL constructor instead of url.parse()
Related: https://github.com/socketio/socket.io/issues/5377
2025-12-19 14:44:30 +01:00
Damien Arrachequesne
d88f3f4578 ci: use actions/checkout@v6 and actions/setup-node@v6
Release notes:

- https://github.com/actions/checkout/blob/main/CHANGELOG.md
- https://github.com/actions/setup-node/releases/tag/v6.0.0
2025-12-15 09:38:45 +01:00
Damien Arrachequesne
f5ee981ee8 ci(publish): use trusted publishing
Reference: https://docs.npmjs.com/trusted-publishers

[skip ci]
2025-12-15 08:57:17 +01:00
Damien Arrachequesne
76e3a72bba docs: add missing changelog links
[skip ci]
2025-12-15 08:55:26 +01:00
Damien Arrachequesne
a7b1938d06 test: regenerate SSL certs 2025-12-15 08:45:47 +01:00
Damien Arrachequesne
54743633ff chore(release): @socket.io/redis-streams-emitter@0.1.1
Diff: https://github.com/socketio/socket.io/compare/@socket.io/redis-streams-emitter@0.1.0...@socket.io/redis-streams-emitter@0.1.1
2025-11-07 10:33:07 +01:00
Damien Arrachequesne
7617707ed8 fix(redis-streams-emitter): remove dependency on socket.io-adapter
Related: https://github.com/socketio/socket.io/issues/5414
2025-11-07 10:28:27 +01:00
Damien Arrachequesne
599001d213 chore(release): @socket.io/redis-streams-emitter@0.1.0 2025-11-06 18:23:00 +01:00
Damien Arrachequesne
1c3e4711c1 feat: add emitter based on Redis streams
Related: https://github.com/socketio/socket.io-redis-streams-adapter/issues/8
2025-11-06 18:03:37 +01:00
Damien Arrachequesne
693080cac7 refactor(sio-adapter): add more debug logs 2025-10-20 15:11:21 +02:00
Damien Arrachequesne
5080c73e1e refactor: fix npm command 2025-10-17 09:41:39 +02:00
Damien Arrachequesne
47ff1cd04c chore(release): @socket.io/cluster-adapter@0.3.0
Diff: https://github.com/socketio/socket.io-cluster-adapter/compare/0.2.2...0.3.0
2025-10-16 19:55:56 +02:00
Damien Arrachequesne
0ae76360f9 Merge remote-tracking branch 'socket.io-cluster-adapter/monorepo' 2025-10-16 19:35:51 +02:00
Damien Arrachequesne
27fd420e75 refactor: prepare migration to monorepo 2025-10-16 19:31:30 +02:00
Damien Arrachequesne
0c431243e2 refactor: use the ClusterAdapter class from socket.io-adapter package
The ClusterAdapter class has been moved to [1], so that this adapter
only needs to implement to pub/sub mechanism.

Also, [2] should reduce the number of "timeout reached: only x
responses received out of y" errors, since the fetchSockets() requests
will now succeed even if a server leaves the cluster.

[1]: https://github.com/socketio/socket.io/tree/main/packages/socket.io-adapter
[2]: 0e23ff0cc6
2025-10-16 19:16:20 +02:00
Damien Arrachequesne
4fc25d80ec ci: add Node.js 24
Reference: https://github.com/nodejs/Release
2025-10-16 12:03:32 +02:00
Damien Arrachequesne
1dd729b1a1 refactor: upgrade to prettier 3 2025-10-16 11:51:11 +02:00
Damien Arrachequesne
6877512f57 refactor: upgrade to TypeScript 5 2025-10-16 11:49:12 +02:00
Damien Arrachequesne
cf6816afcf chore: npm audit fix 2025-10-10 09:29:29 +02:00
Damien Arrachequesne
625fd66d73 chore: dedupe debug dependency 2025-10-10 09:11:54 +02:00
Damien Arrachequesne
f3e1f5ebdf fix(sio): call adapter.init() when creating each namespace
The init() method of the adapter will now be called when creating a namespace with `io.of(<the-namespace>)`.

Note: any promise rejection is silently caught, as I don't see how we could properly expose the promise.

```js
const io = new Server({
  adapter: myAdapter
});
// under the hood, this:
// - implicitly creates the main namespace (/)
// - creates an instance of `myAdapter` for the main namespace
// - calls `myAdapter.init()` (with this change)
```

Related:

- https://github.com/socketio/socket.io/issues/3662
- https://github.com/socketio/socket.io-postgres-adapter/issues/16
2025-10-09 09:48:07 +02:00
Damien Arrachequesne
e97549259e ci(browser): use Windows 8 for IE tests 2025-09-30 11:44:50 +02:00
MiaoWoo
1da9cddeab fix(eio-client): properly handle port option (#5241)
Passing { port: "443" } would include the port in the URL (":443").
2025-09-30 10:57:43 +02:00
Avi Vahl
6f9b198bc8 chore(deps): ws@8.18.3, debug@4.4.1 (#5335)
Release notes:

- https://github.com/websockets/ws/releases/tag/8.18.3
- https://github.com/debug-js/debug/releases/tag/4.4.1
2025-09-11 07:51:07 +02:00
Damien Arrachequesne
ac3df9a747 chore(release): @socket.io/postgres-emitter@0.1.1 2025-09-05 07:27:48 +02:00
Damien Arrachequesne
21fd54ece6 refactor(postgres-emitter): update compose file 2025-09-05 07:20:29 +02:00
Damien Arrachequesne
96d907b9b5 docs(postgres-emitter): add dark version of the explanation diagram 2025-09-05 07:19:07 +02:00
Damien Arrachequesne
32257b6cb8 fix(postgres-emitter): use parameterized query to send the NOTIFY command
Related:

- https://github.com/socketio/socket.io-postgres-emitter/issues/1
- https://github.com/socketio/socket.io-postgres-adapter/pull/1
2025-09-05 07:18:36 +02:00
Damien Arrachequesne
c7144920e3 Merge remote-tracking branch 'socket.io-postgres-emitter/main' into monorepo 2025-09-04 09:30:26 +02:00
Damien Arrachequesne
42480e9a7f chore: prepare migration to monorepo 2025-09-04 09:23:32 +02:00
Lou Klepner
0a8f91047c docs: fix adapter link (#2) 2025-09-04 09:22:40 +02:00
Damien Arrachequesne
a66ed68506 docs(protocol): add test with cancelled request
Related: 8f1ea3d58f
2025-09-03 09:02:44 +02:00
Damien Arrachequesne
3be6481d9d ci: pin Node.js 22 version
Related: https://github.com/nodejs/node/issues/59364
2025-08-09 09:31:12 +02:00
Wang Guan
be13cca94c refactor: improve type annotations and comments (#5364) 2025-08-09 08:43:34 +02:00
Damien Arrachequesne
e95f6abf93 docs: fix message handler latency in test suites
Related: https://github.com/socketio/socket.io-protocol/issues/32
2025-03-28 21:29:20 +01:00
Damien Arrachequesne
72d61dab82 chore(release): engine.io@6.6.4
Diff: https://github.com/socketio/socket.io/compare/engine.io@6.6.3...engine.io@6.6.4
2025-01-28 09:23:04 +01:00
Damien Arrachequesne
5a31aaf917 chore(eio): revert cookie to version ~0.7.2
This reverts commit 7427109658.

The new version of the `cookie` package contains code with optional chaining (`?.`), which is not supported by older Node.js versions (< 14).

The types for cookie are now bundled, so that there is no conflict with the types coming from `cookie@1`:

> error TS2724: '"cookie"' has no exported member named 'CookieSerializeOptions'. Did you mean 'SerializeOptions'?
>
> import type { CookieSerializeOptions } from "cookie";
>               ~~~~~~~~~~~~~~~~~~~~~~

Related: https://github.com/socketio/socket.io/issues/5283
2025-01-28 09:13:39 +01:00
Damien Arrachequesne
62e4da125e chore(release): engine.io@6.6.3
Diff: https://github.com/socketio/socket.io/compare/engine.io@6.6.2...engine.io@6.6.3
2025-01-23 07:55:43 +01:00
Damien Arrachequesne
bfa6eab195 chore(release): engine.io-client@6.6.3
Diff: https://github.com/socketio/socket.io/compare/engine.io-client@6.6.2...engine.io-client@6.6.3
2025-01-23 07:38:37 +01:00
Ben McCann
7fcddcb3bb fix(engine.io-client): correctly consume the ws package (#5220)
This should fix the following issue:

```
SyntaxError: Named export 'WebSocket' not found. The requested module 'ws' is a CommonJS module, which may not support all module.exports as named exports.
CommonJS modules can always be imported via the default export, for example using:

import pkg from 'ws';
const { WebSocket } = pkg;
```
2025-01-07 10:53:32 +01:00
Damien Arrachequesne
7427109658 refactor(eio): bump cookie to version 1.0.2
Release notes: https://github.com/jshttp/cookie/releases/tag/v1.0.0

The types are now included in the npm package. The `CookieSerializeOptions` type, which is used in our `ServerOptions` type, has been renamed to `SerializeOptions`, but there are no breaking change.

Related: https://github.com/socketio/socket.io/issues/5231
2024-11-21 08:57:37 +01:00
Damien Arrachequesne
91e1c8b358 chore(release): socket.io@4.8.1
Diff: https://github.com/socketio/socket.io/compare/socket.io@4.8.0...socket.io@4.8.1
2024-10-25 08:13:15 +02:00
Damien Arrachequesne
8d5528aa2a chore(release): socket.io-client@4.8.1
Diff: https://github.com/socketio/socket.io/compare/socket.io-client@4.8.0...socket.io-client@4.8.1
2024-10-25 08:00:26 +02:00
Damien Arrachequesne
71387e5294 refactor(sio-client): reexport transports from the engine 2024-10-25 07:54:35 +02:00
Samuel Vogelsanger
aead83560d refactor(sio): make Namespace._fns private (#5196)
Related: https://github.com/socketio/socket.io/issues/5179
2024-10-23 10:59:23 +02:00
Damien Arrachequesne
029e010901 chore(release): engine.io-client@6.6.2
Diff: https://github.com/socketio/socket.io/compare/engine.io-client@6.6.1...engine.io-client@6.6.2
2024-10-23 10:48:16 +02:00
Damien Arrachequesne
4ca6ddb3a2 docs(nuxt): update example with latest version
Related: https://github.com/socketio/socket.io/issues/5208

[skip ci]
2024-10-23 08:19:48 +02:00
Damien Arrachequesne
ca9e994815 fix(sio-client): do not mangle the "_placeholder" attribute
The "_placeholder" attribute is used when sending binary data, and was
incorrectly mangled (converted to a random short property, like "it",
to reduce the bundle size).

This bug was introduced in [1], included in `socket.io-client@4.8.0`.

[1]: 7085f0e3e4

Related: https://github.com/socketio/socket.io/issues/5215
2024-10-22 11:41:08 +02:00
Damien Arrachequesne
4865f2e62e fix(eio-client): prevent infinite loop with Node.js built-in WebSocket
Related: https://github.com/socketio/socket.io/issues/5194
2024-10-22 00:49:25 +02:00
Damien Arrachequesne
d4b3ddedff ci: use Node.js 22
Reference: https://github.com/nodejs/Release
2024-10-21 23:30:11 +02:00
Damien Arrachequesne
3b68658201 chore: bump @fails-components/webtransport to version 1.1.4 (dev) 2024-10-21 23:29:29 +02:00
Damien Arrachequesne
175a2c58c1 fix(eio-client/types): remove ws type from .d.ts file
Before this change, the following error would be thrown when compiling
with TypeScript:

```
node_modules/engine.io-client/build/esm/transports/websocket.node.d.ts:12:101 - error TS1340: Module 'ws' does not refer to a type, but is used as a type here. Did you mean 'typeof import('ws')'?

12     createSocket(uri: string, protocols: string | string[] | undefined, opts: Record<string, any>): import("ws");
                                                                                                       ~~~~~~~~~~~~
```

This behavior was introduced in [1], included in version `6.6.0`.

The return type is forced as `any`, so that the `@types/ws` dependency
is optional.

[1]: f4d898ee96

Related: https://github.com/socketio/socket.io/issues/5202
2024-10-19 07:09:19 +02:00
Damien Arrachequesne
9b80ab42d6 chore(release): engine.io@6.6.2
Diff: https://github.com/socketio/socket.io/compare/engine.io@6.6.1...engine.io@6.6.2
2024-10-09 19:01:17 +02:00
Damien Arrachequesne
a5d2368512 ci: ignore tests when publishing to npm (bis)
`tags-ignore` seems to disable the workflow in all cases.
2024-10-09 18:56:59 +02:00
k725
88efd446f1 chore(deps): bump cookie to version 0.7.2 (#5205)
Related: https://github.com/advisories/GHSA-pxg6-pf52-xh8x
2024-10-09 17:47:42 +02:00
Damien Arrachequesne
d0fc720420 chore(release): socket.io@4.8.0
Diff: https://github.com/socketio/socket.io/compare/socket.io@4.7.5...socket.io@4.8.0
2024-09-21 10:02:24 +02:00
Damien Arrachequesne
4a0555c671 chore(release): socket.io-client@4.8.0
Diff: https://github.com/socketio/socket.io/compare/socket.io-client@4.7.5...socket.io-client@4.8.0
2024-09-21 09:45:51 +02:00
Damien Arrachequesne
2b60df18a8 chore(release): engine.io@6.6.1
Diff: https://github.com/socketio/socket.io/compare/engine.io@6.6.0...engine.io@6.6.1
2024-09-21 09:30:07 +02:00
Damien Arrachequesne
d4cb375856 ci: ignore tests when publishing to npm 2024-09-21 09:27:42 +02:00
Damien Arrachequesne
c251ae7ba7 chore(release): engine.io-client@6.6.1
Diff: https://github.com/socketio/socket.io/compare/engine.io-client@6.6.0...engine.io-client@6.6.1
2024-09-21 09:07:48 +02:00
Damien Arrachequesne
8a2f5a3da0 fix(eio-client): move 'offline' event listener at the top
Related: https://github.com/socketio/socket.io/issues/5125
2024-09-21 08:47:20 +02:00
Damien Arrachequesne
b04fa64365 fix(sio): allow to join a room in a middleware (uws)
Related:

- https://github.com/socketio/socket.io/issues/4810
- https://github.com/socketio/socket.io/issues/5139
2024-09-21 08:19:32 +02:00
Damien Arrachequesne
7085f0e3e4 refactor(sio-client): mangle private attributes
|          | before  | after   |
|----------|---------|---------|
| min+gzip | 14.6 KB | 14.3 KB |
| min+br   | 13.1 KB | 12.9 KB |

Reference: https://terser.org/docs/options/#mangle-properties-options
2024-09-21 07:48:50 +02:00
Damien Arrachequesne
4f66708210 chore(sio-client): use babel loose mode when transpiling classes
By default, Babel uses `Object.defineProperty()` when transpiling
classes. We'll now use the loose mode which creates a more terse
output.

|          | before  | after   |
|----------|---------|---------|
| min+gzip | 14.9 KB | 14.6 KB |
| min+br   | 13.4 KB | 13.1 KB |

Reference: https://babeljs.io/docs/babel-plugin-transform-classes
2024-09-21 07:44:15 +02:00
Damien Arrachequesne
1a95db2145 chore(sio-client): add a script to compute the bundle size 2024-09-21 07:34:49 +02:00
Damien Arrachequesne
282ae922a4 chore(sio-client): restore the debug package in the dev bundle
The debug package was not included anymore in the dev bundle since the
migration from webpack to rollup ([1]) in version 4.3.0.

[1]: 0661564dc2

Related: https://github.com/socketio/socket.io/issues/5108
2024-09-21 07:33:53 +02:00
Damien Arrachequesne
93010ca3c4 chore(eio-client): bump xmlhttprequest-ssl to version 2.1.1
Related:

- https://github.com/socketio/socket.io/issues/4402
- b01f69a689
2024-09-21 07:32:12 +02:00
Damien Arrachequesne
132d05fc0b fix(sio): expose type of default engine
Related: https://github.com/socketio/socket.io/issues/4693
2024-09-20 11:18:23 +02:00
Damien Arrachequesne
d5095fe98c fix(eio): prevent the client from upgrading twice (uws)
Related: https://github.com/socketio/socket.io/issues/5066
2024-09-19 12:13:42 +02:00
Damien Arrachequesne
da613810fd test(eio): bump uWebSockets.js to version 20.48.0 2024-09-19 12:12:58 +02:00
Damien Arrachequesne
19c48a44e6 refactor(sio): break circular dependency in source code
Related: https://github.com/socketio/socket.io/issues/4329
2024-09-19 09:27:12 +02:00
Damien Arrachequesne
9b3c9abeca fix(eio-client): only remove the event listener if it exists
Related: https://github.com/socketio/socket.io/issues/5088#issuecomment-2217202350
2024-09-19 09:26:26 +02:00
Damien Arrachequesne
043b55c418 refactor(sio): simplify middleware execution (bis) 2024-09-18 22:15:32 +02:00
Damien Arrachequesne
32c761f02f refactor(sio): export the ExtendedError type
Related: https://github.com/socketio/socket.io/issues/4798
2024-09-18 18:22:06 +02:00
Damien Arrachequesne
1f54ee08c6 refactor(sio): simplify middleware execution 2024-09-18 18:21:58 +02:00
Damien Arrachequesne
923a12e2de fix(eio): discard all pending packets when the server is closed
In some specific cases, the transport was not closed right away,
leaving the Node.js process alive even after closing the server.
The HTTP long-polling transport would be closed after the heartbeat
failure and the `closeTimeout` delay (20 + 25 + 30 seconds).

Example:

```js
io.on("connection", (socket) => {
  // the writeBuffer is not empty, so the transport is not closed right away
  io.close();
});
```

Related: https://github.com/socketio/socket.io/issues/5088
2024-09-18 18:17:14 +02:00
Damien Arrachequesne
13c6d2e89d fix(sio-client): allow to manually stop the reconnection loop
```js
socket.io.on("reconnect_attempt", () => {
  socket.io.reconnection(false); // will now work properly
});
```

Related: https://github.com/socketio/socket.io/issues/5126
2024-09-18 11:19:28 +02:00
Tom Jenkinson
8adcfbfde5 fix(sio-client): do not send a packet on an expired connection (#5134)
When a laptop is suspended or a phone is locked, the timer that is used
to check the liveness of the connection is paused and is not able to
detect that the heartbeat has failed.

Previously, emitting a message after resuming the page would lose the
message. The status of the timer will now be checked before sending the
message, so that it gets buffered and sent upon reconnection.

Note: we could also have used the Page Visibility API or a custom
setTimeout() method based on setInterval(), but this would not be as
reliable as the current solution.

Reference: https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API

Related: https://github.com/socketio/socket.io/issues/5135
2024-09-18 11:11:22 +02:00
Damien Arrachequesne
7a23dde6ef perf: do not reset the hearbeat timer on each packet
This reverts ed34a45a07

See also: 5359bae683
2024-09-18 11:06:39 +02:00
Damien Arrachequesne
60c757f718 fix(sio-client): accept string | undefined as init argument (bis)
Following: 5a3eafed1c

Related: https://github.com/socketio/socket.io/issues/4873
2024-09-18 08:09:47 +02:00
Damien Arrachequesne
04c8dd979c fix(sio-client): close the engine upon decoding exception
Related: https://github.com/socketio/socket.io/issues/5128
2024-09-18 07:52:37 +02:00
Damien Arrachequesne
2194264820 test(eio-client): reduce test suite duration 2024-09-18 07:52:12 +02:00
Damien Arrachequesne
09f573cad8 test(sio-client): reduce test suite duration 2024-09-18 07:52:12 +02:00
Damien Arrachequesne
fcbecd4f46 ci: restore package-specific tests 2024-09-17 15:07:23 +02:00
Damien Arrachequesne
fd99f2e15f refactor(sio): export the DefaultEventsMap type
Related: https://github.com/socketio/socket.io/issues/4747
2024-09-16 15:49:15 +02:00
Damien Arrachequesne
02d59a0e99 chore: re-enable publish workflow
This reverts commit 7160eb7eb0.

[skip ci]
2024-09-16 09:11:35 +02:00
Damien Arrachequesne
7160eb7eb0 chore: temporarily disable publish workflow
In order to import tags from other repositories.

[skip ci]
2024-09-16 08:56:07 +02:00
Wang Guan
a1ccba3a77 chore: use prettier v3 everywhere (#5169) 2024-09-16 08:43:08 +02:00
Mark Nelissen
e347a3c24e fix(sio): correctly await async close on adapters (#4971)
Following: bf64870957
2024-09-14 08:51:08 +02:00
nayounsang
b5ccfd4838 refactor(eio-client): improve transports type (#5188)
Related: https://github.com/socketio/socket.io/issues/5187
2024-09-14 08:17:36 +02:00
Wang Guan
5d9a2d5544 chore(socket.io-client): bump engine.io-client to version 6.6.0 2024-08-23 23:19:45 +02:00
Wang Guan
d5b22f5a76 chore(socket.io): bump engine.io to version 6.6.0 2024-08-23 23:19:31 +02:00
Damien Arrachequesne
582655f679 test(cluster-engine): fix flaky test cleanup 2024-07-26 09:26:47 +02:00
KartikeSingh
b79d80aa59 docs: fix conjunction with fastify example (#5057)
[skip ci]
2024-07-22 11:32:58 +02:00
Damien Arrachequesne
7fd75e6aac docs(changelog): add changelog for socket.io-parser@3.3.4
Diff: https://github.com/Automattic/socket.io-parser/compare/3.3.3...3.3.4

[skip ci]
2024-07-22 11:28:47 +02:00
Damien Arrachequesne
b7577556e3 docs: add example with NestJS
Reference: https://docs.nestjs.com/websockets/gateways

[skip ci]
2024-07-22 10:07:45 +02:00
Damien Arrachequesne
6e9bff4fcf docs: add example with @socket.io/postgres-adapter
[skip ci]
2024-07-19 15:20:12 +02:00
Damien Arrachequesne
8b0a40fd4a docs: add examples with @socket.io/cluster-engine
[skip ci]
2024-07-19 08:42:38 +02:00
Damien Arrachequesne
1f09a3e979 ci(publish): compile all packages before publishing
As some packages depend on the types of others.

[skip ci]
2024-07-18 10:07:47 +02:00
Damien Arrachequesne
0af50758f6 chore(cluster-engine): update homepage URL 2024-07-18 10:03:57 +02:00
Damien Arrachequesne
b9b16132c2 chore(socket.io-adapter): remove dist before compilation 2024-07-17 10:34:57 +02:00
Damien Arrachequesne
be1e4cb24d ci(publish): update the tag regex
In order to also match "@socket.io/some-package@1.2.3".

Reference: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet

[skip ci]
2024-07-17 10:26:56 +02:00
Damien Arrachequesne
7521ac227b chore(release): @socket.io/cluster-engine@0.1.0 2024-07-17 09:53:34 +02:00
Damien Arrachequesne
b00124b65a feat: implement cluster-friendly engine 2024-07-17 09:27:12 +02:00
Damien Arrachequesne
b7da542890 chore: normalize repository URLs
In order to address the following warning when publishing:

> npm warn publish npm auto-corrected some errors in your package.json when publishing.  Please run "npm pkg fix" to address these errors.
> npm warn publish errors corrected:
> npm warn publish "repository.url" was normalized to "git+https://github.com/socketio/socket.io.git"
2024-07-11 14:59:00 +02:00
Damien Arrachequesne
0692bed462 chore: fix the publish workflow
It seems the "registry-url" variable is mandatory: https://github.com/npm/cli/issues/6184
2024-07-11 13:45:44 +02:00
Damien Arrachequesne
56a53bceb9 ci: add Node.js 20 in the test matrix 2023-07-09 10:03:47 +02:00
Damien Arrachequesne
683720a67d test: fix flaky test 2023-07-09 10:03:46 +02:00
Damien Arrachequesne
a529eb08d6 chore: bump dev dependencies 2023-07-09 10:03:32 +02:00
Damien Arrachequesne
cddb78e5fa chore(release): 0.2.2
Diff: https://github.com/socketio/socket.io-cluster-adapter/compare/0.2.1...0.2.2
2023-03-24 17:32:35 +01:00
Damien Arrachequesne
15fd56e78d chore: add socket.io-parser to peerDependencies
This should (at least in theory) fix sync issues for the
`socket.io-adapter` package, which is imported by both the `socket.io`
and `@socket.io/cluster-adapter` packages:

- `socket.io@4.5.0` should resolve `socket.io-adapter@~2.4.0`
- `socket.io@4.6.0` should resolve `socket.io-adapter@~2.5.0`
2023-03-24 17:28:57 +01:00
Damien Arrachequesne
e86ef45f87 ci: upgrade to actions/checkout@3 and actions/setup-node@3
Reference: https://github.blog/changelog/2022-09-22-github-actions-all-actions-will-begin-running-on-node16-instead-of-node12/
2023-03-24 17:16:25 +01:00
Damien Arrachequesne
fe840e2eb3 chore(release): 0.2.1
Diff: https://github.com/socketio/socket.io-cluster-adapter/compare/0.2.0...0.2.1
2022-10-13 09:23:36 +02:00
Damien Arrachequesne
a5a1c29082 chore: update dev dependencies 2022-10-13 09:17:46 +02:00
Damien Arrachequesne
66b4079953 ci: add Node.js 18 in the test matrix
Reference: https://github.com/nodejs/Release
2022-10-13 09:15:34 +02:00
Rolando Andrade
be0a0e3217 fix: properly handle ERR_IPC_CHANNEL_CLOSED errors (#6)
Related: https://github.com/socketio/socket.io-cluster-adapter/issues/5
2022-10-13 09:11:37 +02:00
Damien Arrachequesne
43f9ee8d23 chore(release): 0.2.0
Diff: https://github.com/socketio/socket.io-cluster-adapter/compare/0.1.0...0.2.0
2022-04-28 16:16:13 +02:00
Damien Arrachequesne
055b7840d8 feat: broadcast and expect multiple acks
This feature was added in `socket.io@4.5.0`:

```js
io.timeout(1000).emit("some-event", (err, responses) => {
  // ...
});
```

Thanks to this change, it will now work with multiple Socket.IO
servers.

Related: https://github.com/socketio/socket.io/issues/4163
2022-04-28 16:11:12 +02:00
Damien Arrachequesne
6397c1bdfd chore(release): 0.1.0 2021-06-22 07:06:15 +02:00
Damien Arrachequesne
ff370cfc46 Initial commit 2021-06-22 07:02:16 +02:00
Damien Arrachequesne
1f8a6c4ecb docs: add link to related packages 2021-06-14 08:21:28 +02:00
Damien Arrachequesne
eb01ff5803 chore(release): 0.1.0 2021-06-14 08:02:20 +02:00
Damien Arrachequesne
f2e3d162ab Initial commit 2021-06-14 07:59:12 +02:00
252 changed files with 25261 additions and 10064 deletions

View File

@@ -19,6 +19,8 @@ jobs:
- custom-parsers
- typescript-example/cjs
- typescript-example/esm
- typescript-client-example/cjs
- typescript-client-example/esm
- webpack-build
- webpack-build-server
- basic-crud-application/angular-client

View File

@@ -2,6 +2,8 @@ name: CI (browser)
on:
push:
branches:
- '**'
paths:
- 'packages/engine.io-parser/**'
- 'packages/engine.io-client/**'
@@ -14,7 +16,7 @@ permissions:
jobs:
test-browser:
runs-on: ubuntu-latest
timeout-minutes: 10
timeout-minutes: 20
steps:
- name: Checkout repository

View File

@@ -2,6 +2,8 @@ name: CI
on:
push:
branches:
- '**'
pull_request:
schedule:
- cron: '0 0 * * 0'
@@ -18,28 +20,59 @@ jobs:
fail-fast: false
matrix:
node-version:
- 18
- 20
- 22
- 24
services:
redis:
image: redis:7
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 6379:6379
postgres:
image: postgres:14
env:
POSTGRES_PASSWORD: changeit
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
run: npm ci
# in order to test our compliance with TypeScript v4.2 (older versions are not tested)
- name: Install TypeScript 4.2
run: npm i typescript@4.2
if: ${{ matrix.node-version == '16' }}
- name: Compile each package
run: npm run compile --workspaces --if-present
- name: Run tests
run: npm test --workspaces
- name: Run tests with uws (engine.io)
run: npm run test:uws --workspace=engine.io
if: ${{ matrix.node-version == '18' }}
- name: Run tests with fetch instead of XHR (engine.io-client)
run: npm run test:node-fetch --workspace=engine.io-client
if: ${{ matrix.node-version == '18' }}
- name: Run tests with Node.js native WebSocket (engine.io-client)
run: npm run test:node-builtin-ws --workspace=engine.io-client
if: ${{ matrix.node-version == '22' }}

View File

@@ -1,4 +1,4 @@
# reference: https://docs.npmjs.com/generating-provenance-statements
# reference: https://docs.npmjs.com/trusted-publishers#for-github-actions
name: Publish
@@ -6,7 +6,7 @@ on:
push:
tags:
# expected format: <package>@<version> (example: socket.io@1.2.3)
- '*@*'
- '**@*'
jobs:
publish:
@@ -19,15 +19,17 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
- name: Use Node.js 20
- name: Use Node.js 24
uses: actions/setup-node@v4
with:
node-version: 20
node-version: 24
registry-url: 'https://registry.npmjs.org'
- name: Install dependencies
run: npm ci
- name: Compile each package
run: npm run compile --workspaces --if-present
- name: Publish package
run: npm publish --workspace=${GITHUB_REF_NAME%@*} --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: npm publish --workspace=${GITHUB_REF_NAME%@*} --access public

View File

@@ -2,13 +2,17 @@
Here are the detailed changelogs for each package in this monorepo:
| Package | Changelog |
|--------------------------------|---------------------------------------------------------|
| `engine.io` | [link](packages/engine.io/CHANGELOG.md) |
| `engine.io-client` | [link](packages/engine.io-client/CHANGELOG.md) |
| `engine.io-parser` | [link](packages/engine.io-parser/CHANGELOG.md) |
| `socket.io` | [link](packages/socket.io/CHANGELOG.md) |
| `socket.io-adapter` | [link](packages/socket.io-adapter/CHANGELOG.md) |
| `socket.io-client` | [link](packages/socket.io-client/CHANGELOG.md) |
| `@socket.io/component-emitter` | [link](packages/socket.io-component-emitter/History.md) |
| `socket.io-parser` | [link](packages/socket.io-parser/CHANGELOG.md) |
| Package | Changelog |
|------------------------------------|----------------------------------------------------------------|
| `engine.io` | [link](packages/engine.io/CHANGELOG.md) |
| `engine.io-client` | [link](packages/engine.io-client/CHANGELOG.md) |
| `engine.io-parser` | [link](packages/engine.io-parser/CHANGELOG.md) |
| `socket.io` | [link](packages/socket.io/CHANGELOG.md) |
| `socket.io-adapter` | [link](packages/socket.io-adapter/CHANGELOG.md) |
| `socket.io-client` | [link](packages/socket.io-client/CHANGELOG.md) |
| `@socket.io/cluster-adapter` | [link](packages/socket.io-cluster-adapter/CHANGELOG.md) |
| `@socket.io/cluster-engine` | [link](packages/socket.io-cluster-engine/CHANGELOG.md) |
| `@socket.io/component-emitter` | [link](packages/socket.io-component-emitter/History.md) |
| `socket.io-parser` | [link](packages/socket.io-parser/CHANGELOG.md) |
| `@socket.io/postgres-emitter` | [link](packages/socket.io-postgres-emitter/CHANGELOG.md) |
| `@socket.io/redis-streams-emitter` | [link](/packages/socket.io-redis-streams-emitter/CHANGELOG.md) |

View File

@@ -78,6 +78,7 @@ This repository is a [monorepo](https://en.wikipedia.org/wiki/Monorepo) which co
| `socket.io` | The server-side implementation of the bidirectional channel, built on top on the `engine.io` package. |
| `socket.io-adapter` | An extensible component responsible for broadcasting a packet to all connected clients, used by the `socket.io` package. |
| `socket.io-client` | The client-side implementation of the bidirectional channel, built on top on the `engine.io-client` package. |
| `@socket.io/cluster-engine` | A cluster-friendly engine to share load between multiple Node.js processes (without sticky sessions) |
| `@socket.io/component-emitter` | An `EventEmitter` implementation, similar to the one provided by [Node.js](https://nodejs.org/api/events.html) but for all platforms. |
| `socket.io-parser` | The parser responsible for encoding and decoding Socket.IO packets, used by both the `socket.io` and `socket.io-client` packages. |
@@ -149,3 +150,18 @@ For a specific workspace:
```bash
npm test --workspace=socket.io
```
### Generate the changelog
Install the [`conventional-changelog-cli`](https://www.npmjs.com/package/conventional-changelog-cli) package:
```bash
npm i -g conventional-changelog-cli
```
Then run:
```bash
cd packages/engine.io-client
conventional-changelog -p angular --tag-prefix "engine.io-client@" --commit-path .
```

View File

@@ -17,16 +17,35 @@ function sleep(delay) {
return new Promise((resolve) => setTimeout(resolve, delay));
}
function createWebSocket(url) {
const socket = new WebSocket(url);
socket._eventBuffer = {};
socket._pendingPromises = {};
for (const eventType of ["open", "close", "message"]) {
socket._eventBuffer[eventType] = [];
socket._pendingPromises[eventType] = [];
socket.addEventListener(eventType, (event) => {
if (socket._pendingPromises[eventType].length) {
socket._pendingPromises[eventType].shift()(event);
} else {
socket._eventBuffer[eventType].push(event);
}
});
}
return socket;
}
function waitFor(socket, eventType) {
return new Promise((resolve) => {
socket.addEventListener(
eventType,
(event) => {
resolve(event);
},
{ once: true }
);
});
if (socket._eventBuffer[eventType].length) {
return Promise.resolve(socket._eventBuffer[eventType].shift());
} else {
return new Promise((resolve) => {
socket._pendingPromises[eventType].push(resolve);
});
}
}
async function initLongPollingSession() {
@@ -110,7 +129,7 @@ describe("Engine.IO protocol", () => {
describe("WebSocket", () => {
it("successfully opens a session", async () => {
const socket = new WebSocket(
const socket = createWebSocket(
`${WS_URL}/engine.io/?EIO=4&transport=websocket`
);
@@ -137,7 +156,7 @@ describe("Engine.IO protocol", () => {
});
it("fails with an invalid 'EIO' query parameter", async () => {
const socket = new WebSocket(
const socket = createWebSocket(
`${WS_URL}/engine.io/?transport=websocket`
);
@@ -145,9 +164,9 @@ describe("Engine.IO protocol", () => {
socket.on("error", () => {});
}
waitFor(socket, "close");
await waitFor(socket, "close");
const socket2 = new WebSocket(
const socket2 = createWebSocket(
`${WS_URL}/engine.io/?EIO=abc&transport=websocket`
);
@@ -155,19 +174,19 @@ describe("Engine.IO protocol", () => {
socket2.on("error", () => {});
}
waitFor(socket2, "close");
await waitFor(socket2, "close");
});
it("fails with an invalid 'transport' query parameter", async () => {
const socket = new WebSocket(`${WS_URL}/engine.io/?EIO=4`);
const socket = createWebSocket(`${WS_URL}/engine.io/?EIO=4`);
if (isNodejs) {
socket.on("error", () => {});
}
waitFor(socket, "close");
await waitFor(socket, "close");
const socket2 = new WebSocket(
const socket2 = createWebSocket(
`${WS_URL}/engine.io/?EIO=4&transport=abc`
);
@@ -175,7 +194,7 @@ describe("Engine.IO protocol", () => {
socket2.on("error", () => {});
}
waitFor(socket2, "close");
await waitFor(socket2, "close");
});
});
});
@@ -313,11 +332,30 @@ describe("Engine.IO protocol", () => {
expect(pollResponse.status).to.eql(400);
});
it("closes the session upon cancelled polling request", async () => {
const sid = await initLongPollingSession();
const controller = new AbortController();
fetch(`${URL}/engine.io/?EIO=4&transport=polling&sid=${sid}`, {
signal: controller.signal,
}).catch(() => {});
await sleep(5);
controller.abort();
const pollResponse = await fetch(
`${URL}/engine.io/?EIO=4&transport=polling&sid=${sid}`,
);
expect(pollResponse.status).to.eql(400);
});
});
describe("WebSocket", () => {
it("sends and receives a plain text packet", async () => {
const socket = new WebSocket(
const socket = createWebSocket(
`${WS_URL}/engine.io/?EIO=4&transport=websocket`
);
@@ -335,7 +373,7 @@ describe("Engine.IO protocol", () => {
});
it("sends and receives a binary packet", async () => {
const socket = new WebSocket(
const socket = createWebSocket(
`${WS_URL}/engine.io/?EIO=4&transport=websocket`
);
socket.binaryType = "arraybuffer";
@@ -352,7 +390,7 @@ describe("Engine.IO protocol", () => {
});
it("closes the session upon invalid packet format", async () => {
const socket = new WebSocket(
const socket = createWebSocket(
`${WS_URL}/engine.io/?EIO=4&transport=websocket`
);
@@ -412,7 +450,7 @@ describe("Engine.IO protocol", () => {
describe("WebSocket", () => {
it("sends ping/pong packets", async () => {
const socket = new WebSocket(
const socket = createWebSocket(
`${WS_URL}/engine.io/?EIO=4&transport=websocket`
);
@@ -430,7 +468,7 @@ describe("Engine.IO protocol", () => {
});
it("closes the session upon ping timeout", async () => {
const socket = new WebSocket(
const socket = createWebSocket(
`${WS_URL}/engine.io/?EIO=4&transport=websocket`
);
@@ -468,7 +506,7 @@ describe("Engine.IO protocol", () => {
describe("WebSocket", () => {
it("forcefully closes the session", async () => {
const socket = new WebSocket(
const socket = createWebSocket(
`${WS_URL}/engine.io/?EIO=4&transport=websocket`
);
@@ -485,7 +523,7 @@ describe("Engine.IO protocol", () => {
it("successfully upgrades from HTTP long-polling to WebSocket", async () => {
const sid = await initLongPollingSession();
const socket = new WebSocket(
const socket = createWebSocket(
`${WS_URL}/engine.io/?EIO=4&transport=websocket&sid=${sid}`
);
@@ -521,12 +559,13 @@ describe("Engine.IO protocol", () => {
it("ignores HTTP requests with same sid after upgrade", async () => {
const sid = await initLongPollingSession();
const socket = new WebSocket(
const socket = createWebSocket(
`${WS_URL}/engine.io/?EIO=4&transport=websocket&sid=${sid}`
);
await waitFor(socket, "open");
socket.send("2probe");
await waitFor(socket, "message"); // "3probe"
socket.send("5");
const pollResponse = await fetch(
@@ -545,15 +584,16 @@ describe("Engine.IO protocol", () => {
it("ignores WebSocket connection with same sid after upgrade", async () => {
const sid = await initLongPollingSession();
const socket = new WebSocket(
const socket = createWebSocket(
`${WS_URL}/engine.io/?EIO=4&transport=websocket&sid=${sid}`
);
await waitFor(socket, "open");
socket.send("2probe");
await waitFor(socket, "message"); // "3probe"
socket.send("5");
const socket2 = new WebSocket(
const socket2 = createWebSocket(
`${WS_URL}/engine.io/?EIO=4&transport=websocket&sid=${sid}`
);

View File

@@ -17,16 +17,35 @@ function sleep(delay) {
return new Promise((resolve) => setTimeout(resolve, delay));
}
function createWebSocket(url) {
const socket = new WebSocket(url);
socket._eventBuffer = {};
socket._pendingPromises = {};
for (const eventType of ["open", "close", "message"]) {
socket._eventBuffer[eventType] = [];
socket._pendingPromises[eventType] = [];
socket.addEventListener(eventType, (event) => {
if (socket._pendingPromises[eventType].length) {
socket._pendingPromises[eventType].shift()(event);
} else {
socket._eventBuffer[eventType].push(event);
}
});
}
return socket;
}
function waitFor(socket, eventType) {
return new Promise((resolve) => {
socket.addEventListener(
eventType,
(event) => {
resolve(event);
},
{ once: true }
);
});
if (socket._eventBuffer[eventType].length) {
return Promise.resolve(socket._eventBuffer[eventType].shift());
} else {
return new Promise((resolve) => {
socket._pendingPromises[eventType].push(resolve);
});
}
}
function waitForPackets(socket, count) {
@@ -55,7 +74,7 @@ async function initLongPollingSession() {
}
async function initSocketIOConnection() {
const socket = new WebSocket(
const socket = createWebSocket(
`${WS_URL}/socket.io/?EIO=4&transport=websocket`
);
socket.binaryType = "arraybuffer";
@@ -145,7 +164,7 @@ describe("Engine.IO protocol", () => {
describe("WebSocket", () => {
it("should successfully open a session", async () => {
const socket = new WebSocket(
const socket = createWebSocket(
`${WS_URL}/socket.io/?EIO=4&transport=websocket`
);
@@ -172,7 +191,7 @@ describe("Engine.IO protocol", () => {
});
it("should fail with an invalid 'EIO' query parameter", async () => {
const socket = new WebSocket(
const socket = createWebSocket(
`${WS_URL}/socket.io/?transport=websocket`
);
@@ -180,9 +199,9 @@ describe("Engine.IO protocol", () => {
socket.on("error", () => {});
}
waitFor(socket, "close");
await waitFor(socket, "close");
const socket2 = new WebSocket(
const socket2 = createWebSocket(
`${WS_URL}/socket.io/?EIO=abc&transport=websocket`
);
@@ -190,19 +209,19 @@ describe("Engine.IO protocol", () => {
socket2.on("error", () => {});
}
waitFor(socket2, "close");
await waitFor(socket2, "close");
});
it("should fail with an invalid 'transport' query parameter", async () => {
const socket = new WebSocket(`${WS_URL}/socket.io/?EIO=4`);
const socket = createWebSocket(`${WS_URL}/socket.io/?EIO=4`);
if (isNodejs) {
socket.on("error", () => {});
}
waitFor(socket, "close");
await waitFor(socket, "close");
const socket2 = new WebSocket(
const socket2 = createWebSocket(
`${WS_URL}/socket.io/?EIO=4&transport=abc`
);
@@ -210,7 +229,7 @@ describe("Engine.IO protocol", () => {
socket2.on("error", () => {});
}
waitFor(socket2, "close");
await waitFor(socket2, "close");
});
});
});
@@ -260,7 +279,7 @@ describe("Engine.IO protocol", () => {
describe("WebSocket", () => {
it("should send ping/pong packets", async () => {
const socket = new WebSocket(
const socket = createWebSocket(
`${WS_URL}/socket.io/?EIO=4&transport=websocket`
);
@@ -278,7 +297,7 @@ describe("Engine.IO protocol", () => {
});
it("should close the session upon ping timeout", async () => {
const socket = new WebSocket(
const socket = createWebSocket(
`${WS_URL}/socket.io/?EIO=4&transport=websocket`
);
@@ -316,7 +335,7 @@ describe("Engine.IO protocol", () => {
describe("WebSocket", () => {
it("should forcefully close the session", async () => {
const socket = new WebSocket(
const socket = createWebSocket(
`${WS_URL}/socket.io/?EIO=4&transport=websocket`
);
@@ -333,7 +352,7 @@ describe("Engine.IO protocol", () => {
it("should successfully upgrade from HTTP long-polling to WebSocket", async () => {
const sid = await initLongPollingSession();
const socket = new WebSocket(
const socket = createWebSocket(
`${WS_URL}/socket.io/?EIO=4&transport=websocket&sid=${sid}`
);
@@ -353,12 +372,13 @@ describe("Engine.IO protocol", () => {
it("should ignore HTTP requests with same sid after upgrade", async () => {
const sid = await initLongPollingSession();
const socket = new WebSocket(
const socket = createWebSocket(
`${WS_URL}/socket.io/?EIO=4&transport=websocket&sid=${sid}`
);
await waitFor(socket, "open");
socket.send("2probe");
await waitFor(socket, "message"); // "3probe"
socket.send("5");
const pollResponse = await fetch(
@@ -371,15 +391,16 @@ describe("Engine.IO protocol", () => {
it("should ignore WebSocket connection with same sid after upgrade", async () => {
const sid = await initLongPollingSession();
const socket = new WebSocket(
const socket = createWebSocket(
`${WS_URL}/socket.io/?EIO=4&transport=websocket&sid=${sid}`
);
await waitFor(socket, "open");
socket.send("2probe");
await waitFor(socket, "message"); // "3probe"
socket.send("5");
const socket2 = new WebSocket(
const socket2 = createWebSocket(
`${WS_URL}/socket.io/?EIO=4&transport=websocket&sid=${sid}`
);
@@ -391,7 +412,7 @@ describe("Engine.IO protocol", () => {
describe("Socket.IO protocol", () => {
describe("connect", () => {
it("should allow connection to the main namespace", async () => {
const socket = new WebSocket(
const socket = createWebSocket(
`${WS_URL}/socket.io/?EIO=4&transport=websocket`
);
@@ -414,7 +435,7 @@ describe("Socket.IO protocol", () => {
});
it("should allow connection to the main namespace with a payload", async () => {
const socket = new WebSocket(
const socket = createWebSocket(
`${WS_URL}/socket.io/?EIO=4&transport=websocket`
);
@@ -437,7 +458,7 @@ describe("Socket.IO protocol", () => {
});
it("should allow connection to a custom namespace", async () => {
const socket = new WebSocket(
const socket = createWebSocket(
`${WS_URL}/socket.io/?EIO=4&transport=websocket`
);
@@ -460,7 +481,7 @@ describe("Socket.IO protocol", () => {
});
it("should allow connection to a custom namespace with a payload", async () => {
const socket = new WebSocket(
const socket = createWebSocket(
`${WS_URL}/socket.io/?EIO=4&transport=websocket`
);
@@ -483,7 +504,7 @@ describe("Socket.IO protocol", () => {
});
it("should disallow connection to an unknown namespace", async () => {
const socket = new WebSocket(
const socket = createWebSocket(
`${WS_URL}/socket.io/?EIO=4&transport=websocket`
);
@@ -497,7 +518,7 @@ describe("Socket.IO protocol", () => {
});
it("should disallow connection with an invalid handshake", async () => {
const socket = new WebSocket(
const socket = createWebSocket(
`${WS_URL}/socket.io/?EIO=4&transport=websocket`
);
@@ -508,10 +529,9 @@ describe("Socket.IO protocol", () => {
await waitFor(socket, "close");
});
it("should close the connection if no handshake is received", async () => {
const socket = new WebSocket(
`${WS_URL}/socket.io/?EIO=4&transport=websocket`
const socket = createWebSocket(
`${WS_URL}/socket.io/?EIO=4&transport=websocket`
);
await waitFor(socket, "close");

View File

@@ -7,7 +7,7 @@
"prettier": "^2.8.4",
"rollup": "^3.20.2",
"socket.io": "^4.6.1",
"ws": "^8.13.0"
"ws": "^8.18.3"
},
"scripts": {
"bundle": "rollup -c",

View File

@@ -0,0 +1,19 @@
# Example with `@socket.io/cluster-engine` and Node.js cluster
## How to use
```bash
# run the server
$ node server.js
# run the client
$ node client.js
```
## Explanation
The `server.js` script will create one Socket.IO server per core, each listening on the same port (`3000`).
With the default engine (provided by the `engine.io` package), sticky sessions would be required, so that each HTTP request of the same Engine.IO session reaches the same worker.
The `NodeClusterEngine` is a custom engine which takes care of the synchronization between the servers by using [the IPC channel](https://nodejs.org/api/cluster.html#workersendmessage-sendhandle-options-callback) and removes the need for sticky sessions when scaling horizontally.

View File

@@ -0,0 +1,26 @@
import { io } from "socket.io-client";
const CLIENTS_COUNT = 3;
for (let i = 0; i < CLIENTS_COUNT; i++) {
const socket = io("ws://localhost:3000/", {
// transports: ["polling"],
// transports: ["websocket"],
});
socket.on("connect", () => {
console.log(`connected as ${socket.id}`);
});
socket.on("disconnect", (reason) => {
console.log(`disconnected due to ${reason}`);
});
socket.on("hello", (socketId, workerId) => {
console.log(`received "hello" from ${socketId} (worker: ${workerId})`);
});
setInterval(() => {
socket.emit("hello");
}, 2000);
}

View File

@@ -0,0 +1,12 @@
{
"private": true,
"name": "cluster-engine-node-cluster",
"version": "0.0.1",
"type": "module",
"dependencies": {
"@socket.io/cluster-adapter": "^0.2.2",
"@socket.io/cluster-engine": "^0.1.0",
"socket.io": "^4.7.5",
"socket.io-client": "^4.7.5"
}
}

View File

@@ -0,0 +1,63 @@
import cluster from "node:cluster";
import process from "node:process";
import { availableParallelism } from "node:os";
import {
setupPrimary as setupPrimaryEngine,
NodeClusterEngine,
} from "@socket.io/cluster-engine";
import {
setupPrimary as setupPrimaryAdapter,
createAdapter,
} from "@socket.io/cluster-adapter";
import { createServer } from "node:http";
import { Server } from "socket.io";
if (cluster.isPrimary) {
console.log(`Primary ${process.pid} is running`);
const numCPUs = availableParallelism();
// fork workers
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
setupPrimaryEngine();
setupPrimaryAdapter();
// needed for packets containing Buffer objects (you can ignore it if you only send plaintext objects)
cluster.setupPrimary({
serialization: "advanced",
});
cluster.on("exit", (worker, code, signal) => {
console.log(`worker ${worker.process.pid} died`);
});
} else {
const httpServer = createServer((req, res) => {
res.writeHead(404).end();
});
const engine = new NodeClusterEngine();
engine.attach(httpServer, {
path: "/socket.io/",
});
const io = new Server({
adapter: createAdapter(),
});
io.bind(engine);
io.on("connection", (socket) => {
socket.on("hello", () => {
socket.broadcast.emit("hello", socket.id, process.pid);
});
});
// workers will share the same port
httpServer.listen(3000);
console.log(`Worker ${process.pid} started`);
}

View File

@@ -0,0 +1,22 @@
# Example with `@socket.io/cluster-engine` and Redis
## How to use
```bash
# start the redis server
$ docker compose up -d
# run the server
$ node server.js
# run the client
$ node client.js
```
## Explanation
The `server.js` script will create 3 Socket.IO servers, each listening on a distinct port (`3001`, `3002` and `3003`), and a proxy server listening on port `3000` which randomly redirects to one of those servers.
With the default engine (provided by the `engine.io` package), sticky sessions would be required, so that each HTTP request of the same Engine.IO session reaches the same server.
The `RedisEngine` is a custom engine which takes care of the synchronization between the servers by using [Redis pub/sub](https://redis.io/docs/latest/develop/interact/pubsub/) and removes the need for sticky sessions when scaling horizontally.

View File

@@ -0,0 +1,26 @@
import { io } from "socket.io-client";
const CLIENTS_COUNT = 3;
for (let i = 0; i < CLIENTS_COUNT; i++) {
const socket = io("ws://localhost:3000/", {
// transports: ["polling"],
// transports: ["websocket"],
});
socket.on("connect", () => {
console.log(`connected as ${socket.id}`);
});
socket.on("disconnect", (reason) => {
console.log(`disconnected due to ${reason}`);
});
socket.on("hello", (socketId, workerId) => {
console.log(`received "hello" from ${socketId} (worker: ${workerId})`);
});
setInterval(() => {
socket.emit("hello");
}, 2000);
}

View File

@@ -0,0 +1,5 @@
services:
redis:
image: redis:7
ports:
- "6379:6379"

View File

@@ -0,0 +1,14 @@
{
"private": true,
"name": "cluster-engine-redis",
"version": "0.0.1",
"type": "module",
"dependencies": {
"@socket.io/cluster-engine": "^0.1.0",
"@socket.io/redis-adapter": "^8.3.0",
"http-proxy": "^1.18.1",
"redis": "^4.6.15",
"socket.io": "^4.7.5",
"socket.io-client": "^4.7.5"
}
}

View File

@@ -0,0 +1,65 @@
import { RedisEngine } from "@socket.io/cluster-engine";
import { createServer } from "node:http";
import { createClient } from "redis";
import { Server } from "socket.io";
import { createAdapter } from "@socket.io/redis-adapter";
import proxyModule from "http-proxy";
const { createProxyServer } = proxyModule;
async function initServer(port) {
const httpServer = createServer((req, res) => {
res.writeHead(404).end();
});
const pubClient = createClient();
const subClient = pubClient.duplicate();
await Promise.all([pubClient.connect(), subClient.connect()]);
const engine = new RedisEngine(pubClient, subClient);
engine.attach(httpServer, {
path: "/socket.io/",
});
const io = new Server({
adapter: createAdapter(pubClient, subClient),
});
io.bind(engine);
io.on("connection", (socket) => {
socket.on("hello", () => {
socket.broadcast.emit("hello", socket.id, port);
});
});
httpServer.listen(port);
}
function initProxy() {
const proxy = createProxyServer();
function randomTarget() {
return [
"http://localhost:3001",
"http://localhost:3002",
"http://localhost:3003",
][Math.floor(Math.random() * 3)];
}
const httpServer = createServer((req, res) => {
proxy.web(req, res, { target: randomTarget() });
});
httpServer.on("upgrade", function (req, socket, head) {
proxy.ws(req, socket, head, { target: randomTarget() });
});
httpServer.listen(3000);
}
await Promise.all([initServer(3001), initServer(3002), initServer(3003)]);
initProxy();

View File

@@ -0,0 +1,25 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
tsconfigRootDir: __dirname,
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
root: true,
env: {
node: true,
jest: true,
},
ignorePatterns: ['.eslintrc.js'],
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
};

56
examples/nestjs-example/.gitignore vendored Normal file
View File

@@ -0,0 +1,56 @@
# compiled output
/dist
/node_modules
/build
# Logs
logs
*.log
npm-debug.log*
pnpm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# OS
.DS_Store
# Tests
/coverage
/.nyc_output
# 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
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# temp directory
.temp
.tmp
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

View File

@@ -0,0 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}

View File

@@ -0,0 +1,73 @@
<p align="center">
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="200" alt="Nest Logo" /></a>
</p>
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
[circleci-url]: https://circleci.com/gh/nestjs/nest
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
<p align="center">
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
<a href="https://coveralls.io/github/nestjs/nest?branch=master" target="_blank"><img src="https://coveralls.io/repos/github/nestjs/nest/badge.svg?branch=master#9" alt="Coverage" /></a>
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg"/></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow"></a>
</p>
<!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
[![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](https://opencollective.com/nest#sponsor)-->
## Description
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
## Installation
```bash
$ npm install
```
## Running the app
```bash
# development
$ npm run start
# watch mode
$ npm run start:dev
# production mode
$ npm run start:prod
```
## Test
```bash
# unit tests
$ npm run test
# e2e tests
$ npm run test:e2e
# test coverage
$ npm run test:cov
```
## Support
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
## Stay in touch
- Author - [Kamil Myśliwiec](https://kamilmysliwiec.com)
- Website - [https://nestjs.com](https://nestjs.com/)
- Twitter - [@nestframework](https://twitter.com/nestframework)
## License
Nest is [MIT licensed](LICENSE).

View File

@@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}

View File

@@ -0,0 +1,72 @@
{
"name": "nestjs-example",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@nestjs/common": "^10.0.0",
"@nestjs/core": "^10.0.0",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/platform-socket.io": "^10.3.10",
"@nestjs/websockets": "^10.3.10",
"hbs": "^4.2.0",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1"
},
"devDependencies": {
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
"@nestjs/testing": "^10.0.0",
"@types/express": "^4.17.17",
"@types/jest": "^29.5.2",
"@types/node": "^20.3.1",
"@types/supertest": "^6.0.0",
"@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^7.0.0",
"eslint": "^8.42.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"jest": "^29.5.0",
"prettier": "^3.0.0",
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",
"ts-jest": "^29.1.0",
"ts-loader": "^9.4.3",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.1.3"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}

View File

@@ -0,0 +1,22 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';
describe('AppController', () => {
let appController: AppController;
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService],
}).compile();
appController = app.get<AppController>(AppController);
});
describe('root', () => {
it('should return "Hello World!"', () => {
expect(appController.getHello()).toBe('Hello World!');
});
});
});

View File

@@ -0,0 +1,13 @@
import { Controller, Get, Render } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
@Render('index')
root() {
return { message: 'Hello world2!' };
}
}

View File

@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { EventsModule } from './events/events.module';
@Module({
imports: [EventsModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

View File

@@ -0,0 +1,8 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}

View File

@@ -0,0 +1,18 @@
import {
MessageBody,
SubscribeMessage,
WebSocketGateway,
WebSocketServer,
} from '@nestjs/websockets';
import { Server } from 'socket.io';
@WebSocketGateway({})
export class EventsGateway {
@WebSocketServer()
io: Server;
@SubscribeMessage('hello')
handleEvent(@MessageBody() data: string): string {
return data.split('').reverse().join('');
}
}

View File

@@ -0,0 +1,7 @@
import { Module } from '@nestjs/common';
import { EventsGateway } from './events.gateway';
@Module({
providers: [EventsGateway],
})
export class EventsModule {}

View File

@@ -0,0 +1,15 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { NestExpressApplication } from '@nestjs/platform-express';
import { join } from 'node:path';
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
app.setBaseViewsDir(join(__dirname, '..', 'views'));
app.setViewEngine('hbs');
await app.listen(3000);
}
bootstrap();

View File

@@ -0,0 +1,24 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './../src/app.module';
describe('AppController (e2e)', () => {
let app: INestApplication;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
it('/ (GET)', () => {
return request(app.getHttpServer())
.get('/')
.expect(200)
.expect('Hello World!');
});
});

View File

@@ -0,0 +1,9 @@
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
}
}

View File

@@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}

View File

@@ -0,0 +1,21 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "ES2021",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": false,
"noImplicitAny": false,
"strictBindCallApply": false,
"forceConsistentCasingInFileNames": false,
"noFallthroughCasesInSwitch": false
}
}

View File

@@ -0,0 +1,47 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>App</title>
</head>
<body>
<p>Status: <span id="status"></span></p>
<p>Transport: <span id="transport"></span></p>
<script src="/socket.io/socket.io.js"></script>
<script>
const statusSpan = document.getElementById("status");
const transportSpan = document.getElementById("transport");
const socket = io({
// transports: ["polling"],
// transports: ["websocket"],
});
statusSpan.innerText = "Disconnected";
transportSpan.innerText = "N/A";
socket.on("connect", () => {
statusSpan.innerText = "Connected";
transportSpan.innerText = socket.io.engine.transport.name;
socket.io.engine.on("upgrade", (transport) => {
transportSpan.innerText = transport.name;
});
console.log(`connect ${socket.id}`);
socket.emit("hello", "world", (val) => {
console.log(`got ${val}`);
});
});
socket.on("connect_error", (err) => {
console.log(`connect_error due to ${err.message}`);
});
socket.on("disconnect", (reason) => {
statusSpan.innerText = "Disconnected";
transportSpan.innerText = "N/A";
console.log(`disconnect due to ${reason}`);
});
</script>
</body>
</html>

View File

@@ -20,17 +20,18 @@ export default defineNitroPlugin((nitroApp: NitroApp) => {
},
websocket: {
open(peer) {
const nodeContext = peer.ctx.node;
const req = nodeContext.req;
// crossws >= 0.3.0
// @ts-expect-error private method and property
engine.prepare(peer._internal.nodeReq);
// @ts-expect-error private method and property
engine.onWebSocket(peer._internal.nodeReq, peer._internal.nodeReq.socket, peer.websocket);
// @ts-expect-error private method
engine.prepare(req);
const rawSocket = nodeContext.req.socket;
const websocket = nodeContext.ws;
// @ts-expect-error private method
engine.onWebSocket(req, rawSocket, websocket);
// crossws < 0.3.0
// const context = peer.ctx.node;
// // @ts-expect-error private method
// engine.prepare(context.req);
// // @ts-expect-error private method
// engine.onWebSocket(context.req, context.req.socket, context.ws);
}
}
}));

View File

@@ -0,0 +1,25 @@
# Example with `@socket.io/postgres-adapter`
**Table of contents**
<!-- TOC -->
* [How to use](#how-to-use)
* [Documentation](#documentation)
<!-- TOC -->
## How to use
```bash
# start the postgres server
$ docker compose up -d
# run the cluster
$ node cluster.js
# run the client
$ node client.js
```
## Documentation
The documentation can be found here: https://socket.io/docs/v4/postgres-adapter/

View File

@@ -0,0 +1,31 @@
import { io } from "socket.io-client";
const CLIENTS_COUNT = 3;
const PORTS = [3000, 3001, 3002];
for (let i = 0; i < CLIENTS_COUNT; i++) {
const socket = io(`ws://localhost:${PORTS[i % 3]}`, {
// transports: ["polling"],
// transports: ["websocket"],
});
socket.on("connect", () => {
console.log(`connected as ${socket.id}`);
});
socket.on("connect_error", () => {
console.log(`connect_error`);
});
socket.on("disconnect", (reason) => {
console.log(`disconnected due to ${reason}`);
});
socket.on("hello", (socketId, pid) => {
console.log(`received "hello" from ${socketId} (process: ${pid})`);
});
setInterval(() => {
socket.emit("hello");
}, 2000);
}

View File

@@ -0,0 +1,13 @@
import cluster from 'node:cluster';
const SERVERS_COUNT = 3;
cluster.setupPrimary({
exec: 'server.js',
});
for (let i = 0; i < SERVERS_COUNT; i++) {
cluster.fork({
PORT: 3000 + i
});
}

View File

@@ -0,0 +1,7 @@
services:
postgres:
image: postgres:14
ports:
- "5432:5432"
environment:
POSTGRES_PASSWORD: "changeit"

View File

@@ -0,0 +1,12 @@
{
"private": true,
"name": "postgres-adapter-example",
"version": "0.0.1",
"type": "module",
"dependencies": {
"@socket.io/postgres-adapter": "^0.4.0",
"pg": "^8.12.0",
"socket.io": "^4.7.5",
"socket.io-client": "^4.7.5"
}
}

View File

@@ -0,0 +1,40 @@
import { Server } from "socket.io";
import { createAdapter } from "@socket.io/postgres-adapter";
import pg from "pg";
import process from "node:process";
const PORT = process.env.PORT || 3000;
const pool = new pg.Pool({
user: "postgres",
host: "localhost",
database: "postgres",
password: "changeit",
port: 5432,
});
await pool.query(`
CREATE TABLE IF NOT EXISTS socket_io_attachments (
id bigserial UNIQUE,
created_at timestamptz DEFAULT NOW(),
payload bytea
);
`);
pool.on("error", (err) => {
console.error("Postgres error", err);
});
const io = new Server({
adapter: createAdapter(pool)
});
io.on("connection", (socket) => {
socket.on("hello", () => {
// send to anyone except the sender
socket.broadcast.emit("hello", socket.id, process.pid);
});
});
io.listen(PORT);
console.log(`server listening on port ${PORT}`);

View File

@@ -0,0 +1,30 @@
import { io, type Socket } from "socket.io-client";
interface ServerToClientEvents {
hello: (val: string) => void;
}
interface ClientToServerEvents {
ping: (cb: () => void) => void;
}
const socket: Socket<ServerToClientEvents, ClientToServerEvents> = io("ws://localhost:8080/");
socket.on("connect", () => {
console.log(`connect ${socket.id}`);
});
socket.on("hello", (val) => {
console.log(`got ${val}`);
});
socket.on("disconnect", () => {
console.log(`disconnect`);
});
setInterval(() => {
const start = Date.now();
socket.emit("ping", () => {
console.log(`pong (latency: ${Date.now() - start} ms)`);
});
}, 1000);

View File

@@ -0,0 +1,17 @@
{
"name": "typescript-client-example-cjs",
"version": "0.0.1",
"description": "An example with TypeScript",
"type": "commonjs",
"private": true,
"scripts": {
"build": "tsc",
"start": "ts-node client.ts"
},
"license": "MIT",
"dependencies": {
"socket.io-client": "^4.8.0",
"ts-node": "^10.9.2",
"typescript": "^5.4.5"
}
}

View File

@@ -0,0 +1,9 @@
{
"compilerOptions": {
"outDir": "dist",
"target": "es2022",
"module": "nodenext",
"moduleResolution": "nodenext",
"strict": true
}
}

View File

@@ -0,0 +1,30 @@
import { io, type Socket } from "socket.io-client";
interface ServerToClientEvents {
hello: (val: string) => void;
}
interface ClientToServerEvents {
ping: (cb: () => void) => void;
}
const socket: Socket<ServerToClientEvents, ClientToServerEvents> = io("ws://localhost:8080/");
socket.on("connect", () => {
console.log(`connect ${socket.id}`);
});
socket.on("hello", (val) => {
console.log(`got ${val}`);
});
socket.on("disconnect", () => {
console.log(`disconnect`);
});
setInterval(() => {
const start = Date.now();
socket.emit("ping", () => {
console.log(`pong (latency: ${Date.now() - start} ms)`);
});
}, 1000);

View File

@@ -0,0 +1,17 @@
{
"name": "typescript-client-example-esm",
"version": "0.0.1",
"description": "An example with TypeScript",
"type": "module",
"private": true,
"scripts": {
"build": "tsc",
"start": "node --no-warnings=ExperimentalWarning --loader ts-node/esm client.ts"
},
"license": "MIT",
"dependencies": {
"socket.io-client": "^4.8.0",
"ts-node": "^10.9.2",
"typescript": "^5.4.5"
}
}

View File

@@ -0,0 +1,9 @@
{
"compilerOptions": {
"outDir": "dist",
"target": "es2022",
"module": "esnext",
"moduleResolution": "node",
"strict": true
}
}

5447
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,35 +1,44 @@
{
"name": "socket.io",
"private": true,
"workspaces": [
"packages/socket.io-component-emitter",
"packages/engine.io-parser",
"packages/engine.io",
"packages/socket.io-cluster-engine",
"packages/engine.io-client",
"packages/socket.io-adapter",
"packages/socket.io-cluster-adapter",
"packages/socket.io-parser",
"packages/socket.io-client",
"packages/socket.io"
"packages/socket.io",
"packages/socket.io-postgres-emitter",
"packages/socket.io-redis-streams-emitter"
],
"overrides": {
"@types/estree": "0.0.52",
"@types/lodash": "4.14.189",
"ws": "8.17.1"
"ws": "8.18.3"
},
"devDependencies": {
"@babel/core": "^7.24.7",
"@babel/plugin-transform-object-assign": "^7.24.7",
"@babel/preset-env": "^7.24.7",
"@babel/register": "^7.24.6",
"@fails-components/webtransport": "^0.1.7",
"@fails-components/webtransport": "^1.1.4",
"@fails-components/webtransport-transport-http3-quiche": "^1.1.4",
"@rollup/plugin-alias": "^5.1.0",
"@rollup/plugin-babel": "^6.0.4",
"@rollup/plugin-commonjs": "^26.0.1",
"@rollup/plugin-node-resolve": "^15.2.3",
"@sinonjs/fake-timers": "^11.2.2",
"@socket.io/postgres-adapter": "^0.1.0",
"@socket.io/redis-streams-adapter": "~0.2.2",
"@types/debug": "^4.1.12",
"@types/expect.js": "^0.3.32",
"@types/mocha": "^10.0.7",
"@types/node": "18.15.3",
"@types/pg": "^8.6.0",
"@types/sinonjs__fake-timers": "^8.1.5",
"@wdio/cli": "^8.39.1",
"@wdio/local-runner": "^8.39.1",
@@ -40,6 +49,7 @@
"base64-arraybuffer": "^1.0.2",
"benchmark": "^2.1.4",
"blob": "^0.1.0",
"cookie": "~0.7.2",
"eiows": "^7.1.0",
"engine.io-client-v3": "npm:engine.io-client@^3.5.2",
"expect.js": "^0.3.1",
@@ -47,10 +57,13 @@
"express-session": "^1.18.0",
"has-cors": "^1.1.0",
"helmet": "^7.1.0",
"ioredis": "^5.4.1",
"mocha": "^10.6.0",
"node-forge": "^1.3.1",
"nyc": "^17.0.0",
"prettier": "^2.8.8",
"pg": "^8.6.0",
"prettier": "^3.3.2",
"redis": "^4.6.15",
"rimraf": "^6.0.0",
"rollup": "^2.79.1",
"rollup-plugin-terser": "^7.0.2",
@@ -63,8 +76,9 @@
"ts-loader": "^9.5.1",
"ts-node": "^10.9.2",
"tsd": "^0.31.1",
"tsx": "~4.20.6",
"typescript": "^5.5.3",
"uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.30.0",
"uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.56.0",
"wdio-geckodriver-service": "^5.0.2"
}
}

View File

@@ -1,7 +1,11 @@
# History
# Changelog
| Version | Release date | Bundle size (UMD min+gzip) |
|-------------------------------------------------------------------------------------------------------------|----------------|----------------------------|
| [6.6.4](#664-2025-12-23) | December 2025 | `8.7 KB` |
| [6.6.3](#663-2025-01-23) | January 2025 | `8.7 KB` |
| [6.6.2](#662-2024-10-23) | October 2024 | `8.7 KB` |
| [6.6.1](#661-2024-09-21) | September 2024 | `8.7 KB` |
| [6.6.0](#660-2024-06-21) | June 2024 | `8.6 KB` |
| [6.5.4](#654-2024-06-18) (from the [6.5.x](https://github.com/socketio/engine.io-client/tree/6.5.x) branch) | June 2024 | `8.8 KB` |
| [3.5.4](#354-2024-06-18) (from the [3.5.x](https://github.com/socketio/engine.io-client/tree/3.5.x) branch) | June 2024 | `-` |
@@ -36,7 +40,74 @@
| [4.1.1](#411-2021-02-02) | February 2021 | `9.1 KB` |
| [4.1.0](#410-2021-01-14) | January 2021 | `9.1 KB` |
# Release notes
## [6.6.4](https://github.com/socketio/socket.io/compare/engine.io-client@6.6.3...engine.io-client@6.6.4) (2025-12-23)
This release contains a bump of:
- `ws` from `~8.17.1` to `~8.18.3`
- `debug` from `~4.3.1` to `~4.4.1`
### Bug Fixes
* properly handle port option ([#5241](https://github.com/socketio/socket.io/issues/5241)) ([1da9cdd](https://github.com/socketio/socket.io/commit/1da9cddeab0bf5ce41890d156d73af8194cef656))
### Dependencies
- [`ws@~8.18.3`](https://github.com/websockets/ws/releases/tag/8.18.3) ([diff](https://github.com/websockets/ws/compare/8.17.1...8.18.3))
## [6.6.3](https://github.com/socketio/socket.io/compare/engine.io-client@6.6.2...engine.io-client@6.6.3) (2025-01-23)
### Bug Fixes
* correctly consume the `ws` package ([#5220](https://github.com/socketio/socket.io/issues/5220)) ([7fcddcb](https://github.com/socketio/socket.io/commit/7fcddcb3bbd236b46aa8fee6f4ce6c45afb7b03a))
### Dependencies
- [`ws@~8.17.1`](https://github.com/websockets/ws/releases/tag/8.17.1) (no change)
## [6.6.2](https://github.com/socketio/socket.io/compare/engine.io-client@6.6.1...engine.io-client@6.6.2) (2024-10-23)
### Bug Fixes
* **types:** remove ws type from .d.ts file ([175a2c5](https://github.com/socketio/socket.io/commit/175a2c58c1bc37eb9b87f87df47e1f9388b01d55))
* prevent infinite loop with Node.js built-in WebSocket ([4865f2e](https://github.com/socketio/socket.io/commit/4865f2e62eff9cf59f602e753d9f84159a3139af))
### Dependencies
- [`ws@~8.17.1`](https://github.com/websockets/ws/releases/tag/8.17.1) (no change)
## [6.6.1](https://github.com/socketio/socket.io/compare/engine.io-client@6.6.0...engine.io-client@6.6.1) (2024-09-21)
### Bug Fixes
* move 'offline' event listener at the top ([8a2f5a3](https://github.com/socketio/socket.io/commit/8a2f5a3da0addb386e7a0f4970e1a9696b82797e))
* only remove the event listener if it exists ([9b3c9ab](https://github.com/socketio/socket.io/commit/9b3c9abecab028822357beb6e2b502f548e312eb)), closes [/github.com/socketio/socket.io/issues/5088#issuecomment-2217202350](https://github.com//github.com/socketio/socket.io/issues/5088/issues/issuecomment-2217202350)
* do not send a packet on an expired connection ([#5134](https://github.com/socketio/socket.io/issues/5134)) ([8adcfbf](https://github.com/socketio/socket.io/commit/8adcfbfde50679095ec2abe376650cf2b6814325))
### Performance Improvements
* do not reset the heartbeat timer on each packet ([7a23dde](https://github.com/socketio/socket.io/commit/7a23dde6efff3079edeeda951fe0ee25516da833))
### Dependencies
- [`ws@~8.17.1`](https://github.com/websockets/ws/releases/tag/8.17.1) (no change)
## [6.6.0](https://github.com/socketio/engine.io-client/compare/6.5.3...6.6.0) (2024-06-21)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -55,7 +55,7 @@ export function parse(setCookieString: string): Cookie {
case "Max-Age":
const expiration = new Date();
expiration.setUTCSeconds(
expiration.getUTCSeconds() + parseInt(value, 10)
expiration.getUTCSeconds() + parseInt(value, 10),
);
cookie.expires = expiration;
break;

View File

@@ -10,11 +10,34 @@ import {
CookieJar,
createCookieJar,
defaultBinaryType,
nextTick,
} from "./globals.node.js";
import debugModule from "debug"; // debug()
const debug = debugModule("engine.io-client:socket"); // debug()
const withEventListeners =
typeof addEventListener === "function" &&
typeof removeEventListener === "function";
const OFFLINE_EVENT_LISTENERS = [];
if (withEventListeners) {
// within a ServiceWorker, any event handler for the 'offline' event must be added on the initial evaluation of the
// script, so we create one single event listener here which will forward the event to the socket instances
addEventListener(
"offline",
() => {
debug(
"closing %d connection(s) because the network was lost",
OFFLINE_EVENT_LISTENERS.length,
);
OFFLINE_EVENT_LISTENERS.forEach((listener) => listener());
},
false,
);
}
export interface SocketOptions {
/**
* The host that we're connecting to. Set from the URI passed when connecting
@@ -85,7 +108,9 @@ export interface SocketOptions {
*
* @default ['polling','websocket', 'webtransport']
*/
transports?: string[] | TransportCtor[];
transports?:
| ("polling" | "websocket" | "webtransport" | string)[]
| TransportCtor[];
/**
* Whether all the transports should be tested, instead of just the first one.
@@ -318,6 +343,11 @@ export class SocketWithoutUpgrade extends Emitter<
private _pingTimeout: number = -1;
private _maxPayload?: number = -1;
private _pingTimeoutTimer: NodeJS.Timer;
/**
* The expiration timestamp of the {@link _pingTimeoutTimer} object is tracked, in case the timer is throttled and the
* callback is not fired on time. This can happen for example when a laptop is suspended or when a phone is locked.
*/
private _pingTimeoutTime = Infinity;
private clearTimeoutFn: typeof clearTimeout;
private readonly _beforeunloadEventListener: () => void;
private readonly _offlineEventListener: () => void;
@@ -379,8 +409,8 @@ export class SocketWithoutUpgrade extends Emitter<
(typeof location !== "undefined" && location.port
? location.port
: this.secure
? "443"
: "80");
? "443"
: "80");
this.transports = [];
this._transportsByName = {};
@@ -406,7 +436,7 @@ export class SocketWithoutUpgrade extends Emitter<
transportOptions: {},
closeOnBeforeunload: false,
},
opts
opts,
);
this.opts.path =
@@ -417,7 +447,7 @@ export class SocketWithoutUpgrade extends Emitter<
this.opts.query = decode(this.opts.query);
}
if (typeof addEventListener === "function") {
if (withEventListeners) {
if (this.opts.closeOnBeforeunload) {
// Firefox closes the connection when the "beforeunload" event is emitted but not Chrome. This event listener
// ensures every browser behaves the same (no "disconnect" event at the Socket.IO level when the page is
@@ -432,16 +462,17 @@ export class SocketWithoutUpgrade extends Emitter<
addEventListener(
"beforeunload",
this._beforeunloadEventListener,
false
false,
);
}
if (this.hostname !== "localhost") {
debug("adding listener for the 'offline' event");
this._offlineEventListener = () => {
this._onClose("transport close", {
description: "network connection lost",
});
};
addEventListener("offline", this._offlineEventListener, false);
OFFLINE_EVENT_LISTENERS.push(this._offlineEventListener);
}
}
@@ -482,7 +513,7 @@ export class SocketWithoutUpgrade extends Emitter<
secure: this.secure,
port: this.port,
},
this.opts.transportOptions[name]
this.opts.transportOptions[name],
);
debug("options: %j", opts);
@@ -572,7 +603,6 @@ export class SocketWithoutUpgrade extends Emitter<
// Socket is live - any packet counts
this.emitReserved("heartbeat");
this._resetPingTimeout();
switch (packet.type) {
case "open":
@@ -583,6 +613,7 @@ export class SocketWithoutUpgrade extends Emitter<
this._sendPacket("pong");
this.emitReserved("ping");
this.emitReserved("pong");
this._resetPingTimeout();
break;
case "error":
@@ -628,9 +659,11 @@ export class SocketWithoutUpgrade extends Emitter<
*/
private _resetPingTimeout() {
this.clearTimeoutFn(this._pingTimeoutTimer);
const delay = this._pingInterval + this._pingTimeout;
this._pingTimeoutTime = Date.now() + delay;
this._pingTimeoutTimer = this.setTimeoutFn(() => {
this._onClose("ping timeout");
}, this._pingInterval + this._pingTimeout);
}, delay);
if (this.opts.autoUnref) {
this._pingTimeoutTimer.unref();
}
@@ -708,6 +741,31 @@ export class SocketWithoutUpgrade extends Emitter<
return this.writeBuffer;
}
/**
* Checks whether the heartbeat timer has expired but the socket has not yet been notified.
*
* Note: this method is private for now because it does not really fit the WebSocket API, but if we put it in the
* `write()` method then the message would not be buffered by the Socket.IO client.
*
* @return {boolean}
* @private
*/
/* private */ _hasPingExpired() {
if (!this._pingTimeoutTime) return true;
const hasExpired = Date.now() > this._pingTimeoutTime;
if (hasExpired) {
debug("throttled timer detected, scheduling connection close");
this._pingTimeoutTime = 0;
nextTick(() => {
this._onClose("ping timeout");
}, this.setTimeoutFn);
}
return hasExpired;
}
/**
* Sends a message.
*
@@ -747,7 +805,7 @@ export class SocketWithoutUpgrade extends Emitter<
type: PacketType,
data?: RawData,
options?: WriteOptions,
fn?: () => void
fn?: () => void,
) {
if ("function" === typeof data) {
fn = data;
@@ -868,13 +926,21 @@ export class SocketWithoutUpgrade extends Emitter<
// ignore further transport communication
this.transport.removeAllListeners();
if (typeof removeEventListener === "function") {
removeEventListener(
"beforeunload",
this._beforeunloadEventListener,
false
);
removeEventListener("offline", this._offlineEventListener, false);
if (withEventListeners) {
if (this._beforeunloadEventListener) {
removeEventListener(
"beforeunload",
this._beforeunloadEventListener,
false,
);
}
if (this._offlineEventListener) {
const i = OFFLINE_EVENT_LISTENERS.indexOf(this._offlineEventListener);
if (i !== -1) {
debug("removing listener for the 'offline' event");
OFFLINE_EVENT_LISTENERS.splice(i, 1);
}
}
}
// set ready state

View File

@@ -14,7 +14,7 @@ export class TransportError extends Error {
constructor(
reason: string,
readonly description: any,
readonly context: any
readonly context: any,
) {
super(reason);
}
@@ -79,7 +79,7 @@ export abstract class Transport extends Emitter<
protected onError(reason: string, description: any, context?: any) {
super.emitReserved(
"error",
new TransportError(reason, description, context)
new TransportError(reason, description, context),
);
return this;
}
@@ -192,7 +192,7 @@ export abstract class Transport extends Emitter<
private _port() {
if (
this.opts.port &&
((this.opts.secure && Number(this.opts.port !== 443)) ||
((this.opts.secure && Number(this.opts.port) !== 443) ||
(!this.opts.secure && Number(this.opts.port) !== 80))
) {
return ":" + this.opts.port;

View File

@@ -15,12 +15,12 @@ export class XHR extends BaseXHR {
Object.assign(
opts,
{ xd: this.xd, cookieJar: this.socket?._cookieJar },
this.opts
this.opts,
);
return new Request(
(opts) => new XMLHttpRequest(opts),
this.uri(),
opts as RequestOptions
opts as RequestOptions,
);
}
}

View File

@@ -122,7 +122,7 @@ export class Request extends Emitter<
constructor(
private readonly createRequest: (opts: RequestOptions) => XMLHttpRequest,
uri: string,
opts: RequestOptions
opts: RequestOptions,
) {
super();
installTimerFunctions(this, opts);
@@ -151,7 +151,7 @@ export class Request extends Emitter<
"ca",
"ciphers",
"rejectUnauthorized",
"autoUnref"
"autoUnref",
);
opts.xdomain = !!this._opts.xd;
@@ -197,7 +197,7 @@ export class Request extends Emitter<
if (xhr.readyState === 3) {
this._opts.cookieJar?.parseCookies(
// @ts-ignore
xhr.getResponseHeader("set-cookie")
xhr.getResponseHeader("set-cookie"),
);
}
@@ -354,7 +354,7 @@ function newRequest(opts) {
if (!xdomain) {
try {
return new globalThis[["Active"].concat("Object").join("X")](
"Microsoft.XMLHTTP"
"Microsoft.XMLHTTP",
);
} catch (e) {}
}

View File

@@ -1,4 +1,4 @@
import { WebSocket } from "ws";
import * as ws from "ws";
import type { Packet, RawData } from "engine.io-parser";
import { BaseWS } from "./websocket.js";
@@ -14,8 +14,8 @@ export class WS extends BaseWS {
createSocket(
uri: string,
protocols: string | string[] | undefined,
opts: Record<string, any>
) {
opts: Record<string, any>,
): any {
if (this.socket?._cookieJar) {
opts.headers = opts.headers || {};
@@ -27,7 +27,7 @@ export class WS extends BaseWS {
opts.headers.cookie.push(`${name}=${cookie.value}`);
}
}
return new WebSocket(uri, protocols, opts);
return new ws.WebSocket(uri, protocols, opts);
}
doWrite(packet: Packet, data: RawData) {

View File

@@ -43,7 +43,7 @@ export abstract class BaseWS extends Transport {
"origin",
"maxPayload",
"family",
"checkServerIdentity"
"checkServerIdentity",
);
if (this.opts.extraHeaders) {
@@ -64,7 +64,7 @@ export abstract class BaseWS extends Transport {
abstract createSocket(
uri: string,
protocols: string | string[] | undefined,
opts: Record<string, any>
opts: Record<string, any>,
);
/**
@@ -123,6 +123,7 @@ export abstract class BaseWS extends Transport {
override doClose() {
if (typeof this.ws !== "undefined") {
this.ws.onerror = () => {};
this.ws.close();
this.ws = null;
}
@@ -166,7 +167,7 @@ export class WS extends BaseWS {
createSocket(
uri: string,
protocols: string | string[] | undefined,
opts: Record<string, any>
opts: Record<string, any>,
) {
return !isReactNative
? protocols

View File

@@ -30,7 +30,7 @@ export class WT extends Transport {
// @ts-ignore
this._transport = new WebTransport(
this.createUri("https"),
this.opts.transportOptions[this.name]
this.opts.transportOptions[this.name],
);
} catch (err) {
return this.emitReserved("error", err);
@@ -51,7 +51,7 @@ export class WT extends Transport {
this._transport.createBidirectionalStream().then((stream) => {
const decoderStream = createPacketDecoderStream(
Number.MAX_SAFE_INTEGER,
this.socket.binaryType
this.socket.binaryType,
);
const reader = stream.readable.pipeThrough(decoderStream).getReader();

View File

@@ -2,7 +2,7 @@
"name": "engine.io-client",
"description": "Client for the realtime Engine",
"license": "MIT",
"version": "6.6.0",
"version": "6.6.4",
"main": "./build/cjs/index.js",
"module": "./build/esm/index.js",
"exports": {
@@ -53,16 +53,17 @@
],
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1",
"debug": "~4.4.1",
"engine.io-parser": "~5.2.1",
"ws": "~8.17.1",
"xmlhttprequest-ssl": "~2.0.0"
"ws": "~8.18.3",
"xmlhttprequest-ssl": "~2.1.1"
},
"scripts": {
"compile": "rimraf ./build && tsc && tsc -p tsconfig.esm.json && ./postcompile.sh",
"test": "npm run format:check && npm run compile && if test \"$BROWSERS\" = \"1\" ; then npm run test:browser; else npm run test:node; fi",
"test:node": "mocha --bail --require test/support/hooks.js test/index.js test/webtransport.mjs",
"test:node-fetch": "USE_FETCH=1 npm run test:node",
"test:node-builtin-ws": "USE_BUILTIN_WS=1 npm run test:node",
"test:browser": "zuul test/index.js",
"build": "rimraf ./dist && rollup -c support/rollup.config.umd.js && rollup -c support/rollup.config.esm.js",
"bundle-size": "node support/bundle-size.js",
@@ -82,7 +83,7 @@
"homepage": "https://github.com/socketio/socket.io/tree/main/packages/engine.io-client#readme",
"repository": {
"type": "git",
"url": "https://github.com/socketio/socket.io.git"
"url": "git+https://github.com/socketio/socket.io.git"
},
"bugs": {
"url": "https://github.com/socketio/socket.io/issues"

View File

@@ -17,6 +17,19 @@ describe("connection", function () {
});
});
it("should connect to localhost (ws)", (done) => {
const socket = new Socket({
transports: ["websocket"],
});
socket.on("open", () => {
socket.on("message", (data) => {
expect(data).to.equal("hi");
socket.close();
done();
});
});
});
it("should receive multibyte utf-8 strings with polling", (done) => {
const socket = new Socket();
socket.on("open", () => {
@@ -34,12 +47,12 @@ describe("connection", function () {
const socket = new Socket();
socket.on("open", () => {
socket.send(
"\uD800\uDC00-\uDB7F\uDFFF\uDB80\uDC00-\uDBFF\uDFFF\uE000-\uF8FF"
"\uD800\uDC00-\uDB7F\uDFFF\uDB80\uDC00-\uDBFF\uDFFF\uE000-\uF8FF",
);
socket.on("message", (data) => {
if ("hi" === data) return;
expect(data).to.be(
"\uD800\uDC00-\uDB7F\uDFFF\uDB80\uDC00-\uDBFF\uDFFF\uE000-\uF8FF"
"\uD800\uDC00-\uDB7F\uDFFF\uDB80\uDC00-\uDBFF\uDFFF\uE000-\uF8FF",
);
socket.close();
done();
@@ -59,7 +72,7 @@ describe("connection", function () {
setTimeout(() => {
expect(noPacket).to.be(true);
done();
}, 1200);
}, 200);
});
});
@@ -177,7 +190,7 @@ describe("connection", function () {
setTimeout(() => {
expect(noPacket).to.be(true);
done();
}, 1200);
}, 200);
});
});

View File

@@ -5,4 +5,4 @@ const socket = new Socket("http://localhost:3000", {
setTimeout(() => {
console.log("process should not exit");
}, 500);
}, 50);

View File

@@ -10,4 +10,4 @@ socket.on("open", () => {
setTimeout(() => {
console.log("process should exit now");
}, 500);
}, 50);

View File

@@ -10,4 +10,4 @@ socket.on("open", () => {
setTimeout(() => {
console.log("process should exit now");
}, 500);
}, 50);

View File

@@ -9,4 +9,4 @@ socket.on("open", () => {
setTimeout(() => {
console.log("process should exit now");
}, 500);
}, 50);

View File

@@ -7,8 +7,6 @@ if (env.browser) {
require("./node");
}
const Blob = require("blob");
require("./engine.io-client");
require("./socket");
require("./transport");
@@ -23,6 +21,6 @@ if (typeof ArrayBuffer !== "undefined") {
}
// Blob is available in Node.js since v18, but not yet supported by the `engine.io-parser` package
if (Blob && env.browser) {
if (typeof Blob === "function" && env.browser) {
require("./blob");
}

View File

@@ -34,7 +34,7 @@ describe("node.js", () => {
isComplete = true;
process.kill();
done();
}, 1000);
}, 100);
});
});

View File

@@ -90,7 +90,7 @@ describe("Socket", function () {
socket.on("error", (err) => {
expect(err.message).to.eql(
useFetch ? "fetch read error" : "xhr poll error"
useFetch ? "fetch read error" : "xhr poll error",
);
done();
});
@@ -237,7 +237,7 @@ describe("Socket", function () {
// err.context is a XMLHttpRequest object
expect(err.context.readyState).to.eql(4);
expect(err.context.responseText).to.eql(
'{"code":1,"message":"Session ID unknown"}'
'{"code":1,"message":"Session ID unknown"}',
);
}
});
@@ -270,4 +270,30 @@ describe("Socket", function () {
});
});
});
describe("throttled timer", () => {
it("checks the state of the timer", (done) => {
const socket = new Socket();
expect(socket._hasPingExpired()).to.be(false);
socket.on("open", () => {
expect(socket._hasPingExpired()).to.be(false);
// simulate a throttled timer
socket._pingTimeoutTime = Date.now() - 1;
expect(socket._hasPingExpired()).to.be(true);
// subsequent calls should not trigger more 'close' events
expect(socket._hasPingExpired()).to.be(true);
expect(socket._hasPingExpired()).to.be(true);
});
socket.on("close", (reason) => {
expect(reason).to.eql("ping timeout");
done();
});
});
});
});

View File

@@ -35,3 +35,11 @@ if (exports.useFetch) {
const { transports, Fetch } = require("../..");
transports.polling = Fetch;
}
exports.useBuiltinWs = process.env.USE_BUILTIN_WS !== undefined;
if (exports.useBuiltinWs) {
console.warn("testing with built-in WebSocket object");
const { transports, WebSocket } = require("../..");
transports.websocket = WebSocket;
}

View File

@@ -21,7 +21,7 @@ exports.mochaHooks = {
maxHttpBufferSize: 100,
allowRequest: (req, fn) => {
const denyRequest = new URL(`http://${req.url}`).searchParams.has(
"deny"
"deny",
);
fn(null, !denyRequest);
},

View File

@@ -100,7 +100,7 @@ describe("Transport", () => {
timestampRequests: false,
});
expect(polling.uri()).to.contain(
"http://localhost:3000/engine.io?sid=test"
"http://localhost:3000/engine.io?sid=test",
);
});
@@ -116,6 +116,32 @@ describe("Transport", () => {
expect(polling.uri()).to.contain("https://localhost/engine.io?sid=test");
});
it("should generate an https uri w/o a port (string)", () => {
const polling = new eio.transports.polling({
path: "/engine.io",
hostname: "localhost",
secure: true,
query: { sid: "test" },
port: "443",
timestampRequests: false,
});
expect(polling.uri()).to.contain("https://localhost/engine.io?sid=test");
});
it("should generate an https uri with a port", () => {
const polling = new eio.transports.polling({
path: "/engine.io",
hostname: "localhost",
secure: true,
query: { sid: "test" },
port: 8443,
timestampRequests: false,
});
expect(polling.uri()).to.contain(
"https://localhost:8443/engine.io?sid=test",
);
});
it("should generate a timestamped uri", () => {
const polling = new eio.transports.polling({
path: "/engine.io",
@@ -124,7 +150,7 @@ describe("Transport", () => {
timestampRequests: true,
});
expect(polling.uri()).to.match(
/http:\/\/localhost\/engine\.io\?(j=[0-9]+&)?(t=[0-9A-Za-z-_]+)/
/http:\/\/localhost\/engine\.io\?(j=[0-9]+&)?(t=[0-9A-Za-z-_]+)/,
);
});
@@ -180,7 +206,7 @@ describe("Transport", () => {
timestampRequests: true,
});
expect(ws.uri()).to.match(
/ws:\/\/localhost\/engine\.io\?woot=[0-9A-Za-z-_]+/
/ws:\/\/localhost\/engine\.io\?woot=[0-9A-Za-z-_]+/,
);
});
@@ -210,7 +236,10 @@ describe("Transport", () => {
// these are server only
if (!env.browser) {
describe("options", () => {
it("should accept an `agent` option for WebSockets", (done) => {
it("should accept an `agent` option for WebSockets", function (done) {
if (env.useBuiltinWs) {
return this.skip();
}
const polling = new eio.transports.websocket({
path: "/engine.io",
hostname: "localhost",
@@ -269,7 +298,10 @@ describe("Transport", () => {
});
describe("perMessageDeflate", () => {
it("should set threshold", (done) => {
it("should set threshold", function (done) {
if (env.useBuiltinWs) {
return this.skip();
}
const socket = new eio.Socket({
transports: ["websocket"],
perMessageDeflate: { threshold: 0 },
@@ -289,7 +321,10 @@ describe("Transport", () => {
});
});
it("should not compress when the byte size is below threshold", (done) => {
it("should not compress when the byte size is below threshold", function (done) {
if (env.useBuiltinWs) {
return this.skip();
}
const socket = new eio.Socket({ transports: ["websocket"] });
socket.on("open", () => {
const ws = socket.transport.ws;

View File

@@ -1,5 +1,4 @@
import { Http3Server, WebTransport } from "@fails-components/webtransport";
import { Http3EventLoop } from "@fails-components/webtransport/lib/event-loop.js";
import expect from "expect.js";
import { Server } from "engine.io";
import { Socket } from "../build/esm-debug/index.js";
@@ -16,8 +15,8 @@ async function setup(opts, cb) {
const certificate = await generateWebTransportCertificate(
[{ shortName: "CN", value: "localhost" }],
{
days: 14, // the total length of the validity period MUST NOT exceed two weeks (https://w3c.github.io/webtransport/#custom-certificate-requirements)
}
days: 13, // the total length of the validity period MUST NOT exceed two weeks (https://w3c.github.io/webtransport/#custom-certificate-requirements)
},
);
const engine = new Server(opts);
@@ -48,7 +47,30 @@ async function setup(opts, cb) {
})();
h3Server.startServer();
h3Server.onServerListening = () => cb({ engine, h3Server, certificate });
await h3Server.ready;
cb({ engine, h3Server, certificate });
}
function createHttpServer(port) {
const httpServer = createServer();
let retryCount = 0;
return new Promise((resolve, reject) => {
httpServer.listen(port, () => resolve(httpServer));
httpServer.on("error", (e) => {
if (e.code === "EADDRINUSE" && ++retryCount <= 3) {
console.warn("port already in use, retrying...");
setTimeout(() => {
httpServer.listen(port, () => resolve(httpServer));
}, 100);
}
reject(e);
});
});
}
function success(engine, h3server, done) {
@@ -73,16 +95,12 @@ function createSocket(port, certificate, opts) {
},
},
},
opts
)
opts,
),
);
}
describe("WebTransport", () => {
after(() => {
Http3EventLoop.globalLoop.shutdownEventLoop(); // manually shutdown the event loop, instead of waiting 20s
});
it("should allow to connect with WebTransport directly", (done) => {
setup({}, ({ engine, h3Server, certificate }) => {
const socket = createSocket(h3Server.port, certificate, {
@@ -100,10 +118,9 @@ describe("WebTransport", () => {
{
transports: ["polling", "webtransport"],
},
({ engine, h3Server, certificate }) => {
const httpServer = createServer();
async ({ engine, h3Server, certificate }) => {
const httpServer = await createHttpServer(h3Server.port);
engine.attach(httpServer);
httpServer.listen(h3Server.port);
const socket = createSocket(h3Server.port, certificate, {
transports: ["polling", "webtransport"],
@@ -113,7 +130,7 @@ describe("WebTransport", () => {
httpServer.close();
success(engine, h3Server, done);
});
}
},
);
});
@@ -122,10 +139,9 @@ describe("WebTransport", () => {
{
transports: ["polling", "websocket", "webtransport"],
},
({ engine, h3Server, certificate }) => {
const httpServer = createServer();
async ({ engine, h3Server, certificate }) => {
const httpServer = await createHttpServer(h3Server.port);
engine.attach(httpServer);
httpServer.listen(h3Server.port);
const socket = createSocket(h3Server.port, certificate, {
transports: ["polling", "websocket", "webtransport"],
@@ -137,7 +153,7 @@ describe("WebTransport", () => {
httpServer.close();
success(engine, h3Server, done);
});
}
},
);
});
@@ -160,7 +176,7 @@ describe("WebTransport", () => {
success(engine, h3Server, done);
}
});
}
},
);
});

View File

@@ -1,56 +0,0 @@
'use strict';
const browsers = require('socket.io-browsers');
const zuulConfig = module.exports = {
ui: 'mocha-bdd',
// test on localhost by default
local: true,
open: true,
concurrency: 2, // ngrok only accepts two tunnels by default
// if browser does not sends output in 120s since last output:
// stop testing, something is wrong
browser_output_timeout: 120 * 1000,
browser_open_timeout: 60 * 4 * 1000,
// we want to be notified something is wrong asap, so no retry
browser_retries: 1,
server: './test/support/server.js',
builder: 'zuul-builder-webpack',
webpack: require('./support/webpack.config.js')
};
if (process.env.CI === 'true') {
zuulConfig.local = false;
zuulConfig.tunnel = {
type: 'ngrok',
bind_tls: true
};
}
zuulConfig.browsers = [
{
name: 'firefox',
version: 'latest'
}, {
name: 'internet explorer',
version: '9..11'
}, {
name: 'safari',
version: '14'
}, {
name: 'iphone',
version: '14'
}, {
name: 'android',
version: '5.1..6.0'
}, {
name: 'ipad',
version: '14'
}, {
name: 'MicrosoftEdge',
version: 'latest'
}
];

View File

@@ -32,7 +32,11 @@ export type RawData = any;
export interface Packet {
type: PacketType;
options?: { compress: boolean };
options?: {
compress: boolean;
wsPreEncoded?: string; // deprecated in favor of `wsPreEncodedFrame`
wsPreEncodedFrame?: any; // computed in the socket.io-adapter package (should be typed as Buffer)
};
data?: RawData;
}

View File

@@ -51,7 +51,7 @@ const mapBinary = (data: RawData, binaryType?: BinaryType) => {
);
} else {
// from WebTransport (Uint8Array)
return data.buffer;
return data.slice().buffer;
}
case "nodebuffer":
default:

View File

@@ -10,13 +10,10 @@
"require": "./build/cjs/index.js"
},
"types": "build/esm/index.d.ts",
"devDependencies": {
"prettier": "^3.3.2"
},
"scripts": {
"compile": "rimraf ./build && tsc && tsc -p tsconfig.esm.json && ./postcompile.sh",
"test": "npm run format:check && npm run compile && if test \"$BROWSERS\" = \"1\" ; then npm run test:browser; else npm run test:node; fi",
"test:node": "nyc mocha -r ts-node/register test/index.ts",
"test:node": "nyc mocha --import=tsx test/index.ts",
"test:browser": "zuul test/index.ts --no-coverage",
"format:check": "prettier --check 'lib/**/*.ts' 'test/**/*.ts'",
"format:fix": "prettier --write 'lib/**/*.ts' 'test/**/*.ts'",
@@ -25,7 +22,7 @@
"homepage": "https://github.com/socketio/socket.io/tree/main/packages/engine.io-parser#readme",
"repository": {
"type": "git",
"url": "https://github.com/socketio/socket.io.git"
"url": "git+https://github.com/socketio/socket.io.git"
},
"bugs": {
"url": "https://github.com/socketio/socket.io/issues"

View File

@@ -1,7 +1,12 @@
# History
# Changelog
| Version | Release date |
|------------------------------------------------------------------------------------------------------|----------------|
| [6.6.5](#665-2025-12-22) | December 2025 |
| [6.6.4](#664-2025-01-28) | January 2025 |
| [6.6.3](#663-2025-01-23) | January 2025 |
| [6.6.2](#662-2024-10-09) | October 2024 |
| [6.6.1](#661-2024-09-21) | September 2024 |
| [6.6.0](#660-2024-06-21) | June 2024 |
| [6.5.5](#655-2024-06-18) (from the [6.5.x](https://github.com/socketio/engine.io/tree/6.5.x) branch) | June 2024 |
| [3.6.2](#362-2024-06-18) (from the [3.x](https://github.com/socketio/engine.io/tree/3.x) branch) | June 2024 |
@@ -43,7 +48,68 @@
| [3.4.1](#341-2020-04-17) | April 2020 |
# Release notes
## [6.6.5](https://github.com/socketio/socket.io/compare/engine.io@6.6.4...engine.io@6.6.5) (2025-12-22)
The `url.parse()` function is now deprecated and has been replaced by `new URL()` (see [e08293b](https://github.com/socketio/socket.io/commit/e08293bc3735de5b824b347383e86e0b8ab9fbd5)).
### Dependencies
- [`ws@~8.18.3`](https://github.com/websockets/ws/releases/tag/8.18.3) ([diff](https://github.com/websockets/ws/compare/8.17.1...8.18.3))
## [6.6.4](https://github.com/socketio/socket.io/compare/engine.io@6.6.3...engine.io@6.6.4) (2025-01-28)
The bump of the `cookie` dependency was reverted, as it drops support for older Node.js versions (< 14).
### Dependencies
- [`ws@~8.17.1`](https://github.com/websockets/ws/releases/tag/8.17.1) (no change)
## [6.6.3](https://github.com/socketio/socket.io/compare/engine.io@6.6.2...engine.io@6.6.3) (2025-01-23)
This release contains a bump of the `cookie` dependency.
Release notes: https://github.com/jshttp/cookie/releases/tag/v1.0.0
### Dependencies
- [`ws@~8.17.1`](https://github.com/websockets/ws/releases/tag/8.17.1) (no change)
## [6.6.2](https://github.com/socketio/socket.io/compare/engine.io@6.6.1...engine.io@6.6.2) (2024-10-09)
This release contains a bump of the `cookie` dependency.
See also: https://github.com/advisories/GHSA-pxg6-pf52-xh8x
### Dependencies
- [`ws@~8.17.1`](https://github.com/websockets/ws/releases/tag/8.17.1) (no change)
## [6.6.1](https://github.com/socketio/socket.io/compare/engine.io@6.6.0...engine.io@6.6.1) (2024-09-21)
### Bug Fixes
* discard all pending packets when the server is closed ([923a12e](https://github.com/socketio/socket.io/commit/923a12e2de83ecaa75746a575e71a4739815d5c5))
* **uws:** prevent the client from upgrading twice ([d5095fe](https://github.com/socketio/socket.io/commit/d5095fe98c3976673c19f433c0114d06dbd8de1b))
### Dependencies
- [`ws@~8.17.1`](https://github.com/websockets/ws/releases/tag/8.17.1) (no change)
## [6.6.0](https://github.com/socketio/engine.io/compare/6.5.4...6.6.0) (2024-06-21)

View File

@@ -0,0 +1,117 @@
// imported from https://github.com/DefinitelyTyped/DefinitelyTyped/blob/b83cf9ef8b044e69f05b2a00aa7c6cb767a9acd2/types/cookie/index.d.ts (now deleted)
/**
* Basic HTTP cookie parser and serializer for HTTP servers.
*/
/**
* Additional serialization options
*/
export interface CookieSerializeOptions {
/**
* Specifies the value for the {@link https://tools.ietf.org/html/rfc6265#section-5.2.3|Domain Set-Cookie attribute}. By default, no
* domain is set, and most clients will consider the cookie to apply to only
* the current domain.
*/
domain?: string | undefined;
/**
* Specifies a function that will be used to encode a cookie's value. Since
* value of a cookie has a limited character set (and must be a simple
* string), this function can be used to encode a value into a string suited
* for a cookie's value.
*
* The default function is the global `encodeURIComponent`, which will
* encode a JavaScript string into UTF-8 byte sequences and then URL-encode
* any that fall outside of the cookie range.
*/
encode?(value: string): string;
/**
* Specifies the `Date` object to be the value for the {@link https://tools.ietf.org/html/rfc6265#section-5.2.1|`Expires` `Set-Cookie` attribute}. By default,
* no expiration is set, and most clients will consider this a "non-persistent cookie" and will delete
* it on a condition like exiting a web browser application.
*
* *Note* the {@link https://tools.ietf.org/html/rfc6265#section-5.3|cookie storage model specification}
* states that if both `expires` and `maxAge` are set, then `maxAge` takes precedence, but it is
* possible not all clients by obey this, so if both are set, they should
* point to the same date and time.
*/
expires?: Date | undefined;
/**
* Specifies the boolean value for the {@link https://tools.ietf.org/html/rfc6265#section-5.2.6|`HttpOnly` `Set-Cookie` attribute}.
* When truthy, the `HttpOnly` attribute is set, otherwise it is not. By
* default, the `HttpOnly` attribute is not set.
*
* *Note* be careful when setting this to true, as compliant clients will
* not allow client-side JavaScript to see the cookie in `document.cookie`.
*/
httpOnly?: boolean | undefined;
/**
* Specifies the number (in seconds) to be the value for the `Max-Age`
* `Set-Cookie` attribute. The given number will be converted to an integer
* by rounding down. By default, no maximum age is set.
*
* *Note* the {@link https://tools.ietf.org/html/rfc6265#section-5.3|cookie storage model specification}
* states that if both `expires` and `maxAge` are set, then `maxAge` takes precedence, but it is
* possible not all clients by obey this, so if both are set, they should
* point to the same date and time.
*/
maxAge?: number | undefined;
/**
* Specifies the `boolean` value for the [`Partitioned` `Set-Cookie`](rfc-cutler-httpbis-partitioned-cookies)
* attribute. When truthy, the `Partitioned` attribute is set, otherwise it is not. By default, the
* `Partitioned` attribute is not set.
*
* **note** This is an attribute that has not yet been fully standardized, and may change in the future.
* This also means many clients may ignore this attribute until they understand it.
*
* More information about can be found in [the proposal](https://github.com/privacycg/CHIPS)
*/
partitioned?: boolean | undefined;
/**
* Specifies the value for the {@link https://tools.ietf.org/html/rfc6265#section-5.2.4|`Path` `Set-Cookie` attribute}.
* By default, the path is considered the "default path".
*/
path?: string | undefined;
/**
* Specifies the `string` to be the value for the [`Priority` `Set-Cookie` attribute][rfc-west-cookie-priority-00-4.1].
*
* - `'low'` will set the `Priority` attribute to `Low`.
* - `'medium'` will set the `Priority` attribute to `Medium`, the default priority when not set.
* - `'high'` will set the `Priority` attribute to `High`.
*
* More information about the different priority levels can be found in
* [the specification][rfc-west-cookie-priority-00-4.1].
*
* **note** This is an attribute that has not yet been fully standardized, and may change in the future.
* This also means many clients may ignore this attribute until they understand it.
*/
priority?: "low" | "medium" | "high" | undefined;
/**
* Specifies the boolean or string to be the value for the {@link https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.2.7|`SameSite` `Set-Cookie` attribute}.
*
* - `true` will set the `SameSite` attribute to `Strict` for strict same
* site enforcement.
* - `false` will not set the `SameSite` attribute.
* - `'lax'` will set the `SameSite` attribute to Lax for lax same site
* enforcement.
* - `'strict'` will set the `SameSite` attribute to Strict for strict same
* site enforcement.
* - `'none'` will set the SameSite attribute to None for an explicit
* cross-site cookie.
*
* More information about the different enforcement levels can be found in {@link https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.2.7|the specification}.
*
* *note* This is an attribute that has not yet been fully standardized, and may change in the future. This also means many clients may ignore this attribute until they understand it.
*/
sameSite?: true | false | "lax" | "strict" | "none" | undefined;
/**
* Specifies the boolean value for the {@link https://tools.ietf.org/html/rfc6265#section-5.2.5|`Secure` `Set-Cookie` attribute}. When truthy, the
* `Secure` attribute is set, otherwise it is not. By default, the `Secure` attribute is not set.
*
* *Note* be careful when setting this to `true`, as compliant clients will
* not send the cookie back to the server in the future if the browser does
* not have an HTTPS connection.
*/
secure?: boolean | undefined;
}

View File

@@ -1,27 +1,36 @@
import { createServer } from "http";
import { createServer, Server as HttpServer } from "http";
import { Server, AttachOptions, ServerOptions } from "./server";
import transports from "./transports/index";
import * as parser from "engine.io-parser";
export { Server, transports, listen, attach, parser };
export type { AttachOptions, ServerOptions, BaseServer } from "./server";
export type {
AttachOptions,
ServerOptions,
BaseServer,
ErrorCallback,
} from "./server";
export { uServer } from "./userver";
export { Socket } from "./socket";
export { Transport } from "./transport";
export const protocol = parser.protocol;
/**
* Creates an http.Server exclusively used for WS upgrades.
* Creates an http.Server exclusively used for WS upgrades, and starts listening.
*
* @param {Number} port
* @param {Function} callback
* @param {Object} options
* @return {Server} websocket.io server
* @param port
* @param options
* @param listenCallback - callback for http.Server.listen()
* @return engine.io server
*/
function listen(port, options: AttachOptions & ServerOptions, fn) {
function listen(
port: number,
options?: AttachOptions & ServerOptions,
listenCallback?: () => void,
): Server {
if ("function" === typeof options) {
fn = options;
listenCallback = options;
options = {};
}
@@ -34,7 +43,7 @@ function listen(port, options: AttachOptions & ServerOptions, fn) {
const engine = attach(server, options);
engine.httpServer = server;
server.listen(port, fn);
server.listen(port, listenCallback);
return engine;
}
@@ -42,12 +51,15 @@ function listen(port, options: AttachOptions & ServerOptions, fn) {
/**
* Captures upgrade requests for a http.Server.
*
* @param {http.Server} server
* @param {Object} options
* @return {Server} engine server
* @param server
* @param options
* @return engine.io server
*/
function attach(server, options: AttachOptions & ServerOptions) {
function attach(
server: HttpServer,
options: AttachOptions & ServerOptions,
): Server {
const engine = new Server(options);
engine.attach(server, options);
return engine;

View File

@@ -59,8 +59,7 @@ const EMPTY_BUFFER = Buffer.concat([]);
*
* @api private
*/
export function encodePacket (packet, supportsBinary, utf8encode, callback) {
export function encodePacket (packet: any, supportsBinary?: any, utf8encode?: any, callback?: any) {
if (typeof supportsBinary === 'function') {
callback = supportsBinary;
supportsBinary = null;
@@ -86,7 +85,7 @@ export function encodePacket (packet, supportsBinary, utf8encode, callback) {
}
return callback('' + encoded);
};
}
/**
* Encode Buffer data
@@ -120,16 +119,16 @@ export function encodeBase64Packet (packet, callback){
/**
* Decodes a packet. Data also available as an ArrayBuffer if requested.
*
* @return {Object} with `type` and `data` (if any)
* @return {import('engine.io-parser').Packet} with `type` and `data` (if any)
* @api private
*/
export function decodePacket (data, binaryType, utf8decode) {
export function decodePacket (data: any, binaryType?: any, utf8decode?: any): any {
if (data === undefined) {
return err;
}
var type;
let type: string | number;
// String data
if (typeof data === 'string') {
@@ -147,6 +146,7 @@ export function decodePacket (data, binaryType, utf8decode) {
}
}
// @ts-expect-error
if (Number(type) != type || !packetslist[type]) {
return err;
}
@@ -274,7 +274,7 @@ function map(ary, each, done) {
* @api public
*/
export function decodePayload (data, binaryType, callback) {
export function decodePayload (data: any, binaryType?: any, callback?: any) {
if (typeof data !== 'string') {
return decodePayloadAsBinary(data, binaryType, callback);
}

View File

@@ -1,29 +1,37 @@
import * as qs from "querystring";
import { parse } from "url";
import * as base64id from "base64id";
import transports from "./transports";
import { EventEmitter } from "events";
import { Socket } from "./socket";
import debugModule from "debug";
import { serialize } from "cookie";
import { Server as DEFAULT_WS_ENGINE } from "ws";
import {
Server as DEFAULT_WS_ENGINE,
type Server as WsServer,
type PerMessageDeflateOptions,
type WebSocket as WsWebSocket,
} from "ws";
import type {
IncomingMessage,
Server as HttpServer,
ServerResponse,
} from "http";
import type { CookieSerializeOptions } from "cookie";
import type { CorsOptions, CorsOptionsDelegate } from "cors";
import type { Duplex } from "stream";
import { WebTransport } from "./transports/webtransport";
import { createPacketDecoderStream } from "engine.io-parser";
import type { EngineRequest } from "./transport";
import type { EngineRequest, Transport } from "./transport";
import type { CookieSerializeOptions } from "./contrib/types.cookie";
const debug = debugModule("engine");
const kResponseHeaders = Symbol("responseHeaders");
type Transport = "polling" | "websocket" | "webtransport";
type TransportName = "polling" | "websocket" | "webtransport";
export type ErrorCallback = (
errorCode?: (typeof Server.errors)[keyof typeof Server.errors],
errorContext?: Record<string, unknown> & { name?: string; message?: string },
) => void;
export interface AttachOptions {
/**
@@ -78,7 +86,7 @@ export interface ServerOptions {
*/
allowRequest?: (
req: IncomingMessage,
fn: (err: string | null | undefined, success: boolean) => void
fn: (err: string | null | undefined, success: boolean) => void,
) => void;
/**
* The low-level transports that are enabled. WebTransport is disabled by default and must be manually enabled:
@@ -90,7 +98,7 @@ export interface ServerOptions {
*
* @default ["polling", "websocket"]
*/
transports?: Transport[];
transports?: TransportName[];
/**
* whether to allow transport upgrades
* @default true
@@ -100,7 +108,7 @@ export interface ServerOptions {
* parameters of the WebSocket permessage-deflate extension (see ws module api docs). Set to false to disable.
* @default false
*/
perMessageDeflate?: boolean | object;
perMessageDeflate?: boolean | PerMessageDeflateOptions;
/**
* parameters of the http compression for the polling transports (see zlib api docs). Set to false to disable.
* @default true
@@ -146,10 +154,10 @@ export interface ServerOptions {
type Middleware = (
req: IncomingMessage,
res: ServerResponse,
next: (err?: any) => void
next: (err?: any) => void,
) => void;
function parseSessionId(data: string) {
function parseSessionId(data: string): string | undefined {
try {
const parsed = JSON.parse(data);
if (typeof parsed.sid === "string") {
@@ -192,7 +200,7 @@ export abstract class BaseServer extends EventEmitter {
cors: false,
allowEIO3: false,
},
opts
opts,
);
if (opts.cookie) {
@@ -204,7 +212,7 @@ export abstract class BaseServer extends EventEmitter {
httpOnly: opts.cookie.path !== false,
sameSite: "lax",
},
opts.cookie
opts.cookie,
);
}
@@ -217,14 +225,14 @@ export abstract class BaseServer extends EventEmitter {
{
threshold: 1024,
},
opts.perMessageDeflate
opts.perMessageDeflate,
);
}
this.init();
}
protected abstract init();
protected abstract init(): void;
/**
* Compute the pathname of the requests that are handled by the server
@@ -244,10 +252,8 @@ export abstract class BaseServer extends EventEmitter {
/**
* Returns a list of available transports for upgrade given a certain transport.
*
* @return {Array}
*/
public upgrades(transport: string) {
public upgrades(transport: TransportName): string[] {
if (!this.opts.allowUpgrades) return [];
return transports[transport].upgradesTo || [];
}
@@ -259,17 +265,18 @@ export abstract class BaseServer extends EventEmitter {
* @param upgrade - whether it's an upgrade request
* @param fn
* @protected
* @return whether the request is valid
*/
protected verify(
req: any,
req: EngineRequest,
upgrade: boolean,
fn: (errorCode?: number, errorContext?: any) => void
) {
fn: ErrorCallback,
): void | boolean {
// transport check
const transport = req._query.transport;
// WebTransport does not go through the verify() method, see the onWebTransportSession() method
if (
!~this.opts.transports.indexOf(transport) ||
!~this.opts.transports.indexOf(transport as TransportName) ||
transport === "webtransport"
) {
debug('unknown transport "%s"', transport);
@@ -361,7 +368,7 @@ export abstract class BaseServer extends EventEmitter {
protected _applyMiddlewares(
req: IncomingMessage,
res: ServerResponse,
callback: (err?: any) => void
callback: (err?: any) => void,
) {
if (this.middlewares.length === 0) {
debug("no middleware to apply, skipping");
@@ -408,7 +415,7 @@ export abstract class BaseServer extends EventEmitter {
*
* @param {IncomingMessage} req - the request object
*/
public generateId(req: IncomingMessage) {
public generateId(req: IncomingMessage): string | PromiseLike<string> {
return base64id.generateId();
}
@@ -422,9 +429,9 @@ export abstract class BaseServer extends EventEmitter {
* @protected
*/
protected async handshake(
transportName: string,
req: any,
closeConnection: (errorCode?: number, errorContext?: any) => void
transportName: TransportName,
req: EngineRequest,
closeConnection: ErrorCallback,
) {
const protocol = req._query.EIO === "4" ? 4 : 3; // 3rd revision by default
if (protocol === 3 && !this.opts.allowEIO3) {
@@ -519,7 +526,7 @@ export abstract class BaseServer extends EventEmitter {
public async onWebTransportSession(session: any) {
const timeout = setTimeout(() => {
debug(
"the client failed to establish a bidirectional stream in the given period"
"the client failed to establish a bidirectional stream in the given period",
);
session.close();
}, this.opts.upgradeTimeout);
@@ -535,7 +542,7 @@ export abstract class BaseServer extends EventEmitter {
const stream = result.value;
const transformStream = createPacketDecoderStream(
this.opts.maxHttpBufferSize,
"nodebuffer"
"nodebuffer",
);
const reader = stream.readable.pipeThrough(transformStream).getReader();
@@ -600,7 +607,10 @@ export abstract class BaseServer extends EventEmitter {
}
}
protected abstract createTransport(transportName, req);
protected abstract createTransport(
transportName: TransportName,
req: EngineRequest,
);
/**
* Protocol errors mappings.
@@ -613,7 +623,7 @@ export abstract class BaseServer extends EventEmitter {
BAD_REQUEST: 3,
FORBIDDEN: 4,
UNSUPPORTED_PROTOCOL_VERSION: 5,
};
} as const;
static errorMessages = {
0: "Transport unknown",
@@ -622,7 +632,7 @@ export abstract class BaseServer extends EventEmitter {
3: "Bad request",
4: "Forbidden",
5: "Unsupported protocol version",
};
} as const;
}
/**
@@ -632,7 +642,10 @@ export abstract class BaseServer extends EventEmitter {
* @see https://nodejs.org/api/http.html#class-httpserverresponse
*/
class WebSocketResponse {
constructor(readonly req, readonly socket: Duplex) {
constructor(
readonly req,
readonly socket: Duplex,
) {
// temporarily store the response headers on the req object (see the "headers" event)
req[kResponseHeaders] = {};
}
@@ -659,9 +672,12 @@ class WebSocketResponse {
}
}
/**
* An Engine.IO server based on Node.js built-in HTTP server and the `ws` package for WebSocket connections.
*/
export class Server extends BaseServer {
public httpServer?: HttpServer;
private ws: any;
private ws: WsServer;
/**
* Initialize websocket server
@@ -681,7 +697,7 @@ export class Server extends BaseServer {
});
if (typeof this.ws.on === "function") {
this.ws.on("headers", (headersArray, req) => {
this.ws.on("headers", (headersArray, req: EngineRequest) => {
// note: 'ws' uses an array of headers, while Engine.IO uses an object (response.writeHead() accepts both formats)
// we could also try to parse the array and then sync the values, but that will be error-prone
const additionalHeaders = req[kResponseHeaders] || {};
@@ -718,13 +734,16 @@ export class Server extends BaseServer {
private prepare(req: EngineRequest) {
// try to leverage pre-existing `req._query` (e.g: from connect)
if (!req._query) {
req._query = (
~req.url.indexOf("?") ? qs.parse(parse(req.url).query) : {}
) as Record<string, string>;
const url = new URL(req.url, "https://socket.io");
req._query = Object.fromEntries(url.searchParams.entries());
}
}
protected createTransport(transportName: string, req: IncomingMessage) {
protected createTransport(
transportName: TransportName,
req: IncomingMessage,
): Transport {
// @ts-expect-error 'polling' is a plain function used as constructor
return new transports[transportName](req);
}
@@ -739,7 +758,7 @@ export class Server extends BaseServer {
this.prepare(req);
req.res = res;
const callback = (errorCode, errorContext) => {
const callback: ErrorCallback = (errorCode, errorContext) => {
if (errorCode !== undefined) {
this.emit("connection_error", {
req,
@@ -757,7 +776,11 @@ export class Server extends BaseServer {
} else {
const closeConnection = (errorCode, errorContext) =>
abortRequest(res, errorCode, errorContext);
this.handshake(req._query.transport, req, closeConnection);
this.handshake(
req._query.transport as TransportName,
req,
closeConnection,
);
}
};
@@ -776,12 +799,12 @@ export class Server extends BaseServer {
public handleUpgrade(
req: EngineRequest,
socket: Duplex,
upgradeHead: Buffer
upgradeHead: Buffer,
) {
this.prepare(req);
const res = new WebSocketResponse(req, socket);
const callback = (errorCode, errorContext) => {
const callback: ErrorCallback = (errorCode, errorContext) => {
if (errorCode !== undefined) {
this.emit("connection_error", {
req,
@@ -817,11 +840,16 @@ export class Server extends BaseServer {
/**
* Called upon a ws.io connection.
*
* @param {ws.Socket} websocket
* @param req
* @param socket
* @param websocket
* @private
*/
private onWebSocket(req, socket, websocket) {
private onWebSocket(
req: EngineRequest,
socket: Duplex,
websocket: WsWebSocket,
) {
websocket.on("error", onUpgradeError);
if (
@@ -856,14 +884,22 @@ export class Server extends BaseServer {
// transport error handling takes over
websocket.removeListener("error", onUpgradeError);
const transport = this.createTransport(req._query.transport, req);
const transport = this.createTransport(
req._query.transport as TransportName,
req,
);
// @ts-expect-error this option is only for WebSocket impl
transport.perMessageDeflate = this.opts.perMessageDeflate;
client._maybeUpgrade(transport);
}
} else {
const closeConnection = (errorCode, errorContext) =>
abortUpgrade(socket, errorCode, errorContext);
this.handshake(req._query.transport, req, closeConnection);
this.handshake(
req._query.transport as TransportName,
req,
closeConnection,
);
}
function onUpgradeError() {
@@ -941,7 +977,11 @@ export class Server extends BaseServer {
* @private
*/
function abortRequest(res, errorCode, errorContext) {
function abortRequest(
res: ServerResponse,
errorCode: number,
errorContext?: { message?: string },
) {
const statusCode = errorCode === Server.errors.FORBIDDEN ? 403 : 400;
const message =
errorContext && errorContext.message
@@ -953,7 +993,7 @@ function abortRequest(res, errorCode, errorContext) {
JSON.stringify({
code: errorCode,
message,
})
}),
);
}
@@ -968,7 +1008,7 @@ function abortRequest(res, errorCode, errorContext) {
function abortUpgrade(
socket,
errorCode,
errorContext: { message?: string } = {}
errorContext: { message?: string } = {},
) {
socket.on("error", () => {
debug("ignoring error from closed connection");
@@ -984,7 +1024,7 @@ function abortUpgrade(
length +
"\r\n" +
"\r\n" +
message
message,
);
}
socket.destroy();
@@ -1024,7 +1064,7 @@ const validHdrChars = [
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 // ... 255
]
function checkInvalidHeaderChar(val) {
function checkInvalidHeaderChar(val?: string) {
val += "";
if (val.length < 1) return false;
if (!validHdrChars[val.charCodeAt(0)]) {

View File

@@ -5,6 +5,7 @@ import type { EngineRequest, Transport } from "./transport";
import type { BaseServer } from "./server";
import { setTimeout, clearTimeout } from "timers";
import type { Packet, PacketType, RawData } from "engine.io-parser";
import type transports from "./transports";
const debug = debugModule("engine:socket");
@@ -80,7 +81,7 @@ export class Socket extends EventEmitter {
server: BaseServer,
transport: Transport,
req: EngineRequest,
protocol: number
protocol: number,
) {
super();
this.id = id;
@@ -125,7 +126,7 @@ export class Socket extends EventEmitter {
pingInterval: this.server.opts.pingInterval,
pingTimeout: this.server.opts.pingTimeout,
maxPayload: this.server.opts.maxHttpBufferSize,
})
}),
);
if (this.server.opts.initialPacket) {
@@ -212,7 +213,7 @@ export class Socket extends EventEmitter {
this.pingIntervalTimer = setTimeout(() => {
debug(
"writing ping packet - expecting pong within %sms",
this.server.opts.pingTimeout
this.server.opts.pingTimeout,
);
this.sendPacket("ping");
this.resetPingTimeout();
@@ -233,7 +234,7 @@ export class Socket extends EventEmitter {
},
this.protocol === 3
? this.server.opts.pingInterval + this.server.opts.pingTimeout
: this.server.opts.pingTimeout
: this.server.opts.pingTimeout,
);
}
@@ -293,7 +294,7 @@ export class Socket extends EventEmitter {
debug(
'might upgrade socket transport from "%s" to "%s"',
this.transport.name,
transport.name
transport.name,
);
this.upgrading = true;
@@ -468,7 +469,7 @@ export class Socket extends EventEmitter {
type: PacketType,
data?: RawData,
options: SendOptions = {},
callback?: SendCallback
callback?: SendCallback,
) {
if ("function" === typeof options) {
callback = options;
@@ -537,9 +538,11 @@ export class Socket extends EventEmitter {
*/
private getAvailableUpgrades() {
const availableUpgrades = [];
const allUpgrades = this.server.upgrades(this.transport.name);
const allUpgrades = this.server.upgrades(
this.transport.name as keyof typeof transports,
);
for (let i = 0; i < allUpgrades.length; ++i) {
const upg = allUpgrades[i];
const upg = allUpgrades[i] as keyof typeof transports;
if (this.server.opts.transports.indexOf(upg) !== -1) {
availableUpgrades.push(upg);
}
@@ -554,6 +557,13 @@ export class Socket extends EventEmitter {
* @return {Socket} for chaining
*/
public close(discard?: boolean) {
if (
discard &&
(this.readyState === "open" || this.readyState === "closing")
) {
return this.closeTransport(discard);
}
if ("open" !== this.readyState) return;
this.readyState = "closing";
@@ -561,7 +571,7 @@ export class Socket extends EventEmitter {
if (this.writeBuffer.length) {
debug(
"there are %d remaining packets in the buffer, waiting for the 'drain' event",
this.writeBuffer.length
this.writeBuffer.length,
);
this.once("drain", () => {
debug("all packets have been sent, closing the transport");
@@ -570,7 +580,7 @@ export class Socket extends EventEmitter {
return;
}
debug("the buffer is empty, closing the transport right away", discard);
debug("the buffer is empty, closing the transport right away");
this.closeTransport(discard);
}
@@ -581,7 +591,7 @@ export class Socket extends EventEmitter {
* @private
*/
private closeTransport(discard: boolean) {
debug("closing the transport (discard? %s)", discard);
debug("closing the transport (discard? %s)", !!discard);
if (discard) this.transport.discard();
this.transport.close(this.onClose.bind(this, "forced close"));
}

View File

@@ -4,6 +4,7 @@ import * as parser_v3 from "./parser-v3/index";
import debugModule from "debug";
import type { IncomingMessage, ServerResponse } from "http";
import { Packet, RawData } from "engine.io-parser";
import type { WebSocket } from "ws";
const debug = debugModule("engine:transport");
@@ -15,7 +16,11 @@ export type EngineRequest = IncomingMessage & {
_query: Record<string, string>;
res?: ServerResponse;
cleanup?: Function;
websocket?: any;
websocket?: WebSocket & {
_socket?: {
remoteAddress: string;
};
};
};
export abstract class Transport extends EventEmitter {
@@ -37,7 +42,7 @@ export abstract class Transport extends EventEmitter {
*
* @see https://github.com/socketio/engine.io-protocol
*/
public protocol: number;
public protocol: 3 | 4;
/**
* The current state of the transport.
@@ -53,7 +58,7 @@ export abstract class Transport extends EventEmitter {
* The parser to use (depends on the revision of the {@link Transport#protocol}.
* @protected
*/
protected parser: any;
protected parser: typeof parser_v4 | typeof parser_v3;
/**
* Whether the transport supports binary payloads (else it will be base64-encoded)
* @protected
@@ -69,11 +74,16 @@ export abstract class Transport extends EventEmitter {
"readyState updated from %s to %s (%s)",
this._readyState,
state,
this.name
this.name,
);
this._readyState = state;
}
/**
* The list of transports this transport can be upgraded to.
*/
static upgradesTo: string[] = [];
/**
* Transport constructor.
*
@@ -148,7 +158,7 @@ export abstract class Transport extends EventEmitter {
/**
* Called with the encoded packet data.
*
* @param {String} data
* @param data
* @protected
*/
protected onData(data: RawData) {

View File

@@ -3,6 +3,8 @@ import { createGzip, createDeflate } from "zlib";
import * as accepts from "accepts";
import debugModule from "debug";
import { HttpRequest, HttpResponse } from "uWebSockets.js";
import type * as parser_v4 from "engine.io-parser";
import type * as parser_v3 from "../parser-v3/index";
const debug = debugModule("engine:polling");
@@ -228,9 +230,9 @@ export class Polling extends Transport {
};
if (this.protocol === 3) {
this.parser.decodePayload(data, callback);
(this.parser as typeof parser_v3).decodePayload(data, callback);
} else {
this.parser.decodePayload(data).forEach(callback);
(this.parser as typeof parser_v4).decodePayload(data).forEach(callback);
}
}
@@ -263,7 +265,7 @@ export class Polling extends Transport {
this.shouldClose = null;
}
const doWrite = (data) => {
const doWrite = (data: string) => {
const compress = packets.some((packet) => {
return packet.options && packet.options.compress;
});
@@ -271,9 +273,13 @@ export class Polling extends Transport {
};
if (this.protocol === 3) {
this.parser.encodePayload(packets, this.supportsBinary, doWrite);
(this.parser as typeof parser_v3).encodePayload(
packets,
this.supportsBinary,
doWrite,
);
} else {
this.parser.encodePayload(packets, doWrite);
(this.parser as typeof parser_v4).encodePayload(packets, doWrite);
}
}

View File

@@ -2,9 +2,10 @@ import { Polling as XHR } from "./polling";
import { JSONP } from "./polling-jsonp";
import { WebSocket } from "./websocket";
import { WebTransport } from "./webtransport";
import type { EngineRequest } from "../transport";
export default {
polling: polling,
polling,
websocket: WebSocket,
webtransport: WebTransport,
};
@@ -12,8 +13,7 @@ export default {
/**
* Polling polymorphic constructor.
*/
function polling(req) {
function polling(req: EngineRequest) {
if ("string" === typeof req._query.j) {
return new JSONP(req);
} else {

View File

@@ -4,6 +4,8 @@ import * as accepts from "accepts";
import debugModule from "debug";
import type { IncomingMessage, ServerResponse } from "http";
import type { Packet, RawData } from "engine.io-parser";
import type * as parser_v4 from "engine.io-parser";
import type * as parser_v3 from "../parser-v3/index";
const debug = debugModule("engine:polling");
@@ -196,9 +198,9 @@ export class Polling extends Transport {
};
if (this.protocol === 3) {
this.parser.decodePayload(data, callback);
(this.parser as typeof parser_v3).decodePayload(data, callback);
} else {
this.parser.decodePayload(data).forEach(callback);
(this.parser as typeof parser_v4).decodePayload(data).forEach(callback);
}
}
@@ -225,7 +227,7 @@ export class Polling extends Transport {
this.shouldClose = null;
}
const doWrite = (data) => {
const doWrite = (data: string) => {
const compress = packets.some((packet) => {
return packet.options && packet.options.compress;
});
@@ -233,9 +235,13 @@ export class Polling extends Transport {
};
if (this.protocol === 3) {
this.parser.encodePayload(packets, this.supportsBinary, doWrite);
(this.parser as typeof parser_v3).encodePayload(
packets,
this.supportsBinary,
doWrite,
);
} else {
this.parser.encodePayload(packets, doWrite);
(this.parser as typeof parser_v4).encodePayload(packets, doWrite);
}
}

View File

@@ -1,12 +1,13 @@
import { EngineRequest, Transport } from "../transport";
import debugModule from "debug";
import type { Packet, RawData } from "engine.io-parser";
import type { PerMessageDeflateOptions, WebSocket as WsWebSocket } from "ws";
const debug = debugModule("engine:ws");
export class WebSocket extends Transport {
protected perMessageDeflate: any;
private socket: any;
perMessageDeflate?: boolean | PerMessageDeflateOptions;
private socket: WsWebSocket;
/**
* WebSocket transport
@@ -51,16 +52,16 @@ export class WebSocket extends Transport {
if (this._canSendPreEncodedFrame(packet)) {
// the WebSocket frame was computed with WebSocket.Sender.frame()
// see https://github.com/websockets/ws/issues/617#issuecomment-283002469
// @ts-expect-error use of untyped member
this.socket._sender.sendFrame(
// @ts-ignore
packet.options.wsPreEncodedFrame,
isLast ? this._onSentLast : this._onSent
isLast ? this._onSentLast : this._onSent,
);
} else {
this.parser.encodePacket(
packet,
this.supportsBinary,
isLast ? this._doSendLast : this._doSend
isLast ? this._doSendLast : this._doSend,
);
}
}
@@ -74,8 +75,8 @@ export class WebSocket extends Transport {
private _canSendPreEncodedFrame(packet: Packet) {
return (
!this.perMessageDeflate &&
// @ts-expect-error use of untyped member
typeof this.socket?._sender?.sendFrame === "function" &&
// @ts-ignore
packet.options?.wsPreEncodedFrame !== undefined
);
}

View File

@@ -10,7 +10,11 @@ const debug = debugModule("engine:webtransport");
export class WebTransport extends Transport {
private readonly writer;
constructor(private readonly session, stream, reader) {
constructor(
private readonly session,
stream,
reader,
) {
super({ _query: { EIO: "4" } });
const transformStream = createPacketEncoderStream();

View File

@@ -2,6 +2,7 @@ import debugModule from "debug";
import { AttachOptions, BaseServer, Server } from "./server";
import { HttpRequest, HttpResponse, TemplatedApp } from "uWebSockets.js";
import transports from "./transports-uws";
import { EngineRequest } from "./transport";
const debug = debugModule("engine:uws");
@@ -23,6 +24,10 @@ export interface uOptions {
maxBackpressure?: number;
}
/**
* An Engine.IO server based on the `uWebSockets.js` package.
*/
// TODO export it into its own package
export class uServer extends BaseServer {
protected init() {}
protected cleanup() {}
@@ -32,7 +37,7 @@ export class uServer extends BaseServer {
*
* @private
*/
private prepare(req, res: HttpResponse) {
private prepare(req: HttpRequest & EngineRequest, res: HttpResponse) {
req.method = req.getMethod().toUpperCase();
req.url = req.getUrl();
@@ -44,6 +49,7 @@ export class uServer extends BaseServer {
req.headers[key] = value;
});
// @ts-expect-error
req.connection = {
remoteAddress: Buffer.from(res.getRemoteAddressAsText()).toString(),
};
@@ -53,7 +59,7 @@ export class uServer extends BaseServer {
});
}
protected createTransport(transportName, req) {
protected createTransport(transportName: string, req: EngineRequest) {
return new transports[transportName](req);
}
@@ -64,7 +70,7 @@ export class uServer extends BaseServer {
*/
public attach(
app /* : TemplatedApp */,
options: AttachOptions & uOptions = {}
options: AttachOptions & uOptions = {},
) {
const path = this._computePath(options);
(app as TemplatedApp)
@@ -84,7 +90,7 @@ export class uServer extends BaseServer {
},
message: (ws, message, isBinary) => {
ws.getUserData().transport.onData(
isBinary ? message : Buffer.from(message).toString()
isBinary ? message : Buffer.from(message).toString(),
);
},
close: (ws, code, message) => {
@@ -96,7 +102,7 @@ export class uServer extends BaseServer {
override _applyMiddlewares(
req: any,
res: any,
callback: (err?: any) => void
callback: (err?: any) => void,
): void {
if (this.middlewares.length === 0) {
return callback();
@@ -116,10 +122,10 @@ export class uServer extends BaseServer {
private handleRequest(
res: HttpResponse,
req: HttpRequest & { res: any; _query: any }
req: HttpRequest & { res: any; _query: any },
) {
debug('handling "%s" http request "%s"', req.getMethod(), req.getUrl());
this.prepare(req, res);
this.prepare(req as unknown as HttpRequest & EngineRequest, res);
req.res = res;
@@ -142,7 +148,11 @@ export class uServer extends BaseServer {
} else {
const closeConnection = (errorCode, errorContext) =>
this.abortRequest(res, errorCode, errorContext);
this.handshake(req._query.transport, req, closeConnection);
this.handshake(
req._query.transport,
req as unknown as EngineRequest,
closeConnection,
);
}
};
@@ -150,7 +160,11 @@ export class uServer extends BaseServer {
if (err) {
callback(Server.errors.BAD_REQUEST, { name: "MIDDLEWARE_FAILURE" });
} else {
this.verify(req, false, callback);
this.verify(
req as unknown as HttpRequest & EngineRequest,
false,
callback,
);
}
});
}
@@ -158,11 +172,11 @@ export class uServer extends BaseServer {
private handleUpgrade(
res: HttpResponse,
req: HttpRequest & { res: any; _query: any },
context
context,
) {
debug("on upgrade");
this.prepare(req, res);
this.prepare(req as unknown as HttpRequest & EngineRequest, res);
req.res = res;
@@ -185,24 +199,27 @@ export class uServer extends BaseServer {
const client = this.clients[id];
if (!client) {
debug("upgrade attempt for closed client");
res.close();
return res.close();
} else if (client.upgrading) {
debug("transport has already been trying to upgrade");
res.close();
return res.close();
} else if (client.upgraded) {
debug("transport had already been upgraded");
res.close();
return res.close();
} else {
debug("upgrading existing transport");
transport = this.createTransport(req._query.transport, req);
transport = this.createTransport(
req._query.transport,
req as unknown as EngineRequest,
);
client._maybeUpgrade(transport);
}
} else {
transport = await this.handshake(
req._query.transport,
req,
req as unknown as EngineRequest,
(errorCode, errorContext) =>
this.abortRequest(res, errorCode, errorContext)
this.abortRequest(res, errorCode, errorContext),
);
if (!transport) {
return;
@@ -219,7 +236,7 @@ export class uServer extends BaseServer {
req.getHeader("sec-websocket-key"),
req.getHeader("sec-websocket-protocol"),
req.getHeader("sec-websocket-extensions"),
context
context,
);
};
@@ -227,15 +244,15 @@ export class uServer extends BaseServer {
if (err) {
callback(Server.errors.BAD_REQUEST, { name: "MIDDLEWARE_FAILURE" });
} else {
this.verify(req, true, callback);
this.verify(req as unknown as EngineRequest, true, callback);
}
});
}
private abortRequest(
res: HttpResponse | ResponseWrapper,
errorCode,
errorContext
errorCode: number,
errorContext?: { message?: string },
) {
const statusCode =
errorCode === Server.errors.FORBIDDEN
@@ -252,7 +269,7 @@ export class uServer extends BaseServer {
JSON.stringify({
code: errorCode,
message,
})
}),
);
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "engine.io",
"version": "6.6.0",
"version": "6.6.5",
"description": "The realtime engine behind Socket.IO. Provides the foundation of a bidirectional connection between client and server",
"type": "commonjs",
"main": "./build/engine.io.js",
@@ -31,16 +31,15 @@
],
"license": "MIT",
"dependencies": {
"@types/cookie": "^0.4.1",
"@types/cors": "^2.8.12",
"@types/node": ">=10.0.0",
"accepts": "~1.3.4",
"base64id": "2.0.0",
"cookie": "~0.4.1",
"cookie": "~0.7.2",
"cors": "~2.8.5",
"debug": "~4.3.1",
"debug": "~4.4.1",
"engine.io-parser": "~5.2.1",
"ws": "~8.17.1"
"ws": "~8.18.3"
},
"scripts": {
"compile": "rimraf ./build && tsc",
@@ -56,7 +55,7 @@
"homepage": "https://github.com/socketio/socket.io/tree/main/packages/engine.io#readme",
"repository": {
"type": "git",
"url": "https://github.com/socketio/socket.io.git"
"url": "git+https://github.com/socketio/socket.io.git"
},
"bugs": {
"url": "https://github.com/socketio/socket.io/issues"

View File

@@ -1,9 +1,26 @@
if (process.env.EIO_CLIENT === "3") {
// we need the WebSocket object provided by the "ws" library to test the SSL certs and HTTP headers so we hide the now built-in WebSocket constructor
// ref: https://nodejs.org/api/globals.html#class-websocket
global.WebSocket = null;
}
const { listen, uServer } = require("..");
const { Socket } =
process.env.EIO_CLIENT === "3"
? require("engine.io-client-v3")
: require("engine.io-client");
switch (process.env.EIO_WS_ENGINE) {
case "uws":
console.log(
"[WARN] testing with uWebSockets.js instead of Node.js built-in HTTP server",
);
break;
case "eiows":
console.log("[WARN] testing with eiows instead of ws");
break;
}
/**
* Listen shortcut that fires a callback on an ephemeral port.
*/

View File

@@ -93,7 +93,7 @@ describe("engine", () => {
"Upgrade: IRC/6.9",
"",
"",
].join("\r\n")
].join("\r\n"),
);
const check = setTimeout(() => {
@@ -122,7 +122,7 @@ describe("engine", () => {
"Upgrade: IRC/6.9",
"",
"",
].join("\r\n")
].join("\r\n"),
);
setTimeout(() => {
@@ -154,7 +154,7 @@ describe("engine", () => {
"Upgrade: IRC/6.9",
"",
"",
].join("\r\n")
].join("\r\n"),
);
// send from client to server
@@ -198,7 +198,7 @@ describe("engine", () => {
"Upgrade: IRC/6.9",
"",
"",
].join("\r\n")
].join("\r\n"),
);
// test that socket is still open by writing after the timeout period
@@ -245,7 +245,7 @@ describe("engine", () => {
server.once("close", done);
server.close();
});
}
},
);
});
});

View File

@@ -1,33 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIFtTCCA52gAwIBAgIJAJKBPV3nMXjsMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV
BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
aWRnaXRzIFB0eSBMdGQwHhcNMTUxMTE4MTczODAwWhcNMjUxMTE1MTczODAwWjBF
MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
CgKCAgEAmNWKTumE1f+ptArhPTOcaUReoqBlri/ujIzm1N8Qr0hghS6B8eXGngAS
YM7ziTlLZmLKgZg7TYOs+qK+xNNjSMbkA4Tions7vX3FYAokfh1iSiQigAw3TAwg
brUaA0phucJBjvWI2mDuwzTLhQp1wmGrliAJhXag2ZQt817m6wrsFWuwiviMIHlq
mQhC+vwd2SvW4xGf5zxjzCM8m7pOiJCLjxXwvNphiTR3tb807W00mi5cMFwhmAUT
uSiVkVERubIYEKNSW2ynxzGFfb+GF/ddUxCKsnMDfM+SpMrsTBv9BzJzXU7Hc9jP
NPFtrZiVo9aKn8csTSvKifmfiNwl2YGuWlW++1+ew6Q9rqEqvKHnKU+Cuwt3y37U
ryqrBS47cz1xxFb3fCn+a72ytcHjI9lMqIQ0+IZ0/4cf0TK80ECEQ0CyrCk0E9Qz
eMEzIALRa/pI8uTXdoAtQIlOsfALWeni+QphZ1BVjwZRmr+F1Px2/R30+gAcZHKc
D+0Bm6owvpBWDe1s0DrkwtY3fyZ+OPS5/3eQtyhy9/3vnz9WBw0BGZyN2nzs5HsB
RB5qDBRx+NQz1QYp/Ba3WeVmZURe2NMnS4uEypkWahW1XNQ+g+JJhK1p01s0+v/B
f4DodYEcsw/3fRU0AKdsAkabQ68VIJAYyfQyinpNR9sHDKZ6Dx8CAwEAAaOBpzCB
pDAdBgNVHQ4EFgQUdwTc4idMFJo0xYmoLTJQeD7A59kwdQYDVR0jBG4wbIAUdwTc
4idMFJo0xYmoLTJQeD7A59mhSaRHMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpT
b21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGSCCQCS
gT1d5zF47DAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4ICAQBRLzphOdJO
ekU+iwFLGXJiLfUQi3EORaA3mBtozvyRRUyxu0FqWdGGINcyq7xaZxkQ0P+eBMDv
V3F7FvidhaVe6TR7VJB2gyaWeLd1BSOi5gGBk5kuuqV3VbusNzY+WhJip98HEK+y
XP+Lt/LXJpPwOORF9TiR6IBFQn3fBVhPjsqQRzT468QuZ5DCGQ5UW+wWl43I8OxS
PDiUmHTlwLnLqqVFgSE+VnX4vSUZD8kDf0kkxssg1r56IzneSlRBegSVXIuRCbRf
QmWaxz+D6ffM1eNiE3nQxsgJy3dPL1Lfsaidgz39yAC099pjLyVH23cfmFmT/l5b
OdhRE5D75rL8eXAiI2/voz1M+v7XznHjZEhcVUlFsBXsk3zHb2vQQZRNPLnybTb8
biFpReSIWdpno+F5IrT7z0L8JI3LU0leEFV+Rf525Q+78Rffohxd51fUj0vLKoy9
un0IEkOcaJhHTPc2dMXb2qGcV4MaUEUsERrnanGZmdNd1aD3YAG3C+nJ8gxrEGHO
veD6Xbyf1K8e7H2sqhGCm8eyHgCFGQiul6yQ41ZwjKgoSCJvOJaYUFE18k0S9k/I
rWYvYWRYbDj4GYx+6LUTfyo30KK5jl4KAil92LrGlIfWK4IjUJyPlOJkb6gXkj0l
lfbUHTmnKthFwJS0958xBq7UM7+RzyFIOg==
MIIDBTCCAe2gAwIBAgIUPV8LcHEkiA3LPuNyt8kuDxRqjlQwDQYJKoZIhvcNAQEL
BQAwEjEQMA4GA1UEAwwHVGVzdCBDQTAeFw0yNTEyMTUwNzQ1MDdaFw0yNjEyMTUw
NzQ1MDdaMBIxEDAOBgNVBAMMB1Rlc3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IB
DwAwggEKAoIBAQChKwRqFDrGgqMHjDUx5he6MDQ+BxlMqqpm0qu5pOCCPzSvYDFo
iU4n3lK6uCbemv9Gdk/U99ev8LAGg2SkZAZXBpDrdZepkA84ehUHu5u7PHlXNodE
KL7DLKsvoaYYiQ5rLyBYieDOYqtYJxtfLOeh+tmnNa+G4chpYzkll7OCeEQhbocQ
QLdP8novscoSibp6bPmoVsat8RRru0CK9ND1v+FvJ2R7Lz2isBIr+p9ZrkYkIXa3
OqN3wacZ+doYfAC/a4SK64Jgv+Lz6wuzsc7XVjBGEEaa1P1zd2rh70wNm8Lgmwr/
Oq1Lv7Lg3plXY7e/7V915p96/bxbIiVHiu1JAgMBAAGjUzBRMB0GA1UdDgQWBBRN
SIfG8SbjQTQCuymcNnQh4R5oxDAfBgNVHSMEGDAWgBRNSIfG8SbjQTQCuymcNnQh
4R5oxDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQADHV/KfXlN
ZBmjWWqQRCmJCUsFkn29C0RQQMILbxYkNl08DuSQw68+PsA2YeB4aUikmW5sRHYF
TgLXxgZ+aaGH4WtedumD4RGodcFbxI2WLIilKD4nH0FmB4I9bkULgMyyOZ0g+Vc7
ekynqzYtzQBgK+HLWtIWZRTM/BR5IAzt/SdAVoQwL3EHzlc57Q9rRqYeBiyse3lC
8Ojb9ZLhwv/hihWNd+mFKeWzLAGJIB439AUzKcg96YDKB3Nwosa16g0xslwxvAL/
cKJQ3mh7pIOX3iv9YV/uifRvdCiI/e2p00YjZiS3ggdravSGjLLypw6HFH3hFnw7
2ZzA88NgFQXi
-----END CERTIFICATE-----

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