reactor-effect-client 0.0.0-reserved.0 → 0.2.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/LICENSE +201 -0
- package/NOTICE +29 -0
- package/README.md +124 -2
- package/dist/FetchHttp.d.ts +9 -0
- package/dist/FetchHttp.d.ts.map +1 -0
- package/dist/FetchHttp.js +63 -0
- package/dist/FetchHttp.js.map +1 -0
- package/dist/PeerFactory.d.ts +15 -0
- package/dist/PeerFactory.d.ts.map +1 -0
- package/dist/PeerFactory.js +4 -0
- package/dist/PeerFactory.js.map +1 -0
- package/dist/PeerTypes.d.ts +64 -0
- package/dist/PeerTypes.d.ts.map +1 -0
- package/dist/PeerTypes.js +2 -0
- package/dist/PeerTypes.js.map +1 -0
- package/dist/Sequence.d.ts +81 -0
- package/dist/Sequence.d.ts.map +1 -0
- package/dist/Sequence.js +215 -0
- package/dist/Sequence.js.map +1 -0
- package/dist/SessionTypes.d.ts +164 -0
- package/dist/SessionTypes.d.ts.map +1 -0
- package/dist/SessionTypes.js +2 -0
- package/dist/SessionTypes.js.map +1 -0
- package/dist/Submission.d.ts +42 -0
- package/dist/Submission.d.ts.map +1 -0
- package/dist/Submission.js +52 -0
- package/dist/Submission.js.map +1 -0
- package/dist/bytes.d.ts +6 -0
- package/dist/bytes.d.ts.map +1 -0
- package/dist/bytes.js +32 -0
- package/dist/bytes.js.map +1 -0
- package/dist/contract.d.ts +58 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +87 -0
- package/dist/contract.js.map +1 -0
- package/dist/coordinator/_internal/client.d.ts +127 -0
- package/dist/coordinator/_internal/client.d.ts.map +1 -0
- package/dist/coordinator/_internal/client.js +450 -0
- package/dist/coordinator/_internal/client.js.map +1 -0
- package/dist/coordinator/_internal/response.d.ts +15 -0
- package/dist/coordinator/_internal/response.d.ts.map +1 -0
- package/dist/coordinator/_internal/response.js +57 -0
- package/dist/coordinator/_internal/response.js.map +1 -0
- package/dist/coordinator/_internal/schemas.d.ts +51 -0
- package/dist/coordinator/_internal/schemas.d.ts.map +1 -0
- package/dist/coordinator/_internal/schemas.js +170 -0
- package/dist/coordinator/_internal/schemas.js.map +1 -0
- package/dist/coordinator/index.d.ts +28 -0
- package/dist/coordinator/index.d.ts.map +1 -0
- package/dist/coordinator/index.js +25 -0
- package/dist/coordinator/index.js.map +1 -0
- package/dist/correlation.d.ts +32 -0
- package/dist/correlation.d.ts.map +1 -0
- package/dist/correlation.js +113 -0
- package/dist/correlation.js.map +1 -0
- package/dist/errors.d.ts +88 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +93 -0
- package/dist/errors.js.map +1 -0
- package/dist/h3/_internal/client.d.ts +9 -0
- package/dist/h3/_internal/client.d.ts.map +1 -0
- package/dist/h3/_internal/client.js +510 -0
- package/dist/h3/_internal/client.js.map +1 -0
- package/dist/h3/_internal/contracts.d.ts +123 -0
- package/dist/h3/_internal/contracts.d.ts.map +1 -0
- package/dist/h3/_internal/contracts.js +113 -0
- package/dist/h3/_internal/contracts.js.map +1 -0
- package/dist/h3/_internal/deployment.d.ts +8 -0
- package/dist/h3/_internal/deployment.d.ts.map +1 -0
- package/dist/h3/_internal/deployment.js +132 -0
- package/dist/h3/_internal/deployment.js.map +1 -0
- package/dist/h3/_internal/evidence.d.ts +17 -0
- package/dist/h3/_internal/evidence.d.ts.map +1 -0
- package/dist/h3/_internal/evidence.js +48 -0
- package/dist/h3/_internal/evidence.js.map +1 -0
- package/dist/h3/_internal/references.d.ts +18 -0
- package/dist/h3/_internal/references.d.ts.map +1 -0
- package/dist/h3/_internal/references.js +186 -0
- package/dist/h3/_internal/references.js.map +1 -0
- package/dist/h3/_internal/request.d.ts +16 -0
- package/dist/h3/_internal/request.d.ts.map +1 -0
- package/dist/h3/_internal/request.js +79 -0
- package/dist/h3/_internal/request.js.map +1 -0
- package/dist/h3/_internal/state.d.ts +30 -0
- package/dist/h3/_internal/state.d.ts.map +1 -0
- package/dist/h3/_internal/state.js +243 -0
- package/dist/h3/_internal/state.js.map +1 -0
- package/dist/h3/index.d.ts +10 -0
- package/dist/h3/index.d.ts.map +1 -0
- package/dist/h3/index.js +7 -0
- package/dist/h3/index.js.map +1 -0
- package/dist/h3/messages.d.ts +310 -0
- package/dist/h3/messages.d.ts.map +1 -0
- package/dist/h3/messages.js +114 -0
- package/dist/h3/messages.js.map +1 -0
- package/dist/h3/profile.d.ts +109 -0
- package/dist/h3/profile.d.ts.map +1 -0
- package/dist/h3/profile.js +70 -0
- package/dist/h3/profile.js.map +1 -0
- package/dist/h3/types.d.ts +173 -0
- package/dist/h3/types.d.ts.map +1 -0
- package/dist/h3/types.js +2 -0
- package/dist/h3/types.js.map +1 -0
- package/dist/host.d.ts +25 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +20 -0
- package/dist/host.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/json.d.ts +18 -0
- package/dist/json.d.ts.map +1 -0
- package/dist/json.js +123 -0
- package/dist/json.js.map +1 -0
- package/dist/media-stream.d.ts +10 -0
- package/dist/media-stream.d.ts.map +1 -0
- package/dist/media-stream.js +20 -0
- package/dist/media-stream.js.map +1 -0
- package/dist/observation.d.ts +26 -0
- package/dist/observation.d.ts.map +1 -0
- package/dist/observation.js +106 -0
- package/dist/observation.js.map +1 -0
- package/dist/orchestration/h3-source.d.ts +30 -0
- package/dist/orchestration/h3-source.d.ts.map +1 -0
- package/dist/orchestration/h3-source.js +303 -0
- package/dist/orchestration/h3-source.js.map +1 -0
- package/dist/orchestration/index.d.ts +12 -0
- package/dist/orchestration/index.d.ts.map +1 -0
- package/dist/orchestration/index.js +9 -0
- package/dist/orchestration/index.js.map +1 -0
- package/dist/orchestration/media-buffer.d.ts +23 -0
- package/dist/orchestration/media-buffer.d.ts.map +1 -0
- package/dist/orchestration/media-buffer.js +73 -0
- package/dist/orchestration/media-buffer.js.map +1 -0
- package/dist/orchestration/queries.d.ts +10 -0
- package/dist/orchestration/queries.d.ts.map +1 -0
- package/dist/orchestration/queries.js +40 -0
- package/dist/orchestration/queries.js.map +1 -0
- package/dist/orchestration/references.d.ts +16 -0
- package/dist/orchestration/references.d.ts.map +1 -0
- package/dist/orchestration/references.js +76 -0
- package/dist/orchestration/references.js.map +1 -0
- package/dist/orchestration/renewal-state.d.ts +58 -0
- package/dist/orchestration/renewal-state.d.ts.map +1 -0
- package/dist/orchestration/renewal-state.js +60 -0
- package/dist/orchestration/renewal-state.js.map +1 -0
- package/dist/orchestration/renewal.d.ts +76 -0
- package/dist/orchestration/renewal.d.ts.map +1 -0
- package/dist/orchestration/renewal.js +633 -0
- package/dist/orchestration/renewal.js.map +1 -0
- package/dist/orchestration/request.d.ts +53 -0
- package/dist/orchestration/request.d.ts.map +1 -0
- package/dist/orchestration/request.js +122 -0
- package/dist/orchestration/request.js.map +1 -0
- package/dist/orchestration/routing.d.ts +21 -0
- package/dist/orchestration/routing.d.ts.map +1 -0
- package/dist/orchestration/routing.js +90 -0
- package/dist/orchestration/routing.js.map +1 -0
- package/dist/orchestration/source-slot.d.ts +99 -0
- package/dist/orchestration/source-slot.d.ts.map +1 -0
- package/dist/orchestration/source-slot.js +206 -0
- package/dist/orchestration/source-slot.js.map +1 -0
- package/dist/orchestration/types.d.ts +334 -0
- package/dist/orchestration/types.d.ts.map +1 -0
- package/dist/orchestration/types.js +11 -0
- package/dist/orchestration/types.js.map +1 -0
- package/dist/protobuf.d.ts +63 -0
- package/dist/protobuf.d.ts.map +1 -0
- package/dist/protobuf.js +252 -0
- package/dist/protobuf.js.map +1 -0
- package/dist/session/_internal/acquire.d.ts +23 -0
- package/dist/session/_internal/acquire.d.ts.map +1 -0
- package/dist/session/_internal/acquire.js +167 -0
- package/dist/session/_internal/acquire.js.map +1 -0
- package/dist/session/_internal/cleanup.d.ts +18 -0
- package/dist/session/_internal/cleanup.d.ts.map +1 -0
- package/dist/session/_internal/cleanup.js +94 -0
- package/dist/session/_internal/cleanup.js.map +1 -0
- package/dist/session/_internal/lifecycle.d.ts +62 -0
- package/dist/session/_internal/lifecycle.d.ts.map +1 -0
- package/dist/session/_internal/lifecycle.js +132 -0
- package/dist/session/_internal/lifecycle.js.map +1 -0
- package/dist/session/_internal/remote.d.ts +28 -0
- package/dist/session/_internal/remote.d.ts.map +1 -0
- package/dist/session/_internal/remote.js +61 -0
- package/dist/session/_internal/remote.js.map +1 -0
- package/dist/session/_internal/uploads.d.ts +4 -0
- package/dist/session/_internal/uploads.d.ts.map +1 -0
- package/dist/session/_internal/uploads.js +33 -0
- package/dist/session/_internal/uploads.js.map +1 -0
- package/dist/session/commands.d.ts +22 -0
- package/dist/session/commands.d.ts.map +1 -0
- package/dist/session/commands.js +13 -0
- package/dist/session/commands.js.map +1 -0
- package/dist/session/files.d.ts +15 -0
- package/dist/session/files.d.ts.map +1 -0
- package/dist/session/files.js +54 -0
- package/dist/session/files.js.map +1 -0
- package/dist/session/index.d.ts +76 -0
- package/dist/session/index.d.ts.map +1 -0
- package/dist/session/index.js +16 -0
- package/dist/session/index.js.map +1 -0
- package/dist/session/media.d.ts +67 -0
- package/dist/session/media.d.ts.map +1 -0
- package/dist/session/media.js +2 -0
- package/dist/session/media.js.map +1 -0
- package/dist/session.d.ts +104 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +872 -0
- package/dist/session.js.map +1 -0
- package/dist/simulation/_internal/source.d.ts +12 -0
- package/dist/simulation/_internal/source.d.ts.map +1 -0
- package/dist/simulation/_internal/source.js +477 -0
- package/dist/simulation/_internal/source.js.map +1 -0
- package/dist/simulation/index.d.ts +14 -0
- package/dist/simulation/index.d.ts.map +1 -0
- package/dist/simulation/index.js +13 -0
- package/dist/simulation/index.js.map +1 -0
- package/dist/simulation/types.d.ts +29 -0
- package/dist/simulation/types.d.ts.map +1 -0
- package/dist/simulation/types.js +2 -0
- package/dist/simulation/types.js.map +1 -0
- package/dist/stats.d.ts +33 -0
- package/dist/stats.d.ts.map +1 -0
- package/dist/stats.js +134 -0
- package/dist/stats.js.map +1 -0
- package/dist/testing/Faults.d.ts +4 -0
- package/dist/testing/Faults.d.ts.map +1 -0
- package/dist/testing/Faults.js +7 -0
- package/dist/testing/Faults.js.map +1 -0
- package/dist/testing/Png.d.ts +3 -0
- package/dist/testing/Png.d.ts.map +1 -0
- package/dist/testing/Png.js +68 -0
- package/dist/testing/Png.js.map +1 -0
- package/dist/testing/index.d.ts +3 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +3 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/wire.d.ts +7 -0
- package/dist/wire.d.ts.map +1 -0
- package/dist/wire.generated.d.ts +305 -0
- package/dist/wire.generated.d.ts.map +1 -0
- package/dist/wire.generated.js +1257 -0
- package/dist/wire.generated.js.map +1 -0
- package/dist/wire.js +5 -0
- package/dist/wire.js.map +1 -0
- package/notices/effect-LICENSE +21 -0
- package/notices/google-protobuf-LICENSE +32 -0
- package/notices/owned-webrtc/Effect-MIT.txt +21 -0
- package/notices/owned-webrtc/IETF.txt +43 -0
- package/notices/owned-webrtc/LICENSE +21 -0
- package/notices/owned-webrtc/UPSTREAM.md +20 -0
- package/notices/owned-webrtc/VP8-ADAPTATION.md +32 -0
- package/notices/owned-webrtc/VP8-AUTHORS.txt +42 -0
- package/notices/owned-webrtc/VP8-LICENSE.txt +25 -0
- package/notices/owned-webrtc/VP8-PATENTS.txt +21 -0
- package/notices/owned-webrtc/Werift-MIT.txt +21 -0
- package/notices/owned-webrtc/libvpx-v1.12.0-LICENSE.txt +28 -0
- package/notices/owned-webrtc/libvpx-v1.12.0-PATENTS.txt +22 -0
- package/notices/owned-webrtc/rfc3711.txt +3139 -0
- package/notices/owned-webrtc/str0m-MIT.txt +7 -0
- package/notices/owned-webrtc-LICENSE +21 -0
- package/notices/reactor-runtime-LICENSE +201 -0
- package/notices/reactor-runtime-NOTICE +5 -0
- package/notices/reactor-sdk-LICENSE +201 -0
- package/package.json +62 -4
package/dist/session.js
ADDED
|
@@ -0,0 +1,872 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Deferred from "effect/Deferred";
|
|
3
|
+
import * as Fiber from "effect/Fiber";
|
|
4
|
+
import * as Scope from "effect/Scope";
|
|
5
|
+
import * as Exit from "effect/Exit";
|
|
6
|
+
import * as Queue from "effect/Queue";
|
|
7
|
+
import * as Cause from "effect/Cause";
|
|
8
|
+
import * as Clock from "effect/Clock";
|
|
9
|
+
import * as Stream from "effect/Stream";
|
|
10
|
+
import { errorOf, positiveLimit, ReactorError } from "./errors.js";
|
|
11
|
+
import { nonempty, uint32, structFromObject, objectFromStruct } from "./json.js";
|
|
12
|
+
import { CoordinatorClient } from "./coordinator/_internal/client.js";
|
|
13
|
+
import { Correlator } from "./correlation.js";
|
|
14
|
+
import { Observations } from "./observation.js";
|
|
15
|
+
import * as W from "./wire.generated.js";
|
|
16
|
+
import { StatsSampler } from "./stats.js";
|
|
17
|
+
import { CommandFailure } from "./session/commands.js";
|
|
18
|
+
import { captureUploads } from "./session/_internal/uploads.js";
|
|
19
|
+
import { SessionLifecycle } from "./session/_internal/lifecycle.js";
|
|
20
|
+
import { isKnownRemote, RemoteSession } from "./session/_internal/remote.js";
|
|
21
|
+
import { cleanupSession } from "./session/_internal/cleanup.js";
|
|
22
|
+
const pure = (body) => Effect.try({ try: body, catch: errorOf });
|
|
23
|
+
const withDeadline = (effect, ms, operation) => effect.pipe(Effect.timeoutOrElse({
|
|
24
|
+
duration: ms,
|
|
25
|
+
orElse: () => Effect.fail(new ReactorError("Timeout", `${operation}: deadline`)),
|
|
26
|
+
}));
|
|
27
|
+
/** The canonical Reactor state and wire engine for both public host exports.
|
|
28
|
+
* Platform and peer dependencies are supplied by the configured client factory. */
|
|
29
|
+
export class Session {
|
|
30
|
+
options;
|
|
31
|
+
makePeer;
|
|
32
|
+
lifecycle = new SessionLifecycle((status) => this.emit({ _tag: "Status", status }));
|
|
33
|
+
remote = new RemoteSession();
|
|
34
|
+
data;
|
|
35
|
+
control;
|
|
36
|
+
observations = new Observations();
|
|
37
|
+
sequence = 0n;
|
|
38
|
+
sampler = new StatsSampler();
|
|
39
|
+
closeGate;
|
|
40
|
+
closeReport;
|
|
41
|
+
received = new Set();
|
|
42
|
+
bitrates = new Map();
|
|
43
|
+
commandTimeout;
|
|
44
|
+
connectTimeout;
|
|
45
|
+
readyTimeout;
|
|
46
|
+
heartbeat;
|
|
47
|
+
uploadBound;
|
|
48
|
+
http;
|
|
49
|
+
constructor(options, makePeer, http) {
|
|
50
|
+
this.options = options;
|
|
51
|
+
this.makePeer = makePeer;
|
|
52
|
+
if (options.intent._tag === "Attach") {
|
|
53
|
+
nonempty(options.intent.sessionId, "attach sessionId");
|
|
54
|
+
if (options.intent.connectionId !== undefined)
|
|
55
|
+
uint32(options.intent.connectionId, "attach connectionId");
|
|
56
|
+
}
|
|
57
|
+
else
|
|
58
|
+
nonempty(options.intent.model.name, "model name");
|
|
59
|
+
this.http = http;
|
|
60
|
+
this.commandTimeout = positiveLimit(options.commandTimeoutMs ?? 10_000, "command timeout", 600_000);
|
|
61
|
+
this.connectTimeout = positiveLimit(options.connectTimeoutMs ?? 180_000, "connect timeout", 600_000);
|
|
62
|
+
this.readyTimeout = positiveLimit(options.readyTimeoutMs ?? 30_000, "ready timeout", 600_000);
|
|
63
|
+
this.heartbeat = options.heartbeatMs ?? 10_000;
|
|
64
|
+
if (this.heartbeat !== 0)
|
|
65
|
+
positiveLimit(this.heartbeat, "heartbeat interval", 600_000);
|
|
66
|
+
this.uploadBound = positiveLimit(options.maxUploadBytes ?? 16_777_216, "upload byte bound", 64 * 1024 * 1024);
|
|
67
|
+
this.data = new Correlator("data", options.maxPending ?? 128, options.requestNamespace);
|
|
68
|
+
this.control = new Correlator("ctrl", options.maxPending ?? 128, options.requestNamespace);
|
|
69
|
+
}
|
|
70
|
+
get snapshot() {
|
|
71
|
+
const r = this.remote.current, c = this.lifecycle.connection;
|
|
72
|
+
const details = {
|
|
73
|
+
generation: this.lifecycle.generation,
|
|
74
|
+
pending: Object.freeze({ data: this.data.size, control: this.control.size }),
|
|
75
|
+
pausedLocally: Object.freeze([...(c?.paused ?? [])]),
|
|
76
|
+
claimedTracks: Object.freeze([...(c?.claimed ?? [])]),
|
|
77
|
+
receivedTracks: Object.freeze([...this.received]),
|
|
78
|
+
unresolvedPublications: Object.freeze([...(c?.pendingClaims.values() ?? [])]),
|
|
79
|
+
observationOverflows: this.observations.overflowCount,
|
|
80
|
+
subscribers: this.observations.size,
|
|
81
|
+
...(this.lifecycle.lastError === undefined ? {} : { lastError: this.lifecycle.lastError }),
|
|
82
|
+
...(this.closeReport === undefined ? {} : { close: this.closeReport }),
|
|
83
|
+
};
|
|
84
|
+
if (this.lifecycle.status === "ready")
|
|
85
|
+
return Object.freeze({ ...details, status: "ready", remote: this.currentReady().negotiated });
|
|
86
|
+
return Object.freeze({
|
|
87
|
+
...details,
|
|
88
|
+
status: this.lifecycle.status,
|
|
89
|
+
...(r === undefined
|
|
90
|
+
? {}
|
|
91
|
+
: {
|
|
92
|
+
remote: Object.freeze({
|
|
93
|
+
ownership: r.ownership,
|
|
94
|
+
...(isKnownRemote(r)
|
|
95
|
+
? {
|
|
96
|
+
sessionId: r.id,
|
|
97
|
+
...(r.descriptor === undefined ? {} : { descriptor: r.descriptor }),
|
|
98
|
+
...(r.connectionId === undefined ? {} : { connectionId: r.connectionId }),
|
|
99
|
+
}
|
|
100
|
+
: {}),
|
|
101
|
+
}),
|
|
102
|
+
}),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
events(options) {
|
|
106
|
+
return this.observations.stream(options);
|
|
107
|
+
}
|
|
108
|
+
observe(options) {
|
|
109
|
+
const self = this;
|
|
110
|
+
return Effect.gen(function* () {
|
|
111
|
+
const events = yield* self.observations.subscribe(options);
|
|
112
|
+
const initial = self.snapshot;
|
|
113
|
+
const revision = self.sequence;
|
|
114
|
+
return {
|
|
115
|
+
initial,
|
|
116
|
+
revision,
|
|
117
|
+
events: events.pipe(Stream.filter((event) => event.sequence > revision)),
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
emit(event, bytes) {
|
|
122
|
+
this.observations.emit({ ...event, sequence: ++this.sequence, generation: this.lifecycle.generation }, bytes);
|
|
123
|
+
}
|
|
124
|
+
assertCurrent(c) {
|
|
125
|
+
this.lifecycle.assertCurrent(c);
|
|
126
|
+
}
|
|
127
|
+
currentReady() {
|
|
128
|
+
return this.lifecycle.currentReady();
|
|
129
|
+
}
|
|
130
|
+
currentRemote() {
|
|
131
|
+
return this.remote.requireKnown();
|
|
132
|
+
}
|
|
133
|
+
fail(c, error) {
|
|
134
|
+
if (c.failure !== undefined)
|
|
135
|
+
return;
|
|
136
|
+
c.failure = error;
|
|
137
|
+
Deferred.doneUnsafe(c.failed, Effect.fail(error));
|
|
138
|
+
Deferred.doneUnsafe(c.ready, Effect.fail(error));
|
|
139
|
+
Queue.failCauseUnsafe(c.iceWake, Cause.fail(error));
|
|
140
|
+
this.data.failGeneration(c.generation, error);
|
|
141
|
+
this.control.failGeneration(c.generation, error);
|
|
142
|
+
c.peer.close();
|
|
143
|
+
for (const track of c.sending.values())
|
|
144
|
+
track.stop();
|
|
145
|
+
c.sending.clear();
|
|
146
|
+
c.iceBuffer.length = 0;
|
|
147
|
+
c.iceBytes = 0;
|
|
148
|
+
c.claimed.clear();
|
|
149
|
+
c.paused.clear();
|
|
150
|
+
if (this.lifecycle.connection === c)
|
|
151
|
+
this.received.clear();
|
|
152
|
+
if (this.lifecycle.disconnect(c, error)) {
|
|
153
|
+
this.emit({ _tag: "Diagnostic", error });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
guard(c, effect) {
|
|
157
|
+
return pure(() => this.assertCurrent(c)).pipe(Effect.andThen(effect), Effect.raceFirst(Deferred.await(c.failed)), Effect.tap(() => pure(() => this.assertCurrent(c))), Effect.mapError((e) => c.failure ?? e));
|
|
158
|
+
}
|
|
159
|
+
onPeer(c, event) {
|
|
160
|
+
if (!this.lifecycle.accepts(c))
|
|
161
|
+
return;
|
|
162
|
+
switch (event.type) {
|
|
163
|
+
case "state":
|
|
164
|
+
if (event.state === "failed" || event.state === "disconnected" || event.state === "closed")
|
|
165
|
+
this.fail(c, new ReactorError("Disconnected", `peer state ${event.state}`, {
|
|
166
|
+
generation: c.generation,
|
|
167
|
+
}));
|
|
168
|
+
else {
|
|
169
|
+
c.peerConnected = event.state === "connected";
|
|
170
|
+
c.readyGate();
|
|
171
|
+
}
|
|
172
|
+
break;
|
|
173
|
+
case "channel":
|
|
174
|
+
if (!event.open) {
|
|
175
|
+
this.fail(c, new ReactorError("Disconnected", `${event.channel} channel closed`));
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
if (event.channel === "control")
|
|
179
|
+
c.controlOpen = true;
|
|
180
|
+
else
|
|
181
|
+
c.dataOpen = true;
|
|
182
|
+
c.readyGate();
|
|
183
|
+
break;
|
|
184
|
+
case "ice":
|
|
185
|
+
if (event.candidate === undefined)
|
|
186
|
+
c.iceDone = true;
|
|
187
|
+
else {
|
|
188
|
+
c.iceBytes += event.candidate.candidate.length * 2;
|
|
189
|
+
if (c.iceBuffer.length >= 256 || c.iceBytes > 262_144 || c.finalSent) {
|
|
190
|
+
this.fail(c, new ReactorError("Overflow", "ICE buffer bound or candidate after final batch"));
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
c.iceBuffer.push(event.candidate);
|
|
194
|
+
}
|
|
195
|
+
Queue.offerUnsafe(c.iceWake, undefined);
|
|
196
|
+
break;
|
|
197
|
+
case "decoded":
|
|
198
|
+
this.received.add(event.name);
|
|
199
|
+
this.emit({ _tag: "Decoded", kind: event.kind, name: event.name, mid: event.mid });
|
|
200
|
+
break;
|
|
201
|
+
case "track":
|
|
202
|
+
this.received.add(event.name);
|
|
203
|
+
this.emit({ _tag: "Track", name: event.name, mid: event.mid });
|
|
204
|
+
break;
|
|
205
|
+
case "error":
|
|
206
|
+
this.fail(c, event.error);
|
|
207
|
+
break;
|
|
208
|
+
case "message":
|
|
209
|
+
this.receive(c, event.channel, event.bytes);
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
receive(c, channel, bytes) {
|
|
214
|
+
try {
|
|
215
|
+
if (channel === "data") {
|
|
216
|
+
const message = W.DataServerMessage.decode(bytes), payload = message.payload;
|
|
217
|
+
if (payload?.case === "error") {
|
|
218
|
+
const error = new ReactorError("Remote", payload.value.message, {
|
|
219
|
+
remoteCode: payload.value.code,
|
|
220
|
+
requestId: message.request_id,
|
|
221
|
+
generation: c.generation,
|
|
222
|
+
outcome: "replied",
|
|
223
|
+
detail: message,
|
|
224
|
+
});
|
|
225
|
+
this.data.settleWith(message.request_id, c.generation, (correlation) => {
|
|
226
|
+
this.emit({ _tag: "CommandError", requestId: message.request_id, error, correlation }, bytes.length);
|
|
227
|
+
return Effect.fail(error);
|
|
228
|
+
});
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const value = payload === undefined
|
|
232
|
+
? { kind: "ack", raw: message }
|
|
233
|
+
: {
|
|
234
|
+
kind: "message",
|
|
235
|
+
type: payload.value.type,
|
|
236
|
+
...(payload.value.data === undefined
|
|
237
|
+
? {}
|
|
238
|
+
: { data: objectFromStruct(payload.value.data) }),
|
|
239
|
+
raw: message,
|
|
240
|
+
};
|
|
241
|
+
this.data.settleWith(message.request_id, c.generation, (correlation) => {
|
|
242
|
+
const reply = Object.freeze({
|
|
243
|
+
...value,
|
|
244
|
+
_tag: "Model",
|
|
245
|
+
outcome: "replied",
|
|
246
|
+
requestId: message.request_id,
|
|
247
|
+
generation: c.generation,
|
|
248
|
+
sequence: ++this.sequence,
|
|
249
|
+
correlation,
|
|
250
|
+
});
|
|
251
|
+
// Publish the exact value that completes the request. Observers cannot
|
|
252
|
+
// delay correlation; overflow terminates only that observation.
|
|
253
|
+
this.observations.emit(reply, bytes.length);
|
|
254
|
+
return Effect.succeed(reply);
|
|
255
|
+
}, value.kind === "ack" ? "acknowledged" : "replied");
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
const message = W.ControlServerMessage.decode(bytes), payload = message.payload;
|
|
259
|
+
// Unlike the data correlator, a bodyless control message is NOT an acknowledgement.
|
|
260
|
+
if (payload === undefined) {
|
|
261
|
+
this.emit({
|
|
262
|
+
_tag: "Diagnostic",
|
|
263
|
+
error: new ReactorError("Protocol", "bodyless control response does not resolve a request", { requestId: message.request_id }),
|
|
264
|
+
}, bytes.length);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const result = payload.case === "error"
|
|
268
|
+
? Effect.fail(new ReactorError("Remote", payload.value.message, {
|
|
269
|
+
remoteCode: payload.value.code,
|
|
270
|
+
requestId: message.request_id,
|
|
271
|
+
outcome: "replied",
|
|
272
|
+
detail: message,
|
|
273
|
+
}))
|
|
274
|
+
: Effect.succeed(payload);
|
|
275
|
+
const claim = c.pendingClaims.get(message.request_id);
|
|
276
|
+
if (claim !== undefined) {
|
|
277
|
+
if (payload.case === "publish_track" && payload.value.name === claim) {
|
|
278
|
+
// Record remote ownership even after the local publish caller stops
|
|
279
|
+
// waiting. Attaching a source still requires that caller to continue.
|
|
280
|
+
c.claimed.add(claim);
|
|
281
|
+
c.pendingClaims.delete(message.request_id);
|
|
282
|
+
}
|
|
283
|
+
else if (payload.case === "error")
|
|
284
|
+
c.pendingClaims.delete(message.request_id);
|
|
285
|
+
else {
|
|
286
|
+
this.fail(c, new ReactorError("UnexpectedReply", "publisher claim reply did not identify the requested track", {
|
|
287
|
+
operation: "publish_track",
|
|
288
|
+
requestId: message.request_id,
|
|
289
|
+
outcome: "unknown",
|
|
290
|
+
}));
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
const correlation = this.control.settle(message.request_id, c.generation, result);
|
|
295
|
+
if (payload.case !== "error" || correlation !== "matched")
|
|
296
|
+
this.emit({ _tag: "Control", message, correlation }, bytes.length);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
catch (cause) {
|
|
300
|
+
this.emit({ _tag: "Diagnostic", error: errorOf(cause, "Protocol") }, bytes.length);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
flushIce(c) {
|
|
304
|
+
const self = this;
|
|
305
|
+
return Effect.gen(function* () {
|
|
306
|
+
self.assertCurrent(c);
|
|
307
|
+
if (c.connectionId === undefined)
|
|
308
|
+
return;
|
|
309
|
+
while (c.iceBuffer.length > 0 || (c.iceDone && !c.finalSent)) {
|
|
310
|
+
const candidates = c.iceBuffer.splice(0);
|
|
311
|
+
c.iceBytes = 0;
|
|
312
|
+
const isFinal = c.iceDone && !c.finalSent;
|
|
313
|
+
yield* self.guard(c, self.http.ice(self.currentRemote().id, c.connectionId, candidates, isFinal));
|
|
314
|
+
if (isFinal)
|
|
315
|
+
c.finalSent = true;
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
background(c, body) {
|
|
320
|
+
return Effect.forkIn(body.pipe(Effect.raceFirst(Deferred.await(c.failed)), Effect.catchCause((cause) => Effect.sync(() => {
|
|
321
|
+
const failure = Cause.findError(cause);
|
|
322
|
+
this.fail(c, failure._tag === "Success" && failure.success instanceof ReactorError
|
|
323
|
+
? failure.success
|
|
324
|
+
: new ReactorError("Protocol", "session task failed", {
|
|
325
|
+
detail: cause,
|
|
326
|
+
generation: c.generation,
|
|
327
|
+
}));
|
|
328
|
+
}))), c.scope).pipe(Effect.asVoid);
|
|
329
|
+
}
|
|
330
|
+
begin(reconnect) {
|
|
331
|
+
const self = this;
|
|
332
|
+
return Effect.gen(function* () {
|
|
333
|
+
const previous = self.lifecycle.connection;
|
|
334
|
+
const c = yield* pure(() => {
|
|
335
|
+
const connection = self.lifecycle.begin(reconnect, self.remote.isKnown, () => self.makePeer());
|
|
336
|
+
self.sampler.reset();
|
|
337
|
+
self.received.clear();
|
|
338
|
+
self.lifecycle.transition("connecting");
|
|
339
|
+
return connection;
|
|
340
|
+
});
|
|
341
|
+
yield* Scope.addFinalizer(c.scope, Effect.gen(function* () {
|
|
342
|
+
if (c.failure === undefined)
|
|
343
|
+
self.fail(c, new ReactorError("Aborted", "connection scope closed", { generation: c.generation }));
|
|
344
|
+
if (c.peer.shutdown !== undefined)
|
|
345
|
+
yield* c.peer.shutdown().pipe(Effect.orDie);
|
|
346
|
+
}));
|
|
347
|
+
if (previous !== undefined) {
|
|
348
|
+
self.fail(previous, new ReactorError("Disconnected", "connection retired for reconnect"));
|
|
349
|
+
yield* Scope.close(previous.scope, Exit.void);
|
|
350
|
+
}
|
|
351
|
+
return c;
|
|
352
|
+
}).pipe(Effect.uninterruptible);
|
|
353
|
+
}
|
|
354
|
+
/** Allocate or identify one remote session, without opening a peer connection. */
|
|
355
|
+
allocate() {
|
|
356
|
+
return this.remote.allocate(this.options, this.http, this.lifecycle);
|
|
357
|
+
}
|
|
358
|
+
get id() {
|
|
359
|
+
return this.remote.id;
|
|
360
|
+
}
|
|
361
|
+
get rawMedia() {
|
|
362
|
+
return this.lifecycle.connection?.peer.rawMedia;
|
|
363
|
+
}
|
|
364
|
+
readyState() {
|
|
365
|
+
return pure(() => {
|
|
366
|
+
const connection = this.currentReady();
|
|
367
|
+
return Object.freeze({
|
|
368
|
+
status: "ready",
|
|
369
|
+
generation: connection.generation,
|
|
370
|
+
remote: connection.negotiated,
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
mediaGeneration() {
|
|
375
|
+
return pure(() => {
|
|
376
|
+
const connection = this.currentReady();
|
|
377
|
+
const media = connection.peer.rawMedia;
|
|
378
|
+
if (media === undefined)
|
|
379
|
+
throw new ReactorError("UnsupportedCapability", "peer has no decoded media capability", {
|
|
380
|
+
outcome: "not-submitted",
|
|
381
|
+
});
|
|
382
|
+
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
|
+
return Object.freeze({
|
|
384
|
+
generation: connection.generation,
|
|
385
|
+
tracks: connection.negotiated.descriptor.capabilities.tracks,
|
|
386
|
+
retired: Deferred.await(connection.failed),
|
|
387
|
+
video: (name) => owned(media.video(name)),
|
|
388
|
+
audio: (name) => owned(media.audio(name)),
|
|
389
|
+
snapshot: media.snapshot,
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
trackGeneration() {
|
|
394
|
+
return pure(() => {
|
|
395
|
+
const connection = this.currentReady();
|
|
396
|
+
if (connection.peer.nativeTracks !== true) {
|
|
397
|
+
throw new ReactorError("UnsupportedCapability", "peer has no browser track capability", {
|
|
398
|
+
outcome: "not-submitted",
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
return Object.freeze({
|
|
402
|
+
generation: connection.generation,
|
|
403
|
+
tracks: connection.negotiated.descriptor.capabilities.tracks,
|
|
404
|
+
retired: Deferred.await(connection.failed),
|
|
405
|
+
track: (name) => this.track(name, connection),
|
|
406
|
+
publish: (name, source) => this.publish(name, source, connection),
|
|
407
|
+
unpublish: (name) => this.unpublish(name, connection),
|
|
408
|
+
setTrackActive: (name, active) => this.setTrackActive(name, active, connection),
|
|
409
|
+
setMaxBitrate: (name, bits) => this.setMaxBitrate(name, bits, connection),
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
/** Internal initial acquisition. Reconnect is a distinct operation and never replays a model command. */
|
|
414
|
+
start() {
|
|
415
|
+
return this.connectAttempt(false);
|
|
416
|
+
}
|
|
417
|
+
reconnect() {
|
|
418
|
+
return this.connectAttempt(true);
|
|
419
|
+
}
|
|
420
|
+
connectAttempt(reconnect) {
|
|
421
|
+
const self = this;
|
|
422
|
+
return Effect.gen(function* () {
|
|
423
|
+
const c = yield* self.begin(reconnect);
|
|
424
|
+
const work = Effect.gen(function* () {
|
|
425
|
+
if (!reconnect)
|
|
426
|
+
yield* self.guard(c, self.allocate());
|
|
427
|
+
self.assertCurrent(c);
|
|
428
|
+
self.lifecycle.transition("waiting");
|
|
429
|
+
const remote = self.currentRemote();
|
|
430
|
+
const descriptor = yield* self.guard(c, self.http.ready(remote.id, reconnect ? undefined : remote.descriptor));
|
|
431
|
+
remote.descriptor = descriptor;
|
|
432
|
+
const capabilities = descriptor.capabilities, transport = descriptor.selected_transport;
|
|
433
|
+
if (capabilities === undefined || transport === undefined)
|
|
434
|
+
return yield* Effect.fail(new ReactorError("Protocol", "missing ready capabilities/transport"));
|
|
435
|
+
if (transport.protocol !== "webrtc" || transport.version !== "1.0")
|
|
436
|
+
return yield* Effect.fail(new ReactorError("VersionMismatch", `unsupported transport ${transport.protocol}/${transport.version}`));
|
|
437
|
+
const readyDescriptor = Object.freeze({
|
|
438
|
+
...descriptor,
|
|
439
|
+
capabilities,
|
|
440
|
+
selected_transport: transport,
|
|
441
|
+
});
|
|
442
|
+
const servers = yield* self.guard(c, self.http.iceServers(remote.id));
|
|
443
|
+
const prepared = yield* self.guard(c, Scope.provide(c.peer.prepare(servers, capabilities.tracks, (event) => self.onPeer(c, event)), c.scope));
|
|
444
|
+
c.mapping = prepared.mapping;
|
|
445
|
+
const previousId = remote.connectionId;
|
|
446
|
+
const cid = previousId ?? (yield* self.guard(c, self.http.register(remote.id)));
|
|
447
|
+
c.connectionId = cid;
|
|
448
|
+
remote.connectionId = cid;
|
|
449
|
+
// Source order: registration -> buffered ICE (including empty final) -> offer -> poll answer.
|
|
450
|
+
yield* self.flushIce(c);
|
|
451
|
+
yield* self.background(c, Effect.gen(function* () {
|
|
452
|
+
while (true) {
|
|
453
|
+
yield* Queue.take(c.iceWake);
|
|
454
|
+
yield* self.flushIce(c);
|
|
455
|
+
}
|
|
456
|
+
}));
|
|
457
|
+
yield* self.guard(c, self.http.offer(remote.id, cid, prepared.sdp, prepared.mapping, reconnect && previousId !== undefined));
|
|
458
|
+
const answer = yield* self.guard(c, self.http.answer(remote.id, cid));
|
|
459
|
+
if (answer.connection_id !== undefined) {
|
|
460
|
+
c.connectionId = answer.connection_id;
|
|
461
|
+
remote.connectionId = answer.connection_id;
|
|
462
|
+
}
|
|
463
|
+
yield* self.guard(c, c.peer.answer(answer.sdp_answer));
|
|
464
|
+
yield* withDeadline(self.guard(c, Deferred.await(c.ready)), self.readyTimeout, "peer and both channels ready");
|
|
465
|
+
self.assertCurrent(c);
|
|
466
|
+
c.negotiated = Object.freeze({
|
|
467
|
+
ownership: remote.ownership,
|
|
468
|
+
sessionId: remote.id,
|
|
469
|
+
descriptor: readyDescriptor,
|
|
470
|
+
connectionId: c.connectionId,
|
|
471
|
+
});
|
|
472
|
+
self.lifecycle.transition("ready");
|
|
473
|
+
if (self.options.autoResumeTracks ?? self.options.intent._tag === "Create")
|
|
474
|
+
for (const track of capabilities.tracks)
|
|
475
|
+
if (track.direction === "recvonly") {
|
|
476
|
+
const outcome = yield* Effect.result(self.setTrackActive(track.name, true));
|
|
477
|
+
if (outcome._tag === "Failure")
|
|
478
|
+
self.emit({ _tag: "Diagnostic", error: outcome.failure });
|
|
479
|
+
}
|
|
480
|
+
for (const [name, bitrate] of self.bitrates) {
|
|
481
|
+
const outcome = yield* Effect.result(self.guard(c, c.peer.maxBitrate(name, bitrate)));
|
|
482
|
+
if (outcome._tag === "Failure")
|
|
483
|
+
self.emit({ _tag: "Diagnostic", error: outcome.failure });
|
|
484
|
+
}
|
|
485
|
+
if (self.heartbeat !== 0)
|
|
486
|
+
yield* self.background(c, Effect.gen(function* () {
|
|
487
|
+
while (true) {
|
|
488
|
+
yield* self.ping();
|
|
489
|
+
yield* Effect.sleep(self.heartbeat);
|
|
490
|
+
}
|
|
491
|
+
}));
|
|
492
|
+
});
|
|
493
|
+
yield* withDeadline(work, self.connectTimeout, reconnect ? "reconnect" : "connect").pipe(Effect.onExit((exit) => Exit.isFailure(exit)
|
|
494
|
+
? Effect.gen(function* () {
|
|
495
|
+
self.remote.allocationLost();
|
|
496
|
+
const error = Cause.findError(exit.cause);
|
|
497
|
+
self.fail(c, error._tag === "Success" && error.success instanceof ReactorError
|
|
498
|
+
? error.success
|
|
499
|
+
: new ReactorError("Aborted", "connection attempt interrupted"));
|
|
500
|
+
yield* Scope.close(c.scope, Exit.void);
|
|
501
|
+
})
|
|
502
|
+
: Effect.void));
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
request(c, correlator, operation, encode, channel, timeoutMs, publication) {
|
|
506
|
+
const self = this;
|
|
507
|
+
return Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
|
|
508
|
+
const pending = yield* pure(() => {
|
|
509
|
+
self.assertCurrent(c);
|
|
510
|
+
return correlator.register(c.generation, operation);
|
|
511
|
+
});
|
|
512
|
+
if (publication !== undefined)
|
|
513
|
+
c.pendingClaims.set(pending.id, publication);
|
|
514
|
+
const encoded = yield* Effect.result(pure(() => encode(pending.id)));
|
|
515
|
+
if (encoded._tag === "Failure") {
|
|
516
|
+
correlator.cancel(pending);
|
|
517
|
+
c.pendingClaims.delete(pending.id);
|
|
518
|
+
return yield* Effect.fail(new ReactorError(encoded.failure.code, encoded.failure.message, {
|
|
519
|
+
...encoded.failure.context,
|
|
520
|
+
operation,
|
|
521
|
+
requestId: pending.id,
|
|
522
|
+
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"),
|
|
532
|
+
});
|
|
533
|
+
const sending = pure(() => {
|
|
534
|
+
self.assertCurrent(c);
|
|
535
|
+
// The record already exists when a synchronous peer reply arrives.
|
|
536
|
+
pending.submitted = true;
|
|
537
|
+
}).pipe(Effect.andThen(Effect.suspend(() => c.peer.send(channel, encoded.success))), Effect.catch((error) => Effect.sync(() => {
|
|
538
|
+
if (!correlator.has(pending))
|
|
539
|
+
return;
|
|
540
|
+
if (error.context.outcome === "not-submitted") {
|
|
541
|
+
correlator.cancel(pending);
|
|
542
|
+
c.pendingClaims.delete(pending.id);
|
|
543
|
+
}
|
|
544
|
+
Deferred.doneUnsafe(pending.deferred, Effect.fail(failure(error)));
|
|
545
|
+
})));
|
|
546
|
+
const execution = Effect.scoped(Effect.gen(function* () {
|
|
547
|
+
// The application reply can precede the transport's acknowledgement.
|
|
548
|
+
// Ending the reply wait joins this sender's local ACK waiter, while the
|
|
549
|
+
// native backend remains responsible for any foreign operation slot.
|
|
550
|
+
yield* Effect.forkIn(sending, yield* Effect.scope, { startImmediately: true });
|
|
551
|
+
return yield* Deferred.await(pending.deferred);
|
|
552
|
+
})).pipe(Effect.interruptible, (effect) => withDeadline(effect, timeoutMs, operation), Effect.mapError(failure), Effect.onExit((exit) => Effect.sync(() => {
|
|
553
|
+
if (!pending.submitted) {
|
|
554
|
+
correlator.cancel(pending);
|
|
555
|
+
c.pendingClaims.delete(pending.id);
|
|
556
|
+
}
|
|
557
|
+
else if (Exit.isFailure(exit))
|
|
558
|
+
pending.waiting = false;
|
|
559
|
+
// Unresolved submitted records remain bounded by maxPending. A late
|
|
560
|
+
// reply or retiring this generation releases the slot; a timeout does not.
|
|
561
|
+
})));
|
|
562
|
+
const owner = yield* Effect.forkIn(execution, c.scope, { startImmediately: true });
|
|
563
|
+
return yield* restore(Fiber.join(owner));
|
|
564
|
+
}));
|
|
565
|
+
}
|
|
566
|
+
command(type, data, uploads = new Map(), timeoutMs = this.commandTimeout) {
|
|
567
|
+
return pure(() => {
|
|
568
|
+
const c = this.currentReady(), payload = {
|
|
569
|
+
type: nonempty(type, "command type"),
|
|
570
|
+
data: structFromObject(data),
|
|
571
|
+
uploads: captureUploads(uploads),
|
|
572
|
+
};
|
|
573
|
+
positiveLimit(timeoutMs, "command timeout", 600_000);
|
|
574
|
+
return { c, payload };
|
|
575
|
+
}).pipe(Effect.mapError((error) => new CommandFailure(error, {
|
|
576
|
+
...error.context,
|
|
577
|
+
operation: type,
|
|
578
|
+
outcome: "not-submitted",
|
|
579
|
+
})), Effect.flatMap(({ c, payload }) => this.request(c, this.data, type, (id) => W.DataClientMessage.encode({
|
|
580
|
+
request_id: id,
|
|
581
|
+
kind: 1,
|
|
582
|
+
payload: { case: "command", value: payload },
|
|
583
|
+
}), "data", timeoutMs).pipe(Effect.mapError((error) => {
|
|
584
|
+
const { outcome, requestId, generation } = error.context;
|
|
585
|
+
if (outcome === "unknown" || outcome === "replied") {
|
|
586
|
+
if (requestId === undefined || generation === undefined)
|
|
587
|
+
throw new Error("dispatched command lost its attribution");
|
|
588
|
+
const context = {
|
|
589
|
+
...error.context,
|
|
590
|
+
operation: type,
|
|
591
|
+
outcome,
|
|
592
|
+
requestId,
|
|
593
|
+
generation,
|
|
594
|
+
};
|
|
595
|
+
return new CommandFailure(error, context);
|
|
596
|
+
}
|
|
597
|
+
return new CommandFailure(error, {
|
|
598
|
+
...error.context,
|
|
599
|
+
operation: type,
|
|
600
|
+
outcome: "not-submitted",
|
|
601
|
+
});
|
|
602
|
+
}))));
|
|
603
|
+
}
|
|
604
|
+
controlRequest(operation, payload, expected) {
|
|
605
|
+
return pure(() => {
|
|
606
|
+
const connection = expected ?? this.currentReady();
|
|
607
|
+
this.assertCurrent(connection);
|
|
608
|
+
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.commandTimeout, payload.case === "publish_track" ? payload.value.name : undefined)));
|
|
610
|
+
}
|
|
611
|
+
notification(c, payload) {
|
|
612
|
+
return pure(() => {
|
|
613
|
+
this.assertCurrent(c);
|
|
614
|
+
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.commandTimeout, "control notification"));
|
|
616
|
+
}
|
|
617
|
+
ping() {
|
|
618
|
+
return pure(() => this.currentReady()).pipe(Effect.flatMap((c) => this.notification(c, { case: "ping", value: {} })));
|
|
619
|
+
}
|
|
620
|
+
schema() {
|
|
621
|
+
return this.controlRequest("request_schema", { case: "request_schema", value: {} }).pipe(Effect.flatMap((reply) => pure(() => {
|
|
622
|
+
if (reply.case !== "model_schema")
|
|
623
|
+
throw new ReactorError("UnexpectedReply", `schema reply was ${reply.case}`, {
|
|
624
|
+
outcome: "replied",
|
|
625
|
+
});
|
|
626
|
+
return {
|
|
627
|
+
raw: reply.value,
|
|
628
|
+
...(reply.value.openapi === undefined
|
|
629
|
+
? {}
|
|
630
|
+
: { openapi: objectFromStruct(reply.value.openapi) }),
|
|
631
|
+
};
|
|
632
|
+
})));
|
|
633
|
+
}
|
|
634
|
+
clip(payload) {
|
|
635
|
+
return this.controlRequest(payload.case, payload).pipe(Effect.flatMap((reply) => pure(() => {
|
|
636
|
+
if (reply.case === "clip_failed")
|
|
637
|
+
throw new ReactorError(/recorder disabled|encoder crashed/i.test(reply.value.reason)
|
|
638
|
+
? "RecorderDisabled"
|
|
639
|
+
: "Remote", reply.value.reason, { outcome: "replied" });
|
|
640
|
+
if (reply.case !== "clip_ready")
|
|
641
|
+
throw new ReactorError("UnexpectedReply", `clip reply was ${reply.case}`, {
|
|
642
|
+
outcome: "replied",
|
|
643
|
+
});
|
|
644
|
+
return {
|
|
645
|
+
...reply.value,
|
|
646
|
+
playlist_url: new URL(reply.value.playlist_url, `${this.http.apiUrl}/`).href,
|
|
647
|
+
};
|
|
648
|
+
})));
|
|
649
|
+
}
|
|
650
|
+
requestClip(seconds) {
|
|
651
|
+
return pure(() => {
|
|
652
|
+
if (!Number.isFinite(seconds) || seconds <= 0)
|
|
653
|
+
throw new ReactorError("Protocol", "clip duration must be finite and positive");
|
|
654
|
+
}).pipe(Effect.andThen(this.clip({ case: "request_clip", value: { duration_seconds: seconds } })));
|
|
655
|
+
}
|
|
656
|
+
recording() {
|
|
657
|
+
return this.clip({ case: "request_recording", value: {} });
|
|
658
|
+
}
|
|
659
|
+
namedTrack(name, connection) {
|
|
660
|
+
if (connection.peer.mediaSupported === false)
|
|
661
|
+
throw new ReactorError("UnsupportedCapability", "This peer is data-only; track/media operations are unavailable", { outcome: "not-submitted" });
|
|
662
|
+
const track = connection.negotiated.descriptor.capabilities.tracks.find((t) => t.name === name);
|
|
663
|
+
if (track === undefined)
|
|
664
|
+
throw new ReactorError("InvalidState", `unknown track: ${name}`);
|
|
665
|
+
return track;
|
|
666
|
+
}
|
|
667
|
+
trackOperation(name, body, expected) {
|
|
668
|
+
return Effect.acquireUseRelease(pure(() => {
|
|
669
|
+
const c = expected ?? this.currentReady();
|
|
670
|
+
this.assertCurrent(c);
|
|
671
|
+
const track = this.namedTrack(name, c);
|
|
672
|
+
if (c.trackBusy.has(name))
|
|
673
|
+
throw new ReactorError("InvalidState", `another track operation is in flight: ${name}`);
|
|
674
|
+
c.trackBusy.add(name);
|
|
675
|
+
return { c, track };
|
|
676
|
+
}), ({ c, track }) => this.guard(c, body(c, track)), ({ c }) => Effect.sync(() => {
|
|
677
|
+
c.trackBusy.delete(name);
|
|
678
|
+
}));
|
|
679
|
+
}
|
|
680
|
+
setTrackActive(name, active, expected) {
|
|
681
|
+
return this.trackOperation(name, (c) => c.peer.direction(name, active).pipe(Effect.andThen(pure(() => {
|
|
682
|
+
if (active)
|
|
683
|
+
c.paused.delete(name);
|
|
684
|
+
else
|
|
685
|
+
c.paused.add(name);
|
|
686
|
+
})), Effect.andThen(this.notification(c, active
|
|
687
|
+
? { case: "resume_track", value: { name } }
|
|
688
|
+
: { case: "pause_track", value: { name } }))), expected);
|
|
689
|
+
}
|
|
690
|
+
/** Native replaceTrack cannot be cancelled. Atomic local ownership transfer is
|
|
691
|
+
* masked; only the bounded native wait is interruptible. An abandoned wait retires
|
|
692
|
+
* this generation, preventing late replacement from racing another sender operation. */
|
|
693
|
+
replaceSender(c, name, source) {
|
|
694
|
+
const self = this;
|
|
695
|
+
return Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
|
|
696
|
+
const clone = yield* pure(() => {
|
|
697
|
+
self.assertCurrent(c);
|
|
698
|
+
return source === null ? null : source.clone();
|
|
699
|
+
});
|
|
700
|
+
let invoked = false, deadline = false;
|
|
701
|
+
const mutation = pure(() => {
|
|
702
|
+
self.assertCurrent(c);
|
|
703
|
+
invoked = true;
|
|
704
|
+
}).pipe(Effect.andThen(Effect.suspend(() => c.peer.replace(name, clone))), Effect.timeoutOrElse({
|
|
705
|
+
duration: self.commandTimeout,
|
|
706
|
+
orElse: () => {
|
|
707
|
+
deadline = true;
|
|
708
|
+
return Effect.fail(new ReactorError("Timeout", "sender replacement deadline; local generation retired", { operation: name, outcome: "unknown" }));
|
|
709
|
+
},
|
|
710
|
+
}));
|
|
711
|
+
yield* restore(mutation).pipe(Effect.onExit((exit) => Exit.isFailure(exit)
|
|
712
|
+
? Effect.sync(() => {
|
|
713
|
+
clone?.stop();
|
|
714
|
+
if (invoked && (deadline || Cause.hasInterrupts(exit.cause)))
|
|
715
|
+
self.fail(c, new ReactorError(deadline ? "Timeout" : "Disconnected", "native sender replacement abandoned; reconnect explicitly before reusing publication", { operation: name, outcome: "unknown" }));
|
|
716
|
+
})
|
|
717
|
+
: Effect.void));
|
|
718
|
+
// No interruptible boundary between successful mutation and recording its owner.
|
|
719
|
+
// If another task retired the generation, do not resurrect a sending clone.
|
|
720
|
+
yield* pure(() => {
|
|
721
|
+
try {
|
|
722
|
+
self.assertCurrent(c);
|
|
723
|
+
c.sending.get(name)?.stop();
|
|
724
|
+
if (clone === null)
|
|
725
|
+
c.sending.delete(name);
|
|
726
|
+
else
|
|
727
|
+
c.sending.set(name, clone);
|
|
728
|
+
}
|
|
729
|
+
catch (error) {
|
|
730
|
+
clone?.stop();
|
|
731
|
+
throw error;
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
}));
|
|
735
|
+
}
|
|
736
|
+
/** Claims the publisher slot, then attaches a session-owned clone. Caller retains its source track. */
|
|
737
|
+
publish(name, source, expected) {
|
|
738
|
+
const self = this;
|
|
739
|
+
return this.trackOperation(name, (c, track) => Effect.gen(function* () {
|
|
740
|
+
if (c.peer.nativeTracks === false)
|
|
741
|
+
return yield* Effect.fail(new ReactorError("UnsupportedCapability", "this peer does not accept browser media tracks", { outcome: "not-submitted" }));
|
|
742
|
+
if (track.direction !== "sendonly" ||
|
|
743
|
+
source.kind !== track.kind ||
|
|
744
|
+
source.readyState !== "live")
|
|
745
|
+
return yield* Effect.fail(new ReactorError("InvalidState", "publish requires a live matching input track"));
|
|
746
|
+
if (!c.claimed.has(name)) {
|
|
747
|
+
const reply = yield* self.controlRequest("publish_track", { case: "publish_track", value: { name } }, c);
|
|
748
|
+
if (reply.case !== "publish_track" || reply.value.name !== name)
|
|
749
|
+
return yield* Effect.fail(new ReactorError("UnexpectedReply", "publisher claim reply mismatch", {
|
|
750
|
+
outcome: "replied",
|
|
751
|
+
}));
|
|
752
|
+
c.claimed.add(name);
|
|
753
|
+
}
|
|
754
|
+
yield* self.replaceSender(c, name, source);
|
|
755
|
+
}), expected);
|
|
756
|
+
}
|
|
757
|
+
unpublish(name, expected) {
|
|
758
|
+
return this.trackOperation(name, (c) => c.peer.nativeTracks === false
|
|
759
|
+
? Effect.fail(new ReactorError("UnsupportedCapability", "this peer does not expose browser track publication", { outcome: "not-submitted" }))
|
|
760
|
+
: this.replaceSender(c, name, null).pipe(Effect.andThen(this.notification(c, { case: "unpublish_track", value: { name } })), Effect.tap(() => Effect.sync(() => {
|
|
761
|
+
c.claimed.delete(name);
|
|
762
|
+
}))), expected);
|
|
763
|
+
}
|
|
764
|
+
track(name, expected) {
|
|
765
|
+
return Effect.acquireRelease(pure(() => {
|
|
766
|
+
const c = expected ?? this.currentReady();
|
|
767
|
+
this.assertCurrent(c);
|
|
768
|
+
if (c.peer.nativeTracks === false)
|
|
769
|
+
throw new ReactorError("UnsupportedCapability", "this peer exposes owned decoded frames instead of browser tracks", { outcome: "not-submitted" });
|
|
770
|
+
return { peer: c.peer, track: c.peer.lease(name) };
|
|
771
|
+
}), ({ peer, track }) => Effect.sync(() => peer.release(track))).pipe(Effect.map(({ track }) => track));
|
|
772
|
+
}
|
|
773
|
+
setMaxBitrate(name, bitsPerSecond, expected) {
|
|
774
|
+
return this.trackOperation(name, (c) => c.peer.maxBitrate(name, bitsPerSecond).pipe(Effect.tap(() => Effect.sync(() => {
|
|
775
|
+
this.bitrates.set(name, bitsPerSecond);
|
|
776
|
+
}))), expected);
|
|
777
|
+
}
|
|
778
|
+
stats() {
|
|
779
|
+
const self = this;
|
|
780
|
+
return Effect.gen(function* () {
|
|
781
|
+
const connection = yield* pure(() => self.currentReady());
|
|
782
|
+
const raw = yield* self.guard(connection, connection.peer.stats());
|
|
783
|
+
// Elapsed sampling time belongs to this Effect boundary, not the pure sampler.
|
|
784
|
+
const atNanos = yield* Clock.monotonicTimeNanos;
|
|
785
|
+
const atMs = Number(atNanos) / 1_000_000;
|
|
786
|
+
return yield* pure(() => self.sampler.sample(raw, connection.generation, atMs));
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
rawStats() {
|
|
790
|
+
return pure(() => this.currentReady()).pipe(Effect.flatMap((c) => this.guard(c, c.peer.stats())));
|
|
791
|
+
}
|
|
792
|
+
upload(name, mimeType, bytes, timeoutMs = 60_000) {
|
|
793
|
+
const self = this;
|
|
794
|
+
return Effect.suspend(() => {
|
|
795
|
+
let progress = {
|
|
796
|
+
allocation: "not-requested",
|
|
797
|
+
transfer: "not-requested",
|
|
798
|
+
notification: "not-submitted",
|
|
799
|
+
};
|
|
800
|
+
const operation = Effect.gen(function* () {
|
|
801
|
+
const { c, remote, copy } = yield* pure(() => {
|
|
802
|
+
const c = self.currentReady(), remote = self.currentRemote();
|
|
803
|
+
nonempty(name, "upload name");
|
|
804
|
+
nonempty(mimeType, "upload MIME type");
|
|
805
|
+
positiveLimit(bytes.byteLength, "upload size", self.uploadBound);
|
|
806
|
+
positiveLimit(timeoutMs, "upload timeout", 600_000);
|
|
807
|
+
return { c, remote, copy: new Uint8Array(bytes) };
|
|
808
|
+
});
|
|
809
|
+
progress = { ...progress, allocation: "unknown" };
|
|
810
|
+
const allocation = yield* self.guard(c, self.http.allocateUpload(remote.id, name, mimeType, copy.length));
|
|
811
|
+
const file = {
|
|
812
|
+
upload_id: allocation.presigned_id,
|
|
813
|
+
name,
|
|
814
|
+
mime_type: mimeType,
|
|
815
|
+
size: BigInt(copy.length),
|
|
816
|
+
};
|
|
817
|
+
progress = { ...progress, allocation: "confirmed", file, transfer: "unknown" };
|
|
818
|
+
yield* self.guard(c, self.http.putUpload(allocation, copy, mimeType));
|
|
819
|
+
progress = { ...progress, transfer: "confirmed" };
|
|
820
|
+
progress = { ...progress, notification: "unknown" };
|
|
821
|
+
yield* self.notification(c, { case: "file_uploaded", value: file }).pipe(Effect.tapError((error) => Effect.sync(() => {
|
|
822
|
+
if (error.context.outcome === "not-submitted")
|
|
823
|
+
progress = { ...progress, notification: "not-submitted" };
|
|
824
|
+
})));
|
|
825
|
+
progress = { ...progress, notification: "submitted" };
|
|
826
|
+
return { file, transfer: "confirmed", notification: "submitted" };
|
|
827
|
+
});
|
|
828
|
+
return withDeadline(operation, timeoutMs, "upload").pipe(Effect.mapError((e) => new ReactorError("Upload", e.message, {
|
|
829
|
+
...e.context,
|
|
830
|
+
operation: "upload",
|
|
831
|
+
detail: { cause: e, progress: { ...progress } },
|
|
832
|
+
})), Effect.ensuring(Effect.sync(() => self.emit({ _tag: "Upload", progress: Object.freeze({ ...progress }) }))));
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
/** Idempotent close. Owned-session termination is attempted once; response is not terminal proof. */
|
|
836
|
+
close() {
|
|
837
|
+
const self = this;
|
|
838
|
+
return Effect.uninterruptible(Effect.suspend(() => {
|
|
839
|
+
if (self.closeGate !== undefined)
|
|
840
|
+
return Deferred.await(self.closeGate);
|
|
841
|
+
const gate = Deferred.makeUnsafe();
|
|
842
|
+
self.closeGate = gate;
|
|
843
|
+
return Effect.gen(function* () {
|
|
844
|
+
self.lifecycle.transition("closing");
|
|
845
|
+
Deferred.doneUnsafe(self.lifecycle.closing, Effect.fail(new ReactorError("Closed", "session closing")));
|
|
846
|
+
const report = yield* cleanupSession({
|
|
847
|
+
connection: self.lifecycle.connection,
|
|
848
|
+
scope: self.lifecycle.scope,
|
|
849
|
+
remote: self.remote,
|
|
850
|
+
http: self.http,
|
|
851
|
+
commandTimeout: self.commandTimeout,
|
|
852
|
+
retire: (connection, error) => self.fail(connection, error),
|
|
853
|
+
});
|
|
854
|
+
self.closeReport = report;
|
|
855
|
+
self.lifecycle.transition("closed");
|
|
856
|
+
self.observations.end();
|
|
857
|
+
Deferred.doneUnsafe(gate, Effect.succeed(report));
|
|
858
|
+
try {
|
|
859
|
+
self.options.onClose?.(report);
|
|
860
|
+
}
|
|
861
|
+
catch {
|
|
862
|
+
/* Consumer callbacks cannot undo or stall cleanup. */
|
|
863
|
+
}
|
|
864
|
+
return report;
|
|
865
|
+
});
|
|
866
|
+
}));
|
|
867
|
+
}
|
|
868
|
+
isKnownTerminal() {
|
|
869
|
+
return this.remote.isKnownTerminal();
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
//# sourceMappingURL=session.js.map
|