effect-browser 0.3.0-beta.7 → 0.3.0-beta.8

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/src/Supervisor.ts CHANGED
@@ -22,10 +22,10 @@ import {
22
22
  Cause,
23
23
  Clock,
24
24
  DateTime,
25
+ Deferred,
25
26
  Duration,
26
27
  Effect,
27
28
  Exit,
28
- Fiber,
29
29
  FiberHandle,
30
30
  FiberMap,
31
31
  Option,
@@ -34,6 +34,7 @@ import {
34
34
  Schedule,
35
35
  Schema,
36
36
  Scope,
37
+ Semaphore,
37
38
  Stream,
38
39
  SubscriptionRef,
39
40
  type Take,
@@ -45,6 +46,7 @@ import * as Lifecycle from "./internal/supervisor/lifecycle.ts";
45
46
  export {
46
47
  Closed,
47
48
  Down,
49
+ Draining,
48
50
  GenerationState,
49
51
  Kept,
50
52
  Lost,
@@ -60,6 +62,7 @@ export {
60
62
  export class Generation extends Schema.Class<Generation>("effect-browser/Generation")({
61
63
  number: Schema.Int.check(Schema.isGreaterThan(0)),
62
64
  at: Schema.Finite,
65
+ hostTime: Schema.Finite,
63
66
  state: Lifecycle.GenerationState,
64
67
  }) {}
65
68
 
@@ -78,7 +81,8 @@ export class Unavailable extends Schema.TaggedError<Unavailable>()("Unavailable"
78
81
  }
79
82
 
80
83
  /** One generation, as a provider opened it in the scope the supervisor gave it. */
81
- export interface Opened {
84
+ export interface Opened<A = void> {
85
+ readonly value: A;
82
86
  readonly browser: Browser.Service;
83
87
  /**
84
88
  * End the generation, and say whether the provider confirmed its end. The supervisor asks it
@@ -88,9 +92,9 @@ export interface Opened {
88
92
  readonly release?: Effect.Effect<Lifecycle.Released> | undefined;
89
93
  }
90
94
 
91
- export interface Options<E, R> {
95
+ export interface Options<E, R, A = void> {
92
96
  /** Opens one generation in the scope it is given. */
93
- readonly open: Effect.Effect<Opened, E, R>;
97
+ readonly open: Effect.Effect<Opened<A>, E, R>;
94
98
  /** When to try a failed open again. Defaults to exponential from 1 second, for 5 minutes. */
95
99
  readonly reopen?: Schedule.Schedule<unknown, E> | undefined;
96
100
  /**
@@ -105,6 +109,8 @@ export interface Options<E, R> {
105
109
  readonly rotateBefore?: Duration.Input | undefined;
106
110
  /** Generations must not overlap: a rotation releases the current one before it opens the next. */
107
111
  readonly exclusive?: boolean | undefined;
112
+ /** Finite, nonnegative time outgoing holds may finish; defaults to 30 seconds. */
113
+ readonly handover?: Duration.Input | undefined;
108
114
  /**
109
115
  * Closing the scope leaves the serving generation running, `Kept`: it closes the generation's
110
116
  * scope but never asks `release`. For providers whose scope only disconnects, such as
@@ -115,7 +121,34 @@ export interface Options<E, R> {
115
121
  readonly waitTimeout?: Duration.Input | undefined;
116
122
  }
117
123
 
118
- export interface Supervisor {
124
+ /** Work whose generation was revoked; input receipts still determine whether input reached a site. */
125
+ export class Superseded extends Schema.TaggedError<Superseded>()("Superseded", {
126
+ generation: Schema.Int,
127
+ reason: Schema.Literals(["rotated", "lost", "retired"]),
128
+ revokeAt: Schema.Finite,
129
+ }) {}
130
+
131
+ export interface Lease<A> {
132
+ readonly generation: number;
133
+ readonly browser: Browser.Service;
134
+ readonly value: A;
135
+ /** First notice that serving ended; loss or retirement may revoke before its deadline. */
136
+ readonly ending: Effect.Effect<Superseded>;
137
+ /** An independent scope; its resources close before provider release. Fails once revoked. */
138
+ readonly retain: Effect.Effect<Scope.Closeable, Superseded>;
139
+ }
140
+
141
+ export interface UseOptions {
142
+ /** Finite, nonnegative wait for a serving generation. Zero makes an immediate attempt. */
143
+ readonly wait?: Duration.Input | undefined;
144
+ }
145
+
146
+ export interface Supervisor<A = void> {
147
+ /** Scoped work on the current generation. Revocation interrupts it and waits for its cleanup. */
148
+ readonly use: <B, E, R>(
149
+ work: (lease: Lease<A>) => Effect.Effect<B, E, R>,
150
+ options?: UseOptions,
151
+ ) => Effect.Effect<B, E | Unavailable | Superseded, Exclude<R, Scope.Scope>>;
119
152
  /** The current generation's browser, waiting up to `waitTimeout` while none is open. */
120
153
  readonly browser: Effect.Effect<Browser.Service, Unavailable>;
121
154
  /** Each generation's changes, in order. A new reader first gets the latest 64; ends on retiring. */
@@ -126,11 +159,22 @@ export interface Supervisor {
126
159
  readonly retire: Effect.Effect<void>;
127
160
  }
128
161
 
129
- interface Held {
130
- readonly opened: Opened;
162
+ type Hold =
163
+ | { readonly _tag: "Use" }
164
+ | { readonly _tag: "Retained"; readonly scope: Scope.Closeable };
165
+
166
+ interface Held<A> {
167
+ readonly opened: Opened<A>;
168
+ readonly ending: Deferred.Deferred<Superseded>;
169
+ readonly revoked: Deferred.Deferred<Superseded>;
131
170
  readonly scope: Scope.Closeable;
132
171
  }
133
172
 
173
+ type State<A> = Lifecycle.State<Held<A>, Hold>;
174
+ type Apply<A> = (
175
+ input: Lifecycle.Input<Held<A>, Hold>,
176
+ ) => Effect.Effect<Lifecycle.Step<Held<A>, Hold>>;
177
+
134
178
  const retired = () => new Unavailable({ reason: "retired", detail: "the supervisor was retired" });
135
179
 
136
180
  const describe = (cause: Cause.Cause<unknown>) => {
@@ -139,46 +183,55 @@ const describe = (cause: Cause.Cause<unknown>) => {
139
183
  return Predicate.isError(error) ? error.message : String(error);
140
184
  };
141
185
 
142
- /**
143
- * Wait up to `waitTimeout` for generation `target`, or for any when it is undefined, as `state`
144
- * changes.
145
- */
146
- const waitFor = <A extends { readonly opened: Opened }>(
147
- state: SubscriptionRef.SubscriptionRef<Lifecycle.State<A>>,
148
- waitTimeout: Duration.Duration,
149
- target: number | undefined,
186
+ const unavailable = (
187
+ resolution: Extract<Lifecycle.Resolution<unknown>, { readonly _tag: "Unavailable" }>,
150
188
  ) =>
151
- SubscriptionRef.changes(state).pipe(
152
- Stream.map((current) => Lifecycle.resolve(current, target)),
153
- Stream.filter(Predicate.isNotUndefined),
154
- Stream.runHead,
155
- // The state's changes never end, so the first resolution always comes.
156
- Effect.flatMap(
157
- Option.match({
158
- onNone: () => Effect.fail(retired()),
159
- onSome: (resolution) =>
160
- resolution._tag === "Serve"
161
- ? Effect.succeed(resolution.live.value.opened.browser)
162
- : Effect.fail(
163
- new Unavailable({
164
- reason: resolution.reason,
165
- detail: resolution.detail,
166
- cause: resolution.cause,
167
- }),
168
- ),
169
- }),
170
- ),
171
- Effect.timeoutOrElse({
172
- duration: waitTimeout,
173
- orElse: () =>
174
- Effect.fail(
175
- new Unavailable({
176
- reason: "opening",
177
- detail: `no generation opened within ${Duration.format(waitTimeout)}`,
178
- }),
179
- ),
180
- }),
181
- );
189
+ new Unavailable({
190
+ reason: resolution.reason,
191
+ detail: resolution.detail,
192
+ cause: resolution.cause,
193
+ });
194
+
195
+ const timedOut = () =>
196
+ new Unavailable({ reason: "opening", detail: "no generation opened within the wait" });
197
+
198
+ const bounded = (input: Duration.Input, name: string) =>
199
+ Effect.suspend(() => {
200
+ const duration = Duration.fromInput(input);
201
+
202
+ return Option.isSome(duration) &&
203
+ Duration.isFinite(duration.value) &&
204
+ !Duration.isNegative(duration.value)
205
+ ? Effect.succeed(duration.value)
206
+ : Effect.die(new RangeError(`${name} must be a finite nonnegative duration`));
207
+ });
208
+
209
+ /** Resolve immediately before starting a bounded wait, including when its duration is zero. */
210
+ const waitFor = <A, H>(
211
+ state: SubscriptionRef.SubscriptionRef<Lifecycle.State<A, H>>,
212
+ wait: Duration.Duration,
213
+ clock: Clock.Clock,
214
+ target?: number,
215
+ ) => {
216
+ const result = (resolution: Lifecycle.Resolution<A>) =>
217
+ resolution._tag === "Serve"
218
+ ? Effect.succeed(resolution.live)
219
+ : Effect.fail(unavailable(resolution));
220
+
221
+ return Effect.suspend(() => {
222
+ const current = Lifecycle.resolve(state.value, target);
223
+
224
+ return current === undefined
225
+ ? SubscriptionRef.changes(state).pipe(
226
+ Stream.map((state) => Lifecycle.resolve(state, target)),
227
+ Stream.filter(Predicate.isNotUndefined),
228
+ Stream.runHead,
229
+ Effect.flatMap(Option.match({ onNone: () => Effect.fail(retired()), onSome: result })),
230
+ Effect.timeoutOrElse({ duration: wait, orElse: () => Effect.fail(timedOut()) }),
231
+ )
232
+ : result(current);
233
+ }).pipe(Effect.provideService(Clock.Clock, clock));
234
+ };
182
235
 
183
236
  /** Sleep until `before` ahead of `expiresAt`, but no sooner than halfway there from now. */
184
237
  const untilRotation = (expiresAt: DateTime.Utc, before: Duration.Duration) =>
@@ -190,11 +243,185 @@ const untilRotation = (expiresAt: DateTime.Utc, before: Duration.Duration) =>
190
243
  }),
191
244
  );
192
245
 
193
- /** Supervise generations of a provider's browser for as long as the scope is open. */
194
- export const make = Effect.fn("Supervisor.make")(function* <E, R>(
195
- options: Options<E, R>,
196
- ): Effect.fn.Return<Supervisor, never, Scope.Scope | Exclude<R, Scope.Scope>> {
246
+ /** Native timers may wake a fraction early; a revocation must reach its monotonic deadline. */
247
+ const until = (at: number, now: () => number): Effect.Effect<void> =>
248
+ Effect.suspend(() =>
249
+ now() >= at
250
+ ? Effect.void
251
+ : Effect.sleep(Duration.millis(Math.ceil(at - now()))).pipe(Effect.andThen(until(at, now))),
252
+ );
253
+
254
+ /** Borrowed work and transferred scopes hold a generation until cleanup lets it go. */
255
+ const leasing = <A>(
256
+ state: SubscriptionRef.SubscriptionRef<State<A>>,
257
+ apply: Apply<A>,
258
+ waitTimeout: Duration.Duration,
259
+ now: () => number,
260
+ clock: Clock.Clock,
261
+ ) => {
262
+ const borrow = (hold: Hold, wait: Duration.Duration) =>
263
+ Effect.gen(function* () {
264
+ const until = now() + Duration.toMillis(wait);
265
+
266
+ for (;;) {
267
+ const step = yield* apply({ _tag: "Hold", hold });
268
+
269
+ if (step.borrowed !== undefined) return step.borrowed;
270
+ yield* waitFor(state, Duration.millis(Math.max(0, until - now())), clock).pipe(
271
+ Effect.interruptible,
272
+ );
273
+ if (now() >= until) return yield* timedOut();
274
+ }
275
+ });
276
+
277
+ const leaseOf = (live: Lifecycle.Live<Held<A>>): Lease<A> => ({
278
+ generation: live.number,
279
+ browser: live.value.opened.browser,
280
+ value: live.value.opened.value,
281
+ ending: Deferred.await(live.value.ending),
282
+ retain: Effect.uninterruptible(
283
+ Effect.gen(function* () {
284
+ const scope = yield* Scope.make();
285
+ const hold: Hold = { _tag: "Retained", scope };
286
+ const step = yield* apply({ _tag: "Hold", number: live.number, hold });
287
+
288
+ if (step.borrowed === undefined) {
289
+ yield* Scope.close(scope, Exit.void);
290
+
291
+ return yield* yield* Deferred.await(live.value.ending);
292
+ }
293
+ yield* Scope.addFinalizer(
294
+ scope,
295
+ apply({ _tag: "Let", number: live.number, hold }).pipe(Effect.asVoid),
296
+ );
297
+
298
+ return scope;
299
+ }),
300
+ ),
301
+ });
302
+
303
+ const use: Supervisor<A>["use"] = (work, options = {}) =>
304
+ Effect.scoped(
305
+ Effect.gen(function* () {
306
+ const hold: Hold = { _tag: "Use" };
307
+ const wait = yield* bounded(options.wait ?? waitTimeout, "wait");
308
+
309
+ const live = yield* Effect.acquireRelease(borrow(hold, wait), (live) =>
310
+ apply({ _tag: "Let", number: live.number, hold }).pipe(Effect.asVoid),
311
+ );
312
+
313
+ return yield* Effect.raceFirst(
314
+ work(leaseOf(live)),
315
+ Deferred.await(live.value.revoked).pipe(Effect.flatMap(Effect.fail)),
316
+ );
317
+ }),
318
+ );
319
+
320
+ return use;
321
+ };
322
+
323
+ type Provider = Omit<Opened, "value">;
324
+ type MakeOptions<E, R, A> = Omit<Options<E, R, A>, "open"> & {
325
+ readonly open: Effect.Effect<Opened<A> | Provider, E, R>;
326
+ };
327
+
328
+ const acquireGeneration = <A, E, R>(
329
+ open: Effect.Effect<Opened<A> | Provider, E, R>,
330
+ own: Scope.Scope,
331
+ ) =>
332
+ Effect.uninterruptibleMask((restore) =>
333
+ Effect.flatMap(Scope.fork(own), (scope) =>
334
+ restore(open.pipe(Scope.provide(scope))).pipe(
335
+ Effect.onError((cause) => Scope.close(scope, Exit.failCause(cause))),
336
+ Effect.map((opened): Held<A | void> => ({
337
+ opened: { ...opened, value: "value" in opened ? opened.value : undefined },
338
+ scope,
339
+ ending: Deferred.makeUnsafe<Superseded>(),
340
+ revoked: Deferred.makeUnsafe<Superseded>(),
341
+ })),
342
+ ),
343
+ ),
344
+ );
345
+
346
+ const changedUntil = <A>(
347
+ state: SubscriptionRef.SubscriptionRef<State<A>>,
348
+ predicate: (current: State<A>) => boolean,
349
+ ) => SubscriptionRef.changes(state).pipe(Stream.filter(predicate), Stream.runHead, Effect.asVoid);
350
+
351
+ const watchGeneration = <A>(
352
+ live: Lifecycle.Live<Held<A>>,
353
+ rotateBefore: Duration.Duration | undefined,
354
+ apply: Apply<A>,
355
+ ) => {
356
+ const { browser } = live.value.opened;
357
+
358
+ const loss = browser.disconnected.pipe(
359
+ Effect.flatMap((cause) => apply({ _tag: "Lost", number: live.number, cause })),
360
+ );
361
+
362
+ const rotation =
363
+ browser.expiresAt === undefined || rotateBefore === undefined
364
+ ? Effect.void
365
+ : untilRotation(browser.expiresAt, rotateBefore).pipe(
366
+ Effect.andThen(apply({ _tag: "Rotate", due: live.number })),
367
+ );
368
+
369
+ return Effect.all([loss, rotation], { concurrency: 2, discard: true });
370
+ };
371
+
372
+ // Revocation stops use work; retained resources and use finalizers let go before the provider.
373
+ const releaseGeneration = <A>(
374
+ command: Extract<Lifecycle.Command<Held<A>>, { readonly _tag: "Release" | "Leave" }>,
375
+ state: SubscriptionRef.SubscriptionRef<State<A>>,
376
+ apply: Apply<A>,
377
+ ) =>
378
+ 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
+ );
389
+ const holds = Lifecycle.lookup(state.value, live.number)?.holds ?? [];
390
+
391
+ const cleaned = yield* Effect.forEach(
392
+ holds,
393
+ (hold) =>
394
+ Effect.exit(hold._tag === "Retained" ? Scope.close(hold.scope, Exit.void) : Effect.void),
395
+ { concurrency: 8 },
396
+ );
397
+
398
+ yield* changedUntil(
399
+ state,
400
+ (current) => (Lifecycle.lookup(current, live.number)?.holds.size ?? 0) === 0,
401
+ );
402
+
403
+ const released =
404
+ command._tag === "Leave"
405
+ ? undefined
406
+ : yield* (live.value.opened.release ?? Effect.succeed(new Lifecycle.Settled())).pipe(
407
+ Effect.catchCause((cause) =>
408
+ Effect.succeed(
409
+ new Lifecycle.Unconfirmed({ detail: `the release failed: ${describe(cause)}` }),
410
+ ),
411
+ ),
412
+ );
413
+
414
+ const closed = yield* Effect.exit(Scope.close(live.value.scope, Exit.void));
415
+
416
+ yield* apply({ _tag: "Released", number: live.number, released });
417
+ yield* Exit.asVoidAll([...cleaned, closed]);
418
+ });
419
+
420
+ const makeScoped = Effect.fn("Supervisor.make")(function* <E, R, A>(
421
+ options: MakeOptions<E, R, A>,
422
+ ): Effect.fn.Return<Supervisor<A | void>, never, Scope.Scope | Exclude<R, Scope.Scope>> {
197
423
  const clock = yield* Clock.Clock;
424
+ const now = () => Number(clock.monotonicTimeNanosUnsafe()) / 1e6;
198
425
  const context = yield* Effect.context<Exclude<R, Scope.Scope>>();
199
426
  const own = yield* Scope.fork(yield* Effect.scope);
200
427
  const exclusive = options.exclusive ?? false;
@@ -207,29 +434,48 @@ export const make = Effect.fn("Supervisor.make")(function* <E, R>(
207
434
  const rotateBefore =
208
435
  options.rotateBefore === undefined ? undefined : Duration.fromInputUnsafe(options.rotateBefore);
209
436
 
210
- const waitTimeout = Duration.fromInputUnsafe(options.waitTimeout ?? Duration.minutes(2));
211
- const started = Lifecycle.start<Held>();
437
+ const waitTimeout = yield* bounded(options.waitTimeout ?? Duration.minutes(2), "waitTimeout");
438
+
439
+ const handover = Duration.toMillis(
440
+ yield* bounded(options.handover ?? Duration.seconds(30), "handover"),
441
+ );
442
+
443
+ const started = Lifecycle.start<Held<A | void>, Hold>();
212
444
  const state = yield* SubscriptionRef.make(started.state);
445
+ const applying = yield* Semaphore.make(1);
213
446
  const events = yield* PubSub.unbounded<Take.Take<Generation>>({ replay: 64 });
214
447
  const opening = yield* FiberHandle.make<unknown, never>();
215
448
  const releases = yield* FiberMap.make<number, unknown, never>();
216
449
  const runOpen = yield* FiberHandle.runtime(opening)();
217
450
  const runRelease = yield* FiberMap.runtime(releases)();
218
451
 
219
- // Each input changes the state under one lock, which also publishes its events and starts its
220
- // work, so neither can be reordered against another input's.
221
- const apply = (input: Lifecycle.Input<Held>): Effect.Effect<number | undefined> =>
222
- SubscriptionRef.modifyEffect(state, (current) => {
223
- const step = Lifecycle.transition(current, input, exclusive);
452
+ const apply = (
453
+ input: Lifecycle.Input<Held<A | void>, Hold>,
454
+ ): Effect.Effect<Lifecycle.Step<Held<A | void>, Hold>> =>
455
+ applying
456
+ .withPermit(
457
+ Effect.gen(function* () {
458
+ const current = state.value;
459
+ const step = Lifecycle.transition(current, input, exclusive, now(), handover);
224
460
 
225
- return Effect.as(perform(step), [step.reply, step.state] as const);
226
- }).pipe(Effect.uninterruptible);
461
+ if (step.state !== current) yield* SubscriptionRef.set(state, step.state);
462
+ yield* perform(step);
463
+
464
+ return step;
465
+ }),
466
+ )
467
+ .pipe(Effect.uninterruptible, Effect.provideService(Clock.Clock, clock));
227
468
 
228
- const perform = (step: Lifecycle.Step<Held>) =>
469
+ const perform = (step: Lifecycle.Step<Held<A | void>, Hold>): Effect.Effect<void> =>
229
470
  Effect.gen(function* () {
230
471
  for (const [number, generation] of step.events)
231
472
  PubSub.publishUnsafe(events, [
232
- new Generation({ number, at: clock.currentTimeMillisUnsafe(), state: generation }),
473
+ new Generation({
474
+ number,
475
+ at: clock.currentTimeMillisUnsafe(),
476
+ hostTime: now(),
477
+ state: generation,
478
+ }),
233
479
  ]);
234
480
  for (const command of step.commands)
235
481
  switch (command._tag) {
@@ -238,100 +484,81 @@ export const make = Effect.fn("Supervisor.make")(function* <E, R>(
238
484
  break;
239
485
  case "Watch":
240
486
  yield* Effect.forkIn(
241
- Effect.provideContext(watch(command.live), context),
487
+ Effect.provideContext(watchGeneration(command.live, rotateBefore, apply), context),
242
488
  command.live.value.scope,
243
489
  );
244
490
  break;
245
- case "Release":
246
- runRelease(command.live.number, release(command.live));
491
+ case "Drain": {
492
+ const ending = new Superseded({
493
+ generation: command.live.number,
494
+ reason: command.ending.reason,
495
+ revokeAt: command.ending.revokeAt,
496
+ });
497
+
498
+ yield* Deferred.succeed(command.live.value.ending, ending);
499
+ yield* Effect.forkIn(
500
+ until(ending.revokeAt, now).pipe(
501
+ Effect.andThen(
502
+ Effect.suspend(() => apply({ _tag: "Revoke", number: command.live.number })),
503
+ ),
504
+ ),
505
+ command.live.value.scope,
506
+ );
247
507
  break;
508
+ }
509
+ case "Release":
248
510
  case "Leave":
249
- runRelease(command.live.number, Scope.close(command.live.value.scope, Exit.void));
511
+ runRelease(command.live.number, releaseGeneration(command, state, apply));
512
+ break;
250
513
  }
251
514
  });
252
515
 
253
- const openOnce = Effect.uninterruptibleMask((restore) =>
254
- Effect.flatMap(Scope.fork(own), (scope) =>
255
- restore(options.open.pipe(Scope.provide(scope), Effect.provideContext(context))).pipe(
256
- Effect.onError((cause) => Scope.close(scope, Exit.failCause(cause))),
257
- Effect.map((opened): Held => ({ opened, scope })),
258
- ),
259
- ),
260
- );
516
+ const openOnce = acquireGeneration(options.open, own).pipe(Effect.provideContext(context));
261
517
 
262
- // Retiring interrupts the open between tries or during one; the provider releases what an
263
- // interrupted try had made, as its scope closes.
264
518
  const openGeneration = (number: number, after: number | undefined) =>
265
- Effect.uninterruptibleMask((restore) => {
266
- const previous =
267
- after === undefined
519
+ Effect.uninterruptibleMask((restore) =>
520
+ restore(
521
+ (after === undefined
268
522
  ? Effect.void
269
- : FiberMap.get(releases, after).pipe(
270
- Effect.flatMap(Option.match({ onNone: () => Effect.void, onSome: Fiber.await })),
271
- );
272
-
273
- const tries = Effect.retry(openOnce, {
274
- schedule: reopen,
275
- while: (error) => !definite(error),
276
- });
277
-
278
- return restore(previous.pipe(Effect.andThen(tries))).pipe(
523
+ : changedUntil(state, (current) => !current.draining.has(after))
524
+ ).pipe(
525
+ Effect.andThen(
526
+ Effect.retry(openOnce, { schedule: reopen, while: (error) => !definite(error) }),
527
+ ),
528
+ ),
529
+ ).pipe(
279
530
  Effect.exit,
280
- Effect.flatMap((exit) => {
281
- if (Exit.isSuccess(exit))
282
- return apply({ _tag: "Opened", live: { number, value: exit.value } });
283
-
284
- return Cause.hasInterruptsOnly(exit.cause)
285
- ? apply({ _tag: "Abandoned", number })
286
- : apply({
287
- _tag: "Failed",
288
- number,
289
- detail: describe(exit.cause),
290
- cause: Cause.squash(exit.cause),
291
- });
292
- }),
293
- );
294
- });
295
-
296
- // A generation's own release loses its browser too, which the transition ignores.
297
- const watch = (live: Lifecycle.Live<Held>) => {
298
- const { browser } = live.value.opened;
299
-
300
- const loss = browser.disconnected.pipe(
301
- Effect.flatMap((cause) => apply({ _tag: "Lost", number: live.number, cause })),
302
- );
303
-
304
- const rotation =
305
- browser.expiresAt === undefined || rotateBefore === undefined
306
- ? Effect.void
307
- : untilRotation(browser.expiresAt, rotateBefore).pipe(
308
- Effect.andThen(apply({ _tag: "Rotate", due: live.number })),
309
- );
310
-
311
- return Effect.all([loss, rotation], { concurrency: 2, discard: true });
312
- };
313
-
314
- // The provider releases before the scope closes, so what the scope holds, such as a stored
315
- // context's lease, goes only once the release has reported.
316
- const release = (live: Lifecycle.Live<Held>) =>
317
- (live.value.opened.release ?? Effect.succeed(new Lifecycle.Settled())).pipe(
318
- Effect.catchCause((cause) =>
319
- Effect.succeed(
320
- new Lifecycle.Unconfirmed({ detail: `the release failed: ${describe(cause)}` }),
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
+ }),
321
542
  ),
322
543
  ),
323
- Effect.ensuring(Scope.close(live.value.scope, Exit.void)),
324
- Effect.flatMap((released) => apply({ _tag: "Released", number: live.number, released })),
325
544
  );
326
545
 
546
+ const use = leasing(state, apply, waitTimeout, now, clock);
547
+
327
548
  const end = yield* Effect.cached(PubSub.publish(events, Exit.void));
328
549
 
329
- // Stop at once, end or leave what is open, and wait for every release.
330
- const stop = (input: Lifecycle.Input<Held>) =>
550
+ const stop = (input: Lifecycle.Input<Held<A | void>, Hold>) =>
331
551
  apply(input).pipe(
332
552
  Effect.andThen(FiberHandle.clear(opening)),
333
553
  Effect.andThen(FiberMap.awaitEmpty(releases)),
334
- Effect.andThen(end),
554
+ Effect.andThen(
555
+ Effect.suspend(() =>
556
+ Deferred.isDone(releases.deferred).pipe(
557
+ Effect.flatMap((failed) => (failed ? FiberMap.join(releases) : Effect.void)),
558
+ ),
559
+ ),
560
+ ),
561
+ Effect.ensuring(end),
335
562
  Effect.asVoid,
336
563
  );
337
564
 
@@ -341,13 +568,32 @@ export const make = Effect.fn("Supervisor.make")(function* <E, R>(
341
568
  yield* perform(started);
342
569
 
343
570
  return {
344
- browser: waitFor(state, waitTimeout, undefined),
571
+ browser: Effect.map(waitFor(state, waitTimeout, clock), (live) => live.value.opened.browser),
572
+ use,
345
573
  states: Stream.fromPubSubTake(events),
346
574
  rotate: apply({ _tag: "Rotate" }).pipe(
347
- Effect.flatMap((target) =>
348
- target === undefined ? Effect.fail(retired()) : waitFor(state, waitTimeout, target),
575
+ Effect.flatMap(({ reply }) =>
576
+ reply === undefined
577
+ ? Effect.fail(retired())
578
+ : Effect.map(
579
+ waitFor(state, waitTimeout, clock, reply),
580
+ (live) => live.value.opened.browser,
581
+ ),
349
582
  ),
350
583
  ),
351
584
  retire,
352
- } satisfies Supervisor;
585
+ };
353
586
  });
587
+
588
+ /** Supervise scoped generation values. Browser-only providers retain their existing void value. */
589
+ export function make<E, R, A = void>(
590
+ options: Options<E, R, A>,
591
+ ): Effect.Effect<Supervisor<A>, never, Scope.Scope | Exclude<R, Scope.Scope>>;
592
+
593
+ export function make<E, R>(
594
+ options: MakeOptions<E, R, never>,
595
+ ): Effect.Effect<Supervisor<void>, never, Scope.Scope | Exclude<R, Scope.Scope>>;
596
+
597
+ export function make<E, R, A>(options: MakeOptions<E, R, A>) {
598
+ return makeScoped(options);
599
+ }