appium-remote-debugger 12.2.10 → 13.1.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 (71) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/build/lib/mixins/connect.d.ts.map +1 -1
  3. package/build/lib/mixins/connect.js +32 -37
  4. package/build/lib/mixins/connect.js.map +1 -1
  5. package/build/lib/mixins/execute.d.ts.map +1 -1
  6. package/build/lib/mixins/execute.js +14 -12
  7. package/build/lib/mixins/execute.js.map +1 -1
  8. package/build/lib/mixins/misc.d.ts +7 -0
  9. package/build/lib/mixins/misc.d.ts.map +1 -1
  10. package/build/lib/mixins/misc.js +21 -0
  11. package/build/lib/mixins/misc.js.map +1 -1
  12. package/build/lib/mixins/navigate.d.ts.map +1 -1
  13. package/build/lib/mixins/navigate.js +9 -96
  14. package/build/lib/mixins/navigate.js.map +1 -1
  15. package/build/lib/protocol/index.d.ts +3 -1
  16. package/build/lib/protocol/index.d.ts.map +1 -1
  17. package/build/lib/protocol/index.js +22 -23
  18. package/build/lib/protocol/index.js.map +1 -1
  19. package/build/lib/remote-debugger.d.ts +1 -0
  20. package/build/lib/remote-debugger.d.ts.map +1 -1
  21. package/build/lib/remote-debugger.js +1 -0
  22. package/build/lib/remote-debugger.js.map +1 -1
  23. package/build/lib/rpc/index.d.ts +1 -2
  24. package/build/lib/rpc/index.d.ts.map +1 -1
  25. package/build/lib/rpc/index.js +1 -36
  26. package/build/lib/rpc/index.js.map +1 -1
  27. package/build/lib/rpc/remote-messages.d.ts +24 -18
  28. package/build/lib/rpc/remote-messages.d.ts.map +1 -1
  29. package/build/lib/rpc/remote-messages.js +24 -11
  30. package/build/lib/rpc/remote-messages.js.map +1 -1
  31. package/build/lib/rpc/rpc-client-real-device.d.ts.map +1 -1
  32. package/build/lib/rpc/rpc-client-real-device.js +1 -3
  33. package/build/lib/rpc/rpc-client-real-device.js.map +1 -1
  34. package/build/lib/rpc/rpc-client-simulator.d.ts.map +1 -1
  35. package/build/lib/rpc/rpc-client-simulator.js +1 -3
  36. package/build/lib/rpc/rpc-client-simulator.js.map +1 -1
  37. package/build/lib/rpc/rpc-client.d.ts +63 -44
  38. package/build/lib/rpc/rpc-client.d.ts.map +1 -1
  39. package/build/lib/rpc/rpc-client.js +209 -122
  40. package/build/lib/rpc/rpc-client.js.map +1 -1
  41. package/build/lib/rpc/rpc-message-handler.d.ts +8 -5
  42. package/build/lib/rpc/rpc-message-handler.d.ts.map +1 -1
  43. package/build/lib/rpc/rpc-message-handler.js +61 -75
  44. package/build/lib/rpc/rpc-message-handler.js.map +1 -1
  45. package/build/lib/types.d.ts +28 -3
  46. package/build/lib/types.d.ts.map +1 -1
  47. package/build/lib/utils.d.ts +4 -3
  48. package/build/lib/utils.d.ts.map +1 -1
  49. package/build/lib/utils.js +4 -2
  50. package/build/lib/utils.js.map +1 -1
  51. package/build/tsconfig.tsbuildinfo +1 -1
  52. package/lib/mixins/connect.js +34 -38
  53. package/lib/mixins/execute.js +15 -12
  54. package/lib/mixins/misc.js +22 -0
  55. package/lib/mixins/navigate.js +11 -99
  56. package/lib/protocol/index.js +22 -24
  57. package/lib/remote-debugger.ts +1 -0
  58. package/lib/rpc/index.js +1 -3
  59. package/lib/rpc/remote-messages.js +28 -11
  60. package/lib/rpc/rpc-client-real-device.js +1 -3
  61. package/lib/rpc/rpc-client-simulator.js +1 -3
  62. package/lib/rpc/rpc-client.js +244 -133
  63. package/lib/rpc/rpc-message-handler.js +62 -74
  64. package/lib/types.ts +33 -3
  65. package/lib/utils.js +4 -2
  66. package/package.json +2 -1
  67. package/build/lib/rpc/constants.d.ts +0 -2
  68. package/build/lib/rpc/constants.d.ts.map +0 -1
  69. package/build/lib/rpc/constants.js +0 -5
  70. package/build/lib/rpc/constants.js.map +0 -1
  71. package/lib/rpc/constants.js +0 -1
@@ -6,7 +6,7 @@ import B from 'bluebird';
6
6
  import RpcMessageHandler from './rpc-message-handler';
7
7
  import { util, timing } from '@appium/support';
8
8
  import { EventEmitter } from 'node:events';
9
- import { ON_TARGET_PROVISIONED_EVENT } from './constants';
9
+ import AsyncLock from 'async-lock';
10
10
 
11
11
  const DATA_LOG_LENGTH = {length: 200};
12
12
  const WAIT_FOR_TARGET_TIMEOUT_MS = 10000;
@@ -20,6 +20,8 @@ const NO_TARGET_PRESENT_YET_ERRORS = [
20
20
  `some arguments of method`,
21
21
  `missing target`,
22
22
  ];
23
+ export const NEW_APP_CONNECTED_ERROR = 'New application has connected';
24
+ export const EMPTY_PAGE_DICTIONARY_ERROR = 'Empty page dictionary received';
23
25
 
24
26
  /**
25
27
  * @param {boolean} isSafari
@@ -85,14 +87,14 @@ export class RpcClient {
85
87
  /** @type {string[]} */
86
88
  _contexts;
87
89
 
88
- /** @type {import('@appium/types').StringRecord} */
90
+ /** @type {AppToTargetsMap} */
89
91
  _targets;
90
92
 
91
93
  /** @type {EventEmitter} */
92
94
  _targetSubscriptions;
93
95
 
94
- /** @type {boolean} */
95
- _shouldCheckForTarget;
96
+ /** @type {[import('../types').AppIdKey, import('../types').PageIdKey] | undefined} */
97
+ _pendingTargetNotification;
96
98
 
97
99
  /**
98
100
  *
@@ -135,7 +137,6 @@ export class RpcClient {
135
137
  this._targetSubscriptions = new EventEmitter();
136
138
 
137
139
  // start with a best guess for the protocol
138
- this._shouldCheckForTarget = !!opts.shouldCheckForTarget;
139
140
  this.isTargetBased = platformVersion ? isTargetBased(isSafari, platformVersion) : true;
140
141
  }
141
142
 
@@ -150,27 +151,16 @@ export class RpcClient {
150
151
  * @returns {boolean}
151
152
  */
152
153
  get needsTarget () {
153
- return this.shouldCheckForTarget && this.isTargetBased;
154
+ return this.isTargetBased;
154
155
  }
155
156
 
156
157
  /**
157
- * @returns {import('@appium/types').StringRecord}
158
+ * @returns {AppToTargetsMap}
158
159
  */
159
160
  get targets () {
160
161
  return this._targets;
161
162
  }
162
163
 
163
- /**
164
- * @returns {boolean}
165
- */
166
- get shouldCheckForTarget () {
167
- return this._shouldCheckForTarget;
168
- }
169
-
170
- set shouldCheckForTarget (shouldCheckForTarget) {
171
- this._shouldCheckForTarget = !!shouldCheckForTarget;
172
- }
173
-
174
164
  /**
175
165
  * @returns {boolean}
176
166
  */
@@ -265,33 +255,39 @@ export class RpcClient {
265
255
  *
266
256
  * @param {import('../types').AppIdKey} appIdKey
267
257
  * @param {import('../types').PageIdKey} pageIdKey
268
- * @returns {Promise<void>}
258
+ * @returns {Promise<import('../types').TargetId | undefined>}
269
259
  */
270
260
  async waitForTarget (appIdKey, pageIdKey) {
271
261
  if (!this.needsTarget) {
272
262
  log.debug(`Target-based communication is not needed, skipping wait for target`);
273
263
  return;
274
264
  }
275
- const target = this.getTarget(appIdKey, pageIdKey);
265
+ let target = this.getTarget(appIdKey, pageIdKey);
276
266
  if (target) {
277
267
  log.debug(
278
268
  `The target '${target}' for app '${appIdKey}' and page '${pageIdKey}' already exists, no need to wait`
279
269
  );
280
- return;
270
+ return target;
281
271
  }
282
272
 
283
273
  // otherwise waiting is necessary to see what the target is
284
274
  try {
285
- await waitForCondition(() => !_.isEmpty(this.getTarget(appIdKey, pageIdKey)), {
275
+ await waitForCondition(() => {
276
+ target = this.getTarget(appIdKey, pageIdKey);
277
+ return !_.isEmpty(target);
278
+ }, {
286
279
  waitMs: WAIT_FOR_TARGET_TIMEOUT_MS,
287
280
  intervalMs: WAIT_FOR_TARGET_INTERVAL_MS,
288
- error: 'No targets found, unable to communicate with device',
289
281
  });
282
+ return target;
290
283
  } catch (err) {
291
284
  if (!err.message.includes('Condition unmet')) {
292
285
  throw err;
293
286
  }
294
- throw new Error('No targets found, unable to communicate with device');
287
+ throw new Error(
288
+ `No targets could be matched for the app '${appIdKey}' and page '${pageIdKey}' after ` +
289
+ `${WAIT_FOR_TARGET_TIMEOUT_MS}ms. This is likely a bug.`
290
+ );
295
291
  }
296
292
  }
297
293
 
@@ -299,18 +295,18 @@ export class RpcClient {
299
295
  *
300
296
  * @param {string} command
301
297
  * @param {import('../types').RemoteCommandOpts} opts
302
- * @param {boolean} [waitForResponse]
298
+ * @param {boolean} [waitForResponse=true]
303
299
  * @returns {Promise<any>}
304
300
  */
305
301
  async send (command, opts, waitForResponse = true) {
306
302
  const timer = new timing.Timer().start();
307
- const {
308
- appIdKey,
309
- pageIdKey
310
- } = opts;
311
303
  try {
312
304
  return await this.sendToDevice(command, opts, waitForResponse);
313
305
  } catch (err) {
306
+ const {
307
+ appIdKey,
308
+ pageIdKey
309
+ } = opts;
314
310
  const messageLc = (err.message || '').toLowerCase();
315
311
  if (messageLc.includes(NO_TARGET_SUPPORTED_ERROR)) {
316
312
  log.info(
@@ -332,11 +328,13 @@ export class RpcClient {
332
328
 
333
329
  /**
334
330
  *
331
+ * @template {boolean} TWaitForResponse
335
332
  * @param {string} command
336
333
  * @param {import('../types').RemoteCommandOpts} opts
337
- * @param {boolean} [waitForResponse]
338
- * @returns {Promise<any>}
334
+ * @param {TWaitForResponse} [waitForResponse=true]
335
+ * @returns {Promise<TWaitForResponse extends true ? import('../types').RemoteCommandOpts : any>}
339
336
  */
337
+ // @ts-ignore Compiler issue
340
338
  async sendToDevice (command, opts, waitForResponse = true) {
341
339
  return await new B(async (resolve, reject) => {
342
340
  // promise to be resolved whenever remote debugger
@@ -359,25 +357,41 @@ export class RpcClient {
359
357
 
360
358
  const appIdKey = opts.appIdKey;
361
359
  const pageIdKey = opts.pageIdKey;
362
- const targetId = this.getTarget(appIdKey, pageIdKey);
360
+ const targetId = opts.targetId ?? this.getTarget(appIdKey, pageIdKey);
363
361
 
364
362
  // retrieve the correct command to send
363
+ /** @type {import('../types').RemoteCommandOpts} */
365
364
  const fullOpts = _.defaults({
366
365
  connId: this.connId,
367
366
  senderId: this.senderId,
368
367
  targetId,
369
368
  id: msgId,
370
369
  }, opts);
371
- // @ts-ignore remoteMessages must be defined
372
- const cmd = this.remoteMessages.getRemoteCommand(command, fullOpts);
370
+ /** @type {import('../types').RawRemoteCommand} */
371
+ let cmd;
372
+ try {
373
+ // @ts-ignore remoteMessages must be defined
374
+ cmd = this.remoteMessages.getRemoteCommand(command, fullOpts);
375
+ } catch (err) {
376
+ log.error(err);
377
+ return reject(err);
378
+ }
379
+
380
+ /** @type {import('../types').RemoteCommand} */
381
+ const finalCommand = {
382
+ __argument: _.omit(cmd.__argument, ['WIRSocketDataKey']),
383
+ __selector: cmd.__selector,
384
+ };
373
385
 
374
- if (cmd?.__argument?.WIRSocketDataKey) {
386
+ const hasSocketData = _.isPlainObject(cmd.__argument?.WIRSocketDataKey);
387
+ if (hasSocketData) {
375
388
  // make sure the message being sent has all the information that is needed
389
+ // @ts-ignore We have asserted it's a plain object above
376
390
  if (_.isNil(cmd.__argument.WIRSocketDataKey.id)) {
391
+ // @ts-ignore We have already asserted it's a plain object above
377
392
  cmd.__argument.WIRSocketDataKey.id = wrapperMsgId;
378
393
  }
379
- cmd.__argument.WIRSocketDataKey =
380
- Buffer.from(JSON.stringify(cmd.__argument.WIRSocketDataKey));
394
+ finalCommand.__argument.WIRSocketDataKey = Buffer.from(JSON.stringify(cmd.__argument.WIRSocketDataKey));
381
395
  }
382
396
 
383
397
  let messageHandled = true;
@@ -390,7 +404,10 @@ export class RpcClient {
390
404
  if (err) {
391
405
  // we are not waiting for this, and if it errors it is most likely
392
406
  // a protocol change. Log and check during testing
393
- log.error(`Received error from send that is not being waited for (id: ${msgId}): '${_.truncate(JSON.stringify(err), DATA_LOG_LENGTH)}'`);
407
+ log.error(
408
+ `Received error from send that is not being waited for (id: ${msgId}): ` +
409
+ _.truncate(JSON.stringify(err), DATA_LOG_LENGTH)
410
+ );
394
411
  // reject, though it is very rare that this will be triggered, since
395
412
  // the promise is resolved directlty after send. On the off chance,
396
413
  // though, it will alert of a protocol change.
@@ -405,9 +422,10 @@ export class RpcClient {
405
422
  return reject(err);
406
423
  }
407
424
  log.debug(`Received response from send (id: ${msgId}): '${_.truncate(JSON.stringify(args), DATA_LOG_LENGTH)}'`);
425
+ // @ts-ignore This is ok
408
426
  resolve(args);
409
427
  });
410
- } else if (cmd?.__argument?.WIRSocketDataKey) {
428
+ } else if (hasSocketData) {
411
429
  // @ts-ignore messageHandler must be defined
412
430
  this.messageHandler.once(msgId.toString(), function (err, value) {
413
431
  if (err) {
@@ -428,11 +446,12 @@ export class RpcClient {
428
446
  ` (id: ${msgId}): '${command}'`;
429
447
  log.debug(msg);
430
448
  try {
431
- const res = await this.sendMessage(cmd);
449
+ await this.sendMessage(finalCommand);
432
450
  if (!messageHandled) {
433
451
  // There are no handlers waiting for a response before resolving,
434
452
  // and no errors sending the message over the socket, so resolve
435
- resolve(res);
453
+ // @ts-ignore This is ok
454
+ resolve(fullOpts);
436
455
  }
437
456
  } catch (err) {
438
457
  return reject(err);
@@ -468,53 +487,101 @@ export class RpcClient {
468
487
 
469
488
  /**
470
489
  *
471
- * @param {Error?} err
472
- * @param {string} app
473
- * @param {Record<string, any>} targetInfo
474
- * @returns {void}
490
+ * @param {Error | undefined} err
491
+ * @param {import('../types').AppIdKey} app
492
+ * @param {import('../types').TargetInfo} targetInfo
493
+ * @returns {Promise<void>}
475
494
  */
476
- addTarget (err, app, targetInfo) {
495
+ async addTarget (err, app, targetInfo) {
477
496
  if (_.isNil(targetInfo?.targetId)) {
478
- log.warn(`Received 'Target.targetCreated' event for app '${app}' with no target. Skipping`);
497
+ log.info(`Received 'Target.targetCreated' event for app '${app}' with no target. Skipping`);
479
498
  return;
480
499
  }
481
- if (_.isEmpty(this.pendingTargetNotification) && !targetInfo.isProvisional) {
482
- log.warn(`Received 'Target.targetCreated' event for app '${app}' with no pending request: ${JSON.stringify(targetInfo)}`);
500
+ if (!this._pendingTargetNotification) {
501
+ log.info(
502
+ `Received 'Target.targetCreated' event for app '${app}' with no pending request: ${JSON.stringify(targetInfo)}`
503
+ );
483
504
  return;
484
505
  }
485
506
 
486
- if (targetInfo.isProvisional) {
487
- log.debug(`Provisional target created for app '${app}', '${targetInfo.targetId}'. Ignoring until target update event`);
507
+ const [appIdKey, pageIdKey] = this._pendingTargetNotification;
508
+ if (appIdKey !== app) {
509
+ log.info(
510
+ `Received 'Target.targetCreated' event for app '${app}' with no pending request: ${JSON.stringify(targetInfo)}`
511
+ );
488
512
  return;
489
513
  }
490
514
 
491
- // @ts-ignore this.pendingTargetNotification must be defined here
492
- const [appIdKey, pageIdKey] = this.pendingTargetNotification;
515
+ if (targetInfo.isProvisional) {
516
+ log.debug(
517
+ `Provisional target created for app '${appIdKey}' and page '${pageIdKey}': '${targetInfo.targetId}'`
518
+ );
519
+ if (!targetInfo.isPaused) {
520
+ return;
521
+ }
522
+
523
+ log.debug(`The target ${targetInfo.targetId}@${appIdKey} is paused`);
524
+ const appTargetsMap = this.targets[appIdKey];
525
+ if (appTargetsMap) {
526
+ await appTargetsMap.lock.acquire(toLockKey(appIdKey, pageIdKey), async () => {
527
+ try {
528
+ await this._initializePage(appIdKey, pageIdKey, targetInfo.targetId);
529
+ } finally {
530
+ await this._resumeTarget(appIdKey, pageIdKey, targetInfo.targetId);
531
+ }
532
+ });
533
+ }
534
+ return;
535
+ }
493
536
 
494
537
  log.debug(`Target created for app '${appIdKey}' and page '${pageIdKey}': ${JSON.stringify(targetInfo)}`);
495
538
  if (_.has(this.targets[appIdKey], pageIdKey)) {
496
539
  log.debug(`There is already a target for this app and page ('${this.targets[appIdKey][pageIdKey]}'). This might cause problems`);
497
540
  }
498
- this.targets[app] = this.targets[app] || {};
541
+ const lock = new AsyncLock();
542
+ this.targets[app] = this.targets[app] || { lock };
499
543
  this.targets[appIdKey][pageIdKey] = targetInfo.targetId;
544
+
545
+ await lock.acquire(toLockKey(appIdKey, pageIdKey), async () => {
546
+ try {
547
+ await this.send('Target.setPauseOnStart', {
548
+ pauseOnStart: true,
549
+ appIdKey,
550
+ pageIdKey,
551
+ });
552
+ } catch {}
553
+ try {
554
+ await this._initializePage(appIdKey, pageIdKey);
555
+ } finally {
556
+ if (targetInfo.isPaused) {
557
+ await this._resumeTarget(appIdKey, pageIdKey);
558
+ }
559
+ }
560
+ });
500
561
  }
501
562
 
502
563
  /**
503
564
  *
504
- * @param {Error?} err
505
- * @param {string} app
506
- * @param {string} oldTargetId
507
- * @param {string} newTargetId
508
- * @returns {void}
565
+ * @param {Error | undefined} err
566
+ * @param {import('../types').AppIdKey} app
567
+ * @param {import('../types').ProvisionalTargetInfo} targetInfo
568
+ * @returns {Promise<void>}
509
569
  */
510
- updateTarget (err, app, oldTargetId, newTargetId) {
570
+ async updateTarget (err, app, targetInfo) {
571
+ const {
572
+ oldTargetId,
573
+ newTargetId,
574
+ } = targetInfo;
511
575
  log.debug(`Target updated for app '${app}'. Old target: '${oldTargetId}', new target: '${newTargetId}'`);
512
- if (!this.targets[app]) {
576
+
577
+ const appTargetsMap = this.targets[app];
578
+ if (!appTargetsMap) {
513
579
  log.warn(`No existing target for app '${app}'. Not sure what to do`);
514
580
  return;
515
581
  }
582
+
516
583
  // save this, to be used if/when the existing target is destroyed
517
- this.targets[app].provisional = {
584
+ appTargetsMap.provisional = {
518
585
  oldTargetId,
519
586
  newTargetId,
520
587
  };
@@ -522,12 +589,12 @@ export class RpcClient {
522
589
 
523
590
  /**
524
591
  *
525
- * @param {Error?} err
526
- * @param {string} app
527
- * @param {Record<string, any>} targetInfo
528
- * @returns {void}
592
+ * @param {Error | undefined} err
593
+ * @param {import('../types').AppIdKey} app
594
+ * @param {import('../types').TargetInfo} targetInfo
595
+ * @returns {Promise<void>}
529
596
  */
530
- removeTarget (err, app, targetInfo) {
597
+ async removeTarget (err, app, targetInfo) {
531
598
  if (_.isNil(targetInfo?.targetId)) {
532
599
  log.debug(`Received 'Target.targetDestroyed' event with no target. Skipping`);
533
600
  return;
@@ -541,29 +608,23 @@ export class RpcClient {
541
608
  delete this.targets[app].provisional;
542
609
 
543
610
  // we do not know the page, so go through and find the existing target
544
- const targets = this.targets[app];
545
- for (const [page, targetId] of _.toPairs(targets)) {
611
+ const appTargetsMap = this.targets[app];
612
+ for (const [page, targetId] of _.toPairs(appTargetsMap)) {
546
613
  if (targetId === oldTargetId) {
547
- log.debug(`Found provisional target for app '${app}'. Old target: '${oldTargetId}', new target: '${newTargetId}'. Updating`);
548
- targets[page] = newTargetId;
549
- const opts = {appIdKey: app, pageIdKey: parseInt(page, 10)};
550
- (async () => {
551
- if (this.fullPageInitialization) {
552
- await this.initializePageFull(opts.appIdKey, opts.pageIdKey);
553
- } else {
554
- await this.initializePage(opts.appIdKey, opts.pageIdKey);
555
- }
556
- this._targetSubscriptions.emit(ON_TARGET_PROVISIONED_EVENT, {
557
- ...opts,
558
- oldTargetId,
559
- targetId: newTargetId,
560
- });
561
- })();
614
+ log.debug(
615
+ `Found provisional target for app '${app}'. ` +
616
+ `Old target: '${oldTargetId}', new target: '${newTargetId}'. Updating`
617
+ );
618
+ appTargetsMap[page] = newTargetId;
562
619
  return;
563
620
  }
564
621
  }
565
- log.warn(`Provisional target for app '${app}' found, but no suitable existing target found. This may cause problems`);
566
- log.warn(`Old target: '${oldTargetId}', new target: '${newTargetId}'. Existing targets: ${JSON.stringify(targets)}`);
622
+ log.warn(
623
+ `Provisional target for app '${app}' found, but no suitable existing target found. This may cause problems`
624
+ );
625
+ log.warn(
626
+ `Old target: '${oldTargetId}', new target: '${newTargetId}'. Existing targets: ${JSON.stringify(appTargetsMap)}`
627
+ );
567
628
  }
568
629
 
569
630
  // if there is no waiting provisional target, just get rid of the existing one
@@ -595,9 +656,7 @@ export class RpcClient {
595
656
  * @returns {Promise<void>}
596
657
  */
597
658
  async selectPage (appIdKey, pageIdKey) {
598
- /** @type {[import('../types').AppIdKey, import('../types').PageIdKey]} */
599
- this.pendingTargetNotification = [appIdKey, pageIdKey];
600
- this.shouldCheckForTarget = false;
659
+ this._pendingTargetNotification = [appIdKey, pageIdKey];
601
660
 
602
661
  // go through the steps that the Desktop Safari system
603
662
  // goes through to initialize the Web Inspector session
@@ -617,51 +676,17 @@ export class RpcClient {
617
676
  await this.send('setSenderKey', sendOpts);
618
677
  log.debug('Sender key set');
619
678
 
679
+ if (!this.isTargetBased) {
680
+ await this._initializePage(appIdKey, pageIdKey);
681
+ return;
682
+ }
683
+
620
684
  if (this.isTargetBased && util.compareVersions(this.platformVersion, '<', MIN_PLATFORM_NO_TARGET_EXISTS)) {
621
685
  await this.send('Target.exists', sendOpts, false);
622
686
  }
623
687
 
624
- this.shouldCheckForTarget = true;
625
-
626
688
  await this.waitForTarget(appIdKey, pageIdKey);
627
- if (this.fullPageInitialization) {
628
- await this.initializePageFull(appIdKey, pageIdKey);
629
- } else {
630
- await this.initializePage(appIdKey, pageIdKey);
631
- }
632
- }
633
-
634
- /**
635
- * Perform the minimal initialization to get the Web Inspector working
636
- * @param {import('../types').AppIdKey} appIdKey
637
- * @param {import('../types').PageIdKey} pageIdKey
638
- * @returns {Promise<void>}
639
- */
640
- async initializePage (appIdKey, pageIdKey) {
641
- const sendOpts = {
642
- appIdKey,
643
- pageIdKey,
644
- };
645
-
646
- // The sequence of domains is important
647
- for (const domain of [
648
- 'Inspector.enable',
649
- 'Page.enable',
650
- 'Runtime.enable',
651
-
652
- 'Network.enable',
653
- 'Heap.enable',
654
- 'Debugger.enable',
655
- 'Console.enable',
656
-
657
- 'Inspector.initialized',
658
- ]) {
659
- try {
660
- await this.send(domain, sendOpts);
661
- } catch (err) {
662
- log.info(`Cannot enable domain '${domain}' during initialization: ${err.message}`);
663
- }
664
- }
689
+ await this.waitForPageInitialization(appIdKey, pageIdKey);
665
690
  }
666
691
 
667
692
  /**
@@ -670,14 +695,41 @@ export class RpcClient {
670
695
  *
671
696
  * @param {import('../types').AppIdKey} appIdKey
672
697
  * @param {import('../types').PageIdKey} pageIdKey
698
+ * @param {import('../types').TargetId} [targetId]
673
699
  * @returns {Promise<void>}
674
700
  */
675
- async initializePageFull (appIdKey, pageIdKey) {
701
+ async _initializePage (appIdKey, pageIdKey, targetId) {
676
702
  const sendOpts = {
677
703
  appIdKey,
678
704
  pageIdKey,
705
+ targetId,
679
706
  };
680
707
 
708
+ log.debug(`Initializing page '${pageIdKey}' for app '${appIdKey}'`);
709
+ if (!this.fullPageInitialization) {
710
+ // The sequence of domains is important
711
+ for (const domain of [
712
+ 'Inspector.enable',
713
+ 'Page.enable',
714
+ 'Runtime.enable',
715
+
716
+ 'Network.enable',
717
+ 'Heap.enable',
718
+ 'Debugger.enable',
719
+ 'Console.enable',
720
+
721
+ 'Inspector.initialized',
722
+ ]) {
723
+ try {
724
+ await this.send(domain, sendOpts);
725
+ } catch (err) {
726
+ log.info(`Cannot enable domain '${domain}' during initialization: ${err.message}`);
727
+ }
728
+ }
729
+ log.debug(`Simple initialization of page '${pageIdKey}' for app '${appIdKey}' completed`);
730
+ return;
731
+ }
732
+
681
733
  // The sequence of commands here is important
682
734
  const domainsToOptsMap = {
683
735
  'Inspector.enable': sendOpts,
@@ -756,6 +808,7 @@ export class RpcClient {
756
808
  log.info(`Cannot enable domain '${domain}' during full initialization: ${err.message}`);
757
809
  }
758
810
  }
811
+ log.debug(`Full initialization of page '${pageIdKey}' for app '${appIdKey}' completed`);
759
812
  }
760
813
 
761
814
  /**
@@ -783,7 +836,7 @@ export class RpcClient {
783
836
  `Using id ${correctAppIdKey} instead of ${oldAppIdKey}`);
784
837
  }
785
838
 
786
- reject(new Error('New application has connected'));
839
+ reject(new Error(NEW_APP_CONNECTED_ERROR));
787
840
  };
788
841
  this.messageHandler?.prependOnceListener('_rpc_applicationConnected:', onAppChange);
789
842
 
@@ -794,7 +847,7 @@ export class RpcClient {
794
847
  // sometimes the connect logic happens, but with an empty dictionary
795
848
  // which leads to the remote debugger getting disconnected, and into a loop
796
849
  if (_.isEmpty(pageDict)) {
797
- reject(new Error('Empty page dictionary received'));
850
+ reject(new Error(EMPTY_PAGE_DICTIONARY_ERROR));
798
851
  } else {
799
852
  resolve([connectedAppIdKey, pageDict]);
800
853
  }
@@ -837,6 +890,56 @@ export class RpcClient {
837
890
  // { scriptId: '13', url: '', startLine: 0, startColumn: 0, endLine: 82, endColumn: 3 }
838
891
  log.debug(`Script parsed: ${JSON.stringify(scriptInfo)}`);
839
892
  }
893
+
894
+ /**
895
+ *
896
+ * @param {import('../types').AppIdKey} appIdKey
897
+ * @param {import('../types').PageIdKey} pageIdKey
898
+ * @param {import('../types').TargetId} [targetId]
899
+ * @returns {Promise<void>}
900
+ */
901
+ async _resumeTarget (appIdKey, pageIdKey, targetId) {
902
+ try {
903
+ await this.send('Target.resume', {
904
+ appIdKey,
905
+ pageIdKey,
906
+ targetId,
907
+ });
908
+ log.debug(`Successfully resumed the target ${targetId}@${appIdKey}`);
909
+ } catch {}
910
+ }
911
+
912
+ /**
913
+ *
914
+ * @param {import('../types').AppIdKey} appIdKey
915
+ * @param {import('../types').PageIdKey} pageIdKey
916
+ */
917
+ async waitForPageInitialization (appIdKey, pageIdKey) {
918
+ const appTargetsMap = this.targets[appIdKey];
919
+ if (!appTargetsMap) {
920
+ throw new Error(`No targets found for app '${appIdKey}'`);
921
+ }
922
+ /** @type {AsyncLock | undefined} */
923
+ const lock = appTargetsMap.lock;
924
+ if (lock?.isBusy()) {
925
+ const timer = new timing.Timer().start();
926
+ log.debug(`Waiting until page ${pageIdKey}@${appIdKey} is fully initialized`);
927
+ await lock.acquire(toLockKey(appIdKey, pageIdKey), async () => await B.delay(0));
928
+ log.debug(
929
+ `The initalization of the page ${pageIdKey}@${appIdKey} took ${timer.getDuration().asMilliSeconds}ms`
930
+ );
931
+ }
932
+ }
933
+ }
934
+
935
+ /**
936
+ *
937
+ * @param {import('../types').AppIdKey} appIdKey
938
+ * @param {import('../types').PageIdKey} pageIdKey
939
+ * @returns {string}
940
+ */
941
+ export function toLockKey(appIdKey, pageIdKey) {
942
+ return `${appIdKey}-${pageIdKey}`;
840
943
  }
841
944
 
842
945
  export default RpcClient;
@@ -852,5 +955,13 @@ export default RpcClient;
852
955
  * @property {number} [socketChunkSize]
853
956
  * @property {boolean} [fullPageInitialization=false]
854
957
  * @property {string} [udid]
855
- * @property {boolean} [shouldCheckForTarget]
856
958
  */
959
+
960
+ /**
961
+ * @typedef {{[key: import('../types').PageIdKey]: import('../types').TargetId}} PageDict
962
+ */
963
+
964
+ /**
965
+ * @typedef {PageDict & {provisional?: import('../types').ProvisionalTargetInfo, lock: import('async-lock').default}} PagesToTargets
966
+ * @typedef {{[key: import('../types').AppIdKey]: PagesToTargets}} AppToTargetsMap
967
+ */