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

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
@@ -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,23 @@ 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.
111
+ */
112
+ export class Releasing extends Context.Service<
113
+ Releasing,
114
+ { readonly released: (outcome: Lifecycle.Released) => Effect.Effect<void> }
115
+ >()("effect-browser/Supervisor/Releasing") {}
116
+
105
117
  export interface Options<E, R, A = void> {
106
118
  /** Opens one generation in the scope it is given. */
107
119
  readonly open: Effect.Effect<Opened<A>, E, R>;
@@ -186,9 +198,9 @@ interface Held<A> {
186
198
  }
187
199
 
188
200
  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>>;
201
+ type Input<A> = Lifecycle.Input<Held<A>, Hold>;
202
+ type Step<A> = Lifecycle.Step<Held<A>, Hold>;
203
+ type Apply<A> = (input: Input<A>) => Effect.Effect<Step<A>>;
192
204
 
193
205
  const superseded = (generation: number, { reason, revokeAt }: Lifecycle.Draining) =>
194
206
  new Superseded({ generation, reason, revokeAt });
@@ -201,15 +213,6 @@ const describe = (cause: Cause.Cause<unknown>) => {
201
213
  return Predicate.isError(error) ? error.message : String(error);
202
214
  };
203
215
 
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
216
  const timedOut = () =>
214
217
  new Unavailable({ reason: "opening", detail: "no generation opened within the wait" });
215
218
 
@@ -234,7 +237,7 @@ const waitFor = <A, H>(
234
237
  const result = (resolution: Lifecycle.Resolution<A>) =>
235
238
  resolution._tag === "Serve"
236
239
  ? Effect.succeed(resolution.live)
237
- : Effect.fail(unavailable(resolution));
240
+ : Effect.fail(new Unavailable(resolution));
238
241
 
239
242
  return Effect.suspend(() => {
240
243
  const current = Lifecycle.resolve(state.value, target);
@@ -277,20 +280,19 @@ const leasing = <A>(
277
280
  now: () => number,
278
281
  clock: Clock.Clock,
279
282
  ) => {
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
- });
283
+ const borrow = Effect.fnUntraced(function* (hold: Hold, wait: Duration.Duration) {
284
+ const until = now() + Duration.toMillis(wait);
285
+
286
+ for (;;) {
287
+ const step = yield* apply({ _tag: "Hold", hold });
288
+
289
+ if (step.borrowed !== undefined) return step.borrowed;
290
+ yield* waitFor(state, Duration.millis(Math.max(0, until - now())), clock).pipe(
291
+ Effect.interruptible,
292
+ );
293
+ if (now() >= until) return yield* timedOut();
294
+ }
295
+ });
294
296
 
295
297
  const leaseOf = (live: Lifecycle.Live<Held<A>>): Lease<A> => ({
296
298
  generation: live.number,
@@ -373,11 +375,6 @@ const acquireGeneration = <A, E, R>(
373
375
  ),
374
376
  );
375
377
 
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
378
  const watchGeneration = <A>(
382
379
  live: Lifecycle.Live<Held<A>>,
383
380
  rotateBefore: Duration.Duration | undefined,
@@ -400,59 +397,61 @@ const watchGeneration = <A>(
400
397
  };
401
398
 
402
399
  // After revocation, retained resources and use finalizers let go before the provider.
403
- const releaseGeneration = <A>(
400
+ const releaseGeneration = Effect.fnUntraced(function* <A>(
404
401
  command: Extract<Lifecycle.Command<Held<A>>, { readonly _tag: "Release" | "Leave" }>,
405
402
  state: SubscriptionRef.SubscriptionRef<State<A>>,
406
403
  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
- );
404
+ ) {
405
+ const { live } = command;
406
+
407
+ const cleaned = yield* Effect.forEach(
408
+ Lifecycle.lookup(state.value, live.number)?.holds ?? [],
409
+ (hold) =>
410
+ Effect.exit(hold._tag === "Retained" ? Scope.close(hold.scope, Exit.void) : Effect.void),
411
+ { concurrency: 8 },
412
+ );
418
413
 
419
- yield* changedUntil(
420
- state,
421
- (current) => (Lifecycle.lookup(current, live.number)?.holds.size ?? 0) === 0,
422
- );
414
+ yield* SubscriptionRef.changes(state).pipe(
415
+ Stream.filter((current) => (Lifecycle.lookup(current, live.number)?.holds.size ?? 0) === 0),
416
+ Stream.runHead,
417
+ );
423
418
 
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
- ),
419
+ const released =
420
+ command._tag === "Leave"
421
+ ? undefined
422
+ : yield* (live.value.opened.release ?? Effect.succeed(new Lifecycle.Settled())).pipe(
423
+ Effect.catchCause((cause) =>
424
+ Effect.succeed(
425
+ new Lifecycle.Unconfirmed({ detail: `the release failed: ${describe(cause)}` }),
432
426
  ),
433
- );
427
+ ),
428
+ );
434
429
 
435
- const closed = yield* Effect.exit(Scope.close(live.value.scope, Exit.void));
430
+ const closed = yield* Effect.exit(Scope.close(live.value.scope, Exit.void));
436
431
 
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
- });
432
+ yield* apply({ _tag: "Released", number: live.number, released });
433
+ if (released !== undefined) yield* Deferred.succeed(live.value.released, released);
434
+ yield* Exit.asVoidAll([...cleaned, closed]);
435
+ });
441
436
 
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)
437
+ const admissionAfter = <A>(state: State<A>) => {
438
+ const released = state.released;
439
+
440
+ const pending = [...state.draining.values()]
441
+ .filter((drain) => drain._tag !== "Leaving")
442
+ .map(({ live }) => live.value.released);
443
+
444
+ return pending.length === 0 && released === undefined
454
445
  ? undefined
455
- : Deferred.await(previous.value.released);
446
+ : Effect.gen(function* () {
447
+ let outcome = released;
448
+
449
+ for (const release of pending)
450
+ outcome = Lifecycle.combine(outcome, yield* Deferred.await(release));
451
+
452
+ return outcome ?? new Lifecycle.Settled();
453
+ });
454
+ };
456
455
 
457
456
  const makeScoped = Effect.fn("Supervisor.make")(function* <E, R, A>(
458
457
  options: MakeOptions<E, R, A>,
@@ -462,7 +461,6 @@ const makeScoped = Effect.fn("Supervisor.make")(function* <E, R, A>(
462
461
  const context = yield* Effect.context<Exclude<R, Scope.Scope>>();
463
462
  const own = yield* Scope.fork(yield* Effect.scope);
464
463
  const exclusive = options.exclusive ?? false;
465
- const definite = options.definite ?? (() => false);
466
464
 
467
465
  const reopen: Schedule.Schedule<unknown, E> =
468
466
  options.reopen ??
@@ -486,113 +484,116 @@ const makeScoped = Effect.fn("Supervisor.make")(function* <E, R, A>(
486
484
  const runOpen = yield* FiberHandle.runtime(opening)();
487
485
  const runRelease = yield* FiberMap.runtime(releases)();
488
486
 
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);
487
+ const apply: Apply<A | void> = Effect.fnUntraced(
488
+ function* (input: Input<A | void>) {
489
+ const current = state.value;
490
+ const step = Lifecycle.transition(current, input, exclusive, now(), handover);
497
491
 
498
- if (step.state !== current) yield* SubscriptionRef.set(state, step.state);
499
- yield* perform(step);
492
+ if (step.state !== current) yield* SubscriptionRef.set(state, step.state);
493
+ yield* perform(step);
494
+
495
+ return step;
496
+ },
497
+ (effect) => applying.withPermit(effect),
498
+ Effect.uninterruptible,
499
+ Effect.provideService(Clock.Clock, clock),
500
+ );
500
501
 
501
- return step;
502
+ const perform = Effect.fnUntraced(function* (step: Step<A | void>): Effect.fn.Return<void> {
503
+ for (const [number, generation] of step.events)
504
+ PubSub.publishUnsafe(events, [
505
+ new Generation({
506
+ number,
507
+ at: clock.currentTimeMillisUnsafe(),
508
+ hostTime: now(),
509
+ state: generation,
502
510
  }),
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
- ),
511
+ ]);
512
+ for (const command of step.commands)
513
+ switch (command._tag) {
514
+ case "Open":
515
+ runOpen(openGeneration(command.number));
516
+ break;
517
+ case "Watch":
518
+ yield* Effect.forkIn(
519
+ Effect.provideContext(watchGeneration(command.live, rotateBefore, apply), context),
520
+ command.live.value.scope,
521
+ );
522
+ break;
523
+ case "Drain": {
524
+ const ending = superseded(command.live.number, command.ending);
525
+
526
+ yield* Deferred.succeed(command.live.value.ending, ending);
527
+ yield* Effect.forkIn(
528
+ until(ending.revokeAt, now).pipe(
529
+ Effect.andThen(
530
+ Effect.suspend(() => apply({ _tag: "Revoke", number: command.live.number })),
537
531
  ),
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;
532
+ ),
533
+ command.live.value.scope,
534
+ );
535
+ break;
550
536
  }
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);
537
+ case "Release":
538
+ case "Leave":
539
+ yield* Deferred.succeed(
540
+ command.live.value.revoked,
541
+ superseded(command.live.number, command.ending),
542
+ );
543
+ runRelease(command.live.number, releaseGeneration(command, state, apply));
544
+ break;
545
+ }
546
+ });
555
547
 
548
+ const openGeneration = (number: number) => {
556
549
  const openOnce = acquireGeneration(
557
- Effect.suspend(
558
- () =>
559
- options.admit?.({ number, after: admissionAfter(previous, state.value) }) ?? Effect.void,
560
- ).pipe(Effect.andThen(options.open)),
550
+ Effect.gen(function* () {
551
+ const after = admissionAfter(state.value);
552
+
553
+ if (exclusive && after !== undefined) yield* after;
554
+ yield* options.admit?.({ number, after }) ?? Effect.void;
555
+
556
+ return yield* options.open.pipe(
557
+ Effect.provideService(Releasing, {
558
+ released: (released) =>
559
+ apply({ _tag: "AttemptReleased", released }).pipe(Effect.asVoid),
560
+ }),
561
+ );
562
+ }),
561
563
  own,
562
564
  ).pipe(Effect.provideContext(context));
563
565
 
564
566
  return Effect.uninterruptibleMask((restore) =>
565
567
  restore(
566
- (exclusive && after !== undefined ? after : Effect.void).pipe(
567
- Effect.andThen(
568
- Effect.retry(openOnce, { schedule: reopen, while: (error) => !definite(error) }),
569
- ),
570
- ),
568
+ Effect.retry(openOnce, {
569
+ schedule: reopen,
570
+ while: (error) => !options.definite?.(error),
571
+ }),
571
572
  ).pipe(
572
573
  Effect.exit,
573
- Effect.flatMap((exit) => apply(openedInput(number, exit))),
574
+ Effect.flatMap((exit) => {
575
+ if (Exit.isSuccess(exit))
576
+ return apply({ _tag: "Opened", live: { number, value: exit.value } });
577
+ const cause = exit.cause;
578
+
579
+ return apply(
580
+ Cause.hasInterruptsOnly(cause)
581
+ ? { _tag: "Abandoned", number }
582
+ : { _tag: "Failed", number, detail: describe(cause), cause: Cause.squash(cause) },
583
+ );
584
+ }),
574
585
  ),
575
586
  );
576
587
  };
577
588
 
578
- const use = leasing(state, apply, waitTimeout, now, clock);
579
-
580
589
  const end = yield* Effect.cached(PubSub.publish(events, Exit.void));
581
590
 
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
- );
591
+ const stop = Effect.fnUntraced(function* (input: Input<A | void>) {
592
+ yield* apply(input);
593
+ yield* FiberHandle.clear(opening);
594
+ yield* FiberMap.awaitEmpty(releases);
595
+ if (yield* Deferred.isDone(releases.deferred)) yield* FiberMap.join(releases);
596
+ }, Effect.ensuring(end));
596
597
 
597
598
  const retire = yield* Effect.cached(stop({ _tag: "Retire" }));
598
599
 
@@ -601,7 +602,7 @@ const makeScoped = Effect.fn("Supervisor.make")(function* <E, R, A>(
601
602
 
602
603
  return {
603
604
  browser: Effect.map(waitFor(state, waitTimeout, clock), (live) => live.value.opened.browser),
604
- use,
605
+ use: leasing(state, apply, waitTimeout, now, clock),
605
606
  states: Stream.fromPubSubTake(events),
606
607
  rotate: apply({ _tag: "Rotate" }).pipe(
607
608
  Effect.flatMap(({ reply }) =>
@@ -104,12 +104,13 @@ export type State<A, H = unknown> = (
104
104
  | { readonly _tag: "Retired" }
105
105
  ) & {
106
106
  readonly draining: ReadonlyMap<number, Drain<A, H>>;
107
- /** One bounded predecessor survives release and failed acquisitions for admission. */
108
- readonly latest: Live<A> | undefined;
107
+ /** Retain the first unconfirmed end, otherwise the latest settled end, without its resources. */
108
+ readonly released?: Released | undefined;
109
109
  };
110
110
 
111
111
  export type Input<A, H = unknown> =
112
112
  | { readonly _tag: "Opened"; readonly live: Live<A> }
113
+ | { readonly _tag: "AttemptReleased"; readonly released: Released }
113
114
  | {
114
115
  readonly _tag: "Failed";
115
116
  readonly number: number;
@@ -140,7 +141,7 @@ export type Input<A, H = unknown> =
140
141
  };
141
142
 
142
143
  export type Command<A> =
143
- | { readonly _tag: "Open"; readonly number: number; readonly after: Live<A> | undefined }
144
+ | { readonly _tag: "Open"; readonly number: number }
144
145
  | { readonly _tag: "Watch"; readonly live: Live<A> }
145
146
  /** Signal ending and arrange revocation. Repeated only when loss/retirement accelerates it. */
146
147
  | { readonly _tag: "Drain"; readonly live: Live<A>; readonly ending: Draining }
@@ -157,9 +158,9 @@ export interface Step<A, H = unknown> {
157
158
  }
158
159
 
159
160
  export const start = <A, H = unknown>(): Step<A, H> => ({
160
- state: { _tag: "Opening", number: 1, serving: undefined, draining: new Map(), latest: undefined },
161
+ state: { _tag: "Opening", number: 1, serving: undefined, draining: new Map() },
161
162
  events: [[1, new Opening()]],
162
- commands: [{ _tag: "Open", number: 1, after: undefined }],
163
+ commands: [{ _tag: "Open", number: 1 }],
163
164
  });
164
165
 
165
166
  const servingOf = <A, H>(state: State<A, H>) =>
@@ -270,8 +271,6 @@ const next = <A, H>(change: Change<A, H>, number: number, reopen: boolean) => {
270
271
  const { exclusive } = change;
271
272
  const current = servingOf(change.state);
272
273
 
273
- const after = change.state.latest;
274
-
275
274
  if (current !== undefined && (reopen || exclusive))
276
275
  end(change, current, reopen ? "lost" : "rotated");
277
276
  change.state = {
@@ -279,10 +278,10 @@ const next = <A, H>(change: Change<A, H>, number: number, reopen: boolean) => {
279
278
  number,
280
279
  serving: reopen || exclusive ? undefined : current,
281
280
  draining: change.state.draining,
282
- latest: after,
281
+ released: change.state.released,
283
282
  };
284
283
  change.events.push([number, reopen ? new Reopening() : new Opening()]);
285
- change.commands.push({ _tag: "Open", number, after });
284
+ change.commands.push({ _tag: "Open", number });
286
285
  };
287
286
 
288
287
  const lost = <A, H>(
@@ -346,7 +345,7 @@ export const transition = <A, H = unknown>(
346
345
  _tag: "Open",
347
346
  serving: live,
348
347
  draining: change.state.draining,
349
- latest: input.live,
348
+ released: change.state.released,
350
349
  };
351
350
  }
352
351
  break;
@@ -362,6 +361,9 @@ export const transition = <A, H = unknown>(
362
361
  case "Abandoned":
363
362
  change.events.push([input.number, new Closed({})]);
364
363
  break;
364
+ case "AttemptReleased":
365
+ change.state = { ...state, released: combine(state.released, input.released) };
366
+ break;
365
367
  case "Released": {
366
368
  const drain = state.draining.get(input.number);
367
369
 
@@ -369,7 +371,7 @@ export const transition = <A, H = unknown>(
369
371
  const draining = new Map(state.draining);
370
372
 
371
373
  draining.delete(input.number);
372
- change.state = { ...state, draining };
374
+ change.state = { ...state, draining, released: combine(state.released, input.released) };
373
375
  change.events.push([
374
376
  input.number,
375
377
  input.released === undefined ? new Kept() : new Closed({ released: input.released }),
@@ -397,12 +399,20 @@ export const transition = <A, H = unknown>(
397
399
  if (state._tag === "Retired") break;
398
400
  for (const number of state.draining.keys()) revoke(change, number, "retired");
399
401
  if (state.serving !== undefined) end(change, state.serving, "retired", input._tag === "Keep");
400
- change.state = { _tag: "Retired", draining: change.state.draining, latest: undefined };
402
+ change.state = {
403
+ _tag: "Retired",
404
+ draining: change.state.draining,
405
+ released: change.state.released,
406
+ };
401
407
  }
402
408
 
403
409
  return change;
404
410
  };
405
411
 
412
+ /** Later confirmations cannot establish that an earlier unconfirmed session ended. */
413
+ export const combine = (before: Released | undefined, next: Released | undefined) =>
414
+ before?._tag === "Unconfirmed" ? before : (next ?? before);
415
+
406
416
  export type Resolution<A> =
407
417
  | { readonly _tag: "Serve"; readonly live: Live<A> }
408
418
  | {