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.
Files changed (267) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +29 -0
  3. package/README.md +124 -2
  4. package/dist/FetchHttp.d.ts +9 -0
  5. package/dist/FetchHttp.d.ts.map +1 -0
  6. package/dist/FetchHttp.js +63 -0
  7. package/dist/FetchHttp.js.map +1 -0
  8. package/dist/PeerFactory.d.ts +15 -0
  9. package/dist/PeerFactory.d.ts.map +1 -0
  10. package/dist/PeerFactory.js +4 -0
  11. package/dist/PeerFactory.js.map +1 -0
  12. package/dist/PeerTypes.d.ts +64 -0
  13. package/dist/PeerTypes.d.ts.map +1 -0
  14. package/dist/PeerTypes.js +2 -0
  15. package/dist/PeerTypes.js.map +1 -0
  16. package/dist/Sequence.d.ts +81 -0
  17. package/dist/Sequence.d.ts.map +1 -0
  18. package/dist/Sequence.js +215 -0
  19. package/dist/Sequence.js.map +1 -0
  20. package/dist/SessionTypes.d.ts +164 -0
  21. package/dist/SessionTypes.d.ts.map +1 -0
  22. package/dist/SessionTypes.js +2 -0
  23. package/dist/SessionTypes.js.map +1 -0
  24. package/dist/Submission.d.ts +42 -0
  25. package/dist/Submission.d.ts.map +1 -0
  26. package/dist/Submission.js +52 -0
  27. package/dist/Submission.js.map +1 -0
  28. package/dist/bytes.d.ts +6 -0
  29. package/dist/bytes.d.ts.map +1 -0
  30. package/dist/bytes.js +32 -0
  31. package/dist/bytes.js.map +1 -0
  32. package/dist/contract.d.ts +58 -0
  33. package/dist/contract.d.ts.map +1 -0
  34. package/dist/contract.js +87 -0
  35. package/dist/contract.js.map +1 -0
  36. package/dist/coordinator/_internal/client.d.ts +127 -0
  37. package/dist/coordinator/_internal/client.d.ts.map +1 -0
  38. package/dist/coordinator/_internal/client.js +450 -0
  39. package/dist/coordinator/_internal/client.js.map +1 -0
  40. package/dist/coordinator/_internal/response.d.ts +15 -0
  41. package/dist/coordinator/_internal/response.d.ts.map +1 -0
  42. package/dist/coordinator/_internal/response.js +57 -0
  43. package/dist/coordinator/_internal/response.js.map +1 -0
  44. package/dist/coordinator/_internal/schemas.d.ts +51 -0
  45. package/dist/coordinator/_internal/schemas.d.ts.map +1 -0
  46. package/dist/coordinator/_internal/schemas.js +170 -0
  47. package/dist/coordinator/_internal/schemas.js.map +1 -0
  48. package/dist/coordinator/index.d.ts +28 -0
  49. package/dist/coordinator/index.d.ts.map +1 -0
  50. package/dist/coordinator/index.js +25 -0
  51. package/dist/coordinator/index.js.map +1 -0
  52. package/dist/correlation.d.ts +32 -0
  53. package/dist/correlation.d.ts.map +1 -0
  54. package/dist/correlation.js +113 -0
  55. package/dist/correlation.js.map +1 -0
  56. package/dist/errors.d.ts +88 -0
  57. package/dist/errors.d.ts.map +1 -0
  58. package/dist/errors.js +93 -0
  59. package/dist/errors.js.map +1 -0
  60. package/dist/h3/_internal/client.d.ts +9 -0
  61. package/dist/h3/_internal/client.d.ts.map +1 -0
  62. package/dist/h3/_internal/client.js +510 -0
  63. package/dist/h3/_internal/client.js.map +1 -0
  64. package/dist/h3/_internal/contracts.d.ts +123 -0
  65. package/dist/h3/_internal/contracts.d.ts.map +1 -0
  66. package/dist/h3/_internal/contracts.js +113 -0
  67. package/dist/h3/_internal/contracts.js.map +1 -0
  68. package/dist/h3/_internal/deployment.d.ts +8 -0
  69. package/dist/h3/_internal/deployment.d.ts.map +1 -0
  70. package/dist/h3/_internal/deployment.js +132 -0
  71. package/dist/h3/_internal/deployment.js.map +1 -0
  72. package/dist/h3/_internal/evidence.d.ts +17 -0
  73. package/dist/h3/_internal/evidence.d.ts.map +1 -0
  74. package/dist/h3/_internal/evidence.js +48 -0
  75. package/dist/h3/_internal/evidence.js.map +1 -0
  76. package/dist/h3/_internal/references.d.ts +18 -0
  77. package/dist/h3/_internal/references.d.ts.map +1 -0
  78. package/dist/h3/_internal/references.js +186 -0
  79. package/dist/h3/_internal/references.js.map +1 -0
  80. package/dist/h3/_internal/request.d.ts +16 -0
  81. package/dist/h3/_internal/request.d.ts.map +1 -0
  82. package/dist/h3/_internal/request.js +79 -0
  83. package/dist/h3/_internal/request.js.map +1 -0
  84. package/dist/h3/_internal/state.d.ts +30 -0
  85. package/dist/h3/_internal/state.d.ts.map +1 -0
  86. package/dist/h3/_internal/state.js +243 -0
  87. package/dist/h3/_internal/state.js.map +1 -0
  88. package/dist/h3/index.d.ts +10 -0
  89. package/dist/h3/index.d.ts.map +1 -0
  90. package/dist/h3/index.js +7 -0
  91. package/dist/h3/index.js.map +1 -0
  92. package/dist/h3/messages.d.ts +310 -0
  93. package/dist/h3/messages.d.ts.map +1 -0
  94. package/dist/h3/messages.js +114 -0
  95. package/dist/h3/messages.js.map +1 -0
  96. package/dist/h3/profile.d.ts +109 -0
  97. package/dist/h3/profile.d.ts.map +1 -0
  98. package/dist/h3/profile.js +70 -0
  99. package/dist/h3/profile.js.map +1 -0
  100. package/dist/h3/types.d.ts +173 -0
  101. package/dist/h3/types.d.ts.map +1 -0
  102. package/dist/h3/types.js +2 -0
  103. package/dist/h3/types.js.map +1 -0
  104. package/dist/host.d.ts +25 -0
  105. package/dist/host.d.ts.map +1 -0
  106. package/dist/host.js +20 -0
  107. package/dist/host.js.map +1 -0
  108. package/dist/index.d.ts +17 -0
  109. package/dist/index.d.ts.map +1 -0
  110. package/dist/index.js +8 -0
  111. package/dist/index.js.map +1 -0
  112. package/dist/json.d.ts +18 -0
  113. package/dist/json.d.ts.map +1 -0
  114. package/dist/json.js +123 -0
  115. package/dist/json.js.map +1 -0
  116. package/dist/media-stream.d.ts +10 -0
  117. package/dist/media-stream.d.ts.map +1 -0
  118. package/dist/media-stream.js +20 -0
  119. package/dist/media-stream.js.map +1 -0
  120. package/dist/observation.d.ts +26 -0
  121. package/dist/observation.d.ts.map +1 -0
  122. package/dist/observation.js +106 -0
  123. package/dist/observation.js.map +1 -0
  124. package/dist/orchestration/h3-source.d.ts +30 -0
  125. package/dist/orchestration/h3-source.d.ts.map +1 -0
  126. package/dist/orchestration/h3-source.js +303 -0
  127. package/dist/orchestration/h3-source.js.map +1 -0
  128. package/dist/orchestration/index.d.ts +12 -0
  129. package/dist/orchestration/index.d.ts.map +1 -0
  130. package/dist/orchestration/index.js +9 -0
  131. package/dist/orchestration/index.js.map +1 -0
  132. package/dist/orchestration/media-buffer.d.ts +23 -0
  133. package/dist/orchestration/media-buffer.d.ts.map +1 -0
  134. package/dist/orchestration/media-buffer.js +73 -0
  135. package/dist/orchestration/media-buffer.js.map +1 -0
  136. package/dist/orchestration/queries.d.ts +10 -0
  137. package/dist/orchestration/queries.d.ts.map +1 -0
  138. package/dist/orchestration/queries.js +40 -0
  139. package/dist/orchestration/queries.js.map +1 -0
  140. package/dist/orchestration/references.d.ts +16 -0
  141. package/dist/orchestration/references.d.ts.map +1 -0
  142. package/dist/orchestration/references.js +76 -0
  143. package/dist/orchestration/references.js.map +1 -0
  144. package/dist/orchestration/renewal-state.d.ts +58 -0
  145. package/dist/orchestration/renewal-state.d.ts.map +1 -0
  146. package/dist/orchestration/renewal-state.js +60 -0
  147. package/dist/orchestration/renewal-state.js.map +1 -0
  148. package/dist/orchestration/renewal.d.ts +76 -0
  149. package/dist/orchestration/renewal.d.ts.map +1 -0
  150. package/dist/orchestration/renewal.js +633 -0
  151. package/dist/orchestration/renewal.js.map +1 -0
  152. package/dist/orchestration/request.d.ts +53 -0
  153. package/dist/orchestration/request.d.ts.map +1 -0
  154. package/dist/orchestration/request.js +122 -0
  155. package/dist/orchestration/request.js.map +1 -0
  156. package/dist/orchestration/routing.d.ts +21 -0
  157. package/dist/orchestration/routing.d.ts.map +1 -0
  158. package/dist/orchestration/routing.js +90 -0
  159. package/dist/orchestration/routing.js.map +1 -0
  160. package/dist/orchestration/source-slot.d.ts +99 -0
  161. package/dist/orchestration/source-slot.d.ts.map +1 -0
  162. package/dist/orchestration/source-slot.js +206 -0
  163. package/dist/orchestration/source-slot.js.map +1 -0
  164. package/dist/orchestration/types.d.ts +334 -0
  165. package/dist/orchestration/types.d.ts.map +1 -0
  166. package/dist/orchestration/types.js +11 -0
  167. package/dist/orchestration/types.js.map +1 -0
  168. package/dist/protobuf.d.ts +63 -0
  169. package/dist/protobuf.d.ts.map +1 -0
  170. package/dist/protobuf.js +252 -0
  171. package/dist/protobuf.js.map +1 -0
  172. package/dist/session/_internal/acquire.d.ts +23 -0
  173. package/dist/session/_internal/acquire.d.ts.map +1 -0
  174. package/dist/session/_internal/acquire.js +167 -0
  175. package/dist/session/_internal/acquire.js.map +1 -0
  176. package/dist/session/_internal/cleanup.d.ts +18 -0
  177. package/dist/session/_internal/cleanup.d.ts.map +1 -0
  178. package/dist/session/_internal/cleanup.js +94 -0
  179. package/dist/session/_internal/cleanup.js.map +1 -0
  180. package/dist/session/_internal/lifecycle.d.ts +62 -0
  181. package/dist/session/_internal/lifecycle.d.ts.map +1 -0
  182. package/dist/session/_internal/lifecycle.js +132 -0
  183. package/dist/session/_internal/lifecycle.js.map +1 -0
  184. package/dist/session/_internal/remote.d.ts +28 -0
  185. package/dist/session/_internal/remote.d.ts.map +1 -0
  186. package/dist/session/_internal/remote.js +61 -0
  187. package/dist/session/_internal/remote.js.map +1 -0
  188. package/dist/session/_internal/uploads.d.ts +4 -0
  189. package/dist/session/_internal/uploads.d.ts.map +1 -0
  190. package/dist/session/_internal/uploads.js +33 -0
  191. package/dist/session/_internal/uploads.js.map +1 -0
  192. package/dist/session/commands.d.ts +22 -0
  193. package/dist/session/commands.d.ts.map +1 -0
  194. package/dist/session/commands.js +13 -0
  195. package/dist/session/commands.js.map +1 -0
  196. package/dist/session/files.d.ts +15 -0
  197. package/dist/session/files.d.ts.map +1 -0
  198. package/dist/session/files.js +54 -0
  199. package/dist/session/files.js.map +1 -0
  200. package/dist/session/index.d.ts +76 -0
  201. package/dist/session/index.d.ts.map +1 -0
  202. package/dist/session/index.js +16 -0
  203. package/dist/session/index.js.map +1 -0
  204. package/dist/session/media.d.ts +67 -0
  205. package/dist/session/media.d.ts.map +1 -0
  206. package/dist/session/media.js +2 -0
  207. package/dist/session/media.js.map +1 -0
  208. package/dist/session.d.ts +104 -0
  209. package/dist/session.d.ts.map +1 -0
  210. package/dist/session.js +872 -0
  211. package/dist/session.js.map +1 -0
  212. package/dist/simulation/_internal/source.d.ts +12 -0
  213. package/dist/simulation/_internal/source.d.ts.map +1 -0
  214. package/dist/simulation/_internal/source.js +477 -0
  215. package/dist/simulation/_internal/source.js.map +1 -0
  216. package/dist/simulation/index.d.ts +14 -0
  217. package/dist/simulation/index.d.ts.map +1 -0
  218. package/dist/simulation/index.js +13 -0
  219. package/dist/simulation/index.js.map +1 -0
  220. package/dist/simulation/types.d.ts +29 -0
  221. package/dist/simulation/types.d.ts.map +1 -0
  222. package/dist/simulation/types.js +2 -0
  223. package/dist/simulation/types.js.map +1 -0
  224. package/dist/stats.d.ts +33 -0
  225. package/dist/stats.d.ts.map +1 -0
  226. package/dist/stats.js +134 -0
  227. package/dist/stats.js.map +1 -0
  228. package/dist/testing/Faults.d.ts +4 -0
  229. package/dist/testing/Faults.d.ts.map +1 -0
  230. package/dist/testing/Faults.js +7 -0
  231. package/dist/testing/Faults.js.map +1 -0
  232. package/dist/testing/Png.d.ts +3 -0
  233. package/dist/testing/Png.d.ts.map +1 -0
  234. package/dist/testing/Png.js +68 -0
  235. package/dist/testing/Png.js.map +1 -0
  236. package/dist/testing/index.d.ts +3 -0
  237. package/dist/testing/index.d.ts.map +1 -0
  238. package/dist/testing/index.js +3 -0
  239. package/dist/testing/index.js.map +1 -0
  240. package/dist/wire.d.ts +7 -0
  241. package/dist/wire.d.ts.map +1 -0
  242. package/dist/wire.generated.d.ts +305 -0
  243. package/dist/wire.generated.d.ts.map +1 -0
  244. package/dist/wire.generated.js +1257 -0
  245. package/dist/wire.generated.js.map +1 -0
  246. package/dist/wire.js +5 -0
  247. package/dist/wire.js.map +1 -0
  248. package/notices/effect-LICENSE +21 -0
  249. package/notices/google-protobuf-LICENSE +32 -0
  250. package/notices/owned-webrtc/Effect-MIT.txt +21 -0
  251. package/notices/owned-webrtc/IETF.txt +43 -0
  252. package/notices/owned-webrtc/LICENSE +21 -0
  253. package/notices/owned-webrtc/UPSTREAM.md +20 -0
  254. package/notices/owned-webrtc/VP8-ADAPTATION.md +32 -0
  255. package/notices/owned-webrtc/VP8-AUTHORS.txt +42 -0
  256. package/notices/owned-webrtc/VP8-LICENSE.txt +25 -0
  257. package/notices/owned-webrtc/VP8-PATENTS.txt +21 -0
  258. package/notices/owned-webrtc/Werift-MIT.txt +21 -0
  259. package/notices/owned-webrtc/libvpx-v1.12.0-LICENSE.txt +28 -0
  260. package/notices/owned-webrtc/libvpx-v1.12.0-PATENTS.txt +22 -0
  261. package/notices/owned-webrtc/rfc3711.txt +3139 -0
  262. package/notices/owned-webrtc/str0m-MIT.txt +7 -0
  263. package/notices/owned-webrtc-LICENSE +21 -0
  264. package/notices/reactor-runtime-LICENSE +201 -0
  265. package/notices/reactor-runtime-NOTICE +5 -0
  266. package/notices/reactor-sdk-LICENSE +201 -0
  267. package/package.json +62 -4
@@ -0,0 +1,633 @@
1
+ import * as Cause from "effect/Cause";
2
+ import * as Clock from "effect/Clock";
3
+ import * as Context from "effect/Context";
4
+ import * as Crypto from "effect/Crypto";
5
+ import * as Deferred from "effect/Deferred";
6
+ import * as Effect from "effect/Effect";
7
+ import * as Exit from "effect/Exit";
8
+ import * as Fiber from "effect/Fiber";
9
+ import * as Option from "effect/Option";
10
+ import * as Result from "effect/Result";
11
+ import * as Scope from "effect/Scope";
12
+ import * as Semaphore from "effect/Semaphore";
13
+ import { ReactorError, errorOf } from "../errors.js";
14
+ import { Observations } from "../observation.js";
15
+ import * as Sequence from "../Sequence.js";
16
+ import * as Submission from "../Submission.js";
17
+ import { CommandFailure } from "../session/commands.js";
18
+ import { AcquisitionFailure } from "../session/index.js";
19
+ import * as MediaBuffer from "./media-buffer.js";
20
+ import * as SourceSlot from "./source-slot.js";
21
+ import { canHandoff, decideRenewal } from "./renewal-state.js";
22
+ import { PolicyFailure, captureRequest } from "./request.js";
23
+ import { emptyState, isIdle } from "./queries.js";
24
+ import { activeIds, generation, resolve } from "./routing.js";
25
+ /**
26
+ * Explicit application policy for renewal, sequence affinity and continuous
27
+ * recovering media. A physical source still owns each connection generation.
28
+ */
29
+ export const make = (options) => Effect.gen(function* () {
30
+ const scope = yield* Effect.scope;
31
+ const context = yield* Effect.context();
32
+ const clock = yield* Clock.Clock;
33
+ const random = yield* (yield* Crypto.Crypto)
34
+ .randomBytes(16)
35
+ .pipe(Effect.mapError((cause) => errorOf(cause, "InvalidState")));
36
+ const namespace = Array.from(random, (byte) => byte.toString(16).padStart(2, "0")).join("");
37
+ const commands = yield* Semaphore.make(1);
38
+ const closeGate = yield* Semaphore.make(1);
39
+ const affinity = yield* Sequence.makeAffinity().pipe(Effect.mapError((cause) => errorOf(cause, "InvalidInput")));
40
+ const events = new Observations();
41
+ const buffer = yield* MediaBuffer.make;
42
+ const fatal = yield* Deferred.make();
43
+ const slots = new Map();
44
+ const cleanups = [];
45
+ const seenCleanups = new Set();
46
+ const maxSessions = options.maxSessions ?? 64;
47
+ const reconnectTimeout = options.reconnectTimeoutMs ?? 10_000;
48
+ const leadSeconds = options.leadSeconds ?? 30;
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
+ }
58
+ let current;
59
+ let replacement = { _tag: "Absent" };
60
+ let opened = 0;
61
+ let openFailures = 0;
62
+ let retryAt = 0;
63
+ let autoplay = true;
64
+ let closing = false;
65
+ let finalReport;
66
+ let mediaState = { _tag: "Closed" };
67
+ let terminalFailure;
68
+ let submissionSequence = 0n;
69
+ const observe = (event) => options.onRenewal?.(event) ?? Effect.void;
70
+ const log = (message) => Effect.sync(() => options.log?.(message));
71
+ const emit = (event) => events.emit(event, 256);
72
+ const fail = (cause) => Effect.suspend(() => {
73
+ if (terminalFailure !== undefined)
74
+ return Effect.void;
75
+ terminalFailure = cause;
76
+ // Publish the failed state and queues before waking a failure waiter.
77
+ mediaState = { _tag: "Failed", cause };
78
+ emit({ _tag: "SessionFailed", failure: cause });
79
+ buffer.fail(cause);
80
+ Deferred.doneUnsafe(fatal, Effect.succeed(cause));
81
+ return observe({ _tag: "Failed", reason: cause.message });
82
+ });
83
+ // Every asynchronous activation uses this final fence. Closed/failed state
84
+ // cannot be replaced by a reconnect or autoplay operation that finished late.
85
+ const publishReady = (slot) => {
86
+ if (closing || slot.closed || slot !== current || terminalFailure !== undefined)
87
+ return false;
88
+ mediaState = { _tag: "Ready", sessionId: slot.source.id, generation: slot.media.generation };
89
+ return true;
90
+ };
91
+ const recordCleanup = (cleanup) => {
92
+ if (seenCleanups.has(cleanup.lease))
93
+ return;
94
+ seenCleanups.add(cleanup.lease);
95
+ cleanups.push(cleanup);
96
+ };
97
+ const openSequences = (slot) => affinity.snapshots.pipe(Effect.map((entries) => entries.some((entry) => entry.owner === slot.source.id && entry.status === "open")));
98
+ const expired = (slot) => slot.expired();
99
+ const recoveryBudget = (slot) => slot.recoveryBudget();
100
+ const retired = (slot) => slot.retired(buffer.forwarded);
101
+ const closeSlot = (slot) => slot.close;
102
+ const replace = (slot, cause) => Effect.gen(function* () {
103
+ if (slot.closed || closing || terminalFailure !== undefined)
104
+ return;
105
+ const state = yield* slot.source.state;
106
+ const lost = activeIds(state);
107
+ const tail = yield* retired(slot);
108
+ for (const clipId of lost)
109
+ emit({
110
+ _tag: "Failed",
111
+ clipId,
112
+ reason: `Session lost: ${cause.message}`,
113
+ sessionId: slot.source.id,
114
+ });
115
+ yield* closeSlot(slot);
116
+ yield* observe({
117
+ _tag: "Replaced",
118
+ reason: cause.message,
119
+ lostClips: lost.length,
120
+ ...tail,
121
+ });
122
+ if (replacement._tag === "Ready" && replacement.slot === slot)
123
+ replacement = { _tag: "Absent" };
124
+ if (slot !== current)
125
+ return;
126
+ const pending = replacement;
127
+ const selected = pending._tag === "Ready"
128
+ ? pending.slot
129
+ : yield* pending._tag === "Opening" ? Fiber.join(pending.fiber) : acquire;
130
+ replacement = { _tag: "Absent" };
131
+ if (closing || terminalFailure !== undefined) {
132
+ yield* selected.close;
133
+ return;
134
+ }
135
+ current = selected;
136
+ yield* current.source.setAutoplay(autoplay);
137
+ if (!publishReady(current))
138
+ return;
139
+ yield* log("Replaced lost session; local queue resumes on the new connection");
140
+ }).pipe(Effect.catch((failure) => fail(errorOf(failure, "Disconnected"))));
141
+ const recover = (slot, cause) => Effect.gen(function* () {
142
+ if (slot.closed || closing || terminalFailure !== undefined)
143
+ return;
144
+ if (slot === current)
145
+ mediaState = { _tag: "Recovering", sessionId: slot.source.id, cause };
146
+ yield* observe({ _tag: "Recovering", sessionId: slot.source.id, reason: cause.message });
147
+ yield* slot.joinCommitted(recoveryBudget(slot));
148
+ if (slot.closed || closing || terminalFailure !== undefined)
149
+ return;
150
+ if (slot.needsReplacement()) {
151
+ yield* replace(slot, cause);
152
+ return;
153
+ }
154
+ const before = activeIds(yield* slot.source.state);
155
+ // Read the retiring receiver before reconnect makes its counters unavailable.
156
+ const previousPressure = yield* Effect.result(slot.media.pressure.pipe(Effect.timeout(recoveryBudget(slot))));
157
+ if (expired(slot)) {
158
+ yield* replace(slot, new ReactorError("TerminalSession", "Source expired during recovery", {
159
+ operation: "renewal",
160
+ }));
161
+ return;
162
+ }
163
+ yield* slot.closeMedia;
164
+ if (slot.closed || closing || terminalFailure !== undefined)
165
+ return;
166
+ const connected = yield* Effect.result(slot.source.reconnect.pipe(Effect.timeoutOrElse({
167
+ duration: recoveryBudget(slot),
168
+ orElse: () => Effect.fail(new ReactorError("Disconnected", "Explicit source reconnect timed out")),
169
+ })));
170
+ if (slot.closed || closing || terminalFailure !== undefined)
171
+ return;
172
+ if (Result.isFailure(connected)) {
173
+ yield* replace(slot, connected.failure);
174
+ return;
175
+ }
176
+ if (slot.needsReplacement()) {
177
+ yield* replace(slot, cause);
178
+ return;
179
+ }
180
+ const after = new Set(activeIds(yield* slot.source.state));
181
+ for (const clipId of before)
182
+ if (!after.has(clipId))
183
+ emit({
184
+ _tag: "Failed",
185
+ clipId,
186
+ reason: "Clip left the provider's active queues during the observation gap",
187
+ sessionId: slot.source.id,
188
+ });
189
+ const restarted = yield* Effect.result(Effect.gen(function* () {
190
+ const media = yield* slot.source.media;
191
+ yield* slot.replaceMedia(media, previousPressure);
192
+ return yield* startMedia(slot);
193
+ }));
194
+ if (slot.closed || closing || terminalFailure !== undefined)
195
+ return;
196
+ if (Result.isFailure(restarted)) {
197
+ yield* replace(slot, restarted.failure);
198
+ return;
199
+ }
200
+ if (slot.needsReplacement() || !slot.recovered()) {
201
+ yield* replace(slot, cause);
202
+ return;
203
+ }
204
+ if (slot === current && !publishReady(slot))
205
+ return;
206
+ yield* observe({
207
+ _tag: "Reconnected",
208
+ sessionId: slot.source.id,
209
+ generation: slot.media.generation,
210
+ });
211
+ });
212
+ const scheduleRecovery = (slot, cause, mode) => Effect.gen(function* () {
213
+ if (slot.closed || closing || terminalFailure !== undefined)
214
+ return;
215
+ if (!slot.beginRecovery(mode))
216
+ return;
217
+ yield* recover(slot, cause).pipe(commands.withPermit, Effect.forkIn(scope));
218
+ });
219
+ const startMedia = (slot) => slot.startMedia({
220
+ video: (frame) => Effect.suspend(() => {
221
+ if (slot !== current || terminalFailure !== undefined)
222
+ return Effect.void;
223
+ slot.recordVideo();
224
+ return buffer.offerVideo(frame).pipe(Effect.catch(fail));
225
+ }),
226
+ audio: (frame) => Effect.suspend(() => {
227
+ if (slot !== current || terminalFailure !== undefined)
228
+ return Effect.void;
229
+ return buffer.offerAudio(frame).pipe(Effect.tap(() => Effect.sync(() => {
230
+ slot.recordAudio(frame.samples.length);
231
+ })), Effect.catch(fail));
232
+ }),
233
+ lost: (cause) => (closing ? Effect.void : scheduleRecovery(slot, cause, "reconnect")),
234
+ });
235
+ const acquire = Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
236
+ if (opened >= maxSessions)
237
+ return yield* Effect.fail(new ReactorError("Overflow", "Orchestration session and cleanup-history bound reached"));
238
+ const owned = yield* Scope.make();
239
+ let acquired;
240
+ let acquiredSource;
241
+ return yield* restore(Effect.gen(function* () {
242
+ // Replace the captured Scope explicitly in one context operation.
243
+ // Dependency provision order cannot move resources to the caller's owner.
244
+ const value = yield* options.open.pipe(Effect.provideContext(Context.add(context, Scope.Scope, owned)), Effect.timeoutOrElse({
245
+ duration: "30 seconds",
246
+ orElse: () => Effect.fail(new ReactorError("InvalidState", "Source acquisition timed out")),
247
+ }), Effect.mapError((cause) => errorOf(cause, "InvalidState")));
248
+ acquiredSource = value.source;
249
+ if (!(value.maxSeconds > 0) ||
250
+ (value.maxSeconds !== Infinity && !Number.isFinite(value.maxSeconds))) {
251
+ return yield* Effect.fail(new ReactorError("InvalidInput", "Source lifetime must be positive or infinite"));
252
+ }
253
+ if (slots.has(value.source.id))
254
+ return yield* Effect.fail(new ReactorError("InvalidInput", "Orchestration sources must have distinct session identities"));
255
+ const media = yield* value.source.media;
256
+ const slot = yield* SourceSlot.make({
257
+ source: value.source,
258
+ scope: owned,
259
+ media,
260
+ openedAt: clock.currentTimeMillisUnsafe(),
261
+ maxSeconds: value.maxSeconds,
262
+ cleanupBudgetMs: reconnectTimeout,
263
+ recordCleanup,
264
+ retireSequences: affinity.retire(value.source.id).pipe(Effect.asVoid),
265
+ });
266
+ acquired = slot;
267
+ slots.set(slot.source.id, slot);
268
+ opened++;
269
+ yield* slot.source.setAutoplay(false);
270
+ yield* slot.observe((event) => Effect.suspend(() => {
271
+ if (event._tag === "SessionFailed")
272
+ return scheduleRecovery(slot, event.failure, "replace");
273
+ if (event._tag !== "Starved" || slot === current)
274
+ emit(event);
275
+ return Effect.void;
276
+ }), (cause) => scheduleRecovery(slot, cause, "replace"));
277
+ yield* startMedia(slot);
278
+ yield* observe({
279
+ _tag: "Opened",
280
+ sessionId: slot.source.id,
281
+ maxSeconds: slot.maxSeconds,
282
+ });
283
+ return slot;
284
+ })).pipe(Effect.onExit((exit) => Exit.isSuccess(exit)
285
+ ? Effect.void
286
+ : Effect.gen(function* () {
287
+ if (acquired !== undefined)
288
+ yield* closeSlot(acquired);
289
+ else {
290
+ if (acquiredSource !== undefined)
291
+ recordCleanup(yield* acquiredSource.close);
292
+ yield* Scope.close(owned, exit);
293
+ if (Exit.isFailure(exit)) {
294
+ const failure = Cause.findErrorOption(exit.cause);
295
+ if (Option.isSome(failure) && failure.value instanceof AcquisitionFailure)
296
+ recordCleanup({ lease: failure.value.cleanup, policy: [] });
297
+ }
298
+ }
299
+ })));
300
+ }));
301
+ const close = closeGate.withPermit(Effect.uninterruptible(Effect.gen(function* () {
302
+ if (finalReport !== undefined)
303
+ return finalReport;
304
+ closing = true;
305
+ if (replacement._tag === "Opening")
306
+ yield* Fiber.interrupt(replacement.fiber);
307
+ yield* Effect.forEach([...slots.values()], closeSlot, { discard: true });
308
+ buffer.end();
309
+ events.end();
310
+ mediaState = { _tag: "Closed" };
311
+ finalReport = Object.freeze({ sessions: Object.freeze([...cleanups]) });
312
+ return finalReport;
313
+ })));
314
+ yield* Effect.addFinalizer(() => close);
315
+ current = yield* acquire.pipe(Effect.tap((slot) => slot.source.setAutoplay(true)), Effect.catch((cause) => close.pipe(Effect.flatMap((report) => {
316
+ const lease = report.sessions[0]?.lease;
317
+ return Effect.fail(lease === undefined ? cause : new AcquisitionFailure(cause, lease));
318
+ }))));
319
+ mediaState = {
320
+ _tag: "Ready",
321
+ sessionId: current.source.id,
322
+ generation: current.media.generation,
323
+ };
324
+ const guard = (operation, effect) => Effect.gen(function* () {
325
+ if (closing)
326
+ return yield* Effect.fail(new PolicyFailure("session_closed", "Orchestration is closed", operation));
327
+ if (yield* Deferred.isDone(fatal))
328
+ return yield* Effect.fail(new CommandFailure(yield* Deferred.await(fatal), {
329
+ operation,
330
+ outcome: "not-submitted",
331
+ }));
332
+ return yield* effect;
333
+ });
334
+ const route = (request) => Effect.gen(function* () {
335
+ const values = yield* Effect.suspend(() => Effect.forEach([...slots.values()], (slot) => Effect.map(slot.source.state, (state) => ({
336
+ owner: slot.source.id,
337
+ state,
338
+ accepted: slot.accepted,
339
+ closed: slot.closed,
340
+ recovering: slot.recovering,
341
+ }))));
342
+ const binding = request.sequence === undefined ? undefined : yield* affinity.get(request.sequence.id);
343
+ const next = replacement._tag === "Ready" ? replacement.slot : undefined;
344
+ const preferred = next !== undefined && current !== undefined && !(yield* openSequences(current))
345
+ ? next
346
+ : current;
347
+ if (preferred === undefined)
348
+ return yield* Effect.fail(new PolicyFailure("session_recovering", "No source is ready"));
349
+ return yield* resolve(request, values, preferred.source.id, binding);
350
+ });
351
+ const sequenceError = (error) => new PolicyFailure(`sequence_${error.reason}`, `Sequence ${error.sequenceId}: ${error.reason}`);
352
+ const prepare = (input) => Effect.gen(function* () {
353
+ const request = yield* captureRequest(input);
354
+ const id = `${namespace}:${++submissionSequence}`;
355
+ const gate = yield* Semaphore.make(1);
356
+ let active;
357
+ let activeOwner;
358
+ const submit = gate.withPermit(Effect.gen(function* () {
359
+ if (active === undefined) {
360
+ // A committed submission remains a readable outcome after the handle
361
+ // closes. Only selecting or committing fresh work needs a live owner.
362
+ yield* guard("enqueue", Effect.void);
363
+ const decision = yield* route(request).pipe(commands.withPermit);
364
+ const target = slots.get(decision.owner);
365
+ if (target === undefined)
366
+ return yield* Effect.fail(new PolicyFailure("session_retired", "Selected source was retired"));
367
+ const sequence = request.sequence;
368
+ active = yield* target.source.prepareRouted({ request, position: decision.position }, {
369
+ commit: (submissionId) => commands.withPermit(guard("enqueue", Effect.gen(function* () {
370
+ const checked = yield* route(request);
371
+ if (checked.owner !== decision.owner ||
372
+ checked.position !== decision.position) {
373
+ return yield* Effect.fail(new PolicyFailure("route_changed", "Request ownership or insertion position changed during preparation"));
374
+ }
375
+ if (active === undefined)
376
+ return yield* Effect.die(new Error("Source committed before returning its inert submission"));
377
+ yield* target.register(active, Effect.gen(function* () {
378
+ if (sequence !== undefined) {
379
+ yield* affinity
380
+ .bind(sequence.id, target.source.id)
381
+ .pipe(Effect.mapError(sequenceError));
382
+ yield* affinity
383
+ .begin(sequence.id, sequence.memberId ?? submissionId)
384
+ .pipe(Effect.mapError(sequenceError));
385
+ }
386
+ }));
387
+ }))),
388
+ result: (submissionId, result) => Effect.gen(function* () {
389
+ target.recordResult(result);
390
+ if (sequence !== undefined) {
391
+ const memberId = sequence.memberId ?? submissionId;
392
+ const account = Result.isSuccess(result)
393
+ ? affinity.accepted(sequence.id, memberId, result.success, sequence.final)
394
+ : result.failure.context.outcome === "unknown"
395
+ ? affinity.uncertain(sequence.id, memberId, result.failure.message)
396
+ : affinity.rejected(sequence.id, memberId, result.failure.message, sequence.final);
397
+ yield* account.pipe(Effect.catch((cause) => Effect.gen(function* () {
398
+ target.markIndeterminate();
399
+ yield* affinity.retire(target.source.id);
400
+ yield* fail(new ReactorError("InvalidState", "Committed sequence accounting failed", { detail: cause }));
401
+ })));
402
+ }
403
+ target.finishAccounting();
404
+ if (Result.isFailure(result) &&
405
+ result.failure.context.outcome === "unknown") {
406
+ yield* scheduleRecovery(target, result.failure, "replace");
407
+ }
408
+ }),
409
+ });
410
+ activeOwner = target;
411
+ }
412
+ const selected = active;
413
+ return yield* selected.submit.pipe(Effect.onExit(() => selected.state.pipe(Effect.flatMap((state) => Effect.sync(() => {
414
+ if (state._tag === "Completed")
415
+ activeOwner?.forgetCompleted(selected);
416
+ if (state._tag === "Prepared" && active === selected) {
417
+ active = undefined;
418
+ activeOwner = undefined;
419
+ }
420
+ })))));
421
+ }));
422
+ return {
423
+ id,
424
+ submit,
425
+ state: Effect.suspend(() => active === undefined
426
+ ? Effect.succeed({ _tag: "Prepared" })
427
+ : active.state),
428
+ };
429
+ });
430
+ const state = Effect.gen(function* () {
431
+ const live = [...slots.values()].filter((slot) => !slot.closed);
432
+ const values = yield* Effect.forEach(live, (slot) => slot.source.state);
433
+ const primary = values[live.indexOf(current)] ?? emptyState();
434
+ const builds = values.flatMap((value) => Option.isSome(value.building) ? [value.building.value] : []);
435
+ return Object.freeze({
436
+ ...primary,
437
+ availability: values.some((value) => value.availability === "Unavailable")
438
+ ? "Unavailable"
439
+ : values.some((value) => value.availability === "Synchronizing")
440
+ ? "Synchronizing"
441
+ : primary.availability,
442
+ queued: Object.freeze([
443
+ ...values.flatMap((value) => value.queued),
444
+ ...builds.slice(1).map((build) => build.record),
445
+ ]),
446
+ generationOrder: Object.freeze(values.flatMap((value) => value.generationOrder)),
447
+ building: Option.fromUndefinedOr(builds[0]),
448
+ ready: Object.freeze(values.flatMap((value) => value.ready)),
449
+ continuable: Object.freeze(values.flatMap((value) => value.continuable)),
450
+ failed: Object.freeze(values.flatMap((value) => value.failed)),
451
+ capacities: {
452
+ generation: Math.min(primary.capacities.generation, ...values.map((value) => value.capacities.generation)),
453
+ playout: Math.min(primary.capacities.playout, ...values.map((value) => value.capacities.playout)),
454
+ },
455
+ });
456
+ });
457
+ const owner = (id, operation) => Effect.gen(function* () {
458
+ const matches = yield* Effect.forEach([...slots.values()].filter((slot) => !slot.closed), (slot) => slot.source.state.pipe(Effect.map((state) => ({ slot, state }))));
459
+ const found = matches.filter(({ state }) => activeIds(state).includes(id));
460
+ if (found.length !== 1)
461
+ return yield* Effect.fail(new PolicyFailure(found.length === 0 ? "not_found" : "owner_conflict", "Clip has no unique active owning session", operation));
462
+ if (found[0].slot.recovering)
463
+ return yield* Effect.fail(new PolicyFailure("session_recovering", "Owning session is recovering", operation));
464
+ return found[0];
465
+ });
466
+ const engine = {
467
+ prepare,
468
+ enqueue: (request) => prepare(request).pipe(Effect.flatMap((submission) => submission.submit)),
469
+ state,
470
+ events: events.stream(),
471
+ failure: Deferred.await(fatal),
472
+ setAutoplay: (enabled) => commands.withPermit(guard("set_autoplay", Effect.gen(function* () {
473
+ autoplay = enabled;
474
+ if (current !== undefined && !current.closed)
475
+ yield* current.source.setAutoplay(enabled);
476
+ }))),
477
+ pauseAndStop: commands.withPermit(guard("pauseAndStop", Effect.gen(function* () {
478
+ autoplay = false;
479
+ for (const slot of slots.values())
480
+ if (!slot.closed) {
481
+ yield* slot.source.setAutoplay(false);
482
+ yield* slot.source.stop;
483
+ }
484
+ }))),
485
+ remove: (id) => commands.withPermit(guard("pop", owner(id, "pop").pipe(Effect.flatMap(({ slot }) => slot.source.remove(id))))),
486
+ move: (id, position, queue) => commands.withPermit(guard("move", Effect.gen(function* () {
487
+ if (!Number.isSafeInteger(position) || position < 0)
488
+ return yield* Effect.fail(new PolicyFailure("invalid_request", "Move position must be a nonnegative integer", "move"));
489
+ const selected = yield* owner(id, "move");
490
+ const own = queue === "generation" ? generation(selected.state) : selected.state.ready;
491
+ if (!own.some((clip) => clip.clipId === id))
492
+ return yield* Effect.fail(new PolicyFailure("queue_changed", "Clip is no longer in the selected application queue", "move"));
493
+ let preceding = 0;
494
+ for (const slot of slots.values()) {
495
+ if (slot === selected.slot)
496
+ break;
497
+ if (slot.closed)
498
+ continue;
499
+ const value = yield* slot.source.state;
500
+ preceding += queue === "generation" ? generation(value).length : value.ready.length;
501
+ }
502
+ yield* selected.slot.source.move(id, Math.max(0, position - preceding));
503
+ }))),
504
+ setCanvas: (canvas) => commands.withPermit(guard("set_canvas", Effect.gen(function* () {
505
+ if (!isIdle(yield* state))
506
+ return yield* Effect.fail(new PolicyFailure("busy", "Canvas can only change while every source is idle", "set_canvas"));
507
+ for (const slot of slots.values())
508
+ if (!slot.closed)
509
+ yield* slot.source.setCanvas(canvas);
510
+ }))),
511
+ };
512
+ const tick = commands
513
+ .withPermit(Effect.gen(function* () {
514
+ if (closing ||
515
+ current === undefined ||
516
+ current.closed ||
517
+ current.recovering ||
518
+ (yield* Deferred.isDone(fatal)))
519
+ return;
520
+ if (replacement._tag === "Opening") {
521
+ const result = replacement.fiber.pollUnsafe();
522
+ if (result !== undefined) {
523
+ replacement = { _tag: "Absent" };
524
+ if (Exit.isSuccess(result)) {
525
+ replacement = { _tag: "Ready", slot: result.value };
526
+ openFailures = 0;
527
+ yield* log("Prepared the next session for renewal");
528
+ yield* observe({ _tag: "Prepared" });
529
+ }
530
+ else {
531
+ openFailures++;
532
+ retryAt = clock.currentTimeMillisUnsafe() + 5000;
533
+ yield* observe({
534
+ _tag: "SetupFailed",
535
+ reason: "Could not prepare the next source",
536
+ consecutive: openFailures,
537
+ });
538
+ if (openFailures >= 3)
539
+ return yield* fail(new ReactorError("Disconnected", "Repeated source renewal acquisition failed"));
540
+ }
541
+ }
542
+ }
543
+ const decision = decideRenewal({
544
+ running: !closing && terminalFailure === undefined,
545
+ now: clock.currentTimeMillisUnsafe(),
546
+ current,
547
+ replacement: replacement._tag === "Ready" ? replacement.slot.phase : replacement._tag,
548
+ leadSeconds,
549
+ retryAt,
550
+ });
551
+ switch (decision) {
552
+ case "Retain":
553
+ return;
554
+ case "Expire":
555
+ yield* replace(current, new ReactorError("TerminalSession", "Source lifetime limit reached", {
556
+ operation: "renewal",
557
+ }));
558
+ return;
559
+ case "Prepare":
560
+ replacement = { _tag: "Opening", fiber: yield* acquire.pipe(Effect.forkIn(scope)) };
561
+ return;
562
+ case "InspectHandoff":
563
+ break;
564
+ }
565
+ const next = replacement._tag === "Ready" ? replacement.slot : undefined;
566
+ if (next === undefined || (yield* openSequences(current)))
567
+ return;
568
+ const oldState = yield* current.source.state;
569
+ const nextState = yield* next.source.state;
570
+ if (!isIdle(oldState) ||
571
+ nextState.availability !== "Ready" ||
572
+ nextState.ready.length === 0)
573
+ return;
574
+ const tail = yield* retired(current);
575
+ if (!canHandoff({
576
+ sequenceOpen: false,
577
+ currentIdle: isIdle(oldState),
578
+ replacementReady: nextState.availability === "Ready" && nextState.ready.length > 0,
579
+ video: tail.tail.video.status,
580
+ droppedVideo: tail.tail.sourceDrops.video,
581
+ droppedAudio: tail.tail.sourceDrops.audio,
582
+ }))
583
+ return;
584
+ const old = current;
585
+ current = next;
586
+ replacement = { _tag: "Absent" };
587
+ yield* current.source.setAutoplay(autoplay);
588
+ const activated = publishReady(current);
589
+ yield* closeSlot(old);
590
+ if (!activated)
591
+ return;
592
+ yield* observe({ _tag: "Switched", ...tail });
593
+ yield* log("Switched prepared sessions at a sequence boundary");
594
+ }))
595
+ .pipe(Effect.catch((cause) => fail(errorOf(cause, "Disconnected"))));
596
+ yield* Effect.forever(Effect.sleep(100).pipe(Effect.andThen(tick))).pipe(Effect.forkIn(scope));
597
+ const pressure = Effect.suspend(() => {
598
+ if (current === undefined)
599
+ return Effect.fail(new ReactorError("InvalidState", "No active media source"));
600
+ const owner = current;
601
+ return owner.pressure.pipe(Effect.map((source) => {
602
+ const queued = buffer.pressure();
603
+ return {
604
+ ...source,
605
+ closed: closing,
606
+ queuedVideo: source.queuedVideo + queued.queuedVideo,
607
+ queuedAudio: source.queuedAudio + queued.queuedAudio,
608
+ queuedBytes: source.queuedBytes + queued.queuedBytes,
609
+ };
610
+ }));
611
+ });
612
+ return {
613
+ engine,
614
+ media: {
615
+ video: buffer.video,
616
+ audio: buffer.audio,
617
+ pressure,
618
+ videoFramesPerSecond: current.media.videoFramesPerSecond,
619
+ },
620
+ mediaState: Effect.sync(() => mediaState),
621
+ sessionId: Effect.sync(() => current === undefined || current.closed ? Option.none() : Option.some(current.source.id)),
622
+ close,
623
+ cleanup: Effect.sync(() => finalReport === undefined ? Option.none() : Option.some(finalReport)),
624
+ sequences: {
625
+ get: affinity.get,
626
+ snapshots: affinity.snapshots,
627
+ seal: affinity.seal,
628
+ acknowledgeIndeterminate: affinity.acknowledgeIndeterminate,
629
+ release: affinity.release,
630
+ },
631
+ };
632
+ });
633
+ //# sourceMappingURL=renewal.js.map