@observertc/observer-js 1.3.0-rc.2 → 1.3.0-rc.4

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/index.d.ts CHANGED
@@ -48,6 +48,23 @@ interface Sink<T = unknown> {
48
48
  once(event: keyof SinkEvents, listener: (...args: any[]) => void): this;
49
49
  off(event: keyof SinkEvents, listener: (...args: any[]) => void): this;
50
50
  }
51
+ /**
52
+ * Where records go, exactly one of: `jsonlPath`, the path of a JSONL file (a `JsonlFileSink` is
53
+ * created there), or `sink`, a sink of your own. Giving both is a compile error.
54
+ */
55
+ type SinkDestination<T = unknown> = {
56
+ jsonlPath: string;
57
+ sink?: never;
58
+ } | {
59
+ sink: Sink<T>;
60
+ jsonlPath?: never;
61
+ };
62
+ /** What the `saveClientSamples` callbacks receive: `id` is the client's sample sink id, the client id. */
63
+ type ClientSinkParams<Call = unknown> = {
64
+ id: string;
65
+ observedCall: Call;
66
+ clientId: string;
67
+ };
51
68
 
52
69
  /** A sink class `SinkRegistry.create` can construct: its constructor takes one settings object (with `id`). */
53
70
  type SinkConstructor = new (settings: any) => Sink<any>;
@@ -70,6 +87,7 @@ type SinkCreateSettings<C extends SinkConstructor> = Omit<ConstructorParameters<
70
87
  declare class SinkRegistry {
71
88
  readonly parent?: SinkRegistry | undefined;
72
89
  private readonly _sinks;
90
+ private readonly _ownIds;
73
91
  constructor(parent?: SinkRegistry | undefined);
74
92
  /** Number of open sinks in this registry and its descendants. */
75
93
  get size(): number;
@@ -82,21 +100,41 @@ declare class SinkRegistry {
82
100
  * `undefined` is returned and a warning logged.
83
101
  */
84
102
  create<C extends SinkConstructor>(SinkClass: C, settings: SinkCreateSettings<C>): InstanceType<C> | undefined;
103
+ /**
104
+ * Register a sink constructed elsewhere, under its own `id`. Returns `false`, registering nothing,
105
+ * when the id is already in use in the observer (the sink is then the caller's to end). Prefer
106
+ * `create` when the sink opens a resource on construction: it checks the id first.
107
+ */
108
+ add(sink: Sink<any>): boolean;
109
+ /**
110
+ * Register the sink a `SinkDestination` describes, under `id`: for `jsonlPath`, a `JsonlFileSink`
111
+ * is created (the id is checked before the file is opened); a `sink` is added, and its `id` must
112
+ * be `id`. Returns `undefined` when the id is taken, the ids differ, or both are given (possible
113
+ * from JavaScript); a given `sink` that is not registered is ended.
114
+ */
115
+ createFromDestination<T>(id: string, destination: SinkDestination<T>): Sink<T> | undefined;
116
+ /** Whether the sink `id` was added to this registry itself, rather than to a descendant. */
117
+ owns(id: string): boolean;
85
118
  /** Write `record` to the sink `id`. `false` when there is no such sink, or on backpressure. */
86
119
  write(id: string, record: unknown): boolean;
87
120
  /** End every sink of this registry and of its descendants. */
88
121
  clear(): void;
89
122
  /** This registry and its ancestors, up to the root. */
123
+ private _register;
90
124
  private _lineage;
91
125
  }
92
126
 
93
- /** The settings of `createExtensionStats` on an observer, a call or a mediasoup router. */
94
- type CreateExtensionStatsSettings<C extends SinkConstructor> = {
127
+ /**
128
+ * The settings of `createExtensionStats` on an observer, a call or a mediasoup router: the `id`,
129
+ * and at most one of `jsonlPath` and `sink` (a `SinkDestination`).
130
+ */
131
+ type CreateExtensionStatsSettings<T = unknown> = {
95
132
  /** Unique per observer: it is also the id of the sink that receives the stats. */
96
133
  id: string;
97
- /** What `sinks.create` takes; the sink is created under `id` (an `id` in its settings is ignored). */
98
- createSink?: [SinkClass: C, settings: Omit<SinkCreateSettings<C>, 'id'>];
99
- };
134
+ } & (SinkDestination<T> | {
135
+ jsonlPath?: never;
136
+ sink?: never;
137
+ });
100
138
  /**
101
139
  * The extension stats of one object — `observer.extensionStats`, `observedCall.extensionStats`,
102
140
  * `observedMediasoupRouter.extensionStats`: the last data per id (`values`), written to the
@@ -118,7 +156,7 @@ declare class ExtensionStatsRegistry {
118
156
  * owner's `sinks.clear()`. Called by the owner when it closes.
119
157
  */
120
158
  close(): void;
121
- create<C extends SinkConstructor>(settings: CreateExtensionStatsSettings<C>): InstanceType<C> | undefined;
159
+ create<T = unknown>(settings: CreateExtensionStatsSettings<T>): Sink<T> | undefined;
122
160
  accept(id: string, data: unknown): void;
123
161
  get<T>(id: string): T | undefined;
124
162
  }
@@ -3147,39 +3185,6 @@ declare function isClientIssueResolutionEntry(issue: ClientIssue): boolean;
3147
3185
  /** Strip the `-resolved` suffix, so both entries of a lifecycle share one logical type. */
3148
3186
  declare function baseIssueType(type: string): string;
3149
3187
 
3150
- type JsonlFileSinkOptions<T = ClientSample> = Partial<SinkSettings> & {
3151
- /** Absolute or relative file path to append JSONL lines to. */
3152
- path: string;
3153
- /** Optional custom serializer; defaults to `JSON.stringify`. */
3154
- serializeSample?: (record: T) => string;
3155
- };
3156
- /**
3157
- * A sink that serializes each record to a JSON line and appends it to a file. Defaults to client
3158
- * samples, but takes any record type: `sinks.create(JsonlFileSink<PipeTransportStats[]>, { id, path })`. It wraps an
3159
- * `fs.WriteStream` and re-emits its lifecycle events, so `close` fires once the file is flushed
3160
- * and its descriptor is closed (file ready) and `error` surfaces file errors.
3161
- */
3162
- declare interface JsonlFileSink {
3163
- on<E extends keyof SinkEvents>(event: E, listener: (...args: SinkEvents[E]) => void): this;
3164
- once<E extends keyof SinkEvents>(event: E, listener: (...args: SinkEvents[E]) => void): this;
3165
- off<E extends keyof SinkEvents>(event: E, listener: (...args: SinkEvents[E]) => void): this;
3166
- emit<E extends keyof SinkEvents>(event: E, ...args: SinkEvents[E]): boolean;
3167
- }
3168
- declare class JsonlFileSink<T = ClientSample> extends EventEmitter implements Sink<T> {
3169
- readonly id: string;
3170
- /** The file this sink writes to. Read it (e.g. in a `close` handler) to upload/move the file. */
3171
- readonly path: string;
3172
- private readonly _stream;
3173
- private readonly _serializeSample;
3174
- private _ended;
3175
- constructor(options: JsonlFileSinkOptions<T>);
3176
- get ended(): boolean;
3177
- write(record: T): boolean;
3178
- end(): void;
3179
- }
3180
- /** Build a JSONL file sink for a single path. */
3181
- declare function createJsonlFileSink<T = ClientSample>(options: JsonlFileSinkOptions<T>): JsonlFileSink<T>;
3182
-
3183
3188
  /** mediasoup `TransportTuple` (the selected local/remote address pair of a transport). */
3184
3189
  type TransportTuple = {
3185
3190
  localAddress: string;
@@ -3467,7 +3472,7 @@ declare class ObservedMediasoupRouter<AppData extends Record<string, unknown> =
3467
3472
  */
3468
3473
  snapshot(): MediasoupRouterSample;
3469
3474
  close(): void;
3470
- createExtensionStats<C extends SinkConstructor>(settings: CreateExtensionStatsSettings<C>): InstanceType<C> | undefined;
3475
+ createExtensionStats<T = unknown>(settings: CreateExtensionStatsSettings<T>): Sink<T> | undefined;
3471
3476
  acceptExtensionStats(id: string, data: unknown): void;
3472
3477
  getExtensionStats<T>(id: string): T | undefined;
3473
3478
  addTransport: (transport: types.Transport) => void;
@@ -3565,7 +3570,7 @@ type ObserverEvents = {
3565
3570
  }];
3566
3571
  'client-added': [ObservedClientScope];
3567
3572
  'client-sink-created': [ObservedClientScope & {
3568
- sink: JsonlFileSink;
3573
+ sink: Sink<ClientSample>;
3569
3574
  }];
3570
3575
  'client-updated': [ObservedClientScope & {
3571
3576
  sample: ClientSample;
@@ -4005,7 +4010,7 @@ declare class ObservedClient<AppData extends Record<string, unknown> = Record<st
4005
4010
  /** The distinct `publicAddress`es currently known by this client's open peer connections. */
4006
4011
  /**
4007
4012
  * The sink this client's samples are written to: whatever is registered under its client id in
4008
- * `call.sinks` (the `createClientJsonlSink` file, or a sink the application added), if any.
4013
+ * `call.sinks` (the one created through `saveClientSamples`, or a sink the application added), if any.
4009
4014
  */
4010
4015
  get sink(): Sink<ClientSample> | undefined;
4011
4016
  get publicAddresses(): string[];
@@ -5874,7 +5879,7 @@ declare class ObservedCall<AppData extends Record<string, unknown> = Record<stri
5874
5879
  */
5875
5880
  addIssue(issue: Omit<CallIssue, 'scope'>): void;
5876
5881
  close(): void;
5877
- createExtensionStats<C extends SinkConstructor>(settings: CreateExtensionStatsSettings<C>): InstanceType<C> | undefined;
5882
+ createExtensionStats<T = unknown>(settings: CreateExtensionStatsSettings<T>): Sink<T> | undefined;
5878
5883
  acceptExtensionStats(id: string, data: unknown): void;
5879
5884
  getExtensionStats<T>(id: string): T | undefined;
5880
5885
  getObservedClient<ClientAppData extends Record<string, unknown> = Record<string, unknown>>(clientId: string): ObservedClient<ClientAppData> | undefined;
@@ -5884,6 +5889,7 @@ declare class ObservedCall<AppData extends Record<string, unknown> = Record<stri
5884
5889
  private _onClientUpdate;
5885
5890
  private _clientJoined;
5886
5891
  private _clientLeft;
5892
+ private _createClientSink;
5887
5893
  /** Emit an Observer-bus event scoped to this call. */
5888
5894
  private _notify;
5889
5895
  }
@@ -6445,6 +6451,14 @@ type ClientAppDataFactory = (params: {
6445
6451
  observedCall: ObservedCall;
6446
6452
  acceptCtx?: AcceptContext;
6447
6453
  }) => Record<string, unknown>;
6454
+ /** `ObserverConfig.saveClientSamples`: exactly one of the two per-client callbacks. */
6455
+ type SaveClientSamplesConfig = {
6456
+ jsonlPath: (params: ClientSinkParams<ObservedCall>) => string | undefined;
6457
+ sink?: never;
6458
+ } | {
6459
+ sink: (params: ClientSinkParams<ObservedCall>) => Sink<ClientSample> | undefined;
6460
+ jsonlPath?: never;
6461
+ };
6448
6462
  type ObserverConfig<AppData extends Record<string, unknown> = Record<string, unknown>> = {
6449
6463
  /**
6450
6464
  * Application-owned data for the observer itself. Never read or modified by the library.
@@ -6598,38 +6612,15 @@ type ObserverConfig<AppData extends Record<string, unknown> = Record<string, unk
6598
6612
  /** Same as `createCallAppData`, for clients. Receives the (already-created) parent call. */
6599
6613
  createClientAppData?: ClientAppDataFactory;
6600
6614
  /**
6601
- * Write every sample a client accepts to a JSONL file: return the file path for a newly-created
6602
- * client, or `undefined` for no file. The directory must exist. The `JsonlFileSink` is added to
6603
- * `observedCall.sinks` under the client id and is ended when the client closes; `client-sink-created`
6604
- * announces it, so a `close` handler can upload or move the file once it is flushed.
6605
- *
6606
- * For any other destination, add your own sink under the client id instead, e.g. on
6607
- * `client-added`: `observedCall.sinks.create(MySink, { id: observedClient.clientId, ... })`.
6608
- *
6609
- * Replaces `createClientSink` (removed in 1.3.0), and with it `createJsonlFileSinkFactory`.
6610
- * To migrate:
6615
+ * Where each client's samples are saved: exactly one of `jsonlPath` (return the path of the
6616
+ * client's JSONL file) and `sink` (return a sink whose `id` is `params.id`), each called per
6617
+ * client; `undefined` gives that client no sink. See the README's *Sinks* section.
6611
6618
  *
6612
- * ```ts
6613
- * // before (1.2): JSONL files
6614
- * new Observer({ createClientSink: createJsonlFileSinkFactory({ directory: './stats' }) });
6615
- * // after: return the path; `${callId}__${clientId}.jsonl` was the factory's default file name
6616
- * new Observer({
6617
- * createClientJsonlSink: ({ observedCall, clientId }) => path.join('./stats', `${observedCall.callId}__${clientId}.jsonl`),
6618
- * });
6619
- *
6620
- * // before (1.2): any other sink
6621
- * new Observer({ createClientSink: ({ clientId, observedCall }) => new HttpSink(url(observedCall, clientId)) });
6622
- * // after: create it under the client id when the client is added (HttpSink implements
6623
- * // `ClientSampleSink`, takes `SinkSettings & { ... }` and emits `ended` from `end()`)
6624
- * observer.on('client-added', ({ observedCall, observedClient }) => {
6625
- * observedCall.sinks.create(HttpSink, { id: observedClient.clientId, url: url(observedCall, observedClient.clientId) });
6626
- * });
6627
- * ```
6619
+ * A client's sample sink id is its client id (`params.id`). Sink ids are unique per observer, so
6620
+ * client ids must be unique across the observer's open calls; a client whose id is already a sink
6621
+ * id gets no sink, and a warning is logged.
6628
6622
  */
6629
- createClientJsonlSink?: (params: {
6630
- observedCall: ObservedCall;
6631
- clientId: string;
6632
- }) => string | undefined;
6623
+ saveClientSamples?: SaveClientSamplesConfig;
6633
6624
  /**
6634
6625
  * Optional factory invoked when a call is created, producing the call's `RemoteTrackResolver`
6635
6626
  * (or `undefined` for none). Use the built-ins
@@ -6800,7 +6791,7 @@ declare class Observer<AppData extends Record<string, unknown> = Record<string,
6800
6791
  matchPeerConnectionByWebRtcTransportId?: boolean;
6801
6792
  }): ObservedMediasoupRouter<Record<string, unknown>> | undefined;
6802
6793
  close(): void;
6803
- createExtensionStats<C extends SinkConstructor>(settings: CreateExtensionStatsSettings<C>): InstanceType<C> | undefined;
6794
+ createExtensionStats<T = unknown>(settings: CreateExtensionStatsSettings<T>): Sink<T> | undefined;
6804
6795
  acceptExtensionStats(id: string, data: unknown): void;
6805
6796
  getExtensionStats<T>(id: string): T | undefined;
6806
6797
  accept(sample: ClientSample, context?: AcceptContext): void;
@@ -7199,9 +7190,9 @@ declare function setObserverLogger(logger: ObserverLogger): void;
7199
7190
  /** The lifecycle events a client sample sink may emit. Same as {@link SinkEvents}. */
7200
7191
  type ClientSampleSinkEvents = SinkEvents;
7201
7192
  /**
7202
- * A per-client destination for accepted samples. Create one under the client id with
7203
- * `observedCall.sinks.create(MySink, { id: clientId, ... })`, or let
7204
- * `ObserverConfig.createClientJsonlSink` create a `JsonlFileSink` there.
7193
+ * A per-client destination for accepted samples. Return one from `ObserverConfig.saveClientSamples.sink`
7194
+ * (with the `id` it receives), or create one under the client id with
7195
+ * `observedCall.sinks.create(MySink, { id: clientId, ... })`.
7205
7196
  *
7206
7197
  * The library calls `write(sample)` synchronously per accepted sample (not awaited) and `end()`s
7207
7198
  * the sink when the client closes; the registry attaches an `error` listener so a failing sink
@@ -7210,6 +7201,39 @@ type ClientSampleSinkEvents = SinkEvents;
7210
7201
  */
7211
7202
  type ClientSampleSink = Sink<ClientSample>;
7212
7203
 
7204
+ type JsonlFileSinkOptions<T = ClientSample> = Partial<SinkSettings> & {
7205
+ /** Absolute or relative file path to append JSONL lines to. */
7206
+ path: string;
7207
+ /** Optional custom serializer; defaults to `JSON.stringify`. */
7208
+ serializeSample?: (record: T) => string;
7209
+ };
7210
+ /**
7211
+ * A sink that serializes each record to a JSON line and appends it to a file. Defaults to client
7212
+ * samples, but takes any record type: `sinks.create(JsonlFileSink<PipeTransportStats[]>, { id, path })`. It wraps an
7213
+ * `fs.WriteStream` and re-emits its lifecycle events, so `close` fires once the file is flushed
7214
+ * and its descriptor is closed (file ready) and `error` surfaces file errors.
7215
+ */
7216
+ declare interface JsonlFileSink {
7217
+ on<E extends keyof SinkEvents>(event: E, listener: (...args: SinkEvents[E]) => void): this;
7218
+ once<E extends keyof SinkEvents>(event: E, listener: (...args: SinkEvents[E]) => void): this;
7219
+ off<E extends keyof SinkEvents>(event: E, listener: (...args: SinkEvents[E]) => void): this;
7220
+ emit<E extends keyof SinkEvents>(event: E, ...args: SinkEvents[E]): boolean;
7221
+ }
7222
+ declare class JsonlFileSink<T = ClientSample> extends EventEmitter implements Sink<T> {
7223
+ readonly id: string;
7224
+ /** The file this sink writes to. Read it (e.g. in a `close` handler) to upload/move the file. */
7225
+ readonly path: string;
7226
+ private readonly _stream;
7227
+ private readonly _serializeSample;
7228
+ private _ended;
7229
+ constructor(options: JsonlFileSinkOptions<T>);
7230
+ get ended(): boolean;
7231
+ write(record: T): boolean;
7232
+ end(): void;
7233
+ }
7234
+ /** Build a JSONL file sink for a single path. */
7235
+ declare function createJsonlFileSink<T = ClientSample>(options: JsonlFileSinkOptions<T>): JsonlFileSink<T>;
7236
+
7213
7237
  declare interface InMemorySink {
7214
7238
  on<E extends keyof SinkEvents>(event: E, listener: (...args: SinkEvents[E]) => void): this;
7215
7239
  once<E extends keyof SinkEvents>(event: E, listener: (...args: SinkEvents[E]) => void): this;
@@ -7236,4 +7260,4 @@ declare function createInMemorySink(samples?: ClientSample[]): InMemorySink;
7236
7260
  declare function createDefaultMediasoupRemoteTrackResolverFactory(): RemoteTrackResolverFactory;
7237
7261
  declare function createP2pRemoteTrackResolverFactory(): RemoteTrackResolverFactory;
7238
7262
 
7239
- export { type AcceptContext, type AcceptMiddleware, type AcceptMiddlewarePayload, type ActiveClientIssue, type ActiveIssueTracker, ActiveIssuesRegistry, AudioEchoDetector, type AudioEchoDetectorConfig, AudioEchoTypes, type AudioInputDevice, type AvailableCallScopeDetectorsConfigs, type AvailableDetectorsConfigs, type AvailableObserverScopeDetectorsConfigs, type AvailableValidatorConfigs, CODEC_MISMATCH_ISSUE, type CalculatedScore, type CallAppDataFactory, CallConcurrentIssueDetector, type CallConcurrentIssueDetectorConfig, type CallConcurrentIssueGroup, CallConcurrentIssueTypes, type CallHealth, CallHealthAggregator, type CallIssue, type CallIssueSpread, type CallScopedEventName, type CallSummary, type CallSummaryClients, CallSummaryCollector, type CallSummaryConfig, type CallSummaryEnricher, type CallSummaryEnrichers, type CallSummaryScores, type CallSummarySection, type CallSummaryTruncation, type CallSummaryTurnServers, type CertificateStats, type ClientAppDataFactory, type ClientEvent, ClientEventTypes, type ClientHealth, type ClientHealthThresholds, type ClientIssue, type ClientLocation, type ClientLocationResolver, type ClientMetaData, ClientMetaTypes, type ClientPopulation, type ClientPopulationAxis, ClientPopulationIssueDetector, type ClientPopulationIssueDetectorConfig, ClientPopulationIssueTypes, type ClientSample, type ClientSampleSink, type ClientSampleSinkEvents, type CodecConsistencyReportPayload, CodecConsistencyValidator, type CodecConsistencyValidatorConfig, type CodecEvidence, type CodecStats, type CorroboratedPublisherFault, type CreateExtensionStatsSettings, type DataChannelStats, type Detector, Detectors, type ExtensionStat, ExtensionStatsRegistry, GEOHASH_CELL_SIZES, type IceCandidatePairStats, type IceCandidateStats, type IceTransportStats, InMemorySink, type InboundRtpStats, type InboundTrackSample, type Issue, type IssueBase, type IssueConclusion, IssueFanOutDetector, type IssueFanOutDetectorConfig, IssueFanOutTypes, type IssueFaultDomain, JsonlFileSink, type JsonlFileSinkOptions, LOWEST_COMMON_DENOMINATOR_ISSUE, type Logger, type MannKendallResult, type MediaKind, type MediaPlayoutStats, type MediaSourceStats, type MediasoupConsumerSample, type MediasoupConsumerSampleEvent, type MediasoupDataConsumerSample, type MediasoupDataProducerSample, type MediasoupDirectTransportSample, type MediasoupDirectTransportSampleEventMap, type MediasoupPipeTransportSample, type MediasoupPipeTransportSampleEventMap, type MediasoupPlainTransportSample, type MediasoupPlainTransportSampleEventMap, type MediasoupProducerSample, type MediasoupProducerSampleEvent, type MediasoupRouterSample, type MediasoupSampleEnricher, type MediasoupTransportSample, type MediasoupWebRtcTransportSample, type MediasoupWebRtcTransportSampleEventMap, type Middleware, MiddlewareProcessor, ObservedCall, type ObservedCallScope, type ObservedCallSettings, ObservedCertificate, ObservedClient, ObservedClientIssueRegistry, type ObservedClientScope, type ObservedClientSettings, ObservedCodec, ObservedDataChannel, ObservedIceCandidate, ObservedIceCandidatePair, ObservedIceTransport, ObservedInboundRtp, ObservedInboundTrack, ObservedMediaPlayout, ObservedMediaSource, ObservedMediasoupRouter, type ObservedMediasoupRouterEvents, type ObservedMediasoupRouterScope, type ObservedMediasoupRouterSettings, ObservedOutboundRtp, ObservedOutboundTrack, ObservedPeerConnection, type ObservedPeerConnectionScope, ObservedPeerConnectionTransport, ObservedRemoteInboundRtp, ObservedRemoteOutboundRtp, ObservedTURN, type ObservedTURNEventMap, ObservedTurnServer, Observer, ObserverConcurrentIssueDetector, type ObserverConcurrentIssueDetectorConfig, type ObserverConcurrentIssueGroup, ObserverConcurrentIssueTypes, type ObserverEventBase, type ObserverEvents, type ObserverIssue, type ObserverIssueSpread, type ObserverLogger, type OutboundRtpStats, type OutboundTrackSample, type PageHinkleyResult, type PeerConnectionSample, type PeerConnectionTransportStats, type PsnrSum, PublisherFaultCorroborationDetector, type PublisherFaultCorroborationDetectorConfig, PublisherFaultTypes, type QualityLimitationDurations, RESOLVED_ISSUE_SUFFIX, RTT_HISTOGRAM_BUCKET_WIDTH_IN_MS, RTT_HISTOGRAM_NUMBER_OF_BUCKETS, type RemoteInboundRtpStats, type RemoteOutboundRtpStats, type RemoteTrackLinkEvidence, RemoteTrackResolver, type RemoteTrackResolverFactory, type RemoteTrackResolverReportPayload, RemoteTrackResolverValidator, type RemoteTrackResolverValidatorConfig, type RemoteTrackResolvers, type ResolvedActiveClientIssue, type RttSummary, type RunningValidator, SCORE_HISTOGRAM_BUCKET_WIDTH, SCORE_HISTOGRAM_NUMBER_OF_BUCKETS, type SampleRejectedReason, type ScoreCalculator, type ScoreSummary, SfuCongestionDetector, type SfuCongestionDetectorBucket, type SfuCongestionDetectorConfig, type SfuCongestionDetectorEvaluation, type SfuCongestionDetectorReport, type SimulcastReceiverEvidence, type SimulcastReceiverReportPayload, SimulcastReceiverValidator, type SimulcastReceiverValidatorConfig, type Sink, type SinkConstructor, type SinkCreateSettings, type SinkEvents, SinkRegistry, type SinkSettings, SlidingWindow, type SlidingWindowEntry, type StatsSummary, TrackDeliveryMismatchDetector, type TrackDeliveryMismatchDetectorConfig, TrackDeliveryMismatchTypes, TrendTester, type TrendTesterConfig, type TurnServerHealth, TurnServerHealthDetector, type TurnServerHealthDetectorConfig, TurnServerHealthTypes, TurnServerOutageDetector, type TurnServerOutageDetectorConfig, TurnServerOutageTypes, UNRESOLVED_TRACK_LINKS_ISSUE, UnconsumedTrackDetector, type UnconsumedTrackDetectorConfig, UnconsumedTrackTypes, type ValidationReport, type Validator, type ValidatorName, baseIssueType, concludeCallIssue, concludeObserverIssue, correlation, counterDelta, createCallSummary, createDefaultMediasoupRemoteTrackResolverFactory, createInMemorySink, createJsonlFileSink, createLogger, createP2pRemoteTrackResolverFactory, defaultCallSummaryConfig, defaultClientHealthThresholds, geohash, isClientIssueResolutionEntry, isPublicIpAddress, issuePayloadAsString, mannKendall, mannKendallVerdict, median, medianAbsoluteDeviation, pageHinkley, percentile, percentileOfHistogram, percentileOfSorted, robustZScore, schemaVersion, setObserverLogger, summarize };
7263
+ export { type AcceptContext, type AcceptMiddleware, type AcceptMiddlewarePayload, type ActiveClientIssue, type ActiveIssueTracker, ActiveIssuesRegistry, AudioEchoDetector, type AudioEchoDetectorConfig, AudioEchoTypes, type AudioInputDevice, type AvailableCallScopeDetectorsConfigs, type AvailableDetectorsConfigs, type AvailableObserverScopeDetectorsConfigs, type AvailableValidatorConfigs, CODEC_MISMATCH_ISSUE, type CalculatedScore, type CallAppDataFactory, CallConcurrentIssueDetector, type CallConcurrentIssueDetectorConfig, type CallConcurrentIssueGroup, CallConcurrentIssueTypes, type CallHealth, CallHealthAggregator, type CallIssue, type CallIssueSpread, type CallScopedEventName, type CallSummary, type CallSummaryClients, CallSummaryCollector, type CallSummaryConfig, type CallSummaryEnricher, type CallSummaryEnrichers, type CallSummaryScores, type CallSummarySection, type CallSummaryTruncation, type CallSummaryTurnServers, type CertificateStats, type ClientAppDataFactory, type ClientEvent, ClientEventTypes, type ClientHealth, type ClientHealthThresholds, type ClientIssue, type ClientLocation, type ClientLocationResolver, type ClientMetaData, ClientMetaTypes, type ClientPopulation, type ClientPopulationAxis, ClientPopulationIssueDetector, type ClientPopulationIssueDetectorConfig, ClientPopulationIssueTypes, type ClientSample, type ClientSampleSink, type ClientSampleSinkEvents, type ClientSinkParams, type CodecConsistencyReportPayload, CodecConsistencyValidator, type CodecConsistencyValidatorConfig, type CodecEvidence, type CodecStats, type CorroboratedPublisherFault, type CreateExtensionStatsSettings, type DataChannelStats, type Detector, Detectors, type ExtensionStat, ExtensionStatsRegistry, GEOHASH_CELL_SIZES, type IceCandidatePairStats, type IceCandidateStats, type IceTransportStats, InMemorySink, type InboundRtpStats, type InboundTrackSample, type Issue, type IssueBase, type IssueConclusion, IssueFanOutDetector, type IssueFanOutDetectorConfig, IssueFanOutTypes, type IssueFaultDomain, JsonlFileSink, type JsonlFileSinkOptions, LOWEST_COMMON_DENOMINATOR_ISSUE, type Logger, type MannKendallResult, type MediaKind, type MediaPlayoutStats, type MediaSourceStats, type MediasoupConsumerSample, type MediasoupConsumerSampleEvent, type MediasoupDataConsumerSample, type MediasoupDataProducerSample, type MediasoupDirectTransportSample, type MediasoupDirectTransportSampleEventMap, type MediasoupPipeTransportSample, type MediasoupPipeTransportSampleEventMap, type MediasoupPlainTransportSample, type MediasoupPlainTransportSampleEventMap, type MediasoupProducerSample, type MediasoupProducerSampleEvent, type MediasoupRouterSample, type MediasoupSampleEnricher, type MediasoupTransportSample, type MediasoupWebRtcTransportSample, type MediasoupWebRtcTransportSampleEventMap, type Middleware, MiddlewareProcessor, ObservedCall, type ObservedCallScope, type ObservedCallSettings, ObservedCertificate, ObservedClient, ObservedClientIssueRegistry, type ObservedClientScope, type ObservedClientSettings, ObservedCodec, ObservedDataChannel, ObservedIceCandidate, ObservedIceCandidatePair, ObservedIceTransport, ObservedInboundRtp, ObservedInboundTrack, ObservedMediaPlayout, ObservedMediaSource, ObservedMediasoupRouter, type ObservedMediasoupRouterEvents, type ObservedMediasoupRouterScope, type ObservedMediasoupRouterSettings, ObservedOutboundRtp, ObservedOutboundTrack, ObservedPeerConnection, type ObservedPeerConnectionScope, ObservedPeerConnectionTransport, ObservedRemoteInboundRtp, ObservedRemoteOutboundRtp, ObservedTURN, type ObservedTURNEventMap, ObservedTurnServer, Observer, ObserverConcurrentIssueDetector, type ObserverConcurrentIssueDetectorConfig, type ObserverConcurrentIssueGroup, ObserverConcurrentIssueTypes, type ObserverEventBase, type ObserverEvents, type ObserverIssue, type ObserverIssueSpread, type ObserverLogger, type OutboundRtpStats, type OutboundTrackSample, type PageHinkleyResult, type PeerConnectionSample, type PeerConnectionTransportStats, type PsnrSum, PublisherFaultCorroborationDetector, type PublisherFaultCorroborationDetectorConfig, PublisherFaultTypes, type QualityLimitationDurations, RESOLVED_ISSUE_SUFFIX, RTT_HISTOGRAM_BUCKET_WIDTH_IN_MS, RTT_HISTOGRAM_NUMBER_OF_BUCKETS, type RemoteInboundRtpStats, type RemoteOutboundRtpStats, type RemoteTrackLinkEvidence, RemoteTrackResolver, type RemoteTrackResolverFactory, type RemoteTrackResolverReportPayload, RemoteTrackResolverValidator, type RemoteTrackResolverValidatorConfig, type RemoteTrackResolvers, type ResolvedActiveClientIssue, type RttSummary, type RunningValidator, SCORE_HISTOGRAM_BUCKET_WIDTH, SCORE_HISTOGRAM_NUMBER_OF_BUCKETS, type SampleRejectedReason, type SaveClientSamplesConfig, type ScoreCalculator, type ScoreSummary, SfuCongestionDetector, type SfuCongestionDetectorBucket, type SfuCongestionDetectorConfig, type SfuCongestionDetectorEvaluation, type SfuCongestionDetectorReport, type SimulcastReceiverEvidence, type SimulcastReceiverReportPayload, SimulcastReceiverValidator, type SimulcastReceiverValidatorConfig, type Sink, type SinkConstructor, type SinkCreateSettings, type SinkDestination, type SinkEvents, SinkRegistry, type SinkSettings, SlidingWindow, type SlidingWindowEntry, type StatsSummary, TrackDeliveryMismatchDetector, type TrackDeliveryMismatchDetectorConfig, TrackDeliveryMismatchTypes, TrendTester, type TrendTesterConfig, type TurnServerHealth, TurnServerHealthDetector, type TurnServerHealthDetectorConfig, TurnServerHealthTypes, TurnServerOutageDetector, type TurnServerOutageDetectorConfig, TurnServerOutageTypes, UNRESOLVED_TRACK_LINKS_ISSUE, UnconsumedTrackDetector, type UnconsumedTrackDetectorConfig, UnconsumedTrackTypes, type ValidationReport, type Validator, type ValidatorName, baseIssueType, concludeCallIssue, concludeObserverIssue, correlation, counterDelta, createCallSummary, createDefaultMediasoupRemoteTrackResolverFactory, createInMemorySink, createJsonlFileSink, createLogger, createP2pRemoteTrackResolverFactory, defaultCallSummaryConfig, defaultClientHealthThresholds, geohash, isClientIssueResolutionEntry, isPublicIpAddress, issuePayloadAsString, mannKendall, mannKendallVerdict, median, medianAbsoluteDeviation, pageHinkley, percentile, percentileOfHistogram, percentileOfSorted, robustZScore, schemaVersion, setObserverLogger, summarize };
package/dist/index.js CHANGED
@@ -178,6 +178,46 @@ function setObserverLogger(logger13) {
178
178
  // src/ObservedCall.ts
179
179
  var import_node_events4 = require("events");
180
180
 
181
+ // src/sinks/JsonlFileSink.ts
182
+ var fs = __toESM(require("fs"));
183
+ var import_node_events = require("events");
184
+ var JsonlFileSink = class extends import_node_events.EventEmitter {
185
+ id;
186
+ /** The file this sink writes to. Read it (e.g. in a `close` handler) to upload/move the file. */
187
+ path;
188
+ _stream;
189
+ _serializeSample;
190
+ _ended = false;
191
+ constructor(options) {
192
+ super();
193
+ this.id = options.id ?? options.path;
194
+ this.path = options.path;
195
+ this._stream = fs.createWriteStream(this.path, { flags: "a" });
196
+ this._stream.on("close", () => this.emit("close"));
197
+ this._stream.on("finish", () => this.emit("finish"));
198
+ this._stream.on("drain", () => this.emit("drain"));
199
+ this._stream.on("error", (err) => this.emit("error", err));
200
+ this._serializeSample = options.serializeSample ?? ((record) => JSON.stringify(record));
201
+ }
202
+ get ended() {
203
+ return this._ended;
204
+ }
205
+ write(record) {
206
+ if (this._ended) return false;
207
+ return this._stream.write(`${this._serializeSample(record)}
208
+ `);
209
+ }
210
+ end() {
211
+ if (this._ended) return;
212
+ this._ended = true;
213
+ this.emit("ended");
214
+ this._stream.end();
215
+ }
216
+ };
217
+ function createJsonlFileSink(options) {
218
+ return new JsonlFileSink(options);
219
+ }
220
+
181
221
  // src/sinks/SinkRegistry.ts
182
222
  var import_uuid = require("uuid");
183
223
  var logger = createLogger("SinkRegistry");
@@ -189,6 +229,8 @@ var SinkRegistry = class {
189
229
  // This registry's sinks and its descendants'.
190
230
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
191
231
  _sinks = /* @__PURE__ */ new Map();
232
+ // The ids of the sinks added to this registry itself (not to a descendant).
233
+ _ownIds = /* @__PURE__ */ new Set();
192
234
  /** Number of open sinks in this registry and its descendants. */
193
235
  get size() {
194
236
  return this._sinks.size;
@@ -217,17 +259,48 @@ var SinkRegistry = class {
217
259
  return void 0;
218
260
  }
219
261
  const sink = new SinkClass({ ...settings, id });
220
- const onError = (err) => logger.warn("Sink %s failed: %o", id, err);
221
- sink.on("error", onError);
222
- sink.once("close", () => sink.off("error", onError));
223
- sink.once("ended", () => {
224
- for (const registry of lineage) {
225
- if (registry._sinks.get(id) === sink) registry._sinks.delete(id);
226
- }
227
- });
228
- for (const registry of lineage) registry._sinks.set(id, sink);
262
+ this._register(sink, lineage);
263
+ return sink;
264
+ }
265
+ /**
266
+ * Register a sink constructed elsewhere, under its own `id`. Returns `false`, registering nothing,
267
+ * when the id is already in use in the observer (the sink is then the caller's to end). Prefer
268
+ * `create` when the sink opens a resource on construction: it checks the id first.
269
+ */
270
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
271
+ add(sink) {
272
+ const lineage = this._lineage();
273
+ if (lineage[lineage.length - 1]._sinks.has(sink.id)) {
274
+ logger.warn("Sink id %s is already in use; sink ids must be unique per observer. The sink was not added", sink.id);
275
+ return false;
276
+ }
277
+ this._register(sink, lineage);
278
+ return true;
279
+ }
280
+ /**
281
+ * Register the sink a `SinkDestination` describes, under `id`: for `jsonlPath`, a `JsonlFileSink`
282
+ * is created (the id is checked before the file is opened); a `sink` is added, and its `id` must
283
+ * be `id`. Returns `undefined` when the id is taken, the ids differ, or both are given (possible
284
+ * from JavaScript); a given `sink` that is not registered is ended.
285
+ */
286
+ createFromDestination(id, destination) {
287
+ const { jsonlPath, sink } = destination;
288
+ const refuse = (reason, ...args) => {
289
+ logger.warn(`Sink ${id}: ${reason}`, ...args);
290
+ sink?.end();
291
+ return void 0;
292
+ };
293
+ if (jsonlPath !== void 0 && sink) return refuse("give either jsonlPath or sink, not both");
294
+ if (jsonlPath !== void 0) return this.create(JsonlFileSink, { id, path: jsonlPath });
295
+ if (!sink) return void 0;
296
+ if (sink.id !== id) return refuse("the sink id %s must be %s; it is ended", sink.id, id);
297
+ if (!this.add(sink)) return refuse("the id is already in use; the sink is ended");
229
298
  return sink;
230
299
  }
300
+ /** Whether the sink `id` was added to this registry itself, rather than to a descendant. */
301
+ owns(id) {
302
+ return this._ownIds.has(id);
303
+ }
231
304
  /** Write `record` to the sink `id`. `false` when there is no such sink, or on backpressure. */
232
305
  write(id, record) {
233
306
  const sink = this._sinks.get(id);
@@ -239,15 +312,30 @@ var SinkRegistry = class {
239
312
  }
240
313
  /** End every sink of this registry and of its descendants. */
241
314
  clear() {
242
- for (const sink of [...this._sinks.values()]) {
315
+ for (const [id, sink] of [...this._sinks.entries()]) {
243
316
  try {
244
317
  sink.end();
245
318
  } catch (err) {
246
- logger.warn("Sink %s failed to end: %o", sink.id, err);
319
+ logger.warn("Sink %s failed to end: %o", id, err);
247
320
  }
248
321
  }
249
322
  }
250
323
  /** This registry and its ancestors, up to the root. */
324
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
325
+ _register(sink, lineage) {
326
+ const id = sink.id;
327
+ const onError = (err) => logger.warn("Sink %s failed: %o", id, err);
328
+ sink.on("error", onError);
329
+ sink.once("close", () => sink.off("error", onError));
330
+ sink.once("ended", () => {
331
+ if (this._sinks.get(id) === sink) this._ownIds.delete(id);
332
+ for (const registry of lineage) {
333
+ if (registry._sinks.get(id) === sink) registry._sinks.delete(id);
334
+ }
335
+ });
336
+ this._ownIds.add(id);
337
+ for (const registry of lineage) registry._sinks.set(id, sink);
338
+ }
251
339
  _lineage() {
252
340
  return [this, ...this.parent?._lineage() ?? []];
253
341
  }
@@ -278,19 +366,20 @@ var ExtensionStatsRegistry = class {
278
366
  }
279
367
  }
280
368
  create(settings) {
281
- if (this.closed) {
282
- logger2.warn("Attempted to create extension stats %s on closed %s", settings.id, this.id);
369
+ const { id, jsonlPath, sink } = settings;
370
+ const refuse = (reason) => {
371
+ logger2.warn("Extension stats %s on %s: %s", id, this.id, reason);
372
+ sink?.end();
283
373
  return void 0;
284
- }
285
- if (this.values.has(settings.id)) {
286
- logger2.warn("Extension stats with id %s already exists on %s", settings.id, this.id);
287
- return void 0;
288
- }
289
- this.values.set(settings.id, void 0);
290
- if (!settings.createSink) return void 0;
291
- const [SinkClass, sinkSettings] = settings.createSink;
292
- this._sinks.get(settings.id)?.end();
293
- return this._sinks.create(SinkClass, { ...sinkSettings, id: settings.id });
374
+ };
375
+ if (this.closed) return refuse("the owner is closed");
376
+ if (this.values.has(id)) return refuse("the id already exists");
377
+ if (jsonlPath !== void 0 && sink) return refuse("give either jsonlPath or sink, not both");
378
+ if (sink && sink.id !== id) return refuse(`the sink id ${sink.id} differs from the extension stats id`);
379
+ this.values.set(id, void 0);
380
+ if (jsonlPath === void 0 && !sink) return void 0;
381
+ if (this._sinks.owns(id)) this._sinks.get(id)?.end();
382
+ return this._sinks.createFromDestination(id, settings);
294
383
  }
295
384
  accept(id, data) {
296
385
  if (this.closed) {
@@ -309,46 +398,6 @@ var ExtensionStatsRegistry = class {
309
398
  }
310
399
  };
311
400
 
312
- // src/sinks/JsonlFileSink.ts
313
- var fs = __toESM(require("fs"));
314
- var import_node_events = require("events");
315
- var JsonlFileSink = class extends import_node_events.EventEmitter {
316
- id;
317
- /** The file this sink writes to. Read it (e.g. in a `close` handler) to upload/move the file. */
318
- path;
319
- _stream;
320
- _serializeSample;
321
- _ended = false;
322
- constructor(options) {
323
- super();
324
- this.id = options.id ?? options.path;
325
- this.path = options.path;
326
- this._stream = fs.createWriteStream(this.path, { flags: "a" });
327
- this._stream.on("close", () => this.emit("close"));
328
- this._stream.on("finish", () => this.emit("finish"));
329
- this._stream.on("drain", () => this.emit("drain"));
330
- this._stream.on("error", (err) => this.emit("error", err));
331
- this._serializeSample = options.serializeSample ?? ((record) => JSON.stringify(record));
332
- }
333
- get ended() {
334
- return this._ended;
335
- }
336
- write(record) {
337
- if (this._ended) return false;
338
- return this._stream.write(`${this._serializeSample(record)}
339
- `);
340
- }
341
- end() {
342
- if (this._ended) return;
343
- this._ended = true;
344
- this.emit("ended");
345
- this._stream.end();
346
- }
347
- };
348
- function createJsonlFileSink(options) {
349
- return new JsonlFileSink(options);
350
- }
351
-
352
401
  // src/ObservedClient.ts
353
402
  var import_node_events3 = require("events");
354
403
 
@@ -3209,7 +3258,7 @@ var ObservedClient = class extends import_node_events3.EventEmitter {
3209
3258
  /** The distinct `publicAddress`es currently known by this client's open peer connections. */
3210
3259
  /**
3211
3260
  * The sink this client's samples are written to: whatever is registered under its client id in
3212
- * `call.sinks` (the `createClientJsonlSink` file, or a sink the application added), if any.
3261
+ * `call.sinks` (the one created through `saveClientSamples`, or a sink the application added), if any.
3213
3262
  */
3214
3263
  get sink() {
3215
3264
  return this.call.sinks.get(this.clientId);
@@ -5312,17 +5361,11 @@ var ObservedCall = class extends import_node_events4.EventEmitter {
5312
5361
  clearTimeout(this.closeTimer);
5313
5362
  this.closeTimer = void 0;
5314
5363
  }
5315
- const jsonlPath = this.observer.config.createClientJsonlSink?.({ observedCall: this, clientId: settings.clientId });
5316
- let jsonlSink;
5317
- if (jsonlPath !== void 0 && this.observer.sinks.get(settings.clientId)) {
5318
- logger6.warn("No sample file for client %s in call %s: sink id %s is already in use", settings.clientId, this.callId, settings.clientId);
5319
- } else if (jsonlPath !== void 0) {
5320
- jsonlSink = this.sinks.create(JsonlFileSink, { id: settings.clientId, path: jsonlPath });
5321
- }
5364
+ const clientSink = this._createClientSink(settings.clientId);
5322
5365
  this.emit("newclient", result);
5323
5366
  this._notify("client-added", { ...this.eventScope, observedClient: result });
5324
- if (jsonlSink) {
5325
- this._notify("client-sink-created", { ...this.eventScope, observedClient: result, sink: jsonlSink });
5367
+ if (clientSink) {
5368
+ this._notify("client-sink-created", { ...this.eventScope, observedClient: result, sink: clientSink });
5326
5369
  }
5327
5370
  if (wasEmpty) {
5328
5371
  this.emit("not-empty");
@@ -5368,6 +5411,25 @@ var ObservedCall = class extends import_node_events4.EventEmitter {
5368
5411
  if (!client.leftAt) return;
5369
5412
  this.endedAt = Math.max(this.endedAt ?? client.leftAt, client.leftAt);
5370
5413
  }
5414
+ _createClientSink(clientId) {
5415
+ const config = this.observer.config.saveClientSamples;
5416
+ if (!config || !config.jsonlPath && !config.sink) return void 0;
5417
+ if (config.jsonlPath && config.sink) {
5418
+ logger6.warn("saveClientSamples: give either jsonlPath or sink, not both; no client samples are saved");
5419
+ return void 0;
5420
+ }
5421
+ if (this.observer.sinks.get(clientId)) {
5422
+ logger6.warn("No sample sink for client %s in call %s: sink id %s is already in use", clientId, this.callId, clientId);
5423
+ return void 0;
5424
+ }
5425
+ const params = { id: clientId, observedCall: this, clientId };
5426
+ if (config.jsonlPath) {
5427
+ const jsonlPath = config.jsonlPath(params);
5428
+ return jsonlPath === void 0 ? void 0 : this.sinks.createFromDestination(clientId, { jsonlPath });
5429
+ }
5430
+ const sink = config.sink?.(params);
5431
+ return sink ? this.sinks.createFromDestination(clientId, { sink }) : void 0;
5432
+ }
5371
5433
  /** Emit an Observer-bus event scoped to this call. */
5372
5434
  _notify(type, ...args) {
5373
5435
  this.observer.emit(type, ...args);