hobots 1.3.3 → 1.3.5
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 +7 -7
- package/dist/index.cjs +181 -23
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +40 -5
- package/dist/index.d.ts +40 -5
- package/dist/index.js +181 -23
- package/dist/index.js.map +1 -1
- package/dist/protocol.cjs.map +1 -1
- package/dist/protocol.d.cts +1 -1
- package/dist/protocol.d.ts +1 -1
- package/docs/README.md +1 -1
- package/docs/api-reference.md +5 -1
- package/docs/getting-started.md +1 -1
- package/docs/manager.md +23 -11
- package/docs/protocol.md +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -48,7 +48,7 @@ Hobots.init({
|
|
|
48
48
|
});
|
|
49
49
|
|
|
50
50
|
Hobots.register('processar-nfe', async (params, ctx) => {
|
|
51
|
-
ctx.log('
|
|
51
|
+
ctx.log.info('começando…');
|
|
52
52
|
return { processadas: 12 };
|
|
53
53
|
});
|
|
54
54
|
|
|
@@ -120,12 +120,12 @@ try {
|
|
|
120
120
|
throw error;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
Hobots.captureMessage('Lote processado com sucesso', '
|
|
123
|
+
Hobots.captureMessage('Lote processado com sucesso', 'success');
|
|
124
124
|
```
|
|
125
125
|
|
|
126
126
|
Ambas retornam o `event_id` (ou `undefined` se não inicializado). O hint aceita `level`, `tags`, `extra` e `fingerprint`. Erros **não tratados** são capturados e flushados automaticamente antes do processo sair — desligue com `captureUnhandled: false`.
|
|
127
127
|
|
|
128
|
-
`Severity = 'fatal' | 'error' | 'warning' | 'info' | 'debug'`.
|
|
128
|
+
`Severity = 'fatal' | 'error' | 'warning' | 'info' | 'debug' | 'success'`. Use `'success'` para reportar itens concluídos com êxito — contam como eventos, mas **não** viram issue nem disparam alertas.
|
|
129
129
|
|
|
130
130
|
### Breadcrumbs
|
|
131
131
|
|
|
@@ -235,9 +235,9 @@ Hobots.init({
|
|
|
235
235
|
});
|
|
236
236
|
|
|
237
237
|
Hobots.register('processar-nfe', async (params, ctx) => {
|
|
238
|
-
ctx.log(
|
|
238
|
+
ctx.log.info(`processando lote ${params.lote}…`);
|
|
239
239
|
if (ctx.signal.aborted) return; // cancelamento cooperativo
|
|
240
|
-
ctx.log('
|
|
240
|
+
ctx.log.info('12 notas processadas');
|
|
241
241
|
});
|
|
242
242
|
|
|
243
243
|
Hobots.start(); // começa o polling e MANTÉM O PROCESSO VIVO
|
|
@@ -277,7 +277,7 @@ interface RunContext {
|
|
|
277
277
|
```
|
|
278
278
|
|
|
279
279
|
- **`params`** — os dados do formulário configurado no app (JSON validado). Tipável: `Hobots.register<{ lote: number }>(...)`.
|
|
280
|
-
- **`ctx.log(
|
|
280
|
+
- **`ctx.log.<level>(message)`** — logs enviados **incrementalmente** ao app (PATCHes com throttle), visíveis na run quase em tempo real.
|
|
281
281
|
- **`ctx.signal`** — `AbortSignal` que dispara quando a run é **cancelada no app**, em **timeout** ou no **shutdown**.
|
|
282
282
|
- **Retorno** — **ignorado**: o SDK não captura nem envia o valor retornado pelo handler (resultados podem conter dados sensíveis e não são coletados).
|
|
283
283
|
- **Erro lançado** — a run é reportada como `failed` (mensagem cap 10k, stack 20k).
|
|
@@ -292,7 +292,7 @@ Uma run cancelada no app **não mata** o handler — o SDK apenas dispara `ctx.s
|
|
|
292
292
|
Hobots.register('processar-nfe', async (params, ctx) => {
|
|
293
293
|
for (const nota of params.notas) {
|
|
294
294
|
if (ctx.signal.aborted) {
|
|
295
|
-
ctx.log('
|
|
295
|
+
ctx.log.warn('cancelamento detectado — parando');
|
|
296
296
|
return;
|
|
297
297
|
}
|
|
298
298
|
await processar(nota);
|
package/dist/index.cjs
CHANGED
|
@@ -111,7 +111,7 @@ function parseStacktrace(stack) {
|
|
|
111
111
|
|
|
112
112
|
// src/utils.ts
|
|
113
113
|
var SDK_NAME = "hobots";
|
|
114
|
-
var SDK_VERSION = true ? "1.3.
|
|
114
|
+
var SDK_VERSION = true ? "1.3.4" : "0.0.0-dev";
|
|
115
115
|
function isNode() {
|
|
116
116
|
return typeof process !== "undefined" && typeof process.versions?.node === "string";
|
|
117
117
|
}
|
|
@@ -433,28 +433,61 @@ var Heartbeat = class {
|
|
|
433
433
|
}
|
|
434
434
|
};
|
|
435
435
|
|
|
436
|
+
// src/internal-handlers.ts
|
|
437
|
+
var INTERNAL = /* @__PURE__ */ Symbol.for("hobots.sdk.internalListener");
|
|
438
|
+
function markInternal(listener) {
|
|
439
|
+
listener[INTERNAL] = true;
|
|
440
|
+
return listener;
|
|
441
|
+
}
|
|
442
|
+
function externalListenerCount(event) {
|
|
443
|
+
const listeners = process.listeners(event);
|
|
444
|
+
return listeners.filter((listener) => !listener[INTERNAL]).length;
|
|
445
|
+
}
|
|
446
|
+
var emergencyTasks = [];
|
|
447
|
+
function addEmergencyTask(task) {
|
|
448
|
+
emergencyTasks.push(task);
|
|
449
|
+
}
|
|
450
|
+
async function awaitEmergencyTasks(capMs) {
|
|
451
|
+
if (emergencyTasks.length === 0) return;
|
|
452
|
+
await Promise.race([Promise.allSettled(emergencyTasks), sleep(capMs)]);
|
|
453
|
+
}
|
|
454
|
+
|
|
436
455
|
// src/instrument.ts
|
|
456
|
+
var EMERGENCY_REPORT_CAP_MS = 3e3;
|
|
437
457
|
var installed = false;
|
|
458
|
+
function hasInstalledGlobalHandlers() {
|
|
459
|
+
return installed;
|
|
460
|
+
}
|
|
438
461
|
function installGlobalHandlers(client2, getScope) {
|
|
439
462
|
if (installed) return;
|
|
440
463
|
installed = true;
|
|
441
464
|
if (isNode()) {
|
|
442
|
-
process.on(
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
process.on(
|
|
456
|
-
|
|
457
|
-
|
|
465
|
+
process.on(
|
|
466
|
+
"uncaughtException",
|
|
467
|
+
markInternal((error) => {
|
|
468
|
+
client2.captureException(error, { level: "fatal", tags: { mechanism: "uncaughtException" } }, getScope());
|
|
469
|
+
void client2.flush(2e3).then(async () => {
|
|
470
|
+
if (externalListenerCount("uncaughtException") === 0) {
|
|
471
|
+
await awaitEmergencyTasks(EMERGENCY_REPORT_CAP_MS);
|
|
472
|
+
console.error(error);
|
|
473
|
+
process.exit(1);
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
})
|
|
477
|
+
);
|
|
478
|
+
process.on(
|
|
479
|
+
"unhandledRejection",
|
|
480
|
+
markInternal((reason) => {
|
|
481
|
+
client2.captureException(reason, { level: "error", tags: { mechanism: "unhandledRejection" } }, getScope());
|
|
482
|
+
void client2.flush(2e3);
|
|
483
|
+
})
|
|
484
|
+
);
|
|
485
|
+
process.on(
|
|
486
|
+
"beforeExit",
|
|
487
|
+
markInternal(() => {
|
|
488
|
+
void client2.flush(2e3);
|
|
489
|
+
})
|
|
490
|
+
);
|
|
458
491
|
return;
|
|
459
492
|
}
|
|
460
493
|
if (typeof window !== "undefined") {
|
|
@@ -750,12 +783,46 @@ var ManagerApi = class {
|
|
|
750
783
|
await sleep(delay);
|
|
751
784
|
}
|
|
752
785
|
}
|
|
786
|
+
/**
|
|
787
|
+
* Um único PATCH com timeout curto — exit path (o processo está morrendo, o retry
|
|
788
|
+
* ladder do updateRun é proibido aqui). Status HTTP é ignorado: 409/404 significa
|
|
789
|
+
* que a run já foi cancelada/finalizada, o que é aceitável nesse caminho.
|
|
790
|
+
*/
|
|
791
|
+
async updateRunOnce(runId, update, timeoutMs = 2e3) {
|
|
792
|
+
await fetch(`${this.baseUrl}/runs/${encodeURIComponent(runId)}`, {
|
|
793
|
+
method: "PATCH",
|
|
794
|
+
headers: this.headers(),
|
|
795
|
+
body: JSON.stringify(update),
|
|
796
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
797
|
+
});
|
|
798
|
+
}
|
|
753
799
|
};
|
|
754
800
|
|
|
755
801
|
// src/manager/context.ts
|
|
802
|
+
function runCanceledError() {
|
|
803
|
+
return Object.assign(new Error("execu\xE7\xE3o cancelada"), { name: "RunCanceled" });
|
|
804
|
+
}
|
|
805
|
+
function cancelableSleep(ms, signal) {
|
|
806
|
+
return new Promise((resolve, reject) => {
|
|
807
|
+
if (signal.aborted) {
|
|
808
|
+
reject(runCanceledError());
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
const onAbort = () => {
|
|
812
|
+
clearTimeout(timer);
|
|
813
|
+
reject(runCanceledError());
|
|
814
|
+
};
|
|
815
|
+
const timer = setTimeout(() => {
|
|
816
|
+
signal.removeEventListener("abort", onAbort);
|
|
817
|
+
resolve();
|
|
818
|
+
}, ms);
|
|
819
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
820
|
+
});
|
|
821
|
+
}
|
|
756
822
|
var DEFAULT_LOG_FLUSH_INTERVAL_MS = 2e3;
|
|
757
823
|
var DEFAULT_CANCEL_CHECK_INTERVAL_MS = 5e3;
|
|
758
824
|
var MIN_CANCEL_CHECK_INTERVAL_MS = 1e3;
|
|
825
|
+
var MAX_CANCEL_CHECK_INTERVAL_MS = 15e3;
|
|
759
826
|
var MAX_LOG_MESSAGE_CHARS = 1e4;
|
|
760
827
|
var MAX_LOGS_PER_RUN = 500;
|
|
761
828
|
var MAX_LOGS_PER_PATCH = 100;
|
|
@@ -766,15 +833,26 @@ var RunReporter = class {
|
|
|
766
833
|
this.runId = runId;
|
|
767
834
|
this.logger = logger;
|
|
768
835
|
this.flushIntervalMs = flushIntervalMs ?? DEFAULT_LOG_FLUSH_INTERVAL_MS;
|
|
769
|
-
this.cancelCheckIntervalMs = Math.
|
|
770
|
-
|
|
771
|
-
cancelCheckIntervalMs ?? DEFAULT_CANCEL_CHECK_INTERVAL_MS
|
|
836
|
+
this.cancelCheckIntervalMs = Math.min(
|
|
837
|
+
MAX_CANCEL_CHECK_INTERVAL_MS,
|
|
838
|
+
Math.max(MIN_CANCEL_CHECK_INTERVAL_MS, cancelCheckIntervalMs ?? DEFAULT_CANCEL_CHECK_INTERVAL_MS)
|
|
772
839
|
);
|
|
840
|
+
const log = {
|
|
841
|
+
debug: (message) => this.log("debug", message),
|
|
842
|
+
info: (message) => this.log("info", message),
|
|
843
|
+
warn: (message) => this.log("warning", message),
|
|
844
|
+
error: (message) => this.log("error", message)
|
|
845
|
+
};
|
|
846
|
+
const signal = this.abortController.signal;
|
|
773
847
|
this.context = {
|
|
774
848
|
runId,
|
|
775
849
|
task,
|
|
776
|
-
log
|
|
777
|
-
signal
|
|
850
|
+
log,
|
|
851
|
+
signal,
|
|
852
|
+
throwIfCanceled: () => {
|
|
853
|
+
if (signal.aborted) throw runCanceledError();
|
|
854
|
+
},
|
|
855
|
+
sleep: (ms) => cancelableSleep(ms, signal)
|
|
778
856
|
};
|
|
779
857
|
}
|
|
780
858
|
context;
|
|
@@ -797,6 +875,17 @@ var RunReporter = class {
|
|
|
797
875
|
this.stopCancelWatch();
|
|
798
876
|
if (!this.abortController.signal.aborted) this.abortController.abort();
|
|
799
877
|
}
|
|
878
|
+
/**
|
|
879
|
+
* Mata o reporter de vez: aborta o signal e suprime todo reporte futuro.
|
|
880
|
+
* Usado no exit path — depois que o agente reportou AgentTerminated por fora
|
|
881
|
+
* da fila, um handler zumbi não pode reenviar status por cima.
|
|
882
|
+
*/
|
|
883
|
+
kill() {
|
|
884
|
+
this.abort();
|
|
885
|
+
this.dead = true;
|
|
886
|
+
this.clearFlushTimer();
|
|
887
|
+
this.buffer = [];
|
|
888
|
+
}
|
|
800
889
|
log(level, message) {
|
|
801
890
|
if (this.dead) return;
|
|
802
891
|
if (this.loggedCount >= MAX_LOGS_PER_RUN) {
|
|
@@ -912,6 +1001,50 @@ var RunReporter = class {
|
|
|
912
1001
|
}
|
|
913
1002
|
};
|
|
914
1003
|
|
|
1004
|
+
// src/manager/shutdown.ts
|
|
1005
|
+
var WATCHED_SIGNALS = ["SIGINT", "SIGTERM"];
|
|
1006
|
+
function installShutdownHandlers(agent2) {
|
|
1007
|
+
if (!isNode()) return () => {
|
|
1008
|
+
};
|
|
1009
|
+
let signaled = false;
|
|
1010
|
+
const signalHandlers = /* @__PURE__ */ new Map();
|
|
1011
|
+
for (const signal of WATCHED_SIGNALS) {
|
|
1012
|
+
const handler = markInternal(() => {
|
|
1013
|
+
if (externalListenerCount(signal) > 0) return;
|
|
1014
|
+
if (signaled) return;
|
|
1015
|
+
signaled = true;
|
|
1016
|
+
const reraise = () => {
|
|
1017
|
+
process.removeListener(signal, handler);
|
|
1018
|
+
process.kill(process.pid, signal);
|
|
1019
|
+
};
|
|
1020
|
+
if (agent2.activeCount === 0) {
|
|
1021
|
+
reraise();
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
const report = agent2.reportAgentTerminated(`o processo do agente recebeu ${signal} durante a execu\xE7\xE3o`);
|
|
1025
|
+
addEmergencyTask(report);
|
|
1026
|
+
void report.finally(reraise);
|
|
1027
|
+
});
|
|
1028
|
+
process.on(signal, handler);
|
|
1029
|
+
signalHandlers.set(signal, handler);
|
|
1030
|
+
}
|
|
1031
|
+
const onFatal = markInternal((error) => {
|
|
1032
|
+
const report = agent2.activeCount > 0 ? agent2.reportAgentTerminated("o processo do agente morreu com um erro n\xE3o tratado durante a execu\xE7\xE3o") : Promise.resolve();
|
|
1033
|
+
addEmergencyTask(report);
|
|
1034
|
+
if (!hasInstalledGlobalHandlers() && externalListenerCount("uncaughtException") === 0) {
|
|
1035
|
+
void report.finally(() => {
|
|
1036
|
+
console.error(error);
|
|
1037
|
+
process.exit(1);
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
});
|
|
1041
|
+
process.on("uncaughtException", onFatal);
|
|
1042
|
+
return () => {
|
|
1043
|
+
for (const [signal, handler] of signalHandlers) process.removeListener(signal, handler);
|
|
1044
|
+
process.removeListener("uncaughtException", onFatal);
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
|
|
915
1048
|
// src/manager/agent.ts
|
|
916
1049
|
var DEFAULT_POLL_INTERVAL_MS = 5e3;
|
|
917
1050
|
var MIN_POLL_INTERVAL_MS = 1e3;
|
|
@@ -952,6 +1085,7 @@ var ManagerAgent = class {
|
|
|
952
1085
|
consecutiveAuthFailures = 0;
|
|
953
1086
|
activeRuns = /* @__PURE__ */ new Set();
|
|
954
1087
|
reporters = /* @__PURE__ */ new Set();
|
|
1088
|
+
uninstallShutdown;
|
|
955
1089
|
get activeCount() {
|
|
956
1090
|
return this.activeRuns.size;
|
|
957
1091
|
}
|
|
@@ -964,6 +1098,9 @@ var ManagerAgent = class {
|
|
|
964
1098
|
throw new Error("[hobots-manager] nenhuma task registrada \u2014 chame register() antes de start()");
|
|
965
1099
|
}
|
|
966
1100
|
this.state = "started";
|
|
1101
|
+
if (this.options.installExitHandlers !== false && !this.uninstallShutdown) {
|
|
1102
|
+
this.uninstallShutdown = installShutdownHandlers(this);
|
|
1103
|
+
}
|
|
967
1104
|
this.logger.debug(`polling iniciado (tasks: ${[...this.registry.keys()].join(", ")})`);
|
|
968
1105
|
this.schedule(0);
|
|
969
1106
|
}
|
|
@@ -980,11 +1117,32 @@ var ManagerAgent = class {
|
|
|
980
1117
|
const finished = timeoutMs > 0 ? await Promise.race([all, sleep(timeoutMs).then(() => false)]) : false;
|
|
981
1118
|
if (!finished && this.activeRuns.size > 0) {
|
|
982
1119
|
this.logger.warn(`${this.activeRuns.size} run(s) ainda em execu\xE7\xE3o ap\xF3s ${timeoutMs}ms \u2014 abandonando a espera`);
|
|
983
|
-
|
|
1120
|
+
await this.reportAgentTerminated("agente encerrado durante a execu\xE7\xE3o (shutdown excedeu o tempo de espera)");
|
|
984
1121
|
}
|
|
1122
|
+
this.uninstallShutdown?.();
|
|
1123
|
+
this.uninstallShutdown = void 0;
|
|
985
1124
|
this.state = "idle";
|
|
986
1125
|
return finished;
|
|
987
1126
|
}
|
|
1127
|
+
/**
|
|
1128
|
+
* Best-effort do exit path: reporta 'failed'/AgentTerminated para as runs ativas
|
|
1129
|
+
* antes do processo morrer, fora da fila do reporter e com timeout curto. Runs já
|
|
1130
|
+
* canceladas no app (reporter morto) são puladas; erros de rede são engolidos —
|
|
1131
|
+
* o sweep server-side é o backstop.
|
|
1132
|
+
*/
|
|
1133
|
+
async reportAgentTerminated(message, timeoutMs = 2500) {
|
|
1134
|
+
const pending = [];
|
|
1135
|
+
for (const reporter of this.reporters) {
|
|
1136
|
+
if (reporter.isDead) continue;
|
|
1137
|
+
reporter.kill();
|
|
1138
|
+
pending.push(
|
|
1139
|
+
this.api.updateRunOnce(reporter.context.runId, { status: "failed", error: { type: "AgentTerminated", message } }, timeoutMs).catch(() => {
|
|
1140
|
+
})
|
|
1141
|
+
);
|
|
1142
|
+
}
|
|
1143
|
+
if (pending.length === 0) return;
|
|
1144
|
+
await Promise.race([Promise.allSettled(pending), sleep(timeoutMs)]);
|
|
1145
|
+
}
|
|
988
1146
|
/** Timer intencionalmente NÃO unref'd: o agente é a razão do processo existir. */
|
|
989
1147
|
schedule(delayMs) {
|
|
990
1148
|
if (this.state !== "started" || this.timer) return;
|