reactor-effect-client 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -6
- package/dist/FetchHttp.js +1 -1
- package/dist/FetchHttp.js.map +1 -1
- package/dist/PeerFactory.d.ts +13 -2
- package/dist/PeerFactory.d.ts.map +1 -1
- package/dist/PeerFactory.js.map +1 -1
- package/dist/PeerTypes.d.ts +2 -2
- package/dist/PeerTypes.d.ts.map +1 -1
- package/dist/Sequence.d.ts +18 -14
- package/dist/Sequence.d.ts.map +1 -1
- package/dist/Sequence.js +117 -122
- package/dist/Sequence.js.map +1 -1
- package/dist/SessionTypes.d.ts +172 -15
- package/dist/SessionTypes.d.ts.map +1 -1
- package/dist/SessionTypes.js +18 -1
- package/dist/SessionTypes.js.map +1 -1
- package/dist/bytes.js +3 -3
- package/dist/bytes.js.map +1 -1
- package/dist/contract.d.ts +120 -29
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +105 -73
- package/dist/contract.js.map +1 -1
- package/dist/coordinator/_internal/client.d.ts +111 -17
- package/dist/coordinator/_internal/client.d.ts.map +1 -1
- package/dist/coordinator/_internal/client.js +187 -74
- package/dist/coordinator/_internal/client.js.map +1 -1
- package/dist/coordinator/_internal/recording.d.ts +33 -0
- package/dist/coordinator/_internal/recording.d.ts.map +1 -0
- package/dist/coordinator/_internal/recording.js +123 -0
- package/dist/coordinator/_internal/recording.js.map +1 -0
- package/dist/coordinator/_internal/response.d.ts.map +1 -1
- package/dist/coordinator/_internal/response.js +5 -7
- package/dist/coordinator/_internal/response.js.map +1 -1
- package/dist/coordinator/_internal/schemas.d.ts +40 -6
- package/dist/coordinator/_internal/schemas.d.ts.map +1 -1
- package/dist/coordinator/_internal/schemas.js +94 -64
- package/dist/coordinator/_internal/schemas.js.map +1 -1
- package/dist/coordinator/index.d.ts +14 -2
- package/dist/coordinator/index.d.ts.map +1 -1
- package/dist/coordinator/index.js +14 -18
- package/dist/coordinator/index.js.map +1 -1
- package/dist/correlation.d.ts +15 -0
- package/dist/correlation.d.ts.map +1 -1
- package/dist/correlation.js +26 -8
- package/dist/correlation.js.map +1 -1
- package/dist/duration.d.ts +25 -0
- package/dist/duration.d.ts.map +1 -0
- package/dist/duration.js +47 -0
- package/dist/duration.js.map +1 -0
- package/dist/errors.d.ts +1264 -49
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +698 -46
- package/dist/errors.js.map +1 -1
- package/dist/h3/_internal/client.d.ts +2 -2
- package/dist/h3/_internal/client.d.ts.map +1 -1
- package/dist/h3/_internal/client.js +280 -146
- package/dist/h3/_internal/client.js.map +1 -1
- package/dist/h3/_internal/contracts.d.ts.map +1 -1
- package/dist/h3/_internal/contracts.js +2 -2
- package/dist/h3/_internal/contracts.js.map +1 -1
- package/dist/h3/_internal/deployment.d.ts.map +1 -1
- package/dist/h3/_internal/deployment.js +5 -4
- package/dist/h3/_internal/deployment.js.map +1 -1
- package/dist/h3/_internal/evidence.js +1 -1
- package/dist/h3/_internal/evidence.js.map +1 -1
- package/dist/h3/_internal/operations.d.ts +58 -0
- package/dist/h3/_internal/operations.d.ts.map +1 -0
- package/dist/h3/_internal/operations.js +268 -0
- package/dist/h3/_internal/operations.js.map +1 -0
- package/dist/h3/_internal/references.d.ts.map +1 -1
- package/dist/h3/_internal/references.js +3 -8
- package/dist/h3/_internal/references.js.map +1 -1
- package/dist/h3/_internal/request.js +1 -1
- package/dist/h3/_internal/request.js.map +1 -1
- package/dist/h3/_internal/retained.d.ts +22 -0
- package/dist/h3/_internal/retained.d.ts.map +1 -0
- package/dist/h3/_internal/retained.js +89 -0
- package/dist/h3/_internal/retained.js.map +1 -0
- package/dist/h3/_internal/state.d.ts +15 -1
- package/dist/h3/_internal/state.d.ts.map +1 -1
- package/dist/h3/_internal/state.js +69 -29
- package/dist/h3/_internal/state.js.map +1 -1
- package/dist/h3/index.d.ts +1 -1
- package/dist/h3/index.d.ts.map +1 -1
- package/dist/h3/messages.d.ts +118 -109
- package/dist/h3/messages.d.ts.map +1 -1
- package/dist/h3/messages.js +73 -51
- package/dist/h3/messages.js.map +1 -1
- package/dist/h3/types.d.ts +109 -13
- package/dist/h3/types.d.ts.map +1 -1
- package/dist/host.d.ts +6 -6
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +4 -5
- package/dist/host.js.map +1 -1
- package/dist/index.d.ts +11 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/json.d.ts +13 -6
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +34 -14
- package/dist/json.js.map +1 -1
- package/dist/media-stream.d.ts.map +1 -1
- package/dist/media-stream.js +9 -11
- package/dist/media-stream.js.map +1 -1
- package/dist/observation.d.ts +9 -0
- package/dist/observation.d.ts.map +1 -1
- package/dist/observation.js +20 -18
- package/dist/observation.js.map +1 -1
- package/dist/orchestration/h3-source.d.ts +32 -4
- package/dist/orchestration/h3-source.d.ts.map +1 -1
- package/dist/orchestration/h3-source.js +47 -14
- package/dist/orchestration/h3-source.js.map +1 -1
- package/dist/orchestration/index.d.ts +9 -6
- package/dist/orchestration/index.d.ts.map +1 -1
- package/dist/orchestration/index.js +5 -3
- package/dist/orchestration/index.js.map +1 -1
- package/dist/orchestration/media-buffer.d.ts +12 -4
- package/dist/orchestration/media-buffer.d.ts.map +1 -1
- package/dist/orchestration/media-buffer.js +31 -11
- package/dist/orchestration/media-buffer.js.map +1 -1
- package/dist/orchestration/open-h3.d.ts +65 -0
- package/dist/orchestration/open-h3.d.ts.map +1 -0
- package/dist/orchestration/open-h3.js +65 -0
- package/dist/orchestration/open-h3.js.map +1 -0
- package/dist/orchestration/references.d.ts +5 -1
- package/dist/orchestration/references.d.ts.map +1 -1
- package/dist/orchestration/references.js +37 -34
- package/dist/orchestration/references.js.map +1 -1
- package/dist/orchestration/renewal-state.js +1 -1
- package/dist/orchestration/renewal-state.js.map +1 -1
- package/dist/orchestration/renewal.d.ts +64 -64
- package/dist/orchestration/renewal.d.ts.map +1 -1
- package/dist/orchestration/renewal.js +149 -88
- package/dist/orchestration/renewal.js.map +1 -1
- package/dist/orchestration/request.d.ts +11 -12
- package/dist/orchestration/request.d.ts.map +1 -1
- package/dist/orchestration/request.js +63 -66
- package/dist/orchestration/request.js.map +1 -1
- package/dist/orchestration/routing.d.ts.map +1 -1
- package/dist/orchestration/routing.js +17 -14
- package/dist/orchestration/routing.js.map +1 -1
- package/dist/orchestration/source-slot.d.ts +15 -5
- package/dist/orchestration/source-slot.d.ts.map +1 -1
- package/dist/orchestration/source-slot.js +40 -21
- package/dist/orchestration/source-slot.js.map +1 -1
- package/dist/orchestration/types.d.ts +594 -39
- package/dist/orchestration/types.d.ts.map +1 -1
- package/dist/orchestration/types.js +20 -0
- package/dist/orchestration/types.js.map +1 -1
- package/dist/protobuf.js +2 -2
- package/dist/protobuf.js.map +1 -1
- package/dist/session/_internal/acquire.d.ts +4 -6
- package/dist/session/_internal/acquire.d.ts.map +1 -1
- package/dist/session/_internal/acquire.js +40 -50
- package/dist/session/_internal/acquire.js.map +1 -1
- package/dist/session/_internal/cleanup.d.ts +2 -1
- package/dist/session/_internal/cleanup.d.ts.map +1 -1
- package/dist/session/_internal/cleanup.js +23 -15
- package/dist/session/_internal/cleanup.js.map +1 -1
- package/dist/session/_internal/lifecycle.d.ts +5 -0
- package/dist/session/_internal/lifecycle.d.ts.map +1 -1
- package/dist/session/_internal/lifecycle.js +16 -6
- package/dist/session/_internal/lifecycle.js.map +1 -1
- package/dist/session/_internal/remote.d.ts.map +1 -1
- package/dist/session/_internal/remote.js +16 -7
- package/dist/session/_internal/remote.js.map +1 -1
- package/dist/session/_internal/uploads.d.ts +4 -1
- package/dist/session/_internal/uploads.d.ts.map +1 -1
- package/dist/session/_internal/uploads.js +25 -27
- package/dist/session/_internal/uploads.js.map +1 -1
- package/dist/session/commands.d.ts +1 -21
- package/dist/session/commands.d.ts.map +1 -1
- package/dist/session/commands.js +1 -12
- package/dist/session/commands.js.map +1 -1
- package/dist/session/files.d.ts +10 -2
- package/dist/session/files.d.ts.map +1 -1
- package/dist/session/files.js +11 -16
- package/dist/session/files.js.map +1 -1
- package/dist/session/index.d.ts +6 -6
- package/dist/session/index.d.ts.map +1 -1
- package/dist/session/index.js.map +1 -1
- package/dist/session/media.d.ts +53 -6
- package/dist/session/media.d.ts.map +1 -1
- package/dist/session/recorder.d.ts +24 -0
- package/dist/session/recorder.d.ts.map +1 -0
- package/dist/session/recorder.js +19 -0
- package/dist/session/recorder.js.map +1 -0
- package/dist/session.d.ts +5 -4
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +213 -110
- package/dist/session.js.map +1 -1
- package/dist/simulation/_internal/source.d.ts.map +1 -1
- package/dist/simulation/_internal/source.js +57 -48
- package/dist/simulation/_internal/source.js.map +1 -1
- package/dist/simulation/index.d.ts +4 -5
- package/dist/simulation/index.d.ts.map +1 -1
- package/dist/simulation/index.js +3 -4
- package/dist/simulation/index.js.map +1 -1
- package/dist/simulation/types.d.ts +16 -2
- package/dist/simulation/types.d.ts.map +1 -1
- package/dist/stats.d.ts +19 -0
- package/dist/stats.d.ts.map +1 -1
- package/dist/stats.js +79 -8
- package/dist/stats.js.map +1 -1
- package/package.json +5 -4
- package/notices/owned-webrtc/Effect-MIT.txt +0 -21
- package/notices/owned-webrtc/IETF.txt +0 -43
- package/notices/owned-webrtc/LICENSE +0 -21
- package/notices/owned-webrtc/UPSTREAM.md +0 -20
- package/notices/owned-webrtc/VP8-ADAPTATION.md +0 -32
- package/notices/owned-webrtc/VP8-AUTHORS.txt +0 -42
- package/notices/owned-webrtc/VP8-LICENSE.txt +0 -25
- package/notices/owned-webrtc/VP8-PATENTS.txt +0 -21
- package/notices/owned-webrtc/Werift-MIT.txt +0 -21
- package/notices/owned-webrtc/libvpx-v1.12.0-LICENSE.txt +0 -28
- package/notices/owned-webrtc/libvpx-v1.12.0-PATENTS.txt +0 -22
- package/notices/owned-webrtc/rfc3711.txt +0 -3139
- package/notices/owned-webrtc/str0m-MIT.txt +0 -7
|
@@ -3,25 +3,31 @@ import * as Clock from "effect/Clock";
|
|
|
3
3
|
import * as Context from "effect/Context";
|
|
4
4
|
import * as Crypto from "effect/Crypto";
|
|
5
5
|
import * as Deferred from "effect/Deferred";
|
|
6
|
+
import * as Duration from "effect/Duration";
|
|
6
7
|
import * as Effect from "effect/Effect";
|
|
7
8
|
import * as Exit from "effect/Exit";
|
|
8
9
|
import * as Fiber from "effect/Fiber";
|
|
10
|
+
import * as Layer from "effect/Layer";
|
|
9
11
|
import * as Option from "effect/Option";
|
|
10
12
|
import * as Result from "effect/Result";
|
|
11
13
|
import * as Scope from "effect/Scope";
|
|
12
14
|
import * as Semaphore from "effect/Semaphore";
|
|
13
|
-
import
|
|
15
|
+
import * as Stream from "effect/Stream";
|
|
16
|
+
import { duration } from "../duration.js";
|
|
17
|
+
import { AcquisitionFailure, CommandFailure, ReactorError, errorOf, parsed, positiveLimit, } from "../errors.js";
|
|
14
18
|
import { Observations } from "../observation.js";
|
|
19
|
+
import { noAcquisition } from "../session/_internal/acquire.js";
|
|
15
20
|
import * as Sequence from "../Sequence.js";
|
|
16
21
|
import * as Submission from "../Submission.js";
|
|
17
|
-
import { CommandFailure } from "../session/commands.js";
|
|
18
|
-
import { AcquisitionFailure } from "../session/index.js";
|
|
19
22
|
import * as MediaBuffer from "./media-buffer.js";
|
|
20
23
|
import * as SourceSlot from "./source-slot.js";
|
|
21
24
|
import { canHandoff, decideRenewal } from "./renewal-state.js";
|
|
22
25
|
import { PolicyFailure, captureRequest } from "./request.js";
|
|
23
26
|
import { emptyState, isIdle } from "./queries.js";
|
|
24
27
|
import { activeIds, generation, resolve } from "./routing.js";
|
|
28
|
+
import { handleContext } from "./types.js";
|
|
29
|
+
const engineOnly = (event) => event._tag === "Engine" ? Result.succeed(event.event) : Result.fail(event);
|
|
30
|
+
const renewalsOnly = (event) => event._tag === "Renewal" ? Result.succeed(event.event) : Result.fail(event);
|
|
25
31
|
/**
|
|
26
32
|
* Explicit application policy for renewal, sequence affinity and continuous
|
|
27
33
|
* recovering media. A physical source still owns each connection generation.
|
|
@@ -37,24 +43,22 @@ export const make = (options) => Effect.gen(function* () {
|
|
|
37
43
|
const commands = yield* Semaphore.make(1);
|
|
38
44
|
const closeGate = yield* Semaphore.make(1);
|
|
39
45
|
const affinity = yield* Sequence.makeAffinity().pipe(Effect.mapError((cause) => errorOf(cause, "InvalidInput")));
|
|
40
|
-
|
|
41
|
-
const
|
|
46
|
+
// One ordered stream: engine events, renewals and media transitions.
|
|
47
|
+
const observations = new Observations();
|
|
48
|
+
const buffer = yield* MediaBuffer.make(yield* parsed(() => ({
|
|
49
|
+
videoFrames: positiveLimit(options.maxQueuedVideoFrames ?? MediaBuffer.defaultLimits.videoFrames, "orchestration maxQueuedVideoFrames"),
|
|
50
|
+
audioSamples: positiveLimit(options.maxQueuedAudioSamples ?? MediaBuffer.defaultLimits.audioSamples, "orchestration maxQueuedAudioSamples"),
|
|
51
|
+
})));
|
|
42
52
|
const fatal = yield* Deferred.make();
|
|
43
53
|
const slots = new Map();
|
|
44
54
|
const cleanups = [];
|
|
45
55
|
const seenCleanups = new Set();
|
|
46
56
|
const maxSessions = options.maxSessions ?? 64;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (!Number.isSafeInteger(maxSessions) ||
|
|
50
|
-
maxSessions < 1 ||
|
|
51
|
-
maxSessions > 4096 ||
|
|
52
|
-
!Number.isFinite(reconnectTimeout) ||
|
|
53
|
-
reconnectTimeout <= 0 ||
|
|
54
|
-
!Number.isFinite(leadSeconds) ||
|
|
55
|
-
leadSeconds < 0) {
|
|
56
|
-
return yield* Effect.fail(new ReactorError("InvalidInput", "Invalid orchestration bounds"));
|
|
57
|
+
if (!Number.isSafeInteger(maxSessions) || maxSessions < 1 || maxSessions > 4096) {
|
|
58
|
+
return yield* ReactorError.fromCode("InvalidInput", "Invalid orchestration bounds");
|
|
57
59
|
}
|
|
60
|
+
const reconnectTimeout = Duration.toMillis(yield* parsed(() => duration(options.reconnectTimeout ?? "10 seconds", "orchestration reconnectTimeout")));
|
|
61
|
+
const leadSeconds = Duration.toSeconds(yield* parsed(() => duration(options.lead ?? "30 seconds", "orchestration lead", { allowZero: true })));
|
|
58
62
|
let current;
|
|
59
63
|
let replacement = { _tag: "Absent" };
|
|
60
64
|
let opened = 0;
|
|
@@ -66,26 +70,35 @@ export const make = (options) => Effect.gen(function* () {
|
|
|
66
70
|
let mediaState = { _tag: "Closed" };
|
|
67
71
|
let terminalFailure;
|
|
68
72
|
let submissionSequence = 0n;
|
|
69
|
-
const
|
|
70
|
-
const log = (message) => Effect.
|
|
71
|
-
const emit = (event) =>
|
|
73
|
+
const announce = (event) => Effect.sync(() => observations.emit({ _tag: "Renewal", event }, 256));
|
|
74
|
+
const log = (message) => Effect.logDebug(message).pipe(Effect.annotateLogs({ module: "reactor.orchestration" }));
|
|
75
|
+
const emit = (event) => observations.emit({ _tag: "Engine", event }, 256);
|
|
76
|
+
const setMedia = (state) => {
|
|
77
|
+
mediaState = state;
|
|
78
|
+
observations.emit({ _tag: "Media", state }, 256);
|
|
79
|
+
};
|
|
80
|
+
const onRenewal = options.onRenewal;
|
|
81
|
+
// Subscribed before the first source opens, so the reader sees its Opened.
|
|
82
|
+
const renewalReader = onRenewal === undefined
|
|
83
|
+
? undefined
|
|
84
|
+
: yield* observations.subscribe({ capacity: 4096 }).pipe(Effect.flatMap((stream) => stream.pipe(Stream.filterMap(renewalsOnly), Stream.runForEach((event) => Effect.suspend(() => onRenewal(event)).pipe(Effect.catchCause((cause) => Effect.logWarning("onRenewal failed", cause).pipe(Effect.annotateLogs({ module: "reactor.orchestration" }))))), Effect.catchCause((cause) => Effect.logWarning("onRenewal reader stopped", cause).pipe(Effect.annotateLogs({ module: "reactor.orchestration" }))), Effect.forkIn(scope))));
|
|
72
85
|
const fail = (cause) => Effect.suspend(() => {
|
|
73
86
|
if (terminalFailure !== undefined)
|
|
74
87
|
return Effect.void;
|
|
75
88
|
terminalFailure = cause;
|
|
76
89
|
// Publish the failed state and queues before waking a failure waiter.
|
|
77
|
-
|
|
90
|
+
setMedia({ _tag: "Failed", cause });
|
|
78
91
|
emit({ _tag: "SessionFailed", failure: cause });
|
|
79
92
|
buffer.fail(cause);
|
|
80
93
|
Deferred.doneUnsafe(fatal, Effect.succeed(cause));
|
|
81
|
-
return
|
|
94
|
+
return announce({ _tag: "Failed", reason: cause.message });
|
|
82
95
|
});
|
|
83
96
|
// Every asynchronous activation uses this final fence. Closed/failed state
|
|
84
97
|
// cannot be replaced by a reconnect or autoplay operation that finished late.
|
|
85
98
|
const publishReady = (slot) => {
|
|
86
99
|
if (closing || slot.closed || slot !== current || terminalFailure !== undefined)
|
|
87
100
|
return false;
|
|
88
|
-
|
|
101
|
+
setMedia({ _tag: "Ready", sessionId: slot.source.id, generation: slot.media.generation });
|
|
89
102
|
return true;
|
|
90
103
|
};
|
|
91
104
|
const recordCleanup = (cleanup) => {
|
|
@@ -97,6 +110,19 @@ export const make = (options) => Effect.gen(function* () {
|
|
|
97
110
|
const openSequences = (slot) => affinity.snapshots.pipe(Effect.map((entries) => entries.some((entry) => entry.owner === slot.source.id && entry.status === "open")));
|
|
98
111
|
const expired = (slot) => slot.expired();
|
|
99
112
|
const recoveryBudget = (slot) => slot.recoveryBudget();
|
|
113
|
+
// The logical output's loss: every former owner's loss while it fed the
|
|
114
|
+
// output, plus the current owner's since it became the owner. A prepared
|
|
115
|
+
// replacement's loss before it fed the output is not the output's.
|
|
116
|
+
let outputLoss = SourceSlot.noLoss;
|
|
117
|
+
let ownerBaseline = SourceSlot.noLoss;
|
|
118
|
+
const ownerLoss = (slot) => Effect.result(slot.pressure.pipe(Effect.timeout(recoveryBudget(slot)))).pipe(Effect.map(SourceSlot.lossOf));
|
|
119
|
+
const activateOwner = (slot) => ownerLoss(slot).pipe(Effect.map((loss) => {
|
|
120
|
+
ownerBaseline = loss;
|
|
121
|
+
}));
|
|
122
|
+
const retireOwner = (slot) => ownerLoss(slot).pipe(Effect.map((loss) => {
|
|
123
|
+
outputLoss = SourceSlot.addLoss(outputLoss, SourceSlot.subtractLoss(loss, ownerBaseline));
|
|
124
|
+
ownerBaseline = SourceSlot.noLoss;
|
|
125
|
+
}));
|
|
100
126
|
const retired = (slot) => slot.retired(buffer.forwarded);
|
|
101
127
|
const closeSlot = (slot) => slot.close;
|
|
102
128
|
const replace = (slot, cause) => Effect.gen(function* () {
|
|
@@ -112,8 +138,10 @@ export const make = (options) => Effect.gen(function* () {
|
|
|
112
138
|
reason: `Session lost: ${cause.message}`,
|
|
113
139
|
sessionId: slot.source.id,
|
|
114
140
|
});
|
|
141
|
+
if (slot === current)
|
|
142
|
+
yield* retireOwner(slot);
|
|
115
143
|
yield* closeSlot(slot);
|
|
116
|
-
yield*
|
|
144
|
+
yield* announce({
|
|
117
145
|
_tag: "Replaced",
|
|
118
146
|
reason: cause.message,
|
|
119
147
|
lostClips: lost.length,
|
|
@@ -133,17 +161,18 @@ export const make = (options) => Effect.gen(function* () {
|
|
|
133
161
|
return;
|
|
134
162
|
}
|
|
135
163
|
current = selected;
|
|
164
|
+
yield* activateOwner(selected);
|
|
136
165
|
yield* current.source.setAutoplay(autoplay);
|
|
137
166
|
if (!publishReady(current))
|
|
138
167
|
return;
|
|
139
168
|
yield* log("Replaced lost session; local queue resumes on the new connection");
|
|
140
|
-
}).pipe(Effect.
|
|
169
|
+
}).pipe(Effect.withSpan("reactor.orchestration.renewal.replace", { attributes: { "reactor.session.id": slot.source.id } }, { captureStackTrace: false }), Effect.catch(fail));
|
|
141
170
|
const recover = (slot, cause) => Effect.gen(function* () {
|
|
142
171
|
if (slot.closed || closing || terminalFailure !== undefined)
|
|
143
172
|
return;
|
|
144
173
|
if (slot === current)
|
|
145
|
-
|
|
146
|
-
yield*
|
|
174
|
+
setMedia({ _tag: "Recovering", sessionId: slot.source.id, cause });
|
|
175
|
+
yield* announce({ _tag: "Recovering", sessionId: slot.source.id, reason: cause.message });
|
|
147
176
|
yield* slot.joinCommitted(recoveryBudget(slot));
|
|
148
177
|
if (slot.closed || closing || terminalFailure !== undefined)
|
|
149
178
|
return;
|
|
@@ -155,7 +184,7 @@ export const make = (options) => Effect.gen(function* () {
|
|
|
155
184
|
// Read the retiring receiver before reconnect makes its counters unavailable.
|
|
156
185
|
const previousPressure = yield* Effect.result(slot.media.pressure.pipe(Effect.timeout(recoveryBudget(slot))));
|
|
157
186
|
if (expired(slot)) {
|
|
158
|
-
yield* replace(slot,
|
|
187
|
+
yield* replace(slot, ReactorError.fromCode("TerminalSession", "Source expired during recovery", {
|
|
159
188
|
operation: "renewal",
|
|
160
189
|
}));
|
|
161
190
|
return;
|
|
@@ -165,7 +194,7 @@ export const make = (options) => Effect.gen(function* () {
|
|
|
165
194
|
return;
|
|
166
195
|
const connected = yield* Effect.result(slot.source.reconnect.pipe(Effect.timeoutOrElse({
|
|
167
196
|
duration: recoveryBudget(slot),
|
|
168
|
-
orElse: () => Effect.fail(
|
|
197
|
+
orElse: () => Effect.fail(ReactorError.fromCode("Disconnected", "Explicit source reconnect timed out")),
|
|
169
198
|
})));
|
|
170
199
|
if (slot.closed || closing || terminalFailure !== undefined)
|
|
171
200
|
return;
|
|
@@ -203,18 +232,18 @@ export const make = (options) => Effect.gen(function* () {
|
|
|
203
232
|
}
|
|
204
233
|
if (slot === current && !publishReady(slot))
|
|
205
234
|
return;
|
|
206
|
-
yield*
|
|
235
|
+
yield* announce({
|
|
207
236
|
_tag: "Reconnected",
|
|
208
237
|
sessionId: slot.source.id,
|
|
209
238
|
generation: slot.media.generation,
|
|
210
239
|
});
|
|
211
|
-
});
|
|
240
|
+
}).pipe(Effect.withSpan("reactor.orchestration.renewal.recover", { attributes: { "reactor.session.id": slot.source.id } }, { captureStackTrace: false }));
|
|
212
241
|
const scheduleRecovery = (slot, cause, mode) => Effect.gen(function* () {
|
|
213
242
|
if (slot.closed || closing || terminalFailure !== undefined)
|
|
214
243
|
return;
|
|
215
244
|
if (!slot.beginRecovery(mode))
|
|
216
245
|
return;
|
|
217
|
-
yield* recover(slot, cause).pipe(commands.
|
|
246
|
+
yield* recover(slot, cause).pipe(commands.withPermits(1), Effect.forkIn(scope));
|
|
218
247
|
});
|
|
219
248
|
const startMedia = (slot) => slot.startMedia({
|
|
220
249
|
video: (frame) => Effect.suspend(() => {
|
|
@@ -234,7 +263,7 @@ export const make = (options) => Effect.gen(function* () {
|
|
|
234
263
|
});
|
|
235
264
|
const acquire = Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
|
|
236
265
|
if (opened >= maxSessions)
|
|
237
|
-
return yield*
|
|
266
|
+
return yield* ReactorError.fromCode("Overflow", "Orchestration session and cleanup-history bound reached");
|
|
238
267
|
const owned = yield* Scope.make();
|
|
239
268
|
let acquired;
|
|
240
269
|
let acquiredSource;
|
|
@@ -243,22 +272,23 @@ export const make = (options) => Effect.gen(function* () {
|
|
|
243
272
|
// Dependency provision order cannot move resources to the caller's owner.
|
|
244
273
|
const value = yield* options.open.pipe(Effect.provideContext(Context.add(context, Scope.Scope, owned)), Effect.timeoutOrElse({
|
|
245
274
|
duration: "30 seconds",
|
|
246
|
-
orElse: () => Effect.fail(
|
|
247
|
-
})
|
|
275
|
+
orElse: () => Effect.fail(ReactorError.fromCode("InvalidState", "Source acquisition timed out")),
|
|
276
|
+
}));
|
|
248
277
|
acquiredSource = value.source;
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
278
|
+
const lifetime = yield* parsed(() => duration(value.lifetime, "source lifetime", { allowInfinite: true }));
|
|
279
|
+
// A lead as long as the lifetime would prepare a replacement at once,
|
|
280
|
+
// after every open, bounded only by maxSessions.
|
|
281
|
+
if (Duration.isFinite(lifetime) && leadSeconds >= Duration.toSeconds(lifetime))
|
|
282
|
+
return yield* ReactorError.fromCode("InvalidInput", "renewal lead must be shorter than the source lifetime", { operation: "renewal", outcome: "not-submitted" });
|
|
253
283
|
if (slots.has(value.source.id))
|
|
254
|
-
return yield*
|
|
284
|
+
return yield* ReactorError.fromCode("InvalidInput", "Orchestration sources must have distinct session identities");
|
|
255
285
|
const media = yield* value.source.media;
|
|
256
286
|
const slot = yield* SourceSlot.make({
|
|
257
287
|
source: value.source,
|
|
258
288
|
scope: owned,
|
|
259
289
|
media,
|
|
260
290
|
openedAt: clock.currentTimeMillisUnsafe(),
|
|
261
|
-
maxSeconds:
|
|
291
|
+
maxSeconds: Duration.toSeconds(lifetime),
|
|
262
292
|
cleanupBudgetMs: reconnectTimeout,
|
|
263
293
|
recordCleanup,
|
|
264
294
|
retireSequences: affinity.retire(value.source.id).pipe(Effect.asVoid),
|
|
@@ -275,10 +305,10 @@ export const make = (options) => Effect.gen(function* () {
|
|
|
275
305
|
return Effect.void;
|
|
276
306
|
}), (cause) => scheduleRecovery(slot, cause, "replace"));
|
|
277
307
|
yield* startMedia(slot);
|
|
278
|
-
yield*
|
|
308
|
+
yield* announce({
|
|
279
309
|
_tag: "Opened",
|
|
280
310
|
sessionId: slot.source.id,
|
|
281
|
-
|
|
311
|
+
lifetime: Duration.seconds(slot.maxSeconds),
|
|
282
312
|
});
|
|
283
313
|
return slot;
|
|
284
314
|
})).pipe(Effect.onExit((exit) => Exit.isSuccess(exit)
|
|
@@ -292,12 +322,12 @@ export const make = (options) => Effect.gen(function* () {
|
|
|
292
322
|
yield* Scope.close(owned, exit);
|
|
293
323
|
if (Exit.isFailure(exit)) {
|
|
294
324
|
const failure = Cause.findErrorOption(exit.cause);
|
|
295
|
-
if (Option.isSome(failure) && failure.value
|
|
325
|
+
if (Option.isSome(failure) && AcquisitionFailure.is(failure.value))
|
|
296
326
|
recordCleanup({ lease: failure.value.cleanup, policy: [] });
|
|
297
327
|
}
|
|
298
328
|
}
|
|
299
329
|
})));
|
|
300
|
-
}));
|
|
330
|
+
})).pipe(Effect.tap((slot) => Effect.annotateCurrentSpan("reactor.session.id", slot.source.id)), Effect.withSpan("reactor.orchestration.renewal.open", {}, { captureStackTrace: false }));
|
|
301
331
|
const close = closeGate.withPermit(Effect.uninterruptible(Effect.gen(function* () {
|
|
302
332
|
if (finalReport !== undefined)
|
|
303
333
|
return finalReport;
|
|
@@ -306,29 +336,37 @@ export const make = (options) => Effect.gen(function* () {
|
|
|
306
336
|
yield* Fiber.interrupt(replacement.fiber);
|
|
307
337
|
yield* Effect.forEach([...slots.values()], closeSlot, { discard: true });
|
|
308
338
|
buffer.end();
|
|
309
|
-
|
|
310
|
-
|
|
339
|
+
setMedia({ _tag: "Closed" });
|
|
340
|
+
observations.end();
|
|
341
|
+
// The reader delivers what was announced before close, within a bound.
|
|
342
|
+
if (renewalReader !== undefined)
|
|
343
|
+
yield* Fiber.await(renewalReader).pipe(Effect.interruptible, Effect.timeout("1 second"), Effect.ignore);
|
|
311
344
|
finalReport = Object.freeze({ sessions: Object.freeze([...cleanups]) });
|
|
312
345
|
return finalReport;
|
|
313
346
|
})));
|
|
314
347
|
yield* Effect.addFinalizer(() => close);
|
|
315
348
|
current = yield* acquire.pipe(Effect.tap((slot) => slot.source.setAutoplay(true)), Effect.catch((cause) => close.pipe(Effect.flatMap((report) => {
|
|
349
|
+
// An AcquisitionFailure from `open` already carries the lease it
|
|
350
|
+
// recorded, by reference; any later failure takes the recorded lease.
|
|
316
351
|
const lease = report.sessions[0]?.lease;
|
|
317
|
-
return Effect.fail(lease === undefined
|
|
352
|
+
return Effect.fail(AcquisitionFailure.is(cause) || (lease === undefined && ReactorError.is(cause))
|
|
353
|
+
? cause
|
|
354
|
+
: AcquisitionFailure.from(cause, lease ?? noAcquisition));
|
|
318
355
|
}))));
|
|
319
|
-
|
|
356
|
+
// The first owner's loss from its start is the output's: no baseline.
|
|
357
|
+
setMedia({
|
|
320
358
|
_tag: "Ready",
|
|
321
359
|
sessionId: current.source.id,
|
|
322
360
|
generation: current.media.generation,
|
|
323
|
-
};
|
|
361
|
+
});
|
|
324
362
|
const guard = (operation, effect) => Effect.gen(function* () {
|
|
325
363
|
if (closing)
|
|
326
|
-
return yield*
|
|
364
|
+
return yield* PolicyFailure.refuse("SessionClosed", "Orchestration is closed", operation);
|
|
327
365
|
if (yield* Deferred.isDone(fatal))
|
|
328
|
-
return yield*
|
|
366
|
+
return yield* CommandFailure.from(yield* Deferred.await(fatal), {
|
|
329
367
|
operation,
|
|
330
368
|
outcome: "not-submitted",
|
|
331
|
-
})
|
|
369
|
+
});
|
|
332
370
|
return yield* effect;
|
|
333
371
|
});
|
|
334
372
|
const route = (request) => Effect.gen(function* () {
|
|
@@ -345,10 +383,10 @@ export const make = (options) => Effect.gen(function* () {
|
|
|
345
383
|
? next
|
|
346
384
|
: current;
|
|
347
385
|
if (preferred === undefined)
|
|
348
|
-
return yield*
|
|
386
|
+
return yield* PolicyFailure.refuse("SessionRecovering", "No source is ready");
|
|
349
387
|
return yield* resolve(request, values, preferred.source.id, binding);
|
|
350
388
|
});
|
|
351
|
-
const sequenceError = (error) =>
|
|
389
|
+
const sequenceError = (error) => PolicyFailure.sequence(error.sequenceId, error.code);
|
|
352
390
|
const prepare = (input) => Effect.gen(function* () {
|
|
353
391
|
const request = yield* captureRequest(input);
|
|
354
392
|
const id = `${namespace}:${++submissionSequence}`;
|
|
@@ -360,30 +398,26 @@ export const make = (options) => Effect.gen(function* () {
|
|
|
360
398
|
// A committed submission remains a readable outcome after the handle
|
|
361
399
|
// closes. Only selecting or committing fresh work needs a live owner.
|
|
362
400
|
yield* guard("enqueue", Effect.void);
|
|
363
|
-
const decision = yield* route(request).pipe(commands.
|
|
401
|
+
const decision = yield* route(request).pipe(commands.withPermits(1));
|
|
364
402
|
const target = slots.get(decision.owner);
|
|
365
403
|
if (target === undefined)
|
|
366
|
-
return yield*
|
|
404
|
+
return yield* PolicyFailure.refuse("SessionRetired", "Selected source was retired");
|
|
367
405
|
const sequence = request.sequence;
|
|
368
406
|
active = yield* target.source.prepareRouted({ request, position: decision.position }, {
|
|
369
407
|
commit: (submissionId) => commands.withPermit(guard("enqueue", Effect.gen(function* () {
|
|
370
408
|
const checked = yield* route(request);
|
|
371
409
|
if (checked.owner !== decision.owner ||
|
|
372
410
|
checked.position !== decision.position) {
|
|
373
|
-
return yield*
|
|
411
|
+
return yield* PolicyFailure.refuse("RouteChanged", "Request ownership or insertion position changed during preparation");
|
|
374
412
|
}
|
|
375
413
|
if (active === undefined)
|
|
376
414
|
return yield* Effect.die(new Error("Source committed before returning its inert submission"));
|
|
377
415
|
yield* target.register(active, Effect.gen(function* () {
|
|
378
416
|
if (sequence !== undefined) {
|
|
379
|
-
yield* affinity
|
|
380
|
-
|
|
381
|
-
.pipe(Effect.mapError(sequenceError));
|
|
382
|
-
yield* affinity
|
|
383
|
-
.begin(sequence.id, sequence.memberId ?? submissionId)
|
|
384
|
-
.pipe(Effect.mapError(sequenceError));
|
|
417
|
+
yield* affinity.bind(sequence.id, target.source.id);
|
|
418
|
+
yield* affinity.begin(sequence.id, sequence.memberId ?? submissionId);
|
|
385
419
|
}
|
|
386
|
-
}));
|
|
420
|
+
}).pipe(Effect.mapError(sequenceError)));
|
|
387
421
|
}))),
|
|
388
422
|
result: (submissionId, result) => Effect.gen(function* () {
|
|
389
423
|
target.recordResult(result);
|
|
@@ -397,7 +431,7 @@ export const make = (options) => Effect.gen(function* () {
|
|
|
397
431
|
yield* account.pipe(Effect.catch((cause) => Effect.gen(function* () {
|
|
398
432
|
target.markIndeterminate();
|
|
399
433
|
yield* affinity.retire(target.source.id);
|
|
400
|
-
yield* fail(
|
|
434
|
+
yield* fail(ReactorError.fromCode("InvalidState", "Committed sequence accounting failed", { detail: cause }));
|
|
401
435
|
})));
|
|
402
436
|
}
|
|
403
437
|
target.finishAccounting();
|
|
@@ -458,16 +492,20 @@ export const make = (options) => Effect.gen(function* () {
|
|
|
458
492
|
const matches = yield* Effect.forEach([...slots.values()].filter((slot) => !slot.closed), (slot) => slot.source.state.pipe(Effect.map((state) => ({ slot, state }))));
|
|
459
493
|
const found = matches.filter(({ state }) => activeIds(state).includes(id));
|
|
460
494
|
if (found.length !== 1)
|
|
461
|
-
return yield*
|
|
495
|
+
return yield* PolicyFailure.refuse(found.length === 0 ? "NotFound" : "OwnerConflict", "Clip has no unique active owning session", operation);
|
|
462
496
|
if (found[0].slot.recovering)
|
|
463
|
-
return yield*
|
|
497
|
+
return yield* PolicyFailure.refuse("SessionRecovering", "Owning session is recovering", operation);
|
|
464
498
|
return found[0];
|
|
465
499
|
});
|
|
466
500
|
const engine = {
|
|
467
501
|
prepare,
|
|
468
502
|
enqueue: (request) => prepare(request).pipe(Effect.flatMap((submission) => submission.submit)),
|
|
469
503
|
state,
|
|
470
|
-
events:
|
|
504
|
+
events: Stream.filterMap(observations.stream(), engineOnly),
|
|
505
|
+
observe: (options) => observations.observeWith(state, options).pipe(Effect.map(({ initial, events }) => ({
|
|
506
|
+
initial,
|
|
507
|
+
events: Stream.filterMap(events, engineOnly),
|
|
508
|
+
}))),
|
|
471
509
|
failure: Deferred.await(fatal),
|
|
472
510
|
setAutoplay: (enabled) => commands.withPermit(guard("set_autoplay", Effect.gen(function* () {
|
|
473
511
|
autoplay = enabled;
|
|
@@ -485,11 +523,11 @@ export const make = (options) => Effect.gen(function* () {
|
|
|
485
523
|
remove: (id) => commands.withPermit(guard("pop", owner(id, "pop").pipe(Effect.flatMap(({ slot }) => slot.source.remove(id))))),
|
|
486
524
|
move: (id, position, queue) => commands.withPermit(guard("move", Effect.gen(function* () {
|
|
487
525
|
if (!Number.isSafeInteger(position) || position < 0)
|
|
488
|
-
return yield*
|
|
526
|
+
return yield* PolicyFailure.refuse("InvalidRequest", "Move position must be a nonnegative integer", "move");
|
|
489
527
|
const selected = yield* owner(id, "move");
|
|
490
528
|
const own = queue === "generation" ? generation(selected.state) : selected.state.ready;
|
|
491
529
|
if (!own.some((clip) => clip.clipId === id))
|
|
492
|
-
return yield*
|
|
530
|
+
return yield* PolicyFailure.refuse("QueueChanged", "Clip is no longer in the selected application queue", "move");
|
|
493
531
|
let preceding = 0;
|
|
494
532
|
for (const slot of slots.values()) {
|
|
495
533
|
if (slot === selected.slot)
|
|
@@ -503,7 +541,7 @@ export const make = (options) => Effect.gen(function* () {
|
|
|
503
541
|
}))),
|
|
504
542
|
setCanvas: (canvas) => commands.withPermit(guard("set_canvas", Effect.gen(function* () {
|
|
505
543
|
if (!isIdle(yield* state))
|
|
506
|
-
return yield*
|
|
544
|
+
return yield* PolicyFailure.refuse("Busy", "Canvas can only change while every source is idle", "set_canvas");
|
|
507
545
|
for (const slot of slots.values())
|
|
508
546
|
if (!slot.closed)
|
|
509
547
|
yield* slot.source.setCanvas(canvas);
|
|
@@ -525,18 +563,18 @@ export const make = (options) => Effect.gen(function* () {
|
|
|
525
563
|
replacement = { _tag: "Ready", slot: result.value };
|
|
526
564
|
openFailures = 0;
|
|
527
565
|
yield* log("Prepared the next session for renewal");
|
|
528
|
-
yield*
|
|
566
|
+
yield* announce({ _tag: "Prepared" });
|
|
529
567
|
}
|
|
530
568
|
else {
|
|
531
569
|
openFailures++;
|
|
532
570
|
retryAt = clock.currentTimeMillisUnsafe() + 5000;
|
|
533
|
-
yield*
|
|
571
|
+
yield* announce({
|
|
534
572
|
_tag: "SetupFailed",
|
|
535
573
|
reason: "Could not prepare the next source",
|
|
536
574
|
consecutive: openFailures,
|
|
537
575
|
});
|
|
538
576
|
if (openFailures >= 3)
|
|
539
|
-
return yield* fail(
|
|
577
|
+
return yield* fail(ReactorError.fromCode("Disconnected", "Repeated source renewal acquisition failed"));
|
|
540
578
|
}
|
|
541
579
|
}
|
|
542
580
|
}
|
|
@@ -552,7 +590,7 @@ export const make = (options) => Effect.gen(function* () {
|
|
|
552
590
|
case "Retain":
|
|
553
591
|
return;
|
|
554
592
|
case "Expire":
|
|
555
|
-
yield* replace(current,
|
|
593
|
+
yield* replace(current, ReactorError.fromCode("TerminalSession", "Source lifetime limit reached", {
|
|
556
594
|
operation: "renewal",
|
|
557
595
|
}));
|
|
558
596
|
return;
|
|
@@ -582,31 +620,47 @@ export const make = (options) => Effect.gen(function* () {
|
|
|
582
620
|
}))
|
|
583
621
|
return;
|
|
584
622
|
const old = current;
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
623
|
+
// The switch is traced; the tick that found it is not.
|
|
624
|
+
yield* Effect.gen(function* () {
|
|
625
|
+
yield* retireOwner(old);
|
|
626
|
+
current = next;
|
|
627
|
+
yield* activateOwner(next);
|
|
628
|
+
replacement = { _tag: "Absent" };
|
|
629
|
+
yield* next.source.setAutoplay(autoplay);
|
|
630
|
+
const activated = publishReady(next);
|
|
631
|
+
yield* closeSlot(old);
|
|
632
|
+
if (!activated)
|
|
633
|
+
return;
|
|
634
|
+
yield* announce({ _tag: "Switched", ...tail });
|
|
635
|
+
yield* log("Switched prepared sessions at a sequence boundary");
|
|
636
|
+
}).pipe(Effect.withSpan("reactor.orchestration.renewal.switch", {
|
|
637
|
+
attributes: {
|
|
638
|
+
"reactor.session.id": next.source.id,
|
|
639
|
+
"reactor.session.previous_id": old.source.id,
|
|
640
|
+
},
|
|
641
|
+
}, { captureStackTrace: false }));
|
|
594
642
|
}))
|
|
595
|
-
.pipe(Effect.catch(
|
|
643
|
+
.pipe(Effect.catch(fail));
|
|
596
644
|
yield* Effect.forever(Effect.sleep(100).pipe(Effect.andThen(tick))).pipe(Effect.forkIn(scope));
|
|
597
645
|
const pressure = Effect.suspend(() => {
|
|
598
646
|
if (current === undefined)
|
|
599
|
-
return Effect.fail(
|
|
647
|
+
return Effect.fail(ReactorError.fromCode("InvalidState", "No active media source"));
|
|
600
648
|
const owner = current;
|
|
601
|
-
return owner.pressure.pipe(Effect.
|
|
649
|
+
return owner.pressure.pipe(Effect.flatMap((source) => {
|
|
602
650
|
const queued = buffer.pressure();
|
|
603
|
-
|
|
651
|
+
const loss = SourceSlot.addLoss(outputLoss, SourceSlot.subtractLoss(SourceSlot.lossOf(Result.succeed(source)), ownerBaseline));
|
|
652
|
+
if (loss.video === null || loss.audio === null || loss.readers === null)
|
|
653
|
+
return Effect.fail(ReactorError.fromCode("InvalidState", "A former media owner's loss totals are unknown"));
|
|
654
|
+
return Effect.succeed({
|
|
604
655
|
...source,
|
|
605
656
|
closed: closing,
|
|
606
657
|
queuedVideo: source.queuedVideo + queued.queuedVideo,
|
|
607
658
|
queuedAudio: source.queuedAudio + queued.queuedAudio,
|
|
608
659
|
queuedBytes: source.queuedBytes + queued.queuedBytes,
|
|
609
|
-
|
|
660
|
+
droppedVideo: loss.video,
|
|
661
|
+
droppedAudio: loss.audio,
|
|
662
|
+
readerOverflows: loss.readers,
|
|
663
|
+
});
|
|
610
664
|
}));
|
|
611
665
|
});
|
|
612
666
|
return {
|
|
@@ -618,6 +672,7 @@ export const make = (options) => Effect.gen(function* () {
|
|
|
618
672
|
videoFramesPerSecond: current.media.videoFramesPerSecond,
|
|
619
673
|
},
|
|
620
674
|
mediaState: Effect.sync(() => mediaState),
|
|
675
|
+
observe: (options) => observations.observeWith(Effect.map(state, (engine) => ({ engine, media: mediaState })), options),
|
|
621
676
|
sessionId: Effect.sync(() => current === undefined || current.closed ? Option.none() : Option.some(current.source.id)),
|
|
622
677
|
close,
|
|
623
678
|
cleanup: Effect.sync(() => finalReport === undefined ? Option.none() : Option.some(finalReport)),
|
|
@@ -630,4 +685,10 @@ export const make = (options) => Effect.gen(function* () {
|
|
|
630
685
|
},
|
|
631
686
|
};
|
|
632
687
|
});
|
|
688
|
+
/**
|
|
689
|
+
* The handle's `Engine`, `Media` and `Handle` services from one orchestration,
|
|
690
|
+
* so the three can never come from two paid session chains. Each build of the
|
|
691
|
+
* layer opens its own chain: bind it to a `const` and provide that one value.
|
|
692
|
+
*/
|
|
693
|
+
export const layer = (options) => Layer.effectContext(Effect.map(make(options), handleContext));
|
|
633
694
|
//# sourceMappingURL=renewal.js.map
|