@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.
@@ -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;
@@ -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;
@@ -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,26 +4179,6 @@ function parseAmount(raw) {
4088
4179
  function supportsDeterministicMint(engine) {
4089
4180
  return engine.deterministicMint === true;
4090
4181
  }
4091
- function makeCoalescedRefresh(refresh) {
4092
- let inFlight = false;
4093
- let queued = false;
4094
- const run = () => {
4095
- inFlight = true;
4096
- void refresh().catch(() => void 0).finally(() => {
4097
- inFlight = false;
4098
- if (!queued) return;
4099
- queued = false;
4100
- run();
4101
- });
4102
- };
4103
- return () => {
4104
- if (inFlight) {
4105
- queued = true;
4106
- return;
4107
- }
4108
- run();
4109
- };
4110
- }
4111
4182
  function composeFacadeParts(deps, hooks) {
4112
4183
  const ownPubkeyBytes = hexToBytes(deps.ownPubkey);
4113
4184
  const ledger = new ReservationLedger();
@@ -4137,9 +4208,11 @@ function composeFacadeParts(deps, hooks) {
4137
4208
  const machineDeps = buildMachineDeps(deps, hooks, historyStore);
4138
4209
  const machineStores = createMachineStores(deps.kv);
4139
4210
  const heldStates = /* @__PURE__ */ new Map();
4211
+ const refreshView = coalesced(() => view.delta());
4140
4212
  return {
4141
4213
  ownPubkeyBytes,
4142
4214
  view,
4215
+ refreshView,
4143
4216
  ledger,
4144
4217
  pins: buildPins(deps, hooks, { ledger, view, machineStores, machineDeps }),
4145
4218
  queue,
@@ -4148,7 +4221,7 @@ function composeFacadeParts(deps, hooks) {
4148
4221
  machineDeps,
4149
4222
  machine: new TransferMachine(machineDeps),
4150
4223
  heldStates,
4151
- receiveLoop: buildReceive(deps, hooks, historyStore, heldStates, view),
4224
+ receiveLoop: buildReceive(deps, hooks, historyStore, heldStates, refreshView),
4152
4225
  requests: buildRequests(deps, hooks),
4153
4226
  restoreDeps: buildRestoreDeps(deps, machineDeps, machineStores, view)
4154
4227
  };
@@ -4215,7 +4288,7 @@ function buildMachineDeps(deps, hooks, historyStore) {
4215
4288
  }
4216
4289
  };
4217
4290
  }
4218
- function buildReceive(deps, hooks, historyStore, heldStates, view) {
4291
+ function buildReceive(deps, hooks, historyStore, heldStates, refreshView) {
4219
4292
  return new Receive({
4220
4293
  delivery: deps.deliveryPort,
4221
4294
  engine: () => hooks.engine(),
@@ -4229,8 +4302,7 @@ function buildReceive(deps, hooks, historyStore, heldStates, view) {
4229
4302
  registry: deps.registry,
4230
4303
  recordReceived: (record) => recordReceived(historyStore, record),
4231
4304
  emit: (event, transfer) => deps.emit(event, transfer),
4232
- // Coalesced — see makeCoalescedRefresh.
4233
- refreshView: makeCoalescedRefresh(() => view.delta()),
4305
+ refreshView,
4234
4306
  attention: (transferId, code, detail) => {
4235
4307
  deps.emit(
4236
4308
  "transfer:attention",
@@ -4282,7 +4354,7 @@ var PaymentsFacade = class {
4282
4354
  isActiveOp: (id) => this.activeMoneyOps.has(id)
4283
4355
  });
4284
4356
  this.view = parts.view;
4285
- this.wakeRefresh = makeCoalescedRefresh(() => this.view.delta());
4357
+ this.wakeRefresh = parts.refreshView;
4286
4358
  this.ledger = parts.ledger;
4287
4359
  this.queue = parts.queue;
4288
4360
  this.machine = parts.machine;
@@ -4615,7 +4687,7 @@ var PaymentsFacade = class {
4615
4687
  ...spend.plan.direct,
4616
4688
  ...spend.plan.split !== void 0 ? [spend.plan.split.tokenId] : []
4617
4689
  ];
4618
- for (const tokenId of sourceIds) this.view.markInFlight(tokenId);
4690
+ this.view.markInFlightMany(sourceIds);
4619
4691
  if (spend.plan.split !== void 0) {
4620
4692
  this.queue.declareExpectedChange(transferId, coinId, spend.plan.split.remainderAmount);
4621
4693
  }