effect-browser 0.3.0-beta.7 → 0.3.0-beta.9
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 +114 -19
- package/dist/{Browser-CNwK0r0q.mjs → Browser-Bc_LIqTC.mjs} +184 -16
- package/dist/Browser-Bc_LIqTC.mjs.map +1 -0
- package/dist/Browser.d.mts +16 -4
- package/dist/Browser.mjs +2 -2
- package/dist/{BrowserError-DXXIA6Rk.d.mts → BrowserError-Coy7EW1N.d.mts} +10 -5
- package/dist/BrowserError.d.mts +2 -2
- package/dist/BrowserError.mjs +10 -1
- package/dist/BrowserError.mjs.map +1 -1
- package/dist/Cdp.d.mts +1 -1
- package/dist/Cdp.mjs +3 -3
- package/dist/Chromium.d.mts +1 -1
- package/dist/Chromium.mjs +2 -2
- package/dist/Expect.d.mts +2 -2
- package/dist/Expect.mjs +2 -2
- package/dist/Moment.d.mts +1 -1
- package/dist/Moment.mjs +1 -1
- package/dist/{Page-D4281dIi.mjs → Page-B5cjaUp5.mjs} +2 -2
- package/dist/{Page-D4281dIi.mjs.map → Page-B5cjaUp5.mjs.map} +1 -1
- package/dist/{Page-C30aG2RQ.d.mts → Page-BlPjfibv.d.mts} +2 -2
- package/dist/Page.d.mts +1 -1
- package/dist/Page.mjs +1 -1
- package/dist/{Plan-BvCS9qAf.d.mts → Plan-BI8XGvDu.d.mts} +3 -3
- package/dist/{Plan-CyQNv49C.mjs → Plan-D1wrJVs8.mjs} +3 -3
- package/dist/{Plan-CyQNv49C.mjs.map → Plan-D1wrJVs8.mjs.map} +1 -1
- package/dist/Plan.d.mts +1 -1
- package/dist/Plan.mjs +1 -1
- package/dist/Policy.d.mts +1 -1
- package/dist/Policy.mjs +1 -1
- package/dist/Presentation.d.mts +1 -1
- package/dist/Presentation.mjs +1 -1
- package/dist/Stage.d.mts +2 -2
- package/dist/Stage.mjs +2 -2
- package/dist/{Supervisor-CLJaT7rG.d.mts → Supervisor-CYWFK3vM.d.mts} +105 -55
- package/dist/Supervisor-TNKNOVzu.mjs +599 -0
- package/dist/Supervisor-TNKNOVzu.mjs.map +1 -0
- package/dist/Supervisor.d.mts +2 -2
- package/dist/Supervisor.mjs +2 -2
- package/dist/{context-DAYjATgC.mjs → context-B6xr8nVi.mjs} +134 -10
- package/dist/context-B6xr8nVi.mjs.map +1 -0
- package/dist/index.d.mts +4 -4
- package/dist/index.mjs +4 -4
- package/dist/{page-DHeYWEs-.mjs → page-D-OedyBw.mjs} +59 -126
- package/dist/page-D-OedyBw.mjs.map +1 -0
- package/dist/{url-CcL0K7S0.mjs → url-Cd04Okid.mjs} +6 -4
- package/dist/url-Cd04Okid.mjs.map +1 -0
- package/package.json +1 -1
- package/src/Browser.ts +57 -16
- package/src/BrowserError.ts +9 -0
- package/src/Supervisor.ts +384 -138
- package/src/internal/input/gate.ts +224 -0
- package/src/internal/input/guard.ts +3 -2
- package/src/internal/input/perform.ts +84 -32
- package/src/internal/input/replies.ts +52 -16
- package/src/internal/page/context.ts +21 -11
- package/src/internal/page/lane.ts +20 -10
- package/src/internal/supervisor/lifecycle.ts +291 -140
- package/src/internal/timeline/events.ts +1 -0
- package/dist/Browser-CNwK0r0q.mjs.map +0 -1
- package/dist/Supervisor-BtEG93gR.mjs +0 -420
- package/dist/Supervisor-BtEG93gR.mjs.map +0 -1
- package/dist/context-DAYjATgC.mjs.map +0 -1
- package/dist/page-DHeYWEs-.mjs.map +0 -1
- package/dist/url-CcL0K7S0.mjs.map +0 -1
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
|
|
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
|
-
|
|
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
|
-
|
|
130
|
-
readonly
|
|
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
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
/**
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
)
|
|
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 =
|
|
211
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
226
|
-
|
|
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({
|
|
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(
|
|
487
|
+
Effect.provideContext(watchGeneration(command.live, rotateBefore, apply), context),
|
|
242
488
|
command.live.value.scope,
|
|
243
489
|
);
|
|
244
490
|
break;
|
|
245
|
-
case "
|
|
246
|
-
|
|
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,
|
|
511
|
+
runRelease(command.live.number, releaseGeneration(command, state, apply));
|
|
512
|
+
break;
|
|
250
513
|
}
|
|
251
514
|
});
|
|
252
515
|
|
|
253
|
-
const openOnce = Effect.
|
|
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
|
-
|
|
267
|
-
after === undefined
|
|
519
|
+
Effect.uninterruptibleMask((restore) =>
|
|
520
|
+
restore(
|
|
521
|
+
(after === undefined
|
|
268
522
|
? Effect.void
|
|
269
|
-
:
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
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(
|
|
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,
|
|
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((
|
|
348
|
-
|
|
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
|
-
}
|
|
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
|
+
}
|