@observertc/observer-js 1.0.0-beta.15 → 1.0.0-beta.16

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/README.md CHANGED
@@ -497,9 +497,15 @@ Key members:
497
497
  - `getOrCreateObservedCall<T>(settings): ObservedCall<T> | undefined`
498
498
  - `update(): void` — force an aggregation/`observer-updated` tick
499
499
  - `addObserverDetector(name, config?): this` — build a cross-call detector onto `observer.detectors`
500
- - `addCallDetector(name, config?): this` / `removeCallDetector(name): this` — register a call-scoped
501
- detector for every call created **from now on**
500
+ - `addCallDetector(name, config?): this` — register a call-scoped detector for every call created
501
+ from now on
502
+ - `removeCallDetector(name, { includeOpenCalls? }): number` — stop building it, and (by default) drop
503
+ it from calls already open. Returns how many live instances were removed
504
+ - `removeObserverDetector(name): number` — remove an observer-scoped detector. For one specific
505
+ instance use `observer.detectors.remove(detector)`
502
506
  - `addValidator(name, config?): this` — start a one-shot structural check
507
+ - `cancelValidator(name | validator, reason?): number` — stop a running check; it finishes
508
+ `inconclusive` with the reason and emits `validation-ready`
503
509
  - `close(): void`
504
510
  - `readonly detectors: Detectors` — observer-scoped registry. **Starts empty**; nothing is implicit
505
511
  - `readonly callDetectorConfigs: Map<name, config>` — what `addCallDetector` recorded
@@ -532,6 +538,8 @@ Key members:
532
538
  - `getObservedClient<T>(clientId)`, `createObservedClient<T>(settings)`, `getOrCreateObservedClient<T>(settings)` (all `… | undefined`)
533
539
  - `addIssue(issue: ObserverIssue): void` — raise a **call-level** issue → emits `call-issue`
534
540
  - `addDetector(name, config?): this` — build a call-scoped detector onto this call only
541
+ - `removeDetector(name): number` — remove it from this call, `close()`ing it. For one specific
542
+ instance use `call.detectors.remove(detector)`
535
543
  - `readonly detectors: Detectors` — server-side detector registry (empty by default; see [Detectors](#detectors-server-side-extension-point))
536
544
  - `readonly activeIssuesRegistry: ActiveIssuesRegistry` — this call's open client issues, propagating into the observer's
537
545
  - `readonly unconsumedOutboundTracks: Set<ObservedOutboundTrack>` — maintained by the resolver
@@ -959,12 +967,63 @@ observer.addObserverDetector('turn-server-outage-detector', { minClientsAtPeak:
959
967
  observer.addCallDetector('call-concurrent-issue-detector', {
960
968
  issueTypes: [ 'congestion', 'ice-disconnected' ],
961
969
  });
962
- observer.removeCallDetector('unconsumed-track-detector'); // undo, for future calls
963
-
964
970
  // one specific call
965
971
  observedCall.addDetector('issue-fan-out-detector', { issueTypes: [ 'freezed-video-track' ] });
966
972
  ```
967
973
 
974
+ Every `add*` is **chainable** — it returns the owning entity:
975
+
976
+ ```ts
977
+ observer
978
+ .addObserverDetector('turn-server-health-detector')
979
+ .addObserverDetector('turn-server-outage-detector', { minClientsAtPeak: 10 })
980
+ .addValidator('remote-track-resolver');
981
+ ```
982
+
983
+ #### Removing them
984
+
985
+ By **name**, on the entity — which removes *every* instance under that name:
986
+
987
+ ```ts
988
+ observer.removeObserverDetector('turn-server-outage-detector'); // → 1
989
+ observer.removeCallDetector('call-concurrent-issue-detector'); // stops it everywhere
990
+ observedCall.removeDetector('issue-fan-out-detector'); // this call only
991
+ ```
992
+
993
+ By **instance**, through the registry — which is where instances live, since `add*` returns the
994
+ entity rather than the detector:
995
+
996
+ ```ts
997
+ observer
998
+ .addObserverDetector('client-population-issue-detector', { issueTypes: [ 'cpulimitation' ], groupBy: 'browser' })
999
+ .addObserverDetector('client-population-issue-detector', { issueTypes: [ 'cpulimitation' ], groupBy: 'operationSystem' });
1000
+
1001
+ const [ byBrowser, byOs ] = observer.detectors.getAll('client-population-issue-detector');
1002
+
1003
+ observer.detectors.remove(byOs); // keeps the browser axis running
1004
+ ```
1005
+
1006
+ `Detectors` is a small collection: `instances` (a copy, in registration order), `listOfNames`,
1007
+ `size`, `get(name)`, `getAll(name)`, `has(name)`, `add(detector)`, `remove(detector)`,
1008
+ `removeByName(name)`, `clear()`, and it is iterable — `for (const detector of call.detectors)`.
1009
+ `instances` being a copy is deliberate: removing while iterating the live array would skip entries,
1010
+ and "drop the ones that look like X" is the most natural thing to want to write.
1011
+
1012
+ Two things worth knowing:
1013
+
1014
+ - **By name removes every instance under it**, not the first. A name can legitimately be registered
1015
+ more than once — `ClientPopulationIssueDetector` is meant to be added once per `groupBy` axis — and
1016
+ "remove whichever is first in the array" is not something a caller can predict from a name. Go via
1017
+ `detectors.getAll(name)` + `detectors.remove(instance)` when you mean one of them.
1018
+ - **`removeCallDetector` affects calls already open, by default.** Otherwise whether a detector runs
1019
+ would depend on when a call happened to join, which is not a state anyone can reason about. Pass
1020
+ `{ includeOpenCalls: false }` to change only what future calls are built with.
1021
+
1022
+ Every removal path calls the detector's `close()`, so it unsubscribes from the issue registry and
1023
+ drops any timers or bus listeners. A detector removed without closing would keep being fed matching
1024
+ issues for the life of the call — invisible, unbounded, and it would still look healthy if you
1025
+ inspected it.
1026
+
968
1027
  Detectors are named by their kebab-case `NAME`, and the name types the config — an unknown name or a
969
1028
  key that belongs to a different detector will not compile. Each detector owns its defaults in its own
970
1029
  constructor, beside the doc explaining what each threshold means; there is no central table to keep
@@ -1093,6 +1152,23 @@ onDeploy(() => observer.addValidator('simulcast-receivers')); // check again
1093
1152
  finishing. There is no revalidation timer: a deploy, not elapsed time, is what makes a structural
1094
1153
  verdict stale, so re-checking means starting another.
1095
1154
 
1155
+ **Cancelling.** A check that has not decided can be stopped, by name or by instance:
1156
+
1157
+ ```ts
1158
+ observer.cancelValidator('simulcast-receivers', 'sfu redeployed');
1159
+
1160
+ // or one specific instance — `observer.validators` holds what is running
1161
+ for (const validator of observer.validators) observer.cancelValidator(validator, 'shutting down');
1162
+ ```
1163
+
1164
+ Cancelling is **not** silent discarding. The validator finishes `inconclusive` with your reason,
1165
+ emits `validation-ready` like any other completion, and removes itself. That matters twice over:
1166
+ anything waiting on the verdict would otherwise wait forever, and *"we stopped asking"* is a
1167
+ materially different outcome from *"we asked and learned nothing"* — which is exactly what an
1168
+ `inconclusive` carrying a reason records. Pass a real reason; the default tells the reader nothing
1169
+ they could not already infer. `observer.close()` cancels whatever is still running with
1170
+ `'observer closed'`.
1171
+
1096
1172
  | Validator | `addValidator` name | Question | Also raises |
1097
1173
  |-----------|---------------------|----------|-------------|
1098
1174
  | `SimulcastReceiverValidator` 🔗 | `simulcast-receivers` | Does the SFU pick layers per receiver, or drag the publisher down to the worst one? | `WORST_RECEIVER_CONTAGION` |
package/dist/index.d.mts CHANGED
@@ -2468,8 +2468,13 @@ interface Validator<S extends Record<string, unknown> = Record<string, unknown>>
2468
2468
  onDone: (report: ValidationReport<S>) => void;
2469
2469
  /** Gather evidence; decide if there is now enough. Called on every `observer.update()`. */
2470
2470
  update(): void;
2471
- /** Give up without a verdict. Finishes with `inconclusive`, so a caller waiting on it is freed. */
2472
- cancel: () => void;
2471
+ /**
2472
+ * Give up without a verdict. Finishes with `inconclusive`, so a caller waiting on it is freed.
2473
+ *
2474
+ * `reason` is carried into the report. Worth passing something specific — "cancelled" tells the
2475
+ * reader nothing, whereas "sfu redeployed" explains why a check that was running has no verdict.
2476
+ */
2477
+ cancel: (reason?: string) => void;
2473
2478
  }
2474
2479
  /**
2475
2480
  * The part of a validator the observer needs in order to drive it.
@@ -4412,11 +4417,48 @@ type AvailableDetectorsConfigs = AvailableObserverScopeDetectorsConfigs | Availa
4412
4417
  declare class Detectors {
4413
4418
  private _detectors;
4414
4419
  constructor(...detectors: Detector[]);
4420
+ /**
4421
+ * Every registered detector, in registration order.
4422
+ *
4423
+ * This is **the** way to get hold of an instance: `addDetector` / `addObserverDetector` are
4424
+ * chainable and return the owning entity, so the registry is where instances live. Read it to
4425
+ * inspect a detector's state, or to pick one out and {@link remove} it.
4426
+ *
4427
+ * A copy, not the live array — a caller iterating this while removing would otherwise skip
4428
+ * entries, and that is exactly what "remove the ones that look like X" does.
4429
+ */
4430
+ get instances(): Detector[];
4431
+ /** Iterate the registry directly: `for (const detector of call.detectors)`. */
4432
+ [Symbol.iterator](): IterableIterator<Detector>;
4433
+ /** The names in registration order. Duplicates are meaningful — see {@link getAll}. */
4415
4434
  get listOfNames(): string[];
4416
4435
  get size(): number;
4417
4436
  add(detector: Detector): void;
4437
+ /** The first detector registered under `name`. See {@link getAll} when several can share one. */
4418
4438
  get(name: string): Detector | undefined;
4419
- remove(detector: Detector): void;
4439
+ /**
4440
+ * Every detector registered under `name`.
4441
+ *
4442
+ * More than one is legitimate: `ClientPopulationIssueDetector` is meant to be added once per
4443
+ * `groupBy` axis, and two instances of it share a name.
4444
+ */
4445
+ getAll(name: string): Detector[];
4446
+ has(name: string): boolean;
4447
+ /** Remove one specific instance. Returns `false` if it was not registered here. */
4448
+ remove(detector: Detector): boolean;
4449
+ /**
4450
+ * Remove **every** detector registered under `name`, returning how many were removed.
4451
+ *
4452
+ * All of them rather than the first, because a name can legitimately be registered more than once
4453
+ * (see {@link getAll}) and "remove the `client-population-issue-detector`" cannot sensibly mean
4454
+ * "remove whichever axis happens to be first in the array". Removing all of them is the only
4455
+ * behaviour that leaves the registry in a state the caller can predict from the name alone.
4456
+ *
4457
+ * Each removed detector gets `close()`, so trackers unsubscribe from the issue registry, bus
4458
+ * listeners drop, and timers clear — a detector removed without closing keeps being fed issues
4459
+ * forever.
4460
+ */
4461
+ removeByName(name: string): number;
4420
4462
  update(): void;
4421
4463
  clear(): void;
4422
4464
  private _close;
@@ -4565,7 +4607,32 @@ declare class ObservedCall<AppData extends Record<string, unknown> = Record<stri
4565
4607
  constructor(settings: ObservedCallSettings<AppData>, observer: Observer, activeIssuesRegistry: ActiveIssuesRegistry);
4566
4608
  get numberOfClients(): number;
4567
4609
  get score(): number | undefined;
4610
+ /**
4611
+ * Build a call-scoped detector onto this call. Chainable.
4612
+ *
4613
+ * To get a handle on what was built — to inspect it, or to remove that exact instance later — read
4614
+ * it back off the registry: `call.detectors.getAll(name)`, or `call.detectors.instances`.
4615
+ */
4568
4616
  addDetector<K extends keyof AvailableCallScopeDetectorsConfigs>(name: K, config?: Partial<AvailableCallScopeDetectorsConfigs[K]>): this;
4617
+ /**
4618
+ * Remove a detector from **this call** by name, returning how many were removed.
4619
+ *
4620
+ * **Every** instance under the name goes — a name can legitimately be registered more than once.
4621
+ * When you want one of them specifically, go through the registry, which deals in instances:
4622
+ *
4623
+ * ```ts
4624
+ * const [ first ] = call.detectors.getAll('issue-fan-out-detector');
4625
+ *
4626
+ * call.detectors.remove(first);
4627
+ * ```
4628
+ *
4629
+ * Either route `close()`s the detector, so it unsubscribes from `activeIssuesRegistry` — without
4630
+ * that the registry keeps feeding a detector nobody is running any more, and its tracked set grows
4631
+ * for the life of the call.
4632
+ *
4633
+ * To stop building it on *future* calls too, use `observer.removeCallDetector(name)`.
4634
+ */
4635
+ removeDetector(name: keyof AvailableCallScopeDetectorsConfigs): number;
4569
4636
  /**
4570
4637
  * Raise a call-level (server-side) finding; surfaced on the Observer bus as `call-issue`.
4571
4638
  *
@@ -5113,6 +5180,12 @@ declare class Observer<AppData extends Record<string, unknown> = Record<string,
5113
5180
  constructor(config?: Partial<ObserverConfig<AppData>>);
5114
5181
  get numberOfCalls(): number;
5115
5182
  get appData(): AppData | undefined;
5183
+ /**
5184
+ * Build a cross-call detector onto `observer.detectors`. Chainable.
5185
+ *
5186
+ * To get a handle on what was built — to inspect it, or to remove that exact instance later — read
5187
+ * it back off the registry: `observer.detectors.getAll(name)`, or `observer.detectors.instances`.
5188
+ */
5116
5189
  addObserverDetector<K extends keyof AvailableObserverScopeDetectorsConfigs>(name: K, config?: Partial<AvailableObserverScopeDetectorsConfigs[K]>): this;
5117
5190
  /**
5118
5191
  * Enable a call-scoped detector for calls created **from now on**.
@@ -5121,8 +5194,36 @@ declare class Observer<AppData extends Record<string, unknown> = Record<string,
5121
5194
  * built with. To add one to an existing call, use `observedCall.addDetector(...)` directly.
5122
5195
  */
5123
5196
  addCallDetector<K extends keyof AvailableCallScopeDetectorsConfigs>(name: K, config?: Partial<AvailableCallScopeDetectorsConfigs[K]>): this;
5124
- /** Stop building `name` on calls created from now on. Calls already open are untouched. */
5125
- removeCallDetector(name: keyof AvailableCallScopeDetectorsConfigs): this;
5197
+ /**
5198
+ * Remove an observer-scoped detector by name, returning how many were removed.
5199
+ *
5200
+ * **Every** instance registered under the name goes, since a name can legitimately be registered
5201
+ * more than once (`ClientPopulationIssueDetector` is meant to be added once per `groupBy` axis).
5202
+ * When you want one of them specifically, go through the registry, which deals in instances:
5203
+ *
5204
+ * ```ts
5205
+ * const [ byBrowser, byOs ] = observer.detectors.getAll('client-population-issue-detector');
5206
+ *
5207
+ * observer.detectors.remove(byOs); // keeps the browser axis running
5208
+ * ```
5209
+ *
5210
+ * Either route `close()`s the detector, so it unsubscribes from the issue registry and drops any
5211
+ * timers or bus listeners it held.
5212
+ */
5213
+ removeObserverDetector(name: keyof AvailableObserverScopeDetectorsConfigs): number;
5214
+ /**
5215
+ * Stop building `name` on calls created from now on.
5216
+ *
5217
+ * By default this also removes it from the calls **already open**, so that "remove this detector"
5218
+ * means the same thing whether you say it before or after a call started — the alternative leaves
5219
+ * a fleet where the detector is live on some calls and not others, decided by join time. Pass
5220
+ * `{ includeOpenCalls: false }` to change only what future calls are built with.
5221
+ *
5222
+ * Returns the number of live detector instances removed (`0` when only the config changed).
5223
+ */
5224
+ removeCallDetector(name: keyof AvailableCallScopeDetectorsConfigs, { includeOpenCalls }?: {
5225
+ includeOpenCalls?: boolean;
5226
+ }): number;
5126
5227
  /**
5127
5228
  * Start a structural check. It runs on each `observer.update()` until it can decide, reports once
5128
5229
  * on `validation-ready`, and removes itself.
@@ -5136,6 +5237,25 @@ declare class Observer<AppData extends Record<string, unknown> = Record<string,
5136
5237
  * elapsed time is what makes a structural verdict stale.
5137
5238
  */
5138
5239
  addValidator<K extends keyof AvailableValidatorConfigs>(name: K, config?: Partial<AvailableValidatorConfigs[K]>): this;
5240
+ /**
5241
+ * Stop a running validation, by name or by instance. Returns how many were cancelled.
5242
+ *
5243
+ * Cancelling is **not** silent discarding. The validator finishes with `inconclusive` and the given
5244
+ * `reason`, emits `validation-ready` like any other completion, and removes itself. That matters
5245
+ * because anything waiting on the verdict — a deploy gate, a dashboard, a promise — would otherwise
5246
+ * wait forever, and because "we stopped asking" is a materially different outcome from "we asked
5247
+ * and learned nothing", which is exactly what `inconclusive` with a reason records.
5248
+ *
5249
+ * ```ts
5250
+ * observer.cancelValidator('simulcast-receivers', 'sfu redeployed');
5251
+ *
5252
+ * // or one specific instance — `observer.validators` holds what is running
5253
+ * for (const validator of observer.validators) observer.cancelValidator(validator, 'shutting down');
5254
+ * ```
5255
+ *
5256
+ * Pass a real reason. The default tells the reader nothing they could not already infer.
5257
+ */
5258
+ cancelValidator(target: keyof AvailableValidatorConfigs | RunningValidator, reason?: string): number;
5139
5259
  getObservedCall<T extends Record<string, unknown> = Record<string, unknown>>(callId: string): ObservedCall<T> | undefined;
5140
5260
  createObservedCall<T extends Record<string, unknown> = Record<string, unknown>>(settings: ObservedCallSettings<T>): ObservedCall<T> | undefined;
5141
5261
  getOrCreateObservedCall<T extends Record<string, unknown> = Record<string, unknown>>(settings: ObservedCallSettings<T>): ObservedCall<T> | undefined;
package/dist/index.d.ts CHANGED
@@ -2468,8 +2468,13 @@ interface Validator<S extends Record<string, unknown> = Record<string, unknown>>
2468
2468
  onDone: (report: ValidationReport<S>) => void;
2469
2469
  /** Gather evidence; decide if there is now enough. Called on every `observer.update()`. */
2470
2470
  update(): void;
2471
- /** Give up without a verdict. Finishes with `inconclusive`, so a caller waiting on it is freed. */
2472
- cancel: () => void;
2471
+ /**
2472
+ * Give up without a verdict. Finishes with `inconclusive`, so a caller waiting on it is freed.
2473
+ *
2474
+ * `reason` is carried into the report. Worth passing something specific — "cancelled" tells the
2475
+ * reader nothing, whereas "sfu redeployed" explains why a check that was running has no verdict.
2476
+ */
2477
+ cancel: (reason?: string) => void;
2473
2478
  }
2474
2479
  /**
2475
2480
  * The part of a validator the observer needs in order to drive it.
@@ -4412,11 +4417,48 @@ type AvailableDetectorsConfigs = AvailableObserverScopeDetectorsConfigs | Availa
4412
4417
  declare class Detectors {
4413
4418
  private _detectors;
4414
4419
  constructor(...detectors: Detector[]);
4420
+ /**
4421
+ * Every registered detector, in registration order.
4422
+ *
4423
+ * This is **the** way to get hold of an instance: `addDetector` / `addObserverDetector` are
4424
+ * chainable and return the owning entity, so the registry is where instances live. Read it to
4425
+ * inspect a detector's state, or to pick one out and {@link remove} it.
4426
+ *
4427
+ * A copy, not the live array — a caller iterating this while removing would otherwise skip
4428
+ * entries, and that is exactly what "remove the ones that look like X" does.
4429
+ */
4430
+ get instances(): Detector[];
4431
+ /** Iterate the registry directly: `for (const detector of call.detectors)`. */
4432
+ [Symbol.iterator](): IterableIterator<Detector>;
4433
+ /** The names in registration order. Duplicates are meaningful — see {@link getAll}. */
4415
4434
  get listOfNames(): string[];
4416
4435
  get size(): number;
4417
4436
  add(detector: Detector): void;
4437
+ /** The first detector registered under `name`. See {@link getAll} when several can share one. */
4418
4438
  get(name: string): Detector | undefined;
4419
- remove(detector: Detector): void;
4439
+ /**
4440
+ * Every detector registered under `name`.
4441
+ *
4442
+ * More than one is legitimate: `ClientPopulationIssueDetector` is meant to be added once per
4443
+ * `groupBy` axis, and two instances of it share a name.
4444
+ */
4445
+ getAll(name: string): Detector[];
4446
+ has(name: string): boolean;
4447
+ /** Remove one specific instance. Returns `false` if it was not registered here. */
4448
+ remove(detector: Detector): boolean;
4449
+ /**
4450
+ * Remove **every** detector registered under `name`, returning how many were removed.
4451
+ *
4452
+ * All of them rather than the first, because a name can legitimately be registered more than once
4453
+ * (see {@link getAll}) and "remove the `client-population-issue-detector`" cannot sensibly mean
4454
+ * "remove whichever axis happens to be first in the array". Removing all of them is the only
4455
+ * behaviour that leaves the registry in a state the caller can predict from the name alone.
4456
+ *
4457
+ * Each removed detector gets `close()`, so trackers unsubscribe from the issue registry, bus
4458
+ * listeners drop, and timers clear — a detector removed without closing keeps being fed issues
4459
+ * forever.
4460
+ */
4461
+ removeByName(name: string): number;
4420
4462
  update(): void;
4421
4463
  clear(): void;
4422
4464
  private _close;
@@ -4565,7 +4607,32 @@ declare class ObservedCall<AppData extends Record<string, unknown> = Record<stri
4565
4607
  constructor(settings: ObservedCallSettings<AppData>, observer: Observer, activeIssuesRegistry: ActiveIssuesRegistry);
4566
4608
  get numberOfClients(): number;
4567
4609
  get score(): number | undefined;
4610
+ /**
4611
+ * Build a call-scoped detector onto this call. Chainable.
4612
+ *
4613
+ * To get a handle on what was built — to inspect it, or to remove that exact instance later — read
4614
+ * it back off the registry: `call.detectors.getAll(name)`, or `call.detectors.instances`.
4615
+ */
4568
4616
  addDetector<K extends keyof AvailableCallScopeDetectorsConfigs>(name: K, config?: Partial<AvailableCallScopeDetectorsConfigs[K]>): this;
4617
+ /**
4618
+ * Remove a detector from **this call** by name, returning how many were removed.
4619
+ *
4620
+ * **Every** instance under the name goes — a name can legitimately be registered more than once.
4621
+ * When you want one of them specifically, go through the registry, which deals in instances:
4622
+ *
4623
+ * ```ts
4624
+ * const [ first ] = call.detectors.getAll('issue-fan-out-detector');
4625
+ *
4626
+ * call.detectors.remove(first);
4627
+ * ```
4628
+ *
4629
+ * Either route `close()`s the detector, so it unsubscribes from `activeIssuesRegistry` — without
4630
+ * that the registry keeps feeding a detector nobody is running any more, and its tracked set grows
4631
+ * for the life of the call.
4632
+ *
4633
+ * To stop building it on *future* calls too, use `observer.removeCallDetector(name)`.
4634
+ */
4635
+ removeDetector(name: keyof AvailableCallScopeDetectorsConfigs): number;
4569
4636
  /**
4570
4637
  * Raise a call-level (server-side) finding; surfaced on the Observer bus as `call-issue`.
4571
4638
  *
@@ -5113,6 +5180,12 @@ declare class Observer<AppData extends Record<string, unknown> = Record<string,
5113
5180
  constructor(config?: Partial<ObserverConfig<AppData>>);
5114
5181
  get numberOfCalls(): number;
5115
5182
  get appData(): AppData | undefined;
5183
+ /**
5184
+ * Build a cross-call detector onto `observer.detectors`. Chainable.
5185
+ *
5186
+ * To get a handle on what was built — to inspect it, or to remove that exact instance later — read
5187
+ * it back off the registry: `observer.detectors.getAll(name)`, or `observer.detectors.instances`.
5188
+ */
5116
5189
  addObserverDetector<K extends keyof AvailableObserverScopeDetectorsConfigs>(name: K, config?: Partial<AvailableObserverScopeDetectorsConfigs[K]>): this;
5117
5190
  /**
5118
5191
  * Enable a call-scoped detector for calls created **from now on**.
@@ -5121,8 +5194,36 @@ declare class Observer<AppData extends Record<string, unknown> = Record<string,
5121
5194
  * built with. To add one to an existing call, use `observedCall.addDetector(...)` directly.
5122
5195
  */
5123
5196
  addCallDetector<K extends keyof AvailableCallScopeDetectorsConfigs>(name: K, config?: Partial<AvailableCallScopeDetectorsConfigs[K]>): this;
5124
- /** Stop building `name` on calls created from now on. Calls already open are untouched. */
5125
- removeCallDetector(name: keyof AvailableCallScopeDetectorsConfigs): this;
5197
+ /**
5198
+ * Remove an observer-scoped detector by name, returning how many were removed.
5199
+ *
5200
+ * **Every** instance registered under the name goes, since a name can legitimately be registered
5201
+ * more than once (`ClientPopulationIssueDetector` is meant to be added once per `groupBy` axis).
5202
+ * When you want one of them specifically, go through the registry, which deals in instances:
5203
+ *
5204
+ * ```ts
5205
+ * const [ byBrowser, byOs ] = observer.detectors.getAll('client-population-issue-detector');
5206
+ *
5207
+ * observer.detectors.remove(byOs); // keeps the browser axis running
5208
+ * ```
5209
+ *
5210
+ * Either route `close()`s the detector, so it unsubscribes from the issue registry and drops any
5211
+ * timers or bus listeners it held.
5212
+ */
5213
+ removeObserverDetector(name: keyof AvailableObserverScopeDetectorsConfigs): number;
5214
+ /**
5215
+ * Stop building `name` on calls created from now on.
5216
+ *
5217
+ * By default this also removes it from the calls **already open**, so that "remove this detector"
5218
+ * means the same thing whether you say it before or after a call started — the alternative leaves
5219
+ * a fleet where the detector is live on some calls and not others, decided by join time. Pass
5220
+ * `{ includeOpenCalls: false }` to change only what future calls are built with.
5221
+ *
5222
+ * Returns the number of live detector instances removed (`0` when only the config changed).
5223
+ */
5224
+ removeCallDetector(name: keyof AvailableCallScopeDetectorsConfigs, { includeOpenCalls }?: {
5225
+ includeOpenCalls?: boolean;
5226
+ }): number;
5126
5227
  /**
5127
5228
  * Start a structural check. It runs on each `observer.update()` until it can decide, reports once
5128
5229
  * on `validation-ready`, and removes itself.
@@ -5136,6 +5237,25 @@ declare class Observer<AppData extends Record<string, unknown> = Record<string,
5136
5237
  * elapsed time is what makes a structural verdict stale.
5137
5238
  */
5138
5239
  addValidator<K extends keyof AvailableValidatorConfigs>(name: K, config?: Partial<AvailableValidatorConfigs[K]>): this;
5240
+ /**
5241
+ * Stop a running validation, by name or by instance. Returns how many were cancelled.
5242
+ *
5243
+ * Cancelling is **not** silent discarding. The validator finishes with `inconclusive` and the given
5244
+ * `reason`, emits `validation-ready` like any other completion, and removes itself. That matters
5245
+ * because anything waiting on the verdict — a deploy gate, a dashboard, a promise — would otherwise
5246
+ * wait forever, and because "we stopped asking" is a materially different outcome from "we asked
5247
+ * and learned nothing", which is exactly what `inconclusive` with a reason records.
5248
+ *
5249
+ * ```ts
5250
+ * observer.cancelValidator('simulcast-receivers', 'sfu redeployed');
5251
+ *
5252
+ * // or one specific instance — `observer.validators` holds what is running
5253
+ * for (const validator of observer.validators) observer.cancelValidator(validator, 'shutting down');
5254
+ * ```
5255
+ *
5256
+ * Pass a real reason. The default tells the reader nothing they could not already infer.
5257
+ */
5258
+ cancelValidator(target: keyof AvailableValidatorConfigs | RunningValidator, reason?: string): number;
5139
5259
  getObservedCall<T extends Record<string, unknown> = Record<string, unknown>>(callId: string): ObservedCall<T> | undefined;
5140
5260
  createObservedCall<T extends Record<string, unknown> = Record<string, unknown>>(settings: ObservedCallSettings<T>): ObservedCall<T> | undefined;
5141
5261
  getOrCreateObservedCall<T extends Record<string, unknown> = Record<string, unknown>>(settings: ObservedCallSettings<T>): ObservedCall<T> | undefined;