effect-browser 0.2.0-beta.9 → 0.3.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +705 -1184
- package/dist/Browser-CM65Uo7f.mjs +437 -0
- package/dist/Browser-CM65Uo7f.mjs.map +1 -0
- package/dist/Browser.d.mts +115 -3
- package/dist/Browser.mjs +2 -22
- package/dist/BrowserError-DXXIA6Rk.d.mts +135 -0
- package/dist/BrowserError.d.mts +2 -0
- package/dist/BrowserError.mjs +202 -0
- package/dist/BrowserError.mjs.map +1 -0
- package/dist/BrowserEvent-DzdNdvoh.d.mts +277 -0
- package/dist/BrowserEvent.d.mts +2 -0
- package/dist/BrowserEvent.mjs +295 -0
- package/dist/BrowserEvent.mjs.map +1 -0
- package/dist/Cdp.d.mts +32 -0
- package/dist/Cdp.mjs +61 -0
- package/dist/Cdp.mjs.map +1 -0
- package/dist/Change-DNvztNLu.mjs +138 -0
- package/dist/Change-DNvztNLu.mjs.map +1 -0
- package/dist/Change-DVP2IJWm.d.mts +71 -0
- package/dist/Change.d.mts +2 -0
- package/dist/Change.mjs +2 -0
- package/dist/Chromium.d.mts +29 -81
- package/dist/Chromium.mjs +57 -392
- package/dist/Chromium.mjs.map +1 -1
- package/dist/Expect.d.mts +62 -0
- package/dist/Expect.mjs +114 -0
- package/dist/Expect.mjs.map +1 -0
- package/dist/Frame-BERmf_gM.d.mts +117 -0
- package/dist/Frame.d.mts +2 -0
- package/dist/Frame.mjs +123 -0
- package/dist/Frame.mjs.map +1 -0
- package/dist/Moment.d.mts +2 -0
- package/dist/Moment.mjs +288 -0
- package/dist/Moment.mjs.map +1 -0
- package/dist/Motion.d.mts +40 -0
- package/dist/Motion.mjs +122 -0
- package/dist/Motion.mjs.map +1 -0
- package/dist/Page-D4281dIi.mjs +618 -0
- package/dist/Page-D4281dIi.mjs.map +1 -0
- package/dist/Page-DYTdVhYa.d.mts +593 -0
- package/dist/Page.d.mts +2 -0
- package/dist/Page.mjs +2 -0
- package/dist/Plan-CNNOU1Tx.d.mts +112 -0
- package/dist/Plan-Dtrb-8im.mjs +311 -0
- package/dist/Plan-Dtrb-8im.mjs.map +1 -0
- package/dist/Plan.d.mts +2 -3
- package/dist/Plan.mjs +2 -2
- package/dist/Policy.d.mts +76 -0
- package/dist/Policy.mjs +237 -0
- package/dist/Policy.mjs.map +1 -0
- package/dist/Presentation.d.mts +48 -0
- package/dist/Presentation.mjs +117 -0
- package/dist/Presentation.mjs.map +1 -0
- package/dist/Snapshot-B34hZ4Jl.d.mts +68 -0
- package/dist/Snapshot.d.mts +2 -0
- package/dist/Snapshot.mjs +79 -0
- package/dist/Snapshot.mjs.map +1 -0
- package/dist/Stage.d.mts +54 -0
- package/dist/Stage.mjs +133 -0
- package/dist/Stage.mjs.map +1 -0
- package/dist/Supervisor-BtEG93gR.mjs +420 -0
- package/dist/Supervisor-BtEG93gR.mjs.map +1 -0
- package/dist/Supervisor-Im6-wbg5.d.mts +230 -0
- package/dist/Supervisor.d.mts +2 -0
- package/dist/Supervisor.mjs +2 -0
- package/dist/context-DAYjATgC.mjs +137 -0
- package/dist/context-DAYjATgC.mjs.map +1 -0
- package/dist/index.d.mts +17 -14
- package/dist/index.mjs +18 -13
- package/dist/match.inpage-xOe9LNO0.mjs +48 -0
- package/dist/match.inpage-xOe9LNO0.mjs.map +1 -0
- package/dist/page-Banfm_zc.mjs +5097 -0
- package/dist/page-Banfm_zc.mjs.map +1 -0
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/dist/url-CcL0K7S0.mjs +262 -0
- package/dist/url-CcL0K7S0.mjs.map +1 -0
- package/package.json +96 -61
- package/src/Browser.ts +625 -0
- package/src/BrowserError.ts +225 -0
- package/src/BrowserEvent.ts +294 -0
- package/src/Cdp.ts +101 -0
- package/src/Change.ts +88 -0
- package/src/Chromium.ts +102 -0
- package/src/Expect.ts +180 -0
- package/src/Frame.ts +130 -0
- package/src/Moment.ts +479 -0
- package/src/Motion.ts +166 -0
- package/src/Page.ts +520 -0
- package/src/Plan.ts +350 -0
- package/src/Policy.ts +325 -0
- package/src/Presentation.ts +195 -0
- package/src/Snapshot.ts +89 -0
- package/src/Stage.ts +223 -0
- package/src/Supervisor.ts +353 -0
- package/src/index.ts +20 -0
- package/src/internal/input/actions.ts +607 -0
- package/src/internal/input/dispatch.ts +161 -0
- package/src/internal/input/edit.inpage.ts +162 -0
- package/src/internal/input/evidence.inpage.ts +164 -0
- package/src/internal/input/guard.inpage.ts +605 -0
- package/src/internal/input/guard.ts +183 -0
- package/src/internal/input/keyboard.ts +112 -0
- package/src/internal/input/keys.ts +127 -0
- package/src/internal/input/perform.ts +322 -0
- package/src/internal/input/pointer.ts +153 -0
- package/src/internal/input/replies.ts +159 -0
- package/src/internal/input/settle.ts +50 -0
- package/src/internal/input/style.ts +186 -0
- package/src/internal/input/targets.inpage.ts +293 -0
- package/src/internal/input/targets.ts +148 -0
- package/src/internal/page/bridge.ts +386 -0
- package/src/internal/page/context.ts +302 -0
- package/src/internal/page/lane.ts +301 -0
- package/src/internal/page/navigation.ts +183 -0
- package/src/internal/page/openings.ts +87 -0
- package/src/internal/page/page.ts +137 -0
- package/src/internal/page/url.ts +100 -0
- package/src/internal/page/viewport.ts +43 -0
- package/src/internal/pictures/capture.ts +778 -0
- package/src/internal/pictures/clock.ts +141 -0
- package/src/internal/pictures/pictures.ts +520 -0
- package/src/internal/pictures/transport.ts +242 -0
- package/src/internal/reading/choose.ts +100 -0
- package/src/internal/reading/context.inpage.ts +201 -0
- package/src/internal/reading/controls.inpage.ts +158 -0
- package/src/internal/reading/expect.inpage.ts +70 -0
- package/src/internal/reading/match.inpage.ts +92 -0
- package/src/internal/reading/names.inpage.ts +306 -0
- package/src/internal/reading/outline.inpage.ts +260 -0
- package/src/internal/reading/reading.ts +223 -0
- package/src/internal/reading/ready.inpage.ts +154 -0
- package/src/internal/reading/ready.ts +105 -0
- package/src/internal/reading/subjects.inpage.ts +188 -0
- package/src/internal/reading/text.inpage.ts +135 -0
- package/src/internal/reading/walk.inpage.ts +220 -0
- package/src/internal/supervisor/lifecycle.ts +265 -0
- package/src/internal/timeline/changes.inpage.ts +216 -0
- package/src/internal/timeline/changes.ts +100 -0
- package/src/internal/timeline/events.ts +93 -0
- package/src/internal/timeline/fold.inpage.ts +123 -0
- package/src/internal/timeline/history.inpage.ts +229 -0
- package/src/internal/timeline/marks.inpage.ts +207 -0
- package/src/internal/timeline/record.inpage.ts +133 -0
- package/src/internal/timeline/sight.inpage.ts +122 -0
- package/src/internal/timeline/window.ts +96 -0
- package/src/internal/timeline/words.ts +65 -0
- package/dist/Bootstrap-B9UUXsvF.d.mts +0 -183
- package/dist/Bootstrap-CeQOWBy6.mjs +0 -189
- package/dist/Bootstrap-CeQOWBy6.mjs.map +0 -1
- package/dist/Bootstrap.d.mts +0 -3
- package/dist/Bootstrap.mjs +0 -2
- package/dist/Browser-BF9_sORP.d.mts +0 -388
- package/dist/Browser.mjs.map +0 -1
- package/dist/BrowserData-CPIIIHol.d.mts +0 -585
- package/dist/BrowserData.d.mts +0 -3
- package/dist/BrowserData.mjs +0 -695
- package/dist/BrowserData.mjs.map +0 -1
- package/dist/BrowserRuntime-B6nT1G4X.d.mts +0 -148
- package/dist/BrowserRuntime-C1r2aSRK.mjs +0 -6820
- package/dist/BrowserRuntime-C1r2aSRK.mjs.map +0 -1
- package/dist/BrowserRuntime.d.mts +0 -3
- package/dist/BrowserRuntime.mjs +0 -3
- package/dist/Capture.d.mts +0 -56
- package/dist/Capture.mjs +0 -678
- package/dist/Capture.mjs.map +0 -1
- package/dist/CaptureData-CYriZHX1.mjs +0 -201
- package/dist/CaptureData-CYriZHX1.mjs.map +0 -1
- package/dist/CaptureData.d.mts +0 -219
- package/dist/CaptureData.mjs +0 -2
- package/dist/Errors.d.mts +0 -158
- package/dist/Errors.mjs +0 -207
- package/dist/Errors.mjs.map +0 -1
- package/dist/Images-yCG9BmRU.mjs +0 -76
- package/dist/Images-yCG9BmRU.mjs.map +0 -1
- package/dist/OperationOptions-DAeRS6O9.mjs +0 -55
- package/dist/OperationOptions-DAeRS6O9.mjs.map +0 -1
- package/dist/PageControl.d.mts +0 -15
- package/dist/PageControl.mjs +0 -34
- package/dist/PageControl.mjs.map +0 -1
- package/dist/PageRegistry-B3q9DS3g.mjs +0 -164
- package/dist/PageRegistry-B3q9DS3g.mjs.map +0 -1
- package/dist/Plan-Dyxu2q8C.mjs +0 -543
- package/dist/Plan-Dyxu2q8C.mjs.map +0 -1
- package/dist/PlanData-B668RxbQ.d.mts +0 -4831
- package/dist/PlanData.d.mts +0 -3
- package/dist/PlanData.mjs +0 -268
- package/dist/PlanData.mjs.map +0 -1
- package/dist/PlanInput-BJcPPncs.mjs +0 -75
- package/dist/PlanInput-BJcPPncs.mjs.map +0 -1
- package/dist/PublicSession-DCSMMqOj.mjs +0 -5602
- package/dist/PublicSession-DCSMMqOj.mjs.map +0 -1
- package/dist/Testing.d.mts +0 -479
- package/dist/Testing.mjs +0 -2147
- package/dist/Testing.mjs.map +0 -1
- package/dist/Timeline-fV22Xk-S.d.mts +0 -29
- package/dist/Timeline.d.mts +0 -4
- package/dist/Timeline.mjs +0 -20
- package/dist/Timeline.mjs.map +0 -1
- package/dist/TimelineData-BsOn-YGZ.d.mts +0 -3818
- package/dist/TimelineData.d.mts +0 -3
- package/dist/TimelineData.mjs +0 -482
- package/dist/TimelineData.mjs.map +0 -1
package/dist/Stage.d.mts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { t as BrowserError } from "./BrowserError-DXXIA6Rk.mjs";
|
|
2
|
+
import { n as CaptureStats, o as ScreencastOptions, r as Frame } from "./Frame-BERmf_gM.mjs";
|
|
3
|
+
import { u as Page } from "./Page-DYTdVhYa.mjs";
|
|
4
|
+
import { Duration, Effect, Option, Schema, Stream } from "effect";
|
|
5
|
+
declare namespace Stage_d_exports {
|
|
6
|
+
export { Options, Presented, Stage, make };
|
|
7
|
+
}
|
|
8
|
+
export interface Options extends ScreencastOptions {
|
|
9
|
+
/** How long a switch waits for the new page's first frame. Defaults to 5 seconds. */
|
|
10
|
+
readonly firstFrame?: Duration.Input | undefined;
|
|
11
|
+
}
|
|
12
|
+
declare const Presented_base: Schema.Class<Presented, Schema.Struct<{
|
|
13
|
+
readonly page: Schema.String;
|
|
14
|
+
/**
|
|
15
|
+
* When it took effect, in host monotonic milliseconds on the frame clock: no earlier than the
|
|
16
|
+
* time asked for, the frames it followed or the new page's first.
|
|
17
|
+
*/
|
|
18
|
+
readonly at: Schema.Finite;
|
|
19
|
+
/** How long after the time asked for it took effect. */
|
|
20
|
+
readonly latency: Schema.Finite;
|
|
21
|
+
}>, {}>;
|
|
22
|
+
/** A switch that took effect. */
|
|
23
|
+
export declare class Presented extends Presented_base {}
|
|
24
|
+
export interface Stage {
|
|
25
|
+
readonly present: (page: Page, options?: {
|
|
26
|
+
readonly at?: number | undefined;
|
|
27
|
+
}) => Effect.Effect<Presented, BrowserError>;
|
|
28
|
+
/**
|
|
29
|
+
* The presented page's frames, each naming its page, session, document and address. A reader
|
|
30
|
+
* more than 16 frames behind loses the oldest.
|
|
31
|
+
*/
|
|
32
|
+
readonly frames: Stream.Stream<Frame>;
|
|
33
|
+
readonly current: Effect.Effect<Option.Option<Page>>;
|
|
34
|
+
/**
|
|
35
|
+
* The presented page's capture counts over the latest `window`, at most a minute: its frames,
|
|
36
|
+
* their largest gap, and those filtered out, as another capture's or a crop's.
|
|
37
|
+
*/
|
|
38
|
+
readonly stats: (options?: {
|
|
39
|
+
readonly window?: Duration.Input | undefined;
|
|
40
|
+
}) => Effect.Effect<Option.Option<CaptureStats>, BrowserError>;
|
|
41
|
+
}
|
|
42
|
+
export declare const make: (options?: Options | undefined) => Effect.Effect<{
|
|
43
|
+
present: (page: Page, options?: {
|
|
44
|
+
readonly at?: number | undefined;
|
|
45
|
+
}) => Effect.Effect<Presented, BrowserError>;
|
|
46
|
+
frames: Stream.Stream<Frame, never, never>;
|
|
47
|
+
current: Effect.Effect<Option.Option<Page>, never, never>;
|
|
48
|
+
stats: (statsOptions?: {
|
|
49
|
+
readonly window?: Duration.Input | undefined;
|
|
50
|
+
} | undefined) => Effect.Effect<Option.Option<CaptureStats>, BrowserError, never>;
|
|
51
|
+
}, BrowserError, import("effect/Scope").Scope>;
|
|
52
|
+
//#endregion
|
|
53
|
+
export { Stage_d_exports as t };
|
|
54
|
+
//# sourceMappingURL=Stage.d.mts.map
|
package/dist/Stage.mjs
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
|
|
2
|
+
import { InvalidRequest, Timeout, consequence } from "./BrowserError.mjs";
|
|
3
|
+
import { i as failWith } from "./context-DAYjATgC.mjs";
|
|
4
|
+
import { t as internalsOf } from "./page-Banfm_zc.mjs";
|
|
5
|
+
import { Clock, Deferred, Duration, Effect, Exit, Fiber, Option, PubSub, Schedule, Schema, Semaphore, Stream } from "effect";
|
|
6
|
+
//#region src/Stage.ts
|
|
7
|
+
/**
|
|
8
|
+
* The stage: whichever page's capture feeds a live output, switched between pages in one browser
|
|
9
|
+
* session or across two, each switch stamped with when it took effect on the frame clock. Make one
|
|
10
|
+
* per live output.
|
|
11
|
+
*
|
|
12
|
+
* `present(page, { at })` switches the output to `page` at `at`, host monotonic milliseconds on the
|
|
13
|
+
* clock that times frames, or at once. It starts the new page's capture ahead, waits for its first
|
|
14
|
+
* frame, then for any input already under way on the old page, and turns: from then on `frames`
|
|
15
|
+
* carries the new page's frames, its newest first, and the old page's capture stops. The captures
|
|
16
|
+
* overlap, so a switch shows no dark spell; on Browserbase two ran in one session at the frame
|
|
17
|
+
* rate of one. A first frame that does not come in time fails `present`, and the old page stays. A
|
|
18
|
+
* capture that fails restarts on the same page while its page and browser stand.
|
|
19
|
+
*
|
|
20
|
+
* What a live output does with frames stays the application's: a delay line, a liveness rule,
|
|
21
|
+
* redaction and repeating a held frame. A still page sends no frames until it changes, so the frame
|
|
22
|
+
* a switch begins with may be older than the frames before it; `Presented.at` says when it took
|
|
23
|
+
* effect.
|
|
24
|
+
*
|
|
25
|
+
* @since 0.3.0
|
|
26
|
+
*/
|
|
27
|
+
var Stage_exports = /* @__PURE__ */ __exportAll({
|
|
28
|
+
Presented: () => Presented,
|
|
29
|
+
make: () => make
|
|
30
|
+
});
|
|
31
|
+
/** A switch that took effect. */
|
|
32
|
+
var Presented = class extends Schema.Class("effect-browser/Presented")({
|
|
33
|
+
page: Schema.String,
|
|
34
|
+
/**
|
|
35
|
+
* When it took effect, in host monotonic milliseconds on the frame clock: no earlier than the
|
|
36
|
+
* time asked for, the frames it followed or the new page's first.
|
|
37
|
+
*/
|
|
38
|
+
at: Schema.Finite,
|
|
39
|
+
/** How long after the time asked for it took effect. */
|
|
40
|
+
latency: Schema.Finite
|
|
41
|
+
}) {};
|
|
42
|
+
const restartable = (error) => {
|
|
43
|
+
const { lost, repeat } = consequence(error);
|
|
44
|
+
return lost === "nothing" && repeat !== "pointless";
|
|
45
|
+
};
|
|
46
|
+
const make = Effect.fn("Stage.make")(function* (options = {}) {
|
|
47
|
+
const clock = yield* Clock.Clock;
|
|
48
|
+
const scope = yield* Effect.scope;
|
|
49
|
+
const now = () => Number(clock.monotonicTimeNanosUnsafe()) / 1e6;
|
|
50
|
+
const waited = Duration.fromInput(options.firstFrame ?? Duration.seconds(5));
|
|
51
|
+
if (Option.isNone(waited) || !Duration.isPositive(waited.value)) return yield* failWith("stage", new InvalidRequest({ detail: "firstFrame must be a positive duration" }));
|
|
52
|
+
const firstFrame = waited.value;
|
|
53
|
+
const settings = {
|
|
54
|
+
quality: options.quality,
|
|
55
|
+
size: options.size
|
|
56
|
+
};
|
|
57
|
+
const output = yield* PubSub.sliding(16);
|
|
58
|
+
const switching = yield* Semaphore.make(1);
|
|
59
|
+
let shown;
|
|
60
|
+
let painted = Number.NEGATIVE_INFINITY;
|
|
61
|
+
const feed = (page) => Effect.gen(function* () {
|
|
62
|
+
const fed = {
|
|
63
|
+
page,
|
|
64
|
+
first: yield* Deferred.make(),
|
|
65
|
+
latest: void 0,
|
|
66
|
+
live: false,
|
|
67
|
+
fiber: void 0
|
|
68
|
+
};
|
|
69
|
+
fed.fiber = yield* page.screencast(settings).pipe(Stream.runForEach((frame) => Effect.sync(() => {
|
|
70
|
+
fed.latest = frame;
|
|
71
|
+
if (fed.live) {
|
|
72
|
+
painted = frame.hostTime;
|
|
73
|
+
PubSub.publishUnsafe(output, frame);
|
|
74
|
+
}
|
|
75
|
+
Deferred.doneUnsafe(fed.first, Exit.void);
|
|
76
|
+
})), Effect.retry({
|
|
77
|
+
while: restartable,
|
|
78
|
+
schedule: Schedule.spaced(Duration.millis(500))
|
|
79
|
+
}), Effect.forkIn(scope));
|
|
80
|
+
return fed;
|
|
81
|
+
});
|
|
82
|
+
const stop = (fed) => Effect.suspend(() => {
|
|
83
|
+
fed.live = false;
|
|
84
|
+
return fed.fiber === void 0 ? Effect.void : Fiber.interrupt(fed.fiber);
|
|
85
|
+
});
|
|
86
|
+
const firstOf = (fed) => Effect.raceFirst(Deferred.await(fed.first), fed.fiber === void 0 ? Effect.never : Effect.andThen(Fiber.join(fed.fiber), Effect.never)).pipe(Effect.timeoutOrElse({
|
|
87
|
+
duration: firstFrame,
|
|
88
|
+
orElse: () => failWith("present", new Timeout({ millis: Duration.toMillis(firstFrame) }))
|
|
89
|
+
}));
|
|
90
|
+
const until = (at) => Effect.suspend(() => at > now() ? Effect.andThen(Effect.sleep(Duration.millis(Math.ceil(at - now()))), until(at)) : Effect.void);
|
|
91
|
+
const present = (page, presentOptions = {}) => switching.withPermits(1)(Effect.gen(function* () {
|
|
92
|
+
const asked = presentOptions.at ?? now();
|
|
93
|
+
const old = shown;
|
|
94
|
+
if (old?.feed.page === page) return old.presented;
|
|
95
|
+
const fed = yield* feed(page);
|
|
96
|
+
yield* Effect.gen(function* () {
|
|
97
|
+
yield* firstOf(fed);
|
|
98
|
+
yield* until(asked);
|
|
99
|
+
if (old !== void 0) yield* Effect.ignore(internalsOf(old.feed.page)?.inputEnded ?? Effect.void);
|
|
100
|
+
}).pipe(Effect.onError(() => stop(fed)));
|
|
101
|
+
const presented = yield* Effect.sync(() => {
|
|
102
|
+
const frame = fed.latest;
|
|
103
|
+
const at = Math.max(asked, frame?.hostTime ?? asked, painted);
|
|
104
|
+
if (old !== void 0) old.feed.live = false;
|
|
105
|
+
fed.live = true;
|
|
106
|
+
if (frame !== void 0) {
|
|
107
|
+
painted = frame.hostTime;
|
|
108
|
+
PubSub.publishUnsafe(output, frame);
|
|
109
|
+
}
|
|
110
|
+
shown = {
|
|
111
|
+
feed: fed,
|
|
112
|
+
presented: new Presented({
|
|
113
|
+
page: page.id,
|
|
114
|
+
at,
|
|
115
|
+
latency: at - asked
|
|
116
|
+
})
|
|
117
|
+
};
|
|
118
|
+
return shown.presented;
|
|
119
|
+
});
|
|
120
|
+
if (old !== void 0) yield* stop(old.feed);
|
|
121
|
+
return presented;
|
|
122
|
+
}).pipe(Effect.provideService(Clock.Clock, clock)));
|
|
123
|
+
return {
|
|
124
|
+
present,
|
|
125
|
+
frames: Stream.fromPubSub(output),
|
|
126
|
+
current: Effect.sync(() => Option.fromNullishOr(shown?.feed.page)),
|
|
127
|
+
stats: (statsOptions = {}) => shown === void 0 ? Effect.succeedNone : Effect.asSome(shown.feed.page.captureStats({ window: statsOptions.window ?? "1 second" }))
|
|
128
|
+
};
|
|
129
|
+
});
|
|
130
|
+
//#endregion
|
|
131
|
+
export { Presented, make, Stage_exports as t };
|
|
132
|
+
|
|
133
|
+
//# sourceMappingURL=Stage.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Stage.mjs","names":[],"sources":["../src/Stage.ts"],"sourcesContent":["/**\n * The stage: whichever page's capture feeds a live output, switched between pages in one browser\n * session or across two, each switch stamped with when it took effect on the frame clock. Make one\n * per live output.\n *\n * `present(page, { at })` switches the output to `page` at `at`, host monotonic milliseconds on the\n * clock that times frames, or at once. It starts the new page's capture ahead, waits for its first\n * frame, then for any input already under way on the old page, and turns: from then on `frames`\n * carries the new page's frames, its newest first, and the old page's capture stops. The captures\n * overlap, so a switch shows no dark spell; on Browserbase two ran in one session at the frame\n * rate of one. A first frame that does not come in time fails `present`, and the old page stays. A\n * capture that fails restarts on the same page while its page and browser stand.\n *\n * What a live output does with frames stays the application's: a delay line, a liveness rule,\n * redaction and repeating a held frame. A still page sends no frames until it changes, so the frame\n * a switch begins with may be older than the frames before it; `Presented.at` says when it took\n * effect.\n *\n * @since 0.3.0\n */\nimport {\n Clock,\n Deferred,\n Duration,\n Effect,\n Exit,\n Fiber,\n Option,\n PubSub,\n Schedule,\n Schema,\n Semaphore,\n Stream,\n} from \"effect\";\n\nimport { type BrowserError, consequence, InvalidRequest, Timeout } from \"./BrowserError.ts\";\nimport type { CaptureStats, Frame, ScreencastOptions } from \"./Frame.ts\";\nimport { failWith } from \"./internal/page/context.ts\";\nimport { internalsOf } from \"./internal/page/page.ts\";\nimport type { Page } from \"./Page.ts\";\n\nexport interface Options extends ScreencastOptions {\n /** How long a switch waits for the new page's first frame. Defaults to 5 seconds. */\n readonly firstFrame?: Duration.Input | undefined;\n}\n\n/** A switch that took effect. */\nexport class Presented extends Schema.Class<Presented>(\"effect-browser/Presented\")({\n page: Schema.String,\n /**\n * When it took effect, in host monotonic milliseconds on the frame clock: no earlier than the\n * time asked for, the frames it followed or the new page's first.\n */\n at: Schema.Finite,\n /** How long after the time asked for it took effect. */\n latency: Schema.Finite,\n}) {}\n\nexport interface Stage {\n readonly present: (\n page: Page,\n options?: { readonly at?: number | undefined },\n ) => Effect.Effect<Presented, BrowserError>;\n /**\n * The presented page's frames, each naming its page, session, document and address. A reader\n * more than 16 frames behind loses the oldest.\n */\n readonly frames: Stream.Stream<Frame>;\n readonly current: Effect.Effect<Option.Option<Page>>;\n /**\n * The presented page's capture counts over the latest `window`, at most a minute: its frames,\n * their largest gap, and those filtered out, as another capture's or a crop's.\n */\n readonly stats: (options?: {\n readonly window?: Duration.Input | undefined;\n }) => Effect.Effect<Option.Option<CaptureStats>, BrowserError>;\n}\n\n/** A page's capture for the stage: its newest frame, and whether its frames go out. */\ninterface Feed {\n readonly page: Page;\n readonly first: Deferred.Deferred<void>;\n latest: Frame | undefined;\n live: boolean;\n fiber: Fiber.Fiber<void, BrowserError> | undefined;\n}\n\n// A capture restarts unless its page or browser is gone, or it would fail the same way again.\nconst restartable = (error: BrowserError) => {\n const { lost, repeat } = consequence(error);\n\n return lost === \"nothing\" && repeat !== \"pointless\";\n};\n\nexport const make = Effect.fn(\"Stage.make\")(function* (options: Options = {}) {\n const clock = yield* Clock.Clock;\n const scope = yield* Effect.scope;\n const now = () => Number(clock.monotonicTimeNanosUnsafe()) / 1e6;\n const waited = Duration.fromInput(options.firstFrame ?? Duration.seconds(5));\n\n if (Option.isNone(waited) || !Duration.isPositive(waited.value))\n return yield* failWith(\n \"stage\",\n new InvalidRequest({ detail: \"firstFrame must be a positive duration\" }),\n );\n const firstFrame = waited.value;\n const settings = { quality: options.quality, size: options.size };\n const output = yield* PubSub.sliding<Frame>(16);\n const switching = yield* Semaphore.make(1);\n // What is on the stage, which only `present` changes, and the paint of the latest frame out.\n let shown: { readonly feed: Feed; readonly presented: Presented } | undefined;\n let painted = Number.NEGATIVE_INFINITY;\n\n const feed = (page: Page) =>\n Effect.gen(function* () {\n const fed: Feed = {\n page,\n first: yield* Deferred.make<void>(),\n latest: undefined,\n live: false,\n fiber: undefined,\n };\n\n fed.fiber = yield* page.screencast(settings).pipe(\n Stream.runForEach((frame) =>\n Effect.sync(() => {\n fed.latest = frame;\n if (fed.live) {\n painted = frame.hostTime;\n PubSub.publishUnsafe(output, frame);\n }\n // Last: a switch waiting for this frame may turn, and send it, at once.\n Deferred.doneUnsafe(fed.first, Exit.void);\n }),\n ),\n Effect.retry({ while: restartable, schedule: Schedule.spaced(Duration.millis(500)) }),\n Effect.forkIn(scope),\n );\n\n return fed;\n });\n\n // No frame of it goes out once this returns, and its capture stops.\n const stop = (fed: Feed) =>\n Effect.suspend(() => {\n fed.live = false;\n\n return fed.fiber === undefined ? Effect.void : Fiber.interrupt(fed.fiber);\n });\n\n // The new page's first frame; its capture's own failure, or `Timeout`.\n const firstOf = (fed: Feed) =>\n Effect.raceFirst(\n Deferred.await(fed.first),\n fed.fiber === undefined ? Effect.never : Effect.andThen(Fiber.join(fed.fiber), Effect.never),\n ).pipe(\n Effect.timeoutOrElse({\n duration: firstFrame,\n orElse: () => failWith(\"present\", new Timeout({ millis: Duration.toMillis(firstFrame) })),\n }),\n );\n\n // A timer can fire a fraction of a millisecond early, so the wait looks again.\n const until = (at: number): Effect.Effect<void> =>\n Effect.suspend(() =>\n at > now()\n ? Effect.andThen(Effect.sleep(Duration.millis(Math.ceil(at - now()))), until(at))\n : Effect.void,\n );\n\n const present: Stage[\"present\"] = (page, presentOptions = {}) =>\n switching.withPermits(1)(\n Effect.gen(function* () {\n const asked = presentOptions.at ?? now();\n const old = shown;\n\n if (old?.feed.page === page) return old.presented;\n const fed = yield* feed(page);\n\n yield* Effect.gen(function* () {\n yield* firstOf(fed);\n yield* until(asked);\n // Input under way on the old page ends first, within its action timeout.\n if (old !== undefined)\n yield* Effect.ignore(internalsOf(old.feed.page)?.inputEnded ?? Effect.void);\n }).pipe(Effect.onError(() => stop(fed)));\n\n const presented = yield* Effect.sync(() => {\n const frame = fed.latest;\n const at = Math.max(asked, frame?.hostTime ?? asked, painted);\n\n if (old !== undefined) old.feed.live = false;\n fed.live = true;\n if (frame !== undefined) {\n painted = frame.hostTime;\n PubSub.publishUnsafe(output, frame);\n }\n shown = {\n feed: fed,\n presented: new Presented({ page: page.id, at, latency: at - asked }),\n };\n\n return shown.presented;\n });\n\n if (old !== undefined) yield* stop(old.feed);\n\n return presented;\n }).pipe(Effect.provideService(Clock.Clock, clock)),\n );\n\n return {\n present,\n frames: Stream.fromPubSub(output),\n current: Effect.sync(() => Option.fromNullishOr(shown?.feed.page)),\n stats: (statsOptions = {}) =>\n shown === undefined\n ? Effect.succeedNone\n : Effect.asSome(\n shown.feed.page.captureStats({ window: statsOptions.window ?? \"1 second\" }),\n ),\n } satisfies Stage;\n});\n"],"mappings":"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"}
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
|
|
2
|
+
import { DisconnectCause } from "./BrowserEvent.mjs";
|
|
3
|
+
import { Cause, Clock, DateTime, Duration, Effect, Exit, Fiber, FiberHandle, FiberMap, Option, Predicate, PubSub, Schedule, Schema, Scope, Stream, SubscriptionRef } from "effect";
|
|
4
|
+
//#region src/internal/supervisor/lifecycle.ts
|
|
5
|
+
/**
|
|
6
|
+
* A supervisor's life as one state and one transition function, and the generation states it
|
|
7
|
+
* publishes. The transition has no effects: `Supervisor.make` applies each input under one lock,
|
|
8
|
+
* publishes the events in order and runs the commands, so the rules here are tested alone.
|
|
9
|
+
*/
|
|
10
|
+
/** The provider confirmed that the generation's browser ended. */
|
|
11
|
+
var Settled = class extends Schema.TaggedClass()("Settled", {}) {};
|
|
12
|
+
/** The provider could not confirm the end by its deadline, so the browser may still run, and bill. */
|
|
13
|
+
var Unconfirmed = class extends Schema.TaggedClass()("Unconfirmed", { detail: Schema.String }) {};
|
|
14
|
+
const Released = Schema.Union([Settled, Unconfirmed]);
|
|
15
|
+
/** Opening, but not after a loss: the first generation, a rotation, or the one after `Down`. */
|
|
16
|
+
var Opening = class extends Schema.TaggedClass()("Opening", {}) {};
|
|
17
|
+
/** Opening after the generation before it was lost. */
|
|
18
|
+
var Reopening = class extends Schema.TaggedClass()("Reopening", {}) {};
|
|
19
|
+
/** Open: `browser` gives this generation until the next one opens. */
|
|
20
|
+
var Open = class extends Schema.TaggedClass()("Open", {}) {};
|
|
21
|
+
/** Lost: its connection dropped, or its provider ended its session, as `cause` says. */
|
|
22
|
+
var Lost = class extends Schema.TaggedClass()("Lost", { cause: DisconnectCause }) {};
|
|
23
|
+
/**
|
|
24
|
+
* It never opened: its last try failed with `cause`, which the provider deems definite, or after
|
|
25
|
+
* which the reopen schedule gave up.
|
|
26
|
+
*/
|
|
27
|
+
var Down = class extends Schema.TaggedClass()("Down", {
|
|
28
|
+
detail: Schema.String,
|
|
29
|
+
cause: Schema.Defect()
|
|
30
|
+
}) {};
|
|
31
|
+
/** It ended. `released` says how its release went; one retired while opening has none. */
|
|
32
|
+
var Closed = class extends Schema.TaggedClass()("Closed", { released: Schema.optional(Released) }) {};
|
|
33
|
+
/**
|
|
34
|
+
* It was serving as the supervisor's scope closed, and was left running, not released: its scope
|
|
35
|
+
* closed, and its provider runs it on, and bills, until something adopts and releases it.
|
|
36
|
+
*/
|
|
37
|
+
var Kept = class extends Schema.TaggedClass()("Kept", {}) {};
|
|
38
|
+
const GenerationState = Schema.Union([
|
|
39
|
+
Opening,
|
|
40
|
+
Reopening,
|
|
41
|
+
Open,
|
|
42
|
+
Lost,
|
|
43
|
+
Down,
|
|
44
|
+
Closed,
|
|
45
|
+
Kept
|
|
46
|
+
]);
|
|
47
|
+
const start = () => ({
|
|
48
|
+
state: {
|
|
49
|
+
_tag: "Opening",
|
|
50
|
+
number: 1,
|
|
51
|
+
serving: void 0
|
|
52
|
+
},
|
|
53
|
+
events: [[1, new Opening()]],
|
|
54
|
+
commands: [{
|
|
55
|
+
_tag: "Open",
|
|
56
|
+
number: 1,
|
|
57
|
+
after: void 0
|
|
58
|
+
}]
|
|
59
|
+
});
|
|
60
|
+
const servingOf = (state) => state._tag === "Retired" ? void 0 : state.serving;
|
|
61
|
+
/** The state after `input`, with the events to publish and the commands to run. */
|
|
62
|
+
const transition = (state, input, exclusive) => {
|
|
63
|
+
const release = (live) => ({
|
|
64
|
+
_tag: "Release",
|
|
65
|
+
live
|
|
66
|
+
});
|
|
67
|
+
const stay = {
|
|
68
|
+
state,
|
|
69
|
+
events: [],
|
|
70
|
+
commands: []
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Open `number`, ending `current` now if it `ends`; exclusive generations open only once its
|
|
74
|
+
* release has finished. Otherwise `current` serves until the next one opens.
|
|
75
|
+
*/
|
|
76
|
+
const next = (number, reopen, current, ends) => ({
|
|
77
|
+
state: {
|
|
78
|
+
_tag: "Opening",
|
|
79
|
+
number,
|
|
80
|
+
serving: ends ? void 0 : current
|
|
81
|
+
},
|
|
82
|
+
events: [[number, reopen ? new Reopening() : new Opening()]],
|
|
83
|
+
commands: [...ends && current !== void 0 ? [release(current)] : [], {
|
|
84
|
+
_tag: "Open",
|
|
85
|
+
number,
|
|
86
|
+
after: ends && exclusive ? current?.number : void 0
|
|
87
|
+
}]
|
|
88
|
+
});
|
|
89
|
+
switch (input._tag) {
|
|
90
|
+
case "Opened": {
|
|
91
|
+
const { live } = input;
|
|
92
|
+
if (state._tag !== "Opening" || state.number !== live.number) return {
|
|
93
|
+
...stay,
|
|
94
|
+
commands: [release(live)]
|
|
95
|
+
};
|
|
96
|
+
return {
|
|
97
|
+
state: {
|
|
98
|
+
_tag: "Open",
|
|
99
|
+
serving: live
|
|
100
|
+
},
|
|
101
|
+
events: [[live.number, new Open()]],
|
|
102
|
+
commands: [{
|
|
103
|
+
_tag: "Watch",
|
|
104
|
+
live
|
|
105
|
+
}, ...state.serving === void 0 ? [] : [release(state.serving)]]
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
case "Failed": {
|
|
109
|
+
const { number, detail, cause } = input;
|
|
110
|
+
const events = [[number, new Down({
|
|
111
|
+
detail,
|
|
112
|
+
cause
|
|
113
|
+
})]];
|
|
114
|
+
if (state._tag !== "Opening" || state.number !== input.number) return {
|
|
115
|
+
...stay,
|
|
116
|
+
events
|
|
117
|
+
};
|
|
118
|
+
return {
|
|
119
|
+
state: {
|
|
120
|
+
_tag: "Down",
|
|
121
|
+
number,
|
|
122
|
+
detail,
|
|
123
|
+
cause,
|
|
124
|
+
serving: state.serving
|
|
125
|
+
},
|
|
126
|
+
events,
|
|
127
|
+
commands: []
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
case "Abandoned": return {
|
|
131
|
+
...stay,
|
|
132
|
+
events: [[input.number, new Closed({})]]
|
|
133
|
+
};
|
|
134
|
+
case "Released": return {
|
|
135
|
+
...stay,
|
|
136
|
+
events: [[input.number, new Closed({ released: input.released })]]
|
|
137
|
+
};
|
|
138
|
+
case "Lost": {
|
|
139
|
+
const lost = servingOf(state);
|
|
140
|
+
if (state._tag === "Retired" || lost?.number !== input.number) return stay;
|
|
141
|
+
const events = [[lost.number, new Lost({ cause: input.cause })]];
|
|
142
|
+
if (state._tag === "Opening") return {
|
|
143
|
+
state: {
|
|
144
|
+
...state,
|
|
145
|
+
serving: void 0
|
|
146
|
+
},
|
|
147
|
+
events,
|
|
148
|
+
commands: [release(lost)]
|
|
149
|
+
};
|
|
150
|
+
const reopened = next((state._tag === "Open" ? lost.number : state.number) + 1, true, lost, true);
|
|
151
|
+
return {
|
|
152
|
+
...reopened,
|
|
153
|
+
events: [...events, ...reopened.events]
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
case "Rotate": {
|
|
157
|
+
if (state._tag === "Retired") return stay;
|
|
158
|
+
if (input.due !== void 0 && (state._tag !== "Open" || state.serving.number !== input.due)) return stay;
|
|
159
|
+
if (state._tag === "Opening") return {
|
|
160
|
+
...stay,
|
|
161
|
+
reply: state.number
|
|
162
|
+
};
|
|
163
|
+
const number = (state._tag === "Open" ? state.serving.number : state.number) + 1;
|
|
164
|
+
return {
|
|
165
|
+
...next(number, false, state.serving, exclusive),
|
|
166
|
+
reply: number
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
case "Retire":
|
|
170
|
+
case "Keep": {
|
|
171
|
+
const current = servingOf(state);
|
|
172
|
+
if (state._tag === "Retired") return stay;
|
|
173
|
+
if (current === void 0) return {
|
|
174
|
+
state: { _tag: "Retired" },
|
|
175
|
+
events: [],
|
|
176
|
+
commands: []
|
|
177
|
+
};
|
|
178
|
+
return input._tag === "Retire" ? {
|
|
179
|
+
state: { _tag: "Retired" },
|
|
180
|
+
events: [],
|
|
181
|
+
commands: [release(current)]
|
|
182
|
+
} : {
|
|
183
|
+
state: { _tag: "Retired" },
|
|
184
|
+
events: [[current.number, new Kept()]],
|
|
185
|
+
commands: [{
|
|
186
|
+
_tag: "Leave",
|
|
187
|
+
live: current
|
|
188
|
+
}]
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
/**
|
|
194
|
+
* What a caller waiting for generation `target`, or for any when it is undefined, gets in
|
|
195
|
+
* `state`; undefined while it should go on waiting.
|
|
196
|
+
*/
|
|
197
|
+
const resolve = (state, target) => {
|
|
198
|
+
const serve = (live) => ({
|
|
199
|
+
_tag: "Serve",
|
|
200
|
+
live
|
|
201
|
+
});
|
|
202
|
+
switch (state._tag) {
|
|
203
|
+
case "Retired": return {
|
|
204
|
+
_tag: "Unavailable",
|
|
205
|
+
reason: "retired",
|
|
206
|
+
detail: "the supervisor was retired"
|
|
207
|
+
};
|
|
208
|
+
case "Open": return target === void 0 || state.serving.number >= target ? serve(state.serving) : void 0;
|
|
209
|
+
case "Opening": return target === void 0 && state.serving !== void 0 ? serve(state.serving) : void 0;
|
|
210
|
+
case "Down":
|
|
211
|
+
if (target === void 0 && state.serving !== void 0) return serve(state.serving);
|
|
212
|
+
return target === void 0 || state.number >= target ? {
|
|
213
|
+
_tag: "Unavailable",
|
|
214
|
+
reason: "down",
|
|
215
|
+
detail: state.detail,
|
|
216
|
+
cause: state.cause
|
|
217
|
+
} : void 0;
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
//#endregion
|
|
221
|
+
//#region src/Supervisor.ts
|
|
222
|
+
/**
|
|
223
|
+
* A browser kept open across losses and session ends, as a series of generations, each a new
|
|
224
|
+
* browser from the same provider. Pages don't carry over from one generation to the next.
|
|
225
|
+
*
|
|
226
|
+
* `make` opens the first generation at once with the provider's `open`, in a scope of its own, so
|
|
227
|
+
* a caller that stops waiting never interrupts a half-open browser. `browser` gives the current
|
|
228
|
+
* generation; callers wait, bounded, and share one open. A loss is published at once and the next
|
|
229
|
+
* generation opens on the `reopen` schedule, which also retries a failed open, unless the provider
|
|
230
|
+
* deems its failure `definite`: that goes `Down` at once, with its cause. `rotate` opens the
|
|
231
|
+
* next generation now, and so does the time `rotateBefore` ahead of a generation's `expiresAt`. A
|
|
232
|
+
* rotation makes the next generation before it breaks the current one, unless generations are
|
|
233
|
+
* `exclusive`, as sessions saving to one stored context must be: then the current one is released
|
|
234
|
+
* first. `retire` stops reopening at once and releases what is open; the scope's close retires
|
|
235
|
+
* too, unless the supervisor `keep`s: then it leaves the serving generation running, `Kept`. `states`
|
|
236
|
+
* publishes each generation's changes, with the provider's release outcome last.
|
|
237
|
+
*
|
|
238
|
+
* Providers supply the specifics: `Browserbase.supervise` opens hosted sessions this way.
|
|
239
|
+
*
|
|
240
|
+
* @since 0.3.0
|
|
241
|
+
*/
|
|
242
|
+
var Supervisor_exports = /* @__PURE__ */ __exportAll({
|
|
243
|
+
Closed: () => Closed,
|
|
244
|
+
Down: () => Down,
|
|
245
|
+
Generation: () => Generation,
|
|
246
|
+
GenerationState: () => GenerationState,
|
|
247
|
+
Kept: () => Kept,
|
|
248
|
+
Lost: () => Lost,
|
|
249
|
+
Open: () => Open,
|
|
250
|
+
Opening: () => Opening,
|
|
251
|
+
Released: () => Released,
|
|
252
|
+
Reopening: () => Reopening,
|
|
253
|
+
Settled: () => Settled,
|
|
254
|
+
Unavailable: () => Unavailable,
|
|
255
|
+
Unconfirmed: () => Unconfirmed,
|
|
256
|
+
make: () => make
|
|
257
|
+
});
|
|
258
|
+
/** One change in a generation's life. `at` is wall-clock epoch milliseconds from the Effect `Clock`. */
|
|
259
|
+
var Generation = class extends Schema.Class("effect-browser/Generation")({
|
|
260
|
+
number: Schema.Int.check(Schema.isGreaterThan(0)),
|
|
261
|
+
at: Schema.Finite,
|
|
262
|
+
state: GenerationState
|
|
263
|
+
}) {};
|
|
264
|
+
/**
|
|
265
|
+
* No browser to give: none opened within `waitTimeout` (`opening`), the latest generation failed
|
|
266
|
+
* to open (`down`, with what it failed with as `cause`), or the supervisor was retired.
|
|
267
|
+
*/
|
|
268
|
+
var Unavailable = class extends Schema.TaggedError()("Unavailable", {
|
|
269
|
+
reason: Schema.Literals([
|
|
270
|
+
"opening",
|
|
271
|
+
"down",
|
|
272
|
+
"retired"
|
|
273
|
+
]),
|
|
274
|
+
detail: Schema.String,
|
|
275
|
+
cause: Schema.optional(Schema.Defect())
|
|
276
|
+
}) {
|
|
277
|
+
get message() {
|
|
278
|
+
return `no browser: ${this.detail}`;
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
const retired = () => new Unavailable({
|
|
282
|
+
reason: "retired",
|
|
283
|
+
detail: "the supervisor was retired"
|
|
284
|
+
});
|
|
285
|
+
const describe = (cause) => {
|
|
286
|
+
const error = Cause.squash(cause);
|
|
287
|
+
return Predicate.isError(error) ? error.message : String(error);
|
|
288
|
+
};
|
|
289
|
+
/**
|
|
290
|
+
* Wait up to `waitTimeout` for generation `target`, or for any when it is undefined, as `state`
|
|
291
|
+
* changes.
|
|
292
|
+
*/
|
|
293
|
+
const waitFor = (state, waitTimeout, target) => SubscriptionRef.changes(state).pipe(Stream.map((current) => resolve(current, target)), Stream.filter(Predicate.isNotUndefined), Stream.runHead, Effect.flatMap(Option.match({
|
|
294
|
+
onNone: () => Effect.fail(retired()),
|
|
295
|
+
onSome: (resolution) => resolution._tag === "Serve" ? Effect.succeed(resolution.live.value.opened.browser) : Effect.fail(new Unavailable({
|
|
296
|
+
reason: resolution.reason,
|
|
297
|
+
detail: resolution.detail,
|
|
298
|
+
cause: resolution.cause
|
|
299
|
+
}))
|
|
300
|
+
})), Effect.timeoutOrElse({
|
|
301
|
+
duration: waitTimeout,
|
|
302
|
+
orElse: () => Effect.fail(new Unavailable({
|
|
303
|
+
reason: "opening",
|
|
304
|
+
detail: `no generation opened within ${Duration.format(waitTimeout)}`
|
|
305
|
+
}))
|
|
306
|
+
}));
|
|
307
|
+
/** Sleep until `before` ahead of `expiresAt`, but no sooner than halfway there from now. */
|
|
308
|
+
const untilRotation = (expiresAt, before) => Clock.currentTimeMillis.pipe(Effect.flatMap((now) => {
|
|
309
|
+
const left = DateTime.toEpochMillis(expiresAt) - now;
|
|
310
|
+
return Effect.sleep(Duration.millis(Math.max(left - Duration.toMillis(before), left / 2, 0)));
|
|
311
|
+
}));
|
|
312
|
+
/** Supervise generations of a provider's browser for as long as the scope is open. */
|
|
313
|
+
const make = Effect.fn("Supervisor.make")(function* (options) {
|
|
314
|
+
const clock = yield* Clock.Clock;
|
|
315
|
+
const context = yield* Effect.context();
|
|
316
|
+
const own = yield* Scope.fork(yield* Effect.scope);
|
|
317
|
+
const exclusive = options.exclusive ?? false;
|
|
318
|
+
const definite = options.definite ?? (() => false);
|
|
319
|
+
const reopen = options.reopen ?? Schedule.exponential("1 second").pipe(Schedule.upTo({ duration: Duration.minutes(5) }));
|
|
320
|
+
const rotateBefore = options.rotateBefore === void 0 ? void 0 : Duration.fromInputUnsafe(options.rotateBefore);
|
|
321
|
+
const waitTimeout = Duration.fromInputUnsafe(options.waitTimeout ?? Duration.minutes(2));
|
|
322
|
+
const started = start();
|
|
323
|
+
const state = yield* SubscriptionRef.make(started.state);
|
|
324
|
+
const events = yield* PubSub.unbounded({ replay: 64 });
|
|
325
|
+
const opening = yield* FiberHandle.make();
|
|
326
|
+
const releases = yield* FiberMap.make();
|
|
327
|
+
const runOpen = yield* FiberHandle.runtime(opening)();
|
|
328
|
+
const runRelease = yield* FiberMap.runtime(releases)();
|
|
329
|
+
const apply = (input) => SubscriptionRef.modifyEffect(state, (current) => {
|
|
330
|
+
const step = transition(current, input, exclusive);
|
|
331
|
+
return Effect.as(perform(step), [step.reply, step.state]);
|
|
332
|
+
}).pipe(Effect.uninterruptible);
|
|
333
|
+
const perform = (step) => Effect.gen(function* () {
|
|
334
|
+
for (const [number, generation] of step.events) PubSub.publishUnsafe(events, [new Generation({
|
|
335
|
+
number,
|
|
336
|
+
at: clock.currentTimeMillisUnsafe(),
|
|
337
|
+
state: generation
|
|
338
|
+
})]);
|
|
339
|
+
for (const command of step.commands) switch (command._tag) {
|
|
340
|
+
case "Open":
|
|
341
|
+
runOpen(openGeneration(command.number, command.after));
|
|
342
|
+
break;
|
|
343
|
+
case "Watch":
|
|
344
|
+
yield* Effect.forkIn(Effect.provideContext(watch(command.live), context), command.live.value.scope);
|
|
345
|
+
break;
|
|
346
|
+
case "Release":
|
|
347
|
+
runRelease(command.live.number, release(command.live));
|
|
348
|
+
break;
|
|
349
|
+
case "Leave": runRelease(command.live.number, Scope.close(command.live.value.scope, Exit.void));
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
const openOnce = Effect.uninterruptibleMask((restore) => Effect.flatMap(Scope.fork(own), (scope) => restore(options.open.pipe(Scope.provide(scope), Effect.provideContext(context))).pipe(Effect.onError((cause) => Scope.close(scope, Exit.failCause(cause))), Effect.map((opened) => ({
|
|
353
|
+
opened,
|
|
354
|
+
scope
|
|
355
|
+
})))));
|
|
356
|
+
const openGeneration = (number, after) => Effect.uninterruptibleMask((restore) => {
|
|
357
|
+
const previous = after === void 0 ? Effect.void : FiberMap.get(releases, after).pipe(Effect.flatMap(Option.match({
|
|
358
|
+
onNone: () => Effect.void,
|
|
359
|
+
onSome: Fiber.await
|
|
360
|
+
})));
|
|
361
|
+
const tries = Effect.retry(openOnce, {
|
|
362
|
+
schedule: reopen,
|
|
363
|
+
while: (error) => !definite(error)
|
|
364
|
+
});
|
|
365
|
+
return restore(previous.pipe(Effect.andThen(tries))).pipe(Effect.exit, Effect.flatMap((exit) => {
|
|
366
|
+
if (Exit.isSuccess(exit)) return apply({
|
|
367
|
+
_tag: "Opened",
|
|
368
|
+
live: {
|
|
369
|
+
number,
|
|
370
|
+
value: exit.value
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
return Cause.hasInterruptsOnly(exit.cause) ? apply({
|
|
374
|
+
_tag: "Abandoned",
|
|
375
|
+
number
|
|
376
|
+
}) : apply({
|
|
377
|
+
_tag: "Failed",
|
|
378
|
+
number,
|
|
379
|
+
detail: describe(exit.cause),
|
|
380
|
+
cause: Cause.squash(exit.cause)
|
|
381
|
+
});
|
|
382
|
+
}));
|
|
383
|
+
});
|
|
384
|
+
const watch = (live) => {
|
|
385
|
+
const { browser } = live.value.opened;
|
|
386
|
+
const loss = browser.disconnected.pipe(Effect.flatMap((cause) => apply({
|
|
387
|
+
_tag: "Lost",
|
|
388
|
+
number: live.number,
|
|
389
|
+
cause
|
|
390
|
+
})));
|
|
391
|
+
const rotation = browser.expiresAt === void 0 || rotateBefore === void 0 ? Effect.void : untilRotation(browser.expiresAt, rotateBefore).pipe(Effect.andThen(apply({
|
|
392
|
+
_tag: "Rotate",
|
|
393
|
+
due: live.number
|
|
394
|
+
})));
|
|
395
|
+
return Effect.all([loss, rotation], {
|
|
396
|
+
concurrency: 2,
|
|
397
|
+
discard: true
|
|
398
|
+
});
|
|
399
|
+
};
|
|
400
|
+
const release = (live) => (live.value.opened.release ?? Effect.succeed(new Settled())).pipe(Effect.catchCause((cause) => Effect.succeed(new Unconfirmed({ detail: `the release failed: ${describe(cause)}` }))), Effect.ensuring(Scope.close(live.value.scope, Exit.void)), Effect.flatMap((released) => apply({
|
|
401
|
+
_tag: "Released",
|
|
402
|
+
number: live.number,
|
|
403
|
+
released
|
|
404
|
+
})));
|
|
405
|
+
const end = yield* Effect.cached(PubSub.publish(events, Exit.void));
|
|
406
|
+
const stop = (input) => apply(input).pipe(Effect.andThen(FiberHandle.clear(opening)), Effect.andThen(FiberMap.awaitEmpty(releases)), Effect.andThen(end), Effect.asVoid);
|
|
407
|
+
const retire = yield* Effect.cached(stop({ _tag: "Retire" }));
|
|
408
|
+
yield* Effect.addFinalizer(() => options.keep === true ? stop({ _tag: "Keep" }) : retire);
|
|
409
|
+
yield* perform(started);
|
|
410
|
+
return {
|
|
411
|
+
browser: waitFor(state, waitTimeout, void 0),
|
|
412
|
+
states: Stream.fromPubSubTake(events),
|
|
413
|
+
rotate: apply({ _tag: "Rotate" }).pipe(Effect.flatMap((target) => target === void 0 ? Effect.fail(retired()) : waitFor(state, waitTimeout, target))),
|
|
414
|
+
retire
|
|
415
|
+
};
|
|
416
|
+
});
|
|
417
|
+
//#endregion
|
|
418
|
+
export { Closed as a, Kept as c, Opening as d, Released as f, Unconfirmed as h, make as i, Lost as l, Settled as m, Supervisor_exports as n, Down as o, Reopening as p, Unavailable as r, GenerationState as s, Generation as t, Open as u };
|
|
419
|
+
|
|
420
|
+
//# sourceMappingURL=Supervisor-BtEG93gR.mjs.map
|