@effect-agent/platform-cloudflare 0.1.0-beta.112 → 0.1.0-beta.113
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/dist/Alarm.d.mts +35 -11
- package/dist/Alarm.mjs +110 -24
- package/dist/Alarm.mjs.map +1 -1
- package/dist/CloudflareThreadClient.d.mts +32 -32
- package/dist/{ThreadObject-0k-SVpY_.mjs → ThreadObject-BPt5uByD.mjs} +17 -8
- package/dist/ThreadObject-BPt5uByD.mjs.map +1 -0
- package/dist/{ThreadObject-DVxgrU7c.d.mts → ThreadObject-DAuaRyGl.d.mts} +34 -34
- package/dist/ThreadObject.d.mts +1 -1
- package/dist/ThreadObject.mjs +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
- package/src/Alarm.ts +234 -36
- package/src/internal/message-delivery.ts +33 -23
- package/dist/ThreadObject-0k-SVpY_.mjs.map +0 -1
package/src/Alarm.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
Fiber,
|
|
10
10
|
Layer,
|
|
11
11
|
Option,
|
|
12
|
+
PubSub,
|
|
12
13
|
Random,
|
|
13
14
|
Ref,
|
|
14
15
|
Result,
|
|
@@ -285,6 +286,65 @@ export class ThreadPublication extends Context.Service<
|
|
|
285
286
|
});
|
|
286
287
|
}
|
|
287
288
|
|
|
289
|
+
/** Event-local accounting shared by the existing maintenance pumps and native scheduler. */
|
|
290
|
+
export class ThreadMaintenanceActivity extends Context.Service<
|
|
291
|
+
ThreadMaintenanceActivity,
|
|
292
|
+
{
|
|
293
|
+
/**
|
|
294
|
+
* Account for one finite selection/dispatch, through joined completion or interruption.
|
|
295
|
+
* Register before reading/claiming work. After dispatch closes the body is not started.
|
|
296
|
+
* Host waves retain their declared allowance from this call; message Claims keep the driver's
|
|
297
|
+
* own deadline. Waiting for notifications belongs outside this bracket.
|
|
298
|
+
*/
|
|
299
|
+
readonly run: <R>(
|
|
300
|
+
wave: Effect.Effect<void, DurableAlarmError, R>,
|
|
301
|
+
) => Effect.Effect<void, DurableAlarmError, R>;
|
|
302
|
+
/** Acknowledge all initial subscriptions/scans. Composite hosts use `all` below. */
|
|
303
|
+
readonly ready: Effect.Effect<void>;
|
|
304
|
+
/**
|
|
305
|
+
* Acquire an independent scoped subscription before the initial scan and `ready`.
|
|
306
|
+
* The returned wait observes coalesced native scan hints, including those published before
|
|
307
|
+
* its first execution. Each hint requests a current local due-work check.
|
|
308
|
+
*/
|
|
309
|
+
readonly subscribeChanges: Effect.Effect<Effect.Effect<void>, never, Scope.Scope>;
|
|
310
|
+
}
|
|
311
|
+
>()("@effect-agent/platform-cloudflare/ThreadMaintenanceActivity") {
|
|
312
|
+
/** Join child pumps; acknowledge the parent only after every child is ready or has exited. */
|
|
313
|
+
static readonly all = Effect.fnUntraced(function* <A, E, R>(
|
|
314
|
+
lanes: ReadonlyArray<Effect.Effect<A, E, R>>,
|
|
315
|
+
): Effect.fn.Return<ReadonlyArray<A>, E, R | ThreadMaintenanceActivity> {
|
|
316
|
+
const activity = yield* ThreadMaintenanceActivity;
|
|
317
|
+
let remaining = lanes.length;
|
|
318
|
+
|
|
319
|
+
if (remaining === 0) return yield* activity.ready.pipe(Effect.as([]));
|
|
320
|
+
|
|
321
|
+
return yield* Effect.forEach(
|
|
322
|
+
lanes,
|
|
323
|
+
(lane) => {
|
|
324
|
+
let ready = false;
|
|
325
|
+
|
|
326
|
+
const child: ThreadMaintenanceActivity["Service"] = {
|
|
327
|
+
...activity,
|
|
328
|
+
ready: Effect.suspend(() => {
|
|
329
|
+
if (ready) return Effect.void;
|
|
330
|
+
ready = true;
|
|
331
|
+
|
|
332
|
+
return --remaining === 0 ? activity.ready : Effect.void;
|
|
333
|
+
}).pipe(Effect.uninterruptible),
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
// A failed initial setup is accounted for without swallowing its Cause. Callers
|
|
337
|
+
// may collect Exits when sibling pumps must keep their independent opportunities.
|
|
338
|
+
return lane.pipe(
|
|
339
|
+
Effect.provideService(ThreadMaintenanceActivity, child),
|
|
340
|
+
Effect.onExit(() => child.ready),
|
|
341
|
+
);
|
|
342
|
+
},
|
|
343
|
+
{ concurrency: "unbounded" },
|
|
344
|
+
);
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
288
348
|
/**
|
|
289
349
|
* Host-assembled native message recovery. The driver bounds each actual Claim and persists its
|
|
290
350
|
* timeout/retry before this pump returns. Do not add a second timer starting at batch selection:
|
|
@@ -294,7 +354,7 @@ export const ThreadMessageDelivery = Context.Reference<{
|
|
|
294
354
|
readonly drainUntil: (
|
|
295
355
|
dispatchClosed: Effect.Effect<void>,
|
|
296
356
|
dispatchUntil: DateTime.Utc,
|
|
297
|
-
) => Effect.Effect<void, DurableAlarmError, Scope.Scope>;
|
|
357
|
+
) => Effect.Effect<void, DurableAlarmError, Scope.Scope | ThreadMaintenanceActivity>;
|
|
298
358
|
readonly pendingDeadline: Effect.Effect<Option.Option<number>, DurableAlarmError>;
|
|
299
359
|
}>("@effect-agent/platform-cloudflare/ThreadMessageDelivery", {
|
|
300
360
|
defaultValue: () => ({
|
|
@@ -305,14 +365,16 @@ export const ThreadMessageDelivery = Context.Reference<{
|
|
|
305
365
|
|
|
306
366
|
/**
|
|
307
367
|
* Application obligations sharing this Object's alarm. Admit one initial external wave even on
|
|
308
|
-
* a caught-up pass, then respond to wakes
|
|
309
|
-
*
|
|
310
|
-
*
|
|
368
|
+
* a caught-up pass, then respond to wakes and ThreadMaintenanceActivity.subscribeChanges until
|
|
369
|
+
* dispatchClosed. Yield the event's ThreadMaintenanceActivity to bracket each finite lane with
|
|
370
|
+
* run and acknowledge initial setup with ready.
|
|
371
|
+
* Passive subscriptions do not count as active work. Closure stops both new waves and native
|
|
372
|
+
* Attempts; local admission/control subscriptions belong to the event Scope until teardown.
|
|
311
373
|
* No deadline sleeps or automatic retry loops. Return after already-admitted waves finish.
|
|
312
374
|
*
|
|
313
375
|
* Declare a finite whole-wave allowance (1..300000ms): maximum for parallel lanes, sum for
|
|
314
376
|
* sequential operations. Admit a wave only if its allowance fits before dispatchUntil. Later
|
|
315
|
-
* arrivals cannot renew
|
|
377
|
+
* arrivals cannot renew an admitted wave's allowance. Maintenance bounds each registered wave,
|
|
316
378
|
* interrupts and joins event Scope, then reads local deadlines under the mutation gate.
|
|
317
379
|
*
|
|
318
380
|
* Setup and pendingDeadline are bounded local operations. Persist claims/envelopes before
|
|
@@ -326,7 +388,7 @@ export const ThreadHostMaintenance = Context.Reference<{
|
|
|
326
388
|
readonly drainUntil: (
|
|
327
389
|
dispatchClosed: Effect.Effect<void>,
|
|
328
390
|
dispatchUntil: DateTime.Utc,
|
|
329
|
-
) => Effect.Effect<void, DurableAlarmError, Scope.Scope>;
|
|
391
|
+
) => Effect.Effect<void, DurableAlarmError, Scope.Scope | ThreadMaintenanceActivity>;
|
|
330
392
|
readonly pendingDeadline: Effect.Effect<Option.Option<number>, DurableAlarmError>;
|
|
331
393
|
}>("@effect-agent/platform-cloudflare/ThreadHostMaintenance", {
|
|
332
394
|
defaultValue: () => ({
|
|
@@ -622,10 +684,10 @@ export type MaintenancePassFailure =
|
|
|
622
684
|
* recovery, ledger scans or canonical-history reads.
|
|
623
685
|
* 2. Reconcile before each head Attempt, then checkpoint only the observed generation. A racing
|
|
624
686
|
* producer keeps its newer generation dirty. Native retries retain their durable backoff.
|
|
625
|
-
* 3. Keep delivery admission open while
|
|
626
|
-
*
|
|
627
|
-
*
|
|
628
|
-
* 4. Native message delivery retains its driver-owned Claim deadline. Host/backfill
|
|
687
|
+
* 3. Keep native and delivery admission open together while finite waves remain active, so
|
|
688
|
+
* fresh replies and abort controls can progress during unrelated cleanup. Close atomically
|
|
689
|
+
* at quiescence, or at the original ten-minute yield deadline, before retiring listeners.
|
|
690
|
+
* 4. Native message delivery retains its driver-owned Claim deadline. Host/backfill waves are
|
|
629
691
|
* bounded independently; incoming native work never restarts or cancels their attempts.
|
|
630
692
|
* Auxiliary failures are reported after the current native opportunity.
|
|
631
693
|
* 5. Close every event resource before the final gated deadline snapshot and alarm decision.
|
|
@@ -1392,6 +1454,83 @@ export class ThreadMaintenance extends Context.Service<
|
|
|
1392
1454
|
Scope.close(scope, exit),
|
|
1393
1455
|
);
|
|
1394
1456
|
|
|
1457
|
+
const dispatchClosed = yield* Deferred.make<void>();
|
|
1458
|
+
const stopDispatch = Deferred.await(dispatchClosed);
|
|
1459
|
+
// Coalesce local scan hints while a lane is busy; one hint requests a current read.
|
|
1460
|
+
const checks = yield* PubSub.sliding<void>(1);
|
|
1461
|
+
|
|
1462
|
+
yield* Effect.addFinalizer(() => PubSub.shutdown(checks));
|
|
1463
|
+
let dispatchOpen = true;
|
|
1464
|
+
let activityChanged = yield* Deferred.make<void>();
|
|
1465
|
+
const signalActivity = Effect.suspend(() => Deferred.succeed(activityChanged, undefined));
|
|
1466
|
+
|
|
1467
|
+
const closeDispatch = Effect.sync(() => {
|
|
1468
|
+
dispatchOpen = false;
|
|
1469
|
+
}).pipe(Effect.andThen(Deferred.succeed(dispatchClosed, undefined)));
|
|
1470
|
+
|
|
1471
|
+
const makeActivity = Effect.fnUntraced(function* (allowance?: number) {
|
|
1472
|
+
const initialized = yield* Deferred.make<void>();
|
|
1473
|
+
let active = 0;
|
|
1474
|
+
let failed = false;
|
|
1475
|
+
|
|
1476
|
+
const ready = Deferred.succeed(initialized, undefined).pipe(
|
|
1477
|
+
Effect.andThen(signalActivity),
|
|
1478
|
+
Effect.asVoid,
|
|
1479
|
+
Effect.uninterruptible,
|
|
1480
|
+
);
|
|
1481
|
+
|
|
1482
|
+
const activity: ThreadMaintenanceActivity["Service"] = {
|
|
1483
|
+
ready,
|
|
1484
|
+
subscribeChanges: PubSub.subscribe(checks).pipe(Effect.map(PubSub.take)),
|
|
1485
|
+
run: (wave) =>
|
|
1486
|
+
Effect.acquireUseRelease(
|
|
1487
|
+
Effect.sync(() => {
|
|
1488
|
+
if (!dispatchOpen) return false;
|
|
1489
|
+
active++;
|
|
1490
|
+
|
|
1491
|
+
return true;
|
|
1492
|
+
}),
|
|
1493
|
+
(admitted) =>
|
|
1494
|
+
!admitted
|
|
1495
|
+
? Effect.void
|
|
1496
|
+
: (allowance === undefined
|
|
1497
|
+
? wave
|
|
1498
|
+
: wave.pipe(
|
|
1499
|
+
Effect.timeoutOrElse({
|
|
1500
|
+
duration: allowance,
|
|
1501
|
+
orElse: () =>
|
|
1502
|
+
DurableAlarmError.make({
|
|
1503
|
+
operation: "host dispatch allowance",
|
|
1504
|
+
message:
|
|
1505
|
+
"The admitted host wave exceeded its allowance; durable work remains pending",
|
|
1506
|
+
}),
|
|
1507
|
+
}),
|
|
1508
|
+
)
|
|
1509
|
+
).pipe(
|
|
1510
|
+
Effect.onExit((exit) =>
|
|
1511
|
+
Effect.sync(() => {
|
|
1512
|
+
if (Exit.isFailure(exit)) failed = true;
|
|
1513
|
+
}),
|
|
1514
|
+
),
|
|
1515
|
+
),
|
|
1516
|
+
(admitted) =>
|
|
1517
|
+
Effect.sync(() => {
|
|
1518
|
+
if (admitted) active--;
|
|
1519
|
+
}).pipe(Effect.andThen(signalActivity)),
|
|
1520
|
+
),
|
|
1521
|
+
};
|
|
1522
|
+
|
|
1523
|
+
return {
|
|
1524
|
+
activity,
|
|
1525
|
+
initialized: Deferred.await(initialized),
|
|
1526
|
+
quiet: () => active === 0 && Deferred.isDoneUnsafe(initialized),
|
|
1527
|
+
failed: () => failed,
|
|
1528
|
+
};
|
|
1529
|
+
});
|
|
1530
|
+
|
|
1531
|
+
const messageActivity = yield* makeActivity();
|
|
1532
|
+
const hostActivity = yield* makeActivity(host.dispatchTimeoutMillis);
|
|
1533
|
+
|
|
1395
1534
|
const recovery: NativeRecovery = {
|
|
1396
1535
|
queue: yield* Deferred.make<ReadonlyArray<ThreadId>>(),
|
|
1397
1536
|
pending: new Set(),
|
|
@@ -1429,13 +1568,15 @@ export class ThreadMaintenance extends Context.Service<
|
|
|
1429
1568
|
auxiliaryScope,
|
|
1430
1569
|
);
|
|
1431
1570
|
|
|
1432
|
-
const dispatchClosed = yield* Deferred.make<void>();
|
|
1433
|
-
const stopDispatch = Deferred.await(dispatchClosed);
|
|
1434
|
-
|
|
1435
1571
|
// Fork setup too: an ordinary auxiliary setup failure is reported after native work,
|
|
1436
1572
|
// rather than gating its opportunity. Event interruption still closes every fiber.
|
|
1437
1573
|
const deliveryFiber = yield* Effect.forkIn(
|
|
1438
|
-
Scope.provide(auxiliaryScope)(
|
|
1574
|
+
Scope.provide(auxiliaryScope)(
|
|
1575
|
+
messages.drainUntil(stopDispatch, dispatchUntil).pipe(
|
|
1576
|
+
Effect.provideService(ThreadMaintenanceActivity, messageActivity.activity),
|
|
1577
|
+
Effect.onExit(() => messageActivity.activity.ready),
|
|
1578
|
+
),
|
|
1579
|
+
),
|
|
1439
1580
|
auxiliaryScope,
|
|
1440
1581
|
);
|
|
1441
1582
|
|
|
@@ -1452,8 +1593,29 @@ export class ThreadMaintenance extends Context.Service<
|
|
|
1452
1593
|
}),
|
|
1453
1594
|
),
|
|
1454
1595
|
);
|
|
1455
|
-
|
|
1456
|
-
|
|
1596
|
+
|
|
1597
|
+
// Initial setup must also be finite. Once it is accounted for, each actual
|
|
1598
|
+
// wave has its own unchanged allowance; passive listeners have no timer.
|
|
1599
|
+
const initialization = hostActivity.initialized.pipe(
|
|
1600
|
+
Effect.timeoutOrElse({
|
|
1601
|
+
duration: host.dispatchTimeoutMillis,
|
|
1602
|
+
orElse: () =>
|
|
1603
|
+
DurableAlarmError.make({
|
|
1604
|
+
operation: "host initialization",
|
|
1605
|
+
message:
|
|
1606
|
+
"The host did not account for initial maintenance before its allowance; durable work remains pending",
|
|
1607
|
+
}),
|
|
1608
|
+
}),
|
|
1609
|
+
Effect.andThen(Effect.never),
|
|
1610
|
+
);
|
|
1611
|
+
|
|
1612
|
+
yield* Effect.raceFirst(
|
|
1613
|
+
host
|
|
1614
|
+
.drainUntil(stopDispatch, dispatchUntil)
|
|
1615
|
+
.pipe(Effect.provideService(ThreadMaintenanceActivity, hostActivity.activity)),
|
|
1616
|
+
initialization,
|
|
1617
|
+
);
|
|
1618
|
+
}).pipe(Effect.onExit(() => hostActivity.activity.ready)),
|
|
1457
1619
|
),
|
|
1458
1620
|
auxiliaryScope,
|
|
1459
1621
|
);
|
|
@@ -1463,6 +1625,7 @@ export class ThreadMaintenance extends Context.Service<
|
|
|
1463
1625
|
drainDue.pipe(
|
|
1464
1626
|
Effect.provideService(ThreadProjectionMaintenance, projection),
|
|
1465
1627
|
Effect.timeoutOption(config.projectionDispatchTimeoutMillis),
|
|
1628
|
+
Effect.onExit(() => signalActivity),
|
|
1466
1629
|
),
|
|
1467
1630
|
auxiliaryScope,
|
|
1468
1631
|
);
|
|
@@ -1477,9 +1640,8 @@ export class ThreadMaintenance extends Context.Service<
|
|
|
1477
1640
|
|
|
1478
1641
|
observed.nativeOnly = false;
|
|
1479
1642
|
|
|
1480
|
-
//
|
|
1481
|
-
//
|
|
1482
|
-
// waiting for host retirement to close it would create a circular join.
|
|
1643
|
+
// Pump termination and quiescence are distinct: listeners await closure, while their
|
|
1644
|
+
// finite waves report activity. A held sibling never retires another lane's controls.
|
|
1483
1645
|
const hostJoin = yield* Effect.forkIn(
|
|
1484
1646
|
Effect.gen(function* () {
|
|
1485
1647
|
yield* stopDispatch;
|
|
@@ -1502,28 +1664,60 @@ export class ThreadMaintenance extends Context.Service<
|
|
|
1502
1664
|
Fiber.joinAll([deliveryFiber, hostJoin, backfill, recoveryFiber]),
|
|
1503
1665
|
);
|
|
1504
1666
|
|
|
1505
|
-
const auxiliaryPending = () =>
|
|
1506
|
-
deliveryFiber.pollUnsafe() === undefined ||
|
|
1507
|
-
hostFiber.pollUnsafe() === undefined ||
|
|
1508
|
-
backfill.pollUnsafe() === undefined ||
|
|
1509
|
-
recoveryFiber.pollUnsafe() === undefined;
|
|
1510
|
-
|
|
1511
1667
|
while (true) {
|
|
1512
1668
|
const recoveryFinished = recoveryFiber.pollUnsafe() !== undefined;
|
|
1669
|
+
const retiredExit = retired.pollUnsafe();
|
|
1513
1670
|
|
|
1514
1671
|
if (recoveryFinished) {
|
|
1515
1672
|
if (recovery.needsCheckpoint) {
|
|
1516
1673
|
yield* Fiber.join(recoveryFiber);
|
|
1517
|
-
//
|
|
1518
|
-
// delivery window closes, never after all reply listeners have retired.
|
|
1674
|
+
// Fold the original recovery observation into its checkpoint before closing.
|
|
1519
1675
|
result = yield* advance(started, yieldAfter, observed, recovery, settled === 0);
|
|
1520
1676
|
if (result.phase === "actionable") phase = "actionable";
|
|
1521
1677
|
settled += result.settled;
|
|
1522
1678
|
recovery.needsCheckpoint = false;
|
|
1679
|
+
yield* PubSub.publish(checks, undefined);
|
|
1523
1680
|
}
|
|
1524
|
-
yield* Deferred.succeed(dispatchClosed, undefined);
|
|
1525
1681
|
}
|
|
1526
|
-
if (
|
|
1682
|
+
if (retiredExit !== undefined && Exit.isFailure(retiredExit)) break;
|
|
1683
|
+
|
|
1684
|
+
if (
|
|
1685
|
+
recoveryFinished &&
|
|
1686
|
+
backfill.pollUnsafe() !== undefined &&
|
|
1687
|
+
messageActivity.quiet() &&
|
|
1688
|
+
hostActivity.quiet()
|
|
1689
|
+
) {
|
|
1690
|
+
const now = yield* Clock.currentTimeMillis;
|
|
1691
|
+
const messageDeadline = yield* messages.pendingDeadline;
|
|
1692
|
+
const hostDeadline = yield* host.pendingDeadline;
|
|
1693
|
+
|
|
1694
|
+
const messageDue =
|
|
1695
|
+
deliveryFiber.pollUnsafe() === undefined &&
|
|
1696
|
+
!messageActivity.failed() &&
|
|
1697
|
+
Option.isSome(messageDeadline) &&
|
|
1698
|
+
messageDeadline.value <= now;
|
|
1699
|
+
|
|
1700
|
+
const hostDue =
|
|
1701
|
+
hostFiber.pollUnsafe() === undefined &&
|
|
1702
|
+
!hostActivity.failed() &&
|
|
1703
|
+
now + host.dispatchTimeoutMillis <= DateTime.toEpochMillis(dispatchUntil) &&
|
|
1704
|
+
Option.isSome(hostDeadline) &&
|
|
1705
|
+
hostDeadline.value <= now;
|
|
1706
|
+
|
|
1707
|
+
// No asynchronous operation separates the activity recheck from closure. A
|
|
1708
|
+
// racing registration either keeps this window open or cannot start its body.
|
|
1709
|
+
const closed = yield* Effect.sync(() => {
|
|
1710
|
+
if (messageDue || hostDue || !messageActivity.quiet() || !hostActivity.quiet())
|
|
1711
|
+
return false;
|
|
1712
|
+
|
|
1713
|
+
dispatchOpen = false;
|
|
1714
|
+
|
|
1715
|
+
return true;
|
|
1716
|
+
});
|
|
1717
|
+
|
|
1718
|
+
if (closed) break;
|
|
1719
|
+
yield* PubSub.publish(checks, undefined);
|
|
1720
|
+
}
|
|
1527
1721
|
const now = yield* Clock.currentTimeMillis;
|
|
1528
1722
|
const until = DateTime.toEpochMillis(yieldAfter);
|
|
1529
1723
|
|
|
@@ -1537,6 +1731,8 @@ export class ThreadMaintenance extends Context.Service<
|
|
|
1537
1731
|
|
|
1538
1732
|
// A completion racing this iteration stays armed until the loop handles it.
|
|
1539
1733
|
const recoveryDone = recoveryFinished ? Effect.never : Fiber.await(recoveryFiber);
|
|
1734
|
+
const retirementDone = retiredExit === undefined ? Fiber.await(retired) : Effect.never;
|
|
1735
|
+
const changed = activityChanged;
|
|
1540
1736
|
|
|
1541
1737
|
const ready = yield* Effect.raceFirst(
|
|
1542
1738
|
Effect.raceFirst(notified, Effect.sleep(Math.max(0, next - now))).pipe(
|
|
@@ -1544,16 +1740,17 @@ export class ThreadMaintenance extends Context.Service<
|
|
|
1544
1740
|
),
|
|
1545
1741
|
Effect.raceFirst(
|
|
1546
1742
|
recoveryDone.pipe(Effect.as("recovery" as const)),
|
|
1547
|
-
|
|
1743
|
+
Effect.raceFirst(
|
|
1744
|
+
retirementDone.pipe(Effect.as("retired" as const)),
|
|
1745
|
+
Deferred.await(changed).pipe(Effect.as("activity" as const)),
|
|
1746
|
+
),
|
|
1548
1747
|
),
|
|
1549
1748
|
);
|
|
1550
1749
|
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
if (ready === "recovery") continue;
|
|
1554
|
-
if (ready === "retired" || retired.pollUnsafe() !== undefined) break;
|
|
1555
|
-
if (!auxiliaryPending()) continue;
|
|
1750
|
+
if (ready === "recovery" || ready === "retired") continue;
|
|
1751
|
+
if (ready === "activity") activityChanged = yield* Deferred.make<void>();
|
|
1556
1752
|
if ((yield* Clock.currentTimeMillis) >= until) break;
|
|
1753
|
+
if (ready === "native") yield* PubSub.publish(checks, undefined);
|
|
1557
1754
|
|
|
1558
1755
|
const awakened = yield* beginNative(observed);
|
|
1559
1756
|
|
|
@@ -1561,10 +1758,11 @@ export class ThreadMaintenance extends Context.Service<
|
|
|
1561
1758
|
if (result.phase === "actionable") phase = "actionable";
|
|
1562
1759
|
settled += result.settled;
|
|
1563
1760
|
observed.nativeOnly = false;
|
|
1761
|
+
if (result.settled > 0) yield* PubSub.publish(checks, undefined);
|
|
1564
1762
|
}
|
|
1565
1763
|
// The original native yield deadline closes all new waves even if old recovery
|
|
1566
1764
|
// is still pending. No recovery or delivery receives a renewed event budget.
|
|
1567
|
-
yield*
|
|
1765
|
+
yield* closeDispatch;
|
|
1568
1766
|
// Preserve driver-owned Claim deadlines and failures, then close every
|
|
1569
1767
|
// listener before the one final alarm decision.
|
|
1570
1768
|
yield* Fiber.join(retired);
|
|
@@ -18,7 +18,12 @@ import {
|
|
|
18
18
|
} from "effect-agent/message-delivery";
|
|
19
19
|
import { WakeScheduler } from "effect-agent/wake-scheduler";
|
|
20
20
|
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
DurableAlarmError,
|
|
23
|
+
ThreadMaintenanceActivity,
|
|
24
|
+
ThreadMessageDelivery,
|
|
25
|
+
ThreadMutationGate,
|
|
26
|
+
} from "../Alarm.ts";
|
|
22
27
|
import { ThreadObjectPlacement } from "../CloudflareBindings.ts";
|
|
23
28
|
|
|
24
29
|
/** Every write prearms its owner; the delivery due index owns its recovery deadline. */
|
|
@@ -137,38 +142,43 @@ export const threadMessageDeliveryLayer = Layer.effectContext(
|
|
|
137
142
|
return Context.make(ThreadMessageDelivery, {
|
|
138
143
|
drainUntil: (dispatchClosed, dispatchUntil) =>
|
|
139
144
|
Effect.gen(function* () {
|
|
145
|
+
const activity = yield* ThreadMaintenanceActivity;
|
|
140
146
|
// Subscribe before the initial scan. Only admission of new waves stops;
|
|
141
147
|
// the enclosing event owns these resources until its actual teardown.
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
);
|
|
148
|
+
const hinted = yield* Stream.toPull(wakes.wakes);
|
|
149
|
+
const checked = (yield* activity.subscribeChanges).pipe(Effect.as([undefined]));
|
|
150
|
+
const notified = Effect.raceFirst(hinted, checked).pipe(Effect.catch(() => Effect.never));
|
|
145
151
|
|
|
146
152
|
const done = yield* Effect.forkScoped(dispatchClosed);
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
153
|
+
let exhausted = false;
|
|
154
|
+
|
|
155
|
+
const select = Effect.fnUntraced(function* (initial = false) {
|
|
156
|
+
if (exhausted) return;
|
|
157
|
+
const wave = yield* prepare;
|
|
158
|
+
const now = yield* Clock.currentTimeMillis;
|
|
159
|
+
|
|
160
|
+
// Recheck after local preparation: a late selection cannot start another wave once
|
|
161
|
+
// dispatch closes. Always grant the initial opportunity to a caught-up alarm.
|
|
162
|
+
if (!initial && done.pollUnsafe() !== undefined) return;
|
|
163
|
+
if (now + wave.timeoutMillis > DateTime.toEpochMillis(dispatchUntil)) {
|
|
164
|
+
// This pump has used its event opportunity. Keep the durable deadline for the
|
|
165
|
+
// next alarm without repeatedly selecting a wave that cannot fit this event.
|
|
166
|
+
exhausted = true;
|
|
167
|
+
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
yield* wave.run;
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
yield* activity.run(activity.ready.pipe(Effect.andThen(select(true))));
|
|
174
|
+
while (!exhausted && done.pollUnsafe() === undefined) {
|
|
165
175
|
const wake = yield* Effect.raceFirst(
|
|
166
176
|
notified.pipe(Effect.map(Option.some)),
|
|
167
177
|
Fiber.join(done).pipe(Effect.as(Option.none())),
|
|
168
178
|
);
|
|
169
179
|
|
|
170
180
|
if (Option.isNone(wake)) return;
|
|
171
|
-
yield* Effect.forEach(wake.value, () => select(), { discard: true });
|
|
181
|
+
yield* Effect.forEach(wake.value, () => activity.run(select()), { discard: true });
|
|
172
182
|
}
|
|
173
183
|
// No deadline sleeps: the driver finishes its one active parallel wave, including
|
|
174
184
|
// timeout/backoff commits; retained retries belong to a future physical alarm.
|