@effect-agent/platform-cloudflare 0.1.0-beta.113 → 0.1.0-beta.115

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  import { DurableObjectContext } from "./CloudflareBindings.mjs";
2
2
  import { s as CloudflareDurableRuntimeConfig } from "./CloudflareConfig-DKXGo8L3.mjs";
3
- import { Cause, Context, DateTime, Effect, Layer, Option, Schema, Scope } from "effect";
3
+ import { Cause, Context, Effect, Layer, Option, Schema, Scope } from "effect";
4
4
  import { DurableBindingFailure } from "effect-agent/agent-registration";
5
5
  import { DurableAgentRuntime, DurableRuntimeConfig, DurableWorkerFailure, RecoveryFailure } from "effect-agent/durable-agent-runtime";
6
6
  import { ThreadId } from "effect-agent/identifiers";
@@ -10,7 +10,7 @@ import { ThreadProjectionError, ThreadProjectionMaintenance } from "effect-agent
10
10
  import { WakeScheduler } from "effect-agent/wake-scheduler";
11
11
  import { SqlClient } from "effect/unstable/sql/SqlClient";
12
12
  declare namespace Alarm_d_exports {
13
- export { DurableAlarmError, DurableAlarmService, MaintenancePassFailure, MaintenancePassReport, ThreadHostMaintenance, ThreadMaintenance, ThreadMaintenanceActivity, ThreadMaintenanceFailpoint, ThreadMaintenanceFailpointHandler, ThreadMaintenanceFailpointLocation, ThreadMessageDelivery, ThreadMutationGate, ThreadPublication, ThreadPublicationService, ThreadRecoveryFault, publishCommitted };
13
+ export { DurableAlarmError, DurableAlarmService, MaintenancePassFailure, MaintenancePassReport, ThreadHostMaintenance, ThreadHostMaintenanceLane, ThreadMaintenance, ThreadMaintenanceFailpoint, ThreadMaintenanceFailpointHandler, ThreadMaintenanceFailpointLocation, ThreadMessageDelivery, ThreadMutationGate, ThreadPublication, ThreadPublicationService, ThreadRecoveryFault, publishCommitted };
14
14
  }
15
15
  declare const DurableAlarmError_base: Schema.Class<DurableAlarmError, Schema.TaggedStruct<"DurableAlarmError", {
16
16
  readonly operation: Schema.String;
@@ -114,61 +114,44 @@ declare const ThreadPublication_base: Context.ServiceClass<ThreadPublication, "@
114
114
  export declare class ThreadPublication extends ThreadPublication_base {
115
115
  static readonly layer: Layer.Layer<ThreadPublication, never, never>;
116
116
  }
117
- declare const ThreadMaintenanceActivity_base: Context.ServiceClass<ThreadMaintenanceActivity, "@effect-agent/platform-cloudflare/ThreadMaintenanceActivity", {
118
- /**
119
- * Account for one finite selection/dispatch, through joined completion or interruption.
120
- * Register before reading/claiming work. After dispatch closes the body is not started.
121
- * Host waves retain their declared allowance from this call; message Claims keep the driver's
122
- * own deadline. Waiting for notifications belongs outside this bracket.
123
- */
124
- readonly run: <R>(wave: Effect.Effect<void, DurableAlarmError, R>) => Effect.Effect<void, DurableAlarmError, R>;
125
- /** Acknowledge all initial subscriptions/scans. Composite hosts use `all` below. */
126
- readonly ready: Effect.Effect<void>;
127
- /**
128
- * Acquire an independent scoped subscription before the initial scan and `ready`.
129
- * The returned wait observes coalesced native scan hints, including those published before
130
- * its first execution. Each hint requests a current local due-work check.
131
- */
132
- readonly subscribeChanges: Effect.Effect<Effect.Effect<void>, never, Scope.Scope>;
133
- }>;
134
- /** Event-local accounting shared by the existing maintenance pumps and native scheduler. */
135
- export declare class ThreadMaintenanceActivity extends ThreadMaintenanceActivity_base {
136
- /** Join child pumps; acknowledge the parent only after every child is ready or has exited. */
137
- static readonly all: <A, E, R>(lanes: readonly Effect.Effect<A, E, R>[]) => Effect.Effect<readonly A[], E, R | ThreadMaintenanceActivity>;
138
- }
139
117
  /**
140
- * Host-assembled native message recovery. The driver bounds each actual Claim and persists its
141
- * timeout/retry before this pump returns. Do not add a second timer starting at batch selection:
142
- * local Claim setup may take time, and expiration still owes the driver's local retry commit.
118
+ * Host-assembled native message recovery. Preparation is a bounded local selection; the
119
+ * driver bounds each actual Claim and persists its timeout/retry before `run` returns.
120
+ * The alarm must not add a timer starting at selection: Claim setup and retry commits belong
121
+ * to the driver. The prepared allowance decides only whether a wave fits this event.
143
122
  */
144
123
  export declare const ThreadMessageDelivery: Context.Reference<{
145
- readonly drainUntil: (dispatchClosed: Effect.Effect<void>, dispatchUntil: DateTime.Utc) => Effect.Effect<void, DurableAlarmError, Scope.Scope | ThreadMaintenanceActivity>;
124
+ readonly prepare: Effect.Effect<{
125
+ readonly timeoutMillis: number;
126
+ readonly run: Effect.Effect<void, DurableAlarmError>;
127
+ }, DurableAlarmError>;
146
128
  readonly pendingDeadline: Effect.Effect<Option.Option<number>, DurableAlarmError>;
147
129
  }>;
148
130
  /**
149
- * Application obligations sharing this Object's alarm. Admit one initial external wave even on
150
- * a caught-up pass, then respond to wakes and ThreadMaintenanceActivity.subscribeChanges until
151
- * dispatchClosed. Yield the event's ThreadMaintenanceActivity to bracket each finite lane with
152
- * run and acknowledge initial setup with ready.
153
- * Passive subscriptions do not count as active work. Closure stops both new waves and native
154
- * Attempts; local admission/control subscriptions belong to the event Scope until teardown.
155
- * No deadline sleeps or automatic retry loops. Return after already-admitted waves finish.
131
+ * One independently schedulable application obligation. `run` selects and joins one finite
132
+ * wave, including its retry/receipt commits. The alarm gives every lane an initial opportunity,
133
+ * then checks its local pending deadline on completion, native progress, wakes and bounded scans.
134
+ * No subscriptions, readiness acknowledgements, deadline sleeps or dispatch loops are needed.
156
135
  *
157
- * Declare a finite whole-wave allowance (1..300000ms): maximum for parallel lanes, sum for
158
- * sequential operations. Admit a wave only if its allowance fits before dispatchUntil. Later
159
- * arrivals cannot renew an admitted wave's allowance. Maintenance bounds each registered wave,
160
- * interrupts and joins event Scope, then reads local deadlines under the mutation gate.
136
+ * Declare a whole-wave allowance (1..300000ms), including selection, local commits and cleanup. A wave
137
+ * starts only if that allowance fits the event and later arrivals cannot renew it. A failed
138
+ * lane is not retried within the event. The alarm joins admitted work and closes its Scope
139
+ * before reading durable deadlines under the shared ThreadMutationGate.
161
140
  *
162
- * Setup and pendingDeadline are bounded local operations. Persist claims/envelopes before
163
- * dispatch; interrupted waits leave exact retries/receipts recoverable. Cancellation is local,
164
- * not remote rollback. Retry accounting is unchanged. Network waits/finalizers must be
165
- * interruptible; short local atomic commits may be uninterruptible. Hooks never write the raw
166
- * alarm slot. Required native publication gates belong to ThreadPublication.
141
+ * `pendingDeadline` is bounded local control state, never a retained-history scan. Persist
142
+ * envelopes/claims before dispatch; cancellation is local, not remote rollback. Network waits
143
+ * and finalizers must be interruptible; short atomic commits may be uninterruptible. Mutations
144
+ * use the shared gate and producers notify the existing WakeScheduler after commit. Hooks never
145
+ * write the raw alarm slot. Required native publication gates belong to ThreadPublication.
167
146
  */
168
- export declare const ThreadHostMaintenance: Context.Reference<{
147
+ export interface ThreadHostMaintenanceLane {
169
148
  readonly dispatchTimeoutMillis: number;
170
- readonly drainUntil: (dispatchClosed: Effect.Effect<void>, dispatchUntil: DateTime.Utc) => Effect.Effect<void, DurableAlarmError, Scope.Scope | ThreadMaintenanceActivity>;
149
+ readonly run: Effect.Effect<void, DurableAlarmError, Scope.Scope>;
171
150
  readonly pendingDeadline: Effect.Effect<Option.Option<number>, DurableAlarmError>;
151
+ }
152
+ /** Independent lanes share the existing alarm; compose hosts by concatenating their lanes. */
153
+ export declare const ThreadHostMaintenance: Context.Reference<{
154
+ readonly lanes: ReadonlyArray<ThreadHostMaintenanceLane>;
172
155
  }>;
173
156
  /** @internal A committed source operation must not become a failed operation because delivery failed. */
174
157
  export declare const publishCommitted: Effect.Effect<void, never, ThreadPublication>;
@@ -240,10 +223,11 @@ declare const ThreadMaintenance_base: Context.ServiceClass<ThreadMaintenance, "@
240
223
  * 1. Prearm before any work. A caught-up native step uses the O(1) generation record without
241
224
  * recovery, ledger scans or canonical-history reads.
242
225
  * 2. Reconcile before each head Attempt, then checkpoint only the observed generation. A racing
243
- * producer keeps its newer generation dirty. Native retries retain their durable backoff.
226
+ * producer keeps its generation dirty and immediately eligible. Quiescent native retries
227
+ * retain their durable backoff.
244
228
  * 3. Keep native and delivery admission open together while finite waves remain active, so
245
229
  * fresh replies and abort controls can progress during unrelated cleanup. Close atomically
246
- * at quiescence, or at the original ten-minute yield deadline, before retiring listeners.
230
+ * at quiescence, or at the original ten-minute yield deadline, then join admitted waves.
247
231
  * 4. Native message delivery retains its driver-owned Claim deadline. Host/backfill waves are
248
232
  * bounded independently; incoming native work never restarts or cancels their attempts.
249
233
  * Auxiliary failures are reported after the current native opportunity.
package/dist/Alarm.mjs CHANGED
@@ -2,7 +2,7 @@ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
2
  import { DurableObjectContext } from "./CloudflareBindings.mjs";
3
3
  import { AuxiliaryDispatchMillis, CloudflareDurableRuntimeConfig } from "./CloudflareConfig.mjs";
4
4
  import { r as safeCauseMessage } from "./boundary-BguazVkh.mjs";
5
- import { Cause, Clock, Context, DateTime, Deferred, Effect, Exit, Fiber, Layer, Option, PubSub, Random, Ref, Result, Schema, Scope, Semaphore, Stream, Struct } from "effect";
5
+ import { Cause, Clock, Context, DateTime, Deferred, Effect, Exit, Fiber, Layer, Option, Random, Ref, Result, Schema, Scope, Semaphore, Stream, Struct } from "effect";
6
6
  import "effect-agent/agent-registration";
7
7
  import { DurableAgentRuntime, DurableRuntimeConfig, RecoveryBlocked, RecoveryFailure, RecoverySweepResult, isolateRecovery } from "effect-agent/durable-agent-runtime";
8
8
  import { SubmissionId, ThreadId } from "effect-agent/identifiers";
@@ -18,7 +18,6 @@ var Alarm_exports = /* @__PURE__ */ __exportAll({
18
18
  MaintenancePassReport: () => MaintenancePassReport,
19
19
  ThreadHostMaintenance: () => ThreadHostMaintenance,
20
20
  ThreadMaintenance: () => ThreadMaintenance,
21
- ThreadMaintenanceActivity: () => ThreadMaintenanceActivity,
22
21
  ThreadMaintenanceFailpoint: () => ThreadMaintenanceFailpoint,
23
22
  ThreadMessageDelivery: () => ThreadMessageDelivery,
24
23
  ThreadMutationGate: () => ThreadMutationGate,
@@ -112,61 +111,21 @@ var ThreadPublication = class extends Context.Service()("@effect-agent/platform-
112
111
  pendingDeadline: Effect.succeed(Option.none())
113
112
  });
114
113
  };
115
- /** Event-local accounting shared by the existing maintenance pumps and native scheduler. */
116
- var ThreadMaintenanceActivity = class ThreadMaintenanceActivity extends Context.Service()("@effect-agent/platform-cloudflare/ThreadMaintenanceActivity") {
117
- /** Join child pumps; acknowledge the parent only after every child is ready or has exited. */
118
- static all = Effect.fnUntraced(function* (lanes) {
119
- const activity = yield* ThreadMaintenanceActivity;
120
- let remaining = lanes.length;
121
- if (remaining === 0) return yield* activity.ready.pipe(Effect.as([]));
122
- return yield* Effect.forEach(lanes, (lane) => {
123
- let ready = false;
124
- const child = {
125
- ...activity,
126
- ready: Effect.suspend(() => {
127
- if (ready) return Effect.void;
128
- ready = true;
129
- return --remaining === 0 ? activity.ready : Effect.void;
130
- }).pipe(Effect.uninterruptible)
131
- };
132
- return lane.pipe(Effect.provideService(ThreadMaintenanceActivity, child), Effect.onExit(() => child.ready));
133
- }, { concurrency: "unbounded" });
134
- });
135
- };
136
114
  /**
137
- * Host-assembled native message recovery. The driver bounds each actual Claim and persists its
138
- * timeout/retry before this pump returns. Do not add a second timer starting at batch selection:
139
- * local Claim setup may take time, and expiration still owes the driver's local retry commit.
115
+ * Host-assembled native message recovery. Preparation is a bounded local selection; the
116
+ * driver bounds each actual Claim and persists its timeout/retry before `run` returns.
117
+ * The alarm must not add a timer starting at selection: Claim setup and retry commits belong
118
+ * to the driver. The prepared allowance decides only whether a wave fits this event.
140
119
  */
141
120
  const ThreadMessageDelivery = Context.Reference("@effect-agent/platform-cloudflare/ThreadMessageDelivery", { defaultValue: () => ({
142
- drainUntil: () => Effect.void,
143
- pendingDeadline: Effect.succeed(Option.none())
144
- }) });
145
- /**
146
- * Application obligations sharing this Object's alarm. Admit one initial external wave even on
147
- * a caught-up pass, then respond to wakes and ThreadMaintenanceActivity.subscribeChanges until
148
- * dispatchClosed. Yield the event's ThreadMaintenanceActivity to bracket each finite lane with
149
- * run and acknowledge initial setup with ready.
150
- * Passive subscriptions do not count as active work. Closure stops both new waves and native
151
- * Attempts; local admission/control subscriptions belong to the event Scope until teardown.
152
- * No deadline sleeps or automatic retry loops. Return after already-admitted waves finish.
153
- *
154
- * Declare a finite whole-wave allowance (1..300000ms): maximum for parallel lanes, sum for
155
- * sequential operations. Admit a wave only if its allowance fits before dispatchUntil. Later
156
- * arrivals cannot renew an admitted wave's allowance. Maintenance bounds each registered wave,
157
- * interrupts and joins event Scope, then reads local deadlines under the mutation gate.
158
- *
159
- * Setup and pendingDeadline are bounded local operations. Persist claims/envelopes before
160
- * dispatch; interrupted waits leave exact retries/receipts recoverable. Cancellation is local,
161
- * not remote rollback. Retry accounting is unchanged. Network waits/finalizers must be
162
- * interruptible; short local atomic commits may be uninterruptible. Hooks never write the raw
163
- * alarm slot. Required native publication gates belong to ThreadPublication.
164
- */
165
- const ThreadHostMaintenance = Context.Reference("@effect-agent/platform-cloudflare/ThreadHostMaintenance", { defaultValue: () => ({
166
- dispatchTimeoutMillis: 1,
167
- drainUntil: () => Effect.void,
121
+ prepare: Effect.succeed({
122
+ timeoutMillis: 1,
123
+ run: Effect.void
124
+ }),
168
125
  pendingDeadline: Effect.succeed(Option.none())
169
126
  }) });
127
+ /** Independent lanes share the existing alarm; compose hosts by concatenating their lanes. */
128
+ const ThreadHostMaintenance = Context.Reference("@effect-agent/platform-cloudflare/ThreadHostMaintenance", { defaultValue: () => ({ lanes: [] }) });
170
129
  const earliestDeadline = (left, right) => Option.isSome(left) ? Option.isSome(right) ? Option.some(Math.min(left.value, right.value)) : left : right;
171
130
  /** @internal A committed source operation must not become a failed operation because delivery failed. */
172
131
  const publishCommitted = Effect.gen(function* () {
@@ -309,10 +268,11 @@ var ThreadMutationGate = class ThreadMutationGate extends Context.Service()("@ef
309
268
  * 1. Prearm before any work. A caught-up native step uses the O(1) generation record without
310
269
  * recovery, ledger scans or canonical-history reads.
311
270
  * 2. Reconcile before each head Attempt, then checkpoint only the observed generation. A racing
312
- * producer keeps its newer generation dirty. Native retries retain their durable backoff.
271
+ * producer keeps its generation dirty and immediately eligible. Quiescent native retries
272
+ * retain their durable backoff.
313
273
  * 3. Keep native and delivery admission open together while finite waves remain active, so
314
274
  * fresh replies and abort controls can progress during unrelated cleanup. Close atomically
315
- * at quiescence, or at the original ten-minute yield deadline, before retiring listeners.
275
+ * at quiescence, or at the original ten-minute yield deadline, then join admitted waves.
316
276
  * 4. Native message delivery retains its driver-owned Claim deadline. Host/backfill waves are
317
277
  * bounded independently; incoming native work never restarts or cancels their attempts.
318
278
  * Auxiliary failures are reported after the current native opportunity.
@@ -337,7 +297,7 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
337
297
  const authorizer = yield* OperationAuthorizer;
338
298
  const projectionDeadline = projection.pendingDeadline.pipe(Effect.catchCauseIf((cause) => !Cause.hasInterrupts(cause), (cause) => Effect.logError("Thread projection deadline unavailable", cause).pipe(Effect.as(Option.some(0)))));
339
299
  const pendingDeadline = Effect.gen(function* () {
340
- return earliestDeadline(earliestDeadline(yield* publication.pendingDeadline, yield* messages.pendingDeadline), earliestDeadline(yield* projectionDeadline, yield* host.pendingDeadline));
300
+ return earliestDeadline(earliestDeadline(yield* publication.pendingDeadline, yield* messages.pendingDeadline), earliestDeadline(yield* projectionDeadline, (yield* Effect.forEach(host.lanes, (lane) => lane.pendingDeadline)).reduce(earliestDeadline, Option.none())));
341
301
  });
342
302
  const maintenancePassGate = yield* Semaphore.make(1);
343
303
  const minimumAlarmDelay = Math.max(1, Math.ceil(config.alarmBackoffBase / 2));
@@ -632,13 +592,15 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
632
592
  yield* failpoint.hit("maintenance:checkpoint:before");
633
593
  const nextAttemptAt = yield* mutations.withSnapshot((active) => runTransaction("checkpoint native maintenance", () => ctx.storage.transaction(async (transaction) => {
634
594
  const { state } = await readMaintenanceState(transaction);
635
- const processed = autonomous || observation.activeAtStart > 0 || started.activeAtStart > 0 || active > 0 ? state.processed : state.processed > observation.generation ? state.processed : observation.generation;
595
+ const mutationOverlap = observation.activeAtStart > 0 || started.activeAtStart > 0 || active > 0;
596
+ const processed = autonomous || mutationOverlap ? state.processed : state.processed > observation.generation ? state.processed : observation.generation;
597
+ const canBackoff = !mutationOverlap && state.dirty === started.generation;
636
598
  const next = ThreadMaintenanceState.make({
637
599
  ...Struct.omit(state, ["retry"]),
638
600
  processed,
639
601
  nonterminal: remaining.length,
640
602
  bindingRetries: (state.bindingRetries ?? []).filter((retry) => remaining.some((row) => row.submissionId === retry.submissionId)),
641
- ...autonomous && !progressed ? { retry: MaintenanceRetry.make({
603
+ ...autonomous && !progressed && canBackoff ? { retry: MaintenanceRetry.make({
642
604
  generation: started.generation,
643
605
  notBefore: now + delay,
644
606
  nativeOnly: true,
@@ -646,8 +608,8 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
646
608
  }) } : {}
647
609
  });
648
610
  await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(next));
649
- if (autonomous) return started.activeAtStart > 0 || active > 0 || state.dirty !== started.generation ? now + minimumAlarmDelay : now + delay;
650
- return started.activeAtStart > 0 || active > 0 || next.dirty > next.processed ? now + minimumAlarmDelay : void 0;
611
+ if (autonomous) return now + (canBackoff ? delay : minimumAlarmDelay);
612
+ return mutationOverlap || next.dirty > next.processed ? now + minimumAlarmDelay : void 0;
651
613
  })));
652
614
  yield* failpoint.hit("maintenance:checkpoint:after");
653
615
  return {
@@ -661,47 +623,17 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
661
623
  const notified = (yield* Stream.toPull(wakes.wakes)).pipe(Effect.asVoid, Effect.catch(() => Effect.never));
662
624
  const started = yield* beginNative(observed);
663
625
  const auxiliaryScope = yield* Effect.acquireRelease(Scope.make("parallel"), (scope, exit) => Scope.close(scope, exit));
664
- const dispatchClosed = yield* Deferred.make();
665
- const stopDispatch = Deferred.await(dispatchClosed);
666
- const checks = yield* PubSub.sliding(1);
667
- yield* Effect.addFinalizer(() => PubSub.shutdown(checks));
668
- let dispatchOpen = true;
669
- let activityChanged = yield* Deferred.make();
670
- const signalActivity = Effect.suspend(() => Deferred.succeed(activityChanged, void 0));
671
- const closeDispatch = Effect.sync(() => {
672
- dispatchOpen = false;
673
- }).pipe(Effect.andThen(Deferred.succeed(dispatchClosed, void 0)));
674
- const makeActivity = Effect.fnUntraced(function* (allowance) {
675
- const initialized = yield* Deferred.make();
676
- let active = 0;
677
- let failed = false;
678
- return {
679
- activity: {
680
- ready: Deferred.succeed(initialized, void 0).pipe(Effect.andThen(signalActivity), Effect.asVoid, Effect.uninterruptible),
681
- subscribeChanges: PubSub.subscribe(checks).pipe(Effect.map(PubSub.take)),
682
- run: (wave) => Effect.acquireUseRelease(Effect.sync(() => {
683
- if (!dispatchOpen) return false;
684
- active++;
685
- return true;
686
- }), (admitted) => !admitted ? Effect.void : (allowance === void 0 ? wave : wave.pipe(Effect.timeoutOrElse({
687
- duration: allowance,
688
- orElse: () => DurableAlarmError.make({
689
- operation: "host dispatch allowance",
690
- message: "The admitted host wave exceeded its allowance; durable work remains pending"
691
- })
692
- }))).pipe(Effect.onExit((exit) => Effect.sync(() => {
693
- if (Exit.isFailure(exit)) failed = true;
694
- }))), (admitted) => Effect.sync(() => {
695
- if (admitted) active--;
696
- }).pipe(Effect.andThen(signalActivity)))
697
- },
698
- initialized: Deferred.await(initialized),
699
- quiet: () => active === 0 && Deferred.isDoneUnsafe(initialized),
700
- failed: () => failed
701
- };
702
- });
703
- const messageActivity = yield* makeActivity();
704
- const hostActivity = yield* makeActivity(host.dispatchTimeoutMillis);
626
+ const fork = (work) => Effect.forkIn(work, auxiliaryScope);
627
+ const lanes = host.lanes.map((lane) => ({
628
+ ...lane,
629
+ initial: true,
630
+ check: true,
631
+ exhausted: false,
632
+ fiber: void 0
633
+ }));
634
+ let messageExhausted = false;
635
+ let delivery;
636
+ let failure;
705
637
  const recovery = {
706
638
  queue: yield* Deferred.make(),
707
639
  pending: /* @__PURE__ */ new Set(),
@@ -713,7 +645,7 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
713
645
  recovered: 0,
714
646
  repaired: false
715
647
  };
716
- const recoveryFiber = yield* Effect.forkIn(Effect.gen(function* () {
648
+ const recoveryFiber = yield* fork(Effect.gen(function* () {
717
649
  for (const threadId of yield* Deferred.await(recovery.queue)) {
718
650
  yield* failpoint.hit("maintenance:select:before");
719
651
  yield* runTransaction("select old recovery lane", () => ctx.storage.transaction(async (transaction) => {
@@ -728,90 +660,156 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
728
660
  recovery.pending.delete(threadId);
729
661
  yield* wakes.notify(threadId);
730
662
  }
731
- }), auxiliaryScope);
732
- const deliveryFiber = yield* Effect.forkIn(Scope.provide(auxiliaryScope)(messages.drainUntil(stopDispatch, dispatchUntil).pipe(Effect.provideService(ThreadMaintenanceActivity, messageActivity.activity), Effect.onExit(() => messageActivity.activity.ready))), auxiliaryScope);
733
- const hostFiber = yield* Effect.forkIn(Scope.provide(auxiliaryScope)(Effect.gen(function* () {
734
- yield* Schema.decodeEffect(AuxiliaryDispatchMillis)(host.dispatchTimeoutMillis).pipe(Effect.mapError((cause) => DurableAlarmError.make({
735
- operation: "host dispatch allowance",
736
- message: "Declare an integer whole-wave allowance between 1 and 300000 milliseconds",
737
- cause
738
- })));
739
- const initialization = hostActivity.initialized.pipe(Effect.timeoutOrElse({
740
- duration: host.dispatchTimeoutMillis,
741
- orElse: () => DurableAlarmError.make({
742
- operation: "host initialization",
743
- message: "The host did not account for initial maintenance before its allowance; durable work remains pending"
744
- })
745
- }), Effect.andThen(Effect.never));
746
- yield* Effect.raceFirst(host.drainUntil(stopDispatch, dispatchUntil).pipe(Effect.provideService(ThreadMaintenanceActivity, hostActivity.activity)), initialization);
747
- }).pipe(Effect.onExit(() => hostActivity.activity.ready))), auxiliaryScope);
748
- const backfill = yield* Effect.forkIn(drainDue.pipe(Effect.provideService(ThreadProjectionMaintenance, projection), Effect.timeoutOption(config.projectionDispatchTimeoutMillis), Effect.onExit(() => signalActivity)), auxiliaryScope);
749
- let result = yield* advance(started, yieldAfter, observed, recovery);
750
- recovery.started = true;
751
- yield* Deferred.succeed(recovery.queue, []);
663
+ }));
664
+ const backfill = yield* fork(drainDue.pipe(Effect.provideService(ThreadProjectionMaintenance, projection), Effect.timeoutOption(config.projectionDispatchTimeoutMillis)));
665
+ let backfillObserved = false;
666
+ let recoveryObserved = false;
667
+ let native = yield* fork(advance(started, yieldAfter, observed, recovery));
668
+ let result = {
669
+ phase: "caught-up",
670
+ settled: 0,
671
+ nonterminal: started.nonterminal,
672
+ nextAttemptAt: void 0
673
+ };
752
674
  let phase = result.phase;
753
- let settled = result.settled;
754
- observed.nativeOnly = false;
755
- const hostJoin = yield* Effect.forkIn(Effect.gen(function* () {
756
- yield* stopDispatch;
757
- const remaining = Math.max(1, DateTime.toEpochMillis(dispatchUntil) - (yield* Clock.currentTimeMillis));
758
- const outcome = yield* Fiber.join(hostFiber).pipe(Effect.timeoutOption(Math.min(host.dispatchTimeoutMillis, remaining)));
759
- yield* Effect.annotateCurrentSpan({ "host.timedOut": Option.isNone(outcome) });
760
- }), auxiliaryScope);
761
- const retired = yield* Effect.forkChild(Fiber.joinAll([
762
- deliveryFiber,
763
- hostJoin,
764
- backfill,
765
- recoveryFiber
766
- ]));
675
+ let settled = 0;
676
+ let nativeCheck = false;
677
+ const until = DateTime.toEpochMillis(yieldAfter);
678
+ const dispatchEnd = DateTime.toEpochMillis(dispatchUntil);
679
+ const checkLanes = () => {
680
+ for (const lane of lanes) lane.check = true;
681
+ };
767
682
  while (true) {
768
- const recoveryFinished = recoveryFiber.pollUnsafe() !== void 0;
769
- const retiredExit = retired.pollUnsafe();
770
- if (recoveryFinished) {
771
- if (recovery.needsCheckpoint) {
772
- yield* Fiber.join(recoveryFiber);
773
- result = yield* advance(started, yieldAfter, observed, recovery, settled === 0);
774
- if (result.phase === "actionable") phase = "actionable";
775
- settled += result.settled;
776
- recovery.needsCheckpoint = false;
777
- yield* PubSub.publish(checks, void 0);
683
+ if (native?.pollUnsafe() !== void 0) {
684
+ result = yield* Fiber.join(native);
685
+ native = void 0;
686
+ if (result.phase === "actionable") phase = "actionable";
687
+ settled += result.settled;
688
+ observed.nativeOnly = false;
689
+ recovery.started = true;
690
+ yield* Deferred.succeed(recovery.queue, []);
691
+ checkLanes();
692
+ }
693
+ if (!recoveryObserved && recoveryFiber.pollUnsafe() !== void 0) {
694
+ yield* Fiber.join(recoveryFiber);
695
+ recoveryObserved = true;
696
+ nativeCheck = recovery.needsCheckpoint;
697
+ }
698
+ if (!backfillObserved) {
699
+ const exit = backfill.pollUnsafe();
700
+ if (exit !== void 0) {
701
+ backfillObserved = true;
702
+ if (Exit.isFailure(exit)) failure ??= exit.cause;
778
703
  }
779
704
  }
780
- if (retiredExit !== void 0 && Exit.isFailure(retiredExit)) break;
781
- if (recoveryFinished && backfill.pollUnsafe() !== void 0 && messageActivity.quiet() && hostActivity.quiet()) {
782
- const now = yield* Clock.currentTimeMillis;
783
- const messageDeadline = yield* messages.pendingDeadline;
784
- const hostDeadline = yield* host.pendingDeadline;
785
- const messageDue = deliveryFiber.pollUnsafe() === void 0 && !messageActivity.failed() && Option.isSome(messageDeadline) && messageDeadline.value <= now;
786
- const hostDue = hostFiber.pollUnsafe() === void 0 && !hostActivity.failed() && now + host.dispatchTimeoutMillis <= DateTime.toEpochMillis(dispatchUntil) && Option.isSome(hostDeadline) && hostDeadline.value <= now;
787
- if (yield* Effect.sync(() => {
788
- if (messageDue || hostDue || !messageActivity.quiet() || !hostActivity.quiet()) return false;
789
- dispatchOpen = false;
790
- return true;
791
- })) break;
792
- yield* PubSub.publish(checks, void 0);
705
+ for (const lane of lanes) {
706
+ const exit = lane.fiber?.pollUnsafe();
707
+ if (exit === void 0) continue;
708
+ lane.fiber = void 0;
709
+ if (Exit.isFailure(exit)) {
710
+ failure ??= exit.cause;
711
+ lane.exhausted = true;
712
+ } else lane.check ||= exit.value;
713
+ }
714
+ const deliveryExit = delivery?.pollUnsafe();
715
+ if (deliveryExit !== void 0) {
716
+ delivery = void 0;
717
+ if (Exit.isFailure(deliveryExit)) {
718
+ failure ??= deliveryExit.cause;
719
+ messageExhausted = true;
720
+ }
793
721
  }
722
+ if (failure !== void 0 && native === void 0) return yield* Effect.failCause(failure);
794
723
  const now = yield* Clock.currentTimeMillis;
795
- const until = DateTime.toEpochMillis(yieldAfter);
796
724
  if (now >= until) break;
797
- const next = Math.min(result.nextAttemptAt ?? Infinity, now + config.wakeScanInterval, until);
798
- const recoveryDone = recoveryFinished ? Effect.never : Fiber.await(recoveryFiber);
799
- const retirementDone = retiredExit === void 0 ? Fiber.await(retired) : Effect.never;
800
- const changed = activityChanged;
801
- const ready = yield* Effect.raceFirst(Effect.raceFirst(notified, Effect.sleep(Math.max(0, next - now))).pipe(Effect.as("native")), Effect.raceFirst(recoveryDone.pipe(Effect.as("recovery")), Effect.raceFirst(retirementDone.pipe(Effect.as("retired")), Deferred.await(changed).pipe(Effect.as("activity")))));
802
- if (ready === "recovery" || ready === "retired") continue;
803
- if (ready === "activity") activityChanged = yield* Deferred.make();
804
- if ((yield* Clock.currentTimeMillis) >= until) break;
805
- if (ready === "native") yield* PubSub.publish(checks, void 0);
806
- const awakened = yield* beginNative(observed);
807
- result = yield* advance(awakened, yieldAfter, observed, recovery);
725
+ for (const lane of lanes) {
726
+ if (lane.fiber !== void 0 || lane.exhausted || !lane.check) continue;
727
+ lane.check = false;
728
+ lane.fiber = yield* fork(Effect.gen(function* () {
729
+ yield* Schema.decodeEffect(AuxiliaryDispatchMillis)(lane.dispatchTimeoutMillis).pipe(Effect.mapError((cause) => DurableAlarmError.make({
730
+ operation: "host dispatch allowance",
731
+ message: "Declare an integer whole-wave allowance between 1 and 300000 milliseconds",
732
+ cause
733
+ })));
734
+ if ((yield* Clock.currentTimeMillis) + lane.dispatchTimeoutMillis > dispatchEnd) {
735
+ lane.exhausted = true;
736
+ return false;
737
+ }
738
+ return yield* Effect.gen(function* () {
739
+ if (!lane.initial) {
740
+ const deadline = yield* lane.pendingDeadline;
741
+ if (Option.isNone(deadline) || deadline.value > (yield* Clock.currentTimeMillis)) return false;
742
+ }
743
+ const selectedAt = yield* Clock.currentTimeMillis;
744
+ if (selectedAt >= until || selectedAt + lane.dispatchTimeoutMillis > dispatchEnd) {
745
+ lane.exhausted = true;
746
+ return false;
747
+ }
748
+ lane.initial = false;
749
+ yield* lane.run;
750
+ return true;
751
+ }).pipe(Effect.scoped, Effect.timeoutOrElse({
752
+ duration: lane.dispatchTimeoutMillis,
753
+ orElse: () => DurableAlarmError.make({
754
+ operation: "host dispatch allowance",
755
+ message: "The admitted host wave exceeded its allowance; durable work remains pending"
756
+ })
757
+ }));
758
+ }));
759
+ }
760
+ if (delivery === void 0 && !messageExhausted) {
761
+ const selected = yield* Effect.gen(function* () {
762
+ const deadline = yield* messages.pendingDeadline;
763
+ return Option.isSome(deadline) && deadline.value <= (yield* Clock.currentTimeMillis) ? Option.some(yield* messages.prepare) : Option.none();
764
+ }).pipe(Effect.exit);
765
+ if (Exit.isFailure(selected)) {
766
+ failure ??= selected.cause;
767
+ messageExhausted = true;
768
+ } else if (Option.isSome(selected.value)) {
769
+ const wave = selected.value.value;
770
+ const selectedAt = yield* Clock.currentTimeMillis;
771
+ if (selectedAt >= until || selectedAt + wave.timeoutMillis > dispatchEnd) messageExhausted = true;
772
+ else delivery = yield* fork(wave.run);
773
+ }
774
+ }
775
+ if (native === void 0 && recoveryObserved && !recovery.needsCheckpoint && backfillObserved && delivery === void 0 && lanes.every((lane) => lane.fiber === void 0)) break;
776
+ if (native === void 0 && nativeCheck) {
777
+ nativeCheck = false;
778
+ const checkpoint = recoveryObserved && recovery.needsCheckpoint;
779
+ if (checkpoint) recovery.needsCheckpoint = false;
780
+ native = yield* fork(Effect.gen(function* () {
781
+ const awakened = checkpoint ? started : yield* beginNative(observed);
782
+ return yield* advance(awakened, yieldAfter, observed, recovery, !checkpoint || settled === 0);
783
+ }));
784
+ }
785
+ const next = Math.min(native === void 0 ? result.nextAttemptAt ?? Infinity : Infinity, now + config.wakeScanInterval, until);
786
+ const completing = [
787
+ ...native === void 0 ? [] : [native],
788
+ ...recoveryObserved ? [] : [recoveryFiber],
789
+ ...backfillObserved ? [] : [backfill],
790
+ ...delivery === void 0 ? [] : [delivery],
791
+ ...lanes.flatMap((lane) => lane.fiber === void 0 ? [] : [lane.fiber])
792
+ ];
793
+ if ((yield* Effect.raceAllFirst([...completing.map((fiber) => Fiber.await(fiber).pipe(Effect.as("completed"))), Effect.raceFirst(notified, Effect.sleep(Math.max(0, next - now))).pipe(Effect.as("scan"))])) === "scan") {
794
+ nativeCheck = true;
795
+ checkLanes();
796
+ }
797
+ }
798
+ if (native !== void 0) {
799
+ result = yield* Fiber.join(native);
808
800
  if (result.phase === "actionable") phase = "actionable";
809
801
  settled += result.settled;
810
802
  observed.nativeOnly = false;
811
- if (result.settled > 0) yield* PubSub.publish(checks, void 0);
803
+ recovery.started = true;
804
+ yield* Deferred.succeed(recovery.queue, []);
812
805
  }
813
- yield* closeDispatch;
814
- yield* Fiber.join(retired);
806
+ yield* Fiber.joinAll([
807
+ recoveryFiber,
808
+ backfill,
809
+ ...delivery === void 0 ? [] : [delivery],
810
+ ...lanes.flatMap((lane) => lane.fiber === void 0 ? [] : [lane.fiber])
811
+ ]);
812
+ if (failure !== void 0) return yield* Effect.failCause(failure);
815
813
  if (recovery.needsCheckpoint) {
816
814
  result = yield* advance(started, yieldAfter, observed, recovery, false);
817
815
  if (result.phase === "actionable") phase = "actionable";
@@ -872,6 +870,6 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
872
870
  }));
873
871
  };
874
872
  //#endregion
875
- export { DurableAlarmError, DurableAlarmService, MaintenancePassReport, ThreadHostMaintenance, ThreadMaintenance, ThreadMaintenanceActivity, ThreadMaintenanceFailpoint, ThreadMessageDelivery, ThreadMutationGate, ThreadPublication, ThreadRecoveryFault, publishCommitted, Alarm_exports as t };
873
+ export { DurableAlarmError, DurableAlarmService, MaintenancePassReport, ThreadHostMaintenance, ThreadMaintenance, ThreadMaintenanceFailpoint, ThreadMessageDelivery, ThreadMutationGate, ThreadPublication, ThreadRecoveryFault, publishCommitted, Alarm_exports as t };
876
874
 
877
875
  //# sourceMappingURL=Alarm.mjs.map