@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/.tsbuildinfo +1 -1
- package/dist/browser.js +316 -169
- package/dist/browser.mjs +316 -169
- package/dist/cjs/index.js +317 -170
- package/dist/esm/core/aggregation/participant.js +11 -0
- package/dist/esm/core/aggregation/participant.js.map +1 -1
- package/dist/esm/core/aggregation/runner/participant-runner.js +37 -2
- package/dist/esm/core/aggregation/runner/participant-runner.js.map +1 -1
- package/dist/esm/core/aggregation/runner/service-runner.js +318 -192
- package/dist/esm/core/aggregation/runner/service-runner.js.map +1 -1
- package/dist/types/core/aggregation/participant.d.ts +9 -0
- package/dist/types/core/aggregation/participant.d.ts.map +1 -1
- package/dist/types/core/aggregation/runner/events.d.ts +22 -11
- package/dist/types/core/aggregation/runner/events.d.ts.map +1 -1
- package/dist/types/core/aggregation/runner/participant-runner.d.ts +21 -12
- package/dist/types/core/aggregation/runner/participant-runner.d.ts.map +1 -1
- package/dist/types/core/aggregation/runner/service-runner.d.ts +76 -22
- package/dist/types/core/aggregation/runner/service-runner.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/core/aggregation/participant.ts +12 -0
- package/src/core/aggregation/runner/events.ts +23 -14
- package/src/core/aggregation/runner/participant-runner.ts +43 -13
- package/src/core/aggregation/runner/service-runner.ts +370 -199
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
|
-
#
|
|
4031
|
+
#defaultConfig;
|
|
4021
4032
|
#onOptInReceived;
|
|
4022
4033
|
#onReadyToFinalize;
|
|
4023
4034
|
#onProvideTxData;
|
|
4024
4035
|
#cohortTtlMs;
|
|
4025
4036
|
#phaseTimeoutMs;
|
|
4026
4037
|
#advertRepeatIntervalMs;
|
|
4027
|
-
|
|
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.#
|
|
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
|
|
4068
|
-
* recent receive() and surface them as `message-rejected` events.
|
|
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
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
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
|
|
4079
|
-
*
|
|
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
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
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
|
|
4106
|
-
* until
|
|
4107
|
-
*
|
|
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
|
-
|
|
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
|
|
4124
|
-
#stopAdvertRepeating() {
|
|
4125
|
-
if (!
|
|
4126
|
-
const stop =
|
|
4127
|
-
|
|
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
|
|
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
|
-
|
|
4134
|
-
const reason = `Cohort ${
|
|
4135
|
-
this.emit("cohort-failed", { cohortId:
|
|
4136
|
-
this.#
|
|
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
|
|
4142
|
-
#resetPhaseTimer() {
|
|
4143
|
-
if (
|
|
4144
|
-
|
|
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
|
-
|
|
4147
|
-
const reason = `Cohort ${
|
|
4148
|
-
this.emit("cohort-failed", { cohortId:
|
|
4149
|
-
this.#
|
|
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
|
|
4153
|
-
#onPhaseMaybeChanged() {
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
this.#
|
|
4158
|
-
|
|
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
|
-
/**
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
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
|
|
4170
|
-
* handlers so a restart or a new runner doesn't inherit
|
|
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.#
|
|
4175
|
-
|
|
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)
|
|
4210
|
-
* 2)
|
|
4211
|
-
* 3)
|
|
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(
|
|
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 =
|
|
4234
|
-
const cohortNow = this.session.getCohort(
|
|
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(
|
|
4239
|
-
this.emit("participant-accepted", { participantDid: msg.from });
|
|
4240
|
-
const cohort = this.session.getCohort(
|
|
4241
|
-
if (cohort.participants.length >=
|
|
4242
|
-
|
|
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:
|
|
4376
|
+
minRequired: ctx.config.minParticipants
|
|
4246
4377
|
});
|
|
4247
4378
|
if (!finalizeDecision.finalize) {
|
|
4248
|
-
|
|
4379
|
+
ctx.finalizing = false;
|
|
4249
4380
|
return;
|
|
4250
4381
|
}
|
|
4251
|
-
const readyMsgs = this.session.finalizeKeygen(
|
|
4252
|
-
this.#stopAdvertRepeating();
|
|
4382
|
+
const readyMsgs = this.session.finalizeKeygen(ctx.cohortId);
|
|
4383
|
+
this.#stopAdvertRepeating(ctx);
|
|
4253
4384
|
this.emit("keygen-complete", {
|
|
4254
|
-
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.#
|
|
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(
|
|
4279
|
-
const distributeMsgs = this.session.buildAndDistribute(
|
|
4280
|
-
this.emit("data-distributed", { 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.#
|
|
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(
|
|
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:
|
|
4307
|
-
this.#
|
|
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(
|
|
4442
|
+
const cohort = this.session.getCohort(ctx.cohortId);
|
|
4312
4443
|
const txData = await this.#onProvideTxData({
|
|
4313
|
-
cohortId:
|
|
4444
|
+
cohortId: ctx.cohortId,
|
|
4314
4445
|
beaconAddress: cohort.beaconAddress,
|
|
4315
4446
|
signalBytes: cohort.signalBytes
|
|
4316
4447
|
});
|
|
4317
|
-
const authMsgs = this.session.startSigning(
|
|
4318
|
-
const sessionId = this.session.getSigningSessionId(
|
|
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.#
|
|
4454
|
+
this.#failCohort(ctx, err);
|
|
4324
4455
|
}
|
|
4325
4456
|
}
|
|
4326
4457
|
/**
|
|
4327
|
-
* Handler for receiving nonce contributions
|
|
4328
|
-
*
|
|
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(
|
|
4342
|
-
await this.#sendAll(this.session.sendAggregatedNonce(
|
|
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.#
|
|
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
|
|
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(
|
|
4492
|
+
this.#drainRejections(ctx);
|
|
4493
|
+
this.#onPhaseMaybeChanged(ctx);
|
|
4494
|
+
const result = this.session.getResult(ctx.cohortId);
|
|
4363
4495
|
if (result) {
|
|
4364
|
-
this.#
|
|
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.#
|
|
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.
|
|
4764
|
+
const validation = this.session.getValidation(cohortId);
|
|
4618
4765
|
this.emit("cohort-complete", {
|
|
4619
4766
|
cohortId,
|
|
4620
4767
|
beaconAddress: info.beaconAddress,
|