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.
Files changed (205) hide show
  1. package/README.md +861 -1161
  2. package/dist/Browser-B4qncxe7.mjs +628 -0
  3. package/dist/Browser-B4qncxe7.mjs.map +1 -0
  4. package/dist/Browser.d.mts +129 -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-Di2PxyF7.d.mts +119 -0
  29. package/dist/Frame.d.mts +2 -0
  30. package/dist/Frame.mjs +125 -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-B7X6DQUn.d.mts +595 -0
  41. package/dist/Page.d.mts +2 -0
  42. package/dist/Page.mjs +2 -0
  43. package/dist/Plan-Akcac63g.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-BAf__WKX.mjs +5075 -0
  73. package/dist/page-BAf__WKX.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 +672 -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 +132 -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 +391 -0
  113. package/src/internal/page/context.ts +314 -0
  114. package/src/internal/page/identity.ts +35 -0
  115. package/src/internal/page/lane.ts +311 -0
  116. package/src/internal/page/navigation.ts +183 -0
  117. package/src/internal/page/openings.ts +87 -0
  118. package/src/internal/page/page.ts +165 -0
  119. package/src/internal/page/url.ts +100 -0
  120. package/src/internal/page/viewport.ts +43 -0
  121. package/src/internal/pictures/capture.ts +776 -0
  122. package/src/internal/pictures/clock.ts +141 -0
  123. package/src/internal/pictures/counts.ts +92 -0
  124. package/src/internal/pictures/pictures.ts +521 -0
  125. package/src/internal/pictures/transport.ts +243 -0
  126. package/src/internal/reading/choose.ts +100 -0
  127. package/src/internal/reading/context.inpage.ts +201 -0
  128. package/src/internal/reading/controls.inpage.ts +158 -0
  129. package/src/internal/reading/expect.inpage.ts +70 -0
  130. package/src/internal/reading/match.inpage.ts +92 -0
  131. package/src/internal/reading/names.inpage.ts +306 -0
  132. package/src/internal/reading/outline.inpage.ts +260 -0
  133. package/src/internal/reading/reading.ts +223 -0
  134. package/src/internal/reading/ready.inpage.ts +164 -0
  135. package/src/internal/reading/ready.ts +111 -0
  136. package/src/internal/reading/subjects.inpage.ts +188 -0
  137. package/src/internal/reading/text.inpage.ts +135 -0
  138. package/src/internal/reading/walk.inpage.ts +220 -0
  139. package/src/internal/supervisor/lifecycle.ts +416 -0
  140. package/src/internal/timeline/changes.inpage.ts +216 -0
  141. package/src/internal/timeline/changes.ts +100 -0
  142. package/src/internal/timeline/events.ts +94 -0
  143. package/src/internal/timeline/fold.inpage.ts +123 -0
  144. package/src/internal/timeline/history.inpage.ts +229 -0
  145. package/src/internal/timeline/marks.inpage.ts +207 -0
  146. package/src/internal/timeline/record.inpage.ts +133 -0
  147. package/src/internal/timeline/sight.inpage.ts +122 -0
  148. package/src/internal/timeline/window.ts +96 -0
  149. package/src/internal/timeline/words.ts +65 -0
  150. package/dist/Bootstrap-B9UUXsvF.d.mts +0 -183
  151. package/dist/Bootstrap-CeQOWBy6.mjs +0 -189
  152. package/dist/Bootstrap-CeQOWBy6.mjs.map +0 -1
  153. package/dist/Bootstrap.d.mts +0 -3
  154. package/dist/Bootstrap.mjs +0 -2
  155. package/dist/Browser-BF9_sORP.d.mts +0 -388
  156. package/dist/Browser.mjs.map +0 -1
  157. package/dist/BrowserData-CPIIIHol.d.mts +0 -585
  158. package/dist/BrowserData.d.mts +0 -3
  159. package/dist/BrowserData.mjs +0 -695
  160. package/dist/BrowserData.mjs.map +0 -1
  161. package/dist/BrowserRuntime-B6nT1G4X.d.mts +0 -148
  162. package/dist/BrowserRuntime-C1r2aSRK.mjs +0 -6820
  163. package/dist/BrowserRuntime-C1r2aSRK.mjs.map +0 -1
  164. package/dist/BrowserRuntime.d.mts +0 -3
  165. package/dist/BrowserRuntime.mjs +0 -3
  166. package/dist/Capture.d.mts +0 -56
  167. package/dist/Capture.mjs +0 -678
  168. package/dist/Capture.mjs.map +0 -1
  169. package/dist/CaptureData-CYriZHX1.mjs +0 -201
  170. package/dist/CaptureData-CYriZHX1.mjs.map +0 -1
  171. package/dist/CaptureData.d.mts +0 -219
  172. package/dist/CaptureData.mjs +0 -2
  173. package/dist/Errors.d.mts +0 -158
  174. package/dist/Errors.mjs +0 -207
  175. package/dist/Errors.mjs.map +0 -1
  176. package/dist/Images-yCG9BmRU.mjs +0 -76
  177. package/dist/Images-yCG9BmRU.mjs.map +0 -1
  178. package/dist/OperationOptions-DAeRS6O9.mjs +0 -55
  179. package/dist/OperationOptions-DAeRS6O9.mjs.map +0 -1
  180. package/dist/PageControl.d.mts +0 -15
  181. package/dist/PageControl.mjs +0 -34
  182. package/dist/PageControl.mjs.map +0 -1
  183. package/dist/PageRegistry-B3q9DS3g.mjs +0 -164
  184. package/dist/PageRegistry-B3q9DS3g.mjs.map +0 -1
  185. package/dist/Plan-Dyxu2q8C.mjs +0 -543
  186. package/dist/Plan-Dyxu2q8C.mjs.map +0 -1
  187. package/dist/PlanData-B668RxbQ.d.mts +0 -4831
  188. package/dist/PlanData.d.mts +0 -3
  189. package/dist/PlanData.mjs +0 -268
  190. package/dist/PlanData.mjs.map +0 -1
  191. package/dist/PlanInput-BJcPPncs.mjs +0 -75
  192. package/dist/PlanInput-BJcPPncs.mjs.map +0 -1
  193. package/dist/PublicSession-DCSMMqOj.mjs +0 -5602
  194. package/dist/PublicSession-DCSMMqOj.mjs.map +0 -1
  195. package/dist/Testing.d.mts +0 -479
  196. package/dist/Testing.mjs +0 -2147
  197. package/dist/Testing.mjs.map +0 -1
  198. package/dist/Timeline-fV22Xk-S.d.mts +0 -29
  199. package/dist/Timeline.d.mts +0 -4
  200. package/dist/Timeline.mjs +0 -20
  201. package/dist/Timeline.mjs.map +0 -1
  202. package/dist/TimelineData-BsOn-YGZ.d.mts +0 -3818
  203. package/dist/TimelineData.d.mts +0 -3
  204. package/dist/TimelineData.mjs +0 -482
  205. package/dist/TimelineData.mjs.map +0 -1
@@ -0,0 +1,391 @@
1
+ /**
2
+ * The bridge to the page script, and the page's documents as its own session sees them commit and
3
+ * load, so a load follows its commit. At the library's first read of a page, its own protocol
4
+ * session registers the script, so every later document runs it in an isolated world from its
5
+ * start; calls go to the current document's world. The script is composed from the domains'
6
+ * page-side parts, the `*.inpage.ts` modules, in the order they depend on one another. At the first
7
+ * read of a page's changes, its session registers the recorder too, so every later document
8
+ * records from its start.
9
+ */
10
+ import { Effect, Schema, SynchronizedRef } from "effect";
11
+
12
+ import { type BrowserError, Failed } from "../../BrowserError.ts";
13
+ import { Navigated, PageLoaded } from "../../BrowserEvent.ts";
14
+ import { edit, type Edit } from "../input/edit.inpage.ts";
15
+ import { evidence } from "../input/evidence.inpage.ts";
16
+ import { guard, type Guard } from "../input/guard.inpage.ts";
17
+ import { targets, type Targets } from "../input/targets.inpage.ts";
18
+ import { context } from "../reading/context.inpage.ts";
19
+ import { controls, type Controls } from "../reading/controls.inpage.ts";
20
+ import { expectations, type Expectations } from "../reading/expect.inpage.ts";
21
+ import { match } from "../reading/match.inpage.ts";
22
+ import { names } from "../reading/names.inpage.ts";
23
+ import { outline, type Outline } from "../reading/outline.inpage.ts";
24
+ import { ready } from "../reading/ready.inpage.ts";
25
+ import { subjects, type Subjects } from "../reading/subjects.inpage.ts";
26
+ import { text, type Texts } from "../reading/text.inpage.ts";
27
+ import { walk } from "../reading/walk.inpage.ts";
28
+ import { changes, type Changes } from "../timeline/changes.inpage.ts";
29
+ import { fold } from "../timeline/fold.inpage.ts";
30
+ import { history } from "../timeline/history.inpage.ts";
31
+ import { marks } from "../timeline/marks.inpage.ts";
32
+ import { record, type Recorder } from "../timeline/record.inpage.ts";
33
+ import { sight } from "../timeline/sight.inpage.ts";
34
+ import { contextGone, failWith, type PageContext, undispatched } from "./context.ts";
35
+ import * as Url from "./url.ts";
36
+
37
+ /** What the script installs: a version, and a function of one of its parts for each call. */
38
+ export interface PageApi {
39
+ readonly version: number;
40
+ snapshot: Outline["snapshot"];
41
+ find: Subjects["find"];
42
+ until: Expectations["until"];
43
+ waitUntil: Controls["waitUntil"];
44
+ text: Texts["read"];
45
+ record: Recorder["start"];
46
+ changes: Changes["read"];
47
+ ready: ReturnType<typeof ready>["wait"];
48
+ settle: ReturnType<typeof ready>["settle"];
49
+ point: Targets["point"];
50
+ scrollPlan: Targets["scrollPlan"];
51
+ viewport: Outline["viewport"];
52
+ /** The screen as the page reads it, which another session may emulate. */
53
+ screenSize: () => { readonly width: number; readonly height: number };
54
+ prepareInput: Guard["prepareInput"];
55
+ validateInput: Guard["validateInput"];
56
+ typeable: Edit["typeable"];
57
+ focus: Edit["focus"];
58
+ focusOn: Edit["focusOn"];
59
+ select: Edit["select"];
60
+ }
61
+
62
+ declare global {
63
+ // The API this script installs, which exists only inside the library's isolated world.
64
+ var __effectBrowser: PageApi | undefined;
65
+ }
66
+
67
+ // Like the parts it receives, this is evaluated from its source text.
68
+ const install = (
69
+ makeNames: typeof names,
70
+ makeWalk: typeof walk,
71
+ makeMatch: typeof match,
72
+ makeContext: typeof context,
73
+ makeSubjects: typeof subjects,
74
+ makeExpectations: typeof expectations,
75
+ makeText: typeof text,
76
+ makeControls: typeof controls,
77
+ makeFold: typeof fold,
78
+ makeHistory: typeof history,
79
+ makeSight: typeof sight,
80
+ makeMarks: typeof marks,
81
+ makeRecord: typeof record,
82
+ makeChanges: typeof changes,
83
+ makeReady: typeof ready,
84
+ makeOutline: typeof outline,
85
+ makeTargets: typeof targets,
86
+ makeEvidence: typeof evidence,
87
+ makeGuard: typeof guard,
88
+ makeEdit: typeof edit,
89
+ makeAddresses: typeof Url.addresses,
90
+ ): PageApi => {
91
+ const installed = globalThis.__effectBrowser;
92
+
93
+ if (installed !== undefined && installed.version === 19) return installed;
94
+ const named = makeNames();
95
+ const walked = makeWalk(named);
96
+ const placing = makeContext(named, walked);
97
+ const subjected = makeSubjects(named, walked, makeMatch(), placing);
98
+ const texts = makeText(named, walked);
99
+ const controlled = makeControls(named, walked, subjected, texts);
100
+ const kept = makeHistory(makeFold());
101
+ const seeing = makeSight(named, walked, texts, kept);
102
+ const marking = makeMarks(named, kept, seeing);
103
+ const recorder = makeRecord(named, texts, kept, seeing, marking);
104
+ const read = makeOutline(named, walked, subjected, texts, controlled, makeAddresses());
105
+ const located = makeTargets(named, walked, placing);
106
+ const guarded = makeGuard(named, located, makeEvidence(named, placing));
107
+ const edited = makeEdit(named, guarded, placing);
108
+ const readiness = makeReady(walked, texts, kept, recorder);
109
+
110
+ const api: PageApi = {
111
+ version: 19,
112
+ snapshot: read.snapshot,
113
+ find: subjected.find,
114
+ until: makeExpectations(named, subjected).until,
115
+ waitUntil: controlled.waitUntil,
116
+ text: texts.read,
117
+ record: recorder.start,
118
+ changes: makeChanges(named, placing, kept, seeing, marking, recorder).read,
119
+ ready: readiness.wait,
120
+ settle: readiness.settle,
121
+ point: located.point,
122
+ scrollPlan: located.scrollPlan,
123
+ viewport: read.viewport,
124
+ screenSize: () => ({ width: screen.width, height: screen.height }),
125
+ prepareInput: guarded.prepareInput,
126
+ validateInput: guarded.validateInput,
127
+ typeable: edited.typeable,
128
+ focus: edited.focus,
129
+ focusOn: edited.focusOn,
130
+ select: edited.select,
131
+ };
132
+
133
+ globalThis.__effectBrowser = api;
134
+
135
+ return api;
136
+ };
137
+
138
+ /** The expression that installs the script and evaluates to its API. */
139
+ export const installSource = `(${install.toString()})(${[names, walk, match, context, subjects, expectations, text, controls, fold, history, sight, marks, record, changes, ready, outline, targets, evidence, guard, edit, Url.addresses].join(", ")})`;
140
+
141
+ const worldName = "effect-browser";
142
+
143
+ // What starts the recorder in each new document, after the script installed itself there.
144
+ const recorderSource = `if (globalThis === globalThis.top) globalThis.__effectBrowser?.record();`;
145
+
146
+ // What a call reads in a world without the script's API. A context id can name another world once
147
+ // the page has moved to a new process, so a cached id that reads this is not the library's world.
148
+ const missing = "effect-browser: no __effectBrowser in this world";
149
+
150
+ const isMissing = (error: BrowserError) =>
151
+ error.reason._tag === "Failed" && error.reason.detail === missing;
152
+
153
+ /** A frame as a page session reports its commit. */
154
+ export const Commit = Schema.Struct({
155
+ parentId: Schema.optionalKey(Schema.String),
156
+ loaderId: Schema.String,
157
+ url: Schema.String,
158
+ urlFragment: Schema.optionalKey(Schema.String),
159
+ });
160
+
161
+ /** `Page.frameNavigated` or `Page.navigatedWithinDocument`, as far as `mainFrame` reads them. */
162
+ export const MainFrameEvent = Schema.Union([
163
+ Schema.Struct({ frame: Commit }),
164
+ Schema.Struct({ frameId: Schema.String, url: Schema.String }),
165
+ ]);
166
+
167
+ /** A commit's loader and its address, redacted. */
168
+ export const commitOf = ({ loaderId, url, urlFragment = "" }: typeof Commit.Type) => ({
169
+ loader: loaderId,
170
+ url: Url.redact(url + urlFragment),
171
+ });
172
+
173
+ /**
174
+ * What a page session's commit or move says of `page`'s main frame: a new document's `loader` and
175
+ * its address, or a move's address; nothing for another frame. Only a commit is a new document:
176
+ * `frameStartedLoading` also fires on `pushState`, a move within the document.
177
+ */
178
+ export const mainFrame = (page: string, event: typeof MainFrameEvent.Type) =>
179
+ "frame" in event
180
+ ? event.frame.parentId === undefined
181
+ ? commitOf(event.frame)
182
+ : undefined
183
+ : event.frameId === page
184
+ ? { loader: undefined, url: Url.redact(event.url) }
185
+ : undefined;
186
+
187
+ /** A call to the page script, its arguments quoted as JavaScript literals. */
188
+ export const scriptCall = (name: string, ...args: ReadonlyArray<unknown>): string =>
189
+ `${name}(${args.map((arg) => JSON.stringify(arg)).join(", ")})`;
190
+
191
+ export const make = Effect.fnUntraced(function* (page: PageContext) {
192
+ const { id, cdp, native, span, publish, now } = page;
193
+ const { send } = page.protocol;
194
+ // How far this session has registered: the script, then the recorder, which the first read of
195
+ // changes asks for. One caller takes each step, once, while the others wait.
196
+ const registration = yield* SynchronizedRef.make<"none" | "script" | "recorder">("none");
197
+ // The current document's world, forgotten when the main frame commits another document; the
198
+ // commits this session has seen, the latest marked on the page; the page's address; and how far
199
+ // the current document has loaded.
200
+ let world: number | undefined;
201
+ let documents = 0;
202
+ let url = Url.redact(page.url);
203
+ let loaded: PageLoaded | undefined;
204
+ // The latest commits' loaders and the documents they began, newest last: a capture connection
205
+ // sees the same commits, and numbers its frames by them.
206
+ const loaders = new Map<string, number>();
207
+
208
+ const onMainFrame = (event: typeof MainFrameEvent.Type) => {
209
+ const seen = mainFrame(id, event);
210
+
211
+ if (seen === undefined) return;
212
+ if (seen.loader !== undefined) {
213
+ documents++;
214
+ loaders.delete(seen.loader);
215
+ loaders.set(seen.loader, documents);
216
+ const [oldest] = loaders.keys();
217
+
218
+ if (loaders.size > 8 && oldest !== undefined) loaders.delete(oldest);
219
+ page.activity.documentAt = now();
220
+ world = undefined;
221
+ loaded = undefined;
222
+ }
223
+ url = seen.url;
224
+ const sameDocument = seen.loader === undefined;
225
+
226
+ publish(new Navigated({ at: now(), page: id, url, document: documents, sameDocument }));
227
+ };
228
+
229
+ // The session tells these of the main frame alone.
230
+ const onLoaded = (state: PageLoaded["state"]) => () => {
231
+ loaded = new PageLoaded({ at: now(), page: id, state });
232
+ publish(loaded);
233
+ };
234
+
235
+ cdp.on("Page.frameNavigated", onMainFrame);
236
+ cdp.on("Page.navigatedWithinDocument", onMainFrame);
237
+ cdp.on("Page.domContentEventFired", onLoaded("domcontentloaded"));
238
+ cdp.on("Page.loadEventFired", onLoaded("load"));
239
+
240
+ /**
241
+ * The main-frame commits seen so far, once the Page domain, which registration turned on, has
242
+ * answered, so that every later one counts. A commit within about one round trip of the count
243
+ * can still be missed.
244
+ */
245
+ const currentDocument = (operation: string) =>
246
+ page.paging(operation).pipe(Effect.map(() => documents));
247
+
248
+ /** What a frame that arrives now shows: its session, the page's current document and address. */
249
+ const frameTag = () => ({
250
+ session: page.session,
251
+ connection: page.connection,
252
+ document: documents,
253
+ url,
254
+ });
255
+
256
+ /** The document the main frame's latest commit of `loader` began, if this session counted it. */
257
+ const documentOf = (loader: string) => loaders.get(loader);
258
+
259
+ // The registration belongs to this session and the world to the page, so a session that attaches
260
+ // later, as after a reconnect, registers again and finds the script already installed. With the
261
+ // Page domain enabled, a source runs in each new document, in the order registered;
262
+ // `runImmediately` runs it now.
263
+ const addScript = (operation: string, source: string) =>
264
+ native(operation, () =>
265
+ send("Page.addScriptToEvaluateOnNewDocument", { source, worldName, runImmediately: true }),
266
+ );
267
+
268
+ const register = (operation: string) =>
269
+ Effect.all(
270
+ [
271
+ page.paging(operation),
272
+ addScript(operation, `if (globalThis === globalThis.top) ${installSource};`),
273
+ ],
274
+ { concurrency: "unbounded" },
275
+ ).pipe(span("Page.register", {}, "Debug"));
276
+
277
+ /** Take registration from `from` to `to` by `step`, unless it is past `from` already. */
278
+ const advance = (
279
+ from: "none" | "script",
280
+ to: "script" | "recorder",
281
+ step: Effect.Effect<unknown, BrowserError>,
282
+ ) =>
283
+ SynchronizedRef.updateEffect(registration, (state) =>
284
+ state === from ? Effect.as(step, to) : Effect.succeed(state),
285
+ );
286
+
287
+ /** Register the script once, and the recorder after it once a read of changes asks for it. */
288
+ const ensure = (operation: string, recorder: boolean) =>
289
+ Effect.andThen(
290
+ advance("none", "script", register(operation)),
291
+ recorder ? advance("script", "recorder", addScript(operation, recorderSource)) : Effect.void,
292
+ );
293
+
294
+ const createWorld = (operation: string) =>
295
+ Effect.gen(function* () {
296
+ yield* ensure(operation, false);
297
+ const document = documents;
298
+
299
+ // The main frame's id is the page's target id.
300
+ const created = yield* native(operation, () =>
301
+ send("Page.createIsolatedWorld", { frameId: id, worldName }),
302
+ );
303
+
304
+ // A world the page replaced while it was being looked up serves this call, not later ones.
305
+ if (document === documents) world = created.executionContextId;
306
+
307
+ return created.executionContextId;
308
+ }).pipe(span("Page.createWorld", {}, "Debug"));
309
+
310
+ /** The current document's world, created if it has none yet. */
311
+ const current = (operation: string) =>
312
+ Effect.suspend(() => (world === undefined ? createWorld(operation) : Effect.succeed(world)));
313
+
314
+ /** A world of the current document without the page script, as the clock probe needs. */
315
+ const bareWorld = (operation: string) =>
316
+ native(operation, () =>
317
+ send("Page.createIsolatedWorld", { frameId: id, worldName: `${worldName}-clock` }),
318
+ ).pipe(Effect.map((created) => created.executionContextId));
319
+
320
+ const run = (operation: string, expression: string, contextId: number) =>
321
+ native(operation, () =>
322
+ send("Runtime.evaluate", { contextId, expression, returnByValue: true, awaitPromise: true }),
323
+ ).pipe(
324
+ Effect.flatMap(({ result, exceptionDetails: thrown }) =>
325
+ thrown === undefined
326
+ ? Effect.succeed<unknown>(result.value)
327
+ : failWith(
328
+ operation,
329
+ new Failed({ detail: thrown.exception?.description ?? thrown.text }),
330
+ ),
331
+ ),
332
+ );
333
+
334
+ const evaluateIn = (operation: string, call: string, contextId: number) =>
335
+ run(
336
+ operation,
337
+ `globalThis.__effectBrowser === undefined ? ${JSON.stringify(missing)} : globalThis.__effectBrowser.${call}`,
338
+ contextId,
339
+ ).pipe(
340
+ Effect.filterOrFail(
341
+ (value) => value !== missing,
342
+ () => undispatched(operation, new Failed({ detail: missing })),
343
+ ),
344
+ // One round trip to the page. Its arguments can hold typed text, so only the call is named.
345
+ span("Page.evaluate", { function: call.split("(")[0] }, "Trace"),
346
+ );
347
+
348
+ // Ordinary reads can recreate a document's world. Approval validation deliberately cannot. A
349
+ // world found by its name that lacks the script, in a document the registration missed, gets it.
350
+ const evaluateWithContext = (operation: string, call: string) => {
351
+ const attempt = (contextId: number) =>
352
+ evaluateIn(operation, call, contextId).pipe(Effect.map((value) => ({ contextId, value })));
353
+
354
+ const again = createWorld(operation).pipe(
355
+ Effect.flatMap((contextId) =>
356
+ attempt(contextId).pipe(
357
+ Effect.catchIf(isMissing, () =>
358
+ run(operation, `${installSource}.version`, contextId).pipe(
359
+ Effect.andThen(attempt(contextId)),
360
+ ),
361
+ ),
362
+ ),
363
+ ),
364
+ );
365
+
366
+ return current(operation).pipe(
367
+ Effect.flatMap(attempt),
368
+ Effect.catchIf(contextGone, () => again),
369
+ );
370
+ };
371
+
372
+ const evaluate = (operation: string, call: string) =>
373
+ evaluateWithContext(operation, call).pipe(Effect.map(({ value }) => value));
374
+
375
+ return {
376
+ current,
377
+ bareWorld,
378
+ evaluate,
379
+ evaluateIn,
380
+ evaluateWithContext,
381
+ currentDocument,
382
+ frameTag,
383
+ documentOf,
384
+ /** How far the current document has loaded, as this session heard; nothing until parsed. */
385
+ loaded: () => loaded,
386
+ /** Register the recorder, so every later document records from its start. */
387
+ registerRecorder: (operation: string) => ensure(operation, true),
388
+ };
389
+ });
390
+
391
+ export type Bridge = Effect.Success<ReturnType<typeof make>>;