effect-browser 0.2.0-beta.9 → 0.3.0-beta.10
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 +849 -1161
- package/dist/Browser-S4kvpmqa.mjs +605 -0
- package/dist/Browser-S4kvpmqa.mjs.map +1 -0
- package/dist/Browser.d.mts +127 -3
- package/dist/Browser.mjs +2 -22
- package/dist/BrowserError-Coy7EW1N.d.mts +140 -0
- package/dist/BrowserError.d.mts +2 -0
- package/dist/BrowserError.mjs +211 -0
- package/dist/BrowserError.mjs.map +1 -0
- package/dist/BrowserEvent-BRu2juEz.d.mts +279 -0
- package/dist/BrowserEvent.d.mts +2 -0
- package/dist/BrowserEvent.mjs +297 -0
- package/dist/BrowserEvent.mjs.map +1 -0
- package/dist/Cdp.d.mts +32 -0
- package/dist/Cdp.mjs +61 -0
- package/dist/Cdp.mjs.map +1 -0
- package/dist/Change-DNvztNLu.mjs +138 -0
- package/dist/Change-DNvztNLu.mjs.map +1 -0
- package/dist/Change-iMlcuzVD.d.mts +71 -0
- package/dist/Change.d.mts +2 -0
- package/dist/Change.mjs +2 -0
- package/dist/Chromium.d.mts +29 -81
- package/dist/Chromium.mjs +57 -392
- package/dist/Chromium.mjs.map +1 -1
- package/dist/Expect.d.mts +62 -0
- package/dist/Expect.mjs +114 -0
- package/dist/Expect.mjs.map +1 -0
- package/dist/Frame-BERmf_gM.d.mts +117 -0
- package/dist/Frame.d.mts +2 -0
- package/dist/Frame.mjs +123 -0
- package/dist/Frame.mjs.map +1 -0
- package/dist/Moment.d.mts +2 -0
- package/dist/Moment.mjs +288 -0
- package/dist/Moment.mjs.map +1 -0
- package/dist/Motion.d.mts +40 -0
- package/dist/Motion.mjs +122 -0
- package/dist/Motion.mjs.map +1 -0
- package/dist/Page-B5cjaUp5.mjs +618 -0
- package/dist/Page-B5cjaUp5.mjs.map +1 -0
- package/dist/Page-BlPjfibv.d.mts +595 -0
- package/dist/Page.d.mts +2 -0
- package/dist/Page.mjs +2 -0
- package/dist/Plan-BI8XGvDu.d.mts +209 -0
- package/dist/Plan-D1wrJVs8.mjs +508 -0
- package/dist/Plan-D1wrJVs8.mjs.map +1 -0
- package/dist/Plan.d.mts +2 -3
- package/dist/Plan.mjs +2 -2
- package/dist/Policy.d.mts +76 -0
- package/dist/Policy.mjs +237 -0
- package/dist/Policy.mjs.map +1 -0
- package/dist/Presentation.d.mts +48 -0
- package/dist/Presentation.mjs +117 -0
- package/dist/Presentation.mjs.map +1 -0
- package/dist/Snapshot-B34hZ4Jl.d.mts +68 -0
- package/dist/Snapshot.d.mts +2 -0
- package/dist/Snapshot.mjs +79 -0
- package/dist/Snapshot.mjs.map +1 -0
- package/dist/Stage.d.mts +64 -0
- package/dist/Stage.mjs +237 -0
- package/dist/Stage.mjs.map +1 -0
- package/dist/Supervisor-CYWFK3vM.d.mts +280 -0
- package/dist/Supervisor-TNKNOVzu.mjs +599 -0
- package/dist/Supervisor-TNKNOVzu.mjs.map +1 -0
- package/dist/Supervisor.d.mts +2 -0
- package/dist/Supervisor.mjs +2 -0
- package/dist/context-B6xr8nVi.mjs +261 -0
- package/dist/context-B6xr8nVi.mjs.map +1 -0
- package/dist/index.d.mts +17 -14
- package/dist/index.mjs +18 -13
- package/dist/match.inpage-xOe9LNO0.mjs +48 -0
- package/dist/match.inpage-xOe9LNO0.mjs.map +1 -0
- package/dist/page-B8tUQELg.mjs +5073 -0
- package/dist/page-B8tUQELg.mjs.map +1 -0
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/dist/url-Cd04Okid.mjs +264 -0
- package/dist/url-Cd04Okid.mjs.map +1 -0
- package/package.json +96 -61
- package/src/Browser.ts +666 -0
- package/src/BrowserError.ts +234 -0
- package/src/BrowserEvent.ts +296 -0
- package/src/Cdp.ts +101 -0
- package/src/Change.ts +88 -0
- package/src/Chromium.ts +102 -0
- package/src/Expect.ts +180 -0
- package/src/Frame.ts +130 -0
- package/src/Moment.ts +479 -0
- package/src/Motion.ts +166 -0
- package/src/Page.ts +522 -0
- package/src/Plan.ts +663 -0
- package/src/Policy.ts +325 -0
- package/src/Presentation.ts +195 -0
- package/src/Snapshot.ts +89 -0
- package/src/Stage.ts +467 -0
- package/src/Supervisor.ts +599 -0
- package/src/index.ts +20 -0
- package/src/internal/input/actions.ts +610 -0
- package/src/internal/input/dispatch.ts +161 -0
- package/src/internal/input/edit.inpage.ts +162 -0
- package/src/internal/input/evidence.inpage.ts +164 -0
- package/src/internal/input/gate.ts +224 -0
- package/src/internal/input/guard.inpage.ts +605 -0
- package/src/internal/input/guard.ts +184 -0
- package/src/internal/input/keyboard.ts +112 -0
- package/src/internal/input/keys.ts +127 -0
- package/src/internal/input/perform.ts +376 -0
- package/src/internal/input/pointer.ts +153 -0
- package/src/internal/input/replies.ts +195 -0
- package/src/internal/input/settle.ts +50 -0
- package/src/internal/input/style.ts +186 -0
- package/src/internal/input/targets.inpage.ts +293 -0
- package/src/internal/input/targets.ts +148 -0
- package/src/internal/page/bridge.ts +386 -0
- package/src/internal/page/context.ts +312 -0
- package/src/internal/page/lane.ts +311 -0
- package/src/internal/page/navigation.ts +183 -0
- package/src/internal/page/openings.ts +87 -0
- package/src/internal/page/page.ts +165 -0
- package/src/internal/page/url.ts +100 -0
- package/src/internal/page/viewport.ts +43 -0
- package/src/internal/pictures/capture.ts +776 -0
- package/src/internal/pictures/clock.ts +141 -0
- package/src/internal/pictures/counts.ts +92 -0
- package/src/internal/pictures/pictures.ts +520 -0
- package/src/internal/pictures/transport.ts +242 -0
- package/src/internal/reading/choose.ts +100 -0
- package/src/internal/reading/context.inpage.ts +201 -0
- package/src/internal/reading/controls.inpage.ts +158 -0
- package/src/internal/reading/expect.inpage.ts +70 -0
- package/src/internal/reading/match.inpage.ts +92 -0
- package/src/internal/reading/names.inpage.ts +306 -0
- package/src/internal/reading/outline.inpage.ts +260 -0
- package/src/internal/reading/reading.ts +223 -0
- package/src/internal/reading/ready.inpage.ts +164 -0
- package/src/internal/reading/ready.ts +111 -0
- package/src/internal/reading/subjects.inpage.ts +188 -0
- package/src/internal/reading/text.inpage.ts +135 -0
- package/src/internal/reading/walk.inpage.ts +220 -0
- package/src/internal/supervisor/lifecycle.ts +416 -0
- package/src/internal/timeline/changes.inpage.ts +216 -0
- package/src/internal/timeline/changes.ts +100 -0
- package/src/internal/timeline/events.ts +94 -0
- package/src/internal/timeline/fold.inpage.ts +123 -0
- package/src/internal/timeline/history.inpage.ts +229 -0
- package/src/internal/timeline/marks.inpage.ts +207 -0
- package/src/internal/timeline/record.inpage.ts +133 -0
- package/src/internal/timeline/sight.inpage.ts +122 -0
- package/src/internal/timeline/window.ts +96 -0
- package/src/internal/timeline/words.ts +65 -0
- package/dist/Bootstrap-B9UUXsvF.d.mts +0 -183
- package/dist/Bootstrap-CeQOWBy6.mjs +0 -189
- package/dist/Bootstrap-CeQOWBy6.mjs.map +0 -1
- package/dist/Bootstrap.d.mts +0 -3
- package/dist/Bootstrap.mjs +0 -2
- package/dist/Browser-BF9_sORP.d.mts +0 -388
- package/dist/Browser.mjs.map +0 -1
- package/dist/BrowserData-CPIIIHol.d.mts +0 -585
- package/dist/BrowserData.d.mts +0 -3
- package/dist/BrowserData.mjs +0 -695
- package/dist/BrowserData.mjs.map +0 -1
- package/dist/BrowserRuntime-B6nT1G4X.d.mts +0 -148
- package/dist/BrowserRuntime-C1r2aSRK.mjs +0 -6820
- package/dist/BrowserRuntime-C1r2aSRK.mjs.map +0 -1
- package/dist/BrowserRuntime.d.mts +0 -3
- package/dist/BrowserRuntime.mjs +0 -3
- package/dist/Capture.d.mts +0 -56
- package/dist/Capture.mjs +0 -678
- package/dist/Capture.mjs.map +0 -1
- package/dist/CaptureData-CYriZHX1.mjs +0 -201
- package/dist/CaptureData-CYriZHX1.mjs.map +0 -1
- package/dist/CaptureData.d.mts +0 -219
- package/dist/CaptureData.mjs +0 -2
- package/dist/Errors.d.mts +0 -158
- package/dist/Errors.mjs +0 -207
- package/dist/Errors.mjs.map +0 -1
- package/dist/Images-yCG9BmRU.mjs +0 -76
- package/dist/Images-yCG9BmRU.mjs.map +0 -1
- package/dist/OperationOptions-DAeRS6O9.mjs +0 -55
- package/dist/OperationOptions-DAeRS6O9.mjs.map +0 -1
- package/dist/PageControl.d.mts +0 -15
- package/dist/PageControl.mjs +0 -34
- package/dist/PageControl.mjs.map +0 -1
- package/dist/PageRegistry-B3q9DS3g.mjs +0 -164
- package/dist/PageRegistry-B3q9DS3g.mjs.map +0 -1
- package/dist/Plan-Dyxu2q8C.mjs +0 -543
- package/dist/Plan-Dyxu2q8C.mjs.map +0 -1
- package/dist/PlanData-B668RxbQ.d.mts +0 -4831
- package/dist/PlanData.d.mts +0 -3
- package/dist/PlanData.mjs +0 -268
- package/dist/PlanData.mjs.map +0 -1
- package/dist/PlanInput-BJcPPncs.mjs +0 -75
- package/dist/PlanInput-BJcPPncs.mjs.map +0 -1
- package/dist/PublicSession-DCSMMqOj.mjs +0 -5602
- package/dist/PublicSession-DCSMMqOj.mjs.map +0 -1
- package/dist/Testing.d.mts +0 -479
- package/dist/Testing.mjs +0 -2147
- package/dist/Testing.mjs.map +0 -1
- package/dist/Timeline-fV22Xk-S.d.mts +0 -29
- package/dist/Timeline.d.mts +0 -4
- package/dist/Timeline.mjs +0 -20
- package/dist/Timeline.mjs.map +0 -1
- package/dist/TimelineData-BsOn-YGZ.d.mts +0 -3818
- package/dist/TimelineData.d.mts +0 -3
- package/dist/TimelineData.mjs +0 -482
- package/dist/TimelineData.mjs.map +0 -1
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded input replies owned by the page, including after an action is interrupted.
|
|
3
|
+
* Canceling a caller cannot cancel a protocol command already sent. The next action waits for
|
|
4
|
+
* those replies, while a stopped run releases only held keys or buttons whose release was never submitted.
|
|
5
|
+
*/
|
|
6
|
+
import { Context, Data, Effect } from "effect";
|
|
7
|
+
|
|
8
|
+
import { maximumSamples } from "../../Motion.ts";
|
|
9
|
+
|
|
10
|
+
export const capacity = 64;
|
|
11
|
+
|
|
12
|
+
export class InputFailure extends Data.TaggedError("InputFailure")<{
|
|
13
|
+
readonly cause: unknown;
|
|
14
|
+
}> {}
|
|
15
|
+
|
|
16
|
+
export interface Run {
|
|
17
|
+
/** A native call made by this operation: cancellation cannot cancel its actual reply. */
|
|
18
|
+
readonly track: <A>(reply: Promise<A>) => Promise<A>;
|
|
19
|
+
/** End ownership after the record and all this operation's native replies have settled. */
|
|
20
|
+
readonly afterIdle: (ended: () => void) => void;
|
|
21
|
+
/** Reserve a complete stroke, including its releases, before sending its first event. */
|
|
22
|
+
readonly reserve: (commands: number) => Effect.Effect<void, InputFailure>;
|
|
23
|
+
/** Admit every sample before a motion starts its published clock; ordinary input stays bounded separately. */
|
|
24
|
+
readonly reserveMotion: (commands: number) => Effect.Effect<void, InputFailure>;
|
|
25
|
+
readonly send: (command: () => Promise<unknown>) => Effect.Effect<void, InputFailure>;
|
|
26
|
+
readonly down: (
|
|
27
|
+
key: string,
|
|
28
|
+
press: () => Promise<unknown>,
|
|
29
|
+
release: () => Promise<unknown>,
|
|
30
|
+
) => Effect.Effect<void, InputFailure>;
|
|
31
|
+
readonly up: (key: string) => Effect.Effect<void, InputFailure>;
|
|
32
|
+
readonly drain: Effect.Effect<void, InputFailure>;
|
|
33
|
+
/** Stop future input and submit missing releases without waiting indefinitely for replies. */
|
|
34
|
+
readonly close: Effect.Effect<void>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Native page calls join their operation's reply ownership, including script mutations. */
|
|
38
|
+
export const Current = Context.Reference<Run | undefined>("effect-browser/internal/input/Current", {
|
|
39
|
+
defaultValue: () => undefined,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
export const make = () => {
|
|
43
|
+
const outstanding = new Set<Promise<void>>();
|
|
44
|
+
|
|
45
|
+
// A timed-out action keeps capacity until its real replies arrive. Waiting for them sends
|
|
46
|
+
// nothing, so cancellation here cannot leave a detached continuation that later types.
|
|
47
|
+
const idle = Effect.gen(function* () {
|
|
48
|
+
while (outstanding.size > 0) yield* Effect.promise(() => Promise.all(outstanding));
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const begin = Effect.gen(function* () {
|
|
52
|
+
yield* idle;
|
|
53
|
+
const pending = new Set<Promise<void>>();
|
|
54
|
+
const held: Array<{ readonly key: string; readonly release: () => Promise<unknown> }> = [];
|
|
55
|
+
let failure: InputFailure | undefined;
|
|
56
|
+
let reserved = 0;
|
|
57
|
+
let closed = false;
|
|
58
|
+
const observed = new WeakMap<Promise<unknown>, { input: boolean }>();
|
|
59
|
+
const endings = new Set<() => void>();
|
|
60
|
+
|
|
61
|
+
const track = <A>(response: Promise<A>, input = false): Promise<A> => {
|
|
62
|
+
const existing = observed.get(response);
|
|
63
|
+
|
|
64
|
+
if (existing !== undefined) {
|
|
65
|
+
existing.input ||= input;
|
|
66
|
+
|
|
67
|
+
return response;
|
|
68
|
+
}
|
|
69
|
+
const entry = { input };
|
|
70
|
+
|
|
71
|
+
observed.set(response, entry);
|
|
72
|
+
|
|
73
|
+
const reply = response
|
|
74
|
+
.then(
|
|
75
|
+
() => undefined,
|
|
76
|
+
(cause: unknown) => {
|
|
77
|
+
if (entry.input) failure ??= new InputFailure({ cause });
|
|
78
|
+
},
|
|
79
|
+
)
|
|
80
|
+
.then(() => {
|
|
81
|
+
pending.delete(reply);
|
|
82
|
+
outstanding.delete(reply);
|
|
83
|
+
if (pending.size === 0) {
|
|
84
|
+
for (const end of endings) end();
|
|
85
|
+
endings.clear();
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
pending.add(reply);
|
|
90
|
+
outstanding.add(reply);
|
|
91
|
+
|
|
92
|
+
return response;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const requireOpen = Effect.suspend(() =>
|
|
96
|
+
closed
|
|
97
|
+
? Effect.fail(new InputFailure({ cause: new Error("the input run has stopped") }))
|
|
98
|
+
: Effect.void,
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const requireSuccess = Effect.suspend(() =>
|
|
102
|
+
failure === undefined ? Effect.void : Effect.fail(failure),
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const submit = (command: () => Promise<unknown>) => {
|
|
106
|
+
if (reserved < 1) throw new Error("input must reserve capacity before dispatch");
|
|
107
|
+
reserved--;
|
|
108
|
+
let response: Promise<unknown>;
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
response = command();
|
|
112
|
+
} catch (cause) {
|
|
113
|
+
failure ??= new InputFailure({ cause });
|
|
114
|
+
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
void track(response, true);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const reserve = (commands: number, maximum: number, limit: number) =>
|
|
122
|
+
Effect.gen(function* () {
|
|
123
|
+
yield* requireOpen;
|
|
124
|
+
yield* requireSuccess;
|
|
125
|
+
if (!Number.isInteger(commands) || commands < 1 || commands > maximum)
|
|
126
|
+
return yield* Effect.die(new Error("invalid input reservation"));
|
|
127
|
+
while (outstanding.size + reserved + commands > limit) {
|
|
128
|
+
yield* Effect.promise(() => Promise.race(outstanding));
|
|
129
|
+
yield* requireOpen;
|
|
130
|
+
yield* requireSuccess;
|
|
131
|
+
}
|
|
132
|
+
reserved += commands;
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const run: Run = {
|
|
136
|
+
track,
|
|
137
|
+
afterIdle: (end) => {
|
|
138
|
+
if (pending.size === 0) end();
|
|
139
|
+
else endings.add(end);
|
|
140
|
+
},
|
|
141
|
+
reserve: (commands) => reserve(commands, capacity, capacity),
|
|
142
|
+
// A dense stroke cannot wait for replies midway without changing its published timing.
|
|
143
|
+
// Its finite reservation also leaves room for the ordinary pool's already-owned releases.
|
|
144
|
+
reserveMotion: (commands) => reserve(commands, maximumSamples, capacity + maximumSamples),
|
|
145
|
+
send: (command) =>
|
|
146
|
+
Effect.gen(function* () {
|
|
147
|
+
yield* requireOpen;
|
|
148
|
+
yield* requireSuccess;
|
|
149
|
+
yield* Effect.sync(() => submit(command));
|
|
150
|
+
yield* requireSuccess;
|
|
151
|
+
}),
|
|
152
|
+
down: (key, press, release) =>
|
|
153
|
+
Effect.gen(function* () {
|
|
154
|
+
yield* requireOpen;
|
|
155
|
+
yield* requireSuccess;
|
|
156
|
+
yield* Effect.sync(() => {
|
|
157
|
+
held.push({ key, release });
|
|
158
|
+
submit(press);
|
|
159
|
+
});
|
|
160
|
+
yield* requireSuccess;
|
|
161
|
+
}),
|
|
162
|
+
up: (key) =>
|
|
163
|
+
Effect.gen(function* () {
|
|
164
|
+
yield* requireOpen;
|
|
165
|
+
yield* Effect.sync(() => {
|
|
166
|
+
const index = held.findLastIndex((entry) => entry.key === key);
|
|
167
|
+
const entry = held[index];
|
|
168
|
+
|
|
169
|
+
if (entry === undefined) return;
|
|
170
|
+
// Submission owns the release, even if its reply fails. Retrying an uncertain
|
|
171
|
+
// release could run page handlers twice, so cleanup never sends it again.
|
|
172
|
+
held.splice(index, 1);
|
|
173
|
+
submit(entry.release);
|
|
174
|
+
});
|
|
175
|
+
yield* requireSuccess;
|
|
176
|
+
}),
|
|
177
|
+
drain: Effect.gen(function* () {
|
|
178
|
+
while (pending.size > 0) yield* Effect.promise(() => Promise.all(pending));
|
|
179
|
+
yield* requireSuccess;
|
|
180
|
+
}),
|
|
181
|
+
close: Effect.sync(() => {
|
|
182
|
+
if (closed) return;
|
|
183
|
+
closed = true;
|
|
184
|
+
for (const entry of held.toReversed()) submit(entry.release);
|
|
185
|
+
held.length = 0;
|
|
186
|
+
reserved = 0;
|
|
187
|
+
}),
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
return run;
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
/** `idle` lets an action wait, in its page's turn, for the page's earlier replies. */
|
|
194
|
+
return { idle, begin };
|
|
195
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wait after input: one task and one frame in the page, in one call, then a document only if
|
|
3
|
+
* one committed meanwhile, until it is parsed. Chromium answers a call to a page that a link, a form
|
|
4
|
+
* or a script's timer has asked to navigate only once the new document commits, or the navigation
|
|
5
|
+
* stops without one, as on a 204, so the call spans the navigation the input asked for. A
|
|
6
|
+
* `pushState` asks for no document, and a navigation a handler starts after a fetch answers is the
|
|
7
|
+
* next look's to see. A tab the input opened waits for the browser's registration too.
|
|
8
|
+
*/
|
|
9
|
+
import { Deferred, Duration, Effect, Exit } from "effect";
|
|
10
|
+
|
|
11
|
+
import { type Bridge, scriptCall } from "../page/bridge.ts";
|
|
12
|
+
import { contextGone, type PageContext } from "../page/context.ts";
|
|
13
|
+
|
|
14
|
+
// A document still loading after this, or a closed page, is the next look's to show.
|
|
15
|
+
const settling = Duration.seconds(5);
|
|
16
|
+
|
|
17
|
+
export const make = (page: PageContext, bridge: Bridge) => {
|
|
18
|
+
const document = () => bridge.frameTag().document;
|
|
19
|
+
// The latest document whose DOMContentLoaded came, and what wakes a wait for the next.
|
|
20
|
+
let parsed = -1;
|
|
21
|
+
let changed = Deferred.makeUnsafe<void>();
|
|
22
|
+
|
|
23
|
+
page.cdp.on("Page.domContentEventFired", () => {
|
|
24
|
+
parsed = document();
|
|
25
|
+
Deferred.doneUnsafe(changed, Exit.void);
|
|
26
|
+
changed = Deferred.makeUnsafe<void>();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
/** The page's document before input, which `settle` measures from. */
|
|
30
|
+
const mark = () => ({ document: document(), openings: page.openings.mark() });
|
|
31
|
+
|
|
32
|
+
/** Wait for the page to settle after input sent since `before`. */
|
|
33
|
+
const settle = (operation: string, before: ReturnType<typeof mark>) =>
|
|
34
|
+
Effect.gen(function* () {
|
|
35
|
+
// A commit ends the call's world; the loop then waits for that document.
|
|
36
|
+
yield* bridge
|
|
37
|
+
.evaluate(operation, scriptCall("settle"))
|
|
38
|
+
.pipe(Effect.catchIf(contextGone, () => Effect.void));
|
|
39
|
+
while (document() > before.document && parsed < document()) yield* Deferred.await(changed);
|
|
40
|
+
}).pipe(
|
|
41
|
+
Effect.timeoutOrElse({ duration: settling, orElse: () => Effect.fail("unsettled") }),
|
|
42
|
+
Effect.catch(() => Effect.annotateCurrentSpan("settled", false)),
|
|
43
|
+
Effect.andThen(page.openings.settle(before.openings)),
|
|
44
|
+
page.span("Page.settle", {}, "Debug"),
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
return { mark, settle };
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type Settle = ReturnType<typeof make>;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How input is sent: plainly, as fast as the page takes it, or performed for viewers, with a
|
|
3
|
+
* pointer that glides, keys typed at a person's pace, held buttons and wheels in bursts. A page's
|
|
4
|
+
* actions take one, and the input reaches the same controls either way; only its timing, and
|
|
5
|
+
* what viewers see of it, differ. A presenter builds the performed one.
|
|
6
|
+
*/
|
|
7
|
+
import { Effect, type MutableRef, type Option, Random } from "effect";
|
|
8
|
+
|
|
9
|
+
import type * as Motion from "../../Motion.ts";
|
|
10
|
+
import type { Point } from "../../Page.ts";
|
|
11
|
+
import * as Keys from "./keys.ts";
|
|
12
|
+
|
|
13
|
+
export interface TypingEvent {
|
|
14
|
+
/** From the start of the typing. */
|
|
15
|
+
readonly afterMillis: number;
|
|
16
|
+
readonly phase: "down" | "up" | "insert";
|
|
17
|
+
readonly key: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface WheelStep {
|
|
21
|
+
readonly dx: number;
|
|
22
|
+
readonly dy: number;
|
|
23
|
+
/** From the first step. */
|
|
24
|
+
readonly afterMillis: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface Style {
|
|
28
|
+
/**
|
|
29
|
+
* Whether viewers watch the input: then the pointer clicks into a field before typing, finds
|
|
30
|
+
* what lies under it as it scrolls, and scrolls to a target out of view with the wheel.
|
|
31
|
+
*/
|
|
32
|
+
readonly shown: boolean;
|
|
33
|
+
/** Where viewers last saw the pointer, which the next glide starts from; else the page's own. */
|
|
34
|
+
readonly pointer: MutableRef.MutableRef<Option.Option<Point>> | undefined;
|
|
35
|
+
/** The pointer's path to `to`, ending exactly there. */
|
|
36
|
+
readonly glide: (
|
|
37
|
+
from: Point,
|
|
38
|
+
to: Point,
|
|
39
|
+
dragging: boolean,
|
|
40
|
+
) => Effect.Effect<ReadonlyArray<Motion.Sample>>;
|
|
41
|
+
/** How long a button stays down when a click does not say. */
|
|
42
|
+
readonly hold: Effect.Effect<number>;
|
|
43
|
+
/** The key events that type `text`, or one insertion of it under a guard, timed from the start. */
|
|
44
|
+
readonly typing: (text: string, guarded: boolean) => Effect.Effect<ReadonlyArray<TypingEvent>>;
|
|
45
|
+
/** The pause between repeated presses of the same keys. */
|
|
46
|
+
readonly keyGap: Effect.Effect<number>;
|
|
47
|
+
/** A wheel scroll as the wheel turns: in one step, or in notches and bursts. */
|
|
48
|
+
readonly wheel: (dx: number, dy: number) => Effect.Effect<ReadonlyArray<WheelStep>>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Each key as it comes, with no pause; a drag moves in eight short steps so its target sees it. */
|
|
52
|
+
export const plain: Style = {
|
|
53
|
+
shown: false,
|
|
54
|
+
pointer: undefined,
|
|
55
|
+
glide: (from, to, dragging) =>
|
|
56
|
+
Effect.succeed(
|
|
57
|
+
dragging
|
|
58
|
+
? Array.from({ length: 8 }, (_, index) => ({
|
|
59
|
+
x: index === 7 ? to.x : from.x + ((to.x - from.x) * (index + 1)) / 8,
|
|
60
|
+
y: index === 7 ? to.y : from.y + ((to.y - from.y) * (index + 1)) / 8,
|
|
61
|
+
afterMillis: (index + 1) * 8,
|
|
62
|
+
}))
|
|
63
|
+
: [{ ...to, afterMillis: 0 }],
|
|
64
|
+
),
|
|
65
|
+
hold: Effect.succeed(0),
|
|
66
|
+
typing: (text, guarded) =>
|
|
67
|
+
Effect.succeed(
|
|
68
|
+
guarded && text !== ""
|
|
69
|
+
? [{ afterMillis: 0, phase: "insert", key: text }]
|
|
70
|
+
: [...text].flatMap((key): ReadonlyArray<TypingEvent> =>
|
|
71
|
+
Keys.description(key) === undefined
|
|
72
|
+
? [{ afterMillis: 0, phase: "insert", key }]
|
|
73
|
+
: [
|
|
74
|
+
{ afterMillis: 0, phase: "down", key },
|
|
75
|
+
{ afterMillis: 0, phase: "up", key },
|
|
76
|
+
],
|
|
77
|
+
),
|
|
78
|
+
),
|
|
79
|
+
keyGap: Effect.succeed(0),
|
|
80
|
+
wheel: (dx, dy) => Effect.succeed([{ dx, dy, afterMillis: 0 }]),
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// A right-skewed draw, clamped so that cleanup and deadlines stay predictable; 1 - U keeps the
|
|
84
|
+
// logarithm defined when Random returns zero.
|
|
85
|
+
const lognormal = Effect.fnUntraced(function* (
|
|
86
|
+
median: number,
|
|
87
|
+
sigma: number,
|
|
88
|
+
minimum: number,
|
|
89
|
+
maximum: number,
|
|
90
|
+
) {
|
|
91
|
+
const radius = Math.sqrt(-2 * Math.log(1 - (yield* Random.next)));
|
|
92
|
+
const normal = radius * Math.cos(2 * Math.PI * (yield* Random.next));
|
|
93
|
+
|
|
94
|
+
return Math.max(minimum, Math.min(maximum, median * Math.exp(sigma * normal)));
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
/** A pause around `median`, as the research measured reactions: at most four times it. */
|
|
98
|
+
export const pause = (median: number, sigma: number) =>
|
|
99
|
+
median <= 0 ? Effect.succeed(0) : lognormal(median, sigma, 0, median * 4);
|
|
100
|
+
|
|
101
|
+
// A mouse press about 80 ms long on average, a key about 110 ms, each now and then longer: their
|
|
102
|
+
// medians are the means times exp(-sigma^2 / 2).
|
|
103
|
+
const pressHold = lognormal(75.2, 0.35, 35, 200);
|
|
104
|
+
const keyHold = lognormal(107.8, 0.2, 60, 220);
|
|
105
|
+
const eventOrder = { up: 0, insert: 1, down: 2 };
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Plan the whole text before any key goes, so holds overlap without waiting on the network. A
|
|
109
|
+
* physical key is never held twice, as with A and a, or ! and 1. The first key waits for the hand
|
|
110
|
+
* to reach the keys, each later one follows the last by a gap drawn around `gap`, and a word's
|
|
111
|
+
* first key comes 60 ms later.
|
|
112
|
+
*/
|
|
113
|
+
const typing = Effect.fnUntraced(function* (
|
|
114
|
+
text: string,
|
|
115
|
+
gap: number,
|
|
116
|
+
): Effect.fn.Return<ReadonlyArray<TypingEvent>> {
|
|
117
|
+
const events: Array<TypingEvent> = [];
|
|
118
|
+
const releases = new Map<string, number>();
|
|
119
|
+
let due = text === "" ? 0 : yield* Random.nextBetween(150, 400);
|
|
120
|
+
let previous = "";
|
|
121
|
+
|
|
122
|
+
for (const character of text) {
|
|
123
|
+
if (/\s/u.test(previous) && /[\p{L}\p{N}]/u.test(character)) due += 60;
|
|
124
|
+
const description = Keys.description(character);
|
|
125
|
+
|
|
126
|
+
if (description === undefined)
|
|
127
|
+
events.push({ afterMillis: due, phase: "insert", key: character });
|
|
128
|
+
else {
|
|
129
|
+
due = Math.max(due, (releases.get(description.code) ?? -1) + 1);
|
|
130
|
+
const released = due + (yield* keyHold);
|
|
131
|
+
|
|
132
|
+
events.push(
|
|
133
|
+
{ afterMillis: due, phase: "down", key: character },
|
|
134
|
+
{ afterMillis: released, phase: "up", key: character },
|
|
135
|
+
);
|
|
136
|
+
releases.set(description.code, released);
|
|
137
|
+
}
|
|
138
|
+
due += yield* Random.nextBetween(gap / 2, (gap * 3) / 2);
|
|
139
|
+
previous = character;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return events.toSorted(
|
|
143
|
+
(left, right) =>
|
|
144
|
+
left.afterMillis - right.afterMillis || eventOrder[left.phase] - eventOrder[right.phase],
|
|
145
|
+
);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
/** A word is five characters; the gap leaves room for each word's later start. */
|
|
149
|
+
export const keyGap = (wordsPerMinute: number) => 12_000 / wordsPerMinute - 10;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* The wheel in 100 px notches, 30 to 140 ms apart, in bursts of at most nine, with 0.6 to 2.6 s
|
|
153
|
+
* between bursts, as people scroll.
|
|
154
|
+
*/
|
|
155
|
+
const bursts = Effect.fnUntraced(function* (dx: number, dy: number) {
|
|
156
|
+
const notches = Math.max(1, Math.ceil(Math.hypot(dx, dy) / 100));
|
|
157
|
+
const steps: Array<WheelStep> = [];
|
|
158
|
+
let due = 0;
|
|
159
|
+
|
|
160
|
+
for (let index = 0; index < notches; index++) {
|
|
161
|
+
if (index > 0)
|
|
162
|
+
due += yield* index % 9 === 0 ? Random.nextBetween(600, 2600) : Random.nextBetween(30, 140);
|
|
163
|
+
steps.push({ dx: dx / notches, dy: dy / notches, afterMillis: due });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return steps;
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
/** Input performed for viewers, with this pointer and pace. */
|
|
170
|
+
export const performed = (options: {
|
|
171
|
+
readonly pointer: MutableRef.MutableRef<Option.Option<Point>>;
|
|
172
|
+
readonly motion: Motion.Service;
|
|
173
|
+
readonly wordsPerMinute: number;
|
|
174
|
+
}): Style => {
|
|
175
|
+
const gap = keyGap(options.wordsPerMinute);
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
shown: true,
|
|
179
|
+
pointer: options.pointer,
|
|
180
|
+
glide: (from, to) => options.motion.plan(from, to),
|
|
181
|
+
hold: pressHold,
|
|
182
|
+
typing: (text) => typing(text, gap),
|
|
183
|
+
keyGap: Random.nextBetween(gap / 2, (gap * 3) / 2),
|
|
184
|
+
wheel: bursts,
|
|
185
|
+
};
|
|
186
|
+
};
|