@yielded/agent-platform-cloudflare 0.1.0-beta.174 → 0.1.0-beta.175
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-4J9B8Fg8.mjs → Alarm-B44efvB1.mjs} +111 -74
- package/dist/Alarm-B44efvB1.mjs.map +1 -0
- package/dist/Alarm.d.mts +2 -0
- package/dist/Alarm.mjs +1 -1
- package/dist/CloudflareScheduling.mjs +2 -2
- package/dist/CloudflareSubscriptions.mjs +2 -2
- package/dist/{CloudflareThreadClient-DU548EEj.mjs → CloudflareThreadClient-Ai3MTs3X.mjs} +4 -4
- package/dist/CloudflareThreadClient-Ai3MTs3X.mjs.map +1 -0
- package/dist/{CloudflareThreadClient-lTm6baDP.d.mts → CloudflareThreadClient-HO_3rkM7.d.mts} +362 -362
- package/dist/CloudflareThreadClient.d.mts +1 -1
- package/dist/CloudflareThreadClient.mjs +1 -1
- package/dist/{ThreadObject-BLYgzcdT.d.mts → ThreadObject-CxNlhoPL.d.mts} +13 -13
- package/dist/{ThreadObject-C2LF3btz.mjs → ThreadObject-DGvlHWYi.mjs} +42 -43
- package/dist/ThreadObject-DGvlHWYi.mjs.map +1 -0
- package/dist/ThreadObject.d.mts +1 -1
- package/dist/ThreadObject.mjs +1 -1
- package/dist/WakeScheduler.mjs +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +3 -3
- package/dist/{prepared-admission-BoUPHSNQ.mjs → prepared-admission-DYdffQ8K.mjs} +2 -2
- package/dist/{prepared-admission-BoUPHSNQ.mjs.map → prepared-admission-DYdffQ8K.mjs.map} +1 -1
- package/package.json +1 -1
- package/src/Alarm.ts +211 -116
- package/src/CloudflareThreadClient.ts +3 -2
- package/src/ThreadObject.ts +17 -22
- package/src/internal/layers.ts +55 -34
- package/src/internal/message-delivery.ts +61 -37
- package/dist/Alarm-4J9B8Fg8.mjs.map +0 -1
- package/dist/CloudflareThreadClient-DU548EEj.mjs.map +0 -1
- package/dist/ThreadObject-C2LF3btz.mjs.map +0 -1
package/src/Alarm.ts
CHANGED
|
@@ -734,6 +734,25 @@ interface NativeDispatch {
|
|
|
734
734
|
scanRevision?: number;
|
|
735
735
|
}
|
|
736
736
|
|
|
737
|
+
/** Observe already-owned waves without creating one waiter fiber for each wave. */
|
|
738
|
+
const awaitAnyCompletion = Effect.fnUntraced(function* (
|
|
739
|
+
fibers: ReadonlyArray<Fiber.Fiber<unknown, MaintenancePassFailure>>,
|
|
740
|
+
revision: number,
|
|
741
|
+
) {
|
|
742
|
+
const mutations = yield* ThreadMutationGate;
|
|
743
|
+
|
|
744
|
+
return yield* Effect.acquireUseRelease(
|
|
745
|
+
Effect.sync<Array<() => void>>(() => []),
|
|
746
|
+
(observers) =>
|
|
747
|
+
Effect.callback<"completed" | "scan">((resume) => {
|
|
748
|
+
observers.push(mutations.observeChange(revision, () => resume(Effect.succeed("scan"))));
|
|
749
|
+
for (const fiber of fibers)
|
|
750
|
+
observers.push(fiber.addObserver(() => resume(Effect.succeed("completed"))));
|
|
751
|
+
}),
|
|
752
|
+
(observers) => Effect.sync(() => observers.forEach((remove) => remove())),
|
|
753
|
+
);
|
|
754
|
+
});
|
|
755
|
+
|
|
737
756
|
/** Post-native delivery shares a finite connection budget across independent host lanes. */
|
|
738
757
|
const afterNativeDispatchConcurrency = 2;
|
|
739
758
|
|
|
@@ -914,6 +933,8 @@ export class ThreadMutationGate extends Context.Service<
|
|
|
914
933
|
readonly activeLanes: Effect.Effect<ReadonlySet<string>>;
|
|
915
934
|
readonly revision: Effect.Effect<number>;
|
|
916
935
|
readonly awaitChange: (revision: number) => Effect.Effect<void>;
|
|
936
|
+
/** Register an incarnation-local observer; its caller must detach it on completion. */
|
|
937
|
+
readonly observeChange: (revision: number, notify: () => void) => () => void;
|
|
917
938
|
readonly withSnapshot: <A, E, R>(
|
|
918
939
|
body: (active: number) => Effect.Effect<A, E, R>,
|
|
919
940
|
) => Effect.Effect<A, E, R>;
|
|
@@ -934,14 +955,27 @@ export class ThreadMutationGate extends Context.Service<
|
|
|
934
955
|
const activeLanes = new Map<string, number>();
|
|
935
956
|
const generationGate = yield* Semaphore.make(1);
|
|
936
957
|
let revision = 0;
|
|
937
|
-
|
|
958
|
+
const listeners = new Set<() => void>();
|
|
959
|
+
|
|
960
|
+
const observeChange = (observed: number, listener: () => void): (() => void) => {
|
|
961
|
+
if (observed !== revision) {
|
|
962
|
+
listener();
|
|
963
|
+
|
|
964
|
+
return () => {};
|
|
965
|
+
}
|
|
966
|
+
listeners.add(listener);
|
|
938
967
|
|
|
939
|
-
|
|
940
|
-
|
|
968
|
+
return () => {
|
|
969
|
+
listeners.delete(listener);
|
|
970
|
+
};
|
|
971
|
+
};
|
|
941
972
|
|
|
942
|
-
|
|
973
|
+
const notify = Effect.sync(() => {
|
|
943
974
|
revision++;
|
|
944
|
-
|
|
975
|
+
const pending = [...listeners];
|
|
976
|
+
|
|
977
|
+
listeners.clear();
|
|
978
|
+
for (const listener of pending) listener();
|
|
945
979
|
});
|
|
946
980
|
|
|
947
981
|
const minimumAlarmDelay = Math.max(1, Math.ceil(config.alarmBackoffBase / 2));
|
|
@@ -1182,8 +1216,13 @@ export class ThreadMutationGate extends Context.Service<
|
|
|
1182
1216
|
recordProgress,
|
|
1183
1217
|
activeLanes: Effect.sync(() => new Set(activeLanes.keys())),
|
|
1184
1218
|
revision: Effect.sync(() => revision),
|
|
1219
|
+
observeChange,
|
|
1185
1220
|
awaitChange: (observed) =>
|
|
1186
|
-
Effect.
|
|
1221
|
+
Effect.callback((resume) => {
|
|
1222
|
+
const remove = observeChange(observed, () => resume(Effect.void));
|
|
1223
|
+
|
|
1224
|
+
return Effect.sync(remove);
|
|
1225
|
+
}),
|
|
1187
1226
|
withSnapshot: (body) =>
|
|
1188
1227
|
generationGate.withPermit(Effect.flatMap(Ref.get(activeMutations), body)),
|
|
1189
1228
|
});
|
|
@@ -1259,6 +1298,7 @@ export class ThreadMaintenance extends Context.Service<
|
|
|
1259
1298
|
const runtime = yield* DurableAgentRuntime;
|
|
1260
1299
|
const recoveryConfig = yield* DurableRuntimeConfig;
|
|
1261
1300
|
const ledger = yield* SubmissionLedger;
|
|
1301
|
+
const listNonterminal = ledger.listNonterminal ?? Stream.runCollect(ledger.scanNonterminal);
|
|
1262
1302
|
const wakes = yield* WakeScheduler;
|
|
1263
1303
|
const hints = yield* DurableAlarmService;
|
|
1264
1304
|
const alarms = yield* ThreadMaintenanceAlarm;
|
|
@@ -1280,6 +1320,8 @@ export class ThreadMaintenance extends Context.Service<
|
|
|
1280
1320
|
const messages = yield* ThreadMessageDelivery;
|
|
1281
1321
|
const host = yield* ThreadHostMaintenance;
|
|
1282
1322
|
const framework = yield* ThreadNativeMaintenance;
|
|
1323
|
+
const placement = yield* Effect.serviceOption(ThreadObjectPlacement);
|
|
1324
|
+
const nativeThread = Option.isSome(placement) ? placement.value.threadId : undefined;
|
|
1283
1325
|
|
|
1284
1326
|
const nativeDispatchConcurrency = () => (framework.singleThreadRuntime === runtime ? 1 : 2);
|
|
1285
1327
|
|
|
@@ -1564,9 +1606,7 @@ export class ThreadMaintenance extends Context.Service<
|
|
|
1564
1606
|
const now = yield* Clock.currentTimeMillis;
|
|
1565
1607
|
|
|
1566
1608
|
// Fault-only discovery uses control state even when retained payloads cannot decode.
|
|
1567
|
-
const submissions =
|
|
1568
|
-
current ??
|
|
1569
|
-
(result.blocked.length === 0 ? [] : yield* Stream.runCollect(ledger.scanNonterminal));
|
|
1609
|
+
const submissions = current ?? (result.blocked.length === 0 ? [] : yield* listNonterminal);
|
|
1570
1610
|
|
|
1571
1611
|
yield* failpoint.hit("maintenance:recovery-status:before");
|
|
1572
1612
|
|
|
@@ -1681,7 +1721,11 @@ export class ThreadMaintenance extends Context.Service<
|
|
|
1681
1721
|
|
|
1682
1722
|
do {
|
|
1683
1723
|
const result = yield* runtime
|
|
1684
|
-
.runRecovery({
|
|
1724
|
+
.runRecovery({
|
|
1725
|
+
threadId,
|
|
1726
|
+
deferUnclaimedAdmissions: true,
|
|
1727
|
+
...(cursor === undefined ? {} : { cursor }),
|
|
1728
|
+
})
|
|
1685
1729
|
.pipe(Effect.provideService(CurrentNativeSource, true));
|
|
1686
1730
|
|
|
1687
1731
|
// Persist each bounded pass before a claim; eviction resumes past consumed metadata.
|
|
@@ -2124,19 +2168,31 @@ export class ThreadMaintenance extends Context.Service<
|
|
|
2124
2168
|
|
|
2125
2169
|
return {
|
|
2126
2170
|
scannedGeneration: state.dirty,
|
|
2127
|
-
|
|
2171
|
+
// Single-Thread placement would immediately wrap past its own cursor.
|
|
2172
|
+
afterWorkThread:
|
|
2173
|
+
framework.singleThreadRuntime === runtime &&
|
|
2174
|
+
state.lastRecoveredThreadId === nativeThread
|
|
2175
|
+
? undefined
|
|
2176
|
+
: state.lastRecoveredThreadId,
|
|
2128
2177
|
};
|
|
2129
2178
|
},
|
|
2130
2179
|
);
|
|
2131
2180
|
|
|
2132
|
-
const current = yield*
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
.
|
|
2181
|
+
const current = yield* listNonterminal;
|
|
2182
|
+
|
|
2183
|
+
// A current admission proves this native placement is materialized. Empty Objects
|
|
2184
|
+
// still discover retained canonical obligations without inventing a recovery owner.
|
|
2185
|
+
let workOwners =
|
|
2186
|
+
framework.singleThreadRuntime === runtime &&
|
|
2187
|
+
nativeThread !== undefined &&
|
|
2188
|
+
current.some((submission) => submission.threadId === nativeThread)
|
|
2189
|
+
? { threadIds: [nativeThread], afterThreadId: undefined }
|
|
2190
|
+
: yield* runtime
|
|
2191
|
+
.discoverWorkThreads({
|
|
2192
|
+
limit: 32,
|
|
2193
|
+
...(afterWorkThread === undefined ? {} : { afterThreadId: afterWorkThread }),
|
|
2194
|
+
})
|
|
2195
|
+
.pipe(Effect.provideService(CurrentNativeSource, true));
|
|
2140
2196
|
|
|
2141
2197
|
if (workOwners.threadIds.length === 0 && afterWorkThread !== undefined)
|
|
2142
2198
|
workOwners = yield* runtime
|
|
@@ -2398,7 +2454,7 @@ export class ThreadMaintenance extends Context.Service<
|
|
|
2398
2454
|
};
|
|
2399
2455
|
}
|
|
2400
2456
|
|
|
2401
|
-
const remaining = yield*
|
|
2457
|
+
const remaining = yield* listNonterminal;
|
|
2402
2458
|
const waitingHeads = new Map<ThreadId, boolean>();
|
|
2403
2459
|
|
|
2404
2460
|
// Selected recovery must finish before its generation can be acknowledged: terminal
|
|
@@ -2565,12 +2621,22 @@ export class ThreadMaintenance extends Context.Service<
|
|
|
2565
2621
|
dispatchUntil: DateTime.Utc,
|
|
2566
2622
|
observed: MaintenanceObservation,
|
|
2567
2623
|
) {
|
|
2624
|
+
const leanSelection =
|
|
2625
|
+
framework.singleThreadRuntime === runtime &&
|
|
2626
|
+
publication === emptyPublication &&
|
|
2627
|
+
host.lanes.length === 0 &&
|
|
2628
|
+
framework.lanes.length === 0;
|
|
2629
|
+
|
|
2568
2630
|
// Subscribe before the first snapshot. Durable enrollment recovers eviction;
|
|
2569
2631
|
// producer completion and wake hints make work visible during this incarnation.
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2632
|
+
// Native source mutations already notify the revision waiter below. A replacement
|
|
2633
|
+
// or shared runtime keeps the generic wake stream; hints confer no authority.
|
|
2634
|
+
const notified = leanSelection
|
|
2635
|
+
? Effect.never
|
|
2636
|
+
: (yield* Stream.toPull(wakes.wakes)).pipe(
|
|
2637
|
+
Effect.asVoid,
|
|
2638
|
+
Effect.catch(() => Effect.never),
|
|
2639
|
+
);
|
|
2574
2640
|
|
|
2575
2641
|
// A wake may observe temporary backoff while this event's recovery is still running.
|
|
2576
2642
|
// Its completion must retain the original actionable observation for acknowledgement.
|
|
@@ -2622,56 +2688,8 @@ export class ThreadMaintenance extends Context.Service<
|
|
|
2622
2688
|
discoveryPending: false,
|
|
2623
2689
|
};
|
|
2624
2690
|
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
for (const threadId of yield* Deferred.await(recovery.queue)) {
|
|
2628
|
-
yield* failpoint.hit("maintenance:select:before");
|
|
2629
|
-
yield* runTransaction("select old recovery lane", () =>
|
|
2630
|
-
dueQueue.transaction(async (transaction) => {
|
|
2631
|
-
const { state } = await readMaintenanceState(transaction);
|
|
2632
|
-
|
|
2633
|
-
await transaction.put(
|
|
2634
|
-
MAINTENANCE_STATE_KEY,
|
|
2635
|
-
encodeMaintenanceState(
|
|
2636
|
-
ThreadMaintenanceState.make({
|
|
2637
|
-
...state,
|
|
2638
|
-
lastRecoveredThreadId: threadId,
|
|
2639
|
-
}),
|
|
2640
|
-
),
|
|
2641
|
-
);
|
|
2642
|
-
}),
|
|
2643
|
-
);
|
|
2644
|
-
yield* failpoint.hit("maintenance:select:after");
|
|
2645
|
-
yield* recoverThread(threadId, recovery);
|
|
2646
|
-
recovery.pending.delete(threadId);
|
|
2647
|
-
if (!recovery.faults.has(threadId)) yield* wakes.notify(threadId);
|
|
2648
|
-
}
|
|
2649
|
-
}),
|
|
2650
|
-
);
|
|
2651
|
-
|
|
2652
|
-
// Backfill is one disposable wave; native and host work keep their own opportunities.
|
|
2653
|
-
const backfill = yield* fork(
|
|
2654
|
-
Effect.gen(function* () {
|
|
2655
|
-
if ((yield* mutations.activeLanes).has(DueQueue.Projection)) return;
|
|
2656
|
-
const row = (yield* queueSnapshot).find((row) => row.id === DueQueue.Projection);
|
|
2657
|
-
|
|
2658
|
-
if (
|
|
2659
|
-
row?.dueAt === null ||
|
|
2660
|
-
row === undefined ||
|
|
2661
|
-
row.dueAt > (yield* Clock.currentTimeMillis)
|
|
2662
|
-
)
|
|
2663
|
-
return;
|
|
2664
|
-
|
|
2665
|
-
return yield* runQueued(
|
|
2666
|
-
row,
|
|
2667
|
-
projection.drain.pipe(Effect.andThen(projection.pendingDeadline)),
|
|
2668
|
-
observed,
|
|
2669
|
-
).pipe(Effect.timeoutOption(config.projectionDispatchTimeoutMillis));
|
|
2670
|
-
}),
|
|
2671
|
-
);
|
|
2672
|
-
|
|
2673
|
-
let backfillObserved = false;
|
|
2674
|
-
let recoveryObserved = false;
|
|
2691
|
+
let recoveryFiber: Fiber.Fiber<void, MaintenancePassFailure> | undefined;
|
|
2692
|
+
let backfill: Fiber.Fiber<void, DurableAlarmError | ThreadProjectionError> | undefined;
|
|
2675
2693
|
|
|
2676
2694
|
const dispatch: NativeDispatch = {
|
|
2677
2695
|
scope: auxiliaryScope,
|
|
@@ -2683,18 +2701,79 @@ export class ThreadMaintenance extends Context.Service<
|
|
|
2683
2701
|
needsCheckpoint: false,
|
|
2684
2702
|
};
|
|
2685
2703
|
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2704
|
+
// Selection is bounded local work, not an independently owned wave. It opens
|
|
2705
|
+
// actual Attempt scopes, then returns; empty lanes create no fibers.
|
|
2706
|
+
let native: Fiber.Fiber<NativePassResult, MaintenancePassFailure> | undefined;
|
|
2707
|
+
let result: NativePassResult;
|
|
2708
|
+
let backlog: ReadonlyArray<ThreadId> | undefined;
|
|
2689
2709
|
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2710
|
+
if (leanSelection) {
|
|
2711
|
+
result = yield* advance(started, yieldAfter, observed, recovery, dispatch);
|
|
2712
|
+
recovery.started = true;
|
|
2713
|
+
yield* Deferred.succeed(recovery.queue, []);
|
|
2714
|
+
backlog = yield* Deferred.await(recovery.queue);
|
|
2715
|
+
} else {
|
|
2716
|
+
// Required publication and arbitrary host lanes keep independent opportunities
|
|
2717
|
+
// while selection is suspended in an application callback.
|
|
2718
|
+
native = yield* fork(advance(started, yieldAfter, observed, recovery, dispatch));
|
|
2719
|
+
result = {
|
|
2720
|
+
phase: "caught-up",
|
|
2721
|
+
nonterminal: started.nonterminal,
|
|
2722
|
+
nextAttemptAt: undefined,
|
|
2723
|
+
};
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
if (backlog === undefined || backlog.length > 0) {
|
|
2727
|
+
recoveryFiber = yield* fork(
|
|
2728
|
+
Effect.gen(function* () {
|
|
2729
|
+
for (const threadId of backlog ?? (yield* Deferred.await(recovery.queue))) {
|
|
2730
|
+
yield* failpoint.hit("maintenance:select:before");
|
|
2731
|
+
yield* runTransaction("select old recovery lane", () =>
|
|
2732
|
+
dueQueue.transaction(async (transaction) => {
|
|
2733
|
+
const { state } = await readMaintenanceState(transaction);
|
|
2734
|
+
|
|
2735
|
+
await transaction.put(
|
|
2736
|
+
MAINTENANCE_STATE_KEY,
|
|
2737
|
+
encodeMaintenanceState(
|
|
2738
|
+
ThreadMaintenanceState.make({
|
|
2739
|
+
...state,
|
|
2740
|
+
lastRecoveredThreadId: threadId,
|
|
2741
|
+
}),
|
|
2742
|
+
),
|
|
2743
|
+
);
|
|
2744
|
+
}),
|
|
2745
|
+
);
|
|
2746
|
+
yield* failpoint.hit("maintenance:select:after");
|
|
2747
|
+
yield* recoverThread(threadId, recovery);
|
|
2748
|
+
recovery.pending.delete(threadId);
|
|
2749
|
+
if (!recovery.faults.has(threadId)) yield* wakes.notify(threadId);
|
|
2750
|
+
}
|
|
2751
|
+
}),
|
|
2752
|
+
);
|
|
2753
|
+
}
|
|
2754
|
+
const projectionRow = (yield* queueSnapshot).find((row) => row.id === DueQueue.Projection);
|
|
2755
|
+
|
|
2756
|
+
if (
|
|
2757
|
+
projectionRow !== undefined &&
|
|
2758
|
+
projectionRow.dueAt !== null &&
|
|
2759
|
+
projectionRow.dueAt <= (yield* Clock.currentTimeMillis) &&
|
|
2760
|
+
!(yield* mutations.activeLanes).has(DueQueue.Projection)
|
|
2761
|
+
) {
|
|
2762
|
+
backfill = yield* fork(
|
|
2763
|
+
runQueued(
|
|
2764
|
+
projectionRow,
|
|
2765
|
+
projection.drain.pipe(Effect.andThen(projection.pendingDeadline)),
|
|
2766
|
+
observed,
|
|
2767
|
+
).pipe(Effect.timeoutOption(config.projectionDispatchTimeoutMillis), Effect.asVoid),
|
|
2768
|
+
);
|
|
2769
|
+
}
|
|
2695
2770
|
|
|
2696
2771
|
let phase = result.phase;
|
|
2697
|
-
|
|
2772
|
+
|
|
2773
|
+
let nativeCheck =
|
|
2774
|
+
result.dispatched === true && dispatch.active.size < nativeDispatchConcurrency();
|
|
2775
|
+
|
|
2776
|
+
observed.nativeOnly = dispatch.active.size > 0;
|
|
2698
2777
|
const until = DateTime.toEpochMillis(yieldAfter);
|
|
2699
2778
|
const dispatchEnd = DateTime.toEpochMillis(dispatchUntil);
|
|
2700
2779
|
|
|
@@ -2725,19 +2804,17 @@ export class ThreadMaintenance extends Context.Service<
|
|
|
2725
2804
|
if (exit.value === 0) dispatch.deferred.add(threadId);
|
|
2726
2805
|
nativeCheck = true;
|
|
2727
2806
|
}
|
|
2728
|
-
if (
|
|
2807
|
+
if (recoveryFiber?.pollUnsafe() !== undefined) {
|
|
2729
2808
|
yield* Fiber.join(recoveryFiber);
|
|
2730
|
-
|
|
2809
|
+
recoveryFiber = undefined;
|
|
2731
2810
|
dispatch.scanGeneration = undefined;
|
|
2732
2811
|
nativeCheck ||= recovery.needsCheckpoint;
|
|
2733
2812
|
}
|
|
2734
|
-
|
|
2735
|
-
const exit = backfill.pollUnsafe();
|
|
2813
|
+
const backfillExit = backfill?.pollUnsafe();
|
|
2736
2814
|
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
}
|
|
2815
|
+
if (backfillExit !== undefined) {
|
|
2816
|
+
backfill = undefined;
|
|
2817
|
+
if (Exit.isFailure(backfillExit)) failure ??= backfillExit.cause;
|
|
2741
2818
|
}
|
|
2742
2819
|
for (const lane of lanes) {
|
|
2743
2820
|
const exit = lane.fiber?.pollUnsafe();
|
|
@@ -2844,9 +2921,9 @@ export class ThreadMaintenance extends Context.Service<
|
|
|
2844
2921
|
if (
|
|
2845
2922
|
native === undefined &&
|
|
2846
2923
|
dispatch.active.size === 0 &&
|
|
2847
|
-
|
|
2924
|
+
recoveryFiber === undefined &&
|
|
2848
2925
|
!recovery.needsCheckpoint &&
|
|
2849
|
-
|
|
2926
|
+
backfill === undefined &&
|
|
2850
2927
|
delivery === undefined &&
|
|
2851
2928
|
lanes.every((lane) => lane.fiber === undefined)
|
|
2852
2929
|
)
|
|
@@ -2859,7 +2936,9 @@ export class ThreadMaintenance extends Context.Service<
|
|
|
2859
2936
|
) {
|
|
2860
2937
|
nativeCheck = false;
|
|
2861
2938
|
const nativeCheckpoint = dispatch.needsCheckpoint && dispatch.active.size === 0;
|
|
2862
|
-
|
|
2939
|
+
|
|
2940
|
+
const checkpoint =
|
|
2941
|
+
(recoveryFiber === undefined && recovery.needsCheckpoint) || nativeCheckpoint;
|
|
2863
2942
|
|
|
2864
2943
|
if (checkpoint) recovery.needsCheckpoint = false;
|
|
2865
2944
|
// Keep this opportunity's reservations across beginNative's storage awaits.
|
|
@@ -2894,26 +2973,39 @@ export class ThreadMaintenance extends Context.Service<
|
|
|
2894
2973
|
);
|
|
2895
2974
|
|
|
2896
2975
|
const completing = [
|
|
2897
|
-
|
|
2976
|
+
native,
|
|
2898
2977
|
...dispatch.active.values(),
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
...lanes.
|
|
2903
|
-
];
|
|
2904
|
-
|
|
2905
|
-
const
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2978
|
+
recoveryFiber,
|
|
2979
|
+
backfill,
|
|
2980
|
+
delivery,
|
|
2981
|
+
...lanes.map((lane) => lane.fiber),
|
|
2982
|
+
].filter((fiber) => fiber !== undefined);
|
|
2983
|
+
|
|
2984
|
+
const changed = awaitAnyCompletion(completing, queueRevision);
|
|
2985
|
+
|
|
2986
|
+
// An occupied native-only slot cannot admit another Attempt. With no auxiliary
|
|
2987
|
+
// wake before yield, the timer would only switch to joining that same Attempt.
|
|
2988
|
+
// Incoming work still wakes this callback; all new waves recheck the deadline.
|
|
2989
|
+
const joiningNative =
|
|
2990
|
+
leanSelection &&
|
|
2991
|
+
native === undefined &&
|
|
2992
|
+
dispatch.active.size === 1 &&
|
|
2993
|
+
completing.length === 1 &&
|
|
2994
|
+
!queued.some(
|
|
2995
|
+
(row) =>
|
|
2996
|
+
row.id !== DueQueue.Native &&
|
|
2997
|
+
row.dueAt !== null &&
|
|
2998
|
+
row.dueAt > now &&
|
|
2999
|
+
row.dueAt < until,
|
|
3000
|
+
);
|
|
3001
|
+
|
|
3002
|
+
const ready = yield* joiningNative
|
|
3003
|
+
? changed
|
|
3004
|
+
: Effect.raceAllFirst([
|
|
3005
|
+
changed,
|
|
3006
|
+
...(leanSelection ? [] : [notified.pipe(Effect.as("scan" as const))]),
|
|
3007
|
+
Effect.sleep(Math.max(0, next - now)).pipe(Effect.as("scan" as const)),
|
|
3008
|
+
]);
|
|
2917
3009
|
|
|
2918
3010
|
if (ready === "scan") {
|
|
2919
3011
|
nativeCheck = true;
|
|
@@ -2939,9 +3031,10 @@ export class ThreadMaintenance extends Context.Service<
|
|
|
2939
3031
|
for (const fiber of [
|
|
2940
3032
|
recoveryFiber,
|
|
2941
3033
|
backfill,
|
|
2942
|
-
|
|
2943
|
-
...lanes.
|
|
3034
|
+
delivery,
|
|
3035
|
+
...lanes.map((lane) => lane.fiber),
|
|
2944
3036
|
]) {
|
|
3037
|
+
if (fiber === undefined) continue;
|
|
2945
3038
|
// Await exits without failing fast: every admitted wave owns its original
|
|
2946
3039
|
// bounded allowance and cleanup even if another wave has already failed.
|
|
2947
3040
|
const exit = yield* Fiber.await<unknown, MaintenancePassFailure>(fiber);
|
|
@@ -2964,7 +3057,9 @@ export class ThreadMaintenance extends Context.Service<
|
|
|
2964
3057
|
observed: MaintenanceObservation,
|
|
2965
3058
|
): Effect.fn.Return<MaintenancePassReport, MaintenancePassFailure, Scope.Scope> {
|
|
2966
3059
|
const concurrent = yield* Effect.exit(
|
|
2967
|
-
Effect.scoped(dispatch(yieldAfter, dispatchUntil, observed))
|
|
3060
|
+
Effect.scoped(dispatch(yieldAfter, dispatchUntil, observed)).pipe(
|
|
3061
|
+
Effect.provideService(ThreadMutationGate, mutations),
|
|
3062
|
+
),
|
|
2968
3063
|
);
|
|
2969
3064
|
|
|
2970
3065
|
// Closing the concurrent scope first releases every native Attempt and its resources.
|
|
@@ -386,13 +386,14 @@ const liveTextProtocolFailure = () =>
|
|
|
386
386
|
message: "Provisional text is incomplete or invalid; discard drafts and read canonical records",
|
|
387
387
|
});
|
|
388
388
|
|
|
389
|
+
const decodeLiveTextFrame = Schema.decodeUnknownSync(Schema.fromJsonString(LiveTextFrame));
|
|
390
|
+
|
|
389
391
|
const decodeLiveText = (
|
|
390
392
|
threadId: ThreadId,
|
|
391
393
|
source: ReadableStream<unknown>,
|
|
392
394
|
): Stream.Stream<LiveTextFrame, HostProtocolError | ThreadClientError> =>
|
|
393
395
|
Stream.suspend(() => {
|
|
394
396
|
const decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: false });
|
|
395
|
-
const decode = Schema.decodeUnknownSync(Schema.fromJsonString(LiveTextFrame));
|
|
396
397
|
let pending = "";
|
|
397
398
|
let pendingBytes = 0;
|
|
398
399
|
let expected = 0;
|
|
@@ -488,7 +489,7 @@ const decodeLiveText = (
|
|
|
488
489
|
if (newline < 0) break;
|
|
489
490
|
|
|
490
491
|
const frame = yield* Effect.try({
|
|
491
|
-
try: () =>
|
|
492
|
+
try: () => decodeLiveTextFrame(pending),
|
|
492
493
|
catch: liveTextProtocolFailure,
|
|
493
494
|
});
|
|
494
495
|
|
package/src/ThreadObject.ts
CHANGED
|
@@ -351,16 +351,17 @@ const gateAdmissionLimits = Effect.fnUntraced(function* (
|
|
|
351
351
|
);
|
|
352
352
|
}
|
|
353
353
|
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
354
|
+
const queueDepth = yield* Stream.runFold(
|
|
355
|
+
ledger.scanNonterminal,
|
|
356
|
+
() => 0,
|
|
357
|
+
(depth, submission) => depth + (submission.threadId === threadId ? 1 : 0),
|
|
357
358
|
);
|
|
358
359
|
|
|
359
|
-
if (
|
|
360
|
+
if (queueDepth >= config.limits.maxQueueDepthPerLane) {
|
|
360
361
|
return yield* refuseFresh(
|
|
361
362
|
AdmissionLimitExceeded.make({
|
|
362
363
|
limit: "queue-depth",
|
|
363
|
-
actual:
|
|
364
|
+
actual: queueDepth,
|
|
364
365
|
maximum: config.limits.maxQueueDepthPerLane,
|
|
365
366
|
}),
|
|
366
367
|
);
|
|
@@ -1186,31 +1187,25 @@ const effectCfPlatformLayer = (
|
|
|
1186
1187
|
DurableObjectContext | ThreadObjectNamespace,
|
|
1187
1188
|
CloudflareBindingError,
|
|
1188
1189
|
EffectCfDurableObjectState.DurableObjectState | WorkerEnvironment
|
|
1189
|
-
> =>
|
|
1190
|
-
|
|
1190
|
+
> =>
|
|
1191
|
+
Layer.effectContext(
|
|
1191
1192
|
Effect.gen(function* () {
|
|
1192
1193
|
const state = yield* EffectCfDurableObjectState.DurableObjectState;
|
|
1193
1194
|
const env = yield* WorkerEnvironment;
|
|
1194
|
-
|
|
1195
|
-
return DurableObjectContext.of({ ctx: state.raw, env });
|
|
1196
|
-
}),
|
|
1197
|
-
);
|
|
1198
|
-
|
|
1199
|
-
const namespace = Layer.effect(ThreadObjectNamespace)(
|
|
1200
|
-
Effect.gen(function* () {
|
|
1201
|
-
const env = yield* WorkerEnvironment;
|
|
1202
1195
|
const binding = yield* threadNamespaceFromEnv(env, namespaceBinding);
|
|
1203
1196
|
|
|
1204
|
-
return
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1197
|
+
return Context.make(DurableObjectContext, { ctx: state.raw, env }).pipe(
|
|
1198
|
+
Context.add(
|
|
1199
|
+
ThreadObjectNamespace,
|
|
1200
|
+
ThreadObjectNamespace.of({
|
|
1201
|
+
get: (threadId) => binding.get(binding.idFromName(threadId)),
|
|
1202
|
+
...(rpcTracing === true ? { rpcTracing: namespaceBinding } : {}),
|
|
1203
|
+
}),
|
|
1204
|
+
),
|
|
1205
|
+
);
|
|
1208
1206
|
}),
|
|
1209
1207
|
);
|
|
1210
1208
|
|
|
1211
|
-
return Layer.merge(context, namespace);
|
|
1212
|
-
};
|
|
1213
|
-
|
|
1214
1209
|
/** The public endpoints and effect-cf invocation hook of one Thread Object instance. */
|
|
1215
1210
|
export interface Instance<EventServices = never> extends InstanceType<
|
|
1216
1211
|
EffectCfDurableObject.DurableObjectClass<
|