hci-atrium 0.1.0

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.
Files changed (163) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +182 -0
  3. package/dist/atrium.d.ts +522 -0
  4. package/dist/atrium.d.ts.map +1 -0
  5. package/dist/atrium.js +806 -0
  6. package/dist/atrium.js.map +1 -0
  7. package/dist/auth.d.ts +85 -0
  8. package/dist/auth.d.ts.map +1 -0
  9. package/dist/auth.js +369 -0
  10. package/dist/auth.js.map +1 -0
  11. package/dist/by.d.ts +84 -0
  12. package/dist/by.d.ts.map +1 -0
  13. package/dist/by.js +87 -0
  14. package/dist/by.js.map +1 -0
  15. package/dist/decider.d.ts +40 -0
  16. package/dist/decider.d.ts.map +1 -0
  17. package/dist/decider.js +76 -0
  18. package/dist/decider.js.map +1 -0
  19. package/dist/discovery.d.ts +15 -0
  20. package/dist/discovery.d.ts.map +1 -0
  21. package/dist/discovery.js +38 -0
  22. package/dist/discovery.js.map +1 -0
  23. package/dist/env.d.ts +11 -0
  24. package/dist/env.d.ts.map +1 -0
  25. package/dist/env.js +28 -0
  26. package/dist/env.js.map +1 -0
  27. package/dist/errors.d.ts +67 -0
  28. package/dist/errors.d.ts.map +1 -0
  29. package/dist/errors.js +86 -0
  30. package/dist/errors.js.map +1 -0
  31. package/dist/files.d.ts +7 -0
  32. package/dist/files.d.ts.map +1 -0
  33. package/dist/files.js +2 -0
  34. package/dist/files.js.map +1 -0
  35. package/dist/http.d.ts +11 -0
  36. package/dist/http.d.ts.map +1 -0
  37. package/dist/http.js +21 -0
  38. package/dist/http.js.map +1 -0
  39. package/dist/index.d.ts +17 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +14 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/models.d.ts +456 -0
  44. package/dist/models.d.ts.map +1 -0
  45. package/dist/models.js +487 -0
  46. package/dist/models.js.map +1 -0
  47. package/dist/pkce.d.ts +16 -0
  48. package/dist/pkce.d.ts.map +1 -0
  49. package/dist/pkce.js +40 -0
  50. package/dist/pkce.js.map +1 -0
  51. package/dist/react/index.d.ts +37 -0
  52. package/dist/react/index.d.ts.map +1 -0
  53. package/dist/react/index.js +89 -0
  54. package/dist/react/index.js.map +1 -0
  55. package/dist/rooms/armed-set.d.ts +33 -0
  56. package/dist/rooms/armed-set.d.ts.map +1 -0
  57. package/dist/rooms/armed-set.js +65 -0
  58. package/dist/rooms/armed-set.js.map +1 -0
  59. package/dist/rooms/brokers.d.ts +62 -0
  60. package/dist/rooms/brokers.d.ts.map +1 -0
  61. package/dist/rooms/brokers.js +109 -0
  62. package/dist/rooms/brokers.js.map +1 -0
  63. package/dist/rooms/codec.d.ts +100 -0
  64. package/dist/rooms/codec.d.ts.map +1 -0
  65. package/dist/rooms/codec.js +101 -0
  66. package/dist/rooms/codec.js.map +1 -0
  67. package/dist/rooms/index.d.ts +40 -0
  68. package/dist/rooms/index.d.ts.map +1 -0
  69. package/dist/rooms/index.js +45 -0
  70. package/dist/rooms/index.js.map +1 -0
  71. package/dist/rooms/offer-capture.d.ts +96 -0
  72. package/dist/rooms/offer-capture.d.ts.map +1 -0
  73. package/dist/rooms/offer-capture.js +338 -0
  74. package/dist/rooms/offer-capture.js.map +1 -0
  75. package/dist/rooms/offer-transcription.d.ts +63 -0
  76. package/dist/rooms/offer-transcription.d.ts.map +1 -0
  77. package/dist/rooms/offer-transcription.js +231 -0
  78. package/dist/rooms/offer-transcription.js.map +1 -0
  79. package/dist/rooms/participant.d.ts +35 -0
  80. package/dist/rooms/participant.d.ts.map +1 -0
  81. package/dist/rooms/participant.js +53 -0
  82. package/dist/rooms/participant.js.map +1 -0
  83. package/dist/rooms/recorder.d.ts +74 -0
  84. package/dist/rooms/recorder.d.ts.map +1 -0
  85. package/dist/rooms/recorder.js +157 -0
  86. package/dist/rooms/recorder.js.map +1 -0
  87. package/dist/rooms/room.d.ts +542 -0
  88. package/dist/rooms/room.d.ts.map +1 -0
  89. package/dist/rooms/room.js +1357 -0
  90. package/dist/rooms/room.js.map +1 -0
  91. package/dist/rooms/snippet.d.ts +48 -0
  92. package/dist/rooms/snippet.d.ts.map +1 -0
  93. package/dist/rooms/snippet.js +70 -0
  94. package/dist/rooms/snippet.js.map +1 -0
  95. package/dist/rooms/sse.d.ts +102 -0
  96. package/dist/rooms/sse.d.ts.map +1 -0
  97. package/dist/rooms/sse.js +347 -0
  98. package/dist/rooms/sse.js.map +1 -0
  99. package/dist/rooms/standing-source.d.ts +190 -0
  100. package/dist/rooms/standing-source.d.ts.map +1 -0
  101. package/dist/rooms/standing-source.js +431 -0
  102. package/dist/rooms/standing-source.js.map +1 -0
  103. package/dist/rooms/transcriber.d.ts +81 -0
  104. package/dist/rooms/transcriber.d.ts.map +1 -0
  105. package/dist/rooms/transcriber.js +111 -0
  106. package/dist/rooms/transcriber.js.map +1 -0
  107. package/dist/rooms/transport-shared.d.ts +28 -0
  108. package/dist/rooms/transport-shared.d.ts.map +1 -0
  109. package/dist/rooms/transport-shared.js +42 -0
  110. package/dist/rooms/transport-shared.js.map +1 -0
  111. package/dist/rooms/transport.d.ts +161 -0
  112. package/dist/rooms/transport.d.ts.map +1 -0
  113. package/dist/rooms/transport.js +258 -0
  114. package/dist/rooms/transport.js.map +1 -0
  115. package/dist/shaping/assets.d.ts +42 -0
  116. package/dist/shaping/assets.d.ts.map +1 -0
  117. package/dist/shaping/assets.js +77 -0
  118. package/dist/shaping/assets.js.map +1 -0
  119. package/dist/shaping/chat.d.ts +39 -0
  120. package/dist/shaping/chat.d.ts.map +1 -0
  121. package/dist/shaping/chat.js +62 -0
  122. package/dist/shaping/chat.js.map +1 -0
  123. package/dist/shaping/index.d.ts +14 -0
  124. package/dist/shaping/index.d.ts.map +1 -0
  125. package/dist/shaping/index.js +13 -0
  126. package/dist/shaping/index.js.map +1 -0
  127. package/dist/shaping/library.d.ts +84 -0
  128. package/dist/shaping/library.d.ts.map +1 -0
  129. package/dist/shaping/library.js +288 -0
  130. package/dist/shaping/library.js.map +1 -0
  131. package/dist/shaping/request.d.ts +47 -0
  132. package/dist/shaping/request.d.ts.map +1 -0
  133. package/dist/shaping/request.js +25 -0
  134. package/dist/shaping/request.js.map +1 -0
  135. package/dist/shaping/rooms.d.ts +128 -0
  136. package/dist/shaping/rooms.d.ts.map +1 -0
  137. package/dist/shaping/rooms.js +330 -0
  138. package/dist/shaping/rooms.js.map +1 -0
  139. package/dist/shaping/sources.d.ts +29 -0
  140. package/dist/shaping/sources.d.ts.map +1 -0
  141. package/dist/shaping/sources.js +51 -0
  142. package/dist/shaping/sources.js.map +1 -0
  143. package/dist/shaping/structured.d.ts +47 -0
  144. package/dist/shaping/structured.d.ts.map +1 -0
  145. package/dist/shaping/structured.js +115 -0
  146. package/dist/shaping/structured.js.map +1 -0
  147. package/dist/storage.d.ts +36 -0
  148. package/dist/storage.d.ts.map +1 -0
  149. package/dist/storage.js +86 -0
  150. package/dist/storage.js.map +1 -0
  151. package/dist/transcribe-stream.d.ts +81 -0
  152. package/dist/transcribe-stream.d.ts.map +1 -0
  153. package/dist/transcribe-stream.js +253 -0
  154. package/dist/transcribe-stream.js.map +1 -0
  155. package/dist/transcribe.d.ts +57 -0
  156. package/dist/transcribe.d.ts.map +1 -0
  157. package/dist/transcribe.js +267 -0
  158. package/dist/transcribe.js.map +1 -0
  159. package/dist/types.d.ts +20 -0
  160. package/dist/types.d.ts.map +1 -0
  161. package/dist/types.js +3 -0
  162. package/dist/types.js.map +1 -0
  163. package/package.json +56 -0
@@ -0,0 +1,231 @@
1
+ /**
2
+ * The transcription standing service behind {@link ./room.Room.offerTranscription} — armed by a
3
+ * ``transcription.start`` request, wound down by ``transcription.stop`` or the room's close. The
4
+ * TypeScript port of Python's ``_AsyncOfferedTranscription`` + ``_publish_room_transcription`` (aio.py).
5
+ *
6
+ * Engage is **sender-scoped** (see {@link ./armed-set.ArmedSet}): only armed sources are transcribed
7
+ * — an unarmed source's frames are dropped at the demux (no engine spawns). While engaged it
8
+ * subscribes the room's ``audio.snippet`` stream, demuxes frames per source, and runs one
9
+ * transcription engine per source (the same engine as {@link ./room.Room.transcribe}), stamping each
10
+ * ``transcript.*`` signal's payload with the source's connection id and roster-resolved
11
+ * ``source_name``. Up to ``maxSources`` sources; a departed source's engine is reaped promptly.
12
+ */
13
+ import { asError } from "../http.js";
14
+ import { decodeAudioSnippetFrame } from "./snippet.js";
15
+ import { OFFER_CLIP_QUEUE_MAX, OFFER_MAX_SOURCES, StandingSource, } from "./standing-source.js";
16
+ import { publishRoomTranscripts, TRANSCRIBER_SOURCE } from "./transcriber.js";
17
+ /** A bounded per-source clip queue — drops the **oldest** queued clip when full (live captions favour
18
+ * recency over completeness). A ``null`` end-sentinel always lands, ending the async iterator so the
19
+ * engine flushes and returns. The port of Python's ``asyncio.Queue`` + ``_put_newest`` + ``_queue_bytes``. */
20
+ class ClipQueue {
21
+ max;
22
+ items = [];
23
+ wake = null;
24
+ constructor(max) {
25
+ this.max = max;
26
+ }
27
+ /** Put an item, first dropping the oldest queued item if the queue is full. Returns whether a drop
28
+ * happened (never throws — so the ``null`` sentinel always lands too). */
29
+ putNewest(item) {
30
+ let dropped = false;
31
+ if (this.items.length >= this.max) {
32
+ this.items.shift();
33
+ dropped = true;
34
+ }
35
+ this.items.push(item);
36
+ this.wake?.();
37
+ this.wake = null;
38
+ return dropped;
39
+ }
40
+ /** Discard every buffered clip (a per-source stop/leave drops what hasn't been transcribed — no
41
+ * captions past the disarm boundary). A subsequent {@link putNewest}(null) still ends the iterator. */
42
+ clear() {
43
+ this.items.length = 0;
44
+ }
45
+ async *[Symbol.asyncIterator]() {
46
+ while (true) {
47
+ while (this.items.length > 0) {
48
+ const item = this.items.shift();
49
+ if (item === null)
50
+ return;
51
+ yield item;
52
+ }
53
+ await new Promise((resolve) => {
54
+ this.wake = resolve;
55
+ });
56
+ }
57
+ }
58
+ }
59
+ export class OfferedTranscription extends StandingSource {
60
+ signal = "transcription.available";
61
+ logName = "transcription offer";
62
+ identityPrefix = "transcription-offer";
63
+ engineFn;
64
+ model;
65
+ language;
66
+ queues = new Map();
67
+ engines = new Map();
68
+ // Per-source abort — a single source's stop/leave cancels *its* engine immediately (the streaming
69
+ // path's events read unblocks at once), so no captions publish past that source's disarm boundary.
70
+ aborts = new Map();
71
+ reaped = [];
72
+ stalled = new Set();
73
+ constructor(deps, opts = {}) {
74
+ super(deps, opts.maxSources ?? OFFER_MAX_SOURCES);
75
+ this.engineFn = deps.engine;
76
+ this.model = opts.model;
77
+ this.language = opts.language;
78
+ }
79
+ /** A v1 (id-less) source being transcribed under an all-arm surfaces as a null-source entry. */
80
+ activeExtra() {
81
+ return this.engines.has(null) ? [{ source: null, name: null }] : [];
82
+ }
83
+ async run() {
84
+ const ws = await this.openWs();
85
+ let failed = false;
86
+ try {
87
+ for await (const raw of this.frames()) {
88
+ const snippet = decodeAudioSnippetFrame(raw);
89
+ if (!snippet)
90
+ continue; // a foreign / DSP binary frame — not ours
91
+ this.admit(snippet.source, snippet.data);
92
+ }
93
+ }
94
+ catch (err) {
95
+ failed = true;
96
+ this.err = asError(err);
97
+ console.error(`Atrium transcription offer for room ${this.deps.roomId} died:`, err);
98
+ }
99
+ if (!failed && this.codeRotated) {
100
+ this.stoppedReason = "code-rotated";
101
+ console.info(`Atrium transcription offer for room ${this.deps.roomId}: room code rotated`);
102
+ }
103
+ this.stopSweeper();
104
+ for (const queue of this.queues.values())
105
+ queue.putNewest(null); // drain each engine
106
+ await Promise.allSettled([...this.engines.values(), ...this.reaped]);
107
+ this.queues.clear();
108
+ this.engines.clear();
109
+ this.aborts.clear();
110
+ this.reaped = [];
111
+ await Promise.allSettled(this.bg);
112
+ this.bg.length = 0;
113
+ ws.close();
114
+ }
115
+ /** Route one clip to its source's engine — dropping it if the source is unarmed or past the cap,
116
+ * and spawning the engine on a source's first clip. */
117
+ admit(source, data) {
118
+ if (!this.armed.isArmed(source))
119
+ return; // unarmed source — dropped at the demux
120
+ let queue = this.queues.get(source);
121
+ if (!queue) {
122
+ if (this.engines.size >= this.maxSources) {
123
+ this.warnCapped(source);
124
+ return;
125
+ }
126
+ queue = new ClipQueue(OFFER_CLIP_QUEUE_MAX);
127
+ this.queues.set(source, queue);
128
+ const ac = new AbortController();
129
+ this.aborts.set(source, ac);
130
+ const sourceName = source !== null ? this.nameFor(source) : null;
131
+ this.engines.set(source, this.spawnEngine(source, sourceName, queue, ac.signal));
132
+ this.spawnBroadcast(); // a new source is now active (v1 surfaces the null entry)
133
+ }
134
+ this.enqueue(source, queue, data);
135
+ }
136
+ spawnEngine(source, sourceName, queue, signal) {
137
+ return this.publishTranscription(source, sourceName, queue, signal).catch((err) => {
138
+ // An engine error (an auth reject, a mid-stream drop) is swallowed like Python's suppressed
139
+ // engine-task exceptions — the demux loop, not the engine, owns the offer's terminal error.
140
+ console.debug(`Atrium transcription offer engine for ${JSON.stringify(source)} ended:`, err);
141
+ });
142
+ }
143
+ /** Run the engine over one source's clip queue and publish each result — the shared
144
+ * {@link publishRoomTranscripts} with per-source attribution, published over the REST ``/signal``
145
+ * POST (so the ephemeral partial hint survives) as ``source: "transcriber"``. */
146
+ publishTranscription(source, sourceName, queue, signal) {
147
+ return publishRoomTranscripts({
148
+ clips: queue,
149
+ engine: this.engineFn,
150
+ publish: (type, payload, o) => this.deps.publishSignal(type, payload, {
151
+ source: TRANSCRIBER_SOURCE,
152
+ ephemeral: o.ephemeral,
153
+ }),
154
+ model: this.model,
155
+ language: this.language,
156
+ signal,
157
+ attribution: { source, source_name: sourceName },
158
+ });
159
+ }
160
+ /** Hand one clip to a source's engine, dropping the oldest queued clip if the engine has stalled.
161
+ * Warns once per source per stall episode. */
162
+ enqueue(source, queue, data) {
163
+ if (queue.putNewest(data)) {
164
+ if (!this.stalled.has(source)) {
165
+ this.stalled.add(source);
166
+ console.warn(`Atrium transcription offer for room ${this.deps.roomId}: source ${JSON.stringify(source)} ` +
167
+ "is backing up (engine stalled) — dropping oldest clips");
168
+ }
169
+ }
170
+ else {
171
+ this.stalled.delete(source);
172
+ }
173
+ }
174
+ /** On leave, cancel that source's engine immediately (mirror of Python's ``engine.cancel()``):
175
+ * abort its signal and drop its buffered clips, so no captions publish for a source that has left
176
+ * (consent-relevant). The reaped engine is awaited at teardown (the base handles the rest). */
177
+ onPresenceSource(event, cid) {
178
+ if (event !== "leave")
179
+ return;
180
+ const queue = this.queues.get(cid);
181
+ const engine = this.engines.get(cid);
182
+ this.aborts.get(cid)?.abort(); // cancel now — the streaming events read unblocks at once
183
+ this.queues.delete(cid);
184
+ this.engines.delete(cid);
185
+ this.aborts.delete(cid);
186
+ if (queue) {
187
+ queue.clear(); // drop buffered clips — nothing transcribes past the leave
188
+ queue.putNewest(null); // …then end the iterator (batch path exits its for-await)
189
+ }
190
+ if (engine)
191
+ this.reaped.push(engine);
192
+ this.stalled.delete(cid);
193
+ }
194
+ // Serialize the per-source stop with the other state transitions (start / stop) under the lock —
195
+ // it mutates the queue/engine/armed maps the frame loop reads. Cancel-immediately, like Python:
196
+ // abort the engine, drop its buffered clips, ack promptly (no captions after the disarm boundary).
197
+ async stopSource(source) {
198
+ await this.lock.run(async () => {
199
+ this.armed.disarm(source);
200
+ const queue = this.queues.get(source);
201
+ const engine = this.engines.get(source);
202
+ this.aborts.get(source)?.abort(); // cancel now — the streaming path stops publishing at once
203
+ this.queues.delete(source);
204
+ this.engines.delete(source);
205
+ this.aborts.delete(source);
206
+ if (queue) {
207
+ queue.clear(); // drop buffered clips — no captions past disarm
208
+ queue.putNewest(null); // …then end the iterator so the batch path exits and the engine settles
209
+ }
210
+ if (engine)
211
+ await engine.catch(() => { });
212
+ this.stalled.delete(source);
213
+ this.cappedWarned.delete(source);
214
+ });
215
+ await this.broadcast();
216
+ }
217
+ async stop() {
218
+ this.armed.clear();
219
+ for (const ac of this.aborts.values())
220
+ ac.abort(); // cancel every source's engine
221
+ this.ws?.close();
222
+ for (const queue of this.queues.values())
223
+ queue.putNewest(null);
224
+ const running = this.runPromise;
225
+ this.runPromise = null;
226
+ if (running)
227
+ await running.catch(() => { });
228
+ this.clearLastBroadcast();
229
+ }
230
+ }
231
+ //# sourceMappingURL=offer-transcription.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"offer-transcription.js","sourceRoot":"","sources":["../../src/rooms/offer-transcription.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * The typed presence-roster participant behind ``room.participants()`` / ``room.has()`` — the
3
+ * camelCase port of the Python SDK's ``models.Participant`` (``_wrap`` / ``_merge_roles`` /
4
+ * ``_participant_matches``). Distinct from the transport-level {@link ./transport.RosterEntry} (the
5
+ * raw live-roster item ``room.presence`` yields): this is the parsed, query-friendly view.
6
+ */
7
+ /** One member of a room's presence roster (from ``room.participants()``, behind ``room.has()``). */
8
+ export interface Participant {
9
+ connectionId: string;
10
+ identity: string | null;
11
+ /** The announced display name (``announce.name``), or ``null``. */
12
+ name: string | null;
13
+ /** The announce's ``role`` (singular) and ``roles`` (list) merged, deduped, order-preserving. */
14
+ roles: string[];
15
+ /** When this connection joined — a fresh joiner vs. a long-standing tab (rooms are always open). */
16
+ connectedAt: Date | null;
17
+ /** The original server roster dict — the escape hatch for fields this type doesn't surface. */
18
+ raw: Record<string, unknown>;
19
+ }
20
+ /** A ``has()`` predicate: the given filters are ANDed (each omitted filter passes). */
21
+ export interface ParticipantPredicate {
22
+ role?: string;
23
+ name?: string;
24
+ identityPrefix?: string;
25
+ }
26
+ /** Merge an announce's ``role`` (string) and ``roles`` (list) into one deduped, order-preserving list.
27
+ * Both shapes exist in the wild — a consumer reading only one would silently miss the other. */
28
+ export declare function mergeRoles(announce: Record<string, unknown>): string[];
29
+ /** Parse one server roster entry into a typed {@link Participant} (mirror of Python's ``_wrap``). */
30
+ export declare function parseParticipant(data: Record<string, unknown>): Participant;
31
+ /** The shared ``has()`` predicate: the given filters ANDed (each omitted filter passes). A hosted
32
+ * Player announces no role but identity ``player:web``, so ``{ identityPrefix: "player:" }`` is how
33
+ * the old ``hasPlayer()`` guard is spelled now (mirror of Python's ``_participant_matches``). */
34
+ export declare function participantMatches(p: Participant, pred: ParticipantPredicate): boolean;
35
+ //# sourceMappingURL=participant.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"participant.d.ts","sourceRoot":"","sources":["../../src/rooms/participant.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,oGAAoG;AACpG,MAAM,WAAW,WAAW;IAC1B,YAAY,EAAE,MAAM,CAAA;IACpB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,mEAAmE;IACnE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,iGAAiG;IACjG,KAAK,EAAE,MAAM,EAAE,CAAA;IACf,oGAAoG;IACpG,WAAW,EAAE,IAAI,GAAG,IAAI,CAAA;IACxB,+FAA+F;IAC/F,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAC7B;AAED,uFAAuF;AACvF,MAAM,WAAW,oBAAoB;IACnC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;AAED;gGACgG;AAChG,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,EAAE,CAWtE;AAQD,qGAAqG;AACrG,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,WAAW,CAa3E;AAED;;iGAEiG;AACjG,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,WAAW,EAAE,IAAI,EAAE,oBAAoB,GAAG,OAAO,CAItF"}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The typed presence-roster participant behind ``room.participants()`` / ``room.has()`` — the
3
+ * camelCase port of the Python SDK's ``models.Participant`` (``_wrap`` / ``_merge_roles`` /
4
+ * ``_participant_matches``). Distinct from the transport-level {@link ./transport.RosterEntry} (the
5
+ * raw live-roster item ``room.presence`` yields): this is the parsed, query-friendly view.
6
+ */
7
+ /** Merge an announce's ``role`` (string) and ``roles`` (list) into one deduped, order-preserving list.
8
+ * Both shapes exist in the wild — a consumer reading only one would silently miss the other. */
9
+ export function mergeRoles(announce) {
10
+ const out = [];
11
+ const single = announce.role;
12
+ if (typeof single === "string" && single && !out.includes(single))
13
+ out.push(single);
14
+ const many = announce.roles;
15
+ if (Array.isArray(many)) {
16
+ for (const role of many) {
17
+ if (typeof role === "string" && role && !out.includes(role))
18
+ out.push(role);
19
+ }
20
+ }
21
+ return out;
22
+ }
23
+ function parseIso(value) {
24
+ if (typeof value !== "string" || !value)
25
+ return null;
26
+ const date = new Date(value);
27
+ return Number.isNaN(date.getTime()) ? null : date;
28
+ }
29
+ /** Parse one server roster entry into a typed {@link Participant} (mirror of Python's ``_wrap``). */
30
+ export function parseParticipant(data) {
31
+ const announce = data.announce && typeof data.announce === "object"
32
+ ? data.announce
33
+ : {};
34
+ return {
35
+ connectionId: String(data.connection_id ?? ""),
36
+ identity: typeof data.identity === "string" ? data.identity : null,
37
+ name: typeof announce.name === "string" ? announce.name : null,
38
+ roles: mergeRoles(announce),
39
+ connectedAt: parseIso(data.connected_at),
40
+ raw: data,
41
+ };
42
+ }
43
+ /** The shared ``has()`` predicate: the given filters ANDed (each omitted filter passes). A hosted
44
+ * Player announces no role but identity ``player:web``, so ``{ identityPrefix: "player:" }`` is how
45
+ * the old ``hasPlayer()`` guard is spelled now (mirror of Python's ``_participant_matches``). */
46
+ export function participantMatches(p, pred) {
47
+ if (pred.role !== undefined && !p.roles.includes(pred.role))
48
+ return false;
49
+ if (pred.name !== undefined && p.name !== pred.name)
50
+ return false;
51
+ return pred.identityPrefix === undefined || Boolean(p.identity?.startsWith(pred.identityPrefix));
52
+ }
53
+ //# sourceMappingURL=participant.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"participant.js","sourceRoot":"","sources":["../../src/rooms/participant.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAuBH;gGACgG;AAChG,MAAM,UAAU,UAAU,CAAC,QAAiC;IAC1D,MAAM,GAAG,GAAa,EAAE,CAAA;IACxB,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAA;IAC5B,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC;QAAE,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;IACnF,MAAM,IAAI,GAAG,QAAQ,CAAC,KAAK,CAAA;IAC3B,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACxB,KAAK,MAAM,IAAI,IAAI,IAAI,EAAE,CAAC;YACxB,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC;gBAAE,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAC7E,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,SAAS,QAAQ,CAAC,KAAc;IAC9B,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAA;IACpD,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC,CAAA;IAC5B,OAAO,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAA;AACnD,CAAC;AAED,qGAAqG;AACrG,MAAM,UAAU,gBAAgB,CAAC,IAA6B;IAC5D,MAAM,QAAQ,GACZ,IAAI,CAAC,QAAQ,IAAI,OAAO,IAAI,CAAC,QAAQ,KAAK,QAAQ;QAChD,CAAC,CAAE,IAAI,CAAC,QAAoC;QAC5C,CAAC,CAAC,EAAE,CAAA;IACR,OAAO;QACL,YAAY,EAAE,MAAM,CAAC,IAAI,CAAC,aAAa,IAAI,EAAE,CAAC;QAC9C,QAAQ,EAAE,OAAO,IAAI,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI;QAClE,IAAI,EAAE,OAAO,QAAQ,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI;QAC9D,KAAK,EAAE,UAAU,CAAC,QAAQ,CAAC;QAC3B,WAAW,EAAE,QAAQ,CAAC,IAAI,CAAC,YAAY,CAAC;QACxC,GAAG,EAAE,IAAI;KACV,CAAA;AACH,CAAC;AAED;;iGAEiG;AACjG,MAAM,UAAU,kBAAkB,CAAC,CAAc,EAAE,IAA0B;IAC3E,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,KAAK,CAAA;IACzE,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,IAAI,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI;QAAE,OAAO,KAAK,CAAA;IACjE,OAAO,IAAI,CAAC,cAAc,KAAK,SAAS,IAAI,OAAO,CAAC,CAAC,CAAC,QAAQ,EAAE,UAAU,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,CAAA;AAClG,CAAC"}
@@ -0,0 +1,74 @@
1
+ /**
2
+ * The room→session recorder — the single durable binding between a live room (pure transport,
3
+ * nothing stored) and a session (an append-only durable log). Started by {@link ./room.Room.record};
4
+ * a line-for-line port of the Python SDK's ``_AsyncRecorder`` (aio.py).
5
+ *
6
+ * It rides an **observe** SSE stream ({@link ./sse.RoomEventStream} in observe mode — no presence
7
+ * connection, so the recorder is invisible in the roster) and bridges every durable-worthy signal
8
+ * into ``session.event(...)``, stamping provenance (``via``). The skip rules mirror Python exactly:
9
+ * a ``ready`` frame, a ``presence.*`` signal, and anything marked ``ephemeral`` (beats, cursors, live
10
+ * captions, relayed vectors) are **not** recorded — only real, durable signals are.
11
+ *
12
+ * Lifecycle: a dropped stream reconnects with backoff (the gap is lost — rooms have no replay), so a
13
+ * blip never ends capture. Only a **terminal** end stops it, cleanly, with a {@link RecorderStopReason}:
14
+ * a connect-code rotation (``code-rotated`` — the consent boundary between studies) or the room being
15
+ * gone / its code rejected. An explicit {@link Recorder.stop} is the ``stopped`` reason. It never
16
+ * throws to the caller; check {@link Recorder.error} for a server refusal.
17
+ */
18
+ import type { Session } from "../models.js";
19
+ import type { ShapedRequest } from "../shaping/index.js";
20
+ import type { FetchImpl } from "./sse.js";
21
+ /** Why a {@link Recorder} stopped for good — no reconnect recovers. ``stopped`` is an explicit
22
+ * {@link Recorder.stop}; the rest mirror the transport's terminal {@link RoomEndReason}. */
23
+ export type RecorderStopReason = "stopped" | "code-rotated" | "room-gone" | "bad-code";
24
+ /** The REST caller a recorder needs to append session events (the authenticated host transport). */
25
+ export type RecorderRest = (req: ShapedRequest) => Promise<unknown>;
26
+ /** Everything a {@link Recorder} is built from — the room's connection facts plus the seam callback. */
27
+ export interface RecorderDeps {
28
+ roomId: string;
29
+ /** The room's connect code — the observe stream's ``?code=`` auth. */
30
+ code: string;
31
+ /** The room's display name — stamped into each event's ``via`` provenance. */
32
+ roomName: string | null;
33
+ baseUrl: string;
34
+ /** Authenticated caller for the session-event POST (a host handle; absent for a participant). */
35
+ rest: RecorderRest;
36
+ reconnect?: boolean;
37
+ /** Injected ``fetch`` for the observe SSE stream — the test seam. */
38
+ fetchImpl?: FetchImpl;
39
+ /** Fired when this recorder starts and when it stops — the room's ``notifyRecordersChanged`` seam
40
+ * (so a wave-4 capture offer can re-derive availability off the active-recorder set). */
41
+ onChange: () => void;
42
+ }
43
+ export declare class Recorder {
44
+ private readonly deps;
45
+ private readonly session;
46
+ private stream;
47
+ private started;
48
+ private ended;
49
+ private reason;
50
+ private failure;
51
+ constructor(deps: RecorderDeps, into: Session);
52
+ /** The session this recorder writes into — the durable binding a wave-4 capture offer engages
53
+ * under. */
54
+ get into(): Session;
55
+ /** True while the capture loop runs (streaming or reconnecting) and has not begun winding down —
56
+ * a recorder in its terminal wind-down already counts as gone (mirror of Python's ``alive``). */
57
+ get alive(): boolean;
58
+ /** Why the recorder stopped, or ``null`` while alive. */
59
+ get stoppedReason(): RecorderStopReason | null;
60
+ /** The error that ended the capture for good — a server refusal (the room gone, or a rejected
61
+ * code). ``null`` while alive, after a clean rotation, or after an explicit {@link stop}. */
62
+ get error(): Error | null;
63
+ /** Begin capturing (idempotent). Starts immediately in the background — the returned handle is
64
+ * live at once, like Python's ``room.record`` side effect. */
65
+ start(): void;
66
+ private record;
67
+ private onEnd;
68
+ /** Stop capturing (idempotent). Also fires the room's recorder-changed seam so a wave-4 capture
69
+ * offer engaged under this recorder disengages. */
70
+ stop(): void;
71
+ [Symbol.dispose]: () => void;
72
+ [Symbol.asyncDispose]: () => Promise<void>;
73
+ }
74
+ //# sourceMappingURL=recorder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recorder.d.ts","sourceRoot":"","sources":["../../src/rooms/recorder.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAC3C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAExD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AAIzC;4FAC4F;AAC5F,MAAM,MAAM,kBAAkB,GAAG,SAAS,GAAG,cAAc,GAAG,WAAW,GAAG,UAAU,CAAA;AAEtF,oGAAoG;AACpG,MAAM,MAAM,YAAY,GAAG,CAAC,GAAG,EAAE,aAAa,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;AAEnE,wGAAwG;AACxG,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAA;IACd,sEAAsE;IACtE,IAAI,EAAE,MAAM,CAAA;IACZ,8EAA8E;IAC9E,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,OAAO,EAAE,MAAM,CAAA;IACf,iGAAiG;IACjG,IAAI,EAAE,YAAY,CAAA;IAClB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,qEAAqE;IACrE,SAAS,CAAC,EAAE,SAAS,CAAA;IACrB;6FACyF;IACzF,QAAQ,EAAE,MAAM,IAAI,CAAA;CACrB;AAED,qBAAa,QAAQ;IACnB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAc;IACnC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,MAAM,CAA6B;IAC3C,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,KAAK,CAAQ;IACrB,OAAO,CAAC,MAAM,CAAkC;IAChD,OAAO,CAAC,OAAO,CAAqB;gBAExB,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,OAAO;IAK7C;gBACY;IACZ,IAAI,IAAI,IAAI,OAAO,CAElB;IAED;qGACiG;IACjG,IAAI,KAAK,IAAI,OAAO,CAEnB;IAED,yDAAyD;IACzD,IAAI,aAAa,IAAI,kBAAkB,GAAG,IAAI,CAE7C;IAED;iGAC6F;IAC7F,IAAI,KAAK,IAAI,KAAK,GAAG,IAAI,CAExB;IAED;kEAC8D;IAC9D,KAAK,IAAI,IAAI;YAyBC,MAAM;IA4CpB,OAAO,CAAC,KAAK;IAWb;uDACmD;IACnD,IAAI,IAAI,IAAI;IAUJ,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;IAC5B,CAAC,MAAM,CAAC,YAAY,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CACnD"}
@@ -0,0 +1,157 @@
1
+ /**
2
+ * The room→session recorder — the single durable binding between a live room (pure transport,
3
+ * nothing stored) and a session (an append-only durable log). Started by {@link ./room.Room.record};
4
+ * a line-for-line port of the Python SDK's ``_AsyncRecorder`` (aio.py).
5
+ *
6
+ * It rides an **observe** SSE stream ({@link ./sse.RoomEventStream} in observe mode — no presence
7
+ * connection, so the recorder is invisible in the roster) and bridges every durable-worthy signal
8
+ * into ``session.event(...)``, stamping provenance (``via``). The skip rules mirror Python exactly:
9
+ * a ``ready`` frame, a ``presence.*`` signal, and anything marked ``ephemeral`` (beats, cursors, live
10
+ * captions, relayed vectors) are **not** recorded — only real, durable signals are.
11
+ *
12
+ * Lifecycle: a dropped stream reconnects with backoff (the gap is lost — rooms have no replay), so a
13
+ * blip never ends capture. Only a **terminal** end stops it, cleanly, with a {@link RecorderStopReason}:
14
+ * a connect-code rotation (``code-rotated`` — the consent boundary between studies) or the room being
15
+ * gone / its code rejected. An explicit {@link Recorder.stop} is the ``stopped`` reason. It never
16
+ * throws to the caller; check {@link Recorder.error} for a server refusal.
17
+ */
18
+ import { APIError } from "../errors.js";
19
+ import { buildSessionEvent } from "../shaping/rooms.js";
20
+ import { RoomEventStream } from "./sse.js";
21
+ export class Recorder {
22
+ deps;
23
+ session;
24
+ stream = null;
25
+ started = false;
26
+ ended = false;
27
+ reason = null;
28
+ failure = null;
29
+ constructor(deps, into) {
30
+ this.deps = deps;
31
+ this.session = into;
32
+ }
33
+ /** The session this recorder writes into — the durable binding a wave-4 capture offer engages
34
+ * under. */
35
+ get into() {
36
+ return this.session;
37
+ }
38
+ /** True while the capture loop runs (streaming or reconnecting) and has not begun winding down —
39
+ * a recorder in its terminal wind-down already counts as gone (mirror of Python's ``alive``). */
40
+ get alive() {
41
+ return this.started && !this.ended;
42
+ }
43
+ /** Why the recorder stopped, or ``null`` while alive. */
44
+ get stoppedReason() {
45
+ return this.reason;
46
+ }
47
+ /** The error that ended the capture for good — a server refusal (the room gone, or a rejected
48
+ * code). ``null`` while alive, after a clean rotation, or after an explicit {@link stop}. */
49
+ get error() {
50
+ return this.failure;
51
+ }
52
+ /** Begin capturing (idempotent). Starts immediately in the background — the returned handle is
53
+ * live at once, like Python's ``room.record`` side effect. */
54
+ start() {
55
+ if (this.started)
56
+ return;
57
+ this.started = true;
58
+ // Announce the fresh recorder before the stream opens, so a capture offer can engage against it.
59
+ this.deps.onChange();
60
+ const stream = new RoomEventStream({
61
+ roomId: this.deps.roomId,
62
+ code: this.deps.code,
63
+ baseUrl: this.deps.baseUrl,
64
+ observe: true,
65
+ reconnect: this.deps.reconnect,
66
+ fetchImpl: this.deps.fetchImpl,
67
+ onSignal: (sig) => this.record(sig),
68
+ onEnd: (r) => this.onEnd(r),
69
+ });
70
+ this.stream = stream;
71
+ // Terminal ends arrive via onEnd; a pre-ready reject (reconnect off) is swallowed the same way.
72
+ void stream.connect().catch(() => { });
73
+ }
74
+ // Bridge one live signal into the session, applying the durable-worthy skip rules. Awaited by the
75
+ // observe stream's read loop (onSignal), so the write is **inline**: the next frame isn't read
76
+ // until this settles — Python's backpressure, no unbounded queue of in-flight POSTs. The observe
77
+ // stream only delivers ``event: signal`` frames here (a ``ready`` never reaches onSignal), so the
78
+ // filter mirrors Python's ``event.ephemeral or event.is_presence`` on top of the kind check.
79
+ async record(sig) {
80
+ if (this.ended)
81
+ return;
82
+ const type = sig.type ?? "";
83
+ // Skip the rotation advance-signal by name (Python checks it before the ephemeral skip; the
84
+ // transport also winds the recorder down via onEnd), then presence + ephemeral noise.
85
+ if (type === "room.code_rotated")
86
+ return;
87
+ if (sig.ephemeral || type.startsWith("presence."))
88
+ return;
89
+ const via = {
90
+ room_id: this.deps.roomId,
91
+ room_name: this.deps.roomName,
92
+ from: sig.from ?? null,
93
+ to: sig.to || "all",
94
+ };
95
+ const req = buildSessionEvent(this.session.id, {
96
+ type,
97
+ payload: sig.payload ?? {},
98
+ source: sig.source ?? undefined,
99
+ via,
100
+ });
101
+ try {
102
+ await this.deps.rest(req);
103
+ }
104
+ catch (err) {
105
+ if (this.ended)
106
+ return;
107
+ // Mirror Python's recorder: a session-write refusal with status < 500 (and ≠ 429) is TERMINAL
108
+ // — the server rejected us and no retry heals a 4xx, so stop for good (``.error`` set, seam
109
+ // fires so a bound capture offer disengages). A transient failure (5xx / 429 / network) is
110
+ // rethrown so the observe stream drops and reconnects with backoff (the gap is lost — rooms
111
+ // have no replay — but capture continues), exactly as Python backs off on a transient write.
112
+ if (err instanceof APIError && err.status < 500 && err.status !== 429) {
113
+ this.failure = err;
114
+ this.ended = true;
115
+ this.stream?.close();
116
+ this.stream = null;
117
+ console.error(`Atrium recorder: session event '${type}' refused (${err.status}); stopping:`, err);
118
+ this.deps.onChange();
119
+ return;
120
+ }
121
+ throw err;
122
+ }
123
+ }
124
+ onEnd(reason) {
125
+ if (this.ended)
126
+ return;
127
+ this.ended = true;
128
+ this.reason = reason;
129
+ // A rotation is a clean wind-down (the consent boundary); a gone room / bad code is a refusal.
130
+ if (reason !== "code-rotated") {
131
+ this.failure = new Error(`recorder stopped: room ${reason}`);
132
+ }
133
+ this.deps.onChange();
134
+ }
135
+ /** Stop capturing (idempotent). Also fires the room's recorder-changed seam so a wave-4 capture
136
+ * offer engaged under this recorder disengages. */
137
+ stop() {
138
+ if (!this.ended) {
139
+ this.ended = true;
140
+ this.reason = "stopped";
141
+ }
142
+ this.stream?.close();
143
+ this.stream = null;
144
+ this.deps.onChange();
145
+ }
146
+ }
147
+ if (typeof Symbol.dispose === "symbol") {
148
+ Recorder.prototype[Symbol.dispose] = function () {
149
+ this.stop();
150
+ };
151
+ }
152
+ if (typeof Symbol.asyncDispose === "symbol") {
153
+ Recorder.prototype[Symbol.asyncDispose] = async function () {
154
+ this.stop();
155
+ };
156
+ }
157
+ //# sourceMappingURL=recorder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recorder.js","sourceRoot":"","sources":["../../src/rooms/recorder.ts"],"names":[],"mappings":"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"}