@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.cjs CHANGED
@@ -11361,6 +11361,26 @@ var SerialChain = class {
11361
11361
  return run;
11362
11362
  }
11363
11363
  };
11364
+ function coalesced(run) {
11365
+ let inFlight = false;
11366
+ let queued = false;
11367
+ const start = () => {
11368
+ inFlight = true;
11369
+ void run().catch(() => void 0).finally(() => {
11370
+ inFlight = false;
11371
+ if (!queued) return;
11372
+ queued = false;
11373
+ start();
11374
+ });
11375
+ };
11376
+ return () => {
11377
+ if (inFlight) {
11378
+ queued = true;
11379
+ return;
11380
+ }
11381
+ start();
11382
+ };
11383
+ }
11364
11384
 
11365
11385
  // modules/payments-v2/stores.ts
11366
11386
  function createScopedKV(storage, network, chainPubkey) {
@@ -12808,6 +12828,16 @@ var InventoryView = class {
12808
12828
  this.inFlight.add(tokenId);
12809
12829
  this.deps.emit("inventory:updated");
12810
12830
  }
12831
+ /** Reserve a whole plan as ONE change — per token emitted 54 events per send. */
12832
+ markInFlightMany(tokenIds) {
12833
+ let changed = false;
12834
+ for (const tokenId of tokenIds) {
12835
+ if (this.inFlight.has(tokenId)) continue;
12836
+ this.inFlight.add(tokenId);
12837
+ changed = true;
12838
+ }
12839
+ if (changed) this.deps.emit("inventory:updated");
12840
+ }
12811
12841
  release(tokenId) {
12812
12842
  if (this.inFlight.delete(tokenId)) this.deps.emit("inventory:updated");
12813
12843
  }
@@ -13032,6 +13062,11 @@ var InventoryView = class {
13032
13062
  }
13033
13063
  };
13034
13064
 
13065
+ // modules/payments-v2/ports.ts
13066
+ function isRetryableAckError(err) {
13067
+ return err !== null && typeof err === "object" && err.retryable === true;
13068
+ }
13069
+
13035
13070
  // modules/payments-v2/receive/Receive.ts
13036
13071
  var ACK_BATCH_SIZE = 200;
13037
13072
  var REFRESH_INTERVAL_MS = 2500;
@@ -13113,6 +13148,7 @@ var Receive = class {
13113
13148
  }
13114
13149
  } catch (err) {
13115
13150
  logger.warn("PaymentsV2", "receive drain interrupted \u2014 unacked entries retry next drain:", err);
13151
+ if (isRetryableAckError(err)) return stored;
13116
13152
  await flushAcks(deps, pending, pageEpoch).catch((flushErr) => {
13117
13153
  logger.warn("PaymentsV2", "receive ack flush failed \u2014 cursor holds at the acked prefix:", flushErr);
13118
13154
  });
@@ -13187,14 +13223,18 @@ async function announce(deps, entry, record) {
13187
13223
  };
13188
13224
  }
13189
13225
  async function flushAcks(deps, pending, epochOf) {
13226
+ if (pending.length === 0) return;
13190
13227
  let lastAcked = null;
13191
13228
  try {
13192
- while (pending.length > 0) {
13193
- const next = pending[0];
13194
- await ackOne(deps, next);
13195
- lastAcked = next.cursor;
13196
- pending.shift();
13229
+ const settled = await settleAcks(deps, pending);
13230
+ let i = 0;
13231
+ while (i < pending.length && settled.has(pending[i].deliveryId)) {
13232
+ lastAcked = pending[i].cursor;
13233
+ i += 1;
13197
13234
  }
13235
+ const stuck = pending.filter((p) => !settled.has(p.deliveryId));
13236
+ pending.length = 0;
13237
+ pending.push(...stuck);
13198
13238
  } finally {
13199
13239
  if (lastAcked !== null) {
13200
13240
  const record = { cursor: lastAcked, syncEpoch: epochOf() };
@@ -13202,14 +13242,65 @@ async function flushAcks(deps, pending, epochOf) {
13202
13242
  }
13203
13243
  }
13204
13244
  }
13245
+ async function settleAcks(deps, pending) {
13246
+ const batch = deps.delivery.ackBatch?.bind(deps.delivery);
13247
+ if (batch === void 0) return settleOneByOne(deps, pending);
13248
+ let outcomes;
13249
+ try {
13250
+ outcomes = await batch(pending.map(toAckRequest));
13251
+ } catch (err) {
13252
+ if (isRetryableAckError(err)) throw err;
13253
+ logger.warn("PaymentsV2", "batched mailbox ack failed \u2014 settling one at a time:", err);
13254
+ return settleOneByOne(deps, pending);
13255
+ }
13256
+ const settled = new Set(outcomes.filter((o) => o.status === "settled").map((o) => o.deliveryId));
13257
+ const conflicts = outcomes.filter((o) => o.status === "conflict").map((o) => o.deliveryId);
13258
+ if (conflicts.length > 0) await resolveConflicts(deps, pending, conflicts, settled);
13259
+ return settled;
13260
+ }
13261
+ async function resolveConflicts(deps, pending, conflicts, settled) {
13262
+ for (const deliveryId of conflicts) {
13263
+ const entry = pending.find((p) => p.deliveryId === deliveryId);
13264
+ try {
13265
+ await rejectStaleClaim(deps, deliveryId, entry?.transferId);
13266
+ } catch (err) {
13267
+ logger.warn("PaymentsV2", `stale-reject failed for ${deliveryId} \u2014 cursor holds here:`, err);
13268
+ continue;
13269
+ }
13270
+ settled.add(deliveryId);
13271
+ }
13272
+ }
13273
+ async function rejectStaleClaim(deps, deliveryId, transferId) {
13274
+ logger.warn("PaymentsV2", `mailbox claim CONFLICT for ${deliveryId} \u2014 rejected('other') as stale`);
13275
+ await deps.delivery.ack(deliveryId, "rejected", "other");
13276
+ deps.attention(transferId ?? "", ATTENTION_CLAIM_CONFLICT, deliveryId);
13277
+ }
13278
+ async function settleOneByOne(deps, pending) {
13279
+ const settled = /* @__PURE__ */ new Set();
13280
+ for (const ack of pending) {
13281
+ try {
13282
+ await ackOne(deps, ack);
13283
+ } catch (err) {
13284
+ logger.warn("PaymentsV2", `mailbox ack failed for ${ack.deliveryId} \u2014 cursor holds here:`, err);
13285
+ break;
13286
+ }
13287
+ settled.add(ack.deliveryId);
13288
+ }
13289
+ return settled;
13290
+ }
13291
+ function toAckRequest(ack) {
13292
+ return {
13293
+ deliveryId: ack.deliveryId,
13294
+ disposition: ack.disposition,
13295
+ ...ack.reason !== void 0 ? { reason: ack.reason } : {}
13296
+ };
13297
+ }
13205
13298
  async function ackOne(deps, ack) {
13206
13299
  try {
13207
13300
  await deps.delivery.ack(ack.deliveryId, ack.disposition, ack.reason);
13208
13301
  } catch (err) {
13209
13302
  if (ack.disposition !== "claimed" || !isClaimConflict(err)) throw err;
13210
- logger.warn("PaymentsV2", `mailbox claim CONFLICT for ${ack.deliveryId} \u2014 rejected('other') as stale`);
13211
- await deps.delivery.ack(ack.deliveryId, "rejected", "other");
13212
- deps.attention(ack.transferId ?? "", ATTENTION_CLAIM_CONFLICT, ack.deliveryId);
13303
+ await rejectStaleClaim(deps, ack.deliveryId, ack.transferId);
13213
13304
  }
13214
13305
  }
13215
13306
  function claimAck(entry) {
@@ -13963,26 +14054,6 @@ function parseAmount(raw) {
13963
14054
  function supportsDeterministicMint(engine) {
13964
14055
  return engine.deterministicMint === true;
13965
14056
  }
13966
- function makeCoalescedRefresh(refresh) {
13967
- let inFlight = false;
13968
- let queued = false;
13969
- const run = () => {
13970
- inFlight = true;
13971
- void refresh().catch(() => void 0).finally(() => {
13972
- inFlight = false;
13973
- if (!queued) return;
13974
- queued = false;
13975
- run();
13976
- });
13977
- };
13978
- return () => {
13979
- if (inFlight) {
13980
- queued = true;
13981
- return;
13982
- }
13983
- run();
13984
- };
13985
- }
13986
14057
  function composeFacadeParts(deps, hooks) {
13987
14058
  const ownPubkeyBytes = hexToBytes4(deps.ownPubkey);
13988
14059
  const ledger = new ReservationLedger();
@@ -14012,9 +14083,11 @@ function composeFacadeParts(deps, hooks) {
14012
14083
  const machineDeps = buildMachineDeps(deps, hooks, historyStore);
14013
14084
  const machineStores = createMachineStores(deps.kv);
14014
14085
  const heldStates = /* @__PURE__ */ new Map();
14086
+ const refreshView = coalesced(() => view.delta());
14015
14087
  return {
14016
14088
  ownPubkeyBytes,
14017
14089
  view,
14090
+ refreshView,
14018
14091
  ledger,
14019
14092
  pins: buildPins(deps, hooks, { ledger, view, machineStores, machineDeps }),
14020
14093
  queue,
@@ -14023,7 +14096,7 @@ function composeFacadeParts(deps, hooks) {
14023
14096
  machineDeps,
14024
14097
  machine: new TransferMachine(machineDeps),
14025
14098
  heldStates,
14026
- receiveLoop: buildReceive(deps, hooks, historyStore, heldStates, view),
14099
+ receiveLoop: buildReceive(deps, hooks, historyStore, heldStates, refreshView),
14027
14100
  requests: buildRequests(deps, hooks),
14028
14101
  restoreDeps: buildRestoreDeps(deps, machineDeps, machineStores, view)
14029
14102
  };
@@ -14090,7 +14163,7 @@ function buildMachineDeps(deps, hooks, historyStore) {
14090
14163
  }
14091
14164
  };
14092
14165
  }
14093
- function buildReceive(deps, hooks, historyStore, heldStates, view) {
14166
+ function buildReceive(deps, hooks, historyStore, heldStates, refreshView) {
14094
14167
  return new Receive({
14095
14168
  delivery: deps.deliveryPort,
14096
14169
  engine: () => hooks.engine(),
@@ -14104,8 +14177,7 @@ function buildReceive(deps, hooks, historyStore, heldStates, view) {
14104
14177
  registry: deps.registry,
14105
14178
  recordReceived: (record) => recordReceived(historyStore, record),
14106
14179
  emit: (event, transfer) => deps.emit(event, transfer),
14107
- // Coalesced — see makeCoalescedRefresh.
14108
- refreshView: makeCoalescedRefresh(() => view.delta()),
14180
+ refreshView,
14109
14181
  attention: (transferId, code, detail) => {
14110
14182
  deps.emit(
14111
14183
  "transfer:attention",
@@ -14157,7 +14229,7 @@ var PaymentsFacade = class {
14157
14229
  isActiveOp: (id) => this.activeMoneyOps.has(id)
14158
14230
  });
14159
14231
  this.view = parts.view;
14160
- this.wakeRefresh = makeCoalescedRefresh(() => this.view.delta());
14232
+ this.wakeRefresh = parts.refreshView;
14161
14233
  this.ledger = parts.ledger;
14162
14234
  this.queue = parts.queue;
14163
14235
  this.machine = parts.machine;
@@ -14490,7 +14562,7 @@ var PaymentsFacade = class {
14490
14562
  ...spend.plan.direct,
14491
14563
  ...spend.plan.split !== void 0 ? [spend.plan.split.tokenId] : []
14492
14564
  ];
14493
- for (const tokenId of sourceIds) this.view.markInFlight(tokenId);
14565
+ this.view.markInFlightMany(sourceIds);
14494
14566
  if (spend.plan.split !== void 0) {
14495
14567
  this.queue.declareExpectedChange(transferId, coinId, spend.plan.split.remainderAmount);
14496
14568
  }
@@ -14776,6 +14848,10 @@ var WalletApiHttpError = class extends Error {
14776
14848
  if (retryAfter !== void 0) this.retryAfter = retryAfter;
14777
14849
  }
14778
14850
  };
14851
+ var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([0, 408, 425, 429, 500, 502, 503, 504]);
14852
+ function isRetryableStatus(err) {
14853
+ return err instanceof WalletApiHttpError && RETRYABLE_STATUSES.has(err.status);
14854
+ }
14779
14855
  function isWalletApiHttpError(err, status) {
14780
14856
  return err instanceof WalletApiHttpError && (status === void 0 || err.status === status);
14781
14857
  }
@@ -15488,6 +15564,23 @@ function computeDeliveryId(tokenIdHex, stateHashHex) {
15488
15564
  return bytesToHex3(sha256(joined));
15489
15565
  }
15490
15566
  var DELIVERY_SEEN_KEY = "delivery:seen";
15567
+ var ACK_CHUNK = 50;
15568
+ function chunked(items, size) {
15569
+ const out = [];
15570
+ for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
15571
+ return out;
15572
+ }
15573
+ var RetryableAckFailure = class extends Error {
15574
+ retryable = true;
15575
+ constructor(cause) {
15576
+ super(`mailbox ack batch could not be attempted: ${String(cause)}`);
15577
+ this.name = "RetryableAckFailure";
15578
+ this.cause = cause;
15579
+ }
15580
+ };
15581
+ function asRetryable(err) {
15582
+ return isRetryableStatus(err) ? new RetryableAckFailure(err) : err;
15583
+ }
15491
15584
  var WalletApiDeliveryPort = class {
15492
15585
  custody;
15493
15586
  client;
@@ -15653,7 +15746,53 @@ var WalletApiDeliveryPort = class {
15653
15746
  } else {
15654
15747
  await this.client.reject([deliveryId], reason);
15655
15748
  }
15656
- await this.addSeen(deliveryId);
15749
+ await this.addSeenAll([deliveryId]);
15750
+ }
15751
+ /**
15752
+ * §5.7/#757 batched settle. Claims go out grouped, rejects grouped by reason
15753
+ * (the endpoint takes one reason per call), and the seen-set is written ONCE
15754
+ * after every HTTP call has succeeded — never on a throw, so an aborted batch
15755
+ * re-lists rather than being silently forgotten.
15756
+ */
15757
+ async ackBatch(acks) {
15758
+ const settled = [];
15759
+ const outcomes = [];
15760
+ try {
15761
+ await this.settleClaims(acks, settled, outcomes);
15762
+ await this.settleRejects(acks, settled);
15763
+ } catch (err) {
15764
+ throw asRetryable(err);
15765
+ }
15766
+ await this.addSeenAll(settled);
15767
+ return [...outcomes, ...settled.map((deliveryId) => ({ deliveryId, status: "settled" }))];
15768
+ }
15769
+ async settleClaims(acks, settled, outcomes) {
15770
+ const ids = acks.filter((a) => a.disposition === "claimed").map((a) => a.deliveryId);
15771
+ for (const chunk of chunked(ids, ACK_CHUNK)) {
15772
+ const result = await this.client.claim(chunk, this.custody === "inventory");
15773
+ settled.push(...result.claimed);
15774
+ settled.push(...result.alreadyClaimed.map((e) => e.entryId));
15775
+ for (const f of result.failed) {
15776
+ if (f.code === "CONFLICT") outcomes.push({ deliveryId: f.entryId, status: "conflict" });
15777
+ else throw new MailboxClaimFailedError(f.entryId, f.code);
15778
+ }
15779
+ }
15780
+ }
15781
+ async settleRejects(acks, settled) {
15782
+ const byReason = /* @__PURE__ */ new Map();
15783
+ for (const a of acks) {
15784
+ if (a.disposition !== "rejected") continue;
15785
+ const key = a.reason ?? "";
15786
+ const group = byReason.get(key) ?? [];
15787
+ group.push(a.deliveryId);
15788
+ byReason.set(key, group);
15789
+ }
15790
+ for (const [reason, group] of byReason) {
15791
+ for (const chunk of chunked(group, ACK_CHUNK)) {
15792
+ await this.client.reject(chunk, reason === "" ? void 0 : reason);
15793
+ settled.push(...chunk);
15794
+ }
15795
+ }
15657
15796
  }
15658
15797
  onWake(cb) {
15659
15798
  if (this.wake === null) return () => void 0;
@@ -15664,10 +15803,12 @@ var WalletApiDeliveryPort = class {
15664
15803
  const raw = await this.kv.get(DELIVERY_SEEN_KEY);
15665
15804
  return new Set(raw ?? []);
15666
15805
  }
15667
- addSeen(deliveryId) {
15806
+ /** SerialChain settles-not-fulfills: a rejected write never bricks later acks. */
15807
+ addSeenAll(deliveryIds) {
15808
+ if (deliveryIds.length === 0) return Promise.resolve();
15668
15809
  return this.seenChain.enqueue(async () => {
15669
15810
  const seen = await this.readSeen();
15670
- seen.add(deliveryId);
15811
+ for (const id of deliveryIds) seen.add(id);
15671
15812
  await this.kv.set(DELIVERY_SEEN_KEY, [...seen]);
15672
15813
  });
15673
15814
  }