@unicitylabs/sphere-sdk 0.14.0-dev.5 → 0.14.0-dev.6

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.
@@ -26077,6 +26077,10 @@ function isValidationReject(err) {
26077
26077
  if (e === null) return false;
26078
26078
  return e.status === 422 || e.statusCode === 422 || e.code === "VALIDATION" || e.code === "VALIDATION_FAILED";
26079
26079
  }
26080
+ function retryAfterMsOf(err) {
26081
+ const retryAfter = err !== null && typeof err === "object" ? err.retryAfter : void 0;
26082
+ return typeof retryAfter === "number" && Number.isFinite(retryAfter) && retryAfter >= 0 ? retryAfter : null;
26083
+ }
26080
26084
  var chainRegistry = /* @__PURE__ */ new WeakMap();
26081
26085
  var replayRegistry = /* @__PURE__ */ new WeakMap();
26082
26086
  function registryFor(registry, kv, storeKey, make) {
@@ -26714,6 +26718,217 @@ function validatePayload(raw) {
26714
26718
  return p;
26715
26719
  }
26716
26720
 
26721
+ // modules/payments-v2/convergence.ts
26722
+ var HEARTBEAT_SEED_MS = 5e3;
26723
+ var HEARTBEAT_CAP_MS = 12e4;
26724
+ var ConvergenceHeartbeat = class {
26725
+ constructor(deps) {
26726
+ this.deps = deps;
26727
+ }
26728
+ timer = null;
26729
+ delayMs = HEARTBEAT_SEED_MS;
26730
+ notBefore = 0;
26731
+ running = false;
26732
+ start() {
26733
+ this.running = true;
26734
+ this.delayMs = HEARTBEAT_SEED_MS;
26735
+ this.notBefore = 0;
26736
+ }
26737
+ /** §7 quiescence unchanged: a mid-backoff stop() cancels cleanly. */
26738
+ stop() {
26739
+ this.running = false;
26740
+ this.clearTimer();
26741
+ }
26742
+ /** Reset to seed on progress, ×2 to the cap otherwise; idle stops rescheduling. */
26743
+ settle(outcome, pending) {
26744
+ if (outcome.retryAfterMs !== null) {
26745
+ this.notBefore = Math.max(this.notBefore, this.deps.now() + outcome.retryAfterMs);
26746
+ }
26747
+ if (!pending) {
26748
+ this.delayMs = HEARTBEAT_SEED_MS;
26749
+ return;
26750
+ }
26751
+ const delay = outcome.progress ? HEARTBEAT_SEED_MS : this.delayMs;
26752
+ this.delayMs = Math.min(delay * 2, HEARTBEAT_CAP_MS);
26753
+ this.schedule(delay);
26754
+ }
26755
+ /** New pending work (keep-open park / journaled delivery / mint hold): prompt retry. */
26756
+ arm() {
26757
+ if (!this.running) return;
26758
+ this.delayMs = Math.min(HEARTBEAT_SEED_MS * 2, HEARTBEAT_CAP_MS);
26759
+ this.schedule(HEARTBEAT_SEED_MS);
26760
+ }
26761
+ /** connection:status recovery: the backoff resets; a pending tick re-fires at the seed. */
26762
+ recovered() {
26763
+ this.delayMs = HEARTBEAT_SEED_MS;
26764
+ if (this.timer !== null) this.arm();
26765
+ }
26766
+ schedule(delayMs) {
26767
+ this.clearTimer();
26768
+ if (!this.running) return;
26769
+ const floored = Math.max(delayMs, this.notBefore - this.deps.now());
26770
+ this.timer = setTimeout(() => {
26771
+ this.timer = null;
26772
+ this.deps.onTick();
26773
+ }, floored);
26774
+ }
26775
+ clearTimer() {
26776
+ if (this.timer !== null) clearTimeout(this.timer);
26777
+ this.timer = null;
26778
+ }
26779
+ };
26780
+ var Converger = class {
26781
+ constructor(deps) {
26782
+ this.deps = deps;
26783
+ }
26784
+ /** Resume open intents, then drain the delivery + mint journals (all idempotent). */
26785
+ async convergeOnce() {
26786
+ let progress = false;
26787
+ let retryAfterMs = null;
26788
+ try {
26789
+ progress = await this.resumePass();
26790
+ } catch (err) {
26791
+ retryAfterMs = retryAfterMsOf(err);
26792
+ }
26793
+ try {
26794
+ const stats = await this.deps.stores.deliveryJournal.replay(this.replayDeps());
26795
+ progress = progress || stats.delivered > 0;
26796
+ } catch (err) {
26797
+ retryAfterMs ??= retryAfterMsOf(err);
26798
+ }
26799
+ try {
26800
+ progress = await this.deps.replayMints() > 0 || progress;
26801
+ } catch (err) {
26802
+ retryAfterMs ??= retryAfterMsOf(err);
26803
+ }
26804
+ return { progress, retryAfterMs };
26805
+ }
26806
+ /** §4 heartbeat predicate — read fresh from the §6 stores, never cached. */
26807
+ async pendingWork() {
26808
+ const [backstop, journal, mints] = await Promise.all([
26809
+ this.deps.stores.backstop.list(),
26810
+ this.deps.stores.deliveryJournal.list(),
26811
+ this.deps.stores.mintJournal.list()
26812
+ ]);
26813
+ return backstop.length > 0 || journal.some((e) => e.undeliverable !== true) || mints.length > 0;
26814
+ }
26815
+ async resumePass() {
26816
+ const report = await resumeAll(this.filteredMachineDeps());
26817
+ const progress = report.resumed.length > 0 || report.conflicted.length > 0;
26818
+ await this.deps.reconcile({
26819
+ resumed: report.resumed,
26820
+ conflicted: report.conflicted,
26821
+ open: report.failed
26822
+ });
26823
+ if (progress) await this.deps.refreshView().catch(() => void 0);
26824
+ return progress;
26825
+ }
26826
+ filteredMachineDeps() {
26827
+ const intents = this.deps.machineDeps.intents;
26828
+ return {
26829
+ ...this.deps.machineDeps,
26830
+ intents: {
26831
+ ...intents,
26832
+ listOpen: async () => (await intents.listOpen()).filter((intent) => !this.deps.isActiveOp(intent.transferId))
26833
+ }
26834
+ };
26835
+ }
26836
+ replayDeps() {
26837
+ return {
26838
+ delivery: this.deps.delivery,
26839
+ now: () => this.deps.now(),
26840
+ attention: (transferId, code, detail) => {
26841
+ this.deps.emit(
26842
+ "transfer:attention",
26843
+ detail === void 0 ? { transferId, code } : { transferId, code, detail }
26844
+ );
26845
+ }
26846
+ };
26847
+ }
26848
+ };
26849
+ async function derivePendingTransfers(stores, decryptPayload) {
26850
+ const [backstop, journal, shortfalls] = await Promise.all([
26851
+ stores.backstop.list(),
26852
+ stores.deliveryJournal.list(),
26853
+ stores.shortfalls.list()
26854
+ ]);
26855
+ const byTransfer = groupJournal(journal);
26856
+ const out = [];
26857
+ const seen = /* @__PURE__ */ new Set();
26858
+ for (const entry of backstop) {
26859
+ if (entry.disposition !== "open") continue;
26860
+ seen.add(entry.transferId);
26861
+ out.push(await openRow(entry, byTransfer.get(entry.transferId) ?? [], decryptPayload));
26862
+ }
26863
+ for (const [transferId, entries] of byTransfer) {
26864
+ if (seen.has(transferId)) continue;
26865
+ seen.add(transferId);
26866
+ out.push(journalOnlyRow(transferId, entries));
26867
+ }
26868
+ for (const entry of shortfalls) {
26869
+ if (!seen.has(entry.transferId)) out.push(shortfallRow(entry));
26870
+ }
26871
+ return out;
26872
+ }
26873
+ function groupJournal(journal) {
26874
+ const byTransfer = /* @__PURE__ */ new Map();
26875
+ for (const entry of journal) {
26876
+ const mine = byTransfer.get(entry.transferId);
26877
+ if (mine === void 0) byTransfer.set(entry.transferId, [entry]);
26878
+ else mine.push(entry);
26879
+ }
26880
+ return byTransfer;
26881
+ }
26882
+ async function openRow(entry, journal, decryptPayload) {
26883
+ let payload = null;
26884
+ try {
26885
+ const raw = await decryptPayload(entry.payloadEnvelope);
26886
+ if (raw !== null && typeof raw === "object") payload = raw;
26887
+ } catch {
26888
+ payload = null;
26889
+ }
26890
+ const total = Array.isArray(payload?.direct) ? payload.direct.length + (payload.split !== void 0 ? 1 : 0) : 0;
26891
+ const certified = distinctOpCount(journal);
26892
+ return {
26893
+ transferId: entry.transferId,
26894
+ kind: "open",
26895
+ recipient: typeof payload?.recipient === "string" ? payload.recipient : "",
26896
+ coinId: typeof payload?.coinId === "string" ? payload.coinId : "",
26897
+ amount: typeof payload?.amount === "string" ? payload.amount : "",
26898
+ legs: { certified, total: Math.max(total, certified) },
26899
+ deliveryPending: journal.length > 0,
26900
+ createdAt: entry.createdAt
26901
+ };
26902
+ }
26903
+ function journalOnlyRow(transferId, entries) {
26904
+ const certified = distinctOpCount(entries);
26905
+ return {
26906
+ transferId,
26907
+ kind: "open",
26908
+ recipient: entries[0].recipientPubkey,
26909
+ coinId: "",
26910
+ amount: "",
26911
+ legs: { certified, total: certified },
26912
+ deliveryPending: true,
26913
+ createdAt: 0
26914
+ };
26915
+ }
26916
+ function shortfallRow(entry) {
26917
+ return {
26918
+ transferId: entry.transferId,
26919
+ kind: "shortfall",
26920
+ recipient: entry.recipient,
26921
+ coinId: entry.coinId,
26922
+ amount: entry.remainingAmount,
26923
+ legs: { certified: entry.committedTokenIds.length, total: entry.committedTokenIds.length },
26924
+ deliveryPending: false,
26925
+ createdAt: entry.createdAt
26926
+ };
26927
+ }
26928
+ function distinctOpCount(entries) {
26929
+ return new Set(entries.map((entry) => entry.opIndex)).size;
26930
+ }
26931
+
26717
26932
  // modules/payments-v2/history/History.ts
26718
26933
  var WIRE_TOKEN_ID = /^(?:[0-9a-f]{2}){1,64}$/;
26719
26934
  var WIRE_PUBKEY = /^0[23][0-9a-f]{64}$/;
@@ -28103,6 +28318,21 @@ var PaymentsFacade = class {
28103
28318
  this.ownPubkeyBytes = parts.ownPubkeyBytes;
28104
28319
  this.requests = parts.requests;
28105
28320
  deps.deliveryPort.bindDeliveryKeys((blob) => this.engine().deliveryKeys(blob));
28321
+ this.heartbeat = new ConvergenceHeartbeat({
28322
+ now: () => this.nowMs(),
28323
+ onTick: () => this.trackTail(this.runConvergencePass())
28324
+ });
28325
+ this.converger = new Converger({
28326
+ machineDeps: this.machineDeps,
28327
+ stores: this.machineStores,
28328
+ delivery: deps.deliveryPort,
28329
+ reconcile: (report) => this.requests.reconcile(report),
28330
+ refreshView: () => this.view.delta(),
28331
+ replayMints: () => this.replayMints(),
28332
+ isActiveOp: (id) => this.activeMoneyOps.has(id),
28333
+ emit: deps.emit,
28334
+ now: () => this.nowMs()
28335
+ });
28106
28336
  }
28107
28337
  view;
28108
28338
  ledger;
@@ -28119,10 +28349,18 @@ var PaymentsFacade = class {
28119
28349
  pendingOps = /* @__PURE__ */ new Set();
28120
28350
  unsubscribers = [];
28121
28351
  started = false;
28352
+ // §7 heartbeat: in-memory timing cell + pass runner (see convergence.ts).
28353
+ heartbeat;
28354
+ converger;
28355
+ /** Resume single-flight (§7): ticks, start() and resumeNow() coalesce onto ONE pass. */
28356
+ resumeFlight = new SingleFlight();
28357
+ /** §7 ownership: ids with an in-process machine attempt — the pass never adopts one. */
28358
+ activeMoneyOps = /* @__PURE__ */ new Set();
28122
28359
  // ── lifecycle ──────────────────────────────────────────────────────────────
28123
28360
  async start() {
28124
28361
  if (this.started) return;
28125
28362
  this.started = true;
28363
+ this.heartbeat.start();
28126
28364
  await this.deps.session.start();
28127
28365
  this.unsubscribers.push(
28128
28366
  this.deps.session.subscribeStream("mailbox", () => {
@@ -28135,20 +28373,11 @@ var PaymentsFacade = class {
28135
28373
  this.trackTail(this.requests.drainIncoming());
28136
28374
  })
28137
28375
  );
28138
- this.trackTail(this.runResume());
28139
- this.trackTail(
28140
- this.machineStores.deliveryJournal.replay({
28141
- delivery: this.deps.deliveryPort,
28142
- now: this.deps.now ?? Date.now,
28143
- attention: (transferId, code, detail) => {
28144
- this.deps.emit(
28145
- "transfer:attention",
28146
- detail === void 0 ? { transferId, code } : { transferId, code, detail }
28147
- );
28148
- }
28149
- })
28150
- );
28151
- this.trackTail(this.replayMints());
28376
+ const statusUnsub = this.deps.session.subscribeStatus?.((status) => {
28377
+ if (status === "connected") this.heartbeat.recovered();
28378
+ });
28379
+ if (statusUnsub !== void 0) this.unsubscribers.push(statusUnsub);
28380
+ this.trackTail(this.runConvergencePass());
28152
28381
  this.trackTail(this.seedHeldStates());
28153
28382
  await this.view.fullPull().catch(() => void 0);
28154
28383
  this.receiveLoop.start(this.deps.receivePollMs);
@@ -28156,6 +28385,7 @@ var PaymentsFacade = class {
28156
28385
  /** §7 same-address restart gate: resolves only after in-flight ops settle. */
28157
28386
  async stop() {
28158
28387
  this.started = false;
28388
+ this.heartbeat.stop();
28159
28389
  this.receiveLoop.stop();
28160
28390
  for (const unsubscribe of this.unsubscribers) unsubscribe();
28161
28391
  this.unsubscribers = [];
@@ -28181,6 +28411,10 @@ var PaymentsFacade = class {
28181
28411
  history(page) {
28182
28412
  return this.historyStore.page(page ?? {});
28183
28413
  }
28414
+ /** §4 pending-transfers UI surface — derived on read, never cached (convergence.ts). */
28415
+ pendingTransfers() {
28416
+ return derivePendingTransfers(this.machineStores, this.machineDeps.decryptPayload);
28417
+ }
28184
28418
  // ── money movement ─────────────────────────────────────────────────────────
28185
28419
  send(request) {
28186
28420
  return this.track(this.sendWithPolicy(request));
@@ -28193,6 +28427,22 @@ var PaymentsFacade = class {
28193
28427
  mint(coinId, amount) {
28194
28428
  return this.track(this.mintInner(coinId, amount));
28195
28429
  }
28430
+ // §4 retry verb: coalesces onto the running pass — NEVER re-issue send() (#631/#676).
28431
+ async resumeNow() {
28432
+ if (!this.started) return;
28433
+ await this.track(this.runConvergencePass());
28434
+ }
28435
+ /** One single-flighted pass + its reschedule: concurrent callers coalesce. */
28436
+ runConvergencePass() {
28437
+ return this.resumeFlight.run(async () => {
28438
+ const outcome = await this.converger.convergeOnce();
28439
+ const pending = await this.converger.pendingWork().catch(() => true);
28440
+ this.heartbeat.settle(outcome, pending);
28441
+ });
28442
+ }
28443
+ nowMs() {
28444
+ return (this.deps.now ?? Date.now)();
28445
+ }
28196
28446
  // ── send policy (§5.5 + old-loop parity; the machine stays policy-free) ────
28197
28447
  async sendWithPolicy(request) {
28198
28448
  const recipient = await this.requireSameNetworkRecipient(request.recipient);
@@ -28242,12 +28492,15 @@ var PaymentsFacade = class {
28242
28492
  return recipient;
28243
28493
  }
28244
28494
  async runAttempt(ctx) {
28495
+ this.activeMoneyOps.add(ctx.transferId);
28245
28496
  try {
28246
28497
  const { deliveryPending } = await this.machine.run(ctx.plan);
28247
28498
  this.settleSuccess(ctx, ctx.sourceIds);
28248
28499
  return { kind: "success", deliveryPending };
28249
28500
  } catch (err) {
28250
- return this.disposeFailedAttempt(ctx, err);
28501
+ return await this.disposeFailedAttempt(ctx, err);
28502
+ } finally {
28503
+ this.activeMoneyOps.delete(ctx.transferId);
28251
28504
  }
28252
28505
  }
28253
28506
  /**
@@ -28256,7 +28509,10 @@ var PaymentsFacade = class {
28256
28509
  * the remainder under a NEW transferId — never the full amount.
28257
28510
  */
28258
28511
  async consumePartial(ctx, disposition, run, attempt) {
28259
- run.pendingAny = run.pendingAny || disposition.deliveryPending;
28512
+ if (disposition.deliveryPending) {
28513
+ run.pendingAny = true;
28514
+ this.heartbeat.arm();
28515
+ }
28260
28516
  const partial = disposition.entry;
28261
28517
  run.delivered.push(...partial.committedTokenIds);
28262
28518
  run.firstPartialId ??= partial.transferId;
@@ -28414,6 +28670,7 @@ var PaymentsFacade = class {
28414
28670
  /** Open intent: sources stay reserved + in-flight (§5.2) until resume adopts them. */
28415
28671
  settleKeepOpen(ctx) {
28416
28672
  this.queue.clearExpectedChange(ctx.transferId);
28673
+ this.heartbeat.arm();
28417
28674
  }
28418
28675
  settlePartial(ctx, partial) {
28419
28676
  this.ledger.cancel(ctx.transferId);
@@ -28450,6 +28707,7 @@ var PaymentsFacade = class {
28450
28707
  for (const id of run.settledShortfalls.splice(0)) {
28451
28708
  await this.machineStores.shortfalls.removeByKey(id).catch(() => void 0);
28452
28709
  }
28710
+ if (run.pendingAny) this.heartbeat.arm();
28453
28711
  const result = {
28454
28712
  id: transferId,
28455
28713
  status: run.pendingAny ? "confirmed" : "delivered",
@@ -28494,21 +28752,29 @@ var PaymentsFacade = class {
28494
28752
  if (!/^(?:[0-9a-f]{2})+$/.test(coinId) || amount <= 0n) {
28495
28753
  return { success: false, error: "coinId must be even-length lowercase hex and amount positive" };
28496
28754
  }
28497
- const engine = this.engine();
28498
28755
  const mintId = this.newId();
28499
- const params = this.mintParams(coinId, amount);
28756
+ this.activeMoneyOps.add(mintId);
28757
+ try {
28758
+ return await this.mintUnderJournal(mintId, coinId, amount);
28759
+ } finally {
28760
+ this.activeMoneyOps.delete(mintId);
28761
+ }
28762
+ }
28763
+ async mintUnderJournal(mintId, coinId, amount) {
28764
+ const engine = this.engine();
28500
28765
  const entry = {
28501
28766
  mintId,
28502
28767
  coinId,
28503
28768
  amount: amount.toString(),
28504
28769
  tokenId: "",
28505
- createdAt: (this.deps.now ?? Date.now)()
28770
+ createdAt: this.nowMs()
28506
28771
  };
28507
28772
  await this.machineStores.mintJournal.upsert(entry);
28508
28773
  let token;
28509
28774
  try {
28510
- token = await engine.mint(params, { transferId: mintId });
28775
+ token = await engine.mint(this.mintParams(coinId, amount), { transferId: mintId });
28511
28776
  } catch (err) {
28777
+ this.heartbeat.arm();
28512
28778
  return { success: false, error: messageOf(err) };
28513
28779
  }
28514
28780
  if (token.blob.tokenId !== entry.tokenId) {
@@ -28517,6 +28783,7 @@ var PaymentsFacade = class {
28517
28783
  try {
28518
28784
  await this.finalizeMint(engine, mintId, token, coinId, amount.toString());
28519
28785
  } catch (err) {
28786
+ this.heartbeat.arm();
28520
28787
  return { success: false, tokenId: token.blob.tokenId, error: messageOf(err) };
28521
28788
  }
28522
28789
  return { success: true, tokenId: token.blob.tokenId };
@@ -28539,18 +28806,22 @@ var PaymentsFacade = class {
28539
28806
  this.heldStates.set(token.blob.tokenId, (await engine.deliveryKeys(bytes)).stateHash);
28540
28807
  this.trackTail(this.view.delta());
28541
28808
  }
28809
+ /** @returns how many journal entries were RESOLVED (cleared) — heartbeat progress. */
28542
28810
  async replayMints() {
28811
+ let resolved = 0;
28543
28812
  for (const entry of await this.machineStores.mintJournal.list()) {
28813
+ if (this.activeMoneyOps.has(entry.mintId)) continue;
28544
28814
  try {
28545
- await this.replayMint(entry);
28815
+ if (await this.replayMint(entry)) resolved += 1;
28546
28816
  } catch {
28547
28817
  }
28548
28818
  }
28819
+ return resolved;
28549
28820
  }
28550
28821
  async replayMint(entry) {
28551
28822
  if (entry.tokenId !== "" && await this.tokenInServerInventory(entry.tokenId)) {
28552
28823
  await this.machineStores.mintJournal.removeByKey(entry.mintId);
28553
- return;
28824
+ return true;
28554
28825
  }
28555
28826
  const engine = this.engine();
28556
28827
  if (!supportsDeterministicMint(engine)) {
@@ -28558,7 +28829,7 @@ var PaymentsFacade = class {
28558
28829
  transferId: entry.mintId,
28559
28830
  code: ATTENTION_MINT_UNRESOLVED
28560
28831
  });
28561
- return;
28832
+ return false;
28562
28833
  }
28563
28834
  const token = await engine.mint(this.mintParams(entry.coinId, BigInt(entry.amount)), {
28564
28835
  transferId: entry.mintId
@@ -28567,6 +28838,7 @@ var PaymentsFacade = class {
28567
28838
  await this.machineStores.mintJournal.upsert({ ...entry, tokenId: token.blob.tokenId });
28568
28839
  }
28569
28840
  await this.finalizeMint(engine, entry.mintId, token, entry.coinId, entry.amount);
28841
+ return true;
28570
28842
  }
28571
28843
  mintParams(coinId, amount) {
28572
28844
  return {
@@ -28592,17 +28864,6 @@ var PaymentsFacade = class {
28592
28864
  newId() {
28593
28865
  return (this.deps.newId ?? randomUUID)();
28594
28866
  }
28595
- async runResume() {
28596
- const report = await resumeAll(this.machineDeps);
28597
- await this.requests.reconcile({
28598
- resumed: report.resumed,
28599
- conflicted: report.conflicted,
28600
- open: report.failed
28601
- });
28602
- if (report.resumed.length > 0 || report.conflicted.length > 0) {
28603
- await this.view.delta().catch(() => void 0);
28604
- }
28605
- }
28606
28867
  async seedHeldStates() {
28607
28868
  let page = await this.deps.storagePort.listInventory();
28608
28869
  for (let i = 0; i < INVENTORY_SCAN_PAGE_LIMIT; i++) {
@@ -28616,7 +28877,7 @@ var PaymentsFacade = class {
28616
28877
  toUiToken(tokenId, coinId, amount) {
28617
28878
  const registry = this.deps.registry;
28618
28879
  const iconUrl = registry.getIconUrl(coinId);
28619
- const now = (this.deps.now ?? Date.now)();
28880
+ const now = this.nowMs();
28620
28881
  return {
28621
28882
  id: tokenId,
28622
28883
  coinId,
@@ -29011,6 +29272,7 @@ var WalletApiSession = class {
29011
29272
  reseedChain = Promise.resolve();
29012
29273
  reseedsPending = 0;
29013
29274
  handlers = /* @__PURE__ */ new Map();
29275
+ statusHandlers = /* @__PURE__ */ new Set();
29014
29276
  socket = null;
29015
29277
  started = false;
29016
29278
  stopped = false;
@@ -29161,10 +29423,16 @@ var WalletApiSession = class {
29161
29423
  status() {
29162
29424
  return this.lastStatus ?? "offline";
29163
29425
  }
29426
+ /** Transition feed off the existing emitStatus point (FacadeSession.subscribeStatus). */
29427
+ subscribeStatus(handler) {
29428
+ this.statusHandlers.add(handler);
29429
+ return () => this.statusHandlers.delete(handler);
29430
+ }
29164
29431
  emitStatus(status) {
29165
29432
  if (this.stopped || this.lastStatus === status) return;
29166
29433
  this.lastStatus = status;
29167
29434
  this.deps.emitStatus(status);
29435
+ for (const handler of this.statusHandlers) handler(status);
29168
29436
  }
29169
29437
  async connect() {
29170
29438
  if (this.stopped) return;