effect-browser 0.2.0-beta.9 → 0.3.0-beta.11
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 +861 -1161
- package/dist/Browser-B4qncxe7.mjs +628 -0
- package/dist/Browser-B4qncxe7.mjs.map +1 -0
- package/dist/Browser.d.mts +129 -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-Di2PxyF7.d.mts +119 -0
- package/dist/Frame.d.mts +2 -0
- package/dist/Frame.mjs +125 -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-B7X6DQUn.d.mts +595 -0
- package/dist/Page.d.mts +2 -0
- package/dist/Page.mjs +2 -0
- package/dist/Plan-Akcac63g.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-BAf__WKX.mjs +5075 -0
- package/dist/page-BAf__WKX.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 +672 -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 +132 -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 +391 -0
- package/src/internal/page/context.ts +314 -0
- package/src/internal/page/identity.ts +35 -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 +521 -0
- package/src/internal/pictures/transport.ts +243 -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,416 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A supervisor's life as one state and one transition function, and the generation states it
|
|
3
|
+
* publishes. The transition has no effects: `Supervisor.make` applies each input under one lock,
|
|
4
|
+
* publishes the events in order and runs the commands, so the rules here are tested alone.
|
|
5
|
+
*/
|
|
6
|
+
import { Schema } from "effect";
|
|
7
|
+
|
|
8
|
+
import { DisconnectCause } from "../../BrowserEvent.ts";
|
|
9
|
+
|
|
10
|
+
/** The provider confirmed that the generation's browser ended. */
|
|
11
|
+
export class Settled extends Schema.TaggedClass<Settled>()("Settled", {}) {}
|
|
12
|
+
|
|
13
|
+
/** The provider could not confirm the end by its deadline, so the browser may still run, and bill. */
|
|
14
|
+
export class Unconfirmed extends Schema.TaggedClass<Unconfirmed>()("Unconfirmed", {
|
|
15
|
+
detail: Schema.String,
|
|
16
|
+
}) {}
|
|
17
|
+
|
|
18
|
+
export const Released = Schema.Union([Settled, Unconfirmed]);
|
|
19
|
+
|
|
20
|
+
export type Released = typeof Released.Type;
|
|
21
|
+
|
|
22
|
+
/** Opening, but not after a loss: the first generation, a rotation, or the one after `Down`. */
|
|
23
|
+
export class Opening extends Schema.TaggedClass<Opening>()("Opening", {}) {}
|
|
24
|
+
|
|
25
|
+
/** Opening after the generation before it was lost. */
|
|
26
|
+
export class Reopening extends Schema.TaggedClass<Reopening>()("Reopening", {}) {}
|
|
27
|
+
|
|
28
|
+
/** Open: `browser` gives this generation until the next one opens. */
|
|
29
|
+
export class Open extends Schema.TaggedClass<Open>()("Open", {}) {}
|
|
30
|
+
|
|
31
|
+
/** No longer serving; holds end at this host-monotonic deadline or when their owners let go. */
|
|
32
|
+
export class Draining extends Schema.TaggedClass<Draining>()("Draining", {
|
|
33
|
+
reason: Schema.Literals(["rotated", "lost", "retired"]),
|
|
34
|
+
revokeAt: Schema.Finite,
|
|
35
|
+
}) {}
|
|
36
|
+
|
|
37
|
+
/** Lost: its connection dropped, or its provider ended its session, as `cause` says. */
|
|
38
|
+
export class Lost extends Schema.TaggedClass<Lost>()("Lost", {
|
|
39
|
+
cause: DisconnectCause,
|
|
40
|
+
}) {}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* It never opened: its last try failed with `cause`, which the provider deems definite, or after
|
|
44
|
+
* which the reopen schedule gave up.
|
|
45
|
+
*/
|
|
46
|
+
export class Down extends Schema.TaggedClass<Down>()("Down", {
|
|
47
|
+
detail: Schema.String,
|
|
48
|
+
cause: Schema.Defect(),
|
|
49
|
+
}) {}
|
|
50
|
+
|
|
51
|
+
/** It ended. `released` says how its release went; one retired while opening has none. */
|
|
52
|
+
export class Closed extends Schema.TaggedClass<Closed>()("Closed", {
|
|
53
|
+
released: Schema.optional(Released),
|
|
54
|
+
}) {}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* It was serving as the supervisor's scope closed, and was left running, not released: its scope
|
|
58
|
+
* closed, and its provider runs it on, and bills, until something adopts and releases it.
|
|
59
|
+
*/
|
|
60
|
+
export class Kept extends Schema.TaggedClass<Kept>()("Kept", {}) {}
|
|
61
|
+
|
|
62
|
+
export const GenerationState = Schema.Union([
|
|
63
|
+
Opening,
|
|
64
|
+
Reopening,
|
|
65
|
+
Open,
|
|
66
|
+
Draining,
|
|
67
|
+
Lost,
|
|
68
|
+
Down,
|
|
69
|
+
Closed,
|
|
70
|
+
Kept,
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
export type GenerationState = typeof GenerationState.Type;
|
|
74
|
+
|
|
75
|
+
/** A generation that opened: its number, and what the runtime keeps to serve and end it. */
|
|
76
|
+
export interface Live<A> {
|
|
77
|
+
readonly number: number;
|
|
78
|
+
readonly value: A;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface Held<A, H> extends Live<A> {
|
|
82
|
+
readonly holds: ReadonlySet<H>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Release ownership remains here until the provider and generation scope have finished. */
|
|
86
|
+
export interface Drain<A, H> {
|
|
87
|
+
readonly _tag: "Draining" | "Releasing" | "Leaving";
|
|
88
|
+
readonly live: Held<A, H>;
|
|
89
|
+
readonly ending: Draining;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** One serving generation, one shared acquisition, and prior generations still owned. */
|
|
93
|
+
export type State<A, H = unknown> = (
|
|
94
|
+
| { readonly _tag: "Opening"; readonly number: number; readonly serving: Held<A, H> | undefined }
|
|
95
|
+
| { readonly _tag: "Open"; readonly serving: Held<A, H> }
|
|
96
|
+
| {
|
|
97
|
+
readonly _tag: "Down";
|
|
98
|
+
readonly number: number;
|
|
99
|
+
readonly detail: string;
|
|
100
|
+
readonly cause: unknown;
|
|
101
|
+
readonly serving: Held<A, H> | undefined;
|
|
102
|
+
}
|
|
103
|
+
| { readonly _tag: "Retired" }
|
|
104
|
+
) & { readonly draining: ReadonlyMap<number, Drain<A, H>> };
|
|
105
|
+
|
|
106
|
+
export type Input<A, H = unknown> =
|
|
107
|
+
| { readonly _tag: "Opened"; readonly live: Live<A> }
|
|
108
|
+
| {
|
|
109
|
+
readonly _tag: "Failed";
|
|
110
|
+
readonly number: number;
|
|
111
|
+
readonly detail: string;
|
|
112
|
+
readonly cause: unknown;
|
|
113
|
+
}
|
|
114
|
+
| { readonly _tag: "Abandoned"; readonly number: number }
|
|
115
|
+
| { readonly _tag: "Lost"; readonly number: number; readonly cause: DisconnectCause }
|
|
116
|
+
| { readonly _tag: "Rotate"; readonly due?: number | undefined }
|
|
117
|
+
| { readonly _tag: "Retire" }
|
|
118
|
+
| { readonly _tag: "Keep" }
|
|
119
|
+
| {
|
|
120
|
+
readonly _tag: "Hold";
|
|
121
|
+
readonly number?: number | undefined;
|
|
122
|
+
readonly hold: H;
|
|
123
|
+
}
|
|
124
|
+
| { readonly _tag: "Let"; readonly number: number; readonly hold: H }
|
|
125
|
+
| { readonly _tag: "Revoke"; readonly number: number }
|
|
126
|
+
| {
|
|
127
|
+
readonly _tag: "Released";
|
|
128
|
+
readonly number: number;
|
|
129
|
+
readonly released?: Released | undefined;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export type Command<A> =
|
|
133
|
+
| { readonly _tag: "Open"; readonly number: number; readonly after: number | undefined }
|
|
134
|
+
| { readonly _tag: "Watch"; readonly live: Live<A> }
|
|
135
|
+
/** Signal ending and arrange revocation. Repeated only when loss/retirement accelerates it. */
|
|
136
|
+
| { readonly _tag: "Drain"; readonly live: Live<A>; readonly ending: Draining }
|
|
137
|
+
/** Revoke/close holds, await their Let inputs, release the provider, then close its scope. */
|
|
138
|
+
| { readonly _tag: "Release"; readonly live: Live<A>; readonly ending: Draining }
|
|
139
|
+
| { readonly _tag: "Leave"; readonly live: Live<A>; readonly ending: Draining };
|
|
140
|
+
|
|
141
|
+
export interface Step<A, H = unknown> {
|
|
142
|
+
readonly state: State<A, H>;
|
|
143
|
+
readonly events: ReadonlyArray<readonly [number, GenerationState]>;
|
|
144
|
+
readonly commands: ReadonlyArray<Command<A>>;
|
|
145
|
+
readonly reply?: number | undefined;
|
|
146
|
+
readonly borrowed?: Live<A> | undefined;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export const start = <A, H = unknown>(): Step<A, H> => ({
|
|
150
|
+
state: { _tag: "Opening", number: 1, serving: undefined, draining: new Map() },
|
|
151
|
+
events: [[1, new Opening()]],
|
|
152
|
+
commands: [{ _tag: "Open", number: 1, after: undefined }],
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const servingOf = <A, H>(state: State<A, H>) =>
|
|
156
|
+
state._tag === "Retired" ? undefined : state.serving;
|
|
157
|
+
|
|
158
|
+
/** The holds still owned for a generation, including one whose release has begun. */
|
|
159
|
+
export const lookup = <A, H>(state: State<A, H>, number: number): Held<A, H> | undefined =>
|
|
160
|
+
servingOf(state)?.number === number ? servingOf(state) : state.draining.get(number)?.live;
|
|
161
|
+
|
|
162
|
+
/** A transition's local result builder; input state and its collections remain immutable. */
|
|
163
|
+
interface Change<A, H> {
|
|
164
|
+
readonly now: number;
|
|
165
|
+
readonly handover: number;
|
|
166
|
+
readonly exclusive: boolean;
|
|
167
|
+
state: State<A, H>;
|
|
168
|
+
events: Array<readonly [number, GenerationState]>;
|
|
169
|
+
commands: Array<Command<A>>;
|
|
170
|
+
reply?: number;
|
|
171
|
+
borrowed?: Live<A>;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** The sole writer of a drain's ending and phase; each release command is emitted once. */
|
|
175
|
+
const draining = <A, H>(change: Change<A, H>, drain: Drain<A, H>) => {
|
|
176
|
+
const before = change.state.draining.get(drain.live.number);
|
|
177
|
+
const { live, ending, _tag } = drain;
|
|
178
|
+
|
|
179
|
+
change.state = {
|
|
180
|
+
...change.state,
|
|
181
|
+
draining: new Map(change.state.draining).set(live.number, drain),
|
|
182
|
+
};
|
|
183
|
+
if (before?.ending !== ending) {
|
|
184
|
+
change.events.push([live.number, ending]);
|
|
185
|
+
change.commands.push({ _tag: "Drain", live, ending });
|
|
186
|
+
}
|
|
187
|
+
if (_tag !== "Draining" && before?._tag !== _tag)
|
|
188
|
+
change.commands.push({ _tag: _tag === "Leaving" ? "Leave" : "Release", live, ending });
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const end = <A, H>(
|
|
192
|
+
change: Change<A, H>,
|
|
193
|
+
live: Held<A, H>,
|
|
194
|
+
reason: Draining["reason"],
|
|
195
|
+
keep = false,
|
|
196
|
+
) => {
|
|
197
|
+
const ending = new Draining({
|
|
198
|
+
reason,
|
|
199
|
+
revokeAt: change.now + (reason === "rotated" ? change.handover : 0),
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
draining(change, {
|
|
203
|
+
live,
|
|
204
|
+
ending,
|
|
205
|
+
_tag: keep
|
|
206
|
+
? "Leaving"
|
|
207
|
+
: live.holds.size === 0 || ending.revokeAt <= change.now
|
|
208
|
+
? "Releasing"
|
|
209
|
+
: "Draining",
|
|
210
|
+
});
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const revoke = <A, H>(change: Change<A, H>, number: number, reason?: "lost" | "retired") => {
|
|
214
|
+
const drain = change.state.draining.get(number);
|
|
215
|
+
|
|
216
|
+
if (drain?._tag !== "Draining" || (reason === undefined && change.now < drain.ending.revokeAt))
|
|
217
|
+
return;
|
|
218
|
+
draining(change, {
|
|
219
|
+
...drain,
|
|
220
|
+
_tag: "Releasing",
|
|
221
|
+
ending: reason === undefined ? drain.ending : new Draining({ reason, revokeAt: change.now }),
|
|
222
|
+
});
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const holds = <A, H>(
|
|
226
|
+
change: Change<A, H>,
|
|
227
|
+
input: Extract<Input<A, H>, { readonly _tag: "Hold" | "Let" }>,
|
|
228
|
+
) => {
|
|
229
|
+
const number = input.number ?? servingOf(change.state)?.number;
|
|
230
|
+
const live = number === undefined ? undefined : lookup(change.state, number);
|
|
231
|
+
|
|
232
|
+
if (live === undefined) return;
|
|
233
|
+
const drain = change.state.draining.get(live.number);
|
|
234
|
+
|
|
235
|
+
if (
|
|
236
|
+
input._tag === "Hold" &&
|
|
237
|
+
drain !== undefined &&
|
|
238
|
+
(drain._tag !== "Draining" || change.now >= drain.ending.revokeAt)
|
|
239
|
+
)
|
|
240
|
+
return;
|
|
241
|
+
if (live.holds.has(input.hold) === (input._tag === "Hold")) return;
|
|
242
|
+
const holds = new Set(live.holds);
|
|
243
|
+
|
|
244
|
+
if (input._tag === "Hold") {
|
|
245
|
+
holds.add(input.hold);
|
|
246
|
+
change.borrowed = live;
|
|
247
|
+
} else holds.delete(input.hold);
|
|
248
|
+
const held = { ...live, holds };
|
|
249
|
+
|
|
250
|
+
if (drain !== undefined)
|
|
251
|
+
draining(change, {
|
|
252
|
+
...drain,
|
|
253
|
+
live: held,
|
|
254
|
+
_tag: drain._tag === "Draining" && holds.size === 0 ? "Releasing" : drain._tag,
|
|
255
|
+
});
|
|
256
|
+
else if (change.state._tag !== "Retired") change.state = { ...change.state, serving: held };
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const next = <A, H>(change: Change<A, H>, number: number, reopen: boolean) => {
|
|
260
|
+
const { exclusive } = change;
|
|
261
|
+
const current = servingOf(change.state);
|
|
262
|
+
|
|
263
|
+
const after = exclusive
|
|
264
|
+
? (current?.number ?? change.state.draining.keys().next().value)
|
|
265
|
+
: undefined;
|
|
266
|
+
|
|
267
|
+
if (current !== undefined && (reopen || exclusive))
|
|
268
|
+
end(change, current, reopen ? "lost" : "rotated");
|
|
269
|
+
change.state = {
|
|
270
|
+
_tag: "Opening",
|
|
271
|
+
number,
|
|
272
|
+
serving: reopen || exclusive ? undefined : current,
|
|
273
|
+
draining: change.state.draining,
|
|
274
|
+
};
|
|
275
|
+
change.events.push([number, reopen ? new Reopening() : new Opening()]);
|
|
276
|
+
change.commands.push({ _tag: "Open", number, after });
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const lost = <A, H>(
|
|
280
|
+
change: Change<A, H>,
|
|
281
|
+
input: Extract<Input<A, H>, { readonly _tag: "Lost" }>,
|
|
282
|
+
) => {
|
|
283
|
+
const { state } = change;
|
|
284
|
+
const drain = state.draining.get(input.number);
|
|
285
|
+
const current = servingOf(state);
|
|
286
|
+
|
|
287
|
+
if (drain?._tag === "Draining") {
|
|
288
|
+
change.events.push([input.number, new Lost({ cause: input.cause })]);
|
|
289
|
+
revoke(change, input.number, "lost");
|
|
290
|
+
} else if (state._tag !== "Retired" && current?.number === input.number) {
|
|
291
|
+
change.events.push([input.number, new Lost({ cause: input.cause })]);
|
|
292
|
+
if (state._tag !== "Opening")
|
|
293
|
+
next(change, (state._tag === "Open" ? current.number : state.number) + 1, true);
|
|
294
|
+
else {
|
|
295
|
+
change.state = { ...state, serving: undefined };
|
|
296
|
+
end(change, current, "lost");
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
/** Pure admission and lifecycle changes. A rejected hold preserves state identity. */
|
|
302
|
+
export const transition = <A, H = unknown>(
|
|
303
|
+
state: State<A, H>,
|
|
304
|
+
input: Input<A, H>,
|
|
305
|
+
exclusive: boolean,
|
|
306
|
+
now = 0,
|
|
307
|
+
handover = 30_000,
|
|
308
|
+
): Step<A, H> => {
|
|
309
|
+
const change: Change<A, H> = {
|
|
310
|
+
state,
|
|
311
|
+
events: [],
|
|
312
|
+
commands: [],
|
|
313
|
+
exclusive,
|
|
314
|
+
handover,
|
|
315
|
+
now,
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
switch (input._tag) {
|
|
319
|
+
case "Hold":
|
|
320
|
+
case "Let":
|
|
321
|
+
holds(change, input);
|
|
322
|
+
break;
|
|
323
|
+
case "Revoke":
|
|
324
|
+
revoke(change, input.number);
|
|
325
|
+
break;
|
|
326
|
+
case "Opened": {
|
|
327
|
+
const live = { ...input.live, holds: new Set<H>() };
|
|
328
|
+
|
|
329
|
+
if (lookup(state, live.number) !== undefined) break;
|
|
330
|
+
if (state._tag !== "Opening" || state.number !== live.number) end(change, live, "retired");
|
|
331
|
+
else {
|
|
332
|
+
change.events.push([live.number, new Open()]);
|
|
333
|
+
change.commands.push({ _tag: "Watch", live });
|
|
334
|
+
if (state.serving !== undefined) end(change, state.serving, "rotated");
|
|
335
|
+
change.state = { _tag: "Open", serving: live, draining: change.state.draining };
|
|
336
|
+
}
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
case "Failed": {
|
|
340
|
+
const { number, detail, cause } = input;
|
|
341
|
+
|
|
342
|
+
if (state._tag === "Opening" && state.number === number)
|
|
343
|
+
change.state = { ...state, _tag: "Down", detail, cause };
|
|
344
|
+
change.events.push([number, new Down({ detail, cause })]);
|
|
345
|
+
break;
|
|
346
|
+
}
|
|
347
|
+
case "Abandoned":
|
|
348
|
+
change.events.push([input.number, new Closed({})]);
|
|
349
|
+
break;
|
|
350
|
+
case "Released": {
|
|
351
|
+
const drain = state.draining.get(input.number);
|
|
352
|
+
|
|
353
|
+
if (drain === undefined || drain._tag === "Draining" || drain.live.holds.size > 0) break;
|
|
354
|
+
const draining = new Map(state.draining);
|
|
355
|
+
|
|
356
|
+
draining.delete(input.number);
|
|
357
|
+
change.state = { ...state, draining };
|
|
358
|
+
change.events.push([
|
|
359
|
+
input.number,
|
|
360
|
+
input.released === undefined ? new Kept() : new Closed({ released: input.released }),
|
|
361
|
+
]);
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
case "Lost":
|
|
365
|
+
lost(change, input);
|
|
366
|
+
break;
|
|
367
|
+
case "Rotate": {
|
|
368
|
+
if (
|
|
369
|
+
state._tag === "Retired" ||
|
|
370
|
+
(input.due !== undefined && (state._tag !== "Open" || state.serving.number !== input.due))
|
|
371
|
+
)
|
|
372
|
+
break;
|
|
373
|
+
change.reply =
|
|
374
|
+
state._tag === "Opening"
|
|
375
|
+
? state.number
|
|
376
|
+
: (state._tag === "Open" ? state.serving.number : state.number) + 1;
|
|
377
|
+
if (state._tag !== "Opening") next(change, change.reply, false);
|
|
378
|
+
break;
|
|
379
|
+
}
|
|
380
|
+
case "Keep":
|
|
381
|
+
case "Retire":
|
|
382
|
+
if (state._tag === "Retired") break;
|
|
383
|
+
for (const number of state.draining.keys()) revoke(change, number, "retired");
|
|
384
|
+
if (state.serving !== undefined) end(change, state.serving, "retired", input._tag === "Keep");
|
|
385
|
+
change.state = { _tag: "Retired", draining: change.state.draining };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return change;
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
export type Resolution<A> =
|
|
392
|
+
| { readonly _tag: "Serve"; readonly live: Live<A> }
|
|
393
|
+
| {
|
|
394
|
+
readonly _tag: "Unavailable";
|
|
395
|
+
readonly reason: "down" | "retired";
|
|
396
|
+
readonly detail: string;
|
|
397
|
+
readonly cause?: unknown;
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
/** An unheld browser resolution; use obtains its hold atomically through Hold instead. */
|
|
401
|
+
export const resolve = <A, H>(
|
|
402
|
+
state: State<A, H>,
|
|
403
|
+
target: number | undefined,
|
|
404
|
+
): Resolution<A> | undefined => {
|
|
405
|
+
if (state._tag === "Retired")
|
|
406
|
+
return { _tag: "Unavailable", reason: "retired", detail: "the supervisor was retired" };
|
|
407
|
+
if (
|
|
408
|
+
state.serving !== undefined &&
|
|
409
|
+
(target === undefined || (state._tag === "Open" && state.serving.number >= target))
|
|
410
|
+
)
|
|
411
|
+
return { _tag: "Serve", live: state.serving };
|
|
412
|
+
if (state._tag === "Down" && (target === undefined || state.number >= target))
|
|
413
|
+
return { _tag: "Unavailable", reason: "down", detail: state.detail, cause: state.cause };
|
|
414
|
+
|
|
415
|
+
return undefined;
|
|
416
|
+
};
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In the page: what changed after `since`, up to `until`, as a read returns it. A read starts the
|
|
3
|
+
* record if it has not, and keeps it going. What waited for the page to render is judged first, as
|
|
4
|
+
* it stands; then each element's history is folded over the window (`history.inpage.ts`), and only
|
|
5
|
+
* now named: its role, its name when something else names it, its tag, and its context, where it
|
|
6
|
+
* is or, for what was removed, where it was. A field's value reads `••••` unless the read unmasks
|
|
7
|
+
* it, and a secret field's always does. A change names the input it followed only where it was
|
|
8
|
+
* the input's own doing, by `Change.cause`'s rule. See `names.inpage.ts` for what a page-side part
|
|
9
|
+
* may use.
|
|
10
|
+
*/
|
|
11
|
+
import { Schema } from "effect";
|
|
12
|
+
|
|
13
|
+
import { Change } from "../../Change.ts";
|
|
14
|
+
import type { Known, ContextReader } from "../reading/context.inpage.ts";
|
|
15
|
+
import type { Names } from "../reading/names.inpage.ts";
|
|
16
|
+
import type { Folded } from "./fold.inpage.ts";
|
|
17
|
+
import type { History } from "./history.inpage.ts";
|
|
18
|
+
import type { Marks } from "./marks.inpage.ts";
|
|
19
|
+
import type { Recorder } from "./record.inpage.ts";
|
|
20
|
+
import type { Sight } from "./sight.inpage.ts";
|
|
21
|
+
|
|
22
|
+
/** A window on the page's own clock: epoch milliseconds, or the record's start and now. */
|
|
23
|
+
export interface ChangesRequest {
|
|
24
|
+
readonly since: number | null;
|
|
25
|
+
readonly until: number | null;
|
|
26
|
+
readonly unmask: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* What a read sends back: its window and the record's state, and each change as `Change` encodes
|
|
31
|
+
* it, all on the page's clock.
|
|
32
|
+
*/
|
|
33
|
+
export const ChangesResultSchema = Schema.Struct({
|
|
34
|
+
until: Schema.Finite,
|
|
35
|
+
from: Schema.Finite,
|
|
36
|
+
dropped: Schema.Int,
|
|
37
|
+
records: Schema.Array(Change),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export type ChangesResult = typeof ChangesResultSchema.Encoded;
|
|
41
|
+
|
|
42
|
+
export const changes = (
|
|
43
|
+
names: Names,
|
|
44
|
+
placing: ContextReader,
|
|
45
|
+
kept: History,
|
|
46
|
+
seeing: Sight,
|
|
47
|
+
marking: Marks,
|
|
48
|
+
recorder: Recorder,
|
|
49
|
+
) => {
|
|
50
|
+
const { clean, isInput, roleOf, nameOf } = names;
|
|
51
|
+
const groups = "tr,[role=row],form,dialog,[role=dialog],[role=alertdialog]";
|
|
52
|
+
|
|
53
|
+
/** Whether `element` lies in what an input on `target` acted on, its group, or what it controls. */
|
|
54
|
+
const local = (target: Element, element: Element): boolean => {
|
|
55
|
+
const controls = target.closest("[aria-controls]")?.getAttribute("aria-controls") ?? "";
|
|
56
|
+
const group = target.closest(groups);
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
target.contains(element) ||
|
|
60
|
+
group?.contains(element) === true ||
|
|
61
|
+
controls
|
|
62
|
+
.split(/\s+/)
|
|
63
|
+
.some((id) => id !== "" && document.getElementById(id)?.contains(element) === true)
|
|
64
|
+
);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** What an element sits in, or sat in as it went. */
|
|
68
|
+
const parentOf = (key: object): Element | null =>
|
|
69
|
+
key instanceof Element
|
|
70
|
+
? key.isConnected
|
|
71
|
+
? key.parentElement
|
|
72
|
+
: (marking.placeOf(key) ?? null)
|
|
73
|
+
: null;
|
|
74
|
+
|
|
75
|
+
// What changed in the 10 s before an input is in flux at it, and a change in the 2 s before says
|
|
76
|
+
// the page was not still.
|
|
77
|
+
const fluxMillis = 10_000;
|
|
78
|
+
const stillMillis = 2000;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Each input a change may follow, with what the page did before it: what changed in the 10 s
|
|
82
|
+
* before, the parents that gained or lost a child then, and whether nothing changed, and nothing
|
|
83
|
+
* was let go, in the 2 s before. What holds the input's target, as a menu holds its item, came
|
|
84
|
+
* with it, so it counts only as changing itself.
|
|
85
|
+
*/
|
|
86
|
+
const before = () => {
|
|
87
|
+
const inputs = recorder.inputs();
|
|
88
|
+
|
|
89
|
+
if (inputs.length === 0) return [];
|
|
90
|
+
const { tracks, losses } = kept.recent();
|
|
91
|
+
|
|
92
|
+
return inputs.map(({ at, target }) => {
|
|
93
|
+
const changed = new Set<object>();
|
|
94
|
+
const moved = new Set<Element | null>();
|
|
95
|
+
let still = losses.every((lost) => lost >= at || lost < at - stillMillis);
|
|
96
|
+
|
|
97
|
+
for (const { key, kind, initial, samples } of tracks) {
|
|
98
|
+
// Whether each change in the 10 s before came or went: it, or what it followed, showed
|
|
99
|
+
// nothing. A field's own edits are the input's, not the page's.
|
|
100
|
+
const prior = samples.flatMap(({ at: when, shown }, index) =>
|
|
101
|
+
kind === "value" || when >= at || when < at - fluxMillis
|
|
102
|
+
? []
|
|
103
|
+
: [shown === null || (index === 0 ? initial : samples[index - 1]?.shown) === null],
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
if (prior.length > 0) changed.add(key);
|
|
107
|
+
if (prior.length === 0 || (key instanceof Element && key.contains(target))) continue;
|
|
108
|
+
if (prior.includes(true)) moved.add(parentOf(key));
|
|
109
|
+
if (samples.some(({ at: when }) => when < at && when >= at - stillMillis)) still = false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return { at, target, changed, moved, still };
|
|
113
|
+
});
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/** The input a change followed, on the page's clock, where `Change.cause`'s rule names one. */
|
|
117
|
+
const causeOf = (
|
|
118
|
+
one: Folded,
|
|
119
|
+
element: Element,
|
|
120
|
+
inputs: ReturnType<typeof before>,
|
|
121
|
+
): number | undefined => {
|
|
122
|
+
const parent = parentOf(one.track.key);
|
|
123
|
+
|
|
124
|
+
for (const input of inputs.toReversed()) {
|
|
125
|
+
const after = one.startedAt - input.at;
|
|
126
|
+
|
|
127
|
+
// A change in the input's own tick followed it, as when a busy page runs a press and its
|
|
128
|
+
// release back to back.
|
|
129
|
+
if (after < 0) continue;
|
|
130
|
+
if (after > 3000) break;
|
|
131
|
+
|
|
132
|
+
// In flux: it changed, or another came into or left what it sits in, in the 10 s before.
|
|
133
|
+
const changing =
|
|
134
|
+
one.track.known >= input.at - fluxMillis ||
|
|
135
|
+
input.changed.has(one.track.key) ||
|
|
136
|
+
(parent !== null && input.moved.has(parent));
|
|
137
|
+
|
|
138
|
+
if (!changing && (local(input.target, element) || (after <= 500 && input.still)))
|
|
139
|
+
return input.at;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return undefined;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const describe = (
|
|
146
|
+
one: Folded,
|
|
147
|
+
known: Known,
|
|
148
|
+
unmask: boolean,
|
|
149
|
+
inputs: ReturnType<typeof before>,
|
|
150
|
+
): typeof Change.Encoded => {
|
|
151
|
+
const { track, kind, before, after, lowest, highest, ...times } = one;
|
|
152
|
+
const element = track.key instanceof Element ? track.key : null;
|
|
153
|
+
const where = element?.isConnected === true ? element : (marking.placeOf(track.key) ?? null);
|
|
154
|
+
const role = element === null ? null : roleOf(element);
|
|
155
|
+
const field = kind === "value";
|
|
156
|
+
|
|
157
|
+
const checkable =
|
|
158
|
+
element !== null && isInput(element) && /^(checkbox|radio)$/.test(element.type);
|
|
159
|
+
|
|
160
|
+
// A field's value is masked unless asked, empty or a box's state; a secret one never kept more.
|
|
161
|
+
const masked = (value: string | undefined) =>
|
|
162
|
+
value === undefined || !field || unmask || checkable || value === "" ? value : "••••";
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
...times,
|
|
166
|
+
kind,
|
|
167
|
+
subject: {
|
|
168
|
+
role,
|
|
169
|
+
// An element's own words are what changed, so it is named only by what else names it.
|
|
170
|
+
name:
|
|
171
|
+
element === null
|
|
172
|
+
? ""
|
|
173
|
+
: field
|
|
174
|
+
? nameOf(element, role)
|
|
175
|
+
: clean(element.getAttribute("aria-label"), 120),
|
|
176
|
+
tag: element === null ? "title" : element.tagName.toLowerCase(),
|
|
177
|
+
context: kind === "title" || where === null ? {} : placing.contextOf(where, known),
|
|
178
|
+
},
|
|
179
|
+
before: masked(before),
|
|
180
|
+
after: masked(after),
|
|
181
|
+
lowest: field ? undefined : lowest,
|
|
182
|
+
highest: field ? undefined : highest,
|
|
183
|
+
// A field's own edit needs no cause.
|
|
184
|
+
cause: field || where === null ? undefined : causeOf(one, where, inputs),
|
|
185
|
+
};
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
/** What changed after `since`, up to `until`, and no later than now. */
|
|
189
|
+
const read = (request: ChangesRequest): ChangesResult => {
|
|
190
|
+
const now = performance.timeOrigin + performance.now();
|
|
191
|
+
|
|
192
|
+
recorder.start();
|
|
193
|
+
seeing.settle();
|
|
194
|
+
const until = Math.min(request.until ?? now, now);
|
|
195
|
+
|
|
196
|
+
const { from, dropped, folded } = kept.read(
|
|
197
|
+
request.since ?? Number.NEGATIVE_INFINITY,
|
|
198
|
+
until,
|
|
199
|
+
now,
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const known = placing.known();
|
|
203
|
+
const inputs = before();
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
until,
|
|
207
|
+
from,
|
|
208
|
+
dropped,
|
|
209
|
+
records: folded.map((one) => describe(one, known, request.unmask, inputs)),
|
|
210
|
+
};
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
return { read };
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
export type Changes = ReturnType<typeof changes>;
|