effect-browser 0.2.0-beta.9 → 0.3.0-beta.11

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 (205) hide show
  1. package/README.md +861 -1161
  2. package/dist/Browser-B4qncxe7.mjs +628 -0
  3. package/dist/Browser-B4qncxe7.mjs.map +1 -0
  4. package/dist/Browser.d.mts +129 -3
  5. package/dist/Browser.mjs +2 -22
  6. package/dist/BrowserError-Coy7EW1N.d.mts +140 -0
  7. package/dist/BrowserError.d.mts +2 -0
  8. package/dist/BrowserError.mjs +211 -0
  9. package/dist/BrowserError.mjs.map +1 -0
  10. package/dist/BrowserEvent-BRu2juEz.d.mts +279 -0
  11. package/dist/BrowserEvent.d.mts +2 -0
  12. package/dist/BrowserEvent.mjs +297 -0
  13. package/dist/BrowserEvent.mjs.map +1 -0
  14. package/dist/Cdp.d.mts +32 -0
  15. package/dist/Cdp.mjs +61 -0
  16. package/dist/Cdp.mjs.map +1 -0
  17. package/dist/Change-DNvztNLu.mjs +138 -0
  18. package/dist/Change-DNvztNLu.mjs.map +1 -0
  19. package/dist/Change-iMlcuzVD.d.mts +71 -0
  20. package/dist/Change.d.mts +2 -0
  21. package/dist/Change.mjs +2 -0
  22. package/dist/Chromium.d.mts +29 -81
  23. package/dist/Chromium.mjs +57 -392
  24. package/dist/Chromium.mjs.map +1 -1
  25. package/dist/Expect.d.mts +62 -0
  26. package/dist/Expect.mjs +114 -0
  27. package/dist/Expect.mjs.map +1 -0
  28. package/dist/Frame-Di2PxyF7.d.mts +119 -0
  29. package/dist/Frame.d.mts +2 -0
  30. package/dist/Frame.mjs +125 -0
  31. package/dist/Frame.mjs.map +1 -0
  32. package/dist/Moment.d.mts +2 -0
  33. package/dist/Moment.mjs +288 -0
  34. package/dist/Moment.mjs.map +1 -0
  35. package/dist/Motion.d.mts +40 -0
  36. package/dist/Motion.mjs +122 -0
  37. package/dist/Motion.mjs.map +1 -0
  38. package/dist/Page-B5cjaUp5.mjs +618 -0
  39. package/dist/Page-B5cjaUp5.mjs.map +1 -0
  40. package/dist/Page-B7X6DQUn.d.mts +595 -0
  41. package/dist/Page.d.mts +2 -0
  42. package/dist/Page.mjs +2 -0
  43. package/dist/Plan-Akcac63g.d.mts +209 -0
  44. package/dist/Plan-D1wrJVs8.mjs +508 -0
  45. package/dist/Plan-D1wrJVs8.mjs.map +1 -0
  46. package/dist/Plan.d.mts +2 -3
  47. package/dist/Plan.mjs +2 -2
  48. package/dist/Policy.d.mts +76 -0
  49. package/dist/Policy.mjs +237 -0
  50. package/dist/Policy.mjs.map +1 -0
  51. package/dist/Presentation.d.mts +48 -0
  52. package/dist/Presentation.mjs +117 -0
  53. package/dist/Presentation.mjs.map +1 -0
  54. package/dist/Snapshot-B34hZ4Jl.d.mts +68 -0
  55. package/dist/Snapshot.d.mts +2 -0
  56. package/dist/Snapshot.mjs +79 -0
  57. package/dist/Snapshot.mjs.map +1 -0
  58. package/dist/Stage.d.mts +64 -0
  59. package/dist/Stage.mjs +237 -0
  60. package/dist/Stage.mjs.map +1 -0
  61. package/dist/Supervisor-CYWFK3vM.d.mts +280 -0
  62. package/dist/Supervisor-TNKNOVzu.mjs +599 -0
  63. package/dist/Supervisor-TNKNOVzu.mjs.map +1 -0
  64. package/dist/Supervisor.d.mts +2 -0
  65. package/dist/Supervisor.mjs +2 -0
  66. package/dist/context-B6xr8nVi.mjs +261 -0
  67. package/dist/context-B6xr8nVi.mjs.map +1 -0
  68. package/dist/index.d.mts +17 -14
  69. package/dist/index.mjs +18 -13
  70. package/dist/match.inpage-xOe9LNO0.mjs +48 -0
  71. package/dist/match.inpage-xOe9LNO0.mjs.map +1 -0
  72. package/dist/page-BAf__WKX.mjs +5075 -0
  73. package/dist/page-BAf__WKX.mjs.map +1 -0
  74. package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
  75. package/dist/url-Cd04Okid.mjs +264 -0
  76. package/dist/url-Cd04Okid.mjs.map +1 -0
  77. package/package.json +96 -61
  78. package/src/Browser.ts +672 -0
  79. package/src/BrowserError.ts +234 -0
  80. package/src/BrowserEvent.ts +296 -0
  81. package/src/Cdp.ts +101 -0
  82. package/src/Change.ts +88 -0
  83. package/src/Chromium.ts +102 -0
  84. package/src/Expect.ts +180 -0
  85. package/src/Frame.ts +132 -0
  86. package/src/Moment.ts +479 -0
  87. package/src/Motion.ts +166 -0
  88. package/src/Page.ts +522 -0
  89. package/src/Plan.ts +663 -0
  90. package/src/Policy.ts +325 -0
  91. package/src/Presentation.ts +195 -0
  92. package/src/Snapshot.ts +89 -0
  93. package/src/Stage.ts +467 -0
  94. package/src/Supervisor.ts +599 -0
  95. package/src/index.ts +20 -0
  96. package/src/internal/input/actions.ts +610 -0
  97. package/src/internal/input/dispatch.ts +161 -0
  98. package/src/internal/input/edit.inpage.ts +162 -0
  99. package/src/internal/input/evidence.inpage.ts +164 -0
  100. package/src/internal/input/gate.ts +224 -0
  101. package/src/internal/input/guard.inpage.ts +605 -0
  102. package/src/internal/input/guard.ts +184 -0
  103. package/src/internal/input/keyboard.ts +112 -0
  104. package/src/internal/input/keys.ts +127 -0
  105. package/src/internal/input/perform.ts +376 -0
  106. package/src/internal/input/pointer.ts +153 -0
  107. package/src/internal/input/replies.ts +195 -0
  108. package/src/internal/input/settle.ts +50 -0
  109. package/src/internal/input/style.ts +186 -0
  110. package/src/internal/input/targets.inpage.ts +293 -0
  111. package/src/internal/input/targets.ts +148 -0
  112. package/src/internal/page/bridge.ts +391 -0
  113. package/src/internal/page/context.ts +314 -0
  114. package/src/internal/page/identity.ts +35 -0
  115. package/src/internal/page/lane.ts +311 -0
  116. package/src/internal/page/navigation.ts +183 -0
  117. package/src/internal/page/openings.ts +87 -0
  118. package/src/internal/page/page.ts +165 -0
  119. package/src/internal/page/url.ts +100 -0
  120. package/src/internal/page/viewport.ts +43 -0
  121. package/src/internal/pictures/capture.ts +776 -0
  122. package/src/internal/pictures/clock.ts +141 -0
  123. package/src/internal/pictures/counts.ts +92 -0
  124. package/src/internal/pictures/pictures.ts +521 -0
  125. package/src/internal/pictures/transport.ts +243 -0
  126. package/src/internal/reading/choose.ts +100 -0
  127. package/src/internal/reading/context.inpage.ts +201 -0
  128. package/src/internal/reading/controls.inpage.ts +158 -0
  129. package/src/internal/reading/expect.inpage.ts +70 -0
  130. package/src/internal/reading/match.inpage.ts +92 -0
  131. package/src/internal/reading/names.inpage.ts +306 -0
  132. package/src/internal/reading/outline.inpage.ts +260 -0
  133. package/src/internal/reading/reading.ts +223 -0
  134. package/src/internal/reading/ready.inpage.ts +164 -0
  135. package/src/internal/reading/ready.ts +111 -0
  136. package/src/internal/reading/subjects.inpage.ts +188 -0
  137. package/src/internal/reading/text.inpage.ts +135 -0
  138. package/src/internal/reading/walk.inpage.ts +220 -0
  139. package/src/internal/supervisor/lifecycle.ts +416 -0
  140. package/src/internal/timeline/changes.inpage.ts +216 -0
  141. package/src/internal/timeline/changes.ts +100 -0
  142. package/src/internal/timeline/events.ts +94 -0
  143. package/src/internal/timeline/fold.inpage.ts +123 -0
  144. package/src/internal/timeline/history.inpage.ts +229 -0
  145. package/src/internal/timeline/marks.inpage.ts +207 -0
  146. package/src/internal/timeline/record.inpage.ts +133 -0
  147. package/src/internal/timeline/sight.inpage.ts +122 -0
  148. package/src/internal/timeline/window.ts +96 -0
  149. package/src/internal/timeline/words.ts +65 -0
  150. package/dist/Bootstrap-B9UUXsvF.d.mts +0 -183
  151. package/dist/Bootstrap-CeQOWBy6.mjs +0 -189
  152. package/dist/Bootstrap-CeQOWBy6.mjs.map +0 -1
  153. package/dist/Bootstrap.d.mts +0 -3
  154. package/dist/Bootstrap.mjs +0 -2
  155. package/dist/Browser-BF9_sORP.d.mts +0 -388
  156. package/dist/Browser.mjs.map +0 -1
  157. package/dist/BrowserData-CPIIIHol.d.mts +0 -585
  158. package/dist/BrowserData.d.mts +0 -3
  159. package/dist/BrowserData.mjs +0 -695
  160. package/dist/BrowserData.mjs.map +0 -1
  161. package/dist/BrowserRuntime-B6nT1G4X.d.mts +0 -148
  162. package/dist/BrowserRuntime-C1r2aSRK.mjs +0 -6820
  163. package/dist/BrowserRuntime-C1r2aSRK.mjs.map +0 -1
  164. package/dist/BrowserRuntime.d.mts +0 -3
  165. package/dist/BrowserRuntime.mjs +0 -3
  166. package/dist/Capture.d.mts +0 -56
  167. package/dist/Capture.mjs +0 -678
  168. package/dist/Capture.mjs.map +0 -1
  169. package/dist/CaptureData-CYriZHX1.mjs +0 -201
  170. package/dist/CaptureData-CYriZHX1.mjs.map +0 -1
  171. package/dist/CaptureData.d.mts +0 -219
  172. package/dist/CaptureData.mjs +0 -2
  173. package/dist/Errors.d.mts +0 -158
  174. package/dist/Errors.mjs +0 -207
  175. package/dist/Errors.mjs.map +0 -1
  176. package/dist/Images-yCG9BmRU.mjs +0 -76
  177. package/dist/Images-yCG9BmRU.mjs.map +0 -1
  178. package/dist/OperationOptions-DAeRS6O9.mjs +0 -55
  179. package/dist/OperationOptions-DAeRS6O9.mjs.map +0 -1
  180. package/dist/PageControl.d.mts +0 -15
  181. package/dist/PageControl.mjs +0 -34
  182. package/dist/PageControl.mjs.map +0 -1
  183. package/dist/PageRegistry-B3q9DS3g.mjs +0 -164
  184. package/dist/PageRegistry-B3q9DS3g.mjs.map +0 -1
  185. package/dist/Plan-Dyxu2q8C.mjs +0 -543
  186. package/dist/Plan-Dyxu2q8C.mjs.map +0 -1
  187. package/dist/PlanData-B668RxbQ.d.mts +0 -4831
  188. package/dist/PlanData.d.mts +0 -3
  189. package/dist/PlanData.mjs +0 -268
  190. package/dist/PlanData.mjs.map +0 -1
  191. package/dist/PlanInput-BJcPPncs.mjs +0 -75
  192. package/dist/PlanInput-BJcPPncs.mjs.map +0 -1
  193. package/dist/PublicSession-DCSMMqOj.mjs +0 -5602
  194. package/dist/PublicSession-DCSMMqOj.mjs.map +0 -1
  195. package/dist/Testing.d.mts +0 -479
  196. package/dist/Testing.mjs +0 -2147
  197. package/dist/Testing.mjs.map +0 -1
  198. package/dist/Timeline-fV22Xk-S.d.mts +0 -29
  199. package/dist/Timeline.d.mts +0 -4
  200. package/dist/Timeline.mjs +0 -20
  201. package/dist/Timeline.mjs.map +0 -1
  202. package/dist/TimelineData-BsOn-YGZ.d.mts +0 -3818
  203. package/dist/TimelineData.d.mts +0 -3
  204. package/dist/TimelineData.mjs +0 -482
  205. package/dist/TimelineData.mjs.map +0 -1
package/dist/Capture.mjs DELETED
@@ -1,678 +0,0 @@
1
- import { BrowserError, Reasons } from "./Errors.mjs";
2
- import { t as capturePageParent } from "./PageRegistry-B3q9DS3g.mjs";
3
- import { A as stream$1, O as span, k as start$1, n as checkedOperationOptions, t as checked } from "./PublicSession-DCSMMqOj.mjs";
4
- import { t as jpegGeometry } from "./Images-yCG9BmRU.mjs";
5
- import { a as CaptureSummary, c as CaptureLimits, i as CaptureSnapshot, l as CaptureMaxima, n as CaptureQualification, o as CapturedFrame, r as CaptureSize, s as CaptureDefaults, t as CaptureOptions } from "./CaptureData-CYriZHX1.mjs";
6
- import { Cause, Clock, Crypto, Deferred, Duration, Effect, Exit, Option, Queue, Schema, Stream } from "effect";
7
- //#region src/internal/capture/FrameBuffer.ts
8
- /** Callback-side bounded retention. No Promise, Effect, blocked producer, or per-frame fiber. */
9
- var FrameBuffer = class {
10
- maximumFrames;
11
- maximumBytes;
12
- items = [];
13
- retained = 0;
14
- dropped = 0;
15
- received = 0;
16
- highWaterBytes = 0;
17
- highWaterFrames = 0;
18
- constructor(maximumFrames, maximumBytes) {
19
- if (!Number.isSafeInteger(maximumFrames) || maximumFrames < 1 || maximumFrames > CaptureMaxima.frames || !Number.isSafeInteger(maximumBytes) || maximumBytes < 1 || maximumBytes > 67108864) throw new RangeError("Invalid capture retention limits");
20
- this.maximumFrames = maximumFrames;
21
- this.maximumBytes = maximumBytes;
22
- }
23
- offer(frame) {
24
- this.received++;
25
- if (frame.bytes.byteLength > this.maximumBytes) {
26
- this.dropped++;
27
- return false;
28
- }
29
- while (this.items.length >= this.maximumFrames || this.retained + frame.bytes.byteLength > this.maximumBytes) {
30
- const removed = this.items.shift();
31
- if (removed === void 0) break;
32
- this.retained -= removed.bytes.byteLength;
33
- this.dropped++;
34
- }
35
- this.items.push(frame);
36
- this.retained += frame.bytes.byteLength;
37
- this.highWaterBytes = Math.max(this.highWaterBytes, this.retained);
38
- this.highWaterFrames = Math.max(this.highWaterFrames, this.items.length);
39
- return true;
40
- }
41
- take() {
42
- const frame = this.items.shift();
43
- if (frame !== void 0) this.retained -= frame.bytes.byteLength;
44
- return frame;
45
- }
46
- clear() {
47
- this.items.length = 0;
48
- this.retained = 0;
49
- }
50
- get size() {
51
- return this.items.length;
52
- }
53
- get bytes() {
54
- return this.retained;
55
- }
56
- };
57
- //#endregion
58
- //#region src/internal/capture/Capture.ts
59
- const MaxParentCaptures = 4;
60
- const MaxParentBufferedBytes = 67108864;
61
- const MaxDocumentBoundaries = 64;
62
- const MaxDocumentUrlLength = 8192;
63
- /** An over-long address is recorded as null rather than cut into one that was never shown. */
64
- const documentUrl = (url) => typeof url === "string" && url.length <= MaxDocumentUrlLength ? url : null;
65
- /**
66
- * Chromium stamps a screencast frame on its UI thread, encodes it on an unsequenced thread
67
- * pool, and emits it when that encode completes. It admits a new frame while at most two are
68
- * unacknowledged (`kMaxScreencastFramesInFlight`), so up to three encode at once and two
69
- * frames stamped close together can complete in either order. Every frame stamped before an
70
- * accepted one was already in flight when that one was stamped, so at most two late frames
71
- * can arrive in a row. A longer run is source time that really went backwards.
72
- */
73
- const MaxConsecutiveLateFrames = 2;
74
- const Metadata = Schema.Struct({
75
- timestamp: Schema.Finite.check(Schema.isGreaterThan(0)),
76
- viewportWidth: Schema.Int.check(Schema.isBetween({
77
- minimum: 1,
78
- maximum: 16384
79
- })),
80
- viewportHeight: Schema.Int.check(Schema.isBetween({
81
- minimum: 1,
82
- maximum: 16384
83
- }))
84
- });
85
- const decodeMetadata = Schema.decodeUnknownOption(Metadata);
86
- /** Private seam for deterministic callback/lifetime tests. The public start accepts a real live session. */
87
- const acquireCapture = Effect.fnUntraced(function* (parent, options = {}) {
88
- const maxFrames = options.maxFrames ?? CaptureDefaults.maxFrames;
89
- const maxBytes = options.maxBufferedBytes ?? CaptureDefaults.maxBufferedBytes;
90
- const maxFrameBytes = options.maxFrameBytes ?? CaptureDefaults.maxFrameBytes;
91
- const duration = options.maxDurationMillis ?? CaptureDefaults.maxDurationMillis;
92
- const quality = options.quality ?? CaptureDefaults.quality;
93
- const size = options.size === void 0 ? void 0 : yield* Schema.decodeEffect(CaptureSize)(options.size, { onExcessProperty: "error" }).pipe(Effect.map(({ width, height }) => Object.freeze({
94
- width,
95
- height
96
- })), Effect.mapError(() => BrowserError.make({
97
- operation: "capture",
98
- reason: Reasons.Configuration.make({}),
99
- outcome: "undispatched"
100
- })));
101
- yield* Schema.decodeEffect(CaptureLimits)({
102
- maxFrames,
103
- maxBufferedBytes: maxBytes,
104
- maxFrameBytes,
105
- maxDurationMillis: duration,
106
- quality
107
- }).pipe(Effect.mapError(() => BrowserError.make({
108
- operation: "capture",
109
- reason: Reasons.Configuration.make({}),
110
- outcome: "undispatched"
111
- })));
112
- let interval;
113
- yield* Effect.addFinalizer((exit) => Effect.sync(() => interval?.end(exit)));
114
- const clock = parent.owner.clock;
115
- const captureId = yield* parent.newCaptureId;
116
- const wake = yield* Queue.dropping(1);
117
- const finished = yield* Queue.dropping(1);
118
- const completed = yield* Deferred.make();
119
- yield* Effect.addFinalizer(() => Queue.shutdown(wake).pipe(Effect.andThen(Queue.shutdown(finished)), Effect.asVoid));
120
- const buffer = new FrameBuffer(maxFrames, maxBytes);
121
- let source;
122
- let startPromise;
123
- let startSettled = true;
124
- let target;
125
- let leaseKey;
126
- let targetStatus;
127
- let metadata;
128
- let terminalAuthority;
129
- let ended = false, subscribed = false, cleanupFinished = false;
130
- let reason = "stopped";
131
- let error;
132
- let received = 0, delivered = 0, rejected = 0, duplicates = 0, late = 0, lateRun = 0;
133
- let first, last;
134
- let document = 0;
135
- let captureBoundary = 0;
136
- let acceptedBoundary = -1;
137
- let initialUrl = null;
138
- let initialUrlQualification = "Unread";
139
- let documentBoundariesTruncated = false;
140
- const documentBoundaries = [];
141
- /** A URL change is recorded; only a new document advances the frame attribution. */
142
- const nextDocument = (url, sameDocument = false) => {
143
- if (ended) return;
144
- captureBoundary++;
145
- if (!sameDocument) document++;
146
- if (documentBoundaries.length >= MaxDocumentBoundaries) {
147
- documentBoundaries.shift();
148
- documentBoundariesTruncated = true;
149
- }
150
- const boundary = {
151
- document,
152
- sameDocument,
153
- observedMonotonicNanos: clock.monotonicTimeNanosUnsafe(),
154
- afterSequence: received === 0 ? null : received - 1,
155
- url: documentUrl(url)
156
- };
157
- documentBoundaries.push(boundary);
158
- if (target !== void 0) metadata?.({
159
- _tag: "CaptureBoundary",
160
- captureId,
161
- target,
162
- captureBoundary,
163
- captureDocument: document,
164
- sameDocument: boundary.sameDocument,
165
- observedMonotonicNanos: boundary.observedMonotonicNanos,
166
- afterSequence: boundary.afterSequence,
167
- url: boundary.url,
168
- urlQualification: boundary.url === null ? "Omitted" : "NativeCached"
169
- });
170
- };
171
- let geometry;
172
- let nativeStop = "unconfirmed";
173
- let lease;
174
- let reservationReleased = false;
175
- let preStartStopConfirmed = false;
176
- let postStartStopStarted = false;
177
- const releaseReservation = () => {
178
- if (reservationReleased) return;
179
- reservationReleased = true;
180
- source?.release?.();
181
- if (leaseKey !== void 0 && lease !== void 0 && parent.captureLeases.get(leaseKey) === lease) {
182
- parent.captureLeases.delete(leaseKey);
183
- parent.captureReservedBytes = Math.max(0, parent.captureReservedBytes - lease.reservedBytes);
184
- }
185
- };
186
- let confirmPostStartStop = () => {};
187
- const observeStop = (stopping, afterStart) => {
188
- stopping.then(() => {
189
- if (afterStart) {
190
- nativeStop = "confirmed";
191
- releaseReservation();
192
- if (cleanupFinished) publishCapture("Stopped");
193
- } else {
194
- preStartStopConfirmed = true;
195
- confirmPostStartStop();
196
- }
197
- }, () => {});
198
- };
199
- confirmPostStartStop = () => {
200
- const stoppingSource = source;
201
- if (!preStartStopConfirmed || !startSettled || postStartStopStarted || stoppingSource === void 0 || leaseKey === void 0 || lease === void 0 || parent.captureLeases.get(leaseKey) !== lease) return;
202
- postStartStopStarted = true;
203
- observeStop(Promise.resolve().then(() => stoppingSource.stop()), true);
204
- };
205
- const finish = (why, failure) => {
206
- if (ended) return;
207
- ended = true;
208
- reason = why;
209
- error = failure;
210
- publishCapture("Ended");
211
- Queue.offerUnsafe(wake, void 0);
212
- Queue.offerUnsafe(finished, void 0);
213
- };
214
- const snapshot = () => {
215
- if (target === void 0) throw new Error("Capture target was not admitted");
216
- const status = targetStatus?.();
217
- const ownerPhase = parent.owner.state.phase;
218
- const ownerGeneration = parent.owner.state.generation;
219
- const sessionFenced = [
220
- "uncertain",
221
- "faulted",
222
- "closing",
223
- "closed"
224
- ].includes(ownerPhase);
225
- const containment = status !== void 0 && status.containment._tag !== "NotRequired" ? { ...status.containment } : sessionFenced ? {
226
- _tag: "SessionFenced",
227
- generation: ownerGeneration
228
- } : { _tag: "NotRequired" };
229
- const qualification = {
230
- authority: status?.phase === "closed" || terminalAuthority === "closed" ? "closed" : target.generation !== ownerGeneration ? "stale" : status?.phase ?? terminalAuthority ?? "open",
231
- containment,
232
- ownerPhase,
233
- ownerGeneration
234
- };
235
- return CaptureSummary.make({
236
- target,
237
- qualification,
238
- reason,
239
- received,
240
- delivered,
241
- discarded: buffer.dropped + rejected + late + duplicates,
242
- overflow: buffer.dropped,
243
- duplicates,
244
- late,
245
- rejected,
246
- peakBufferedFrames: buffer.highWaterFrames,
247
- peakBufferedBytes: buffer.highWaterBytes,
248
- bufferedFrames: buffer.size,
249
- bufferedBytes: buffer.bytes,
250
- sourceFirstMillis: first ?? null,
251
- sourceLastMillis: last ?? null,
252
- initialUrl,
253
- documentBoundaries: documentBoundaries.map((boundary) => ({ ...boundary })),
254
- documentBoundariesTruncated,
255
- nativeStop,
256
- upstreamDrops: "unknown",
257
- ...error === void 0 ? {} : { error: BrowserError.make({
258
- operation: error.operation,
259
- reason: { ...error.reason },
260
- outcome: error.outcome,
261
- ...error.containment !== void 0 ? { containment: { ...error.containment } } : containment._tag === "NotRequired" ? {} : { containment }
262
- }) }
263
- });
264
- };
265
- const publishCapture = (phase) => {
266
- if (metadata === void 0 || target === void 0) return;
267
- const facts = snapshot();
268
- metadata({
269
- _tag: "Capture",
270
- captureId,
271
- target,
272
- phase,
273
- latePhase: ended && (phase === "Watching" || phase === "Started"),
274
- observedMonotonicNanos: clock.monotonicTimeNanosUnsafe(),
275
- captureBoundary,
276
- captureDocument: document,
277
- qualification: facts.qualification,
278
- initialUrl,
279
- initialUrlQualification,
280
- reason: ended ? reason : null,
281
- nativeStop: phase === "Stopped" ? nativeStop : null,
282
- received,
283
- delivered,
284
- discarded: facts.discarded,
285
- overflow: facts.overflow,
286
- late,
287
- duplicates,
288
- rejected,
289
- upstreamDrops: "unknown"
290
- });
291
- };
292
- const stopNative = (yield* Effect.cached(Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
293
- finish("stopped");
294
- if (startPromise !== void 0 && !startSettled) {
295
- const starting = startPromise;
296
- yield* restore(Effect.tryPromise({
297
- try: () => starting,
298
- catch: () => BrowserError.make({
299
- operation: "capture-start",
300
- reason: Reasons.Provider.make({}),
301
- outcome: "unknown"
302
- })
303
- }).pipe(Effect.timeout(2e3), span("Browser.capture.start-settlement"), Effect.exit));
304
- }
305
- if (source !== void 0) {
306
- const stopping = source;
307
- const afterStart = startSettled;
308
- const stop = Promise.resolve().then(() => stopping.stop());
309
- observeStop(stop, afterStart);
310
- const stopped = yield* restore(Effect.tryPromise({
311
- try: () => stop,
312
- catch: () => BrowserError.make({
313
- operation: "capture-stop",
314
- reason: Reasons.Provider.make({}),
315
- outcome: "unknown"
316
- })
317
- }).pipe(Effect.timeout(3e3), span("Browser.capture.native-stop"))).pipe(Effect.exit);
318
- if (Exit.isSuccess(stopped) && afterStart) {
319
- nativeStop = "confirmed";
320
- releaseReservation();
321
- }
322
- } else nativeStop = "confirmed";
323
- if (nativeStop === "confirmed") releaseReservation();
324
- cleanupFinished = true;
325
- publishCapture("Stopped");
326
- interval?.end(error === void 0 ? Exit.void : Exit.fail(error), {
327
- "browser.capture.reason": reason,
328
- "browser.capture.native-stop": nativeStop,
329
- "browser.capture.received": received,
330
- "browser.capture.discarded": buffer.dropped + rejected + late + duplicates
331
- });
332
- return snapshot();
333
- }).pipe(span("Browser.capture.cleanup"))).pipe(Effect.provideService(Clock.Clock, clock)))).pipe(Effect.tap(() => Deferred.succeed(completed, void 0)));
334
- const receive = (frame) => {
335
- if (ended) return;
336
- if (target === void 0 || parent.owner.state.generation !== target.generation || parent.owner.state.phase !== "open") {
337
- finish("parent-unavailable", BrowserError.make({
338
- operation: "capture",
339
- reason: Reasons.Closed.make({}),
340
- outcome: "undispatched"
341
- }));
342
- return;
343
- }
344
- const sequence = received++;
345
- try {
346
- const decoded = decodeMetadata(frame);
347
- if (Option.isNone(decoded)) {
348
- rejected++;
349
- finish("malformed-frame", BrowserError.make({
350
- operation: "capture",
351
- reason: Reasons.Malformed.make({}),
352
- outcome: "undispatched"
353
- }));
354
- return;
355
- }
356
- const meta = decoded.value;
357
- if (!(frame.data instanceof Uint8Array)) {
358
- rejected++;
359
- finish("malformed-frame", BrowserError.make({
360
- operation: "capture",
361
- reason: Reasons.Malformed.make({ path: "bytes" }),
362
- outcome: "undispatched"
363
- }));
364
- return;
365
- }
366
- if (frame.data.length > maxFrameBytes) {
367
- rejected++;
368
- finish("frame-limit", BrowserError.make({
369
- operation: "capture",
370
- reason: Reasons.Limit.make({
371
- dimension: "frame-bytes",
372
- maximum: maxFrameBytes,
373
- observed: frame.data.length
374
- }),
375
- outcome: "undispatched"
376
- }));
377
- return;
378
- }
379
- if (last !== void 0 && meta.timestamp < last) {
380
- late++;
381
- if (++lateRun > MaxConsecutiveLateFrames) finish("timestamp-discontinuity", BrowserError.make({
382
- operation: "capture",
383
- reason: Reasons.Timestamp.make({}),
384
- outcome: "undispatched"
385
- }));
386
- return;
387
- }
388
- if (last === meta.timestamp) {
389
- duplicates++;
390
- return;
391
- }
392
- const dimensions = jpegGeometry(frame.data);
393
- if (frame.data[frame.data.length - 2] !== 255 || frame.data[frame.data.length - 1] !== 217) {
394
- rejected++;
395
- finish("malformed-frame", BrowserError.make({
396
- operation: "capture",
397
- reason: Reasons.Malformed.make({ path: "bytes" }),
398
- outcome: "undispatched"
399
- }));
400
- return;
401
- }
402
- const maximumWidth = Math.min(16384, size?.width ?? 16384);
403
- const maximumHeight = Math.min(16384, size?.height ?? 16384);
404
- const excessive = dimensions.width > maximumWidth ? Reasons.Limit.make({
405
- dimension: "width",
406
- maximum: maximumWidth,
407
- observed: dimensions.width
408
- }) : dimensions.height > maximumHeight ? Reasons.Limit.make({
409
- dimension: "height",
410
- maximum: maximumHeight,
411
- observed: dimensions.height
412
- }) : dimensions.width * dimensions.height > 33554432 ? Reasons.Limit.make({
413
- dimension: "pixels",
414
- maximum: 33554432,
415
- observed: dimensions.width * dimensions.height
416
- }) : void 0;
417
- if (excessive !== void 0) {
418
- rejected++;
419
- finish("frame-limit", BrowserError.make({
420
- operation: "capture",
421
- reason: excessive,
422
- outcome: "undispatched"
423
- }));
424
- return;
425
- }
426
- if (geometry !== void 0 && (geometry.width !== dimensions.width || geometry.height !== dimensions.height || geometry.viewportWidth !== meta.viewportWidth || geometry.viewportHeight !== meta.viewportHeight)) {
427
- rejected++;
428
- finish("resized", BrowserError.make({
429
- operation: "capture",
430
- reason: Reasons.Resized.make({}),
431
- outcome: "undispatched"
432
- }));
433
- return;
434
- }
435
- geometry ??= {
436
- ...dimensions,
437
- viewportWidth: meta.viewportWidth,
438
- viewportHeight: meta.viewportHeight
439
- };
440
- first ??= meta.timestamp;
441
- last = meta.timestamp;
442
- lateRun = 0;
443
- const receivedMonotonicNanos = clock.monotonicTimeNanosUnsafe();
444
- buffer.offer({
445
- bytes: new Uint8Array(frame.data),
446
- mediaType: "image/jpeg",
447
- target,
448
- sequence,
449
- document,
450
- sourceTimeMillis: meta.timestamp,
451
- sourceClock: "presentation-unix-millis",
452
- receivedMonotonicNanos,
453
- ...dimensions,
454
- viewportWidth: meta.viewportWidth,
455
- viewportHeight: meta.viewportHeight
456
- });
457
- if (captureBoundary !== acceptedBoundary) {
458
- acceptedBoundary = captureBoundary;
459
- metadata?.({
460
- _tag: "FirstFrame",
461
- captureId,
462
- target,
463
- captureBoundary,
464
- captureDocument: document,
465
- frameSequence: sequence,
466
- sourceTimeMillis: meta.timestamp,
467
- sourceClock: "presentation-unix-millis",
468
- receivedMonotonicNanos,
469
- ...dimensions,
470
- viewportWidth: meta.viewportWidth,
471
- viewportHeight: meta.viewportHeight
472
- });
473
- }
474
- Queue.offerUnsafe(wake, void 0);
475
- } catch {
476
- rejected++;
477
- finish("malformed-frame", BrowserError.make({
478
- operation: "capture",
479
- reason: Reasons.Malformed.make({}),
480
- outcome: "undispatched"
481
- }));
482
- }
483
- };
484
- yield* parent.validate("capture-start");
485
- const requested = parent.page;
486
- const generation = parent.owner.state.generation;
487
- yield* parent.owner.guard("capture-start", (ticket) => Effect.gen(function* () {
488
- const resolved = yield* parent.resolve(ticket, requested);
489
- target = Object.freeze(resolved.target);
490
- targetStatus = resolved.status;
491
- metadata = resolved.metadata;
492
- const resolvedSource = resolved.source;
493
- source = resolvedSource;
494
- leaseKey = resolved.key;
495
- if (parent.captureLeases.has(leaseKey)) return yield* BrowserError.make({
496
- operation: "capture",
497
- reason: Reasons.Busy.make({}),
498
- outcome: "undispatched"
499
- });
500
- if (parent.captureLeases.size >= MaxParentCaptures || parent.captureReservedBytes + maxBytes > MaxParentBufferedBytes) return yield* BrowserError.make({
501
- operation: "capture",
502
- reason: parent.captureLeases.size >= MaxParentCaptures ? Reasons.Limit.make({
503
- dimension: "captures",
504
- maximum: MaxParentCaptures,
505
- observed: parent.captureLeases.size + 1
506
- }) : Reasons.Limit.make({
507
- dimension: "buffered-bytes",
508
- maximum: MaxParentBufferedBytes,
509
- observed: parent.captureReservedBytes + maxBytes
510
- }),
511
- outcome: "undispatched"
512
- });
513
- yield* Effect.uninterruptible(start$1("Browser.capture.interval", { attributes: {
514
- "browser.operation": "capture",
515
- "browser.capture.lifetime": options.lifetime === "page" ? "page" : "document"
516
- } }).pipe(Effect.tap((started) => Effect.sync(() => {
517
- interval = started;
518
- }))));
519
- lease = {
520
- pageId: target.pageId,
521
- reservedBytes: maxBytes,
522
- stop: stopNative.pipe(Effect.asVoid),
523
- invalidate: (why) => {
524
- if (why === "target-closed") terminalAuthority = "closed";
525
- else if (why === "closed") terminalAuthority ??= "closing";
526
- if (why === "target-closed") releaseReservation();
527
- finish(why === "resized" ? "resized" : "target-changed", BrowserError.make({
528
- operation: "capture",
529
- reason: why === "resized" ? Reasons.Resized.make({}) : Reasons.TargetChanged.make({}),
530
- outcome: "undispatched"
531
- }));
532
- }
533
- };
534
- parent.captureLeases.set(leaseKey, lease);
535
- parent.captureReservedBytes += maxBytes;
536
- yield* Effect.addFinalizer(() => stopNative.pipe(Effect.andThen(Effect.sync(() => {
537
- rejected += buffer.size;
538
- buffer.clear();
539
- }))));
540
- publishCapture("Reserved");
541
- startSettled = false;
542
- yield* Effect.tryPromise({
543
- try: () => {
544
- try {
545
- startPromise = resolvedSource.start({
546
- receive,
547
- quality,
548
- invalidate: (why) => lease?.invalidate(why),
549
- opened: (url) => {
550
- initialUrl = documentUrl(url);
551
- initialUrlQualification = initialUrl === null ? "Omitted" : "NativeCached";
552
- publishCapture("Watching");
553
- },
554
- ...size === void 0 ? {} : { size },
555
- ...options.lifetime === "page" ? { document: nextDocument } : {}
556
- });
557
- } catch (error) {
558
- startSettled = true;
559
- throw error;
560
- }
561
- const cleanupAfterLateStart = () => {
562
- startSettled = true;
563
- confirmPostStartStop();
564
- };
565
- startPromise.then(cleanupAfterLateStart, cleanupAfterLateStart);
566
- return startPromise;
567
- },
568
- catch: () => BrowserError.make({
569
- operation: "capture-start",
570
- reason: Reasons.Provider.make({}),
571
- outcome: "unknown"
572
- })
573
- }).pipe(span("Browser.capture.native-start"));
574
- publishCapture("Started");
575
- ticket.check();
576
- }), {
577
- charge: false,
578
- admission: options.admission,
579
- targetScope: () => ({ pageId: requested.pageId }),
580
- preflight: parent.validate("capture-start").pipe(Effect.andThen(Effect.suspend(() => generation === parent.owner.state.generation ? Effect.void : Effect.fail(BrowserError.make({
581
- operation: "capture-start",
582
- reason: Reasons.Stale.make({}),
583
- outcome: "undispatched"
584
- })))))
585
- }).pipe(Effect.provideService(Clock.Clock, clock), Effect.onExit((exit) => Effect.sync(() => {
586
- if (Exit.isFailure(exit)) interval?.end(exit);
587
- })), Effect.onError(() => lease === void 0 ? Effect.void : stopNative.pipe(Effect.asVoid)), Effect.onInterrupt(() => lease === void 0 ? Effect.void : stopNative.pipe(Effect.asVoid)));
588
- yield* Effect.raceFirst(Queue.take(finished).pipe(Effect.as(false)), clock.sleep(Duration.millis(duration)).pipe(Effect.as(true))).pipe(Effect.tap((expired) => expired ? Effect.sync(() => finish("duration-limit")) : Effect.void), Effect.andThen(stopNative), Effect.forkScoped);
589
- const next = Effect.suspend(() => {
590
- const frame = buffer.take();
591
- if (frame !== void 0) {
592
- delivered++;
593
- return Effect.succeed(frame);
594
- }
595
- if (ended) return error === void 0 ? Cause.done() : Effect.fail(error);
596
- return Queue.take(wake).pipe(Effect.andThen(next));
597
- });
598
- return {
599
- id: captureId,
600
- frames: Stream.unwrap(Effect.suspend(() => {
601
- if (subscribed) return Effect.fail(BrowserError.make({
602
- operation: "capture-consume",
603
- reason: Reasons.Busy.make({}),
604
- outcome: "undispatched"
605
- }));
606
- subscribed = true;
607
- return Effect.succeed(Stream.fromEffectRepeat(next).pipe(Stream.ensuring(stopNative.pipe(Effect.asVoid))));
608
- })).pipe(stream$1("Browser.capture.consume")),
609
- snapshot: Effect.sync(() => {
610
- const { reason, nativeStop, ...metadata } = snapshot();
611
- return CaptureSnapshot.make({
612
- ...metadata,
613
- phase: cleanupFinished ? "stopped" : ended ? "stopping" : "capturing",
614
- observedMonotonicNanos: clock.monotonicTimeNanosUnsafe(),
615
- currentDocument: document,
616
- reason: ended ? reason : null,
617
- nativeStop: cleanupFinished ? nativeStop : null
618
- });
619
- }),
620
- stop: stopNative,
621
- completed: Deferred.await(completed).pipe(Effect.map(snapshot))
622
- };
623
- });
624
- const startCapture = (parent, options = {}) => acquireCapture(parent, options).pipe(span("Browser.capture.start", { attributes: { "browser.operation": "capture-start" } }));
625
- //#endregion
626
- //#region src/Capture.ts
627
- /**
628
- * Capture one remote page independently of the session's selected page.
629
- *
630
- * Requires an exact issued Page; copying an object or decoding a durable reference cannot
631
- * copy its capture authority. The private owner is not frame data.
632
- * `CapturedFrame` and `CaptureOptions` are Schema values as well as structural types. Frame
633
- * decoding checks binary/metadata fields, not the complete JPEG bitstream or target authority,
634
- * and does not copy bytes. Options decoding preserves omissions; admission applies defaults.
635
- * `CaptureInterval` remains a live scoped capability, not a schema or JSON/Tool value.
636
- */
637
- const start = (page, options = {}) => Effect.suspend(() => {
638
- const parent = capturePageParent(page);
639
- if (parent === void 0) return Effect.fail(BrowserError.make({
640
- operation: "capture",
641
- reason: Reasons.UnregisteredSession.make({}),
642
- outcome: "undispatched"
643
- }));
644
- return checked(CaptureOptions, options, "capture").pipe(Effect.flatMap(({ admission, ...capture }) => checkedOperationOptions(admission === void 0 ? {} : { admission }, "capture").pipe(Effect.flatMap((operationOptions) => startCapture(parent, {
645
- ...capture,
646
- ...operationOptions.admission === void 0 ? {} : { admission: operationOptions.admission }
647
- })))));
648
- });
649
- /**
650
- * A lazy, scoped frame stream. Each subscription acquires one interval and finalizes it when
651
- * consumption ends, fails or is interrupted; the browser remains owned by its caller.
652
- * Concurrent subscriptions on the same page are refused by the existing capture reservation.
653
- * Unconfirmed native cleanup retains the page reservation. Use start when the host needs
654
- * explicit stop acknowledgement, interval snapshots and the final capture summary.
655
- */
656
- const stream = (page, options = {}) => Stream.unwrap(start(page, options).pipe(Effect.map((interval) => interval.frames)));
657
- /**
658
- * Frames as motion JPEG for an `<img>`: one `multipart/x-mixed-replace` response, as WHATWG HTML
659
- * defines it for images. Each frame is followed at once by the next part's delimiter and headers,
660
- * because a browser shows a part only when it has read the headers of the one after it; without
661
- * that, a still page's last picture would never be shown. Parts carry no metadata, so nothing
662
- * but the pictures reaches a viewer. The boundary is drawn from `Crypto` for each response, so a
663
- * page cannot shape its pixels into it. Call it once per viewer.
664
- */
665
- const multipart = (frames) => Effect.map(Effect.flatMap(Crypto.Crypto, (crypto) => crypto.randomUUIDv4), (uuid) => {
666
- const boundary = `frame-${uuid.replaceAll("-", "")}`;
667
- const encoder = new TextEncoder();
668
- const opening = encoder.encode(`--${boundary}\r\nContent-Type: image/jpeg\r\n\r\n`);
669
- const next = encoder.encode(`\r\n--${boundary}\r\nContent-Type: image/jpeg\r\n\r\n`);
670
- return {
671
- contentType: `multipart/x-mixed-replace; boundary=${boundary}`,
672
- body: Stream.make(opening).pipe(Stream.concat(frames.pipe(Stream.flatMap((frame) => Stream.make(frame.bytes, next)))), Stream.concat(Stream.make(encoder.encode(`--${boundary}--\r\n`))))
673
- };
674
- });
675
- //#endregion
676
- export { CaptureOptions, CaptureQualification, CaptureSize, CaptureSnapshot, CaptureSummary, CapturedFrame, multipart, start, stream };
677
-
678
- //# sourceMappingURL=Capture.mjs.map