@observertc/observer-js 1.0.0-beta.4 → 1.0.0-beta.6
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 +133 -65
- package/dist/index.d.mts +104 -126
- package/dist/index.d.ts +2771 -0
- package/dist/index.js +3665 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +232 -355
- package/dist/index.mjs.map +1 -1
- package/package.json +15 -8
package/README.md
CHANGED
|
@@ -15,8 +15,9 @@ and emits a single, unified stream of typed events the application can react to.
|
|
|
15
15
|
> agent) should be able to integrate the library, or develop it further, from this file alone.
|
|
16
16
|
> A companion doc, [`docs/logging.md`](./docs/logging.md), covers logging integration in depth.
|
|
17
17
|
|
|
18
|
-
> **Packaging:**
|
|
19
|
-
>
|
|
18
|
+
> **Packaging:** server-side, **Node.js ≥ 22**, shipped as a **dual ESM + CommonJS** build — so it
|
|
19
|
+
> works whether your project uses `import` (ESM) or `require()` (CommonJS). Everything — including
|
|
20
|
+
> the built-in file sink — is exported from the single `@observertc/observer-js` entry.
|
|
20
21
|
|
|
21
22
|
---
|
|
22
23
|
|
|
@@ -51,22 +52,20 @@ npm install @observertc/observer-js
|
|
|
51
52
|
yarn add @observertc/observer-js
|
|
52
53
|
```
|
|
53
54
|
|
|
54
|
-
**
|
|
55
|
-
|
|
56
|
-
compiled to ESM. Everything is exported from the single `@observertc/observer-js` entry:
|
|
55
|
+
**Server-side, Node.js ≥ 22, dual ESM + CommonJS.** The package ships both module formats, so it
|
|
56
|
+
works the same whether your project is ESM or CommonJS — your import line is unchanged either way:
|
|
57
57
|
|
|
58
58
|
```ts
|
|
59
59
|
import { Observer, ClientSample, createJsonlFileSinkFactory } from '@observertc/observer-js';
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
In an ESM project this resolves to the `.mjs` build; in a CommonJS project (where TypeScript
|
|
63
|
+
compiles your `import` down to `require()`) it resolves to the `.js` build. Everything is exported
|
|
64
|
+
from the single `@observertc/observer-js` entry. Written in TypeScript; ships type declarations for
|
|
65
|
+
both formats (`dist/index.d.ts` for `require`, `dist/index.d.mts` for `import`). Runtime
|
|
63
66
|
dependencies: `@bufbuild/protobuf`, `events`, `uuid`. The library does **not** bundle a logger or
|
|
64
67
|
any transport — see [Logging](#logging).
|
|
65
68
|
|
|
66
|
-
> **Note on `require()`.** Being ESM-only, the package can't be loaded with CommonJS
|
|
67
|
-
> `require('@observertc/observer-js')`; consume it with `import` (or `await import()` from a CJS
|
|
68
|
-
> module). If you need a CommonJS build, a dual ESM+CJS output is a small change — ask.
|
|
69
|
-
|
|
70
69
|
`ClientSample` and friends are re-exported from this package, and are also published as the
|
|
71
70
|
shared schema in [`@observertc/schemas`](https://github.com/observertc/schemas); samples
|
|
72
71
|
produced on the client (e.g. by `@observertc/client-monitor-js`) conform to the same shape.
|
|
@@ -105,9 +104,8 @@ import { Observer, ClientSample } from '@observertc/observer-js';
|
|
|
105
104
|
|
|
106
105
|
// 1. Create an observer.
|
|
107
106
|
const observer = new Observer({
|
|
108
|
-
//
|
|
109
|
-
updatePolicy: 'update-
|
|
110
|
-
updateIntervalInMs: 5000,
|
|
107
|
+
// when the observer aggregates call/client metrics:
|
|
108
|
+
updatePolicy: 'update-when-all-call-updated',
|
|
111
109
|
// default policy applied to calls created automatically by accept():
|
|
112
110
|
defaultCallUpdatePolicy: 'update-on-any-client-updated',
|
|
113
111
|
// optional auto-teardown:
|
|
@@ -178,7 +176,7 @@ client getStats() ──► ClientSample ──► observer.accept(sample, c
|
|
|
178
176
|
|-------|-----------|------------------------|-------|
|
|
179
177
|
| `Observer` | `new Observer(config?)` | — (root) | `observedCalls: Map<string, ObservedCall>`, global counters, the event bus |
|
|
180
178
|
| `ObservedCall` | `observer.createObservedCall(settings)` / lazily by `accept` | `observedCalls` | `observedClients: Map<string, ObservedClient>`, call-wide metrics, `detectors`, `scoreCalculator` |
|
|
181
|
-
| `ObservedClient` | `call.createObservedClient(settings)` / lazily | `observedClients` | `observedPeerConnections: Map<string, ObservedPeerConnection>`, per-client metrics
|
|
179
|
+
| `ObservedClient` | `call.createObservedClient(settings)` / lazily | `observedClients` | `observedPeerConnections: Map<string, ObservedPeerConnection>`, per-client metrics |
|
|
182
180
|
| `ObservedPeerConnection` | lazily, from `sample.peerConnections[]` | `observedPeerConnections` | the 15 sub-stat maps below, transport/RTT/bitrate metrics |
|
|
183
181
|
| Sub-stats | lazily, from the `PeerConnectionSample` | maps on the PC | individual WebRTC stat objects |
|
|
184
182
|
|
|
@@ -207,12 +205,49 @@ fields from the schema plus derived fields (deltas, bitrates).
|
|
|
207
205
|
|
|
208
206
|
The single entry point. It:
|
|
209
207
|
|
|
210
|
-
1. drops + emits `sample-rejected` if the observer is closed
|
|
211
|
-
2.
|
|
212
|
-
3.
|
|
213
|
-
4.
|
|
208
|
+
1. drops + emits `sample-rejected` if the observer is closed;
|
|
209
|
+
2. runs the sample through the **global accept-middleware chain** (see below);
|
|
210
|
+
3. (chain terminal) drops + emits `sample-rejected` if `callId`/`clientId` is missing;
|
|
211
|
+
4. gets or lazily creates the `ObservedCall` and `ObservedClient` (their `appData` comes from the
|
|
212
|
+
configured factories, never from `context`);
|
|
213
|
+
5. delegates to `client.accept(sample, context)`, which fans out to each
|
|
214
214
|
`ObservedPeerConnection.accept(pcSample, context)`.
|
|
215
215
|
|
|
216
|
+
### Accept middlewares (global pre-dispatch hook)
|
|
217
|
+
|
|
218
|
+
`observer.addAcceptMiddleware(...)` registers middlewares run on **every** sample inside
|
|
219
|
+
`accept()`, in order, **before** the sample is dispatched to any call or client. Each middleware
|
|
220
|
+
gets a `{ sample, context }` payload; it can inspect or mutate the sample (set/normalize
|
|
221
|
+
`callId`/`clientId`, enrich, redact) or the context, then call `next(payload)` to continue.
|
|
222
|
+
**Not calling `next` drops the sample** — nothing is created and no event fires. A throwing
|
|
223
|
+
middleware is caught and warns (the sample is dropped), never crashing `accept()`.
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
import { Observer, AcceptMiddleware } from '@observertc/observer-js';
|
|
227
|
+
|
|
228
|
+
const observer = new Observer();
|
|
229
|
+
|
|
230
|
+
// derive callId/clientId from the app's own attachment, before dispatch
|
|
231
|
+
const route: AcceptMiddleware = ({ sample }, next) => {
|
|
232
|
+
sample.callId ??= sample.attachments?.roomId as string;
|
|
233
|
+
sample.clientId ??= sample.attachments?.peerId as string;
|
|
234
|
+
next({ sample });
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
// drop samples from a blocklisted client (never dispatched)
|
|
238
|
+
const filter: AcceptMiddleware = (payload, next) => {
|
|
239
|
+
if (blocked.has(payload.sample.clientId)) return; // no next() => dropped
|
|
240
|
+
next(payload);
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
observer.addAcceptMiddleware(route, filter);
|
|
244
|
+
// observer.removeAcceptMiddleware(route);
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
This is a lightweight global injection point, distinct from the larger (not-yet-built)
|
|
248
|
+
`ClientSampleProcessor` pipeline in the roadmap. When no middleware is registered, `accept()`
|
|
249
|
+
dispatches directly with no overhead.
|
|
250
|
+
|
|
216
251
|
### `context` (the `AcceptContext`)
|
|
217
252
|
|
|
218
253
|
```ts
|
|
@@ -260,9 +295,9 @@ These return `undefined` (and warn) when the parent is closed; `createObservedCa
|
|
|
260
295
|
## Update policies
|
|
261
296
|
|
|
262
297
|
"Update" means *recompute aggregated metrics and emit the `*-updated` event* at that level.
|
|
263
|
-
Both the observer and each call have a configurable trigger.
|
|
264
|
-
|
|
265
|
-
|
|
298
|
+
Both the observer and each call have a configurable trigger. Updates are **event-driven** — there
|
|
299
|
+
is no built-in timer. An app that wants a fixed cadence can call `observer.update()` /
|
|
300
|
+
`call.update()` from its own `setInterval`.
|
|
266
301
|
|
|
267
302
|
**Observer-level** (`ObserverConfig.updatePolicy`, default `update-when-all-call-updated`):
|
|
268
303
|
|
|
@@ -270,7 +305,6 @@ runtime a missing interval warns and falls back rather than throwing.
|
|
|
270
305
|
|--------|-------------------------------------|
|
|
271
306
|
| `update-on-any-call-updated` | any call updates |
|
|
272
307
|
| `update-when-all-call-updated` | every call has updated since the last observer update |
|
|
273
|
-
| `update-on-interval` | a timer fires (`updateIntervalInMs` required) |
|
|
274
308
|
|
|
275
309
|
**Call-level** (`ObservedCallSettings.updatePolicy`, defaulted from
|
|
276
310
|
`ObserverConfig.defaultCallUpdatePolicy`):
|
|
@@ -279,7 +313,6 @@ runtime a missing interval warns and falls back rather than throwing.
|
|
|
279
313
|
|--------|--------------------------------|
|
|
280
314
|
| `update-on-any-client-updated` | any client in the call updates |
|
|
281
315
|
| `update-when-all-client-updated` | every client has updated since the last call update |
|
|
282
|
-
| `update-on-interval` | a timer fires (`updateIntervalInMs` required) |
|
|
283
316
|
|
|
284
317
|
---
|
|
285
318
|
|
|
@@ -354,7 +387,6 @@ additional field(s) on top of that scope.
|
|
|
354
387
|
| `client-metadata` | `{ metaData: ClientMetaData }` | a client meta item arrived |
|
|
355
388
|
| `client-extension-stats` | `{ extensionStats: ExtensionStat }` | an app-defined extension stat arrived |
|
|
356
389
|
| `client-event` | `{ event: ClientEvent }` | any client event was processed |
|
|
357
|
-
| `client-track-report` | `{ report: TrackReport }` | a track was removed; final per-track report |
|
|
358
390
|
|
|
359
391
|
#### Peer-connection level — scope `{ observer, observedCall, observedClient, observedPeerConnection }`
|
|
360
392
|
|
|
@@ -404,12 +436,9 @@ listen to them, but prefer the bus equivalents above for application logic.
|
|
|
404
436
|
```ts
|
|
405
437
|
new Observer<AppData>(config?: ObserverConfig<AppData>)
|
|
406
438
|
|
|
407
|
-
type ObserverConfig<AppData = Record<string, unknown>> =
|
|
408
|
-
|
|
409
|
-
| { updatePolicy?: 'update-on-any-call-updated' | 'update-when-all-call-updated'; updateIntervalInMs?: number }
|
|
410
|
-
) & {
|
|
439
|
+
type ObserverConfig<AppData = Record<string, unknown>> = {
|
|
440
|
+
updatePolicy?: 'update-on-any-call-updated' | 'update-when-all-call-updated';
|
|
411
441
|
defaultCallUpdatePolicy?: ObservedCallSettings['updatePolicy'];
|
|
412
|
-
defaultCallUpdateIntervalInMs?: number;
|
|
413
442
|
appData?: AppData;
|
|
414
443
|
closeClientIfIdleForMs?: number;
|
|
415
444
|
closeCallIfEmptyForMs?: number;
|
|
@@ -419,6 +448,8 @@ type ObserverConfig<AppData = Record<string, unknown>> =
|
|
|
419
448
|
createClientAppData?: (p: { clientId: string; observedCall: ObservedCall }) => Record<string, unknown>;
|
|
420
449
|
// sink factory — produces a per-client sink that receives every accepted sample (see Sinks).
|
|
421
450
|
createClientSink?: (p: { clientId: string; observedCall: ObservedCall }) => ClientSampleSink | undefined;
|
|
451
|
+
// track-resolver factory — produces a call's RemoteTrackResolver (see Remote track resolution).
|
|
452
|
+
createTrackResolver?: (observedCall: ObservedCall) => RemoteTrackResolver | undefined;
|
|
422
453
|
};
|
|
423
454
|
```
|
|
424
455
|
|
|
@@ -439,6 +470,7 @@ const observer = new Observer({
|
|
|
439
470
|
Key members:
|
|
440
471
|
|
|
441
472
|
- `accept(sample: ClientSample, context?: AcceptContext): void`
|
|
473
|
+
- `addAcceptMiddleware(...mw: AcceptMiddleware[]): this` / `removeAcceptMiddleware(...mw): this` — global pre-dispatch sample hooks (see [Accept middlewares](#accept-middlewares-global-pre-dispatch-hook))
|
|
442
474
|
- `getObservedCall<T>(callId): ObservedCall<T> | undefined`
|
|
443
475
|
- `createObservedCall<T>(settings): ObservedCall<T> | undefined`
|
|
444
476
|
- `getOrCreateObservedCall<T>(settings): ObservedCall<T> | undefined`
|
|
@@ -455,13 +487,10 @@ Key members:
|
|
|
455
487
|
### `ObservedCall`
|
|
456
488
|
|
|
457
489
|
```ts
|
|
458
|
-
type ObservedCallSettings<AppData = Record<string, unknown>> =
|
|
459
|
-
|
|
460
|
-
| { updatePolicy?: 'update-on-any-client-updated' | 'update-when-all-client-updated'; updateIntervalInMs?: number }
|
|
461
|
-
) & {
|
|
490
|
+
type ObservedCallSettings<AppData = Record<string, unknown>> = {
|
|
491
|
+
updatePolicy?: 'update-on-any-client-updated' | 'update-when-all-client-updated';
|
|
462
492
|
callId: string;
|
|
463
493
|
appData?: AppData;
|
|
464
|
-
remoteTrackResolvePolicy?: 'p2p' | 'mediasoup-sfu' | 'none';
|
|
465
494
|
closeCallIfEmptyForMs?: number;
|
|
466
495
|
};
|
|
467
496
|
```
|
|
@@ -474,7 +503,7 @@ Key members:
|
|
|
474
503
|
- `addIssue(issue: ClientIssue): void` — raise a **call-level** issue → emits `call-issue`
|
|
475
504
|
- `readonly detectors: Detectors` — server-side detector registry (empty by default; see [Detectors](#detectors-server-side-extension-point))
|
|
476
505
|
- `scoreCalculator: ScoreCalculator`, `get score()`, `readonly calculatedScore`
|
|
477
|
-
- `remoteTrackResolver?: RemoteTrackResolver`
|
|
506
|
+
- `remoteTrackResolver?: RemoteTrackResolver` — set from `ObserverConfig.createTrackResolver` at call creation (see [Remote track resolution](#remote-track-resolution-mediasoup--sfu))
|
|
478
507
|
- aggregates: `numberOfIssues`, `numberOfPeerConnections`, `numberOfInboundRtpStreams`,
|
|
479
508
|
`numberOfOutboundRtpStreams`, `numberOfDataChannels`, `maxNumberOfClients`,
|
|
480
509
|
`clientsUsedTurn: Set<string>`, `startedAt?`, `endedAt?`, `closedAt?`, `closed`
|
|
@@ -508,7 +537,6 @@ Key members:
|
|
|
508
537
|
- Per-tick deltas: `deltaReceivedAudioBytes`, `deltaSentAudioBytes`, … (see source for the full set)
|
|
509
538
|
- Lifecycle: `joinedAt?`, `leftAt?`, `closedAt?`, `closed`, `get score()`
|
|
510
539
|
- Metadata: `browser?`, `engine?`, `platform?`, `operationSystem?`, `mediaDevices`, `mediaConstraints`
|
|
511
|
-
- `readonly report: ClientReport` — cumulative report (byte/packet totals + RTT & score distributions)
|
|
512
540
|
- `accept(sample, context?)`, `close()`
|
|
513
541
|
|
|
514
542
|
### `ObservedPeerConnection`
|
|
@@ -589,9 +617,6 @@ mediasoup set `PRODUCER_*` / `CONSUMER_*` / `DATA_PRODUCER_*` / `DATA_CONSUMER_*
|
|
|
589
617
|
`MEDIA_DEVICES_SUPPORTED_CONSTRAINTS`, `USER_MEDIA_ERROR`, `LOCAL_SDP`, `OPERATION_SYSTEM`,
|
|
590
618
|
`ENGINE`, `PLATFORM`, `BROWSER`.
|
|
591
619
|
|
|
592
|
-
`Reports` exported: `ClientReport` (cumulative per-client totals + RTT/score distributions) and
|
|
593
|
-
`TrackReport` (per-track final report, delivered on `client-track-report`).
|
|
594
|
-
|
|
595
620
|
---
|
|
596
621
|
|
|
597
622
|
## Detectors (server-side extension point)
|
|
@@ -644,29 +669,47 @@ class Detectors {
|
|
|
644
669
|
## Remote track resolution (mediasoup / SFU)
|
|
645
670
|
|
|
646
671
|
In an SFU, one participant's **outbound** track is delivered to other participants as **inbound**
|
|
647
|
-
tracks
|
|
648
|
-
|
|
649
|
-
|
|
672
|
+
tracks (one **publisher** → many **subscribers**). Correlation is **opt-in** per observer: set
|
|
673
|
+
`ObserverConfig.createTrackResolver`, a factory invoked when each call is created that returns the
|
|
674
|
+
call's `RemoteTrackResolver` (or `undefined` for none).
|
|
675
|
+
|
|
676
|
+
`RemoteTrackResolver` is a generic, strategy-driven class. It subscribes to the bus (filtered to
|
|
677
|
+
its call) and links tracks by **publisher id** — the link key — maintaining the links directly on
|
|
678
|
+
the tracks: `inboundTrack.remoteOutboundTrack` and `outboundTrack.remoteInboundTracks: Set`.
|
|
650
679
|
|
|
651
680
|
```ts
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
const
|
|
655
|
-
|
|
681
|
+
import { Observer, createDefaultMediasoupRemoteTrackResolverFactory } from '@observertc/observer-js';
|
|
682
|
+
|
|
683
|
+
const observer = new Observer({
|
|
684
|
+
createTrackResolver: createDefaultMediasoupRemoteTrackResolverFactory(),
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
// later, given tracks (links are kept up to date as tracks come and go):
|
|
688
|
+
const source = inboundTrack.remoteOutboundTrack; // the publishing ObservedOutboundTrack
|
|
689
|
+
const receivers = [ ...outboundTrack.remoteInboundTracks ]; // the subscribing ObservedInboundTrack[]
|
|
656
690
|
```
|
|
657
691
|
|
|
658
|
-
|
|
659
|
-
`
|
|
692
|
+
Two built-in factories ship: `createDefaultMediasoupRemoteTrackResolverFactory()` (publisher =
|
|
693
|
+
`attachments.producerId`, subscriber = `attachments.consumerId`) and
|
|
694
|
+
`createP2pRemoteTrackResolverFactory()` (matches by RTP **SSRC**, preserved end-to-end in p2p).
|
|
695
|
+
|
|
696
|
+
For any other topology, build a `RemoteTrackResolver` with your own key resolvers — the publisher
|
|
697
|
+
id is just whatever links a subscribed track to the published one:
|
|
660
698
|
|
|
661
699
|
```ts
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
700
|
+
import { Observer, RemoteTrackResolver } from '@observertc/observer-js';
|
|
701
|
+
|
|
702
|
+
const observer = new Observer({
|
|
703
|
+
createTrackResolver: (observedCall) => new RemoteTrackResolver(observedCall, {
|
|
704
|
+
resolveOutboundTrackPublisherId: (out) => out.attachments?.mediaId as string | undefined,
|
|
705
|
+
resolveInboundTrackPublisherId: (inb) => inb.attachments?.mediaId as string | undefined,
|
|
706
|
+
resolveInboundTrackSubscriberId: (inb) => inb.attachments?.subId as string | undefined, // optional
|
|
707
|
+
}),
|
|
708
|
+
});
|
|
666
709
|
```
|
|
667
710
|
|
|
668
|
-
|
|
669
|
-
|
|
711
|
+
For the mediasoup factory, the application puts `producerId` / `consumerId` (and optionally
|
|
712
|
+
`direction`, `label`) into the track `attachments`.
|
|
670
713
|
|
|
671
714
|
---
|
|
672
715
|
|
|
@@ -733,12 +776,32 @@ observer.on('client-sink-created', ({ observedClient, sink }) => {
|
|
|
733
776
|
|--------|-----------|-------|
|
|
734
777
|
| `createJsonlFileSinkFactory` | `({ directory, flags?, getFileName?, serializeSample? }) => ClientSampleSinkFactory` | per-client JSONL files; path defaults to `${callId}__${clientId}.jsonl` under `directory` (which **must exist**) |
|
|
735
778
|
| `createJsonlFileSink` | `({ path, flags?, serializeSample? }) => ClientSampleSink` | a single JSONL file; wraps `fs.WriteStream` and re-emits its `close`/`finish`/`drain`/`error` |
|
|
736
|
-
| `JsonlFileSink` | `class extends ClientSampleSink` | the underlying class
|
|
779
|
+
| `JsonlFileSink` | `class extends ClientSampleSink` | the underlying class; exposes `readonly path` so a `close` handler knows which file is ready |
|
|
737
780
|
| `createInMemorySink` / `InMemorySink` | `(samples?: ClientSample[]) => InMemorySink` | collects the accepted **sample objects** into `.samples: ClientSample[]`; emits `close` on `end()` |
|
|
738
781
|
|
|
739
782
|
`serializeSample?: (sample: ClientSample) => string` overrides the default `JSON.stringify` for
|
|
740
783
|
the JSONL sinks (e.g. to redact or reshape before writing).
|
|
741
784
|
|
|
785
|
+
### Reading sink-specific info (e.g. the file path)
|
|
786
|
+
|
|
787
|
+
The bus hands you the sink as the base `ClientSampleSink`. To read information specific to a sink
|
|
788
|
+
type — for a file sink, where it was written — **narrow with `instanceof`** and read the sink's
|
|
789
|
+
public fields. `JsonlFileSink` exposes `path`:
|
|
790
|
+
|
|
791
|
+
```ts
|
|
792
|
+
import { JsonlFileSink } from '@observertc/observer-js';
|
|
793
|
+
|
|
794
|
+
observer.on('client-sink-created', ({ observedClient, sink }) => {
|
|
795
|
+
if (sink instanceof JsonlFileSink) {
|
|
796
|
+
const { path } = sink; // the file this client's samples go to
|
|
797
|
+
sink.once('close', () => uploadFile(path)); // close = flushed & fd closed → ready
|
|
798
|
+
}
|
|
799
|
+
});
|
|
800
|
+
```
|
|
801
|
+
|
|
802
|
+
The general pattern: each concrete sink exposes whatever it wants as `public readonly` fields, and
|
|
803
|
+
consumers narrow (`instanceof YourSink`) to read them. Your own sinks do the same.
|
|
804
|
+
|
|
742
805
|
### Writing your own sink
|
|
743
806
|
|
|
744
807
|
Subclass `ClientSampleSink` and emit the lifecycle events yourself — for any non-file
|
|
@@ -800,14 +863,15 @@ filtering, per-module routing, and full silencing.
|
|
|
800
863
|
|
|
801
864
|
The library **warns and degrades; it does not throw** on operational problems:
|
|
802
865
|
|
|
803
|
-
- Bad config (`update-on-interval` without an interval) → warn + safe fallback policy.
|
|
804
866
|
- `createObservedCall` / `createObservedClient` on a closed parent → warn + return `undefined`.
|
|
805
867
|
- Duplicate id → warn + return the **existing** instance.
|
|
806
868
|
- `accept()` on a closed client → warn + no-op.
|
|
807
869
|
- Sample missing `callId`/`clientId`, or observer closed → `sample-rejected` event.
|
|
870
|
+
- A throwing accept-middleware → warn + drop that sample (never crashes `accept()`).
|
|
808
871
|
|
|
809
|
-
Therefore `create*` and `getOrCreate*` return `T | undefined`; **guard the result.** The
|
|
810
|
-
|
|
872
|
+
Therefore `create*` and `getOrCreate*` return `T | undefined`; **guard the result.** The
|
|
873
|
+
`Middleware` utility's internal invariants (e.g. calling `next()` twice) throw, but those throws
|
|
874
|
+
are caught by `accept()` and surfaced as a warning.
|
|
811
875
|
|
|
812
876
|
---
|
|
813
877
|
|
|
@@ -844,7 +908,11 @@ export type { JsonlFileSinkOptions, JsonlFileSinkFactoryOptions } from './sinks/
|
|
|
844
908
|
export { InMemorySink, createInMemorySink } from './sinks/InMemorySink';
|
|
845
909
|
|
|
846
910
|
export { Middleware } from './common/Middleware';
|
|
847
|
-
|
|
911
|
+
|
|
912
|
+
// remote track correlation
|
|
913
|
+
export { RemoteTrackResolver } from './utils/RemoteTrackResolver';
|
|
914
|
+
export type { RemoteTrackResolvers, RemoteTrackResolverFactory } from './utils/RemoteTrackResolver';
|
|
915
|
+
export { createDefaultMediasoupRemoteTrackResolverFactory, createP2pRemoteTrackResolverFactory } from './utils/RemoteTrackResolverFactories';
|
|
848
916
|
```
|
|
849
917
|
|
|
850
918
|
---
|
|
@@ -853,23 +921,23 @@ export type { TrackReport, ClientReport } from './Reports';
|
|
|
853
921
|
|
|
854
922
|
```bash
|
|
855
923
|
yarn install
|
|
856
|
-
yarn build # tsup → dist/ (ESM
|
|
924
|
+
yarn build # tsup → dist/ (dual ESM .mjs + CJS .js, single entry, .d.ts/.d.mts + sourcemaps)
|
|
857
925
|
yarn lint # eslint -c .eslintrc.json "src/**/*.ts"
|
|
858
926
|
yarn typecheck # tsc --noEmit
|
|
859
927
|
yarn test # jest
|
|
860
928
|
```
|
|
861
929
|
|
|
862
930
|
The build is driven by [`tsup`](https://tsup.egoist.dev) (config in `tsup.config.ts`): a single
|
|
863
|
-
entry (`src/index.ts`), ESM output to `dist/`
|
|
864
|
-
|
|
931
|
+
entry (`src/index.ts`), dual ESM + CommonJS output to `dist/` (`index.mjs` / `index.js`) with
|
|
932
|
+
`.d.mts` / `.d.ts` types and sourcemaps, targeting Node 22. CI (`.github/workflows/ci.yml`) runs
|
|
933
|
+
lint + typecheck + **build** + test on every push/PR.
|
|
865
934
|
|
|
866
935
|
**Project layout** (`src/`): `Observer.ts`, `ObservedCall.ts`, `ObservedClient.ts`,
|
|
867
936
|
`ObservedPeerConnection.ts`, the `Observed*` sub-stat classes, `ObserverEvents.ts` (the typed
|
|
868
937
|
event map + scope types), `detectors/` (`Detector`, `Detectors`), `scores/`, `updaters/`
|
|
869
938
|
(update-policy strategies), `utils/` (remote-track resolvers), `common/` (`logger`, `utils`,
|
|
870
|
-
`Middleware`), `schema/` (sample/event/meta types), and `sinks/` (the
|
|
871
|
-
`
|
|
872
|
-
`@observertc/observer-js/sinks` subpath).
|
|
939
|
+
`Middleware`), `schema/` (sample/event/meta types), and `sinks/` (the `ClientSampleSink` base +
|
|
940
|
+
`JsonlFileSink` / `InMemorySink`, re-exported from the package root).
|
|
873
941
|
|
|
874
942
|
**Conventions to follow when developing further:**
|
|
875
943
|
|