mirror of
https://github.com/socketio/socket.io.git
synced 2026-01-11 16:08:24 -05:00
Compare commits
37 Commits
4.7.1
...
socket.io@
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50176812a1 | ||
|
|
bf64870957 | ||
|
|
748e18c22e | ||
|
|
b9ce6a25d1 | ||
|
|
54dabe5bff | ||
|
|
e426f3e8e1 | ||
|
|
e36062ca2d | ||
|
|
0bbe8aec77 | ||
|
|
914a8bd2b9 | ||
|
|
d943c3e0b0 | ||
|
|
6ab2509d52 | ||
|
|
d9fb2f64b6 | ||
|
|
2c0a81cd87 | ||
|
|
f8d2644921 | ||
|
|
04640d68cf | ||
|
|
cb6d2e02aa | ||
|
|
80b2c34478 | ||
|
|
0d893196f8 | ||
|
|
df8e70f798 | ||
|
|
8c9ebc30e5 | ||
|
|
efb5c21e85 | ||
|
|
3848280125 | ||
|
|
9a2a83fdd4 | ||
|
|
f6ef267b03 | ||
|
|
1cdf36bfea | ||
|
|
bbf1fdc7a6 | ||
|
|
b4dc83eb9b | ||
|
|
ccbb4c0773 | ||
|
|
d744fda772 | ||
|
|
8259cdac84 | ||
|
|
fd9dd74eee | ||
|
|
c332643ad8 | ||
|
|
3468a197af | ||
|
|
09d45491c4 | ||
|
|
0731c0d2f4 | ||
|
|
03046a64ad | ||
|
|
443e447087 |
38
.github/workflows/ci.yml
vendored
38
.github/workflows/ci.yml
vendored
@@ -22,17 +22,51 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Install TypeScript 4.2
|
||||
run: npm i typescript@4.2
|
||||
if: ${{ matrix.node-version == '16' }}
|
||||
|
||||
- name: Run tests
|
||||
run: npm test
|
||||
env:
|
||||
CI: true
|
||||
|
||||
build-examples:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
example:
|
||||
- custom-parsers
|
||||
- typescript
|
||||
- webpack-build
|
||||
- webpack-build-server
|
||||
- basic-crud-application/angular-client
|
||||
- basic-crud-application/vue-client
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js 20
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Build ${{ matrix.example }}
|
||||
run: |
|
||||
cd examples/${{ matrix.example }}
|
||||
npm install
|
||||
npm run build
|
||||
|
||||
75
CHANGELOG.md
75
CHANGELOG.md
@@ -1,7 +1,14 @@
|
||||
# History
|
||||
|
||||
## 2024
|
||||
|
||||
- [4.7.5](#475-2024-03-14) (Mar 2024)
|
||||
- [4.7.4](#474-2024-01-12) (Jan 2024)
|
||||
- [4.7.3](#473-2024-01-03) (Jan 2024)
|
||||
|
||||
## 2023
|
||||
|
||||
- [4.7.2](#472-2023-08-02) (Aug 2023)
|
||||
- [4.7.1](#471-2023-06-28) (Jun 2023)
|
||||
- [4.7.0](#470-2023-06-22) (Jun 2023)
|
||||
- [4.6.2](#462-2023-05-31) (May 2023)
|
||||
@@ -60,6 +67,70 @@
|
||||
|
||||
# Release notes
|
||||
|
||||
## [4.7.5](https://github.com/socketio/socket.io/compare/4.7.4...4.7.5) (2024-03-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* close the adapters when the server is closed ([bf64870](https://github.com/socketio/socket.io/commit/bf64870957e626a73e0544716a1a41a4ba5093bb))
|
||||
* remove duplicate pipeline when serving bundle ([e426f3e](https://github.com/socketio/socket.io/commit/e426f3e8e1bfea5720c32d30a3663303200ee6ad))
|
||||
|
||||
|
||||
### Dependencies
|
||||
|
||||
- [`engine.io@~6.5.2`](https://github.com/socketio/engine.io/releases/tag/6.5.2) (no change)
|
||||
- [`ws@~8.11.0`](https://github.com/websockets/ws/releases/tag/8.11.0) (no change)
|
||||
|
||||
|
||||
|
||||
## [4.7.4](https://github.com/socketio/socket.io/compare/4.7.3...4.7.4) (2024-01-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **typings:** calling io.emit with no arguments incorrectly errored ([cb6d2e0](https://github.com/socketio/socket.io/commit/cb6d2e02aa7ec03c2de1817d35cffa1128b107ef)), closes [#4914](https://github.com/socketio/socket.io/issues/4914)
|
||||
|
||||
|
||||
### Dependencies
|
||||
|
||||
- [`engine.io@~6.5.2`](https://github.com/socketio/engine.io/releases/tag/6.5.2) (no change)
|
||||
- [`ws@~8.11.0`](https://github.com/websockets/ws/releases/tag/8.11.0) (no change)
|
||||
|
||||
|
||||
|
||||
## [4.7.3](https://github.com/socketio/socket.io/compare/4.7.2...4.7.3) (2024-01-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* return the first response when broadcasting to a single socket ([#4878](https://github.com/socketio/socket.io/issues/4878)) ([df8e70f](https://github.com/socketio/socket.io/commit/df8e70f79822e3887b4f21ca718af8a53bbda2c4))
|
||||
* **typings:** allow to bind to a non-secure Http2Server ([#4853](https://github.com/socketio/socket.io/issues/4853)) ([8c9ebc3](https://github.com/socketio/socket.io/commit/8c9ebc30e5452ff9381af5d79f547394fa55633c))
|
||||
|
||||
|
||||
### Dependencies
|
||||
|
||||
- [`engine.io@~6.5.2`](https://github.com/socketio/engine.io/releases/tag/6.5.2) (no change)
|
||||
- [`ws@~8.11.0`](https://github.com/websockets/ws/releases/tag/8.11.0) (no change)
|
||||
|
||||
|
||||
|
||||
## [4.7.2](https://github.com/socketio/socket.io/compare/4.7.1...4.7.2) (2023-08-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* clean up child namespace when client is rejected in middleware ([#4773](https://github.com/socketio/socket.io/issues/4773)) ([0731c0d](https://github.com/socketio/socket.io/commit/0731c0d2f497d5cce596ea1ec32a67c08bcccbcd))
|
||||
* **webtransport:** properly handle WebTransport-only connections ([3468a19](https://github.com/socketio/socket.io/commit/3468a197afe87e65eb0d779fabd347fe683013ab))
|
||||
* **webtransport:** add proper framing ([a306db0](https://github.com/socketio/engine.io/commit/a306db09e8ddb367c7d62f45fec920f979580b7c))
|
||||
|
||||
|
||||
### Dependencies
|
||||
|
||||
- [`engine.io@~6.5.2`](https://github.com/socketio/engine.io/releases/tag/6.5.2) ([diff](https://github.com/socketio/engine.io/compare/6.5.0...6.5.2))
|
||||
- [`ws@~8.11.0`](https://github.com/websockets/ws/releases/tag/8.11.0) (no change)
|
||||
|
||||
|
||||
|
||||
## [4.7.1](https://github.com/socketio/socket.io/compare/4.7.0...4.7.1) (2023-06-28)
|
||||
|
||||
The client bundle contains a few fixes regarding the WebTransport support.
|
||||
@@ -837,7 +908,7 @@ new Server(3000, {
|
||||
const socket = io("/admin");
|
||||
|
||||
// server-side
|
||||
io.on("connect", socket => {
|
||||
io.on("connection", socket => {
|
||||
// not triggered anymore
|
||||
})
|
||||
|
||||
@@ -988,7 +1059,7 @@ new Server(3000, {
|
||||
const socket = io("/admin");
|
||||
|
||||
// server-side
|
||||
io.on("connect", socket => {
|
||||
io.on("connection", socket => {
|
||||
// not triggered anymore
|
||||
})
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ Some implementations in other languages are also available:
|
||||
- [Python](https://github.com/miguelgrinberg/python-socketio)
|
||||
- [.NET](https://github.com/doghappy/socket.io-client-csharp)
|
||||
- [Rust](https://github.com/1c3t3a/rust-socketio)
|
||||
- [PHP](https://github.com/ElephantIO/elephant.io)
|
||||
|
||||
Its main features are:
|
||||
|
||||
|
||||
6
client-dist/socket.io.esm.min.js
vendored
6
client-dist/socket.io.esm.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
||||
/*!
|
||||
* Socket.IO v4.7.1
|
||||
* (c) 2014-2023 Guillermo Rauch
|
||||
* Socket.IO v4.7.5
|
||||
* (c) 2014-2024 Guillermo Rauch
|
||||
* Released under the MIT License.
|
||||
*/
|
||||
(function (global, factory) {
|
||||
@@ -466,16 +466,124 @@
|
||||
}
|
||||
return packets;
|
||||
};
|
||||
function createPacketEncoderStream() {
|
||||
return new TransformStream({
|
||||
transform: function transform(packet, controller) {
|
||||
encodePacketToBinary(packet, function (encodedPacket) {
|
||||
var payloadLength = encodedPacket.length;
|
||||
var header;
|
||||
// inspired by the WebSocket format: https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers#decoding_payload_length
|
||||
if (payloadLength < 126) {
|
||||
header = new Uint8Array(1);
|
||||
new DataView(header.buffer).setUint8(0, payloadLength);
|
||||
} else if (payloadLength < 65536) {
|
||||
header = new Uint8Array(3);
|
||||
var view = new DataView(header.buffer);
|
||||
view.setUint8(0, 126);
|
||||
view.setUint16(1, payloadLength);
|
||||
} else {
|
||||
header = new Uint8Array(9);
|
||||
var _view = new DataView(header.buffer);
|
||||
_view.setUint8(0, 127);
|
||||
_view.setBigUint64(1, BigInt(payloadLength));
|
||||
}
|
||||
// first bit indicates whether the payload is plain text (0) or binary (1)
|
||||
if (packet.data && typeof packet.data !== "string") {
|
||||
header[0] |= 0x80;
|
||||
}
|
||||
controller.enqueue(header);
|
||||
controller.enqueue(encodedPacket);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
var TEXT_DECODER;
|
||||
function decodePacketFromBinary(data, isBinary, binaryType) {
|
||||
function totalLength(chunks) {
|
||||
return chunks.reduce(function (acc, chunk) {
|
||||
return acc + chunk.length;
|
||||
}, 0);
|
||||
}
|
||||
function concatChunks(chunks, size) {
|
||||
if (chunks[0].length === size) {
|
||||
return chunks.shift();
|
||||
}
|
||||
var buffer = new Uint8Array(size);
|
||||
var j = 0;
|
||||
for (var i = 0; i < size; i++) {
|
||||
buffer[i] = chunks[0][j++];
|
||||
if (j === chunks[0].length) {
|
||||
chunks.shift();
|
||||
j = 0;
|
||||
}
|
||||
}
|
||||
if (chunks.length && j < chunks[0].length) {
|
||||
chunks[0] = chunks[0].slice(j);
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
function createPacketDecoderStream(maxPayload, binaryType) {
|
||||
if (!TEXT_DECODER) {
|
||||
// lazily created for compatibility with old browser platforms
|
||||
TEXT_DECODER = new TextDecoder();
|
||||
}
|
||||
// 48 === "0".charCodeAt(0) (OPEN packet type)
|
||||
// 54 === "6".charCodeAt(0) (NOOP packet type)
|
||||
var isPlainBinary = isBinary || data[0] < 48 || data[0] > 54;
|
||||
return decodePacket(isPlainBinary ? data : TEXT_DECODER.decode(data), binaryType);
|
||||
var chunks = [];
|
||||
var state = 0 /* READ_HEADER */;
|
||||
var expectedLength = -1;
|
||||
var isBinary = false;
|
||||
return new TransformStream({
|
||||
transform: function transform(chunk, controller) {
|
||||
chunks.push(chunk);
|
||||
while (true) {
|
||||
if (state === 0 /* READ_HEADER */) {
|
||||
if (totalLength(chunks) < 1) {
|
||||
break;
|
||||
}
|
||||
var header = concatChunks(chunks, 1);
|
||||
isBinary = (header[0] & 0x80) === 0x80;
|
||||
expectedLength = header[0] & 0x7f;
|
||||
if (expectedLength < 126) {
|
||||
state = 3 /* READ_PAYLOAD */;
|
||||
} else if (expectedLength === 126) {
|
||||
state = 1 /* READ_EXTENDED_LENGTH_16 */;
|
||||
} else {
|
||||
state = 2 /* READ_EXTENDED_LENGTH_64 */;
|
||||
}
|
||||
} else if (state === 1 /* READ_EXTENDED_LENGTH_16 */) {
|
||||
if (totalLength(chunks) < 2) {
|
||||
break;
|
||||
}
|
||||
var headerArray = concatChunks(chunks, 2);
|
||||
expectedLength = new DataView(headerArray.buffer, headerArray.byteOffset, headerArray.length).getUint16(0);
|
||||
state = 3 /* READ_PAYLOAD */;
|
||||
} else if (state === 2 /* READ_EXTENDED_LENGTH_64 */) {
|
||||
if (totalLength(chunks) < 8) {
|
||||
break;
|
||||
}
|
||||
var _headerArray = concatChunks(chunks, 8);
|
||||
var view = new DataView(_headerArray.buffer, _headerArray.byteOffset, _headerArray.length);
|
||||
var n = view.getUint32(0);
|
||||
if (n > Math.pow(2, 53 - 32) - 1) {
|
||||
// the maximum safe integer in JavaScript is 2^53 - 1
|
||||
controller.enqueue(ERROR_PACKET);
|
||||
break;
|
||||
}
|
||||
expectedLength = n * Math.pow(2, 32) + view.getUint32(4);
|
||||
state = 3 /* READ_PAYLOAD */;
|
||||
} else {
|
||||
if (totalLength(chunks) < expectedLength) {
|
||||
break;
|
||||
}
|
||||
var data = concatChunks(chunks, expectedLength);
|
||||
controller.enqueue(decodePacket(isBinary ? data : TEXT_DECODER.decode(data), binaryType));
|
||||
state = 0 /* READ_HEADER */;
|
||||
}
|
||||
|
||||
if (expectedLength === 0 || expectedLength > maxPayload) {
|
||||
controller.enqueue(ERROR_PACKET);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
var protocol$1 = 4;
|
||||
|
||||
@@ -1455,7 +1563,7 @@
|
||||
} catch (err) {
|
||||
return this.emitReserved("error", err);
|
||||
}
|
||||
this.ws.binaryType = this.socket.binaryType || defaultBinaryType;
|
||||
this.ws.binaryType = this.socket.binaryType;
|
||||
this.addEventListeners();
|
||||
}
|
||||
/**
|
||||
@@ -1565,11 +1673,6 @@
|
||||
return WS;
|
||||
}(Transport);
|
||||
|
||||
function shouldIncludeBinaryHeader(packet, encoded) {
|
||||
// 48 === "0".charCodeAt(0) (OPEN packet type)
|
||||
// 54 === "6".charCodeAt(0) (NOOP packet type)
|
||||
return packet.type === "message" && typeof packet.data !== "string" && encoded[0] >= 48 && encoded[0] <= 54;
|
||||
}
|
||||
var WT = /*#__PURE__*/function (_Transport) {
|
||||
_inherits(WT, _Transport);
|
||||
var _super = _createSuper(WT);
|
||||
@@ -1600,9 +1703,11 @@
|
||||
// note: we could have used async/await, but that would require some additional polyfills
|
||||
this.transport.ready.then(function () {
|
||||
_this.transport.createBidirectionalStream().then(function (stream) {
|
||||
var reader = stream.readable.getReader();
|
||||
_this.writer = stream.writable.getWriter();
|
||||
var binaryFlag;
|
||||
var decoderStream = createPacketDecoderStream(Number.MAX_SAFE_INTEGER, _this.socket.binaryType);
|
||||
var reader = stream.readable.pipeThrough(decoderStream).getReader();
|
||||
var encoderStream = createPacketEncoderStream();
|
||||
encoderStream.readable.pipeTo(stream.writable);
|
||||
_this.writer = encoderStream.writable.getWriter();
|
||||
var read = function read() {
|
||||
reader.read().then(function (_ref) {
|
||||
var done = _ref.done,
|
||||
@@ -1610,19 +1715,18 @@
|
||||
if (done) {
|
||||
return;
|
||||
}
|
||||
if (!binaryFlag && value.byteLength === 1 && value[0] === 54) {
|
||||
binaryFlag = true;
|
||||
} else {
|
||||
// TODO expose binarytype
|
||||
_this.onPacket(decodePacketFromBinary(value, binaryFlag, "arraybuffer"));
|
||||
binaryFlag = false;
|
||||
}
|
||||
_this.onPacket(value);
|
||||
read();
|
||||
})["catch"](function (err) {});
|
||||
};
|
||||
read();
|
||||
var handshake = _this.query.sid ? "0{\"sid\":\"".concat(_this.query.sid, "\"}") : "0";
|
||||
_this.writer.write(new TextEncoder().encode(handshake)).then(function () {
|
||||
var packet = {
|
||||
type: "open"
|
||||
};
|
||||
if (_this.query.sid) {
|
||||
packet.data = "{\"sid\":\"".concat(_this.query.sid, "\"}");
|
||||
}
|
||||
_this.writer.write(packet).then(function () {
|
||||
return _this.onOpen();
|
||||
});
|
||||
});
|
||||
@@ -1636,18 +1740,13 @@
|
||||
var _loop = function _loop() {
|
||||
var packet = packets[i];
|
||||
var lastPacket = i === packets.length - 1;
|
||||
encodePacketToBinary(packet, function (data) {
|
||||
if (shouldIncludeBinaryHeader(packet, data)) {
|
||||
_this2.writer.write(Uint8Array.of(54));
|
||||
_this2.writer.write(packet).then(function () {
|
||||
if (lastPacket) {
|
||||
nextTick(function () {
|
||||
_this2.writable = true;
|
||||
_this2.emitReserved("drain");
|
||||
}, _this2.setTimeoutFn);
|
||||
}
|
||||
_this2.writer.write(data).then(function () {
|
||||
if (lastPacket) {
|
||||
nextTick(function () {
|
||||
_this2.writable = true;
|
||||
_this2.emitReserved("drain");
|
||||
}, _this2.setTimeoutFn);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
for (var i = 0; i < packets.length; i++) {
|
||||
@@ -1749,6 +1848,7 @@
|
||||
var opts = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
||||
_classCallCheck(this, Socket);
|
||||
_this = _super.call(this);
|
||||
_this.binaryType = defaultBinaryType;
|
||||
_this.writeBuffer = [];
|
||||
if (uri && "object" === _typeof(uri)) {
|
||||
opts = uri;
|
||||
@@ -2038,12 +2138,12 @@
|
||||
this.emitReserved("packet", packet);
|
||||
// Socket is live - any packet counts
|
||||
this.emitReserved("heartbeat");
|
||||
this.resetPingTimeout();
|
||||
switch (packet.type) {
|
||||
case "open":
|
||||
this.onHandshake(JSON.parse(packet.data));
|
||||
break;
|
||||
case "ping":
|
||||
this.resetPingTimeout();
|
||||
this.sendPacket("pong");
|
||||
this.emitReserved("ping");
|
||||
this.emitReserved("pong");
|
||||
@@ -2922,6 +3022,29 @@
|
||||
*/
|
||||
_this._queueSeq = 0;
|
||||
_this.ids = 0;
|
||||
/**
|
||||
* A map containing acknowledgement handlers.
|
||||
*
|
||||
* The `withError` attribute is used to differentiate handlers that accept an error as first argument:
|
||||
*
|
||||
* - `socket.emit("test", (err, value) => { ... })` with `ackTimeout` option
|
||||
* - `socket.timeout(5000).emit("test", (err, value) => { ... })`
|
||||
* - `const value = await socket.emitWithAck("test")`
|
||||
*
|
||||
* From those that don't:
|
||||
*
|
||||
* - `socket.emit("test", (value) => { ... });`
|
||||
*
|
||||
* In the first case, the handlers will be called with an error when:
|
||||
*
|
||||
* - the timeout is reached
|
||||
* - the socket gets disconnected
|
||||
*
|
||||
* In the second case, the handlers will be simply discarded upon disconnection, since the client will never receive
|
||||
* an acknowledgement from the server.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_this.acks = {};
|
||||
_this.flags = {};
|
||||
_this.io = io;
|
||||
@@ -3116,14 +3239,16 @@
|
||||
}
|
||||
ack.call(_this2, new Error("operation has timed out"));
|
||||
}, timeout);
|
||||
this.acks[id] = function () {
|
||||
var fn = function fn() {
|
||||
// @ts-ignore
|
||||
_this2.io.clearTimeoutFn(timer);
|
||||
for (var _len3 = arguments.length, args = new Array(_len3), _key3 = 0; _key3 < _len3; _key3++) {
|
||||
args[_key3] = arguments[_key3];
|
||||
}
|
||||
ack.apply(_this2, [null].concat(args));
|
||||
ack.apply(_this2, args);
|
||||
};
|
||||
fn.withError = true;
|
||||
this.acks[id] = fn;
|
||||
}
|
||||
/**
|
||||
* Emits an event and waits for an acknowledgement
|
||||
@@ -3148,16 +3273,12 @@
|
||||
for (var _len4 = arguments.length, args = new Array(_len4 > 1 ? _len4 - 1 : 0), _key4 = 1; _key4 < _len4; _key4++) {
|
||||
args[_key4 - 1] = arguments[_key4];
|
||||
}
|
||||
// the timeout flag is optional
|
||||
var withErr = this.flags.timeout !== undefined || this._opts.ackTimeout !== undefined;
|
||||
return new Promise(function (resolve, reject) {
|
||||
args.push(function (arg1, arg2) {
|
||||
if (withErr) {
|
||||
return arg1 ? reject(arg1) : resolve(arg2);
|
||||
} else {
|
||||
return resolve(arg1);
|
||||
}
|
||||
});
|
||||
var fn = function fn(arg1, arg2) {
|
||||
return arg1 ? reject(arg1) : resolve(arg2);
|
||||
};
|
||||
fn.withError = true;
|
||||
args.push(fn);
|
||||
_this3.emit.apply(_this3, [ev].concat(args));
|
||||
});
|
||||
}
|
||||
@@ -3305,6 +3426,31 @@
|
||||
this.connected = false;
|
||||
delete this.id;
|
||||
this.emitReserved("disconnect", reason, description);
|
||||
this._clearAcks();
|
||||
}
|
||||
/**
|
||||
* Clears the acknowledgement handlers upon disconnection, since the client will never receive an acknowledgement from
|
||||
* the server.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
}, {
|
||||
key: "_clearAcks",
|
||||
value: function _clearAcks() {
|
||||
var _this6 = this;
|
||||
Object.keys(this.acks).forEach(function (id) {
|
||||
var isBuffered = _this6.sendBuffer.some(function (packet) {
|
||||
return String(packet.id) === id;
|
||||
});
|
||||
if (!isBuffered) {
|
||||
// note: handlers that do not accept an error as first argument are ignored here
|
||||
var ack = _this6.acks[id];
|
||||
delete _this6.acks[id];
|
||||
if (ack.withError) {
|
||||
ack.call(_this6, new Error("socket has been disconnected"));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Called with socket packet.
|
||||
@@ -3412,7 +3558,7 @@
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Called upon a server acknowlegement.
|
||||
* Called upon a server acknowledgement.
|
||||
*
|
||||
* @param packet
|
||||
* @private
|
||||
@@ -3421,10 +3567,16 @@
|
||||
key: "onack",
|
||||
value: function onack(packet) {
|
||||
var ack = this.acks[packet.id];
|
||||
if ("function" === typeof ack) {
|
||||
ack.apply(this, packet.data);
|
||||
delete this.acks[packet.id];
|
||||
if (typeof ack !== "function") {
|
||||
return;
|
||||
}
|
||||
delete this.acks[packet.id];
|
||||
// @ts-ignore FIXME ack is incorrectly inferred as 'never'
|
||||
if (ack.withError) {
|
||||
packet.data.unshift(null);
|
||||
}
|
||||
// @ts-ignore
|
||||
ack.apply(this, packet.data);
|
||||
}
|
||||
/**
|
||||
* Called upon server connect.
|
||||
@@ -3450,14 +3602,14 @@
|
||||
}, {
|
||||
key: "emitBuffered",
|
||||
value: function emitBuffered() {
|
||||
var _this6 = this;
|
||||
var _this7 = this;
|
||||
this.receiveBuffer.forEach(function (args) {
|
||||
return _this6.emitEvent(args);
|
||||
return _this7.emitEvent(args);
|
||||
});
|
||||
this.receiveBuffer = [];
|
||||
this.sendBuffer.forEach(function (packet) {
|
||||
_this6.notifyOutgoingListeners(packet);
|
||||
_this6.packet(packet);
|
||||
_this7.notifyOutgoingListeners(packet);
|
||||
_this7.packet(packet);
|
||||
});
|
||||
this.sendBuffer = [];
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
6
client-dist/socket.io.min.js
vendored
6
client-dist/socket.io.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
6
client-dist/socket.io.msgpack.min.js
vendored
6
client-dist/socket.io.msgpack.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -15,7 +15,7 @@ interface Todo {
|
||||
|
||||
let todos: Array<Todo> = [];
|
||||
|
||||
io.on("connect", (socket) => {
|
||||
io.on("connection", (socket) => {
|
||||
socket.emit("todos", todos);
|
||||
|
||||
// note: we could also create a CRUD (create/read/update/delete) service for the todo list
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
|
||||
# For additional information regarding the format and rule options, please see:
|
||||
# https://github.com/browserslist/browserslist#queries
|
||||
|
||||
# For the full list of supported browsers by the Angular framework, please see:
|
||||
# https://angular.io/guide/browser-support
|
||||
|
||||
# You can see what browsers were selected by your queries by running:
|
||||
# npx browserslist
|
||||
|
||||
last 1 Chrome version
|
||||
last 1 Firefox version
|
||||
last 2 Edge major versions
|
||||
last 2 Safari major versions
|
||||
last 2 iOS major versions
|
||||
Firefox ESR
|
||||
not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line.
|
||||
@@ -1,21 +1,18 @@
|
||||
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# compiled output
|
||||
# Compiled output
|
||||
/dist
|
||||
/tmp
|
||||
/out-tsc
|
||||
# Only exists if Bazel was run
|
||||
/bazel-out
|
||||
|
||||
# dependencies
|
||||
# Node
|
||||
/node_modules
|
||||
|
||||
# profiling files
|
||||
chrome-profiler-events*.json
|
||||
speed-measure-plugin*.json
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.idea/
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
@@ -23,7 +20,7 @@ speed-measure-plugin*.json
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# IDE - VSCode
|
||||
# Visual Studio Code
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
@@ -31,16 +28,15 @@ speed-measure-plugin*.json
|
||||
!.vscode/extensions.json
|
||||
.history/*
|
||||
|
||||
# misc
|
||||
/.sass-cache
|
||||
# Miscellaneous
|
||||
/.angular/cache
|
||||
.sass-cache/
|
||||
/connect.lock
|
||||
/coverage
|
||||
/libpeerconnection.log
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
testem.log
|
||||
/typings
|
||||
|
||||
# System Files
|
||||
# System files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
# Angular TodoMVC + Socket.IO
|
||||
# AngularClient
|
||||
|
||||
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 11.0.4.
|
||||
|
||||
Inspired from the [TodoMVC](http://todomvc.com/) [angular example](https://github.com/tastejs/todomvc/tree/master/examples/angular2).
|
||||
|
||||

|
||||
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 17.0.2.
|
||||
|
||||
## Development server
|
||||
|
||||
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
|
||||
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files.
|
||||
|
||||
## Code scaffolding
|
||||
|
||||
@@ -16,7 +12,7 @@ Run `ng generate component component-name` to generate a new component. You can
|
||||
|
||||
## Build
|
||||
|
||||
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build.
|
||||
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
|
||||
|
||||
## Running unit tests
|
||||
|
||||
@@ -24,7 +20,7 @@ Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.
|
||||
|
||||
## Running end-to-end tests
|
||||
|
||||
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
|
||||
Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.
|
||||
|
||||
## Further help
|
||||
|
||||
|
||||
@@ -3,26 +3,23 @@
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"angular-todomvc": {
|
||||
"angular-client": {
|
||||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@schematics/angular:application": {
|
||||
"strict": true
|
||||
}
|
||||
},
|
||||
"schematics": {},
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"builder": "@angular-devkit/build-angular:application",
|
||||
"options": {
|
||||
"outputPath": "dist/angular-todomvc",
|
||||
"outputPath": "dist/angular-client",
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"browser": "src/main.ts",
|
||||
"polyfills": [
|
||||
"zone.js"
|
||||
],
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"aot": true,
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets"
|
||||
@@ -34,19 +31,6 @@
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.prod.ts"
|
||||
}
|
||||
],
|
||||
"optimization": true,
|
||||
"outputHashing": "all",
|
||||
"sourceMap": false,
|
||||
"namedChunks": false,
|
||||
"extractLicenses": true,
|
||||
"vendorChunk": false,
|
||||
"buildOptimizer": true,
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
@@ -58,34 +42,49 @@
|
||||
"maximumWarning": "2kb",
|
||||
"maximumError": "4kb"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
},
|
||||
"development": {
|
||||
"optimization": false,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true,
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.development.ts"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"options": {
|
||||
"browserTarget": "angular-todomvc:build"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "angular-todomvc:build:production"
|
||||
"buildTarget": "angular-client:build:production"
|
||||
},
|
||||
"development": {
|
||||
"buildTarget": "angular-client:build:development"
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"browserTarget": "angular-todomvc:build"
|
||||
"buildTarget": "angular-client:build"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"main": "src/test.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"polyfills": [
|
||||
"zone.js",
|
||||
"zone.js/testing"
|
||||
],
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"karmaConfig": "karma.conf.js",
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets"
|
||||
@@ -95,34 +94,8 @@
|
||||
],
|
||||
"scripts": []
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"options": {
|
||||
"tsConfig": [
|
||||
"tsconfig.app.json",
|
||||
"tsconfig.spec.json",
|
||||
"e2e/tsconfig.json"
|
||||
],
|
||||
"exclude": [
|
||||
"**/node_modules/**"
|
||||
]
|
||||
}
|
||||
},
|
||||
"e2e": {
|
||||
"builder": "@angular-devkit/build-angular:protractor",
|
||||
"options": {
|
||||
"protractorConfig": "e2e/protractor.conf.js",
|
||||
"devServerTarget": "angular-todomvc:serve"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"devServerTarget": "angular-todomvc:serve:production"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultProject": "angular-todomvc"
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 205 KiB |
@@ -1,37 +0,0 @@
|
||||
// @ts-check
|
||||
// Protractor configuration file, see link for more information
|
||||
// https://github.com/angular/protractor/blob/master/lib/config.ts
|
||||
|
||||
const { SpecReporter, StacktraceOption } = require('jasmine-spec-reporter');
|
||||
|
||||
/**
|
||||
* @type { import("protractor").Config }
|
||||
*/
|
||||
exports.config = {
|
||||
allScriptsTimeout: 11000,
|
||||
specs: [
|
||||
'./src/**/*.e2e-spec.ts'
|
||||
],
|
||||
capabilities: {
|
||||
browserName: 'chrome'
|
||||
},
|
||||
directConnect: true,
|
||||
SELENIUM_PROMISE_MANAGER: false,
|
||||
baseUrl: 'http://localhost:4200/',
|
||||
framework: 'jasmine',
|
||||
jasmineNodeOpts: {
|
||||
showColors: true,
|
||||
defaultTimeoutInterval: 30000,
|
||||
print: function() {}
|
||||
},
|
||||
onPrepare() {
|
||||
require('ts-node').register({
|
||||
project: require('path').join(__dirname, './tsconfig.json')
|
||||
});
|
||||
jasmine.getEnv().addReporter(new SpecReporter({
|
||||
spec: {
|
||||
displayStacktrace: StacktraceOption.PRETTY
|
||||
}
|
||||
}));
|
||||
}
|
||||
};
|
||||
@@ -1,23 +0,0 @@
|
||||
import { AppPage } from './app.po';
|
||||
import { browser, logging } from 'protractor';
|
||||
|
||||
describe('workspace-project App', () => {
|
||||
let page: AppPage;
|
||||
|
||||
beforeEach(() => {
|
||||
page = new AppPage();
|
||||
});
|
||||
|
||||
it('should display welcome message', async () => {
|
||||
await page.navigateTo();
|
||||
expect(await page.getTitleText()).toEqual('angular-todomvc app is running!');
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// Assert that there are no errors emitted from the browser
|
||||
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
|
||||
expect(logs).not.toContain(jasmine.objectContaining({
|
||||
level: logging.Level.SEVERE,
|
||||
} as logging.Entry));
|
||||
});
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import { browser, by, element } from 'protractor';
|
||||
|
||||
export class AppPage {
|
||||
async navigateTo(): Promise<unknown> {
|
||||
return browser.get(browser.baseUrl);
|
||||
}
|
||||
|
||||
async getTitleText(): Promise<string> {
|
||||
return element(by.css('app-root .content span')).getText();
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../out-tsc/e2e",
|
||||
"module": "commonjs",
|
||||
"target": "es2018",
|
||||
"types": [
|
||||
"jasmine",
|
||||
"node"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
// Karma configuration file, see link for more information
|
||||
// https://karma-runner.github.io/1.0/config/configuration-file.html
|
||||
|
||||
module.exports = function (config) {
|
||||
config.set({
|
||||
basePath: '',
|
||||
frameworks: ['jasmine', '@angular-devkit/build-angular'],
|
||||
plugins: [
|
||||
require('karma-jasmine'),
|
||||
require('karma-chrome-launcher'),
|
||||
require('karma-jasmine-html-reporter'),
|
||||
require('karma-coverage'),
|
||||
require('@angular-devkit/build-angular/plugins/karma')
|
||||
],
|
||||
client: {
|
||||
jasmine: {
|
||||
// you can add configuration options for Jasmine here
|
||||
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
|
||||
// for example, you can disable the random execution with `random: false`
|
||||
// or set a specific seed with `seed: 4321`
|
||||
},
|
||||
clearContext: false // leave Jasmine Spec Runner output visible in browser
|
||||
},
|
||||
jasmineHtmlReporter: {
|
||||
suppressAll: true // removes the duplicated traces
|
||||
},
|
||||
coverageReporter: {
|
||||
dir: require('path').join(__dirname, './coverage/angular-todomvc'),
|
||||
subdir: '.',
|
||||
reporters: [
|
||||
{ type: 'html' },
|
||||
{ type: 'text-summary' }
|
||||
]
|
||||
},
|
||||
reporters: ['progress', 'kjhtml'],
|
||||
port: 9876,
|
||||
colors: true,
|
||||
logLevel: config.LOG_INFO,
|
||||
autoWatch: true,
|
||||
browsers: ['Chrome'],
|
||||
singleRun: false,
|
||||
restartOnFileChange: true
|
||||
});
|
||||
};
|
||||
@@ -1,46 +1,40 @@
|
||||
{
|
||||
"name": "angular-todomvc",
|
||||
"name": "angular-client",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"build": "ng build",
|
||||
"test": "ng test",
|
||||
"lint": "ng lint",
|
||||
"e2e": "ng e2e"
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"test": "ng test"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "~11.0.4",
|
||||
"@angular/common": "~11.0.4",
|
||||
"@angular/compiler": "~11.0.4",
|
||||
"@angular/core": "~11.0.4",
|
||||
"@angular/forms": "~11.0.4",
|
||||
"@angular/platform-browser": "~11.0.4",
|
||||
"@angular/platform-browser-dynamic": "~11.0.4",
|
||||
"@angular/router": "~11.0.4",
|
||||
"rxjs": "~6.6.0",
|
||||
"socket.io-client": "^4.0.0",
|
||||
"tslib": "^2.0.0",
|
||||
"zone.js": "~0.10.2"
|
||||
"@angular/animations": "^17.0.0",
|
||||
"@angular/common": "^17.0.0",
|
||||
"@angular/compiler": "^17.0.0",
|
||||
"@angular/core": "^17.0.0",
|
||||
"@angular/forms": "^17.0.0",
|
||||
"@angular/platform-browser": "^17.0.0",
|
||||
"@angular/platform-browser-dynamic": "^17.0.0",
|
||||
"@angular/router": "^17.0.0",
|
||||
"rxjs": "~7.8.0",
|
||||
"socket.io-client": "^4.7.2",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.14.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "~0.1100.4",
|
||||
"@angular/cli": "~11.0.4",
|
||||
"@angular/compiler-cli": "~11.0.4",
|
||||
"@types/jasmine": "~3.6.0",
|
||||
"@types/node": "^12.11.1",
|
||||
"codelyzer": "^6.0.0",
|
||||
"jasmine-core": "~3.6.0",
|
||||
"jasmine-spec-reporter": "~5.0.0",
|
||||
"karma": "~5.1.0",
|
||||
"karma-chrome-launcher": "~3.1.0",
|
||||
"karma-coverage": "~2.0.3",
|
||||
"karma-jasmine": "~4.0.0",
|
||||
"karma-jasmine-html-reporter": "~1.5.0",
|
||||
"protractor": "~7.0.0",
|
||||
"ts-node": "~8.3.0",
|
||||
"tslint": "~6.1.0",
|
||||
"typescript": "~4.0.2"
|
||||
"@angular-devkit/build-angular": "^17.0.2",
|
||||
"@angular/cli": "^17.0.2",
|
||||
"@angular/compiler-cli": "^17.0.0",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
"@types/node": "^20.9.2",
|
||||
"jasmine-core": "~5.1.0",
|
||||
"karma": "~6.4.0",
|
||||
"karma-chrome-launcher": "~3.2.0",
|
||||
"karma-coverage": "~2.2.0",
|
||||
"karma-jasmine": "~5.1.0",
|
||||
"karma-jasmine-html-reporter": "~2.1.0",
|
||||
"typescript": "~5.2.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<section class="todoapp">
|
||||
<header class="header">
|
||||
<h1>todos</h1>
|
||||
<input class="new-todo" placeholder="What needs to be done?" autofocus="" [(ngModel)]="newTodoText" (keyup.enter)="addTodo()">
|
||||
<!-- <input class="new-todo" placeholder="What needs to be done?" autofocus="" [(ngModel)]="newTodoText" (keyup.enter)="addTodo()">-->
|
||||
<input class="new-todo" placeholder="What needs to be done?" autofocus="" [formControl]="newTodoText" (keyup.enter)="addTodo()">
|
||||
</header>
|
||||
<section class="main" *ngIf="todoStore.todos.length > 0">
|
||||
<input id="toggle-all" class="toggle-all" type="checkbox" *ngIf="todoStore.todos.length" #toggleall [checked]="todoStore.allCompleted()" (click)="todoStore.setAllTo(toggleall.checked)">
|
||||
|
||||
@@ -4,9 +4,7 @@ import { AppComponent } from './app.component';
|
||||
describe('AppComponent', () => {
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
AppComponent
|
||||
],
|
||||
imports: [AppComponent],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
@@ -16,16 +14,16 @@ describe('AppComponent', () => {
|
||||
expect(app).toBeTruthy();
|
||||
});
|
||||
|
||||
it(`should have as title 'angular-todomvc'`, () => {
|
||||
it(`should have the 'angular-client' title`, () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.componentInstance;
|
||||
expect(app.title).toEqual('angular-todomvc');
|
||||
expect(app.title).toEqual('angular-client');
|
||||
});
|
||||
|
||||
it('should render title', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
fixture.detectChanges();
|
||||
const compiled = fixture.nativeElement;
|
||||
expect(compiled.querySelector('.content span').textContent).toContain('angular-todomvc app is running!');
|
||||
const compiled = fixture.nativeElement as HTMLElement;
|
||||
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, angular-client');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { TodoStore, Todo } from './store';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterOutlet } from '@angular/router';
|
||||
import {type Todo, TodoStore} from "./store";
|
||||
import { FormControl, ReactiveFormsModule } from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
standalone: true,
|
||||
imports: [CommonModule, RouterOutlet, ReactiveFormsModule],
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.css']
|
||||
styleUrl: './app.component.css',
|
||||
providers: [TodoStore]
|
||||
})
|
||||
export class AppComponent {
|
||||
todoStore: TodoStore;
|
||||
newTodoText = '';
|
||||
newTodoText = new FormControl('');
|
||||
|
||||
constructor(todoStore: TodoStore) {
|
||||
this.todoStore = todoStore;
|
||||
constructor(readonly todoStore: TodoStore) {
|
||||
}
|
||||
|
||||
stopEditing(todo: Todo, editedTitle: string) {
|
||||
@@ -51,9 +55,9 @@ export class AppComponent {
|
||||
}
|
||||
|
||||
addTodo() {
|
||||
if (this.newTodoText.trim().length) {
|
||||
this.todoStore.add(this.newTodoText);
|
||||
this.newTodoText = '';
|
||||
if (this.newTodoText.value?.trim().length) {
|
||||
this.todoStore.add(this.newTodoText.value!);
|
||||
this.newTodoText.setValue('');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { ApplicationConfig } from '@angular/core';
|
||||
import { provideRouter } from '@angular/router';
|
||||
|
||||
import { routes } from './app.routes';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [provideRouter(routes)]
|
||||
};
|
||||
@@ -1,19 +0,0 @@
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
import { AppComponent } from './app.component';
|
||||
import { TodoStore } from './store';
|
||||
import { FormsModule } from "@angular/forms";
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
FormsModule
|
||||
],
|
||||
providers: [TodoStore],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
export class AppModule { }
|
||||
@@ -0,0 +1,3 @@
|
||||
import { Routes } from '@angular/router';
|
||||
|
||||
export const routes: Routes = [];
|
||||
@@ -1,6 +1,7 @@
|
||||
import { io, Socket } from "socket.io-client";
|
||||
import { ClientEvents, ServerEvents } from "../../../server/lib/events";
|
||||
import { ClientEvents, ServerEvents } from "../../../common/events";
|
||||
import { environment } from '../environments/environment';
|
||||
import {Injectable} from "@angular/core";
|
||||
|
||||
export interface Todo {
|
||||
id: string,
|
||||
@@ -18,6 +19,7 @@ const mapTodo = (todo: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class TodoStore {
|
||||
public todos: Array<Todo> = [];
|
||||
private socket: Socket<ServerEvents, ClientEvents>;
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export const environment = {
|
||||
serverUrl: "http://localhost:3000"
|
||||
};
|
||||
@@ -1,4 +0,0 @@
|
||||
export const environment = {
|
||||
production: true,
|
||||
serverUrl: "https://my-custom-domain.com"
|
||||
};
|
||||
@@ -1,17 +1,3 @@
|
||||
// This file can be replaced during build by using the `fileReplacements` array.
|
||||
// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
|
||||
// The list of file replacements can be found in `angular.json`.
|
||||
|
||||
export const environment = {
|
||||
production: false,
|
||||
serverUrl: "http://localhost:3000"
|
||||
serverUrl: "https://my-custom-domain.com"
|
||||
};
|
||||
|
||||
/*
|
||||
* For easier debugging in development mode, you can import the following file
|
||||
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
|
||||
*
|
||||
* This import should be commented out in production mode because it will have a negative impact
|
||||
* on performance if an error is thrown.
|
||||
*/
|
||||
// import 'zone.js/dist/zone-error'; // Included with Angular CLI.
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 948 B After Width: | Height: | Size: 15 KiB |
@@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Angular Todo MVC</title>
|
||||
<title>AngularClient</title>
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import { enableProdMode } from '@angular/core';
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||
import { bootstrapApplication } from '@angular/platform-browser';
|
||||
import { appConfig } from './app/app.config';
|
||||
import { AppComponent } from './app/app.component';
|
||||
|
||||
import { AppModule } from './app/app.module';
|
||||
import { environment } from './environments/environment';
|
||||
|
||||
if (environment.production) {
|
||||
enableProdMode();
|
||||
}
|
||||
|
||||
platformBrowserDynamic().bootstrapModule(AppModule)
|
||||
.catch(err => console.error(err));
|
||||
bootstrapApplication(AppComponent, appConfig)
|
||||
.catch((err) => console.error(err));
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
/**
|
||||
* This file includes polyfills needed by Angular and is loaded before the app.
|
||||
* You can add your own extra polyfills to this file.
|
||||
*
|
||||
* This file is divided into 2 sections:
|
||||
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
|
||||
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
|
||||
* file.
|
||||
*
|
||||
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
|
||||
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
|
||||
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
|
||||
*
|
||||
* Learn more in https://angular.io/guide/browser-support
|
||||
*/
|
||||
|
||||
/***************************************************************************************************
|
||||
* BROWSER POLYFILLS
|
||||
*/
|
||||
|
||||
/** IE11 requires the following for NgClass support on SVG elements */
|
||||
// import 'classlist.js'; // Run `npm install --save classlist.js`.
|
||||
|
||||
/**
|
||||
* Web Animations `@angular/platform-browser/animations`
|
||||
* Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
|
||||
* Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
|
||||
*/
|
||||
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
|
||||
|
||||
/**
|
||||
* By default, zone.js will patch all possible macroTask and DomEvents
|
||||
* user can disable parts of macroTask/DomEvents patch by setting following flags
|
||||
* because those flags need to be set before `zone.js` being loaded, and webpack
|
||||
* will put import in the top of bundle, so user need to create a separate file
|
||||
* in this directory (for example: zone-flags.ts), and put the following flags
|
||||
* into that file, and then add the following code before importing zone.js.
|
||||
* import './zone-flags';
|
||||
*
|
||||
* The flags allowed in zone-flags.ts are listed here.
|
||||
*
|
||||
* The following flags will work for all browsers.
|
||||
*
|
||||
* (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
|
||||
* (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
|
||||
* (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
|
||||
*
|
||||
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
|
||||
* with the following flag, it will bypass `zone.js` patch for IE/Edge
|
||||
*
|
||||
* (window as any).__Zone_enable_cross_context_check = true;
|
||||
*
|
||||
*/
|
||||
|
||||
/***************************************************************************************************
|
||||
* Zone JS is required by default for Angular itself.
|
||||
*/
|
||||
import 'zone.js/dist/zone'; // Included with Angular CLI.
|
||||
|
||||
|
||||
/***************************************************************************************************
|
||||
* APPLICATION IMPORTS
|
||||
*/
|
||||
@@ -1,25 +0,0 @@
|
||||
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
|
||||
|
||||
import 'zone.js/dist/zone-testing';
|
||||
import { getTestBed } from '@angular/core/testing';
|
||||
import {
|
||||
BrowserDynamicTestingModule,
|
||||
platformBrowserDynamicTesting
|
||||
} from '@angular/platform-browser-dynamic/testing';
|
||||
|
||||
declare const require: {
|
||||
context(path: string, deep?: boolean, filter?: RegExp): {
|
||||
keys(): string[];
|
||||
<T>(id: string): T;
|
||||
};
|
||||
};
|
||||
|
||||
// First, initialize the Angular testing environment.
|
||||
getTestBed().initTestEnvironment(
|
||||
BrowserDynamicTestingModule,
|
||||
platformBrowserDynamicTesting()
|
||||
);
|
||||
// Then we find all the tests.
|
||||
const context = require.context('./', true, /\.spec\.ts$/);
|
||||
// And load the modules.
|
||||
context.keys().map(context);
|
||||
@@ -6,8 +6,7 @@
|
||||
"types": []
|
||||
},
|
||||
"files": [
|
||||
"src/main.ts",
|
||||
"src/polyfills.ts"
|
||||
"src/main.ts"
|
||||
],
|
||||
"include": [
|
||||
"src/**/*.d.ts"
|
||||
|
||||
@@ -2,26 +2,29 @@
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"outDir": "./dist/out-tsc",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"esModuleInterop": true,
|
||||
"sourceMap": true,
|
||||
"declaration": false,
|
||||
"downlevelIteration": true,
|
||||
"experimentalDecorators": true,
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"target": "es2015",
|
||||
"module": "es2020",
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
"useDefineForClassFields": false,
|
||||
"lib": [
|
||||
"es2018",
|
||||
"ES2022",
|
||||
"dom"
|
||||
]
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"strictTemplates": true
|
||||
|
||||
@@ -7,10 +7,6 @@
|
||||
"jasmine"
|
||||
]
|
||||
},
|
||||
"files": [
|
||||
"src/test.ts",
|
||||
"src/polyfills.ts"
|
||||
],
|
||||
"include": [
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.d.ts"
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
{
|
||||
"extends": "tslint:recommended",
|
||||
"rulesDirectory": [
|
||||
"codelyzer"
|
||||
],
|
||||
"rules": {
|
||||
"align": {
|
||||
"options": [
|
||||
"parameters",
|
||||
"statements"
|
||||
]
|
||||
},
|
||||
"array-type": false,
|
||||
"arrow-return-shorthand": true,
|
||||
"curly": true,
|
||||
"deprecation": {
|
||||
"severity": "warning"
|
||||
},
|
||||
"eofline": true,
|
||||
"import-blacklist": [
|
||||
true,
|
||||
"rxjs/Rx"
|
||||
],
|
||||
"import-spacing": true,
|
||||
"indent": {
|
||||
"options": [
|
||||
"spaces"
|
||||
]
|
||||
},
|
||||
"max-classes-per-file": false,
|
||||
"max-line-length": [
|
||||
true,
|
||||
140
|
||||
],
|
||||
"member-ordering": [
|
||||
true,
|
||||
{
|
||||
"order": [
|
||||
"static-field",
|
||||
"instance-field",
|
||||
"static-method",
|
||||
"instance-method"
|
||||
]
|
||||
}
|
||||
],
|
||||
"no-console": [
|
||||
true,
|
||||
"debug",
|
||||
"info",
|
||||
"time",
|
||||
"timeEnd",
|
||||
"trace"
|
||||
],
|
||||
"no-empty": false,
|
||||
"no-inferrable-types": [
|
||||
true,
|
||||
"ignore-params"
|
||||
],
|
||||
"no-non-null-assertion": true,
|
||||
"no-redundant-jsdoc": true,
|
||||
"no-switch-case-fall-through": true,
|
||||
"no-var-requires": false,
|
||||
"object-literal-key-quotes": [
|
||||
true,
|
||||
"as-needed"
|
||||
],
|
||||
"quotemark": [
|
||||
true,
|
||||
"single"
|
||||
],
|
||||
"semicolon": {
|
||||
"options": [
|
||||
"always"
|
||||
]
|
||||
},
|
||||
"space-before-function-paren": {
|
||||
"options": {
|
||||
"anonymous": "never",
|
||||
"asyncArrow": "always",
|
||||
"constructor": "never",
|
||||
"method": "never",
|
||||
"named": "never"
|
||||
}
|
||||
},
|
||||
"typedef": [
|
||||
true,
|
||||
"call-signature"
|
||||
],
|
||||
"typedef-whitespace": {
|
||||
"options": [
|
||||
{
|
||||
"call-signature": "nospace",
|
||||
"index-signature": "nospace",
|
||||
"parameter": "nospace",
|
||||
"property-declaration": "nospace",
|
||||
"variable-declaration": "nospace"
|
||||
},
|
||||
{
|
||||
"call-signature": "onespace",
|
||||
"index-signature": "onespace",
|
||||
"parameter": "onespace",
|
||||
"property-declaration": "onespace",
|
||||
"variable-declaration": "onespace"
|
||||
}
|
||||
]
|
||||
},
|
||||
"variable-name": {
|
||||
"options": [
|
||||
"ban-keywords",
|
||||
"check-format",
|
||||
"allow-pascal-case"
|
||||
]
|
||||
},
|
||||
"whitespace": {
|
||||
"options": [
|
||||
"check-branch",
|
||||
"check-decl",
|
||||
"check-operator",
|
||||
"check-separator",
|
||||
"check-type",
|
||||
"check-typecast"
|
||||
]
|
||||
},
|
||||
"component-class-suffix": true,
|
||||
"contextual-lifecycle": true,
|
||||
"directive-class-suffix": true,
|
||||
"no-conflicting-lifecycle": true,
|
||||
"no-host-metadata-property": true,
|
||||
"no-input-rename": true,
|
||||
"no-inputs-metadata-property": true,
|
||||
"no-output-native": true,
|
||||
"no-output-on-prefix": true,
|
||||
"no-output-rename": true,
|
||||
"no-outputs-metadata-property": true,
|
||||
"template-banana-in-box": true,
|
||||
"template-no-negated-async": true,
|
||||
"use-lifecycle-interface": true,
|
||||
"use-pipe-transform-interface": true,
|
||||
"directive-selector": [
|
||||
true,
|
||||
"attribute",
|
||||
"app",
|
||||
"camelCase"
|
||||
],
|
||||
"component-selector": [
|
||||
true,
|
||||
"element",
|
||||
"app",
|
||||
"kebab-case"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,18 @@
|
||||
import { Todo, TodoID } from "./todo-management/todo.repository";
|
||||
import { ValidationErrorItem } from "joi";
|
||||
export type TodoID = string;
|
||||
|
||||
export interface Todo {
|
||||
id: TodoID;
|
||||
completed: boolean;
|
||||
title: string;
|
||||
}
|
||||
|
||||
interface Error {
|
||||
error: string;
|
||||
errorDetails?: ValidationErrorItem[];
|
||||
errorDetails?: {
|
||||
message: string;
|
||||
path: Array<string | number>;
|
||||
type: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
interface Success<T> {
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Server as HttpServer } from "http";
|
||||
import { Server, ServerOptions } from "socket.io";
|
||||
import { ClientEvents, ServerEvents } from "./events";
|
||||
import { ClientEvents, ServerEvents } from "../../common/events";
|
||||
import { TodoRepository } from "./todo-management/todo.repository";
|
||||
import createTodoHandlers from "./todo-management/todo.handlers";
|
||||
|
||||
|
||||
@@ -2,8 +2,13 @@ import { Errors, mapErrorDetails, sanitizeErrorMessage } from "../util";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import { Components } from "../app";
|
||||
import Joi = require("joi");
|
||||
import { Todo, TodoID } from "./todo.repository";
|
||||
import { ClientEvents, Response, ServerEvents } from "../events";
|
||||
import {
|
||||
Todo,
|
||||
TodoID,
|
||||
ClientEvents,
|
||||
Response,
|
||||
ServerEvents,
|
||||
} from "../../../common/events";
|
||||
import { Socket } from "socket.io";
|
||||
|
||||
const idSchema = Joi.string().guid({
|
||||
@@ -19,8 +24,7 @@ const todoSchema = Joi.object({
|
||||
completed: Joi.boolean().required(),
|
||||
});
|
||||
|
||||
export default function (components: Components) {
|
||||
const { todoRepository } = components;
|
||||
export default function ({ todoRepository }: Components) {
|
||||
return {
|
||||
createTodo: async function (
|
||||
payload: Omit<Todo, "id">,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Errors } from "../util";
|
||||
import { Todo, TodoID } from "../../../common/events";
|
||||
|
||||
abstract class CrudRepository<T, ID> {
|
||||
abstract findAll(): Promise<T[]>;
|
||||
@@ -7,14 +8,6 @@ abstract class CrudRepository<T, ID> {
|
||||
abstract deleteById(id: ID): Promise<void>;
|
||||
}
|
||||
|
||||
export type TodoID = string;
|
||||
|
||||
export interface Todo {
|
||||
id: TodoID;
|
||||
completed: boolean;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export abstract class TodoRepository extends CrudRepository<Todo, TodoID> {}
|
||||
|
||||
export class InMemoryTodoRepository extends TodoRepository {
|
||||
|
||||
23
examples/basic-crud-application/vue-client/.gitignore
vendored
Normal file
23
examples/basic-crud-application/vue-client/.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
24
examples/basic-crud-application/vue-client/README.md
Normal file
24
examples/basic-crud-application/vue-client/README.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# vue-client
|
||||
|
||||
## Project setup
|
||||
```
|
||||
yarn install
|
||||
```
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
```
|
||||
yarn serve
|
||||
```
|
||||
|
||||
### Compiles and minifies for production
|
||||
```
|
||||
yarn build
|
||||
```
|
||||
|
||||
### Lints and fixes files
|
||||
```
|
||||
yarn lint
|
||||
```
|
||||
|
||||
### Customize configuration
|
||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
||||
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
||||
19
examples/basic-crud-application/vue-client/jsconfig.json
Normal file
19
examples/basic-crud-application/vue-client/jsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"module": "esnext",
|
||||
"baseUrl": "./",
|
||||
"moduleResolution": "node",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
},
|
||||
"lib": [
|
||||
"esnext",
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"scripthost"
|
||||
]
|
||||
}
|
||||
}
|
||||
45
examples/basic-crud-application/vue-client/package.json
Normal file
45
examples/basic-crud-application/vue-client/package.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "vue-client",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve --port 4200",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"core-js": "^3.8.3",
|
||||
"pinia": "^2.1.7",
|
||||
"socket.io-client": "^4.7.2",
|
||||
"vue": "^3.2.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.12.16",
|
||||
"@babel/eslint-parser": "^7.12.16",
|
||||
"@vue/cli-plugin-babel": "~5.0.0",
|
||||
"@vue/cli-plugin-eslint": "~5.0.0",
|
||||
"@vue/cli-service": "~5.0.0",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-plugin-vue": "^8.0.3"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"env": {
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"plugin:vue/vue3-essential",
|
||||
"eslint:recommended"
|
||||
],
|
||||
"parserOptions": {
|
||||
"parser": "@babel/eslint-parser"
|
||||
},
|
||||
"rules": {}
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not dead",
|
||||
"not ie 11"
|
||||
]
|
||||
}
|
||||
BIN
examples/basic-crud-application/vue-client/public/favicon.ico
Normal file
BIN
examples/basic-crud-application/vue-client/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
18
examples/basic-crud-application/vue-client/public/index.html
Normal file
18
examples/basic-crud-application/vue-client/public/index.html
Normal file
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<link href="styles.css" rel="stylesheet" type="text/css" />
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
||||
381
examples/basic-crud-application/vue-client/public/styles.css
Normal file
381
examples/basic-crud-application/vue-client/public/styles.css
Normal file
@@ -0,0 +1,381 @@
|
||||
/* imported from node_modules/todomvc-app-css/index.css */
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: none;
|
||||
font-size: 100%;
|
||||
vertical-align: baseline;
|
||||
font-family: inherit;
|
||||
font-weight: inherit;
|
||||
color: inherit;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body {
|
||||
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
line-height: 1.4em;
|
||||
background: #f5f5f5;
|
||||
color: #111111;
|
||||
min-width: 230px;
|
||||
max-width: 550px;
|
||||
margin: 0 auto;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.todoapp {
|
||||
background: #fff;
|
||||
margin: 130px 0 40px 0;
|
||||
position: relative;
|
||||
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
|
||||
0 25px 50px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.todoapp input::-webkit-input-placeholder {
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.todoapp input::-moz-placeholder {
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.todoapp input::input-placeholder {
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.todoapp h1 {
|
||||
position: absolute;
|
||||
top: -140px;
|
||||
width: 100%;
|
||||
font-size: 80px;
|
||||
font-weight: 200;
|
||||
text-align: center;
|
||||
color: #b83f45;
|
||||
-webkit-text-rendering: optimizeLegibility;
|
||||
-moz-text-rendering: optimizeLegibility;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
.new-todo,
|
||||
.edit {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
font-size: 24px;
|
||||
font-family: inherit;
|
||||
font-weight: inherit;
|
||||
line-height: 1.4em;
|
||||
color: inherit;
|
||||
padding: 6px;
|
||||
border: 1px solid #999;
|
||||
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
|
||||
box-sizing: border-box;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.new-todo {
|
||||
padding: 16px 16px 16px 60px;
|
||||
border: none;
|
||||
background: rgba(0, 0, 0, 0.003);
|
||||
box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
|
||||
}
|
||||
|
||||
.main {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
border-top: 1px solid #e6e6e6;
|
||||
}
|
||||
|
||||
.toggle-all {
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
border: none; /* Mobile Safari */
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
right: 100%;
|
||||
bottom: 100%;
|
||||
}
|
||||
|
||||
.toggle-all + label {
|
||||
width: 60px;
|
||||
height: 34px;
|
||||
font-size: 0;
|
||||
position: absolute;
|
||||
top: -52px;
|
||||
left: -13px;
|
||||
-webkit-transform: rotate(90deg);
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.toggle-all + label:before {
|
||||
content: '❯';
|
||||
font-size: 22px;
|
||||
color: #e6e6e6;
|
||||
padding: 10px 27px 10px 27px;
|
||||
}
|
||||
|
||||
.toggle-all:checked + label:before {
|
||||
color: #737373;
|
||||
}
|
||||
|
||||
.todo-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.todo-list li {
|
||||
position: relative;
|
||||
font-size: 24px;
|
||||
border-bottom: 1px solid #ededed;
|
||||
}
|
||||
|
||||
.todo-list li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.todo-list li.editing {
|
||||
border-bottom: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.todo-list li.editing .edit {
|
||||
display: block;
|
||||
width: calc(100% - 43px);
|
||||
padding: 12px 16px;
|
||||
margin: 0 0 0 43px;
|
||||
}
|
||||
|
||||
.todo-list li.editing .view {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.todo-list li .toggle {
|
||||
text-align: center;
|
||||
width: 40px;
|
||||
/* auto, since non-WebKit browsers doesn't support input styling */
|
||||
height: auto;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
margin: auto 0;
|
||||
border: none; /* Mobile Safari */
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
.todo-list li .toggle {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.todo-list li .toggle + label {
|
||||
/*
|
||||
Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433
|
||||
IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/
|
||||
*/
|
||||
background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E');
|
||||
background-repeat: no-repeat;
|
||||
background-position: center left;
|
||||
}
|
||||
|
||||
.todo-list li .toggle:checked + label {
|
||||
background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E');
|
||||
}
|
||||
|
||||
.todo-list li label {
|
||||
word-break: break-all;
|
||||
padding: 15px 15px 15px 60px;
|
||||
display: block;
|
||||
line-height: 1.2;
|
||||
transition: color 0.4s;
|
||||
font-weight: 400;
|
||||
color: #4d4d4d;
|
||||
}
|
||||
|
||||
.todo-list li.completed label {
|
||||
color: #cdcdcd;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.todo-list li .destroy {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 10px;
|
||||
bottom: 0;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin: auto 0;
|
||||
font-size: 30px;
|
||||
color: #cc9a9a;
|
||||
margin-bottom: 11px;
|
||||
transition: color 0.2s ease-out;
|
||||
}
|
||||
|
||||
.todo-list li .destroy:hover {
|
||||
color: #af5b5e;
|
||||
}
|
||||
|
||||
.todo-list li .destroy:after {
|
||||
content: '×';
|
||||
}
|
||||
|
||||
.todo-list li:hover .destroy {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.todo-list li .edit {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.todo-list li.editing:last-child {
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding: 10px 15px;
|
||||
height: 20px;
|
||||
text-align: center;
|
||||
font-size: 15px;
|
||||
border-top: 1px solid #e6e6e6;
|
||||
}
|
||||
|
||||
.footer:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 50px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
|
||||
0 8px 0 -3px #f6f6f6,
|
||||
0 9px 1px -3px rgba(0, 0, 0, 0.2),
|
||||
0 16px 0 -6px #f6f6f6,
|
||||
0 17px 2px -6px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.todo-count {
|
||||
float: left;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.todo-count strong {
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.filters {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.filters li {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.filters li a {
|
||||
color: inherit;
|
||||
margin: 3px;
|
||||
padding: 3px 7px;
|
||||
text-decoration: none;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.filters li a:hover {
|
||||
border-color: rgba(175, 47, 47, 0.1);
|
||||
}
|
||||
|
||||
.filters li a.selected {
|
||||
border-color: rgba(175, 47, 47, 0.2);
|
||||
}
|
||||
|
||||
.clear-completed,
|
||||
html .clear-completed:active {
|
||||
float: right;
|
||||
position: relative;
|
||||
line-height: 20px;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.clear-completed:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.info {
|
||||
margin: 65px auto 0;
|
||||
color: #4d4d4d;
|
||||
font-size: 11px;
|
||||
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.info p {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.info a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.info a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/*
|
||||
Hack to remove background from Mobile Safari.
|
||||
Can't use it globally since it destroys checkboxes in Firefox
|
||||
*/
|
||||
@media screen and (-webkit-min-device-pixel-ratio:0) {
|
||||
.toggle-all,
|
||||
.todo-list li .toggle {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.todo-list li .toggle {
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 430px) {
|
||||
.footer {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.filters {
|
||||
bottom: 10px;
|
||||
}
|
||||
}
|
||||
123
examples/basic-crud-application/vue-client/src/App.vue
Normal file
123
examples/basic-crud-application/vue-client/src/App.vue
Normal file
@@ -0,0 +1,123 @@
|
||||
<script setup>
|
||||
import { computed, ref } from "vue";
|
||||
import { useTodoStore } from "@/stores/todo";
|
||||
import { socket } from "@/socket";
|
||||
|
||||
const newTodo = ref("");
|
||||
const editedTodo = ref(undefined);
|
||||
const newTitle = ref("");
|
||||
|
||||
const store = useTodoStore();
|
||||
|
||||
// remove any existing listeners (in case of hot reload)
|
||||
socket.off();
|
||||
|
||||
store.bindEvents();
|
||||
|
||||
function addTodo() {
|
||||
const value = newTodo.value && newTodo.value.trim();
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
store.add(value);
|
||||
newTodo.value = "";
|
||||
}
|
||||
|
||||
function editTodo(todo) {
|
||||
editedTodo.value = todo;
|
||||
newTitle.value = todo.title;
|
||||
}
|
||||
|
||||
function doneEdit(todo) {
|
||||
if (newTitle.value) {
|
||||
store.setTitle(todo, newTitle.value);
|
||||
} else {
|
||||
store.delete(todo);
|
||||
}
|
||||
editedTodo.value = undefined;
|
||||
}
|
||||
|
||||
function cancelEdit() {
|
||||
editedTodo.value = undefined;
|
||||
}
|
||||
|
||||
const allDone = computed({
|
||||
get: () => {
|
||||
return store.remaining === 0;
|
||||
},
|
||||
set: (value) => {
|
||||
store.toggleAll(value);
|
||||
},
|
||||
});
|
||||
|
||||
function pluralize(word, count) {
|
||||
return word + (count === 1 ? "" : "s");
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="todoapp" v-cloak>
|
||||
<header class="header">
|
||||
<h1>todos</h1>
|
||||
<input
|
||||
class="new-todo"
|
||||
autofocus
|
||||
autocomplete="off"
|
||||
placeholder="What needs to be done?"
|
||||
v-model="newTodo"
|
||||
@keydown.enter="addTodo"
|
||||
/>
|
||||
</header>
|
||||
<section class="main" v-show="store.todos.length">
|
||||
<input
|
||||
id="toggle-all"
|
||||
class="toggle-all"
|
||||
type="checkbox"
|
||||
v-model="allDone"
|
||||
/>
|
||||
<label for="toggle-all">Mark all as complete</label>
|
||||
<ul class="todo-list">
|
||||
<li
|
||||
class="todo"
|
||||
v-for="todo in store.todos"
|
||||
:key="todo.id"
|
||||
:class="{ completed: todo.completed, editing: todo === editedTodo }"
|
||||
>
|
||||
<div class="view">
|
||||
<input
|
||||
class="toggle"
|
||||
type="checkbox"
|
||||
v-model="todo.completed"
|
||||
@click="store.toggleOne(todo)"
|
||||
/>
|
||||
<label @dblclick="editTodo(todo)">{{ todo.title }}</label>
|
||||
<button class="destroy" @click="store.delete(todo)"></button>
|
||||
</div>
|
||||
<input
|
||||
class="edit"
|
||||
type="text"
|
||||
v-model="newTitle"
|
||||
@blur="doneEdit"
|
||||
@keydown.enter="doneEdit(todo)"
|
||||
@keydown.esc="cancelEdit(todo)"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<footer class="footer" v-show="store.todos.length">
|
||||
<span class="todo-count">
|
||||
<strong v-text="store.remaining"></strong>
|
||||
{{ pluralize("item", store.remaining) }} left
|
||||
</span>
|
||||
<button
|
||||
class="clear-completed"
|
||||
@click="store.deleteCompleted"
|
||||
v-show="store.todos.length > store.remaining"
|
||||
>
|
||||
Clear complete
|
||||
</button>
|
||||
</footer>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
BIN
examples/basic-crud-application/vue-client/src/assets/logo.png
Normal file
BIN
examples/basic-crud-application/vue-client/src/assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.7 KiB |
9
examples/basic-crud-application/vue-client/src/main.js
Normal file
9
examples/basic-crud-application/vue-client/src/main.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import { createApp } from "vue";
|
||||
import { createPinia } from "pinia";
|
||||
import App from "./App.vue";
|
||||
|
||||
const pinia = createPinia();
|
||||
const app = createApp(App);
|
||||
|
||||
app.use(pinia);
|
||||
app.mount("#app");
|
||||
7
examples/basic-crud-application/vue-client/src/socket.js
Normal file
7
examples/basic-crud-application/vue-client/src/socket.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import { io } from "socket.io-client";
|
||||
|
||||
// "undefined" means the URL will be computed from the `window.location` object
|
||||
const URL =
|
||||
process.env.NODE_ENV === "production" ? undefined : "http://localhost:3000";
|
||||
|
||||
export const socket = io(URL);
|
||||
106
examples/basic-crud-application/vue-client/src/stores/todo.js
Normal file
106
examples/basic-crud-application/vue-client/src/stores/todo.js
Normal file
@@ -0,0 +1,106 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { socket } from "@/socket";
|
||||
|
||||
export const useTodoStore = defineStore("todo", {
|
||||
state: () => ({
|
||||
todos: [],
|
||||
}),
|
||||
|
||||
getters: {
|
||||
remaining(state) {
|
||||
let count = 0;
|
||||
state.todos.forEach((todo) => {
|
||||
if (!todo.completed) {
|
||||
count++;
|
||||
}
|
||||
});
|
||||
return count;
|
||||
},
|
||||
},
|
||||
|
||||
actions: {
|
||||
bindEvents() {
|
||||
socket.on("connect", () => {
|
||||
socket.emit("todo:list", (res) => {
|
||||
this.todos = res.data;
|
||||
});
|
||||
});
|
||||
|
||||
socket.on("todo:created", (todo) => {
|
||||
this.todos.push(todo);
|
||||
});
|
||||
|
||||
socket.on("todo:updated", (todo) => {
|
||||
const existingTodo = this.todos.find((t) => {
|
||||
return t.id === todo.id;
|
||||
});
|
||||
if (existingTodo) {
|
||||
existingTodo.title = todo.title;
|
||||
existingTodo.completed = todo.completed;
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("todo:deleted", (id) => {
|
||||
const i = this.todos.findIndex((t) => {
|
||||
return t.id === id;
|
||||
});
|
||||
if (i !== -1) {
|
||||
this.todos.splice(i, 1);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
add(title) {
|
||||
const todo = {
|
||||
id: Date.now(),
|
||||
title,
|
||||
completed: false,
|
||||
};
|
||||
this.todos.push(todo);
|
||||
socket.emit("todo:create", { title, completed: false }, (res) => {
|
||||
todo.id = res.data;
|
||||
});
|
||||
},
|
||||
|
||||
setTitle(todo, title) {
|
||||
todo.title = title;
|
||||
socket.emit("todo:update", todo, () => {});
|
||||
},
|
||||
|
||||
delete(todo) {
|
||||
const i = this.todos.findIndex((t) => {
|
||||
return t.id === todo.id;
|
||||
});
|
||||
|
||||
if (i !== -1) {
|
||||
this.todos.splice(i, 1);
|
||||
socket.emit("todo:delete", todo.id, () => {});
|
||||
}
|
||||
},
|
||||
|
||||
deleteCompleted() {
|
||||
this.todos.forEach((todo) => {
|
||||
if (todo.completed) {
|
||||
socket.emit("todo:delete", todo.id, () => {});
|
||||
}
|
||||
});
|
||||
|
||||
this.todos = this.todos.filter((t) => {
|
||||
return !t.completed;
|
||||
});
|
||||
},
|
||||
|
||||
toggleOne(todo) {
|
||||
todo.completed = !todo.completed;
|
||||
socket.emit("todo:update", todo, () => {});
|
||||
},
|
||||
|
||||
toggleAll(onlyActive) {
|
||||
this.todos.forEach((todo) => {
|
||||
if (!onlyActive || !todo.completed) {
|
||||
this.toggleOne(todo);
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
4
examples/basic-crud-application/vue-client/vue.config.js
Normal file
4
examples/basic-crud-application/vue-client/vue.config.js
Normal file
@@ -0,0 +1,4 @@
|
||||
const { defineConfig } = require('@vue/cli-service')
|
||||
module.exports = defineConfig({
|
||||
transpileDependencies: true
|
||||
})
|
||||
6309
examples/basic-crud-application/vue-client/yarn.lock
Normal file
6309
examples/basic-crud-application/vue-client/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
25
examples/connection-state-recovery-example/README.md
Normal file
25
examples/connection-state-recovery-example/README.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Example with connection state recovery
|
||||
|
||||
This example shows how to use the [Connection state recovery feature](https://socket.io/docs/v4/connection-state-recovery).
|
||||
|
||||

|
||||
|
||||
## How to use
|
||||
|
||||
```shell
|
||||
# choose your module syntax (either ES modules or CommonJS)
|
||||
$ cd esm/
|
||||
|
||||
# install the dependencies
|
||||
$ npm i
|
||||
|
||||
# start the server
|
||||
$ node index.js
|
||||
```
|
||||
|
||||
And point your browser to `http://localhost:3000`.
|
||||
|
||||
You can also run this example directly in your browser on:
|
||||
|
||||
- [CodeSandbox](https://codesandbox.io/p/sandbox/github/socketio/socket.io/tree/main/examples/connection-state-recovery-example/esm?file=index.js)
|
||||
- [StackBlitz](https://stackblitz.com/github/socketio/socket.io/tree/main/examples/connection-state-recovery-example/esm?file=index.js)
|
||||
BIN
examples/connection-state-recovery-example/assets/csr.gif
Normal file
BIN
examples/connection-state-recovery-example/assets/csr.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 103 KiB |
@@ -0,0 +1 @@
|
||||
FROM node:20-bullseye
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
// These tasks will run in order when initializing your CodeSandbox project.
|
||||
"setupTasks": [
|
||||
{
|
||||
"name": "Install Dependencies",
|
||||
"command": "npm install"
|
||||
}
|
||||
],
|
||||
|
||||
// These tasks can be run from CodeSandbox. Running one will open a log in the app.
|
||||
"tasks": {
|
||||
"npm start": {
|
||||
"name": "npm start",
|
||||
"command": "npm start",
|
||||
"runAtStart": true
|
||||
}
|
||||
}
|
||||
}
|
||||
49
examples/connection-state-recovery-example/cjs/index.html
Normal file
49
examples/connection-state-recovery-example/cjs/index.html
Normal file
@@ -0,0 +1,49 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Connection state recovery | Socket.IO</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Status: <span id="connectionStatus">disconnected</span></p>
|
||||
<p>Recovered? <span id="recoveryStatus">-</span></p>
|
||||
|
||||
<p>Latest messages:</p>
|
||||
<ul id="messages"></ul>
|
||||
|
||||
<script src="/socket.io/socket.io.js"></script>
|
||||
<script>
|
||||
const $connectionStatus = document.getElementById("connectionStatus");
|
||||
const $recoveryStatus = document.getElementById("recoveryStatus");
|
||||
const $messages = document.getElementById("messages");
|
||||
|
||||
const socket = io({
|
||||
reconnectionDelay: 5000 // 1000 by default
|
||||
});
|
||||
|
||||
socket.on("connect", () => {
|
||||
$connectionStatus.innerText = "connected";
|
||||
$recoveryStatus.innerText = "" + socket.recovered;
|
||||
|
||||
setTimeout(() => {
|
||||
// close the low-level connection and trigger a reconnection
|
||||
socket.io.engine.close();
|
||||
}, Math.random() * 5000 + 1000);
|
||||
});
|
||||
|
||||
socket.on("disconnect", () => {
|
||||
$connectionStatus.innerText = "disconnected";
|
||||
$recoveryStatus.innerText = "-"
|
||||
});
|
||||
|
||||
socket.on("ping", (value) => {
|
||||
const item = document.createElement("li");
|
||||
item.textContent = value;
|
||||
$messages.prepend(item);
|
||||
if ($messages.childElementCount > 10) {
|
||||
$messages.removeChild($messages.lastChild);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
53
examples/connection-state-recovery-example/cjs/index.js
Normal file
53
examples/connection-state-recovery-example/cjs/index.js
Normal file
@@ -0,0 +1,53 @@
|
||||
const { readFile } = require("node:fs/promises");
|
||||
const { createServer } = require("node:http");
|
||||
const { Server } = require("socket.io");
|
||||
|
||||
const httpServer = createServer(async (req, res) => {
|
||||
if (req.url !== "/") {
|
||||
res.writeHead(404);
|
||||
res.end("Not found");
|
||||
return;
|
||||
}
|
||||
// reload the file every time
|
||||
const content = await readFile("index.html");
|
||||
const length = Buffer.byteLength(content);
|
||||
|
||||
res.writeHead(200, {
|
||||
"Content-Type": "text/html",
|
||||
"Content-Length": length,
|
||||
});
|
||||
res.end(content);
|
||||
});
|
||||
|
||||
const io = new Server(httpServer, {
|
||||
connectionStateRecovery: {
|
||||
// the backup duration of the sessions and the packets
|
||||
maxDisconnectionDuration: 2 * 60 * 1000,
|
||||
// whether to skip middlewares upon successful recovery
|
||||
skipMiddlewares: true,
|
||||
},
|
||||
});
|
||||
|
||||
io.on("connection", (socket) => {
|
||||
console.log(`connect ${socket.id}`);
|
||||
|
||||
if (socket.recovered) {
|
||||
console.log("recovered!");
|
||||
console.log("socket.rooms:", socket.rooms);
|
||||
console.log("socket.data:", socket.data);
|
||||
} else {
|
||||
console.log("new connection");
|
||||
socket.join("sample room");
|
||||
socket.data.foo = "bar";
|
||||
}
|
||||
|
||||
socket.on("disconnect", (reason) => {
|
||||
console.log(`disconnect ${socket.id} due to ${reason}`);
|
||||
});
|
||||
});
|
||||
|
||||
setInterval(() => {
|
||||
io.emit("ping", new Date().toISOString());
|
||||
}, 1000);
|
||||
|
||||
httpServer.listen(3000);
|
||||
13
examples/connection-state-recovery-example/cjs/package.json
Normal file
13
examples/connection-state-recovery-example/cjs/package.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "connection-state-recovery-example",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "commonjs",
|
||||
"description": "Example with connection state recovery",
|
||||
"scripts": {
|
||||
"start": "node index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"socket.io": "^4.7.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
FROM node:20-bullseye
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
// These tasks will run in order when initializing your CodeSandbox project.
|
||||
"setupTasks": [
|
||||
{
|
||||
"name": "Install Dependencies",
|
||||
"command": "npm install"
|
||||
}
|
||||
],
|
||||
|
||||
// These tasks can be run from CodeSandbox. Running one will open a log in the app.
|
||||
"tasks": {
|
||||
"npm start": {
|
||||
"name": "npm start",
|
||||
"command": "npm start",
|
||||
"runAtStart": true
|
||||
}
|
||||
}
|
||||
}
|
||||
49
examples/connection-state-recovery-example/esm/index.html
Normal file
49
examples/connection-state-recovery-example/esm/index.html
Normal file
@@ -0,0 +1,49 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Connection state recovery | Socket.IO</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Status: <span id="connectionStatus">disconnected</span></p>
|
||||
<p>Recovered? <span id="recoveryStatus">-</span></p>
|
||||
|
||||
<p>Latest messages:</p>
|
||||
<ul id="messages"></ul>
|
||||
|
||||
<script src="/socket.io/socket.io.js"></script>
|
||||
<script>
|
||||
const $connectionStatus = document.getElementById("connectionStatus");
|
||||
const $recoveryStatus = document.getElementById("recoveryStatus");
|
||||
const $messages = document.getElementById("messages");
|
||||
|
||||
const socket = io({
|
||||
reconnectionDelay: 5000 // 1000 by default
|
||||
});
|
||||
|
||||
socket.on("connect", () => {
|
||||
$connectionStatus.innerText = "connected";
|
||||
$recoveryStatus.innerText = "" + socket.recovered;
|
||||
|
||||
setTimeout(() => {
|
||||
// close the low-level connection and trigger a reconnection
|
||||
socket.io.engine.close();
|
||||
}, Math.random() * 5000 + 1000);
|
||||
});
|
||||
|
||||
socket.on("disconnect", () => {
|
||||
$connectionStatus.innerText = "disconnected";
|
||||
$recoveryStatus.innerText = "-"
|
||||
});
|
||||
|
||||
socket.on("ping", (value) => {
|
||||
const item = document.createElement("li");
|
||||
item.textContent = value;
|
||||
$messages.prepend(item);
|
||||
if ($messages.childElementCount > 10) {
|
||||
$messages.removeChild($messages.lastChild);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
53
examples/connection-state-recovery-example/esm/index.js
Normal file
53
examples/connection-state-recovery-example/esm/index.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { createServer } from "node:http";
|
||||
import { Server } from "socket.io";
|
||||
|
||||
const httpServer = createServer(async (req, res) => {
|
||||
if (req.url !== "/") {
|
||||
res.writeHead(404);
|
||||
res.end("Not found");
|
||||
return;
|
||||
}
|
||||
// reload the file every time
|
||||
const content = await readFile("index.html");
|
||||
const length = Buffer.byteLength(content);
|
||||
|
||||
res.writeHead(200, {
|
||||
"Content-Type": "text/html",
|
||||
"Content-Length": length,
|
||||
});
|
||||
res.end(content);
|
||||
});
|
||||
|
||||
const io = new Server(httpServer, {
|
||||
connectionStateRecovery: {
|
||||
// the backup duration of the sessions and the packets
|
||||
maxDisconnectionDuration: 2 * 60 * 1000,
|
||||
// whether to skip middlewares upon successful recovery
|
||||
skipMiddlewares: true,
|
||||
},
|
||||
});
|
||||
|
||||
io.on("connection", (socket) => {
|
||||
console.log(`connect ${socket.id}`);
|
||||
|
||||
if (socket.recovered) {
|
||||
console.log("recovered!");
|
||||
console.log("socket.rooms:", socket.rooms);
|
||||
console.log("socket.data:", socket.data);
|
||||
} else {
|
||||
console.log("new connection");
|
||||
socket.join("sample room");
|
||||
socket.data.foo = "bar";
|
||||
}
|
||||
|
||||
socket.on("disconnect", (reason) => {
|
||||
console.log(`disconnect ${socket.id} due to ${reason}`);
|
||||
});
|
||||
});
|
||||
|
||||
setInterval(() => {
|
||||
io.emit("ping", new Date().toISOString());
|
||||
}, 1000);
|
||||
|
||||
httpServer.listen(3000);
|
||||
13
examples/connection-state-recovery-example/esm/package.json
Normal file
13
examples/connection-state-recovery-example/esm/package.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "connection-state-recovery-example",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Example with connection state recovery",
|
||||
"scripts": {
|
||||
"start": "node index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"socket.io": "^4.7.2"
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { Server } from "socket.io";
|
||||
|
||||
const io = new Server(8080);
|
||||
|
||||
io.on("connect", (socket) => {
|
||||
io.on("connection", (socket) => {
|
||||
console.log(`connect ${socket.id}`);
|
||||
|
||||
socket.on("ping", (cb) => {
|
||||
|
||||
@@ -52,6 +52,10 @@
|
||||
socket.on("disconnect", () => {
|
||||
ioStatus.innerText = "disconnected";
|
||||
});
|
||||
|
||||
socket.on("current count", (count) => {
|
||||
ioCount.innerText = count;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
66
examples/express-session-example/cjs/index.js
Normal file
66
examples/express-session-example/cjs/index.js
Normal file
@@ -0,0 +1,66 @@
|
||||
const express = require("express");
|
||||
const { createServer } = require("node:http");
|
||||
const { join } = require("node:path");
|
||||
const { Server } = require("socket.io");
|
||||
const session = require("express-session");
|
||||
|
||||
const port = process.env.PORT || 3000;
|
||||
|
||||
const app = express();
|
||||
const httpServer = createServer(app);
|
||||
|
||||
const sessionMiddleware = session({
|
||||
secret: "changeit",
|
||||
resave: true,
|
||||
saveUninitialized: true,
|
||||
});
|
||||
|
||||
app.use(sessionMiddleware);
|
||||
|
||||
app.get("/", (req, res) => {
|
||||
res.sendFile(join(__dirname, "index.html"));
|
||||
});
|
||||
|
||||
app.post("/incr", (req, res) => {
|
||||
const session = req.session;
|
||||
session.count = (session.count || 0) + 1;
|
||||
res.status(200).end("" + session.count);
|
||||
|
||||
io.to(session.id).emit("current count", session.count);
|
||||
});
|
||||
|
||||
app.post("/logout", (req, res) => {
|
||||
const sessionId = req.session.id;
|
||||
req.session.destroy(() => {
|
||||
// disconnect all Socket.IO connections linked to this session ID
|
||||
io.to(sessionId).disconnectSockets();
|
||||
res.status(204).end();
|
||||
});
|
||||
});
|
||||
|
||||
const io = new Server(httpServer);
|
||||
|
||||
io.engine.use(sessionMiddleware);
|
||||
|
||||
io.on("connection", (socket) => {
|
||||
const req = socket.request;
|
||||
|
||||
socket.join(req.session.id);
|
||||
|
||||
socket.on("incr", (cb) => {
|
||||
req.session.reload((err) => {
|
||||
if (err) {
|
||||
// session has expired
|
||||
return socket.disconnect();
|
||||
}
|
||||
req.session.count = (req.session.count || 0) + 1;
|
||||
req.session.save(() => {
|
||||
cb(req.session.count);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
httpServer.listen(port, () => {
|
||||
console.log(`application is running at: http://localhost:${port}`);
|
||||
});
|
||||
15
examples/express-session-example/cjs/package.json
Normal file
15
examples/express-session-example/cjs/package.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "express-session-example",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "commonjs",
|
||||
"description": "Example with express-session (https://github.com/expressjs/session)",
|
||||
"scripts": {
|
||||
"start": "node index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "~4.17.3",
|
||||
"express-session": "~1.17.2",
|
||||
"socket.io": "^4.7.2"
|
||||
}
|
||||
}
|
||||
61
examples/express-session-example/esm/index.html
Normal file
61
examples/express-session-example/esm/index.html
Normal file
@@ -0,0 +1,61 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Example with express-session</title>
|
||||
</head>
|
||||
<body>
|
||||
<button onclick="incrementWithFetch()">Increment with fetch()</button>
|
||||
<button onclick="logout()">Logout</button>
|
||||
<p>Count: <span id="httpCount">0</span></p>
|
||||
|
||||
<button onclick="incrementWithEmit()">
|
||||
Increment with Socket.IO emit()
|
||||
</button>
|
||||
<p>Status: <span id="ioStatus">disconnected</span></p>
|
||||
<p>Count: <span id="ioCount">0</span></p>
|
||||
|
||||
<script src="/socket.io/socket.io.js"></script>
|
||||
<script>
|
||||
const httpCount = document.getElementById("httpCount");
|
||||
const ioStatus = document.getElementById("ioStatus");
|
||||
const ioCount = document.getElementById("ioCount");
|
||||
|
||||
const socket = io({
|
||||
// with WebSocket only
|
||||
// transports: ["websocket"],
|
||||
});
|
||||
|
||||
async function incrementWithFetch() {
|
||||
const response = await fetch("/incr", {
|
||||
method: "post",
|
||||
});
|
||||
httpCount.innerText = await response.text();
|
||||
}
|
||||
|
||||
function logout() {
|
||||
fetch("/logout", {
|
||||
method: "post",
|
||||
});
|
||||
}
|
||||
|
||||
async function incrementWithEmit() {
|
||||
socket.emit("incr", (count) => {
|
||||
ioCount.innerText = count;
|
||||
});
|
||||
}
|
||||
|
||||
socket.on("connect", () => {
|
||||
ioStatus.innerText = "connected";
|
||||
});
|
||||
|
||||
socket.on("disconnect", () => {
|
||||
ioStatus.innerText = "disconnected";
|
||||
});
|
||||
|
||||
socket.on("current count", (count) => {
|
||||
ioCount.innerText = count;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,5 +1,5 @@
|
||||
import express from "express";
|
||||
import { createServer } from "http";
|
||||
import { createServer } from "node:http";
|
||||
import { Server } from "socket.io";
|
||||
import session from "express-session";
|
||||
|
||||
@@ -17,13 +17,15 @@ const sessionMiddleware = session({
|
||||
app.use(sessionMiddleware);
|
||||
|
||||
app.get("/", (req, res) => {
|
||||
res.sendFile("./index.html", { root: process.cwd() });
|
||||
res.sendFile(new URL("./index.html", import.meta.url).pathname);
|
||||
});
|
||||
|
||||
app.post("/incr", (req, res) => {
|
||||
const session = req.session;
|
||||
session.count = (session.count || 0) + 1;
|
||||
res.status(200).end("" + session.count);
|
||||
|
||||
io.to(session.id).emit("current count", session.count);
|
||||
});
|
||||
|
||||
app.post("/logout", (req, res) => {
|
||||
@@ -35,39 +37,11 @@ app.post("/logout", (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
const io = new Server(httpServer, {
|
||||
allowRequest: (req, callback) => {
|
||||
// with HTTP long-polling, we have access to the HTTP response here, but this is not
|
||||
// the case with WebSocket, so we provide a dummy response object
|
||||
const fakeRes = {
|
||||
getHeader() {
|
||||
return [];
|
||||
},
|
||||
setHeader(key, values) {
|
||||
req.cookieHolder = values[0];
|
||||
},
|
||||
writeHead() {},
|
||||
};
|
||||
sessionMiddleware(req, fakeRes, () => {
|
||||
if (req.session) {
|
||||
// trigger the setHeader() above
|
||||
fakeRes.writeHead();
|
||||
// manually save the session (normally triggered by res.end())
|
||||
req.session.save();
|
||||
}
|
||||
callback(null, true);
|
||||
});
|
||||
},
|
||||
});
|
||||
const io = new Server(httpServer);
|
||||
|
||||
io.engine.on("initial_headers", (headers, req) => {
|
||||
if (req.cookieHolder) {
|
||||
headers["set-cookie"] = req.cookieHolder;
|
||||
delete req.cookieHolder;
|
||||
}
|
||||
});
|
||||
io.engine.use(sessionMiddleware);
|
||||
|
||||
io.on("connect", (socket) => {
|
||||
io.on("connection", (socket) => {
|
||||
const req = socket.request;
|
||||
|
||||
socket.join(req.session.id);
|
||||
@@ -10,6 +10,6 @@
|
||||
"dependencies": {
|
||||
"express": "~4.17.3",
|
||||
"express-session": "~1.17.2",
|
||||
"socket.io": "~4.4.1"
|
||||
"socket.io": "^4.7.2"
|
||||
}
|
||||
}
|
||||
61
examples/express-session-example/ts/index.html
Normal file
61
examples/express-session-example/ts/index.html
Normal file
@@ -0,0 +1,61 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Example with express-session</title>
|
||||
</head>
|
||||
<body>
|
||||
<button onclick="incrementWithFetch()">Increment with fetch()</button>
|
||||
<button onclick="logout()">Logout</button>
|
||||
<p>Count: <span id="httpCount">0</span></p>
|
||||
|
||||
<button onclick="incrementWithEmit()">
|
||||
Increment with Socket.IO emit()
|
||||
</button>
|
||||
<p>Status: <span id="ioStatus">disconnected</span></p>
|
||||
<p>Count: <span id="ioCount">0</span></p>
|
||||
|
||||
<script src="/socket.io/socket.io.js"></script>
|
||||
<script>
|
||||
const httpCount = document.getElementById("httpCount");
|
||||
const ioStatus = document.getElementById("ioStatus");
|
||||
const ioCount = document.getElementById("ioCount");
|
||||
|
||||
const socket = io({
|
||||
// with WebSocket only
|
||||
// transports: ["websocket"],
|
||||
});
|
||||
|
||||
async function incrementWithFetch() {
|
||||
const response = await fetch("/incr", {
|
||||
method: "post",
|
||||
});
|
||||
httpCount.innerText = await response.text();
|
||||
}
|
||||
|
||||
function logout() {
|
||||
fetch("/logout", {
|
||||
method: "post",
|
||||
});
|
||||
}
|
||||
|
||||
async function incrementWithEmit() {
|
||||
socket.emit("incr", (count) => {
|
||||
ioCount.innerText = count;
|
||||
});
|
||||
}
|
||||
|
||||
socket.on("connect", () => {
|
||||
ioStatus.innerText = "connected";
|
||||
});
|
||||
|
||||
socket.on("disconnect", () => {
|
||||
ioStatus.innerText = "disconnected";
|
||||
});
|
||||
|
||||
socket.on("current count", (count) => {
|
||||
ioCount.innerText = count;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
72
examples/express-session-example/ts/index.ts
Normal file
72
examples/express-session-example/ts/index.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import express = require("express");
|
||||
import { createServer } from "http";
|
||||
import { Server } from "socket.io";
|
||||
import session from "express-session";
|
||||
import { type Request } from "express";
|
||||
|
||||
declare module "express-session" {
|
||||
interface SessionData {
|
||||
count: number;
|
||||
}
|
||||
}
|
||||
|
||||
const port = process.env.PORT || 3000;
|
||||
|
||||
const app = express();
|
||||
const httpServer = createServer(app);
|
||||
|
||||
const sessionMiddleware = session({
|
||||
secret: "changeit",
|
||||
resave: true,
|
||||
saveUninitialized: true,
|
||||
});
|
||||
|
||||
app.use(sessionMiddleware);
|
||||
|
||||
app.get("/", (req, res) => {
|
||||
res.sendFile(new URL("./index.html", import.meta.url).pathname);
|
||||
});
|
||||
|
||||
app.post("/incr", (req, res) => {
|
||||
const session = req.session;
|
||||
session.count = (session.count || 0) + 1;
|
||||
res.status(200).end("" + session.count);
|
||||
|
||||
io.to(session.id).emit("current count", session.count);
|
||||
});
|
||||
|
||||
app.post("/logout", (req, res) => {
|
||||
const sessionId = req.session.id;
|
||||
req.session.destroy(() => {
|
||||
// disconnect all Socket.IO connections linked to this session ID
|
||||
io.to(sessionId).disconnectSockets();
|
||||
res.status(204).end();
|
||||
});
|
||||
});
|
||||
|
||||
const io = new Server(httpServer);
|
||||
|
||||
io.engine.use(sessionMiddleware);
|
||||
|
||||
io.on("connection", (socket) => {
|
||||
const req = socket.request as Request;
|
||||
|
||||
socket.join(req.session.id);
|
||||
|
||||
socket.on("incr", (cb) => {
|
||||
req.session.reload((err) => {
|
||||
if (err) {
|
||||
// session has expired
|
||||
return socket.disconnect();
|
||||
}
|
||||
req.session.count = (req.session.count || 0) + 1;
|
||||
req.session.save(() => {
|
||||
cb(req.session.count);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
httpServer.listen(port, () => {
|
||||
console.log(`application is running at: http://localhost:${port}`);
|
||||
});
|
||||
20
examples/express-session-example/ts/package.json
Normal file
20
examples/express-session-example/ts/package.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "express-session-example",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Example with express-session (https://github.com/expressjs/session)",
|
||||
"scripts": {
|
||||
"start": "ts-node index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/express-session": "^1.17.7",
|
||||
"@types/node": "^20.6.0",
|
||||
"express": "~4.17.3",
|
||||
"express-session": "~1.17.2",
|
||||
"socket.io": "^4.7.2",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.2.2"
|
||||
}
|
||||
}
|
||||
11
examples/express-session-example/ts/tsconfig.json
Normal file
11
examples/express-session-example/ts/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"target": "ES2022",
|
||||
"strict": true
|
||||
},
|
||||
"ts-node": {
|
||||
"esm": true
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,8 @@ This example shows how to retrieve the authentication context from a basic [Expr
|
||||
|
||||

|
||||
|
||||
Please read the related guide: https://socket.io/how-to/use-with-passport
|
||||
|
||||
## How to use
|
||||
|
||||
```
|
||||
@@ -12,3 +14,33 @@ $ npm ci && npm start
|
||||
```
|
||||
|
||||
And point your browser to `http://localhost:3000`. Optionally, specify a port by supplying the `PORT` env variable.
|
||||
|
||||
## How it works
|
||||
|
||||
The Socket.IO server retrieves the user context from the session:
|
||||
|
||||
```js
|
||||
function onlyForHandshake(middleware) {
|
||||
return (req, res, next) => {
|
||||
const isHandshake = req._query.sid === undefined;
|
||||
if (isHandshake) {
|
||||
middleware(req, res, next);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
io.engine.use(onlyForHandshake(sessionMiddleware));
|
||||
io.engine.use(onlyForHandshake(passport.session()));
|
||||
io.engine.use(
|
||||
onlyForHandshake((req, res, next) => {
|
||||
if (req.user) {
|
||||
next();
|
||||
} else {
|
||||
res.writeHead(401);
|
||||
res.end();
|
||||
}
|
||||
}),
|
||||
);
|
||||
```
|
||||
|
||||
55
examples/passport-example/cjs/index.html
Normal file
55
examples/passport-example/cjs/index.html
Normal file
@@ -0,0 +1,55 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Passport example</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Authenticated!</p>
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td><span id="status">Disconnected</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Socket ID</td>
|
||||
<td><span id="socket-id"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Username</td>
|
||||
<td><span id="username"></span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<form action="/logout" method="post">
|
||||
<div>
|
||||
<input type="submit" value="Log out" />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script src="/socket.io/socket.io.js"></script>
|
||||
<script>
|
||||
const socket = io();
|
||||
const socketIdSpan = document.getElementById('socket-id');
|
||||
const usernameSpan = document.getElementById('username');
|
||||
const statusSpan = document.getElementById('status');
|
||||
|
||||
socket.on('connect', () => {
|
||||
statusSpan.innerText = 'connected';
|
||||
socketIdSpan.innerText = socket.id;
|
||||
|
||||
socket.emit('whoami', (username) => {
|
||||
usernameSpan.innerText = username;
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
statusSpan.innerText = 'disconnected';
|
||||
socketIdSpan.innerText = '-';
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
117
examples/passport-example/cjs/index.js
Normal file
117
examples/passport-example/cjs/index.js
Normal file
@@ -0,0 +1,117 @@
|
||||
const express = require("express");
|
||||
const { createServer } = require("node:http");
|
||||
const { Server } = require("socket.io");
|
||||
const session = require("express-session");
|
||||
const bodyParser = require("body-parser");
|
||||
const passport = require("passport");
|
||||
const LocalStrategy = require("passport-local").Strategy;
|
||||
const { join } = require("node:path");
|
||||
|
||||
const port = process.env.PORT || 3000;
|
||||
|
||||
const app = express();
|
||||
const httpServer = createServer(app);
|
||||
|
||||
const sessionMiddleware = session({
|
||||
secret: "changeit",
|
||||
resave: true,
|
||||
saveUninitialized: true,
|
||||
});
|
||||
|
||||
app.use(sessionMiddleware);
|
||||
app.use(bodyParser.urlencoded({ extended: false }));
|
||||
app.use(passport.session());
|
||||
|
||||
app.get("/", (req, res) => {
|
||||
if (!req.user) {
|
||||
return res.redirect("/login");
|
||||
}
|
||||
res.sendFile(join(__dirname, "index.html"));
|
||||
});
|
||||
|
||||
app.get("/login", (req, res) => {
|
||||
if (req.user) {
|
||||
return res.redirect("/");
|
||||
}
|
||||
res.sendFile(join(__dirname, "login.html"));
|
||||
});
|
||||
|
||||
app.post(
|
||||
"/login",
|
||||
passport.authenticate("local", {
|
||||
successRedirect: "/",
|
||||
failureRedirect: "/",
|
||||
}),
|
||||
);
|
||||
|
||||
app.post("/logout", (req, res) => {
|
||||
const sessionId = req.session.id;
|
||||
req.session.destroy(() => {
|
||||
// disconnect all Socket.IO connections linked to this session ID
|
||||
io.to(`session:${sessionId}`).disconnectSockets();
|
||||
res.status(204).end();
|
||||
});
|
||||
});
|
||||
|
||||
passport.use(
|
||||
new LocalStrategy((username, password, done) => {
|
||||
if (username === "john" && password === "changeit") {
|
||||
console.log("authentication OK");
|
||||
return done(null, { id: 1, username });
|
||||
} else {
|
||||
console.log("wrong credentials");
|
||||
return done(null, false);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
passport.serializeUser((user, cb) => {
|
||||
console.log(`serializeUser ${user.id}`);
|
||||
cb(null, user);
|
||||
});
|
||||
|
||||
passport.deserializeUser((user, cb) => {
|
||||
console.log(`deserializeUser ${user.id}`);
|
||||
cb(null, user);
|
||||
});
|
||||
|
||||
const io = new Server(httpServer);
|
||||
|
||||
function onlyForHandshake(middleware) {
|
||||
return (req, res, next) => {
|
||||
const isHandshake = req._query.sid === undefined;
|
||||
if (isHandshake) {
|
||||
middleware(req, res, next);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
io.engine.use(onlyForHandshake(sessionMiddleware));
|
||||
io.engine.use(onlyForHandshake(passport.session()));
|
||||
io.engine.use(
|
||||
onlyForHandshake((req, res, next) => {
|
||||
if (req.user) {
|
||||
next();
|
||||
} else {
|
||||
res.writeHead(401);
|
||||
res.end();
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
io.on("connection", (socket) => {
|
||||
const req = socket.request;
|
||||
|
||||
socket.join(`session:${req.session.id}`);
|
||||
socket.join(`user:${req.user.id}`);
|
||||
|
||||
socket.on("whoami", (cb) => {
|
||||
cb(req.user.username);
|
||||
});
|
||||
});
|
||||
|
||||
httpServer.listen(port, () => {
|
||||
console.log(`application is running at: http://localhost:${port}`);
|
||||
});
|
||||
@@ -8,17 +8,17 @@
|
||||
<p>Not authenticated</p>
|
||||
<form action="/login" method="post">
|
||||
<div>
|
||||
<label>Username:</label>
|
||||
<input type="text" name="username" />
|
||||
<label for="username">Username:</label>
|
||||
<input type="text" id="username" name="username" />
|
||||
<br/>
|
||||
</div>
|
||||
<div>
|
||||
<label>Password:</label>
|
||||
<input type="password" name="password" />
|
||||
<label for="password">Password:</label>
|
||||
<input type="password" id="password" name="password" />
|
||||
</div>
|
||||
<div>
|
||||
<input type="submit" value="Submit" />
|
||||
</div>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
20
examples/passport-example/cjs/package.json
Normal file
20
examples/passport-example/cjs/package.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "passport-example",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "commonjs",
|
||||
"description": "Example with passport (https://www.passportjs.org)",
|
||||
"scripts": {
|
||||
"start": "node index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "~4.17.3",
|
||||
"express-session": "~1.17.2",
|
||||
"passport": "^0.7.0",
|
||||
"passport-local": "^1.0.0",
|
||||
"socket.io": "^4.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^3.1.1"
|
||||
}
|
||||
}
|
||||
55
examples/passport-example/esm/index.html
Normal file
55
examples/passport-example/esm/index.html
Normal file
@@ -0,0 +1,55 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Passport example</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Authenticated!</p>
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td><span id="status">Disconnected</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Socket ID</td>
|
||||
<td><span id="socket-id"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Username</td>
|
||||
<td><span id="username"></span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<form action="/logout" method="post">
|
||||
<div>
|
||||
<input type="submit" value="Log out" />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script src="/socket.io/socket.io.js"></script>
|
||||
<script>
|
||||
const socket = io();
|
||||
const socketIdSpan = document.getElementById('socket-id');
|
||||
const usernameSpan = document.getElementById('username');
|
||||
const statusSpan = document.getElementById('status');
|
||||
|
||||
socket.on('connect', () => {
|
||||
statusSpan.innerText = 'connected';
|
||||
socketIdSpan.innerText = socket.id;
|
||||
|
||||
socket.emit('whoami', (username) => {
|
||||
usernameSpan.innerText = username;
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
statusSpan.innerText = 'disconnected';
|
||||
socketIdSpan.innerText = '-';
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
120
examples/passport-example/esm/index.js
Normal file
120
examples/passport-example/esm/index.js
Normal file
@@ -0,0 +1,120 @@
|
||||
import express from "express";
|
||||
import { createServer } from "http";
|
||||
import { Server } from "socket.io";
|
||||
import session from "express-session";
|
||||
import bodyParser from "body-parser";
|
||||
import passport from "passport";
|
||||
import { Strategy as LocalStrategy } from "passport-local";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const port = process.env.PORT || 3000;
|
||||
|
||||
const app = express();
|
||||
const httpServer = createServer(app);
|
||||
|
||||
const sessionMiddleware = session({
|
||||
secret: "changeit",
|
||||
resave: true,
|
||||
saveUninitialized: true,
|
||||
});
|
||||
|
||||
app.use(sessionMiddleware);
|
||||
app.use(bodyParser.urlencoded({ extended: false }));
|
||||
app.use(passport.session());
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
app.get("/", (req, res) => {
|
||||
if (!req.user) {
|
||||
return res.redirect("/login");
|
||||
}
|
||||
res.sendFile(join(__dirname, "index.html"));
|
||||
});
|
||||
|
||||
app.get("/login", (req, res) => {
|
||||
if (req.user) {
|
||||
return res.redirect("/");
|
||||
}
|
||||
res.sendFile(join(__dirname, "login.html"));
|
||||
});
|
||||
|
||||
app.post(
|
||||
"/login",
|
||||
passport.authenticate("local", {
|
||||
successRedirect: "/",
|
||||
failureRedirect: "/",
|
||||
}),
|
||||
);
|
||||
|
||||
app.post("/logout", (req, res) => {
|
||||
const sessionId = req.session.id;
|
||||
req.session.destroy(() => {
|
||||
// disconnect all Socket.IO connections linked to this session ID
|
||||
io.to(`session:${sessionId}`).disconnectSockets();
|
||||
res.status(204).end();
|
||||
});
|
||||
});
|
||||
|
||||
passport.use(
|
||||
new LocalStrategy((username, password, done) => {
|
||||
if (username === "john" && password === "changeit") {
|
||||
console.log("authentication OK");
|
||||
return done(null, { id: 1, username });
|
||||
} else {
|
||||
console.log("wrong credentials");
|
||||
return done(null, false);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
passport.serializeUser((user, cb) => {
|
||||
console.log(`serializeUser ${user.id}`);
|
||||
cb(null, user);
|
||||
});
|
||||
|
||||
passport.deserializeUser((user, cb) => {
|
||||
console.log(`deserializeUser ${user.id}`);
|
||||
cb(null, user);
|
||||
});
|
||||
|
||||
const io = new Server(httpServer);
|
||||
|
||||
function onlyForHandshake(middleware) {
|
||||
return (req, res, next) => {
|
||||
const isHandshake = req._query.sid === undefined;
|
||||
if (isHandshake) {
|
||||
middleware(req, res, next);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
io.engine.use(onlyForHandshake(sessionMiddleware));
|
||||
io.engine.use(onlyForHandshake(passport.session()));
|
||||
io.engine.use(
|
||||
onlyForHandshake((req, res, next) => {
|
||||
if (req.user) {
|
||||
next();
|
||||
} else {
|
||||
res.writeHead(401);
|
||||
res.end();
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
io.on("connection", (socket) => {
|
||||
const req = socket.request;
|
||||
|
||||
socket.join(`session:${req.session.id}`);
|
||||
socket.join(`user:${req.user.id}`);
|
||||
|
||||
socket.on("whoami", (cb) => {
|
||||
cb(req.user.username);
|
||||
});
|
||||
});
|
||||
|
||||
httpServer.listen(port, () => {
|
||||
console.log(`application is running at: http://localhost:${port}`);
|
||||
});
|
||||
24
examples/passport-example/esm/login.html
Normal file
24
examples/passport-example/esm/login.html
Normal file
@@ -0,0 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Passport example</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Not authenticated</p>
|
||||
<form action="/login" method="post">
|
||||
<div>
|
||||
<label for="username">Username:</label>
|
||||
<input type="text" id="username" name="username" />
|
||||
<br/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="password">Password:</label>
|
||||
<input type="password" id="password" name="password" />
|
||||
</div>
|
||||
<div>
|
||||
<input type="submit" value="Submit" />
|
||||
</div>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
20
examples/passport-example/esm/package.json
Normal file
20
examples/passport-example/esm/package.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "passport-example",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Example with passport (https://www.passportjs.org)",
|
||||
"scripts": {
|
||||
"start": "node index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "~4.17.3",
|
||||
"express-session": "~1.17.2",
|
||||
"passport": "^0.7.0",
|
||||
"passport-local": "^1.0.0",
|
||||
"socket.io": "^4.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^3.1.1"
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Passport example</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Authenticated!</p>
|
||||
<p>Socket ID: <span id="socketId"></span></p>
|
||||
<p>Username: <span id="username"></span></p>
|
||||
<form action="/logout" method="post">
|
||||
<div>
|
||||
<input type="submit" value="Log out" />
|
||||
</div>
|
||||
</form>
|
||||
<script src="/socket.io/socket.io.js"></script>
|
||||
<script>
|
||||
const socket = io();
|
||||
const socketIdSpan = document.getElementById("socketId");
|
||||
const usernameSpan = document.getElementById("username");
|
||||
|
||||
socket.on('connect', () => {
|
||||
socketIdSpan.innerText = socket.id;
|
||||
|
||||
socket.emit('whoami', (username) => {
|
||||
usernameSpan.innerText = username;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,104 +0,0 @@
|
||||
const app = require("express")();
|
||||
const server = require("http").createServer(app);
|
||||
const port = process.env.PORT || 3000;
|
||||
|
||||
const session = require("express-session");
|
||||
const bodyParser = require("body-parser");
|
||||
const passport = require("passport");
|
||||
const LocalStrategy = require("passport-local").Strategy;
|
||||
|
||||
const sessionMiddleware = session({ secret: "changeit", resave: false, saveUninitialized: false });
|
||||
app.use(sessionMiddleware);
|
||||
app.use(bodyParser.urlencoded({ extended: false }));
|
||||
app.use(passport.initialize());
|
||||
app.use(passport.session());
|
||||
|
||||
const DUMMY_USER = {
|
||||
id: 1,
|
||||
username: "john",
|
||||
};
|
||||
|
||||
passport.use(
|
||||
new LocalStrategy((username, password, done) => {
|
||||
if (username === "john" && password === "doe") {
|
||||
console.log("authentication OK");
|
||||
return done(null, DUMMY_USER);
|
||||
} else {
|
||||
console.log("wrong credentials");
|
||||
return done(null, false);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
app.get("/", (req, res) => {
|
||||
const isAuthenticated = !!req.user;
|
||||
if (isAuthenticated) {
|
||||
console.log(`user is authenticated, session is ${req.session.id}`);
|
||||
} else {
|
||||
console.log("unknown user");
|
||||
}
|
||||
res.sendFile(isAuthenticated ? "index.html" : "login.html", { root: __dirname });
|
||||
});
|
||||
|
||||
app.post(
|
||||
"/login",
|
||||
passport.authenticate("local", {
|
||||
successRedirect: "/",
|
||||
failureRedirect: "/",
|
||||
})
|
||||
);
|
||||
|
||||
app.post("/logout", (req, res) => {
|
||||
console.log(`logout ${req.session.id}`);
|
||||
const socketId = req.session.socketId;
|
||||
if (socketId && io.of("/").sockets.get(socketId)) {
|
||||
console.log(`forcefully closing socket ${socketId}`);
|
||||
io.of("/").sockets.get(socketId).disconnect(true);
|
||||
}
|
||||
req.logout();
|
||||
res.cookie("connect.sid", "", { expires: new Date() });
|
||||
res.redirect("/");
|
||||
});
|
||||
|
||||
passport.serializeUser((user, cb) => {
|
||||
console.log(`serializeUser ${user.id}`);
|
||||
cb(null, user.id);
|
||||
});
|
||||
|
||||
passport.deserializeUser((id, cb) => {
|
||||
console.log(`deserializeUser ${id}`);
|
||||
cb(null, DUMMY_USER);
|
||||
});
|
||||
|
||||
const io = require('socket.io')(server);
|
||||
|
||||
// convert a connect middleware to a Socket.IO middleware
|
||||
const wrap = middleware => (socket, next) => middleware(socket.request, {}, next);
|
||||
|
||||
io.use(wrap(sessionMiddleware));
|
||||
io.use(wrap(passport.initialize()));
|
||||
io.use(wrap(passport.session()));
|
||||
|
||||
io.use((socket, next) => {
|
||||
if (socket.request.user) {
|
||||
next();
|
||||
} else {
|
||||
next(new Error('unauthorized'))
|
||||
}
|
||||
});
|
||||
|
||||
io.on('connect', (socket) => {
|
||||
console.log(`new connection ${socket.id}`);
|
||||
socket.on('whoami', (cb) => {
|
||||
cb(socket.request.user ? socket.request.user.username : '');
|
||||
});
|
||||
|
||||
const session = socket.request.session;
|
||||
console.log(`saving sid ${socket.id} in session ${session.id}`);
|
||||
session.socketId = socket.id;
|
||||
session.save();
|
||||
});
|
||||
|
||||
server.listen(port, () => {
|
||||
console.log(`application is running at: http://localhost:${port}`);
|
||||
});
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"name": "passport-example",
|
||||
"version": "0.0.1",
|
||||
"description": "Example with Passport (http://www.passportjs.org/)",
|
||||
"dependencies": {
|
||||
"body-parser": "~1.19.0",
|
||||
"express": "~4.17.1",
|
||||
"express-session": "~1.17.1",
|
||||
"passport": "~0.4.1",
|
||||
"passport-local": "~1.0.0",
|
||||
"socket.io": "^4.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node index.js"
|
||||
}
|
||||
}
|
||||
55
examples/passport-example/ts/index.html
Normal file
55
examples/passport-example/ts/index.html
Normal file
@@ -0,0 +1,55 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Passport example</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Authenticated!</p>
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td><span id="status">Disconnected</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Socket ID</td>
|
||||
<td><span id="socket-id"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Username</td>
|
||||
<td><span id="username"></span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<form action="/logout" method="post">
|
||||
<div>
|
||||
<input type="submit" value="Log out" />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script src="/socket.io/socket.io.js"></script>
|
||||
<script>
|
||||
const socket = io();
|
||||
const socketIdSpan = document.getElementById('socket-id');
|
||||
const usernameSpan = document.getElementById('username');
|
||||
const statusSpan = document.getElementById('status');
|
||||
|
||||
socket.on('connect', () => {
|
||||
statusSpan.innerText = 'connected';
|
||||
socketIdSpan.innerText = socket.id;
|
||||
|
||||
socket.emit('whoami', (username) => {
|
||||
usernameSpan.innerText = username;
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
statusSpan.innerText = 'disconnected';
|
||||
socketIdSpan.innerText = '-';
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
137
examples/passport-example/ts/index.ts
Normal file
137
examples/passport-example/ts/index.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import express = require("express");
|
||||
import { createServer } from "http";
|
||||
import { Server } from "socket.io";
|
||||
import session from "express-session";
|
||||
import { type Request, type Response } from "express";
|
||||
import bodyParser = require("body-parser");
|
||||
import passport = require("passport");
|
||||
import { Strategy as LocalStrategy } from "passport-local";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
declare global {
|
||||
namespace Express {
|
||||
interface User {
|
||||
id: number;
|
||||
username: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const port = process.env.PORT || 3000;
|
||||
|
||||
const app = express();
|
||||
const httpServer = createServer(app);
|
||||
|
||||
const sessionMiddleware = session({
|
||||
secret: "changeit",
|
||||
resave: true,
|
||||
saveUninitialized: true,
|
||||
});
|
||||
|
||||
app.use(sessionMiddleware);
|
||||
app.use(bodyParser.urlencoded({ extended: false }));
|
||||
app.use(passport.initialize());
|
||||
app.use(passport.session());
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
app.get("/", (req, res) => {
|
||||
if (!req.user) {
|
||||
return res.redirect("/login");
|
||||
}
|
||||
res.sendFile(join(__dirname, "index.html"));
|
||||
});
|
||||
|
||||
app.get("/login", (req, res) => {
|
||||
if (req.user) {
|
||||
return res.redirect("/");
|
||||
}
|
||||
res.sendFile(join(__dirname, "login.html"));
|
||||
});
|
||||
|
||||
app.post(
|
||||
"/login",
|
||||
passport.authenticate("local", {
|
||||
successRedirect: "/",
|
||||
failureRedirect: "/",
|
||||
}),
|
||||
);
|
||||
|
||||
app.post("/logout", (req, res) => {
|
||||
const sessionId = req.session.id;
|
||||
req.session.destroy(() => {
|
||||
// disconnect all Socket.IO connections linked to this session ID
|
||||
io.to(`session:${sessionId}`).disconnectSockets();
|
||||
res.status(204).end();
|
||||
});
|
||||
});
|
||||
|
||||
passport.use(
|
||||
new LocalStrategy((username, password, done) => {
|
||||
if (username === "john" && password === "changeit") {
|
||||
console.log("authentication OK");
|
||||
return done(null, { id: 1, username });
|
||||
} else {
|
||||
console.log("wrong credentials");
|
||||
return done(null, false);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
passport.serializeUser((user, cb) => {
|
||||
console.log(`serializeUser ${user.id}`);
|
||||
cb(null, user);
|
||||
});
|
||||
|
||||
passport.deserializeUser((user: Express.User, cb) => {
|
||||
console.log(`deserializeUser ${user.id}`);
|
||||
cb(null, user);
|
||||
});
|
||||
|
||||
const io = new Server(httpServer);
|
||||
|
||||
function onlyForHandshake(
|
||||
middleware: (req: Request, res: Response, next: any) => void,
|
||||
) {
|
||||
return (
|
||||
req: Request & { _query: Record<string, string> },
|
||||
res: Response,
|
||||
next: (err?: Error) => void,
|
||||
) => {
|
||||
const isHandshake = req._query.sid === undefined;
|
||||
if (isHandshake) {
|
||||
middleware(req, res, next);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
io.engine.use(onlyForHandshake(sessionMiddleware));
|
||||
io.engine.use(onlyForHandshake(passport.session()));
|
||||
io.engine.use(
|
||||
onlyForHandshake((req, res, next) => {
|
||||
if (req.user) {
|
||||
next();
|
||||
} else {
|
||||
res.writeHead(401);
|
||||
res.end();
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
io.on("connection", (socket) => {
|
||||
const req = socket.request as Request & { user: Express.User };
|
||||
|
||||
socket.join(`session:${req.session.id}`);
|
||||
socket.join(`user:${req.user.id}`);
|
||||
|
||||
socket.on("whoami", (cb) => {
|
||||
cb(req.user.username);
|
||||
});
|
||||
});
|
||||
|
||||
httpServer.listen(port, () => {
|
||||
console.log(`application is running at: http://localhost:${port}`);
|
||||
});
|
||||
24
examples/passport-example/ts/login.html
Normal file
24
examples/passport-example/ts/login.html
Normal file
@@ -0,0 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Passport example</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Not authenticated</p>
|
||||
<form action="/login" method="post">
|
||||
<div>
|
||||
<label for="username">Username:</label>
|
||||
<input type="text" id="username" name="username" />
|
||||
<br/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="password">Password:</label>
|
||||
<input type="password" id="password" name="password" />
|
||||
</div>
|
||||
<div>
|
||||
<input type="submit" value="Submit" />
|
||||
</div>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
27
examples/passport-example/ts/package.json
Normal file
27
examples/passport-example/ts/package.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "passport-example",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Example with passport (https://www.passportjs.org)",
|
||||
"scripts": {
|
||||
"start": "ts-node index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/express-session": "^1.17.7",
|
||||
"@types/node": "^20.6.0",
|
||||
"@types/passport": "^1.0.16",
|
||||
"express": "~4.17.3",
|
||||
"express-session": "~1.17.2",
|
||||
"passport": "^0.7.0",
|
||||
"passport-local": "^1.0.0",
|
||||
"socket.io": "^4.7.2",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/passport-local": "^1.0.38",
|
||||
"prettier": "^3.1.1"
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user