chore(protocol): make timeout a protocol-level field#41712
Conversation
Follows #41141, which made signal a protocol-level field. This does the same for timeout: every message carries it on Metadata, computed once on the client and read from progress.timeout on the server, instead of each method declaring its own timeout: float. 0 is the sentinel for no deadline.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
…ut-field # Conflicts: # packages/playwright-core/src/client/channels.d.ts # packages/playwright-core/src/server/channels.d.ts # packages/protocol/src/validator.ts
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
| addScheme(`${derived}${titleCase(methodName)}ErrorDetails`, `tType('${errorDetailsName}')`); | ||
| } | ||
| channels_ts.push(` ${methodName}(params: ${paramsName}, ${isDispatcher ? `progress: Progress` : `signal: AbortSignal | undefined`}): Promise<${resultName}>;`); | ||
| channels_ts.push(` ${methodName}(params: ${paramsName}, ${isDispatcher ? `progress: Progress` : `options?: CommandOptions`}): Promise<${resultName}>;`); |
There was a problem hiding this comment.
Why are options now optional? Let's make them mandatory (like we had with the signal), so every caller thinks about it?
|
|
||
| async uncheck(options: channels.ElementHandleUncheckOptions & TimeoutOptions = {}) { | ||
| return await this._elementChannel.uncheck({ ...options, timeout: this._frame._timeout(options) }, options.signal); | ||
| return await this._elementChannel.uncheck({ ...options }, { signal: options.signal, timeout: this._frame._timeout(options) }); |
There was a problem hiding this comment.
Let's make all the helper methods like this._frame._timeout(options) handle both timeout and signal - this way the code will be much easier everywhere.
| export type { Binary, Channel, ${structNames.join(', ')} } from '@protocol/structs'; | ||
| `]; | ||
| ` + (target === 'Dispatcher' ? '' : ` | ||
| export type CommandOptions = { signal?: AbortSignal, timeout?: number }; |
There was a problem hiding this comment.
Make both properties required: { signal: AbortSignal | undefined, timeout: number } to ensure all callers doing the right thing?
Make signal and timeout required fields on the generated CommandOptions struct, and pass them explicitly at every channel call site. Internal calls that opt out of a deadline use timeout: 0 as the disabled sentinel.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
| this._timeoutSettings.setDefaultTimeout(timeout); | ||
| } | ||
|
|
||
| _timeout(options?: types.TimeoutOptions): channels.CommandOptions { |
There was a problem hiding this comment.
- Call this
_timeoutOptions()now? - Rename
channels.CommandOptionstochannels.TimeoutOptions?
| if (this._connection.isRemote()) | ||
| throw new Error(`Path is not available when connecting remotely. Use saveAs() to save a local copy.`); | ||
| return (await this._channel.pathAfterFinished({}, undefined)).value; | ||
| return (await this._channel.pathAfterFinished({}, { signal: undefined, timeout: 0 })).value; |
There was a problem hiding this comment.
WDYT about exporting a kNoTimeout constant from some common helper, e.g. TimeoutSettings.ts?
| }; | ||
| return await this._wrapApiCall(async () => { | ||
| const browser = Browser.from((await this._channel.launch(launchOptions, options.signal)).browser); | ||
| const browser = Browser.from((await this._channel.launch(launchOptions, { signal: options.signal, timeout: new TimeoutSettings().launchTimeout(options) })).browser); |
There was a problem hiding this comment.
TimeoutSettings.launchTimeout might as well handle the signal now.
| noDefaults: params.noDefaults, | ||
| artifactsDir: params.artifactsDir, | ||
| }, undefined); | ||
| }, { signal: undefined, timeout: new TimeoutSettings().timeout(params) }); |
There was a problem hiding this comment.
Same here, let TimeoutSettings.timeout() handle the signal as well.
| } | ||
|
|
||
| export async function connectToEndpoint(parentConnection: Connection, params: channels.LocalUtilsConnectParams): Promise<Connection> { | ||
| export async function connectToEndpoint(parentConnection: Connection, params: channels.LocalUtilsConnectParams, timeout: number): Promise<Connection> { |
There was a problem hiding this comment.
Accept TimeoutOptions instead of a number?
Addresses review feedback: rename CommandOptions to TimeoutOptions,
add a shared kNoTimeout sentinel, have TimeoutSettings resolvers return
the full { signal, timeout } options, and thread TimeoutOptions through
connectToEndpoint.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 110d728d-5d20-42f6-93b9-e80ab89b3f66
Inline options.signal in the resolvers and rename the loose input alias to TimeoutInput to distinguish it from the resolved channels.TimeoutOptions. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 110d728d-5d20-42f6-93b9-e80ab89b3f66
Test results for "MCP"2 failed 7758 passed, 1249 skipped Merge workflow run. |
Test results for "tests 1"3 flaky49546 passed, 1168 skipped Merge workflow run. |
Follows #41141, which made
signala protocol-level field. This does the same fortimeout: every message carries it onMetadata, computed once on the client and read fromprogress.timeouton the server, instead of each method declaring its owntimeout: float.0is the sentinel for "no deadline", matching whatProgressController.runalready does. It's required onsendMessageToServerso the0is spelled out where it's genuinely meant, rather than hidden behind a?? 0.Most of the diff is mechanical - spec deletions plus regenerated channels/validator, staged separately for review.