@synapse-sdk/client 0.3.0 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/topics.js CHANGED
@@ -12,11 +12,11 @@
12
12
  // when every listed publisher not held is tried again; no timer of our own. A datagram publisher
13
13
  // whose own `desc` has `fec` gets a follower (the video path: `FecReader` per publisher).
14
14
  import * as Moq from "@moq/net";
15
- import { FLAGS, MSGPACK, SynapseError, TYPES, decode as decodeBody, decodeHeader, encode as encodeBody, fecSymbolSizeFor, frame, moqPriority, nowUs, stampSentUs, hasPendingSentUs, mediaHeader, topicPath, validateSync, } from "@synapse-sdk/core";
15
+ import { FLAGS, MSGPACK, SynapseError, TYPES, auParamSets, auSyncPoints, decode as decodeBody, decodeHeader, encode as encodeBody, fecSymbolSizeFor, frame, isVideo, mediaFlags, moqPriority, nowUs, stampSentUs, hasPendingSentUs, mediaHeader, topicPath, validateSync, } from "@synapse-sdk/core";
16
16
  import { Handle, start as startHandle } from "./channel.js";
17
17
  import { FecReader, FecWriter, ReportWriter, feedbackListener, requireFec } from "./fec.js";
18
18
  import { RETAINED_FRAME_MAX_AGE_MS, StreamWriter, latestSubscription, moqTimestamp, streamTopicSubscription, waitActive, writeLatest } from "./session.js";
19
- import { AsyncQueue, deferred, withTimeout, TIMEOUT } from "./util.js";
19
+ import { AsyncQueue, deferred, rethrowLater, withTimeout, TIMEOUT } from "./util.js";
20
20
  /** moq's cap on one `stream` frame (a group's cache). */
21
21
  const STREAM_FRAME_MAX = 32 * 1024 * 1024;
22
22
  /** The bound on adding one publisher (its broadcast, its `desc`, its `data` subscription). */
@@ -152,26 +152,35 @@ export class TopicPublisher {
152
152
  else
153
153
  this.droppedDisconnected++;
154
154
  }
155
- /** The typed form: validates against the contract's schema, encodes (msgpack, or the bytes of a raw topic). */
155
+ /**
156
+ * The typed form: validates against the contract's schema and encodes msgpack; a raw topic's bytes
157
+ * go as given, and on a `video/*` topic as a `MEDIA` frame whose `KEY`/`CONFIG`/`RECOVERY` flags
158
+ * are read off the bitstream (Rust `Publisher<RawBody>::publish`).
159
+ */
156
160
  publish(value, opts = {}) {
157
- let body;
161
+ const tUs = opts.tUs ?? nowUs();
158
162
  if (this.contract.raw) {
159
163
  if (!(value instanceof Uint8Array))
160
164
  throw new SynapseError("invalid_input", `topic ${this.name} (${this.contract.contentType}) takes bytes`);
161
- body = value;
162
- }
163
- else {
164
- let v = value;
165
- if (this.contract.schema) {
166
- const r = validateSync(this.contract.schema, value);
167
- if (r.issues)
168
- throw new SynapseError("invalid_input", { message: r.issues[0]?.message ?? "invalid value", data: { issues: r.issues } });
169
- v = r.value;
165
+ if (!isVideo(this.contract.contentType)) {
166
+ this.publishFrame(TYPES.PUB, opts.flags ?? 0, tUs, [], value);
167
+ return;
170
168
  }
171
- body = encodeBody(v);
169
+ const { idr, recovery } = auSyncPoints(value);
170
+ this.publishFrame(TYPES.MEDIA, mediaFlags({ key: idr, config: auParamSets(value) !== undefined, recovery }) | (opts.flags ?? 0), tUs, [], value);
171
+ return;
172
172
  }
173
- this.publishFrame(TYPES.PUB, opts.flags ?? 0, opts.tUs ?? nowUs(), [], body);
173
+ let v = value;
174
+ if (this.contract.schema) {
175
+ const r = validateSync(this.contract.schema, value);
176
+ if (r.issues)
177
+ throw new SynapseError("invalid_input", { message: r.issues[0]?.message ?? "invalid value", data: { issues: r.issues } });
178
+ v = r.value;
179
+ }
180
+ this.publishFrame(TYPES.PUB, opts.flags ?? 0, tUs, [], encodeBody(v));
174
181
  }
182
+ /** An encoder's frame with the flags it already carries and its `STAGES` stamps (no re-parse); `tUs` 0 = now. The video helper's path (Rust `publish_media_frame`), not a user verb. */
183
+ publishMediaFrame(f) { const h = mediaHeader(f, 0); this.publishFrame(h.type, h.flags, f.tUs || nowUs(), h.tlvs ?? [], f.data); }
175
184
  stats() {
176
185
  const s = { published: this.published, droppedDisconnected: this.droppedDisconnected };
177
186
  if (this.fecStats)
@@ -199,6 +208,12 @@ function finish(b) {
199
208
  }
200
209
  catch { /* closed */ }
201
210
  }
211
+ /**
212
+ * The key of a publisher handle's encoder-frame path, the JS spelling of Rust's `pub(crate)
213
+ * publish_media_frame`: `@synapse-sdk/media`'s `videoPublisher` reaches it by the same
214
+ * `Symbol.for` key (the two packages share no module), so it is on no public type.
215
+ */
216
+ export const PUBLISH_MEDIA_FRAME = Symbol.for("@synapse-sdk/client:publishMediaFrame");
202
217
  /** The application's handle: the last one closed ends the publisher's task (`unregister`). */
203
218
  export class PublisherHandle {
204
219
  p;
@@ -216,10 +231,8 @@ export class PublisherHandle {
216
231
  #open() { if (this.#closed)
217
232
  throw new SynapseError("not_connected", "publisher closed"); }
218
233
  publish(value, opts) { this.#open(); this.p.publish(value, opts); }
219
- /** Any frame type and TLVs (`MEDIA` with `STAGES`, WP §4); raw topics only. */
220
- publishFrame(type, flags, tUs, tlvs, body) { this.#open(); this.p.publishFrame(type, flags, tUs, tlvs, body); }
221
- /** `tUs` 0 = capture now (as the Rust client stamps it). */
222
- publishMedia(f) { this.#open(); const h = mediaHeader(f, 0); this.p.publishFrame(h.type, h.flags, f.tUs || nowUs(), h.tlvs ?? [], f.data); }
234
+ /** `@synapse-sdk/media`'s video publisher: an encoder's frame with its flags and `STAGES` (see `PUBLISH_MEDIA_FRAME`). */
235
+ [PUBLISH_MEDIA_FRAME](f) { this.#open(); this.p.publishMediaFrame(f); }
223
236
  stats() { return this.p.stats(); }
224
237
  close() {
225
238
  if (this.#closed)
@@ -415,7 +428,12 @@ export class TopicSubscription {
415
428
  /** A new session (or none): the `fec` frames and a batch held by `throttleMs` are the previous session's and go. */
416
429
  clearFec() { this.#fec.length = 0; this.#throttled = undefined; }
417
430
  #signal() { const w = this.#wake; this.#wake = undefined; w?.resolve(); }
418
- /** The decoding step: header facts plus the body through the contract, or `mismatch` and dropped. */
431
+ /**
432
+ * The established descriptor's `content_type` (the catalog's, the runtime's statement), set by the
433
+ * follow task from each snapshot: bodies decode by it, not by what this subscription declared.
434
+ */
435
+ contentType;
436
+ /** The decoding step: header facts plus the body by the established `content_type` (msgpack → a value, validated when the contract has a schema; anything else → bytes), or `mismatch` and dropped. */
419
437
  envelope(payload, publisher, dom) {
420
438
  let header, hlen;
421
439
  try {
@@ -427,7 +445,7 @@ export class TopicSubscription {
427
445
  }
428
446
  const body = payload.subarray(hlen);
429
447
  let data;
430
- if (this.contract.raw)
448
+ if ((this.contentType ?? this.contract.contentType) !== MSGPACK)
431
449
  data = body;
432
450
  else {
433
451
  let v;
@@ -448,7 +466,8 @@ export class TopicSubscription {
448
466
  }
449
467
  data = v;
450
468
  }
451
- return { data, tUs: Number(header.tUs), seq: header.seq, publisher, dom, flags: header.flags, type: header.type, tlvs: header.tlvs ?? [] };
469
+ const f = header.flags;
470
+ return { data, tUs: Number(header.tUs), seq: header.seq, publisher, dom, flags: f, type: header.type, tlvs: header.tlvs ?? [], key: (f & FLAGS.KEY) !== 0, config: (f & FLAGS.CONFIG) !== 0, recovery: (f & FLAGS.RECOVERY) !== 0 };
452
471
  }
453
472
  /** One payload taken from `src` (Rust `drain_source`'s step): every-message mode keeps it, latest mode keeps the newest `seq`. */
454
473
  #take(src, payload, t, nowUs_) {
@@ -608,6 +627,22 @@ export class TopicSubscription {
608
627
  this.#signal();
609
628
  }
610
629
  async [Symbol.asyncDispose]() { this.unsubscribe(); }
630
+ /** The callback form's read loop (`node.subscribe(topic, cb)`): `cb` with each batch `next()` returns, until the subscription ends; a throwing `cb` is rethrown later and the loop goes on. */
631
+ deliverTo(cb) {
632
+ void (async () => {
633
+ for (;;) {
634
+ const b = await this.next();
635
+ if (!b)
636
+ return;
637
+ try {
638
+ cb(b);
639
+ }
640
+ catch (e) {
641
+ rethrowLater(e);
642
+ }
643
+ }
644
+ })();
645
+ }
611
646
  }
612
647
  /** A source is done: its subscription and its request are closed (its pending read settles as the end). */
613
648
  function closeSource(src) {
@@ -676,6 +711,8 @@ async function followTopic(sub) {
676
711
  if (session && snapshot) {
677
712
  const entry = snapshot.topics[contract.name];
678
713
  delivery = entry?.desc?.delivery ?? contract.delivery;
714
+ // ... and so is the body's `content_type` (what the read decodes by).
715
+ sub.contentType = entry?.desc?.content_type;
679
716
  const wanted = new Map();
680
717
  if (entry)
681
718
  for (const [prefix, l] of Object.entries(entry.publishers))