diff --git a/package-lock.json b/package-lock.json index 501d037..1d4af11 100644 --- a/package-lock.json +++ b/package-lock.json @@ -83,6 +83,73 @@ "tough-cookie": "^4.1.4" } }, + "node_modules/@chialab/esbuild-plugin-meta-url": { + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/@chialab/esbuild-plugin-meta-url/-/esbuild-plugin-meta-url-0.18.2.tgz", + "integrity": "sha512-uIRIdLvYnw5mLrTRXY0BTgeZx6ANL2/OHkWFl8FaiTYNb7cyXmwEDRE1mh6kBXPRPtGuqv6XSpNX+koEkElu4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@chialab/esbuild-rna": "^0.18.1", + "@chialab/estransform": "^0.18.1", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@chialab/esbuild-plugin-worker": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/@chialab/esbuild-plugin-worker/-/esbuild-plugin-worker-0.18.1.tgz", + "integrity": "sha512-FCpdhMQkrwBejY+uWo3xLdqHhUK3hbn0ICedyqo97hzRX98ErB2fhRq4LEEPMEaiplF2se2ToYTQaoxHDpkouw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@chialab/esbuild-plugin-meta-url": "^0.18.2", + "@chialab/esbuild-rna": "^0.18.0", + "@chialab/estransform": "^0.18.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@chialab/esbuild-rna": { + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/@chialab/esbuild-rna/-/esbuild-rna-0.18.2.tgz", + "integrity": "sha512-ckzskez7bxstVQ4c5cxbx0DRP2teldzrcSGQl2KPh1VJGdO2ZmRrb6vNkBBD5K3dx9tgTyvskWp4dV+Fbg07Ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@chialab/estransform": "^0.18.0", + "@chialab/node-resolve": "^0.18.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@chialab/estransform": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/@chialab/estransform/-/estransform-0.18.1.tgz", + "integrity": "sha512-W/WmjpQL2hndD0/XfR0FcPBAUj+aLNeoAVehOjV/Q9bSnioz0GVSAXXhzp59S33ZynxJBBfn8DNiMTVNJmk4Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/source-map": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@chialab/node-resolve": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@chialab/node-resolve/-/node-resolve-0.18.0.tgz", + "integrity": "sha512-eV1m70Qn9pLY9xwFmZ2FlcOzwiaUywsJ7NB/ud8VB7DouvCQtIHkQ3Om7uPX0ojXGEG1LCyO96kZkvbNTxNu0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.24.0", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz", @@ -742,6 +809,19 @@ "dev": true, "license": "MIT" }, + "node_modules/@parcel/source-map": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@parcel/source-map/-/source-map-2.1.1.tgz", + "integrity": "sha512-Ejx1P/mj+kMjQb8/y5XxDUn4reGdr+WyKYloBljpppUy8gs42T+BNoEOuRYqDVdgPc6NxduzIDoJS9pOFfV5Ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^1.0.3" + }, + "engines": { + "node": "^12.18.3 || >=14" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -1012,6 +1092,10 @@ "win32" ] }, + "node_modules/@tcpip/c2w": { + "resolved": "packages/c2w", + "link": true + }, "node_modules/@tcpip/v86": { "resolved": "packages/v86", "link": true @@ -1037,9 +1121,9 @@ } }, "node_modules/@testing-library/user-event": { - "version": "14.5.2", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.5.2.tgz", - "integrity": "sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==", + "version": "14.6.0", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.0.tgz", + "integrity": "sha512-+jsfK7kVJbqnCYtLTln8Ja/NmVrZRwBJHmHR9IxIVccMWSOZ6Oy0FkDJNeyVu4QSpMNmRfy10Xb76ObRDlWWBQ==", "dev": true, "license": "MIT", "engines": { @@ -1601,6 +1685,12 @@ "dev": true, "license": "MIT" }, + "node_modules/comlink": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/comlink/-/comlink-4.4.2.tgz", + "integrity": "sha512-OxGdvBmJuNKSCMO4NTl1L47VRp6xn2wG4F/2hYzB6tiCb709otOxtEYCSvK80PtjODfXXZu8ds+Nw5kVCjqd2g==", + "license": "Apache-2.0" + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -1647,9 +1737,9 @@ } }, "node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "dev": true, "license": "MIT", "dependencies": { @@ -1684,6 +1774,19 @@ "node": ">=6" } }, + "node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/dom-accessibility-api": { "version": "0.5.16", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", @@ -2037,15 +2140,38 @@ } }, "node_modules/magic-string": { - "version": "0.30.12", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz", - "integrity": "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==", + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -2873,9 +2999,9 @@ "license": "MIT" }, "node_modules/tinyexec": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.1.tgz", - "integrity": "sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==", + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", "dev": true, "license": "MIT" }, @@ -2894,9 +3020,9 @@ } }, "node_modules/tinypool": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.1.tgz", - "integrity": "sha512-URZYihUbRPcGv95En+sz6MfghfIc2OJ1sv/RmhWZLouPY0/8Vo80viwPvg3dlaS9fuq7fQMEfgRRK7BBZThBEA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz", + "integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==", "dev": true, "license": "MIT", "engines": { @@ -4130,6 +4256,13 @@ } } }, + "node_modules/web-worker": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.3.0.tgz", + "integrity": "sha512-BSR9wyRsy/KOValMgd5kMyr3JzpdeoR9KVId8u5GVlTTAtNChlsE4yTxeY7zMdNSyOmoKBv8NH2qeRY9Tg+IaA==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/webidl-conversions": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", @@ -4399,6 +4532,304 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "packages/c2w": { + "name": "@tcpip/c2w", + "version": "0.1.0", + "dependencies": { + "@bjorn3/browser_wasi_shim": "^0.3.0", + "comlink": "^4.4.2" + }, + "devDependencies": { + "@chialab/esbuild-plugin-worker": "^0.18.1", + "@total-typescript/tsconfig": "^1.0.4", + "tcpip": "0.2", + "typescript": "^5.0.4", + "vitest": "^3.0.1", + "web-worker": "^1.3.0" + } + }, + "packages/c2w/node_modules/@vitest/browser": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-3.0.2.tgz", + "integrity": "sha512-EVXRQTEmwyCNh6qDXIf/fGWp3YXa3KtsMCOXmlD4Yeq62pJaqJ5+iUIY1XLN7TO2iXatGDdLZYbHbR6YQT4FDw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@testing-library/dom": "^10.4.0", + "@testing-library/user-event": "^14.6.0", + "@vitest/mocker": "3.0.2", + "@vitest/utils": "3.0.2", + "magic-string": "^0.30.17", + "msw": "^2.7.0", + "sirv": "^3.0.0", + "tinyrainbow": "^2.0.0", + "ws": "^8.18.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "playwright": "*", + "vitest": "3.0.2", + "webdriverio": "*" + }, + "peerDependenciesMeta": { + "playwright": { + "optional": true + }, + "safaridriver": { + "optional": true + }, + "webdriverio": { + "optional": true + } + } + }, + "packages/c2w/node_modules/@vitest/expect": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.2.tgz", + "integrity": "sha512-dKSHLBcoZI+3pmP5hiZ7I5grNru2HRtEW8Z5Zp4IXog8QYcxhlox7JUPyIIFWfN53+3HW3KPLIl6nSzUGgKSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.0.2", + "@vitest/utils": "3.0.2", + "chai": "^5.1.2", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/c2w/node_modules/@vitest/mocker": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.2.tgz", + "integrity": "sha512-Hr09FoBf0jlwwSyzIF4Xw31OntpO3XtZjkccpcBf8FeVW3tpiyKlkeUzxS/txzHqpUCNIX157NaTySxedyZLvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.0.2", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "packages/c2w/node_modules/@vitest/pretty-format": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.2.tgz", + "integrity": "sha512-yBohcBw/T/p0/JRgYD+IYcjCmuHzjC3WLAKsVE4/LwiubzZkE8N49/xIQ/KGQwDRA8PaviF8IRO8JMWMngdVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/c2w/node_modules/@vitest/runner": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.2.tgz", + "integrity": "sha512-GHEsWoncrGxWuW8s405fVoDfSLk6RF2LCXp6XhevbtDjdDme1WV/eNmUueDfpY1IX3MJaCRelVCEXsT9cArfEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.0.2", + "pathe": "^2.0.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/c2w/node_modules/@vitest/snapshot": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.2.tgz", + "integrity": "sha512-h9s67yD4+g+JoYG0zPCo/cLTabpDqzqNdzMawmNPzDStTiwxwkyYM1v5lWE8gmGv3SVJ2DcxA2NpQJZJv9ym3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.0.2", + "magic-string": "^0.30.17", + "pathe": "^2.0.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/c2w/node_modules/@vitest/spy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.2.tgz", + "integrity": "sha512-8mI2iUn+PJFMT44e3ISA1R+K6ALVs47W6eriDTfXe6lFqlflID05MB4+rIFhmDSLBj8iBsZkzBYlgSkinxLzSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/c2w/node_modules/@vitest/ui": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-3.0.2.tgz", + "integrity": "sha512-R0E4nG0OAafsCKwKnENLdjpMbxAyDqT/hdbJp71eeAR1wE+C7IFv1G158sRj5gUfJ7pM7IxtcwIqa34beYzLhg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@vitest/utils": "3.0.2", + "fflate": "^0.8.2", + "flatted": "^3.3.2", + "pathe": "^2.0.1", + "sirv": "^3.0.0", + "tinyglobby": "^0.2.10", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "3.0.2" + } + }, + "packages/c2w/node_modules/@vitest/utils": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.2.tgz", + "integrity": "sha512-Qu01ZYZlgHvDP02JnMBRpX43nRaZtNpIzw3C1clDXmn8eakgX6iQVGzTQ/NjkIr64WD8ioqOjkaYRVvHQI5qiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.0.2", + "loupe": "^3.1.2", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/c2w/node_modules/pathe": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.2.tgz", + "integrity": "sha512-15Ztpk+nov8DR524R4BF7uEuzESgzUEAV4Ah7CUMNGXdE5ELuvxElxGXndBl32vMSsWa1jpNf22Z+Er3sKwq+w==", + "dev": true, + "license": "MIT" + }, + "packages/c2w/node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "packages/c2w/node_modules/vite-node": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.2.tgz", + "integrity": "sha512-hsEQerBAHvVAbv40m3TFQe/lTEbOp7yDpyqMJqr2Tnd+W58+DEYOt+fluQgekOePcsNBmR77lpVAnIU2Xu4SvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.0", + "es-module-lexer": "^1.6.0", + "pathe": "^2.0.1", + "vite": "^5.0.0 || ^6.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/c2w/node_modules/vitest": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.2.tgz", + "integrity": "sha512-5bzaHakQ0hmVVKLhfh/jXf6oETDBtgPo8tQCHYB+wftNgFJ+Hah67IsWc8ivx4vFL025Ow8UiuTf4W57z4izvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "3.0.2", + "@vitest/mocker": "3.0.2", + "@vitest/pretty-format": "^3.0.2", + "@vitest/runner": "3.0.2", + "@vitest/snapshot": "3.0.2", + "@vitest/spy": "3.0.2", + "@vitest/utils": "3.0.2", + "chai": "^5.1.2", + "debug": "^4.4.0", + "expect-type": "^1.1.0", + "magic-string": "^0.30.17", + "pathe": "^2.0.1", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinypool": "^1.0.2", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0", + "vite-node": "3.0.2", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.0.2", + "@vitest/ui": "3.0.2", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, "packages/tcpip": { "version": "0.2.2", "license": "MIT", @@ -4422,7 +4853,7 @@ "version": "0.2.1", "devDependencies": { "@total-typescript/tsconfig": "^1.0.4", - "tcpip": "^0.2.0-dev.1", + "tcpip": "0.2", "typescript": "^5.0.4" } }, diff --git a/packages/c2w/package.json b/packages/c2w/package.json new file mode 100644 index 0000000..a0f78fd --- /dev/null +++ b/packages/c2w/package.json @@ -0,0 +1,35 @@ +{ + "name": "@tcpip/c2w", + "version": "0.1.0", + "description": "Network adapter that connects tcpip.js with container2wasm VMs", + "main": "dist/index.cjs", + "types": "dist/index.d.ts", + "type": "module", + "scripts": { + "build": "tsup --clean", + "test": "vitest", + "prepublishOnly": "npm run build" + }, + "files": [ + "dist/**/*" + ], + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts", + "default": "./dist/index.cjs" + } + }, + "dependencies": { + "@bjorn3/browser_wasi_shim": "^0.3.0", + "comlink": "^4.4.2" + }, + "devDependencies": { + "@chialab/esbuild-plugin-worker": "^0.18.1", + "@total-typescript/tsconfig": "^1.0.4", + "tcpip": "0.2", + "typescript": "^5.0.4", + "vitest": "^3.0.1", + "web-worker": "^1.3.0" + } +} diff --git a/packages/c2w/patches/web-worker.ts b/packages/c2w/patches/web-worker.ts new file mode 100644 index 0000000..4e56870 --- /dev/null +++ b/packages/c2w/patches/web-worker.ts @@ -0,0 +1,11 @@ +/** + * The 'web-worker' package is a Node.js polyfill for the Web Worker API. + * + * It uses `__filename` to get the current file path, which isn't supported + * in Node.js ES modules. The equivalent in ES modules is `import.meta.url`. + * + * This is an ESBuild patch to replace `__filename` with `import.meta.url`. + */ +const metaUrl = new URL(import.meta.url); + +export { metaUrl as __filename }; diff --git a/packages/c2w/src/container/index.ts b/packages/c2w/src/container/index.ts new file mode 100644 index 0000000..bbbade9 --- /dev/null +++ b/packages/c2w/src/container/index.ts @@ -0,0 +1,107 @@ +import { proxy, wrap } from 'comlink'; +import Worker from 'web-worker'; +import { NetworkInterface } from './network-interface.js'; +import { StdioInterface } from './stdio-interface.js'; +import type { VM, VMOptions } from './vm.js'; + +export type ContainerNetOptions = { + /** + * The MAC address to assign to the VM. + * + * If not provided, a random MAC address will be generated. + */ + macAddress?: string; +}; + +export type ContainerOptions = { + /** + * The URL of the c2w-compiled WASM file to load. + */ + wasmUrl: string | URL; + + /** + * The entrypoint to the container. + */ + entrypoint?: string; + + /** + * The command to run in the container. + */ + command?: string[]; + + /** + * Environment variables to pass to the container. + */ + env?: Record; + + /** + * Network configuration for the container VM. + */ + net?: ContainerNetOptions; + + /** + * Callback when the container VM exits. + */ + onExit?: (exitCode: number) => void; + + /** + * Enable debug logging. + */ + debug?: boolean; +}; + +/** + * Creates a `container2wasm` VM. + * + * Returns an object with `stdio` and `net` properties, which are interfaces for + * interacting with the VM's standard I/O and network interfaces. + */ +export async function createContainer(options: ContainerOptions) { + const stdioInterface = new StdioInterface({ + debug: options.debug, + }); + const netInterface = new NetworkInterface({ + macAddress: options.net?.macAddress, + debug: options.debug, + }); + + const vmWorker = await createVMWorker({ + wasmUrl: options.wasmUrl, + stdio: stdioInterface.vmStdioOptions, + net: netInterface.vmNetOptions, + entrypoint: options.entrypoint, + command: options.command, + env: options.env, + debug: options.debug, + }); + + vmWorker.run().then((exitCode) => { + vmWorker.close(); + options.onExit?.(exitCode); + }); + + return { + stdio: stdioInterface, + net: netInterface, + }; +} + +async function createVMWorker(options: VMOptions) { + const worker = new Worker(new URL('./vm-worker.ts', import.meta.url), { + type: 'module', + }); + + const VMWorker = wrap(worker); + return await new VMWorker( + { + wasmUrl: String(options.wasmUrl), + stdio: options.stdio, + net: options.net, + entrypoint: options.entrypoint, + command: options.command, + env: options.env, + debug: options.debug, + }, + proxy(console.log) + ); +} diff --git a/packages/c2w/src/container/network-interface.ts b/packages/c2w/src/container/network-interface.ts new file mode 100644 index 0000000..e38542c --- /dev/null +++ b/packages/c2w/src/container/network-interface.ts @@ -0,0 +1,110 @@ +import { frameStream } from '../frame/length-prefixed-frames.js'; +import { createAsyncRingBuffer } from '../ring-buffer/index.js'; +import { RingBuffer } from '../ring-buffer/ring-buffer.js'; +import type { DuplexStream } from '../types.js'; +import { fromReadable, generateMacAddress, parseMacAddress } from '../util.js'; +import type { VMNetOptions } from './vm.js'; + +export type NetworkInterfaceOptions = { + /** + * The MAC address to assign to the VM. + * + * If not provided, a random MAC address will be generated. + */ + macAddress?: string; + + /** + * Enable debug logging. + */ + debug?: boolean; +}; + +export class NetworkInterface + implements DuplexStream, AsyncIterable +{ + #receiveBuffer: SharedArrayBuffer; + #sendBuffer: SharedArrayBuffer; + + readonly readable: ReadableStream; + readonly writable: WritableStream; + readonly macAddress: string; + + /** + * The network options to pass to the VM. + * + * Internal use only. + */ + get vmNetOptions(): VMNetOptions { + return { + receiveBuffer: this.#sendBuffer, // VM reads from sendBuffer + sendBuffer: this.#receiveBuffer, // VM writes to receiveBuffer + macAddress: this.macAddress, + }; + } + + constructor(options: NetworkInterfaceOptions) { + this.macAddress = + options.macAddress ?? parseMacAddress(generateMacAddress()); + + // Create shared buffers for network communication + this.#receiveBuffer = new SharedArrayBuffer(1024 * 1024); + this.#sendBuffer = new SharedArrayBuffer(1024 * 1024); + + // Create ring buffers for network communication + const receiveRingPromise = createAsyncRingBuffer( + this.#receiveBuffer, + (...data: unknown[]) => console.log('Net interface: Receive:', ...data), + options.debug + ); + const sendRing = new RingBuffer( + this.#sendBuffer, + (...data: unknown[]) => console.log('Net interface: Send:', ...data), + options.debug + ); + + // Create a raw duplex stream for reading and writing frames + const rawStream: DuplexStream = { + readable: new ReadableStream( + { + async pull(controller) { + const receiveRing = await receiveRingPromise; + const data = await receiveRing.read( + controller.desiredSize ?? undefined + ); + + controller.enqueue(data); + }, + }, + { + highWaterMark: 1024 * 1024, + size(chunk) { + return chunk.length; + }, + } + ), + writable: new WritableStream({ + write(chunk) { + sendRing.write(chunk); + }, + }), + }; + + // c2w uses 4-byte length-prefixed frames + const { readable, writable } = frameStream(rawStream, { headerLength: 4 }); + + // Expose streams for external reading and writing + this.readable = readable; + this.writable = writable; + } + + listen() { + if (this.readable.locked) { + throw new Error('readable stream already locked'); + } + return fromReadable(this.readable); + } + + [Symbol.asyncIterator](): AsyncIterableIterator { + return this.listen(); + } +} diff --git a/packages/c2w/src/container/stdio-interface.ts b/packages/c2w/src/container/stdio-interface.ts new file mode 100644 index 0000000..63000f3 --- /dev/null +++ b/packages/c2w/src/container/stdio-interface.ts @@ -0,0 +1,106 @@ +import { createAsyncRingBuffer } from '../ring-buffer/index.js'; +import { RingBuffer } from '../ring-buffer/ring-buffer.js'; +import { fromReadable } from '../util.js'; +import type { VMStdioOptions } from './vm.js'; + +export type StdioInterfaceOptions = { + /** + * Enable debug logging. + */ + debug?: boolean; +}; + +export class StdioInterface { + #stdinBuffer: SharedArrayBuffer; + #stdoutBuffer: SharedArrayBuffer; + #stderrBuffer: SharedArrayBuffer; + + readonly stdin: WritableStream; + readonly stdout: ReadableStream; + readonly stderr: ReadableStream; + + get iterateStdout() { + return fromReadable(this.stdout); + } + + get iterateStderr() { + return fromReadable(this.stderr); + } + + /** + * The stdio options to pass to the VM. + * + * Internal use only. + */ + get vmStdioOptions(): VMStdioOptions { + return { + stdinBuffer: this.#stdinBuffer, + stdoutBuffer: this.#stdoutBuffer, + stderrBuffer: this.#stderrBuffer, + }; + } + + constructor(options: StdioInterfaceOptions = {}) { + // Create shared buffers for network communication + this.#stdinBuffer = new SharedArrayBuffer(1024 * 1024); + this.#stdoutBuffer = new SharedArrayBuffer(1024 * 1024); + this.#stderrBuffer = new SharedArrayBuffer(1024 * 1024); + + // Create ring buffers for network communication + const stdinRing = new RingBuffer( + this.#stdinBuffer, + (...data: unknown[]) => console.log('Stdio interface: Stdin:', ...data), + options.debug + ); + const stdoutRingPromise = createAsyncRingBuffer( + this.#stdoutBuffer, + (...data: unknown[]) => console.log('Stdio interface: Stdout:', ...data), + options.debug + ); + const stderrRingPromise = createAsyncRingBuffer( + this.#stderrBuffer, + (...data: unknown[]) => console.log('Stdio interface: Stderr:', ...data), + options.debug + ); + + this.stdin = new WritableStream({ + write(chunk) { + stdinRing.write(chunk); + }, + }); + + this.stdout = new ReadableStream( + { + async pull(controller) { + const ring = await stdoutRingPromise; + const data = await ring.read(controller.desiredSize ?? undefined); + + controller.enqueue(data); + }, + }, + { + highWaterMark: 1024 * 1024, + size(chunk) { + return chunk.length; + }, + } + ); + + this.stderr = new ReadableStream( + { + async pull(controller) { + const ring = await stderrRingPromise; + const data = await ring.read(controller.desiredSize ?? undefined); + + controller.enqueue(data); + }, + }, + { + highWaterMark: 1024 * 1024, + size(chunk) { + return chunk.length; + }, + } + ); + } +} diff --git a/packages/c2w/src/container/vm-worker.ts b/packages/c2w/src/container/vm-worker.ts new file mode 100644 index 0000000..3e6ad90 --- /dev/null +++ b/packages/c2w/src/container/vm-worker.ts @@ -0,0 +1,4 @@ +import { expose } from 'comlink'; +import { VM } from './vm.js'; + +expose(VM); diff --git a/packages/c2w/src/container/vm.ts b/packages/c2w/src/container/vm.ts new file mode 100644 index 0000000..435545d --- /dev/null +++ b/packages/c2w/src/container/vm.ts @@ -0,0 +1,146 @@ +import { PreopenDirectory, File, WASI, Fd } from '@bjorn3/browser_wasi_shim'; +import { fetchFile } from '../fetch-file.js'; +import { RingBuffer } from '../ring-buffer/ring-buffer.js'; +import { handleWasiSocket } from '../wasi/socket-extension.js'; + +type WasiInstance = WebAssembly.Instance & { + exports: { + memory: WebAssembly.Memory; + _start: () => unknown; + }; +}; + +export type VMNetOptions = { + receiveBuffer: SharedArrayBuffer; + sendBuffer: SharedArrayBuffer; + macAddress: string; +}; + +export type VMStdioOptions = { + stdinBuffer: SharedArrayBuffer; + stdoutBuffer: SharedArrayBuffer; + stderrBuffer: SharedArrayBuffer; +}; + +export type VMOptions = { + wasmUrl: string | URL; + stdio: VMStdioOptions; + net: VMNetOptions; + entrypoint?: string; + command?: string[]; + env?: Record; + debug?: boolean; +}; + +export class VM { + #options: VMOptions; + + #stdinRing: RingBuffer; + #stdoutRing: RingBuffer; + #stderrRing: RingBuffer; + + #receiveRing: RingBuffer; + #sendRing: RingBuffer; + + #debug: (...data: unknown[]) => void = () => {}; + + constructor(options: VMOptions, log?: (...data: unknown[]) => void) { + if (options.debug && log) { + this.#debug = (...data: unknown[]) => log('VM:', ...data); + } + + this.#options = options; + + this.#stdinRing = new RingBuffer( + options.stdio.stdinBuffer, + (data) => this.#debug('Stdin:', data), + options.debug + ); + this.#stdoutRing = new RingBuffer( + options.stdio.stdoutBuffer, + (data) => this.#debug('Stdout:', data), + options.debug + ); + this.#stderrRing = new RingBuffer( + options.stdio.stderrBuffer, + (data) => this.#debug('Stderr:', data), + options.debug + ); + + this.#receiveRing = new RingBuffer( + options.net.receiveBuffer, + (...data: unknown[]) => this.#debug('Receive:', ...data), + options.debug + ); + this.#sendRing = new RingBuffer( + options.net.sendBuffer, + (...data: unknown[]) => this.#debug('Send:', ...data), + options.debug + ); + } + + async run() { + const env = Object.entries(this.#options.env ?? {}).map( + ([key, value]) => `${key}=${value}` + ); + + const args = ['arg0']; + + args.push('--net', 'socket'); + + args.push('--mac', this.#options.net.macAddress); + + if (this.#options.entrypoint) { + args.push('--entrypoint', this.#options.entrypoint); + } + + if (this.#options.command) { + args.push('--', ...this.#options.command); + } + + const wasi = new WASI(args, env, []); + + const listenFd = 3; + const connectionFd = 4; + + handleWasiSocket(wasi, { + listenFd, + connectionFd, + stdin: { + read: (len) => this.#stdinRing.read(len), + hasData: () => this.#stdinRing.hasData, + waitForData: (timeout) => this.#stdinRing.waitForData(timeout), + }, + stdout: { + write: (data) => { + this.#stdoutRing.write(data); + }, + }, + stderr: { + write: (data) => this.#stderrRing.write(data), + }, + net: { + accept: () => true, + send: (data) => this.#sendRing.write(data), + receive: (len) => this.#receiveRing.read(len), + hasData: () => this.#receiveRing.hasData, + waitForData: (timeout) => this.#receiveRing.waitForData(timeout), + }, + }); + + const wasmResponse = await fetchFile( + this.#options.wasmUrl, + 'application/wasm' + ); + const { instance } = await WebAssembly.instantiateStreaming(wasmResponse, { + wasi_snapshot_preview1: wasi.wasiImport, + }); + + const wasiInstance = instance as WasiInstance; + return wasi.start(wasiInstance); + } + + close() { + self.close(); + } +} diff --git a/packages/c2w/src/fetch-file.ts b/packages/c2w/src/fetch-file.ts new file mode 100644 index 0000000..bbd6cb8 --- /dev/null +++ b/packages/c2w/src/fetch-file.ts @@ -0,0 +1,24 @@ +const IN_NODE = + typeof process === 'object' && + typeof process.versions === 'object' && + typeof process.versions.node === 'string'; + +/** + * Fetches a file from the network or filesystem + * depending on the environment. + */ +export async function fetchFile(input: string | URL, type: string) { + if (IN_NODE) { + return fetchFileNode(input, type); + } + return fetch(input); +} + +async function fetchFileNode(input: string | URL, type: string) { + const fs = await import('node:fs'); + const { Readable } = await import('node:stream'); + const url = new URL(input); + const nodeStream = fs.createReadStream(url); + const stream = Readable.toWeb(nodeStream) as ReadableStream; + return new Response(stream, { headers: { 'Content-Type': type } }); +} diff --git a/packages/c2w/src/frame/collect.ts b/packages/c2w/src/frame/collect.ts new file mode 100644 index 0000000..35178fb --- /dev/null +++ b/packages/c2w/src/frame/collect.ts @@ -0,0 +1,70 @@ +export type CollectFrameOptions = { + /** + * The length of the header. Optionally accepts a callback for situations + * where header length could change over the course of the protocol lifecycle. + */ + headerLength: number | (() => number | Promise); + + /** + * The length of the entire frame (including header). Accepts a callback passing + * in the header bytes that can be used to determine the full frame length. + */ + frameLength: number | ((header: Uint8Array) => number | Promise); +}; + +/** + * General purpose method to buffer partial byte chunks and yield whole frames as + * they become available. + * + * Works with any header-based framing protocol that can determine the full size + * of each frame from the header. + */ +export async function* collectFrames( + chunks: AsyncIterable, + options: CollectFrameOptions, +): AsyncIterable { + let buffer = new Uint8Array(); + + const getHeaderLength = async () => + typeof options.headerLength === 'number' ? options.headerLength : await options.headerLength(); + + const getFrameLength = async (header: Uint8Array) => + typeof options.frameLength === 'number' + ? options.frameLength + : await options.frameLength(header); + + for await (const chunk of chunks) { + // Append chunk to buffer + buffer = concat(buffer, chunk); + + let headerLength = await getHeaderLength(); + + // Loop as long as we have enough bytes for a header + while (buffer.byteLength >= headerLength) { + const header = buffer.subarray(0, headerLength); + const frameLength = await getFrameLength(header); + + // If we're still waiting on bytes, break + if (buffer.byteLength < frameLength) { + break; + } + + // Yield the frame + yield buffer.subarray(0, frameLength); + + // Update the buffer to discard this frame + buffer = buffer.subarray(frameLength); + + // Some protocols allow different header lengths + // throughout their lifecycle, so re-evaluate + headerLength = await getHeaderLength(); + } + } +} + +export function concat(bufferA: Uint8Array, bufferB: Uint8Array): Uint8Array { + const concatenatedArray = new Uint8Array(bufferA.length + bufferB.length); + concatenatedArray.set(bufferA); + concatenatedArray.set(bufferB, bufferA.length); + return concatenatedArray; +} diff --git a/packages/c2w/src/frame/index.ts b/packages/c2w/src/frame/index.ts new file mode 100644 index 0000000..62e1d09 --- /dev/null +++ b/packages/c2w/src/frame/index.ts @@ -0,0 +1 @@ +export * from './length-prefixed-frames.js'; diff --git a/packages/c2w/src/frame/length-prefixed-frames.ts b/packages/c2w/src/frame/length-prefixed-frames.ts new file mode 100644 index 0000000..36c9807 --- /dev/null +++ b/packages/c2w/src/frame/length-prefixed-frames.ts @@ -0,0 +1,145 @@ +import type { DuplexStream } from '../types.js'; +import { fromReadable, toReadable } from '../util.js'; +import { collectFrames, concat } from './collect.js'; + +export type FrameStreamOptions = { + headerLength?: 1 | 2 | 4; +}; + +/** + * Frames each message in a stream so that whole messages are guaranteed + * after they are sent over a transport that segments messages. + * + * This uses a simple length-prefixed framing protocol where the length + * of the data is prefixed before the data itself. Defaults to a + * 4-byte (32 bit) header which supports messages up to 4GB each. + * You can adjust this header using the `headerLength` option. + * Use shorter headers to reduce per-message overhead at the cost + * of limiting the maximum message size. Use longer headers to + * increase maximum message size at the cost of higher overhead + * per-message. + */ +export function frameStream( + stream: DuplexStream, + options: FrameStreamOptions = {} +): DuplexStream { + const { headerLength = 4 } = options; + + // Collect incoming chunks into messages + const messageIterator = collectMessages(fromReadable(stream.readable), { + headerLength, + })[Symbol.asyncIterator](); + + // Add frame to outgoing messages + const frameTransform = new TransformStream({ + transform(message, controller) { + controller.enqueue(frameMessage(message, { headerLength })); + }, + }); + frameTransform.readable.pipeTo(stream.writable); + + const readable = toReadable(messageIterator); + const writable = frameTransform.writable; + + return { readable, writable }; +} + +/** + * Buffers partial byte chunks and yields whole message frames as + * they become available. + * + * This uses a simple length-prefixed framing protocol where the length + * of the data is prefixed before the data itself. Defaults to a + * 4-byte (32 bit) header which supports messages up to 4GB each. + * You can adjust this header using the `headerLength` option. + * Use shorter headers to reduce per-message overhead at the cost + * of limiting the maximum message size. Use longer headers to + * increase maximum message size at the cost of higher overhead + * per-message. + * + * Strips the header from the yielded message. + */ +export async function* collectMessages( + chunks: AsyncIterable, + options: FrameStreamOptions = {} +): AsyncIterable { + const { headerLength = 4 } = options; + + const frames = collectFrames(chunks, { + headerLength, + frameLength(headerBytes) { + const length = parseHeader(headerBytes, headerLength); + return headerLength + length; + }, + }); + + // Strip the header from each frame + for await (const frame of frames) { + yield frame.subarray(headerLength); + } +} + +/** + * Frames a message using a simple length-prefixed framing protocol + * where the length of the data is prefixed before the data itself. + * + * Defaults to a 4-byte (32 bit) header which supports messages up to + * 4GB each. You can adjust this header using the `headerLength` option. + * Use shorter headers to reduce per-message overhead at the cost + * of limiting the maximum message size. Use longer headers to + * increase maximum message size at the cost of higher overhead + * per-message. + */ +export function frameMessage( + message: Uint8Array, + options: FrameStreamOptions = {} +) { + const { headerLength = 4 } = options; + const maxMessageLength = 2 ** (8 * headerLength); + + if (message.byteLength > maxMessageLength) { + throw new Error(`message exceeds max length of ${maxMessageLength} bytes`); + } + + const headerBuffer = new ArrayBuffer(headerLength); + const dataView = new DataView(headerBuffer); + + switch (headerLength) { + case 1: { + dataView.setUint8(0, message.byteLength); + break; + } + case 2: { + dataView.setUint16(0, message.byteLength); + break; + } + case 4: { + dataView.setUint32(0, message.byteLength); + break; + } + } + + return concat(new Uint8Array(headerBuffer), message); +} + +function parseHeader(headerBytes: Uint8Array, headerLength: 1 | 2 | 4) { + const dataView = new DataView( + headerBytes.buffer, + headerBytes.byteOffset, + headerBytes.byteLength + ); + + switch (headerLength) { + case 1: { + return dataView.getUint8(0); + } + case 2: { + return dataView.getUint16(0); + } + case 4: { + return dataView.getUint32(0); + } + } + + throw new Error('invalid header length'); +} diff --git a/packages/c2w/src/index.test.ts b/packages/c2w/src/index.test.ts new file mode 100644 index 0000000..ee20eb3 --- /dev/null +++ b/packages/c2w/src/index.test.ts @@ -0,0 +1,14 @@ +import { describe, test } from 'vitest'; +import { createContainer } from './container/index.js'; + +describe('c2w', () => { + test('container communication', async () => { + const container = await createContainer({ + wasmUrl: new URL('../shell.wasm', import.meta.url), + }); + + for await (const chunk of container.netInterface) { + console.log('chunk', chunk.length); + } + }); +}); diff --git a/packages/c2w/src/index.ts b/packages/c2w/src/index.ts new file mode 100644 index 0000000..84b04b0 --- /dev/null +++ b/packages/c2w/src/index.ts @@ -0,0 +1 @@ +export * from './container/index.js'; diff --git a/packages/c2w/src/ring-buffer/index.ts b/packages/c2w/src/ring-buffer/index.ts new file mode 100644 index 0000000..76137b0 --- /dev/null +++ b/packages/c2w/src/ring-buffer/index.ts @@ -0,0 +1,28 @@ +import { proxy, wrap } from 'comlink'; +import Worker from 'web-worker'; +import { RingBuffer } from './ring-buffer.js'; + +/** + * Creates an asynchronous ring buffer over a shared array buffer. + * + * Vanilla ring buffers are synchronous and block via Atomics until + * data is added to the shared array buffer from another thread. + * This wrapper uses a worker to provide an asynchronous interface. + * + * Can be replaced by `Atomics.waitAsync` once it has better support. + */ +export async function createAsyncRingBuffer( + buffer: SharedArrayBuffer, + log?: (...data: unknown[]) => void, + debug?: boolean +) { + const worker = new Worker( + new URL('./ring-buffer-worker.ts', import.meta.url), + { + type: 'module', + } + ); + + const AsyncRingBuffer = wrap(worker); + return await new AsyncRingBuffer(buffer, log ? proxy(log) : undefined, debug); +} diff --git a/packages/c2w/src/ring-buffer/ring-buffer-worker.ts b/packages/c2w/src/ring-buffer/ring-buffer-worker.ts new file mode 100644 index 0000000..96bd0eb --- /dev/null +++ b/packages/c2w/src/ring-buffer/ring-buffer-worker.ts @@ -0,0 +1,4 @@ +import { expose } from 'comlink'; +import { RingBuffer } from './ring-buffer.js'; + +expose(RingBuffer); diff --git a/packages/c2w/src/ring-buffer/ring-buffer.ts b/packages/c2w/src/ring-buffer/ring-buffer.ts new file mode 100644 index 0000000..f68da91 --- /dev/null +++ b/packages/c2w/src/ring-buffer/ring-buffer.ts @@ -0,0 +1,240 @@ +import { asHex } from '../util.js'; + +// Constants for buffer configuration +const CONTROL_SIZE = 2; // READ_PTR and WRITE_PTR +const WRITE_PTR_INDEX = 0; +const READ_PTR_INDEX = 1; + +/** + * A lock-free ring buffer implementation using SharedArrayBuffer for cross-worker communication. + * The buffer layout consists of a control section and a data section: + * + * Control section (8 bytes): + * - 4 bytes: Write pointer (Int32) + * - 4 bytes: Read pointer (Int32) + * + * Data section: + * Continuous stream of bytes that can be partially read/written. + * The write and read pointers wrap around when they reach the end. + */ +export class RingBuffer { + readonly #control: Int32Array; + readonly #data: Uint8Array; + + // Logging function, helpful for debugging in web workers + #debug: (...data: unknown[]) => void = () => {}; + + /** + * Creates a new RingBuffer instance. + * @param sharedBuffer - The SharedArrayBuffer to use for storage + * @param log - Optional logging function + */ + constructor( + sharedBuffer: SharedArrayBuffer, + log?: (...data: unknown[]) => void, + debug?: boolean + ) { + if (debug && log) { + this.#debug = (...data: unknown[]) => log('RingBuffer:', ...data); + } + + // Ensure we have enough space for the control structure + const minSize = CONTROL_SIZE * Int32Array.BYTES_PER_ELEMENT; + if (sharedBuffer.byteLength < minSize) { + throw new Error( + `SharedArrayBuffer too small: need at least ${minSize} bytes for control structure` + ); + } + + // Initialize control structure + this.#control = new Int32Array(sharedBuffer, 0, CONTROL_SIZE); + + // Set up data region + const dataOffset = CONTROL_SIZE * Int32Array.BYTES_PER_ELEMENT; + this.#data = new Uint8Array(sharedBuffer, dataOffset); + + // Ensure we have at least some space for data + if (this.#data.length <= 1) { + throw new Error('Buffer too small: no space available for data'); + } + } + + /** + * Checks if there is data available to read. + */ + get hasData(): boolean { + return this.writePtr !== this.readPtr; + } + + /** + * Gets the current write pointer position. + */ + get writePtr(): number { + return Atomics.load(this.#control, WRITE_PTR_INDEX); + } + + /** + * Gets the current read pointer position. + */ + get readPtr(): number { + return Atomics.load(this.#control, READ_PTR_INDEX); + } + + /** + * Gets the buffer capacity in bytes. + */ + get capacity(): number { + return this.#data.length; + } + + /** + * Gets the number of bytes available to read. + */ + get availableData(): number { + const wrPtr = this.writePtr; + const rdPtr = this.readPtr; + return (wrPtr - rdPtr + this.capacity) % this.capacity; + } + + /** + * Calculates available free space in the buffer. + */ + get freeSpace(): number { + return this.#calculateFreeSpace(this.readPtr, this.writePtr); + } + + /** + * Writes data to the buffer. + * @throws {Error} If there isn't enough space + */ + write(data: Uint8Array): void { + const wrPtr = this.writePtr; + const rdPtr = this.readPtr; + + // Check if we have enough space + const available = this.#calculateFreeSpace(rdPtr, wrPtr); + if (data.length > available) { + throw new Error( + `Buffer full: need ${data.length} bytes, have ${available} bytes` + ); + } + + // Write the data and update the pointer + const newWrPtr = this.#writeData(data, wrPtr); + + this.#debug('Wrote data:', data.length, 'bytes'); + + // Update write pointer atomically + Atomics.store(this.#control, WRITE_PTR_INDEX, newWrPtr); + + // Wake up any waiting readers + Atomics.notify(this.#control, WRITE_PTR_INDEX, 1); + } + + /** + * Reads data from the buffer. Blocks if the buffer is empty. + * @param length - Number of bytes to read. If not specified, reads all available data. + * @returns The read data + */ + read(length?: number): Uint8Array { + if (length !== undefined && length <= 0) { + throw new Error(`Invalid read length: ${length}`); + } + + while (true) { + const wrPtr = this.writePtr; + let rdPtr = this.readPtr; + + if (wrPtr === rdPtr) { + // Buffer is empty, wait for the write pointer to change + Atomics.wait(this.#control, WRITE_PTR_INDEX, wrPtr); + continue; + } + + // Calculate available data + const available = (wrPtr - rdPtr + this.capacity) % this.capacity; + this.#debug('Data available:', available, 'bytes, requested:', length); + + // Read what we can + const readLength = length ? Math.min(length, available) : available; + const data = this.#readData(rdPtr, readLength); + + this.#debug('Read data:', asHex(data), ',', data.length, 'bytes'); + + // Update read pointer atomically + const newRdPtr = (rdPtr + readLength) % this.capacity; + Atomics.store(this.#control, READ_PTR_INDEX, newRdPtr); + + return data; + } + } + + /** + * Waits for data to be available in the ring buffer. + * @param timeout - Optional timeout in milliseconds + * @returns true if data is available, false if timed out + */ + waitForData(timeout?: number): boolean { + if (this.hasData) { + return true; + } + + const currentWritePtr = this.writePtr; + Atomics.wait(this.#control, WRITE_PTR_INDEX, currentWritePtr, timeout); + + return this.hasData; + } + + /** + * Calculates available free space between read and write pointers. + */ + #calculateFreeSpace(rdPtr: number, wrPtr: number): number { + // We reserve one byte to distinguish between full and empty buffer + return (rdPtr - wrPtr - 1 + this.capacity) % this.capacity; + } + + /** + * Writes data to the buffer, handling wrap-around using modulo. + * @returns New write pointer position + */ + #writeData(data: Uint8Array, startPos: number): number { + const capacity = this.capacity; + const endPos = (startPos + data.length) % capacity; + + if (endPos > startPos) { + // No wrap-around needed + this.#data.set(data, startPos); + } else { + // Handle wrap-around + const firstChunkSize = capacity - startPos; + this.#data.set(data.subarray(0, firstChunkSize), startPos); + this.#data.set(data.subarray(firstChunkSize), 0); + } + + return endPos; + } + + /** + * Reads data from the buffer, handling wrap-around using modulo. + */ + #readData(startPos: number, length: number): Uint8Array { + const result = new Uint8Array(length); + const capacity = this.capacity; + const endPos = (startPos + length) % capacity; + + if (endPos > startPos) { + // No wrap-around needed + result.set(this.#data.subarray(startPos, startPos + length)); + } else { + // Handle wrap-around + const firstChunkSize = capacity - startPos; + result.set(this.#data.subarray(startPos, startPos + firstChunkSize), 0); + result.set( + this.#data.subarray(0, length - firstChunkSize), + firstChunkSize + ); + } + + return result; + } +} diff --git a/packages/c2w/src/test.ts b/packages/c2w/src/test.ts new file mode 100644 index 0000000..9cfdc00 --- /dev/null +++ b/packages/c2w/src/test.ts @@ -0,0 +1,4 @@ +// self.postMessage('hello from test worker'); +export function test() { + return 'hello from test'; +} diff --git a/packages/c2w/src/types.ts b/packages/c2w/src/types.ts new file mode 100644 index 0000000..4f29d2d --- /dev/null +++ b/packages/c2w/src/types.ts @@ -0,0 +1,4 @@ +export interface DuplexStream { + readable: ReadableStream; + writable: WritableStream; +} diff --git a/packages/c2w/src/util.ts b/packages/c2w/src/util.ts new file mode 100644 index 0000000..a76f53a --- /dev/null +++ b/packages/c2w/src/util.ts @@ -0,0 +1,95 @@ +/** + * Parses a MAC address `Uint8Array` into a `string`. + */ +export function parseMacAddress(mac: Uint8Array) { + if (mac.length !== 6) { + throw new Error('invalid mac address'); + } + + return Array.from(mac) + .map((byte) => byte.toString(16).padStart(2, '0')) + .join(':'); +} + +/** + * Generates a random MAC address. + * + * The generated address is locally administered (so won't conflict + * with real devices) and unicast (so it can be used as a source address). + */ +export function generateMacAddress() { + const mac = new Uint8Array(6); + crypto.getRandomValues(mac); + + mac[0] = + // Clear the 2 least significant bits + (mac[0]! & 0b11111100) | + // Set locally administered bit (bit 1) to 1 and unicast bit (bit 0) to 0 + 0b00000010; + + return mac; +} + +/** + * Converts a `AsyncIterator` into an `ReadableStream`. + */ +export function toReadable(iterator: AsyncIterator) { + return new ReadableStream({ + async pull(controller) { + try { + const { value, done } = await iterator.next(); + if (done) { + controller.close(); + } else { + controller.enqueue(value); + } + } catch (err) { + controller.error(err); + } + }, + }); +} + +/** + * Converts a `ReadableStream` into an `AsyncIterableIterator`. + * + * Allows you to use ReadableStreams in a `for await ... of` loop. + */ +export function fromReadable( + readable: ReadableStream, + options?: { preventCancel?: boolean } +): AsyncIterableIterator { + const reader = readable.getReader(); + return fromReader(reader, options); +} + +/** + * Converts a `ReadableStreamDefaultReader` into an `AsyncIterableIterator`. + * + * Allows you to use Readers in a `for await ... of` loop. + */ +export async function* fromReader( + reader: ReadableStreamDefaultReader, + options?: { preventCancel?: boolean } +): AsyncIterableIterator { + try { + while (true) { + const { done, value } = await reader.read(); + if (done) { + return value; + } + yield value; + } + } finally { + if (!options?.preventCancel) { + await reader.cancel(); + } + reader.releaseLock(); + } +} + +export function asHex(data: Uint8Array, delimiter = ' ') { + return Array.from(data) + .map((byte) => byte.toString(16).padStart(2, '0')) + .join(delimiter); +} diff --git a/packages/c2w/src/wasi/socket-extension.ts b/packages/c2w/src/wasi/socket-extension.ts new file mode 100644 index 0000000..1240697 --- /dev/null +++ b/packages/c2w/src/wasi/socket-extension.ts @@ -0,0 +1,533 @@ +import { WASI, wasi as WasiDefs } from '@bjorn3/browser_wasi_shim'; + +export type WasiSocketOptions = { + listenFd: number; + connectionFd: number; + stdin: { + read(len: number): Uint8Array; + hasData(): boolean; + waitForData(timeout?: number): boolean; + }; + stdout: { + write(data: Uint8Array): void; + }; + stderr: { + write(data: Uint8Array): void; + }; + net: { + accept(): boolean; + send(data: Uint8Array): void; + receive(len: number): Uint8Array; + hasData(): boolean; + waitForData(timeout?: number): boolean; + }; +}; + +/** + * Extends the WASI implementation with socket function handlers. + */ +export function handleWasiSocket(wasi: WASI, options: WasiSocketOptions) { + const { listenFd, connectionFd, stdin, stdout, stderr, net } = options; + + /** + * Implements the `poll_oneoff` WASI syscall. + * Required to support socket and TTY operations on the c2w VM. + * + * `poll_oneoff` is a one-shot version of `poll` that allows the caller to + * wait for I/O events to occur on a set of file descriptors. + * + * Types of I/O events include: + * - Readable data available on a file descriptor (`fd_read`). + * - Writable space available on a file descriptor (`fd_write`). + * - A timeout (`clock`). + * Currently only supports `fd_read` and `clock` events. + * + * The function will block until one or more of the subscriptions parsed + * from the input pointer have occurred. It will return the events that + * have occurred, serialized to the output pointer. + */ + wasi.wasiImport.poll_oneoff = ( + in_ptr: number, + out_ptr: number, + nsubscriptions: number, + nevents_ptr: number + ) => { + if (nsubscriptions === 0) { + return ERRNO_INVAL; + } + + const buffer = new DataView(wasi.inst.exports.memory.buffer); + const subscriptions = parseSubscriptions(buffer, in_ptr, nsubscriptions); + + let clockSub: ClockSubscription | undefined; + let stdinSub: FdReadSubscription | undefined; + let socketSub: FdReadSubscription | undefined; + + let timeout = Number.MAX_VALUE; + + for (const sub of subscriptions) { + switch (sub.type) { + case 'fd_read': { + if (sub.fd !== 0 && sub.fd !== connectionFd) { + return ERRNO_INVAL; + } + if (sub.fd === 0) { + stdinSub = sub; + } else { + socketSub = sub; + } + break; + } + case 'clock': { + if (sub.timeout < timeout) { + timeout = sub.timeout; + clockSub = sub; + } + break; + } + default: + return ERRNO_INVAL; + } + } + + const events: PollEvent[] = []; + + if (clockSub || stdinSub || socketSub) { + // Nanoseconds to milliseconds + const timeoutMilliseconds = timeout / 1e6; + + if (stdinSub) { + const isStdinReadable = stdin.waitForData(timeoutMilliseconds); + + if (isStdinReadable) { + events.push({ + type: 'fd_read', + userdata: stdinSub.userdata, + error: 0, + }); + } + } + if (socketSub) { + const isSocketReadable = net.waitForData(timeoutMilliseconds); + + if (isSocketReadable) { + events.push({ + type: 'fd_read', + userdata: socketSub.userdata, + error: 0, + }); + } + } + + if (clockSub) { + events.push({ + type: 'clock', + userdata: clockSub.userdata, + error: 0, + }); + } + } + + // Serialize the events to the output pointer + serializeEvents(events, buffer, out_ptr); + + // Update the number of events pointer + buffer.setUint32(nevents_ptr, events.length, true); + + return 0; + }; + + // definition from wasi-libc https://github.com/WebAssembly/wasi-libc/blob/wasi-sdk-19/expected/wasm32-wasi/predefined-macros.txt + const ERRNO_INVAL = 28; + const ERRNO_AGAIN = 6; + + let hasActiveSocket = false; + + const fd_close = wasi.wasiImport.fd_close!; + + /** + * Augments `fd_close` to capture the closing of the connection socket. + */ + wasi.wasiImport.fd_close = (fd: number) => { + if (fd === connectionFd) { + hasActiveSocket = false; + return 0; + } + return fd_close.apply(wasi.wasiImport, [fd]); + }; + + const fd_read = wasi.wasiImport.fd_read!; + + /** + * Augments `fd_read` to handle reading from the connection socket. + */ + wasi.wasiImport.fd_read = ( + fd: number, + iovs_ptr: number, + iovs_len: number, + nread_ptr: number + ) => { + if (fd === 0) { + const buffer = new DataView(wasi.inst.exports.memory.buffer); + const buffer8 = new Uint8Array(wasi.inst.exports.memory.buffer); + const iovecs = WasiDefs.Iovec.read_bytes_array( + buffer, + iovs_ptr, + iovs_len + ); + let nread = 0; + for (let i = 0; i < iovecs.length; i++) { + const iovec = iovecs[i]!; + if (iovec.buf_len == 0) { + continue; + } + const data = stdin.read(iovec.buf_len); + buffer8.set(data, iovec.buf); + nread += data.length; + } + buffer.setUint32(nread_ptr, nread, true); + return 0; + } + if (fd === connectionFd) { + return wasi.wasiImport.sock_recv!( + fd, + iovs_ptr, + iovs_len, + 0, + nread_ptr, + 0 + ); + } + return fd_read.apply(wasi.wasiImport, [fd, iovs_ptr, iovs_len, nread_ptr]); + }; + + const fd_write = wasi.wasiImport.fd_write!; + + /** + * Augments `fd_write` to handle writing to the connection socket. + */ + wasi.wasiImport.fd_write = ( + fd: number, + iovs_ptr: number, + iovs_len: number, + nwritten_ptr: number + ) => { + if (fd == 1 || fd == 2) { + const buffer = new DataView(wasi.inst.exports.memory.buffer); + const buffer8 = new Uint8Array(wasi.inst.exports.memory.buffer); + const iovecs = WasiDefs.Ciovec.read_bytes_array( + buffer, + iovs_ptr, + iovs_len + ); + let wtotal = 0; + for (let i = 0; i < iovecs.length; i++) { + const iovec = iovecs[i]!; + const buf = buffer8.slice(iovec.buf, iovec.buf + iovec.buf_len); + if (buf.length == 0) { + continue; + } + if (fd == 1) { + stdout.write(buf); + } else { + stderr.write(buf); + } + wtotal += buf.length; + } + buffer.setUint32(nwritten_ptr, wtotal, true); + return 0; + } + if (fd === connectionFd) { + return wasi.wasiImport.sock_send!( + fd, + iovs_ptr, + iovs_len, + 0, + nwritten_ptr + ); + } + return fd_write.apply(wasi.wasiImport, [ + fd, + iovs_ptr, + iovs_len, + nwritten_ptr, + ]); + }; + + const fd_fdstat_get = wasi.wasiImport.fd_fdstat_get!; + + /** + * Augments `fd_fdstat_get` to provide information about the socket file descriptors. + */ + wasi.wasiImport.fd_fdstat_get = (fd: number, fdstat_ptr: number) => { + if (fd === listenFd || (fd === connectionFd && hasActiveSocket)) { + let buffer = new DataView(wasi.inst.exports.memory.buffer); + + // https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fdstat-struct + buffer.setUint8(fdstat_ptr, 6); // filetype = 6 (socket_stream) + buffer.setUint8(fdstat_ptr + 1, 2); // fdflags = 2 (nonblock) + + return 0; + } + return fd_fdstat_get.apply(wasi.wasiImport, [fd, fdstat_ptr]); + }; + + const fd_prestat_get = wasi.wasiImport.fd_prestat_get!; + + /** + * Augments `fd_prestat_get` to provide information about the socket file descriptors. + */ + wasi.wasiImport.fd_prestat_get = (fd: number, prestat_ptr: number) => { + if (fd === listenFd || fd === connectionFd) { + // reserve socket-related fds + let buffer = new DataView(wasi.inst.exports.memory.buffer); + buffer.setUint8(prestat_ptr, 1); + + return 0; + } + return fd_prestat_get.apply(wasi.wasiImport, [fd, prestat_ptr]); + }; + + /** + * Implements the `sock_accept` WASI syscall. + * + * Accepts a connection on a socket. + */ + wasi.wasiImport.sock_accept = (fd: number, flags: number, fd_ptr: number) => { + if (fd !== listenFd) { + console.log('sock_accept: unknown fd ' + fd); + return ERRNO_INVAL; + } + + if (hasActiveSocket) { + console.log('sock_accept: multi-connection is unsupported'); + return ERRNO_INVAL; + } + + if (!net.accept()) { + return ERRNO_AGAIN; + } + + hasActiveSocket = true; + const buffer = new DataView(wasi.inst.exports.memory.buffer); + buffer.setUint32(fd_ptr, connectionFd, true); + + return 0; + }; + + /** + * Implements the `sock_send` WASI syscall. + * + * Sends data on a socket. + */ + wasi.wasiImport.sock_send = ( + fd: number, + iovs_ptr: number, + iovs_len: number, + si_flags: number, + nwritten_ptr: number + ) => { + if (fd !== connectionFd) { + console.log('sock_send: unknown fd ' + fd); + return ERRNO_INVAL; + } + + const buffer = new DataView(wasi.inst.exports.memory.buffer); + const buffer8 = new Uint8Array(wasi.inst.exports.memory.buffer); + const iovecs = WasiDefs.Ciovec.read_bytes_array(buffer, iovs_ptr, iovs_len); + + let wtotal = 0; + + for (let i = 0; i < iovecs.length; i++) { + const iovec = iovecs[i]!; + const buf = buffer8.slice(iovec.buf, iovec.buf + iovec.buf_len); + if (buf.length === 0) { + continue; + } + + try { + net.send(buf); + } catch (error) { + console.log('sock_send: error ' + error); + return ERRNO_INVAL; + } + + wtotal += buf.length; + } + + buffer.setUint32(nwritten_ptr, wtotal, true); + + return 0; + }; + + /** + * Implements the `sock_recv` WASI syscall. + * + * Receives data on a socket. + */ + wasi.wasiImport.sock_recv = ( + fd: number, + iovs_ptr: number, + iovs_len: number, + ri_flags: number, + nread_ptr: number, + ro_flags_ptr: number + ) => { + if (ri_flags !== 0) { + console.log('ri_flags are unsupported'); // TODO + } + + if (fd !== connectionFd) { + console.log('sock_recv: unknown fd ' + fd); + return ERRNO_INVAL; + } + + if (!net.hasData()) { + return ERRNO_AGAIN; + } + + const buffer = new DataView(wasi.inst.exports.memory.buffer); + const buffer8 = new Uint8Array(wasi.inst.exports.memory.buffer); + const iovecs = WasiDefs.Iovec.read_bytes_array(buffer, iovs_ptr, iovs_len); + + let nread = 0; + + for (let i = 0; i < iovecs.length; i++) { + const iovec = iovecs[i]; + if (!iovec || iovec.buf_len === 0) { + continue; + } + + const data = net.receive(iovec.buf_len); + + buffer8.set(data, iovec.buf); + nread += data.length; + } + + buffer.setUint32(nread_ptr, nread, true); + // TODO: support ro_flags_ptr + + return 0; + }; + + /** + * Implements the `sock_recv_is_readable` WASI syscall. + * + * Captures the closing of the connection socket. + */ + wasi.wasiImport.sock_shutdown = (fd: number, sdflags: number) => { + if (fd === connectionFd) { + hasActiveSocket = false; + } + return 0; + }; +} + +type BaseSubscription = { + userdata: bigint; +}; + +type ClockSubscription = BaseSubscription & { + type: 'clock'; + timeout: number; +}; + +type FdReadSubscription = BaseSubscription & { + type: 'fd_read'; + fd: number; +}; + +type FdWriteSubscription = BaseSubscription & { + type: 'fd_write'; + fd: number; +}; + +type Subscription = + | ClockSubscription + | FdReadSubscription + | FdWriteSubscription; + +type EventType = Subscription['type']; + +type PollEvent = { + type: EventType; + userdata: bigint; + error: number; +}; + +function serializeEvent(event: PollEvent, view: DataView, ptr: number) { + view.setBigUint64(ptr, event.userdata, true); + view.setUint8(ptr + 8, event.error); + view.setUint8(ptr + 9, 0); + view.setUint8(ptr + 10, serializeEventType(event.type)); +} + +function serializeEvents(events: PollEvent[], view: DataView, ptr: number) { + for (let i = 0; i < events.length; i++) { + serializeEvent(events[i]!, view, ptr + 32 * i); + } +} + +const EVENTTYPE_CLOCK = 0; +const EVENTTYPE_FD_READ = 1; +const EVENTTYPE_FD_WRITE = 2; + +function parseEventType(data: number): EventType { + switch (data) { + case EVENTTYPE_CLOCK: + return 'clock'; + case EVENTTYPE_FD_READ: + return 'fd_read'; + case EVENTTYPE_FD_WRITE: + return 'fd_write'; + default: + throw new Error(`invalid event type ${data}`); + } +} + +function serializeEventType(eventType: EventType) { + switch (eventType) { + case 'clock': + return EVENTTYPE_CLOCK; + case 'fd_read': + return EVENTTYPE_FD_READ; + case 'fd_write': + return EVENTTYPE_FD_WRITE; + default: + throw new Error('unreachable'); + } +} + +function parseSubscription(view: DataView, ptr: number): Subscription { + const userdata = view.getBigUint64(ptr, true); + const type = parseEventType(view.getUint8(ptr + 8)); + switch (type) { + case 'clock': + const timeout = Number(view.getBigUint64(ptr + 16, true)); + return { userdata, type, timeout }; + case 'fd_read': + case 'fd_write': + const fd = view.getUint32(ptr + 16, true); + return { userdata, type, fd }; + default: + throw new Error(`invalid event type ${type}`); + } +} + +function parseSubscriptions( + view: DataView, + ptr: number, + len: number +): Subscription[] { + const subscriptions = []; + for (let i = 0; i < len; i++) { + subscriptions.push(parseSubscription(view, ptr + 48 * i)); + } + return subscriptions; +} + +function asHex(data: Uint8Array, delimiter = ' ') { + return Array.from(data) + .map((byte) => byte.toString(16).padStart(2, '0')) + .join(delimiter); +} diff --git a/packages/c2w/tsconfig.json b/packages/c2w/tsconfig.json new file mode 100644 index 0000000..1d0215a --- /dev/null +++ b/packages/c2w/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "@total-typescript/tsconfig/bundler/dom/library", + "include": ["src/**/*.ts"] +} diff --git a/packages/c2w/tsup.config.ts b/packages/c2w/tsup.config.ts new file mode 100644 index 0000000..2b2cc17 --- /dev/null +++ b/packages/c2w/tsup.config.ts @@ -0,0 +1,19 @@ +import workerPlugin from '@chialab/esbuild-plugin-worker'; +import { defineConfig } from 'tsup'; + +export default defineConfig([ + { + entry: ['src/index.ts'], + format: ['cjs', 'esm'], + outDir: 'dist', + sourcemap: true, + dts: true, + minify: true, + splitting: true, + external: ['node:fs', 'node:stream'], + esbuildPlugins: [workerPlugin()], + esbuildOptions: (options) => { + options.inject = ['./patches/web-worker.ts']; + }, + }, +]); diff --git a/packages/c2w/vitest.config.ts b/packages/c2w/vitest.config.ts new file mode 100644 index 0000000..a37e2c9 --- /dev/null +++ b/packages/c2w/vitest.config.ts @@ -0,0 +1,25 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + server: { + headers: { + 'Cross-Origin-Opener-Policy': 'same-origin', + 'Cross-Origin-Embedder-Policy': 'require-corp', + }, + }, + test: { + include: ['src/**/*.{test,spec}.ts'], + browser: { + enabled: true, + provider: 'playwright', + instances: [ + { + browser: 'chromium', + }, + ], + headless: true, + screenshotFailures: false, + }, + forceRerunTriggers: ['**/*-worker.ts'], + }, +}); diff --git a/packages/v86/package.json b/packages/v86/package.json index 098d957..5cb6dcd 100644 --- a/packages/v86/package.json +++ b/packages/v86/package.json @@ -21,7 +21,7 @@ "dependencies": {}, "devDependencies": { "@total-typescript/tsconfig": "^1.0.4", - "tcpip": "^0.2.0-dev.1", + "tcpip": "0.2", "typescript": "^5.0.4" } }