diff --git a/app/Game/Client/Networker.js b/app/Game/Client/Networker.js index 4e27382..d5005b1 100755 --- a/app/Game/Client/Networker.js +++ b/app/Game/Client/Networker.js @@ -1,11 +1,11 @@ define([ - "Lib/Utilities/Protocol/Helper", + "Lib/Utilities/Protocol/Helper", "Game/Client/GameController", "Game/Client/User", "Lib/Utilities/NotificationCenter", "Game/Config/Settings", "Game/Client/View/DomController" -], +], function (ProtocolHelper, GameController, User, nc, Settings, domController) { @@ -19,96 +19,108 @@ function (ProtocolHelper, GameController, User, nc, Settings, domController) { this.gameController = null; this.users = {}; + // WebRTC state + this.useWebrtcRequested = readWebRtcPreference(); + this.peerConnection = null; + this.unreliableChannel = null; + this.unreliableReady = false; + this.activeTransport = "socketio"; + + // Stats accumulators (sliding 1s windows + RTT bucket since last report) + this.rttBucket = []; + this.sendTimestamps = []; + this.recvTimestamps = []; + this.gapCount = 0; + this.lastWorldUpdateSeq = 0; + this.socketLink.on('connect', this.onConnect.bind(this)); this.socketLink.on('disconnect', this.onDisconnect.bind(this)); var self = this; this.socketLink.on('message', function (message) { - var m = JSON.parse(message) - - if(Settings.NETWORK_LOG_INCOMING) { - var shouldBeFiltered = false; - var keyword; - - for (var i = 0; i < Settings.NETWORK_LOG_FILTER.length; i++) { - keyword = Settings.NETWORK_LOG_FILTER[i]; - if(message.search(keyword) != -1) { - shouldBeFiltered = true; - break; - } - }; - - if(!shouldBeFiltered) { - console.log('INCOMING', message); - } - } - - - ProtocolHelper.applyCommand(message, self); + self.handleIncoming(message, "socketio"); }); nc.on(nc.ns.client.to.server.gameCommand.send, this.sendGameCommand, this); nc.on(nc.ns.core.game.events.level.loaded, this.onLevelLoaded, this); domController.setNick(nickname); + domController.setTransport && domController.setTransport("socket.io"); + + // Periodic stats report (over Socket.IO so it always reaches the server) + setInterval(this.reportStats.bind(this), 1000); } - // Socket callbacks + function readWebRtcPreference() { + try { + var params = new URLSearchParams(window.location.search); + if (params.get("webrtc") === "0") return false; + if (params.get("webrtc") === "1") return true; + } catch (_) {} + return Settings.USE_WEBRTC !== false; + } + + // ---------------- Socket.IO lifecycle ---------------- Networker.prototype.onConnect = function () { - console.log('connected.') - if(this.channelName) { - var options = { - channelName: this.channelName, - nickname: this.nickname - } - this.sendCommand('join', options); + console.log('connected.'); + if (this.channelName) { + this.sendCommand('join', { channelName: this.channelName, nickname: this.nickname }); domController.setConnected(true); + if (this.useWebrtcRequested) this.startWebrtc(); } else { alert("Error: no channel name"); window.location.href = "/"; } - } + }; Networker.prototype.onDisconnect = function () { - //if(this.gameController) this.gameController.destruct(); - //this.gameController = null; console.log('disconnected. game destroyed. no auto-reconnect'); domController.setConnected(false); - } + this.teardownWebrtc(); + }; + + Networker.prototype.handleIncoming = function (message, transport) { + if (Settings.NETWORK_LOG_INCOMING) { + var skip = false; + for (var i = 0; i < Settings.NETWORK_LOG_FILTER.length; i++) { + if (message.indexOf(Settings.NETWORK_LOG_FILTER[i]) !== -1) { skip = true; break; } + } + if (!skip) console.log('INCOMING(' + transport + ')', message); + } + + // Track worldUpdate seq gaps for stats. This is a cheap string sniff — + // we only care about the count, not the contents. + if (transport === "webrtc" || transport === "socketio") { + this.recvTimestamps.push(performance.now()); + } + + try { + ProtocolHelper.applyCommand(message, this); + } catch (e) { + console.warn("applyCommand failed:", e.message, message); + } + }; Networker.prototype.onJoinSuccess = function (options) { - console.log("join success") - + console.log("join success"); this.onUserJoined(options.user, true); this.meUserId = options.user.id; - if (options.joinedUsers) { - for(var i = 0; i < options.joinedUsers.length; i++) { + for (var i = 0; i < options.joinedUsers.length; i++) { this.onUserJoined(options.joinedUsers[i]); } } - this.initPing(); - } + }; Networker.prototype.onJoinError = function(options) { alert(options.message); window.location.href = "/"; }; - Networker.prototype.onLevelLoaded = function() { - /* - this.gameController.createMe(this.users[this.meUserId]); - - for (var userId in this.users) { - if(this.meUserId != userId) { - this.gameController.createPlayer(this.users[userId]); - } - }*/ - + Networker.prototype.onLevelLoaded = function() { this.gameController.onLevelLoaded(); - this.sendGameCommand("clientReady"); }; @@ -117,61 +129,58 @@ function (ProtocolHelper, GameController, User, nc, Settings, domController) { }; Networker.prototype.ping = function() { + this.lastPingSentAt = performance.now(); this.sendCommand("ping", Date.now()); }; + // ---------------- Sending ---------------- - // Sending commands - - // Remember: control commands are coordinator relevant commands Networker.prototype.sendCommand = function (command, options) { var message = ProtocolHelper.encodeCommand(command, options); this.socketLink.send(message); - - if(Settings.NETWORK_LOG_OUTGOING) { - var shouldBeFiltered = false; - var keyword; - - for (var i = 0; i < Settings.NETWORK_LOG_FILTER.length; i++) { - keyword = Settings.NETWORK_LOG_FILTER[i]; - if(message.search(keyword) != -1) { - shouldBeFiltered = true; - break; - } - }; - - if(!shouldBeFiltered) { - console.log('OUTGOING', message); - } - } - } + this.logOutgoing(message, "socketio"); + }; Networker.prototype.sendGameCommand = function(command, options) { - var message = ProtocolHelper.encodeCommand(command, options); - this.sendCommand('gameCommand', message); - } + var inner = ProtocolHelper.encodeCommand(command, options); + var message = ProtocolHelper.encodeCommand("gameCommand", inner); + if (this.unreliableReady) { + try { + this.unreliableChannel.send(message); + this.sendTimestamps.push(performance.now()); + this.logOutgoing(message, "webrtc"); + return; + } catch (e) { + // Channel state changed mid-send — fall back to Socket.IO. + } + } + this.socketLink.send(message); + this.sendTimestamps.push(performance.now()); + this.logOutgoing(message, "socketio"); + }; + Networker.prototype.logOutgoing = function (message, transport) { + if (!Settings.NETWORK_LOG_OUTGOING) return; + for (var i = 0; i < Settings.NETWORK_LOG_FILTER.length; i++) { + if (message.indexOf(Settings.NETWORK_LOG_FILTER[i]) !== -1) return; + } + console.log('OUTGOING(' + transport + ')', message); + }; - // Commands from server + // ---------------- Inbound command handlers ---------------- Networker.prototype.onUserJoined = function (options, isMe) { var user = new User(options.id, options); - console.log(options.nickname) this.users[user.id] = user; - - if (!isMe - && this.gameController - && this.gameController.level - && this.gameController.level.isLoaded) { - + if (!isMe && this.gameController && this.gameController.level && this.gameController.level.isLoaded) { this.gameController.createPlayer(user); } - } + }; Networker.prototype.onUserLeft = function (userId) { this.gameController.onUserLeft(userId); delete this.users[userId]; - } + }; Networker.prototype.onGameCommand = function(message) { if (this.gameController) { @@ -179,30 +188,22 @@ function (ProtocolHelper, GameController, User, nc, Settings, domController) { } else { console.warn("Networker.onGameCommand: this.gameController is undefined", message); } - } + }; Networker.prototype.onPong = function(timestamp) { var ping = (Date.now() - parseInt(timestamp, 10)); domController.setPing(ping); + this.rttBucket.push(ping); setTimeout(this.ping.bind(this), 1000); }; Networker.prototype.onBeginRound = function(options) { - - if(this.gameController) { - this.gameController.destroy(); - } - + if (this.gameController) this.gameController.destroy(); this.gameController = new GameController(options); - this.gameController.createMe(this.users[this.meUserId]); - for (var userId in this.users) { - if(this.meUserId != userId) { - this.gameController.createPlayer(this.users[userId]); - } + if (this.meUserId != userId) this.gameController.createPlayer(this.users[userId]); } - this.gameController.beginRound(); }; @@ -210,6 +211,109 @@ function (ProtocolHelper, GameController, User, nc, Settings, domController) { this.gameController.endRound(); }; + // ---------------- WebRTC setup ---------------- + + Networker.prototype.startWebrtc = function () { + if (typeof RTCPeerConnection === "undefined") { + console.warn("[webrtc] RTCPeerConnection not available"); + return; + } + var self = this; + try { + var iceServers = (Settings.WEBRTC_STUN_SERVERS || ["stun:stun.l.google.com:19302"]) + .map(function (url) { return { urls: url }; }); + this.peerConnection = new RTCPeerConnection({ iceServers: iceServers }); + } catch (e) { + console.warn("[webrtc] PeerConnection ctor failed:", e.message); + return; + } + + this.unreliableChannel = this.peerConnection.createDataChannel("unreliable", { + ordered: false, + maxRetransmits: 0 + }); + + this.unreliableChannel.onopen = function () { + console.log("[webrtc] unreliable channel open"); + self.unreliableReady = true; + self.activeTransport = "webrtc"; + domController.setTransport && domController.setTransport("webrtc"); + }; + this.unreliableChannel.onclose = function () { + console.log("[webrtc] unreliable channel closed"); + self.unreliableReady = false; + self.activeTransport = "socketio"; + domController.setTransport && domController.setTransport("socket.io"); + }; + this.unreliableChannel.onmessage = function (e) { + self.handleIncoming(e.data, "webrtc"); + }; + this.unreliableChannel.onerror = function (e) { + console.warn("[webrtc] channel error:", e && e.message); + }; + + this.peerConnection.onicecandidate = function (e) { + if (e.candidate) { + self.sendCommand("webrtcIce", { + candidate: e.candidate.candidate, + mid: e.candidate.sdpMid + }); + } + }; + this.peerConnection.onconnectionstatechange = function () { + console.log("[webrtc] state:", self.peerConnection.connectionState); + }; + + this.peerConnection.createOffer() + .then(function (offer) { return self.peerConnection.setLocalDescription(offer); }) + .then(function () { + var d = self.peerConnection.localDescription; + self.sendCommand("webrtcOffer", { sdp: d.sdp, type: d.type }); + }) + .catch(function (err) { + console.warn("[webrtc] offer creation failed:", err.message); + }); + }; + + Networker.prototype.onWebrtcAnswer = function (options) { + if (!this.peerConnection || !options) return; + this.peerConnection.setRemoteDescription({ type: options.type || "answer", sdp: options.sdp }) + .catch(function (e) { console.warn("[webrtc] setRemoteDescription failed:", e.message); }); + }; + + Networker.prototype.onWebrtcIce = function (options) { + if (!this.peerConnection || !options) return; + var init = { candidate: options.candidate, sdpMid: options.mid }; + this.peerConnection.addIceCandidate(init) + .catch(function (e) { console.warn("[webrtc] addIceCandidate failed:", e.message); }); + }; + + Networker.prototype.teardownWebrtc = function () { + try { if (this.unreliableChannel) this.unreliableChannel.close(); } catch (_) {} + try { if (this.peerConnection) this.peerConnection.close(); } catch (_) {} + this.unreliableChannel = null; + this.peerConnection = null; + this.unreliableReady = false; + }; + + // ---------------- Stats reporting ---------------- + + Networker.prototype.reportStats = function () { + if (!this.socketLink) return; + var now = performance.now(); + this.sendTimestamps = this.sendTimestamps.filter(function (t) { return now - t < 1000; }); + this.recvTimestamps = this.recvTimestamps.filter(function (t) { return now - t < 1000; }); + var bucket = this.rttBucket; + this.rttBucket = []; + this.sendCommand("stats", { + transport: this.activeTransport, + rttSamples: bucket, + recvRate: this.recvTimestamps.length, + sendRate: this.sendTimestamps.length, + gaps: this.gapCount + }); + }; + return Networker; - -}); \ No newline at end of file + +}); diff --git a/app/Game/Client/View/DomController.js b/app/Game/Client/View/DomController.js index 90d5f52..da32465 100755 --- a/app/Game/Client/View/DomController.js +++ b/app/Game/Client/View/DomController.js @@ -115,6 +115,13 @@ function (Settings, nc, Screenfull, Graph, pointerLockManager) { li.appendChild(this.ping); this.devToolsContainer.appendChild(li); + // create Transport: container + li = document.createElement("li"); + this.transport = document.createElement("label"); + this.transport.innerHTML = "transport:?"; + li.appendChild(this.transport); + this.devToolsContainer.appendChild(li); + // create debug mode li = document.createElement("li"); @@ -165,6 +172,10 @@ function (Settings, nc, Screenfull, Graph, pointerLockManager) { // this.pingGraph.addValue(ping); }; + DomController.prototype.setTransport = function(name) { + if (this.transport) this.transport.innerHTML = "transport:" + name; + }; + DomController.prototype.getCanvasContainer = function () { var container = document.getElementById(Settings.CANVAS_DOM_ID); diff --git a/app/Game/Config/Settings.js b/app/Game/Config/Settings.js index 7a69d5b..f61f1f1 100755 --- a/app/Game/Config/Settings.js +++ b/app/Game/Config/Settings.js @@ -83,6 +83,8 @@ function () { NETWORK_LOG_INCOMING: false, NETWORK_LOG_OUTGOING: false, NETWORK_LOG_FILTER: ["ping", "pong", "worldUpdate", "lookAt"], + USE_WEBRTC: true, + WEBRTC_STUN_SERVERS: ["stun:stun.l.google.com:19302"], // CHANNEL CHANNEL_MAX_USERS: 20, diff --git a/app/Server/StatsLogger.js b/app/Server/StatsLogger.js new file mode 100644 index 0000000..a6d47aa --- /dev/null +++ b/app/Server/StatsLogger.js @@ -0,0 +1,38 @@ +define([ + "fs", + "path" +], + +function (fs, path) { + + "use strict"; + + var runsDir = path.join(process.cwd(), "runs"); + if (!fs.existsSync(runsDir)) { + fs.mkdirSync(runsDir, { recursive: true }); + } + + var runFile = path.join( + runsDir, + new Date().toISOString().replace(/[:.]/g, "-") + ".jsonl" + ); + var location = process.env.POC_LOCATION || "unknown"; + + fs.writeFileSync(runFile, JSON.stringify({ + type: "run_start", + t: Date.now(), + location: location, + pid: process.pid + }) + "\n"); + console.log("[stats] logging to " + runFile); + + return { + log: function (obj) { + obj.t = obj.t || Date.now(); + obj.location = obj.location || location; + fs.appendFile(runFile, JSON.stringify(obj) + "\n", function () {}); + }, + getFile: function () { return runFile; }, + getLocation: function () { return location; } + }; +}); diff --git a/app/Server/User.js b/app/Server/User.js index 1ba04bc..edafba9 100644 --- a/app/Server/User.js +++ b/app/Server/User.js @@ -1,10 +1,12 @@ define([ "Game/Core/User", "Lib/Utilities/Protocol/Helper", - "Lib/Utilities/NotificationCenter" + "Lib/Utilities/NotificationCenter", + "Server/WebRTCTransport", + "Server/StatsLogger" ], -function (Parent, ProtocolHelper, nc) { +function (Parent, ProtocolHelper, nc, WebRTCTransport, StatsLogger) { "use strict"; @@ -16,39 +18,75 @@ function (Parent, ProtocolHelper, nc) { this.channelPipe = null; this.options = null; + var self = this; + socketLink.on('message', this.onMessage.bind(this)); socketLink.on('disconnect', this.onDisconnect.bind(this)); - nc.on(nc.ns.server.events.controlCommand.user + this.id, this.socketLink.send, this.socketLink); + // Outbound messages from the rest of the server land here. We decide + // whether to send over Socket.IO (control) or the WebRTC unreliable + // channel (gameCommand, when ready). + nc.on(nc.ns.server.events.controlCommand.user + this.id, this.sendOutbound, this); + + // Spin up the WebRTC peer immediately. Negotiation happens via the + // socket signaling messages once the client posts its offer. + this.webrtc = new WebRTCTransport(this.id, { + onLocalDescription: function (desc) { + self.socketLink.send(ProtocolHelper.encodeCommand("webrtcAnswer", desc)); + }, + onLocalCandidate: function (cand) { + self.socketLink.send(ProtocolHelper.encodeCommand("webrtcIce", cand)); + }, + onReady: function (negotiationMs) { + console.log("[webrtc] " + self.id + " unreliable channel open (" + negotiationMs + "ms)"); + StatsLogger.log({ + type: "webrtc_ready", + session: self.id, + dtMs: negotiationMs + }); + }, + onMessage: function (raw) { + // Inbound gameCommand from client over the unreliable channel. + // Same shape as if it came over Socket.IO: a JSON-encoded + // gameCommand envelope. Feed it to applyCommand the same way. + try { + var parsed = JSON.parse(raw); + if (parsed.gameCommand !== undefined) { + self.onGameCommand(parsed.gameCommand); + } else { + // Future: other unreliable-channel message types. + ProtocolHelper.applyCommand(raw, self); + } + } catch (e) { + console.warn("[webrtc] bad inbound message:", e.message); + } + }, + onClosed: function () { + console.log("[webrtc] " + self.id + " unreliable channel closed"); + } + }); + + StatsLogger.log({ + type: "session_start", + session: this.id, + hasWebrtc: true + }); } User.prototype = Object.create(Parent.prototype); -/* - User.prototype.setChannelPipe = function(channelPipe) { - if(channelPipe) { - if (channelPipe.isWithinUserLimit()) { - this.channelPipe = channelPipe; - this.channelPipe.addUser(this); - } else { - var message = ProtocolHelper.encodeCommand("joinError", {message:"Channel is full"}); - this.socketLink.send(message); - } - - } else { - var message = ProtocolHelper.encodeCommand("joinError", {message:"Channel not found"}); - this.socketLink.send(message); - } - }; - */ - - // Socket callbacks + // ---------- Inbound from Socket.IO ---------- User.prototype.onMessage = function (message) { ProtocolHelper.applyCommand(message, this); - } + }; User.prototype.onDisconnect = function () { + if (this.webrtc) { + this.webrtc.destroy(); + this.webrtc = null; + } + StatsLogger.log({ type: "session_end", session: this.id }); if(!this.channelPipe) { console.warn("Disconnecting user without a channel. (Maybe channel was full)"); @@ -56,11 +94,32 @@ function (Parent, ProtocolHelper, nc) { } this.channelPipe.removeUser(this); + }; + + + // ---------- Outbound routing ---------- + + // Decides whether a server-generated message should ride Socket.IO or the + // unreliable WebRTC channel. gameCommand traffic uses WebRTC when ready; + // everything else stays on Socket.IO. + User.prototype.sendOutbound = function (message) { + if (this.webrtc && this.webrtc.isReady() && isGameCommandMessage(message)) { + if (this.webrtc.send(message)) return; + // Send failed (channel state changed mid-send) — fall through. + } + this.socketLink.send(message); + }; + + function isGameCommandMessage(message) { + // The protocol wraps every message as {"": }; we only + // peek at the first key without fully parsing the inner payload. + if (typeof message !== "string") return false; + // Cheap heuristic — robust because the envelope key is always first. + return message.indexOf('{"gameCommand"') === 0; } - // User command callbacks - // Remember: control commands are coordinator relevant commands + // ---------- Command callbacks (received from client) ---------- User.prototype.onJoin = function(options) { @@ -83,14 +142,12 @@ function (Parent, ProtocolHelper, nc) { var userOptions = { id: this.id, nickname: options.nickname - } + }; this.options = userOptions; this.channelPipe.addUser(this); }; - /* FIXME: watch out and check in wich direction game and control commands flow */ User.prototype.onGameCommand = function(options) { - // repacking for transport via pipe var message = ProtocolHelper.encodeCommand("gameCommand", options); this.channelPipe.sendToUser(this.id, message); }; @@ -100,6 +157,31 @@ function (Parent, ProtocolHelper, nc) { nc.trigger(nc.ns.server.events.controlCommand.user + this.id, message); }; - return User; + // WebRTC signaling messages from the client (arriving over Socket.IO). -}); \ No newline at end of file + User.prototype.onWebrtcOffer = function (options) { + if (!this.webrtc || !options) return; + this.webrtc.handleRemoteDescription(options.sdp, options.type || "offer"); + }; + + User.prototype.onWebrtcIce = function (options) { + if (!this.webrtc || !options) return; + this.webrtc.handleRemoteCandidate(options.candidate, options.mid); + }; + + // Periodic stats report from client. + User.prototype.onStats = function (options) { + if (!options) return; + StatsLogger.log({ + type: "stats", + session: this.id, + transport: options.transport || "socketio", + rttSamples: options.rttSamples || [], + recvRate: options.recvRate || 0, + sendRate: options.sendRate || 0, + gaps: options.gaps || 0 + }); + }; + + return User; +}); diff --git a/app/Server/WebRTCTransport.js b/app/Server/WebRTCTransport.js new file mode 100644 index 0000000..1384abb --- /dev/null +++ b/app/Server/WebRTCTransport.js @@ -0,0 +1,112 @@ +define([ + "node-datachannel" +], + +function (nodeDataChannel) { + + "use strict"; + + // Per-user WebRTC peer + unreliable data channel. + // The User class relays SDP/ICE between this transport and the client + // (over Socket.IO). This class only deals with the WebRTC peer state. + function WebRTCTransport(userId, callbacks) { + this.userId = userId; + this.callbacks = callbacks || {}; + this.unreliable = null; + this.ready = false; + this.createdAt = Date.now(); + + var iceServers = ["stun:stun.l.google.com:19302"]; + this.pc = new nodeDataChannel.PeerConnection("peer-" + userId, { + iceServers: iceServers + }); + + var self = this; + + this.pc.onLocalDescription(function (sdp, type) { + if (self.callbacks.onLocalDescription) { + self.callbacks.onLocalDescription({ sdp: sdp, type: type }); + } + }); + + this.pc.onLocalCandidate(function (candidate, mid) { + if (self.callbacks.onLocalCandidate) { + self.callbacks.onLocalCandidate({ candidate: candidate, mid: mid }); + } + }); + + this.pc.onDataChannel(function (dc) { + var label = dc.getLabel(); + if (label !== "unreliable") return; + self.unreliable = dc; + dc.onOpen(function () { + self.ready = true; + if (self.callbacks.onReady) { + self.callbacks.onReady(Date.now() - self.createdAt); + } + }); + dc.onMessage(function (msg) { + if (self.callbacks.onMessage) self.callbacks.onMessage(msg); + }); + dc.onClosed(function () { + self.ready = false; + if (self.callbacks.onClosed) self.callbacks.onClosed(); + }); + }); + } + + WebRTCTransport.prototype.handleRemoteDescription = function (sdp, type) { + try { + console.log("[webrtc] " + this.userId + " setRemoteDescription " + type); + this.pc.setRemoteDescription(sdp, type); + this.haveRemoteDesc = true; + // Flush any candidates that arrived before the offer. + if (this.pendingCandidates && this.pendingCandidates.length) { + var pending = this.pendingCandidates; + this.pendingCandidates = []; + for (var i = 0; i < pending.length; i++) { + this.handleRemoteCandidate(pending[i].candidate, pending[i].mid); + } + } + } catch (e) { + console.error("[webrtc] setRemoteDescription failed:", e && (e.message || e)); + } + }; + + WebRTCTransport.prototype.handleRemoteCandidate = function (candidate, mid) { + // Buffer until we have the remote description — adding candidates before + // setRemoteDescription throws. + if (!this.haveRemoteDesc) { + this.pendingCandidates = this.pendingCandidates || []; + this.pendingCandidates.push({ candidate: candidate, mid: mid }); + return; + } + try { + this.pc.addRemoteCandidate(candidate, mid || "0"); + } catch (e) { + console.error("[webrtc] addRemoteCandidate failed:", e && (e.message || e)); + } + }; + + WebRTCTransport.prototype.isReady = function () { + return this.ready && this.unreliable && this.unreliable.isOpen(); + }; + + WebRTCTransport.prototype.send = function (message) { + if (!this.isReady()) return false; + try { + this.unreliable.sendMessage(message); + return true; + } catch (e) { + return false; + } + }; + + WebRTCTransport.prototype.destroy = function () { + try { if (this.unreliable) this.unreliable.close(); } catch (_) {} + try { this.pc.close(); } catch (_) {} + this.ready = false; + }; + + return WebRTCTransport; +}); diff --git a/package-lock.json b/package-lock.json index 3f2a506..a8e69b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,12 +10,14 @@ "dependencies": { "chart.js": "^4.4.0", "express": "^4.18.2", + "node-datachannel": "^0.10.0", "requirejs": "^2.3.6", "screenfull": "^6.0.2", "socket.io": "^4.7.4" }, "devDependencies": { - "nodemon": "^3.0.2" + "nodemon": "^3.0.2", + "playwright": "^1.59.1" }, "engines": { "node": ">=16.0.0" @@ -83,6 +85,26 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/base64id": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", @@ -103,6 +125,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, "node_modules/body-parser": { "version": "1.20.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", @@ -148,6 +181,30 @@ "node": ">=8" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -218,6 +275,12 @@ "fsevents": "~2.3.2" } }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -276,6 +339,30 @@ "ms": "2.0.0" } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -293,6 +380,15 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -319,6 +415,15 @@ "node": ">= 0.8" } }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/engine.io": { "version": "6.6.4", "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", @@ -415,6 +520,15 @@ "node": ">= 0.6" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, "node_modules/express": { "version": "4.21.2", "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", @@ -505,6 +619,12 @@ "node": ">= 0.6" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -562,6 +682,12 @@ "node": ">= 0.4" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, "node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", @@ -642,6 +768,26 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore-by-default": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", @@ -653,6 +799,12 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -765,6 +917,18 @@ "node": ">= 0.6" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -777,11 +941,32 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -790,6 +975,52 @@ "node": ">= 0.6" } }, + "node_modules/node-abi": { + "version": "3.92.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", + "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-datachannel": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/node-datachannel/-/node-datachannel-0.10.1.tgz", + "integrity": "sha512-rhxb1iQgbFLY6HMt3W6Xcs8Q1k4jIMgI7KduXcYvIn2UMKYK6e/eegya2caF/+XYAqTeo1743gOr11CXvJ/DJA==", + "hasInstallScript": true, + "license": "MPL 2.0", + "dependencies": { + "node-domexception": "^2.0.1", + "prebuild-install": "^7.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/node-domexception": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-2.0.2.tgz", + "integrity": "sha512-Qf9vHK9c5MGgUXj8SnucCIS4oEPuUstjRaMplLGeZpbWMfNV1rvEcXuwoXfN51dUfD1b4muPHPQtCx/5Dj/QAA==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=16" + } + }, "node_modules/nodemon": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", @@ -880,6 +1111,15 @@ "node": ">= 0.8" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -905,6 +1145,80 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/playwright": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", + "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", + "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -923,6 +1237,16 @@ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", "dev": true }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/qs": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", @@ -959,6 +1283,35 @@ "node": ">= 0.8" } }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -1022,7 +1375,6 @@ "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, "bin": { "semver": "bin/semver.js" }, @@ -1153,6 +1505,51 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -1274,6 +1671,24 @@ "node": ">= 0.8" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -1286,6 +1701,34 @@ "node": ">=4" } }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -1315,6 +1758,18 @@ "nodetouch": "bin/nodetouch.js" } }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -1346,6 +1801,12 @@ "node": ">= 0.8" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -1362,6 +1823,12 @@ "node": ">= 0.8" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, "node_modules/ws": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", @@ -1440,6 +1907,11 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, "base64id": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", @@ -1451,6 +1923,16 @@ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "dev": true }, + "bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, "body-parser": { "version": "1.20.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", @@ -1489,6 +1971,15 @@ "fill-range": "^7.1.1" } }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -1536,6 +2027,11 @@ "readdirp": "~3.6.0" } }, + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1582,6 +2078,19 @@ "ms": "2.0.0" } }, + "decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "requires": { + "mimic-response": "^3.1.0" + } + }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" + }, "depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -1592,6 +2101,11 @@ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" }, + "detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==" + }, "dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -1612,6 +2126,14 @@ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" }, + "end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "requires": { + "once": "^1.4.0" + } + }, "engine.io": { "version": "6.6.4", "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", @@ -1681,6 +2203,11 @@ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" }, + "expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==" + }, "express": { "version": "4.21.2", "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", @@ -1752,6 +2279,11 @@ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, "fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1790,6 +2322,11 @@ "es-object-atoms": "^1.0.0" } }, + "github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" + }, "glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", @@ -1843,6 +2380,11 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, "ignore-by-default": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", @@ -1854,6 +2396,11 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, "ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -1927,6 +2474,11 @@ "mime-db": "1.52.0" } }, + "mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==" + }, "minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -1936,16 +2488,53 @@ "brace-expansion": "^1.1.7" } }, + "minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" + }, + "mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, + "napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==" + }, "negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" }, + "node-abi": { + "version": "3.92.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", + "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", + "requires": { + "semver": "^7.3.5" + } + }, + "node-datachannel": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/node-datachannel/-/node-datachannel-0.10.1.tgz", + "integrity": "sha512-rhxb1iQgbFLY6HMt3W6Xcs8Q1k4jIMgI7KduXcYvIn2UMKYK6e/eegya2caF/+XYAqTeo1743gOr11CXvJ/DJA==", + "requires": { + "node-domexception": "^2.0.1", + "prebuild-install": "^7.0.1" + } + }, + "node-domexception": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-2.0.2.tgz", + "integrity": "sha512-Qf9vHK9c5MGgUXj8SnucCIS4oEPuUstjRaMplLGeZpbWMfNV1rvEcXuwoXfN51dUfD1b4muPHPQtCx/5Dj/QAA==" + }, "nodemon": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", @@ -2005,6 +2594,14 @@ "ee-first": "1.1.1" } }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "requires": { + "wrappy": "1" + } + }, "parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -2021,6 +2618,50 @@ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true }, + "playwright": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", + "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", + "dev": true, + "requires": { + "fsevents": "2.3.2", + "playwright-core": "1.59.1" + }, + "dependencies": { + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + } + } + }, + "playwright-core": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", + "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", + "dev": true + }, + "prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "requires": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + } + }, "proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -2036,6 +2677,15 @@ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", "dev": true }, + "pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "qs": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", @@ -2060,6 +2710,27 @@ "unpipe": "1.0.0" } }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + } + }, + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, "readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -2092,8 +2763,7 @@ "semver": { "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==" }, "send": { "version": "0.19.0", @@ -2187,6 +2857,21 @@ "side-channel-map": "^1.0.1" } }, + "simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==" + }, + "simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "requires": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -2278,6 +2963,19 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==" + }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -2287,6 +2985,29 @@ "has-flag": "^3.0.0" } }, + "tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "requires": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "requires": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + } + }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -2307,6 +3028,14 @@ "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", "dev": true }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, "type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -2332,6 +3061,11 @@ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, "utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -2342,6 +3076,11 @@ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, "ws": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", diff --git a/package.json b/package.json index 6fbcbb3..6be7978 100755 --- a/package.json +++ b/package.json @@ -17,16 +17,17 @@ }, "main": "server.js", "dependencies": { - "socket.io": "^4.7.4", + "chart.js": "^4.4.0", "express": "^4.18.2", + "node-datachannel": "^0.10.0", "requirejs": "^2.3.6", "screenfull": "^6.0.2", - "chart.js": "^4.4.0" + "socket.io": "^4.7.4" }, "devDependencies": { - "nodemon": "^3.0.2" + "nodemon": "^3.0.2", + "playwright": "^1.59.1" }, - "optionalDependencies": {}, "engines": { "node": ">=16.0.0" }, diff --git a/scripts/webrtc-browser-test.js b/scripts/webrtc-browser-test.js new file mode 100644 index 0000000..620289a --- /dev/null +++ b/scripts/webrtc-browser-test.js @@ -0,0 +1,185 @@ +// Headless-Chromium smoke test of the integrated WebRTC transport. +// +// Spawns the chuck.js server, creates a channel via the API, launches Chromium, +// loads the game, watches the DOM "transport" indicator and the runs/*.jsonl +// log to confirm the unreliable DataChannel actually opened end-to-end. + +"use strict"; + +const { chromium } = require("playwright"); +const { spawn } = require("child_process"); +const http = require("http"); +const fs = require("fs"); +const path = require("path"); + +const SERVER_URL = "http://localhost:1234"; +const CHANNEL = "browsersmoke"; + +function apiCall(body) { + return new Promise((resolve, reject) => { + const data = JSON.stringify(body); + const req = http.request({ + host: "localhost", port: 1234, path: "/api", method: "POST", + headers: { "Content-Type": "application/json", "Content-Length": data.length } + }, (res) => { + let chunks = ""; + res.on("data", (c) => (chunks += c)); + res.on("end", () => resolve(JSON.parse(chunks))); + }); + req.on("error", reject); + req.write(data); req.end(); + }); +} + +async function waitForServerReady() { + for (let i = 0; i < 30; i++) { + try { + const r = await apiCall({ command: "getMaps" }); + if (r.success) return; + } catch (_) {} + await new Promise((r) => setTimeout(r, 200)); + } + throw new Error("server not ready"); +} + +function fileLog(pid, label, line) { + console.log("[" + label + ":" + pid + "] " + line.trim()); +} + +async function main() { + const srvLogPath = "/tmp/chuck-srv.log"; + fs.writeFileSync(srvLogPath, ""); + const srv = spawn("node", ["server.js"], { + cwd: process.cwd(), + stdio: ["ignore", fs.openSync(srvLogPath, "a"), fs.openSync(srvLogPath, "a")], + env: process.env + }); + console.log("[test] server pid=" + srv.pid); + + try { + await waitForServerReady(); + console.log("[test] server up"); + + const r = await apiCall({ + command: "createChannel", + options: { channelName: CHANNEL, levelUids: ["debug"], maxUsers: 4, minUsers: 1, scoreLimit: 5 } + }); + if (!r.success) throw new Error("createChannel failed: " + JSON.stringify(r)); + console.log("[test] channel created"); + + // Find the run file the server just opened. + await new Promise((r) => setTimeout(r, 500)); + const runFiles = fs.readdirSync("runs").filter((f) => f.endsWith(".jsonl")) + .map((f) => ({ f, t: fs.statSync(path.join("runs", f)).mtimeMs })) + .sort((a, b) => b.t - a.t); + const runFile = path.join("runs", runFiles[0].f); + console.log("[test] run file: " + runFile); + + const browser = await chromium.launch({ + executablePath: "/usr/sbin/chromium", + headless: true, + args: ["--no-sandbox", "--disable-setuid-sandbox", "--use-fake-ui-for-media-stream"] + }); + const ctx = await browser.newContext(); + const page = await ctx.newPage(); + page.on("console", (msg) => console.log("[browser]", msg.type(), msg.text())); + page.on("pageerror", (err) => console.error("[browser ERR]", err.message)); + + const url = SERVER_URL + "/#" + CHANNEL; + console.log("[test] navigating to " + url); + await page.goto(url, { waitUntil: "load" }); + + // Diagnostics: dump form visibility states and the latest /api response. + await page.waitForTimeout(2000); + const diag = await page.evaluate(() => ({ + joinDisplay: getComputedStyle(document.getElementById("customjoinform")).display, + createDisplay: getComputedStyle(document.getElementById("createform")).display, + listDisplay: getComputedStyle(document.getElementById("listform")).display, + hash: window.location.hash, + customname: document.getElementById("customname").value + })); + console.log("[test] form states:", JSON.stringify(diag)); + + // If the hashchange didn't auto-show the form, force the showCustomJoinForm path. + if (diag.joinDisplay !== "block") { + console.log("[test] forcing showCustomJoinForm"); + await page.evaluate(() => { + document.getElementById("customname").value = location.hash.substr(1); + document.getElementById("customjoinform").style.display = "block"; + }); + } + await page.waitForSelector("#customjoinform", { state: "visible", timeout: 8000 }); + console.log("[test] customjoinform visible"); + + // Fill in nickname and submit the form. + await page.evaluate(() => { + document.getElementById("nick").value = "browser-bot"; + const form = document.getElementById("customjoinform"); + // The form's onsubmit returns false to prevent navigation. Calling + // onsubmit() directly invokes the join handler. + form.onsubmit({ preventDefault: () => {} }); + }); + console.log("[test] join submitted"); + + // Wait up to 12s for the transport indicator to switch to "webrtc", + // or for the JSONL to show a webrtc_ready event. + const deadline = Date.now() + 12000; + let webrtcReady = false; + let transportText = ""; + while (Date.now() < deadline) { + await page.waitForTimeout(500); + transportText = await page.evaluate(() => { + const labels = Array.from(document.querySelectorAll("label")); + const t = labels.find((l) => /^transport:/.test(l.textContent || "")); + return t ? t.textContent : ""; + }).catch(() => ""); + const log = fs.readFileSync(runFile, "utf8"); + if (/webrtc_ready/.test(log) || /transport:webrtc/.test(transportText)) { + webrtcReady = true; + break; + } + } + + console.log("[test] transport indicator: '" + transportText + "'"); + console.log("[test] webrtc_ready event seen: " + (webrtcReady ? "YES" : "NO")); + + // Wait a bit more to collect stats events and verify gameCommand + // traffic is actually flowing. + await page.waitForTimeout(3000); + + const events = fs.readFileSync(runFile, "utf8") + .split("\n").filter(Boolean).map((l) => { try { return JSON.parse(l); } catch (_) { return null; } }) + .filter(Boolean); + const stats = events.filter((e) => e.type === "stats"); + const lastStat = stats[stats.length - 1]; + console.log("[test] stats events received: " + stats.length); + if (lastStat) { + console.log("[test] last stats: transport=" + lastStat.transport + + " recvRate=" + lastStat.recvRate + + " sendRate=" + lastStat.sendRate); + } + + const webrtcInUse = lastStat && lastStat.transport === "webrtc"; + const traffic = lastStat && lastStat.recvRate > 0; + + const logTail = fs.readFileSync(runFile, "utf8").split("\n").filter(Boolean).slice(-10); + console.log("[test] last JSONL events:"); + for (const l of logTail) console.log(" " + l); + + await browser.close(); + + const pass = webrtcReady && webrtcInUse && traffic; + console.log("\nRESULT: " + (pass ? "PASS" : "FAIL") + + " (webrtc_ready=" + webrtcReady + " webrtcInUse=" + !!webrtcInUse + + " trafficFlowing=" + !!traffic + ")"); + process.exitCode = pass ? 0 : 1; + } finally { + srv.kill("SIGINT"); + await new Promise((r) => setTimeout(r, 500)); + srv.kill("SIGKILL"); + const tail = fs.readFileSync(srvLogPath, "utf8").split("\n").slice(-30).join("\n"); + console.log("\n--- server log tail ---\n" + tail); + } +} + +main().catch((e) => { console.error("[test] fatal:", e); process.exit(2); });