effect-browser 0.2.0-beta.9 → 0.3.0-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +849 -1161
- package/dist/Browser-S4kvpmqa.mjs +605 -0
- package/dist/Browser-S4kvpmqa.mjs.map +1 -0
- package/dist/Browser.d.mts +127 -3
- package/dist/Browser.mjs +2 -22
- package/dist/BrowserError-Coy7EW1N.d.mts +140 -0
- package/dist/BrowserError.d.mts +2 -0
- package/dist/BrowserError.mjs +211 -0
- package/dist/BrowserError.mjs.map +1 -0
- package/dist/BrowserEvent-BRu2juEz.d.mts +279 -0
- package/dist/BrowserEvent.d.mts +2 -0
- package/dist/BrowserEvent.mjs +297 -0
- package/dist/BrowserEvent.mjs.map +1 -0
- package/dist/Cdp.d.mts +32 -0
- package/dist/Cdp.mjs +61 -0
- package/dist/Cdp.mjs.map +1 -0
- package/dist/Change-DNvztNLu.mjs +138 -0
- package/dist/Change-DNvztNLu.mjs.map +1 -0
- package/dist/Change-iMlcuzVD.d.mts +71 -0
- package/dist/Change.d.mts +2 -0
- package/dist/Change.mjs +2 -0
- package/dist/Chromium.d.mts +29 -81
- package/dist/Chromium.mjs +57 -392
- package/dist/Chromium.mjs.map +1 -1
- package/dist/Expect.d.mts +62 -0
- package/dist/Expect.mjs +114 -0
- package/dist/Expect.mjs.map +1 -0
- package/dist/Frame-BERmf_gM.d.mts +117 -0
- package/dist/Frame.d.mts +2 -0
- package/dist/Frame.mjs +123 -0
- package/dist/Frame.mjs.map +1 -0
- package/dist/Moment.d.mts +2 -0
- package/dist/Moment.mjs +288 -0
- package/dist/Moment.mjs.map +1 -0
- package/dist/Motion.d.mts +40 -0
- package/dist/Motion.mjs +122 -0
- package/dist/Motion.mjs.map +1 -0
- package/dist/Page-B5cjaUp5.mjs +618 -0
- package/dist/Page-B5cjaUp5.mjs.map +1 -0
- package/dist/Page-BlPjfibv.d.mts +595 -0
- package/dist/Page.d.mts +2 -0
- package/dist/Page.mjs +2 -0
- package/dist/Plan-BI8XGvDu.d.mts +209 -0
- package/dist/Plan-D1wrJVs8.mjs +508 -0
- package/dist/Plan-D1wrJVs8.mjs.map +1 -0
- package/dist/Plan.d.mts +2 -3
- package/dist/Plan.mjs +2 -2
- package/dist/Policy.d.mts +76 -0
- package/dist/Policy.mjs +237 -0
- package/dist/Policy.mjs.map +1 -0
- package/dist/Presentation.d.mts +48 -0
- package/dist/Presentation.mjs +117 -0
- package/dist/Presentation.mjs.map +1 -0
- package/dist/Snapshot-B34hZ4Jl.d.mts +68 -0
- package/dist/Snapshot.d.mts +2 -0
- package/dist/Snapshot.mjs +79 -0
- package/dist/Snapshot.mjs.map +1 -0
- package/dist/Stage.d.mts +64 -0
- package/dist/Stage.mjs +237 -0
- package/dist/Stage.mjs.map +1 -0
- package/dist/Supervisor-CYWFK3vM.d.mts +280 -0
- package/dist/Supervisor-TNKNOVzu.mjs +599 -0
- package/dist/Supervisor-TNKNOVzu.mjs.map +1 -0
- package/dist/Supervisor.d.mts +2 -0
- package/dist/Supervisor.mjs +2 -0
- package/dist/context-B6xr8nVi.mjs +261 -0
- package/dist/context-B6xr8nVi.mjs.map +1 -0
- package/dist/index.d.mts +17 -14
- package/dist/index.mjs +18 -13
- package/dist/match.inpage-xOe9LNO0.mjs +48 -0
- package/dist/match.inpage-xOe9LNO0.mjs.map +1 -0
- package/dist/page-B8tUQELg.mjs +5073 -0
- package/dist/page-B8tUQELg.mjs.map +1 -0
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/dist/url-Cd04Okid.mjs +264 -0
- package/dist/url-Cd04Okid.mjs.map +1 -0
- package/package.json +96 -61
- package/src/Browser.ts +666 -0
- package/src/BrowserError.ts +234 -0
- package/src/BrowserEvent.ts +296 -0
- package/src/Cdp.ts +101 -0
- package/src/Change.ts +88 -0
- package/src/Chromium.ts +102 -0
- package/src/Expect.ts +180 -0
- package/src/Frame.ts +130 -0
- package/src/Moment.ts +479 -0
- package/src/Motion.ts +166 -0
- package/src/Page.ts +522 -0
- package/src/Plan.ts +663 -0
- package/src/Policy.ts +325 -0
- package/src/Presentation.ts +195 -0
- package/src/Snapshot.ts +89 -0
- package/src/Stage.ts +467 -0
- package/src/Supervisor.ts +599 -0
- package/src/index.ts +20 -0
- package/src/internal/input/actions.ts +610 -0
- package/src/internal/input/dispatch.ts +161 -0
- package/src/internal/input/edit.inpage.ts +162 -0
- package/src/internal/input/evidence.inpage.ts +164 -0
- package/src/internal/input/gate.ts +224 -0
- package/src/internal/input/guard.inpage.ts +605 -0
- package/src/internal/input/guard.ts +184 -0
- package/src/internal/input/keyboard.ts +112 -0
- package/src/internal/input/keys.ts +127 -0
- package/src/internal/input/perform.ts +376 -0
- package/src/internal/input/pointer.ts +153 -0
- package/src/internal/input/replies.ts +195 -0
- package/src/internal/input/settle.ts +50 -0
- package/src/internal/input/style.ts +186 -0
- package/src/internal/input/targets.inpage.ts +293 -0
- package/src/internal/input/targets.ts +148 -0
- package/src/internal/page/bridge.ts +386 -0
- package/src/internal/page/context.ts +312 -0
- package/src/internal/page/lane.ts +311 -0
- package/src/internal/page/navigation.ts +183 -0
- package/src/internal/page/openings.ts +87 -0
- package/src/internal/page/page.ts +165 -0
- package/src/internal/page/url.ts +100 -0
- package/src/internal/page/viewport.ts +43 -0
- package/src/internal/pictures/capture.ts +776 -0
- package/src/internal/pictures/clock.ts +141 -0
- package/src/internal/pictures/counts.ts +92 -0
- package/src/internal/pictures/pictures.ts +520 -0
- package/src/internal/pictures/transport.ts +242 -0
- package/src/internal/reading/choose.ts +100 -0
- package/src/internal/reading/context.inpage.ts +201 -0
- package/src/internal/reading/controls.inpage.ts +158 -0
- package/src/internal/reading/expect.inpage.ts +70 -0
- package/src/internal/reading/match.inpage.ts +92 -0
- package/src/internal/reading/names.inpage.ts +306 -0
- package/src/internal/reading/outline.inpage.ts +260 -0
- package/src/internal/reading/reading.ts +223 -0
- package/src/internal/reading/ready.inpage.ts +164 -0
- package/src/internal/reading/ready.ts +111 -0
- package/src/internal/reading/subjects.inpage.ts +188 -0
- package/src/internal/reading/text.inpage.ts +135 -0
- package/src/internal/reading/walk.inpage.ts +220 -0
- package/src/internal/supervisor/lifecycle.ts +416 -0
- package/src/internal/timeline/changes.inpage.ts +216 -0
- package/src/internal/timeline/changes.ts +100 -0
- package/src/internal/timeline/events.ts +94 -0
- package/src/internal/timeline/fold.inpage.ts +123 -0
- package/src/internal/timeline/history.inpage.ts +229 -0
- package/src/internal/timeline/marks.inpage.ts +207 -0
- package/src/internal/timeline/record.inpage.ts +133 -0
- package/src/internal/timeline/sight.inpage.ts +122 -0
- package/src/internal/timeline/window.ts +96 -0
- package/src/internal/timeline/words.ts +65 -0
- package/dist/Bootstrap-B9UUXsvF.d.mts +0 -183
- package/dist/Bootstrap-CeQOWBy6.mjs +0 -189
- package/dist/Bootstrap-CeQOWBy6.mjs.map +0 -1
- package/dist/Bootstrap.d.mts +0 -3
- package/dist/Bootstrap.mjs +0 -2
- package/dist/Browser-BF9_sORP.d.mts +0 -388
- package/dist/Browser.mjs.map +0 -1
- package/dist/BrowserData-CPIIIHol.d.mts +0 -585
- package/dist/BrowserData.d.mts +0 -3
- package/dist/BrowserData.mjs +0 -695
- package/dist/BrowserData.mjs.map +0 -1
- package/dist/BrowserRuntime-B6nT1G4X.d.mts +0 -148
- package/dist/BrowserRuntime-C1r2aSRK.mjs +0 -6820
- package/dist/BrowserRuntime-C1r2aSRK.mjs.map +0 -1
- package/dist/BrowserRuntime.d.mts +0 -3
- package/dist/BrowserRuntime.mjs +0 -3
- package/dist/Capture.d.mts +0 -56
- package/dist/Capture.mjs +0 -678
- package/dist/Capture.mjs.map +0 -1
- package/dist/CaptureData-CYriZHX1.mjs +0 -201
- package/dist/CaptureData-CYriZHX1.mjs.map +0 -1
- package/dist/CaptureData.d.mts +0 -219
- package/dist/CaptureData.mjs +0 -2
- package/dist/Errors.d.mts +0 -158
- package/dist/Errors.mjs +0 -207
- package/dist/Errors.mjs.map +0 -1
- package/dist/Images-yCG9BmRU.mjs +0 -76
- package/dist/Images-yCG9BmRU.mjs.map +0 -1
- package/dist/OperationOptions-DAeRS6O9.mjs +0 -55
- package/dist/OperationOptions-DAeRS6O9.mjs.map +0 -1
- package/dist/PageControl.d.mts +0 -15
- package/dist/PageControl.mjs +0 -34
- package/dist/PageControl.mjs.map +0 -1
- package/dist/PageRegistry-B3q9DS3g.mjs +0 -164
- package/dist/PageRegistry-B3q9DS3g.mjs.map +0 -1
- package/dist/Plan-Dyxu2q8C.mjs +0 -543
- package/dist/Plan-Dyxu2q8C.mjs.map +0 -1
- package/dist/PlanData-B668RxbQ.d.mts +0 -4831
- package/dist/PlanData.d.mts +0 -3
- package/dist/PlanData.mjs +0 -268
- package/dist/PlanData.mjs.map +0 -1
- package/dist/PlanInput-BJcPPncs.mjs +0 -75
- package/dist/PlanInput-BJcPPncs.mjs.map +0 -1
- package/dist/PublicSession-DCSMMqOj.mjs +0 -5602
- package/dist/PublicSession-DCSMMqOj.mjs.map +0 -1
- package/dist/Testing.d.mts +0 -479
- package/dist/Testing.mjs +0 -2147
- package/dist/Testing.mjs.map +0 -1
- package/dist/Timeline-fV22Xk-S.d.mts +0 -29
- package/dist/Timeline.d.mts +0 -4
- package/dist/Timeline.mjs +0 -20
- package/dist/Timeline.mjs.map +0 -1
- package/dist/TimelineData-BsOn-YGZ.d.mts +0 -3818
- package/dist/TimelineData.d.mts +0 -3
- package/dist/TimelineData.mjs +0 -482
- package/dist/TimelineData.mjs.map +0 -1
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A browser kept open across losses and session ends, as a series of generations, each a new
|
|
3
|
+
* browser from the same provider. Pages don't carry over from one generation to the next.
|
|
4
|
+
*
|
|
5
|
+
* `make` opens the first generation at once with the provider's `open`, in a scope of its own, so
|
|
6
|
+
* a caller that stops waiting never interrupts a half-open browser. `browser` gives the current
|
|
7
|
+
* generation; callers wait, bounded, and share one open. A loss is published at once and the next
|
|
8
|
+
* generation opens on the `reopen` schedule, which also retries a failed open, unless the provider
|
|
9
|
+
* deems its failure `definite`: that goes `Down` at once, with its cause. `rotate` opens the
|
|
10
|
+
* next generation now, and so does the time `rotateBefore` ahead of a generation's `expiresAt`. A
|
|
11
|
+
* rotation makes the next generation before it breaks the current one, unless generations are
|
|
12
|
+
* `exclusive`, as sessions saving to one stored context must be: then the current one is released
|
|
13
|
+
* first. `retire` stops reopening at once and releases what is open; the scope's close retires
|
|
14
|
+
* too, unless the supervisor `keep`s: then it leaves the serving generation running, `Kept`. `states`
|
|
15
|
+
* publishes each generation's changes, with the provider's release outcome last.
|
|
16
|
+
*
|
|
17
|
+
* Providers supply the specifics: `Browserbase.supervise` opens hosted sessions this way.
|
|
18
|
+
*
|
|
19
|
+
* @since 0.3.0
|
|
20
|
+
*/
|
|
21
|
+
import {
|
|
22
|
+
Cause,
|
|
23
|
+
Clock,
|
|
24
|
+
DateTime,
|
|
25
|
+
Deferred,
|
|
26
|
+
Duration,
|
|
27
|
+
Effect,
|
|
28
|
+
Exit,
|
|
29
|
+
FiberHandle,
|
|
30
|
+
FiberMap,
|
|
31
|
+
Option,
|
|
32
|
+
Predicate,
|
|
33
|
+
PubSub,
|
|
34
|
+
Schedule,
|
|
35
|
+
Schema,
|
|
36
|
+
Scope,
|
|
37
|
+
Semaphore,
|
|
38
|
+
Stream,
|
|
39
|
+
SubscriptionRef,
|
|
40
|
+
type Take,
|
|
41
|
+
} from "effect";
|
|
42
|
+
|
|
43
|
+
import type * as Browser from "./Browser.ts";
|
|
44
|
+
import * as Lifecycle from "./internal/supervisor/lifecycle.ts";
|
|
45
|
+
|
|
46
|
+
export {
|
|
47
|
+
Closed,
|
|
48
|
+
Down,
|
|
49
|
+
Draining,
|
|
50
|
+
GenerationState,
|
|
51
|
+
Kept,
|
|
52
|
+
Lost,
|
|
53
|
+
Open,
|
|
54
|
+
Opening,
|
|
55
|
+
Released,
|
|
56
|
+
Reopening,
|
|
57
|
+
Settled,
|
|
58
|
+
Unconfirmed,
|
|
59
|
+
} from "./internal/supervisor/lifecycle.ts";
|
|
60
|
+
|
|
61
|
+
/** One change in a generation's life. `at` is wall-clock epoch milliseconds from the Effect `Clock`. */
|
|
62
|
+
export class Generation extends Schema.Class<Generation>("effect-browser/Generation")({
|
|
63
|
+
number: Schema.Int.check(Schema.isGreaterThan(0)),
|
|
64
|
+
at: Schema.Finite,
|
|
65
|
+
hostTime: Schema.Finite,
|
|
66
|
+
state: Lifecycle.GenerationState,
|
|
67
|
+
}) {}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* No browser to give: none opened within `waitTimeout` (`opening`), the latest generation failed
|
|
71
|
+
* to open (`down`, with what it failed with as `cause`), or the supervisor was retired.
|
|
72
|
+
*/
|
|
73
|
+
export class Unavailable extends Schema.TaggedError<Unavailable>()("Unavailable", {
|
|
74
|
+
reason: Schema.Literals(["opening", "down", "retired"]),
|
|
75
|
+
detail: Schema.String,
|
|
76
|
+
cause: Schema.optional(Schema.Defect()),
|
|
77
|
+
}) {
|
|
78
|
+
override get message() {
|
|
79
|
+
return `no browser: ${this.detail}`;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** One generation, as a provider opened it in the scope the supervisor gave it. */
|
|
84
|
+
export interface Opened<A = void> {
|
|
85
|
+
readonly value: A;
|
|
86
|
+
readonly browser: Browser.Service;
|
|
87
|
+
/**
|
|
88
|
+
* End the generation, and say whether the provider confirmed its end. The supervisor asks it
|
|
89
|
+
* before it closes the generation's scope; without it, closing the scope ends the generation,
|
|
90
|
+
* which counts as `Settled`.
|
|
91
|
+
*/
|
|
92
|
+
readonly release?: Effect.Effect<Lifecycle.Released> | undefined;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface Options<E, R, A = void> {
|
|
96
|
+
/** Opens one generation in the scope it is given. */
|
|
97
|
+
readonly open: Effect.Effect<Opened<A>, E, R>;
|
|
98
|
+
/** When to try a failed open again. Defaults to exponential from 1 second, for 5 minutes. */
|
|
99
|
+
readonly reopen?: Schedule.Schedule<unknown, E> | undefined;
|
|
100
|
+
/**
|
|
101
|
+
* Failures no new try mends, such as a refused key: an open that fails so is `Down` at once.
|
|
102
|
+
* Defaults to none.
|
|
103
|
+
*/
|
|
104
|
+
readonly definite?: ((error: E) => boolean) | undefined;
|
|
105
|
+
/**
|
|
106
|
+
* Rotate this long ahead of a generation's `expiresAt`, but no sooner than halfway through its
|
|
107
|
+
* life. Without it, a generation's end is a loss.
|
|
108
|
+
*/
|
|
109
|
+
readonly rotateBefore?: Duration.Input | undefined;
|
|
110
|
+
/** Generations must not overlap: a rotation releases the current one before it opens the next. */
|
|
111
|
+
readonly exclusive?: boolean | undefined;
|
|
112
|
+
/** Finite, nonnegative time outgoing holds may finish; defaults to 30 seconds. */
|
|
113
|
+
readonly handover?: Duration.Input | undefined;
|
|
114
|
+
/**
|
|
115
|
+
* Closing the scope leaves the serving generation running, `Kept`: it closes the generation's
|
|
116
|
+
* scope but never asks `release`. For providers whose scope only disconnects, such as
|
|
117
|
+
* `Browserbase.supervise` with `keep`. `retire` still releases it.
|
|
118
|
+
*/
|
|
119
|
+
readonly keep?: boolean | undefined;
|
|
120
|
+
/** How long `browser` and `rotate` wait for a generation to open. Defaults to 2 minutes. */
|
|
121
|
+
readonly waitTimeout?: Duration.Input | undefined;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Work whose generation was revoked; input receipts still determine whether input reached a site. */
|
|
125
|
+
export class Superseded extends Schema.TaggedError<Superseded>()("Superseded", {
|
|
126
|
+
generation: Schema.Int,
|
|
127
|
+
reason: Schema.Literals(["rotated", "lost", "retired"]),
|
|
128
|
+
revokeAt: Schema.Finite,
|
|
129
|
+
}) {}
|
|
130
|
+
|
|
131
|
+
export interface Lease<A> {
|
|
132
|
+
readonly generation: number;
|
|
133
|
+
readonly browser: Browser.Service;
|
|
134
|
+
readonly value: A;
|
|
135
|
+
/** First notice that serving ended; loss or retirement may revoke before its deadline. */
|
|
136
|
+
readonly ending: Effect.Effect<Superseded>;
|
|
137
|
+
/** An independent scope; its resources close before provider release. Fails once revoked. */
|
|
138
|
+
readonly retain: Effect.Effect<Scope.Closeable, Superseded>;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface UseOptions {
|
|
142
|
+
/** Finite, nonnegative wait for a serving generation. Zero makes an immediate attempt. */
|
|
143
|
+
readonly wait?: Duration.Input | undefined;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface Supervisor<A = void> {
|
|
147
|
+
/** Scoped work on the current generation. Revocation interrupts it and waits for its cleanup. */
|
|
148
|
+
readonly use: <B, E, R>(
|
|
149
|
+
work: (lease: Lease<A>) => Effect.Effect<B, E, R>,
|
|
150
|
+
options?: UseOptions,
|
|
151
|
+
) => Effect.Effect<B, E | Unavailable | Superseded, Exclude<R, Scope.Scope>>;
|
|
152
|
+
/** The current generation's browser, waiting up to `waitTimeout` while none is open. */
|
|
153
|
+
readonly browser: Effect.Effect<Browser.Service, Unavailable>;
|
|
154
|
+
/** Each generation's changes, in order. A new reader first gets the latest 64; ends on retiring. */
|
|
155
|
+
readonly states: Stream.Stream<Generation>;
|
|
156
|
+
/** Open the next generation now, or join the one opening, and wait for it. */
|
|
157
|
+
readonly rotate: Effect.Effect<Browser.Service, Unavailable>;
|
|
158
|
+
/** Stop opening at once, release every generation, and wait until each release has finished. */
|
|
159
|
+
readonly retire: Effect.Effect<void>;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
type Hold =
|
|
163
|
+
| { readonly _tag: "Use" }
|
|
164
|
+
| { readonly _tag: "Retained"; readonly scope: Scope.Closeable };
|
|
165
|
+
|
|
166
|
+
interface Held<A> {
|
|
167
|
+
readonly opened: Opened<A>;
|
|
168
|
+
readonly ending: Deferred.Deferred<Superseded>;
|
|
169
|
+
readonly revoked: Deferred.Deferred<Superseded>;
|
|
170
|
+
readonly scope: Scope.Closeable;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
type State<A> = Lifecycle.State<Held<A>, Hold>;
|
|
174
|
+
type Apply<A> = (
|
|
175
|
+
input: Lifecycle.Input<Held<A>, Hold>,
|
|
176
|
+
) => Effect.Effect<Lifecycle.Step<Held<A>, Hold>>;
|
|
177
|
+
|
|
178
|
+
const retired = () => new Unavailable({ reason: "retired", detail: "the supervisor was retired" });
|
|
179
|
+
|
|
180
|
+
const describe = (cause: Cause.Cause<unknown>) => {
|
|
181
|
+
const error = Cause.squash(cause);
|
|
182
|
+
|
|
183
|
+
return Predicate.isError(error) ? error.message : String(error);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const unavailable = (
|
|
187
|
+
resolution: Extract<Lifecycle.Resolution<unknown>, { readonly _tag: "Unavailable" }>,
|
|
188
|
+
) =>
|
|
189
|
+
new Unavailable({
|
|
190
|
+
reason: resolution.reason,
|
|
191
|
+
detail: resolution.detail,
|
|
192
|
+
cause: resolution.cause,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const timedOut = () =>
|
|
196
|
+
new Unavailable({ reason: "opening", detail: "no generation opened within the wait" });
|
|
197
|
+
|
|
198
|
+
const bounded = (input: Duration.Input, name: string) =>
|
|
199
|
+
Effect.suspend(() => {
|
|
200
|
+
const duration = Duration.fromInput(input);
|
|
201
|
+
|
|
202
|
+
return Option.isSome(duration) &&
|
|
203
|
+
Duration.isFinite(duration.value) &&
|
|
204
|
+
!Duration.isNegative(duration.value)
|
|
205
|
+
? Effect.succeed(duration.value)
|
|
206
|
+
: Effect.die(new RangeError(`${name} must be a finite nonnegative duration`));
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
/** Resolve immediately before starting a bounded wait, including when its duration is zero. */
|
|
210
|
+
const waitFor = <A, H>(
|
|
211
|
+
state: SubscriptionRef.SubscriptionRef<Lifecycle.State<A, H>>,
|
|
212
|
+
wait: Duration.Duration,
|
|
213
|
+
clock: Clock.Clock,
|
|
214
|
+
target?: number,
|
|
215
|
+
) => {
|
|
216
|
+
const result = (resolution: Lifecycle.Resolution<A>) =>
|
|
217
|
+
resolution._tag === "Serve"
|
|
218
|
+
? Effect.succeed(resolution.live)
|
|
219
|
+
: Effect.fail(unavailable(resolution));
|
|
220
|
+
|
|
221
|
+
return Effect.suspend(() => {
|
|
222
|
+
const current = Lifecycle.resolve(state.value, target);
|
|
223
|
+
|
|
224
|
+
return current === undefined
|
|
225
|
+
? SubscriptionRef.changes(state).pipe(
|
|
226
|
+
Stream.map((state) => Lifecycle.resolve(state, target)),
|
|
227
|
+
Stream.filter(Predicate.isNotUndefined),
|
|
228
|
+
Stream.runHead,
|
|
229
|
+
Effect.flatMap(Option.match({ onNone: () => Effect.fail(retired()), onSome: result })),
|
|
230
|
+
Effect.timeoutOrElse({ duration: wait, orElse: () => Effect.fail(timedOut()) }),
|
|
231
|
+
)
|
|
232
|
+
: result(current);
|
|
233
|
+
}).pipe(Effect.provideService(Clock.Clock, clock));
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
/** Sleep until `before` ahead of `expiresAt`, but no sooner than halfway there from now. */
|
|
237
|
+
const untilRotation = (expiresAt: DateTime.Utc, before: Duration.Duration) =>
|
|
238
|
+
Clock.currentTimeMillis.pipe(
|
|
239
|
+
Effect.flatMap((now) => {
|
|
240
|
+
const left = DateTime.toEpochMillis(expiresAt) - now;
|
|
241
|
+
|
|
242
|
+
return Effect.sleep(Duration.millis(Math.max(left - Duration.toMillis(before), left / 2, 0)));
|
|
243
|
+
}),
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
/** Native timers may wake a fraction early; a revocation must reach its monotonic deadline. */
|
|
247
|
+
const until = (at: number, now: () => number): Effect.Effect<void> =>
|
|
248
|
+
Effect.suspend(() =>
|
|
249
|
+
now() >= at
|
|
250
|
+
? Effect.void
|
|
251
|
+
: Effect.sleep(Duration.millis(Math.ceil(at - now()))).pipe(Effect.andThen(until(at, now))),
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
/** Borrowed work and transferred scopes hold a generation until cleanup lets it go. */
|
|
255
|
+
const leasing = <A>(
|
|
256
|
+
state: SubscriptionRef.SubscriptionRef<State<A>>,
|
|
257
|
+
apply: Apply<A>,
|
|
258
|
+
waitTimeout: Duration.Duration,
|
|
259
|
+
now: () => number,
|
|
260
|
+
clock: Clock.Clock,
|
|
261
|
+
) => {
|
|
262
|
+
const borrow = (hold: Hold, wait: Duration.Duration) =>
|
|
263
|
+
Effect.gen(function* () {
|
|
264
|
+
const until = now() + Duration.toMillis(wait);
|
|
265
|
+
|
|
266
|
+
for (;;) {
|
|
267
|
+
const step = yield* apply({ _tag: "Hold", hold });
|
|
268
|
+
|
|
269
|
+
if (step.borrowed !== undefined) return step.borrowed;
|
|
270
|
+
yield* waitFor(state, Duration.millis(Math.max(0, until - now())), clock).pipe(
|
|
271
|
+
Effect.interruptible,
|
|
272
|
+
);
|
|
273
|
+
if (now() >= until) return yield* timedOut();
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
const leaseOf = (live: Lifecycle.Live<Held<A>>): Lease<A> => ({
|
|
278
|
+
generation: live.number,
|
|
279
|
+
browser: live.value.opened.browser,
|
|
280
|
+
value: live.value.opened.value,
|
|
281
|
+
ending: Deferred.await(live.value.ending),
|
|
282
|
+
retain: Effect.uninterruptible(
|
|
283
|
+
Effect.gen(function* () {
|
|
284
|
+
const scope = yield* Scope.make();
|
|
285
|
+
const hold: Hold = { _tag: "Retained", scope };
|
|
286
|
+
const step = yield* apply({ _tag: "Hold", number: live.number, hold });
|
|
287
|
+
|
|
288
|
+
if (step.borrowed === undefined) {
|
|
289
|
+
yield* Scope.close(scope, Exit.void);
|
|
290
|
+
|
|
291
|
+
return yield* yield* Deferred.await(live.value.ending);
|
|
292
|
+
}
|
|
293
|
+
yield* Scope.addFinalizer(
|
|
294
|
+
scope,
|
|
295
|
+
apply({ _tag: "Let", number: live.number, hold }).pipe(Effect.asVoid),
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
return scope;
|
|
299
|
+
}),
|
|
300
|
+
),
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const use: Supervisor<A>["use"] = (work, options = {}) =>
|
|
304
|
+
Effect.scoped(
|
|
305
|
+
Effect.gen(function* () {
|
|
306
|
+
const hold: Hold = { _tag: "Use" };
|
|
307
|
+
const wait = yield* bounded(options.wait ?? waitTimeout, "wait");
|
|
308
|
+
|
|
309
|
+
const live = yield* Effect.acquireRelease(borrow(hold, wait), (live) =>
|
|
310
|
+
apply({ _tag: "Let", number: live.number, hold }).pipe(Effect.asVoid),
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
return yield* Effect.raceFirst(
|
|
314
|
+
work(leaseOf(live)),
|
|
315
|
+
Deferred.await(live.value.revoked).pipe(Effect.flatMap(Effect.fail)),
|
|
316
|
+
);
|
|
317
|
+
}),
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
return use;
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
type Provider = Omit<Opened, "value">;
|
|
324
|
+
type MakeOptions<E, R, A> = Omit<Options<E, R, A>, "open"> & {
|
|
325
|
+
readonly open: Effect.Effect<Opened<A> | Provider, E, R>;
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
const acquireGeneration = <A, E, R>(
|
|
329
|
+
open: Effect.Effect<Opened<A> | Provider, E, R>,
|
|
330
|
+
own: Scope.Scope,
|
|
331
|
+
) =>
|
|
332
|
+
Effect.uninterruptibleMask((restore) =>
|
|
333
|
+
Effect.flatMap(Scope.fork(own), (scope) =>
|
|
334
|
+
restore(open.pipe(Scope.provide(scope))).pipe(
|
|
335
|
+
Effect.onError((cause) => Scope.close(scope, Exit.failCause(cause))),
|
|
336
|
+
Effect.map((opened): Held<A | void> => ({
|
|
337
|
+
opened: { ...opened, value: "value" in opened ? opened.value : undefined },
|
|
338
|
+
scope,
|
|
339
|
+
ending: Deferred.makeUnsafe<Superseded>(),
|
|
340
|
+
revoked: Deferred.makeUnsafe<Superseded>(),
|
|
341
|
+
})),
|
|
342
|
+
),
|
|
343
|
+
),
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
const changedUntil = <A>(
|
|
347
|
+
state: SubscriptionRef.SubscriptionRef<State<A>>,
|
|
348
|
+
predicate: (current: State<A>) => boolean,
|
|
349
|
+
) => SubscriptionRef.changes(state).pipe(Stream.filter(predicate), Stream.runHead, Effect.asVoid);
|
|
350
|
+
|
|
351
|
+
const watchGeneration = <A>(
|
|
352
|
+
live: Lifecycle.Live<Held<A>>,
|
|
353
|
+
rotateBefore: Duration.Duration | undefined,
|
|
354
|
+
apply: Apply<A>,
|
|
355
|
+
) => {
|
|
356
|
+
const { browser } = live.value.opened;
|
|
357
|
+
|
|
358
|
+
const loss = browser.disconnected.pipe(
|
|
359
|
+
Effect.flatMap((cause) => apply({ _tag: "Lost", number: live.number, cause })),
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
const rotation =
|
|
363
|
+
browser.expiresAt === undefined || rotateBefore === undefined
|
|
364
|
+
? Effect.void
|
|
365
|
+
: untilRotation(browser.expiresAt, rotateBefore).pipe(
|
|
366
|
+
Effect.andThen(apply({ _tag: "Rotate", due: live.number })),
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
return Effect.all([loss, rotation], { concurrency: 2, discard: true });
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
// Revocation stops use work; retained resources and use finalizers let go before the provider.
|
|
373
|
+
const releaseGeneration = <A>(
|
|
374
|
+
command: Extract<Lifecycle.Command<Held<A>>, { readonly _tag: "Release" | "Leave" }>,
|
|
375
|
+
state: SubscriptionRef.SubscriptionRef<State<A>>,
|
|
376
|
+
apply: Apply<A>,
|
|
377
|
+
) =>
|
|
378
|
+
Effect.gen(function* () {
|
|
379
|
+
const { live, ending } = command;
|
|
380
|
+
|
|
381
|
+
yield* Deferred.succeed(
|
|
382
|
+
live.value.revoked,
|
|
383
|
+
new Superseded({
|
|
384
|
+
generation: live.number,
|
|
385
|
+
reason: ending.reason,
|
|
386
|
+
revokeAt: ending.revokeAt,
|
|
387
|
+
}),
|
|
388
|
+
);
|
|
389
|
+
const holds = Lifecycle.lookup(state.value, live.number)?.holds ?? [];
|
|
390
|
+
|
|
391
|
+
const cleaned = yield* Effect.forEach(
|
|
392
|
+
holds,
|
|
393
|
+
(hold) =>
|
|
394
|
+
Effect.exit(hold._tag === "Retained" ? Scope.close(hold.scope, Exit.void) : Effect.void),
|
|
395
|
+
{ concurrency: 8 },
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
yield* changedUntil(
|
|
399
|
+
state,
|
|
400
|
+
(current) => (Lifecycle.lookup(current, live.number)?.holds.size ?? 0) === 0,
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
const released =
|
|
404
|
+
command._tag === "Leave"
|
|
405
|
+
? undefined
|
|
406
|
+
: yield* (live.value.opened.release ?? Effect.succeed(new Lifecycle.Settled())).pipe(
|
|
407
|
+
Effect.catchCause((cause) =>
|
|
408
|
+
Effect.succeed(
|
|
409
|
+
new Lifecycle.Unconfirmed({ detail: `the release failed: ${describe(cause)}` }),
|
|
410
|
+
),
|
|
411
|
+
),
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
const closed = yield* Effect.exit(Scope.close(live.value.scope, Exit.void));
|
|
415
|
+
|
|
416
|
+
yield* apply({ _tag: "Released", number: live.number, released });
|
|
417
|
+
yield* Exit.asVoidAll([...cleaned, closed]);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
const makeScoped = Effect.fn("Supervisor.make")(function* <E, R, A>(
|
|
421
|
+
options: MakeOptions<E, R, A>,
|
|
422
|
+
): Effect.fn.Return<Supervisor<A | void>, never, Scope.Scope | Exclude<R, Scope.Scope>> {
|
|
423
|
+
const clock = yield* Clock.Clock;
|
|
424
|
+
const now = () => Number(clock.monotonicTimeNanosUnsafe()) / 1e6;
|
|
425
|
+
const context = yield* Effect.context<Exclude<R, Scope.Scope>>();
|
|
426
|
+
const own = yield* Scope.fork(yield* Effect.scope);
|
|
427
|
+
const exclusive = options.exclusive ?? false;
|
|
428
|
+
const definite = options.definite ?? (() => false);
|
|
429
|
+
|
|
430
|
+
const reopen: Schedule.Schedule<unknown, E> =
|
|
431
|
+
options.reopen ??
|
|
432
|
+
Schedule.exponential("1 second").pipe(Schedule.upTo({ duration: Duration.minutes(5) }));
|
|
433
|
+
|
|
434
|
+
const rotateBefore =
|
|
435
|
+
options.rotateBefore === undefined ? undefined : Duration.fromInputUnsafe(options.rotateBefore);
|
|
436
|
+
|
|
437
|
+
const waitTimeout = yield* bounded(options.waitTimeout ?? Duration.minutes(2), "waitTimeout");
|
|
438
|
+
|
|
439
|
+
const handover = Duration.toMillis(
|
|
440
|
+
yield* bounded(options.handover ?? Duration.seconds(30), "handover"),
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
const started = Lifecycle.start<Held<A | void>, Hold>();
|
|
444
|
+
const state = yield* SubscriptionRef.make(started.state);
|
|
445
|
+
const applying = yield* Semaphore.make(1);
|
|
446
|
+
const events = yield* PubSub.unbounded<Take.Take<Generation>>({ replay: 64 });
|
|
447
|
+
const opening = yield* FiberHandle.make<unknown, never>();
|
|
448
|
+
const releases = yield* FiberMap.make<number, unknown, never>();
|
|
449
|
+
const runOpen = yield* FiberHandle.runtime(opening)();
|
|
450
|
+
const runRelease = yield* FiberMap.runtime(releases)();
|
|
451
|
+
|
|
452
|
+
const apply = (
|
|
453
|
+
input: Lifecycle.Input<Held<A | void>, Hold>,
|
|
454
|
+
): Effect.Effect<Lifecycle.Step<Held<A | void>, Hold>> =>
|
|
455
|
+
applying
|
|
456
|
+
.withPermit(
|
|
457
|
+
Effect.gen(function* () {
|
|
458
|
+
const current = state.value;
|
|
459
|
+
const step = Lifecycle.transition(current, input, exclusive, now(), handover);
|
|
460
|
+
|
|
461
|
+
if (step.state !== current) yield* SubscriptionRef.set(state, step.state);
|
|
462
|
+
yield* perform(step);
|
|
463
|
+
|
|
464
|
+
return step;
|
|
465
|
+
}),
|
|
466
|
+
)
|
|
467
|
+
.pipe(Effect.uninterruptible, Effect.provideService(Clock.Clock, clock));
|
|
468
|
+
|
|
469
|
+
const perform = (step: Lifecycle.Step<Held<A | void>, Hold>): Effect.Effect<void> =>
|
|
470
|
+
Effect.gen(function* () {
|
|
471
|
+
for (const [number, generation] of step.events)
|
|
472
|
+
PubSub.publishUnsafe(events, [
|
|
473
|
+
new Generation({
|
|
474
|
+
number,
|
|
475
|
+
at: clock.currentTimeMillisUnsafe(),
|
|
476
|
+
hostTime: now(),
|
|
477
|
+
state: generation,
|
|
478
|
+
}),
|
|
479
|
+
]);
|
|
480
|
+
for (const command of step.commands)
|
|
481
|
+
switch (command._tag) {
|
|
482
|
+
case "Open":
|
|
483
|
+
runOpen(openGeneration(command.number, command.after));
|
|
484
|
+
break;
|
|
485
|
+
case "Watch":
|
|
486
|
+
yield* Effect.forkIn(
|
|
487
|
+
Effect.provideContext(watchGeneration(command.live, rotateBefore, apply), context),
|
|
488
|
+
command.live.value.scope,
|
|
489
|
+
);
|
|
490
|
+
break;
|
|
491
|
+
case "Drain": {
|
|
492
|
+
const ending = new Superseded({
|
|
493
|
+
generation: command.live.number,
|
|
494
|
+
reason: command.ending.reason,
|
|
495
|
+
revokeAt: command.ending.revokeAt,
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
yield* Deferred.succeed(command.live.value.ending, ending);
|
|
499
|
+
yield* Effect.forkIn(
|
|
500
|
+
until(ending.revokeAt, now).pipe(
|
|
501
|
+
Effect.andThen(
|
|
502
|
+
Effect.suspend(() => apply({ _tag: "Revoke", number: command.live.number })),
|
|
503
|
+
),
|
|
504
|
+
),
|
|
505
|
+
command.live.value.scope,
|
|
506
|
+
);
|
|
507
|
+
break;
|
|
508
|
+
}
|
|
509
|
+
case "Release":
|
|
510
|
+
case "Leave":
|
|
511
|
+
runRelease(command.live.number, releaseGeneration(command, state, apply));
|
|
512
|
+
break;
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
const openOnce = acquireGeneration(options.open, own).pipe(Effect.provideContext(context));
|
|
517
|
+
|
|
518
|
+
const openGeneration = (number: number, after: number | undefined) =>
|
|
519
|
+
Effect.uninterruptibleMask((restore) =>
|
|
520
|
+
restore(
|
|
521
|
+
(after === undefined
|
|
522
|
+
? Effect.void
|
|
523
|
+
: changedUntil(state, (current) => !current.draining.has(after))
|
|
524
|
+
).pipe(
|
|
525
|
+
Effect.andThen(
|
|
526
|
+
Effect.retry(openOnce, { schedule: reopen, while: (error) => !definite(error) }),
|
|
527
|
+
),
|
|
528
|
+
),
|
|
529
|
+
).pipe(
|
|
530
|
+
Effect.exit,
|
|
531
|
+
Effect.flatMap((exit) =>
|
|
532
|
+
Exit.isSuccess(exit)
|
|
533
|
+
? apply({ _tag: "Opened", live: { number, value: exit.value } })
|
|
534
|
+
: Cause.hasInterruptsOnly(exit.cause)
|
|
535
|
+
? apply({ _tag: "Abandoned", number })
|
|
536
|
+
: apply({
|
|
537
|
+
_tag: "Failed",
|
|
538
|
+
number,
|
|
539
|
+
detail: describe(exit.cause),
|
|
540
|
+
cause: Cause.squash(exit.cause),
|
|
541
|
+
}),
|
|
542
|
+
),
|
|
543
|
+
),
|
|
544
|
+
);
|
|
545
|
+
|
|
546
|
+
const use = leasing(state, apply, waitTimeout, now, clock);
|
|
547
|
+
|
|
548
|
+
const end = yield* Effect.cached(PubSub.publish(events, Exit.void));
|
|
549
|
+
|
|
550
|
+
const stop = (input: Lifecycle.Input<Held<A | void>, Hold>) =>
|
|
551
|
+
apply(input).pipe(
|
|
552
|
+
Effect.andThen(FiberHandle.clear(opening)),
|
|
553
|
+
Effect.andThen(FiberMap.awaitEmpty(releases)),
|
|
554
|
+
Effect.andThen(
|
|
555
|
+
Effect.suspend(() =>
|
|
556
|
+
Deferred.isDone(releases.deferred).pipe(
|
|
557
|
+
Effect.flatMap((failed) => (failed ? FiberMap.join(releases) : Effect.void)),
|
|
558
|
+
),
|
|
559
|
+
),
|
|
560
|
+
),
|
|
561
|
+
Effect.ensuring(end),
|
|
562
|
+
Effect.asVoid,
|
|
563
|
+
);
|
|
564
|
+
|
|
565
|
+
const retire = yield* Effect.cached(stop({ _tag: "Retire" }));
|
|
566
|
+
|
|
567
|
+
yield* Effect.addFinalizer(() => (options.keep === true ? stop({ _tag: "Keep" }) : retire));
|
|
568
|
+
yield* perform(started);
|
|
569
|
+
|
|
570
|
+
return {
|
|
571
|
+
browser: Effect.map(waitFor(state, waitTimeout, clock), (live) => live.value.opened.browser),
|
|
572
|
+
use,
|
|
573
|
+
states: Stream.fromPubSubTake(events),
|
|
574
|
+
rotate: apply({ _tag: "Rotate" }).pipe(
|
|
575
|
+
Effect.flatMap(({ reply }) =>
|
|
576
|
+
reply === undefined
|
|
577
|
+
? Effect.fail(retired())
|
|
578
|
+
: Effect.map(
|
|
579
|
+
waitFor(state, waitTimeout, clock, reply),
|
|
580
|
+
(live) => live.value.opened.browser,
|
|
581
|
+
),
|
|
582
|
+
),
|
|
583
|
+
),
|
|
584
|
+
retire,
|
|
585
|
+
};
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
/** Supervise scoped generation values. Browser-only providers retain their existing void value. */
|
|
589
|
+
export function make<E, R, A = void>(
|
|
590
|
+
options: Options<E, R, A>,
|
|
591
|
+
): Effect.Effect<Supervisor<A>, never, Scope.Scope | Exclude<R, Scope.Scope>>;
|
|
592
|
+
|
|
593
|
+
export function make<E, R>(
|
|
594
|
+
options: MakeOptions<E, R, never>,
|
|
595
|
+
): Effect.Effect<Supervisor<void>, never, Scope.Scope | Exclude<R, Scope.Scope>>;
|
|
596
|
+
|
|
597
|
+
export function make<E, R, A>(options: MakeOptions<E, R, A>) {
|
|
598
|
+
return makeScoped(options);
|
|
599
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @since 0.3.0
|
|
3
|
+
*/
|
|
4
|
+
export * as Browser from "./Browser.ts";
|
|
5
|
+
export * as BrowserError from "./BrowserError.ts";
|
|
6
|
+
export * as BrowserEvent from "./BrowserEvent.ts";
|
|
7
|
+
export * as Cdp from "./Cdp.ts";
|
|
8
|
+
export * as Change from "./Change.ts";
|
|
9
|
+
export * as Chromium from "./Chromium.ts";
|
|
10
|
+
export * as Expect from "./Expect.ts";
|
|
11
|
+
export * as Frame from "./Frame.ts";
|
|
12
|
+
export * as Moment from "./Moment.ts";
|
|
13
|
+
export * as Motion from "./Motion.ts";
|
|
14
|
+
export * as Page from "./Page.ts";
|
|
15
|
+
export * as Plan from "./Plan.ts";
|
|
16
|
+
export * as Policy from "./Policy.ts";
|
|
17
|
+
export * as Presentation from "./Presentation.ts";
|
|
18
|
+
export * as Snapshot from "./Snapshot.ts";
|
|
19
|
+
export * as Stage from "./Stage.ts";
|
|
20
|
+
export * as Supervisor from "./Supervisor.ts";
|