effect-browser 0.2.0-beta.0 → 0.2.0-beta.2
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 +163 -12
- package/dist/{Association-zlgInoaj.mjs → Association-BsSfBYVx.mjs} +14 -10
- package/dist/Association-BsSfBYVx.mjs.map +1 -0
- package/dist/{Bootstrap-Be-2Ddmh.mjs → Bootstrap-CeQOWBy6.mjs} +6 -4
- package/dist/Bootstrap-CeQOWBy6.mjs.map +1 -0
- package/dist/Bootstrap.mjs +1 -1
- package/dist/Browser.d.mts +13 -1
- package/dist/Browser.mjs.map +1 -1
- package/dist/{BrowserData-DJAnUHoZ.d.mts → BrowserData-D_zsf79q.d.mts} +102 -11
- package/dist/BrowserData.d.mts +2 -2
- package/dist/BrowserData.mjs +101 -10
- package/dist/BrowserData.mjs.map +1 -1
- package/dist/BrowserRuntime-e7MXqjOK.d.mts +143 -0
- package/dist/{BrowserRuntime-fZz1b0VI.mjs → BrowserRuntime-xBCLDy_5.mjs} +2847 -2226
- package/dist/BrowserRuntime-xBCLDy_5.mjs.map +1 -0
- package/dist/BrowserRuntime.d.mts +3 -139
- package/dist/BrowserRuntime.mjs +1 -1
- package/dist/Capture.d.mts +15 -1
- package/dist/Capture.mjs +58 -22
- package/dist/Capture.mjs.map +1 -1
- package/dist/{CaptureData-AmqVP_Ba.mjs → CaptureData-B5-mXlyq.mjs} +24 -6
- package/dist/CaptureData-B5-mXlyq.mjs.map +1 -0
- package/dist/CaptureData.d.mts +11 -2
- package/dist/CaptureData.mjs +1 -1
- package/dist/Chromium.d.mts +4 -3
- package/dist/Chromium.mjs +22 -16
- package/dist/Chromium.mjs.map +1 -1
- package/dist/Errors.d.mts +2 -2
- package/dist/Errors.mjs +1 -0
- package/dist/Errors.mjs.map +1 -1
- package/dist/PageControl.d.mts +1 -1
- package/dist/PageControl.mjs +1 -1
- package/dist/Testing.d.mts +437 -0
- package/dist/Testing.mjs +1561 -0
- package/dist/Testing.mjs.map +1 -0
- package/package.json +5 -1
- package/dist/Association-zlgInoaj.mjs.map +0 -1
- package/dist/Bootstrap-Be-2Ddmh.mjs.map +0 -1
- package/dist/BrowserRuntime-fZz1b0VI.mjs.map +0 -1
- package/dist/CaptureData-AmqVP_Ba.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -17,7 +17,8 @@ Mutations are serialized. An observed node remains usable only until an invalida
|
|
|
17
17
|
`effect-browser/chromium` provides `Chromium`. It uses the same modeled browser owner, native driver, exact-node observations, bootstrap bindings, capture and page control as a hosted session. It requires no Browserbase client, project, key, allocation response or provider endpoint. `BrowserSession<E>` is their shared modeled capability; `BrowserbaseSession<E>` retains the separate provider reference, artifacts, Live View, handoff and release contracts.
|
|
18
18
|
|
|
19
19
|
```ts
|
|
20
|
-
import {
|
|
20
|
+
import { NodeServices } from "@effect/platform-node";
|
|
21
|
+
import { Effect, Layer } from "effect";
|
|
21
22
|
import * as Browser from "effect-browser/browser";
|
|
22
23
|
import { BrowserPolicy } from "effect-browser/browser-data";
|
|
23
24
|
import { Chromium } from "effect-browser/chromium";
|
|
@@ -33,12 +34,12 @@ const program = Browser.scoped(Chromium.launch(BrowserPolicy.unrestricted()), (b
|
|
|
33
34
|
viewport: { width: 1280, height: 720 },
|
|
34
35
|
pageControl: true,
|
|
35
36
|
launch: { headless: true, chromiumSandbox: true },
|
|
36
|
-
}),
|
|
37
|
+
}).pipe(Layer.provide(NodeServices.layer)),
|
|
37
38
|
),
|
|
38
39
|
);
|
|
39
40
|
```
|
|
40
41
|
|
|
41
|
-
Layer construction validates configuration and starts nothing. The optional `playwright-core` peer is loaded only when needed. `Chromium.acquire(policy, { bootstrap })` starts one owned Chromium process and registers cleanup before waiting for a connection; its cached `connect` yields one `ChromiumSession<E>`. `Chromium.launch` combines those steps. These static operations access the configured service, as do `Chromium.attach` and the corresponding `BrowserbaseBrowser` operations. Bootstrap consumer errors and services remain in the acquisition signatures.
|
|
42
|
+
The Layer requires Effect's `Crypto`, which the host platform supplies: `NodeServices.layer` here, `BunServices.layer` on Bun, or `NodeCrypto.layer` alone. Process references, connection ids, native binding names and handoff tokens are drawn from it. The Layer captures it once, so `launch`, `acquire` and `attach` never require it. Layer construction validates configuration and starts nothing. The optional `playwright-core` peer is loaded only when needed. `Chromium.acquire(policy, { bootstrap })` starts one owned Chromium process and registers cleanup before waiting for a connection; its cached `connect` yields one `ChromiumSession<E>`. `Chromium.launch` combines those steps. These static operations access the configured service, as do `Chromium.attach` and the corresponding `BrowserbaseBrowser` operations. Bootstrap consumer errors and services remain in the acquisition signatures.
|
|
42
43
|
|
|
43
44
|
`Browser.scoped(open, use)` is the common workflow supervisor for both sources, including borrowed attachment. It runs the acquisition once in its own scope, retains the concrete browser type in `use`, and races workflow completion against typed fail-session callbacks. The workflow has its own child scope: fibers and finalizers finish before `closeChecked` runs, so callback cleanup may still use a healthy browser. Checked closure runs on success, failure, thrown defects and cancellation. A body failure and a cleanup failure both remain in this workflow's own final Effect cause; neither is overwritten. An outer race or timeout can select another result and discard that losing cause even though cleanup ran. Configure the provider's `onCleanup` to record its receipt in a host-owned sink outside the raced workflow when cleanup evidence must survive that composition. A body that completes concurrently with a fail-session callback can still win the race; supervision does not establish failure priority. The ownership finalizer still runs if acquisition itself fails. No failed acquisition or action is replayed.
|
|
44
45
|
|
|
@@ -49,6 +50,7 @@ The same combinator supports `open.pipe(Browser.scoped(use))`, including a reusa
|
|
|
49
50
|
An application can share one acquired session through a Layer for its finite application scope:
|
|
50
51
|
|
|
51
52
|
```ts
|
|
53
|
+
import { NodeServices } from "@effect/platform-node";
|
|
52
54
|
import { Context, Effect, Layer } from "effect";
|
|
53
55
|
import { Chromium, type ChromiumSession } from "effect-browser/chromium";
|
|
54
56
|
import { BrowserPolicy } from "effect-browser/browser-data";
|
|
@@ -60,7 +62,10 @@ class SharedBrowser extends Context.Service<SharedBrowser, ChromiumSession>()(
|
|
|
60
62
|
const shared = Layer.effect(
|
|
61
63
|
SharedBrowser,
|
|
62
64
|
Chromium.launch(BrowserPolicy.unrestricted({ maxElapsedMillis: 300_000 })),
|
|
63
|
-
).pipe(
|
|
65
|
+
).pipe(
|
|
66
|
+
Layer.provide(Chromium.layer({ onCleanup: recordReceipt })),
|
|
67
|
+
Layer.provide(NodeServices.layer),
|
|
68
|
+
);
|
|
64
69
|
|
|
65
70
|
const program = Effect.gen(function* () {
|
|
66
71
|
const browser = yield* SharedBrowser;
|
|
@@ -129,7 +134,7 @@ Chromium identity is `{ provider: "chromium", id }`, identifying this ownership
|
|
|
129
134
|
```ts
|
|
130
135
|
const status = yield * session.status;
|
|
131
136
|
const diagnostics = yield * session.diagnostics;
|
|
132
|
-
// status: { phase, reason, generation, busy, unresolvedDispatch }
|
|
137
|
+
// status: { phase, reason, generation, busy, unresolvedDispatch, actions: { used, maximum } }
|
|
133
138
|
// diagnostics: { records, total, dropped, truncated }
|
|
134
139
|
```
|
|
135
140
|
|
|
@@ -142,6 +147,16 @@ new case. `reason` retains the original terminal trigger through later cleanup.
|
|
|
142
147
|
active admission or pending policy cleanup. Action-count or host-read exhaustion leaves the owner
|
|
143
148
|
open and is not a terminal failure.
|
|
144
149
|
|
|
150
|
+
`actions` is the model-reachable allowance as the owner counts it: `maximum` is the policy's
|
|
151
|
+
`maxActions` and `used` the operations admitted against it, whether they then succeeded or not.
|
|
152
|
+
Every step of `fillForm` and its submit is an action, and so is each reading an agent Tool takes
|
|
153
|
+
after its action; a form's verification, `checkpoint`, `controlFacts` and `status` itself are not.
|
|
154
|
+
A refused operation is not counted, so `used` never passes `maximum`, and at `maximum` the next
|
|
155
|
+
action fails `Limit { dimension: "actions" }` undispatched while the owner stays open. A host that
|
|
156
|
+
runs one long session reads `used` instead of counting actions itself. `BrowserPolicy.maxActions`
|
|
157
|
+
accepts 1–1,000,000 (100 by default, like the host-read allowance's bound); a long session raises it
|
|
158
|
+
together with `maxElapsedMillis`.
|
|
159
|
+
|
|
145
160
|
`unresolvedDispatch` is separate from the trigger. Idle expiry records `expired` without inventing
|
|
146
161
|
an unknown dispatch. Expiry or a fail-session callback overlapping native work retains both the
|
|
147
162
|
known trigger and unresolved control. A fence or borrowed disconnection is not acknowledgement
|
|
@@ -317,7 +332,16 @@ const seen = yield * session.observe({ scope: "viewport" });
|
|
|
317
332
|
seen.viewport; // { clippedText, coveredText, uncertainText, unreachableControls, exhausted, ... }
|
|
318
333
|
```
|
|
319
334
|
|
|
320
|
-
Visibility here is geometry and hit-testing, never a pixel comparison, and the counts say which was which. Text is kept when its line boxes intersect the viewport and the browser finds its own element at a sampled point. A text node that crosses the viewport edge contributes only its lines on screen (`clippedText`). Text behind another element is left out (`coveredText`).
|
|
335
|
+
Visibility here is geometry and hit-testing, never a pixel comparison, and the counts say which was which. Text is kept when its line boxes intersect the viewport and the browser finds its own element at a sampled point. A text node that crosses the viewport edge contributes only its lines on screen (`clippedText`). Text behind another element is left out (`coveredText`). Something that takes no pointer events is invisible to that hit test, so for a point beneath one the browser is asked, on the existing connection, which box is on top with pointer events ignored; that also finds boxes inside closed shadow roots. A box that paints at the point, with a fill, border, shadow, filter, image, canvas, SVG, text or a generated `::before`/`::after`, covers the text (`coveredText`); an empty transparent container over the whole page hides nothing. Text the browser could not settle, for example beneath a second such box, in a child frame, or past 256 such points in one reading, is left out as `uncertainText` rather than called visible. `exhausted` means the traversal budget ran out first and the reading is known to be incomplete. Canvas pixels and compositing effects are not interpreted.
|
|
336
|
+
|
|
337
|
+
`match` narrows a reading, in either scope, to what contains a piece of text, ignoring case: text lines, and controls whose label contains it. A select is kept when its own label or any of its option labels matches, with its options beside it. The filter runs inside the page before `maxControls` and `maxTextBytes` are spent, so twenty header links cannot crowd out the one control asked for:
|
|
338
|
+
|
|
339
|
+
```ts
|
|
340
|
+
const found = yield * session.observe({ scope: "document", match: "create account" });
|
|
341
|
+
found.match; // "create account"
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
It filters what is read and never searches for, re-finds or substitutes a node: references still come from the reading itself, with every exact-node check. What a matched reading leaves out is not evidence of absence, which is why it names its `match`.
|
|
321
345
|
|
|
322
346
|
An `Observation` is safe to show a model, and the adapter's `browser_inspect` Tool returns it as is. It therefore carries no destination, form target or field value, in either scope. What a host needs to decide whether a control may be acted on is a separate, host-only read from the exact node:
|
|
323
347
|
|
|
@@ -350,6 +374,8 @@ yield *
|
|
|
350
374
|
|
|
351
375
|
Anything but `true`, or a policy that throws, sends nothing and fails `denied`. The policy is a plain synchronous function on purpose: it runs while the owner's permit is held, where waiting on a model or a network call would stall every other operation. It is not an atomic check-and-input transaction, because page script can still run before the native input lands.
|
|
352
376
|
|
|
377
|
+
`fillElement` also refuses, undispatched, what the maintained engine would otherwise refuse only after dispatch, where the unknown outcome would fence the owner: a hidden control (`not-visible`), a disabled one (`disabled`), one that is not an editable input, textarea or content-editable element, and text that a `number`, `date`, `time`, `range` or other value-typed input would not keep (`unsupported`). The value is checked on a detached copy with the same constraints; the page's own control is not touched until the fill is sent.
|
|
378
|
+
|
|
353
379
|
### Exact native option selection
|
|
354
380
|
|
|
355
381
|
`selectOption(reference, options, admission?)` selects once on the native `<select>` named by
|
|
@@ -377,6 +403,33 @@ maintained engine, returns `ActionResult` and retires the observation on that pa
|
|
|
377
403
|
claim the website finished work triggered by those events. The same synchronous host `admission`
|
|
378
404
|
used for exact-node input applies to the selected control.
|
|
379
405
|
|
|
406
|
+
### Filling a form in one operation
|
|
407
|
+
|
|
408
|
+
`fillForm(request, admission?, options?)` sets several controls of one observation, in order, then optionally clicks one submit control:
|
|
409
|
+
|
|
410
|
+
```ts
|
|
411
|
+
const result =
|
|
412
|
+
yield *
|
|
413
|
+
session.fillForm({
|
|
414
|
+
observationId: seen.observationId,
|
|
415
|
+
fields: [
|
|
416
|
+
{ elementId: email, value: "ada@example.test" },
|
|
417
|
+
{ elementId: terms, checked: true },
|
|
418
|
+
{ elementId: plan, options: [pro] },
|
|
419
|
+
],
|
|
420
|
+
submit: create,
|
|
421
|
+
});
|
|
422
|
+
// { fields: [{ elementId, status: "set" | "unchanged" }], submitted, url, stopped? }
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
Each field gives exactly one of `value`, text that replaces the contents of an input, textarea or content-editable element; `checked`, the state a checkbox, radio or switch should end in, clicked only when it differs, and a native radio is never asked to clear itself; or `options`, issued option IDs of a native select exactly as for `selectOption`. A form has at most 32 fields, each control at most once, and its submit control is not also one of its fields.
|
|
426
|
+
|
|
427
|
+
Every step is its own admitted and charged action on the exact observed node, after the same fresh checks as `fillElement`, `selectOption` and `clickElement`, including the host's `admission` policy. One difference is deliberate: a control may have become enabled since it was observed, such as a submit button a form enables once it is complete, but it must be enabled when its step runs. The observation stays usable for this form's own steps only. Navigation, a page hold or any other caller's action still retires it, and the form retires it when it ends.
|
|
428
|
+
|
|
429
|
+
After each dispatched step, the page's own handlers get at least two rendered frames and `settleMillis` (50 by default, at most 5000, 0 to skip). A text field is then left, as a person moving on would leave it, so formatting a page applies on blur belongs to that step. Before submit, `verify` (true by default) reads every field again and stops the form when one no longer holds what its own step left there: a re-render, an asynchronous reset, anything that changed a field after it was set. What was read is compared on the host and never returned.
|
|
430
|
+
|
|
431
|
+
The first refused or uncertain step ends the form, and nothing after it is sent. A form is not a transaction: fields set before the stop stay set, and `stopped` says where it ended (`field`, `verify` or `submit`) and why, as a `BrowserError` whose outcome says whether that step itself was dispatched. A toggle the page would not change stops with `failed` and `rejected`. `submitted` is true only when the submit click completed. The operation fails outright only when its first step does, exactly as that single action would.
|
|
432
|
+
|
|
380
433
|
### Bounded waits that leave room for recording
|
|
381
434
|
|
|
382
435
|
`waitFor({ selector, state })` still supports the host's bounded selector conditions: `visible`,
|
|
@@ -426,7 +479,8 @@ Exhaustion reports `Limit { dimension: "host-reads", maximum, observed } / undis
|
|
|
426
479
|
owner stays open. These operations still obey the same lifetime, bytes, deadline and fail-fast
|
|
427
480
|
concurrency bounds: a checkpoint can execute page script and is not unlimited free work.
|
|
428
481
|
`observe`, `readText`, `screenshot` and `waitFor` continue consuming the model-reachable action
|
|
429
|
-
allowance. Neither budget is a tool parameter, and `maxActions`
|
|
482
|
+
allowance, which `status.actions` reports. Neither budget is a tool parameter, and `maxActions`
|
|
483
|
+
has not become mutations-only.
|
|
430
484
|
|
|
431
485
|
### Real pointer and wheel input
|
|
432
486
|
|
|
@@ -582,6 +636,34 @@ The old `dropped` field is removed, not redefined as overflow. A late frame was
|
|
|
582
636
|
delivery; that fact does not prove network loss. The same counters appear in live snapshots,
|
|
583
637
|
and `upstreamDrops` remains `"unknown"`. The default frame capacity remains four.
|
|
584
638
|
|
|
639
|
+
### Watching live or with a delay
|
|
640
|
+
|
|
641
|
+
A viewer can watch as it happens, or a few seconds behind so that text or other media made about a moment is ready when that moment is shown. Both are the same interval. For a delay, the consumer waits until each frame's `receivedMonotonicNanos` plus the delay before passing it on. While it waits, frames stay in the interval's own buffer, so `maxFrames` (at most 1024) and `maxBufferedBytes` are the delay's memory bound, and anything beyond them is dropped oldest-first and counted as `overflow`. Size them for the delay: five seconds of a busy page at up to 60 frames a second is 300 frames, and a 1280×720 JPEG of dense text is about 200 KB. The same clock stamps `documentBoundaries`, so an address bar drawn beside the frames changes when the frame after a boundary is shown. The record keeps the latest 64 boundaries, so a long stream can still name what it is showing. An interval may last up to six hours (`maxDurationMillis`, 60 seconds by default).
|
|
642
|
+
|
|
643
|
+
```ts
|
|
644
|
+
import { Clock, Effect, Stream } from "effect";
|
|
645
|
+
|
|
646
|
+
const interval =
|
|
647
|
+
yield *
|
|
648
|
+
Capture.start(session, {
|
|
649
|
+
lifetime: "page",
|
|
650
|
+
size: { width: 1280, height: 720 },
|
|
651
|
+
maxFrames: 400,
|
|
652
|
+
maxBufferedBytes: 64 * 1024 * 1024,
|
|
653
|
+
maxDurationMillis: 30 * 60_000,
|
|
654
|
+
});
|
|
655
|
+
const delayed = interval.frames.pipe(
|
|
656
|
+
Stream.mapEffect((frame) =>
|
|
657
|
+
Effect.flatMap(Clock.monotonicTimeNanos, (now) => {
|
|
658
|
+
const wait = Number(frame.receivedMonotonicNanos + 5_000_000_000n - now) / 1e6;
|
|
659
|
+
return Effect.as(wait > 0 ? Effect.sleep(wait) : Effect.void, frame);
|
|
660
|
+
}),
|
|
661
|
+
),
|
|
662
|
+
);
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
`Capture.multipart(frames)` turns frames into one `multipart/x-mixed-replace` response for an `<img>`, the motion JPEG that WHATWG HTML defines for images. It closes each frame with the next part's delimiter and headers at once, because a browser shows a part only when it has read the headers of the one after it: without that, a viewer runs one frame behind and never shows the last picture of a page that has gone still. It writes no metadata, so nothing but pictures reaches a viewer, and it draws a new boundary from `Crypto` for each response. Call it once per viewer, over one fan-out of the interval, for example a sliding `PubSub` with `replay: 1` so that a slow viewer skips frames without slowing anyone else and a new one is shown the current picture. End that fan-out before the HTTP server stops: a server waits for its open responses.
|
|
666
|
+
|
|
585
667
|
Closing, navigating, detaching a relevant frame, or resizing the captured page ends its interval explicitly without ending a sibling page's capture. `Capture.start(session, { lifetime: "page" })` instead follows a page's main frame across documents: start it before a navigation and it covers the loading in between. The native screencast is never restarted for a navigation, so a boundary is not a gap this package introduced. Each frame carries the `document` it was received during (0, then one more per navigation), and the summary's bounded `documentBoundaries` give the last sequence before each one and the address it committed, with `initialUrl` for document 0. That is attribution by receipt order, not proof of whose pixels a frame shows: one received just after a navigation can still show the document before it. Selecting another page or frame does not invalidate an unrelated interval. Handoff pause, connection loss, an uncertain owner and session closure still invalidate all child intervals. A confirmed native stop releases only its own reservation; a failed stop on a live page keeps that target quarantined. A definitively closed page releases its capture reservation. Stopping a child capture does not close its browser. The frame seam has **no website-audio source**, so this package does not synthesize silent samples or infer audio support from a video container. Caller encoding is demonstrated in [the caller encoder example](../browserbase/examples/record-video.ts); the example decodes every generated frame with the caller's FFmpeg and checks presentation timestamps and pixel checksums. Native acceptance requires changing pixels and source-time agreement rather than accepting container headers as video evidence. Filming across a navigation with one page-lifetime interval, resampled onto a constant-rate reel with the address of each document reported, is demonstrated in [the footage example](../browserbase/examples/realistic-footage/README.md).
|
|
586
668
|
|
|
587
669
|
### Read metadata while capture is running
|
|
@@ -596,8 +678,8 @@ const current = yield * interval.snapshot;
|
|
|
596
678
|
no browser work, consumes no frames, charges no action, and works while the page
|
|
597
679
|
is held. Repeated reads neither stop nor restart capture. `observedMonotonicNanos`
|
|
598
680
|
stamps the read; each boundary retains its own commit observation time. The
|
|
599
|
-
|
|
600
|
-
and `currentDocument` continues counting. Addresses longer than the existing
|
|
681
|
+
record keeps the latest 64 boundaries; `documentBoundariesTruncated` reports that earlier
|
|
682
|
+
ones were let go, and `currentDocument` continues counting. Addresses longer than the existing
|
|
601
683
|
bound remain `null`. The model should not receive these host-only addresses by
|
|
602
684
|
accident merely because it can inspect the page.
|
|
603
685
|
|
|
@@ -638,9 +720,77 @@ Partial native failures fence the session as uncertain; there is no success rece
|
|
|
638
720
|
|
|
639
721
|
Only explicit trusted-host `Unrestricted` network policy is implemented. `ExactHosts` and `PublicWeb` fail before acquisition. A local debugger endpoint, an input-admission callback or a filtered initial navigation URL does not establish containment for redirects, subresources, popups, workers or DNS. Applications requiring egress restrictions must operate and independently qualify that enforcing boundary. Chromium can select a host-operated proxy at launch; provider routing is configured by its own integration. These choices never silently substitute a browser provider.
|
|
640
722
|
|
|
723
|
+
## Testing without a browser
|
|
724
|
+
|
|
725
|
+
`effect-browser/testing` opens the real owner over a scripted native engine. `Testing.open(script, options)` returns a `ScriptedSession<E>`: the same `BrowserSession<E>` that Chromium and Browserbase return, and admission, budgets, staleness, dispatch evidence, capture accounting, page holds and typed callbacks are the production code paths. Only the pages and the native outcomes are scripted, so an application, a Toolkit composition or a Layer runs on the pinned Node and Bun with no Chromium process, no Playwright installation and no credentials. The defaults are the ones a Chromium launch would use: an unrestricted policy, a 1280×720 viewport and a ten-second action timeout.
|
|
726
|
+
|
|
727
|
+
A script lists documents by address, each with its text and the controls it offers. A control's `id` becomes its issued `elementId`, and observations are numbered `observation-1`, `observation-2`, …, so a scripted model turn or a stored expectation can name a node statically. A `link` or a submit control carries a `destination`, which is both its host-only destination fact and where activating it leads; a plain button navigates through `activates` and reports no destination, as a real `onclick` handler has none, and the schema refuses a destination where a real document would report none. Navigating to an unlisted address reaches an empty document at that address. A checkbox or radio `input` without `checked` starts unchecked, as a real one does. `fillForm` and matched readings take the same steps over the script: a text step records its value in `document.values` and leaves the control, a toggle is clicked only when it differs, options are the observation's issued option IDs, and `next("fill-form", …)` arms the next step, verification read or submit.
|
|
728
|
+
|
|
729
|
+
```ts
|
|
730
|
+
import { expect, it } from "@effect/vitest";
|
|
731
|
+
import { Effect } from "effect";
|
|
732
|
+
import * as Browser from "effect-browser/browser";
|
|
733
|
+
import { BrowserPolicy } from "effect-browser/browser-data";
|
|
734
|
+
import { Reasons } from "effect-browser/errors";
|
|
735
|
+
import * as Testing from "effect-browser/testing";
|
|
736
|
+
|
|
737
|
+
const shop: Testing.Script = {
|
|
738
|
+
documents: [
|
|
739
|
+
{
|
|
740
|
+
url: "https://shop.test/",
|
|
741
|
+
text: "We use cookies.",
|
|
742
|
+
controls: [
|
|
743
|
+
{
|
|
744
|
+
id: "accept",
|
|
745
|
+
kind: "button",
|
|
746
|
+
label: "Accept all",
|
|
747
|
+
activates: "https://shop.test/?consent=1",
|
|
748
|
+
},
|
|
749
|
+
],
|
|
750
|
+
},
|
|
751
|
+
{ url: "https://shop.test/?consent=1", text: "Welcome back." },
|
|
752
|
+
],
|
|
753
|
+
};
|
|
754
|
+
|
|
755
|
+
it.effect("an unknown click is never replayed", () =>
|
|
756
|
+
Browser.scoped(
|
|
757
|
+
Testing.open(shop, { policy: BrowserPolicy.unrestricted({ maxActions: 10 }) }),
|
|
758
|
+
(browser) =>
|
|
759
|
+
Effect.gen(function* () {
|
|
760
|
+
yield* browser.control.next("click", {
|
|
761
|
+
_tag: "Fail",
|
|
762
|
+
reason: Reasons.Timeout.make({}),
|
|
763
|
+
outcome: "unknown",
|
|
764
|
+
});
|
|
765
|
+
const observation = yield* browser.observe();
|
|
766
|
+
const accept = { observationId: observation.observationId, elementId: "accept" };
|
|
767
|
+
|
|
768
|
+
const first = yield* browser.clickElement(accept).pipe(Effect.flip);
|
|
769
|
+
|
|
770
|
+
expect(first).toMatchObject({ reason: { _tag: "Timeout" }, outcome: "unknown" });
|
|
771
|
+
const retry = yield* browser.clickElement(accept).pipe(Effect.flip);
|
|
772
|
+
|
|
773
|
+
expect(retry).toMatchObject({ reason: { _tag: "Closed" }, outcome: "undispatched" });
|
|
774
|
+
const clicks = (yield* browser.control.calls).filter((c) => c.operation === "click");
|
|
775
|
+
|
|
776
|
+
expect(clicks).toHaveLength(1);
|
|
777
|
+
expect(clicks[0]).toMatchObject({ dispatched: true, settled: "failed" });
|
|
778
|
+
}),
|
|
779
|
+
),
|
|
780
|
+
);
|
|
781
|
+
```
|
|
782
|
+
|
|
783
|
+
`browser.control` is the test's side of the scripted browser. The browser outlives any one connection, as a keep-alive browser does: a reconnection to the same address finds the pages it left, and the control keeps working while no connection is open, so a test can change a page while its owner is detached. `connections` lists every connection made to that browser and how each ended so far (`open`, `closed`, `dropped`, `close-failed`, or `refused` by the script's `connections: ["refuse", …]`), which makes a connection an owner left open visible. `next("disconnect", …)` scripts the owner's native teardown: `Fail` makes it fail, so the receipt reports `connection: "failed"` with a `disconnect` issue, and `Hold` parks it at a gate however long cleanup waits. A navigation stop is recorded in `calls` as `navigate-stop`, and its arms apply before or after it is sent. The rest of `browser.control`: `next(operation, outcome)` arms what the next admitted call of one operation does. `Fail` with `outcome: "unknown"` dispatches and then fails, so the owner fences the session and refuses every later mutation `Closed` and `undispatched`; `Fail` with `undispatched` or `rejected` never dispatches. `Hold` parks the call at a `Gate` before or after dispatch, so a test can interrupt or time out a call at a known point and then check what the owner made of it. `Disconnect` drops the connection inside the call. `calls` is the recorder: every admitted call with its operation, page, node, `dispatched` and `settled`, and never a filled value, typed text or address; `document.values` and `document.files` hold those separately. `document.replace` swaps the page's document as a navigation would, so retained nodes and pending waits go stale and a capture learns of a new document; `document.update` changes the same document in place, so controls that keep their `id` keep their identity and waits re-evaluate. `capture.emit` hands a frame to a running interval, `invoke` calls a registered binding as a page would, and `disconnect` drops every open connection outside any call. `closeChecked` returns a `ScriptedCleanupResult`, and `onCleanup` receives it, as the concrete owners do.
|
|
784
|
+
|
|
785
|
+
Time is the caller's clock. In-flight navigations, waits, holds and callback deadlines run under the context the session was opened in, so under `it.effect` from `@effect/vitest` a `TestClock.adjust` advances them and nothing real elapses, while `it.live` runs them in real time. `Testing.binding(script)` is the same engine as an opaque `BrowserBinding` for code that constructs a runtime itself; its `browsers` has one control handle for each address connected to, in first-connection order; `Testing.jpeg()` is a small valid JPEG for frames.
|
|
786
|
+
|
|
787
|
+
Randomness is scripted too. The owner's ids and handoff tokens come from `Testing.sequentialCrypto`, a `Crypto` Layer whose bytes count up from one, so `Testing.open` requires no platform service and its values repeat from run to run. A provider Layer built over `Testing.binding` still requires a `Crypto`; provide `Testing.sequentialCrypto` to keep that test free of a platform package. It is not random and computes no digest, so never give it to a real browser.
|
|
788
|
+
|
|
789
|
+
A scripted pass establishes the owner's behaviour over any engine, not what real Chromium reports for a page. `test/native/scripted-parity.test.ts` therefore runs one case list against both `Testing.open` and `Chromium.launch` on a loopback page, and every case must end in the same reason and outcome under both. Nothing scripted establishes anything about a hosted provider.
|
|
790
|
+
|
|
641
791
|
## Integration construction
|
|
642
792
|
|
|
643
|
-
`effect-browser/browser-runtime` is the supported host integration boundary. `make` validates an immutable runtime configuration and returns `acquire(policy, source, request)`. A source acquires one concrete lifetime, registers its release in the supplied Scope before returning, resolves its authorized endpoint, and supplies detailed release evidence plus `closeChecked`. Browserbase owns remote allocation/status and writer settlement; Chromium owns process termination/profile removal. The runtime supplies connection cleanup, captures bootstrap services, and creates the one registered session.
|
|
793
|
+
`effect-browser/browser-runtime` is the supported host integration boundary. `make` validates an immutable runtime configuration and returns `acquire(policy, source, request)`. It requires Effect's `Crypto` and captures it for connection ids and handoff tokens, so an integration's Layer requires `Crypto` once and `acquire` never does. A source acquires one concrete lifetime, registers its release in the supplied Scope before returning, resolves its authorized endpoint, and supplies detailed release evidence plus `closeChecked`. Browserbase owns remote allocation/status and writer settlement; Chromium owns process termination/profile removal. The runtime supplies connection cleanup, captures bootstrap services, and creates the one registered session.
|
|
644
794
|
|
|
645
795
|
The connection exposes that exact session and a bounded set of modeled integration operations. An integration can add its own resource identity and authorized file-selection or handoff methods to the same object. It must authorize stored file paths before using integration file selection; ordinary browser sessions do not expose a server-path interface. Hosted handoff pauses under the existing owner before a provider Live View is issued, and resume/reconnect remain owned state transitions.
|
|
646
796
|
|
|
@@ -658,9 +808,10 @@ The native driver, action permits, mutable capture leases and registry lookup ar
|
|
|
658
808
|
| `page-control` | Explicit host-owned page holds and receipt-based resume |
|
|
659
809
|
| `chromium` | Self-managed Chromium launch, borrowed loopback attachment and process cleanup |
|
|
660
810
|
| `browser-runtime` | Supported construction for integrations supplying browser lifetimes |
|
|
811
|
+
| `testing` | The real owner over a scripted engine: scripts, armed outcomes, a call recorder |
|
|
661
812
|
|
|
662
|
-
The root intentionally excludes the Chromium namespace. Import its entry point explicitly.
|
|
813
|
+
The root intentionally excludes the Chromium namespace. Import its entry point explicitly.
|
|
663
814
|
|
|
664
815
|
## Validation
|
|
665
816
|
|
|
666
|
-
Use the pinned workspace and Vite+ commands described in [Contributing](../../CONTRIBUTING.md). Unit tests exercise ownership, callback errors, native-call classification and bounded capture. Native Chromium tests use real loopback pages, capture and page holds,
|
|
817
|
+
Use the pinned workspace and Vite+ commands described in [Contributing](../../CONTRIBUTING.md). Unit tests exercise ownership, callback errors, native-call classification and bounded capture; `test/testing.test.ts` exercises the public scripted engine through the same owner. Native Chromium tests use real loopback pages, capture and page holds, verify that borrowed closure leaves the external browser running, and run the scripted-parity case list against both engines. The `browser` installed consumer has no Browserbase or Effect Agent package; the `agent` consumer adds only the common adapter and framework. Provider-backed native integration remains separately exercised by Browserbase consumers. None of these local checks establishes hosted-provider equivalence.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { BrowserError, BrowserOutcome, Reasons } from "./Errors.mjs";
|
|
2
|
-
import { Schema } from "effect";
|
|
2
|
+
import { Exit, Schema } from "effect";
|
|
3
3
|
//#region src/internal/browser/NativeCalls.ts
|
|
4
4
|
/**
|
|
5
5
|
* A native step failed. It names no operation: the owner stamps the one the caller asked for
|
|
@@ -29,11 +29,9 @@ const failure = (reason, outcome) => NativeFailure.make({
|
|
|
29
29
|
...outcome === void 0 ? {} : { outcome }
|
|
30
30
|
});
|
|
31
31
|
const safeDecode = (codec, raw) => {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
throw failure(Reasons.Malformed.make({}));
|
|
36
|
-
}
|
|
32
|
+
const decoded = Schema.decodeUnknownExit(codec)(raw);
|
|
33
|
+
if (Exit.isFailure(decoded)) throw failure(Reasons.Malformed.make({}));
|
|
34
|
+
return decoded.value;
|
|
37
35
|
};
|
|
38
36
|
/**
|
|
39
37
|
* No raw exception from Playwright crosses this private boundary. A ticket's own fence is
|
|
@@ -59,6 +57,12 @@ const timeout = (ticket) => {
|
|
|
59
57
|
//#endregion
|
|
60
58
|
//#region src/internal/browser/Images.ts
|
|
61
59
|
const malformed = () => failure(Reasons.Malformed.make({}));
|
|
60
|
+
/** A checked read: a truncated segment is malformed framing, never `undefined` arithmetic. */
|
|
61
|
+
const byteAt = (bytes, index) => {
|
|
62
|
+
const byte = bytes[index];
|
|
63
|
+
if (byte === void 0) throw malformed();
|
|
64
|
+
return byte;
|
|
65
|
+
};
|
|
62
66
|
/** Parse framing only; a caller decoding media must still validate its complete bitstream. */
|
|
63
67
|
const pngGeometry = (bytes) => {
|
|
64
68
|
if (bytes.length < 33 || [
|
|
@@ -91,7 +95,7 @@ const jpegGeometry = (bytes) => {
|
|
|
91
95
|
if (marker === void 0 || marker === 217 || marker === 218) break;
|
|
92
96
|
if (marker === 1 || marker >= 208 && marker <= 215) continue;
|
|
93
97
|
if (offset + 1 >= bytes.length) throw malformed();
|
|
94
|
-
const size = bytes
|
|
98
|
+
const size = byteAt(bytes, offset) * 256 + byteAt(bytes, offset + 1);
|
|
95
99
|
if (size < 2 || offset + size > bytes.length) throw malformed();
|
|
96
100
|
if ([
|
|
97
101
|
192,
|
|
@@ -109,8 +113,8 @@ const jpegGeometry = (bytes) => {
|
|
|
109
113
|
207
|
|
110
114
|
].includes(marker)) {
|
|
111
115
|
if (size < 8) throw malformed();
|
|
112
|
-
const height = bytes
|
|
113
|
-
const width = bytes
|
|
116
|
+
const height = byteAt(bytes, offset + 3) * 256 + byteAt(bytes, offset + 4);
|
|
117
|
+
const width = byteAt(bytes, offset + 5) * 256 + byteAt(bytes, offset + 6);
|
|
114
118
|
if (width === 0 || height === 0) throw malformed();
|
|
115
119
|
return {
|
|
116
120
|
width,
|
|
@@ -131,4 +135,4 @@ const captureParent = (session) => parents.get(session);
|
|
|
131
135
|
//#endregion
|
|
132
136
|
export { NativeFailure as a, publicError as c, timeout as d, pngGeometry as i, safeDecode as l, captureParent as n, closeWithin as o, jpegGeometry as r, failure as s, associate as t, sanitize as u };
|
|
133
137
|
|
|
134
|
-
//# sourceMappingURL=Association-
|
|
138
|
+
//# sourceMappingURL=Association-BsSfBYVx.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Association-BsSfBYVx.mjs","names":[],"sources":["../src/internal/browser/NativeCalls.ts","../src/internal/browser/Images.ts","../src/internal/browser/Association.ts"],"sourcesContent":["import { Exit, Schema } from \"effect\";\n\nimport {\n BrowserError,\n BrowserOutcome,\n Reasons,\n type BrowserOperation,\n type BrowserReason,\n} from \"../../Errors.ts\";\nimport type { ReadTicket } from \"./Owner.ts\";\n\n/**\n * A native step failed. It names no operation: the owner stamps the one the caller asked for\n * when it admits the work, so a step's own vocabulary can never become public API.\n */\nexport class NativeFailure extends Schema.TaggedError<NativeFailure>()(\"NativeFailure\", {\n reason: BrowserError.fields.reason,\n // Some native helpers run both before and after dispatch. Only their owner has that evidence.\n outcome: Schema.optionalKey(BrowserOutcome),\n}) {}\n\n/**\n * The only way a native failure becomes public. A fenced ticket already speaks publicly and\n * passes through with the operation the owner gave it; a native failure keeps its reason and\n * takes the caller's operation; anything else is the caller's fallback.\n */\nexport const publicError = (\n error: unknown,\n operation: BrowserOperation,\n fallback: Pick<BrowserError, \"reason\" | \"outcome\">,\n): BrowserError => {\n if (Schema.is(BrowserError)(error)) return error;\n const known = Schema.is(NativeFailure)(error) ? error : fallback;\n\n return BrowserError.make({\n operation,\n reason: known.reason,\n outcome: known.outcome ?? fallback.outcome,\n });\n};\n\n/** Shared by the Playwright driver's seams, so every native call fails in the same vocabulary. */\nexport const failure = (reason: BrowserReason, outcome?: BrowserOutcome) =>\n NativeFailure.make({ reason, ...(outcome === undefined ? {} : { outcome }) });\n\nexport const safeDecode = <A>(codec: Schema.Codec<A, unknown, never, never>, raw: unknown): A => {\n const decoded = Schema.decodeUnknownExit(codec)(raw);\n\n // A schema issue and a throw while reading the reply are both a malformed reply.\n if (Exit.isFailure(decoded)) throw failure(Reasons.Malformed.make({}));\n\n return decoded.value;\n};\n\n/**\n * No raw exception from Playwright crosses this private boundary. A ticket's own fence is\n * already a public error and passes through with the operation the owner gave it.\n */\nexport const sanitize = <A>(action: () => Promise<A>): Promise<A> =>\n Promise.resolve()\n .then(action)\n .catch((error: unknown) => {\n throw Schema.is(BrowserError)(error) || Schema.is(NativeFailure)(error)\n ? error\n : failure(Reasons.Provider.make({}));\n });\n\nexport const closeWithin = async (\n action: () => Promise<unknown>,\n milliseconds = 2000,\n): Promise<void> => {\n let timer: ReturnType<typeof setTimeout> | undefined;\n\n try {\n await Promise.race([\n Promise.resolve().then(action),\n new Promise<never>((_, reject) => {\n timer = setTimeout(() => reject(failure(Reasons.Timeout.make({}))), milliseconds);\n }),\n ]);\n } finally {\n clearTimeout(timer);\n }\n};\n\n// The native timeout is finite as well as Effect's authoritative deadline. It cannot undo dispatch.\nexport const timeout = (ticket: ReadTicket) => {\n ticket.check();\n\n return ticket.remainingMillis();\n};\n","import { Reasons } from \"../../Errors.ts\";\nimport { failure } from \"./NativeCalls.ts\";\n\nconst malformed = () => failure(Reasons.Malformed.make({}));\n\n/** A checked read: a truncated segment is malformed framing, never `undefined` arithmetic. */\nconst byteAt = (bytes: Uint8Array, index: number): number => {\n const byte = bytes[index];\n\n if (byte === undefined) throw malformed();\n\n return byte;\n};\n\n/** Parse framing only; a caller decoding media must still validate its complete bitstream. */\nexport const pngGeometry = (bytes: Uint8Array): { width: number; height: number } => {\n const signature = [137, 80, 78, 71, 13, 10, 26, 10];\n\n if (\n bytes.length < 33 ||\n signature.some((b, i) => bytes[i] !== b) ||\n bytes[12] !== 73 ||\n bytes[13] !== 72 ||\n bytes[14] !== 68 ||\n bytes[15] !== 82\n )\n throw malformed();\n const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n\n if (view.getUint32(8) !== 13) throw malformed();\n\n const width = view.getUint32(16),\n height = view.getUint32(20);\n\n if (width === 0 || height === 0) throw malformed();\n\n return { width, height };\n};\n\n/** JPEG SOF dimensions avoid trusting the requested size or CSS viewport dimensions. */\nexport const jpegGeometry = (bytes: Uint8Array): { width: number; height: number } => {\n if (bytes.length < 4 || bytes[0] !== 255 || bytes[1] !== 216) throw malformed();\n let offset = 2;\n\n while (offset + 3 < bytes.length) {\n if (bytes[offset++] !== 255) throw malformed();\n while (bytes[offset] === 255) offset++;\n const marker = bytes[offset++];\n\n if (marker === undefined || marker === 217 || marker === 218) break;\n if (marker === 1 || (marker >= 208 && marker <= 215)) continue;\n if (offset + 1 >= bytes.length) throw malformed();\n const size = byteAt(bytes, offset) * 256 + byteAt(bytes, offset + 1);\n\n if (size < 2 || offset + size > bytes.length) throw malformed();\n if ([192, 193, 194, 195, 197, 198, 199, 201, 202, 203, 205, 206, 207].includes(marker)) {\n if (size < 8) throw malformed();\n const height = byteAt(bytes, offset + 3) * 256 + byteAt(bytes, offset + 4);\n const width = byteAt(bytes, offset + 5) * 256 + byteAt(bytes, offset + 6);\n\n if (width === 0 || height === 0) throw malformed();\n\n return { width, height };\n }\n offset += size;\n }\n throw malformed();\n};\n","import type { Effect } from \"effect\";\n\nimport type { PageInfo, Target } from \"../../BrowserData.ts\";\nimport type { BrowserError } from \"../../Errors.ts\";\nimport type { CaptureSource } from \"./Driver.ts\";\nimport type { Owner, Ticket } from \"./Owner.ts\";\n\nexport interface CaptureLease {\n readonly stop: Effect.Effect<void>;\n readonly invalidate: (reason: string) => void;\n readonly reservedBytes: number;\n}\n\nexport interface CaptureResolution {\n /** Stable native page identity used to quarantine an unconfirmed screencast across reconnects. */\n readonly key: string;\n readonly target: Target;\n readonly source: CaptureSource;\n}\n\nexport interface CaptureParent {\n readonly owner: Owner;\n readonly resolve: (\n ticket: Ticket,\n target?: PageInfo,\n ) => Effect.Effect<CaptureResolution, BrowserError>;\n readonly target: () => Target;\n readonly captureLeases: Map<string, CaptureLease>;\n captureReservedBytes: number;\n}\n\n// This is a private capability registry, not stored domain data. Exact live session identity is\n// required: spreading/cloning a session must not copy authority to mutate its capture owner,\n// leases or reservations. A public/enumerable parent field would weaken that boundary.\n// Keep this access centralized here; schemas describe data, never this mutable ownership state.\nconst parents = new WeakMap<object, CaptureParent>();\n\nexport const associate = (session: object, parent: CaptureParent): void => {\n parents.set(session, parent);\n};\n\nexport const captureParent = (session: object): CaptureParent | undefined => parents.get(session);\n"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Identifier } from "./BrowserData.mjs";
|
|
2
1
|
import { BrowserError, InitializationError, Reasons } from "./Errors.mjs";
|
|
3
|
-
import {
|
|
2
|
+
import { Identifier } from "./BrowserData.mjs";
|
|
3
|
+
import { Effect, Result, Schema } from "effect";
|
|
4
4
|
//#region src/internal/browser/BindingRegistration.ts
|
|
5
5
|
const Invoke = Symbol("BrowserBindingRegistration");
|
|
6
6
|
const issued = /* @__PURE__ */ new WeakSet();
|
|
@@ -48,7 +48,7 @@ const issue = (metadata, invoke) => {
|
|
|
48
48
|
/** Pack heterogeneous codecs and handlers while their concrete input/output types are known. */
|
|
49
49
|
const make = (options) => {
|
|
50
50
|
const { maxConcurrent = 1, maxInputBytes = 65536, maxOutputBytes = 65536, timeoutMillis = 1e4, failureMode = "reject-call" } = options;
|
|
51
|
-
const
|
|
51
|
+
const decoded = Schema.decodeResult(metadataSchema)({
|
|
52
52
|
...options,
|
|
53
53
|
maxConcurrent,
|
|
54
54
|
maxInputBytes,
|
|
@@ -56,6 +56,8 @@ const make = (options) => {
|
|
|
56
56
|
timeoutMillis,
|
|
57
57
|
failureMode
|
|
58
58
|
});
|
|
59
|
+
if (Result.isFailure(decoded)) throw decoded.failure;
|
|
60
|
+
const metadata = decoded.success;
|
|
59
61
|
const { input, output, handle } = options;
|
|
60
62
|
if (!Schema.isSchema(input) || !Schema.isSchema(output) || typeof handle !== "function") throw new TypeError("Invalid binding registration");
|
|
61
63
|
const fail = (reason) => InitializationError.make({
|
|
@@ -184,4 +186,4 @@ function combine(...plans) {
|
|
|
184
186
|
//#endregion
|
|
185
187
|
export { PermissionGrant as a, binding as c, init as d, permissions as f, Permission as i, combine as l, snapshot as m, InitScript as n, Plan as o, invoke as p, Origin as r, Readiness as s, BindingFailureMode as t, empty as u };
|
|
186
188
|
|
|
187
|
-
//# sourceMappingURL=Bootstrap-
|
|
189
|
+
//# sourceMappingURL=Bootstrap-CeQOWBy6.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Bootstrap-CeQOWBy6.mjs","names":["Origin","Registration.Origin","Registration.FailureMode","Registration.schema","Registration.make"],"sources":["../src/internal/browser/BindingRegistration.ts","../src/Bootstrap.ts"],"sourcesContent":["import { Effect, Result, Schema } from \"effect\";\n\nimport type * as Bootstrap from \"../../Bootstrap.ts\";\nimport { Identifier } from \"../../BrowserData.ts\";\nimport { BrowserError, InitializationError, Reasons } from \"../../Errors.ts\";\n\nconst Invoke = Symbol(\"BrowserBindingRegistration\");\nconst issued = new WeakSet<object>();\n\nexport const Origin = Schema.NonEmptyString.check(\n Schema.isMaxLength(2048),\n Schema.makeFilter(\n (value) => {\n try {\n const url = new URL(value);\n\n return (\n [\"http:\", \"https:\"].includes(url.protocol) &&\n url.origin === value &&\n !url.username &&\n !url.password\n );\n } catch {\n return false;\n }\n },\n { title: \"an exact http(s) origin\" },\n ),\n);\n\nexport const FailureMode = Schema.Literals([\"reject-call\", \"fail-session\"]);\n\nconst metadataSchema = Schema.Struct({\n name: Identifier,\n origins: Schema.Array(Origin).check(\n Schema.isMaxLength(16),\n Schema.isUnique(),\n Schema.makeFilter((origins) => origins.length > 0, { title: \"at least one allowed origin\" }),\n ),\n maxConcurrent: Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 64 })),\n maxInputBytes: Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 1024 * 1024 })),\n maxOutputBytes: Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 4 * 1024 * 1024 })),\n timeoutMillis: Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 120_000 })),\n failureMode: FailureMode,\n});\n\ntype Metadata = typeof metadataSchema.Type;\n\nexport interface Registration<E, R> extends Metadata {\n readonly [Invoke]: (\n text: string,\n check: Effect.Effect<void, InitializationError>,\n ) => Effect.Effect<string, E | InitializationError, R>;\n}\n\n/** Retain an issued live capability at schema boundaries; a copied shape is not authority. */\nexport const schema = Schema.declare<Registration<unknown, unknown>>(\n (value): value is Registration<unknown, unknown> =>\n typeof value === \"object\" && value !== null && issued.has(value),\n { title: \"an issued browser binding registration\" },\n);\n\nconst issue = <E, R>(\n metadata: Metadata,\n invoke: Registration<E, R>[typeof Invoke],\n): Registration<E, R> => {\n const registration = Object.freeze({\n ...metadata,\n origins: Object.freeze([...metadata.origins]),\n [Invoke]: invoke,\n });\n\n issued.add(registration);\n\n return registration;\n};\n\n/** Pack heterogeneous codecs and handlers while their concrete input/output types are known. */\nexport const make = <I, IEncoded, O, OEncoded, E, R>(\n options: Bootstrap.BindingOptions<I, IEncoded, O, OEncoded, E, R>,\n): Registration<E, R> => {\n const {\n maxConcurrent = 1,\n maxInputBytes = 65536,\n maxOutputBytes = 65536,\n timeoutMillis = 10000,\n failureMode = \"reject-call\",\n } = options;\n\n const decoded = Schema.decodeResult(metadataSchema)({\n ...options,\n maxConcurrent,\n maxInputBytes,\n maxOutputBytes,\n timeoutMillis,\n failureMode,\n });\n\n if (Result.isFailure(decoded)) throw decoded.failure;\n const metadata = decoded.success;\n\n const { input, output, handle } = options;\n\n if (!Schema.isSchema(input) || !Schema.isSchema(output) || typeof handle !== \"function\") {\n throw new TypeError(\"Invalid binding registration\");\n }\n\n const fail = (reason: \"input\" | \"output\") =>\n InitializationError.make({ operation: \"callback\", step: metadata.name, reason });\n\n return issue(\n metadata,\n Effect.fnUntraced(function* (text, check) {\n if (new TextEncoder().encode(text).length > metadata.maxInputBytes)\n return yield* fail(\"input\");\n\n const decoded = yield* Schema.decodeEffect(Schema.fromJsonString(input))(text).pipe(\n Effect.catchCause(() => Effect.fail(fail(\"input\"))),\n );\n\n yield* check;\n const value = yield* Effect.suspend(() => handle(decoded));\n\n const encoded = yield* Schema.encodeEffect(output)(value).pipe(\n Effect.catchCause(() => Effect.fail(fail(\"output\"))),\n );\n\n const json = yield* Schema.decodeUnknownEffect(Schema.Json)(encoded).pipe(\n Effect.catchCause(() => Effect.fail(fail(\"output\"))),\n );\n\n const result = yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Json))(json).pipe(\n Effect.catchCause(() => Effect.fail(fail(\"output\"))),\n );\n\n if (new TextEncoder().encode(result).length > metadata.maxOutputBytes)\n return yield* fail(\"output\");\n\n return result;\n }),\n );\n};\n\n/** Authentication is identity-based even when a caller copies every enumerable field/symbol. */\nexport const snapshot = Effect.fnUntraced(function* <E, R>(\n registration: Registration<E, R>,\n): Effect.fn.Return<Registration<E, R>, BrowserError> {\n const invalid = () =>\n BrowserError.make({\n operation: \"configure\",\n reason: Reasons.Configuration.make({}),\n outcome: \"undispatched\",\n });\n\n if (!issued.has(registration)) return yield* invalid();\n\n const metadata = yield* Schema.decodeEffect(metadataSchema)(registration).pipe(\n Effect.mapError(invalid),\n );\n\n return issue(metadata, registration[Invoke]);\n});\n\nexport const invoke = <E, R>(\n registration: Registration<E, R>,\n text: string,\n check: Effect.Effect<void, InitializationError>,\n): Effect.Effect<string, E | InitializationError, R> => registration[Invoke](text, check);\n","import { type Cause, type Effect, Schema } from \"effect\";\n\nimport { Identifier } from \"./BrowserData.ts\";\nimport type { InitializationError } from \"./Errors.ts\";\nimport * as Registration from \"./internal/browser/BindingRegistration.ts\";\n\n/**\n * Exact scheme-and-host origin, as a document reports it. Patterns are deliberately absent:\n * a registration that cannot say exactly where it applies does not belong in a plan.\n */\nexport const Origin = Registration.Origin;\n\nexport type Origin = typeof Origin.Type;\n\n/**\n * The reviewed subset of native permission capabilities. Each name is checked against a real\n * browser in native acceptance; an unsupported capability is refused before it is exposed.\n */\nexport const Permission = Schema.Literals([\n \"accelerometer\",\n \"background-sync\",\n \"camera\",\n \"clipboard-read\",\n \"clipboard-write\",\n \"geolocation\",\n \"gyroscope\",\n \"magnetometer\",\n \"microphone\",\n \"midi\",\n \"notifications\",\n \"payment-handler\",\n \"storage-access\",\n]);\n\nexport type Permission = typeof Permission.Type;\n\nexport const PermissionGrant = Schema.Struct({\n origin: Origin,\n permissions: Schema.Array(Permission).check(Schema.isMaxLength(16), Schema.isUnique()),\n});\n\nexport type PermissionGrant = typeof PermissionGrant.Type;\n\n/**\n * Registration is not readiness. A script may start asynchronous work, but it cannot pause the\n * website's own scripts, so a document is ready only when its expression resolves to exactly\n * `true`. A document that was already running when the bundle was registered never ran it:\n * `RequireFreshNavigation` reports that instead of pretending otherwise, and an explicit\n * reload of possibly uncertain work stays a caller's decision.\n */\nexport const Readiness = Schema.Struct({\n expression: Schema.NonEmptyString.check(Schema.isMaxLength(4096)),\n timeoutMillis: Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 120_000 })),\n existingDocuments: Schema.Literals([\"RequireFreshNavigation\", \"AcceptAlreadyRunning\"]),\n});\n\nexport type Readiness = typeof Readiness.Type;\n\n/** Trusted host configuration. Script content is never model output or page input. */\nexport const InitScript = Schema.Struct({\n id: Identifier,\n content: Schema.NonEmptyString.check(Schema.isMaxLength(64 * 1024)),\n /** When present, the step runs only on these documents and readiness skips the rest. */\n origins: Schema.optionalKey(\n Schema.Array(Origin).check(Schema.isMaxLength(16), Schema.isUnique()),\n ),\n readiness: Schema.optionalKey(Readiness),\n});\n\nexport type InitScript = typeof InitScript.Type;\n\n/** A callback failure can reject one page invocation or permanently fence the session. */\nexport const BindingFailureMode = Registration.FailureMode;\n\nexport type BindingFailureMode = typeof BindingFailureMode.Type;\n\n/**\n * A registration issued by `binding`. Its private executable closure carries the real consumer\n * E/R; copying the metadata or fabricating an object does not grant callback authority.\n */\nexport type BindingRegistration<E = never, R = never> = Registration.Registration<E, R>;\n\n/**\n * Environment-free boundary codecs keep page data validation separate from consumer services.\n * The handler's E/R remain visible on the Plan and are supplied only by the trusted host.\n */\nexport interface BindingOptions<I, IEncoded, O, OEncoded, E, R> {\n readonly name: string;\n readonly origins: ReadonlyArray<Origin>;\n readonly input: Schema.ConstraintCodec<I, IEncoded, never, never>;\n readonly output: Schema.ConstraintCodec<O, OEncoded, never, never>;\n /** Defaults to one concurrent invocation. Explicit values still pass the bounded schema. */\n readonly maxConcurrent?: number;\n /** Defaults to 64 KiB. */\n readonly maxInputBytes?: number;\n /** Defaults to 64 KiB. */\n readonly maxOutputBytes?: number;\n /** Defaults to 10 seconds for the complete invocation. */\n readonly timeoutMillis?: number;\n /** Defaults to rejecting this invocation without terminating the session. */\n readonly failureMode?: BindingFailureMode;\n readonly handle: (input: I) => Effect.Effect<O, E, R>;\n}\n\n/**\n * One ordered bundle plus its host capabilities. Callback registrations preserve E/R through\n * their executable closures; static script/permission plans remain serializable values.\n */\nexport interface Plan<E = unknown, R = unknown> {\n readonly scripts: ReadonlyArray<InitScript>;\n readonly permissions: ReadonlyArray<PermissionGrant>;\n readonly bindings?: ReadonlyArray<BindingRegistration<E, R>>;\n}\n\n/** Host-only typed evidence. Causes are never logged or returned to page callers. */\nexport interface BindingFailure<E> {\n readonly name: string;\n readonly mode: BindingFailureMode;\n readonly cause: Cause.Cause<E | InitializationError>;\n}\n\nexport interface BindingStatistics {\n readonly name: string;\n readonly inFlight: number;\n readonly pendingNative: number;\n /** Finished/interrupted invocations whose native work or disposal has not settled. */\n readonly retired: number;\n readonly accepted: number;\n readonly succeeded: number;\n readonly rejected: number;\n}\n\n/** Copied snapshot: at most sixteen bindings and the latest thirty-two failure records. */\nexport interface BindingDiagnostics<E> {\n readonly faulted: boolean;\n readonly bindings: ReadonlyArray<BindingStatistics>;\n readonly failures: ReadonlyArray<BindingFailure<E>>;\n readonly droppedFailures: number;\n}\n\n/**\n * Static plans remain serializable. Live binding plans retain issued registrations by identity;\n * this schema neither executes their callbacks nor turns copied metadata into authority. Typed\n * acquisition validates a separate snapshot so consumer E/R is not erased by a generic decoder.\n */\nexport const Plan = Schema.Struct({\n scripts: Schema.Array(InitScript).check(Schema.isMaxLength(16)),\n permissions: Schema.Array(PermissionGrant).check(Schema.isMaxLength(16)),\n bindings: Schema.optionalKey(Schema.Array(Registration.schema).check(Schema.isMaxLength(16))),\n});\n\ntype AnyPlan = Plan<unknown, unknown>;\n\nexport type PlanError<P extends AnyPlan> = P extends Plan<infer E, infer _R> ? E : never;\nexport type PlanRequirements<P extends AnyPlan> = P extends Plan<infer _E, infer R> ? R : never;\n\nexport const empty: Plan<never, never> = { scripts: [], permissions: [] };\n\nexport const init = (script: InitScript): Plan<never, never> => ({\n scripts: [script],\n permissions: [],\n});\n\nexport const permissions = (grant: PermissionGrant): Plan<never, never> => ({\n scripts: [],\n permissions: [grant],\n});\n\n/**\n * Register a page-callable host callback without running it or capturing an ambient runtime.\n * Native installation later owns origin/document checks, bounded admission and disposal.\n */\nexport const binding = <I, IEncoded, O, OEncoded, E, R>(\n options: BindingOptions<I, IEncoded, O, OEncoded, E, R>,\n): Plan<E, R> => ({\n scripts: [],\n permissions: [],\n bindings: [Registration.make(options)],\n});\n\n/** Combination is ordered and preserves every callback's consumer E/R union. */\nexport function combine<const Plans extends ReadonlyArray<AnyPlan>>(\n ...plans: Plans\n): Plan<PlanError<Plans[number]>, PlanRequirements<Plans[number]>>;\n\nexport function combine(...plans: ReadonlyArray<AnyPlan>): AnyPlan {\n const bindings = plans.flatMap((plan) => (plan.bindings === undefined ? [] : [...plan.bindings]));\n\n return {\n scripts: plans.flatMap((plan) => [...plan.scripts]),\n permissions: plans.flatMap((plan) => [...plan.permissions]),\n ...(bindings.length === 0 ? {} : { bindings }),\n };\n}\n\n/** What the current document is, not a promise about any other document. */\nexport type ReadinessOutcome =\n | { readonly _tag: \"Ready\" }\n | { readonly _tag: \"NotApplicable\" }\n | { readonly _tag: \"RequiresNavigation\" };\n"],"mappings":";;;;AAMA,MAAM,SAAS,OAAO,4BAA4B;AAClD,MAAM,yBAAS,IAAI,QAAgB;AAEnC,MAAaA,WAAS,OAAO,eAAe,MAC1C,OAAO,YAAY,IAAI,GACvB,OAAO,YACJ,UAAU;CACT,IAAI;EACF,MAAM,MAAM,IAAI,IAAI,KAAK;EAEzB,OACE,CAAC,SAAS,QAAQ,CAAC,CAAC,SAAS,IAAI,QAAQ,KACzC,IAAI,WAAW,SACf,CAAC,IAAI,YACL,CAAC,IAAI;CAET,QAAQ;EACN,OAAO;CACT;AACF,GACA,EAAE,OAAO,0BAA0B,CACrC,CACF;AAEA,MAAa,cAAc,OAAO,SAAS,CAAC,eAAe,cAAc,CAAC;AAE1E,MAAM,iBAAiB,OAAO,OAAO;CACnC,MAAM;CACN,SAAS,OAAO,MAAMA,QAAM,CAAC,CAAC,MAC5B,OAAO,YAAY,EAAE,GACrB,OAAO,SAAS,GAChB,OAAO,YAAY,YAAY,QAAQ,SAAS,GAAG,EAAE,OAAO,8BAA8B,CAAC,CAC7F;CACA,eAAe,OAAO,IAAI,MAAM,OAAO,UAAU;EAAE,SAAS;EAAG,SAAS;CAAG,CAAC,CAAC;CAC7E,eAAe,OAAO,IAAI,MAAM,OAAO,UAAU;EAAE,SAAS;EAAG,SAAS;CAAY,CAAC,CAAC;CACtF,gBAAgB,OAAO,IAAI,MAAM,OAAO,UAAU;EAAE,SAAS;EAAG,SAAS;CAAgB,CAAC,CAAC;CAC3F,eAAe,OAAO,IAAI,MAAM,OAAO,UAAU;EAAE,SAAS;EAAG,SAAS;CAAQ,CAAC,CAAC;CAClF,aAAa;AACf,CAAC;;AAYD,MAAa,SAAS,OAAO,SAC1B,UACC,OAAO,UAAU,YAAY,UAAU,QAAQ,OAAO,IAAI,KAAK,GACjE,EAAE,OAAO,yCAAyC,CACpD;AAEA,MAAM,SACJ,UACA,WACuB;CACvB,MAAM,eAAe,OAAO,OAAO;EACjC,GAAG;EACH,SAAS,OAAO,OAAO,CAAC,GAAG,SAAS,OAAO,CAAC;GAC3C,SAAS;CACZ,CAAC;CAED,OAAO,IAAI,YAAY;CAEvB,OAAO;AACT;;AAGA,MAAa,QACX,YACuB;CACvB,MAAM,EACJ,gBAAgB,GAChB,gBAAgB,OAChB,iBAAiB,OACjB,gBAAgB,KAChB,cAAc,kBACZ;CAEJ,MAAM,UAAU,OAAO,aAAa,cAAc,CAAC,CAAC;EAClD,GAAG;EACH;EACA;EACA;EACA;EACA;CACF,CAAC;CAED,IAAI,OAAO,UAAU,OAAO,GAAG,MAAM,QAAQ;CAC7C,MAAM,WAAW,QAAQ;CAEzB,MAAM,EAAE,OAAO,QAAQ,WAAW;CAElC,IAAI,CAAC,OAAO,SAAS,KAAK,KAAK,CAAC,OAAO,SAAS,MAAM,KAAK,OAAO,WAAW,YAC3E,MAAM,IAAI,UAAU,8BAA8B;CAGpD,MAAM,QAAQ,WACZ,oBAAoB,KAAK;EAAE,WAAW;EAAY,MAAM,SAAS;EAAM;CAAO,CAAC;CAEjF,OAAO,MACL,UACA,OAAO,WAAW,WAAW,MAAM,OAAO;EACxC,IAAI,IAAI,YAAY,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,SAAS,SAAS,eACnD,OAAO,OAAO,KAAK,OAAO;EAE5B,MAAM,UAAU,OAAO,OAAO,aAAa,OAAO,eAAe,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,KAC7E,OAAO,iBAAiB,OAAO,KAAK,KAAK,OAAO,CAAC,CAAC,CACpD;EAEA,OAAO;EACP,MAAM,QAAQ,OAAO,OAAO,cAAc,OAAO,OAAO,CAAC;EAEzD,MAAM,UAAU,OAAO,OAAO,aAAa,MAAM,CAAC,CAAC,KAAK,CAAC,CAAC,KACxD,OAAO,iBAAiB,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CACrD;EAEA,MAAM,OAAO,OAAO,OAAO,oBAAoB,OAAO,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,KACnE,OAAO,iBAAiB,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CACrD;EAEA,MAAM,SAAS,OAAO,OAAO,aAAa,OAAO,eAAe,OAAO,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,KAClF,OAAO,iBAAiB,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CACrD;EAEA,IAAI,IAAI,YAAY,CAAC,CAAC,OAAO,MAAM,CAAC,CAAC,SAAS,SAAS,gBACrD,OAAO,OAAO,KAAK,QAAQ;EAE7B,OAAO;CACT,CAAC,CACH;AACF;;AAGA,MAAa,WAAW,OAAO,WAAW,WACxC,cACoD;CACpD,MAAM,gBACJ,aAAa,KAAK;EAChB,WAAW;EACX,QAAQ,QAAQ,cAAc,KAAK,CAAC,CAAC;EACrC,SAAS;CACX,CAAC;CAEH,IAAI,CAAC,OAAO,IAAI,YAAY,GAAG,OAAO,OAAO,QAAQ;CAErD,MAAM,WAAW,OAAO,OAAO,aAAa,cAAc,CAAC,CAAC,YAAY,CAAC,CAAC,KACxE,OAAO,SAAS,OAAO,CACzB;CAEA,OAAO,MAAM,UAAU,aAAa,OAAO;AAC7C,CAAC;AAED,MAAa,UACX,cACA,MACA,UACsD,aAAa,OAAO,CAAC,MAAM,KAAK;;;;;;;AC7JxF,MAAa,SAASC;;;;;AAQtB,MAAa,aAAa,OAAO,SAAS;CACxC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;AAID,MAAa,kBAAkB,OAAO,OAAO;CAC3C,QAAQ;CACR,aAAa,OAAO,MAAM,UAAU,CAAC,CAAC,MAAM,OAAO,YAAY,EAAE,GAAG,OAAO,SAAS,CAAC;AACvF,CAAC;;;;;;;;AAWD,MAAa,YAAY,OAAO,OAAO;CACrC,YAAY,OAAO,eAAe,MAAM,OAAO,YAAY,IAAI,CAAC;CAChE,eAAe,OAAO,IAAI,MAAM,OAAO,UAAU;EAAE,SAAS;EAAG,SAAS;CAAQ,CAAC,CAAC;CAClF,mBAAmB,OAAO,SAAS,CAAC,0BAA0B,sBAAsB,CAAC;AACvF,CAAC;;AAKD,MAAa,aAAa,OAAO,OAAO;CACtC,IAAI;CACJ,SAAS,OAAO,eAAe,MAAM,OAAO,YAAY,KAAS,CAAC;;CAElE,SAAS,OAAO,YACd,OAAO,MAAM,MAAM,CAAC,CAAC,MAAM,OAAO,YAAY,EAAE,GAAG,OAAO,SAAS,CAAC,CACtE;CACA,WAAW,OAAO,YAAY,SAAS;AACzC,CAAC;;AAKD,MAAa,qBAAqBC;;;;;;AAyElC,MAAa,OAAO,OAAO,OAAO;CAChC,SAAS,OAAO,MAAM,UAAU,CAAC,CAAC,MAAM,OAAO,YAAY,EAAE,CAAC;CAC9D,aAAa,OAAO,MAAM,eAAe,CAAC,CAAC,MAAM,OAAO,YAAY,EAAE,CAAC;CACvE,UAAU,OAAO,YAAY,OAAO,MAAMC,MAAmB,CAAC,CAAC,MAAM,OAAO,YAAY,EAAE,CAAC,CAAC;AAC9F,CAAC;AAOD,MAAa,QAA4B;CAAE,SAAS,CAAC;CAAG,aAAa,CAAC;AAAE;AAExE,MAAa,QAAQ,YAA4C;CAC/D,SAAS,CAAC,MAAM;CAChB,aAAa,CAAC;AAChB;AAEA,MAAa,eAAe,WAAgD;CAC1E,SAAS,CAAC;CACV,aAAa,CAAC,KAAK;AACrB;;;;;AAMA,MAAa,WACX,aACgB;CAChB,SAAS,CAAC;CACV,aAAa,CAAC;CACd,UAAU,CAACC,KAAkB,OAAO,CAAC;AACvC;AAOA,SAAgB,QAAQ,GAAG,OAAwC;CACjE,MAAM,WAAW,MAAM,SAAS,SAAU,KAAK,aAAa,KAAA,IAAY,CAAC,IAAI,CAAC,GAAG,KAAK,QAAQ,CAAE;CAEhG,OAAO;EACL,SAAS,MAAM,SAAS,SAAS,CAAC,GAAG,KAAK,OAAO,CAAC;EAClD,aAAa,MAAM,SAAS,SAAS,CAAC,GAAG,KAAK,WAAW,CAAC;EAC1D,GAAI,SAAS,WAAW,IAAI,CAAC,IAAI,EAAE,SAAS;CAC9C;AACF"}
|
package/dist/Bootstrap.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as PermissionGrant, c as binding, d as init, f as permissions, i as Permission, l as combine, n as InitScript, o as Plan, r as Origin, s as Readiness, t as BindingFailureMode, u as empty } from "./Bootstrap-
|
|
1
|
+
import { a as PermissionGrant, c as binding, d as init, f as permissions, i as Permission, l as combine, n as InitScript, o as Plan, r as Origin, s as Readiness, t as BindingFailureMode, u as empty } from "./Bootstrap-CeQOWBy6.mjs";
|
|
2
2
|
export { BindingFailureMode, InitScript, Origin, Permission, PermissionGrant, Plan, Readiness, binding, combine, empty, init, permissions };
|
package/dist/Browser.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { f as Plan, g as ReadinessOutcome, s as Bootstrap_d_exports, t as BindingDiagnostics } from "./Bootstrap-B9UUXsvF.mjs";
|
|
2
2
|
import { BrowserError, InitializationError } from "./Errors.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import { $ as TypeRequest, D as NavigationResult, E as NavigateRequest, F as PointerMoveRequest, H as ScreenshotResult, J as SessionStatus, L as PressRequest, N as PageInfo, O as Observation, Q as TextResult, R as ReadTextRequest, S as InputReceipt, U as ScrollRequest, V as ScreenshotRequest, W as SelectOptions, X as Target, Y as StartNavigationRequest, _ as FrameInfo, at as WheelRequest, c as ClickRequest, d as FillFormRequest, et as Viewport, f as FillFormResult, i as BrowserDiagnostics, it as WaitForElementRequest, j as ObservedElement, k as ObservationOptions, l as ControlFacts, o as Checkpoint, p as FillRequest, s as CheckpointOptions, t as ActionResult, u as FillFormOptions, v as HoverRequest, w as KeyStroke } from "./BrowserData-D_zsf79q.mjs";
|
|
4
4
|
import { Effect, Scope } from "effect";
|
|
5
5
|
//#region src/Browser.d.ts
|
|
6
6
|
/**
|
|
@@ -124,6 +124,18 @@ export interface BrowserSession<E = never> extends TargetOperations {
|
|
|
124
124
|
readonly pressElement: (reference: ObservedElement, stroke: KeyStroke, admission?: ElementAdmission) => Effect.Effect<InputReceipt, BrowserError>;
|
|
125
125
|
/** Real typing with `fillElement`'s exactness: that node must already have focus. */
|
|
126
126
|
readonly typeElement: (reference: ObservedElement, text: string, admission?: ElementAdmission) => Effect.Effect<InputReceipt, BrowserError>;
|
|
127
|
+
/**
|
|
128
|
+
* Set several controls of one observation in order, then optionally click one submit control.
|
|
129
|
+
* Every step acts on the exact observed node after the same fresh checks as `fillElement`,
|
|
130
|
+
* except that a control may have become enabled since it was observed, and each is admitted
|
|
131
|
+
* and charged as its own action. The observation stays usable for this form's own steps only;
|
|
132
|
+
* anything else that changes the page still retires it, and the form retires it when it ends.
|
|
133
|
+
* The first refused or uncertain step ends the form. Submit is sent only after every field
|
|
134
|
+
* succeeded and, unless `verify` is false, still holds what its step left there. Fields set
|
|
135
|
+
* before a stop stay set. It fails only when its first step does; otherwise `stopped` says
|
|
136
|
+
* where it ended and why.
|
|
137
|
+
*/
|
|
138
|
+
readonly fillForm: (request: FillFormRequest, admission?: ElementAdmission, options?: FillFormOptions) => Effect.Effect<FillFormResult, BrowserError>;
|
|
127
139
|
readonly pages: Effect.Effect<ReadonlyArray<PageInfo>, BrowserError>;
|
|
128
140
|
readonly frames: Effect.Effect<ReadonlyArray<FrameInfo>, BrowserError>;
|
|
129
141
|
/** List frames on one exact page without selecting it. */
|
package/dist/Browser.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Browser.mjs","names":[],"sources":["../src/Browser.ts"],"sourcesContent":["import { Effect, type Scope } from \"effect\";\nimport { dual } from \"effect/Function\";\n\nimport type * as Bootstrap from \"./Bootstrap.ts\";\nimport type {\n BrowserDiagnostics,\n Checkpoint,\n CheckpointOptions,\n FillRequest,\n HoverRequest,\n InputReceipt,\n KeyStroke,\n NavigateRequest,\n NavigationResult,\n ObservationOptions,\n ObservedElement,\n PointerMoveRequest,\n PressRequest,\n ReadTextRequest,\n ScreenshotRequest,\n ScreenshotResult,\n ScrollRequest,\n SelectOptions,\n SessionStatus,\n StartNavigationRequest,\n TextResult,\n TypeRequest,\n WaitForElementRequest,\n WheelRequest,\n ControlFacts,\n ActionResult,\n ClickRequest,\n FrameInfo,\n Observation,\n PageInfo,\n Target,\n Viewport,\n} from \"./BrowserData.ts\";\nimport type { BrowserError, InitializationError } from \"./Errors.ts\";\n\n/**\n * A host's decision about one control, made on facts read from the exact node immediately\n * before input. Returning anything but `true`, or throwing, sends nothing and fails `denied`.\n * It is a plain synchronous function on purpose: it runs while the owner's permit is held, where\n * waiting on a model or a network call would stall every other operation. It is not an atomic\n * check-and-input transaction, because page script can still run before the native input lands.\n */\nexport interface ElementAdmission {\n readonly admit: (facts: ControlFacts) => boolean;\n}\n\n/**\n * A navigation the browser is still performing. The owner's permit was released when it was\n * dispatched, so while it loads a host may read, `checkpoint`, hold and resume this page, and\n * use any other page. Anything that would change this page fails `busy` until it settles.\n *\n * Leaving its scope unsettled fences the session, exactly as an interrupted mutation does,\n * because nothing then knows what the browser did. It is never replayed.\n */\nexport interface NavigationOperation {\n /** What was navigated, read before dispatch. */\n readonly target: Target;\n /**\n * The document reached DOMContentLoaded. It belongs to this one navigation: a successor that\n * reaches the same URL fails it instead. Interrupting a waiter stops nothing in the browser.\n */\n readonly completed: Effect.Effect<NavigationResult, BrowserError>;\n /**\n * Asks the browser to stop loading and waits for this navigation to settle, after which\n * `completed` fails `interrupted`. Success is a known outcome and the session stays usable:\n * the page holds whatever had loaded. It does not undo anything the page already did.\n * An already-completed navigation cannot stop a successor. Concurrent callers share their\n * active attempt. Before dispatch, cancellation or a busy refusal permits a later request\n * once native setup has retired. A dispatched attempt's outcome, including failure or\n * interruption, is retained permanently; an uncertain stop is never replayed.\n */\n readonly stop: Effect.Effect<void, BrowserError>;\n}\n\n/** Common operations. Direct, retained and pinned views choose their target at different times. */\nexport interface TargetOperations {\n readonly navigate: (request: NavigateRequest) => Effect.Effect<NavigationResult, BrowserError>;\n /** `navigate`, left in flight: the same single dispatch, completed by the caller. */\n readonly startNavigation: (\n request: StartNavigationRequest,\n ) => Effect.Effect<NavigationOperation, BrowserError, Scope.Scope>;\n readonly readText: (request: ReadTextRequest) => Effect.Effect<TextResult, BrowserError>;\n readonly click: (request: ClickRequest) => Effect.Effect<ActionResult, BrowserError>;\n readonly fill: (request: FillRequest) => Effect.Effect<ActionResult, BrowserError>;\n /** Script in the page: instantaneous, and it raises no wheel event. */\n readonly scroll: (request: ScrollRequest) => Effect.Effect<ActionResult, BrowserError>;\n /** One real pointer move, in main-frame viewport pixels. */\n readonly pointerMove: (request: PointerMoveRequest) => Effect.Effect<InputReceipt, BrowserError>;\n /** Places the pointer on one exact element where it is, or fails `not-visible` unsent. */\n readonly hover: (request: HoverRequest) => Effect.Effect<InputReceipt, BrowserError>;\n /** One real wheel event; the browser decides what under the pointer scrolls. */\n readonly wheel: (request: WheelRequest) => Effect.Effect<InputReceipt, BrowserError>;\n /** One real key stroke to whatever has focus, or fails `not-focused` unsent if `into` lacks it. */\n readonly press: (request: PressRequest) => Effect.Effect<InputReceipt, BrowserError>;\n /** Text as the real key strokes that produce it, under the same focus rule as `press`. */\n readonly type: (request: TypeRequest) => Effect.Effect<InputReceipt, BrowserError>;\n readonly screenshot: (\n request: ScreenshotRequest,\n ) => Effect.Effect<ScreenshotResult, BrowserError>;\n}\n\n/**\n * One explicit page/frame target. Selection may move independently; each operation re-resolves\n * this identity inside the same owner and connection. Closing/detaching it or reconnecting makes\n * the handle stale before dispatch.\n */\nexport interface PinnedTarget extends TargetOperations {\n readonly target: Target;\n}\n\n/** A checked selection retained at acquisition; moving selection away and back makes it stale. */\nexport interface RetainedTarget extends TargetOperations {}\n\n/**\n * Host control over one owned browser. This is not a serializable model value: copying a\n * session object cannot copy its capture, page-control or connection authority.\n *\n * The inherited target operations resolve the selected page/frame when their Effect executes.\n * Use `retain` to retain the current selection with stale-on-selection-change semantics, or\n * `pinPage` / `pinFrame` when work must stay on an explicit target while selection moves.\n */\nexport interface BrowserSession<E = never> extends TargetOperations {\n /** The implementation which owns this live connection. */\n readonly implementation: string;\n /** Copied host-only state, readable without admission in every lifecycle phase. */\n readonly status: Effect.Effect<SessionStatus>;\n /** Bounded native/policy diagnostics. Typed callback causes remain in bindingDiagnostics. */\n readonly diagnostics: Effect.Effect<BrowserDiagnostics>;\n /** Close this scope and require its own ownership-specific cleanup evidence. */\n readonly closeChecked: Effect.Effect<void, BrowserError>;\n /** First fail-session callback cause, preserving the consumer's error type on the host. */\n readonly failure: Effect.Effect<never, E | InitializationError>;\n /** Bounded host-only evidence; consumer causes are never projected into a page reply. */\n readonly bindingDiagnostics: Effect.Effect<Bootstrap.BindingDiagnostics<E>>;\n /** Resolve, validate and retain the selection under owner admission. */\n readonly retain: Effect.Effect<RetainedTarget, BrowserError>;\n readonly target: Effect.Effect<Target, BrowserError>;\n /**\n * The one observation whose nodes later actions may name. `scope: \"viewport\"` keeps only text\n * and controls that are on screen and reachable, plus bounded choices of visible native\n * selects; the default reads the whole document. It carries no destination, form or field value.\n */\n readonly observe: (options?: ObservationOptions) => Effect.Effect<Observation, BrowserError>;\n /**\n * Passive evidence for a recorder, with a picture when asked. It issues no references and\n * leaves the observation above exactly as it was. Host-only: it carries control facts.\n */\n readonly checkpoint: (options?: CheckpointOptions) => Effect.Effect<Checkpoint, BrowserError>;\n /** Host-only facts about one observed control, read from that exact node just now. */\n readonly controlFacts: (reference: ObservedElement) => Effect.Effect<ControlFacts, BrowserError>;\n /**\n * After a page hold, nothing observed on that page may be acted on unchecked. This checks one\n * reference: still attached, and still the control that was inspected. It never searches by\n * selector or label for a substitute, and it sends nothing.\n */\n readonly revalidateElement: (\n reference: ObservedElement,\n ) => Effect.Effect<ObservedElement, BrowserError>;\n readonly clickElement: (\n reference: ObservedElement,\n admission?: ElementAdmission,\n ) => Effect.Effect<ActionResult, BrowserError>;\n readonly fillElement: (\n reference: ObservedElement,\n value: string,\n admission?: ElementAdmission,\n ) => Effect.Effect<ActionResult, BrowserError>;\n /** One selection using this exact select and its issued option element IDs, with fresh checks. */\n readonly selectOption: (\n reference: ObservedElement,\n options: SelectOptions,\n admission?: ElementAdmission,\n ) => Effect.Effect<ActionResult, BrowserError>;\n readonly hoverElement: (\n reference: ObservedElement,\n admission?: ElementAdmission,\n ) => Effect.Effect<InputReceipt, BrowserError>;\n /** A key stroke sent only if the exact node an observation named already has focus. */\n readonly pressElement: (\n reference: ObservedElement,\n stroke: KeyStroke,\n admission?: ElementAdmission,\n ) => Effect.Effect<InputReceipt, BrowserError>;\n /** Real typing with `fillElement`'s exactness: that node must already have focus. */\n readonly typeElement: (\n reference: ObservedElement,\n text: string,\n admission?: ElementAdmission,\n ) => Effect.Effect<InputReceipt, BrowserError>;\n readonly pages: Effect.Effect<ReadonlyArray<PageInfo>, BrowserError>;\n readonly frames: Effect.Effect<ReadonlyArray<FrameInfo>, BrowserError>;\n /** List frames on one exact page without selecting it. */\n readonly framesOf: (page: PageInfo) => Effect.Effect<ReadonlyArray<FrameInfo>, BrowserError>;\n /** Pin the page's main frame without changing the session selection. */\n readonly pinPage: (page: PageInfo) => Effect.Effect<PinnedTarget, BrowserError>;\n /** Pin one frame that currently belongs to the exact page, without changing selection. */\n readonly pinFrame: (\n page: PageInfo,\n frame: FrameInfo,\n ) => Effect.Effect<PinnedTarget, BrowserError>;\n readonly selectPage: (page: PageInfo) => Effect.Effect<void, BrowserError>;\n readonly selectFrame: (frameId: string) => Effect.Effect<void, BrowserError>;\n /** Create a tab without selecting it and return that exact tab's checked identity. */\n readonly createPage: Effect.Effect<PageInfo, BrowserError>;\n readonly closePage: (page: PageInfo) => Effect.Effect<void, BrowserError>;\n readonly resizeViewport: (viewport: Viewport) => Effect.Effect<void, BrowserError>;\n /**\n * Wait on the original observed node within the host deadline. Hidden includes its detachment;\n * document replacement is stale. Success observes a condition, without authorizing later input.\n */\n readonly waitForElement: (request: WaitForElementRequest) => Effect.Effect<void, BrowserError>;\n readonly waitFor: (request: {\n readonly selector: string;\n readonly state: \"visible\" | \"hidden\" | \"attached\" | \"detached\";\n }) => Effect.Effect<void, BrowserError>;\n /** Navigation observation is registered before the single click dispatch. */\n readonly clickAndWait: (request: ClickRequest) => Effect.Effect<ActionResult, BrowserError>;\n /**\n * Readiness of the current document only. Dependent operations wait for it themselves;\n * this reports it without charging an action, so a caller can decide what to do about a\n * document that predates the registrations.\n */\n readonly ready: Effect.Effect<Bootstrap.ReadinessOutcome, InitializationError>;\n}\n\n/** Helpers that do not supervise callback failures accept any live browser session. */\nexport type AnySession = BrowserSession<unknown>;\n\n/** Dependencies are captured at acquisition, not erased into an environment-free service Layer. */\nexport interface OpenOptions<E = never, R = never> {\n readonly bootstrap?: Bootstrap.Plan<E, R>;\n}\n\n/**\n * Acquire, supervise and close one browser in a fresh scope. The callback keeps the concrete\n * Chromium or provider session type. Its scoped work is joined before checked browser cleanup,\n * and cleanup failures remain typed even when the callback has already failed or was cancelled.\n * Acquisition is evaluated once; an uncertain result is never retried.\n */\nexport const scoped: {\n <S extends BrowserSession<unknown>, A, E2, R2>(\n f: (session: S) => Effect.Effect<A, E2, R2>,\n ): <E, AE, AR>(\n open: Effect.Effect<S & BrowserSession<E>, AE, AR>,\n ) => Effect.Effect<\n A,\n AE | E | E2 | InitializationError | BrowserError,\n Exclude<AR | R2, Scope.Scope>\n >;\n <S, E, AE, AR, A, E2, R2>(\n open: Effect.Effect<S & BrowserSession<E>, AE, AR>,\n f: (session: S & BrowserSession<E>) => Effect.Effect<A, E2, R2>,\n ): Effect.Effect<\n A,\n AE | E | E2 | InitializationError | BrowserError,\n Exclude<AR | R2, Scope.Scope>\n >;\n} = dual(\n 2,\n <S, E, AE, AR, A, E2, R2>(\n open: Effect.Effect<S & BrowserSession<E>, AE, AR>,\n f: (session: S & BrowserSession<E>) => Effect.Effect<A, E2, R2>,\n ) =>\n Effect.scoped(\n Effect.flatMap(open, (session) =>\n Effect.scoped(\n Effect.raceFirst(\n session.failure,\n Effect.suspend(() => f(session)),\n ),\n ).pipe(Effect.onExit(() => session.closeChecked)),\n ),\n ),\n);\n"],"mappings":";;;;;;;;;AAoPA,MAAa,SAkBT,KACF,IAEE,MACA,MAEA,OAAO,OACL,OAAO,QAAQ,OAAO,YACpB,OAAO,OACL,OAAO,UACL,QAAQ,SACR,OAAO,cAAc,EAAE,OAAO,CAAC,CACjC,CACF,CAAC,CAAC,KAAK,OAAO,aAAa,QAAQ,YAAY,CAAC,CAClD,CACF,CACJ"}
|
|
1
|
+
{"version":3,"file":"Browser.mjs","names":[],"sources":["../src/Browser.ts"],"sourcesContent":["import { Effect, type Scope } from \"effect\";\nimport { dual } from \"effect/Function\";\n\nimport type * as Bootstrap from \"./Bootstrap.ts\";\nimport type {\n BrowserDiagnostics,\n Checkpoint,\n CheckpointOptions,\n FillFormOptions,\n FillFormRequest,\n FillFormResult,\n FillRequest,\n HoverRequest,\n InputReceipt,\n KeyStroke,\n NavigateRequest,\n NavigationResult,\n ObservationOptions,\n ObservedElement,\n PointerMoveRequest,\n PressRequest,\n ReadTextRequest,\n ScreenshotRequest,\n ScreenshotResult,\n ScrollRequest,\n SelectOptions,\n SessionStatus,\n StartNavigationRequest,\n TextResult,\n TypeRequest,\n WaitForElementRequest,\n WheelRequest,\n ControlFacts,\n ActionResult,\n ClickRequest,\n FrameInfo,\n Observation,\n PageInfo,\n Target,\n Viewport,\n} from \"./BrowserData.ts\";\nimport type { BrowserError, InitializationError } from \"./Errors.ts\";\n\n/**\n * A host's decision about one control, made on facts read from the exact node immediately\n * before input. Returning anything but `true`, or throwing, sends nothing and fails `denied`.\n * It is a plain synchronous function on purpose: it runs while the owner's permit is held, where\n * waiting on a model or a network call would stall every other operation. It is not an atomic\n * check-and-input transaction, because page script can still run before the native input lands.\n */\nexport interface ElementAdmission {\n readonly admit: (facts: ControlFacts) => boolean;\n}\n\n/**\n * A navigation the browser is still performing. The owner's permit was released when it was\n * dispatched, so while it loads a host may read, `checkpoint`, hold and resume this page, and\n * use any other page. Anything that would change this page fails `busy` until it settles.\n *\n * Leaving its scope unsettled fences the session, exactly as an interrupted mutation does,\n * because nothing then knows what the browser did. It is never replayed.\n */\nexport interface NavigationOperation {\n /** What was navigated, read before dispatch. */\n readonly target: Target;\n /**\n * The document reached DOMContentLoaded. It belongs to this one navigation: a successor that\n * reaches the same URL fails it instead. Interrupting a waiter stops nothing in the browser.\n */\n readonly completed: Effect.Effect<NavigationResult, BrowserError>;\n /**\n * Asks the browser to stop loading and waits for this navigation to settle, after which\n * `completed` fails `interrupted`. Success is a known outcome and the session stays usable:\n * the page holds whatever had loaded. It does not undo anything the page already did.\n * An already-completed navigation cannot stop a successor. Concurrent callers share their\n * active attempt. Before dispatch, cancellation or a busy refusal permits a later request\n * once native setup has retired. A dispatched attempt's outcome, including failure or\n * interruption, is retained permanently; an uncertain stop is never replayed.\n */\n readonly stop: Effect.Effect<void, BrowserError>;\n}\n\n/** Common operations. Direct, retained and pinned views choose their target at different times. */\nexport interface TargetOperations {\n readonly navigate: (request: NavigateRequest) => Effect.Effect<NavigationResult, BrowserError>;\n /** `navigate`, left in flight: the same single dispatch, completed by the caller. */\n readonly startNavigation: (\n request: StartNavigationRequest,\n ) => Effect.Effect<NavigationOperation, BrowserError, Scope.Scope>;\n readonly readText: (request: ReadTextRequest) => Effect.Effect<TextResult, BrowserError>;\n readonly click: (request: ClickRequest) => Effect.Effect<ActionResult, BrowserError>;\n readonly fill: (request: FillRequest) => Effect.Effect<ActionResult, BrowserError>;\n /** Script in the page: instantaneous, and it raises no wheel event. */\n readonly scroll: (request: ScrollRequest) => Effect.Effect<ActionResult, BrowserError>;\n /** One real pointer move, in main-frame viewport pixels. */\n readonly pointerMove: (request: PointerMoveRequest) => Effect.Effect<InputReceipt, BrowserError>;\n /** Places the pointer on one exact element where it is, or fails `not-visible` unsent. */\n readonly hover: (request: HoverRequest) => Effect.Effect<InputReceipt, BrowserError>;\n /** One real wheel event; the browser decides what under the pointer scrolls. */\n readonly wheel: (request: WheelRequest) => Effect.Effect<InputReceipt, BrowserError>;\n /** One real key stroke to whatever has focus, or fails `not-focused` unsent if `into` lacks it. */\n readonly press: (request: PressRequest) => Effect.Effect<InputReceipt, BrowserError>;\n /** Text as the real key strokes that produce it, under the same focus rule as `press`. */\n readonly type: (request: TypeRequest) => Effect.Effect<InputReceipt, BrowserError>;\n readonly screenshot: (\n request: ScreenshotRequest,\n ) => Effect.Effect<ScreenshotResult, BrowserError>;\n}\n\n/**\n * One explicit page/frame target. Selection may move independently; each operation re-resolves\n * this identity inside the same owner and connection. Closing/detaching it or reconnecting makes\n * the handle stale before dispatch.\n */\nexport interface PinnedTarget extends TargetOperations {\n readonly target: Target;\n}\n\n/** A checked selection retained at acquisition; moving selection away and back makes it stale. */\nexport interface RetainedTarget extends TargetOperations {}\n\n/**\n * Host control over one owned browser. This is not a serializable model value: copying a\n * session object cannot copy its capture, page-control or connection authority.\n *\n * The inherited target operations resolve the selected page/frame when their Effect executes.\n * Use `retain` to retain the current selection with stale-on-selection-change semantics, or\n * `pinPage` / `pinFrame` when work must stay on an explicit target while selection moves.\n */\nexport interface BrowserSession<E = never> extends TargetOperations {\n /** The implementation which owns this live connection. */\n readonly implementation: string;\n /** Copied host-only state, readable without admission in every lifecycle phase. */\n readonly status: Effect.Effect<SessionStatus>;\n /** Bounded native/policy diagnostics. Typed callback causes remain in bindingDiagnostics. */\n readonly diagnostics: Effect.Effect<BrowserDiagnostics>;\n /** Close this scope and require its own ownership-specific cleanup evidence. */\n readonly closeChecked: Effect.Effect<void, BrowserError>;\n /** First fail-session callback cause, preserving the consumer's error type on the host. */\n readonly failure: Effect.Effect<never, E | InitializationError>;\n /** Bounded host-only evidence; consumer causes are never projected into a page reply. */\n readonly bindingDiagnostics: Effect.Effect<Bootstrap.BindingDiagnostics<E>>;\n /** Resolve, validate and retain the selection under owner admission. */\n readonly retain: Effect.Effect<RetainedTarget, BrowserError>;\n readonly target: Effect.Effect<Target, BrowserError>;\n /**\n * The one observation whose nodes later actions may name. `scope: \"viewport\"` keeps only text\n * and controls that are on screen and reachable, plus bounded choices of visible native\n * selects; the default reads the whole document. It carries no destination, form or field value.\n */\n readonly observe: (options?: ObservationOptions) => Effect.Effect<Observation, BrowserError>;\n /**\n * Passive evidence for a recorder, with a picture when asked. It issues no references and\n * leaves the observation above exactly as it was. Host-only: it carries control facts.\n */\n readonly checkpoint: (options?: CheckpointOptions) => Effect.Effect<Checkpoint, BrowserError>;\n /** Host-only facts about one observed control, read from that exact node just now. */\n readonly controlFacts: (reference: ObservedElement) => Effect.Effect<ControlFacts, BrowserError>;\n /**\n * After a page hold, nothing observed on that page may be acted on unchecked. This checks one\n * reference: still attached, and still the control that was inspected. It never searches by\n * selector or label for a substitute, and it sends nothing.\n */\n readonly revalidateElement: (\n reference: ObservedElement,\n ) => Effect.Effect<ObservedElement, BrowserError>;\n readonly clickElement: (\n reference: ObservedElement,\n admission?: ElementAdmission,\n ) => Effect.Effect<ActionResult, BrowserError>;\n readonly fillElement: (\n reference: ObservedElement,\n value: string,\n admission?: ElementAdmission,\n ) => Effect.Effect<ActionResult, BrowserError>;\n /** One selection using this exact select and its issued option element IDs, with fresh checks. */\n readonly selectOption: (\n reference: ObservedElement,\n options: SelectOptions,\n admission?: ElementAdmission,\n ) => Effect.Effect<ActionResult, BrowserError>;\n readonly hoverElement: (\n reference: ObservedElement,\n admission?: ElementAdmission,\n ) => Effect.Effect<InputReceipt, BrowserError>;\n /** A key stroke sent only if the exact node an observation named already has focus. */\n readonly pressElement: (\n reference: ObservedElement,\n stroke: KeyStroke,\n admission?: ElementAdmission,\n ) => Effect.Effect<InputReceipt, BrowserError>;\n /** Real typing with `fillElement`'s exactness: that node must already have focus. */\n readonly typeElement: (\n reference: ObservedElement,\n text: string,\n admission?: ElementAdmission,\n ) => Effect.Effect<InputReceipt, BrowserError>;\n /**\n * Set several controls of one observation in order, then optionally click one submit control.\n * Every step acts on the exact observed node after the same fresh checks as `fillElement`,\n * except that a control may have become enabled since it was observed, and each is admitted\n * and charged as its own action. The observation stays usable for this form's own steps only;\n * anything else that changes the page still retires it, and the form retires it when it ends.\n * The first refused or uncertain step ends the form. Submit is sent only after every field\n * succeeded and, unless `verify` is false, still holds what its step left there. Fields set\n * before a stop stay set. It fails only when its first step does; otherwise `stopped` says\n * where it ended and why.\n */\n readonly fillForm: (\n request: FillFormRequest,\n admission?: ElementAdmission,\n options?: FillFormOptions,\n ) => Effect.Effect<FillFormResult, BrowserError>;\n readonly pages: Effect.Effect<ReadonlyArray<PageInfo>, BrowserError>;\n readonly frames: Effect.Effect<ReadonlyArray<FrameInfo>, BrowserError>;\n /** List frames on one exact page without selecting it. */\n readonly framesOf: (page: PageInfo) => Effect.Effect<ReadonlyArray<FrameInfo>, BrowserError>;\n /** Pin the page's main frame without changing the session selection. */\n readonly pinPage: (page: PageInfo) => Effect.Effect<PinnedTarget, BrowserError>;\n /** Pin one frame that currently belongs to the exact page, without changing selection. */\n readonly pinFrame: (\n page: PageInfo,\n frame: FrameInfo,\n ) => Effect.Effect<PinnedTarget, BrowserError>;\n readonly selectPage: (page: PageInfo) => Effect.Effect<void, BrowserError>;\n readonly selectFrame: (frameId: string) => Effect.Effect<void, BrowserError>;\n /** Create a tab without selecting it and return that exact tab's checked identity. */\n readonly createPage: Effect.Effect<PageInfo, BrowserError>;\n readonly closePage: (page: PageInfo) => Effect.Effect<void, BrowserError>;\n readonly resizeViewport: (viewport: Viewport) => Effect.Effect<void, BrowserError>;\n /**\n * Wait on the original observed node within the host deadline. Hidden includes its detachment;\n * document replacement is stale. Success observes a condition, without authorizing later input.\n */\n readonly waitForElement: (request: WaitForElementRequest) => Effect.Effect<void, BrowserError>;\n readonly waitFor: (request: {\n readonly selector: string;\n readonly state: \"visible\" | \"hidden\" | \"attached\" | \"detached\";\n }) => Effect.Effect<void, BrowserError>;\n /** Navigation observation is registered before the single click dispatch. */\n readonly clickAndWait: (request: ClickRequest) => Effect.Effect<ActionResult, BrowserError>;\n /**\n * Readiness of the current document only. Dependent operations wait for it themselves;\n * this reports it without charging an action, so a caller can decide what to do about a\n * document that predates the registrations.\n */\n readonly ready: Effect.Effect<Bootstrap.ReadinessOutcome, InitializationError>;\n}\n\n/** Helpers that do not supervise callback failures accept any live browser session. */\nexport type AnySession = BrowserSession<unknown>;\n\n/** Dependencies are captured at acquisition, not erased into an environment-free service Layer. */\nexport interface OpenOptions<E = never, R = never> {\n readonly bootstrap?: Bootstrap.Plan<E, R>;\n}\n\n/**\n * Acquire, supervise and close one browser in a fresh scope. The callback keeps the concrete\n * Chromium or provider session type. Its scoped work is joined before checked browser cleanup,\n * and cleanup failures remain typed even when the callback has already failed or was cancelled.\n * Acquisition is evaluated once; an uncertain result is never retried.\n */\nexport const scoped: {\n <S extends BrowserSession<unknown>, A, E2, R2>(\n f: (session: S) => Effect.Effect<A, E2, R2>,\n ): <E, AE, AR>(\n open: Effect.Effect<S & BrowserSession<E>, AE, AR>,\n ) => Effect.Effect<\n A,\n AE | E | E2 | InitializationError | BrowserError,\n Exclude<AR | R2, Scope.Scope>\n >;\n <S, E, AE, AR, A, E2, R2>(\n open: Effect.Effect<S & BrowserSession<E>, AE, AR>,\n f: (session: S & BrowserSession<E>) => Effect.Effect<A, E2, R2>,\n ): Effect.Effect<\n A,\n AE | E | E2 | InitializationError | BrowserError,\n Exclude<AR | R2, Scope.Scope>\n >;\n} = dual(\n 2,\n <S, E, AE, AR, A, E2, R2>(\n open: Effect.Effect<S & BrowserSession<E>, AE, AR>,\n f: (session: S & BrowserSession<E>) => Effect.Effect<A, E2, R2>,\n ) =>\n Effect.scoped(\n Effect.flatMap(open, (session) =>\n Effect.scoped(\n Effect.raceFirst(\n session.failure,\n Effect.suspend(() => f(session)),\n ),\n ).pipe(Effect.onExit(() => session.closeChecked)),\n ),\n ),\n);\n"],"mappings":";;;;;;;;;AAuQA,MAAa,SAkBT,KACF,IAEE,MACA,MAEA,OAAO,OACL,OAAO,QAAQ,OAAO,YACpB,OAAO,OACL,OAAO,UACL,QAAQ,SACR,OAAO,cAAc,EAAE,OAAO,CAAC,CACjC,CACF,CAAC,CAAC,KAAK,OAAO,aAAa,QAAQ,YAAY,CAAC,CAClD,CACF,CACJ"}
|