@empereur-rouge/pms-sdk 0.3.6 → 0.3.8

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.cjs CHANGED
@@ -253,7 +253,10 @@ var DEFAULT_CONFIG = {
253
253
  protocolVersion: 1,
254
254
  timeout: 3e4,
255
255
  seedNodes: [],
256
- enableRacing: true
256
+ enableRacing: true,
257
+ retries: 2,
258
+ perAttemptTimeoutMs: 4e3,
259
+ retryBaseDelayMs: 100
257
260
  };
258
261
 
259
262
  // src/crypto.ts
@@ -365,14 +368,14 @@ var PmsClient = class {
365
368
  const res = await this.fetch("/v1/dag/tips", {
366
369
  method: "POST",
367
370
  body: JSON.stringify({ limit: 10 })
368
- });
371
+ }, { idempotent: true });
369
372
  return res;
370
373
  }
371
374
  /**
372
375
  * Récupère les informations publiques du coordinateur (clés).
373
376
  */
374
377
  async getCoordinatorInfo() {
375
- return this.fetch("/v1/coordinator/info");
378
+ return this.fetch("/v1/coordinator/info", void 0, { idempotent: true });
376
379
  }
377
380
  /**
378
381
  * Liste tous les ledgers actifs avec leur nombre de blocs.
@@ -396,13 +399,13 @@ var PmsClient = class {
396
399
  }
397
400
  const qs = params.toString();
398
401
  const path = `/v1/ledgers${qs ? `?${qs}` : ""}`;
399
- return (await this.fetch(path)).ledgers ?? [];
402
+ return (await this.fetch(path, void 0, { idempotent: true })).ledgers ?? [];
400
403
  }
401
404
  /**
402
405
  * Récupère un bloc par son ID.
403
406
  */
404
407
  async getBlock(blockId) {
405
- const wb = await this.fetch(`/v1/blocks/${blockId}`);
408
+ const wb = await this.fetch(`/v1/blocks/${blockId}`, void 0, { idempotent: true });
406
409
  return {
407
410
  id: wb.id,
408
411
  parents: wb.parents,
@@ -416,26 +419,26 @@ var PmsClient = class {
416
419
  */
417
420
  async getSupply(assetId) {
418
421
  const query = assetId ? `?asset_id=${encodeURIComponent(assetId)}` : "";
419
- return this.fetch(`/v1/supply${query}`);
422
+ return this.fetch(`/v1/supply${query}`, void 0, { idempotent: true });
420
423
  }
421
424
  /**
422
425
  * Liste tous les tokens enregistrés.
423
426
  */
424
427
  async listTokens() {
425
- const res = await this.fetch("/v1/tokens");
428
+ const res = await this.fetch("/v1/tokens", void 0, { idempotent: true });
426
429
  return res.tokens ?? [];
427
430
  }
428
431
  /**
429
432
  * Récupère les métadonnées d'un token par son asset_id.
430
433
  */
431
434
  async getToken(assetId) {
432
- return this.fetch(`/v1/tokens/${encodeURIComponent(assetId)}`);
435
+ return this.fetch(`/v1/tokens/${encodeURIComponent(assetId)}`, void 0, { idempotent: true });
433
436
  }
434
437
  /**
435
438
  * Récupère les UTXOs d'une adresse.
436
439
  */
437
440
  async getUtxos(address) {
438
- const res = await this.fetch(`/v1/wallet/${address}/utxos`);
441
+ const res = await this.fetch(`/v1/wallet/${address}/utxos`, void 0, { idempotent: true });
439
442
  return res.utxos ?? [];
440
443
  }
441
444
  // ═══════════════════════════════════════════════════════════════════════
@@ -478,7 +481,7 @@ var PmsClient = class {
478
481
  return this.fetch("/v1/wallet/restore/mnemonic", {
479
482
  method: "POST",
480
483
  body: JSON.stringify({ mnemonic })
481
- });
484
+ }, { idempotent: true });
482
485
  }
483
486
  /**
484
487
  * Restaure un wallet depuis une clé privée hexadécimale.
@@ -498,7 +501,7 @@ var PmsClient = class {
498
501
  return this.fetch("/v1/wallet/restore/private-key", {
499
502
  method: "POST",
500
503
  body: JSON.stringify({ private_key_hex: privateKeyHex })
501
- });
504
+ }, { idempotent: true });
502
505
  }
503
506
  // ═══════════════════════════════════════════════════════════════════════
504
507
  // Préparation de Transaction (Server-Side)
@@ -533,7 +536,7 @@ var PmsClient = class {
533
536
  return this.fetch("/v1/tx/prepare", {
534
537
  method: "POST",
535
538
  body: JSON.stringify(params)
536
- });
539
+ }, { idempotent: true });
537
540
  }
538
541
  /**
539
542
  * Récupère la balance d'une adresse.
@@ -545,7 +548,7 @@ var PmsClient = class {
545
548
  const res = await this.fetch("/v1/balance", {
546
549
  method: "POST",
547
550
  body: JSON.stringify({ address })
548
- });
551
+ }, { idempotent: true });
549
552
  return res.balance;
550
553
  }
551
554
  /**
@@ -579,7 +582,7 @@ var PmsClient = class {
579
582
  * ```
580
583
  */
581
584
  async getNfts(address) {
582
- const res = await this.fetch(`/v1/wallet/${address}/nfts`);
585
+ const res = await this.fetch(`/v1/wallet/${address}/nfts`, void 0, { idempotent: true });
583
586
  return res.token_ids ?? [];
584
587
  }
585
588
  /**
@@ -598,7 +601,7 @@ var PmsClient = class {
598
601
  * ```
599
602
  */
600
603
  async getNft(tokenId) {
601
- return this.fetch(`/v1/nft/${tokenId}`);
604
+ return this.fetch(`/v1/nft/${tokenId}`, void 0, { idempotent: true });
602
605
  }
603
606
  /**
604
607
  * Récupère l'historique des transactions d'un wallet.
@@ -613,7 +616,7 @@ var PmsClient = class {
613
616
  const res = await this.fetch("/wallet/history", {
614
617
  method: "POST",
615
618
  body: JSON.stringify({ bech32_addr: address, limit })
616
- });
619
+ }, { idempotent: true });
617
620
  if (options?.decryptionWallet) {
618
621
  const wallet = options.decryptionWallet;
619
622
  for (const item of res.items) {
@@ -706,7 +709,7 @@ var PmsClient = class {
706
709
  }
707
710
  const qs = params.toString();
708
711
  const path = `/v1/wallet/${encodeURIComponent(address)}/activity${qs ? `?${qs}` : ""}`;
709
- return this.fetch(path);
712
+ return this.fetch(path, void 0, { idempotent: true });
710
713
  }
711
714
  /**
712
715
  * Écoute l'activité d'un wallet en temps réel via Server-Sent Events (SSE).
@@ -810,7 +813,7 @@ var PmsClient = class {
810
813
  if (this.configCache && Date.now() < this.configCache.expires) {
811
814
  return this.configCache.data;
812
815
  }
813
- const cfg = await this.fetch("/v1/config");
816
+ const cfg = await this.fetch("/v1/config", void 0, { idempotent: true });
814
817
  this.configCache = { data: cfg, expires: Date.now() + this.CONFIG_TTL };
815
818
  return cfg;
816
819
  }
@@ -1257,14 +1260,18 @@ var PmsClient = class {
1257
1260
  // ═══════════════════════════════════════════════════════════════════════
1258
1261
  // Helper HTTP
1259
1262
  // ═══════════════════════════════════════════════════════════════════════
1260
- async fetch(path, init) {
1261
- return this.fetchUrl(this.config.nodeUrl, path, init);
1263
+ async fetch(path, init, opts) {
1264
+ return this.fetchUrl(this.config.nodeUrl, path, init, opts);
1262
1265
  }
1263
- async fetchUrl(baseUrl, path, init) {
1264
- const url = `${baseUrl.replace(/\/$/, "")}/${path.replace(/^\//, "")}`;
1265
- const controller = new AbortController();
1266
- const timeout = setTimeout(() => controller.abort(), this.config.timeout);
1267
- const signal = init?.signal || controller.signal;
1266
+ /**
1267
+ * Single-attempt HTTP request. Throws a tagged error on transient conditions
1268
+ * (network failure, per-attempt abort, 5xx) so the retry loop can decide
1269
+ * whether to retry. 4xx and other non-2xx are thrown as fatal HttpError.
1270
+ */
1271
+ async fetchOnce(url, init, attemptTimeoutMs, outerSignal) {
1272
+ const attemptController = new AbortController();
1273
+ const attemptTimer = setTimeout(() => attemptController.abort(), attemptTimeoutMs);
1274
+ const signal = mergeSignalsAny(attemptController.signal, outerSignal);
1268
1275
  try {
1269
1276
  const res = await fetch(url, {
1270
1277
  ...init,
@@ -1278,8 +1285,9 @@ var PmsClient = class {
1278
1285
  signal
1279
1286
  });
1280
1287
  if (!res.ok) {
1281
- const text2 = await res.text();
1282
- throw new Error(`HTTP ${res.status} (${url}): ${text2}`);
1288
+ const text2 = await res.text().catch(() => "");
1289
+ const retryAfter = res.headers?.get?.("Retry-After") ?? null;
1290
+ throw new HttpError(res.status, `HTTP ${res.status} (${url}): ${text2}`, retryAfter);
1283
1291
  }
1284
1292
  const text = await res.text();
1285
1293
  if (!text) {
@@ -1287,14 +1295,132 @@ var PmsClient = class {
1287
1295
  }
1288
1296
  try {
1289
1297
  return JSON.parse(text);
1290
- } catch (e) {
1298
+ } catch {
1291
1299
  throw new Error(`Invalid JSON response: ${text.substring(0, 50)}...`);
1292
1300
  }
1301
+ } catch (err) {
1302
+ if (err && err.name === "AbortError") {
1303
+ if (attemptController.signal.aborted && !(outerSignal && outerSignal.aborted)) {
1304
+ throw new PerAttemptTimeoutError(`Per-attempt timeout after ${attemptTimeoutMs}ms`);
1305
+ }
1306
+ }
1307
+ throw err;
1293
1308
  } finally {
1294
- clearTimeout(timeout);
1309
+ clearTimeout(attemptTimer);
1295
1310
  }
1296
1311
  }
1312
+ async fetchUrl(baseUrl, path, init, opts) {
1313
+ const url = `${baseUrl.replace(/\/$/, "")}/${path.replace(/^\//, "")}`;
1314
+ const outerSignal = init?.signal ?? void 0;
1315
+ if (!opts?.idempotent) {
1316
+ return this.fetchOnce(url, init, this.config.timeout, outerSignal);
1317
+ }
1318
+ const maxAttempts = 1 + Math.max(0, this.config.retries | 0);
1319
+ const perAttemptMs = this.config.perAttemptTimeoutMs;
1320
+ const baseDelayMs = this.config.retryBaseDelayMs;
1321
+ const overallDeadline = Date.now() + this.config.timeout;
1322
+ let lastErr;
1323
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
1324
+ if (outerSignal?.aborted) {
1325
+ const e = new Error("Request aborted by caller");
1326
+ e.name = "AbortError";
1327
+ throw e;
1328
+ }
1329
+ if (Date.now() > overallDeadline) {
1330
+ throw lastErr ?? new Error(`Overall timeout (${this.config.timeout}ms) exceeded`);
1331
+ }
1332
+ try {
1333
+ return await this.fetchOnce(url, init, perAttemptMs, outerSignal);
1334
+ } catch (err) {
1335
+ lastErr = err;
1336
+ if (outerSignal?.aborted) throw err;
1337
+ const retriable = isRetriableError(err);
1338
+ if (!retriable) throw err;
1339
+ if (attempt === maxAttempts - 1) throw err;
1340
+ let delay;
1341
+ const ra = err instanceof HttpError ? parseRetryAfterSeconds(err.retryAfter) : null;
1342
+ if (ra !== null) {
1343
+ delay = Math.min(ra * 1e3, 5e3);
1344
+ } else {
1345
+ const cap = Math.min(baseDelayMs * Math.pow(2, attempt), 2e3);
1346
+ delay = Math.floor(Math.random() * cap);
1347
+ }
1348
+ if (Date.now() + delay > overallDeadline) {
1349
+ throw err;
1350
+ }
1351
+ await sleepAbortable(delay, outerSignal);
1352
+ }
1353
+ }
1354
+ throw lastErr ?? new Error("Retry loop exhausted with no error captured");
1355
+ }
1356
+ };
1357
+ var HttpError = class extends Error {
1358
+ status;
1359
+ retryAfter;
1360
+ constructor(status, message, retryAfter) {
1361
+ super(message);
1362
+ this.name = "HttpError";
1363
+ this.status = status;
1364
+ this.retryAfter = retryAfter;
1365
+ }
1366
+ };
1367
+ var PerAttemptTimeoutError = class extends Error {
1368
+ constructor(message) {
1369
+ super(message);
1370
+ this.name = "PerAttemptTimeoutError";
1371
+ }
1297
1372
  };
1373
+ function isRetriableError(err) {
1374
+ if (err instanceof PerAttemptTimeoutError) return true;
1375
+ if (err instanceof HttpError) {
1376
+ return err.status === 502 || err.status === 503 || err.status === 504;
1377
+ }
1378
+ if (err instanceof TypeError) return true;
1379
+ return false;
1380
+ }
1381
+ function parseRetryAfterSeconds(raw) {
1382
+ if (!raw) return null;
1383
+ const n = Number(raw.trim());
1384
+ if (!Number.isFinite(n) || n < 0) return null;
1385
+ return n;
1386
+ }
1387
+ function mergeSignalsAny(attemptSignal, outerSignal) {
1388
+ if (!outerSignal) return attemptSignal;
1389
+ const anyFn = AbortSignal.any;
1390
+ if (typeof anyFn === "function") {
1391
+ return anyFn([attemptSignal, outerSignal]);
1392
+ }
1393
+ const merged = new AbortController();
1394
+ const onAbort = (sig) => () => {
1395
+ if (!merged.signal.aborted) merged.abort(sig.reason);
1396
+ };
1397
+ if (attemptSignal.aborted) merged.abort(attemptSignal.reason);
1398
+ else attemptSignal.addEventListener("abort", onAbort(attemptSignal), { once: true });
1399
+ if (outerSignal.aborted) merged.abort(outerSignal.reason);
1400
+ else outerSignal.addEventListener("abort", onAbort(outerSignal), { once: true });
1401
+ return merged.signal;
1402
+ }
1403
+ function sleepAbortable(ms, signal) {
1404
+ if (ms <= 0) return Promise.resolve();
1405
+ return new Promise((resolve) => {
1406
+ const t = setTimeout(() => {
1407
+ if (signal) signal.removeEventListener("abort", onAbort);
1408
+ resolve();
1409
+ }, ms);
1410
+ const onAbort = () => {
1411
+ clearTimeout(t);
1412
+ resolve();
1413
+ };
1414
+ if (signal) {
1415
+ if (signal.aborted) {
1416
+ clearTimeout(t);
1417
+ resolve();
1418
+ return;
1419
+ }
1420
+ signal.addEventListener("abort", onAbort, { once: true });
1421
+ }
1422
+ });
1423
+ }
1298
1424
  function mergeAbortSignals(a, b) {
1299
1425
  const controller = new AbortController();
1300
1426
  const onAbort = () => controller.abort();
package/dist/index.d.cts CHANGED
@@ -528,6 +528,12 @@ interface PmsClientConfig {
528
528
  timeout?: number;
529
529
  /** Activer le mode racing (envoie à tous les noeuds connus) (défaut: true) */
530
530
  enableRacing?: boolean;
531
+ /** Number of automatic retries for idempotent reads on transient failure. Default: 2 (so up to 3 attempts total). */
532
+ retries?: number;
533
+ /** Per-attempt timeout in ms. Default: 4000. Should be > p90 of normal latency but << overall `timeout`. */
534
+ perAttemptTimeoutMs?: number;
535
+ /** Base delay (ms) for exponential backoff with full jitter between retries. Default: 100. */
536
+ retryBaseDelayMs?: number;
531
537
  }
532
538
  /** Réponse de l'endpoint d'information du coordinateur */
533
539
  interface CoordinatorInfoResponse {
@@ -1044,6 +1050,12 @@ declare class PmsClient {
1044
1050
  */
1045
1051
  private generateRandomHex;
1046
1052
  private fetch;
1053
+ /**
1054
+ * Single-attempt HTTP request. Throws a tagged error on transient conditions
1055
+ * (network failure, per-attempt abort, 5xx) so the retry loop can decide
1056
+ * whether to retry. 4xx and other non-2xx are thrown as fatal HttpError.
1057
+ */
1058
+ private fetchOnce;
1047
1059
  private fetchUrl;
1048
1060
  }
1049
1061
 
package/dist/index.d.ts CHANGED
@@ -528,6 +528,12 @@ interface PmsClientConfig {
528
528
  timeout?: number;
529
529
  /** Activer le mode racing (envoie à tous les noeuds connus) (défaut: true) */
530
530
  enableRacing?: boolean;
531
+ /** Number of automatic retries for idempotent reads on transient failure. Default: 2 (so up to 3 attempts total). */
532
+ retries?: number;
533
+ /** Per-attempt timeout in ms. Default: 4000. Should be > p90 of normal latency but << overall `timeout`. */
534
+ perAttemptTimeoutMs?: number;
535
+ /** Base delay (ms) for exponential backoff with full jitter between retries. Default: 100. */
536
+ retryBaseDelayMs?: number;
531
537
  }
532
538
  /** Réponse de l'endpoint d'information du coordinateur */
533
539
  interface CoordinatorInfoResponse {
@@ -1044,6 +1050,12 @@ declare class PmsClient {
1044
1050
  */
1045
1051
  private generateRandomHex;
1046
1052
  private fetch;
1053
+ /**
1054
+ * Single-attempt HTTP request. Throws a tagged error on transient conditions
1055
+ * (network failure, per-attempt abort, 5xx) so the retry loop can decide
1056
+ * whether to retry. 4xx and other non-2xx are thrown as fatal HttpError.
1057
+ */
1058
+ private fetchOnce;
1047
1059
  private fetchUrl;
1048
1060
  }
1049
1061
 
package/dist/index.js CHANGED
@@ -220,7 +220,10 @@ var DEFAULT_CONFIG = {
220
220
  protocolVersion: 1,
221
221
  timeout: 3e4,
222
222
  seedNodes: [],
223
- enableRacing: true
223
+ enableRacing: true,
224
+ retries: 2,
225
+ perAttemptTimeoutMs: 4e3,
226
+ retryBaseDelayMs: 100
224
227
  };
225
228
 
226
229
  // src/crypto.ts
@@ -332,14 +335,14 @@ var PmsClient = class {
332
335
  const res = await this.fetch("/v1/dag/tips", {
333
336
  method: "POST",
334
337
  body: JSON.stringify({ limit: 10 })
335
- });
338
+ }, { idempotent: true });
336
339
  return res;
337
340
  }
338
341
  /**
339
342
  * Récupère les informations publiques du coordinateur (clés).
340
343
  */
341
344
  async getCoordinatorInfo() {
342
- return this.fetch("/v1/coordinator/info");
345
+ return this.fetch("/v1/coordinator/info", void 0, { idempotent: true });
343
346
  }
344
347
  /**
345
348
  * Liste tous les ledgers actifs avec leur nombre de blocs.
@@ -363,13 +366,13 @@ var PmsClient = class {
363
366
  }
364
367
  const qs = params.toString();
365
368
  const path = `/v1/ledgers${qs ? `?${qs}` : ""}`;
366
- return (await this.fetch(path)).ledgers ?? [];
369
+ return (await this.fetch(path, void 0, { idempotent: true })).ledgers ?? [];
367
370
  }
368
371
  /**
369
372
  * Récupère un bloc par son ID.
370
373
  */
371
374
  async getBlock(blockId) {
372
- const wb = await this.fetch(`/v1/blocks/${blockId}`);
375
+ const wb = await this.fetch(`/v1/blocks/${blockId}`, void 0, { idempotent: true });
373
376
  return {
374
377
  id: wb.id,
375
378
  parents: wb.parents,
@@ -383,26 +386,26 @@ var PmsClient = class {
383
386
  */
384
387
  async getSupply(assetId) {
385
388
  const query = assetId ? `?asset_id=${encodeURIComponent(assetId)}` : "";
386
- return this.fetch(`/v1/supply${query}`);
389
+ return this.fetch(`/v1/supply${query}`, void 0, { idempotent: true });
387
390
  }
388
391
  /**
389
392
  * Liste tous les tokens enregistrés.
390
393
  */
391
394
  async listTokens() {
392
- const res = await this.fetch("/v1/tokens");
395
+ const res = await this.fetch("/v1/tokens", void 0, { idempotent: true });
393
396
  return res.tokens ?? [];
394
397
  }
395
398
  /**
396
399
  * Récupère les métadonnées d'un token par son asset_id.
397
400
  */
398
401
  async getToken(assetId) {
399
- return this.fetch(`/v1/tokens/${encodeURIComponent(assetId)}`);
402
+ return this.fetch(`/v1/tokens/${encodeURIComponent(assetId)}`, void 0, { idempotent: true });
400
403
  }
401
404
  /**
402
405
  * Récupère les UTXOs d'une adresse.
403
406
  */
404
407
  async getUtxos(address) {
405
- const res = await this.fetch(`/v1/wallet/${address}/utxos`);
408
+ const res = await this.fetch(`/v1/wallet/${address}/utxos`, void 0, { idempotent: true });
406
409
  return res.utxos ?? [];
407
410
  }
408
411
  // ═══════════════════════════════════════════════════════════════════════
@@ -445,7 +448,7 @@ var PmsClient = class {
445
448
  return this.fetch("/v1/wallet/restore/mnemonic", {
446
449
  method: "POST",
447
450
  body: JSON.stringify({ mnemonic })
448
- });
451
+ }, { idempotent: true });
449
452
  }
450
453
  /**
451
454
  * Restaure un wallet depuis une clé privée hexadécimale.
@@ -465,7 +468,7 @@ var PmsClient = class {
465
468
  return this.fetch("/v1/wallet/restore/private-key", {
466
469
  method: "POST",
467
470
  body: JSON.stringify({ private_key_hex: privateKeyHex })
468
- });
471
+ }, { idempotent: true });
469
472
  }
470
473
  // ═══════════════════════════════════════════════════════════════════════
471
474
  // Préparation de Transaction (Server-Side)
@@ -500,7 +503,7 @@ var PmsClient = class {
500
503
  return this.fetch("/v1/tx/prepare", {
501
504
  method: "POST",
502
505
  body: JSON.stringify(params)
503
- });
506
+ }, { idempotent: true });
504
507
  }
505
508
  /**
506
509
  * Récupère la balance d'une adresse.
@@ -512,7 +515,7 @@ var PmsClient = class {
512
515
  const res = await this.fetch("/v1/balance", {
513
516
  method: "POST",
514
517
  body: JSON.stringify({ address })
515
- });
518
+ }, { idempotent: true });
516
519
  return res.balance;
517
520
  }
518
521
  /**
@@ -546,7 +549,7 @@ var PmsClient = class {
546
549
  * ```
547
550
  */
548
551
  async getNfts(address) {
549
- const res = await this.fetch(`/v1/wallet/${address}/nfts`);
552
+ const res = await this.fetch(`/v1/wallet/${address}/nfts`, void 0, { idempotent: true });
550
553
  return res.token_ids ?? [];
551
554
  }
552
555
  /**
@@ -565,7 +568,7 @@ var PmsClient = class {
565
568
  * ```
566
569
  */
567
570
  async getNft(tokenId) {
568
- return this.fetch(`/v1/nft/${tokenId}`);
571
+ return this.fetch(`/v1/nft/${tokenId}`, void 0, { idempotent: true });
569
572
  }
570
573
  /**
571
574
  * Récupère l'historique des transactions d'un wallet.
@@ -580,7 +583,7 @@ var PmsClient = class {
580
583
  const res = await this.fetch("/wallet/history", {
581
584
  method: "POST",
582
585
  body: JSON.stringify({ bech32_addr: address, limit })
583
- });
586
+ }, { idempotent: true });
584
587
  if (options?.decryptionWallet) {
585
588
  const wallet = options.decryptionWallet;
586
589
  for (const item of res.items) {
@@ -673,7 +676,7 @@ var PmsClient = class {
673
676
  }
674
677
  const qs = params.toString();
675
678
  const path = `/v1/wallet/${encodeURIComponent(address)}/activity${qs ? `?${qs}` : ""}`;
676
- return this.fetch(path);
679
+ return this.fetch(path, void 0, { idempotent: true });
677
680
  }
678
681
  /**
679
682
  * Écoute l'activité d'un wallet en temps réel via Server-Sent Events (SSE).
@@ -777,7 +780,7 @@ var PmsClient = class {
777
780
  if (this.configCache && Date.now() < this.configCache.expires) {
778
781
  return this.configCache.data;
779
782
  }
780
- const cfg = await this.fetch("/v1/config");
783
+ const cfg = await this.fetch("/v1/config", void 0, { idempotent: true });
781
784
  this.configCache = { data: cfg, expires: Date.now() + this.CONFIG_TTL };
782
785
  return cfg;
783
786
  }
@@ -1224,14 +1227,18 @@ var PmsClient = class {
1224
1227
  // ═══════════════════════════════════════════════════════════════════════
1225
1228
  // Helper HTTP
1226
1229
  // ═══════════════════════════════════════════════════════════════════════
1227
- async fetch(path, init) {
1228
- return this.fetchUrl(this.config.nodeUrl, path, init);
1230
+ async fetch(path, init, opts) {
1231
+ return this.fetchUrl(this.config.nodeUrl, path, init, opts);
1229
1232
  }
1230
- async fetchUrl(baseUrl, path, init) {
1231
- const url = `${baseUrl.replace(/\/$/, "")}/${path.replace(/^\//, "")}`;
1232
- const controller = new AbortController();
1233
- const timeout = setTimeout(() => controller.abort(), this.config.timeout);
1234
- const signal = init?.signal || controller.signal;
1233
+ /**
1234
+ * Single-attempt HTTP request. Throws a tagged error on transient conditions
1235
+ * (network failure, per-attempt abort, 5xx) so the retry loop can decide
1236
+ * whether to retry. 4xx and other non-2xx are thrown as fatal HttpError.
1237
+ */
1238
+ async fetchOnce(url, init, attemptTimeoutMs, outerSignal) {
1239
+ const attemptController = new AbortController();
1240
+ const attemptTimer = setTimeout(() => attemptController.abort(), attemptTimeoutMs);
1241
+ const signal = mergeSignalsAny(attemptController.signal, outerSignal);
1235
1242
  try {
1236
1243
  const res = await fetch(url, {
1237
1244
  ...init,
@@ -1245,8 +1252,9 @@ var PmsClient = class {
1245
1252
  signal
1246
1253
  });
1247
1254
  if (!res.ok) {
1248
- const text2 = await res.text();
1249
- throw new Error(`HTTP ${res.status} (${url}): ${text2}`);
1255
+ const text2 = await res.text().catch(() => "");
1256
+ const retryAfter = res.headers?.get?.("Retry-After") ?? null;
1257
+ throw new HttpError(res.status, `HTTP ${res.status} (${url}): ${text2}`, retryAfter);
1250
1258
  }
1251
1259
  const text = await res.text();
1252
1260
  if (!text) {
@@ -1254,14 +1262,132 @@ var PmsClient = class {
1254
1262
  }
1255
1263
  try {
1256
1264
  return JSON.parse(text);
1257
- } catch (e) {
1265
+ } catch {
1258
1266
  throw new Error(`Invalid JSON response: ${text.substring(0, 50)}...`);
1259
1267
  }
1268
+ } catch (err) {
1269
+ if (err && err.name === "AbortError") {
1270
+ if (attemptController.signal.aborted && !(outerSignal && outerSignal.aborted)) {
1271
+ throw new PerAttemptTimeoutError(`Per-attempt timeout after ${attemptTimeoutMs}ms`);
1272
+ }
1273
+ }
1274
+ throw err;
1260
1275
  } finally {
1261
- clearTimeout(timeout);
1276
+ clearTimeout(attemptTimer);
1262
1277
  }
1263
1278
  }
1279
+ async fetchUrl(baseUrl, path, init, opts) {
1280
+ const url = `${baseUrl.replace(/\/$/, "")}/${path.replace(/^\//, "")}`;
1281
+ const outerSignal = init?.signal ?? void 0;
1282
+ if (!opts?.idempotent) {
1283
+ return this.fetchOnce(url, init, this.config.timeout, outerSignal);
1284
+ }
1285
+ const maxAttempts = 1 + Math.max(0, this.config.retries | 0);
1286
+ const perAttemptMs = this.config.perAttemptTimeoutMs;
1287
+ const baseDelayMs = this.config.retryBaseDelayMs;
1288
+ const overallDeadline = Date.now() + this.config.timeout;
1289
+ let lastErr;
1290
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
1291
+ if (outerSignal?.aborted) {
1292
+ const e = new Error("Request aborted by caller");
1293
+ e.name = "AbortError";
1294
+ throw e;
1295
+ }
1296
+ if (Date.now() > overallDeadline) {
1297
+ throw lastErr ?? new Error(`Overall timeout (${this.config.timeout}ms) exceeded`);
1298
+ }
1299
+ try {
1300
+ return await this.fetchOnce(url, init, perAttemptMs, outerSignal);
1301
+ } catch (err) {
1302
+ lastErr = err;
1303
+ if (outerSignal?.aborted) throw err;
1304
+ const retriable = isRetriableError(err);
1305
+ if (!retriable) throw err;
1306
+ if (attempt === maxAttempts - 1) throw err;
1307
+ let delay;
1308
+ const ra = err instanceof HttpError ? parseRetryAfterSeconds(err.retryAfter) : null;
1309
+ if (ra !== null) {
1310
+ delay = Math.min(ra * 1e3, 5e3);
1311
+ } else {
1312
+ const cap = Math.min(baseDelayMs * Math.pow(2, attempt), 2e3);
1313
+ delay = Math.floor(Math.random() * cap);
1314
+ }
1315
+ if (Date.now() + delay > overallDeadline) {
1316
+ throw err;
1317
+ }
1318
+ await sleepAbortable(delay, outerSignal);
1319
+ }
1320
+ }
1321
+ throw lastErr ?? new Error("Retry loop exhausted with no error captured");
1322
+ }
1323
+ };
1324
+ var HttpError = class extends Error {
1325
+ status;
1326
+ retryAfter;
1327
+ constructor(status, message, retryAfter) {
1328
+ super(message);
1329
+ this.name = "HttpError";
1330
+ this.status = status;
1331
+ this.retryAfter = retryAfter;
1332
+ }
1333
+ };
1334
+ var PerAttemptTimeoutError = class extends Error {
1335
+ constructor(message) {
1336
+ super(message);
1337
+ this.name = "PerAttemptTimeoutError";
1338
+ }
1264
1339
  };
1340
+ function isRetriableError(err) {
1341
+ if (err instanceof PerAttemptTimeoutError) return true;
1342
+ if (err instanceof HttpError) {
1343
+ return err.status === 502 || err.status === 503 || err.status === 504;
1344
+ }
1345
+ if (err instanceof TypeError) return true;
1346
+ return false;
1347
+ }
1348
+ function parseRetryAfterSeconds(raw) {
1349
+ if (!raw) return null;
1350
+ const n = Number(raw.trim());
1351
+ if (!Number.isFinite(n) || n < 0) return null;
1352
+ return n;
1353
+ }
1354
+ function mergeSignalsAny(attemptSignal, outerSignal) {
1355
+ if (!outerSignal) return attemptSignal;
1356
+ const anyFn = AbortSignal.any;
1357
+ if (typeof anyFn === "function") {
1358
+ return anyFn([attemptSignal, outerSignal]);
1359
+ }
1360
+ const merged = new AbortController();
1361
+ const onAbort = (sig) => () => {
1362
+ if (!merged.signal.aborted) merged.abort(sig.reason);
1363
+ };
1364
+ if (attemptSignal.aborted) merged.abort(attemptSignal.reason);
1365
+ else attemptSignal.addEventListener("abort", onAbort(attemptSignal), { once: true });
1366
+ if (outerSignal.aborted) merged.abort(outerSignal.reason);
1367
+ else outerSignal.addEventListener("abort", onAbort(outerSignal), { once: true });
1368
+ return merged.signal;
1369
+ }
1370
+ function sleepAbortable(ms, signal) {
1371
+ if (ms <= 0) return Promise.resolve();
1372
+ return new Promise((resolve) => {
1373
+ const t = setTimeout(() => {
1374
+ if (signal) signal.removeEventListener("abort", onAbort);
1375
+ resolve();
1376
+ }, ms);
1377
+ const onAbort = () => {
1378
+ clearTimeout(t);
1379
+ resolve();
1380
+ };
1381
+ if (signal) {
1382
+ if (signal.aborted) {
1383
+ clearTimeout(t);
1384
+ resolve();
1385
+ return;
1386
+ }
1387
+ signal.addEventListener("abort", onAbort, { once: true });
1388
+ }
1389
+ });
1390
+ }
1265
1391
  function mergeAbortSignals(a, b) {
1266
1392
  const controller = new AbortController();
1267
1393
  const onAbort = () => controller.abort();
package/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "@empereur-rouge/pms-sdk",
3
- "version": "0.3.6",
3
+ "version": "0.3.8",
4
4
  "description": "TypeScript SDK for PMS (Planetary Monetary System) — wallet management, transactions, NFTs, and DAG interactions",
5
5
  "author": "empereur-rouge",
6
6
  "license": "MIT",
7
- "main": "dist/index.js",
8
- "module": "dist/index.mjs",
7
+ "main": "dist/index.cjs",
8
+ "module": "dist/index.js",
9
9
  "types": "dist/index.d.ts",
10
10
  "type": "module",
11
11
  "exports": {
12
12
  ".": {
13
13
  "types": "./dist/index.d.ts",
14
- "import": "./dist/index.mjs",
15
- "require": "./dist/index.js"
14
+ "import": "./dist/index.js",
15
+ "require": "./dist/index.cjs"
16
16
  }
17
17
  },
18
18
  "files": [