@observertc/observer-js 1.0.0-beta.10 → 1.0.0-beta.11

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/llms-full.txt CHANGED
@@ -1,7 +1,7 @@
1
1
  # observer-js (@observertc/observer-js) — full documentation
2
2
 
3
3
  > Single-file export of the project's documentation for one-shot LLM ingestion.
4
- > Concatenates: README.md, docs/logging.md, CHANGELOG.md. Generated from source —
4
+ > Concatenates: README.md, docs/logging.md, CHANGELOG.md. Generated by scripts/build-llms-full.mjs —
5
5
  > the canonical, always-current copies live in the repository; see llms.txt for the link index.
6
6
 
7
7
  ---
@@ -75,9 +75,10 @@ and emits a single, unified stream of typed events the application can react to.
75
75
  11. [Remote track resolution (mediasoup / SFU)](#remote-track-resolution-mediasoup--sfu)
76
76
  12. [Mediasoup router observation](#mediasoup-router-observation)
77
77
  13. [Sinks (per-client sample persistence)](#sinks-per-client-sample-persistence)
78
- 14. [Logging](#logging)
79
- 15. [Error-handling philosophy](#error-handling-philosophy)
80
- 16. [Development & extension guide](#development--extension-guide)
78
+ 14. [Injecting data into a client](#injecting-data-into-a-client)
79
+ 15. [Logging](#logging)
80
+ 16. [Error-handling philosophy](#error-handling-philosophy)
81
+ 17. [Development & extension guide](#development--extension-guide)
81
82
 
82
83
  ---
83
84
 
@@ -381,7 +382,7 @@ additional field(s) on top of that scope.
381
382
  | Event | Extra | Fires when |
382
383
  |-------|-------|-----------|
383
384
  | `mediasoup-router-added` | — | `observer.createObservedMediasoupRouter(...)` registered a router |
384
- | `mediasoup-router-matched-with-peer-connection` | `{ observedCall, observedClient, observedPeerConnection }` | a newly added peer connection's id matched one of the router's WebRTC transport ids. |
385
+ | `mediasoup-router-matched-with-peer-connection` | `{ observedCall, observedClient, observedPeerConnection }` | a newly added peer connection's id matched one of the router's WebRTC transport ids. **Opt-in** via `matchPeerConnectionByWebRtcTransportId: true`. |
385
386
  | `mediasoup-router-removed` | — | the underlying mediasoup router closed (its `router.observer` `close` fired) |
386
387
 
387
388
  See [Mediasoup router observation](#mediasoup-router-observation) for the full design and examples.
@@ -551,7 +552,7 @@ Key members:
551
552
  - `readonly sink?: ClientSampleSink` — the per-client sink (see [Sinks](#sinks-per-client-sample-persistence)), if `createClientSink` is configured; listen on it for `close`/`error`
552
553
  - **Injection API** (queue app data to be merged into the next sample processing):
553
554
  `injectEvent(ClientEvent)`, `injectIssue(ClientIssue)`, `injectMetaData(ClientMetaData)`,
554
- `injectExtensionStat(ExtensionStat)`, `injectAttachment(key, value)`
555
+ `injectExtensionStat(ExtensionStat)`, `injectAttachment(attachments: Record<string, unknown>)`
555
556
  - **Direct add API** (process immediately): `addIssue(ClientIssue)`, `addMetadata(ClientMetaData)`,
556
557
  `addExtensionStats(ExtensionStat)`
557
558
  - Metrics (current/derived): `currentAvgRttInMs?`, `currentMinRttInMs?`, `currentMaxRttInMs?`,
@@ -844,11 +845,13 @@ ancestry, so you get the router **and** the matched `observedPeerConnection`, `o
844
845
  `observedCall` in one place. Stamp the `routerId` into the peer connection's / client's `appData`,
845
846
  build your own index, attach the server sample to the call in your database — whatever fits.
846
847
 
847
- How it works: as peer connections are observed (`peer-connection-added`), the observer checks whether
848
- the peer connection's id is one of the router's WebRTC transport ids. On a hit it emits — **once per
849
- matching peer connection** (de-duplicated by peer-connection id) — and keeps watching, so a router
850
- that serves many participants emits one match per participant's transport. The internal listener is
851
- removed automatically when the router closes or the observer closes.
848
+ This matching is **opt-in**: pass `matchPeerConnectionByWebRtcTransportId: true` to
849
+ `createObservedMediasoupRouter`. When enabled, as peer connections are observed
850
+ (`peer-connection-added`) the observer checks whether the peer connection's id is one of the router's
851
+ WebRTC transport ids; on a hit it emits — once per matching peer connection — and keeps watching, so a
852
+ router serving many participants emits one match per participant's transport. When the flag is omitted
853
+ or `false`, no matching is performed and the event never fires. The internal listener is removed
854
+ automatically when the router closes or the observer closes.
852
855
 
853
856
  When the underlying mediasoup router closes, its `close` propagates to `ObservedMediasoupRouter`,
854
857
  which emits **`mediasoup-router-removed`**. That is your cue to do whatever cleanup or persistence
@@ -862,10 +865,11 @@ you want with the now-final `sample` — again, the observer itself keeps nothin
862
865
  | `routerId` | `string` | yes | your id for the router (the sample also carries `router.id`) |
863
866
  | `appData` | `Record<string, unknown>` | no | application-owned bag on the `ObservedMediasoupRouter` |
864
867
  | `attachments` | `Record<string, unknown>` | no | free-form data copied onto `sample.attachments` |
868
+ | `matchPeerConnectionByWebRtcTransportId` | `boolean` | no | opt in to peer-connection matching: emit `mediasoup-router-matched-with-peer-connection` for each peer connection whose id matches one of the router's WebRTC transport ids. Omitted / `false` → no matching, the event never fires |
865
869
 
866
- Peer-connection matching is automatic — there is nothing to opt into. Returns the
867
- `ObservedMediasoupRouter`, or `undefined` if the observer is closed (a router with the same id
868
- returns the existing instance — both warn).
870
+ Peer-connection matching is **off by default**; enable it with
871
+ `matchPeerConnectionByWebRtcTransportId: true`. Returns the `ObservedMediasoupRouter`, or `undefined`
872
+ if the observer is closed (a router with the same id returns the existing instance — both warn).
869
873
 
870
874
  Useful members on the returned object: `.sample` (the `MediasoupRouterSample`), `.appData`,
871
875
  `.attachments` (getter over `sample.attachments`), `.webrtcTransportIds: Set<string>`, `.id`,
@@ -882,9 +886,13 @@ const observer = new Observer();
882
886
  // 1) Feed client samples as usual so the observer knows about calls, clients & peer connections.
883
887
  // (e.g. transport-layer: observer.accept(clientSample, context))
884
888
 
885
- // 2) Observe the SFU side. The observer auto-matches peer connections to this router's transports.
889
+ // 2) Observe the SFU side, opting in to peer-connection matching for this router's transports.
886
890
  const router = /* your mediasoup router */ undefined as any;
887
- const observedRouter = observer.createObservedMediasoupRouter({ router, routerId: router.id });
891
+ const observedRouter = observer.createObservedMediasoupRouter({
892
+ router,
893
+ routerId: router.id,
894
+ matchPeerConnectionByWebRtcTransportId: true,
895
+ });
888
896
 
889
897
  // 3) Every peer connection whose id matches one of the router's WebRTC transport ids fires this —
890
898
  // WE decide what to do with each pairing. The payload carries the full ancestry.
@@ -1043,6 +1051,60 @@ const observer = new Observer({ createClientSink });
1043
1051
  the bus with full ancestry. `ClientSampleSinkFactory` is
1044
1052
  `(p: { clientId: string; observedCall: ObservedCall }) => ClientSampleSink | undefined`.
1045
1053
 
1054
+ ---
1055
+
1056
+ ## Injecting data into a client
1057
+
1058
+ Sometimes the application holds data that belongs on a client's record but isn't part of the
1059
+ client-reported `ClientSample` — a room id or display name, an application-level event
1060
+ (*"recording started"*), a server-detected issue, an extension stat, or a device/meta item.
1061
+ `ObservedClient` exposes **injection** methods that merge such data into the client's sample stream,
1062
+ so it updates the live model **and** is persisted to the client's
1063
+ [sink](#sinks-per-client-sample-persistence) exactly like sampled data.
1064
+
1065
+ | Method | Adds to the sample's | Surfaces as |
1066
+ |--------|----------------------|-------------|
1067
+ | `injectAttachment(attachments)` | `attachments` (merged via `Object.assign`) | `observedClient.attachments` |
1068
+ | `injectEvent(event: ClientEvent)` | `clientEvents` | `client-event` (plus any state the event drives) |
1069
+ | `injectIssue(issue: ClientIssue)` | `clientIssues` | `client-issue` |
1070
+ | `injectMetaData(meta: ClientMetaData)` | `clientMetaItems` | `client-metadata` |
1071
+ | `injectExtensionStat(stat: ExtensionStat)` | `extensionStats` | `client-extension-stats` |
1072
+
1073
+ ### When the injected data lands
1074
+
1075
+ Injection is timing-aware so nothing is dropped, regardless of *when* you call it:
1076
+
1077
+ - **During a sample's processing** — e.g. from inside a `client-updated` / `client-event` handler,
1078
+ which run within `accept()` — the data is applied to the **current** sample immediately: reflected
1079
+ in entity state and written to the sink as part of that sample.
1080
+ - **Between samples** — the data is buffered and merged into the **next** `accept()`'s sample.
1081
+ - **On `close()` with pending injections and no further sample** — the buffer is flushed as a final
1082
+ synthetic sample (applied to state and written to the sink) before the sink is ended, so a
1083
+ last-moment injection is never lost.
1084
+
1085
+ In every case the injected data both updates the live `ObservedClient` and reaches the per-client
1086
+ sink — the sink always receives the final, **injection-merged** sample (the sink write happens at the
1087
+ end of `accept()`, after the merge).
1088
+
1089
+ ### Example
1090
+
1091
+ ```ts
1092
+ // Enrich at creation from your app's knowledge of the participant. Injecting in `client-added`
1093
+ // (which runs just before the first accept) lands on the first sample.
1094
+ observer.on('client-added', ({ observedClient }) => {
1095
+ observedClient.injectAttachment({ roomId: lookupRoomId(observedClient.clientId) });
1096
+ });
1097
+
1098
+ // Application-level signals at any time:
1099
+ const client = observer.getObservedCall(callId)?.getObservedClient(clientId);
1100
+ client?.injectEvent({ type: 'RECORDING_STARTED', timestamp: Date.now() });
1101
+ client?.injectIssue({ type: 'app-kicked-participant', timestamp: Date.now() });
1102
+ ```
1103
+
1104
+ `attachments` are latest-wins (like sampled `attachments`): injecting a key overwrites its previous
1105
+ value. `appData` is unaffected — injections flow into the sample/telemetry, not the app-owned
1106
+ `appData` bag (see [Ingestion](#ingestion-accept-context--lifecycle)).
1107
+
1046
1108
  ## Logging
1047
1109
 
1048
1110
  `observer-js` logs through a single, swappable sink. Out of the box it writes `debug` and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@observertc/observer-js",
3
- "version": "1.0.0-beta.10",
3
+ "version": "1.0.0-beta.11",
4
4
  "description": "Server-side Node.js library for processing ObserveRTC Samples",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -32,7 +32,8 @@
32
32
  "build": "tsup",
33
33
  "typecheck": "tsc --noEmit",
34
34
  "test": "jest --config jest.config.js",
35
- "test:coverage": "jest --config jest.config.js --coverage"
35
+ "test:coverage": "jest --config jest.config.js --coverage",
36
+ "docs:llms-full": "node scripts/build-llms-full.mjs"
36
37
  },
37
38
  "keywords": [
38
39
  "webrtc",