@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.
@@ -11303,6 +11303,26 @@ var SerialChain = class {
11303
11303
  return run;
11304
11304
  }
11305
11305
  };
11306
+ function coalesced(run) {
11307
+ let inFlight = false;
11308
+ let queued = false;
11309
+ const start = () => {
11310
+ inFlight = true;
11311
+ void run().catch(() => void 0).finally(() => {
11312
+ inFlight = false;
11313
+ if (!queued) return;
11314
+ queued = false;
11315
+ start();
11316
+ });
11317
+ };
11318
+ return () => {
11319
+ if (inFlight) {
11320
+ queued = true;
11321
+ return;
11322
+ }
11323
+ start();
11324
+ };
11325
+ }
11306
11326
 
11307
11327
  // modules/payments-v2/stores.ts
11308
11328
  function createScopedKV(storage, network, chainPubkey) {
@@ -12750,6 +12770,16 @@ var InventoryView = class {
12750
12770
  this.inFlight.add(tokenId);
12751
12771
  this.deps.emit("inventory:updated");
12752
12772
  }
12773
+ /** Reserve a whole plan as ONE change — per token emitted 54 events per send. */
12774
+ markInFlightMany(tokenIds) {
12775
+ let changed = false;
12776
+ for (const tokenId of tokenIds) {
12777
+ if (this.inFlight.has(tokenId)) continue;
12778
+ this.inFlight.add(tokenId);
12779
+ changed = true;
12780
+ }
12781
+ if (changed) this.deps.emit("inventory:updated");
12782
+ }
12753
12783
  release(tokenId) {
12754
12784
  if (this.inFlight.delete(tokenId)) this.deps.emit("inventory:updated");
12755
12785
  }
@@ -12974,6 +13004,11 @@ var InventoryView = class {
12974
13004
  }
12975
13005
  };
12976
13006
 
13007
+ // modules/payments-v2/ports.ts
13008
+ function isRetryableAckError(err) {
13009
+ return err !== null && typeof err === "object" && err.retryable === true;
13010
+ }
13011
+
12977
13012
  // modules/payments-v2/receive/Receive.ts
12978
13013
  var ACK_BATCH_SIZE = 200;
12979
13014
  var REFRESH_INTERVAL_MS = 2500;
@@ -13055,6 +13090,7 @@ var Receive = class {
13055
13090
  }
13056
13091
  } catch (err) {
13057
13092
  logger.warn("PaymentsV2", "receive drain interrupted \u2014 unacked entries retry next drain:", err);
13093
+ if (isRetryableAckError(err)) return stored;
13058
13094
  await flushAcks(deps, pending, pageEpoch).catch((flushErr) => {
13059
13095
  logger.warn("PaymentsV2", "receive ack flush failed \u2014 cursor holds at the acked prefix:", flushErr);
13060
13096
  });
@@ -13129,14 +13165,18 @@ async function announce(deps, entry, record) {
13129
13165
  };
13130
13166
  }
13131
13167
  async function flushAcks(deps, pending, epochOf) {
13168
+ if (pending.length === 0) return;
13132
13169
  let lastAcked = null;
13133
13170
  try {
13134
- while (pending.length > 0) {
13135
- const next = pending[0];
13136
- await ackOne(deps, next);
13137
- lastAcked = next.cursor;
13138
- pending.shift();
13171
+ const settled = await settleAcks(deps, pending);
13172
+ let i = 0;
13173
+ while (i < pending.length && settled.has(pending[i].deliveryId)) {
13174
+ lastAcked = pending[i].cursor;
13175
+ i += 1;
13139
13176
  }
13177
+ const stuck = pending.filter((p) => !settled.has(p.deliveryId));
13178
+ pending.length = 0;
13179
+ pending.push(...stuck);
13140
13180
  } finally {
13141
13181
  if (lastAcked !== null) {
13142
13182
  const record = { cursor: lastAcked, syncEpoch: epochOf() };
@@ -13144,14 +13184,65 @@ async function flushAcks(deps, pending, epochOf) {
13144
13184
  }
13145
13185
  }
13146
13186
  }
13187
+ async function settleAcks(deps, pending) {
13188
+ const batch = deps.delivery.ackBatch?.bind(deps.delivery);
13189
+ if (batch === void 0) return settleOneByOne(deps, pending);
13190
+ let outcomes;
13191
+ try {
13192
+ outcomes = await batch(pending.map(toAckRequest));
13193
+ } catch (err) {
13194
+ if (isRetryableAckError(err)) throw err;
13195
+ logger.warn("PaymentsV2", "batched mailbox ack failed \u2014 settling one at a time:", err);
13196
+ return settleOneByOne(deps, pending);
13197
+ }
13198
+ const settled = new Set(outcomes.filter((o) => o.status === "settled").map((o) => o.deliveryId));
13199
+ const conflicts = outcomes.filter((o) => o.status === "conflict").map((o) => o.deliveryId);
13200
+ if (conflicts.length > 0) await resolveConflicts(deps, pending, conflicts, settled);
13201
+ return settled;
13202
+ }
13203
+ async function resolveConflicts(deps, pending, conflicts, settled) {
13204
+ for (const deliveryId of conflicts) {
13205
+ const entry = pending.find((p) => p.deliveryId === deliveryId);
13206
+ try {
13207
+ await rejectStaleClaim(deps, deliveryId, entry?.transferId);
13208
+ } catch (err) {
13209
+ logger.warn("PaymentsV2", `stale-reject failed for ${deliveryId} \u2014 cursor holds here:`, err);
13210
+ continue;
13211
+ }
13212
+ settled.add(deliveryId);
13213
+ }
13214
+ }
13215
+ async function rejectStaleClaim(deps, deliveryId, transferId) {
13216
+ logger.warn("PaymentsV2", `mailbox claim CONFLICT for ${deliveryId} \u2014 rejected('other') as stale`);
13217
+ await deps.delivery.ack(deliveryId, "rejected", "other");
13218
+ deps.attention(transferId ?? "", ATTENTION_CLAIM_CONFLICT, deliveryId);
13219
+ }
13220
+ async function settleOneByOne(deps, pending) {
13221
+ const settled = /* @__PURE__ */ new Set();
13222
+ for (const ack of pending) {
13223
+ try {
13224
+ await ackOne(deps, ack);
13225
+ } catch (err) {
13226
+ logger.warn("PaymentsV2", `mailbox ack failed for ${ack.deliveryId} \u2014 cursor holds here:`, err);
13227
+ break;
13228
+ }
13229
+ settled.add(ack.deliveryId);
13230
+ }
13231
+ return settled;
13232
+ }
13233
+ function toAckRequest(ack) {
13234
+ return {
13235
+ deliveryId: ack.deliveryId,
13236
+ disposition: ack.disposition,
13237
+ ...ack.reason !== void 0 ? { reason: ack.reason } : {}
13238
+ };
13239
+ }
13147
13240
  async function ackOne(deps, ack) {
13148
13241
  try {
13149
13242
  await deps.delivery.ack(ack.deliveryId, ack.disposition, ack.reason);
13150
13243
  } catch (err) {
13151
13244
  if (ack.disposition !== "claimed" || !isClaimConflict(err)) throw err;
13152
- logger.warn("PaymentsV2", `mailbox claim CONFLICT for ${ack.deliveryId} \u2014 rejected('other') as stale`);
13153
- await deps.delivery.ack(ack.deliveryId, "rejected", "other");
13154
- deps.attention(ack.transferId ?? "", ATTENTION_CLAIM_CONFLICT, ack.deliveryId);
13245
+ await rejectStaleClaim(deps, ack.deliveryId, ack.transferId);
13155
13246
  }
13156
13247
  }
13157
13248
  function claimAck(entry) {
@@ -13905,26 +13996,6 @@ function parseAmount(raw) {
13905
13996
  function supportsDeterministicMint(engine) {
13906
13997
  return engine.deterministicMint === true;
13907
13998
  }
13908
- function makeCoalescedRefresh(refresh) {
13909
- let inFlight = false;
13910
- let queued = false;
13911
- const run = () => {
13912
- inFlight = true;
13913
- void refresh().catch(() => void 0).finally(() => {
13914
- inFlight = false;
13915
- if (!queued) return;
13916
- queued = false;
13917
- run();
13918
- });
13919
- };
13920
- return () => {
13921
- if (inFlight) {
13922
- queued = true;
13923
- return;
13924
- }
13925
- run();
13926
- };
13927
- }
13928
13999
  function composeFacadeParts(deps, hooks) {
13929
14000
  const ownPubkeyBytes = hexToBytes4(deps.ownPubkey);
13930
14001
  const ledger = new ReservationLedger();
@@ -13954,9 +14025,11 @@ function composeFacadeParts(deps, hooks) {
13954
14025
  const machineDeps = buildMachineDeps(deps, hooks, historyStore);
13955
14026
  const machineStores = createMachineStores(deps.kv);
13956
14027
  const heldStates = /* @__PURE__ */ new Map();
14028
+ const refreshView = coalesced(() => view.delta());
13957
14029
  return {
13958
14030
  ownPubkeyBytes,
13959
14031
  view,
14032
+ refreshView,
13960
14033
  ledger,
13961
14034
  pins: buildPins(deps, hooks, { ledger, view, machineStores, machineDeps }),
13962
14035
  queue,
@@ -13965,7 +14038,7 @@ function composeFacadeParts(deps, hooks) {
13965
14038
  machineDeps,
13966
14039
  machine: new TransferMachine(machineDeps),
13967
14040
  heldStates,
13968
- receiveLoop: buildReceive(deps, hooks, historyStore, heldStates, view),
14041
+ receiveLoop: buildReceive(deps, hooks, historyStore, heldStates, refreshView),
13969
14042
  requests: buildRequests(deps, hooks),
13970
14043
  restoreDeps: buildRestoreDeps(deps, machineDeps, machineStores, view)
13971
14044
  };
@@ -14032,7 +14105,7 @@ function buildMachineDeps(deps, hooks, historyStore) {
14032
14105
  }
14033
14106
  };
14034
14107
  }
14035
- function buildReceive(deps, hooks, historyStore, heldStates, view) {
14108
+ function buildReceive(deps, hooks, historyStore, heldStates, refreshView) {
14036
14109
  return new Receive({
14037
14110
  delivery: deps.deliveryPort,
14038
14111
  engine: () => hooks.engine(),
@@ -14046,8 +14119,7 @@ function buildReceive(deps, hooks, historyStore, heldStates, view) {
14046
14119
  registry: deps.registry,
14047
14120
  recordReceived: (record) => recordReceived(historyStore, record),
14048
14121
  emit: (event, transfer) => deps.emit(event, transfer),
14049
- // Coalesced — see makeCoalescedRefresh.
14050
- refreshView: makeCoalescedRefresh(() => view.delta()),
14122
+ refreshView,
14051
14123
  attention: (transferId, code, detail) => {
14052
14124
  deps.emit(
14053
14125
  "transfer:attention",
@@ -14099,7 +14171,7 @@ var PaymentsFacade = class {
14099
14171
  isActiveOp: (id) => this.activeMoneyOps.has(id)
14100
14172
  });
14101
14173
  this.view = parts.view;
14102
- this.wakeRefresh = makeCoalescedRefresh(() => this.view.delta());
14174
+ this.wakeRefresh = parts.refreshView;
14103
14175
  this.ledger = parts.ledger;
14104
14176
  this.queue = parts.queue;
14105
14177
  this.machine = parts.machine;
@@ -14432,7 +14504,7 @@ var PaymentsFacade = class {
14432
14504
  ...spend.plan.direct,
14433
14505
  ...spend.plan.split !== void 0 ? [spend.plan.split.tokenId] : []
14434
14506
  ];
14435
- for (const tokenId of sourceIds) this.view.markInFlight(tokenId);
14507
+ this.view.markInFlightMany(sourceIds);
14436
14508
  if (spend.plan.split !== void 0) {
14437
14509
  this.queue.declareExpectedChange(transferId, coinId, spend.plan.split.remainderAmount);
14438
14510
  }
@@ -14718,6 +14790,10 @@ var WalletApiHttpError = class extends Error {
14718
14790
  if (retryAfter !== void 0) this.retryAfter = retryAfter;
14719
14791
  }
14720
14792
  };
14793
+ var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([0, 408, 425, 429, 500, 502, 503, 504]);
14794
+ function isRetryableStatus(err) {
14795
+ return err instanceof WalletApiHttpError && RETRYABLE_STATUSES.has(err.status);
14796
+ }
14721
14797
  function isWalletApiHttpError(err, status) {
14722
14798
  return err instanceof WalletApiHttpError && (status === void 0 || err.status === status);
14723
14799
  }
@@ -15430,6 +15506,23 @@ function computeDeliveryId(tokenIdHex, stateHashHex) {
15430
15506
  return bytesToHex3(sha256(joined));
15431
15507
  }
15432
15508
  var DELIVERY_SEEN_KEY = "delivery:seen";
15509
+ var ACK_CHUNK = 50;
15510
+ function chunked(items, size) {
15511
+ const out = [];
15512
+ for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
15513
+ return out;
15514
+ }
15515
+ var RetryableAckFailure = class extends Error {
15516
+ retryable = true;
15517
+ constructor(cause) {
15518
+ super(`mailbox ack batch could not be attempted: ${String(cause)}`);
15519
+ this.name = "RetryableAckFailure";
15520
+ this.cause = cause;
15521
+ }
15522
+ };
15523
+ function asRetryable(err) {
15524
+ return isRetryableStatus(err) ? new RetryableAckFailure(err) : err;
15525
+ }
15433
15526
  var WalletApiDeliveryPort = class {
15434
15527
  custody;
15435
15528
  client;
@@ -15595,7 +15688,53 @@ var WalletApiDeliveryPort = class {
15595
15688
  } else {
15596
15689
  await this.client.reject([deliveryId], reason);
15597
15690
  }
15598
- await this.addSeen(deliveryId);
15691
+ await this.addSeenAll([deliveryId]);
15692
+ }
15693
+ /**
15694
+ * §5.7/#757 batched settle. Claims go out grouped, rejects grouped by reason
15695
+ * (the endpoint takes one reason per call), and the seen-set is written ONCE
15696
+ * after every HTTP call has succeeded — never on a throw, so an aborted batch
15697
+ * re-lists rather than being silently forgotten.
15698
+ */
15699
+ async ackBatch(acks) {
15700
+ const settled = [];
15701
+ const outcomes = [];
15702
+ try {
15703
+ await this.settleClaims(acks, settled, outcomes);
15704
+ await this.settleRejects(acks, settled);
15705
+ } catch (err) {
15706
+ throw asRetryable(err);
15707
+ }
15708
+ await this.addSeenAll(settled);
15709
+ return [...outcomes, ...settled.map((deliveryId) => ({ deliveryId, status: "settled" }))];
15710
+ }
15711
+ async settleClaims(acks, settled, outcomes) {
15712
+ const ids = acks.filter((a) => a.disposition === "claimed").map((a) => a.deliveryId);
15713
+ for (const chunk of chunked(ids, ACK_CHUNK)) {
15714
+ const result = await this.client.claim(chunk, this.custody === "inventory");
15715
+ settled.push(...result.claimed);
15716
+ settled.push(...result.alreadyClaimed.map((e) => e.entryId));
15717
+ for (const f of result.failed) {
15718
+ if (f.code === "CONFLICT") outcomes.push({ deliveryId: f.entryId, status: "conflict" });
15719
+ else throw new MailboxClaimFailedError(f.entryId, f.code);
15720
+ }
15721
+ }
15722
+ }
15723
+ async settleRejects(acks, settled) {
15724
+ const byReason = /* @__PURE__ */ new Map();
15725
+ for (const a of acks) {
15726
+ if (a.disposition !== "rejected") continue;
15727
+ const key = a.reason ?? "";
15728
+ const group = byReason.get(key) ?? [];
15729
+ group.push(a.deliveryId);
15730
+ byReason.set(key, group);
15731
+ }
15732
+ for (const [reason, group] of byReason) {
15733
+ for (const chunk of chunked(group, ACK_CHUNK)) {
15734
+ await this.client.reject(chunk, reason === "" ? void 0 : reason);
15735
+ settled.push(...chunk);
15736
+ }
15737
+ }
15599
15738
  }
15600
15739
  onWake(cb) {
15601
15740
  if (this.wake === null) return () => void 0;
@@ -15606,10 +15745,12 @@ var WalletApiDeliveryPort = class {
15606
15745
  const raw = await this.kv.get(DELIVERY_SEEN_KEY);
15607
15746
  return new Set(raw ?? []);
15608
15747
  }
15609
- addSeen(deliveryId) {
15748
+ /** SerialChain settles-not-fulfills: a rejected write never bricks later acks. */
15749
+ addSeenAll(deliveryIds) {
15750
+ if (deliveryIds.length === 0) return Promise.resolve();
15610
15751
  return this.seenChain.enqueue(async () => {
15611
15752
  const seen = await this.readSeen();
15612
- seen.add(deliveryId);
15753
+ for (const id of deliveryIds) seen.add(id);
15613
15754
  await this.kv.set(DELIVERY_SEEN_KEY, [...seen]);
15614
15755
  });
15615
15756
  }