@unicitylabs/sphere-sdk 0.14.11-dev.6 → 0.14.11-dev.8

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,33 @@ var SerialChain = class {
581
581
  return run;
582
582
  }
583
583
  };
584
+ function coalesced(run, observe) {
585
+ let inFlight = false;
586
+ let queued = false;
587
+ const start = () => {
588
+ inFlight = true;
589
+ let running;
590
+ try {
591
+ running = run();
592
+ } catch {
593
+ running = Promise.resolve();
594
+ }
595
+ observe?.(running);
596
+ void running.catch(() => void 0).finally(() => {
597
+ inFlight = false;
598
+ if (!queued) return;
599
+ queued = false;
600
+ start();
601
+ });
602
+ };
603
+ return () => {
604
+ if (inFlight) {
605
+ queued = true;
606
+ return;
607
+ }
608
+ start();
609
+ };
610
+ }
584
611
 
585
612
  // modules/payments-v2/machine/journal.ts
586
613
  var MAX_DELIVERY_ATTEMPTS = 6;
@@ -2886,6 +2913,16 @@ var InventoryView = class {
2886
2913
  this.inFlight.add(tokenId);
2887
2914
  this.deps.emit("inventory:updated");
2888
2915
  }
2916
+ /** Reserve a whole plan as ONE change — per token emitted 54 events per send. */
2917
+ markInFlightMany(tokenIds) {
2918
+ let changed = false;
2919
+ for (const tokenId of tokenIds) {
2920
+ if (this.inFlight.has(tokenId)) continue;
2921
+ this.inFlight.add(tokenId);
2922
+ changed = true;
2923
+ }
2924
+ if (changed) this.deps.emit("inventory:updated");
2925
+ }
2889
2926
  release(tokenId) {
2890
2927
  if (this.inFlight.delete(tokenId)) this.deps.emit("inventory:updated");
2891
2928
  }
@@ -3202,6 +3239,11 @@ var logger = {
3202
3239
  }
3203
3240
  };
3204
3241
 
3242
+ // modules/payments-v2/ports.ts
3243
+ function isRetryableAckError(err) {
3244
+ return err !== null && typeof err === "object" && err.retryable === true;
3245
+ }
3246
+
3205
3247
  // modules/payments-v2/receive/Receive.ts
3206
3248
  var ACK_BATCH_SIZE = 200;
3207
3249
  var REFRESH_INTERVAL_MS = 2500;
@@ -3258,6 +3300,17 @@ var Receive = class {
3258
3300
  await flushAcks(deps, pending, epochOf);
3259
3301
  deps.refreshView();
3260
3302
  }
3303
+ /** One listing pass: process each entry, flushing and refreshing as it goes. */
3304
+ async drainPages(ctx, basis) {
3305
+ const { deps, engine, pending, stored, pageEpoch } = ctx;
3306
+ for await (const entry of deps.delivery.incoming(basis === null ? void 0 : String(basis.cursor))) {
3307
+ const storedBefore = stored.length;
3308
+ await this.processEntry(deps, engine, entry, pending, stored);
3309
+ if (pending.length >= ACK_BATCH_SIZE) await flushAcks(deps, pending, pageEpoch);
3310
+ if (stored.length > storedBefore) await this.maybeRefresh(deps, pending, pageEpoch);
3311
+ }
3312
+ await flushAcks(deps, pending, pageEpoch);
3313
+ }
3261
3314
  async doDrain() {
3262
3315
  const deps = this.deps;
3263
3316
  this.lastRefreshAt = deps.now?.() ?? Date.now();
@@ -3269,13 +3322,7 @@ var Receive = class {
3269
3322
  const record = await deps.kv.get(STORE_KEYS.streamCursor("mailbox"));
3270
3323
  let basis = record !== null && record.syncEpoch === deps.syncEpoch() ? record : null;
3271
3324
  for (let pass = 0; pass < 2; pass++) {
3272
- for await (const entry of deps.delivery.incoming(basis === null ? void 0 : String(basis.cursor))) {
3273
- const storedBefore = stored.length;
3274
- await this.processEntry(deps, engine, entry, pending, stored);
3275
- if (pending.length >= ACK_BATCH_SIZE) await flushAcks(deps, pending, pageEpoch);
3276
- if (stored.length > storedBefore) await this.maybeRefresh(deps, pending, pageEpoch);
3277
- }
3278
- await flushAcks(deps, pending, pageEpoch);
3325
+ await this.drainPages({ deps, engine, pending, stored, pageEpoch }, basis);
3279
3326
  const served = deps.delivery.incomingEpoch();
3280
3327
  if (basis === null || served === null || served === basis.syncEpoch) break;
3281
3328
  await deps.kv.remove(STORE_KEYS.streamCursor("mailbox"));
@@ -3283,10 +3330,12 @@ var Receive = class {
3283
3330
  }
3284
3331
  } catch (err) {
3285
3332
  logger.warn("PaymentsV2", "receive drain interrupted \u2014 unacked entries retry next drain:", err);
3333
+ if (isRetryableAckError(err)) return stored;
3286
3334
  await flushAcks(deps, pending, pageEpoch).catch((flushErr) => {
3287
3335
  logger.warn("PaymentsV2", "receive ack flush failed \u2014 cursor holds at the acked prefix:", flushErr);
3288
3336
  });
3289
3337
  }
3338
+ if (stored.length > 0) deps.refreshView?.();
3290
3339
  return stored;
3291
3340
  }
3292
3341
  async processEntry(deps, engine, entry, pending, stored) {
@@ -3357,14 +3406,18 @@ async function announce(deps, entry, record) {
3357
3406
  };
3358
3407
  }
3359
3408
  async function flushAcks(deps, pending, epochOf) {
3409
+ if (pending.length === 0) return;
3360
3410
  let lastAcked = null;
3361
3411
  try {
3362
- while (pending.length > 0) {
3363
- const next = pending[0];
3364
- await ackOne(deps, next);
3365
- lastAcked = next.cursor;
3366
- pending.shift();
3412
+ const settled = await settleAcks(deps, pending);
3413
+ let i = 0;
3414
+ while (i < pending.length && settled.has(pending[i].deliveryId)) {
3415
+ lastAcked = pending[i].cursor;
3416
+ i += 1;
3367
3417
  }
3418
+ const stuck = pending.filter((p) => !settled.has(p.deliveryId));
3419
+ pending.length = 0;
3420
+ pending.push(...stuck);
3368
3421
  } finally {
3369
3422
  if (lastAcked !== null) {
3370
3423
  const record = { cursor: lastAcked, syncEpoch: epochOf() };
@@ -3372,14 +3425,65 @@ async function flushAcks(deps, pending, epochOf) {
3372
3425
  }
3373
3426
  }
3374
3427
  }
3428
+ async function settleAcks(deps, pending) {
3429
+ const batch = deps.delivery.ackBatch?.bind(deps.delivery);
3430
+ if (batch === void 0) return settleOneByOne(deps, pending);
3431
+ let outcomes;
3432
+ try {
3433
+ outcomes = await batch(pending.map(toAckRequest));
3434
+ } catch (err) {
3435
+ if (isRetryableAckError(err)) throw err;
3436
+ logger.warn("PaymentsV2", "batched mailbox ack failed \u2014 settling one at a time:", err);
3437
+ return settleOneByOne(deps, pending);
3438
+ }
3439
+ const settled = new Set(outcomes.filter((o) => o.status === "settled").map((o) => o.deliveryId));
3440
+ const conflicts = outcomes.filter((o) => o.status === "conflict").map((o) => o.deliveryId);
3441
+ if (conflicts.length > 0) await resolveConflicts(deps, pending, conflicts, settled);
3442
+ return settled;
3443
+ }
3444
+ async function resolveConflicts(deps, pending, conflicts, settled) {
3445
+ for (const deliveryId of conflicts) {
3446
+ const entry = pending.find((p) => p.deliveryId === deliveryId);
3447
+ try {
3448
+ await rejectStaleClaim(deps, deliveryId, entry?.transferId);
3449
+ } catch (err) {
3450
+ logger.warn("PaymentsV2", `stale-reject failed for ${deliveryId} \u2014 cursor holds here:`, err);
3451
+ continue;
3452
+ }
3453
+ settled.add(deliveryId);
3454
+ }
3455
+ }
3456
+ async function rejectStaleClaim(deps, deliveryId, transferId) {
3457
+ logger.warn("PaymentsV2", `mailbox claim CONFLICT for ${deliveryId} \u2014 rejected('other') as stale`);
3458
+ await deps.delivery.ack(deliveryId, "rejected", "other");
3459
+ deps.attention(transferId ?? "", ATTENTION_CLAIM_CONFLICT, deliveryId);
3460
+ }
3461
+ async function settleOneByOne(deps, pending) {
3462
+ const settled = /* @__PURE__ */ new Set();
3463
+ for (const ack of pending) {
3464
+ try {
3465
+ await ackOne(deps, ack);
3466
+ } catch (err) {
3467
+ logger.warn("PaymentsV2", `mailbox ack failed for ${ack.deliveryId} \u2014 cursor holds here:`, err);
3468
+ break;
3469
+ }
3470
+ settled.add(ack.deliveryId);
3471
+ }
3472
+ return settled;
3473
+ }
3474
+ function toAckRequest(ack) {
3475
+ return {
3476
+ deliveryId: ack.deliveryId,
3477
+ disposition: ack.disposition,
3478
+ ...ack.reason !== void 0 ? { reason: ack.reason } : {}
3479
+ };
3480
+ }
3375
3481
  async function ackOne(deps, ack) {
3376
3482
  try {
3377
3483
  await deps.delivery.ack(ack.deliveryId, ack.disposition, ack.reason);
3378
3484
  } catch (err) {
3379
3485
  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);
3486
+ await rejectStaleClaim(deps, ack.deliveryId, ack.transferId);
3383
3487
  }
3384
3488
  }
3385
3489
  function claimAck(entry) {
@@ -4133,26 +4237,6 @@ function parseAmount(raw) {
4133
4237
  function supportsDeterministicMint(engine) {
4134
4238
  return engine.deterministicMint === true;
4135
4239
  }
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
4240
  function composeFacadeParts(deps, hooks) {
4157
4241
  const ownPubkeyBytes = hexToBytes(deps.ownPubkey);
4158
4242
  const ledger = new ReservationLedger();
@@ -4182,9 +4266,11 @@ function composeFacadeParts(deps, hooks) {
4182
4266
  const machineDeps = buildMachineDeps(deps, hooks, historyStore);
4183
4267
  const machineStores = createMachineStores(deps.kv);
4184
4268
  const heldStates = /* @__PURE__ */ new Map();
4269
+ const refreshView = coalesced(() => view.delta(), hooks.track);
4185
4270
  return {
4186
4271
  ownPubkeyBytes,
4187
4272
  view,
4273
+ refreshView,
4188
4274
  ledger,
4189
4275
  pins: buildPins(deps, hooks, { ledger, view, machineStores, machineDeps }),
4190
4276
  queue,
@@ -4193,7 +4279,7 @@ function composeFacadeParts(deps, hooks) {
4193
4279
  machineDeps,
4194
4280
  machine: new TransferMachine(machineDeps),
4195
4281
  heldStates,
4196
- receiveLoop: buildReceive(deps, hooks, historyStore, heldStates, view),
4282
+ receiveLoop: buildReceive(deps, hooks, historyStore, heldStates, refreshView),
4197
4283
  requests: buildRequests(deps, hooks),
4198
4284
  restoreDeps: buildRestoreDeps(deps, machineDeps, machineStores, view)
4199
4285
  };
@@ -4260,7 +4346,7 @@ function buildMachineDeps(deps, hooks, historyStore) {
4260
4346
  }
4261
4347
  };
4262
4348
  }
4263
- function buildReceive(deps, hooks, historyStore, heldStates, view) {
4349
+ function buildReceive(deps, hooks, historyStore, heldStates, refreshView) {
4264
4350
  return new Receive({
4265
4351
  delivery: deps.deliveryPort,
4266
4352
  engine: () => hooks.engine(),
@@ -4274,8 +4360,7 @@ function buildReceive(deps, hooks, historyStore, heldStates, view) {
4274
4360
  registry: deps.registry,
4275
4361
  recordReceived: (record) => recordReceived(historyStore, record),
4276
4362
  emit: (event, transfer) => deps.emit(event, transfer),
4277
- // Coalesced — see makeCoalescedRefresh.
4278
- refreshView: makeCoalescedRefresh(() => view.delta()),
4363
+ refreshView,
4279
4364
  attention: (transferId, code, detail) => {
4280
4365
  deps.emit(
4281
4366
  "transfer:attention",
@@ -4324,10 +4409,11 @@ var PaymentsFacade = class {
4324
4409
  const parts = composeFacadeParts(deps, {
4325
4410
  engine: () => this.engine(),
4326
4411
  send: (request) => this.send(request),
4327
- isActiveOp: (id) => this.activeMoneyOps.has(id)
4412
+ isActiveOp: (id) => this.activeMoneyOps.has(id),
4413
+ track: (op) => this.trackTail(op)
4328
4414
  });
4329
4415
  this.view = parts.view;
4330
- this.wakeRefresh = makeCoalescedRefresh(() => this.view.delta());
4416
+ this.wakeRefresh = parts.refreshView;
4331
4417
  this.ledger = parts.ledger;
4332
4418
  this.queue = parts.queue;
4333
4419
  this.machine = parts.machine;
@@ -4464,7 +4550,6 @@ var PaymentsFacade = class {
4464
4550
  }
4465
4551
  async receive() {
4466
4552
  const transfers = await this.track(this.receiveLoop.drainOnce());
4467
- if (transfers.length > 0) this.trackTail(this.view.delta());
4468
4553
  return { transfers };
4469
4554
  }
4470
4555
  mint(coinId, amount) {
@@ -4660,7 +4745,7 @@ var PaymentsFacade = class {
4660
4745
  ...spend.plan.direct,
4661
4746
  ...spend.plan.split !== void 0 ? [spend.plan.split.tokenId] : []
4662
4747
  ];
4663
- for (const tokenId of sourceIds) this.view.markInFlight(tokenId);
4748
+ this.view.markInFlightMany(sourceIds);
4664
4749
  if (spend.plan.split !== void 0) {
4665
4750
  this.queue.declareExpectedChange(transferId, coinId, spend.plan.split.remainderAmount);
4666
4751
  }