@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/calls.d.ts +7 -1
- package/dist/calls.js +19 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/node.d.ts +62 -24
- package/dist/node.js +208 -68
- package/dist/scene.d.ts +150 -56
- package/dist/scene.js +995 -163
- package/dist/scene_frames.d.ts +2 -2
- package/dist/scene_frames.js +1 -1
- package/dist/session.d.ts +74 -11
- package/dist/session.js +279 -59
- package/dist/store.d.ts +31 -10
- package/dist/store.js +140 -28
- package/dist/synapse.d.ts +77 -46
- package/dist/synapse.js +193 -114
- package/dist/tasks.d.ts +37 -0
- package/dist/tasks.js +146 -0
- package/dist/topics.d.ts +23 -6
- package/dist/topics.js +59 -22
- package/dist/types.d.ts +405 -93
- package/dist/util.d.ts +43 -0
- package/dist/util.js +97 -1
- package/dist/view.d.ts +5 -3
- package/dist/view.js +21 -10
- package/package.json +2 -2
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
220
|
-
|
|
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
|
-
/**
|
|
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.
|
|
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
|
-
|
|
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))
|