@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.
@@ -11217,6 +11217,26 @@ var SerialChain = class {
11217
11217
  return run;
11218
11218
  }
11219
11219
  };
11220
+ function coalesced(run) {
11221
+ let inFlight = false;
11222
+ let queued = false;
11223
+ const start = () => {
11224
+ inFlight = true;
11225
+ void run().catch(() => void 0).finally(() => {
11226
+ inFlight = false;
11227
+ if (!queued) return;
11228
+ queued = false;
11229
+ start();
11230
+ });
11231
+ };
11232
+ return () => {
11233
+ if (inFlight) {
11234
+ queued = true;
11235
+ return;
11236
+ }
11237
+ start();
11238
+ };
11239
+ }
11220
11240
 
11221
11241
  // modules/payments-v2/stores.ts
11222
11242
  function createScopedKV(storage, network, chainPubkey) {
@@ -12664,6 +12684,16 @@ var InventoryView = class {
12664
12684
  this.inFlight.add(tokenId);
12665
12685
  this.deps.emit("inventory:updated");
12666
12686
  }
12687
+ /** Reserve a whole plan as ONE change — per token emitted 54 events per send. */
12688
+ markInFlightMany(tokenIds) {
12689
+ let changed = false;
12690
+ for (const tokenId of tokenIds) {
12691
+ if (this.inFlight.has(tokenId)) continue;
12692
+ this.inFlight.add(tokenId);
12693
+ changed = true;
12694
+ }
12695
+ if (changed) this.deps.emit("inventory:updated");
12696
+ }
12667
12697
  release(tokenId) {
12668
12698
  if (this.inFlight.delete(tokenId)) this.deps.emit("inventory:updated");
12669
12699
  }
@@ -12888,6 +12918,11 @@ var InventoryView = class {
12888
12918
  }
12889
12919
  };
12890
12920
 
12921
+ // modules/payments-v2/ports.ts
12922
+ function isRetryableAckError(err) {
12923
+ return err !== null && typeof err === "object" && err.retryable === true;
12924
+ }
12925
+
12891
12926
  // modules/payments-v2/receive/Receive.ts
12892
12927
  var ACK_BATCH_SIZE = 200;
12893
12928
  var REFRESH_INTERVAL_MS = 2500;
@@ -12969,6 +13004,7 @@ var Receive = class {
12969
13004
  }
12970
13005
  } catch (err) {
12971
13006
  logger.warn("PaymentsV2", "receive drain interrupted \u2014 unacked entries retry next drain:", err);
13007
+ if (isRetryableAckError(err)) return stored;
12972
13008
  await flushAcks(deps, pending, pageEpoch).catch((flushErr) => {
12973
13009
  logger.warn("PaymentsV2", "receive ack flush failed \u2014 cursor holds at the acked prefix:", flushErr);
12974
13010
  });
@@ -13043,14 +13079,18 @@ async function announce(deps, entry, record) {
13043
13079
  };
13044
13080
  }
13045
13081
  async function flushAcks(deps, pending, epochOf) {
13082
+ if (pending.length === 0) return;
13046
13083
  let lastAcked = null;
13047
13084
  try {
13048
- while (pending.length > 0) {
13049
- const next = pending[0];
13050
- await ackOne(deps, next);
13051
- lastAcked = next.cursor;
13052
- pending.shift();
13085
+ const settled = await settleAcks(deps, pending);
13086
+ let i = 0;
13087
+ while (i < pending.length && settled.has(pending[i].deliveryId)) {
13088
+ lastAcked = pending[i].cursor;
13089
+ i += 1;
13053
13090
  }
13091
+ const stuck = pending.filter((p) => !settled.has(p.deliveryId));
13092
+ pending.length = 0;
13093
+ pending.push(...stuck);
13054
13094
  } finally {
13055
13095
  if (lastAcked !== null) {
13056
13096
  const record = { cursor: lastAcked, syncEpoch: epochOf() };
@@ -13058,14 +13098,65 @@ async function flushAcks(deps, pending, epochOf) {
13058
13098
  }
13059
13099
  }
13060
13100
  }
13101
+ async function settleAcks(deps, pending) {
13102
+ const batch = deps.delivery.ackBatch?.bind(deps.delivery);
13103
+ if (batch === void 0) return settleOneByOne(deps, pending);
13104
+ let outcomes;
13105
+ try {
13106
+ outcomes = await batch(pending.map(toAckRequest));
13107
+ } catch (err) {
13108
+ if (isRetryableAckError(err)) throw err;
13109
+ logger.warn("PaymentsV2", "batched mailbox ack failed \u2014 settling one at a time:", err);
13110
+ return settleOneByOne(deps, pending);
13111
+ }
13112
+ const settled = new Set(outcomes.filter((o) => o.status === "settled").map((o) => o.deliveryId));
13113
+ const conflicts = outcomes.filter((o) => o.status === "conflict").map((o) => o.deliveryId);
13114
+ if (conflicts.length > 0) await resolveConflicts(deps, pending, conflicts, settled);
13115
+ return settled;
13116
+ }
13117
+ async function resolveConflicts(deps, pending, conflicts, settled) {
13118
+ for (const deliveryId of conflicts) {
13119
+ const entry = pending.find((p) => p.deliveryId === deliveryId);
13120
+ try {
13121
+ await rejectStaleClaim(deps, deliveryId, entry?.transferId);
13122
+ } catch (err) {
13123
+ logger.warn("PaymentsV2", `stale-reject failed for ${deliveryId} \u2014 cursor holds here:`, err);
13124
+ continue;
13125
+ }
13126
+ settled.add(deliveryId);
13127
+ }
13128
+ }
13129
+ async function rejectStaleClaim(deps, deliveryId, transferId) {
13130
+ logger.warn("PaymentsV2", `mailbox claim CONFLICT for ${deliveryId} \u2014 rejected('other') as stale`);
13131
+ await deps.delivery.ack(deliveryId, "rejected", "other");
13132
+ deps.attention(transferId ?? "", ATTENTION_CLAIM_CONFLICT, deliveryId);
13133
+ }
13134
+ async function settleOneByOne(deps, pending) {
13135
+ const settled = /* @__PURE__ */ new Set();
13136
+ for (const ack of pending) {
13137
+ try {
13138
+ await ackOne(deps, ack);
13139
+ } catch (err) {
13140
+ logger.warn("PaymentsV2", `mailbox ack failed for ${ack.deliveryId} \u2014 cursor holds here:`, err);
13141
+ break;
13142
+ }
13143
+ settled.add(ack.deliveryId);
13144
+ }
13145
+ return settled;
13146
+ }
13147
+ function toAckRequest(ack) {
13148
+ return {
13149
+ deliveryId: ack.deliveryId,
13150
+ disposition: ack.disposition,
13151
+ ...ack.reason !== void 0 ? { reason: ack.reason } : {}
13152
+ };
13153
+ }
13061
13154
  async function ackOne(deps, ack) {
13062
13155
  try {
13063
13156
  await deps.delivery.ack(ack.deliveryId, ack.disposition, ack.reason);
13064
13157
  } catch (err) {
13065
13158
  if (ack.disposition !== "claimed" || !isClaimConflict(err)) throw err;
13066
- logger.warn("PaymentsV2", `mailbox claim CONFLICT for ${ack.deliveryId} \u2014 rejected('other') as stale`);
13067
- await deps.delivery.ack(ack.deliveryId, "rejected", "other");
13068
- deps.attention(ack.transferId ?? "", ATTENTION_CLAIM_CONFLICT, ack.deliveryId);
13159
+ await rejectStaleClaim(deps, ack.deliveryId, ack.transferId);
13069
13160
  }
13070
13161
  }
13071
13162
  function claimAck(entry) {
@@ -13819,26 +13910,6 @@ function parseAmount(raw) {
13819
13910
  function supportsDeterministicMint(engine) {
13820
13911
  return engine.deterministicMint === true;
13821
13912
  }
13822
- function makeCoalescedRefresh(refresh) {
13823
- let inFlight = false;
13824
- let queued = false;
13825
- const run = () => {
13826
- inFlight = true;
13827
- void refresh().catch(() => void 0).finally(() => {
13828
- inFlight = false;
13829
- if (!queued) return;
13830
- queued = false;
13831
- run();
13832
- });
13833
- };
13834
- return () => {
13835
- if (inFlight) {
13836
- queued = true;
13837
- return;
13838
- }
13839
- run();
13840
- };
13841
- }
13842
13913
  function composeFacadeParts(deps, hooks) {
13843
13914
  const ownPubkeyBytes = hexToBytes4(deps.ownPubkey);
13844
13915
  const ledger = new ReservationLedger();
@@ -13868,9 +13939,11 @@ function composeFacadeParts(deps, hooks) {
13868
13939
  const machineDeps = buildMachineDeps(deps, hooks, historyStore);
13869
13940
  const machineStores = createMachineStores(deps.kv);
13870
13941
  const heldStates = /* @__PURE__ */ new Map();
13942
+ const refreshView = coalesced(() => view.delta());
13871
13943
  return {
13872
13944
  ownPubkeyBytes,
13873
13945
  view,
13946
+ refreshView,
13874
13947
  ledger,
13875
13948
  pins: buildPins(deps, hooks, { ledger, view, machineStores, machineDeps }),
13876
13949
  queue,
@@ -13879,7 +13952,7 @@ function composeFacadeParts(deps, hooks) {
13879
13952
  machineDeps,
13880
13953
  machine: new TransferMachine(machineDeps),
13881
13954
  heldStates,
13882
- receiveLoop: buildReceive(deps, hooks, historyStore, heldStates, view),
13955
+ receiveLoop: buildReceive(deps, hooks, historyStore, heldStates, refreshView),
13883
13956
  requests: buildRequests(deps, hooks),
13884
13957
  restoreDeps: buildRestoreDeps(deps, machineDeps, machineStores, view)
13885
13958
  };
@@ -13946,7 +14019,7 @@ function buildMachineDeps(deps, hooks, historyStore) {
13946
14019
  }
13947
14020
  };
13948
14021
  }
13949
- function buildReceive(deps, hooks, historyStore, heldStates, view) {
14022
+ function buildReceive(deps, hooks, historyStore, heldStates, refreshView) {
13950
14023
  return new Receive({
13951
14024
  delivery: deps.deliveryPort,
13952
14025
  engine: () => hooks.engine(),
@@ -13960,8 +14033,7 @@ function buildReceive(deps, hooks, historyStore, heldStates, view) {
13960
14033
  registry: deps.registry,
13961
14034
  recordReceived: (record) => recordReceived(historyStore, record),
13962
14035
  emit: (event, transfer) => deps.emit(event, transfer),
13963
- // Coalesced — see makeCoalescedRefresh.
13964
- refreshView: makeCoalescedRefresh(() => view.delta()),
14036
+ refreshView,
13965
14037
  attention: (transferId, code, detail) => {
13966
14038
  deps.emit(
13967
14039
  "transfer:attention",
@@ -14013,7 +14085,7 @@ var PaymentsFacade = class {
14013
14085
  isActiveOp: (id) => this.activeMoneyOps.has(id)
14014
14086
  });
14015
14087
  this.view = parts.view;
14016
- this.wakeRefresh = makeCoalescedRefresh(() => this.view.delta());
14088
+ this.wakeRefresh = parts.refreshView;
14017
14089
  this.ledger = parts.ledger;
14018
14090
  this.queue = parts.queue;
14019
14091
  this.machine = parts.machine;
@@ -14346,7 +14418,7 @@ var PaymentsFacade = class {
14346
14418
  ...spend.plan.direct,
14347
14419
  ...spend.plan.split !== void 0 ? [spend.plan.split.tokenId] : []
14348
14420
  ];
14349
- for (const tokenId of sourceIds) this.view.markInFlight(tokenId);
14421
+ this.view.markInFlightMany(sourceIds);
14350
14422
  if (spend.plan.split !== void 0) {
14351
14423
  this.queue.declareExpectedChange(transferId, coinId, spend.plan.split.remainderAmount);
14352
14424
  }
@@ -14632,6 +14704,10 @@ var WalletApiHttpError = class extends Error {
14632
14704
  if (retryAfter !== void 0) this.retryAfter = retryAfter;
14633
14705
  }
14634
14706
  };
14707
+ var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([0, 408, 425, 429, 500, 502, 503, 504]);
14708
+ function isRetryableStatus(err) {
14709
+ return err instanceof WalletApiHttpError && RETRYABLE_STATUSES.has(err.status);
14710
+ }
14635
14711
  function isWalletApiHttpError(err, status) {
14636
14712
  return err instanceof WalletApiHttpError && (status === void 0 || err.status === status);
14637
14713
  }
@@ -15344,6 +15420,23 @@ function computeDeliveryId(tokenIdHex, stateHashHex) {
15344
15420
  return bytesToHex3(sha256(joined));
15345
15421
  }
15346
15422
  var DELIVERY_SEEN_KEY = "delivery:seen";
15423
+ var ACK_CHUNK = 50;
15424
+ function chunked(items, size) {
15425
+ const out = [];
15426
+ for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
15427
+ return out;
15428
+ }
15429
+ var RetryableAckFailure = class extends Error {
15430
+ retryable = true;
15431
+ constructor(cause) {
15432
+ super(`mailbox ack batch could not be attempted: ${String(cause)}`);
15433
+ this.name = "RetryableAckFailure";
15434
+ this.cause = cause;
15435
+ }
15436
+ };
15437
+ function asRetryable(err) {
15438
+ return isRetryableStatus(err) ? new RetryableAckFailure(err) : err;
15439
+ }
15347
15440
  var WalletApiDeliveryPort = class {
15348
15441
  custody;
15349
15442
  client;
@@ -15509,7 +15602,53 @@ var WalletApiDeliveryPort = class {
15509
15602
  } else {
15510
15603
  await this.client.reject([deliveryId], reason);
15511
15604
  }
15512
- await this.addSeen(deliveryId);
15605
+ await this.addSeenAll([deliveryId]);
15606
+ }
15607
+ /**
15608
+ * §5.7/#757 batched settle. Claims go out grouped, rejects grouped by reason
15609
+ * (the endpoint takes one reason per call), and the seen-set is written ONCE
15610
+ * after every HTTP call has succeeded — never on a throw, so an aborted batch
15611
+ * re-lists rather than being silently forgotten.
15612
+ */
15613
+ async ackBatch(acks) {
15614
+ const settled = [];
15615
+ const outcomes = [];
15616
+ try {
15617
+ await this.settleClaims(acks, settled, outcomes);
15618
+ await this.settleRejects(acks, settled);
15619
+ } catch (err) {
15620
+ throw asRetryable(err);
15621
+ }
15622
+ await this.addSeenAll(settled);
15623
+ return [...outcomes, ...settled.map((deliveryId) => ({ deliveryId, status: "settled" }))];
15624
+ }
15625
+ async settleClaims(acks, settled, outcomes) {
15626
+ const ids = acks.filter((a) => a.disposition === "claimed").map((a) => a.deliveryId);
15627
+ for (const chunk of chunked(ids, ACK_CHUNK)) {
15628
+ const result = await this.client.claim(chunk, this.custody === "inventory");
15629
+ settled.push(...result.claimed);
15630
+ settled.push(...result.alreadyClaimed.map((e) => e.entryId));
15631
+ for (const f of result.failed) {
15632
+ if (f.code === "CONFLICT") outcomes.push({ deliveryId: f.entryId, status: "conflict" });
15633
+ else throw new MailboxClaimFailedError(f.entryId, f.code);
15634
+ }
15635
+ }
15636
+ }
15637
+ async settleRejects(acks, settled) {
15638
+ const byReason = /* @__PURE__ */ new Map();
15639
+ for (const a of acks) {
15640
+ if (a.disposition !== "rejected") continue;
15641
+ const key = a.reason ?? "";
15642
+ const group = byReason.get(key) ?? [];
15643
+ group.push(a.deliveryId);
15644
+ byReason.set(key, group);
15645
+ }
15646
+ for (const [reason, group] of byReason) {
15647
+ for (const chunk of chunked(group, ACK_CHUNK)) {
15648
+ await this.client.reject(chunk, reason === "" ? void 0 : reason);
15649
+ settled.push(...chunk);
15650
+ }
15651
+ }
15513
15652
  }
15514
15653
  onWake(cb) {
15515
15654
  if (this.wake === null) return () => void 0;
@@ -15520,10 +15659,12 @@ var WalletApiDeliveryPort = class {
15520
15659
  const raw = await this.kv.get(DELIVERY_SEEN_KEY);
15521
15660
  return new Set(raw ?? []);
15522
15661
  }
15523
- addSeen(deliveryId) {
15662
+ /** SerialChain settles-not-fulfills: a rejected write never bricks later acks. */
15663
+ addSeenAll(deliveryIds) {
15664
+ if (deliveryIds.length === 0) return Promise.resolve();
15524
15665
  return this.seenChain.enqueue(async () => {
15525
15666
  const seen = await this.readSeen();
15526
- seen.add(deliveryId);
15667
+ for (const id of deliveryIds) seen.add(id);
15527
15668
  await this.kv.set(DELIVERY_SEEN_KEY, [...seen]);
15528
15669
  });
15529
15670
  }