diff --git a/.github/workflows/socketio_e2e.yml b/.github/workflows/socketio_e2e.yml index 73d0954b3..cb1b237d6 100644 --- a/.github/workflows/socketio_e2e.yml +++ b/.github/workflows/socketio_e2e.yml @@ -7,10 +7,6 @@ on: branches: [ "main" ] paths: - 'sdk/**' - pull_request_target: - branches: [ "main" ] - paths: - - 'sdk/**' env: NODE_VERSION: '18.x' # set this to the node version to use jobs: @@ -49,4 +45,4 @@ jobs: SocketIoPort: 3000 run: | pushd sdk/webpubsub-socketio-extension - yarn run test \ No newline at end of file + yarn run test diff --git a/sdk/webpubsub-socketio-extension/examples/admin-ui/chat/README.md b/sdk/webpubsub-socketio-extension/examples/admin-ui/chat/README.md new file mode 100644 index 000000000..b16ee1fe1 --- /dev/null +++ b/sdk/webpubsub-socketio-extension/examples/admin-ui/chat/README.md @@ -0,0 +1,22 @@ + +# Azure Socket.IO Admin UI Sample + +This sample is modified from the sample "chat" to show how to use Azure Socket.IO Admin UI. + +## How to use +1. Open Azure Socket.IO Admin UI (its URL is not determined so far) in your browser. +2. Click the "Update" button in the right top corner and fill in the service endpoint of your resource. +3. Install the dependencies and start the server + ```bash + $ npm install + $ npm run start -- "" + ``` +4. Open a new tab to `http://localhost:3000` in your browser. Try the chat room. +5. Go back to the Admin UI and check related information. + +## Note +- The two lines below are necessary to make the Admin UI work + ```javascript + const { Namespace } = require("socket.io"); + Namespace.prototype["fetchSockets"] = async function() { return this.local.fetchSockets(); }; + ``` \ No newline at end of file diff --git a/sdk/webpubsub-socketio-extension/examples/admin-ui/chat/index.js b/sdk/webpubsub-socketio-extension/examples/admin-ui/chat/index.js new file mode 100644 index 000000000..1e066977a --- /dev/null +++ b/sdk/webpubsub-socketio-extension/examples/admin-ui/chat/index.js @@ -0,0 +1,76 @@ +const { instrument } = require("@socket.io/admin-ui"); +const azure = require("@azure/web-pubsub-socket.io"); +const express = require('express'); +const app = express(); +const path = require('path'); +const { Namespace, Server } = require("socket.io"); +const server = require('http').createServer(app); + +// Add an Web PubSub Option +const wpsOptions = { + hub: "eio_hub", + connectionString: process.argv[2] || process.env.WebPubSubConnectionString, + webPubSubServiceClientOptions: { allowInsecureConnection: true } +}; +const port = process.env.port || 3000; + +async function main() { + const io = await new Server(server).useAzureSocketIO(wpsOptions); + app.use(express.static(path.join(__dirname, 'public'))); + app.get("/negotiate", azure.negotiate(io, () => {})); + + // Add Support for Azure Socket.IO Admin UI + instrument(io, { auth: false, mode: "development" }); + Namespace.prototype["fetchSockets"] = async function() { return this.local.fetchSockets(); }; + + let numUsers = 0; + + io.on('connection', socket => { + let addedUser = false; + + // when the client emits 'new message', this listens and executes + socket.on('new message', (data) => { + // we tell the client to execute 'new message' + socket.broadcast.emit('new message', { + username: socket.username, + message: data + }); + }); + + // when the client emits 'add user', this listens and executes + socket.on('add user', (username) => { + if (addedUser) return; + + // we store the username in the socket session for this client + socket.username = username; + ++numUsers; + addedUser = true; + socket.emit('login', { + numUsers: numUsers + }); + // echo globally (all clients) that a person has connected + socket.broadcast.emit('user joined', { + username: socket.username, + numUsers: numUsers + }); + }); + + // when the user disconnects.. perform this + socket.on('disconnect', () => { + if (addedUser) { + --numUsers; + + // echo globally that this client has left + socket.broadcast.emit('user left', { + username: socket.username, + numUsers: numUsers + }); + } + }); + }); + io.httpServer.listen(port, () => { + console.log('Visit http://localhost:%d', port); + }); +} + +main(); \ No newline at end of file diff --git a/sdk/webpubsub-socketio-extension/examples/admin-ui/chat/package.json b/sdk/webpubsub-socketio-extension/examples/admin-ui/chat/package.json new file mode 100644 index 000000000..fb88582ce --- /dev/null +++ b/sdk/webpubsub-socketio-extension/examples/admin-ui/chat/package.json @@ -0,0 +1,17 @@ +{ + "name": "socket.io-chat", + "version": "0.0.0", + "description": "A simple chat client using socket.io", + "main": "index.js", + "private": true, + "license": "BSD", + "dependencies": { + "@azure/web-pubsub-socket.io": "^1.1.0", + "@socket.io/admin-ui": "^0.5.1", + "express": "~4.17.1", + "socket.io": "^4.6.1" + }, + "scripts": { + "start": "node index.js" + } +} diff --git a/sdk/webpubsub-socketio-extension/examples/admin-ui/chat/public/index.html b/sdk/webpubsub-socketio-extension/examples/admin-ui/chat/public/index.html new file mode 100644 index 000000000..d47f95ab1 --- /dev/null +++ b/sdk/webpubsub-socketio-extension/examples/admin-ui/chat/public/index.html @@ -0,0 +1,33 @@ + + + + + Socket.IO Chat Example + + + + + + + + + + + \ No newline at end of file diff --git a/sdk/webpubsub-socketio-extension/examples/admin-ui/chat/public/main.js b/sdk/webpubsub-socketio-extension/examples/admin-ui/chat/public/main.js new file mode 100644 index 000000000..740b99700 --- /dev/null +++ b/sdk/webpubsub-socketio-extension/examples/admin-ui/chat/public/main.js @@ -0,0 +1,252 @@ +async function main() { + const FADE_TIME = 150; // ms + const TYPING_TIMER_LENGTH = 400; // ms + const COLORS = [ + '#e21400', '#91580f', '#f8a700', '#f78b00', + '#58dc00', '#287b00', '#a8f07a', '#4ae8c4', + '#3b88eb', '#3824aa', '#a700ff', '#d300e7' + ]; + + // Initialize variables + const $window = $(window); + const $usernameInput = $('.usernameInput'); // Input for username + const $messages = $('.messages'); // Messages area + const $inputMessage = $('.inputMessage'); // Input message input box + + const $loginPage = $('.login.page'); // The login page + const $chatPage = $('.chat.page'); // The chatroom page + + const negotiateResponse = await fetch(`/negotiate`); + if (!negotiateResponse.ok) { + console.log("Failed to negotiate, status code =", negotiateResponse.status); + return ; + } + const json = await negotiateResponse.json(); + console.log("endpoint=", json.endpoint); + var socket = io(json.endpoint, { + path: json.path, + query: { + access_token: json.token + } + }); + + // Prompt for setting a username + let username; + let connected = false; + let $currentInput = $usernameInput.focus(); + + const addParticipantsMessage = (data) => { + let message = ''; + if (data.numUsers === 1) { + message += `there's 1 participant`; + } else { + message += `there are ${data.numUsers} participants`; + } + log(message); + } + + // Sets the client's username + const setUsername = () => { + username = cleanInput($usernameInput.val().trim()); + + // If the username is valid + if (username) { + $loginPage.fadeOut(); + $chatPage.show(); + $loginPage.off('click'); + $currentInput = $inputMessage.focus(); + + // Tell the server your username + socket.emit('add user', username); + } + } + + // Sends a chat message + const sendMessage = () => { + let message = $inputMessage.val(); + // Prevent markup from being injected into the message + message = cleanInput(message); + // if there is a non-empty message and a socket connection + if (message && connected) { + $inputMessage.val(''); + addChatMessage({ username, message }); + // tell server to execute 'new message' and send along one parameter + socket.emit('new message', message); + } + } + + // Log a message + const log = (message, options) => { + const $el = $('
  • ').addClass('log').text(message); + addMessageElement($el, options); + } + + // Adds the visual chat message to the message list + const addChatMessage = (data, options = {}) => { + // Don't fade the message in if there is an 'X was typing' + const $typingMessages = getTypingMessages(data); + if ($typingMessages.length !== 0) { + options.fade = false; + $typingMessages.remove(); + } + + const $usernameDiv = $('') + .text(data.username) + .css('color', getUsernameColor(data.username)); + const $messageBodyDiv = $('') + .text(data.message); + + const typingClass = data.typing ? 'typing' : ''; + const $messageDiv = $('
  • ') + .data('username', data.username) + .addClass(typingClass) + .append($usernameDiv, $messageBodyDiv); + + addMessageElement($messageDiv, options); + } + + // Adds the visual chat typing message + const addChatTyping = (data) => { + data.typing = true; + data.message = 'is typing'; + addChatMessage(data); + } + + // Removes the visual chat typing message + const removeChatTyping = (data) => { + getTypingMessages(data).fadeOut(function () { + $(this).remove(); + }); + } + + // Adds a message element to the messages and scrolls to the bottom + // el - The element to add as a message + // options.fade - If the element should fade-in (default = true) + // options.prepend - If the element should prepend + // all other messages (default = false) + const addMessageElement = (el, options) => { + const $el = $(el); + // Setup default options + if (!options) { + options = {}; + } + if (typeof options.fade === 'undefined') { + options.fade = true; + } + if (typeof options.prepend === 'undefined') { + options.prepend = false; + } + + // Apply options + if (options.fade) { + $el.hide().fadeIn(FADE_TIME); + } + if (options.prepend) { + $messages.prepend($el); + } else { + $messages.append($el); + } + + $messages[0].scrollTop = $messages[0].scrollHeight; + } + + // Prevents input from having injected markup + const cleanInput = (input) => { + return $('
    ').text(input).html(); + } + + // Gets the 'X is typing' messages of a user + const getTypingMessages = (data) => { + return $('.typing.message').filter(function (i) { + return $(this).data('username') === data.username; + }); + } + + // Gets the color of a username through our hash function + const getUsernameColor = (username) => { + // Compute hash code + let hash = 7; + for (let i = 0; i < username.length; i++) { + hash = username.charCodeAt(i) + (hash << 5) - hash; + } + // Calculate color + const index = Math.abs(hash % COLORS.length); + return COLORS[index]; + } + + // Keyboard events + + $window.keydown(event => { + // Auto-focus the current input when a key is typed + if (!(event.ctrlKey || event.metaKey || event.altKey)) { + $currentInput.focus(); + } + // When the client hits ENTER on their keyboard + if (event.which === 13) { + if (username) { + sendMessage(); + } else { + setUsername(); + } + } + }); + + // Click events + + // Focus input when clicking anywhere on login page + $loginPage.click(() => { + $currentInput.focus(); + }); + + // Focus input when clicking on the message input's border + $inputMessage.click(() => { + $inputMessage.focus(); + }); + + // Socket events + + // Whenever the server emits 'login', log the login message + socket.on('login', (data) => { + connected = true; + // Display the welcome message + const message = 'Welcome to Socket.IO Chat – '; + log(message, { + prepend: true + }); + addParticipantsMessage(data); + }); + + // Whenever the server emits 'new message', update the chat body + socket.on('new message', (data) => { + addChatMessage(data); + }); + + // Whenever the server emits 'user joined', log it in the chat body + socket.on('user joined', (data) => { + log(`${data.username} joined`); + addParticipantsMessage(data); + }); + + // Whenever the server emits 'user left', log it in the chat body + socket.on('user left', (data) => { + log(`${data.username} left`); + addParticipantsMessage(data); + removeChatTyping(data); + }); + + socket.on('disconnect', () => { + log('you have been disconnected'); + }); + + socket.io.on('reconnect', () => { + log('you have been reconnected'); + if (username) { + socket.emit('add user', username); + } + }); + + socket.io.on('reconnect_error', () => { + log('attempt to reconnect has failed'); + }); +} +main(); \ No newline at end of file diff --git a/sdk/webpubsub-socketio-extension/examples/admin-ui/chat/public/style.css b/sdk/webpubsub-socketio-extension/examples/admin-ui/chat/public/style.css new file mode 100644 index 000000000..cd0a59a65 --- /dev/null +++ b/sdk/webpubsub-socketio-extension/examples/admin-ui/chat/public/style.css @@ -0,0 +1,150 @@ +/* Fix user-agent */ + +* { + box-sizing: border-box; +} + +html { + font-weight: 300; + -webkit-font-smoothing: antialiased; + background-color: rgb(62, 62, 62); +} + +html, input { + font-family: + "HelveticaNeue-Light", + "Helvetica Neue Light", + "Helvetica Neue", + Helvetica, + Arial, + "Lucida Grande", + sans-serif; +} + +html, body { + height: 100%; + margin: 0; + padding: 0; +} + +ul { + list-style: none; + word-wrap: break-word; +} + +/* Pages */ + +.pages { + height: 100%; + margin: 0; + padding: 0; + width: 100%; +} + +.page { + height: 100%; + position: absolute; + width: 100%; +} + +/* Login Page */ + +.login.page { + background-color: #000; +} + +.login.page .form { + height: 100px; + margin-top: -100px; + position: absolute; + + text-align: center; + top: 50%; + width: 100%; +} + +.login.page .form .usernameInput { + background-color: transparent; + border: none; + border-bottom: 2px solid #fff; + outline: none; + padding-bottom: 15px; + text-align: center; + width: 400px; +} + +.login.page .title { + font-size: 200%; +} + +.login.page .usernameInput { + font-size: 200%; + letter-spacing: 3px; +} + +.login.page .title, .login.page .usernameInput { + color: #fff; + font-weight: 100; +} + +/* Chat page */ + +.chat.page { + display: none; +} + +/* Font */ + +.messages { + font-size: 150%; +} + +.inputMessage { + font-size: 100%; +} + +.log { + color: gray; + font-size: 70%; + margin: 5px; + text-align: center; +} + +/* Messages */ + +.chatArea { + height: 100%; + padding-bottom: 60px; +} + +.messages { + height: 100%; + margin: 0; + overflow-y: scroll; + padding: 10px 20px 10px 20px; +} + +.message.typing .messageBody { + color: gray; +} + +.username { + font-weight: 700; + overflow: hidden; + padding-right: 15px; + text-align: right; +} + +/* Input */ + +.inputMessage { + border: 10px solid #000; + bottom: 0; + height: 60px; + left: 0; + outline: none; + padding-left: 10px; + position: absolute; + right: 0; + width: 100%; +} diff --git a/sdk/webpubsub-socketio-extension/examples/admin-ui/simple-benchmark/README.md b/sdk/webpubsub-socketio-extension/examples/admin-ui/simple-benchmark/README.md new file mode 100644 index 000000000..1676810f1 --- /dev/null +++ b/sdk/webpubsub-socketio-extension/examples/admin-ui/simple-benchmark/README.md @@ -0,0 +1,27 @@ + +# Azure Socket.IO Admin UI Simple Benchmark + +This sample is an Azure Socket.IO server to run a simple echo performance benchmark cooperating with Azure Socket.IO Admin UI. + +## How to use +1. Open Azure Socket.IO Admin UI website (its URL is not determined so far) in your browser. +2. Click the "Update" button in the right top corner and fill in the service endpoint of your resource. +3. Install the dependencies and start the server + ```bash + $ npm install + $ export PORT=3000 # Set environmental variable for server port number. Default value is 3000 + $ npm run start -- "" + ``` +4. Open Admin UI page and click the "Benchmark" tab in the left sidebar. +5. Fill in parameters and then start the benchmark. + +## Note +- The two lines below are necessary to make the Admin UI work + ```javascript + const { Namespace } = require("socket.io"); + Namespace.prototype["fetchSockets"] = async function() { return this.local.fetchSockets(); }; + ``` + +- The simple echo benchmark is designed for dev/test use. DO NOT refer to it for production use. + + The benchmark result is largely different from a serious benchmark result, for all clients are hosted in a web browser of a single machine. \ No newline at end of file diff --git a/sdk/webpubsub-socketio-extension/examples/admin-ui/simple-benchmark/index.js b/sdk/webpubsub-socketio-extension/examples/admin-ui/simple-benchmark/index.js new file mode 100644 index 000000000..2317b4051 --- /dev/null +++ b/sdk/webpubsub-socketio-extension/examples/admin-ui/simple-benchmark/index.js @@ -0,0 +1,39 @@ +const { instrument } = require("@socket.io/admin-ui"); +const azure = require("@azure/web-pubsub-socket.io"); +const express = require('express'); +const app = express(); +const path = require('path'); +const { Namespace, Server } = require("socket.io"); +const server = require('http').createServer(app); + +// Add an Web PubSub Option +const wpsOptions = { + hub: "eio_hub", + connectionString: process.argv[2] || process.env.WebPubSubConnectionString, + webPubSubServiceClientOptions: { allowInsecureConnection: true } +}; +const port = process.env.port || 3000; + +async function main() { + const io = await new Server(server).useAzureSocketIO(wpsOptions); + app.use(express.static(path.join(__dirname, 'public'))); + app.get("/negotiate", azure.negotiate(io, () => {})); + + // Add Support for Azure Socket.IO Admin UI + instrument(io, { auth: false, mode: "development" }); + Namespace.prototype["fetchSockets"] = async function() { return this.local.fetchSockets(); }; + + const echoBenchmark = io.of("/echoBenchmark"); + + echoBenchmark.on('connection', (socket) => { + socket.on('client to server event', (data) => { + socket.emit("server to client event", (data)); + }); + }); + + io.httpServer.listen(port, () => { + console.log('Visit http://localhost:%d', port); + }); +} + +main(); \ No newline at end of file diff --git a/sdk/webpubsub-socketio-extension/examples/admin-ui/simple-benchmark/package.json b/sdk/webpubsub-socketio-extension/examples/admin-ui/simple-benchmark/package.json new file mode 100644 index 000000000..fb88582ce --- /dev/null +++ b/sdk/webpubsub-socketio-extension/examples/admin-ui/simple-benchmark/package.json @@ -0,0 +1,17 @@ +{ + "name": "socket.io-chat", + "version": "0.0.0", + "description": "A simple chat client using socket.io", + "main": "index.js", + "private": true, + "license": "BSD", + "dependencies": { + "@azure/web-pubsub-socket.io": "^1.1.0", + "@socket.io/admin-ui": "^0.5.1", + "express": "~4.17.1", + "socket.io": "^4.6.1" + }, + "scripts": { + "start": "node index.js" + } +}