@effect-agent/platform-cloudflare 0.1.0-beta.125 → 0.1.0-beta.129
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 +2 -1
- package/dist/Alarm.mjs +132 -69
- package/dist/Alarm.mjs.map +1 -1
- package/dist/{BrowserSession-DuOUcC6G.mjs → BrowserSession-BplIoNfi.mjs} +3 -3
- package/dist/BrowserSession-BplIoNfi.mjs.map +1 -0
- package/dist/BrowserSession.mjs +2 -2
- package/dist/CloudflareBindings.mjs +1 -1
- package/dist/CloudflareScheduling.mjs +1 -1
- package/dist/CloudflareSubscriptions.mjs +1 -1
- package/dist/CloudflareThreadClient.d.mts +106 -51
- package/dist/CloudflareThreadClient.mjs +1 -1
- package/dist/{InteractiveBrowser-DeOlcu-2.mjs → InteractiveBrowser-D-xxDv_f.mjs} +7 -4
- package/dist/InteractiveBrowser-D-xxDv_f.mjs.map +1 -0
- package/dist/InteractiveBrowser.mjs +1 -1
- package/dist/{ThreadObject-BTo1jqab.d.mts → ThreadObject-9ho9DTjd.d.mts} +28 -28
- package/dist/{ThreadObject-DaC-IhCm.mjs → ThreadObject-CVhZC-7l.mjs} +2 -2
- package/dist/{ThreadObject-DaC-IhCm.mjs.map → ThreadObject-CVhZC-7l.mjs.map} +1 -1
- package/dist/ThreadObject.d.mts +1 -1
- package/dist/ThreadObject.mjs +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +2 -2
- package/package.json +1 -1
- package/src/Alarm.ts +252 -106
- package/src/BrowserSession.ts +3 -1
- package/src/internal/browser-binding.ts +5 -3
- package/dist/BrowserSession-DuOUcC6G.mjs.map +0 -1
- package/dist/InteractiveBrowser-DeOlcu-2.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -8,8 +8,8 @@ import { t as CloudflareThreadClient_exports } from "./CloudflareThreadClient.mj
|
|
|
8
8
|
import { t as CloudflareScheduling_exports } from "./CloudflareScheduling.mjs";
|
|
9
9
|
import { t as CloudflareSubscriptions_exports } from "./CloudflareSubscriptions.mjs";
|
|
10
10
|
import { t as WakeScheduler_exports } from "./WakeScheduler.mjs";
|
|
11
|
-
import { l as ThreadObject_exports } from "./ThreadObject-
|
|
11
|
+
import { l as ThreadObject_exports } from "./ThreadObject-CVhZC-7l.mjs";
|
|
12
12
|
import { t as CloudflareAiGateway_exports } from "./CloudflareAiGateway.mjs";
|
|
13
13
|
import { t as BrowserCredentials_exports } from "./BrowserCredentials.mjs";
|
|
14
|
-
import { i as BrowserSession_exports } from "./BrowserSession-
|
|
14
|
+
import { i as BrowserSession_exports } from "./BrowserSession-BplIoNfi.mjs";
|
|
15
15
|
export { Alarm_exports as Alarm, BrowserCredentials_exports as BrowserCredentials, BrowserSession_exports as BrowserSession, CloudflareAiGateway_exports as CloudflareAiGateway, CloudflareBindings_exports as CloudflareBindings, CloudflareBrowser_exports as CloudflareBrowser, CloudflareCodeMode_exports as CloudflareCodeMode, CloudflareConfig_exports as CloudflareConfig, CloudflareMemory_exports as CloudflareMemory, CloudflareScheduling_exports as CloudflareScheduling, CloudflareSubscriptions_exports as CloudflareSubscriptions, CloudflareThreadClient_exports as CloudflareThreadClient, ThreadObject_exports as ThreadObject, WakeScheduler_exports as WakeScheduler };
|
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name":"@effect-agent/platform-cloudflare","version":"0.1.0-beta.
|
|
1
|
+
{"name":"@effect-agent/platform-cloudflare","version":"0.1.0-beta.129","dependencies":{"@effect-agent/storage-cloudflare":"0.1.0-beta.129","@effect/platform-browser":"4.0.0-rc.116","@effect/sql-sqlite-do":"4.0.0-rc.116","effect-agent":"0.1.0-beta.129","puppeteer-core":"24.29.0"},"devDependencies":{"@cloudflare/vitest-pool-workers":"0.21.3","@cloudflare/workers-types":"5.20260825.1","@effect-agent/testing":"0.1.0-beta.129","@effect/platform-node":"4.0.0-rc.116","@effect/sql-d1":"4.0.0-rc.116","@effect/vitest":"4.0.0-rc.116","effect":"4.0.0-rc.116","effect-cf":"0.44.1","esbuild":"0.28.1","miniflare":"5.20260811.1-alpha","typescript":"7.0.2","vite-plus":"0.3.2","vitest":"4.1.11"},"peerDependencies":{"effect":"^4.0.0-rc.116","effect-cf":"^0.44.1 || ^0.45.0"},"exports":{".":{"types":"./dist/index.d.mts","default":"./dist/index.mjs"},"./alarm":{"types":"./dist/Alarm.d.mts","default":"./dist/Alarm.mjs"},"./browser-rest-capture":{"types":"./dist/BrowserRestCapture.d.mts","default":"./dist/BrowserRestCapture.mjs"},"./browser-rest-crawl":{"types":"./dist/BrowserRestCrawl.d.mts","default":"./dist/BrowserRestCrawl.mjs"},"./cloudflare-bindings":{"types":"./dist/CloudflareBindings.d.mts","default":"./dist/CloudflareBindings.mjs"},"./cloudflare-browser":{"types":"./dist/CloudflareBrowser.d.mts","default":"./dist/CloudflareBrowser.mjs"},"./cloudflare-code-mode":{"types":"./dist/CloudflareCodeMode.d.mts","default":"./dist/CloudflareCodeMode.mjs"},"./cloudflare-config":{"types":"./dist/CloudflareConfig.d.mts","default":"./dist/CloudflareConfig.mjs"},"./cloudflare-memory":{"types":"./dist/CloudflareMemory.d.mts","default":"./dist/CloudflareMemory.mjs"},"./cloudflare-scheduling":{"types":"./dist/CloudflareScheduling.d.mts","default":"./dist/CloudflareScheduling.mjs"},"./cloudflare-subscriptions":{"types":"./dist/CloudflareSubscriptions.d.mts","default":"./dist/CloudflareSubscriptions.mjs"},"./cloudflare-thread-client":{"types":"./dist/CloudflareThreadClient.d.mts","default":"./dist/CloudflareThreadClient.mjs"},"./interactive-browser":{"types":"./dist/InteractiveBrowser.d.mts","default":"./dist/InteractiveBrowser.mjs"},"./browser-session":{"types":"./dist/BrowserSession.d.mts","default":"./dist/BrowserSession.mjs"},"./browser-credentials":{"types":"./dist/BrowserCredentials.d.mts","default":"./dist/BrowserCredentials.mjs"},"./thread-object":{"types":"./dist/ThreadObject.d.mts","default":"./dist/ThreadObject.mjs"},"./wake-scheduler":{"types":"./dist/WakeScheduler.d.mts","default":"./dist/WakeScheduler.mjs"},"./cloudflare-ai-gateway":{"types":"./dist/CloudflareAiGateway.d.mts","default":"./dist/CloudflareAiGateway.mjs"}},"description":"Cloudflare Layer assembly for Effect Agent: Durable Objects and Browser Run adapters.","license":"MIT","repository":{"type":"git","url":"git+https://github.com/danieljvdm/effect-agent.git","directory":"packages/platform-cloudflare"},"files":["dist","src"],"type":"module","sideEffects":[],"publishConfig":{"access":"public"},"scripts":{"build":"vp pack","check":"tsc --noEmit -p tsconfig.json"}}
|
package/src/Alarm.ts
CHANGED
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
type ThreadProjectionError,
|
|
47
47
|
} from "effect-agent/thread-projection-maintenance";
|
|
48
48
|
import { WakeScheduler } from "effect-agent/wake-scheduler";
|
|
49
|
+
import { DurableObjectStorage } from "effect-cf";
|
|
49
50
|
import { SqlClient } from "effect/unstable/sql/SqlClient";
|
|
50
51
|
|
|
51
52
|
import { DurableObjectContext } from "./CloudflareBindings.ts";
|
|
@@ -85,14 +86,12 @@ const alarmFailure =
|
|
|
85
86
|
|
|
86
87
|
// SQL and raw KV/alarm operations share one physical SQLite transaction. Reserve its
|
|
87
88
|
// connection for each short storage operation, never around a mutation or snapshot body.
|
|
88
|
-
const
|
|
89
|
+
const makeStorageEffect = Effect.map(
|
|
89
90
|
SqlClient,
|
|
90
91
|
(sql) =>
|
|
91
|
-
<A>(operation: string, execute:
|
|
92
|
+
<A, R>(operation: string, execute: Effect.Effect<A, DurableAlarmError, R>) =>
|
|
92
93
|
Effect.flatMap(Effect.serviceOption(sql.transactionService), (current) => {
|
|
93
|
-
const body = Effect.uninterruptible(
|
|
94
|
-
Effect.tryPromise({ try: execute, catch: alarmFailure(operation) }),
|
|
95
|
-
);
|
|
94
|
+
const body = Effect.uninterruptible(execute);
|
|
96
95
|
|
|
97
96
|
return current._tag === "Some"
|
|
98
97
|
? body
|
|
@@ -102,6 +101,13 @@ const makeStorageOperation = Effect.map(
|
|
|
102
101
|
}),
|
|
103
102
|
);
|
|
104
103
|
|
|
104
|
+
const makeStorageOperation = Effect.map(
|
|
105
|
+
makeStorageEffect,
|
|
106
|
+
(run) =>
|
|
107
|
+
<A>(operation: string, execute: () => Promise<A>) =>
|
|
108
|
+
run(operation, Effect.tryPromise({ try: execute, catch: alarmFailure(operation) })),
|
|
109
|
+
);
|
|
110
|
+
|
|
105
111
|
/** `ctx.storage` alarm slot as an Effect service; storage is truth, never a memory field. */
|
|
106
112
|
export class DurableAlarmService extends Context.Service<
|
|
107
113
|
DurableAlarmService,
|
|
@@ -408,11 +414,26 @@ const encodeRecoveryFault = Schema.encodeSync(ThreadRecoveryFault);
|
|
|
408
414
|
|
|
409
415
|
interface NativePassResult {
|
|
410
416
|
readonly phase: "caught-up" | "actionable";
|
|
411
|
-
readonly settled: number;
|
|
412
417
|
readonly nonterminal: number;
|
|
413
418
|
readonly nextAttemptAt: number | undefined;
|
|
419
|
+
readonly dispatched?: boolean;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/** Independent Threads share two bounded native slots; one active head per Thread. */
|
|
423
|
+
interface NativeDispatch {
|
|
424
|
+
readonly scope: Scope.Scope;
|
|
425
|
+
readonly active: Map<ThreadId, Fiber.Fiber<number, MaintenancePassFailure>>;
|
|
426
|
+
readonly deferred: Set<ThreadId>;
|
|
427
|
+
dispatched: number;
|
|
428
|
+
settled: number;
|
|
429
|
+
progressed: boolean;
|
|
430
|
+
needsCheckpoint: boolean;
|
|
431
|
+
generation?: bigint;
|
|
432
|
+
scanGeneration?: bigint;
|
|
414
433
|
}
|
|
415
434
|
|
|
435
|
+
const nativeDispatchConcurrency = 2;
|
|
436
|
+
|
|
416
437
|
/** Event-local observations only; durable ingress keeps racing mutations dirty. */
|
|
417
438
|
interface NativeRecovery {
|
|
418
439
|
readonly queue: Deferred.Deferred<ReadonlyArray<ThreadId>>;
|
|
@@ -624,7 +645,8 @@ export type MaintenancePassFailure =
|
|
|
624
645
|
* 2. Reconcile before each head Attempt, then checkpoint only the observed generation. A racing
|
|
625
646
|
* producer keeps its generation dirty and immediately eligible. Quiescent native retries
|
|
626
647
|
* retain their durable backoff.
|
|
627
|
-
* 3.
|
|
648
|
+
* 3. Admit at most two independent Thread Attempts, one active head per Thread.
|
|
649
|
+
* Keep native and delivery admission open together while finite waves remain active, so
|
|
628
650
|
* fresh replies and abort controls can progress during unrelated cleanup. Close atomically
|
|
629
651
|
* at quiescence, or at the original ten-minute yield deadline, then join admitted waves.
|
|
630
652
|
* 4. Native message delivery retains its driver-owned Claim deadline. Host/backfill waves are
|
|
@@ -687,6 +709,8 @@ export class ThreadMaintenance extends Context.Service<
|
|
|
687
709
|
const alarm = yield* DurableAlarmService;
|
|
688
710
|
const config = yield* CloudflareDurableRuntimeConfig;
|
|
689
711
|
const { ctx } = yield* DurableObjectContext;
|
|
712
|
+
const storage = DurableObjectStorage.fromDurableObjectStorage(ctx.storage);
|
|
713
|
+
const runStorage = yield* makeStorageEffect;
|
|
690
714
|
const failpoint = yield* ThreadMaintenanceFailpoint;
|
|
691
715
|
|
|
692
716
|
const mutations = yield* ThreadMutationGate;
|
|
@@ -1005,12 +1029,104 @@ export class ThreadMaintenance extends Context.Service<
|
|
|
1005
1029
|
);
|
|
1006
1030
|
});
|
|
1007
1031
|
|
|
1032
|
+
const attemptSelected = Effect.fn("ThreadMaintenance.attemptSelected")(function* (
|
|
1033
|
+
selected: SubmissionWorkItem,
|
|
1034
|
+
previousRetries: ReadonlyArray<BindingRetry>,
|
|
1035
|
+
yieldAfter: DateTime.Utc,
|
|
1036
|
+
recovery: NativeRecovery,
|
|
1037
|
+
): Effect.fn.Return<number, MaintenancePassFailure> {
|
|
1038
|
+
yield* recoverThread(selected.threadId, recovery);
|
|
1039
|
+
|
|
1040
|
+
let bindingFailure: DurableBindingFailure | undefined;
|
|
1041
|
+
|
|
1042
|
+
// A missing binding releases its slot after retaining this exact Submission
|
|
1043
|
+
// and committing its retry deadline below.
|
|
1044
|
+
const settlement = recovery.faults.has(selected.threadId)
|
|
1045
|
+
? Option.none()
|
|
1046
|
+
: yield* runtime.processThreadHead(selected.threadId, { yieldAfter }).pipe(
|
|
1047
|
+
Effect.catchTag("BindingUnavailable", (failure) => {
|
|
1048
|
+
bindingFailure = failure;
|
|
1049
|
+
|
|
1050
|
+
return Effect.succeed(Option.none());
|
|
1051
|
+
}),
|
|
1052
|
+
);
|
|
1053
|
+
|
|
1054
|
+
const previous = previousRetries.find(
|
|
1055
|
+
(retry) => retry.submissionId === selected.submissionId,
|
|
1056
|
+
);
|
|
1057
|
+
|
|
1058
|
+
let retry: BindingRetry | undefined;
|
|
1059
|
+
let reportBindingFailure = false;
|
|
1060
|
+
|
|
1061
|
+
if (bindingFailure !== undefined) {
|
|
1062
|
+
const now = yield* Clock.currentTimeMillis;
|
|
1063
|
+
const attempts = Math.min(30, (previous?.attempts ?? 0) + 1);
|
|
1064
|
+
|
|
1065
|
+
reportBindingFailure = previous === undefined || now - previous.reportedAt >= 15 * 60_000;
|
|
1066
|
+
retry = BindingRetry.make({
|
|
1067
|
+
threadId: selected.threadId,
|
|
1068
|
+
submissionId: selected.submissionId,
|
|
1069
|
+
attempts,
|
|
1070
|
+
notBefore: now + Math.min(60_000, 5_000 * 2 ** (attempts - 1)),
|
|
1071
|
+
reportedAt: reportBindingFailure ? now : (previous?.reportedAt ?? now),
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
if (retry !== undefined || previous !== undefined) {
|
|
1075
|
+
// The Attempt released its Claim. Commit its binding wait (or clear) once,
|
|
1076
|
+
// before joining fallible auxiliary work. This local fact neither acknowledges
|
|
1077
|
+
// a generation nor changes the shared alarm.
|
|
1078
|
+
yield* failpoint.hit("maintenance:binding-retry:before");
|
|
1079
|
+
yield* runStorage(
|
|
1080
|
+
"record submission binding retry",
|
|
1081
|
+
storage
|
|
1082
|
+
.transaction((transaction) =>
|
|
1083
|
+
Effect.gen(function* () {
|
|
1084
|
+
const encoded = yield* transaction.get(MAINTENANCE_STATE_KEY);
|
|
1085
|
+
|
|
1086
|
+
const state =
|
|
1087
|
+
encoded === undefined
|
|
1088
|
+
? initialMaintenanceState()
|
|
1089
|
+
: yield* Schema.decodeUnknownEffect(ThreadMaintenanceState)(encoded);
|
|
1090
|
+
|
|
1091
|
+
const bindingRetries = [
|
|
1092
|
+
...(state.bindingRetries ?? []).filter(
|
|
1093
|
+
(entry) => entry.submissionId !== selected.submissionId,
|
|
1094
|
+
),
|
|
1095
|
+
...(retry === undefined ? [] : [retry]),
|
|
1096
|
+
];
|
|
1097
|
+
|
|
1098
|
+
yield* transaction.put(
|
|
1099
|
+
MAINTENANCE_STATE_KEY,
|
|
1100
|
+
yield* Schema.encodeEffect(ThreadMaintenanceState)(
|
|
1101
|
+
ThreadMaintenanceState.make({ ...state, bindingRetries }),
|
|
1102
|
+
),
|
|
1103
|
+
);
|
|
1104
|
+
}),
|
|
1105
|
+
)
|
|
1106
|
+
.pipe(Effect.mapError(alarmFailure("record submission binding retry"))),
|
|
1107
|
+
);
|
|
1108
|
+
yield* failpoint.hit("maintenance:binding-retry:after");
|
|
1109
|
+
}
|
|
1110
|
+
if (bindingFailure !== undefined) {
|
|
1111
|
+
yield* reportBindingFailure
|
|
1112
|
+
? Effect.logError(
|
|
1113
|
+
"Thread awaits a current agent binding; original work remains pending",
|
|
1114
|
+
Cause.fail(bindingFailure),
|
|
1115
|
+
)
|
|
1116
|
+
: Effect.logDebug("Thread binding retry remains pending", Cause.fail(bindingFailure));
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
return Option.isSome(settlement) ? 1 : 0;
|
|
1120
|
+
});
|
|
1121
|
+
|
|
1008
1122
|
const advance = Effect.fn("ThreadMaintenance.advance")(function* (
|
|
1009
1123
|
started: Effect.Success<ReturnType<typeof beginNative>>,
|
|
1010
1124
|
yieldAfter: DateTime.Utc,
|
|
1011
1125
|
observed: MaintenanceObservation,
|
|
1012
1126
|
recovery: NativeRecovery,
|
|
1127
|
+
native: NativeDispatch,
|
|
1013
1128
|
dispatch = true,
|
|
1129
|
+
reserved: ReadonlySet<ThreadId> = new Set(native.active.keys()),
|
|
1014
1130
|
): Effect.fn.Return<NativePassResult, MaintenancePassFailure> {
|
|
1015
1131
|
const deadline = yield* publication.pendingDeadline;
|
|
1016
1132
|
|
|
@@ -1046,11 +1162,26 @@ export class ThreadMaintenance extends Context.Service<
|
|
|
1046
1162
|
|
|
1047
1163
|
return {
|
|
1048
1164
|
phase: "caught-up",
|
|
1049
|
-
settled: 0,
|
|
1050
1165
|
nonterminal: started.nonterminal,
|
|
1051
1166
|
nextAttemptAt,
|
|
1052
1167
|
};
|
|
1053
1168
|
}
|
|
1169
|
+
if (native.generation !== started.generation) {
|
|
1170
|
+
native.deferred.clear();
|
|
1171
|
+
native.generation = started.generation;
|
|
1172
|
+
}
|
|
1173
|
+
// A stable generation with no freed slot needs no second ledger/history scan.
|
|
1174
|
+
// Enrollment precedes admission: never cache a scan overlapping a producer body,
|
|
1175
|
+
// which may make a head ready later without another generation increment.
|
|
1176
|
+
if (native.active.size > 0 && native.scanGeneration === started.generation) {
|
|
1177
|
+
return {
|
|
1178
|
+
phase: "actionable",
|
|
1179
|
+
nonterminal: started.nonterminal,
|
|
1180
|
+
nextAttemptAt: undefined,
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
native.scanGeneration = started.activeAtStart === 0 ? started.generation : undefined;
|
|
1184
|
+
|
|
1054
1185
|
// Select from control state before reading execution history. A recovering or faulted
|
|
1055
1186
|
// Thread cannot enter dispatch; old cleanup has its own scoped opportunity below.
|
|
1056
1187
|
observed.nativeOnly = true;
|
|
@@ -1094,6 +1225,7 @@ export class ThreadMaintenance extends Context.Service<
|
|
|
1094
1225
|
for (const head of heads.values()) {
|
|
1095
1226
|
if (
|
|
1096
1227
|
head.state !== "ready" ||
|
|
1228
|
+
native.active.has(head.threadId) ||
|
|
1097
1229
|
recovery.pending.has(head.threadId) ||
|
|
1098
1230
|
recoveryFaults.has(head.threadId)
|
|
1099
1231
|
)
|
|
@@ -1151,17 +1283,30 @@ export class ThreadMaintenance extends Context.Service<
|
|
|
1151
1283
|
|
|
1152
1284
|
const runnable = eligible.filter(
|
|
1153
1285
|
(threadId) =>
|
|
1286
|
+
!reserved.has(threadId) &&
|
|
1287
|
+
!native.active.has(threadId) &&
|
|
1288
|
+
!native.deferred.has(threadId) &&
|
|
1154
1289
|
!retries.some(
|
|
1155
1290
|
(retry) => retry.threadId === threadId && retry.notBefore > selectionTime,
|
|
1156
1291
|
),
|
|
1157
1292
|
);
|
|
1158
1293
|
|
|
1159
|
-
const
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1294
|
+
const pivot = Math.max(
|
|
1295
|
+
0,
|
|
1296
|
+
runnable.findIndex(
|
|
1297
|
+
(threadId) =>
|
|
1298
|
+
state.lastServedThreadId === undefined || threadId > state.lastServedThreadId,
|
|
1299
|
+
),
|
|
1300
|
+
);
|
|
1301
|
+
|
|
1302
|
+
const selected = dispatch
|
|
1303
|
+
? [...runnable.slice(pivot), ...runnable.slice(0, pivot)].slice(
|
|
1304
|
+
0,
|
|
1305
|
+
nativeDispatchConcurrency - reserved.size,
|
|
1306
|
+
)
|
|
1307
|
+
: [];
|
|
1308
|
+
|
|
1309
|
+
const next = selected.at(-1);
|
|
1165
1310
|
|
|
1166
1311
|
if (next !== undefined) {
|
|
1167
1312
|
await transaction.put(
|
|
@@ -1196,7 +1341,7 @@ export class ThreadMaintenance extends Context.Service<
|
|
|
1196
1341
|
threadId <= state.lastRecoveredThreadId,
|
|
1197
1342
|
);
|
|
1198
1343
|
|
|
1199
|
-
return { selected
|
|
1344
|
+
return { selected, retries, backlog: [...after, ...before] };
|
|
1200
1345
|
}),
|
|
1201
1346
|
);
|
|
1202
1347
|
|
|
@@ -1208,81 +1353,33 @@ export class ThreadMaintenance extends Context.Service<
|
|
|
1208
1353
|
yield* Deferred.succeed(recovery.queue, selection.backlog);
|
|
1209
1354
|
}
|
|
1210
1355
|
|
|
1211
|
-
const selected
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
bindingFailure = failure;
|
|
1227
|
-
|
|
1228
|
-
return Effect.succeed(Option.none());
|
|
1229
|
-
}),
|
|
1230
|
-
);
|
|
1231
|
-
|
|
1232
|
-
if (selected !== undefined) {
|
|
1233
|
-
const previous = retries.find((retry) => retry.submissionId === selected.submissionId);
|
|
1234
|
-
let retry: BindingRetry | undefined;
|
|
1235
|
-
let reportBindingFailure = false;
|
|
1236
|
-
|
|
1237
|
-
if (bindingFailure !== undefined) {
|
|
1238
|
-
const now = yield* Clock.currentTimeMillis;
|
|
1239
|
-
const attempts = Math.min(30, (previous?.attempts ?? 0) + 1);
|
|
1240
|
-
|
|
1241
|
-
reportBindingFailure =
|
|
1242
|
-
previous === undefined || now - previous.reportedAt >= 15 * 60_000;
|
|
1243
|
-
retry = BindingRetry.make({
|
|
1244
|
-
threadId: selected.threadId,
|
|
1245
|
-
submissionId: selected.submissionId,
|
|
1246
|
-
attempts,
|
|
1247
|
-
notBefore: now + Math.min(60_000, 5_000 * 2 ** (attempts - 1)),
|
|
1248
|
-
reportedAt: reportBindingFailure ? now : (previous?.reportedAt ?? now),
|
|
1249
|
-
});
|
|
1250
|
-
}
|
|
1251
|
-
if (retry !== undefined || previous !== undefined) {
|
|
1252
|
-
// The Attempt released its Claim. Commit its binding wait (or clear) once,
|
|
1253
|
-
// before joining fallible auxiliary work. This local fact neither acknowledges
|
|
1254
|
-
// a generation nor changes the shared alarm.
|
|
1255
|
-
yield* failpoint.hit("maintenance:binding-retry:before");
|
|
1256
|
-
retries = yield* runTransaction("record submission binding retry", () =>
|
|
1257
|
-
ctx.storage.transaction(async (transaction) => {
|
|
1258
|
-
const { state } = await readMaintenanceState(transaction);
|
|
1259
|
-
|
|
1260
|
-
const bindingRetries = [
|
|
1261
|
-
...(state.bindingRetries ?? []).filter(
|
|
1262
|
-
(entry) => entry.submissionId !== selected.submissionId,
|
|
1263
|
-
),
|
|
1264
|
-
...(retry === undefined ? [] : [retry]),
|
|
1265
|
-
];
|
|
1266
|
-
|
|
1267
|
-
await transaction.put(
|
|
1268
|
-
MAINTENANCE_STATE_KEY,
|
|
1269
|
-
encodeMaintenanceState(ThreadMaintenanceState.make({ ...state, bindingRetries })),
|
|
1270
|
-
);
|
|
1356
|
+
for (const threadId of selection.selected) {
|
|
1357
|
+
const selected = heads.get(threadId);
|
|
1358
|
+
|
|
1359
|
+
if (selected === undefined) continue;
|
|
1360
|
+
// Reserve before recovery or claim acquisition. The durable Claim remains the
|
|
1361
|
+
// authority; this event-local reservation only prevents duplicate dispatch.
|
|
1362
|
+
native.dispatched++;
|
|
1363
|
+
native.active.set(
|
|
1364
|
+
threadId,
|
|
1365
|
+
yield* Effect.forkIn(
|
|
1366
|
+
attemptSelected(selected, selection.retries, yieldAfter, recovery),
|
|
1367
|
+
native.scope,
|
|
1368
|
+
),
|
|
1369
|
+
);
|
|
1370
|
+
}
|
|
1271
1371
|
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
Cause.fail(bindingFailure),
|
|
1282
|
-
)
|
|
1283
|
-
: Effect.logDebug("Thread binding retry remains pending", Cause.fail(bindingFailure));
|
|
1284
|
-
}
|
|
1372
|
+
if (native.active.size > 0) {
|
|
1373
|
+
// Active Attempts own their claims and may still append/settle. Only acknowledge
|
|
1374
|
+
// a generation after joining every admitted native Attempt.
|
|
1375
|
+
return {
|
|
1376
|
+
phase: "actionable",
|
|
1377
|
+
nonterminal: current.length,
|
|
1378
|
+
nextAttemptAt: undefined,
|
|
1379
|
+
dispatched: selection.selected.length > 0,
|
|
1380
|
+
};
|
|
1285
1381
|
}
|
|
1382
|
+
const retries = selection.retries;
|
|
1286
1383
|
|
|
1287
1384
|
const remaining = yield* Stream.runCollect(ledger.scanNonterminal);
|
|
1288
1385
|
const waitingHeads = new Map<ThreadId, boolean>();
|
|
@@ -1304,7 +1401,7 @@ export class ThreadMaintenance extends Context.Service<
|
|
|
1304
1401
|
return !waiting(snapshot);
|
|
1305
1402
|
});
|
|
1306
1403
|
|
|
1307
|
-
const progressed =
|
|
1404
|
+
const progressed = native.progressed || recovery.repaired;
|
|
1308
1405
|
|
|
1309
1406
|
const now = yield* Clock.currentTimeMillis;
|
|
1310
1407
|
const ordinaryDelay = autonomous ? yield* rearmDelay(progressed, started.stalls) : 0;
|
|
@@ -1379,10 +1476,12 @@ export class ThreadMaintenance extends Context.Service<
|
|
|
1379
1476
|
);
|
|
1380
1477
|
|
|
1381
1478
|
yield* failpoint.hit("maintenance:checkpoint:after");
|
|
1479
|
+
native.deferred.clear();
|
|
1480
|
+
native.progressed = false;
|
|
1481
|
+
native.needsCheckpoint = false;
|
|
1382
1482
|
|
|
1383
1483
|
return {
|
|
1384
1484
|
phase: "actionable",
|
|
1385
|
-
settled: Option.isSome(settlement) ? 1 : 0,
|
|
1386
1485
|
nonterminal: remaining.length,
|
|
1387
1486
|
nextAttemptAt,
|
|
1388
1487
|
};
|
|
@@ -1473,19 +1572,27 @@ export class ThreadMaintenance extends Context.Service<
|
|
|
1473
1572
|
let backfillObserved = false;
|
|
1474
1573
|
let recoveryObserved = false;
|
|
1475
1574
|
|
|
1575
|
+
const dispatch: NativeDispatch = {
|
|
1576
|
+
scope: auxiliaryScope,
|
|
1577
|
+
active: new Map(),
|
|
1578
|
+
deferred: new Set(),
|
|
1579
|
+
dispatched: 0,
|
|
1580
|
+
settled: 0,
|
|
1581
|
+
progressed: false,
|
|
1582
|
+
needsCheckpoint: false,
|
|
1583
|
+
};
|
|
1584
|
+
|
|
1476
1585
|
let native: Fiber.Fiber<NativePassResult, MaintenancePassFailure> | undefined = yield* fork(
|
|
1477
|
-
advance(started, yieldAfter, observed, recovery),
|
|
1586
|
+
advance(started, yieldAfter, observed, recovery, dispatch),
|
|
1478
1587
|
);
|
|
1479
1588
|
|
|
1480
1589
|
let result: NativePassResult = {
|
|
1481
1590
|
phase: "caught-up",
|
|
1482
|
-
settled: 0,
|
|
1483
1591
|
nonterminal: started.nonterminal,
|
|
1484
1592
|
nextAttemptAt: undefined,
|
|
1485
1593
|
};
|
|
1486
1594
|
|
|
1487
1595
|
let phase = result.phase;
|
|
1488
|
-
let settled = 0;
|
|
1489
1596
|
let nativeCheck = false;
|
|
1490
1597
|
const until = DateTime.toEpochMillis(yieldAfter);
|
|
1491
1598
|
const dispatchEnd = DateTime.toEpochMillis(dispatchUntil);
|
|
@@ -1499,17 +1606,35 @@ export class ThreadMaintenance extends Context.Service<
|
|
|
1499
1606
|
result = yield* Fiber.join(native);
|
|
1500
1607
|
native = undefined;
|
|
1501
1608
|
if (result.phase === "actionable") phase = "actionable";
|
|
1502
|
-
|
|
1503
|
-
|
|
1609
|
+
observed.nativeOnly = dispatch.active.size > 0;
|
|
1610
|
+
if (result.dispatched === true && dispatch.active.size < nativeDispatchConcurrency) {
|
|
1611
|
+
dispatch.scanGeneration = undefined;
|
|
1612
|
+
nativeCheck = true;
|
|
1613
|
+
}
|
|
1504
1614
|
// An empty initial scan still opens exactly one old-recovery opportunity.
|
|
1505
1615
|
recovery.started = true;
|
|
1506
1616
|
yield* Deferred.succeed(recovery.queue, []);
|
|
1507
1617
|
checkLanes();
|
|
1508
1618
|
}
|
|
1619
|
+
for (const [threadId, attempt] of dispatch.active) {
|
|
1620
|
+
const exit = attempt.pollUnsafe();
|
|
1621
|
+
|
|
1622
|
+
if (exit === undefined) continue;
|
|
1623
|
+
dispatch.active.delete(threadId);
|
|
1624
|
+
dispatch.scanGeneration = undefined;
|
|
1625
|
+
if (Exit.isFailure(exit)) return yield* Effect.failCause(exit.cause);
|
|
1626
|
+
dispatch.settled += exit.value;
|
|
1627
|
+
dispatch.progressed ||= exit.value > 0;
|
|
1628
|
+
dispatch.needsCheckpoint = true;
|
|
1629
|
+
if (exit.value === 0) dispatch.deferred.add(threadId);
|
|
1630
|
+
nativeCheck = true;
|
|
1631
|
+
checkLanes();
|
|
1632
|
+
}
|
|
1509
1633
|
if (!recoveryObserved && recoveryFiber.pollUnsafe() !== undefined) {
|
|
1510
1634
|
yield* Fiber.join(recoveryFiber);
|
|
1511
1635
|
recoveryObserved = true;
|
|
1512
|
-
|
|
1636
|
+
dispatch.scanGeneration = undefined;
|
|
1637
|
+
nativeCheck ||= recovery.needsCheckpoint;
|
|
1513
1638
|
}
|
|
1514
1639
|
if (!backfillObserved) {
|
|
1515
1640
|
const exit = backfill.pollUnsafe();
|
|
@@ -1542,7 +1667,7 @@ export class ThreadMaintenance extends Context.Service<
|
|
|
1542
1667
|
}
|
|
1543
1668
|
|
|
1544
1669
|
// Preserve the initial native opportunity even when auxiliary setup fails.
|
|
1545
|
-
if (failure !== undefined && native === undefined)
|
|
1670
|
+
if (failure !== undefined && native === undefined && dispatch.active.size === 0)
|
|
1546
1671
|
return yield* Effect.failCause(failure);
|
|
1547
1672
|
const now = yield* Clock.currentTimeMillis;
|
|
1548
1673
|
|
|
@@ -1642,6 +1767,7 @@ export class ThreadMaintenance extends Context.Service<
|
|
|
1642
1767
|
// Recovery still gets its checkpoint and, if needed, initial dispatch opportunity.
|
|
1643
1768
|
if (
|
|
1644
1769
|
native === undefined &&
|
|
1770
|
+
dispatch.active.size === 0 &&
|
|
1645
1771
|
recoveryObserved &&
|
|
1646
1772
|
!recovery.needsCheckpoint &&
|
|
1647
1773
|
backfillObserved &&
|
|
@@ -1650,11 +1776,20 @@ export class ThreadMaintenance extends Context.Service<
|
|
|
1650
1776
|
)
|
|
1651
1777
|
break;
|
|
1652
1778
|
|
|
1653
|
-
if (
|
|
1779
|
+
if (
|
|
1780
|
+
native === undefined &&
|
|
1781
|
+
nativeCheck &&
|
|
1782
|
+
dispatch.active.size < nativeDispatchConcurrency
|
|
1783
|
+
) {
|
|
1654
1784
|
nativeCheck = false;
|
|
1655
|
-
const
|
|
1785
|
+
const nativeCheckpoint = dispatch.needsCheckpoint && dispatch.active.size === 0;
|
|
1786
|
+
const checkpoint = (recoveryObserved && recovery.needsCheckpoint) || nativeCheckpoint;
|
|
1656
1787
|
|
|
1657
1788
|
if (checkpoint) recovery.needsCheckpoint = false;
|
|
1789
|
+
// Keep this opportunity's reservations across beginNative's storage awaits.
|
|
1790
|
+
// A finishing Attempt cannot turn a refill into another same-Thread Attempt.
|
|
1791
|
+
const reserved = new Set(dispatch.active.keys());
|
|
1792
|
+
|
|
1658
1793
|
native = yield* fork(
|
|
1659
1794
|
Effect.gen(function* () {
|
|
1660
1795
|
const awakened = checkpoint ? started : yield* beginNative(observed);
|
|
@@ -1664,7 +1799,11 @@ export class ThreadMaintenance extends Context.Service<
|
|
|
1664
1799
|
yieldAfter,
|
|
1665
1800
|
observed,
|
|
1666
1801
|
recovery,
|
|
1667
|
-
|
|
1802
|
+
dispatch,
|
|
1803
|
+
failure === undefined &&
|
|
1804
|
+
!nativeCheckpoint &&
|
|
1805
|
+
(!checkpoint || dispatch.settled === 0),
|
|
1806
|
+
reserved,
|
|
1668
1807
|
);
|
|
1669
1808
|
}),
|
|
1670
1809
|
);
|
|
@@ -1678,6 +1817,7 @@ export class ThreadMaintenance extends Context.Service<
|
|
|
1678
1817
|
|
|
1679
1818
|
const completing = [
|
|
1680
1819
|
...(native === undefined ? [] : [native]),
|
|
1820
|
+
...dispatch.active.values(),
|
|
1681
1821
|
...(recoveryObserved ? [] : [recoveryFiber]),
|
|
1682
1822
|
...(backfillObserved ? [] : [backfill]),
|
|
1683
1823
|
...(delivery === undefined ? [] : [delivery]),
|
|
@@ -1705,11 +1845,18 @@ export class ThreadMaintenance extends Context.Service<
|
|
|
1705
1845
|
if (native !== undefined) {
|
|
1706
1846
|
result = yield* Fiber.join(native);
|
|
1707
1847
|
if (result.phase === "actionable") phase = "actionable";
|
|
1708
|
-
|
|
1709
|
-
observed.nativeOnly = false;
|
|
1848
|
+
observed.nativeOnly = dispatch.active.size > 0;
|
|
1710
1849
|
recovery.started = true;
|
|
1711
1850
|
yield* Deferred.succeed(recovery.queue, []);
|
|
1712
1851
|
}
|
|
1852
|
+
for (const attempt of dispatch.active.values()) {
|
|
1853
|
+
const settled = yield* Fiber.join(attempt);
|
|
1854
|
+
|
|
1855
|
+
dispatch.settled += settled;
|
|
1856
|
+
dispatch.progressed ||= settled > 0;
|
|
1857
|
+
}
|
|
1858
|
+
dispatch.active.clear();
|
|
1859
|
+
observed.nativeOnly = false;
|
|
1713
1860
|
yield* Fiber.joinAll([
|
|
1714
1861
|
recoveryFiber,
|
|
1715
1862
|
backfill,
|
|
@@ -1717,10 +1864,9 @@ export class ThreadMaintenance extends Context.Service<
|
|
|
1717
1864
|
...lanes.flatMap((lane) => (lane.fiber === undefined ? [] : [lane.fiber])),
|
|
1718
1865
|
]);
|
|
1719
1866
|
if (failure !== undefined) return yield* Effect.failCause(failure);
|
|
1720
|
-
if (recovery.needsCheckpoint) {
|
|
1721
|
-
result = yield* advance(started, yieldAfter, observed, recovery, false);
|
|
1867
|
+
if (recovery.needsCheckpoint || dispatch.dispatched > 0) {
|
|
1868
|
+
result = yield* advance(started, yieldAfter, observed, recovery, dispatch, false);
|
|
1722
1869
|
if (result.phase === "actionable") phase = "actionable";
|
|
1723
|
-
settled += result.settled;
|
|
1724
1870
|
}
|
|
1725
1871
|
yield* Scope.close(auxiliaryScope, Exit.void);
|
|
1726
1872
|
yield* failpoint.hit("maintenance:finish:before");
|
|
@@ -1763,7 +1909,7 @@ export class ThreadMaintenance extends Context.Service<
|
|
|
1763
1909
|
const report = MaintenancePassReport.make({
|
|
1764
1910
|
phase,
|
|
1765
1911
|
recovered: recovery.recovered,
|
|
1766
|
-
settled,
|
|
1912
|
+
settled: dispatch.settled,
|
|
1767
1913
|
nonterminal: result.nonterminal,
|
|
1768
1914
|
alarm: disposition,
|
|
1769
1915
|
});
|
package/src/BrowserSession.ts
CHANGED
|
@@ -242,8 +242,10 @@ export class BrowserSessions extends Context.Service<
|
|
|
242
242
|
attachment === undefined
|
|
243
243
|
? Effect.void
|
|
244
244
|
: attachment.retire.pipe(
|
|
245
|
+
// Dispatch is already fenced; give the socket close handshake the same
|
|
246
|
+
// bounded window as keepalive instead of failing a settled command after 1s.
|
|
245
247
|
Effect.timeoutOrElse({
|
|
246
|
-
duration: "
|
|
248
|
+
duration: "10 seconds",
|
|
247
249
|
orElse: () => Effect.fail(failure("cleanup")),
|
|
248
250
|
}),
|
|
249
251
|
Effect.catchCause((cause) =>
|
|
@@ -148,7 +148,8 @@ export class BrowserRunBinding extends Context.Service<
|
|
|
148
148
|
retirementFailure ??= browserFailure(disconnectOperation, cause);
|
|
149
149
|
}
|
|
150
150
|
try {
|
|
151
|
-
|
|
151
|
+
// Manual-close runtimes also require a reply when the peer leaves us CLOSING.
|
|
152
|
+
if (socket !== undefined && socket.readyState < WebSocket.CLOSED) socket.close();
|
|
152
153
|
} catch (cause) {
|
|
153
154
|
retirementFailure ??= browserFailure(disconnectOperation, cause);
|
|
154
155
|
}
|
|
@@ -183,7 +184,7 @@ export class BrowserRunBinding extends Context.Service<
|
|
|
183
184
|
"close",
|
|
184
185
|
() => {
|
|
185
186
|
try {
|
|
186
|
-
|
|
187
|
+
retireNow();
|
|
187
188
|
} catch (cause) {
|
|
188
189
|
retirementFailure ??= browserFailure(disconnectOperation, cause);
|
|
189
190
|
}
|
|
@@ -191,7 +192,8 @@ export class BrowserRunBinding extends Context.Service<
|
|
|
191
192
|
{ once: true },
|
|
192
193
|
);
|
|
193
194
|
|
|
194
|
-
|
|
195
|
+
// Attaching must not replace the viewport owned by the host or another viewer.
|
|
196
|
+
return await puppeteer.connect({ transport, defaultViewport: null });
|
|
195
197
|
} catch (cause) {
|
|
196
198
|
try {
|
|
197
199
|
retireNow();
|