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

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
@@ -404,7 +404,7 @@ See [Mediasoup router observation](#mediasoup-router-observation) for the full d
404
404
  | Event | Extra | Fires when |
405
405
  |-------|-------|-----------|
406
406
  | `client-added` | — | a client is created |
407
- | `client-sink-created` | `{ sink: JsonlFileSink }` | the client's JSONL sample file was created (only when `createClientJsonlSink` returns a path); fires right after `client-added` |
407
+ | `client-sink-created` | `{ sink: Sink<ClientSample> }` | the client's sample sink was created by `createClientSink`; fires right after `client-added` |
408
408
  | `client-updated` | `{ sample: ClientSample, elapsedTimeInMs: number, context?: AcceptContext }` | the client processed a sample |
409
409
  | `client-closed` | — | the client closed |
410
410
  | `client-joined` | — | first `CLIENT_JOINED` event seen |
@@ -480,7 +480,7 @@ type ObserverConfig<AppData = Record<string, unknown>> = {
480
480
  createCallAppData?: (p: { callId: string; observer: Observer; acceptCtx?: AcceptContext }) => Record<string, unknown>;
481
481
  createClientAppData?: (p: { clientId: string; observedCall: ObservedCall; acceptCtx?: AcceptContext }) => Record<string, unknown>;
482
482
  // a JSONL file per client: return its path, or undefined for none (see Sinks).
483
- createClientJsonlSink?: (p: { clientId: string; observedCall: ObservedCall }) => string | undefined;
483
+ createClientSink?: (p: { id: string; clientId: string; observedCall: ObservedCall }) => SinkDestination<ClientSample> | undefined;
484
484
  // remote-track-resolver factory — produces a call's RemoteTrackResolver (see Remote track resolution).
485
485
  createRemoteTrackResolver?: (observedCall: ObservedCall) => RemoteTrackResolver | undefined;
486
486
  };
@@ -1929,6 +1929,9 @@ that are still open, since the observer does not close mediasoup routers.
1929
1929
  | Method | Does |
1930
1930
  |--------|------|
1931
1931
  | `create(SinkClass, settings)` | constructs `new SinkClass(settings)` and registers it under `settings.id` (generated when omitted); `undefined` if the id is taken |
1932
+ | `add(sink): boolean` | registers a sink constructed elsewhere under its own `id`; `false` if the id is taken (the sink stays yours to end). Prefer `create` for sinks that open a resource on construction: it checks the id first |
1933
+ | `createFromDestination(id, destination)` | registers the sink a `SinkDestination` describes under `id`: `{ jsonlPath }` creates a `JsonlFileSink` (the id is checked first), `{ sink }` adds your sink (its `id` must be `id`). `undefined` if refused; a refused `sink` is ended |
1934
+ | `owns(id): boolean` | whether `id` was added to this registry itself, not to a descendant |
1932
1935
  | `get<T>(id)` | the sink, if any (own or a descendant's) |
1933
1936
  | `write(id, record): boolean` | writes to the sink; `false` if there is none, or on backpressure |
1934
1937
  | `clear()` | ends every sink of the registry and its descendants |
@@ -1974,21 +1977,31 @@ its id — refuse writes after it, and emit `close` / `error` for its destinatio
1974
1977
  ### Client samples
1975
1978
 
1976
1979
  A client writes every accepted sample to the sink registered under its **client id** in its call's
1977
- registry (`observedClient.sink` returns it), and removes — ends — that sink when it closes. For the
1978
- common case, a JSONL file per client, return a path from `createClientJsonlSink`:
1980
+ registry (`observedClient.sink` returns it), and ends that sink when it closes. `createClientSink`
1981
+ decides it, per client, by returning a **`SinkDestination`** — exactly one of `jsonlPath` and `sink`:
1982
+
1983
+ | Returns | Sink |
1984
+ |---------|------|
1985
+ | `{ jsonlPath }` | a JSONL file at that path: a `JsonlFileSink` created by the library under the client id |
1986
+ | `{ sink }` | your `Sink<ClientSample>`; its `id` must be `params.id`, the client id |
1987
+ | `undefined` | none |
1979
1988
 
1980
1989
  ```ts
1981
1990
  const observer = new Observer({
1982
- // return undefined for no file
1983
- createClientJsonlSink: ({ observedCall, clientId }) => `./stats/${observedCall.callId}__${clientId}.jsonl`,
1991
+ createClientSink: ({ observedCall, clientId }) => ({ jsonlPath: `./stats/${observedCall.callId}__${clientId}.jsonl` }),
1984
1992
  });
1985
1993
 
1986
1994
  observer.on('client-sink-created', ({ observedClient, sink }) => {
1987
- sink.once('close', () => uploadFile(sink.path)); // flushed & fd closed → ready
1995
+ if (sink instanceof JsonlFileSink) sink.once('close', () => uploadFile(sink.path)); // flushed & fd closed → ready
1988
1996
  });
1989
1997
  ```
1990
1998
 
1991
- For any other destination, create your own sink under the client id when the client is created:
1999
+ `createClientSink` is called only when the client id is free as a sink id, so a clash opens no file.
2000
+ A returned sink with a different id is ended and not used. Giving both `jsonlPath` and `sink` is a
2001
+ compile error (and refused at runtime). For a JSONL file with a custom serializer, return
2002
+ `{ sink: new JsonlFileSink({ id, path, serializeSample }) }`.
2003
+
2004
+ Any other destination works the same way — return your own implementation:
1992
2005
 
1993
2006
  ```ts
1994
2007
  class HttpSink extends EventEmitter implements ClientSampleSink {
@@ -2017,9 +2030,12 @@ class HttpSink extends EventEmitter implements ClientSampleSink {
2017
2030
  }
2018
2031
  }
2019
2032
 
2020
- observer.on('client-added', ({ observedCall, observedClient }) => {
2021
- observedCall.sinks.create(HttpSink, { id: observedClient.clientId, url: `https://stats.example.com/${observedClient.clientId}` });
2033
+ new Observer({
2034
+ createClientSink: ({ id, clientId }) => ({ sink: new HttpSink({ id, url: `https://stats.example.com/${clientId}` }) }),
2022
2035
  });
2036
+
2037
+ // Or later, on `client-added`, if the client has no sink yet:
2038
+ // observedCall.sinks.create(HttpSink, { id: observedClient.clientId, url })
2023
2039
  ```
2024
2040
 
2025
2041
  Client ids double as sink ids, so they must be unique across the observer's open calls (client
@@ -2037,8 +2053,11 @@ the call) or `observedMediasoupRouter` (a pipe transport's `getStats()`): the la
2037
2053
  optionally a sink that receives every data. The API is the same on all three; a call is shown.
2038
2054
 
2039
2055
  ```ts
2040
- // Register the id; with `createSink` (what `sinks.create` takes), a sink is created under the same id.
2041
- call.createExtensionStats({ id: `${call.callId}:sfu`, createSink: [ JsonlFileSink<SfuStats>, { path: './stats/sfu.jsonl' } ] });
2056
+ // Register the id, with a JSONL file that receives every data accepted for it.
2057
+ call.createExtensionStats({ id: `${call.callId}:sfu`, jsonlPath: './stats/sfu.jsonl' });
2058
+
2059
+ // Or with a sink of your own; its id must be the extension stats id.
2060
+ call.createExtensionStats({ id: `${call.callId}:metrics`, sink: new HttpSink({ id: `${call.callId}:metrics`, url }) });
2042
2061
 
2043
2062
  // Your own polling: keep the last data, and write it to the sink with the same id, if any.
2044
2063
  call.acceptExtensionStats(`${call.callId}:sfu`, await collectSfuStats(call.callId));
@@ -2049,7 +2068,7 @@ call.extensionStats.values; // Map<id, last data>
2049
2068
 
2050
2069
  | Method | Behaviour |
2051
2070
  |--------|-----------|
2052
- | `createExtensionStats({ id, createSink? })` | Registers `id`. With `createSink` — what `sinks.create` takes, `[SinkClass, settings]` — it also creates a sink under `id` (an `id` in the sink settings is ignored), ending and replacing a sink the owner already holds under `id`. Returns the created sink, or `undefined`: without `createSink`, on a closed owner, when `id` is already in `extensionStats.values` (created or accepted before), or when another object of the observer holds a sink with this id |
2071
+ | `createExtensionStats({ id, jsonlPath? \| sink? })` | Registers `id`, optionally with a sink — at most one of: `jsonlPath`, the path of a JSONL file, for which a `JsonlFileSink` is created under `id`; or `sink`, a `Sink` of your own, whose `id` must be `id`. Either ends and replaces a sink the owner itself already holds under `id`. Returns the sink, or `undefined`: with neither, on a closed owner, when `id` is already in `extensionStats.values` (created or accepted before), with a `sink` whose id differs, or when another object of the observer holds a sink with this id. Giving both is a compile error (refused at runtime). A refused `sink` is ended; for `jsonlPath`, the id is checked before the file is opened |
2053
2072
  | `acceptExtensionStats(id, data)` | Keeps `data` as the last data of `id` and writes it to the owner's sink with the same id, if there is one (a sink that throws is logged). `id` does not have to be created first. Dropped once the owner is closed |
2054
2073
  | `getExtensionStats<T>(id)` | The last data accepted for `id`, or `undefined` |
2055
2074
  | `extensionStats` | The owner's `ExtensionStatsRegistry`: `values` (`Map<id, last data>`), `closed`, `close()`, and `create` / `accept` / `get`, which the three methods above delegate to |
@@ -2057,12 +2076,12 @@ call.extensionStats.values; // Map<id, last data>
2057
2076
  **Extension stats ids share the sink id namespace, so they must be unique per observer**: the sink
2058
2077
  of extension stats `x` is the sink with id `x`. Consequently:
2059
2078
 
2060
- - With `createSink`, a sink the owner already holds under the id is ended and replaced. If another
2061
- object of the observer holds it, no sink is created and the data is only kept.
2062
- - Without `createSink`, a sink you create later under the id (`sinks.create(SinkClass, { id, ... })`)
2063
- receives the data accepted from then on.
2064
- - An id accepted before it was created can no longer be created with `createSink`; give it a sink
2065
- with `sinks.create` instead.
2079
+ - With `jsonlPath` or `sink`, a sink the owner itself already holds under the id is ended and replaced. A sink another object holds under it is never touched: no sink is created and the data
2080
+ is only kept.
2081
+ - With neither, a sink you add later under the id (`sinks.create(SinkClass, { id, ... })` or
2082
+ `sinks.add(sink)`) receives the data accepted from then on.
2083
+ - An id accepted before it was created can no longer be created with a sink; give it one with
2084
+ `sinks.create` / `sinks.add` instead.
2066
2085
 
2067
2086
  **Lifecycle.** When the owner (the call, the router or the observer) closes, it closes its
2068
2087
  `extensionStats`: data accepted afterwards is dropped, `createExtensionStats` returns `undefined`,
package/dist/index.d.mts CHANGED
@@ -48,6 +48,17 @@ 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
+ };
51
62
 
52
63
  /** A sink class `SinkRegistry.create` can construct: its constructor takes one settings object (with `id`). */
53
64
  type SinkConstructor = new (settings: any) => Sink<any>;
@@ -70,6 +81,7 @@ type SinkCreateSettings<C extends SinkConstructor> = Omit<ConstructorParameters<
70
81
  declare class SinkRegistry {
71
82
  readonly parent?: SinkRegistry | undefined;
72
83
  private readonly _sinks;
84
+ private readonly _ownIds;
73
85
  constructor(parent?: SinkRegistry | undefined);
74
86
  /** Number of open sinks in this registry and its descendants. */
75
87
  get size(): number;
@@ -82,21 +94,41 @@ declare class SinkRegistry {
82
94
  * `undefined` is returned and a warning logged.
83
95
  */
84
96
  create<C extends SinkConstructor>(SinkClass: C, settings: SinkCreateSettings<C>): InstanceType<C> | undefined;
97
+ /**
98
+ * Register a sink constructed elsewhere, under its own `id`. Returns `false`, registering nothing,
99
+ * when the id is already in use in the observer (the sink is then the caller's to end). Prefer
100
+ * `create` when the sink opens a resource on construction: it checks the id first.
101
+ */
102
+ add(sink: Sink<any>): boolean;
103
+ /**
104
+ * Register the sink a `SinkDestination` describes, under `id`: for `jsonlPath`, a `JsonlFileSink`
105
+ * is created (the id is checked before the file is opened); a `sink` is added, and its `id` must
106
+ * be `id`. Returns `undefined` when the id is taken, the ids differ, or both are given (possible
107
+ * from JavaScript); a given `sink` that is not registered is ended.
108
+ */
109
+ createFromDestination<T>(id: string, destination: SinkDestination<T>): Sink<T> | undefined;
110
+ /** Whether the sink `id` was added to this registry itself, rather than to a descendant. */
111
+ owns(id: string): boolean;
85
112
  /** Write `record` to the sink `id`. `false` when there is no such sink, or on backpressure. */
86
113
  write(id: string, record: unknown): boolean;
87
114
  /** End every sink of this registry and of its descendants. */
88
115
  clear(): void;
89
116
  /** This registry and its ancestors, up to the root. */
117
+ private _register;
90
118
  private _lineage;
91
119
  }
92
120
 
93
- /** The settings of `createExtensionStats` on an observer, a call or a mediasoup router. */
94
- type CreateExtensionStatsSettings<C extends SinkConstructor> = {
121
+ /**
122
+ * The settings of `createExtensionStats` on an observer, a call or a mediasoup router: the `id`,
123
+ * and at most one of `jsonlPath` and `sink` (a `SinkDestination`).
124
+ */
125
+ type CreateExtensionStatsSettings<T = unknown> = {
95
126
  /** Unique per observer: it is also the id of the sink that receives the stats. */
96
127
  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
- };
128
+ } & (SinkDestination<T> | {
129
+ jsonlPath?: never;
130
+ sink?: never;
131
+ });
100
132
  /**
101
133
  * The extension stats of one object — `observer.extensionStats`, `observedCall.extensionStats`,
102
134
  * `observedMediasoupRouter.extensionStats`: the last data per id (`values`), written to the
@@ -118,7 +150,7 @@ declare class ExtensionStatsRegistry {
118
150
  * owner's `sinks.clear()`. Called by the owner when it closes.
119
151
  */
120
152
  close(): void;
121
- create<C extends SinkConstructor>(settings: CreateExtensionStatsSettings<C>): InstanceType<C> | undefined;
153
+ create<T = unknown>(settings: CreateExtensionStatsSettings<T>): Sink<T> | undefined;
122
154
  accept(id: string, data: unknown): void;
123
155
  get<T>(id: string): T | undefined;
124
156
  }
@@ -3147,39 +3179,6 @@ declare function isClientIssueResolutionEntry(issue: ClientIssue): boolean;
3147
3179
  /** Strip the `-resolved` suffix, so both entries of a lifecycle share one logical type. */
3148
3180
  declare function baseIssueType(type: string): string;
3149
3181
 
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
3182
  /** mediasoup `TransportTuple` (the selected local/remote address pair of a transport). */
3184
3183
  type TransportTuple = {
3185
3184
  localAddress: string;
@@ -3467,7 +3466,7 @@ declare class ObservedMediasoupRouter<AppData extends Record<string, unknown> =
3467
3466
  */
3468
3467
  snapshot(): MediasoupRouterSample;
3469
3468
  close(): void;
3470
- createExtensionStats<C extends SinkConstructor>(settings: CreateExtensionStatsSettings<C>): InstanceType<C> | undefined;
3469
+ createExtensionStats<T = unknown>(settings: CreateExtensionStatsSettings<T>): Sink<T> | undefined;
3471
3470
  acceptExtensionStats(id: string, data: unknown): void;
3472
3471
  getExtensionStats<T>(id: string): T | undefined;
3473
3472
  addTransport: (transport: types.Transport) => void;
@@ -3565,7 +3564,7 @@ type ObserverEvents = {
3565
3564
  }];
3566
3565
  'client-added': [ObservedClientScope];
3567
3566
  'client-sink-created': [ObservedClientScope & {
3568
- sink: JsonlFileSink;
3567
+ sink: Sink<ClientSample>;
3569
3568
  }];
3570
3569
  'client-updated': [ObservedClientScope & {
3571
3570
  sample: ClientSample;
@@ -4005,7 +4004,7 @@ declare class ObservedClient<AppData extends Record<string, unknown> = Record<st
4005
4004
  /** The distinct `publicAddress`es currently known by this client's open peer connections. */
4006
4005
  /**
4007
4006
  * 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.
4007
+ * `call.sinks` (the one `createClientSink` returned, or a sink the application added), if any.
4009
4008
  */
4010
4009
  get sink(): Sink<ClientSample> | undefined;
4011
4010
  get publicAddresses(): string[];
@@ -5874,7 +5873,7 @@ declare class ObservedCall<AppData extends Record<string, unknown> = Record<stri
5874
5873
  */
5875
5874
  addIssue(issue: Omit<CallIssue, 'scope'>): void;
5876
5875
  close(): void;
5877
- createExtensionStats<C extends SinkConstructor>(settings: CreateExtensionStatsSettings<C>): InstanceType<C> | undefined;
5876
+ createExtensionStats<T = unknown>(settings: CreateExtensionStatsSettings<T>): Sink<T> | undefined;
5878
5877
  acceptExtensionStats(id: string, data: unknown): void;
5879
5878
  getExtensionStats<T>(id: string): T | undefined;
5880
5879
  getObservedClient<ClientAppData extends Record<string, unknown> = Record<string, unknown>>(clientId: string): ObservedClient<ClientAppData> | undefined;
@@ -5885,6 +5884,7 @@ declare class ObservedCall<AppData extends Record<string, unknown> = Record<stri
5885
5884
  private _clientJoined;
5886
5885
  private _clientLeft;
5887
5886
  /** Emit an Observer-bus event scoped to this call. */
5887
+ private _createClientSink;
5888
5888
  private _notify;
5889
5889
  }
5890
5890
 
@@ -6598,38 +6598,22 @@ type ObserverConfig<AppData extends Record<string, unknown> = Record<string, unk
6598
6598
  /** Same as `createCallAppData`, for clients. Receives the (already-created) parent call. */
6599
6599
  createClientAppData?: ClientAppDataFactory;
6600
6600
  /**
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:
6601
+ * The sink a newly-created client writes its samples to: `{ jsonlPath }` (a `JsonlFileSink` is
6602
+ * created at that path under the client id), `{ sink }` (a `Sink` whose `id` is `params.id`, the
6603
+ * client id), or `undefined` for none. The sink is added to `observedCall.sinks` and is ended
6604
+ * when the client closes; `client-sink-created` announces it.
6611
6605
  *
6612
6606
  * ```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
- * });
6607
+ * createClientSink: ({ observedCall, clientId }) => ({ jsonlPath: `./stats/${observedCall.callId}__${clientId}.jsonl` }),
6627
6608
  * ```
6609
+ *
6610
+ * Called only when the client id is free as a sink id, so a clash opens no file.
6628
6611
  */
6629
- createClientJsonlSink?: (params: {
6612
+ createClientSink?: (params: {
6613
+ id: string;
6630
6614
  observedCall: ObservedCall;
6631
6615
  clientId: string;
6632
- }) => string | undefined;
6616
+ }) => SinkDestination<ClientSample> | undefined;
6633
6617
  /**
6634
6618
  * Optional factory invoked when a call is created, producing the call's `RemoteTrackResolver`
6635
6619
  * (or `undefined` for none). Use the built-ins
@@ -6800,7 +6784,7 @@ declare class Observer<AppData extends Record<string, unknown> = Record<string,
6800
6784
  matchPeerConnectionByWebRtcTransportId?: boolean;
6801
6785
  }): ObservedMediasoupRouter<Record<string, unknown>> | undefined;
6802
6786
  close(): void;
6803
- createExtensionStats<C extends SinkConstructor>(settings: CreateExtensionStatsSettings<C>): InstanceType<C> | undefined;
6787
+ createExtensionStats<T = unknown>(settings: CreateExtensionStatsSettings<T>): Sink<T> | undefined;
6804
6788
  acceptExtensionStats(id: string, data: unknown): void;
6805
6789
  getExtensionStats<T>(id: string): T | undefined;
6806
6790
  accept(sample: ClientSample, context?: AcceptContext): void;
@@ -7199,9 +7183,9 @@ declare function setObserverLogger(logger: ObserverLogger): void;
7199
7183
  /** The lifecycle events a client sample sink may emit. Same as {@link SinkEvents}. */
7200
7184
  type ClientSampleSinkEvents = SinkEvents;
7201
7185
  /**
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.
7186
+ * A per-client destination for accepted samples. Return one from `ObserverConfig.createClientSink`
7187
+ * (with the `id` it receives), or create one under the client id with
7188
+ * `observedCall.sinks.create(MySink, { id: clientId, ... })`.
7205
7189
  *
7206
7190
  * The library calls `write(sample)` synchronously per accepted sample (not awaited) and `end()`s
7207
7191
  * the sink when the client closes; the registry attaches an `error` listener so a failing sink
@@ -7210,6 +7194,39 @@ type ClientSampleSinkEvents = SinkEvents;
7210
7194
  */
7211
7195
  type ClientSampleSink = Sink<ClientSample>;
7212
7196
 
7197
+ type JsonlFileSinkOptions<T = ClientSample> = Partial<SinkSettings> & {
7198
+ /** Absolute or relative file path to append JSONL lines to. */
7199
+ path: string;
7200
+ /** Optional custom serializer; defaults to `JSON.stringify`. */
7201
+ serializeSample?: (record: T) => string;
7202
+ };
7203
+ /**
7204
+ * A sink that serializes each record to a JSON line and appends it to a file. Defaults to client
7205
+ * samples, but takes any record type: `sinks.create(JsonlFileSink<PipeTransportStats[]>, { id, path })`. It wraps an
7206
+ * `fs.WriteStream` and re-emits its lifecycle events, so `close` fires once the file is flushed
7207
+ * and its descriptor is closed (file ready) and `error` surfaces file errors.
7208
+ */
7209
+ declare interface JsonlFileSink {
7210
+ on<E extends keyof SinkEvents>(event: E, listener: (...args: SinkEvents[E]) => void): this;
7211
+ once<E extends keyof SinkEvents>(event: E, listener: (...args: SinkEvents[E]) => void): this;
7212
+ off<E extends keyof SinkEvents>(event: E, listener: (...args: SinkEvents[E]) => void): this;
7213
+ emit<E extends keyof SinkEvents>(event: E, ...args: SinkEvents[E]): boolean;
7214
+ }
7215
+ declare class JsonlFileSink<T = ClientSample> extends EventEmitter implements Sink<T> {
7216
+ readonly id: string;
7217
+ /** The file this sink writes to. Read it (e.g. in a `close` handler) to upload/move the file. */
7218
+ readonly path: string;
7219
+ private readonly _stream;
7220
+ private readonly _serializeSample;
7221
+ private _ended;
7222
+ constructor(options: JsonlFileSinkOptions<T>);
7223
+ get ended(): boolean;
7224
+ write(record: T): boolean;
7225
+ end(): void;
7226
+ }
7227
+ /** Build a JSONL file sink for a single path. */
7228
+ declare function createJsonlFileSink<T = ClientSample>(options: JsonlFileSinkOptions<T>): JsonlFileSink<T>;
7229
+
7213
7230
  declare interface InMemorySink {
7214
7231
  on<E extends keyof SinkEvents>(event: E, listener: (...args: SinkEvents[E]) => void): this;
7215
7232
  once<E extends keyof SinkEvents>(event: E, listener: (...args: SinkEvents[E]) => void): this;
@@ -7236,4 +7253,4 @@ declare function createInMemorySink(samples?: ClientSample[]): InMemorySink;
7236
7253
  declare function createDefaultMediasoupRemoteTrackResolverFactory(): RemoteTrackResolverFactory;
7237
7254
  declare function createP2pRemoteTrackResolverFactory(): RemoteTrackResolverFactory;
7238
7255
 
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 };
7256
+ 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 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 };