@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.js CHANGED
@@ -104727,6 +104727,17 @@ a=end-of-candidates
104727
104727
  }
104728
104728
  return map3;
104729
104729
  }
104730
+ /**
104731
+ * The validated aggregated data retained for a cohort, regardless of phase.
104732
+ * Unlike {@link pendingValidations} (which lists only cohorts still awaiting
104733
+ * the validate decision), this returns the stored validation — including the
104734
+ * participant's sidecar (the CAS Announcement map or its SMT inclusion proof)
104735
+ * — so it is still readable once the cohort reaches Complete. Returns
104736
+ * undefined before aggregated data has been received.
104737
+ */
104738
+ getValidation(cohortId) {
104739
+ return this.#cohortStates.get(cohortId)?.validation;
104740
+ }
104730
104741
  #handleDistributeAggregatedData(message2) {
104731
104742
  const cohortId = message2.body?.cohortId;
104732
104743
  if (!cohortId) return;
@@ -111603,35 +111614,22 @@ ${value2}`;
111603
111614
  session;
111604
111615
  #transport;
111605
111616
  #did;
111606
- #config;
111617
+ #defaultConfig;
111607
111618
  #onOptInReceived;
111608
111619
  #onReadyToFinalize;
111609
111620
  #onProvideTxData;
111610
111621
  #cohortTtlMs;
111611
111622
  #phaseTimeoutMs;
111612
111623
  #advertRepeatIntervalMs;
111613
- #cohortId;
111624
+ /** Per-cohort run state, keyed by cohortId. */
111625
+ #contexts = /* @__PURE__ */ new Map();
111614
111626
  #handlersRegistered = false;
111615
111627
  #stopped = false;
111616
- /**
111617
- * Guard against the async race where two concurrent #handleOptIn invocations
111618
- * both pass the `participants.length >= minParticipants` check before either
111619
- * mutates the cohort phase. Set synchronously before any `await` so subsequent
111620
- * handlers observe it on their next resumption.
111621
- */
111622
- #finalizing = false;
111623
- #resolveRun;
111624
- #rejectRun;
111625
- #cohortTtlTimer;
111626
- #phaseTimer;
111627
- #lastObservedPhase;
111628
- /** Stop handle for the repeating COHORT_ADVERT publish loop. */
111629
- #stopAdvertRepeat;
111630
111628
  constructor(options2) {
111631
111629
  super();
111632
111630
  this.#transport = options2.transport;
111633
111631
  this.#did = options2.did;
111634
- this.#config = options2.config;
111632
+ this.#defaultConfig = options2.config;
111635
111633
  this.#onOptInReceived = options2.onOptInReceived ?? (async () => ({ accepted: true }));
111636
111634
  this.#onReadyToFinalize = options2.onReadyToFinalize ?? (async ({ acceptedCount, minRequired }) => ({
111637
111635
  finalize: acceptedCount >= minRequired
@@ -111649,55 +111647,126 @@ ${value2}`;
111649
111647
  maxUpdateSizeBytes: options2.maxUpdateSizeBytes
111650
111648
  });
111651
111649
  }
111650
+ /** Resolve the {@link RunContext} an inbound message belongs to, by cohortId. */
111651
+ #contextFor(msg) {
111652
+ const cohortId = msg.body?.cohortId;
111653
+ if (!cohortId) return void 0;
111654
+ return this.#contexts.get(cohortId);
111655
+ }
111652
111656
  /**
111653
- * Drain any silent rejections the state machine recorded during the most
111654
- * recent receive() and surface them as `message-rejected` events. Safe to
111655
- * call even before a cohortId is assigned.
111657
+ * Drain any silent rejections the state machine recorded for a cohort during
111658
+ * the most recent receive() and surface them as `message-rejected` events.
111656
111659
  */
111657
- #drainRejections() {
111658
- if (!this.#cohortId) return;
111659
- for (const r2 of this.session.drainRejections(this.#cohortId)) {
111660
- this.emit("message-rejected", { cohortId: this.#cohortId, ...r2 });
111660
+ #drainRejections(ctx) {
111661
+ for (const r2 of this.session.drainRejections(ctx.cohortId)) {
111662
+ this.emit("message-rejected", { cohortId: ctx.cohortId, ...r2 });
111661
111663
  }
111662
111664
  }
111663
111665
  /**
111664
- * Run the protocol to completion. Resolves with the final aggregation result
111665
- * (signature + signed transaction) once signing is complete.
111666
+ * Advertise a new cohort and begin driving it to completion. Callable many
111667
+ * times on one runner; each cohort runs concurrently and independently.
111668
+ *
111669
+ * @param config Per-cohort conditions + network (see {@link CohortConfig}).
111670
+ * @returns The new cohort's id and a `completion` promise that resolves with
111671
+ * that cohort's {@link AggregationResult} (or rejects if it fails/stalls).
111672
+ * @throws If the runner has been stopped, or the config is invalid
111673
+ * (fail-fast via `createCohort`).
111674
+ */
111675
+ advertiseCohort(config) {
111676
+ if (this.#stopped) {
111677
+ throw new AggregationServiceError("Cannot advertise on a stopped runner.", "RUNNER_STOPPED", {});
111678
+ }
111679
+ this.#registerHandlers();
111680
+ const cohortId = this.session.createCohort(config);
111681
+ let resolve;
111682
+ let reject;
111683
+ const completion = new Promise((res, rej) => {
111684
+ resolve = res;
111685
+ reject = rej;
111686
+ });
111687
+ const ctx = {
111688
+ cohortId,
111689
+ config,
111690
+ resolve,
111691
+ reject,
111692
+ completion,
111693
+ finalizing: false,
111694
+ settled: false
111695
+ };
111696
+ this.#contexts.set(cohortId, ctx);
111697
+ try {
111698
+ this.#startTimers(ctx);
111699
+ const advertMsgs = this.session.advertise(cohortId);
111700
+ this.#onPhaseMaybeChanged(ctx);
111701
+ this.emit("cohort-advertised", { cohortId });
111702
+ if (this.#advertRepeatIntervalMs > 0) {
111703
+ this.#startAdvertRepeat(ctx, advertMsgs);
111704
+ } else {
111705
+ this.#sendAll(advertMsgs).catch((err) => this.#failCohort(ctx, err));
111706
+ }
111707
+ } catch (err) {
111708
+ this.#failCohort(ctx, err);
111709
+ }
111710
+ return { cohortId, completion };
111711
+ }
111712
+ /**
111713
+ * Run a single cohort to completion using the `config` supplied in the
111714
+ * runner options. Thin convenience over {@link advertiseCohort} for the
111715
+ * single-cohort case (and the path {@link AggregationRunner.solo} rides).
111666
111716
  *
111667
111717
  * @returns {Promise<AggregationResult>} The final result with signature and signed tx.
111668
111718
  */
111669
111719
  run() {
111670
- return new Promise((resolve, reject) => {
111671
- this.#resolveRun = resolve;
111672
- this.#rejectRun = reject;
111673
- try {
111674
- this.#registerHandlers();
111675
- this.#cohortId = this.session.createCohort(this.#config);
111676
- this.#startTimers();
111677
- const advertMsgs = this.session.advertise(this.#cohortId);
111678
- this.#onPhaseMaybeChanged();
111679
- this.emit("cohort-advertised", { cohortId: this.#cohortId });
111680
- if (this.#advertRepeatIntervalMs > 0) {
111681
- this.#startAdvertRepeat(advertMsgs);
111682
- } else {
111683
- this.#sendAll(advertMsgs).catch((err) => this.#fail(err));
111684
- }
111685
- } catch (err) {
111686
- this.#fail(err);
111720
+ if (!this.#defaultConfig) {
111721
+ return Promise.reject(new AggregationServiceError(
111722
+ "run() requires `config` in the runner options; use advertiseCohort(config) to drive cohorts explicitly.",
111723
+ "MISSING_COHORT_CONFIG",
111724
+ {}
111725
+ ));
111726
+ }
111727
+ try {
111728
+ return this.advertiseCohort(this.#defaultConfig).completion;
111729
+ } catch (err) {
111730
+ return Promise.reject(err);
111731
+ }
111732
+ }
111733
+ /**
111734
+ * Wait for every currently-outstanding cohort to settle and return the
111735
+ * successful results. Dynamic drain: cohorts advertised while this is pending
111736
+ * are included, and it resolves only once no cohorts remain. Failed cohorts
111737
+ * are surfaced via `error` / `cohort-failed` events and their rejected
111738
+ * `completion` promises; they are omitted from the returned array (this
111739
+ * method does not throw). Bound long-running cohorts with `cohortTtlMs` /
111740
+ * `phaseTimeoutMs` or this may never resolve.
111741
+ *
111742
+ * @returns {Promise<AggregationResult[]>} Results of the cohorts that completed.
111743
+ */
111744
+ async runAll() {
111745
+ const collected = /* @__PURE__ */ new Map();
111746
+ const onComplete = (result) => {
111747
+ collected.set(result.cohortId, result);
111748
+ };
111749
+ this.on("signing-complete", onComplete);
111750
+ try {
111751
+ while (this.#contexts.size > 0) {
111752
+ await Promise.allSettled([...this.#contexts.values()].map((c2) => c2.completion));
111687
111753
  }
111688
- });
111754
+ } finally {
111755
+ this.off("signing-complete", onComplete);
111756
+ }
111757
+ return [...collected.values()];
111689
111758
  }
111690
111759
  /**
111691
- * Begin publishing the cohort advert immediately and on a repeating interval
111692
- * until {@link #stopAdvertRepeating} is called. Each advert is broadcast
111693
- * (no recipient) via the transport's `publishRepeating` primitive.
111760
+ * Begin publishing a cohort's advert immediately and on a repeating interval
111761
+ * until the cohort's advert loop is stopped. Each advert is broadcast (no
111762
+ * recipient) via the transport's `publishRepeating` primitive.
111694
111763
  */
111695
- #startAdvertRepeat(advertMsgs) {
111764
+ #startAdvertRepeat(ctx, advertMsgs) {
111696
111765
  const stops = [];
111697
111766
  for (const msg of advertMsgs) {
111698
111767
  stops.push(this.#transport.publishRepeating(msg, this.#did, this.#advertRepeatIntervalMs));
111699
111768
  }
111700
- this.#stopAdvertRepeat = () => {
111769
+ ctx.stopAdvertRepeat = () => {
111701
111770
  for (const stop2 of stops) {
111702
111771
  try {
111703
111772
  stop2();
@@ -111706,61 +111775,118 @@ ${value2}`;
111706
111775
  }
111707
111776
  };
111708
111777
  }
111709
- /** Stop the advert republish loop. Idempotent. */
111710
- #stopAdvertRepeating() {
111711
- if (!this.#stopAdvertRepeat) return;
111712
- const stop2 = this.#stopAdvertRepeat;
111713
- this.#stopAdvertRepeat = void 0;
111778
+ /** Stop a cohort's advert republish loop. Idempotent. */
111779
+ #stopAdvertRepeating(ctx) {
111780
+ if (!ctx.stopAdvertRepeat) return;
111781
+ const stop2 = ctx.stopAdvertRepeat;
111782
+ ctx.stopAdvertRepeat = void 0;
111714
111783
  stop2();
111715
111784
  }
111716
- /** Schedule cohort TTL + phase timeout at the start of a run. */
111717
- #startTimers() {
111785
+ /** Schedule a cohort's TTL + phase timeout when it is advertised. */
111786
+ #startTimers(ctx) {
111718
111787
  if (this.#cohortTtlMs !== void 0) {
111719
- this.#cohortTtlTimer = setTimeout(() => {
111720
- const reason = `Cohort ${this.#cohortId ?? ""} exceeded TTL of ${this.#cohortTtlMs}ms`;
111721
- this.emit("cohort-failed", { cohortId: this.#cohortId ?? "", reason });
111722
- this.#fail(new Error(reason));
111788
+ ctx.cohortTtlTimer = setTimeout(() => {
111789
+ const reason = `Cohort ${ctx.cohortId} exceeded TTL of ${this.#cohortTtlMs}ms`;
111790
+ this.emit("cohort-failed", { cohortId: ctx.cohortId, reason });
111791
+ this.#failCohort(ctx, new Error(reason));
111723
111792
  }, this.#cohortTtlMs);
111724
111793
  }
111725
- this.#resetPhaseTimer();
111794
+ this.#resetPhaseTimer(ctx);
111726
111795
  }
111727
- /** Reset the per-phase stall timer. Called when a phase transition is observed. */
111728
- #resetPhaseTimer() {
111729
- if (this.#phaseTimer) clearTimeout(this.#phaseTimer);
111730
- this.#phaseTimer = void 0;
111796
+ /** Reset a cohort's per-phase stall timer. Called when a phase transition is observed. */
111797
+ #resetPhaseTimer(ctx) {
111798
+ if (ctx.phaseTimer) clearTimeout(ctx.phaseTimer);
111799
+ ctx.phaseTimer = void 0;
111731
111800
  if (this.#phaseTimeoutMs === void 0) return;
111732
- this.#phaseTimer = setTimeout(() => {
111733
- const reason = `Cohort ${this.#cohortId ?? ""} stalled in phase ${this.#lastObservedPhase ?? "?"} for ${this.#phaseTimeoutMs}ms`;
111734
- this.emit("cohort-failed", { cohortId: this.#cohortId ?? "", reason });
111735
- this.#fail(new Error(reason));
111801
+ ctx.phaseTimer = setTimeout(() => {
111802
+ const reason = `Cohort ${ctx.cohortId} stalled in phase ${ctx.lastObservedPhase ?? "?"} for ${this.#phaseTimeoutMs}ms`;
111803
+ this.emit("cohort-failed", { cohortId: ctx.cohortId, reason });
111804
+ this.#failCohort(ctx, new Error(reason));
111736
111805
  }, this.#phaseTimeoutMs);
111737
111806
  }
111738
- /** Detect a phase change since the last observation and reset the phase timer. */
111739
- #onPhaseMaybeChanged() {
111740
- if (!this.#cohortId) return;
111741
- const phase = this.session.getCohortPhase(this.#cohortId);
111742
- if (phase !== this.#lastObservedPhase) {
111743
- this.#lastObservedPhase = phase;
111744
- this.#resetPhaseTimer();
111807
+ /** Detect a phase change for a cohort since the last observation and reset its phase timer. */
111808
+ #onPhaseMaybeChanged(ctx) {
111809
+ const phase = this.session.getCohortPhase(ctx.cohortId);
111810
+ if (phase !== ctx.lastObservedPhase) {
111811
+ ctx.lastObservedPhase = phase;
111812
+ this.#resetPhaseTimer(ctx);
111745
111813
  }
111746
111814
  }
111747
- /** Clear both timers. Called on successful completion, stop(), and #fail. */
111748
- #clearTimers() {
111749
- if (this.#cohortTtlTimer) clearTimeout(this.#cohortTtlTimer);
111750
- if (this.#phaseTimer) clearTimeout(this.#phaseTimer);
111751
- this.#cohortTtlTimer = void 0;
111752
- this.#phaseTimer = void 0;
111815
+ /** Clear a cohort's timers. Called on completion, stop, and failure. */
111816
+ #clearTimers(ctx) {
111817
+ if (ctx.cohortTtlTimer) clearTimeout(ctx.cohortTtlTimer);
111818
+ if (ctx.phaseTimer) clearTimeout(ctx.phaseTimer);
111819
+ ctx.cohortTtlTimer = void 0;
111820
+ ctx.phaseTimer = void 0;
111821
+ }
111822
+ /**
111823
+ * Reclaim one cohort's runner-layer bookkeeping: stop its advert loop, clear
111824
+ * its timers, and drop its {@link RunContext}. Does NOT touch sibling cohorts
111825
+ * and does NOT detach the shared transport handlers. Leaves the cohort in the
111826
+ * state machine; whether that cohort's `session` state is also removed is the
111827
+ * caller's choice (see {@link #completeCohort} vs {@link #failCohort}).
111828
+ */
111829
+ #disposeCohort(ctx) {
111830
+ this.#stopAdvertRepeating(ctx);
111831
+ this.#clearTimers(ctx);
111832
+ this.#contexts.delete(ctx.cohortId);
111833
+ }
111834
+ /**
111835
+ * Settle one cohort successfully. Reclaims the runner context but leaves the
111836
+ * completed cohort in `session` so callers can read its beaconAddress / cohort
111837
+ * via `session.getCohort(result.cohortId)`; reclaim it with
111838
+ * `session.removeCohort(cohortId)` when done. Idempotent via `ctx.settled`.
111839
+ */
111840
+ #completeCohort(ctx, result) {
111841
+ if (ctx.settled) return;
111842
+ ctx.settled = true;
111843
+ this.#disposeCohort(ctx);
111844
+ this.emit("signing-complete", result);
111845
+ ctx.resolve(result);
111846
+ }
111847
+ /**
111848
+ * Fail one cohort. Reclaims its runner context, drops its now-dead state from
111849
+ * the state machine, and rejects only its completion; siblings keep running
111850
+ * and the shared transport handlers stay registered. Idempotent via
111851
+ * `ctx.settled`.
111852
+ */
111853
+ #failCohort(ctx, err) {
111854
+ if (ctx.settled) return;
111855
+ ctx.settled = true;
111856
+ this.#disposeCohort(ctx);
111857
+ this.session.removeCohort(ctx.cohortId);
111858
+ this.emit("error", err);
111859
+ ctx.reject(err);
111860
+ }
111861
+ /**
111862
+ * Stop a single cohort early without affecting the rest of the runner. Drops
111863
+ * the cohort's state machine state; its `completion` promise rejects with a
111864
+ * stopped error.
111865
+ */
111866
+ stopCohort(cohortId) {
111867
+ const ctx = this.#contexts.get(cohortId);
111868
+ if (!ctx || ctx.settled) return;
111869
+ ctx.settled = true;
111870
+ this.#disposeCohort(ctx);
111871
+ this.session.removeCohort(cohortId);
111872
+ ctx.reject(new AggregationServiceError(`Cohort ${cohortId} stopped.`, "COHORT_STOPPED", { cohortId }));
111753
111873
  }
111754
111874
  /**
111755
- * Stop the runner early. Marks the runner stopped and detaches transport
111756
- * handlers so a restart or a new runner doesn't inherit stale dispatch.
111875
+ * Stop the whole runner. Fails every outstanding cohort, then detaches the
111876
+ * shared transport handlers so a restart or a new runner doesn't inherit
111877
+ * stale dispatch. Safe to call repeatedly.
111757
111878
  */
111758
111879
  stop() {
111759
111880
  this.#stopped = true;
111760
- this.#stopAdvertRepeating();
111761
- this.#clearTimers();
111881
+ for (const ctx of [...this.#contexts.values()]) {
111882
+ if (ctx.settled) continue;
111883
+ ctx.settled = true;
111884
+ this.#disposeCohort(ctx);
111885
+ this.session.removeCohort(ctx.cohortId);
111886
+ ctx.reject(new AggregationServiceError("Service runner stopped.", "RUNNER_STOPPED", { cohortId: ctx.cohortId }));
111887
+ }
111888
+ this.#contexts.clear();
111762
111889
  this.#unregisterHandlers();
111763
- if (this.#cohortId) this.session.removeCohort(this.#cohortId);
111764
111890
  }
111765
111891
  /** Message types this runner listens for on the transport. */
111766
111892
  static #HANDLED_MESSAGE_TYPES = [
@@ -111771,7 +111897,10 @@ ${value2}`;
111771
111897
  SIGNATURE_AUTHORIZATION
111772
111898
  ];
111773
111899
  /**
111774
- * Internal: handler registration with the transport. Idempotent.
111900
+ * Internal: handler registration with the transport. Idempotent. Handlers
111901
+ * are DID-scoped and cohort-agnostic — one registration serves every cohort
111902
+ * this runner drives; demux to the right {@link RunContext} happens in each
111903
+ * handler via the inbound message's cohortId.
111775
111904
  */
111776
111905
  #registerHandlers() {
111777
111906
  if (this.#handlersRegistered) return;
@@ -111792,23 +111921,25 @@ ${value2}`;
111792
111921
  }
111793
111922
  /**
111794
111923
  * Internal: message handlers for each protocol step. Each handler:
111795
- * 1) feeds the message into the state machine via session.receive()
111796
- * 2) emits a high-level event for external observers
111797
- * 3) checks if the new state triggers any automatic next steps, and if so:
111924
+ * 1) resolves the cohort the message belongs to (by cohortId); ignores it if unknown
111925
+ * 2) feeds the message into the state machine via session.receive()
111926
+ * 3) emits a high-level event (carrying cohortId) for external observers
111927
+ * 4) checks if the new state triggers any automatic next steps, and if so:
111798
111928
  * a) calls the appropriate decision callback(s)
111799
111929
  * b) sends any resulting messages from the state machine
111930
+ * Errors fail only the owning cohort. A stopped runner ignores messages.
111800
111931
  * @param {BaseMessage} msg - The incoming message to handle.
111801
111932
  * @returns {Promise<void>} Resolves when handling is complete.
111802
- * @throws {Error} If any step of handling fails, the error is emitted and the run promise is rejected.
111803
- * Note: if the runner has been stopped, handlers will ignore incoming messages.
111804
111933
  */
111805
111934
  async #handleOptIn(msg) {
111806
111935
  if (this.#stopped) return;
111936
+ const ctx = this.#contextFor(msg);
111937
+ if (!ctx) return;
111807
111938
  try {
111808
111939
  this.session.receive(msg);
111809
- this.#drainRejections();
111810
- this.#onPhaseMaybeChanged();
111811
- const optIn = this.session.pendingOptIns(this.#cohortId).get(msg.from);
111940
+ this.#drainRejections(ctx);
111941
+ this.#onPhaseMaybeChanged(ctx);
111942
+ const optIn = this.session.pendingOptIns(ctx.cohortId).get(msg.from);
111812
111943
  if (!optIn) return;
111813
111944
  this.emit("opt-in-received", optIn);
111814
111945
  if (optIn.communicationPk) {
@@ -111816,34 +111947,34 @@ ${value2}`;
111816
111947
  }
111817
111948
  const decision = await this.#onOptInReceived(optIn);
111818
111949
  if (!decision.accepted) return;
111819
- const maxParticipants = this.#config.maxParticipants;
111820
- const cohortNow = this.session.getCohort(this.#cohortId);
111950
+ const maxParticipants = ctx.config.maxParticipants;
111951
+ const cohortNow = this.session.getCohort(ctx.cohortId);
111821
111952
  if (maxParticipants !== void 0 && cohortNow && cohortNow.participants.length >= maxParticipants) {
111822
111953
  return;
111823
111954
  }
111824
- await this.#sendAll(this.session.acceptParticipant(this.#cohortId, msg.from));
111825
- this.emit("participant-accepted", { participantDid: msg.from });
111826
- const cohort = this.session.getCohort(this.#cohortId);
111827
- if (cohort.participants.length >= this.#config.minParticipants && !this.#finalizing) {
111828
- this.#finalizing = true;
111955
+ await this.#sendAll(this.session.acceptParticipant(ctx.cohortId, msg.from));
111956
+ this.emit("participant-accepted", { cohortId: ctx.cohortId, participantDid: msg.from });
111957
+ const cohort = this.session.getCohort(ctx.cohortId);
111958
+ if (cohort.participants.length >= ctx.config.minParticipants && !ctx.finalizing) {
111959
+ ctx.finalizing = true;
111829
111960
  const finalizeDecision = await this.#onReadyToFinalize({
111830
111961
  acceptedCount: cohort.participants.length,
111831
- minRequired: this.#config.minParticipants
111962
+ minRequired: ctx.config.minParticipants
111832
111963
  });
111833
111964
  if (!finalizeDecision.finalize) {
111834
- this.#finalizing = false;
111965
+ ctx.finalizing = false;
111835
111966
  return;
111836
111967
  }
111837
- const readyMsgs = this.session.finalizeKeygen(this.#cohortId);
111838
- this.#stopAdvertRepeating();
111968
+ const readyMsgs = this.session.finalizeKeygen(ctx.cohortId);
111969
+ this.#stopAdvertRepeating(ctx);
111839
111970
  this.emit("keygen-complete", {
111840
- cohortId: this.#cohortId,
111971
+ cohortId: ctx.cohortId,
111841
111972
  beaconAddress: cohort.beaconAddress
111842
111973
  });
111843
111974
  await this.#sendAll(readyMsgs);
111844
111975
  }
111845
111976
  } catch (err) {
111846
- this.#fail(err);
111977
+ this.#failCohort(ctx, err);
111847
111978
  }
111848
111979
  }
111849
111980
  /**
@@ -111851,23 +111982,23 @@ ${value2}`;
111851
111982
  * and distributes the data for validation.
111852
111983
  * @param {BaseMessage} msg - The incoming message to handle.
111853
111984
  * @returns {Promise<void>} Resolves when handling is complete.
111854
- * @throws {Error} If any step of handling fails, the error is emitted and the run promise is rejected.
111855
- * Note: if the runner has been stopped, handlers will ignore incoming messages.
111856
111985
  */
111857
111986
  async #handleSubmitUpdate(msg) {
111858
111987
  if (this.#stopped) return;
111988
+ const ctx = this.#contextFor(msg);
111989
+ if (!ctx) return;
111859
111990
  try {
111860
111991
  this.session.receive(msg);
111861
- this.#drainRejections();
111862
- this.#onPhaseMaybeChanged();
111863
- this.emit("update-received", { participantDid: msg.from });
111864
- if (this.session.getCohortPhase(this.#cohortId) === "UpdatesCollected" /* UpdatesCollected */) {
111865
- const distributeMsgs = this.session.buildAndDistribute(this.#cohortId);
111866
- this.emit("data-distributed", { cohortId: this.#cohortId });
111992
+ this.#drainRejections(ctx);
111993
+ this.#onPhaseMaybeChanged(ctx);
111994
+ this.emit("update-received", { cohortId: ctx.cohortId, participantDid: msg.from });
111995
+ if (this.session.getCohortPhase(ctx.cohortId) === "UpdatesCollected" /* UpdatesCollected */) {
111996
+ const distributeMsgs = this.session.buildAndDistribute(ctx.cohortId);
111997
+ this.emit("data-distributed", { cohortId: ctx.cohortId });
111867
111998
  await this.#sendAll(distributeMsgs);
111868
111999
  }
111869
112000
  } catch (err) {
111870
- this.#fail(err);
112001
+ this.#failCohort(ctx, err);
111871
112002
  }
111872
112003
  }
111873
112004
  /**
@@ -111875,111 +112006,95 @@ ${value2}`;
111875
112006
  * automatically requests tx data and starts signing.
111876
112007
  * @param {BaseMessage} msg - The incoming message to handle.
111877
112008
  * @returns {Promise<void>} Resolves when handling is complete.
111878
- * @throws {Error} If any step of handling fails, the error is emitted and the run promise is rejected.
111879
- * Note: if the runner has been stopped, handlers will ignore incoming messages.
111880
112009
  */
111881
112010
  async #handleValidationAck(msg) {
111882
112011
  if (this.#stopped) return;
112012
+ const ctx = this.#contextFor(msg);
112013
+ if (!ctx) return;
111883
112014
  try {
111884
112015
  this.session.receive(msg);
111885
- this.#drainRejections();
111886
- this.#onPhaseMaybeChanged();
112016
+ this.#drainRejections(ctx);
112017
+ this.#onPhaseMaybeChanged(ctx);
111887
112018
  const approved = !!msg.body?.approved;
111888
- this.emit("validation-received", { participantDid: msg.from, approved });
111889
- const phase = this.session.getCohortPhase(this.#cohortId);
112019
+ this.emit("validation-received", { cohortId: ctx.cohortId, participantDid: msg.from, approved });
112020
+ const phase = this.session.getCohortPhase(ctx.cohortId);
111890
112021
  if (phase === "Failed" /* Failed */) {
111891
112022
  const reason = `Validation rejected by participant ${msg.from}`;
111892
- this.emit("cohort-failed", { cohortId: this.#cohortId, reason });
111893
- this.#fail(new Error(reason));
112023
+ this.emit("cohort-failed", { cohortId: ctx.cohortId, reason });
112024
+ this.#failCohort(ctx, new Error(reason));
111894
112025
  return;
111895
112026
  }
111896
112027
  if (phase === "Validated" /* Validated */) {
111897
- const cohort = this.session.getCohort(this.#cohortId);
112028
+ const cohort = this.session.getCohort(ctx.cohortId);
111898
112029
  const txData = await this.#onProvideTxData({
111899
- cohortId: this.#cohortId,
112030
+ cohortId: ctx.cohortId,
111900
112031
  beaconAddress: cohort.beaconAddress,
111901
112032
  signalBytes: cohort.signalBytes
111902
112033
  });
111903
- const authMsgs = this.session.startSigning(this.#cohortId, txData);
111904
- const sessionId = this.session.getSigningSessionId(this.#cohortId) ?? "";
111905
- this.emit("signing-started", { sessionId });
112034
+ const authMsgs = this.session.startSigning(ctx.cohortId, txData);
112035
+ const sessionId = this.session.getSigningSessionId(ctx.cohortId) ?? "";
112036
+ this.emit("signing-started", { cohortId: ctx.cohortId, sessionId });
111906
112037
  await this.#sendAll(authMsgs);
111907
112038
  }
111908
112039
  } catch (err) {
111909
- this.#fail(err);
112040
+ this.#failCohort(ctx, err);
111910
112041
  }
111911
112042
  }
111912
112043
  /**
111913
- * Handler for receiving nonce contributions and signature authorizations. When all nonces or
111914
- * signatures are received,
112044
+ * Handler for receiving nonce contributions. When all nonces are received, sends the aggregated
112045
+ * nonce back to the cohort.
111915
112046
  * @param {BaseMessage} msg - The incoming message to handle.
111916
112047
  * @returns {Promise<void>} Resolves when handling is complete.
111917
- * @throws {Error} If any step of handling fails, the error is emitted and the run promise is rejected.
111918
- * Note: if the runner has been stopped, handlers will ignore incoming messages.
111919
112048
  */
111920
112049
  async #handleNonceContribution(msg) {
111921
112050
  if (this.#stopped) return;
112051
+ const ctx = this.#contextFor(msg);
112052
+ if (!ctx) return;
111922
112053
  try {
111923
112054
  this.session.receive(msg);
111924
- this.#drainRejections();
111925
- this.#onPhaseMaybeChanged();
111926
- this.emit("nonce-received", { participantDid: msg.from });
111927
- if (this.session.getCohortPhase(this.#cohortId) === "NoncesCollected" /* NoncesCollected */) {
111928
- await this.#sendAll(this.session.sendAggregatedNonce(this.#cohortId));
112055
+ this.#drainRejections(ctx);
112056
+ this.#onPhaseMaybeChanged(ctx);
112057
+ this.emit("nonce-received", { cohortId: ctx.cohortId, participantDid: msg.from });
112058
+ if (this.session.getCohortPhase(ctx.cohortId) === "NoncesCollected" /* NoncesCollected */) {
112059
+ await this.#sendAll(this.session.sendAggregatedNonce(ctx.cohortId));
111929
112060
  }
111930
112061
  } catch (err) {
111931
- this.#fail(err);
112062
+ this.#failCohort(ctx, err);
111932
112063
  }
111933
112064
  }
111934
112065
  /**
111935
112066
  * Handler for receiving signature authorizations. When all partial signatures are received, the
111936
- * session automatically completes and the final result is emitted and the run() promise is resolved.
112067
+ * session automatically completes; the final result is emitted and the cohort's completion
112068
+ * promise resolves.
111937
112069
  * @param {BaseMessage} msg - The incoming message to handle.
111938
112070
  * @returns {Promise<void>} Resolves when handling is complete.
111939
- * @throws {Error} If any step of handling fails, the error is emitted and the run promise is rejected.
111940
- * Note: if the runner has been stopped, handlers will ignore incoming messages.
111941
112071
  */
111942
112072
  async #handleSignatureAuthorization(msg) {
111943
112073
  if (this.#stopped) return;
112074
+ const ctx = this.#contextFor(msg);
112075
+ if (!ctx) return;
111944
112076
  try {
111945
112077
  this.session.receive(msg);
111946
- this.#drainRejections();
111947
- this.#onPhaseMaybeChanged();
111948
- const result = this.session.getResult(this.#cohortId);
112078
+ this.#drainRejections(ctx);
112079
+ this.#onPhaseMaybeChanged(ctx);
112080
+ const result = this.session.getResult(ctx.cohortId);
111949
112081
  if (result) {
111950
- this.#clearTimers();
111951
- this.#unregisterHandlers();
111952
- this.emit("signing-complete", result);
111953
- this.#resolveRun?.(result);
112082
+ this.#completeCohort(ctx, result);
111954
112083
  }
111955
112084
  } catch (err) {
111956
- this.#fail(err);
112085
+ this.#failCohort(ctx, err);
111957
112086
  }
111958
112087
  }
111959
112088
  /**
111960
112089
  * Internal: helper to send all messages sequentially. Catches and propagates errors.
111961
112090
  * @param {BaseMessage[]} msgs - The messages to send.
111962
112091
  * @returns {Promise<void>} Resolves when all messages have been sent.
111963
- * @throws {Error} If sending any message fails, the error is emitted and the run promise is
111964
- * rejected.
111965
112092
  */
111966
112093
  async #sendAll(msgs) {
111967
112094
  for (const m2 of msgs) {
111968
112095
  await this.#transport.sendMessage(m2, this.#did, m2.to);
111969
112096
  }
111970
112097
  }
111971
- /**
111972
- * Internal: helper to handle errors. Emits an 'error' event and rejects the run promise.
111973
- * @param {Error} err - The error to handle.
111974
- */
111975
- #fail(err) {
111976
- this.#stopAdvertRepeating();
111977
- this.#clearTimers();
111978
- this.#unregisterHandlers();
111979
- if (this.#cohortId) this.session.removeCohort(this.#cohortId);
111980
- this.emit("error", err);
111981
- this.#rejectRun?.(err);
111982
- }
111983
112098
  };
111984
112099
 
111985
112100
  // src/core/aggregation/runner/participant-runner.ts
@@ -112040,7 +112155,8 @@ ${value2}`;
112040
112155
  }
112041
112156
  /**
112042
112157
  * Single-shot helper: start, join the first cohort that passes `shouldJoin`,
112043
- * drive it to completion, and resolve. Convenient for tests and demos.
112158
+ * drive it to completion, and resolve. Convenient for tests and demos. The
112159
+ * single-cohort special case of {@link joinMatching} (count = 1).
112044
112160
  */
112045
112161
  static async joinFirst(options2) {
112046
112162
  return new Promise((resolve, reject) => {
@@ -112053,6 +112169,37 @@ ${value2}`;
112053
112169
  runner.start().catch(reject);
112054
112170
  });
112055
112171
  }
112172
+ /**
112173
+ * Multi-cohort helper: start, join EVERY cohort whose advert passes
112174
+ * `shouldJoin`, drive each to completion in parallel, and resolve once
112175
+ * `count` cohorts have completed (the runner stops at that point). The
112176
+ * N-cohort generalization of {@link joinFirst}, for a participant that joins
112177
+ * several cohorts advertised by one service.
112178
+ *
112179
+ * For an open-ended, long-lived subscriber (no fixed count), construct an
112180
+ * {@link AggregationParticipantRunner} directly, set `shouldJoin`, call
112181
+ * `start()`, and listen for `cohort-complete` — the runner already drives
112182
+ * any number of cohorts concurrently.
112183
+ *
112184
+ * @param options Participant runner options (set `shouldJoin` to select cohorts).
112185
+ * @param count Number of completed cohorts to collect before resolving.
112186
+ * @returns The {@link CohortCompleteInfo} for each completed cohort, in completion order.
112187
+ */
112188
+ static async joinMatching(options2, count) {
112189
+ return new Promise((resolve, reject) => {
112190
+ const runner = new _AggregationParticipantRunner(options2);
112191
+ const completed = [];
112192
+ runner.on("cohort-complete", (info) => {
112193
+ completed.push(info);
112194
+ if (completed.length >= count) {
112195
+ runner.stop();
112196
+ resolve(completed);
112197
+ }
112198
+ });
112199
+ runner.on("error", reject);
112200
+ runner.start().catch(reject);
112201
+ });
112202
+ }
112056
112203
  /**
112057
112204
  * Internal: handler registration with the transport. Idempotent and safe to call multiple times,
112058
112205
  * but only registers handlers once.
@@ -112201,7 +112348,7 @@ ${value2}`;
112201
112348
  if (this.session.getCohortPhase(cohortId) === "Complete" /* Complete */) {
112202
112349
  const info = this.session.joinedCohorts.get(cohortId);
112203
112350
  if (info) {
112204
- const validation = this.session.pendingValidations.get(cohortId);
112351
+ const validation = this.session.getValidation(cohortId);
112205
112352
  this.emit("cohort-complete", {
112206
112353
  cohortId,
112207
112354
  beaconAddress: info.beaconAddress,