@unicitylabs/sphere-sdk 0.14.11-dev.2 → 0.14.11-dev.4

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.
@@ -2844,6 +2844,12 @@ var InventoryView = class {
2844
2844
  release(tokenId) {
2845
2845
  if (this.inFlight.delete(tokenId)) this.deps.emit("inventory:updated");
2846
2846
  }
2847
+ /** Release a whole set as ONE change (per-token emitted 54 events per send). */
2848
+ releaseMany(tokenIds) {
2849
+ let changed = false;
2850
+ for (const tokenId of tokenIds) if (this.inFlight.delete(tokenId)) changed = true;
2851
+ if (changed) this.deps.emit("inventory:updated");
2852
+ }
2847
2853
  pinned(tokenId) {
2848
2854
  return this.deps.isPinned?.(tokenId) ?? false;
2849
2855
  }
@@ -3188,16 +3194,28 @@ var Receive = class {
3188
3194
  this.unsubscribeWake?.();
3189
3195
  this.unsubscribeWake = null;
3190
3196
  }
3191
- /** Ask for a mirror refresh at most once per REFRESH_INTERVAL_MS. */
3192
- maybeRefresh(deps) {
3197
+ /**
3198
+ * Flush what has been accepted, then ask for a mirror refresh — at most once
3199
+ * per REFRESH_INTERVAL_MS.
3200
+ *
3201
+ * The flush is not incidental. A `claimed` ack is what materializes a token
3202
+ * into SERVER inventory, and acks otherwise sit queued until ACK_BATCH_SIZE
3203
+ * (200) — so for any ordinary drain, including the 54-token case this exists
3204
+ * for, refreshing before the flush would query an inventory holding none of
3205
+ * the tokens just accepted, and the balance would still only move at the end.
3206
+ * Ordering is preserved: store() still precedes the ack (§5.7).
3207
+ */
3208
+ async maybeRefresh(deps, pending, epochOf) {
3193
3209
  if (deps.refreshView === void 0) return;
3194
3210
  const now = deps.now?.() ?? Date.now();
3195
3211
  if (now - this.lastRefreshAt < REFRESH_INTERVAL_MS) return;
3196
3212
  this.lastRefreshAt = now;
3213
+ await flushAcks(deps, pending, epochOf);
3197
3214
  deps.refreshView();
3198
3215
  }
3199
3216
  async doDrain() {
3200
3217
  const deps = this.deps;
3218
+ this.lastRefreshAt = deps.now?.() ?? Date.now();
3201
3219
  const engine = deps.engine();
3202
3220
  const stored = [];
3203
3221
  const pending = [];
@@ -3207,9 +3225,10 @@ var Receive = class {
3207
3225
  let basis = record !== null && record.syncEpoch === deps.syncEpoch() ? record : null;
3208
3226
  for (let pass = 0; pass < 2; pass++) {
3209
3227
  for await (const entry of deps.delivery.incoming(basis === null ? void 0 : String(basis.cursor))) {
3228
+ const storedBefore = stored.length;
3210
3229
  await this.processEntry(deps, engine, entry, pending, stored);
3211
3230
  if (pending.length >= ACK_BATCH_SIZE) await flushAcks(deps, pending, pageEpoch);
3212
- if (stored.length > 0) this.maybeRefresh(deps);
3231
+ if (stored.length > storedBefore) await this.maybeRefresh(deps, pending, pageEpoch);
3213
3232
  }
3214
3233
  await flushAcks(deps, pending, pageEpoch);
3215
3234
  const served = deps.delivery.incomingEpoch();
@@ -4069,6 +4088,16 @@ function parseAmount(raw) {
4069
4088
  function supportsDeterministicMint(engine) {
4070
4089
  return engine.deterministicMint === true;
4071
4090
  }
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
+ }
4072
4101
  function composeFacadeParts(deps, hooks) {
4073
4102
  const ownPubkeyBytes = hexToBytes(deps.ownPubkey);
4074
4103
  const ledger = new ReservationLedger();
@@ -4190,11 +4219,10 @@ function buildReceive(deps, hooks, historyStore, heldStates, view) {
4190
4219
  registry: deps.registry,
4191
4220
  recordReceived: (record) => recordReceived(historyStore, record),
4192
4221
  emit: (event, transfer) => deps.emit(event, transfer),
4193
- // The mirror is the balance the wallet renders; refresh it as the drain runs
4194
- // so a multi-token receive lands progressively instead of all at the end.
4195
- refreshView: () => {
4196
- void view.delta().catch(() => void 0);
4197
- },
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()),
4198
4226
  attention: (transferId, code, detail) => {
4199
4227
  deps.emit(
4200
4228
  "transfer:attention",
@@ -4639,7 +4667,7 @@ var PaymentsFacade = class {
4639
4667
  settleFailure(transferId, coinId, releaseIds) {
4640
4668
  this.ledger.cancel(transferId);
4641
4669
  this.queue.clearExpectedChange(transferId);
4642
- for (const tokenId of releaseIds) this.view.release(tokenId);
4670
+ this.view.releaseMany(releaseIds);
4643
4671
  this.queue.notifyChange(coinId);
4644
4672
  }
4645
4673
  /** Open intent: sources stay reserved + in-flight (§5.2) until resume adopts them. */
@@ -4664,7 +4692,7 @@ var PaymentsFacade = class {
4664
4692
  async refreshThenRelease(coinId, spentIds) {
4665
4693
  try {
4666
4694
  await this.view.delta();
4667
- for (const tokenId of spentIds) this.view.release(tokenId);
4695
+ this.view.releaseMany(spentIds);
4668
4696
  } finally {
4669
4697
  this.queue.notifyChange(coinId);
4670
4698
  }