@unicitylabs/sphere-sdk 0.14.11-dev.6 → 0.14.11-dev.7

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.js CHANGED
@@ -11233,6 +11233,26 @@ var SerialChain = class {
11233
11233
  return run;
11234
11234
  }
11235
11235
  };
11236
+ function coalesced(run) {
11237
+ let inFlight = false;
11238
+ let queued = false;
11239
+ const start = () => {
11240
+ inFlight = true;
11241
+ void run().catch(() => void 0).finally(() => {
11242
+ inFlight = false;
11243
+ if (!queued) return;
11244
+ queued = false;
11245
+ start();
11246
+ });
11247
+ };
11248
+ return () => {
11249
+ if (inFlight) {
11250
+ queued = true;
11251
+ return;
11252
+ }
11253
+ start();
11254
+ };
11255
+ }
11236
11256
 
11237
11257
  // modules/payments-v2/stores.ts
11238
11258
  function createScopedKV(storage, network, chainPubkey) {
@@ -12680,6 +12700,16 @@ var InventoryView = class {
12680
12700
  this.inFlight.add(tokenId);
12681
12701
  this.deps.emit("inventory:updated");
12682
12702
  }
12703
+ /** Reserve a whole plan as ONE change — per token emitted 54 events per send. */
12704
+ markInFlightMany(tokenIds) {
12705
+ let changed = false;
12706
+ for (const tokenId of tokenIds) {
12707
+ if (this.inFlight.has(tokenId)) continue;
12708
+ this.inFlight.add(tokenId);
12709
+ changed = true;
12710
+ }
12711
+ if (changed) this.deps.emit("inventory:updated");
12712
+ }
12683
12713
  release(tokenId) {
12684
12714
  if (this.inFlight.delete(tokenId)) this.deps.emit("inventory:updated");
12685
12715
  }
@@ -12904,6 +12934,11 @@ var InventoryView = class {
12904
12934
  }
12905
12935
  };
12906
12936
 
12937
+ // modules/payments-v2/ports.ts
12938
+ function isRetryableAckError(err) {
12939
+ return err !== null && typeof err === "object" && err.retryable === true;
12940
+ }
12941
+
12907
12942
  // modules/payments-v2/receive/Receive.ts
12908
12943
  var ACK_BATCH_SIZE = 200;
12909
12944
  var REFRESH_INTERVAL_MS = 2500;
@@ -12985,6 +13020,7 @@ var Receive = class {
12985
13020
  }
12986
13021
  } catch (err) {
12987
13022
  logger.warn("PaymentsV2", "receive drain interrupted \u2014 unacked entries retry next drain:", err);
13023
+ if (isRetryableAckError(err)) return stored;
12988
13024
  await flushAcks(deps, pending, pageEpoch).catch((flushErr) => {
12989
13025
  logger.warn("PaymentsV2", "receive ack flush failed \u2014 cursor holds at the acked prefix:", flushErr);
12990
13026
  });
@@ -13059,14 +13095,18 @@ async function announce(deps, entry, record) {
13059
13095
  };
13060
13096
  }
13061
13097
  async function flushAcks(deps, pending, epochOf) {
13098
+ if (pending.length === 0) return;
13062
13099
  let lastAcked = null;
13063
13100
  try {
13064
- while (pending.length > 0) {
13065
- const next = pending[0];
13066
- await ackOne(deps, next);
13067
- lastAcked = next.cursor;
13068
- pending.shift();
13101
+ const settled = await settleAcks(deps, pending);
13102
+ let i = 0;
13103
+ while (i < pending.length && settled.has(pending[i].deliveryId)) {
13104
+ lastAcked = pending[i].cursor;
13105
+ i += 1;
13069
13106
  }
13107
+ const stuck = pending.filter((p) => !settled.has(p.deliveryId));
13108
+ pending.length = 0;
13109
+ pending.push(...stuck);
13070
13110
  } finally {
13071
13111
  if (lastAcked !== null) {
13072
13112
  const record = { cursor: lastAcked, syncEpoch: epochOf() };
@@ -13074,14 +13114,65 @@ async function flushAcks(deps, pending, epochOf) {
13074
13114
  }
13075
13115
  }
13076
13116
  }
13117
+ async function settleAcks(deps, pending) {
13118
+ const batch = deps.delivery.ackBatch?.bind(deps.delivery);
13119
+ if (batch === void 0) return settleOneByOne(deps, pending);
13120
+ let outcomes;
13121
+ try {
13122
+ outcomes = await batch(pending.map(toAckRequest));
13123
+ } catch (err) {
13124
+ if (isRetryableAckError(err)) throw err;
13125
+ logger.warn("PaymentsV2", "batched mailbox ack failed \u2014 settling one at a time:", err);
13126
+ return settleOneByOne(deps, pending);
13127
+ }
13128
+ const settled = new Set(outcomes.filter((o) => o.status === "settled").map((o) => o.deliveryId));
13129
+ const conflicts = outcomes.filter((o) => o.status === "conflict").map((o) => o.deliveryId);
13130
+ if (conflicts.length > 0) await resolveConflicts(deps, pending, conflicts, settled);
13131
+ return settled;
13132
+ }
13133
+ async function resolveConflicts(deps, pending, conflicts, settled) {
13134
+ for (const deliveryId of conflicts) {
13135
+ const entry = pending.find((p) => p.deliveryId === deliveryId);
13136
+ try {
13137
+ await rejectStaleClaim(deps, deliveryId, entry?.transferId);
13138
+ } catch (err) {
13139
+ logger.warn("PaymentsV2", `stale-reject failed for ${deliveryId} \u2014 cursor holds here:`, err);
13140
+ continue;
13141
+ }
13142
+ settled.add(deliveryId);
13143
+ }
13144
+ }
13145
+ async function rejectStaleClaim(deps, deliveryId, transferId) {
13146
+ logger.warn("PaymentsV2", `mailbox claim CONFLICT for ${deliveryId} \u2014 rejected('other') as stale`);
13147
+ await deps.delivery.ack(deliveryId, "rejected", "other");
13148
+ deps.attention(transferId ?? "", ATTENTION_CLAIM_CONFLICT, deliveryId);
13149
+ }
13150
+ async function settleOneByOne(deps, pending) {
13151
+ const settled = /* @__PURE__ */ new Set();
13152
+ for (const ack of pending) {
13153
+ try {
13154
+ await ackOne(deps, ack);
13155
+ } catch (err) {
13156
+ logger.warn("PaymentsV2", `mailbox ack failed for ${ack.deliveryId} \u2014 cursor holds here:`, err);
13157
+ break;
13158
+ }
13159
+ settled.add(ack.deliveryId);
13160
+ }
13161
+ return settled;
13162
+ }
13163
+ function toAckRequest(ack) {
13164
+ return {
13165
+ deliveryId: ack.deliveryId,
13166
+ disposition: ack.disposition,
13167
+ ...ack.reason !== void 0 ? { reason: ack.reason } : {}
13168
+ };
13169
+ }
13077
13170
  async function ackOne(deps, ack) {
13078
13171
  try {
13079
13172
  await deps.delivery.ack(ack.deliveryId, ack.disposition, ack.reason);
13080
13173
  } catch (err) {
13081
13174
  if (ack.disposition !== "claimed" || !isClaimConflict(err)) throw err;
13082
- logger.warn("PaymentsV2", `mailbox claim CONFLICT for ${ack.deliveryId} \u2014 rejected('other') as stale`);
13083
- await deps.delivery.ack(ack.deliveryId, "rejected", "other");
13084
- deps.attention(ack.transferId ?? "", ATTENTION_CLAIM_CONFLICT, ack.deliveryId);
13175
+ await rejectStaleClaim(deps, ack.deliveryId, ack.transferId);
13085
13176
  }
13086
13177
  }
13087
13178
  function claimAck(entry) {
@@ -13835,26 +13926,6 @@ function parseAmount(raw) {
13835
13926
  function supportsDeterministicMint(engine) {
13836
13927
  return engine.deterministicMint === true;
13837
13928
  }
13838
- function makeCoalescedRefresh(refresh) {
13839
- let inFlight = false;
13840
- let queued = false;
13841
- const run = () => {
13842
- inFlight = true;
13843
- void refresh().catch(() => void 0).finally(() => {
13844
- inFlight = false;
13845
- if (!queued) return;
13846
- queued = false;
13847
- run();
13848
- });
13849
- };
13850
- return () => {
13851
- if (inFlight) {
13852
- queued = true;
13853
- return;
13854
- }
13855
- run();
13856
- };
13857
- }
13858
13929
  function composeFacadeParts(deps, hooks) {
13859
13930
  const ownPubkeyBytes = hexToBytes4(deps.ownPubkey);
13860
13931
  const ledger = new ReservationLedger();
@@ -13884,9 +13955,11 @@ function composeFacadeParts(deps, hooks) {
13884
13955
  const machineDeps = buildMachineDeps(deps, hooks, historyStore);
13885
13956
  const machineStores = createMachineStores(deps.kv);
13886
13957
  const heldStates = /* @__PURE__ */ new Map();
13958
+ const refreshView = coalesced(() => view.delta());
13887
13959
  return {
13888
13960
  ownPubkeyBytes,
13889
13961
  view,
13962
+ refreshView,
13890
13963
  ledger,
13891
13964
  pins: buildPins(deps, hooks, { ledger, view, machineStores, machineDeps }),
13892
13965
  queue,
@@ -13895,7 +13968,7 @@ function composeFacadeParts(deps, hooks) {
13895
13968
  machineDeps,
13896
13969
  machine: new TransferMachine(machineDeps),
13897
13970
  heldStates,
13898
- receiveLoop: buildReceive(deps, hooks, historyStore, heldStates, view),
13971
+ receiveLoop: buildReceive(deps, hooks, historyStore, heldStates, refreshView),
13899
13972
  requests: buildRequests(deps, hooks),
13900
13973
  restoreDeps: buildRestoreDeps(deps, machineDeps, machineStores, view)
13901
13974
  };
@@ -13962,7 +14035,7 @@ function buildMachineDeps(deps, hooks, historyStore) {
13962
14035
  }
13963
14036
  };
13964
14037
  }
13965
- function buildReceive(deps, hooks, historyStore, heldStates, view) {
14038
+ function buildReceive(deps, hooks, historyStore, heldStates, refreshView) {
13966
14039
  return new Receive({
13967
14040
  delivery: deps.deliveryPort,
13968
14041
  engine: () => hooks.engine(),
@@ -13976,8 +14049,7 @@ function buildReceive(deps, hooks, historyStore, heldStates, view) {
13976
14049
  registry: deps.registry,
13977
14050
  recordReceived: (record) => recordReceived(historyStore, record),
13978
14051
  emit: (event, transfer) => deps.emit(event, transfer),
13979
- // Coalesced — see makeCoalescedRefresh.
13980
- refreshView: makeCoalescedRefresh(() => view.delta()),
14052
+ refreshView,
13981
14053
  attention: (transferId, code, detail) => {
13982
14054
  deps.emit(
13983
14055
  "transfer:attention",
@@ -14029,7 +14101,7 @@ var PaymentsFacade = class {
14029
14101
  isActiveOp: (id) => this.activeMoneyOps.has(id)
14030
14102
  });
14031
14103
  this.view = parts.view;
14032
- this.wakeRefresh = makeCoalescedRefresh(() => this.view.delta());
14104
+ this.wakeRefresh = parts.refreshView;
14033
14105
  this.ledger = parts.ledger;
14034
14106
  this.queue = parts.queue;
14035
14107
  this.machine = parts.machine;
@@ -14362,7 +14434,7 @@ var PaymentsFacade = class {
14362
14434
  ...spend.plan.direct,
14363
14435
  ...spend.plan.split !== void 0 ? [spend.plan.split.tokenId] : []
14364
14436
  ];
14365
- for (const tokenId of sourceIds) this.view.markInFlight(tokenId);
14437
+ this.view.markInFlightMany(sourceIds);
14366
14438
  if (spend.plan.split !== void 0) {
14367
14439
  this.queue.declareExpectedChange(transferId, coinId, spend.plan.split.remainderAmount);
14368
14440
  }
@@ -14648,6 +14720,10 @@ var WalletApiHttpError = class extends Error {
14648
14720
  if (retryAfter !== void 0) this.retryAfter = retryAfter;
14649
14721
  }
14650
14722
  };
14723
+ var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([0, 408, 425, 429, 500, 502, 503, 504]);
14724
+ function isRetryableStatus(err) {
14725
+ return err instanceof WalletApiHttpError && RETRYABLE_STATUSES.has(err.status);
14726
+ }
14651
14727
  function isWalletApiHttpError(err, status) {
14652
14728
  return err instanceof WalletApiHttpError && (status === void 0 || err.status === status);
14653
14729
  }
@@ -15360,6 +15436,23 @@ function computeDeliveryId(tokenIdHex, stateHashHex) {
15360
15436
  return bytesToHex3(sha256(joined));
15361
15437
  }
15362
15438
  var DELIVERY_SEEN_KEY = "delivery:seen";
15439
+ var ACK_CHUNK = 50;
15440
+ function chunked(items, size) {
15441
+ const out = [];
15442
+ for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
15443
+ return out;
15444
+ }
15445
+ var RetryableAckFailure = class extends Error {
15446
+ retryable = true;
15447
+ constructor(cause) {
15448
+ super(`mailbox ack batch could not be attempted: ${String(cause)}`);
15449
+ this.name = "RetryableAckFailure";
15450
+ this.cause = cause;
15451
+ }
15452
+ };
15453
+ function asRetryable(err) {
15454
+ return isRetryableStatus(err) ? new RetryableAckFailure(err) : err;
15455
+ }
15363
15456
  var WalletApiDeliveryPort = class {
15364
15457
  custody;
15365
15458
  client;
@@ -15525,7 +15618,53 @@ var WalletApiDeliveryPort = class {
15525
15618
  } else {
15526
15619
  await this.client.reject([deliveryId], reason);
15527
15620
  }
15528
- await this.addSeen(deliveryId);
15621
+ await this.addSeenAll([deliveryId]);
15622
+ }
15623
+ /**
15624
+ * §5.7/#757 batched settle. Claims go out grouped, rejects grouped by reason
15625
+ * (the endpoint takes one reason per call), and the seen-set is written ONCE
15626
+ * after every HTTP call has succeeded — never on a throw, so an aborted batch
15627
+ * re-lists rather than being silently forgotten.
15628
+ */
15629
+ async ackBatch(acks) {
15630
+ const settled = [];
15631
+ const outcomes = [];
15632
+ try {
15633
+ await this.settleClaims(acks, settled, outcomes);
15634
+ await this.settleRejects(acks, settled);
15635
+ } catch (err) {
15636
+ throw asRetryable(err);
15637
+ }
15638
+ await this.addSeenAll(settled);
15639
+ return [...outcomes, ...settled.map((deliveryId) => ({ deliveryId, status: "settled" }))];
15640
+ }
15641
+ async settleClaims(acks, settled, outcomes) {
15642
+ const ids = acks.filter((a) => a.disposition === "claimed").map((a) => a.deliveryId);
15643
+ for (const chunk of chunked(ids, ACK_CHUNK)) {
15644
+ const result = await this.client.claim(chunk, this.custody === "inventory");
15645
+ settled.push(...result.claimed);
15646
+ settled.push(...result.alreadyClaimed.map((e) => e.entryId));
15647
+ for (const f of result.failed) {
15648
+ if (f.code === "CONFLICT") outcomes.push({ deliveryId: f.entryId, status: "conflict" });
15649
+ else throw new MailboxClaimFailedError(f.entryId, f.code);
15650
+ }
15651
+ }
15652
+ }
15653
+ async settleRejects(acks, settled) {
15654
+ const byReason = /* @__PURE__ */ new Map();
15655
+ for (const a of acks) {
15656
+ if (a.disposition !== "rejected") continue;
15657
+ const key = a.reason ?? "";
15658
+ const group = byReason.get(key) ?? [];
15659
+ group.push(a.deliveryId);
15660
+ byReason.set(key, group);
15661
+ }
15662
+ for (const [reason, group] of byReason) {
15663
+ for (const chunk of chunked(group, ACK_CHUNK)) {
15664
+ await this.client.reject(chunk, reason === "" ? void 0 : reason);
15665
+ settled.push(...chunk);
15666
+ }
15667
+ }
15529
15668
  }
15530
15669
  onWake(cb) {
15531
15670
  if (this.wake === null) return () => void 0;
@@ -15536,10 +15675,12 @@ var WalletApiDeliveryPort = class {
15536
15675
  const raw = await this.kv.get(DELIVERY_SEEN_KEY);
15537
15676
  return new Set(raw ?? []);
15538
15677
  }
15539
- addSeen(deliveryId) {
15678
+ /** SerialChain settles-not-fulfills: a rejected write never bricks later acks. */
15679
+ addSeenAll(deliveryIds) {
15680
+ if (deliveryIds.length === 0) return Promise.resolve();
15540
15681
  return this.seenChain.enqueue(async () => {
15541
15682
  const seen = await this.readSeen();
15542
- seen.add(deliveryId);
15683
+ for (const id of deliveryIds) seen.add(id);
15543
15684
  await this.kv.set(DELIVERY_SEEN_KEY, [...seen]);
15544
15685
  });
15545
15686
  }