effect-browser 0.2.0-beta.7 → 0.2.0-beta.9

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 (68) hide show
  1. package/README.md +504 -152
  2. package/dist/Browser-BF9_sORP.d.mts +388 -0
  3. package/dist/Browser.d.mts +3 -200
  4. package/dist/Browser.mjs +9 -1
  5. package/dist/Browser.mjs.map +1 -1
  6. package/dist/{BrowserData-KHk4o__c.d.mts → BrowserData-CPIIIHol.d.mts} +81 -7
  7. package/dist/BrowserData.d.mts +2 -2
  8. package/dist/BrowserData.mjs +104 -6
  9. package/dist/BrowserData.mjs.map +1 -1
  10. package/dist/{BrowserRuntime-hxkTNlRa.d.mts → BrowserRuntime-B6nT1G4X.d.mts} +16 -11
  11. package/dist/{BrowserRuntime-Cr76CN4I.mjs → BrowserRuntime-C1r2aSRK.mjs} +5065 -4305
  12. package/dist/BrowserRuntime-C1r2aSRK.mjs.map +1 -0
  13. package/dist/BrowserRuntime.d.mts +2 -2
  14. package/dist/BrowserRuntime.mjs +2 -1
  15. package/dist/Capture.d.mts +9 -7
  16. package/dist/Capture.mjs +157 -32
  17. package/dist/Capture.mjs.map +1 -1
  18. package/dist/{CaptureData-Dys0Ko9d.mjs → CaptureData-CYriZHX1.mjs} +25 -6
  19. package/dist/CaptureData-CYriZHX1.mjs.map +1 -0
  20. package/dist/CaptureData.d.mts +62 -3
  21. package/dist/CaptureData.mjs +2 -2
  22. package/dist/Chromium.d.mts +2 -2
  23. package/dist/Chromium.mjs +19 -10
  24. package/dist/Chromium.mjs.map +1 -1
  25. package/dist/Errors.d.mts +66 -17
  26. package/dist/Errors.mjs +47 -5
  27. package/dist/Errors.mjs.map +1 -1
  28. package/dist/Images-yCG9BmRU.mjs +76 -0
  29. package/dist/Images-yCG9BmRU.mjs.map +1 -0
  30. package/dist/OperationOptions-DAeRS6O9.mjs +55 -0
  31. package/dist/OperationOptions-DAeRS6O9.mjs.map +1 -0
  32. package/dist/PageControl.d.mts +6 -6
  33. package/dist/PageControl.mjs +16 -10
  34. package/dist/PageControl.mjs.map +1 -1
  35. package/dist/PageRegistry-B3q9DS3g.mjs +164 -0
  36. package/dist/PageRegistry-B3q9DS3g.mjs.map +1 -0
  37. package/dist/Plan-Dyxu2q8C.mjs +543 -0
  38. package/dist/Plan-Dyxu2q8C.mjs.map +1 -0
  39. package/dist/Plan.d.mts +3 -0
  40. package/dist/Plan.mjs +2 -0
  41. package/dist/PlanData-B668RxbQ.d.mts +4831 -0
  42. package/dist/PlanData.d.mts +3 -0
  43. package/dist/PlanData.mjs +268 -0
  44. package/dist/PlanData.mjs.map +1 -0
  45. package/dist/PlanInput-BJcPPncs.mjs +75 -0
  46. package/dist/PlanInput-BJcPPncs.mjs.map +1 -0
  47. package/dist/PublicSession-DCSMMqOj.mjs +5602 -0
  48. package/dist/PublicSession-DCSMMqOj.mjs.map +1 -0
  49. package/dist/Testing.d.mts +48 -6
  50. package/dist/Testing.mjs +651 -113
  51. package/dist/Testing.mjs.map +1 -1
  52. package/dist/Timeline-fV22Xk-S.d.mts +29 -0
  53. package/dist/Timeline.d.mts +4 -0
  54. package/dist/Timeline.mjs +20 -0
  55. package/dist/Timeline.mjs.map +1 -0
  56. package/dist/TimelineData-BsOn-YGZ.d.mts +3818 -0
  57. package/dist/TimelineData.d.mts +3 -0
  58. package/dist/TimelineData.mjs +482 -0
  59. package/dist/TimelineData.mjs.map +1 -0
  60. package/dist/index.d.mts +5 -1
  61. package/dist/index.mjs +5 -1
  62. package/package.json +18 -2
  63. package/dist/Association-CH3kPHXE.mjs +0 -167
  64. package/dist/Association-CH3kPHXE.mjs.map +0 -1
  65. package/dist/BrowserRuntime-Cr76CN4I.mjs.map +0 -1
  66. package/dist/CaptureData-Dys0Ko9d.mjs.map +0 -1
  67. package/dist/PageControlAssociation-DabEg5AR.mjs +0 -10
  68. package/dist/PageControlAssociation-DabEg5AR.mjs.map +0 -1
@@ -1,3 +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-hxkTNlRa.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 };
1
+ import { S as reported, _ as CleanupStep, a as DownloadEvent, b as cleanupStep, c as Lifetime, d as Runtime, f as RuntimeOptions, g as playwright, h as make, i as Connection, l as PlaywrightOptions, m as TransferOperations, n as Acquisition, o as FileRequest, p as Source, r as BrowserBinding, s as Integration, t as AcquireOptions, u as ResolvedFileSelection, v as ConnectionCleanup, x as noConnection, y as ConnectionState } from "./BrowserRuntime-B6nT1G4X.mjs";
2
+ export { AcquireOptions, Acquisition, BrowserBinding, type CleanupStep, Connection, type ConnectionCleanup, type ConnectionState, DownloadEvent, FileRequest, Integration, Lifetime, PlaywrightOptions, ResolvedFileSelection, Runtime, RuntimeOptions, Source, TransferOperations, cleanupStep, make, noConnection, playwright, reported };
3
3
  export {};
@@ -1,2 +1,3 @@
1
- import { a as cleanupStep, n as make, o as noConnection, r as playwright, s as reported, t as DownloadEvent } from "./BrowserRuntime-Cr76CN4I.mjs";
1
+ import { n as make, r as playwright, t as DownloadEvent } from "./BrowserRuntime-C1r2aSRK.mjs";
2
+ import { c as noConnection, l as reported, s as cleanupStep } from "./PublicSession-DCSMMqOj.mjs";
2
3
  export { DownloadEvent, cleanupStep, make, noConnection, playwright, reported };
@@ -1,10 +1,12 @@
1
1
  import { BrowserError } from "./Errors.mjs";
2
- import { BrowserSession } from "./Browser.mjs";
3
- import { CaptureOptions, CaptureSize, CaptureSnapshot, CaptureSummary, CapturedFrame } from "./CaptureData.mjs";
2
+ import { u as Page } from "./Browser-BF9_sORP.mjs";
3
+ import { CaptureOptions, CaptureQualification, CaptureSize, CaptureSnapshot, CaptureSummary, CapturedFrame } from "./CaptureData.mjs";
4
4
  import { Crypto, Effect, PlatformError, Scope, Stream } from "effect";
5
5
  //#region src/Capture.d.ts
6
6
  /** Live stream/Effect capabilities intentionally have no data schema or serialization contract. */
7
7
  export interface CaptureInterval {
8
+ /** Opaque original interval identity; metadata refers to this id and actual frame sequences. */
9
+ readonly id: string;
8
10
  /** Single subscription. Ending or interrupting it stops this interval, not its browser. */
9
11
  readonly frames: Stream.Stream<CapturedFrame, BrowserError>;
10
12
  /**
@@ -18,14 +20,14 @@ export interface CaptureInterval {
18
20
  /**
19
21
  * Capture one remote page independently of the session's selected page.
20
22
  *
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
+ * Requires an exact issued Page; copying an object or decoding a durable reference cannot
24
+ * copy its capture authority. The private owner is not frame data.
23
25
  * `CapturedFrame` and `CaptureOptions` are Schema values as well as structural types. Frame
24
26
  * decoding checks binary/metadata fields, not the complete JPEG bitstream or target authority,
25
27
  * and does not copy bytes. Options decoding preserves omissions; admission applies defaults.
26
28
  * `CaptureInterval` remains a live scoped capability, not a schema or JSON/Tool value.
27
29
  */
28
- export declare const start: <E>(session: BrowserSession<E>, options?: CaptureOptions) => Effect.Effect<CaptureInterval, BrowserError, Scope.Scope>;
30
+ export declare const start: (page: Page, options?: CaptureOptions) => Effect.Effect<CaptureInterval, BrowserError, Scope.Scope>;
29
31
  /**
30
32
  * A lazy, scoped frame stream. Each subscription acquires one interval and finalizes it when
31
33
  * consumption ends, fails or is interrupted; the browser remains owned by its caller.
@@ -33,7 +35,7 @@ export declare const start: <E>(session: BrowserSession<E>, options?: CaptureOpt
33
35
  * Unconfirmed native cleanup retains the page reservation. Use start when the host needs
34
36
  * explicit stop acknowledgement, interval snapshots and the final capture summary.
35
37
  */
36
- export declare const stream: <E>(session: BrowserSession<E>, options?: CaptureOptions) => Stream.Stream<CapturedFrame, BrowserError>;
38
+ export declare const stream: (page: Page, options?: CaptureOptions) => Stream.Stream<CapturedFrame, BrowserError>;
37
39
  /** A `multipart/x-mixed-replace` body and the content type that names its boundary. */
38
40
  export interface MultipartBody<E, R> {
39
41
  readonly contentType: string;
@@ -49,6 +51,6 @@ export interface MultipartBody<E, R> {
49
51
  */
50
52
  export declare const multipart: <E, R>(frames: Stream.Stream<CapturedFrame, E, R>) => Effect.Effect<MultipartBody<E, R>, PlatformError.PlatformError, Crypto.Crypto>;
51
53
  //#endregion
52
- export { CaptureOptions, CaptureSize, CaptureSnapshot, CaptureSummary, CapturedFrame };
54
+ export { CaptureOptions, CaptureQualification, CaptureSize, CaptureSnapshot, CaptureSummary, CapturedFrame };
53
55
  export {};
54
56
  //# sourceMappingURL=Capture.d.mts.map
package/dist/Capture.mjs CHANGED
@@ -1,8 +1,9 @@
1
1
  import { BrowserError, Reasons } from "./Errors.mjs";
2
- import { PageInfo } from "./BrowserData.mjs";
3
- import { n as captureParent, r as jpegGeometry } from "./Association-CH3kPHXE.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-Dys0Ko9d.mjs";
5
- import { Cause, Clock, Crypto, Deferred, Effect, Exit, Option, Queue, Schema, Stream } from "effect";
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";
6
7
  //#region src/internal/capture/FrameBuffer.ts
7
8
  /** Callback-side bounded retention. No Promise, Effect, blocked producer, or per-frame fiber. */
8
9
  var FrameBuffer = class {
@@ -83,7 +84,7 @@ const Metadata = Schema.Struct({
83
84
  });
84
85
  const decodeMetadata = Schema.decodeUnknownOption(Metadata);
85
86
  /** 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 acquireCapture = Effect.fnUntraced(function* (parent, options = {}) {
87
88
  const maxFrames = options.maxFrames ?? CaptureDefaults.maxFrames;
88
89
  const maxBytes = options.maxBufferedBytes ?? CaptureDefaults.maxBufferedBytes;
89
90
  const maxFrameBytes = options.maxFrameBytes ?? CaptureDefaults.maxFrameBytes;
@@ -108,7 +109,10 @@ const startCapture = Effect.fnUntraced(function* (parent, options = {}) {
108
109
  reason: Reasons.Configuration.make({}),
109
110
  outcome: "undispatched"
110
111
  })));
111
- const clock = yield* Clock.Clock;
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;
112
116
  const wake = yield* Queue.dropping(1);
113
117
  const finished = yield* Queue.dropping(1);
114
118
  const completed = yield* Deferred.make();
@@ -119,29 +123,49 @@ const startCapture = Effect.fnUntraced(function* (parent, options = {}) {
119
123
  let startSettled = true;
120
124
  let target;
121
125
  let leaseKey;
126
+ let targetStatus;
127
+ let metadata;
128
+ let terminalAuthority;
122
129
  let ended = false, subscribed = false, cleanupFinished = false;
123
130
  let reason = "stopped";
124
131
  let error;
125
132
  let received = 0, delivered = 0, rejected = 0, duplicates = 0, late = 0, lateRun = 0;
126
133
  let first, last;
127
134
  let document = 0;
135
+ let captureBoundary = 0;
136
+ let acceptedBoundary = -1;
128
137
  let initialUrl = null;
138
+ let initialUrlQualification = "Unread";
129
139
  let documentBoundariesTruncated = false;
130
140
  const documentBoundaries = [];
131
141
  /** A URL change is recorded; only a new document advances the frame attribution. */
132
142
  const nextDocument = (url, sameDocument = false) => {
133
143
  if (ended) return;
144
+ captureBoundary++;
134
145
  if (!sameDocument) document++;
135
146
  if (documentBoundaries.length >= MaxDocumentBoundaries) {
136
147
  documentBoundaries.shift();
137
148
  documentBoundariesTruncated = true;
138
149
  }
139
- documentBoundaries.push({
150
+ const boundary = {
140
151
  document,
141
152
  sameDocument,
142
153
  observedMonotonicNanos: clock.monotonicTimeNanosUnsafe(),
143
154
  afterSequence: received === 0 ? null : received - 1,
144
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"
145
169
  });
146
170
  };
147
171
  let geometry;
@@ -165,6 +189,7 @@ const startCapture = Effect.fnUntraced(function* (parent, options = {}) {
165
189
  if (afterStart) {
166
190
  nativeStop = "confirmed";
167
191
  releaseReservation();
192
+ if (cleanupFinished) publishCapture("Stopped");
168
193
  } else {
169
194
  preStartStopConfirmed = true;
170
195
  confirmPostStartStop();
@@ -182,13 +207,34 @@ const startCapture = Effect.fnUntraced(function* (parent, options = {}) {
182
207
  ended = true;
183
208
  reason = why;
184
209
  error = failure;
210
+ publishCapture("Ended");
185
211
  Queue.offerUnsafe(wake, void 0);
186
212
  Queue.offerUnsafe(finished, void 0);
187
213
  };
188
214
  const snapshot = () => {
189
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
+ };
190
235
  return CaptureSummary.make({
191
236
  target,
237
+ qualification,
192
238
  reason,
193
239
  received,
194
240
  delivered,
@@ -211,10 +257,38 @@ const startCapture = Effect.fnUntraced(function* (parent, options = {}) {
211
257
  ...error === void 0 ? {} : { error: BrowserError.make({
212
258
  operation: error.operation,
213
259
  reason: { ...error.reason },
214
- outcome: error.outcome
260
+ outcome: error.outcome,
261
+ ...error.containment !== void 0 ? { containment: { ...error.containment } } : containment._tag === "NotRequired" ? {} : { containment }
215
262
  }) }
216
263
  });
217
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
+ };
218
292
  const stopNative = (yield* Effect.cached(Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
219
293
  finish("stopped");
220
294
  if (startPromise !== void 0 && !startSettled) {
@@ -226,7 +300,7 @@ const startCapture = Effect.fnUntraced(function* (parent, options = {}) {
226
300
  reason: Reasons.Provider.make({}),
227
301
  outcome: "unknown"
228
302
  })
229
- }).pipe(Effect.timeout(2e3), Effect.exit));
303
+ }).pipe(Effect.timeout(2e3), span("Browser.capture.start-settlement"), Effect.exit));
230
304
  }
231
305
  if (source !== void 0) {
232
306
  const stopping = source;
@@ -240,7 +314,7 @@ const startCapture = Effect.fnUntraced(function* (parent, options = {}) {
240
314
  reason: Reasons.Provider.make({}),
241
315
  outcome: "unknown"
242
316
  })
243
- }).pipe(Effect.timeout(3e3))).pipe(Effect.exit);
317
+ }).pipe(Effect.timeout(3e3), span("Browser.capture.native-stop"))).pipe(Effect.exit);
244
318
  if (Exit.isSuccess(stopped) && afterStart) {
245
319
  nativeStop = "confirmed";
246
320
  releaseReservation();
@@ -248,8 +322,15 @@ const startCapture = Effect.fnUntraced(function* (parent, options = {}) {
248
322
  } else nativeStop = "confirmed";
249
323
  if (nativeStop === "confirmed") releaseReservation();
250
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
+ });
251
332
  return snapshot();
252
- })))).pipe(Effect.tap(() => Deferred.succeed(completed, void 0)));
333
+ }).pipe(span("Browser.capture.cleanup"))).pipe(Effect.provideService(Clock.Clock, clock)))).pipe(Effect.tap(() => Deferred.succeed(completed, void 0)));
253
334
  const receive = (frame) => {
254
335
  if (ended) return;
255
336
  if (target === void 0 || parent.owner.state.generation !== target.generation || parent.owner.state.phase !== "open") {
@@ -359,6 +440,7 @@ const startCapture = Effect.fnUntraced(function* (parent, options = {}) {
359
440
  first ??= meta.timestamp;
360
441
  last = meta.timestamp;
361
442
  lateRun = 0;
443
+ const receivedMonotonicNanos = clock.monotonicTimeNanosUnsafe();
362
444
  buffer.offer({
363
445
  bytes: new Uint8Array(frame.data),
364
446
  mediaType: "image/jpeg",
@@ -367,11 +449,28 @@ const startCapture = Effect.fnUntraced(function* (parent, options = {}) {
367
449
  document,
368
450
  sourceTimeMillis: meta.timestamp,
369
451
  sourceClock: "presentation-unix-millis",
370
- receivedMonotonicNanos: clock.monotonicTimeNanosUnsafe(),
452
+ receivedMonotonicNanos,
371
453
  ...dimensions,
372
454
  viewportWidth: meta.viewportWidth,
373
455
  viewportHeight: meta.viewportHeight
374
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
+ }
375
474
  Queue.offerUnsafe(wake, void 0);
376
475
  } catch {
377
476
  rejected++;
@@ -382,9 +481,14 @@ const startCapture = Effect.fnUntraced(function* (parent, options = {}) {
382
481
  }));
383
482
  }
384
483
  };
484
+ yield* parent.validate("capture-start");
485
+ const requested = parent.page;
486
+ const generation = parent.owner.state.generation;
385
487
  yield* parent.owner.guard("capture-start", (ticket) => Effect.gen(function* () {
386
- const resolved = yield* parent.resolve(ticket, options.target);
488
+ const resolved = yield* parent.resolve(ticket, requested);
387
489
  target = Object.freeze(resolved.target);
490
+ targetStatus = resolved.status;
491
+ metadata = resolved.metadata;
388
492
  const resolvedSource = resolved.source;
389
493
  source = resolvedSource;
390
494
  leaseKey = resolved.key;
@@ -406,10 +510,19 @@ const startCapture = Effect.fnUntraced(function* (parent, options = {}) {
406
510
  }),
407
511
  outcome: "undispatched"
408
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
+ }))));
409
519
  lease = {
520
+ pageId: target.pageId,
410
521
  reservedBytes: maxBytes,
411
522
  stop: stopNative.pipe(Effect.asVoid),
412
523
  invalidate: (why) => {
524
+ if (why === "target-closed") terminalAuthority = "closed";
525
+ else if (why === "closed") terminalAuthority ??= "closing";
413
526
  if (why === "target-closed") releaseReservation();
414
527
  finish(why === "resized" ? "resized" : "target-changed", BrowserError.make({
415
528
  operation: "capture",
@@ -424,6 +537,7 @@ const startCapture = Effect.fnUntraced(function* (parent, options = {}) {
424
537
  rejected += buffer.size;
425
538
  buffer.clear();
426
539
  }))));
540
+ publishCapture("Reserved");
427
541
  startSettled = false;
428
542
  yield* Effect.tryPromise({
429
543
  try: () => {
@@ -434,6 +548,8 @@ const startCapture = Effect.fnUntraced(function* (parent, options = {}) {
434
548
  invalidate: (why) => lease?.invalidate(why),
435
549
  opened: (url) => {
436
550
  initialUrl = documentUrl(url);
551
+ initialUrlQualification = initialUrl === null ? "Omitted" : "NativeCached";
552
+ publishCapture("Watching");
437
553
  },
438
554
  ...size === void 0 ? {} : { size },
439
555
  ...options.lifetime === "page" ? { document: nextDocument } : {}
@@ -454,10 +570,22 @@ const startCapture = Effect.fnUntraced(function* (parent, options = {}) {
454
570
  reason: Reasons.Provider.make({}),
455
571
  outcome: "unknown"
456
572
  })
457
- });
573
+ }).pipe(span("Browser.capture.native-start"));
574
+ publishCapture("Started");
458
575
  ticket.check();
459
- }), { 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)));
460
- 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);
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);
461
589
  const next = Effect.suspend(() => {
462
590
  const frame = buffer.take();
463
591
  if (frame !== void 0) {
@@ -468,6 +596,7 @@ const startCapture = Effect.fnUntraced(function* (parent, options = {}) {
468
596
  return Queue.take(wake).pipe(Effect.andThen(next));
469
597
  });
470
598
  return {
599
+ id: captureId,
471
600
  frames: Stream.unwrap(Effect.suspend(() => {
472
601
  if (subscribed) return Effect.fail(BrowserError.make({
473
602
  operation: "capture-consume",
@@ -476,7 +605,7 @@ const startCapture = Effect.fnUntraced(function* (parent, options = {}) {
476
605
  }));
477
606
  subscribed = true;
478
607
  return Effect.succeed(Stream.fromEffectRepeat(next).pipe(Stream.ensuring(stopNative.pipe(Effect.asVoid))));
479
- })),
608
+ })).pipe(stream$1("Browser.capture.consume")),
480
609
  snapshot: Effect.sync(() => {
481
610
  const { reason, nativeStop, ...metadata } = snapshot();
482
611
  return CaptureSnapshot.make({
@@ -492,34 +621,30 @@ const startCapture = Effect.fnUntraced(function* (parent, options = {}) {
492
621
  completed: Deferred.await(completed).pipe(Effect.map(snapshot))
493
622
  };
494
623
  });
624
+ const startCapture = (parent, options = {}) => acquireCapture(parent, options).pipe(span("Browser.capture.start", { attributes: { "browser.operation": "capture-start" } }));
495
625
  //#endregion
496
626
  //#region src/Capture.ts
497
627
  /**
498
628
  * Capture one remote page independently of the session's selected page.
499
629
  *
500
- * Requires the exact live session returned by the host; copying a session object or decoding
501
- * a durable reference cannot copy its capture authority. The private owner is not frame data.
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.
502
632
  * `CapturedFrame` and `CaptureOptions` are Schema values as well as structural types. Frame
503
633
  * decoding checks binary/metadata fields, not the complete JPEG bitstream or target authority,
504
634
  * and does not copy bytes. Options decoding preserves omissions; admission applies defaults.
505
635
  * `CaptureInterval` remains a live scoped capability, not a schema or JSON/Tool value.
506
636
  */
507
- const start = (session, options = {}) => Effect.suspend(() => {
508
- const parent = captureParent(session);
637
+ const start = (page, options = {}) => Effect.suspend(() => {
638
+ const parent = capturePageParent(page);
509
639
  if (parent === void 0) return Effect.fail(BrowserError.make({
510
640
  operation: "capture",
511
641
  reason: Reasons.UnregisteredSession.make({}),
512
642
  outcome: "undispatched"
513
643
  }));
514
- if (options.target === void 0) return startCapture(parent, options);
515
- return Schema.decodeEffect(PageInfo)(options.target, { onExcessProperty: "error" }).pipe(Effect.mapError(() => BrowserError.make({
516
- operation: "capture",
517
- reason: Reasons.Configuration.make({}),
518
- outcome: "undispatched"
519
- })), Effect.flatMap((target) => startCapture(parent, {
520
- ...options,
521
- target
522
- })));
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
+ })))));
523
648
  });
524
649
  /**
525
650
  * A lazy, scoped frame stream. Each subscription acquires one interval and finalizes it when
@@ -528,7 +653,7 @@ const start = (session, options = {}) => Effect.suspend(() => {
528
653
  * Unconfirmed native cleanup retains the page reservation. Use start when the host needs
529
654
  * explicit stop acknowledgement, interval snapshots and the final capture summary.
530
655
  */
531
- const stream = (session, options = {}) => Stream.unwrap(start(session, options).pipe(Effect.map((interval) => interval.frames)));
656
+ const stream = (page, options = {}) => Stream.unwrap(start(page, options).pipe(Effect.map((interval) => interval.frames)));
532
657
  /**
533
658
  * Frames as motion JPEG for an `<img>`: one `multipart/x-mixed-replace` response, as WHATWG HTML
534
659
  * defines it for images. Each frame is followed at once by the next part's delimiter and headers,
@@ -548,6 +673,6 @@ const multipart = (frames) => Effect.map(Effect.flatMap(Crypto.Crypto, (crypto)
548
673
  };
549
674
  });
550
675
  //#endregion
551
- export { CaptureOptions, CaptureSize, CaptureSnapshot, CaptureSummary, CapturedFrame, multipart, start, stream };
676
+ export { CaptureOptions, CaptureQualification, CaptureSize, CaptureSnapshot, CaptureSummary, CapturedFrame, multipart, start, stream };
552
677
 
553
678
  //# sourceMappingURL=Capture.mjs.map