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.
Files changed (204) hide show
  1. package/README.md +849 -1161
  2. package/dist/Browser-S4kvpmqa.mjs +605 -0
  3. package/dist/Browser-S4kvpmqa.mjs.map +1 -0
  4. package/dist/Browser.d.mts +127 -3
  5. package/dist/Browser.mjs +2 -22
  6. package/dist/BrowserError-Coy7EW1N.d.mts +140 -0
  7. package/dist/BrowserError.d.mts +2 -0
  8. package/dist/BrowserError.mjs +211 -0
  9. package/dist/BrowserError.mjs.map +1 -0
  10. package/dist/BrowserEvent-BRu2juEz.d.mts +279 -0
  11. package/dist/BrowserEvent.d.mts +2 -0
  12. package/dist/BrowserEvent.mjs +297 -0
  13. package/dist/BrowserEvent.mjs.map +1 -0
  14. package/dist/Cdp.d.mts +32 -0
  15. package/dist/Cdp.mjs +61 -0
  16. package/dist/Cdp.mjs.map +1 -0
  17. package/dist/Change-DNvztNLu.mjs +138 -0
  18. package/dist/Change-DNvztNLu.mjs.map +1 -0
  19. package/dist/Change-iMlcuzVD.d.mts +71 -0
  20. package/dist/Change.d.mts +2 -0
  21. package/dist/Change.mjs +2 -0
  22. package/dist/Chromium.d.mts +29 -81
  23. package/dist/Chromium.mjs +57 -392
  24. package/dist/Chromium.mjs.map +1 -1
  25. package/dist/Expect.d.mts +62 -0
  26. package/dist/Expect.mjs +114 -0
  27. package/dist/Expect.mjs.map +1 -0
  28. package/dist/Frame-BERmf_gM.d.mts +117 -0
  29. package/dist/Frame.d.mts +2 -0
  30. package/dist/Frame.mjs +123 -0
  31. package/dist/Frame.mjs.map +1 -0
  32. package/dist/Moment.d.mts +2 -0
  33. package/dist/Moment.mjs +288 -0
  34. package/dist/Moment.mjs.map +1 -0
  35. package/dist/Motion.d.mts +40 -0
  36. package/dist/Motion.mjs +122 -0
  37. package/dist/Motion.mjs.map +1 -0
  38. package/dist/Page-B5cjaUp5.mjs +618 -0
  39. package/dist/Page-B5cjaUp5.mjs.map +1 -0
  40. package/dist/Page-BlPjfibv.d.mts +595 -0
  41. package/dist/Page.d.mts +2 -0
  42. package/dist/Page.mjs +2 -0
  43. package/dist/Plan-BI8XGvDu.d.mts +209 -0
  44. package/dist/Plan-D1wrJVs8.mjs +508 -0
  45. package/dist/Plan-D1wrJVs8.mjs.map +1 -0
  46. package/dist/Plan.d.mts +2 -3
  47. package/dist/Plan.mjs +2 -2
  48. package/dist/Policy.d.mts +76 -0
  49. package/dist/Policy.mjs +237 -0
  50. package/dist/Policy.mjs.map +1 -0
  51. package/dist/Presentation.d.mts +48 -0
  52. package/dist/Presentation.mjs +117 -0
  53. package/dist/Presentation.mjs.map +1 -0
  54. package/dist/Snapshot-B34hZ4Jl.d.mts +68 -0
  55. package/dist/Snapshot.d.mts +2 -0
  56. package/dist/Snapshot.mjs +79 -0
  57. package/dist/Snapshot.mjs.map +1 -0
  58. package/dist/Stage.d.mts +64 -0
  59. package/dist/Stage.mjs +237 -0
  60. package/dist/Stage.mjs.map +1 -0
  61. package/dist/Supervisor-CYWFK3vM.d.mts +280 -0
  62. package/dist/Supervisor-TNKNOVzu.mjs +599 -0
  63. package/dist/Supervisor-TNKNOVzu.mjs.map +1 -0
  64. package/dist/Supervisor.d.mts +2 -0
  65. package/dist/Supervisor.mjs +2 -0
  66. package/dist/context-B6xr8nVi.mjs +261 -0
  67. package/dist/context-B6xr8nVi.mjs.map +1 -0
  68. package/dist/index.d.mts +17 -14
  69. package/dist/index.mjs +18 -13
  70. package/dist/match.inpage-xOe9LNO0.mjs +48 -0
  71. package/dist/match.inpage-xOe9LNO0.mjs.map +1 -0
  72. package/dist/page-B8tUQELg.mjs +5073 -0
  73. package/dist/page-B8tUQELg.mjs.map +1 -0
  74. package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
  75. package/dist/url-Cd04Okid.mjs +264 -0
  76. package/dist/url-Cd04Okid.mjs.map +1 -0
  77. package/package.json +96 -61
  78. package/src/Browser.ts +666 -0
  79. package/src/BrowserError.ts +234 -0
  80. package/src/BrowserEvent.ts +296 -0
  81. package/src/Cdp.ts +101 -0
  82. package/src/Change.ts +88 -0
  83. package/src/Chromium.ts +102 -0
  84. package/src/Expect.ts +180 -0
  85. package/src/Frame.ts +130 -0
  86. package/src/Moment.ts +479 -0
  87. package/src/Motion.ts +166 -0
  88. package/src/Page.ts +522 -0
  89. package/src/Plan.ts +663 -0
  90. package/src/Policy.ts +325 -0
  91. package/src/Presentation.ts +195 -0
  92. package/src/Snapshot.ts +89 -0
  93. package/src/Stage.ts +467 -0
  94. package/src/Supervisor.ts +599 -0
  95. package/src/index.ts +20 -0
  96. package/src/internal/input/actions.ts +610 -0
  97. package/src/internal/input/dispatch.ts +161 -0
  98. package/src/internal/input/edit.inpage.ts +162 -0
  99. package/src/internal/input/evidence.inpage.ts +164 -0
  100. package/src/internal/input/gate.ts +224 -0
  101. package/src/internal/input/guard.inpage.ts +605 -0
  102. package/src/internal/input/guard.ts +184 -0
  103. package/src/internal/input/keyboard.ts +112 -0
  104. package/src/internal/input/keys.ts +127 -0
  105. package/src/internal/input/perform.ts +376 -0
  106. package/src/internal/input/pointer.ts +153 -0
  107. package/src/internal/input/replies.ts +195 -0
  108. package/src/internal/input/settle.ts +50 -0
  109. package/src/internal/input/style.ts +186 -0
  110. package/src/internal/input/targets.inpage.ts +293 -0
  111. package/src/internal/input/targets.ts +148 -0
  112. package/src/internal/page/bridge.ts +386 -0
  113. package/src/internal/page/context.ts +312 -0
  114. package/src/internal/page/lane.ts +311 -0
  115. package/src/internal/page/navigation.ts +183 -0
  116. package/src/internal/page/openings.ts +87 -0
  117. package/src/internal/page/page.ts +165 -0
  118. package/src/internal/page/url.ts +100 -0
  119. package/src/internal/page/viewport.ts +43 -0
  120. package/src/internal/pictures/capture.ts +776 -0
  121. package/src/internal/pictures/clock.ts +141 -0
  122. package/src/internal/pictures/counts.ts +92 -0
  123. package/src/internal/pictures/pictures.ts +520 -0
  124. package/src/internal/pictures/transport.ts +242 -0
  125. package/src/internal/reading/choose.ts +100 -0
  126. package/src/internal/reading/context.inpage.ts +201 -0
  127. package/src/internal/reading/controls.inpage.ts +158 -0
  128. package/src/internal/reading/expect.inpage.ts +70 -0
  129. package/src/internal/reading/match.inpage.ts +92 -0
  130. package/src/internal/reading/names.inpage.ts +306 -0
  131. package/src/internal/reading/outline.inpage.ts +260 -0
  132. package/src/internal/reading/reading.ts +223 -0
  133. package/src/internal/reading/ready.inpage.ts +164 -0
  134. package/src/internal/reading/ready.ts +111 -0
  135. package/src/internal/reading/subjects.inpage.ts +188 -0
  136. package/src/internal/reading/text.inpage.ts +135 -0
  137. package/src/internal/reading/walk.inpage.ts +220 -0
  138. package/src/internal/supervisor/lifecycle.ts +416 -0
  139. package/src/internal/timeline/changes.inpage.ts +216 -0
  140. package/src/internal/timeline/changes.ts +100 -0
  141. package/src/internal/timeline/events.ts +94 -0
  142. package/src/internal/timeline/fold.inpage.ts +123 -0
  143. package/src/internal/timeline/history.inpage.ts +229 -0
  144. package/src/internal/timeline/marks.inpage.ts +207 -0
  145. package/src/internal/timeline/record.inpage.ts +133 -0
  146. package/src/internal/timeline/sight.inpage.ts +122 -0
  147. package/src/internal/timeline/window.ts +96 -0
  148. package/src/internal/timeline/words.ts +65 -0
  149. package/dist/Bootstrap-B9UUXsvF.d.mts +0 -183
  150. package/dist/Bootstrap-CeQOWBy6.mjs +0 -189
  151. package/dist/Bootstrap-CeQOWBy6.mjs.map +0 -1
  152. package/dist/Bootstrap.d.mts +0 -3
  153. package/dist/Bootstrap.mjs +0 -2
  154. package/dist/Browser-BF9_sORP.d.mts +0 -388
  155. package/dist/Browser.mjs.map +0 -1
  156. package/dist/BrowserData-CPIIIHol.d.mts +0 -585
  157. package/dist/BrowserData.d.mts +0 -3
  158. package/dist/BrowserData.mjs +0 -695
  159. package/dist/BrowserData.mjs.map +0 -1
  160. package/dist/BrowserRuntime-B6nT1G4X.d.mts +0 -148
  161. package/dist/BrowserRuntime-C1r2aSRK.mjs +0 -6820
  162. package/dist/BrowserRuntime-C1r2aSRK.mjs.map +0 -1
  163. package/dist/BrowserRuntime.d.mts +0 -3
  164. package/dist/BrowserRuntime.mjs +0 -3
  165. package/dist/Capture.d.mts +0 -56
  166. package/dist/Capture.mjs +0 -678
  167. package/dist/Capture.mjs.map +0 -1
  168. package/dist/CaptureData-CYriZHX1.mjs +0 -201
  169. package/dist/CaptureData-CYriZHX1.mjs.map +0 -1
  170. package/dist/CaptureData.d.mts +0 -219
  171. package/dist/CaptureData.mjs +0 -2
  172. package/dist/Errors.d.mts +0 -158
  173. package/dist/Errors.mjs +0 -207
  174. package/dist/Errors.mjs.map +0 -1
  175. package/dist/Images-yCG9BmRU.mjs +0 -76
  176. package/dist/Images-yCG9BmRU.mjs.map +0 -1
  177. package/dist/OperationOptions-DAeRS6O9.mjs +0 -55
  178. package/dist/OperationOptions-DAeRS6O9.mjs.map +0 -1
  179. package/dist/PageControl.d.mts +0 -15
  180. package/dist/PageControl.mjs +0 -34
  181. package/dist/PageControl.mjs.map +0 -1
  182. package/dist/PageRegistry-B3q9DS3g.mjs +0 -164
  183. package/dist/PageRegistry-B3q9DS3g.mjs.map +0 -1
  184. package/dist/Plan-Dyxu2q8C.mjs +0 -543
  185. package/dist/Plan-Dyxu2q8C.mjs.map +0 -1
  186. package/dist/PlanData-B668RxbQ.d.mts +0 -4831
  187. package/dist/PlanData.d.mts +0 -3
  188. package/dist/PlanData.mjs +0 -268
  189. package/dist/PlanData.mjs.map +0 -1
  190. package/dist/PlanInput-BJcPPncs.mjs +0 -75
  191. package/dist/PlanInput-BJcPPncs.mjs.map +0 -1
  192. package/dist/PublicSession-DCSMMqOj.mjs +0 -5602
  193. package/dist/PublicSession-DCSMMqOj.mjs.map +0 -1
  194. package/dist/Testing.d.mts +0 -479
  195. package/dist/Testing.mjs +0 -2147
  196. package/dist/Testing.mjs.map +0 -1
  197. package/dist/Timeline-fV22Xk-S.d.mts +0 -29
  198. package/dist/Timeline.d.mts +0 -4
  199. package/dist/Timeline.mjs +0 -20
  200. package/dist/Timeline.mjs.map +0 -1
  201. package/dist/TimelineData-BsOn-YGZ.d.mts +0 -3818
  202. package/dist/TimelineData.d.mts +0 -3
  203. package/dist/TimelineData.mjs +0 -482
  204. 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>;