hobots 1.9.2 → 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/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;
@@ -167,11 +167,41 @@ interface RunLogger {
167
167
  error(message: string): void;
168
168
  success(message: string): void;
169
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
+ }
170
198
  interface RunContext {
171
199
  runId: string;
172
200
  task: string;
173
201
  /** Log bufferizado — enviado ao servidor em PATCHes incrementais com throttle */
174
202
  log: RunLogger;
203
+ /** Itens processados/gerados pela execução — a entrega do bot, com status e atributos */
204
+ items: RunItems;
175
205
  /**
176
206
  * Transaction de performance desta execução — já correlacionada à run e finalizada
177
207
  * automaticamente ao fim do handler. Use `ctx.tx.startChild(op, description)` pra medir
@@ -303,4 +333,4 @@ declare function close(timeoutMs?: number): Promise<boolean>;
303
333
 
304
334
  declare function scrubSecrets(text: string): string;
305
335
 
306
- 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;
@@ -167,11 +167,41 @@ interface RunLogger {
167
167
  error(message: string): void;
168
168
  success(message: string): void;
169
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
+ }
170
198
  interface RunContext {
171
199
  runId: string;
172
200
  task: string;
173
201
  /** Log bufferizado — enviado ao servidor em PATCHes incrementais com throttle */
174
202
  log: RunLogger;
203
+ /** Itens processados/gerados pela execução — a entrega do bot, com status e atributos */
204
+ items: RunItems;
175
205
  /**
176
206
  * Transaction de performance desta execução — já correlacionada à run e finalizada
177
207
  * automaticamente ao fim do handler. Use `ctx.tx.startChild(op, description)` pra medir
@@ -303,4 +333,4 @@ declare function close(timeoutMs?: number): Promise<boolean>;
303
333
 
304
334
  declare function scrubSecrets(text: string): string;
305
335
 
306
- 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.9.1" : "0.0.0-dev";
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
@@ -1699,12 +1891,14 @@ var TaskAgent = class {
1699
1891
  () => this.invokeWithTimeout(handler, run, reporter)
1700
1892
  );
1701
1893
  await reporter.finalizeAttachments();
1894
+ await reporter.finalizeItems();
1702
1895
  tx.setStatus("ok");
1703
1896
  tx.finish();
1704
1897
  reporter.reportSuccess();
1705
1898
  this.logger.debug(`run ${run.id} conclu\xEDda`);
1706
1899
  } catch (error) {
1707
1900
  await reporter.finalizeAttachments();
1901
+ await reporter.finalizeItems();
1708
1902
  tx.setStatus("error");
1709
1903
  tx.finish();
1710
1904
  reporter.reportFailure(toRunError(error));