cloudbroker-sdk 0.9.0 → 0.9.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 +17 -0
- package/package.json +1 -1
- package/src/client.js +2 -2
- package/src/cloud-wrapper.js +12 -3
- package/src/executions.js +54 -7
- package/src/observe.js +29 -0
- package/src/operation.js +4 -3
- package/src/test-executions.js +93 -0
- package/src/test-operations.js +1 -1
package/README.md
CHANGED
|
@@ -349,6 +349,23 @@ await CloudBrokerCloud.trackExecution("job:fechamento-mensal", () => fecharMes()
|
|
|
349
349
|
de um deploy pela versão que de fato rodou.
|
|
350
350
|
- No shutdown: `await execucoes.flush(); execucoes.stop();`.
|
|
351
351
|
|
|
352
|
+
#### Falhas e duração (0.9.1)
|
|
353
|
+
|
|
354
|
+
Sem mudar nada no seu código, `expressMiddleware`, `trackExecution` e
|
|
355
|
+
`CloudBroker.operation` passam a informar, junto com a contagem, quantas
|
|
356
|
+
execuções **terminaram**, quantas **falharam** e a **duração** somada:
|
|
357
|
+
|
|
358
|
+
- requisição: termina quando a resposta é enviada; status 5xx = falha. Uma
|
|
359
|
+
requisição abortada antes de responder conta como execução, mas não como
|
|
360
|
+
terminada;
|
|
361
|
+
- `trackExecution`/`operation`: falha = o callback lançou ou a Promise rejeitou.
|
|
362
|
+
O retorno e o erro chegam intactos ao seu código.
|
|
363
|
+
|
|
364
|
+
Por isso a taxa de falha e a duração média são calculadas sobre as execuções
|
|
365
|
+
**terminadas**, não sobre o total. Duração por execução tem teto de 24 h.
|
|
366
|
+
|
|
367
|
+
Para contar à mão: `const fim = execucoes.count({ label }); … fim({ failed })`.
|
|
368
|
+
|
|
352
369
|
### Custo por operação (0.9.0)
|
|
353
370
|
|
|
354
371
|
Cada execução ganha um `operationId` (`op_` + UUID v7, gerado localmente). As chamadas de nuvem e de IA feitas durante a execução levam esse id, e o CloudBroker passa a somar o custo de **uma** execução (ex.: S3 + DynamoDB + OpenAI de um `POST /checkout`).
|
package/package.json
CHANGED
package/src/client.js
CHANGED
|
@@ -48,7 +48,7 @@ export class CloudBrokerClient {
|
|
|
48
48
|
try {
|
|
49
49
|
const payload = {
|
|
50
50
|
...event,
|
|
51
|
-
sdkVersion: "0.9.
|
|
51
|
+
sdkVersion: "0.9.1",
|
|
52
52
|
sentAt: new Date().toISOString(),
|
|
53
53
|
};
|
|
54
54
|
if (this.#cloudQueue.length < MAX_QUEUE_SIZE) {
|
|
@@ -104,7 +104,7 @@ export class CloudBrokerClient {
|
|
|
104
104
|
try {
|
|
105
105
|
const payload = {
|
|
106
106
|
...event,
|
|
107
|
-
sdkVersion: "0.9.
|
|
107
|
+
sdkVersion: "0.9.1",
|
|
108
108
|
sentAt: new Date().toISOString(),
|
|
109
109
|
};
|
|
110
110
|
|
package/src/cloud-wrapper.js
CHANGED
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
AZURE_COSMOS_ITEMS_OPS,
|
|
36
36
|
AZURE_COSMOS_ITEMS_ITERATOR_OPS,
|
|
37
37
|
} from "./pricing-cloud.js";
|
|
38
|
+
import { runObserved } from "./observe.js";
|
|
38
39
|
import { cbStore, childOperationIds, operationFields } from "./context.js";
|
|
39
40
|
import { resolveVersion } from "./executions.js";
|
|
40
41
|
|
|
@@ -249,7 +250,14 @@ export class CloudBrokerCloud {
|
|
|
249
250
|
// durante a requisição levam o mesmo operationId.
|
|
250
251
|
const ctx = { label: safe(resolveLabel), product: safe(resolveProduct), team: safe(resolveTeam), ...childOperationIds() };
|
|
251
252
|
try {
|
|
252
|
-
if (executions && typeof executions.count === "function")
|
|
253
|
+
if (executions && typeof executions.count === "function") {
|
|
254
|
+
const fim = executions.count(ctx);
|
|
255
|
+
// Fim da requisição: resposta enviada. 5xx = falha. Abortada antes
|
|
256
|
+
// de responder não chega aqui — conta só em executions, não em completed.
|
|
257
|
+
if (typeof fim === "function" && typeof res?.once === "function") {
|
|
258
|
+
res.once("finish", () => { try { fim({ failed: res.statusCode >= 500 }); } catch { /* nunca lança */ } });
|
|
259
|
+
}
|
|
260
|
+
}
|
|
253
261
|
} catch { /* contador nunca derruba a requisição */ }
|
|
254
262
|
cbStore.run(ctx, () => next());
|
|
255
263
|
};
|
|
@@ -270,10 +278,11 @@ export class CloudBrokerCloud {
|
|
|
270
278
|
static trackExecution(label, fn, options = {}) {
|
|
271
279
|
// Uma operação por execução (RFC-0026); dentro de outra operação, vira filha.
|
|
272
280
|
const ctx = { label, product: options.product, team: options.team, ...childOperationIds() };
|
|
281
|
+
let fim = null;
|
|
273
282
|
try {
|
|
274
|
-
if (options.executions && typeof options.executions.count === "function") options.executions.count(ctx);
|
|
283
|
+
if (options.executions && typeof options.executions.count === "function") fim = options.executions.count(ctx);
|
|
275
284
|
} catch { /* contador nunca derruba a execução */ }
|
|
276
|
-
return
|
|
285
|
+
return runObserved(ctx, fn, fim);
|
|
277
286
|
}
|
|
278
287
|
|
|
279
288
|
/**
|
package/src/executions.js
CHANGED
|
@@ -20,6 +20,14 @@
|
|
|
20
20
|
* • contagem com mais de 7 dias sem conseguir enviar é descartada (o backend
|
|
21
21
|
* recusa, e reenviar para sempre seria inútil).
|
|
22
22
|
*
|
|
23
|
+
* Falhas e duração (0.9.1, RFC-0026): `count()` devolve uma função de
|
|
24
|
+
* término. Chamada no fim (`fim({ failed })`), ela soma na MESMA combinação
|
|
25
|
+
* da hora de início: `completed`, `failures` e `durationMsSum` (relógio
|
|
26
|
+
* monotônico). Execução que nunca termina (requisição abortada, processo
|
|
27
|
+
* morto) conta em `count` e não em `completed` — por isso a taxa de falha e a
|
|
28
|
+
* duração média se calculam sobre `completed`. O expressMiddleware e o
|
|
29
|
+
* trackExecution chamam o término sozinhos.
|
|
30
|
+
*
|
|
23
31
|
* Uso:
|
|
24
32
|
* const execucoes = new ExecutionCounter({ apiKey: "cb-key-xxx" }); // version ← CLOUDBROKER_VERSION
|
|
25
33
|
* app.use(CloudBrokerCloud.expressMiddleware({ label: ..., executions: execucoes }));
|
|
@@ -29,6 +37,8 @@
|
|
|
29
37
|
const DEFAULT_EXECUTIONS_ENDPOINT = "https://api.cloudbroker.app.br/api/cloud/executions";
|
|
30
38
|
const HOUR_MS = 3_600_000;
|
|
31
39
|
const MAX_AGE_MS = 7 * 24 * HOUR_MS;
|
|
40
|
+
const MAX_DURATION_MS = 24 * HOUR_MS; // o backend recusa acima disso
|
|
41
|
+
const NOOP = () => {};
|
|
32
42
|
|
|
33
43
|
/** Versão do código do cliente: explícita > CLOUDBROKER_VERSION > null. */
|
|
34
44
|
export function resolveVersion(explicit) {
|
|
@@ -48,10 +58,13 @@ export class ExecutionCounter {
|
|
|
48
58
|
#debug;
|
|
49
59
|
#maxKeys;
|
|
50
60
|
#now;
|
|
61
|
+
#mono;
|
|
51
62
|
#pending = new Map();
|
|
52
63
|
#flushing = false;
|
|
53
64
|
#timer = null;
|
|
54
65
|
dropped = 0;
|
|
66
|
+
/** Términos que não couberam no teto de combinações (sem a execução perdida). */
|
|
67
|
+
droppedCompletions = 0;
|
|
55
68
|
|
|
56
69
|
/**
|
|
57
70
|
* @param {object} options
|
|
@@ -62,7 +75,7 @@ export class ExecutionCounter {
|
|
|
62
75
|
* @param {number} [options.maxKeys=200] — o backend recusa lote com mais de 200
|
|
63
76
|
* @param {boolean} [options.debug=false]
|
|
64
77
|
*/
|
|
65
|
-
constructor({ apiKey, version, endpoint = DEFAULT_EXECUTIONS_ENDPOINT, flushIntervalMs = 60_000, maxKeys = 200, debug = false, now } = {}) {
|
|
78
|
+
constructor({ apiKey, version, endpoint = DEFAULT_EXECUTIONS_ENDPOINT, flushIntervalMs = 60_000, maxKeys = 200, debug = false, now, monotonicNow } = {}) {
|
|
66
79
|
if (!apiKey) throw new Error("[CloudBroker] apiKey é obrigatório.");
|
|
67
80
|
this.#apiKey = apiKey;
|
|
68
81
|
this.#endpoint = endpoint;
|
|
@@ -70,6 +83,7 @@ export class ExecutionCounter {
|
|
|
70
83
|
this.#debug = debug;
|
|
71
84
|
this.#maxKeys = Math.max(1, Math.min(200, Math.floor(maxKeys) || 200));
|
|
72
85
|
this.#now = typeof now === "function" ? now : Date.now;
|
|
86
|
+
this.#mono = typeof monotonicNow === "function" ? monotonicNow : () => performance.now();
|
|
73
87
|
if (flushIntervalMs > 0) {
|
|
74
88
|
this.#timer = setInterval(() => { this.flush(); }, flushIntervalMs);
|
|
75
89
|
if (typeof this.#timer?.unref === "function") this.#timer.unref();
|
|
@@ -85,21 +99,48 @@ export class ExecutionCounter {
|
|
|
85
99
|
/**
|
|
86
100
|
* Soma uma execução do trecho. Nunca lança.
|
|
87
101
|
* @param {{label?: string, product?: string}} ctx
|
|
102
|
+
* @returns {(result?: {failed?: boolean}) => void} término da execução —
|
|
103
|
+
* soma completed/failures/durationMsSum; só a 1ª chamada conta. Sem rótulo
|
|
104
|
+
* (execução não contada), é uma função vazia.
|
|
88
105
|
*/
|
|
89
106
|
count(ctx) {
|
|
90
107
|
try {
|
|
91
108
|
const label = ctx?.label, product = ctx?.product;
|
|
92
|
-
if (typeof label !== "string" || !label.trim()) return;
|
|
109
|
+
if (typeof label !== "string" || !label.trim()) return NOOP;
|
|
93
110
|
const hour = hourStartIso(this.#now());
|
|
94
111
|
const prod = typeof product === "string" && product.trim() ? product : null;
|
|
95
112
|
const key = `${hour}\u0000${label}\u0000${prod || ""}`;
|
|
96
113
|
const cur = this.#pending.get(key);
|
|
97
|
-
if (cur)
|
|
98
|
-
if (this.#pending.size >= this.#maxKeys) { this.dropped++; return; }
|
|
99
|
-
this.#pending.set(key, { label, product: prod, version: this.#version, hour, count: 1 });
|
|
114
|
+
if (cur) cur.count++;
|
|
115
|
+
else if (this.#pending.size >= this.#maxKeys) { this.dropped++; return NOOP; }
|
|
116
|
+
else this.#pending.set(key, { label, product: prod, version: this.#version, hour, count: 1 });
|
|
117
|
+
const start = this.#mono();
|
|
118
|
+
let done = false;
|
|
119
|
+
return (result) => {
|
|
120
|
+
if (done) return;
|
|
121
|
+
done = true;
|
|
122
|
+
try { this.#complete(key, { label, product: prod, hour }, start, result?.failed === true); }
|
|
123
|
+
catch (err) { if (this.#debug) console.warn("[CloudBroker] executions finish error:", err.message); }
|
|
124
|
+
};
|
|
100
125
|
} catch (err) {
|
|
101
126
|
if (this.#debug) console.warn("[CloudBroker] executions.count error:", err.message);
|
|
127
|
+
return NOOP;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
#complete(key, base, start, failed) {
|
|
132
|
+
const d = this.#mono() - start;
|
|
133
|
+
const ms = Number.isFinite(d) && d > 0 ? Math.min(Math.round(d), MAX_DURATION_MS) : 0;
|
|
134
|
+
let it = this.#pending.get(key);
|
|
135
|
+
if (!it) {
|
|
136
|
+
// A combinação já foi enviada: o término vai num item só com fim (count 0).
|
|
137
|
+
if (this.#pending.size >= this.#maxKeys) { this.droppedCompletions++; return; }
|
|
138
|
+
it = { ...base, version: this.#version, count: 0 };
|
|
139
|
+
this.#pending.set(key, it);
|
|
102
140
|
}
|
|
141
|
+
it.completed = (it.completed || 0) + 1;
|
|
142
|
+
it.failures = (it.failures || 0) + (failed ? 1 : 0);
|
|
143
|
+
it.durationMsSum = (it.durationMsSum || 0) + ms;
|
|
103
144
|
}
|
|
104
145
|
|
|
105
146
|
/** Envia o pendente. Nunca lança; em falha transitória, a contagem volta para a fila. */
|
|
@@ -138,8 +179,14 @@ export class ExecutionCounter {
|
|
|
138
179
|
if (Date.parse(it.hour) < limite) { this.dropped += it.count; continue; }
|
|
139
180
|
const key = `${it.hour}\u0000${it.label}\u0000${it.product || ""}`;
|
|
140
181
|
const cur = this.#pending.get(key);
|
|
141
|
-
if (cur)
|
|
142
|
-
|
|
182
|
+
if (cur) {
|
|
183
|
+
cur.count += it.count;
|
|
184
|
+
if (it.completed) {
|
|
185
|
+
cur.completed = (cur.completed || 0) + it.completed;
|
|
186
|
+
cur.failures = (cur.failures || 0) + it.failures;
|
|
187
|
+
cur.durationMsSum = (cur.durationMsSum || 0) + it.durationMsSum;
|
|
188
|
+
}
|
|
189
|
+
} else if (this.#pending.size < this.#maxKeys) this.#pending.set(key, it);
|
|
143
190
|
else this.dropped += it.count;
|
|
144
191
|
}
|
|
145
192
|
}
|
package/src/observe.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Roda `fn` no contexto `ctx` e avisa o término da execução (0.9.1, RFC-0026).
|
|
3
|
+
*
|
|
4
|
+
* `fim` é a função devolvida por ExecutionCounter.count(); null = sem contador.
|
|
5
|
+
* Garantias: o retorno de `fn` (inclusive a MESMA Promise) e o erro de `fn`
|
|
6
|
+
* chegam intactos ao chamador; a observação nunca lança nem cria rejeição
|
|
7
|
+
* não tratada nova (o ramo `.then` trata as duas saídas).
|
|
8
|
+
*/
|
|
9
|
+
import { cbStore } from "./context.js";
|
|
10
|
+
|
|
11
|
+
export function runObserved(ctx, fn, fim) {
|
|
12
|
+
if (typeof fim !== "function") return cbStore.run(ctx, fn);
|
|
13
|
+
const end = (failed) => { try { fim({ failed }); } catch { /* nunca lança */ } };
|
|
14
|
+
let result;
|
|
15
|
+
try {
|
|
16
|
+
result = cbStore.run(ctx, fn);
|
|
17
|
+
} catch (err) {
|
|
18
|
+
end(true);
|
|
19
|
+
throw err;
|
|
20
|
+
}
|
|
21
|
+
let then = null;
|
|
22
|
+
try { then = result !== null && (typeof result === "object" || typeof result === "function") ? result.then : null; } catch { then = null; }
|
|
23
|
+
if (typeof then === "function") {
|
|
24
|
+
try { then.call(result, () => end(false), () => end(true)); } catch { end(false); }
|
|
25
|
+
} else {
|
|
26
|
+
end(false);
|
|
27
|
+
}
|
|
28
|
+
return result;
|
|
29
|
+
}
|
package/src/operation.js
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
* servidor), customerId e metadata (risco de dado pessoal — RFC-0026 §3).
|
|
23
23
|
*/
|
|
24
24
|
import { cbStore, childOperationIds, operationFields } from "./context.js";
|
|
25
|
+
import { runObserved } from "./observe.js";
|
|
25
26
|
|
|
26
27
|
const OPERATION_TYPES = new Set(["http", "business", "job", "queue", "graphql", "database", "ai", "custom"]);
|
|
27
28
|
|
|
@@ -51,7 +52,7 @@ export class CloudBroker {
|
|
|
51
52
|
throw new TypeError("[CloudBroker] operation(name, options, callback): callback deve ser uma função.");
|
|
52
53
|
}
|
|
53
54
|
|
|
54
|
-
let ctx = null;
|
|
55
|
+
let ctx = null, fim = null;
|
|
55
56
|
try {
|
|
56
57
|
const parent = cbStore.getStore() || {};
|
|
57
58
|
const o = options && typeof options === "object" ? options : {};
|
|
@@ -65,12 +66,12 @@ export class CloudBroker {
|
|
|
65
66
|
...childOperationIds(),
|
|
66
67
|
};
|
|
67
68
|
try {
|
|
68
|
-
if (o.executions && typeof o.executions.count === "function") o.executions.count(ctx);
|
|
69
|
+
if (o.executions && typeof o.executions.count === "function") fim = o.executions.count(ctx);
|
|
69
70
|
} catch { /* contador nunca derruba a operação */ }
|
|
70
71
|
} catch {
|
|
71
72
|
ctx = null;
|
|
72
73
|
}
|
|
73
|
-
return ctx ?
|
|
74
|
+
return ctx ? runObserved(ctx, callback, fim) : callback();
|
|
74
75
|
}
|
|
75
76
|
|
|
76
77
|
/**
|
package/src/test-executions.js
CHANGED
|
@@ -178,6 +178,99 @@ ok(resolveVersion("x".repeat(100)).length === 64, "versão com teto de 64");
|
|
|
178
178
|
ok(sent.filter((s) => s.url.endsWith("/api/cloud/events")).at(-1)?.body.events[0].version === "v9", "versão explícita do wrapAWS vence o env");
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
+
// ─── falhas e duração (0.9.1, RFC-0026) ──────────────────────────────────────
|
|
182
|
+
{
|
|
183
|
+
const { EventEmitter } = await import("node:events");
|
|
184
|
+
let mono = 1000;
|
|
185
|
+
const c = mk({ monotonicNow: () => mono });
|
|
186
|
+
const fim1 = c.count({ label: "checkout", product: "Loja" });
|
|
187
|
+
const fim2 = c.count({ label: "checkout", product: "Loja" });
|
|
188
|
+
c.count({ label: "checkout", product: "Loja" }); // em andamento: sem fim
|
|
189
|
+
mono += 120; fim1();
|
|
190
|
+
mono += 30; fim2({ failed: true });
|
|
191
|
+
fim1({ failed: true }); // 2ª chamada não conta
|
|
192
|
+
ok(typeof c.count({}) === "function", "count sem rótulo devolve término vazio (chamável)");
|
|
193
|
+
await c.flush();
|
|
194
|
+
let it = sent.at(-1).body.executions[0];
|
|
195
|
+
ok(it.count === 3 && it.completed === 2 && it.failures === 1 && it.durationMsSum === 270, `contagem 3, fim 2, 1 falha, duração somada (veio ${JSON.stringify(it)})`);
|
|
196
|
+
|
|
197
|
+
const fim3 = c.count({ label: "checkout", product: "Loja" });
|
|
198
|
+
await c.flush(); // início já enviado
|
|
199
|
+
mono += 50; fim3();
|
|
200
|
+
await c.flush();
|
|
201
|
+
it = sent.at(-1).body.executions[0];
|
|
202
|
+
ok(it.count === 0 && it.completed === 1 && it.durationMsSum === 50 && it.hour === "2026-10-12T15:00:00.000Z", "término depois do envio do início → item só com fim, na hora do INÍCIO");
|
|
203
|
+
|
|
204
|
+
const c0 = mk({ monotonicNow: () => mono });
|
|
205
|
+
c0.count({ label: "v1" });
|
|
206
|
+
await c0.flush();
|
|
207
|
+
ok(!("completed" in sent.at(-1).body.executions[0]), "sem término observado não manda completed (ausente ≠ 0)");
|
|
208
|
+
|
|
209
|
+
const cNeg = mk({ monotonicNow: () => NaN });
|
|
210
|
+
cNeg.count({ label: "n" })();
|
|
211
|
+
await cNeg.flush();
|
|
212
|
+
ok(sent.at(-1).body.executions[0].durationMsSum === 0, "relógio inválido → duração 0, nunca NaN");
|
|
213
|
+
let longa = 0; const cL = mk({ monotonicNow: () => longa });
|
|
214
|
+
const fimL = cL.count({ label: "longa" }); longa = 48 * 3_600_000; fimL();
|
|
215
|
+
await cL.flush();
|
|
216
|
+
ok(sent.at(-1).body.executions[0].durationMsSum === 24 * 3_600_000, "duração limitada a 24 h (teto do backend)");
|
|
217
|
+
|
|
218
|
+
const cR = mk({ monotonicNow: () => mono });
|
|
219
|
+
respond = () => ({ ok: false, status: 503 });
|
|
220
|
+
cR.count({ label: "r" })({ failed: true });
|
|
221
|
+
await cR.flush();
|
|
222
|
+
respond = () => ({ ok: true, status: 200 });
|
|
223
|
+
cR.count({ label: "r" })();
|
|
224
|
+
await cR.flush();
|
|
225
|
+
it = sent.at(-1).body.executions[0];
|
|
226
|
+
ok(it.count === 2 && it.completed === 2 && it.failures === 1, "falha transitória: fim volta à fila e soma com o seguinte");
|
|
227
|
+
|
|
228
|
+
// middleware: 'finish' com 5xx = falha; abortada (sem finish) não completa
|
|
229
|
+
const cm = mk({ monotonicNow: () => mono });
|
|
230
|
+
const mw = CloudBrokerCloud.expressMiddleware({ label: "mod", executions: cm });
|
|
231
|
+
const r1 = Object.assign(new EventEmitter(), { statusCode: 200 });
|
|
232
|
+
const r2 = Object.assign(new EventEmitter(), { statusCode: 503 });
|
|
233
|
+
const r3 = Object.assign(new EventEmitter(), { statusCode: 200 });
|
|
234
|
+
for (const r of [r1, r2, r3]) await new Promise((ok_) => mw({ method: "GET", path: "/" }, r, ok_));
|
|
235
|
+
r1.emit("finish"); r2.emit("finish"); r3.emit("close");
|
|
236
|
+
await cm.flush();
|
|
237
|
+
it = sent.at(-1).body.executions[0];
|
|
238
|
+
ok(it.count === 3 && it.completed === 2 && it.failures === 1, "middleware: 2xx ok, 5xx falha, abortada só em count");
|
|
239
|
+
|
|
240
|
+
// trackExecution e operation: sync, async, erro; retorno e erro intactos
|
|
241
|
+
const ct = mk({ monotonicNow: () => mono });
|
|
242
|
+
const p = Promise.resolve(9);
|
|
243
|
+
ok(CloudBrokerCloud.trackExecution("t", () => p, { executions: ct }) === p, "trackExecution devolve a MESMA Promise");
|
|
244
|
+
ok(CloudBrokerCloud.trackExecution("t", () => 1, { executions: ct }) === 1, "síncrono devolve o valor");
|
|
245
|
+
class E extends Error {}
|
|
246
|
+
let e1; try { CloudBrokerCloud.trackExecution("t", () => { throw new E("s"); }, { executions: ct }); } catch (e) { e1 = e; }
|
|
247
|
+
let e2; try { await CloudBrokerCloud.trackExecution("t", async () => { throw new E("a"); }, { executions: ct }); } catch (e) { e2 = e; }
|
|
248
|
+
ok(e1 instanceof E && e2 instanceof E, "erro síncrono e assíncrono propagam com o mesmo tipo");
|
|
249
|
+
const { CloudBroker } = await import("./operation.js");
|
|
250
|
+
await CloudBroker.operation("t", { executions: ct }, async () => 1);
|
|
251
|
+
let e3; try { await CloudBroker.operation("t", { executions: ct }, async () => { throw new E("o"); }); } catch (e) { e3 = e; }
|
|
252
|
+
ok(e3 instanceof E, "CloudBroker.operation propaga o erro");
|
|
253
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
254
|
+
await ct.flush();
|
|
255
|
+
it = sent.at(-1).body.executions[0];
|
|
256
|
+
ok(it.count === 6 && it.completed === 6 && it.failures === 3, `trackExecution + operation: 6 execuções, 3 falhas (veio ${it.count}/${it.completed}/${it.failures})`);
|
|
257
|
+
|
|
258
|
+
// nenhuma rejeição não tratada nova
|
|
259
|
+
const unhandled = [];
|
|
260
|
+
const h = (r) => unhandled.push(r);
|
|
261
|
+
process.on("unhandledRejection", h);
|
|
262
|
+
const pr = CloudBrokerCloud.trackExecution("t", async () => { throw new E("x"); }, { executions: ct });
|
|
263
|
+
await pr.catch(() => {});
|
|
264
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
265
|
+
process.off("unhandledRejection", h);
|
|
266
|
+
ok(unhandled.length === 0, "a observação não cria rejeição não tratada");
|
|
267
|
+
|
|
268
|
+
const quebrado = { count() { return () => { throw new Error("x"); }; } };
|
|
269
|
+
ok(CloudBrokerCloud.trackExecution("t", () => 5, { executions: quebrado }) === 5, "término que lança não derruba a execução");
|
|
270
|
+
const thenHostil = { get then() { throw new Error("x"); } };
|
|
271
|
+
ok(CloudBrokerCloud.trackExecution("t", () => thenHostil, { executions: ct }) === thenHostil, "retorno com 'then' hostil volta intacto");
|
|
272
|
+
}
|
|
273
|
+
|
|
181
274
|
// ─── versão do pacote ────────────────────────────────────────────────────────
|
|
182
275
|
{
|
|
183
276
|
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
package/src/test-operations.js
CHANGED
|
@@ -169,7 +169,7 @@ const ask = () => openai.chat.completions.create({ model: "gpt-4o-mini", message
|
|
|
169
169
|
{
|
|
170
170
|
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
171
171
|
const client = readFileSync(new URL("./client.js", import.meta.url), "utf8");
|
|
172
|
-
ok(pkg.version === "0.9.
|
|
172
|
+
ok(pkg.version === "0.9.1" && [...client.matchAll(/sdkVersion:\s*"([^"]+)"/g)].every((m) => m[1] === pkg.version), "sdkVersion 0.9.1 nos eventos");
|
|
173
173
|
}
|
|
174
174
|
|
|
175
175
|
console.log(`\n${pass} asserções ok — custo por operação`);
|