perf: multiple peerRoom implementation

This commit is contained in:
molvqingtai 2024-09-24 15:02:12 +08:00
parent ec62b1155e
commit e373993899
15 changed files with 628 additions and 276 deletions

View file

@ -56,8 +56,11 @@
"@radix-ui/react-scroll-area": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.0",
"@resreq/event-hub": "^1.6.0",
"@resreq/timer": "^1.1.5",
"@rtco/client": "^0.2.17",
"@tailwindcss/typography": "^0.5.15",
"@webext-core/proxy-service": "^1.2.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",

View file

@ -44,12 +44,21 @@ importers:
'@radix-ui/react-switch':
specifier: ^1.1.0
version: 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@resreq/event-hub':
specifier: ^1.6.0
version: 1.6.0
'@resreq/timer':
specifier: ^1.1.5
version: 1.1.5
'@rtco/client':
specifier: ^0.2.17
version: 0.2.17
'@tailwindcss/typography':
specifier: ^0.5.15
version: 0.5.15(tailwindcss@3.4.12)
'@webext-core/proxy-service':
specifier: ^1.2.0
version: 1.2.0(@webext-core/messaging@1.4.0)(webextension-polyfill@0.12.0)
class-variance-authority:
specifier: ^0.7.0
version: 0.7.0
@ -115,7 +124,7 @@ importers:
version: 2.5.2
trystero:
specifier: ^0.20.0
version: 0.20.0(@libp2p/interface@1.7.0)(@waku/core@0.0.27(@multiformats/multiaddr@12.3.1)(libp2p@1.9.4))(@waku/enr@0.0.21)(@waku/interfaces@0.0.22)(@waku/message-hash@0.1.16)(@waku/proto@0.0.7)(@waku/relay@0.0.11(@waku/core@0.0.27(@multiformats/multiaddr@12.3.1)(libp2p@1.9.4))(@waku/interfaces@0.0.22)(@waku/proto@0.0.7)(@waku/utils@0.0.20))(@waku/utils@0.0.20)
version: 0.20.0(@libp2p/interface@1.7.0)(@waku/core@0.0.27(@multiformats/multiaddr@12.3.1)(libp2p@1.9.4))(@waku/enr@0.0.21)(@waku/interfaces@0.0.22)(@waku/message-hash@0.1.16)(@waku/proto@0.0.7)(@waku/relay@0.0.11(@waku/core@0.0.27(@multiformats/multiaddr@12.3.1)(libp2p@1.9.4))(@waku/interfaces@0.0.22)(@waku/proto@0.0.7)(@waku/utils@0.0.20))(@waku/utils@0.0.20)(bufferutil@4.0.8)(utf-8-validate@6.0.4)
type-fest:
specifier: ^4.26.1
version: 4.26.1
@ -227,7 +236,7 @@ importers:
version: 6.0.1
wxt:
specifier: ^0.19.9
version: 0.19.9(@types/node@22.5.5)(rollup@4.21.3)
version: 0.19.9(@types/node@22.5.5)(bufferutil@4.0.8)(rollup@4.21.3)(utf-8-validate@6.0.4)
packages:
@ -1741,6 +1750,12 @@ packages:
cpu: [x64]
os: [win32]
'@rtco/client@0.2.17':
resolution: {integrity: sha512-nV/KJGBh/j0fK069uADXNr30JBbWe7CZ0xe0cEx1JjuBQIkH10Ny5mQm4WZd/vID+EF7l+tqkCG6QUyCAW5PFg==}
'@rtco/peer@0.2.17':
resolution: {integrity: sha512-jxKQzAIMiofkJ5UHIbeq2JUl+fBOCnWRxgxemzuI7TKw96pbkfaMowr3fj+ElXnKPWwCi5WRX3hwitqjfNkwFQ==}
'@sec-ant/readable-stream@0.4.1':
resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
@ -1756,6 +1771,9 @@ packages:
resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==}
engines: {node: '>=18'}
'@socket.io/component-emitter@3.1.2':
resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==}
'@supabase/auth-js@2.65.0':
resolution: {integrity: sha512-+wboHfZufAE2Y612OsKeVP4rVOeGZzzMLD/Ac3HrTQkkY4qXNjI6Af9gtmxwccE5nFvTiF114FEbIQ1hRq5uUw==}
@ -2105,6 +2123,15 @@ packages:
'@webext-core/match-patterns@1.0.3':
resolution: {integrity: sha512-NY39ACqCxdKBmHgw361M9pfJma8e4AZo20w9AY+5ZjIj1W2dvXC8J31G5fjfOGbulW9w4WKpT8fPooi0mLkn9A==}
'@webext-core/messaging@1.4.0':
resolution: {integrity: sha512-gzXQ13HfKR3Yrn9TnrvTC/5seA7uPFvaqxqNFBsFOOdSZa5LyXt58Rhym8BYXarkWUGp+fh8f6AYM3RYuNbS+A==}
'@webext-core/proxy-service@1.2.0':
resolution: {integrity: sha512-MCUadVakeb7L47AvdtlbJfBUDjFdejr5t4E2WrwZagnev3a5I/xh2wHCkE+G0ihO/VUt/m0R1MPX+y4YVFRyPA==}
peerDependencies:
'@webext-core/messaging': '>=1.3.1'
webextension-polyfill: ^0.10.0
'@xobotyi/scrollbar-width@1.9.5':
resolution: {integrity: sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==}
@ -2311,6 +2338,10 @@ packages:
buffer@6.0.3:
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
bufferutil@4.0.8:
resolution: {integrity: sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==}
engines: {node: '>=6.14.2'}
bundle-name@3.0.0:
resolution: {integrity: sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw==}
engines: {node: '>=12'}
@ -2825,6 +2856,13 @@ packages:
end-of-stream@1.4.4:
resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==}
engine.io-client@6.6.1:
resolution: {integrity: sha512-aYuoak7I+R83M/BBPIOs2to51BmFIpC1wZe6zZzMrT2llVsHy5cvcmdsJgP2Qz6smHu+sD9oexiSUAVd8OfBPw==}
engine.io-parser@5.2.3:
resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==}
engines: {node: '>=10.0.0'}
entities@4.5.0:
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
engines: {node: '>=0.12'}
@ -3271,6 +3309,10 @@ packages:
resolution: {integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==}
engines: {node: '>= 0.4'}
get-value@3.0.1:
resolution: {integrity: sha512-mKZj9JLQrwMBtj5wxi6MH8Z5eSKaERpAwjg43dPtlGI1ZVEgH/qC7T8/6R2OBSUA+zzHBZgICsVJaEIV2tKTDA==}
engines: {node: '>=6.0'}
giget@1.2.3:
resolution: {integrity: sha512-8EHPljDvs7qKykr6uw8b+lqLiUc/vUg+KVTI0uND4s63TdsZM2Xus3mflvF0DDG9SiM4RlCkFGL+7aAjRmV7KA==}
hasBin: true
@ -4415,6 +4457,10 @@ packages:
resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==}
engines: {node: '>= 6.13.0'}
node-gyp-build@4.8.2:
resolution: {integrity: sha512-IRUxE4BVsHWXkV/SFOut4qTlagw2aM8T5/vnTsmrHJvVoKueJHRc/JaFND7QDDc61kLYUJ6qlZM3sqTSyx2dTw==}
hasBin: true
node-notifier@10.0.1:
resolution: {integrity: sha512-YX7TSyDukOZ0g+gmzjB6abKu+hTGvO8+8+gIFDsRCU2t8fLV/P2unmt+LGFaIa4y64aX98Qksa97rgz4vMNeLQ==}
@ -5128,6 +5174,10 @@ packages:
engines: {node: '>=10'}
hasBin: true
serialize-error@11.0.3:
resolution: {integrity: sha512-2G2y++21dhj2R7iHAdd0FIzjGwuKZld+7Pl/bTU6YIkrC2ZMbVUjm+luj6A6V34Rv9XfKJDKpTWu9W4Gse1D9g==}
engines: {node: '>=14.16'}
serialize-error@9.1.1:
resolution: {integrity: sha512-6uZQLGyUkNA4N+Zii9fYukmNu9PEA1F5rqcwXzN/3LtBjwl2dFBbVZ1Zyn08/CGkB4H440PIemdOQBt1Wvjbrg==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
@ -5211,6 +5261,14 @@ packages:
resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==}
engines: {node: '>=18'}
socket.io-client@4.8.0:
resolution: {integrity: sha512-C0jdhD5yQahMws9alf/yvtsMGTaIDBnZ8Rb5HU56svyq0l5LIrGzIDZZD5pHQlmzxLuU91Gz+VpQMKgCTNYtkw==}
engines: {node: '>=10.0.0'}
socket.io-parser@4.2.4:
resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==}
engines: {node: '>=10.0.0'}
sonner@1.5.0:
resolution: {integrity: sha512-FBjhG/gnnbN6FY0jaNnqZOMmB73R+5IiyYAw8yBj7L54ER7HB3fOSE5OFiQiE2iXWxeXKvg6fIP4LtVppHEdJA==}
peerDependencies:
@ -5756,6 +5814,10 @@ packages:
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
utf-8-validate@6.0.4:
resolution: {integrity: sha512-xu9GQDeFp+eZ6LnCywXN/zBancWvOpUMzgjLPSjy4BRHSmTelvn2E0DG0o1sTiw5hkCKBHo8rwSKncfRfv2EEQ==}
engines: {node: '>=6.14.2'}
utf8-bytes@0.0.1:
resolution: {integrity: sha512-GifWmJAx2qAXT+lZLhbkWhBsy7pr6xWHiPWlVToDiELdWgZwt4Ogjf9tlgvKuALzTFR/d+EPQQI9ogJV3957Jg==}
@ -5849,6 +5911,9 @@ packages:
webext-bridge@6.0.1:
resolution: {integrity: sha512-GruIrN+vNwbxVCi8UW4Dqk5YkcGA9V0ZfJ57jXP9JXHbrsDs5k2N6NNYQR5e+wSCnQpGYOGAGihwUpKlhg8QIw==}
webextension-polyfill@0.10.0:
resolution: {integrity: sha512-c5s35LgVa5tFaHhrZDnr3FpQpjj1BB+RXhLTYUxGqBVN460HkbM8TBtEqdXWbpTKfzwCcjAZVF7zXCYSKtcp9g==}
webextension-polyfill@0.12.0:
resolution: {integrity: sha512-97TBmpoWJEE+3nFBQ4VocyCdLKfw54rFaJ6EVQYLBCXqCIpLSZkwGgASpv4oPt9gdKCJ80RJlcmNzNn008Ag6Q==}
@ -5941,6 +6006,18 @@ packages:
write-file-atomic@3.0.3:
resolution: {integrity: sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==}
ws@8.17.1:
resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==}
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
ws@8.18.0:
resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==}
engines: {node: '>=10.0.0'}
@ -5969,6 +6046,10 @@ packages:
resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==}
engines: {node: '>=4.0'}
xmlhttprequest-ssl@2.1.1:
resolution: {integrity: sha512-ptjR8YSJIXoA3Mbv5po7RtSYHO6mZr8s7i5VGmEk7QY2pQWyT1o0N+W1gKbOyJPUCGXGnuw0wqe8f0L6Y0ny7g==}
engines: {node: '>=0.4.0'}
y18n@5.0.8:
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
engines: {node: '>=10'}
@ -7186,7 +7267,7 @@ snapshots:
uint8arraylist: 2.4.8
uint8arrays: 5.1.0
'@libp2p/websockets@8.2.0':
'@libp2p/websockets@8.2.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)':
dependencies:
'@libp2p/interface': 1.7.0
'@libp2p/utils': 5.4.9
@ -7194,12 +7275,12 @@ snapshots:
'@multiformats/multiaddr': 12.3.1
'@multiformats/multiaddr-to-uri': 10.1.0
'@types/ws': 8.5.12
it-ws: 6.1.5
it-ws: 6.1.5(bufferutil@4.0.8)(utf-8-validate@6.0.4)
p-defer: 4.0.1
progress-events: 1.0.1
race-signal: 1.1.0
wherearewe: 2.0.1
ws: 8.18.0
ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)
transitivePeerDependencies:
- bufferutil
- utf-8-validate
@ -7781,6 +7862,21 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.21.3':
optional: true
'@rtco/client@0.2.17':
dependencies:
'@rtco/peer': 0.2.17
bufferutil: 4.0.8
eventemitter3: 5.0.1
nanoid: 5.0.7
socket.io-client: 4.8.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)
utf-8-validate: 6.0.4
transitivePeerDependencies:
- supports-color
'@rtco/peer@0.2.17':
dependencies:
eventemitter3: 5.0.1
'@sec-ant/readable-stream@0.4.1': {}
'@sindresorhus/fnv1a@3.1.0': {}
@ -7789,6 +7885,8 @@ snapshots:
'@sindresorhus/merge-streams@4.0.0': {}
'@socket.io/component-emitter@3.1.2': {}
'@supabase/auth-js@2.65.0':
dependencies:
'@supabase/node-fetch': 2.6.15
@ -7805,12 +7903,12 @@ snapshots:
dependencies:
'@supabase/node-fetch': 2.6.15
'@supabase/realtime-js@2.10.2':
'@supabase/realtime-js@2.10.2(bufferutil@4.0.8)(utf-8-validate@6.0.4)':
dependencies:
'@supabase/node-fetch': 2.6.15
'@types/phoenix': 1.6.5
'@types/ws': 8.5.12
ws: 8.18.0
ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)
transitivePeerDependencies:
- bufferutil
- utf-8-validate
@ -7819,13 +7917,13 @@ snapshots:
dependencies:
'@supabase/node-fetch': 2.6.15
'@supabase/supabase-js@2.45.4':
'@supabase/supabase-js@2.45.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)':
dependencies:
'@supabase/auth-js': 2.65.0
'@supabase/functions-js': 2.4.1
'@supabase/node-fetch': 2.6.15
'@supabase/postgrest-js': 1.16.1
'@supabase/realtime-js': 2.10.2
'@supabase/realtime-js': 2.10.2(bufferutil@4.0.8)(utf-8-validate@6.0.4)
'@supabase/storage-js': 2.7.0
transitivePeerDependencies:
- bufferutil
@ -8213,13 +8311,13 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@waku/sdk@0.0.26(@libp2p/interface@1.7.0)(@waku/core@0.0.27(@multiformats/multiaddr@12.3.1)(libp2p@1.9.4))(@waku/enr@0.0.21)(@waku/interfaces@0.0.22)(@waku/message-hash@0.1.16)(@waku/relay@0.0.11(@waku/core@0.0.27(@multiformats/multiaddr@12.3.1)(libp2p@1.9.4))(@waku/interfaces@0.0.22)(@waku/proto@0.0.7)(@waku/utils@0.0.20))(@waku/utils@0.0.20)':
'@waku/sdk@0.0.26(@libp2p/interface@1.7.0)(@waku/core@0.0.27(@multiformats/multiaddr@12.3.1)(libp2p@1.9.4))(@waku/enr@0.0.21)(@waku/interfaces@0.0.22)(@waku/message-hash@0.1.16)(@waku/relay@0.0.11(@waku/core@0.0.27(@multiformats/multiaddr@12.3.1)(libp2p@1.9.4))(@waku/interfaces@0.0.22)(@waku/proto@0.0.7)(@waku/utils@0.0.20))(@waku/utils@0.0.20)(bufferutil@4.0.8)(utf-8-validate@6.0.4)':
dependencies:
'@chainsafe/libp2p-noise': 14.1.0
'@libp2p/identify': 1.0.21
'@libp2p/mplex': 10.1.5
'@libp2p/ping': 1.1.6
'@libp2p/websockets': 8.2.0
'@libp2p/websockets': 8.2.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)
'@noble/hashes': 1.5.0
'@waku/core': 0.0.27(@multiformats/multiaddr@12.3.1)(libp2p@1.9.4)
'@waku/discovery': 0.0.3(@libp2p/interface@1.7.0)(@waku/core@0.0.27(@multiformats/multiaddr@12.3.1)(libp2p@1.9.4))(@waku/enr@0.0.21)(@waku/interfaces@0.0.22)(@waku/proto@0.0.7)(@waku/utils@0.0.20)
@ -8267,6 +8365,17 @@ snapshots:
'@webext-core/match-patterns@1.0.3': {}
'@webext-core/messaging@1.4.0':
dependencies:
serialize-error: 11.0.3
webextension-polyfill: 0.10.0
'@webext-core/proxy-service@1.2.0(@webext-core/messaging@1.4.0)(webextension-polyfill@0.12.0)':
dependencies:
'@webext-core/messaging': 1.4.0
get-value: 3.0.1
webextension-polyfill: 0.12.0
'@xobotyi/scrollbar-width@1.9.5': {}
JSONStream@1.3.5:
@ -8482,6 +8591,10 @@ snapshots:
base64-js: 1.5.1
ieee754: 1.2.1
bufferutil@4.0.8:
dependencies:
node-gyp-build: 4.8.2
bundle-name@3.0.0:
dependencies:
run-applescript: 5.0.0
@ -9011,6 +9124,20 @@ snapshots:
dependencies:
once: 1.4.0
engine.io-client@6.6.1(bufferutil@4.0.8)(utf-8-validate@6.0.4):
dependencies:
'@socket.io/component-emitter': 3.1.2
debug: 4.3.7
engine.io-parser: 5.2.3
ws: 8.17.1(bufferutil@4.0.8)(utf-8-validate@6.0.4)
xmlhttprequest-ssl: 2.1.1
transitivePeerDependencies:
- bufferutil
- supports-color
- utf-8-validate
engine.io-parser@5.2.3: {}
entities@4.5.0: {}
env-paths@2.2.1: {}
@ -9669,6 +9796,10 @@ snapshots:
es-errors: 1.3.0
get-intrinsic: 1.2.4
get-value@3.0.1:
dependencies:
isobject: 3.0.1
giget@1.2.3:
dependencies:
citty: 0.1.6
@ -10209,13 +10340,13 @@ snapshots:
it-take@3.0.6: {}
it-ws@6.1.5:
it-ws@6.1.5(bufferutil@4.0.8)(utf-8-validate@6.0.4):
dependencies:
'@types/ws': 8.5.12
event-iterator: 2.0.0
it-stream-types: 2.0.2
uint8arrays: 5.1.0
ws: 8.18.0
ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)
transitivePeerDependencies:
- bufferutil
- utf-8-validate
@ -10932,7 +11063,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
mqtt@5.10.1:
mqtt@5.10.1(bufferutil@4.0.8)(utf-8-validate@6.0.4):
dependencies:
'@types/readable-stream': 4.0.15
'@types/ws': 8.5.12
@ -10949,7 +11080,7 @@ snapshots:
rfdc: 1.4.1
split2: 4.2.0
worker-timers: 7.1.8
ws: 8.18.0
ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)
transitivePeerDependencies:
- bufferutil
- supports-color
@ -11044,6 +11175,8 @@ snapshots:
node-forge@1.3.1: {}
node-gyp-build@4.8.2: {}
node-notifier@10.0.1:
dependencies:
growly: 1.3.0
@ -11863,6 +11996,10 @@ snapshots:
semver@7.6.3: {}
serialize-error@11.0.3:
dependencies:
type-fest: 2.19.0
serialize-error@9.1.1:
dependencies:
type-fest: 2.19.0
@ -11945,6 +12082,24 @@ snapshots:
ansi-styles: 6.2.1
is-fullwidth-code-point: 5.0.0
socket.io-client@4.8.0(bufferutil@4.0.8)(utf-8-validate@6.0.4):
dependencies:
'@socket.io/component-emitter': 3.1.2
debug: 4.3.7
engine.io-client: 6.6.1(bufferutil@4.0.8)(utf-8-validate@6.0.4)
socket.io-parser: 4.2.4
transitivePeerDependencies:
- bufferutil
- supports-color
- utf-8-validate
socket.io-parser@4.2.4:
dependencies:
'@socket.io/component-emitter': 3.1.2
debug: 4.3.7
transitivePeerDependencies:
- supports-color
sonner@1.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
react: 18.3.1
@ -12246,16 +12401,16 @@ snapshots:
trough@2.2.0: {}
trystero@0.20.0(@libp2p/interface@1.7.0)(@waku/core@0.0.27(@multiformats/multiaddr@12.3.1)(libp2p@1.9.4))(@waku/enr@0.0.21)(@waku/interfaces@0.0.22)(@waku/message-hash@0.1.16)(@waku/proto@0.0.7)(@waku/relay@0.0.11(@waku/core@0.0.27(@multiformats/multiaddr@12.3.1)(libp2p@1.9.4))(@waku/interfaces@0.0.22)(@waku/proto@0.0.7)(@waku/utils@0.0.20))(@waku/utils@0.0.20):
trystero@0.20.0(@libp2p/interface@1.7.0)(@waku/core@0.0.27(@multiformats/multiaddr@12.3.1)(libp2p@1.9.4))(@waku/enr@0.0.21)(@waku/interfaces@0.0.22)(@waku/message-hash@0.1.16)(@waku/proto@0.0.7)(@waku/relay@0.0.11(@waku/core@0.0.27(@multiformats/multiaddr@12.3.1)(libp2p@1.9.4))(@waku/interfaces@0.0.22)(@waku/proto@0.0.7)(@waku/utils@0.0.20))(@waku/utils@0.0.20)(bufferutil@4.0.8)(utf-8-validate@6.0.4):
dependencies:
'@noble/curves': 1.6.0
'@supabase/supabase-js': 2.45.4
'@supabase/supabase-js': 2.45.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
'@thaunknown/simple-peer': 10.0.10
'@waku/discovery': 0.0.3(@libp2p/interface@1.7.0)(@waku/core@0.0.27(@multiformats/multiaddr@12.3.1)(libp2p@1.9.4))(@waku/enr@0.0.21)(@waku/interfaces@0.0.22)(@waku/proto@0.0.7)(@waku/utils@0.0.20)
'@waku/sdk': 0.0.26(@libp2p/interface@1.7.0)(@waku/core@0.0.27(@multiformats/multiaddr@12.3.1)(libp2p@1.9.4))(@waku/enr@0.0.21)(@waku/interfaces@0.0.22)(@waku/message-hash@0.1.16)(@waku/relay@0.0.11(@waku/core@0.0.27(@multiformats/multiaddr@12.3.1)(libp2p@1.9.4))(@waku/interfaces@0.0.22)(@waku/proto@0.0.7)(@waku/utils@0.0.20))(@waku/utils@0.0.20)
'@waku/sdk': 0.0.26(@libp2p/interface@1.7.0)(@waku/core@0.0.27(@multiformats/multiaddr@12.3.1)(libp2p@1.9.4))(@waku/enr@0.0.21)(@waku/interfaces@0.0.22)(@waku/message-hash@0.1.16)(@waku/relay@0.0.11(@waku/core@0.0.27(@multiformats/multiaddr@12.3.1)(libp2p@1.9.4))(@waku/interfaces@0.0.22)(@waku/proto@0.0.7)(@waku/utils@0.0.20))(@waku/utils@0.0.20)(bufferutil@4.0.8)(utf-8-validate@6.0.4)
firebase: 10.13.1
libp2p: 1.9.4
mqtt: 5.10.1
mqtt: 5.10.1(bufferutil@4.0.8)(utf-8-validate@6.0.4)
transitivePeerDependencies:
- '@libp2p/bootstrap'
- '@libp2p/interface'
@ -12541,6 +12696,10 @@ snapshots:
dependencies:
react: 18.3.1
utf-8-validate@6.0.4:
dependencies:
node-gyp-build: 4.8.2
utf8-bytes@0.0.1: {}
utf8-codec@1.0.0: {}
@ -12614,7 +12773,7 @@ snapshots:
ms: 3.0.0-canary.1
supports-color: 9.4.0
web-ext-run@0.2.1:
web-ext-run@0.2.1(bufferutil@4.0.8)(utf-8-validate@6.0.4):
dependencies:
'@babel/runtime': 7.24.7
'@devicefarmer/adbkit': 3.2.6
@ -12638,7 +12797,7 @@ snapshots:
tmp: 0.2.3
update-notifier: 6.0.2
watchpack: 2.4.1
ws: 8.18.0
ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)
zip-dir: 2.0.0
transitivePeerDependencies:
- bufferutil
@ -12653,6 +12812,8 @@ snapshots:
tiny-uid: 1.1.2
webextension-polyfill: 0.9.0
webextension-polyfill@0.10.0: {}
webextension-polyfill@0.12.0: {}
webextension-polyfill@0.9.0: {}
@ -12768,9 +12929,17 @@ snapshots:
signal-exit: 3.0.7
typedarray-to-buffer: 3.1.5
ws@8.18.0: {}
ws@8.17.1(bufferutil@4.0.8)(utf-8-validate@6.0.4):
optionalDependencies:
bufferutil: 4.0.8
utf-8-validate: 6.0.4
wxt@0.19.9(@types/node@22.5.5)(rollup@4.21.3):
ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4):
optionalDependencies:
bufferutil: 4.0.8
utf-8-validate: 6.0.4
wxt@0.19.9(@types/node@22.5.5)(bufferutil@4.0.8)(rollup@4.21.3)(utf-8-validate@6.0.4):
dependencies:
'@aklinker1/rollup-plugin-visualizer': 5.12.0(rollup@4.21.3)
'@types/chrome': 0.0.269
@ -12814,7 +12983,7 @@ snapshots:
unimport: 3.12.0(rollup@4.21.3)
vite: 5.4.5(@types/node@22.5.5)
vite-node: 2.1.1(@types/node@22.5.5)
web-ext-run: 0.2.1
web-ext-run: 0.2.1(bufferutil@4.0.8)(utf-8-validate@6.0.4)
webextension-polyfill: 0.12.0
transitivePeerDependencies:
- '@types/node'
@ -12840,6 +13009,8 @@ snapshots:
xmlbuilder@11.0.1: {}
xmlhttprequest-ssl@2.1.1: {}
y18n@5.0.8: {}
yallist@3.1.1: {}

View file

@ -1,3 +1,4 @@
import { EVENT } from '@/constants/event'
import { browser } from 'wxt/browser'
import { defineBackground } from 'wxt/sandbox'
@ -7,8 +8,10 @@ export default defineBackground({
type: 'module',
main() {
browser.runtime.onMessage.addListener(async () => {
browser.runtime.openOptionsPage()
browser.runtime.onMessage.addListener(async (event: EVENT) => {
if (event === EVENT.OPEN_OPTIONS_PAGE) {
browser.runtime.openOptionsPage()
}
})
}
})

View file

@ -5,15 +5,12 @@ import AppButton from '@/app/content/views/AppButton'
import AppContainer from '@/app/content/views/AppContainer'
import { useRemeshDomain, useRemeshQuery, useRemeshSend } from 'remesh-react'
import RoomDomain from '@/domain/Room'
import { stringToHex } from '@/utils'
import { Toaster } from '@/components/ui/Sonner'
import UserInfoDomain from '@/domain/UserInfo'
import Setup from '@/app/content/views/Setup'
import MessageListDomain from '@/domain/MessageList'
import { useEffect } from 'react'
const hostRoomId = stringToHex(document.location.host)
export default function App() {
const send = useRemeshSend()
const roomDomain = useRemeshDomain(RoomDomain())
@ -29,7 +26,7 @@ export default function App() {
useEffect(() => {
if (userInfoFinished) {
if (userInfo) {
!roomFinished && send(roomDomain.command.JoinRoomCommand(hostRoomId))
!roomFinished && send(roomDomain.command.JoinRoomCommand())
} else {
send(messageListDomain.command.ClearListCommand())
}

View file

@ -12,7 +12,7 @@ export interface PromptItemProps {
const PromptItem: FC<PromptItemProps> = ({ data, className }) => {
return (
<div className={cn('flex justify-center py-1', className)}>
<div className={cn('flex justify-center py-1 px-4', className)}>
<Badge variant="secondary" className="gap-x-2 rounded-full font-medium text-slate-400">
<Avatar className="size-4">
<AvatarImage src={data.userAvatar} alt="avatar" />

View file

@ -8,7 +8,8 @@ import { createShadowRootUi } from 'wxt/client'
import App from './App'
import { IndexDBStorageImpl, BrowserSyncStorageImpl } from '@/domain/impls/Storage'
import { PeerRoomImpl } from '@/domain/impls/PeerRoom'
// import { PeerRoomImpl } from '@/domain/impls/PeerRoom'
import { PeerRoomImpl } from '@/domain/impls/PeerRoom2'
import '@/assets/styles/tailwind.css'
import { createElement } from '@/utils'
import { ToastImpl } from '@/domain/impls/Toast'
@ -19,7 +20,7 @@ export default defineContentScript({
async main(ctx) {
const store = Remesh.store({
externs: [IndexDBStorageImpl, BrowserSyncStorageImpl, PeerRoomImpl, ToastImpl],
inspectors: !__DEV__ ? [RemeshLogger()] : []
inspectors: __DEV__ ? [RemeshLogger()] : []
})
const ui = await createShadowRootUi(ctx, {

View file

@ -6,13 +6,12 @@ import { Button } from '@/components/ui/Button'
import { getSiteInfo } from '@/utils'
import { useRemeshDomain, useRemeshQuery } from 'remesh-react'
import RoomDomain from '@/domain/Room'
import { selfId } from 'trystero'
const Header: FC = () => {
const siteInfo = getSiteInfo()
const roomDomain = useRemeshDomain(RoomDomain())
const userList = useRemeshQuery(roomDomain.query.UserListQuery())
console.log('userList', [...userList], userList.length)
const peerId = useRemeshQuery(roomDomain.query.PeerIdQuery())
return (
<div className="z-10 grid h-12 grid-flow-col items-center justify-between gap-x-4 rounded-t-xl bg-white px-4 backdrop-blur-lg">
@ -27,7 +26,7 @@ const Header: FC = () => {
<Button className="overflow-hidden" variant="link">
<span className="truncate text-lg font-medium text-slate-600">
{/* {siteInfo.hostname.replace(/^www\./i, '')} */}
{selfId}
{peerId}
</span>
</Button>
</HoverCardTrigger>

View file

@ -21,8 +21,8 @@ const AvatarSelect = React.forwardRef<HTMLInputElement, AvatarSelectProps>(
const handleChange = async (e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (file) {
if (!/image\/(png|jpeg)/.test(file.type)) {
onWarning?.(new Error('Only PNG and JPEG image are supported.'))
if (!/image\/(png|jpeg|webp)/.test(file.type)) {
onWarning?.(new Error('Only PNG, JPEG and WebP image are supported.'))
return
}

View file

@ -42,7 +42,7 @@ const formSchema = v.object({
avatar: v.pipe(
v.string(),
v.notLength(0, 'Please select your avatar.'),
v.maxBytes(8 * 1024, 'Your avatar cannot exceed 8kb.')
v.maxBytes(8 * 1024, `Your avatar cannot exceed 8kb.`)
),
themeMode: v.pipe(
v.string(),
@ -92,32 +92,24 @@ const ProfileForm = () => {
control={form.control}
name="avatar"
render={({ field }) => (
<FormItem className="absolute left-1/2 top-0 grid -translate-x-1/2 -translate-y-1/2 justify-items-center">
<FormItem className="absolute inset-x-1 top-0 mx-auto grid w-fit -translate-y-1/2 justify-items-center">
<FormControl>
<div className="grid justify-items-center gap-y-2">
<AvatarSelect
compressSize={MAX_AVATAR_SIZE}
onError={handleError}
onWarning={handleWarning}
className="shadow-lg"
{...field}
></AvatarSelect>
<Button
type="button"
size="xs"
className="mx-auto flex items-center gap-x-2"
onClick={handleRefreshAvatar}
>
<RefreshCcwIcon size={14} />
Ugly Avatar
</Button>
</div>
<AvatarSelect
compressSize={MAX_AVATAR_SIZE}
onError={handleError}
onWarning={handleWarning}
className="shadow-lg"
{...field}
></AvatarSelect>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="button" size="xs" className="mx-auto flex items-center gap-x-2" onClick={handleRefreshAvatar}>
<RefreshCcwIcon size={14} />
Ugly Avatar
</Button>
<FormField
control={form.control}
name="name"

View file

@ -194,4 +194,4 @@ export const STORAGE_NAME = 'WEB_CHAT' as const
* Image is encoded as base64, and the size is increased by about 33%.
* 8kb * (1 - 0.33) = 5488 bytes
*/
export const MAX_AVATAR_SIZE = 5488 as const
export const MAX_AVATAR_SIZE = 5120 as const

View file

@ -1,5 +1,5 @@
import { Remesh } from 'remesh'
import { map, merge, switchMap, tap, of, EMPTY, mergeMap } from 'rxjs'
import { map, merge, of, EMPTY, mergeMap } from 'rxjs'
import { NormalMessage, type MessageUser } from './MessageList'
import { PeerRoomExtern } from '@/domain/externs/PeerRoom'
import MessageListDomain, { MessageType } from '@/domain/MessageList'
@ -51,6 +51,18 @@ const RoomDomain = Remesh.domain({
const userInfoDomain = domain.getDomain(UserInfoDomain())
const peerRoom = domain.getExtern(PeerRoomExtern)
const PeerIdState = domain.state<string>({
name: 'Room.PeerIdState',
default: peerRoom.peerId
})
const PeerIdQuery = domain.query({
name: 'Room.PeerIdQuery',
impl: ({ get }) => {
return get(PeerIdState())
}
})
const MessageListQuery = messageListDomain.query.ListQuery
const RoomStatusModule = StatusModule(domain, {
@ -71,16 +83,17 @@ const RoomDomain = Remesh.domain({
const JoinRoomCommand = domain.command({
name: 'RoomJoinRoomCommand',
impl: ({ get }, roomId: string) => {
peerRoom.joinRoom(roomId)
impl: ({ get }) => {
peerRoom.joinRoom()
const { id: userId, name: username, avatar: userAvatar } = get(userInfoDomain.query.UserInfoQuery())!
return [
JoinRoomEvent(roomId),
RoomStatusModule.command.SetFinishedCommand(),
UpdateUserListCommand({
type: 'create',
user: { peerId: peerRoom.selfId, joinTime: Date.now(), userId, username, userAvatar }
})
user: { peerId: peerRoom.peerId, joinTime: Date.now(), userId, username, userAvatar }
}),
RoomStatusModule.command.SetFinishedCommand(),
JoinRoomEvent(peerRoom.roomId)
]
}
})
@ -91,12 +104,12 @@ const RoomDomain = Remesh.domain({
peerRoom.leaveRoom()
const { id: userId, name: username, avatar: userAvatar } = get(userInfoDomain.query.UserInfoQuery())!
return [
LeaveRoomEvent(roomId),
RoomStatusModule.command.SetInitialCommand(),
UpdateUserListCommand({
type: 'delete',
user: { peerId: peerRoom.selfId, joinTime: Date.now(), userId, username, userAvatar }
})
user: { peerId: peerRoom.peerId, joinTime: Date.now(), userId, username, userAvatar }
}),
RoomStatusModule.command.SetInitialCommand(),
LeaveRoomEvent(roomId)
]
}
})
@ -105,22 +118,26 @@ const RoomDomain = Remesh.domain({
name: 'RoomSendTextMessageCommand',
impl: ({ get }, message: string) => {
const { id: userId, name: username, avatar: userAvatar } = get(userInfoDomain.query.UserInfoQuery())!
const id = nanoid()
const date = Date.now()
return [
messageListDomain.command.CreateItemCommand({
id,
type: MessageType.Normal,
body: message,
date,
userId,
username,
userAvatar,
likeUsers: [],
hateUsers: []
}),
SendTextMessageEvent({ id, body: message, userId, username, userAvatar, type: SendType.Text })
]
const textMessage: TextMessage = {
id: nanoid(),
type: SendType.Text,
body: message,
userId,
username,
userAvatar
}
const listMessage: NormalMessage = {
...textMessage,
type: MessageType.Normal,
date: Date.now(),
likeUsers: [],
hateUsers: []
}
peerRoom.sendMessage<RoomMessage>(textMessage)
return [messageListDomain.command.CreateItemCommand(listMessage), SendTextMessageEvent(textMessage)]
}
})
@ -128,28 +145,23 @@ const RoomDomain = Remesh.domain({
name: 'RoomSendLikeMessageCommand',
impl: ({ get }, messageId: string) => {
const { id: userId, name: username, avatar: userAvatar } = get(userInfoDomain.query.UserInfoQuery())!
const _message = get(messageListDomain.query.ItemQuery(messageId)) as NormalMessage
return [
messageListDomain.command.UpdateItemCommand({
..._message,
likeUsers: desert(
_message.likeUsers,
{
userId,
username,
userAvatar
},
'userId'
)
}),
SendLikeMessageEvent({
id: messageId,
userId,
username,
userAvatar,
type: SendType.Like
})
]
const localMessage = get(messageListDomain.query.ItemQuery(messageId)) as NormalMessage
const likeMessage: LikeMessage = {
id: messageId,
userId,
username,
userAvatar,
type: SendType.Like
}
const listMessage: NormalMessage = {
...localMessage,
type: MessageType.Normal,
date: Date.now(),
likeUsers: desert(localMessage.likeUsers, likeMessage, 'userId')
}
peerRoom.sendMessage<RoomMessage>(likeMessage)
return [messageListDomain.command.UpdateItemCommand(listMessage), SendLikeMessageEvent(likeMessage)]
}
})
@ -157,47 +169,55 @@ const RoomDomain = Remesh.domain({
name: 'RoomSendHateMessageCommand',
impl: ({ get }, messageId: string) => {
const { id: userId, name: username, avatar: userAvatar } = get(userInfoDomain.query.UserInfoQuery())!
const _message = get(messageListDomain.query.ItemQuery(messageId)) as NormalMessage
const localMessage = get(messageListDomain.query.ItemQuery(messageId)) as NormalMessage
return [
messageListDomain.command.UpdateItemCommand({
..._message,
hateUsers: desert(
_message.hateUsers,
{
userId,
username,
userAvatar
},
'userId'
)
}),
SendHateMessageEvent({ id: messageId, userId, username, userAvatar, type: SendType.Hate })
]
const hateMessage: HateMessage = {
id: messageId,
userId,
username,
userAvatar,
type: SendType.Hate
}
const listMessage: NormalMessage = {
...localMessage,
type: MessageType.Normal,
date: Date.now(),
hateUsers: desert(localMessage.hateUsers, hateMessage, 'userId')
}
peerRoom.sendMessage<RoomMessage>(hateMessage)
return [messageListDomain.command.UpdateItemCommand(listMessage), SendHateMessageEvent(hateMessage)]
}
})
const SendUserSyncMessageCommand = domain.command({
name: 'RoomSendUserSyncMessageCommand',
impl: ({ get }, targetPeerId: string) => {
const { id: userId, name: username, avatar: userAvatar } = get(userInfoDomain.query.UserInfoQuery())!
const joinTime = get(UserListQuery()).find((u) => u.peerId === peerRoom.selfId)?.joinTime || Date.now()
return [
SendUserSyncMessageEvent({
id: nanoid(),
peerId: peerRoom.selfId,
targetPeerId,
userId,
joinTime,
username,
userAvatar,
type: SendType.UserSync
})
]
const self = get(UserListQuery()).find((user) => user.peerId === peerRoom.peerId)!
const syncUserMessage: SyncUserMessage = {
...self,
id: nanoid(),
type: SendType.UserSync
}
peerRoom.sendMessage<RoomMessage>(syncUserMessage, targetPeerId)
return [SendUserSyncMessageEvent(syncUserMessage)]
}
})
const SendUserSyncMessageEvent = domain.event<SyncUserMessage & { targetPeerId: string }>({
const UpdateUserListCommand = domain.command({
name: 'RoomUpdateUserListCommand',
impl: ({ get }, action: { type: 'create' | 'delete'; user: RoomUser }) => {
const userList = get(UserListState())
if (action.type === 'create') {
return [UserListState().new(upsert(userList, action.user, 'peerId'))]
} else {
return [UserListState().new(userList.filter(({ peerId }) => peerId !== action.user.peerId))]
}
}
})
const SendUserSyncMessageEvent = domain.event<SyncUserMessage>({
name: 'RoomSendUserSyncMessageEvent'
})
@ -233,94 +253,47 @@ const RoomDomain = Remesh.domain({
name: 'RoomOnLeaveRoomEvent'
})
const UpdateUserListCommand = domain.command({
name: 'RoomUpdateUserListCommand',
impl: ({ get }, action: { type: 'create' | 'delete'; user: RoomUser }) => {
const userList = get(UserListState())
if (action.type === 'create') {
return [UserListState().new(upsert(userList, action.user, 'peerId'))]
} else {
return [UserListState().new(userList.filter(({ peerId }) => peerId !== action.user.peerId))]
}
}
})
domain.effect({
name: 'RoomSendTextMessageEffect',
impl: ({ fromEvent }) => {
const sendMessage$ = fromEvent(SendTextMessageEvent).pipe(
tap(async (message) => {
peerRoom.sendMessage<RoomMessage>(message)
name: 'RoomOnJoinRoomEffect',
impl: () => {
const onJoinRoom$ = callbackToObservable<string>(peerRoom.onJoinRoom).pipe(
mergeMap((peerId) => {
console.log('onJoinRoom', peerId)
if (peerRoom.peerId === peerId) {
return [OnJoinRoomEvent(peerId)]
} else {
return [SendUserSyncMessageCommand(peerId), OnJoinRoomEvent(peerId)]
}
})
)
return merge(sendMessage$).pipe(map(() => null))
}
})
domain.effect({
name: 'RoomSendLikeMessageEffect',
impl: ({ fromEvent }) => {
const likeMessage$ = fromEvent(SendLikeMessageEvent).pipe(
tap(async (message) => {
return peerRoom.sendMessage<RoomMessage>(message)
})
)
return merge(likeMessage$).pipe(map(() => null))
}
})
domain.effect({
name: 'RoomSendHateMessageEffect',
impl: ({ fromEvent }) => {
const hateMessage$ = fromEvent(SendHateMessageEvent).pipe(
tap(async (message) => {
peerRoom.sendMessage<RoomMessage>(message)
})
)
return merge(hateMessage$).pipe(map(() => null))
}
})
domain.effect({
name: 'RoomSendUserSyncMessageEffect',
impl: ({ fromEvent }) => {
const userSyncMessage$ = fromEvent(SendUserSyncMessageEvent).pipe(
tap(async (message) => {
console.log('sendMessage', message)
peerRoom.sendMessage<RoomMessage>(message, message.targetPeerId)
})
)
return merge(userSyncMessage$).pipe(map(() => null))
return onJoinRoom$
}
})
domain.effect({
name: 'RoomOnMessageEffect',
impl: ({ fromEvent, get }) => {
const onMessage$ = fromEvent(JoinRoomEvent).pipe(
switchMap(() => callbackToObservable<RoomMessage>(peerRoom.onMessage.bind(peerRoom))),
impl: ({ get }) => {
const onMessage$ = callbackToObservable<RoomMessage>(peerRoom.onMessage).pipe(
mergeMap((message) => {
console.log('onMessage', message)
const messageEvent$ = of(OnMessageEvent(message))
const commandEvent$ = (() => {
switch (message.type) {
case SendType.UserSync: {
const self = get(UserListQuery()).find((user) => user.peerId === peerRoom.selfId)!
if (self.joinTime > message.joinTime) {
return EMPTY
}
const self = get(UserListQuery()).find((user) => user.peerId === peerRoom.peerId)!
const isJoining = self.joinTime < message.joinTime
return of(
UpdateUserListCommand({ type: 'create', user: message }),
messageListDomain.command.CreateItemCommand({
...message,
id: nanoid(),
body: `"${message.username}" joined the chat`,
type: MessageType.Prompt,
date: Date.now()
})
isJoining
? messageListDomain.command.CreateItemCommand({
...message,
id: nanoid(),
body: `"${message.username}" joined the chat`,
type: MessageType.Prompt,
date: Date.now()
})
: null
)
}
case SendType.Text:
@ -356,7 +329,7 @@ const RoomDomain = Remesh.domain({
)
}
default:
console.warn('未知消息类型', message)
console.warn('Unsupported message type', message)
return EMPTY
}
})()
@ -367,36 +340,14 @@ const RoomDomain = Remesh.domain({
}
})
domain.effect({
name: 'RoomOnJoinRoomEffect',
impl: ({ fromEvent, get }) => {
const onJoinRoom$ = fromEvent(JoinRoomEvent).pipe(
switchMap(() => callbackToObservable<string>(peerRoom.onJoinRoom.bind(peerRoom))),
mergeMap((peerId) => {
console.log('onJoinRoom', peerId)
const { id: userId, name: username, avatar: userAvatar } = get(userInfoDomain.query.UserInfoQuery())!
return [
SendUserSyncMessageCommand(peerId),
UpdateUserListCommand({
type: 'create',
user: { peerId, joinTime: Date.now(), userId, username, userAvatar }
}),
OnJoinRoomEvent(peerId)
]
})
)
return onJoinRoom$
}
})
domain.effect({
name: 'RoomOnLeaveRoomEffect',
impl: ({ fromEvent, get }) => {
const onLeaveRoom$ = fromEvent(JoinRoomEvent).pipe(
switchMap(() => callbackToObservable<string>(peerRoom.onLeaveRoom.bind(peerRoom))),
impl: ({ get }) => {
const onLeaveRoom$ = callbackToObservable<string>(peerRoom.onLeaveRoom).pipe(
map((peerId) => {
console.log('onLeaveRoom', peerId)
const user = get(UserListQuery()).find((user) => user.peerId === peerId)
if (user) {
return [
UpdateUserListCommand({ type: 'delete', user }),
@ -420,6 +371,7 @@ const RoomDomain = Remesh.domain({
return {
query: {
PeerIdQuery,
UserListQuery,
MessageListQuery,
...RoomStatusModule.query

View file

@ -1,25 +1,27 @@
import { Remesh } from 'remesh'
import { type Promisable } from 'type-fest'
export type PeerMessage = object | Blob | ArrayBuffer | ArrayBufferView
export interface PeerRoom {
readonly selfId: string
joinRoom: (roomId: string) => Promise<any>
sendMessage: <T extends PeerMessage>(message: T, id?: string) => Promise<any>
onMessage: <T extends PeerMessage>(callback: (message: T) => void) => Promisable<void>
leaveRoom: () => Promisable<void>
onJoinRoom: (callback: (id: string) => void) => Promisable<void>
onLeaveRoom: (callback: (id: string) => void) => Promisable<void>
readonly peerId: string
readonly roomId: string
joinRoom: () => PeerRoom
sendMessage: <T extends PeerMessage>(message: T, id?: string) => PeerRoom
onMessage: <T extends PeerMessage>(callback: (message: T) => void) => PeerRoom
leaveRoom: () => PeerRoom
onJoinRoom: (callback: (id: string) => void) => PeerRoom
onLeaveRoom: (callback: (id: string) => void) => PeerRoom
onError: (callback: (error: Error) => void) => PeerRoom
}
export const PeerRoomExtern = Remesh.extern<PeerRoom>({
default: {
selfId: '',
joinRoom: async () => {
peerId: '',
roomId: '',
joinRoom: () => {
throw new Error('"joinRoom" not implemented.')
},
sendMessage: async () => {
sendMessage: () => {
throw new Error('"sendMessage" not implemented.')
},
onMessage: () => {
@ -33,6 +35,9 @@ export const PeerRoomExtern = Remesh.extern<PeerRoom>({
},
onLeaveRoom: () => {
throw new Error('"onLeaveRoom" not implemented.')
},
onError: () => {
throw new Error('"onError" not implemented.')
}
}
})

View file

@ -1,64 +1,151 @@
import { type DataPayload, type Room, joinRoom, selfId } from 'trystero'
// import { joinRoom } from 'trystero/firebase'
import { PeerRoomExtern, type PeerMessage } from '@/domain/externs/PeerRoom'
import { stringToHex } from '@/utils'
import EventHub from '@resreq/event-hub'
export interface Config {
appId: string
peerId?: string
roomId: string
}
class PeerRoom {
class PeerRoom extends EventHub {
readonly appId: string
room: Room | null
readonly selfId: string
private room?: Room
readonly roomId: string
readonly peerId: string
constructor(config: Config) {
this.appId = config.appId
this.room = null
this.selfId = selfId
super()
this.appId = __NAME__
this.roomId = config.roomId
this.peerId = selfId
this.joinRoom = this.joinRoom.bind(this)
this.sendMessage = this.sendMessage.bind(this)
this.onMessage = this.onMessage.bind(this)
this.onJoinRoom = this.onJoinRoom.bind(this)
this.onLeaveRoom = this.onLeaveRoom.bind(this)
this.leaveRoom = this.leaveRoom.bind(this)
this.onError = this.onError.bind(this)
}
async joinRoom(roomId: string) {
this.room = joinRoom({ appId: this.appId }, roomId)
return this.room
joinRoom() {
this.room = joinRoom({ appId: this.appId }, this.roomId)
/**
* If we wait to join, it will result in not being able to listen to our own join event.
* This might be related to the fact that:
* (If called more than once, only the latest callback registered is ever called.)
* Multiple listeners may overwrite each other.
* @see: https://github.com/dmotz/trystero?tab=readme-ov-file#onpeerjoincallback
*/
// this.room.onPeerJoin(() => this.emit('action'))
this.emit('action')
return this
}
async sendMessage<T extends PeerMessage>(message: T, id?: string) {
sendMessage<T extends PeerMessage>(message: T, id?: string) {
if (!this.room) {
throw new Error('Room not joined')
this.once('action', () => {
if (!this.room) {
const error = new Error('Room not joined')
this.emit('error', error)
throw error
}
const [send] = this.room.makeAction('MESSAGE')
send(message as DataPayload, id)
})
} else {
const [send] = this.room.makeAction('MESSAGE')
send(message as DataPayload, id)
}
const [send] = this.room!.makeAction('MESSAGE')
return await send(message as DataPayload, id)
return this
}
onMessage<T extends PeerMessage>(callback: (message: T) => void) {
if (!this.room) {
throw new Error('Room not joined')
this.once('action', () => {
if (!this.room) {
const error = new Error('Room not joined')
this.emit('error', error)
throw error
}
const [, on] = this.room.makeAction('MESSAGE')
on((message) => callback(message as T))
})
} else {
const [, on] = this.room.makeAction('MESSAGE')
on((message) => callback(message as T))
}
const [, on] = this.room!.makeAction('MESSAGE')
on((message) => callback(message as T))
return this
}
onJoinRoom(callback: (id: string) => void) {
if (!this.room) {
throw new Error('Room not joined')
this.once('action', () => {
if (!this.room) {
const error = new Error('Room not joined')
this.emit('error', error)
throw error
}
this.room.onPeerJoin((peerId) => {
callback(peerId)
})
})
} else {
this.room.onPeerJoin((peerId) => {
callback(peerId)
})
}
this.room.onPeerJoin((peerId) => callback(peerId))
return this
}
onLeaveRoom(callback: (id: string) => void) {
if (!this.room) {
throw new Error('Room not joined')
this.once('action', () => {
if (!this.room) {
const error = new Error('Room not joined')
this.emit('error', error)
throw error
}
this.room.onPeerLeave((peerId) => callback(peerId))
})
} else {
this.room.onPeerLeave((peerId) => callback(peerId))
}
this.room.onPeerLeave((peerId) => callback(peerId))
return this
}
async leaveRoom() {
return await this.room?.leave()
leaveRoom() {
if (!this.room) {
this.once('action', () => {
if (!this.room) {
const error = new Error('Room not joined')
this.emit('error', error)
throw error
}
this.room.leave()
this.room = undefined
})
} else {
this.room.leave()
this.room = undefined
}
return this
}
onError(callback: (error: Error) => void) {
this.on('error', (error: Error) => callback(error))
return this
}
}
const peerRoom = new PeerRoom({ appId: stringToHex(__NAME__) })
const hostRoomId = stringToHex(document.location.host)
const peerRoom = new PeerRoom({ roomId: hostRoomId })
export const PeerRoomImpl = PeerRoomExtern.impl(peerRoom)
// https://github.com/w3c/webextensions/issues/72
// https://issues.chromium.org/issues/40251342
// https://github.com/w3c/webrtc-extensions/issues/77

View file

@ -0,0 +1,142 @@
import { Artico, Room } from '@rtco/client'
import { PeerRoomExtern, type PeerMessage } from '@/domain/externs/PeerRoom'
import { stringToHex } from '@/utils'
import { nanoid } from 'nanoid'
import EventHub from '@resreq/event-hub'
export interface Config {
peerId?: string
roomId: string
}
class PeerRoom extends EventHub {
readonly roomId: string
private rtco?: Artico
readonly peerId: string
private room?: Room
constructor(config: Config) {
super()
this.roomId = config.roomId
this.peerId = config.peerId || nanoid()
this.joinRoom = this.joinRoom.bind(this)
this.sendMessage = this.sendMessage.bind(this)
this.onMessage = this.onMessage.bind(this)
this.onJoinRoom = this.onJoinRoom.bind(this)
this.onLeaveRoom = this.onLeaveRoom.bind(this)
this.leaveRoom = this.leaveRoom.bind(this)
this.onError = this.onError.bind(this)
}
joinRoom() {
if (!this.rtco) {
this.rtco = new Artico({ id: this.peerId })
}
if (this.room) {
this.room = this.rtco.join(this.roomId)
} else {
this.rtco!.on('open', () => {
this.room = this.rtco!.join(this.roomId)
this.emit('action')
})
}
return this
}
sendMessage<T extends PeerMessage>(message: T, id?: string) {
if (!this.room) {
this.once('action', () => {
if (!this.room) {
const error = new Error('Room not joined')
this.emit('error', error)
throw error
}
this.room.send(JSON.stringify(message), id)
})
} else {
this.room.send(JSON.stringify(message), id)
}
return this
}
onMessage<T extends PeerMessage>(callback: (message: T) => void) {
if (!this.room) {
this.once('action', () => {
if (!this.room) {
const error = new Error('Room not joined')
this.emit('error', error)
throw error
}
this.room.on('message', (message) => callback(JSON.parse(message) as T))
})
} else {
this.room.on('message', (message) => callback(JSON.parse(message) as T))
}
return this
}
onJoinRoom(callback: (id: string) => void) {
if (!this.room) {
this.once('action', () => {
if (!this.room) {
const error = new Error('Room not joined')
this.emit('error', error)
throw error
}
this.room.on('join', (id) => callback(id))
})
} else {
this.room.on('join', (id) => callback(id))
}
return this
}
onLeaveRoom(callback: (id: string) => void) {
if (!this.room) {
this.once('action', () => {
if (!this.room) {
const error = new Error('Room not joined')
this.emit('error', error)
throw error
}
this.room.on('leave', (id) => callback(id))
})
} else {
this.room.on('leave', (id) => callback(id))
}
return this
}
leaveRoom() {
if (!this.room) {
this.once('action', () => {
if (!this.room) {
const error = new Error('Room not joined')
this.emit('error', error)
throw error
}
this.room.leave()
this.room = undefined
})
} else {
this.room.leave()
this.room = undefined
}
return this
}
onError(callback: (error: Error) => void) {
this.rtco?.on('error', (error) => callback(error))
this.on('error', (error: Error) => callback(error))
return this
}
}
const hostRoomId = stringToHex(document.location.host)
const peerRoom = new PeerRoom({ roomId: hostRoomId })
export const PeerRoomImpl = PeerRoomExtern.impl(peerRoom)
// https://github.com/w3c/webextensions/issues/72
// https://issues.chromium.org/issues/40251342
// https://github.com/w3c/webrtc-extensions/issues/77

View file

@ -1,11 +1,11 @@
import generateUglyAvatar from '@/lib/uglyAvatar'
import compressImage from './compressImage'
const generateRandomAvatar = async (idealSize: number) => {
const generateRandomAvatar = async (targetSize: number) => {
const svgBlob = generateUglyAvatar()
// compressImage can't directly compress svg, need to convert to jpeg first
const jpegBlob = await new Promise<Blob>((resolve, reject) => {
const imageBlob = await new Promise<Blob>((resolve, reject) => {
const image = new Image()
image.onload = async () => {
const canvas = new OffscreenCanvas(image.width, image.height)
@ -17,7 +17,7 @@ const generateRandomAvatar = async (idealSize: number) => {
image.onerror = () => reject(new Error('Failed to load SVG'))
image.src = URL.createObjectURL(svgBlob)
})
const miniAvatarBlob = await compressImage({ input: jpegBlob, targetSize: idealSize })
const miniAvatarBlob = await compressImage({ input: imageBlob, targetSize })
const miniAvatarBase64 = await new Promise<string>((resolve, reject) => {
const reader = new FileReader()
reader.onload = (e) => resolve(e.target?.result as string)