reactor-effect-client 0.2.0 → 0.3.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.
- package/README.md +66 -6
- package/dist/FetchHttp.js +1 -1
- package/dist/FetchHttp.js.map +1 -1
- package/dist/PeerFactory.d.ts +13 -2
- package/dist/PeerFactory.d.ts.map +1 -1
- package/dist/PeerFactory.js.map +1 -1
- package/dist/PeerTypes.d.ts +2 -2
- package/dist/PeerTypes.d.ts.map +1 -1
- package/dist/Sequence.d.ts +18 -14
- package/dist/Sequence.d.ts.map +1 -1
- package/dist/Sequence.js +117 -122
- package/dist/Sequence.js.map +1 -1
- package/dist/SessionTypes.d.ts +172 -15
- package/dist/SessionTypes.d.ts.map +1 -1
- package/dist/SessionTypes.js +18 -1
- package/dist/SessionTypes.js.map +1 -1
- package/dist/bytes.js +3 -3
- package/dist/bytes.js.map +1 -1
- package/dist/contract.d.ts +120 -29
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +105 -73
- package/dist/contract.js.map +1 -1
- package/dist/coordinator/_internal/client.d.ts +111 -17
- package/dist/coordinator/_internal/client.d.ts.map +1 -1
- package/dist/coordinator/_internal/client.js +187 -74
- package/dist/coordinator/_internal/client.js.map +1 -1
- package/dist/coordinator/_internal/recording.d.ts +33 -0
- package/dist/coordinator/_internal/recording.d.ts.map +1 -0
- package/dist/coordinator/_internal/recording.js +123 -0
- package/dist/coordinator/_internal/recording.js.map +1 -0
- package/dist/coordinator/_internal/response.d.ts.map +1 -1
- package/dist/coordinator/_internal/response.js +5 -7
- package/dist/coordinator/_internal/response.js.map +1 -1
- package/dist/coordinator/_internal/schemas.d.ts +40 -6
- package/dist/coordinator/_internal/schemas.d.ts.map +1 -1
- package/dist/coordinator/_internal/schemas.js +94 -64
- package/dist/coordinator/_internal/schemas.js.map +1 -1
- package/dist/coordinator/index.d.ts +14 -2
- package/dist/coordinator/index.d.ts.map +1 -1
- package/dist/coordinator/index.js +14 -18
- package/dist/coordinator/index.js.map +1 -1
- package/dist/correlation.d.ts +15 -0
- package/dist/correlation.d.ts.map +1 -1
- package/dist/correlation.js +26 -8
- package/dist/correlation.js.map +1 -1
- package/dist/duration.d.ts +25 -0
- package/dist/duration.d.ts.map +1 -0
- package/dist/duration.js +47 -0
- package/dist/duration.js.map +1 -0
- package/dist/errors.d.ts +1264 -49
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +698 -46
- package/dist/errors.js.map +1 -1
- package/dist/h3/_internal/client.d.ts +2 -2
- package/dist/h3/_internal/client.d.ts.map +1 -1
- package/dist/h3/_internal/client.js +280 -146
- package/dist/h3/_internal/client.js.map +1 -1
- package/dist/h3/_internal/contracts.d.ts.map +1 -1
- package/dist/h3/_internal/contracts.js +2 -2
- package/dist/h3/_internal/contracts.js.map +1 -1
- package/dist/h3/_internal/deployment.d.ts.map +1 -1
- package/dist/h3/_internal/deployment.js +5 -4
- package/dist/h3/_internal/deployment.js.map +1 -1
- package/dist/h3/_internal/evidence.js +1 -1
- package/dist/h3/_internal/evidence.js.map +1 -1
- package/dist/h3/_internal/operations.d.ts +58 -0
- package/dist/h3/_internal/operations.d.ts.map +1 -0
- package/dist/h3/_internal/operations.js +268 -0
- package/dist/h3/_internal/operations.js.map +1 -0
- package/dist/h3/_internal/references.d.ts.map +1 -1
- package/dist/h3/_internal/references.js +3 -8
- package/dist/h3/_internal/references.js.map +1 -1
- package/dist/h3/_internal/request.js +1 -1
- package/dist/h3/_internal/request.js.map +1 -1
- package/dist/h3/_internal/retained.d.ts +22 -0
- package/dist/h3/_internal/retained.d.ts.map +1 -0
- package/dist/h3/_internal/retained.js +89 -0
- package/dist/h3/_internal/retained.js.map +1 -0
- package/dist/h3/_internal/state.d.ts +15 -1
- package/dist/h3/_internal/state.d.ts.map +1 -1
- package/dist/h3/_internal/state.js +69 -29
- package/dist/h3/_internal/state.js.map +1 -1
- package/dist/h3/index.d.ts +1 -1
- package/dist/h3/index.d.ts.map +1 -1
- package/dist/h3/messages.d.ts +118 -109
- package/dist/h3/messages.d.ts.map +1 -1
- package/dist/h3/messages.js +73 -51
- package/dist/h3/messages.js.map +1 -1
- package/dist/h3/types.d.ts +109 -13
- package/dist/h3/types.d.ts.map +1 -1
- package/dist/host.d.ts +6 -6
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +4 -5
- package/dist/host.js.map +1 -1
- package/dist/index.d.ts +11 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/json.d.ts +13 -6
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +34 -14
- package/dist/json.js.map +1 -1
- package/dist/media-stream.d.ts.map +1 -1
- package/dist/media-stream.js +9 -11
- package/dist/media-stream.js.map +1 -1
- package/dist/observation.d.ts +9 -0
- package/dist/observation.d.ts.map +1 -1
- package/dist/observation.js +20 -18
- package/dist/observation.js.map +1 -1
- package/dist/orchestration/h3-source.d.ts +32 -4
- package/dist/orchestration/h3-source.d.ts.map +1 -1
- package/dist/orchestration/h3-source.js +47 -14
- package/dist/orchestration/h3-source.js.map +1 -1
- package/dist/orchestration/index.d.ts +9 -6
- package/dist/orchestration/index.d.ts.map +1 -1
- package/dist/orchestration/index.js +5 -3
- package/dist/orchestration/index.js.map +1 -1
- package/dist/orchestration/media-buffer.d.ts +12 -4
- package/dist/orchestration/media-buffer.d.ts.map +1 -1
- package/dist/orchestration/media-buffer.js +31 -11
- package/dist/orchestration/media-buffer.js.map +1 -1
- package/dist/orchestration/open-h3.d.ts +65 -0
- package/dist/orchestration/open-h3.d.ts.map +1 -0
- package/dist/orchestration/open-h3.js +65 -0
- package/dist/orchestration/open-h3.js.map +1 -0
- package/dist/orchestration/references.d.ts +5 -1
- package/dist/orchestration/references.d.ts.map +1 -1
- package/dist/orchestration/references.js +37 -34
- package/dist/orchestration/references.js.map +1 -1
- package/dist/orchestration/renewal-state.js +1 -1
- package/dist/orchestration/renewal-state.js.map +1 -1
- package/dist/orchestration/renewal.d.ts +64 -64
- package/dist/orchestration/renewal.d.ts.map +1 -1
- package/dist/orchestration/renewal.js +149 -88
- package/dist/orchestration/renewal.js.map +1 -1
- package/dist/orchestration/request.d.ts +11 -12
- package/dist/orchestration/request.d.ts.map +1 -1
- package/dist/orchestration/request.js +63 -66
- package/dist/orchestration/request.js.map +1 -1
- package/dist/orchestration/routing.d.ts.map +1 -1
- package/dist/orchestration/routing.js +17 -14
- package/dist/orchestration/routing.js.map +1 -1
- package/dist/orchestration/source-slot.d.ts +15 -5
- package/dist/orchestration/source-slot.d.ts.map +1 -1
- package/dist/orchestration/source-slot.js +40 -21
- package/dist/orchestration/source-slot.js.map +1 -1
- package/dist/orchestration/types.d.ts +594 -39
- package/dist/orchestration/types.d.ts.map +1 -1
- package/dist/orchestration/types.js +20 -0
- package/dist/orchestration/types.js.map +1 -1
- package/dist/protobuf.js +2 -2
- package/dist/protobuf.js.map +1 -1
- package/dist/session/_internal/acquire.d.ts +4 -6
- package/dist/session/_internal/acquire.d.ts.map +1 -1
- package/dist/session/_internal/acquire.js +40 -50
- package/dist/session/_internal/acquire.js.map +1 -1
- package/dist/session/_internal/cleanup.d.ts +2 -1
- package/dist/session/_internal/cleanup.d.ts.map +1 -1
- package/dist/session/_internal/cleanup.js +23 -15
- package/dist/session/_internal/cleanup.js.map +1 -1
- package/dist/session/_internal/lifecycle.d.ts +5 -0
- package/dist/session/_internal/lifecycle.d.ts.map +1 -1
- package/dist/session/_internal/lifecycle.js +16 -6
- package/dist/session/_internal/lifecycle.js.map +1 -1
- package/dist/session/_internal/remote.d.ts.map +1 -1
- package/dist/session/_internal/remote.js +16 -7
- package/dist/session/_internal/remote.js.map +1 -1
- package/dist/session/_internal/uploads.d.ts +4 -1
- package/dist/session/_internal/uploads.d.ts.map +1 -1
- package/dist/session/_internal/uploads.js +25 -27
- package/dist/session/_internal/uploads.js.map +1 -1
- package/dist/session/commands.d.ts +1 -21
- package/dist/session/commands.d.ts.map +1 -1
- package/dist/session/commands.js +1 -12
- package/dist/session/commands.js.map +1 -1
- package/dist/session/files.d.ts +10 -2
- package/dist/session/files.d.ts.map +1 -1
- package/dist/session/files.js +11 -16
- package/dist/session/files.js.map +1 -1
- package/dist/session/index.d.ts +6 -6
- package/dist/session/index.d.ts.map +1 -1
- package/dist/session/index.js.map +1 -1
- package/dist/session/media.d.ts +53 -6
- package/dist/session/media.d.ts.map +1 -1
- package/dist/session/recorder.d.ts +24 -0
- package/dist/session/recorder.d.ts.map +1 -0
- package/dist/session/recorder.js +19 -0
- package/dist/session/recorder.js.map +1 -0
- package/dist/session.d.ts +5 -4
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +213 -110
- package/dist/session.js.map +1 -1
- package/dist/simulation/_internal/source.d.ts.map +1 -1
- package/dist/simulation/_internal/source.js +57 -48
- package/dist/simulation/_internal/source.js.map +1 -1
- package/dist/simulation/index.d.ts +4 -5
- package/dist/simulation/index.d.ts.map +1 -1
- package/dist/simulation/index.js +3 -4
- package/dist/simulation/index.js.map +1 -1
- package/dist/simulation/types.d.ts +16 -2
- package/dist/simulation/types.d.ts.map +1 -1
- package/dist/stats.d.ts +19 -0
- package/dist/stats.d.ts.map +1 -1
- package/dist/stats.js +79 -8
- package/dist/stats.js.map +1 -1
- package/package.json +5 -4
- package/notices/owned-webrtc/Effect-MIT.txt +0 -21
- package/notices/owned-webrtc/IETF.txt +0 -43
- package/notices/owned-webrtc/LICENSE +0 -21
- package/notices/owned-webrtc/UPSTREAM.md +0 -20
- package/notices/owned-webrtc/VP8-ADAPTATION.md +0 -32
- package/notices/owned-webrtc/VP8-AUTHORS.txt +0 -42
- package/notices/owned-webrtc/VP8-LICENSE.txt +0 -25
- package/notices/owned-webrtc/VP8-PATENTS.txt +0 -21
- package/notices/owned-webrtc/Werift-MIT.txt +0 -21
- package/notices/owned-webrtc/libvpx-v1.12.0-LICENSE.txt +0 -28
- package/notices/owned-webrtc/libvpx-v1.12.0-PATENTS.txt +0 -22
- package/notices/owned-webrtc/rfc3711.txt +0 -3139
- package/notices/owned-webrtc/str0m-MIT.txt +0 -7
package/dist/session.js
CHANGED
|
@@ -6,10 +6,12 @@ import * as Exit from "effect/Exit";
|
|
|
6
6
|
import * as Queue from "effect/Queue";
|
|
7
7
|
import * as Cause from "effect/Cause";
|
|
8
8
|
import * as Clock from "effect/Clock";
|
|
9
|
+
import * as Duration from "effect/Duration";
|
|
9
10
|
import * as Stream from "effect/Stream";
|
|
10
|
-
import {
|
|
11
|
+
import { duration } from "./duration.js";
|
|
12
|
+
import { parsed, parsedInput, positiveLimit, ReactorError, Remote } from "./errors.js";
|
|
11
13
|
import { nonempty, uint32, structFromObject, objectFromStruct } from "./json.js";
|
|
12
|
-
import { CoordinatorClient } from "./coordinator/_internal/client.js";
|
|
14
|
+
import { CoordinatorClient, terminationAttributes } from "./coordinator/_internal/client.js";
|
|
13
15
|
import { Correlator } from "./correlation.js";
|
|
14
16
|
import { Observations } from "./observation.js";
|
|
15
17
|
import * as W from "./wire.generated.js";
|
|
@@ -19,10 +21,24 @@ import { captureUploads } from "./session/_internal/uploads.js";
|
|
|
19
21
|
import { SessionLifecycle } from "./session/_internal/lifecycle.js";
|
|
20
22
|
import { isKnownRemote, RemoteSession } from "./session/_internal/remote.js";
|
|
21
23
|
import { cleanupSession } from "./session/_internal/cleanup.js";
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
/** A provider error reply: its code routes, its text stays in `body` for inspection. */
|
|
25
|
+
const remoteError = (value, context) => new ReactorError({
|
|
26
|
+
reason: new Remote({
|
|
27
|
+
_tag: "Remote",
|
|
28
|
+
message: `remote command error ${value.code}`,
|
|
29
|
+
remoteCode: value.code,
|
|
30
|
+
body: value.message,
|
|
31
|
+
}),
|
|
32
|
+
context,
|
|
33
|
+
});
|
|
34
|
+
const pure = (body) => parsed(body);
|
|
35
|
+
/** Mark a connect phase on the current span, as SqlClient marks a transaction's. */
|
|
36
|
+
const phase = (name) => Effect.currentSpan.pipe(Effect.flatMap((span) => Clock.currentTimeNanos.pipe(Effect.map((now) => span.event(`reactor.connect.${name}`, now)))), Effect.ignore);
|
|
37
|
+
const replyTimeout = (input) => duration(input, "reply timeout", { maximum: "10 minutes" });
|
|
38
|
+
const uploadTimeout = (input) => duration(input, "upload timeout", { maximum: "10 minutes" });
|
|
39
|
+
const withDeadline = (effect, deadline, operation) => effect.pipe(Effect.timeoutOrElse({
|
|
40
|
+
duration: deadline,
|
|
41
|
+
orElse: () => Effect.fail(ReactorError.fromCode("Timeout", `${operation}: deadline`)),
|
|
26
42
|
}));
|
|
27
43
|
/** The canonical Reactor state and wire engine for both public host exports.
|
|
28
44
|
* Platform and peer dependencies are supplied by the configured client factory. */
|
|
@@ -40,7 +56,8 @@ export class Session {
|
|
|
40
56
|
closeReport;
|
|
41
57
|
received = new Set();
|
|
42
58
|
bitrates = new Map();
|
|
43
|
-
|
|
59
|
+
replyTimeout;
|
|
60
|
+
uploadTimeout;
|
|
44
61
|
connectTimeout;
|
|
45
62
|
readyTimeout;
|
|
46
63
|
heartbeat;
|
|
@@ -57,12 +74,18 @@ export class Session {
|
|
|
57
74
|
else
|
|
58
75
|
nonempty(options.intent.model.name, "model name");
|
|
59
76
|
this.http = http;
|
|
60
|
-
this.
|
|
61
|
-
this.
|
|
62
|
-
this.
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
77
|
+
this.replyTimeout = replyTimeout(options.replyTimeout ?? "10 seconds");
|
|
78
|
+
this.uploadTimeout = uploadTimeout(options.uploadTimeout ?? "60 seconds");
|
|
79
|
+
this.connectTimeout = duration(options.connectTimeout ?? "3 minutes", "connect timeout", {
|
|
80
|
+
maximum: "10 minutes",
|
|
81
|
+
});
|
|
82
|
+
this.readyTimeout = duration(options.readyTimeout ?? "30 seconds", "ready timeout", {
|
|
83
|
+
maximum: "10 minutes",
|
|
84
|
+
});
|
|
85
|
+
this.heartbeat = duration(options.heartbeatInterval ?? "10 seconds", "heartbeat interval", {
|
|
86
|
+
maximum: "10 minutes",
|
|
87
|
+
allowInfinite: true,
|
|
88
|
+
});
|
|
66
89
|
this.uploadBound = positiveLimit(options.maxUploadBytes ?? 16_777_216, "upload byte bound", 64 * 1024 * 1024);
|
|
67
90
|
this.data = new Correlator("data", options.maxPending ?? 128, options.requestNamespace);
|
|
68
91
|
this.control = new Correlator("ctrl", options.maxPending ?? 128, options.requestNamespace);
|
|
@@ -162,7 +185,7 @@ export class Session {
|
|
|
162
185
|
switch (event.type) {
|
|
163
186
|
case "state":
|
|
164
187
|
if (event.state === "failed" || event.state === "disconnected" || event.state === "closed")
|
|
165
|
-
this.fail(c,
|
|
188
|
+
this.fail(c, ReactorError.fromCode("Disconnected", `peer state ${event.state}`, {
|
|
166
189
|
generation: c.generation,
|
|
167
190
|
}));
|
|
168
191
|
else {
|
|
@@ -172,7 +195,10 @@ export class Session {
|
|
|
172
195
|
break;
|
|
173
196
|
case "channel":
|
|
174
197
|
if (!event.open) {
|
|
175
|
-
this.fail(c,
|
|
198
|
+
this.fail(c, ReactorError.fromCode("ChannelClosed", `${event.channel} channel closed`, {
|
|
199
|
+
generation: c.generation,
|
|
200
|
+
detail: { channel: event.channel },
|
|
201
|
+
}));
|
|
176
202
|
break;
|
|
177
203
|
}
|
|
178
204
|
if (event.channel === "control")
|
|
@@ -187,7 +213,7 @@ export class Session {
|
|
|
187
213
|
else {
|
|
188
214
|
c.iceBytes += event.candidate.candidate.length * 2;
|
|
189
215
|
if (c.iceBuffer.length >= 256 || c.iceBytes > 262_144 || c.finalSent) {
|
|
190
|
-
this.fail(c,
|
|
216
|
+
this.fail(c, ReactorError.fromCode("Overflow", "ICE buffer bound or candidate after final batch"));
|
|
191
217
|
break;
|
|
192
218
|
}
|
|
193
219
|
c.iceBuffer.push(event.candidate);
|
|
@@ -215,8 +241,7 @@ export class Session {
|
|
|
215
241
|
if (channel === "data") {
|
|
216
242
|
const message = W.DataServerMessage.decode(bytes), payload = message.payload;
|
|
217
243
|
if (payload?.case === "error") {
|
|
218
|
-
const error =
|
|
219
|
-
remoteCode: payload.value.code,
|
|
244
|
+
const error = remoteError(payload.value, {
|
|
220
245
|
requestId: message.request_id,
|
|
221
246
|
generation: c.generation,
|
|
222
247
|
outcome: "replied",
|
|
@@ -260,13 +285,12 @@ export class Session {
|
|
|
260
285
|
if (payload === undefined) {
|
|
261
286
|
this.emit({
|
|
262
287
|
_tag: "Diagnostic",
|
|
263
|
-
error:
|
|
288
|
+
error: ReactorError.fromCode("Protocol", "bodyless control response does not resolve a request", { requestId: message.request_id }),
|
|
264
289
|
}, bytes.length);
|
|
265
290
|
return;
|
|
266
291
|
}
|
|
267
292
|
const result = payload.case === "error"
|
|
268
|
-
? Effect.fail(
|
|
269
|
-
remoteCode: payload.value.code,
|
|
293
|
+
? Effect.fail(remoteError(payload.value, {
|
|
270
294
|
requestId: message.request_id,
|
|
271
295
|
outcome: "replied",
|
|
272
296
|
detail: message,
|
|
@@ -283,7 +307,7 @@ export class Session {
|
|
|
283
307
|
else if (payload.case === "error")
|
|
284
308
|
c.pendingClaims.delete(message.request_id);
|
|
285
309
|
else {
|
|
286
|
-
this.fail(c,
|
|
310
|
+
this.fail(c, ReactorError.fromCode("UnexpectedReply", "publisher claim reply did not identify the requested track", {
|
|
287
311
|
operation: "publish_track",
|
|
288
312
|
requestId: message.request_id,
|
|
289
313
|
outcome: "unknown",
|
|
@@ -297,7 +321,11 @@ export class Session {
|
|
|
297
321
|
}
|
|
298
322
|
}
|
|
299
323
|
catch (cause) {
|
|
300
|
-
|
|
324
|
+
// Decoding rejects a malformed reply with a ReactorError; anything else it
|
|
325
|
+
// throws is a bug and propagates to the peer rather than becoming typed.
|
|
326
|
+
if (!ReactorError.is(cause))
|
|
327
|
+
throw cause;
|
|
328
|
+
this.emit({ _tag: "Diagnostic", error: cause }, bytes.length);
|
|
301
329
|
}
|
|
302
330
|
}
|
|
303
331
|
flushIce(c) {
|
|
@@ -319,9 +347,9 @@ export class Session {
|
|
|
319
347
|
background(c, body) {
|
|
320
348
|
return Effect.forkIn(body.pipe(Effect.raceFirst(Deferred.await(c.failed)), Effect.catchCause((cause) => Effect.sync(() => {
|
|
321
349
|
const failure = Cause.findError(cause);
|
|
322
|
-
this.fail(c, failure._tag === "Success" && failure.success
|
|
350
|
+
this.fail(c, failure._tag === "Success" && ReactorError.is(failure.success)
|
|
323
351
|
? failure.success
|
|
324
|
-
:
|
|
352
|
+
: ReactorError.fromCode("Protocol", "session task failed", {
|
|
325
353
|
detail: cause,
|
|
326
354
|
generation: c.generation,
|
|
327
355
|
}));
|
|
@@ -340,12 +368,14 @@ export class Session {
|
|
|
340
368
|
});
|
|
341
369
|
yield* Scope.addFinalizer(c.scope, Effect.gen(function* () {
|
|
342
370
|
if (c.failure === undefined)
|
|
343
|
-
self.fail(c,
|
|
371
|
+
self.fail(c, ReactorError.fromCode("Aborted", "connection scope closed", {
|
|
372
|
+
generation: c.generation,
|
|
373
|
+
}));
|
|
344
374
|
if (c.peer.shutdown !== undefined)
|
|
345
|
-
yield* c.peer.shutdown
|
|
375
|
+
yield* c.peer.shutdown.pipe(Effect.orDie);
|
|
346
376
|
}));
|
|
347
377
|
if (previous !== undefined) {
|
|
348
|
-
self.fail(previous,
|
|
378
|
+
self.fail(previous, ReactorError.fromCode("Disconnected", "connection retired for reconnect"));
|
|
349
379
|
yield* Scope.close(previous.scope, Exit.void);
|
|
350
380
|
}
|
|
351
381
|
return c;
|
|
@@ -376,9 +406,7 @@ export class Session {
|
|
|
376
406
|
const connection = this.currentReady();
|
|
377
407
|
const media = connection.peer.rawMedia;
|
|
378
408
|
if (media === undefined)
|
|
379
|
-
throw
|
|
380
|
-
outcome: "not-submitted",
|
|
381
|
-
});
|
|
409
|
+
throw ReactorError.fromCode("UnsupportedCapability", "peer has no decoded media capability", { outcome: "not-submitted" });
|
|
382
410
|
const owned = (source) => Stream.transformPull(source, (pull) => Effect.succeed(pure(() => this.assertCurrent(connection)).pipe(Effect.andThen(pull), Effect.raceFirst(Deferred.await(connection.failed)), Effect.tap(() => pure(() => this.assertCurrent(connection))))));
|
|
383
411
|
return Object.freeze({
|
|
384
412
|
generation: connection.generation,
|
|
@@ -394,9 +422,7 @@ export class Session {
|
|
|
394
422
|
return pure(() => {
|
|
395
423
|
const connection = this.currentReady();
|
|
396
424
|
if (connection.peer.nativeTracks !== true) {
|
|
397
|
-
throw
|
|
398
|
-
outcome: "not-submitted",
|
|
399
|
-
});
|
|
425
|
+
throw ReactorError.fromCode("UnsupportedCapability", "peer has no browser track capability", { outcome: "not-submitted" });
|
|
400
426
|
}
|
|
401
427
|
return Object.freeze({
|
|
402
428
|
generation: connection.generation,
|
|
@@ -421,19 +447,22 @@ export class Session {
|
|
|
421
447
|
const self = this;
|
|
422
448
|
return Effect.gen(function* () {
|
|
423
449
|
const c = yield* self.begin(reconnect);
|
|
450
|
+
yield* Effect.annotateCurrentSpan("reactor.connection.generation", c.generation);
|
|
424
451
|
const work = Effect.gen(function* () {
|
|
425
452
|
if (!reconnect)
|
|
426
453
|
yield* self.guard(c, self.allocate());
|
|
427
454
|
self.assertCurrent(c);
|
|
428
455
|
self.lifecycle.transition("waiting");
|
|
429
456
|
const remote = self.currentRemote();
|
|
457
|
+
yield* Effect.annotateCurrentSpan("reactor.session.id", remote.id);
|
|
430
458
|
const descriptor = yield* self.guard(c, self.http.ready(remote.id, reconnect ? undefined : remote.descriptor));
|
|
431
459
|
remote.descriptor = descriptor;
|
|
460
|
+
yield* phase("described");
|
|
432
461
|
const capabilities = descriptor.capabilities, transport = descriptor.selected_transport;
|
|
433
462
|
if (capabilities === undefined || transport === undefined)
|
|
434
|
-
return yield*
|
|
463
|
+
return yield* ReactorError.fromCode("Protocol", "missing ready capabilities/transport");
|
|
435
464
|
if (transport.protocol !== "webrtc" || transport.version !== "1.0")
|
|
436
|
-
return yield*
|
|
465
|
+
return yield* ReactorError.fromCode("VersionMismatch", `unsupported transport ${transport.protocol}/${transport.version}`);
|
|
437
466
|
const readyDescriptor = Object.freeze({
|
|
438
467
|
...descriptor,
|
|
439
468
|
capabilities,
|
|
@@ -442,10 +471,12 @@ export class Session {
|
|
|
442
471
|
const servers = yield* self.guard(c, self.http.iceServers(remote.id));
|
|
443
472
|
const prepared = yield* self.guard(c, Scope.provide(c.peer.prepare(servers, capabilities.tracks, (event) => self.onPeer(c, event)), c.scope));
|
|
444
473
|
c.mapping = prepared.mapping;
|
|
474
|
+
yield* phase("prepared");
|
|
445
475
|
const previousId = remote.connectionId;
|
|
446
476
|
const cid = previousId ?? (yield* self.guard(c, self.http.register(remote.id)));
|
|
447
477
|
c.connectionId = cid;
|
|
448
478
|
remote.connectionId = cid;
|
|
479
|
+
yield* phase("registered");
|
|
449
480
|
// Source order: registration -> buffered ICE (including empty final) -> offer -> poll answer.
|
|
450
481
|
yield* self.flushIce(c);
|
|
451
482
|
yield* self.background(c, Effect.gen(function* () {
|
|
@@ -455,22 +486,28 @@ export class Session {
|
|
|
455
486
|
}
|
|
456
487
|
}));
|
|
457
488
|
yield* self.guard(c, self.http.offer(remote.id, cid, prepared.sdp, prepared.mapping, reconnect && previousId !== undefined));
|
|
489
|
+
yield* phase("offered");
|
|
458
490
|
const answer = yield* self.guard(c, self.http.answer(remote.id, cid));
|
|
459
491
|
if (answer.connection_id !== undefined) {
|
|
460
492
|
c.connectionId = answer.connection_id;
|
|
461
493
|
remote.connectionId = answer.connection_id;
|
|
462
494
|
}
|
|
463
495
|
yield* self.guard(c, c.peer.answer(answer.sdp_answer));
|
|
496
|
+
yield* phase("answered");
|
|
464
497
|
yield* withDeadline(self.guard(c, Deferred.await(c.ready)), self.readyTimeout, "peer and both channels ready");
|
|
465
498
|
self.assertCurrent(c);
|
|
466
|
-
|
|
499
|
+
yield* phase("ready");
|
|
500
|
+
self.lifecycle.ready(c, Object.freeze({
|
|
467
501
|
ownership: remote.ownership,
|
|
468
502
|
sessionId: remote.id,
|
|
469
503
|
descriptor: readyDescriptor,
|
|
470
504
|
connectionId: c.connectionId,
|
|
471
|
-
});
|
|
472
|
-
|
|
473
|
-
|
|
505
|
+
}));
|
|
506
|
+
// Each connection subscribes to its own receive-only tracks, an attached
|
|
507
|
+
// one as much as the one that created the session: hosted Reactor holds
|
|
508
|
+
// a connection's media until that connection resumes them. A data-only
|
|
509
|
+
// peer has no tracks to resume.
|
|
510
|
+
if ((self.options.autoResumeTracks ?? true) && c.peer.mediaSupported !== false)
|
|
474
511
|
for (const track of capabilities.tracks)
|
|
475
512
|
if (track.direction === "recvonly") {
|
|
476
513
|
const outcome = yield* Effect.result(self.setTrackActive(track.name, true));
|
|
@@ -482,7 +519,7 @@ export class Session {
|
|
|
482
519
|
if (outcome._tag === "Failure")
|
|
483
520
|
self.emit({ _tag: "Diagnostic", error: outcome.failure });
|
|
484
521
|
}
|
|
485
|
-
if (self.heartbeat
|
|
522
|
+
if (Duration.isFinite(self.heartbeat))
|
|
486
523
|
yield* self.background(c, Effect.gen(function* () {
|
|
487
524
|
while (true) {
|
|
488
525
|
yield* self.ping();
|
|
@@ -494,15 +531,17 @@ export class Session {
|
|
|
494
531
|
? Effect.gen(function* () {
|
|
495
532
|
self.remote.allocationLost();
|
|
496
533
|
const error = Cause.findError(exit.cause);
|
|
497
|
-
self.fail(c, error._tag === "Success" && error.success
|
|
534
|
+
self.fail(c, error._tag === "Success" && ReactorError.is(error.success)
|
|
498
535
|
? error.success
|
|
499
|
-
:
|
|
536
|
+
: ReactorError.fromCode("Aborted", "connection attempt interrupted"));
|
|
500
537
|
yield* Scope.close(c.scope, Exit.void);
|
|
501
538
|
})
|
|
502
539
|
: Effect.void));
|
|
503
|
-
})
|
|
540
|
+
}).pipe(
|
|
541
|
+
// A caller-boundary span with an event per phase; the caller can cancel it.
|
|
542
|
+
Effect.withSpan(reconnect ? "reactor.session.reconnect" : "reactor.session.connect", { kind: "client" }, { captureStackTrace: false }));
|
|
504
543
|
}
|
|
505
|
-
request(c, correlator, operation, encode, channel,
|
|
544
|
+
request(c, correlator, operation, encode, channel, deadline, publication) {
|
|
506
545
|
const self = this;
|
|
507
546
|
return Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
|
|
508
547
|
const pending = yield* pure(() => {
|
|
@@ -515,20 +554,26 @@ export class Session {
|
|
|
515
554
|
if (encoded._tag === "Failure") {
|
|
516
555
|
correlator.cancel(pending);
|
|
517
556
|
c.pendingClaims.delete(pending.id);
|
|
518
|
-
return yield*
|
|
519
|
-
|
|
557
|
+
return yield* new ReactorError({
|
|
558
|
+
reason: encoded.failure.reason,
|
|
559
|
+
context: {
|
|
560
|
+
...encoded.failure.context,
|
|
561
|
+
operation,
|
|
562
|
+
requestId: pending.id,
|
|
563
|
+
generation: c.generation,
|
|
564
|
+
outcome: "not-submitted",
|
|
565
|
+
},
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
const failure = (error) => new ReactorError({
|
|
569
|
+
reason: error.reason,
|
|
570
|
+
context: {
|
|
571
|
+
...error.context,
|
|
520
572
|
operation,
|
|
521
573
|
requestId: pending.id,
|
|
522
574
|
generation: c.generation,
|
|
523
|
-
outcome: "not-submitted",
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
const failure = (error) => new ReactorError(error.code, error.message, {
|
|
527
|
-
...error.context,
|
|
528
|
-
operation,
|
|
529
|
-
requestId: pending.id,
|
|
530
|
-
generation: c.generation,
|
|
531
|
-
outcome: error.context.outcome ?? (pending.submitted ? "unknown" : "not-submitted"),
|
|
575
|
+
outcome: error.context.outcome ?? (pending.submitted ? "unknown" : "not-submitted"),
|
|
576
|
+
},
|
|
532
577
|
});
|
|
533
578
|
const sending = pure(() => {
|
|
534
579
|
self.assertCurrent(c);
|
|
@@ -549,7 +594,7 @@ export class Session {
|
|
|
549
594
|
// native backend remains responsible for any foreign operation slot.
|
|
550
595
|
yield* Effect.forkIn(sending, yield* Effect.scope, { startImmediately: true });
|
|
551
596
|
return yield* Deferred.await(pending.deferred);
|
|
552
|
-
})).pipe(Effect.interruptible, (effect) => withDeadline(effect,
|
|
597
|
+
})).pipe(Effect.interruptible, (effect) => withDeadline(effect, deadline, operation), Effect.mapError(failure), Effect.onExit((exit) => Effect.sync(() => {
|
|
553
598
|
if (!pending.submitted) {
|
|
554
599
|
correlator.cancel(pending);
|
|
555
600
|
c.pendingClaims.delete(pending.id);
|
|
@@ -558,29 +603,56 @@ export class Session {
|
|
|
558
603
|
pending.waiting = false;
|
|
559
604
|
// Unresolved submitted records remain bounded by maxPending. A late
|
|
560
605
|
// reply or retiring this generation releases the slot; a timeout does not.
|
|
561
|
-
}))
|
|
606
|
+
})),
|
|
607
|
+
// The span covers the owned execution, not the caller's wait: the wire has no
|
|
608
|
+
// cancel, so it ends with the request's own outcome even after the caller stops
|
|
609
|
+
// waiting. It records identity and dispatch evidence only; failure messages are
|
|
610
|
+
// library-written, so its Exit carries no input, reply or provider text.
|
|
611
|
+
Effect.onExit((exit) => {
|
|
612
|
+
const error = Exit.findError(exit);
|
|
613
|
+
return Effect.annotateCurrentSpan(Exit.isSuccess(exit)
|
|
614
|
+
? { "reactor.command.outcome": "replied" }
|
|
615
|
+
: error._tag === "Success"
|
|
616
|
+
? {
|
|
617
|
+
"reactor.command.outcome": error.success.context.outcome,
|
|
618
|
+
"error.type": error.success.reason._tag,
|
|
619
|
+
}
|
|
620
|
+
: // No typed failure: the connection scope closed, or a defect.
|
|
621
|
+
{ "reactor.command.outcome": pending.submitted ? "unknown" : "not-submitted" });
|
|
622
|
+
}), Effect.withSpan(channel === "data" ? "reactor.session.command" : "reactor.session.control", {
|
|
623
|
+
kind: "client",
|
|
624
|
+
attributes: {
|
|
625
|
+
"reactor.operation": operation,
|
|
626
|
+
"reactor.request.id": pending.id,
|
|
627
|
+
"reactor.connection.generation": c.generation,
|
|
628
|
+
},
|
|
629
|
+
}, { captureStackTrace: false }));
|
|
562
630
|
const owner = yield* Effect.forkIn(execution, c.scope, { startImmediately: true });
|
|
563
631
|
return yield* restore(Fiber.join(owner));
|
|
564
632
|
}));
|
|
565
633
|
}
|
|
566
|
-
command(type, data,
|
|
567
|
-
return pure(() =>
|
|
568
|
-
|
|
634
|
+
command(type, data, options = {}) {
|
|
635
|
+
return pure(() => this.currentReady()).pipe(
|
|
636
|
+
// The caller's name, data, uploads and deadline: a rejection is InvalidInput.
|
|
637
|
+
Effect.flatMap((c) => parsedInput(() => ({
|
|
638
|
+
c,
|
|
639
|
+
deadline: options.replyTimeout === undefined
|
|
640
|
+
? this.replyTimeout
|
|
641
|
+
: replyTimeout(options.replyTimeout),
|
|
642
|
+
payload: {
|
|
569
643
|
type: nonempty(type, "command type"),
|
|
570
644
|
data: structFromObject(data),
|
|
571
|
-
uploads: captureUploads(uploads),
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
return { c, payload };
|
|
575
|
-
}).pipe(Effect.mapError((error) => new CommandFailure(error, {
|
|
645
|
+
uploads: captureUploads(options.uploads ?? new Map()),
|
|
646
|
+
},
|
|
647
|
+
}), type)), Effect.mapError((error) => CommandFailure.from(error, {
|
|
576
648
|
...error.context,
|
|
577
649
|
operation: type,
|
|
578
650
|
outcome: "not-submitted",
|
|
579
|
-
})), Effect.flatMap(({ c, payload }) => this.request(c, this.data, type, (id) => W.DataClientMessage.encode({
|
|
651
|
+
})), Effect.flatMap(({ c, deadline, payload }) => this.request(c, this.data, type, (id) => W.DataClientMessage.encode({
|
|
580
652
|
request_id: id,
|
|
581
653
|
kind: 1,
|
|
582
654
|
payload: { case: "command", value: payload },
|
|
583
|
-
}), "data",
|
|
655
|
+
}), "data", deadline).pipe(Effect.mapError((error) => {
|
|
584
656
|
const { outcome, requestId, generation } = error.context;
|
|
585
657
|
if (outcome === "unknown" || outcome === "replied") {
|
|
586
658
|
if (requestId === undefined || generation === undefined)
|
|
@@ -592,9 +664,9 @@ export class Session {
|
|
|
592
664
|
requestId,
|
|
593
665
|
generation,
|
|
594
666
|
};
|
|
595
|
-
return
|
|
667
|
+
return CommandFailure.from(error, context);
|
|
596
668
|
}
|
|
597
|
-
return
|
|
669
|
+
return CommandFailure.from(error, {
|
|
598
670
|
...error.context,
|
|
599
671
|
operation: type,
|
|
600
672
|
outcome: "not-submitted",
|
|
@@ -606,13 +678,13 @@ export class Session {
|
|
|
606
678
|
const connection = expected ?? this.currentReady();
|
|
607
679
|
this.assertCurrent(connection);
|
|
608
680
|
return connection;
|
|
609
|
-
}).pipe(Effect.flatMap((c) => this.request(c, this.control, operation, (id) => W.ControlClientMessage.encode({ request_id: id, kind: 1, payload }), "control", this.
|
|
681
|
+
}).pipe(Effect.flatMap((c) => this.request(c, this.control, operation, (id) => W.ControlClientMessage.encode({ request_id: id, kind: 1, payload }), "control", this.replyTimeout, payload.case === "publish_track" ? payload.value.name : undefined)));
|
|
610
682
|
}
|
|
611
683
|
notification(c, payload) {
|
|
612
684
|
return pure(() => {
|
|
613
685
|
this.assertCurrent(c);
|
|
614
686
|
return W.ControlClientMessage.encode({ request_id: "", kind: 3, payload });
|
|
615
|
-
}).pipe(Effect.flatMap((bytes) => this.guard(c, c.peer.send("control", bytes))), (effect) => withDeadline(effect, this.
|
|
687
|
+
}).pipe(Effect.flatMap((bytes) => this.guard(c, c.peer.send("control", bytes))), (effect) => withDeadline(effect, this.replyTimeout, "control notification"));
|
|
616
688
|
}
|
|
617
689
|
ping() {
|
|
618
690
|
return pure(() => this.currentReady()).pipe(Effect.flatMap((c) => this.notification(c, { case: "ping", value: {} })));
|
|
@@ -620,7 +692,7 @@ export class Session {
|
|
|
620
692
|
schema() {
|
|
621
693
|
return this.controlRequest("request_schema", { case: "request_schema", value: {} }).pipe(Effect.flatMap((reply) => pure(() => {
|
|
622
694
|
if (reply.case !== "model_schema")
|
|
623
|
-
throw
|
|
695
|
+
throw ReactorError.fromCode("UnexpectedReply", `schema reply was ${reply.case}`, {
|
|
624
696
|
outcome: "replied",
|
|
625
697
|
});
|
|
626
698
|
return {
|
|
@@ -634,23 +706,41 @@ export class Session {
|
|
|
634
706
|
clip(payload) {
|
|
635
707
|
return this.controlRequest(payload.case, payload).pipe(Effect.flatMap((reply) => pure(() => {
|
|
636
708
|
if (reply.case === "clip_failed")
|
|
637
|
-
throw new ReactorError(
|
|
638
|
-
|
|
639
|
-
|
|
709
|
+
throw new ReactorError({
|
|
710
|
+
reason: new Remote({
|
|
711
|
+
// The one regex classification of provider free text, a deliberate exception:
|
|
712
|
+
// ClipFailed carries only a reason string, and a caller must tell a disabled
|
|
713
|
+
// recorder from other clip failures. Add no others; drop this once the wire
|
|
714
|
+
// carries a code.
|
|
715
|
+
_tag: /recorder disabled|encoder crashed/i.test(reply.value.reason)
|
|
716
|
+
? "RecorderDisabled"
|
|
717
|
+
: "Remote",
|
|
718
|
+
message: "clip failed",
|
|
719
|
+
body: reply.value.reason,
|
|
720
|
+
}),
|
|
721
|
+
context: { outcome: "replied" },
|
|
722
|
+
});
|
|
640
723
|
if (reply.case !== "clip_ready")
|
|
641
|
-
throw
|
|
724
|
+
throw ReactorError.fromCode("UnexpectedReply", `clip reply was ${reply.case}`, {
|
|
642
725
|
outcome: "replied",
|
|
643
726
|
});
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
}
|
|
727
|
+
let playlist;
|
|
728
|
+
try {
|
|
729
|
+
playlist = new URL(reply.value.playlist_url, `${this.http.apiUrl}/`);
|
|
730
|
+
}
|
|
731
|
+
catch (cause) {
|
|
732
|
+
throw ReactorError.fromCode("Protocol", "clip playlist URL is malformed", {
|
|
733
|
+
outcome: "replied",
|
|
734
|
+
detail: cause,
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
return { ...reply.value, playlist_url: playlist.href };
|
|
648
738
|
})));
|
|
649
739
|
}
|
|
650
740
|
requestClip(seconds) {
|
|
651
741
|
return pure(() => {
|
|
652
742
|
if (!Number.isFinite(seconds) || seconds <= 0)
|
|
653
|
-
throw
|
|
743
|
+
throw ReactorError.fromCode("Protocol", "clip duration must be finite and positive");
|
|
654
744
|
}).pipe(Effect.andThen(this.clip({ case: "request_clip", value: { duration_seconds: seconds } })));
|
|
655
745
|
}
|
|
656
746
|
recording() {
|
|
@@ -658,10 +748,10 @@ export class Session {
|
|
|
658
748
|
}
|
|
659
749
|
namedTrack(name, connection) {
|
|
660
750
|
if (connection.peer.mediaSupported === false)
|
|
661
|
-
throw
|
|
751
|
+
throw ReactorError.fromCode("UnsupportedCapability", "This peer is data-only; track/media operations are unavailable", { outcome: "not-submitted" });
|
|
662
752
|
const track = connection.negotiated.descriptor.capabilities.tracks.find((t) => t.name === name);
|
|
663
753
|
if (track === undefined)
|
|
664
|
-
throw
|
|
754
|
+
throw ReactorError.fromCode("InvalidState", `unknown track: ${name}`);
|
|
665
755
|
return track;
|
|
666
756
|
}
|
|
667
757
|
trackOperation(name, body, expected) {
|
|
@@ -670,7 +760,7 @@ export class Session {
|
|
|
670
760
|
this.assertCurrent(c);
|
|
671
761
|
const track = this.namedTrack(name, c);
|
|
672
762
|
if (c.trackBusy.has(name))
|
|
673
|
-
throw
|
|
763
|
+
throw ReactorError.fromCode("InvalidState", `another track operation is in flight: ${name}`);
|
|
674
764
|
c.trackBusy.add(name);
|
|
675
765
|
return { c, track };
|
|
676
766
|
}), ({ c, track }) => this.guard(c, body(c, track)), ({ c }) => Effect.sync(() => {
|
|
@@ -702,17 +792,17 @@ export class Session {
|
|
|
702
792
|
self.assertCurrent(c);
|
|
703
793
|
invoked = true;
|
|
704
794
|
}).pipe(Effect.andThen(Effect.suspend(() => c.peer.replace(name, clone))), Effect.timeoutOrElse({
|
|
705
|
-
duration: self.
|
|
795
|
+
duration: self.replyTimeout,
|
|
706
796
|
orElse: () => {
|
|
707
797
|
deadline = true;
|
|
708
|
-
return Effect.fail(
|
|
798
|
+
return Effect.fail(ReactorError.fromCode("Timeout", "sender replacement deadline; local generation retired", { operation: name, outcome: "unknown" }));
|
|
709
799
|
},
|
|
710
800
|
}));
|
|
711
801
|
yield* restore(mutation).pipe(Effect.onExit((exit) => Exit.isFailure(exit)
|
|
712
802
|
? Effect.sync(() => {
|
|
713
803
|
clone?.stop();
|
|
714
804
|
if (invoked && (deadline || Cause.hasInterrupts(exit.cause)))
|
|
715
|
-
self.fail(c,
|
|
805
|
+
self.fail(c, ReactorError.fromCode(deadline ? "Timeout" : "Disconnected", "native sender replacement abandoned; reconnect explicitly before reusing publication", { operation: name, outcome: "unknown" }));
|
|
716
806
|
})
|
|
717
807
|
: Effect.void));
|
|
718
808
|
// No interruptible boundary between successful mutation and recording its owner.
|
|
@@ -738,17 +828,15 @@ export class Session {
|
|
|
738
828
|
const self = this;
|
|
739
829
|
return this.trackOperation(name, (c, track) => Effect.gen(function* () {
|
|
740
830
|
if (c.peer.nativeTracks === false)
|
|
741
|
-
return yield*
|
|
831
|
+
return yield* ReactorError.fromCode("UnsupportedCapability", "this peer does not accept browser media tracks", { outcome: "not-submitted" });
|
|
742
832
|
if (track.direction !== "sendonly" ||
|
|
743
833
|
source.kind !== track.kind ||
|
|
744
834
|
source.readyState !== "live")
|
|
745
|
-
return yield*
|
|
835
|
+
return yield* ReactorError.fromCode("InvalidState", "publish requires a live matching input track");
|
|
746
836
|
if (!c.claimed.has(name)) {
|
|
747
837
|
const reply = yield* self.controlRequest("publish_track", { case: "publish_track", value: { name } }, c);
|
|
748
838
|
if (reply.case !== "publish_track" || reply.value.name !== name)
|
|
749
|
-
return yield*
|
|
750
|
-
outcome: "replied",
|
|
751
|
-
}));
|
|
839
|
+
return yield* ReactorError.fromCode("UnexpectedReply", "publisher claim reply mismatch", { outcome: "replied" });
|
|
752
840
|
c.claimed.add(name);
|
|
753
841
|
}
|
|
754
842
|
yield* self.replaceSender(c, name, source);
|
|
@@ -756,7 +844,7 @@ export class Session {
|
|
|
756
844
|
}
|
|
757
845
|
unpublish(name, expected) {
|
|
758
846
|
return this.trackOperation(name, (c) => c.peer.nativeTracks === false
|
|
759
|
-
? Effect.fail(
|
|
847
|
+
? Effect.fail(ReactorError.fromCode("UnsupportedCapability", "this peer does not expose browser track publication", { outcome: "not-submitted" }))
|
|
760
848
|
: this.replaceSender(c, name, null).pipe(Effect.andThen(this.notification(c, { case: "unpublish_track", value: { name } })), Effect.tap(() => Effect.sync(() => {
|
|
761
849
|
c.claimed.delete(name);
|
|
762
850
|
}))), expected);
|
|
@@ -766,7 +854,7 @@ export class Session {
|
|
|
766
854
|
const c = expected ?? this.currentReady();
|
|
767
855
|
this.assertCurrent(c);
|
|
768
856
|
if (c.peer.nativeTracks === false)
|
|
769
|
-
throw
|
|
857
|
+
throw ReactorError.fromCode("UnsupportedCapability", "this peer exposes owned decoded frames instead of browser tracks", { outcome: "not-submitted" });
|
|
770
858
|
return { peer: c.peer, track: c.peer.lease(name) };
|
|
771
859
|
}), ({ peer, track }) => Effect.sync(() => peer.release(track))).pipe(Effect.map(({ track }) => track));
|
|
772
860
|
}
|
|
@@ -779,7 +867,7 @@ export class Session {
|
|
|
779
867
|
const self = this;
|
|
780
868
|
return Effect.gen(function* () {
|
|
781
869
|
const connection = yield* pure(() => self.currentReady());
|
|
782
|
-
const raw = yield* self.guard(connection, connection.peer.stats
|
|
870
|
+
const raw = yield* self.guard(connection, connection.peer.stats);
|
|
783
871
|
// Elapsed sampling time belongs to this Effect boundary, not the pure sampler.
|
|
784
872
|
const atNanos = yield* Clock.monotonicTimeNanos;
|
|
785
873
|
const atMs = Number(atNanos) / 1_000_000;
|
|
@@ -787,9 +875,9 @@ export class Session {
|
|
|
787
875
|
});
|
|
788
876
|
}
|
|
789
877
|
rawStats() {
|
|
790
|
-
return pure(() => this.currentReady()).pipe(Effect.flatMap((c) => this.guard(c, c.peer.stats
|
|
878
|
+
return pure(() => this.currentReady()).pipe(Effect.flatMap((c) => this.guard(c, c.peer.stats)));
|
|
791
879
|
}
|
|
792
|
-
upload(name, mimeType, bytes,
|
|
880
|
+
upload(name, mimeType, bytes, options = {}) {
|
|
793
881
|
const self = this;
|
|
794
882
|
return Effect.suspend(() => {
|
|
795
883
|
let progress = {
|
|
@@ -803,7 +891,6 @@ export class Session {
|
|
|
803
891
|
nonempty(name, "upload name");
|
|
804
892
|
nonempty(mimeType, "upload MIME type");
|
|
805
893
|
positiveLimit(bytes.byteLength, "upload size", self.uploadBound);
|
|
806
|
-
positiveLimit(timeoutMs, "upload timeout", 600_000);
|
|
807
894
|
return { c, remote, copy: new Uint8Array(bytes) };
|
|
808
895
|
});
|
|
809
896
|
progress = { ...progress, allocation: "unknown" };
|
|
@@ -825,12 +912,22 @@ export class Session {
|
|
|
825
912
|
progress = { ...progress, notification: "submitted" };
|
|
826
913
|
return { file, transfer: "confirmed", notification: "submitted" };
|
|
827
914
|
});
|
|
828
|
-
return
|
|
915
|
+
return pure(() => options.uploadTimeout === undefined
|
|
916
|
+
? self.uploadTimeout
|
|
917
|
+
: uploadTimeout(options.uploadTimeout)).pipe(Effect.flatMap((deadline) => withDeadline(operation, deadline, "upload")), Effect.mapError((e) => ReactorError.fromCode("Upload", e.message, {
|
|
829
918
|
...e.context,
|
|
830
919
|
operation: "upload",
|
|
831
920
|
detail: { cause: e, progress: { ...progress } },
|
|
832
921
|
})), Effect.ensuring(Effect.sync(() => self.emit({ _tag: "Upload", progress: Object.freeze({ ...progress }) }))));
|
|
833
|
-
})
|
|
922
|
+
}).pipe(
|
|
923
|
+
// The MIME type and size only: never the name or the bytes.
|
|
924
|
+
Effect.withSpan("reactor.session.upload", {
|
|
925
|
+
kind: "client",
|
|
926
|
+
attributes: {
|
|
927
|
+
"reactor.upload.mime_type": mimeType,
|
|
928
|
+
"reactor.upload.size": bytes.byteLength,
|
|
929
|
+
},
|
|
930
|
+
}, { captureStackTrace: false }));
|
|
834
931
|
}
|
|
835
932
|
/** Idempotent close. Owned-session termination is attempted once; response is not terminal proof. */
|
|
836
933
|
close() {
|
|
@@ -842,27 +939,33 @@ export class Session {
|
|
|
842
939
|
self.closeGate = gate;
|
|
843
940
|
return Effect.gen(function* () {
|
|
844
941
|
self.lifecycle.transition("closing");
|
|
845
|
-
Deferred.doneUnsafe(self.lifecycle.closing, Effect.fail(
|
|
942
|
+
Deferred.doneUnsafe(self.lifecycle.closing, Effect.fail(ReactorError.fromCode("Closed", "session closing")));
|
|
846
943
|
const report = yield* cleanupSession({
|
|
847
944
|
connection: self.lifecycle.connection,
|
|
848
945
|
scope: self.lifecycle.scope,
|
|
849
946
|
remote: self.remote,
|
|
850
947
|
http: self.http,
|
|
851
|
-
|
|
948
|
+
replyTimeout: self.replyTimeout,
|
|
852
949
|
retire: (connection, error) => self.fail(connection, error),
|
|
853
950
|
});
|
|
854
951
|
self.closeReport = report;
|
|
855
952
|
self.lifecycle.transition("closed");
|
|
856
953
|
self.observations.end();
|
|
857
954
|
Deferred.doneUnsafe(gate, Effect.succeed(report));
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
955
|
+
// Consumer callbacks cannot undo or stall cleanup.
|
|
956
|
+
yield* Effect.ignore(Effect.try(() => self.options.onClose?.(report)));
|
|
957
|
+
yield* Effect.annotateCurrentSpan({
|
|
958
|
+
"reactor.close.local_closed": report.localClosed,
|
|
959
|
+
"reactor.close.allocation": report.allocation,
|
|
960
|
+
...terminationAttributes(report.remote),
|
|
961
|
+
});
|
|
864
962
|
return report;
|
|
865
|
-
})
|
|
963
|
+
}).pipe(
|
|
964
|
+
// Only the close that runs cleanup is traced; a later close awaits its report.
|
|
965
|
+
Effect.withSpan("reactor.session.close", {
|
|
966
|
+
kind: "client",
|
|
967
|
+
attributes: self.remote.id === undefined ? {} : { "reactor.session.id": self.remote.id },
|
|
968
|
+
}, { captureStackTrace: false }));
|
|
866
969
|
}));
|
|
867
970
|
}
|
|
868
971
|
isKnownTerminal() {
|