diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7416329..1d8a57a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,7 +9,7 @@ jobs: timeout-minutes: 2 strategy: matrix: - node: [20.x] + node: [22.x] os: [ubuntu-latest] steps: - uses: actions/checkout@v4 diff --git a/example/index.mts b/example/index.mts index 0e536c3..3e804b5 100644 --- a/example/index.mts +++ b/example/index.mts @@ -1,4 +1,4 @@ -import { Network, FileSystem } from '@sinclair/smoke' +import { Network, FileSystem, Proxy } from '@sinclair/smoke' // ------------------------------------------------------------------ // Store Static Files @@ -27,3 +27,15 @@ Http.listen({ port: 5000 }, (request) => { const html = await Http.fetch('http://localhost:5000/index.html').then((x) => x.text()) console.log(html) + +// ------------------------------------------------------------------ +// Proxy Intecept +// ------------------------------------------------------------------ + +await Proxy.listen({ path: '/proxy' }, (request) => { + return new Response('hello from proxy') +}) + +const result = await fetch('/proxy').then((res) => res.text()) + +console.log(result) diff --git a/hammer.mjs b/hammer.mjs index ad75099..a876cf3 100644 --- a/hammer.mjs +++ b/hammer.mjs @@ -1,3 +1,5 @@ +import * as Http from 'node:http' +import * as Path from 'node:path' import * as Fs from 'node:fs' // ------------------------------------------------------------------------------- @@ -16,9 +18,20 @@ export async function format() { // Start // ------------------------------------------------------------------------------- export async function start(target = 'target/example') { + const worker = shell(`hammer watch src/proxy/service/worker.mts --dist ${target}`) const start = shell(`hammer serve example/index.html --dist ${target}`) const drift = shell(`drift wait 100 url http://localhost:5000`) - await Promise.all([start, drift]) + await Promise.all([worker, start, drift]) +} +// ------------------------------------------------------------------------------- +// Chrome +// ------------------------------------------------------------------------------- +async function chrome_warmup() { + // Chrome will not have been run in CI environment due to fresh install. It's been + // noted that initialization of Chrome seems to involve running a latent optimization + // process on the user directory which takes some time to complete. The following just + // runs chrome, waits 8 seconds and exits. + await shell(`drift wait 8000 close`) } // ------------------------------------------------------------------------------- // Test @@ -26,9 +39,22 @@ export async function start(target = 'target/example') { export async function test_serve(target = 'target/test') { await shell(`hammer serve test/index.html --dist ${target}`) } +function test_server(target = 'target/test', port = 5010) { + return Http.createServer((req, res) => { + const [path, extname] = [Path.join(target, req.url), Path.extname(req.url)] + if(Fs.existsSync(path) && Fs.statSync(path).isFile()) { + if(extname === '.js') res.writeHead(200, { 'Content-Type': 'application/javascript' }) + res.end(Fs.readFileSync(path)) + } else { + res.end('NotFound') + } + }).listen(port) +} export async function test(filter = '', target = 'target/test') { + await chrome_warmup() + const server = test_server(target, 5010) + await shell(`hammer build src/proxy/service/worker.mts --dist ${target}`) await shell(`hammer build test/index.mts --dist ${target} --platform node`) - const server = require('http').createServer((_, res) => res.end('')).listen(5010) await shell(`drift url http://localhost:5010 wait 1000 run ./${target}/index.mjs args "${filter}"`) server.close() } @@ -38,6 +64,7 @@ export async function test(filter = '', target = 'target/test') { export async function build(target = 'target/build') { await clean() await shell(`tsc -p src/tsconfig.json --outDir ${target} --declaration`) + await shell(`hammer build src/proxy/service/worker.mts --dist ${target}`) await folder(target).add('package.json') await folder(target).add('readme.md') await folder(target).add('license') @@ -52,4 +79,4 @@ export async function publish(otp, target = 'target/build') { await shell(`cd ${target} && npm publish sinclair-smoke-${version}.tgz --access=public --otp ${otp}`) await shell(`git tag ${version}`) await shell(`git push origin ${version}`) -} +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 95622ff..862e613 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@sinclair/smoke", - "version": "0.8.8", + "version": "0.8.9", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@sinclair/smoke", - "version": "0.8.8", + "version": "0.8.9", "license": "MIT", "devDependencies": { "@sinclair/drift": "^0.9.1", @@ -15,10 +15,26 @@ "typescript": "^5.4.3" } }, + "node_modules/@esbuild/android-arm": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.18.tgz", + "integrity": "sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@esbuild/linux-loong64": { - "version": "0.15.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.7.tgz", - "integrity": "sha512-IKznSJOsVUuyt7cDzzSZyqBEcZe+7WlBqTVXiF1OXP/4Nm387ToaXZ0fyLwI1iBlI/bzpxVq411QE2/Bt2XWWw==", + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.18.tgz", + "integrity": "sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ==", "cpu": [ "loong64" ], @@ -56,7 +72,23 @@ "hammer": "hammer" } }, - "node_modules/esbuild": { + "node_modules/@sinclair/hammer/node_modules/@esbuild/linux-loong64": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.7.tgz", + "integrity": "sha512-IKznSJOsVUuyt7cDzzSZyqBEcZe+7WlBqTVXiF1OXP/4Nm387ToaXZ0fyLwI1iBlI/bzpxVq411QE2/Bt2XWWw==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@sinclair/hammer/node_modules/esbuild": { "version": "0.15.7", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.7.tgz", "integrity": "sha512-7V8tzllIbAQV1M4QoE52ImKu8hT/NLGlGXkiDsbEU5PS6K8Mn09ZnYoS+dcmHxOS9CRsV4IRAMdT3I67IyUNXw==", @@ -92,7 +124,7 @@ "esbuild-windows-arm64": "0.15.7" } }, - "node_modules/esbuild-android-64": { + "node_modules/@sinclair/hammer/node_modules/esbuild-android-64": { "version": "0.15.7", "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.7.tgz", "integrity": "sha512-p7rCvdsldhxQr3YHxptf1Jcd86dlhvc3EQmQJaZzzuAxefO9PvcI0GLOa5nCWem1AJ8iMRu9w0r5TG8pHmbi9w==", @@ -108,7 +140,7 @@ "node": ">=12" } }, - "node_modules/esbuild-android-arm64": { + "node_modules/@sinclair/hammer/node_modules/esbuild-android-arm64": { "version": "0.15.7", "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.7.tgz", "integrity": "sha512-L775l9ynJT7rVqRM5vo+9w5g2ysbOCfsdLV4CWanTZ1k/9Jb3IYlQ06VCI1edhcosTYJRECQFJa3eAvkx72eyQ==", @@ -124,7 +156,7 @@ "node": ">=12" } }, - "node_modules/esbuild-darwin-64": { + "node_modules/@sinclair/hammer/node_modules/esbuild-darwin-64": { "version": "0.15.7", "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.7.tgz", "integrity": "sha512-KGPt3r1c9ww009t2xLB6Vk0YyNOXh7hbjZ3EecHoVDxgtbUlYstMPDaReimKe6eOEfyY4hBEEeTvKwPsiH5WZg==", @@ -140,7 +172,7 @@ "node": ">=12" } }, - "node_modules/esbuild-darwin-arm64": { + "node_modules/@sinclair/hammer/node_modules/esbuild-darwin-arm64": { "version": "0.15.7", "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.7.tgz", "integrity": "sha512-kBIHvtVqbSGajN88lYMnR3aIleH3ABZLLFLxwL2stiuIGAjGlQW741NxVTpUHQXUmPzxi6POqc9npkXa8AcSZQ==", @@ -156,7 +188,7 @@ "node": ">=12" } }, - "node_modules/esbuild-freebsd-64": { + "node_modules/@sinclair/hammer/node_modules/esbuild-freebsd-64": { "version": "0.15.7", "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.7.tgz", "integrity": "sha512-hESZB91qDLV5MEwNxzMxPfbjAhOmtfsr9Wnuci7pY6TtEh4UDuevmGmkUIjX/b+e/k4tcNBMf7SRQ2mdNuK/HQ==", @@ -172,7 +204,7 @@ "node": ">=12" } }, - "node_modules/esbuild-freebsd-arm64": { + "node_modules/@sinclair/hammer/node_modules/esbuild-freebsd-arm64": { "version": "0.15.7", "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.7.tgz", "integrity": "sha512-dLFR0ChH5t+b3J8w0fVKGvtwSLWCv7GYT2Y2jFGulF1L5HftQLzVGN+6pi1SivuiVSmTh28FwUhi9PwQicXI6Q==", @@ -188,7 +220,7 @@ "node": ">=12" } }, - "node_modules/esbuild-linux-32": { + "node_modules/@sinclair/hammer/node_modules/esbuild-linux-32": { "version": "0.15.7", "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.7.tgz", "integrity": "sha512-v3gT/LsONGUZcjbt2swrMjwxo32NJzk+7sAgtxhGx1+ZmOFaTRXBAi1PPfgpeo/J//Un2jIKm/I+qqeo4caJvg==", @@ -204,7 +236,7 @@ "node": ">=12" } }, - "node_modules/esbuild-linux-64": { + "node_modules/@sinclair/hammer/node_modules/esbuild-linux-64": { "version": "0.15.7", "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.7.tgz", "integrity": "sha512-LxXEfLAKwOVmm1yecpMmWERBshl+Kv5YJ/1KnyAr6HRHFW8cxOEsEfisD3sVl/RvHyW//lhYUVSuy9jGEfIRAQ==", @@ -220,7 +252,7 @@ "node": ">=12" } }, - "node_modules/esbuild-linux-arm": { + "node_modules/@sinclair/hammer/node_modules/esbuild-linux-arm": { "version": "0.15.7", "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.7.tgz", "integrity": "sha512-JKgAHtMR5f75wJTeuNQbyznZZa+pjiUHV7sRZp42UNdyXC6TiUYMW/8z8yIBAr2Fpad8hM1royZKQisqPABPvQ==", @@ -236,7 +268,7 @@ "node": ">=12" } }, - "node_modules/esbuild-linux-arm64": { + "node_modules/@sinclair/hammer/node_modules/esbuild-linux-arm64": { "version": "0.15.7", "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.7.tgz", "integrity": "sha512-P3cfhudpzWDkglutWgXcT2S7Ft7o2e3YDMrP1n0z2dlbUZghUkKCyaWw0zhp4KxEEzt/E7lmrtRu/pGWnwb9vw==", @@ -252,7 +284,7 @@ "node": ">=12" } }, - "node_modules/esbuild-linux-mips64le": { + "node_modules/@sinclair/hammer/node_modules/esbuild-linux-mips64le": { "version": "0.15.7", "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.7.tgz", "integrity": "sha512-T7XKuxl0VpeFLCJXub6U+iybiqh0kM/bWOTb4qcPyDDwNVhLUiPcGdG2/0S7F93czUZOKP57YiLV8YQewgLHKw==", @@ -268,7 +300,7 @@ "node": ">=12" } }, - "node_modules/esbuild-linux-ppc64le": { + "node_modules/@sinclair/hammer/node_modules/esbuild-linux-ppc64le": { "version": "0.15.7", "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.7.tgz", "integrity": "sha512-6mGuC19WpFN7NYbecMIJjeQgvDb5aMuvyk0PDYBJrqAEMkTwg3Z98kEKuCm6THHRnrgsdr7bp4SruSAxEM4eJw==", @@ -284,7 +316,7 @@ "node": ">=12" } }, - "node_modules/esbuild-linux-riscv64": { + "node_modules/@sinclair/hammer/node_modules/esbuild-linux-riscv64": { "version": "0.15.7", "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.7.tgz", "integrity": "sha512-uUJsezbswAYo/X7OU/P+PuL/EI9WzxsEQXDekfwpQ23uGiooxqoLFAPmXPcRAt941vjlY9jtITEEikWMBr+F/g==", @@ -300,7 +332,7 @@ "node": ">=12" } }, - "node_modules/esbuild-linux-s390x": { + "node_modules/@sinclair/hammer/node_modules/esbuild-linux-s390x": { "version": "0.15.7", "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.7.tgz", "integrity": "sha512-+tO+xOyTNMc34rXlSxK7aCwJgvQyffqEM5MMdNDEeMU3ss0S6wKvbBOQfgd5jRPblfwJ6b+bKiz0g5nABpY0QQ==", @@ -316,7 +348,7 @@ "node": ">=12" } }, - "node_modules/esbuild-netbsd-64": { + "node_modules/@sinclair/hammer/node_modules/esbuild-netbsd-64": { "version": "0.15.7", "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.7.tgz", "integrity": "sha512-yVc4Wz+Pu3cP5hzm5kIygNPrjar/v5WCSoRmIjCPWfBVJkZNb5brEGKUlf+0Y759D48BCWa0WHrWXaNy0DULTQ==", @@ -332,7 +364,7 @@ "node": ">=12" } }, - "node_modules/esbuild-openbsd-64": { + "node_modules/@sinclair/hammer/node_modules/esbuild-openbsd-64": { "version": "0.15.7", "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.7.tgz", "integrity": "sha512-GsimbwC4FSR4lN3wf8XmTQ+r8/0YSQo21rWDL0XFFhLHKlzEA4SsT1Tl8bPYu00IU6UWSJ+b3fG/8SB69rcuEQ==", @@ -348,7 +380,7 @@ "node": ">=12" } }, - "node_modules/esbuild-sunos-64": { + "node_modules/@sinclair/hammer/node_modules/esbuild-sunos-64": { "version": "0.15.7", "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.7.tgz", "integrity": "sha512-8CDI1aL/ts0mDGbWzjEOGKXnU7p3rDzggHSBtVryQzkSOsjCHRVe0iFYUuhczlxU1R3LN/E7HgUO4NXzGGP/Ag==", @@ -364,7 +396,7 @@ "node": ">=12" } }, - "node_modules/esbuild-windows-32": { + "node_modules/@sinclair/hammer/node_modules/esbuild-windows-32": { "version": "0.15.7", "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.7.tgz", "integrity": "sha512-cOnKXUEPS8EGCzRSFa1x6NQjGhGsFlVgjhqGEbLTPsA7x4RRYiy2RKoArNUU4iR2vHmzqS5Gr84MEumO/wxYKA==", @@ -380,7 +412,7 @@ "node": ">=12" } }, - "node_modules/esbuild-windows-64": { + "node_modules/@sinclair/hammer/node_modules/esbuild-windows-64": { "version": "0.15.7", "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.7.tgz", "integrity": "sha512-7MI08Ec2sTIDv+zH6StNBKO+2hGUYIT42GmFyW6MBBWWtJhTcQLinKS6ldIN1d52MXIbiJ6nXyCJ+LpL4jBm3Q==", @@ -396,7 +428,7 @@ "node": ">=12" } }, - "node_modules/esbuild-windows-arm64": { + "node_modules/@sinclair/hammer/node_modules/esbuild-windows-arm64": { "version": "0.15.7", "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.7.tgz", "integrity": "sha512-R06nmqBlWjKHddhRJYlqDd3Fabx9LFdKcjoOy08YLimwmsswlFBJV4rXzZCxz/b7ZJXvrZgj8DDv1ewE9+StMw==", @@ -412,10 +444,367 @@ "node": ">=12" } }, + "node_modules/esbuild": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.18.tgz", + "integrity": "sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.15.18", + "@esbuild/linux-loong64": "0.15.18", + "esbuild-android-64": "0.15.18", + "esbuild-android-arm64": "0.15.18", + "esbuild-darwin-64": "0.15.18", + "esbuild-darwin-arm64": "0.15.18", + "esbuild-freebsd-64": "0.15.18", + "esbuild-freebsd-arm64": "0.15.18", + "esbuild-linux-32": "0.15.18", + "esbuild-linux-64": "0.15.18", + "esbuild-linux-arm": "0.15.18", + "esbuild-linux-arm64": "0.15.18", + "esbuild-linux-mips64le": "0.15.18", + "esbuild-linux-ppc64le": "0.15.18", + "esbuild-linux-riscv64": "0.15.18", + "esbuild-linux-s390x": "0.15.18", + "esbuild-netbsd-64": "0.15.18", + "esbuild-openbsd-64": "0.15.18", + "esbuild-sunos-64": "0.15.18", + "esbuild-windows-32": "0.15.18", + "esbuild-windows-64": "0.15.18", + "esbuild-windows-arm64": "0.15.18" + } + }, + "node_modules/esbuild-android-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.18.tgz", + "integrity": "sha512-wnpt3OXRhcjfIDSZu9bnzT4/TNTDsOUvip0foZOUBG7QbSt//w3QV4FInVJxNhKc/ErhUxc5z4QjHtMi7/TbgA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-android-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.18.tgz", + "integrity": "sha512-G4xu89B8FCzav9XU8EjsXacCKSG2FT7wW9J6hOc18soEHJdtWu03L3TQDGf0geNxfLTtxENKBzMSq9LlbjS8OQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.18.tgz", + "integrity": "sha512-2WAvs95uPnVJPuYKP0Eqx+Dl/jaYseZEUUT1sjg97TJa4oBtbAKnPnl3b5M9l51/nbx7+QAEtuummJZW0sBEmg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.18.tgz", + "integrity": "sha512-tKPSxcTJ5OmNb1btVikATJ8NftlyNlc8BVNtyT/UAr62JFOhwHlnoPrhYWz09akBLHI9nElFVfWSTSRsrZiDUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.18.tgz", + "integrity": "sha512-TT3uBUxkteAjR1QbsmvSsjpKjOX6UkCstr8nMr+q7zi3NuZ1oIpa8U41Y8I8dJH2fJgdC3Dj3CXO5biLQpfdZA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.18.tgz", + "integrity": "sha512-R/oVr+X3Tkh+S0+tL41wRMbdWtpWB8hEAMsOXDumSSa6qJR89U0S/PpLXrGF7Wk/JykfpWNokERUpCeHDl47wA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-32": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.18.tgz", + "integrity": "sha512-lphF3HiCSYtaa9p1DtXndiQEeQDKPl9eN/XNoBf2amEghugNuqXNZA/ZovthNE2aa4EN43WroO0B85xVSjYkbg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.18.tgz", + "integrity": "sha512-hNSeP97IviD7oxLKFuii5sDPJ+QHeiFTFLoLm7NZQligur8poNOWGIgpQ7Qf8Balb69hptMZzyOBIPtY09GZYw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.18.tgz", + "integrity": "sha512-UH779gstRblS4aoS2qpMl3wjg7U0j+ygu3GjIeTonCcN79ZvpPee12Qun3vcdxX+37O5LFxz39XeW2I9bybMVA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.18.tgz", + "integrity": "sha512-54qr8kg/6ilcxd+0V3h9rjT4qmjc0CccMVWrjOEM/pEcUzt8X62HfBSeZfT2ECpM7104mk4yfQXkosY8Quptug==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-mips64le": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.18.tgz", + "integrity": "sha512-Mk6Ppwzzz3YbMl/ZZL2P0q1tnYqh/trYZ1VfNP47C31yT0K8t9s7Z077QrDA/guU60tGNp2GOwCQnp+DYv7bxQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-ppc64le": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.18.tgz", + "integrity": "sha512-b0XkN4pL9WUulPTa/VKHx2wLCgvIAbgwABGnKMY19WhKZPT+8BxhZdqz6EgkqCLld7X5qiCY2F/bfpUUlnFZ9w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-riscv64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.18.tgz", + "integrity": "sha512-ba2COaoF5wL6VLZWn04k+ACZjZ6NYniMSQStodFKH/Pu6RxzQqzsmjR1t9QC89VYJxBeyVPTaHuBMCejl3O/xg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-s390x": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.18.tgz", + "integrity": "sha512-VbpGuXEl5FCs1wDVp93O8UIzl3ZrglgnSQ+Hu79g7hZu6te6/YHgVJxCM2SqfIila0J3k0csfnf8VD2W7u2kzQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-netbsd-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.18.tgz", + "integrity": "sha512-98ukeCdvdX7wr1vUYQzKo4kQ0N2p27H7I11maINv73fVEXt2kyh4K4m9f35U1K43Xc2QGXlzAw0K9yoU7JUjOg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-openbsd-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.18.tgz", + "integrity": "sha512-yK5NCcH31Uae076AyQAXeJzt/vxIo9+omZRKj1pauhk3ITuADzuOx5N2fdHrAKPxN+zH3w96uFKlY7yIn490xQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-sunos-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.18.tgz", + "integrity": "sha512-On22LLFlBeLNj/YF3FT+cXcyKPEI263nflYlAhz5crxtp3yRG1Ugfr7ITyxmCmjm4vbN/dGrb/B7w7U8yJR9yw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-32": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.18.tgz", + "integrity": "sha512-o+eyLu2MjVny/nt+E0uPnBxYuJHBvho8vWsC2lV61A7wwTWC3jkN2w36jtA+yv1UgYkHRihPuQsL23hsCYGcOQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.18.tgz", + "integrity": "sha512-qinug1iTTaIIrCorAUjR0fcBk24fjzEedFYhhispP8Oc7SFvs+XeW3YpAKiKp8dRpizl4YYAhxMjlftAMJiaUw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.18.tgz", + "integrity": "sha512-q9bsYzegpZcLziq0zgUi5KqGVtfhjxGbnksaBFYmWLxeV/S1fK4OLdq2DFYnXcLMjlZw2L0jLsk1eGoB522WXQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/prettier": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", - "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", + "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" @@ -428,9 +817,9 @@ } }, "node_modules/typescript": { - "version": "5.4.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz", - "integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -463,10 +852,17 @@ } }, "dependencies": { + "@esbuild/android-arm": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.18.tgz", + "integrity": "sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw==", + "dev": true, + "optional": true + }, "@esbuild/linux-loong64": { - "version": "0.15.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.7.tgz", - "integrity": "sha512-IKznSJOsVUuyt7cDzzSZyqBEcZe+7WlBqTVXiF1OXP/4Nm387ToaXZ0fyLwI1iBlI/bzpxVq411QE2/Bt2XWWw==", + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.18.tgz", + "integrity": "sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ==", "dev": true, "optional": true }, @@ -487,187 +883,366 @@ "dev": true, "requires": { "esbuild": "0.15.7" + }, + "dependencies": { + "@esbuild/linux-loong64": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.7.tgz", + "integrity": "sha512-IKznSJOsVUuyt7cDzzSZyqBEcZe+7WlBqTVXiF1OXP/4Nm387ToaXZ0fyLwI1iBlI/bzpxVq411QE2/Bt2XWWw==", + "dev": true, + "optional": true + }, + "esbuild": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.7.tgz", + "integrity": "sha512-7V8tzllIbAQV1M4QoE52ImKu8hT/NLGlGXkiDsbEU5PS6K8Mn09ZnYoS+dcmHxOS9CRsV4IRAMdT3I67IyUNXw==", + "dev": true, + "requires": { + "@esbuild/linux-loong64": "0.15.7", + "esbuild-android-64": "0.15.7", + "esbuild-android-arm64": "0.15.7", + "esbuild-darwin-64": "0.15.7", + "esbuild-darwin-arm64": "0.15.7", + "esbuild-freebsd-64": "0.15.7", + "esbuild-freebsd-arm64": "0.15.7", + "esbuild-linux-32": "0.15.7", + "esbuild-linux-64": "0.15.7", + "esbuild-linux-arm": "0.15.7", + "esbuild-linux-arm64": "0.15.7", + "esbuild-linux-mips64le": "0.15.7", + "esbuild-linux-ppc64le": "0.15.7", + "esbuild-linux-riscv64": "0.15.7", + "esbuild-linux-s390x": "0.15.7", + "esbuild-netbsd-64": "0.15.7", + "esbuild-openbsd-64": "0.15.7", + "esbuild-sunos-64": "0.15.7", + "esbuild-windows-32": "0.15.7", + "esbuild-windows-64": "0.15.7", + "esbuild-windows-arm64": "0.15.7" + } + }, + "esbuild-android-64": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.7.tgz", + "integrity": "sha512-p7rCvdsldhxQr3YHxptf1Jcd86dlhvc3EQmQJaZzzuAxefO9PvcI0GLOa5nCWem1AJ8iMRu9w0r5TG8pHmbi9w==", + "dev": true, + "optional": true + }, + "esbuild-android-arm64": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.7.tgz", + "integrity": "sha512-L775l9ynJT7rVqRM5vo+9w5g2ysbOCfsdLV4CWanTZ1k/9Jb3IYlQ06VCI1edhcosTYJRECQFJa3eAvkx72eyQ==", + "dev": true, + "optional": true + }, + "esbuild-darwin-64": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.7.tgz", + "integrity": "sha512-KGPt3r1c9ww009t2xLB6Vk0YyNOXh7hbjZ3EecHoVDxgtbUlYstMPDaReimKe6eOEfyY4hBEEeTvKwPsiH5WZg==", + "dev": true, + "optional": true + }, + "esbuild-darwin-arm64": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.7.tgz", + "integrity": "sha512-kBIHvtVqbSGajN88lYMnR3aIleH3ABZLLFLxwL2stiuIGAjGlQW741NxVTpUHQXUmPzxi6POqc9npkXa8AcSZQ==", + "dev": true, + "optional": true + }, + "esbuild-freebsd-64": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.7.tgz", + "integrity": "sha512-hESZB91qDLV5MEwNxzMxPfbjAhOmtfsr9Wnuci7pY6TtEh4UDuevmGmkUIjX/b+e/k4tcNBMf7SRQ2mdNuK/HQ==", + "dev": true, + "optional": true + }, + "esbuild-freebsd-arm64": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.7.tgz", + "integrity": "sha512-dLFR0ChH5t+b3J8w0fVKGvtwSLWCv7GYT2Y2jFGulF1L5HftQLzVGN+6pi1SivuiVSmTh28FwUhi9PwQicXI6Q==", + "dev": true, + "optional": true + }, + "esbuild-linux-32": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.7.tgz", + "integrity": "sha512-v3gT/LsONGUZcjbt2swrMjwxo32NJzk+7sAgtxhGx1+ZmOFaTRXBAi1PPfgpeo/J//Un2jIKm/I+qqeo4caJvg==", + "dev": true, + "optional": true + }, + "esbuild-linux-64": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.7.tgz", + "integrity": "sha512-LxXEfLAKwOVmm1yecpMmWERBshl+Kv5YJ/1KnyAr6HRHFW8cxOEsEfisD3sVl/RvHyW//lhYUVSuy9jGEfIRAQ==", + "dev": true, + "optional": true + }, + "esbuild-linux-arm": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.7.tgz", + "integrity": "sha512-JKgAHtMR5f75wJTeuNQbyznZZa+pjiUHV7sRZp42UNdyXC6TiUYMW/8z8yIBAr2Fpad8hM1royZKQisqPABPvQ==", + "dev": true, + "optional": true + }, + "esbuild-linux-arm64": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.7.tgz", + "integrity": "sha512-P3cfhudpzWDkglutWgXcT2S7Ft7o2e3YDMrP1n0z2dlbUZghUkKCyaWw0zhp4KxEEzt/E7lmrtRu/pGWnwb9vw==", + "dev": true, + "optional": true + }, + "esbuild-linux-mips64le": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.7.tgz", + "integrity": "sha512-T7XKuxl0VpeFLCJXub6U+iybiqh0kM/bWOTb4qcPyDDwNVhLUiPcGdG2/0S7F93czUZOKP57YiLV8YQewgLHKw==", + "dev": true, + "optional": true + }, + "esbuild-linux-ppc64le": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.7.tgz", + "integrity": "sha512-6mGuC19WpFN7NYbecMIJjeQgvDb5aMuvyk0PDYBJrqAEMkTwg3Z98kEKuCm6THHRnrgsdr7bp4SruSAxEM4eJw==", + "dev": true, + "optional": true + }, + "esbuild-linux-riscv64": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.7.tgz", + "integrity": "sha512-uUJsezbswAYo/X7OU/P+PuL/EI9WzxsEQXDekfwpQ23uGiooxqoLFAPmXPcRAt941vjlY9jtITEEikWMBr+F/g==", + "dev": true, + "optional": true + }, + "esbuild-linux-s390x": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.7.tgz", + "integrity": "sha512-+tO+xOyTNMc34rXlSxK7aCwJgvQyffqEM5MMdNDEeMU3ss0S6wKvbBOQfgd5jRPblfwJ6b+bKiz0g5nABpY0QQ==", + "dev": true, + "optional": true + }, + "esbuild-netbsd-64": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.7.tgz", + "integrity": "sha512-yVc4Wz+Pu3cP5hzm5kIygNPrjar/v5WCSoRmIjCPWfBVJkZNb5brEGKUlf+0Y759D48BCWa0WHrWXaNy0DULTQ==", + "dev": true, + "optional": true + }, + "esbuild-openbsd-64": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.7.tgz", + "integrity": "sha512-GsimbwC4FSR4lN3wf8XmTQ+r8/0YSQo21rWDL0XFFhLHKlzEA4SsT1Tl8bPYu00IU6UWSJ+b3fG/8SB69rcuEQ==", + "dev": true, + "optional": true + }, + "esbuild-sunos-64": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.7.tgz", + "integrity": "sha512-8CDI1aL/ts0mDGbWzjEOGKXnU7p3rDzggHSBtVryQzkSOsjCHRVe0iFYUuhczlxU1R3LN/E7HgUO4NXzGGP/Ag==", + "dev": true, + "optional": true + }, + "esbuild-windows-32": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.7.tgz", + "integrity": "sha512-cOnKXUEPS8EGCzRSFa1x6NQjGhGsFlVgjhqGEbLTPsA7x4RRYiy2RKoArNUU4iR2vHmzqS5Gr84MEumO/wxYKA==", + "dev": true, + "optional": true + }, + "esbuild-windows-64": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.7.tgz", + "integrity": "sha512-7MI08Ec2sTIDv+zH6StNBKO+2hGUYIT42GmFyW6MBBWWtJhTcQLinKS6ldIN1d52MXIbiJ6nXyCJ+LpL4jBm3Q==", + "dev": true, + "optional": true + }, + "esbuild-windows-arm64": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.7.tgz", + "integrity": "sha512-R06nmqBlWjKHddhRJYlqDd3Fabx9LFdKcjoOy08YLimwmsswlFBJV4rXzZCxz/b7ZJXvrZgj8DDv1ewE9+StMw==", + "dev": true, + "optional": true + } } }, "esbuild": { - "version": "0.15.7", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.7.tgz", - "integrity": "sha512-7V8tzllIbAQV1M4QoE52ImKu8hT/NLGlGXkiDsbEU5PS6K8Mn09ZnYoS+dcmHxOS9CRsV4IRAMdT3I67IyUNXw==", + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.18.tgz", + "integrity": "sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q==", "dev": true, "requires": { - "@esbuild/linux-loong64": "0.15.7", - "esbuild-android-64": "0.15.7", - "esbuild-android-arm64": "0.15.7", - "esbuild-darwin-64": "0.15.7", - "esbuild-darwin-arm64": "0.15.7", - "esbuild-freebsd-64": "0.15.7", - "esbuild-freebsd-arm64": "0.15.7", - "esbuild-linux-32": "0.15.7", - "esbuild-linux-64": "0.15.7", - "esbuild-linux-arm": "0.15.7", - "esbuild-linux-arm64": "0.15.7", - "esbuild-linux-mips64le": "0.15.7", - "esbuild-linux-ppc64le": "0.15.7", - "esbuild-linux-riscv64": "0.15.7", - "esbuild-linux-s390x": "0.15.7", - "esbuild-netbsd-64": "0.15.7", - "esbuild-openbsd-64": "0.15.7", - "esbuild-sunos-64": "0.15.7", - "esbuild-windows-32": "0.15.7", - "esbuild-windows-64": "0.15.7", - "esbuild-windows-arm64": "0.15.7" + "@esbuild/android-arm": "0.15.18", + "@esbuild/linux-loong64": "0.15.18", + "esbuild-android-64": "0.15.18", + "esbuild-android-arm64": "0.15.18", + "esbuild-darwin-64": "0.15.18", + "esbuild-darwin-arm64": "0.15.18", + "esbuild-freebsd-64": "0.15.18", + "esbuild-freebsd-arm64": "0.15.18", + "esbuild-linux-32": "0.15.18", + "esbuild-linux-64": "0.15.18", + "esbuild-linux-arm": "0.15.18", + "esbuild-linux-arm64": "0.15.18", + "esbuild-linux-mips64le": "0.15.18", + "esbuild-linux-ppc64le": "0.15.18", + "esbuild-linux-riscv64": "0.15.18", + "esbuild-linux-s390x": "0.15.18", + "esbuild-netbsd-64": "0.15.18", + "esbuild-openbsd-64": "0.15.18", + "esbuild-sunos-64": "0.15.18", + "esbuild-windows-32": "0.15.18", + "esbuild-windows-64": "0.15.18", + "esbuild-windows-arm64": "0.15.18" } }, "esbuild-android-64": { - "version": "0.15.7", - "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.7.tgz", - "integrity": "sha512-p7rCvdsldhxQr3YHxptf1Jcd86dlhvc3EQmQJaZzzuAxefO9PvcI0GLOa5nCWem1AJ8iMRu9w0r5TG8pHmbi9w==", + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.18.tgz", + "integrity": "sha512-wnpt3OXRhcjfIDSZu9bnzT4/TNTDsOUvip0foZOUBG7QbSt//w3QV4FInVJxNhKc/ErhUxc5z4QjHtMi7/TbgA==", "dev": true, "optional": true }, "esbuild-android-arm64": { - "version": "0.15.7", - "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.7.tgz", - "integrity": "sha512-L775l9ynJT7rVqRM5vo+9w5g2ysbOCfsdLV4CWanTZ1k/9Jb3IYlQ06VCI1edhcosTYJRECQFJa3eAvkx72eyQ==", + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.18.tgz", + "integrity": "sha512-G4xu89B8FCzav9XU8EjsXacCKSG2FT7wW9J6hOc18soEHJdtWu03L3TQDGf0geNxfLTtxENKBzMSq9LlbjS8OQ==", "dev": true, "optional": true }, "esbuild-darwin-64": { - "version": "0.15.7", - "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.7.tgz", - "integrity": "sha512-KGPt3r1c9ww009t2xLB6Vk0YyNOXh7hbjZ3EecHoVDxgtbUlYstMPDaReimKe6eOEfyY4hBEEeTvKwPsiH5WZg==", + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.18.tgz", + "integrity": "sha512-2WAvs95uPnVJPuYKP0Eqx+Dl/jaYseZEUUT1sjg97TJa4oBtbAKnPnl3b5M9l51/nbx7+QAEtuummJZW0sBEmg==", "dev": true, "optional": true }, "esbuild-darwin-arm64": { - "version": "0.15.7", - "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.7.tgz", - "integrity": "sha512-kBIHvtVqbSGajN88lYMnR3aIleH3ABZLLFLxwL2stiuIGAjGlQW741NxVTpUHQXUmPzxi6POqc9npkXa8AcSZQ==", + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.18.tgz", + "integrity": "sha512-tKPSxcTJ5OmNb1btVikATJ8NftlyNlc8BVNtyT/UAr62JFOhwHlnoPrhYWz09akBLHI9nElFVfWSTSRsrZiDUA==", "dev": true, "optional": true }, "esbuild-freebsd-64": { - "version": "0.15.7", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.7.tgz", - "integrity": "sha512-hESZB91qDLV5MEwNxzMxPfbjAhOmtfsr9Wnuci7pY6TtEh4UDuevmGmkUIjX/b+e/k4tcNBMf7SRQ2mdNuK/HQ==", + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.18.tgz", + "integrity": "sha512-TT3uBUxkteAjR1QbsmvSsjpKjOX6UkCstr8nMr+q7zi3NuZ1oIpa8U41Y8I8dJH2fJgdC3Dj3CXO5biLQpfdZA==", "dev": true, "optional": true }, "esbuild-freebsd-arm64": { - "version": "0.15.7", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.7.tgz", - "integrity": "sha512-dLFR0ChH5t+b3J8w0fVKGvtwSLWCv7GYT2Y2jFGulF1L5HftQLzVGN+6pi1SivuiVSmTh28FwUhi9PwQicXI6Q==", + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.18.tgz", + "integrity": "sha512-R/oVr+X3Tkh+S0+tL41wRMbdWtpWB8hEAMsOXDumSSa6qJR89U0S/PpLXrGF7Wk/JykfpWNokERUpCeHDl47wA==", "dev": true, "optional": true }, "esbuild-linux-32": { - "version": "0.15.7", - "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.7.tgz", - "integrity": "sha512-v3gT/LsONGUZcjbt2swrMjwxo32NJzk+7sAgtxhGx1+ZmOFaTRXBAi1PPfgpeo/J//Un2jIKm/I+qqeo4caJvg==", + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.18.tgz", + "integrity": "sha512-lphF3HiCSYtaa9p1DtXndiQEeQDKPl9eN/XNoBf2amEghugNuqXNZA/ZovthNE2aa4EN43WroO0B85xVSjYkbg==", "dev": true, "optional": true }, "esbuild-linux-64": { - "version": "0.15.7", - "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.7.tgz", - "integrity": "sha512-LxXEfLAKwOVmm1yecpMmWERBshl+Kv5YJ/1KnyAr6HRHFW8cxOEsEfisD3sVl/RvHyW//lhYUVSuy9jGEfIRAQ==", + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.18.tgz", + "integrity": "sha512-hNSeP97IviD7oxLKFuii5sDPJ+QHeiFTFLoLm7NZQligur8poNOWGIgpQ7Qf8Balb69hptMZzyOBIPtY09GZYw==", "dev": true, "optional": true }, "esbuild-linux-arm": { - "version": "0.15.7", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.7.tgz", - "integrity": "sha512-JKgAHtMR5f75wJTeuNQbyznZZa+pjiUHV7sRZp42UNdyXC6TiUYMW/8z8yIBAr2Fpad8hM1royZKQisqPABPvQ==", + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.18.tgz", + "integrity": "sha512-UH779gstRblS4aoS2qpMl3wjg7U0j+ygu3GjIeTonCcN79ZvpPee12Qun3vcdxX+37O5LFxz39XeW2I9bybMVA==", "dev": true, "optional": true }, "esbuild-linux-arm64": { - "version": "0.15.7", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.7.tgz", - "integrity": "sha512-P3cfhudpzWDkglutWgXcT2S7Ft7o2e3YDMrP1n0z2dlbUZghUkKCyaWw0zhp4KxEEzt/E7lmrtRu/pGWnwb9vw==", + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.18.tgz", + "integrity": "sha512-54qr8kg/6ilcxd+0V3h9rjT4qmjc0CccMVWrjOEM/pEcUzt8X62HfBSeZfT2ECpM7104mk4yfQXkosY8Quptug==", "dev": true, "optional": true }, "esbuild-linux-mips64le": { - "version": "0.15.7", - "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.7.tgz", - "integrity": "sha512-T7XKuxl0VpeFLCJXub6U+iybiqh0kM/bWOTb4qcPyDDwNVhLUiPcGdG2/0S7F93czUZOKP57YiLV8YQewgLHKw==", + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.18.tgz", + "integrity": "sha512-Mk6Ppwzzz3YbMl/ZZL2P0q1tnYqh/trYZ1VfNP47C31yT0K8t9s7Z077QrDA/guU60tGNp2GOwCQnp+DYv7bxQ==", "dev": true, "optional": true }, "esbuild-linux-ppc64le": { - "version": "0.15.7", - "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.7.tgz", - "integrity": "sha512-6mGuC19WpFN7NYbecMIJjeQgvDb5aMuvyk0PDYBJrqAEMkTwg3Z98kEKuCm6THHRnrgsdr7bp4SruSAxEM4eJw==", + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.18.tgz", + "integrity": "sha512-b0XkN4pL9WUulPTa/VKHx2wLCgvIAbgwABGnKMY19WhKZPT+8BxhZdqz6EgkqCLld7X5qiCY2F/bfpUUlnFZ9w==", "dev": true, "optional": true }, "esbuild-linux-riscv64": { - "version": "0.15.7", - "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.7.tgz", - "integrity": "sha512-uUJsezbswAYo/X7OU/P+PuL/EI9WzxsEQXDekfwpQ23uGiooxqoLFAPmXPcRAt941vjlY9jtITEEikWMBr+F/g==", + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.18.tgz", + "integrity": "sha512-ba2COaoF5wL6VLZWn04k+ACZjZ6NYniMSQStodFKH/Pu6RxzQqzsmjR1t9QC89VYJxBeyVPTaHuBMCejl3O/xg==", "dev": true, "optional": true }, "esbuild-linux-s390x": { - "version": "0.15.7", - "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.7.tgz", - "integrity": "sha512-+tO+xOyTNMc34rXlSxK7aCwJgvQyffqEM5MMdNDEeMU3ss0S6wKvbBOQfgd5jRPblfwJ6b+bKiz0g5nABpY0QQ==", + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.18.tgz", + "integrity": "sha512-VbpGuXEl5FCs1wDVp93O8UIzl3ZrglgnSQ+Hu79g7hZu6te6/YHgVJxCM2SqfIila0J3k0csfnf8VD2W7u2kzQ==", "dev": true, "optional": true }, "esbuild-netbsd-64": { - "version": "0.15.7", - "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.7.tgz", - "integrity": "sha512-yVc4Wz+Pu3cP5hzm5kIygNPrjar/v5WCSoRmIjCPWfBVJkZNb5brEGKUlf+0Y759D48BCWa0WHrWXaNy0DULTQ==", + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.18.tgz", + "integrity": "sha512-98ukeCdvdX7wr1vUYQzKo4kQ0N2p27H7I11maINv73fVEXt2kyh4K4m9f35U1K43Xc2QGXlzAw0K9yoU7JUjOg==", "dev": true, "optional": true }, "esbuild-openbsd-64": { - "version": "0.15.7", - "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.7.tgz", - "integrity": "sha512-GsimbwC4FSR4lN3wf8XmTQ+r8/0YSQo21rWDL0XFFhLHKlzEA4SsT1Tl8bPYu00IU6UWSJ+b3fG/8SB69rcuEQ==", + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.18.tgz", + "integrity": "sha512-yK5NCcH31Uae076AyQAXeJzt/vxIo9+omZRKj1pauhk3ITuADzuOx5N2fdHrAKPxN+zH3w96uFKlY7yIn490xQ==", "dev": true, "optional": true }, "esbuild-sunos-64": { - "version": "0.15.7", - "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.7.tgz", - "integrity": "sha512-8CDI1aL/ts0mDGbWzjEOGKXnU7p3rDzggHSBtVryQzkSOsjCHRVe0iFYUuhczlxU1R3LN/E7HgUO4NXzGGP/Ag==", + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.18.tgz", + "integrity": "sha512-On22LLFlBeLNj/YF3FT+cXcyKPEI263nflYlAhz5crxtp3yRG1Ugfr7ITyxmCmjm4vbN/dGrb/B7w7U8yJR9yw==", "dev": true, "optional": true }, "esbuild-windows-32": { - "version": "0.15.7", - "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.7.tgz", - "integrity": "sha512-cOnKXUEPS8EGCzRSFa1x6NQjGhGsFlVgjhqGEbLTPsA7x4RRYiy2RKoArNUU4iR2vHmzqS5Gr84MEumO/wxYKA==", + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.18.tgz", + "integrity": "sha512-o+eyLu2MjVny/nt+E0uPnBxYuJHBvho8vWsC2lV61A7wwTWC3jkN2w36jtA+yv1UgYkHRihPuQsL23hsCYGcOQ==", "dev": true, "optional": true }, "esbuild-windows-64": { - "version": "0.15.7", - "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.7.tgz", - "integrity": "sha512-7MI08Ec2sTIDv+zH6StNBKO+2hGUYIT42GmFyW6MBBWWtJhTcQLinKS6ldIN1d52MXIbiJ6nXyCJ+LpL4jBm3Q==", + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.18.tgz", + "integrity": "sha512-qinug1iTTaIIrCorAUjR0fcBk24fjzEedFYhhispP8Oc7SFvs+XeW3YpAKiKp8dRpizl4YYAhxMjlftAMJiaUw==", "dev": true, "optional": true }, "esbuild-windows-arm64": { - "version": "0.15.7", - "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.7.tgz", - "integrity": "sha512-R06nmqBlWjKHddhRJYlqDd3Fabx9LFdKcjoOy08YLimwmsswlFBJV4rXzZCxz/b7ZJXvrZgj8DDv1ewE9+StMw==", + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.18.tgz", + "integrity": "sha512-q9bsYzegpZcLziq0zgUi5KqGVtfhjxGbnksaBFYmWLxeV/S1fK4OLdq2DFYnXcLMjlZw2L0jLsk1eGoB522WXQ==", "dev": true, "optional": true }, "prettier": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", - "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", + "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", "dev": true }, "typescript": { - "version": "5.4.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz", - "integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", "dev": true }, "ws": { diff --git a/package.json b/package.json index 864503d..0678cd6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@sinclair/smoke", - "version": "0.8.8", + "version": "0.8.9", "description": "Run Web Servers in Web Browsers over WebRTC", "type": "module", "main": "./index.mjs", diff --git a/readme.md b/readme.md index 1b11dd3..fce6a14 100644 --- a/readme.md +++ b/readme.md @@ -53,9 +53,9 @@ $ npm install @sinclair/smoke ## Overview -Smoke is an experimental browser networking and storage framework that provides Http, Tcp and WebSocket emulation over WebRTC and large file storage via IndexedDB. It is built as a foundation for developing peer to peer web services in the browser with each browser accessible via an application controlled virtual network. +Smoke is an experimental browser networking and storage framework that provides Http, Tcp, and WebSocket emulation over WebRTC, as well as large file storage using IndexedDB. It is designed as a foundation for developing peer-to-peer web services directly in the browser, with each browser accessible through an application-controlled virtual network. -Smoke reshapes WebRTC into WinterCG compatible interfaces enabling traditional web server applications to be made portable between server and browser environments. It is developed in support of alternative software architectures where user centric services can be moved away from the cloud and run peer to peer in the browser. +Smoke reshapes WebRTC into standard Http compatible interfaces enabling traditional web server applications to be made portable between server and browser environments. It is developed in support of alternative software architectures where user centric services can be moved away from the cloud and run peer to peer in the browser. Licence MIT @@ -78,6 +78,9 @@ Licence MIT - [Audio](#Media-Audio) - [Video](#Media-Video) - [Pattern](#Media-Pattern) +- [Proxy](#Proxy) + - [Listen](#Proxy-Listen) + - [Worker](#Proxy-Worker) - [FileSystem](#FileSystem) - [Open](#FileSystem-Open) - [Stat](#FileSystem-Stat) @@ -297,6 +300,41 @@ const pattern = Media.pattern() const sender = Media.send({ port: 5000 }, pattern.mediastream) ``` + +## Proxy + +A Smoke Proxy enables a web page to intercept outbound HTTP requests. It uses a Service Worker to redirect these requests back to the calling page, allowing the page to handle its own requests. This functionality supports both fetch requests and referenced assets embedded in HTML. Currently, the Smoke Proxy is supported only in Chromium-based browsers. + + +### Listen + +Use the listen function to intercept Http requests made to a given path. + +```typescript +import { Proxy } from '@sinclair/smoke' + +Proxy.listen({ path: '/some-path', workerPath: 'worker.js' }, request => { + + return new Response('hello world') +}) + +// ... + +const result = await fetch('/some-path/foo').then(res => res.text()) + +``` + + +### Worker + +The Proxy requires a Service Worker script to be loaded at the root path of the website. Smoke provides a prebuilt worker script that you can copy into the website's root directory. + +```bash +# Copy this JavaScript file to the website root. + +node_modules/@sinclair/smoke/worker.js +``` + ## FileSystem diff --git a/src/http/websocket/protocol.mts b/src/http/websocket/protocol.mts index 691cad8..94bd22c 100644 --- a/src/http/websocket/protocol.mts +++ b/src/http/websocket/protocol.mts @@ -77,11 +77,11 @@ export function decodeAny(value: Uint8Array): [MessageType, ArrayBuffer] { case MessageType.MessageData: return [MessageType.MessageData, data.buffer] case MessageType.MessageText: - return [MessageType.MessageText, data] + return [MessageType.MessageText, data.buffer] case MessageType.Ping: - return [MessageType.Ping, data] + return [MessageType.Ping, data.buffer] case MessageType.Pong: - return [MessageType.Pong, data] + return [MessageType.Pong, data.buffer] default: throw Error('Unknown protocol type') } diff --git a/src/index.mts b/src/index.mts index ade2e4b..c71c0b8 100644 --- a/src/index.mts +++ b/src/index.mts @@ -42,6 +42,7 @@ export * as Hubs from './hubs/index.mjs' export * as IndexedDb from './indexeddb/index.mjs' export * as Os from './os/index.mjs' export * as Path from './path/index.mjs' +export * as Proxy from './proxy/index.mjs' export * as Stream from './stream/index.mjs' export * as Url from './url/index.mjs' diff --git a/src/media/receiver.mts b/src/media/receiver.mts index 788e649..7ad5621 100644 --- a/src/media/receiver.mts +++ b/src/media/receiver.mts @@ -107,7 +107,6 @@ export class MediaReceiver { const data = JSON.parse(Buffer.decode(buffer)) return new MessageEvent('message', { data }) } catch { - console.log(Buffer.decode(buffer)) return null } } diff --git a/src/stream/iterator/iterator.mts b/src/proxy/index.mts similarity index 71% rename from src/stream/iterator/iterator.mts rename to src/proxy/index.mts index 6f0ed4f..0743546 100644 --- a/src/stream/iterator/iterator.mts +++ b/src/proxy/index.mts @@ -26,21 +26,4 @@ THE SOFTWARE. ---------------------------------------------------------------------------*/ -import { Read, ReadSync } from '../read.mjs' - -/** Returns an asynchronous iterator for the given Read */ -export async function* toAsyncIterator(read: Read): AsyncIterableIterator { - while (true) { - const next = await read.read() - if (next === null) return - yield next - } -} -/** Returns an iterator for the given ReadSync */ -export function* toIterator(read: ReadSync): IterableIterator { - while (true) { - const next = read.read() - if (next === null) return - yield next - } -} +export * from './proxy/index.mjs' diff --git a/src/stream/iterator/index.mts b/src/proxy/protocol/index.mts similarity index 97% rename from src/stream/iterator/index.mts rename to src/proxy/protocol/index.mts index 4ffed4c..1046e51 100644 --- a/src/stream/iterator/index.mts +++ b/src/proxy/protocol/index.mts @@ -26,4 +26,4 @@ THE SOFTWARE. ---------------------------------------------------------------------------*/ -export * from './iterator.mjs' +export * from './protocol.mjs' diff --git a/src/proxy/protocol/protocol.mts b/src/proxy/protocol/protocol.mts new file mode 100644 index 0000000..313815d --- /dev/null +++ b/src/proxy/protocol/protocol.mts @@ -0,0 +1,168 @@ +/*-------------------------------------------------------------------------- + +@sinclair/smoke + +The MIT License (MIT) + +Copyright (c) 2024 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +// ------------------------------------------------------------------ +// Guard +// ------------------------------------------------------------------ +function isEqual(value: unknown, equal: Value): value is Value { + return value === equal +} +function isObject(value: unknown): value is Record { + return typeof value === 'object' && value !== null +} +function isString(value: unknown): value is string { + return typeof value === 'string' +} +function isNumber(value: unknown): value is number { + return typeof value === 'number' +} +function isUint8Array(value: unknown): value is Uint8Array { + return value instanceof Uint8Array +} +// ------------------------------------------------------------------ +// RegisterRequest +// ------------------------------------------------------------------ +export interface RegisterRequest { + type: 'RegisterRequest' + path: string +} +export function isRegisterRequest(value: unknown): value is RegisterRequest { + return isObject(value) && value.type === 'RegisterRequest' && isString(value.path) +} +export function assertRegisterRequest(value: unknown): asserts value is RegisterRequest { + if (!isRegisterRequest(value)) throw new Error('Expected RegisterRequest') +} +// ------------------------------------------------------------------ +// RegisterResponse +// ------------------------------------------------------------------ +export interface RegisterResponse { + type: 'RegisterResponse' + clientId: string +} +export function isRegisterResponse(value: unknown): value is RegisterResponse { + return isObject(value) && value.type === 'RegisterResponse' && isString(value.clientId) +} +export function assertRegisterResponse(value: unknown): asserts value is RegisterResponse { + if (!isRegisterResponse(value)) throw new Error('Expected RegisterResponse') +} +// ------------------------------------------------------------------ +// RequestInit +// ------------------------------------------------------------------ +export interface RequestInit { + type: 'RequestInit' + requestId: number + url: string + init: globalThis.RequestInit +} +export function isRequestInit(value: unknown): value is RequestInit { + return isObject(value) && isEqual(value.type, 'RequestInit') && isString(value.url) && isNumber(value.requestId) && isObject(value.init) +} +// ------------------------------------------------------------------ +// RequestBody +// ------------------------------------------------------------------ +export interface RequestData { + type: 'RequestData' + requestId: number + data: Uint8Array +} +export function isRequestData(value: unknown): value is RequestData { + return isObject(value) && isEqual(value.type, 'RequestData') && isNumber(value.requestId) && isUint8Array(value.data) +} +// ------------------------------------------------------------------ +// RequestEnd +// ------------------------------------------------------------------ +export interface RequestEnd { + type: 'RequestEnd' + requestId: number +} +export function isRequestEnd(value: unknown): value is RequestEnd { + return isObject(value) && isEqual(value.type, 'RequestEnd') && isNumber(value.requestId) +} +// ------------------------------------------------------------------ +// ResponseInit +// ------------------------------------------------------------------ +export interface ResponseInit { + type: 'ResponseInit' + requestId: number + init: globalThis.ResponseInit +} +export function isResponseInit(value: unknown): value is ResponseInit { + return isObject(value) && isEqual(value.type, 'ResponseInit') && isNumber(value.requestId) && isObject(value.init) +} +// ------------------------------------------------------------------ +// ResponseData +// ------------------------------------------------------------------ +export interface ResponseData { + type: 'ResponseData' + requestId: number + data: Uint8Array +} +export function isResponseData(value: unknown): value is ResponseData { + return isObject(value) && isEqual(value.type, 'ResponseData') && isNumber(value.requestId) && isUint8Array(value.data) +} +// ------------------------------------------------------------------ +// ResponseEnd +// ------------------------------------------------------------------ +export interface ResponseEnd { + type: 'ResponseEnd' + requestId: number +} +export function isResponseEnd(value: unknown): value is ResponseEnd { + return isObject(value) && isEqual(value.type, 'ResponseEnd') && isNumber(value.requestId) +} + +// ------------------------------------------------------------------- +// RequestInitFromRequest +// ------------------------------------------------------------------- +export function requestInitFromRequest(request: Request): globalThis.RequestInit { + return { + cache: request.cache, + credentials: request.credentials, + headers: Object.fromEntries(request.headers.entries()), + integrity: request.integrity, + keepalive: request.keepalive, + method: request.method, + mode: request.mode, + priority: undefined, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + signal: undefined, + window: undefined, + } +} +// ------------------------------------------------------------------- +// ResponseInitFromResponse +// ------------------------------------------------------------------- +export function responseInitFromResponse(response: Response): globalThis.ResponseInit { + return { + headers: Object.fromEntries(response.headers.entries()), + status: response.status, + statusText: response.statusText, + } +} diff --git a/src/proxy/proxy/index.mts b/src/proxy/proxy/index.mts new file mode 100644 index 0000000..50987e4 --- /dev/null +++ b/src/proxy/proxy/index.mts @@ -0,0 +1,30 @@ +/*-------------------------------------------------------------------------- + +@sinclair/smoke + +The MIT License (MIT) + +Copyright (c) 2024 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './listener.mjs' +export * from './proxy.mjs' diff --git a/src/proxy/proxy/listener.mts b/src/proxy/proxy/listener.mts new file mode 100644 index 0000000..8e6cfd8 --- /dev/null +++ b/src/proxy/proxy/listener.mts @@ -0,0 +1,149 @@ +/*-------------------------------------------------------------------------- + +@sinclair/smoke + +The MIT License (MIT) + +Copyright (c) 2024 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import * as Channel from '../../channel/index.mjs' +import * as Protocol from '../protocol/index.mjs' + +// ------------------------------------------------------------------ +// ProxyListener +// ------------------------------------------------------------------ +export interface ListenCallback { + (request: Request): Promise | Response +} +export class ProxyListener { + readonly #worker: ServiceWorker + readonly #port: MessagePort + readonly #clientId: string + readonly #callback: ListenCallback + readonly #receivers: Map> + constructor(worker: ServiceWorker, port: MessagePort, clientId: string, callback: ListenCallback) { + this.#receivers = new Map>() + this.#worker = worker + this.#port = port + this.#clientId = clientId + this.#callback = callback + this.#port.addEventListener('message', (event) => this.#onMessage(event)) + } + // ---------------------------------------------------------------- + // Properties + // ---------------------------------------------------------------- + public get serviceWorker(): ServiceWorker { + return this.#worker + } + public get clientId(): string { + return this.#clientId + } + // ---------------------------------------------------------------- + // Body + // ---------------------------------------------------------------- + #isHasBody(message: Protocol.RequestInit) { + return message.init.method !== 'GET' && message.init.method !== 'HEAD' + } + // prettier-ignore + #createReadableStreamBody(message: Protocol.RequestInit): ReadableStream { + const receiver = new Channel.Channel() + this.#receivers.set(message.requestId, receiver) + return new ReadableStream({ + pull: async (controller) => { + const next = await receiver.next() + return next !== null + ? controller.enqueue(next) + : controller.close() + } + }) + } + #createBody(message: Protocol.RequestInit): ReadableStream | null { + if (!this.#isHasBody(message)) return null + return this.#createReadableStreamBody(message) + } + #createHeaders(message: Protocol.RequestInit): Headers { + return new Headers(message.init.headers) + } + // ---------------------------------------------------------------- + // Send + // ---------------------------------------------------------------- + #send(message: Message) { + this.#port.postMessage(message) + } + async #sendResponse(message: Protocol.RequestInit, response: globalThis.Response) { + const { requestId } = message + const init = Protocol.responseInitFromResponse(response) + this.#send({ type: 'ResponseInit', requestId, init }) + if (response.body) { + const reader = response.body.getReader() + while (true) { + const { value: data, done } = await reader.read() + if (done) break + this.#send({ type: 'ResponseData', requestId, data }) + } + } + this.#send({ type: 'ResponseEnd', requestId }) + } + // ---------------------------------------------------------------- + // Events + // ---------------------------------------------------------------- + async #onRequestInit(message: Protocol.RequestInit) { + const body = this.#createBody(message) + const headers = this.#createHeaders(message) + const requestInit = { ...message.init, headers, body, duplex: 'half' } as globalThis.RequestInit + const request = new Request(message.url, requestInit) + try { + const response = await this.#callback(request) + await this.#sendResponse(message, response) + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Error' + await this.#sendResponse(message, new Response(errorMessage, { status: 500 })) + } + } + async #onRequestData(message: Protocol.RequestData) { + const receiver = this.#receivers.get(message.requestId) + if (receiver === undefined) return + receiver.send(message.data) + } + async #onRequestEnd(message: Protocol.RequestEnd) { + const receiver = this.#receivers.get(message.requestId) + if (receiver === undefined) return + this.#receivers.delete(message.requestId) + receiver.end() + } + #onMessage(event: MessageEvent) { + switch (true) { + case Protocol.isRequestInit(event.data): + return this.#onRequestInit(event.data) + case Protocol.isRequestData(event.data): + return this.#onRequestData(event.data) + case Protocol.isRequestEnd(event.data): + return this.#onRequestEnd(event.data) + default: + this.#throw('Unknown message') + } + } + #throw(message: string): never { + throw new Error(`ProxyListener: ${message}`) + } +} diff --git a/src/proxy/proxy/proxy.mts b/src/proxy/proxy/proxy.mts new file mode 100644 index 0000000..d382622 --- /dev/null +++ b/src/proxy/proxy/proxy.mts @@ -0,0 +1,43 @@ +/*-------------------------------------------------------------------------- + +@sinclair/smoke + +The MIT License (MIT) + +Copyright (c) 2024 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { ProxyListener, ListenCallback } from './listener.mjs' +import * as Setup from './setup.mjs' + +export interface ProxyListenOptions { + /** The path this listener should intercept Http requests */ + path: string + /** (Optional) The path to the ServiceWorker script. The default is `worker.js` */ + workerPath?: string +} +/** Listens for http requests made to the given path prefix */ +export async function listen(options: ProxyListenOptions, callback: ListenCallback): Promise { + const workerPath = options.workerPath || 'worker.js' + const { clientId, port, worker } = await Setup.resolveWorker({ path: options.path, workerPath }) + return new ProxyListener(worker, port, clientId, callback) +} diff --git a/src/proxy/proxy/setup.mts b/src/proxy/proxy/setup.mts new file mode 100644 index 0000000..37ffedb --- /dev/null +++ b/src/proxy/proxy/setup.mts @@ -0,0 +1,103 @@ +/*-------------------------------------------------------------------------- + +@sinclair/smoke + +The MIT License (MIT) + +Copyright (c) 2024 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import * as Protocol from '../protocol/index.mjs' +import * as Path from '../../path/index.mjs' +// ------------------------------------------------------------------ +// Timeout +// ------------------------------------------------------------------ +function timeout(reject: Function, message: string) { + setTimeout(() => reject(message), 4000) +} +// ------------------------------------------------------------------ +// Registration +// ------------------------------------------------------------------ +// prettier-ignore +async function getCurrentRegistration(workerPath: string): Promise { + const registrations = await navigator.serviceWorker.getRegistrations() + const basename = Path.basename(workerPath) + return registrations.find((registration) => { + return ( + (registration.installing && registration.installing.scriptURL.includes(basename)) || + (registration.active && registration.active.scriptURL.includes(basename)) + ) + }) +} +async function getNewRegistration(workerPath: string): Promise { + return await navigator.serviceWorker.register(workerPath, { scope: '/' }) +} +async function getRegistration(workerPath: string): Promise { + const current = await getCurrentRegistration(workerPath) + if (current) return current + return await getNewRegistration(workerPath) +} +// ------------------------------------------------------------------ +// ServiceWorker +// ------------------------------------------------------------------ +async function waitForServiceWorkerActivate(serviceWorker: ServiceWorker): Promise { + if (serviceWorker.state === 'activated') return serviceWorker + return new Promise((resolve, reject) => { + timeout(reject, 'Timeout waiting for Service Worker to activate') + serviceWorker.addEventListener('statechange', () => { + if (serviceWorker.state !== 'activated') return + resolve(serviceWorker) + }) + }) +} +/** Attaches this page to a service worker */ +async function resolveWorkerInstance(workerPath: string) { + const registration = await getRegistration(workerPath) + if (registration.active) return waitForServiceWorkerActivate(registration.active) + if (registration.installing) return waitForServiceWorkerActivate(registration.installing) + throw Error('Registration has no active or installing workers') +} +// ------------------------------------------------------------------ +// NegotiatedServiceWorker +// ------------------------------------------------------------------ +export interface ServiceWorkerRequest { + path: string + workerPath: string +} +export interface ServiceWorkerResponse { + worker: ServiceWorker + port: MessagePort + clientId: string +} +/** Attaches this page to a service worker and provisions a messaging channel */ +// prettier-ignore +export async function resolveWorker(options: ServiceWorkerRequest): Promise { + const worker = await resolveWorkerInstance(options.workerPath) + const { port1, port2 } = new MessageChannel() + port1.start() + worker.postMessage({ port: port2 }, [port2]) + port1.postMessage({ type: 'RegisterRequest', path: options.path } as Protocol.RegisterRequest) + return new Promise((resolve) => port1.addEventListener('message', (event) => { + Protocol.assertRegisterResponse(event.data) + resolve({ worker, port: port1, clientId: event.data.clientId }) + }, { once: true })) +} diff --git a/src/proxy/service/client.mts b/src/proxy/service/client.mts new file mode 100644 index 0000000..4f780b9 --- /dev/null +++ b/src/proxy/service/client.mts @@ -0,0 +1,164 @@ +/*-------------------------------------------------------------------------- + +@sinclair/smoke + +The MIT License (MIT) + +Copyright (c) 2024 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import * as Async from '../../async/index.mjs' +import * as Channel from '../../channel/index.mjs' +import * as Protocol from '../protocol/index.mjs' + +// ------------------------------------------------------------------ +// Registry +// ------------------------------------------------------------------ +const registered = new Set() +export function addClient(windowClient: WindowClient, port: MessagePort, clientId: string) { + registered.add(new ProxyClient(windowClient, port, clientId)) +} +export function findClient(clientId: string, url: URL): ServiceClient { + for (const client of registered) if (client.shouldAccept(clientId, url)) return client + return new DefaultClient() +} +// ------------------------------------------------------------------ +// Client +// ------------------------------------------------------------------ +export interface ServiceClient { + fetch(request: Request): Promise +} +// ------------------------------------------------------------------ +// DefaultClient +// ------------------------------------------------------------------ +export class DefaultClient implements ServiceClient { + public async fetch(request: Request): Promise { + return await fetch(request) + } +} +// ------------------------------------------------------------------ +// ProxyClient +// ------------------------------------------------------------------ +let requestOrdinal = 0 + +export class ProxyClient implements ServiceClient { + #windowClient: WindowClient + #deferred: Map> + #receivers: Map> + #port: MessagePort + #path: string + constructor(windowClient: WindowClient, port: MessagePort, path: string) { + this.#windowClient = windowClient + this.#deferred = new Map>() + this.#receivers = new Map>() + this.#port = port + this.#path = path + this.#port.addEventListener('message', (event) => this.#onMessage(event)) + } + // ---------------------------------------------------------------- + // ShouldAccept + // ---------------------------------------------------------------- + /** Returns true of this client should accept this request */ + public shouldAccept(clientId: string, url: URL): boolean { + return this.#windowClient.id === clientId && url.pathname.startsWith(this.#path) + } + // ---------------------------------------------------------------- + // Fetch + // ---------------------------------------------------------------- + #send(message: Message) { + this.#port.postMessage(message) + } + // prettier-ignore + async #sendRequest(requestId: number, request: Request) { + this.#send({ type: 'RequestInit', requestId, url: request.url, init: Protocol.requestInitFromRequest(request) }) + if (request.body !== null) { + const reader = request.body.getReader() + while (true) { + const { value, done } = await reader.read() + if (done) break + this.#send({ requestId, type: 'RequestData', data: value! }) + } + } + this.#send({ requestId, type: 'RequestEnd' }) + } + /** Performs a fetch operation on this client. */ + public async fetch(request: Request): Promise { + const requestId = requestOrdinal++ + const response = new Async.Deferred() + const receiver = new Channel.Channel() + this.#deferred.set(requestId, response) + this.#receivers.set(requestId, receiver) + await this.#sendRequest(requestId, request) + return response.promise() + } + // ---------------------------------------------------------------- + // Events + // ---------------------------------------------------------------- + #createReadableStreamBody(message: Protocol.ResponseInit) { + const receiver = this.#receivers.get(message.requestId)! + if (receiver === undefined) this.#throw('Cannot find receiver') + return new ReadableStream({ + pull: async (controller) => { + const next = await receiver.next() + return next !== null ? controller.enqueue(next) : controller.close() + }, + }) + } + #onResponseInit(message: Protocol.ResponseInit) { + const deferred = this.#deferred.get(message.requestId) + if (deferred === undefined) this.#throw('Cannot find response') + this.#deferred.delete(message.requestId) + const body = this.#createReadableStreamBody(message) + const response = new Response(body, { + headers: new Headers(message.init.headers), + status: message.init.status, + statusText: message.init.statusText, + }) + deferred.resolve(response) + } + #onResponseData(message: Protocol.ResponseData) { + const receiver = this.#receivers.get(message.requestId) + if (receiver === undefined) this.#throw('Cannot find receiver') + receiver.send(message.data) + } + #onResponseEnd(message: Protocol.ResponseEnd) { + const receiver = this.#receivers.get(message.requestId) + if (receiver === undefined) this.#throw('Cannot find receiver') + this.#receivers.delete(message.requestId) + receiver.end() + } + #onMessage(event: MessageEvent) { + switch (true) { + case Protocol.isResponseInit(event.data): + return this.#onResponseInit(event.data) + case Protocol.isResponseData(event.data): + return this.#onResponseData(event.data) + case Protocol.isResponseEnd(event.data): + return this.#onResponseEnd(event.data) + default: + this.#throw('Unknown message') + } + } + #throw(message: string): never { + throw Error(`ProxyClient: ${message}`) + } +} diff --git a/src/proxy/service/worker.mts b/src/proxy/service/worker.mts new file mode 100644 index 0000000..d880773 --- /dev/null +++ b/src/proxy/service/worker.mts @@ -0,0 +1,81 @@ +/*-------------------------------------------------------------------------- + +@sinclair/smoke + +The MIT License (MIT) + +Copyright (c) 2024 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +/// +/// +/// + +import * as Protocol from '../protocol/index.mjs' +import { addClient, findClient } from './client.mjs' + +// ------------------------------------------------------------------ +// Start +// ------------------------------------------------------------------ +function onStart(callback: (self: ServiceWorkerGlobalScope) => unknown) { + callback(globalThis.self as never) +} +onStart((self) => { + self.addEventListener('install', (event) => onInstall(self, event)) + self.addEventListener('activate', (event) => onActivate(self, event)) + self.addEventListener('message', (event) => onMessage(event)) + self.addEventListener('fetch', (event) => onFetch(event)) +}) +// ------------------------------------------------------------------ +// Install +// ------------------------------------------------------------------ +function onInstall(self: ServiceWorkerGlobalScope, event: ExtendableEvent) { + event.waitUntil(self.skipWaiting()) +} +// ------------------------------------------------------------------ +// Activate +// ------------------------------------------------------------------ +function onActivate(self: ServiceWorkerGlobalScope, event: ExtendableEvent) { + event.waitUntil(self.clients.claim()) +} +// ------------------------------------------------------------------ +// Message +// ------------------------------------------------------------------ +// prettier-ignore +function onMessage(event: ExtendableMessageEvent) { + const windowClient = event.source as WindowClient + const port = event.data.port as MessagePort + port.start() + port.addEventListener('message', (event) => { + Protocol.assertRegisterRequest(event.data) + port.postMessage({ type: 'RegisterResponse', clientId: windowClient.id }) + addClient(windowClient, port, event.data.path) + }, { once: true }) +} + +// ------------------------------------------------------------------ +// Fetch +// ------------------------------------------------------------------ +async function onFetch(event: FetchEvent) { + const client = findClient(event.clientId, new URL(event.request.url)) + event.respondWith(client.fetch(event.request)) +} diff --git a/src/stream/index.mts b/src/stream/index.mts index 3e1e74c..521ac4f 100644 --- a/src/stream/index.mts +++ b/src/stream/index.mts @@ -27,7 +27,6 @@ THE SOFTWARE. ---------------------------------------------------------------------------*/ export * from './frames/index.mjs' -export * from './iterator/index.mjs' export * from './close.mjs' export * from './read.mjs' export * from './write.mjs' diff --git a/test/index.mts b/test/index.mts index 824bf67..85a7c0d 100644 --- a/test/index.mts +++ b/test/index.mts @@ -15,6 +15,7 @@ import './filesystem/index.mjs' import './http/index.mjs' import './net/index.mjs' import './os/index.mjs' +import './proxy/index.mjs' // ------------------------------------------------------------------ // Drift diff --git a/test/proxy/index.mts b/test/proxy/index.mts new file mode 100644 index 0000000..5b7fa6e --- /dev/null +++ b/test/proxy/index.mts @@ -0,0 +1 @@ +import './proxy.mjs' diff --git a/test/proxy/proxy.mts b/test/proxy/proxy.mts new file mode 100644 index 0000000..e90a0f4 --- /dev/null +++ b/test/proxy/proxy.mts @@ -0,0 +1,77 @@ +import { Proxy, Buffer } from '@sinclair/smoke' +import { Test, Assert } from '../test/index.mjs' + +Test.describe('Proxy', () => { + Test.before(async () => { + await Proxy.listen({ path: '/route1' }, () => new Response('hello')) + await Proxy.listen({ path: '/route2' }, () => new Response('world')) + await Proxy.listen({ path: '/status' }, () => new Response('status', { status: 400, statusText: '400' })) + await Proxy.listen({ path: '/echo' }, (request) => new Response(request.body)) + }) + // ---------------------------------------------------------------- + // Root + // ---------------------------------------------------------------- + Test.it('should fetch root', async () => { + const result = await fetch('/').then((res) => res.text()) + Assert.isTrue(result.includes('')) + }) + // ---------------------------------------------------------------- + // Routes + // ---------------------------------------------------------------- + Test.it('should fetch server 1', async () => { + const result = await fetch('/route1').then((res) => res.text()) + Assert.isEqual(result, 'hello') + }) + Test.it('should fetch server 2', async () => { + const result = await fetch('/route2').then((res) => res.text()) + Assert.isEqual(result, 'world') + }) + // ---------------------------------------------------------------- + // Status + // ---------------------------------------------------------------- + Test.it('should fetch status', async () => { + const result = await fetch('/status') + Assert.isEqual(result.status, 400) + Assert.isEqual(result.statusText, '400') + }) + // ---------------------------------------------------------------- + // Echo + // ---------------------------------------------------------------- + Test.it('should fetch echo (string)', async () => { + const result = await fetch('/echo', { method: 'POST', body: 'hello' }).then((res) => res.text()) + Assert.isEqual(result, 'hello') + }) + Test.it('should fetch echo (blob)', async () => { + const result = await fetch('/echo', { method: 'POST', body: new Blob(['hello']) }).then((res) => res.text()) + Assert.isEqual(result, 'hello') + }) + // ----------------------------------------------------------------- + // Readable Echo + // ----------------------------------------------------------------- + async function readableEcho() { + const buffers = Array.from({ length: 1024 }, () => Buffer.random(1000)) + const expect = Buffer.concat(buffers) + const queue = [...buffers] + const readable = new ReadableStream({ + pull: (controller) => { + return queue.length > 0 ? controller.enqueue(queue.shift()!) : controller.close() + }, + }) + const result = await fetch('/echo', { method: 'POST', body: readable, duplex: 'half' } as any).then((res) => res.arrayBuffer()) + const actual = new Uint8Array(result) + Assert.isEqual(expect, actual) + } + Test.it('should fetch echo (readable)', async () => { + await readableEcho() + }) + // ----------------------------------------------------------------- + // Parallel Echo + // ----------------------------------------------------------------- + Test.it('should fetch parallel echo (readable x 2)', async () => { + await Promise.all([readableEcho(), readableEcho()]) + }) + Test.it('should fetch parallel echo (readable x 16)', async () => { + const tasks = Array.from({ length: 16 }, () => readableEcho()) + await Promise.all(tasks) + }) +})