浏览代码

chore: release (#1130)

Jonas Gloning 1 年之前
父节点
当前提交
d89099aff4
共有 100 个文件被更改,包括 5997 次插入4331 次删除
  1. 1 0
      .github/FUNDING.yml
  2. 38 0
      .github/workflows/browserstack.yml
  3. 6 8
      .github/workflows/prettier.yml
  4. 3 3
      .github/workflows/release.yml
  5. 36 0
      .github/workflows/test.yml
  6. 2 0
      .gitignore
  7. 0 6
      .parcelrc
  8. 120 117
      README.md
  9. 1 1
      __test__/faker.ts
  10. 12 12
      __test__/logger.spec.ts
  11. 222 0
      __test__/peer.spec.ts
  12. 0 0
      __test__/setup.ts
  13. 11 0
      __test__/util.spec.ts
  14. 0 552
      docs/api.json
  15. 0 4
      docs/api.md
  16. 0 13
      docs/build.js
  17. 0 393
      docs/css/docs.css
  18. 0 753
      docs/index.html
  19. 0 79
      docs/js/docs.js
  20. 0 3
      docs/js/jquery.min.js
  21. 0 21
      docs/package.json
  22. 0 6
      docs/readme.md
  23. 0 212
      docs/template.html
  24. 5 0
      e2e/.eslintrc
  25. 5 0
      e2e/alice.html
  26. 5 0
      e2e/bob.html
  27. 1385 0
      e2e/commit_data.js
  28. 75 0
      e2e/data.js
  29. 18 0
      e2e/datachannel/Int32Array.js
  30. 18 0
      e2e/datachannel/Int32Array_as_ArrayBuffer.js
  31. 18 0
      e2e/datachannel/Int32Array_as_Uint8Array.js
  32. 17 0
      e2e/datachannel/TypedArrayView_as_ArrayBuffer.js
  33. 18 0
      e2e/datachannel/Uint8Array.js
  34. 18 0
      e2e/datachannel/Uint8Array_as_ArrayBuffer.js
  35. 18 0
      e2e/datachannel/arraybuffers.js
  36. 18 0
      e2e/datachannel/arraybuffers_as_uint8array.js
  37. 15 0
      e2e/datachannel/arrays.js
  38. 18 0
      e2e/datachannel/arrays_unchunked.js
  39. 15 0
      e2e/datachannel/dates.js
  40. 16 0
      e2e/datachannel/dates_as_json_string.js
  41. 16 0
      e2e/datachannel/dates_as_string.js
  42. 13 0
      e2e/datachannel/long_string.js
  43. 15 0
      e2e/datachannel/numbers.js
  44. 16 0
      e2e/datachannel/objects.js
  45. 23 0
      e2e/datachannel/serialization.html
  46. 94 0
      e2e/datachannel/serialization.js
  47. 67 0
      e2e/datachannel/serialization.page.ts
  48. 23 0
      e2e/datachannel/serializationTest.ts
  49. 71 0
      e2e/datachannel/serialization_binary.spec.ts
  50. 35 0
      e2e/datachannel/serialization_cbor.spec.ts
  51. 53 0
      e2e/datachannel/serialization_json.spec.ts
  52. 44 0
      e2e/datachannel/serialization_msgpack.spec.ts
  53. 15 0
      e2e/datachannel/strings.js
  54. 17 0
      e2e/datachannel/typed_array_view.js
  55. 38 0
      e2e/mediachannel/close.html
  56. 70 0
      e2e/mediachannel/close.js
  57. 61 0
      e2e/mediachannel/close.page.ts
  58. 33 0
      e2e/mediachannel/close.spec.ts
  59. 3 0
      e2e/package.json
  60. 34 0
      e2e/peer/disconnected.html
  61. 56 0
      e2e/peer/id-taken.html
  62. 26 0
      e2e/peer/peer.page.ts
  63. 20 0
      e2e/peer/peer.spec.ts
  64. 31 0
      e2e/peer/server-unavailable.html
  65. 26 0
      e2e/style.css
  66. 70 0
      e2e/tsconfig.json
  67. 11 0
      e2e/types.d.ts
  68. 87 0
      e2e/wdio.bstack.conf.ts
  69. 23 0
      e2e/wdio.local.conf.ts
  70. 250 0
      e2e/wdio.shared.conf.ts
  71. 8 0
      jest.config.cjs
  72. 1 1
      lib/api.ts
  73. 57 10
      lib/baseconnection.ts
  74. 9 0
      lib/cborPeer.ts
  75. 0 359
      lib/dataconnection.ts
  76. 100 0
      lib/dataconnection/BufferedConnection/BinaryPack.ts
  77. 92 0
      lib/dataconnection/BufferedConnection/BufferedConnection.ts
  78. 38 0
      lib/dataconnection/BufferedConnection/Json.ts
  79. 14 0
      lib/dataconnection/BufferedConnection/Raw.ts
  80. 53 0
      lib/dataconnection/BufferedConnection/binaryPackChunker.ts
  81. 161 0
      lib/dataconnection/DataConnection.ts
  82. 75 0
      lib/dataconnection/StreamConnection/Cbor.ts
  83. 27 0
      lib/dataconnection/StreamConnection/MsgPack.ts
  84. 61 0
      lib/dataconnection/StreamConnection/StreamConnection.ts
  85. 53 0
      lib/enums.ts
  86. 16 10
      lib/exports.ts
  87. 13 1
      lib/logger.ts
  88. 56 11
      lib/mediaconnection.ts
  89. 9 0
      lib/msgPackPeer.ts
  90. 37 52
      lib/negotiator.ts
  91. 24 0
      lib/optionInterfaces.ts
  92. 122 47
      lib/peer.ts
  93. 44 0
      lib/peerError.ts
  94. 1 1
      lib/servermessage.ts
  95. 1 1
      lib/socket.ts
  96. 49 54
      lib/util.ts
  97. 1 0
      lib/utils/randomToken.ts
  98. 4 0
      lib/utils/validateId.ts
  99. 1331 1572
      package-lock.json
  100. 65 29
      package.json

+ 1 - 0
.github/FUNDING.yml

@@ -1 +1,2 @@
+github: peers
 open_collective: peer
 open_collective: peer

+ 38 - 0
.github/workflows/browserstack.yml

@@ -0,0 +1,38 @@
+name: "BrowserStack Test"
+on: [push, pull_request]
+jobs:
+  ubuntu-job:
+    name: "BrowserStack Test on Ubuntu"
+    runs-on: ubuntu-latest # Can be self-hosted runner also
+    steps:
+      - name: "BrowserStack Env Setup" # Invokes the setup-env action
+        uses: browserstack/github-actions/setup-env@master
+        with:
+          username: ${{ secrets.BROWSERSTACK_USERNAME }}
+          access-key: ${{ secrets.BROWSERSTACK_ACCESS_KEY }}
+
+      - name: "BrowserStack Local Tunnel Setup" # Invokes the setup-local action
+        uses: browserstack/github-actions/setup-local@master
+        with:
+          local-testing: "start"
+          local-logging-level: "all-logs"
+          local-identifier: "random"
+
+      # The next 3 steps are for building the web application to be tested and starting the web server on the runner environment
+
+      - name: "Checkout the repository"
+        uses: actions/checkout@v3
+
+      - name: "Building web application to be tested"
+        run: npm install && npm run build
+
+      - name: "Running application under test"
+        run: npx http-server -p 3000 --cors &
+
+      - name: "Running test on BrowserStack" # Invokes the actual test script that would run on BrowserStack browsers
+        run: npm run e2e:bstack # See sample test script above
+
+      - name: "BrowserStackLocal Stop" # Terminating the BrowserStackLocal tunnel connection
+        uses: browserstack/github-actions/setup-local@master
+        with:
+          local-testing: "stop"

+ 6 - 8
.github/workflows/prettier.yml

@@ -10,14 +10,12 @@ jobs:
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
     steps:
     steps:
       - name: Check out repo
       - name: Check out repo
-        uses: actions/checkout@v2
-      - uses: actions/cache@v2
-        name: Configure npm caching
+        uses: actions/checkout@v3
+      - uses: actions/setup-node@v3
         with:
         with:
-          path: ~/.npm
-          key: ${{ runner.os }}-npm-${{ hashFiles('**/workflows/prettier.yml') }}
-          restore-keys: |
-            ${{ runner.os }}-npm-
+          node-version: 16
+          cache: "npm"
+      - run: npm ci
       - name: Run prettier
       - name: Run prettier
         run: |-
         run: |-
-          npx prettier --check .
+          npm run format:check

+ 3 - 3
.github/workflows/release.yml

@@ -10,18 +10,18 @@ jobs:
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
     steps:
     steps:
       - name: Checkout
       - name: Checkout
-        uses: actions/checkout@v2
+        uses: actions/checkout@v3
         with:
         with:
           fetch-depth: 0
           fetch-depth: 0
       - name: Setup Node.js
       - name: Setup Node.js
-        uses: actions/setup-node@v2
+        uses: actions/setup-node@v3
         with:
         with:
           node-version: "lts/*"
           node-version: "lts/*"
       - name: Install dependencies
       - name: Install dependencies
         run: npm ci
         run: npm ci
       - name: Import GPG key
       - name: Import GPG key
         id: import_gpg
         id: import_gpg
-        uses: crazy-max/ghaction-import-gpg@v4
+        uses: crazy-max/ghaction-import-gpg@v5
         with:
         with:
           gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
           gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
           passphrase: ${{ secrets.GPG_PASSPHRASE }}
           passphrase: ${{ secrets.GPG_PASSPHRASE }}

+ 36 - 0
.github/workflows/test.yml

@@ -0,0 +1,36 @@
+# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
+# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
+
+name: Node.js CI
+
+on:
+  push:
+    branches: ["master"]
+  pull_request:
+    branches: ["master"]
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+
+    strategy:
+      matrix:
+        node-version: [16.x, 18.x, 20.x]
+        # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
+
+    steps:
+      - uses: actions/checkout@v3
+      - name: Use Node.js ${{ matrix.node-version }}
+        uses: actions/setup-node@v3
+        with:
+          node-version: ${{ matrix.node-version }}
+          cache: "npm"
+      - run: npm ci
+      - run: npm run check
+      - run: npm run build
+      # - run: npm run lint
+      - run: npm run coverage
+      - name: Publish code coverage to CodeClimate
+        uses: paambaati/codeclimate-action@v5.0.0
+        env:
+          CC_TEST_REPORTER_ID: ${{secrets.CC_TEST_REPORTER_ID}}

+ 2 - 0
.gitignore

@@ -1,4 +1,5 @@
 lib-cov
 lib-cov
+coverage
 *.seed
 *.seed
 *.log
 *.log
 *.csv
 *.csv
@@ -20,6 +21,7 @@ npm-debug.log
 .DS_STORE
 .DS_STORE
 
 
 test/output
 test/output
+browserstack.err
 .tscache
 .tscache
 test/public
 test/public
 .vscode/
 .vscode/

+ 0 - 6
.parcelrc

@@ -1,6 +0,0 @@
-{
-  "extends": "@parcel/config-default",
-  "transformers": {
-    "*.{ts,tsx}": ["parcel-transformer-tsc-sourcemaps"]
-  }
-}

+ 120 - 117
README.md

@@ -102,15 +102,18 @@ peer.on("call", (call) => {
 npm test
 npm test
 ```
 ```
 
 
-## Browsers support
+## Browser support
 
 
-| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari |
-| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| last 4 versions                                                                                                                                                                                                   | last 4 versions                                                                                                                                                                                               | 12.1+                                                                                                                                                                                                         |
+| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)<br/>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)<br/>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)<br/>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)<br/>Safari |
+| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| 80+                                                                                                                                                                                                               | 83+                                                                                                                                                                                                           | 83+                                                                                                                                                                                                     | 15+                                                                                                                                                                                                           |
 
 
-## Safari
+We test PeerJS against these versions of Chrome, Edge, Firefox, and Safari with [BrowserStack](https://www.browserstack.com) to ensure compatibility.
+It may work in other and older browsers, but we don't officially support them.
+Changes to browser support will be a breaking change going forward.
 
 
-1. Safari supports only string data when sending via DataConnection. Use JSON serialization type if you want to communicate with Safari. By default, DataConnection uses Binary serialization type.
+> [!NOTE]  
+> Firefox 102+ is required for CBOR / MessagePack support.
 
 
 ## FAQ
 ## FAQ
 
 
@@ -137,122 +140,122 @@ This project exists thanks to all the people who contribute.
 
 
 Thank you to all our backers! [[Become a backer](https://opencollective.com/peer#backer)]
 Thank you to all our backers! [[Become a backer](https://opencollective.com/peer#backer)]
 
 
-<a href="https://opencollective.com/peer/backer/0/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/0/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/1/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/1/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/2/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/2/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/3/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/3/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/4/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/4/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/5/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/5/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/6/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/6/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/7/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/7/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/8/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/8/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/9/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/9/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/10/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/10/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/11/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/11/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/12/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/12/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/13/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/13/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/14/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/14/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/15/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/15/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/16/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/16/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/17/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/17/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/18/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/18/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/19/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/19/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/20/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/20/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/21/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/21/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/22/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/22/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/23/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/23/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/24/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/24/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/25/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/25/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/26/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/26/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/27/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/27/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/28/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/28/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/29/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/29/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/30/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/30/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/31/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/31/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/32/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/32/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/33/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/33/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/34/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/34/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/35/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/35/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/36/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/36/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/37/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/37/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/38/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/38/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/39/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/39/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/40/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/40/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/41/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/41/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/42/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/42/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/43/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/43/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/44/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/44/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/45/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/45/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/46/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/46/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/47/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/47/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/48/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/48/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/49/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/49/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/50/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/50/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/51/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/51/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/52/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/52/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/53/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/53/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/54/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/54/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/55/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/55/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/56/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/56/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/57/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/57/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/58/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/58/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/59/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/59/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/60/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/60/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/61/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/61/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/62/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/62/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/63/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/63/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/64/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/64/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/65/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/65/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/66/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/66/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/67/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/67/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/68/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/68/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/69/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/69/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/70/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/70/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/71/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/71/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/72/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/72/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/73/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/73/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/74/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/74/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/75/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/75/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/76/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/76/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/77/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/77/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/78/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/78/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/79/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/79/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/80/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/80/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/81/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/81/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/82/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/82/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/83/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/83/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/84/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/84/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/85/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/85/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/86/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/86/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/87/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/87/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/88/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/88/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/89/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/89/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/90/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/90/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/91/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/91/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/92/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/92/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/93/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/93/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/94/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/94/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/95/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/95/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/96/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/96/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/97/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/97/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/98/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/98/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/99/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/99/avatar.svg?requireActive=false"></a>
-<a href="https://opencollective.com/peer/backer/100/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/100/avatar.svg?requireActive=false"></a>
+<a href="https://opencollective.com/peer/backer/0/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/0/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/1/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/1/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/2/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/2/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/3/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/3/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/4/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/4/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/5/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/5/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/6/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/6/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/7/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/7/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/8/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/8/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/9/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/9/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/10/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/10/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/11/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/11/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/12/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/12/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/13/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/13/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/14/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/14/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/15/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/15/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/16/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/16/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/17/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/17/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/18/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/18/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/19/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/19/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/20/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/20/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/21/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/21/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/22/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/22/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/23/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/23/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/24/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/24/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/25/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/25/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/26/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/26/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/27/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/27/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/28/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/28/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/29/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/29/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/30/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/30/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/31/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/31/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/32/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/32/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/33/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/33/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/34/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/34/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/35/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/35/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/36/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/36/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/37/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/37/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/38/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/38/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/39/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/39/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/40/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/40/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/41/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/41/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/42/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/42/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/43/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/43/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/44/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/44/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/45/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/45/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/46/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/46/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/47/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/47/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/48/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/48/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/49/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/49/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/50/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/50/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/51/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/51/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/52/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/52/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/53/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/53/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/54/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/54/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/55/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/55/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/56/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/56/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/57/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/57/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/58/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/58/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/59/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/59/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/60/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/60/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/61/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/61/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/62/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/62/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/63/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/63/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/64/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/64/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/65/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/65/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/66/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/66/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/67/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/67/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/68/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/68/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/69/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/69/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/70/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/70/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/71/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/71/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/72/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/72/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/73/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/73/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/74/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/74/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/75/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/75/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/76/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/76/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/77/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/77/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/78/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/78/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/79/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/79/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/80/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/80/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/81/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/81/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/82/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/82/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/83/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/83/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/84/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/84/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/85/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/85/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/86/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/86/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/87/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/87/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/88/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/88/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/89/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/89/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/90/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/90/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/91/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/91/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/92/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/92/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/93/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/93/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/94/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/94/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/95/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/95/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/96/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/96/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/97/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/97/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/98/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/98/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/99/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/99/avatar.svg?requireActive=false"/></a>
+<a href="https://opencollective.com/peer/backer/100/website?requireActive=false" target="_blank"><img src="https://opencollective.com/peer/backer/100/avatar.svg?requireActive=false"/></a>
 
 
 ## Sponsors
 ## Sponsors
 
 
 Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/peer#sponsor)]
 Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/peer#sponsor)]
 
 
-<a href="https://opencollective.com/peer/sponsor/0/website" target="_blank"><img src="https://opencollective.com/peer/sponsor/0/avatar.svg"></a>
-<a href="https://opencollective.com/peer/sponsor/1/website" target="_blank"><img src="https://opencollective.com/peer/sponsor/1/avatar.svg"></a>
-<a href="https://opencollective.com/peer/sponsor/2/website" target="_blank"><img src="https://opencollective.com/peer/sponsor/2/avatar.svg"></a>
-<a href="https://opencollective.com/peer/sponsor/3/website" target="_blank"><img src="https://opencollective.com/peer/sponsor/3/avatar.svg"></a>
-<a href="https://opencollective.com/peer/sponsor/4/website" target="_blank"><img src="https://opencollective.com/peer/sponsor/4/avatar.svg"></a>
-<a href="https://opencollective.com/peer/sponsor/5/website" target="_blank"><img src="https://opencollective.com/peer/sponsor/5/avatar.svg"></a>
-<a href="https://opencollective.com/peer/sponsor/6/website" target="_blank"><img src="https://opencollective.com/peer/sponsor/6/avatar.svg"></a>
-<a href="https://opencollective.com/peer/sponsor/7/website" target="_blank"><img src="https://opencollective.com/peer/sponsor/7/avatar.svg"></a>
-<a href="https://opencollective.com/peer/sponsor/8/website" target="_blank"><img src="https://opencollective.com/peer/sponsor/8/avatar.svg"></a>
-<a href="https://opencollective.com/peer/sponsor/9/website" target="_blank"><img src="https://opencollective.com/peer/sponsor/9/avatar.svg"></a>
+<a href="https://opencollective.com/peer/sponsor/1/website" target="_blank"><img src="https://opencollective.com/peer/sponsor/1/avatar.svg"/></a>
+<a href="https://opencollective.com/peer/sponsor/2/website" target="_blank"><img src="https://opencollective.com/peer/sponsor/2/avatar.svg"/></a>
+<a href="https://opencollective.com/peer/sponsor/0/website" target="_blank"><img src="https://opencollective.com/peer/sponsor/0/avatar.svg"/></a>
+<a href="https://opencollective.com/peer/sponsor/3/website" target="_blank"><img src="https://opencollective.com/peer/sponsor/3/avatar.svg"/></a>
+<a href="https://opencollective.com/peer/sponsor/4/website" target="_blank"><img src="https://opencollective.com/peer/sponsor/4/avatar.svg"/></a>
+<a href="https://opencollective.com/peer/sponsor/5/website" target="_blank"><img src="https://opencollective.com/peer/sponsor/5/avatar.svg"/></a>
+<a href="https://opencollective.com/peer/sponsor/6/website" target="_blank"><img src="https://opencollective.com/peer/sponsor/6/avatar.svg"/></a>
+<a href="https://opencollective.com/peer/sponsor/7/website" target="_blank"><img src="https://opencollective.com/peer/sponsor/7/avatar.svg"/></a>
+<a href="https://opencollective.com/peer/sponsor/8/website" target="_blank"><img src="https://opencollective.com/peer/sponsor/8/avatar.svg"/></a>
+<a href="https://opencollective.com/peer/sponsor/9/website" target="_blank"><img src="https://opencollective.com/peer/sponsor/9/avatar.svg"/></a>
 
 
 ## License
 ## License
 
 

+ 1 - 1
test/faker.ts → __test__/faker.ts

@@ -4,7 +4,7 @@ import "webrtc-adapter";
 const fakeGlobals = {
 const fakeGlobals = {
 	WebSocket,
 	WebSocket,
 	MediaStream: class MediaStream {
 	MediaStream: class MediaStream {
-		private _tracks: MediaStreamTrack[] = [];
+		private readonly _tracks: MediaStreamTrack[] = [];
 
 
 		constructor(tracks?: MediaStreamTrack[]) {
 		constructor(tracks?: MediaStreamTrack[]) {
 			if (tracks) {
 			if (tracks) {

+ 12 - 12
test/logger.ts → __test__/logger.spec.ts

@@ -1,18 +1,18 @@
-import { expect } from "chai";
 import Logger, { LogLevel } from "../lib/logger";
 import Logger, { LogLevel } from "../lib/logger";
+import { expect, beforeAll, afterAll, describe, it } from "@jest/globals";
 
 
-describe("Logger", function () {
+describe("Logger", () => {
 	let oldLoggerPrint;
 	let oldLoggerPrint;
-	before(() => {
+	beforeAll(() => {
 		//@ts-ignore
 		//@ts-ignore
 		oldLoggerPrint = Logger._print;
 		oldLoggerPrint = Logger._print;
 	});
 	});
 
 
-	it("should be disabled by default", function () {
-		expect(Logger.logLevel).to.eq(LogLevel.Disabled);
+	it("should be disabled by default", () => {
+		expect(Logger.logLevel).toBe(LogLevel.Disabled);
 	});
 	});
 
 
-	it("should be accept new log level", function () {
+	it("should be accept new log level", () => {
 		const checkedLevels = [];
 		const checkedLevels = [];
 
 
 		Logger.setLogFunction((logLevel) => {
 		Logger.setLogFunction((logLevel) => {
@@ -21,16 +21,16 @@ describe("Logger", function () {
 
 
 		Logger.logLevel = LogLevel.Warnings;
 		Logger.logLevel = LogLevel.Warnings;
 
 
-		expect(Logger.logLevel).to.eq(LogLevel.Warnings);
+		expect(Logger.logLevel).toBe(LogLevel.Warnings);
 
 
 		Logger.log("");
 		Logger.log("");
 		Logger.warn("");
 		Logger.warn("");
 		Logger.error("");
 		Logger.error("");
 
 
-		expect(checkedLevels).to.deep.eq([LogLevel.Warnings, LogLevel.Errors]);
+		expect(checkedLevels).toEqual([LogLevel.Warnings, LogLevel.Errors]);
 	});
 	});
 
 
-	it("should accept new log function", function () {
+	it("should accept new log function", () => {
 		Logger.logLevel = LogLevel.All;
 		Logger.logLevel = LogLevel.All;
 
 
 		const checkedLevels = [];
 		const checkedLevels = [];
@@ -39,21 +39,21 @@ describe("Logger", function () {
 		Logger.setLogFunction((logLevel, ...args) => {
 		Logger.setLogFunction((logLevel, ...args) => {
 			checkedLevels.push(logLevel);
 			checkedLevels.push(logLevel);
 
 
-			expect(args[0]).to.eq(testMessage);
+			expect(args[0]).toBe(testMessage);
 		});
 		});
 
 
 		Logger.log(testMessage);
 		Logger.log(testMessage);
 		Logger.warn(testMessage);
 		Logger.warn(testMessage);
 		Logger.error(testMessage);
 		Logger.error(testMessage);
 
 
-		expect(checkedLevels).to.deep.eq([
+		expect(checkedLevels).toEqual([
 			LogLevel.All,
 			LogLevel.All,
 			LogLevel.Warnings,
 			LogLevel.Warnings,
 			LogLevel.Errors,
 			LogLevel.Errors,
 		]);
 		]);
 	});
 	});
 
 
-	after(() => {
+	afterAll(() => {
 		Logger.setLogFunction(oldLoggerPrint);
 		Logger.setLogFunction(oldLoggerPrint);
 	});
 	});
 });
 });

+ 222 - 0
__test__/peer.spec.ts

@@ -0,0 +1,222 @@
+import "./setup";
+import { Peer } from "../lib/peer";
+import { Server } from "mock-socket";
+import { ConnectionType, PeerErrorType, ServerMessageType } from "../lib/enums";
+import { expect, beforeAll, afterAll, describe, it } from "@jest/globals";
+
+const createMockServer = (): Server => {
+	const fakeURL = "ws://localhost:8080/peerjs?key=peerjs&id=1&token=testToken";
+	const mockServer = new Server(fakeURL);
+
+	mockServer.on("connection", (socket) => {
+		//@ts-ignore
+		socket.on("message", (data) => {
+			socket.send("test message from mock server");
+		});
+
+		socket.send(JSON.stringify({ type: ServerMessageType.Open }));
+	});
+
+	return mockServer;
+};
+describe("Peer", () => {
+	describe("after construct without parameters", () => {
+		it("shouldn't contains any connection", () => {
+			const peer = new Peer();
+
+			expect(peer.open).toBe(false);
+			expect(peer.connections).toEqual({});
+			expect(peer.id).toBeNull();
+			expect(peer.disconnected).toBe(false);
+			expect(peer.destroyed).toBe(false);
+
+			peer.destroy();
+		});
+	});
+
+	describe("after construct with parameters", () => {
+		it("should contains id and key", () => {
+			const peer = new Peer("1", { key: "anotherKey" });
+
+			expect(peer.id).toBe("1");
+			expect(peer.options.key).toBe("anotherKey");
+
+			peer.destroy();
+		});
+	});
+
+	describe.skip("after call to peer #2", () => {
+		let mockServer;
+
+		beforeAll(() => {
+			mockServer = createMockServer();
+		});
+
+		it("Peer#1 should has id #1", (done) => {
+			const peer1 = new Peer("1", { port: 8080, host: "localhost" });
+			expect(peer1.open).toBe(false);
+
+			const mediaOptions = {
+				metadata: { var: "123" },
+				constraints: {
+					mandatory: {
+						OfferToReceiveAudio: true,
+						OfferToReceiveVideo: true,
+					},
+				},
+			};
+
+			const track = new MediaStreamTrack();
+			const mediaStream = new MediaStream([track]);
+
+			const mediaConnection = peer1.call("2", mediaStream, { ...mediaOptions });
+
+			expect(typeof mediaConnection.connectionId).toBe("string");
+			expect(mediaConnection.type).toBe(ConnectionType.Media);
+			expect(mediaConnection.peer).toBe("2");
+			expect(mediaConnection.options).toEqual(
+				// expect.arrayContaining([mediaOptions]),mediaOptions
+				expect.objectContaining(mediaOptions),
+			);
+			expect(mediaConnection.metadata).toEqual(mediaOptions.metadata);
+			expect(mediaConnection.peerConnection.getSenders()[0].track.id).toBe(
+				track.id,
+			);
+
+			peer1.once("open", (id) => {
+				expect(id).toBe("1");
+				//@ts-ignore
+				expect(peer1._lastServerId).toBe("1");
+				expect(peer1.disconnected).toBe(false);
+				expect(peer1.destroyed).toBe(false);
+				expect(peer1.open).toBe(true);
+
+				peer1.destroy();
+
+				expect(peer1.disconnected).toBe(true);
+				expect(peer1.destroyed).toBe(true);
+				expect(peer1.open).toBe(false);
+				expect(peer1.connections).toEqual({});
+
+				done();
+			});
+		});
+
+		afterAll(() => {
+			mockServer.stop();
+		});
+	});
+
+	describe("reconnect", () => {
+		let mockServer;
+
+		beforeAll(() => {
+			mockServer = createMockServer();
+		});
+
+		it("connect to server => disconnect => reconnect => destroy", (done) => {
+			const peer1 = new Peer("1", { port: 8080, host: "localhost" });
+
+			peer1.once("open", () => {
+				expect(peer1.open).toBe(true);
+
+				peer1.once("disconnected", () => {
+					expect(peer1.disconnected).toBe(true);
+					expect(peer1.destroyed).toBe(false);
+					expect(peer1.open).toBe(false);
+
+					peer1.once("open", (id) => {
+						expect(id).toBe("1");
+						expect(peer1.disconnected).toBe(false);
+						expect(peer1.destroyed).toBe(false);
+						expect(peer1.open).toBe(true);
+
+						peer1.once("disconnected", () => {
+							expect(peer1.disconnected).toBe(true);
+							expect(peer1.destroyed).toBe(false);
+							expect(peer1.open).toBe(false);
+
+							peer1.once("close", () => {
+								expect(peer1.disconnected).toBe(true);
+								expect(peer1.destroyed).toBe(true);
+								expect(peer1.open).toBe(false);
+
+								done();
+							});
+						});
+
+						peer1.destroy();
+					});
+
+					peer1.reconnect();
+				});
+
+				peer1.disconnect();
+			});
+		});
+
+		it("disconnect => reconnect => destroy", (done) => {
+			mockServer.stop();
+
+			const peer1 = new Peer("1", { port: 8080, host: "localhost" });
+
+			peer1.once("disconnected", (id) => {
+				expect(id).toBe("1");
+				expect(peer1.disconnected).toBe(true);
+				expect(peer1.destroyed).toBe(false);
+				expect(peer1.open).toBe(false);
+
+				peer1.once("open", (id) => {
+					expect(id).toBe("1");
+					expect(peer1.disconnected).toBe(false);
+					expect(peer1.destroyed).toBe(false);
+					expect(peer1.open).toBe(true);
+
+					peer1.once("disconnected", () => {
+						expect(peer1.disconnected).toBe(true);
+						expect(peer1.destroyed).toBe(false);
+						expect(peer1.open).toBe(false);
+
+						peer1.once("close", () => {
+							expect(peer1.disconnected).toBe(true);
+							expect(peer1.destroyed).toBe(true);
+							expect(peer1.open).toBe(false);
+
+							done();
+						});
+					});
+
+					peer1.destroy();
+				});
+
+				mockServer = createMockServer();
+
+				peer1.reconnect();
+			});
+		});
+
+		it("destroy peer if no id and no connection", (done) => {
+			mockServer.stop();
+
+			const peer1 = new Peer({ port: 8080, host: "localhost" });
+
+			peer1.once("error", (error) => {
+				expect(error.type).toBe(PeerErrorType.ServerError);
+
+				peer1.once("close", () => {
+					expect(peer1.disconnected).toBe(true);
+					expect(peer1.destroyed).toBe(true);
+					expect(peer1.open).toBe(false);
+
+					done();
+				});
+
+				mockServer = createMockServer();
+			});
+		});
+
+		afterAll(() => {
+			mockServer.stop();
+		});
+	});
+});

+ 0 - 0
test/setup.ts → __test__/setup.ts


+ 11 - 0
__test__/util.spec.ts

@@ -0,0 +1,11 @@
+import "./setup";
+import { util } from "../lib/util";
+import { expect, describe, it } from "@jest/globals";
+
+describe("util", () => {
+	describe("#chunkedMTU", () => {
+		it("should be 16300", () => {
+			expect(util.chunkedMTU).toBe(16300);
+		});
+	});
+});

+ 0 - 552
docs/api.json

@@ -1,552 +0,0 @@
-[
-  {
-    "name": "Peer",
-    "type": "constructor",
-    "snippet": "const peer = new Peer([id], [options]);",
-    "description": "A peer can connect to other peers and listen for connections.",
-    "children": [
-      {
-        "name": "id",
-        "optional": true,
-        "type": "string",
-        "description": "Other peers can connect to this peer using the provided ID. If no ID is given, one will be generated by the brokering server. The ID must start and end with an alphanumeric character (lower or upper case character or a digit). In the middle of the ID spaces, dashes (-) and underscores (_) are allowed.<span class='warn'>It's not recommended that you use this ID to identify peers, as it's meant to be used for brokering connections only. You're recommended to set the <a href='#peerconnect-options'><code>metadata</code></a> option to send other identifying information.</span>"
-      },
-      {
-        "name": "options",
-        "optional": true,
-        "type": "object",
-        "children": [
-          {
-            "name": "key",
-            "type": "string",
-            "description": "API key for the cloud PeerServer. This is not used for servers other than <code>0.peerjs.com</code>.<span class='warn'>PeerServer cloud runs on port 443. Please ensure it is not blocked or consider running your own PeerServer instead.</span>"
-          },
-          {
-            "name": "host",
-            "type": "string",
-            "description": "Server host. Defaults to <code>0.peerjs.com</code>. Also accepts <code>'/'</code> to signify relative hostname."
-          },
-          {
-            "name": "port",
-            "type": "number",
-            "description": "Server port. Defaults to <code>443</code>."
-          },
-          {
-            "name": "pingInterval",
-            "type": "number",
-            "description": "Ping interval in ms. Defaults to <code>5000</code>."
-          },
-          {
-            "name": "path",
-            "type": "string",
-            "description": "The path where your self-hosted PeerServer is running. Defaults to <code>'/'</code>."
-          },
-          {
-            "name": "secure",
-            "type": "boolean",
-            "description": "<code>true</code> if you're using SSL.<span class='tip'>Note that our cloud-hosted server and assets may not support SSL.</span>"
-          },
-          {
-            "name": "config",
-            "type": "object",
-            "description": "Configuration hash passed to RTCPeerConnection. This hash contains any custom ICE/TURN server configuration. Defaults to <code>{ 'iceServers': [{ 'urls': 'stun:stun.l.google.com:19302' }, { 'urls': 'turn:0.peerjs.com:3478', username: 'peerjs', credential: 'peerjsp' }], 'sdpSemantics': 'unified-plan' }</code>"
-          },
-          {
-            "name": "debug",
-            "type": "number",
-            "description": "Prints log messages depending on the debug level passed in. Defaults to <code>0</code>.",
-            "children": [
-              {
-                "name": 0,
-                "description": "Prints no logs."
-              },
-              {
-                "name": 1,
-                "description": "Prints only errors."
-              },
-              {
-                "name": 2,
-                "description": "Prints errors and warnings."
-              },
-              {
-                "name": 3,
-                "description": "Prints all logs."
-              }
-            ]
-          }
-        ]
-      }
-    ]
-  },
-  {
-    "name": "peer.connect",
-    "type": "method",
-    "snippet": "const <a href='#dataconnection'>dataConnection</a> = peer.connect(id, [options]);",
-    "description": "Connects to the remote peer specified by <code>id</code> and returns a data connection. Be sure to listen on the <a href='#peeron-error'><code>error</code></a> event in case the connection fails.",
-    "children": [
-      {
-        "name": "id",
-        "type": "string",
-        "description": "The brokering ID of the remote peer (their <a href='#peerid'><code>peer.id</code></a>)."
-      },
-      {
-        "name": "options",
-        "optional": true,
-        "type": "object",
-        "children": [
-          {
-            "name": "label",
-            "type": "string",
-            "description": "A unique label by which you want to identify this data connection. If left unspecified, a label will be generated at random. Can be accessed with <a href='#dataconnection-label'><code>dataConnection.label</code></a>."
-          },
-          {
-            "name": "metadata",
-            "description": "Metadata associated with the connection, passed in by whoever initiated the connection. Can be accessed with <a href='#dataconnection-metadata'><code>dataConnection.metadata</code></a>. Can be any serializable type."
-          },
-          {
-            "name": "serialization",
-            "type": "string",
-            "description": "Can be <code>binary</code> (default), <code>binary-utf8</code>, <code>json</code>, or <code>none</code>. Can be accessed with <a href='#dataconnection-serialization'><code>dataConnection.serialization</code></a>.<span class='tip'><code>binary-utf8</code> will take a performance hit because of the way UTF8 strings are packed into binary format.</span>"
-          },
-          {
-            "name": "reliable",
-            "type": "boolean",
-            "description": "Whether the underlying data channels should be reliable (e.g. for large file transfers) or not (e.g. for gaming or streaming). Defaults to <code>false</code>.<span class='warn'>Setting reliable to true will use a shim for incompatible browsers (Chrome 30 and below only) and thus may not offer full performance.</span>"
-          }
-        ]
-      }
-    ]
-  },
-  {
-    "name": "peer.call",
-    "type": "method",
-    "snippet": "const <a href='#mediaconnection'>mediaConnection</a> = peer.call(id, stream, [options]);",
-    "description": "Calls the remote peer specified by <code>id</code> and returns a media connection. Be sure to listen on the <a href='#peeron-error'><code>error</code></a> event in case the connection fails.",
-    "children": [
-      {
-        "name": "id",
-        "type": "string",
-        "description": "The brokering ID of the remote peer (their <a href='#peerid'><code>peer.id</code></a>)."
-      },
-      {
-        "name": "stream",
-        "type": "MediaStream",
-        "description": "The caller's media stream"
-      },
-      {
-        "name": "options",
-        "optional": true,
-        "type": "object",
-        "children": [
-          {
-            "name": "metadata",
-            "description": "Metadata associated with the connection, passed in by whoever initiated the connection. Can be accessed with <a href='#mediaconnection-metadata'><code>mediaConnection.metadata</code></a>. Can be any serializable type."
-          },
-          {
-            "name": "sdpTransform",
-            "type": "method",
-            "description": "Function which runs before create offer to modify sdp offer message."
-          }
-        ]
-      }
-    ]
-  },
-  {
-    "name": "peer.on",
-    "type": "method",
-    "snippet": "peer.on(event, callback);",
-    "description": "Set listeners for peer events.",
-    "children": [
-      {
-        "name": "'open'",
-        "type": "event",
-        "snippet": "peer.on('open', function(id) { ... });",
-        "description": "Emitted when a connection to the PeerServer is established. You may use the peer before this is emitted, but messages to the server will be queued. <code>id</code> is the brokering ID of the peer (which was either provided in the constructor or assigned by the server).<span class='tip'>You should not wait for this event before connecting to other peers if connection speed is important.</span>"
-      },
-      {
-        "name": "'connection'",
-        "type": "event",
-        "snippet": "peer.on('connection', function(<a href='#dataconnection'>dataConnection</a>) { ... });",
-        "description": "Emitted when a new data connection is established from a remote peer."
-      },
-      {
-        "name": "'call'",
-        "type": "event",
-        "snippet": "peer.on('call', function(<a href='#mediaconnection'>mediaConnection</a>) { ... });",
-        "description": "Emitted when a remote peer attempts to call you. The emitted <code>mediaConnection</code> is not yet active; you must first answer the call (<a href='#mediaconnection-answer'><code>mediaConnection.answer([stream]);</code></a>). Then, you can listen for the <a href='#mediaconnection-on'><code>stream</code></a> event."
-      },
-      {
-        "name": "'close'",
-        "type": "event",
-        "snippet": "peer.on('close', function() { ... });",
-        "description": "Emitted when the peer is <a href='#peerdestroy'>destroyed</a> and can no longer accept or create any new connections. At this time, the peer's connections will all be closed. <span class='tip'>To be extra certain that peers clean up correctly, we recommend calling <code>peer.destroy()</code> on a peer when it is no longer needed.</span>"
-      },
-      {
-        "name": "'disconnected'",
-        "type": "event",
-        "snippet": "peer.on('disconnected', function() { ... });",
-        "description": "Emitted when the peer is disconnected from the signalling server, either <a href='#peerdisconnect'>manually</a> or because the connection to the signalling server was lost. When a peer is disconnected, its existing connections will stay alive, but the peer cannot accept or create any new connections. You can reconnect to the server by calling <a href='#peerreconnect'><code>peer.reconnect()</code></a>."
-      },
-      {
-        "name": "'error'",
-        "type": "event",
-        "snippet": "peer.on('error', function(err) { ... });",
-        "description": "Errors on the peer are <strong>almost always fatal</strong> and will destroy the peer. Errors from the underlying socket and PeerConnections are forwarded here.<br><br>These come in the following <code>err.type</code> flavors:",
-        "children": [
-          {
-            "name": "'browser-incompatible'",
-            "type": "Error",
-            "tags": [
-              "fatal"
-            ],
-            "description": "The client's browser does not support some or all WebRTC features that you are trying to use."
-          },
-          {
-            "name": "'disconnected'",
-            "type": "Error",
-            "description": "You've already disconnected this peer from the server and can no longer make any new connections on it."
-          },
-          {
-            "name": "'invalid-id'",
-            "type": "Error",
-            "tags": [
-              "fatal"
-            ],
-            "description": "The ID passed into the Peer constructor contains illegal characters."
-          },
-          {
-            "name": "'invalid-key'",
-            "type": "Error",
-            "tags": [
-              "fatal"
-            ],
-            "description": "The API key passed into the Peer constructor contains illegal characters or is not in the system (cloud server only)."
-          },
-          {
-            "name": "'network'",
-            "type": "Error",
-            "description": "Lost or cannot establish a connection to the signalling server."
-          },
-          {
-            "name": "'peer-unavailable'",
-            "type": "Error",
-            "description": "The peer you're trying to connect to does not exist."
-          },
-          {
-            "name": "'ssl-unavailable'",
-            "type": "Error",
-            "tags": [
-              "fatal"
-            ],
-            "description": "PeerJS is being used securely, but the cloud server does not support SSL. Use a custom PeerServer."
-          },
-          {
-            "name": "'server-error'",
-            "type": "Error",
-            "tags": [
-              "fatal"
-            ],
-            "description": "Unable to reach the server."
-          },
-          {
-            "name": "'socket-error'",
-            "type": "Error",
-            "tags": [
-              "fatal"
-            ],
-            "description": "An error from the underlying socket."
-          },
-          {
-            "name": "'socket-closed'",
-            "type": "Error",
-            "tags": [
-              "fatal"
-            ],
-            "description": "The underlying socket closed unexpectedly."
-          },
-          {
-            "name": "'unavailable-id'",
-            "type": "Error",
-            "tags": [
-              "sometimes fatal"
-            ],
-            "description": "The ID passed into the Peer constructor is already taken.<span class='warn'>This error is not fatal if your peer has open peer-to-peer connections. This can happen if you attempt to <a href='#peerreconnect'>reconnect</a> a peer that has been <a href='#peerdisconnect'>disconnected from the server</a>, but its old ID has now been taken.</span>"
-          },
-          {
-            "name": "'webrtc'",
-            "type": "Error",
-            "description": "Native WebRTC errors."
-          }
-        ]
-      }
-    ]
-  },
-  {
-    "name": "peer.disconnect",
-    "type": "method",
-    "snippet": "peer.disconnect();",
-    "description": "Close the connection to the server, leaving all existing data and media connections intact. <a href='#peerdisconnected'><code>peer.disconnected</code></a> will be set to <code>true</code> and the <a href='#peeron-disconnected'><code>disconnected</code></a> event will fire.<span class='warn'>This cannot be undone; the respective peer object will no longer be able to create or receive any connections and its ID will be forfeited on the (cloud) server.</span>"
-  },
-  {
-    "name": "peer.reconnect",
-    "type": "method",
-    "snippet": "peer.reconnect();",
-    "description": "Attempt to reconnect to the server with the peer's old ID. Only <a href='#peerdisconnect'>disconnected peers</a> can be reconnected. Destroyed peers cannot be reconnected. If the connection fails (as an example, if the peer's old ID is now taken), the peer's existing connections will not close, but any associated errors events will fire."
-  },
-  {
-    "name": "peer.destroy",
-    "type": "method",
-    "snippet": "peer.destroy();",
-    "description": "Close the connection to the server and terminate all existing connections. <a href='#peerdestroyed'><code>peer.destroyed</code></a> will be set to <code>true</code>.<span class='warn'>This cannot be undone; the respective peer object will no longer be able to create or receive any connections, its ID will be forfeited on the (cloud) server, and all of its data and media connections will be closed.</span>"
-  },
-  {
-    "name": "peer.id",
-    "type": "string",
-    "description": "The brokering ID of this peer. If no ID was specified in <a href='#peer'>the constructor</a>, this will be <code>undefined</code> until the <a href='#peeron-open'><code>open</code></a> event is emitted."
-  },
-  {
-    "name": "peer.connections",
-    "type": "object",
-    "description": "A hash of all connections associated with this peer, keyed by the remote peer's ID.<span class='tip'>We recommend keeping track of connections yourself rather than relying on this hash.</span>"
-  },
-  {
-    "name": "peer.disconnected",
-    "type": "boolean",
-    "description": "<code>false</code> if there is an active connection to the PeerServer."
-  },
-  {
-    "name": "peer.destroyed",
-    "type": "boolean",
-    "description": "<code>true</code> if this peer and all of its connections can no longer be used."
-  },
-  {
-    "name": "DataConnection",
-    "type": "class",
-    "description": "Wraps WebRTC's DataChannel. To get one, use <a href='#peerconnect'><code>peer.connect</code></a> or listen for the <a href='#peeron-connect'><code>connect</code></a> event.",
-    "children": [
-      {
-        "name": ".send",
-        "type": "method",
-        "snippet": "dataConnection.send(data);",
-        "description": "<code>data</code> is serialized by BinaryPack by default and sent to the remote peer.",
-        "children": {
-          "name": "data",
-          "description": "You can send any type of data, including objects, strings, and blobs."
-        }
-      },
-      {
-        "name": ".close",
-        "type": "method",
-        "snippet": "dataConnection.close();",
-        "description": "Closes the data connection gracefully, cleaning up underlying DataChannels and PeerConnections."
-      },
-      {
-        "name": ".on",
-        "type": "method",
-        "snippet": "dataConnection.on(event, callback);",
-        "description": "Set listeners for data connection events.",
-        "children": [
-          {
-            "name": "'data'",
-            "type": "event",
-            "snippet": "dataConnection.on('data', function(data) { ... });",
-            "description": "Emitted when data is received from the remote peer."
-          },
-          {
-            "name": "'open'",
-            "type": "event",
-            "snippet": "dataConnection.on('open', function() { ... });",
-            "description": "Emitted when the connection is established and ready-to-use."
-          },
-          {
-            "name": "'close'",
-            "type": "event",
-            "snippet": "dataConnection.on('close', function() { ... });",
-            "description": "Emitted when either you or the remote peer closes the data connection.<span class='warn'>Firefox does not yet support this event.</span>"
-          },
-          {
-            "name": "'error'",
-            "type": "event",
-            "snippet": "dataConnection.on('error', function(err) { ... });"
-          }
-        ]
-      },
-      {
-        "name": ".dataChannel",
-        "type": "object",
-        "description": "A reference to the RTCDataChannel object associated with the connection."
-      },
-      {
-        "name": ".label",
-        "type": "string",
-        "description": "The optional label passed in or assigned by PeerJS when the connection was initiated."
-      },
-      {
-        "name": ".metadata",
-        "description": "Any type of metadata associated with the connection, passed in by whoever initiated the connection."
-      },
-      {
-        "name": ".open",
-        "type": "boolean",
-        "description": "This is true if the connection is open and ready for read/write."
-      },
-      {
-        "name": ".peerConnection",
-        "type": "object",
-        "description": "A reference to the RTCPeerConnection object associated with the connection."
-      },
-      {
-        "name": ".peer",
-        "type": "string",
-        "description": "The ID of the peer on the other end of this connection."
-      },
-      {
-        "name": ".reliable",
-        "type": "boolean",
-        "description": "Whether the underlying data channels are reliable; defined when the connection was initiated."
-      },
-      {
-        "name": ".serialization",
-        "type": "string",
-        "description": "The serialization format of the data sent over the connection. Can be <code>binary</code> (default), <code>binary-utf8</code>, <code>json</code>, or <code>none</code>."
-      },
-      {
-        "name": ".type",
-        "type": "string",
-        "description": "For data connections, this is always <code>'data'</code>."
-      },
-      {
-        "name": ".bufferSize",
-        "type": "number",
-        "description": "The number of messages queued to be sent once the browser buffer is no longer full."
-      }
-    ]
-  },
-  {
-    "name": "MediaConnection",
-    "type": "class",
-    "description": "Wraps WebRTC's media streams. To get one, use <a href='#peercall'><code>peer.call</code></a> or listen for the <a href='#peeron-call'><code>call</code></a> event.",
-    "children": [
-      {
-        "name": ".answer",
-        "type": "method",
-        "snippet": "mediaConnection.answer([stream],[options]);",
-        "description": "When receiving a <a href='#peeron-call'><code>call</code></a> event on a peer, you can call <code>.answer</code> on the media connection provided by the callback to accept the call and optionally send your own media stream.",
-        "children": [
-          {
-            "name": "stream",
-            "optional": true,
-            "type": "MediaStream",
-            "description": "A WebRTC media stream from <a href='https://developer.mozilla.org/en-US/docs/Web/API/Navigator.getUserMedia'><code>getUserMedia</code></a>."
-          },
-          {
-            "name": "options",
-            "optional": true,
-            "type": "object",
-            "children": [
-              {
-                "name": "sdpTransform",
-                "type": "method",
-                "description": "Function which runs before create answer to modify sdp answer message."
-              }
-            ]
-          }
-        ]
-      },
-      {
-        "name": ".close",
-        "type": "method",
-        "snippet": "mediaConnection.close();",
-        "description": "Closes the media connection."
-      },
-      {
-        "name": ".on",
-        "type": "method",
-        "snippet": "mediaConnection.on(event, callback);",
-        "description": "Set listeners for media connection events.",
-        "children": [
-          {
-            "name": "'stream'",
-            "type": "event",
-            "snippet": "mediaConnection.on('stream', function(stream) { ... });",
-            "description": "Emitted when a remote peer adds a <code>stream</code>."
-          },
-          {
-            "name": "'close'",
-            "type": "event",
-            "snippet": "mediaConnection.on('close', function() { ... });",
-            "description": "Emitted when either you or the remote peer closes the media connection. <span class='warn'>Firefox does not yet support this event.</span>"
-          },
-          {
-            "name": "'error'",
-            "type": "event",
-            "snippet": "mediaConnection.on('error', function(err) { ... });"
-          }
-        ]
-      },
-      {
-        "name": ".open",
-        "type": "boolean",
-        "description": "Whether the media connection is active (e.g. your call has been answered). You can check this if you want to set a maximum wait time for a one-sided call."
-      },
-      {
-        "name": ".metadata",
-        "description": "Any type of metadata associated with the connection, passed in by whoever initiated the connection."
-      },
-      {
-        "name": ".peer",
-        "type": "string",
-        "description": "The ID of the peer on the other end of this connection."
-      },
-      {
-        "name": ".type",
-        "type": "string",
-        "description": "For media connections, this is always <code>'media'</code>."
-      }
-    ]
-  },
-  {
-    "name": "util",
-    "type": "object",
-    "tags": [
-      "utility"
-    ],
-    "description": "Provides a variety of helpful utilities.<span class='warn'>Only the utilities documented here are guaranteed to be present on <code>util</code>. Undocumented utilities can be removed without warning. We don't consider these to be 'breaking changes.'</span>",
-    "children": [
-      {
-        "name": ".browser",
-        "type": "string",
-        "snippet": "if (util.browser === 'firefox') { /* OK to peer with Firefox peers. */ }",
-        "description": "The current browser. This property can be useful in determining whether or not two peers can connect. For example, as of now data connections are not yet interoperable between major browsers. <code>util.browser</code> can currently have the values <code>'firefox'</code>, <code>'chrome'</code>, <code>'safari'</code>, <code>'edge'</code>, <code>'Not a supported browser.'</code>, or <code>'Not a browser.'</code> (unknown WebRTC-compatible agent)."
-      },
-      {
-        "name": ".supports",
-        "type": "object",
-        "snippet": "if (util.supports.data) { /* OK to start a data connection. */ }",
-        "description": "A hash of WebRTC features mapped to booleans that correspond to whether the feature is supported by the current browser.<span class='warn'>Only the properties documented here are guaranteed to be present on <code>util.supports</code>.</span>",
-        "children": [
-          {
-            "name": ".audioVideo",
-            "type": "boolean",
-            "description": "True if the current browser supports media streams and PeerConnection."
-          },
-          {
-            "name": ".data",
-            "type": "boolean",
-            "description": "True if the current browser supports DataChannel and PeerConnection."
-          },
-          {
-            "name": ".binary",
-            "type": "boolean",
-            "description": "True if the current browser supports binary DataChannels."
-          },
-          {
-            "name": ".reliable",
-            "type": "boolean",
-            "description": "True if the current browser supports reliable DataChannels."
-          }
-        ]
-      }
-    ]
-  }
-]

+ 0 - 4
docs/api.md

@@ -1,4 +0,0 @@
-# PeerJS API Reference
-
-We've moved! <a href="https://peerjs.com/docs.html#api">Check out our new API
-reference.</a>

+ 0 - 13
docs/build.js

@@ -1,13 +0,0 @@
-const fs = require('fs');
-const handlebars = require('handlebars');
-const reference = require('reference');
-
-const file = fs.readFileSync('./api.json');
-
-const template = handlebars.compile(
-  fs.readFileSync('./template.html', { encoding: 'utf8' })
-);
-fs.writeFileSync(
-  './index.html',
-  template({ html: reference(file, { anchor: true }) })
-);

+ 0 - 393
docs/css/docs.css

@@ -1,393 +0,0 @@
-html{color:#000;background:#FFF;}body,div,dl,dt,dd,ul,ol,li,pre,code,form,fieldset,legend,input,button,textarea,p,blockquote,th,td{margin:0;padding:0;}table{border-collapse:collapse;border-spacing:0;}fieldset,img{border:0;}address,caption,cite,code,dfn,em,th,var,optgroup{font-style:inherit;font-weight:inherit;}del,ins{text-decoration:none;}li{list-style:none;}caption,th{text-align:left;}q:before,q:after{content:'';}abbr,acronym{border:0;font-variant:normal;}sup{vertical-align:baseline;}sub{vertical-align:baseline;}legend{color:#000;}input,button,textarea,select,optgroup,option{font-family:inherit;font-size:inherit;font-style:inherit;font-weight:inherit;}input,button,textarea,select{font-size:100%;}
-a {outline: none;} /* Gets rid of Firefox's dotted borders */
-a img {border: none;} /* Gets rid of IE's blue borders */
-
-
-body, html {
-  font-size: 14px;
-  line-height: 24px;
-  font-family: "Lato", Helvetica, sans-serif;
-  color: #454545;
-}
-
-header.right, header.left {
-  cursor: pointer;
-  z-index: 100;
-  position: fixed;
-  top: 0;
-  height: 35px;
-  box-sizing: border-box;
-  -moz-box-sizing: border-box;
-}
-
-header.left {
-  border-bottom: 1px solid rgba(0,0,0,0.1);
-  border-right: 4px solid rgba(0,0,0,0.1);
-  background-color: #50484e;
-  text-align: right;
-  left: 0;
-  width: 47%;
-}
-header.right {
-  border-bottom: 1px solid rgba(0,0,0,0.1);
-  border-left: 4px solid rgba(0,0,0,0.1);
-  background-color: #eee;
-  text-align: left;
-  right: 0;
-  width: 53%;
-}
-
-.left h2, .right h2 {
-  color: #E2A62E;
-  letter-spacing: 1px;
-  text-transform: uppercase;
-  font-size: 14px;
-  margin: 0;
-  padding: 6px 10px;
-  text-shadow: 0 -1px 0 rgba(0,0,0,0.2);
-}
-
-.right h2 {
-  color: #5a5157;
-  text-shadow: 0 -1px 0 #fff;
-}
-
-.left .icon {
-  color: #E2A62E;
-  cursor: pointer;
-  font-family: monospace;
-  font-size: 20px;
-  display: inline-block;
-  margin-left: 5px;
-}
-
-.icon.show {
-  display: none;
-}
-
-
-.api, .start {
-  position: absolute;
-  top: 0px;
-  bottom: 0px;
-  overflow-y: scroll;
-  overflow-x: hidden;
-  box-sizing: border-box;
-  -moz-box-sizing: border-box;
-  transition: left 300ms;
-}
-
-.api.fullscreen {
-  width: 100%;
-}
-
-.start.full {
-  left: 30px;
-  width: auto;
-}
-
-/** hiding */
-.api.hidden {
-  left: -370px;
-  width: 400px;
-  overflow: hidden;
-}
-
-.api.hidden > div {
-  opacity: 0.6;
-}
-
-.start {
-  background-color: #fcfcfc;
-  width: 53%;
-  right: 0px;
-  top: 35px;
-  text-shadow: 0px -1px 0 #fff;
-  border-top: 1px solid #fff;
-  z-index: 99;
-  color: #645b61;
-}
-
-.start h1 {
-  margin: 0;
-  background-color: #fff;
-  font-size: 40px;
-  line-height: 40px;
-  padding: 30px 20px;
-  border-bottom: 1px solid #eee;
-  color: #5a5157;
-  border-left: 4px solid #ddd;
-}
-
-/** Code stylings */
-section.start pre {
-  font-family: Consolas, Inconsolata, 'Bitstream Vera Sans Mono', Menlo, Monaco, 'Andale Mono', 'Courier New', monospace;
-  font-size: 12px;
-  background-color: #474045;
-  border-left: 4px solid #40393e;
-  padding: 15px 30px 15px 40px;
-  color: #e7e0e5;
-  text-shadow: 0 -1px 0 rgba(0,0,0,0.1);
-  max-width: 100%;
-  overflow: auto;
-}
-/** /code */
-
-h1 a {
-  color: #5a5157;
-  text-decoration: none;
-  transition: color 300ms;
-}
-
-h1 a:hover {
-  color: #745e6d;
-}
-
-h1 .title {
-  color: #fff;
-  display: inline-block;
-  font-size: 15px;
-  text-transform: uppercase;
-  font-variant: small-caps;
-  background-color: #E2A62E;
-  padding: 2px 4px;
-  line-height: 15px;
-  border-radius: 2px;
-  border: 2px solid rgba(0,0,0,0.2);
-  text-shadow: 0 -1px 0 rgba(0,0,0,0.2);
-}
-
-.start > p, .start > div {
-  padding: 5px 30px 5px 40px;
-  border-left: 4px solid #ddd;
-}
-
-.start p.red {
-  color: #8F4537;
-}
-
-.start h2, .start h3, .start h4 {
-  color: #5a5157;
-  padding: 12px 25px 5px 25px;
-  margin: 0;
-  font-size: 20px;
-  border-left: 4px solid #ccc;
-}
-
-.start h3 {
-  font-size: 15px;
-  padding: 12px 25px 5px 25px;
-  border-bottom: 1px solid #eee;
-}
-
-a {
-  font-weight: 600;
-  color: #1295D8;
-  text-decoration: none;
-  transition: color 200ms;
-}
-a:hover {
-  color: #33a2dc;
-}
-
-
-.api {
-  color: #f5eff3;
-  font-weight: 300;
-  top: 35px;
-  left: 0px;
-  width: 47%;
-  border-right: 4px solid #474046;
-  border-top: 1px solid rgba(255,255,255,0.1);
-  background-color: #5a5157;
-  text-shadow: 0px -1px 0 #474045;
-  z-index: 100;
-}
-
-.child {
-  padding: 10px 0 10px 15px;
-}
-
-.bracket {
-  font-weight: 800;
-  display: inline-block;
-  margin: 0 2px;
-  color: rgba(255,255,255,0.2);
-}
-
-.toplevel {
-  border-bottom: 1px solid rgba(0,0,0,0.1);
-  padding: 30px 35px;
-}
-
-.api:not(:first-child) {
-  border-top: 1px solid rgba(255,255,255,0.1);
-}
-
-.toplevel > .children > .child {
-  padding-right: 5px;
-}
-
-.children {
-  border-left: 1px solid rgba(0,0,0,0.1);
-}
-
-.beta_030 .children {
-  border-left: 1px solid rgba(233, 97, 81, 0.5);
-}
-
-.api div p {
-  margin: 0 0 5px 0;
-}
-
-.child:hover {
-  background-color: #635960;
-}
-
-/** Label stylings */
-.tag {
-  display: inline-block;
-  background-color: #454545;
-  border-radius: 2px;
-  color: rgba(255,255,255,0.8);
-  line-height: 11px;
-  padding: 1px 2px;
-  margin-left: 5px;
-  font-size: 9px;
-  letter-spacing: 1px;
-  font-weight: 600;
-  font-family: "Lato", Helvetica, sans-serif;
-  text-transform: uppercase;
-  border: 1px solid rgba(0,0,0,0.4);
-  text-shadow: 0px -1px 0px rgba(0,0,0,0.2);
-}
-
-.tag.type {
-  background-color: #757E2B;
-}
-
-.tag.beta_030 {
-  background-color: #E96151;
-}
-
-.tag.method, .tag.function {
-  background-color: #E2A62E;
-}
-
-.tag.class, .tag.constructor, .tag.utility {
-  background-color: #468F81;
-}
-
-.tag.error {
-  background-color: #8F4537;
-}
-
-.tag.event {
-  background-color: #1295D8;
-}
-
-.toplevel > .name {
-  font-size: 20px;
-}
-
-/** /Label stylings */
-
-.api .snippet {
-  color: #a2949d;
-  border-left: 1px solid #40393e;
-  border-top: 1px solid #40393e;
-  border-right: 1px solid #6b666a;
-  border-bottom: 1px solid #6b666a;
-  text-shadow: 0 -1px 0 #393438;
-  font-size: 12px;
-  font-family: Consolas, Inconsolata, 'Bitstream Vera Sans Mono', Menlo, Monaco, 'Andale Mono', 'Courier New', monospace;
-  font-weight: 400;
-  display: inline-block;
-  background-color: #474045;
-  padding: 3px 8px;
-  margin: 5px 0 0 10px;
-}
-
-/* We want to be able to use the names as an anchor. */
-.api .name {
-  font-weight: 600;
-  display: inline-block;
-  margin-bottom: 5px;
-}
-
-.name a {
-  cursor: pointer;
-  color: #fff;
-  text-decoration: none;
-  transition: color 300ms;
-}
-
-.name a:hover {
-  color: #e7e0e5;
-}
-/* /name */
-
-.tip, .warn {
-  opacity: 0.9;
-  display: block;
-  background-color: #d1c7be;
-  font-size: 13px;
-  line-height: 18px;
-  border-radius: 2px;
-  padding: 5px 8px;
-  border: 2px solid rgba(0,0,0,0.2);
-  margin: 8px 8px 0 0;
-}
-
-.warn {
-  background-color: #8F4537;
-}
-.tip {
-  color: #544e4a;
-  text-shadow: 0px -1px 0px rgba(255,255,255,0.2);
-  font-weight: 600;
-}
-
-#peer-options-debug .child {
-  padding: 0 15px;
-}
-
-#peer-options-debug .child .description {
-  display: inline-block;
-  margin-left: 10px;
-}
-
-.start code, .api code {
-  font-family: Consolas, Inconsolata, 'Bitstream Vera Sans Mono', Menlo, Monaco, 'Andale Mono', 'Courier New', monospace;
-  border-radius: 2px;
-  border: 1px solid rgba(255,255,255,0.2);
-  background: rgba(255,255,255,0.1);
-  font-size: 12px;
-  padding: 2px;
-}
-
-.start code {
-  background-color: #f6eee8;
-  border: 1px solid #d1c7be;
-}
-
-.start > .two-col {
-  padding-left: 0;
-  padding-right: 0;
-}
-.two-col .col{
-  float: left;
-  width: 50%;
-}
-
-.two-col .col.col-header {
-  font-weight: 600;
-  box-sizing: border-box;
-  -moz-box-sizing: border-box;
-  padding-left: 20px;
-  padding-right: 20px;
-}
-
-.clear {
-  clear: both;
-}

+ 0 - 753
docs/index.html

@@ -1,753 +0,0 @@
-<head>
-  <title>PeerJS Documentation</title>
-  <meta name="viewport" content="width=device-width, maximum-scale=1">
-  <link href='https://fonts.googleapis.com/css?family=Lato:300,400,700,900' rel='stylesheet' type='text/css'>
-  <link href="/css/docs.css" rel="stylesheet" type="text/css">
-  <script type="text/javascript" src="/js/jquery.min.js"></script>
-  <script type="text/javascript" src="/js/docs.js"></script>
-</head>
-
-<body>
-  <section class="start">
-    <h1>
-      <a href="/">PeerJS</a>
-      <span class="title">docs</span>
-    </h1>
-    <p>
-      <br>PeerJS simplifies peer-to-peer data, video, and audio calls.</p>
-    <p>This guide will show you the basic concepts of the PeerJS API.</p>
-    <h2>Setup</h2>
-    <h3>1. Include the Javascript client</h3>
-    <p>Add the PeerJS client library to your webpage.</p>
-    <pre>&lt;script src="https://unpkg.com/peerjs@1.3.1/dist/peerjs.min.js"&gt;&lt;/script&gt;</pre>
-    <p>If you prefer, you can host it yourself:
-      <a download href="https://unpkg.com/peerjs@1.3.1/dist/peerjs.min.js">peerjs.min.js</a>, or
-      <a href="https://github.com/peers/peerjs">fork us on Github</a>.</p>
-    <h3>2. Create the Peer object</h3>
-    <p>The Peer object is where we create and receive connections.</p>
-    <pre>var peer = new Peer();</pre>
-    <p>PeerJS uses PeerServer for session
-      metadata and candidate signaling. You can also
-      <a href="https://github.com/peers/peerjs-server">run your own PeerServer</a> if you don't like the cloud.</p>
-    <p>We're now ready to start making connections!</p>
-
-    <h2>Usage</h2>
-    <p>Every Peer object is assigned a random, unique ID when it's created.</p>
-    <pre>peer.on('open', function(id) {
-  console.log('My peer ID is: ' + id);
-});</pre>
-    <p>When we want to connect to another peer, we'll need to know their peer id. You're in charge of communicating the
-      peer
-      IDs between users of your site. Optionally, you can pass in your own IDs to the
-      <a href="#peer">
-        <code>Peer</code> constructor
-      </a>.</p>
-
-    <p>Read the
-      <a href="#peer">Peer API reference</a> for complete information on its
-      <a href="#peer-options">options</a>, methods,
-      <a href="#peeron">events</a>, and
-      <a href="#peeron-error">error handling</a>.</p>
-
-    <h3>Data connections</h3>
-    <p>Start a data connection by calling
-      <code>peer.connect</code> with the peer ID of the destination peer. Anytime another peer attempts to connect to
-      your peer ID, you'll receive
-      a
-      <code>connection</code> event. </p>
-    <div class="two-col">
-      <div class="col col-header">Start connection</div>
-      <div class="col col-header">Receive connection</div>
-      <div class="col">
-        <pre>var conn = peer.connect('dest-peer-id');</pre>
-      </div>
-      <div class="col">
-        <pre>peer.on('connection', function(conn) { ... });</pre>
-      </div>
-      <div class="clear"></div>
-    </div>
-    <p>
-      <code>peer.connect</code> and the callback of the
-      <code>connection</code> event will both provide a
-      <code>DataConnection</code> object. This object will allow you to send and receive data:</p>
-    <pre>conn.on('open', function() {
-  // Receive messages
-  conn.on('data', function(data) {
-    console.log('Received', data);
-  });
-
-  // Send messages
-  conn.send('Hello!');
-});</pre>
-    <p>Read the
-      <a href="#dataconnection">DataConnection API reference</a> for complete details on its methods and events.</p>
-    <h3>Video/audio calls</h3>
-    <p>Call another peer by calling
-      <code>peer.call</code> with the peer ID of the destination peer. When a peer calls you, the
-      <code>call</code> event is emitted.</p>
-    <p>Unlike data connections, when receiving a
-      <code>call</code> event, the call must be answered or no connection is established.</p>
-    <div class="two-col">
-      <div class="col col-header">Start call</div>
-      <div class="col col-header">Answer call</div>
-      <div class="col">
-        <pre>// Call a peer, providing our mediaStream
-var call = peer.call('dest-peer-id',
-  mediaStream);
-
-</pre>
-      </div>
-      <div class="col">
-        <pre>peer.on('call', function(call) {
-  // Answer the call, providing our mediaStream
-  call.answer(mediaStream);
-});</pre>
-      </div>
-      <div class="clear"></div>
-    </div>
-    <p>When calling or answering a call, a MediaStream should be provided. The MediaStream represents the local video
-      (webcam)
-      or audio stream and can be obtained with some (browser-specific) version of
-      <a href="https://developer.mozilla.org/en-US/docs/Web/API/Navigator.getUserMedia">
-        <code>navigator.getUserMedia</code>
-      </a>. When answering a call, the MediaStream is optional and if none is provided then a one-way call is
-      established.
-      Once the call is established, its
-      <code>open</code> property is set to true.</p>
-    <p>
-      <code>peer.call</code> and the callback of the
-      <code>call</code> event provide a MediaConnection object. The MediaConnection object itself emits a
-      <code>stream</code> event whose callback includes the video/audio stream of the other peer.</p>
-    <pre>call.on('stream', function(stream) {
-  // `stream` is the MediaStream of the remote peer.
-  // Here you'd add it to an HTML video/canvas element.
-});</pre>
-    <p>Read the
-      <a href="#mediaconnection">MediaConnection API reference</a> for complete details on its methods and events.</p>
-
-    <h2>Common questions</h2>
-
-    <h3>What kind of data can I send?</h3>
-
-    <p>PeerJS has the
-      <a href="https://github.com/binaryjs/js-binarypack">BinaryPack</a>
-      serialization format built-in. This means you can send any JSON type as well as binary Blobs and ArrayBuffers.
-      Simply send
-      arbitrary data and you'll get it out the other side:</p>
-    <pre>
-conn.send({
-  strings: 'hi!',
-  numbers: 150,
-  arrays: [1,2,3],
-  evenBinary: new Blob([1,2,3]),
-  andMore: {bool: true}
-});</pre>
-
-    <h3>Are there any caveats?</h3>
-
-    <p>A small percentage of users are behind symmetric NATs. When two symmetric NAT users try to connect to each other,
-      NAT
-      traversal is impossible and no connection can be made. A workaround is to proxy through the connection through a
-      TURN
-      server. The PeerServer cloud service provides a free TURN server. This will allow your PeerJS app to work
-      seamlessly for this situation</p>
-    <h3>How do I use a TURN server?</h3>
-    <p>When creating your Peer object, pass in the ICE servers as the config key of the options hash.</p>
-    <pre>
-var peer = new Peer({
-  config: {'iceServers': [
-    { url: 'stun:stun.l.google.com:19302' },
-    { url: 'turn:homeo@turn.bistri.com:80', credential: 'homeo' }
-  ]} /* Sample servers, please use appropriate ones */
-});
-</pre>
-    <h3>What if my peer has not yet connected to the server when I attempt to connect to it?</h3>
-
-    <p>When you try to connect to a peer, PeerServer will hold a connection offer for up to 5 seconds before rejecting
-      it. This
-      is useful if you want to reconnect to a peer as it disconnects and reconnects rapidly between web pages.</p>
-
-    <h3>Why am I unable to connect?</h3>
-    <p>You could be behind a symmetric NAT, in which case you'll need to set up a TURN server.</p>
-    <p>Another possible issue is your network blocking port 443, which the PeerServer cloud runs on. In this you must
-      use your
-      own PeerServer running on an appropriate port instead of the cloud service.</p>
-
-    <h3>What about latency/bandwidth?</h3>
-
-    <p>Data sent between the two peers do not touch any other servers, so the connection speed is limited only by the
-      upload
-      and download rates of the two peers. This also means you don't have the additional latency of an intermediary
-      server.</p>
-    <p>The latency to establish a connection can be split into two components: the brokering of data and the
-      identification
-      of clients. PeerJS has been designed to minimize the time you spend in these two areas. For brokering, data is
-      sent
-      through an XHR streaming request before a WebSocket connection is established, then through WebSockets. For client
-      identification, we provide you the ability to pass in your own peer IDs, thus eliminating the RTT for retrieving
-      an
-      ID from the server.</p>
-
-    <h3>More questions?</h3>
-    <p>
-      <a href="https://t.me/joinchat/ENhPuhTvhm8WlIxTjQf7Og">Discuss PeerJS on our Telegram channel.</a>
-      <br>
-      <br>
-    </p>
-  </section>
-
-  <header class="left">
-    <h2>API Reference
-      <a class="hide icon">&laquo;</a>
-      <a class="show icon">&raquo;</a>
-    </h2>
-  </header>
-  <header class="right">
-    <h2>Getting Started</h2>
-  </header>
-
-  <section class="api">
-    <div class="toplevel " id="peer"><span class="name"><a href="#peer">Peer</a><span
-          class="tag type constructor">constructor</span><span class="snippet">const peer = new Peer([id],
-          [options]);</span></span>
-      <p class="description">A peer can connect to other peers and listen for connections.</p>
-      <div class="children">
-        <div class="child " id="peer-id"><span class="name"><a href="#peer-id"><span class="optional"><span
-                  class="bracket">[</span>id<span class="bracket">]</span></span></a><span
-              class="tag type string">string</span></span>
-          <p class="description">Other peers can connect to this peer using the provided ID. If no ID is given, one will
-            be generated by the brokering server. The ID must start and end with an alphanumeric character (lower or
-            upper case character or a digit). In the middle of the ID spaces, dashes (-) and underscores (_) are
-            allowed.<span class='warn'>It's not recommended that you use this ID to identify peers, as it's meant to be
-              used for brokering connections only. You're recommended to set the <a
-                href='#peerconnect-options'><code>metadata</code></a> option to send other identifying
-              information.</span></p>
-        </div>
-        <div class="child " id="peer-options"><span class="name"><a href="#peer-options"><span class="optional"><span
-                  class="bracket">[</span>options<span class="bracket">]</span></span></a><span
-              class="tag type object">object</span></span>
-          <div class="children">
-            <div class="child " id="peer-options-key"><span class="name"><a href="#peer-options-key">key</a><span
-                  class="tag type string">string</span></span>
-              <p class="description">API key for the cloud PeerServer. This is not used for servers other than
-                <code>0.peerjs.com</code>.<span class='warn'>PeerServer cloud runs on port 443. Please ensure it is not
-                  blocked or consider running your own PeerServer instead.</span></p>
-            </div>
-            <div class="child " id="peer-options-host"><span class="name"><a href="#peer-options-host">host</a><span
-                  class="tag type string">string</span></span>
-              <p class="description">Server host. Defaults to <code>0.peerjs.com</code>. Also accepts <code>'/'</code>
-                to signify relative hostname.</p>
-            </div>
-            <div class="child " id="peer-options-port"><span class="name"><a href="#peer-options-port">port</a><span
-                  class="tag type number">number</span></span>
-              <p class="description">Server port. Defaults to <code>443</code>.</p>
-            </div>
-            <div class="child " id="peer-options-pinginterval"><span class="name"><a
-                  href="#peer-options-pinginterval">pingInterval</a><span class="tag type number">number</span></span>
-              <p class="description">Ping interval in ms. Defaults to <code>5000</code>.</p>
-            </div>
-            <div class="child " id="peer-options-path"><span class="name"><a href="#peer-options-path">path</a><span
-                  class="tag type string">string</span></span>
-              <p class="description">The path where your self-hosted PeerServer is running. Defaults to
-                <code>'/'</code>.</p>
-            </div>
-            <div class="child " id="peer-options-secure"><span class="name"><a
-                  href="#peer-options-secure">secure</a><span class="tag type boolean">boolean</span></span>
-              <p class="description"><code>true</code> if you're using SSL.<span class='tip'>Note that our cloud-hosted
-                  server and assets may not support SSL.</span></p>
-            </div>
-            <div class="child " id="peer-options-config"><span class="name"><a
-                  href="#peer-options-config">config</a><span class="tag type object">object</span></span>
-              <p class="description">Configuration hash passed to RTCPeerConnection. This hash contains any custom
-                ICE/TURN server configuration. Defaults to
-                <code>{ 'iceServers': [{ 'urls': 'stun:stun.l.google.com:19302' }, { 'urls': 'turn:0.peerjs.com:3478', username: 'peerjs', credential: 'peerjsp' }], 'sdpSemantics': 'unified-plan' }</code>
-              </p>
-            </div>
-            <div class="child " id="peer-options-debug"><span class="name"><a href="#peer-options-debug">debug</a><span
-                  class="tag type number">number</span></span>
-              <p class="description">Prints log messages depending on the debug level passed in. Defaults to
-                <code>0</code>.</p>
-              <div class="children">
-                <div class="child " id="peer-options-debug-0"><span class="name"><a
-                      href="#peer-options-debug-0">0</a></span>
-                  <p class="description">Prints no logs.</p>
-                </div>
-                <div class="child " id="peer-options-debug-1"><span class="name"><a
-                      href="#peer-options-debug-1">1</a></span>
-                  <p class="description">Prints only errors.</p>
-                </div>
-                <div class="child " id="peer-options-debug-2"><span class="name"><a
-                      href="#peer-options-debug-2">2</a></span>
-                  <p class="description">Prints errors and warnings.</p>
-                </div>
-                <div class="child " id="peer-options-debug-3"><span class="name"><a
-                      href="#peer-options-debug-3">3</a></span>
-                  <p class="description">Prints all logs.</p>
-                </div>
-              </div>
-            </div>
-          </div>
-        </div>
-      </div>
-    </div>
-    <div class="toplevel " id="peerconnect"><span class="name"><a href="#peerconnect">peer.connect</a><span
-          class="tag type method">method</span><span class="snippet">const <a href='#dataconnection'>dataConnection</a>
-          = peer.connect(id, [options]);</span></span>
-      <p class="description">Connects to the remote peer specified by <code>id</code> and returns a data connection. Be
-        sure to listen on the <a href='#peeron-error'><code>error</code></a> event in case the connection fails.</p>
-      <div class="children">
-        <div class="child " id="peerconnect-id"><span class="name"><a href="#peerconnect-id">id</a><span
-              class="tag type string">string</span></span>
-          <p class="description">The brokering ID of the remote peer (their <a href='#peerid'><code>peer.id</code></a>).
-          </p>
-        </div>
-        <div class="child " id="peerconnect-options"><span class="name"><a href="#peerconnect-options"><span
-                class="optional"><span class="bracket">[</span>options<span class="bracket">]</span></span></a><span
-              class="tag type object">object</span></span>
-          <div class="children">
-            <div class="child " id="peerconnect-options-label"><span class="name"><a
-                  href="#peerconnect-options-label">label</a><span class="tag type string">string</span></span>
-              <p class="description">A unique label by which you want to identify this data connection. If left
-                unspecified, a label will be generated at random. Can be accessed with <a
-                  href='#dataconnection-label'><code>dataConnection.label</code></a>.</p>
-            </div>
-            <div class="child " id="peerconnect-options-metadata"><span class="name"><a
-                  href="#peerconnect-options-metadata">metadata</a></span>
-              <p class="description">Metadata associated with the connection, passed in by whoever initiated the
-                connection. Can be accessed with <a
-                  href='#dataconnection-metadata'><code>dataConnection.metadata</code></a>. Can be any serializable
-                type.</p>
-            </div>
-            <div class="child " id="peerconnect-options-serialization"><span class="name"><a
-                  href="#peerconnect-options-serialization">serialization</a><span
-                  class="tag type string">string</span></span>
-              <p class="description">Can be <code>binary</code> (default), <code>binary-utf8</code>, <code>json</code>,
-                or <code>none</code>. Can be accessed with <a
-                  href='#dataconnection-serialization'><code>dataConnection.serialization</code></a>.<span
-                  class='tip'><code>binary-utf8</code> will take a performance hit because of the way UTF8 strings are
-                  packed into binary format.</span></p>
-            </div>
-            <div class="child " id="peerconnect-options-reliable"><span class="name"><a
-                  href="#peerconnect-options-reliable">reliable</a><span class="tag type boolean">boolean</span></span>
-              <p class="description">Whether the underlying data channels should be reliable (e.g. for large file
-                transfers) or not (e.g. for gaming or streaming). Defaults to <code>false</code>.<span
-                  class='warn'>Setting reliable to true will use a shim for incompatible browsers (Chrome 30 and below
-                  only) and thus may not offer full performance.</span></p>
-            </div>
-          </div>
-        </div>
-      </div>
-    </div>
-    <div class="toplevel " id="peercall"><span class="name"><a href="#peercall">peer.call</a><span
-          class="tag type method">method</span><span class="snippet">const <a
-            href='#mediaconnection'>mediaConnection</a> = peer.call(id, stream, [options]);</span></span>
-      <p class="description">Calls the remote peer specified by <code>id</code> and returns a media connection. Be sure
-        to listen on the <a href='#peeron-error'><code>error</code></a> event in case the connection fails.</p>
-      <div class="children">
-        <div class="child " id="peercall-id"><span class="name"><a href="#peercall-id">id</a><span
-              class="tag type string">string</span></span>
-          <p class="description">The brokering ID of the remote peer (their <a href='#peerid'><code>peer.id</code></a>).
-          </p>
-        </div>
-        <div class="child " id="peercall-stream"><span class="name"><a href="#peercall-stream">stream</a><span
-              class="tag type MediaStream">MediaStream</span></span>
-          <p class="description">The caller's media stream</p>
-        </div>
-        <div class="child " id="peercall-options"><span class="name"><a href="#peercall-options"><span
-                class="optional"><span class="bracket">[</span>options<span class="bracket">]</span></span></a><span
-              class="tag type object">object</span></span>
-          <div class="children">
-            <div class="child " id="peercall-options-metadata"><span class="name"><a
-                  href="#peercall-options-metadata">metadata</a></span>
-              <p class="description">Metadata associated with the connection, passed in by whoever initiated the
-                connection. Can be accessed with <a
-                  href='#mediaconnection-metadata'><code>mediaConnection.metadata</code></a>. Can be any serializable
-                type.</p>
-            </div>
-            <div class="child " id="peercall-options-sdptransform"><span class="name"><a
-                  href="#peercall-options-sdptransform">sdpTransform</a><span
-                  class="tag type method">method</span></span>
-              <p class="description">Function which runs before create offer to modify sdp offer message.</p>
-            </div>
-          </div>
-        </div>
-      </div>
-    </div>
-    <div class="toplevel " id="peeron"><span class="name"><a href="#peeron">peer.on</a><span
-          class="tag type method">method</span><span class="snippet">peer.on(event, callback);</span></span>
-      <p class="description">Set listeners for peer events.</p>
-      <div class="children">
-        <div class="child " id="peeron-open"><span class="name"><a href="#peeron-open">'open'</a><span
-              class="tag type event">event</span><span class="snippet">peer.on('open', function(id) { ...
-              });</span></span>
-          <p class="description">Emitted when a connection to the PeerServer is established. You may use the peer before
-            this is emitted, but messages to the server will be queued. <code>id</code> is the brokering ID of the peer
-            (which was either provided in the constructor or assigned by the server).<span class='tip'>You should not
-              wait for this event before connecting to other peers if connection speed is important.</span></p>
-        </div>
-        <div class="child " id="peeron-connection"><span class="name"><a href="#peeron-connection">'connection'</a><span
-              class="tag type event">event</span><span class="snippet">peer.on('connection', function(<a
-                href='#dataconnection'>dataConnection</a>) { ... });</span></span>
-          <p class="description">Emitted when a new data connection is established from a remote peer.</p>
-        </div>
-        <div class="child " id="peeron-call"><span class="name"><a href="#peeron-call">'call'</a><span
-              class="tag type event">event</span><span class="snippet">peer.on('call', function(<a
-                href='#mediaconnection'>mediaConnection</a>) { ... });</span></span>
-          <p class="description">Emitted when a remote peer attempts to call you. The emitted
-            <code>mediaConnection</code> is not yet active; you must first answer the call (<a
-              href='#mediaconnection-answer'><code>mediaConnection.answer([stream]);</code></a>). Then, you can listen
-            for the <a href='#mediaconnection-on'><code>stream</code></a> event.</p>
-        </div>
-        <div class="child " id="peeron-close"><span class="name"><a href="#peeron-close">'close'</a><span
-              class="tag type event">event</span><span class="snippet">peer.on('close', function() { ...
-              });</span></span>
-          <p class="description">Emitted when the peer is <a href='#peerdestroy'>destroyed</a> and can no longer accept
-            or create any new connections. At this time, the peer's connections will all be closed. <span class='tip'>To
-              be extra certain that peers clean up correctly, we recommend calling <code>peer.destroy()</code> on a peer
-              when it is no longer needed.</span></p>
-        </div>
-        <div class="child " id="peeron-disconnected"><span class="name"><a
-              href="#peeron-disconnected">'disconnected'</a><span class="tag type event">event</span><span
-              class="snippet">peer.on('disconnected', function() { ... });</span></span>
-          <p class="description">Emitted when the peer is disconnected from the signalling server, either <a
-              href='#peerdisconnect'>manually</a> or because the connection to the signalling server was lost. When a
-            peer is disconnected, its existing connections will stay alive, but the peer cannot accept or create any new
-            connections. You can reconnect to the server by calling <a
-              href='#peerreconnect'><code>peer.reconnect()</code></a>.</p>
-        </div>
-        <div class="child " id="peeron-error"><span class="name"><a href="#peeron-error">'error'</a><span
-              class="tag type event">event</span><span class="snippet">peer.on('error', function(err) { ...
-              });</span></span>
-          <p class="description">Errors on the peer are <strong>almost always fatal</strong> and will destroy the peer.
-            Errors from the underlying socket and PeerConnections are forwarded here.<br><br>These come in the following
-            <code>err.type</code> flavors:</p>
-          <div class="children">
-            <div class="child fatal " id="peeron-error-browser-incompatible"><span class="name"><a
-                  href="#peeron-error-browser-incompatible">'browser-incompatible'</a><span
-                  class="tag type Error">Error</span><span class="tag fatal">fatal</span></span>
-              <p class="description">The client's browser does not support some or all WebRTC features that you are
-                trying to use.</p>
-            </div>
-            <div class="child " id="peeron-error-disconnected"><span class="name"><a
-                  href="#peeron-error-disconnected">'disconnected'</a><span class="tag type Error">Error</span></span>
-              <p class="description">You've already disconnected this peer from the server and can no longer make any
-                new connections on it.</p>
-            </div>
-            <div class="child fatal " id="peeron-error-invalid-id"><span class="name"><a
-                  href="#peeron-error-invalid-id">'invalid-id'</a><span class="tag type Error">Error</span><span
-                  class="tag fatal">fatal</span></span>
-              <p class="description">The ID passed into the Peer constructor contains illegal characters.</p>
-            </div>
-            <div class="child fatal " id="peeron-error-invalid-key"><span class="name"><a
-                  href="#peeron-error-invalid-key">'invalid-key'</a><span class="tag type Error">Error</span><span
-                  class="tag fatal">fatal</span></span>
-              <p class="description">The API key passed into the Peer constructor contains illegal characters or is not
-                in the system (cloud server only).</p>
-            </div>
-            <div class="child " id="peeron-error-network"><span class="name"><a
-                  href="#peeron-error-network">'network'</a><span class="tag type Error">Error</span></span>
-              <p class="description">Lost or cannot establish a connection to the signalling server.</p>
-            </div>
-            <div class="child " id="peeron-error-peer-unavailable"><span class="name"><a
-                  href="#peeron-error-peer-unavailable">'peer-unavailable'</a><span
-                  class="tag type Error">Error</span></span>
-              <p class="description">The peer you're trying to connect to does not exist.</p>
-            </div>
-            <div class="child fatal " id="peeron-error-ssl-unavailable"><span class="name"><a
-                  href="#peeron-error-ssl-unavailable">'ssl-unavailable'</a><span
-                  class="tag type Error">Error</span><span class="tag fatal">fatal</span></span>
-              <p class="description">PeerJS is being used securely, but the cloud server does not support SSL. Use a
-                custom PeerServer.</p>
-            </div>
-            <div class="child fatal " id="peeron-error-server-error"><span class="name"><a
-                  href="#peeron-error-server-error">'server-error'</a><span class="tag type Error">Error</span><span
-                  class="tag fatal">fatal</span></span>
-              <p class="description">Unable to reach the server.</p>
-            </div>
-            <div class="child fatal " id="peeron-error-socket-error"><span class="name"><a
-                  href="#peeron-error-socket-error">'socket-error'</a><span class="tag type Error">Error</span><span
-                  class="tag fatal">fatal</span></span>
-              <p class="description">An error from the underlying socket.</p>
-            </div>
-            <div class="child fatal " id="peeron-error-socket-closed"><span class="name"><a
-                  href="#peeron-error-socket-closed">'socket-closed'</a><span class="tag type Error">Error</span><span
-                  class="tag fatal">fatal</span></span>
-              <p class="description">The underlying socket closed unexpectedly.</p>
-            </div>
-            <div class="child sometimes_fatal " id="peeron-error-unavailable-id"><span class="name"><a
-                  href="#peeron-error-unavailable-id">'unavailable-id'</a><span class="tag type Error">Error</span><span
-                  class="tag sometimes_fatal">sometimes fatal</span></span>
-              <p class="description">The ID passed into the Peer constructor is already taken.<span class='warn'>This
-                  error is not fatal if your peer has open peer-to-peer connections. This can happen if you attempt to
-                  <a href='#peerreconnect'>reconnect</a> a peer that has been <a href='#peerdisconnect'>disconnected
-                    from the server</a>, but its old ID has now been taken.</span></p>
-            </div>
-            <div class="child " id="peeron-error-webrtc"><span class="name"><a
-                  href="#peeron-error-webrtc">'webrtc'</a><span class="tag type Error">Error</span></span>
-              <p class="description">Native WebRTC errors.</p>
-            </div>
-          </div>
-        </div>
-      </div>
-    </div>
-    <div class="toplevel " id="peerdisconnect"><span class="name"><a href="#peerdisconnect">peer.disconnect</a><span
-          class="tag type method">method</span><span class="snippet">peer.disconnect();</span></span>
-      <p class="description">Close the connection to the server, leaving all existing data and media connections intact.
-        <a href='#peerdisconnected'><code>peer.disconnected</code></a> will be set to <code>true</code> and the <a
-          href='#peeron-disconnected'><code>disconnected</code></a> event will fire.<span class='warn'>This cannot be
-          undone; the respective peer object will no longer be able to create or receive any connections and its ID will
-          be forfeited on the (cloud) server.</span></p>
-    </div>
-    <div class="toplevel " id="peerreconnect"><span class="name"><a href="#peerreconnect">peer.reconnect</a><span
-          class="tag type method">method</span><span class="snippet">peer.reconnect();</span></span>
-      <p class="description">Attempt to reconnect to the server with the peer's old ID. Only <a
-          href='#peerdisconnect'>disconnected peers</a> can be reconnected. Destroyed peers cannot be reconnected. If
-        the connection fails (as an example, if the peer's old ID is now taken), the peer's existing connections will
-        not close, but any associated errors events will fire.</p>
-    </div>
-    <div class="toplevel " id="peerdestroy"><span class="name"><a href="#peerdestroy">peer.destroy</a><span
-          class="tag type method">method</span><span class="snippet">peer.destroy();</span></span>
-      <p class="description">Close the connection to the server and terminate all existing connections. <a
-          href='#peerdestroyed'><code>peer.destroyed</code></a> will be set to <code>true</code>.<span class='warn'>This
-          cannot be undone; the respective peer object will no longer be able to create or receive any connections, its
-          ID will be forfeited on the (cloud) server, and all of its data and media connections will be closed.</span>
-      </p>
-    </div>
-    <div class="toplevel " id="peerid"><span class="name"><a href="#peerid">peer.id</a><span
-          class="tag type string">string</span></span>
-      <p class="description">The brokering ID of this peer. If no ID was specified in <a href='#peer'>the
-          constructor</a>, this will be <code>undefined</code> until the <a href='#peeron-open'><code>open</code></a>
-        event is emitted.</p>
-    </div>
-    <div class="toplevel " id="peerconnections"><span class="name"><a href="#peerconnections">peer.connections</a><span
-          class="tag type object">object</span></span>
-      <p class="description">A hash of all connections associated with this peer, keyed by the remote peer's ID.<span
-          class='tip'>We recommend keeping track of connections yourself rather than relying on this hash.</span></p>
-    </div>
-    <div class="toplevel " id="peerdisconnected"><span class="name"><a
-          href="#peerdisconnected">peer.disconnected</a><span class="tag type boolean">boolean</span></span>
-      <p class="description"><code>false</code> if there is an active connection to the PeerServer.</p>
-    </div>
-    <div class="toplevel " id="peerdestroyed"><span class="name"><a href="#peerdestroyed">peer.destroyed</a><span
-          class="tag type boolean">boolean</span></span>
-      <p class="description"><code>true</code> if this peer and all of its connections can no longer be used.</p>
-    </div>
-    <div class="toplevel " id="dataconnection"><span class="name"><a href="#dataconnection">DataConnection</a><span
-          class="tag type class">class</span></span>
-      <p class="description">Wraps WebRTC's DataChannel. To get one, use <a
-          href='#peerconnect'><code>peer.connect</code></a> or listen for the <a
-          href='#peeron-connect'><code>connect</code></a> event.</p>
-      <div class="children">
-        <div class="child " id="dataconnection-send"><span class="name"><a href="#dataconnection-send">.send</a><span
-              class="tag type method">method</span><span class="snippet">dataConnection.send(data);</span></span>
-          <p class="description"><code>data</code> is serialized by BinaryPack by default and sent to the remote peer.
-          </p>
-          <div class="children">
-            <div class="child " id="dataconnection-send-data"><span class="name"><a
-                  href="#dataconnection-send-data">data</a></span>
-              <p class="description">You can send any type of data, including objects, strings, and blobs.</p>
-            </div>
-          </div>
-        </div>
-        <div class="child " id="dataconnection-close"><span class="name"><a href="#dataconnection-close">.close</a><span
-              class="tag type method">method</span><span class="snippet">dataConnection.close();</span></span>
-          <p class="description">Closes the data connection gracefully, cleaning up underlying DataChannels and
-            PeerConnections.</p>
-        </div>
-        <div class="child " id="dataconnection-on"><span class="name"><a href="#dataconnection-on">.on</a><span
-              class="tag type method">method</span><span class="snippet">dataConnection.on(event,
-              callback);</span></span>
-          <p class="description">Set listeners for data connection events.</p>
-          <div class="children">
-            <div class="child " id="dataconnection-on-data"><span class="name"><a
-                  href="#dataconnection-on-data">'data'</a><span class="tag type event">event</span><span
-                  class="snippet">dataConnection.on('data', function(data) { ... });</span></span>
-              <p class="description">Emitted when data is received from the remote peer.</p>
-            </div>
-            <div class="child " id="dataconnection-on-open"><span class="name"><a
-                  href="#dataconnection-on-open">'open'</a><span class="tag type event">event</span><span
-                  class="snippet">dataConnection.on('open', function() { ... });</span></span>
-              <p class="description">Emitted when the connection is established and ready-to-use.</p>
-            </div>
-            <div class="child " id="dataconnection-on-close"><span class="name"><a
-                  href="#dataconnection-on-close">'close'</a><span class="tag type event">event</span><span
-                  class="snippet">dataConnection.on('close', function() { ... });</span></span>
-              <p class="description">Emitted when either you or the remote peer closes the data connection.<span
-                  class='warn'>Firefox does not yet support this event.</span></p>
-            </div>
-            <div class="child " id="dataconnection-on-error"><span class="name"><a
-                  href="#dataconnection-on-error">'error'</a><span class="tag type event">event</span><span
-                  class="snippet">dataConnection.on('error', function(err) { ... });</span></span></div>
-          </div>
-        </div>
-        <div class="child " id="dataconnection-datachannel"><span class="name"><a
-              href="#dataconnection-datachannel">.dataChannel</a><span class="tag type object">object</span></span>
-          <p class="description">A reference to the RTCDataChannel object associated with the connection.</p>
-        </div>
-        <div class="child " id="dataconnection-label"><span class="name"><a href="#dataconnection-label">.label</a><span
-              class="tag type string">string</span></span>
-          <p class="description">The optional label passed in or assigned by PeerJS when the connection was initiated.
-          </p>
-        </div>
-        <div class="child " id="dataconnection-metadata"><span class="name"><a
-              href="#dataconnection-metadata">.metadata</a></span>
-          <p class="description">Any type of metadata associated with the connection, passed in by whoever initiated the
-            connection.</p>
-        </div>
-        <div class="child " id="dataconnection-open"><span class="name"><a href="#dataconnection-open">.open</a><span
-              class="tag type boolean">boolean</span></span>
-          <p class="description">This is true if the connection is open and ready for read/write.</p>
-        </div>
-        <div class="child " id="dataconnection-peerconnection"><span class="name"><a
-              href="#dataconnection-peerconnection">.peerConnection</a><span
-              class="tag type object">object</span></span>
-          <p class="description">A reference to the RTCPeerConnection object associated with the connection.</p>
-        </div>
-        <div class="child " id="dataconnection-peer"><span class="name"><a href="#dataconnection-peer">.peer</a><span
-              class="tag type string">string</span></span>
-          <p class="description">The ID of the peer on the other end of this connection.</p>
-        </div>
-        <div class="child " id="dataconnection-reliable"><span class="name"><a
-              href="#dataconnection-reliable">.reliable</a><span class="tag type boolean">boolean</span></span>
-          <p class="description">Whether the underlying data channels are reliable; defined when the connection was
-            initiated.</p>
-        </div>
-        <div class="child " id="dataconnection-serialization"><span class="name"><a
-              href="#dataconnection-serialization">.serialization</a><span class="tag type string">string</span></span>
-          <p class="description">The serialization format of the data sent over the connection. Can be
-            <code>binary</code> (default), <code>binary-utf8</code>, <code>json</code>, or <code>none</code>.</p>
-        </div>
-        <div class="child " id="dataconnection-type"><span class="name"><a href="#dataconnection-type">.type</a><span
-              class="tag type string">string</span></span>
-          <p class="description">For data connections, this is always <code>'data'</code>.</p>
-        </div>
-        <div class="child " id="dataconnection-buffersize"><span class="name"><a
-              href="#dataconnection-buffersize">.bufferSize</a><span class="tag type number">number</span></span>
-          <p class="description">The number of messages queued to be sent once the browser buffer is no longer full.</p>
-        </div>
-      </div>
-    </div>
-    <div class="toplevel " id="mediaconnection"><span class="name"><a href="#mediaconnection">MediaConnection</a><span
-          class="tag type class">class</span></span>
-      <p class="description">Wraps WebRTC's media streams. To get one, use <a
-          href='#peercall'><code>peer.call</code></a> or listen for the <a href='#peeron-call'><code>call</code></a>
-        event.</p>
-      <div class="children">
-        <div class="child " id="mediaconnection-answer"><span class="name"><a
-              href="#mediaconnection-answer">.answer</a><span class="tag type method">method</span><span
-              class="snippet">mediaConnection.answer([stream],[options]);</span></span>
-          <p class="description">When receiving a <a href='#peeron-call'><code>call</code></a> event on a peer, you can
-            call <code>.answer</code> on the media connection provided by the callback to accept the call and optionally
-            send your own media stream.</p>
-          <div class="children">
-            <div class="child " id="mediaconnection-answer-stream"><span class="name"><a
-                  href="#mediaconnection-answer-stream"><span class="optional"><span class="bracket">[</span>stream<span
-                      class="bracket">]</span></span></a><span class="tag type MediaStream">MediaStream</span></span>
-              <p class="description">A WebRTC media stream from <a
-                  href='https://developer.mozilla.org/en-US/docs/Web/API/Navigator.getUserMedia'><code>getUserMedia</code></a>.
-              </p>
-            </div>
-            <div class="child " id="mediaconnection-answer-options"><span class="name"><a
-                  href="#mediaconnection-answer-options"><span class="optional"><span
-                      class="bracket">[</span>options<span class="bracket">]</span></span></a><span
-                  class="tag type object">object</span></span>
-              <div class="children">
-                <div class="child " id="mediaconnection-answer-options-sdptransform"><span class="name"><a
-                      href="#mediaconnection-answer-options-sdptransform">sdpTransform</a><span
-                      class="tag type method">method</span></span>
-                  <p class="description">Function which runs before create answer to modify sdp answer message.</p>
-                </div>
-              </div>
-            </div>
-          </div>
-        </div>
-        <div class="child " id="mediaconnection-close"><span class="name"><a
-              href="#mediaconnection-close">.close</a><span class="tag type method">method</span><span
-              class="snippet">mediaConnection.close();</span></span>
-          <p class="description">Closes the media connection.</p>
-        </div>
-        <div class="child " id="mediaconnection-on"><span class="name"><a href="#mediaconnection-on">.on</a><span
-              class="tag type method">method</span><span class="snippet">mediaConnection.on(event,
-              callback);</span></span>
-          <p class="description">Set listeners for media connection events.</p>
-          <div class="children">
-            <div class="child " id="mediaconnection-on-stream"><span class="name"><a
-                  href="#mediaconnection-on-stream">'stream'</a><span class="tag type event">event</span><span
-                  class="snippet">mediaConnection.on('stream', function(stream) { ... });</span></span>
-              <p class="description">Emitted when a remote peer adds a <code>stream</code>.</p>
-            </div>
-            <div class="child " id="mediaconnection-on-close"><span class="name"><a
-                  href="#mediaconnection-on-close">'close'</a><span class="tag type event">event</span><span
-                  class="snippet">mediaConnection.on('close', function() { ... });</span></span>
-              <p class="description">Emitted when either you or the remote peer closes the media connection. <span
-                  class='warn'>Firefox does not yet support this event.</span></p>
-            </div>
-            <div class="child " id="mediaconnection-on-error"><span class="name"><a
-                  href="#mediaconnection-on-error">'error'</a><span class="tag type event">event</span><span
-                  class="snippet">mediaConnection.on('error', function(err) { ... });</span></span></div>
-          </div>
-        </div>
-        <div class="child " id="mediaconnection-open"><span class="name"><a href="#mediaconnection-open">.open</a><span
-              class="tag type boolean">boolean</span></span>
-          <p class="description">Whether the media connection is active (e.g. your call has been answered). You can
-            check this if you want to set a maximum wait time for a one-sided call.</p>
-        </div>
-        <div class="child " id="mediaconnection-metadata"><span class="name"><a
-              href="#mediaconnection-metadata">.metadata</a></span>
-          <p class="description">Any type of metadata associated with the connection, passed in by whoever initiated the
-            connection.</p>
-        </div>
-        <div class="child " id="mediaconnection-peer"><span class="name"><a href="#mediaconnection-peer">.peer</a><span
-              class="tag type string">string</span></span>
-          <p class="description">The ID of the peer on the other end of this connection.</p>
-        </div>
-        <div class="child " id="mediaconnection-type"><span class="name"><a href="#mediaconnection-type">.type</a><span
-              class="tag type string">string</span></span>
-          <p class="description">For media connections, this is always <code>'media'</code>.</p>
-        </div>
-      </div>
-    </div>
-    <div class="toplevel utility " id="util"><span class="name"><a href="#util">util</a><span
-          class="tag type object">object</span><span class="tag utility">utility</span></span>
-      <p class="description">Provides a variety of helpful utilities.<span class='warn'>Only the utilities documented
-          here are guaranteed to be present on <code>util</code>. Undocumented utilities can be removed without warning.
-          We don't consider these to be 'breaking changes.'</span></p>
-      <div class="children">
-        <div class="child " id="util-browser"><span class="name"><a href="#util-browser">.browser</a><span
-              class="tag type string">string</span><span class="snippet">if (util.browser === 'Firefox') { /* OK to peer
-              with Firefox peers. */ }</span></span>
-          <p class="description">The current browser. This property can be useful in determining whether or not two
-            peers can connect. For example, as of now data connections are not yet interoperable between major browsers.
-            <code>util.browser</code> can currently have the values 'Firefox', 'Chrome', 'Unsupported', or 'Supported'
-            (unknown WebRTC-compatible browser).</p>
-        </div>
-        <div class="child " id="util-supports"><span class="name"><a href="#util-supports">.supports</a><span
-              class="tag type object">object</span><span class="snippet">if (util.supports.data) { /* OK to start a data
-              connection. */ }</span></span>
-          <p class="description">A hash of WebRTC features mapped to booleans that correspond to whether the feature is
-            supported by the current browser.<span class='warn'>Only the properties documented here are guaranteed to be
-              present on <code>util.supports</code>.</span></p>
-          <div class="children">
-            <div class="child " id="util-supports-audiovideo"><span class="name"><a
-                  href="#util-supports-audiovideo">.audioVideo</a><span class="tag type boolean">boolean</span></span>
-              <p class="description">True if the current browser supports media streams and PeerConnection.</p>
-            </div>
-            <div class="child " id="util-supports-data"><span class="name"><a href="#util-supports-data">.data</a><span
-                  class="tag type boolean">boolean</span></span>
-              <p class="description">True if the current browser supports DataChannel and PeerConnection.</p>
-            </div>
-            <div class="child " id="util-supports-binary"><span class="name"><a
-                  href="#util-supports-binary">.binary</a><span class="tag type boolean">boolean</span></span>
-              <p class="description">True if the current browser supports binary DataChannels.</p>
-            </div>
-            <div class="child " id="util-supports-reliable"><span class="name"><a
-                  href="#util-supports-reliable">.reliable</a><span class="tag type boolean">boolean</span></span>
-              <p class="description">True if the current browser supports reliable DataChannels.</p>
-            </div>
-          </div>
-        </div>
-      </div>
-    </div>
-  </section>
-
-</body>

+ 0 - 79
docs/js/docs.js

@@ -1,79 +0,0 @@
-$(document).ready(function() {
-  var $api = $('.api');
-  var $start = $('.start');
-  var $show = $('.left .show');
-  var $hide = $('.left .hide');
-  var width = $(window).width();
-  var height = $(window).height();
-  var THRESHOLD = 700;
-
-  init();
-
-  $(window).on('resize', function() {
-    width = $(window).width();
-    height = $(window).height();
-
-    init();
-  });
-
-  var hash = window.location.hash;
-  if (hash === '#start' && width < THRESHOLD) {
-    hideAPI();
-  }
-
-
-  function init() {
-    if (width < THRESHOLD) {
-      $api.addClass('fullscreen');
-      $start.addClass('full');
-      $show.hide();
-      $hide.hide();
-    } else {
-      $start.removeClass('full');
-      $api.removeClass('fullscreen');
-      $show.show();
-      $hide.show();
-    }
-
-    if ($api.attr('class').indexOf('hidden') === -1) {
-      showAPI();
-    } else {
-      hideAPI();
-    }
-  }
-
-  function hideAPI() {
-    $api.addClass('hidden');
-    if (width >= THRESHOLD) {
-      $start.addClass('full');
-      $hide.hide();
-      $show.show();
-    }
-  }
-
-  function showAPI() {
-    if (width >= THRESHOLD) {
-      $start.removeClass('full');
-      $show.hide();
-      $hide.show();
-    }
-    $api.removeClass('hidden');
-  }
-
-  $('body').on('click', '.left', function() {
-    if ($api.attr('class').indexOf('hidden') !== -1) {
-      showAPI();
-    } else if ($api.attr('class').indexOf('fullscreen') === -1) {
-      // Now the headers are only links.
-      hideAPI();
-    }
-  });
-  $('body').on('click', '.right', function() {
-    hideAPI();
-  });
-  $('body').on('click', 'a', function() {
-    if ($(this).attr('href').indexOf('#') === 0) {
-      showAPI();
-    }
-  });
-});

文件差异内容过多而无法显示
+ 0 - 3
docs/js/jquery.min.js


+ 0 - 21
docs/package.json

@@ -1,21 +0,0 @@
-{
-  "name": "docs",
-  "version": "0.0.0",
-  "description": "**Due to browsers' incomplete support of the WebRTC DataChannel specification, many features of PeerJS have caveats.\r [View the status page for full details](http://peerjs.com/status).**",
-  "main": "build.js",
-  "scripts": {
-    "test": "echo \"Error: no test specified\" && exit 1",
-    "build": "node build.js"
-  },
-  "repository": "",
-  "author": "",
-  "license": "BSD",
-  "readmeFilename": "readme.md",
-  "dependencies": {
-    "reference": "^0.1.1",
-    "handlebars": ">=4.0.0"
-  },
-  "devDependencies": {
-    "standard": "^12.0.1"
-  }
-}

+ 0 - 6
docs/readme.md

@@ -1,6 +0,0 @@
-## PeerJS Documentation
-
-We've moved! <a href="https://peerjs.com/docs.html">Check out our new
-documentation.</a>
-
-### [Discuss PeerJS on our Telegram Channel](https://t.me/joinchat/ENhPuhTvhm8WlIxTjQf7Og)

+ 0 - 212
docs/template.html

@@ -1,212 +0,0 @@
-<head>
-  <title>PeerJS Documentation</title>
-  <meta name="viewport" content="width=device-width, maximum-scale=1">
-  <link href='https://fonts.googleapis.com/css?family=Lato:300,400,700,900' rel='stylesheet' type='text/css'>
-  <link href="/css/docs.css" rel="stylesheet" type="text/css">
-  <script type="text/javascript" src="/js/jquery.min.js"></script>
-  <script type="text/javascript" src="/js/docs.js"></script>
-</head>
-
-<body>
-  <section class="start">
-    <h1>
-      <a href="/">PeerJS</a>
-      <span class="title">docs</span>
-    </h1>
-    <p>
-      <br>PeerJS simplifies peer-to-peer data, video, and audio calls.</p>
-    <p>This guide will show you the basic concepts of the PeerJS API.</p>
-    <h2>Setup</h2>
-    <h3>1. Include the Javascript client</h3>
-    <p>Add the PeerJS client library to your webpage.</p>
-    <pre>&lt;script src="https://unpkg.com/peerjs@1.3.1/dist/peerjs.min.js"&gt;&lt;/script&gt;</pre>
-    <p>If you prefer, you can host it yourself:
-      <a download href="https://unpkg.com/peerjs@1.3.1/dist/peerjs.min.js">peerjs.min.js</a>, or
-      <a href="https://github.com/peers/peerjs">fork us on Github</a>.</p>
-    <h3>2. Create the Peer object</h3>
-    <p>The Peer object is where we create and receive connections.</p>
-    <pre>var peer = new Peer();</pre>
-    <p>PeerJS uses PeerServer for session
-      metadata and candidate signaling. You can also
-      <a href="https://github.com/peers/peerjs-server">run your own PeerServer</a> if you don't like the cloud.</p>
-    <p>We're now ready to start making connections!</p>
-
-    <h2>Usage</h2>
-    <p>Every Peer object is assigned a random, unique ID when it's created.</p>
-    <pre>peer.on('open', function(id) {
-  console.log('My peer ID is: ' + id);
-});</pre>
-    <p>When we want to connect to another peer, we'll need to know their peer id. You're in charge of communicating the
-      peer
-      IDs between users of your site. Optionally, you can pass in your own IDs to the
-      <a href="#peer">
-        <code>Peer</code> constructor
-      </a>.</p>
-
-    <p>Read the
-      <a href="#peer">Peer API reference</a> for complete information on its
-      <a href="#peer-options">options</a>, methods,
-      <a href="#peeron">events</a>, and
-      <a href="#peeron-error">error handling</a>.</p>
-
-    <h3>Data connections</h3>
-    <p>Start a data connection by calling
-      <code>peer.connect</code> with the peer ID of the destination peer. Anytime another peer attempts to connect to
-      your peer ID, you'll receive
-      a
-      <code>connection</code> event. </p>
-    <div class="two-col">
-      <div class="col col-header">Start connection</div>
-      <div class="col col-header">Receive connection</div>
-      <div class="col">
-        <pre>var conn = peer.connect('dest-peer-id');</pre>
-      </div>
-      <div class="col">
-        <pre>peer.on('connection', function(conn) { ... });</pre>
-      </div>
-      <div class="clear"></div>
-    </div>
-    <p>
-      <code>peer.connect</code> and the callback of the
-      <code>connection</code> event will both provide a
-      <code>DataConnection</code> object. This object will allow you to send and receive data:</p>
-    <pre>conn.on('open', function() {
-  // Receive messages
-  conn.on('data', function(data) {
-    console.log('Received', data);
-  });
-
-  // Send messages
-  conn.send('Hello!');
-});</pre>
-    <p>Read the
-      <a href="#dataconnection">DataConnection API reference</a> for complete details on its methods and events.</p>
-    <h3>Video/audio calls</h3>
-    <p>Call another peer by calling
-      <code>peer.call</code> with the peer ID of the destination peer. When a peer calls you, the
-      <code>call</code> event is emitted.</p>
-    <p>Unlike data connections, when receiving a
-      <code>call</code> event, the call must be answered or no connection is established.</p>
-    <div class="two-col">
-      <div class="col col-header">Start call</div>
-      <div class="col col-header">Answer call</div>
-      <div class="col">
-        <pre>// Call a peer, providing our mediaStream
-var call = peer.call('dest-peer-id',
-  mediaStream);
-
-</pre>
-      </div>
-      <div class="col">
-        <pre>peer.on('call', function(call) {
-  // Answer the call, providing our mediaStream
-  call.answer(mediaStream);
-});</pre>
-      </div>
-      <div class="clear"></div>
-    </div>
-    <p>When calling or answering a call, a MediaStream should be provided. The MediaStream represents the local video
-      (webcam)
-      or audio stream and can be obtained with some (browser-specific) version of
-      <a href="https://developer.mozilla.org/en-US/docs/Web/API/Navigator.getUserMedia">
-        <code>navigator.getUserMedia</code>
-      </a>. When answering a call, the MediaStream is optional and if none is provided then a one-way call is
-      established.
-      Once the call is established, its
-      <code>open</code> property is set to true.</p>
-    <p>
-      <code>peer.call</code> and the callback of the
-      <code>call</code> event provide a MediaConnection object. The MediaConnection object itself emits a
-      <code>stream</code> event whose callback includes the video/audio stream of the other peer.</p>
-    <pre>call.on('stream', function(stream) {
-  // `stream` is the MediaStream of the remote peer.
-  // Here you'd add it to an HTML video/canvas element.
-});</pre>
-    <p>Read the
-      <a href="#mediaconnection">MediaConnection API reference</a> for complete details on its methods and events.</p>
-
-    <h2>Common questions</h2>
-
-    <h3>What kind of data can I send?</h3>
-
-    <p>PeerJS has the
-      <a href="https://github.com/binaryjs/js-binarypack">BinaryPack</a>
-      serialization format built-in. This means you can send any JSON type as well as binary Blobs and ArrayBuffers.
-      Simply send
-      arbitrary data and you'll get it out the other side:</p>
-    <pre>
-conn.send({
-  strings: 'hi!',
-  numbers: 150,
-  arrays: [1,2,3],
-  evenBinary: new Blob([1,2,3]),
-  andMore: {bool: true}
-});</pre>
-
-    <h3>Are there any caveats?</h3>
-
-    <p>A small percentage of users are behind symmetric NATs. When two symmetric NAT users try to connect to each other,
-      NAT
-      traversal is impossible and no connection can be made. A workaround is to proxy through the connection through a
-      TURN
-      server. The PeerServer cloud service provides a free TURN server. This will allow your PeerJS app to work seamlessly for this situation</p>
-    <h3>How do I use a TURN server?</h3>
-    <p>When creating your Peer object, pass in the ICE servers as the config key of the options hash.</p>
-    <pre>
-var peer = new Peer({
-  config: {'iceServers': [
-    { url: 'stun:stun.l.google.com:19302' },
-    { url: 'turn:homeo@turn.bistri.com:80', credential: 'homeo' }
-  ]} /* Sample servers, please use appropriate ones */
-});
-</pre>
-    <h3>What if my peer has not yet connected to the server when I attempt to connect to it?</h3>
-
-    <p>When you try to connect to a peer, PeerServer will hold a connection offer for up to 5 seconds before rejecting
-      it. This
-      is useful if you want to reconnect to a peer as it disconnects and reconnects rapidly between web pages.</p>
-
-    <h3>Why am I unable to connect?</h3>
-    <p>You could be behind a symmetric NAT, in which case you'll need to set up a TURN server.</p>
-    <p>Another possible issue is your network blocking port 443, which the PeerServer cloud runs on. In this you must
-      use your
-      own PeerServer running on an appropriate port instead of the cloud service.</p>
-
-    <h3>What about latency/bandwidth?</h3>
-
-    <p>Data sent between the two peers do not touch any other servers, so the connection speed is limited only by the
-      upload
-      and download rates of the two peers. This also means you don't have the additional latency of an intermediary
-      server.</p>
-    <p>The latency to establish a connection can be split into two components: the brokering of data and the
-      identification
-      of clients. PeerJS has been designed to minimize the time you spend in these two areas. For brokering, data is
-      sent
-      through an XHR streaming request before a WebSocket connection is established, then through WebSockets. For client
-      identification, we provide you the ability to pass in your own peer IDs, thus eliminating the RTT for retrieving
-      an
-      ID from the server.</p>
-
-    <h3>More questions?</h3>
-    <p>
-      <a href="https://t.me/joinchat/ENhPuhTvhm8WlIxTjQf7Og">Discuss PeerJS on our Telegram channel.</a>
-      <br>
-      <br>
-    </p>
-  </section>
-
-  <header class="left">
-    <h2>API Reference
-      <a class="hide icon">&laquo;</a>
-      <a class="show icon">&raquo;</a>
-    </h2>
-  </header>
-  <header class="right">
-    <h2>Getting Started</h2>
-  </header>
-
-  <section class="api">
-    {{{html}}}
-  </section>
-
-</body>

+ 5 - 0
e2e/.eslintrc

@@ -0,0 +1,5 @@
+{
+	"env": {
+		"browser": true
+	}
+}

+ 5 - 0
e2e/alice.html

@@ -0,0 +1,5 @@
+<html>
+	<head>
+		<title>Alice</title>
+	</head>
+</html>

+ 5 - 0
e2e/bob.html

@@ -0,0 +1,5 @@
+<html>
+	<head>
+		<title>Bob</title>
+	</head>
+</html>

+ 1385 - 0
e2e/commit_data.js

@@ -0,0 +1,1385 @@
+export const commit_data = [
+	{
+		created_at: "2013-11-07T19:41:25-08:00",
+		payload: { issue_id: 22111847, comment_id: 28032260 },
+		public: true,
+		type: "IssueCommentEvent",
+		url: "https://github.com/peers/peerjs/issues/101#issuecomment-28032260",
+		actor: "ericz",
+		actor_attributes: {
+			login: "ericz",
+			type: "User",
+			gravatar_id: "c584ef7fe434331f7068ea49cacd88b9",
+			name: "Eric Zhang",
+			company: "Lever",
+			blog: "https://twitter.com/reallyez",
+			location: "Berkeley",
+			email: "eric@ericzhang.com",
+		},
+		repository: {
+			id: 7292898,
+			name: "peerjs",
+			url: "https://github.com/peers/peerjs",
+			description: "Peer-to-peer data in the browser.",
+			homepage: "https://peerjs.com",
+			watchers: 1647,
+			stargazers: 1647,
+			forks: 145,
+			fork: false,
+			size: 2188,
+			owner: "peers",
+			private: false,
+			open_issues: 20,
+			has_issues: true,
+			has_downloads: true,
+			has_wiki: true,
+			language: "JavaScript",
+			created_at: "2012-12-22T23:28:47-08:00",
+			pushed_at: "2013-11-09T22:58:31-08:00",
+			master_branch: "master",
+			organization: "peers",
+		},
+	},
+	{
+		created_at: "2013-11-07T19:35:50-08:00",
+		payload: { issue_id: 22196839, comment_id: 28031970 },
+		public: true,
+		type: "IssueCommentEvent",
+		url: "https://github.com/peers/peerjs/issues/103#issuecomment-28031970",
+		actor: "ericz",
+		actor_attributes: {
+			login: "ericz",
+			type: "User",
+			gravatar_id: "c584ef7fe434331f7068ea49cacd88b9",
+			name: "Eric Zhang",
+			company: "Lever",
+			blog: "https://twitter.com/reallyez",
+			location: "Berkeley",
+			email: "eric@ericzhang.com",
+		},
+		repository: {
+			id: 7292898,
+			name: "peerjs",
+			url: "https://github.com/peers/peerjs",
+			description: "Peer-to-peer data in the browser.",
+			homepage: "https://peerjs.com",
+			watchers: 1647,
+			stargazers: 1647,
+			forks: 145,
+			fork: false,
+			size: 2188,
+			owner: "peers",
+			private: false,
+			open_issues: 20,
+			has_issues: true,
+			has_downloads: true,
+			has_wiki: true,
+			language: "JavaScript",
+			created_at: "2012-12-22T23:28:47-08:00",
+			pushed_at: "2013-11-09T22:58:31-08:00",
+			master_branch: "master",
+			organization: "peers",
+		},
+	},
+	{
+		created_at: "2013-11-02T15:18:51-07:00",
+		payload: {
+			ref: "0.3.3",
+			ref_type: "tag",
+			master_branch: "master",
+			description: "Peer-to-peer data in the browser.",
+		},
+		public: true,
+		type: "CreateEvent",
+		url: "https://github.com/peers/peerjs/tree/0.3.3",
+		actor: "ericz",
+		actor_attributes: {
+			login: "ericz",
+			type: "User",
+			gravatar_id: "c584ef7fe434331f7068ea49cacd88b9",
+			name: "Eric Zhang",
+			company: "Lever",
+			blog: "https://twitter.com/reallyez",
+			location: "Berkeley",
+			email: "eric@ericzhang.com",
+		},
+		repository: {
+			id: 7292898,
+			name: "peerjs",
+			url: "https://github.com/peers/peerjs",
+			description: "Peer-to-peer data in the browser.",
+			homepage: "https://peerjs.com",
+			watchers: 1647,
+			stargazers: 1647,
+			forks: 145,
+			fork: false,
+			size: 2188,
+			owner: "peers",
+			private: false,
+			open_issues: 20,
+			has_issues: true,
+			has_downloads: true,
+			has_wiki: true,
+			language: "JavaScript",
+			created_at: "2012-12-22T23:28:47-08:00",
+			pushed_at: "2013-11-09T22:58:31-08:00",
+			master_branch: "master",
+			organization: "peers",
+		},
+	},
+	{
+		created_at: "2013-11-02T15:18:49-07:00",
+		payload: {
+			shas: [
+				[
+					"9976990c61c0f9c3c44f1de2a997ff8f21013d2a",
+					"really.ez@gmail.com",
+					"Bump to 0.3.3",
+					"ericz",
+					true,
+				],
+			],
+			size: 1,
+			ref: "refs/heads/master",
+			head: "9976990c61c0f9c3c44f1de2a997ff8f21013d2a",
+		},
+		public: true,
+		type: "PushEvent",
+		url: "https://github.com/peers/peerjs/compare/c9adf5076e...9976990c61",
+		actor: "ericz",
+		actor_attributes: {
+			login: "ericz",
+			type: "User",
+			gravatar_id: "c584ef7fe434331f7068ea49cacd88b9",
+			name: "Eric Zhang",
+			company: "Lever",
+			blog: "https://twitter.com/reallyez",
+			location: "Berkeley",
+			email: "eric@ericzhang.com",
+		},
+		repository: {
+			id: 7292898,
+			name: "peerjs",
+			url: "https://github.com/peers/peerjs",
+			description: "Peer-to-peer data in the browser.",
+			homepage: "https://peerjs.com",
+			watchers: 1647,
+			stargazers: 1647,
+			forks: 145,
+			fork: false,
+			size: 2188,
+			owner: "peers",
+			private: false,
+			open_issues: 20,
+			has_issues: true,
+			has_downloads: true,
+			has_wiki: true,
+			language: "JavaScript",
+			created_at: "2012-12-22T23:28:47-08:00",
+			pushed_at: "2013-11-09T22:58:31-08:00",
+			master_branch: "master",
+			organization: "peers",
+		},
+	},
+	{
+		created_at: "2013-11-02T15:15:18-07:00",
+		payload: {
+			shas: [
+				[
+					"ec1424c0f264ea5d8ce08ac2cc9ea2bd027a6a71",
+					"really.ez@gmail.com",
+					"Errant comma",
+					"ericz",
+					true,
+				],
+				[
+					"c9adf5076ea41df60231e11d032f371939228609",
+					"really.ez@gmail.com",
+					"Dont throw exception on failures",
+					"ericz",
+					true,
+				],
+			],
+			size: 2,
+			ref: "refs/heads/master",
+			head: "c9adf5076ea41df60231e11d032f371939228609",
+		},
+		public: true,
+		type: "PushEvent",
+		url: "https://github.com/peers/peerjs/compare/bb1045bf92...c9adf5076e",
+		actor: "ericz",
+		actor_attributes: {
+			login: "ericz",
+			type: "User",
+			gravatar_id: "c584ef7fe434331f7068ea49cacd88b9",
+			name: "Eric Zhang",
+			company: "Lever",
+			blog: "https://twitter.com/reallyez",
+			location: "Berkeley",
+			email: "eric@ericzhang.com",
+		},
+		repository: {
+			id: 7292898,
+			name: "peerjs",
+			url: "https://github.com/peers/peerjs",
+			description: "Peer-to-peer data in the browser.",
+			homepage: "https://peerjs.com",
+			watchers: 1647,
+			stargazers: 1647,
+			forks: 145,
+			fork: false,
+			size: 2188,
+			owner: "peers",
+			private: false,
+			open_issues: 20,
+			has_issues: true,
+			has_downloads: true,
+			has_wiki: true,
+			language: "JavaScript",
+			created_at: "2012-12-22T23:28:47-08:00",
+			pushed_at: "2013-11-09T22:58:31-08:00",
+			master_branch: "master",
+			organization: "peers",
+		},
+	},
+	{
+		created_at: "2013-11-02T15:02:02-07:00",
+		payload: {
+			shas: [
+				[
+					"bb1045bf92fbaef6e2156c3ac47015f70af5d866",
+					"really.ez@gmail.com",
+					"Fix errant comma",
+					"ericz",
+					true,
+				],
+			],
+			size: 1,
+			ref: "refs/heads/master",
+			head: "bb1045bf92fbaef6e2156c3ac47015f70af5d866",
+		},
+		public: true,
+		type: "PushEvent",
+		url: "https://github.com/peers/peerjs/compare/2db1c59998...bb1045bf92",
+		actor: "ericz",
+		actor_attributes: {
+			login: "ericz",
+			type: "User",
+			gravatar_id: "c584ef7fe434331f7068ea49cacd88b9",
+			name: "Eric Zhang",
+			company: "Lever",
+			blog: "https://twitter.com/reallyez",
+			location: "Berkeley",
+			email: "eric@ericzhang.com",
+		},
+		repository: {
+			id: 7292898,
+			name: "peerjs",
+			url: "https://github.com/peers/peerjs",
+			description: "Peer-to-peer data in the browser.",
+			homepage: "https://peerjs.com",
+			watchers: 1647,
+			stargazers: 1647,
+			forks: 145,
+			fork: false,
+			size: 2188,
+			owner: "peers",
+			private: false,
+			open_issues: 20,
+			has_issues: true,
+			has_downloads: true,
+			has_wiki: true,
+			language: "JavaScript",
+			created_at: "2012-12-22T23:28:47-08:00",
+			pushed_at: "2013-11-09T22:58:31-08:00",
+			master_branch: "master",
+			organization: "peers",
+		},
+	},
+	{
+		created_at: "2013-11-02T14:56:14-07:00",
+		payload: {
+			shas: [
+				[
+					"2db1c599987753079d197f73272a9e40e9290f73",
+					"really.ez@gmail.com",
+					"Remove errant comma",
+					"ericz",
+					true,
+				],
+			],
+			size: 1,
+			ref: "refs/heads/master",
+			head: "2db1c599987753079d197f73272a9e40e9290f73",
+		},
+		public: true,
+		type: "PushEvent",
+		url: "https://github.com/peers/peerjs/compare/214a14cc10...2db1c59998",
+		actor: "ericz",
+		actor_attributes: {
+			login: "ericz",
+			type: "User",
+			gravatar_id: "c584ef7fe434331f7068ea49cacd88b9",
+			name: "Eric Zhang",
+			company: "Lever",
+			blog: "https://twitter.com/reallyez",
+			location: "Berkeley",
+			email: "eric@ericzhang.com",
+		},
+		repository: {
+			id: 7292898,
+			name: "peerjs",
+			url: "https://github.com/peers/peerjs",
+			description: "Peer-to-peer data in the browser.",
+			homepage: "https://peerjs.com",
+			watchers: 1647,
+			stargazers: 1647,
+			forks: 145,
+			fork: false,
+			size: 2188,
+			owner: "peers",
+			private: false,
+			open_issues: 20,
+			has_issues: true,
+			has_downloads: true,
+			has_wiki: true,
+			language: "JavaScript",
+			created_at: "2012-12-22T23:28:47-08:00",
+			pushed_at: "2013-11-09T22:58:31-08:00",
+			master_branch: "master",
+			organization: "peers",
+		},
+	},
+	{
+		created_at: "2013-11-02T14:56:13-07:00",
+		payload: {
+			shas: [
+				[
+					"841921c349aff234b022bb966774d00ae22fef5e",
+					"really.ez@gmail.com",
+					"Errant comma",
+					"ericz",
+					true,
+				],
+			],
+			size: 1,
+			ref: "refs/heads/better-supports",
+			head: "841921c349aff234b022bb966774d00ae22fef5e",
+		},
+		public: true,
+		type: "PushEvent",
+		url: "https://github.com/peers/peerjs/compare/ccd80612ae...841921c349",
+		actor: "ericz",
+		actor_attributes: {
+			login: "ericz",
+			type: "User",
+			gravatar_id: "c584ef7fe434331f7068ea49cacd88b9",
+			name: "Eric Zhang",
+			company: "Lever",
+			blog: "https://twitter.com/reallyez",
+			location: "Berkeley",
+			email: "eric@ericzhang.com",
+		},
+		repository: {
+			id: 7292898,
+			name: "peerjs",
+			url: "https://github.com/peers/peerjs",
+			description: "Peer-to-peer data in the browser.",
+			homepage: "https://peerjs.com",
+			watchers: 1647,
+			stargazers: 1647,
+			forks: 145,
+			fork: false,
+			size: 2188,
+			owner: "peers",
+			private: false,
+			open_issues: 20,
+			has_issues: true,
+			has_downloads: true,
+			has_wiki: true,
+			language: "JavaScript",
+			created_at: "2012-12-22T23:28:47-08:00",
+			pushed_at: "2013-11-09T22:58:31-08:00",
+			master_branch: "master",
+			organization: "peers",
+		},
+	},
+	{
+		created_at: "2013-11-01T11:41:34-07:00",
+		payload: {
+			shas: [
+				[
+					"37350aaef3763d5a1bc63c4903f0f34ea9780d36",
+					"really.ez@gmail.com",
+					"Fix",
+					"Eric Zhang",
+					true,
+				],
+			],
+			size: 1,
+			ref: "refs/heads/master",
+			head: "37350aaef3763d5a1bc63c4903f0f34ea9780d36",
+		},
+		public: true,
+		type: "PushEvent",
+		url: "https://github.com/HackBerkeley/ascam/compare/7d76223acc...37350aaef3",
+		actor: "ericz",
+		actor_attributes: {
+			login: "ericz",
+			type: "User",
+			gravatar_id: "c584ef7fe434331f7068ea49cacd88b9",
+			name: "Eric Zhang",
+			company: "Lever",
+			blog: "https://twitter.com/reallyez",
+			location: "Berkeley",
+			email: "eric@ericzhang.com",
+		},
+		repository: {
+			id: 5557070,
+			name: "ascam",
+			url: "https://github.com/HackBerkeley/ascam",
+			description: "Ascii Webcam",
+			homepage: "",
+			watchers: 1,
+			stargazers: 1,
+			forks: 1,
+			fork: true,
+			size: 1973,
+			owner: "HackBerkeley",
+			private: false,
+			open_issues: 0,
+			has_issues: false,
+			has_downloads: true,
+			has_wiki: true,
+			language: "JavaScript",
+			created_at: "2012-08-25T20:19:01-07:00",
+			pushed_at: "2013-11-01T11:41:33-07:00",
+			master_branch: "master",
+			organization: "HackBerkeley",
+		},
+	},
+	{
+		created_at: "2013-10-31T10:56:20-07:00",
+		payload: { issue_id: 13813188, comment_id: 27509702 },
+		public: true,
+		type: "IssueCommentEvent",
+		url: "https://github.com/peers/peerjs/pull/45#issuecomment-27509702",
+		actor: "ericz",
+		actor_attributes: {
+			login: "ericz",
+			type: "User",
+			gravatar_id: "c584ef7fe434331f7068ea49cacd88b9",
+			name: "Eric Zhang",
+			company: "Lever",
+			blog: "https://twitter.com/reallyez",
+			location: "Berkeley",
+			email: "eric@ericzhang.com",
+		},
+		repository: {
+			id: 7292898,
+			name: "peerjs",
+			url: "https://github.com/peers/peerjs",
+			description: "Peer-to-peer data in the browser.",
+			homepage: "https://peerjs.com",
+			watchers: 1647,
+			stargazers: 1647,
+			forks: 145,
+			fork: false,
+			size: 2188,
+			owner: "peers",
+			private: false,
+			open_issues: 20,
+			has_issues: true,
+			has_downloads: true,
+			has_wiki: true,
+			language: "JavaScript",
+			created_at: "2012-12-22T23:28:47-08:00",
+			pushed_at: "2013-11-09T22:58:31-08:00",
+			master_branch: "master",
+			organization: "peers",
+		},
+	},
+	{
+		created_at: "2013-10-26T22:53:49-07:00",
+		payload: { issue_id: 21646195, comment_id: 27163465 },
+		public: true,
+		type: "IssueCommentEvent",
+		url: "https://github.com/peers/peerjs-server/issues/25#issuecomment-27163465",
+		actor: "ericz",
+		actor_attributes: {
+			login: "ericz",
+			type: "User",
+			gravatar_id: "c584ef7fe434331f7068ea49cacd88b9",
+			name: "Eric Zhang",
+			company: "Lever",
+			blog: "https://twitter.com/reallyez",
+			location: "Berkeley",
+			email: "eric@ericzhang.com",
+		},
+		repository: {
+			id: 7452705,
+			name: "peerjs-server",
+			url: "https://github.com/peers/peerjs-server",
+			description: "Server for PeerJS",
+			homepage: "https://peerjs.com",
+			watchers: 328,
+			stargazers: 328,
+			forks: 56,
+			fork: false,
+			size: 389,
+			owner: "peers",
+			private: false,
+			open_issues: 12,
+			has_issues: true,
+			has_downloads: true,
+			has_wiki: true,
+			language: "JavaScript",
+			created_at: "2013-01-04T22:49:08-08:00",
+			pushed_at: "2013-10-24T00:40:28-07:00",
+			master_branch: "master",
+			organization: "peers",
+		},
+	},
+	{
+		created_at: "2013-10-25T11:04:07-07:00",
+		payload: { action: "closed", issue: 16608955, number: 68 },
+		public: true,
+		type: "IssuesEvent",
+		url: "https://github.com/peers/peerjs/issues/68",
+		actor: "ericz",
+		actor_attributes: {
+			login: "ericz",
+			type: "User",
+			gravatar_id: "c584ef7fe434331f7068ea49cacd88b9",
+			name: "Eric Zhang",
+			company: "Lever",
+			blog: "https://twitter.com/reallyez",
+			location: "Berkeley",
+			email: "eric@ericzhang.com",
+		},
+		repository: {
+			id: 7292898,
+			name: "peerjs",
+			url: "https://github.com/peers/peerjs",
+			description: "Peer-to-peer data in the browser.",
+			homepage: "https://peerjs.com",
+			watchers: 1647,
+			stargazers: 1647,
+			forks: 145,
+			fork: false,
+			size: 2188,
+			owner: "peers",
+			private: false,
+			open_issues: 20,
+			has_issues: true,
+			has_downloads: true,
+			has_wiki: true,
+			language: "JavaScript",
+			created_at: "2012-12-22T23:28:47-08:00",
+			pushed_at: "2013-11-09T22:58:31-08:00",
+			master_branch: "master",
+			organization: "peers",
+		},
+	},
+	{
+		created_at: "2013-10-25T11:02:50-07:00",
+		payload: { issue_id: 21379154, comment_id: 27113277 },
+		public: true,
+		type: "IssueCommentEvent",
+		url: "https://github.com/peers/peerjs/issues/91#issuecomment-27113277",
+		actor: "ericz",
+		actor_attributes: {
+			login: "ericz",
+			type: "User",
+			gravatar_id: "c584ef7fe434331f7068ea49cacd88b9",
+			name: "Eric Zhang",
+			company: "Lever",
+			blog: "https://twitter.com/reallyez",
+			location: "Berkeley",
+			email: "eric@ericzhang.com",
+		},
+		repository: {
+			id: 7292898,
+			name: "peerjs",
+			url: "https://github.com/peers/peerjs",
+			description: "Peer-to-peer data in the browser.",
+			homepage: "https://peerjs.com",
+			watchers: 1647,
+			stargazers: 1647,
+			forks: 145,
+			fork: false,
+			size: 2188,
+			owner: "peers",
+			private: false,
+			open_issues: 20,
+			has_issues: true,
+			has_downloads: true,
+			has_wiki: true,
+			language: "JavaScript",
+			created_at: "2012-12-22T23:28:47-08:00",
+			pushed_at: "2013-11-09T22:58:31-08:00",
+			master_branch: "master",
+			organization: "peers",
+		},
+	},
+	{
+		created_at: "2013-10-25T11:01:53-07:00",
+		payload: { action: "closed", issue: 21531959, number: 96 },
+		public: true,
+		type: "IssuesEvent",
+		url: "https://github.com/peers/peerjs/issues/96",
+		actor: "ericz",
+		actor_attributes: {
+			login: "ericz",
+			type: "User",
+			gravatar_id: "c584ef7fe434331f7068ea49cacd88b9",
+			name: "Eric Zhang",
+			company: "Lever",
+			blog: "https://twitter.com/reallyez",
+			location: "Berkeley",
+			email: "eric@ericzhang.com",
+		},
+		repository: {
+			id: 7292898,
+			name: "peerjs",
+			url: "https://github.com/peers/peerjs",
+			description: "Peer-to-peer data in the browser.",
+			homepage: "https://peerjs.com",
+			watchers: 1647,
+			stargazers: 1647,
+			forks: 145,
+			fork: false,
+			size: 2188,
+			owner: "peers",
+			private: false,
+			open_issues: 20,
+			has_issues: true,
+			has_downloads: true,
+			has_wiki: true,
+			language: "JavaScript",
+			created_at: "2012-12-22T23:28:47-08:00",
+			pushed_at: "2013-11-09T22:58:31-08:00",
+			master_branch: "master",
+			organization: "peers",
+		},
+	},
+	{
+		created_at: "2013-10-25T11:01:25-07:00",
+		payload: { issue_id: 21379154, comment_id: 27113184 },
+		public: true,
+		type: "IssueCommentEvent",
+		url: "https://github.com/peers/peerjs/issues/91#issuecomment-27113184",
+		actor: "ericz",
+		actor_attributes: {
+			login: "ericz",
+			type: "User",
+			gravatar_id: "c584ef7fe434331f7068ea49cacd88b9",
+			name: "Eric Zhang",
+			company: "Lever",
+			blog: "https://twitter.com/reallyez",
+			location: "Berkeley",
+			email: "eric@ericzhang.com",
+		},
+		repository: {
+			id: 7292898,
+			name: "peerjs",
+			url: "https://github.com/peers/peerjs",
+			description: "Peer-to-peer data in the browser.",
+			homepage: "https://peerjs.com",
+			watchers: 1647,
+			stargazers: 1647,
+			forks: 145,
+			fork: false,
+			size: 2188,
+			owner: "peers",
+			private: false,
+			open_issues: 20,
+			has_issues: true,
+			has_downloads: true,
+			has_wiki: true,
+			language: "JavaScript",
+			created_at: "2012-12-22T23:28:47-08:00",
+			pushed_at: "2013-11-09T22:58:31-08:00",
+			master_branch: "master",
+			organization: "peers",
+		},
+	},
+	{
+		created_at: "2013-10-25T10:42:32-07:00",
+		payload: { issue_id: 21531959, comment_id: 27111740 },
+		public: true,
+		type: "IssueCommentEvent",
+		url: "https://github.com/peers/peerjs/issues/96#issuecomment-27111740",
+		actor: "ericz",
+		actor_attributes: {
+			login: "ericz",
+			type: "User",
+			gravatar_id: "c584ef7fe434331f7068ea49cacd88b9",
+			name: "Eric Zhang",
+			company: "Lever",
+			blog: "https://twitter.com/reallyez",
+			location: "Berkeley",
+			email: "eric@ericzhang.com",
+		},
+		repository: {
+			id: 7292898,
+			name: "peerjs",
+			url: "https://github.com/peers/peerjs",
+			description: "Peer-to-peer data in the browser.",
+			homepage: "https://peerjs.com",
+			watchers: 1647,
+			stargazers: 1647,
+			forks: 145,
+			fork: false,
+			size: 2188,
+			owner: "peers",
+			private: false,
+			open_issues: 20,
+			has_issues: true,
+			has_downloads: true,
+			has_wiki: true,
+			language: "JavaScript",
+			created_at: "2012-12-22T23:28:47-08:00",
+			pushed_at: "2013-11-09T22:58:31-08:00",
+			master_branch: "master",
+			organization: "peers",
+		},
+	},
+	{
+		created_at: "2013-10-25T10:42:13-07:00",
+		payload: { issue_id: 21531959, comment_id: 27111710 },
+		public: true,
+		type: "IssueCommentEvent",
+		url: "https://github.com/peers/peerjs/issues/96#issuecomment-27111710",
+		actor: "ericz",
+		actor_attributes: {
+			login: "ericz",
+			type: "User",
+			gravatar_id: "c584ef7fe434331f7068ea49cacd88b9",
+			name: "Eric Zhang",
+			company: "Lever",
+			blog: "https://twitter.com/reallyez",
+			location: "Berkeley",
+			email: "eric@ericzhang.com",
+		},
+		repository: {
+			id: 7292898,
+			name: "peerjs",
+			url: "https://github.com/peers/peerjs",
+			description: "Peer-to-peer data in the browser.",
+			homepage: "https://peerjs.com",
+			watchers: 1647,
+			stargazers: 1647,
+			forks: 145,
+			fork: false,
+			size: 2188,
+			owner: "peers",
+			private: false,
+			open_issues: 20,
+			has_issues: true,
+			has_downloads: true,
+			has_wiki: true,
+			language: "JavaScript",
+			created_at: "2012-12-22T23:28:47-08:00",
+			pushed_at: "2013-11-09T22:58:31-08:00",
+			master_branch: "master",
+			organization: "peers",
+		},
+	},
+	{
+		created_at: "2013-10-25T10:36:48-07:00",
+		payload: { issue_id: 21568882, comment_id: 27111312 },
+		public: true,
+		type: "IssueCommentEvent",
+		url: "https://github.com/peers/peerjs/issues/97#issuecomment-27111312",
+		actor: "ericz",
+		actor_attributes: {
+			login: "ericz",
+			type: "User",
+			gravatar_id: "c584ef7fe434331f7068ea49cacd88b9",
+			name: "Eric Zhang",
+			company: "Lever",
+			blog: "https://twitter.com/reallyez",
+			location: "Berkeley",
+			email: "eric@ericzhang.com",
+		},
+		repository: {
+			id: 7292898,
+			name: "peerjs",
+			url: "https://github.com/peers/peerjs",
+			description: "Peer-to-peer data in the browser.",
+			homepage: "https://peerjs.com",
+			watchers: 1647,
+			stargazers: 1647,
+			forks: 145,
+			fork: false,
+			size: 2188,
+			owner: "peers",
+			private: false,
+			open_issues: 20,
+			has_issues: true,
+			has_downloads: true,
+			has_wiki: true,
+			language: "JavaScript",
+			created_at: "2012-12-22T23:28:47-08:00",
+			pushed_at: "2013-11-09T22:58:31-08:00",
+			master_branch: "master",
+			organization: "peers",
+		},
+	},
+	{
+		created_at: "2013-10-23T15:52:46-07:00",
+		payload: { issue_id: 21487688, comment_id: 26953214 },
+		public: true,
+		type: "IssueCommentEvent",
+		url: "https://github.com/peers/peerjs/issues/95#issuecomment-26953214",
+		actor: "ericz",
+		actor_attributes: {
+			login: "ericz",
+			type: "User",
+			gravatar_id: "c584ef7fe434331f7068ea49cacd88b9",
+			name: "Eric Zhang",
+			company: "Lever",
+			blog: "https://twitter.com/reallyez",
+			location: "Berkeley",
+			email: "eric@ericzhang.com",
+		},
+		repository: {
+			id: 7292898,
+			name: "peerjs",
+			url: "https://github.com/peers/peerjs",
+			description: "Peer-to-peer data in the browser.",
+			homepage: "https://peerjs.com",
+			watchers: 1647,
+			stargazers: 1647,
+			forks: 145,
+			fork: false,
+			size: 2188,
+			owner: "peers",
+			private: false,
+			open_issues: 20,
+			has_issues: true,
+			has_downloads: true,
+			has_wiki: true,
+			language: "JavaScript",
+			created_at: "2012-12-22T23:28:47-08:00",
+			pushed_at: "2013-11-09T22:58:31-08:00",
+			master_branch: "master",
+			organization: "peers",
+		},
+	},
+	{
+		created_at: "2013-10-22T20:07:24-07:00",
+		payload: { action: "opened", issue: 21431237, number: 93 },
+		public: true,
+		type: "IssuesEvent",
+		url: "https://github.com/peers/peerjs/issues/93",
+		actor: "ericz",
+		actor_attributes: {
+			login: "ericz",
+			type: "User",
+			gravatar_id: "c584ef7fe434331f7068ea49cacd88b9",
+			name: "Eric Zhang",
+			company: "Lever",
+			blog: "https://twitter.com/reallyez",
+			location: "Berkeley",
+			email: "eric@ericzhang.com",
+		},
+		repository: {
+			id: 7292898,
+			name: "peerjs",
+			url: "https://github.com/peers/peerjs",
+			description: "Peer-to-peer data in the browser.",
+			homepage: "https://peerjs.com",
+			watchers: 1647,
+			stargazers: 1647,
+			forks: 145,
+			fork: false,
+			size: 2188,
+			owner: "peers",
+			private: false,
+			open_issues: 20,
+			has_issues: true,
+			has_downloads: true,
+			has_wiki: true,
+			language: "JavaScript",
+			created_at: "2012-12-22T23:28:47-08:00",
+			pushed_at: "2013-11-09T22:58:31-08:00",
+			master_branch: "master",
+			organization: "peers",
+		},
+	},
+	{
+		created_at: "2013-10-22T19:15:46-07:00",
+		payload: { issue_id: 21259595, comment_id: 26875922 },
+		public: true,
+		type: "IssueCommentEvent",
+		url: "https://github.com/peers/peerjs/issues/89#issuecomment-26875922",
+		actor: "ericz",
+		actor_attributes: {
+			login: "ericz",
+			type: "User",
+			gravatar_id: "c584ef7fe434331f7068ea49cacd88b9",
+			name: "Eric Zhang",
+			company: "Lever",
+			blog: "https://twitter.com/reallyez",
+			location: "Berkeley",
+			email: "eric@ericzhang.com",
+		},
+		repository: {
+			id: 7292898,
+			name: "peerjs",
+			url: "https://github.com/peers/peerjs",
+			description: "Peer-to-peer data in the browser.",
+			homepage: "https://peerjs.com",
+			watchers: 1647,
+			stargazers: 1647,
+			forks: 145,
+			fork: false,
+			size: 2188,
+			owner: "peers",
+			private: false,
+			open_issues: 20,
+			has_issues: true,
+			has_downloads: true,
+			has_wiki: true,
+			language: "JavaScript",
+			created_at: "2012-12-22T23:28:47-08:00",
+			pushed_at: "2013-11-09T22:58:31-08:00",
+			master_branch: "master",
+			organization: "peers",
+		},
+	},
+	{
+		created_at: "2013-10-22T19:06:59-07:00",
+		payload: { issue_id: 20917093, comment_id: 26875655 },
+		public: true,
+		type: "IssueCommentEvent",
+		url: "https://github.com/peers/peerjs/issues/86#issuecomment-26875655",
+		actor: "ericz",
+		actor_attributes: {
+			login: "ericz",
+			type: "User",
+			gravatar_id: "c584ef7fe434331f7068ea49cacd88b9",
+			name: "Eric Zhang",
+			company: "Lever",
+			blog: "https://twitter.com/reallyez",
+			location: "Berkeley",
+			email: "eric@ericzhang.com",
+		},
+		repository: {
+			id: 7292898,
+			name: "peerjs",
+			url: "https://github.com/peers/peerjs",
+			description: "Peer-to-peer data in the browser.",
+			homepage: "https://peerjs.com",
+			watchers: 1647,
+			stargazers: 1647,
+			forks: 145,
+			fork: false,
+			size: 2188,
+			owner: "peers",
+			private: false,
+			open_issues: 20,
+			has_issues: true,
+			has_downloads: true,
+			has_wiki: true,
+			language: "JavaScript",
+			created_at: "2012-12-22T23:28:47-08:00",
+			pushed_at: "2013-11-09T22:58:31-08:00",
+			master_branch: "master",
+			organization: "peers",
+		},
+	},
+	{
+		created_at: "2013-10-22T10:42:17-07:00",
+		payload: { issue_id: 21337614, comment_id: 26824752 },
+		public: true,
+		type: "IssueCommentEvent",
+		url: "https://github.com/peers/peerjs/issues/90#issuecomment-26824752",
+		actor: "ericz",
+		actor_attributes: {
+			login: "ericz",
+			type: "User",
+			gravatar_id: "c584ef7fe434331f7068ea49cacd88b9",
+			name: "Eric Zhang",
+			company: "Lever",
+			blog: "https://twitter.com/reallyez",
+			location: "Berkeley",
+			email: "eric@ericzhang.com",
+		},
+		repository: {
+			id: 7292898,
+			name: "peerjs",
+			url: "https://github.com/peers/peerjs",
+			description: "Peer-to-peer data in the browser.",
+			homepage: "https://peerjs.com",
+			watchers: 1647,
+			stargazers: 1647,
+			forks: 145,
+			fork: false,
+			size: 2188,
+			owner: "peers",
+			private: false,
+			open_issues: 20,
+			has_issues: true,
+			has_downloads: true,
+			has_wiki: true,
+			language: "JavaScript",
+			created_at: "2012-12-22T23:28:47-08:00",
+			pushed_at: "2013-11-09T22:58:31-08:00",
+			master_branch: "master",
+			organization: "peers",
+		},
+	},
+	{
+		created_at: "2013-10-21T21:00:18-07:00",
+		payload: { issue_id: 21337614, comment_id: 26775684 },
+		public: true,
+		type: "IssueCommentEvent",
+		url: "https://github.com/peers/peerjs/issues/90#issuecomment-26775684",
+		actor: "ericz",
+		actor_attributes: {
+			login: "ericz",
+			type: "User",
+			gravatar_id: "c584ef7fe434331f7068ea49cacd88b9",
+			name: "Eric Zhang",
+			company: "Lever",
+			blog: "https://twitter.com/reallyez",
+			location: "Berkeley",
+			email: "eric@ericzhang.com",
+		},
+		repository: {
+			id: 7292898,
+			name: "peerjs",
+			url: "https://github.com/peers/peerjs",
+			description: "Peer-to-peer data in the browser.",
+			homepage: "https://peerjs.com",
+			watchers: 1647,
+			stargazers: 1647,
+			forks: 145,
+			fork: false,
+			size: 2188,
+			owner: "peers",
+			private: false,
+			open_issues: 20,
+			has_issues: true,
+			has_downloads: true,
+			has_wiki: true,
+			language: "JavaScript",
+			created_at: "2012-12-22T23:28:47-08:00",
+			pushed_at: "2013-11-09T22:58:31-08:00",
+			master_branch: "master",
+			organization: "peers",
+		},
+	},
+	{
+		created_at: "2013-10-19T02:06:52-07:00",
+		payload: {
+			ref: "0.3.1",
+			ref_type: "tag",
+			master_branch: "master",
+			description: "Peer-to-peer data in the browser.",
+		},
+		public: true,
+		type: "CreateEvent",
+		url: "https://github.com/peers/peerjs/tree/0.3.1",
+		actor: "ericz",
+		actor_attributes: {
+			login: "ericz",
+			type: "User",
+			gravatar_id: "c584ef7fe434331f7068ea49cacd88b9",
+			name: "Eric Zhang",
+			company: "Lever",
+			blog: "https://twitter.com/reallyez",
+			location: "Berkeley",
+			email: "eric@ericzhang.com",
+		},
+		repository: {
+			id: 7292898,
+			name: "peerjs",
+			url: "https://github.com/peers/peerjs",
+			description: "Peer-to-peer data in the browser.",
+			homepage: "https://peerjs.com",
+			watchers: 1647,
+			stargazers: 1647,
+			forks: 145,
+			fork: false,
+			size: 2188,
+			owner: "peers",
+			private: false,
+			open_issues: 20,
+			has_issues: true,
+			has_downloads: true,
+			has_wiki: true,
+			language: "JavaScript",
+			created_at: "2012-12-22T23:28:47-08:00",
+			pushed_at: "2013-11-09T22:58:31-08:00",
+			master_branch: "master",
+			organization: "peers",
+		},
+	},
+	{
+		created_at: "2013-10-19T02:06:43-07:00",
+		payload: { ref: "0.3.1", ref_type: "tag" },
+		public: true,
+		type: "DeleteEvent",
+		url: "https://github.com/",
+		actor: "ericz",
+		actor_attributes: {
+			login: "ericz",
+			type: "User",
+			gravatar_id: "c584ef7fe434331f7068ea49cacd88b9",
+			name: "Eric Zhang",
+			company: "Lever",
+			blog: "https://twitter.com/reallyez",
+			location: "Berkeley",
+			email: "eric@ericzhang.com",
+		},
+		repository: {
+			id: 7292898,
+			name: "peerjs",
+			url: "https://github.com/peers/peerjs",
+			description: "Peer-to-peer data in the browser.",
+			homepage: "https://peerjs.com",
+			watchers: 1647,
+			stargazers: 1647,
+			forks: 145,
+			fork: false,
+			size: 2188,
+			owner: "peers",
+			private: false,
+			open_issues: 20,
+			has_issues: true,
+			has_downloads: true,
+			has_wiki: true,
+			language: "JavaScript",
+			created_at: "2012-12-22T23:28:47-08:00",
+			pushed_at: "2013-11-09T22:58:31-08:00",
+			master_branch: "master",
+			organization: "peers",
+		},
+	},
+	{
+		created_at: "2013-10-19T02:05:42-07:00",
+		payload: {
+			shas: [
+				[
+					"720eb3e881220f78eaca3d715ce7afe9324d1a3e",
+					"really.ez@gmail.com",
+					"0.3.1",
+					"ericz",
+					true,
+				],
+				[
+					"79e10688c56524479f3b2c0cb069c4ac7e065b57",
+					"really.ez@gmail.com",
+					"Set maxRetransmits to 0 when reliable false",
+					"ericz",
+					true,
+				],
+			],
+			size: 2,
+			ref: "refs/heads/master",
+			head: "79e10688c56524479f3b2c0cb069c4ac7e065b57",
+		},
+		public: true,
+		type: "PushEvent",
+		url: "https://github.com/peers/peerjs/compare/b474a4cba6...79e10688c5",
+		actor: "ericz",
+		actor_attributes: {
+			login: "ericz",
+			type: "User",
+			gravatar_id: "c584ef7fe434331f7068ea49cacd88b9",
+			name: "Eric Zhang",
+			company: "Lever",
+			blog: "https://twitter.com/reallyez",
+			location: "Berkeley",
+			email: "eric@ericzhang.com",
+		},
+		repository: {
+			id: 7292898,
+			name: "peerjs",
+			url: "https://github.com/peers/peerjs",
+			description: "Peer-to-peer data in the browser.",
+			homepage: "https://peerjs.com",
+			watchers: 1647,
+			stargazers: 1647,
+			forks: 145,
+			fork: false,
+			size: 2188,
+			owner: "peers",
+			private: false,
+			open_issues: 20,
+			has_issues: true,
+			has_downloads: true,
+			has_wiki: true,
+			language: "JavaScript",
+			created_at: "2012-12-22T23:28:47-08:00",
+			pushed_at: "2013-11-09T22:58:31-08:00",
+			master_branch: "master",
+			organization: "peers",
+		},
+	},
+	{
+		created_at: "2013-10-19T02:02:15-07:00",
+		payload: {
+			ref: "0.3.1",
+			ref_type: "tag",
+			master_branch: "master",
+			description: "Peer-to-peer data in the browser.",
+		},
+		public: true,
+		type: "CreateEvent",
+		url: "https://github.com/peers/peerjs/tree/0.3.1",
+		actor: "ericz",
+		actor_attributes: {
+			login: "ericz",
+			type: "User",
+			gravatar_id: "c584ef7fe434331f7068ea49cacd88b9",
+			name: "Eric Zhang",
+			company: "Lever",
+			blog: "https://twitter.com/reallyez",
+			location: "Berkeley",
+			email: "eric@ericzhang.com",
+		},
+		repository: {
+			id: 7292898,
+			name: "peerjs",
+			url: "https://github.com/peers/peerjs",
+			description: "Peer-to-peer data in the browser.",
+			homepage: "https://peerjs.com",
+			watchers: 1647,
+			stargazers: 1647,
+			forks: 145,
+			fork: false,
+			size: 2188,
+			owner: "peers",
+			private: false,
+			open_issues: 20,
+			has_issues: true,
+			has_downloads: true,
+			has_wiki: true,
+			language: "JavaScript",
+			created_at: "2012-12-22T23:28:47-08:00",
+			pushed_at: "2013-11-09T22:58:31-08:00",
+			master_branch: "master",
+			organization: "peers",
+		},
+	},
+	{
+		created_at: "2013-10-19T02:02:13-07:00",
+		payload: {
+			shas: [
+				[
+					"b474a4cba6156dabd1312cd25a520b4286e362f6",
+					"really.ez@gmail.com",
+					"Setting reliable to false by default",
+					"ericz",
+					true,
+				],
+			],
+			size: 1,
+			ref: "refs/heads/master",
+			head: "b474a4cba6156dabd1312cd25a520b4286e362f6",
+		},
+		public: true,
+		type: "PushEvent",
+		url: "https://github.com/peers/peerjs/compare/93fc4931b2...b474a4cba6",
+		actor: "ericz",
+		actor_attributes: {
+			login: "ericz",
+			type: "User",
+			gravatar_id: "c584ef7fe434331f7068ea49cacd88b9",
+			name: "Eric Zhang",
+			company: "Lever",
+			blog: "https://twitter.com/reallyez",
+			location: "Berkeley",
+			email: "eric@ericzhang.com",
+		},
+		repository: {
+			id: 7292898,
+			name: "peerjs",
+			url: "https://github.com/peers/peerjs",
+			description: "Peer-to-peer data in the browser.",
+			homepage: "https://peerjs.com",
+			watchers: 1647,
+			stargazers: 1647,
+			forks: 145,
+			fork: false,
+			size: 2188,
+			owner: "peers",
+			private: false,
+			open_issues: 20,
+			has_issues: true,
+			has_downloads: true,
+			has_wiki: true,
+			language: "JavaScript",
+			created_at: "2012-12-22T23:28:47-08:00",
+			pushed_at: "2013-11-09T22:58:31-08:00",
+			master_branch: "master",
+			organization: "peers",
+		},
+	},
+	{
+		created_at: "2013-10-19T01:53:16-07:00",
+		payload: {
+			shas: [
+				[
+					"3949c236345171987b9291059fbaf9024eeca680",
+					"really.ez@gmail.com",
+					"0.3.1",
+					"ericz",
+					true,
+				],
+				[
+					"93fc4931b24c0261c5fda71e0441a5ee8bda70b2",
+					"really.ez@gmail.com",
+					"Update reliable doc",
+					"ericz",
+					true,
+				],
+			],
+			size: 2,
+			ref: "refs/heads/master",
+			head: "93fc4931b24c0261c5fda71e0441a5ee8bda70b2",
+		},
+		public: true,
+		type: "PushEvent",
+		url: "https://github.com/peers/peerjs/compare/cd287e2fae...93fc4931b2",
+		actor: "ericz",
+		actor_attributes: {
+			login: "ericz",
+			type: "User",
+			gravatar_id: "c584ef7fe434331f7068ea49cacd88b9",
+			name: "Eric Zhang",
+			company: "Lever",
+			blog: "https://twitter.com/reallyez",
+			location: "Berkeley",
+			email: "eric@ericzhang.com",
+		},
+		repository: {
+			id: 7292898,
+			name: "peerjs",
+			url: "https://github.com/peers/peerjs",
+			description: "Peer-to-peer data in the browser.",
+			homepage: "https://peerjs.com",
+			watchers: 1647,
+			stargazers: 1647,
+			forks: 145,
+			fork: false,
+			size: 2188,
+			owner: "peers",
+			private: false,
+			open_issues: 20,
+			has_issues: true,
+			has_downloads: true,
+			has_wiki: true,
+			language: "JavaScript",
+			created_at: "2012-12-22T23:28:47-08:00",
+			pushed_at: "2013-11-09T22:58:31-08:00",
+			master_branch: "master",
+			organization: "peers",
+		},
+	},
+];

+ 75 - 0
e2e/data.js

@@ -0,0 +1,75 @@
+export const numbers = [
+	0,
+	1,
+	-1,
+	//
+	Math.PI,
+	-Math.PI,
+	//8 bit
+	0x7f,
+	0x0f,
+	//16 bit
+	0x7fff,
+	0x0fff,
+	//32 bit
+	0x7fffffff,
+	0x0fffffff,
+	//64 bit
+	// 0x7FFFFFFFFFFFFFFF,
+	// eslint-disable-next-line no-loss-of-precision
+	0x0fffffffffffffff,
+];
+export const long_string = "ThisIsÁTèstString".repeat(100000);
+export const strings = [
+	"",
+	"hello",
+	"café",
+	"中文",
+	"broccoli🥦līp𨋢grin😃ok",
+	"\u{10ffff}",
+];
+
+export { commit_data } from "./commit_data.js";
+
+export const uint8_arrays = [
+	new Uint8Array(),
+	new Uint8Array([0]),
+	new Uint8Array([0, 1, 2, 3, 4, 6, 7]),
+	new Uint8Array([0, 1, 2, 3, 4, 6, 78, 9, 10, 11, 12, 13, 14, 15]),
+	new Uint8Array([
+		0, 1, 2, 3, 4, 6, 78, 9, 10, 11, 12, 13, 14, 15, 17, 18, 19, 20, 21, 22, 23,
+		24, 25, 26, 27, 28, 30, 31,
+	]),
+];
+
+export const int32_arrays = [
+	new Int32Array([0].map((x) => -x)),
+	new Int32Array([0, 1, 2, 3, 4, 6, 7].map((x) => -x)),
+	new Int32Array(
+		[0, 1, 2, 3, 4, 6, 78, 9, 10, 11, 12, 13, 14, 15].map((x) => -x),
+	),
+	new Int32Array(
+		[
+			0, 1, 2, 3, 4, 6, 78, 9, 10, 11, 12, 13, 14, 15, 17, 18, 19, 20, 21, 22,
+			23, 24, 25, 26, 27, 28, 30, 31,
+		].map((x) => -x),
+	),
+];
+
+export const typed_array_view = new Uint8Array(uint8_arrays[4].buffer, 4);
+
+export const array_buffers = [
+	new Uint8Array(),
+	new Uint8Array([0]),
+	new Uint8Array([0, 1, 2, 3, 4, 6, 7]),
+	new Uint8Array([0, 1, 2, 3, 4, 6, 78, 9, 10, 11, 12, 13, 14, 15]),
+	new Uint8Array([
+		0, 1, 2, 3, 4, 6, 78, 9, 10, 11, 12, 13, 14, 15, 17, 18, 19, 20, 21, 22, 23,
+		24, 25, 26, 27, 28, 30, 31,
+	]),
+].map((x) => x.buffer);
+
+export const dates = [
+	new Date(Date.UTC(2024, 1, 1, 1, 1, 1, 1)),
+	new Date(Date.UTC(1, 1, 1, 1, 1, 1, 1)),
+];

+ 18 - 0
e2e/datachannel/Int32Array.js

@@ -0,0 +1,18 @@
+import { int32_arrays } from "../data.js";
+import { expect } from "https://esm.sh/v126/chai@4.3.7/X-dHMvZXhwZWN0/es2021/chai.bundle.mjs";
+
+/** @param {unknown[]} received */
+export const check = (received) => {
+	for (const [i, typed_array] of int32_arrays.entries()) {
+		expect(received[i]).to.be.an.instanceof(Int32Array);
+		expect(received[i]).to.deep.equal(typed_array);
+	}
+};
+/**
+ * @param {import("../peerjs").DataConnection} dataConnection
+ */
+export const send = (dataConnection) => {
+	for (const typed_array of int32_arrays) {
+		dataConnection.send(typed_array);
+	}
+};

+ 18 - 0
e2e/datachannel/Int32Array_as_ArrayBuffer.js

@@ -0,0 +1,18 @@
+import { int32_arrays } from "../data.js";
+import { expect } from "https://esm.sh/v126/chai@4.3.7/X-dHMvZXhwZWN0/es2021/chai.bundle.mjs";
+
+/** @param {unknown[]} received */
+export const check = (received) => {
+	for (const [i, typed_array] of int32_arrays.entries()) {
+		expect(received[i]).to.be.an.instanceof(ArrayBuffer);
+		expect(new Int32Array(received[i])).to.deep.equal(typed_array);
+	}
+};
+/**
+ * @param {import("../peerjs").DataConnection} dataConnection
+ */
+export const send = (dataConnection) => {
+	for (const typed_array of int32_arrays) {
+		dataConnection.send(typed_array);
+	}
+};

+ 18 - 0
e2e/datachannel/Int32Array_as_Uint8Array.js

@@ -0,0 +1,18 @@
+import { int32_arrays } from "../data.js";
+import { expect } from "https://esm.sh/v126/chai@4.3.7/X-dHMvZXhwZWN0/es2021/chai.bundle.mjs";
+
+/** @param {unknown[]} received */
+export const check = (received) => {
+	for (const [i, typed_array] of int32_arrays.entries()) {
+		expect(received[i]).to.be.an.instanceof(Uint8Array);
+		expect(received[i]).to.deep.equal(new Uint8Array(typed_array.buffer));
+	}
+};
+/**
+ * @param {import("../peerjs").DataConnection} dataConnection
+ */
+export const send = (dataConnection) => {
+	for (const typed_array of int32_arrays) {
+		dataConnection.send(typed_array);
+	}
+};

+ 17 - 0
e2e/datachannel/TypedArrayView_as_ArrayBuffer.js

@@ -0,0 +1,17 @@
+import { typed_array_view } from "../data.js";
+import { expect } from "https://esm.sh/v126/chai@4.3.7/X-dHMvZXhwZWN0/es2021/chai.bundle.mjs";
+
+/** @param {unknown[]} received */
+export const check = (received) => {
+	const received_typed_array_view = received[0];
+	expect(received_typed_array_view).to.be.instanceOf(ArrayBuffer);
+	expect(new Uint8Array(received_typed_array_view)).to.deep.equal(
+		typed_array_view,
+	);
+};
+/**
+ * @param {import("../peerjs").DataConnection} dataConnection
+ */
+export const send = (dataConnection) => {
+	dataConnection.send(typed_array_view);
+};

+ 18 - 0
e2e/datachannel/Uint8Array.js

@@ -0,0 +1,18 @@
+import { uint8_arrays } from "../data.js";
+import { expect } from "https://esm.sh/v126/chai@4.3.7/X-dHMvZXhwZWN0/es2021/chai.bundle.mjs";
+
+/** @param {unknown[]} received */
+export const check = (received) => {
+	for (const [i, typed_array] of uint8_arrays.entries()) {
+		expect(received[i]).to.be.an.instanceof(Uint8Array);
+		expect(received[i]).to.deep.equal(typed_array);
+	}
+};
+/**
+ * @param {import("../peerjs").DataConnection} dataConnection
+ */
+export const send = (dataConnection) => {
+	for (const typed_array of uint8_arrays) {
+		dataConnection.send(typed_array);
+	}
+};

+ 18 - 0
e2e/datachannel/Uint8Array_as_ArrayBuffer.js

@@ -0,0 +1,18 @@
+import { uint8_arrays } from "../data.js";
+import { expect } from "https://esm.sh/v126/chai@4.3.7/X-dHMvZXhwZWN0/es2021/chai.bundle.mjs";
+
+/** @param {unknown[]} received */
+export const check = (received) => {
+	for (const [i, typed_array] of uint8_arrays.entries()) {
+		expect(received[i]).to.be.an.instanceof(ArrayBuffer);
+		expect(new Uint8Array(received[i])).to.deep.equal(typed_array);
+	}
+};
+/**
+ * @param {import("../peerjs").DataConnection} dataConnection
+ */
+export const send = (dataConnection) => {
+	for (const typed_array of uint8_arrays) {
+		dataConnection.send(typed_array);
+	}
+};

+ 18 - 0
e2e/datachannel/arraybuffers.js

@@ -0,0 +1,18 @@
+import { array_buffers } from "../data.js";
+import { expect } from "https://esm.sh/v126/chai@4.3.7/X-dHMvZXhwZWN0/es2021/chai.bundle.mjs";
+
+/** @param {unknown[]} received */
+export const check = (received) => {
+	for (const [i, array_buffer] of array_buffers.entries()) {
+		expect(received[i]).to.be.an.instanceof(ArrayBuffer);
+		expect(received[i]).to.deep.equal(array_buffer);
+	}
+};
+/**
+ * @param {import("../peerjs").DataConnection} dataConnection
+ */
+export const send = (dataConnection) => {
+	for (const array_buffer of array_buffers) {
+		dataConnection.send(array_buffer);
+	}
+};

+ 18 - 0
e2e/datachannel/arraybuffers_as_uint8array.js

@@ -0,0 +1,18 @@
+import { array_buffers } from "../data.js";
+import { expect } from "https://esm.sh/v126/chai@4.3.7/X-dHMvZXhwZWN0/es2021/chai.bundle.mjs";
+
+/** @param {unknown[]} received */
+export const check = (received) => {
+	for (const [i, array_buffer] of array_buffers.entries()) {
+		expect(received[i]).to.be.an.instanceof(Uint8Array);
+		expect(received[i]).to.deep.equal(new Uint8Array(array_buffer));
+	}
+};
+/**
+ * @param {import("../peerjs").DataConnection} dataConnection
+ */
+export const send = (dataConnection) => {
+	for (const array_buffer of array_buffers) {
+		dataConnection.send(array_buffer);
+	}
+};

+ 15 - 0
e2e/datachannel/arrays.js

@@ -0,0 +1,15 @@
+import { commit_data } from "../data.js";
+import { expect } from "https://esm.sh/v126/chai@4.3.7/X-dHMvZXhwZWN0/es2021/chai.bundle.mjs";
+
+/** @param {unknown[]} received */
+export const check = (received) => {
+	expect(received[1]).to.be.an("array").with.lengthOf(commit_data.length);
+	expect(received).to.deep.equal([[], commit_data]);
+};
+/**
+ * @param {import("../peerjs").DataConnection} dataConnection
+ */
+export const send = (dataConnection) => {
+	dataConnection.send([]);
+	dataConnection.send(commit_data);
+};

+ 18 - 0
e2e/datachannel/arrays_unchunked.js

@@ -0,0 +1,18 @@
+/**
+ * JSON transfer does not chunk, commit_data is too large to send
+ */
+
+import { commit_data } from "../data.js";
+import { expect } from "https://esm.sh/v126/chai@4.3.7/X-dHMvZXhwZWN0/es2021/chai.bundle.mjs";
+
+/** @param {unknown[]} received */
+export const check = (received) => {
+	expect(received).to.deep.equal([[], commit_data.slice(0, 2)]);
+};
+/**
+ * @param {import("../peerjs").DataConnection} dataConnection
+ */
+export const send = (dataConnection) => {
+	dataConnection.send([]);
+	dataConnection.send(commit_data.slice(0, 2));
+};

+ 15 - 0
e2e/datachannel/dates.js

@@ -0,0 +1,15 @@
+import { dates } from "../data.js";
+import { expect } from "https://esm.sh/v126/chai@4.3.7/X-dHMvZXhwZWN0/es2021/chai.bundle.mjs";
+
+/** @param {unknown[]} received */
+export const check = (received) => {
+	expect(received).to.deep.equal(dates);
+};
+/**
+ * @param {import("../peerjs").DataConnection} dataConnection
+ */
+export const send = (dataConnection) => {
+	for (const date of dates) {
+		dataConnection.send(date);
+	}
+};

+ 16 - 0
e2e/datachannel/dates_as_json_string.js

@@ -0,0 +1,16 @@
+import { dates } from "../data.js";
+import { expect } from "https://esm.sh/v126/chai@4.3.7/X-dHMvZXhwZWN0/es2021/chai.bundle.mjs";
+
+/** @param {unknown[]} received */
+export const check = (received) => {
+	console.log(dates.map((date) => date.toString()));
+	expect(received).to.deep.equal(dates.map((date) => date.toJSON()));
+};
+/**
+ * @param {import("../peerjs").DataConnection} dataConnection
+ */
+export const send = (dataConnection) => {
+	for (const date of dates) {
+		dataConnection.send(date);
+	}
+};

+ 16 - 0
e2e/datachannel/dates_as_string.js

@@ -0,0 +1,16 @@
+import { dates } from "../data.js";
+import { expect } from "https://esm.sh/v126/chai@4.3.7/X-dHMvZXhwZWN0/es2021/chai.bundle.mjs";
+
+/** @param {unknown[]} received */
+export const check = (received) => {
+	console.log(dates.map((date) => date.toString()));
+	expect(received).to.deep.equal(dates.map((date) => date.toString()));
+};
+/**
+ * @param {import("../peerjs").DataConnection} dataConnection
+ */
+export const send = (dataConnection) => {
+	for (const date of dates) {
+		dataConnection.send(date);
+	}
+};

+ 13 - 0
e2e/datachannel/long_string.js

@@ -0,0 +1,13 @@
+import { long_string } from "../data.js";
+import { expect } from "https://esm.sh/v126/chai@4.3.7/X-dHMvZXhwZWN0/es2021/chai.bundle.mjs";
+
+/** @param {unknown[]} received */
+export const check = (received) => {
+	expect(received).to.deep.equal([long_string]);
+};
+/**
+ * @param {import("../peerjs").DataConnection} dataConnection
+ */
+export const send = (dataConnection) => {
+	dataConnection.send(long_string);
+};

+ 15 - 0
e2e/datachannel/numbers.js

@@ -0,0 +1,15 @@
+import { numbers } from "../data.js";
+import { expect } from "https://esm.sh/v126/chai@4.3.7/X-dHMvZXhwZWN0/es2021/chai.bundle.mjs";
+
+/** @param {unknown[]} received */
+export const check = (received) => {
+	expect(received).to.deep.equal(numbers);
+};
+/**
+ * @param {import("../peerjs").DataConnection} dataConnection
+ */
+export const send = (dataConnection) => {
+	for (const number of numbers) {
+		dataConnection.send(number);
+	}
+};

+ 16 - 0
e2e/datachannel/objects.js

@@ -0,0 +1,16 @@
+import { commit_data } from "../data.js";
+import { expect } from "https://esm.sh/v126/chai@4.3.7/X-dHMvZXhwZWN0/es2021/chai.bundle.mjs";
+
+/** @param {unknown[]} received */
+export const check = (received) => {
+	expect(received).to.be.an("array").with.lengthOf(commit_data.length);
+	expect(received).to.deep.equal(commit_data);
+};
+/**
+ * @param {import("../peerjs").DataConnection} dataConnection
+ */
+export const send = (dataConnection) => {
+	for (const commit of commit_data) {
+		dataConnection.send(commit);
+	}
+};

+ 23 - 0
e2e/datachannel/serialization.html

@@ -0,0 +1,23 @@
+<!doctype html>
+<html lang="en">
+	<head>
+		<meta charset="UTF-8" />
+		<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+		<title></title>
+		<link rel="stylesheet" href="../style.css" />
+	</head>
+	<body>
+		<h1>Serialization</h1>
+		<div id="inputs">
+			<input type="text" id="receiver-id" placeholder="Receiver ID" />
+			<button id="connect-btn" disabled>Connect</button>
+			<button id="send-btn">Send</button>
+			<button id="check-btn">Check</button>
+		</div>
+		<div id="messages"></div>
+		<div id="result"></div>
+		<div id="error-message"></div>
+		<script src="/dist/peerjs.min.js" type="application/javascript"></script>
+		<script src="serialization.js" type="module"></script>
+	</body>
+</html>

+ 94 - 0
e2e/datachannel/serialization.js

@@ -0,0 +1,94 @@
+/**
+ * @type {typeof import("../../lib/exports.js").Peer}
+ */
+const Peer = window.peerjs.Peer;
+
+const params = new URLSearchParams(document.location.search);
+const testfile = params.get("testfile");
+const serialization = params.get("serialization");
+
+(async () => {
+	let serializers = {};
+	try {
+		const { Cbor } = await import("/dist/serializer.cbor.mjs");
+		const { MsgPack } = await import("/dist/serializer.msgpack.mjs");
+		serializers = {
+			Cbor,
+			MsgPack,
+		};
+	} catch (e) {
+		console.log(e);
+	}
+
+	const { check, send } = await import(`./${testfile}.js`);
+	document.getElementsByTagName("title")[0].innerText =
+		window.location.hash.substring(1);
+
+	const checkBtn = document.getElementById("check-btn");
+	const sendBtn = document.getElementById("send-btn");
+	const receiverIdInput = document.getElementById("receiver-id");
+	const connectBtn = document.getElementById("connect-btn");
+	const messages = document.getElementById("messages");
+	const result = document.getElementById("result");
+	const errorMessage = document.getElementById("error-message");
+
+	const peer = new Peer({ debug: 3, serializers });
+	const received = [];
+	/**
+	 * @type {import("../../lib/exports.js").DataConnection}
+	 */
+	let dataConnection;
+	peer
+		.once("open", (id) => {
+			messages.textContent = `Your Peer ID: ${id}`;
+		})
+		.once("error", (error) => {
+			errorMessage.textContent = JSON.stringify(error);
+		})
+		.once("connection", (connection) => {
+			dataConnection = connection;
+			dataConnection.on("data", (data) => {
+				console.log(data);
+				received.push(data);
+			});
+			dataConnection.once("close", () => {
+				messages.textContent = "Closed!";
+			});
+		});
+
+	connectBtn.addEventListener("click", () => {
+		const receiverId = receiverIdInput.value;
+		if (receiverId) {
+			dataConnection = peer.connect(receiverId, {
+				reliable: true,
+				serialization,
+			});
+			dataConnection.once("open", () => {
+				messages.textContent = "Connected!";
+			});
+		}
+	});
+
+	checkBtn.addEventListener("click", async () => {
+		try {
+			console.log(received);
+			check(received);
+			result.textContent = "Success!";
+		} catch (e) {
+			result.textContent = "Failed!";
+			errorMessage.textContent = JSON.stringify(e.message);
+		} finally {
+			messages.textContent = "Checked!";
+		}
+	});
+
+	sendBtn.addEventListener("click", () => {
+		dataConnection.once("error", (err) => {
+			errorMessage.innerText = err.toString();
+		});
+		send(dataConnection);
+		dataConnection.close({ flush: true });
+		messages.textContent = "Sent!";
+	});
+	window["connect-btn"].disabled = false;
+})();

+ 67 - 0
e2e/datachannel/serialization.page.ts

@@ -0,0 +1,67 @@
+import { browser, $ } from "@wdio/globals";
+class SerializationPage {
+	get sendBtn() {
+		return $("button[id='send-btn']");
+	}
+
+	get checkBtn() {
+		return $("button[id='check-btn']");
+	}
+	get connectBtn() {
+		return $("button[id='connect-btn']");
+	}
+
+	get receiverId() {
+		return $("input[id='receiver-id']");
+	}
+
+	get messages() {
+		return $("div[id='messages']");
+	}
+
+	get errorMessage() {
+		return $("div[id='error-message']");
+	}
+
+	get result() {
+		return $("div[id='result']");
+	}
+
+	waitForMessage(right: string) {
+		return browser.waitUntil(
+			async () => {
+				const messages = await this.messages.getText();
+				return messages.startsWith(right);
+			},
+			{ timeoutMsg: `Expected message to start with ${right}`, timeout: 10000 },
+		);
+	}
+
+	async open(testFile: string, serialization: string) {
+		await browser.switchWindow("Alice");
+		await browser.url(
+			`/e2e/datachannel/serialization.html?testfile=${testFile}&serialization=${serialization}#Alice`,
+		);
+		await this.connectBtn.waitForEnabled();
+
+		await browser.switchWindow("Bob");
+		await browser.url(
+			`/e2e/datachannel/serialization.html?testfile=${testFile}#Bob`,
+		);
+		await this.connectBtn.waitForEnabled();
+	}
+	async init() {
+		await browser.url("/e2e/alice.html");
+		await browser.waitUntil(async () => {
+			const title = await browser.getTitle();
+			return title === "Alice";
+		});
+		await browser.newWindow("/e2e/bob.html");
+		await browser.waitUntil(async () => {
+			const title = await browser.getTitle();
+			return title === "Bob";
+		});
+	}
+}
+
+export default new SerializationPage();

+ 23 - 0
e2e/datachannel/serializationTest.ts

@@ -0,0 +1,23 @@
+import P from "./serialization.page.js";
+import { browser, expect } from "@wdio/globals";
+
+export const serializationTest =
+	(testFile: string, serialization: string) => async () => {
+		await P.open(testFile, serialization);
+		await P.waitForMessage("Your Peer ID: ");
+		const bobId = (await P.messages.getText()).split("Your Peer ID: ")[1];
+		await browser.switchWindow("Alice");
+		await P.waitForMessage("Your Peer ID: ");
+		await P.receiverId.setValue(bobId);
+		await P.connectBtn.click();
+		await P.waitForMessage("Connected!");
+		await P.sendBtn.click();
+		await P.waitForMessage("Sent!");
+		await browser.switchWindow("Bob");
+		await P.waitForMessage("Closed!");
+		await P.checkBtn.click();
+		await P.waitForMessage("Checked!");
+
+		await expect(await P.errorMessage.getText()).toBe("");
+		await expect(await P.result.getText()).toBe("Success!");
+	};

+ 71 - 0
e2e/datachannel/serialization_binary.spec.ts

@@ -0,0 +1,71 @@
+import P from "./serialization.page.js";
+import { serializationTest } from "./serializationTest.js";
+describe("DataChannel:Binary", () => {
+	beforeAll(
+		async () => {
+			await P.init();
+		},
+		jasmine.DEFAULT_TIMEOUT_INTERVAL,
+		2,
+	);
+	it(
+		"should transfer numbers",
+		serializationTest("./numbers", "binary"),
+		jasmine.DEFAULT_TIMEOUT_INTERVAL,
+		2,
+	);
+	it(
+		"should transfer strings",
+		serializationTest("./strings", "binary"),
+		jasmine.DEFAULT_TIMEOUT_INTERVAL,
+		2,
+	);
+	it(
+		"should transfer long string",
+		serializationTest("./long_string", "binary"),
+		jasmine.DEFAULT_TIMEOUT_INTERVAL,
+		2,
+	);
+	it(
+		"should transfer objects",
+		serializationTest("./objects", "binary"),
+		jasmine.DEFAULT_TIMEOUT_INTERVAL,
+		2,
+	);
+	it(
+		"should transfer arrays",
+		serializationTest("./arrays", "binary"),
+		jasmine.DEFAULT_TIMEOUT_INTERVAL,
+		2,
+	);
+	it(
+		"should transfer Dates as strings",
+		serializationTest("./dates_as_string", "binary"),
+		jasmine.DEFAULT_TIMEOUT_INTERVAL,
+		2,
+	);
+	it(
+		"should transfer ArrayBuffers",
+		serializationTest("./arraybuffers", "binary"),
+		jasmine.DEFAULT_TIMEOUT_INTERVAL,
+		2,
+	);
+	it(
+		"should transfer TypedArrayView as ArrayBuffer",
+		serializationTest("./TypedArrayView_as_ArrayBuffer", "binary"),
+		jasmine.DEFAULT_TIMEOUT_INTERVAL,
+		2,
+	);
+	it(
+		"should transfer Uint8Arrays as ArrayBuffer",
+		serializationTest("./Uint8Array_as_ArrayBuffer", "binary"),
+		jasmine.DEFAULT_TIMEOUT_INTERVAL,
+		2,
+	);
+	it(
+		"should transfer Int32Arrays as ArrayBuffer",
+		serializationTest("./Int32Array_as_ArrayBuffer", "binary"),
+		jasmine.DEFAULT_TIMEOUT_INTERVAL,
+		2,
+	);
+});

+ 35 - 0
e2e/datachannel/serialization_cbor.spec.ts

@@ -0,0 +1,35 @@
+import P from "./serialization.page.js";
+import { serializationTest } from "./serializationTest.js";
+import { browser } from "@wdio/globals";
+
+describe("DataChannel:CBOR", function () {
+	beforeAll(async function () {
+		await P.init();
+	});
+	beforeEach(async function () {
+		if (
+			// @ts-ignore
+			browser.capabilities.browserName === "firefox" &&
+			// @ts-ignore
+			parseInt(browser.capabilities.browserVersion) < 102
+		) {
+			pending("Firefox 102+ required for Streams");
+		}
+	});
+	it("should transfer numbers", serializationTest("./numbers", "Cbor"));
+	it("should transfer strings", serializationTest("./strings", "Cbor"));
+	it("should transfer long string", serializationTest("./long_string", "Cbor"));
+	it("should transfer objects", serializationTest("./objects", "Cbor"));
+	it("should transfer arrays", serializationTest("./arrays", "Cbor"));
+	it("should transfer dates", serializationTest("./dates", "Cbor"));
+	it(
+		"should transfer ArrayBuffers as Uint8Arrays",
+		serializationTest("./arraybuffers_as_uint8array", "Cbor"),
+	);
+	it(
+		"should transfer TypedArrayView",
+		serializationTest("./typed_array_view", "Cbor"),
+	);
+	it("should transfer Uint8Arrays", serializationTest("./Uint8Array", "Cbor"));
+	it("should transfer Int32Arrays", serializationTest("./Int32Array", "Cbor"));
+});

+ 53 - 0
e2e/datachannel/serialization_json.spec.ts

@@ -0,0 +1,53 @@
+import P from "./serialization.page.js";
+import { serializationTest } from "./serializationTest.js";
+
+describe("DataChannel:JSON", () => {
+	beforeAll(
+		async () => {
+			await P.init();
+		},
+		jasmine.DEFAULT_TIMEOUT_INTERVAL,
+		2,
+	);
+	it(
+		"should transfer numbers",
+		serializationTest("./numbers", "json"),
+		jasmine.DEFAULT_TIMEOUT_INTERVAL,
+		2,
+	);
+	it(
+		"should transfer strings",
+		serializationTest("./strings", "json"),
+		jasmine.DEFAULT_TIMEOUT_INTERVAL,
+		2,
+	);
+	// it("should transfer long string", serializationTest("./long_string", "json"));
+	it(
+		"should transfer objects",
+		serializationTest("./objects", "json"),
+		jasmine.DEFAULT_TIMEOUT_INTERVAL,
+		2,
+	);
+	it(
+		"should transfer arrays (no chunks)",
+		serializationTest("./arrays_unchunked", "json"),
+		jasmine.DEFAULT_TIMEOUT_INTERVAL,
+		2,
+	);
+	it(
+		"should transfer Dates as strings",
+		serializationTest("./dates_as_json_string", "json"),
+		jasmine.DEFAULT_TIMEOUT_INTERVAL,
+		2,
+	);
+	// it("should transfer ArrayBuffers", serializationTest("./arraybuffers", "json"));
+	// it("should transfer TypedArrayView", serializationTest("./typed_array_view", "json"));
+	// it(
+	// 	"should transfer Uint8Arrays",
+	// 	serializationTest("./Uint8Array", "json"),
+	// );
+	// it(
+	// 	"should transfer Int32Arrays",
+	// 	serializationTest("./Int32Array", "json"),
+	// );
+});

+ 44 - 0
e2e/datachannel/serialization_msgpack.spec.ts

@@ -0,0 +1,44 @@
+import P from "./serialization.page.js";
+import { serializationTest } from "./serializationTest.js";
+import { browser } from "@wdio/globals";
+
+describe("DataChannel:MsgPack", function () {
+	beforeAll(async function () {
+		await P.init();
+	});
+	beforeEach(async function () {
+		if (
+			// @ts-ignore
+			browser.capabilities.browserName === "firefox" &&
+			// @ts-ignore
+			parseInt(browser.capabilities.browserVersion) < 102
+		) {
+			pending("Firefox 102+ required for Streams");
+		}
+	});
+	it("should transfer numbers", serializationTest("./numbers", "MsgPack"));
+	it("should transfer strings", serializationTest("./strings", "MsgPack"));
+	it(
+		"should transfer long string",
+		serializationTest("./long_string", "MsgPack"),
+	);
+	it("should transfer objects", serializationTest("./objects", "MsgPack"));
+	it("should transfer arrays", serializationTest("./arrays", "MsgPack"));
+	it(
+		"should transfer Dates as strings",
+		serializationTest("./dates", "MsgPack"),
+	);
+	// it("should transfer ArrayBuffers", serializationTest("./arraybuffers", "MsgPack"));
+	it(
+		"should transfer TypedArrayView",
+		serializationTest("./typed_array_view", "MsgPack"),
+	);
+	it(
+		"should transfer Uint8Arrays",
+		serializationTest("./Uint8Array", "MsgPack"),
+	);
+	it(
+		"should transfer Int32Arrays as Uint8Arrays",
+		serializationTest("./Int32Array_as_Uint8Array", "MsgPack"),
+	);
+});

+ 15 - 0
e2e/datachannel/strings.js

@@ -0,0 +1,15 @@
+import { strings } from "../data.js";
+import { expect } from "https://esm.sh/v126/chai@4.3.7/X-dHMvZXhwZWN0/es2021/chai.bundle.mjs";
+
+/** @param {unknown[]} received */
+export const check = (received) => {
+	expect(received).to.deep.equal(strings);
+};
+/**
+ * @param {import("../peerjs").DataConnection} dataConnection
+ */
+export const send = (dataConnection) => {
+	for (const string of strings) {
+		dataConnection.send(string);
+	}
+};

+ 17 - 0
e2e/datachannel/typed_array_view.js

@@ -0,0 +1,17 @@
+import { typed_array_view } from "../data.js";
+import { expect } from "https://esm.sh/v126/chai@4.3.7/X-dHMvZXhwZWN0/es2021/chai.bundle.mjs";
+
+/** @param {unknown[]} received */
+export const check = (received) => {
+	const received_typed_array_view = received[0];
+	expect(received_typed_array_view).to.deep.equal(typed_array_view);
+	for (const [i, v] of received_typed_array_view.entries()) {
+		expect(v).to.deep.equal(typed_array_view[i]);
+	}
+};
+/**
+ * @param {import("../peerjs").DataConnection} dataConnection
+ */
+export const send = (dataConnection) => {
+	dataConnection.send(typed_array_view);
+};

+ 38 - 0
e2e/mediachannel/close.html

@@ -0,0 +1,38 @@
+<!doctype html>
+<html lang="en">
+	<head>
+		<meta charset="UTF-8" />
+		<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+		<title></title>
+		<link rel="stylesheet" href="../style.css" />
+	</head>
+	<body>
+		<h1>MediaChannel</h1>
+		<canvas id="sender-stream" width="200" height="100"></canvas>
+		<video id="receiver-stream" autoplay></video>
+		<script>
+			const canvas = document.getElementById("sender-stream");
+			const ctx = canvas.getContext("2d");
+
+			// Set the canvas background color to white
+			ctx.fillStyle = "white";
+			ctx.fillRect(0, 0, canvas.width, canvas.height);
+
+			// Draw the text "Alice" in black
+			ctx.font = "30px sans-serif";
+			ctx.fillStyle = "black";
+			ctx.fillText(window.location.hash.substring(1), 50, 50);
+		</script>
+		<div id="inputs">
+			<input type="text" id="receiver-id" placeholder="Receiver ID" />
+			<button id="call-btn" disabled>Call</button>
+			<button id="close-btn">Hang up</button>
+		</div>
+		<div id="messages"></div>
+		<div id="result"></div>
+		<div id="error-message"></div>
+		<video></video>
+		<script src="/dist/peerjs.js"></script>
+		<script src="close.js" type="application/javascript"></script>
+	</body>
+</html>

+ 70 - 0
e2e/mediachannel/close.js

@@ -0,0 +1,70 @@
+/**
+ * @type {typeof import("../..").Peer}
+ */
+const Peer = window.peerjs.Peer;
+
+document.getElementsByTagName("title")[0].innerText =
+	window.location.hash.substring(1);
+
+const callBtn = document.getElementById("call-btn");
+console.log(callBtn);
+const receiverIdInput = document.getElementById("receiver-id");
+const closeBtn = document.getElementById("close-btn");
+const messages = document.getElementById("messages");
+const errorMessage = document.getElementById("error-message");
+
+const stream = window["sender-stream"].captureStream(30);
+const peer = new Peer({ debug: 3 });
+/**
+ * @type {import("peerjs").MediaConnection}
+ */
+let mediaConnection;
+peer
+	.once("open", (id) => {
+		messages.textContent = `Your Peer ID: ${id}`;
+	})
+	.once("error", (error) => {
+		errorMessage.textContent = JSON.stringify(error);
+	})
+	.once("call", (call) => {
+		mediaConnection = call;
+		mediaConnection
+			.once("stream", function (stream) {
+				const video = document.getElementById("receiver-stream");
+				video.srcObject = stream;
+				video.play();
+			})
+			.once("close", () => {
+				messages.textContent = "Closed!";
+			})
+			.once("willCloseOnRemote", () => {
+				messages.textContent = "Connected!";
+			});
+		call.answer(stream);
+	});
+
+callBtn.addEventListener("click", async () => {
+	console.log("calling");
+
+	/** @type {string} */
+	const receiverId = receiverIdInput.value;
+	if (receiverId) {
+		mediaConnection = peer.call(receiverId, stream);
+		mediaConnection
+			.once("stream", (stream) => {
+				const video = document.getElementById("receiver-stream");
+				video.srcObject = stream;
+				video.play();
+				messages.innerText = "Connected!";
+			})
+			.once("close", () => {
+				messages.textContent = "Closed!";
+			});
+	}
+});
+
+closeBtn.addEventListener("click", () => {
+	mediaConnection.close();
+});
+
+callBtn.disabled = false;

+ 61 - 0
e2e/mediachannel/close.page.ts

@@ -0,0 +1,61 @@
+import { browser, $ } from "@wdio/globals";
+class SerializationPage {
+	get receiverId() {
+		return $("input[id='receiver-id']");
+	}
+	get callBtn() {
+		return $("button[id='call-btn']");
+	}
+
+	get messages() {
+		return $("div[id='messages']");
+	}
+
+	get closeBtn() {
+		return $("button[id='close-btn']");
+	}
+
+	get errorMessage() {
+		return $("div[id='error-message']");
+	}
+
+	get result() {
+		return $("div[id='result']");
+	}
+
+	waitForMessage(right: string) {
+		return browser.waitUntil(
+			async () => {
+				const messages = await this.messages.getText();
+				return messages.startsWith(right);
+			},
+			{ timeoutMsg: `Expected message to start with ${right}`, timeout: 10000 },
+		);
+	}
+
+	async open() {
+		await browser.switchWindow("Alice");
+		await browser.url(`/e2e/mediachannel/close.html#Alice`);
+		await this.callBtn.waitForEnabled();
+
+		await browser.switchWindow("Bob");
+		await browser.url(`/e2e/mediachannel/close.html#Bob`);
+		await this.callBtn.waitForEnabled();
+	}
+	async init() {
+		await browser.url("/e2e/alice.html");
+		await browser.waitUntil(async () => {
+			const title = await browser.getTitle();
+			return title === "Alice";
+		});
+		await browser.pause(1000);
+		await browser.newWindow("/e2e/bob.html");
+		await browser.waitUntil(async () => {
+			const title = await browser.getTitle();
+			return title === "Bob";
+		});
+		await browser.pause(1000);
+	}
+}
+
+export default new SerializationPage();

+ 33 - 0
e2e/mediachannel/close.spec.ts

@@ -0,0 +1,33 @@
+import P from "./close.page.js";
+import { browser } from "@wdio/globals";
+
+describe("MediaStream", () => {
+	beforeAll(
+		async () => {
+			await P.init();
+		},
+		jasmine.DEFAULT_TIMEOUT_INTERVAL,
+		1,
+	);
+	it(
+		"should close the remote stream",
+		async () => {
+			await P.open();
+			await P.waitForMessage("Your Peer ID: ");
+			const bobId = (await P.messages.getText()).split("Your Peer ID: ")[1];
+			await browser.switchWindow("Alice");
+			await P.waitForMessage("Your Peer ID: ");
+			await P.receiverId.setValue(bobId);
+			await P.callBtn.click();
+			await P.waitForMessage("Connected!");
+			await browser.switchWindow("Bob");
+			await P.waitForMessage("Connected!");
+			await P.closeBtn.click();
+			await P.waitForMessage("Closed!");
+			await browser.switchWindow("Alice");
+			await P.waitForMessage("Closed!");
+		},
+		jasmine.DEFAULT_TIMEOUT_INTERVAL,
+		2,
+	);
+});

+ 3 - 0
e2e/package.json

@@ -0,0 +1,3 @@
+{
+	"type": "module"
+}

+ 34 - 0
e2e/peer/disconnected.html

@@ -0,0 +1,34 @@
+<!doctype html>
+<html lang="en">
+	<head>
+		<meta charset="UTF-8" />
+		<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+		<title></title>
+		<link rel="stylesheet" href="../style.css" />
+	</head>
+	<body>
+		<h1>ID-TAKEN</h1>
+		<div id="messages"></div>
+		<div id="error-message"></div>
+		<script src="/dist/peerjs.js"></script>
+		<script type="application/javascript">
+			/**
+			 * @type {typeof import("../..").Peer}
+			 */
+			const Peer = window.peerjs.Peer;
+
+			const messages = document.getElementById("messages");
+
+			const peer = new Peer();
+			peer
+				.once(
+					"error",
+					(error) => void (messages.textContent = JSON.stringify(error)),
+				)
+				.once("open", (id) => {
+					peer.disconnect();
+					peer.connect("otherid");
+				});
+		</script>
+	</body>
+</html>

+ 56 - 0
e2e/peer/id-taken.html

@@ -0,0 +1,56 @@
+<!doctype html>
+<html lang="en">
+	<head>
+		<meta charset="UTF-8" />
+		<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+		<title></title>
+		<link rel="stylesheet" href="../style.css" />
+	</head>
+	<body>
+		<h1>ID-TAKEN</h1>
+		<div id="messages"></div>
+		<div id="error-message"></div>
+		<script src="/dist/peerjs.js"></script>
+		<script type="application/javascript">
+			/**
+			 * @type {typeof import("../..").Peer}
+			 */
+			const Peer = window.peerjs.Peer;
+
+			const messages = document.getElementById("messages");
+			const errorMessage = document.getElementById("error-message");
+
+			// Peer A should be created without an error
+			const peerA = new Peer()
+				.once(
+					"error",
+					(err) => (errorMessage.textContent += JSON.stringify(err)),
+				)
+				.once("open", (id) => {
+					// Create 10 new `Peer`s that will try to steel A's id
+					let peers_try_to_take = Array.from(
+						{ length: 10 },
+						(_, i) =>
+							new Promise((resolve, reject) =>
+								new Peer(id)
+									.once("open", () =>
+										reject(`Peer ${i} failed! Connection got established.`),
+									)
+									.once("error", (error) => {
+										if (error.type === "unavailable-id") {
+											resolve(`ID already taken. (${i})`);
+										} else {
+											reject(error);
+										}
+									}),
+							),
+					);
+					Promise.all(peers_try_to_take)
+						.then(() => (messages.textContent = "No ID takeover"))
+						.catch(
+							(error) => (errorMessage.textContent += JSON.stringify(error)),
+						);
+				});
+		</script>
+	</body>
+</html>

+ 26 - 0
e2e/peer/peer.page.ts

@@ -0,0 +1,26 @@
+import { browser, $ } from "@wdio/globals";
+class PeerPage {
+	get messages() {
+		return $("div[id='messages']");
+	}
+
+	get errorMessage() {
+		return $("div[id='error-message']");
+	}
+
+	waitForMessage(right: string) {
+		return browser.waitUntil(
+			async () => {
+				const messages = await this.messages.getText();
+				return messages.startsWith(right);
+			},
+			{ timeoutMsg: `Expected message to start with ${right}`, timeout: 10000 },
+		);
+	}
+
+	async open(test: string) {
+		await browser.url(`/e2e/peer/${test}.html`);
+	}
+}
+
+export default new PeerPage();

+ 20 - 0
e2e/peer/peer.spec.ts

@@ -0,0 +1,20 @@
+import P from "./peer.page.js";
+import { browser, expect } from "@wdio/globals";
+
+describe("Peer", () => {
+	it("should emit an error, when the ID is already taken", async () => {
+		await P.open("id-taken");
+		await P.waitForMessage("No ID takeover");
+		expect(await P.errorMessage.getText()).toBe("");
+	});
+	it("should emit an error, when the server is unavailable", async () => {
+		await P.open("server-unavailable");
+		await P.waitForMessage('{"type":"server-error"}');
+		expect(await P.errorMessage.getText()).toBe("");
+	});
+	it("should emit an error, when it got disconnected from the server", async () => {
+		await P.open("disconnected");
+		await P.waitForMessage('{"type":"disconnected"}');
+		expect(await P.errorMessage.getText()).toBe("");
+	});
+});

+ 31 - 0
e2e/peer/server-unavailable.html

@@ -0,0 +1,31 @@
+<!doctype html>
+<html lang="en">
+	<head>
+		<meta charset="UTF-8" />
+		<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+		<title></title>
+		<link rel="stylesheet" href="../style.css" />
+	</head>
+	<body>
+		<h1>ID-TAKEN</h1>
+		<div id="messages"></div>
+		<div id="error-message"></div>
+		<script src="/dist/peerjs.js"></script>
+		<script type="application/javascript">
+			/**
+			 * @type {typeof import("../..").Peer}
+			 */
+			const Peer = window.peerjs.Peer;
+
+			const messages = document.getElementById("messages");
+			const errorMessage = document.getElementById("error-message");
+
+			new Peer({
+				host: "thisserverwillneverexist.example.com",
+			}).once(
+				"error",
+				(error) => void (messages.textContent = JSON.stringify(error)),
+			);
+		</script>
+	</body>
+</html>

+ 26 - 0
e2e/style.css

@@ -0,0 +1,26 @@
+body {
+	font-family: Arial, sans-serif;
+	max-width: 800px;
+	margin: 0 auto;
+	padding: 20px;
+}
+
+#inputs {
+	display: flex;
+	flex-wrap: wrap;
+	gap: 10px;
+	align-items: center;
+}
+
+button {
+	background-color: #007bff;
+	color: white;
+	border: none;
+	padding: 8px 16px;
+	border-radius: 4px;
+	cursor: pointer;
+}
+
+button:hover {
+	background-color: #0056b3;
+}

+ 70 - 0
e2e/tsconfig.json

@@ -0,0 +1,70 @@
+{
+	"compilerOptions": {
+		/* Visit https://aka.ms/tsconfig.json to read more about this file */
+
+		/* Basic Options */
+		// "incremental": true,                   /* Enable incremental compilation */
+		"target": "ES2022" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */,
+		"module": "ESNext" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
+		// "lib": [],                             /* Specify library files to be included in the compilation. */
+		// "allowJs": true,                       /* Allow javascript files to be compiled. */
+		// "checkJs": true,                       /* Report errors in .js files. */
+		// "jsx": "preserve",                     /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
+		// "declaration": true,                   /* Generates corresponding '.d.ts' file. */
+		// "declarationMap": true,                /* Generates a sourcemap for each corresponding '.d.ts' file. */
+		// "sourceMap": true,                     /* Generates corresponding '.map' file. */
+		// "outFile": "./",                       /* Concatenate and emit output to single file. */
+		// "outDir": "./",                        /* Redirect output structure to the directory. */
+		// "rootDir": "./",                       /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
+		// "composite": true,                     /* Enable project compilation */
+		// "tsBuildInfoFile": "./",               /* Specify file to store incremental compilation information */
+		// "removeComments": true,                /* Do not emit comments to output. */
+		// "noEmit": true,                        /* Do not emit outputs. */
+		// "importHelpers": true,                 /* Import emit helpers from 'tslib'. */
+		// "downlevelIteration": true,            /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
+		// "isolatedModules": true,               /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
+
+		/* Strict Type-Checking Options */
+		"strict": true /* Enable all strict type-checking options. */,
+		// "noImplicitAny": true,                 /* Raise error on expressions and declarations with an implied 'any' type. */
+		// "strictNullChecks": true,              /* Enable strict null checks. */
+		// "strictFunctionTypes": true,           /* Enable strict checking of function types. */
+		// "strictBindCallApply": true,           /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
+		// "strictPropertyInitialization": true,  /* Enable strict checking of property initialization in classes. */
+		// "noImplicitThis": true,                /* Raise error on 'this' expressions with an implied 'any' type. */
+		// "alwaysStrict": true,                  /* Parse in strict mode and emit "use strict" for each source file. */
+
+		/* Additional Checks */
+		// "noUnusedLocals": true,                /* Report errors on unused locals. */
+		// "noUnusedParameters": true,            /* Report errors on unused parameters. */
+		// "noImplicitReturns": true,             /* Report error when not all code paths in function return a value. */
+		// "noFallthroughCasesInSwitch": true,    /* Report errors for fallthrough cases in switch statement. */
+		// "noUncheckedIndexedAccess": true,      /* Include 'undefined' in index signature results */
+
+		/* Module Resolution Options */
+		"moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
+		// "baseUrl": "./",                       /* Base directory to resolve non-absolute module names. */
+		// "paths": {},                           /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
+		// "rootDirs": [],                        /* List of root folders whose combined content represents the structure of the project at runtime. */
+		// "typeRoots": [],                       /* List of folders to include type definitions from. */
+		"types": ["node", "@wdio/globals/types", "@wdio/jasmine-framework"],
+		"allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */,
+		// "esModuleInterop": true,               /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
+		// "preserveSymlinks": true,              /* Do not resolve the real path of symlinks. */
+		// "allowUmdGlobalAccess": true,          /* Allow accessing UMD globals from modules. */
+
+		/* Source Map Options */
+		// "sourceRoot": "",                      /* Specify the location where debugger should locate TypeScript files instead of source locations. */
+		// "mapRoot": "",                         /* Specify the location where debugger should locate map files instead of generated locations. */
+		// "inlineSourceMap": true,               /* Emit a single file with source maps instead of having a separate file. */
+		// "inlineSources": true,                 /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
+
+		/* Experimental Options */
+		// "experimentalDecorators": true,        /* Enables experimental support for ES7 decorators. */
+		// "emitDecoratorMetadata": true,         /* Enables experimental support for emitting type metadata for decorators. */
+
+		/* Advanced Options */
+		"skipLibCheck": true /* Skip type checking of declaration files. */,
+		"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
+	}
+}

+ 11 - 0
e2e/types.d.ts

@@ -0,0 +1,11 @@
+/* eslint no-unused-vars: 0 */
+
+declare module "https://esm.sh/v126/chai@4.3.7/X-dHMvZXhwZWN0/es2021/chai.bundle.mjs" {
+	export = chai;
+}
+
+interface Window {
+	"connect-btn": HTMLButtonElement;
+	send: (dataConnection: import("../../peerjs").DataConnection) => void;
+	check: (received: unknown[]) => void;
+}

+ 87 - 0
e2e/wdio.bstack.conf.ts

@@ -0,0 +1,87 @@
+import { config as sharedConfig } from "./wdio.shared.conf.js";
+
+export const config: WebdriverIO.Config = {
+	...sharedConfig,
+	...{
+		/**
+		 * Only allow one instance. We are limited to 5 parallel tests on BrowserStack.
+		 */
+		maxInstances: 1,
+		user: process.env.BROWSERSTACK_USERNAME,
+		key: process.env.BROWSERSTACK_ACCESS_KEY,
+		hostname: "hub.browserstack.com",
+		services: [
+			[
+				"browserstack",
+				{
+					browserstackLocal: true,
+				},
+			],
+		],
+		capabilities: [
+			{
+				browserName: "Edge",
+				"bstack:options": {
+					os: "Windows",
+					osVersion: "11",
+					browserVersion: "83",
+					localIdentifier: process.env.BROWSERSTACK_LOCAL_IDENTIFIER,
+				},
+			},
+			{
+				browserName: "Chrome",
+				"bstack:options": {
+					os: "Windows",
+					osVersion: "11",
+					browserVersion: "83",
+					localIdentifier: process.env.BROWSERSTACK_LOCAL_IDENTIFIER,
+				},
+			},
+			{
+				browserName: "Chrome",
+				"bstack:options": {
+					browserVersion: "latest",
+					os: "Windows",
+					osVersion: "11",
+					localIdentifier: process.env.BROWSERSTACK_LOCAL_IDENTIFIER,
+				},
+			},
+			{
+				browserName: "Firefox",
+				"bstack:options": {
+					os: "Windows",
+					osVersion: "7",
+					browserVersion: "80.0",
+					localIdentifier: process.env.BROWSERSTACK_LOCAL_IDENTIFIER,
+				},
+			},
+			{
+				browserName: "Firefox",
+				"bstack:options": {
+					browserVersion: "105",
+					os: "OS X",
+					osVersion: "Ventura",
+					localIdentifier: process.env.BROWSERSTACK_LOCAL_IDENTIFIER,
+				},
+			},
+			// {
+			//     browserName: "Safari",
+			//     "bstack:options": {
+			//         browserVersion: "latest",
+			//         os: "OS X",
+			//         osVersion: "Monterey",
+			//         localIdentifier: process.env.BROWSERSTACK_LOCAL_IDENTIFIER,
+			//     },
+			// },
+			// {
+			//     browserName: 'Safari',
+			//     'bstack:options': {
+			//         browserVersion: 'latest',
+			//         os: 'OS X',
+			//         osVersion: 'Ventura',
+			//         localIdentifier: process.env.BROWSERSTACK_LOCAL_IDENTIFIER,
+			//     }
+			// },
+		],
+	},
+};

+ 23 - 0
e2e/wdio.local.conf.ts

@@ -0,0 +1,23 @@
+import { config as sharedConfig } from "./wdio.shared.conf.js";
+
+export const config: WebdriverIO.Config = {
+	runner: "local",
+	...sharedConfig,
+	services: ["geckodriver"],
+	...{
+		capabilities: [
+			{
+				browserName: "chrome",
+				"goog:chromeOptions": {
+					args: ["headless", "disable-gpu"],
+				},
+			},
+			{
+				browserName: "firefox",
+				"moz:firefoxOptions": {
+					args: ["-headless"],
+				},
+			},
+		],
+	},
+};

+ 250 - 0
e2e/wdio.shared.conf.ts

@@ -0,0 +1,250 @@
+import url from "node:url";
+import path from "node:path";
+
+const __dirname = url.fileURLToPath(new URL(".", import.meta.url));
+
+export const config: Omit<WebdriverIO.Config, "capabilities"> = {
+	injectGlobals: false,
+	//
+	// ====================
+	// Runner Configuration
+	// ====================
+	//
+	// WebdriverIO allows it to run your tests in arbitrary locations (e.g. locally or
+	// on a remote machine).
+	// runner: 'local',
+	//
+	// ==================
+	// Specify Test Files
+	// ==================
+	// Define which test specs should run. The pattern is relative to the directory
+	// from which `wdio` was called. Notice that, if you are calling `wdio` from an
+	// NPM script (see https://docs.npmjs.com/cli/run-script) then the current working
+	// directory is where your package.json resides, so `wdio` will be called from there.
+	//
+	specs: ["./**/*.spec.ts"],
+	// Patterns to exclude.
+	exclude: [
+		// 'path/to/excluded/files'
+	],
+	//
+	// ============
+	// Capabilities
+	// ============
+	// Define your capabilities here. WebdriverIO can run multiple capabilities at the same
+	// time. Depending on the number of capabilities, WebdriverIO launches several test
+	// sessions. Within your capabilities you can overwrite the spec and exclude options in
+	// order to group specific specs to a specific capability.
+	//
+	// First, you can define how many instances should be started at the same time. Let's
+	// say you have 3 different capabilities (Chrome, Firefox, and Safari) and you have
+	// set maxInstances to 1; wdio will spawn 3 processes. Therefore, if you have 10 spec
+	// files and you set maxInstances to 10, all spec files will get tested at the same time
+	// and 30 processes will get spawned. The property handles how many capabilities
+	// from the same test should run tests.
+	//
+	maxInstances: 5,
+	//
+	// ===================
+	// Test Configurations
+	// ===================
+	// Define all options that are relevant for the WebdriverIO instance here
+	//
+	// Level of logging verbosity: trace | debug | info | warn | error | silent
+	logLevel: "trace",
+	outputDir: path.resolve(__dirname, "logs"),
+	//
+	// Set specific log levels per logger
+	// loggers:
+	// - webdriver, webdriverio
+	// - @wdio/applitools-service, @wdio/browserstack-service, @wdio/devtools-service, @wdio/sauce-service
+	// - @wdio/mocha-framework, @wdio/jasmine-framework
+	// - @wdio/local-runner, @wdio/lambda-runner
+	// - @wdio/sumologic-reporter
+	// - @wdio/cli, @wdio/config, @wdio/sync, @wdio/utils
+	// Level of logging verbosity: trace | debug | info | warn | error | silent
+	// logLevels: {
+	//     webdriver: 'info',
+	//     '@wdio/applitools-service': 'info'
+	// },
+	//
+	// If you only want to run your tests until a specific amount of tests have failed use
+	// bail (default is 0 - don't bail, run all tests).
+	bail: 0,
+	//
+	// Set a base URL in order to shorten url command calls. If your `url` parameter starts
+	// with `/`, the base url gets prepended, not including the path portion of your baseUrl.
+	// If your `url` parameter starts without a scheme or `/` (like `some/path`), the base url
+	// gets prepended directly.
+	baseUrl: "http://localhost:3000",
+	//
+	// Default timeout for all waitFor* commands.
+	waitforTimeout: 10000,
+	//
+	// Default timeout in milliseconds for request
+	// if browser driver or grid doesn't send response
+	connectionRetryTimeout: 90000,
+	//
+	// Default request retries count
+	connectionRetryCount: 3,
+	//
+	// Framework you want to run your specs with.
+	// The following are supported: Mocha, Jasmine, and Cucumber
+	// see also: https://webdriver.io/docs/frameworks.html
+	//
+	// Make sure you have the wdio adapter package for the specific framework installed
+	// before running any tests.
+	framework: "jasmine",
+	//
+	// The number of times to retry the entire specfile when it fails as a whole
+	specFileRetries: 1,
+	//
+	// Whether or not retried specfiles should be retried immediately or deferred to the end of the queue
+	specFileRetriesDeferred: true,
+	//
+	// Test reporter for stdout.
+	// The only one supported by default is 'dot'
+	// see also: https://webdriver.io/docs/dot-reporter.html
+	reporters: ["spec"],
+	//
+	// Options to be passed to Jasmine.
+	jasmineOpts: {
+		// Jasmine default timeout
+		defaultTimeoutInterval: 60000,
+		//
+		// The Jasmine framework allows interception of each assertion in order to log the state of the application
+		// or website depending on the result. For example, it is pretty handy to take a screenshot every time
+		// an assertion fails.
+		// expectationResultHandler: function(passed, assertion) {
+		// do something
+		// }
+	},
+	//
+	// =====
+	// Hooks
+	// =====
+	// WebdriverIO provides several hooks you can use to interfere with the test process in order to enhance
+	// it and to build services around it. You can either apply a single function or an array of
+	// methods to it. If one of them returns with a promise, WebdriverIO will wait until that promise got
+	// resolved to continue.
+	/**
+	 * Gets executed once before all workers get launched.
+	 * @param {Object} config wdio configuration object
+	 * @param {Array.<Object>} capabilities list of capabilities details
+	 */
+	// onPrepare: function (config, capabilities) {
+	// },
+	/**
+	 * Gets executed before a worker process is spawned and can be used to initialise specific service
+	 * for that worker as well as modify runtime environments in an async fashion.
+	 * @param  {String} cid      capability id (e.g 0-0)
+	 * @param  {[type]} caps     object containing capabilities for session that will be spawn in the worker
+	 * @param  {[type]} specs    specs to be run in the worker process
+	 * @param  {[type]} args     object that will be merged with the main configuration once worker is initialised
+	 * @param  {[type]} execArgv list of string arguments passed to the worker process
+	 */
+	// onWorkerStart: function (cid, caps, specs, args, execArgv) {
+	// },
+	/**
+	 * Gets executed just before initialising the webdriver session and test framework. It allows you
+	 * to manipulate configurations depending on the capability or spec.
+	 * @param {Object} config wdio configuration object
+	 * @param {Array.<Object>} capabilities list of capabilities details
+	 * @param {Array.<String>} specs List of spec file paths that are to be run
+	 */
+	// beforeSession: function (config, capabilities, specs) {
+	// },
+	/**
+	 * Gets executed before test execution begins. At this point you can access to all global
+	 * variables like `browser`. It is the perfect place to define custom commands.
+	 * @param {Array.<Object>} capabilities list of capabilities details
+	 * @param {Array.<String>} specs List of spec file paths that are to be run
+	 */
+	// before: function (capabilities, specs) {
+	// },
+	/**
+	 * Runs before a WebdriverIO command gets executed.
+	 * @param {String} commandName hook command name
+	 * @param {Array} args arguments that command would receive
+	 */
+	// beforeCommand: function (commandName, args) {
+	// },
+	/**
+	 * Hook that gets executed before the suite starts
+	 * @param {Object} suite suite details
+	 */
+	// beforeSuite: function (suite) {
+	// },
+	/**
+	 * Function to be executed before a test (in Mocha/Jasmine) starts.
+	 */
+	// beforeTest: function (test, context) {
+	// },
+	/**
+	 * Hook that gets executed _before_ a hook within the suite starts (e.g. runs before calling
+	 * beforeEach in Mocha)
+	 */
+	// beforeHook: function (test, context) {
+	// },
+	/**
+	 * Hook that gets executed _after_ a hook within the suite starts (e.g. runs after calling
+	 * afterEach in Mocha)
+	 */
+	// afterHook: function (test, context, { error, result, duration, passed, retries }) {
+	// },
+	/**
+	 * Function to be executed after a test (in Mocha/Jasmine).
+	 */
+	// afterTest: function(test, context, { error, result, duration, passed, retries }) {
+	// },
+
+	/**
+	 * Hook that gets executed after the suite has ended
+	 * @param {Object} suite suite details
+	 */
+	// afterSuite: function (suite) {
+	// },
+	/**
+	 * Runs after a WebdriverIO command gets executed
+	 * @param {String} commandName hook command name
+	 * @param {Array} args arguments that command would receive
+	 * @param {Number} result 0 - command success, 1 - command error
+	 * @param {Object} error error object if any
+	 */
+	// afterCommand: function (commandName, args, result, error) {
+	// },
+	/**
+	 * Gets executed after all tests are done. You still have access to all global variables from
+	 * the test.
+	 * @param {Number} result 0 - test pass, 1 - test fail
+	 * @param {Array.<Object>} capabilities list of capabilities details
+	 * @param {Array.<String>} specs List of spec file paths that ran
+	 */
+	// after: function (result, capabilities, specs) {
+	// },
+	/**
+	 * Gets executed right after terminating the webdriver session.
+	 * @param {Object} config wdio configuration object
+	 * @param {Array.<Object>} capabilities list of capabilities details
+	 * @param {Array.<String>} specs List of spec file paths that ran
+	 */
+	// afterSession: function (config, capabilities, specs) {
+	// },
+	/**
+	 * Gets executed after all workers got shut down and the process is about to exit. An error
+	 * thrown in the onComplete hook will result in the test run failing.
+	 * @param {Object} exitCode 0 - success, 1 - fail
+	 * @param {Object} config wdio configuration object
+	 * @param {Array.<Object>} capabilities list of capabilities details
+	 * @param {<Object>} results object containing test results
+	 */
+	// onComplete: function(exitCode, config, capabilities, results) {
+	// },
+	/**
+	 * Gets executed when a refresh happens.
+	 * @param {String} oldSessionId session ID of the old session
+	 * @param {String} newSessionId session ID of the new session
+	 */
+	//onReload: function(oldSessionId, newSessionId) {
+	//}
+};

+ 8 - 0
jest.config.cjs

@@ -0,0 +1,8 @@
+/** @type {import('ts-jest').JestConfigWithTsJest} */
+module.exports = {
+	testEnvironment: "jsdom",
+	transform: {
+		"^.+\\.(t|j)sx?$": ["@swc/jest"],
+	},
+	modulePathIgnorePatterns: ["e2e"],
+};

+ 1 - 1
lib/api.ts

@@ -1,6 +1,6 @@
 import { util } from "./util";
 import { util } from "./util";
 import logger from "./logger";
 import logger from "./logger";
-import { PeerJSOption } from "./optionInterfaces";
+import type { PeerJSOption } from "./optionInterfaces";
 import { version } from "../package.json";
 import { version } from "../package.json";
 
 
 export class API {
 export class API {

+ 57 - 10
lib/baseconnection.ts

@@ -1,34 +1,72 @@
-import { EventEmitter, ValidEventTypes } from "eventemitter3";
-import { Peer } from "./peer";
-import { ServerMessage } from "./servermessage";
-import { ConnectionType } from "./enums";
+import type { Peer } from "./peer";
+import type { ServerMessage } from "./servermessage";
+import type { ConnectionType } from "./enums";
+import { BaseConnectionErrorType } from "./enums";
+import {
+	EventEmitterWithError,
+	type EventsWithError,
+	PeerError,
+} from "./peerError";
+import type { ValidEventTypes } from "eventemitter3";
 
 
-export type BaseConnectionEvents = {
+export interface BaseConnectionEvents<
+	ErrorType extends string = BaseConnectionErrorType,
+> extends EventsWithError<ErrorType> {
 	/**
 	/**
 	 * Emitted when either you or the remote peer closes the connection.
 	 * Emitted when either you or the remote peer closes the connection.
+	 *
+	 * ```ts
+	 * connection.on('close', () => { ... });
+	 * ```
 	 */
 	 */
 	close: () => void;
 	close: () => void;
-	error: (error: Error) => void;
+	/**
+	 * ```ts
+	 * connection.on('error', (error) => { ... });
+	 * ```
+	 */
+	error: (error: PeerError<`${ErrorType}`>) => void;
 	iceStateChanged: (state: RTCIceConnectionState) => void;
 	iceStateChanged: (state: RTCIceConnectionState) => void;
-};
+}
 
 
 export abstract class BaseConnection<
 export abstract class BaseConnection<
-	T extends ValidEventTypes,
-> extends EventEmitter<T & BaseConnectionEvents> {
+	SubClassEvents extends ValidEventTypes,
+	ErrorType extends string = never,
+> extends EventEmitterWithError<
+	ErrorType | BaseConnectionErrorType,
+	SubClassEvents & BaseConnectionEvents<BaseConnectionErrorType | ErrorType>
+> {
 	protected _open = false;
 	protected _open = false;
 
 
+	/**
+	 * Any type of metadata associated with the connection,
+	 * passed in by whoever initiated the connection.
+	 */
 	readonly metadata: any;
 	readonly metadata: any;
 	connectionId: string;
 	connectionId: string;
 
 
 	peerConnection: RTCPeerConnection;
 	peerConnection: RTCPeerConnection;
+	dataChannel: RTCDataChannel;
 
 
 	abstract get type(): ConnectionType;
 	abstract get type(): ConnectionType;
 
 
+	/**
+	 * The optional label passed in or assigned by PeerJS when the connection was initiated.
+	 */
+	label: string;
+
+	/**
+	 * Whether the media connection is active (e.g. your call has been answered).
+	 * You can check this if you want to set a maximum wait time for a one-sided call.
+	 */
 	get open() {
 	get open() {
 		return this._open;
 		return this._open;
 	}
 	}
 
 
-	constructor(
+	protected constructor(
+		/**
+		 * The ID of the peer on the other end of this connection.
+		 */
 		readonly peer: string,
 		readonly peer: string,
 		public provider: Peer,
 		public provider: Peer,
 		readonly options: any,
 		readonly options: any,
@@ -40,5 +78,14 @@ export abstract class BaseConnection<
 
 
 	abstract close(): void;
 	abstract close(): void;
 
 
+	/**
+	 * @internal
+	 */
 	abstract handleMessage(message: ServerMessage): void;
 	abstract handleMessage(message: ServerMessage): void;
+
+	/**
+	 * Called by the Negotiator when the DataChannel is ready.
+	 * @internal
+	 * */
+	abstract _initializeDataChannel(dc: RTCDataChannel): void;
 }
 }

+ 9 - 0
lib/cborPeer.ts

@@ -0,0 +1,9 @@
+import { Peer, type SerializerMapping } from "./peer";
+import { Cbor } from "./dataconnection/StreamConnection/Cbor";
+
+export class CborPeer extends Peer {
+	override _serializers: SerializerMapping = {
+		Cbor,
+		default: Cbor,
+	};
+}

+ 0 - 359
lib/dataconnection.ts

@@ -1,359 +0,0 @@
-import { util } from "./util";
-import logger from "./logger";
-import { Negotiator } from "./negotiator";
-import { ConnectionType, SerializationType, ServerMessageType } from "./enums";
-import { Peer } from "./peer";
-import { BaseConnection } from "./baseconnection";
-import { ServerMessage } from "./servermessage";
-import { EncodingQueue } from "./encodingQueue";
-import type { DataConnection as IDataConnection } from "./dataconnection";
-
-type DataConnectionEvents = {
-	/**
-	 * Emitted when data is received from the remote peer.
-	 */
-	data: (data: unknown) => void;
-	/**
-	 * Emitted when the connection is established and ready-to-use.
-	 */
-	open: () => void;
-};
-
-/**
- * Wraps a DataChannel between two Peers.
- */
-export class DataConnection
-	extends BaseConnection<DataConnectionEvents>
-	implements IDataConnection
-{
-	private static readonly ID_PREFIX = "dc_";
-	private static readonly MAX_BUFFERED_AMOUNT = 8 * 1024 * 1024;
-
-	private _negotiator: Negotiator<DataConnectionEvents, DataConnection>;
-	readonly label: string;
-	readonly serialization: SerializationType;
-	readonly reliable: boolean;
-	stringify: (data: any) => string = JSON.stringify;
-	parse: (data: string) => any = JSON.parse;
-
-	get type() {
-		return ConnectionType.Data;
-	}
-
-	private _buffer: any[] = [];
-	private _bufferSize = 0;
-	private _buffering = false;
-	private _chunkedData: {
-		[id: number]: {
-			data: Blob[];
-			count: number;
-			total: number;
-		};
-	} = {};
-
-	private _dc: RTCDataChannel;
-	private _encodingQueue = new EncodingQueue();
-
-	get dataChannel(): RTCDataChannel {
-		return this._dc;
-	}
-
-	get bufferSize(): number {
-		return this._bufferSize;
-	}
-
-	constructor(peerId: string, provider: Peer, options: any) {
-		super(peerId, provider, options);
-
-		this.connectionId =
-			this.options.connectionId ||
-			DataConnection.ID_PREFIX + util.randomToken();
-
-		this.label = this.options.label || this.connectionId;
-		this.serialization = this.options.serialization || SerializationType.Binary;
-		this.reliable = !!this.options.reliable;
-
-		this._encodingQueue.on("done", (ab: ArrayBuffer) => {
-			this._bufferedSend(ab);
-		});
-
-		this._encodingQueue.on("error", () => {
-			logger.error(
-				`DC#${this.connectionId}: Error occured in encoding from blob to arraybuffer, close DC`,
-			);
-			this.close();
-		});
-
-		this._negotiator = new Negotiator(this);
-
-		this._negotiator.startConnection(
-			this.options._payload || {
-				originator: true,
-			},
-		);
-	}
-
-	/** Called by the Negotiator when the DataChannel is ready. */
-	initialize(dc: RTCDataChannel): void {
-		this._dc = dc;
-		this._configureDataChannel();
-	}
-
-	private _configureDataChannel(): void {
-		if (!util.supports.binaryBlob || util.supports.reliable) {
-			this.dataChannel.binaryType = "arraybuffer";
-		}
-
-		this.dataChannel.onopen = () => {
-			logger.log(`DC#${this.connectionId} dc connection success`);
-			this._open = true;
-			this.emit("open");
-		};
-
-		this.dataChannel.onmessage = (e) => {
-			logger.log(`DC#${this.connectionId} dc onmessage:`, e.data);
-			this._handleDataMessage(e);
-		};
-
-		this.dataChannel.onclose = () => {
-			logger.log(`DC#${this.connectionId} dc closed for:`, this.peer);
-			this.close();
-		};
-	}
-
-	// Handles a DataChannel message.
-	private _handleDataMessage({
-		data,
-	}: {
-		data: Blob | ArrayBuffer | string;
-	}): void {
-		const datatype = data.constructor;
-
-		const isBinarySerialization =
-			this.serialization === SerializationType.Binary ||
-			this.serialization === SerializationType.BinaryUTF8;
-
-		let deserializedData: any = data;
-
-		if (isBinarySerialization) {
-			if (datatype === Blob) {
-				// Datatype should never be blob
-				util.blobToArrayBuffer(data as Blob, (ab) => {
-					const unpackedData = util.unpack(ab);
-					this.emit("data", unpackedData);
-				});
-				return;
-			} else if (datatype === ArrayBuffer) {
-				deserializedData = util.unpack(data as ArrayBuffer);
-			} else if (datatype === String) {
-				// String fallback for binary data for browsers that don't support binary yet
-				const ab = util.binaryStringToArrayBuffer(data as string);
-				deserializedData = util.unpack(ab);
-			}
-		} else if (this.serialization === SerializationType.JSON) {
-			deserializedData = this.parse(data as string);
-		}
-
-		// Check if we've chunked--if so, piece things back together.
-		// We're guaranteed that this isn't 0.
-		if (deserializedData.__peerData) {
-			this._handleChunk(deserializedData);
-			return;
-		}
-
-		super.emit("data", deserializedData);
-	}
-
-	private _handleChunk(data: {
-		__peerData: number;
-		n: number;
-		total: number;
-		data: Blob;
-	}): void {
-		const id = data.__peerData;
-		const chunkInfo = this._chunkedData[id] || {
-			data: [],
-			count: 0,
-			total: data.total,
-		};
-
-		chunkInfo.data[data.n] = data.data;
-		chunkInfo.count++;
-		this._chunkedData[id] = chunkInfo;
-
-		if (chunkInfo.total === chunkInfo.count) {
-			// Clean up before making the recursive call to `_handleDataMessage`.
-			delete this._chunkedData[id];
-
-			// We've received all the chunks--time to construct the complete data.
-			const data = new Blob(chunkInfo.data);
-			this._handleDataMessage({ data });
-		}
-	}
-
-	/**
-	 * Exposed functionality for users.
-	 */
-
-	/** Allows user to close connection. */
-	close(): void {
-		this._buffer = [];
-		this._bufferSize = 0;
-		this._chunkedData = {};
-
-		if (this._negotiator) {
-			this._negotiator.cleanup();
-			this._negotiator = null;
-		}
-
-		if (this.provider) {
-			this.provider._removeConnection(this);
-
-			this.provider = null;
-		}
-
-		if (this.dataChannel) {
-			this.dataChannel.onopen = null;
-			this.dataChannel.onmessage = null;
-			this.dataChannel.onclose = null;
-			this._dc = null;
-		}
-
-		if (this._encodingQueue) {
-			this._encodingQueue.destroy();
-			this._encodingQueue.removeAllListeners();
-			this._encodingQueue = null;
-		}
-
-		if (!this.open) {
-			return;
-		}
-
-		this._open = false;
-
-		super.emit("close");
-	}
-
-	/** Allows user to send data. */
-	send(data: any, chunked?: boolean): void {
-		if (!this.open) {
-			super.emit(
-				"error",
-				new Error(
-					"Connection is not open. You should listen for the `open` event before sending messages.",
-				),
-			);
-			return;
-		}
-
-		if (this.serialization === SerializationType.JSON) {
-			this._bufferedSend(this.stringify(data));
-		} else if (
-			this.serialization === SerializationType.Binary ||
-			this.serialization === SerializationType.BinaryUTF8
-		) {
-			const blob = util.pack(data);
-
-			if (!chunked && blob.size > util.chunkedMTU) {
-				this._sendChunks(blob);
-				return;
-			}
-
-			if (!util.supports.binaryBlob) {
-				// We only do this if we really need to (e.g. blobs are not supported),
-				// because this conversion is costly.
-				this._encodingQueue.enque(blob);
-			} else {
-				this._bufferedSend(blob);
-			}
-		} else {
-			this._bufferedSend(data);
-		}
-	}
-
-	private _bufferedSend(msg: any): void {
-		if (this._buffering || !this._trySend(msg)) {
-			this._buffer.push(msg);
-			this._bufferSize = this._buffer.length;
-		}
-	}
-
-	// Returns true if the send succeeds.
-	private _trySend(msg: any): boolean {
-		if (!this.open) {
-			return false;
-		}
-
-		if (this.dataChannel.bufferedAmount > DataConnection.MAX_BUFFERED_AMOUNT) {
-			this._buffering = true;
-			setTimeout(() => {
-				this._buffering = false;
-				this._tryBuffer();
-			}, 50);
-
-			return false;
-		}
-
-		try {
-			this.dataChannel.send(msg);
-		} catch (e) {
-			logger.error(`DC#:${this.connectionId} Error when sending:`, e);
-			this._buffering = true;
-
-			this.close();
-
-			return false;
-		}
-
-		return true;
-	}
-
-	// Try to send the first message in the buffer.
-	private _tryBuffer(): void {
-		if (!this.open) {
-			return;
-		}
-
-		if (this._buffer.length === 0) {
-			return;
-		}
-
-		const msg = this._buffer[0];
-
-		if (this._trySend(msg)) {
-			this._buffer.shift();
-			this._bufferSize = this._buffer.length;
-			this._tryBuffer();
-		}
-	}
-
-	private _sendChunks(blob: Blob): void {
-		const blobs = util.chunk(blob);
-		logger.log(`DC#${this.connectionId} Try to send ${blobs.length} chunks...`);
-
-		for (let blob of blobs) {
-			this.send(blob, true);
-		}
-	}
-
-	handleMessage(message: ServerMessage): void {
-		const payload = message.payload;
-
-		switch (message.type) {
-			case ServerMessageType.Answer:
-				this._negotiator.handleSDP(message.type, payload.sdp);
-				break;
-			case ServerMessageType.Candidate:
-				this._negotiator.handleCandidate(payload.candidate);
-				break;
-			default:
-				logger.warn(
-					"Unrecognized message type:",
-					message.type,
-					"from peer:",
-					this.peer,
-				);
-				break;
-		}
-	}
-}

+ 100 - 0
lib/dataconnection/BufferedConnection/BinaryPack.ts

@@ -0,0 +1,100 @@
+import { BinaryPackChunker, concatArrayBuffers } from "./binaryPackChunker";
+import logger from "../../logger";
+import type { Peer } from "../../peer";
+import { BufferedConnection } from "./BufferedConnection";
+import { SerializationType } from "../../enums";
+import { pack, type Packable, unpack } from "peerjs-js-binarypack";
+
+export class BinaryPack extends BufferedConnection {
+	private readonly chunker = new BinaryPackChunker();
+	readonly serialization = SerializationType.Binary;
+
+	private _chunkedData: {
+		[id: number]: {
+			data: Uint8Array[];
+			count: number;
+			total: number;
+		};
+	} = {};
+
+	public override close(options?: { flush?: boolean }) {
+		super.close(options);
+		this._chunkedData = {};
+	}
+
+	constructor(peerId: string, provider: Peer, options: any) {
+		super(peerId, provider, options);
+	}
+
+	// Handles a DataChannel message.
+	protected override _handleDataMessage({ data }: { data: Uint8Array }): void {
+		const deserializedData = unpack(data);
+
+		// PeerJS specific message
+		const peerData = deserializedData["__peerData"];
+		if (peerData) {
+			if (peerData.type === "close") {
+				this.close();
+				return;
+			}
+
+			// Chunked data -- piece things back together.
+			// @ts-ignore
+			this._handleChunk(deserializedData);
+			return;
+		}
+
+		this.emit("data", deserializedData);
+	}
+
+	private _handleChunk(data: {
+		__peerData: number;
+		n: number;
+		total: number;
+		data: ArrayBuffer;
+	}): void {
+		const id = data.__peerData;
+		const chunkInfo = this._chunkedData[id] || {
+			data: [],
+			count: 0,
+			total: data.total,
+		};
+
+		chunkInfo.data[data.n] = new Uint8Array(data.data);
+		chunkInfo.count++;
+		this._chunkedData[id] = chunkInfo;
+
+		if (chunkInfo.total === chunkInfo.count) {
+			// Clean up before making the recursive call to `_handleDataMessage`.
+			delete this._chunkedData[id];
+
+			// We've received all the chunks--time to construct the complete data.
+			// const data = new Blob(chunkInfo.data);
+			const data = concatArrayBuffers(chunkInfo.data);
+			this._handleDataMessage({ data });
+		}
+	}
+
+	protected override _send(
+		data: Packable,
+		chunked: boolean,
+	): void | Promise<void> {
+		const blob = pack(data);
+
+		if (!chunked && blob.byteLength > this.chunker.chunkedMTU) {
+			this._sendChunks(blob);
+			return;
+		}
+
+		this._bufferedSend(blob);
+	}
+
+	private _sendChunks(blob: ArrayBuffer) {
+		const blobs = this.chunker.chunk(blob);
+		logger.log(`DC#${this.connectionId} Try to send ${blobs.length} chunks...`);
+
+		for (const blob of blobs) {
+			this.send(blob, true);
+		}
+	}
+}

+ 92 - 0
lib/dataconnection/BufferedConnection/BufferedConnection.ts

@@ -0,0 +1,92 @@
+import logger from "../../logger";
+import { DataConnection } from "../DataConnection";
+
+export abstract class BufferedConnection extends DataConnection {
+	private _buffer: any[] = [];
+	private _bufferSize = 0;
+	private _buffering = false;
+
+	public get bufferSize(): number {
+		return this._bufferSize;
+	}
+
+	public override _initializeDataChannel(dc: RTCDataChannel) {
+		super._initializeDataChannel(dc);
+		this.dataChannel.binaryType = "arraybuffer";
+		this.dataChannel.addEventListener("message", (e) =>
+			this._handleDataMessage(e),
+		);
+	}
+
+	protected abstract _handleDataMessage(e: MessageEvent): void;
+
+	protected _bufferedSend(msg: ArrayBuffer): void {
+		if (this._buffering || !this._trySend(msg)) {
+			this._buffer.push(msg);
+			this._bufferSize = this._buffer.length;
+		}
+	}
+
+	// Returns true if the send succeeds.
+	private _trySend(msg: ArrayBuffer): boolean {
+		if (!this.open) {
+			return false;
+		}
+
+		if (this.dataChannel.bufferedAmount > DataConnection.MAX_BUFFERED_AMOUNT) {
+			this._buffering = true;
+			setTimeout(() => {
+				this._buffering = false;
+				this._tryBuffer();
+			}, 50);
+
+			return false;
+		}
+
+		try {
+			this.dataChannel.send(msg);
+		} catch (e) {
+			logger.error(`DC#:${this.connectionId} Error when sending:`, e);
+			this._buffering = true;
+
+			this.close();
+
+			return false;
+		}
+
+		return true;
+	}
+
+	// Try to send the first message in the buffer.
+	private _tryBuffer(): void {
+		if (!this.open) {
+			return;
+		}
+
+		if (this._buffer.length === 0) {
+			return;
+		}
+
+		const msg = this._buffer[0];
+
+		if (this._trySend(msg)) {
+			this._buffer.shift();
+			this._bufferSize = this._buffer.length;
+			this._tryBuffer();
+		}
+	}
+
+	public override close(options?: { flush?: boolean }) {
+		if (options?.flush) {
+			this.send({
+				__peerData: {
+					type: "close",
+				},
+			});
+			return;
+		}
+		this._buffer = [];
+		this._bufferSize = 0;
+		super.close();
+	}
+}

+ 38 - 0
lib/dataconnection/BufferedConnection/Json.ts

@@ -0,0 +1,38 @@
+import { BufferedConnection } from "./BufferedConnection";
+import { DataConnectionErrorType, SerializationType } from "../../enums";
+import { util } from "../../util";
+
+export class Json extends BufferedConnection {
+	readonly serialization = SerializationType.JSON;
+	private readonly encoder = new TextEncoder();
+	private readonly decoder = new TextDecoder();
+
+	stringify: (data: any) => string = JSON.stringify;
+	parse: (data: string) => any = JSON.parse;
+
+	// Handles a DataChannel message.
+	protected override _handleDataMessage({ data }: { data: Uint8Array }): void {
+		const deserializedData = this.parse(this.decoder.decode(data));
+
+		// PeerJS specific message
+		const peerData = deserializedData["__peerData"];
+		if (peerData && peerData.type === "close") {
+			this.close();
+			return;
+		}
+
+		this.emit("data", deserializedData);
+	}
+
+	override _send(data, _chunked) {
+		const encodedData = this.encoder.encode(this.stringify(data));
+		if (encodedData.byteLength >= util.chunkedMTU) {
+			this.emitError(
+				DataConnectionErrorType.MessageToBig,
+				"Message too big for JSON channel",
+			);
+			return;
+		}
+		this._bufferedSend(encodedData);
+	}
+}

+ 14 - 0
lib/dataconnection/BufferedConnection/Raw.ts

@@ -0,0 +1,14 @@
+import { BufferedConnection } from "./BufferedConnection";
+import { SerializationType } from "../../enums";
+
+export class Raw extends BufferedConnection {
+	readonly serialization = SerializationType.None;
+
+	protected _handleDataMessage({ data }) {
+		super.emit("data", data);
+	}
+
+	override _send(data, _chunked) {
+		this._bufferedSend(data);
+	}
+}

+ 53 - 0
lib/dataconnection/BufferedConnection/binaryPackChunker.ts

@@ -0,0 +1,53 @@
+export class BinaryPackChunker {
+	readonly chunkedMTU = 16300; // The original 60000 bytes setting does not work when sending data from Firefox to Chrome, which is "cut off" after 16384 bytes and delivered individually.
+
+	// Binary stuff
+
+	private _dataCount: number = 1;
+
+	chunk = (
+		blob: ArrayBuffer,
+	): { __peerData: number; n: number; total: number; data: Uint8Array }[] => {
+		const chunks = [];
+		const size = blob.byteLength;
+		const total = Math.ceil(size / this.chunkedMTU);
+
+		let index = 0;
+		let start = 0;
+
+		while (start < size) {
+			const end = Math.min(size, start + this.chunkedMTU);
+			const b = blob.slice(start, end);
+
+			const chunk = {
+				__peerData: this._dataCount,
+				n: index,
+				data: b,
+				total,
+			};
+
+			chunks.push(chunk);
+
+			start = end;
+			index++;
+		}
+
+		this._dataCount++;
+
+		return chunks;
+	};
+}
+
+export function concatArrayBuffers(bufs: Uint8Array[]) {
+	let size = 0;
+	for (const buf of bufs) {
+		size += buf.byteLength;
+	}
+	const result = new Uint8Array(size);
+	let offset = 0;
+	for (const buf of bufs) {
+		result.set(buf, offset);
+		offset += buf.byteLength;
+	}
+	return result;
+}

+ 161 - 0
lib/dataconnection/DataConnection.ts

@@ -0,0 +1,161 @@
+import logger from "../logger";
+import { Negotiator } from "../negotiator";
+import {
+	BaseConnectionErrorType,
+	ConnectionType,
+	DataConnectionErrorType,
+	ServerMessageType,
+} from "../enums";
+import type { Peer } from "../peer";
+import { BaseConnection, type BaseConnectionEvents } from "../baseconnection";
+import type { ServerMessage } from "../servermessage";
+import type { EventsWithError } from "../peerError";
+import { randomToken } from "../utils/randomToken";
+
+export interface DataConnectionEvents
+	extends EventsWithError<DataConnectionErrorType | BaseConnectionErrorType>,
+		BaseConnectionEvents<DataConnectionErrorType | BaseConnectionErrorType> {
+	/**
+	 * Emitted when data is received from the remote peer.
+	 */
+	data: (data: unknown) => void;
+	/**
+	 * Emitted when the connection is established and ready-to-use.
+	 */
+	open: () => void;
+}
+
+/**
+ * Wraps a DataChannel between two Peers.
+ */
+export abstract class DataConnection extends BaseConnection<
+	DataConnectionEvents,
+	DataConnectionErrorType
+> {
+	protected static readonly ID_PREFIX = "dc_";
+	protected static readonly MAX_BUFFERED_AMOUNT = 8 * 1024 * 1024;
+
+	private _negotiator: Negotiator<DataConnectionEvents, this>;
+	abstract readonly serialization: string;
+	readonly reliable: boolean;
+
+	public get type() {
+		return ConnectionType.Data;
+	}
+
+	constructor(peerId: string, provider: Peer, options: any) {
+		super(peerId, provider, options);
+
+		this.connectionId =
+			this.options.connectionId || DataConnection.ID_PREFIX + randomToken();
+
+		this.label = this.options.label || this.connectionId;
+		this.reliable = !!this.options.reliable;
+
+		this._negotiator = new Negotiator(this);
+
+		this._negotiator.startConnection(
+			this.options._payload || {
+				originator: true,
+				reliable: this.reliable,
+			},
+		);
+	}
+
+	/** Called by the Negotiator when the DataChannel is ready. */
+	override _initializeDataChannel(dc: RTCDataChannel): void {
+		this.dataChannel = dc;
+
+		this.dataChannel.onopen = () => {
+			logger.log(`DC#${this.connectionId} dc connection success`);
+			this._open = true;
+			this.emit("open");
+		};
+
+		this.dataChannel.onmessage = (e) => {
+			logger.log(`DC#${this.connectionId} dc onmessage:`, e.data);
+			// this._handleDataMessage(e);
+		};
+
+		this.dataChannel.onclose = () => {
+			logger.log(`DC#${this.connectionId} dc closed for:`, this.peer);
+			this.close();
+		};
+	}
+
+	/**
+	 * Exposed functionality for users.
+	 */
+
+	/** Allows user to close connection. */
+	close(options?: { flush?: boolean }): void {
+		if (options?.flush) {
+			this.send({
+				__peerData: {
+					type: "close",
+				},
+			});
+			return;
+		}
+		if (this._negotiator) {
+			this._negotiator.cleanup();
+			this._negotiator = null;
+		}
+
+		if (this.provider) {
+			this.provider._removeConnection(this);
+
+			this.provider = null;
+		}
+
+		if (this.dataChannel) {
+			this.dataChannel.onopen = null;
+			this.dataChannel.onmessage = null;
+			this.dataChannel.onclose = null;
+			this.dataChannel = null;
+		}
+
+		if (!this.open) {
+			return;
+		}
+
+		this._open = false;
+
+		super.emit("close");
+	}
+
+	protected abstract _send(data: any, chunked: boolean): void;
+
+	/** Allows user to send data. */
+	public send(data: any, chunked = false) {
+		if (!this.open) {
+			this.emitError(
+				DataConnectionErrorType.NotOpenYet,
+				"Connection is not open. You should listen for the `open` event before sending messages.",
+			);
+			return;
+		}
+		return this._send(data, chunked);
+	}
+
+	async handleMessage(message: ServerMessage) {
+		const payload = message.payload;
+
+		switch (message.type) {
+			case ServerMessageType.Answer:
+				await this._negotiator.handleSDP(message.type, payload.sdp);
+				break;
+			case ServerMessageType.Candidate:
+				await this._negotiator.handleCandidate(payload.candidate);
+				break;
+			default:
+				logger.warn(
+					"Unrecognized message type:",
+					message.type,
+					"from peer:",
+					this.peer,
+				);
+				break;
+		}
+	}
+}

+ 75 - 0
lib/dataconnection/StreamConnection/Cbor.ts

@@ -0,0 +1,75 @@
+import type { Peer } from "../../peer.js";
+import { Decoder, Encoder } from "cbor-x";
+import { StreamConnection } from "./StreamConnection.js";
+
+const NullValue = Symbol.for(null);
+
+function concatUint8Array(buffer1: Uint8Array, buffer2: Uint8Array) {
+	const tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength);
+	tmp.set(buffer1, 0);
+	tmp.set(buffer2, buffer1.byteLength);
+	return new Uint8Array(tmp.buffer);
+}
+
+const iterateOver = async function* (stream: ReadableStream) {
+	const reader = stream.getReader();
+	try {
+		while (true) {
+			const { done, value } = await reader.read();
+			if (done) return;
+			yield value;
+		}
+	} finally {
+		reader.releaseLock();
+	}
+};
+
+export class Cbor extends StreamConnection {
+	readonly serialization = "Cbor";
+	private _encoder = new Encoder();
+	private _decoder = new Decoder();
+	private _inc;
+	private _decoderStream = new TransformStream<ArrayBuffer, unknown>({
+		transform: (abchunk, controller) => {
+			let chunk = new Uint8Array(abchunk);
+			if (this._inc) {
+				chunk = concatUint8Array(this._inc, chunk);
+				this._inc = null;
+			}
+			let values;
+			try {
+				values = this._decoder.decodeMultiple(chunk);
+			} catch (error) {
+				if (error.incomplete) {
+					this._inc = chunk.subarray(error.lastPosition);
+					values = error.values;
+				} else throw error;
+			} finally {
+				for (let value of values || []) {
+					if (value === null) value = NullValue;
+					controller.enqueue(value);
+				}
+			}
+		},
+	});
+
+	constructor(peerId: string, provider: Peer, options: any) {
+		super(peerId, provider, { ...options, reliable: true });
+
+		void this._rawReadStream.pipeTo(this._decoderStream.writable);
+
+		(async () => {
+			for await (const msg of iterateOver(this._decoderStream.readable)) {
+				if (msg.__peerData?.type === "close") {
+					this.close();
+					return;
+				}
+				this.emit("data", msg);
+			}
+		})();
+	}
+
+	protected override _send(data) {
+		return this.writer.write(this._encoder.encode(data));
+	}
+}

+ 27 - 0
lib/dataconnection/StreamConnection/MsgPack.ts

@@ -0,0 +1,27 @@
+import { decodeMultiStream, Encoder } from "@msgpack/msgpack";
+import { StreamConnection } from "./StreamConnection.js";
+import type { Peer } from "../../peer.js";
+
+export class MsgPack extends StreamConnection {
+	readonly serialization = "MsgPack";
+	private _encoder = new Encoder();
+
+	constructor(peerId: string, provider: Peer, options: any) {
+		super(peerId, provider, options);
+
+		(async () => {
+			for await (const msg of decodeMultiStream(this._rawReadStream)) {
+				// @ts-ignore
+				if (msg.__peerData?.type === "close") {
+					this.close();
+					return;
+				}
+				this.emit("data", msg);
+			}
+		})();
+	}
+
+	protected override _send(data) {
+		return this.writer.write(this._encoder.encode(data));
+	}
+}

+ 61 - 0
lib/dataconnection/StreamConnection/StreamConnection.ts

@@ -0,0 +1,61 @@
+import logger from "../../logger.js";
+import type { Peer } from "../../peer.js";
+import { DataConnection } from "../DataConnection.js";
+
+export abstract class StreamConnection extends DataConnection {
+	private _CHUNK_SIZE = 1024 * 8 * 4;
+	private _splitStream = new TransformStream<Uint8Array>({
+		transform: (chunk, controller) => {
+			for (let split = 0; split < chunk.length; split += this._CHUNK_SIZE) {
+				controller.enqueue(chunk.subarray(split, split + this._CHUNK_SIZE));
+			}
+		},
+	});
+	private _rawSendStream = new WritableStream<ArrayBuffer>({
+		write: async (chunk, controller) => {
+			const openEvent = new Promise((resolve) =>
+				this.dataChannel.addEventListener("bufferedamountlow", resolve, {
+					once: true,
+				}),
+			);
+
+			// if we can send the chunk now, send it
+			// if not, we wait until at least half of the sending buffer is free again
+			await (this.dataChannel.bufferedAmount <=
+				DataConnection.MAX_BUFFERED_AMOUNT - chunk.byteLength || openEvent);
+
+			// TODO: what can go wrong here?
+			try {
+				this.dataChannel.send(chunk);
+			} catch (e) {
+				logger.error(`DC#:${this.connectionId} Error when sending:`, e);
+				controller.error(e);
+				this.close();
+			}
+		},
+	});
+	protected writer = this._splitStream.writable.getWriter();
+
+	protected _rawReadStream = new ReadableStream<ArrayBuffer>({
+		start: (controller) => {
+			this.once("open", () => {
+				this.dataChannel.addEventListener("message", (e) => {
+					controller.enqueue(e.data);
+				});
+			});
+		},
+	});
+
+	protected constructor(peerId: string, provider: Peer, options: any) {
+		super(peerId, provider, { ...options, reliable: true });
+
+		void this._splitStream.readable.pipeTo(this._rawSendStream);
+	}
+
+	public override _initializeDataChannel(dc) {
+		super._initializeDataChannel(dc);
+		this.dataChannel.binaryType = "arraybuffer";
+		this.dataChannel.bufferedAmountLowThreshold =
+			DataConnection.MAX_BUFFERED_AMOUNT / 2;
+	}
+}

+ 53 - 0
lib/enums.ts

@@ -4,24 +4,77 @@ export enum ConnectionType {
 }
 }
 
 
 export enum PeerErrorType {
 export enum PeerErrorType {
+	/**
+	 * The client's browser does not support some or all WebRTC features that you are trying to use.
+	 */
 	BrowserIncompatible = "browser-incompatible",
 	BrowserIncompatible = "browser-incompatible",
+	/**
+	 * You've already disconnected this peer from the server and can no longer make any new connections on it.
+	 */
 	Disconnected = "disconnected",
 	Disconnected = "disconnected",
+	/**
+	 * The ID passed into the Peer constructor contains illegal characters.
+	 */
 	InvalidID = "invalid-id",
 	InvalidID = "invalid-id",
+	/**
+	 * The API key passed into the Peer constructor contains illegal characters or is not in the system (cloud server only).
+	 */
 	InvalidKey = "invalid-key",
 	InvalidKey = "invalid-key",
+	/**
+	 * Lost or cannot establish a connection to the signalling server.
+	 */
 	Network = "network",
 	Network = "network",
+	/**
+	 * The peer you're trying to connect to does not exist.
+	 */
 	PeerUnavailable = "peer-unavailable",
 	PeerUnavailable = "peer-unavailable",
+	/**
+	 * PeerJS is being used securely, but the cloud server does not support SSL. Use a custom PeerServer.
+	 */
 	SslUnavailable = "ssl-unavailable",
 	SslUnavailable = "ssl-unavailable",
+	/**
+	 * Unable to reach the server.
+	 */
 	ServerError = "server-error",
 	ServerError = "server-error",
+	/**
+	 * An error from the underlying socket.
+	 */
 	SocketError = "socket-error",
 	SocketError = "socket-error",
+	/**
+	 * The underlying socket closed unexpectedly.
+	 */
 	SocketClosed = "socket-closed",
 	SocketClosed = "socket-closed",
+	/**
+	 * The ID passed into the Peer constructor is already taken.
+	 *
+	 * :::caution
+	 * This error is not fatal if your peer has open peer-to-peer connections.
+	 * This can happen if you attempt to {@apilink Peer.reconnect} a peer that has been disconnected from the server,
+	 * but its old ID has now been taken.
+	 * :::
+	 */
 	UnavailableID = "unavailable-id",
 	UnavailableID = "unavailable-id",
+	/**
+	 * Native WebRTC errors.
+	 */
 	WebRTC = "webrtc",
 	WebRTC = "webrtc",
 }
 }
 
 
+export enum BaseConnectionErrorType {
+	NegotiationFailed = "negotiation-failed",
+	ConnectionClosed = "connection-closed",
+}
+
+export enum DataConnectionErrorType {
+	NotOpenYet = "not-open-yet",
+	MessageToBig = "message-too-big",
+}
+
 export enum SerializationType {
 export enum SerializationType {
 	Binary = "binary",
 	Binary = "binary",
 	BinaryUTF8 = "binary-utf8",
 	BinaryUTF8 = "binary-utf8",
 	JSON = "json",
 	JSON = "json",
+	None = "raw",
 }
 }
 
 
 export enum SocketEventType {
 export enum SocketEventType {

+ 16 - 10
lib/exports.ts

@@ -1,5 +1,9 @@
-import { util } from "./util";
+export { util, type Util } from "./util";
 import { Peer } from "./peer";
 import { Peer } from "./peer";
+import { CborPeer } from "./cborPeer";
+import { MsgPackPeer } from "./msgPackPeer";
+
+export type { PeerEvents, PeerOptions } from "./peer";
 
 
 export type {
 export type {
 	PeerJSOption,
 	PeerJSOption,
@@ -8,16 +12,18 @@ export type {
 	CallOption,
 	CallOption,
 } from "./optionInterfaces";
 } from "./optionInterfaces";
 export type { UtilSupportsObj } from "./util";
 export type { UtilSupportsObj } from "./util";
-export type { DataConnection } from "./dataconnection";
+export type { DataConnection } from "./dataconnection/DataConnection";
 export type { MediaConnection } from "./mediaconnection";
 export type { MediaConnection } from "./mediaconnection";
 export type { LogLevel } from "./logger";
 export type { LogLevel } from "./logger";
-export type {
-	ConnectionType,
-	PeerErrorType,
-	SerializationType,
-	SocketEventType,
-	ServerMessageType,
-} from "./enums";
+export * from "./enums";
+
+export { BufferedConnection } from "./dataconnection/BufferedConnection/BufferedConnection";
+export { StreamConnection } from "./dataconnection/StreamConnection/StreamConnection";
+export { Cbor } from "./dataconnection/StreamConnection/Cbor";
+export { MsgPack } from "./dataconnection/StreamConnection/MsgPack";
+export type { SerializerMapping } from "./peer";
+
+export { Peer, MsgPackPeer, CborPeer };
 
 
-export { Peer, util };
+export { PeerError } from "./peerError";
 export default Peer;
 export default Peer;

+ 13 - 1
lib/logger.ts

@@ -8,9 +8,21 @@ Prints log messages depending on the debug level passed in. Defaults to 0.
 3  Prints all logs.
 3  Prints all logs.
 */
 */
 export enum LogLevel {
 export enum LogLevel {
+	/**
+	 * Prints no logs.
+	 */
 	Disabled,
 	Disabled,
+	/**
+	 * Prints only errors.
+	 */
 	Errors,
 	Errors,
+	/**
+	 * Prints errors and warnings.
+	 */
 	Warnings,
 	Warnings,
+	/**
+	 * Prints all logs.
+	 */
 	All,
 	All,
 }
 }
 
 
@@ -50,7 +62,7 @@ class Logger {
 	private _print(logLevel: LogLevel, ...rest: any[]): void {
 	private _print(logLevel: LogLevel, ...rest: any[]): void {
 		const copy = [LOG_PREFIX, ...rest];
 		const copy = [LOG_PREFIX, ...rest];
 
 
-		for (let i in copy) {
+		for (const i in copy) {
 			if (copy[i] instanceof Error) {
 			if (copy[i] instanceof Error) {
 				copy[i] = "(" + copy[i].name + ") " + copy[i].message;
 				copy[i] = "(" + copy[i].name + ") " + copy[i].message;
 			}
 			}

+ 56 - 11
lib/mediaconnection.ts

@@ -2,28 +2,43 @@ import { util } from "./util";
 import logger from "./logger";
 import logger from "./logger";
 import { Negotiator } from "./negotiator";
 import { Negotiator } from "./negotiator";
 import { ConnectionType, ServerMessageType } from "./enums";
 import { ConnectionType, ServerMessageType } from "./enums";
-import { Peer } from "./peer";
-import { BaseConnection } from "./baseconnection";
-import { ServerMessage } from "./servermessage";
+import type { Peer } from "./peer";
+import { BaseConnection, type BaseConnectionEvents } from "./baseconnection";
+import type { ServerMessage } from "./servermessage";
 import type { AnswerOption } from "./optionInterfaces";
 import type { AnswerOption } from "./optionInterfaces";
 
 
-type MediaConnectionEvents = {
+export interface MediaConnectionEvents extends BaseConnectionEvents<never> {
 	/**
 	/**
 	 * Emitted when a connection to the PeerServer is established.
 	 * Emitted when a connection to the PeerServer is established.
+	 *
+	 * ```ts
+	 * mediaConnection.on('stream', (stream) => { ... });
+	 * ```
 	 */
 	 */
 	stream: (stream: MediaStream) => void;
 	stream: (stream: MediaStream) => void;
-};
+	/**
+	 * Emitted when the auxiliary data channel is established.
+	 * After this event, hanging up will close the connection cleanly on the remote peer.
+	 * @beta
+	 */
+	willCloseOnRemote: () => void;
+}
 
 
 /**
 /**
- * Wraps the streaming interface between two Peers.
+ * Wraps WebRTC's media streams.
+ * To get one, use {@apilink Peer.call} or listen for the {@apilink PeerEvents | `call`} event.
  */
  */
 export class MediaConnection extends BaseConnection<MediaConnectionEvents> {
 export class MediaConnection extends BaseConnection<MediaConnectionEvents> {
 	private static readonly ID_PREFIX = "mc_";
 	private static readonly ID_PREFIX = "mc_";
+	readonly label: string;
 
 
-	private _negotiator: Negotiator<MediaConnectionEvents, MediaConnection>;
+	private _negotiator: Negotiator<MediaConnectionEvents, this>;
 	private _localStream: MediaStream;
 	private _localStream: MediaStream;
 	private _remoteStream: MediaStream;
 	private _remoteStream: MediaStream;
 
 
+	/**
+	 * For media connections, this is always 'media'.
+	 */
 	get type() {
 	get type() {
 		return ConnectionType.Media;
 		return ConnectionType.Media;
 	}
 	}
@@ -31,6 +46,7 @@ export class MediaConnection extends BaseConnection<MediaConnectionEvents> {
 	get localStream(): MediaStream {
 	get localStream(): MediaStream {
 		return this._localStream;
 		return this._localStream;
 	}
 	}
+
 	get remoteStream(): MediaStream {
 	get remoteStream(): MediaStream {
 		return this._remoteStream;
 		return this._remoteStream;
 	}
 	}
@@ -53,6 +69,20 @@ export class MediaConnection extends BaseConnection<MediaConnectionEvents> {
 		}
 		}
 	}
 	}
 
 
+	/** Called by the Negotiator when the DataChannel is ready. */
+	override _initializeDataChannel(dc: RTCDataChannel): void {
+		this.dataChannel = dc;
+
+		this.dataChannel.onopen = () => {
+			logger.log(`DC#${this.connectionId} dc connection success`);
+			this.emit("willCloseOnRemote");
+		};
+
+		this.dataChannel.onclose = () => {
+			logger.log(`DC#${this.connectionId} dc closed for:`, this.peer);
+			this.close();
+		};
+	}
 	addStream(remoteStream) {
 	addStream(remoteStream) {
 		logger.log("Receiving stream", remoteStream);
 		logger.log("Receiving stream", remoteStream);
 
 
@@ -60,6 +90,9 @@ export class MediaConnection extends BaseConnection<MediaConnectionEvents> {
 		super.emit("stream", remoteStream); // Should we call this `open`?
 		super.emit("stream", remoteStream); // Should we call this `open`?
 	}
 	}
 
 
+	/**
+	 * @internal
+	 */
 	handleMessage(message: ServerMessage): void {
 	handleMessage(message: ServerMessage): void {
 		const type = message.type;
 		const type = message.type;
 		const payload = message.payload;
 		const payload = message.payload;
@@ -67,11 +100,11 @@ export class MediaConnection extends BaseConnection<MediaConnectionEvents> {
 		switch (message.type) {
 		switch (message.type) {
 			case ServerMessageType.Answer:
 			case ServerMessageType.Answer:
 				// Forward to negotiator
 				// Forward to negotiator
-				this._negotiator.handleSDP(type, payload.sdp);
+				void this._negotiator.handleSDP(type, payload.sdp);
 				this._open = true;
 				this._open = true;
 				break;
 				break;
 			case ServerMessageType.Candidate:
 			case ServerMessageType.Candidate:
-				this._negotiator.handleCandidate(payload.candidate);
+				void this._negotiator.handleCandidate(payload.candidate);
 				break;
 				break;
 			default:
 			default:
 				logger.warn(`Unrecognized message type:${type} from peer:${this.peer}`);
 				logger.warn(`Unrecognized message type:${type} from peer:${this.peer}`);
@@ -79,6 +112,16 @@ export class MediaConnection extends BaseConnection<MediaConnectionEvents> {
 		}
 		}
 	}
 	}
 
 
+	/**
+     * When receiving a {@apilink PeerEvents | `call`} event on a peer, you can call
+     * `answer` on the media connection provided by the callback to accept the call
+     * and optionally send your own media stream.
+
+     *
+     * @param stream A WebRTC media stream.
+     * @param options
+     * @returns
+     */
 	answer(stream?: MediaStream, options: AnswerOption = {}): void {
 	answer(stream?: MediaStream, options: AnswerOption = {}): void {
 		if (this._localStream) {
 		if (this._localStream) {
 			logger.warn(
 			logger.warn(
@@ -100,7 +143,7 @@ export class MediaConnection extends BaseConnection<MediaConnectionEvents> {
 		// Retrieve lost messages stored because PeerConnection not set up.
 		// Retrieve lost messages stored because PeerConnection not set up.
 		const messages = this.provider._getMessages(this.connectionId);
 		const messages = this.provider._getMessages(this.connectionId);
 
 
-		for (let message of messages) {
+		for (const message of messages) {
 			this.handleMessage(message);
 			this.handleMessage(message);
 		}
 		}
 
 
@@ -111,7 +154,9 @@ export class MediaConnection extends BaseConnection<MediaConnectionEvents> {
 	 * Exposed functionality for users.
 	 * Exposed functionality for users.
 	 */
 	 */
 
 
-	/** Allows user to close connection. */
+	/**
+	 * Closes the media connection.
+	 */
 	close(): void {
 	close(): void {
 		if (this._negotiator) {
 		if (this._negotiator) {
 			this._negotiator.cleanup();
 			this._negotiator.cleanup();

+ 9 - 0
lib/msgPackPeer.ts

@@ -0,0 +1,9 @@
+import { Peer, type SerializerMapping } from "./peer";
+import { MsgPack } from "./exports";
+
+export class MsgPackPeer extends Peer {
+	override _serializers: SerializerMapping = {
+		MsgPack,
+		default: MsgPack,
+	};
+}

+ 37 - 52
lib/negotiator.ts

@@ -1,19 +1,23 @@
-import { util } from "./util";
 import logger from "./logger";
 import logger from "./logger";
-import { MediaConnection } from "./mediaconnection";
-import { DataConnection } from "./dataconnection";
-import { ConnectionType, PeerErrorType, ServerMessageType } from "./enums";
-import { BaseConnection, BaseConnectionEvents } from "./baseconnection";
-import { ValidEventTypes } from "eventemitter3";
+import type { MediaConnection } from "./mediaconnection";
+import type { DataConnection } from "./dataconnection/DataConnection";
+import {
+	BaseConnectionErrorType,
+	ConnectionType,
+	PeerErrorType,
+	ServerMessageType,
+} from "./enums";
+import type { BaseConnection, BaseConnectionEvents } from "./baseconnection";
+import type { ValidEventTypes } from "eventemitter3";
 
 
 /**
 /**
  * Manages all negotiations between Peers.
  * Manages all negotiations between Peers.
  */
  */
 export class Negotiator<
 export class Negotiator<
-	A extends ValidEventTypes,
-	T extends BaseConnection<A | BaseConnectionEvents>,
+	Events extends ValidEventTypes,
+	ConnectionType extends BaseConnection<Events | BaseConnectionEvents>,
 > {
 > {
-	constructor(readonly connection: T) {}
+	constructor(readonly connection: ConnectionType) {}
 
 
 	/** Returns a PeerConnection object set up correctly (for data, media). */
 	/** Returns a PeerConnection object set up correctly (for data, media). */
 	startConnection(options: any) {
 	startConnection(options: any) {
@@ -28,21 +32,19 @@ export class Negotiator<
 
 
 		// What do we need to do now?
 		// What do we need to do now?
 		if (options.originator) {
 		if (options.originator) {
-			if (this.connection.type === ConnectionType.Data) {
-				const dataConnection = <DataConnection>(<unknown>this.connection);
+			const dataConnection = this.connection;
 
 
-				const config: RTCDataChannelInit = { ordered: !!options.reliable };
+			const config: RTCDataChannelInit = { ordered: !!options.reliable };
 
 
-				const dataChannel = peerConnection.createDataChannel(
-					dataConnection.label,
-					config,
-				);
-				dataConnection.initialize(dataChannel);
-			}
+			const dataChannel = peerConnection.createDataChannel(
+				dataConnection.label,
+				config,
+			);
+			dataConnection._initializeDataChannel(dataChannel);
 
 
-			this._makeOffer();
+			void this._makeOffer();
 		} else {
 		} else {
-			this.handleSDP("OFFER", options.sdp);
+			void this.handleSDP("OFFER", options.sdp);
 		}
 		}
 	}
 	}
 
 
@@ -91,9 +93,9 @@ export class Negotiator<
 					logger.log(
 					logger.log(
 						"iceConnectionState is failed, closing connections to " + peerId,
 						"iceConnectionState is failed, closing connections to " + peerId,
 					);
 					);
-					this.connection.emit(
-						"error",
-						new Error("Negotiation of connection to " + peerId + " failed."),
+					this.connection.emitError(
+						BaseConnectionErrorType.NegotiationFailed,
+						"Negotiation of connection to " + peerId + " failed.",
 					);
 					);
 					this.connection.close();
 					this.connection.close();
 					break;
 					break;
@@ -101,9 +103,9 @@ export class Negotiator<
 					logger.log(
 					logger.log(
 						"iceConnectionState is closed, closing connections to " + peerId,
 						"iceConnectionState is closed, closing connections to " + peerId,
 					);
 					);
-					this.connection.emit(
-						"error",
-						new Error("Connection to " + peerId + " closed."),
+					this.connection.emitError(
+						BaseConnectionErrorType.ConnectionClosed,
+						"Connection to " + peerId + " closed.",
 					);
 					);
 					this.connection.close();
 					this.connection.close();
 					break;
 					break;
@@ -114,7 +116,7 @@ export class Negotiator<
 					);
 					);
 					break;
 					break;
 				case "completed":
 				case "completed":
-					peerConnection.onicecandidate = util.noop;
+					peerConnection.onicecandidate = () => {};
 					break;
 					break;
 			}
 			}
 
 
@@ -136,7 +138,7 @@ export class Negotiator<
 				provider.getConnection(peerId, connectionId)
 				provider.getConnection(peerId, connectionId)
 			);
 			);
 
 
-			connection.initialize(dataChannel);
+			connection._initializeDataChannel(dataChannel);
 		};
 		};
 
 
 		// MEDIACONNECTION.
 		// MEDIACONNECTION.
@@ -177,14 +179,11 @@ export class Negotiator<
 		const peerConnectionNotClosed = peerConnection.signalingState !== "closed";
 		const peerConnectionNotClosed = peerConnection.signalingState !== "closed";
 		let dataChannelNotClosed = false;
 		let dataChannelNotClosed = false;
 
 
-		if (this.connection.type === ConnectionType.Data) {
-			const dataConnection = <DataConnection>(<unknown>this.connection);
-			const dataChannel = dataConnection.dataChannel;
+		const dataChannel = this.connection.dataChannel;
 
 
-			if (dataChannel) {
-				dataChannelNotClosed =
-					!!dataChannel.readyState && dataChannel.readyState !== "closed";
-			}
+		if (dataChannel) {
+			dataChannelNotClosed =
+				!!dataChannel.readyState && dataChannel.readyState !== "closed";
 		}
 		}
 
 
 		if (peerConnectionNotClosed || dataChannelNotClosed) {
 		if (peerConnectionNotClosed || dataChannelNotClosed) {
@@ -225,7 +224,6 @@ export class Negotiator<
 					type: this.connection.type,
 					type: this.connection.type,
 					connectionId: this.connection.connectionId,
 					connectionId: this.connection.connectionId,
 					metadata: this.connection.metadata,
 					metadata: this.connection.metadata,
-					browser: util.browser,
 				};
 				};
 
 
 				if (this.connection.type === ConnectionType.Data) {
 				if (this.connection.type === ConnectionType.Data) {
@@ -291,7 +289,6 @@ export class Negotiator<
 						sdp: answer,
 						sdp: answer,
 						type: this.connection.type,
 						type: this.connection.type,
 						connectionId: this.connection.connectionId,
 						connectionId: this.connection.connectionId,
-						browser: util.browser,
 					},
 					},
 					dst: this.connection.peer,
 					dst: this.connection.peer,
 				});
 				});
@@ -328,26 +325,14 @@ export class Negotiator<
 	}
 	}
 
 
 	/** Handle a candidate. */
 	/** Handle a candidate. */
-	async handleCandidate(ice: any): Promise<void> {
+	async handleCandidate(ice: RTCIceCandidate) {
 		logger.log(`handleCandidate:`, ice);
 		logger.log(`handleCandidate:`, ice);
 
 
-		const candidate = ice.candidate;
-		const sdpMLineIndex = ice.sdpMLineIndex;
-		const sdpMid = ice.sdpMid;
-		const peerConnection = this.connection.peerConnection;
-		const provider = this.connection.provider;
-
 		try {
 		try {
-			await peerConnection.addIceCandidate(
-				new RTCIceCandidate({
-					sdpMid: sdpMid,
-					sdpMLineIndex: sdpMLineIndex,
-					candidate: candidate,
-				}),
-			);
+			await this.connection.peerConnection.addIceCandidate(ice);
 			logger.log(`Added ICE candidate for:${this.connection.peer}`);
 			logger.log(`Added ICE candidate for:${this.connection.peer}`);
 		} catch (err) {
 		} catch (err) {
-			provider.emitError(PeerErrorType.WebRTC, err);
+			this.connection.provider.emitError(PeerErrorType.WebRTC, err);
 			logger.log("Failed to handleCandidate, ", err);
 			logger.log("Failed to handleCandidate, ", err);
 		}
 		}
 	}
 	}

+ 24 - 0
lib/optionInterfaces.ts

@@ -1,4 +1,7 @@
 export interface AnswerOption {
 export interface AnswerOption {
+	/**
+	 * Function which runs before create answer to modify sdp answer message.
+	 */
 	sdpTransform?: Function;
 	sdpTransform?: Function;
 }
 }
 
 
@@ -15,13 +18,34 @@ export interface PeerJSOption {
 }
 }
 
 
 export interface PeerConnectOption {
 export interface PeerConnectOption {
+	/**
+	 * A unique label by which you want to identify this data connection.
+	 * If left unspecified, a label will be generated at random.
+	 *
+	 * Can be accessed with {@apilink DataConnection.label}
+	 */
 	label?: string;
 	label?: string;
+	/**
+	 * Metadata associated with the connection, passed in by whoever initiated the connection.
+	 *
+	 * Can be accessed with {@apilink DataConnection.metadata}.
+	 * Can be any serializable type.
+	 */
 	metadata?: any;
 	metadata?: any;
 	serialization?: string;
 	serialization?: string;
 	reliable?: boolean;
 	reliable?: boolean;
 }
 }
 
 
 export interface CallOption {
 export interface CallOption {
+	/**
+	 * Metadata associated with the connection, passed in by whoever initiated the connection.
+	 *
+	 * Can be accessed with {@apilink MediaConnection.metadata}.
+	 * Can be any serializable type.
+	 */
 	metadata?: any;
 	metadata?: any;
+	/**
+	 * Function which runs before create offer to modify sdp offer message.
+	 */
 	sdpTransform?: Function;
 	sdpTransform?: Function;
 }
 }

+ 122 - 47
lib/peer.ts

@@ -1,40 +1,87 @@
-import { EventEmitter } from "eventemitter3";
 import { util } from "./util";
 import { util } from "./util";
 import logger, { LogLevel } from "./logger";
 import logger, { LogLevel } from "./logger";
 import { Socket } from "./socket";
 import { Socket } from "./socket";
 import { MediaConnection } from "./mediaconnection";
 import { MediaConnection } from "./mediaconnection";
-import { DataConnection } from "./dataconnection";
+import type { DataConnection } from "./dataconnection/DataConnection";
 import {
 import {
 	ConnectionType,
 	ConnectionType,
 	PeerErrorType,
 	PeerErrorType,
-	SocketEventType,
 	ServerMessageType,
 	ServerMessageType,
+	SocketEventType,
 } from "./enums";
 } from "./enums";
-import { ServerMessage } from "./servermessage";
+import type { ServerMessage } from "./servermessage";
 import { API } from "./api";
 import { API } from "./api";
 import type {
 import type {
+	CallOption,
 	PeerConnectOption,
 	PeerConnectOption,
 	PeerJSOption,
 	PeerJSOption,
-	CallOption,
 } from "./optionInterfaces";
 } from "./optionInterfaces";
+import { BinaryPack } from "./dataconnection/BufferedConnection/BinaryPack";
+import { Raw } from "./dataconnection/BufferedConnection/Raw";
+import { Json } from "./dataconnection/BufferedConnection/Json";
+
+import { EventEmitterWithError, PeerError } from "./peerError";
 
 
 class PeerOptions implements PeerJSOption {
 class PeerOptions implements PeerJSOption {
-	debug?: LogLevel; // 1: Errors, 2: Warnings, 3: All logs
+	/**
+	 * Prints log messages depending on the debug level passed in.
+	 */
+	debug?: LogLevel;
+	/**
+	 * Server host. Defaults to `0.peerjs.com`.
+	 * Also accepts `'/'` to signify relative hostname.
+	 */
 	host?: string;
 	host?: string;
+	/**
+	 * Server port. Defaults to `443`.
+	 */
 	port?: number;
 	port?: number;
+	/**
+	 * The path where your self-hosted PeerServer is running. Defaults to `'/'`
+	 */
 	path?: string;
 	path?: string;
+	/**
+	 * API key for the PeerServer.
+	 * This is not used anymore.
+	 * @deprecated
+	 */
 	key?: string;
 	key?: string;
 	token?: string;
 	token?: string;
+	/**
+	 * Configuration hash passed to RTCPeerConnection.
+	 * This hash contains any custom ICE/TURN server configuration.
+	 *
+	 * Defaults to {@apilink util.defaultConfig}
+	 */
 	config?: any;
 	config?: any;
+	/**
+	 * Set to true `true` if you're using TLS.
+	 * :::danger
+	 * If possible *always use TLS*
+	 * :::
+	 */
 	secure?: boolean;
 	secure?: boolean;
 	pingInterval?: number;
 	pingInterval?: number;
 	referrerPolicy?: ReferrerPolicy;
 	referrerPolicy?: ReferrerPolicy;
 	logFunction?: (logLevel: LogLevel, ...rest: any[]) => void;
 	logFunction?: (logLevel: LogLevel, ...rest: any[]) => void;
+	serializers?: SerializerMapping;
 }
 }
 
 
-type PeerEvents = {
+export { type PeerOptions };
+
+export interface SerializerMapping {
+	[key: string]: new (
+		peerId: string,
+		provider: Peer,
+		options: any,
+	) => DataConnection;
+}
+
+export interface PeerEvents {
 	/**
 	/**
 	 * Emitted when a connection to the PeerServer is established.
 	 * Emitted when a connection to the PeerServer is established.
+	 *
+	 * You may use the peer before this is emitted, but messages to the server will be queued. <code>id</code> is the brokering ID of the peer (which was either provided in the constructor or assigned by the server).<span class='tip'>You should not wait for this event before connecting to other peers if connection speed is important.</span>
 	 */
 	 */
 	open: (id: string) => void;
 	open: (id: string) => void;
 	/**
 	/**
@@ -55,15 +102,25 @@ type PeerEvents = {
 	disconnected: (currentId: string) => void;
 	disconnected: (currentId: string) => void;
 	/**
 	/**
 	 * Errors on the peer are almost always fatal and will destroy the peer.
 	 * Errors on the peer are almost always fatal and will destroy the peer.
+	 *
+	 * Errors from the underlying socket and PeerConnections are forwarded here.
 	 */
 	 */
-	error: (error: Error) => void;
-};
+	error: (error: PeerError<`${PeerErrorType}`>) => void;
+}
 /**
 /**
  * A peer who can initiate connections with other peers.
  * A peer who can initiate connections with other peers.
  */
  */
-export class Peer extends EventEmitter<PeerEvents> {
+export class Peer extends EventEmitterWithError<PeerErrorType, PeerEvents> {
 	private static readonly DEFAULT_KEY = "peerjs";
 	private static readonly DEFAULT_KEY = "peerjs";
 
 
+	protected readonly _serializers: SerializerMapping = {
+		raw: Raw,
+		json: Json,
+		binary: BinaryPack,
+		"binary-utf8": BinaryPack,
+
+		default: BinaryPack,
+	};
 	private readonly _options: PeerOptions;
 	private readonly _options: PeerOptions;
 	private readonly _api: API;
 	private readonly _api: API;
 	private readonly _socket: Socket;
 	private readonly _socket: Socket;
@@ -82,6 +139,9 @@ export class Peer extends EventEmitter<PeerEvents> {
 	private readonly _lostMessages: Map<string, ServerMessage[]> = new Map(); // src => [list of messages]
 	private readonly _lostMessages: Map<string, ServerMessage[]> = new Map(); // src => [list of messages]
 	/**
 	/**
 	 * The brokering ID of this peer
 	 * The brokering ID of this peer
+	 *
+	 * If no ID was specified in {@apilink Peer | the constructor},
+	 * this will be `undefined` until the {@apilink PeerEvents | `open`} event is emitted.
 	 */
 	 */
 	get id() {
 	get id() {
 		return this._id;
 		return this._id;
@@ -95,6 +155,9 @@ export class Peer extends EventEmitter<PeerEvents> {
 		return this._open;
 		return this._open;
 	}
 	}
 
 
+	/**
+	 * @internal
+	 */
 	get socket() {
 	get socket() {
 		return this._socket;
 		return this._socket;
 	}
 	}
@@ -107,7 +170,7 @@ export class Peer extends EventEmitter<PeerEvents> {
 	get connections(): Object {
 	get connections(): Object {
 		const plainConnections = Object.create(null);
 		const plainConnections = Object.create(null);
 
 
-		for (let [k, v] of this._connections) {
+		for (const [k, v] of this._connections) {
 			plainConnections[k] = v;
 			plainConnections[k] = v;
 		}
 		}
 
 
@@ -142,6 +205,7 @@ export class Peer extends EventEmitter<PeerEvents> {
 	 * A peer can connect to other peers and listen for connections.
 	 * A peer can connect to other peers and listen for connections.
 	 * @param id Other peers can connect to this peer using the provided ID.
 	 * @param id Other peers can connect to this peer using the provided ID.
 	 *     If no ID is given, one will be generated by the brokering server.
 	 *     If no ID is given, one will be generated by the brokering server.
+	 * The ID must start and end with an alphanumeric character (lower or upper case character or a digit). In the middle of the ID spaces, dashes (-) and underscores (_) are allowed. Use {@apilink PeerOptions.metadata } to send identifying information.
 	 * @param options for specifying details about PeerServer
 	 * @param options for specifying details about PeerServer
 	 */
 	 */
 	constructor(id: string, options?: PeerOptions);
 	constructor(id: string, options?: PeerOptions);
@@ -168,9 +232,11 @@ export class Peer extends EventEmitter<PeerEvents> {
 			token: util.randomToken(),
 			token: util.randomToken(),
 			config: util.defaultConfig,
 			config: util.defaultConfig,
 			referrerPolicy: "strict-origin-when-cross-origin",
 			referrerPolicy: "strict-origin-when-cross-origin",
+			serializers: {},
 			...options,
 			...options,
 		};
 		};
 		this._options = options;
 		this._options = options;
+		this._serializers = { ...this._serializers, ...this.options.serializers };
 
 
 		// Detect relative URL host.
 		// Detect relative URL host.
 		if (this._options.host === "/") {
 		if (this._options.host === "/") {
@@ -337,15 +403,20 @@ export class Peer extends EventEmitter<PeerEvents> {
 					this._addConnection(peerId, connection);
 					this._addConnection(peerId, connection);
 					this.emit("call", mediaConnection);
 					this.emit("call", mediaConnection);
 				} else if (payload.type === ConnectionType.Data) {
 				} else if (payload.type === ConnectionType.Data) {
-					const dataConnection = new DataConnection(peerId, this, {
-						connectionId: connectionId,
-						_payload: payload,
-						metadata: payload.metadata,
-						label: payload.label,
-						serialization: payload.serialization,
-						reliable: payload.reliable,
-					});
+					const dataConnection = new this._serializers[payload.serialization](
+						peerId,
+						this,
+						{
+							connectionId: connectionId,
+							_payload: payload,
+							metadata: payload.metadata,
+							label: payload.label,
+							serialization: payload.serialization,
+							reliable: payload.reliable,
+						},
+					);
 					connection = dataConnection;
 					connection = dataConnection;
+
 					this._addConnection(peerId, connection);
 					this._addConnection(peerId, connection);
 					this.emit("connection", dataConnection);
 					this.emit("connection", dataConnection);
 				} else {
 				} else {
@@ -355,7 +426,7 @@ export class Peer extends EventEmitter<PeerEvents> {
 
 
 				// Find messages.
 				// Find messages.
 				const messages = this._getMessages(connectionId);
 				const messages = this._getMessages(connectionId);
-				for (let message of messages) {
+				for (const message of messages) {
 					connection.handleMessage(message);
 					connection.handleMessage(message);
 				}
 				}
 
 
@@ -395,7 +466,10 @@ export class Peer extends EventEmitter<PeerEvents> {
 		this._lostMessages.get(connectionId).push(message);
 		this._lostMessages.get(connectionId).push(message);
 	}
 	}
 
 
-	/** Retrieve messages from lost message store */
+	/**
+	 * Retrieve messages from lost message store
+	 * @internal
+	 */
 	//TODO Change it to private
 	//TODO Change it to private
 	public _getMessages(connectionId: string): ServerMessage[] {
 	public _getMessages(connectionId: string): ServerMessage[] {
 		const messages = this._lostMessages.get(connectionId);
 		const messages = this._lostMessages.get(connectionId);
@@ -410,10 +484,14 @@ export class Peer extends EventEmitter<PeerEvents> {
 
 
 	/**
 	/**
 	 * Connects to the remote peer specified by id and returns a data connection.
 	 * Connects to the remote peer specified by id and returns a data connection.
-	 * @param peer The brokering ID of the remote peer (their peer.id).
+	 * @param peer The brokering ID of the remote peer (their {@apilink Peer.id}).
 	 * @param options for specifying details about Peer Connection
 	 * @param options for specifying details about Peer Connection
 	 */
 	 */
 	connect(peer: string, options: PeerConnectOption = {}): DataConnection {
 	connect(peer: string, options: PeerConnectOption = {}): DataConnection {
+		options = {
+			serialization: "default",
+			...options,
+		};
 		if (this.disconnected) {
 		if (this.disconnected) {
 			logger.warn(
 			logger.warn(
 				"You cannot connect to a new Peer because you called " +
 				"You cannot connect to a new Peer because you called " +
@@ -428,7 +506,11 @@ export class Peer extends EventEmitter<PeerEvents> {
 			return;
 			return;
 		}
 		}
 
 
-		const dataConnection = new DataConnection(peer, this, options);
+		const dataConnection = new this._serializers[options.serialization](
+			peer,
+			this,
+			options,
+		);
 		this._addConnection(peer, dataConnection);
 		this._addConnection(peer, dataConnection);
 		return dataConnection;
 		return dataConnection;
 	}
 	}
@@ -513,7 +595,7 @@ export class Peer extends EventEmitter<PeerEvents> {
 			return null;
 			return null;
 		}
 		}
 
 
-		for (let connection of connections) {
+		for (const connection of connections) {
 			if (connection.connectionId === connectionId) {
 			if (connection.connectionId === connectionId) {
 				return connection;
 				return connection;
 			}
 			}
@@ -545,28 +627,15 @@ export class Peer extends EventEmitter<PeerEvents> {
 		}
 		}
 	}
 	}
 
 
-	/** Emits a typed error message. */
-	emitError(type: PeerErrorType, err: string | Error): void {
-		logger.error("Error:", err);
-
-		let error: Error & { type?: PeerErrorType };
-
-		if (typeof err === "string") {
-			error = new Error(err);
-		} else {
-			error = err as Error;
-		}
-
-		error.type = type;
-
-		this.emit("error", error);
-	}
-
 	/**
 	/**
 	 * Destroys the Peer: closes all active connections as well as the connection
 	 * Destroys the Peer: closes all active connections as well as the connection
-	 *  to the server.
-	 * Warning: The peer can no longer create or accept connections after being
-	 *  destroyed.
+	 * to the server.
+	 *
+	 * :::caution
+	 * This cannot be undone; the respective peer object will no longer be able
+	 * to create or receive any connections, its ID will be forfeited on the server,
+	 * and all of its data and media connections will be closed.
+	 * :::
 	 */
 	 */
 	destroy(): void {
 	destroy(): void {
 		if (this.destroyed) {
 		if (this.destroyed) {
@@ -585,7 +654,7 @@ export class Peer extends EventEmitter<PeerEvents> {
 
 
 	/** Disconnects every connection on this peer. */
 	/** Disconnects every connection on this peer. */
 	private _cleanup(): void {
 	private _cleanup(): void {
-		for (let peerId of this._connections.keys()) {
+		for (const peerId of this._connections.keys()) {
 			this._cleanupPeer(peerId);
 			this._cleanupPeer(peerId);
 			this._connections.delete(peerId);
 			this._connections.delete(peerId);
 		}
 		}
@@ -599,7 +668,7 @@ export class Peer extends EventEmitter<PeerEvents> {
 
 
 		if (!connections) return;
 		if (!connections) return;
 
 
-		for (let connection of connections) {
+		for (const connection of connections) {
 			connection.close();
 			connection.close();
 		}
 		}
 	}
 	}
@@ -630,7 +699,13 @@ export class Peer extends EventEmitter<PeerEvents> {
 		this.emit("disconnected", currentId);
 		this.emit("disconnected", currentId);
 	}
 	}
 
 
-	/** Attempts to reconnect with the same ID. */
+	/** Attempts to reconnect with the same ID.
+	 *
+	 * Only {@apilink Peer.disconnect | disconnected peers} can be reconnected.
+	 * Destroyed peers cannot be reconnected.
+	 * If the connection fails (as an example, if the peer's old ID is now taken),
+	 * the peer's existing connections will not close, but any associated errors events will fire.
+	 */
 	reconnect(): void {
 	reconnect(): void {
 		if (this.disconnected && !this.destroyed) {
 		if (this.disconnected && !this.destroyed) {
 			logger.log(
 			logger.log(

+ 44 - 0
lib/peerError.ts

@@ -0,0 +1,44 @@
+import { EventEmitter } from "eventemitter3";
+import logger from "./logger";
+
+export interface EventsWithError<ErrorType extends string> {
+	error: (error: PeerError<`${ErrorType}`>) => void;
+}
+
+export class EventEmitterWithError<
+	ErrorType extends string,
+	Events extends EventsWithError<ErrorType>,
+> extends EventEmitter<Events, never> {
+	/**
+	 * Emits a typed error message.
+	 *
+	 * @internal
+	 */
+	emitError(type: ErrorType, err: string | Error): void {
+		logger.error("Error:", err);
+
+		// @ts-ignore
+		this.emit("error", new PeerError<`${ErrorType}`>(`${type}`, err));
+	}
+}
+/**
+ * A PeerError is emitted whenever an error occurs.
+ * It always has a `.type`, which can be used to identify the error.
+ */
+export class PeerError<T extends string> extends Error {
+	/**
+	 * @internal
+	 */
+	constructor(type: T, err: Error | string) {
+		if (typeof err === "string") {
+			super(err);
+		} else {
+			super();
+			Object.assign(this, err);
+		}
+
+		this.type = type;
+	}
+
+	public type: T;
+}

+ 1 - 1
lib/servermessage.ts

@@ -1,4 +1,4 @@
-import { ServerMessageType } from "./enums";
+import type { ServerMessageType } from "./enums";
 
 
 export class ServerMessage {
 export class ServerMessage {
 	type: ServerMessageType;
 	type: ServerMessageType;

+ 1 - 1
lib/socket.ts

@@ -1,6 +1,6 @@
 import { EventEmitter } from "eventemitter3";
 import { EventEmitter } from "eventemitter3";
 import logger from "./logger";
 import logger from "./logger";
-import { SocketEventType, ServerMessageType } from "./enums";
+import { ServerMessageType, SocketEventType } from "./enums";
 import { version } from "../package.json";
 import { version } from "../package.json";
 
 
 /**
 /**

+ 49 - 54
lib/util.ts

@@ -1,14 +1,37 @@
-// Types aren’t accurate
-//@ts-ignore
-import BinaryPack from "peerjs-js-binarypack";
+import { BinaryPackChunker } from "./dataconnection/BufferedConnection/binaryPackChunker";
+import * as BinaryPack from "peerjs-js-binarypack";
 import { Supports } from "./supports";
 import { Supports } from "./supports";
+import { validateId } from "./utils/validateId";
+import { randomToken } from "./utils/randomToken";
 
 
 export interface UtilSupportsObj {
 export interface UtilSupportsObj {
+	/**
+	 * The current browser.
+	 * This property can be useful in determining whether two peers can connect.
+	 *
+	 * ```ts
+	 * if (util.browser === 'firefox') {
+	 *  // OK to peer with Firefox peers.
+	 * }
+	 * ```
+	 *
+	 * `util.browser` can currently have the values
+	 * `'firefox', 'chrome', 'safari', 'edge', 'Not a supported browser.', 'Not a browser.' (unknown WebRTC-compatible agent).
+	 */
 	browser: boolean;
 	browser: boolean;
 	webRTC: boolean;
 	webRTC: boolean;
+	/**
+	 * True if the current browser supports media streams and PeerConnection.
+	 */
 	audioVideo: boolean;
 	audioVideo: boolean;
+	/**
+	 * True if the current browser supports DataChannel and PeerConnection.
+	 */
 	data: boolean;
 	data: boolean;
 	binaryBlob: boolean;
 	binaryBlob: boolean;
+	/**
+	 * True if the current browser supports reliable DataChannels.
+	 */
 	reliable: boolean;
 	reliable: boolean;
 }
 }
 
 
@@ -27,7 +50,7 @@ const DEFAULT_CONFIG = {
 	sdpSemantics: "unified-plan",
 	sdpSemantics: "unified-plan",
 };
 };
 
 
-class Util {
+export class Util extends BinaryPackChunker {
 	noop(): void {}
 	noop(): void {}
 
 
 	readonly CLOUD_HOST = "0.peerjs.com";
 	readonly CLOUD_HOST = "0.peerjs.com";
@@ -35,7 +58,6 @@ class Util {
 
 
 	// Browsers that need chunking:
 	// Browsers that need chunking:
 	readonly chunkedBrowsers = { Chrome: 1, chrome: 1 };
 	readonly chunkedBrowsers = { Chrome: 1, chrome: 1 };
-	readonly chunkedMTU = 16300; // The original 60000 bytes setting does not work when sending data from Firefox to Chrome, which is "cut off" after 16384 bytes and delivered individually.
 
 
 	// Returns browser-agnostic default config
 	// Returns browser-agnostic default config
 	readonly defaultConfig = DEFAULT_CONFIG;
 	readonly defaultConfig = DEFAULT_CONFIG;
@@ -43,7 +65,16 @@ class Util {
 	readonly browser = Supports.getBrowser();
 	readonly browser = Supports.getBrowser();
 	readonly browserVersion = Supports.getVersion();
 	readonly browserVersion = Supports.getVersion();
 
 
-	// Lists which features are supported
+	pack = BinaryPack.pack;
+	unpack = BinaryPack.unpack;
+
+	/**
+	 * A hash of WebRTC features mapped to booleans that correspond to whether the feature is supported by the current browser.
+	 *
+	 * :::caution
+	 * Only the properties documented here are guaranteed to be present on `util.supports`
+	 * :::
+	 */
 	readonly supports = (function () {
 	readonly supports = (function () {
 		const supported: UtilSupportsObj = {
 		const supported: UtilSupportsObj = {
 			browser: Supports.isBrowserSupported(),
 			browser: Supports.isBrowserSupported(),
@@ -92,49 +123,8 @@ class Util {
 	})();
 	})();
 
 
 	// Ensure alphanumeric ids
 	// Ensure alphanumeric ids
-	validateId(id: string): boolean {
-		// Allow empty ids
-		return !id || /^[A-Za-z0-9]+(?:[ _-][A-Za-z0-9]+)*$/.test(id);
-	}
-
-	pack = BinaryPack.pack;
-	unpack = BinaryPack.unpack;
-
-	// Binary stuff
-
-	private _dataCount: number = 1;
-
-	chunk(
-		blob: Blob,
-	): { __peerData: number; n: number; total: number; data: Blob }[] {
-		const chunks = [];
-		const size = blob.size;
-		const total = Math.ceil(size / util.chunkedMTU);
-
-		let index = 0;
-		let start = 0;
-
-		while (start < size) {
-			const end = Math.min(size, start + util.chunkedMTU);
-			const b = blob.slice(start, end);
-
-			const chunk = {
-				__peerData: this._dataCount,
-				n: index,
-				data: b,
-				total,
-			};
-
-			chunks.push(chunk);
-
-			start = end;
-			index++;
-		}
-
-		this._dataCount++;
-
-		return chunks;
-	}
+	validateId = validateId;
+	randomToken = randomToken;
 
 
 	blobToArrayBuffer(
 	blobToArrayBuffer(
 		blob: Blob,
 		blob: Blob,
@@ -162,13 +152,18 @@ class Util {
 
 
 		return byteArray.buffer;
 		return byteArray.buffer;
 	}
 	}
-
-	randomToken(): string {
-		return Math.random().toString(36).slice(2);
-	}
-
 	isSecure(): boolean {
 	isSecure(): boolean {
 		return location.protocol === "https:";
 		return location.protocol === "https:";
 	}
 	}
 }
 }
+
+/**
+ * Provides a variety of helpful utilities.
+ *
+ * :::caution
+ * Only the utilities documented here are guaranteed to be present on `util`.
+ * Undocumented utilities can be removed without warning.
+ * We don't consider these to be breaking changes.
+ * :::
+ */
 export const util = new Util();
 export const util = new Util();

+ 1 - 0
lib/utils/randomToken.ts

@@ -0,0 +1 @@
+export const randomToken = () => Math.random().toString(36).slice(2);

+ 4 - 0
lib/utils/validateId.ts

@@ -0,0 +1,4 @@
+export const validateId = (id: string): boolean => {
+	// Allow empty ids
+	return !id || /^[A-Za-z0-9]+(?:[ _-][A-Za-z0-9]+)*$/.test(id);
+};

文件差异内容过多而无法显示
+ 1331 - 1572
package-lock.json


+ 65 - 29
package.json

@@ -107,9 +107,11 @@
 	"module": "dist/bundler.mjs",
 	"module": "dist/bundler.mjs",
 	"browser-minified": "dist/peerjs.min.js",
 	"browser-minified": "dist/peerjs.min.js",
 	"browser-unminified": "dist/peerjs.js",
 	"browser-unminified": "dist/peerjs.js",
+	"browser-minified-cbor": "dist/serializer.cbor.mjs",
+	"browser-minified-msgpack": "dist/serializer.msgpack.mjs",
 	"types": "dist/types.d.ts",
 	"types": "dist/types.d.ts",
 	"engines": {
 	"engines": {
-		"node": ">= 10"
+		"node": ">= 14"
 	},
 	},
 	"targets": {
 	"targets": {
 		"types": {
 		"types": {
@@ -135,7 +137,7 @@
 			"outputFormat": "global",
 			"outputFormat": "global",
 			"optimize": true,
 			"optimize": true,
 			"engines": {
 			"engines": {
-				"browsers": "cover 99%, not dead"
+				"browsers": "chrome >= 83, edge >= 83, firefox >= 80, safari >= 15"
 			},
 			},
 			"source": "lib/global.ts"
 			"source": "lib/global.ts"
 		},
 		},
@@ -144,49 +146,83 @@
 			"outputFormat": "global",
 			"outputFormat": "global",
 			"optimize": false,
 			"optimize": false,
 			"engines": {
 			"engines": {
-				"browsers": "cover 99%, not dead"
+				"browsers": "chrome >= 83, edge >= 83, firefox >= 80, safari >= 15"
 			},
 			},
 			"source": "lib/global.ts"
 			"source": "lib/global.ts"
+		},
+		"browser-minified-cbor": {
+			"context": "browser",
+			"outputFormat": "esmodule",
+			"isLibrary": true,
+			"optimize": true,
+			"engines": {
+				"browsers": "chrome >= 83, edge >= 83, firefox >= 102, safari >= 15"
+			},
+			"source": "lib/dataconnection/StreamConnection/Cbor.ts"
+		},
+		"browser-minified-msgpack": {
+			"context": "browser",
+			"outputFormat": "esmodule",
+			"isLibrary": true,
+			"optimize": true,
+			"engines": {
+				"browsers": "chrome >= 83, edge >= 83, firefox >= 102, safari >= 15"
+			},
+			"source": "lib/dataconnection/StreamConnection/MsgPack.ts"
 		}
 		}
 	},
 	},
 	"scripts": {
 	"scripts": {
 		"contributors": "git-authors-cli --print=false && prettier --write package.json && git add package.json package-lock.json && git commit -m \"chore(contributors): update and sort contributors list\"",
 		"contributors": "git-authors-cli --print=false && prettier --write package.json && git add package.json package-lock.json && git commit -m \"chore(contributors): update and sort contributors list\"",
-		"check": "tsc --noEmit",
+		"check": "tsc --noEmit && tsc -p e2e/tsconfig.json --noEmit",
 		"watch": "parcel watch",
 		"watch": "parcel watch",
 		"build": "rm -rf dist && parcel build",
 		"build": "rm -rf dist && parcel build",
 		"prepublishOnly": "npm run build",
 		"prepublishOnly": "npm run build",
-		"test": "mocha -r ts-node/register -r jsdom-global/register test/**/*.ts",
+		"test": "jest",
+		"test:watch": "jest --watch",
+		"coverage": "jest --coverage --collectCoverageFrom=\"./lib/**\"",
 		"format": "prettier --write .",
 		"format": "prettier --write .",
-		"semantic-release": "semantic-release"
+		"format:check": "prettier --check .",
+		"semantic-release": "semantic-release",
+		"e2e": "wdio run e2e/wdio.local.conf.ts",
+		"e2e:bstack": "wdio run e2e/wdio.bstack.conf.ts"
 	},
 	},
 	"devDependencies": {
 	"devDependencies": {
-		"@parcel/config-default": "^2.5.0",
-		"@parcel/packager-ts": "^2.5.0",
-		"@parcel/transformer-typescript-tsc": "^2.5.0",
-		"@parcel/transformer-typescript-types": "^2.5.0",
+		"@parcel/config-default": "^2.9.3",
+		"@parcel/packager-ts": "^2.9.3",
+		"@parcel/transformer-typescript-tsc": "^2.9.3",
+		"@parcel/transformer-typescript-types": "^2.9.3",
 		"@semantic-release/changelog": "^6.0.1",
 		"@semantic-release/changelog": "^6.0.1",
 		"@semantic-release/git": "^10.0.1",
 		"@semantic-release/git": "^10.0.1",
-		"@types/chai": "^4.3.0",
-		"@types/mocha": "^9.1.0",
-		"@types/node": "^17.0.18",
-		"chai": "^4.3.6",
-		"git-authors-cli": "^1.0.40",
-		"jsdom": "^19.0.0",
-		"jsdom-global": "^3.0.2",
-		"mocha": "^9.2.0",
-		"mock-socket": "8.0.5",
-		"parcel": "^2.5.0",
-		"parcel-transformer-tsc-sourcemaps": "^1.0.2",
-		"prettier": "^2.6.2",
-		"semantic-release": "^19.0.2",
-		"standard": "^16.0.4",
-		"ts-node": "^10.5.0",
-		"typescript": "^4.5.5"
+		"@swc/core": "^1.3.27",
+		"@swc/jest": "^0.2.24",
+		"@types/jasmine": "^4.3.4",
+		"@wdio/browserstack-service": "^8.11.2",
+		"@wdio/cli": "^8.11.2",
+		"@wdio/globals": "^8.11.2",
+		"@wdio/jasmine-framework": "^8.11.2",
+		"@wdio/local-runner": "^8.11.2",
+		"@wdio/spec-reporter": "^8.11.2",
+		"@wdio/types": "^8.10.4",
+		"http-server": "^14.1.1",
+		"jest": "^29.3.1",
+		"jest-environment-jsdom": "^29.3.1",
+		"mock-socket": "^9.0.0",
+		"parcel": "^2.9.3",
+		"prettier": "^3.0.0",
+		"semantic-release": "^21.0.0",
+		"ts-node": "^10.9.1",
+		"typescript": "^5.0.0",
+		"wdio-geckodriver-service": "^5.0.1"
 	},
 	},
 	"dependencies": {
 	"dependencies": {
-		"@swc/helpers": "^0.3.13",
+		"@msgpack/msgpack": "^2.8.0",
+		"cbor-x": "^1.5.3",
 		"eventemitter3": "^4.0.7",
 		"eventemitter3": "^4.0.7",
-		"peerjs-js-binarypack": "1.0.1",
-		"webrtc-adapter": "^7.7.1"
+		"peerjs-js-binarypack": "^2.0.0",
+		"webrtc-adapter": "^8.0.0"
+	},
+	"alias": {
+		"process": false,
+		"buffer": false
 	}
 	}
 }
 }

部分文件因为文件数量过多而无法显示