hobots 5.2.0 → 6.1.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 CHANGED
@@ -112,7 +112,7 @@ A opção passada em `register()`/`start()` tem precedência sobre a env — út
112
112
  | --- | --- |
113
113
  | `hobots.log.debug/info/warn/error/success(msg, ...rest)` | Log da execução, enviado incrementalmente ao app (e ecoado no terminal como `[hobots] INFO …`). Aceita vários argumentos como `console.log` — `hobots.log.info('nota', numero, 'ok')` — cada um vira texto, unidos por espaço. Um `hobots.image(...)` como **último** argumento pendura um print na linha. **Nunca lança** — pode ir em `finally`. |
114
114
  | `hobots.items.succeeded(payload?, { id, image }?)` · `.failed(msg, payload?, …)` · `.occurrence(msg, payload?, …)` · `.add({...})` | **A entrega do robô**: cada item processado, com status e atributos (viram colunas no app — declare-as com a opção `items: { columns: [...] }` do `register`/`start`). `image` pendura um print no item. Bufferizado, em lotes. |
115
- | `hobots.items.each(lista, async (el, item) => …, { id: 'campo', payload, retries: 1, retryDelayMs: 5000, skipDone: true }?)` | **Loop com o contrato garantido**: cada elemento vira etapa + item; exceção no handler = nova tentativa e, esgotada, item `failed` com a mensagem e o loop **segue** até o fim; `throw new Occurrence(msg)` = item `occurrence`, sem repetir; na reexecução do mesmo trabalho pula o que já deu `succeeded`/`occurrence`; progresso automático; `hobots.flush()` ao fim de cada ciclo (`flush: false` desliga). Só cancelamento/timeout interrompem. No handler, `item.data({ nf: 123 })` vai guardando os atributos do item sem decidir o desfecho — entram no `succeeded`/`failed`/`occurrence`, inclusive nos implícitos. |
115
+ | `hobots.items.each(lista, async (item, index?) => …, { id: 'campo', payload, retries: 1, retryDelayMs: 5000, skipDone: true }?)` | **Loop com o contrato garantido**: o handler recebe o handle do item (e, se declarar, o índice) — o elemento é `item.value` e a posição, `item.index`; sem `payload`, os campos do elemento (objeto simples) viram as colunas do item (`payload: false` desliga); cada elemento vira etapa + item; exceção no handler = nova tentativa e, esgotada, item `failed` com a mensagem e o loop **segue** até o fim; `throw new Occurrence(msg)` = item `occurrence`, sem repetir; na reexecução do mesmo trabalho pula o que já deu `succeeded`/`occurrence`; progresso automático; `hobots.flush()` ao fim de cada ciclo (`flush: false` desliga). Só cancelamento/timeout interrompem. No handler, `item.data({ nf: 123 })` vai guardando os atributos do item sem decidir o desfecho — entram no `succeeded`/`failed`/`occurrence`, inclusive nos implícitos. |
116
116
  | `hobots.progress.total(n)` · `.advance(k = 1)` · `.set(atual, total?)` · `.current` · `.snapshot` | Barra, percentual e tempo restante na execução. Sem `total()` nada é enviado. |
117
117
  | `hobots.flush(timeoutMs?)` | **Entrega já** o que a execução fez: as etapas raiz finalizadas viram transactions parciais (waterfall na Central sem esperar a run terminar) e itens, logs e progresso pendentes saem sem esperar o throttle. `step` (raiz) e cada ciclo do `items.each` já chamam sozinhos; à mão, para o resto da rodada de um agente `start()`. Aguarda com teto (15 s) e nunca lança. |
118
118
  | `hobots.step(nome, async etapa => …, { retries = 1, retryDelayMs = 5_000, flush = true }?)` | Uma **etapa** no waterfall de performance da execução: roda a função, **tenta de novo** se ela lançar (`retries: 0` desliga — use em etapas não idempotentes) e fecha a etapa. Resolve com o retorno da função. `hobots.step` dentro de outra vira sub-etapa. A etapa raiz faz `flush()` ao terminar (`flush: false` desliga). O handle `etapa` tem `warning(motivo, { image }?)`, `image(png)` e `data(chave, valor)`. |
@@ -231,6 +231,8 @@ Sem handler próprio de sinal, um `SIGINT`/`SIGTERM` (ou erro fatal) com execuç
231
231
 
232
232
  ## Changelog resumido
233
233
 
234
+ - **6.1.0** — cada item do `items.each` termina com uma linha `[item] FINISH <chave> (<ms>)` no log, no nível do desfecho (`success`/`error`/`warning`), no lugar do `[step] FINISH item — …`; `item.message` com o motivo. Detalhes no [CHANGELOG](./CHANGELOG.md).
235
+ - **6.0.0** — **breaking**: o handler de `items.each` recebe o handle do item, `async (item, index) => …` (`index` opcional); o elemento da lista é `item.value` e a posição, `item.index`. Migração: `async (conta, item, i) => …` → `async (item, i) => { const conta = item.value; … }`. `options.id`/`payload`/`description` continuam recebendo `(element, index)`. **Payload padrão:** sem `options.payload`, os campos do elemento (objeto simples, cópia rasa) viram o payload base do item; `options.payload` substitui, `payload: false` desliga. Detalhes no [CHANGELOG](./CHANGELOG.md).
234
236
  - **5.2.0** — **`item.data(payload)`** no handle do `items.each`: guarda atributos do item sem registrar desfecho; mescla a cada chamada e entra em qualquer desfecho (inclusive nos implícitos), entre o `options.payload` do `each` e o payload da chamada de desfecho; zera na nova tentativa. Detalhes no [CHANGELOG](./CHANGELOG.md).
235
237
  - **5.1.0** — `items.each` com **nova tentativa por item** (`retries: 1`, `retryDelayMs: 5000`; uma etapa só por elemento, `warning` quando a repetição deu certo, `error` + item `failed` quando esgotou), **`Occurrence`** exportada (`throw new Occurrence(msg, payload?, { image }?)` no handler = item `occurrence`, sem repetir), `id` por campo (`'numero'`), chave composta (`['cnpj', 'competencia']`) ou função — padrão: o campo `id` do elemento — e **reexecução sem repetir**: ao executar novamente o trabalho, pula os ids já `succeeded`/`occurrence` (`skipDone: true`; `POST /v1/runs/{id}/items/done`); resultado com `skipped`; `item.id` sempre `string`, `item.attempt`. Detalhes no [CHANGELOG](./CHANGELOG.md).
236
238
  - **5.0.0** — **uma API só de etapas** (breaking): `hobots.step(nome, fn, { retries, retryDelayMs }?)` abre a etapa, tenta de novo quando `fn` lança (default 1 tentativa extra) e fecha; `fn` recebe o handle `etapa` (`warning`/`image`/`data`); `hobots.step` dentro de outra (ou do handler do `items.each`) vira sub-etapa. `hobots.transaction`, `ctx.transaction` e os tipos `Transaction`/`Step`/`StepOutcomeOptions` foram removidos; `item.step` é o handle novo. Modo local ecoa `[hobots:step] FINISH …`. Nomes de etapa são recortados em 2048 no cliente. Flush automático ao fim da etapa raiz e de cada ciclo do `items.each` (`flush: false` desliga). Antes/depois no [CHANGELOG](./CHANGELOG.md).
package/dist/index.cjs CHANGED
@@ -45,7 +45,7 @@ module.exports = __toCommonJS(src_exports);
45
45
 
46
46
  // src/utils.ts
47
47
  var SDK_NAME = "hobots";
48
- var SDK_VERSION = true ? "5.2.0" : "0.0.0-dev";
48
+ var SDK_VERSION = true ? "6.1.0" : "0.0.0-dev";
49
49
  function isNode() {
50
50
  return typeof process !== "undefined" && typeof process.versions?.node === "string";
51
51
  }
@@ -1547,6 +1547,10 @@ function stepId() {
1547
1547
  function stepLabel(op, description) {
1548
1548
  return [op, description].filter(Boolean).join(" \u2014 ") || "(sem nome)";
1549
1549
  }
1550
+ var ITEM_OP = "item";
1551
+ function lifecycleLabel(kind, op, description) {
1552
+ return stepLabel(kind === "item" && op === ITEM_OP ? void 0 : op, description);
1553
+ }
1550
1554
  function errorText(error) {
1551
1555
  if (error instanceof Error) return error.message || error.name;
1552
1556
  return safeString(error);
@@ -1579,21 +1583,25 @@ function colorForStatus(status) {
1579
1583
  }
1580
1584
  }
1581
1585
  var Step = class {
1582
- constructor(transaction, parentStepId, op, description) {
1586
+ constructor(transaction, parentStepId, op, description, kind = "step") {
1583
1587
  this.transaction = transaction;
1584
1588
  this.parentStepId = parentStepId;
1585
1589
  this.op = op;
1586
1590
  this.description = description;
1591
+ this.kind = kind;
1587
1592
  }
1588
1593
  transaction;
1589
1594
  parentStepId;
1590
1595
  op;
1591
1596
  description;
1597
+ kind;
1592
1598
  stepId = stepId();
1593
1599
  startTimestamp = nowISO();
1594
1600
  status = "ok";
1595
1601
  endTimestamp;
1596
1602
  _data;
1603
+ // desfecho que a linha de FINISH reporta no lugar do status da etapa (item do `each`)
1604
+ outcome;
1597
1605
  // ── API do bot ────────────────────────────────────────────────────────────
1598
1606
  warning(message, options) {
1599
1607
  if (this.status !== "error") this._setStatus("warning", message);
@@ -1632,6 +1640,13 @@ var Step = class {
1632
1640
  this._close();
1633
1641
  return this;
1634
1642
  }
1643
+ /**
1644
+ * Faz a linha de FINISH reportar `status`/`reason` em vez do status da etapa — o
1645
+ * `items.each` usa para o log do item seguir o desfecho dele (sucesso/falha/ocorrência).
1646
+ */
1647
+ _setOutcome(status, reason) {
1648
+ this.outcome = { status, reason };
1649
+ }
1635
1650
  _setData(key, value) {
1636
1651
  this._data = { ...this._data, [key]: value };
1637
1652
  }
@@ -1663,14 +1678,15 @@ var Step = class {
1663
1678
  _close() {
1664
1679
  if (this.endTimestamp) return;
1665
1680
  this.endTimestamp = nowISO();
1681
+ const { status, reason } = this.outcome ?? { status: this.status, reason: this.reason() };
1666
1682
  this.transaction.emit(
1667
- "step",
1683
+ this.kind,
1668
1684
  "finish",
1669
- stepLabel(this.op, this.description),
1685
+ lifecycleLabel(this.kind, this.op, this.description),
1670
1686
  this.startTimestamp,
1671
1687
  this.endTimestamp,
1672
- this.status,
1673
- this.reason()
1688
+ status,
1689
+ reason
1674
1690
  );
1675
1691
  }
1676
1692
  toJSON() {
@@ -1725,7 +1741,7 @@ var Transaction = class {
1725
1741
  emit(kind, phase, label, startTs, endTs, status, reason) {
1726
1742
  const durationMs = startTs && endTs ? new Date(endTs).getTime() - new Date(startTs).getTime() : void 0;
1727
1743
  const suffix = (durationMs != null ? ` (${durationMs}ms)` : "") + (reason ? `: ${reason}` : "");
1728
- const localFinish = this.echo && kind === "step" && phase === "finish";
1744
+ const localFinish = this.echo && kind !== "transaction" && phase === "finish";
1729
1745
  if (this.client?.debug || localFinish) {
1730
1746
  const tag = colorize(`[hobots:${kind}] ${phase.toUpperCase()}`, phase === "finish" ? colorForStatus(status ?? "ok") : "cyan");
1731
1747
  console.log(`${tag} ${label}${suffix}`);
@@ -1772,13 +1788,16 @@ var Transaction = class {
1772
1788
  if (!step) return fn();
1773
1789
  return runWithStepScope({ transaction: this, step }, fn);
1774
1790
  }
1775
- /** Abre uma etapa sob a corrente (ou direta da transaction). Interno: o bot usa `step()`. */
1776
- start(op, description) {
1791
+ /**
1792
+ * Abre uma etapa sob a corrente (ou direta da transaction). Interno: o bot usa `step()`; o
1793
+ * `items.each` abre com `kind: 'item'` (linhas `[item] START/FINISH`).
1794
+ */
1795
+ start(op, description, kind = "step") {
1777
1796
  this.stepGuard?.();
1778
1797
  const parent = this.currentStep();
1779
- const step = new Step(this, parent ? parent.stepId : this.stepId, op, description);
1798
+ const step = new Step(this, parent ? parent.stepId : this.stepId, op, description, kind);
1780
1799
  this.steps.push(step);
1781
- this.emit("step", "start", stepLabel(op, description));
1800
+ this.emit(kind, "start", lifecycleLabel(kind, op, description));
1782
1801
  return step;
1783
1802
  }
1784
1803
  /**
@@ -1925,13 +1944,15 @@ function thrownMessage(error) {
1925
1944
  return safeString(error);
1926
1945
  }
1927
1946
  var ItemHandle = class {
1928
- constructor(index, id, step, basePayload, sink) {
1947
+ constructor(value, index, id, step, basePayload2, sink) {
1948
+ this.value = value;
1929
1949
  this.index = index;
1930
1950
  this.step = step;
1931
- this.basePayload = basePayload;
1951
+ this.basePayload = basePayload2;
1932
1952
  this.sink = sink;
1933
1953
  this.id = id ?? uuid4();
1934
1954
  }
1955
+ value;
1935
1956
  index;
1936
1957
  step;
1937
1958
  basePayload;
@@ -1940,6 +1961,7 @@ var ItemHandle = class {
1940
1961
  id;
1941
1962
  reported = false;
1942
1963
  status;
1964
+ message;
1943
1965
  attempt = 1;
1944
1966
  accumulated = {};
1945
1967
  data(payload) {
@@ -1961,6 +1983,7 @@ var ItemHandle = class {
1961
1983
  this.attempt = attempt;
1962
1984
  this.reported = false;
1963
1985
  this.status = void 0;
1986
+ this.message = void 0;
1964
1987
  this.accumulated = {};
1965
1988
  if (this.step && (this.step.status === "error" || this.step.status === "warning")) {
1966
1989
  this.step._setStatus("ok");
@@ -1970,10 +1993,23 @@ var ItemHandle = class {
1970
1993
  record(status, message, payload, image3) {
1971
1994
  this.reported = true;
1972
1995
  this.status = status;
1996
+ this.message = message;
1973
1997
  const merged = { ...this.basePayload ?? {}, ...this.accumulated, ...payload ?? {} };
1974
1998
  this.sink.report(status, message, Object.keys(merged).length > 0 ? merged : void 0, this.id, image3);
1975
1999
  }
1976
2000
  };
2001
+ function defaultPayload(element) {
2002
+ if (typeof element !== "object" || element === null) return void 0;
2003
+ const proto = Object.getPrototypeOf(element);
2004
+ if (proto !== Object.prototype && proto !== null) return void 0;
2005
+ const copy = { ...element };
2006
+ return Object.keys(copy).length > 0 ? copy : void 0;
2007
+ }
2008
+ function basePayload(spec, element, index) {
2009
+ if (spec === false) return void 0;
2010
+ if (spec === void 0) return defaultPayload(element);
2011
+ return spec(element, index);
2012
+ }
1977
2013
  function field(element, name) {
1978
2014
  if (typeof element !== "object" || element === null) return { present: false, value: void 0 };
1979
2015
  const record = element;
@@ -2009,6 +2045,10 @@ function knownLength(list) {
2009
2045
  const length = list?.length;
2010
2046
  return typeof length === "number" && Number.isInteger(length) && length >= 1 ? length : void 0;
2011
2047
  }
2048
+ var ITEM_LOG_STATUS = { succeeded: "ok", occurrence: "warning", failed: "error" };
2049
+ function setItemOutcome(step, status, message) {
2050
+ step._setOutcome(ITEM_LOG_STATUS[status], status === "succeeded" ? void 0 : message);
2051
+ }
2012
2052
  function skippedMessage(count) {
2013
2053
  if (count === 1) return "1 item pulado: j\xE1 conclu\xEDdo neste trabalho numa execu\xE7\xE3o anterior";
2014
2054
  return `${count} itens pulados: j\xE1 conclu\xEDdos neste trabalho numa execu\xE7\xE3o anterior`;
@@ -2018,23 +2058,34 @@ async function eachItem(list, handler, options, sink) {
2018
2058
  const requested = options.retries ?? DEFAULT_EACH_RETRIES;
2019
2059
  const totalRetries = Number.isFinite(requested) ? Math.max(0, Math.floor(requested)) : DEFAULT_EACH_RETRIES;
2020
2060
  const explicitId = options.id !== void 0;
2021
- const length = options.progress === false ? void 0 : knownLength(list);
2022
- if (length !== void 0) sink.declareTotal(length);
2023
2061
  const done = options.skipDone === false ? void 0 : await sink.doneItemIds();
2024
- let index = 0;
2025
- for await (const element of list) {
2062
+ const isDone = (id) => id !== void 0 && done !== void 0 && done.has(id);
2063
+ const length = knownLength(list);
2064
+ let entries;
2065
+ if (length !== void 0 && done !== void 0 && done.size > 0) {
2066
+ const pending = [];
2067
+ let position = 0;
2068
+ for await (const element of list) {
2069
+ const id = resolveId(options.id, element, position, explicitId);
2070
+ if (isDone(id)) result.skipped += 1;
2071
+ else pending.push({ element, index: position, id });
2072
+ position += 1;
2073
+ }
2074
+ if (options.progress !== false && pending.length > 0) sink.declareTotal(pending.length);
2075
+ entries = pending;
2076
+ } else {
2077
+ if (options.progress !== false && length !== void 0) sink.declareTotal(length);
2078
+ entries = lazyEntries(list, options.id, explicitId);
2079
+ }
2080
+ for await (const { element, index, id } of entries) {
2026
2081
  sink.checkCanceled();
2027
- const id = resolveId(options.id, element, index, explicitId);
2028
- if (done?.size && id !== void 0 && done.has(id)) {
2029
- result.total += 1;
2082
+ if (isDone(id)) {
2030
2083
  result.skipped += 1;
2031
- if (options.progress !== false) sink.advance();
2032
- index += 1;
2033
2084
  continue;
2034
2085
  }
2035
2086
  const description = options.description?.(element, index) ?? id ?? `#${index + 1}`;
2036
2087
  const step = sink.startStep(options.op ?? "item", description);
2037
- const handle = new ItemHandle(index, id, step, options.payload?.(element, index), sink);
2088
+ const handle = new ItemHandle(element, index, id, step, basePayload(options.payload, element, index), sink);
2038
2089
  const current = index;
2039
2090
  let attempts = 0;
2040
2091
  let lastFailure;
@@ -2043,7 +2094,7 @@ async function eachItem(list, handler, options, sink) {
2043
2094
  (attempt) => {
2044
2095
  attempts = attempt;
2045
2096
  if (attempt > 1) handle._nextAttempt(attempt);
2046
- return sink.stepScope(step, async () => handler(element, handle, current));
2097
+ return sink.stepScope(step, async () => handler(handle, current));
2047
2098
  },
2048
2099
  {
2049
2100
  retries: totalRetries,
@@ -2079,20 +2130,34 @@ async function eachItem(list, handler, options, sink) {
2079
2130
  } else {
2080
2131
  const message = thrownMessage(error);
2081
2132
  if (!handle.reported) handle.failed(message);
2082
- step?._failed(error);
2133
+ if (step) {
2134
+ setItemOutcome(step, handle.status ?? "failed", handle.message);
2135
+ step._failed(error);
2136
+ }
2083
2137
  sink.logError(`item ${description} falhou: ${message}`);
2084
2138
  }
2085
2139
  }
2086
- step?._close();
2140
+ const final = handle.status ?? "succeeded";
2141
+ if (step) {
2142
+ setItemOutcome(step, final, handle.message);
2143
+ step._close();
2144
+ }
2087
2145
  if (options.progress !== false) sink.advance();
2088
2146
  result.total += 1;
2089
- result[handle.status ?? "succeeded"] += 1;
2090
- index += 1;
2147
+ result[final] += 1;
2091
2148
  if (options.flush !== false) await sink.flush();
2092
2149
  }
2150
+ result.total += result.skipped;
2093
2151
  if (result.skipped > 0) sink.logInfo(skippedMessage(result.skipped));
2094
2152
  return result;
2095
2153
  }
2154
+ async function* lazyEntries(list, spec, explicit) {
2155
+ let index = 0;
2156
+ for await (const element of list) {
2157
+ yield { element, index, id: resolveId(spec, element, index, explicit) };
2158
+ index += 1;
2159
+ }
2160
+ }
2096
2161
  function noteAttempts(step, attempts) {
2097
2162
  if (attempts > 1) step._setData("attempts", attempts);
2098
2163
  }
@@ -2478,7 +2543,7 @@ var RunReporter = class {
2478
2543
  tx.setStepGuard(checkCanceled);
2479
2544
  const eachSink = {
2480
2545
  report: (status, message, payload, id, image3) => this.addItem(status, message, payload, id, image3),
2481
- startStep: (op, description) => tx.start(op, description),
2546
+ startStep: (op, description) => tx.start(op, description, "item"),
2482
2547
  stepScope: (step, fn) => tx.scope(step, fn),
2483
2548
  declareTotal: (total) => {
2484
2549
  if (this.progressTotal == null) this.setProgress(this.progressCurrent, total);