effect-browser 0.3.0-beta.14 → 0.3.0-beta.16

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 (83) hide show
  1. package/README.md +24 -11
  2. package/dist/{Browser-BBSySyjT.mjs → Browser-DRPthkNO.mjs} +4 -4
  3. package/dist/Browser-DRPthkNO.mjs.map +1 -0
  4. package/dist/Browser.d.mts +2 -2
  5. package/dist/Browser.mjs +1 -1
  6. package/dist/{BrowserEvent-BRu2juEz.d.mts → BrowserEvent-dq6FRT8w.d.mts} +9 -9
  7. package/dist/BrowserEvent.d.mts +1 -1
  8. package/dist/BrowserEvent.mjs +7 -12
  9. package/dist/BrowserEvent.mjs.map +1 -1
  10. package/dist/Cdp.mjs +2 -2
  11. package/dist/{Change-iMlcuzVD.d.mts → Change-DqING3tr.d.mts} +2 -2
  12. package/dist/Change.d.mts +1 -1
  13. package/dist/Chromium.mjs +2 -2
  14. package/dist/Expect.d.mts +1 -1
  15. package/dist/Expect.mjs +2 -2
  16. package/dist/Moment.d.mts +1 -1
  17. package/dist/Moment.mjs +1 -1
  18. package/dist/Motion-DND6I-64.d.mts +40 -0
  19. package/dist/Motion.d.mts +2 -40
  20. package/dist/Motion.mjs +1 -2
  21. package/dist/Motion.mjs.map +1 -1
  22. package/dist/{Page-B5cjaUp5.mjs → Page-C_VkBVwI.mjs} +5 -9
  23. package/dist/Page-C_VkBVwI.mjs.map +1 -0
  24. package/dist/{Page-DL9eylp3.d.mts → Page-jHmSJeOS.d.mts} +6 -13
  25. package/dist/Page.d.mts +1 -1
  26. package/dist/Page.mjs +1 -1
  27. package/dist/{Plan-6As6vcwT.d.mts → Plan-DLtRksuu.d.mts} +3 -3
  28. package/dist/{Plan-CXA2BW-G.mjs → Plan-X-JkACAB.mjs} +6 -9
  29. package/dist/Plan-X-JkACAB.mjs.map +1 -0
  30. package/dist/Plan.d.mts +1 -1
  31. package/dist/Plan.mjs +1 -1
  32. package/dist/Policy.d.mts +1 -1
  33. package/dist/Policy.mjs +1 -1
  34. package/dist/Presentation.d.mts +2 -2
  35. package/dist/Presentation.mjs +3 -2
  36. package/dist/Presentation.mjs.map +1 -1
  37. package/dist/Stage.d.mts +1 -1
  38. package/dist/Stage.mjs +3 -3
  39. package/dist/Stage.mjs.map +1 -1
  40. package/dist/{Supervisor-BE2sGHBK.mjs → Supervisor-BgWP8o9t.mjs} +116 -66
  41. package/dist/Supervisor-BgWP8o9t.mjs.map +1 -0
  42. package/dist/{Supervisor-W60pZWHh.d.mts → Supervisor-mVPeEUp8.d.mts} +33 -12
  43. package/dist/Supervisor.d.mts +2 -2
  44. package/dist/Supervisor.mjs +2 -2
  45. package/dist/{context-BAjRj7Xo.mjs → context-rYTk-3Cv.mjs} +68 -3
  46. package/dist/context-rYTk-3Cv.mjs.map +1 -0
  47. package/dist/index.d.mts +6 -6
  48. package/dist/index.mjs +4 -4
  49. package/dist/match.inpage-xOe9LNO0.mjs.map +1 -1
  50. package/dist/{page-B92QnRfT.mjs → page-ClEMFnBG.mjs} +48 -110
  51. package/dist/page-ClEMFnBG.mjs.map +1 -0
  52. package/package.json +1 -1
  53. package/src/Browser.ts +1 -1
  54. package/src/BrowserEvent.ts +7 -12
  55. package/src/Motion.ts +1 -2
  56. package/src/Page.ts +5 -9
  57. package/src/Plan.ts +1 -2
  58. package/src/Presentation.ts +2 -1
  59. package/src/Stage.ts +2 -1
  60. package/src/Supervisor.ts +168 -166
  61. package/src/internal/input/actions.ts +25 -33
  62. package/src/internal/input/dispatch.ts +3 -4
  63. package/src/internal/input/edit.inpage.ts +3 -9
  64. package/src/internal/input/evidence.inpage.ts +2 -20
  65. package/src/internal/input/guard.inpage.ts +9 -29
  66. package/src/internal/input/guard.ts +12 -1
  67. package/src/internal/input/targets.inpage.ts +4 -8
  68. package/src/internal/input/targets.ts +8 -11
  69. package/src/internal/page/context.ts +2 -2
  70. package/src/internal/pictures/capture.ts +4 -4
  71. package/src/internal/pictures/clock.ts +8 -3
  72. package/src/internal/pictures/pictures.ts +2 -1
  73. package/src/internal/reading/controls.inpage.ts +8 -1
  74. package/src/internal/reading/match.inpage.ts +2 -1
  75. package/src/internal/reading/outline.inpage.ts +7 -4
  76. package/src/internal/reading/reading.ts +5 -16
  77. package/src/internal/supervisor/lifecycle.ts +73 -20
  78. package/dist/Browser-BBSySyjT.mjs.map +0 -1
  79. package/dist/Page-B5cjaUp5.mjs.map +0 -1
  80. package/dist/Plan-CXA2BW-G.mjs.map +0 -1
  81. package/dist/Supervisor-BE2sGHBK.mjs.map +0 -1
  82. package/dist/context-BAjRj7Xo.mjs.map +0 -1
  83. package/dist/page-B92QnRfT.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.14",
3
+ "version": "0.3.0-beta.16",
4
4
  "description": "Effect browser sessions, page control, screencast and moments over Playwright and CDP.",
5
5
  "license": "MIT",
6
6
  "repository": {
package/src/Browser.ts CHANGED
@@ -492,7 +492,7 @@ export const make = Effect.fn("Browser.make")(function* (
492
492
  ) {
493
493
  const clock = yield* Clock.Clock;
494
494
  const connection = yield* Identity.make;
495
- const now = () => Number(clock.monotonicTimeNanosUnsafe()) / 1e6;
495
+ const now = () => BrowserClock.monotonicMillis(clock);
496
496
  const { settings, eventHistory, maxPages } = yield* settingsOf(options);
497
497
  // Measured on first need: the first capture of any page measures it, and input never waits.
498
498
  const mapping = BrowserClock.mapping(now);
@@ -119,8 +119,7 @@ const Button = Schema.Literals(["left", "right", "middle"]);
119
119
 
120
120
  /** An element's box in viewport CSS pixels; it may lie partly or wholly outside the viewport. */
121
121
  export const Box = Schema.Struct({
122
- x: Schema.Finite,
123
- y: Schema.Finite,
122
+ ...Motion.Point.fields,
124
123
  width: Schema.Finite,
125
124
  height: Schema.Finite,
126
125
  });
@@ -169,8 +168,8 @@ export class Action extends Schema.TaggedClass<Action>()("Action", {
169
168
  /** For an action at a point, the box of what it found there. */
170
169
  box: Schema.optional(Box),
171
170
  text: Schema.optional(Schema.String),
172
- x: Schema.optional(Schema.Finite),
173
- y: Schema.optional(Schema.Finite),
171
+ x: Schema.optional(Motion.Point.fields.x),
172
+ y: Schema.optional(Motion.Point.fields.y),
174
173
  ok: Schema.Boolean,
175
174
  dispatched: Schema.Boolean,
176
175
  error: Schema.optional(Schema.String),
@@ -196,16 +195,14 @@ export class TrackPerformed extends Schema.TaggedClass<TrackPerformed>()("TrackP
196
195
  page: Schema.String,
197
196
  plan: Sequence,
198
197
  dispatched: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
199
- x: Schema.Finite,
200
- y: Schema.Finite,
198
+ ...Motion.Point.fields,
201
199
  complete: Schema.Boolean,
202
200
  }) {}
203
201
 
204
202
  export class PointerPressed extends Schema.TaggedClass<PointerPressed>()("PointerPressed", {
205
203
  at: Schema.Finite,
206
204
  page: Schema.String,
207
- x: Schema.Finite,
208
- y: Schema.Finite,
205
+ ...Motion.Point.fields,
209
206
  button: Button,
210
207
  clickCount: Schema.Int.check(Schema.isGreaterThan(0)),
211
208
  }) {}
@@ -213,8 +210,7 @@ export class PointerPressed extends Schema.TaggedClass<PointerPressed>()("Pointe
213
210
  export class PointerReleased extends Schema.TaggedClass<PointerReleased>()("PointerReleased", {
214
211
  at: Schema.Finite,
215
212
  page: Schema.String,
216
- x: Schema.Finite,
217
- y: Schema.Finite,
213
+ ...Motion.Point.fields,
218
214
  button: Button,
219
215
  clickCount: Schema.Int.check(Schema.isGreaterThan(0)),
220
216
  }) {}
@@ -222,8 +218,7 @@ export class PointerReleased extends Schema.TaggedClass<PointerReleased>()("Poin
222
218
  export class WheelScrolled extends Schema.TaggedClass<WheelScrolled>()("WheelScrolled", {
223
219
  at: Schema.Finite,
224
220
  page: Schema.String,
225
- x: Schema.Finite,
226
- y: Schema.Finite,
221
+ ...Motion.Point.fields,
227
222
  dx: Schema.Finite,
228
223
  dy: Schema.Finite,
229
224
  }) {}
package/src/Motion.ts CHANGED
@@ -13,8 +13,7 @@ export const Point = Schema.Struct({ x: Schema.Finite, y: Schema.Finite });
13
13
  export type Point = typeof Point.Type;
14
14
 
15
15
  export const Sample = Schema.Struct({
16
- x: Schema.Finite,
17
- y: Schema.Finite,
16
+ ...Point.fields,
18
17
  afterMillis: Schema.Finite.check(
19
18
  Schema.isGreaterThanOrEqualTo(0),
20
19
  Schema.isLessThanOrEqualTo(maximumDurationMillis),
package/src/Page.ts CHANGED
@@ -27,12 +27,10 @@ import * as Guard from "./internal/input/guard.inpage.ts";
27
27
  import { Correlation, FailFast } from "./internal/page/lane.ts";
28
28
  import * as Url from "./internal/page/url.ts";
29
29
  import type { Window } from "./Moment.ts";
30
+ import * as Motion from "./Motion.ts";
30
31
  import { type Snapshot, type SnapshotOptions } from "./Snapshot.ts";
31
32
 
32
- export interface Point {
33
- readonly x: number;
34
- readonly y: number;
35
- }
33
+ export type Point = Motion.Point;
36
34
 
37
35
  /** A ref from a snapshot, such as `"e12"`, or a viewport point. */
38
36
  export type Target = string | Point;
@@ -54,7 +52,7 @@ export class Zoom extends Schema.Class<Zoom>("effect-browser/Zoom")({
54
52
 
55
53
  /** The target as it read before input. Point coordinates are never snapped to another position. */
56
54
  export class ResolvedTarget extends Schema.Class<ResolvedTarget>("effect-browser/ResolvedTarget")({
57
- point: Schema.Struct({ x: Schema.Finite, y: Schema.Finite }),
55
+ point: Motion.Point,
58
56
  element: Schema.String,
59
57
  /** The element's lowercase tag name. */
60
58
  tag: Schema.String,
@@ -131,9 +129,7 @@ export interface ScreenshotOptions {
131
129
  /** The oldest a reused frame may be, as for `frame`. Defaults to 250 milliseconds. */
132
130
  readonly maxAge?: Duration.Input | undefined;
133
131
  /** A region of the viewport, captured in CSS pixels at the viewport's own scale; always new. */
134
- readonly clip?:
135
- | { readonly x: number; readonly y: number; readonly width: number; readonly height: number }
136
- | undefined;
132
+ readonly clip?: Box | undefined;
137
133
  /** JPEG quality, 0 to 100. Defaults to 80. */
138
134
  readonly quality?: number | undefined;
139
135
  /** `"jpeg"`, the default, or `"png"`, which is always a new picture. */
@@ -335,7 +331,7 @@ export class InputRequest extends Schema.Class<InputRequest>("effect-browser/Inp
335
331
  target: Schema.optional(Schema.String),
336
332
  element: Schema.optional(Schema.String),
337
333
  /** Literal pixel targets only; ref coordinates are resolved after the policy allows them. */
338
- point: Schema.optional(Schema.Struct({ x: Schema.Finite, y: Schema.Finite })),
334
+ point: Schema.optional(Motion.Point),
339
335
  text: Schema.optional(Schema.String),
340
336
  role: Schema.optional(Schema.NullOr(Schema.String)),
341
337
  name: Schema.optional(Schema.String),
package/src/Plan.ts CHANGED
@@ -33,6 +33,7 @@ import {
33
33
  } from "./BrowserEvent.ts";
34
34
  import { undispatched } from "./internal/page/context.ts";
35
35
  import { Ambiguous, choose, Drifted, Missing } from "./internal/reading/choose.ts";
36
+ import { Point as ViewportPoint } from "./Motion.ts";
36
37
  import {
37
38
  type Found,
38
39
  type Page,
@@ -44,8 +45,6 @@ import {
44
45
 
45
46
  export { Ambiguous, Drifted, Missing } from "./internal/reading/choose.ts";
46
47
 
47
- const ViewportPoint = Schema.Struct({ x: Schema.Finite, y: Schema.Finite });
48
-
49
48
  export const StepAction = Schema.Literals([
50
49
  "navigate",
51
50
  "back",
@@ -20,6 +20,7 @@ import { Clock, Duration, Effect, Fiber, MutableRef, Option, Semaphore } from "e
20
20
  import type { BrowserError } from "./BrowserError.ts";
21
21
  import * as Style from "./internal/input/style.ts";
22
22
  import { type Internals, internalsOf, viewOf } from "./internal/page/page.ts";
23
+ import { monotonicMillis } from "./internal/pictures/clock.ts";
23
24
  import * as Motion from "./Motion.ts";
24
25
  import type { Page, Point, Target } from "./Page.ts";
25
26
 
@@ -81,7 +82,7 @@ export const make = Effect.fn("Presentation.make")(function* (options: Options =
81
82
  const pacing = options.pacing ?? human;
82
83
  const clock = yield* Clock.Clock;
83
84
  const scope = yield* Effect.scope;
84
- const now = () => Number(clock.monotonicTimeNanosUnsafe()) / 1e6;
85
+ const now = () => monotonicMillis(clock);
85
86
  const pointer = MutableRef.make(Option.none<Point>());
86
87
  const hand = yield* Semaphore.make(1);
87
88
 
package/src/Stage.ts CHANGED
@@ -26,6 +26,7 @@ import { type BrowserError, Closed, consequence, InvalidRequest, Timeout } from
26
26
  import type { CaptureStats, Frame, ScreencastOptions } from "./Frame.ts";
27
27
  import { failWith, undispatched } from "./internal/page/context.ts";
28
28
  import { internalsOf } from "./internal/page/page.ts";
29
+ import { monotonicMillis } from "./internal/pictures/clock.ts";
29
30
  import type { Page } from "./Page.ts";
30
31
 
31
32
  export interface Options extends ScreencastOptions {
@@ -390,7 +391,7 @@ export const make = Effect.fn("Stage.make")(function* (
390
391
  ): Effect.fn.Return<Stage, BrowserError, Scope.Scope> {
391
392
  const clock = yield* Clock.Clock;
392
393
  const owner = yield* Effect.scope;
393
- const now = () => Number(clock.monotonicTimeNanosUnsafe()) / 1e6;
394
+ const now = () => monotonicMillis(clock);
394
395
  const waited = Duration.fromInput(options.firstFrame ?? Duration.seconds(5));
395
396
 
396
397
  if (
package/src/Supervisor.ts CHANGED
@@ -21,6 +21,7 @@
21
21
  import {
22
22
  Cause,
23
23
  Clock,
24
+ Context,
24
25
  DateTime,
25
26
  Deferred,
26
27
  Duration,
@@ -96,12 +97,24 @@ export interface Opened<A = void> {
96
97
  export interface Admission {
97
98
  readonly number: number;
98
99
  /**
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.
100
+ * Every earlier session that stopped serving, including failed attempts and older drains.
101
+ * Waits for all pending releases; an Unconfirmed end remains visible to later attempts.
102
+ * Undefined before any release. A still-serving nonexclusive predecessor is excluded.
101
103
  */
102
104
  readonly after: Effect.Effect<Lifecycle.Released> | undefined;
103
105
  }
104
106
 
107
+ /**
108
+ * Optional service supplied during `open`. Providers report failed-attempt cleanup here before
109
+ * their scope closes; successful generations instead return `Opened.release`. Reporting does
110
+ * not release resources or replace the original acquisition failure. Terminal `Down` and opening
111
+ * `Closed` states publish the reported outcomes of their own generation's tries.
112
+ */
113
+ export class Releasing extends Context.Service<
114
+ Releasing,
115
+ { readonly released: (outcome: Lifecycle.Released) => Effect.Effect<void> }
116
+ >()("effect-browser/Supervisor/Releasing") {}
117
+
105
118
  export interface Options<E, R, A = void> {
106
119
  /** Opens one generation in the scope it is given. */
107
120
  readonly open: Effect.Effect<Opened<A>, E, R>;
@@ -186,9 +199,9 @@ interface Held<A> {
186
199
  }
187
200
 
188
201
  type State<A> = Lifecycle.State<Held<A>, Hold>;
189
- type Apply<A> = (
190
- input: Lifecycle.Input<Held<A>, Hold>,
191
- ) => Effect.Effect<Lifecycle.Step<Held<A>, Hold>>;
202
+ type Input<A> = Lifecycle.Input<Held<A>, Hold>;
203
+ type Step<A> = Lifecycle.Step<Held<A>, Hold>;
204
+ type Apply<A> = (input: Input<A>) => Effect.Effect<Step<A>>;
192
205
 
193
206
  const superseded = (generation: number, { reason, revokeAt }: Lifecycle.Draining) =>
194
207
  new Superseded({ generation, reason, revokeAt });
@@ -201,15 +214,6 @@ const describe = (cause: Cause.Cause<unknown>) => {
201
214
  return Predicate.isError(error) ? error.message : String(error);
202
215
  };
203
216
 
204
- const unavailable = (
205
- resolution: Extract<Lifecycle.Resolution<unknown>, { readonly _tag: "Unavailable" }>,
206
- ) =>
207
- new Unavailable({
208
- reason: resolution.reason,
209
- detail: resolution.detail,
210
- cause: resolution.cause,
211
- });
212
-
213
217
  const timedOut = () =>
214
218
  new Unavailable({ reason: "opening", detail: "no generation opened within the wait" });
215
219
 
@@ -234,7 +238,7 @@ const waitFor = <A, H>(
234
238
  const result = (resolution: Lifecycle.Resolution<A>) =>
235
239
  resolution._tag === "Serve"
236
240
  ? Effect.succeed(resolution.live)
237
- : Effect.fail(unavailable(resolution));
241
+ : Effect.fail(new Unavailable(resolution));
238
242
 
239
243
  return Effect.suspend(() => {
240
244
  const current = Lifecycle.resolve(state.value, target);
@@ -277,20 +281,19 @@ const leasing = <A>(
277
281
  now: () => number,
278
282
  clock: Clock.Clock,
279
283
  ) => {
280
- const borrow = (hold: Hold, wait: Duration.Duration) =>
281
- Effect.gen(function* () {
282
- const until = now() + Duration.toMillis(wait);
283
-
284
- for (;;) {
285
- const step = yield* apply({ _tag: "Hold", hold });
286
-
287
- if (step.borrowed !== undefined) return step.borrowed;
288
- yield* waitFor(state, Duration.millis(Math.max(0, until - now())), clock).pipe(
289
- Effect.interruptible,
290
- );
291
- if (now() >= until) return yield* timedOut();
292
- }
293
- });
284
+ const borrow = Effect.fnUntraced(function* (hold: Hold, wait: Duration.Duration) {
285
+ const until = now() + Duration.toMillis(wait);
286
+
287
+ for (;;) {
288
+ const step = yield* apply({ _tag: "Hold", hold });
289
+
290
+ if (step.borrowed !== undefined) return step.borrowed;
291
+ yield* waitFor(state, Duration.millis(Math.max(0, until - now())), clock).pipe(
292
+ Effect.interruptible,
293
+ );
294
+ if (now() >= until) return yield* timedOut();
295
+ }
296
+ });
294
297
 
295
298
  const leaseOf = (live: Lifecycle.Live<Held<A>>): Lease<A> => ({
296
299
  generation: live.number,
@@ -373,11 +376,6 @@ const acquireGeneration = <A, E, R>(
373
376
  ),
374
377
  );
375
378
 
376
- const changedUntil = <A>(
377
- state: SubscriptionRef.SubscriptionRef<State<A>>,
378
- predicate: (current: State<A>) => boolean,
379
- ) => SubscriptionRef.changes(state).pipe(Stream.filter(predicate), Stream.runHead, Effect.asVoid);
380
-
381
379
  const watchGeneration = <A>(
382
380
  live: Lifecycle.Live<Held<A>>,
383
381
  rotateBefore: Duration.Duration | undefined,
@@ -400,59 +398,61 @@ const watchGeneration = <A>(
400
398
  };
401
399
 
402
400
  // After revocation, retained resources and use finalizers let go before the provider.
403
- const releaseGeneration = <A>(
401
+ const releaseGeneration = Effect.fnUntraced(function* <A>(
404
402
  command: Extract<Lifecycle.Command<Held<A>>, { readonly _tag: "Release" | "Leave" }>,
405
403
  state: SubscriptionRef.SubscriptionRef<State<A>>,
406
404
  apply: Apply<A>,
407
- ) =>
408
- Effect.gen(function* () {
409
- const { live } = command;
410
- const holds = Lifecycle.lookup(state.value, live.number)?.holds ?? [];
411
-
412
- const cleaned = yield* Effect.forEach(
413
- holds,
414
- (hold) =>
415
- Effect.exit(hold._tag === "Retained" ? Scope.close(hold.scope, Exit.void) : Effect.void),
416
- { concurrency: 8 },
417
- );
405
+ ) {
406
+ const { live } = command;
407
+
408
+ const cleaned = yield* Effect.forEach(
409
+ Lifecycle.lookup(state.value, live.number)?.holds ?? [],
410
+ (hold) =>
411
+ Effect.exit(hold._tag === "Retained" ? Scope.close(hold.scope, Exit.void) : Effect.void),
412
+ { concurrency: 8 },
413
+ );
418
414
 
419
- yield* changedUntil(
420
- state,
421
- (current) => (Lifecycle.lookup(current, live.number)?.holds.size ?? 0) === 0,
422
- );
415
+ yield* SubscriptionRef.changes(state).pipe(
416
+ Stream.filter((current) => (Lifecycle.lookup(current, live.number)?.holds.size ?? 0) === 0),
417
+ Stream.runHead,
418
+ );
423
419
 
424
- const released =
425
- command._tag === "Leave"
426
- ? undefined
427
- : yield* (live.value.opened.release ?? Effect.succeed(new Lifecycle.Settled())).pipe(
428
- Effect.catchCause((cause) =>
429
- Effect.succeed(
430
- new Lifecycle.Unconfirmed({ detail: `the release failed: ${describe(cause)}` }),
431
- ),
420
+ const released =
421
+ command._tag === "Leave"
422
+ ? undefined
423
+ : yield* (live.value.opened.release ?? Effect.succeed(new Lifecycle.Settled())).pipe(
424
+ Effect.catchCause((cause) =>
425
+ Effect.succeed(
426
+ new Lifecycle.Unconfirmed({ detail: `the release failed: ${describe(cause)}` }),
432
427
  ),
433
- );
428
+ ),
429
+ );
434
430
 
435
- const closed = yield* Effect.exit(Scope.close(live.value.scope, Exit.void));
431
+ const closed = yield* Effect.exit(Scope.close(live.value.scope, Exit.void));
436
432
 
437
- yield* apply({ _tag: "Released", number: live.number, released });
438
- if (released !== undefined) yield* Deferred.succeed(live.value.released, released);
439
- yield* Exit.asVoidAll([...cleaned, closed]);
440
- });
433
+ yield* apply({ _tag: "Released", number: live.number, released });
434
+ if (released !== undefined) yield* Deferred.succeed(live.value.released, released);
435
+ yield* Exit.asVoidAll([...cleaned, closed]);
436
+ });
441
437
 
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)
438
+ const admissionAfter = <A>(state: State<A>) => {
439
+ const released = state.released;
440
+
441
+ const pending = [...state.draining.values()]
442
+ .filter((drain) => drain._tag !== "Leaving")
443
+ .map(({ live }) => live.value.released);
444
+
445
+ return pending.length === 0 && released === undefined
454
446
  ? undefined
455
- : Deferred.await(previous.value.released);
447
+ : Effect.gen(function* () {
448
+ let outcome = released;
449
+
450
+ for (const release of pending)
451
+ outcome = Lifecycle.combine(outcome, yield* Deferred.await(release));
452
+
453
+ return outcome ?? new Lifecycle.Settled();
454
+ });
455
+ };
456
456
 
457
457
  const makeScoped = Effect.fn("Supervisor.make")(function* <E, R, A>(
458
458
  options: MakeOptions<E, R, A>,
@@ -462,7 +462,6 @@ const makeScoped = Effect.fn("Supervisor.make")(function* <E, R, A>(
462
462
  const context = yield* Effect.context<Exclude<R, Scope.Scope>>();
463
463
  const own = yield* Scope.fork(yield* Effect.scope);
464
464
  const exclusive = options.exclusive ?? false;
465
- const definite = options.definite ?? (() => false);
466
465
 
467
466
  const reopen: Schedule.Schedule<unknown, E> =
468
467
  options.reopen ??
@@ -486,113 +485,116 @@ const makeScoped = Effect.fn("Supervisor.make")(function* <E, R, A>(
486
485
  const runOpen = yield* FiberHandle.runtime(opening)();
487
486
  const runRelease = yield* FiberMap.runtime(releases)();
488
487
 
489
- const apply = (
490
- input: Lifecycle.Input<Held<A | void>, Hold>,
491
- ): Effect.Effect<Lifecycle.Step<Held<A | void>, Hold>> =>
492
- applying
493
- .withPermit(
494
- Effect.gen(function* () {
495
- const current = state.value;
496
- const step = Lifecycle.transition(current, input, exclusive, now(), handover);
488
+ const apply: Apply<A | void> = Effect.fnUntraced(
489
+ function* (input: Input<A | void>) {
490
+ const current = state.value;
491
+ const step = Lifecycle.transition(current, input, exclusive, now(), handover);
497
492
 
498
- if (step.state !== current) yield* SubscriptionRef.set(state, step.state);
499
- yield* perform(step);
493
+ if (step.state !== current) yield* SubscriptionRef.set(state, step.state);
494
+ yield* perform(step);
495
+
496
+ return step;
497
+ },
498
+ (effect) => applying.withPermit(effect),
499
+ Effect.uninterruptible,
500
+ Effect.provideService(Clock.Clock, clock),
501
+ );
500
502
 
501
- return step;
503
+ const perform = Effect.fnUntraced(function* (step: Step<A | void>): Effect.fn.Return<void> {
504
+ for (const [number, generation] of step.events)
505
+ PubSub.publishUnsafe(events, [
506
+ new Generation({
507
+ number,
508
+ at: clock.currentTimeMillisUnsafe(),
509
+ hostTime: now(),
510
+ state: generation,
502
511
  }),
503
- )
504
- .pipe(Effect.uninterruptible, Effect.provideService(Clock.Clock, clock));
505
-
506
- const perform = (step: Lifecycle.Step<Held<A | void>, Hold>): Effect.Effect<void> =>
507
- Effect.gen(function* () {
508
- for (const [number, generation] of step.events)
509
- PubSub.publishUnsafe(events, [
510
- new Generation({
511
- number,
512
- at: clock.currentTimeMillisUnsafe(),
513
- hostTime: now(),
514
- state: generation,
515
- }),
516
- ]);
517
- for (const command of step.commands)
518
- switch (command._tag) {
519
- case "Open":
520
- runOpen(openGeneration(command.number, command.after));
521
- break;
522
- case "Watch":
523
- yield* Effect.forkIn(
524
- Effect.provideContext(watchGeneration(command.live, rotateBefore, apply), context),
525
- command.live.value.scope,
526
- );
527
- break;
528
- case "Drain": {
529
- const ending = superseded(command.live.number, command.ending);
530
-
531
- yield* Deferred.succeed(command.live.value.ending, ending);
532
- yield* Effect.forkIn(
533
- until(ending.revokeAt, now).pipe(
534
- Effect.andThen(
535
- Effect.suspend(() => apply({ _tag: "Revoke", number: command.live.number })),
536
- ),
512
+ ]);
513
+ for (const command of step.commands)
514
+ switch (command._tag) {
515
+ case "Open":
516
+ runOpen(openGeneration(command.number));
517
+ break;
518
+ case "Watch":
519
+ yield* Effect.forkIn(
520
+ Effect.provideContext(watchGeneration(command.live, rotateBefore, apply), context),
521
+ command.live.value.scope,
522
+ );
523
+ break;
524
+ case "Drain": {
525
+ const ending = superseded(command.live.number, command.ending);
526
+
527
+ yield* Deferred.succeed(command.live.value.ending, ending);
528
+ yield* Effect.forkIn(
529
+ until(ending.revokeAt, now).pipe(
530
+ Effect.andThen(
531
+ Effect.suspend(() => apply({ _tag: "Revoke", number: command.live.number })),
537
532
  ),
538
- command.live.value.scope,
539
- );
540
- break;
541
- }
542
- case "Release":
543
- case "Leave":
544
- yield* Deferred.succeed(
545
- command.live.value.revoked,
546
- superseded(command.live.number, command.ending),
547
- );
548
- runRelease(command.live.number, releaseGeneration(command, state, apply));
549
- break;
533
+ ),
534
+ command.live.value.scope,
535
+ );
536
+ break;
550
537
  }
551
- });
552
-
553
- const openGeneration = (number: number, previous: Lifecycle.Live<Held<A | void>> | undefined) => {
554
- const after = previous === undefined ? undefined : Deferred.await(previous.value.released);
538
+ case "Release":
539
+ case "Leave":
540
+ yield* Deferred.succeed(
541
+ command.live.value.revoked,
542
+ superseded(command.live.number, command.ending),
543
+ );
544
+ runRelease(command.live.number, releaseGeneration(command, state, apply));
545
+ break;
546
+ }
547
+ });
555
548
 
549
+ const openGeneration = (number: number) => {
556
550
  const openOnce = acquireGeneration(
557
- Effect.suspend(
558
- () =>
559
- options.admit?.({ number, after: admissionAfter(previous, state.value) }) ?? Effect.void,
560
- ).pipe(Effect.andThen(options.open)),
551
+ Effect.gen(function* () {
552
+ const after = admissionAfter(state.value);
553
+
554
+ if (exclusive && after !== undefined) yield* after;
555
+ yield* options.admit?.({ number, after }) ?? Effect.void;
556
+
557
+ return yield* options.open.pipe(
558
+ Effect.provideService(Releasing, {
559
+ released: (released) =>
560
+ apply({ _tag: "AttemptReleased", number, released }).pipe(Effect.asVoid),
561
+ }),
562
+ );
563
+ }),
561
564
  own,
562
565
  ).pipe(Effect.provideContext(context));
563
566
 
564
567
  return Effect.uninterruptibleMask((restore) =>
565
568
  restore(
566
- (exclusive && after !== undefined ? after : Effect.void).pipe(
567
- Effect.andThen(
568
- Effect.retry(openOnce, { schedule: reopen, while: (error) => !definite(error) }),
569
- ),
570
- ),
569
+ Effect.retry(openOnce, {
570
+ schedule: reopen,
571
+ while: (error) => !options.definite?.(error),
572
+ }),
571
573
  ).pipe(
572
574
  Effect.exit,
573
- Effect.flatMap((exit) => apply(openedInput(number, exit))),
575
+ Effect.flatMap((exit) => {
576
+ if (Exit.isSuccess(exit))
577
+ return apply({ _tag: "Opened", live: { number, value: exit.value } });
578
+ const cause = exit.cause;
579
+
580
+ return apply(
581
+ Cause.hasInterruptsOnly(cause)
582
+ ? { _tag: "Abandoned", number }
583
+ : { _tag: "Failed", number, detail: describe(cause), cause: Cause.squash(cause) },
584
+ );
585
+ }),
574
586
  ),
575
587
  );
576
588
  };
577
589
 
578
- const use = leasing(state, apply, waitTimeout, now, clock);
579
-
580
590
  const end = yield* Effect.cached(PubSub.publish(events, Exit.void));
581
591
 
582
- const stop = (input: Lifecycle.Input<Held<A | void>, Hold>) =>
583
- apply(input).pipe(
584
- Effect.andThen(FiberHandle.clear(opening)),
585
- Effect.andThen(FiberMap.awaitEmpty(releases)),
586
- Effect.andThen(
587
- Effect.suspend(() =>
588
- Deferred.isDone(releases.deferred).pipe(
589
- Effect.flatMap((failed) => (failed ? FiberMap.join(releases) : Effect.void)),
590
- ),
591
- ),
592
- ),
593
- Effect.ensuring(end),
594
- Effect.asVoid,
595
- );
592
+ const stop = Effect.fnUntraced(function* (input: Input<A | void>) {
593
+ yield* apply(input);
594
+ yield* FiberHandle.clear(opening);
595
+ yield* FiberMap.awaitEmpty(releases);
596
+ if (yield* Deferred.isDone(releases.deferred)) yield* FiberMap.join(releases);
597
+ }, Effect.ensuring(end));
596
598
 
597
599
  const retire = yield* Effect.cached(stop({ _tag: "Retire" }));
598
600
 
@@ -601,7 +603,7 @@ const makeScoped = Effect.fn("Supervisor.make")(function* <E, R, A>(
601
603
 
602
604
  return {
603
605
  browser: Effect.map(waitFor(state, waitTimeout, clock), (live) => live.value.opened.browser),
604
- use,
606
+ use: leasing(state, apply, waitTimeout, now, clock),
605
607
  states: Stream.fromPubSubTake(events),
606
608
  rotate: apply({ _tag: "Rotate" }).pipe(
607
609
  Effect.flatMap(({ reply }) =>