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.
Files changed (52) hide show
  1. package/README.md +50 -19
  2. package/dist/{Browser-QiT6Cdn_.mjs → Browser-BBSySyjT.mjs} +2 -2
  3. package/dist/{Browser-QiT6Cdn_.mjs.map → Browser-BBSySyjT.mjs.map} +1 -1
  4. package/dist/Browser.d.mts +1 -1
  5. package/dist/Browser.mjs +1 -1
  6. package/dist/Cdp.mjs +1 -1
  7. package/dist/Chromium.mjs +1 -1
  8. package/dist/Expect.d.mts +1 -1
  9. package/dist/Expect.mjs +1 -1
  10. package/dist/{Frame-Di2PxyF7.d.mts → Frame-DkNdjjjr.d.mts} +3 -5
  11. package/dist/Frame.d.mts +1 -1
  12. package/dist/Frame.mjs +2 -4
  13. package/dist/Frame.mjs.map +1 -1
  14. package/dist/Moment.d.mts +1 -1
  15. package/dist/Moment.mjs +7 -3
  16. package/dist/Moment.mjs.map +1 -1
  17. package/dist/{Page-B7X6DQUn.d.mts → Page-DL9eylp3.d.mts} +2 -2
  18. package/dist/Page.d.mts +1 -1
  19. package/dist/{Plan-BS0jpENY.d.mts → Plan-6As6vcwT.d.mts} +3 -3
  20. package/dist/{Plan-Bl2NM1D5.mjs → Plan-CXA2BW-G.mjs} +4 -6
  21. package/dist/Plan-CXA2BW-G.mjs.map +1 -0
  22. package/dist/Plan.d.mts +1 -1
  23. package/dist/Plan.mjs +1 -1
  24. package/dist/Policy.d.mts +1 -1
  25. package/dist/Presentation.d.mts +1 -1
  26. package/dist/Presentation.mjs +1 -1
  27. package/dist/Stage.d.mts +5 -4
  28. package/dist/Stage.mjs +53 -20
  29. package/dist/Stage.mjs.map +1 -1
  30. package/dist/{Supervisor-TNKNOVzu.mjs → Supervisor-BE2sGHBK.mjs} +67 -41
  31. package/dist/Supervisor-BE2sGHBK.mjs.map +1 -0
  32. package/dist/{Supervisor-CYWFK3vM.d.mts → Supervisor-W60pZWHh.d.mts} +24 -8
  33. package/dist/Supervisor.d.mts +2 -2
  34. package/dist/Supervisor.mjs +1 -1
  35. package/dist/index.d.mts +4 -4
  36. package/dist/index.mjs +3 -3
  37. package/dist/{page-ZxMhw16l.mjs → page-B92QnRfT.mjs} +196 -166
  38. package/dist/page-B92QnRfT.mjs.map +1 -0
  39. package/package.json +1 -1
  40. package/src/Frame.ts +2 -4
  41. package/src/Moment.ts +14 -3
  42. package/src/Plan.ts +7 -6
  43. package/src/Stage.ts +61 -21
  44. package/src/Supervisor.ts +69 -37
  45. package/src/internal/pictures/capture.ts +172 -188
  46. package/src/internal/pictures/pictures.ts +50 -73
  47. package/src/internal/pictures/quiescence.ts +94 -0
  48. package/src/internal/reading/ready.ts +9 -3
  49. package/src/internal/supervisor/lifecycle.ts +27 -12
  50. package/dist/Plan-Bl2NM1D5.mjs.map +0 -1
  51. package/dist/Supervisor-TNKNOVzu.mjs.map +0 -1
  52. package/dist/page-ZxMhw16l.mjs.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "effect-browser",
3
- "version": "0.3.0-beta.13",
3
+ "version": "0.3.0-beta.14",
4
4
  "description": "Effect browser sessions, page control, screencast and moments over Playwright and CDP.",
5
5
  "license": "MIT",
6
6
  "repository": {
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 dropped because they may have been painted, or arrived, while the library took a clipped
97
- * or scaled picture of the page, such as a zoom: Chromium draws those into the screencast too.
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 at
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.frame({ after: "input" })).pipe(
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
- * that acts on the page, it waits for `Page.ready`; then it finds the one element the step's
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 for the page first, or false not to wait. */
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
- const settle = options.settle === false ? Effect.void : page.ready(options.settle);
531
-
532
- if (!["navigate", "back", "reload"].includes(step.action)) yield* settle;
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 about its last image; loss ends this page's evidence. */
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
- /** Current-page paint; a reader more than 16 frames behind loses the oldest. */
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
- const closed = () => undispatched("cue", new Closed({ cause: "released" }));
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(closed()),
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(closed());
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<Shown>();
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<Shown>();
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
- gone = feed;
253
-
254
- return Option.some<Shown>({ _tag: "Gone", page: feed.page.id, at });
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<Shown>({
258
- _tag: Result.isSuccess(confirmed) && confirmed.success ? "Still" : "Stalled",
269
+ return Option.some<Evidence>({
270
+ feed,
259
271
  frame,
260
- at,
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.filter(({ feed }) => gone !== feed),
267
- Stream.map(({ frame }): Shown => ({ _tag: "Painted", frame })),
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, frame.hostTime);
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(closed());
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
- Deferred.await(live.value.revoked).pipe(Effect.flatMap(Effect.fail)),
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
- // Revocation stops use work; retained resources and use finalizers let go before the provider.
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, ending } = command;
380
-
381
- yield* Deferred.succeed(
382
- live.value.revoked,
383
- new Superseded({
384
- generation: live.number,
385
- reason: ending.reason,
386
- revokeAt: ending.revokeAt,
387
- }),
388
- );
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 = new Superseded({
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 openOnce = acquireGeneration(options.open, own).pipe(Effect.provideContext(context));
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
- const openGeneration = (number: number, after: number | undefined) =>
519
- Effect.uninterruptibleMask((restore) =>
564
+ return Effect.uninterruptibleMask((restore) =>
520
565
  restore(
521
- (after === undefined
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