reactor-effect-client 0.2.0 → 0.3.0-rc.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 +62 -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 +168 -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 +210 -125
- 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 +48 -0
- package/dist/h3/_internal/operations.d.ts.map +1 -0
- package/dist/h3/_internal/operations.js +230 -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 +108 -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 +208 -109
- 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.map +1 -1
- package/dist/stats.js +4 -5
- 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,21 +486,23 @@ 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
|
-
self.lifecycle.transition("ready");
|
|
505
|
+
}));
|
|
473
506
|
if (self.options.autoResumeTracks ?? self.options.intent._tag === "Create")
|
|
474
507
|
for (const track of capabilities.tracks)
|
|
475
508
|
if (track.direction === "recvonly") {
|
|
@@ -482,7 +515,7 @@ export class Session {
|
|
|
482
515
|
if (outcome._tag === "Failure")
|
|
483
516
|
self.emit({ _tag: "Diagnostic", error: outcome.failure });
|
|
484
517
|
}
|
|
485
|
-
if (self.heartbeat
|
|
518
|
+
if (Duration.isFinite(self.heartbeat))
|
|
486
519
|
yield* self.background(c, Effect.gen(function* () {
|
|
487
520
|
while (true) {
|
|
488
521
|
yield* self.ping();
|
|
@@ -494,15 +527,17 @@ export class Session {
|
|
|
494
527
|
? Effect.gen(function* () {
|
|
495
528
|
self.remote.allocationLost();
|
|
496
529
|
const error = Cause.findError(exit.cause);
|
|
497
|
-
self.fail(c, error._tag === "Success" && error.success
|
|
530
|
+
self.fail(c, error._tag === "Success" && ReactorError.is(error.success)
|
|
498
531
|
? error.success
|
|
499
|
-
:
|
|
532
|
+
: ReactorError.fromCode("Aborted", "connection attempt interrupted"));
|
|
500
533
|
yield* Scope.close(c.scope, Exit.void);
|
|
501
534
|
})
|
|
502
535
|
: Effect.void));
|
|
503
|
-
})
|
|
536
|
+
}).pipe(
|
|
537
|
+
// A caller-boundary span with an event per phase; the caller can cancel it.
|
|
538
|
+
Effect.withSpan(reconnect ? "reactor.session.reconnect" : "reactor.session.connect", { kind: "client" }, { captureStackTrace: false }));
|
|
504
539
|
}
|
|
505
|
-
request(c, correlator, operation, encode, channel,
|
|
540
|
+
request(c, correlator, operation, encode, channel, deadline, publication) {
|
|
506
541
|
const self = this;
|
|
507
542
|
return Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
|
|
508
543
|
const pending = yield* pure(() => {
|
|
@@ -515,20 +550,26 @@ export class Session {
|
|
|
515
550
|
if (encoded._tag === "Failure") {
|
|
516
551
|
correlator.cancel(pending);
|
|
517
552
|
c.pendingClaims.delete(pending.id);
|
|
518
|
-
return yield*
|
|
519
|
-
|
|
553
|
+
return yield* new ReactorError({
|
|
554
|
+
reason: encoded.failure.reason,
|
|
555
|
+
context: {
|
|
556
|
+
...encoded.failure.context,
|
|
557
|
+
operation,
|
|
558
|
+
requestId: pending.id,
|
|
559
|
+
generation: c.generation,
|
|
560
|
+
outcome: "not-submitted",
|
|
561
|
+
},
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
const failure = (error) => new ReactorError({
|
|
565
|
+
reason: error.reason,
|
|
566
|
+
context: {
|
|
567
|
+
...error.context,
|
|
520
568
|
operation,
|
|
521
569
|
requestId: pending.id,
|
|
522
570
|
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"),
|
|
571
|
+
outcome: error.context.outcome ?? (pending.submitted ? "unknown" : "not-submitted"),
|
|
572
|
+
},
|
|
532
573
|
});
|
|
533
574
|
const sending = pure(() => {
|
|
534
575
|
self.assertCurrent(c);
|
|
@@ -549,7 +590,7 @@ export class Session {
|
|
|
549
590
|
// native backend remains responsible for any foreign operation slot.
|
|
550
591
|
yield* Effect.forkIn(sending, yield* Effect.scope, { startImmediately: true });
|
|
551
592
|
return yield* Deferred.await(pending.deferred);
|
|
552
|
-
})).pipe(Effect.interruptible, (effect) => withDeadline(effect,
|
|
593
|
+
})).pipe(Effect.interruptible, (effect) => withDeadline(effect, deadline, operation), Effect.mapError(failure), Effect.onExit((exit) => Effect.sync(() => {
|
|
553
594
|
if (!pending.submitted) {
|
|
554
595
|
correlator.cancel(pending);
|
|
555
596
|
c.pendingClaims.delete(pending.id);
|
|
@@ -558,29 +599,56 @@ export class Session {
|
|
|
558
599
|
pending.waiting = false;
|
|
559
600
|
// Unresolved submitted records remain bounded by maxPending. A late
|
|
560
601
|
// reply or retiring this generation releases the slot; a timeout does not.
|
|
561
|
-
}))
|
|
602
|
+
})),
|
|
603
|
+
// The span covers the owned execution, not the caller's wait: the wire has no
|
|
604
|
+
// cancel, so it ends with the request's own outcome even after the caller stops
|
|
605
|
+
// waiting. It records identity and dispatch evidence only; failure messages are
|
|
606
|
+
// library-written, so its Exit carries no input, reply or provider text.
|
|
607
|
+
Effect.onExit((exit) => {
|
|
608
|
+
const error = Exit.findError(exit);
|
|
609
|
+
return Effect.annotateCurrentSpan(Exit.isSuccess(exit)
|
|
610
|
+
? { "reactor.command.outcome": "replied" }
|
|
611
|
+
: error._tag === "Success"
|
|
612
|
+
? {
|
|
613
|
+
"reactor.command.outcome": error.success.context.outcome,
|
|
614
|
+
"error.type": error.success.reason._tag,
|
|
615
|
+
}
|
|
616
|
+
: // No typed failure: the connection scope closed, or a defect.
|
|
617
|
+
{ "reactor.command.outcome": pending.submitted ? "unknown" : "not-submitted" });
|
|
618
|
+
}), Effect.withSpan(channel === "data" ? "reactor.session.command" : "reactor.session.control", {
|
|
619
|
+
kind: "client",
|
|
620
|
+
attributes: {
|
|
621
|
+
"reactor.operation": operation,
|
|
622
|
+
"reactor.request.id": pending.id,
|
|
623
|
+
"reactor.connection.generation": c.generation,
|
|
624
|
+
},
|
|
625
|
+
}, { captureStackTrace: false }));
|
|
562
626
|
const owner = yield* Effect.forkIn(execution, c.scope, { startImmediately: true });
|
|
563
627
|
return yield* restore(Fiber.join(owner));
|
|
564
628
|
}));
|
|
565
629
|
}
|
|
566
|
-
command(type, data,
|
|
567
|
-
return pure(() =>
|
|
568
|
-
|
|
630
|
+
command(type, data, options = {}) {
|
|
631
|
+
return pure(() => this.currentReady()).pipe(
|
|
632
|
+
// The caller's name, data, uploads and deadline: a rejection is InvalidInput.
|
|
633
|
+
Effect.flatMap((c) => parsedInput(() => ({
|
|
634
|
+
c,
|
|
635
|
+
deadline: options.replyTimeout === undefined
|
|
636
|
+
? this.replyTimeout
|
|
637
|
+
: replyTimeout(options.replyTimeout),
|
|
638
|
+
payload: {
|
|
569
639
|
type: nonempty(type, "command type"),
|
|
570
640
|
data: structFromObject(data),
|
|
571
|
-
uploads: captureUploads(uploads),
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
return { c, payload };
|
|
575
|
-
}).pipe(Effect.mapError((error) => new CommandFailure(error, {
|
|
641
|
+
uploads: captureUploads(options.uploads ?? new Map()),
|
|
642
|
+
},
|
|
643
|
+
}), type)), Effect.mapError((error) => CommandFailure.from(error, {
|
|
576
644
|
...error.context,
|
|
577
645
|
operation: type,
|
|
578
646
|
outcome: "not-submitted",
|
|
579
|
-
})), Effect.flatMap(({ c, payload }) => this.request(c, this.data, type, (id) => W.DataClientMessage.encode({
|
|
647
|
+
})), Effect.flatMap(({ c, deadline, payload }) => this.request(c, this.data, type, (id) => W.DataClientMessage.encode({
|
|
580
648
|
request_id: id,
|
|
581
649
|
kind: 1,
|
|
582
650
|
payload: { case: "command", value: payload },
|
|
583
|
-
}), "data",
|
|
651
|
+
}), "data", deadline).pipe(Effect.mapError((error) => {
|
|
584
652
|
const { outcome, requestId, generation } = error.context;
|
|
585
653
|
if (outcome === "unknown" || outcome === "replied") {
|
|
586
654
|
if (requestId === undefined || generation === undefined)
|
|
@@ -592,9 +660,9 @@ export class Session {
|
|
|
592
660
|
requestId,
|
|
593
661
|
generation,
|
|
594
662
|
};
|
|
595
|
-
return
|
|
663
|
+
return CommandFailure.from(error, context);
|
|
596
664
|
}
|
|
597
|
-
return
|
|
665
|
+
return CommandFailure.from(error, {
|
|
598
666
|
...error.context,
|
|
599
667
|
operation: type,
|
|
600
668
|
outcome: "not-submitted",
|
|
@@ -606,13 +674,13 @@ export class Session {
|
|
|
606
674
|
const connection = expected ?? this.currentReady();
|
|
607
675
|
this.assertCurrent(connection);
|
|
608
676
|
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.
|
|
677
|
+
}).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
678
|
}
|
|
611
679
|
notification(c, payload) {
|
|
612
680
|
return pure(() => {
|
|
613
681
|
this.assertCurrent(c);
|
|
614
682
|
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.
|
|
683
|
+
}).pipe(Effect.flatMap((bytes) => this.guard(c, c.peer.send("control", bytes))), (effect) => withDeadline(effect, this.replyTimeout, "control notification"));
|
|
616
684
|
}
|
|
617
685
|
ping() {
|
|
618
686
|
return pure(() => this.currentReady()).pipe(Effect.flatMap((c) => this.notification(c, { case: "ping", value: {} })));
|
|
@@ -620,7 +688,7 @@ export class Session {
|
|
|
620
688
|
schema() {
|
|
621
689
|
return this.controlRequest("request_schema", { case: "request_schema", value: {} }).pipe(Effect.flatMap((reply) => pure(() => {
|
|
622
690
|
if (reply.case !== "model_schema")
|
|
623
|
-
throw
|
|
691
|
+
throw ReactorError.fromCode("UnexpectedReply", `schema reply was ${reply.case}`, {
|
|
624
692
|
outcome: "replied",
|
|
625
693
|
});
|
|
626
694
|
return {
|
|
@@ -634,23 +702,41 @@ export class Session {
|
|
|
634
702
|
clip(payload) {
|
|
635
703
|
return this.controlRequest(payload.case, payload).pipe(Effect.flatMap((reply) => pure(() => {
|
|
636
704
|
if (reply.case === "clip_failed")
|
|
637
|
-
throw new ReactorError(
|
|
638
|
-
|
|
639
|
-
|
|
705
|
+
throw new ReactorError({
|
|
706
|
+
reason: new Remote({
|
|
707
|
+
// The one regex classification of provider free text, a deliberate exception:
|
|
708
|
+
// ClipFailed carries only a reason string, and a caller must tell a disabled
|
|
709
|
+
// recorder from other clip failures. Add no others; drop this once the wire
|
|
710
|
+
// carries a code.
|
|
711
|
+
_tag: /recorder disabled|encoder crashed/i.test(reply.value.reason)
|
|
712
|
+
? "RecorderDisabled"
|
|
713
|
+
: "Remote",
|
|
714
|
+
message: "clip failed",
|
|
715
|
+
body: reply.value.reason,
|
|
716
|
+
}),
|
|
717
|
+
context: { outcome: "replied" },
|
|
718
|
+
});
|
|
640
719
|
if (reply.case !== "clip_ready")
|
|
641
|
-
throw
|
|
720
|
+
throw ReactorError.fromCode("UnexpectedReply", `clip reply was ${reply.case}`, {
|
|
642
721
|
outcome: "replied",
|
|
643
722
|
});
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
}
|
|
723
|
+
let playlist;
|
|
724
|
+
try {
|
|
725
|
+
playlist = new URL(reply.value.playlist_url, `${this.http.apiUrl}/`);
|
|
726
|
+
}
|
|
727
|
+
catch (cause) {
|
|
728
|
+
throw ReactorError.fromCode("Protocol", "clip playlist URL is malformed", {
|
|
729
|
+
outcome: "replied",
|
|
730
|
+
detail: cause,
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
return { ...reply.value, playlist_url: playlist.href };
|
|
648
734
|
})));
|
|
649
735
|
}
|
|
650
736
|
requestClip(seconds) {
|
|
651
737
|
return pure(() => {
|
|
652
738
|
if (!Number.isFinite(seconds) || seconds <= 0)
|
|
653
|
-
throw
|
|
739
|
+
throw ReactorError.fromCode("Protocol", "clip duration must be finite and positive");
|
|
654
740
|
}).pipe(Effect.andThen(this.clip({ case: "request_clip", value: { duration_seconds: seconds } })));
|
|
655
741
|
}
|
|
656
742
|
recording() {
|
|
@@ -658,10 +744,10 @@ export class Session {
|
|
|
658
744
|
}
|
|
659
745
|
namedTrack(name, connection) {
|
|
660
746
|
if (connection.peer.mediaSupported === false)
|
|
661
|
-
throw
|
|
747
|
+
throw ReactorError.fromCode("UnsupportedCapability", "This peer is data-only; track/media operations are unavailable", { outcome: "not-submitted" });
|
|
662
748
|
const track = connection.negotiated.descriptor.capabilities.tracks.find((t) => t.name === name);
|
|
663
749
|
if (track === undefined)
|
|
664
|
-
throw
|
|
750
|
+
throw ReactorError.fromCode("InvalidState", `unknown track: ${name}`);
|
|
665
751
|
return track;
|
|
666
752
|
}
|
|
667
753
|
trackOperation(name, body, expected) {
|
|
@@ -670,7 +756,7 @@ export class Session {
|
|
|
670
756
|
this.assertCurrent(c);
|
|
671
757
|
const track = this.namedTrack(name, c);
|
|
672
758
|
if (c.trackBusy.has(name))
|
|
673
|
-
throw
|
|
759
|
+
throw ReactorError.fromCode("InvalidState", `another track operation is in flight: ${name}`);
|
|
674
760
|
c.trackBusy.add(name);
|
|
675
761
|
return { c, track };
|
|
676
762
|
}), ({ c, track }) => this.guard(c, body(c, track)), ({ c }) => Effect.sync(() => {
|
|
@@ -702,17 +788,17 @@ export class Session {
|
|
|
702
788
|
self.assertCurrent(c);
|
|
703
789
|
invoked = true;
|
|
704
790
|
}).pipe(Effect.andThen(Effect.suspend(() => c.peer.replace(name, clone))), Effect.timeoutOrElse({
|
|
705
|
-
duration: self.
|
|
791
|
+
duration: self.replyTimeout,
|
|
706
792
|
orElse: () => {
|
|
707
793
|
deadline = true;
|
|
708
|
-
return Effect.fail(
|
|
794
|
+
return Effect.fail(ReactorError.fromCode("Timeout", "sender replacement deadline; local generation retired", { operation: name, outcome: "unknown" }));
|
|
709
795
|
},
|
|
710
796
|
}));
|
|
711
797
|
yield* restore(mutation).pipe(Effect.onExit((exit) => Exit.isFailure(exit)
|
|
712
798
|
? Effect.sync(() => {
|
|
713
799
|
clone?.stop();
|
|
714
800
|
if (invoked && (deadline || Cause.hasInterrupts(exit.cause)))
|
|
715
|
-
self.fail(c,
|
|
801
|
+
self.fail(c, ReactorError.fromCode(deadline ? "Timeout" : "Disconnected", "native sender replacement abandoned; reconnect explicitly before reusing publication", { operation: name, outcome: "unknown" }));
|
|
716
802
|
})
|
|
717
803
|
: Effect.void));
|
|
718
804
|
// No interruptible boundary between successful mutation and recording its owner.
|
|
@@ -738,17 +824,15 @@ export class Session {
|
|
|
738
824
|
const self = this;
|
|
739
825
|
return this.trackOperation(name, (c, track) => Effect.gen(function* () {
|
|
740
826
|
if (c.peer.nativeTracks === false)
|
|
741
|
-
return yield*
|
|
827
|
+
return yield* ReactorError.fromCode("UnsupportedCapability", "this peer does not accept browser media tracks", { outcome: "not-submitted" });
|
|
742
828
|
if (track.direction !== "sendonly" ||
|
|
743
829
|
source.kind !== track.kind ||
|
|
744
830
|
source.readyState !== "live")
|
|
745
|
-
return yield*
|
|
831
|
+
return yield* ReactorError.fromCode("InvalidState", "publish requires a live matching input track");
|
|
746
832
|
if (!c.claimed.has(name)) {
|
|
747
833
|
const reply = yield* self.controlRequest("publish_track", { case: "publish_track", value: { name } }, c);
|
|
748
834
|
if (reply.case !== "publish_track" || reply.value.name !== name)
|
|
749
|
-
return yield*
|
|
750
|
-
outcome: "replied",
|
|
751
|
-
}));
|
|
835
|
+
return yield* ReactorError.fromCode("UnexpectedReply", "publisher claim reply mismatch", { outcome: "replied" });
|
|
752
836
|
c.claimed.add(name);
|
|
753
837
|
}
|
|
754
838
|
yield* self.replaceSender(c, name, source);
|
|
@@ -756,7 +840,7 @@ export class Session {
|
|
|
756
840
|
}
|
|
757
841
|
unpublish(name, expected) {
|
|
758
842
|
return this.trackOperation(name, (c) => c.peer.nativeTracks === false
|
|
759
|
-
? Effect.fail(
|
|
843
|
+
? Effect.fail(ReactorError.fromCode("UnsupportedCapability", "this peer does not expose browser track publication", { outcome: "not-submitted" }))
|
|
760
844
|
: this.replaceSender(c, name, null).pipe(Effect.andThen(this.notification(c, { case: "unpublish_track", value: { name } })), Effect.tap(() => Effect.sync(() => {
|
|
761
845
|
c.claimed.delete(name);
|
|
762
846
|
}))), expected);
|
|
@@ -766,7 +850,7 @@ export class Session {
|
|
|
766
850
|
const c = expected ?? this.currentReady();
|
|
767
851
|
this.assertCurrent(c);
|
|
768
852
|
if (c.peer.nativeTracks === false)
|
|
769
|
-
throw
|
|
853
|
+
throw ReactorError.fromCode("UnsupportedCapability", "this peer exposes owned decoded frames instead of browser tracks", { outcome: "not-submitted" });
|
|
770
854
|
return { peer: c.peer, track: c.peer.lease(name) };
|
|
771
855
|
}), ({ peer, track }) => Effect.sync(() => peer.release(track))).pipe(Effect.map(({ track }) => track));
|
|
772
856
|
}
|
|
@@ -779,7 +863,7 @@ export class Session {
|
|
|
779
863
|
const self = this;
|
|
780
864
|
return Effect.gen(function* () {
|
|
781
865
|
const connection = yield* pure(() => self.currentReady());
|
|
782
|
-
const raw = yield* self.guard(connection, connection.peer.stats
|
|
866
|
+
const raw = yield* self.guard(connection, connection.peer.stats);
|
|
783
867
|
// Elapsed sampling time belongs to this Effect boundary, not the pure sampler.
|
|
784
868
|
const atNanos = yield* Clock.monotonicTimeNanos;
|
|
785
869
|
const atMs = Number(atNanos) / 1_000_000;
|
|
@@ -787,9 +871,9 @@ export class Session {
|
|
|
787
871
|
});
|
|
788
872
|
}
|
|
789
873
|
rawStats() {
|
|
790
|
-
return pure(() => this.currentReady()).pipe(Effect.flatMap((c) => this.guard(c, c.peer.stats
|
|
874
|
+
return pure(() => this.currentReady()).pipe(Effect.flatMap((c) => this.guard(c, c.peer.stats)));
|
|
791
875
|
}
|
|
792
|
-
upload(name, mimeType, bytes,
|
|
876
|
+
upload(name, mimeType, bytes, options = {}) {
|
|
793
877
|
const self = this;
|
|
794
878
|
return Effect.suspend(() => {
|
|
795
879
|
let progress = {
|
|
@@ -803,7 +887,6 @@ export class Session {
|
|
|
803
887
|
nonempty(name, "upload name");
|
|
804
888
|
nonempty(mimeType, "upload MIME type");
|
|
805
889
|
positiveLimit(bytes.byteLength, "upload size", self.uploadBound);
|
|
806
|
-
positiveLimit(timeoutMs, "upload timeout", 600_000);
|
|
807
890
|
return { c, remote, copy: new Uint8Array(bytes) };
|
|
808
891
|
});
|
|
809
892
|
progress = { ...progress, allocation: "unknown" };
|
|
@@ -825,12 +908,22 @@ export class Session {
|
|
|
825
908
|
progress = { ...progress, notification: "submitted" };
|
|
826
909
|
return { file, transfer: "confirmed", notification: "submitted" };
|
|
827
910
|
});
|
|
828
|
-
return
|
|
911
|
+
return pure(() => options.uploadTimeout === undefined
|
|
912
|
+
? self.uploadTimeout
|
|
913
|
+
: uploadTimeout(options.uploadTimeout)).pipe(Effect.flatMap((deadline) => withDeadline(operation, deadline, "upload")), Effect.mapError((e) => ReactorError.fromCode("Upload", e.message, {
|
|
829
914
|
...e.context,
|
|
830
915
|
operation: "upload",
|
|
831
916
|
detail: { cause: e, progress: { ...progress } },
|
|
832
917
|
})), Effect.ensuring(Effect.sync(() => self.emit({ _tag: "Upload", progress: Object.freeze({ ...progress }) }))));
|
|
833
|
-
})
|
|
918
|
+
}).pipe(
|
|
919
|
+
// The MIME type and size only: never the name or the bytes.
|
|
920
|
+
Effect.withSpan("reactor.session.upload", {
|
|
921
|
+
kind: "client",
|
|
922
|
+
attributes: {
|
|
923
|
+
"reactor.upload.mime_type": mimeType,
|
|
924
|
+
"reactor.upload.size": bytes.byteLength,
|
|
925
|
+
},
|
|
926
|
+
}, { captureStackTrace: false }));
|
|
834
927
|
}
|
|
835
928
|
/** Idempotent close. Owned-session termination is attempted once; response is not terminal proof. */
|
|
836
929
|
close() {
|
|
@@ -842,27 +935,33 @@ export class Session {
|
|
|
842
935
|
self.closeGate = gate;
|
|
843
936
|
return Effect.gen(function* () {
|
|
844
937
|
self.lifecycle.transition("closing");
|
|
845
|
-
Deferred.doneUnsafe(self.lifecycle.closing, Effect.fail(
|
|
938
|
+
Deferred.doneUnsafe(self.lifecycle.closing, Effect.fail(ReactorError.fromCode("Closed", "session closing")));
|
|
846
939
|
const report = yield* cleanupSession({
|
|
847
940
|
connection: self.lifecycle.connection,
|
|
848
941
|
scope: self.lifecycle.scope,
|
|
849
942
|
remote: self.remote,
|
|
850
943
|
http: self.http,
|
|
851
|
-
|
|
944
|
+
replyTimeout: self.replyTimeout,
|
|
852
945
|
retire: (connection, error) => self.fail(connection, error),
|
|
853
946
|
});
|
|
854
947
|
self.closeReport = report;
|
|
855
948
|
self.lifecycle.transition("closed");
|
|
856
949
|
self.observations.end();
|
|
857
950
|
Deferred.doneUnsafe(gate, Effect.succeed(report));
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
951
|
+
// Consumer callbacks cannot undo or stall cleanup.
|
|
952
|
+
yield* Effect.ignore(Effect.try(() => self.options.onClose?.(report)));
|
|
953
|
+
yield* Effect.annotateCurrentSpan({
|
|
954
|
+
"reactor.close.local_closed": report.localClosed,
|
|
955
|
+
"reactor.close.allocation": report.allocation,
|
|
956
|
+
...terminationAttributes(report.remote),
|
|
957
|
+
});
|
|
864
958
|
return report;
|
|
865
|
-
})
|
|
959
|
+
}).pipe(
|
|
960
|
+
// Only the close that runs cleanup is traced; a later close awaits its report.
|
|
961
|
+
Effect.withSpan("reactor.session.close", {
|
|
962
|
+
kind: "client",
|
|
963
|
+
attributes: self.remote.id === undefined ? {} : { "reactor.session.id": self.remote.id },
|
|
964
|
+
}, { captureStackTrace: false }));
|
|
866
965
|
}));
|
|
867
966
|
}
|
|
868
967
|
isKnownTerminal() {
|