hobots 2.3.0 → 3.0.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.
- package/README.md +112 -369
- package/dist/index.cjs +1233 -852
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +223 -205
- package/dist/index.d.ts +223 -205
- package/dist/index.js +1233 -852
- package/dist/index.js.map +1 -1
- package/dist/protocol.cjs.map +1 -1
- package/dist/protocol.d.cts +43 -4
- package/dist/protocol.d.ts +43 -4
- package/docs/README.md +16 -33
- package/docs/api-reference.md +74 -213
- package/docs/concepts.md +60 -389
- package/docs/getting-started.md +38 -84
- package/docs/protocol.md +29 -19
- package/docs/tasks.md +92 -251
- package/package.json +2 -2
- package/docs/telemetry.md +0 -162
package/dist/index.js
CHANGED
|
@@ -1,54 +1,3 @@
|
|
|
1
|
-
// src/config.ts
|
|
2
|
-
var DEFAULT_BASE_URL = "https://api2.hobots.app";
|
|
3
|
-
var SECRET_PATTERN = /^hb_[a-f0-9]{64}$/i;
|
|
4
|
-
function envBaseUrl() {
|
|
5
|
-
if (typeof process === "undefined" || !process.env) return void 0;
|
|
6
|
-
return process.env.HOBOTS_API_URL?.trim() || void 0;
|
|
7
|
-
}
|
|
8
|
-
function isLoopbackHost(hostname2) {
|
|
9
|
-
return hostname2 === "localhost" || hostname2.endsWith(".localhost") || hostname2.startsWith("127.") || hostname2 === "[::1]" || hostname2 === "::1" || hostname2 === "0.0.0.0";
|
|
10
|
-
}
|
|
11
|
-
var warnedInsecureBaseUrl = false;
|
|
12
|
-
function validateBaseUrl(raw, prefix) {
|
|
13
|
-
let url;
|
|
14
|
-
try {
|
|
15
|
-
url = new URL(raw);
|
|
16
|
-
} catch {
|
|
17
|
-
throw new Error(`${prefix} HOBOTS_API_URL inv\xE1lida \u2014 esperado URL http(s), ex.: http://localhost:3001`);
|
|
18
|
-
}
|
|
19
|
-
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
20
|
-
throw new Error(`${prefix} HOBOTS_API_URL com esquema "${url.protocol}" n\xE3o suportado \u2014 use http(s)`);
|
|
21
|
-
}
|
|
22
|
-
if (url.username || url.password) {
|
|
23
|
-
throw new Error(`${prefix} HOBOTS_API_URL n\xE3o pode conter credenciais na URL \u2014 a autentica\xE7\xE3o \xE9 s\xF3 pelo Client Secret`);
|
|
24
|
-
}
|
|
25
|
-
if (url.protocol === "http:" && !isLoopbackHost(url.hostname) && !warnedInsecureBaseUrl) {
|
|
26
|
-
warnedInsecureBaseUrl = true;
|
|
27
|
-
console.warn(
|
|
28
|
-
`${prefix} HOBOTS_API_URL usa http para o host remoto "${url.hostname}" \u2014 o Client Secret trafega sem TLS`
|
|
29
|
-
);
|
|
30
|
-
}
|
|
31
|
-
return raw.replace(/\/+$/, "");
|
|
32
|
-
}
|
|
33
|
-
function resolveConfig(options, prefix = "[hobots]") {
|
|
34
|
-
const key = options.clientSecret?.trim();
|
|
35
|
-
if (!key) {
|
|
36
|
-
throw new Error(`${prefix} clientSecret ausente \u2014 copie o Client Secret do agente em Configura\xE7\xF5es \u2192 Agentes`);
|
|
37
|
-
}
|
|
38
|
-
if (!SECRET_PATTERN.test(key)) {
|
|
39
|
-
throw new Error(
|
|
40
|
-
`${prefix} clientSecret com formato inv\xE1lido (${key.length} caracteres) \u2014 esperado "hb_" + 64 hex; copie o Client Secret do agente em Configura\xE7\xF5es \u2192 Agentes`
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
const baseUrl = validateBaseUrl(envBaseUrl() ?? DEFAULT_BASE_URL, prefix);
|
|
44
|
-
return {
|
|
45
|
-
key,
|
|
46
|
-
baseUrl,
|
|
47
|
-
ingestEndpoint: `${baseUrl}/v1/ingest`,
|
|
48
|
-
tasksBaseUrl: `${baseUrl}/v1/tasks`
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
1
|
// src/stacktrace.ts
|
|
53
2
|
var MAX_FRAMES = 50;
|
|
54
3
|
var FRAME_RE = /^\s*at\s+(?:async\s+)?(?:(.+?)\s+\()?(.+?)\)?$/;
|
|
@@ -103,7 +52,7 @@ function scrubSecrets(text) {
|
|
|
103
52
|
|
|
104
53
|
// src/utils.ts
|
|
105
54
|
var SDK_NAME = "hobots";
|
|
106
|
-
var SDK_VERSION = true ? "
|
|
55
|
+
var SDK_VERSION = true ? "3.0.0" : "0.0.0-dev";
|
|
107
56
|
function isNode() {
|
|
108
57
|
return typeof process !== "undefined" && typeof process.versions?.node === "string";
|
|
109
58
|
}
|
|
@@ -131,11 +80,28 @@ function unrefTimer(timer) {
|
|
|
131
80
|
timer?.unref?.();
|
|
132
81
|
}
|
|
133
82
|
function sleep(ms) {
|
|
134
|
-
return new Promise((
|
|
135
|
-
const timer = setTimeout(
|
|
83
|
+
return new Promise((resolve2) => {
|
|
84
|
+
const timer = setTimeout(resolve2, ms);
|
|
136
85
|
unrefTimer(timer);
|
|
137
86
|
});
|
|
138
87
|
}
|
|
88
|
+
function abortableSleep(ms, signal) {
|
|
89
|
+
return new Promise((resolve2) => {
|
|
90
|
+
if (signal?.aborted) {
|
|
91
|
+
resolve2();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const onAbort = () => {
|
|
95
|
+
clearTimeout(timer);
|
|
96
|
+
resolve2();
|
|
97
|
+
};
|
|
98
|
+
const timer = setTimeout(() => {
|
|
99
|
+
signal?.removeEventListener("abort", onAbort);
|
|
100
|
+
resolve2();
|
|
101
|
+
}, ms);
|
|
102
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
103
|
+
});
|
|
104
|
+
}
|
|
139
105
|
function jitter(baseMs) {
|
|
140
106
|
return Math.floor(baseMs / 2 + Math.random() * (baseMs / 2));
|
|
141
107
|
}
|
|
@@ -256,14 +222,6 @@ function eventFromException(error, type = "error") {
|
|
|
256
222
|
}
|
|
257
223
|
};
|
|
258
224
|
}
|
|
259
|
-
function eventFromMessage(message, type = "warning") {
|
|
260
|
-
return {
|
|
261
|
-
event_id: uuid4(),
|
|
262
|
-
timestamp: nowISO(),
|
|
263
|
-
type,
|
|
264
|
-
message: scrubSecrets(message)
|
|
265
|
-
};
|
|
266
|
-
}
|
|
267
225
|
|
|
268
226
|
// src/transport.ts
|
|
269
227
|
var REQUEST_TIMEOUT_MS = 1e4;
|
|
@@ -397,7 +355,7 @@ var Client = class {
|
|
|
397
355
|
#config;
|
|
398
356
|
transport;
|
|
399
357
|
machine;
|
|
400
|
-
constructor(options, config
|
|
358
|
+
constructor(options, config) {
|
|
401
359
|
this.#options = options;
|
|
402
360
|
this.debug = !!options.debug;
|
|
403
361
|
this.logger = makeLogger(this.debug);
|
|
@@ -427,43 +385,28 @@ var Client = class {
|
|
|
427
385
|
[/* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom")]() {
|
|
428
386
|
return `Client { baseUrl: '${this.#config.baseUrl}' }`;
|
|
429
387
|
}
|
|
430
|
-
captureException(error, hint
|
|
431
|
-
return this.captureEvent(eventFromException(error, hint?.type ?? "error"), hint
|
|
388
|
+
captureException(error, hint) {
|
|
389
|
+
return this.captureEvent(eventFromException(error, hint?.type ?? "error"), hint);
|
|
432
390
|
}
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
}
|
|
436
|
-
captureEvent(event, hint, scope2) {
|
|
437
|
-
const sampleRate = this.options.sampleRate ?? 1;
|
|
438
|
-
if (sampleRate < 1 && Math.random() >= sampleRate) {
|
|
439
|
-
this.logger.debug("evento descartado por sampleRate");
|
|
440
|
-
return event.event_id;
|
|
441
|
-
}
|
|
442
|
-
const prepared = this.prepareEvent(event, hint, scope2);
|
|
443
|
-
const final = this.options.beforeSend ? this.options.beforeSend(prepared) : prepared;
|
|
444
|
-
if (!final) {
|
|
445
|
-
this.logger.debug("evento descartado por beforeSend");
|
|
446
|
-
return event.event_id;
|
|
447
|
-
}
|
|
391
|
+
captureEvent(event, hint) {
|
|
392
|
+
const final = this.prepareEvent(event, hint);
|
|
448
393
|
this.transport.enqueue({ type: "event", payload: final });
|
|
449
394
|
return final.event_id;
|
|
450
395
|
}
|
|
451
|
-
prepareEvent(event, hint
|
|
452
|
-
const tags = { ...this.options.tags, ...
|
|
453
|
-
const extra = { ...
|
|
396
|
+
prepareEvent(event, hint) {
|
|
397
|
+
const tags = { ...this.options.tags, ...event.tags, ...hint?.tags };
|
|
398
|
+
const extra = { ...event.extra, ...hint?.extra };
|
|
454
399
|
const sampled = this.machine?.sample();
|
|
455
400
|
const machine = sampled ? { machine: sampled } : {};
|
|
456
401
|
return {
|
|
457
402
|
...event,
|
|
458
|
-
type: hint?.type ?? event.type ??
|
|
403
|
+
type: hint?.type ?? event.type ?? "error",
|
|
459
404
|
release: event.release ?? this.options.release,
|
|
460
|
-
environment:
|
|
405
|
+
environment: this.resolveEnvironment(event.environment),
|
|
461
406
|
tags: Object.keys(tags).length > 0 ? tags : void 0,
|
|
462
407
|
extra: Object.keys(extra).length > 0 ? extra : void 0,
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
breadcrumbs: scope2.breadcrumbs.length > 0 ? [...scope2.breadcrumbs] : event.breadcrumbs,
|
|
466
|
-
fingerprint: hint?.fingerprint ?? event.fingerprint ?? scope2.fingerprint
|
|
408
|
+
contexts: { ...runtimeContexts(), ...machine, ...event.contexts },
|
|
409
|
+
fingerprint: hint?.fingerprint ?? event.fingerprint
|
|
467
410
|
};
|
|
468
411
|
}
|
|
469
412
|
sendTransaction(payload) {
|
|
@@ -472,7 +415,7 @@ var Client = class {
|
|
|
472
415
|
payload: {
|
|
473
416
|
...payload,
|
|
474
417
|
release: payload.release ?? this.options.release,
|
|
475
|
-
environment:
|
|
418
|
+
environment: this.resolveEnvironment(payload.environment),
|
|
476
419
|
tags: { ...this.options.tags, ...payload.tags }
|
|
477
420
|
}
|
|
478
421
|
});
|
|
@@ -480,9 +423,22 @@ var Client = class {
|
|
|
480
423
|
sendHeartbeat(payload) {
|
|
481
424
|
this.transport.enqueue({
|
|
482
425
|
type: "heartbeat",
|
|
483
|
-
payload: {
|
|
426
|
+
payload: {
|
|
427
|
+
...payload,
|
|
428
|
+
release: payload.release ?? this.options.release,
|
|
429
|
+
// o ambiente viaja no beat: é assim que a instância se classifica no app
|
|
430
|
+
environment: this.resolveEnvironment(payload.environment)
|
|
431
|
+
}
|
|
484
432
|
});
|
|
485
433
|
}
|
|
434
|
+
/**
|
|
435
|
+
* Ambiente do item: o dele, senão o das opções, senão produção. Centralizado
|
|
436
|
+
* porque evento, transaction e heartbeat precisam responder igual — o servidor
|
|
437
|
+
* normaliza apelidos legados, então aqui basta repassar.
|
|
438
|
+
*/
|
|
439
|
+
resolveEnvironment(value) {
|
|
440
|
+
return value ?? this.options.environment ?? "production";
|
|
441
|
+
}
|
|
486
442
|
flush(timeoutMs) {
|
|
487
443
|
return this.transport.flush(timeoutMs);
|
|
488
444
|
}
|
|
@@ -491,23 +447,103 @@ var Client = class {
|
|
|
491
447
|
}
|
|
492
448
|
};
|
|
493
449
|
|
|
450
|
+
// src/config.ts
|
|
451
|
+
var DEFAULT_BASE_URL = "https://api2.hobots.app";
|
|
452
|
+
var CLIENT_SECRET_ENV = "HOBOTS_CLIENT_SECRET";
|
|
453
|
+
function envClientSecret() {
|
|
454
|
+
if (typeof process === "undefined" || !process.env) return void 0;
|
|
455
|
+
return process.env[CLIENT_SECRET_ENV]?.trim() || void 0;
|
|
456
|
+
}
|
|
457
|
+
var SECRET_PATTERN = /^hb_[a-f0-9]{64}$/i;
|
|
458
|
+
function envBaseUrl() {
|
|
459
|
+
if (typeof process === "undefined" || !process.env) return void 0;
|
|
460
|
+
return process.env.HOBOTS_API_URL?.trim() || void 0;
|
|
461
|
+
}
|
|
462
|
+
function isLoopbackHost(hostname2) {
|
|
463
|
+
return hostname2 === "localhost" || hostname2.endsWith(".localhost") || hostname2.startsWith("127.") || hostname2 === "[::1]" || hostname2 === "::1" || hostname2 === "0.0.0.0";
|
|
464
|
+
}
|
|
465
|
+
var warnedInsecureBaseUrl = false;
|
|
466
|
+
function validateBaseUrl(raw, prefix) {
|
|
467
|
+
let url;
|
|
468
|
+
try {
|
|
469
|
+
url = new URL(raw);
|
|
470
|
+
} catch {
|
|
471
|
+
throw new Error(`${prefix} HOBOTS_API_URL inv\xE1lida \u2014 esperado URL http(s), ex.: http://localhost:3001`);
|
|
472
|
+
}
|
|
473
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
474
|
+
throw new Error(`${prefix} HOBOTS_API_URL com esquema "${url.protocol}" n\xE3o suportado \u2014 use http(s)`);
|
|
475
|
+
}
|
|
476
|
+
if (url.username || url.password) {
|
|
477
|
+
throw new Error(`${prefix} HOBOTS_API_URL n\xE3o pode conter credenciais na URL \u2014 a autentica\xE7\xE3o \xE9 s\xF3 pelo Client Secret`);
|
|
478
|
+
}
|
|
479
|
+
if (url.protocol === "http:" && !isLoopbackHost(url.hostname) && !warnedInsecureBaseUrl) {
|
|
480
|
+
warnedInsecureBaseUrl = true;
|
|
481
|
+
console.warn(
|
|
482
|
+
`${prefix} HOBOTS_API_URL usa http para o host remoto "${url.hostname}" \u2014 o Client Secret trafega sem TLS`
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
return raw.replace(/\/+$/, "");
|
|
486
|
+
}
|
|
487
|
+
function resolveConfig(options = {}, prefix = "[hobots]") {
|
|
488
|
+
const fromOption = options.clientSecret?.trim();
|
|
489
|
+
const key = fromOption || envClientSecret();
|
|
490
|
+
const origin = fromOption ? "clientSecret" : `env ${CLIENT_SECRET_ENV}`;
|
|
491
|
+
if (!key) {
|
|
492
|
+
throw new Error(
|
|
493
|
+
`${prefix} Client Secret ausente \u2014 defina a env ${CLIENT_SECRET_ENV} (ou passe clientSecret nas op\xE7\xF5es da task); copie o Client Secret do agente em Configura\xE7\xF5es \u2192 Agentes`
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
if (!SECRET_PATTERN.test(key)) {
|
|
497
|
+
throw new Error(
|
|
498
|
+
`${prefix} ${origin} com formato inv\xE1lido (${key.length} caracteres) \u2014 esperado "hb_" + 64 hex; copie o Client Secret do agente em Configura\xE7\xF5es \u2192 Agentes`
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
const baseUrl = validateBaseUrl(envBaseUrl() ?? DEFAULT_BASE_URL, prefix);
|
|
502
|
+
return {
|
|
503
|
+
key,
|
|
504
|
+
baseUrl,
|
|
505
|
+
ingestEndpoint: `${baseUrl}/v1/ingest`,
|
|
506
|
+
tasksBaseUrl: `${baseUrl}/v1/tasks`
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// src/env.ts
|
|
511
|
+
var INSTANCE_ID_ENV = "HOBOTS_INSTANCE_ID";
|
|
512
|
+
function resolveInstanceId(option) {
|
|
513
|
+
const fromOption = option?.trim();
|
|
514
|
+
if (fromOption) return fromOption;
|
|
515
|
+
if (typeof process === "undefined" || !process.env) return void 0;
|
|
516
|
+
return process.env[INSTANCE_ID_ENV]?.trim() || void 0;
|
|
517
|
+
}
|
|
518
|
+
async function fallbackInstanceId() {
|
|
519
|
+
if (!isNode()) return "browser";
|
|
520
|
+
try {
|
|
521
|
+
const os = await import("os");
|
|
522
|
+
return os.hostname() || "unknown-host";
|
|
523
|
+
} catch {
|
|
524
|
+
return "unknown-host";
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
494
528
|
// src/heartbeat.ts
|
|
495
529
|
var DEFAULT_INTERVAL_MS = 3e4;
|
|
496
530
|
var MIN_REPORTED_INTERVAL_MS = 1e3;
|
|
497
531
|
var MAX_REPORTED_INTERVAL_MS = 9e5;
|
|
498
532
|
var Heartbeat = class {
|
|
499
|
-
constructor(
|
|
500
|
-
this.client =
|
|
533
|
+
constructor(client, options = {}, instanceId, sampler) {
|
|
534
|
+
this.client = client;
|
|
501
535
|
this.sampler = sampler;
|
|
502
536
|
this.intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
|
|
503
537
|
this.instanceId = instanceId;
|
|
504
538
|
this.name = options.name ?? instanceId;
|
|
505
539
|
this.metadata = options.metadata;
|
|
540
|
+
this.keepAlive = options.keepAlive ?? false;
|
|
506
541
|
}
|
|
507
542
|
intervalMs;
|
|
508
543
|
instanceId;
|
|
509
544
|
name;
|
|
510
545
|
metadata;
|
|
546
|
+
keepAlive;
|
|
511
547
|
timer;
|
|
512
548
|
start() {
|
|
513
549
|
if (this.timer) return;
|
|
@@ -523,7 +559,7 @@ var Heartbeat = class {
|
|
|
523
559
|
},
|
|
524
560
|
this.intervalMs * (0.9 + Math.random() * 0.2)
|
|
525
561
|
);
|
|
526
|
-
unrefTimer(this.timer);
|
|
562
|
+
if (!this.keepAlive) unrefTimer(this.timer);
|
|
527
563
|
}
|
|
528
564
|
stop() {
|
|
529
565
|
if (this.timer) {
|
|
@@ -569,13 +605,13 @@ var installed = false;
|
|
|
569
605
|
function hasInstalledGlobalHandlers() {
|
|
570
606
|
return installed;
|
|
571
607
|
}
|
|
572
|
-
function installGlobalHandlers(
|
|
608
|
+
function installGlobalHandlers(client) {
|
|
573
609
|
if (installed) return () => void 0;
|
|
574
610
|
installed = true;
|
|
575
611
|
if (isNode()) {
|
|
576
612
|
const onUncaught = markInternal((error) => {
|
|
577
|
-
|
|
578
|
-
void
|
|
613
|
+
client.captureException(error, { type: "fatal", tags: { mechanism: "uncaughtException" } });
|
|
614
|
+
void client.flush(2e3).then(async () => {
|
|
579
615
|
if (externalListenerCount("uncaughtException") === 0) {
|
|
580
616
|
await awaitEmergencyTasks(EMERGENCY_REPORT_CAP_MS);
|
|
581
617
|
console.error(error);
|
|
@@ -584,11 +620,11 @@ function installGlobalHandlers(client2, getScope) {
|
|
|
584
620
|
});
|
|
585
621
|
});
|
|
586
622
|
const onRejection = markInternal((reason) => {
|
|
587
|
-
|
|
588
|
-
void
|
|
623
|
+
client.captureException(reason, { type: "error", tags: { mechanism: "unhandledRejection" } });
|
|
624
|
+
void client.flush(2e3);
|
|
589
625
|
});
|
|
590
626
|
const onBeforeExit = markInternal(() => {
|
|
591
|
-
void
|
|
627
|
+
void client.flush(2e3);
|
|
592
628
|
});
|
|
593
629
|
process.on("uncaughtException", onUncaught);
|
|
594
630
|
process.on("unhandledRejection", onRejection);
|
|
@@ -602,13 +638,13 @@ function installGlobalHandlers(client2, getScope) {
|
|
|
602
638
|
}
|
|
603
639
|
if (typeof window !== "undefined") {
|
|
604
640
|
const onError = (event) => {
|
|
605
|
-
|
|
641
|
+
client.captureException(event.error ?? event.message, { tags: { mechanism: "onerror" } });
|
|
606
642
|
};
|
|
607
643
|
const onRejection = (event) => {
|
|
608
|
-
|
|
644
|
+
client.captureException(event.reason, { tags: { mechanism: "onunhandledrejection" } });
|
|
609
645
|
};
|
|
610
646
|
const onPageHide = () => {
|
|
611
|
-
void
|
|
647
|
+
void client.flush(1e3);
|
|
612
648
|
};
|
|
613
649
|
window.addEventListener("error", onError);
|
|
614
650
|
window.addEventListener("unhandledrejection", onRejection);
|
|
@@ -748,349 +784,138 @@ var MachineSampler = class {
|
|
|
748
784
|
}
|
|
749
785
|
};
|
|
750
786
|
|
|
751
|
-
// src/
|
|
752
|
-
var
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
this.tags[key] = value;
|
|
762
|
-
return this;
|
|
787
|
+
// src/tasks/api.ts
|
|
788
|
+
var POLL_TIMEOUT_MS = 3e4;
|
|
789
|
+
var UPDATE_TIMEOUT_MS = 1e4;
|
|
790
|
+
var UPLOAD_TIMEOUT_MS = 3e4;
|
|
791
|
+
var UPLOAD_RETRY_DELAYS_MS = [1e3, 4e3];
|
|
792
|
+
var TasksApiError = class extends Error {
|
|
793
|
+
constructor(message, status) {
|
|
794
|
+
super(message);
|
|
795
|
+
this.status = status;
|
|
796
|
+
this.name = "TasksApiError";
|
|
763
797
|
}
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
798
|
+
};
|
|
799
|
+
var TasksApi = class {
|
|
800
|
+
baseUrl;
|
|
801
|
+
key;
|
|
802
|
+
logger;
|
|
803
|
+
constructor(options) {
|
|
804
|
+
this.baseUrl = options.baseUrl;
|
|
805
|
+
this.key = options.key;
|
|
806
|
+
this.logger = options.logger;
|
|
767
807
|
}
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
808
|
+
headers() {
|
|
809
|
+
return {
|
|
810
|
+
"content-type": "application/json",
|
|
811
|
+
authorization: `Bearer ${this.key}`
|
|
812
|
+
};
|
|
771
813
|
}
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
814
|
+
/** Um request só — o loop de polling é quem decide o backoff entre tentativas. */
|
|
815
|
+
async poll(request, signal) {
|
|
816
|
+
const { signal: timeoutSignal, cleanup } = withTimeoutSignal(POLL_TIMEOUT_MS, signal);
|
|
817
|
+
let response;
|
|
818
|
+
try {
|
|
819
|
+
response = await fetch(`${this.baseUrl}/poll`, {
|
|
820
|
+
method: "POST",
|
|
821
|
+
headers: this.headers(),
|
|
822
|
+
body: JSON.stringify(request),
|
|
823
|
+
signal: timeoutSignal
|
|
824
|
+
});
|
|
825
|
+
} finally {
|
|
826
|
+
cleanup();
|
|
777
827
|
}
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
this.
|
|
782
|
-
return this;
|
|
783
|
-
}
|
|
784
|
-
setExtras(extras) {
|
|
785
|
-
Object.assign(this.extra, extras);
|
|
786
|
-
return this;
|
|
828
|
+
if (!response.ok) {
|
|
829
|
+
throw new TasksApiError(`poll falhou (HTTP ${response.status})`, response.status);
|
|
830
|
+
}
|
|
831
|
+
return this.parsePollResponse(await response.json(), request.max ?? 1);
|
|
787
832
|
}
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
833
|
+
/**
|
|
834
|
+
* Abre uma run por conta própria (modo start). Um request só — como no poll, o
|
|
835
|
+
* ladder de espera entre tentativas é decisão do chamador (LocalRunner).
|
|
836
|
+
* 4xx (401 = chave rejeitada) vira TasksApiError com o status.
|
|
837
|
+
*/
|
|
838
|
+
async createRun(request, signal) {
|
|
839
|
+
const { signal: timeoutSignal, cleanup } = withTimeoutSignal(UPDATE_TIMEOUT_MS, signal);
|
|
840
|
+
let response;
|
|
841
|
+
try {
|
|
842
|
+
response = await fetch(`${this.baseUrl}/runs`, {
|
|
843
|
+
method: "POST",
|
|
844
|
+
headers: this.headers(),
|
|
845
|
+
body: JSON.stringify(request),
|
|
846
|
+
signal: timeoutSignal
|
|
847
|
+
});
|
|
848
|
+
} finally {
|
|
849
|
+
cleanup();
|
|
850
|
+
}
|
|
851
|
+
if (!response.ok) {
|
|
852
|
+
throw new TasksApiError(`cria\xE7\xE3o da run falhou (HTTP ${response.status})`, response.status);
|
|
853
|
+
}
|
|
854
|
+
const body = await response.json().catch(() => null);
|
|
855
|
+
if (typeof body?.id !== "string" || typeof body.task !== "string") {
|
|
856
|
+
throw new TasksApiError("resposta inv\xE1lida da cria\xE7\xE3o de run (id/task ausentes)", 0);
|
|
857
|
+
}
|
|
858
|
+
return {
|
|
859
|
+
id: body.id,
|
|
860
|
+
task: body.task,
|
|
861
|
+
params: body.params && typeof body.params === "object" && !Array.isArray(body.params) ? body.params : {},
|
|
862
|
+
form: null,
|
|
863
|
+
source: typeof body.source === "string" ? body.source : "agent",
|
|
864
|
+
created_at: typeof body.created_at === "string" ? body.created_at : (/* @__PURE__ */ new Date()).toISOString()
|
|
865
|
+
};
|
|
791
866
|
}
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
867
|
+
/** Nunca confia no shape da resposta: runs malformadas são descartadas e o `max` é respeitado. */
|
|
868
|
+
parsePollResponse(body, max) {
|
|
869
|
+
const runs = body?.runs;
|
|
870
|
+
if (!Array.isArray(runs)) {
|
|
871
|
+
throw new TasksApiError("resposta inv\xE1lida do poll (campo runs ausente)", 0);
|
|
872
|
+
}
|
|
873
|
+
const valid = [];
|
|
874
|
+
for (const run of runs.slice(0, Math.max(0, max))) {
|
|
875
|
+
if (typeof run?.id !== "string" || typeof run.task !== "string") continue;
|
|
876
|
+
const params = run.params && typeof run.params === "object" && !Array.isArray(run.params) ? run.params : {};
|
|
877
|
+
valid.push({ ...run, id: run.id, task: run.task, params });
|
|
878
|
+
}
|
|
879
|
+
if (valid.length < runs.length) {
|
|
880
|
+
this.logger.warn(`poll retornou ${runs.length - valid.length} run(s) inv\xE1lida(s)/excedente(s) \u2014 descartadas`);
|
|
881
|
+
}
|
|
882
|
+
return { runs: valid };
|
|
795
883
|
}
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
884
|
+
/**
|
|
885
|
+
* PATCH de status/logs com retry em falha de rede/5xx/429.
|
|
886
|
+
* 4xx é definitivo e vira TasksApiError imediato (409 = run cancelada/transição inválida).
|
|
887
|
+
*/
|
|
888
|
+
async updateRun(runId, update) {
|
|
889
|
+
let response;
|
|
890
|
+
try {
|
|
891
|
+
response = await fetchWithRetry(
|
|
892
|
+
() => fetch(`${this.baseUrl}/runs/${encodeURIComponent(runId)}`, {
|
|
893
|
+
method: "PATCH",
|
|
894
|
+
headers: this.headers(),
|
|
895
|
+
body: JSON.stringify(update),
|
|
896
|
+
signal: AbortSignal.timeout(UPDATE_TIMEOUT_MS)
|
|
897
|
+
}),
|
|
898
|
+
{ logger: this.logger, label: `update da run ${runId}` }
|
|
899
|
+
);
|
|
900
|
+
} catch (error) {
|
|
901
|
+
throw new Error(`update da run ${runId} descartado ap\xF3s retries: ${String(error)}`);
|
|
902
|
+
}
|
|
903
|
+
if (!response.ok) {
|
|
904
|
+
throw new TasksApiError(`update rejeitado (HTTP ${response.status})`, response.status);
|
|
800
905
|
}
|
|
801
|
-
return this;
|
|
802
906
|
}
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
next.breadcrumbs = [...this.breadcrumbs];
|
|
816
|
-
return next;
|
|
817
|
-
}
|
|
818
|
-
};
|
|
819
|
-
|
|
820
|
-
// src/transaction.ts
|
|
821
|
-
function stepId() {
|
|
822
|
-
return uuid4().slice(0, 16);
|
|
823
|
-
}
|
|
824
|
-
function stepLabel(op, description) {
|
|
825
|
-
return [op, description].filter(Boolean).join(" \u2014 ") || "(sem nome)";
|
|
826
|
-
}
|
|
827
|
-
function levelForStatus(status) {
|
|
828
|
-
switch (status) {
|
|
829
|
-
case "warning":
|
|
830
|
-
return "warning";
|
|
831
|
-
case "error":
|
|
832
|
-
return "error";
|
|
833
|
-
case "cancelled":
|
|
834
|
-
return "warning";
|
|
835
|
-
default:
|
|
836
|
-
return "success";
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
function colorForStatus(status) {
|
|
840
|
-
switch (status) {
|
|
841
|
-
case "warning":
|
|
842
|
-
return "yellow";
|
|
843
|
-
case "error":
|
|
844
|
-
return "red";
|
|
845
|
-
case "cancelled":
|
|
846
|
-
return "gray";
|
|
847
|
-
default:
|
|
848
|
-
return "green";
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
var Step = class {
|
|
852
|
-
constructor(transaction, parentStepId, op, description) {
|
|
853
|
-
this.transaction = transaction;
|
|
854
|
-
this.parentStepId = parentStepId;
|
|
855
|
-
this.op = op;
|
|
856
|
-
this.description = description;
|
|
857
|
-
}
|
|
858
|
-
stepId = stepId();
|
|
859
|
-
startTimestamp = nowISO();
|
|
860
|
-
status = "ok";
|
|
861
|
-
endTimestamp;
|
|
862
|
-
data;
|
|
863
|
-
setStatus(status) {
|
|
864
|
-
this.status = status;
|
|
865
|
-
return this;
|
|
866
|
-
}
|
|
867
|
-
setData(key, value) {
|
|
868
|
-
this.data = { ...this.data, [key]: value };
|
|
869
|
-
return this;
|
|
870
|
-
}
|
|
871
|
-
/** Step aninhada (filha desta step). */
|
|
872
|
-
startChild(op, description) {
|
|
873
|
-
return this.transaction.createStep(this.stepId, op, description);
|
|
874
|
-
}
|
|
875
|
-
finish() {
|
|
876
|
-
if (this.endTimestamp) return;
|
|
877
|
-
this.endTimestamp = nowISO();
|
|
878
|
-
this.transaction.emit(
|
|
879
|
-
"step",
|
|
880
|
-
"finish",
|
|
881
|
-
stepLabel(this.op, this.description),
|
|
882
|
-
this.startTimestamp,
|
|
883
|
-
this.endTimestamp,
|
|
884
|
-
this.status
|
|
885
|
-
);
|
|
886
|
-
}
|
|
887
|
-
toJSON() {
|
|
888
|
-
return {
|
|
889
|
-
step_id: this.stepId,
|
|
890
|
-
parent_step_id: this.parentStepId,
|
|
891
|
-
op: this.op,
|
|
892
|
-
description: this.description,
|
|
893
|
-
status: this.status,
|
|
894
|
-
start_timestamp: this.startTimestamp,
|
|
895
|
-
timestamp: this.endTimestamp ?? nowISO(),
|
|
896
|
-
data: this.data
|
|
897
|
-
};
|
|
898
|
-
}
|
|
899
|
-
};
|
|
900
|
-
var Transaction = class {
|
|
901
|
-
constructor(client2, name, op, sampled = true, runId) {
|
|
902
|
-
this.client = client2;
|
|
903
|
-
this.name = name;
|
|
904
|
-
this.op = op;
|
|
905
|
-
this.sampled = sampled;
|
|
906
|
-
this.runId = runId;
|
|
907
|
-
this.emit("transaction", "start", op ? `${name} (${op})` : name);
|
|
908
|
-
}
|
|
909
|
-
eventId = uuid4();
|
|
910
|
-
stepId = stepId();
|
|
911
|
-
startTimestamp = nowISO();
|
|
912
|
-
status = "ok";
|
|
913
|
-
steps = [];
|
|
914
|
-
tags = {};
|
|
915
|
-
finished = false;
|
|
916
|
-
lifecycleSink;
|
|
917
|
-
// eventos emitidos antes do sink existir (o START da transaction sai no construtor,
|
|
918
|
-
// antes do RunReporter conectar) — replay no setLifecycleSink
|
|
919
|
-
lifecycleBacklog = [];
|
|
920
|
-
/**
|
|
921
|
-
* Registra o início/fim de uma step ou transaction: espelha a linha no sink de
|
|
922
|
-
* lifecycle, se conectado (é assim que os eventos de performance aparecem nos
|
|
923
|
-
* logs da run no app), e ecoa no terminal apenas com `debug: true` — o SDK não
|
|
924
|
-
* escreve no stdout do processo do usuário sem permissão.
|
|
925
|
-
*/
|
|
926
|
-
emit(kind, phase, label, startTs, endTs, status) {
|
|
927
|
-
const durationMs = startTs && endTs ? new Date(endTs).getTime() - new Date(startTs).getTime() : void 0;
|
|
928
|
-
const suffix = durationMs != null ? ` (${durationMs}ms)` : "";
|
|
929
|
-
if (this.client?.debug) {
|
|
930
|
-
const tag = colorize(`[hobots:${kind}] ${phase.toUpperCase()}`, phase === "finish" ? colorForStatus(status ?? "ok") : "cyan");
|
|
931
|
-
console.log(`${tag} ${label}${suffix}`);
|
|
932
|
-
}
|
|
933
|
-
const level = phase === "finish" ? levelForStatus(status ?? "ok") : "info";
|
|
934
|
-
const message = `[${kind}] ${phase.toUpperCase()} ${label}${suffix}`;
|
|
935
|
-
if (this.lifecycleSink) this.lifecycleSink(level, message);
|
|
936
|
-
else if (this.lifecycleBacklog.length < 100) this.lifecycleBacklog.push([level, message]);
|
|
937
|
-
}
|
|
938
|
-
/** Conecta o espelho de lifecycle (RunReporter) e faz replay do backlog em ordem. */
|
|
939
|
-
setLifecycleSink(sink) {
|
|
940
|
-
this.lifecycleSink = sink;
|
|
941
|
-
const backlog = this.lifecycleBacklog;
|
|
942
|
-
this.lifecycleBacklog = [];
|
|
943
|
-
for (const [level, message] of backlog) sink(level, message);
|
|
944
|
-
}
|
|
945
|
-
createStep(parentStepId, op, description) {
|
|
946
|
-
const step = new Step(this, parentStepId, op, description);
|
|
947
|
-
this.steps.push(step);
|
|
948
|
-
this.emit("step", "start", stepLabel(op, description));
|
|
949
|
-
return step;
|
|
950
|
-
}
|
|
951
|
-
/** Step direta da transaction. */
|
|
952
|
-
startChild(op, description) {
|
|
953
|
-
return this.createStep(this.stepId, op, description);
|
|
954
|
-
}
|
|
955
|
-
setTag(key, value) {
|
|
956
|
-
this.tags[key] = value;
|
|
957
|
-
return this;
|
|
958
|
-
}
|
|
959
|
-
setStatus(status) {
|
|
960
|
-
this.status = status;
|
|
961
|
-
return this;
|
|
962
|
-
}
|
|
963
|
-
setName(name) {
|
|
964
|
-
this.name = name;
|
|
965
|
-
return this;
|
|
966
|
-
}
|
|
967
|
-
finish() {
|
|
968
|
-
if (this.finished) return;
|
|
969
|
-
this.finished = true;
|
|
970
|
-
for (const step of this.steps) step.finish();
|
|
971
|
-
const endTimestamp = nowISO();
|
|
972
|
-
this.emit(
|
|
973
|
-
"transaction",
|
|
974
|
-
"finish",
|
|
975
|
-
this.op ? `${this.name} (${this.op})` : this.name,
|
|
976
|
-
this.startTimestamp,
|
|
977
|
-
endTimestamp,
|
|
978
|
-
this.status
|
|
979
|
-
);
|
|
980
|
-
if (!this.sampled || !this.client) return;
|
|
981
|
-
const payload = {
|
|
982
|
-
event_id: this.eventId,
|
|
983
|
-
name: this.name,
|
|
984
|
-
op: this.op,
|
|
985
|
-
status: this.status,
|
|
986
|
-
start_timestamp: this.startTimestamp,
|
|
987
|
-
timestamp: endTimestamp,
|
|
988
|
-
steps: this.steps.map((step) => step.toJSON()),
|
|
989
|
-
tags: Object.keys(this.tags).length > 0 ? this.tags : void 0,
|
|
990
|
-
run_id: this.runId
|
|
991
|
-
};
|
|
992
|
-
this.client.sendTransaction(payload);
|
|
993
|
-
}
|
|
994
|
-
};
|
|
995
|
-
|
|
996
|
-
// src/tasks/api.ts
|
|
997
|
-
var POLL_TIMEOUT_MS = 3e4;
|
|
998
|
-
var UPDATE_TIMEOUT_MS = 1e4;
|
|
999
|
-
var UPLOAD_TIMEOUT_MS = 3e4;
|
|
1000
|
-
var UPLOAD_RETRY_DELAYS_MS = [1e3, 4e3];
|
|
1001
|
-
var TasksApiError = class extends Error {
|
|
1002
|
-
constructor(message, status) {
|
|
1003
|
-
super(message);
|
|
1004
|
-
this.status = status;
|
|
1005
|
-
this.name = "TasksApiError";
|
|
1006
|
-
}
|
|
1007
|
-
};
|
|
1008
|
-
var TasksApi = class {
|
|
1009
|
-
baseUrl;
|
|
1010
|
-
key;
|
|
1011
|
-
logger;
|
|
1012
|
-
constructor(options) {
|
|
1013
|
-
this.baseUrl = options.baseUrl;
|
|
1014
|
-
this.key = options.key;
|
|
1015
|
-
this.logger = options.logger;
|
|
1016
|
-
}
|
|
1017
|
-
headers() {
|
|
1018
|
-
return {
|
|
1019
|
-
"content-type": "application/json",
|
|
1020
|
-
authorization: `Bearer ${this.key}`
|
|
1021
|
-
};
|
|
1022
|
-
}
|
|
1023
|
-
/** Um request só — o loop de polling é quem decide o backoff entre tentativas. */
|
|
1024
|
-
async poll(request, signal) {
|
|
1025
|
-
const { signal: timeoutSignal, cleanup } = withTimeoutSignal(POLL_TIMEOUT_MS, signal);
|
|
1026
|
-
let response;
|
|
1027
|
-
try {
|
|
1028
|
-
response = await fetch(`${this.baseUrl}/poll`, {
|
|
1029
|
-
method: "POST",
|
|
1030
|
-
headers: this.headers(),
|
|
1031
|
-
body: JSON.stringify(request),
|
|
1032
|
-
signal: timeoutSignal
|
|
1033
|
-
});
|
|
1034
|
-
} finally {
|
|
1035
|
-
cleanup();
|
|
1036
|
-
}
|
|
1037
|
-
if (!response.ok) {
|
|
1038
|
-
throw new TasksApiError(`poll falhou (HTTP ${response.status})`, response.status);
|
|
1039
|
-
}
|
|
1040
|
-
return this.parsePollResponse(await response.json(), request.max ?? 1);
|
|
1041
|
-
}
|
|
1042
|
-
/** Nunca confia no shape da resposta: runs malformadas são descartadas e o `max` é respeitado. */
|
|
1043
|
-
parsePollResponse(body, max) {
|
|
1044
|
-
const runs = body?.runs;
|
|
1045
|
-
if (!Array.isArray(runs)) {
|
|
1046
|
-
throw new TasksApiError("resposta inv\xE1lida do poll (campo runs ausente)", 0);
|
|
1047
|
-
}
|
|
1048
|
-
const valid = [];
|
|
1049
|
-
for (const run of runs.slice(0, Math.max(0, max))) {
|
|
1050
|
-
if (typeof run?.id !== "string" || typeof run.task !== "string") continue;
|
|
1051
|
-
const params = run.params && typeof run.params === "object" && !Array.isArray(run.params) ? run.params : {};
|
|
1052
|
-
valid.push({ ...run, id: run.id, task: run.task, params });
|
|
1053
|
-
}
|
|
1054
|
-
if (valid.length < runs.length) {
|
|
1055
|
-
this.logger.warn(`poll retornou ${runs.length - valid.length} run(s) inv\xE1lida(s)/excedente(s) \u2014 descartadas`);
|
|
1056
|
-
}
|
|
1057
|
-
return { runs: valid };
|
|
1058
|
-
}
|
|
1059
|
-
/**
|
|
1060
|
-
* PATCH de status/logs com retry em falha de rede/5xx/429.
|
|
1061
|
-
* 4xx é definitivo e vira TasksApiError imediato (409 = run cancelada/transição inválida).
|
|
1062
|
-
*/
|
|
1063
|
-
async updateRun(runId, update) {
|
|
1064
|
-
let response;
|
|
1065
|
-
try {
|
|
1066
|
-
response = await fetchWithRetry(
|
|
1067
|
-
() => fetch(`${this.baseUrl}/runs/${encodeURIComponent(runId)}`, {
|
|
1068
|
-
method: "PATCH",
|
|
1069
|
-
headers: this.headers(),
|
|
1070
|
-
body: JSON.stringify(update),
|
|
1071
|
-
signal: AbortSignal.timeout(UPDATE_TIMEOUT_MS)
|
|
1072
|
-
}),
|
|
1073
|
-
{ logger: this.logger, label: `update da run ${runId}` }
|
|
1074
|
-
);
|
|
1075
|
-
} catch (error) {
|
|
1076
|
-
throw new Error(`update da run ${runId} descartado ap\xF3s retries: ${String(error)}`);
|
|
1077
|
-
}
|
|
1078
|
-
if (!response.ok) {
|
|
1079
|
-
throw new TasksApiError(`update rejeitado (HTTP ${response.status})`, response.status);
|
|
1080
|
-
}
|
|
1081
|
-
}
|
|
1082
|
-
/**
|
|
1083
|
-
* Um único PATCH com timeout curto — exit path (o processo está morrendo, o retry
|
|
1084
|
-
* ladder do updateRun é proibido aqui). Status HTTP é ignorado: 409/404 significa
|
|
1085
|
-
* que a run já foi cancelada/finalizada, o que é aceitável nesse caminho.
|
|
1086
|
-
*/
|
|
1087
|
-
async updateRunOnce(runId, update, timeoutMs = 2e3) {
|
|
1088
|
-
await fetch(`${this.baseUrl}/runs/${encodeURIComponent(runId)}`, {
|
|
1089
|
-
method: "PATCH",
|
|
1090
|
-
headers: this.headers(),
|
|
1091
|
-
body: JSON.stringify(update),
|
|
1092
|
-
signal: AbortSignal.timeout(timeoutMs)
|
|
1093
|
-
});
|
|
907
|
+
/**
|
|
908
|
+
* Um único PATCH com timeout curto — exit path (o processo está morrendo, o retry
|
|
909
|
+
* ladder do updateRun é proibido aqui). Status HTTP é ignorado: 409/404 significa
|
|
910
|
+
* que a run já foi cancelada/finalizada, o que é aceitável nesse caminho.
|
|
911
|
+
*/
|
|
912
|
+
async updateRunOnce(runId, update, timeoutMs = 2e3) {
|
|
913
|
+
await fetch(`${this.baseUrl}/runs/${encodeURIComponent(runId)}`, {
|
|
914
|
+
method: "PATCH",
|
|
915
|
+
headers: this.headers(),
|
|
916
|
+
body: JSON.stringify(update),
|
|
917
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
918
|
+
});
|
|
1094
919
|
}
|
|
1095
920
|
/**
|
|
1096
921
|
* Envia um lote de itens de execução. Retry em falha de rede/5xx/429-transporte;
|
|
@@ -1193,13 +1018,83 @@ var TasksApi = class {
|
|
|
1193
1018
|
body: JSON.stringify(request),
|
|
1194
1019
|
signal: AbortSignal.timeout(UPDATE_TIMEOUT_MS)
|
|
1195
1020
|
}
|
|
1196
|
-
);
|
|
1197
|
-
if (!response.ok) {
|
|
1198
|
-
throw new TasksApiError(`confirma\xE7\xE3o de anexo rejeitada (HTTP ${response.status})`, response.status);
|
|
1021
|
+
);
|
|
1022
|
+
if (!response.ok) {
|
|
1023
|
+
throw new TasksApiError(`confirma\xE7\xE3o de anexo rejeitada (HTTP ${response.status})`, response.status);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
};
|
|
1027
|
+
|
|
1028
|
+
// src/tasks/backoff.ts
|
|
1029
|
+
var RETRY_DELAYS_MS = [1e3, 4e3, 16e3];
|
|
1030
|
+
var AUTH_FAILURE_INTERVAL_MS = 3e4;
|
|
1031
|
+
var Backoff = class {
|
|
1032
|
+
constructor(logger, label, floorMs = 0) {
|
|
1033
|
+
this.logger = logger;
|
|
1034
|
+
this.label = label;
|
|
1035
|
+
this.floorMs = floorMs;
|
|
1036
|
+
}
|
|
1037
|
+
failures = 0;
|
|
1038
|
+
authFailures = 0;
|
|
1039
|
+
reset() {
|
|
1040
|
+
this.failures = 0;
|
|
1041
|
+
this.authFailures = 0;
|
|
1042
|
+
}
|
|
1043
|
+
next(error) {
|
|
1044
|
+
if (error instanceof TasksApiError && error.status === 401) {
|
|
1045
|
+
this.authFailures++;
|
|
1046
|
+
if (this.authFailures === 1 || this.authFailures % 10 === 0) {
|
|
1047
|
+
this.logger.error("chave rejeitada (401) \u2014 verifique o Client Secret do agente em Configura\xE7\xF5es \u2192 Agentes");
|
|
1048
|
+
}
|
|
1049
|
+
return Math.max(this.floorMs, AUTH_FAILURE_INTERVAL_MS);
|
|
1199
1050
|
}
|
|
1051
|
+
this.failures++;
|
|
1052
|
+
const ladder = RETRY_DELAYS_MS[Math.min(this.failures - 1, RETRY_DELAYS_MS.length - 1)];
|
|
1053
|
+
const delay = Math.min(Math.max(jitter(ladder), 0), Math.max(this.floorMs, 16e3));
|
|
1054
|
+
this.logger.warn(`${this.label} falhou (${String(error)}), pr\xF3xima tentativa em ${delay}ms`);
|
|
1055
|
+
return delay;
|
|
1200
1056
|
}
|
|
1201
1057
|
};
|
|
1202
1058
|
|
|
1059
|
+
// src/tasks/errors.ts
|
|
1060
|
+
function runCanceledError() {
|
|
1061
|
+
return Object.assign(new Error("execu\xE7\xE3o cancelada"), { name: "RunCanceled" });
|
|
1062
|
+
}
|
|
1063
|
+
function taskTimeoutError(timeoutMs) {
|
|
1064
|
+
return Object.assign(new Error(`timeout ap\xF3s ${timeoutMs}ms (o handler pode continuar em background)`), {
|
|
1065
|
+
name: "TaskTimeout"
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
1068
|
+
var TERMINAL_ERROR_NAMES = /* @__PURE__ */ new Set(["RunCanceled", "AbortError", "TaskTimeout"]);
|
|
1069
|
+
function isTerminalError(error) {
|
|
1070
|
+
return error instanceof Error && TERMINAL_ERROR_NAMES.has(error.name);
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// src/retry.ts
|
|
1074
|
+
var DEFAULT_RETRIES = 3;
|
|
1075
|
+
var DEFAULT_DELAY_MS = 1e3;
|
|
1076
|
+
function resolveDelay(delay, attempt, fallbackMs) {
|
|
1077
|
+
const value = typeof delay === "function" ? delay(attempt) : delay ?? fallbackMs;
|
|
1078
|
+
return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
|
|
1079
|
+
}
|
|
1080
|
+
async function retryWithBackoff(fn, options = {}) {
|
|
1081
|
+
const retries = Math.max(0, Math.floor(options.retries ?? DEFAULT_RETRIES));
|
|
1082
|
+
const wait = options.wait ?? sleep;
|
|
1083
|
+
for (let attempt = 1; ; attempt++) {
|
|
1084
|
+
if (options.signal?.aborted) throw runCanceledError();
|
|
1085
|
+
try {
|
|
1086
|
+
return await fn(attempt);
|
|
1087
|
+
} catch (error) {
|
|
1088
|
+
const exhausted = attempt > retries;
|
|
1089
|
+
const retriable = !isTerminalError(error) && !options.signal?.aborted && (options.shouldRetry?.(error) ?? true);
|
|
1090
|
+
if (exhausted || !retriable) throw error;
|
|
1091
|
+
const delay = resolveDelay(options.delayMs, attempt, DEFAULT_DELAY_MS);
|
|
1092
|
+
options.onRetry?.(error, attempt, delay);
|
|
1093
|
+
await wait(delay);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1203
1098
|
// src/tasks/attachments.ts
|
|
1204
1099
|
var MAX_ATTACHMENT_BYTES = 5242880;
|
|
1205
1100
|
var MAX_ATTACHMENTS_PER_RUN = 20;
|
|
@@ -1251,14 +1146,11 @@ var LOG_LEVEL_COLOR = {
|
|
|
1251
1146
|
};
|
|
1252
1147
|
function printRunLog(task, level, message) {
|
|
1253
1148
|
const line = colorize(`[hobots:${task}] ${level.toUpperCase()}`, LOG_LEVEL_COLOR[level]);
|
|
1254
|
-
const
|
|
1255
|
-
console[
|
|
1256
|
-
}
|
|
1257
|
-
function runCanceledError() {
|
|
1258
|
-
return Object.assign(new Error("execu\xE7\xE3o cancelada"), { name: "RunCanceled" });
|
|
1149
|
+
const method2 = level === "warning" ? "warn" : level === "success" ? "info" : level;
|
|
1150
|
+
console[method2](line, message);
|
|
1259
1151
|
}
|
|
1260
1152
|
function cancelableSleep(ms, signal) {
|
|
1261
|
-
return new Promise((
|
|
1153
|
+
return new Promise((resolve2, reject) => {
|
|
1262
1154
|
if (signal.aborted) {
|
|
1263
1155
|
reject(runCanceledError());
|
|
1264
1156
|
return;
|
|
@@ -1269,7 +1161,7 @@ function cancelableSleep(ms, signal) {
|
|
|
1269
1161
|
};
|
|
1270
1162
|
const timer = setTimeout(() => {
|
|
1271
1163
|
signal.removeEventListener("abort", onAbort);
|
|
1272
|
-
|
|
1164
|
+
resolve2();
|
|
1273
1165
|
}, ms);
|
|
1274
1166
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
1275
1167
|
});
|
|
@@ -1299,7 +1191,7 @@ function scrubPayloadValue(value, depth) {
|
|
|
1299
1191
|
return out;
|
|
1300
1192
|
}
|
|
1301
1193
|
var RunReporter = class {
|
|
1302
|
-
constructor(api, runId, task, logger, tx, flushIntervalMs, cancelCheckIntervalMs) {
|
|
1194
|
+
constructor(api, runId, task, logger, tx, flushIntervalMs, cancelCheckIntervalMs, params = {}) {
|
|
1303
1195
|
this.api = api;
|
|
1304
1196
|
this.runId = runId;
|
|
1305
1197
|
this.logger = logger;
|
|
@@ -1316,17 +1208,40 @@ var RunReporter = class {
|
|
|
1316
1208
|
success: (message) => this.log("success", message)
|
|
1317
1209
|
};
|
|
1318
1210
|
tx.setLifecycleSink((level, message) => this.push(level, message));
|
|
1211
|
+
const checkCanceled = () => this.checkCanceled();
|
|
1212
|
+
tx.setStepGuard(checkCanceled);
|
|
1319
1213
|
const items = {
|
|
1320
|
-
add: (item) =>
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1214
|
+
add: (item) => {
|
|
1215
|
+
checkCanceled();
|
|
1216
|
+
this.addItem(item.status, item.message, item.payload, item.id);
|
|
1217
|
+
},
|
|
1218
|
+
succeeded: (payload, options) => {
|
|
1219
|
+
checkCanceled();
|
|
1220
|
+
this.addItem("succeeded", void 0, payload, options?.id);
|
|
1221
|
+
},
|
|
1222
|
+
failed: (message, payload, options) => {
|
|
1223
|
+
checkCanceled();
|
|
1224
|
+
this.addItem("failed", message, payload, options?.id);
|
|
1225
|
+
},
|
|
1226
|
+
occurrence: (message, payload, options) => {
|
|
1227
|
+
checkCanceled();
|
|
1228
|
+
this.addItem("occurrence", message, payload, options?.id);
|
|
1229
|
+
}
|
|
1324
1230
|
};
|
|
1325
1231
|
const progress = Object.defineProperties(
|
|
1326
1232
|
{
|
|
1327
|
-
total: (total) =>
|
|
1328
|
-
|
|
1329
|
-
|
|
1233
|
+
total: (total) => {
|
|
1234
|
+
checkCanceled();
|
|
1235
|
+
this.setProgress(this.progressCurrent, total);
|
|
1236
|
+
},
|
|
1237
|
+
advance: (count = 1) => {
|
|
1238
|
+
checkCanceled();
|
|
1239
|
+
this.setProgress(this.progressCurrent + count, this.progressTotal);
|
|
1240
|
+
},
|
|
1241
|
+
set: (current, total) => {
|
|
1242
|
+
checkCanceled();
|
|
1243
|
+
this.setProgress(current, total ?? this.progressTotal);
|
|
1244
|
+
}
|
|
1330
1245
|
},
|
|
1331
1246
|
{
|
|
1332
1247
|
current: { enumerable: true, get: () => this.progressCurrent },
|
|
@@ -1338,18 +1253,20 @@ var RunReporter = class {
|
|
|
1338
1253
|
// defineProperties não tipa os getters adicionados — a interface é a fonte de verdade
|
|
1339
1254
|
);
|
|
1340
1255
|
const signal = this.abortController.signal;
|
|
1256
|
+
const sleep2 = (ms) => cancelableSleep(ms, signal);
|
|
1341
1257
|
this.context = {
|
|
1342
1258
|
runId,
|
|
1343
1259
|
task,
|
|
1260
|
+
params,
|
|
1344
1261
|
log,
|
|
1345
1262
|
items,
|
|
1346
1263
|
progress,
|
|
1347
|
-
tx,
|
|
1264
|
+
transaction: tx,
|
|
1348
1265
|
signal,
|
|
1349
|
-
throwIfCanceled:
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1266
|
+
throwIfCanceled: checkCanceled,
|
|
1267
|
+
sleep: sleep2,
|
|
1268
|
+
attach: (image, options) => this.attach(image, options),
|
|
1269
|
+
retry: (fn, options) => retryWithBackoff(fn, { ...options, signal, wait: sleep2 })
|
|
1353
1270
|
};
|
|
1354
1271
|
}
|
|
1355
1272
|
context;
|
|
@@ -1398,6 +1315,10 @@ var RunReporter = class {
|
|
|
1398
1315
|
get isDead() {
|
|
1399
1316
|
return this.dead;
|
|
1400
1317
|
}
|
|
1318
|
+
/** Lança `RunCanceled` se o signal desta run já disparou. */
|
|
1319
|
+
checkCanceled() {
|
|
1320
|
+
if (this.abortController.signal.aborted) throw runCanceledError();
|
|
1321
|
+
}
|
|
1401
1322
|
/** Aborta o ctx.signal sem matar o reporter (timeout/shutdown ainda reportam o terminal). */
|
|
1402
1323
|
abort() {
|
|
1403
1324
|
this.stopCancelWatch();
|
|
@@ -1676,13 +1597,13 @@ var RunReporter = class {
|
|
|
1676
1597
|
this.flushItems();
|
|
1677
1598
|
await Promise.race([
|
|
1678
1599
|
this.itemsChain,
|
|
1679
|
-
new Promise((
|
|
1600
|
+
new Promise((resolve2) => unrefTimer(setTimeout(resolve2, timeoutMs)))
|
|
1680
1601
|
]);
|
|
1681
1602
|
if (this.itemsBuffer.length > 0 && !this.itemsUnsupported && !this.itemsQuotaExceeded) {
|
|
1682
1603
|
this.flushItems();
|
|
1683
1604
|
await Promise.race([
|
|
1684
1605
|
this.itemsChain,
|
|
1685
|
-
new Promise((
|
|
1606
|
+
new Promise((resolve2) => unrefTimer(setTimeout(resolve2, timeoutMs)))
|
|
1686
1607
|
]);
|
|
1687
1608
|
}
|
|
1688
1609
|
}
|
|
@@ -1690,8 +1611,8 @@ var RunReporter = class {
|
|
|
1690
1611
|
/**
|
|
1691
1612
|
* Registra a linha de log com o id do anexo (síncrono, na ordem certa) e dispara
|
|
1692
1613
|
* o upload em segundo plano. Nunca lança: todo caminho de erro vira `{ ok: false }`
|
|
1693
|
-
* com um aviso no log da run.
|
|
1694
|
-
* `
|
|
1614
|
+
* com um aviso no log da run. É o `hobots.attach()`; o hint `attachment` de
|
|
1615
|
+
* `hobots.captureException` também cai aqui via run-context-store.
|
|
1695
1616
|
*/
|
|
1696
1617
|
async attach(image, options = {}) {
|
|
1697
1618
|
const id = uuid4();
|
|
@@ -1713,8 +1634,8 @@ var RunReporter = class {
|
|
|
1713
1634
|
printRunLog(this.context.task, level, `${scrubbed} [imagem]`);
|
|
1714
1635
|
const logTs = this.push(level, scrubbed, true, [id]);
|
|
1715
1636
|
let settle;
|
|
1716
|
-
const result = new Promise((
|
|
1717
|
-
settle =
|
|
1637
|
+
const result = new Promise((resolve2) => {
|
|
1638
|
+
settle = resolve2;
|
|
1718
1639
|
});
|
|
1719
1640
|
const work = this.uploadAttachment(id, image, { caption, logTs, contentType: options.contentType }).then((reason) => settle(reason ? { id, ok: false, reason } : { id, ok: true })).catch(() => settle({ id, ok: false, reason: "upload-failed" }));
|
|
1720
1641
|
this.pendingAttachments.add(work);
|
|
@@ -1796,8 +1717,8 @@ var RunReporter = class {
|
|
|
1796
1717
|
this.uploadSlots--;
|
|
1797
1718
|
return Promise.resolve(release);
|
|
1798
1719
|
}
|
|
1799
|
-
return new Promise((
|
|
1800
|
-
this.uploadQueue.push(() =>
|
|
1720
|
+
return new Promise((resolve2) => {
|
|
1721
|
+
this.uploadQueue.push(() => resolve2(release));
|
|
1801
1722
|
});
|
|
1802
1723
|
}
|
|
1803
1724
|
/**
|
|
@@ -1808,7 +1729,7 @@ var RunReporter = class {
|
|
|
1808
1729
|
if (this.pendingAttachments.size === 0) return;
|
|
1809
1730
|
await Promise.race([
|
|
1810
1731
|
Promise.allSettled([...this.pendingAttachments]),
|
|
1811
|
-
new Promise((
|
|
1732
|
+
new Promise((resolve2) => unrefTimer(setTimeout(resolve2, timeoutMs)))
|
|
1812
1733
|
]);
|
|
1813
1734
|
}
|
|
1814
1735
|
enqueue(updateIn) {
|
|
@@ -1879,119 +1800,354 @@ async function runWithRunContext(store, fn) {
|
|
|
1879
1800
|
await ensureStorage();
|
|
1880
1801
|
return storage ? storage.run(store, fn) : fn();
|
|
1881
1802
|
}
|
|
1882
|
-
function
|
|
1883
|
-
return storage?.getStore()?.
|
|
1803
|
+
function currentRunReporter() {
|
|
1804
|
+
return storage?.getStore()?.reporter;
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
// src/transaction.ts
|
|
1808
|
+
function stepId() {
|
|
1809
|
+
return uuid4().slice(0, 16);
|
|
1810
|
+
}
|
|
1811
|
+
function stepLabel(op, description) {
|
|
1812
|
+
return [op, description].filter(Boolean).join(" \u2014 ") || "(sem nome)";
|
|
1813
|
+
}
|
|
1814
|
+
function levelForStatus(status) {
|
|
1815
|
+
switch (status) {
|
|
1816
|
+
case "warning":
|
|
1817
|
+
return "warning";
|
|
1818
|
+
case "error":
|
|
1819
|
+
return "error";
|
|
1820
|
+
case "cancelled":
|
|
1821
|
+
return "warning";
|
|
1822
|
+
default:
|
|
1823
|
+
return "success";
|
|
1824
|
+
}
|
|
1884
1825
|
}
|
|
1885
|
-
function
|
|
1886
|
-
|
|
1826
|
+
function colorForStatus(status) {
|
|
1827
|
+
switch (status) {
|
|
1828
|
+
case "warning":
|
|
1829
|
+
return "yellow";
|
|
1830
|
+
case "error":
|
|
1831
|
+
return "red";
|
|
1832
|
+
case "cancelled":
|
|
1833
|
+
return "gray";
|
|
1834
|
+
default:
|
|
1835
|
+
return "green";
|
|
1836
|
+
}
|
|
1887
1837
|
}
|
|
1838
|
+
var Step = class {
|
|
1839
|
+
constructor(transaction, parentStepId, op, description) {
|
|
1840
|
+
this.transaction = transaction;
|
|
1841
|
+
this.parentStepId = parentStepId;
|
|
1842
|
+
this.op = op;
|
|
1843
|
+
this.description = description;
|
|
1844
|
+
}
|
|
1845
|
+
stepId = stepId();
|
|
1846
|
+
startTimestamp = nowISO();
|
|
1847
|
+
status = "ok";
|
|
1848
|
+
endTimestamp;
|
|
1849
|
+
data;
|
|
1850
|
+
setStatus(status) {
|
|
1851
|
+
this.status = status;
|
|
1852
|
+
return this;
|
|
1853
|
+
}
|
|
1854
|
+
setData(key, value) {
|
|
1855
|
+
this.data = { ...this.data, [key]: value };
|
|
1856
|
+
return this;
|
|
1857
|
+
}
|
|
1858
|
+
/** Step aninhada (filha desta step). */
|
|
1859
|
+
startChild(op, description) {
|
|
1860
|
+
return this.transaction.createStep(this.stepId, op, description);
|
|
1861
|
+
}
|
|
1862
|
+
finish() {
|
|
1863
|
+
if (this.endTimestamp) return;
|
|
1864
|
+
this.endTimestamp = nowISO();
|
|
1865
|
+
this.transaction.emit(
|
|
1866
|
+
"step",
|
|
1867
|
+
"finish",
|
|
1868
|
+
stepLabel(this.op, this.description),
|
|
1869
|
+
this.startTimestamp,
|
|
1870
|
+
this.endTimestamp,
|
|
1871
|
+
this.status
|
|
1872
|
+
);
|
|
1873
|
+
}
|
|
1874
|
+
toJSON() {
|
|
1875
|
+
return {
|
|
1876
|
+
step_id: this.stepId,
|
|
1877
|
+
parent_step_id: this.parentStepId,
|
|
1878
|
+
op: this.op,
|
|
1879
|
+
description: this.description,
|
|
1880
|
+
status: this.status,
|
|
1881
|
+
start_timestamp: this.startTimestamp,
|
|
1882
|
+
timestamp: this.endTimestamp ?? nowISO(),
|
|
1883
|
+
data: this.data
|
|
1884
|
+
};
|
|
1885
|
+
}
|
|
1886
|
+
};
|
|
1887
|
+
var Transaction = class {
|
|
1888
|
+
constructor(client, name, op, runId) {
|
|
1889
|
+
this.client = client;
|
|
1890
|
+
this.name = name;
|
|
1891
|
+
this.op = op;
|
|
1892
|
+
this.runId = runId;
|
|
1893
|
+
this.emit("transaction", "start", op ? `${name} (${op})` : name);
|
|
1894
|
+
}
|
|
1895
|
+
eventId = uuid4();
|
|
1896
|
+
stepId = stepId();
|
|
1897
|
+
startTimestamp = nowISO();
|
|
1898
|
+
status = "ok";
|
|
1899
|
+
steps = [];
|
|
1900
|
+
tags = {};
|
|
1901
|
+
finished = false;
|
|
1902
|
+
lifecycleSink;
|
|
1903
|
+
// instalado pelo RunReporter: abrir uma step é um ponto de parada cooperativo (lança RunCanceled)
|
|
1904
|
+
stepGuard;
|
|
1905
|
+
// eventos emitidos antes do sink existir (o START da transaction sai no construtor,
|
|
1906
|
+
// antes do RunReporter conectar) — replay no setLifecycleSink
|
|
1907
|
+
lifecycleBacklog = [];
|
|
1908
|
+
/**
|
|
1909
|
+
* Registra o início/fim de uma step ou transaction: espelha a linha no sink de
|
|
1910
|
+
* lifecycle, se conectado (é assim que os eventos de performance aparecem nos
|
|
1911
|
+
* logs da run no app), e ecoa no terminal apenas com `debug: true` — o SDK não
|
|
1912
|
+
* escreve no stdout do processo do usuário sem permissão.
|
|
1913
|
+
*/
|
|
1914
|
+
emit(kind, phase, label, startTs, endTs, status) {
|
|
1915
|
+
const durationMs = startTs && endTs ? new Date(endTs).getTime() - new Date(startTs).getTime() : void 0;
|
|
1916
|
+
const suffix = durationMs != null ? ` (${durationMs}ms)` : "";
|
|
1917
|
+
if (this.client?.debug) {
|
|
1918
|
+
const tag = colorize(`[hobots:${kind}] ${phase.toUpperCase()}`, phase === "finish" ? colorForStatus(status ?? "ok") : "cyan");
|
|
1919
|
+
console.log(`${tag} ${label}${suffix}`);
|
|
1920
|
+
}
|
|
1921
|
+
const level = phase === "finish" ? levelForStatus(status ?? "ok") : "info";
|
|
1922
|
+
const message = `[${kind}] ${phase.toUpperCase()} ${label}${suffix}`;
|
|
1923
|
+
if (this.lifecycleSink) this.lifecycleSink(level, message);
|
|
1924
|
+
else if (this.lifecycleBacklog.length < 100) this.lifecycleBacklog.push([level, message]);
|
|
1925
|
+
}
|
|
1926
|
+
/** Conecta o espelho de lifecycle (RunReporter) e faz replay do backlog em ordem. */
|
|
1927
|
+
setLifecycleSink(sink) {
|
|
1928
|
+
this.lifecycleSink = sink;
|
|
1929
|
+
const backlog = this.lifecycleBacklog;
|
|
1930
|
+
this.lifecycleBacklog = [];
|
|
1931
|
+
for (const [level, message] of backlog) sink(level, message);
|
|
1932
|
+
}
|
|
1933
|
+
/**
|
|
1934
|
+
* Conecta a checagem de cancelamento chamada antes de cada step nova — é assim que
|
|
1935
|
+
* `hobots.transaction.startChild()` (e `step.startChild()`) param uma run cancelada.
|
|
1936
|
+
*/
|
|
1937
|
+
setStepGuard(guard) {
|
|
1938
|
+
this.stepGuard = guard;
|
|
1939
|
+
}
|
|
1940
|
+
createStep(parentStepId, op, description) {
|
|
1941
|
+
this.stepGuard?.();
|
|
1942
|
+
const step = new Step(this, parentStepId, op, description);
|
|
1943
|
+
this.steps.push(step);
|
|
1944
|
+
this.emit("step", "start", stepLabel(op, description));
|
|
1945
|
+
return step;
|
|
1946
|
+
}
|
|
1947
|
+
/** Step direta da transaction. */
|
|
1948
|
+
startChild(op, description) {
|
|
1949
|
+
return this.createStep(this.stepId, op, description);
|
|
1950
|
+
}
|
|
1951
|
+
setTag(key, value) {
|
|
1952
|
+
this.tags[key] = value;
|
|
1953
|
+
return this;
|
|
1954
|
+
}
|
|
1955
|
+
setStatus(status) {
|
|
1956
|
+
this.status = status;
|
|
1957
|
+
return this;
|
|
1958
|
+
}
|
|
1959
|
+
setName(name) {
|
|
1960
|
+
this.name = name;
|
|
1961
|
+
return this;
|
|
1962
|
+
}
|
|
1963
|
+
finish() {
|
|
1964
|
+
if (this.finished) return;
|
|
1965
|
+
this.finished = true;
|
|
1966
|
+
for (const step of this.steps) step.finish();
|
|
1967
|
+
const endTimestamp = nowISO();
|
|
1968
|
+
this.emit(
|
|
1969
|
+
"transaction",
|
|
1970
|
+
"finish",
|
|
1971
|
+
this.op ? `${this.name} (${this.op})` : this.name,
|
|
1972
|
+
this.startTimestamp,
|
|
1973
|
+
endTimestamp,
|
|
1974
|
+
this.status
|
|
1975
|
+
);
|
|
1976
|
+
if (!this.client) return;
|
|
1977
|
+
const payload = {
|
|
1978
|
+
event_id: this.eventId,
|
|
1979
|
+
name: this.name,
|
|
1980
|
+
op: this.op,
|
|
1981
|
+
status: this.status,
|
|
1982
|
+
start_timestamp: this.startTimestamp,
|
|
1983
|
+
timestamp: endTimestamp,
|
|
1984
|
+
steps: this.steps.map((step) => step.toJSON()),
|
|
1985
|
+
tags: Object.keys(this.tags).length > 0 ? this.tags : void 0,
|
|
1986
|
+
run_id: this.runId
|
|
1987
|
+
};
|
|
1988
|
+
this.client.sendTransaction(payload);
|
|
1989
|
+
}
|
|
1990
|
+
};
|
|
1888
1991
|
|
|
1889
|
-
// src/tasks/
|
|
1890
|
-
var
|
|
1891
|
-
function
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
const
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1992
|
+
// src/tasks/runner.ts
|
|
1993
|
+
var DEFAULT_RETRY_DELAY_MS = 5e3;
|
|
1994
|
+
async function executeRun(input) {
|
|
1995
|
+
const { run, task, api, client, logger, tracker } = input;
|
|
1996
|
+
const options = task.options;
|
|
1997
|
+
const tx = new Transaction(client, `run:${run.task}`, "hobots.task", run.id);
|
|
1998
|
+
const reporter = new RunReporter(
|
|
1999
|
+
api,
|
|
2000
|
+
run.id,
|
|
2001
|
+
run.task,
|
|
2002
|
+
logger,
|
|
2003
|
+
tx,
|
|
2004
|
+
options.logFlushIntervalMs,
|
|
2005
|
+
options.cancelCheckIntervalMs,
|
|
2006
|
+
run.params
|
|
2007
|
+
);
|
|
2008
|
+
tracker.add(reporter);
|
|
2009
|
+
try {
|
|
2010
|
+
const ok = await reporter.reportRunning();
|
|
2011
|
+
if (!ok) {
|
|
2012
|
+
logger.debug(`run ${run.id} cancelada antes de iniciar \u2014 handler n\xE3o executado`);
|
|
2013
|
+
tx.setStatus("cancelled");
|
|
2014
|
+
tx.finish();
|
|
2015
|
+
return;
|
|
2016
|
+
}
|
|
2017
|
+
logger.debug(`executando run ${run.id} (${run.task})`);
|
|
2018
|
+
try {
|
|
2019
|
+
await runWithRunContext({ reporter }, () => invokeWithRetry(task, run, reporter, logger));
|
|
2020
|
+
await reporter.finalizeAttachments();
|
|
2021
|
+
await reporter.finalizeItems();
|
|
2022
|
+
tx.setStatus("ok");
|
|
2023
|
+
tx.finish();
|
|
2024
|
+
reporter.reportSuccess();
|
|
2025
|
+
logger.debug(`run ${run.id} conclu\xEDda`);
|
|
2026
|
+
} catch (error) {
|
|
2027
|
+
await reporter.finalizeAttachments();
|
|
2028
|
+
await reporter.finalizeItems();
|
|
2029
|
+
tx.setStatus("error");
|
|
2030
|
+
tx.finish();
|
|
2031
|
+
reporter.reportFailure(toRunError(error));
|
|
2032
|
+
logger.debug(`run ${run.id} falhou: ${String(error)}`);
|
|
2033
|
+
}
|
|
2034
|
+
await reporter.finalize();
|
|
2035
|
+
} finally {
|
|
2036
|
+
tracker.remove(reporter);
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
function invokeWithRetry(task, run, reporter, logger) {
|
|
2040
|
+
const { retries, retryDelayMs } = task.options;
|
|
2041
|
+
const total = retries + 1;
|
|
2042
|
+
const ctx = reporter.context;
|
|
2043
|
+
return retryWithBackoff(() => invokeWithTimeout(task, run, reporter, logger), {
|
|
2044
|
+
retries,
|
|
2045
|
+
delayMs: (attempt) => resolveDelay(retryDelayMs, attempt, DEFAULT_RETRY_DELAY_MS),
|
|
2046
|
+
shouldRetry: () => !reporter.isDead,
|
|
2047
|
+
signal: ctx.signal,
|
|
2048
|
+
wait: (ms) => ctx.sleep(ms),
|
|
2049
|
+
onRetry: (error, attempt, delayMs) => {
|
|
2050
|
+
const reason = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
|
2051
|
+
ctx.log.warn(`tentativa ${attempt}/${total} falhou: ${truncate(reason, 500)} \u2014 nova tentativa em ${delayMs}ms`);
|
|
2052
|
+
}
|
|
2053
|
+
});
|
|
2054
|
+
}
|
|
2055
|
+
var TIMEOUT_SENTINEL = /* @__PURE__ */ Symbol("task-timeout");
|
|
2056
|
+
var CANCEL_SENTINEL = /* @__PURE__ */ Symbol("task-canceled");
|
|
2057
|
+
async function invokeWithTimeout(task, run, reporter, logger) {
|
|
2058
|
+
const { timeoutMs, cancelGraceMs } = task.options;
|
|
2059
|
+
const handler = task.handler;
|
|
2060
|
+
const invocation = Promise.resolve().then(() => handler(run.params, reporter.context));
|
|
2061
|
+
const races = [];
|
|
2062
|
+
if (timeoutMs && timeoutMs > 0) {
|
|
2063
|
+
races.push(sleep(timeoutMs).then(() => TIMEOUT_SENTINEL));
|
|
2064
|
+
}
|
|
2065
|
+
if (cancelGraceMs > 0) {
|
|
2066
|
+
races.push(waitForCancelGrace(reporter.context.signal, cancelGraceMs).then(() => CANCEL_SENTINEL));
|
|
2067
|
+
}
|
|
2068
|
+
if (races.length === 0) {
|
|
2069
|
+
await invocation;
|
|
2070
|
+
return;
|
|
1915
2071
|
}
|
|
1916
|
-
const
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
2072
|
+
const outcome = await Promise.race([invocation, ...races]);
|
|
2073
|
+
if (outcome !== TIMEOUT_SENTINEL && outcome !== CANCEL_SENTINEL) return;
|
|
2074
|
+
if (outcome === CANCEL_SENTINEL) {
|
|
2075
|
+
logger.warn(
|
|
2076
|
+
`run ${run.id} cancelada \u2014 handler n\xE3o respondeu a hobots.signal em ${cancelGraceMs}ms, liberando o slot (o handler pode continuar rodando em segundo plano)`
|
|
2077
|
+
);
|
|
2078
|
+
return;
|
|
2079
|
+
}
|
|
2080
|
+
reporter.abort();
|
|
2081
|
+
invocation.then(() => logger.warn(`run ${run.id} terminou depois do timeout de ${timeoutMs}ms`)).catch(() => logger.warn(`run ${run.id} falhou depois do timeout de ${timeoutMs}ms (erro descartado)`));
|
|
2082
|
+
throw taskTimeoutError(timeoutMs);
|
|
2083
|
+
}
|
|
2084
|
+
function waitForCancelGrace(signal, graceMs) {
|
|
2085
|
+
return new Promise((resolve2) => {
|
|
2086
|
+
if (signal.aborted) {
|
|
2087
|
+
setTimeout(resolve2, graceMs);
|
|
2088
|
+
return;
|
|
1924
2089
|
}
|
|
2090
|
+
signal.addEventListener("abort", () => setTimeout(resolve2, graceMs), { once: true });
|
|
1925
2091
|
});
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
2092
|
+
}
|
|
2093
|
+
var MAX_ERROR_MESSAGE_CHARS = 1e4;
|
|
2094
|
+
var MAX_ERROR_STACK_CHARS = 2e4;
|
|
2095
|
+
function toRunError(error) {
|
|
2096
|
+
if (error instanceof Error) {
|
|
2097
|
+
return {
|
|
2098
|
+
type: error.name,
|
|
2099
|
+
message: scrubSecrets(truncate(error.message, MAX_ERROR_MESSAGE_CHARS)),
|
|
2100
|
+
stack: error.stack ? scrubSecrets(truncate(error.stack, MAX_ERROR_STACK_CHARS)) : void 0
|
|
2101
|
+
};
|
|
2102
|
+
}
|
|
2103
|
+
return { message: scrubSecrets(truncate(String(error), MAX_ERROR_MESSAGE_CHARS)) };
|
|
1931
2104
|
}
|
|
1932
2105
|
|
|
1933
2106
|
// src/tasks/agent.ts
|
|
1934
|
-
var DEFAULT_POLL_INTERVAL_MS =
|
|
2107
|
+
var DEFAULT_POLL_INTERVAL_MS = 2e3;
|
|
1935
2108
|
var MIN_POLL_INTERVAL_MS = 1e3;
|
|
1936
|
-
var
|
|
1937
|
-
var POLL_RETRY_DELAYS_MS = [1e3, 4e3, 16e3];
|
|
1938
|
-
var AUTH_FAILURE_INTERVAL_MS = 3e4;
|
|
1939
|
-
var DEFAULT_CANCEL_GRACE_MS = 1e4;
|
|
1940
|
-
function hostname() {
|
|
1941
|
-
if (!isNode()) return void 0;
|
|
1942
|
-
return process.env.HOSTNAME || process.env.COMPUTERNAME || void 0;
|
|
1943
|
-
}
|
|
2109
|
+
var MAX_CLAIM_PER_POLL = 5;
|
|
1944
2110
|
var TaskAgent = class {
|
|
1945
|
-
constructor(options
|
|
2111
|
+
constructor(options) {
|
|
1946
2112
|
this.options = options;
|
|
1947
|
-
this.
|
|
1948
|
-
this.
|
|
1949
|
-
const config = options.config;
|
|
1950
|
-
this.api = new TasksApi({ baseUrl: config.tasksBaseUrl, key: config.key, logger: this.logger });
|
|
2113
|
+
this.logger = options.logger;
|
|
2114
|
+
this.api = options.api;
|
|
1951
2115
|
this.client = options.client;
|
|
2116
|
+
this.registry = options.registry;
|
|
2117
|
+
this.tracker = options.tracker;
|
|
1952
2118
|
this.pollIntervalMs = Math.max(MIN_POLL_INTERVAL_MS, options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS);
|
|
1953
|
-
this.
|
|
1954
|
-
this.instanceId = options.instanceId;
|
|
1955
|
-
this.taskTimeoutMs = options.taskTimeoutMs;
|
|
1956
|
-
this.logFlushIntervalMs = options.logFlushIntervalMs;
|
|
1957
|
-
this.cancelCheckIntervalMs = options.cancelCheckIntervalMs;
|
|
1958
|
-
this.cancelGraceMs = options.cancelGraceMs ?? DEFAULT_CANCEL_GRACE_MS;
|
|
1959
|
-
this.logger.debug("agente configurado", { baseUrl: config.tasksBaseUrl });
|
|
2119
|
+
this.backoff = new Backoff(this.logger, "poll", this.pollIntervalMs);
|
|
1960
2120
|
}
|
|
1961
2121
|
logger;
|
|
2122
|
+
pollIntervalMs;
|
|
1962
2123
|
api;
|
|
1963
2124
|
client;
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
taskTimeoutMs;
|
|
1968
|
-
logFlushIntervalMs;
|
|
1969
|
-
cancelCheckIntervalMs;
|
|
1970
|
-
cancelGraceMs;
|
|
2125
|
+
registry;
|
|
2126
|
+
tracker;
|
|
2127
|
+
backoff;
|
|
1971
2128
|
state = "idle";
|
|
1972
2129
|
timer;
|
|
1973
2130
|
pollAbort;
|
|
1974
|
-
consecutiveFailures = 0;
|
|
1975
|
-
consecutiveAuthFailures = 0;
|
|
1976
2131
|
activeRuns = /* @__PURE__ */ new Set();
|
|
1977
|
-
reporters = /* @__PURE__ */ new Set();
|
|
1978
|
-
uninstallShutdown;
|
|
1979
2132
|
get activeCount() {
|
|
1980
2133
|
return this.activeRuns.size;
|
|
1981
2134
|
}
|
|
1982
2135
|
start() {
|
|
1983
2136
|
if (this.state === "started") return;
|
|
1984
2137
|
if (this.state === "stopping") {
|
|
1985
|
-
throw new Error("[hobots] start
|
|
1986
|
-
}
|
|
1987
|
-
if (this.registry.size === 0) {
|
|
1988
|
-
throw new Error("[hobots] nenhuma task registrada \u2014 chame register() antes de start()");
|
|
2138
|
+
throw new Error("[hobots] start do agente durante o stop \u2014 aguarde o stop terminar");
|
|
1989
2139
|
}
|
|
1990
2140
|
this.state = "started";
|
|
1991
|
-
|
|
1992
|
-
|
|
2141
|
+
this.logger.debug(`polling iniciado (tasks: ${this.registry.remoteTasks().map((task) => task.slug).join(", ")})`);
|
|
2142
|
+
this.schedule(0);
|
|
2143
|
+
}
|
|
2144
|
+
/** Uma task registrada depois do boot entra no próximo poll — que é antecipado para já. */
|
|
2145
|
+
wake() {
|
|
2146
|
+
if (this.state !== "started") return;
|
|
2147
|
+
if (this.timer) {
|
|
2148
|
+
clearTimeout(this.timer);
|
|
2149
|
+
this.timer = void 0;
|
|
1993
2150
|
}
|
|
1994
|
-
this.logger.debug(`polling iniciado (tasks: ${[...this.registry.keys()].join(", ")})`);
|
|
1995
2151
|
this.schedule(0);
|
|
1996
2152
|
}
|
|
1997
2153
|
/** Para o polling e aguarda as runs ativas (até timeoutMs). Retorna true se tudo terminou a tempo. */
|
|
@@ -2004,40 +2160,10 @@ var TaskAgent = class {
|
|
|
2004
2160
|
}
|
|
2005
2161
|
this.pollAbort?.abort();
|
|
2006
2162
|
const all = Promise.allSettled([...this.activeRuns]).then(() => true);
|
|
2007
|
-
const finished = timeoutMs > 0 ? await Promise.race([all, sleep(timeoutMs).then(() => false)]) :
|
|
2008
|
-
if (!finished && this.activeRuns.size > 0) {
|
|
2009
|
-
this.logger.warn(`${this.activeRuns.size} run(s) ainda em execu\xE7\xE3o ap\xF3s ${timeoutMs}ms \u2014 abandonando a espera`);
|
|
2010
|
-
await this.reportAgentTerminated("agente encerrado durante a execu\xE7\xE3o (shutdown excedeu o tempo de espera)");
|
|
2011
|
-
}
|
|
2012
|
-
this.uninstallShutdown?.();
|
|
2013
|
-
this.uninstallShutdown = void 0;
|
|
2163
|
+
const finished = timeoutMs > 0 ? await Promise.race([all, sleep(timeoutMs).then(() => false)]) : this.activeRuns.size === 0;
|
|
2014
2164
|
this.state = "idle";
|
|
2015
2165
|
return finished;
|
|
2016
2166
|
}
|
|
2017
|
-
/**
|
|
2018
|
-
* Best-effort do exit path: reporta 'failed'/AgentTerminated para as runs ativas
|
|
2019
|
-
* antes do processo morrer, fora da fila do reporter e com timeout curto. Runs já
|
|
2020
|
-
* canceladas no app (reporter morto) são puladas; erros de rede são engolidos —
|
|
2021
|
-
* o sweep server-side é o backstop.
|
|
2022
|
-
*/
|
|
2023
|
-
async reportAgentTerminated(message, timeoutMs = 2500) {
|
|
2024
|
-
const pending = [];
|
|
2025
|
-
for (const reporter of this.reporters) {
|
|
2026
|
-
if (reporter.isDead) continue;
|
|
2027
|
-
const logs = reporter.drainLogs();
|
|
2028
|
-
reporter.kill();
|
|
2029
|
-
pending.push(
|
|
2030
|
-
this.api.updateRunOnce(
|
|
2031
|
-
reporter.context.runId,
|
|
2032
|
-
{ status: "failed", error: { type: "AgentTerminated", message }, logs: logs.length > 0 ? logs : void 0 },
|
|
2033
|
-
timeoutMs
|
|
2034
|
-
).catch(() => {
|
|
2035
|
-
})
|
|
2036
|
-
);
|
|
2037
|
-
}
|
|
2038
|
-
if (pending.length === 0) return;
|
|
2039
|
-
await Promise.race([Promise.allSettled(pending), sleep(timeoutMs)]);
|
|
2040
|
-
}
|
|
2041
2167
|
/** Timer intencionalmente NÃO unref'd: o agente é a razão do processo existir. */
|
|
2042
2168
|
schedule(delayMs) {
|
|
2043
2169
|
if (this.state !== "started" || this.timer) return;
|
|
@@ -2046,9 +2172,22 @@ var TaskAgent = class {
|
|
|
2046
2172
|
void this.tick();
|
|
2047
2173
|
}, delayMs);
|
|
2048
2174
|
}
|
|
2175
|
+
/** Slots livres por task remota — a soma vira o `max` do poll e as tasks com slot viram o filtro. */
|
|
2176
|
+
freeSlots() {
|
|
2177
|
+
const tasks = [];
|
|
2178
|
+
let max = 0;
|
|
2179
|
+
for (const task of this.registry.remoteTasks()) {
|
|
2180
|
+
const free = task.options.concurrency - this.tracker.activeFor(task.slug);
|
|
2181
|
+
if (free <= 0) continue;
|
|
2182
|
+
tasks.push(task.slug);
|
|
2183
|
+
max += free;
|
|
2184
|
+
}
|
|
2185
|
+
return { tasks, max: Math.min(MAX_CLAIM_PER_POLL, max) };
|
|
2186
|
+
}
|
|
2049
2187
|
async tick() {
|
|
2050
2188
|
if (this.state !== "started") return;
|
|
2051
|
-
|
|
2189
|
+
const { tasks, max } = this.freeSlots();
|
|
2190
|
+
if (max === 0) {
|
|
2052
2191
|
this.schedule(this.pollIntervalMs);
|
|
2053
2192
|
return;
|
|
2054
2193
|
}
|
|
@@ -2056,221 +2195,448 @@ var TaskAgent = class {
|
|
|
2056
2195
|
try {
|
|
2057
2196
|
const response = await this.api.poll(
|
|
2058
2197
|
{
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
tasks: [...this.registry.keys()],
|
|
2066
|
-
// o servidor calcula a tolerância de offline (3×) a partir disto em
|
|
2067
|
-
// agentes poll-only — sem o campo, um poll lento flaparia offline/online.
|
|
2068
|
-
// Clampado à faixa aceita pelo schema do poll (1s..15min).
|
|
2069
|
-
poll_interval_ms: Math.min(Math.max(this.pollIntervalMs, 1e3), 9e5),
|
|
2070
|
-
// recursos da máquina — alimenta a série da instância em agentes sem heartbeat
|
|
2071
|
-
// (o servidor grava no máx. 1 ponto/30s por instância)
|
|
2072
|
-
machine: this.options.machineSampler?.sample()
|
|
2073
|
-
},
|
|
2074
|
-
max: Math.min(MAX_CONCURRENCY, this.concurrency - this.activeRuns.size),
|
|
2075
|
-
tasks: [...this.registry.keys()]
|
|
2198
|
+
// identidade canônica do agente no painel — o mesmo instanceId do heartbeat,
|
|
2199
|
+
// então os dois canais convergem em um agente. `instance.tasks` lista TODAS as
|
|
2200
|
+
// tasks (register + start); o filtro `tasks` abaixo só as que este poll pode rodar.
|
|
2201
|
+
instance: this.options.instanceInfo(),
|
|
2202
|
+
max,
|
|
2203
|
+
tasks
|
|
2076
2204
|
},
|
|
2077
2205
|
this.pollAbort.signal
|
|
2078
2206
|
);
|
|
2079
|
-
this.
|
|
2080
|
-
this.consecutiveAuthFailures = 0;
|
|
2207
|
+
this.backoff.reset();
|
|
2081
2208
|
for (const run of response.runs) {
|
|
2082
|
-
if (this.activeRuns.size >= this.concurrency) break;
|
|
2083
2209
|
this.dispatch(run);
|
|
2084
2210
|
}
|
|
2085
2211
|
this.schedule(response.runs.length > 0 ? 0 : this.pollIntervalMs);
|
|
2086
2212
|
} catch (error) {
|
|
2087
2213
|
if (this.state !== "started") return;
|
|
2088
|
-
this.schedule(this.
|
|
2214
|
+
this.schedule(this.backoff.next(error));
|
|
2089
2215
|
} finally {
|
|
2090
2216
|
this.pollAbort = void 0;
|
|
2091
2217
|
}
|
|
2092
2218
|
}
|
|
2093
|
-
delayAfterPollError(error) {
|
|
2094
|
-
if (error instanceof TasksApiError && error.status === 401) {
|
|
2095
|
-
this.consecutiveAuthFailures++;
|
|
2096
|
-
if (this.consecutiveAuthFailures === 1 || this.consecutiveAuthFailures % 10 === 0) {
|
|
2097
|
-
this.logger.error("chave rejeitada (401) \u2014 verifique o Client Secret do agente em Configura\xE7\xF5es \u2192 Agentes");
|
|
2098
|
-
}
|
|
2099
|
-
return Math.max(this.pollIntervalMs, AUTH_FAILURE_INTERVAL_MS);
|
|
2100
|
-
}
|
|
2101
|
-
this.consecutiveFailures++;
|
|
2102
|
-
const ladder = POLL_RETRY_DELAYS_MS[Math.min(this.consecutiveFailures - 1, POLL_RETRY_DELAYS_MS.length - 1)];
|
|
2103
|
-
const delay = Math.min(Math.max(jitter(ladder), 0), Math.max(this.pollIntervalMs, 16e3));
|
|
2104
|
-
this.logger.warn(`poll falhou (${String(error)}), pr\xF3xima tentativa em ${delay}ms`);
|
|
2105
|
-
return delay;
|
|
2106
|
-
}
|
|
2107
2219
|
dispatch(run) {
|
|
2108
|
-
const promise = this.
|
|
2220
|
+
const promise = this.executeClaimed(run).catch((error) => this.logger.error(`erro inesperado na run ${run.id}:`, error)).finally(() => {
|
|
2109
2221
|
this.activeRuns.delete(promise);
|
|
2110
2222
|
if (this.state === "started") this.schedule(0);
|
|
2111
2223
|
});
|
|
2112
2224
|
this.activeRuns.add(promise);
|
|
2113
2225
|
}
|
|
2114
|
-
async
|
|
2115
|
-
const
|
|
2116
|
-
|
|
2117
|
-
|
|
2226
|
+
async executeClaimed(run) {
|
|
2227
|
+
const task = this.registry.get(run.task);
|
|
2228
|
+
if (!task || task.mode !== "remote") {
|
|
2229
|
+
const tx = new Transaction(this.client, `run:${run.task}`, "hobots.task", run.id);
|
|
2230
|
+
const reporter = new RunReporter(this.api, run.id, run.task, this.logger, tx);
|
|
2231
|
+
reporter.reportFailure(toRunError(Object.assign(new Error(`tarefa n\xE3o registrada no agente: ${run.task}`), { name: "HandlerNotFound" })));
|
|
2232
|
+
await reporter.finalize();
|
|
2233
|
+
tx.setStatus("error");
|
|
2234
|
+
tx.finish();
|
|
2235
|
+
return;
|
|
2236
|
+
}
|
|
2237
|
+
await executeRun({ run, task, api: this.api, client: this.client, logger: this.logger, tracker: this.tracker });
|
|
2238
|
+
}
|
|
2239
|
+
};
|
|
2240
|
+
|
|
2241
|
+
// src/tasks/local.ts
|
|
2242
|
+
var LocalRunner = class {
|
|
2243
|
+
constructor(task, deps) {
|
|
2244
|
+
this.task = task;
|
|
2245
|
+
this.deps = deps;
|
|
2246
|
+
this.backoff = new Backoff(deps.logger, `cria\xE7\xE3o da run "${task.slug}"`);
|
|
2247
|
+
this.done = new Promise((resolve2) => {
|
|
2248
|
+
this.resolveDone = resolve2;
|
|
2249
|
+
});
|
|
2250
|
+
}
|
|
2251
|
+
abort = new AbortController();
|
|
2252
|
+
backoff;
|
|
2253
|
+
/** Resolve quando a run terminou (ou o runner foi parado antes de abri-la). */
|
|
2254
|
+
done;
|
|
2255
|
+
resolveDone;
|
|
2256
|
+
async run() {
|
|
2118
2257
|
try {
|
|
2119
|
-
const
|
|
2120
|
-
if (!
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
}
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2258
|
+
const run = await this.createRun();
|
|
2259
|
+
if (!run) return;
|
|
2260
|
+
this.deps.logger.debug(`run ${run.id} aberta para "${this.task.slug}" (modo start)`);
|
|
2261
|
+
await executeRun({
|
|
2262
|
+
run,
|
|
2263
|
+
task: this.task,
|
|
2264
|
+
api: this.deps.api,
|
|
2265
|
+
client: this.deps.client,
|
|
2266
|
+
logger: this.deps.logger,
|
|
2267
|
+
tracker: this.deps.tracker
|
|
2268
|
+
});
|
|
2269
|
+
} catch (error) {
|
|
2270
|
+
this.deps.logger.error(`erro inesperado na task "${this.task.slug}":`, error);
|
|
2271
|
+
} finally {
|
|
2272
|
+
this.resolveDone();
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
/** Desiste de abrir a run (close() antes da plataforma responder). Uma run já aberta segue pelo tracker. */
|
|
2276
|
+
stop() {
|
|
2277
|
+
this.abort.abort();
|
|
2278
|
+
}
|
|
2279
|
+
async createRun() {
|
|
2280
|
+
for (; ; ) {
|
|
2281
|
+
if (this.abort.signal.aborted) return void 0;
|
|
2138
2282
|
try {
|
|
2139
|
-
await
|
|
2140
|
-
{ runId: run.id, tx, attach: (image, options) => reporter.attach(image, options) },
|
|
2141
|
-
() => this.invokeWithTimeout(handler, run, reporter)
|
|
2142
|
-
);
|
|
2143
|
-
await reporter.finalizeAttachments();
|
|
2144
|
-
await reporter.finalizeItems();
|
|
2145
|
-
tx.setStatus("ok");
|
|
2146
|
-
tx.finish();
|
|
2147
|
-
reporter.reportSuccess();
|
|
2148
|
-
this.logger.debug(`run ${run.id} conclu\xEDda`);
|
|
2283
|
+
return await this.deps.api.createRun({ task: this.task.slug, instance: this.deps.instanceInfo() }, this.abort.signal);
|
|
2149
2284
|
} catch (error) {
|
|
2150
|
-
|
|
2151
|
-
await
|
|
2152
|
-
tx.setStatus("error");
|
|
2153
|
-
tx.finish();
|
|
2154
|
-
reporter.reportFailure(toRunError(error));
|
|
2155
|
-
this.logger.debug(`run ${run.id} falhou: ${String(error)}`);
|
|
2285
|
+
if (this.abort.signal.aborted) return void 0;
|
|
2286
|
+
await abortableSleep(this.backoff.next(error), this.abort.signal);
|
|
2156
2287
|
}
|
|
2157
|
-
await reporter.finalize();
|
|
2158
|
-
} finally {
|
|
2159
|
-
this.reporters.delete(reporter);
|
|
2160
2288
|
}
|
|
2161
2289
|
}
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
if (this.
|
|
2169
|
-
|
|
2290
|
+
};
|
|
2291
|
+
|
|
2292
|
+
// src/tasks/registry.ts
|
|
2293
|
+
var TaskRegistry = class {
|
|
2294
|
+
tasks = /* @__PURE__ */ new Map();
|
|
2295
|
+
add(task) {
|
|
2296
|
+
if (this.tasks.has(task.slug)) {
|
|
2297
|
+
throw new Error(`[hobots] task "${task.slug}" j\xE1 registrada \u2014 cada slug s\xF3 pode ter um handler (register ou start)`);
|
|
2170
2298
|
}
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2299
|
+
this.tasks.set(task.slug, task);
|
|
2300
|
+
}
|
|
2301
|
+
get(slug) {
|
|
2302
|
+
return this.tasks.get(slug);
|
|
2303
|
+
}
|
|
2304
|
+
get size() {
|
|
2305
|
+
return this.tasks.size;
|
|
2306
|
+
}
|
|
2307
|
+
/** Tasks que chegam pelo poll (register). */
|
|
2308
|
+
remoteTasks() {
|
|
2309
|
+
return [...this.tasks.values()].filter((task) => task.mode === "remote");
|
|
2310
|
+
}
|
|
2311
|
+
/** Tasks abertas pelo próprio bot (start). */
|
|
2312
|
+
localTasks() {
|
|
2313
|
+
return [...this.tasks.values()].filter((task) => task.mode === "local");
|
|
2314
|
+
}
|
|
2315
|
+
/** Todos os slugs, para a presença da instância (`instance.tasks`). */
|
|
2316
|
+
allSlugs() {
|
|
2317
|
+
return [...this.tasks.keys()];
|
|
2318
|
+
}
|
|
2319
|
+
clear() {
|
|
2320
|
+
this.tasks.clear();
|
|
2321
|
+
}
|
|
2322
|
+
};
|
|
2323
|
+
var RunTracker = class {
|
|
2324
|
+
constructor(api) {
|
|
2325
|
+
this.api = api;
|
|
2326
|
+
}
|
|
2327
|
+
reporters = /* @__PURE__ */ new Set();
|
|
2328
|
+
waiters = /* @__PURE__ */ new Set();
|
|
2329
|
+
add(reporter) {
|
|
2330
|
+
this.reporters.add(reporter);
|
|
2331
|
+
}
|
|
2332
|
+
remove(reporter) {
|
|
2333
|
+
this.reporters.delete(reporter);
|
|
2334
|
+
if (this.reporters.size === 0) {
|
|
2335
|
+
for (const resolve2 of this.waiters) resolve2();
|
|
2336
|
+
this.waiters.clear();
|
|
2174
2337
|
}
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2338
|
+
}
|
|
2339
|
+
get activeCount() {
|
|
2340
|
+
return this.reporters.size;
|
|
2341
|
+
}
|
|
2342
|
+
/** Runs ativas de uma task (concorrência por task no poll). */
|
|
2343
|
+
activeFor(slug) {
|
|
2344
|
+
let count = 0;
|
|
2345
|
+
for (const reporter of this.reporters) if (reporter.context.task === slug) count++;
|
|
2346
|
+
return count;
|
|
2347
|
+
}
|
|
2348
|
+
/** A única run ativa, quando há exatamente uma — o que `hobots.*` usa fora de um contexto async. */
|
|
2349
|
+
singleActive() {
|
|
2350
|
+
if (this.reporters.size !== 1) return void 0;
|
|
2351
|
+
return this.reporters.values().next().value;
|
|
2352
|
+
}
|
|
2353
|
+
/** Resolve quando não houver mais runs ativas (ou após timeoutMs, devolvendo false). */
|
|
2354
|
+
async waitIdle(timeoutMs) {
|
|
2355
|
+
if (this.reporters.size === 0) return true;
|
|
2356
|
+
const idle = new Promise((resolve2) => this.waiters.add(() => resolve2(true)));
|
|
2357
|
+
if (timeoutMs <= 0) return false;
|
|
2358
|
+
return Promise.race([idle, sleep(timeoutMs).then(() => false)]);
|
|
2359
|
+
}
|
|
2360
|
+
/**
|
|
2361
|
+
* Best-effort do exit path: reporta 'failed'/AgentTerminated para as runs ativas
|
|
2362
|
+
* antes do processo morrer, fora da fila do reporter e com timeout curto. Runs já
|
|
2363
|
+
* canceladas no app (reporter morto) são puladas; erros de rede são engolidos —
|
|
2364
|
+
* o sweep server-side é o backstop.
|
|
2365
|
+
*/
|
|
2366
|
+
async reportAgentTerminated(message, timeoutMs = 2500) {
|
|
2367
|
+
const pending = [];
|
|
2368
|
+
for (const reporter of this.reporters) {
|
|
2369
|
+
if (reporter.isDead) continue;
|
|
2370
|
+
const logs = reporter.drainLogs();
|
|
2371
|
+
reporter.kill();
|
|
2372
|
+
pending.push(
|
|
2373
|
+
this.api.updateRunOnce(
|
|
2374
|
+
reporter.context.runId,
|
|
2375
|
+
{ status: "failed", error: { type: "AgentTerminated", message }, logs: logs.length > 0 ? logs : void 0 },
|
|
2376
|
+
timeoutMs
|
|
2377
|
+
).catch(() => {
|
|
2378
|
+
})
|
|
2180
2379
|
);
|
|
2181
|
-
return;
|
|
2182
2380
|
}
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
invocation.then(() => this.logger.warn(`run ${run.id} terminou depois do timeout de ${timeoutMs}ms`)).catch(() => this.logger.warn(`run ${run.id} falhou depois do timeout de ${timeoutMs}ms (erro descartado)`));
|
|
2186
|
-
throw Object.assign(new Error(`timeout ap\xF3s ${timeoutMs}ms (o handler pode continuar em background)`), {
|
|
2187
|
-
name: "TaskTimeout"
|
|
2188
|
-
});
|
|
2381
|
+
if (pending.length === 0) return;
|
|
2382
|
+
await Promise.race([Promise.allSettled(pending), sleep(timeoutMs)]);
|
|
2189
2383
|
}
|
|
2190
2384
|
};
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2385
|
+
|
|
2386
|
+
// src/tasks/shutdown.ts
|
|
2387
|
+
var WATCHED_SIGNALS = ["SIGINT", "SIGTERM"];
|
|
2388
|
+
function installShutdownHandlers(agent) {
|
|
2389
|
+
if (!isNode()) return () => {
|
|
2390
|
+
};
|
|
2391
|
+
let signaled = false;
|
|
2392
|
+
const signalHandlers = /* @__PURE__ */ new Map();
|
|
2393
|
+
for (const signal of WATCHED_SIGNALS) {
|
|
2394
|
+
const handler = markInternal(() => {
|
|
2395
|
+
if (externalListenerCount(signal) > 0) return;
|
|
2396
|
+
if (signaled) return;
|
|
2397
|
+
signaled = true;
|
|
2398
|
+
const reraise = () => {
|
|
2399
|
+
process.removeListener(signal, handler);
|
|
2400
|
+
process.kill(process.pid, signal);
|
|
2401
|
+
};
|
|
2402
|
+
if (agent.activeCount === 0) {
|
|
2403
|
+
reraise();
|
|
2404
|
+
return;
|
|
2405
|
+
}
|
|
2406
|
+
const report = agent.reportAgentTerminated(`o processo do agente recebeu ${signal} durante a execu\xE7\xE3o`);
|
|
2407
|
+
addEmergencyTask(report);
|
|
2408
|
+
void report.finally(reraise);
|
|
2409
|
+
});
|
|
2410
|
+
process.on(signal, handler);
|
|
2411
|
+
signalHandlers.set(signal, handler);
|
|
2412
|
+
}
|
|
2413
|
+
const onFatal = markInternal((error) => {
|
|
2414
|
+
const report = agent.activeCount > 0 ? agent.reportAgentTerminated("o processo do agente morreu com um erro n\xE3o tratado durante a execu\xE7\xE3o") : Promise.resolve();
|
|
2415
|
+
addEmergencyTask(report);
|
|
2416
|
+
if (!hasInstalledGlobalHandlers() && externalListenerCount("uncaughtException") === 0) {
|
|
2417
|
+
void report.finally(() => {
|
|
2418
|
+
console.error(error);
|
|
2419
|
+
process.exit(1);
|
|
2420
|
+
});
|
|
2198
2421
|
}
|
|
2199
|
-
signal.addEventListener("abort", () => setTimeout(resolve, graceMs), { once: true });
|
|
2200
2422
|
});
|
|
2423
|
+
process.on("uncaughtException", onFatal);
|
|
2424
|
+
return () => {
|
|
2425
|
+
for (const [signal, handler] of signalHandlers) process.removeListener(signal, handler);
|
|
2426
|
+
process.removeListener("uncaughtException", onFatal);
|
|
2427
|
+
};
|
|
2201
2428
|
}
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2429
|
+
|
|
2430
|
+
// src/runtime.ts
|
|
2431
|
+
var HEARTBEAT_INTERVAL_MS = 3e4;
|
|
2432
|
+
var DEFAULT_RETRIES2 = 1;
|
|
2433
|
+
var DEFAULT_RETRY_DELAY_MS2 = 5e3;
|
|
2434
|
+
var DEFAULT_CANCEL_GRACE_MS = 1e4;
|
|
2435
|
+
var MAX_CONCURRENCY = 5;
|
|
2436
|
+
var MAX_SLUG_CHARS = 255;
|
|
2437
|
+
var PROCESS_LEVEL_KEYS = [
|
|
2438
|
+
"clientSecret",
|
|
2439
|
+
"instanceId",
|
|
2440
|
+
"environment",
|
|
2441
|
+
"release",
|
|
2442
|
+
"tags",
|
|
2443
|
+
"debug",
|
|
2444
|
+
"machineMetrics",
|
|
2445
|
+
"captureUnhandled",
|
|
2446
|
+
"installExitHandlers"
|
|
2447
|
+
];
|
|
2448
|
+
var runtime;
|
|
2449
|
+
var warnedNoRuntime = false;
|
|
2450
|
+
function hostname() {
|
|
2451
|
+
if (!isNode()) return void 0;
|
|
2452
|
+
return process.env.HOSTNAME || process.env.COMPUTERNAME || void 0;
|
|
2453
|
+
}
|
|
2454
|
+
function sameValue(a, b) {
|
|
2455
|
+
if (a === b) return true;
|
|
2456
|
+
if (a && b && typeof a === "object" && typeof b === "object") return JSON.stringify(a) === JSON.stringify(b);
|
|
2457
|
+
return false;
|
|
2458
|
+
}
|
|
2459
|
+
function ensureRuntime(options) {
|
|
2460
|
+
if (runtime) {
|
|
2461
|
+
for (const key of PROCESS_LEVEL_KEYS) {
|
|
2462
|
+
if (options[key] !== void 0 && !sameValue(options[key], runtime.options[key])) {
|
|
2463
|
+
runtime.logger.warn(
|
|
2464
|
+
`op\xE7\xE3o "${key}" divergente ignorada \u2014 o primeiro register()/start() define as op\xE7\xF5es do processo`
|
|
2465
|
+
);
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
return runtime;
|
|
2469
|
+
}
|
|
2470
|
+
const config = resolveConfig({ clientSecret: options.clientSecret });
|
|
2471
|
+
const logger = makeLogger(options.debug ?? false);
|
|
2472
|
+
const client = new Client(
|
|
2473
|
+
{ environment: options.environment, release: options.release, tags: options.tags, debug: options.debug },
|
|
2474
|
+
config
|
|
2475
|
+
);
|
|
2476
|
+
const api = new TasksApi({ baseUrl: config.tasksBaseUrl, key: config.key, logger });
|
|
2477
|
+
runtime = {
|
|
2478
|
+
options: { ...options, clientSecret: void 0 },
|
|
2479
|
+
config,
|
|
2480
|
+
client,
|
|
2481
|
+
logger,
|
|
2482
|
+
api,
|
|
2483
|
+
registry: new TaskRegistry(),
|
|
2484
|
+
tracker: new RunTracker(api),
|
|
2485
|
+
sampler: void 0,
|
|
2486
|
+
heartbeat: void 0,
|
|
2487
|
+
agent: void 0,
|
|
2488
|
+
locals: /* @__PURE__ */ new Map(),
|
|
2489
|
+
instanceId: resolveInstanceId(options.instanceId),
|
|
2490
|
+
pollIntervalMs: void 0,
|
|
2491
|
+
booted: false,
|
|
2492
|
+
bootScheduled: false,
|
|
2493
|
+
bootPromise: void 0,
|
|
2494
|
+
uninstallGlobalHandlers: options.captureUnhandled !== false ? installGlobalHandlers(client) : void 0,
|
|
2495
|
+
uninstallShutdown: void 0
|
|
2496
|
+
};
|
|
2497
|
+
runtime.options.clientSecret = options.clientSecret;
|
|
2498
|
+
logger.debug("inicializado", { endpoint: config.ingestEndpoint });
|
|
2499
|
+
return runtime;
|
|
2500
|
+
}
|
|
2501
|
+
function validateTask(slug, handler, fn) {
|
|
2502
|
+
if (typeof slug !== "string" || slug.trim().length === 0 || slug.length > MAX_SLUG_CHARS) {
|
|
2503
|
+
throw new Error(`[hobots] ${fn}(): o slug da task precisa ser uma string de 1 a ${MAX_SLUG_CHARS} caracteres`);
|
|
2211
2504
|
}
|
|
2212
|
-
|
|
2505
|
+
if (typeof handler !== "function") {
|
|
2506
|
+
throw new Error(`[hobots] ${fn}('${slug}'): o handler precisa ser uma fun\xE7\xE3o`);
|
|
2507
|
+
}
|
|
2508
|
+
return slug.trim();
|
|
2213
2509
|
}
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2510
|
+
function clampConcurrency(value) {
|
|
2511
|
+
if (value === void 0 || !Number.isFinite(value)) return 1;
|
|
2512
|
+
return Math.min(MAX_CONCURRENCY, Math.max(1, Math.floor(value)));
|
|
2513
|
+
}
|
|
2514
|
+
function resolveTaskOptions(options, mode) {
|
|
2515
|
+
const retries = options.retries === void 0 || !Number.isFinite(options.retries) ? DEFAULT_RETRIES2 : Math.max(0, Math.floor(options.retries));
|
|
2516
|
+
return {
|
|
2517
|
+
retries,
|
|
2518
|
+
retryDelayMs: options.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS2,
|
|
2519
|
+
timeoutMs: options.timeoutMs && options.timeoutMs > 0 ? options.timeoutMs : void 0,
|
|
2520
|
+
concurrency: mode === "remote" ? clampConcurrency(options.concurrency) : 1,
|
|
2521
|
+
logFlushIntervalMs: options.logFlushIntervalMs,
|
|
2522
|
+
cancelCheckIntervalMs: options.cancelCheckIntervalMs,
|
|
2523
|
+
cancelGraceMs: options.cancelGraceMs ?? DEFAULT_CANCEL_GRACE_MS
|
|
2524
|
+
};
|
|
2525
|
+
}
|
|
2526
|
+
function instanceInfo(rt) {
|
|
2527
|
+
return {
|
|
2528
|
+
instance_id: rt.instanceId ?? "unknown",
|
|
2529
|
+
version: SDK_VERSION,
|
|
2530
|
+
host: hostname(),
|
|
2531
|
+
// ambiente da instância: junto do heartbeat, é como o servidor sabe onde o robô roda
|
|
2532
|
+
environment: rt.options.environment,
|
|
2533
|
+
// TODAS as tasks (register + start) — o painel mostra o que o agente sabe fazer
|
|
2534
|
+
tasks: rt.registry.allSlugs(),
|
|
2535
|
+
// o servidor calcula a tolerância de offline (3×) a partir disto em agentes que fazem
|
|
2536
|
+
// poll; clampado à faixa aceita pelo schema (1s..15min)
|
|
2537
|
+
poll_interval_ms: rt.agent ? Math.min(Math.max(rt.agent.pollIntervalMs, 1e3), 9e5) : void 0,
|
|
2538
|
+
// recursos da máquina — alimenta a série da instância (o servidor grava no máx. 1 ponto/30s)
|
|
2539
|
+
machine: rt.sampler?.sample()
|
|
2540
|
+
};
|
|
2541
|
+
}
|
|
2542
|
+
function ensureAgent(rt) {
|
|
2543
|
+
rt.agent ??= new TaskAgent({
|
|
2544
|
+
api: rt.api,
|
|
2545
|
+
client: rt.client,
|
|
2546
|
+
logger: rt.logger,
|
|
2547
|
+
registry: rt.registry,
|
|
2548
|
+
tracker: rt.tracker,
|
|
2549
|
+
instanceInfo: () => instanceInfo(rt),
|
|
2550
|
+
pollIntervalMs: rt.pollIntervalMs
|
|
2551
|
+
});
|
|
2552
|
+
return rt.agent;
|
|
2553
|
+
}
|
|
2554
|
+
function launchLocal(rt, task) {
|
|
2555
|
+
const runner = new LocalRunner(task, {
|
|
2556
|
+
api: rt.api,
|
|
2557
|
+
client: rt.client,
|
|
2558
|
+
logger: rt.logger,
|
|
2559
|
+
tracker: rt.tracker,
|
|
2560
|
+
instanceInfo: () => instanceInfo(rt)
|
|
2561
|
+
});
|
|
2562
|
+
rt.locals.set(task.slug, runner);
|
|
2563
|
+
void runner.run();
|
|
2564
|
+
}
|
|
2565
|
+
async function boot(rt) {
|
|
2566
|
+
if (!rt.instanceId) {
|
|
2567
|
+
const id = await fallbackInstanceId();
|
|
2568
|
+
if (runtime !== rt) return;
|
|
2569
|
+
rt.instanceId = id;
|
|
2570
|
+
rt.logger.debug(`instanceId n\xE3o informado (env ${INSTANCE_ID_ENV}) \u2014 usando o hostname "${id}"`);
|
|
2571
|
+
}
|
|
2572
|
+
if (runtime !== rt) return;
|
|
2573
|
+
rt.booted = true;
|
|
2574
|
+
if (rt.options.machineMetrics !== false) {
|
|
2575
|
+
rt.sampler = new MachineSampler(rt.instanceId, rt.logger);
|
|
2576
|
+
void rt.sampler.start();
|
|
2577
|
+
rt.client.attachMachineSampler(rt.sampler);
|
|
2578
|
+
}
|
|
2579
|
+
rt.heartbeat = new Heartbeat(rt.client, { intervalMs: HEARTBEAT_INTERVAL_MS, keepAlive: true }, rt.instanceId, rt.sampler);
|
|
2580
|
+
rt.heartbeat.start();
|
|
2581
|
+
if (rt.options.installExitHandlers !== false) {
|
|
2582
|
+
rt.uninstallShutdown = installShutdownHandlers(rt.tracker);
|
|
2583
|
+
}
|
|
2584
|
+
if (rt.registry.remoteTasks().length > 0) ensureAgent(rt).start();
|
|
2585
|
+
for (const task of rt.registry.localTasks()) launchLocal(rt, task);
|
|
2586
|
+
}
|
|
2587
|
+
function scheduleBoot(rt) {
|
|
2588
|
+
if (rt.bootScheduled) return;
|
|
2589
|
+
rt.bootScheduled = true;
|
|
2590
|
+
queueMicrotask(() => {
|
|
2591
|
+
rt.bootPromise = boot(rt).catch((error) => rt.logger.error("falha ao iniciar o SDK:", error));
|
|
2592
|
+
});
|
|
2593
|
+
}
|
|
2594
|
+
function addTask(fn, mode, task, handler, options) {
|
|
2595
|
+
const slug = validateTask(task, handler, fn);
|
|
2596
|
+
const rt = ensureRuntime(options);
|
|
2597
|
+
if (mode === "remote" && options.pollIntervalMs !== void 0) {
|
|
2598
|
+
if (rt.pollIntervalMs === void 0 && !rt.agent) rt.pollIntervalMs = options.pollIntervalMs;
|
|
2599
|
+
else if (rt.pollIntervalMs !== options.pollIntervalMs) {
|
|
2600
|
+
rt.logger.warn('op\xE7\xE3o "pollIntervalMs" divergente ignorada \u2014 o primeiro register() define o intervalo do poll');
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
const registered = { slug, mode, handler, options: resolveTaskOptions(options, mode) };
|
|
2604
|
+
rt.registry.add(registered);
|
|
2605
|
+
if (!rt.booted) {
|
|
2606
|
+
scheduleBoot(rt);
|
|
2607
|
+
return;
|
|
2608
|
+
}
|
|
2609
|
+
if (mode === "remote") {
|
|
2610
|
+
const agent = ensureAgent(rt);
|
|
2611
|
+
agent.start();
|
|
2612
|
+
agent.wake();
|
|
2613
|
+
} else {
|
|
2614
|
+
launchLocal(rt, registered);
|
|
2257
2615
|
}
|
|
2258
|
-
client.logger.debug("inicializado", { endpoint: client.config.ingestEndpoint });
|
|
2259
2616
|
}
|
|
2260
|
-
function
|
|
2261
|
-
|
|
2617
|
+
function register(task, handler, options = {}) {
|
|
2618
|
+
addTask("register", "remote", task, handler, options);
|
|
2619
|
+
}
|
|
2620
|
+
function start(task, handler, options = {}) {
|
|
2621
|
+
addTask("start", "local", task, handler, options);
|
|
2622
|
+
}
|
|
2623
|
+
function currentReporter() {
|
|
2624
|
+
return currentRunReporter() ?? runtime?.tracker.singleActive();
|
|
2625
|
+
}
|
|
2626
|
+
function activeLogger() {
|
|
2627
|
+
return runtime?.logger;
|
|
2262
2628
|
}
|
|
2263
2629
|
function toRunLogLevel(type) {
|
|
2264
2630
|
return type === "fatal" ? "error" : type;
|
|
2265
2631
|
}
|
|
2266
|
-
function forwardAttachment(attachment, message, type) {
|
|
2267
|
-
const
|
|
2268
|
-
if (!
|
|
2269
|
-
|
|
2632
|
+
function forwardAttachment(rt, attachment, message, type) {
|
|
2633
|
+
const reporter = currentReporter();
|
|
2634
|
+
if (!reporter) {
|
|
2635
|
+
rt.logger.warn("attachment descartado \u2014 s\xF3 \xE9 suportado dentro de uma execu\xE7\xE3o de task");
|
|
2270
2636
|
return;
|
|
2271
2637
|
}
|
|
2272
2638
|
const withMeta = typeof attachment === "object" && "image" in attachment ? attachment : { image: attachment };
|
|
2273
|
-
void attach(withMeta.image, {
|
|
2639
|
+
void reporter.attach(withMeta.image, {
|
|
2274
2640
|
message,
|
|
2275
2641
|
level: toRunLogLevel(type),
|
|
2276
2642
|
caption: withMeta.caption,
|
|
@@ -2278,109 +2644,124 @@ function forwardAttachment(attachment, message, type) {
|
|
|
2278
2644
|
});
|
|
2279
2645
|
}
|
|
2280
2646
|
function captureException(error, hint) {
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2647
|
+
const rt = runtime;
|
|
2648
|
+
if (!rt) {
|
|
2649
|
+
if (!warnedNoRuntime) {
|
|
2650
|
+
warnedNoRuntime = true;
|
|
2651
|
+
console.warn("[hobots] captureException antes de registrar uma task (register/start) \u2014 evento descartado");
|
|
2652
|
+
}
|
|
2653
|
+
return void 0;
|
|
2284
2654
|
}
|
|
2285
|
-
return client?.captureException(error, hint, scope);
|
|
2286
|
-
}
|
|
2287
|
-
function captureMessage(message, type = "warning", hint) {
|
|
2288
2655
|
if (hint?.attachment) {
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
function withScope(callback) {
|
|
2312
|
-
const previous = scope;
|
|
2313
|
-
scope = previous.clone();
|
|
2314
|
-
try {
|
|
2315
|
-
return callback(scope);
|
|
2316
|
-
} finally {
|
|
2317
|
-
scope = previous;
|
|
2318
|
-
}
|
|
2319
|
-
}
|
|
2320
|
-
function startTransaction(name, op) {
|
|
2321
|
-
const active = currentRunTransaction();
|
|
2322
|
-
if (active) {
|
|
2323
|
-
client?.logger.debug(`startTransaction('${name}') dentro de uma execu\xE7\xE3o \u2014 reutilizando a transaction da run`);
|
|
2324
|
-
return active;
|
|
2325
|
-
}
|
|
2326
|
-
const rate = client?.options.tracesSampleRate ?? 1;
|
|
2327
|
-
const sampled = rate >= 1 || Math.random() < rate;
|
|
2328
|
-
return new Transaction(client, name, op, sampled);
|
|
2329
|
-
}
|
|
2330
|
-
function flush(timeoutMs) {
|
|
2331
|
-
return client ? client.flush(timeoutMs) : Promise.resolve(true);
|
|
2332
|
-
}
|
|
2333
|
-
function register(task, handler) {
|
|
2334
|
-
if (registry.has(task)) {
|
|
2335
|
-
(agent?.logger ?? client?.logger)?.warn(`register('${task}') sobrescreveu um handler existente`);
|
|
2336
|
-
}
|
|
2337
|
-
if (client && !agent && !warnedTasksDisabled) {
|
|
2338
|
-
warnedTasksDisabled = true;
|
|
2339
|
-
client.logger.warn(
|
|
2340
|
-
`register('${task}') com tasks desabilitadas \u2014 passe tasks: true (ou op\xE7\xF5es) no init()`
|
|
2341
|
-
);
|
|
2342
|
-
}
|
|
2343
|
-
registry.set(task, handler);
|
|
2344
|
-
}
|
|
2345
|
-
function start() {
|
|
2346
|
-
if (!agent) {
|
|
2347
|
-
throw new Error("[hobots] tasks n\xE3o configuradas \u2014 passe tasks: true (ou op\xE7\xF5es) no init() antes de start()");
|
|
2348
|
-
}
|
|
2349
|
-
agent.start();
|
|
2350
|
-
}
|
|
2351
|
-
function stop(timeoutMs) {
|
|
2352
|
-
return agent ? agent.stop(timeoutMs) : Promise.resolve(true);
|
|
2656
|
+
const message = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
|
2657
|
+
forwardAttachment(rt, hint.attachment, message, hint.type ?? "error");
|
|
2658
|
+
}
|
|
2659
|
+
return rt.client.captureException(error, hint);
|
|
2660
|
+
}
|
|
2661
|
+
async function close(timeoutMs = 3e4) {
|
|
2662
|
+
const rt = runtime;
|
|
2663
|
+
if (!rt) return true;
|
|
2664
|
+
runtime = void 0;
|
|
2665
|
+
await rt.bootPromise;
|
|
2666
|
+
if (rt.agent) await rt.agent.stop(0);
|
|
2667
|
+
for (const local of rt.locals.values()) local.stop();
|
|
2668
|
+
const idle = await rt.tracker.waitIdle(timeoutMs);
|
|
2669
|
+
if (!idle) {
|
|
2670
|
+
rt.logger.warn(`${rt.tracker.activeCount} run(s) ainda em execu\xE7\xE3o ap\xF3s ${timeoutMs}ms \u2014 abandonando a espera`);
|
|
2671
|
+
await rt.tracker.reportAgentTerminated("agente encerrado durante a execu\xE7\xE3o (shutdown excedeu o tempo de espera)");
|
|
2672
|
+
}
|
|
2673
|
+
rt.heartbeat?.stop();
|
|
2674
|
+
rt.uninstallShutdown?.();
|
|
2675
|
+
rt.uninstallGlobalHandlers?.();
|
|
2676
|
+
const flushed = await rt.client.close(timeoutMs);
|
|
2677
|
+
return idle && flushed;
|
|
2353
2678
|
}
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2679
|
+
|
|
2680
|
+
// src/hobots.ts
|
|
2681
|
+
var warned = /* @__PURE__ */ new Set();
|
|
2682
|
+
function warnOutsideRun(member) {
|
|
2683
|
+
if (warned.has(member)) return;
|
|
2684
|
+
warned.add(member);
|
|
2685
|
+
const message = `hobots.${member} usado fora de uma execu\xE7\xE3o de task \u2014 ignorado (registre a task com register()/start() e use dentro do handler)`;
|
|
2686
|
+
const logger = activeLogger();
|
|
2687
|
+
if (logger) logger.warn(message);
|
|
2688
|
+
else console.warn(`[hobots] ${message}`);
|
|
2689
|
+
}
|
|
2690
|
+
var FALLBACK_TASK = "hobots";
|
|
2691
|
+
var fallbackLog = {
|
|
2692
|
+
debug: (message) => printRunLog(FALLBACK_TASK, "debug", message),
|
|
2693
|
+
info: (message) => printRunLog(FALLBACK_TASK, "info", message),
|
|
2694
|
+
warn: (message) => printRunLog(FALLBACK_TASK, "warning", message),
|
|
2695
|
+
error: (message) => printRunLog(FALLBACK_TASK, "error", message),
|
|
2696
|
+
success: (message) => printRunLog(FALLBACK_TASK, "success", message)
|
|
2697
|
+
};
|
|
2698
|
+
var noop = () => void 0;
|
|
2699
|
+
var fallbackItems = { add: noop, succeeded: noop, failed: noop, occurrence: noop };
|
|
2700
|
+
var fallbackProgress = { total: noop, advance: noop, set: noop, current: 0, snapshot: null };
|
|
2701
|
+
var neverAborts = new AbortController().signal;
|
|
2702
|
+
var detachedTransaction;
|
|
2703
|
+
function resolve(member) {
|
|
2704
|
+
const reporter = currentReporter();
|
|
2705
|
+
if (reporter) return reporter.context;
|
|
2706
|
+
warnOutsideRun(member);
|
|
2707
|
+
return void 0;
|
|
2360
2708
|
}
|
|
2709
|
+
function getter(member, fallback) {
|
|
2710
|
+
return { enumerable: true, get: () => resolve(member)?.[member] ?? fallback() };
|
|
2711
|
+
}
|
|
2712
|
+
function method(value) {
|
|
2713
|
+
return { enumerable: true, writable: false, value };
|
|
2714
|
+
}
|
|
2715
|
+
var hobots = Object.defineProperties({}, {
|
|
2716
|
+
// ── contexto da run atual (getters) ─────────────────────────────────────────
|
|
2717
|
+
runId: getter("runId", () => ""),
|
|
2718
|
+
task: getter("task", () => ""),
|
|
2719
|
+
params: getter("params", () => ({})),
|
|
2720
|
+
log: getter("log", () => fallbackLog),
|
|
2721
|
+
items: getter("items", () => fallbackItems),
|
|
2722
|
+
progress: getter("progress", () => fallbackProgress),
|
|
2723
|
+
transaction: getter("transaction", () => detachedTransaction ??= new Transaction(void 0, "hobots.detached")),
|
|
2724
|
+
signal: getter("signal", () => neverAborts),
|
|
2725
|
+
throwIfCanceled: method(() => resolve("throwIfCanceled")?.throwIfCanceled()),
|
|
2726
|
+
sleep: method((ms) => resolve("sleep")?.sleep(ms) ?? sleep(ms)),
|
|
2727
|
+
attach: method(
|
|
2728
|
+
(image, options) => resolve("attach")?.attach(image, options) ?? Promise.resolve({ id: "", ok: false, reason: "canceled" })
|
|
2729
|
+
),
|
|
2730
|
+
retry: method((fn, options) => {
|
|
2731
|
+
const reporter = currentReporter();
|
|
2732
|
+
return reporter ? reporter.context.retry(fn, options) : retryWithBackoff(fn, options);
|
|
2733
|
+
}),
|
|
2734
|
+
// ── processo ────────────────────────────────────────────────────────────────
|
|
2735
|
+
register: method((task, handler, options) => {
|
|
2736
|
+
register(task, handler, options);
|
|
2737
|
+
installGlobalHobots();
|
|
2738
|
+
}),
|
|
2739
|
+
start: method((task, handler, options) => {
|
|
2740
|
+
start(task, handler, options);
|
|
2741
|
+
installGlobalHobots();
|
|
2742
|
+
}),
|
|
2743
|
+
close: method((timeoutMs) => close(timeoutMs)),
|
|
2744
|
+
captureException: method((error, hint) => captureException(error, hint))
|
|
2745
|
+
});
|
|
2746
|
+
function installGlobalHobots() {
|
|
2747
|
+
if (globalThis.hobots === hobots) return;
|
|
2748
|
+
Object.defineProperty(globalThis, "hobots", { value: hobots, configurable: true, writable: true, enumerable: false });
|
|
2749
|
+
}
|
|
2750
|
+
var register2 = hobots.register;
|
|
2751
|
+
var start2 = hobots.start;
|
|
2752
|
+
var close2 = hobots.close;
|
|
2753
|
+
var captureException2 = hobots.captureException;
|
|
2754
|
+
var retry = hobots.retry;
|
|
2755
|
+
|
|
2756
|
+
// src/index.ts
|
|
2757
|
+
var src_default = hobots;
|
|
2361
2758
|
export {
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
captureMessage,
|
|
2370
|
-
close,
|
|
2371
|
-
flush,
|
|
2372
|
-
getClient,
|
|
2373
|
-
init,
|
|
2374
|
-
register,
|
|
2375
|
-
scrubSecrets,
|
|
2376
|
-
setContext,
|
|
2377
|
-
setExtra,
|
|
2378
|
-
setTag,
|
|
2379
|
-
setTags,
|
|
2380
|
-
setUser,
|
|
2381
|
-
start,
|
|
2382
|
-
startTransaction,
|
|
2383
|
-
stop,
|
|
2384
|
-
withScope
|
|
2759
|
+
captureException2 as captureException,
|
|
2760
|
+
close2 as close,
|
|
2761
|
+
src_default as default,
|
|
2762
|
+
hobots,
|
|
2763
|
+
register2 as register,
|
|
2764
|
+
retry,
|
|
2765
|
+
start2 as start
|
|
2385
2766
|
};
|
|
2386
2767
|
//# sourceMappingURL=index.js.map
|