hobots 1.0.0 → 1.1.1
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 +128 -84
- package/dist/index.cjs +478 -39
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +144 -19
- package/dist/index.d.ts +144 -19
- package/dist/index.js +522 -33
- package/dist/index.js.map +1 -1
- package/dist/manager/protocol.cjs.map +1 -1
- package/dist/manager/protocol.d.cts +3 -3
- package/dist/manager/protocol.d.ts +3 -3
- package/dist/protocol.cjs.map +1 -1
- package/dist/protocol.d.cts +6 -3
- package/dist/protocol.d.ts +6 -3
- package/docs/README.md +24 -17
- package/docs/api-reference.md +32 -20
- package/docs/getting-started.md +46 -29
- package/docs/manager.md +30 -25
- package/docs/monitor.md +28 -26
- package/docs/protocol.md +26 -12
- package/package.json +11 -16
- package/dist/chunk-FW32OR7V.js +0 -75
- package/dist/chunk-FW32OR7V.js.map +0 -1
- package/dist/manager.cjs +0 -519
- package/dist/manager.cjs.map +0 -1
- package/dist/manager.d.cts +0 -118
- package/dist/manager.d.ts +0 -118
- package/dist/manager.js +0 -464
- package/dist/manager.js.map +0 -1
- package/dist/utils-LReLqjzN.d.cts +0 -7
- package/dist/utils-LReLqjzN.d.ts +0 -7
package/dist/index.js
CHANGED
|
@@ -1,14 +1,27 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
1
|
+
// src/config.ts
|
|
2
|
+
var DEFAULT_BASE_URL = "https://api.hobots.app";
|
|
3
|
+
function envBaseUrl() {
|
|
4
|
+
if (typeof process === "undefined" || !process.env) return void 0;
|
|
5
|
+
return process.env.HOBOTS_API_URL?.trim() || void 0;
|
|
6
|
+
}
|
|
7
|
+
function resolveConfig(options, prefix = "[hobots]") {
|
|
8
|
+
const key = options.clientSecret?.trim();
|
|
9
|
+
if (!key) {
|
|
10
|
+
throw new Error(`${prefix} clientSecret ausente \u2014 copie o Client Secret do projeto em Configura\xE7\xF5es \u2192 Projetos`);
|
|
11
|
+
}
|
|
12
|
+
const baseUrl = (envBaseUrl() ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
13
|
+
try {
|
|
14
|
+
new URL(baseUrl);
|
|
15
|
+
} catch {
|
|
16
|
+
throw new Error(`${prefix} HOBOTS_API_URL inv\xE1lida: "${baseUrl}" \u2014 esperado ex.: http://localhost:3001`);
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
key,
|
|
20
|
+
baseUrl,
|
|
21
|
+
ingestEndpoint: `${baseUrl}/api/ingest`,
|
|
22
|
+
managerBaseUrl: `${baseUrl}/api/manager`
|
|
23
|
+
};
|
|
24
|
+
}
|
|
12
25
|
|
|
13
26
|
// src/stacktrace.ts
|
|
14
27
|
var MAX_FRAMES = 50;
|
|
@@ -43,6 +56,44 @@ function parseStacktrace(stack) {
|
|
|
43
56
|
return frames;
|
|
44
57
|
}
|
|
45
58
|
|
|
59
|
+
// src/utils.ts
|
|
60
|
+
var SDK_NAME = "hobots";
|
|
61
|
+
var SDK_VERSION = "1.0.0";
|
|
62
|
+
function isNode() {
|
|
63
|
+
return typeof process !== "undefined" && typeof process.versions?.node === "string";
|
|
64
|
+
}
|
|
65
|
+
function uuid4() {
|
|
66
|
+
const cryptoRef = globalThis.crypto;
|
|
67
|
+
if (cryptoRef && typeof cryptoRef.randomUUID === "function") {
|
|
68
|
+
return cryptoRef.randomUUID().replace(/-/g, "");
|
|
69
|
+
}
|
|
70
|
+
let out = "";
|
|
71
|
+
for (let i = 0; i < 32; i++) {
|
|
72
|
+
out += Math.floor(Math.random() * 16).toString(16);
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
function nowISO() {
|
|
77
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
78
|
+
}
|
|
79
|
+
function unrefTimer(timer) {
|
|
80
|
+
timer?.unref?.();
|
|
81
|
+
}
|
|
82
|
+
function sleep(ms) {
|
|
83
|
+
return new Promise((resolve) => {
|
|
84
|
+
const timer = setTimeout(resolve, ms);
|
|
85
|
+
unrefTimer(timer);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
var PREFIX = "[hobots-monitor]";
|
|
89
|
+
function makeLogger(enabled, prefix = PREFIX) {
|
|
90
|
+
return {
|
|
91
|
+
debug: (...args) => enabled && console.debug(prefix, ...args),
|
|
92
|
+
warn: (...args) => enabled && console.warn(prefix, ...args),
|
|
93
|
+
error: (...args) => console.error(prefix, ...args)
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
46
97
|
// src/eventbuilder.ts
|
|
47
98
|
function safeString(value) {
|
|
48
99
|
if (typeof value === "string") return value;
|
|
@@ -92,6 +143,7 @@ var DEFAULT_MAX_BATCH_SIZE = 10;
|
|
|
92
143
|
var DEFAULT_MAX_QUEUE_SIZE = 200;
|
|
93
144
|
var Transport = class {
|
|
94
145
|
endpoint;
|
|
146
|
+
key;
|
|
95
147
|
logger;
|
|
96
148
|
flushIntervalMs;
|
|
97
149
|
maxBatchSize;
|
|
@@ -102,6 +154,7 @@ var Transport = class {
|
|
|
102
154
|
closed = false;
|
|
103
155
|
constructor(options) {
|
|
104
156
|
this.endpoint = options.endpoint;
|
|
157
|
+
this.key = options.key;
|
|
105
158
|
this.logger = options.logger;
|
|
106
159
|
this.flushIntervalMs = options.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS;
|
|
107
160
|
this.maxBatchSize = options.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE;
|
|
@@ -154,7 +207,7 @@ var Transport = class {
|
|
|
154
207
|
try {
|
|
155
208
|
const response = await fetch(this.endpoint, {
|
|
156
209
|
method: "POST",
|
|
157
|
-
headers: { "content-type": "application/json" },
|
|
210
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${this.key}` },
|
|
158
211
|
body: JSON.stringify(envelope)
|
|
159
212
|
});
|
|
160
213
|
if (response.ok) {
|
|
@@ -202,15 +255,16 @@ function runtimeContexts() {
|
|
|
202
255
|
}
|
|
203
256
|
var Client = class {
|
|
204
257
|
options;
|
|
205
|
-
|
|
258
|
+
config;
|
|
206
259
|
logger;
|
|
207
260
|
transport;
|
|
208
261
|
constructor(options) {
|
|
209
262
|
this.options = options;
|
|
210
263
|
this.logger = makeLogger(!!options.debug);
|
|
211
|
-
this.
|
|
264
|
+
this.config = resolveConfig(options);
|
|
212
265
|
this.transport = new Transport({
|
|
213
|
-
endpoint: this.
|
|
266
|
+
endpoint: this.config.ingestEndpoint,
|
|
267
|
+
key: this.config.key,
|
|
214
268
|
logger: this.logger,
|
|
215
269
|
flushIntervalMs: options.flushIntervalMs,
|
|
216
270
|
maxBatchSize: options.maxBatchSize
|
|
@@ -245,7 +299,6 @@ var Client = class {
|
|
|
245
299
|
level: hint?.level ?? event.level ?? scope2.level ?? "error",
|
|
246
300
|
release: event.release ?? this.options.release,
|
|
247
301
|
environment: event.environment ?? this.options.environment ?? "production",
|
|
248
|
-
server_name: event.server_name ?? this.options.serverName,
|
|
249
302
|
tags: Object.keys(tags).length > 0 ? tags : void 0,
|
|
250
303
|
extra: Object.keys(extra).length > 0 ? extra : void 0,
|
|
251
304
|
user: event.user ?? scope2.user,
|
|
@@ -261,7 +314,6 @@ var Client = class {
|
|
|
261
314
|
...payload,
|
|
262
315
|
release: payload.release ?? this.options.release,
|
|
263
316
|
environment: payload.environment ?? this.options.environment ?? "production",
|
|
264
|
-
server_name: payload.server_name ?? this.options.serverName,
|
|
265
317
|
tags: { ...this.options.tags, ...payload.tags }
|
|
266
318
|
}
|
|
267
319
|
});
|
|
@@ -271,12 +323,16 @@ var Client = class {
|
|
|
271
323
|
type: "check_in",
|
|
272
324
|
payload: {
|
|
273
325
|
...payload,
|
|
326
|
+
release: payload.release ?? this.options.release,
|
|
274
327
|
environment: payload.environment ?? this.options.environment ?? "production"
|
|
275
328
|
}
|
|
276
329
|
});
|
|
277
330
|
}
|
|
278
331
|
sendHeartbeat(payload) {
|
|
279
|
-
this.transport.enqueue({
|
|
332
|
+
this.transport.enqueue({
|
|
333
|
+
type: "heartbeat",
|
|
334
|
+
payload: { ...payload, release: payload.release ?? this.options.release }
|
|
335
|
+
});
|
|
280
336
|
}
|
|
281
337
|
flush(timeoutMs) {
|
|
282
338
|
return this.transport.flush(timeoutMs);
|
|
@@ -288,19 +344,12 @@ var Client = class {
|
|
|
288
344
|
|
|
289
345
|
// src/heartbeat.ts
|
|
290
346
|
var DEFAULT_INTERVAL_MS = 3e4;
|
|
291
|
-
function defaultInstanceId() {
|
|
292
|
-
if (isNode()) {
|
|
293
|
-
const host = process.env.HOSTNAME || process.env.COMPUTERNAME;
|
|
294
|
-
if (host) return host;
|
|
295
|
-
}
|
|
296
|
-
return `instance-${uuid4().slice(0, 8)}`;
|
|
297
|
-
}
|
|
298
347
|
var Heartbeat = class {
|
|
299
|
-
constructor(client2, options = {},
|
|
348
|
+
constructor(client2, options = {}, instanceId) {
|
|
300
349
|
this.client = client2;
|
|
301
350
|
this.intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
|
|
302
|
-
this.instanceId =
|
|
303
|
-
this.name = options.name ??
|
|
351
|
+
this.instanceId = instanceId;
|
|
352
|
+
this.name = options.name ?? instanceId;
|
|
304
353
|
this.metadata = options.metadata;
|
|
305
354
|
}
|
|
306
355
|
intervalMs;
|
|
@@ -581,27 +630,437 @@ async function withMonitorImpl(client2, captureError, monitorSlug, fn, options)
|
|
|
581
630
|
}
|
|
582
631
|
}
|
|
583
632
|
|
|
633
|
+
// src/manager/api.ts
|
|
634
|
+
var RETRY_DELAYS_MS2 = [1e3, 4e3, 16e3];
|
|
635
|
+
var ManagerApiError = class extends Error {
|
|
636
|
+
constructor(message, status) {
|
|
637
|
+
super(message);
|
|
638
|
+
this.status = status;
|
|
639
|
+
this.name = "ManagerApiError";
|
|
640
|
+
}
|
|
641
|
+
};
|
|
642
|
+
var ManagerApi = class {
|
|
643
|
+
baseUrl;
|
|
644
|
+
key;
|
|
645
|
+
logger;
|
|
646
|
+
constructor(options) {
|
|
647
|
+
this.baseUrl = options.baseUrl;
|
|
648
|
+
this.key = options.key;
|
|
649
|
+
this.logger = options.logger;
|
|
650
|
+
}
|
|
651
|
+
headers() {
|
|
652
|
+
return {
|
|
653
|
+
"content-type": "application/json",
|
|
654
|
+
authorization: `Bearer ${this.key}`
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
/** Um request só — o loop de polling é quem decide o backoff entre tentativas. */
|
|
658
|
+
async poll(request, signal) {
|
|
659
|
+
const response = await fetch(`${this.baseUrl}/poll`, {
|
|
660
|
+
method: "POST",
|
|
661
|
+
headers: this.headers(),
|
|
662
|
+
body: JSON.stringify(request),
|
|
663
|
+
signal
|
|
664
|
+
});
|
|
665
|
+
if (!response.ok) {
|
|
666
|
+
throw new ManagerApiError(`poll falhou (HTTP ${response.status})`, response.status);
|
|
667
|
+
}
|
|
668
|
+
return await response.json();
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* PATCH de status/logs com retry em falha de rede/5xx/429.
|
|
672
|
+
* 4xx é definitivo e vira ManagerApiError imediato (409 = run cancelada/transição inválida).
|
|
673
|
+
*/
|
|
674
|
+
async updateRun(runId, update) {
|
|
675
|
+
for (let attempt = 0; ; attempt++) {
|
|
676
|
+
let retriable;
|
|
677
|
+
try {
|
|
678
|
+
const response = await fetch(`${this.baseUrl}/runs/${encodeURIComponent(runId)}`, {
|
|
679
|
+
method: "PATCH",
|
|
680
|
+
headers: this.headers(),
|
|
681
|
+
body: JSON.stringify(update)
|
|
682
|
+
});
|
|
683
|
+
if (response.ok) return;
|
|
684
|
+
if (response.status >= 400 && response.status < 500 && response.status !== 429) {
|
|
685
|
+
throw new ManagerApiError(`update rejeitado (HTTP ${response.status})`, response.status);
|
|
686
|
+
}
|
|
687
|
+
retriable = new Error(`HTTP ${response.status}`);
|
|
688
|
+
} catch (error) {
|
|
689
|
+
if (error instanceof ManagerApiError) throw error;
|
|
690
|
+
retriable = error;
|
|
691
|
+
}
|
|
692
|
+
const delay = RETRY_DELAYS_MS2[attempt];
|
|
693
|
+
if (delay === void 0) {
|
|
694
|
+
throw new Error(`update da run ${runId} descartado ap\xF3s retries: ${String(retriable)}`);
|
|
695
|
+
}
|
|
696
|
+
this.logger.warn(`falha no update da run ${runId} (${String(retriable)}), retry em ${delay}ms`);
|
|
697
|
+
await sleep(delay);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
|
|
702
|
+
// src/manager/context.ts
|
|
703
|
+
var DEFAULT_LOG_FLUSH_INTERVAL_MS = 2e3;
|
|
704
|
+
var MAX_LOG_MESSAGE_CHARS = 1e4;
|
|
705
|
+
var MAX_LOGS_PER_RUN = 500;
|
|
706
|
+
var MAX_LOGS_PER_PATCH = 100;
|
|
707
|
+
var FLUSH_AT_BUFFERED = 20;
|
|
708
|
+
var RunReporter = class {
|
|
709
|
+
constructor(api, runId, task, logger, flushIntervalMs) {
|
|
710
|
+
this.api = api;
|
|
711
|
+
this.runId = runId;
|
|
712
|
+
this.logger = logger;
|
|
713
|
+
this.flushIntervalMs = flushIntervalMs ?? DEFAULT_LOG_FLUSH_INTERVAL_MS;
|
|
714
|
+
this.context = {
|
|
715
|
+
runId,
|
|
716
|
+
task,
|
|
717
|
+
log: (level, message) => this.log(level, message),
|
|
718
|
+
signal: this.abortController.signal
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
context;
|
|
722
|
+
buffer = [];
|
|
723
|
+
loggedCount = 0;
|
|
724
|
+
droppedCount = 0;
|
|
725
|
+
dead = false;
|
|
726
|
+
chain = Promise.resolve();
|
|
727
|
+
timer;
|
|
728
|
+
abortController = new AbortController();
|
|
729
|
+
flushIntervalMs;
|
|
730
|
+
get isDead() {
|
|
731
|
+
return this.dead;
|
|
732
|
+
}
|
|
733
|
+
/** Aborta o ctx.signal sem matar o reporter (timeout/shutdown ainda reportam o terminal). */
|
|
734
|
+
abort() {
|
|
735
|
+
if (!this.abortController.signal.aborted) this.abortController.abort();
|
|
736
|
+
}
|
|
737
|
+
log(level, message) {
|
|
738
|
+
if (this.dead) return;
|
|
739
|
+
if (this.loggedCount >= MAX_LOGS_PER_RUN) {
|
|
740
|
+
this.droppedCount++;
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
this.loggedCount++;
|
|
744
|
+
const truncated = message.length > MAX_LOG_MESSAGE_CHARS ? `${message.slice(0, MAX_LOG_MESSAGE_CHARS)}\u2026 [truncado]` : message;
|
|
745
|
+
this.buffer.push({ ts: nowISO(), level, message: truncated });
|
|
746
|
+
if (this.buffer.length >= FLUSH_AT_BUFFERED) {
|
|
747
|
+
this.flushLogs();
|
|
748
|
+
} else {
|
|
749
|
+
this.scheduleFlush();
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
scheduleFlush() {
|
|
753
|
+
if (this.timer) return;
|
|
754
|
+
this.timer = setTimeout(() => {
|
|
755
|
+
this.timer = void 0;
|
|
756
|
+
this.flushLogs();
|
|
757
|
+
}, this.flushIntervalMs);
|
|
758
|
+
unrefTimer(this.timer);
|
|
759
|
+
}
|
|
760
|
+
clearFlushTimer() {
|
|
761
|
+
if (this.timer) {
|
|
762
|
+
clearTimeout(this.timer);
|
|
763
|
+
this.timer = void 0;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
takeLogs() {
|
|
767
|
+
const logs = this.buffer.splice(0, this.buffer.length);
|
|
768
|
+
if (this.droppedCount > 0) {
|
|
769
|
+
logs.push({
|
|
770
|
+
ts: nowISO(),
|
|
771
|
+
level: "warning",
|
|
772
|
+
message: `${this.droppedCount} logs descartados (limite de ${MAX_LOGS_PER_RUN} por execu\xE7\xE3o)`
|
|
773
|
+
});
|
|
774
|
+
this.droppedCount = 0;
|
|
775
|
+
}
|
|
776
|
+
return logs;
|
|
777
|
+
}
|
|
778
|
+
/** Flush incremental: envia o buffer como status 'running' (em chunks). */
|
|
779
|
+
flushLogs() {
|
|
780
|
+
this.clearFlushTimer();
|
|
781
|
+
if (this.dead || this.buffer.length === 0) return;
|
|
782
|
+
const logs = this.buffer.splice(0, this.buffer.length);
|
|
783
|
+
for (let i = 0; i < logs.length; i += MAX_LOGS_PER_PATCH) {
|
|
784
|
+
this.enqueue({ status: "running", logs: logs.slice(i, i + MAX_LOGS_PER_PATCH) });
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
/** Primeiro PATCH (claimed → running). Retorna false se a run já morreu (cancelada). */
|
|
788
|
+
async reportRunning() {
|
|
789
|
+
this.enqueue({ status: "running" });
|
|
790
|
+
await this.chain;
|
|
791
|
+
return !this.dead;
|
|
792
|
+
}
|
|
793
|
+
reportSuccess() {
|
|
794
|
+
this.clearFlushTimer();
|
|
795
|
+
this.enqueue({ status: "succeeded", logs: this.takeLogs() });
|
|
796
|
+
}
|
|
797
|
+
reportFailure(error) {
|
|
798
|
+
this.clearFlushTimer();
|
|
799
|
+
this.enqueue({ status: "failed", error, logs: this.takeLogs() });
|
|
800
|
+
}
|
|
801
|
+
/** Aguarda a fila de PATCHes esvaziar (chame depois do terminal). */
|
|
802
|
+
finalize() {
|
|
803
|
+
return this.chain;
|
|
804
|
+
}
|
|
805
|
+
enqueue(update) {
|
|
806
|
+
if (this.dead) return;
|
|
807
|
+
this.chain = this.chain.then(async () => {
|
|
808
|
+
if (this.dead) return;
|
|
809
|
+
const payload = update.logs && update.logs.length === 0 ? { ...update, logs: void 0 } : update;
|
|
810
|
+
try {
|
|
811
|
+
await this.api.updateRun(this.runId, payload);
|
|
812
|
+
} catch (error) {
|
|
813
|
+
if (error instanceof ManagerApiError && (error.status === 409 || error.status === 404)) {
|
|
814
|
+
this.dead = true;
|
|
815
|
+
this.abort();
|
|
816
|
+
this.buffer = [];
|
|
817
|
+
this.logger.warn(`run ${this.runId} cancelada/removida no servidor \u2014 reporte suprimido`);
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
this.logger.error(`update da run ${this.runId} perdido:`, error);
|
|
821
|
+
}
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
};
|
|
825
|
+
|
|
826
|
+
// src/manager/agent.ts
|
|
827
|
+
var DEFAULT_POLL_INTERVAL_MS = 5e3;
|
|
828
|
+
var MIN_POLL_INTERVAL_MS = 1e3;
|
|
829
|
+
var MAX_CONCURRENCY = 5;
|
|
830
|
+
var POLL_RETRY_DELAYS_MS = [1e3, 4e3, 16e3];
|
|
831
|
+
var AUTH_FAILURE_INTERVAL_MS = 3e4;
|
|
832
|
+
function hostname() {
|
|
833
|
+
if (!isNode()) return void 0;
|
|
834
|
+
return process.env.HOSTNAME || process.env.COMPUTERNAME || void 0;
|
|
835
|
+
}
|
|
836
|
+
var ManagerAgent = class {
|
|
837
|
+
constructor(options, registry2) {
|
|
838
|
+
this.options = options;
|
|
839
|
+
this.registry = registry2;
|
|
840
|
+
this.logger = makeLogger(options.debug ?? false, "[hobots-manager]");
|
|
841
|
+
const config = resolveConfig(options, "[hobots-manager]");
|
|
842
|
+
this.api = new ManagerApi({ baseUrl: config.managerBaseUrl, key: config.key, logger: this.logger });
|
|
843
|
+
this.pollIntervalMs = Math.max(MIN_POLL_INTERVAL_MS, options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS);
|
|
844
|
+
this.concurrency = Math.min(MAX_CONCURRENCY, Math.max(1, options.concurrency ?? 1));
|
|
845
|
+
this.instanceId = options.instanceId;
|
|
846
|
+
this.taskTimeoutMs = options.taskTimeoutMs;
|
|
847
|
+
this.logFlushIntervalMs = options.logFlushIntervalMs;
|
|
848
|
+
this.logger.debug("agente configurado", { baseUrl: config.managerBaseUrl });
|
|
849
|
+
}
|
|
850
|
+
logger;
|
|
851
|
+
api;
|
|
852
|
+
pollIntervalMs;
|
|
853
|
+
concurrency;
|
|
854
|
+
instanceId;
|
|
855
|
+
taskTimeoutMs;
|
|
856
|
+
logFlushIntervalMs;
|
|
857
|
+
state = "idle";
|
|
858
|
+
timer;
|
|
859
|
+
pollAbort;
|
|
860
|
+
consecutiveFailures = 0;
|
|
861
|
+
consecutiveAuthFailures = 0;
|
|
862
|
+
activeRuns = /* @__PURE__ */ new Set();
|
|
863
|
+
reporters = /* @__PURE__ */ new Set();
|
|
864
|
+
get activeCount() {
|
|
865
|
+
return this.activeRuns.size;
|
|
866
|
+
}
|
|
867
|
+
start() {
|
|
868
|
+
if (this.state === "started") return;
|
|
869
|
+
if (this.state === "stopping") {
|
|
870
|
+
throw new Error("[hobots-manager] start() chamado durante stop() \u2014 aguarde o stop terminar");
|
|
871
|
+
}
|
|
872
|
+
if (this.registry.size === 0) {
|
|
873
|
+
throw new Error("[hobots-manager] nenhuma task registrada \u2014 chame register() antes de start()");
|
|
874
|
+
}
|
|
875
|
+
this.state = "started";
|
|
876
|
+
this.logger.debug(`polling iniciado (tasks: ${[...this.registry.keys()].join(", ")})`);
|
|
877
|
+
this.schedule(0);
|
|
878
|
+
}
|
|
879
|
+
/** Para o polling e aguarda as runs ativas (até timeoutMs). Retorna true se tudo terminou a tempo. */
|
|
880
|
+
async stop(timeoutMs = 3e4) {
|
|
881
|
+
if (this.state === "idle") return true;
|
|
882
|
+
this.state = "stopping";
|
|
883
|
+
if (this.timer) {
|
|
884
|
+
clearTimeout(this.timer);
|
|
885
|
+
this.timer = void 0;
|
|
886
|
+
}
|
|
887
|
+
this.pollAbort?.abort();
|
|
888
|
+
const all = Promise.allSettled([...this.activeRuns]).then(() => true);
|
|
889
|
+
const finished = timeoutMs > 0 ? await Promise.race([all, sleep(timeoutMs).then(() => false)]) : false;
|
|
890
|
+
if (!finished && this.activeRuns.size > 0) {
|
|
891
|
+
this.logger.warn(`${this.activeRuns.size} run(s) ainda em execu\xE7\xE3o ap\xF3s ${timeoutMs}ms \u2014 abandonando a espera`);
|
|
892
|
+
for (const reporter of this.reporters) reporter.abort();
|
|
893
|
+
}
|
|
894
|
+
this.state = "idle";
|
|
895
|
+
return finished;
|
|
896
|
+
}
|
|
897
|
+
/** Timer intencionalmente NÃO unref'd: o agente é a razão do processo existir. */
|
|
898
|
+
schedule(delayMs) {
|
|
899
|
+
if (this.state !== "started" || this.timer) return;
|
|
900
|
+
this.timer = setTimeout(() => {
|
|
901
|
+
this.timer = void 0;
|
|
902
|
+
void this.tick();
|
|
903
|
+
}, delayMs);
|
|
904
|
+
}
|
|
905
|
+
async tick() {
|
|
906
|
+
if (this.state !== "started") return;
|
|
907
|
+
if (this.activeRuns.size >= this.concurrency) {
|
|
908
|
+
this.schedule(this.pollIntervalMs);
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
this.pollAbort = new AbortController();
|
|
912
|
+
try {
|
|
913
|
+
const response = await this.api.poll(
|
|
914
|
+
{
|
|
915
|
+
agent: {
|
|
916
|
+
// identidade canônica do agente no painel — o mesmo instanceId do
|
|
917
|
+
// heartbeat do Monitor, então os dois canais convergem em um agente
|
|
918
|
+
instance_id: this.instanceId,
|
|
919
|
+
version: SDK_VERSION,
|
|
920
|
+
host: hostname(),
|
|
921
|
+
tasks: [...this.registry.keys()]
|
|
922
|
+
},
|
|
923
|
+
max: Math.min(MAX_CONCURRENCY, this.concurrency - this.activeRuns.size),
|
|
924
|
+
tasks: [...this.registry.keys()]
|
|
925
|
+
},
|
|
926
|
+
this.pollAbort.signal
|
|
927
|
+
);
|
|
928
|
+
this.consecutiveFailures = 0;
|
|
929
|
+
this.consecutiveAuthFailures = 0;
|
|
930
|
+
for (const run of response.runs) {
|
|
931
|
+
this.dispatch(run);
|
|
932
|
+
}
|
|
933
|
+
this.schedule(response.runs.length > 0 ? 0 : this.pollIntervalMs);
|
|
934
|
+
} catch (error) {
|
|
935
|
+
if (this.state !== "started") return;
|
|
936
|
+
this.schedule(this.delayAfterPollError(error));
|
|
937
|
+
} finally {
|
|
938
|
+
this.pollAbort = void 0;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
delayAfterPollError(error) {
|
|
942
|
+
if (error instanceof ManagerApiError && error.status === 401) {
|
|
943
|
+
this.consecutiveAuthFailures++;
|
|
944
|
+
if (this.consecutiveAuthFailures === 1 || this.consecutiveAuthFailures % 10 === 0) {
|
|
945
|
+
this.logger.error("chave rejeitada (401) \u2014 verifique o Client Secret do projeto em Configura\xE7\xF5es \u2192 Projetos");
|
|
946
|
+
}
|
|
947
|
+
return Math.max(this.pollIntervalMs, AUTH_FAILURE_INTERVAL_MS);
|
|
948
|
+
}
|
|
949
|
+
this.consecutiveFailures++;
|
|
950
|
+
const ladder = POLL_RETRY_DELAYS_MS[Math.min(this.consecutiveFailures - 1, POLL_RETRY_DELAYS_MS.length - 1)];
|
|
951
|
+
const delay = Math.min(Math.max(ladder, 0), Math.max(this.pollIntervalMs, 16e3));
|
|
952
|
+
this.logger.warn(`poll falhou (${String(error)}), pr\xF3xima tentativa em ${delay}ms`);
|
|
953
|
+
return delay;
|
|
954
|
+
}
|
|
955
|
+
dispatch(run) {
|
|
956
|
+
const promise = this.executeRun(run).catch((error) => this.logger.error(`erro inesperado na run ${run.id}:`, error)).finally(() => {
|
|
957
|
+
this.activeRuns.delete(promise);
|
|
958
|
+
if (this.state === "started") this.schedule(0);
|
|
959
|
+
});
|
|
960
|
+
this.activeRuns.add(promise);
|
|
961
|
+
}
|
|
962
|
+
async executeRun(run) {
|
|
963
|
+
const reporter = new RunReporter(this.api, run.id, run.task, this.logger, this.logFlushIntervalMs);
|
|
964
|
+
this.reporters.add(reporter);
|
|
965
|
+
try {
|
|
966
|
+
const handler = this.registry.get(run.task);
|
|
967
|
+
if (!handler) {
|
|
968
|
+
reporter.reportFailure({
|
|
969
|
+
type: "HandlerNotFound",
|
|
970
|
+
message: `tarefa n\xE3o registrada no agente: ${run.task}`
|
|
971
|
+
});
|
|
972
|
+
await reporter.finalize();
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
const ok = await reporter.reportRunning();
|
|
976
|
+
if (!ok) {
|
|
977
|
+
this.logger.debug(`run ${run.id} cancelada antes de iniciar \u2014 handler n\xE3o executado`);
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
this.logger.debug(`executando run ${run.id} (${run.task})`);
|
|
981
|
+
try {
|
|
982
|
+
await this.invokeWithTimeout(handler, run, reporter);
|
|
983
|
+
reporter.reportSuccess();
|
|
984
|
+
this.logger.debug(`run ${run.id} conclu\xEDda`);
|
|
985
|
+
} catch (error) {
|
|
986
|
+
reporter.reportFailure(toRunError(error));
|
|
987
|
+
this.logger.debug(`run ${run.id} falhou: ${String(error)}`);
|
|
988
|
+
}
|
|
989
|
+
await reporter.finalize();
|
|
990
|
+
} finally {
|
|
991
|
+
this.reporters.delete(reporter);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
async invokeWithTimeout(handler, run, reporter) {
|
|
995
|
+
const invocation = Promise.resolve().then(() => handler(run.params, reporter.context));
|
|
996
|
+
if (!this.taskTimeoutMs || this.taskTimeoutMs <= 0) {
|
|
997
|
+
await invocation;
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
const timeoutMs = this.taskTimeoutMs;
|
|
1001
|
+
const timeout = sleep(timeoutMs).then(() => TIMEOUT_SENTINEL);
|
|
1002
|
+
const outcome = await Promise.race([invocation, timeout]);
|
|
1003
|
+
if (outcome !== TIMEOUT_SENTINEL) return;
|
|
1004
|
+
reporter.abort();
|
|
1005
|
+
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)`));
|
|
1006
|
+
throw Object.assign(new Error(`timeout ap\xF3s ${timeoutMs}ms (o handler pode continuar em background)`), {
|
|
1007
|
+
name: "TaskTimeout"
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
};
|
|
1011
|
+
var TIMEOUT_SENTINEL = /* @__PURE__ */ Symbol("task-timeout");
|
|
1012
|
+
var MAX_ERROR_MESSAGE_CHARS = 1e4;
|
|
1013
|
+
var MAX_ERROR_STACK_CHARS = 2e4;
|
|
1014
|
+
function truncate(value, max) {
|
|
1015
|
+
return value.length > max ? `${value.slice(0, max)}\u2026 [truncado]` : value;
|
|
1016
|
+
}
|
|
1017
|
+
function toRunError(error) {
|
|
1018
|
+
if (error instanceof Error) {
|
|
1019
|
+
return {
|
|
1020
|
+
type: error.name,
|
|
1021
|
+
message: truncate(error.message, MAX_ERROR_MESSAGE_CHARS),
|
|
1022
|
+
stack: error.stack ? truncate(error.stack, MAX_ERROR_STACK_CHARS) : void 0
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
return { message: truncate(String(error), MAX_ERROR_MESSAGE_CHARS) };
|
|
1026
|
+
}
|
|
1027
|
+
|
|
584
1028
|
// src/core.ts
|
|
585
1029
|
var DEFAULT_MAX_BREADCRUMBS = 100;
|
|
586
1030
|
var client;
|
|
587
1031
|
var scope = new Scope();
|
|
588
1032
|
var heartbeat;
|
|
1033
|
+
var agent;
|
|
1034
|
+
var registry = /* @__PURE__ */ new Map();
|
|
1035
|
+
var warnedManagerDisabled = false;
|
|
589
1036
|
function init(options) {
|
|
590
1037
|
if (client) {
|
|
591
1038
|
client.logger.warn("init() chamado mais de uma vez \u2014 substituindo o client anterior");
|
|
592
1039
|
heartbeat?.stop();
|
|
593
1040
|
heartbeat = void 0;
|
|
594
1041
|
}
|
|
1042
|
+
if (agent) {
|
|
1043
|
+
void agent.stop(0);
|
|
1044
|
+
agent = void 0;
|
|
1045
|
+
}
|
|
1046
|
+
warnedManagerDisabled = false;
|
|
595
1047
|
client = new Client(options);
|
|
596
1048
|
if (options.captureUnhandled !== false) {
|
|
597
1049
|
installGlobalHandlers(client, () => scope);
|
|
598
1050
|
}
|
|
599
1051
|
if (options.heartbeat) {
|
|
600
1052
|
const heartbeatOptions = options.heartbeat === true ? {} : options.heartbeat;
|
|
601
|
-
heartbeat = new Heartbeat(client, heartbeatOptions, options.
|
|
1053
|
+
heartbeat = new Heartbeat(client, heartbeatOptions, options.instanceId);
|
|
602
1054
|
heartbeat.start();
|
|
603
1055
|
}
|
|
604
|
-
|
|
1056
|
+
if (options.manager) {
|
|
1057
|
+
const managerOptions = options.manager === true ? {} : options.manager;
|
|
1058
|
+
agent = new ManagerAgent(
|
|
1059
|
+
{ ...managerOptions, clientSecret: options.clientSecret, instanceId: options.instanceId, debug: options.debug },
|
|
1060
|
+
registry
|
|
1061
|
+
);
|
|
1062
|
+
}
|
|
1063
|
+
client.logger.debug("inicializado", { endpoint: client.config.ingestEndpoint });
|
|
605
1064
|
}
|
|
606
1065
|
function getClient() {
|
|
607
1066
|
return client;
|
|
@@ -660,13 +1119,40 @@ async function withMonitor(monitorSlug, fn, options) {
|
|
|
660
1119
|
function flush(timeoutMs) {
|
|
661
1120
|
return client ? client.flush(timeoutMs) : Promise.resolve(true);
|
|
662
1121
|
}
|
|
663
|
-
function
|
|
1122
|
+
function register(task, handler) {
|
|
1123
|
+
if (registry.has(task)) {
|
|
1124
|
+
(agent?.logger ?? client?.logger)?.warn(`register('${task}') sobrescreveu um handler existente`);
|
|
1125
|
+
}
|
|
1126
|
+
if (client && !agent && !warnedManagerDisabled) {
|
|
1127
|
+
warnedManagerDisabled = true;
|
|
1128
|
+
client.logger.warn(
|
|
1129
|
+
`register('${task}') com o Manager desabilitado \u2014 passe manager: true (ou op\xE7\xF5es) no init()`
|
|
1130
|
+
);
|
|
1131
|
+
}
|
|
1132
|
+
registry.set(task, handler);
|
|
1133
|
+
}
|
|
1134
|
+
function start() {
|
|
1135
|
+
if (!agent) {
|
|
1136
|
+
throw new Error("[hobots] Manager n\xE3o configurado \u2014 passe manager: true (ou op\xE7\xF5es) no init() antes de start()");
|
|
1137
|
+
}
|
|
1138
|
+
agent.start();
|
|
1139
|
+
}
|
|
1140
|
+
function stop(timeoutMs) {
|
|
1141
|
+
return agent ? agent.stop(timeoutMs) : Promise.resolve(true);
|
|
1142
|
+
}
|
|
1143
|
+
async function close(timeoutMs) {
|
|
1144
|
+
const agentStopped = agent ? await agent.stop(timeoutMs) : true;
|
|
664
1145
|
heartbeat?.stop();
|
|
665
1146
|
heartbeat = void 0;
|
|
666
|
-
|
|
1147
|
+
const clientClosed = client ? await client.close(timeoutMs) : true;
|
|
1148
|
+
return agentStopped && clientClosed;
|
|
667
1149
|
}
|
|
668
1150
|
export {
|
|
669
1151
|
Client,
|
|
1152
|
+
DEFAULT_BASE_URL,
|
|
1153
|
+
ManagerAgent,
|
|
1154
|
+
ManagerApi,
|
|
1155
|
+
ManagerApiError,
|
|
670
1156
|
Scope,
|
|
671
1157
|
Span,
|
|
672
1158
|
Transaction,
|
|
@@ -678,13 +1164,16 @@ export {
|
|
|
678
1164
|
flush,
|
|
679
1165
|
getClient,
|
|
680
1166
|
init,
|
|
681
|
-
|
|
1167
|
+
register,
|
|
1168
|
+
resolveConfig,
|
|
682
1169
|
setContext,
|
|
683
1170
|
setExtra,
|
|
684
1171
|
setTag,
|
|
685
1172
|
setTags,
|
|
686
1173
|
setUser,
|
|
1174
|
+
start,
|
|
687
1175
|
startTransaction,
|
|
1176
|
+
stop,
|
|
688
1177
|
withMonitor,
|
|
689
1178
|
withScope
|
|
690
1179
|
};
|