@testdriverai/runner 7.8.0-test.39 → 7.8.0-test.40
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/lib/ably-service.js +108 -17
- package/package.json +1 -1
package/lib/ably-service.js
CHANGED
|
@@ -170,6 +170,7 @@ class AblyService extends EventEmitter {
|
|
|
170
170
|
callback(null, this._ablyToken);
|
|
171
171
|
},
|
|
172
172
|
clientId: this._clientId,
|
|
173
|
+
echoMessages: false, // don't receive our own published messages
|
|
173
174
|
disconnectedRetryTimeout: 5000, // retry reconnect every 5s (default 15s)
|
|
174
175
|
suspendedRetryTimeout: 15000, // retry from suspended every 15s (default 30s)
|
|
175
176
|
logHandler: (msg) => {
|
|
@@ -230,7 +231,7 @@ class AblyService extends EventEmitter {
|
|
|
230
231
|
level: 'info',
|
|
231
232
|
message,
|
|
232
233
|
timestamp: Date.now(),
|
|
233
|
-
}).catch(() => {}); // best-effort
|
|
234
|
+
}).catch(() => { }); // best-effort
|
|
234
235
|
});
|
|
235
236
|
this._automation.on('warn', (message) => {
|
|
236
237
|
if (!this._debugMode) return;
|
|
@@ -239,7 +240,7 @@ class AblyService extends EventEmitter {
|
|
|
239
240
|
level: 'warn',
|
|
240
241
|
message,
|
|
241
242
|
timestamp: Date.now(),
|
|
242
|
-
}).catch(() => {}); // best-effort
|
|
243
|
+
}).catch(() => { }); // best-effort
|
|
243
244
|
});
|
|
244
245
|
this._automation.on('error', (message) => {
|
|
245
246
|
if (!this._debugMode) return;
|
|
@@ -248,21 +249,24 @@ class AblyService extends EventEmitter {
|
|
|
248
249
|
level: 'error',
|
|
249
250
|
message: typeof message === 'string' ? message : message.message || String(message),
|
|
250
251
|
timestamp: Date.now(),
|
|
251
|
-
}).catch(() => {}); // best-effort
|
|
252
|
+
}).catch(() => { }); // best-effort
|
|
252
253
|
});
|
|
253
254
|
|
|
254
|
-
// Forward exec streaming chunks to SDK
|
|
255
|
+
// Forward exec streaming chunks to SDK with rate limiting.
|
|
256
|
+
// Exec output can produce many chunks rapidly (e.g. verbose commands);
|
|
257
|
+
// throttle to avoid hitting Ably's 50 msg/sec per-connection limit.
|
|
258
|
+
this._execOutputLastTime = 0;
|
|
259
|
+
this._execOutputMinIntervalMs = 50; // 20 msg/sec max for exec.output
|
|
260
|
+
this._execOutputQueue = []; // queued chunks waiting to send
|
|
261
|
+
this._execOutputDraining = false;
|
|
262
|
+
|
|
255
263
|
this._automation.on('exec.output', ({ requestId, chunk }) => {
|
|
256
|
-
this.
|
|
257
|
-
|
|
258
|
-
requestId,
|
|
259
|
-
chunk,
|
|
260
|
-
timestamp: Date.now(),
|
|
261
|
-
}).catch(() => {}); // best-effort, don't block exec
|
|
264
|
+
this._execOutputQueue.push({ requestId, chunk });
|
|
265
|
+
this._drainExecOutputQueue();
|
|
262
266
|
});
|
|
263
267
|
|
|
264
|
-
// Subscribe to commands
|
|
265
|
-
this._sessionChannel.subscribe('command', async (msg) => {
|
|
268
|
+
// Subscribe to commands — save subscription ref for historyBeforeSubscribe()
|
|
269
|
+
this._commandSubscription = await this._sessionChannel.subscribe('command', async (msg) => {
|
|
266
270
|
const message = msg.data;
|
|
267
271
|
if (!message) return;
|
|
268
272
|
|
|
@@ -293,7 +297,7 @@ class AblyService extends EventEmitter {
|
|
|
293
297
|
|
|
294
298
|
// Screenshots are now handled by automation.js (returns { s3Key })
|
|
295
299
|
// No need to check type here - just pass through the result
|
|
296
|
-
|
|
300
|
+
|
|
297
301
|
await this._sendResponse({
|
|
298
302
|
requestId,
|
|
299
303
|
type: `${type}.reply`,
|
|
@@ -407,11 +411,27 @@ class AblyService extends EventEmitter {
|
|
|
407
411
|
Sentry.captureException(err);
|
|
408
412
|
});
|
|
409
413
|
}
|
|
414
|
+
|
|
415
|
+
// Detect discontinuity: channel re-attached but message continuity was lost.
|
|
416
|
+
// Use historyBeforeSubscribe() on each subscription to recover missed messages.
|
|
417
|
+
if (current === 'attached' && stateChange.resumed === false && previous) {
|
|
418
|
+
this.emit('log', `Ably channel [session]: DISCONTINUITY (resumed=false)${reasonMsg ? ' — ' + reasonMsg : ''}`);
|
|
419
|
+
|
|
420
|
+
Sentry.withScope((scope) => {
|
|
421
|
+
scope.setTag('ably.client', 'runner');
|
|
422
|
+
scope.setTag('ably.channel', sessionCh.name);
|
|
423
|
+
scope.setTag('ably.issue', 'discontinuity');
|
|
424
|
+
scope.setFingerprint(['ably-channel-discontinuity', 'runner']);
|
|
425
|
+
Sentry.captureMessage('Ably channel discontinuity (runner)', 'warning');
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
this._recoverFromDiscontinuity();
|
|
429
|
+
}
|
|
410
430
|
});
|
|
411
431
|
}
|
|
412
432
|
|
|
413
|
-
// Subscribe to control messages
|
|
414
|
-
this._sessionChannel.subscribe('control', async (msg) => {
|
|
433
|
+
// Subscribe to control messages — save subscription ref for historyBeforeSubscribe()
|
|
434
|
+
this._controlSubscription = await this._sessionChannel.subscribe('control', async (msg) => {
|
|
415
435
|
const message = msg.data;
|
|
416
436
|
if (!message) return;
|
|
417
437
|
|
|
@@ -453,6 +473,77 @@ class AblyService extends EventEmitter {
|
|
|
453
473
|
this.emit('log', 'Published runner.ready signal');
|
|
454
474
|
}
|
|
455
475
|
|
|
476
|
+
/**
|
|
477
|
+
* Drain the exec.output queue, respecting the rate limit interval.
|
|
478
|
+
* Coalesces queued chunks per-requestId into single messages to reduce
|
|
479
|
+
* message count when output arrives faster than we can send.
|
|
480
|
+
*/
|
|
481
|
+
async _drainExecOutputQueue() {
|
|
482
|
+
if (this._execOutputDraining) return; // already draining
|
|
483
|
+
this._execOutputDraining = true;
|
|
484
|
+
|
|
485
|
+
try {
|
|
486
|
+
while (this._execOutputQueue.length > 0) {
|
|
487
|
+
// Rate limit: wait if needed
|
|
488
|
+
const now = Date.now();
|
|
489
|
+
const elapsed = now - this._execOutputLastTime;
|
|
490
|
+
if (elapsed < this._execOutputMinIntervalMs) {
|
|
491
|
+
await new Promise((resolve) => setTimeout(resolve, this._execOutputMinIntervalMs - elapsed));
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Coalesce all queued chunks for the same requestId
|
|
495
|
+
const batch = {};
|
|
496
|
+
while (this._execOutputQueue.length > 0) {
|
|
497
|
+
const { requestId, chunk } = this._execOutputQueue.shift();
|
|
498
|
+
if (!batch[requestId]) batch[requestId] = '';
|
|
499
|
+
batch[requestId] += chunk;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
this._execOutputLastTime = Date.now();
|
|
503
|
+
|
|
504
|
+
// Send one message per requestId
|
|
505
|
+
for (const [requestId, chunk] of Object.entries(batch)) {
|
|
506
|
+
this._sendResponse({
|
|
507
|
+
type: 'exec.output',
|
|
508
|
+
requestId,
|
|
509
|
+
chunk,
|
|
510
|
+
timestamp: Date.now(),
|
|
511
|
+
}).catch(() => { }); // best-effort
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
} finally {
|
|
515
|
+
this._execOutputDraining = false;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Recover missed messages after a channel discontinuity.
|
|
521
|
+
* Uses historyBeforeSubscribe() on each subscription, which guarantees
|
|
522
|
+
* no gap between historical and live messages.
|
|
523
|
+
*/
|
|
524
|
+
async _recoverFromDiscontinuity() {
|
|
525
|
+
const subs = [
|
|
526
|
+
{ name: 'command', sub: this._commandSubscription },
|
|
527
|
+
{ name: 'control', sub: this._controlSubscription },
|
|
528
|
+
];
|
|
529
|
+
for (const { name, sub } of subs) {
|
|
530
|
+
if (!sub) continue;
|
|
531
|
+
try {
|
|
532
|
+
this.emit('log', `Discontinuity recovery: fetching historyBeforeSubscribe for ${name}...`);
|
|
533
|
+
let page = await sub.historyBeforeSubscribe({ limit: 100 });
|
|
534
|
+
let recovered = 0;
|
|
535
|
+
while (page) {
|
|
536
|
+
recovered += page.items.length;
|
|
537
|
+
page = page.hasNext() ? await page.next() : null;
|
|
538
|
+
}
|
|
539
|
+
this.emit('log', `Discontinuity recovery: found ${recovered} ${name} message(s) in gap`);
|
|
540
|
+
} catch (err) {
|
|
541
|
+
this.emit('log', `Discontinuity recovery failed for ${name}: ${err.message}`);
|
|
542
|
+
Sentry.captureException(err);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
456
547
|
/**
|
|
457
548
|
* Send a response on the session channel.
|
|
458
549
|
*/
|
|
@@ -518,12 +609,12 @@ class AblyService extends EventEmitter {
|
|
|
518
609
|
|
|
519
610
|
try {
|
|
520
611
|
if (this._sessionChannel) this._sessionChannel.detach();
|
|
521
|
-
} catch {}
|
|
612
|
+
} catch { }
|
|
522
613
|
|
|
523
614
|
if (this._ably) {
|
|
524
615
|
try {
|
|
525
616
|
this._ably.close();
|
|
526
|
-
} catch {}
|
|
617
|
+
} catch { }
|
|
527
618
|
this._ably = null;
|
|
528
619
|
}
|
|
529
620
|
|