effect-browser 0.3.0-beta.16 → 0.3.0-beta.19
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 +12 -4
- package/dist/{Browser-DRPthkNO.mjs → Browser-H63wiekr.mjs} +4 -4
- package/dist/Browser-H63wiekr.mjs.map +1 -0
- package/dist/Browser.d.mts +1 -1
- package/dist/Browser.mjs +1 -1
- package/dist/Cdp.mjs +2 -2
- package/dist/Chromium.mjs +2 -2
- package/dist/Expect.d.mts +1 -1
- package/dist/Expect.mjs +2 -2
- package/dist/Moment.d.mts +1 -1
- package/dist/Moment.mjs +3 -3
- package/dist/Moment.mjs.map +1 -1
- package/dist/{Page-C_VkBVwI.mjs → Page-CfB6Zn1p.mjs} +3 -7
- package/dist/Page-CfB6Zn1p.mjs.map +1 -0
- package/dist/{Page-jHmSJeOS.d.mts → Page-ku6j5t-v.d.mts} +18 -13
- package/dist/Page.d.mts +1 -1
- package/dist/Page.mjs +1 -1
- package/dist/{Plan-X-JkACAB.mjs → Plan-C-d5QYS8.mjs} +3 -3
- package/dist/{Plan-X-JkACAB.mjs.map → Plan-C-d5QYS8.mjs.map} +1 -1
- package/dist/{Plan-DLtRksuu.d.mts → Plan-DJ4hEEna.d.mts} +2 -2
- package/dist/Plan.d.mts +1 -1
- package/dist/Plan.mjs +1 -1
- package/dist/Policy.d.mts +1 -1
- package/dist/Policy.mjs +1 -1
- package/dist/Presentation.d.mts +1 -1
- package/dist/Presentation.mjs +2 -2
- package/dist/Stage.d.mts +1 -1
- package/dist/Stage.mjs +7 -6
- package/dist/Stage.mjs.map +1 -1
- package/dist/{Supervisor-BgWP8o9t.mjs → Supervisor-2mPjIYYs.mjs} +4 -3
- package/dist/{Supervisor-BgWP8o9t.mjs.map → Supervisor-2mPjIYYs.mjs.map} +1 -1
- package/dist/Supervisor.mjs +1 -1
- package/dist/{context-rYTk-3Cv.mjs → context-w3NJPRLo.mjs} +15 -8
- package/dist/context-w3NJPRLo.mjs.map +1 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +4 -4
- package/dist/{page-ClEMFnBG.mjs → page-BKwfF6z-.mjs} +169 -112
- package/dist/page-BKwfF6z-.mjs.map +1 -0
- package/package.json +1 -1
- package/src/Moment.ts +3 -6
- package/src/Page.ts +13 -11
- package/src/Stage.ts +8 -16
- package/src/Supervisor.ts +6 -7
- package/src/internal/input/actions.ts +28 -64
- package/src/internal/input/dispatch.ts +22 -7
- package/src/internal/input/edit.inpage.ts +23 -2
- package/src/internal/input/evidence.inpage.ts +0 -2
- package/src/internal/input/guard.ts +3 -9
- package/src/internal/input/perform.ts +14 -10
- package/src/internal/input/pointer.ts +3 -6
- package/src/internal/input/replies.ts +19 -8
- package/src/internal/input/settle.ts +3 -2
- package/src/internal/input/targets.inpage.ts +2 -5
- package/src/internal/input/targets.ts +5 -21
- package/src/internal/page/bridge.ts +2 -2
- package/src/internal/page/context.ts +13 -1
- package/src/internal/page/navigation.ts +3 -12
- package/src/internal/pictures/capture.ts +7 -0
- package/src/internal/pictures/motion.ts +65 -0
- package/src/internal/pictures/pictures.ts +20 -16
- package/src/internal/pictures/utilsBundle.d.ts +9 -0
- package/src/internal/reading/reading.ts +5 -9
- package/src/internal/reading/ready.ts +66 -14
- package/src/internal/timeline/events.ts +5 -8
- package/src/internal/timeline/window.ts +3 -8
- package/dist/Browser-DRPthkNO.mjs.map +0 -1
- package/dist/Page-C_VkBVwI.mjs.map +0 -1
- package/dist/context-rYTk-3Cv.mjs.map +0 -1
- package/dist/page-ClEMFnBG.mjs.map +0 -1
|
@@ -8,7 +8,7 @@ import { Effect } from "effect";
|
|
|
8
8
|
import { InvalidRequest, NotActionable, StaleRef } from "../../BrowserError.ts";
|
|
9
9
|
import { ResolvedTarget, type Target } from "../../Page.ts";
|
|
10
10
|
import { scriptCall } from "../page/bridge.ts";
|
|
11
|
-
import { decodeWith, failWith } from "../page/context.ts";
|
|
11
|
+
import { decodeWith, failWith, refuse } from "../page/context.ts";
|
|
12
12
|
import type { Viewport } from "../page/viewport.ts";
|
|
13
13
|
import type { Dispatch } from "./dispatch.ts";
|
|
14
14
|
import { type Approval, check, type Guard } from "./guard.ts";
|
|
@@ -31,18 +31,8 @@ const pointFailure = (
|
|
|
31
31
|
: new NotActionable({ detail: result.detail }),
|
|
32
32
|
);
|
|
33
33
|
|
|
34
|
-
const resolvedTarget = (
|
|
35
|
-
new ResolvedTarget({
|
|
36
|
-
point: { x: result.x, y: result.y },
|
|
37
|
-
element: result.element,
|
|
38
|
-
tag: result.tag,
|
|
39
|
-
role: result.role,
|
|
40
|
-
name: result.name,
|
|
41
|
-
context: result.context,
|
|
42
|
-
box: result.box,
|
|
43
|
-
cursor: result.cursor,
|
|
44
|
-
...(result.href === undefined ? {} : { href: result.href }),
|
|
45
|
-
});
|
|
34
|
+
const resolvedTarget = ({ x, y, box, ...target }: Script.ResolvedPoint): ResolvedTarget =>
|
|
35
|
+
new ResolvedTarget({ point: { x, y }, box, ...target });
|
|
46
36
|
|
|
47
37
|
export const make = (
|
|
48
38
|
viewport: Viewport,
|
|
@@ -67,15 +57,9 @@ export const make = (
|
|
|
67
57
|
|
|
68
58
|
const resolve = (operation: string, target: Target, approval?: Approval, scroll = true) => {
|
|
69
59
|
if (typeof target === "string" && !/^e\d+$/.test(target))
|
|
70
|
-
return
|
|
71
|
-
operation,
|
|
72
|
-
new InvalidRequest({ detail: target + " is not a ref; refs look like e12" }),
|
|
73
|
-
);
|
|
60
|
+
return refuse(operation, target + " is not a ref; refs look like e12");
|
|
74
61
|
if (typeof target !== "string" && (!Number.isFinite(target.x) || !Number.isFinite(target.y)))
|
|
75
|
-
return
|
|
76
|
-
operation,
|
|
77
|
-
new InvalidRequest({ detail: "point coordinates must be finite" }),
|
|
78
|
-
);
|
|
62
|
+
return refuse(operation, "point coordinates must be finite");
|
|
79
63
|
|
|
80
64
|
return readPoint(operation, target, approval, scroll).pipe(
|
|
81
65
|
Effect.flatMap((result) =>
|
|
@@ -90,7 +90,7 @@ const install = (
|
|
|
90
90
|
): PageApi => {
|
|
91
91
|
const installed = globalThis.__effectBrowser;
|
|
92
92
|
|
|
93
|
-
if (installed !== undefined && installed.version ===
|
|
93
|
+
if (installed !== undefined && installed.version === 20) return installed;
|
|
94
94
|
const named = makeNames();
|
|
95
95
|
const walked = makeWalk(named);
|
|
96
96
|
const placing = makeContext(named, walked);
|
|
@@ -108,7 +108,7 @@ const install = (
|
|
|
108
108
|
const readiness = makeReady(walked, texts, kept, recorder);
|
|
109
109
|
|
|
110
110
|
const api: PageApi = {
|
|
111
|
-
version:
|
|
111
|
+
version: 20,
|
|
112
112
|
snapshot: read.snapshot,
|
|
113
113
|
find: subjected.find,
|
|
114
114
|
until: makeExpectations(named, subjected).until,
|
|
@@ -17,7 +17,14 @@ import {
|
|
|
17
17
|
import type { CDPSession, Page as PlaywrightPage } from "playwright-core";
|
|
18
18
|
|
|
19
19
|
import type { CaptureSource } from "../../Browser.ts";
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
BrowserError,
|
|
22
|
+
Closed,
|
|
23
|
+
Failed,
|
|
24
|
+
InvalidRequest,
|
|
25
|
+
type Reason,
|
|
26
|
+
Timeout,
|
|
27
|
+
} from "../../BrowserError.ts";
|
|
21
28
|
import type { BrowserEvent } from "../../BrowserEvent.ts";
|
|
22
29
|
import type { InputGuard, Point } from "../../Page.ts";
|
|
23
30
|
import type { Gate } from "../input/gate.ts";
|
|
@@ -159,6 +166,10 @@ export const undispatched = (operation: string, reason: Reason) =>
|
|
|
159
166
|
export const failWith = (operation: string, reason: Reason) =>
|
|
160
167
|
Effect.fail(undispatched(operation, reason));
|
|
161
168
|
|
|
169
|
+
/** Refuse a request to `operation` that could never succeed, before sending anything. */
|
|
170
|
+
export const refuse = (operation: string, detail: string) =>
|
|
171
|
+
failWith(operation, new InvalidRequest({ detail }));
|
|
172
|
+
|
|
162
173
|
export const decodeWith =
|
|
163
174
|
<A>(operation: string, schema: Schema.Codec<A, unknown>) =>
|
|
164
175
|
(value: unknown) =>
|
|
@@ -308,6 +319,7 @@ export const make = (options: MakeOptions, scope: Scope.Scope) => {
|
|
|
308
319
|
lane,
|
|
309
320
|
pointer,
|
|
310
321
|
now,
|
|
322
|
+
scope,
|
|
311
323
|
owned,
|
|
312
324
|
span,
|
|
313
325
|
protocol,
|
|
@@ -5,17 +5,11 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { Deferred, Duration, Effect, Schedule } from "effect";
|
|
7
7
|
|
|
8
|
-
import {
|
|
9
|
-
BrowserError,
|
|
10
|
-
InvalidRequest,
|
|
11
|
-
NavigationFailed,
|
|
12
|
-
NotActionable,
|
|
13
|
-
NotFound,
|
|
14
|
-
} from "../../BrowserError.ts";
|
|
8
|
+
import { BrowserError, NavigationFailed, NotActionable, NotFound } from "../../BrowserError.ts";
|
|
15
9
|
import type { Guard } from "../input/guard.ts";
|
|
16
10
|
import type { InputMarks, Perform } from "../input/perform.ts";
|
|
17
11
|
import { Current } from "../input/replies.ts";
|
|
18
|
-
import { failWith, type PageContext } from "./context.ts";
|
|
12
|
+
import { failWith, type PageContext, refuse } from "./context.ts";
|
|
19
13
|
import * as Url from "./url.ts";
|
|
20
14
|
|
|
21
15
|
export const make = (
|
|
@@ -157,10 +151,7 @@ export const make = (
|
|
|
157
151
|
parsed === null ||
|
|
158
152
|
!["http:", "https:", "about:", "data:", "file:"].includes(parsed.protocol)
|
|
159
153
|
)
|
|
160
|
-
return
|
|
161
|
-
"navigate",
|
|
162
|
-
new InvalidRequest({ detail: `"${Url.redact(url)}" is not a URL` }),
|
|
163
|
-
);
|
|
154
|
+
return refuse("navigate", `"${Url.redact(url)}" is not a URL`);
|
|
164
155
|
|
|
165
156
|
return navigation(
|
|
166
157
|
"navigate",
|
|
@@ -47,6 +47,8 @@ interface Options {
|
|
|
47
47
|
readonly frameHistory: Duration.Duration;
|
|
48
48
|
/** The viewport in CSS pixels; a capture is scaled to fit it, as screenshots are. */
|
|
49
49
|
readonly viewport: Effect.Effect<Size, BrowserError>;
|
|
50
|
+
/** Copies the restored whole surface before native capture can restart. */
|
|
51
|
+
readonly restore: () => Promise<unknown>;
|
|
50
52
|
readonly imageSize: (data: Uint8Array) => Size | undefined;
|
|
51
53
|
readonly error: (cause: unknown) => BrowserError;
|
|
52
54
|
}
|
|
@@ -589,6 +591,11 @@ export const make = (options: Options) =>
|
|
|
589
591
|
};
|
|
590
592
|
|
|
591
593
|
if (current === undefined || !resumable()) return;
|
|
594
|
+
// Screenshot replies restore emulation before the compositor has necessarily painted
|
|
595
|
+
// it. Copy the whole restored surface while stopped, retaining its actual native reply.
|
|
596
|
+
void pictures.track("picture restoration", options.restore(), failed(current));
|
|
597
|
+
yield* pictures.wait("picture restoration");
|
|
598
|
+
yield* healthy(current);
|
|
592
599
|
const viewport = yield* readViewport(options);
|
|
593
600
|
|
|
594
601
|
if (!resumable()) return;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a page's screen moves, from its screencast frames: each frame decoded and cut into
|
|
3
|
+
* 16-pixel blocks, a block moving where a few of its pixels, sampled every other one, changed by
|
|
4
|
+
* more than JPEG's noise in any color channel.
|
|
5
|
+
* What moved among the frames before an input is the page's background, such as an idle
|
|
6
|
+
* animation; foreground stillness ignores it.
|
|
7
|
+
*
|
|
8
|
+
* Frames are decoded with the JPEG decoder playwright-core bundles and exports as
|
|
9
|
+
* `playwright-core/lib/utilsBundle`: effect-browser depends on nothing else, and the runtime has
|
|
10
|
+
* no image decoder. That entry point has no types, so `utilsBundle.d.ts` declares what is used.
|
|
11
|
+
*/
|
|
12
|
+
// oxlint-disable-next-line typescript/triple-slash-reference -- a declaration of an untyped module cannot be imported, and every program that compiles this source needs it
|
|
13
|
+
/// <reference path="./utilsBundle.d.ts" />
|
|
14
|
+
import { jpegjs } from "playwright-core/lib/utilsBundle";
|
|
15
|
+
|
|
16
|
+
import type { Frame } from "../../Frame.ts";
|
|
17
|
+
|
|
18
|
+
/** A decoded frame: RGBA, `width` pixels a row. */
|
|
19
|
+
export type Picture = ReturnType<typeof jpegjs.decode>;
|
|
20
|
+
|
|
21
|
+
export const picture = (frame: Frame): Picture => jpegjs.decode(frame.data, { useTArray: true });
|
|
22
|
+
|
|
23
|
+
/** A sample changed where any color channel moved more than this, beyond JPEG's noise. */
|
|
24
|
+
const noise = 32;
|
|
25
|
+
|
|
26
|
+
/** The blocks where two pictures differ, by `row * columns + column`; -1 for different sizes. */
|
|
27
|
+
const moved = (from: Picture, to: Picture): ReadonlyArray<number> => {
|
|
28
|
+
if (from.width !== to.width || from.height !== to.height) return [-1];
|
|
29
|
+
const columns = Math.ceil(from.width / 16);
|
|
30
|
+
const counts = new Map<number, number>();
|
|
31
|
+
const differs = (at: number) => Math.abs((from.data[at] ?? 0) - (to.data[at] ?? 0)) > noise;
|
|
32
|
+
|
|
33
|
+
for (let y = 0; y < from.height; y += 2)
|
|
34
|
+
for (let x = 0; x < from.width; x += 2) {
|
|
35
|
+
const at = (y * from.width + x) * 4;
|
|
36
|
+
const block = Math.floor(y / 16) * columns + Math.floor(x / 16);
|
|
37
|
+
|
|
38
|
+
if (differs(at) || differs(at + 1) || differs(at + 2))
|
|
39
|
+
counts.set(block, (counts.get(block) ?? 0) + 1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// At least three changed samples, so a stray one is no motion.
|
|
43
|
+
return [...counts].flatMap(([block, count]) => (count >= 3 ? [block] : []));
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** The blocks that moved from one picture to the next and their neighbours; none past a resize. */
|
|
47
|
+
export const background = (pictures: ReadonlyArray<Picture>): ReadonlySet<number> => {
|
|
48
|
+
const columns = Math.ceil((pictures[0]?.width ?? 0) / 16);
|
|
49
|
+
|
|
50
|
+
const around = (block: number) =>
|
|
51
|
+
[-1, 0, 1].flatMap((row) =>
|
|
52
|
+
[-1, 0, 1]
|
|
53
|
+
.filter((step) => block >= 0 && (block % columns) + step >= 0)
|
|
54
|
+
.filter((step) => (block % columns) + step < columns)
|
|
55
|
+
.map((step) => block + row * columns + step),
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
return new Set(
|
|
59
|
+
pictures.flatMap((to, index) => moved(pictures[index - 1] ?? to, to).flatMap(around)),
|
|
60
|
+
);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/** Whether anything moved from one picture to another outside the background. */
|
|
64
|
+
export const stirs = (from: Picture, to: Picture, mask: ReadonlySet<number>) =>
|
|
65
|
+
moved(from, to).some((block) => !mask.has(block));
|
|
@@ -11,7 +11,14 @@ import { Frame, Image, Screenshot } from "../../Frame.ts";
|
|
|
11
11
|
import type { Point } from "../../Motion.ts";
|
|
12
12
|
import { type FrameOptions, Region, type ScreenshotOptions, Zoom } from "../../Page.ts";
|
|
13
13
|
import { type Bridge, scriptCall } from "../page/bridge.ts";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
decodeWith,
|
|
16
|
+
failWith,
|
|
17
|
+
type PageContext,
|
|
18
|
+
reasonOf,
|
|
19
|
+
refuse,
|
|
20
|
+
undispatched,
|
|
21
|
+
} from "../page/context.ts";
|
|
15
22
|
import type { Viewport } from "../page/viewport.ts";
|
|
16
23
|
import * as Capture from "./capture.ts";
|
|
17
24
|
import * as BrowserClock from "./clock.ts";
|
|
@@ -118,9 +125,7 @@ const calibrator = (page: PageContext, bridge: Bridge) => {
|
|
|
118
125
|
yield* Effect.forEach(
|
|
119
126
|
[1, 2, 3],
|
|
120
127
|
() => Effect.map(probe(contextId), (one) => probes.push(one)),
|
|
121
|
-
{
|
|
122
|
-
discard: true,
|
|
123
|
-
},
|
|
128
|
+
{ discard: true },
|
|
124
129
|
).pipe(
|
|
125
130
|
within("calibrate", Duration.seconds(2)),
|
|
126
131
|
Effect.catch((error) => (probes.length === 0 ? Effect.fail(error) : Effect.void)),
|
|
@@ -308,10 +313,7 @@ const reuse =
|
|
|
308
313
|
const maxAge = millisOf(options.maxAge ?? defaultMaxAge);
|
|
309
314
|
|
|
310
315
|
if (!(maxAge >= 0))
|
|
311
|
-
return yield*
|
|
312
|
-
operation,
|
|
313
|
-
new InvalidRequest({ detail: "maxAge must be a duration of zero or more" }),
|
|
314
|
-
);
|
|
316
|
+
return yield* refuse(operation, "maxAge must be a duration of zero or more");
|
|
315
317
|
const latest = yield* capture.latest;
|
|
316
318
|
const known = viewport.knownViewport();
|
|
317
319
|
const { activity } = page;
|
|
@@ -371,6 +373,14 @@ export const make = Effect.fnUntraced(function* (
|
|
|
371
373
|
watch: bridge.currentDocument("screencast"),
|
|
372
374
|
frameHistory: settings.frameHistory,
|
|
373
375
|
viewport: Effect.suspend(() => viewport.viewportFor("screencast")),
|
|
376
|
+
// An uncropped snapshot waits for the restored compositor surface. Its pixels are discarded;
|
|
377
|
+
// a renderer animation frame or viewport size alone cannot prove the compositor caught up.
|
|
378
|
+
restore: () =>
|
|
379
|
+
page.protocol.send("Page.captureScreenshot", {
|
|
380
|
+
format: "jpeg",
|
|
381
|
+
quality: 0,
|
|
382
|
+
optimizeForSpeed: true,
|
|
383
|
+
}),
|
|
374
384
|
imageSize: jpegSize,
|
|
375
385
|
error,
|
|
376
386
|
});
|
|
@@ -468,10 +478,7 @@ export const make = Effect.fnUntraced(function* (
|
|
|
468
478
|
const size = viewport.knownViewport() ?? (yield* viewport.viewportFor("zoom"));
|
|
469
479
|
|
|
470
480
|
if (region.x + region.width > size.width || region.y + region.height > size.height)
|
|
471
|
-
return yield*
|
|
472
|
-
"zoom",
|
|
473
|
-
new InvalidRequest({ detail: "the crop must fit entirely within the viewport" }),
|
|
474
|
-
);
|
|
481
|
+
return yield* refuse("zoom", "the crop must fit entirely within the viewport");
|
|
475
482
|
const image = yield* picture("zoom", 80, region);
|
|
476
483
|
|
|
477
484
|
return new Zoom({ page: id, region, image });
|
|
@@ -485,10 +492,7 @@ export const make = Effect.fnUntraced(function* (
|
|
|
485
492
|
|
|
486
493
|
return millis > 0 && millis <= Duration.toMillis(Capture.countsKept)
|
|
487
494
|
? capture.stats(millis)
|
|
488
|
-
:
|
|
489
|
-
"captureStats",
|
|
490
|
-
new InvalidRequest({ detail: "window must be over zero and at most a minute" }),
|
|
491
|
-
);
|
|
495
|
+
: refuse("captureStats", "window must be over zero and at most a minute");
|
|
492
496
|
};
|
|
493
497
|
|
|
494
498
|
// The browser's clock mapping, measured on this page only while the browser has none, as the
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// What `motion.ts` uses of playwright-core's bundled JPEG decoder, which its untyped entry exports.
|
|
2
|
+
declare module "playwright-core/lib/utilsBundle" {
|
|
3
|
+
export const jpegjs: {
|
|
4
|
+
decode(
|
|
5
|
+
data: Uint8Array,
|
|
6
|
+
options: { useTArray: true },
|
|
7
|
+
): { readonly width: number; readonly height: number; readonly data: Uint8Array };
|
|
8
|
+
};
|
|
9
|
+
}
|
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { Duration, Effect, Ref, Schema } from "effect";
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import { StaleRef, Timeout } from "../../BrowserError.ts";
|
|
9
9
|
import { type FindQuery, Found, Text, type TextOptions, type WaitCondition } from "../../Page.ts";
|
|
10
10
|
import { Snapshot, type SnapshotOptions } from "../../Snapshot.ts";
|
|
11
11
|
import { type Bridge, scriptCall } from "../page/bridge.ts";
|
|
12
|
-
import { contextGone, decodeWith, failWith, type PageContext } from "../page/context.ts";
|
|
12
|
+
import { contextGone, decodeWith, failWith, type PageContext, refuse } from "../page/context.ts";
|
|
13
13
|
import * as Url from "../page/url.ts";
|
|
14
14
|
import { type ConditionRequest, ConditionResultSchema } from "./controls.inpage.ts";
|
|
15
15
|
import type { Want } from "./expect.inpage.ts";
|
|
@@ -68,8 +68,7 @@ export const make = Effect.fnUntraced(function* (page: PageContext, bridge: Brid
|
|
|
68
68
|
Effect.flatMap(decodeWith("snapshot", SnapshotResultSchema)),
|
|
69
69
|
);
|
|
70
70
|
|
|
71
|
-
if ("invalid" in result)
|
|
72
|
-
return yield* failWith("snapshot", new InvalidRequest({ detail: result.invalid }));
|
|
71
|
+
if ("invalid" in result) return yield* refuse("snapshot", result.invalid);
|
|
73
72
|
const { text, truncated, url } = result.snapshot;
|
|
74
73
|
|
|
75
74
|
yield* counted(result.nextRef);
|
|
@@ -115,10 +114,7 @@ export const make = Effect.fnUntraced(function* (page: PageContext, bridge: Brid
|
|
|
115
114
|
|
|
116
115
|
return (
|
|
117
116
|
scope !== "viewport" && !/^e\d+$/.test(scope)
|
|
118
|
-
?
|
|
119
|
-
"text",
|
|
120
|
-
new InvalidRequest({ detail: `${scope} is neither "viewport" nor a ref such as e12` }),
|
|
121
|
-
)
|
|
117
|
+
? refuse("text", `${scope} is neither "viewport" nor a ref such as e12`)
|
|
122
118
|
: texts(
|
|
123
119
|
JSON.stringify(request),
|
|
124
120
|
Effect.gen(function* () {
|
|
@@ -159,7 +155,7 @@ export const make = Effect.fnUntraced(function* (page: PageContext, bridge: Brid
|
|
|
159
155
|
Effect.flatMap(decodeWith("waitFor", ConditionResultSchema)),
|
|
160
156
|
Effect.flatMap((result) =>
|
|
161
157
|
"invalid" in result
|
|
162
|
-
?
|
|
158
|
+
? refuse("waitFor", result.invalid)
|
|
163
159
|
: result.met
|
|
164
160
|
? Effect.void
|
|
165
161
|
: unmet(),
|
|
@@ -10,16 +10,27 @@
|
|
|
10
10
|
* its connection, and a frame that arrives first starts the spell again. Where the page's changes
|
|
11
11
|
* are recorded, its evidence includes them: nothing in view may have changed for the spell, which a
|
|
12
12
|
* DOM page's changes show better than its paint, and no stall on the way can hide. Content
|
|
13
|
-
* stillness starts that record itself and ignores capture silence.
|
|
13
|
+
* stillness starts that record itself and ignores capture silence. Foreground stillness lets
|
|
14
|
+
* frames keep coming but compares them, so that only motion outside what was moving before the
|
|
15
|
+
* page's latest input counts; it still waits out unanswered acknowledgements while no frame
|
|
16
|
+
* comes, and its last look follows the round trip too. Each new document starts a
|
|
14
17
|
* new observation, while retries of destroyed script contexts share the caller's deadline.
|
|
15
18
|
*/
|
|
16
19
|
import { Duration, Effect, Option, Schedule, Stream } from "effect";
|
|
17
20
|
|
|
18
|
-
import { Timeout } from "../../BrowserError.ts";
|
|
21
|
+
import { Failed, Timeout } from "../../BrowserError.ts";
|
|
22
|
+
import type { Frame } from "../../Frame.ts";
|
|
19
23
|
import type { ReadyOptions } from "../../Page.ts";
|
|
20
24
|
import { type Bridge, scriptCall } from "../page/bridge.ts";
|
|
21
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
contextGone,
|
|
27
|
+
decodeWith,
|
|
28
|
+
failWith,
|
|
29
|
+
type PageContext,
|
|
30
|
+
undispatched,
|
|
31
|
+
} from "../page/context.ts";
|
|
22
32
|
import type * as Capture from "../pictures/capture.ts";
|
|
33
|
+
import * as Motion from "../pictures/motion.ts";
|
|
23
34
|
import { ReadinessSchema } from "./ready.inpage.ts";
|
|
24
35
|
|
|
25
36
|
export const make =
|
|
@@ -55,27 +66,68 @@ export const make =
|
|
|
55
66
|
const quiet = (millis: number, until: number) =>
|
|
56
67
|
Effect.gen(function* () {
|
|
57
68
|
const running = yield* capture.active;
|
|
58
|
-
|
|
69
|
+
const foreground = options.still === "foreground";
|
|
70
|
+
let counts = yield* capture.stats();
|
|
59
71
|
let stirred = running ? page.now() : Number.POSITIVE_INFINITY;
|
|
72
|
+
let reference: Motion.Picture | undefined;
|
|
60
73
|
|
|
61
|
-
|
|
74
|
+
const decoded = (frame: Frame) =>
|
|
75
|
+
Effect.try({
|
|
76
|
+
try: () => Motion.picture(frame),
|
|
77
|
+
catch: (error) =>
|
|
78
|
+
undispatched("ready", new Failed({ detail: `no picture: ${String(error)}` })),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// The background: what moved in up to eight frames of the document's 1.5 s before its
|
|
82
|
+
// latest input, as far back as the capture keeps them.
|
|
83
|
+
const { inputAt } = page.activity;
|
|
84
|
+
const document = Option.getOrUndefined(yield* capture.latest)?.document;
|
|
85
|
+
|
|
86
|
+
const before = (foreground ? yield* capture.recent : []).filter(
|
|
87
|
+
(frame) =>
|
|
88
|
+
frame.hostTime < inputAt &&
|
|
89
|
+
frame.hostTime >= inputAt - 1500 &&
|
|
90
|
+
frame.document === document,
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const sampled = before.filter((_, index) => index % Math.ceil(before.length / 8) === 0);
|
|
94
|
+
const mask = Motion.background(yield* Effect.forEach(sampled, decoded));
|
|
95
|
+
|
|
96
|
+
// How long the screen has been still: since a frame, or an unanswered acknowledgement. In
|
|
97
|
+
// the foreground, since a frame moved outside the background, or acknowledgements went
|
|
98
|
+
// unanswered with no frame coming.
|
|
62
99
|
const look = Effect.gen(function* () {
|
|
63
100
|
const stats = yield* capture.stats();
|
|
101
|
+
const latest = Option.getOrUndefined(yield* capture.latest);
|
|
102
|
+
const arrived = stats.received !== counts.received;
|
|
103
|
+
const unseen = stats.accepted - counts.accepted;
|
|
64
104
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
stats.ackBacklog > 0
|
|
69
|
-
) {
|
|
70
|
-
received = stats.received;
|
|
105
|
+
counts = stats;
|
|
106
|
+
if (latest === undefined || !capture.current(latest)) stirred = page.now();
|
|
107
|
+
else if (!foreground ? arrived || stats.ackBacklog > 0 : !arrived && stats.ackBacklog > 0)
|
|
71
108
|
stirred = page.now();
|
|
109
|
+
else if (foreground && (unseen > 0 || reference === undefined)) {
|
|
110
|
+
// A move and its return between looks both count. Bound decoding; any frames lost
|
|
111
|
+
// from history, or beyond this batch, deny a quiet spell rather than hiding motion.
|
|
112
|
+
const frames = (yield* capture.recent).slice(-Math.max(1, Math.min(8, unseen)));
|
|
113
|
+
|
|
114
|
+
if (unseen > frames.length) stirred = page.now();
|
|
115
|
+
for (const frame of frames) {
|
|
116
|
+
const seen = yield* decoded(frame);
|
|
117
|
+
|
|
118
|
+
if (reference === undefined || Motion.stirs(reference, seen, mask)) {
|
|
119
|
+
reference = seen;
|
|
120
|
+
stirred = page.now();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
72
123
|
}
|
|
73
124
|
|
|
74
125
|
return page.now() - stirred;
|
|
75
126
|
});
|
|
76
127
|
|
|
128
|
+
// Decode at a fixed cadence: adding a delay after a full batch lets its backlog grow.
|
|
77
129
|
const spell = Effect.repeat(look, {
|
|
78
|
-
schedule: Schedule.
|
|
130
|
+
schedule: Schedule.fixed(Duration.millis(foreground ? 100 : 25)),
|
|
79
131
|
until: (still) => still >= millis,
|
|
80
132
|
});
|
|
81
133
|
|
|
@@ -86,10 +138,10 @@ export const make =
|
|
|
86
138
|
const still = Effect.gen(function* () {
|
|
87
139
|
for (;;) {
|
|
88
140
|
yield* spell;
|
|
89
|
-
const before = received;
|
|
141
|
+
const before = counts.received;
|
|
90
142
|
|
|
91
143
|
yield* Effect.all([settled(until), capture.sync], { concurrency: 2, discard: true });
|
|
92
|
-
if ((yield* look) >= millis && received === before) return;
|
|
144
|
+
if ((yield* look) >= millis && (foreground || counts.received === before)) return;
|
|
93
145
|
}
|
|
94
146
|
});
|
|
95
147
|
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { Cause, Deferred, Effect, Stream } from "effect";
|
|
6
6
|
|
|
7
|
-
import { type BrowserError, EventHistoryExpired
|
|
7
|
+
import { type BrowserError, EventHistoryExpired } from "../../BrowserError.ts";
|
|
8
8
|
import { type BrowserEvent, RecordedEvent } from "../../BrowserEvent.ts";
|
|
9
|
-
import { undispatched } from "../page/context.ts";
|
|
9
|
+
import { refuse, undispatched } from "../page/context.ts";
|
|
10
10
|
|
|
11
11
|
/** The owning Browser validates capacity and closes this timeline with its scope. */
|
|
12
12
|
export const make = (capacity: number) => {
|
|
@@ -41,13 +41,10 @@ export const make = (capacity: number) => {
|
|
|
41
41
|
let cursor = after ?? sequence;
|
|
42
42
|
|
|
43
43
|
if (!Number.isSafeInteger(cursor) || cursor < 0 || cursor > sequence)
|
|
44
|
-
return Stream.
|
|
45
|
-
|
|
44
|
+
return Stream.fromEffect(
|
|
45
|
+
refuse(
|
|
46
46
|
"events",
|
|
47
|
-
|
|
48
|
-
detail:
|
|
49
|
-
"the event cursor must be a nonnegative integer no greater than the latest sequence",
|
|
50
|
-
}),
|
|
47
|
+
"the event cursor must be a nonnegative integer no greater than the latest sequence",
|
|
51
48
|
),
|
|
52
49
|
);
|
|
53
50
|
|
|
@@ -5,13 +5,13 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { Duration, Effect, Option, Result, Schema } from "effect";
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import type { BrowserError } from "../../BrowserError.ts";
|
|
9
9
|
import { type BrowserEvent, TrackEvent } from "../../BrowserEvent.ts";
|
|
10
10
|
import type { Changes } from "../../Change.ts";
|
|
11
11
|
import { Frame } from "../../Frame.ts";
|
|
12
12
|
import { Window } from "../../Moment.ts";
|
|
13
13
|
import type { ChangesOptions, WindowOptions } from "../../Page.ts";
|
|
14
|
-
import {
|
|
14
|
+
import { type PageContext, refuse } from "../page/context.ts";
|
|
15
15
|
|
|
16
16
|
const isWindow = Schema.is(Window);
|
|
17
17
|
const isFrame = Schema.is(Frame);
|
|
@@ -64,12 +64,7 @@ export const make =
|
|
|
64
64
|
const asked = boundsOf({ since, until }, page.now());
|
|
65
65
|
|
|
66
66
|
if (Option.isNone(asked))
|
|
67
|
-
return yield*
|
|
68
|
-
"window",
|
|
69
|
-
new InvalidRequest({
|
|
70
|
-
detail: "a window's bounds must be finite, its reach not negative",
|
|
71
|
-
}),
|
|
72
|
-
);
|
|
67
|
+
return yield* refuse("window", "a window's bounds must be finite, its reach not negative");
|
|
73
68
|
const bounds = asked.value;
|
|
74
69
|
|
|
75
70
|
// A previous window's changes go on exactly where they ended, on the page's own clock, and a
|