@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/cjs/index.js CHANGED
@@ -1645,6 +1645,17 @@ var AggregationParticipant = class {
1645
1645
  }
1646
1646
  return map;
1647
1647
  }
1648
+ /**
1649
+ * The validated aggregated data retained for a cohort, regardless of phase.
1650
+ * Unlike {@link pendingValidations} (which lists only cohorts still awaiting
1651
+ * the validate decision), this returns the stored validation — including the
1652
+ * participant's sidecar (the CAS Announcement map or its SMT inclusion proof)
1653
+ * — so it is still readable once the cohort reaches Complete. Returns
1654
+ * undefined before aggregated data has been received.
1655
+ */
1656
+ getValidation(cohortId) {
1657
+ return this.#cohortStates.get(cohortId)?.validation;
1658
+ }
1648
1659
  #handleDistributeAggregatedData(message) {
1649
1660
  const cohortId = message.body?.cohortId;
1650
1661
  if (!cohortId) return;
@@ -4017,35 +4028,22 @@ var AggregationServiceRunner = class _AggregationServiceRunner extends TypedEven
4017
4028
  session;
4018
4029
  #transport;
4019
4030
  #did;
4020
- #config;
4031
+ #defaultConfig;
4021
4032
  #onOptInReceived;
4022
4033
  #onReadyToFinalize;
4023
4034
  #onProvideTxData;
4024
4035
  #cohortTtlMs;
4025
4036
  #phaseTimeoutMs;
4026
4037
  #advertRepeatIntervalMs;
4027
- #cohortId;
4038
+ /** Per-cohort run state, keyed by cohortId. */
4039
+ #contexts = /* @__PURE__ */ new Map();
4028
4040
  #handlersRegistered = false;
4029
4041
  #stopped = false;
4030
- /**
4031
- * Guard against the async race where two concurrent #handleOptIn invocations
4032
- * both pass the `participants.length >= minParticipants` check before either
4033
- * mutates the cohort phase. Set synchronously before any `await` so subsequent
4034
- * handlers observe it on their next resumption.
4035
- */
4036
- #finalizing = false;
4037
- #resolveRun;
4038
- #rejectRun;
4039
- #cohortTtlTimer;
4040
- #phaseTimer;
4041
- #lastObservedPhase;
4042
- /** Stop handle for the repeating COHORT_ADVERT publish loop. */
4043
- #stopAdvertRepeat;
4044
4042
  constructor(options) {
4045
4043
  super();
4046
4044
  this.#transport = options.transport;
4047
4045
  this.#did = options.did;
4048
- this.#config = options.config;
4046
+ this.#defaultConfig = options.config;
4049
4047
  this.#onOptInReceived = options.onOptInReceived ?? (async () => ({ accepted: true }));
4050
4048
  this.#onReadyToFinalize = options.onReadyToFinalize ?? (async ({ acceptedCount, minRequired }) => ({
4051
4049
  finalize: acceptedCount >= minRequired
@@ -4063,55 +4061,126 @@ var AggregationServiceRunner = class _AggregationServiceRunner extends TypedEven
4063
4061
  maxUpdateSizeBytes: options.maxUpdateSizeBytes
4064
4062
  });
4065
4063
  }
4064
+ /** Resolve the {@link RunContext} an inbound message belongs to, by cohortId. */
4065
+ #contextFor(msg) {
4066
+ const cohortId = msg.body?.cohortId;
4067
+ if (!cohortId) return void 0;
4068
+ return this.#contexts.get(cohortId);
4069
+ }
4066
4070
  /**
4067
- * Drain any silent rejections the state machine recorded during the most
4068
- * recent receive() and surface them as `message-rejected` events. Safe to
4069
- * call even before a cohortId is assigned.
4071
+ * Drain any silent rejections the state machine recorded for a cohort during
4072
+ * the most recent receive() and surface them as `message-rejected` events.
4070
4073
  */
4071
- #drainRejections() {
4072
- if (!this.#cohortId) return;
4073
- for (const r of this.session.drainRejections(this.#cohortId)) {
4074
- this.emit("message-rejected", { cohortId: this.#cohortId, ...r });
4074
+ #drainRejections(ctx) {
4075
+ for (const r of this.session.drainRejections(ctx.cohortId)) {
4076
+ this.emit("message-rejected", { cohortId: ctx.cohortId, ...r });
4077
+ }
4078
+ }
4079
+ /**
4080
+ * Advertise a new cohort and begin driving it to completion. Callable many
4081
+ * times on one runner; each cohort runs concurrently and independently.
4082
+ *
4083
+ * @param config Per-cohort conditions + network (see {@link CohortConfig}).
4084
+ * @returns The new cohort's id and a `completion` promise that resolves with
4085
+ * that cohort's {@link AggregationResult} (or rejects if it fails/stalls).
4086
+ * @throws If the runner has been stopped, or the config is invalid
4087
+ * (fail-fast via `createCohort`).
4088
+ */
4089
+ advertiseCohort(config) {
4090
+ if (this.#stopped) {
4091
+ throw new AggregationServiceError("Cannot advertise on a stopped runner.", "RUNNER_STOPPED", {});
4092
+ }
4093
+ this.#registerHandlers();
4094
+ const cohortId = this.session.createCohort(config);
4095
+ let resolve;
4096
+ let reject;
4097
+ const completion = new Promise((res, rej) => {
4098
+ resolve = res;
4099
+ reject = rej;
4100
+ });
4101
+ const ctx = {
4102
+ cohortId,
4103
+ config,
4104
+ resolve,
4105
+ reject,
4106
+ completion,
4107
+ finalizing: false,
4108
+ settled: false
4109
+ };
4110
+ this.#contexts.set(cohortId, ctx);
4111
+ try {
4112
+ this.#startTimers(ctx);
4113
+ const advertMsgs = this.session.advertise(cohortId);
4114
+ this.#onPhaseMaybeChanged(ctx);
4115
+ this.emit("cohort-advertised", { cohortId });
4116
+ if (this.#advertRepeatIntervalMs > 0) {
4117
+ this.#startAdvertRepeat(ctx, advertMsgs);
4118
+ } else {
4119
+ this.#sendAll(advertMsgs).catch((err) => this.#failCohort(ctx, err));
4120
+ }
4121
+ } catch (err) {
4122
+ this.#failCohort(ctx, err);
4075
4123
  }
4124
+ return { cohortId, completion };
4076
4125
  }
4077
4126
  /**
4078
- * Run the protocol to completion. Resolves with the final aggregation result
4079
- * (signature + signed transaction) once signing is complete.
4127
+ * Run a single cohort to completion using the `config` supplied in the
4128
+ * runner options. Thin convenience over {@link advertiseCohort} for the
4129
+ * single-cohort case (and the path {@link AggregationRunner.solo} rides).
4080
4130
  *
4081
4131
  * @returns {Promise<AggregationResult>} The final result with signature and signed tx.
4082
4132
  */
4083
4133
  run() {
4084
- return new Promise((resolve, reject) => {
4085
- this.#resolveRun = resolve;
4086
- this.#rejectRun = reject;
4087
- try {
4088
- this.#registerHandlers();
4089
- this.#cohortId = this.session.createCohort(this.#config);
4090
- this.#startTimers();
4091
- const advertMsgs = this.session.advertise(this.#cohortId);
4092
- this.#onPhaseMaybeChanged();
4093
- this.emit("cohort-advertised", { cohortId: this.#cohortId });
4094
- if (this.#advertRepeatIntervalMs > 0) {
4095
- this.#startAdvertRepeat(advertMsgs);
4096
- } else {
4097
- this.#sendAll(advertMsgs).catch((err) => this.#fail(err));
4098
- }
4099
- } catch (err) {
4100
- this.#fail(err);
4134
+ if (!this.#defaultConfig) {
4135
+ return Promise.reject(new AggregationServiceError(
4136
+ "run() requires `config` in the runner options; use advertiseCohort(config) to drive cohorts explicitly.",
4137
+ "MISSING_COHORT_CONFIG",
4138
+ {}
4139
+ ));
4140
+ }
4141
+ try {
4142
+ return this.advertiseCohort(this.#defaultConfig).completion;
4143
+ } catch (err) {
4144
+ return Promise.reject(err);
4145
+ }
4146
+ }
4147
+ /**
4148
+ * Wait for every currently-outstanding cohort to settle and return the
4149
+ * successful results. Dynamic drain: cohorts advertised while this is pending
4150
+ * are included, and it resolves only once no cohorts remain. Failed cohorts
4151
+ * are surfaced via `error` / `cohort-failed` events and their rejected
4152
+ * `completion` promises; they are omitted from the returned array (this
4153
+ * method does not throw). Bound long-running cohorts with `cohortTtlMs` /
4154
+ * `phaseTimeoutMs` or this may never resolve.
4155
+ *
4156
+ * @returns {Promise<AggregationResult[]>} Results of the cohorts that completed.
4157
+ */
4158
+ async runAll() {
4159
+ const collected = /* @__PURE__ */ new Map();
4160
+ const onComplete = (result) => {
4161
+ collected.set(result.cohortId, result);
4162
+ };
4163
+ this.on("signing-complete", onComplete);
4164
+ try {
4165
+ while (this.#contexts.size > 0) {
4166
+ await Promise.allSettled([...this.#contexts.values()].map((c) => c.completion));
4101
4167
  }
4102
- });
4168
+ } finally {
4169
+ this.off("signing-complete", onComplete);
4170
+ }
4171
+ return [...collected.values()];
4103
4172
  }
4104
4173
  /**
4105
- * Begin publishing the cohort advert immediately and on a repeating interval
4106
- * until {@link #stopAdvertRepeating} is called. Each advert is broadcast
4107
- * (no recipient) via the transport's `publishRepeating` primitive.
4174
+ * Begin publishing a cohort's advert immediately and on a repeating interval
4175
+ * until the cohort's advert loop is stopped. Each advert is broadcast (no
4176
+ * recipient) via the transport's `publishRepeating` primitive.
4108
4177
  */
4109
- #startAdvertRepeat(advertMsgs) {
4178
+ #startAdvertRepeat(ctx, advertMsgs) {
4110
4179
  const stops = [];
4111
4180
  for (const msg of advertMsgs) {
4112
4181
  stops.push(this.#transport.publishRepeating(msg, this.#did, this.#advertRepeatIntervalMs));
4113
4182
  }
4114
- this.#stopAdvertRepeat = () => {
4183
+ ctx.stopAdvertRepeat = () => {
4115
4184
  for (const stop of stops) {
4116
4185
  try {
4117
4186
  stop();
@@ -4120,61 +4189,118 @@ var AggregationServiceRunner = class _AggregationServiceRunner extends TypedEven
4120
4189
  }
4121
4190
  };
4122
4191
  }
4123
- /** Stop the advert republish loop. Idempotent. */
4124
- #stopAdvertRepeating() {
4125
- if (!this.#stopAdvertRepeat) return;
4126
- const stop = this.#stopAdvertRepeat;
4127
- this.#stopAdvertRepeat = void 0;
4192
+ /** Stop a cohort's advert republish loop. Idempotent. */
4193
+ #stopAdvertRepeating(ctx) {
4194
+ if (!ctx.stopAdvertRepeat) return;
4195
+ const stop = ctx.stopAdvertRepeat;
4196
+ ctx.stopAdvertRepeat = void 0;
4128
4197
  stop();
4129
4198
  }
4130
- /** Schedule cohort TTL + phase timeout at the start of a run. */
4131
- #startTimers() {
4199
+ /** Schedule a cohort's TTL + phase timeout when it is advertised. */
4200
+ #startTimers(ctx) {
4132
4201
  if (this.#cohortTtlMs !== void 0) {
4133
- this.#cohortTtlTimer = setTimeout(() => {
4134
- const reason = `Cohort ${this.#cohortId ?? ""} exceeded TTL of ${this.#cohortTtlMs}ms`;
4135
- this.emit("cohort-failed", { cohortId: this.#cohortId ?? "", reason });
4136
- this.#fail(new Error(reason));
4202
+ ctx.cohortTtlTimer = setTimeout(() => {
4203
+ const reason = `Cohort ${ctx.cohortId} exceeded TTL of ${this.#cohortTtlMs}ms`;
4204
+ this.emit("cohort-failed", { cohortId: ctx.cohortId, reason });
4205
+ this.#failCohort(ctx, new Error(reason));
4137
4206
  }, this.#cohortTtlMs);
4138
4207
  }
4139
- this.#resetPhaseTimer();
4208
+ this.#resetPhaseTimer(ctx);
4140
4209
  }
4141
- /** Reset the per-phase stall timer. Called when a phase transition is observed. */
4142
- #resetPhaseTimer() {
4143
- if (this.#phaseTimer) clearTimeout(this.#phaseTimer);
4144
- this.#phaseTimer = void 0;
4210
+ /** Reset a cohort's per-phase stall timer. Called when a phase transition is observed. */
4211
+ #resetPhaseTimer(ctx) {
4212
+ if (ctx.phaseTimer) clearTimeout(ctx.phaseTimer);
4213
+ ctx.phaseTimer = void 0;
4145
4214
  if (this.#phaseTimeoutMs === void 0) return;
4146
- this.#phaseTimer = setTimeout(() => {
4147
- const reason = `Cohort ${this.#cohortId ?? ""} stalled in phase ${this.#lastObservedPhase ?? "?"} for ${this.#phaseTimeoutMs}ms`;
4148
- this.emit("cohort-failed", { cohortId: this.#cohortId ?? "", reason });
4149
- this.#fail(new Error(reason));
4215
+ ctx.phaseTimer = setTimeout(() => {
4216
+ const reason = `Cohort ${ctx.cohortId} stalled in phase ${ctx.lastObservedPhase ?? "?"} for ${this.#phaseTimeoutMs}ms`;
4217
+ this.emit("cohort-failed", { cohortId: ctx.cohortId, reason });
4218
+ this.#failCohort(ctx, new Error(reason));
4150
4219
  }, this.#phaseTimeoutMs);
4151
4220
  }
4152
- /** Detect a phase change since the last observation and reset the phase timer. */
4153
- #onPhaseMaybeChanged() {
4154
- if (!this.#cohortId) return;
4155
- const phase = this.session.getCohortPhase(this.#cohortId);
4156
- if (phase !== this.#lastObservedPhase) {
4157
- this.#lastObservedPhase = phase;
4158
- this.#resetPhaseTimer();
4159
- }
4221
+ /** Detect a phase change for a cohort since the last observation and reset its phase timer. */
4222
+ #onPhaseMaybeChanged(ctx) {
4223
+ const phase = this.session.getCohortPhase(ctx.cohortId);
4224
+ if (phase !== ctx.lastObservedPhase) {
4225
+ ctx.lastObservedPhase = phase;
4226
+ this.#resetPhaseTimer(ctx);
4227
+ }
4228
+ }
4229
+ /** Clear a cohort's timers. Called on completion, stop, and failure. */
4230
+ #clearTimers(ctx) {
4231
+ if (ctx.cohortTtlTimer) clearTimeout(ctx.cohortTtlTimer);
4232
+ if (ctx.phaseTimer) clearTimeout(ctx.phaseTimer);
4233
+ ctx.cohortTtlTimer = void 0;
4234
+ ctx.phaseTimer = void 0;
4235
+ }
4236
+ /**
4237
+ * Reclaim one cohort's runner-layer bookkeeping: stop its advert loop, clear
4238
+ * its timers, and drop its {@link RunContext}. Does NOT touch sibling cohorts
4239
+ * and does NOT detach the shared transport handlers. Leaves the cohort in the
4240
+ * state machine; whether that cohort's `session` state is also removed is the
4241
+ * caller's choice (see {@link #completeCohort} vs {@link #failCohort}).
4242
+ */
4243
+ #disposeCohort(ctx) {
4244
+ this.#stopAdvertRepeating(ctx);
4245
+ this.#clearTimers(ctx);
4246
+ this.#contexts.delete(ctx.cohortId);
4247
+ }
4248
+ /**
4249
+ * Settle one cohort successfully. Reclaims the runner context but leaves the
4250
+ * completed cohort in `session` so callers can read its beaconAddress / cohort
4251
+ * via `session.getCohort(result.cohortId)`; reclaim it with
4252
+ * `session.removeCohort(cohortId)` when done. Idempotent via `ctx.settled`.
4253
+ */
4254
+ #completeCohort(ctx, result) {
4255
+ if (ctx.settled) return;
4256
+ ctx.settled = true;
4257
+ this.#disposeCohort(ctx);
4258
+ this.emit("signing-complete", result);
4259
+ ctx.resolve(result);
4260
+ }
4261
+ /**
4262
+ * Fail one cohort. Reclaims its runner context, drops its now-dead state from
4263
+ * the state machine, and rejects only its completion; siblings keep running
4264
+ * and the shared transport handlers stay registered. Idempotent via
4265
+ * `ctx.settled`.
4266
+ */
4267
+ #failCohort(ctx, err) {
4268
+ if (ctx.settled) return;
4269
+ ctx.settled = true;
4270
+ this.#disposeCohort(ctx);
4271
+ this.session.removeCohort(ctx.cohortId);
4272
+ this.emit("error", err);
4273
+ ctx.reject(err);
4160
4274
  }
4161
- /** Clear both timers. Called on successful completion, stop(), and #fail. */
4162
- #clearTimers() {
4163
- if (this.#cohortTtlTimer) clearTimeout(this.#cohortTtlTimer);
4164
- if (this.#phaseTimer) clearTimeout(this.#phaseTimer);
4165
- this.#cohortTtlTimer = void 0;
4166
- this.#phaseTimer = void 0;
4275
+ /**
4276
+ * Stop a single cohort early without affecting the rest of the runner. Drops
4277
+ * the cohort's state machine state; its `completion` promise rejects with a
4278
+ * stopped error.
4279
+ */
4280
+ stopCohort(cohortId) {
4281
+ const ctx = this.#contexts.get(cohortId);
4282
+ if (!ctx || ctx.settled) return;
4283
+ ctx.settled = true;
4284
+ this.#disposeCohort(ctx);
4285
+ this.session.removeCohort(cohortId);
4286
+ ctx.reject(new AggregationServiceError(`Cohort ${cohortId} stopped.`, "COHORT_STOPPED", { cohortId }));
4167
4287
  }
4168
4288
  /**
4169
- * Stop the runner early. Marks the runner stopped and detaches transport
4170
- * handlers so a restart or a new runner doesn't inherit stale dispatch.
4289
+ * Stop the whole runner. Fails every outstanding cohort, then detaches the
4290
+ * shared transport handlers so a restart or a new runner doesn't inherit
4291
+ * stale dispatch. Safe to call repeatedly.
4171
4292
  */
4172
4293
  stop() {
4173
4294
  this.#stopped = true;
4174
- this.#stopAdvertRepeating();
4175
- this.#clearTimers();
4295
+ for (const ctx of [...this.#contexts.values()]) {
4296
+ if (ctx.settled) continue;
4297
+ ctx.settled = true;
4298
+ this.#disposeCohort(ctx);
4299
+ this.session.removeCohort(ctx.cohortId);
4300
+ ctx.reject(new AggregationServiceError("Service runner stopped.", "RUNNER_STOPPED", { cohortId: ctx.cohortId }));
4301
+ }
4302
+ this.#contexts.clear();
4176
4303
  this.#unregisterHandlers();
4177
- if (this.#cohortId) this.session.removeCohort(this.#cohortId);
4178
4304
  }
4179
4305
  /** Message types this runner listens for on the transport. */
4180
4306
  static #HANDLED_MESSAGE_TYPES = [
@@ -4185,7 +4311,10 @@ var AggregationServiceRunner = class _AggregationServiceRunner extends TypedEven
4185
4311
  SIGNATURE_AUTHORIZATION
4186
4312
  ];
4187
4313
  /**
4188
- * Internal: handler registration with the transport. Idempotent.
4314
+ * Internal: handler registration with the transport. Idempotent. Handlers
4315
+ * are DID-scoped and cohort-agnostic — one registration serves every cohort
4316
+ * this runner drives; demux to the right {@link RunContext} happens in each
4317
+ * handler via the inbound message's cohortId.
4189
4318
  */
4190
4319
  #registerHandlers() {
4191
4320
  if (this.#handlersRegistered) return;
@@ -4206,23 +4335,25 @@ var AggregationServiceRunner = class _AggregationServiceRunner extends TypedEven
4206
4335
  }
4207
4336
  /**
4208
4337
  * Internal: message handlers for each protocol step. Each handler:
4209
- * 1) feeds the message into the state machine via session.receive()
4210
- * 2) emits a high-level event for external observers
4211
- * 3) checks if the new state triggers any automatic next steps, and if so:
4338
+ * 1) resolves the cohort the message belongs to (by cohortId); ignores it if unknown
4339
+ * 2) feeds the message into the state machine via session.receive()
4340
+ * 3) emits a high-level event (carrying cohortId) for external observers
4341
+ * 4) checks if the new state triggers any automatic next steps, and if so:
4212
4342
  * a) calls the appropriate decision callback(s)
4213
4343
  * b) sends any resulting messages from the state machine
4344
+ * Errors fail only the owning cohort. A stopped runner ignores messages.
4214
4345
  * @param {BaseMessage} msg - The incoming message to handle.
4215
4346
  * @returns {Promise<void>} Resolves when handling is complete.
4216
- * @throws {Error} If any step of handling fails, the error is emitted and the run promise is rejected.
4217
- * Note: if the runner has been stopped, handlers will ignore incoming messages.
4218
4347
  */
4219
4348
  async #handleOptIn(msg) {
4220
4349
  if (this.#stopped) return;
4350
+ const ctx = this.#contextFor(msg);
4351
+ if (!ctx) return;
4221
4352
  try {
4222
4353
  this.session.receive(msg);
4223
- this.#drainRejections();
4224
- this.#onPhaseMaybeChanged();
4225
- const optIn = this.session.pendingOptIns(this.#cohortId).get(msg.from);
4354
+ this.#drainRejections(ctx);
4355
+ this.#onPhaseMaybeChanged(ctx);
4356
+ const optIn = this.session.pendingOptIns(ctx.cohortId).get(msg.from);
4226
4357
  if (!optIn) return;
4227
4358
  this.emit("opt-in-received", optIn);
4228
4359
  if (optIn.communicationPk) {
@@ -4230,34 +4361,34 @@ var AggregationServiceRunner = class _AggregationServiceRunner extends TypedEven
4230
4361
  }
4231
4362
  const decision = await this.#onOptInReceived(optIn);
4232
4363
  if (!decision.accepted) return;
4233
- const maxParticipants = this.#config.maxParticipants;
4234
- const cohortNow = this.session.getCohort(this.#cohortId);
4364
+ const maxParticipants = ctx.config.maxParticipants;
4365
+ const cohortNow = this.session.getCohort(ctx.cohortId);
4235
4366
  if (maxParticipants !== void 0 && cohortNow && cohortNow.participants.length >= maxParticipants) {
4236
4367
  return;
4237
4368
  }
4238
- await this.#sendAll(this.session.acceptParticipant(this.#cohortId, msg.from));
4239
- this.emit("participant-accepted", { participantDid: msg.from });
4240
- const cohort = this.session.getCohort(this.#cohortId);
4241
- if (cohort.participants.length >= this.#config.minParticipants && !this.#finalizing) {
4242
- this.#finalizing = true;
4369
+ await this.#sendAll(this.session.acceptParticipant(ctx.cohortId, msg.from));
4370
+ this.emit("participant-accepted", { cohortId: ctx.cohortId, participantDid: msg.from });
4371
+ const cohort = this.session.getCohort(ctx.cohortId);
4372
+ if (cohort.participants.length >= ctx.config.minParticipants && !ctx.finalizing) {
4373
+ ctx.finalizing = true;
4243
4374
  const finalizeDecision = await this.#onReadyToFinalize({
4244
4375
  acceptedCount: cohort.participants.length,
4245
- minRequired: this.#config.minParticipants
4376
+ minRequired: ctx.config.minParticipants
4246
4377
  });
4247
4378
  if (!finalizeDecision.finalize) {
4248
- this.#finalizing = false;
4379
+ ctx.finalizing = false;
4249
4380
  return;
4250
4381
  }
4251
- const readyMsgs = this.session.finalizeKeygen(this.#cohortId);
4252
- this.#stopAdvertRepeating();
4382
+ const readyMsgs = this.session.finalizeKeygen(ctx.cohortId);
4383
+ this.#stopAdvertRepeating(ctx);
4253
4384
  this.emit("keygen-complete", {
4254
- cohortId: this.#cohortId,
4385
+ cohortId: ctx.cohortId,
4255
4386
  beaconAddress: cohort.beaconAddress
4256
4387
  });
4257
4388
  await this.#sendAll(readyMsgs);
4258
4389
  }
4259
4390
  } catch (err) {
4260
- this.#fail(err);
4391
+ this.#failCohort(ctx, err);
4261
4392
  }
4262
4393
  }
4263
4394
  /**
@@ -4265,23 +4396,23 @@ var AggregationServiceRunner = class _AggregationServiceRunner extends TypedEven
4265
4396
  * and distributes the data for validation.
4266
4397
  * @param {BaseMessage} msg - The incoming message to handle.
4267
4398
  * @returns {Promise<void>} Resolves when handling is complete.
4268
- * @throws {Error} If any step of handling fails, the error is emitted and the run promise is rejected.
4269
- * Note: if the runner has been stopped, handlers will ignore incoming messages.
4270
4399
  */
4271
4400
  async #handleSubmitUpdate(msg) {
4272
4401
  if (this.#stopped) return;
4402
+ const ctx = this.#contextFor(msg);
4403
+ if (!ctx) return;
4273
4404
  try {
4274
4405
  this.session.receive(msg);
4275
- this.#drainRejections();
4276
- this.#onPhaseMaybeChanged();
4277
- this.emit("update-received", { participantDid: msg.from });
4278
- if (this.session.getCohortPhase(this.#cohortId) === "UpdatesCollected" /* UpdatesCollected */) {
4279
- const distributeMsgs = this.session.buildAndDistribute(this.#cohortId);
4280
- this.emit("data-distributed", { cohortId: this.#cohortId });
4406
+ this.#drainRejections(ctx);
4407
+ this.#onPhaseMaybeChanged(ctx);
4408
+ this.emit("update-received", { cohortId: ctx.cohortId, participantDid: msg.from });
4409
+ if (this.session.getCohortPhase(ctx.cohortId) === "UpdatesCollected" /* UpdatesCollected */) {
4410
+ const distributeMsgs = this.session.buildAndDistribute(ctx.cohortId);
4411
+ this.emit("data-distributed", { cohortId: ctx.cohortId });
4281
4412
  await this.#sendAll(distributeMsgs);
4282
4413
  }
4283
4414
  } catch (err) {
4284
- this.#fail(err);
4415
+ this.#failCohort(ctx, err);
4285
4416
  }
4286
4417
  }
4287
4418
  /**
@@ -4289,111 +4420,95 @@ var AggregationServiceRunner = class _AggregationServiceRunner extends TypedEven
4289
4420
  * automatically requests tx data and starts signing.
4290
4421
  * @param {BaseMessage} msg - The incoming message to handle.
4291
4422
  * @returns {Promise<void>} Resolves when handling is complete.
4292
- * @throws {Error} If any step of handling fails, the error is emitted and the run promise is rejected.
4293
- * Note: if the runner has been stopped, handlers will ignore incoming messages.
4294
4423
  */
4295
4424
  async #handleValidationAck(msg) {
4296
4425
  if (this.#stopped) return;
4426
+ const ctx = this.#contextFor(msg);
4427
+ if (!ctx) return;
4297
4428
  try {
4298
4429
  this.session.receive(msg);
4299
- this.#drainRejections();
4300
- this.#onPhaseMaybeChanged();
4430
+ this.#drainRejections(ctx);
4431
+ this.#onPhaseMaybeChanged(ctx);
4301
4432
  const approved = !!msg.body?.approved;
4302
- this.emit("validation-received", { participantDid: msg.from, approved });
4303
- const phase = this.session.getCohortPhase(this.#cohortId);
4433
+ this.emit("validation-received", { cohortId: ctx.cohortId, participantDid: msg.from, approved });
4434
+ const phase = this.session.getCohortPhase(ctx.cohortId);
4304
4435
  if (phase === "Failed" /* Failed */) {
4305
4436
  const reason = `Validation rejected by participant ${msg.from}`;
4306
- this.emit("cohort-failed", { cohortId: this.#cohortId, reason });
4307
- this.#fail(new Error(reason));
4437
+ this.emit("cohort-failed", { cohortId: ctx.cohortId, reason });
4438
+ this.#failCohort(ctx, new Error(reason));
4308
4439
  return;
4309
4440
  }
4310
4441
  if (phase === "Validated" /* Validated */) {
4311
- const cohort = this.session.getCohort(this.#cohortId);
4442
+ const cohort = this.session.getCohort(ctx.cohortId);
4312
4443
  const txData = await this.#onProvideTxData({
4313
- cohortId: this.#cohortId,
4444
+ cohortId: ctx.cohortId,
4314
4445
  beaconAddress: cohort.beaconAddress,
4315
4446
  signalBytes: cohort.signalBytes
4316
4447
  });
4317
- const authMsgs = this.session.startSigning(this.#cohortId, txData);
4318
- const sessionId = this.session.getSigningSessionId(this.#cohortId) ?? "";
4319
- this.emit("signing-started", { sessionId });
4448
+ const authMsgs = this.session.startSigning(ctx.cohortId, txData);
4449
+ const sessionId = this.session.getSigningSessionId(ctx.cohortId) ?? "";
4450
+ this.emit("signing-started", { cohortId: ctx.cohortId, sessionId });
4320
4451
  await this.#sendAll(authMsgs);
4321
4452
  }
4322
4453
  } catch (err) {
4323
- this.#fail(err);
4454
+ this.#failCohort(ctx, err);
4324
4455
  }
4325
4456
  }
4326
4457
  /**
4327
- * Handler for receiving nonce contributions and signature authorizations. When all nonces or
4328
- * signatures are received,
4458
+ * Handler for receiving nonce contributions. When all nonces are received, sends the aggregated
4459
+ * nonce back to the cohort.
4329
4460
  * @param {BaseMessage} msg - The incoming message to handle.
4330
4461
  * @returns {Promise<void>} Resolves when handling is complete.
4331
- * @throws {Error} If any step of handling fails, the error is emitted and the run promise is rejected.
4332
- * Note: if the runner has been stopped, handlers will ignore incoming messages.
4333
4462
  */
4334
4463
  async #handleNonceContribution(msg) {
4335
4464
  if (this.#stopped) return;
4465
+ const ctx = this.#contextFor(msg);
4466
+ if (!ctx) return;
4336
4467
  try {
4337
4468
  this.session.receive(msg);
4338
- this.#drainRejections();
4339
- this.#onPhaseMaybeChanged();
4340
- this.emit("nonce-received", { participantDid: msg.from });
4341
- if (this.session.getCohortPhase(this.#cohortId) === "NoncesCollected" /* NoncesCollected */) {
4342
- await this.#sendAll(this.session.sendAggregatedNonce(this.#cohortId));
4469
+ this.#drainRejections(ctx);
4470
+ this.#onPhaseMaybeChanged(ctx);
4471
+ this.emit("nonce-received", { cohortId: ctx.cohortId, participantDid: msg.from });
4472
+ if (this.session.getCohortPhase(ctx.cohortId) === "NoncesCollected" /* NoncesCollected */) {
4473
+ await this.#sendAll(this.session.sendAggregatedNonce(ctx.cohortId));
4343
4474
  }
4344
4475
  } catch (err) {
4345
- this.#fail(err);
4476
+ this.#failCohort(ctx, err);
4346
4477
  }
4347
4478
  }
4348
4479
  /**
4349
4480
  * Handler for receiving signature authorizations. When all partial signatures are received, the
4350
- * session automatically completes and the final result is emitted and the run() promise is resolved.
4481
+ * session automatically completes; the final result is emitted and the cohort's completion
4482
+ * promise resolves.
4351
4483
  * @param {BaseMessage} msg - The incoming message to handle.
4352
4484
  * @returns {Promise<void>} Resolves when handling is complete.
4353
- * @throws {Error} If any step of handling fails, the error is emitted and the run promise is rejected.
4354
- * Note: if the runner has been stopped, handlers will ignore incoming messages.
4355
4485
  */
4356
4486
  async #handleSignatureAuthorization(msg) {
4357
4487
  if (this.#stopped) return;
4488
+ const ctx = this.#contextFor(msg);
4489
+ if (!ctx) return;
4358
4490
  try {
4359
4491
  this.session.receive(msg);
4360
- this.#drainRejections();
4361
- this.#onPhaseMaybeChanged();
4362
- const result = this.session.getResult(this.#cohortId);
4492
+ this.#drainRejections(ctx);
4493
+ this.#onPhaseMaybeChanged(ctx);
4494
+ const result = this.session.getResult(ctx.cohortId);
4363
4495
  if (result) {
4364
- this.#clearTimers();
4365
- this.#unregisterHandlers();
4366
- this.emit("signing-complete", result);
4367
- this.#resolveRun?.(result);
4496
+ this.#completeCohort(ctx, result);
4368
4497
  }
4369
4498
  } catch (err) {
4370
- this.#fail(err);
4499
+ this.#failCohort(ctx, err);
4371
4500
  }
4372
4501
  }
4373
4502
  /**
4374
4503
  * Internal: helper to send all messages sequentially. Catches and propagates errors.
4375
4504
  * @param {BaseMessage[]} msgs - The messages to send.
4376
4505
  * @returns {Promise<void>} Resolves when all messages have been sent.
4377
- * @throws {Error} If sending any message fails, the error is emitted and the run promise is
4378
- * rejected.
4379
4506
  */
4380
4507
  async #sendAll(msgs) {
4381
4508
  for (const m of msgs) {
4382
4509
  await this.#transport.sendMessage(m, this.#did, m.to);
4383
4510
  }
4384
4511
  }
4385
- /**
4386
- * Internal: helper to handle errors. Emits an 'error' event and rejects the run promise.
4387
- * @param {Error} err - The error to handle.
4388
- */
4389
- #fail(err) {
4390
- this.#stopAdvertRepeating();
4391
- this.#clearTimers();
4392
- this.#unregisterHandlers();
4393
- if (this.#cohortId) this.session.removeCohort(this.#cohortId);
4394
- this.emit("error", err);
4395
- this.#rejectRun?.(err);
4396
- }
4397
4512
  };
4398
4513
 
4399
4514
  // src/core/aggregation/runner/participant-runner.ts
@@ -4453,7 +4568,8 @@ var AggregationParticipantRunner = class _AggregationParticipantRunner extends T
4453
4568
  }
4454
4569
  /**
4455
4570
  * Single-shot helper: start, join the first cohort that passes `shouldJoin`,
4456
- * drive it to completion, and resolve. Convenient for tests and demos.
4571
+ * drive it to completion, and resolve. Convenient for tests and demos. The
4572
+ * single-cohort special case of {@link joinMatching} (count = 1).
4457
4573
  */
4458
4574
  static async joinFirst(options) {
4459
4575
  return new Promise((resolve, reject) => {
@@ -4466,6 +4582,37 @@ var AggregationParticipantRunner = class _AggregationParticipantRunner extends T
4466
4582
  runner.start().catch(reject);
4467
4583
  });
4468
4584
  }
4585
+ /**
4586
+ * Multi-cohort helper: start, join EVERY cohort whose advert passes
4587
+ * `shouldJoin`, drive each to completion in parallel, and resolve once
4588
+ * `count` cohorts have completed (the runner stops at that point). The
4589
+ * N-cohort generalization of {@link joinFirst}, for a participant that joins
4590
+ * several cohorts advertised by one service.
4591
+ *
4592
+ * For an open-ended, long-lived subscriber (no fixed count), construct an
4593
+ * {@link AggregationParticipantRunner} directly, set `shouldJoin`, call
4594
+ * `start()`, and listen for `cohort-complete` — the runner already drives
4595
+ * any number of cohorts concurrently.
4596
+ *
4597
+ * @param options Participant runner options (set `shouldJoin` to select cohorts).
4598
+ * @param count Number of completed cohorts to collect before resolving.
4599
+ * @returns The {@link CohortCompleteInfo} for each completed cohort, in completion order.
4600
+ */
4601
+ static async joinMatching(options, count) {
4602
+ return new Promise((resolve, reject) => {
4603
+ const runner = new _AggregationParticipantRunner(options);
4604
+ const completed = [];
4605
+ runner.on("cohort-complete", (info) => {
4606
+ completed.push(info);
4607
+ if (completed.length >= count) {
4608
+ runner.stop();
4609
+ resolve(completed);
4610
+ }
4611
+ });
4612
+ runner.on("error", reject);
4613
+ runner.start().catch(reject);
4614
+ });
4615
+ }
4469
4616
  /**
4470
4617
  * Internal: handler registration with the transport. Idempotent and safe to call multiple times,
4471
4618
  * but only registers handlers once.
@@ -4614,7 +4761,7 @@ var AggregationParticipantRunner = class _AggregationParticipantRunner extends T
4614
4761
  if (this.session.getCohortPhase(cohortId) === "Complete" /* Complete */) {
4615
4762
  const info = this.session.joinedCohorts.get(cohortId);
4616
4763
  if (info) {
4617
- const validation = this.session.pendingValidations.get(cohortId);
4764
+ const validation = this.session.getValidation(cohortId);
4618
4765
  this.emit("cohort-complete", {
4619
4766
  cohortId,
4620
4767
  beaconAddress: info.beaconAddress,