reactor-effect-client 0.5.0 → 0.6.0
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 +16 -2
- package/dist/_internal/queue.d.ts +6 -0
- package/dist/_internal/queue.d.ts.map +1 -0
- package/dist/_internal/queue.js +30 -0
- package/dist/_internal/queue.js.map +1 -0
- package/dist/host.d.ts +1 -0
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +1 -0
- package/dist/host.js.map +1 -1
- package/dist/observation.d.ts +3 -0
- package/dist/observation.d.ts.map +1 -1
- package/dist/observation.js +9 -4
- package/dist/observation.js.map +1 -1
- package/dist/orchestration/index.d.ts +4 -4
- package/dist/orchestration/index.d.ts.map +1 -1
- package/dist/orchestration/index.js +2 -2
- package/dist/orchestration/index.js.map +1 -1
- package/dist/orchestration/media-buffer.d.ts +2 -1
- package/dist/orchestration/media-buffer.d.ts.map +1 -1
- package/dist/orchestration/media-buffer.js +5 -4
- package/dist/orchestration/media-buffer.js.map +1 -1
- package/dist/orchestration/renewal-state.d.ts +14 -2
- package/dist/orchestration/renewal-state.d.ts.map +1 -1
- package/dist/orchestration/renewal-state.js +34 -4
- package/dist/orchestration/renewal-state.js.map +1 -1
- package/dist/orchestration/renewal.d.ts +27 -3
- package/dist/orchestration/renewal.d.ts.map +1 -1
- package/dist/orchestration/renewal.js +396 -95
- package/dist/orchestration/renewal.js.map +1 -1
- package/dist/orchestration/retention.d.ts +100 -0
- package/dist/orchestration/retention.d.ts.map +1 -0
- package/dist/orchestration/retention.js +134 -0
- package/dist/orchestration/retention.js.map +1 -0
- package/dist/orchestration/scheduler-policy.d.ts +2 -2
- package/dist/orchestration/scheduler-policy.d.ts.map +1 -1
- package/dist/orchestration/scheduler-policy.js +5 -8
- package/dist/orchestration/scheduler-policy.js.map +1 -1
- package/dist/orchestration/scheduler.d.ts +5 -3
- package/dist/orchestration/scheduler.d.ts.map +1 -1
- package/dist/orchestration/scheduler.js +359 -109
- package/dist/orchestration/scheduler.js.map +1 -1
- package/dist/orchestration/source-slot.d.ts +16 -4
- package/dist/orchestration/source-slot.d.ts.map +1 -1
- package/dist/orchestration/source-slot.js +122 -30
- package/dist/orchestration/source-slot.js.map +1 -1
- package/dist/orchestration/types.d.ts +483 -1
- package/dist/orchestration/types.d.ts.map +1 -1
- package/dist/orchestration/types.js +105 -1
- package/dist/orchestration/types.js.map +1 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +2 -1
- package/dist/session.js.map +1 -1
- package/dist/simulation/_internal/source.d.ts.map +1 -1
- package/dist/simulation/_internal/source.js +5 -4
- package/dist/simulation/_internal/source.js.map +1 -1
- package/package.json +2 -2
|
@@ -10,8 +10,10 @@ import * as PubSub from "effect/PubSub";
|
|
|
10
10
|
import * as Queue from "effect/Queue";
|
|
11
11
|
import * as Result from "effect/Result";
|
|
12
12
|
import * as Schema from "effect/Schema";
|
|
13
|
+
import * as Scope from "effect/Scope";
|
|
13
14
|
import * as Stream from "effect/Stream";
|
|
14
15
|
import * as SubscriptionRef from "effect/SubscriptionRef";
|
|
16
|
+
import { take as takeQueue } from "../_internal/queue.js";
|
|
15
17
|
import { duration } from "../duration.js";
|
|
16
18
|
import { parsedInput, ReactorError } from "../errors.js";
|
|
17
19
|
import { monotonicMillis } from "./elapsed.js";
|
|
@@ -51,9 +53,8 @@ const ownedData = (input, fields, name) => {
|
|
|
51
53
|
return Result.fail(invalid(`${name} has unsupported fields or accessors`));
|
|
52
54
|
return Result.succeed(Object.fromEntries(Object.entries(descriptors).map(([field, descriptor]) => [field, descriptor.value])));
|
|
53
55
|
};
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return input;
|
|
56
|
+
/** A Duration tuple or object read from own data properties; no accessor runs. */
|
|
57
|
+
const durationData = (input) => {
|
|
57
58
|
if (Array.isArray(input)) {
|
|
58
59
|
const descriptors = Object.getOwnPropertyDescriptors(input);
|
|
59
60
|
if (Object.getOwnPropertySymbols(input).length > 0 ||
|
|
@@ -61,20 +62,44 @@ const captureDuration = (input) => Effect.gen(function* () {
|
|
|
61
62
|
descriptors.length?.value !== 2 ||
|
|
62
63
|
descriptors["0"] === undefined ||
|
|
63
64
|
descriptors["1"] === undefined)
|
|
64
|
-
return
|
|
65
|
-
|
|
65
|
+
return Result.fail(invalid("Duration tuple has unsupported fields or accessors"));
|
|
66
|
+
const seconds = descriptors["0"].value;
|
|
67
|
+
const nanos = descriptors["1"].value;
|
|
68
|
+
return Result.succeed([seconds, nanos]);
|
|
66
69
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
"microseconds",
|
|
75
|
-
"nanoseconds",
|
|
76
|
-
], "Duration")));
|
|
70
|
+
const fields = ownedData(input, ["weeks", "days", "hours", "minutes", "seconds", "milliseconds", "microseconds", "nanoseconds"], "Duration");
|
|
71
|
+
return Result.isFailure(fields) ? Result.fail(fields.failure) : Result.succeed(fields.success);
|
|
72
|
+
};
|
|
73
|
+
const captureDuration = (input) => Effect.gen(function* () {
|
|
74
|
+
if (input === null || typeof input !== "object" || Duration.isDuration(input))
|
|
75
|
+
return input;
|
|
76
|
+
return yield* Effect.fromResult(durationData(input));
|
|
77
77
|
});
|
|
78
|
+
/**
|
|
79
|
+
* A Duration option read as caller data: a data property, own or inherited,
|
|
80
|
+
* captured without running any accessor. Rejections throw for `parsedInput`.
|
|
81
|
+
*/
|
|
82
|
+
const optionDuration = (options, field, name) => {
|
|
83
|
+
let descriptor;
|
|
84
|
+
for (let owner = options; descriptor === undefined && owner !== null; owner = Object.getPrototypeOf(owner))
|
|
85
|
+
descriptor = Object.getOwnPropertyDescriptor(owner, field);
|
|
86
|
+
if (descriptor === undefined)
|
|
87
|
+
return undefined;
|
|
88
|
+
const reject = () => ReactorError.fromCode("InvalidInput", `${name} must be a Duration of data properties`);
|
|
89
|
+
if (!("value" in descriptor))
|
|
90
|
+
throw reject();
|
|
91
|
+
const value = descriptor.value;
|
|
92
|
+
if (value === undefined)
|
|
93
|
+
return undefined;
|
|
94
|
+
if (value === null)
|
|
95
|
+
throw reject();
|
|
96
|
+
if (typeof value !== "object" || Duration.isDuration(value))
|
|
97
|
+
return value;
|
|
98
|
+
const captured = durationData(value);
|
|
99
|
+
if (Result.isFailure(captured))
|
|
100
|
+
throw reject();
|
|
101
|
+
return captured.success;
|
|
102
|
+
};
|
|
78
103
|
const requestDuration = (input, name, allowZero = false) => Effect.gen(function* () {
|
|
79
104
|
const captured = yield* captureDuration(input);
|
|
80
105
|
return yield* parsedInput(() => Duration.toMillis(duration(captured, name, { allowZero, allowNegative: name === "startBy" })), "submit").pipe(Effect.mapError((error) => invalid(error.message)));
|
|
@@ -178,11 +203,13 @@ export const makeScheduler = (options) => Effect.gen(function* () {
|
|
|
178
203
|
throw ReactorError.fromCode("InvalidInput", "Scheduler lanes must be unique and nonempty");
|
|
179
204
|
return options.lanes.map((lane) => lane.name);
|
|
180
205
|
}, "makeScheduler");
|
|
181
|
-
const { floorSeconds, targetSeconds, maxBuildsInFlight, maxHistory } = yield* parsedInput(() => {
|
|
206
|
+
const { floorSeconds, targetSeconds, maxBuildsInFlight, maxHistory, unknownRecoveryMs } = yield* parsedInput(() => {
|
|
182
207
|
const floorSeconds = Duration.toMillis(duration(options.filler.runway.floor, "runway floor", { allowZero: true })) / 1000;
|
|
183
208
|
const targetSeconds = Duration.toMillis(duration(options.filler.runway.target, "runway target")) / 1000;
|
|
184
209
|
const maxBuildsInFlight = options.maxBuildsInFlight ?? 1;
|
|
185
210
|
const maxHistory = options.maxHistory ?? 4096;
|
|
211
|
+
const unknownRecoveryMs = Duration.toMillis(duration(optionDuration(options, "unknownRecoveryTimeout", "Unknown recovery timeout") ??
|
|
212
|
+
"60 seconds", "unknown recovery timeout", { maximum: "10 minutes" }));
|
|
186
213
|
if (floorSeconds > targetSeconds ||
|
|
187
214
|
!Number.isSafeInteger(maxBuildsInFlight) ||
|
|
188
215
|
maxBuildsInFlight < 1 ||
|
|
@@ -191,7 +218,7 @@ export const makeScheduler = (options) => Effect.gen(function* () {
|
|
|
191
218
|
maxHistory < 0 ||
|
|
192
219
|
maxHistory > 65_536)
|
|
193
220
|
throw ReactorError.fromCode("InvalidInput", "Scheduler runway or build cap is invalid");
|
|
194
|
-
return { floorSeconds, targetSeconds, maxBuildsInFlight, maxHistory };
|
|
221
|
+
return { floorSeconds, targetSeconds, maxBuildsInFlight, maxHistory, unknownRecoveryMs };
|
|
195
222
|
}, "makeScheduler");
|
|
196
223
|
const captureFiller = (index, runway, targetSeconds) => Effect.gen(function* () {
|
|
197
224
|
const request = yield* captureRequest(options.filler.clip({
|
|
@@ -209,7 +236,8 @@ export const makeScheduler = (options) => Effect.gen(function* () {
|
|
|
209
236
|
});
|
|
210
237
|
let fillerIndex = 0;
|
|
211
238
|
let fillerRetryAtMs = 0;
|
|
212
|
-
|
|
239
|
+
const unknownFillers = new Map();
|
|
240
|
+
const maxUnknownFillers = 4096;
|
|
213
241
|
let upcomingFiller = yield* captureFiller(0, 0);
|
|
214
242
|
const inbox = yield* Queue.unbounded();
|
|
215
243
|
const commandQueue = yield* Queue.unbounded();
|
|
@@ -229,6 +257,7 @@ export const makeScheduler = (options) => Effect.gen(function* () {
|
|
|
229
257
|
});
|
|
230
258
|
const initialized = yield* Deferred.make();
|
|
231
259
|
const stopped = yield* Deferred.make();
|
|
260
|
+
const settled = yield* Deferred.make();
|
|
232
261
|
const closedCall = Deferred.await(stopped).pipe(Effect.flatMap((cause) => PolicyFailure.refuse("SessionClosed", cause.message)));
|
|
233
262
|
const items = new Map();
|
|
234
263
|
const history = new Map();
|
|
@@ -248,6 +277,9 @@ export const makeScheduler = (options) => Effect.gen(function* () {
|
|
|
248
277
|
const drainReplies = [];
|
|
249
278
|
const pendingWithdrawals = new Map();
|
|
250
279
|
const pendingBuilds = new Set();
|
|
280
|
+
// A Build whose enqueue has started and whose result the worker has not
|
|
281
|
+
// yet seen. The provider may still accept it after the scheduler ends.
|
|
282
|
+
let dispatching;
|
|
251
283
|
const pendingAtDeferrals = new Set();
|
|
252
284
|
const pendingItemRemovals = new Set();
|
|
253
285
|
const pendingFillerRemovals = new Set();
|
|
@@ -262,7 +294,9 @@ export const makeScheduler = (options) => Effect.gen(function* () {
|
|
|
262
294
|
let drained = false;
|
|
263
295
|
let observationReady = false;
|
|
264
296
|
let blockedMove;
|
|
265
|
-
const emit = (entry, status) => Effect.gen(function* () {
|
|
297
|
+
const emit = (entry, status, terminal = false) => Effect.gen(function* () {
|
|
298
|
+
if (ended !== undefined && !terminal)
|
|
299
|
+
return;
|
|
266
300
|
if (JSON.stringify(entry.status) === JSON.stringify(status))
|
|
267
301
|
return;
|
|
268
302
|
entry.status = status;
|
|
@@ -275,7 +309,10 @@ export const makeScheduler = (options) => Effect.gen(function* () {
|
|
|
275
309
|
: status._tag === "Unknown" && status.terminal !== true
|
|
276
310
|
? "Unknown"
|
|
277
311
|
: "Terminal";
|
|
278
|
-
|
|
312
|
+
// Publish in the step that records it (the PubSub is unbounded): a claim
|
|
313
|
+
// from another fiber cannot land between the two, so asRun never carries
|
|
314
|
+
// a settlement ahead of a recorded status, nor omits one.
|
|
315
|
+
PubSub.publishUnsafe(events, {
|
|
279
316
|
key: entry.key,
|
|
280
317
|
at: clock.currentTimeMillisUnsafe(),
|
|
281
318
|
status,
|
|
@@ -293,31 +330,165 @@ export const makeScheduler = (options) => Effect.gen(function* () {
|
|
|
293
330
|
yield* Deferred.succeed(entry.outcomeWaiter, status);
|
|
294
331
|
}
|
|
295
332
|
});
|
|
296
|
-
|
|
333
|
+
// The claim is synchronous; interruption masking alone would still allow
|
|
334
|
+
// two closers to publish conflicting results. Join only local bookkeeping,
|
|
335
|
+
// never the worker scope (a worker can itself own terminal settlement).
|
|
336
|
+
const terminate = (terminal) => Effect.suspend(() => {
|
|
297
337
|
if (ended !== undefined)
|
|
298
|
-
return;
|
|
299
|
-
ended =
|
|
338
|
+
return Deferred.await(settled);
|
|
339
|
+
ended = terminal;
|
|
300
340
|
accepting = false;
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
341
|
+
// Wake the watchdog so an episode left armed by another owner cannot
|
|
342
|
+
// keep re-arming its timer.
|
|
343
|
+
Deferred.doneUnsafe(recoveryChanged, Exit.void);
|
|
344
|
+
return Effect.gen(function* () {
|
|
345
|
+
for (const item of items.values()) {
|
|
346
|
+
if (Exit.isFailure(terminal)) {
|
|
347
|
+
yield* Deferred.failCause(item.startedWaiter, terminal.cause);
|
|
348
|
+
yield* Deferred.failCause(item.outcomeWaiter, terminal.cause);
|
|
349
|
+
yield* Deferred.failCause(item.firstDecisiveWaiter, terminal.cause);
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
// emit records and publishes a status before completing its waits,
|
|
353
|
+
// and a claim can land in between: a completion can resume an
|
|
354
|
+
// observer that closes the owner, and another fiber can claim while
|
|
355
|
+
// the actor is preempted. Finish those waits from the record.
|
|
356
|
+
const recorded = item.status;
|
|
357
|
+
if (item.phase === "Started" || item.phase === "Terminal") {
|
|
358
|
+
if (isFirstDecisive(recorded))
|
|
359
|
+
yield* Deferred.succeed(item.firstDecisiveWaiter, recorded);
|
|
360
|
+
yield* Deferred.succeed(item.startedWaiter, recorded);
|
|
361
|
+
}
|
|
362
|
+
if (item.phase === "Terminal")
|
|
363
|
+
yield* Deferred.succeed(item.outcomeWaiter, recorded);
|
|
364
|
+
else
|
|
365
|
+
yield* emit(item, item.phase === "Unknown" ||
|
|
366
|
+
(item.clipId === undefined &&
|
|
367
|
+
(item.unknownAtMs !== undefined || item.key === dispatching))
|
|
368
|
+
? { _tag: "Unknown", terminal: true }
|
|
369
|
+
: { _tag: "Failed", reason: { _tag: "Scheduler", cause: terminal.value } }, true);
|
|
308
370
|
}
|
|
309
|
-
|
|
310
|
-
|
|
371
|
+
const closed = Exit.isSuccess(terminal)
|
|
372
|
+
? Exit.fail(PolicyFailure.refuse("SessionClosed", terminal.value.message))
|
|
373
|
+
: Exit.failCause(terminal.cause);
|
|
374
|
+
for (const [key, pending] of pendingWithdrawals) {
|
|
375
|
+
// A drop recorded before the claim is the withdrawal's actual result.
|
|
376
|
+
const reply = Exit.isSuccess(terminal) && items.get(key)?.status._tag === "Dropped"
|
|
377
|
+
? Exit.succeed("withdrawn")
|
|
378
|
+
: closed;
|
|
379
|
+
for (const waiter of pending.replies)
|
|
380
|
+
yield* Deferred.done(waiter, reply);
|
|
381
|
+
}
|
|
382
|
+
pendingWithdrawals.clear();
|
|
383
|
+
for (const reply of drainReplies.splice(0))
|
|
384
|
+
yield* Deferred.done(reply, closed);
|
|
385
|
+
yield* SubscriptionRef.update(stateRef, (state) => ({ ...state, accepting: false }));
|
|
386
|
+
yield* Deferred.done(initialized, Exit.isFailure(terminal)
|
|
387
|
+
? Exit.failCause(terminal.cause)
|
|
388
|
+
: Exit.fail(ReactorError.fromCode("Closed", terminal.value.message)));
|
|
389
|
+
// Supervisors may close the owner immediately when this wakes them.
|
|
390
|
+
yield* Deferred.done(stopped, terminal);
|
|
391
|
+
}).pipe(Effect.onExit((exit) => Effect.gen(function* () {
|
|
392
|
+
// A bookkeeping defect must release joiners with that Cause, never
|
|
393
|
+
// leave an unfinished latch or report a partial settlement as success.
|
|
394
|
+
yield* Deferred.done(settled, exit);
|
|
395
|
+
if (Exit.isFailure(exit)) {
|
|
396
|
+
yield* Deferred.failCause(initialized, exit.cause);
|
|
397
|
+
yield* Deferred.failCause(stopped, exit.cause);
|
|
398
|
+
}
|
|
399
|
+
})));
|
|
400
|
+
}).pipe(Effect.uninterruptible);
|
|
401
|
+
const closeActor = (cause) => terminate(Exit.succeed(cause));
|
|
402
|
+
let recoveryState;
|
|
403
|
+
let capacityRecovery;
|
|
404
|
+
let recoveryDeadline;
|
|
405
|
+
let recoveryChanged = Deferred.makeUnsafe();
|
|
406
|
+
// Retain the original age while Closing hides the old source and acquisition
|
|
407
|
+
// is still pending. Only accepted evidence or a Ready replacement clears it.
|
|
408
|
+
const updateRecovery = (state = recoveryState, fresh) => {
|
|
409
|
+
if (ended !== undefined)
|
|
410
|
+
return;
|
|
411
|
+
recoveryState = state;
|
|
412
|
+
const preferred = state === undefined ? undefined : Option.getOrUndefined(state.preferredSessionId);
|
|
413
|
+
const ready = preferred !== undefined &&
|
|
414
|
+
state?.sessions.some((source) => source.sessionId === preferred && source.availability === "Ready") === true;
|
|
415
|
+
let earliest;
|
|
416
|
+
let drainDeadline;
|
|
417
|
+
const consider = (atMs, sessionId, cause, newlyObserved = false) => {
|
|
418
|
+
const deadlineMs = atMs + unknownRecoveryMs;
|
|
419
|
+
if (((!ready && newlyObserved) || sessionId === undefined || sessionId === preferred) &&
|
|
420
|
+
(earliest === undefined || deadlineMs < earliest.deadlineMs))
|
|
421
|
+
earliest = { deadlineMs, sessionId, cause };
|
|
422
|
+
if (draining && (drainDeadline === undefined || deadlineMs < drainDeadline.deadlineMs))
|
|
423
|
+
drainDeadline = { deadlineMs, sessionId, cause };
|
|
424
|
+
};
|
|
425
|
+
for (const entry of unknownFillers.values())
|
|
426
|
+
consider(entry.atMs, entry.sessionId, entry.cause);
|
|
427
|
+
for (const item of items.values())
|
|
428
|
+
if (item.phase !== "Terminal" &&
|
|
429
|
+
item.clipId === undefined &&
|
|
430
|
+
item.unknownAtMs !== undefined &&
|
|
431
|
+
item.unknownCause !== undefined)
|
|
432
|
+
consider(item.unknownAtMs, item.unknownSessionId, item.unknownCause);
|
|
433
|
+
if (fresh !== undefined)
|
|
434
|
+
consider(fresh.atMs, fresh.sessionId, fresh.cause, true);
|
|
435
|
+
if (ready &&
|
|
436
|
+
(earliest === undefined ||
|
|
437
|
+
(capacityRecovery?.sessionId !== undefined && capacityRecovery.sessionId !== preferred)))
|
|
438
|
+
capacityRecovery = undefined;
|
|
439
|
+
if (earliest !== undefined &&
|
|
440
|
+
(capacityRecovery === undefined || earliest.deadlineMs < capacityRecovery.deadlineMs))
|
|
441
|
+
capacityRecovery = earliest;
|
|
442
|
+
const next = drainDeadline !== undefined &&
|
|
443
|
+
(capacityRecovery === undefined || drainDeadline.deadlineMs < capacityRecovery.deadlineMs)
|
|
444
|
+
? drainDeadline
|
|
445
|
+
: capacityRecovery;
|
|
446
|
+
if (next?.deadlineMs !== recoveryDeadline?.deadlineMs ||
|
|
447
|
+
next?.cause !== recoveryDeadline?.cause) {
|
|
448
|
+
recoveryDeadline = next;
|
|
449
|
+
Deferred.doneUnsafe(recoveryChanged, Exit.void);
|
|
311
450
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
451
|
+
};
|
|
452
|
+
const provedUnknown = (atMs) => {
|
|
453
|
+
if (atMs !== undefined && capacityRecovery?.deadlineMs === atMs + unknownRecoveryMs)
|
|
454
|
+
capacityRecovery = undefined;
|
|
455
|
+
};
|
|
456
|
+
const watchdog = Effect.gen(function* () {
|
|
457
|
+
for (;;) {
|
|
458
|
+
// A committed result and its queued actor evidence can arrive in one
|
|
459
|
+
// turn. Coalesce those revisions before allocating a replacement timer;
|
|
460
|
+
// the original monotonic deadline remains unchanged.
|
|
461
|
+
yield* Effect.yieldNow;
|
|
462
|
+
if (ended !== undefined)
|
|
463
|
+
return;
|
|
464
|
+
recoveryChanged = Deferred.makeUnsafe();
|
|
465
|
+
const episode = recoveryDeadline;
|
|
466
|
+
if (episode === undefined) {
|
|
467
|
+
yield* Deferred.await(recoveryChanged);
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
yield* Deferred.await(recoveryChanged).pipe(Effect.timeoutOrElse({
|
|
471
|
+
duration: Math.max(0, episode.deadlineMs - monotonicMillis(clock)),
|
|
472
|
+
orElse: () => Effect.void,
|
|
473
|
+
}));
|
|
474
|
+
// A canceled timer may already have resumed. Recheck the retained
|
|
475
|
+
// identity without awaiting the actor, Engine state, or its permits.
|
|
476
|
+
if (ended === undefined &&
|
|
477
|
+
recoveryDeadline === episode &&
|
|
478
|
+
monotonicMillis(clock) >= episode.deadlineMs) {
|
|
479
|
+
const causes = new Set([episode.cause]);
|
|
480
|
+
for (const item of items.values())
|
|
481
|
+
if (item.unknownCause !== undefined)
|
|
482
|
+
causes.add(item.unknownCause);
|
|
483
|
+
for (const entry of unknownFillers.values())
|
|
484
|
+
causes.add(entry.cause);
|
|
485
|
+
yield* closeActor(ReactorError.fromCode("Timeout", "Scheduler unknown recovery deadline elapsed", {
|
|
486
|
+
operation: "scheduler.unknownRecovery",
|
|
487
|
+
detail: [...causes],
|
|
488
|
+
}));
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
317
491
|
}
|
|
318
|
-
for (const reply of drainReplies.splice(0))
|
|
319
|
-
yield* Deferred.fail(reply, closed);
|
|
320
|
-
yield* SubscriptionRef.update(stateRef, (state) => ({ ...state, accepting: false }));
|
|
321
492
|
});
|
|
322
493
|
const prune = (state) => {
|
|
323
494
|
const active = new Set(activeIds(state));
|
|
@@ -350,7 +521,44 @@ export const makeScheduler = (options) => Effect.gen(function* () {
|
|
|
350
521
|
while (history.size > maxHistory)
|
|
351
522
|
history.delete(history.keys().next().value);
|
|
352
523
|
};
|
|
353
|
-
|
|
524
|
+
// Capture membership and public rows in one pass. A fresh token reads every
|
|
525
|
+
// source on every observation, including in-place changes to Engine arrays;
|
|
526
|
+
// frozen empty rows can be shared by every published state without treating
|
|
527
|
+
// array identity as proof.
|
|
528
|
+
const noReady = Object.freeze([]);
|
|
529
|
+
const sourceRows = new Map();
|
|
530
|
+
const projectSources = (state) => {
|
|
531
|
+
const seen = Symbol();
|
|
532
|
+
const sessions = state.sessions.map(({ sessionId }) => {
|
|
533
|
+
let cached = sourceRows.get(sessionId);
|
|
534
|
+
if (cached === undefined) {
|
|
535
|
+
cached = { empty: Object.freeze({ sessionId, ready: noReady }), seen };
|
|
536
|
+
sourceRows.set(sessionId, cached);
|
|
537
|
+
}
|
|
538
|
+
else
|
|
539
|
+
cached.seen = seen;
|
|
540
|
+
if (state.ready.length === 0)
|
|
541
|
+
return cached.empty;
|
|
542
|
+
return {
|
|
543
|
+
sessionId,
|
|
544
|
+
ready: state.ready
|
|
545
|
+
.filter((clip) => clip.sessionId === sessionId)
|
|
546
|
+
.map((clip) => {
|
|
547
|
+
const owner = owned.get(clip.clipId);
|
|
548
|
+
return owner === undefined ? "other" : owner._tag === "Filler" ? "filler" : owner.key;
|
|
549
|
+
}),
|
|
550
|
+
};
|
|
551
|
+
});
|
|
552
|
+
// Bound cached rows by snapshot size without scanning unchanged overlap.
|
|
553
|
+
// Membership still requires this observation's token, even before pruning.
|
|
554
|
+
if (sourceRows.size > state.sessions.length)
|
|
555
|
+
for (const [id, cached] of sourceRows)
|
|
556
|
+
if (cached.seen !== seen)
|
|
557
|
+
sourceRows.delete(id);
|
|
558
|
+
// Only the serialized actor uses this membership view, before its next projection.
|
|
559
|
+
return { sessions, has: (id) => sourceRows.get(id)?.seen === seen };
|
|
560
|
+
};
|
|
561
|
+
const publishState = (state, sessions = projectSources(state).sessions) => {
|
|
354
562
|
const playing = Option.getOrUndefined(state.playing);
|
|
355
563
|
const playingOwner = playing === undefined ? undefined : owned.get(playing.clipId);
|
|
356
564
|
const playingKey = playing === undefined
|
|
@@ -372,15 +580,7 @@ export const makeScheduler = (options) => Effect.gen(function* () {
|
|
|
372
580
|
.filter((item) => item.lane === name && item.phase !== "Terminal")
|
|
373
581
|
.map((item) => item.key),
|
|
374
582
|
})),
|
|
375
|
-
sessions
|
|
376
|
-
sessionId,
|
|
377
|
-
ready: state.ready
|
|
378
|
-
.filter((clip) => clip.sessionId === sessionId)
|
|
379
|
-
.map((clip) => {
|
|
380
|
-
const owner = owned.get(clip.clipId);
|
|
381
|
-
return owner === undefined ? "other" : owner._tag === "Filler" ? "filler" : owner.key;
|
|
382
|
-
}),
|
|
383
|
-
})),
|
|
583
|
+
sessions,
|
|
384
584
|
starved,
|
|
385
585
|
});
|
|
386
586
|
};
|
|
@@ -454,8 +654,8 @@ export const makeScheduler = (options) => Effect.gen(function* () {
|
|
|
454
654
|
filler.set(index, { index, clipId: record.clipId, sessionId: record.sessionId });
|
|
455
655
|
const priorIndex = fillerIndex;
|
|
456
656
|
fillerIndex = Math.max(fillerIndex, index + 1);
|
|
457
|
-
|
|
458
|
-
|
|
657
|
+
provedUnknown(unknownFillers.get(index)?.atMs);
|
|
658
|
+
unknownFillers.delete(index);
|
|
459
659
|
if (fillerIndex !== priorIndex)
|
|
460
660
|
upcomingFiller = undefined;
|
|
461
661
|
}
|
|
@@ -467,6 +667,9 @@ export const makeScheduler = (options) => Effect.gen(function* () {
|
|
|
467
667
|
if (resumed !== undefined) {
|
|
468
668
|
item.clipId = resumed.clipId;
|
|
469
669
|
item.sessionId = resumed.sessionId;
|
|
670
|
+
provedUnknown(item.unknownAtMs);
|
|
671
|
+
item.unknownAtMs = undefined;
|
|
672
|
+
item.unknownCause = undefined;
|
|
470
673
|
item.unknownSessionId = undefined;
|
|
471
674
|
owned.set(resumed.clipId, { _tag: "Item", key: item.key });
|
|
472
675
|
}
|
|
@@ -530,7 +733,7 @@ export const makeScheduler = (options) => Effect.gen(function* () {
|
|
|
530
733
|
lastProcessedSnapshotSerial > item.acknowledgedAtSnapshotSerial))
|
|
531
734
|
yield* emit(item, { _tag: "Unobserved" });
|
|
532
735
|
}
|
|
533
|
-
|
|
736
|
+
updateRecovery(state);
|
|
534
737
|
});
|
|
535
738
|
const onEvent = (event) => Effect.gen(function* () {
|
|
536
739
|
if (event._tag === "Starved")
|
|
@@ -662,6 +865,12 @@ export const makeScheduler = (options) => Effect.gen(function* () {
|
|
|
662
865
|
if (command.sessionId === undefined ||
|
|
663
866
|
Option.getOrUndefined((yield* engine.state).preferredSessionId) !== command.sessionId)
|
|
664
867
|
return yield* PolicyFailure.refuse("RouteChanged", "The preferred source changed before dispatch");
|
|
868
|
+
// Termination can be claimed while the route is read; its settlement
|
|
869
|
+
// must not be followed by a new dispatch.
|
|
870
|
+
if (ended !== undefined)
|
|
871
|
+
return yield* PolicyFailure.refuse("SessionClosed", "The scheduler closed");
|
|
872
|
+
if (command._tag === "Build")
|
|
873
|
+
dispatching = command.key;
|
|
665
874
|
return yield* engine.enqueueOnSource === undefined
|
|
666
875
|
? engine.enqueue(command.request)
|
|
667
876
|
: engine.enqueueOnSource(command.request, command.sessionId);
|
|
@@ -679,12 +888,44 @@ export const makeScheduler = (options) => Effect.gen(function* () {
|
|
|
679
888
|
};
|
|
680
889
|
const commandWorker = Effect.gen(function* () {
|
|
681
890
|
for (;;) {
|
|
682
|
-
const command = yield*
|
|
891
|
+
const command = yield* takeQueue(commandQueue);
|
|
892
|
+
if (ended !== undefined)
|
|
893
|
+
continue;
|
|
683
894
|
const result = yield* Effect.result(executeCommand(command));
|
|
895
|
+
dispatching = undefined;
|
|
896
|
+
// Capture uncertainty before actor delivery: drain may be awaiting a
|
|
897
|
+
// permit in stopRenewal while this committed command finishes.
|
|
898
|
+
if (ended === undefined &&
|
|
899
|
+
Result.isFailure(result) &&
|
|
900
|
+
result.failure.context.outcome === "unknown") {
|
|
901
|
+
let fresh;
|
|
902
|
+
const sessionId = engine.enqueueOnSource === undefined ||
|
|
903
|
+
(command._tag !== "Build" && command._tag !== "BuildFiller")
|
|
904
|
+
? undefined
|
|
905
|
+
: command.sessionId;
|
|
906
|
+
if (command._tag === "Build") {
|
|
907
|
+
const item = items.get(command.key);
|
|
908
|
+
if (item !== undefined && item.phase !== "Terminal" && item.clipId === undefined) {
|
|
909
|
+
item.unknownAtMs ??= monotonicMillis(clock);
|
|
910
|
+
item.unknownCause = result.failure;
|
|
911
|
+
item.unknownSessionId = sessionId;
|
|
912
|
+
fresh = { sessionId, atMs: item.unknownAtMs, cause: result.failure };
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
else if (command._tag === "BuildFiller" &&
|
|
916
|
+
!filler.has(command.index) &&
|
|
917
|
+
!unknownFillers.has(command.index)) {
|
|
918
|
+
fresh = { sessionId, atMs: monotonicMillis(clock), cause: result.failure };
|
|
919
|
+
unknownFillers.set(command.index, { ...fresh, index: command.index });
|
|
920
|
+
}
|
|
921
|
+
updateRecovery(recoveryState, fresh);
|
|
922
|
+
}
|
|
684
923
|
yield* Queue.offer(inbox, { _tag: "CommandDone", command, result });
|
|
685
924
|
}
|
|
686
925
|
});
|
|
687
926
|
const sendCommand = (command) => Effect.gen(function* () {
|
|
927
|
+
if (ended !== undefined)
|
|
928
|
+
return;
|
|
688
929
|
if (command._tag === "Build")
|
|
689
930
|
pendingBuilds.add(command.key);
|
|
690
931
|
if (command._tag === "DeferAt")
|
|
@@ -793,6 +1034,12 @@ export const makeScheduler = (options) => Effect.gen(function* () {
|
|
|
793
1034
|
break;
|
|
794
1035
|
}
|
|
795
1036
|
case "BuildFiller": {
|
|
1037
|
+
if (unknownFillers.size >= maxUnknownFillers) {
|
|
1038
|
+
yield* closeActor(ReactorError.fromCode("Overflow", "Scheduler uncertain filler ledger is full", {
|
|
1039
|
+
operation: "scheduler.unknownRecovery",
|
|
1040
|
+
}));
|
|
1041
|
+
break;
|
|
1042
|
+
}
|
|
796
1043
|
const captured = upcomingFiller === undefined
|
|
797
1044
|
? yield* Effect.result(captureFiller(fillerIndex, runwaySeconds(state, monotonicMillis(clock), playingStartedMs, owned, [
|
|
798
1045
|
...items.values(),
|
|
@@ -862,6 +1109,8 @@ export const makeScheduler = (options) => Effect.gen(function* () {
|
|
|
862
1109
|
item.sessionId =
|
|
863
1110
|
knownRecord(yield* engine.state, clipId)?.sessionId ??
|
|
864
1111
|
(engine.enqueueOnSource === undefined ? undefined : command.sessionId);
|
|
1112
|
+
provedUnknown(item.unknownAtMs);
|
|
1113
|
+
item.unknownAtMs = undefined;
|
|
865
1114
|
item.unknownSessionId = undefined;
|
|
866
1115
|
item.unknownCause = undefined;
|
|
867
1116
|
yield* replayEarlyClipEvents(clipId);
|
|
@@ -928,11 +1177,7 @@ export const makeScheduler = (options) => Effect.gen(function* () {
|
|
|
928
1177
|
yield* replayEarlyClipEvents(clipId);
|
|
929
1178
|
}
|
|
930
1179
|
else if (result.failure.context.outcome === "unknown") {
|
|
931
|
-
|
|
932
|
-
unknownFiller = {
|
|
933
|
-
index: command.index,
|
|
934
|
-
sessionId: engine.enqueueOnSource === undefined ? undefined : command.sessionId,
|
|
935
|
-
};
|
|
1180
|
+
// The worker already retained this identity and its original age.
|
|
936
1181
|
}
|
|
937
1182
|
else {
|
|
938
1183
|
fillerRetryAtMs = monotonicMillis(clock) + 1_000;
|
|
@@ -1097,28 +1342,26 @@ export const makeScheduler = (options) => Effect.gen(function* () {
|
|
|
1097
1342
|
};
|
|
1098
1343
|
const reconcile = Effect.gen(function* () {
|
|
1099
1344
|
const state = yield* engine.state;
|
|
1345
|
+
if (ended !== undefined)
|
|
1346
|
+
return;
|
|
1100
1347
|
prune(state);
|
|
1101
1348
|
refreshAnchors();
|
|
1349
|
+
const sources = projectSources(state);
|
|
1102
1350
|
if (!observationReady) {
|
|
1103
|
-
yield* publishState(state);
|
|
1351
|
+
yield* publishState(state, sources.sessions);
|
|
1104
1352
|
return;
|
|
1105
1353
|
}
|
|
1106
|
-
|
|
1107
|
-
const
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
if (sourceRetired
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
fillerIndex <= uncertain.index) {
|
|
1118
|
-
// A replacement may build independently, with a distinct key. The
|
|
1119
|
-
// uncertain old request can still be adopted if it later appears.
|
|
1120
|
-
fillerIndex = uncertain.index + 1;
|
|
1121
|
-
upcomingFiller = undefined;
|
|
1354
|
+
for (const uncertain of unknownFillers.values()) {
|
|
1355
|
+
const sourceRetired = uncertain.sessionId !== undefined && !sources.has(uncertain.sessionId);
|
|
1356
|
+
if (sourceRetired)
|
|
1357
|
+
unknownFillers.delete(uncertain.index);
|
|
1358
|
+
if (sourceRetired ||
|
|
1359
|
+
(uncertain.sessionId !== undefined &&
|
|
1360
|
+
Option.getOrUndefined(state.preferredSessionId) !== uncertain.sessionId)) {
|
|
1361
|
+
if (fillerIndex <= uncertain.index) {
|
|
1362
|
+
fillerIndex = uncertain.index + 1;
|
|
1363
|
+
upcomingFiller = undefined;
|
|
1364
|
+
}
|
|
1122
1365
|
}
|
|
1123
1366
|
}
|
|
1124
1367
|
for (const [key, pending] of pendingWithdrawals) {
|
|
@@ -1134,10 +1377,18 @@ export const makeScheduler = (options) => Effect.gen(function* () {
|
|
|
1134
1377
|
if (item.phase === "Unknown" &&
|
|
1135
1378
|
item.clipId === undefined &&
|
|
1136
1379
|
item.unknownSessionId !== undefined &&
|
|
1137
|
-
!
|
|
1380
|
+
!sources.has(item.unknownSessionId))
|
|
1138
1381
|
yield* resolveRetiredUnknown(item);
|
|
1382
|
+
// After retirement, so a deadline no longer outlives the uncertainty that
|
|
1383
|
+
// set it until the next message.
|
|
1384
|
+
updateRecovery(state);
|
|
1139
1385
|
if (draining)
|
|
1140
1386
|
yield* drainPending(state);
|
|
1387
|
+
const preferred = Option.getOrUndefined(state.preferredSessionId);
|
|
1388
|
+
let unknownFillerCount = 0;
|
|
1389
|
+
for (const entry of unknownFillers.values())
|
|
1390
|
+
if (entry.sessionId === undefined || entry.sessionId === preferred)
|
|
1391
|
+
unknownFillerCount++;
|
|
1141
1392
|
const decision = plan({
|
|
1142
1393
|
engine: state,
|
|
1143
1394
|
items: [...items.values()],
|
|
@@ -1152,12 +1403,11 @@ export const makeScheduler = (options) => Effect.gen(function* () {
|
|
|
1152
1403
|
accepting: accepting || (draining && finishAccepted),
|
|
1153
1404
|
fillerEnabled: !draining || fillerHeld(monotonicMillis(clock)),
|
|
1154
1405
|
fillerRetryAtMs,
|
|
1155
|
-
|
|
1156
|
-
fillerUnknownSessionId: unknownFiller?.sessionId,
|
|
1406
|
+
unknownFillerCount,
|
|
1157
1407
|
blockedMove,
|
|
1158
1408
|
});
|
|
1159
1409
|
refillActive = decision.refillActive;
|
|
1160
|
-
yield* publishState(state);
|
|
1410
|
+
yield* publishState(state, sources.sessions);
|
|
1161
1411
|
if ((!draining || finishAccepted) && commandCount === 0 && decision.action !== undefined)
|
|
1162
1412
|
yield* applyPolicy(decision.action, state);
|
|
1163
1413
|
if (draining &&
|
|
@@ -1173,10 +1423,14 @@ export const makeScheduler = (options) => Effect.gen(function* () {
|
|
|
1173
1423
|
commandCount === 0 &&
|
|
1174
1424
|
[...items.values()].every((item) => item.phase === "Terminal") &&
|
|
1175
1425
|
pendingWithdrawals.size === 0 &&
|
|
1176
|
-
|
|
1426
|
+
unknownFillers.size === 0) {
|
|
1177
1427
|
const after = yield* engine.state;
|
|
1178
1428
|
if (Option.isNone(after.playing) && !activeIds(after).some((clipId) => owned.has(clipId))) {
|
|
1179
1429
|
drained = true;
|
|
1430
|
+
// A truthful completed drain no longer needs a replacement to resume
|
|
1431
|
+
// service, even when retirement left a capacity episode behind.
|
|
1432
|
+
capacityRecovery = undefined;
|
|
1433
|
+
updateRecovery(after);
|
|
1180
1434
|
const replies = drainReplies.splice(0);
|
|
1181
1435
|
for (const reply of replies)
|
|
1182
1436
|
yield* Deferred.succeed(reply, undefined);
|
|
@@ -1185,16 +1439,10 @@ export const makeScheduler = (options) => Effect.gen(function* () {
|
|
|
1185
1439
|
});
|
|
1186
1440
|
const actor = Effect.gen(function* () {
|
|
1187
1441
|
for (;;) {
|
|
1188
|
-
const message = yield*
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
yield* Deferred.fail(message.reply, PolicyFailure.refuse("SessionClosed", ended.message));
|
|
1192
|
-
else if (message._tag === "Withdraw")
|
|
1193
|
-
yield* Deferred.fail(message.reply, PolicyFailure.refuse("SessionClosed", ended.message));
|
|
1194
|
-
else if (message._tag === "Drain")
|
|
1195
|
-
yield* Deferred.fail(message.reply, PolicyFailure.refuse("SessionClosed", ended.message));
|
|
1442
|
+
const message = yield* takeQueue(inbox);
|
|
1443
|
+
// Queued public calls observe the same terminal result through closedCall.
|
|
1444
|
+
if (ended !== undefined && message._tag !== "CommandDone")
|
|
1196
1445
|
continue;
|
|
1197
|
-
}
|
|
1198
1446
|
switch (message._tag) {
|
|
1199
1447
|
case "Submit": {
|
|
1200
1448
|
if (!accepting) {
|
|
@@ -1211,6 +1459,8 @@ export const makeScheduler = (options) => Effect.gen(function* () {
|
|
|
1211
1459
|
}
|
|
1212
1460
|
const nowMs = monotonicMillis(clock);
|
|
1213
1461
|
const state = yield* engine.state;
|
|
1462
|
+
if (ended !== undefined)
|
|
1463
|
+
continue;
|
|
1214
1464
|
const startByMs = message.item.startByOffsetMs === undefined
|
|
1215
1465
|
? undefined
|
|
1216
1466
|
: nowMs + message.item.startByOffsetMs;
|
|
@@ -1219,9 +1469,12 @@ export const makeScheduler = (options) => Effect.gen(function* () {
|
|
|
1219
1469
|
yield* Deferred.fail(message.reply, WouldMissDeadline.of(message.item.key));
|
|
1220
1470
|
break;
|
|
1221
1471
|
}
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1472
|
+
// No yield from the terminal check above through admission and its
|
|
1473
|
+
// publication below: a claim either precedes the item or settles it
|
|
1474
|
+
// after its Accepted evidence.
|
|
1475
|
+
const startedWaiter = Deferred.makeUnsafe();
|
|
1476
|
+
const outcomeWaiter = Deferred.makeUnsafe();
|
|
1477
|
+
const firstDecisiveWaiter = Deferred.makeUnsafe();
|
|
1225
1478
|
const handle = {
|
|
1226
1479
|
key: message.item.key,
|
|
1227
1480
|
started: Deferred.await(startedWaiter),
|
|
@@ -1251,18 +1504,22 @@ export const makeScheduler = (options) => Effect.gen(function* () {
|
|
|
1251
1504
|
sessionId: undefined,
|
|
1252
1505
|
unknownSessionId: undefined,
|
|
1253
1506
|
unknownCause: undefined,
|
|
1507
|
+
unknownAtMs: undefined,
|
|
1254
1508
|
handle,
|
|
1255
1509
|
startedWaiter,
|
|
1256
1510
|
outcomeWaiter,
|
|
1257
1511
|
firstDecisiveWaiter,
|
|
1258
1512
|
};
|
|
1259
1513
|
items.set(item.key, item);
|
|
1260
|
-
|
|
1514
|
+
PubSub.publishUnsafe(events, {
|
|
1261
1515
|
key: item.key,
|
|
1262
1516
|
at: clock.currentTimeMillisUnsafe(),
|
|
1263
1517
|
status: item.status,
|
|
1264
1518
|
});
|
|
1265
1519
|
yield* observed(state);
|
|
1520
|
+
// Submit returns its committed view; other messages publish once
|
|
1521
|
+
// through reconciliation after all their evidence is adopted.
|
|
1522
|
+
yield* publishState(state);
|
|
1266
1523
|
yield* Deferred.succeed(message.reply, handle);
|
|
1267
1524
|
break;
|
|
1268
1525
|
}
|
|
@@ -1288,9 +1545,12 @@ export const makeScheduler = (options) => Effect.gen(function* () {
|
|
|
1288
1545
|
if (!draining) {
|
|
1289
1546
|
draining = true;
|
|
1290
1547
|
finishAccepted = message.finish === "accepted";
|
|
1548
|
+
updateRecovery();
|
|
1291
1549
|
// Renewal admission must stop even while an earlier enqueue is
|
|
1292
1550
|
// awaiting its reply in the command worker.
|
|
1293
1551
|
const stoppedRenewal = yield* Effect.result(engine.stopRenewal);
|
|
1552
|
+
if (ended !== undefined)
|
|
1553
|
+
break;
|
|
1294
1554
|
if (Result.isFailure(stoppedRenewal)) {
|
|
1295
1555
|
drainFailure = stoppedRenewal.failure;
|
|
1296
1556
|
for (const reply of drainReplies.splice(0))
|
|
@@ -1362,22 +1622,12 @@ export const makeScheduler = (options) => Effect.gen(function* () {
|
|
|
1362
1622
|
}
|
|
1363
1623
|
}
|
|
1364
1624
|
});
|
|
1365
|
-
//
|
|
1366
|
-
//
|
|
1625
|
+
// Orderly scope teardown has already claimed and settled Closed before
|
|
1626
|
+
// worker interruption; unexpected interruption retains its original Cause.
|
|
1367
1627
|
const supervise = (effect) => effect.pipe(Effect.onExit((exit) => Exit.isFailure(exit) && ended === undefined
|
|
1368
|
-
?
|
|
1369
|
-
accepting = false;
|
|
1370
|
-
yield* Deferred.failCause(stopped, exit.cause);
|
|
1371
|
-
yield* Deferred.failCause(initialized, exit.cause);
|
|
1372
|
-
for (const item of items.values()) {
|
|
1373
|
-
yield* Deferred.failCause(item.startedWaiter, exit.cause);
|
|
1374
|
-
yield* Deferred.failCause(item.outcomeWaiter, exit.cause);
|
|
1375
|
-
yield* Deferred.failCause(item.firstDecisiveWaiter, exit.cause);
|
|
1376
|
-
}
|
|
1377
|
-
yield* Queue.shutdown(inbox);
|
|
1378
|
-
yield* Queue.shutdown(commandQueue);
|
|
1379
|
-
})
|
|
1628
|
+
? terminate(Exit.failCause(exit.cause))
|
|
1380
1629
|
: Effect.void));
|
|
1630
|
+
yield* Effect.forkScoped(supervise(watchdog));
|
|
1381
1631
|
yield* Effect.forkScoped(supervise(commandWorker));
|
|
1382
1632
|
yield* Effect.forkScoped(supervise(actor));
|
|
1383
1633
|
yield* Effect.forkScoped(supervise(observe));
|
|
@@ -1425,6 +1675,6 @@ export const makeScheduler = (options) => Effect.gen(function* () {
|
|
|
1425
1675
|
state: SubscriptionRef.get(stateRef),
|
|
1426
1676
|
asRun: Stream.fromPubSub(events),
|
|
1427
1677
|
};
|
|
1428
|
-
});
|
|
1678
|
+
}).pipe(Effect.provideServiceEffect(Scope.Scope, Effect.flatMap(Scope.Scope, (parent) => Scope.fork(parent, "sequential"))));
|
|
1429
1679
|
export const layerScheduler = (options) => Layer.effect(Scheduler, makeScheduler(options));
|
|
1430
1680
|
//# sourceMappingURL=scheduler.js.map
|