effect-browser 0.0.0-reserved.0 → 0.2.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +804 -3
- package/dist/Association-BsSfBYVx.mjs +138 -0
- package/dist/Association-BsSfBYVx.mjs.map +1 -0
- package/dist/Bootstrap-B9UUXsvF.d.mts +183 -0
- package/dist/Bootstrap-CeQOWBy6.mjs +189 -0
- package/dist/Bootstrap-CeQOWBy6.mjs.map +1 -0
- package/dist/Bootstrap.d.mts +3 -0
- package/dist/Bootstrap.mjs +2 -0
- package/dist/Browser.d.mts +189 -0
- package/dist/Browser.mjs +14 -0
- package/dist/Browser.mjs.map +1 -0
- package/dist/BrowserData-CRuxEZcn.d.mts +492 -0
- package/dist/BrowserData.d.mts +3 -0
- package/dist/BrowserData.mjs +572 -0
- package/dist/BrowserData.mjs.map +1 -0
- package/dist/BrowserRuntime-Coz-5DYb.d.mts +143 -0
- package/dist/BrowserRuntime-DViKNUdD.mjs +5782 -0
- package/dist/BrowserRuntime-DViKNUdD.mjs.map +1 -0
- package/dist/BrowserRuntime.d.mts +3 -0
- package/dist/BrowserRuntime.mjs +2 -0
- package/dist/Capture.d.mts +54 -0
- package/dist/Capture.mjs +512 -0
- package/dist/Capture.mjs.map +1 -0
- package/dist/CaptureData-B5-mXlyq.mjs +180 -0
- package/dist/CaptureData-B5-mXlyq.mjs.map +1 -0
- package/dist/CaptureData.d.mts +157 -0
- package/dist/CaptureData.mjs +2 -0
- package/dist/Chromium.d.mts +85 -0
- package/dist/Chromium.mjs +387 -0
- package/dist/Chromium.mjs.map +1 -0
- package/dist/Errors.d.mts +106 -0
- package/dist/Errors.mjs +156 -0
- package/dist/Errors.mjs.map +1 -0
- package/dist/PageControl.d.mts +15 -0
- package/dist/PageControl.mjs +28 -0
- package/dist/PageControl.mjs.map +1 -0
- package/dist/PageControlAssociation-DabEg5AR.mjs +10 -0
- package/dist/PageControlAssociation-DabEg5AR.mjs.map +1 -0
- package/dist/Testing.d.mts +437 -0
- package/dist/Testing.mjs +1561 -0
- package/dist/Testing.mjs.map +1 -0
- package/dist/index.d.mts +10 -0
- package/dist/index.mjs +9 -0
- package/package.json +71 -5
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { _ as ConnectionCleanup, a as DownloadEvent, b as noConnection, c as Lifetime, d as Runtime, f as RuntimeOptions, g as CleanupStep, h as playwright, i as Connection, l as PlaywrightOptions, m as make, n as Acquisition, o as FileRequest, p as Source, r as BrowserBinding, s as Integration, t as AcquireOptions, u as ResolvedFileSelection, v as ConnectionState, x as reported, y as cleanupStep } from "./BrowserRuntime-Coz-5DYb.mjs";
|
|
2
|
+
export { AcquireOptions, Acquisition, BrowserBinding, type CleanupStep, Connection, type ConnectionCleanup, type ConnectionState, DownloadEvent, FileRequest, Integration, Lifetime, PlaywrightOptions, ResolvedFileSelection, Runtime, RuntimeOptions, Source, cleanupStep, make, noConnection, playwright, reported };
|
|
3
|
+
export {};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { BrowserError } from "./Errors.mjs";
|
|
2
|
+
import { BrowserSession } from "./Browser.mjs";
|
|
3
|
+
import { CaptureOptions, CaptureSize, CaptureSnapshot, CaptureSummary, CapturedFrame } from "./CaptureData.mjs";
|
|
4
|
+
import { Crypto, Effect, PlatformError, Scope, Stream } from "effect";
|
|
5
|
+
//#region src/Capture.d.ts
|
|
6
|
+
/** Live stream/Effect capabilities intentionally have no data schema or serialization contract. */
|
|
7
|
+
export interface CaptureInterval {
|
|
8
|
+
/** Single subscription. Ending or interrupting it stops this interval, not its browser. */
|
|
9
|
+
readonly frames: Stream.Stream<CapturedFrame, BrowserError>;
|
|
10
|
+
/**
|
|
11
|
+
* A bounded copy of recorded metadata, available during capture and after cleanup. No native
|
|
12
|
+
* work, budget charge or subscription. Drain frames, then read `completed` for final accounting.
|
|
13
|
+
*/
|
|
14
|
+
readonly snapshot: Effect.Effect<CaptureSnapshot>;
|
|
15
|
+
readonly stop: Effect.Effect<CaptureSummary>;
|
|
16
|
+
readonly completed: Effect.Effect<CaptureSummary>;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Capture one remote page independently of the session's selected page.
|
|
20
|
+
*
|
|
21
|
+
* Requires the exact live session returned by the host; copying a session object or decoding
|
|
22
|
+
* a durable reference cannot copy its capture authority. The private owner is not frame data.
|
|
23
|
+
* `CapturedFrame` and `CaptureOptions` are Schema values as well as structural types. Frame
|
|
24
|
+
* decoding checks binary/metadata fields, not the complete JPEG bitstream or target authority,
|
|
25
|
+
* and does not copy bytes. Options decoding preserves omissions; admission applies defaults.
|
|
26
|
+
* `CaptureInterval` remains a live scoped capability, not a schema or JSON/Tool value.
|
|
27
|
+
*/
|
|
28
|
+
export declare const start: <E>(session: BrowserSession<E>, options?: CaptureOptions) => Effect.Effect<CaptureInterval, BrowserError, Scope.Scope>;
|
|
29
|
+
/**
|
|
30
|
+
* A lazy, scoped frame stream. Each subscription acquires one interval and finalizes it when
|
|
31
|
+
* consumption ends, fails or is interrupted; the browser remains owned by its caller.
|
|
32
|
+
* Concurrent subscriptions on the same page are refused by the existing capture reservation.
|
|
33
|
+
* Unconfirmed native cleanup retains the page reservation. Use start when the host needs
|
|
34
|
+
* explicit stop acknowledgement, interval snapshots and the final capture summary.
|
|
35
|
+
*/
|
|
36
|
+
export declare const stream: <E>(session: BrowserSession<E>, options?: CaptureOptions) => Stream.Stream<CapturedFrame, BrowserError>;
|
|
37
|
+
/** A `multipart/x-mixed-replace` body and the content type that names its boundary. */
|
|
38
|
+
export interface MultipartBody<E, R> {
|
|
39
|
+
readonly contentType: string;
|
|
40
|
+
readonly body: Stream.Stream<Uint8Array, E, R>;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Frames as motion JPEG for an `<img>`: one `multipart/x-mixed-replace` response, as WHATWG HTML
|
|
44
|
+
* defines it for images. Each frame is followed at once by the next part's delimiter and headers,
|
|
45
|
+
* because a browser shows a part only when it has read the headers of the one after it; without
|
|
46
|
+
* that, a still page's last picture would never be shown. Parts carry no metadata, so nothing
|
|
47
|
+
* but the pictures reaches a viewer. The boundary is drawn from `Crypto` for each response, so a
|
|
48
|
+
* page cannot shape its pixels into it. Call it once per viewer.
|
|
49
|
+
*/
|
|
50
|
+
export declare const multipart: <E, R>(frames: Stream.Stream<CapturedFrame, E, R>) => Effect.Effect<MultipartBody<E, R>, PlatformError.PlatformError, Crypto.Crypto>;
|
|
51
|
+
//#endregion
|
|
52
|
+
export { CaptureOptions, CaptureSize, CaptureSnapshot, CaptureSummary, CapturedFrame };
|
|
53
|
+
export {};
|
|
54
|
+
//# sourceMappingURL=Capture.d.mts.map
|
package/dist/Capture.mjs
ADDED
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
import { BrowserError, Reasons } from "./Errors.mjs";
|
|
2
|
+
import { PageInfo } from "./BrowserData.mjs";
|
|
3
|
+
import { n as captureParent, r as jpegGeometry } from "./Association-BsSfBYVx.mjs";
|
|
4
|
+
import { a as CapturedFrame, c as CaptureMaxima, i as CaptureSummary, n as CaptureSize, o as CaptureDefaults, r as CaptureSnapshot, s as CaptureLimits, t as CaptureOptions } from "./CaptureData-B5-mXlyq.mjs";
|
|
5
|
+
import { Cause, Clock, Crypto, Deferred, Effect, Exit, Option, Queue, Schema, Stream } from "effect";
|
|
6
|
+
//#region src/internal/capture/FrameBuffer.ts
|
|
7
|
+
/** Callback-side bounded retention. No Promise, Effect, blocked producer, or per-frame fiber. */
|
|
8
|
+
var FrameBuffer = class {
|
|
9
|
+
maximumFrames;
|
|
10
|
+
maximumBytes;
|
|
11
|
+
items = [];
|
|
12
|
+
retained = 0;
|
|
13
|
+
dropped = 0;
|
|
14
|
+
received = 0;
|
|
15
|
+
highWaterBytes = 0;
|
|
16
|
+
highWaterFrames = 0;
|
|
17
|
+
constructor(maximumFrames, maximumBytes) {
|
|
18
|
+
if (!Number.isSafeInteger(maximumFrames) || maximumFrames < 1 || maximumFrames > CaptureMaxima.frames || !Number.isSafeInteger(maximumBytes) || maximumBytes < 1 || maximumBytes > 67108864) throw new RangeError("Invalid capture retention limits");
|
|
19
|
+
this.maximumFrames = maximumFrames;
|
|
20
|
+
this.maximumBytes = maximumBytes;
|
|
21
|
+
}
|
|
22
|
+
offer(frame) {
|
|
23
|
+
this.received++;
|
|
24
|
+
if (frame.bytes.byteLength > this.maximumBytes) {
|
|
25
|
+
this.dropped++;
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
while (this.items.length >= this.maximumFrames || this.retained + frame.bytes.byteLength > this.maximumBytes) {
|
|
29
|
+
const removed = this.items.shift();
|
|
30
|
+
if (removed === void 0) break;
|
|
31
|
+
this.retained -= removed.bytes.byteLength;
|
|
32
|
+
this.dropped++;
|
|
33
|
+
}
|
|
34
|
+
this.items.push(frame);
|
|
35
|
+
this.retained += frame.bytes.byteLength;
|
|
36
|
+
this.highWaterBytes = Math.max(this.highWaterBytes, this.retained);
|
|
37
|
+
this.highWaterFrames = Math.max(this.highWaterFrames, this.items.length);
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
take() {
|
|
41
|
+
const frame = this.items.shift();
|
|
42
|
+
if (frame !== void 0) this.retained -= frame.bytes.byteLength;
|
|
43
|
+
return frame;
|
|
44
|
+
}
|
|
45
|
+
clear() {
|
|
46
|
+
this.items.length = 0;
|
|
47
|
+
this.retained = 0;
|
|
48
|
+
}
|
|
49
|
+
get size() {
|
|
50
|
+
return this.items.length;
|
|
51
|
+
}
|
|
52
|
+
get bytes() {
|
|
53
|
+
return this.retained;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
//#endregion
|
|
57
|
+
//#region src/internal/capture/Capture.ts
|
|
58
|
+
const MaxParentCaptures = 4;
|
|
59
|
+
const MaxParentBufferedBytes = 67108864;
|
|
60
|
+
const MaxDocumentBoundaries = 64;
|
|
61
|
+
const MaxDocumentUrlLength = 8192;
|
|
62
|
+
/** An over-long address is recorded as null rather than cut into one that was never shown. */
|
|
63
|
+
const documentUrl = (url) => typeof url === "string" && url.length <= MaxDocumentUrlLength ? url : null;
|
|
64
|
+
/**
|
|
65
|
+
* Chromium stamps a screencast frame on its UI thread, encodes it on an unsequenced thread
|
|
66
|
+
* pool, and emits it when that encode completes. It admits a new frame while at most two are
|
|
67
|
+
* unacknowledged (`kMaxScreencastFramesInFlight`), so up to three encode at once and two
|
|
68
|
+
* frames stamped close together can complete in either order. Every frame stamped before an
|
|
69
|
+
* accepted one was already in flight when that one was stamped, so at most two late frames
|
|
70
|
+
* can arrive in a row. A longer run is source time that really went backwards.
|
|
71
|
+
*/
|
|
72
|
+
const MaxConsecutiveLateFrames = 2;
|
|
73
|
+
const Metadata = Schema.Struct({
|
|
74
|
+
timestamp: Schema.Finite.check(Schema.isGreaterThan(0)),
|
|
75
|
+
viewportWidth: Schema.Int.check(Schema.isBetween({
|
|
76
|
+
minimum: 1,
|
|
77
|
+
maximum: 16384
|
|
78
|
+
})),
|
|
79
|
+
viewportHeight: Schema.Int.check(Schema.isBetween({
|
|
80
|
+
minimum: 1,
|
|
81
|
+
maximum: 16384
|
|
82
|
+
}))
|
|
83
|
+
});
|
|
84
|
+
const decodeMetadata = Schema.decodeUnknownOption(Metadata);
|
|
85
|
+
/** Private seam for deterministic callback/lifetime tests. The public start accepts a real live session. */
|
|
86
|
+
const startCapture = Effect.fnUntraced(function* (parent, options = {}) {
|
|
87
|
+
const maxFrames = options.maxFrames ?? CaptureDefaults.maxFrames;
|
|
88
|
+
const maxBytes = options.maxBufferedBytes ?? CaptureDefaults.maxBufferedBytes;
|
|
89
|
+
const maxFrameBytes = options.maxFrameBytes ?? CaptureDefaults.maxFrameBytes;
|
|
90
|
+
const duration = options.maxDurationMillis ?? CaptureDefaults.maxDurationMillis;
|
|
91
|
+
const quality = options.quality ?? CaptureDefaults.quality;
|
|
92
|
+
const size = options.size === void 0 ? void 0 : yield* Schema.decodeEffect(CaptureSize)(options.size, { onExcessProperty: "error" }).pipe(Effect.map(({ width, height }) => Object.freeze({
|
|
93
|
+
width,
|
|
94
|
+
height
|
|
95
|
+
})), Effect.mapError(() => BrowserError.make({
|
|
96
|
+
operation: "capture",
|
|
97
|
+
reason: Reasons.Configuration.make({}),
|
|
98
|
+
outcome: "undispatched"
|
|
99
|
+
})));
|
|
100
|
+
yield* Schema.decodeEffect(CaptureLimits)({
|
|
101
|
+
maxFrames,
|
|
102
|
+
maxBufferedBytes: maxBytes,
|
|
103
|
+
maxFrameBytes,
|
|
104
|
+
maxDurationMillis: duration,
|
|
105
|
+
quality
|
|
106
|
+
}).pipe(Effect.mapError(() => BrowserError.make({
|
|
107
|
+
operation: "capture",
|
|
108
|
+
reason: Reasons.Configuration.make({}),
|
|
109
|
+
outcome: "undispatched"
|
|
110
|
+
})));
|
|
111
|
+
const clock = yield* Clock.Clock;
|
|
112
|
+
const wake = yield* Queue.dropping(1);
|
|
113
|
+
const finished = yield* Queue.dropping(1);
|
|
114
|
+
const completed = yield* Deferred.make();
|
|
115
|
+
yield* Effect.addFinalizer(() => Queue.shutdown(wake).pipe(Effect.andThen(Queue.shutdown(finished)), Effect.asVoid));
|
|
116
|
+
const buffer = new FrameBuffer(maxFrames, maxBytes);
|
|
117
|
+
let source;
|
|
118
|
+
let startPromise;
|
|
119
|
+
let startSettled = true;
|
|
120
|
+
let target;
|
|
121
|
+
let leaseKey;
|
|
122
|
+
let ended = false, subscribed = false, cleanupFinished = false;
|
|
123
|
+
let reason = "stopped";
|
|
124
|
+
let error;
|
|
125
|
+
let received = 0, delivered = 0, rejected = 0, duplicates = 0, late = 0, lateRun = 0;
|
|
126
|
+
let first, last;
|
|
127
|
+
let document = 0;
|
|
128
|
+
let initialUrl = null;
|
|
129
|
+
let documentBoundariesTruncated = false;
|
|
130
|
+
const documentBoundaries = [];
|
|
131
|
+
/** The page navigated and its screencast kept running: later frames belong to a new document. */
|
|
132
|
+
const nextDocument = (url) => {
|
|
133
|
+
if (ended) return;
|
|
134
|
+
document++;
|
|
135
|
+
if (documentBoundaries.length >= MaxDocumentBoundaries) {
|
|
136
|
+
documentBoundaries.shift();
|
|
137
|
+
documentBoundariesTruncated = true;
|
|
138
|
+
}
|
|
139
|
+
documentBoundaries.push({
|
|
140
|
+
document,
|
|
141
|
+
observedMonotonicNanos: clock.monotonicTimeNanosUnsafe(),
|
|
142
|
+
afterSequence: received === 0 ? null : received - 1,
|
|
143
|
+
url: documentUrl(url)
|
|
144
|
+
});
|
|
145
|
+
};
|
|
146
|
+
let geometry;
|
|
147
|
+
let nativeStop = "unconfirmed";
|
|
148
|
+
let lease;
|
|
149
|
+
const finish = (why, failure) => {
|
|
150
|
+
if (ended) return;
|
|
151
|
+
ended = true;
|
|
152
|
+
reason = why;
|
|
153
|
+
error = failure;
|
|
154
|
+
Queue.offerUnsafe(wake, void 0);
|
|
155
|
+
Queue.offerUnsafe(finished, void 0);
|
|
156
|
+
};
|
|
157
|
+
const snapshot = () => {
|
|
158
|
+
if (target === void 0) throw new Error("Capture target was not admitted");
|
|
159
|
+
return CaptureSummary.make({
|
|
160
|
+
target,
|
|
161
|
+
reason,
|
|
162
|
+
received,
|
|
163
|
+
delivered,
|
|
164
|
+
discarded: buffer.dropped + rejected + late + duplicates,
|
|
165
|
+
overflow: buffer.dropped,
|
|
166
|
+
duplicates,
|
|
167
|
+
late,
|
|
168
|
+
rejected,
|
|
169
|
+
peakBufferedFrames: buffer.highWaterFrames,
|
|
170
|
+
peakBufferedBytes: buffer.highWaterBytes,
|
|
171
|
+
bufferedFrames: buffer.size,
|
|
172
|
+
bufferedBytes: buffer.bytes,
|
|
173
|
+
sourceFirstMillis: first ?? null,
|
|
174
|
+
sourceLastMillis: last ?? null,
|
|
175
|
+
initialUrl,
|
|
176
|
+
documentBoundaries: documentBoundaries.map((boundary) => ({ ...boundary })),
|
|
177
|
+
documentBoundariesTruncated,
|
|
178
|
+
nativeStop,
|
|
179
|
+
upstreamDrops: "unknown",
|
|
180
|
+
...error === void 0 ? {} : { error: BrowserError.make({
|
|
181
|
+
operation: error.operation,
|
|
182
|
+
reason: { ...error.reason },
|
|
183
|
+
outcome: error.outcome
|
|
184
|
+
}) }
|
|
185
|
+
});
|
|
186
|
+
};
|
|
187
|
+
const stopNative = (yield* Effect.cached(Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
|
|
188
|
+
finish("stopped");
|
|
189
|
+
if (startPromise !== void 0 && !startSettled) {
|
|
190
|
+
const starting = startPromise;
|
|
191
|
+
yield* restore(Effect.tryPromise({
|
|
192
|
+
try: () => starting,
|
|
193
|
+
catch: () => BrowserError.make({
|
|
194
|
+
operation: "capture-start",
|
|
195
|
+
reason: Reasons.Provider.make({}),
|
|
196
|
+
outcome: "unknown"
|
|
197
|
+
})
|
|
198
|
+
}).pipe(Effect.timeout(2e3), Effect.exit));
|
|
199
|
+
}
|
|
200
|
+
if (source !== void 0) {
|
|
201
|
+
const stopping = source;
|
|
202
|
+
const stopped = yield* restore(Effect.tryPromise({
|
|
203
|
+
try: () => stopping.stop(),
|
|
204
|
+
catch: () => BrowserError.make({
|
|
205
|
+
operation: "capture-stop",
|
|
206
|
+
reason: Reasons.Provider.make({}),
|
|
207
|
+
outcome: "unknown"
|
|
208
|
+
})
|
|
209
|
+
}).pipe(Effect.timeout(3e3))).pipe(Effect.exit);
|
|
210
|
+
if (Exit.isSuccess(stopped) && startSettled) nativeStop = "confirmed";
|
|
211
|
+
} else nativeStop = "confirmed";
|
|
212
|
+
if (nativeStop === "confirmed" && leaseKey !== void 0 && lease !== void 0 && parent.captureLeases.get(leaseKey) === lease) {
|
|
213
|
+
parent.captureLeases.delete(leaseKey);
|
|
214
|
+
parent.captureReservedBytes = Math.max(0, parent.captureReservedBytes - lease.reservedBytes);
|
|
215
|
+
}
|
|
216
|
+
cleanupFinished = true;
|
|
217
|
+
return snapshot();
|
|
218
|
+
})))).pipe(Effect.tap(() => Deferred.succeed(completed, void 0)));
|
|
219
|
+
const receive = (frame) => {
|
|
220
|
+
if (ended) return;
|
|
221
|
+
if (target === void 0 || parent.owner.state.generation !== target.generation || parent.owner.state.phase !== "open") {
|
|
222
|
+
finish("parent-unavailable", BrowserError.make({
|
|
223
|
+
operation: "capture",
|
|
224
|
+
reason: Reasons.Closed.make({}),
|
|
225
|
+
outcome: "undispatched"
|
|
226
|
+
}));
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const sequence = received++;
|
|
230
|
+
try {
|
|
231
|
+
const decoded = decodeMetadata(frame);
|
|
232
|
+
if (Option.isNone(decoded)) {
|
|
233
|
+
rejected++;
|
|
234
|
+
finish("malformed-frame", BrowserError.make({
|
|
235
|
+
operation: "capture",
|
|
236
|
+
reason: Reasons.Malformed.make({}),
|
|
237
|
+
outcome: "undispatched"
|
|
238
|
+
}));
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const meta = decoded.value;
|
|
242
|
+
if (!(frame.data instanceof Uint8Array)) {
|
|
243
|
+
rejected++;
|
|
244
|
+
finish("malformed-frame", BrowserError.make({
|
|
245
|
+
operation: "capture",
|
|
246
|
+
reason: Reasons.Malformed.make({ path: "bytes" }),
|
|
247
|
+
outcome: "undispatched"
|
|
248
|
+
}));
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (frame.data.length > maxFrameBytes) {
|
|
252
|
+
rejected++;
|
|
253
|
+
finish("frame-limit", BrowserError.make({
|
|
254
|
+
operation: "capture",
|
|
255
|
+
reason: Reasons.Limit.make({
|
|
256
|
+
dimension: "frame-bytes",
|
|
257
|
+
maximum: maxFrameBytes,
|
|
258
|
+
observed: frame.data.length
|
|
259
|
+
}),
|
|
260
|
+
outcome: "undispatched"
|
|
261
|
+
}));
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (last !== void 0 && meta.timestamp < last) {
|
|
265
|
+
late++;
|
|
266
|
+
if (++lateRun > MaxConsecutiveLateFrames) finish("timestamp-discontinuity", BrowserError.make({
|
|
267
|
+
operation: "capture",
|
|
268
|
+
reason: Reasons.Timestamp.make({}),
|
|
269
|
+
outcome: "undispatched"
|
|
270
|
+
}));
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
if (last === meta.timestamp) {
|
|
274
|
+
duplicates++;
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const dimensions = jpegGeometry(frame.data);
|
|
278
|
+
if (frame.data[frame.data.length - 2] !== 255 || frame.data[frame.data.length - 1] !== 217) {
|
|
279
|
+
rejected++;
|
|
280
|
+
finish("malformed-frame", BrowserError.make({
|
|
281
|
+
operation: "capture",
|
|
282
|
+
reason: Reasons.Malformed.make({ path: "bytes" }),
|
|
283
|
+
outcome: "undispatched"
|
|
284
|
+
}));
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const maximumWidth = Math.min(16384, size?.width ?? 16384);
|
|
288
|
+
const maximumHeight = Math.min(16384, size?.height ?? 16384);
|
|
289
|
+
const excessive = dimensions.width > maximumWidth ? Reasons.Limit.make({
|
|
290
|
+
dimension: "width",
|
|
291
|
+
maximum: maximumWidth,
|
|
292
|
+
observed: dimensions.width
|
|
293
|
+
}) : dimensions.height > maximumHeight ? Reasons.Limit.make({
|
|
294
|
+
dimension: "height",
|
|
295
|
+
maximum: maximumHeight,
|
|
296
|
+
observed: dimensions.height
|
|
297
|
+
}) : dimensions.width * dimensions.height > 33554432 ? Reasons.Limit.make({
|
|
298
|
+
dimension: "pixels",
|
|
299
|
+
maximum: 33554432,
|
|
300
|
+
observed: dimensions.width * dimensions.height
|
|
301
|
+
}) : void 0;
|
|
302
|
+
if (excessive !== void 0) {
|
|
303
|
+
rejected++;
|
|
304
|
+
finish("frame-limit", BrowserError.make({
|
|
305
|
+
operation: "capture",
|
|
306
|
+
reason: excessive,
|
|
307
|
+
outcome: "undispatched"
|
|
308
|
+
}));
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (geometry !== void 0 && (geometry.width !== dimensions.width || geometry.height !== dimensions.height || geometry.viewportWidth !== meta.viewportWidth || geometry.viewportHeight !== meta.viewportHeight)) {
|
|
312
|
+
rejected++;
|
|
313
|
+
finish("resized", BrowserError.make({
|
|
314
|
+
operation: "capture",
|
|
315
|
+
reason: Reasons.Resized.make({}),
|
|
316
|
+
outcome: "undispatched"
|
|
317
|
+
}));
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
geometry ??= {
|
|
321
|
+
...dimensions,
|
|
322
|
+
viewportWidth: meta.viewportWidth,
|
|
323
|
+
viewportHeight: meta.viewportHeight
|
|
324
|
+
};
|
|
325
|
+
first ??= meta.timestamp;
|
|
326
|
+
last = meta.timestamp;
|
|
327
|
+
lateRun = 0;
|
|
328
|
+
buffer.offer({
|
|
329
|
+
bytes: new Uint8Array(frame.data),
|
|
330
|
+
mediaType: "image/jpeg",
|
|
331
|
+
target,
|
|
332
|
+
sequence,
|
|
333
|
+
document,
|
|
334
|
+
sourceTimeMillis: meta.timestamp,
|
|
335
|
+
sourceClock: "presentation-unix-millis",
|
|
336
|
+
receivedMonotonicNanos: clock.monotonicTimeNanosUnsafe(),
|
|
337
|
+
...dimensions,
|
|
338
|
+
viewportWidth: meta.viewportWidth,
|
|
339
|
+
viewportHeight: meta.viewportHeight
|
|
340
|
+
});
|
|
341
|
+
Queue.offerUnsafe(wake, void 0);
|
|
342
|
+
} catch {
|
|
343
|
+
rejected++;
|
|
344
|
+
finish("malformed-frame", BrowserError.make({
|
|
345
|
+
operation: "capture",
|
|
346
|
+
reason: Reasons.Malformed.make({}),
|
|
347
|
+
outcome: "undispatched"
|
|
348
|
+
}));
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
yield* parent.owner.guard("capture-start", (ticket) => Effect.gen(function* () {
|
|
352
|
+
const resolved = yield* parent.resolve(ticket, options.target);
|
|
353
|
+
target = Object.freeze(resolved.target);
|
|
354
|
+
const resolvedSource = resolved.source;
|
|
355
|
+
source = resolvedSource;
|
|
356
|
+
leaseKey = resolved.key;
|
|
357
|
+
if (parent.captureLeases.has(leaseKey)) return yield* BrowserError.make({
|
|
358
|
+
operation: "capture",
|
|
359
|
+
reason: Reasons.Busy.make({}),
|
|
360
|
+
outcome: "undispatched"
|
|
361
|
+
});
|
|
362
|
+
if (parent.captureLeases.size >= MaxParentCaptures || parent.captureReservedBytes + maxBytes > MaxParentBufferedBytes) return yield* BrowserError.make({
|
|
363
|
+
operation: "capture",
|
|
364
|
+
reason: parent.captureLeases.size >= MaxParentCaptures ? Reasons.Limit.make({
|
|
365
|
+
dimension: "captures",
|
|
366
|
+
maximum: MaxParentCaptures,
|
|
367
|
+
observed: parent.captureLeases.size + 1
|
|
368
|
+
}) : Reasons.Limit.make({
|
|
369
|
+
dimension: "buffered-bytes",
|
|
370
|
+
maximum: MaxParentBufferedBytes,
|
|
371
|
+
observed: parent.captureReservedBytes + maxBytes
|
|
372
|
+
}),
|
|
373
|
+
outcome: "undispatched"
|
|
374
|
+
});
|
|
375
|
+
lease = {
|
|
376
|
+
reservedBytes: maxBytes,
|
|
377
|
+
stop: stopNative.pipe(Effect.asVoid),
|
|
378
|
+
invalidate: (why) => finish(why, BrowserError.make({
|
|
379
|
+
operation: "capture",
|
|
380
|
+
reason: why === "resized" ? Reasons.Resized.make({}) : Reasons.TargetChanged.make({}),
|
|
381
|
+
outcome: "undispatched"
|
|
382
|
+
}))
|
|
383
|
+
};
|
|
384
|
+
parent.captureLeases.set(leaseKey, lease);
|
|
385
|
+
parent.captureReservedBytes += maxBytes;
|
|
386
|
+
yield* Effect.addFinalizer(() => stopNative.pipe(Effect.andThen(Effect.sync(() => {
|
|
387
|
+
rejected += buffer.size;
|
|
388
|
+
buffer.clear();
|
|
389
|
+
}))));
|
|
390
|
+
startSettled = false;
|
|
391
|
+
yield* Effect.tryPromise({
|
|
392
|
+
try: () => {
|
|
393
|
+
startPromise = resolvedSource.start({
|
|
394
|
+
receive,
|
|
395
|
+
quality,
|
|
396
|
+
invalidate: (why) => lease?.invalidate(why),
|
|
397
|
+
opened: (url) => {
|
|
398
|
+
initialUrl = documentUrl(url);
|
|
399
|
+
},
|
|
400
|
+
...size === void 0 ? {} : { size },
|
|
401
|
+
...options.lifetime === "page" ? { document: nextDocument } : {}
|
|
402
|
+
});
|
|
403
|
+
startPromise.then(() => {
|
|
404
|
+
startSettled = true;
|
|
405
|
+
if (cleanupFinished && nativeStop === "unconfirmed" && leaseKey !== void 0 && parent.captureLeases.get(leaseKey) === lease) resolvedSource.stop().catch(() => {});
|
|
406
|
+
}, () => {
|
|
407
|
+
startSettled = true;
|
|
408
|
+
});
|
|
409
|
+
return startPromise;
|
|
410
|
+
},
|
|
411
|
+
catch: () => BrowserError.make({
|
|
412
|
+
operation: "capture-start",
|
|
413
|
+
reason: Reasons.Provider.make({}),
|
|
414
|
+
outcome: "unknown"
|
|
415
|
+
})
|
|
416
|
+
});
|
|
417
|
+
ticket.check();
|
|
418
|
+
}), { charge: false }).pipe(Effect.onError(() => lease === void 0 ? Effect.void : stopNative.pipe(Effect.asVoid)), Effect.onInterrupt(() => lease === void 0 ? Effect.void : stopNative.pipe(Effect.asVoid)));
|
|
419
|
+
yield* Effect.raceFirst(Queue.take(finished).pipe(Effect.as(false)), Effect.sleep(duration).pipe(Effect.as(true))).pipe(Effect.tap((expired) => expired ? Effect.sync(() => finish("duration-limit")) : Effect.void), Effect.andThen(stopNative), Effect.forkScoped);
|
|
420
|
+
const next = Effect.suspend(() => {
|
|
421
|
+
const frame = buffer.take();
|
|
422
|
+
if (frame !== void 0) {
|
|
423
|
+
delivered++;
|
|
424
|
+
return Effect.succeed(frame);
|
|
425
|
+
}
|
|
426
|
+
if (ended) return error === void 0 ? Cause.done() : Effect.fail(error);
|
|
427
|
+
return Queue.take(wake).pipe(Effect.andThen(next));
|
|
428
|
+
});
|
|
429
|
+
return {
|
|
430
|
+
frames: Stream.unwrap(Effect.suspend(() => {
|
|
431
|
+
if (subscribed) return Effect.fail(BrowserError.make({
|
|
432
|
+
operation: "capture-consume",
|
|
433
|
+
reason: Reasons.Busy.make({}),
|
|
434
|
+
outcome: "undispatched"
|
|
435
|
+
}));
|
|
436
|
+
subscribed = true;
|
|
437
|
+
return Effect.succeed(Stream.fromEffectRepeat(next).pipe(Stream.ensuring(stopNative.pipe(Effect.asVoid))));
|
|
438
|
+
})),
|
|
439
|
+
snapshot: Effect.sync(() => {
|
|
440
|
+
const { reason, nativeStop, ...metadata } = snapshot();
|
|
441
|
+
return CaptureSnapshot.make({
|
|
442
|
+
...metadata,
|
|
443
|
+
phase: cleanupFinished ? "stopped" : ended ? "stopping" : "capturing",
|
|
444
|
+
observedMonotonicNanos: clock.monotonicTimeNanosUnsafe(),
|
|
445
|
+
currentDocument: document,
|
|
446
|
+
reason: ended ? reason : null,
|
|
447
|
+
nativeStop: cleanupFinished ? nativeStop : null
|
|
448
|
+
});
|
|
449
|
+
}),
|
|
450
|
+
stop: stopNative,
|
|
451
|
+
completed: Deferred.await(completed).pipe(Effect.map(snapshot))
|
|
452
|
+
};
|
|
453
|
+
});
|
|
454
|
+
//#endregion
|
|
455
|
+
//#region src/Capture.ts
|
|
456
|
+
/**
|
|
457
|
+
* Capture one remote page independently of the session's selected page.
|
|
458
|
+
*
|
|
459
|
+
* Requires the exact live session returned by the host; copying a session object or decoding
|
|
460
|
+
* a durable reference cannot copy its capture authority. The private owner is not frame data.
|
|
461
|
+
* `CapturedFrame` and `CaptureOptions` are Schema values as well as structural types. Frame
|
|
462
|
+
* decoding checks binary/metadata fields, not the complete JPEG bitstream or target authority,
|
|
463
|
+
* and does not copy bytes. Options decoding preserves omissions; admission applies defaults.
|
|
464
|
+
* `CaptureInterval` remains a live scoped capability, not a schema or JSON/Tool value.
|
|
465
|
+
*/
|
|
466
|
+
const start = (session, options = {}) => Effect.suspend(() => {
|
|
467
|
+
const parent = captureParent(session);
|
|
468
|
+
if (parent === void 0) return Effect.fail(BrowserError.make({
|
|
469
|
+
operation: "capture",
|
|
470
|
+
reason: Reasons.UnregisteredSession.make({}),
|
|
471
|
+
outcome: "undispatched"
|
|
472
|
+
}));
|
|
473
|
+
if (options.target === void 0) return startCapture(parent, options);
|
|
474
|
+
return Schema.decodeEffect(PageInfo)(options.target, { onExcessProperty: "error" }).pipe(Effect.mapError(() => BrowserError.make({
|
|
475
|
+
operation: "capture",
|
|
476
|
+
reason: Reasons.Configuration.make({}),
|
|
477
|
+
outcome: "undispatched"
|
|
478
|
+
})), Effect.flatMap((target) => startCapture(parent, {
|
|
479
|
+
...options,
|
|
480
|
+
target
|
|
481
|
+
})));
|
|
482
|
+
});
|
|
483
|
+
/**
|
|
484
|
+
* A lazy, scoped frame stream. Each subscription acquires one interval and finalizes it when
|
|
485
|
+
* consumption ends, fails or is interrupted; the browser remains owned by its caller.
|
|
486
|
+
* Concurrent subscriptions on the same page are refused by the existing capture reservation.
|
|
487
|
+
* Unconfirmed native cleanup retains the page reservation. Use start when the host needs
|
|
488
|
+
* explicit stop acknowledgement, interval snapshots and the final capture summary.
|
|
489
|
+
*/
|
|
490
|
+
const stream = (session, options = {}) => Stream.unwrap(start(session, options).pipe(Effect.map((interval) => interval.frames)));
|
|
491
|
+
/**
|
|
492
|
+
* Frames as motion JPEG for an `<img>`: one `multipart/x-mixed-replace` response, as WHATWG HTML
|
|
493
|
+
* defines it for images. Each frame is followed at once by the next part's delimiter and headers,
|
|
494
|
+
* because a browser shows a part only when it has read the headers of the one after it; without
|
|
495
|
+
* that, a still page's last picture would never be shown. Parts carry no metadata, so nothing
|
|
496
|
+
* but the pictures reaches a viewer. The boundary is drawn from `Crypto` for each response, so a
|
|
497
|
+
* page cannot shape its pixels into it. Call it once per viewer.
|
|
498
|
+
*/
|
|
499
|
+
const multipart = (frames) => Effect.map(Effect.flatMap(Crypto.Crypto, (crypto) => crypto.randomUUIDv4), (uuid) => {
|
|
500
|
+
const boundary = `frame-${uuid.replaceAll("-", "")}`;
|
|
501
|
+
const encoder = new TextEncoder();
|
|
502
|
+
const opening = encoder.encode(`--${boundary}\r\nContent-Type: image/jpeg\r\n\r\n`);
|
|
503
|
+
const next = encoder.encode(`\r\n--${boundary}\r\nContent-Type: image/jpeg\r\n\r\n`);
|
|
504
|
+
return {
|
|
505
|
+
contentType: `multipart/x-mixed-replace; boundary=${boundary}`,
|
|
506
|
+
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`))))
|
|
507
|
+
};
|
|
508
|
+
});
|
|
509
|
+
//#endregion
|
|
510
|
+
export { CaptureOptions, CaptureSize, CaptureSnapshot, CaptureSummary, CapturedFrame, multipart, start, stream };
|
|
511
|
+
|
|
512
|
+
//# sourceMappingURL=Capture.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Capture.mjs","names":[],"sources":["../src/internal/capture/FrameBuffer.ts","../src/internal/capture/Capture.ts","../src/Capture.ts"],"sourcesContent":["import { CaptureMaxima } from \"./Options.ts\";\n\n/** Callback-side bounded retention. No Promise, Effect, blocked producer, or per-frame fiber. */\nexport class FrameBuffer<A extends { readonly bytes: Uint8Array }> {\n readonly maximumFrames: number;\n readonly maximumBytes: number;\n private readonly items: A[] = [];\n private retained = 0;\n dropped = 0;\n received = 0;\n highWaterBytes = 0;\n highWaterFrames = 0;\n\n constructor(maximumFrames: number, maximumBytes: number) {\n if (\n !Number.isSafeInteger(maximumFrames) ||\n maximumFrames < 1 ||\n maximumFrames > CaptureMaxima.frames ||\n !Number.isSafeInteger(maximumBytes) ||\n maximumBytes < 1 ||\n maximumBytes > 64 * 1024 * 1024\n ) {\n throw new RangeError(\"Invalid capture retention limits\");\n }\n this.maximumFrames = maximumFrames;\n this.maximumBytes = maximumBytes;\n }\n\n offer(frame: A): boolean {\n this.received++;\n if (frame.bytes.byteLength > this.maximumBytes) {\n this.dropped++;\n\n return false;\n }\n while (\n this.items.length >= this.maximumFrames ||\n this.retained + frame.bytes.byteLength > this.maximumBytes\n ) {\n const removed = this.items.shift();\n\n if (removed === undefined) break;\n this.retained -= removed.bytes.byteLength;\n this.dropped++;\n }\n this.items.push(frame);\n this.retained += frame.bytes.byteLength;\n this.highWaterBytes = Math.max(this.highWaterBytes, this.retained);\n this.highWaterFrames = Math.max(this.highWaterFrames, this.items.length);\n\n return true;\n }\n\n take(): A | undefined {\n const frame = this.items.shift();\n\n if (frame !== undefined) this.retained -= frame.bytes.byteLength;\n\n return frame;\n }\n\n clear(): void {\n this.items.length = 0;\n this.retained = 0;\n }\n\n get size(): number {\n return this.items.length;\n }\n get bytes(): number {\n return this.retained;\n }\n}\n","import { Cause, Clock, Deferred, Effect, Exit, Option, Queue, Schema, Stream } from \"effect\";\n\nimport type { Target } from \"../../BrowserData.ts\";\nimport type { CaptureInterval } from \"../../Capture.ts\";\nimport {\n CaptureSize,\n CaptureSnapshot,\n CaptureSummary,\n type CaptureOptions,\n type CapturedFrame,\n} from \"../../CaptureData.ts\";\nimport { BrowserError, Reasons } from \"../../Errors.ts\";\nimport { type CaptureLease, type CaptureParent } from \"../browser/Association.ts\";\nimport type { CaptureSource, NativeFrame } from \"../browser/Driver.ts\";\nimport { jpegGeometry } from \"../browser/Images.ts\";\nimport { FrameBuffer } from \"./FrameBuffer.ts\";\nimport { CaptureDefaults, CaptureLimits } from \"./Options.ts\";\n\nconst MaxParentCaptures = 4;\nconst MaxParentBufferedBytes = 64 * 1024 * 1024;\nconst MaxDocumentBoundaries = 64;\nconst MaxDocumentUrlLength = 8192;\n\n/** An over-long address is recorded as null rather than cut into one that was never shown. */\nconst documentUrl = (url: unknown): string | null =>\n typeof url === \"string\" && url.length <= MaxDocumentUrlLength ? url : null;\n\n/**\n * Chromium stamps a screencast frame on its UI thread, encodes it on an unsequenced thread\n * pool, and emits it when that encode completes. It admits a new frame while at most two are\n * unacknowledged (`kMaxScreencastFramesInFlight`), so up to three encode at once and two\n * frames stamped close together can complete in either order. Every frame stamped before an\n * accepted one was already in flight when that one was stamped, so at most two late frames\n * can arrive in a row. A longer run is source time that really went backwards.\n */\nconst MaxConsecutiveLateFrames = 2;\n\n// Synchronous Schema decoding at the callback boundary; no Effect/Fiber is allocated for each frame.\nconst Metadata = Schema.Struct({\n timestamp: Schema.Finite.check(Schema.isGreaterThan(0)),\n viewportWidth: Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 16384 })),\n viewportHeight: Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 16384 })),\n});\n\nconst decodeMetadata = Schema.decodeUnknownOption(Metadata);\n\n/** Private seam for deterministic callback/lifetime tests. The public start accepts a real live session. */\nexport const startCapture = Effect.fnUntraced(function* (\n parent: CaptureParent,\n options: CaptureOptions = {},\n) {\n const maxFrames = options.maxFrames ?? CaptureDefaults.maxFrames;\n const maxBytes = options.maxBufferedBytes ?? CaptureDefaults.maxBufferedBytes;\n const maxFrameBytes = options.maxFrameBytes ?? CaptureDefaults.maxFrameBytes;\n const duration = options.maxDurationMillis ?? CaptureDefaults.maxDurationMillis;\n const quality = options.quality ?? CaptureDefaults.quality;\n\n const size =\n options.size === undefined\n ? undefined\n : yield* Schema.decodeEffect(CaptureSize)(options.size, { onExcessProperty: \"error\" }).pipe(\n Effect.map(({ width, height }) => Object.freeze({ width, height })),\n Effect.mapError(() =>\n BrowserError.make({\n operation: \"capture\",\n reason: Reasons.Configuration.make({}),\n outcome: \"undispatched\",\n }),\n ),\n );\n\n yield* Schema.decodeEffect(CaptureLimits)({\n maxFrames,\n maxBufferedBytes: maxBytes,\n maxFrameBytes,\n maxDurationMillis: duration,\n quality,\n }).pipe(\n Effect.mapError(() =>\n BrowserError.make({\n operation: \"capture\",\n reason: Reasons.Configuration.make({}),\n outcome: \"undispatched\",\n }),\n ),\n );\n const clock = yield* Clock.Clock;\n const wake = yield* Queue.dropping<void>(1);\n const finished = yield* Queue.dropping<void>(1);\n const completed = yield* Deferred.make<void>();\n\n yield* Effect.addFinalizer(() =>\n Queue.shutdown(wake).pipe(Effect.andThen(Queue.shutdown(finished)), Effect.asVoid),\n );\n const buffer = new FrameBuffer<CapturedFrame>(maxFrames, maxBytes);\n let source: CaptureSource | undefined;\n let startPromise: Promise<void> | undefined;\n let startSettled = true;\n let target: Target | undefined;\n let leaseKey: string | undefined;\n\n let ended = false,\n subscribed = false,\n cleanupFinished = false;\n\n let reason = \"stopped\";\n let error: BrowserError | undefined;\n\n let received = 0,\n delivered = 0,\n rejected = 0,\n duplicates = 0,\n late = 0,\n lateRun = 0;\n\n let first: number | undefined, last: number | undefined;\n let document = 0;\n let initialUrl: string | null = null;\n let documentBoundariesTruncated = false;\n const documentBoundaries: Array<CaptureSummary[\"documentBoundaries\"][number]> = [];\n\n /** The page navigated and its screencast kept running: later frames belong to a new document. */\n const nextDocument = (url: string): void => {\n if (ended) return;\n document++;\n // The latest boundaries are kept: a live consumer needs the address of what it is showing now.\n if (documentBoundaries.length >= MaxDocumentBoundaries) {\n documentBoundaries.shift();\n documentBoundariesTruncated = true;\n }\n documentBoundaries.push({\n document,\n observedMonotonicNanos: clock.monotonicTimeNanosUnsafe(),\n afterSequence: received === 0 ? null : received - 1,\n url: documentUrl(url),\n });\n };\n\n let geometry:\n | { width: number; height: number; viewportWidth: number; viewportHeight: number }\n | undefined;\n\n let nativeStop: CaptureSummary[\"nativeStop\"] = \"unconfirmed\";\n let lease: CaptureLease | undefined;\n\n const finish = (why: string, failure?: BrowserError) => {\n if (ended) return;\n ended = true;\n reason = why;\n error = failure;\n Queue.offerUnsafe(wake, undefined);\n Queue.offerUnsafe(finished, undefined);\n };\n\n const snapshot = (): CaptureSummary => {\n if (target === undefined) throw new Error(\"Capture target was not admitted\");\n\n return CaptureSummary.make({\n target,\n reason,\n received,\n delivered,\n discarded: buffer.dropped + rejected + late + duplicates,\n overflow: buffer.dropped,\n duplicates,\n late,\n rejected,\n peakBufferedFrames: buffer.highWaterFrames,\n peakBufferedBytes: buffer.highWaterBytes,\n bufferedFrames: buffer.size,\n bufferedBytes: buffer.bytes,\n sourceFirstMillis: first ?? null,\n sourceLastMillis: last ?? null,\n initialUrl,\n documentBoundaries: documentBoundaries.map((boundary) => ({ ...boundary })),\n documentBoundariesTruncated,\n nativeStop,\n upstreamDrops: \"unknown\",\n ...(error === undefined\n ? {}\n : {\n error: BrowserError.make({\n operation: error.operation,\n reason: { ...error.reason },\n outcome: error.outcome,\n }),\n }),\n });\n };\n\n const performStop = yield* Effect.cached(\n Effect.uninterruptibleMask((restore) =>\n Effect.gen(function* () {\n finish(\"stopped\");\n // Wait for an in-flight start before stop. If it will not settle, quarantine this page lease.\n if (startPromise !== undefined && !startSettled) {\n const starting = startPromise;\n\n yield* restore(\n Effect.tryPromise({\n try: () => starting,\n catch: () =>\n BrowserError.make({\n operation: \"capture-start\",\n reason: Reasons.Provider.make({}),\n outcome: \"unknown\",\n }),\n }).pipe(Effect.timeout(2000), Effect.exit),\n );\n }\n if (source !== undefined) {\n const stopping = source;\n\n const stopped = yield* restore(\n Effect.tryPromise({\n try: () => stopping.stop(),\n catch: () =>\n BrowserError.make({\n operation: \"capture-stop\",\n reason: Reasons.Provider.make({}),\n outcome: \"unknown\",\n }),\n }).pipe(Effect.timeout(3000)),\n ).pipe(Effect.exit);\n\n if (Exit.isSuccess(stopped) && startSettled) nativeStop = \"confirmed\";\n } else nativeStop = \"confirmed\";\n // No older attachment may clear a replacement. Unconfirmed native cleanup quarantines only this page.\n if (\n nativeStop === \"confirmed\" &&\n leaseKey !== undefined &&\n lease !== undefined &&\n parent.captureLeases.get(leaseKey) === lease\n ) {\n parent.captureLeases.delete(leaseKey);\n parent.captureReservedBytes = Math.max(\n 0,\n parent.captureReservedBytes - lease.reservedBytes,\n );\n }\n cleanupFinished = true;\n\n return snapshot();\n }),\n ),\n );\n\n // Publish completion only after the cached cleanup Exit has settled. Publishing\n // inside it lets the caller close Scope and interrupt the caching fiber first.\n const stopNative = performStop.pipe(Effect.tap(() => Deferred.succeed(completed, undefined)));\n\n const receive = (frame: NativeFrame): void => {\n if (ended) return;\n if (\n target === undefined ||\n parent.owner.state.generation !== target.generation ||\n parent.owner.state.phase !== \"open\"\n ) {\n finish(\n \"parent-unavailable\",\n BrowserError.make({\n operation: \"capture\",\n reason: Reasons.Closed.make({}),\n outcome: \"undispatched\",\n }),\n );\n\n return;\n }\n const sequence = received++;\n\n try {\n const decoded = decodeMetadata(frame);\n\n if (Option.isNone(decoded)) {\n rejected++;\n finish(\n \"malformed-frame\",\n BrowserError.make({\n operation: \"capture\",\n reason: Reasons.Malformed.make({}),\n outcome: \"undispatched\",\n }),\n );\n\n return;\n }\n const meta = decoded.value;\n\n if (!(frame.data instanceof Uint8Array)) {\n rejected++;\n finish(\n \"malformed-frame\",\n BrowserError.make({\n operation: \"capture\",\n reason: Reasons.Malformed.make({ path: \"bytes\" }),\n outcome: \"undispatched\",\n }),\n );\n\n return;\n }\n if (frame.data.length > maxFrameBytes) {\n rejected++;\n finish(\n \"frame-limit\",\n BrowserError.make({\n operation: \"capture\",\n reason: Reasons.Limit.make({\n dimension: \"frame-bytes\",\n maximum: maxFrameBytes,\n observed: frame.data.length,\n }),\n outcome: \"undispatched\",\n }),\n );\n\n return;\n }\n if (last !== undefined && meta.timestamp < last) {\n // A newer frame is already accepted, so this one can no longer be presented in order.\n // It is discarded and counted rather than sorted in or given an invented time.\n late++;\n if (++lateRun > MaxConsecutiveLateFrames) {\n finish(\n \"timestamp-discontinuity\",\n BrowserError.make({\n operation: \"capture\",\n reason: Reasons.Timestamp.make({}),\n outcome: \"undispatched\",\n }),\n );\n }\n\n return;\n }\n if (last === meta.timestamp) {\n duplicates++;\n\n return;\n }\n const dimensions = jpegGeometry(frame.data);\n\n if (frame.data[frame.data.length - 2] !== 255 || frame.data[frame.data.length - 1] !== 217) {\n rejected++;\n finish(\n \"malformed-frame\",\n BrowserError.make({\n operation: \"capture\",\n reason: Reasons.Malformed.make({ path: \"bytes\" }),\n outcome: \"undispatched\",\n }),\n );\n\n return;\n }\n const maximumWidth = Math.min(16384, size?.width ?? 16384);\n const maximumHeight = Math.min(16384, size?.height ?? 16384);\n\n const excessive =\n dimensions.width > maximumWidth\n ? Reasons.Limit.make({\n dimension: \"width\",\n maximum: maximumWidth,\n observed: dimensions.width,\n })\n : dimensions.height > maximumHeight\n ? Reasons.Limit.make({\n dimension: \"height\",\n maximum: maximumHeight,\n observed: dimensions.height,\n })\n : dimensions.width * dimensions.height > 33_554_432\n ? Reasons.Limit.make({\n dimension: \"pixels\",\n maximum: 33_554_432,\n observed: dimensions.width * dimensions.height,\n })\n : undefined;\n\n if (excessive !== undefined) {\n rejected++;\n finish(\n \"frame-limit\",\n BrowserError.make({ operation: \"capture\", reason: excessive, outcome: \"undispatched\" }),\n );\n\n return;\n }\n if (\n geometry !== undefined &&\n (geometry.width !== dimensions.width ||\n geometry.height !== dimensions.height ||\n geometry.viewportWidth !== meta.viewportWidth ||\n geometry.viewportHeight !== meta.viewportHeight)\n ) {\n rejected++;\n finish(\n \"resized\",\n BrowserError.make({\n operation: \"capture\",\n reason: Reasons.Resized.make({}),\n outcome: \"undispatched\",\n }),\n );\n\n return;\n }\n geometry ??= {\n ...dimensions,\n viewportWidth: meta.viewportWidth,\n viewportHeight: meta.viewportHeight,\n };\n first ??= meta.timestamp;\n last = meta.timestamp;\n lateRun = 0;\n buffer.offer({\n bytes: new Uint8Array(frame.data),\n mediaType: \"image/jpeg\",\n target,\n sequence,\n document,\n sourceTimeMillis: meta.timestamp,\n sourceClock: \"presentation-unix-millis\",\n receivedMonotonicNanos: clock.monotonicTimeNanosUnsafe(),\n ...dimensions,\n viewportWidth: meta.viewportWidth,\n viewportHeight: meta.viewportHeight,\n });\n Queue.offerUnsafe(wake, undefined);\n } catch {\n rejected++;\n finish(\n \"malformed-frame\",\n BrowserError.make({\n operation: \"capture\",\n reason: Reasons.Malformed.make({}),\n outcome: \"undispatched\",\n }),\n );\n }\n };\n\n yield* parent.owner\n .guard(\n \"capture-start\",\n (ticket) =>\n Effect.gen(function* () {\n const resolved = yield* parent.resolve(ticket, options.target);\n\n // Frames and summaries share this identity with the generation guard. It must not\n // become writable through consumer-owned frame data.\n target = Object.freeze(resolved.target);\n const resolvedSource = resolved.source;\n\n source = resolvedSource;\n leaseKey = resolved.key;\n if (parent.captureLeases.has(leaseKey)) {\n return yield* BrowserError.make({\n operation: \"capture\",\n reason: Reasons.Busy.make({}),\n outcome: \"undispatched\",\n });\n }\n if (\n parent.captureLeases.size >= MaxParentCaptures ||\n parent.captureReservedBytes + maxBytes > MaxParentBufferedBytes\n ) {\n return yield* BrowserError.make({\n operation: \"capture\",\n reason:\n parent.captureLeases.size >= MaxParentCaptures\n ? Reasons.Limit.make({\n dimension: \"captures\",\n maximum: MaxParentCaptures,\n observed: parent.captureLeases.size + 1,\n })\n : Reasons.Limit.make({\n dimension: \"buffered-bytes\",\n maximum: MaxParentBufferedBytes,\n observed: parent.captureReservedBytes + maxBytes,\n }),\n outcome: \"undispatched\",\n });\n }\n lease = {\n reservedBytes: maxBytes,\n stop: stopNative.pipe(Effect.asVoid),\n invalidate: (why) =>\n finish(\n why,\n BrowserError.make({\n operation: \"capture\",\n reason:\n why === \"resized\" ? Reasons.Resized.make({}) : Reasons.TargetChanged.make({}),\n outcome: \"undispatched\",\n }),\n ),\n };\n parent.captureLeases.set(leaseKey, lease);\n parent.captureReservedBytes += maxBytes;\n // Installed before native acquisition. A cancelled capture cannot escape its caller's scope.\n yield* Effect.addFinalizer(() =>\n stopNative.pipe(\n Effect.andThen(\n Effect.sync(() => {\n rejected += buffer.size;\n buffer.clear();\n }),\n ),\n ),\n );\n startSettled = false;\n yield* Effect.tryPromise({\n try: () => {\n startPromise = resolvedSource.start({\n receive,\n quality,\n invalidate: (why) => lease?.invalidate(why),\n opened: (url) => {\n initialUrl = documentUrl(url);\n },\n ...(size === undefined ? {} : { size }),\n ...(options.lifetime === \"page\" ? { document: nextDocument } : {}),\n });\n void startPromise.then(\n () => {\n startSettled = true;\n if (\n cleanupFinished &&\n nativeStop === \"unconfirmed\" &&\n leaseKey !== undefined &&\n parent.captureLeases.get(leaseKey) === lease\n ) {\n // One late acquisition cleanup, not a callback-side worker. Keep the page quarantined.\n void resolvedSource.stop().catch(() => {});\n }\n },\n () => {\n startSettled = true;\n },\n );\n\n return startPromise;\n },\n catch: () =>\n BrowserError.make({\n operation: \"capture-start\",\n reason: Reasons.Provider.make({}),\n outcome: \"unknown\",\n }),\n });\n ticket.check();\n }),\n { charge: false },\n )\n .pipe(\n Effect.onError(() => (lease === undefined ? Effect.void : stopNative.pipe(Effect.asVoid))),\n Effect.onInterrupt(() =>\n lease === undefined ? Effect.void : stopNative.pipe(Effect.asVoid),\n ),\n );\n // One monitor, not a fiber per frame. The finalizer also calls stop directly if this monitor is interrupted.\n // The timeout and explicit-finish branches intentionally carry different\n // values before shared cleanup; preserve the proven first-completion semantics.\n // @effect-diagnostics-next-line raceFirstWithSleepToTimeout:off\n yield* Effect.raceFirst(\n Queue.take(finished).pipe(Effect.as(false)),\n Effect.sleep(duration).pipe(Effect.as(true)),\n ).pipe(\n Effect.tap((expired) => (expired ? Effect.sync(() => finish(\"duration-limit\")) : Effect.void)),\n Effect.andThen(stopNative),\n Effect.forkScoped,\n );\n\n const next: Effect.Effect<CapturedFrame, BrowserError | Cause.Done> = Effect.suspend(() => {\n const frame = buffer.take();\n\n if (frame !== undefined) {\n delivered++;\n\n return Effect.succeed(frame);\n }\n if (ended) return error === undefined ? Cause.done() : Effect.fail(error);\n\n return Queue.take(wake).pipe(Effect.andThen(next));\n });\n\n const frames = Stream.unwrap(\n Effect.suspend(() => {\n if (subscribed)\n return Effect.fail(\n BrowserError.make({\n operation: \"capture-consume\",\n reason: Reasons.Busy.make({}),\n outcome: \"undispatched\",\n }),\n );\n subscribed = true;\n\n return Effect.succeed(\n Stream.fromEffectRepeat(next).pipe(Stream.ensuring(stopNative.pipe(Effect.asVoid))),\n );\n }),\n );\n\n return {\n frames,\n snapshot: Effect.sync(() => {\n const { reason, nativeStop, ...metadata } = snapshot();\n\n return CaptureSnapshot.make({\n ...metadata,\n phase: cleanupFinished ? \"stopped\" : ended ? \"stopping\" : \"capturing\",\n observedMonotonicNanos: clock.monotonicTimeNanosUnsafe(),\n currentDocument: document,\n reason: ended ? reason : null,\n nativeStop: cleanupFinished ? nativeStop : null,\n });\n }),\n stop: stopNative,\n completed: Deferred.await(completed).pipe(Effect.map(snapshot)),\n } satisfies CaptureInterval;\n});\n","import { Crypto, Effect, type PlatformError, Schema, type Scope, Stream } from \"effect\";\n\nimport type { BrowserSession } from \"./Browser.ts\";\nimport { PageInfo } from \"./BrowserData.ts\";\nimport type {\n CapturedFrame,\n CaptureOptions,\n CaptureSnapshot,\n CaptureSummary,\n} from \"./CaptureData.ts\";\nimport { BrowserError, Reasons } from \"./Errors.ts\";\nimport { captureParent } from \"./internal/browser/Association.ts\";\nimport { startCapture } from \"./internal/capture/Capture.ts\";\n\nexport {\n CapturedFrame,\n CaptureOptions,\n CaptureSize,\n CaptureSnapshot,\n CaptureSummary,\n} from \"./CaptureData.ts\";\n\n/** Live stream/Effect capabilities intentionally have no data schema or serialization contract. */\nexport interface CaptureInterval {\n /** Single subscription. Ending or interrupting it stops this interval, not its browser. */\n readonly frames: Stream.Stream<CapturedFrame, BrowserError>;\n /**\n * A bounded copy of recorded metadata, available during capture and after cleanup. No native\n * work, budget charge or subscription. Drain frames, then read `completed` for final accounting.\n */\n readonly snapshot: Effect.Effect<CaptureSnapshot>;\n readonly stop: Effect.Effect<CaptureSummary>;\n readonly completed: Effect.Effect<CaptureSummary>;\n}\n\n/**\n * Capture one remote page independently of the session's selected page.\n *\n * Requires the exact live session returned by the host; copying a session object or decoding\n * a durable reference cannot copy its capture authority. The private owner is not frame data.\n * `CapturedFrame` and `CaptureOptions` are Schema values as well as structural types. Frame\n * decoding checks binary/metadata fields, not the complete JPEG bitstream or target authority,\n * and does not copy bytes. Options decoding preserves omissions; admission applies defaults.\n * `CaptureInterval` remains a live scoped capability, not a schema or JSON/Tool value.\n */\nexport const start = <E>(\n session: BrowserSession<E>,\n options: CaptureOptions = {},\n): Effect.Effect<CaptureInterval, BrowserError, Scope.Scope> =>\n Effect.suspend(() => {\n const parent = captureParent(session);\n\n if (parent === undefined)\n return Effect.fail(\n BrowserError.make({\n operation: \"capture\",\n reason: Reasons.UnregisteredSession.make({}),\n outcome: \"undispatched\",\n }),\n );\n if (options.target === undefined) return startCapture(parent, options);\n\n return Schema.decodeEffect(PageInfo)(options.target, { onExcessProperty: \"error\" }).pipe(\n Effect.mapError(() =>\n BrowserError.make({\n operation: \"capture\",\n reason: Reasons.Configuration.make({}),\n outcome: \"undispatched\",\n }),\n ),\n Effect.flatMap((target) => startCapture(parent, { ...options, target })),\n );\n });\n\n/**\n * A lazy, scoped frame stream. Each subscription acquires one interval and finalizes it when\n * consumption ends, fails or is interrupted; the browser remains owned by its caller.\n * Concurrent subscriptions on the same page are refused by the existing capture reservation.\n * Unconfirmed native cleanup retains the page reservation. Use start when the host needs\n * explicit stop acknowledgement, interval snapshots and the final capture summary.\n */\nexport const stream = <E>(\n session: BrowserSession<E>,\n options: CaptureOptions = {},\n): Stream.Stream<CapturedFrame, BrowserError> =>\n Stream.unwrap(start(session, options).pipe(Effect.map((interval) => interval.frames)));\n\n/** A `multipart/x-mixed-replace` body and the content type that names its boundary. */\nexport interface MultipartBody<E, R> {\n readonly contentType: string;\n readonly body: Stream.Stream<Uint8Array, E, R>;\n}\n\n/**\n * Frames as motion JPEG for an `<img>`: one `multipart/x-mixed-replace` response, as WHATWG HTML\n * defines it for images. Each frame is followed at once by the next part's delimiter and headers,\n * because a browser shows a part only when it has read the headers of the one after it; without\n * that, a still page's last picture would never be shown. Parts carry no metadata, so nothing\n * but the pictures reaches a viewer. The boundary is drawn from `Crypto` for each response, so a\n * page cannot shape its pixels into it. Call it once per viewer.\n */\nexport const multipart = <E, R>(\n frames: Stream.Stream<CapturedFrame, E, R>,\n): Effect.Effect<MultipartBody<E, R>, PlatformError.PlatformError, Crypto.Crypto> =>\n Effect.map(\n Effect.flatMap(Crypto.Crypto, (crypto) => crypto.randomUUIDv4),\n (uuid) => {\n const boundary = `frame-${uuid.replaceAll(\"-\", \"\")}`;\n const encoder = new TextEncoder();\n const opening = encoder.encode(`--${boundary}\\r\\nContent-Type: image/jpeg\\r\\n\\r\\n`);\n const next = encoder.encode(`\\r\\n--${boundary}\\r\\nContent-Type: image/jpeg\\r\\n\\r\\n`);\n\n return {\n contentType: `multipart/x-mixed-replace; boundary=${boundary}`,\n body: Stream.make(opening).pipe(\n Stream.concat(frames.pipe(Stream.flatMap((frame) => Stream.make(frame.bytes, next)))),\n Stream.concat(Stream.make(encoder.encode(`--${boundary}--\\r\\n`))),\n ),\n };\n },\n );\n"],"mappings":"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"}
|