effect-browser 0.3.0-beta.15 → 0.3.0-beta.16

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 (83) hide show
  1. package/README.md +6 -2
  2. package/dist/{Browser-BBSySyjT.mjs → Browser-DRPthkNO.mjs} +4 -4
  3. package/dist/Browser-DRPthkNO.mjs.map +1 -0
  4. package/dist/Browser.d.mts +2 -2
  5. package/dist/Browser.mjs +1 -1
  6. package/dist/{BrowserEvent-BRu2juEz.d.mts → BrowserEvent-dq6FRT8w.d.mts} +9 -9
  7. package/dist/BrowserEvent.d.mts +1 -1
  8. package/dist/BrowserEvent.mjs +7 -12
  9. package/dist/BrowserEvent.mjs.map +1 -1
  10. package/dist/Cdp.mjs +2 -2
  11. package/dist/{Change-iMlcuzVD.d.mts → Change-DqING3tr.d.mts} +2 -2
  12. package/dist/Change.d.mts +1 -1
  13. package/dist/Chromium.mjs +2 -2
  14. package/dist/Expect.d.mts +1 -1
  15. package/dist/Expect.mjs +2 -2
  16. package/dist/Moment.d.mts +1 -1
  17. package/dist/Moment.mjs +1 -1
  18. package/dist/Motion-DND6I-64.d.mts +40 -0
  19. package/dist/Motion.d.mts +2 -40
  20. package/dist/Motion.mjs +1 -2
  21. package/dist/Motion.mjs.map +1 -1
  22. package/dist/{Page-B5cjaUp5.mjs → Page-C_VkBVwI.mjs} +5 -9
  23. package/dist/Page-C_VkBVwI.mjs.map +1 -0
  24. package/dist/{Page-DL9eylp3.d.mts → Page-jHmSJeOS.d.mts} +6 -13
  25. package/dist/Page.d.mts +1 -1
  26. package/dist/Page.mjs +1 -1
  27. package/dist/{Plan-6As6vcwT.d.mts → Plan-DLtRksuu.d.mts} +3 -3
  28. package/dist/{Plan-CXA2BW-G.mjs → Plan-X-JkACAB.mjs} +6 -9
  29. package/dist/Plan-X-JkACAB.mjs.map +1 -0
  30. package/dist/Plan.d.mts +1 -1
  31. package/dist/Plan.mjs +1 -1
  32. package/dist/Policy.d.mts +1 -1
  33. package/dist/Policy.mjs +1 -1
  34. package/dist/Presentation.d.mts +2 -2
  35. package/dist/Presentation.mjs +3 -2
  36. package/dist/Presentation.mjs.map +1 -1
  37. package/dist/Stage.d.mts +1 -1
  38. package/dist/Stage.mjs +3 -3
  39. package/dist/Stage.mjs.map +1 -1
  40. package/dist/{Supervisor-DaGlQVjv.mjs → Supervisor-BgWP8o9t.mjs} +41 -17
  41. package/dist/Supervisor-BgWP8o9t.mjs.map +1 -0
  42. package/dist/{Supervisor-5tZ47hR5.d.mts → Supervisor-mVPeEUp8.d.mts} +12 -5
  43. package/dist/Supervisor.d.mts +1 -1
  44. package/dist/Supervisor.mjs +1 -1
  45. package/dist/{context-BAjRj7Xo.mjs → context-rYTk-3Cv.mjs} +68 -3
  46. package/dist/context-rYTk-3Cv.mjs.map +1 -0
  47. package/dist/index.d.mts +6 -6
  48. package/dist/index.mjs +4 -4
  49. package/dist/match.inpage-xOe9LNO0.mjs.map +1 -1
  50. package/dist/{page-B92QnRfT.mjs → page-ClEMFnBG.mjs} +48 -110
  51. package/dist/page-ClEMFnBG.mjs.map +1 -0
  52. package/package.json +1 -1
  53. package/src/Browser.ts +1 -1
  54. package/src/BrowserEvent.ts +7 -12
  55. package/src/Motion.ts +1 -2
  56. package/src/Page.ts +5 -9
  57. package/src/Plan.ts +1 -2
  58. package/src/Presentation.ts +2 -1
  59. package/src/Stage.ts +2 -1
  60. package/src/Supervisor.ts +3 -2
  61. package/src/internal/input/actions.ts +25 -33
  62. package/src/internal/input/dispatch.ts +3 -4
  63. package/src/internal/input/edit.inpage.ts +3 -9
  64. package/src/internal/input/evidence.inpage.ts +2 -20
  65. package/src/internal/input/guard.inpage.ts +9 -29
  66. package/src/internal/input/guard.ts +12 -1
  67. package/src/internal/input/targets.inpage.ts +4 -8
  68. package/src/internal/input/targets.ts +8 -11
  69. package/src/internal/page/context.ts +2 -2
  70. package/src/internal/pictures/capture.ts +4 -4
  71. package/src/internal/pictures/clock.ts +8 -3
  72. package/src/internal/pictures/pictures.ts +2 -1
  73. package/src/internal/reading/controls.inpage.ts +8 -1
  74. package/src/internal/reading/match.inpage.ts +2 -1
  75. package/src/internal/reading/outline.inpage.ts +7 -4
  76. package/src/internal/reading/reading.ts +5 -16
  77. package/src/internal/supervisor/lifecycle.ts +56 -13
  78. package/dist/Browser-BBSySyjT.mjs.map +0 -1
  79. package/dist/Page-B5cjaUp5.mjs.map +0 -1
  80. package/dist/Plan-CXA2BW-G.mjs.map +0 -1
  81. package/dist/Supervisor-DaGlQVjv.mjs.map +0 -1
  82. package/dist/context-BAjRj7Xo.mjs.map +0 -1
  83. package/dist/page-B92QnRfT.mjs.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "effect-browser",
3
- "version": "0.3.0-beta.15",
3
+ "version": "0.3.0-beta.16",
4
4
  "description": "Effect browser sessions, page control, screencast and moments over Playwright and CDP.",
5
5
  "license": "MIT",
6
6
  "repository": {
package/src/Browser.ts CHANGED
@@ -492,7 +492,7 @@ export const make = Effect.fn("Browser.make")(function* (
492
492
  ) {
493
493
  const clock = yield* Clock.Clock;
494
494
  const connection = yield* Identity.make;
495
- const now = () => Number(clock.monotonicTimeNanosUnsafe()) / 1e6;
495
+ const now = () => BrowserClock.monotonicMillis(clock);
496
496
  const { settings, eventHistory, maxPages } = yield* settingsOf(options);
497
497
  // Measured on first need: the first capture of any page measures it, and input never waits.
498
498
  const mapping = BrowserClock.mapping(now);
@@ -119,8 +119,7 @@ const Button = Schema.Literals(["left", "right", "middle"]);
119
119
 
120
120
  /** An element's box in viewport CSS pixels; it may lie partly or wholly outside the viewport. */
121
121
  export const Box = Schema.Struct({
122
- x: Schema.Finite,
123
- y: Schema.Finite,
122
+ ...Motion.Point.fields,
124
123
  width: Schema.Finite,
125
124
  height: Schema.Finite,
126
125
  });
@@ -169,8 +168,8 @@ export class Action extends Schema.TaggedClass<Action>()("Action", {
169
168
  /** For an action at a point, the box of what it found there. */
170
169
  box: Schema.optional(Box),
171
170
  text: Schema.optional(Schema.String),
172
- x: Schema.optional(Schema.Finite),
173
- y: Schema.optional(Schema.Finite),
171
+ x: Schema.optional(Motion.Point.fields.x),
172
+ y: Schema.optional(Motion.Point.fields.y),
174
173
  ok: Schema.Boolean,
175
174
  dispatched: Schema.Boolean,
176
175
  error: Schema.optional(Schema.String),
@@ -196,16 +195,14 @@ export class TrackPerformed extends Schema.TaggedClass<TrackPerformed>()("TrackP
196
195
  page: Schema.String,
197
196
  plan: Sequence,
198
197
  dispatched: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
199
- x: Schema.Finite,
200
- y: Schema.Finite,
198
+ ...Motion.Point.fields,
201
199
  complete: Schema.Boolean,
202
200
  }) {}
203
201
 
204
202
  export class PointerPressed extends Schema.TaggedClass<PointerPressed>()("PointerPressed", {
205
203
  at: Schema.Finite,
206
204
  page: Schema.String,
207
- x: Schema.Finite,
208
- y: Schema.Finite,
205
+ ...Motion.Point.fields,
209
206
  button: Button,
210
207
  clickCount: Schema.Int.check(Schema.isGreaterThan(0)),
211
208
  }) {}
@@ -213,8 +210,7 @@ export class PointerPressed extends Schema.TaggedClass<PointerPressed>()("Pointe
213
210
  export class PointerReleased extends Schema.TaggedClass<PointerReleased>()("PointerReleased", {
214
211
  at: Schema.Finite,
215
212
  page: Schema.String,
216
- x: Schema.Finite,
217
- y: Schema.Finite,
213
+ ...Motion.Point.fields,
218
214
  button: Button,
219
215
  clickCount: Schema.Int.check(Schema.isGreaterThan(0)),
220
216
  }) {}
@@ -222,8 +218,7 @@ export class PointerReleased extends Schema.TaggedClass<PointerReleased>()("Poin
222
218
  export class WheelScrolled extends Schema.TaggedClass<WheelScrolled>()("WheelScrolled", {
223
219
  at: Schema.Finite,
224
220
  page: Schema.String,
225
- x: Schema.Finite,
226
- y: Schema.Finite,
221
+ ...Motion.Point.fields,
227
222
  dx: Schema.Finite,
228
223
  dy: Schema.Finite,
229
224
  }) {}
package/src/Motion.ts CHANGED
@@ -13,8 +13,7 @@ export const Point = Schema.Struct({ x: Schema.Finite, y: Schema.Finite });
13
13
  export type Point = typeof Point.Type;
14
14
 
15
15
  export const Sample = Schema.Struct({
16
- x: Schema.Finite,
17
- y: Schema.Finite,
16
+ ...Point.fields,
18
17
  afterMillis: Schema.Finite.check(
19
18
  Schema.isGreaterThanOrEqualTo(0),
20
19
  Schema.isLessThanOrEqualTo(maximumDurationMillis),
package/src/Page.ts CHANGED
@@ -27,12 +27,10 @@ import * as Guard from "./internal/input/guard.inpage.ts";
27
27
  import { Correlation, FailFast } from "./internal/page/lane.ts";
28
28
  import * as Url from "./internal/page/url.ts";
29
29
  import type { Window } from "./Moment.ts";
30
+ import * as Motion from "./Motion.ts";
30
31
  import { type Snapshot, type SnapshotOptions } from "./Snapshot.ts";
31
32
 
32
- export interface Point {
33
- readonly x: number;
34
- readonly y: number;
35
- }
33
+ export type Point = Motion.Point;
36
34
 
37
35
  /** A ref from a snapshot, such as `"e12"`, or a viewport point. */
38
36
  export type Target = string | Point;
@@ -54,7 +52,7 @@ export class Zoom extends Schema.Class<Zoom>("effect-browser/Zoom")({
54
52
 
55
53
  /** The target as it read before input. Point coordinates are never snapped to another position. */
56
54
  export class ResolvedTarget extends Schema.Class<ResolvedTarget>("effect-browser/ResolvedTarget")({
57
- point: Schema.Struct({ x: Schema.Finite, y: Schema.Finite }),
55
+ point: Motion.Point,
58
56
  element: Schema.String,
59
57
  /** The element's lowercase tag name. */
60
58
  tag: Schema.String,
@@ -131,9 +129,7 @@ export interface ScreenshotOptions {
131
129
  /** The oldest a reused frame may be, as for `frame`. Defaults to 250 milliseconds. */
132
130
  readonly maxAge?: Duration.Input | undefined;
133
131
  /** A region of the viewport, captured in CSS pixels at the viewport's own scale; always new. */
134
- readonly clip?:
135
- | { readonly x: number; readonly y: number; readonly width: number; readonly height: number }
136
- | undefined;
132
+ readonly clip?: Box | undefined;
137
133
  /** JPEG quality, 0 to 100. Defaults to 80. */
138
134
  readonly quality?: number | undefined;
139
135
  /** `"jpeg"`, the default, or `"png"`, which is always a new picture. */
@@ -335,7 +331,7 @@ export class InputRequest extends Schema.Class<InputRequest>("effect-browser/Inp
335
331
  target: Schema.optional(Schema.String),
336
332
  element: Schema.optional(Schema.String),
337
333
  /** Literal pixel targets only; ref coordinates are resolved after the policy allows them. */
338
- point: Schema.optional(Schema.Struct({ x: Schema.Finite, y: Schema.Finite })),
334
+ point: Schema.optional(Motion.Point),
339
335
  text: Schema.optional(Schema.String),
340
336
  role: Schema.optional(Schema.NullOr(Schema.String)),
341
337
  name: Schema.optional(Schema.String),
package/src/Plan.ts CHANGED
@@ -33,6 +33,7 @@ import {
33
33
  } from "./BrowserEvent.ts";
34
34
  import { undispatched } from "./internal/page/context.ts";
35
35
  import { Ambiguous, choose, Drifted, Missing } from "./internal/reading/choose.ts";
36
+ import { Point as ViewportPoint } from "./Motion.ts";
36
37
  import {
37
38
  type Found,
38
39
  type Page,
@@ -44,8 +45,6 @@ import {
44
45
 
45
46
  export { Ambiguous, Drifted, Missing } from "./internal/reading/choose.ts";
46
47
 
47
- const ViewportPoint = Schema.Struct({ x: Schema.Finite, y: Schema.Finite });
48
-
49
48
  export const StepAction = Schema.Literals([
50
49
  "navigate",
51
50
  "back",
@@ -20,6 +20,7 @@ import { Clock, Duration, Effect, Fiber, MutableRef, Option, Semaphore } from "e
20
20
  import type { BrowserError } from "./BrowserError.ts";
21
21
  import * as Style from "./internal/input/style.ts";
22
22
  import { type Internals, internalsOf, viewOf } from "./internal/page/page.ts";
23
+ import { monotonicMillis } from "./internal/pictures/clock.ts";
23
24
  import * as Motion from "./Motion.ts";
24
25
  import type { Page, Point, Target } from "./Page.ts";
25
26
 
@@ -81,7 +82,7 @@ export const make = Effect.fn("Presentation.make")(function* (options: Options =
81
82
  const pacing = options.pacing ?? human;
82
83
  const clock = yield* Clock.Clock;
83
84
  const scope = yield* Effect.scope;
84
- const now = () => Number(clock.monotonicTimeNanosUnsafe()) / 1e6;
85
+ const now = () => monotonicMillis(clock);
85
86
  const pointer = MutableRef.make(Option.none<Point>());
86
87
  const hand = yield* Semaphore.make(1);
87
88
 
package/src/Stage.ts CHANGED
@@ -26,6 +26,7 @@ import { type BrowserError, Closed, consequence, InvalidRequest, Timeout } from
26
26
  import type { CaptureStats, Frame, ScreencastOptions } from "./Frame.ts";
27
27
  import { failWith, undispatched } from "./internal/page/context.ts";
28
28
  import { internalsOf } from "./internal/page/page.ts";
29
+ import { monotonicMillis } from "./internal/pictures/clock.ts";
29
30
  import type { Page } from "./Page.ts";
30
31
 
31
32
  export interface Options extends ScreencastOptions {
@@ -390,7 +391,7 @@ export const make = Effect.fn("Stage.make")(function* (
390
391
  ): Effect.fn.Return<Stage, BrowserError, Scope.Scope> {
391
392
  const clock = yield* Clock.Clock;
392
393
  const owner = yield* Effect.scope;
393
- const now = () => Number(clock.monotonicTimeNanosUnsafe()) / 1e6;
394
+ const now = () => monotonicMillis(clock);
394
395
  const waited = Duration.fromInput(options.firstFrame ?? Duration.seconds(5));
395
396
 
396
397
  if (
package/src/Supervisor.ts CHANGED
@@ -107,7 +107,8 @@ export interface Admission {
107
107
  /**
108
108
  * Optional service supplied during `open`. Providers report failed-attempt cleanup here before
109
109
  * their scope closes; successful generations instead return `Opened.release`. Reporting does
110
- * not release resources or replace the original acquisition failure.
110
+ * not release resources or replace the original acquisition failure. Terminal `Down` and opening
111
+ * `Closed` states publish the reported outcomes of their own generation's tries.
111
112
  */
112
113
  export class Releasing extends Context.Service<
113
114
  Releasing,
@@ -556,7 +557,7 @@ const makeScoped = Effect.fn("Supervisor.make")(function* <E, R, A>(
556
557
  return yield* options.open.pipe(
557
558
  Effect.provideService(Releasing, {
558
559
  released: (released) =>
559
- apply({ _tag: "AttemptReleased", released }).pipe(Effect.asVoid),
560
+ apply({ _tag: "AttemptReleased", number, released }).pipe(Effect.asVoid),
560
561
  }),
561
562
  );
562
563
  }),
@@ -32,12 +32,11 @@ const partsOf = (page: PageContext, bridge: Bridge, viewport: Viewport) => {
32
32
 
33
33
  return {
34
34
  page,
35
- bridge,
36
35
  ...settling,
37
36
  viewportFor: viewport.viewportFor,
38
37
  perform: Perform.make(page, dispatch),
39
38
  ...guard,
40
- ...Targets.make(bridge, viewport, guard, pointer, dispatch, settling),
39
+ ...Targets.make(viewport, guard, pointer, dispatch, settling),
41
40
  ...pointer,
42
41
  ...Keyboard.make(page, bridge, dispatch),
43
42
  sendMouse: dispatch.sendMouse,
@@ -110,16 +109,19 @@ const click = (input: Parts) => {
110
109
  yield* marks.at(point);
111
110
  yield* marks.on(resolved);
112
111
  yield* moveTo("click", marks, point, resolved.cursor);
113
- if (approval !== undefined) yield* approval.check({ presses: [{ index: 0, ...point }] });
112
+ yield* Guard.check(approval, { presses: [{ index: 0, ...point }] });
114
113
  const before = mark();
115
114
 
116
115
  yield* marks.sent;
117
116
  for (let index = 1; index <= count; index++) {
118
117
  // An earlier click of a multi-click can replace the approved control under the
119
118
  // pointer. Under a guard, its handlers run before the next press is checked and sent.
120
- if (approval !== undefined && index > 1) {
121
- yield* flush("click", marks.input);
122
- yield* approval.check({ presses: [{ index: 0, ...point }] }).pipe(
119
+ if (index > 1)
120
+ yield* Guard.check(
121
+ approval,
122
+ { presses: [{ index: 0, ...point }] },
123
+ flush("click", marks.input),
124
+ ).pipe(
123
125
  Effect.catchIf(
124
126
  (error) => error.reason._tag === "StaleRef",
125
127
  () =>
@@ -132,7 +134,6 @@ const click = (input: Parts) => {
132
134
  ),
133
135
  ),
134
136
  );
135
- }
136
137
  yield* sendMouse("click", marks.input, {
137
138
  type: "mousePressed",
138
139
  ...point,
@@ -236,13 +237,12 @@ const drag = (input: Parts) => {
236
237
  yield* moveTo("drag", marks, start.point, start.cursor);
237
238
  // Both ends are checked before the button goes down: once it is down, a release cannot
238
239
  // be withheld, and a dragged element under the pointer would hide the drop target.
239
- if (approval !== undefined)
240
- yield* approval.check({
241
- presses: [
242
- { index: 0, ...start.point },
243
- { index: 1, ...end.point },
244
- ],
245
- });
240
+ yield* Guard.check(approval, {
241
+ presses: [
242
+ { index: 0, ...start.point },
243
+ { index: 1, ...end.point },
244
+ ],
245
+ });
246
246
  yield* marks.sent;
247
247
  yield* sendMouse("drag", marks.input, {
248
248
  type: "mousePressed",
@@ -267,7 +267,7 @@ const drag = (input: Parts) => {
267
267
 
268
268
  const typeText = (input: Parts) => {
269
269
  const { settings, now } = input.page;
270
- const { perform, preparePolicy, mutate, targetFor, moveTo, sendMouse, flush, bridge } = input;
270
+ const { perform, preparePolicy, mutate, targetFor, moveTo, sendMouse, flush, evaluate } = input;
271
271
  const { currentDocument, sameDocument, keyStroke, typeEvent, mark, settle } = input;
272
272
 
273
273
  return (text: string, typeOptions: TypeOptions = {}, style: Style = plain) =>
@@ -301,11 +301,9 @@ const typeText = (input: Parts) => {
301
301
  // those before any input, on every path.
302
302
  const asked = scriptCall("typeable", into ?? null, typeOptions.secret === true);
303
303
 
304
- const typeable = yield* (
305
- approval === undefined
306
- ? bridge.evaluate("type", asked)
307
- : mutate("type", asked, approval)
308
- ).pipe(Effect.flatMap(decodeWith("type", Script.TypeableResultSchema)));
304
+ const typeable = yield* evaluate("type", asked, approval).pipe(
305
+ Effect.flatMap(decodeWith("type", Script.TypeableResultSchema)),
306
+ );
309
307
 
310
308
  if ("error" in typeable)
311
309
  return yield* failWith(
@@ -327,8 +325,7 @@ const typeText = (input: Parts) => {
327
325
  // Viewers see the pointer click into the field.
328
326
  if (marks.style.shown) {
329
327
  yield* moveTo("type", marks, target.point, target.cursor);
330
- if (approval !== undefined)
331
- yield* approval.check({ presses: [{ index: 0, ...target.point }] });
328
+ yield* Guard.check(approval, { presses: [{ index: 0, ...target.point }] });
332
329
  yield* marks.sent;
333
330
  for (const type of ["mousePressed", "mouseReleased"] as const)
334
331
  yield* sendMouse("type", marks.input, {
@@ -354,7 +351,7 @@ const typeText = (input: Parts) => {
354
351
  secret ||= focused.secret;
355
352
  }
356
353
  if (!secret) yield* marks.reveal;
357
- if (approval !== undefined) yield* approval.check({ focused: true });
354
+ yield* Guard.check(approval, { focused: true });
358
355
  yield* marks.sent;
359
356
 
360
357
  // A key's handlers can move focus, and later keys then reach a control nobody approved.
@@ -362,10 +359,7 @@ const typeText = (input: Parts) => {
362
359
  // handler can split, and keys typed one at a time check that the field still has focus,
363
360
  // once the keys before are answered, before each space, which could press a button, and
364
361
  // after the last key.
365
- const focusHeld =
366
- approval === undefined
367
- ? Effect.void
368
- : flush("type", marks.input).pipe(Effect.andThen(approval.check({ focused: true })));
362
+ const focusHeld = Guard.check(approval, { focused: true }, flush("type", marks.input));
369
363
 
370
364
  if (text === "" && replace && into !== undefined) {
371
365
  yield* sameDocument("type", since);
@@ -396,7 +390,7 @@ const typeText = (input: Parts) => {
396
390
  yield* flush("type", marks.input);
397
391
  if (typeOptions.submit === true) {
398
392
  // Typing can move focus or change the form; Enter goes only to the approved field.
399
- if (approval !== undefined) yield* approval.check({ focused: true });
393
+ yield* Guard.check(approval, { focused: true });
400
394
  yield* sameDocument("type", since);
401
395
  const before = mark();
402
396
 
@@ -447,7 +441,7 @@ const press = (input: Parts) => {
447
441
  );
448
442
 
449
443
  if ("error" in focused) return yield* Guard.editFailure("press", on, focused);
450
- if (approval !== undefined) yield* approval.check({ focused: true });
444
+ yield* Guard.check(approval, { focused: true });
451
445
  }
452
446
  const before = mark();
453
447
  let due = now();
@@ -456,10 +450,8 @@ const press = (input: Parts) => {
456
450
  for (let index = 0; index < times; index++) {
457
451
  // A repeated key stays in the document it began in. Under a guard, a repeated Enter
458
452
  // or Space must also reach the approved element after the previous press settled.
459
- if (index > 0 && approval !== undefined && activates) {
460
- yield* flush("press", marks.input);
461
- yield* approval.check({ focused: true });
462
- }
453
+ if (index > 0 && activates)
454
+ yield* Guard.check(approval, { focused: true }, flush("press", marks.input));
463
455
  yield* sameDocument("press", since);
464
456
  yield* keyStroke("press", marks.input, parts, hold);
465
457
  if (index + 1 < times) {
@@ -12,20 +12,19 @@ import {
12
12
  TextInserted,
13
13
  WheelScrolled,
14
14
  } from "../../BrowserEvent.ts";
15
+ import type { Point } from "../../Motion.ts";
15
16
  import { type PageContext, reasonOf, undispatched } from "../page/context.ts";
16
17
  import * as BrowserClock from "../pictures/clock.ts";
17
18
  import type * as Replies from "./replies.ts";
18
19
 
19
- export type MouseEvent = {
20
+ export interface MouseEvent extends Point {
20
21
  readonly type: "mouseMoved" | "mousePressed" | "mouseReleased" | "mouseWheel";
21
- readonly x: number;
22
- readonly y: number;
23
22
  readonly button?: "none" | "left" | "right" | "middle";
24
23
  readonly buttons?: number;
25
24
  readonly clickCount?: number;
26
25
  readonly deltaX?: number;
27
26
  readonly deltaY?: number;
28
- };
27
+ }
29
28
 
30
29
  export const make = (page: PageContext) => {
31
30
  const { id, pointer, publish, now, noteInput, closedBy } = page;
@@ -10,22 +10,16 @@ import type { ContextReader } from "../reading/context.inpage.ts";
10
10
  import type { Names } from "../reading/names.inpage.ts";
11
11
  import type { Guard } from "./guard.inpage.ts";
12
12
 
13
- export type EditResult =
14
- | { readonly ok: true; readonly detail: string }
15
- | { readonly error: string; readonly stale?: boolean };
13
+ export type EditResult = typeof EditResultSchema.Encoded;
16
14
 
17
15
  /** A focused field, and whether it is secret. */
18
- export type FocusResult =
19
- | { readonly ok: true; readonly detail: string; readonly secret: boolean }
20
- | { readonly error: string; readonly stale?: boolean };
16
+ export type FocusResult = typeof FocusResultSchema.Encoded;
21
17
 
22
18
  /**
23
19
  * Whether text may be typed, and whether its field is secret. Typing into focus also names the
24
20
  * focused field, when it is one, so the action records what it typed into.
25
21
  */
26
- export type TypeableResult =
27
- | { readonly ok: true; readonly secret: boolean; readonly subject?: typeof Subject.Encoded }
28
- | { readonly error: "stale" | "untypeable"; readonly detail: string };
22
+ export type TypeableResult = typeof TypeableResultSchema.Encoded;
29
23
 
30
24
  export const edit = (names: Names, guard: Guard, placing: ContextReader) => {
31
25
  const {
@@ -7,28 +7,10 @@ import { Schema } from "effect";
7
7
  import type { ContextReader } from "../reading/context.inpage.ts";
8
8
  import type { Names } from "../reading/names.inpage.ts";
9
9
 
10
- export interface FormField {
11
- readonly type: string;
12
- readonly name: string;
13
- readonly autocomplete?: string | undefined;
14
- readonly filled: boolean;
15
- }
10
+ export type FormField = typeof FormFieldSchema.Encoded;
16
11
 
17
12
  /** Page text about the first target. It never includes what a field holds. */
18
- export interface Evidence {
19
- readonly title: string;
20
- readonly description?: string | undefined;
21
- readonly dialog?: string | undefined;
22
- readonly heading?: string | undefined;
23
- readonly nearby?: string | undefined;
24
- readonly form?:
25
- | {
26
- readonly method: string;
27
- readonly action: string;
28
- readonly fields: ReadonlyArray<FormField>;
29
- }
30
- | undefined;
31
- }
13
+ export type Evidence = typeof EvidenceSchema.Encoded;
32
14
 
33
15
  type Field = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
34
16
 
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import { Schema } from "effect";
7
7
 
8
+ import type { Point } from "../../Motion.ts";
8
9
  import type { Names } from "../reading/names.inpage.ts";
9
10
  import { type Evidence, type EvidenceReader, EvidenceSchema } from "./evidence.inpage.ts";
10
11
  import type { Targets } from "./targets.inpage.ts";
@@ -23,48 +24,26 @@ export type Fact = typeof Fact.Type;
23
24
 
24
25
  export interface InputPlan {
25
26
  readonly action: string;
26
- readonly targets: ReadonlyArray<string | { readonly x: number; readonly y: number } | null>;
27
+ readonly targets: ReadonlyArray<string | Point | null>;
27
28
  readonly submit: boolean;
28
29
  readonly keys: string | null;
29
30
  readonly destination: string | null;
30
31
  }
31
32
 
32
- export interface InspectedTarget {
33
- readonly ref: string;
34
- readonly element: string;
35
- readonly role: string | null;
36
- readonly name: string;
37
- readonly cursor: string;
38
- readonly href?: string | undefined;
39
- /** A field the DOM marks as holding a password, a one-time code or a card's details. */
40
- readonly secret: boolean;
41
- readonly fingerprint: string;
42
- }
33
+ export type InspectedTarget = NonNullable<PreparedInput["targets"][number]>;
43
34
 
44
- export interface PreparedInput {
45
- readonly url: string;
46
- readonly targets: ReadonlyArray<InspectedTarget | null>;
47
- readonly facts: ReadonlyArray<Fact>;
48
- readonly destination?: string | undefined;
49
- readonly evidence: Evidence;
50
- }
35
+ export type PreparedInput = Exclude<PreparedInputResult, InputFailure>;
51
36
 
52
37
  /** A refusal; a stale target is named by its index in the plan. */
53
- export interface InputFailure {
54
- readonly error: string;
55
- readonly detail: string;
56
- readonly index?: number | undefined;
57
- }
38
+ export type InputFailure = typeof InputPreparationError.Encoded;
58
39
 
59
- export type PreparedInputResult = PreparedInput | InputFailure;
40
+ export type PreparedInputResult = typeof PreparedInputResultSchema.Encoded;
60
41
 
61
- export type ValidatedInputResult = { readonly ok: true } | InputFailure;
42
+ export type ValidatedInputResult = typeof ValidatedInputResultSchema.Encoded;
62
43
 
63
44
  /** A point where a target, by its index in the plan, is about to receive a press. */
64
- export interface Press {
45
+ export interface Press extends Point {
65
46
  readonly index: number;
66
- readonly x: number;
67
- readonly y: number;
68
47
  }
69
48
 
70
49
  export interface ValidationOptions {
@@ -587,6 +566,7 @@ export const PreparedInputResultSchema = Schema.Union([
587
566
  name: Schema.String,
588
567
  cursor: Schema.String,
589
568
  href: Schema.optional(Schema.String),
569
+ /** A field the DOM marks as holding a password, a one-time code or a card's details. */
590
570
  secret: Schema.Boolean,
591
571
  fingerprint: Schema.String,
592
572
  }),
@@ -22,6 +22,13 @@ export interface PolicyPlan {
22
22
  readonly validate: Effect.Effect<Approval, BrowserError>;
23
23
  }
24
24
 
25
+ /** Recheck an approval, after any pending input it must see; neither runs without a guard. */
26
+ export const check = (
27
+ approval: Approval | undefined,
28
+ options: Script.ValidationOptions = {},
29
+ before: Effect.Effect<void, BrowserError> = Effect.void,
30
+ ) => (approval === undefined ? Effect.void : before.pipe(Effect.andThen(approval.check(options))));
31
+
25
32
  // A ref that names nothing is stale on every path, named by the target that failed.
26
33
  export const inputFailure = (
27
34
  operation: string,
@@ -178,7 +185,11 @@ export const make = (page: PageContext, bridge: Bridge) => {
178
185
  ),
179
186
  );
180
187
 
181
- return { preparePolicy, mutate };
188
+ /** A read may follow the current document only when no approval binds it to one. */
189
+ const evaluate = (operation: string, call: string, approval: Approval | undefined) =>
190
+ approval === undefined ? bridge.evaluate(operation, call) : mutate(operation, call, approval);
191
+
192
+ return { preparePolicy, mutate, evaluate };
182
193
  };
183
194
 
184
195
  export type Guard = ReturnType<typeof make>;
@@ -6,6 +6,7 @@
6
6
  import { Schema } from "effect";
7
7
 
8
8
  import { Box, SubjectContext } from "../../BrowserEvent.ts";
9
+ import { Point } from "../../Motion.ts";
9
10
  import type { ContextReader } from "../reading/context.inpage.ts";
10
11
  import type { Names } from "../reading/names.inpage.ts";
11
12
  import type { Walk } from "../reading/walk.inpage.ts";
@@ -78,10 +79,7 @@ export const targets = (names: Names, walked: Walk, placing: ContextReader) => {
78
79
  };
79
80
  };
80
81
 
81
- const point = (
82
- target: string | { readonly x: number; readonly y: number },
83
- scroll = true,
84
- ): PointResult => {
82
+ const point = (target: string | Point, scroll = true): PointResult => {
85
83
  if (typeof target !== "string") {
86
84
  const { x, y } = target;
87
85
 
@@ -263,8 +261,7 @@ export const targets = (names: Names, walked: Walk, placing: ContextReader) => {
263
261
  export type Targets = ReturnType<typeof targets>;
264
262
 
265
263
  const ResolvedPointSchema = Schema.Struct({
266
- x: Schema.Finite,
267
- y: Schema.Finite,
264
+ ...Point.fields,
268
265
  element: Schema.String,
269
266
  tag: Schema.String,
270
267
  role: Schema.NullOr(Schema.String),
@@ -285,8 +282,7 @@ export const PointResultSchema = Schema.Union([
285
282
 
286
283
  export const ScrollPlanSchema = Schema.NullOr(
287
284
  Schema.Struct({
288
- x: Schema.Finite,
289
- y: Schema.Finite,
285
+ ...Point.fields,
290
286
  dx: Schema.Finite,
291
287
  dy: Schema.Finite,
292
288
  }),
@@ -7,11 +7,11 @@ import { Effect } from "effect";
7
7
 
8
8
  import { InvalidRequest, NotActionable, StaleRef } from "../../BrowserError.ts";
9
9
  import { ResolvedTarget, type Target } from "../../Page.ts";
10
- import { type Bridge, scriptCall } from "../page/bridge.ts";
10
+ import { scriptCall } from "../page/bridge.ts";
11
11
  import { decodeWith, failWith } from "../page/context.ts";
12
12
  import type { Viewport } from "../page/viewport.ts";
13
13
  import type { Dispatch } from "./dispatch.ts";
14
- import type { Approval, Guard } from "./guard.ts";
14
+ import { type Approval, check, type Guard } from "./guard.ts";
15
15
  import type { InputMarks } from "./perform.ts";
16
16
  import type { Pointer } from "./pointer.ts";
17
17
  import type { Settle } from "./settle.ts";
@@ -45,15 +45,13 @@ const resolvedTarget = (result: Script.ResolvedPoint): ResolvedTarget =>
45
45
  });
46
46
 
47
47
  export const make = (
48
- bridge: Bridge,
49
48
  viewport: Viewport,
50
49
  guard: Guard,
51
50
  pointer: Pointer,
52
51
  dispatch: Dispatch,
53
52
  settling: Settle,
54
53
  ) => {
55
- const { evaluate } = bridge;
56
- const { mutate } = guard;
54
+ const { evaluate, mutate } = guard;
57
55
  const { moveTo, wheel } = pointer;
58
56
  const { flush } = dispatch;
59
57
 
@@ -63,10 +61,9 @@ export const make = (
63
61
  approval: Approval | undefined,
64
62
  scroll: boolean,
65
63
  ) =>
66
- (approval === undefined
67
- ? evaluate(operation, scriptCall("point", target, scroll))
68
- : mutate(operation, scriptCall("point", target, scroll), approval)
69
- ).pipe(Effect.flatMap(decodeWith(operation, Script.PointResultSchema)));
64
+ evaluate(operation, scriptCall("point", target, scroll), approval).pipe(
65
+ Effect.flatMap(decodeWith(operation, Script.PointResultSchema)),
66
+ );
70
67
 
71
68
  const resolve = (operation: string, target: Target, approval?: Approval, scroll = true) => {
72
69
  if (typeof target === "string" && !/^e\d+$/.test(target))
@@ -127,7 +124,7 @@ export const make = (
127
124
  );
128
125
  yield* flush(operation, marks.input);
129
126
  yield* settling.settle(operation, before);
130
- if (approval !== undefined) yield* approval.check();
127
+ yield* check(approval);
131
128
  }
132
129
 
133
130
  // Wheels may be prevented or the target may need unsupported nested/frame geometry. One
@@ -137,7 +134,7 @@ export const make = (
137
134
 
138
135
  yield* resolve(operation, target, approval);
139
136
  yield* settling.settle(operation, before);
140
- if (approval !== undefined) yield* approval.check();
137
+ yield* check(approval);
141
138
 
142
139
  return yield* resolve(operation, target, approval, false);
143
140
  });
@@ -22,7 +22,7 @@ import type { BrowserEvent } from "../../BrowserEvent.ts";
22
22
  import type { InputGuard, Point } from "../../Page.ts";
23
23
  import type { Gate } from "../input/gate.ts";
24
24
  import { Current } from "../input/replies.ts";
25
- import type * as BrowserClock from "../pictures/clock.ts";
25
+ import * as BrowserClock from "../pictures/clock.ts";
26
26
  import * as Lane from "./lane.ts";
27
27
  import type { Openings } from "./openings.ts";
28
28
  import * as Url from "./url.ts";
@@ -168,7 +168,7 @@ export const decodeWith =
168
168
 
169
169
  export const make = (options: MakeOptions, scope: Scope.Scope) => {
170
170
  const { id, clock } = options;
171
- const now = () => Number(clock.monotonicTimeNanosUnsafe()) / 1e6;
171
+ const now = () => BrowserClock.monotonicMillis(clock);
172
172
  // Deadlines and pacing guard the page's lane, so page operations run on the owner's clock; a
173
173
  // caller's clock, such as a TestClock, cannot stall or stretch them.
174
174
  const owned = Effect.provideService(Clock.Clock, clock);