effect-browser 0.3.0-beta.7 → 0.3.0-beta.9
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 +114 -19
- package/dist/{Browser-CNwK0r0q.mjs → Browser-Bc_LIqTC.mjs} +184 -16
- package/dist/Browser-Bc_LIqTC.mjs.map +1 -0
- package/dist/Browser.d.mts +16 -4
- package/dist/Browser.mjs +2 -2
- package/dist/{BrowserError-DXXIA6Rk.d.mts → BrowserError-Coy7EW1N.d.mts} +10 -5
- package/dist/BrowserError.d.mts +2 -2
- package/dist/BrowserError.mjs +10 -1
- package/dist/BrowserError.mjs.map +1 -1
- package/dist/Cdp.d.mts +1 -1
- package/dist/Cdp.mjs +3 -3
- package/dist/Chromium.d.mts +1 -1
- package/dist/Chromium.mjs +2 -2
- package/dist/Expect.d.mts +2 -2
- package/dist/Expect.mjs +2 -2
- package/dist/Moment.d.mts +1 -1
- package/dist/Moment.mjs +1 -1
- package/dist/{Page-D4281dIi.mjs → Page-B5cjaUp5.mjs} +2 -2
- package/dist/{Page-D4281dIi.mjs.map → Page-B5cjaUp5.mjs.map} +1 -1
- package/dist/{Page-C30aG2RQ.d.mts → Page-BlPjfibv.d.mts} +2 -2
- package/dist/Page.d.mts +1 -1
- package/dist/Page.mjs +1 -1
- package/dist/{Plan-BvCS9qAf.d.mts → Plan-BI8XGvDu.d.mts} +3 -3
- package/dist/{Plan-CyQNv49C.mjs → Plan-D1wrJVs8.mjs} +3 -3
- package/dist/{Plan-CyQNv49C.mjs.map → Plan-D1wrJVs8.mjs.map} +1 -1
- package/dist/Plan.d.mts +1 -1
- package/dist/Plan.mjs +1 -1
- package/dist/Policy.d.mts +1 -1
- package/dist/Policy.mjs +1 -1
- package/dist/Presentation.d.mts +1 -1
- package/dist/Presentation.mjs +1 -1
- package/dist/Stage.d.mts +2 -2
- package/dist/Stage.mjs +2 -2
- package/dist/{Supervisor-CLJaT7rG.d.mts → Supervisor-CYWFK3vM.d.mts} +105 -55
- package/dist/Supervisor-TNKNOVzu.mjs +599 -0
- package/dist/Supervisor-TNKNOVzu.mjs.map +1 -0
- package/dist/Supervisor.d.mts +2 -2
- package/dist/Supervisor.mjs +2 -2
- package/dist/{context-DAYjATgC.mjs → context-B6xr8nVi.mjs} +134 -10
- package/dist/context-B6xr8nVi.mjs.map +1 -0
- package/dist/index.d.mts +4 -4
- package/dist/index.mjs +4 -4
- package/dist/{page-DHeYWEs-.mjs → page-D-OedyBw.mjs} +59 -126
- package/dist/page-D-OedyBw.mjs.map +1 -0
- package/dist/{url-CcL0K7S0.mjs → url-Cd04Okid.mjs} +6 -4
- package/dist/url-Cd04Okid.mjs.map +1 -0
- package/package.json +1 -1
- package/src/Browser.ts +57 -16
- package/src/BrowserError.ts +9 -0
- package/src/Supervisor.ts +384 -138
- package/src/internal/input/gate.ts +224 -0
- package/src/internal/input/guard.ts +3 -2
- package/src/internal/input/perform.ts +84 -32
- package/src/internal/input/replies.ts +52 -16
- package/src/internal/page/context.ts +21 -11
- package/src/internal/page/lane.ts +20 -10
- package/src/internal/supervisor/lifecycle.ts +291 -140
- package/src/internal/timeline/events.ts +1 -0
- package/dist/Browser-CNwK0r0q.mjs.map +0 -1
- package/dist/Supervisor-BtEG93gR.mjs +0 -420
- package/dist/Supervisor-BtEG93gR.mjs.map +0 -1
- package/dist/context-DAYjATgC.mjs.map +0 -1
- package/dist/page-DHeYWEs-.mjs.map +0 -1
- package/dist/url-CcL0K7S0.mjs.map +0 -1
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
|
|
2
|
+
import { DisconnectCause } from "./BrowserEvent.mjs";
|
|
3
|
+
import { Cause, Clock, DateTime, Deferred, Duration, Effect, Exit, FiberHandle, FiberMap, Option, Predicate, PubSub, Schedule, Schema, Scope, Semaphore, 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
|
+
/** No longer serving; holds end at this host-monotonic deadline or when their owners let go. */
|
|
22
|
+
var Draining = class extends Schema.TaggedClass()("Draining", {
|
|
23
|
+
reason: Schema.Literals([
|
|
24
|
+
"rotated",
|
|
25
|
+
"lost",
|
|
26
|
+
"retired"
|
|
27
|
+
]),
|
|
28
|
+
revokeAt: Schema.Finite
|
|
29
|
+
}) {};
|
|
30
|
+
/** Lost: its connection dropped, or its provider ended its session, as `cause` says. */
|
|
31
|
+
var Lost = class extends Schema.TaggedClass()("Lost", { cause: DisconnectCause }) {};
|
|
32
|
+
/**
|
|
33
|
+
* It never opened: its last try failed with `cause`, which the provider deems definite, or after
|
|
34
|
+
* which the reopen schedule gave up.
|
|
35
|
+
*/
|
|
36
|
+
var Down = class extends Schema.TaggedClass()("Down", {
|
|
37
|
+
detail: Schema.String,
|
|
38
|
+
cause: Schema.Defect()
|
|
39
|
+
}) {};
|
|
40
|
+
/** It ended. `released` says how its release went; one retired while opening has none. */
|
|
41
|
+
var Closed = class extends Schema.TaggedClass()("Closed", { released: Schema.optional(Released) }) {};
|
|
42
|
+
/**
|
|
43
|
+
* It was serving as the supervisor's scope closed, and was left running, not released: its scope
|
|
44
|
+
* closed, and its provider runs it on, and bills, until something adopts and releases it.
|
|
45
|
+
*/
|
|
46
|
+
var Kept = class extends Schema.TaggedClass()("Kept", {}) {};
|
|
47
|
+
const GenerationState = Schema.Union([
|
|
48
|
+
Opening,
|
|
49
|
+
Reopening,
|
|
50
|
+
Open,
|
|
51
|
+
Draining,
|
|
52
|
+
Lost,
|
|
53
|
+
Down,
|
|
54
|
+
Closed,
|
|
55
|
+
Kept
|
|
56
|
+
]);
|
|
57
|
+
const start = () => ({
|
|
58
|
+
state: {
|
|
59
|
+
_tag: "Opening",
|
|
60
|
+
number: 1,
|
|
61
|
+
serving: void 0,
|
|
62
|
+
draining: /* @__PURE__ */ new Map()
|
|
63
|
+
},
|
|
64
|
+
events: [[1, new Opening()]],
|
|
65
|
+
commands: [{
|
|
66
|
+
_tag: "Open",
|
|
67
|
+
number: 1,
|
|
68
|
+
after: void 0
|
|
69
|
+
}]
|
|
70
|
+
});
|
|
71
|
+
const servingOf = (state) => state._tag === "Retired" ? void 0 : state.serving;
|
|
72
|
+
/** The holds still owned for a generation, including one whose release has begun. */
|
|
73
|
+
const lookup = (state, number) => servingOf(state)?.number === number ? servingOf(state) : state.draining.get(number)?.live;
|
|
74
|
+
/** The sole writer of a drain's ending and phase; each release command is emitted once. */
|
|
75
|
+
const draining = (change, drain) => {
|
|
76
|
+
const before = change.state.draining.get(drain.live.number);
|
|
77
|
+
const { live, ending, _tag } = drain;
|
|
78
|
+
change.state = {
|
|
79
|
+
...change.state,
|
|
80
|
+
draining: new Map(change.state.draining).set(live.number, drain)
|
|
81
|
+
};
|
|
82
|
+
if (before?.ending !== ending) {
|
|
83
|
+
change.events.push([live.number, ending]);
|
|
84
|
+
change.commands.push({
|
|
85
|
+
_tag: "Drain",
|
|
86
|
+
live,
|
|
87
|
+
ending
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
if (_tag !== "Draining" && before?._tag !== _tag) change.commands.push({
|
|
91
|
+
_tag: _tag === "Leaving" ? "Leave" : "Release",
|
|
92
|
+
live,
|
|
93
|
+
ending
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
const end = (change, live, reason, keep = false) => {
|
|
97
|
+
const ending = new Draining({
|
|
98
|
+
reason,
|
|
99
|
+
revokeAt: change.now + (reason === "rotated" ? change.handover : 0)
|
|
100
|
+
});
|
|
101
|
+
draining(change, {
|
|
102
|
+
live,
|
|
103
|
+
ending,
|
|
104
|
+
_tag: keep ? "Leaving" : live.holds.size === 0 || ending.revokeAt <= change.now ? "Releasing" : "Draining"
|
|
105
|
+
});
|
|
106
|
+
};
|
|
107
|
+
const revoke = (change, number, reason) => {
|
|
108
|
+
const drain = change.state.draining.get(number);
|
|
109
|
+
if (drain?._tag !== "Draining" || reason === void 0 && change.now < drain.ending.revokeAt) return;
|
|
110
|
+
draining(change, {
|
|
111
|
+
...drain,
|
|
112
|
+
_tag: "Releasing",
|
|
113
|
+
ending: reason === void 0 ? drain.ending : new Draining({
|
|
114
|
+
reason,
|
|
115
|
+
revokeAt: change.now
|
|
116
|
+
})
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
const holds = (change, input) => {
|
|
120
|
+
const number = input.number ?? servingOf(change.state)?.number;
|
|
121
|
+
const live = number === void 0 ? void 0 : lookup(change.state, number);
|
|
122
|
+
if (live === void 0) return;
|
|
123
|
+
const drain = change.state.draining.get(live.number);
|
|
124
|
+
if (input._tag === "Hold" && drain !== void 0 && (drain._tag !== "Draining" || change.now >= drain.ending.revokeAt)) return;
|
|
125
|
+
if (live.holds.has(input.hold) === (input._tag === "Hold")) return;
|
|
126
|
+
const holds = new Set(live.holds);
|
|
127
|
+
if (input._tag === "Hold") {
|
|
128
|
+
holds.add(input.hold);
|
|
129
|
+
change.borrowed = live;
|
|
130
|
+
} else holds.delete(input.hold);
|
|
131
|
+
const held = {
|
|
132
|
+
...live,
|
|
133
|
+
holds
|
|
134
|
+
};
|
|
135
|
+
if (drain !== void 0) draining(change, {
|
|
136
|
+
...drain,
|
|
137
|
+
live: held,
|
|
138
|
+
_tag: drain._tag === "Draining" && holds.size === 0 ? "Releasing" : drain._tag
|
|
139
|
+
});
|
|
140
|
+
else if (change.state._tag !== "Retired") change.state = {
|
|
141
|
+
...change.state,
|
|
142
|
+
serving: held
|
|
143
|
+
};
|
|
144
|
+
};
|
|
145
|
+
const next = (change, number, reopen) => {
|
|
146
|
+
const { exclusive } = change;
|
|
147
|
+
const current = servingOf(change.state);
|
|
148
|
+
const after = exclusive ? current?.number ?? change.state.draining.keys().next().value : void 0;
|
|
149
|
+
if (current !== void 0 && (reopen || exclusive)) end(change, current, reopen ? "lost" : "rotated");
|
|
150
|
+
change.state = {
|
|
151
|
+
_tag: "Opening",
|
|
152
|
+
number,
|
|
153
|
+
serving: reopen || exclusive ? void 0 : current,
|
|
154
|
+
draining: change.state.draining
|
|
155
|
+
};
|
|
156
|
+
change.events.push([number, reopen ? new Reopening() : new Opening()]);
|
|
157
|
+
change.commands.push({
|
|
158
|
+
_tag: "Open",
|
|
159
|
+
number,
|
|
160
|
+
after
|
|
161
|
+
});
|
|
162
|
+
};
|
|
163
|
+
const lost = (change, input) => {
|
|
164
|
+
const { state } = change;
|
|
165
|
+
const drain = state.draining.get(input.number);
|
|
166
|
+
const current = servingOf(state);
|
|
167
|
+
if (drain?._tag === "Draining") {
|
|
168
|
+
change.events.push([input.number, new Lost({ cause: input.cause })]);
|
|
169
|
+
revoke(change, input.number, "lost");
|
|
170
|
+
} else if (state._tag !== "Retired" && current?.number === input.number) {
|
|
171
|
+
change.events.push([input.number, new Lost({ cause: input.cause })]);
|
|
172
|
+
if (state._tag !== "Opening") next(change, (state._tag === "Open" ? current.number : state.number) + 1, true);
|
|
173
|
+
else {
|
|
174
|
+
change.state = {
|
|
175
|
+
...state,
|
|
176
|
+
serving: void 0
|
|
177
|
+
};
|
|
178
|
+
end(change, current, "lost");
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
/** Pure admission and lifecycle changes. A rejected hold preserves state identity. */
|
|
183
|
+
const transition = (state, input, exclusive, now = 0, handover = 3e4) => {
|
|
184
|
+
const change = {
|
|
185
|
+
state,
|
|
186
|
+
events: [],
|
|
187
|
+
commands: [],
|
|
188
|
+
exclusive,
|
|
189
|
+
handover,
|
|
190
|
+
now
|
|
191
|
+
};
|
|
192
|
+
switch (input._tag) {
|
|
193
|
+
case "Hold":
|
|
194
|
+
case "Let":
|
|
195
|
+
holds(change, input);
|
|
196
|
+
break;
|
|
197
|
+
case "Revoke":
|
|
198
|
+
revoke(change, input.number);
|
|
199
|
+
break;
|
|
200
|
+
case "Opened": {
|
|
201
|
+
const live = {
|
|
202
|
+
...input.live,
|
|
203
|
+
holds: /* @__PURE__ */ new Set()
|
|
204
|
+
};
|
|
205
|
+
if (lookup(state, live.number) !== void 0) break;
|
|
206
|
+
if (state._tag !== "Opening" || state.number !== live.number) end(change, live, "retired");
|
|
207
|
+
else {
|
|
208
|
+
change.events.push([live.number, new Open()]);
|
|
209
|
+
change.commands.push({
|
|
210
|
+
_tag: "Watch",
|
|
211
|
+
live
|
|
212
|
+
});
|
|
213
|
+
if (state.serving !== void 0) end(change, state.serving, "rotated");
|
|
214
|
+
change.state = {
|
|
215
|
+
_tag: "Open",
|
|
216
|
+
serving: live,
|
|
217
|
+
draining: change.state.draining
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
case "Failed": {
|
|
223
|
+
const { number, detail, cause } = input;
|
|
224
|
+
if (state._tag === "Opening" && state.number === number) change.state = {
|
|
225
|
+
...state,
|
|
226
|
+
_tag: "Down",
|
|
227
|
+
detail,
|
|
228
|
+
cause
|
|
229
|
+
};
|
|
230
|
+
change.events.push([number, new Down({
|
|
231
|
+
detail,
|
|
232
|
+
cause
|
|
233
|
+
})]);
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
case "Abandoned":
|
|
237
|
+
change.events.push([input.number, new Closed({})]);
|
|
238
|
+
break;
|
|
239
|
+
case "Released": {
|
|
240
|
+
const drain = state.draining.get(input.number);
|
|
241
|
+
if (drain === void 0 || drain._tag === "Draining" || drain.live.holds.size > 0) break;
|
|
242
|
+
const draining = new Map(state.draining);
|
|
243
|
+
draining.delete(input.number);
|
|
244
|
+
change.state = {
|
|
245
|
+
...state,
|
|
246
|
+
draining
|
|
247
|
+
};
|
|
248
|
+
change.events.push([input.number, input.released === void 0 ? new Kept() : new Closed({ released: input.released })]);
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
case "Lost":
|
|
252
|
+
lost(change, input);
|
|
253
|
+
break;
|
|
254
|
+
case "Rotate":
|
|
255
|
+
if (state._tag === "Retired" || input.due !== void 0 && (state._tag !== "Open" || state.serving.number !== input.due)) break;
|
|
256
|
+
change.reply = state._tag === "Opening" ? state.number : (state._tag === "Open" ? state.serving.number : state.number) + 1;
|
|
257
|
+
if (state._tag !== "Opening") next(change, change.reply, false);
|
|
258
|
+
break;
|
|
259
|
+
case "Keep":
|
|
260
|
+
case "Retire":
|
|
261
|
+
if (state._tag === "Retired") break;
|
|
262
|
+
for (const number of state.draining.keys()) revoke(change, number, "retired");
|
|
263
|
+
if (state.serving !== void 0) end(change, state.serving, "retired", input._tag === "Keep");
|
|
264
|
+
change.state = {
|
|
265
|
+
_tag: "Retired",
|
|
266
|
+
draining: change.state.draining
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
return change;
|
|
270
|
+
};
|
|
271
|
+
/** An unheld browser resolution; use obtains its hold atomically through Hold instead. */
|
|
272
|
+
const resolve = (state, target) => {
|
|
273
|
+
if (state._tag === "Retired") return {
|
|
274
|
+
_tag: "Unavailable",
|
|
275
|
+
reason: "retired",
|
|
276
|
+
detail: "the supervisor was retired"
|
|
277
|
+
};
|
|
278
|
+
if (state.serving !== void 0 && (target === void 0 || state._tag === "Open" && state.serving.number >= target)) return {
|
|
279
|
+
_tag: "Serve",
|
|
280
|
+
live: state.serving
|
|
281
|
+
};
|
|
282
|
+
if (state._tag === "Down" && (target === void 0 || state.number >= target)) return {
|
|
283
|
+
_tag: "Unavailable",
|
|
284
|
+
reason: "down",
|
|
285
|
+
detail: state.detail,
|
|
286
|
+
cause: state.cause
|
|
287
|
+
};
|
|
288
|
+
};
|
|
289
|
+
//#endregion
|
|
290
|
+
//#region src/Supervisor.ts
|
|
291
|
+
/**
|
|
292
|
+
* A browser kept open across losses and session ends, as a series of generations, each a new
|
|
293
|
+
* browser from the same provider. Pages don't carry over from one generation to the next.
|
|
294
|
+
*
|
|
295
|
+
* `make` opens the first generation at once with the provider's `open`, in a scope of its own, so
|
|
296
|
+
* a caller that stops waiting never interrupts a half-open browser. `browser` gives the current
|
|
297
|
+
* generation; callers wait, bounded, and share one open. A loss is published at once and the next
|
|
298
|
+
* generation opens on the `reopen` schedule, which also retries a failed open, unless the provider
|
|
299
|
+
* deems its failure `definite`: that goes `Down` at once, with its cause. `rotate` opens the
|
|
300
|
+
* next generation now, and so does the time `rotateBefore` ahead of a generation's `expiresAt`. A
|
|
301
|
+
* rotation makes the next generation before it breaks the current one, unless generations are
|
|
302
|
+
* `exclusive`, as sessions saving to one stored context must be: then the current one is released
|
|
303
|
+
* first. `retire` stops reopening at once and releases what is open; the scope's close retires
|
|
304
|
+
* too, unless the supervisor `keep`s: then it leaves the serving generation running, `Kept`. `states`
|
|
305
|
+
* publishes each generation's changes, with the provider's release outcome last.
|
|
306
|
+
*
|
|
307
|
+
* Providers supply the specifics: `Browserbase.supervise` opens hosted sessions this way.
|
|
308
|
+
*
|
|
309
|
+
* @since 0.3.0
|
|
310
|
+
*/
|
|
311
|
+
var Supervisor_exports = /* @__PURE__ */ __exportAll({
|
|
312
|
+
Closed: () => Closed,
|
|
313
|
+
Down: () => Down,
|
|
314
|
+
Draining: () => Draining,
|
|
315
|
+
Generation: () => Generation,
|
|
316
|
+
GenerationState: () => GenerationState,
|
|
317
|
+
Kept: () => Kept,
|
|
318
|
+
Lost: () => Lost,
|
|
319
|
+
Open: () => Open,
|
|
320
|
+
Opening: () => Opening,
|
|
321
|
+
Released: () => Released,
|
|
322
|
+
Reopening: () => Reopening,
|
|
323
|
+
Settled: () => Settled,
|
|
324
|
+
Superseded: () => Superseded,
|
|
325
|
+
Unavailable: () => Unavailable,
|
|
326
|
+
Unconfirmed: () => Unconfirmed,
|
|
327
|
+
make: () => make
|
|
328
|
+
});
|
|
329
|
+
/** One change in a generation's life. `at` is wall-clock epoch milliseconds from the Effect `Clock`. */
|
|
330
|
+
var Generation = class extends Schema.Class("effect-browser/Generation")({
|
|
331
|
+
number: Schema.Int.check(Schema.isGreaterThan(0)),
|
|
332
|
+
at: Schema.Finite,
|
|
333
|
+
hostTime: Schema.Finite,
|
|
334
|
+
state: GenerationState
|
|
335
|
+
}) {};
|
|
336
|
+
/**
|
|
337
|
+
* No browser to give: none opened within `waitTimeout` (`opening`), the latest generation failed
|
|
338
|
+
* to open (`down`, with what it failed with as `cause`), or the supervisor was retired.
|
|
339
|
+
*/
|
|
340
|
+
var Unavailable = class extends Schema.TaggedError()("Unavailable", {
|
|
341
|
+
reason: Schema.Literals([
|
|
342
|
+
"opening",
|
|
343
|
+
"down",
|
|
344
|
+
"retired"
|
|
345
|
+
]),
|
|
346
|
+
detail: Schema.String,
|
|
347
|
+
cause: Schema.optional(Schema.Defect())
|
|
348
|
+
}) {
|
|
349
|
+
get message() {
|
|
350
|
+
return `no browser: ${this.detail}`;
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
/** Work whose generation was revoked; input receipts still determine whether input reached a site. */
|
|
354
|
+
var Superseded = class extends Schema.TaggedError()("Superseded", {
|
|
355
|
+
generation: Schema.Int,
|
|
356
|
+
reason: Schema.Literals([
|
|
357
|
+
"rotated",
|
|
358
|
+
"lost",
|
|
359
|
+
"retired"
|
|
360
|
+
]),
|
|
361
|
+
revokeAt: Schema.Finite
|
|
362
|
+
}) {};
|
|
363
|
+
const retired = () => new Unavailable({
|
|
364
|
+
reason: "retired",
|
|
365
|
+
detail: "the supervisor was retired"
|
|
366
|
+
});
|
|
367
|
+
const describe = (cause) => {
|
|
368
|
+
const error = Cause.squash(cause);
|
|
369
|
+
return Predicate.isError(error) ? error.message : String(error);
|
|
370
|
+
};
|
|
371
|
+
const unavailable = (resolution) => new Unavailable({
|
|
372
|
+
reason: resolution.reason,
|
|
373
|
+
detail: resolution.detail,
|
|
374
|
+
cause: resolution.cause
|
|
375
|
+
});
|
|
376
|
+
const timedOut = () => new Unavailable({
|
|
377
|
+
reason: "opening",
|
|
378
|
+
detail: "no generation opened within the wait"
|
|
379
|
+
});
|
|
380
|
+
const bounded = (input, name) => Effect.suspend(() => {
|
|
381
|
+
const duration = Duration.fromInput(input);
|
|
382
|
+
return Option.isSome(duration) && Duration.isFinite(duration.value) && !Duration.isNegative(duration.value) ? Effect.succeed(duration.value) : Effect.die(/* @__PURE__ */ new RangeError(`${name} must be a finite nonnegative duration`));
|
|
383
|
+
});
|
|
384
|
+
/** Resolve immediately before starting a bounded wait, including when its duration is zero. */
|
|
385
|
+
const waitFor = (state, wait, clock, target) => {
|
|
386
|
+
const result = (resolution) => resolution._tag === "Serve" ? Effect.succeed(resolution.live) : Effect.fail(unavailable(resolution));
|
|
387
|
+
return Effect.suspend(() => {
|
|
388
|
+
const current = resolve(state.value, target);
|
|
389
|
+
return current === void 0 ? SubscriptionRef.changes(state).pipe(Stream.map((state) => resolve(state, target)), Stream.filter(Predicate.isNotUndefined), Stream.runHead, Effect.flatMap(Option.match({
|
|
390
|
+
onNone: () => Effect.fail(retired()),
|
|
391
|
+
onSome: result
|
|
392
|
+
})), Effect.timeoutOrElse({
|
|
393
|
+
duration: wait,
|
|
394
|
+
orElse: () => Effect.fail(timedOut())
|
|
395
|
+
})) : result(current);
|
|
396
|
+
}).pipe(Effect.provideService(Clock.Clock, clock));
|
|
397
|
+
};
|
|
398
|
+
/** Sleep until `before` ahead of `expiresAt`, but no sooner than halfway there from now. */
|
|
399
|
+
const untilRotation = (expiresAt, before) => Clock.currentTimeMillis.pipe(Effect.flatMap((now) => {
|
|
400
|
+
const left = DateTime.toEpochMillis(expiresAt) - now;
|
|
401
|
+
return Effect.sleep(Duration.millis(Math.max(left - Duration.toMillis(before), left / 2, 0)));
|
|
402
|
+
}));
|
|
403
|
+
/** Native timers may wake a fraction early; a revocation must reach its monotonic deadline. */
|
|
404
|
+
const until = (at, now) => Effect.suspend(() => now() >= at ? Effect.void : Effect.sleep(Duration.millis(Math.ceil(at - now()))).pipe(Effect.andThen(until(at, now))));
|
|
405
|
+
/** Borrowed work and transferred scopes hold a generation until cleanup lets it go. */
|
|
406
|
+
const leasing = (state, apply, waitTimeout, now, clock) => {
|
|
407
|
+
const borrow = (hold, wait) => Effect.gen(function* () {
|
|
408
|
+
const until = now() + Duration.toMillis(wait);
|
|
409
|
+
for (;;) {
|
|
410
|
+
const step = yield* apply({
|
|
411
|
+
_tag: "Hold",
|
|
412
|
+
hold
|
|
413
|
+
});
|
|
414
|
+
if (step.borrowed !== void 0) return step.borrowed;
|
|
415
|
+
yield* waitFor(state, Duration.millis(Math.max(0, until - now())), clock).pipe(Effect.interruptible);
|
|
416
|
+
if (now() >= until) return yield* timedOut();
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
const leaseOf = (live) => ({
|
|
420
|
+
generation: live.number,
|
|
421
|
+
browser: live.value.opened.browser,
|
|
422
|
+
value: live.value.opened.value,
|
|
423
|
+
ending: Deferred.await(live.value.ending),
|
|
424
|
+
retain: Effect.uninterruptible(Effect.gen(function* () {
|
|
425
|
+
const scope = yield* Scope.make();
|
|
426
|
+
const hold = {
|
|
427
|
+
_tag: "Retained",
|
|
428
|
+
scope
|
|
429
|
+
};
|
|
430
|
+
if ((yield* apply({
|
|
431
|
+
_tag: "Hold",
|
|
432
|
+
number: live.number,
|
|
433
|
+
hold
|
|
434
|
+
})).borrowed === void 0) {
|
|
435
|
+
yield* Scope.close(scope, Exit.void);
|
|
436
|
+
return yield* yield* Deferred.await(live.value.ending);
|
|
437
|
+
}
|
|
438
|
+
yield* Scope.addFinalizer(scope, apply({
|
|
439
|
+
_tag: "Let",
|
|
440
|
+
number: live.number,
|
|
441
|
+
hold
|
|
442
|
+
}).pipe(Effect.asVoid));
|
|
443
|
+
return scope;
|
|
444
|
+
}))
|
|
445
|
+
});
|
|
446
|
+
const use = (work, options = {}) => Effect.scoped(Effect.gen(function* () {
|
|
447
|
+
const hold = { _tag: "Use" };
|
|
448
|
+
const wait = yield* bounded(options.wait ?? waitTimeout, "wait");
|
|
449
|
+
const live = yield* Effect.acquireRelease(borrow(hold, wait), (live) => apply({
|
|
450
|
+
_tag: "Let",
|
|
451
|
+
number: live.number,
|
|
452
|
+
hold
|
|
453
|
+
}).pipe(Effect.asVoid));
|
|
454
|
+
return yield* Effect.raceFirst(work(leaseOf(live)), Deferred.await(live.value.revoked).pipe(Effect.flatMap(Effect.fail)));
|
|
455
|
+
}));
|
|
456
|
+
return use;
|
|
457
|
+
};
|
|
458
|
+
const acquireGeneration = (open, own) => Effect.uninterruptibleMask((restore) => Effect.flatMap(Scope.fork(own), (scope) => restore(open.pipe(Scope.provide(scope))).pipe(Effect.onError((cause) => Scope.close(scope, Exit.failCause(cause))), Effect.map((opened) => ({
|
|
459
|
+
opened: {
|
|
460
|
+
...opened,
|
|
461
|
+
value: "value" in opened ? opened.value : void 0
|
|
462
|
+
},
|
|
463
|
+
scope,
|
|
464
|
+
ending: Deferred.makeUnsafe(),
|
|
465
|
+
revoked: Deferred.makeUnsafe()
|
|
466
|
+
})))));
|
|
467
|
+
const changedUntil = (state, predicate) => SubscriptionRef.changes(state).pipe(Stream.filter(predicate), Stream.runHead, Effect.asVoid);
|
|
468
|
+
const watchGeneration = (live, rotateBefore, apply) => {
|
|
469
|
+
const { browser } = live.value.opened;
|
|
470
|
+
const loss = browser.disconnected.pipe(Effect.flatMap((cause) => apply({
|
|
471
|
+
_tag: "Lost",
|
|
472
|
+
number: live.number,
|
|
473
|
+
cause
|
|
474
|
+
})));
|
|
475
|
+
const rotation = browser.expiresAt === void 0 || rotateBefore === void 0 ? Effect.void : untilRotation(browser.expiresAt, rotateBefore).pipe(Effect.andThen(apply({
|
|
476
|
+
_tag: "Rotate",
|
|
477
|
+
due: live.number
|
|
478
|
+
})));
|
|
479
|
+
return Effect.all([loss, rotation], {
|
|
480
|
+
concurrency: 2,
|
|
481
|
+
discard: true
|
|
482
|
+
});
|
|
483
|
+
};
|
|
484
|
+
const releaseGeneration = (command, state, apply) => Effect.gen(function* () {
|
|
485
|
+
const { live, ending } = command;
|
|
486
|
+
yield* Deferred.succeed(live.value.revoked, new Superseded({
|
|
487
|
+
generation: live.number,
|
|
488
|
+
reason: ending.reason,
|
|
489
|
+
revokeAt: ending.revokeAt
|
|
490
|
+
}));
|
|
491
|
+
const holds = lookup(state.value, live.number)?.holds ?? [];
|
|
492
|
+
const cleaned = yield* Effect.forEach(holds, (hold) => Effect.exit(hold._tag === "Retained" ? Scope.close(hold.scope, Exit.void) : Effect.void), { concurrency: 8 });
|
|
493
|
+
yield* changedUntil(state, (current) => (lookup(current, live.number)?.holds.size ?? 0) === 0);
|
|
494
|
+
const released = command._tag === "Leave" ? void 0 : yield* (live.value.opened.release ?? Effect.succeed(new Settled())).pipe(Effect.catchCause((cause) => Effect.succeed(new Unconfirmed({ detail: `the release failed: ${describe(cause)}` }))));
|
|
495
|
+
const closed = yield* Effect.exit(Scope.close(live.value.scope, Exit.void));
|
|
496
|
+
yield* apply({
|
|
497
|
+
_tag: "Released",
|
|
498
|
+
number: live.number,
|
|
499
|
+
released
|
|
500
|
+
});
|
|
501
|
+
yield* Exit.asVoidAll([...cleaned, closed]);
|
|
502
|
+
});
|
|
503
|
+
const makeScoped = Effect.fn("Supervisor.make")(function* (options) {
|
|
504
|
+
const clock = yield* Clock.Clock;
|
|
505
|
+
const now = () => Number(clock.monotonicTimeNanosUnsafe()) / 1e6;
|
|
506
|
+
const context = yield* Effect.context();
|
|
507
|
+
const own = yield* Scope.fork(yield* Effect.scope);
|
|
508
|
+
const exclusive = options.exclusive ?? false;
|
|
509
|
+
const definite = options.definite ?? (() => false);
|
|
510
|
+
const reopen = options.reopen ?? Schedule.exponential("1 second").pipe(Schedule.upTo({ duration: Duration.minutes(5) }));
|
|
511
|
+
const rotateBefore = options.rotateBefore === void 0 ? void 0 : Duration.fromInputUnsafe(options.rotateBefore);
|
|
512
|
+
const waitTimeout = yield* bounded(options.waitTimeout ?? Duration.minutes(2), "waitTimeout");
|
|
513
|
+
const handover = Duration.toMillis(yield* bounded(options.handover ?? Duration.seconds(30), "handover"));
|
|
514
|
+
const started = start();
|
|
515
|
+
const state = yield* SubscriptionRef.make(started.state);
|
|
516
|
+
const applying = yield* Semaphore.make(1);
|
|
517
|
+
const events = yield* PubSub.unbounded({ replay: 64 });
|
|
518
|
+
const opening = yield* FiberHandle.make();
|
|
519
|
+
const releases = yield* FiberMap.make();
|
|
520
|
+
const runOpen = yield* FiberHandle.runtime(opening)();
|
|
521
|
+
const runRelease = yield* FiberMap.runtime(releases)();
|
|
522
|
+
const apply = (input) => applying.withPermit(Effect.gen(function* () {
|
|
523
|
+
const current = state.value;
|
|
524
|
+
const step = transition(current, input, exclusive, now(), handover);
|
|
525
|
+
if (step.state !== current) yield* SubscriptionRef.set(state, step.state);
|
|
526
|
+
yield* perform(step);
|
|
527
|
+
return step;
|
|
528
|
+
})).pipe(Effect.uninterruptible, Effect.provideService(Clock.Clock, clock));
|
|
529
|
+
const perform = (step) => Effect.gen(function* () {
|
|
530
|
+
for (const [number, generation] of step.events) PubSub.publishUnsafe(events, [new Generation({
|
|
531
|
+
number,
|
|
532
|
+
at: clock.currentTimeMillisUnsafe(),
|
|
533
|
+
hostTime: now(),
|
|
534
|
+
state: generation
|
|
535
|
+
})]);
|
|
536
|
+
for (const command of step.commands) switch (command._tag) {
|
|
537
|
+
case "Open":
|
|
538
|
+
runOpen(openGeneration(command.number, command.after));
|
|
539
|
+
break;
|
|
540
|
+
case "Watch":
|
|
541
|
+
yield* Effect.forkIn(Effect.provideContext(watchGeneration(command.live, rotateBefore, apply), context), command.live.value.scope);
|
|
542
|
+
break;
|
|
543
|
+
case "Drain": {
|
|
544
|
+
const ending = new Superseded({
|
|
545
|
+
generation: command.live.number,
|
|
546
|
+
reason: command.ending.reason,
|
|
547
|
+
revokeAt: command.ending.revokeAt
|
|
548
|
+
});
|
|
549
|
+
yield* Deferred.succeed(command.live.value.ending, ending);
|
|
550
|
+
yield* Effect.forkIn(until(ending.revokeAt, now).pipe(Effect.andThen(Effect.suspend(() => apply({
|
|
551
|
+
_tag: "Revoke",
|
|
552
|
+
number: command.live.number
|
|
553
|
+
})))), command.live.value.scope);
|
|
554
|
+
break;
|
|
555
|
+
}
|
|
556
|
+
case "Release":
|
|
557
|
+
case "Leave": runRelease(command.live.number, releaseGeneration(command, state, apply));
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
const openOnce = acquireGeneration(options.open, own).pipe(Effect.provideContext(context));
|
|
561
|
+
const openGeneration = (number, after) => Effect.uninterruptibleMask((restore) => restore((after === void 0 ? Effect.void : changedUntil(state, (current) => !current.draining.has(after))).pipe(Effect.andThen(Effect.retry(openOnce, {
|
|
562
|
+
schedule: reopen,
|
|
563
|
+
while: (error) => !definite(error)
|
|
564
|
+
})))).pipe(Effect.exit, Effect.flatMap((exit) => Exit.isSuccess(exit) ? apply({
|
|
565
|
+
_tag: "Opened",
|
|
566
|
+
live: {
|
|
567
|
+
number,
|
|
568
|
+
value: exit.value
|
|
569
|
+
}
|
|
570
|
+
}) : Cause.hasInterruptsOnly(exit.cause) ? apply({
|
|
571
|
+
_tag: "Abandoned",
|
|
572
|
+
number
|
|
573
|
+
}) : apply({
|
|
574
|
+
_tag: "Failed",
|
|
575
|
+
number,
|
|
576
|
+
detail: describe(exit.cause),
|
|
577
|
+
cause: Cause.squash(exit.cause)
|
|
578
|
+
}))));
|
|
579
|
+
const use = leasing(state, apply, waitTimeout, now, clock);
|
|
580
|
+
const end = yield* Effect.cached(PubSub.publish(events, Exit.void));
|
|
581
|
+
const stop = (input) => apply(input).pipe(Effect.andThen(FiberHandle.clear(opening)), Effect.andThen(FiberMap.awaitEmpty(releases)), Effect.andThen(Effect.suspend(() => Deferred.isDone(releases.deferred).pipe(Effect.flatMap((failed) => failed ? FiberMap.join(releases) : Effect.void)))), Effect.ensuring(end), Effect.asVoid);
|
|
582
|
+
const retire = yield* Effect.cached(stop({ _tag: "Retire" }));
|
|
583
|
+
yield* Effect.addFinalizer(() => options.keep === true ? stop({ _tag: "Keep" }) : retire);
|
|
584
|
+
yield* perform(started);
|
|
585
|
+
return {
|
|
586
|
+
browser: Effect.map(waitFor(state, waitTimeout, clock), (live) => live.value.opened.browser),
|
|
587
|
+
use,
|
|
588
|
+
states: Stream.fromPubSubTake(events),
|
|
589
|
+
rotate: apply({ _tag: "Rotate" }).pipe(Effect.flatMap(({ reply }) => reply === void 0 ? Effect.fail(retired()) : Effect.map(waitFor(state, waitTimeout, clock, reply), (live) => live.value.opened.browser))),
|
|
590
|
+
retire
|
|
591
|
+
};
|
|
592
|
+
});
|
|
593
|
+
function make(options) {
|
|
594
|
+
return makeScoped(options);
|
|
595
|
+
}
|
|
596
|
+
//#endregion
|
|
597
|
+
export { Unconfirmed as _, make as a, Draining as c, Lost as d, Open as f, Settled as g, Reopening as h, Unavailable as i, GenerationState as l, Released as m, Superseded as n, Closed as o, Opening as p, Supervisor_exports as r, Down as s, Generation as t, Kept as u };
|
|
598
|
+
|
|
599
|
+
//# sourceMappingURL=Supervisor-TNKNOVzu.mjs.map
|