@did-btcr2/method 0.36.0 → 0.36.1

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.
package/dist/browser.mjs CHANGED
@@ -104589,6 +104589,17 @@ var AggregationParticipant = class {
104589
104589
  }
104590
104590
  return map3;
104591
104591
  }
104592
+ /**
104593
+ * The validated aggregated data retained for a cohort, regardless of phase.
104594
+ * Unlike {@link pendingValidations} (which lists only cohorts still awaiting
104595
+ * the validate decision), this returns the stored validation — including the
104596
+ * participant's sidecar (the CAS Announcement map or its SMT inclusion proof)
104597
+ * — so it is still readable once the cohort reaches Complete. Returns
104598
+ * undefined before aggregated data has been received.
104599
+ */
104600
+ getValidation(cohortId) {
104601
+ return this.#cohortStates.get(cohortId)?.validation;
104602
+ }
104592
104603
  #handleDistributeAggregatedData(message2) {
104593
104604
  const cohortId = message2.body?.cohortId;
104594
104605
  if (!cohortId) return;
@@ -111465,35 +111476,22 @@ var AggregationServiceRunner = class _AggregationServiceRunner extends TypedEven
111465
111476
  session;
111466
111477
  #transport;
111467
111478
  #did;
111468
- #config;
111479
+ #defaultConfig;
111469
111480
  #onOptInReceived;
111470
111481
  #onReadyToFinalize;
111471
111482
  #onProvideTxData;
111472
111483
  #cohortTtlMs;
111473
111484
  #phaseTimeoutMs;
111474
111485
  #advertRepeatIntervalMs;
111475
- #cohortId;
111486
+ /** Per-cohort run state, keyed by cohortId. */
111487
+ #contexts = /* @__PURE__ */ new Map();
111476
111488
  #handlersRegistered = false;
111477
111489
  #stopped = false;
111478
- /**
111479
- * Guard against the async race where two concurrent #handleOptIn invocations
111480
- * both pass the `participants.length >= minParticipants` check before either
111481
- * mutates the cohort phase. Set synchronously before any `await` so subsequent
111482
- * handlers observe it on their next resumption.
111483
- */
111484
- #finalizing = false;
111485
- #resolveRun;
111486
- #rejectRun;
111487
- #cohortTtlTimer;
111488
- #phaseTimer;
111489
- #lastObservedPhase;
111490
- /** Stop handle for the repeating COHORT_ADVERT publish loop. */
111491
- #stopAdvertRepeat;
111492
111490
  constructor(options2) {
111493
111491
  super();
111494
111492
  this.#transport = options2.transport;
111495
111493
  this.#did = options2.did;
111496
- this.#config = options2.config;
111494
+ this.#defaultConfig = options2.config;
111497
111495
  this.#onOptInReceived = options2.onOptInReceived ?? (async () => ({ accepted: true }));
111498
111496
  this.#onReadyToFinalize = options2.onReadyToFinalize ?? (async ({ acceptedCount, minRequired }) => ({
111499
111497
  finalize: acceptedCount >= minRequired
@@ -111511,55 +111509,126 @@ var AggregationServiceRunner = class _AggregationServiceRunner extends TypedEven
111511
111509
  maxUpdateSizeBytes: options2.maxUpdateSizeBytes
111512
111510
  });
111513
111511
  }
111512
+ /** Resolve the {@link RunContext} an inbound message belongs to, by cohortId. */
111513
+ #contextFor(msg) {
111514
+ const cohortId = msg.body?.cohortId;
111515
+ if (!cohortId) return void 0;
111516
+ return this.#contexts.get(cohortId);
111517
+ }
111514
111518
  /**
111515
- * Drain any silent rejections the state machine recorded during the most
111516
- * recent receive() and surface them as `message-rejected` events. Safe to
111517
- * call even before a cohortId is assigned.
111519
+ * Drain any silent rejections the state machine recorded for a cohort during
111520
+ * the most recent receive() and surface them as `message-rejected` events.
111518
111521
  */
111519
- #drainRejections() {
111520
- if (!this.#cohortId) return;
111521
- for (const r2 of this.session.drainRejections(this.#cohortId)) {
111522
- this.emit("message-rejected", { cohortId: this.#cohortId, ...r2 });
111522
+ #drainRejections(ctx) {
111523
+ for (const r2 of this.session.drainRejections(ctx.cohortId)) {
111524
+ this.emit("message-rejected", { cohortId: ctx.cohortId, ...r2 });
111523
111525
  }
111524
111526
  }
111525
111527
  /**
111526
- * Run the protocol to completion. Resolves with the final aggregation result
111527
- * (signature + signed transaction) once signing is complete.
111528
+ * Advertise a new cohort and begin driving it to completion. Callable many
111529
+ * times on one runner; each cohort runs concurrently and independently.
111530
+ *
111531
+ * @param config Per-cohort conditions + network (see {@link CohortConfig}).
111532
+ * @returns The new cohort's id and a `completion` promise that resolves with
111533
+ * that cohort's {@link AggregationResult} (or rejects if it fails/stalls).
111534
+ * @throws If the runner has been stopped, or the config is invalid
111535
+ * (fail-fast via `createCohort`).
111536
+ */
111537
+ advertiseCohort(config) {
111538
+ if (this.#stopped) {
111539
+ throw new AggregationServiceError("Cannot advertise on a stopped runner.", "RUNNER_STOPPED", {});
111540
+ }
111541
+ this.#registerHandlers();
111542
+ const cohortId = this.session.createCohort(config);
111543
+ let resolve;
111544
+ let reject;
111545
+ const completion = new Promise((res, rej) => {
111546
+ resolve = res;
111547
+ reject = rej;
111548
+ });
111549
+ const ctx = {
111550
+ cohortId,
111551
+ config,
111552
+ resolve,
111553
+ reject,
111554
+ completion,
111555
+ finalizing: false,
111556
+ settled: false
111557
+ };
111558
+ this.#contexts.set(cohortId, ctx);
111559
+ try {
111560
+ this.#startTimers(ctx);
111561
+ const advertMsgs = this.session.advertise(cohortId);
111562
+ this.#onPhaseMaybeChanged(ctx);
111563
+ this.emit("cohort-advertised", { cohortId });
111564
+ if (this.#advertRepeatIntervalMs > 0) {
111565
+ this.#startAdvertRepeat(ctx, advertMsgs);
111566
+ } else {
111567
+ this.#sendAll(advertMsgs).catch((err) => this.#failCohort(ctx, err));
111568
+ }
111569
+ } catch (err) {
111570
+ this.#failCohort(ctx, err);
111571
+ }
111572
+ return { cohortId, completion };
111573
+ }
111574
+ /**
111575
+ * Run a single cohort to completion using the `config` supplied in the
111576
+ * runner options. Thin convenience over {@link advertiseCohort} for the
111577
+ * single-cohort case (and the path {@link AggregationRunner.solo} rides).
111528
111578
  *
111529
111579
  * @returns {Promise<AggregationResult>} The final result with signature and signed tx.
111530
111580
  */
111531
111581
  run() {
111532
- return new Promise((resolve, reject) => {
111533
- this.#resolveRun = resolve;
111534
- this.#rejectRun = reject;
111535
- try {
111536
- this.#registerHandlers();
111537
- this.#cohortId = this.session.createCohort(this.#config);
111538
- this.#startTimers();
111539
- const advertMsgs = this.session.advertise(this.#cohortId);
111540
- this.#onPhaseMaybeChanged();
111541
- this.emit("cohort-advertised", { cohortId: this.#cohortId });
111542
- if (this.#advertRepeatIntervalMs > 0) {
111543
- this.#startAdvertRepeat(advertMsgs);
111544
- } else {
111545
- this.#sendAll(advertMsgs).catch((err) => this.#fail(err));
111546
- }
111547
- } catch (err) {
111548
- this.#fail(err);
111582
+ if (!this.#defaultConfig) {
111583
+ return Promise.reject(new AggregationServiceError(
111584
+ "run() requires `config` in the runner options; use advertiseCohort(config) to drive cohorts explicitly.",
111585
+ "MISSING_COHORT_CONFIG",
111586
+ {}
111587
+ ));
111588
+ }
111589
+ try {
111590
+ return this.advertiseCohort(this.#defaultConfig).completion;
111591
+ } catch (err) {
111592
+ return Promise.reject(err);
111593
+ }
111594
+ }
111595
+ /**
111596
+ * Wait for every currently-outstanding cohort to settle and return the
111597
+ * successful results. Dynamic drain: cohorts advertised while this is pending
111598
+ * are included, and it resolves only once no cohorts remain. Failed cohorts
111599
+ * are surfaced via `error` / `cohort-failed` events and their rejected
111600
+ * `completion` promises; they are omitted from the returned array (this
111601
+ * method does not throw). Bound long-running cohorts with `cohortTtlMs` /
111602
+ * `phaseTimeoutMs` or this may never resolve.
111603
+ *
111604
+ * @returns {Promise<AggregationResult[]>} Results of the cohorts that completed.
111605
+ */
111606
+ async runAll() {
111607
+ const collected = /* @__PURE__ */ new Map();
111608
+ const onComplete = (result) => {
111609
+ collected.set(result.cohortId, result);
111610
+ };
111611
+ this.on("signing-complete", onComplete);
111612
+ try {
111613
+ while (this.#contexts.size > 0) {
111614
+ await Promise.allSettled([...this.#contexts.values()].map((c2) => c2.completion));
111549
111615
  }
111550
- });
111616
+ } finally {
111617
+ this.off("signing-complete", onComplete);
111618
+ }
111619
+ return [...collected.values()];
111551
111620
  }
111552
111621
  /**
111553
- * Begin publishing the cohort advert immediately and on a repeating interval
111554
- * until {@link #stopAdvertRepeating} is called. Each advert is broadcast
111555
- * (no recipient) via the transport's `publishRepeating` primitive.
111622
+ * Begin publishing a cohort's advert immediately and on a repeating interval
111623
+ * until the cohort's advert loop is stopped. Each advert is broadcast (no
111624
+ * recipient) via the transport's `publishRepeating` primitive.
111556
111625
  */
111557
- #startAdvertRepeat(advertMsgs) {
111626
+ #startAdvertRepeat(ctx, advertMsgs) {
111558
111627
  const stops = [];
111559
111628
  for (const msg of advertMsgs) {
111560
111629
  stops.push(this.#transport.publishRepeating(msg, this.#did, this.#advertRepeatIntervalMs));
111561
111630
  }
111562
- this.#stopAdvertRepeat = () => {
111631
+ ctx.stopAdvertRepeat = () => {
111563
111632
  for (const stop2 of stops) {
111564
111633
  try {
111565
111634
  stop2();
@@ -111568,61 +111637,118 @@ var AggregationServiceRunner = class _AggregationServiceRunner extends TypedEven
111568
111637
  }
111569
111638
  };
111570
111639
  }
111571
- /** Stop the advert republish loop. Idempotent. */
111572
- #stopAdvertRepeating() {
111573
- if (!this.#stopAdvertRepeat) return;
111574
- const stop2 = this.#stopAdvertRepeat;
111575
- this.#stopAdvertRepeat = void 0;
111640
+ /** Stop a cohort's advert republish loop. Idempotent. */
111641
+ #stopAdvertRepeating(ctx) {
111642
+ if (!ctx.stopAdvertRepeat) return;
111643
+ const stop2 = ctx.stopAdvertRepeat;
111644
+ ctx.stopAdvertRepeat = void 0;
111576
111645
  stop2();
111577
111646
  }
111578
- /** Schedule cohort TTL + phase timeout at the start of a run. */
111579
- #startTimers() {
111647
+ /** Schedule a cohort's TTL + phase timeout when it is advertised. */
111648
+ #startTimers(ctx) {
111580
111649
  if (this.#cohortTtlMs !== void 0) {
111581
- this.#cohortTtlTimer = setTimeout(() => {
111582
- const reason = `Cohort ${this.#cohortId ?? ""} exceeded TTL of ${this.#cohortTtlMs}ms`;
111583
- this.emit("cohort-failed", { cohortId: this.#cohortId ?? "", reason });
111584
- this.#fail(new Error(reason));
111650
+ ctx.cohortTtlTimer = setTimeout(() => {
111651
+ const reason = `Cohort ${ctx.cohortId} exceeded TTL of ${this.#cohortTtlMs}ms`;
111652
+ this.emit("cohort-failed", { cohortId: ctx.cohortId, reason });
111653
+ this.#failCohort(ctx, new Error(reason));
111585
111654
  }, this.#cohortTtlMs);
111586
111655
  }
111587
- this.#resetPhaseTimer();
111656
+ this.#resetPhaseTimer(ctx);
111588
111657
  }
111589
- /** Reset the per-phase stall timer. Called when a phase transition is observed. */
111590
- #resetPhaseTimer() {
111591
- if (this.#phaseTimer) clearTimeout(this.#phaseTimer);
111592
- this.#phaseTimer = void 0;
111658
+ /** Reset a cohort's per-phase stall timer. Called when a phase transition is observed. */
111659
+ #resetPhaseTimer(ctx) {
111660
+ if (ctx.phaseTimer) clearTimeout(ctx.phaseTimer);
111661
+ ctx.phaseTimer = void 0;
111593
111662
  if (this.#phaseTimeoutMs === void 0) return;
111594
- this.#phaseTimer = setTimeout(() => {
111595
- const reason = `Cohort ${this.#cohortId ?? ""} stalled in phase ${this.#lastObservedPhase ?? "?"} for ${this.#phaseTimeoutMs}ms`;
111596
- this.emit("cohort-failed", { cohortId: this.#cohortId ?? "", reason });
111597
- this.#fail(new Error(reason));
111663
+ ctx.phaseTimer = setTimeout(() => {
111664
+ const reason = `Cohort ${ctx.cohortId} stalled in phase ${ctx.lastObservedPhase ?? "?"} for ${this.#phaseTimeoutMs}ms`;
111665
+ this.emit("cohort-failed", { cohortId: ctx.cohortId, reason });
111666
+ this.#failCohort(ctx, new Error(reason));
111598
111667
  }, this.#phaseTimeoutMs);
111599
111668
  }
111600
- /** Detect a phase change since the last observation and reset the phase timer. */
111601
- #onPhaseMaybeChanged() {
111602
- if (!this.#cohortId) return;
111603
- const phase = this.session.getCohortPhase(this.#cohortId);
111604
- if (phase !== this.#lastObservedPhase) {
111605
- this.#lastObservedPhase = phase;
111606
- this.#resetPhaseTimer();
111669
+ /** Detect a phase change for a cohort since the last observation and reset its phase timer. */
111670
+ #onPhaseMaybeChanged(ctx) {
111671
+ const phase = this.session.getCohortPhase(ctx.cohortId);
111672
+ if (phase !== ctx.lastObservedPhase) {
111673
+ ctx.lastObservedPhase = phase;
111674
+ this.#resetPhaseTimer(ctx);
111607
111675
  }
111608
111676
  }
111609
- /** Clear both timers. Called on successful completion, stop(), and #fail. */
111610
- #clearTimers() {
111611
- if (this.#cohortTtlTimer) clearTimeout(this.#cohortTtlTimer);
111612
- if (this.#phaseTimer) clearTimeout(this.#phaseTimer);
111613
- this.#cohortTtlTimer = void 0;
111614
- this.#phaseTimer = void 0;
111677
+ /** Clear a cohort's timers. Called on completion, stop, and failure. */
111678
+ #clearTimers(ctx) {
111679
+ if (ctx.cohortTtlTimer) clearTimeout(ctx.cohortTtlTimer);
111680
+ if (ctx.phaseTimer) clearTimeout(ctx.phaseTimer);
111681
+ ctx.cohortTtlTimer = void 0;
111682
+ ctx.phaseTimer = void 0;
111683
+ }
111684
+ /**
111685
+ * Reclaim one cohort's runner-layer bookkeeping: stop its advert loop, clear
111686
+ * its timers, and drop its {@link RunContext}. Does NOT touch sibling cohorts
111687
+ * and does NOT detach the shared transport handlers. Leaves the cohort in the
111688
+ * state machine; whether that cohort's `session` state is also removed is the
111689
+ * caller's choice (see {@link #completeCohort} vs {@link #failCohort}).
111690
+ */
111691
+ #disposeCohort(ctx) {
111692
+ this.#stopAdvertRepeating(ctx);
111693
+ this.#clearTimers(ctx);
111694
+ this.#contexts.delete(ctx.cohortId);
111695
+ }
111696
+ /**
111697
+ * Settle one cohort successfully. Reclaims the runner context but leaves the
111698
+ * completed cohort in `session` so callers can read its beaconAddress / cohort
111699
+ * via `session.getCohort(result.cohortId)`; reclaim it with
111700
+ * `session.removeCohort(cohortId)` when done. Idempotent via `ctx.settled`.
111701
+ */
111702
+ #completeCohort(ctx, result) {
111703
+ if (ctx.settled) return;
111704
+ ctx.settled = true;
111705
+ this.#disposeCohort(ctx);
111706
+ this.emit("signing-complete", result);
111707
+ ctx.resolve(result);
111708
+ }
111709
+ /**
111710
+ * Fail one cohort. Reclaims its runner context, drops its now-dead state from
111711
+ * the state machine, and rejects only its completion; siblings keep running
111712
+ * and the shared transport handlers stay registered. Idempotent via
111713
+ * `ctx.settled`.
111714
+ */
111715
+ #failCohort(ctx, err) {
111716
+ if (ctx.settled) return;
111717
+ ctx.settled = true;
111718
+ this.#disposeCohort(ctx);
111719
+ this.session.removeCohort(ctx.cohortId);
111720
+ this.emit("error", err);
111721
+ ctx.reject(err);
111722
+ }
111723
+ /**
111724
+ * Stop a single cohort early without affecting the rest of the runner. Drops
111725
+ * the cohort's state machine state; its `completion` promise rejects with a
111726
+ * stopped error.
111727
+ */
111728
+ stopCohort(cohortId) {
111729
+ const ctx = this.#contexts.get(cohortId);
111730
+ if (!ctx || ctx.settled) return;
111731
+ ctx.settled = true;
111732
+ this.#disposeCohort(ctx);
111733
+ this.session.removeCohort(cohortId);
111734
+ ctx.reject(new AggregationServiceError(`Cohort ${cohortId} stopped.`, "COHORT_STOPPED", { cohortId }));
111615
111735
  }
111616
111736
  /**
111617
- * Stop the runner early. Marks the runner stopped and detaches transport
111618
- * handlers so a restart or a new runner doesn't inherit stale dispatch.
111737
+ * Stop the whole runner. Fails every outstanding cohort, then detaches the
111738
+ * shared transport handlers so a restart or a new runner doesn't inherit
111739
+ * stale dispatch. Safe to call repeatedly.
111619
111740
  */
111620
111741
  stop() {
111621
111742
  this.#stopped = true;
111622
- this.#stopAdvertRepeating();
111623
- this.#clearTimers();
111743
+ for (const ctx of [...this.#contexts.values()]) {
111744
+ if (ctx.settled) continue;
111745
+ ctx.settled = true;
111746
+ this.#disposeCohort(ctx);
111747
+ this.session.removeCohort(ctx.cohortId);
111748
+ ctx.reject(new AggregationServiceError("Service runner stopped.", "RUNNER_STOPPED", { cohortId: ctx.cohortId }));
111749
+ }
111750
+ this.#contexts.clear();
111624
111751
  this.#unregisterHandlers();
111625
- if (this.#cohortId) this.session.removeCohort(this.#cohortId);
111626
111752
  }
111627
111753
  /** Message types this runner listens for on the transport. */
111628
111754
  static #HANDLED_MESSAGE_TYPES = [
@@ -111633,7 +111759,10 @@ var AggregationServiceRunner = class _AggregationServiceRunner extends TypedEven
111633
111759
  SIGNATURE_AUTHORIZATION
111634
111760
  ];
111635
111761
  /**
111636
- * Internal: handler registration with the transport. Idempotent.
111762
+ * Internal: handler registration with the transport. Idempotent. Handlers
111763
+ * are DID-scoped and cohort-agnostic — one registration serves every cohort
111764
+ * this runner drives; demux to the right {@link RunContext} happens in each
111765
+ * handler via the inbound message's cohortId.
111637
111766
  */
111638
111767
  #registerHandlers() {
111639
111768
  if (this.#handlersRegistered) return;
@@ -111654,23 +111783,25 @@ var AggregationServiceRunner = class _AggregationServiceRunner extends TypedEven
111654
111783
  }
111655
111784
  /**
111656
111785
  * Internal: message handlers for each protocol step. Each handler:
111657
- * 1) feeds the message into the state machine via session.receive()
111658
- * 2) emits a high-level event for external observers
111659
- * 3) checks if the new state triggers any automatic next steps, and if so:
111786
+ * 1) resolves the cohort the message belongs to (by cohortId); ignores it if unknown
111787
+ * 2) feeds the message into the state machine via session.receive()
111788
+ * 3) emits a high-level event (carrying cohortId) for external observers
111789
+ * 4) checks if the new state triggers any automatic next steps, and if so:
111660
111790
  * a) calls the appropriate decision callback(s)
111661
111791
  * b) sends any resulting messages from the state machine
111792
+ * Errors fail only the owning cohort. A stopped runner ignores messages.
111662
111793
  * @param {BaseMessage} msg - The incoming message to handle.
111663
111794
  * @returns {Promise<void>} Resolves when handling is complete.
111664
- * @throws {Error} If any step of handling fails, the error is emitted and the run promise is rejected.
111665
- * Note: if the runner has been stopped, handlers will ignore incoming messages.
111666
111795
  */
111667
111796
  async #handleOptIn(msg) {
111668
111797
  if (this.#stopped) return;
111798
+ const ctx = this.#contextFor(msg);
111799
+ if (!ctx) return;
111669
111800
  try {
111670
111801
  this.session.receive(msg);
111671
- this.#drainRejections();
111672
- this.#onPhaseMaybeChanged();
111673
- const optIn = this.session.pendingOptIns(this.#cohortId).get(msg.from);
111802
+ this.#drainRejections(ctx);
111803
+ this.#onPhaseMaybeChanged(ctx);
111804
+ const optIn = this.session.pendingOptIns(ctx.cohortId).get(msg.from);
111674
111805
  if (!optIn) return;
111675
111806
  this.emit("opt-in-received", optIn);
111676
111807
  if (optIn.communicationPk) {
@@ -111678,34 +111809,34 @@ var AggregationServiceRunner = class _AggregationServiceRunner extends TypedEven
111678
111809
  }
111679
111810
  const decision = await this.#onOptInReceived(optIn);
111680
111811
  if (!decision.accepted) return;
111681
- const maxParticipants = this.#config.maxParticipants;
111682
- const cohortNow = this.session.getCohort(this.#cohortId);
111812
+ const maxParticipants = ctx.config.maxParticipants;
111813
+ const cohortNow = this.session.getCohort(ctx.cohortId);
111683
111814
  if (maxParticipants !== void 0 && cohortNow && cohortNow.participants.length >= maxParticipants) {
111684
111815
  return;
111685
111816
  }
111686
- await this.#sendAll(this.session.acceptParticipant(this.#cohortId, msg.from));
111687
- this.emit("participant-accepted", { participantDid: msg.from });
111688
- const cohort = this.session.getCohort(this.#cohortId);
111689
- if (cohort.participants.length >= this.#config.minParticipants && !this.#finalizing) {
111690
- this.#finalizing = true;
111817
+ await this.#sendAll(this.session.acceptParticipant(ctx.cohortId, msg.from));
111818
+ this.emit("participant-accepted", { cohortId: ctx.cohortId, participantDid: msg.from });
111819
+ const cohort = this.session.getCohort(ctx.cohortId);
111820
+ if (cohort.participants.length >= ctx.config.minParticipants && !ctx.finalizing) {
111821
+ ctx.finalizing = true;
111691
111822
  const finalizeDecision = await this.#onReadyToFinalize({
111692
111823
  acceptedCount: cohort.participants.length,
111693
- minRequired: this.#config.minParticipants
111824
+ minRequired: ctx.config.minParticipants
111694
111825
  });
111695
111826
  if (!finalizeDecision.finalize) {
111696
- this.#finalizing = false;
111827
+ ctx.finalizing = false;
111697
111828
  return;
111698
111829
  }
111699
- const readyMsgs = this.session.finalizeKeygen(this.#cohortId);
111700
- this.#stopAdvertRepeating();
111830
+ const readyMsgs = this.session.finalizeKeygen(ctx.cohortId);
111831
+ this.#stopAdvertRepeating(ctx);
111701
111832
  this.emit("keygen-complete", {
111702
- cohortId: this.#cohortId,
111833
+ cohortId: ctx.cohortId,
111703
111834
  beaconAddress: cohort.beaconAddress
111704
111835
  });
111705
111836
  await this.#sendAll(readyMsgs);
111706
111837
  }
111707
111838
  } catch (err) {
111708
- this.#fail(err);
111839
+ this.#failCohort(ctx, err);
111709
111840
  }
111710
111841
  }
111711
111842
  /**
@@ -111713,23 +111844,23 @@ var AggregationServiceRunner = class _AggregationServiceRunner extends TypedEven
111713
111844
  * and distributes the data for validation.
111714
111845
  * @param {BaseMessage} msg - The incoming message to handle.
111715
111846
  * @returns {Promise<void>} Resolves when handling is complete.
111716
- * @throws {Error} If any step of handling fails, the error is emitted and the run promise is rejected.
111717
- * Note: if the runner has been stopped, handlers will ignore incoming messages.
111718
111847
  */
111719
111848
  async #handleSubmitUpdate(msg) {
111720
111849
  if (this.#stopped) return;
111850
+ const ctx = this.#contextFor(msg);
111851
+ if (!ctx) return;
111721
111852
  try {
111722
111853
  this.session.receive(msg);
111723
- this.#drainRejections();
111724
- this.#onPhaseMaybeChanged();
111725
- this.emit("update-received", { participantDid: msg.from });
111726
- if (this.session.getCohortPhase(this.#cohortId) === "UpdatesCollected" /* UpdatesCollected */) {
111727
- const distributeMsgs = this.session.buildAndDistribute(this.#cohortId);
111728
- this.emit("data-distributed", { cohortId: this.#cohortId });
111854
+ this.#drainRejections(ctx);
111855
+ this.#onPhaseMaybeChanged(ctx);
111856
+ this.emit("update-received", { cohortId: ctx.cohortId, participantDid: msg.from });
111857
+ if (this.session.getCohortPhase(ctx.cohortId) === "UpdatesCollected" /* UpdatesCollected */) {
111858
+ const distributeMsgs = this.session.buildAndDistribute(ctx.cohortId);
111859
+ this.emit("data-distributed", { cohortId: ctx.cohortId });
111729
111860
  await this.#sendAll(distributeMsgs);
111730
111861
  }
111731
111862
  } catch (err) {
111732
- this.#fail(err);
111863
+ this.#failCohort(ctx, err);
111733
111864
  }
111734
111865
  }
111735
111866
  /**
@@ -111737,111 +111868,95 @@ var AggregationServiceRunner = class _AggregationServiceRunner extends TypedEven
111737
111868
  * automatically requests tx data and starts signing.
111738
111869
  * @param {BaseMessage} msg - The incoming message to handle.
111739
111870
  * @returns {Promise<void>} Resolves when handling is complete.
111740
- * @throws {Error} If any step of handling fails, the error is emitted and the run promise is rejected.
111741
- * Note: if the runner has been stopped, handlers will ignore incoming messages.
111742
111871
  */
111743
111872
  async #handleValidationAck(msg) {
111744
111873
  if (this.#stopped) return;
111874
+ const ctx = this.#contextFor(msg);
111875
+ if (!ctx) return;
111745
111876
  try {
111746
111877
  this.session.receive(msg);
111747
- this.#drainRejections();
111748
- this.#onPhaseMaybeChanged();
111878
+ this.#drainRejections(ctx);
111879
+ this.#onPhaseMaybeChanged(ctx);
111749
111880
  const approved = !!msg.body?.approved;
111750
- this.emit("validation-received", { participantDid: msg.from, approved });
111751
- const phase = this.session.getCohortPhase(this.#cohortId);
111881
+ this.emit("validation-received", { cohortId: ctx.cohortId, participantDid: msg.from, approved });
111882
+ const phase = this.session.getCohortPhase(ctx.cohortId);
111752
111883
  if (phase === "Failed" /* Failed */) {
111753
111884
  const reason = `Validation rejected by participant ${msg.from}`;
111754
- this.emit("cohort-failed", { cohortId: this.#cohortId, reason });
111755
- this.#fail(new Error(reason));
111885
+ this.emit("cohort-failed", { cohortId: ctx.cohortId, reason });
111886
+ this.#failCohort(ctx, new Error(reason));
111756
111887
  return;
111757
111888
  }
111758
111889
  if (phase === "Validated" /* Validated */) {
111759
- const cohort = this.session.getCohort(this.#cohortId);
111890
+ const cohort = this.session.getCohort(ctx.cohortId);
111760
111891
  const txData = await this.#onProvideTxData({
111761
- cohortId: this.#cohortId,
111892
+ cohortId: ctx.cohortId,
111762
111893
  beaconAddress: cohort.beaconAddress,
111763
111894
  signalBytes: cohort.signalBytes
111764
111895
  });
111765
- const authMsgs = this.session.startSigning(this.#cohortId, txData);
111766
- const sessionId = this.session.getSigningSessionId(this.#cohortId) ?? "";
111767
- this.emit("signing-started", { sessionId });
111896
+ const authMsgs = this.session.startSigning(ctx.cohortId, txData);
111897
+ const sessionId = this.session.getSigningSessionId(ctx.cohortId) ?? "";
111898
+ this.emit("signing-started", { cohortId: ctx.cohortId, sessionId });
111768
111899
  await this.#sendAll(authMsgs);
111769
111900
  }
111770
111901
  } catch (err) {
111771
- this.#fail(err);
111902
+ this.#failCohort(ctx, err);
111772
111903
  }
111773
111904
  }
111774
111905
  /**
111775
- * Handler for receiving nonce contributions and signature authorizations. When all nonces or
111776
- * signatures are received,
111906
+ * Handler for receiving nonce contributions. When all nonces are received, sends the aggregated
111907
+ * nonce back to the cohort.
111777
111908
  * @param {BaseMessage} msg - The incoming message to handle.
111778
111909
  * @returns {Promise<void>} Resolves when handling is complete.
111779
- * @throws {Error} If any step of handling fails, the error is emitted and the run promise is rejected.
111780
- * Note: if the runner has been stopped, handlers will ignore incoming messages.
111781
111910
  */
111782
111911
  async #handleNonceContribution(msg) {
111783
111912
  if (this.#stopped) return;
111913
+ const ctx = this.#contextFor(msg);
111914
+ if (!ctx) return;
111784
111915
  try {
111785
111916
  this.session.receive(msg);
111786
- this.#drainRejections();
111787
- this.#onPhaseMaybeChanged();
111788
- this.emit("nonce-received", { participantDid: msg.from });
111789
- if (this.session.getCohortPhase(this.#cohortId) === "NoncesCollected" /* NoncesCollected */) {
111790
- await this.#sendAll(this.session.sendAggregatedNonce(this.#cohortId));
111917
+ this.#drainRejections(ctx);
111918
+ this.#onPhaseMaybeChanged(ctx);
111919
+ this.emit("nonce-received", { cohortId: ctx.cohortId, participantDid: msg.from });
111920
+ if (this.session.getCohortPhase(ctx.cohortId) === "NoncesCollected" /* NoncesCollected */) {
111921
+ await this.#sendAll(this.session.sendAggregatedNonce(ctx.cohortId));
111791
111922
  }
111792
111923
  } catch (err) {
111793
- this.#fail(err);
111924
+ this.#failCohort(ctx, err);
111794
111925
  }
111795
111926
  }
111796
111927
  /**
111797
111928
  * Handler for receiving signature authorizations. When all partial signatures are received, the
111798
- * session automatically completes and the final result is emitted and the run() promise is resolved.
111929
+ * session automatically completes; the final result is emitted and the cohort's completion
111930
+ * promise resolves.
111799
111931
  * @param {BaseMessage} msg - The incoming message to handle.
111800
111932
  * @returns {Promise<void>} Resolves when handling is complete.
111801
- * @throws {Error} If any step of handling fails, the error is emitted and the run promise is rejected.
111802
- * Note: if the runner has been stopped, handlers will ignore incoming messages.
111803
111933
  */
111804
111934
  async #handleSignatureAuthorization(msg) {
111805
111935
  if (this.#stopped) return;
111936
+ const ctx = this.#contextFor(msg);
111937
+ if (!ctx) return;
111806
111938
  try {
111807
111939
  this.session.receive(msg);
111808
- this.#drainRejections();
111809
- this.#onPhaseMaybeChanged();
111810
- const result = this.session.getResult(this.#cohortId);
111940
+ this.#drainRejections(ctx);
111941
+ this.#onPhaseMaybeChanged(ctx);
111942
+ const result = this.session.getResult(ctx.cohortId);
111811
111943
  if (result) {
111812
- this.#clearTimers();
111813
- this.#unregisterHandlers();
111814
- this.emit("signing-complete", result);
111815
- this.#resolveRun?.(result);
111944
+ this.#completeCohort(ctx, result);
111816
111945
  }
111817
111946
  } catch (err) {
111818
- this.#fail(err);
111947
+ this.#failCohort(ctx, err);
111819
111948
  }
111820
111949
  }
111821
111950
  /**
111822
111951
  * Internal: helper to send all messages sequentially. Catches and propagates errors.
111823
111952
  * @param {BaseMessage[]} msgs - The messages to send.
111824
111953
  * @returns {Promise<void>} Resolves when all messages have been sent.
111825
- * @throws {Error} If sending any message fails, the error is emitted and the run promise is
111826
- * rejected.
111827
111954
  */
111828
111955
  async #sendAll(msgs) {
111829
111956
  for (const m2 of msgs) {
111830
111957
  await this.#transport.sendMessage(m2, this.#did, m2.to);
111831
111958
  }
111832
111959
  }
111833
- /**
111834
- * Internal: helper to handle errors. Emits an 'error' event and rejects the run promise.
111835
- * @param {Error} err - The error to handle.
111836
- */
111837
- #fail(err) {
111838
- this.#stopAdvertRepeating();
111839
- this.#clearTimers();
111840
- this.#unregisterHandlers();
111841
- if (this.#cohortId) this.session.removeCohort(this.#cohortId);
111842
- this.emit("error", err);
111843
- this.#rejectRun?.(err);
111844
- }
111845
111960
  };
111846
111961
 
111847
111962
  // src/core/aggregation/runner/participant-runner.ts
@@ -111902,7 +112017,8 @@ var AggregationParticipantRunner = class _AggregationParticipantRunner extends T
111902
112017
  }
111903
112018
  /**
111904
112019
  * Single-shot helper: start, join the first cohort that passes `shouldJoin`,
111905
- * drive it to completion, and resolve. Convenient for tests and demos.
112020
+ * drive it to completion, and resolve. Convenient for tests and demos. The
112021
+ * single-cohort special case of {@link joinMatching} (count = 1).
111906
112022
  */
111907
112023
  static async joinFirst(options2) {
111908
112024
  return new Promise((resolve, reject) => {
@@ -111915,6 +112031,37 @@ var AggregationParticipantRunner = class _AggregationParticipantRunner extends T
111915
112031
  runner.start().catch(reject);
111916
112032
  });
111917
112033
  }
112034
+ /**
112035
+ * Multi-cohort helper: start, join EVERY cohort whose advert passes
112036
+ * `shouldJoin`, drive each to completion in parallel, and resolve once
112037
+ * `count` cohorts have completed (the runner stops at that point). The
112038
+ * N-cohort generalization of {@link joinFirst}, for a participant that joins
112039
+ * several cohorts advertised by one service.
112040
+ *
112041
+ * For an open-ended, long-lived subscriber (no fixed count), construct an
112042
+ * {@link AggregationParticipantRunner} directly, set `shouldJoin`, call
112043
+ * `start()`, and listen for `cohort-complete` — the runner already drives
112044
+ * any number of cohorts concurrently.
112045
+ *
112046
+ * @param options Participant runner options (set `shouldJoin` to select cohorts).
112047
+ * @param count Number of completed cohorts to collect before resolving.
112048
+ * @returns The {@link CohortCompleteInfo} for each completed cohort, in completion order.
112049
+ */
112050
+ static async joinMatching(options2, count) {
112051
+ return new Promise((resolve, reject) => {
112052
+ const runner = new _AggregationParticipantRunner(options2);
112053
+ const completed = [];
112054
+ runner.on("cohort-complete", (info) => {
112055
+ completed.push(info);
112056
+ if (completed.length >= count) {
112057
+ runner.stop();
112058
+ resolve(completed);
112059
+ }
112060
+ });
112061
+ runner.on("error", reject);
112062
+ runner.start().catch(reject);
112063
+ });
112064
+ }
111918
112065
  /**
111919
112066
  * Internal: handler registration with the transport. Idempotent and safe to call multiple times,
111920
112067
  * but only registers handlers once.
@@ -112063,7 +112210,7 @@ var AggregationParticipantRunner = class _AggregationParticipantRunner extends T
112063
112210
  if (this.session.getCohortPhase(cohortId) === "Complete" /* Complete */) {
112064
112211
  const info = this.session.joinedCohorts.get(cohortId);
112065
112212
  if (info) {
112066
- const validation = this.session.pendingValidations.get(cohortId);
112213
+ const validation = this.session.getValidation(cohortId);
112067
112214
  this.emit("cohort-complete", {
112068
112215
  cohortId,
112069
112216
  beaconAddress: info.beaconAddress,