effect-browser 0.2.0-beta.9 → 0.3.0-beta.5
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 +705 -1184
- package/dist/Browser-CM65Uo7f.mjs +437 -0
- package/dist/Browser-CM65Uo7f.mjs.map +1 -0
- package/dist/Browser.d.mts +115 -3
- package/dist/Browser.mjs +2 -22
- package/dist/BrowserError-DXXIA6Rk.d.mts +135 -0
- package/dist/BrowserError.d.mts +2 -0
- package/dist/BrowserError.mjs +202 -0
- package/dist/BrowserError.mjs.map +1 -0
- package/dist/BrowserEvent-DzdNdvoh.d.mts +277 -0
- package/dist/BrowserEvent.d.mts +2 -0
- package/dist/BrowserEvent.mjs +295 -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-DVP2IJWm.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-D4281dIi.mjs +618 -0
- package/dist/Page-D4281dIi.mjs.map +1 -0
- package/dist/Page-DYTdVhYa.d.mts +593 -0
- package/dist/Page.d.mts +2 -0
- package/dist/Page.mjs +2 -0
- package/dist/Plan-CNNOU1Tx.d.mts +112 -0
- package/dist/Plan-Dtrb-8im.mjs +311 -0
- package/dist/Plan-Dtrb-8im.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 +54 -0
- package/dist/Stage.mjs +133 -0
- package/dist/Stage.mjs.map +1 -0
- package/dist/Supervisor-BtEG93gR.mjs +420 -0
- package/dist/Supervisor-BtEG93gR.mjs.map +1 -0
- package/dist/Supervisor-Im6-wbg5.d.mts +230 -0
- package/dist/Supervisor.d.mts +2 -0
- package/dist/Supervisor.mjs +2 -0
- package/dist/context-DAYjATgC.mjs +137 -0
- package/dist/context-DAYjATgC.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-Banfm_zc.mjs +5097 -0
- package/dist/page-Banfm_zc.mjs.map +1 -0
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/dist/url-CcL0K7S0.mjs +262 -0
- package/dist/url-CcL0K7S0.mjs.map +1 -0
- package/package.json +96 -61
- package/src/Browser.ts +625 -0
- package/src/BrowserError.ts +225 -0
- package/src/BrowserEvent.ts +294 -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 +520 -0
- package/src/Plan.ts +350 -0
- package/src/Policy.ts +325 -0
- package/src/Presentation.ts +195 -0
- package/src/Snapshot.ts +89 -0
- package/src/Stage.ts +223 -0
- package/src/Supervisor.ts +353 -0
- package/src/index.ts +20 -0
- package/src/internal/input/actions.ts +607 -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/guard.inpage.ts +605 -0
- package/src/internal/input/guard.ts +183 -0
- package/src/internal/input/keyboard.ts +112 -0
- package/src/internal/input/keys.ts +127 -0
- package/src/internal/input/perform.ts +322 -0
- package/src/internal/input/pointer.ts +153 -0
- package/src/internal/input/replies.ts +159 -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 +302 -0
- package/src/internal/page/lane.ts +301 -0
- package/src/internal/page/navigation.ts +183 -0
- package/src/internal/page/openings.ts +87 -0
- package/src/internal/page/page.ts +137 -0
- package/src/internal/page/url.ts +100 -0
- package/src/internal/page/viewport.ts +43 -0
- package/src/internal/pictures/capture.ts +778 -0
- package/src/internal/pictures/clock.ts +141 -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 +154 -0
- package/src/internal/reading/ready.ts +105 -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 +265 -0
- package/src/internal/timeline/changes.inpage.ts +216 -0
- package/src/internal/timeline/changes.ts +100 -0
- package/src/internal/timeline/events.ts +93 -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,265 @@
|
|
|
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
|
+
/** Lost: its connection dropped, or its provider ended its session, as `cause` says. */
|
|
32
|
+
export class Lost extends Schema.TaggedClass<Lost>()("Lost", {
|
|
33
|
+
cause: DisconnectCause,
|
|
34
|
+
}) {}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* It never opened: its last try failed with `cause`, which the provider deems definite, or after
|
|
38
|
+
* which the reopen schedule gave up.
|
|
39
|
+
*/
|
|
40
|
+
export class Down extends Schema.TaggedClass<Down>()("Down", {
|
|
41
|
+
detail: Schema.String,
|
|
42
|
+
cause: Schema.Defect(),
|
|
43
|
+
}) {}
|
|
44
|
+
|
|
45
|
+
/** It ended. `released` says how its release went; one retired while opening has none. */
|
|
46
|
+
export class Closed extends Schema.TaggedClass<Closed>()("Closed", {
|
|
47
|
+
released: Schema.optional(Released),
|
|
48
|
+
}) {}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* It was serving as the supervisor's scope closed, and was left running, not released: its scope
|
|
52
|
+
* closed, and its provider runs it on, and bills, until something adopts and releases it.
|
|
53
|
+
*/
|
|
54
|
+
export class Kept extends Schema.TaggedClass<Kept>()("Kept", {}) {}
|
|
55
|
+
|
|
56
|
+
export const GenerationState = Schema.Union([Opening, Reopening, Open, Lost, Down, Closed, Kept]);
|
|
57
|
+
|
|
58
|
+
export type GenerationState = typeof GenerationState.Type;
|
|
59
|
+
|
|
60
|
+
/** A generation that opened: its number, and what the runtime keeps to serve and end it. */
|
|
61
|
+
export interface Live<A> {
|
|
62
|
+
readonly number: number;
|
|
63
|
+
readonly value: A;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* At most one generation opens and at most one serves. Both do while a rotation makes the next
|
|
68
|
+
* before it breaks the current, and `Down` keeps serving one that a rotation failed to replace.
|
|
69
|
+
*/
|
|
70
|
+
export type State<A> =
|
|
71
|
+
| { readonly _tag: "Opening"; readonly number: number; readonly serving: Live<A> | undefined }
|
|
72
|
+
| { readonly _tag: "Open"; readonly serving: Live<A> }
|
|
73
|
+
| {
|
|
74
|
+
readonly _tag: "Down";
|
|
75
|
+
readonly number: number;
|
|
76
|
+
readonly detail: string;
|
|
77
|
+
readonly cause: unknown;
|
|
78
|
+
readonly serving: Live<A> | undefined;
|
|
79
|
+
}
|
|
80
|
+
| { readonly _tag: "Retired" };
|
|
81
|
+
|
|
82
|
+
export type Input<A> =
|
|
83
|
+
| { readonly _tag: "Opened"; readonly live: Live<A> }
|
|
84
|
+
/** The open of `number` failed for good. */
|
|
85
|
+
| {
|
|
86
|
+
readonly _tag: "Failed";
|
|
87
|
+
readonly number: number;
|
|
88
|
+
readonly detail: string;
|
|
89
|
+
readonly cause: unknown;
|
|
90
|
+
}
|
|
91
|
+
/** The open of `number` was stopped, as on retiring, before it opened. */
|
|
92
|
+
| { readonly _tag: "Abandoned"; readonly number: number }
|
|
93
|
+
| { readonly _tag: "Lost"; readonly number: number; readonly cause: DisconnectCause }
|
|
94
|
+
/** A caller asks for the next generation, or the rotation time of `due` came. */
|
|
95
|
+
| { readonly _tag: "Rotate"; readonly due?: number | undefined }
|
|
96
|
+
| { readonly _tag: "Retire" }
|
|
97
|
+
/** Stop as `Retire` does, but leave the serving generation running. */
|
|
98
|
+
| { readonly _tag: "Keep" }
|
|
99
|
+
| { readonly _tag: "Released"; readonly number: number; readonly released: Released };
|
|
100
|
+
|
|
101
|
+
export type Command<A> =
|
|
102
|
+
/** Open `number`, after the release of `after` has finished. */
|
|
103
|
+
| { readonly _tag: "Open"; readonly number: number; readonly after: number | undefined }
|
|
104
|
+
/** Watch a generation that opened for its loss and its rotation time. */
|
|
105
|
+
| { readonly _tag: "Watch"; readonly live: Live<A> }
|
|
106
|
+
| { readonly _tag: "Release"; readonly live: Live<A> }
|
|
107
|
+
/** Close its scope without asking its provider to release it. */
|
|
108
|
+
| { readonly _tag: "Leave"; readonly live: Live<A> };
|
|
109
|
+
|
|
110
|
+
export interface Step<A> {
|
|
111
|
+
readonly state: State<A>;
|
|
112
|
+
/** Generation numbers and their new states, to publish in this order. */
|
|
113
|
+
readonly events: ReadonlyArray<readonly [number, GenerationState]>;
|
|
114
|
+
readonly commands: ReadonlyArray<Command<A>>;
|
|
115
|
+
/** For `Rotate`, the generation to wait for, unless retired. */
|
|
116
|
+
readonly reply?: number | undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export const start = <A>(): Step<A> => ({
|
|
120
|
+
state: { _tag: "Opening", number: 1, serving: undefined },
|
|
121
|
+
events: [[1, new Opening()]],
|
|
122
|
+
commands: [{ _tag: "Open", number: 1, after: undefined }],
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const servingOf = <A>(state: State<A>) => (state._tag === "Retired" ? undefined : state.serving);
|
|
126
|
+
|
|
127
|
+
/** The state after `input`, with the events to publish and the commands to run. */
|
|
128
|
+
export const transition = <A>(state: State<A>, input: Input<A>, exclusive: boolean): Step<A> => {
|
|
129
|
+
const release = (live: Live<A>): Command<A> => ({ _tag: "Release", live });
|
|
130
|
+
const stay: Step<A> = { state, events: [], commands: [] };
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Open `number`, ending `current` now if it `ends`; exclusive generations open only once its
|
|
134
|
+
* release has finished. Otherwise `current` serves until the next one opens.
|
|
135
|
+
*/
|
|
136
|
+
const next = (
|
|
137
|
+
number: number,
|
|
138
|
+
reopen: boolean,
|
|
139
|
+
current: Live<A> | undefined,
|
|
140
|
+
ends: boolean,
|
|
141
|
+
): Step<A> => ({
|
|
142
|
+
state: { _tag: "Opening", number, serving: ends ? undefined : current },
|
|
143
|
+
events: [[number, reopen ? new Reopening() : new Opening()]],
|
|
144
|
+
commands: [
|
|
145
|
+
...(ends && current !== undefined ? [release(current)] : []),
|
|
146
|
+
{ _tag: "Open", number, after: ends && exclusive ? current?.number : undefined },
|
|
147
|
+
],
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
switch (input._tag) {
|
|
151
|
+
case "Opened": {
|
|
152
|
+
const { live } = input;
|
|
153
|
+
|
|
154
|
+
// A generation nobody waits for any more, as after `Retire`, is released at once.
|
|
155
|
+
if (state._tag !== "Opening" || state.number !== live.number)
|
|
156
|
+
return { ...stay, commands: [release(live)] };
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
state: { _tag: "Open", serving: live },
|
|
160
|
+
events: [[live.number, new Open()]],
|
|
161
|
+
commands: [
|
|
162
|
+
{ _tag: "Watch", live },
|
|
163
|
+
...(state.serving === undefined ? [] : [release(state.serving)]),
|
|
164
|
+
],
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
case "Failed": {
|
|
168
|
+
const { number, detail, cause } = input;
|
|
169
|
+
const events = [[number, new Down({ detail, cause })] as const];
|
|
170
|
+
|
|
171
|
+
if (state._tag !== "Opening" || state.number !== input.number) return { ...stay, events };
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
state: { _tag: "Down", number, detail, cause, serving: state.serving },
|
|
175
|
+
events,
|
|
176
|
+
commands: [],
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
case "Abandoned":
|
|
180
|
+
return { ...stay, events: [[input.number, new Closed({})]] };
|
|
181
|
+
case "Released":
|
|
182
|
+
return { ...stay, events: [[input.number, new Closed({ released: input.released })]] };
|
|
183
|
+
case "Lost": {
|
|
184
|
+
const lost = servingOf(state);
|
|
185
|
+
|
|
186
|
+
if (state._tag === "Retired" || lost?.number !== input.number) return stay;
|
|
187
|
+
const events = [[lost.number, new Lost({ cause: input.cause })] as const];
|
|
188
|
+
|
|
189
|
+
// A rotation already opening the next generation carries on without the lost one.
|
|
190
|
+
if (state._tag === "Opening")
|
|
191
|
+
return { state: { ...state, serving: undefined }, events, commands: [release(lost)] };
|
|
192
|
+
|
|
193
|
+
const reopened = next(
|
|
194
|
+
(state._tag === "Open" ? lost.number : state.number) + 1,
|
|
195
|
+
true,
|
|
196
|
+
lost,
|
|
197
|
+
true,
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
return { ...reopened, events: [...events, ...reopened.events] };
|
|
201
|
+
}
|
|
202
|
+
case "Rotate": {
|
|
203
|
+
if (state._tag === "Retired") return stay;
|
|
204
|
+
// A rotation time comes only for the generation still serving with nothing opening.
|
|
205
|
+
if (input.due !== undefined && (state._tag !== "Open" || state.serving.number !== input.due))
|
|
206
|
+
return stay;
|
|
207
|
+
if (state._tag === "Opening") return { ...stay, reply: state.number };
|
|
208
|
+
const number = (state._tag === "Open" ? state.serving.number : state.number) + 1;
|
|
209
|
+
|
|
210
|
+
return { ...next(number, false, state.serving, exclusive), reply: number };
|
|
211
|
+
}
|
|
212
|
+
case "Retire":
|
|
213
|
+
case "Keep": {
|
|
214
|
+
const current = servingOf(state);
|
|
215
|
+
|
|
216
|
+
if (state._tag === "Retired") return stay;
|
|
217
|
+
if (current === undefined) return { state: { _tag: "Retired" }, events: [], commands: [] };
|
|
218
|
+
|
|
219
|
+
return input._tag === "Retire"
|
|
220
|
+
? { state: { _tag: "Retired" }, events: [], commands: [release(current)] }
|
|
221
|
+
: {
|
|
222
|
+
state: { _tag: "Retired" },
|
|
223
|
+
events: [[current.number, new Kept()]],
|
|
224
|
+
commands: [{ _tag: "Leave", live: current }],
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
export type Resolution<A> =
|
|
231
|
+
| { readonly _tag: "Serve"; readonly live: Live<A> }
|
|
232
|
+
| {
|
|
233
|
+
readonly _tag: "Unavailable";
|
|
234
|
+
readonly reason: "down" | "retired";
|
|
235
|
+
readonly detail: string;
|
|
236
|
+
readonly cause?: unknown;
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* What a caller waiting for generation `target`, or for any when it is undefined, gets in
|
|
241
|
+
* `state`; undefined while it should go on waiting.
|
|
242
|
+
*/
|
|
243
|
+
export const resolve = <A>(
|
|
244
|
+
state: State<A>,
|
|
245
|
+
target: number | undefined,
|
|
246
|
+
): Resolution<A> | undefined => {
|
|
247
|
+
const serve = (live: Live<A>) => ({ _tag: "Serve" as const, live });
|
|
248
|
+
|
|
249
|
+
switch (state._tag) {
|
|
250
|
+
case "Retired":
|
|
251
|
+
return { _tag: "Unavailable", reason: "retired", detail: "the supervisor was retired" };
|
|
252
|
+
case "Open":
|
|
253
|
+
return target === undefined || state.serving.number >= target
|
|
254
|
+
? serve(state.serving)
|
|
255
|
+
: undefined;
|
|
256
|
+
case "Opening":
|
|
257
|
+
return target === undefined && state.serving !== undefined ? serve(state.serving) : undefined;
|
|
258
|
+
case "Down":
|
|
259
|
+
if (target === undefined && state.serving !== undefined) return serve(state.serving);
|
|
260
|
+
|
|
261
|
+
return target === undefined || state.number >= target
|
|
262
|
+
? { _tag: "Unavailable", reason: "down", detail: state.detail, cause: state.cause }
|
|
263
|
+
: undefined;
|
|
264
|
+
}
|
|
265
|
+
};
|
|
@@ -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>;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading a page's record of what changed, in one call to the page. The first read also registers
|
|
3
|
+
* the recorder with the page's own session, in the same round trip, so that every later document
|
|
4
|
+
* records from its start. The page's times map to the owner's clock through the browser's one
|
|
5
|
+
* clock mapping, as frames' do, which a browser's first read measures, once, where no capture has;
|
|
6
|
+
* and a window can begin on the page's own clock where the previous one ended, so consecutive
|
|
7
|
+
* reads neither miss nor repeat a change. A read takes its turn on the page as other reads do, and
|
|
8
|
+
* identical ones in flight share it; none is kept for a later caller, since its window ends when
|
|
9
|
+
* it was read.
|
|
10
|
+
*/
|
|
11
|
+
import { Effect, Schema } from "effect";
|
|
12
|
+
|
|
13
|
+
import type { BrowserError } from "../../BrowserError.ts";
|
|
14
|
+
import { Change, Changes } from "../../Change.ts";
|
|
15
|
+
import type { Frame } from "../../Frame.ts";
|
|
16
|
+
import type { ChangesOptions } from "../../Page.ts";
|
|
17
|
+
import { type Bridge, scriptCall } from "../page/bridge.ts";
|
|
18
|
+
import { decodeWith, type PageContext } from "../page/context.ts";
|
|
19
|
+
import * as BrowserClock from "../pictures/clock.ts";
|
|
20
|
+
import { type ChangesRequest, ChangesResultSchema } from "./changes.inpage.ts";
|
|
21
|
+
|
|
22
|
+
const isChanges = Schema.is(Changes);
|
|
23
|
+
|
|
24
|
+
// Where a window starts or ends as its caller gave it, which tells identical reads apart.
|
|
25
|
+
const markOf = (time: Changes | Frame | number | undefined) =>
|
|
26
|
+
time === undefined || typeof time === "number"
|
|
27
|
+
? time
|
|
28
|
+
: isChanges(time)
|
|
29
|
+
? `cursor ${time.cursor}`
|
|
30
|
+
: `frame ${time.timestamp ?? time.hostTime}`;
|
|
31
|
+
|
|
32
|
+
export const make = (
|
|
33
|
+
page: PageContext,
|
|
34
|
+
bridge: Bridge,
|
|
35
|
+
estimate: Effect.Effect<BrowserClock.Estimate, BrowserError>,
|
|
36
|
+
) => {
|
|
37
|
+
const reads = page.lane.shared<Changes>("changes", false);
|
|
38
|
+
|
|
39
|
+
return (options: ChangesOptions = {}) =>
|
|
40
|
+
reads(
|
|
41
|
+
JSON.stringify([markOf(options.since), markOf(options.until), options.unmask ?? false]),
|
|
42
|
+
Effect.gen(function* () {
|
|
43
|
+
const mapping = yield* estimate;
|
|
44
|
+
const host = (epoch: number) => BrowserClock.toHostTime(mapping, epoch);
|
|
45
|
+
|
|
46
|
+
// A frame's paint is on the page's clock already; a screenshot has only its host time.
|
|
47
|
+
const onPage = (time: Frame | number) =>
|
|
48
|
+
typeof time === "number"
|
|
49
|
+
? time + mapping.offsetMillis
|
|
50
|
+
: (time.timestamp ?? time.hostTime + mapping.offsetMillis);
|
|
51
|
+
|
|
52
|
+
const request: ChangesRequest = {
|
|
53
|
+
since:
|
|
54
|
+
options.since === undefined
|
|
55
|
+
? null
|
|
56
|
+
: isChanges(options.since)
|
|
57
|
+
? options.since.cursor
|
|
58
|
+
: onPage(options.since),
|
|
59
|
+
until: options.until === undefined ? null : onPage(options.until),
|
|
60
|
+
unmask: options.unmask ?? false,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const [, result] = yield* Effect.all(
|
|
64
|
+
[
|
|
65
|
+
bridge.registerRecorder("changes"),
|
|
66
|
+
bridge
|
|
67
|
+
.evaluate("changes", scriptCall("changes", request))
|
|
68
|
+
.pipe(Effect.flatMap(decodeWith("changes", ChangesResultSchema))),
|
|
69
|
+
],
|
|
70
|
+
{ concurrency: "unbounded" },
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
yield* Effect.annotateCurrentSpan({
|
|
74
|
+
changes: result.records.length,
|
|
75
|
+
dropped: result.dropped,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const later = (epoch: number | undefined) =>
|
|
79
|
+
epoch === undefined ? undefined : host(epoch);
|
|
80
|
+
|
|
81
|
+
return new Changes({
|
|
82
|
+
document: bridge.frameTag().document,
|
|
83
|
+
from: host(result.from),
|
|
84
|
+
until: host(result.until),
|
|
85
|
+
cursor: result.until,
|
|
86
|
+
dropped: result.dropped,
|
|
87
|
+
changes: result.records.map(
|
|
88
|
+
(one) =>
|
|
89
|
+
new Change({
|
|
90
|
+
...one,
|
|
91
|
+
startedAt: host(one.startedAt),
|
|
92
|
+
at: host(one.at),
|
|
93
|
+
earlier: later(one.earlier),
|
|
94
|
+
cause: later(one.cause),
|
|
95
|
+
}),
|
|
96
|
+
),
|
|
97
|
+
});
|
|
98
|
+
}).pipe(page.within("changes")),
|
|
99
|
+
).pipe(page.span("Page.changes"), page.owned);
|
|
100
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The browser's bounded event history and replay cursors. Readers share the same records and
|
|
3
|
+
* wakeup generation, so a delayed reader cannot block input or retain an unbounded private queue.
|
|
4
|
+
*/
|
|
5
|
+
import { Cause, Deferred, Effect, Stream } from "effect";
|
|
6
|
+
|
|
7
|
+
import { type BrowserError, EventHistoryExpired, InvalidRequest } from "../../BrowserError.ts";
|
|
8
|
+
import { type BrowserEvent, RecordedEvent } from "../../BrowserEvent.ts";
|
|
9
|
+
import { undispatched } from "../page/context.ts";
|
|
10
|
+
|
|
11
|
+
/** The owning Browser validates capacity and closes this timeline with its scope. */
|
|
12
|
+
export const make = (capacity: number) => {
|
|
13
|
+
let records: ReadonlyArray<RecordedEvent> = [];
|
|
14
|
+
let sequence = 0;
|
|
15
|
+
let closed = false;
|
|
16
|
+
let changed = Deferred.makeUnsafe<void>();
|
|
17
|
+
|
|
18
|
+
const wake = () => {
|
|
19
|
+
const previous = changed;
|
|
20
|
+
|
|
21
|
+
changed = Deferred.makeUnsafe<void>();
|
|
22
|
+
Deferred.doneUnsafe(previous, Effect.void);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const publish = (event: BrowserEvent): number => {
|
|
26
|
+
// Native callbacks may finish after their owning scope. They must not revive closed readers.
|
|
27
|
+
if (closed) return sequence;
|
|
28
|
+
sequence += 1;
|
|
29
|
+
const recorded = new RecordedEvent({ sequence, event });
|
|
30
|
+
|
|
31
|
+
records = records.length < capacity ? [...records, recorded] : [...records.slice(1), recorded];
|
|
32
|
+
wake();
|
|
33
|
+
|
|
34
|
+
return sequence;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const stream = (after?: number): Stream.Stream<RecordedEvent, BrowserError> =>
|
|
38
|
+
Stream.unwrap(
|
|
39
|
+
Effect.sync(() => {
|
|
40
|
+
// This runs for each subscription, rather than when the reusable stream is constructed.
|
|
41
|
+
let cursor = after ?? sequence;
|
|
42
|
+
|
|
43
|
+
if (!Number.isSafeInteger(cursor) || cursor < 0 || cursor > sequence)
|
|
44
|
+
return Stream.fail(
|
|
45
|
+
undispatched(
|
|
46
|
+
"events",
|
|
47
|
+
new InvalidRequest({
|
|
48
|
+
detail:
|
|
49
|
+
"the event cursor must be a nonnegative integer no greater than the latest sequence",
|
|
50
|
+
}),
|
|
51
|
+
),
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
return Stream.fromEffectRepeat(
|
|
55
|
+
Effect.gen(function* () {
|
|
56
|
+
while (true) {
|
|
57
|
+
const oldest = records[0]?.sequence ?? sequence + 1;
|
|
58
|
+
|
|
59
|
+
if (cursor < oldest - 1)
|
|
60
|
+
return yield* undispatched(
|
|
61
|
+
"events",
|
|
62
|
+
new EventHistoryExpired({ after: cursor, oldest, latest: sequence }),
|
|
63
|
+
);
|
|
64
|
+
const next = records[cursor - oldest + 1];
|
|
65
|
+
|
|
66
|
+
if (next !== undefined) {
|
|
67
|
+
cursor = next.sequence;
|
|
68
|
+
|
|
69
|
+
return next;
|
|
70
|
+
}
|
|
71
|
+
if (closed) return yield* Cause.done();
|
|
72
|
+
// Capture the generation in the same synchronous read as the empty history check.
|
|
73
|
+
// A publish before await completes this exact Deferred, so no wakeup can be missed.
|
|
74
|
+
const waiting = changed;
|
|
75
|
+
|
|
76
|
+
yield* Deferred.await(waiting);
|
|
77
|
+
}
|
|
78
|
+
}),
|
|
79
|
+
);
|
|
80
|
+
}),
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
publish,
|
|
85
|
+
recent: Effect.sync(() => records),
|
|
86
|
+
stream,
|
|
87
|
+
close: Effect.sync(() => {
|
|
88
|
+
if (closed) return;
|
|
89
|
+
closed = true;
|
|
90
|
+
wake();
|
|
91
|
+
}),
|
|
92
|
+
};
|
|
93
|
+
};
|