effect-browser 0.3.0-beta.13 → 0.3.0-beta.14
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 +50 -19
- package/dist/{Browser-QiT6Cdn_.mjs → Browser-BBSySyjT.mjs} +2 -2
- package/dist/{Browser-QiT6Cdn_.mjs.map → Browser-BBSySyjT.mjs.map} +1 -1
- package/dist/Browser.d.mts +1 -1
- package/dist/Browser.mjs +1 -1
- package/dist/Cdp.mjs +1 -1
- package/dist/Chromium.mjs +1 -1
- package/dist/Expect.d.mts +1 -1
- package/dist/Expect.mjs +1 -1
- package/dist/{Frame-Di2PxyF7.d.mts → Frame-DkNdjjjr.d.mts} +3 -5
- package/dist/Frame.d.mts +1 -1
- package/dist/Frame.mjs +2 -4
- package/dist/Frame.mjs.map +1 -1
- package/dist/Moment.d.mts +1 -1
- package/dist/Moment.mjs +7 -3
- package/dist/Moment.mjs.map +1 -1
- package/dist/{Page-B7X6DQUn.d.mts → Page-DL9eylp3.d.mts} +2 -2
- package/dist/Page.d.mts +1 -1
- package/dist/{Plan-BS0jpENY.d.mts → Plan-6As6vcwT.d.mts} +3 -3
- package/dist/{Plan-Bl2NM1D5.mjs → Plan-CXA2BW-G.mjs} +4 -6
- package/dist/Plan-CXA2BW-G.mjs.map +1 -0
- package/dist/Plan.d.mts +1 -1
- package/dist/Plan.mjs +1 -1
- package/dist/Policy.d.mts +1 -1
- package/dist/Presentation.d.mts +1 -1
- package/dist/Presentation.mjs +1 -1
- package/dist/Stage.d.mts +5 -4
- package/dist/Stage.mjs +53 -20
- package/dist/Stage.mjs.map +1 -1
- package/dist/{Supervisor-TNKNOVzu.mjs → Supervisor-BE2sGHBK.mjs} +67 -41
- package/dist/Supervisor-BE2sGHBK.mjs.map +1 -0
- package/dist/{Supervisor-CYWFK3vM.d.mts → Supervisor-W60pZWHh.d.mts} +24 -8
- package/dist/Supervisor.d.mts +2 -2
- package/dist/Supervisor.mjs +1 -1
- package/dist/index.d.mts +4 -4
- package/dist/index.mjs +3 -3
- package/dist/{page-ZxMhw16l.mjs → page-B92QnRfT.mjs} +196 -166
- package/dist/page-B92QnRfT.mjs.map +1 -0
- package/package.json +1 -1
- package/src/Frame.ts +2 -4
- package/src/Moment.ts +14 -3
- package/src/Plan.ts +7 -6
- package/src/Stage.ts +61 -21
- package/src/Supervisor.ts +69 -37
- package/src/internal/pictures/capture.ts +172 -188
- package/src/internal/pictures/pictures.ts +50 -73
- package/src/internal/pictures/quiescence.ts +94 -0
- package/src/internal/reading/ready.ts +9 -3
- package/src/internal/supervisor/lifecycle.ts +27 -12
- package/dist/Plan-Bl2NM1D5.mjs.map +0 -1
- package/dist/Supervisor-TNKNOVzu.mjs.map +0 -1
- package/dist/page-ZxMhw16l.mjs.map +0 -1
package/package.json
CHANGED
package/src/Frame.ts
CHANGED
|
@@ -93,10 +93,8 @@ export class CaptureStats extends Schema.Class<CaptureStats>("effect-browser/Cap
|
|
|
93
93
|
*/
|
|
94
94
|
foreignSize: Count,
|
|
95
95
|
/**
|
|
96
|
-
* Frames
|
|
97
|
-
*
|
|
98
|
-
* Where Playwright emulates the viewport, the page's own frames from that time are among them; a
|
|
99
|
-
* crop on the page's own session leaves out only frames of another size than the page's.
|
|
96
|
+
* Frames discarded while native capture is paused for a library picture and its restoration.
|
|
97
|
+
* A pause can produce no callbacks, so this count may remain zero even when pictures were taken.
|
|
100
98
|
*/
|
|
101
99
|
duringPictures: Count,
|
|
102
100
|
/**
|
package/src/Moment.ts
CHANGED
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
* part it could not read, which it names with why.
|
|
20
20
|
*
|
|
21
21
|
* Frames come from a running screencast, as far back as the browser's `frameHistory` keeps them.
|
|
22
|
-
* A moment's picture is the newest frame if it was painted after the page's latest input and
|
|
23
|
-
* most 250 ms ago, else a new screenshot timed by its capture.
|
|
22
|
+
* A moment's picture is the newest frame if it was painted after the page's latest input and
|
|
23
|
+
* completed action, at most 250 ms ago, else a new screenshot timed by its capture.
|
|
24
24
|
*
|
|
25
25
|
* @since 0.3.0
|
|
26
26
|
*/
|
|
@@ -98,6 +98,17 @@ const spread = (frames: ReadonlyArray<Frame>, count: number): ReadonlyArray<Fram
|
|
|
98
98
|
);
|
|
99
99
|
};
|
|
100
100
|
|
|
101
|
+
// An action ends after its navigation settles, which can follow the last paint. A fresh picture
|
|
102
|
+
// keeps that completed action inside the moment without moving events between adjacent windows.
|
|
103
|
+
const pictureOf = Effect.fnUntraced(function* (page: Page.Page) {
|
|
104
|
+
const frame = yield* page.frame({ after: "input" });
|
|
105
|
+
const events = yield* page.recentEvents;
|
|
106
|
+
|
|
107
|
+
return events.some((event) => event._tag === "Action" && event.at > frame.hostTime)
|
|
108
|
+
? yield* page.frame({ maxAge: 0 })
|
|
109
|
+
: frame;
|
|
110
|
+
});
|
|
111
|
+
|
|
101
112
|
export const capture = Effect.fn("Moment.capture")(function* (
|
|
102
113
|
page: Page.Page,
|
|
103
114
|
options: CaptureOptions = {},
|
|
@@ -116,7 +127,7 @@ export const capture = Effect.fn("Moment.capture")(function* (
|
|
|
116
127
|
// outline is taken alongside. What cannot be read is missing from the moment, not its end.
|
|
117
128
|
const { pictured, outlined } = yield* Effect.all(
|
|
118
129
|
{
|
|
119
|
-
pictured: Effect.result(page
|
|
130
|
+
pictured: Effect.result(pictureOf(page)).pipe(
|
|
120
131
|
Effect.flatMap((picture) =>
|
|
121
132
|
Effect.map(
|
|
122
133
|
page.window({
|
package/src/Plan.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* as a named input slot, never the text itself. A navigation to an address whose credentials the
|
|
8
8
|
* events withheld is an input slot too, so replay goes where the caller says, or nowhere, and
|
|
9
9
|
* never to the address without them. `replay` takes the steps in turn. Before a step
|
|
10
|
-
*
|
|
10
|
+
* except an opening navigate, back or reload, it waits for `Page.ready`; then it finds the element its
|
|
11
11
|
* subject names, by role and name among those that repeat all its recorded context, and acts on
|
|
12
12
|
* it. Text typed into focus was typed into the focused field, which replay finds the same way, and
|
|
13
13
|
* text typed into a secret field goes into nothing else. An element step never falls back to
|
|
@@ -382,7 +382,7 @@ const dispatch = (
|
|
|
382
382
|
};
|
|
383
383
|
|
|
384
384
|
export interface TakeOptions {
|
|
385
|
-
/** How each step waits
|
|
385
|
+
/** How each step waits first, except an opening navigation, or false. No final-step wait. */
|
|
386
386
|
readonly settle?: ReadyOptions | false | undefined;
|
|
387
387
|
readonly inputs?: Readonly<Record<string, string>> | undefined;
|
|
388
388
|
/**
|
|
@@ -527,9 +527,11 @@ export const take = Effect.fn("Plan.take")(function* (
|
|
|
527
527
|
(unresolved || (expected !== undefined && "durable" in expected && expected.durable === true))
|
|
528
528
|
)
|
|
529
529
|
return yield* uncertain;
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
530
|
+
if (
|
|
531
|
+
options.settle !== false &&
|
|
532
|
+
(index > 0 || !["navigate", "back", "reload"].includes(step.action))
|
|
533
|
+
)
|
|
534
|
+
yield* page.ready(options.settle);
|
|
533
535
|
const before = plan.steps[index - 1]?.url;
|
|
534
536
|
|
|
535
537
|
if (before !== undefined)
|
|
@@ -551,7 +553,6 @@ export const take = Effect.fn("Plan.take")(function* (
|
|
|
551
553
|
|
|
552
554
|
const receipt = (outcome: Receipt["outcome"]) =>
|
|
553
555
|
Effect.gen(function* () {
|
|
554
|
-
if (index === plan.steps.length - 1) yield* settle;
|
|
555
556
|
yield* arrived(page, [step.url], millis);
|
|
556
557
|
const now = yield* page.state;
|
|
557
558
|
|
package/src/Stage.ts
CHANGED
|
@@ -50,9 +50,9 @@ export interface Cue {
|
|
|
50
50
|
}) => Effect.Effect<Presented, BrowserError>;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
/** What the output establishes
|
|
53
|
+
/** What the output establishes at nondecreasing host times; frames retain their capture times. */
|
|
54
54
|
export type Shown =
|
|
55
|
-
| { readonly _tag: "Painted"; readonly frame: Frame }
|
|
55
|
+
| { readonly _tag: "Painted"; readonly frame: Frame; readonly at: number }
|
|
56
56
|
| { readonly _tag: "Still"; readonly frame: Frame; readonly at: number }
|
|
57
57
|
| { readonly _tag: "Stalled"; readonly frame: Frame; readonly at: number }
|
|
58
58
|
| { readonly _tag: "Gone"; readonly page: string; readonly at: number };
|
|
@@ -65,7 +65,7 @@ export interface Stage {
|
|
|
65
65
|
readonly cue: (page: Page) => Effect.Effect<Cue, BrowserError, Scope.Scope>;
|
|
66
66
|
/** Paint and, after every finite positive hold without paint, current evidence or loss. */
|
|
67
67
|
readonly shown: (options: { readonly hold: Duration.Input }) => Stream.Stream<Shown>;
|
|
68
|
-
/**
|
|
68
|
+
/** Capture-timed current-page paint; use `shown` for output time. Readers retain 16 frames. */
|
|
69
69
|
readonly frames: Stream.Stream<Frame>;
|
|
70
70
|
readonly current: Effect.Effect<Option.Option<Page>>;
|
|
71
71
|
readonly stats: (options?: {
|
|
@@ -87,9 +87,17 @@ interface Feed {
|
|
|
87
87
|
interface Paint {
|
|
88
88
|
readonly feed: Feed;
|
|
89
89
|
readonly frame: Frame;
|
|
90
|
+
readonly at: number;
|
|
90
91
|
}
|
|
91
92
|
|
|
92
|
-
|
|
93
|
+
interface Evidence {
|
|
94
|
+
readonly feed: Feed;
|
|
95
|
+
readonly frame: Frame;
|
|
96
|
+
readonly value: Shown;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const unavailable = () =>
|
|
100
|
+
undispatched("cue", new InvalidRequest({ detail: "the cue's capture is no longer available" }));
|
|
93
101
|
|
|
94
102
|
/** The sole writer of a cue's capture lifecycle; presentation ownership belongs to the stage. */
|
|
95
103
|
const update = (
|
|
@@ -131,13 +139,13 @@ const ended = (feed: Feed): Effect.Effect<never, BrowserError> =>
|
|
|
131
139
|
Effect.flatMap((exit) =>
|
|
132
140
|
Exit.isFailure(exit) && !Cause.hasInterruptsOnly(exit.cause)
|
|
133
141
|
? Effect.failCause(exit.cause)
|
|
134
|
-
: Effect.fail(
|
|
142
|
+
: Effect.fail(unavailable()),
|
|
135
143
|
),
|
|
136
144
|
);
|
|
137
145
|
|
|
138
146
|
const firstOf = (feed: Feed, wait: Duration.Duration) =>
|
|
139
147
|
Effect.suspend(() => {
|
|
140
|
-
if (feed.state._tag === "Stopped") return Effect.fail(
|
|
148
|
+
if (feed.state._tag === "Stopped") return Effect.fail(unavailable());
|
|
141
149
|
if (feed.page.playwright.isClosed()) return failWith("cue", new Closed({ cause: "page" }));
|
|
142
150
|
if (frameOf(feed) !== undefined) return Effect.void;
|
|
143
151
|
|
|
@@ -225,6 +233,8 @@ const evidence = (
|
|
|
225
233
|
throw new RangeError("shown hold must be a finite positive duration");
|
|
226
234
|
const millis = Duration.toMillis(parsed.value);
|
|
227
235
|
let checkedAt = now();
|
|
236
|
+
let shownAt = Number.NEGATIVE_INFINITY;
|
|
237
|
+
let seen: Pick<Paint, "feed" | "frame"> | undefined = current();
|
|
228
238
|
let gone: Feed | undefined;
|
|
229
239
|
|
|
230
240
|
const probe = Effect.gen(function* () {
|
|
@@ -239,37 +249,67 @@ const evidence = (
|
|
|
239
249
|
gone === before.feed ||
|
|
240
250
|
checkedAt - before.changedAt < millis
|
|
241
251
|
)
|
|
242
|
-
return Option.none<
|
|
252
|
+
return Option.none<Evidence>();
|
|
243
253
|
const { feed } = before;
|
|
244
254
|
|
|
245
255
|
const confirmed = yield* Effect.result(confirm(feed, frame));
|
|
246
256
|
|
|
247
|
-
if (current()?.feed !== feed || current()?.frame !== frame) return Option.none<
|
|
257
|
+
if (current()?.feed !== feed || current()?.frame !== frame) return Option.none<Evidence>();
|
|
248
258
|
const at = now();
|
|
249
259
|
|
|
250
260
|
checkedAt = at;
|
|
251
261
|
if (Result.isFailure(confirmed) && consequence(confirmed.failure).lost !== "nothing") {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
262
|
+
return Option.some<Evidence>({
|
|
263
|
+
feed,
|
|
264
|
+
frame,
|
|
265
|
+
value: { _tag: "Gone", page: feed.page.id, at },
|
|
266
|
+
});
|
|
255
267
|
}
|
|
256
268
|
|
|
257
|
-
return Option.some<
|
|
258
|
-
|
|
269
|
+
return Option.some<Evidence>({
|
|
270
|
+
feed,
|
|
259
271
|
frame,
|
|
260
|
-
|
|
272
|
+
value: {
|
|
273
|
+
_tag: Result.isSuccess(confirmed) && confirmed.success ? "Still" : "Stalled",
|
|
274
|
+
frame,
|
|
275
|
+
at,
|
|
276
|
+
},
|
|
261
277
|
});
|
|
262
278
|
});
|
|
263
279
|
|
|
264
280
|
return Stream.merge(
|
|
265
281
|
Stream.fromPubSub(output).pipe(
|
|
266
|
-
Stream.
|
|
267
|
-
|
|
282
|
+
Stream.map(({ feed, frame, at }): Evidence => ({
|
|
283
|
+
feed,
|
|
284
|
+
frame,
|
|
285
|
+
value: { _tag: "Painted", frame, at },
|
|
286
|
+
})),
|
|
268
287
|
),
|
|
269
288
|
Stream.fromEffectRepeat(probe).pipe(
|
|
270
289
|
Stream.filter(Option.isSome),
|
|
271
290
|
Stream.map(({ value }) => value),
|
|
272
291
|
),
|
|
292
|
+
).pipe(
|
|
293
|
+
// A probe can overtake queued paint while its reader is paused. It establishes nothing
|
|
294
|
+
// about that reader's output until the matching frame has reached it.
|
|
295
|
+
Stream.filter(({ feed, frame, value }) => {
|
|
296
|
+
if (value._tag === "Painted" && gone !== feed) {
|
|
297
|
+
seen = { feed, frame };
|
|
298
|
+
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
// The subscription may have begun after its first publication.
|
|
302
|
+
seen ??= { feed, frame };
|
|
303
|
+
|
|
304
|
+
return value._tag !== "Painted" && seen?.feed === feed && seen.frame === frame;
|
|
305
|
+
}),
|
|
306
|
+
Stream.map(({ feed, value }) => {
|
|
307
|
+
if (value._tag === "Gone") gone = feed;
|
|
308
|
+
// A frame captured before a silence probe can arrive after the probe's evidence.
|
|
309
|
+
shownAt = Math.max(shownAt, value.at);
|
|
310
|
+
|
|
311
|
+
return value.at === shownAt ? value : { ...value, at: shownAt };
|
|
312
|
+
}),
|
|
273
313
|
);
|
|
274
314
|
}),
|
|
275
315
|
);
|
|
@@ -375,15 +415,15 @@ export const make = Effect.fn("Stage.make")(function* (
|
|
|
375
415
|
|
|
376
416
|
yield* Effect.addFinalizer(() => PubSub.shutdown(output));
|
|
377
417
|
|
|
378
|
-
const publish = (feed: Feed, frame: Frame) =>
|
|
418
|
+
const publish = (feed: Feed, frame: Frame, at = frame.hostTime) =>
|
|
379
419
|
Effect.suspend(() => {
|
|
380
|
-
painted = Math.max(painted,
|
|
420
|
+
painted = Math.max(painted, at);
|
|
381
421
|
if (showing !== undefined) {
|
|
382
422
|
showing.changedAt = now();
|
|
383
423
|
showing.frame = frame;
|
|
384
424
|
}
|
|
385
425
|
|
|
386
|
-
return PubSub.publish(output, { feed, frame });
|
|
426
|
+
return PubSub.publish(output, { feed, frame, at: painted });
|
|
387
427
|
});
|
|
388
428
|
|
|
389
429
|
const commit = (feed: Feed, options: { readonly at?: number | undefined } = {}) =>
|
|
@@ -407,7 +447,7 @@ export const make = Effect.fn("Stage.make")(function* (
|
|
|
407
447
|
const switchTo = Effect.suspend(() => {
|
|
408
448
|
const frame = frameOf(feed);
|
|
409
449
|
|
|
410
|
-
if (frame === undefined) return Effect.fail(
|
|
450
|
+
if (frame === undefined) return Effect.fail(unavailable());
|
|
411
451
|
const at = Math.max(now(), asked, frame.hostTime, painted);
|
|
412
452
|
|
|
413
453
|
showing = {
|
|
@@ -417,7 +457,7 @@ export const make = Effect.fn("Stage.make")(function* (
|
|
|
417
457
|
changedAt: at,
|
|
418
458
|
};
|
|
419
459
|
|
|
420
|
-
return publish(feed, frame).pipe(Effect.as(showing.presented));
|
|
460
|
+
return publish(feed, frame, at).pipe(Effect.as(showing.presented));
|
|
421
461
|
}).pipe(Effect.uninterruptible);
|
|
422
462
|
|
|
423
463
|
const readyToSwitch = firstOf(feed, firstFrame).pipe(
|
package/src/Supervisor.ts
CHANGED
|
@@ -92,9 +92,21 @@ export interface Opened<A = void> {
|
|
|
92
92
|
readonly release?: Effect.Effect<Lifecycle.Released> | undefined;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
/** Facts available before an acquisition attempt; awaiting `after` never changes release order. */
|
|
96
|
+
export interface Admission {
|
|
97
|
+
readonly number: number;
|
|
98
|
+
/**
|
|
99
|
+
* The preceding generation's release once it stops serving. Undefined on the first open and
|
|
100
|
+
* while a nonexclusive rotation keeps it serving; checked again before each retry.
|
|
101
|
+
*/
|
|
102
|
+
readonly after: Effect.Effect<Lifecycle.Released> | undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
95
105
|
export interface Options<E, R, A = void> {
|
|
96
106
|
/** Opens one generation in the scope it is given. */
|
|
97
107
|
readonly open: Effect.Effect<Opened<A>, E, R>;
|
|
108
|
+
/** Runs before every open attempt, including the first; failures follow `reopen` and `definite`. */
|
|
109
|
+
readonly admit?: ((opening: Admission) => Effect.Effect<void, E, R>) | undefined;
|
|
98
110
|
/** When to try a failed open again. Defaults to exponential from 1 second, for 5 minutes. */
|
|
99
111
|
readonly reopen?: Schedule.Schedule<unknown, E> | undefined;
|
|
100
112
|
/**
|
|
@@ -132,6 +144,8 @@ export interface Lease<A> {
|
|
|
132
144
|
readonly generation: number;
|
|
133
145
|
readonly browser: Browser.Service;
|
|
134
146
|
readonly value: A;
|
|
147
|
+
/** Declare this serving generation unusable and revoke it now; stale declarations do nothing. */
|
|
148
|
+
readonly lose: (detail: string) => Effect.Effect<void>;
|
|
135
149
|
/** First notice that serving ended; loss or retirement may revoke before its deadline. */
|
|
136
150
|
readonly ending: Effect.Effect<Superseded>;
|
|
137
151
|
/** An independent scope; its resources close before provider release. Fails once revoked. */
|
|
@@ -167,6 +181,7 @@ interface Held<A> {
|
|
|
167
181
|
readonly opened: Opened<A>;
|
|
168
182
|
readonly ending: Deferred.Deferred<Superseded>;
|
|
169
183
|
readonly revoked: Deferred.Deferred<Superseded>;
|
|
184
|
+
readonly released: Deferred.Deferred<Lifecycle.Released>;
|
|
170
185
|
readonly scope: Scope.Closeable;
|
|
171
186
|
}
|
|
172
187
|
|
|
@@ -175,6 +190,9 @@ type Apply<A> = (
|
|
|
175
190
|
input: Lifecycle.Input<Held<A>, Hold>,
|
|
176
191
|
) => Effect.Effect<Lifecycle.Step<Held<A>, Hold>>;
|
|
177
192
|
|
|
193
|
+
const superseded = (generation: number, { reason, revokeAt }: Lifecycle.Draining) =>
|
|
194
|
+
new Superseded({ generation, reason, revokeAt });
|
|
195
|
+
|
|
178
196
|
const retired = () => new Unavailable({ reason: "retired", detail: "the supervisor was retired" });
|
|
179
197
|
|
|
180
198
|
const describe = (cause: Cause.Cause<unknown>) => {
|
|
@@ -278,6 +296,8 @@ const leasing = <A>(
|
|
|
278
296
|
generation: live.number,
|
|
279
297
|
browser: live.value.opened.browser,
|
|
280
298
|
value: live.value.opened.value,
|
|
299
|
+
lose: (detail) =>
|
|
300
|
+
apply({ _tag: "Lost", number: live.number, cause: "declared", detail }).pipe(Effect.asVoid),
|
|
281
301
|
ending: Deferred.await(live.value.ending),
|
|
282
302
|
retain: Effect.uninterruptible(
|
|
283
303
|
Effect.gen(function* () {
|
|
@@ -310,9 +330,18 @@ const leasing = <A>(
|
|
|
310
330
|
apply({ _tag: "Let", number: live.number, hold }).pipe(Effect.asVoid),
|
|
311
331
|
);
|
|
312
332
|
|
|
333
|
+
const revoked = Deferred.await(live.value.revoked).pipe(Effect.flatMap(Effect.fail));
|
|
334
|
+
|
|
313
335
|
return yield* Effect.raceFirst(
|
|
314
|
-
work(leaseOf(live))
|
|
315
|
-
|
|
336
|
+
work(leaseOf(live)).pipe(
|
|
337
|
+
// Declaring loss and returning can finish before the race's other fiber runs.
|
|
338
|
+
Effect.tap(() =>
|
|
339
|
+
Deferred.isDone(live.value.revoked).pipe(
|
|
340
|
+
Effect.flatMap((done) => (done ? revoked : Effect.void)),
|
|
341
|
+
),
|
|
342
|
+
),
|
|
343
|
+
),
|
|
344
|
+
revoked,
|
|
316
345
|
);
|
|
317
346
|
}),
|
|
318
347
|
);
|
|
@@ -338,6 +367,7 @@ const acquireGeneration = <A, E, R>(
|
|
|
338
367
|
scope,
|
|
339
368
|
ending: Deferred.makeUnsafe<Superseded>(),
|
|
340
369
|
revoked: Deferred.makeUnsafe<Superseded>(),
|
|
370
|
+
released: Deferred.makeUnsafe<Lifecycle.Released>(),
|
|
341
371
|
})),
|
|
342
372
|
),
|
|
343
373
|
),
|
|
@@ -369,23 +399,14 @@ const watchGeneration = <A>(
|
|
|
369
399
|
return Effect.all([loss, rotation], { concurrency: 2, discard: true });
|
|
370
400
|
};
|
|
371
401
|
|
|
372
|
-
//
|
|
402
|
+
// After revocation, retained resources and use finalizers let go before the provider.
|
|
373
403
|
const releaseGeneration = <A>(
|
|
374
404
|
command: Extract<Lifecycle.Command<Held<A>>, { readonly _tag: "Release" | "Leave" }>,
|
|
375
405
|
state: SubscriptionRef.SubscriptionRef<State<A>>,
|
|
376
406
|
apply: Apply<A>,
|
|
377
407
|
) =>
|
|
378
408
|
Effect.gen(function* () {
|
|
379
|
-
const { live
|
|
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
|
-
);
|
|
409
|
+
const { live } = command;
|
|
389
410
|
const holds = Lifecycle.lookup(state.value, live.number)?.holds ?? [];
|
|
390
411
|
|
|
391
412
|
const cleaned = yield* Effect.forEach(
|
|
@@ -414,9 +435,25 @@ const releaseGeneration = <A>(
|
|
|
414
435
|
const closed = yield* Effect.exit(Scope.close(live.value.scope, Exit.void));
|
|
415
436
|
|
|
416
437
|
yield* apply({ _tag: "Released", number: live.number, released });
|
|
438
|
+
if (released !== undefined) yield* Deferred.succeed(live.value.released, released);
|
|
417
439
|
yield* Exit.asVoidAll([...cleaned, closed]);
|
|
418
440
|
});
|
|
419
441
|
|
|
442
|
+
const openedInput = <A, E>(
|
|
443
|
+
number: number,
|
|
444
|
+
exit: Exit.Exit<Held<A>, E>,
|
|
445
|
+
): Lifecycle.Input<Held<A>, Hold> =>
|
|
446
|
+
Exit.isSuccess(exit)
|
|
447
|
+
? { _tag: "Opened", live: { number, value: exit.value } }
|
|
448
|
+
: Cause.hasInterruptsOnly(exit.cause)
|
|
449
|
+
? { _tag: "Abandoned", number }
|
|
450
|
+
: { _tag: "Failed", number, detail: describe(exit.cause), cause: Cause.squash(exit.cause) };
|
|
451
|
+
|
|
452
|
+
const admissionAfter = <A>(previous: Lifecycle.Live<Held<A>> | undefined, state: State<A>) =>
|
|
453
|
+
previous === undefined || (state._tag !== "Retired" && state.serving?.number === previous.number)
|
|
454
|
+
? undefined
|
|
455
|
+
: Deferred.await(previous.value.released);
|
|
456
|
+
|
|
420
457
|
const makeScoped = Effect.fn("Supervisor.make")(function* <E, R, A>(
|
|
421
458
|
options: MakeOptions<E, R, A>,
|
|
422
459
|
): Effect.fn.Return<Supervisor<A | void>, never, Scope.Scope | Exclude<R, Scope.Scope>> {
|
|
@@ -489,11 +526,7 @@ const makeScoped = Effect.fn("Supervisor.make")(function* <E, R, A>(
|
|
|
489
526
|
);
|
|
490
527
|
break;
|
|
491
528
|
case "Drain": {
|
|
492
|
-
const ending =
|
|
493
|
-
generation: command.live.number,
|
|
494
|
-
reason: command.ending.reason,
|
|
495
|
-
revokeAt: command.ending.revokeAt,
|
|
496
|
-
});
|
|
529
|
+
const ending = superseded(command.live.number, command.ending);
|
|
497
530
|
|
|
498
531
|
yield* Deferred.succeed(command.live.value.ending, ending);
|
|
499
532
|
yield* Effect.forkIn(
|
|
@@ -508,40 +541,39 @@ const makeScoped = Effect.fn("Supervisor.make")(function* <E, R, A>(
|
|
|
508
541
|
}
|
|
509
542
|
case "Release":
|
|
510
543
|
case "Leave":
|
|
544
|
+
yield* Deferred.succeed(
|
|
545
|
+
command.live.value.revoked,
|
|
546
|
+
superseded(command.live.number, command.ending),
|
|
547
|
+
);
|
|
511
548
|
runRelease(command.live.number, releaseGeneration(command, state, apply));
|
|
512
549
|
break;
|
|
513
550
|
}
|
|
514
551
|
});
|
|
515
552
|
|
|
516
|
-
const
|
|
553
|
+
const openGeneration = (number: number, previous: Lifecycle.Live<Held<A | void>> | undefined) => {
|
|
554
|
+
const after = previous === undefined ? undefined : Deferred.await(previous.value.released);
|
|
555
|
+
|
|
556
|
+
const openOnce = acquireGeneration(
|
|
557
|
+
Effect.suspend(
|
|
558
|
+
() =>
|
|
559
|
+
options.admit?.({ number, after: admissionAfter(previous, state.value) }) ?? Effect.void,
|
|
560
|
+
).pipe(Effect.andThen(options.open)),
|
|
561
|
+
own,
|
|
562
|
+
).pipe(Effect.provideContext(context));
|
|
517
563
|
|
|
518
|
-
|
|
519
|
-
Effect.uninterruptibleMask((restore) =>
|
|
564
|
+
return Effect.uninterruptibleMask((restore) =>
|
|
520
565
|
restore(
|
|
521
|
-
(after
|
|
522
|
-
? Effect.void
|
|
523
|
-
: changedUntil(state, (current) => !current.draining.has(after))
|
|
524
|
-
).pipe(
|
|
566
|
+
(exclusive && after !== undefined ? after : Effect.void).pipe(
|
|
525
567
|
Effect.andThen(
|
|
526
568
|
Effect.retry(openOnce, { schedule: reopen, while: (error) => !definite(error) }),
|
|
527
569
|
),
|
|
528
570
|
),
|
|
529
571
|
).pipe(
|
|
530
572
|
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
|
-
),
|
|
573
|
+
Effect.flatMap((exit) => apply(openedInput(number, exit))),
|
|
543
574
|
),
|
|
544
575
|
);
|
|
576
|
+
};
|
|
545
577
|
|
|
546
578
|
const use = leasing(state, apply, waitTimeout, now, clock);
|
|
547
579
|
|