effect-browser 0.3.0-beta.13 → 0.3.0-beta.15
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/README.md +59 -19
- package/dist/{Browser-QiT6Cdn_.mjs → Browser-BBSySyjT.mjs} +2 -2
- package/dist/{Browser-QiT6Cdn_.mjs.map → Browser-BBSySyjT.mjs.map} +1 -1
- package/dist/Browser.d.mts +1 -1
- package/dist/Browser.mjs +1 -1
- package/dist/Cdp.mjs +1 -1
- package/dist/Chromium.mjs +1 -1
- package/dist/Expect.d.mts +1 -1
- package/dist/Expect.mjs +1 -1
- package/dist/{Frame-Di2PxyF7.d.mts → Frame-DkNdjjjr.d.mts} +3 -5
- package/dist/Frame.d.mts +1 -1
- package/dist/Frame.mjs +2 -4
- package/dist/Frame.mjs.map +1 -1
- package/dist/Moment.d.mts +1 -1
- package/dist/Moment.mjs +7 -3
- package/dist/Moment.mjs.map +1 -1
- package/dist/{Page-B7X6DQUn.d.mts → Page-DL9eylp3.d.mts} +2 -2
- package/dist/Page.d.mts +1 -1
- package/dist/{Plan-BS0jpENY.d.mts → Plan-6As6vcwT.d.mts} +3 -3
- package/dist/{Plan-Bl2NM1D5.mjs → Plan-CXA2BW-G.mjs} +4 -6
- package/dist/Plan-CXA2BW-G.mjs.map +1 -0
- package/dist/Plan.d.mts +1 -1
- package/dist/Plan.mjs +1 -1
- package/dist/Policy.d.mts +1 -1
- package/dist/Presentation.d.mts +1 -1
- package/dist/Presentation.mjs +1 -1
- package/dist/Stage.d.mts +5 -4
- package/dist/Stage.mjs +53 -20
- package/dist/Stage.mjs.map +1 -1
- package/dist/{Supervisor-CYWFK3vM.d.mts → Supervisor-5tZ47hR5.d.mts} +40 -10
- package/dist/{Supervisor-TNKNOVzu.mjs → Supervisor-DaGlQVjv.mjs} +120 -68
- package/dist/Supervisor-DaGlQVjv.mjs.map +1 -0
- package/dist/Supervisor.d.mts +2 -2
- package/dist/Supervisor.mjs +2 -2
- package/dist/index.d.mts +4 -4
- package/dist/index.mjs +3 -3
- package/dist/{page-ZxMhw16l.mjs → page-B92QnRfT.mjs} +196 -166
- package/dist/page-B92QnRfT.mjs.map +1 -0
- package/package.json +1 -1
- package/src/Frame.ts +2 -4
- package/src/Moment.ts +14 -3
- package/src/Plan.ts +7 -6
- package/src/Stage.ts +61 -21
- package/src/Supervisor.ts +205 -172
- package/src/internal/pictures/capture.ts +172 -188
- package/src/internal/pictures/pictures.ts +50 -73
- package/src/internal/pictures/quiescence.ts +94 -0
- package/src/internal/reading/ready.ts +9 -3
- package/src/internal/supervisor/lifecycle.ts +40 -15
- package/dist/Plan-Bl2NM1D5.mjs.map +0 -1
- package/dist/Supervisor-TNKNOVzu.mjs.map +0 -1
- package/dist/page-ZxMhw16l.mjs.map +0 -1
|
@@ -20,6 +20,7 @@ import { BrowserPaint, CaptureStats, Frame, type ScreencastOptions } from "../..
|
|
|
20
20
|
import { undispatched } from "../page/context.ts";
|
|
21
21
|
import { type Estimate, toHostTime, uncertaintyAt } from "./clock.ts";
|
|
22
22
|
import { tally } from "./counts.ts";
|
|
23
|
+
import * as Quiescence from "./quiescence.ts";
|
|
23
24
|
import type { NativeFrame, Transport } from "./transport.ts";
|
|
24
25
|
|
|
25
26
|
export { countsKept } from "./counts.ts";
|
|
@@ -76,11 +77,9 @@ interface Generation {
|
|
|
76
77
|
/** The native capture's settings; every reader of this generation shares them. */
|
|
77
78
|
readonly quality: number;
|
|
78
79
|
readonly size: Size | null;
|
|
79
|
-
|
|
80
|
-
accepting: boolean;
|
|
80
|
+
phase: "Capturing" | "Paused" | "Stopped";
|
|
81
81
|
predecessor: number | undefined;
|
|
82
82
|
failure: BrowserError | undefined;
|
|
83
|
-
stopReply: Promise<void> | undefined;
|
|
84
83
|
}
|
|
85
84
|
|
|
86
85
|
export interface Controller {
|
|
@@ -109,15 +108,7 @@ export interface Controller {
|
|
|
109
108
|
/** Run a clipped or scaled picture of the page, keeping what it draws out of the capture. */
|
|
110
109
|
readonly excluding: <A, E, R>(
|
|
111
110
|
picture: Effect.Effect<A, E, R>,
|
|
112
|
-
|
|
113
|
-
) => Effect.Effect<A, E, R>;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/** When the library took a clipped picture: from its call to its reply, in host milliseconds. */
|
|
117
|
-
interface Excluded {
|
|
118
|
-
readonly from: number;
|
|
119
|
-
until: number;
|
|
120
|
-
readonly resized: boolean;
|
|
111
|
+
) => Effect.Effect<A, E | BrowserError, R>;
|
|
121
112
|
}
|
|
122
113
|
|
|
123
114
|
const replyCapacity = 32;
|
|
@@ -137,56 +128,6 @@ const sameShape = (image: Size, device: Size) =>
|
|
|
137
128
|
// the page's own.
|
|
138
129
|
const outlasting = Duration.seconds(1);
|
|
139
130
|
|
|
140
|
-
// No frame painted during a picture arrives this long after it.
|
|
141
|
-
const excludedFor = Duration.seconds(10);
|
|
142
|
-
|
|
143
|
-
// Every frame of a crop is painted before Chromium answers the picture, yet each test of it can
|
|
144
|
-
// land past the reply: its stamp, because Chromium stamps frames on another clock than the one the
|
|
145
|
-
// calibration reads (2 to 3 ms late on an idle machine here), and its arrival, because a frame is
|
|
146
|
-
// encoded after it is painted (3 to 6 ms). A frame is kept only when both land this long after.
|
|
147
|
-
const settling = Duration.millis(50);
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Chromium draws the library's own clipped pictures into the running screencast. Where Playwright
|
|
151
|
-
* emulates the viewport they keep the page's device size, and its shape when the crop has the
|
|
152
|
-
* viewport's proportions, so nothing in a frame tells them apart: a frame painted, or arriving,
|
|
153
|
-
* while one is taken is left out, the page's own included. A crop on the page's own session is
|
|
154
|
-
* drawn at the crop's size, so there only a frame of another size than the page's is left out.
|
|
155
|
-
*/
|
|
156
|
-
const pictureWindows = (now: () => number) => {
|
|
157
|
-
let windows: ReadonlyArray<Excluded> = [];
|
|
158
|
-
|
|
159
|
-
return {
|
|
160
|
-
drawn: ({ hostTime, timing, receivedAt }: Frame, foreign: boolean) =>
|
|
161
|
-
windows.some((window) => {
|
|
162
|
-
const until = window.until + Duration.toMillis(settling);
|
|
163
|
-
|
|
164
|
-
return (
|
|
165
|
-
(foreign || !window.resized) &&
|
|
166
|
-
((hostTime + timing.uncertaintyMillis >= window.from &&
|
|
167
|
-
hostTime - timing.uncertaintyMillis <= until) ||
|
|
168
|
-
(receivedAt >= window.from && receivedAt <= until))
|
|
169
|
-
);
|
|
170
|
-
}),
|
|
171
|
-
excluding: <A, E, R>(picture: Effect.Effect<A, E, R>, resized = false) =>
|
|
172
|
-
Effect.acquireUseRelease(
|
|
173
|
-
Effect.sync(() => {
|
|
174
|
-
const window = { from: now(), until: Number.POSITIVE_INFINITY, resized };
|
|
175
|
-
const since = window.from - Duration.toMillis(excludedFor);
|
|
176
|
-
|
|
177
|
-
windows = [...windows.filter((kept) => kept.until > since), window];
|
|
178
|
-
|
|
179
|
-
return window;
|
|
180
|
-
}),
|
|
181
|
-
() => picture,
|
|
182
|
-
(window) =>
|
|
183
|
-
Effect.sync(() => {
|
|
184
|
-
window.until = now();
|
|
185
|
-
}),
|
|
186
|
-
),
|
|
187
|
-
};
|
|
188
|
-
};
|
|
189
|
-
|
|
190
131
|
/**
|
|
191
132
|
* A native frame as readers get it: its picture's size, else the capture's or its device's, and
|
|
192
133
|
* its paint mapped by the browser's newest estimate, which may be older than the frame and says
|
|
@@ -331,7 +272,6 @@ const sizeFilter = (
|
|
|
331
272
|
expected = viewport;
|
|
332
273
|
},
|
|
333
274
|
/** Whether a frame of this device size shows the page, as far as the capture knows. */
|
|
334
|
-
expects: (device: Size) => expected !== null && sameSize(device, expected),
|
|
335
275
|
offer: (frame: Frame, timestamp: number, image: Size | undefined, device: Size) => {
|
|
336
276
|
if (image !== undefined && !sameShape(image, device)) {
|
|
337
277
|
drop(1);
|
|
@@ -426,7 +366,7 @@ const isCurrent = (
|
|
|
426
366
|
latest: Option.Option<Frame>,
|
|
427
367
|
) =>
|
|
428
368
|
generation !== undefined &&
|
|
429
|
-
generation.
|
|
369
|
+
generation.phase === "Capturing" &&
|
|
430
370
|
generation.failure === undefined &&
|
|
431
371
|
Option.getOrUndefined(latest) === frame &&
|
|
432
372
|
generation.transport.frameTag().document === frame.document;
|
|
@@ -460,6 +400,63 @@ const frames = (
|
|
|
460
400
|
);
|
|
461
401
|
};
|
|
462
402
|
|
|
403
|
+
const renewals = Effect.fnUntraced(function* (options: Options) {
|
|
404
|
+
const renewing = yield* Queue.sliding<void>(1);
|
|
405
|
+
|
|
406
|
+
yield* Queue.take(renewing).pipe(
|
|
407
|
+
Effect.andThen(options.renew),
|
|
408
|
+
Effect.andThen(Effect.sleep(Duration.seconds(1))),
|
|
409
|
+
Effect.provideService(Clock.Clock, options.clock),
|
|
410
|
+
Effect.forever,
|
|
411
|
+
Effect.forkScoped,
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
return renewing;
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
const receive =
|
|
418
|
+
(
|
|
419
|
+
options: Options,
|
|
420
|
+
current: Generation,
|
|
421
|
+
replies: Set<Promise<void>>,
|
|
422
|
+
counts: ReturnType<typeof tally>,
|
|
423
|
+
sizes: Effect.Success<ReturnType<typeof sizeFilter>>,
|
|
424
|
+
renewing: Queue.Queue<void>,
|
|
425
|
+
fail: (current: Generation, error: BrowserError) => void,
|
|
426
|
+
) =>
|
|
427
|
+
(native: NativeFrame) => {
|
|
428
|
+
if (current.phase === "Stopped") return;
|
|
429
|
+
counts.count("received");
|
|
430
|
+
if (replies.size >= replyCapacity) {
|
|
431
|
+
fail(current, options.error(new Error("screencast acknowledgement capacity exceeded")));
|
|
432
|
+
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const reply = current.transport
|
|
437
|
+
.acknowledge(native.sessionId)
|
|
438
|
+
.then(
|
|
439
|
+
() => undefined,
|
|
440
|
+
(cause: unknown) => {
|
|
441
|
+
if (current.phase !== "Stopped") fail(current, options.error(cause));
|
|
442
|
+
},
|
|
443
|
+
)
|
|
444
|
+
.then(() => void replies.delete(reply));
|
|
445
|
+
|
|
446
|
+
replies.add(reply);
|
|
447
|
+
const browserSeconds = native.metadata.timestamp;
|
|
448
|
+
const timestamp = browserSeconds === undefined ? Number.NaN : browserSeconds * 1000;
|
|
449
|
+
|
|
450
|
+
if (!Number.isFinite(timestamp)) return counts.count("missingTimestamp");
|
|
451
|
+
if (current.predecessor !== undefined && timestamp <= current.predecessor)
|
|
452
|
+
return counts.count("late");
|
|
453
|
+
Queue.offerUnsafe(renewing, undefined);
|
|
454
|
+
const { frame, image, device } = timed(options, native, timestamp, current);
|
|
455
|
+
|
|
456
|
+
if (current.phase === "Paused") return counts.count("duringPictures");
|
|
457
|
+
sizes.offer(frame, timestamp, image, device);
|
|
458
|
+
};
|
|
459
|
+
|
|
463
460
|
export const make = (options: Options) =>
|
|
464
461
|
Effect.gen(function* () {
|
|
465
462
|
const lock = yield* Semaphore.make(1);
|
|
@@ -468,9 +465,8 @@ export const make = (options: Options) =>
|
|
|
468
465
|
// Done as the page's scope closes, once the page has gone and its browser's loss, if any, is
|
|
469
466
|
// known.
|
|
470
467
|
const closed = yield* Deferred.make<void>();
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
let unsettledStop: Promise<void> | undefined;
|
|
468
|
+
const pageScope = yield* Effect.scope;
|
|
469
|
+
const isClosed = () => pageScope.state._tag === "Closed" || Deferred.isDoneUnsafe(closed);
|
|
474
470
|
let latest = Option.none<Frame>();
|
|
475
471
|
let history: ReadonlyArray<Frame> = [];
|
|
476
472
|
const keepMillis = Duration.toMillis(options.frameHistory);
|
|
@@ -478,7 +474,6 @@ export const make = (options: Options) =>
|
|
|
478
474
|
const counts = tally(now);
|
|
479
475
|
// Frames delivered, which number them so that a reader can tell what it missed.
|
|
480
476
|
let delivered = 0;
|
|
481
|
-
const pictures = pictureWindows(now);
|
|
482
477
|
|
|
483
478
|
const current = (frame: Frame) => isCurrent(frame, generation, latest);
|
|
484
479
|
|
|
@@ -506,75 +501,105 @@ export const make = (options: Options) =>
|
|
|
506
501
|
|
|
507
502
|
// On air a capture runs for hours, and its frames are timed by the browser's newest estimate,
|
|
508
503
|
// so arriving frames renew it: a check at most each second, a measurement once it is due.
|
|
509
|
-
const renewing = yield*
|
|
510
|
-
|
|
511
|
-
yield* Queue.take(renewing).pipe(
|
|
512
|
-
Effect.andThen(options.renew),
|
|
513
|
-
Effect.andThen(Effect.sleep(Duration.seconds(1))),
|
|
514
|
-
Effect.provideService(Clock.Clock, options.clock),
|
|
515
|
-
Effect.forever,
|
|
516
|
-
Effect.forkScoped,
|
|
517
|
-
);
|
|
504
|
+
const renewing = yield* renewals(options);
|
|
518
505
|
|
|
519
506
|
const notifyFailure = (current: Generation, failure: BrowserError) => {
|
|
520
507
|
if (current.failure !== undefined) return;
|
|
521
508
|
current.failure = failure;
|
|
522
509
|
for (const subscriber of current.subscribers)
|
|
523
510
|
Queue.offerUnsafe(subscriber, { _tag: "Failure", error: failure });
|
|
511
|
+
stop(current);
|
|
524
512
|
};
|
|
525
513
|
|
|
526
|
-
const
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
current.
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
(
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
514
|
+
const failed = (current: Generation) => (cause: unknown) =>
|
|
515
|
+
notifyFailure(current, options.error(cause));
|
|
516
|
+
|
|
517
|
+
const stopNative = (current: Generation) =>
|
|
518
|
+
pictures.track("screencast stop", current.transport.stop(), failed(current));
|
|
519
|
+
|
|
520
|
+
const startNative = (current: Generation) =>
|
|
521
|
+
pictures.track(
|
|
522
|
+
"screencast start",
|
|
523
|
+
current.transport.start({
|
|
524
|
+
format: "jpeg",
|
|
525
|
+
quality: current.quality,
|
|
526
|
+
...(current.size === null
|
|
527
|
+
? {}
|
|
528
|
+
: { maxWidth: current.size.width, maxHeight: current.size.height }),
|
|
529
|
+
}),
|
|
530
|
+
failed(current),
|
|
539
531
|
);
|
|
540
532
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
if (unsettledStop === reply) unsettledStop = undefined;
|
|
545
|
-
});
|
|
546
|
-
};
|
|
533
|
+
const stop = (current: Generation) => {
|
|
534
|
+
if (current.phase === "Stopped") return;
|
|
535
|
+
const paused = current.phase === "Paused";
|
|
547
536
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
537
|
+
current.phase = "Stopped";
|
|
538
|
+
sizes.end();
|
|
539
|
+
current.unlisten();
|
|
540
|
+
// A paused capture already owns its stop, including a reply still in flight.
|
|
541
|
+
if (!paused) void stopNative(current);
|
|
551
542
|
};
|
|
552
543
|
|
|
553
544
|
const gone = () => options.error(new Error("Target page has been closed"));
|
|
554
|
-
const stopDeadline = () => options.error(new Error("screencast stop exceeded its deadline"));
|
|
555
545
|
const why = told(closed, gone, options.clock);
|
|
556
546
|
|
|
557
|
-
// A stop's reply, awaited for at most `deadline`; `late` is what a stop past it means.
|
|
558
|
-
const settled = <E>(reply: () => Promise<void> | undefined, late: Effect.Effect<void, E>) =>
|
|
559
|
-
Effect.suspend(() => {
|
|
560
|
-
const pending = reply();
|
|
561
|
-
|
|
562
|
-
return pending === undefined
|
|
563
|
-
? Effect.void
|
|
564
|
-
: Effect.promise(() => pending).pipe(
|
|
565
|
-
Effect.interruptible,
|
|
566
|
-
Effect.timeoutOrElse({ duration: deadline, orElse: () => late }),
|
|
567
|
-
);
|
|
568
|
-
}).pipe(Effect.provideService(Clock.Clock, options.clock));
|
|
569
|
-
|
|
570
547
|
const awaitStop = (current: Generation) =>
|
|
571
|
-
|
|
572
|
-
()
|
|
573
|
-
Effect.sync(() => notifyFailure(current,
|
|
548
|
+
pictures
|
|
549
|
+
.wait("screencast stop")
|
|
550
|
+
.pipe(Effect.catch((error) => Effect.sync(() => notifyFailure(current, error))));
|
|
551
|
+
|
|
552
|
+
const healthy = (current: Generation) =>
|
|
553
|
+
Effect.suspend(() =>
|
|
554
|
+
current.failure === undefined ? Effect.void : Effect.fail(current.failure),
|
|
574
555
|
);
|
|
575
556
|
|
|
576
|
-
|
|
577
|
-
|
|
557
|
+
const pictures = Quiescence.make({
|
|
558
|
+
clock: options.clock,
|
|
559
|
+
error: options.error,
|
|
560
|
+
closed: isClosed,
|
|
561
|
+
current: () => generation,
|
|
562
|
+
fail: (current, error) => {
|
|
563
|
+
if (current !== undefined) notifyFailure(current, error);
|
|
564
|
+
},
|
|
565
|
+
pause: (current): Effect.Effect<void, BrowserError> =>
|
|
566
|
+
Effect.gen(function* () {
|
|
567
|
+
if (current?.phase !== "Capturing" || current.failure !== undefined) return;
|
|
568
|
+
current.phase = "Paused";
|
|
569
|
+
latest = Option.none();
|
|
570
|
+
sizes.end();
|
|
571
|
+
void stopNative(current);
|
|
572
|
+
yield* pictures
|
|
573
|
+
.wait("screencast stop")
|
|
574
|
+
.pipe(Effect.tapError((error) => Effect.sync(() => notifyFailure(current, error))));
|
|
575
|
+
yield* healthy(current);
|
|
576
|
+
}),
|
|
577
|
+
resume: (current): Effect.Effect<void, BrowserError> =>
|
|
578
|
+
Effect.gen(function* () {
|
|
579
|
+
const resumable = () => {
|
|
580
|
+
if (current === undefined || generation !== current) return false;
|
|
581
|
+
if (current.subscribers.size === 0) {
|
|
582
|
+
stop(current);
|
|
583
|
+
generation = undefined;
|
|
584
|
+
|
|
585
|
+
return false;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
return current.phase !== "Stopped" && current.failure === undefined && !isClosed();
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
if (current === undefined || !resumable()) return;
|
|
592
|
+
const viewport = yield* readViewport(options);
|
|
593
|
+
|
|
594
|
+
if (!resumable()) return;
|
|
595
|
+
sizes.restart(viewport);
|
|
596
|
+
// Its only quiet-page frame can precede the native start acknowledgement.
|
|
597
|
+
current.phase = "Capturing";
|
|
598
|
+
void startNative(current);
|
|
599
|
+
yield* pictures.wait("screencast start");
|
|
600
|
+
yield* healthy(current);
|
|
601
|
+
}),
|
|
602
|
+
});
|
|
578
603
|
|
|
579
604
|
// A reader leaves; the last one stops its generation.
|
|
580
605
|
const leave = (current: Generation, subscription: Queue.Queue<Envelope>) =>
|
|
@@ -590,67 +615,29 @@ export const make = (options: Options) =>
|
|
|
590
615
|
const acquire = (screencast: ScreencastOptions) =>
|
|
591
616
|
lock.withPermits(1)(
|
|
592
617
|
Effect.gen(function* () {
|
|
593
|
-
if (
|
|
618
|
+
if (isClosed()) return yield* gone();
|
|
594
619
|
let current = generation;
|
|
595
620
|
const starting = current === undefined;
|
|
596
621
|
|
|
597
622
|
if (current === undefined) {
|
|
598
|
-
yield*
|
|
623
|
+
yield* pictures.idle;
|
|
599
624
|
const [transport, { calibration, viewport }] = yield* prepare(options, screencast);
|
|
600
625
|
const size = screencast.size ?? viewport;
|
|
601
626
|
|
|
602
|
-
if (
|
|
627
|
+
if (isClosed()) return yield* gone();
|
|
603
628
|
|
|
604
|
-
|
|
629
|
+
current = {
|
|
605
630
|
subscribers: new Set(),
|
|
606
631
|
transport,
|
|
607
632
|
unlisten: () => undefined,
|
|
608
633
|
calibration,
|
|
609
634
|
quality: screencast.quality ?? 80,
|
|
610
635
|
size,
|
|
611
|
-
|
|
636
|
+
phase: "Capturing",
|
|
612
637
|
predecessor: undefined,
|
|
613
638
|
failure: undefined,
|
|
614
|
-
stopReply: undefined,
|
|
615
|
-
onFrame: (native) => {
|
|
616
|
-
if (!created.accepting) return;
|
|
617
|
-
counts.count("received");
|
|
618
|
-
if (replies.size >= replyCapacity) {
|
|
619
|
-
fail(
|
|
620
|
-
created,
|
|
621
|
-
options.error(new Error("screencast acknowledgement capacity exceeded")),
|
|
622
|
-
);
|
|
623
|
-
|
|
624
|
-
return;
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
const reply = created.transport.acknowledge(native.sessionId).then(
|
|
628
|
-
() => {
|
|
629
|
-
replies.delete(reply);
|
|
630
|
-
},
|
|
631
|
-
(cause: unknown) => {
|
|
632
|
-
replies.delete(reply);
|
|
633
|
-
if (created.accepting) fail(created, options.error(cause));
|
|
634
|
-
},
|
|
635
|
-
);
|
|
636
|
-
|
|
637
|
-
replies.add(reply);
|
|
638
|
-
const browserSeconds = native.metadata.timestamp;
|
|
639
|
-
const timestamp = browserSeconds === undefined ? Number.NaN : browserSeconds * 1000;
|
|
640
|
-
|
|
641
|
-
if (!Number.isFinite(timestamp)) return counts.count("missingTimestamp");
|
|
642
|
-
if (created.predecessor !== undefined && timestamp <= created.predecessor)
|
|
643
|
-
return counts.count("late");
|
|
644
|
-
Queue.offerUnsafe(renewing, undefined);
|
|
645
|
-
const { frame, image, device } = timed(options, native, timestamp, created);
|
|
646
|
-
|
|
647
|
-
if (pictures.drawn(frame, !sizes.expects(device)))
|
|
648
|
-
return counts.count("duringPictures");
|
|
649
|
-
sizes.offer(frame, timestamp, image, device);
|
|
650
|
-
},
|
|
651
639
|
};
|
|
652
640
|
|
|
653
|
-
current = created;
|
|
654
641
|
generation = current;
|
|
655
642
|
latest = Option.none();
|
|
656
643
|
sizes.restart(viewport);
|
|
@@ -683,27 +670,19 @@ export const make = (options: Options) =>
|
|
|
683
670
|
const frame = currentFrame(current, latest);
|
|
684
671
|
|
|
685
672
|
if (starting) {
|
|
686
|
-
const { transport, quality, size } = current;
|
|
687
673
|
const listening = current;
|
|
688
674
|
|
|
689
|
-
current.unlisten = transport.listen({
|
|
690
|
-
frame: current
|
|
691
|
-
lost: (error) =>
|
|
675
|
+
current.unlisten = listening.transport.listen({
|
|
676
|
+
frame: receive(options, current, replies, counts, sizes, renewing, notifyFailure),
|
|
677
|
+
lost: (error) => notifyFailure(listening, error),
|
|
692
678
|
});
|
|
693
679
|
|
|
694
|
-
const screencast = Effect.
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
format: "jpeg",
|
|
698
|
-
quality,
|
|
699
|
-
...(size === null ? {} : { maxWidth: size.width, maxHeight: size.height }),
|
|
700
|
-
}),
|
|
701
|
-
catch: options.error,
|
|
702
|
-
});
|
|
680
|
+
const screencast = Effect.promise(() => startNative(listening)).pipe(
|
|
681
|
+
Effect.andThen(healthy(listening)),
|
|
682
|
+
);
|
|
703
683
|
|
|
704
|
-
const started = yield*
|
|
705
|
-
|
|
706
|
-
}).pipe(
|
|
684
|
+
const started = yield* options.watch.pipe(
|
|
685
|
+
Effect.zip(screencast, { concurrent: true }),
|
|
707
686
|
Effect.interruptible,
|
|
708
687
|
Effect.timeoutOrElse({
|
|
709
688
|
duration: deadline,
|
|
@@ -727,7 +706,9 @@ export const make = (options: Options) =>
|
|
|
727
706
|
|
|
728
707
|
const open: Controller["open"] = (screencast = {}) =>
|
|
729
708
|
Effect.acquireRelease(acquire(screencast), ({ current, subscription }) =>
|
|
730
|
-
|
|
709
|
+
Effect.sync(() => current.subscribers.delete(subscription)).pipe(
|
|
710
|
+
Effect.andThen(lock.withPermits(1)(leave(current, subscription))),
|
|
711
|
+
),
|
|
731
712
|
).pipe(
|
|
732
713
|
Effect.map((lease) => ({
|
|
733
714
|
current: lease.frame,
|
|
@@ -739,16 +720,19 @@ export const make = (options: Options) =>
|
|
|
739
720
|
|
|
740
721
|
// The page's scope closes once the page has gone, when its browser's loss, if any, is known.
|
|
741
722
|
yield* Effect.addFinalizer(() =>
|
|
742
|
-
|
|
743
|
-
Effect.
|
|
744
|
-
|
|
723
|
+
Effect.sync(() => Deferred.doneUnsafe(closed, Exit.void)).pipe(
|
|
724
|
+
Effect.andThen(
|
|
725
|
+
lock.withPermits(1)(
|
|
726
|
+
Effect.suspend(() => {
|
|
727
|
+
const current = generation;
|
|
745
728
|
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
fail(current, gone());
|
|
729
|
+
if (current === undefined) return Effect.void;
|
|
730
|
+
notifyFailure(current, gone());
|
|
749
731
|
|
|
750
|
-
|
|
751
|
-
|
|
732
|
+
return awaitStop(current);
|
|
733
|
+
}),
|
|
734
|
+
),
|
|
735
|
+
),
|
|
752
736
|
),
|
|
753
737
|
);
|
|
754
738
|
|
|
@@ -758,7 +742,7 @@ export const make = (options: Options) =>
|
|
|
758
742
|
Stream.unwrap(Effect.map(open(options), ({ stream }) => stream)),
|
|
759
743
|
latest: Effect.sync(() => latest),
|
|
760
744
|
recent: Effect.sync(() => history),
|
|
761
|
-
active: Effect.sync(() => generation
|
|
745
|
+
active: Effect.sync(() => generation !== undefined && generation.phase !== "Stopped"),
|
|
762
746
|
sync: Effect.suspend(() => synced(generation, why, options.error)),
|
|
763
747
|
current,
|
|
764
748
|
confirm: confirmation(
|
|
@@ -767,7 +751,7 @@ export const make = (options: Options) =>
|
|
|
767
751
|
why,
|
|
768
752
|
options.error,
|
|
769
753
|
),
|
|
770
|
-
excluding: pictures.excluding,
|
|
754
|
+
excluding: (picture) => lock.withPermits(1)(pictures.excluding(picture)),
|
|
771
755
|
stats: (windowMillis?: number) =>
|
|
772
756
|
Effect.sync(
|
|
773
757
|
() => new CaptureStats({ ...counts.read(windowMillis), ackBacklog: replies.size }),
|