hobots 1.9.1 → 1.10.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 +21 -0
- package/dist/index.cjs +224 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +40 -3
- package/dist/index.d.ts +40 -3
- package/dist/index.js +224 -5
- package/dist/index.js.map +1 -1
- package/dist/protocol.cjs.map +1 -1
- package/dist/protocol.d.cts +34 -1
- package/dist/protocol.d.ts +34 -1
- package/docs/api-reference.md +11 -0
- package/docs/protocol.md +35 -0
- package/docs/tasks.md +64 -0
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Breadcrumb, EventUser, Severity, EventPayload, TransactionPayload, HeartbeatPayload, StepStatus, RunLogLevel, StepData, RunAttachmentContentType } from './protocol.cjs';
|
|
2
|
-
export { AgentInfo, Envelope, EnvelopeItem, ExceptionData, PollRequest, PollResponse, RunAttachmentConfirmRequest, RunAttachmentConfirmResponse, RunAttachmentCreateRequest, RunAttachmentCreateResponse, RunDescriptor, RunError, RunLogEntry, RunStatus, RunUpdateRequest, RunUpdateResponse, RunUpdateStatus, StackFrame } from './protocol.cjs';
|
|
1
|
+
import { Breadcrumb, EventUser, Severity, EventPayload, TransactionPayload, HeartbeatPayload, StepStatus, RunLogLevel, StepData, RunItemStatus, RunAttachmentContentType } from './protocol.cjs';
|
|
2
|
+
export { AgentInfo, Envelope, EnvelopeItem, ExceptionData, PollRequest, PollResponse, RunAttachmentConfirmRequest, RunAttachmentConfirmResponse, RunAttachmentCreateRequest, RunAttachmentCreateResponse, RunDescriptor, RunError, RunItemInput, RunItemsCreateRequest, RunItemsCreateResponse, RunLogEntry, RunStatus, RunUpdateRequest, RunUpdateResponse, RunUpdateStatus, StackFrame } from './protocol.cjs';
|
|
3
3
|
|
|
4
4
|
/** Aceita tudo que o handler pode ter em mãos: bytes ou um caminho de arquivo (só Node). */
|
|
5
5
|
type AttachInput = Uint8Array | ArrayBuffer | string;
|
|
@@ -146,6 +146,13 @@ interface TasksOptions {
|
|
|
146
146
|
* uma run sem reporte por ~60s. Default: 5_000 (mínimo 1_000, máximo 15_000)
|
|
147
147
|
*/
|
|
148
148
|
cancelCheckIntervalMs?: number;
|
|
149
|
+
/**
|
|
150
|
+
* Tempo de graça após ctx.signal disparar (run cancelada no app) antes do
|
|
151
|
+
* agente desistir de esperar o handler e liberar o slot de concorrência. O
|
|
152
|
+
* handler não é morto — se ele nunca checar ctx.signal, pode continuar
|
|
153
|
+
* rodando em segundo plano; só o agente para de esperar por ele. Default: 10_000
|
|
154
|
+
*/
|
|
155
|
+
cancelGraceMs?: number;
|
|
149
156
|
/**
|
|
150
157
|
* Instala handlers de SIGINT/SIGTERM/uncaughtException que reportam as runs
|
|
151
158
|
* ativas como falha (AgentTerminated) antes do processo morrer. Nunca interfere
|
|
@@ -160,11 +167,41 @@ interface RunLogger {
|
|
|
160
167
|
error(message: string): void;
|
|
161
168
|
success(message: string): void;
|
|
162
169
|
}
|
|
170
|
+
interface RunItemOptions {
|
|
171
|
+
/**
|
|
172
|
+
* Identidade de negócio do item na run (ex.: nº da nota fiscal). Repetido na
|
|
173
|
+
* mesma run = ignorado pelo servidor (registro único, retry idempotente).
|
|
174
|
+
* Omitido: o SDK gera um id único e cada chamada vira um item novo.
|
|
175
|
+
*/
|
|
176
|
+
id?: string;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Registro dos itens que a execução processou/gerou (a entrega do bot: notas
|
|
180
|
+
* fiscais baixadas, boletos emitidos...). Chamadas são síncronas — os itens são
|
|
181
|
+
* bufferizados e enviados em lotes; o agente drena o buffer antes do reporte final.
|
|
182
|
+
*/
|
|
183
|
+
interface RunItems {
|
|
184
|
+
/** Forma genérica — os atalhos succeeded/failed/occurrence cobrem o dia a dia. */
|
|
185
|
+
add(item: {
|
|
186
|
+
status: RunItemStatus;
|
|
187
|
+
id?: string;
|
|
188
|
+
message?: string;
|
|
189
|
+
payload?: Record<string, unknown>;
|
|
190
|
+
}): void;
|
|
191
|
+
/** Item processado com sucesso. `payload` = atributos do item (viram colunas na plataforma). */
|
|
192
|
+
succeeded(payload?: Record<string, unknown>, options?: RunItemOptions): void;
|
|
193
|
+
/** Falha técnica ao processar o item — `message` é o motivo exibido na plataforma. */
|
|
194
|
+
failed(message: string, payload?: Record<string, unknown>, options?: RunItemOptions): void;
|
|
195
|
+
/** Ocorrência de negócio (ex.: "nota nº 10 não encontrada") — não é falha do bot. */
|
|
196
|
+
occurrence(message: string, payload?: Record<string, unknown>, options?: RunItemOptions): void;
|
|
197
|
+
}
|
|
163
198
|
interface RunContext {
|
|
164
199
|
runId: string;
|
|
165
200
|
task: string;
|
|
166
201
|
/** Log bufferizado — enviado ao servidor em PATCHes incrementais com throttle */
|
|
167
202
|
log: RunLogger;
|
|
203
|
+
/** Itens processados/gerados pela execução — a entrega do bot, com status e atributos */
|
|
204
|
+
items: RunItems;
|
|
168
205
|
/**
|
|
169
206
|
* Transaction de performance desta execução — já correlacionada à run e finalizada
|
|
170
207
|
* automaticamente ao fim do handler. Use `ctx.tx.startChild(op, description)` pra medir
|
|
@@ -296,4 +333,4 @@ declare function close(timeoutMs?: number): Promise<boolean>;
|
|
|
296
333
|
|
|
297
334
|
declare function scrubSecrets(text: string): string;
|
|
298
335
|
|
|
299
|
-
export { type AttachInput, Breadcrumb, type BreadcrumbInput, type CaptureAttachment, type CaptureHint, Client, DEFAULT_BASE_URL, EventPayload, EventUser, type HeartbeatOptions, HeartbeatPayload, type InitOptions, RunAttachmentContentType, type RunContext, RunLogLevel, Scope, Severity, Step, StepData, type TaskHandler, type TasksOptions, Transaction, TransactionPayload, addBreadcrumb, captureException, captureMessage, close, flush, getClient, init, register, scrubSecrets, setContext, setExtra, setTag, setTags, setUser, start, startTransaction, stop, withScope };
|
|
336
|
+
export { type AttachInput, Breadcrumb, type BreadcrumbInput, type CaptureAttachment, type CaptureHint, Client, DEFAULT_BASE_URL, EventPayload, EventUser, type HeartbeatOptions, HeartbeatPayload, type InitOptions, RunAttachmentContentType, type RunContext, type RunItemOptions, RunItemStatus, type RunItems, RunLogLevel, Scope, Severity, Step, StepData, type TaskHandler, type TasksOptions, Transaction, TransactionPayload, addBreadcrumb, captureException, captureMessage, close, flush, getClient, init, register, scrubSecrets, setContext, setExtra, setTag, setTags, setUser, start, startTransaction, stop, withScope };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Breadcrumb, EventUser, Severity, EventPayload, TransactionPayload, HeartbeatPayload, StepStatus, RunLogLevel, StepData, RunAttachmentContentType } from './protocol.js';
|
|
2
|
-
export { AgentInfo, Envelope, EnvelopeItem, ExceptionData, PollRequest, PollResponse, RunAttachmentConfirmRequest, RunAttachmentConfirmResponse, RunAttachmentCreateRequest, RunAttachmentCreateResponse, RunDescriptor, RunError, RunLogEntry, RunStatus, RunUpdateRequest, RunUpdateResponse, RunUpdateStatus, StackFrame } from './protocol.js';
|
|
1
|
+
import { Breadcrumb, EventUser, Severity, EventPayload, TransactionPayload, HeartbeatPayload, StepStatus, RunLogLevel, StepData, RunItemStatus, RunAttachmentContentType } from './protocol.js';
|
|
2
|
+
export { AgentInfo, Envelope, EnvelopeItem, ExceptionData, PollRequest, PollResponse, RunAttachmentConfirmRequest, RunAttachmentConfirmResponse, RunAttachmentCreateRequest, RunAttachmentCreateResponse, RunDescriptor, RunError, RunItemInput, RunItemsCreateRequest, RunItemsCreateResponse, RunLogEntry, RunStatus, RunUpdateRequest, RunUpdateResponse, RunUpdateStatus, StackFrame } from './protocol.js';
|
|
3
3
|
|
|
4
4
|
/** Aceita tudo que o handler pode ter em mãos: bytes ou um caminho de arquivo (só Node). */
|
|
5
5
|
type AttachInput = Uint8Array | ArrayBuffer | string;
|
|
@@ -146,6 +146,13 @@ interface TasksOptions {
|
|
|
146
146
|
* uma run sem reporte por ~60s. Default: 5_000 (mínimo 1_000, máximo 15_000)
|
|
147
147
|
*/
|
|
148
148
|
cancelCheckIntervalMs?: number;
|
|
149
|
+
/**
|
|
150
|
+
* Tempo de graça após ctx.signal disparar (run cancelada no app) antes do
|
|
151
|
+
* agente desistir de esperar o handler e liberar o slot de concorrência. O
|
|
152
|
+
* handler não é morto — se ele nunca checar ctx.signal, pode continuar
|
|
153
|
+
* rodando em segundo plano; só o agente para de esperar por ele. Default: 10_000
|
|
154
|
+
*/
|
|
155
|
+
cancelGraceMs?: number;
|
|
149
156
|
/**
|
|
150
157
|
* Instala handlers de SIGINT/SIGTERM/uncaughtException que reportam as runs
|
|
151
158
|
* ativas como falha (AgentTerminated) antes do processo morrer. Nunca interfere
|
|
@@ -160,11 +167,41 @@ interface RunLogger {
|
|
|
160
167
|
error(message: string): void;
|
|
161
168
|
success(message: string): void;
|
|
162
169
|
}
|
|
170
|
+
interface RunItemOptions {
|
|
171
|
+
/**
|
|
172
|
+
* Identidade de negócio do item na run (ex.: nº da nota fiscal). Repetido na
|
|
173
|
+
* mesma run = ignorado pelo servidor (registro único, retry idempotente).
|
|
174
|
+
* Omitido: o SDK gera um id único e cada chamada vira um item novo.
|
|
175
|
+
*/
|
|
176
|
+
id?: string;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Registro dos itens que a execução processou/gerou (a entrega do bot: notas
|
|
180
|
+
* fiscais baixadas, boletos emitidos...). Chamadas são síncronas — os itens são
|
|
181
|
+
* bufferizados e enviados em lotes; o agente drena o buffer antes do reporte final.
|
|
182
|
+
*/
|
|
183
|
+
interface RunItems {
|
|
184
|
+
/** Forma genérica — os atalhos succeeded/failed/occurrence cobrem o dia a dia. */
|
|
185
|
+
add(item: {
|
|
186
|
+
status: RunItemStatus;
|
|
187
|
+
id?: string;
|
|
188
|
+
message?: string;
|
|
189
|
+
payload?: Record<string, unknown>;
|
|
190
|
+
}): void;
|
|
191
|
+
/** Item processado com sucesso. `payload` = atributos do item (viram colunas na plataforma). */
|
|
192
|
+
succeeded(payload?: Record<string, unknown>, options?: RunItemOptions): void;
|
|
193
|
+
/** Falha técnica ao processar o item — `message` é o motivo exibido na plataforma. */
|
|
194
|
+
failed(message: string, payload?: Record<string, unknown>, options?: RunItemOptions): void;
|
|
195
|
+
/** Ocorrência de negócio (ex.: "nota nº 10 não encontrada") — não é falha do bot. */
|
|
196
|
+
occurrence(message: string, payload?: Record<string, unknown>, options?: RunItemOptions): void;
|
|
197
|
+
}
|
|
163
198
|
interface RunContext {
|
|
164
199
|
runId: string;
|
|
165
200
|
task: string;
|
|
166
201
|
/** Log bufferizado — enviado ao servidor em PATCHes incrementais com throttle */
|
|
167
202
|
log: RunLogger;
|
|
203
|
+
/** Itens processados/gerados pela execução — a entrega do bot, com status e atributos */
|
|
204
|
+
items: RunItems;
|
|
168
205
|
/**
|
|
169
206
|
* Transaction de performance desta execução — já correlacionada à run e finalizada
|
|
170
207
|
* automaticamente ao fim do handler. Use `ctx.tx.startChild(op, description)` pra medir
|
|
@@ -296,4 +333,4 @@ declare function close(timeoutMs?: number): Promise<boolean>;
|
|
|
296
333
|
|
|
297
334
|
declare function scrubSecrets(text: string): string;
|
|
298
335
|
|
|
299
|
-
export { type AttachInput, Breadcrumb, type BreadcrumbInput, type CaptureAttachment, type CaptureHint, Client, DEFAULT_BASE_URL, EventPayload, EventUser, type HeartbeatOptions, HeartbeatPayload, type InitOptions, RunAttachmentContentType, type RunContext, RunLogLevel, Scope, Severity, Step, StepData, type TaskHandler, type TasksOptions, Transaction, TransactionPayload, addBreadcrumb, captureException, captureMessage, close, flush, getClient, init, register, scrubSecrets, setContext, setExtra, setTag, setTags, setUser, start, startTransaction, stop, withScope };
|
|
336
|
+
export { type AttachInput, Breadcrumb, type BreadcrumbInput, type CaptureAttachment, type CaptureHint, Client, DEFAULT_BASE_URL, EventPayload, EventUser, type HeartbeatOptions, HeartbeatPayload, type InitOptions, RunAttachmentContentType, type RunContext, type RunItemOptions, RunItemStatus, type RunItems, RunLogLevel, Scope, Severity, Step, StepData, type TaskHandler, type TasksOptions, Transaction, TransactionPayload, addBreadcrumb, captureException, captureMessage, close, flush, getClient, init, register, scrubSecrets, setContext, setExtra, setTag, setTags, setUser, start, startTransaction, stop, withScope };
|
package/dist/index.js
CHANGED
|
@@ -103,7 +103,7 @@ function scrubSecrets(text) {
|
|
|
103
103
|
|
|
104
104
|
// src/utils.ts
|
|
105
105
|
var SDK_NAME = "hobots";
|
|
106
|
-
var SDK_VERSION = true ? "1.
|
|
106
|
+
var SDK_VERSION = true ? "1.10.0" : "0.0.0-dev";
|
|
107
107
|
function isNode() {
|
|
108
108
|
return typeof process !== "undefined" && typeof process.versions?.node === "string";
|
|
109
109
|
}
|
|
@@ -929,6 +929,38 @@ var TasksApi = class {
|
|
|
929
929
|
signal: AbortSignal.timeout(timeoutMs)
|
|
930
930
|
});
|
|
931
931
|
}
|
|
932
|
+
/**
|
|
933
|
+
* Envia um lote de itens de execução. Retry em falha de rede/5xx/429-transporte;
|
|
934
|
+
* 4xx é definitivo (404 = servidor sem o recurso, 429 = cap de itens por run) —
|
|
935
|
+
* o RunReporter trata por status.
|
|
936
|
+
*/
|
|
937
|
+
async createItems(runId, request) {
|
|
938
|
+
let response;
|
|
939
|
+
try {
|
|
940
|
+
response = await fetchWithRetry(
|
|
941
|
+
() => fetch(`${this.baseUrl}/runs/${encodeURIComponent(runId)}/items`, {
|
|
942
|
+
method: "POST",
|
|
943
|
+
headers: this.headers(),
|
|
944
|
+
body: JSON.stringify(request),
|
|
945
|
+
signal: AbortSignal.timeout(UPDATE_TIMEOUT_MS)
|
|
946
|
+
}),
|
|
947
|
+
{ logger: this.logger, label: `itens da run ${runId}` }
|
|
948
|
+
);
|
|
949
|
+
} catch (error) {
|
|
950
|
+
if (error instanceof Error && error.message === "HTTP 429") {
|
|
951
|
+
throw new TasksApiError("envio de itens rejeitado (HTTP 429)", 429);
|
|
952
|
+
}
|
|
953
|
+
throw new Error(`envio de itens da run ${runId} descartado ap\xF3s retries: ${String(error)}`);
|
|
954
|
+
}
|
|
955
|
+
if (!response.ok) {
|
|
956
|
+
throw new TasksApiError(`envio de itens rejeitado (HTTP ${response.status})`, response.status);
|
|
957
|
+
}
|
|
958
|
+
const body = await response.json().catch(() => null);
|
|
959
|
+
return {
|
|
960
|
+
accepted: typeof body?.accepted === "number" ? body.accepted : request.items.length,
|
|
961
|
+
duplicates: typeof body?.duplicates === "number" ? body.duplicates : 0
|
|
962
|
+
};
|
|
963
|
+
}
|
|
932
964
|
/**
|
|
933
965
|
* Registra o anexo e obtém a URL presignada de upload. 404/501/503 significam
|
|
934
966
|
* "esta instalação não tem anexos" — o RunReporter degrada em silêncio a partir daí.
|
|
@@ -1086,6 +1118,22 @@ var MAX_CANCEL_CHECK_INTERVAL_MS = 15e3;
|
|
|
1086
1118
|
var MAX_LOG_MESSAGE_CHARS = 1e4;
|
|
1087
1119
|
var MAX_LOGS_PER_PATCH = 100;
|
|
1088
1120
|
var FLUSH_AT_BUFFERED = 20;
|
|
1121
|
+
var MAX_ITEMS_PER_RUN = 1e4;
|
|
1122
|
+
var MAX_ITEMS_PER_REQUEST = 500;
|
|
1123
|
+
var MAX_ITEM_MESSAGE_CHARS = 2e3;
|
|
1124
|
+
var MAX_ITEM_PAYLOAD_BYTES = 16384;
|
|
1125
|
+
var ITEMS_FLUSH_AT_BUFFERED = 100;
|
|
1126
|
+
var ITEMS_DRAIN_MS = 15e3;
|
|
1127
|
+
function scrubPayloadValue(value, depth) {
|
|
1128
|
+
if (typeof value === "string") return scrubSecrets(value);
|
|
1129
|
+
if (depth >= 4 || value === null || typeof value !== "object") return value;
|
|
1130
|
+
if (Array.isArray(value)) return value.map((entry) => scrubPayloadValue(entry, depth + 1));
|
|
1131
|
+
const out = {};
|
|
1132
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
1133
|
+
out[key] = scrubPayloadValue(entry, depth + 1);
|
|
1134
|
+
}
|
|
1135
|
+
return out;
|
|
1136
|
+
}
|
|
1089
1137
|
var RunReporter = class {
|
|
1090
1138
|
constructor(api, runId, task, logger, tx, flushIntervalMs, cancelCheckIntervalMs) {
|
|
1091
1139
|
this.api = api;
|
|
@@ -1104,11 +1152,18 @@ var RunReporter = class {
|
|
|
1104
1152
|
success: (message) => this.log("success", message)
|
|
1105
1153
|
};
|
|
1106
1154
|
tx.setLifecycleSink((level, message) => this.push(level, message));
|
|
1155
|
+
const items = {
|
|
1156
|
+
add: (item) => this.addItem(item.status, item.message, item.payload, item.id),
|
|
1157
|
+
succeeded: (payload, options) => this.addItem("succeeded", void 0, payload, options?.id),
|
|
1158
|
+
failed: (message, payload, options) => this.addItem("failed", message, payload, options?.id),
|
|
1159
|
+
occurrence: (message, payload, options) => this.addItem("occurrence", message, payload, options?.id)
|
|
1160
|
+
};
|
|
1107
1161
|
const signal = this.abortController.signal;
|
|
1108
1162
|
this.context = {
|
|
1109
1163
|
runId,
|
|
1110
1164
|
task,
|
|
1111
1165
|
log,
|
|
1166
|
+
items,
|
|
1112
1167
|
tx,
|
|
1113
1168
|
signal,
|
|
1114
1169
|
throwIfCanceled: () => {
|
|
@@ -1127,6 +1182,23 @@ var RunReporter = class {
|
|
|
1127
1182
|
abortController = new AbortController();
|
|
1128
1183
|
flushIntervalMs;
|
|
1129
1184
|
cancelCheckIntervalMs;
|
|
1185
|
+
// Itens de execução vivem FORA de `this.chain` (status/liveness não pode ficar
|
|
1186
|
+
// atrás de milhares de itens), mas têm cadeia própria serializada — ordem de
|
|
1187
|
+
// envio preservada e sem lotes sobrepostos.
|
|
1188
|
+
itemsBuffer = [];
|
|
1189
|
+
itemsChain = Promise.resolve();
|
|
1190
|
+
itemsTimer;
|
|
1191
|
+
itemSeq = 0;
|
|
1192
|
+
itemCount = 0;
|
|
1193
|
+
/** Servidor sem o recurso (404) — para de tentar (e de avisar) nesta run. */
|
|
1194
|
+
itemsUnsupported = false;
|
|
1195
|
+
/** Cap de itens por run atingido (client-side ou 429) — degrada com um aviso só. */
|
|
1196
|
+
itemsQuotaExceeded = false;
|
|
1197
|
+
/**
|
|
1198
|
+
* Só o kill() do exit path fecha os itens — `dead` (run cancelada/finalizada no
|
|
1199
|
+
* servidor) NÃO fecha: o servidor aceita itens de run terminal (trabalho já feito).
|
|
1200
|
+
*/
|
|
1201
|
+
itemsClosed = false;
|
|
1130
1202
|
// Uploads de anexo vivem FORA de `this.chain`: a cadeia é um FIFO puro de
|
|
1131
1203
|
// status/logs e não pode ficar bloqueada esperando um binário subir.
|
|
1132
1204
|
pendingAttachments = /* @__PURE__ */ new Set();
|
|
@@ -1153,6 +1225,12 @@ var RunReporter = class {
|
|
|
1153
1225
|
this.dead = true;
|
|
1154
1226
|
this.clearFlushTimer();
|
|
1155
1227
|
this.buffer = [];
|
|
1228
|
+
this.itemsClosed = true;
|
|
1229
|
+
this.clearItemsTimer();
|
|
1230
|
+
if (this.itemsBuffer.length > 0) {
|
|
1231
|
+
this.logger.warn(`${this.itemsBuffer.length} item(ns) de execu\xE7\xE3o descartados (processo encerrando)`);
|
|
1232
|
+
this.itemsBuffer = [];
|
|
1233
|
+
}
|
|
1156
1234
|
const waiting = this.uploadQueue.splice(0, this.uploadQueue.length);
|
|
1157
1235
|
for (const resume of waiting) resume();
|
|
1158
1236
|
}
|
|
@@ -1255,6 +1333,120 @@ var RunReporter = class {
|
|
|
1255
1333
|
finalize() {
|
|
1256
1334
|
return this.chain;
|
|
1257
1335
|
}
|
|
1336
|
+
// ── Itens de execução ────────────────────────────────────────────────────────
|
|
1337
|
+
/**
|
|
1338
|
+
* Bufferiza um item (síncrono — a chamada nunca espera rede). Diferente do PATCH
|
|
1339
|
+
* de status, o servidor aceita itens mesmo com a run cancelada (trabalho já feito),
|
|
1340
|
+
* então um reporter morto não descarta: só um kill() de exit path descarta.
|
|
1341
|
+
*/
|
|
1342
|
+
addItem(status, message, payload, id) {
|
|
1343
|
+
if (this.itemsClosed || this.itemsUnsupported) return;
|
|
1344
|
+
if (this.itemCount >= MAX_ITEMS_PER_RUN) {
|
|
1345
|
+
if (!this.itemsQuotaExceeded) {
|
|
1346
|
+
this.itemsQuotaExceeded = true;
|
|
1347
|
+
this.log("warning", `itens descartados: limite de ${MAX_ITEMS_PER_RUN} por execu\xE7\xE3o atingido`);
|
|
1348
|
+
}
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1351
|
+
this.itemCount++;
|
|
1352
|
+
let scrubbedPayload;
|
|
1353
|
+
if (payload) {
|
|
1354
|
+
scrubbedPayload = scrubPayloadValue(payload, 0);
|
|
1355
|
+
let serializedLength;
|
|
1356
|
+
try {
|
|
1357
|
+
serializedLength = JSON.stringify(scrubbedPayload).length;
|
|
1358
|
+
} catch {
|
|
1359
|
+
}
|
|
1360
|
+
if (serializedLength === void 0 || serializedLength > MAX_ITEM_PAYLOAD_BYTES) {
|
|
1361
|
+
this.logger.warn(
|
|
1362
|
+
`payload do item descartado (${serializedLength === void 0 ? "n\xE3o serializ\xE1vel" : `${serializedLength} bytes > ${MAX_ITEM_PAYLOAD_BYTES}`}) \u2014 o item foi mantido`
|
|
1363
|
+
);
|
|
1364
|
+
scrubbedPayload = void 0;
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
this.itemsBuffer.push({
|
|
1368
|
+
id: id ? truncate(String(id), 255) : uuid4(),
|
|
1369
|
+
seq: this.itemSeq++,
|
|
1370
|
+
status,
|
|
1371
|
+
...message ? { message: truncate(scrubSecrets(message), MAX_ITEM_MESSAGE_CHARS) } : {},
|
|
1372
|
+
...scrubbedPayload ? { payload: scrubbedPayload } : {},
|
|
1373
|
+
ts: nowISO()
|
|
1374
|
+
});
|
|
1375
|
+
if (this.itemsBuffer.length >= ITEMS_FLUSH_AT_BUFFERED) {
|
|
1376
|
+
this.flushItems();
|
|
1377
|
+
} else {
|
|
1378
|
+
this.scheduleItemsFlush();
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
scheduleItemsFlush() {
|
|
1382
|
+
if (this.itemsTimer) return;
|
|
1383
|
+
this.itemsTimer = setTimeout(() => {
|
|
1384
|
+
this.itemsTimer = void 0;
|
|
1385
|
+
this.flushItems();
|
|
1386
|
+
}, this.flushIntervalMs);
|
|
1387
|
+
unrefTimer(this.itemsTimer);
|
|
1388
|
+
}
|
|
1389
|
+
clearItemsTimer() {
|
|
1390
|
+
if (this.itemsTimer) {
|
|
1391
|
+
clearTimeout(this.itemsTimer);
|
|
1392
|
+
this.itemsTimer = void 0;
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
/** Envia o buffer de itens em lotes serializados na cadeia própria. */
|
|
1396
|
+
flushItems() {
|
|
1397
|
+
this.clearItemsTimer();
|
|
1398
|
+
if (this.itemsBuffer.length === 0 || this.itemsUnsupported) return;
|
|
1399
|
+
const items = this.itemsBuffer.splice(0, this.itemsBuffer.length);
|
|
1400
|
+
for (let i = 0; i < items.length; i += MAX_ITEMS_PER_REQUEST) {
|
|
1401
|
+
const batch = items.slice(i, i + MAX_ITEMS_PER_REQUEST);
|
|
1402
|
+
this.itemsChain = this.itemsChain.then(() => this.sendItems(batch));
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
async sendItems(batch) {
|
|
1406
|
+
if (this.itemsUnsupported) return;
|
|
1407
|
+
try {
|
|
1408
|
+
await this.api.createItems(this.runId, { items: batch });
|
|
1409
|
+
} catch (error) {
|
|
1410
|
+
if (error instanceof TasksApiError && error.status === 404) {
|
|
1411
|
+
this.itemsUnsupported = true;
|
|
1412
|
+
this.logger.warn("itens de execu\xE7\xE3o n\xE3o s\xE3o suportados por este servidor Hobots \u2014 descartados");
|
|
1413
|
+
return;
|
|
1414
|
+
}
|
|
1415
|
+
if (error instanceof TasksApiError && error.status === 429) {
|
|
1416
|
+
if (!this.itemsQuotaExceeded) {
|
|
1417
|
+
this.itemsQuotaExceeded = true;
|
|
1418
|
+
this.log("warning", "itens descartados: limite de itens por execu\xE7\xE3o atingido no servidor");
|
|
1419
|
+
}
|
|
1420
|
+
return;
|
|
1421
|
+
}
|
|
1422
|
+
if (error instanceof TasksApiError) {
|
|
1423
|
+
this.logger.error(`lote de ${batch.length} item(ns) rejeitado pelo servidor:`, error);
|
|
1424
|
+
return;
|
|
1425
|
+
}
|
|
1426
|
+
this.itemsBuffer.unshift(...batch);
|
|
1427
|
+
this.scheduleItemsFlush();
|
|
1428
|
+
this.logger.error(`lote de ${batch.length} item(ns) n\xE3o enviado (devolvido ao buffer):`, error);
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
/**
|
|
1432
|
+
* Dreno final de itens antes do PATCH terminal (com teto): garante que os
|
|
1433
|
+
* totalizadores estejam completos quando a run vira succeeded/failed. Como o
|
|
1434
|
+
* servidor aceita itens tardios, perder a corrida aqui não perde dados.
|
|
1435
|
+
*/
|
|
1436
|
+
async finalizeItems(timeoutMs = ITEMS_DRAIN_MS) {
|
|
1437
|
+
this.flushItems();
|
|
1438
|
+
await Promise.race([
|
|
1439
|
+
this.itemsChain,
|
|
1440
|
+
new Promise((resolve) => unrefTimer(setTimeout(resolve, timeoutMs)))
|
|
1441
|
+
]);
|
|
1442
|
+
if (this.itemsBuffer.length > 0 && !this.itemsUnsupported && !this.itemsQuotaExceeded) {
|
|
1443
|
+
this.flushItems();
|
|
1444
|
+
await Promise.race([
|
|
1445
|
+
this.itemsChain,
|
|
1446
|
+
new Promise((resolve) => unrefTimer(setTimeout(resolve, timeoutMs)))
|
|
1447
|
+
]);
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1258
1450
|
// ── Anexos ───────────────────────────────────────────────────────────────────
|
|
1259
1451
|
/**
|
|
1260
1452
|
* Registra a linha de log com o id do anexo (síncrono, na ordem certa) e dispara
|
|
@@ -1501,6 +1693,7 @@ var MIN_POLL_INTERVAL_MS = 1e3;
|
|
|
1501
1693
|
var MAX_CONCURRENCY = 5;
|
|
1502
1694
|
var POLL_RETRY_DELAYS_MS = [1e3, 4e3, 16e3];
|
|
1503
1695
|
var AUTH_FAILURE_INTERVAL_MS = 3e4;
|
|
1696
|
+
var DEFAULT_CANCEL_GRACE_MS = 1e4;
|
|
1504
1697
|
function hostname() {
|
|
1505
1698
|
if (!isNode()) return void 0;
|
|
1506
1699
|
return process.env.HOSTNAME || process.env.COMPUTERNAME || void 0;
|
|
@@ -1519,6 +1712,7 @@ var TaskAgent = class {
|
|
|
1519
1712
|
this.taskTimeoutMs = options.taskTimeoutMs;
|
|
1520
1713
|
this.logFlushIntervalMs = options.logFlushIntervalMs;
|
|
1521
1714
|
this.cancelCheckIntervalMs = options.cancelCheckIntervalMs;
|
|
1715
|
+
this.cancelGraceMs = options.cancelGraceMs ?? DEFAULT_CANCEL_GRACE_MS;
|
|
1522
1716
|
this.logger.debug("agente configurado", { baseUrl: config.tasksBaseUrl });
|
|
1523
1717
|
}
|
|
1524
1718
|
logger;
|
|
@@ -1530,6 +1724,7 @@ var TaskAgent = class {
|
|
|
1530
1724
|
taskTimeoutMs;
|
|
1531
1725
|
logFlushIntervalMs;
|
|
1532
1726
|
cancelCheckIntervalMs;
|
|
1727
|
+
cancelGraceMs;
|
|
1533
1728
|
state = "idle";
|
|
1534
1729
|
timer;
|
|
1535
1730
|
pollAbort;
|
|
@@ -1696,12 +1891,14 @@ var TaskAgent = class {
|
|
|
1696
1891
|
() => this.invokeWithTimeout(handler, run, reporter)
|
|
1697
1892
|
);
|
|
1698
1893
|
await reporter.finalizeAttachments();
|
|
1894
|
+
await reporter.finalizeItems();
|
|
1699
1895
|
tx.setStatus("ok");
|
|
1700
1896
|
tx.finish();
|
|
1701
1897
|
reporter.reportSuccess();
|
|
1702
1898
|
this.logger.debug(`run ${run.id} conclu\xEDda`);
|
|
1703
1899
|
} catch (error) {
|
|
1704
1900
|
await reporter.finalizeAttachments();
|
|
1901
|
+
await reporter.finalizeItems();
|
|
1705
1902
|
tx.setStatus("error");
|
|
1706
1903
|
tx.finish();
|
|
1707
1904
|
reporter.reportFailure(toRunError(error));
|
|
@@ -1714,14 +1911,26 @@ var TaskAgent = class {
|
|
|
1714
1911
|
}
|
|
1715
1912
|
async invokeWithTimeout(handler, run, reporter) {
|
|
1716
1913
|
const invocation = Promise.resolve().then(() => handler(run.params, reporter.context));
|
|
1717
|
-
|
|
1914
|
+
const races = [];
|
|
1915
|
+
if (this.taskTimeoutMs && this.taskTimeoutMs > 0) {
|
|
1916
|
+
races.push(sleep(this.taskTimeoutMs).then(() => TIMEOUT_SENTINEL));
|
|
1917
|
+
}
|
|
1918
|
+
if (this.cancelGraceMs > 0) {
|
|
1919
|
+
races.push(waitForCancelGrace(reporter.context.signal, this.cancelGraceMs).then(() => CANCEL_SENTINEL));
|
|
1920
|
+
}
|
|
1921
|
+
if (races.length === 0) {
|
|
1718
1922
|
await invocation;
|
|
1719
1923
|
return;
|
|
1720
1924
|
}
|
|
1925
|
+
const outcome = await Promise.race([invocation, ...races]);
|
|
1926
|
+
if (outcome !== TIMEOUT_SENTINEL && outcome !== CANCEL_SENTINEL) return;
|
|
1927
|
+
if (outcome === CANCEL_SENTINEL) {
|
|
1928
|
+
this.logger.warn(
|
|
1929
|
+
`run ${run.id} cancelada \u2014 handler n\xE3o respondeu a ctx.signal em ${this.cancelGraceMs}ms, liberando o slot (o handler pode continuar rodando em segundo plano)`
|
|
1930
|
+
);
|
|
1931
|
+
return;
|
|
1932
|
+
}
|
|
1721
1933
|
const timeoutMs = this.taskTimeoutMs;
|
|
1722
|
-
const timeout = sleep(timeoutMs).then(() => TIMEOUT_SENTINEL);
|
|
1723
|
-
const outcome = await Promise.race([invocation, timeout]);
|
|
1724
|
-
if (outcome !== TIMEOUT_SENTINEL) return;
|
|
1725
1934
|
reporter.abort();
|
|
1726
1935
|
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)`));
|
|
1727
1936
|
throw Object.assign(new Error(`timeout ap\xF3s ${timeoutMs}ms (o handler pode continuar em background)`), {
|
|
@@ -1730,6 +1939,16 @@ var TaskAgent = class {
|
|
|
1730
1939
|
}
|
|
1731
1940
|
};
|
|
1732
1941
|
var TIMEOUT_SENTINEL = /* @__PURE__ */ Symbol("task-timeout");
|
|
1942
|
+
var CANCEL_SENTINEL = /* @__PURE__ */ Symbol("task-canceled");
|
|
1943
|
+
function waitForCancelGrace(signal, graceMs) {
|
|
1944
|
+
return new Promise((resolve) => {
|
|
1945
|
+
if (signal.aborted) {
|
|
1946
|
+
setTimeout(resolve, graceMs);
|
|
1947
|
+
return;
|
|
1948
|
+
}
|
|
1949
|
+
signal.addEventListener("abort", () => setTimeout(resolve, graceMs), { once: true });
|
|
1950
|
+
});
|
|
1951
|
+
}
|
|
1733
1952
|
var MAX_ERROR_MESSAGE_CHARS = 1e4;
|
|
1734
1953
|
var MAX_ERROR_STACK_CHARS = 2e4;
|
|
1735
1954
|
function toRunError(error) {
|