From 1c46fe92cdcb644516b0158f3e15da5c02258f5d Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Wed, 18 Mar 2026 13:49:09 -0400 Subject: [PATCH 01/29] Add benchmarking for pass through reqeusts flag --- package.json | 5 +- pnpm-lock.yaml | 257 ++++++++++++++++++- scripts/bench/passthrough-requests.bench.mjs | 139 ++++++++++ 3 files changed, 395 insertions(+), 6 deletions(-) create mode 100644 scripts/bench/passthrough-requests.bench.mjs diff --git a/package.json b/package.json index 581823b18f..29df2cb62a 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,6 @@ "@typescript-eslint/parser": "^7.5.0", "babel-jest": "^29.7.0", "babel-plugin-dev-expression": "^0.2.3", - "picocolors": "^1.1.1", "dox": "^1.0.0", "eslint": "^8.57.0", "eslint-config-react-app": "^7.0.1", @@ -81,6 +80,7 @@ "isbot": "^5.1.11", "jest": "^29.6.4", "jsonfile": "^6.1.0", + "picocolors": "^1.1.1", "prettier": "^3.6.2", "prompts": "^2.4.2", "remark-gfm": "^3.0.1", @@ -91,7 +91,8 @@ "typescript": "catalog:", "unified": "^10.1.2", "unist-util-remove": "^3.1.0", - "vite": "^6.3.0" + "vite": "^6.3.0", + "vitest": "^4.1.0" }, "engines": { "node": ">=20.0.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bc26854688..169498e075 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -144,7 +144,7 @@ importers: version: 7.34.1(eslint@8.57.0) eslint-plugin-react-hooks: specifier: next - version: 7.1.0-canary-e0cc7202-20260227(eslint@8.57.0) + version: 7.1.0-canary-b4546cd0-20260318(eslint@8.57.0) fast-glob: specifier: 3.2.11 version: 3.2.11 @@ -193,6 +193,10 @@ importers: vite: specifier: ^6.3.0 version: 6.4.1(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.19.3)(yaml@2.8.0) + devDependencies: + vitest: + specifier: ^4.1.0 + version: 4.1.0(@types/node@22.14.0)(jsdom@22.1.0)(msw@2.7.5(@types/node@22.14.0)(typescript@5.4.5))(vite@6.4.1(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.19.3)(yaml@2.8.0)) integration: dependencies: @@ -3898,6 +3902,9 @@ packages: '@sinonjs/fake-timers@10.0.2': resolution: {integrity: sha512-SwUDyjWnah1AaNl7kxsa7cfLhlTYoiyhDAIgyh+El30YvXs/o7OLXpYH88Zdhyx9JExKrmHDJ+10bwIcY80Jmw==} + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@swc/core-darwin-arm64@1.11.24': resolution: {integrity: sha512-dhtVj0PC1APOF4fl5qT2neGjRLgHAAYfiVP8poJelhzhB/318bO+QCFWAiimcDoyMgpCXOhTp757gnoJJrheWA==} engines: {node: '>=10'} @@ -4036,6 +4043,9 @@ packages: '@types/body-parser@1.19.5': resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/compression@1.8.1': resolution: {integrity: sha512-kCFuWS0ebDbmxs0AXYn6e2r2nrGAb5KwQhknjSPSPgJcGd8+HVSILlUyFhGqML2gk39HcG7D1ydW9/qpYkN00Q==} @@ -4057,6 +4067,9 @@ packages: '@types/dedent@0.7.2': resolution: {integrity: sha512-kRiitIeUg1mPV9yH4VUJ/1uk2XjyANfeL8/7rH1tsjvHeO9PJLBHJIYsFWmAvmGj5u8rj+1TZx7PZzW2qLw3Lw==} + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/eslint-scope@3.7.7': resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} @@ -4435,6 +4448,35 @@ packages: react-server-dom-webpack: optional: true + '@vitest/expect@4.1.0': + resolution: {integrity: sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==} + + '@vitest/mocker@4.1.0': + resolution: {integrity: sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.0': + resolution: {integrity: sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==} + + '@vitest/runner@4.1.0': + resolution: {integrity: sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ==} + + '@vitest/snapshot@4.1.0': + resolution: {integrity: sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg==} + + '@vitest/spy@4.1.0': + resolution: {integrity: sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==} + + '@vitest/utils@4.1.0': + resolution: {integrity: sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==} + '@web3-storage/multipart-parser@1.0.0': resolution: {integrity: sha512-BEO6al7BYqcnfX15W2cnGR+Q566ACXAT9UQykORCWW80lmkpWsnEob6zJS1ZVBKsSJC8+7vJkHwlp+lXG1UCdw==} @@ -4674,6 +4716,10 @@ packages: asap@2.0.6: resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + ast-types-flow@0.0.8: resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} @@ -4904,6 +4950,10 @@ packages: ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} @@ -5616,8 +5666,8 @@ packages: peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 - eslint-plugin-react-hooks@7.1.0-canary-e0cc7202-20260227: - resolution: {integrity: sha512-Kg4EiP6olCKf9zrf3TGaMfyQfUOADsQDFa6q3Cfv+Fr47dQhOtbq6FkkyNZJEb+yz8kGrJJmIPKb+0Q2f+FrZw==} + eslint-plugin-react-hooks@7.1.0-canary-b4546cd0-20260318: + resolution: {integrity: sha512-VvZ4Ssq54/U7Y5jfp8HnruGoeepoV5gbLgEHD4HRdvRztXU8Mwx6XAlRbAxvmw5b3ZLdDa8kzbUxVJUgAMb4kA==} engines: {node: '>=18'} peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0 @@ -5752,6 +5802,10 @@ packages: resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} engines: {node: '>= 0.8.0'} + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + expect@29.7.0: resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -5809,6 +5863,15 @@ packages: picomatch: optional: true + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + figures@6.1.0: resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} engines: {node: '>=18'} @@ -7402,6 +7465,9 @@ packages: resolution: {integrity: sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==} engines: {node: '>= 0.4'} + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + ohash@2.0.11: resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} @@ -7574,6 +7640,10 @@ packages: resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} engines: {node: '>=12'} + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + pify@2.3.0: resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} engines: {node: '>=0.10.0'} @@ -8239,6 +8309,9 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -8338,6 +8411,9 @@ packages: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + stacktracey@2.1.8: resolution: {integrity: sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==} @@ -8345,6 +8421,9 @@ packages: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} + std-env@4.0.0: + resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==} + stoppable@1.1.0: resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==} engines: {node: '>=4', npm: '>=6'} @@ -8525,13 +8604,28 @@ packages: tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + tinyexec@1.0.4: + resolution: {integrity: sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==} + engines: {node: '>=18'} + tinyglobby@0.2.14: resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} engines: {node: '>=12.0.0'} + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + tmp@0.0.33: resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} engines: {node: '>=0.6.0'} @@ -9042,6 +9136,41 @@ packages: vite: optional: true + vitest@4.1.0: + resolution: {integrity: sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.0 + '@vitest/browser-preview': 4.1.0 + '@vitest/browser-webdriverio': 4.1.0 + '@vitest/ui': 4.1.0 + happy-dom: '*' + jsdom: 22.1.0 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0-0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + w3c-xmlserializer@4.0.0: resolution: {integrity: sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==} engines: {node: '>=14'} @@ -9151,6 +9280,11 @@ packages: engines: {node: '>= 8'} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + wireit@0.14.9: resolution: {integrity: sha512-hFc96BgyslfO1WGSzQqOVYd5N3TB+4u9w70L9GHR/T7SYjvFmeznkYMsRIjMLhPcVabCEYPW1vV66wmIVDs+dQ==} engines: {node: '>=18.0.0'} @@ -11381,6 +11515,8 @@ snapshots: dependencies: '@sinonjs/commons': 2.0.0 + '@standard-schema/spec@1.1.0': {} + '@swc/core-darwin-arm64@1.11.24': optional: true @@ -11514,6 +11650,11 @@ snapshots: '@types/connect': 3.4.38 '@types/node': 22.14.0 + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + '@types/compression@1.8.1': dependencies: '@types/express': 4.17.21 @@ -11537,6 +11678,8 @@ snapshots: '@types/dedent@0.7.2': {} + '@types/deep-eql@4.0.2': {} + '@types/eslint-scope@3.7.7': dependencies: '@types/eslint': 9.6.1 @@ -12154,6 +12297,48 @@ snapshots: optionalDependencies: react-server-dom-webpack: 19.2.3(react-dom@19.3.0-canary-d763f313-20251210(react@19.3.0-canary-d763f313-20251210))(react@19.3.0-canary-d763f313-20251210)(webpack@5.103.0(@swc/core@1.11.24)(esbuild@0.25.4)) + '@vitest/expect@4.1.0': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.0 + '@vitest/utils': 4.1.0 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.0(msw@2.7.5(@types/node@22.14.0)(typescript@5.4.5))(vite@6.4.1(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.19.3)(yaml@2.8.0))': + dependencies: + '@vitest/spy': 4.1.0 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + msw: 2.7.5(@types/node@22.14.0)(typescript@5.4.5) + vite: 6.4.1(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.19.3)(yaml@2.8.0) + + '@vitest/pretty-format@4.1.0': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.0': + dependencies: + '@vitest/utils': 4.1.0 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.0': + dependencies: + '@vitest/pretty-format': 4.1.0 + '@vitest/utils': 4.1.0 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.0': {} + + '@vitest/utils@4.1.0': + dependencies: + '@vitest/pretty-format': 4.1.0 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + '@web3-storage/multipart-parser@1.0.0': {} '@webassemblyjs/ast@1.14.1': @@ -12436,6 +12621,8 @@ snapshots: asap@2.0.6: {} + assertion-error@2.0.1: {} + ast-types-flow@0.0.8: {} ast-types@0.13.4: @@ -12743,6 +12930,8 @@ snapshots: ccount@2.0.1: {} + chai@6.2.2: {} + chalk@2.4.2: dependencies: ansi-styles: 3.2.1 @@ -13638,7 +13827,7 @@ snapshots: dependencies: eslint: 8.57.0 - eslint-plugin-react-hooks@7.1.0-canary-e0cc7202-20260227(eslint@8.57.0): + eslint-plugin-react-hooks@7.1.0-canary-b4546cd0-20260318(eslint@8.57.0): dependencies: '@babel/core': 7.27.7 '@babel/parser': 7.27.7 @@ -13847,6 +14036,8 @@ snapshots: exit@0.1.2: {} + expect-type@1.3.0: {} + expect@29.7.0: dependencies: '@jest/expect-utils': 29.7.0 @@ -13937,6 +14128,10 @@ snapshots: optionalDependencies: picomatch: 4.0.2 + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + figures@6.1.0: dependencies: is-unicode-supported: 2.1.0 @@ -16137,6 +16332,8 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + obug@2.1.1: {} + ohash@2.0.11: {} on-finished@2.3.0: @@ -16312,6 +16509,8 @@ snapshots: picomatch@4.0.2: {} + picomatch@4.0.3: {} + pify@2.3.0: {} pify@4.0.1: {} @@ -17053,6 +17252,8 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} + signal-exit@3.0.7: {} signal-exit@4.1.0: {} @@ -17159,6 +17360,8 @@ snapshots: dependencies: escape-string-regexp: 2.0.0 + stackback@0.0.2: {} + stacktracey@2.1.8: dependencies: as-table: 1.0.55 @@ -17166,6 +17369,8 @@ snapshots: statuses@2.0.1: {} + std-env@4.0.0: {} + stoppable@1.1.0: {} stream-shift@1.0.3: {} @@ -17390,13 +17595,24 @@ snapshots: tiny-invariant@1.3.3: {} + tinybench@2.9.0: {} + tinyexec@0.3.2: {} + tinyexec@1.0.4: {} + tinyglobby@0.2.14: dependencies: fdir: 6.4.6(picomatch@4.0.2) picomatch: 4.0.2 + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tinyrainbow@3.1.0: {} + tmp@0.0.33: dependencies: os-tmpdir: 1.0.2 @@ -18037,6 +18253,34 @@ snapshots: optionalDependencies: vite: 6.4.1(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.19.3)(yaml@2.8.0) + vitest@4.1.0(@types/node@22.14.0)(jsdom@22.1.0)(msw@2.7.5(@types/node@22.14.0)(typescript@5.4.5))(vite@6.4.1(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.19.3)(yaml@2.8.0)): + dependencies: + '@vitest/expect': 4.1.0 + '@vitest/mocker': 4.1.0(msw@2.7.5(@types/node@22.14.0)(typescript@5.4.5))(vite@6.4.1(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.19.3)(yaml@2.8.0)) + '@vitest/pretty-format': 4.1.0 + '@vitest/runner': 4.1.0 + '@vitest/snapshot': 4.1.0 + '@vitest/spy': 4.1.0 + '@vitest/utils': 4.1.0 + es-module-lexer: 2.0.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 4.0.0 + tinybench: 2.9.0 + tinyexec: 1.0.4 + tinyglobby: 0.2.15 + tinyrainbow: 3.1.0 + vite: 6.4.1(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.19.3)(yaml@2.8.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.14.0 + jsdom: 22.1.0 + transitivePeerDependencies: + - msw + w3c-xmlserializer@4.0.0: dependencies: xml-name-validator: 4.0.0 @@ -18225,6 +18469,11 @@ snapshots: dependencies: isexe: 2.0.0 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + wireit@0.14.9: dependencies: brace-expansion: 4.0.0 diff --git a/scripts/bench/passthrough-requests.bench.mjs b/scripts/bench/passthrough-requests.bench.mjs new file mode 100644 index 0000000000..4b38c124f1 --- /dev/null +++ b/scripts/bench/passthrough-requests.bench.mjs @@ -0,0 +1,139 @@ +/** + * Benchmark: unstable_passThroughRequests flag + * + * Measures the overhead of creating new Request objects on each server handler + * invocation (default behavior) vs passing the original request through. + * + * Tests three request types: + * 1. Document requests (GET /) + * 2. Single-fetch loader requests (GET /_root.data) + * 3. Single-fetch action requests (POST /_root.data) + * + * Usage: pnpm vitest bench scripts/bench/passthrough-requests.mjs + */ + +import { bench, describe } from "vitest"; +import { createRequestHandler } from "../../packages/react-router/dist/production/index.js"; + +// --------------------------------------------------------------------------- +// Minimal server build factory +// --------------------------------------------------------------------------- + +function mockServerBuild(future = {}) { + const routeId = "root"; + + return { + ssr: true, + future: { + v8_middleware: false, + unstable_subResourceIntegrity: false, + unstable_passThroughRequests: false, + ...future, + }, + prerender: [], + isSpaMode: false, + routeDiscovery: { mode: "lazy", manifestPath: "/__manifest" }, + assetsBuildDirectory: "", + publicPath: "", + assets: { + entry: { imports: [""], module: "" }, + routes: { + [routeId]: { + hasAction: true, + hasErrorBoundary: false, + hasLoader: true, + hasClientAction: false, + hasClientLoader: false, + hasClientMiddleware: false, + clientActionModule: undefined, + clientLoaderModule: undefined, + clientMiddlewareModule: undefined, + hydrateFallbackModule: undefined, + id: routeId, + module: "", + index: undefined, + path: "", + parentId: undefined, + }, + }, + url: "", + version: "v1", + }, + entry: { + module: { + default: async (_request, statusCode, headers) => + new Response(null, { status: statusCode, headers }), + handleDataRequest: async (response) => response, + handleError: undefined, + unstable_instrumentations: undefined, + }, + }, + routes: { + [routeId]: { + id: routeId, + index: undefined, + path: "", + parentId: undefined, + module: { + default: () => null, + ErrorBoundary: undefined, + action: () => new Response("action"), + loader: () => new Response("loader"), + middleware: undefined, + }, + }, + }, + }; +} + +// --------------------------------------------------------------------------- +// Handlers +// --------------------------------------------------------------------------- + +const handlerDefault = createRequestHandler( + mockServerBuild({ unstable_passThroughRequests: false }), + "production", +); + +const handlerPassThrough = createRequestHandler( + mockServerBuild({ unstable_passThroughRequests: true }), + "production", +); + +// --------------------------------------------------------------------------- +// Benchmarks +// --------------------------------------------------------------------------- + +describe("GET / (document request)", () => { + bench("passThroughRequests: false", async () => { + await handlerDefault(new Request("http://localhost:3000/")); + }); + + bench("passThroughRequests: true", async () => { + await handlerPassThrough(new Request("http://localhost:3000/")); + }); +}); + +describe("GET /_root.data (single-fetch loaders)", () => { + bench("passThroughRequests: false", async () => { + await handlerDefault(new Request("http://localhost:3000/_root.data")); + }); + + bench("passThroughRequests: true", async () => { + await handlerPassThrough(new Request("http://localhost:3000/_root.data")); + }); +}); + +describe("POST /_root.data (single-fetch action)", () => { + bench("passThroughRequests: false", async () => { + await handlerDefault( + new Request("http://localhost:3000/_root.data", { method: "POST" }), + ); + }); + + bench("passThroughRequests: true", async () => { + await handlerPassThrough( + new Request("http://localhost:3000/_root.data", { method: "POST" }), + ); + }); +}); From 6f683e177df319a8a4c275aee37013d7bb17c68d Mon Sep 17 00:00:00 2001 From: Remix Run Bot Date: Wed, 18 Mar 2026 17:49:53 +0000 Subject: [PATCH 02/29] chore: deduplicate `pnpm-lock.yaml` --- pnpm-lock.yaml | 62 +++++++++++++++----------------------------------- 1 file changed, 18 insertions(+), 44 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 169498e075..1191fabb4b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -193,7 +193,6 @@ importers: vite: specifier: ^6.3.0 version: 6.4.1(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.19.3)(yaml@2.8.0) - devDependencies: vitest: specifier: ^4.1.0 version: 4.1.0(@types/node@22.14.0)(jsdom@22.1.0)(msw@2.7.5(@types/node@22.14.0)(typescript@5.4.5))(vite@6.4.1(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.19.3)(yaml@2.8.0)) @@ -1086,7 +1085,7 @@ importers: version: 7.7.2 tinyglobby: specifier: ^0.2.14 - version: 0.2.14 + version: 0.2.15 valibot: specifier: ^1.2.0 version: 1.2.0(typescript@5.4.5) @@ -5855,14 +5854,6 @@ packages: fb-watchman@2.0.2: resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} - fdir@6.4.6: - resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==} - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -7636,10 +7627,6 @@ packages: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} - picomatch@4.0.2: - resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} - engines: {node: '>=12'} - picomatch@4.0.3: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} @@ -8073,6 +8060,7 @@ packages: rolldown-vite@6.3.0-beta.3: resolution: {integrity: sha512-pJrHAajTO0PFqXSdwCkMjTIS/yR6MBd/0sueficbnJzYZYndjGCesntEqG/05vXaPkJ5NQC2FdtpiZgg6OkAMA==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + deprecated: Use 7.3.1 for migration purposes. For the most recent updates, migrate to Vite 8 once you're ready. hasBin: true peerDependencies: '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 @@ -8113,6 +8101,7 @@ packages: rolldown-vite@6.3.0-beta.5: resolution: {integrity: sha512-/seCUlTV3pHNn0Y8qveGmHMNYxH/Z9xc65Ov0uaA/HtThaMZNTacWsMyDG4SA+S/c1RdpWIe85E5NeOmhywrGg==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + deprecated: Use 7.3.1 for migration purposes. For the most recent updates, migrate to Vite 8 once you're ready. hasBin: true peerDependencies: '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 @@ -8614,10 +8603,6 @@ packages: resolution: {integrity: sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==} engines: {node: '>=18'} - tinyglobby@0.2.14: - resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} - engines: {node: '>=12.0.0'} - tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} @@ -10528,7 +10513,7 @@ snapshots: get-port: 7.1.0 miniflare: 4.20250617.5 picocolors: 1.1.1 - tinyglobby: 0.2.14 + tinyglobby: 0.2.15 unenv: 2.0.0-rc.17 vite: 6.4.1(@types/node@20.11.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.19.3)(yaml@2.8.0) wrangler: 4.23.0(@cloudflare/workers-types@4.20250805.0) @@ -14124,10 +14109,6 @@ snapshots: dependencies: bser: 2.1.1 - fdir@6.4.6(picomatch@4.0.2): - optionalDependencies: - picomatch: 4.0.2 - fdir@6.5.0(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 @@ -16507,8 +16488,6 @@ snapshots: picomatch@2.3.1: {} - picomatch@4.0.2: {} - picomatch@4.0.3: {} pify@2.3.0: {} @@ -16961,10 +16940,10 @@ snapshots: dependencies: '@oxc-project/runtime': 0.61.2 lightningcss: 1.30.1 - picomatch: 4.0.2 + picomatch: 4.0.3 postcss: 8.5.3 rolldown: 1.0.0-beta.7-commit.7452fa0(@oxc-project/runtime@0.61.2)(typescript@5.4.5) - tinyglobby: 0.2.14 + tinyglobby: 0.2.15 optionalDependencies: '@types/node': 22.14.0 esbuild: 0.25.4 @@ -16980,10 +16959,10 @@ snapshots: dependencies: '@oxc-project/runtime': 0.61.2 lightningcss: 1.30.1 - picomatch: 4.0.2 + picomatch: 4.0.3 postcss: 8.5.3 rolldown: 1.0.0-beta.7-commit.e117288(@oxc-project/runtime@0.61.2)(typescript@5.4.5) - tinyglobby: 0.2.14 + tinyglobby: 0.2.15 optionalDependencies: '@types/node': 22.14.0 esbuild: 0.25.4 @@ -17601,11 +17580,6 @@ snapshots: tinyexec@1.0.4: {} - tinyglobby@0.2.14: - dependencies: - fdir: 6.4.6(picomatch@4.0.2) - picomatch: 4.0.2 - tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.3) @@ -17692,7 +17666,7 @@ snapshots: source-map: 0.8.0-beta.0 sucrase: 3.35.0 tinyexec: 0.3.2 - tinyglobby: 0.2.14 + tinyglobby: 0.2.15 tree-kill: 1.2.2 optionalDependencies: '@swc/core': 1.11.24 @@ -18197,11 +18171,11 @@ snapshots: vite@6.4.1(@types/node@20.11.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.19.3)(yaml@2.8.0): dependencies: esbuild: 0.25.0 - fdir: 6.4.6(picomatch@4.0.2) - picomatch: 4.0.2 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 postcss: 8.5.3 rollup: 4.43.0 - tinyglobby: 0.2.14 + tinyglobby: 0.2.15 optionalDependencies: '@types/node': 20.11.30 fsevents: 2.3.3 @@ -18214,11 +18188,11 @@ snapshots: vite@6.4.1(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.19.3)(yaml@2.8.0): dependencies: esbuild: 0.25.0 - fdir: 6.4.6(picomatch@4.0.2) - picomatch: 4.0.2 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 postcss: 8.5.3 rollup: 4.43.0 - tinyglobby: 0.2.14 + tinyglobby: 0.2.15 optionalDependencies: '@types/node': 22.14.0 fsevents: 2.3.3 @@ -18231,11 +18205,11 @@ snapshots: vite@7.0.0-beta.0(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.19.3)(yaml@2.8.0): dependencies: esbuild: 0.25.0 - fdir: 6.4.6(picomatch@4.0.2) - picomatch: 4.0.2 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 postcss: 8.5.3 rollup: 4.43.0 - tinyglobby: 0.2.14 + tinyglobby: 0.2.15 optionalDependencies: '@types/node': 22.14.0 fsevents: 2.3.3 From b897f185fbb5614acb2c3d9861a3586d367c4526 Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Wed, 18 Mar 2026 21:36:38 -0700 Subject: [PATCH 03/29] fix: turbo-stream v2 remove recursion to allow for massive payloads (#14838) --- .changeset/sweet-trees-visit.md | 5 + .../__tests__/vendor/turbo-stream-test.ts | 102 +++++++------ .../vendor/turbo-stream-v2/flatten.ts | 72 +++++++--- .../vendor/turbo-stream-v2/turbo-stream.ts | 136 ++++++++++-------- 4 files changed, 190 insertions(+), 125 deletions(-) create mode 100644 .changeset/sweet-trees-visit.md diff --git a/.changeset/sweet-trees-visit.md b/.changeset/sweet-trees-visit.md new file mode 100644 index 0000000000..d0d3d74281 --- /dev/null +++ b/.changeset/sweet-trees-visit.md @@ -0,0 +1,5 @@ +--- +"react-router": patch +--- + +Remove recursion from turbo-stream v2 allowing for encoding / decoding of massive payloads. diff --git a/packages/react-router/__tests__/vendor/turbo-stream-test.ts b/packages/react-router/__tests__/vendor/turbo-stream-test.ts index 8c9d615e55..008be114f3 100644 --- a/packages/react-router/__tests__/vendor/turbo-stream-test.ts +++ b/packages/react-router/__tests__/vendor/turbo-stream-test.ts @@ -4,10 +4,26 @@ import { type EncodePlugin, } from "../../vendor/turbo-stream-v2/utils"; -async function quickDecode(stream: ReadableStream) { +async function quickDecode(stream: ReadableStream) { const decoded = await decode(stream); await decoded.done; - return decoded.value; + return decoded.value as T; +} + +async function readStreamToString(stream: ReadableStream) { + const reader = stream.getReader(); + const decoder = new TextDecoder(); + let text = ""; + + let chunk = await reader.read(); + while (!chunk.done) { + text += decoder.decode(chunk.value, { stream: true }); + chunk = await reader.read(); + } + + text += decoder.decode(); + reader.releaseLock(); + return text; } test("should encode and decode undefined", async () => { @@ -52,7 +68,7 @@ test("should encode and decode Date", async () => { test("should encode and decode invalid Date", async () => { const input = new Date("invalid"); - const output = await quickDecode(encode(input)); + const output = await quickDecode(encode(input)); expect(isNaN(input.getTime())).toBe(true); expect(isNaN(output.getTime())).toBe(true); }); @@ -237,16 +253,7 @@ test("should encode and decode object and dedupe object key, value, and promise expect(partialResult).toEqual(partialInput); expect(await bazResult).toEqual(await bazInput); - let encoded = ""; - const stream = encode(input); - await stream.pipeThrough(new TextDecoderStream()).pipeTo( - new WritableStream({ - write(chunk) { - encoded += chunk; - }, - }), - ); - + const encoded = await readStreamToString(encode(input)); expect(Array.from(encoded.matchAll(/"foo"/g))).toHaveLength(1); expect(Array.from(encoded.matchAll(/"bar"/g))).toHaveLength(1); expect(Array.from(encoded.matchAll(/"baz"/g))).toHaveLength(1); @@ -504,15 +511,7 @@ test("should encode and decode objects with multiple promises resolving to the s await decoded.done; // Ensure we aren't duplicating values in the stream - let encoded = ""; - const stream = encode(input); - await stream.pipeThrough(new TextDecoderStream()).pipeTo( - new WritableStream({ - write(chunk) { - encoded += chunk; - }, - }), - ); + const encoded = await readStreamToString(encode(input)); expect(Array.from(encoded.matchAll(/"baz"/g))).toHaveLength(1); }); @@ -535,15 +534,7 @@ test("should encode and decode objects with reused values", async () => { expect(await value.data).toEqual(await input.data); // Ensure we aren't duplicating values in the stream - let encoded = ""; - const stream = encode(input); - await stream.pipeThrough(new TextDecoderStream()).pipeTo( - new WritableStream({ - write(chunk) { - encoded += chunk; - }, - }), - ); + const encoded = await readStreamToString(encode(input)); expect(Array.from(encoded.matchAll(/"baz"/g))).toHaveLength(1); await decoded.done; }); @@ -566,15 +557,7 @@ test("should encode and decode objects with multiple promises rejecting to the s await decoded.done; // Ensure we aren't duplicating values in the stream - let encoded = ""; - const stream = encode(input); - await stream.pipeThrough(new TextDecoderStream()).pipeTo( - new WritableStream({ - write(chunk) { - encoded += chunk; - }, - }), - ); + const encoded = await readStreamToString(encode(input)); expect(Array.from(encoded.matchAll(/"baz"/g))).toHaveLength(1); }); @@ -597,3 +580,42 @@ test("should allow many nested promises without a memory leak", async () => { expect(currentDecoded.i).toBe(depth - 1); await decoded.done; }); + +test("should encode large payload", async () => { + const input = createDeeplyNestedObject(); + await readStreamToString(encode(input)); +}); + +test("should encode and decode large payload and yield the event loop", async () => { + const input = createDeeplyNestedObject(); + let i = 0; + let interval = setInterval(() => i++, 0); + const decoded = await decode(encode(input)); + + clearInterval(interval); + expect(i > 0).toBe(true); + + let currentInput: Nested | null = input; + let currentDecoded = decoded.value as Nested | null; + + while (currentInput && currentDecoded) { + expect(currentDecoded.value).toBe(currentInput.value); + currentInput = currentInput.next; + currentDecoded = currentDecoded.next; + } + + expect(currentInput).toBeNull(); + expect(currentDecoded).toBeNull(); + + await decoded.done; +}); + +type Nested = { value: number; next: Nested | null }; +function createDeeplyNestedObject(): Nested { + const depth = 100000; + let current = { value: 0, next: null as any }; + for (let i = 1; i < depth; i++) { + current = { value: i, next: current }; + } + return current; +} diff --git a/packages/react-router/vendor/turbo-stream-v2/flatten.ts b/packages/react-router/vendor/turbo-stream-v2/flatten.ts index 50271c5ffc..2b75d95d70 100644 --- a/packages/react-router/vendor/turbo-stream-v2/flatten.ts +++ b/packages/react-router/vendor/turbo-stream-v2/flatten.ts @@ -19,7 +19,15 @@ import { type ThisEncode, } from "./utils"; -export function flatten(this: ThisEncode, input: unknown): number | [number] { +const TIME_LIMIT_MS = 1; +const getNow = () => Date.now(); +const yieldToMain = (): Promise => + new Promise((resolve) => setTimeout(resolve, 0)); + +export async function flatten( + this: ThisEncode, + input: unknown, +): Promise { const { indices } = this; const existing = indices.get(input); if (existing) return [existing]; @@ -33,21 +41,51 @@ export function flatten(this: ThisEncode, input: unknown): number | [number] { const index = this.index++; indices.set(input, index); - stringify.call(this, input, index); + + const stack: [unknown, number][] = [[input, index]]; + await stringify.call(this, stack); + return index; } -function stringify(this: ThisEncode, input: unknown, index: number) { - const { deferred, plugins, postPlugins } = this; +async function stringify(this: ThisEncode, stack: [unknown, number][]) { + const { deferred, indices, plugins, postPlugins } = this; const str = this.stringified; - const stack: [unknown, number][] = [[input, index]]; + let lastYieldTime = getNow(); + + // Helper to assign index and schedule for processing if needed + const flattenValue = (value: unknown): number | [number] => { + const existing = indices.get(value); + if (existing) return [existing]; + + if (value === undefined) return UNDEFINED; + if (value === null) return NULL; + if (Number.isNaN(value)) return NAN; + if (value === Number.POSITIVE_INFINITY) return POSITIVE_INFINITY; + if (value === Number.NEGATIVE_INFINITY) return NEGATIVE_INFINITY; + if (value === 0 && 1 / value < 0) return NEGATIVE_ZERO; + + const index = this.index++; + indices.set(value, index); + stack.push([value, index]); + return index; + }; + + let i = 0; while (stack.length > 0) { + // Yield to main thread if time limit exceeded + const now = getNow(); + if (++i % 6000 === 0 && now - lastYieldTime >= TIME_LIMIT_MS) { + await yieldToMain(); + lastYieldTime = getNow(); + } + const [input, index] = stack.pop()!; const partsForObj = (obj: any) => Object.keys(obj) - .map((k) => `"_${flatten.call(this, k)}":${flatten.call(this, obj[k])}`) + .map((k) => `"_${flattenValue(k)}":${flattenValue(obj[k])}`) .join(","); let error: Error | null = null; @@ -87,9 +125,7 @@ function stringify(this: ThisEncode, input: unknown, index: number) { const [pluginIdentifier, ...rest] = pluginResult; str[index] = `[${JSON.stringify(pluginIdentifier)}`; if (rest.length > 0) { - str[index] += `,${rest - .map((v) => flatten.call(this, v)) - .join(",")}`; + str[index] += `,${rest.map((v) => flattenValue(v)).join(",")}`; } str[index] += "]"; break; @@ -102,8 +138,7 @@ function stringify(this: ThisEncode, input: unknown, index: number) { if (isArray) { for (let i = 0; i < input.length; i++) result += - (i ? "," : "") + - (i in input ? flatten.call(this, input[i]) : HOLE); + (i ? "," : "") + (i in input ? flattenValue(input[i]) : HOLE); str[index] = `${result}]`; } else if (input instanceof Date) { const dateTime = input.getTime(); @@ -119,7 +154,7 @@ function stringify(this: ThisEncode, input: unknown, index: number) { } else if (input instanceof Set) { if (input.size > 0) { str[index] = `["${TYPE_SET}",${[...input] - .map((val) => flatten.call(this, val)) + .map((val) => flattenValue(val)) .join(",")}]`; } else { str[index] = `["${TYPE_SET}"]`; @@ -127,10 +162,7 @@ function stringify(this: ThisEncode, input: unknown, index: number) { } else if (input instanceof Map) { if (input.size > 0) { str[index] = `["${TYPE_MAP}",${[...input] - .flatMap(([k, v]) => [ - flatten.call(this, k), - flatten.call(this, v), - ]) + .flatMap(([k, v]) => [flattenValue(k), flattenValue(v)]) .join(",")}]`; } else { str[index] = `["${TYPE_MAP}"]`; @@ -165,9 +197,7 @@ function stringify(this: ThisEncode, input: unknown, index: number) { const [pluginIdentifier, ...rest] = pluginResult; str[index] = `[${JSON.stringify(pluginIdentifier)}`; if (rest.length > 0) { - str[index] += `,${rest - .map((v) => flatten.call(this, v)) - .join(",")}`; + str[index] += `,${rest.map((v) => flattenValue(v)).join(",")}`; } str[index] += "]"; break; @@ -192,9 +222,7 @@ function stringify(this: ThisEncode, input: unknown, index: number) { const [pluginIdentifier, ...rest] = pluginResult; str[index] = `[${JSON.stringify(pluginIdentifier)}`; if (rest.length > 0) { - str[index] += `,${rest - .map((v) => flatten.call(this, v)) - .join(",")}`; + str[index] += `,${rest.map((v) => flattenValue(v)).join(",")}`; } str[index] += "]"; break; diff --git a/packages/react-router/vendor/turbo-stream-v2/turbo-stream.ts b/packages/react-router/vendor/turbo-stream-v2/turbo-stream.ts index 660072aa42..e6d7824833 100644 --- a/packages/react-router/vendor/turbo-stream-v2/turbo-stream.ts +++ b/packages/react-router/vendor/turbo-stream-v2/turbo-stream.ts @@ -135,13 +135,13 @@ async function decodeDeferred( export function encode( input: unknown, options?: { + onComplete?: () => void; plugins?: EncodePlugin[]; postPlugins?: EncodePlugin[]; signal?: AbortSignal; - onComplete?: () => void; }, ) { - const { plugins, postPlugins, signal, onComplete } = options ?? {}; + const { onComplete, plugins, postPlugins, signal } = options ?? {}; const encoder: ThisEncode = { deferred: {}, @@ -156,7 +156,7 @@ export function encode( let lastSentIndex = 0; const readable = new ReadableStream({ async start(controller) { - const id = flatten.call(encoder, input); + const id = await flatten.call(encoder, input); if (Array.isArray(id)) { throw new Error("This should never happen"); } @@ -170,6 +170,8 @@ export function encode( } const seenPromises = new WeakSet>(); + // Serialize flatten calls to prevent race conditions when yielding + let processingChain: Promise = Promise.resolve(); if (Object.keys(encoder.deferred).length) { let raceDone!: () => void; const racePromise = new Promise((resolve, reject) => { @@ -199,68 +201,74 @@ export function encode( ]) .then( (resolved) => { - const id = flatten.call(encoder, resolved); - if (Array.isArray(id)) { - controller.enqueue( - textEncoder.encode( - `${TYPE_PROMISE}${deferredId}:[["${TYPE_PREVIOUS_RESOLVED}",${id[0]}]]\n`, - ), - ); - encoder.index++; - lastSentIndex++; - } else if (id < 0) { - controller.enqueue( - textEncoder.encode( - `${TYPE_PROMISE}${deferredId}:${id}\n`, - ), - ); - } else { - const values = encoder.stringified - .slice(lastSentIndex + 1) - .join(","); - controller.enqueue( - textEncoder.encode( - `${TYPE_PROMISE}${deferredId}:[${values}]\n`, - ), - ); - lastSentIndex = encoder.stringified.length - 1; - } + processingChain = processingChain.then(async () => { + const id = await flatten.call(encoder, resolved); + if (Array.isArray(id)) { + controller.enqueue( + textEncoder.encode( + `${TYPE_PROMISE}${deferredId}:[["${TYPE_PREVIOUS_RESOLVED}",${id[0]}]]\n`, + ), + ); + encoder.index++; + lastSentIndex++; + } else if (id < 0) { + controller.enqueue( + textEncoder.encode( + `${TYPE_PROMISE}${deferredId}:${id}\n`, + ), + ); + } else { + const values = encoder.stringified + .slice(lastSentIndex + 1) + .join(","); + controller.enqueue( + textEncoder.encode( + `${TYPE_PROMISE}${deferredId}:[${values}]\n`, + ), + ); + lastSentIndex = encoder.stringified.length - 1; + } + }); + return processingChain; }, (reason) => { - if ( - !reason || - typeof reason !== "object" || - !(reason instanceof Error) - ) { - reason = new Error("An unknown error occurred"); - } + processingChain = processingChain.then(async () => { + if ( + !reason || + typeof reason !== "object" || + !(reason instanceof Error) + ) { + reason = new Error("An unknown error occurred"); + } - const id = flatten.call(encoder, reason); - if (Array.isArray(id)) { - controller.enqueue( - textEncoder.encode( - `${TYPE_ERROR}${deferredId}:[["${TYPE_PREVIOUS_RESOLVED}",${id[0]}]]\n`, - ), - ); - encoder.index++; - lastSentIndex++; - } else if (id < 0) { - controller.enqueue( - textEncoder.encode( - `${TYPE_ERROR}${deferredId}:${id}\n`, - ), - ); - } else { - const values = encoder.stringified - .slice(lastSentIndex + 1) - .join(","); - controller.enqueue( - textEncoder.encode( - `${TYPE_ERROR}${deferredId}:[${values}]\n`, - ), - ); - lastSentIndex = encoder.stringified.length - 1; - } + const id = await flatten.call(encoder, reason); + if (Array.isArray(id)) { + controller.enqueue( + textEncoder.encode( + `${TYPE_ERROR}${deferredId}:[["${TYPE_PREVIOUS_RESOLVED}",${id[0]}]]\n`, + ), + ); + encoder.index++; + lastSentIndex++; + } else if (id < 0) { + controller.enqueue( + textEncoder.encode( + `${TYPE_ERROR}${deferredId}:${id}\n`, + ), + ); + } else { + const values = encoder.stringified + .slice(lastSentIndex + 1) + .join(","); + controller.enqueue( + textEncoder.encode( + `${TYPE_ERROR}${deferredId}:[${values}]\n`, + ), + ); + lastSentIndex = encoder.stringified.length - 1; + } + }); + return processingChain; }, ) .finally(() => { @@ -274,9 +282,11 @@ export function encode( raceDone(); } await Promise.all(Object.values(encoder.deferred)); + await processingChain; - onComplete?.(); controller.close(); + + onComplete?.(); }, }); From fd0763ec07fae56373141b767fd07062274bcf3b Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Fri, 20 Mar 2026 16:05:14 -0700 Subject: [PATCH 04/29] chore: update playwright, harden tests (#14898) --- integration/browser-entry-test.ts | 1 + integration/client-data-test.ts | 69 +- integration/defer-test.ts | 790 +++------------------- integration/fetcher-test.ts | 2 + integration/helpers/create-fixture.ts | 13 +- integration/helpers/playwright-fixture.ts | 32 +- integration/package.json | 2 +- integration/prefetch-test.ts | 215 +++--- integration/route-config-test.ts | 5 +- integration/rsc/rsc-nojs-test.ts | 5 +- integration/rsc/rsc-test.ts | 1 + integration/vite-basename-test.ts | 52 +- integration/vite-hmr-hdr-test.ts | 1 - package.json | 2 +- pnpm-lock.yaml | 30 +- 15 files changed, 361 insertions(+), 859 deletions(-) diff --git a/integration/browser-entry-test.ts b/integration/browser-entry-test.ts index ad984fd43d..6affea8776 100644 --- a/integration/browser-entry-test.ts +++ b/integration/browser-entry-test.ts @@ -56,6 +56,7 @@ test( // This sets up the Remix modules cache in memory, priming the error case. await app.goto("/"); await app.clickLink("/burgers"); + await page.waitForSelector("#cheeseburger"); expect(await page.content()).toContain("cheeseburger"); await page.goBack(); await page.waitForSelector("#pizza"); diff --git a/integration/client-data-test.ts b/integration/client-data-test.ts index bceee0d01c..1a71d04791 100644 --- a/integration/client-data-test.ts +++ b/integration/client-data-test.ts @@ -41,9 +41,8 @@ test.describe("Client Data", () => { export function loader() { return { message: 'Parent Server Loader' }; } - ${ - parentClientLoader - ? js` + ${parentClientLoader + ? js` export async function clientLoader({ serverLoader }) { // Need a small delay to ensure we capture the server-rendered // fallbacks for assertions @@ -52,17 +51,16 @@ test.describe("Client Data", () => { return { message: data.message + " (mutated by client)" }; } ` - : "" + : "" } - ${ - parentClientLoaderHydrate - ? js` + ${parentClientLoaderHydrate + ? js` clientLoader.hydrate = true; export function HydrateFallback() { return

Parent Fallback

} ` - : "" + : "" } ${parentAdditions || ""} export default function Component() { @@ -83,9 +81,8 @@ test.describe("Client Data", () => { export function action() { return { message: 'Child Server Action' }; } - ${ - childClientLoader - ? js` + ${childClientLoader + ? js` export async function clientLoader({ serverLoader }) { // Need a small delay to ensure we capture the server-rendered // fallbacks for assertions @@ -94,17 +91,16 @@ test.describe("Client Data", () => { return { message: data.message + " (mutated by client)" }; } ` - : "" + : "" } - ${ - childClientLoaderHydrate - ? js` + ${childClientLoaderHydrate + ? js` clientLoader.hydrate = true; export function HydrateFallback() { return

Child Fallback

} ` - : "" + : "" } ${childAdditions || ""} export default function Component() { @@ -1142,9 +1138,9 @@ test.describe("Client Data", () => { "/client-loader-critical/client-loader-hydrate-is-automatically-implied-when-no-server-loader-exists-without-hydrate-fallback/parent/child", ); let html = await app.getHtml(); - expect(html).toMatch( - "💿 Hey developer 👋. You can provide a way better UX than this", - ); + // Production builds strip dev-only warning logs, but we should + // still render the default root loading shell until hydration runs. + expect(html).toMatch("Loading..."); expect(html).not.toMatch("child-data"); await page.waitForSelector("#child-data"); html = await app.getHtml("main"); @@ -1163,7 +1159,7 @@ test.describe("Client Data", () => { let html = await app.getHtml("#child-error"); expect(html.replace(/\n/g, " ").replace(/ +/g, " ")).toMatch( "400 Error: You are trying to call serverLoader() on a route that does " + - 'not have a server loader (routeId: "routes/client-loader-critical.throws-a-400-if-you-call-serverloader-without-a-server-loader.parent.child")', + 'not have a server loader (routeId: "routes/client-loader-critical.throws-a-400-if-you-call-serverloader-without-a-server-loader.parent.child")', ); }); @@ -1215,7 +1211,7 @@ test.describe("Client Data", () => { page, }) => { let _consoleError = console.error; - console.error = () => {}; + console.error = () => { }; let app = new PlaywrightFixture(appFixture, page); await app.goto( @@ -1236,28 +1232,17 @@ test.describe("Client Data", () => { }) => { // test.skip(browserName === "firefox", "this test fails there due to extra debug logs.") let _consoleError = console.error; - console.error = () => {}; + console.error = () => { }; let app = new PlaywrightFixture(appFixture, page); let logs: string[] = []; page.on("console", (msg) => { - if (msg.type() === "timeStamp") return; - let text = msg.text(); - if ( - // Chrome logs the 500 as a console error, so skip that since it's not - // what we are asserting against here - /500 \(Internal Server Error\)/.test(text) || - // Ignore any dev tools messages. This may only happen locally when dev - // tools is installed and not in CI but either way we don't care - /Download the React DevTools/.test(text) || - (templateName.includes("rsc") && - /The element is a no-op when using RSC and can be safely removed./.test( - text, - )) - ) { - return; + // Firefox surfaces React performance track labels on the console + // during hydration, so only capture the application log this + // assertion actually cares about. + if (text === "running parent client loader") { + logs.push(text); } - logs.push(text); }); await app.goto( "/client-loader-critical/bubbled-server-loader-errors-are-persisted-for-hydrating-routes/parent/child", @@ -1365,7 +1350,7 @@ test.describe("Client Data", () => { let html = await app.getHtml("#child-error"); expect(html.replace(/\n/g, " ").replace(/ +/g, " ")).toMatch( "400 Error: You are trying to call serverLoader() on a route that does " + - 'not have a server loader (routeId: "routes/client-loader-lazy.throws-a-400-if-you-call-serverloader-without-a-server-loader.parent.child")', + 'not have a server loader (routeId: "routes/client-loader-lazy.throws-a-400-if-you-call-serverloader-without-a-server-loader.parent.child")', ); }); @@ -1524,7 +1509,7 @@ test.describe("Client Data", () => { let html = await app.getHtml("#child-error"); expect(html.replace(/\n/g, " ").replace(/ +/g, " ")).toMatch( "400 Error: You are trying to call serverAction() on a route that does " + - 'not have a server action (routeId: "routes/client-action-critical.throws-a-400-if-you-call-serveraction-without-a-server-action.parent.child")', + 'not have a server action (routeId: "routes/client-action-critical.throws-a-400-if-you-call-serveraction-without-a-server-action.parent.child")', ); }); }); @@ -1660,7 +1645,7 @@ test.describe("Client Data", () => { let html = await app.getHtml("#child-error"); expect(html.replace(/\n/g, " ").replace(/ +/g, " ")).toMatch( "400 Error: You are trying to call serverAction() on a route that does " + - 'not have a server action (routeId: "routes/client-action-lazy.throws-a-400-if-you-call-serveraction-without-a-server-action.parent.child")', + 'not have a server action (routeId: "routes/client-action-lazy.throws-a-400-if-you-call-serveraction-without-a-server-action.parent.child")', ); }); }); @@ -1668,4 +1653,4 @@ test.describe("Client Data", () => { } }); } -}); +}); \ No newline at end of file diff --git a/integration/defer-test.ts b/integration/defer-test.ts index 6e4f9a65d7..73b66becad 100644 --- a/integration/defer-test.ts +++ b/integration/defer-test.ts @@ -1,13 +1,9 @@ import { test, expect } from "@playwright/test"; -import type { ConsoleMessage, Page } from "@playwright/test"; +import type { Page } from "@playwright/test"; import { PlaywrightFixture } from "./helpers/playwright-fixture.js"; import type { Fixture, AppFixture } from "./helpers/create-fixture.js"; -import { - createAppFixture, - createFixture, - js, -} from "./helpers/create-fixture.js"; +import { createAppFixture, createFixture, js } from "./helpers/create-fixture.js"; const ROOT_ID = "ROOT_ID"; const INDEX_ID = "INDEX_ID"; @@ -32,17 +28,34 @@ declare global { }; } +function counterHtml(id: string, val: number) { + return `

${val}

`; +} + +const deferredHTMLStartString = "