selenium-webdriver 4.43.0 → 4.45.0

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.
Files changed (48) hide show
  1. package/CHANGES.md +21 -0
  2. package/README.md +57 -185
  3. package/bidi/browsingContextInspector.js +1 -1
  4. package/bidi/generated/bluetooth.d.ts +223 -0
  5. package/bidi/generated/bluetooth.js +154 -0
  6. package/bidi/generated/browser.d.ts +104 -0
  7. package/bidi/generated/browser.js +107 -0
  8. package/bidi/generated/browsing_context.d.ts +394 -0
  9. package/bidi/generated/browsing_context.js +260 -0
  10. package/bidi/generated/common.d.ts +38 -0
  11. package/bidi/generated/common.js +30 -0
  12. package/bidi/generated/emulation.d.ts +176 -0
  13. package/bidi/generated/emulation.js +133 -0
  14. package/bidi/generated/input.d.ts +151 -0
  15. package/bidi/generated/input.js +81 -0
  16. package/bidi/generated/log.d.ts +39 -0
  17. package/bidi/generated/log.js +67 -0
  18. package/bidi/generated/network.d.ts +359 -0
  19. package/bidi/generated/network.js +230 -0
  20. package/bidi/generated/permissions.d.ts +22 -0
  21. package/bidi/generated/permissions.js +43 -0
  22. package/bidi/generated/script.d.ts +410 -0
  23. package/bidi/generated/script.js +146 -0
  24. package/bidi/generated/session.d.ts +119 -0
  25. package/bidi/generated/session.js +82 -0
  26. package/bidi/generated/speculation.d.ts +17 -0
  27. package/bidi/generated/speculation.js +40 -0
  28. package/bidi/generated/storage.d.ts +84 -0
  29. package/bidi/generated/storage.js +70 -0
  30. package/bidi/generated/user_agent_client_hints.d.ts +33 -0
  31. package/bidi/generated/user_agent_client_hints.js +43 -0
  32. package/bidi/generated/webextension.d.ts +42 -0
  33. package/bidi/generated/webextension.js +53 -0
  34. package/bidi/index.js +133 -21
  35. package/bin/linux/selenium-manager +0 -0
  36. package/bin/macos/selenium-manager +0 -0
  37. package/bin/windows/selenium-manager.exe +0 -0
  38. package/common/driverFinder.js +2 -1
  39. package/common/seleniumManager.js +1 -1
  40. package/eslint.config.js +5 -5
  41. package/io/zip.js +1 -1
  42. package/lib/atoms/find-elements.js +272 -68
  43. package/lib/atoms/get-attribute.js +152 -17
  44. package/lib/atoms/is-displayed.js +385 -39
  45. package/lib/http.js +2 -1
  46. package/lib/pinnedScript.js +0 -1
  47. package/lib/webdriver.js +2 -2
  48. package/package.json +18 -16
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ // Licensed to the Software Freedom Conservancy (SFC) under one
3
+ // or more contributor license agreements. See the NOTICE file
4
+ // distributed with this work for additional information
5
+ // regarding copyright ownership. The SFC licenses this file
6
+ // to you under the Apache License, Version 2.0 (the
7
+ // "License"); you may not use this file except in compliance
8
+ // with the License. You may obtain a copy of the License at
9
+ //
10
+ // http://www.apache.org/licenses/LICENSE-2.0
11
+ //
12
+ // Unless required by applicable law or agreed to in writing,
13
+ // software distributed under the License is distributed on an
14
+ // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15
+ // KIND, either express or implied. See the License for the
16
+ // specific language governing permissions and limitations
17
+ // under the License.
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.Session = void 0;
20
+ // --- Implementation ---
21
+ class Session {
22
+ constructor(bidi) {
23
+ this.bidi = bidi;
24
+ }
25
+ static async create(driver) {
26
+ const caps = await driver.getCapabilities();
27
+ if (!caps.get('webSocketUrl')) {
28
+ throw new Error('WebDriver instance must support BiDi protocol');
29
+ }
30
+ const bidi = await driver.getBidi();
31
+ return new Session(bidi);
32
+ }
33
+ async end() {
34
+ const response = await this.bidi.send({
35
+ method: 'session.end',
36
+ params: {},
37
+ });
38
+ if (response['type'] === 'error') {
39
+ throw new Error(`${response['error']}: ${response['message']}`);
40
+ }
41
+ }
42
+ async new(params) {
43
+ const response = await this.bidi.send({
44
+ method: 'session.new',
45
+ params: params,
46
+ });
47
+ if (response['type'] === 'error') {
48
+ throw new Error(`${response['error']}: ${response['message']}`);
49
+ }
50
+ return response.result;
51
+ }
52
+ async status() {
53
+ const response = await this.bidi.send({
54
+ method: 'session.status',
55
+ params: {},
56
+ });
57
+ if (response['type'] === 'error') {
58
+ throw new Error(`${response['error']}: ${response['message']}`);
59
+ }
60
+ return response.result;
61
+ }
62
+ async subscribe(params) {
63
+ const response = await this.bidi.send({
64
+ method: 'session.subscribe',
65
+ params: params,
66
+ });
67
+ if (response['type'] === 'error') {
68
+ throw new Error(`${response['error']}: ${response['message']}`);
69
+ }
70
+ return response.result;
71
+ }
72
+ async unsubscribe(params) {
73
+ const response = await this.bidi.send({
74
+ method: 'session.unsubscribe',
75
+ params: params,
76
+ });
77
+ if (response['type'] === 'error') {
78
+ throw new Error(`${response['error']}: ${response['message']}`);
79
+ }
80
+ }
81
+ }
82
+ exports.Session = Session;
@@ -0,0 +1,17 @@
1
+ export type SpeculationPreloadingStatus = "pending" | "ready" | "success" | "failure";
2
+ export type SpeculationEvent = SpeculationPrefetchStatusUpdated;
3
+ export interface SpeculationPrefetchStatusUpdated {
4
+ method: "speculation.prefetchStatusUpdated";
5
+ params: SpeculationPrefetchStatusUpdatedParameters;
6
+ }
7
+ export interface SpeculationPrefetchStatusUpdatedParameters {
8
+ context: string;
9
+ url: string;
10
+ status: SpeculationPreloadingStatus;
11
+ }
12
+ export declare class Speculation {
13
+ private readonly bidi;
14
+ private constructor();
15
+ static create(driver: unknown): Promise<Speculation>;
16
+ onPrefetchStatusUpdated(callback: (params: SpeculationPrefetchStatusUpdatedParameters) => void): Promise<void>;
17
+ }
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ // Licensed to the Software Freedom Conservancy (SFC) under one
3
+ // or more contributor license agreements. See the NOTICE file
4
+ // distributed with this work for additional information
5
+ // regarding copyright ownership. The SFC licenses this file
6
+ // to you under the Apache License, Version 2.0 (the
7
+ // "License"); you may not use this file except in compliance
8
+ // with the License. You may obtain a copy of the License at
9
+ //
10
+ // http://www.apache.org/licenses/LICENSE-2.0
11
+ //
12
+ // Unless required by applicable law or agreed to in writing,
13
+ // software distributed under the License is distributed on an
14
+ // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15
+ // KIND, either express or implied. See the License for the
16
+ // specific language governing permissions and limitations
17
+ // under the License.
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.Speculation = void 0;
20
+ // --- Implementation ---
21
+ class Speculation {
22
+ constructor(bidi) {
23
+ this.bidi = bidi;
24
+ }
25
+ static async create(driver) {
26
+ const caps = await driver.getCapabilities();
27
+ if (!caps.get('webSocketUrl')) {
28
+ throw new Error('WebDriver instance must support BiDi protocol');
29
+ }
30
+ const bidi = await driver.getBidi();
31
+ return new Speculation(bidi);
32
+ }
33
+ async onPrefetchStatusUpdated(callback) {
34
+ await this.bidi.subscribe('speculation.prefetchStatusUpdated');
35
+ this.bidi.on('speculation.prefetchStatusUpdated', (params) => {
36
+ callback(params);
37
+ });
38
+ }
39
+ }
40
+ exports.Speculation = Speculation;
@@ -0,0 +1,84 @@
1
+ import type { BrowsingContextBrowsingContext } from './browsing_context.js';
2
+ import type { Extensible, JsUint } from './common.js';
3
+ import type { NetworkBytesValue, NetworkCookie, NetworkSameSite } from './network.js';
4
+ export type StorageCommand = StorageDeleteCookies | StorageGetCookies | StorageSetCookie;
5
+ export type StorageResult = StorageDeleteCookiesResult | StorageGetCookiesResult | StorageSetCookieResult;
6
+ export type StoragePartitionKey = Extensible & {
7
+ userContext?: string;
8
+ sourceOrigin?: string;
9
+ };
10
+ export interface StorageGetCookies {
11
+ method: "storage.getCookies";
12
+ params: StorageGetCookiesParameters;
13
+ }
14
+ export type StorageCookieFilter = Extensible & {
15
+ name?: string;
16
+ value?: NetworkBytesValue;
17
+ domain?: string;
18
+ path?: string;
19
+ size?: JsUint;
20
+ httpOnly?: boolean;
21
+ secure?: boolean;
22
+ sameSite?: NetworkSameSite;
23
+ expiry?: JsUint;
24
+ };
25
+ export interface StorageBrowsingContextPartitionDescriptor {
26
+ type: "context";
27
+ context: BrowsingContextBrowsingContext;
28
+ }
29
+ export type StorageStorageKeyPartitionDescriptor = Extensible & {
30
+ type: "storageKey";
31
+ userContext?: string;
32
+ sourceOrigin?: string;
33
+ };
34
+ export type StoragePartitionDescriptor = StorageBrowsingContextPartitionDescriptor | StorageStorageKeyPartitionDescriptor;
35
+ export interface StorageGetCookiesParameters {
36
+ filter?: StorageCookieFilter;
37
+ partition?: StoragePartitionDescriptor;
38
+ }
39
+ export interface StorageGetCookiesResult {
40
+ cookies: NetworkCookie[];
41
+ partitionKey: StoragePartitionKey;
42
+ }
43
+ export interface StorageSetCookie {
44
+ method: "storage.setCookie";
45
+ params: StorageSetCookieParameters;
46
+ }
47
+ export type StoragePartialCookie = Extensible & {
48
+ name: string;
49
+ value: NetworkBytesValue;
50
+ domain: string;
51
+ path?: string;
52
+ httpOnly?: boolean;
53
+ secure?: boolean;
54
+ sameSite?: NetworkSameSite;
55
+ expiry?: JsUint;
56
+ };
57
+ export interface StorageSetCookieParameters {
58
+ cookie: StoragePartialCookie;
59
+ partition?: StoragePartitionDescriptor;
60
+ }
61
+ export interface StorageSetCookieResult {
62
+ partitionKey: StoragePartitionKey;
63
+ }
64
+ export interface StorageDeleteCookies {
65
+ method: "storage.deleteCookies";
66
+ params: StorageDeleteCookiesParameters;
67
+ }
68
+ export interface StorageDeleteCookiesParameters {
69
+ filter?: StorageCookieFilter;
70
+ partition?: StoragePartitionDescriptor;
71
+ }
72
+ export interface StorageDeleteCookiesResult {
73
+ partitionKey: StoragePartitionKey;
74
+ }
75
+ /** @deprecated Use {@link Storage.create} instead — will be removed in a future major version. */
76
+ export declare function getStorageInstance(driver: unknown): Promise<Storage>;
77
+ export declare class Storage {
78
+ private readonly bidi;
79
+ private constructor();
80
+ static create(driver: unknown): Promise<Storage>;
81
+ deleteCookies(params: StorageDeleteCookiesParameters): Promise<StorageDeleteCookiesResult>;
82
+ getCookies(params: StorageGetCookiesParameters): Promise<StorageGetCookiesResult>;
83
+ setCookie(params: StorageSetCookieParameters): Promise<StorageSetCookieResult>;
84
+ }
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ // Licensed to the Software Freedom Conservancy (SFC) under one
3
+ // or more contributor license agreements. See the NOTICE file
4
+ // distributed with this work for additional information
5
+ // regarding copyright ownership. The SFC licenses this file
6
+ // to you under the Apache License, Version 2.0 (the
7
+ // "License"); you may not use this file except in compliance
8
+ // with the License. You may obtain a copy of the License at
9
+ //
10
+ // http://www.apache.org/licenses/LICENSE-2.0
11
+ //
12
+ // Unless required by applicable law or agreed to in writing,
13
+ // software distributed under the License is distributed on an
14
+ // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15
+ // KIND, either express or implied. See the License for the
16
+ // specific language governing permissions and limitations
17
+ // under the License.
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.Storage = exports.getStorageInstance = void 0;
20
+ // --- Additional Types ---
21
+ /** @deprecated Use {@link Storage.create} instead — will be removed in a future major version. */
22
+ async function getStorageInstance(driver) {
23
+ return Storage.create(driver);
24
+ }
25
+ exports.getStorageInstance = getStorageInstance;
26
+ // --- Implementation ---
27
+ class Storage {
28
+ constructor(bidi) {
29
+ this.bidi = bidi;
30
+ }
31
+ static async create(driver) {
32
+ const caps = await driver.getCapabilities();
33
+ if (!caps.get('webSocketUrl')) {
34
+ throw new Error('WebDriver instance must support BiDi protocol');
35
+ }
36
+ const bidi = await driver.getBidi();
37
+ return new Storage(bidi);
38
+ }
39
+ async deleteCookies(params) {
40
+ const response = await this.bidi.send({
41
+ method: 'storage.deleteCookies',
42
+ params: params,
43
+ });
44
+ if (response['type'] === 'error') {
45
+ throw new Error(`${response['error']}: ${response['message']}`);
46
+ }
47
+ return response.result;
48
+ }
49
+ async getCookies(params) {
50
+ const response = await this.bidi.send({
51
+ method: 'storage.getCookies',
52
+ params: params,
53
+ });
54
+ if (response['type'] === 'error') {
55
+ throw new Error(`${response['error']}: ${response['message']}`);
56
+ }
57
+ return response.result;
58
+ }
59
+ async setCookie(params) {
60
+ const response = await this.bidi.send({
61
+ method: 'storage.setCookie',
62
+ params: params,
63
+ });
64
+ if (response['type'] === 'error') {
65
+ throw new Error(`${response['error']}: ${response['message']}`);
66
+ }
67
+ return response.result;
68
+ }
69
+ }
70
+ exports.Storage = Storage;
@@ -0,0 +1,33 @@
1
+ export type UserAgentClientHintsCommand = UserAgentClientHintsSetClientHintsOverrideCommand;
2
+ export interface UserAgentClientHintsSetClientHintsOverrideCommand {
3
+ method: "userAgentClientHints.setClientHintsOverride";
4
+ params: {
5
+ clientHints: UserAgentClientHintsClientHintsMetadata | null;
6
+ contexts?: string[];
7
+ userContexts?: string[];
8
+ };
9
+ }
10
+ export interface UserAgentClientHintsClientHintsMetadata {
11
+ brands?: UserAgentClientHintsBrandVersion[];
12
+ fullVersionList?: UserAgentClientHintsBrandVersion[];
13
+ platform?: string;
14
+ platformVersion?: string;
15
+ architecture?: string;
16
+ model?: string;
17
+ mobile?: boolean;
18
+ bitness?: string;
19
+ wow64?: boolean;
20
+ formFactors?: string[];
21
+ }
22
+ export interface UserAgentClientHintsBrandVersion {
23
+ brand: string;
24
+ version: string;
25
+ }
26
+ export interface UserAgentClientHintsSetClientHintsOverrideResult {
27
+ }
28
+ export declare class UserAgentClientHints {
29
+ private readonly bidi;
30
+ private constructor();
31
+ static create(driver: unknown): Promise<UserAgentClientHints>;
32
+ setClientHintsOverride(): Promise<void>;
33
+ }
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ // Licensed to the Software Freedom Conservancy (SFC) under one
3
+ // or more contributor license agreements. See the NOTICE file
4
+ // distributed with this work for additional information
5
+ // regarding copyright ownership. The SFC licenses this file
6
+ // to you under the Apache License, Version 2.0 (the
7
+ // "License"); you may not use this file except in compliance
8
+ // with the License. You may obtain a copy of the License at
9
+ //
10
+ // http://www.apache.org/licenses/LICENSE-2.0
11
+ //
12
+ // Unless required by applicable law or agreed to in writing,
13
+ // software distributed under the License is distributed on an
14
+ // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15
+ // KIND, either express or implied. See the License for the
16
+ // specific language governing permissions and limitations
17
+ // under the License.
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.UserAgentClientHints = void 0;
20
+ // --- Implementation ---
21
+ class UserAgentClientHints {
22
+ constructor(bidi) {
23
+ this.bidi = bidi;
24
+ }
25
+ static async create(driver) {
26
+ const caps = await driver.getCapabilities();
27
+ if (!caps.get('webSocketUrl')) {
28
+ throw new Error('WebDriver instance must support BiDi protocol');
29
+ }
30
+ const bidi = await driver.getBidi();
31
+ return new UserAgentClientHints(bidi);
32
+ }
33
+ async setClientHintsOverride() {
34
+ const response = await this.bidi.send({
35
+ method: 'userAgentClientHints.setClientHintsOverride',
36
+ params: {},
37
+ });
38
+ if (response['type'] === 'error') {
39
+ throw new Error(`${response['error']}: ${response['message']}`);
40
+ }
41
+ }
42
+ }
43
+ exports.UserAgentClientHints = UserAgentClientHints;
@@ -0,0 +1,42 @@
1
+ import type { EmptyResult } from './common.js';
2
+ export type WebExtensionCommand = WebExtensionInstall | WebExtensionUninstall;
3
+ export type WebExtensionResult = WebExtensionInstallResult | WebExtensionUninstallResult;
4
+ export type WebExtensionExtension = string;
5
+ export interface WebExtensionInstall {
6
+ method: "webExtension.install";
7
+ params: WebExtensionInstallParameters;
8
+ }
9
+ export interface WebExtensionInstallParameters {
10
+ extensionData: WebExtensionExtensionData;
11
+ }
12
+ export type WebExtensionExtensionData = WebExtensionExtensionArchivePath | WebExtensionExtensionBase64Encoded | WebExtensionExtensionPath;
13
+ export interface WebExtensionExtensionPath {
14
+ type: "path";
15
+ path: string;
16
+ }
17
+ export interface WebExtensionExtensionArchivePath {
18
+ type: "archivePath";
19
+ path: string;
20
+ }
21
+ export interface WebExtensionExtensionBase64Encoded {
22
+ type: "base64";
23
+ value: string;
24
+ }
25
+ export interface WebExtensionInstallResult {
26
+ extension: WebExtensionExtension;
27
+ }
28
+ export interface WebExtensionUninstall {
29
+ method: "webExtension.uninstall";
30
+ params: WebExtensionUninstallParameters;
31
+ }
32
+ export interface WebExtensionUninstallParameters {
33
+ extension: WebExtensionExtension;
34
+ }
35
+ export type WebExtensionUninstallResult = EmptyResult;
36
+ export declare class WebExtension {
37
+ private readonly bidi;
38
+ private constructor();
39
+ static create(driver: unknown): Promise<WebExtension>;
40
+ install(params: WebExtensionInstallParameters): Promise<WebExtensionInstallResult>;
41
+ uninstall(params: WebExtensionUninstallParameters): Promise<void>;
42
+ }
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ // Licensed to the Software Freedom Conservancy (SFC) under one
3
+ // or more contributor license agreements. See the NOTICE file
4
+ // distributed with this work for additional information
5
+ // regarding copyright ownership. The SFC licenses this file
6
+ // to you under the Apache License, Version 2.0 (the
7
+ // "License"); you may not use this file except in compliance
8
+ // with the License. You may obtain a copy of the License at
9
+ //
10
+ // http://www.apache.org/licenses/LICENSE-2.0
11
+ //
12
+ // Unless required by applicable law or agreed to in writing,
13
+ // software distributed under the License is distributed on an
14
+ // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15
+ // KIND, either express or implied. See the License for the
16
+ // specific language governing permissions and limitations
17
+ // under the License.
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.WebExtension = void 0;
20
+ // --- Implementation ---
21
+ class WebExtension {
22
+ constructor(bidi) {
23
+ this.bidi = bidi;
24
+ }
25
+ static async create(driver) {
26
+ const caps = await driver.getCapabilities();
27
+ if (!caps.get('webSocketUrl')) {
28
+ throw new Error('WebDriver instance must support BiDi protocol');
29
+ }
30
+ const bidi = await driver.getBidi();
31
+ return new WebExtension(bidi);
32
+ }
33
+ async install(params) {
34
+ const response = await this.bidi.send({
35
+ method: 'webExtension.install',
36
+ params: params,
37
+ });
38
+ if (response['type'] === 'error') {
39
+ throw new Error(`${response['error']}: ${response['message']}`);
40
+ }
41
+ return response.result;
42
+ }
43
+ async uninstall(params) {
44
+ const response = await this.bidi.send({
45
+ method: 'webExtension.uninstall',
46
+ params: params,
47
+ });
48
+ if (response['type'] === 'error') {
49
+ throw new Error(`${response['error']}: ${response['message']}`);
50
+ }
51
+ }
52
+ }
53
+ exports.WebExtension = WebExtension;
package/bidi/index.js CHANGED
@@ -33,10 +33,120 @@ class Index extends EventEmitter {
33
33
  constructor(_webSocketUrl) {
34
34
  super()
35
35
  this.connected = false
36
+ this._closed = false
37
+ this._pending = new Map()
38
+ this._connectWaiters = new Set()
36
39
  this._ws = new WebSocket(_webSocketUrl)
37
40
  this._ws.on('open', () => {
41
+ // The handshake can complete after close()/_failPending() has already
42
+ // marked the connection closed. Don't flip connected back to true and
43
+ // proactively close the now-orphan socket so it does not leak.
44
+ if (this._closed) {
45
+ try {
46
+ this._ws.close()
47
+ } catch {
48
+ /* socket already closing */
49
+ }
50
+ return
51
+ }
38
52
  this.connected = true
53
+ for (const { resolve } of this._connectWaiters) {
54
+ resolve()
55
+ }
56
+ this._connectWaiters.clear()
57
+ })
58
+ // Single shared response dispatcher. Avoids attaching a new 'message'
59
+ // listener for every in-flight send(), which previously caused
60
+ // MaxListenersExceededWarning under concurrent BiDi traffic
61
+ // (e.g. network interception during a page navigation).
62
+ this._ws.on('message', (data) => {
63
+ // Frames can arrive after close() has cleared _pending; ignore them
64
+ // rather than re-emitting parse errors or dispatching to nothing.
65
+ if (this._closed) {
66
+ return
67
+ }
68
+ let payload
69
+ try {
70
+ payload = JSON.parse(data.toString())
71
+ } catch (err) {
72
+ // Surface protocol parse failures rather than silently dropping —
73
+ // otherwise callers see misleading send() timeouts.
74
+ const wrapped = new Error(`Failed to parse BiDi message: ${err.message}`)
75
+ if (this.listenerCount('error') > 0) {
76
+ this.emit('error', wrapped)
77
+ } else {
78
+ process.emitWarning(wrapped.message, 'BiDiProtocolWarning')
79
+ }
80
+ return
81
+ }
82
+ // Messages without a numeric id are BiDi events, not command responses.
83
+ // Re-emit them on this EventEmitter by method name (e.g.
84
+ // 'browsingContext.contextCreated') so that generated domain classes can
85
+ // subscribe via bidi.on(methodName, callback) instead of each attaching
86
+ // a new raw ws.on('message', ...) listener. The existing hand-written
87
+ // modules (logInspector, network, etc.) continue to use their own
88
+ // ws.on('message', ...) listeners unchanged — this emission is purely
89
+ // additive and does not affect those code paths.
90
+ if (payload == null || typeof payload.id !== 'number') {
91
+ if (payload != null && typeof payload.method === 'string') {
92
+ // 'error' is a reserved EventEmitter event — emitting it without a
93
+ // listener throws and crashes the process. Route any peer-supplied
94
+ // method named 'error' through the same guarded path used for JSON
95
+ // parse failures rather than forwarding it directly.
96
+ if (payload.method === 'error') {
97
+ const err = new Error(`BiDi protocol error event: ${JSON.stringify(payload.params)}`)
98
+ if (this.listenerCount('error') > 0) {
99
+ this.emit('error', err)
100
+ } else {
101
+ process.emitWarning(err.message, 'BiDiProtocolWarning')
102
+ }
103
+ } else {
104
+ this.emit(payload.method, payload.params)
105
+ }
106
+ }
107
+ return
108
+ }
109
+ const entry = this._pending.get(payload.id)
110
+ if (entry === undefined) {
111
+ return
112
+ }
113
+ clearTimeout(entry.timeoutId)
114
+ this._pending.delete(payload.id)
115
+ entry.resolve(payload)
116
+ })
117
+ // Fail any in-flight send() calls promptly when the peer disconnects
118
+ // or the socket errors, instead of waiting for RESPONSE_TIMEOUT.
119
+ this._ws.on('close', () => {
120
+ this._failPending(new Error('BiDi connection closed unexpectedly'))
39
121
  })
122
+ this._ws.on('error', (err) => {
123
+ this._failPending(new Error(`BiDi connection error: ${err.message}`))
124
+ })
125
+ }
126
+
127
+ /**
128
+ * Reject any in-flight sends and mark the connection failed. Idempotent so
129
+ * that close() and the underlying 'close'/'error' events do not double-reject.
130
+ * @param {Error} error
131
+ * @private
132
+ */
133
+ _failPending(error) {
134
+ if (this._closed) {
135
+ return
136
+ }
137
+ this._closed = true
138
+ this.connected = false
139
+ for (const { reject, timeoutId } of this._pending.values()) {
140
+ clearTimeout(timeoutId)
141
+ reject(error)
142
+ }
143
+ this._pending.clear()
144
+ // Reject any callers parked in waitForConnection() so close() (or an
145
+ // unexpected disconnect) cannot leave them hanging forever.
146
+ for (const { reject } of this._connectWaiters) {
147
+ reject(error)
148
+ }
149
+ this._connectWaiters.clear()
40
150
  }
41
151
 
42
152
  /**
@@ -69,14 +179,19 @@ class Index extends EventEmitter {
69
179
  * @returns {Promise<unknown>}
70
180
  */
71
181
  async waitForConnection() {
72
- return new Promise((resolve) => {
182
+ return new Promise((resolve, reject) => {
183
+ if (this._closed) {
184
+ reject(new Error('BiDi connection is closed'))
185
+ return
186
+ }
73
187
  if (this.connected) {
74
188
  resolve()
75
- } else {
76
- this._ws.once('open', () => {
77
- resolve()
78
- })
189
+ return
79
190
  }
191
+ // Park the waiter in a Set so the constructor's 'open' handler can
192
+ // resolve it and _failPending() can reject it. Avoids attaching socket
193
+ // listeners that close()'s removeAllListeners('close') would strip.
194
+ this._connectWaiters.add({ resolve, reject })
80
195
  })
81
196
  }
82
197
 
@@ -86,9 +201,18 @@ class Index extends EventEmitter {
86
201
  * @returns {Promise<unknown>}
87
202
  */
88
203
  async send(params) {
204
+ if (this._closed) {
205
+ throw new Error('BiDi connection is closed')
206
+ }
89
207
  if (!this.connected) {
90
208
  await this.waitForConnection()
91
209
  }
210
+ // Defense in depth: even after waitForConnection() resolves, the socket
211
+ // may have transitioned to CLOSING/CLOSED (e.g. caller closed the raw
212
+ // socket). Refuse rather than throwing from inside ws.send().
213
+ if (this._ws.readyState !== WebSocket.OPEN) {
214
+ throw new Error('BiDi connection is not open')
215
+ }
92
216
 
93
217
  const id = ++this.id
94
218
 
@@ -96,25 +220,11 @@ class Index extends EventEmitter {
96
220
 
97
221
  return new Promise((resolve, reject) => {
98
222
  const timeoutId = setTimeout(() => {
223
+ this._pending.delete(id)
99
224
  reject(new Error(`Request with id ${id} timed out`))
100
- handler.off('message', listener)
101
225
  }, RESPONSE_TIMEOUT)
102
226
 
103
- const listener = (data) => {
104
- try {
105
- const payload = JSON.parse(data.toString())
106
- if (payload.id === id) {
107
- clearTimeout(timeoutId)
108
- handler.off('message', listener)
109
- resolve(payload)
110
- }
111
- } catch (err) {
112
- // eslint-disable-next-line no-undef
113
- log.error(`Failed parse message: ${err.message}`)
114
- }
115
- }
116
-
117
- const handler = this._ws.on('message', listener)
227
+ this._pending.set(id, { resolve, reject, timeoutId })
118
228
  })
119
229
  }
120
230
 
@@ -206,6 +316,8 @@ class Index extends EventEmitter {
206
316
  * @returns {Promise<unknown>}
207
317
  */
208
318
  close() {
319
+ this._failPending(new Error('BiDi connection closed before response was received'))
320
+
209
321
  const closeWebSocket = (callback) => {
210
322
  // don't close if it's already closed
211
323
  if (this._ws.readyState === 3) {
Binary file
Binary file
Binary file