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.cjs
CHANGED
|
@@ -21,6 +21,10 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var src_exports = {};
|
|
22
22
|
__export(src_exports, {
|
|
23
23
|
Client: () => Client,
|
|
24
|
+
DEFAULT_BASE_URL: () => DEFAULT_BASE_URL,
|
|
25
|
+
ManagerAgent: () => ManagerAgent,
|
|
26
|
+
ManagerApi: () => ManagerApi,
|
|
27
|
+
ManagerApiError: () => ManagerApiError,
|
|
24
28
|
Scope: () => Scope,
|
|
25
29
|
Span: () => Span,
|
|
26
30
|
Transaction: () => Transaction,
|
|
@@ -32,39 +36,43 @@ __export(src_exports, {
|
|
|
32
36
|
flush: () => flush,
|
|
33
37
|
getClient: () => getClient,
|
|
34
38
|
init: () => init,
|
|
35
|
-
|
|
39
|
+
register: () => register,
|
|
40
|
+
resolveConfig: () => resolveConfig,
|
|
36
41
|
setContext: () => setContext,
|
|
37
42
|
setExtra: () => setExtra,
|
|
38
43
|
setTag: () => setTag,
|
|
39
44
|
setTags: () => setTags,
|
|
40
45
|
setUser: () => setUser,
|
|
46
|
+
start: () => start,
|
|
41
47
|
startTransaction: () => startTransaction,
|
|
48
|
+
stop: () => stop,
|
|
42
49
|
withMonitor: () => withMonitor,
|
|
43
50
|
withScope: () => withScope
|
|
44
51
|
});
|
|
45
52
|
module.exports = __toCommonJS(src_exports);
|
|
46
53
|
|
|
47
|
-
// src/
|
|
48
|
-
|
|
49
|
-
|
|
54
|
+
// src/config.ts
|
|
55
|
+
var DEFAULT_BASE_URL = "https://api.hobots.app";
|
|
56
|
+
function envBaseUrl() {
|
|
57
|
+
if (typeof process === "undefined" || !process.env) return void 0;
|
|
58
|
+
return process.env.HOBOTS_API_URL?.trim() || void 0;
|
|
59
|
+
}
|
|
60
|
+
function resolveConfig(options, prefix = "[hobots]") {
|
|
61
|
+
const key = options.clientSecret?.trim();
|
|
62
|
+
if (!key) {
|
|
63
|
+
throw new Error(`${prefix} clientSecret ausente \u2014 copie o Client Secret do projeto em Configura\xE7\xF5es \u2192 Projetos`);
|
|
64
|
+
}
|
|
65
|
+
const baseUrl = (envBaseUrl() ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
50
66
|
try {
|
|
51
|
-
|
|
67
|
+
new URL(baseUrl);
|
|
52
68
|
} catch {
|
|
53
|
-
throw new Error(`${prefix}
|
|
54
|
-
}
|
|
55
|
-
const key = url.username;
|
|
56
|
-
const path = url.pathname.replace(/^\/+/, "");
|
|
57
|
-
if (!key || !path) {
|
|
58
|
-
throw new Error(`${prefix} DSN inv\xE1lido: "${dsn}" \u2014 esperado https://<key>@host/<projeto>`);
|
|
69
|
+
throw new Error(`${prefix} HOBOTS_API_URL inv\xE1lida: "${baseUrl}" \u2014 esperado ex.: http://localhost:3001`);
|
|
59
70
|
}
|
|
60
|
-
return { key, path, origin: `${url.protocol}//${url.host}` };
|
|
61
|
-
}
|
|
62
|
-
function parseDsn(dsn) {
|
|
63
|
-
const { key, path, origin } = parseDsnUrl(dsn, "[hobots-monitor]");
|
|
64
71
|
return {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
72
|
+
key,
|
|
73
|
+
baseUrl,
|
|
74
|
+
ingestEndpoint: `${baseUrl}/api/ingest`,
|
|
75
|
+
managerBaseUrl: `${baseUrl}/api/manager`
|
|
68
76
|
};
|
|
69
77
|
}
|
|
70
78
|
|
|
@@ -188,6 +196,7 @@ var DEFAULT_MAX_BATCH_SIZE = 10;
|
|
|
188
196
|
var DEFAULT_MAX_QUEUE_SIZE = 200;
|
|
189
197
|
var Transport = class {
|
|
190
198
|
endpoint;
|
|
199
|
+
key;
|
|
191
200
|
logger;
|
|
192
201
|
flushIntervalMs;
|
|
193
202
|
maxBatchSize;
|
|
@@ -198,6 +207,7 @@ var Transport = class {
|
|
|
198
207
|
closed = false;
|
|
199
208
|
constructor(options) {
|
|
200
209
|
this.endpoint = options.endpoint;
|
|
210
|
+
this.key = options.key;
|
|
201
211
|
this.logger = options.logger;
|
|
202
212
|
this.flushIntervalMs = options.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS;
|
|
203
213
|
this.maxBatchSize = options.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE;
|
|
@@ -250,7 +260,7 @@ var Transport = class {
|
|
|
250
260
|
try {
|
|
251
261
|
const response = await fetch(this.endpoint, {
|
|
252
262
|
method: "POST",
|
|
253
|
-
headers: { "content-type": "application/json" },
|
|
263
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${this.key}` },
|
|
254
264
|
body: JSON.stringify(envelope)
|
|
255
265
|
});
|
|
256
266
|
if (response.ok) {
|
|
@@ -298,15 +308,16 @@ function runtimeContexts() {
|
|
|
298
308
|
}
|
|
299
309
|
var Client = class {
|
|
300
310
|
options;
|
|
301
|
-
|
|
311
|
+
config;
|
|
302
312
|
logger;
|
|
303
313
|
transport;
|
|
304
314
|
constructor(options) {
|
|
305
315
|
this.options = options;
|
|
306
316
|
this.logger = makeLogger(!!options.debug);
|
|
307
|
-
this.
|
|
317
|
+
this.config = resolveConfig(options);
|
|
308
318
|
this.transport = new Transport({
|
|
309
|
-
endpoint: this.
|
|
319
|
+
endpoint: this.config.ingestEndpoint,
|
|
320
|
+
key: this.config.key,
|
|
310
321
|
logger: this.logger,
|
|
311
322
|
flushIntervalMs: options.flushIntervalMs,
|
|
312
323
|
maxBatchSize: options.maxBatchSize
|
|
@@ -341,7 +352,6 @@ var Client = class {
|
|
|
341
352
|
level: hint?.level ?? event.level ?? scope2.level ?? "error",
|
|
342
353
|
release: event.release ?? this.options.release,
|
|
343
354
|
environment: event.environment ?? this.options.environment ?? "production",
|
|
344
|
-
server_name: event.server_name ?? this.options.serverName,
|
|
345
355
|
tags: Object.keys(tags).length > 0 ? tags : void 0,
|
|
346
356
|
extra: Object.keys(extra).length > 0 ? extra : void 0,
|
|
347
357
|
user: event.user ?? scope2.user,
|
|
@@ -357,7 +367,6 @@ var Client = class {
|
|
|
357
367
|
...payload,
|
|
358
368
|
release: payload.release ?? this.options.release,
|
|
359
369
|
environment: payload.environment ?? this.options.environment ?? "production",
|
|
360
|
-
server_name: payload.server_name ?? this.options.serverName,
|
|
361
370
|
tags: { ...this.options.tags, ...payload.tags }
|
|
362
371
|
}
|
|
363
372
|
});
|
|
@@ -367,12 +376,16 @@ var Client = class {
|
|
|
367
376
|
type: "check_in",
|
|
368
377
|
payload: {
|
|
369
378
|
...payload,
|
|
379
|
+
release: payload.release ?? this.options.release,
|
|
370
380
|
environment: payload.environment ?? this.options.environment ?? "production"
|
|
371
381
|
}
|
|
372
382
|
});
|
|
373
383
|
}
|
|
374
384
|
sendHeartbeat(payload) {
|
|
375
|
-
this.transport.enqueue({
|
|
385
|
+
this.transport.enqueue({
|
|
386
|
+
type: "heartbeat",
|
|
387
|
+
payload: { ...payload, release: payload.release ?? this.options.release }
|
|
388
|
+
});
|
|
376
389
|
}
|
|
377
390
|
flush(timeoutMs) {
|
|
378
391
|
return this.transport.flush(timeoutMs);
|
|
@@ -384,19 +397,12 @@ var Client = class {
|
|
|
384
397
|
|
|
385
398
|
// src/heartbeat.ts
|
|
386
399
|
var DEFAULT_INTERVAL_MS = 3e4;
|
|
387
|
-
function defaultInstanceId() {
|
|
388
|
-
if (isNode()) {
|
|
389
|
-
const host = process.env.HOSTNAME || process.env.COMPUTERNAME;
|
|
390
|
-
if (host) return host;
|
|
391
|
-
}
|
|
392
|
-
return `instance-${uuid4().slice(0, 8)}`;
|
|
393
|
-
}
|
|
394
400
|
var Heartbeat = class {
|
|
395
|
-
constructor(client2, options = {},
|
|
401
|
+
constructor(client2, options = {}, instanceId) {
|
|
396
402
|
this.client = client2;
|
|
397
403
|
this.intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
|
|
398
|
-
this.instanceId =
|
|
399
|
-
this.name = options.name ??
|
|
404
|
+
this.instanceId = instanceId;
|
|
405
|
+
this.name = options.name ?? instanceId;
|
|
400
406
|
this.metadata = options.metadata;
|
|
401
407
|
}
|
|
402
408
|
intervalMs;
|
|
@@ -677,27 +683,437 @@ async function withMonitorImpl(client2, captureError, monitorSlug, fn, options)
|
|
|
677
683
|
}
|
|
678
684
|
}
|
|
679
685
|
|
|
686
|
+
// src/manager/api.ts
|
|
687
|
+
var RETRY_DELAYS_MS2 = [1e3, 4e3, 16e3];
|
|
688
|
+
var ManagerApiError = class extends Error {
|
|
689
|
+
constructor(message, status) {
|
|
690
|
+
super(message);
|
|
691
|
+
this.status = status;
|
|
692
|
+
this.name = "ManagerApiError";
|
|
693
|
+
}
|
|
694
|
+
};
|
|
695
|
+
var ManagerApi = class {
|
|
696
|
+
baseUrl;
|
|
697
|
+
key;
|
|
698
|
+
logger;
|
|
699
|
+
constructor(options) {
|
|
700
|
+
this.baseUrl = options.baseUrl;
|
|
701
|
+
this.key = options.key;
|
|
702
|
+
this.logger = options.logger;
|
|
703
|
+
}
|
|
704
|
+
headers() {
|
|
705
|
+
return {
|
|
706
|
+
"content-type": "application/json",
|
|
707
|
+
authorization: `Bearer ${this.key}`
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
/** Um request só — o loop de polling é quem decide o backoff entre tentativas. */
|
|
711
|
+
async poll(request, signal) {
|
|
712
|
+
const response = await fetch(`${this.baseUrl}/poll`, {
|
|
713
|
+
method: "POST",
|
|
714
|
+
headers: this.headers(),
|
|
715
|
+
body: JSON.stringify(request),
|
|
716
|
+
signal
|
|
717
|
+
});
|
|
718
|
+
if (!response.ok) {
|
|
719
|
+
throw new ManagerApiError(`poll falhou (HTTP ${response.status})`, response.status);
|
|
720
|
+
}
|
|
721
|
+
return await response.json();
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* PATCH de status/logs com retry em falha de rede/5xx/429.
|
|
725
|
+
* 4xx é definitivo e vira ManagerApiError imediato (409 = run cancelada/transição inválida).
|
|
726
|
+
*/
|
|
727
|
+
async updateRun(runId, update) {
|
|
728
|
+
for (let attempt = 0; ; attempt++) {
|
|
729
|
+
let retriable;
|
|
730
|
+
try {
|
|
731
|
+
const response = await fetch(`${this.baseUrl}/runs/${encodeURIComponent(runId)}`, {
|
|
732
|
+
method: "PATCH",
|
|
733
|
+
headers: this.headers(),
|
|
734
|
+
body: JSON.stringify(update)
|
|
735
|
+
});
|
|
736
|
+
if (response.ok) return;
|
|
737
|
+
if (response.status >= 400 && response.status < 500 && response.status !== 429) {
|
|
738
|
+
throw new ManagerApiError(`update rejeitado (HTTP ${response.status})`, response.status);
|
|
739
|
+
}
|
|
740
|
+
retriable = new Error(`HTTP ${response.status}`);
|
|
741
|
+
} catch (error) {
|
|
742
|
+
if (error instanceof ManagerApiError) throw error;
|
|
743
|
+
retriable = error;
|
|
744
|
+
}
|
|
745
|
+
const delay = RETRY_DELAYS_MS2[attempt];
|
|
746
|
+
if (delay === void 0) {
|
|
747
|
+
throw new Error(`update da run ${runId} descartado ap\xF3s retries: ${String(retriable)}`);
|
|
748
|
+
}
|
|
749
|
+
this.logger.warn(`falha no update da run ${runId} (${String(retriable)}), retry em ${delay}ms`);
|
|
750
|
+
await sleep(delay);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
};
|
|
754
|
+
|
|
755
|
+
// src/manager/context.ts
|
|
756
|
+
var DEFAULT_LOG_FLUSH_INTERVAL_MS = 2e3;
|
|
757
|
+
var MAX_LOG_MESSAGE_CHARS = 1e4;
|
|
758
|
+
var MAX_LOGS_PER_RUN = 500;
|
|
759
|
+
var MAX_LOGS_PER_PATCH = 100;
|
|
760
|
+
var FLUSH_AT_BUFFERED = 20;
|
|
761
|
+
var RunReporter = class {
|
|
762
|
+
constructor(api, runId, task, logger, flushIntervalMs) {
|
|
763
|
+
this.api = api;
|
|
764
|
+
this.runId = runId;
|
|
765
|
+
this.logger = logger;
|
|
766
|
+
this.flushIntervalMs = flushIntervalMs ?? DEFAULT_LOG_FLUSH_INTERVAL_MS;
|
|
767
|
+
this.context = {
|
|
768
|
+
runId,
|
|
769
|
+
task,
|
|
770
|
+
log: (level, message) => this.log(level, message),
|
|
771
|
+
signal: this.abortController.signal
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
context;
|
|
775
|
+
buffer = [];
|
|
776
|
+
loggedCount = 0;
|
|
777
|
+
droppedCount = 0;
|
|
778
|
+
dead = false;
|
|
779
|
+
chain = Promise.resolve();
|
|
780
|
+
timer;
|
|
781
|
+
abortController = new AbortController();
|
|
782
|
+
flushIntervalMs;
|
|
783
|
+
get isDead() {
|
|
784
|
+
return this.dead;
|
|
785
|
+
}
|
|
786
|
+
/** Aborta o ctx.signal sem matar o reporter (timeout/shutdown ainda reportam o terminal). */
|
|
787
|
+
abort() {
|
|
788
|
+
if (!this.abortController.signal.aborted) this.abortController.abort();
|
|
789
|
+
}
|
|
790
|
+
log(level, message) {
|
|
791
|
+
if (this.dead) return;
|
|
792
|
+
if (this.loggedCount >= MAX_LOGS_PER_RUN) {
|
|
793
|
+
this.droppedCount++;
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
this.loggedCount++;
|
|
797
|
+
const truncated = message.length > MAX_LOG_MESSAGE_CHARS ? `${message.slice(0, MAX_LOG_MESSAGE_CHARS)}\u2026 [truncado]` : message;
|
|
798
|
+
this.buffer.push({ ts: nowISO(), level, message: truncated });
|
|
799
|
+
if (this.buffer.length >= FLUSH_AT_BUFFERED) {
|
|
800
|
+
this.flushLogs();
|
|
801
|
+
} else {
|
|
802
|
+
this.scheduleFlush();
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
scheduleFlush() {
|
|
806
|
+
if (this.timer) return;
|
|
807
|
+
this.timer = setTimeout(() => {
|
|
808
|
+
this.timer = void 0;
|
|
809
|
+
this.flushLogs();
|
|
810
|
+
}, this.flushIntervalMs);
|
|
811
|
+
unrefTimer(this.timer);
|
|
812
|
+
}
|
|
813
|
+
clearFlushTimer() {
|
|
814
|
+
if (this.timer) {
|
|
815
|
+
clearTimeout(this.timer);
|
|
816
|
+
this.timer = void 0;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
takeLogs() {
|
|
820
|
+
const logs = this.buffer.splice(0, this.buffer.length);
|
|
821
|
+
if (this.droppedCount > 0) {
|
|
822
|
+
logs.push({
|
|
823
|
+
ts: nowISO(),
|
|
824
|
+
level: "warning",
|
|
825
|
+
message: `${this.droppedCount} logs descartados (limite de ${MAX_LOGS_PER_RUN} por execu\xE7\xE3o)`
|
|
826
|
+
});
|
|
827
|
+
this.droppedCount = 0;
|
|
828
|
+
}
|
|
829
|
+
return logs;
|
|
830
|
+
}
|
|
831
|
+
/** Flush incremental: envia o buffer como status 'running' (em chunks). */
|
|
832
|
+
flushLogs() {
|
|
833
|
+
this.clearFlushTimer();
|
|
834
|
+
if (this.dead || this.buffer.length === 0) return;
|
|
835
|
+
const logs = this.buffer.splice(0, this.buffer.length);
|
|
836
|
+
for (let i = 0; i < logs.length; i += MAX_LOGS_PER_PATCH) {
|
|
837
|
+
this.enqueue({ status: "running", logs: logs.slice(i, i + MAX_LOGS_PER_PATCH) });
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
/** Primeiro PATCH (claimed → running). Retorna false se a run já morreu (cancelada). */
|
|
841
|
+
async reportRunning() {
|
|
842
|
+
this.enqueue({ status: "running" });
|
|
843
|
+
await this.chain;
|
|
844
|
+
return !this.dead;
|
|
845
|
+
}
|
|
846
|
+
reportSuccess() {
|
|
847
|
+
this.clearFlushTimer();
|
|
848
|
+
this.enqueue({ status: "succeeded", logs: this.takeLogs() });
|
|
849
|
+
}
|
|
850
|
+
reportFailure(error) {
|
|
851
|
+
this.clearFlushTimer();
|
|
852
|
+
this.enqueue({ status: "failed", error, logs: this.takeLogs() });
|
|
853
|
+
}
|
|
854
|
+
/** Aguarda a fila de PATCHes esvaziar (chame depois do terminal). */
|
|
855
|
+
finalize() {
|
|
856
|
+
return this.chain;
|
|
857
|
+
}
|
|
858
|
+
enqueue(update) {
|
|
859
|
+
if (this.dead) return;
|
|
860
|
+
this.chain = this.chain.then(async () => {
|
|
861
|
+
if (this.dead) return;
|
|
862
|
+
const payload = update.logs && update.logs.length === 0 ? { ...update, logs: void 0 } : update;
|
|
863
|
+
try {
|
|
864
|
+
await this.api.updateRun(this.runId, payload);
|
|
865
|
+
} catch (error) {
|
|
866
|
+
if (error instanceof ManagerApiError && (error.status === 409 || error.status === 404)) {
|
|
867
|
+
this.dead = true;
|
|
868
|
+
this.abort();
|
|
869
|
+
this.buffer = [];
|
|
870
|
+
this.logger.warn(`run ${this.runId} cancelada/removida no servidor \u2014 reporte suprimido`);
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
this.logger.error(`update da run ${this.runId} perdido:`, error);
|
|
874
|
+
}
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
};
|
|
878
|
+
|
|
879
|
+
// src/manager/agent.ts
|
|
880
|
+
var DEFAULT_POLL_INTERVAL_MS = 5e3;
|
|
881
|
+
var MIN_POLL_INTERVAL_MS = 1e3;
|
|
882
|
+
var MAX_CONCURRENCY = 5;
|
|
883
|
+
var POLL_RETRY_DELAYS_MS = [1e3, 4e3, 16e3];
|
|
884
|
+
var AUTH_FAILURE_INTERVAL_MS = 3e4;
|
|
885
|
+
function hostname() {
|
|
886
|
+
if (!isNode()) return void 0;
|
|
887
|
+
return process.env.HOSTNAME || process.env.COMPUTERNAME || void 0;
|
|
888
|
+
}
|
|
889
|
+
var ManagerAgent = class {
|
|
890
|
+
constructor(options, registry2) {
|
|
891
|
+
this.options = options;
|
|
892
|
+
this.registry = registry2;
|
|
893
|
+
this.logger = makeLogger(options.debug ?? false, "[hobots-manager]");
|
|
894
|
+
const config = resolveConfig(options, "[hobots-manager]");
|
|
895
|
+
this.api = new ManagerApi({ baseUrl: config.managerBaseUrl, key: config.key, logger: this.logger });
|
|
896
|
+
this.pollIntervalMs = Math.max(MIN_POLL_INTERVAL_MS, options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS);
|
|
897
|
+
this.concurrency = Math.min(MAX_CONCURRENCY, Math.max(1, options.concurrency ?? 1));
|
|
898
|
+
this.instanceId = options.instanceId;
|
|
899
|
+
this.taskTimeoutMs = options.taskTimeoutMs;
|
|
900
|
+
this.logFlushIntervalMs = options.logFlushIntervalMs;
|
|
901
|
+
this.logger.debug("agente configurado", { baseUrl: config.managerBaseUrl });
|
|
902
|
+
}
|
|
903
|
+
logger;
|
|
904
|
+
api;
|
|
905
|
+
pollIntervalMs;
|
|
906
|
+
concurrency;
|
|
907
|
+
instanceId;
|
|
908
|
+
taskTimeoutMs;
|
|
909
|
+
logFlushIntervalMs;
|
|
910
|
+
state = "idle";
|
|
911
|
+
timer;
|
|
912
|
+
pollAbort;
|
|
913
|
+
consecutiveFailures = 0;
|
|
914
|
+
consecutiveAuthFailures = 0;
|
|
915
|
+
activeRuns = /* @__PURE__ */ new Set();
|
|
916
|
+
reporters = /* @__PURE__ */ new Set();
|
|
917
|
+
get activeCount() {
|
|
918
|
+
return this.activeRuns.size;
|
|
919
|
+
}
|
|
920
|
+
start() {
|
|
921
|
+
if (this.state === "started") return;
|
|
922
|
+
if (this.state === "stopping") {
|
|
923
|
+
throw new Error("[hobots-manager] start() chamado durante stop() \u2014 aguarde o stop terminar");
|
|
924
|
+
}
|
|
925
|
+
if (this.registry.size === 0) {
|
|
926
|
+
throw new Error("[hobots-manager] nenhuma task registrada \u2014 chame register() antes de start()");
|
|
927
|
+
}
|
|
928
|
+
this.state = "started";
|
|
929
|
+
this.logger.debug(`polling iniciado (tasks: ${[...this.registry.keys()].join(", ")})`);
|
|
930
|
+
this.schedule(0);
|
|
931
|
+
}
|
|
932
|
+
/** Para o polling e aguarda as runs ativas (até timeoutMs). Retorna true se tudo terminou a tempo. */
|
|
933
|
+
async stop(timeoutMs = 3e4) {
|
|
934
|
+
if (this.state === "idle") return true;
|
|
935
|
+
this.state = "stopping";
|
|
936
|
+
if (this.timer) {
|
|
937
|
+
clearTimeout(this.timer);
|
|
938
|
+
this.timer = void 0;
|
|
939
|
+
}
|
|
940
|
+
this.pollAbort?.abort();
|
|
941
|
+
const all = Promise.allSettled([...this.activeRuns]).then(() => true);
|
|
942
|
+
const finished = timeoutMs > 0 ? await Promise.race([all, sleep(timeoutMs).then(() => false)]) : false;
|
|
943
|
+
if (!finished && this.activeRuns.size > 0) {
|
|
944
|
+
this.logger.warn(`${this.activeRuns.size} run(s) ainda em execu\xE7\xE3o ap\xF3s ${timeoutMs}ms \u2014 abandonando a espera`);
|
|
945
|
+
for (const reporter of this.reporters) reporter.abort();
|
|
946
|
+
}
|
|
947
|
+
this.state = "idle";
|
|
948
|
+
return finished;
|
|
949
|
+
}
|
|
950
|
+
/** Timer intencionalmente NÃO unref'd: o agente é a razão do processo existir. */
|
|
951
|
+
schedule(delayMs) {
|
|
952
|
+
if (this.state !== "started" || this.timer) return;
|
|
953
|
+
this.timer = setTimeout(() => {
|
|
954
|
+
this.timer = void 0;
|
|
955
|
+
void this.tick();
|
|
956
|
+
}, delayMs);
|
|
957
|
+
}
|
|
958
|
+
async tick() {
|
|
959
|
+
if (this.state !== "started") return;
|
|
960
|
+
if (this.activeRuns.size >= this.concurrency) {
|
|
961
|
+
this.schedule(this.pollIntervalMs);
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
this.pollAbort = new AbortController();
|
|
965
|
+
try {
|
|
966
|
+
const response = await this.api.poll(
|
|
967
|
+
{
|
|
968
|
+
agent: {
|
|
969
|
+
// identidade canônica do agente no painel — o mesmo instanceId do
|
|
970
|
+
// heartbeat do Monitor, então os dois canais convergem em um agente
|
|
971
|
+
instance_id: this.instanceId,
|
|
972
|
+
version: SDK_VERSION,
|
|
973
|
+
host: hostname(),
|
|
974
|
+
tasks: [...this.registry.keys()]
|
|
975
|
+
},
|
|
976
|
+
max: Math.min(MAX_CONCURRENCY, this.concurrency - this.activeRuns.size),
|
|
977
|
+
tasks: [...this.registry.keys()]
|
|
978
|
+
},
|
|
979
|
+
this.pollAbort.signal
|
|
980
|
+
);
|
|
981
|
+
this.consecutiveFailures = 0;
|
|
982
|
+
this.consecutiveAuthFailures = 0;
|
|
983
|
+
for (const run of response.runs) {
|
|
984
|
+
this.dispatch(run);
|
|
985
|
+
}
|
|
986
|
+
this.schedule(response.runs.length > 0 ? 0 : this.pollIntervalMs);
|
|
987
|
+
} catch (error) {
|
|
988
|
+
if (this.state !== "started") return;
|
|
989
|
+
this.schedule(this.delayAfterPollError(error));
|
|
990
|
+
} finally {
|
|
991
|
+
this.pollAbort = void 0;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
delayAfterPollError(error) {
|
|
995
|
+
if (error instanceof ManagerApiError && error.status === 401) {
|
|
996
|
+
this.consecutiveAuthFailures++;
|
|
997
|
+
if (this.consecutiveAuthFailures === 1 || this.consecutiveAuthFailures % 10 === 0) {
|
|
998
|
+
this.logger.error("chave rejeitada (401) \u2014 verifique o Client Secret do projeto em Configura\xE7\xF5es \u2192 Projetos");
|
|
999
|
+
}
|
|
1000
|
+
return Math.max(this.pollIntervalMs, AUTH_FAILURE_INTERVAL_MS);
|
|
1001
|
+
}
|
|
1002
|
+
this.consecutiveFailures++;
|
|
1003
|
+
const ladder = POLL_RETRY_DELAYS_MS[Math.min(this.consecutiveFailures - 1, POLL_RETRY_DELAYS_MS.length - 1)];
|
|
1004
|
+
const delay = Math.min(Math.max(ladder, 0), Math.max(this.pollIntervalMs, 16e3));
|
|
1005
|
+
this.logger.warn(`poll falhou (${String(error)}), pr\xF3xima tentativa em ${delay}ms`);
|
|
1006
|
+
return delay;
|
|
1007
|
+
}
|
|
1008
|
+
dispatch(run) {
|
|
1009
|
+
const promise = this.executeRun(run).catch((error) => this.logger.error(`erro inesperado na run ${run.id}:`, error)).finally(() => {
|
|
1010
|
+
this.activeRuns.delete(promise);
|
|
1011
|
+
if (this.state === "started") this.schedule(0);
|
|
1012
|
+
});
|
|
1013
|
+
this.activeRuns.add(promise);
|
|
1014
|
+
}
|
|
1015
|
+
async executeRun(run) {
|
|
1016
|
+
const reporter = new RunReporter(this.api, run.id, run.task, this.logger, this.logFlushIntervalMs);
|
|
1017
|
+
this.reporters.add(reporter);
|
|
1018
|
+
try {
|
|
1019
|
+
const handler = this.registry.get(run.task);
|
|
1020
|
+
if (!handler) {
|
|
1021
|
+
reporter.reportFailure({
|
|
1022
|
+
type: "HandlerNotFound",
|
|
1023
|
+
message: `tarefa n\xE3o registrada no agente: ${run.task}`
|
|
1024
|
+
});
|
|
1025
|
+
await reporter.finalize();
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
const ok = await reporter.reportRunning();
|
|
1029
|
+
if (!ok) {
|
|
1030
|
+
this.logger.debug(`run ${run.id} cancelada antes de iniciar \u2014 handler n\xE3o executado`);
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
this.logger.debug(`executando run ${run.id} (${run.task})`);
|
|
1034
|
+
try {
|
|
1035
|
+
await this.invokeWithTimeout(handler, run, reporter);
|
|
1036
|
+
reporter.reportSuccess();
|
|
1037
|
+
this.logger.debug(`run ${run.id} conclu\xEDda`);
|
|
1038
|
+
} catch (error) {
|
|
1039
|
+
reporter.reportFailure(toRunError(error));
|
|
1040
|
+
this.logger.debug(`run ${run.id} falhou: ${String(error)}`);
|
|
1041
|
+
}
|
|
1042
|
+
await reporter.finalize();
|
|
1043
|
+
} finally {
|
|
1044
|
+
this.reporters.delete(reporter);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
async invokeWithTimeout(handler, run, reporter) {
|
|
1048
|
+
const invocation = Promise.resolve().then(() => handler(run.params, reporter.context));
|
|
1049
|
+
if (!this.taskTimeoutMs || this.taskTimeoutMs <= 0) {
|
|
1050
|
+
await invocation;
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
const timeoutMs = this.taskTimeoutMs;
|
|
1054
|
+
const timeout = sleep(timeoutMs).then(() => TIMEOUT_SENTINEL);
|
|
1055
|
+
const outcome = await Promise.race([invocation, timeout]);
|
|
1056
|
+
if (outcome !== TIMEOUT_SENTINEL) return;
|
|
1057
|
+
reporter.abort();
|
|
1058
|
+
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)`));
|
|
1059
|
+
throw Object.assign(new Error(`timeout ap\xF3s ${timeoutMs}ms (o handler pode continuar em background)`), {
|
|
1060
|
+
name: "TaskTimeout"
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
};
|
|
1064
|
+
var TIMEOUT_SENTINEL = /* @__PURE__ */ Symbol("task-timeout");
|
|
1065
|
+
var MAX_ERROR_MESSAGE_CHARS = 1e4;
|
|
1066
|
+
var MAX_ERROR_STACK_CHARS = 2e4;
|
|
1067
|
+
function truncate(value, max) {
|
|
1068
|
+
return value.length > max ? `${value.slice(0, max)}\u2026 [truncado]` : value;
|
|
1069
|
+
}
|
|
1070
|
+
function toRunError(error) {
|
|
1071
|
+
if (error instanceof Error) {
|
|
1072
|
+
return {
|
|
1073
|
+
type: error.name,
|
|
1074
|
+
message: truncate(error.message, MAX_ERROR_MESSAGE_CHARS),
|
|
1075
|
+
stack: error.stack ? truncate(error.stack, MAX_ERROR_STACK_CHARS) : void 0
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
return { message: truncate(String(error), MAX_ERROR_MESSAGE_CHARS) };
|
|
1079
|
+
}
|
|
1080
|
+
|
|
680
1081
|
// src/core.ts
|
|
681
1082
|
var DEFAULT_MAX_BREADCRUMBS = 100;
|
|
682
1083
|
var client;
|
|
683
1084
|
var scope = new Scope();
|
|
684
1085
|
var heartbeat;
|
|
1086
|
+
var agent;
|
|
1087
|
+
var registry = /* @__PURE__ */ new Map();
|
|
1088
|
+
var warnedManagerDisabled = false;
|
|
685
1089
|
function init(options) {
|
|
686
1090
|
if (client) {
|
|
687
1091
|
client.logger.warn("init() chamado mais de uma vez \u2014 substituindo o client anterior");
|
|
688
1092
|
heartbeat?.stop();
|
|
689
1093
|
heartbeat = void 0;
|
|
690
1094
|
}
|
|
1095
|
+
if (agent) {
|
|
1096
|
+
void agent.stop(0);
|
|
1097
|
+
agent = void 0;
|
|
1098
|
+
}
|
|
1099
|
+
warnedManagerDisabled = false;
|
|
691
1100
|
client = new Client(options);
|
|
692
1101
|
if (options.captureUnhandled !== false) {
|
|
693
1102
|
installGlobalHandlers(client, () => scope);
|
|
694
1103
|
}
|
|
695
1104
|
if (options.heartbeat) {
|
|
696
1105
|
const heartbeatOptions = options.heartbeat === true ? {} : options.heartbeat;
|
|
697
|
-
heartbeat = new Heartbeat(client, heartbeatOptions, options.
|
|
1106
|
+
heartbeat = new Heartbeat(client, heartbeatOptions, options.instanceId);
|
|
698
1107
|
heartbeat.start();
|
|
699
1108
|
}
|
|
700
|
-
|
|
1109
|
+
if (options.manager) {
|
|
1110
|
+
const managerOptions = options.manager === true ? {} : options.manager;
|
|
1111
|
+
agent = new ManagerAgent(
|
|
1112
|
+
{ ...managerOptions, clientSecret: options.clientSecret, instanceId: options.instanceId, debug: options.debug },
|
|
1113
|
+
registry
|
|
1114
|
+
);
|
|
1115
|
+
}
|
|
1116
|
+
client.logger.debug("inicializado", { endpoint: client.config.ingestEndpoint });
|
|
701
1117
|
}
|
|
702
1118
|
function getClient() {
|
|
703
1119
|
return client;
|
|
@@ -756,9 +1172,32 @@ async function withMonitor(monitorSlug, fn, options) {
|
|
|
756
1172
|
function flush(timeoutMs) {
|
|
757
1173
|
return client ? client.flush(timeoutMs) : Promise.resolve(true);
|
|
758
1174
|
}
|
|
759
|
-
function
|
|
1175
|
+
function register(task, handler) {
|
|
1176
|
+
if (registry.has(task)) {
|
|
1177
|
+
(agent?.logger ?? client?.logger)?.warn(`register('${task}') sobrescreveu um handler existente`);
|
|
1178
|
+
}
|
|
1179
|
+
if (client && !agent && !warnedManagerDisabled) {
|
|
1180
|
+
warnedManagerDisabled = true;
|
|
1181
|
+
client.logger.warn(
|
|
1182
|
+
`register('${task}') com o Manager desabilitado \u2014 passe manager: true (ou op\xE7\xF5es) no init()`
|
|
1183
|
+
);
|
|
1184
|
+
}
|
|
1185
|
+
registry.set(task, handler);
|
|
1186
|
+
}
|
|
1187
|
+
function start() {
|
|
1188
|
+
if (!agent) {
|
|
1189
|
+
throw new Error("[hobots] Manager n\xE3o configurado \u2014 passe manager: true (ou op\xE7\xF5es) no init() antes de start()");
|
|
1190
|
+
}
|
|
1191
|
+
agent.start();
|
|
1192
|
+
}
|
|
1193
|
+
function stop(timeoutMs) {
|
|
1194
|
+
return agent ? agent.stop(timeoutMs) : Promise.resolve(true);
|
|
1195
|
+
}
|
|
1196
|
+
async function close(timeoutMs) {
|
|
1197
|
+
const agentStopped = agent ? await agent.stop(timeoutMs) : true;
|
|
760
1198
|
heartbeat?.stop();
|
|
761
1199
|
heartbeat = void 0;
|
|
762
|
-
|
|
1200
|
+
const clientClosed = client ? await client.close(timeoutMs) : true;
|
|
1201
|
+
return agentStopped && clientClosed;
|
|
763
1202
|
}
|
|
764
1203
|
//# sourceMappingURL=index.cjs.map
|