@heojeongbo/wirestage 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/LICENSES.md +21 -0
- package/README.md +150 -2
- package/dist/app/playwright/index.d.ts +3 -0
- package/dist/app/playwright/index.d.ts.map +1 -0
- package/dist/app/playwright/index.js +2 -0
- package/dist/app/playwright/index.js.map +1 -0
- package/dist/app/playwright/lib/fixture.d.ts +61 -0
- package/dist/app/playwright/lib/fixture.d.ts.map +1 -0
- package/dist/app/playwright/lib/fixture.js +110 -0
- package/dist/app/playwright/lib/fixture.js.map +1 -0
- package/dist/connect.d.ts +7 -0
- package/dist/connect.d.ts.map +1 -0
- package/dist/connect.js +7 -0
- package/dist/connect.js.map +1 -0
- package/dist/entities/frame/@x/plugin.d.ts +9 -0
- package/dist/entities/frame/@x/plugin.d.ts.map +1 -0
- package/dist/entities/frame/@x/plugin.js +2 -0
- package/dist/entities/frame/@x/plugin.js.map +1 -0
- package/dist/entities/frame/index.d.ts +6 -0
- package/dist/entities/frame/index.d.ts.map +1 -0
- package/dist/entities/frame/index.js +4 -0
- package/dist/entities/frame/index.js.map +1 -0
- package/dist/entities/frame/lib/filter.d.ts +8 -0
- package/dist/entities/frame/lib/filter.d.ts.map +1 -0
- package/dist/entities/frame/lib/filter.js +62 -0
- package/dist/entities/frame/lib/filter.js.map +1 -0
- package/dist/entities/frame/lib/payload.d.ts +8 -0
- package/dist/entities/frame/lib/payload.d.ts.map +1 -0
- package/dist/entities/frame/lib/payload.js +42 -0
- package/dist/entities/frame/lib/payload.js.map +1 -0
- package/dist/entities/frame/lib/snapshot.d.ts +15 -0
- package/dist/entities/frame/lib/snapshot.d.ts.map +1 -0
- package/dist/entities/frame/lib/snapshot.js +90 -0
- package/dist/entities/frame/lib/snapshot.js.map +1 -0
- package/dist/entities/frame/lib/timeline.d.ts +23 -0
- package/dist/entities/frame/lib/timeline.d.ts.map +1 -0
- package/dist/entities/frame/lib/timeline.js +122 -0
- package/dist/entities/frame/lib/timeline.js.map +1 -0
- package/dist/entities/frame/model/frame.d.ts +182 -0
- package/dist/entities/frame/model/frame.d.ts.map +1 -0
- package/dist/entities/frame/model/frame.js +2 -0
- package/dist/entities/frame/model/frame.js.map +1 -0
- package/dist/entities/plugin/index.d.ts +9 -0
- package/dist/entities/plugin/index.d.ts.map +1 -0
- package/dist/entities/plugin/index.js +3 -0
- package/dist/entities/plugin/index.js.map +1 -0
- package/dist/entities/plugin/lib/define-plugin.d.ts +8 -0
- package/dist/entities/plugin/lib/define-plugin.d.ts.map +1 -0
- package/dist/entities/plugin/lib/define-plugin.js +9 -0
- package/dist/entities/plugin/lib/define-plugin.js.map +1 -0
- package/dist/entities/plugin/lib/named.d.ts +10 -0
- package/dist/entities/plugin/lib/named.d.ts.map +1 -0
- package/dist/entities/plugin/lib/named.js +17 -0
- package/dist/entities/plugin/lib/named.js.map +1 -0
- package/dist/entities/plugin/model/attachment.d.ts +18 -0
- package/dist/entities/plugin/model/attachment.d.ts.map +1 -0
- package/dist/entities/plugin/model/attachment.js +2 -0
- package/dist/entities/plugin/model/attachment.js.map +1 -0
- package/dist/entities/plugin/model/http.d.ts +135 -0
- package/dist/entities/plugin/model/http.d.ts.map +1 -0
- package/dist/entities/plugin/model/http.js +2 -0
- package/dist/entities/plugin/model/http.js.map +1 -0
- package/dist/entities/plugin/model/page.d.ts +100 -0
- package/dist/entities/plugin/model/page.d.ts.map +1 -0
- package/dist/entities/plugin/model/page.js +2 -0
- package/dist/entities/plugin/model/page.js.map +1 -0
- package/dist/entities/plugin/model/plugin.d.ts +83 -0
- package/dist/entities/plugin/model/plugin.d.ts.map +1 -0
- package/dist/entities/plugin/model/plugin.js +2 -0
- package/dist/entities/plugin/model/plugin.js.map +1 -0
- package/dist/entities/plugin/model/socket.d.ts +107 -0
- package/dist/entities/plugin/model/socket.d.ts.map +1 -0
- package/dist/entities/plugin/model/socket.js +2 -0
- package/dist/entities/plugin/model/socket.js.map +1 -0
- package/dist/entities/plugin/model/stream.d.ts +46 -0
- package/dist/entities/plugin/model/stream.d.ts.map +1 -0
- package/dist/entities/plugin/model/stream.js +2 -0
- package/dist/entities/plugin/model/stream.js.map +1 -0
- package/dist/features/connect/index.d.ts +5 -0
- package/dist/features/connect/index.d.ts.map +1 -0
- package/dist/features/connect/index.js +4 -0
- package/dist/features/connect/index.js.map +1 -0
- package/dist/features/connect/lib/calls.d.ts +9 -0
- package/dist/features/connect/lib/calls.d.ts.map +1 -0
- package/dist/features/connect/lib/calls.js +46 -0
- package/dist/features/connect/lib/calls.js.map +1 -0
- package/dist/features/connect/lib/client.d.ts +33 -0
- package/dist/features/connect/lib/client.d.ts.map +1 -0
- package/dist/features/connect/lib/client.js +147 -0
- package/dist/features/connect/lib/client.js.map +1 -0
- package/dist/features/connect/lib/create-connect.d.ts +14 -0
- package/dist/features/connect/lib/create-connect.d.ts.map +1 -0
- package/dist/features/connect/lib/create-connect.js +124 -0
- package/dist/features/connect/lib/create-connect.js.map +1 -0
- package/dist/features/connect/lib/faults.d.ts +18 -0
- package/dist/features/connect/lib/faults.d.ts.map +1 -0
- package/dist/features/connect/lib/faults.js +85 -0
- package/dist/features/connect/lib/faults.js.map +1 -0
- package/dist/features/connect/lib/protocol.d.ts +42 -0
- package/dist/features/connect/lib/protocol.d.ts.map +1 -0
- package/dist/features/connect/lib/protocol.js +228 -0
- package/dist/features/connect/lib/protocol.js.map +1 -0
- package/dist/features/connect/lib/record.d.ts +14 -0
- package/dist/features/connect/lib/record.d.ts.map +1 -0
- package/dist/features/connect/lib/record.js +36 -0
- package/dist/features/connect/lib/record.js.map +1 -0
- package/dist/features/connect/lib/recorder.d.ts +77 -0
- package/dist/features/connect/lib/recorder.d.ts.map +1 -0
- package/dist/features/connect/lib/recorder.js +209 -0
- package/dist/features/connect/lib/recorder.js.map +1 -0
- package/dist/features/connect/lib/rules.d.ts +51 -0
- package/dist/features/connect/lib/rules.d.ts.map +1 -0
- package/dist/features/connect/lib/rules.js +222 -0
- package/dist/features/connect/lib/rules.js.map +1 -0
- package/dist/features/connect/lib/serve.d.ts +10 -0
- package/dist/features/connect/lib/serve.d.ts.map +1 -0
- package/dist/features/connect/lib/serve.js +78 -0
- package/dist/features/connect/lib/serve.js.map +1 -0
- package/dist/features/connect/lib/status-of.d.ts +11 -0
- package/dist/features/connect/lib/status-of.d.ts.map +1 -0
- package/dist/features/connect/lib/status-of.js +22 -0
- package/dist/features/connect/lib/status-of.js.map +1 -0
- package/dist/features/connect/lib/stubs.d.ts +18 -0
- package/dist/features/connect/lib/stubs.d.ts.map +1 -0
- package/dist/features/connect/lib/stubs.js +174 -0
- package/dist/features/connect/lib/stubs.js.map +1 -0
- package/dist/features/connect/lib/universal.d.ts +19 -0
- package/dist/features/connect/lib/universal.d.ts.map +1 -0
- package/dist/features/connect/lib/universal.js +71 -0
- package/dist/features/connect/lib/universal.js.map +1 -0
- package/dist/features/connect/model/types.d.ts +247 -0
- package/dist/features/connect/model/types.d.ts.map +1 -0
- package/dist/features/connect/model/types.js +2 -0
- package/dist/features/connect/model/types.js.map +1 -0
- package/dist/features/rest/index.d.ts +5 -0
- package/dist/features/rest/index.d.ts.map +1 -0
- package/dist/features/rest/index.js +3 -0
- package/dist/features/rest/index.js.map +1 -0
- package/dist/features/rest/lib/calls.d.ts +8 -0
- package/dist/features/rest/lib/calls.d.ts.map +1 -0
- package/dist/features/rest/lib/calls.js +32 -0
- package/dist/features/rest/lib/calls.js.map +1 -0
- package/dist/features/rest/lib/create-rest.d.ts +8 -0
- package/dist/features/rest/lib/create-rest.d.ts.map +1 -0
- package/dist/features/rest/lib/create-rest.js +136 -0
- package/dist/features/rest/lib/create-rest.js.map +1 -0
- package/dist/features/rest/lib/path.d.ts +14 -0
- package/dist/features/rest/lib/path.d.ts.map +1 -0
- package/dist/features/rest/lib/path.js +58 -0
- package/dist/features/rest/lib/path.js.map +1 -0
- package/dist/features/rest/lib/reply.d.ts +25 -0
- package/dist/features/rest/lib/reply.d.ts.map +1 -0
- package/dist/features/rest/lib/reply.js +40 -0
- package/dist/features/rest/lib/reply.js.map +1 -0
- package/dist/features/rest/model/types.d.ts +74 -0
- package/dist/features/rest/model/types.d.ts.map +1 -0
- package/dist/features/rest/model/types.js +2 -0
- package/dist/features/rest/model/types.js.map +1 -0
- package/dist/features/sse/index.d.ts +4 -0
- package/dist/features/sse/index.d.ts.map +1 -0
- package/dist/features/sse/index.js +3 -0
- package/dist/features/sse/index.js.map +1 -0
- package/dist/features/sse/lib/create-sse.d.ts +9 -0
- package/dist/features/sse/lib/create-sse.d.ts.map +1 -0
- package/dist/features/sse/lib/create-sse.js +121 -0
- package/dist/features/sse/lib/create-sse.js.map +1 -0
- package/dist/features/sse/lib/encode.d.ts +8 -0
- package/dist/features/sse/lib/encode.d.ts.map +1 -0
- package/dist/features/sse/lib/encode.js +29 -0
- package/dist/features/sse/lib/encode.js.map +1 -0
- package/dist/features/sse/model/types.d.ts +68 -0
- package/dist/features/sse/model/types.d.ts.map +1 -0
- package/dist/features/sse/model/types.js +2 -0
- package/dist/features/sse/model/types.js.map +1 -0
- package/dist/features/webrtc/index.d.ts +3 -0
- package/dist/features/webrtc/index.d.ts.map +1 -0
- package/dist/features/webrtc/index.js +2 -0
- package/dist/features/webrtc/index.js.map +1 -0
- package/dist/features/webrtc/lib/create-webrtc.d.ts +9 -0
- package/dist/features/webrtc/lib/create-webrtc.d.ts.map +1 -0
- package/dist/features/webrtc/lib/create-webrtc.js +123 -0
- package/dist/features/webrtc/lib/create-webrtc.js.map +1 -0
- package/dist/features/webrtc/lib/rtc-page-module.d.ts +22 -0
- package/dist/features/webrtc/lib/rtc-page-module.d.ts.map +1 -0
- package/dist/features/webrtc/lib/rtc-page-module.js +161 -0
- package/dist/features/webrtc/lib/rtc-page-module.js.map +1 -0
- package/dist/features/webrtc/lib/to-frame.d.ts +9 -0
- package/dist/features/webrtc/lib/to-frame.d.ts.map +1 -0
- package/dist/features/webrtc/lib/to-frame.js +43 -0
- package/dist/features/webrtc/lib/to-frame.js.map +1 -0
- package/dist/features/webrtc/model/types.d.ts +70 -0
- package/dist/features/webrtc/model/types.d.ts.map +1 -0
- package/dist/features/webrtc/model/types.js +2 -0
- package/dist/features/webrtc/model/types.js.map +1 -0
- package/dist/features/websocket/index.d.ts +3 -0
- package/dist/features/websocket/index.d.ts.map +1 -0
- package/dist/features/websocket/index.js +2 -0
- package/dist/features/websocket/index.js.map +1 -0
- package/dist/features/websocket/lib/create-websocket.d.ts +9 -0
- package/dist/features/websocket/lib/create-websocket.d.ts.map +1 -0
- package/dist/features/websocket/lib/create-websocket.js +34 -0
- package/dist/features/websocket/lib/create-websocket.js.map +1 -0
- package/dist/features/websocket/lib/mock.d.ts +6 -0
- package/dist/features/websocket/lib/mock.d.ts.map +1 -0
- package/dist/features/websocket/lib/mock.js +96 -0
- package/dist/features/websocket/lib/mock.js.map +1 -0
- package/dist/features/websocket/lib/proxy.d.ts +9 -0
- package/dist/features/websocket/lib/proxy.d.ts.map +1 -0
- package/dist/features/websocket/lib/proxy.js +120 -0
- package/dist/features/websocket/lib/proxy.js.map +1 -0
- package/dist/features/websocket/lib/wire.d.ts +16 -0
- package/dist/features/websocket/lib/wire.d.ts.map +1 -0
- package/dist/features/websocket/lib/wire.js +46 -0
- package/dist/features/websocket/lib/wire.js.map +1 -0
- package/dist/features/websocket/model/types.d.ts +133 -0
- package/dist/features/websocket/model/types.d.ts.map +1 -0
- package/dist/features/websocket/model/types.js +2 -0
- package/dist/features/websocket/model/types.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/playwright.d.ts +4 -0
- package/dist/playwright.d.ts.map +1 -0
- package/dist/playwright.js +2 -0
- package/dist/playwright.js.map +1 -0
- package/dist/rest.d.ts +3 -0
- package/dist/rest.d.ts.map +1 -0
- package/dist/rest.js +3 -0
- package/dist/rest.js.map +1 -0
- package/dist/shared/api/http-server/index.d.ts +3 -0
- package/dist/shared/api/http-server/index.d.ts.map +1 -0
- package/dist/shared/api/http-server/index.js +2 -0
- package/dist/shared/api/http-server/index.js.map +1 -0
- package/dist/shared/api/http-server/listen.d.ts +25 -0
- package/dist/shared/api/http-server/listen.d.ts.map +1 -0
- package/dist/shared/api/http-server/listen.js +44 -0
- package/dist/shared/api/http-server/listen.js.map +1 -0
- package/dist/shared/api/playwright/index.d.ts +2 -0
- package/dist/shared/api/playwright/index.d.ts.map +1 -0
- package/dist/shared/api/playwright/index.js +2 -0
- package/dist/shared/api/playwright/index.js.map +1 -0
- package/dist/shared/api/playwright/ports.d.ts +126 -0
- package/dist/shared/api/playwright/ports.d.ts.map +1 -0
- package/dist/shared/api/playwright/ports.js +11 -0
- package/dist/shared/api/playwright/ports.js.map +1 -0
- package/dist/shared/api/stream-server/index.d.ts +3 -0
- package/dist/shared/api/stream-server/index.d.ts.map +1 -0
- package/dist/shared/api/stream-server/index.js +2 -0
- package/dist/shared/api/stream-server/index.js.map +1 -0
- package/dist/shared/api/stream-server/stream-server.d.ts +45 -0
- package/dist/shared/api/stream-server/stream-server.d.ts.map +1 -0
- package/dist/shared/api/stream-server/stream-server.js +167 -0
- package/dist/shared/api/stream-server/stream-server.js.map +1 -0
- package/dist/shared/lib/async/arrivals.d.ts +31 -0
- package/dist/shared/lib/async/arrivals.d.ts.map +1 -0
- package/dist/shared/lib/async/arrivals.js +57 -0
- package/dist/shared/lib/async/arrivals.js.map +1 -0
- package/dist/shared/lib/async/async-queue.d.ts +23 -0
- package/dist/shared/lib/async/async-queue.d.ts.map +1 -0
- package/dist/shared/lib/async/async-queue.js +80 -0
- package/dist/shared/lib/async/async-queue.js.map +1 -0
- package/dist/shared/lib/async/deferred.d.ts +12 -0
- package/dist/shared/lib/async/deferred.d.ts.map +1 -0
- package/dist/shared/lib/async/deferred.js +34 -0
- package/dist/shared/lib/async/deferred.js.map +1 -0
- package/dist/shared/lib/async/index.d.ts +8 -0
- package/dist/shared/lib/async/index.d.ts.map +1 -0
- package/dist/shared/lib/async/index.js +5 -0
- package/dist/shared/lib/async/index.js.map +1 -0
- package/dist/shared/lib/async/run-all.d.ts +8 -0
- package/dist/shared/lib/async/run-all.d.ts.map +1 -0
- package/dist/shared/lib/async/run-all.js +22 -0
- package/dist/shared/lib/async/run-all.js.map +1 -0
- package/dist/shared/lib/bytes/bytes.d.ts +18 -0
- package/dist/shared/lib/bytes/bytes.d.ts.map +1 -0
- package/dist/shared/lib/bytes/bytes.js +48 -0
- package/dist/shared/lib/bytes/bytes.js.map +1 -0
- package/dist/shared/lib/bytes/index.d.ts +3 -0
- package/dist/shared/lib/bytes/index.d.ts.map +1 -0
- package/dist/shared/lib/bytes/index.js +2 -0
- package/dist/shared/lib/bytes/index.js.map +1 -0
- package/dist/shared/lib/clock/clock.d.ts +29 -0
- package/dist/shared/lib/clock/clock.d.ts.map +1 -0
- package/dist/shared/lib/clock/clock.js +85 -0
- package/dist/shared/lib/clock/clock.js.map +1 -0
- package/dist/shared/lib/clock/index.d.ts +3 -0
- package/dist/shared/lib/clock/index.d.ts.map +1 -0
- package/dist/shared/lib/clock/index.js +2 -0
- package/dist/shared/lib/clock/index.js.map +1 -0
- package/dist/shared/lib/cookies/index.d.ts +3 -0
- package/dist/shared/lib/cookies/index.d.ts.map +1 -0
- package/dist/shared/lib/cookies/index.js +2 -0
- package/dist/shared/lib/cookies/index.js.map +1 -0
- package/dist/shared/lib/cookies/set-cookie.d.ts +22 -0
- package/dist/shared/lib/cookies/set-cookie.d.ts.map +1 -0
- package/dist/shared/lib/cookies/set-cookie.js +89 -0
- package/dist/shared/lib/cookies/set-cookie.js.map +1 -0
- package/dist/shared/lib/envelope/envelope.d.ts +50 -0
- package/dist/shared/lib/envelope/envelope.d.ts.map +1 -0
- package/dist/shared/lib/envelope/envelope.js +111 -0
- package/dist/shared/lib/envelope/envelope.js.map +1 -0
- package/dist/shared/lib/envelope/index.d.ts +3 -0
- package/dist/shared/lib/envelope/index.d.ts.map +1 -0
- package/dist/shared/lib/envelope/index.js +2 -0
- package/dist/shared/lib/envelope/index.js.map +1 -0
- package/dist/shared/lib/errors/index.d.ts +3 -0
- package/dist/shared/lib/errors/index.d.ts.map +1 -0
- package/dist/shared/lib/errors/index.js +2 -0
- package/dist/shared/lib/errors/index.js.map +1 -0
- package/dist/shared/lib/errors/wirestage-error.d.ts +28 -0
- package/dist/shared/lib/errors/wirestage-error.d.ts.map +1 -0
- package/dist/shared/lib/errors/wirestage-error.js +16 -0
- package/dist/shared/lib/errors/wirestage-error.js.map +1 -0
- package/dist/shared/lib/id/id.d.ts +10 -0
- package/dist/shared/lib/id/id.d.ts.map +1 -0
- package/dist/shared/lib/id/id.js +26 -0
- package/dist/shared/lib/id/id.js.map +1 -0
- package/dist/shared/lib/id/index.d.ts +2 -0
- package/dist/shared/lib/id/index.d.ts.map +1 -0
- package/dist/shared/lib/id/index.js +2 -0
- package/dist/shared/lib/id/index.js.map +1 -0
- package/dist/shared/lib/logger/index.d.ts +3 -0
- package/dist/shared/lib/logger/index.d.ts.map +1 -0
- package/dist/shared/lib/logger/index.js +2 -0
- package/dist/shared/lib/logger/index.js.map +1 -0
- package/dist/shared/lib/logger/logger.d.ts +46 -0
- package/dist/shared/lib/logger/logger.d.ts.map +1 -0
- package/dist/shared/lib/logger/logger.js +67 -0
- package/dist/shared/lib/logger/logger.js.map +1 -0
- package/dist/shared/lib/match/index.d.ts +3 -0
- package/dist/shared/lib/match/index.d.ts.map +1 -0
- package/dist/shared/lib/match/index.js +2 -0
- package/dist/shared/lib/match/index.js.map +1 -0
- package/dist/shared/lib/match/url-matcher.d.ts +27 -0
- package/dist/shared/lib/match/url-matcher.d.ts.map +1 -0
- package/dist/shared/lib/match/url-matcher.js +88 -0
- package/dist/shared/lib/match/url-matcher.js.map +1 -0
- package/dist/sse.d.ts +3 -0
- package/dist/sse.d.ts.map +1 -0
- package/dist/sse.js +3 -0
- package/dist/sse.js.map +1 -0
- package/dist/webrtc.d.ts +3 -0
- package/dist/webrtc.d.ts.map +1 -0
- package/dist/webrtc.js +3 -0
- package/dist/webrtc.js.map +1 -0
- package/dist/websocket.d.ts +3 -0
- package/dist/websocket.d.ts.map +1 -0
- package/dist/websocket.js +3 -0
- package/dist/websocket.js.map +1 -0
- package/dist/widgets/stage/index.d.ts +3 -0
- package/dist/widgets/stage/index.d.ts.map +1 -0
- package/dist/widgets/stage/index.js +2 -0
- package/dist/widgets/stage/index.js.map +1 -0
- package/dist/widgets/stage/lib/attached-context.d.ts +10 -0
- package/dist/widgets/stage/lib/attached-context.d.ts.map +1 -0
- package/dist/widgets/stage/lib/attached-context.js +2 -0
- package/dist/widgets/stage/lib/attached-context.js.map +1 -0
- package/dist/widgets/stage/lib/create-stage.d.ts +12 -0
- package/dist/widgets/stage/lib/create-stage.d.ts.map +1 -0
- package/dist/widgets/stage/lib/create-stage.js +250 -0
- package/dist/widgets/stage/lib/create-stage.js.map +1 -0
- package/dist/widgets/stage/lib/http-router.d.ts +44 -0
- package/dist/widgets/stage/lib/http-router.d.ts.map +1 -0
- package/dist/widgets/stage/lib/http-router.js +696 -0
- package/dist/widgets/stage/lib/http-router.js.map +1 -0
- package/dist/widgets/stage/lib/page-host.d.ts +36 -0
- package/dist/widgets/stage/lib/page-host.d.ts.map +1 -0
- package/dist/widgets/stage/lib/page-host.js +240 -0
- package/dist/widgets/stage/lib/page-host.js.map +1 -0
- package/dist/widgets/stage/lib/page-runtime.d.ts +93 -0
- package/dist/widgets/stage/lib/page-runtime.d.ts.map +1 -0
- package/dist/widgets/stage/lib/page-runtime.js +247 -0
- package/dist/widgets/stage/lib/page-runtime.js.map +1 -0
- package/dist/widgets/stage/lib/reads.d.ts +21 -0
- package/dist/widgets/stage/lib/reads.d.ts.map +1 -0
- package/dist/widgets/stage/lib/reads.js +97 -0
- package/dist/widgets/stage/lib/reads.js.map +1 -0
- package/dist/widgets/stage/lib/response-stream.d.ts +23 -0
- package/dist/widgets/stage/lib/response-stream.d.ts.map +1 -0
- package/dist/widgets/stage/lib/response-stream.js +108 -0
- package/dist/widgets/stage/lib/response-stream.js.map +1 -0
- package/dist/widgets/stage/lib/socket-router.d.ts +25 -0
- package/dist/widgets/stage/lib/socket-router.d.ts.map +1 -0
- package/dist/widgets/stage/lib/socket-router.js +233 -0
- package/dist/widgets/stage/lib/socket-router.js.map +1 -0
- package/dist/widgets/stage/model/stage.d.ts +88 -0
- package/dist/widgets/stage/model/stage.d.ts.map +1 -0
- package/dist/widgets/stage/model/stage.js +2 -0
- package/dist/widgets/stage/model/stage.js.map +1 -0
- package/package.json +140 -5
- package/src/app/playwright/index.ts +7 -0
- package/src/app/playwright/lib/fixture.ts +201 -0
- package/src/connect.ts +6 -0
- package/src/entities/frame/@x/plugin.ts +8 -0
- package/src/entities/frame/index.ts +19 -0
- package/src/entities/frame/lib/filter.ts +58 -0
- package/src/entities/frame/lib/payload.ts +42 -0
- package/src/entities/frame/lib/snapshot.ts +105 -0
- package/src/entities/frame/lib/timeline.ts +147 -0
- package/src/entities/frame/model/frame.ts +191 -0
- package/src/entities/plugin/index.ts +36 -0
- package/src/entities/plugin/lib/define-plugin.ts +12 -0
- package/src/entities/plugin/lib/named.ts +21 -0
- package/src/entities/plugin/model/attachment.ts +18 -0
- package/src/entities/plugin/model/http.ts +156 -0
- package/src/entities/plugin/model/page.ts +110 -0
- package/src/entities/plugin/model/plugin.ts +96 -0
- package/src/entities/plugin/model/socket.ts +113 -0
- package/src/entities/plugin/model/stream.ts +46 -0
- package/src/features/connect/index.ts +23 -0
- package/src/features/connect/lib/calls.ts +58 -0
- package/src/features/connect/lib/client.ts +203 -0
- package/src/features/connect/lib/create-connect.ts +164 -0
- package/src/features/connect/lib/faults.ts +132 -0
- package/src/features/connect/lib/protocol.ts +321 -0
- package/src/features/connect/lib/record.ts +48 -0
- package/src/features/connect/lib/recorder.ts +306 -0
- package/src/features/connect/lib/rules.ts +385 -0
- package/src/features/connect/lib/serve.ts +114 -0
- package/src/features/connect/lib/status-of.ts +20 -0
- package/src/features/connect/lib/stubs.ts +282 -0
- package/src/features/connect/lib/universal.ts +100 -0
- package/src/features/connect/model/types.ts +305 -0
- package/src/features/rest/index.ts +23 -0
- package/src/features/rest/lib/calls.ts +31 -0
- package/src/features/rest/lib/create-rest.ts +169 -0
- package/src/features/rest/lib/path.ts +70 -0
- package/src/features/rest/lib/reply.ts +74 -0
- package/src/features/rest/model/types.ts +113 -0
- package/src/features/sse/index.ts +10 -0
- package/src/features/sse/lib/create-sse.ts +129 -0
- package/src/features/sse/lib/encode.ts +28 -0
- package/src/features/sse/model/types.ts +72 -0
- package/src/features/webrtc/index.ts +6 -0
- package/src/features/webrtc/lib/create-webrtc.ts +149 -0
- package/src/features/webrtc/lib/rtc-page-module.ts +189 -0
- package/src/features/webrtc/lib/to-frame.ts +63 -0
- package/src/features/webrtc/model/types.ts +80 -0
- package/src/features/websocket/index.ts +15 -0
- package/src/features/websocket/lib/create-websocket.ts +41 -0
- package/src/features/websocket/lib/mock.ts +114 -0
- package/src/features/websocket/lib/proxy.ts +153 -0
- package/src/features/websocket/lib/wire.ts +78 -0
- package/src/features/websocket/model/types.ts +134 -0
- package/src/index.ts +111 -0
- package/src/playwright.ts +8 -0
- package/src/rest.ts +2 -0
- package/src/shared/api/http-server/index.ts +2 -0
- package/src/shared/api/http-server/listen.ts +71 -0
- package/src/shared/api/playwright/index.ts +17 -0
- package/src/shared/api/playwright/ports.ts +150 -0
- package/src/shared/api/stream-server/index.ts +7 -0
- package/src/shared/api/stream-server/stream-server.ts +254 -0
- package/src/shared/lib/async/arrivals.ts +91 -0
- package/src/shared/lib/async/async-queue.ts +101 -0
- package/src/shared/lib/async/deferred.ts +38 -0
- package/src/shared/lib/async/index.ts +11 -0
- package/src/shared/lib/async/run-all.ts +19 -0
- package/src/shared/lib/bytes/bytes.ts +53 -0
- package/src/shared/lib/bytes/index.ts +9 -0
- package/src/shared/lib/clock/clock.ts +114 -0
- package/src/shared/lib/clock/index.ts +2 -0
- package/src/shared/lib/cookies/index.ts +2 -0
- package/src/shared/lib/cookies/set-cookie.ts +105 -0
- package/src/shared/lib/envelope/envelope.ts +133 -0
- package/src/shared/lib/envelope/index.ts +9 -0
- package/src/shared/lib/errors/index.ts +5 -0
- package/src/shared/lib/errors/wirestage-error.ts +50 -0
- package/src/shared/lib/id/id.ts +29 -0
- package/src/shared/lib/id/index.ts +1 -0
- package/src/shared/lib/logger/index.ts +12 -0
- package/src/shared/lib/logger/logger.ts +126 -0
- package/src/shared/lib/match/index.ts +6 -0
- package/src/shared/lib/match/url-matcher.ts +107 -0
- package/src/sse.ts +2 -0
- package/src/webrtc.ts +2 -0
- package/src/websocket.ts +2 -0
- package/src/widgets/stage/index.ts +9 -0
- package/src/widgets/stage/lib/attached-context.ts +10 -0
- package/src/widgets/stage/lib/create-stage.ts +307 -0
- package/src/widgets/stage/lib/http-router.ts +954 -0
- package/src/widgets/stage/lib/page-host.ts +375 -0
- package/src/widgets/stage/lib/page-runtime.ts +314 -0
- package/src/widgets/stage/lib/reads.ts +128 -0
- package/src/widgets/stage/lib/response-stream.ts +139 -0
- package/src/widgets/stage/lib/socket-router.ts +300 -0
- package/src/widgets/stage/model/stage.ts +110 -0
|
@@ -0,0 +1,954 @@
|
|
|
1
|
+
import type { FrameInit, Timeline } from "../../../entities/frame/index.js";
|
|
2
|
+
import type {
|
|
3
|
+
AbortErrorCode,
|
|
4
|
+
HttpOutcome,
|
|
5
|
+
HttpRequestView,
|
|
6
|
+
HttpRouter,
|
|
7
|
+
HttpRule,
|
|
8
|
+
HttpRuleHandle,
|
|
9
|
+
PassthroughResponse,
|
|
10
|
+
StreamEnd,
|
|
11
|
+
} from "../../../entities/plugin/index.js";
|
|
12
|
+
import type {
|
|
13
|
+
PagePort,
|
|
14
|
+
RequestPort,
|
|
15
|
+
RouteHandler,
|
|
16
|
+
RoutePort,
|
|
17
|
+
UrlPredicate,
|
|
18
|
+
} from "../../../shared/api/playwright/index.js";
|
|
19
|
+
import type {
|
|
20
|
+
NodeHandler,
|
|
21
|
+
StreamServer,
|
|
22
|
+
StreamSession,
|
|
23
|
+
} from "../../../shared/api/stream-server/index.js";
|
|
24
|
+
import { toBuffer, toBytes, utf8 } from "../../../shared/lib/bytes/index.js";
|
|
25
|
+
import type { Clock } from "../../../shared/lib/clock/index.js";
|
|
26
|
+
import { WirestageError } from "../../../shared/lib/errors/index.js";
|
|
27
|
+
import type { ScopedLogger } from "../../../shared/lib/logger/index.js";
|
|
28
|
+
import { compileUrlMatcher } from "../../../shared/lib/match/index.js";
|
|
29
|
+
import type { AttachedContext } from "./attached-context.js";
|
|
30
|
+
import { createResponseStream, type ServedStream } from "./response-stream.js";
|
|
31
|
+
|
|
32
|
+
export interface HttpRouterDeps {
|
|
33
|
+
readonly timeline: Timeline;
|
|
34
|
+
readonly log: ScopedLogger;
|
|
35
|
+
readonly streams: Pick<StreamServer, "open">;
|
|
36
|
+
readonly pageId: (page: PagePort) => string;
|
|
37
|
+
readonly correlation: () => string;
|
|
38
|
+
readonly clock: Clock;
|
|
39
|
+
/**
|
|
40
|
+
* How long the browser may take over what the router waits on: handing
|
|
41
|
+
* over a passed-through response's body, which a dialog holds back, and
|
|
42
|
+
* bringing a live stream's request to the stream server.
|
|
43
|
+
*/
|
|
44
|
+
readonly timeoutMs: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface HttpRouterEngine extends HttpRouter {
|
|
48
|
+
attach(attachment: AttachedContext): Promise<void>;
|
|
49
|
+
detach(attachment: AttachedContext): Promise<void>;
|
|
50
|
+
/**
|
|
51
|
+
* Wait for what is on its way to the timeline: the responses that came
|
|
52
|
+
* back — reading one is a round trip to the browser, waited on for
|
|
53
|
+
* `timeoutMs` at most — and the ends of the streams a stub closed.
|
|
54
|
+
*/
|
|
55
|
+
settle(): Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface Entry {
|
|
59
|
+
readonly rule: HttpRule;
|
|
60
|
+
readonly matchesUrl: (url: URL) => boolean;
|
|
61
|
+
readonly methods: ReadonlySet<string> | undefined;
|
|
62
|
+
remaining: number | undefined;
|
|
63
|
+
hits: number;
|
|
64
|
+
active: boolean;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
type RequestFrame = ReturnType<RequestPort["frame"]>;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* One request, as every rule that sees it shares it: one view, one
|
|
71
|
+
* correlation id, one raw request frame per protocol.
|
|
72
|
+
*/
|
|
73
|
+
interface Exchange {
|
|
74
|
+
readonly view: HttpRequestView;
|
|
75
|
+
readonly attachment: AttachedContext;
|
|
76
|
+
readonly route: RoutePort;
|
|
77
|
+
readonly request: RequestPort;
|
|
78
|
+
/** The frame that sent it; undefined for a service worker's request. */
|
|
79
|
+
readonly frame: RequestFrame | undefined;
|
|
80
|
+
/** Protocol → endpoint of the raw request frames on the timeline. */
|
|
81
|
+
readonly recorded: Map<string, string>;
|
|
82
|
+
/** Rules that passed the request on, newest first. */
|
|
83
|
+
readonly passed: Entry[];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface Installed {
|
|
87
|
+
readonly predicate: UrlPredicate;
|
|
88
|
+
readonly handler: RouteHandler;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
interface Listeners {
|
|
92
|
+
readonly finished: (request: RequestPort) => void;
|
|
93
|
+
readonly failed: (request: RequestPort) => void;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** A live stream redirected to the stream server, before the browser comes. */
|
|
97
|
+
interface Unclaimed {
|
|
98
|
+
readonly attachment: AttachedContext;
|
|
99
|
+
/** Stop waiting: the browser came, or `frame` says why it never will. */
|
|
100
|
+
readonly settle: (frame?: Closing) => void;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** A frame that ends an exchange, before the exchange's own fields. */
|
|
104
|
+
type Closing = Omit<
|
|
105
|
+
FrameInit,
|
|
106
|
+
"protocol" | "endpoint" | "page" | "context" | "correlation"
|
|
107
|
+
>;
|
|
108
|
+
|
|
109
|
+
type Answer = Exclude<HttpOutcome, { readonly type: "fallback" }>;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* How each outcome is carried out. A table keyed by the outcome's `type`,
|
|
113
|
+
* not a `switch`: an outcome added to the union and not here is a compile
|
|
114
|
+
* error, where a missing `case` would let the request hang.
|
|
115
|
+
*
|
|
116
|
+
* Each checks its answer before it touches the route: Playwright marks a
|
|
117
|
+
* route handled before it checks an answer, so one it refuses would leave
|
|
118
|
+
* no way to tell the page anything.
|
|
119
|
+
*/
|
|
120
|
+
type Executors = {
|
|
121
|
+
readonly [Type in Answer["type"]]: (
|
|
122
|
+
outcome: Extract<Answer, { readonly type: Type }>,
|
|
123
|
+
exchange: Exchange,
|
|
124
|
+
entry: Entry,
|
|
125
|
+
) => Promise<void>;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Playwright's abort codes. Keyed by the contract's type, so the two cannot
|
|
130
|
+
* drift: a code missing here, or one too many, does not compile.
|
|
131
|
+
*/
|
|
132
|
+
const ABORT_CODES: Readonly<Record<AbortErrorCode, true>> = {
|
|
133
|
+
aborted: true,
|
|
134
|
+
accessdenied: true,
|
|
135
|
+
addressunreachable: true,
|
|
136
|
+
blockedbyclient: true,
|
|
137
|
+
blockedbyresponse: true,
|
|
138
|
+
connectionaborted: true,
|
|
139
|
+
connectionclosed: true,
|
|
140
|
+
connectionfailed: true,
|
|
141
|
+
connectionrefused: true,
|
|
142
|
+
connectionreset: true,
|
|
143
|
+
internetdisconnected: true,
|
|
144
|
+
namenotresolved: true,
|
|
145
|
+
timedout: true,
|
|
146
|
+
failed: true,
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/** RFC 9110's `token`: what a header name may be made of. */
|
|
150
|
+
const HEADER_NAME = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
|
|
151
|
+
|
|
152
|
+
/** A host whose http pages are secure contexts. */
|
|
153
|
+
const LOOPBACK_HOST = /^(localhost|.+\.localhost|127(\.\d{1,3}){3}|\[::1\])$/;
|
|
154
|
+
|
|
155
|
+
/** A loopback address, as Chromium reports the server of a response. */
|
|
156
|
+
const LOOPBACK_ADDRESS = /^(127\.|::ffff:127\.|\[?::1\]?$)/;
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Refuse a header Chromium refuses: a name that is not a token, a value
|
|
160
|
+
* with a line break or NUL. Playwright reports such an answer sent while
|
|
161
|
+
* Chromium never sends it, and the request hangs.
|
|
162
|
+
*/
|
|
163
|
+
function checkHeader(name: string, value: string): void {
|
|
164
|
+
if (!HEADER_NAME.test(name))
|
|
165
|
+
throw new WirestageError(
|
|
166
|
+
"INVALID_ARGUMENT",
|
|
167
|
+
`${JSON.stringify(name)} is not a valid header name`,
|
|
168
|
+
);
|
|
169
|
+
if (/[\r\n\0]/.test(value))
|
|
170
|
+
throw new WirestageError(
|
|
171
|
+
"INVALID_ARGUMENT",
|
|
172
|
+
`the value of header "${name}" has a line break or NUL in it`,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* A fulfill's headers, as Chromium takes them. A value may carry several,
|
|
178
|
+
* one per line: Playwright sends `set-cookie` as a header line each, and
|
|
179
|
+
* any other header is folded into one value, `, `-separated, as HTTP joins
|
|
180
|
+
* repeated header lines.
|
|
181
|
+
*/
|
|
182
|
+
function fulfillHeaders(
|
|
183
|
+
headers: Readonly<Record<string, string>> = {},
|
|
184
|
+
): Record<string, string> {
|
|
185
|
+
const sent: Record<string, string> = {};
|
|
186
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
187
|
+
const values = value.split("\n");
|
|
188
|
+
for (const each of values) checkHeader(name, each);
|
|
189
|
+
sent[name] =
|
|
190
|
+
name.toLowerCase() === "set-cookie" ? value : values.join(", ");
|
|
191
|
+
}
|
|
192
|
+
return sent;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Where a `continue` goes: relative to the request, in its scheme. */
|
|
196
|
+
function continueUrl(target: string, request: URL): string {
|
|
197
|
+
const url = new URL(target, request);
|
|
198
|
+
if (url.protocol !== request.protocol)
|
|
199
|
+
throw new WirestageError(
|
|
200
|
+
"INVALID_ARGUMENT",
|
|
201
|
+
`continue cannot change the scheme (${request.protocol} to ${url.protocol}): Playwright rewrites a URL within its scheme only`,
|
|
202
|
+
);
|
|
203
|
+
return url.href;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Whether a page at `url` is a secure context: https, or http on loopback. */
|
|
207
|
+
const isSecure = ({ protocol, hostname }: URL): boolean =>
|
|
208
|
+
protocol === "https:" ||
|
|
209
|
+
(protocol === "http:" && LOOPBACK_HOST.test(hostname));
|
|
210
|
+
|
|
211
|
+
/** A request the browser reports failed, with its error text. */
|
|
212
|
+
const networkFault = (error: string | undefined): Closing => ({
|
|
213
|
+
kind: "fault",
|
|
214
|
+
direction: "none",
|
|
215
|
+
source: "network",
|
|
216
|
+
meta: { reason: "network", error },
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
/** A live stream whose request never reached the stream server. */
|
|
220
|
+
const unreached = (error: string): Closing => ({
|
|
221
|
+
kind: "fault",
|
|
222
|
+
direction: "none",
|
|
223
|
+
source: "stub",
|
|
224
|
+
meta: { reason: "unreached", error },
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
/** How a stream that ran its course is on record. */
|
|
228
|
+
const endOf = (end: StreamEnd): Closing =>
|
|
229
|
+
end === "cut"
|
|
230
|
+
? {
|
|
231
|
+
kind: "fault",
|
|
232
|
+
direction: "none",
|
|
233
|
+
source: "stub",
|
|
234
|
+
meta: { reason: "cut" },
|
|
235
|
+
}
|
|
236
|
+
: {
|
|
237
|
+
kind: "lifecycle",
|
|
238
|
+
direction: "none",
|
|
239
|
+
// The page let go of it, or the stub ended it.
|
|
240
|
+
source: end === "cancelled" ? "page" : "stub",
|
|
241
|
+
meta: { event: "close", end },
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const messageOf = (error: unknown): string =>
|
|
245
|
+
error instanceof Error ? error.message : String(error);
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Turns HTTP rules into ONE Playwright route per attached context and runs
|
|
249
|
+
* the chain itself: the newest rule that matches decides, and a `fallback`
|
|
250
|
+
* hands the request to the next.
|
|
251
|
+
*
|
|
252
|
+
* Running the chain here, rather than as one Playwright route per rule, is
|
|
253
|
+
* what makes a request one exchange however many rules see it: they share a
|
|
254
|
+
* view and a correlation id, `times` is claimed without an await between the
|
|
255
|
+
* check and the count, and a rule that passed a request on hears about the
|
|
256
|
+
* network's answer only when the network really gave one.
|
|
257
|
+
*/
|
|
258
|
+
export function createHttpRouter(deps: HttpRouterDeps): HttpRouterEngine {
|
|
259
|
+
/** Active rules, oldest first. */
|
|
260
|
+
const entries: Entry[] = [];
|
|
261
|
+
const attachments = new Map<AttachedContext, Listeners>();
|
|
262
|
+
const routes = new Map<AttachedContext, Promise<Installed>>();
|
|
263
|
+
const pending = new WeakMap<RequestPort, Exchange>();
|
|
264
|
+
/** Live streams whose request the browser has not brought yet. */
|
|
265
|
+
const unclaimed = new Map<RequestPort, Unclaimed>();
|
|
266
|
+
/** Live streams the stream server is serving. */
|
|
267
|
+
const serving = new Set<ServedStream>();
|
|
268
|
+
/** Where each frame's document came from, as its navigation reported. */
|
|
269
|
+
const documents = new WeakMap<
|
|
270
|
+
RequestFrame,
|
|
271
|
+
{ readonly origin: string; readonly loopback: boolean }
|
|
272
|
+
>();
|
|
273
|
+
/** Responses being read off the browser, on their way to the timeline. */
|
|
274
|
+
const settling = new Set<Promise<void>>();
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* The origin a live stream from `frame` needs `local-network-access`
|
|
278
|
+
* for, if any. Chromium's Local Network Access stops a page it cannot
|
|
279
|
+
* place in an address space — any page a route fulfilled — from reaching
|
|
280
|
+
* the loopback stream server unless its origin holds the permission. A
|
|
281
|
+
* page a loopback server really served needs none, and one that is not a
|
|
282
|
+
* secure context is refused loopback, granted or not.
|
|
283
|
+
*/
|
|
284
|
+
const grantFor = (frame: RequestFrame | undefined): string | undefined => {
|
|
285
|
+
// A service worker's request has no frame, and a frame that has shown
|
|
286
|
+
// no document yet no URL: neither has an origin to grant.
|
|
287
|
+
if (frame === undefined || !URL.canParse(frame.url())) return undefined;
|
|
288
|
+
const url = new URL(frame.url());
|
|
289
|
+
if (!isSecure(url)) return undefined;
|
|
290
|
+
const document = documents.get(frame);
|
|
291
|
+
return document?.origin === url.origin && document.loopback
|
|
292
|
+
? undefined
|
|
293
|
+
: url.origin;
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Granted per stream, to that page's origin alone: Playwright merges it
|
|
298
|
+
* with the grants the test made for that origin, and a test that cleared
|
|
299
|
+
* its permissions has it back with the next stream. A browser without
|
|
300
|
+
* the permission refuses it, and then needs none.
|
|
301
|
+
*/
|
|
302
|
+
const allowLocalNetwork = async ({
|
|
303
|
+
attachment,
|
|
304
|
+
frame,
|
|
305
|
+
}: Exchange): Promise<void> => {
|
|
306
|
+
const origin = grantFor(frame);
|
|
307
|
+
if (origin === undefined) return;
|
|
308
|
+
try {
|
|
309
|
+
await attachment.context.grantPermissions?.(["local-network-access"], {
|
|
310
|
+
origin,
|
|
311
|
+
});
|
|
312
|
+
} catch (error) {
|
|
313
|
+
deps.log.debug("local-network-access not granted", { origin }, error);
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
/** Note where a frame's document came from, for `grantFor`. */
|
|
318
|
+
const noteDocument = async (request: RequestPort): Promise<void> => {
|
|
319
|
+
try {
|
|
320
|
+
const frame = request.frame();
|
|
321
|
+
const server = await (await request.response())?.serverAddr();
|
|
322
|
+
documents.set(frame, {
|
|
323
|
+
origin: new URL(request.url()).origin,
|
|
324
|
+
loopback: server != null && LOOPBACK_ADDRESS.test(server.ipAddress),
|
|
325
|
+
});
|
|
326
|
+
} catch (error) {
|
|
327
|
+
// A service worker's document has no frame, a closed page no answer.
|
|
328
|
+
deps.log.debug(
|
|
329
|
+
"could not tell where a document came from",
|
|
330
|
+
{ url: request.url() },
|
|
331
|
+
error,
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
/** The frame and the page a request came from; a service worker's has neither. */
|
|
337
|
+
const senderOf = (
|
|
338
|
+
request: RequestPort,
|
|
339
|
+
): { readonly frame?: RequestFrame; readonly page?: string } => {
|
|
340
|
+
try {
|
|
341
|
+
const frame = request.frame();
|
|
342
|
+
return { frame, page: deps.pageId(frame.page()) };
|
|
343
|
+
} catch {
|
|
344
|
+
// Service-worker requests have no frame.
|
|
345
|
+
return {};
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
const createView = async (
|
|
350
|
+
request: RequestPort,
|
|
351
|
+
attachment: AttachedContext,
|
|
352
|
+
url: URL,
|
|
353
|
+
page: string | undefined,
|
|
354
|
+
): Promise<HttpRequestView> => {
|
|
355
|
+
let body: Uint8Array | undefined;
|
|
356
|
+
let correlation: string | undefined;
|
|
357
|
+
const getBody = () => {
|
|
358
|
+
body ??= toBytes(request.postDataBuffer() ?? new Uint8Array(0));
|
|
359
|
+
return body;
|
|
360
|
+
};
|
|
361
|
+
return {
|
|
362
|
+
get correlation() {
|
|
363
|
+
correlation ??= deps.correlation();
|
|
364
|
+
return correlation;
|
|
365
|
+
},
|
|
366
|
+
method: request.method(),
|
|
367
|
+
url,
|
|
368
|
+
headers: await request.allHeaders(),
|
|
369
|
+
page,
|
|
370
|
+
context: attachment.id,
|
|
371
|
+
resourceType: request.resourceType(),
|
|
372
|
+
body: getBody,
|
|
373
|
+
text: () => utf8(getBody()),
|
|
374
|
+
json: <T>() => JSON.parse(utf8(getBody())) as T,
|
|
375
|
+
};
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
const append = (
|
|
379
|
+
{ view }: Exchange,
|
|
380
|
+
protocol: string,
|
|
381
|
+
endpoint: string,
|
|
382
|
+
frame: Closing,
|
|
383
|
+
) =>
|
|
384
|
+
deps.timeline.append({
|
|
385
|
+
...frame,
|
|
386
|
+
protocol,
|
|
387
|
+
endpoint,
|
|
388
|
+
page: view.page,
|
|
389
|
+
context: view.context,
|
|
390
|
+
correlation: view.correlation,
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
/** End the exchange for every protocol that recorded its request. */
|
|
394
|
+
const close = (exchange: Exchange, frame: Closing) => {
|
|
395
|
+
for (const [protocol, endpoint] of exchange.recorded)
|
|
396
|
+
append(exchange, protocol, endpoint, frame);
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* A rule's URL test, which is the caller's code when the rule was given a
|
|
401
|
+
* predicate. Playwright calls this for every request of the context, so
|
|
402
|
+
* one that throws would break them all: it counts as "no match" instead.
|
|
403
|
+
*/
|
|
404
|
+
const wants = (entry: Entry, url: URL): boolean => {
|
|
405
|
+
try {
|
|
406
|
+
return entry.matchesUrl(url);
|
|
407
|
+
} catch (error) {
|
|
408
|
+
deps.log.error(
|
|
409
|
+
"rule's URL matcher failed",
|
|
410
|
+
{ rule: entry.rule.label, url: url.href },
|
|
411
|
+
error,
|
|
412
|
+
);
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
/** The rule's name for the request; the path when it has none to give. */
|
|
418
|
+
const nameOf = ({ rule }: Entry, view: HttpRequestView): string => {
|
|
419
|
+
try {
|
|
420
|
+
return rule.endpoint?.(view) ?? view.url.pathname;
|
|
421
|
+
} catch {
|
|
422
|
+
// Reached only while reporting a rule that already failed.
|
|
423
|
+
return view.url.pathname;
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
const retire = (entry: Entry) => {
|
|
428
|
+
if (!entry.active) return;
|
|
429
|
+
entry.active = false;
|
|
430
|
+
entries.splice(entries.indexOf(entry), 1);
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Whether `entry` takes the request, and its decision if so. The checks
|
|
435
|
+
* and the count are one synchronous step: two requests in flight cannot
|
|
436
|
+
* both take the last of a rule's `times`.
|
|
437
|
+
*/
|
|
438
|
+
const decide = async (
|
|
439
|
+
entry: Entry,
|
|
440
|
+
exchange: Exchange,
|
|
441
|
+
): Promise<HttpOutcome | undefined> => {
|
|
442
|
+
const { rule } = entry;
|
|
443
|
+
const { view } = exchange;
|
|
444
|
+
if (!entry.active) return undefined;
|
|
445
|
+
if (entry.methods !== undefined && !entry.methods.has(view.method))
|
|
446
|
+
return undefined;
|
|
447
|
+
if (rule.match !== undefined && !rule.match(view)) return undefined;
|
|
448
|
+
entry.hits += 1;
|
|
449
|
+
if (entry.remaining !== undefined && --entry.remaining === 0) retire(entry);
|
|
450
|
+
if (rule.record !== false && !exchange.recorded.has(rule.protocol)) {
|
|
451
|
+
const endpoint = rule.endpoint?.(view) ?? view.url.pathname;
|
|
452
|
+
exchange.recorded.set(rule.protocol, endpoint);
|
|
453
|
+
append(exchange, rule.protocol, endpoint, {
|
|
454
|
+
kind: "request",
|
|
455
|
+
direction: "out",
|
|
456
|
+
source: "page",
|
|
457
|
+
data: () => view.body(),
|
|
458
|
+
meta: {
|
|
459
|
+
method: view.method,
|
|
460
|
+
url: view.url.href,
|
|
461
|
+
headers: view.headers,
|
|
462
|
+
},
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
return rule.handle(view);
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
/** The request goes to the network: remember who wants to hear the answer. */
|
|
469
|
+
const track = (exchange: Exchange) => {
|
|
470
|
+
if (
|
|
471
|
+
exchange.recorded.size > 0 ||
|
|
472
|
+
exchange.passed.some(
|
|
473
|
+
({ rule }) =>
|
|
474
|
+
rule.onPassthroughResponse !== undefined ||
|
|
475
|
+
rule.onPassthroughFailure !== undefined,
|
|
476
|
+
)
|
|
477
|
+
)
|
|
478
|
+
pending.set(exchange.request, exchange);
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
/** A request that outlived its context fails for that reason alone. */
|
|
482
|
+
const levelOf = ({ attachment }: Exchange): "error" | "debug" =>
|
|
483
|
+
attachments.has(attachment) ? "error" : "debug";
|
|
484
|
+
|
|
485
|
+
/** What the page is told of a rule that failed; the app may display it. */
|
|
486
|
+
const failureBody = ({ rule }: Entry, error: unknown): string =>
|
|
487
|
+
`wirestage: rule "${rule.label}" failed: ${messageOf(error)}`;
|
|
488
|
+
|
|
489
|
+
/** Say which rule failed: in the log, and on the timeline. */
|
|
490
|
+
const report = (exchange: Exchange, entry: Entry, error: unknown) => {
|
|
491
|
+
const { rule } = entry;
|
|
492
|
+
deps.log[levelOf(exchange)](
|
|
493
|
+
"rule failed",
|
|
494
|
+
{ rule: rule.label, url: exchange.view.url.href },
|
|
495
|
+
error,
|
|
496
|
+
);
|
|
497
|
+
append(exchange, rule.protocol, nameOf(entry, exchange.view), {
|
|
498
|
+
kind: "fault",
|
|
499
|
+
direction: "none",
|
|
500
|
+
source: "stub",
|
|
501
|
+
meta: { reason: "rule-error", error: messageOf(error) },
|
|
502
|
+
});
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Serve a live stream once the browser has brought its request to the
|
|
507
|
+
* stream server. A rule that throws there is a rule that failed, as a
|
|
508
|
+
* throwing `fulfill` is — not the page letting go.
|
|
509
|
+
*/
|
|
510
|
+
const serveStream =
|
|
511
|
+
(
|
|
512
|
+
outcome: Extract<Answer, { readonly type: "stream" }>,
|
|
513
|
+
exchange: Exchange,
|
|
514
|
+
entry: Entry,
|
|
515
|
+
): NodeHandler =>
|
|
516
|
+
async (request, response) => {
|
|
517
|
+
const served = createResponseStream(request, response, (end, failed) => {
|
|
518
|
+
serving.delete(served);
|
|
519
|
+
// A rule that failed is on record as its error, not as an end.
|
|
520
|
+
if (!failed) close(exchange, endOf(end));
|
|
521
|
+
});
|
|
522
|
+
serving.add(served);
|
|
523
|
+
try {
|
|
524
|
+
await outcome.open(served.stream);
|
|
525
|
+
} catch (error) {
|
|
526
|
+
if (served.fail(failureBody(entry, error)))
|
|
527
|
+
report(exchange, entry, error);
|
|
528
|
+
// It was over first, and is on record as such: a write into a
|
|
529
|
+
// connection the page let go of, or one the rule ended.
|
|
530
|
+
else
|
|
531
|
+
deps.log.debug(
|
|
532
|
+
"a stream's rule failed after the stream was over",
|
|
533
|
+
{ rule: entry.rule.label },
|
|
534
|
+
error,
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Wait for the browser to bring a redirected stream to the stream
|
|
541
|
+
* server. Until it does, the session holds a place on a listener and the
|
|
542
|
+
* exchange has no end — and a page that let go first, while the rule
|
|
543
|
+
* decided or just after the redirect, never comes: Playwright takes the
|
|
544
|
+
* redirect all the same. Given up when the request fails, the context is
|
|
545
|
+
* detached, or `timeoutMs` passes.
|
|
546
|
+
*/
|
|
547
|
+
const awaitBrowser = (exchange: Exchange, session: StreamSession): void => {
|
|
548
|
+
const release = (frame: Closing) => {
|
|
549
|
+
session.dispose();
|
|
550
|
+
close(exchange, frame);
|
|
551
|
+
};
|
|
552
|
+
const failure = exchange.request.failure();
|
|
553
|
+
if (failure !== null) {
|
|
554
|
+
// It failed while the rule decided or the redirect was made.
|
|
555
|
+
release(networkFault(failure.errorText));
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
const deadline = new AbortController();
|
|
559
|
+
const settle = (frame?: Closing) => {
|
|
560
|
+
unclaimed.delete(exchange.request);
|
|
561
|
+
deadline.abort();
|
|
562
|
+
if (frame !== undefined) release(frame);
|
|
563
|
+
};
|
|
564
|
+
unclaimed.set(exchange.request, {
|
|
565
|
+
attachment: exchange.attachment,
|
|
566
|
+
settle,
|
|
567
|
+
});
|
|
568
|
+
void deps.clock.sleep(deps.timeoutMs, deadline.signal).then(
|
|
569
|
+
() => {
|
|
570
|
+
deps.log.warn(
|
|
571
|
+
"a live stream's request never reached the stream server, and was given up",
|
|
572
|
+
{ url: exchange.view.url.href, timeoutMs: deps.timeoutMs },
|
|
573
|
+
);
|
|
574
|
+
settle(
|
|
575
|
+
unreached(
|
|
576
|
+
`the browser did not bring the request to the stream server within ${deps.timeoutMs} ms`,
|
|
577
|
+
),
|
|
578
|
+
);
|
|
579
|
+
},
|
|
580
|
+
// It came, failed or was detached first.
|
|
581
|
+
() => undefined,
|
|
582
|
+
);
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
const execute: Executors = {
|
|
586
|
+
async fulfill(outcome, exchange) {
|
|
587
|
+
const status = outcome.status ?? 200;
|
|
588
|
+
const headers = fulfillHeaders(outcome.headers);
|
|
589
|
+
const body =
|
|
590
|
+
outcome.body === undefined ? undefined : toBytes(outcome.body);
|
|
591
|
+
await exchange.route.fulfill(
|
|
592
|
+
body === undefined
|
|
593
|
+
? { status, headers }
|
|
594
|
+
: { status, headers, body: toBuffer(body) },
|
|
595
|
+
);
|
|
596
|
+
close(exchange, {
|
|
597
|
+
kind: "response",
|
|
598
|
+
direction: "in",
|
|
599
|
+
source: "stub",
|
|
600
|
+
...(body === undefined ? {} : { data: body }),
|
|
601
|
+
meta: { status, headers },
|
|
602
|
+
});
|
|
603
|
+
},
|
|
604
|
+
async stream(outcome, exchange, entry) {
|
|
605
|
+
let claimed = false;
|
|
606
|
+
const serve = serveStream(outcome, exchange, entry);
|
|
607
|
+
const session = await deps.streams.open(
|
|
608
|
+
(request, response) => {
|
|
609
|
+
claimed = true;
|
|
610
|
+
unclaimed.get(exchange.request)?.settle();
|
|
611
|
+
return serve(request, response);
|
|
612
|
+
},
|
|
613
|
+
{ secure: exchange.view.url.protocol === "https:" },
|
|
614
|
+
);
|
|
615
|
+
await allowLocalNetwork(exchange);
|
|
616
|
+
try {
|
|
617
|
+
await exchange.route.continue({ url: session.url });
|
|
618
|
+
} catch (error) {
|
|
619
|
+
session.dispose();
|
|
620
|
+
throw error;
|
|
621
|
+
}
|
|
622
|
+
// The browser may come before Playwright says the redirect is made.
|
|
623
|
+
if (!claimed) awaitBrowser(exchange, session);
|
|
624
|
+
},
|
|
625
|
+
async continue(outcome, exchange, entry) {
|
|
626
|
+
const url =
|
|
627
|
+
outcome.url === undefined
|
|
628
|
+
? undefined
|
|
629
|
+
: continueUrl(outcome.url, exchange.view.url);
|
|
630
|
+
for (const [name, value] of Object.entries(outcome.headers ?? {}))
|
|
631
|
+
checkHeader(name, value);
|
|
632
|
+
exchange.passed.push(entry);
|
|
633
|
+
track(exchange);
|
|
634
|
+
await exchange.route.continue({
|
|
635
|
+
...(url === undefined ? {} : { url }),
|
|
636
|
+
...(outcome.method === undefined ? {} : { method: outcome.method }),
|
|
637
|
+
...(outcome.headers === undefined
|
|
638
|
+
? {}
|
|
639
|
+
: { headers: { ...outcome.headers } }),
|
|
640
|
+
...(outcome.body === undefined
|
|
641
|
+
? {}
|
|
642
|
+
: { postData: toBuffer(toBytes(outcome.body)) }),
|
|
643
|
+
});
|
|
644
|
+
},
|
|
645
|
+
async abort(outcome, exchange) {
|
|
646
|
+
const error = outcome.error ?? "failed";
|
|
647
|
+
if (!Object.hasOwn(ABORT_CODES, error))
|
|
648
|
+
throw new WirestageError(
|
|
649
|
+
"INVALID_ARGUMENT",
|
|
650
|
+
`"${error}" is not an abort error code; Playwright takes ${Object.keys(ABORT_CODES).join(", ")}`,
|
|
651
|
+
);
|
|
652
|
+
await exchange.route.abort(error);
|
|
653
|
+
close(exchange, {
|
|
654
|
+
kind: "fault",
|
|
655
|
+
direction: "none",
|
|
656
|
+
source: "stub",
|
|
657
|
+
meta: { reason: "abort", error },
|
|
658
|
+
});
|
|
659
|
+
},
|
|
660
|
+
};
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* A throwing stub is a bug in the test, as is an answer Playwright or
|
|
664
|
+
* Chromium would refuse, and a stream that cannot open is a setup
|
|
665
|
+
* problem. Either way answer 500 so the page sees an error instead of
|
|
666
|
+
* hanging, and say which rule failed — in the log AND in the body the app
|
|
667
|
+
* may display.
|
|
668
|
+
*/
|
|
669
|
+
const fail = async (exchange: Exchange, entry: Entry, error: unknown) => {
|
|
670
|
+
report(exchange, entry, error);
|
|
671
|
+
await exchange.route
|
|
672
|
+
.fulfill({
|
|
673
|
+
status: 500,
|
|
674
|
+
headers: { "content-type": "text/plain; charset=utf-8" },
|
|
675
|
+
body: failureBody(entry, error),
|
|
676
|
+
})
|
|
677
|
+
.catch((secondary: unknown) => {
|
|
678
|
+
// The route took an answer before the failure, and Playwright
|
|
679
|
+
// takes no other: the page waits for good.
|
|
680
|
+
deps.log[levelOf(exchange)](
|
|
681
|
+
"could not answer a failed rule: the request is left without an answer",
|
|
682
|
+
{ rule: entry.rule.label, url: exchange.view.url.href },
|
|
683
|
+
secondary,
|
|
684
|
+
);
|
|
685
|
+
});
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
const dispatch = async (
|
|
689
|
+
attachment: AttachedContext,
|
|
690
|
+
route: RoutePort,
|
|
691
|
+
request: RequestPort,
|
|
692
|
+
): Promise<void> => {
|
|
693
|
+
const url = new URL(request.url());
|
|
694
|
+
// Newest first. A snapshot: a rule added while this request is being
|
|
695
|
+
// decided does not join its chain.
|
|
696
|
+
const chain = entries.filter((entry) => wants(entry, url)).reverse();
|
|
697
|
+
if (chain.length === 0) return route.fallback();
|
|
698
|
+
const sender = senderOf(request);
|
|
699
|
+
const exchange: Exchange = {
|
|
700
|
+
view: await createView(request, attachment, url, sender.page),
|
|
701
|
+
attachment,
|
|
702
|
+
route,
|
|
703
|
+
request,
|
|
704
|
+
frame: sender.frame,
|
|
705
|
+
recorded: new Map(),
|
|
706
|
+
passed: [],
|
|
707
|
+
};
|
|
708
|
+
for (const entry of chain) {
|
|
709
|
+
try {
|
|
710
|
+
const outcome = await decide(entry, exchange);
|
|
711
|
+
if (outcome === undefined) continue;
|
|
712
|
+
if (outcome.type === "fallback") {
|
|
713
|
+
exchange.passed.push(entry);
|
|
714
|
+
continue;
|
|
715
|
+
}
|
|
716
|
+
// The table has an executor for every answer; the cast is only
|
|
717
|
+
// because TypeScript cannot tie `outcome` to its own key.
|
|
718
|
+
await (execute[outcome.type] as Executors[Answer["type"]])(
|
|
719
|
+
outcome as never,
|
|
720
|
+
exchange,
|
|
721
|
+
entry,
|
|
722
|
+
);
|
|
723
|
+
} catch (error) {
|
|
724
|
+
await fail(exchange, entry, error);
|
|
725
|
+
}
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
track(exchange);
|
|
729
|
+
await route.fallback();
|
|
730
|
+
};
|
|
731
|
+
|
|
732
|
+
const onRoute = async (
|
|
733
|
+
attachment: AttachedContext,
|
|
734
|
+
route: RoutePort,
|
|
735
|
+
request: RequestPort,
|
|
736
|
+
): Promise<void> => {
|
|
737
|
+
try {
|
|
738
|
+
await dispatch(attachment, route, request);
|
|
739
|
+
} catch (error) {
|
|
740
|
+
// Reading the request or handing it on failed: the page or context
|
|
741
|
+
// closed under it. Never rejected to Playwright, where it would
|
|
742
|
+
// surface as an unhandled rejection in whichever test runs next.
|
|
743
|
+
const level = attachments.has(attachment) ? "warn" : "debug";
|
|
744
|
+
deps.log[level]("request abandoned", { url: request.url() }, error);
|
|
745
|
+
}
|
|
746
|
+
};
|
|
747
|
+
|
|
748
|
+
const ensureRoute = async (attachment: AttachedContext): Promise<void> => {
|
|
749
|
+
let installing = routes.get(attachment);
|
|
750
|
+
if (installing === undefined) {
|
|
751
|
+
const installed: Installed = {
|
|
752
|
+
predicate: (url) => entries.some((entry) => wants(entry, url)),
|
|
753
|
+
handler: (route, request) => onRoute(attachment, route, request),
|
|
754
|
+
};
|
|
755
|
+
installing = attachment.context
|
|
756
|
+
.route(installed.predicate, installed.handler)
|
|
757
|
+
.then(() => installed);
|
|
758
|
+
routes.set(attachment, installing);
|
|
759
|
+
// A failed install must not be remembered as done.
|
|
760
|
+
installing.catch(() => routes.delete(attachment));
|
|
761
|
+
}
|
|
762
|
+
await installing;
|
|
763
|
+
};
|
|
764
|
+
|
|
765
|
+
/** One observer failing must not starve the others, nor the timeline. */
|
|
766
|
+
const notify = (entry: Entry, call: (rule: HttpRule) => void) => {
|
|
767
|
+
try {
|
|
768
|
+
call(entry.rule);
|
|
769
|
+
} catch (error) {
|
|
770
|
+
deps.log.error(
|
|
771
|
+
"passthrough observer failed",
|
|
772
|
+
{ rule: entry.rule.label },
|
|
773
|
+
error,
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
};
|
|
777
|
+
|
|
778
|
+
/** Record the network's answer to a request a rule passed on. */
|
|
779
|
+
const record = async (exchange: Exchange, request: RequestPort) => {
|
|
780
|
+
try {
|
|
781
|
+
const response = await request.response();
|
|
782
|
+
if (response === null) return;
|
|
783
|
+
const body = await response.body().then(
|
|
784
|
+
(buffer) => toBytes(buffer),
|
|
785
|
+
() => undefined,
|
|
786
|
+
);
|
|
787
|
+
const passthrough: PassthroughResponse = {
|
|
788
|
+
status: response.status(),
|
|
789
|
+
headers: response.headers(),
|
|
790
|
+
body,
|
|
791
|
+
};
|
|
792
|
+
close(exchange, {
|
|
793
|
+
kind: "response",
|
|
794
|
+
direction: "in",
|
|
795
|
+
source: "network",
|
|
796
|
+
...(body === undefined ? {} : { data: body }),
|
|
797
|
+
meta: { status: passthrough.status, headers: passthrough.headers },
|
|
798
|
+
});
|
|
799
|
+
for (const entry of exchange.passed)
|
|
800
|
+
notify(entry, (rule) =>
|
|
801
|
+
rule.onPassthroughResponse?.(passthrough, exchange.view),
|
|
802
|
+
);
|
|
803
|
+
} catch (error) {
|
|
804
|
+
deps.log.warn(
|
|
805
|
+
"could not read a passed-through response",
|
|
806
|
+
{ url: exchange.view.url.href },
|
|
807
|
+
error,
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
};
|
|
811
|
+
|
|
812
|
+
/** Whether a finished request was passed on, and its recording if so. */
|
|
813
|
+
const onFinished = (request: RequestPort): Promise<void> | undefined => {
|
|
814
|
+
const exchange = pending.get(request);
|
|
815
|
+
if (exchange === undefined) return undefined;
|
|
816
|
+
pending.delete(request);
|
|
817
|
+
return record(exchange, request);
|
|
818
|
+
};
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* `reading`, as long as a read waits for it. Reading a body is a round
|
|
822
|
+
* trip to the page, which a dialog holds up until it closes: a read waits
|
|
823
|
+
* as long as a page is given to answer, then goes on, and the response
|
|
824
|
+
* reaches the timeline when its body does.
|
|
825
|
+
*/
|
|
826
|
+
const bounded = (reading: Promise<void>, url: string): Promise<void> => {
|
|
827
|
+
const waiting = new AbortController();
|
|
828
|
+
return Promise.race([
|
|
829
|
+
reading,
|
|
830
|
+
deps.clock.sleep(deps.timeoutMs, waiting.signal).then(
|
|
831
|
+
() =>
|
|
832
|
+
deps.log.warn(
|
|
833
|
+
"a response did not hand over its body in time; reads go on without it until it does (is a dialog open?)",
|
|
834
|
+
{ url, timeoutMs: deps.timeoutMs },
|
|
835
|
+
),
|
|
836
|
+
// The body came first.
|
|
837
|
+
() => undefined,
|
|
838
|
+
),
|
|
839
|
+
]).finally(() => waiting.abort());
|
|
840
|
+
};
|
|
841
|
+
|
|
842
|
+
const onFailed = (request: RequestPort) => {
|
|
843
|
+
const error = request.failure()?.errorText;
|
|
844
|
+
// A live stream the page let go of before the browser could bring it.
|
|
845
|
+
unclaimed.get(request)?.settle(networkFault(error));
|
|
846
|
+
const exchange = pending.get(request);
|
|
847
|
+
if (exchange === undefined) return;
|
|
848
|
+
pending.delete(request);
|
|
849
|
+
close(exchange, networkFault(error));
|
|
850
|
+
for (const entry of exchange.passed)
|
|
851
|
+
notify(entry, (rule) =>
|
|
852
|
+
rule.onPassthroughFailure?.(error, exchange.view),
|
|
853
|
+
);
|
|
854
|
+
};
|
|
855
|
+
|
|
856
|
+
return {
|
|
857
|
+
async add(rule): Promise<HttpRuleHandle> {
|
|
858
|
+
if (
|
|
859
|
+
rule.times !== undefined &&
|
|
860
|
+
(!Number.isInteger(rule.times) || rule.times < 1)
|
|
861
|
+
)
|
|
862
|
+
throw new WirestageError(
|
|
863
|
+
"INVALID_ARGUMENT",
|
|
864
|
+
`rule "${rule.label}": times must be a positive integer, got ${rule.times}`,
|
|
865
|
+
);
|
|
866
|
+
const methods =
|
|
867
|
+
rule.method === undefined
|
|
868
|
+
? undefined
|
|
869
|
+
: new Set(
|
|
870
|
+
(typeof rule.method === "string"
|
|
871
|
+
? [rule.method]
|
|
872
|
+
: rule.method
|
|
873
|
+
).map((m) => m.toUpperCase()),
|
|
874
|
+
);
|
|
875
|
+
const entry: Entry = {
|
|
876
|
+
rule,
|
|
877
|
+
matchesUrl: compileUrlMatcher(rule.url),
|
|
878
|
+
methods,
|
|
879
|
+
remaining: rule.times,
|
|
880
|
+
hits: 0,
|
|
881
|
+
active: true,
|
|
882
|
+
};
|
|
883
|
+
entries.push(entry);
|
|
884
|
+
try {
|
|
885
|
+
await Promise.all([...attachments.keys()].map(ensureRoute));
|
|
886
|
+
} catch (error) {
|
|
887
|
+
// Half-installed is worse than not installed: the caller got an
|
|
888
|
+
// error, so the rule must not answer anywhere.
|
|
889
|
+
retire(entry);
|
|
890
|
+
throw error;
|
|
891
|
+
}
|
|
892
|
+
return {
|
|
893
|
+
get hits() {
|
|
894
|
+
return entry.hits;
|
|
895
|
+
},
|
|
896
|
+
get active() {
|
|
897
|
+
return entry.active;
|
|
898
|
+
},
|
|
899
|
+
async remove() {
|
|
900
|
+
retire(entry);
|
|
901
|
+
},
|
|
902
|
+
};
|
|
903
|
+
},
|
|
904
|
+
async attach(attachment) {
|
|
905
|
+
const own: Listeners = {
|
|
906
|
+
finished: (request) => {
|
|
907
|
+
if (request.resourceType() === "document") void noteDocument(request);
|
|
908
|
+
const reading = onFinished(request);
|
|
909
|
+
if (reading === undefined) return;
|
|
910
|
+
const waiting = bounded(reading, request.url());
|
|
911
|
+
settling.add(waiting);
|
|
912
|
+
void waiting.then(() => settling.delete(waiting));
|
|
913
|
+
},
|
|
914
|
+
failed: onFailed,
|
|
915
|
+
};
|
|
916
|
+
attachments.set(attachment, own);
|
|
917
|
+
attachment.context.on("requestfinished", own.finished);
|
|
918
|
+
attachment.context.on("requestfailed", own.failed);
|
|
919
|
+
// Routing turns the browser's HTTP cache off and puts every request
|
|
920
|
+
// through Playwright, so a stage with no rules installs nothing.
|
|
921
|
+
if (entries.length > 0) await ensureRoute(attachment);
|
|
922
|
+
},
|
|
923
|
+
async detach(attachment) {
|
|
924
|
+
const own = attachments.get(attachment);
|
|
925
|
+
if (own === undefined) return;
|
|
926
|
+
attachments.delete(attachment);
|
|
927
|
+
attachment.context.off("requestfinished", own.finished);
|
|
928
|
+
attachment.context.off("requestfailed", own.failed);
|
|
929
|
+
// Nothing brings these now, and each holds a place on a listener.
|
|
930
|
+
for (const waiting of [...unclaimed.values()])
|
|
931
|
+
if (waiting.attachment === attachment)
|
|
932
|
+
waiting.settle(unreached("the context was detached first"));
|
|
933
|
+
const installing = routes.get(attachment);
|
|
934
|
+
routes.delete(attachment);
|
|
935
|
+
if (installing === undefined) return;
|
|
936
|
+
try {
|
|
937
|
+
const { predicate, handler } = await installing;
|
|
938
|
+
await attachment.context.unroute(predicate, handler);
|
|
939
|
+
} catch (error) {
|
|
940
|
+
// A context that closed first has no route left to remove.
|
|
941
|
+
deps.log.debug("unroute failed", { context: attachment.id }, error);
|
|
942
|
+
}
|
|
943
|
+
},
|
|
944
|
+
async settle() {
|
|
945
|
+
await Promise.all([
|
|
946
|
+
...settling,
|
|
947
|
+
// A stream the stub ended or cut is on its way to the timeline.
|
|
948
|
+
...[...serving]
|
|
949
|
+
.filter((served) => served.closing)
|
|
950
|
+
.map((served) => served.stream.closed),
|
|
951
|
+
]);
|
|
952
|
+
},
|
|
953
|
+
};
|
|
954
|
+
}
|