From 61fc67c49041384896c02d213c0a2f9e6c970320 Mon Sep 17 00:00:00 2001 From: Robbie Date: Wed, 16 Jul 2025 09:07:53 -0700 Subject: [PATCH 1/4] feat: migrate WebRTC signaling from WebSocket to Ably pub/sub MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace custom WebSocket SignallingClient with Ably Realtime implementation - Remove heartbeat logic as Ably handles connection health automatically - Update ClientConfigResponse to use ablyToken and ablyChannel - Hardcode Ably channel rewind parameter to 100 messages - Defer connection initialization to connect() method for better resource management - Add proper error handling for uninitialized connections - Remove message buffering in favor of explicit connection errors - Add Ably SDK v2.9.0 dependency 🤖 Generated with Claude Code Co-Authored-By: Claude --- package-lock.json | 369 +++++++++++++++++- package.json | 1 + src/AnamClient.ts | 20 +- src/modules/SignallingClient.ts | 237 +++++------ src/types/coreApi/StartSessionResponse.ts | 4 +- .../signalling/SignallingClientOptions.ts | 12 +- src/types/signalling/index.ts | 5 +- 7 files changed, 478 insertions(+), 170 deletions(-) diff --git a/package-lock.json b/package-lock.json index ca86eef..ecde45f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0-automated", "license": "MIT", "dependencies": { + "ably": "^2.9.0", "buffer": "^6.0.3" }, "devDependencies": { @@ -35,6 +36,14 @@ "webpack-cli": "^5.1.4" } }, + "node_modules/@ably/msgpack-js": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@ably/msgpack-js/-/msgpack-js-0.4.0.tgz", + "integrity": "sha512-IPt/BoiQwCWubqoNik1aw/6M/DleMdrxJOUpSja6xmMRbT2p1TA8oqKWgfZabqzrq8emRNeSl/+4XABPNnW5pQ==", + "dependencies": { + "bops": "^1.0.1" + } + }, "node_modules/@babel/code-frame": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", @@ -758,6 +767,39 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, "node_modules/@types/conventional-commits-parser": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/conventional-commits-parser/-/conventional-commits-parser-5.0.0.tgz", @@ -793,21 +835,41 @@ "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "dev": true }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/node": { "version": "20.12.12", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz", "integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==", - "dev": true, "dependencies": { "undici-types": "~5.26.4" } }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "7.9.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.9.0.tgz", @@ -1231,6 +1293,34 @@ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "dev": true }, + "node_modules/ably": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/ably/-/ably-2.10.0.tgz", + "integrity": "sha512-kgjmHAW4ayvruuuxPJ3Hw6gzZNA4Qsd2qc6fmi0Pe6tvJMHrI3iB2eo+9x4GqkA2ruLvxqCPL1no0Qymup6qTg==", + "dependencies": { + "@ably/msgpack-js": "^0.4.0", + "dequal": "^2.0.3", + "fastestsmallesttextencoderdecoder": "^1.0.22", + "got": "^11.8.5", + "ulid": "^2.3.0", + "ws": "^8.17.1" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/acorn": { "version": "8.11.3", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", @@ -1442,6 +1532,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bops": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bops/-/bops-1.0.1.tgz", + "integrity": "sha512-qCMBuZKP36tELrrgXpAfM+gHzqa0nLsWZ+L37ncsb8txYlnAoxOPpVp+g7fK0sGkMXfA0wl8uQkESqw3v4HNag==", + "dependencies": { + "base64-js": "1.0.2", + "to-utf8": "0.0.1" + } + }, + "node_modules/bops/node_modules/base64-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.0.2.tgz", + "integrity": "sha512-ZXBDPMt/v/8fsIqn+Z5VwrhdR6jVka0bYobHdGia0Nxi7BJ9i/Uvml3AocHIBtIIBhZjBw5MR0aR4ROs/8+SNg==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -1525,6 +1632,45 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cacheable-request/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -1787,6 +1933,17 @@ "node": ">=6" } }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2030,12 +2187,45 @@ } } }, + "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==", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/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==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "engines": { + "node": ">=10" + } + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -2070,6 +2260,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "engines": { + "node": ">=6" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -2138,6 +2336,14 @@ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true }, + "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==", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.16.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.16.1.tgz", @@ -2697,6 +2903,11 @@ "node": ">= 4.9.1" } }, + "node_modules/fastestsmallesttextencoderdecoder": { + "version": "1.0.22", + "resolved": "https://registry.npmjs.org/fastestsmallesttextencoderdecoder/-/fastestsmallesttextencoderdecoder-1.0.22.tgz", + "integrity": "sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw==" + }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", @@ -3130,6 +3341,30 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -3235,6 +3470,23 @@ "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", "dev": true }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==" + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, "node_modules/human-signals": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", @@ -3934,8 +4186,7 @@ "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" }, "node_modules/json-parse-better-errors": { "version": "1.0.2", @@ -3990,7 +4241,6 @@ "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, "dependencies": { "json-buffer": "3.0.1" } @@ -4116,6 +4366,14 @@ "integrity": "sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==", "dev": true }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "engines": { + "node": ">=8" + } + }, "node_modules/lru-cache": { "version": "10.2.2", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", @@ -4216,6 +4474,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -4358,6 +4624,17 @@ "node": ">=0.10.0" } }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/npm-run-all": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", @@ -4572,7 +4849,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "dependencies": { "wrappy": "1" } @@ -4626,6 +4902,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "engines": { + "node": ">=8" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -5045,6 +5329,15 @@ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", "dev": true }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -5074,6 +5367,17 @@ } ] }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -5210,6 +5514,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==" + }, "node_modules/resolve-cwd": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", @@ -5240,6 +5549,17 @@ "node": ">=4" } }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -6048,6 +6368,11 @@ "node": ">=8.0" } }, + "node_modules/to-utf8": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/to-utf8/-/to-utf8-0.0.1.tgz", + "integrity": "sha512-zks18/TWT1iHO3v0vFp5qLKOG27m67ycq/Y7a7cTiRuUNlc4gf3HGnkRgMv0NyhnfTamtkYBJl+YeD1/j07gBQ==" + }, "node_modules/touch": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", @@ -6244,6 +6569,14 @@ } } }, + "node_modules/ulid": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ulid/-/ulid-2.4.0.tgz", + "integrity": "sha512-fIRiVTJNcSRmXKPZtGzFQv9WRrZ3M9eoptl/teFJvjOzmpU+/K/JH6HZ8deBfb5vMEpicJcLn7JmvdknlMq7Zg==", + "bin": { + "ulid": "bin/cli.js" + } + }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -6271,8 +6604,7 @@ "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, "node_modules/unicorn-magic": { "version": "0.1.0", @@ -6681,8 +7013,27 @@ "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==", - "dev": true + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } }, "node_modules/y18n": { "version": "5.0.8", diff --git a/package.json b/package.json index 0966592..5131377 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "webpack-cli": "^5.1.4" }, "dependencies": { + "ably": "^2.9.0", "buffer": "^6.0.3" } } diff --git a/src/AnamClient.ts b/src/AnamClient.ts index 50bc50c..98e7e98 100644 --- a/src/AnamClient.ts +++ b/src/AnamClient.ts @@ -182,15 +182,8 @@ export default class AnamClient { config, sessionOptions, ); - const { - sessionId, - clientConfig, - engineHost, - engineProtocol, - signallingEndpoint, - } = response; - const { heartbeatIntervalSeconds, maxWsReconnectionAttempts, iceServers } = - clientConfig; + const { sessionId, clientConfig, engineHost, engineProtocol } = response; + const { ablyToken, ablyChannel, iceServers } = clientConfig; this.sessionId = sessionId; setMetricsContext({ @@ -205,13 +198,8 @@ export default class AnamClient { baseUrl: `${engineProtocol}://${engineHost}`, }, signalling: { - heartbeatIntervalSeconds, - maxWsReconnectionAttempts, - url: { - baseUrl: engineHost, - protocol: engineProtocol, - signallingPath: signallingEndpoint, - }, + ablyToken, + channelName: ablyChannel, }, iceServers, inputAudio: { diff --git a/src/modules/SignallingClient.ts b/src/modules/SignallingClient.ts index a663308..b2b4982 100644 --- a/src/modules/SignallingClient.ts +++ b/src/modules/SignallingClient.ts @@ -1,3 +1,4 @@ +import * as Ably from 'ably'; import { InternalEventEmitter, PublicEventEmitter } from '.'; import { AnamEvent, @@ -9,21 +10,16 @@ import { } from '../types'; import { TalkMessageStreamPayload } from '../types/signalling/TalkMessageStreamPayload'; -const DEFAULT_HEARTBEAT_INTERVAL_SECONDS = 5; -const DEFAULT_WS_RECONNECTION_ATTEMPTS = 5; - export class SignallingClient { private publicEventEmitter: PublicEventEmitter; private internalEventEmitter: InternalEventEmitter; - private url: URL; private sessionId: string; - private heartbeatIntervalSeconds: number; - private maxWsReconnectionAttempts: number; + private ablyToken: string; + private channelName: string; + private realtime: Ably.Realtime | null = null; + private channel: Ably.RealtimeChannel | null = null; private stopSignal = false; - private sendingBuffer: SignalMessage[] = []; - private wsConnectionAttempts = 0; - private socket: WebSocket | null = null; - private heartBeatIntervalRef: ReturnType | null = null; + private isConnected = false; constructor( sessionId: string, @@ -39,46 +35,69 @@ export class SignallingClient { } this.sessionId = sessionId; - const { heartbeatIntervalSeconds, maxWsReconnectionAttempts, url } = - options; - - this.heartbeatIntervalSeconds = - heartbeatIntervalSeconds || DEFAULT_HEARTBEAT_INTERVAL_SECONDS; + const { ablyToken, channelName } = options; - this.maxWsReconnectionAttempts = - maxWsReconnectionAttempts || DEFAULT_WS_RECONNECTION_ATTEMPTS; - - if (!url.baseUrl) { - throw new Error('Signalling Client: baseUrl is required'); + if (!ablyToken) { + throw new Error('Signalling Client: ablyToken is required'); } - const httpProtocol = url.protocol || 'https'; - const initUrl = `${httpProtocol}://${url.baseUrl}`; - this.url = new URL(initUrl); - this.url.protocol = url.protocol === 'http' ? 'ws:' : 'wss:'; - if (url.port) { - this.url.port = url.port; + if (!channelName) { + throw new Error('Signalling Client: channelName is required'); } - this.url.pathname = url.signallingPath ?? '/ws'; - this.url.searchParams.append('session_id', sessionId); + + // Store configuration for later use in connect() + this.ablyToken = ablyToken; + this.channelName = channelName; } public stop() { this.stopSignal = true; - this.closeSocket(); + this.closeConnection(); } - public connect(): WebSocket { - this.socket = new WebSocket(this.url.href); - this.socket.onopen = this.onOpen.bind(this); - this.socket.onclose = this.onClose.bind(this); - this.socket.onerror = this.onError.bind(this); - return this.socket; + public connect(): void { + // Check if already connected + if (this.realtime) { + console.warn('SignallingClient - connect: Already connected'); + return; + } + + // Initialize Ably Realtime client + this.realtime = new Ably.Realtime(this.ablyToken); + + // Get the channel with hardcoded rewind parameter of 100 + this.channel = this.realtime.channels.get(this.channelName, { + params: { rewind: '100' }, + }); + + // Set up connection state listeners + this.realtime.connection.on('connected', () => { + this.onConnected(); + }); + + this.realtime.connection.on('disconnected', () => { + this.onDisconnected(); + }); + + this.realtime.connection.on('failed', () => { + this.onConnectionFailed(); + }); + + // Subscribe to channel messages + this.channel.subscribe((message) => { + this.onMessage(message); + }); } public async sendOffer(localDescription: RTCSessionDescription) { + if (!this.channel) { + throw new Error( + 'SignallingClient - sendOffer: Not connected. Call connect() first.', + ); + } + const offerMessagePayload = { connectionDescription: localDescription, - userUid: this.sessionId, // TODO: this should be renamed to session Id on the server + userUid: this.sessionId, }; const offerMessage: SignalMessage = { actionType: SignalMessageAction.OFFER, @@ -89,6 +108,12 @@ export class SignallingClient { } public async sendIceCandidate(candidate: RTCIceCandidate) { + if (!this.channel) { + throw new Error( + 'SignallingClient - sendIceCandidate: Not connected. Call connect() first.', + ); + } + const iceCandidateMessage: SignalMessage = { actionType: SignalMessageAction.ICE_CANDIDATE, sessionId: this.sessionId, @@ -98,21 +123,36 @@ export class SignallingClient { } private sendSignalMessage(message: SignalMessage) { - if (this.socket?.readyState === WebSocket.OPEN) { - try { - this.socket.send(JSON.stringify(message)); - } catch (error) { - console.error( - 'SignallingClient - sendSignalMessage: error sending message', - error, - ); - } - } else { - this.sendingBuffer.push(message); + if (!this.channel) { + throw new Error( + 'SignallingClient - sendSignalMessage: Cannot send message, not connected. Call connect() first.', + ); + } + + if (!this.isConnected) { + throw new Error( + 'SignallingClient - sendSignalMessage: Cannot send message, connection not established yet.', + ); + } + + try { + this.channel.publish('signal', message); + } catch (error) { + console.error( + 'SignallingClient - sendSignalMessage: error sending message', + error, + ); + throw error; } } public async sendTalkMessage(payload: TalkMessageStreamPayload) { + if (!this.channel) { + throw new Error( + 'SignallingClient - sendTalkMessage: Not connected. Call connect() first.', + ); + } + const chatMessage: SignalMessage = { actionType: SignalMessageAction.TALK_STREAM_INPUT, sessionId: this.sessionId, @@ -121,29 +161,21 @@ export class SignallingClient { this.sendSignalMessage(chatMessage); } - private closeSocket() { - if (this.socket) { - this.socket.close(); - this.socket = null; - } - if (this.heartBeatIntervalRef) { - clearInterval(this.heartBeatIntervalRef); - this.heartBeatIntervalRef = null; + private closeConnection() { + if (this.realtime) { + this.realtime.close(); + this.realtime = null; + this.channel = null; + this.isConnected = false; } } - private async onOpen(): Promise { - if (!this.socket) { - throw new Error('SignallingClient - onOpen: socket is null'); - } + private onConnected(): void { try { - this.wsConnectionAttempts = 0; - this.flushSendingBuffer(); - this.socket.onmessage = this.onMessage.bind(this); - this.startSendingHeartBeats(); + this.isConnected = true; this.internalEventEmitter.emit(InternalEvent.WEB_SOCKET_OPEN); } catch (e) { - console.error('SignallingClient - onOpen: error in onOpen', e); + console.error('SignallingClient - onConnected: error', e); this.publicEventEmitter.emit( AnamEvent.CONNECTION_CLOSED, ConnectionClosedCode.SIGNALLING_CLIENT_CONNECTION_FAILURE, @@ -151,83 +183,30 @@ export class SignallingClient { } } - private async onClose() { - this.wsConnectionAttempts += 1; + private onDisconnected() { + this.isConnected = false; if (this.stopSignal) { return; } - if (this.wsConnectionAttempts <= this.maxWsReconnectionAttempts) { - this.socket = null; - setTimeout(() => { - this.connect(); - }, 100 * this.wsConnectionAttempts); - } else { - if (this.heartBeatIntervalRef) { - clearInterval(this.heartBeatIntervalRef); - this.heartBeatIntervalRef = null; - } - this.publicEventEmitter.emit( - AnamEvent.CONNECTION_CLOSED, - ConnectionClosedCode.SIGNALLING_CLIENT_CONNECTION_FAILURE, - ); - } + // Ably handles reconnection automatically } - private onError(event: Event) { + private onConnectionFailed() { if (this.stopSignal) { return; } - console.error('SignallingClient - onError: ', event); - } - - private flushSendingBuffer() { - const newBuffer: SignalMessage[] = []; - if (this.sendingBuffer.length > 0) { - this.sendingBuffer.forEach((message: SignalMessage) => { - if (this.socket?.readyState === WebSocket.OPEN) { - this.socket.send(JSON.stringify(message)); - } else { - newBuffer.push(message); - } - }); - } - this.sendingBuffer = newBuffer; + this.publicEventEmitter.emit( + AnamEvent.CONNECTION_CLOSED, + ConnectionClosedCode.SIGNALLING_CLIENT_CONNECTION_FAILURE, + ); } - private async onMessage(event: MessageEvent) { - const message: SignalMessage = JSON.parse(event.data); + private onMessage(message: Ably.Message) { + // Extract the SignalMessage from Ably message data + const signalMessage: SignalMessage = message.data; this.internalEventEmitter.emit( InternalEvent.SIGNAL_MESSAGE_RECEIVED, - message, + signalMessage, ); } - - private startSendingHeartBeats() { - if (!this.socket) { - throw new Error( - 'SignallingClient - startSendingHeartBeats: socket is null', - ); - } - if (this.heartBeatIntervalRef) { - console.warn( - 'SignallingClient - startSendingHeartBeats: heartbeat interval already set', - ); - } - // send a heartbeat message every heartbeatIntervalSeconds - const heartbeatInterval = this.heartbeatIntervalSeconds * 1000; - const heartbeatMessage: SignalMessage = { - actionType: SignalMessageAction.HEARTBEAT, - sessionId: this.sessionId, - payload: '', - }; - const heartbeatMessageJson = JSON.stringify(heartbeatMessage); - this.heartBeatIntervalRef = setInterval(() => { - if (this.stopSignal) { - return; - } - if (this.socket?.readyState === WebSocket.OPEN) { - this.socket.send(heartbeatMessageJson); - } - }, heartbeatInterval); - } } diff --git a/src/types/coreApi/StartSessionResponse.ts b/src/types/coreApi/StartSessionResponse.ts index d95a308..ec382a4 100644 --- a/src/types/coreApi/StartSessionResponse.ts +++ b/src/types/coreApi/StartSessionResponse.ts @@ -7,7 +7,7 @@ export interface StartSessionResponse { } export interface ClientConfigResponse { - heartbeatIntervalSeconds: number; - maxWsReconnectionAttempts: number; + ablyToken: string; + ablyChannel: string; iceServers: RTCIceServer[]; } diff --git a/src/types/signalling/SignallingClientOptions.ts b/src/types/signalling/SignallingClientOptions.ts index a49608e..4a57139 100644 --- a/src/types/signalling/SignallingClientOptions.ts +++ b/src/types/signalling/SignallingClientOptions.ts @@ -1,12 +1,4 @@ -export interface SignallingURLOptions { - baseUrl: string; - protocol?: string; - port?: string; - signallingPath?: string; -} - export interface SignallingClientOptions { - heartbeatIntervalSeconds?: number; - maxWsReconnectionAttempts?: number; - url: SignallingURLOptions; + ablyToken: string; + channelName: string; } diff --git a/src/types/signalling/index.ts b/src/types/signalling/index.ts index 16b5f95..4258b60 100644 --- a/src/types/signalling/index.ts +++ b/src/types/signalling/index.ts @@ -1,6 +1,3 @@ -export type { - SignallingClientOptions, - SignallingURLOptions, -} from './SignallingClientOptions'; +export type { SignallingClientOptions } from './SignallingClientOptions'; export type { SignalMessage } from './SignalMessage'; export { SignalMessageAction } from './SignalMessage'; From bfba9528f8088ac224a4d9ca508ff215be7664d8 Mon Sep 17 00:00:00 2001 From: Robbie Date: Wed, 16 Jul 2025 09:16:49 -0700 Subject: [PATCH 2/4] fix: constant int to bool --- src/lib/constants.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 099be98..c6ecb3e 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -9,4 +9,5 @@ export const DEFAULT_API_VERSION = '/v1'; // include the leading slash export const CLIENT_METADATA = { client: 'js-sdk', version: '0.0.0-automated', + supportsPubSubSignalling: true, }; From 7a1a1691dd9e98357099b9fed47ef410755bcb34 Mon Sep 17 00:00:00 2001 From: Robbie Date: Wed, 16 Jul 2025 09:22:57 -0700 Subject: [PATCH 3/4] fix: client metrics update --- src/lib/ClientMetrics.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/ClientMetrics.ts b/src/lib/ClientMetrics.ts index 2c9b71e..5647fab 100644 --- a/src/lib/ClientMetrics.ts +++ b/src/lib/ClientMetrics.ts @@ -44,7 +44,7 @@ export const sendClientMetric = async ( tags?: Record, ) => { try { - const metricTags: Record = { + const metricTags: Record = { ...CLIENT_METADATA, ...tags, }; From 2563143d03849d0636bb5f8b891868d6015c045f Mon Sep 17 00:00:00 2001 From: Robbie Date: Wed, 16 Jul 2025 12:15:16 -0700 Subject: [PATCH 4/4] fix: don't echo messages back --- src/modules/SignallingClient.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/modules/SignallingClient.ts b/src/modules/SignallingClient.ts index b2b4982..692f535 100644 --- a/src/modules/SignallingClient.ts +++ b/src/modules/SignallingClient.ts @@ -61,8 +61,11 @@ export class SignallingClient { return; } - // Initialize Ably Realtime client - this.realtime = new Ably.Realtime(this.ablyToken); + // Initialize Ably Realtime client with echo disabled + this.realtime = new Ably.Realtime({ + token: this.ablyToken, + echoMessages: false, + }); // Get the channel with hardcoded rewind parameter of 100 this.channel = this.realtime.channels.get(this.channelName, {