commit 5355f499577384d37a3de3dccb8f469bc3110aa0 Author: Toshit Chawda Date: Sun Jul 7 01:11:13 2024 -0700 fetch working diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..de4d1f0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +dist +node_modules diff --git a/package.json b/package.json new file mode 100644 index 0000000..77ecdde --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "@mercuryworkshop/bare-mux", + "version": "2.0.0", + "description": "", + "type": "module", + "scripts": { + "build": "rollup -c", + "prepare": "npm run build" + }, + "author": "", + "files": [ + "dist" + ], + "exports": { + ".": { + "import": "dist/client.js", + "types": "dist/client.d.ts" + } + }, + "devDependencies": { + "@rollup/plugin-inject": "^5.0.5", + "@rollup/plugin-replace": "^5.0.5", + "rollup": "^4.9.6", + "rollup-plugin-typescript2": "^0.36.0", + "tslib": "^2.6.3" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..739cef1 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,426 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@rollup/plugin-inject': + specifier: ^5.0.5 + version: 5.0.5(rollup@4.18.0) + '@rollup/plugin-replace': + specifier: ^5.0.5 + version: 5.0.7(rollup@4.18.0) + rollup: + specifier: ^4.9.6 + version: 4.18.0 + rollup-plugin-typescript2: + specifier: ^0.36.0 + version: 0.36.0(rollup@4.18.0)(typescript@5.5.3) + tslib: + specifier: ^2.6.3 + version: 2.6.3 + +packages: + + '@jridgewell/sourcemap-codec@1.4.15': + resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + + '@rollup/plugin-inject@5.0.5': + resolution: {integrity: sha512-2+DEJbNBoPROPkgTDNe8/1YXWcqxbN5DTjASVIOx8HS+pITXushyNiBV56RB08zuptzz8gT3YfkqriTBVycepg==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-replace@5.0.7': + resolution: {integrity: sha512-PqxSfuorkHz/SPpyngLyg5GCEkOcee9M1bkxiVDr41Pd61mqP1PLOoDPbpl44SB2mQGKwV/In74gqQmGITOhEQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/pluginutils@4.2.1': + resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==} + engines: {node: '>= 8.0.0'} + + '@rollup/pluginutils@5.1.0': + resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/rollup-android-arm-eabi@4.18.0': + resolution: {integrity: sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.18.0': + resolution: {integrity: sha512-avCea0RAP03lTsDhEyfy+hpfr85KfyTctMADqHVhLAF3MlIkq83CP8UfAHUssgXTYd+6er6PaAhx/QGv4L1EiA==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.18.0': + resolution: {integrity: sha512-IWfdwU7KDSm07Ty0PuA/W2JYoZ4iTj3TUQjkVsO/6U+4I1jN5lcR71ZEvRh52sDOERdnNhhHU57UITXz5jC1/w==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.18.0': + resolution: {integrity: sha512-n2LMsUz7Ynu7DoQrSQkBf8iNrjOGyPLrdSg802vk6XT3FtsgX6JbE8IHRvposskFm9SNxzkLYGSq9QdpLYpRNA==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-linux-arm-gnueabihf@4.18.0': + resolution: {integrity: sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.18.0': + resolution: {integrity: sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.18.0': + resolution: {integrity: sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.18.0': + resolution: {integrity: sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-powerpc64le-gnu@4.18.0': + resolution: {integrity: sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.18.0': + resolution: {integrity: sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.18.0': + resolution: {integrity: sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.18.0': + resolution: {integrity: sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.18.0': + resolution: {integrity: sha512-LKaqQL9osY/ir2geuLVvRRs+utWUNilzdE90TpyoX0eNqPzWjRm14oMEE+YLve4k/NAqCdPkGYDaDF5Sw+xBfg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-win32-arm64-msvc@4.18.0': + resolution: {integrity: sha512-7J6TkZQFGo9qBKH0pk2cEVSRhJbL6MtfWxth7Y5YmZs57Pi+4x6c2dStAUvaQkHQLnEQv1jzBUW43GvZW8OFqA==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.18.0': + resolution: {integrity: sha512-Txjh+IxBPbkUB9+SXZMpv+b/vnTEtFyfWZgJ6iyCmt2tdx0OF5WhFowLmnh8ENGNpfUlUZkdI//4IEmhwPieNg==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.18.0': + resolution: {integrity: sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g==} + cpu: [x64] + os: [win32] + + '@types/estree@1.0.5': + resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} + + commondir@1.0.1: + resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + find-cache-dir@3.3.2: + resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==} + engines: {node: '>=8'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + magic-string@0.30.10: + resolution: {integrity: sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==} + + make-dir@3.1.0: + resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} + engines: {node: '>=8'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + + rollup-plugin-typescript2@0.36.0: + resolution: {integrity: sha512-NB2CSQDxSe9+Oe2ahZbf+B4bh7pHwjV5L+RSYpCu7Q5ROuN94F9b6ioWwKfz3ueL3KTtmX4o2MUH2cgHDIEUsw==} + peerDependencies: + rollup: '>=1.26.3' + typescript: '>=2.4.0' + + rollup@4.18.0: + resolution: {integrity: sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.6.2: + resolution: {integrity: sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==} + engines: {node: '>=10'} + hasBin: true + + tslib@2.6.3: + resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==} + + typescript@5.5.3: + resolution: {integrity: sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==} + engines: {node: '>=14.17'} + hasBin: true + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + +snapshots: + + '@jridgewell/sourcemap-codec@1.4.15': {} + + '@rollup/plugin-inject@5.0.5(rollup@4.18.0)': + dependencies: + '@rollup/pluginutils': 5.1.0(rollup@4.18.0) + estree-walker: 2.0.2 + magic-string: 0.30.10 + optionalDependencies: + rollup: 4.18.0 + + '@rollup/plugin-replace@5.0.7(rollup@4.18.0)': + dependencies: + '@rollup/pluginutils': 5.1.0(rollup@4.18.0) + magic-string: 0.30.10 + optionalDependencies: + rollup: 4.18.0 + + '@rollup/pluginutils@4.2.1': + dependencies: + estree-walker: 2.0.2 + picomatch: 2.3.1 + + '@rollup/pluginutils@5.1.0(rollup@4.18.0)': + dependencies: + '@types/estree': 1.0.5 + estree-walker: 2.0.2 + picomatch: 2.3.1 + optionalDependencies: + rollup: 4.18.0 + + '@rollup/rollup-android-arm-eabi@4.18.0': + optional: true + + '@rollup/rollup-android-arm64@4.18.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.18.0': + optional: true + + '@rollup/rollup-darwin-x64@4.18.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.18.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.18.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.18.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.18.0': + optional: true + + '@rollup/rollup-linux-powerpc64le-gnu@4.18.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.18.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.18.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.18.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.18.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.18.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.18.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.18.0': + optional: true + + '@types/estree@1.0.5': {} + + commondir@1.0.1: {} + + estree-walker@2.0.2: {} + + find-cache-dir@3.3.2: + dependencies: + commondir: 1.0.1 + make-dir: 3.1.0 + pkg-dir: 4.2.0 + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + fs-extra@10.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + + fsevents@2.3.3: + optional: true + + graceful-fs@4.2.11: {} + + jsonfile@6.1.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + magic-string@0.30.10: + dependencies: + '@jridgewell/sourcemap-codec': 1.4.15 + + make-dir@3.1.0: + dependencies: + semver: 6.3.1 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-try@2.2.0: {} + + path-exists@4.0.0: {} + + picomatch@2.3.1: {} + + pkg-dir@4.2.0: + dependencies: + find-up: 4.1.0 + + rollup-plugin-typescript2@0.36.0(rollup@4.18.0)(typescript@5.5.3): + dependencies: + '@rollup/pluginutils': 4.2.1 + find-cache-dir: 3.3.2 + fs-extra: 10.1.0 + rollup: 4.18.0 + semver: 7.6.2 + tslib: 2.6.3 + typescript: 5.5.3 + + rollup@4.18.0: + dependencies: + '@types/estree': 1.0.5 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.18.0 + '@rollup/rollup-android-arm64': 4.18.0 + '@rollup/rollup-darwin-arm64': 4.18.0 + '@rollup/rollup-darwin-x64': 4.18.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.18.0 + '@rollup/rollup-linux-arm-musleabihf': 4.18.0 + '@rollup/rollup-linux-arm64-gnu': 4.18.0 + '@rollup/rollup-linux-arm64-musl': 4.18.0 + '@rollup/rollup-linux-powerpc64le-gnu': 4.18.0 + '@rollup/rollup-linux-riscv64-gnu': 4.18.0 + '@rollup/rollup-linux-s390x-gnu': 4.18.0 + '@rollup/rollup-linux-x64-gnu': 4.18.0 + '@rollup/rollup-linux-x64-musl': 4.18.0 + '@rollup/rollup-win32-arm64-msvc': 4.18.0 + '@rollup/rollup-win32-ia32-msvc': 4.18.0 + '@rollup/rollup-win32-x64-msvc': 4.18.0 + fsevents: 2.3.3 + + semver@6.3.1: {} + + semver@7.6.2: {} + + tslib@2.6.3: {} + + typescript@5.5.3: {} + + universalify@2.0.1: {} diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 0000000..a95e33d --- /dev/null +++ b/rollup.config.js @@ -0,0 +1,42 @@ +import inject from '@rollup/plugin-inject'; +import typescript from 'rollup-plugin-typescript2'; +import { fileURLToPath } from 'node:url'; + +const commonPlugins = () => [ + typescript(), + inject( + Object.fromEntries( + ['fetch', 'Request', 'Response', 'WebSocket', 'XMLHttpRequest'].map( + (name) => [ + name, + [fileURLToPath(new URL('./src/snapshot.ts', import.meta.url)), name], + ] + ) + ) + ), +]; + +const configs = [ + { + input: './src/worker.ts', + output: { + file: 'dist/worker.js', + format: 'iife', + sourcemap: true, + exports: 'none', + }, + plugins: commonPlugins(), + }, + { + input: './src/client.ts', + output: { + file: 'dist/client.js', + format: 'esm', + sourcemap: true, + exports: 'named', + }, + plugins: commonPlugins() + } +]; + +export default configs; diff --git a/src/baretypes.ts b/src/baretypes.ts new file mode 100644 index 0000000..e2bbe66 --- /dev/null +++ b/src/baretypes.ts @@ -0,0 +1,48 @@ +export type BareHeaders = Record; + +export type BareMeta = + { + // ??? + }; + +export type TransferrableResponse = + { + body: ReadableStream | ArrayBuffer | Blob | string, + headers: BareHeaders, + status: number, + statusText: string + } + +export interface BareTransport { + init: () => Promise; + ready: boolean; + connect: ( + url: URL, + origin: string, + protocols: string[], + requestHeaders: BareHeaders, + onopen: (protocol: string) => void, + onmessage: (data: Blob | ArrayBuffer | string) => void, + onclose: (code: number, reason: string) => void, + onerror: (error: string) => void, + ) => [(data: Blob | ArrayBuffer | string) => void, (code: number, reason: string) => void]; + + request: ( + remote: URL, + method: string, + body: BodyInit | null, + headers: BareHeaders, + signal: AbortSignal | undefined + ) => Promise; + + meta: () => BareMeta +} +export interface BareWebSocketMeta { + protocol: string; + setCookies: string[]; +} + +export type BareHTTPProtocol = 'blob:' | 'http:' | 'https:' | string; +export type BareWSProtocol = 'ws:' | 'wss:' | string; + +export const maxRedirects = 20; diff --git a/src/client.ts b/src/client.ts new file mode 100644 index 0000000..4301fd5 --- /dev/null +++ b/src/client.ts @@ -0,0 +1,205 @@ +import { BareHeaders, BareTransport, maxRedirects } from './baretypes'; +import { WorkerConnection } from './connection'; +import { WebSocketFields } from './snapshot'; + + +// get the unhooked value +const getRealReadyState = Object.getOwnPropertyDescriptor( + WebSocket.prototype, + 'readyState' +)!.get!; + +const wsProtocols = ['ws:', 'wss:']; +const statusEmpty = [101, 204, 205, 304]; +const statusRedirect = [301, 302, 303, 307, 308]; + +export type WebSocketImpl = { + new(...args: ConstructorParameters): WebSocket; +}; + +export namespace BareWebSocket { + export type GetReadyStateCallback = () => number; + export type GetSendErrorCallback = () => Error | undefined; + export type GetProtocolCallback = () => string; + export type HeadersType = BareHeaders | Headers | undefined; + export type HeadersProvider = + | BareHeaders + | (() => BareHeaders | Promise); + + export interface Options { + /** + * A provider of request headers to pass to the remote. + * Usually one of `User-Agent`, `Origin`, and `Cookie` + * Can be just the headers object or an synchronous/asynchronous function that returns the headers object + */ + headers?: BareWebSocket.HeadersProvider; + /** + * A hook executed by this function with helper arguments for hooking the readyState property. If a hook isn't provided, bare-client will hook the property on the instance. Hooking it on an instance basis is good for small projects, but ideally the class should be hooked by the user of bare-client. + */ + readyStateHook?: + | (( + socket: WebSocket, + getReadyState: BareWebSocket.GetReadyStateCallback + ) => void) + | undefined; + /** + * A hook executed by this function with helper arguments for determining if the send function should throw an error. If a hook isn't provided, bare-client will hook the function on the instance. + */ + sendErrorHook?: + | (( + socket: WebSocket, + getSendError: BareWebSocket.GetSendErrorCallback + ) => void) + | undefined; + /** + * A hook executed by this function with the URL. If a hook isn't provided, bare-client will hook the URL. + */ + urlHook?: ((socket: WebSocket, url: URL) => void) | undefined; + /** + * A hook executed by this function with a helper for getting the current fake protocol. If a hook isn't provided, bare-client will hook the protocol. + */ + protocolHook?: + | (( + socket: WebSocket, + getProtocol: BareWebSocket.GetProtocolCallback + ) => void) + | undefined; + /** + * A callback executed by this function with an array of cookies. This is called once the metadata from the server is received. + */ + setCookiesCallback?: ((setCookies: string[]) => void) | undefined; + webSocketImpl?: WebSocket; + } +} + +/** + * A Response with additional properties. + */ +export interface BareResponse extends Response { + rawResponse: Response; + rawHeaders: BareHeaders; +} +/** + * A BareResponse with additional properties. + */ +export interface BareResponseFetch extends BareResponse { + finalURL: string; +} + +export class BareMuxConnection { + worker: WorkerConnection; + + constructor(workerPath: string) { + this.worker = new WorkerConnection(workerPath); + } + + async setTransport(transport: string) { + await this.worker.sendMessage({ + type: "set", + client: transport, + }); + } +} + +export class BareClient { + worker: WorkerConnection; + + /** + * Create a BareClient. Calls to fetch and connect will wait for an implementation to be ready. + */ + constructor(workerPath?: string) { + this.worker = new WorkerConnection(workerPath); + } + + createWebSocket( + remote: string | URL, + protocols: string | string[] | undefined = [], + webSocketImpl: WebSocketImpl, + requestHeaders: BareHeaders, + arrayBufferImpl: typeof ArrayBuffer, + ): WebSocket { + throw new Error("todo"); + } + + async fetch( + url: string | URL, + init?: RequestInit + ): Promise { + // Only create an instance of Request to parse certain parameters of init such as method, headers, redirect + // But use init values whenever possible + const req = new Request(url, init); + + // try to use init.headers because it may contain capitalized headers + // furthermore, important headers on the Request class are blocked... + // we should try to preserve the capitalization due to quirks with earlier servers + const inputHeaders = init?.headers || req.headers; + + const headers: BareHeaders = + inputHeaders instanceof Headers + ? Object.fromEntries(inputHeaders as any) + : (inputHeaders as BareHeaders); + const body = req.body; + + let urlO = new URL(req.url); + + if (urlO.protocol.startsWith('blob:')) { + const response = await fetch(urlO); + const result: Response & Partial = new Response( + response.body, + response + ); + + result.rawHeaders = Object.fromEntries(response.headers as any); + result.rawResponse = response; + + return result as BareResponseFetch; + } + + for (let i = 0; ; i++) { + if ('host' in headers) headers.host = urlO.host; + else headers.Host = urlO.host; + + let resp = (await this.worker.sendMessage({ + type: "fetch", + fetch: { + remote: urlO.toString(), + method: req.method, + body: body, + headers: headers, + } + })).fetch; + + let responseobj: BareResponse & Partial = new Response( + statusEmpty.includes(resp.status) ? undefined : resp.body, { + headers: new Headers(resp.headers as HeadersInit), + status: resp.status, + statusText: resp.statusText, + }) as BareResponse; + responseobj.rawHeaders = resp.headers; + responseobj.rawResponse = new Response(resp.body); + + + responseobj.finalURL = urlO.toString(); + + const redirect = init?.redirect || req.redirect; + + if (statusRedirect.includes(responseobj.status)) { + switch (redirect) { + case 'follow': { + const location = responseobj.headers.get('location'); + if (maxRedirects > i && location !== null) { + urlO = new URL(location, urlO); + continue; + } else throw new TypeError('Failed to fetch'); + } + case 'error': + throw new TypeError('Failed to fetch'); + case 'manual': + return responseobj as BareResponseFetch; + } + } else { + return responseobj as BareResponseFetch; + } + } + } +} diff --git a/src/connection.ts b/src/connection.ts new file mode 100644 index 0000000..c74b2fc --- /dev/null +++ b/src/connection.ts @@ -0,0 +1,80 @@ +import { BareHeaders, TransferrableResponse } from "./baretypes"; + +type SWClient = { postMessage: typeof MessagePort.prototype.postMessage }; + +function tryGetPort(client: SWClient): Promise { + let channel = new MessageChannel(); + return new Promise(resolve => { + client.postMessage({ type: "getPort", port: channel.port2 }, [channel.port2]); + channel.port1.onmessage = event => { + resolve(event.data) + } + }); +} + +export type WorkerMessage = { + type: "fetch" | "set", + fetch?: { + remote: string, + method: string, + body: ReadableStream | null, + headers: BareHeaders, + }, + client?: string, +}; + +export type WorkerRequest = { + message: WorkerMessage, + port: MessagePort, +} + +export type WorkerResponse = { + type: "fetch" | "set" | "error", + fetch?: TransferrableResponse, + error?: Error, +} + +export class WorkerConnection { + port: MessagePort | Promise; + + constructor(workerPath?: string) { + // @ts-expect-error + if (self.clients) { + // @ts-expect-error + const clients: Promise = self.clients.matchAll({ type: "window", includeUncontrolled: true }); + this.port = clients.then(clients => Promise.any(clients.map((x: SWClient) => tryGetPort(x)))); + } else if (workerPath && SharedWorker) { + navigator.serviceWorker.addEventListener("message", event => { + if (event.data.type === "getPort" && event.data.port) { + const worker = new SharedWorker(workerPath); + event.data.port.postMessage(worker.port, [worker.port]); + } + }); + + const worker = new SharedWorker(workerPath, "bare-mux-worker"); + this.port = worker.port; + } else { + throw new Error("workerPath was not passed or SharedWorker does not exist and am not running in a Service Worker."); + } + } + + async sendMessage(message: WorkerMessage): Promise { + if (this.port instanceof Promise) this.port = await this.port; + let channel = new MessageChannel(); + let toTransfer: Transferable[] = [channel.port2]; + if (message.fetch && message.fetch.body) toTransfer.push(message.fetch.body); + + this.port.postMessage({ message: message, port: channel.port2 }, toTransfer); + + return await new Promise((resolve, reject) => { + channel.port1.onmessage = event => { + const message = event.data; + if (message.type === "error") { + reject(message.error); + } else { + resolve(message); + } + } + }); + } +} diff --git a/src/snapshot.ts b/src/snapshot.ts new file mode 100644 index 0000000..b466625 --- /dev/null +++ b/src/snapshot.ts @@ -0,0 +1,18 @@ +// The user likely has overwritten all networking functions after importing bare-client +// It is our responsibility to make sure components of Bare-Client are using native networking functions + +export const fetch = globalThis.fetch; +export const WebSocket = globalThis.WebSocket; +export const Request = globalThis.Request; +export const Response = globalThis.Response; +export const XMLHttpRequest = globalThis.XMLHttpRequest; + +export const WebSocketFields = { + prototype: { + send: WebSocket.prototype.send, + }, + CLOSED: WebSocket.CLOSED, + CLOSING: WebSocket.CLOSING, + CONNECTING: WebSocket.CONNECTING, + OPEN: WebSocket.OPEN, +}; diff --git a/src/worker.ts b/src/worker.ts new file mode 100644 index 0000000..3e6c99d --- /dev/null +++ b/src/worker.ts @@ -0,0 +1,42 @@ +import { BareTransport } from "./baretypes"; +import { WorkerMessage } from "./connection" + +let currentTransport: BareTransport | null = null; + +function handleConnection(port: MessagePort) { + port.onmessage = async (event: MessageEvent) => { + const port = event.data.port; + const message: WorkerMessage = event.data.message; + if (message.type === "set") { + const func = new Function(message.client); + currentTransport = await func(); + console.log("set transport to ", currentTransport); + port.postMessage({ type: "set" }); + } else if (message.type === "fetch") { + try { + if (!currentTransport) throw new Error("No BareTransport was set. Try creating a BareMuxConnection and calling set() on it."); + if (!currentTransport.ready) await currentTransport.init(); + const resp = await currentTransport.request( + new URL(message.fetch.remote), + message.fetch.method, + message.fetch.body, + message.fetch.headers, + null + ); + + if (resp.body instanceof ReadableStream || resp.body instanceof ArrayBuffer) { + port.postMessage({ type: "fetch", fetch: resp }, [resp.body]); + } else { + port.postMessage({ type: "fetch", fetch: resp }); + } + } catch (err) { + port.postMessage({ type: "error", error: err }); + } + } + } +} + +// @ts-expect-error +self.onconnect = (event: MessageEvent) => { + handleConnection(event.ports[0]) +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..567c79e --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "lib": [ + "es2021", + "DOM" + ], + "paths": { + "tslib": [ + "./node_modules/tslib/tslib.d.ts" + ] + }, + "declaration": true + } +}