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/README.md +18 -9
- package/dist/{Supervisor-W60pZWHh.d.mts → Supervisor-5tZ47hR5.d.mts} +24 -10
- package/dist/{Supervisor-BE2sGHBK.mjs → Supervisor-DaGlQVjv.mjs} +84 -58
- package/dist/Supervisor-DaGlQVjv.mjs.map +1 -0
- package/dist/Supervisor.d.mts +2 -2
- package/dist/Supervisor.mjs +2 -2
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
- package/src/Supervisor.ts +167 -166
- package/src/internal/supervisor/lifecycle.ts +22 -12
- package/dist/Supervisor-BE2sGHBK.mjs.map +0 -1
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
|
-
*
|
|
100
|
-
*
|
|
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
|
|
190
|
-
|
|
191
|
-
) => Effect.Effect<
|
|
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(
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
-
|
|
430
|
+
const closed = yield* Effect.exit(Scope.close(live.value.scope, Exit.void));
|
|
436
431
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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
|
-
:
|
|
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:
|
|
491
|
-
|
|
492
|
-
|
|
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
|
-
|
|
499
|
-
|
|
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
|
-
|
|
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
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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
|
-
|
|
539
|
-
|
|
540
|
-
|
|
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
|
-
|
|
554
|
-
|
|
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.
|
|
558
|
-
()
|
|
559
|
-
|
|
560
|
-
|
|
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
|
-
|
|
567
|
-
|
|
568
|
-
|
|
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) =>
|
|
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:
|
|
583
|
-
apply(input)
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
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
|
-
/**
|
|
108
|
-
readonly
|
|
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
|
|
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()
|
|
161
|
+
state: { _tag: "Opening", number: 1, serving: undefined, draining: new Map() },
|
|
161
162
|
events: [[1, new Opening()]],
|
|
162
|
-
commands: [{ _tag: "Open", number: 1
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 = {
|
|
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
|
| {
|