@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.
@@ -581,6 +581,26 @@ var SerialChain = class {
581
581
  return run;
582
582
  }
583
583
  };
584
+ function coalesced(run) {
585
+ let inFlight = false;
586
+ let queued = false;
587
+ const start = () => {
588
+ inFlight = true;
589
+ void run().catch(() => void 0).finally(() => {
590
+ inFlight = false;
591
+ if (!queued) return;
592
+ queued = false;
593
+ start();
594
+ });
595
+ };
596
+ return () => {
597
+ if (inFlight) {
598
+ queued = true;
599
+ return;
600
+ }
601
+ start();
602
+ };
603
+ }
584
604
 
585
605
  // modules/payments-v2/machine/journal.ts
586
606
  var MAX_DELIVERY_ATTEMPTS = 6;
@@ -2886,6 +2906,16 @@ var InventoryView = class {
2886
2906
  this.inFlight.add(tokenId);
2887
2907
  this.deps.emit("inventory:updated");
2888
2908
  }
2909
+ /** Reserve a whole plan as ONE change — per token emitted 54 events per send. */
2910
+ markInFlightMany(tokenIds) {
2911
+ let changed = false;
2912
+ for (const tokenId of tokenIds) {
2913
+ if (this.inFlight.has(tokenId)) continue;
2914
+ this.inFlight.add(tokenId);
2915
+ changed = true;
2916
+ }
2917
+ if (changed) this.deps.emit("inventory:updated");
2918
+ }
2889
2919
  release(tokenId) {
2890
2920
  if (this.inFlight.delete(tokenId)) this.deps.emit("inventory:updated");
2891
2921
  }
@@ -3202,6 +3232,11 @@ var logger = {
3202
3232
  }
3203
3233
  };
3204
3234
 
3235
+ // modules/payments-v2/ports.ts
3236
+ function isRetryableAckError(err) {
3237
+ return err !== null && typeof err === "object" && err.retryable === true;
3238
+ }
3239
+
3205
3240
  // modules/payments-v2/receive/Receive.ts
3206
3241
  var ACK_BATCH_SIZE = 200;
3207
3242
  var REFRESH_INTERVAL_MS = 2500;
@@ -3283,6 +3318,7 @@ var Receive = class {
3283
3318
  }
3284
3319
  } catch (err) {
3285
3320
  logger.warn("PaymentsV2", "receive drain interrupted \u2014 unacked entries retry next drain:", err);
3321
+ if (isRetryableAckError(err)) return stored;
3286
3322
  await flushAcks(deps, pending, pageEpoch).catch((flushErr) => {
3287
3323
  logger.warn("PaymentsV2", "receive ack flush failed \u2014 cursor holds at the acked prefix:", flushErr);
3288
3324
  });
@@ -3357,14 +3393,18 @@ async function announce(deps, entry, record) {
3357
3393
  };
3358
3394
  }
3359
3395
  async function flushAcks(deps, pending, epochOf) {
3396
+ if (pending.length === 0) return;
3360
3397
  let lastAcked = null;
3361
3398
  try {
3362
- while (pending.length > 0) {
3363
- const next = pending[0];
3364
- await ackOne(deps, next);
3365
- lastAcked = next.cursor;
3366
- pending.shift();
3399
+ const settled = await settleAcks(deps, pending);
3400
+ let i = 0;
3401
+ while (i < pending.length && settled.has(pending[i].deliveryId)) {
3402
+ lastAcked = pending[i].cursor;
3403
+ i += 1;
3367
3404
  }
3405
+ const stuck = pending.filter((p) => !settled.has(p.deliveryId));
3406
+ pending.length = 0;
3407
+ pending.push(...stuck);
3368
3408
  } finally {
3369
3409
  if (lastAcked !== null) {
3370
3410
  const record = { cursor: lastAcked, syncEpoch: epochOf() };
@@ -3372,14 +3412,65 @@ async function flushAcks(deps, pending, epochOf) {
3372
3412
  }
3373
3413
  }
3374
3414
  }
3415
+ async function settleAcks(deps, pending) {
3416
+ const batch = deps.delivery.ackBatch?.bind(deps.delivery);
3417
+ if (batch === void 0) return settleOneByOne(deps, pending);
3418
+ let outcomes;
3419
+ try {
3420
+ outcomes = await batch(pending.map(toAckRequest));
3421
+ } catch (err) {
3422
+ if (isRetryableAckError(err)) throw err;
3423
+ logger.warn("PaymentsV2", "batched mailbox ack failed \u2014 settling one at a time:", err);
3424
+ return settleOneByOne(deps, pending);
3425
+ }
3426
+ const settled = new Set(outcomes.filter((o) => o.status === "settled").map((o) => o.deliveryId));
3427
+ const conflicts = outcomes.filter((o) => o.status === "conflict").map((o) => o.deliveryId);
3428
+ if (conflicts.length > 0) await resolveConflicts(deps, pending, conflicts, settled);
3429
+ return settled;
3430
+ }
3431
+ async function resolveConflicts(deps, pending, conflicts, settled) {
3432
+ for (const deliveryId of conflicts) {
3433
+ const entry = pending.find((p) => p.deliveryId === deliveryId);
3434
+ try {
3435
+ await rejectStaleClaim(deps, deliveryId, entry?.transferId);
3436
+ } catch (err) {
3437
+ logger.warn("PaymentsV2", `stale-reject failed for ${deliveryId} \u2014 cursor holds here:`, err);
3438
+ continue;
3439
+ }
3440
+ settled.add(deliveryId);
3441
+ }
3442
+ }
3443
+ async function rejectStaleClaim(deps, deliveryId, transferId) {
3444
+ logger.warn("PaymentsV2", `mailbox claim CONFLICT for ${deliveryId} \u2014 rejected('other') as stale`);
3445
+ await deps.delivery.ack(deliveryId, "rejected", "other");
3446
+ deps.attention(transferId ?? "", ATTENTION_CLAIM_CONFLICT, deliveryId);
3447
+ }
3448
+ async function settleOneByOne(deps, pending) {
3449
+ const settled = /* @__PURE__ */ new Set();
3450
+ for (const ack of pending) {
3451
+ try {
3452
+ await ackOne(deps, ack);
3453
+ } catch (err) {
3454
+ logger.warn("PaymentsV2", `mailbox ack failed for ${ack.deliveryId} \u2014 cursor holds here:`, err);
3455
+ break;
3456
+ }
3457
+ settled.add(ack.deliveryId);
3458
+ }
3459
+ return settled;
3460
+ }
3461
+ function toAckRequest(ack) {
3462
+ return {
3463
+ deliveryId: ack.deliveryId,
3464
+ disposition: ack.disposition,
3465
+ ...ack.reason !== void 0 ? { reason: ack.reason } : {}
3466
+ };
3467
+ }
3375
3468
  async function ackOne(deps, ack) {
3376
3469
  try {
3377
3470
  await deps.delivery.ack(ack.deliveryId, ack.disposition, ack.reason);
3378
3471
  } catch (err) {
3379
3472
  if (ack.disposition !== "claimed" || !isClaimConflict(err)) throw err;
3380
- logger.warn("PaymentsV2", `mailbox claim CONFLICT for ${ack.deliveryId} \u2014 rejected('other') as stale`);
3381
- await deps.delivery.ack(ack.deliveryId, "rejected", "other");
3382
- deps.attention(ack.transferId ?? "", ATTENTION_CLAIM_CONFLICT, ack.deliveryId);
3473
+ await rejectStaleClaim(deps, ack.deliveryId, ack.transferId);
3383
3474
  }
3384
3475
  }
3385
3476
  function claimAck(entry) {
@@ -4133,26 +4224,6 @@ function parseAmount(raw) {
4133
4224
  function supportsDeterministicMint(engine) {
4134
4225
  return engine.deterministicMint === true;
4135
4226
  }
4136
- function makeCoalescedRefresh(refresh) {
4137
- let inFlight = false;
4138
- let queued = false;
4139
- const run = () => {
4140
- inFlight = true;
4141
- void refresh().catch(() => void 0).finally(() => {
4142
- inFlight = false;
4143
- if (!queued) return;
4144
- queued = false;
4145
- run();
4146
- });
4147
- };
4148
- return () => {
4149
- if (inFlight) {
4150
- queued = true;
4151
- return;
4152
- }
4153
- run();
4154
- };
4155
- }
4156
4227
  function composeFacadeParts(deps, hooks) {
4157
4228
  const ownPubkeyBytes = hexToBytes(deps.ownPubkey);
4158
4229
  const ledger = new ReservationLedger();
@@ -4182,9 +4253,11 @@ function composeFacadeParts(deps, hooks) {
4182
4253
  const machineDeps = buildMachineDeps(deps, hooks, historyStore);
4183
4254
  const machineStores = createMachineStores(deps.kv);
4184
4255
  const heldStates = /* @__PURE__ */ new Map();
4256
+ const refreshView = coalesced(() => view.delta());
4185
4257
  return {
4186
4258
  ownPubkeyBytes,
4187
4259
  view,
4260
+ refreshView,
4188
4261
  ledger,
4189
4262
  pins: buildPins(deps, hooks, { ledger, view, machineStores, machineDeps }),
4190
4263
  queue,
@@ -4193,7 +4266,7 @@ function composeFacadeParts(deps, hooks) {
4193
4266
  machineDeps,
4194
4267
  machine: new TransferMachine(machineDeps),
4195
4268
  heldStates,
4196
- receiveLoop: buildReceive(deps, hooks, historyStore, heldStates, view),
4269
+ receiveLoop: buildReceive(deps, hooks, historyStore, heldStates, refreshView),
4197
4270
  requests: buildRequests(deps, hooks),
4198
4271
  restoreDeps: buildRestoreDeps(deps, machineDeps, machineStores, view)
4199
4272
  };
@@ -4260,7 +4333,7 @@ function buildMachineDeps(deps, hooks, historyStore) {
4260
4333
  }
4261
4334
  };
4262
4335
  }
4263
- function buildReceive(deps, hooks, historyStore, heldStates, view) {
4336
+ function buildReceive(deps, hooks, historyStore, heldStates, refreshView) {
4264
4337
  return new Receive({
4265
4338
  delivery: deps.deliveryPort,
4266
4339
  engine: () => hooks.engine(),
@@ -4274,8 +4347,7 @@ function buildReceive(deps, hooks, historyStore, heldStates, view) {
4274
4347
  registry: deps.registry,
4275
4348
  recordReceived: (record) => recordReceived(historyStore, record),
4276
4349
  emit: (event, transfer) => deps.emit(event, transfer),
4277
- // Coalesced — see makeCoalescedRefresh.
4278
- refreshView: makeCoalescedRefresh(() => view.delta()),
4350
+ refreshView,
4279
4351
  attention: (transferId, code, detail) => {
4280
4352
  deps.emit(
4281
4353
  "transfer:attention",
@@ -4327,7 +4399,7 @@ var PaymentsFacade = class {
4327
4399
  isActiveOp: (id) => this.activeMoneyOps.has(id)
4328
4400
  });
4329
4401
  this.view = parts.view;
4330
- this.wakeRefresh = makeCoalescedRefresh(() => this.view.delta());
4402
+ this.wakeRefresh = parts.refreshView;
4331
4403
  this.ledger = parts.ledger;
4332
4404
  this.queue = parts.queue;
4333
4405
  this.machine = parts.machine;
@@ -4660,7 +4732,7 @@ var PaymentsFacade = class {
4660
4732
  ...spend.plan.direct,
4661
4733
  ...spend.plan.split !== void 0 ? [spend.plan.split.tokenId] : []
4662
4734
  ];
4663
- for (const tokenId of sourceIds) this.view.markInFlight(tokenId);
4735
+ this.view.markInFlightMany(sourceIds);
4664
4736
  if (spend.plan.split !== void 0) {
4665
4737
  this.queue.declareExpectedChange(transferId, coinId, spend.plan.split.remainderAmount);
4666
4738
  }