@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.
@@ -340,8 +340,22 @@ interface DeliveryPort {
340
340
  */
341
341
  incomingEpoch(): string | null;
342
342
  ack(deliveryId: string, disposition: 'claimed' | 'rejected', reason?: 'invalid' | 'not-owned' | 'storage-rejected' | 'other'): Promise<void>;
343
+ /** Settle many acks at once (#757). Contract pinned by the delivery-port contract suite. */
344
+ ackBatch?(acks: readonly AckRequest[]): Promise<readonly AckOutcome[]>;
343
345
  onWake?(cb: () => void): () => void;
344
346
  }
347
+ interface AckRequest {
348
+ readonly deliveryId: string;
349
+ readonly disposition: 'claimed' | 'rejected';
350
+ readonly reason?: 'invalid' | 'not-owned' | 'storage-rejected' | 'other';
351
+ }
352
+ type AckOutcome = {
353
+ readonly deliveryId: string;
354
+ readonly status: 'settled';
355
+ } | {
356
+ readonly deliveryId: string;
357
+ readonly status: 'conflict';
358
+ };
345
359
 
346
360
  interface IntentPayload {
347
361
  v: 2;
@@ -1044,6 +1058,7 @@ declare const MAX_RESELECT = 8;
1044
1058
  declare const ATTENTION_MINT_UNRESOLVED = "mint:unresolved";
1045
1059
  declare class PaymentsFacade implements PaymentsV2 {
1046
1060
  private readonly deps;
1061
+ private readonly wakeRefresh;
1047
1062
  private readonly prewarmed;
1048
1063
  private readonly view;
1049
1064
  private readonly ledger;
@@ -340,8 +340,22 @@ interface DeliveryPort {
340
340
  */
341
341
  incomingEpoch(): string | null;
342
342
  ack(deliveryId: string, disposition: 'claimed' | 'rejected', reason?: 'invalid' | 'not-owned' | 'storage-rejected' | 'other'): Promise<void>;
343
+ /** Settle many acks at once (#757). Contract pinned by the delivery-port contract suite. */
344
+ ackBatch?(acks: readonly AckRequest[]): Promise<readonly AckOutcome[]>;
343
345
  onWake?(cb: () => void): () => void;
344
346
  }
347
+ interface AckRequest {
348
+ readonly deliveryId: string;
349
+ readonly disposition: 'claimed' | 'rejected';
350
+ readonly reason?: 'invalid' | 'not-owned' | 'storage-rejected' | 'other';
351
+ }
352
+ type AckOutcome = {
353
+ readonly deliveryId: string;
354
+ readonly status: 'settled';
355
+ } | {
356
+ readonly deliveryId: string;
357
+ readonly status: 'conflict';
358
+ };
345
359
 
346
360
  interface IntentPayload {
347
361
  v: 2;
@@ -1044,6 +1058,7 @@ declare const MAX_RESELECT = 8;
1044
1058
  declare const ATTENTION_MINT_UNRESOLVED = "mint:unresolved";
1045
1059
  declare class PaymentsFacade implements PaymentsV2 {
1046
1060
  private readonly deps;
1061
+ private readonly wakeRefresh;
1047
1062
  private readonly prewarmed;
1048
1063
  private readonly view;
1049
1064
  private readonly ledger;
@@ -536,6 +536,26 @@ var SerialChain = class {
536
536
  return run;
537
537
  }
538
538
  };
539
+ function coalesced(run) {
540
+ let inFlight = false;
541
+ let queued = false;
542
+ const start = () => {
543
+ inFlight = true;
544
+ void run().catch(() => void 0).finally(() => {
545
+ inFlight = false;
546
+ if (!queued) return;
547
+ queued = false;
548
+ start();
549
+ });
550
+ };
551
+ return () => {
552
+ if (inFlight) {
553
+ queued = true;
554
+ return;
555
+ }
556
+ start();
557
+ };
558
+ }
539
559
 
540
560
  // modules/payments-v2/machine/journal.ts
541
561
  var MAX_DELIVERY_ATTEMPTS = 6;
@@ -2841,6 +2861,16 @@ var InventoryView = class {
2841
2861
  this.inFlight.add(tokenId);
2842
2862
  this.deps.emit("inventory:updated");
2843
2863
  }
2864
+ /** Reserve a whole plan as ONE change — per token emitted 54 events per send. */
2865
+ markInFlightMany(tokenIds) {
2866
+ let changed = false;
2867
+ for (const tokenId of tokenIds) {
2868
+ if (this.inFlight.has(tokenId)) continue;
2869
+ this.inFlight.add(tokenId);
2870
+ changed = true;
2871
+ }
2872
+ if (changed) this.deps.emit("inventory:updated");
2873
+ }
2844
2874
  release(tokenId) {
2845
2875
  if (this.inFlight.delete(tokenId)) this.deps.emit("inventory:updated");
2846
2876
  }
@@ -3157,6 +3187,11 @@ var logger = {
3157
3187
  }
3158
3188
  };
3159
3189
 
3190
+ // modules/payments-v2/ports.ts
3191
+ function isRetryableAckError(err) {
3192
+ return err !== null && typeof err === "object" && err.retryable === true;
3193
+ }
3194
+
3160
3195
  // modules/payments-v2/receive/Receive.ts
3161
3196
  var ACK_BATCH_SIZE = 200;
3162
3197
  var REFRESH_INTERVAL_MS = 2500;
@@ -3238,6 +3273,7 @@ var Receive = class {
3238
3273
  }
3239
3274
  } catch (err) {
3240
3275
  logger.warn("PaymentsV2", "receive drain interrupted \u2014 unacked entries retry next drain:", err);
3276
+ if (isRetryableAckError(err)) return stored;
3241
3277
  await flushAcks(deps, pending, pageEpoch).catch((flushErr) => {
3242
3278
  logger.warn("PaymentsV2", "receive ack flush failed \u2014 cursor holds at the acked prefix:", flushErr);
3243
3279
  });
@@ -3312,14 +3348,18 @@ async function announce(deps, entry, record) {
3312
3348
  };
3313
3349
  }
3314
3350
  async function flushAcks(deps, pending, epochOf) {
3351
+ if (pending.length === 0) return;
3315
3352
  let lastAcked = null;
3316
3353
  try {
3317
- while (pending.length > 0) {
3318
- const next = pending[0];
3319
- await ackOne(deps, next);
3320
- lastAcked = next.cursor;
3321
- pending.shift();
3354
+ const settled = await settleAcks(deps, pending);
3355
+ let i = 0;
3356
+ while (i < pending.length && settled.has(pending[i].deliveryId)) {
3357
+ lastAcked = pending[i].cursor;
3358
+ i += 1;
3322
3359
  }
3360
+ const stuck = pending.filter((p) => !settled.has(p.deliveryId));
3361
+ pending.length = 0;
3362
+ pending.push(...stuck);
3323
3363
  } finally {
3324
3364
  if (lastAcked !== null) {
3325
3365
  const record = { cursor: lastAcked, syncEpoch: epochOf() };
@@ -3327,14 +3367,65 @@ async function flushAcks(deps, pending, epochOf) {
3327
3367
  }
3328
3368
  }
3329
3369
  }
3370
+ async function settleAcks(deps, pending) {
3371
+ const batch = deps.delivery.ackBatch?.bind(deps.delivery);
3372
+ if (batch === void 0) return settleOneByOne(deps, pending);
3373
+ let outcomes;
3374
+ try {
3375
+ outcomes = await batch(pending.map(toAckRequest));
3376
+ } catch (err) {
3377
+ if (isRetryableAckError(err)) throw err;
3378
+ logger.warn("PaymentsV2", "batched mailbox ack failed \u2014 settling one at a time:", err);
3379
+ return settleOneByOne(deps, pending);
3380
+ }
3381
+ const settled = new Set(outcomes.filter((o) => o.status === "settled").map((o) => o.deliveryId));
3382
+ const conflicts = outcomes.filter((o) => o.status === "conflict").map((o) => o.deliveryId);
3383
+ if (conflicts.length > 0) await resolveConflicts(deps, pending, conflicts, settled);
3384
+ return settled;
3385
+ }
3386
+ async function resolveConflicts(deps, pending, conflicts, settled) {
3387
+ for (const deliveryId of conflicts) {
3388
+ const entry = pending.find((p) => p.deliveryId === deliveryId);
3389
+ try {
3390
+ await rejectStaleClaim(deps, deliveryId, entry?.transferId);
3391
+ } catch (err) {
3392
+ logger.warn("PaymentsV2", `stale-reject failed for ${deliveryId} \u2014 cursor holds here:`, err);
3393
+ continue;
3394
+ }
3395
+ settled.add(deliveryId);
3396
+ }
3397
+ }
3398
+ async function rejectStaleClaim(deps, deliveryId, transferId) {
3399
+ logger.warn("PaymentsV2", `mailbox claim CONFLICT for ${deliveryId} \u2014 rejected('other') as stale`);
3400
+ await deps.delivery.ack(deliveryId, "rejected", "other");
3401
+ deps.attention(transferId ?? "", ATTENTION_CLAIM_CONFLICT, deliveryId);
3402
+ }
3403
+ async function settleOneByOne(deps, pending) {
3404
+ const settled = /* @__PURE__ */ new Set();
3405
+ for (const ack of pending) {
3406
+ try {
3407
+ await ackOne(deps, ack);
3408
+ } catch (err) {
3409
+ logger.warn("PaymentsV2", `mailbox ack failed for ${ack.deliveryId} \u2014 cursor holds here:`, err);
3410
+ break;
3411
+ }
3412
+ settled.add(ack.deliveryId);
3413
+ }
3414
+ return settled;
3415
+ }
3416
+ function toAckRequest(ack) {
3417
+ return {
3418
+ deliveryId: ack.deliveryId,
3419
+ disposition: ack.disposition,
3420
+ ...ack.reason !== void 0 ? { reason: ack.reason } : {}
3421
+ };
3422
+ }
3330
3423
  async function ackOne(deps, ack) {
3331
3424
  try {
3332
3425
  await deps.delivery.ack(ack.deliveryId, ack.disposition, ack.reason);
3333
3426
  } catch (err) {
3334
3427
  if (ack.disposition !== "claimed" || !isClaimConflict(err)) throw err;
3335
- logger.warn("PaymentsV2", `mailbox claim CONFLICT for ${ack.deliveryId} \u2014 rejected('other') as stale`);
3336
- await deps.delivery.ack(ack.deliveryId, "rejected", "other");
3337
- deps.attention(ack.transferId ?? "", ATTENTION_CLAIM_CONFLICT, ack.deliveryId);
3428
+ await rejectStaleClaim(deps, ack.deliveryId, ack.transferId);
3338
3429
  }
3339
3430
  }
3340
3431
  function claimAck(entry) {
@@ -4088,16 +4179,6 @@ function parseAmount(raw) {
4088
4179
  function supportsDeterministicMint(engine) {
4089
4180
  return engine.deterministicMint === true;
4090
4181
  }
4091
- function makeSingleFlightRefresh(refresh) {
4092
- let inFlight = false;
4093
- return () => {
4094
- if (inFlight) return;
4095
- inFlight = true;
4096
- void refresh().catch(() => void 0).finally(() => {
4097
- inFlight = false;
4098
- });
4099
- };
4100
- }
4101
4182
  function composeFacadeParts(deps, hooks) {
4102
4183
  const ownPubkeyBytes = hexToBytes(deps.ownPubkey);
4103
4184
  const ledger = new ReservationLedger();
@@ -4127,9 +4208,11 @@ function composeFacadeParts(deps, hooks) {
4127
4208
  const machineDeps = buildMachineDeps(deps, hooks, historyStore);
4128
4209
  const machineStores = createMachineStores(deps.kv);
4129
4210
  const heldStates = /* @__PURE__ */ new Map();
4211
+ const refreshView = coalesced(() => view.delta());
4130
4212
  return {
4131
4213
  ownPubkeyBytes,
4132
4214
  view,
4215
+ refreshView,
4133
4216
  ledger,
4134
4217
  pins: buildPins(deps, hooks, { ledger, view, machineStores, machineDeps }),
4135
4218
  queue,
@@ -4138,7 +4221,7 @@ function composeFacadeParts(deps, hooks) {
4138
4221
  machineDeps,
4139
4222
  machine: new TransferMachine(machineDeps),
4140
4223
  heldStates,
4141
- receiveLoop: buildReceive(deps, hooks, historyStore, heldStates, view),
4224
+ receiveLoop: buildReceive(deps, hooks, historyStore, heldStates, refreshView),
4142
4225
  requests: buildRequests(deps, hooks),
4143
4226
  restoreDeps: buildRestoreDeps(deps, machineDeps, machineStores, view)
4144
4227
  };
@@ -4205,7 +4288,7 @@ function buildMachineDeps(deps, hooks, historyStore) {
4205
4288
  }
4206
4289
  };
4207
4290
  }
4208
- function buildReceive(deps, hooks, historyStore, heldStates, view) {
4291
+ function buildReceive(deps, hooks, historyStore, heldStates, refreshView) {
4209
4292
  return new Receive({
4210
4293
  delivery: deps.deliveryPort,
4211
4294
  engine: () => hooks.engine(),
@@ -4219,10 +4302,7 @@ function buildReceive(deps, hooks, historyStore, heldStates, view) {
4219
4302
  registry: deps.registry,
4220
4303
  recordReceived: (record) => recordReceived(historyStore, record),
4221
4304
  emit: (event, transfer) => deps.emit(event, transfer),
4222
- // Single-flight: view.delta() enqueues on a strict-FIFO SerialChain, so
4223
- // firing per throttle tick regardless of the last one builds a queue that
4224
- // drains in a burst at the END — a slower version of the bug being fixed.
4225
- refreshView: makeSingleFlightRefresh(() => view.delta()),
4305
+ refreshView,
4226
4306
  attention: (transferId, code, detail) => {
4227
4307
  deps.emit(
4228
4308
  "transfer:attention",
@@ -4274,6 +4354,7 @@ var PaymentsFacade = class {
4274
4354
  isActiveOp: (id) => this.activeMoneyOps.has(id)
4275
4355
  });
4276
4356
  this.view = parts.view;
4357
+ this.wakeRefresh = parts.refreshView;
4277
4358
  this.ledger = parts.ledger;
4278
4359
  this.queue = parts.queue;
4279
4360
  this.machine = parts.machine;
@@ -4303,6 +4384,7 @@ var PaymentsFacade = class {
4303
4384
  now: () => this.nowMs()
4304
4385
  });
4305
4386
  }
4387
+ wakeRefresh;
4306
4388
  prewarmed = new PrewarmCache();
4307
4389
  view;
4308
4390
  ledger;
@@ -4341,9 +4423,7 @@ var PaymentsFacade = class {
4341
4423
  this.deps.session.subscribeStream("mailbox", () => {
4342
4424
  this.trackTail(this.receiveLoop.drainOnce());
4343
4425
  }),
4344
- this.deps.session.subscribeStream("inventory", () => {
4345
- this.trackTail(this.view.delta());
4346
- }),
4426
+ this.deps.session.subscribeStream("inventory", this.wakeRefresh),
4347
4427
  this.deps.session.subscribeStream("payment_requests", () => {
4348
4428
  this.trackTail(this.requests.drainIncoming());
4349
4429
  })
@@ -4607,7 +4687,7 @@ var PaymentsFacade = class {
4607
4687
  ...spend.plan.direct,
4608
4688
  ...spend.plan.split !== void 0 ? [spend.plan.split.tokenId] : []
4609
4689
  ];
4610
- for (const tokenId of sourceIds) this.view.markInFlight(tokenId);
4690
+ this.view.markInFlightMany(sourceIds);
4611
4691
  if (spend.plan.split !== void 0) {
4612
4692
  this.queue.declareExpectedChange(transferId, coinId, spend.plan.split.remainderAmount);
4613
4693
  }