effect-browser 0.0.0-reserved.0 → 0.2.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +664 -3
  3. package/dist/Association-zlgInoaj.mjs +134 -0
  4. package/dist/Association-zlgInoaj.mjs.map +1 -0
  5. package/dist/Bootstrap-B9UUXsvF.d.mts +183 -0
  6. package/dist/Bootstrap-Be-2Ddmh.mjs +187 -0
  7. package/dist/Bootstrap-Be-2Ddmh.mjs.map +1 -0
  8. package/dist/Bootstrap.d.mts +3 -0
  9. package/dist/Bootstrap.mjs +2 -0
  10. package/dist/Browser.d.mts +177 -0
  11. package/dist/Browser.mjs +14 -0
  12. package/dist/Browser.mjs.map +1 -0
  13. package/dist/BrowserData-DJAnUHoZ.d.mts +412 -0
  14. package/dist/BrowserData.d.mts +3 -0
  15. package/dist/BrowserData.mjs +494 -0
  16. package/dist/BrowserData.mjs.map +1 -0
  17. package/dist/BrowserRuntime-fZz1b0VI.mjs +5169 -0
  18. package/dist/BrowserRuntime-fZz1b0VI.mjs.map +1 -0
  19. package/dist/BrowserRuntime.d.mts +139 -0
  20. package/dist/BrowserRuntime.mjs +2 -0
  21. package/dist/Capture.d.mts +40 -0
  22. package/dist/Capture.mjs +476 -0
  23. package/dist/Capture.mjs.map +1 -0
  24. package/dist/CaptureData-AmqVP_Ba.mjs +162 -0
  25. package/dist/CaptureData-AmqVP_Ba.mjs.map +1 -0
  26. package/dist/CaptureData.d.mts +148 -0
  27. package/dist/CaptureData.mjs +2 -0
  28. package/dist/Chromium.d.mts +84 -0
  29. package/dist/Chromium.mjs +381 -0
  30. package/dist/Chromium.mjs.map +1 -0
  31. package/dist/Errors.d.mts +106 -0
  32. package/dist/Errors.mjs +155 -0
  33. package/dist/Errors.mjs.map +1 -0
  34. package/dist/PageControl.d.mts +15 -0
  35. package/dist/PageControl.mjs +28 -0
  36. package/dist/PageControl.mjs.map +1 -0
  37. package/dist/PageControlAssociation-DabEg5AR.mjs +10 -0
  38. package/dist/PageControlAssociation-DabEg5AR.mjs.map +1 -0
  39. package/dist/index.d.mts +10 -0
  40. package/dist/index.mjs +9 -0
  41. package/package.json +67 -5
@@ -0,0 +1,139 @@
1
+ import { BrowserError, InitializationError } from "./Errors.mjs";
2
+ import { S as Observation, a as BrowserPolicy, c as ClickRequest, m as InlineFile, n as AutomationOptions, q as Viewport, t as ActionResult } from "./BrowserData-DJAnUHoZ.mjs";
3
+ import { BrowserSession, OpenOptions } from "./Browser.mjs";
4
+ import { Effect, Option, Redacted, Schema, Scope } from "effect";
5
+ //#region src/internal/browser/ConnectionCleanup.d.ts
6
+ /** Evidence about this connection, independent of the lifetime of its browser process. */
7
+ type ConnectionState = "closed" | "failed" | "not-connected" | "pending";
8
+ /** Captured teardown operations supplied to the lifetime which owns this connection. */
9
+ interface ConnectionCleanup {
10
+ readonly fence: Effect.Effect<void>;
11
+ readonly capture: Effect.Effect<void, BrowserError>;
12
+ readonly initialization: Effect.Effect<void, BrowserError | InitializationError>;
13
+ readonly disconnect: Effect.Effect<ConnectionState, BrowserError>;
14
+ }
15
+ export declare const noConnection: ConnectionCleanup;
16
+ type CleanupStep<A> = {
17
+ readonly _tag: "Success";
18
+ readonly value: A;
19
+ } | {
20
+ readonly _tag: "Failure";
21
+ readonly reason: "timeout" | "interrupted" | "failed";
22
+ };
23
+ /** Every step settles independently, so one failure never suppresses subsequent teardown. */
24
+ export declare const cleanupStep: <A, E, R>(action: Effect.Effect<A, E, R>, timeoutMillis: number) => Effect.Effect<CleanupStep<A>, never, R>;
25
+ /** Bounded host notification; reporting cannot change the cleanup evidence it describes. */
26
+ export declare const reported: <A, E>(effect: Effect.Effect<A, E>) => Effect.Effect<void>;
27
+ //#endregion
28
+ //#region src/BrowserRuntime.d.ts
29
+ /** Opaque engine configuration. Only this constructor's issued values can acquire a runtime. */
30
+ export interface BrowserBinding {
31
+ readonly _tag: "BrowserBinding";
32
+ }
33
+ export interface PlaywrightOptions {
34
+ /** A trusted integration validates its own endpoint authority before routing the connection. */
35
+ readonly resolveEndpoint?: (connection: {
36
+ readonly url: Redacted.Redacted<string>;
37
+ }) => Effect.Effect<string, BrowserError>;
38
+ /** Trusted instrumentation of the existing native connection, before the owner starts using it. */
39
+ readonly onConnected?: (connection: {
40
+ readonly native: unknown;
41
+ readonly endpoint: string;
42
+ }) => void;
43
+ }
44
+ /** A maintained Playwright connection, loaded lazily. No driver or protocol capability is returned. */
45
+ export declare const playwright: (options?: PlaywrightOptions) => BrowserBinding;
46
+ /**
47
+ * An integration owns endpoint authority and the meaning of release. Its source must register
48
+ * release in the supplied Scope before returning; the runtime owns only the connection below it.
49
+ */
50
+ export interface Lifetime {
51
+ readonly reference: unknown;
52
+ readonly connection: (remainingMillis: number) => Effect.Effect<Redacted.Redacted<string>, BrowserError>;
53
+ readonly release: Effect.Effect<unknown>;
54
+ readonly cleanupResult: Effect.Effect<Option.Option<unknown>>;
55
+ readonly closeChecked: Effect.Effect<void, BrowserError>;
56
+ /**
57
+ * Memory-only evidence that canonical owned cleanup terminated native browser control.
58
+ * Omission is unknown. Borrowed disconnection alone never establishes this fact.
59
+ */
60
+ readonly controlRetired?: Effect.Effect<boolean>;
61
+ readonly verifyReconnect?: Effect.Effect<void, BrowserError>;
62
+ }
63
+ export type Source<L extends Lifetime, E, R = never> = (cleanup: ConnectionCleanup, lifetimeDeadline: number) => Effect.Effect<L, E, R | Scope.Scope>;
64
+ export interface RuntimeOptions {
65
+ readonly implementation: string;
66
+ readonly binding?: BrowserBinding;
67
+ readonly automation?: AutomationOptions;
68
+ readonly viewport?: Viewport;
69
+ readonly preserveViewport?: boolean;
70
+ readonly keepAlive?: boolean;
71
+ }
72
+ export interface AcquireOptions<E = never, R = never> extends OpenOptions<E, R> {
73
+ /** Attach to an existing page without creating a tab or changing its viewport. */
74
+ readonly existingTarget?: {
75
+ readonly targetId?: string;
76
+ };
77
+ }
78
+ /**
79
+ * Resolved host input for an integration constructing its own runtime. Stored paths must already
80
+ * have been authorized by that integration; a session cannot recover these operations afterwards.
81
+ */
82
+ export type ResolvedFileSelection = {
83
+ readonly _tag: "Inline";
84
+ readonly files: ReadonlyArray<InlineFile>;
85
+ } | {
86
+ readonly _tag: "Stored";
87
+ readonly paths: ReadonlyArray<string>;
88
+ };
89
+ export interface FileRequest {
90
+ readonly selector: string;
91
+ readonly selection: ResolvedFileSelection;
92
+ }
93
+ declare const DownloadEvent_base: Schema.Class<DownloadEvent, Schema.Struct<{
94
+ readonly downloadId: Schema.NonEmptyString;
95
+ readonly filename: Schema.NonEmptyString;
96
+ readonly state: Schema.Literals<readonly ["completed", "failed"]>;
97
+ }>, {}>;
98
+ export declare class DownloadEvent extends DownloadEvent_base {}
99
+ /**
100
+ * Modeled host operations for the integration creating this connection. Native authority stays
101
+ * inside the owner; the integration adds only the capabilities its public session supports.
102
+ */
103
+ export interface Integration<Reference> {
104
+ readonly clickForDownload: (request: ClickRequest) => Effect.Effect<DownloadEvent, BrowserError>;
105
+ readonly selectFiles: (request: FileRequest) => Effect.Effect<ActionResult, BrowserError>;
106
+ readonly clickForFileSelection: (request: FileRequest) => Effect.Effect<ActionResult, BrowserError>;
107
+ readonly liveView: <A>(issue: Effect.Effect<A, BrowserError>) => Effect.Effect<A, BrowserError>;
108
+ readonly beginHandoff: <A>(issue: Effect.Effect<A, BrowserError>) => Effect.Effect<{
109
+ readonly token: Redacted.Redacted<string>;
110
+ readonly view: A;
111
+ }, BrowserError>;
112
+ readonly resume: (token: Redacted.Redacted<string>, operatorReleasedControl: boolean) => Effect.Effect<Observation, BrowserError>;
113
+ readonly detach: Effect.Effect<{
114
+ readonly reference: Reference;
115
+ readonly targetId: string;
116
+ }, BrowserError>;
117
+ readonly reconnect: (operatorReleasedControl: boolean) => Effect.Effect<Observation, BrowserError | InitializationError>;
118
+ }
119
+ export interface Connection<Reference, E = never> {
120
+ /** Preserve this object's identity when adding an integration's public capabilities. */
121
+ readonly session: BrowserSession<E>;
122
+ readonly operations: Integration<Reference>;
123
+ }
124
+ export interface Acquisition<L extends Lifetime, E = never> {
125
+ readonly reference: L["reference"];
126
+ readonly lifetime: L;
127
+ readonly failure: Effect.Effect<never, E | InitializationError>;
128
+ readonly close: L["release"];
129
+ readonly connect: Effect.Effect<Connection<L["reference"], E>, BrowserError | InitializationError | E>;
130
+ }
131
+ export interface Runtime {
132
+ readonly acquire: <L extends Lifetime, AE, AR, E = never, R = never>(policy: BrowserPolicy, source: Source<L, AE, AR>, request?: AcquireOptions<E, R>) => Effect.Effect<Acquisition<L, E>, AE | BrowserError, Scope.Scope | AR | Exclude<R, Scope.Scope>>;
133
+ }
134
+ /** Validate immutable connection configuration without loading a peer or acquiring a lifetime. */
135
+ export declare const make: (options: RuntimeOptions) => Effect.Effect<Runtime, BrowserError, never>;
136
+ //#endregion
137
+ export type { CleanupStep, ConnectionCleanup, ConnectionState };
138
+ export {};
139
+ //# sourceMappingURL=BrowserRuntime.d.mts.map
@@ -0,0 +1,2 @@
1
+ import { a as noConnection, i as cleanupStep, n as make, o as reported, r as playwright, t as DownloadEvent } from "./BrowserRuntime-fZz1b0VI.mjs";
2
+ export { DownloadEvent, cleanupStep, make, noConnection, playwright, reported };
@@ -0,0 +1,40 @@
1
+ import { BrowserError } from "./Errors.mjs";
2
+ import { BrowserSession } from "./Browser.mjs";
3
+ import { CaptureOptions, CaptureSize, CaptureSnapshot, CaptureSummary, CapturedFrame } from "./CaptureData.mjs";
4
+ import { Effect, 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
+ //#endregion
38
+ export { CaptureOptions, CaptureSize, CaptureSnapshot, CaptureSummary, CapturedFrame };
39
+ export {};
40
+ //# sourceMappingURL=Capture.d.mts.map
@@ -0,0 +1,476 @@
1
+ import { PageInfo } from "./BrowserData.mjs";
2
+ import { BrowserError, Reasons } from "./Errors.mjs";
3
+ import { n as captureParent, r as jpegGeometry } from "./Association-zlgInoaj.mjs";
4
+ import { a as CapturedFrame, i as CaptureSummary, n as CaptureSize, o as CaptureDefaults, r as CaptureSnapshot, s as CaptureLimits, t as CaptureOptions } from "./CaptureData-AmqVP_Ba.mjs";
5
+ import { Cause, Clock, Deferred, Effect, Exit, 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 > 64 || !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 parseMetadata = Schema.decodeUnknownSync(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) documentBoundariesTruncated = true;
136
+ else documentBoundaries.push({
137
+ document,
138
+ observedMonotonicNanos: clock.monotonicTimeNanosUnsafe(),
139
+ afterSequence: received === 0 ? null : received - 1,
140
+ url: documentUrl(url)
141
+ });
142
+ };
143
+ let geometry;
144
+ let nativeStop = "unconfirmed";
145
+ let lease;
146
+ const finish = (why, failure) => {
147
+ if (ended) return;
148
+ ended = true;
149
+ reason = why;
150
+ error = failure;
151
+ Queue.offerUnsafe(wake, void 0);
152
+ Queue.offerUnsafe(finished, void 0);
153
+ };
154
+ const snapshot = () => {
155
+ if (target === void 0) throw new Error("Capture target was not admitted");
156
+ return CaptureSummary.make({
157
+ target,
158
+ reason,
159
+ received,
160
+ delivered,
161
+ discarded: buffer.dropped + rejected + late + duplicates,
162
+ overflow: buffer.dropped,
163
+ duplicates,
164
+ late,
165
+ rejected,
166
+ peakBufferedFrames: buffer.highWaterFrames,
167
+ peakBufferedBytes: buffer.highWaterBytes,
168
+ bufferedFrames: buffer.size,
169
+ bufferedBytes: buffer.bytes,
170
+ sourceFirstMillis: first ?? null,
171
+ sourceLastMillis: last ?? null,
172
+ initialUrl,
173
+ documentBoundaries: documentBoundaries.map((boundary) => ({ ...boundary })),
174
+ documentBoundariesTruncated,
175
+ nativeStop,
176
+ upstreamDrops: "unknown",
177
+ ...error === void 0 ? {} : { error: BrowserError.make({
178
+ operation: error.operation,
179
+ reason: { ...error.reason },
180
+ outcome: error.outcome
181
+ }) }
182
+ });
183
+ };
184
+ const stopNative = (yield* Effect.cached(Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
185
+ finish("stopped");
186
+ if (startPromise !== void 0 && !startSettled) yield* restore(Effect.tryPromise({
187
+ try: () => startPromise,
188
+ catch: () => BrowserError.make({
189
+ operation: "capture-start",
190
+ reason: Reasons.Provider.make({}),
191
+ outcome: "unknown"
192
+ })
193
+ }).pipe(Effect.timeout(2e3), Effect.exit));
194
+ if (source !== void 0) {
195
+ const stopped = yield* restore(Effect.tryPromise({
196
+ try: () => source.stop(),
197
+ catch: () => BrowserError.make({
198
+ operation: "capture-stop",
199
+ reason: Reasons.Provider.make({}),
200
+ outcome: "unknown"
201
+ })
202
+ }).pipe(Effect.timeout(3e3))).pipe(Effect.exit);
203
+ if (Exit.isSuccess(stopped) && startSettled) nativeStop = "confirmed";
204
+ } else nativeStop = "confirmed";
205
+ if (nativeStop === "confirmed" && leaseKey !== void 0 && lease !== void 0 && parent.captureLeases.get(leaseKey) === lease) {
206
+ parent.captureLeases.delete(leaseKey);
207
+ parent.captureReservedBytes = Math.max(0, parent.captureReservedBytes - lease.reservedBytes);
208
+ }
209
+ cleanupFinished = true;
210
+ return snapshot();
211
+ })))).pipe(Effect.tap(() => Deferred.succeed(completed, void 0)));
212
+ const receive = (frame) => {
213
+ if (ended) return;
214
+ if (target === void 0 || parent.owner.state.generation !== target.generation || parent.owner.state.phase !== "open") {
215
+ finish("parent-unavailable", BrowserError.make({
216
+ operation: "capture",
217
+ reason: Reasons.Closed.make({}),
218
+ outcome: "undispatched"
219
+ }));
220
+ return;
221
+ }
222
+ const sequence = received++;
223
+ try {
224
+ const meta = parseMetadata(frame);
225
+ if (!(frame.data instanceof Uint8Array)) {
226
+ rejected++;
227
+ finish("malformed-frame", BrowserError.make({
228
+ operation: "capture",
229
+ reason: Reasons.Malformed.make({ path: "bytes" }),
230
+ outcome: "undispatched"
231
+ }));
232
+ return;
233
+ }
234
+ if (frame.data.length > maxFrameBytes) {
235
+ rejected++;
236
+ finish("frame-limit", BrowserError.make({
237
+ operation: "capture",
238
+ reason: Reasons.Limit.make({
239
+ dimension: "frame-bytes",
240
+ maximum: maxFrameBytes,
241
+ observed: frame.data.length
242
+ }),
243
+ outcome: "undispatched"
244
+ }));
245
+ return;
246
+ }
247
+ if (last !== void 0 && meta.timestamp < last) {
248
+ late++;
249
+ if (++lateRun > MaxConsecutiveLateFrames) finish("timestamp-discontinuity", BrowserError.make({
250
+ operation: "capture",
251
+ reason: Reasons.Timestamp.make({}),
252
+ outcome: "undispatched"
253
+ }));
254
+ return;
255
+ }
256
+ if (last === meta.timestamp) {
257
+ duplicates++;
258
+ return;
259
+ }
260
+ const dimensions = jpegGeometry(frame.data);
261
+ if (frame.data[frame.data.length - 2] !== 255 || frame.data[frame.data.length - 1] !== 217) {
262
+ rejected++;
263
+ finish("malformed-frame", BrowserError.make({
264
+ operation: "capture",
265
+ reason: Reasons.Malformed.make({ path: "bytes" }),
266
+ outcome: "undispatched"
267
+ }));
268
+ return;
269
+ }
270
+ const maximumWidth = Math.min(16384, size?.width ?? 16384);
271
+ const maximumHeight = Math.min(16384, size?.height ?? 16384);
272
+ const excessive = dimensions.width > maximumWidth ? Reasons.Limit.make({
273
+ dimension: "width",
274
+ maximum: maximumWidth,
275
+ observed: dimensions.width
276
+ }) : dimensions.height > maximumHeight ? Reasons.Limit.make({
277
+ dimension: "height",
278
+ maximum: maximumHeight,
279
+ observed: dimensions.height
280
+ }) : dimensions.width * dimensions.height > 33554432 ? Reasons.Limit.make({
281
+ dimension: "pixels",
282
+ maximum: 33554432,
283
+ observed: dimensions.width * dimensions.height
284
+ }) : void 0;
285
+ if (excessive !== void 0) {
286
+ rejected++;
287
+ finish("frame-limit", BrowserError.make({
288
+ operation: "capture",
289
+ reason: excessive,
290
+ outcome: "undispatched"
291
+ }));
292
+ return;
293
+ }
294
+ if (geometry !== void 0 && (geometry.width !== dimensions.width || geometry.height !== dimensions.height || geometry.viewportWidth !== meta.viewportWidth || geometry.viewportHeight !== meta.viewportHeight)) {
295
+ rejected++;
296
+ finish("resized", BrowserError.make({
297
+ operation: "capture",
298
+ reason: Reasons.Resized.make({}),
299
+ outcome: "undispatched"
300
+ }));
301
+ return;
302
+ }
303
+ geometry ??= {
304
+ ...dimensions,
305
+ viewportWidth: meta.viewportWidth,
306
+ viewportHeight: meta.viewportHeight
307
+ };
308
+ first ??= meta.timestamp;
309
+ last = meta.timestamp;
310
+ lateRun = 0;
311
+ buffer.offer({
312
+ bytes: new Uint8Array(frame.data),
313
+ mediaType: "image/jpeg",
314
+ target,
315
+ sequence,
316
+ document,
317
+ sourceTimeMillis: meta.timestamp,
318
+ sourceClock: "presentation-unix-millis",
319
+ receivedMonotonicNanos: clock.monotonicTimeNanosUnsafe(),
320
+ ...dimensions,
321
+ viewportWidth: meta.viewportWidth,
322
+ viewportHeight: meta.viewportHeight
323
+ });
324
+ Queue.offerUnsafe(wake, void 0);
325
+ } catch {
326
+ rejected++;
327
+ finish("malformed-frame", BrowserError.make({
328
+ operation: "capture",
329
+ reason: Reasons.Malformed.make({}),
330
+ outcome: "undispatched"
331
+ }));
332
+ }
333
+ };
334
+ yield* parent.owner.guard("capture-start", (ticket) => Effect.gen(function* () {
335
+ const resolved = yield* parent.resolve(ticket, options.target);
336
+ target = Object.freeze(resolved.target);
337
+ source = resolved.source;
338
+ leaseKey = resolved.key;
339
+ if (parent.captureLeases.has(leaseKey)) return yield* BrowserError.make({
340
+ operation: "capture",
341
+ reason: Reasons.Busy.make({}),
342
+ outcome: "undispatched"
343
+ });
344
+ if (parent.captureLeases.size >= MaxParentCaptures || parent.captureReservedBytes + maxBytes > MaxParentBufferedBytes) return yield* BrowserError.make({
345
+ operation: "capture",
346
+ reason: parent.captureLeases.size >= MaxParentCaptures ? Reasons.Limit.make({
347
+ dimension: "captures",
348
+ maximum: MaxParentCaptures,
349
+ observed: parent.captureLeases.size + 1
350
+ }) : Reasons.Limit.make({
351
+ dimension: "buffered-bytes",
352
+ maximum: MaxParentBufferedBytes,
353
+ observed: parent.captureReservedBytes + maxBytes
354
+ }),
355
+ outcome: "undispatched"
356
+ });
357
+ lease = {
358
+ reservedBytes: maxBytes,
359
+ stop: stopNative.pipe(Effect.asVoid),
360
+ invalidate: (why) => finish(why, BrowserError.make({
361
+ operation: "capture",
362
+ reason: why === "resized" ? Reasons.Resized.make({}) : Reasons.TargetChanged.make({}),
363
+ outcome: "undispatched"
364
+ }))
365
+ };
366
+ parent.captureLeases.set(leaseKey, lease);
367
+ parent.captureReservedBytes += maxBytes;
368
+ yield* Effect.addFinalizer(() => stopNative.pipe(Effect.andThen(Effect.sync(() => {
369
+ rejected += buffer.size;
370
+ buffer.clear();
371
+ }))));
372
+ startSettled = false;
373
+ yield* Effect.tryPromise({
374
+ try: () => {
375
+ startPromise = source.start({
376
+ receive,
377
+ quality,
378
+ invalidate: (why) => lease?.invalidate(why),
379
+ opened: (url) => {
380
+ initialUrl = documentUrl(url);
381
+ },
382
+ ...size === void 0 ? {} : { size },
383
+ ...options.lifetime === "page" ? { document: nextDocument } : {}
384
+ });
385
+ startPromise.then(() => {
386
+ startSettled = true;
387
+ if (cleanupFinished && nativeStop === "unconfirmed" && leaseKey !== void 0 && parent.captureLeases.get(leaseKey) === lease) source.stop().catch(() => {});
388
+ }, () => {
389
+ startSettled = true;
390
+ });
391
+ return startPromise;
392
+ },
393
+ catch: () => BrowserError.make({
394
+ operation: "capture-start",
395
+ reason: Reasons.Provider.make({}),
396
+ outcome: "unknown"
397
+ })
398
+ });
399
+ ticket.check();
400
+ }), { 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)));
401
+ 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);
402
+ const next = Effect.suspend(() => {
403
+ const frame = buffer.take();
404
+ if (frame !== void 0) {
405
+ delivered++;
406
+ return Effect.succeed(frame);
407
+ }
408
+ if (ended) return error === void 0 ? Cause.done() : Effect.fail(error);
409
+ return Queue.take(wake).pipe(Effect.andThen(next));
410
+ });
411
+ return {
412
+ frames: Stream.unwrap(Effect.suspend(() => {
413
+ if (subscribed) return Effect.fail(BrowserError.make({
414
+ operation: "capture-consume",
415
+ reason: Reasons.Busy.make({}),
416
+ outcome: "undispatched"
417
+ }));
418
+ subscribed = true;
419
+ return Effect.succeed(Stream.fromEffectRepeat(next).pipe(Stream.ensuring(stopNative.pipe(Effect.asVoid))));
420
+ })),
421
+ snapshot: Effect.sync(() => {
422
+ const { reason, nativeStop, ...metadata } = snapshot();
423
+ return CaptureSnapshot.make({
424
+ ...metadata,
425
+ phase: cleanupFinished ? "stopped" : ended ? "stopping" : "capturing",
426
+ observedMonotonicNanos: clock.monotonicTimeNanosUnsafe(),
427
+ currentDocument: document,
428
+ reason: ended ? reason : null,
429
+ nativeStop: cleanupFinished ? nativeStop : null
430
+ });
431
+ }),
432
+ stop: stopNative,
433
+ completed: Deferred.await(completed).pipe(Effect.map(snapshot))
434
+ };
435
+ });
436
+ //#endregion
437
+ //#region src/Capture.ts
438
+ /**
439
+ * Capture one remote page independently of the session's selected page.
440
+ *
441
+ * Requires the exact live session returned by the host; copying a session object or decoding
442
+ * a durable reference cannot copy its capture authority. The private owner is not frame data.
443
+ * `CapturedFrame` and `CaptureOptions` are Schema values as well as structural types. Frame
444
+ * decoding checks binary/metadata fields, not the complete JPEG bitstream or target authority,
445
+ * and does not copy bytes. Options decoding preserves omissions; admission applies defaults.
446
+ * `CaptureInterval` remains a live scoped capability, not a schema or JSON/Tool value.
447
+ */
448
+ const start = (session, options = {}) => Effect.suspend(() => {
449
+ const parent = captureParent(session);
450
+ if (parent === void 0) return Effect.fail(BrowserError.make({
451
+ operation: "capture",
452
+ reason: Reasons.UnregisteredSession.make({}),
453
+ outcome: "undispatched"
454
+ }));
455
+ if (options.target === void 0) return startCapture(parent, options);
456
+ return Schema.decodeEffect(PageInfo)(options.target, { onExcessProperty: "error" }).pipe(Effect.mapError(() => BrowserError.make({
457
+ operation: "capture",
458
+ reason: Reasons.Configuration.make({}),
459
+ outcome: "undispatched"
460
+ })), Effect.flatMap((target) => startCapture(parent, {
461
+ ...options,
462
+ target
463
+ })));
464
+ });
465
+ /**
466
+ * A lazy, scoped frame stream. Each subscription acquires one interval and finalizes it when
467
+ * consumption ends, fails or is interrupted; the browser remains owned by its caller.
468
+ * Concurrent subscriptions on the same page are refused by the existing capture reservation.
469
+ * Unconfirmed native cleanup retains the page reservation. Use start when the host needs
470
+ * explicit stop acknowledgement, interval snapshots and the final capture summary.
471
+ */
472
+ const stream = (session, options = {}) => Stream.unwrap(start(session, options).pipe(Effect.map((interval) => interval.frames)));
473
+ //#endregion
474
+ export { CaptureOptions, CaptureSize, CaptureSnapshot, CaptureSummary, CapturedFrame, start, stream };
475
+
476
+ //# sourceMappingURL=Capture.mjs.map