@unicitylabs/sphere-sdk 0.14.11-dev.5 → 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,16 +14054,6 @@ function parseAmount(raw) {
13963
14054
  function supportsDeterministicMint(engine) {
13964
14055
  return engine.deterministicMint === true;
13965
14056
  }
13966
- function makeSingleFlightRefresh(refresh) {
13967
- let inFlight = false;
13968
- return () => {
13969
- if (inFlight) return;
13970
- inFlight = true;
13971
- void refresh().catch(() => void 0).finally(() => {
13972
- inFlight = false;
13973
- });
13974
- };
13975
- }
13976
14057
  function composeFacadeParts(deps, hooks) {
13977
14058
  const ownPubkeyBytes = hexToBytes4(deps.ownPubkey);
13978
14059
  const ledger = new ReservationLedger();
@@ -14002,9 +14083,11 @@ function composeFacadeParts(deps, hooks) {
14002
14083
  const machineDeps = buildMachineDeps(deps, hooks, historyStore);
14003
14084
  const machineStores = createMachineStores(deps.kv);
14004
14085
  const heldStates = /* @__PURE__ */ new Map();
14086
+ const refreshView = coalesced(() => view.delta());
14005
14087
  return {
14006
14088
  ownPubkeyBytes,
14007
14089
  view,
14090
+ refreshView,
14008
14091
  ledger,
14009
14092
  pins: buildPins(deps, hooks, { ledger, view, machineStores, machineDeps }),
14010
14093
  queue,
@@ -14013,7 +14096,7 @@ function composeFacadeParts(deps, hooks) {
14013
14096
  machineDeps,
14014
14097
  machine: new TransferMachine(machineDeps),
14015
14098
  heldStates,
14016
- receiveLoop: buildReceive(deps, hooks, historyStore, heldStates, view),
14099
+ receiveLoop: buildReceive(deps, hooks, historyStore, heldStates, refreshView),
14017
14100
  requests: buildRequests(deps, hooks),
14018
14101
  restoreDeps: buildRestoreDeps(deps, machineDeps, machineStores, view)
14019
14102
  };
@@ -14080,7 +14163,7 @@ function buildMachineDeps(deps, hooks, historyStore) {
14080
14163
  }
14081
14164
  };
14082
14165
  }
14083
- function buildReceive(deps, hooks, historyStore, heldStates, view) {
14166
+ function buildReceive(deps, hooks, historyStore, heldStates, refreshView) {
14084
14167
  return new Receive({
14085
14168
  delivery: deps.deliveryPort,
14086
14169
  engine: () => hooks.engine(),
@@ -14094,10 +14177,7 @@ function buildReceive(deps, hooks, historyStore, heldStates, view) {
14094
14177
  registry: deps.registry,
14095
14178
  recordReceived: (record) => recordReceived(historyStore, record),
14096
14179
  emit: (event, transfer) => deps.emit(event, transfer),
14097
- // Single-flight: view.delta() enqueues on a strict-FIFO SerialChain, so
14098
- // firing per throttle tick regardless of the last one builds a queue that
14099
- // drains in a burst at the END — a slower version of the bug being fixed.
14100
- refreshView: makeSingleFlightRefresh(() => view.delta()),
14180
+ refreshView,
14101
14181
  attention: (transferId, code, detail) => {
14102
14182
  deps.emit(
14103
14183
  "transfer:attention",
@@ -14149,6 +14229,7 @@ var PaymentsFacade = class {
14149
14229
  isActiveOp: (id) => this.activeMoneyOps.has(id)
14150
14230
  });
14151
14231
  this.view = parts.view;
14232
+ this.wakeRefresh = parts.refreshView;
14152
14233
  this.ledger = parts.ledger;
14153
14234
  this.queue = parts.queue;
14154
14235
  this.machine = parts.machine;
@@ -14178,6 +14259,7 @@ var PaymentsFacade = class {
14178
14259
  now: () => this.nowMs()
14179
14260
  });
14180
14261
  }
14262
+ wakeRefresh;
14181
14263
  prewarmed = new PrewarmCache();
14182
14264
  view;
14183
14265
  ledger;
@@ -14216,9 +14298,7 @@ var PaymentsFacade = class {
14216
14298
  this.deps.session.subscribeStream("mailbox", () => {
14217
14299
  this.trackTail(this.receiveLoop.drainOnce());
14218
14300
  }),
14219
- this.deps.session.subscribeStream("inventory", () => {
14220
- this.trackTail(this.view.delta());
14221
- }),
14301
+ this.deps.session.subscribeStream("inventory", this.wakeRefresh),
14222
14302
  this.deps.session.subscribeStream("payment_requests", () => {
14223
14303
  this.trackTail(this.requests.drainIncoming());
14224
14304
  })
@@ -14482,7 +14562,7 @@ var PaymentsFacade = class {
14482
14562
  ...spend.plan.direct,
14483
14563
  ...spend.plan.split !== void 0 ? [spend.plan.split.tokenId] : []
14484
14564
  ];
14485
- for (const tokenId of sourceIds) this.view.markInFlight(tokenId);
14565
+ this.view.markInFlightMany(sourceIds);
14486
14566
  if (spend.plan.split !== void 0) {
14487
14567
  this.queue.declareExpectedChange(transferId, coinId, spend.plan.split.remainderAmount);
14488
14568
  }
@@ -14768,6 +14848,10 @@ var WalletApiHttpError = class extends Error {
14768
14848
  if (retryAfter !== void 0) this.retryAfter = retryAfter;
14769
14849
  }
14770
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
+ }
14771
14855
  function isWalletApiHttpError(err, status) {
14772
14856
  return err instanceof WalletApiHttpError && (status === void 0 || err.status === status);
14773
14857
  }
@@ -15480,6 +15564,23 @@ function computeDeliveryId(tokenIdHex, stateHashHex) {
15480
15564
  return bytesToHex3(sha256(joined));
15481
15565
  }
15482
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
+ }
15483
15584
  var WalletApiDeliveryPort = class {
15484
15585
  custody;
15485
15586
  client;
@@ -15645,7 +15746,53 @@ var WalletApiDeliveryPort = class {
15645
15746
  } else {
15646
15747
  await this.client.reject([deliveryId], reason);
15647
15748
  }
15648
- 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
+ }
15649
15796
  }
15650
15797
  onWake(cb) {
15651
15798
  if (this.wake === null) return () => void 0;
@@ -15656,10 +15803,12 @@ var WalletApiDeliveryPort = class {
15656
15803
  const raw = await this.kv.get(DELIVERY_SEEN_KEY);
15657
15804
  return new Set(raw ?? []);
15658
15805
  }
15659
- 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();
15660
15809
  return this.seenChain.enqueue(async () => {
15661
15810
  const seen = await this.readSeen();
15662
- seen.add(deliveryId);
15811
+ for (const id of deliveryIds) seen.add(id);
15663
15812
  await this.kv.set(DELIVERY_SEEN_KEY, [...seen]);
15664
15813
  });
15665
15814
  }