Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 | 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 161x 106x 161x 8x 79x 79x 79x 79x 79x 79x 8x 8x 8x 41x 41x 41x 40x 40x 41x 41x 1x 1x 41x 41x 41x 8x 8x 8x 8x 8x 8x 8x 89x 89x 89x 89x 89x 89x 1x 1x 89x 89x 129x 129x 129x 129x 128x 129x 128x 128x 129x 129x 129x 129x 129x 129x 129x 129x 129x 129x 129x 57x 57x 57x 57x 57x 49x 123x 78x 78x 129x 129x 44x 44x 41x 41x 41x 41x 40x 41x 1x 1x 44x 3x 3x 44x 38x 38x 38x 38x 89x | // One logical REST call: attempts, retry decisions, backoff, total deadline, events and logs
// (DESIGN 2.2, 2.3, 2.7). Retries reuse the idempotency key; only timestamp, nonce and signature
// headers are rebuilt per attempt.
import { asYuguError, fromHttpResponse, isRetryable, localError } from './errors.js';
import { computeRetryDelay } from './retry.js';
import { wxRequest } from './transport.js';
import { onAbort } from './abort.js';
const TAG = 'YuguSDK:http';
export function describeError(err) {
if (err.httpStatus) return 'HTTP ' + err.httpStatus + ' code=' + err.code;
return err.category + ' code=' + err.code;
}
function decorate(err, spec, attempt) {
err.attempts = attempt;
if (!err.idempotencyKey && spec.key) err.idempotencyKey = spec.key;
if (!err.op) err.op = spec.op;
return err;
}
/** Sleeps ms, rejecting early with cancelError() when the signal aborts. */
export function sleep(ms, signal, cancelError) {
return new Promise((resolve, reject) => {
let off = null;
const t = setTimeout(() => {
if (off) off();
resolve();
}, ms);
off = onAbort(signal, () => {
clearTimeout(t);
reject(cancelError());
});
});
}
/**
* ctx: {wx, logger, events, random}
* spec: {op, method, path, url, key, retryAllowed, policy, attemptTimeoutMs, totalTimeoutMs, signal,
* cancelError(), build(attempt) -> {header, data}, parse(response, attempt) -> result}
*/
export async function executeCall(ctx, spec) {
const logger = ctx.logger;
const events = ctx.events;
const startedAt = Date.now();
const deadline = startedAt + spec.totalTimeoutMs;
const policy = spec.policy;
if (!spec.retryAllowed && policy.maxRetries > 0) {
logger.debug(TAG, spec.op + ': retries disabled because the call carries no idempotency key');
}
let attempt = 0;
for (;;) {
attempt += 1;
let err;
try {
if (spec.signal && spec.signal.aborted) throw spec.cancelError();
const remaining = deadline - Date.now();
if (remaining <= 0) throw localError(90002, 'total timeout of ' + spec.totalTimeoutMs + ' ms exceeded');
const req = spec.build(attempt);
events.emit('onRequestStart', spec.op, spec.method, spec.path, attempt);
logger.debug(TAG, spec.method + ' ' + spec.path + ' attempt ' + attempt + (spec.key ? ' idempotencyKey=' + spec.key : ''));
const res = await wxRequest(ctx.wx, {
url: spec.url,
method: spec.method,
header: req.header,
data: req.data,
timeoutMs: Math.max(1, Math.min(spec.attemptTimeoutMs, remaining)),
signal: spec.signal,
cancelError: spec.cancelError,
});
if (res.statusCode >= 200 && res.statusCode < 300) {
const out = spec.parse(res, attempt);
events.emit('onRequestEnd', spec.op, res.statusCode, Date.now() - startedAt, attempt, undefined);
logger.debug(TAG, spec.op + ' done: HTTP ' + res.statusCode + ' after ' + attempt + ' attempt(s)');
return out;
}
throw fromHttpResponse(res.statusCode, res.text, { headers: res.header, idempotencyKey: spec.key, op: spec.op });
} catch (e) {
err = decorate(asYuguError(e), spec, attempt);
}
const cancelled = err.category === 'CANCELLED' || err.code === 90004;
if (!cancelled && spec.retryAllowed && attempt <= policy.maxRetries && isRetryable(err)) {
const delay = computeRetryDelay(attempt, policy, ctx.random, err.retryAfterMs);
if (Date.now() + delay <= deadline) {
logger.warn(TAG, 'retry ' + attempt + '/' + policy.maxRetries + ' in ' + delay + ' ms: ' + describeError(err));
events.emit('onRetry', spec.op, attempt, delay, err);
try {
await sleep(delay, spec.signal, spec.cancelError);
continue;
} catch (e2) {
err = decorate(asYuguError(e2), spec, attempt);
}
} else {
logger.warn(TAG, spec.op + ': no retry, the next attempt would pass the total timeout of ' + spec.totalTimeoutMs + ' ms');
}
}
events.emit('onRequestEnd', spec.op, err.httpStatus, Date.now() - startedAt, attempt, err);
logger.info(TAG, spec.op + ' failed after ' + attempt + ' attempt(s): ' + describeError(err) + ' ' + err.message);
throw err;
}
}
|