@yielded/agent-platform-cloudflare 0.1.0-beta.173 → 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-BMEAZo3p.mjs → Alarm-B44efvB1.mjs} +162 -103
- package/dist/Alarm-B44efvB1.mjs.map +1 -0
- package/dist/Alarm.d.mts +2 -0
- package/dist/Alarm.mjs +1 -1
- package/dist/BrowserRestCapture.mjs.map +1 -1
- package/dist/BrowserRestCrawl.mjs.map +1 -1
- package/dist/{BrowserSession-Bx0V1-6_.d.mts → BrowserSession-Cojp7nth.d.mts} +3 -2
- package/dist/{BrowserSession-BfNmyayH.mjs → BrowserSession-aovMjfL4.mjs} +14 -14
- package/dist/BrowserSession-aovMjfL4.mjs.map +1 -0
- package/dist/BrowserSession.d.mts +1 -1
- package/dist/BrowserSession.mjs +2 -2
- package/dist/BrowserUse.d.mts +40 -64
- package/dist/BrowserUse.mjs +223 -2
- package/dist/BrowserUse.mjs.map +1 -0
- package/dist/CloudflareBrowser-Vt8qTdYW.mjs.map +1 -1
- package/dist/CloudflareCodeMode.mjs.map +1 -1
- package/dist/CloudflareScheduling.mjs +9 -9
- package/dist/CloudflareScheduling.mjs.map +1 -1
- package/dist/CloudflareSubscriptions.mjs +15 -15
- package/dist/CloudflareSubscriptions.mjs.map +1 -1
- package/dist/{CloudflareThreadClient-DgZVrZzF.mjs → CloudflareThreadClient-Ai3MTs3X.mjs} +43 -40
- package/dist/CloudflareThreadClient-Ai3MTs3X.mjs.map +1 -0
- package/dist/CloudflareThreadClient.mjs +1 -1
- package/dist/{InteractiveBrowser-LdKzQeLR.mjs → InteractiveBrowser-BHNlwjZL.mjs} +40 -31
- package/dist/InteractiveBrowser-BHNlwjZL.mjs.map +1 -0
- package/dist/InteractiveBrowser.mjs +1 -1
- package/dist/{ThreadObject-CRbLbdXM.d.mts → ThreadObject-CxNlhoPL.d.mts} +3 -3
- package/dist/{ThreadObject-CGIaz6Jv.mjs → ThreadObject-DGvlHWYi.mjs} +50 -52
- package/dist/ThreadObject-DGvlHWYi.mjs.map +1 -0
- package/dist/ThreadObject.d.mts +1 -1
- package/dist/ThreadObject.mjs +1 -1
- package/dist/WakeScheduler.d.mts +1 -1
- package/dist/WakeScheduler.mjs +8 -3
- package/dist/WakeScheduler.mjs.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +5 -5
- package/dist/{prepared-admission-CSdzFM_v.mjs → prepared-admission-DYdffQ8K.mjs} +2 -2
- package/dist/{prepared-admission-CSdzFM_v.mjs.map → prepared-admission-DYdffQ8K.mjs.map} +1 -1
- package/package.json +1 -1
- package/src/Alarm.ts +293 -155
- package/src/BrowserRestCapture.ts +1 -0
- package/src/BrowserRestCrawl.ts +1 -0
- package/src/BrowserSession.ts +3 -2
- package/src/BrowserUse.ts +287 -1504
- package/src/CloudflareCodeMode.ts +6 -0
- package/src/CloudflareScheduling.ts +103 -103
- package/src/CloudflareSubscriptions.ts +192 -199
- package/src/CloudflareThreadClient.ts +258 -269
- package/src/InteractiveBrowser.ts +43 -17
- package/src/ThreadObject.ts +51 -45
- package/src/WakeScheduler.ts +12 -9
- package/src/internal/browser-binding.ts +8 -0
- package/src/internal/browser-credentials.ts +18 -13
- package/src/internal/browser-file-selection.ts +13 -2
- package/src/internal/browser-quick-action.ts +1 -0
- package/src/internal/browser-session-lifecycle.ts +32 -36
- package/src/internal/due-queue.ts +38 -19
- package/src/internal/layers.ts +108 -87
- package/src/internal/message-delivery.ts +61 -37
- package/dist/Alarm-BMEAZo3p.mjs.map +0 -1
- package/dist/BrowserSession-BfNmyayH.mjs.map +0 -1
- package/dist/BrowserUse-Bc2JdLW2.mjs +0 -1528
- package/dist/BrowserUse-Bc2JdLW2.mjs.map +0 -1
- package/dist/CloudflareThreadClient-DgZVrZzF.mjs.map +0 -1
- package/dist/InteractiveBrowser-LdKzQeLR.mjs.map +0 -1
- package/dist/ThreadObject-CGIaz6Jv.mjs.map +0 -1
- package/dist/page-function-CmUCcyNP.mjs +0 -11
- package/dist/page-function-CmUCcyNP.mjs.map +0 -1
- package/src/internal/browser-dom.ts +0 -886
- package/src/internal/browser-jev-dom.ts +0 -362
|
@@ -49,7 +49,7 @@ export class BrowserRunSessionLifecycle extends Context.Service<
|
|
|
49
49
|
!Schema.is(Account)(options.accountId) ||
|
|
50
50
|
Redacted.value(options.apiToken).length === 0
|
|
51
51
|
) {
|
|
52
|
-
return yield*
|
|
52
|
+
return yield* BrowserRunCleanupError.make({ reason: "configuration" });
|
|
53
53
|
}
|
|
54
54
|
const client = yield* HttpClient.HttpClient;
|
|
55
55
|
|
|
@@ -65,39 +65,38 @@ export class BrowserRunSessionLifecycle extends Context.Service<
|
|
|
65
65
|
.pipe(
|
|
66
66
|
// workerd supports manual/follow, not error. Reject every redirect below.
|
|
67
67
|
Effect.provideService(FetchHttpClient.RequestInit, { redirect: "manual" }),
|
|
68
|
-
Effect.mapError(
|
|
69
|
-
(
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}),
|
|
68
|
+
Effect.mapError((cause) =>
|
|
69
|
+
BrowserRunCleanupError.make({
|
|
70
|
+
reason: "provider",
|
|
71
|
+
cause: browserFailure("browser.close.request", cause),
|
|
72
|
+
}),
|
|
74
73
|
),
|
|
75
74
|
);
|
|
76
75
|
|
|
77
76
|
if (response.status === 401 || response.status === 403)
|
|
78
|
-
return yield*
|
|
77
|
+
return yield* BrowserRunCleanupError.make({
|
|
79
78
|
reason: "authorization",
|
|
80
79
|
status: response.status,
|
|
81
80
|
});
|
|
82
81
|
if (response.status === 429)
|
|
83
|
-
return yield*
|
|
82
|
+
return yield* BrowserRunCleanupError.make({
|
|
84
83
|
reason: "rate-limited",
|
|
85
84
|
status: response.status,
|
|
86
85
|
});
|
|
87
86
|
if (response.status !== 200 && response.status !== 404)
|
|
88
|
-
return yield*
|
|
87
|
+
return yield* BrowserRunCleanupError.make({
|
|
89
88
|
reason: "provider",
|
|
90
89
|
status: response.status,
|
|
91
90
|
});
|
|
92
91
|
if (response.headers["content-type"]?.split(";", 1)[0]?.trim() !== "application/json")
|
|
93
|
-
return yield*
|
|
92
|
+
return yield* BrowserRunCleanupError.make({ reason: "malformed" });
|
|
94
93
|
|
|
95
94
|
const bytes = yield* Stream.runFoldEffect(
|
|
96
95
|
response.stream,
|
|
97
96
|
() => new Uint8Array(),
|
|
98
97
|
(body, chunk) => {
|
|
99
98
|
if (body.byteLength + chunk.byteLength > 16_384)
|
|
100
|
-
return Effect.fail(
|
|
99
|
+
return Effect.fail(BrowserRunCleanupError.make({ reason: "malformed" }));
|
|
101
100
|
const combined = new Uint8Array(body.byteLength + chunk.byteLength);
|
|
102
101
|
|
|
103
102
|
combined.set(body);
|
|
@@ -106,19 +105,18 @@ export class BrowserRunSessionLifecycle extends Context.Service<
|
|
|
106
105
|
return Effect.succeed(combined);
|
|
107
106
|
},
|
|
108
107
|
).pipe(
|
|
109
|
-
Effect.mapError(
|
|
110
|
-
(
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}),
|
|
108
|
+
Effect.mapError((cause) =>
|
|
109
|
+
BrowserRunCleanupError.make({
|
|
110
|
+
reason: "malformed",
|
|
111
|
+
cause: browserFailure("browser.close.response", cause),
|
|
112
|
+
}),
|
|
115
113
|
),
|
|
116
114
|
);
|
|
117
115
|
|
|
118
116
|
const body = yield* Effect.try({
|
|
119
117
|
try: () => new TextDecoder("utf-8", { fatal: true, ignoreBOM: false }).decode(bytes),
|
|
120
118
|
catch: (cause) =>
|
|
121
|
-
|
|
119
|
+
BrowserRunCleanupError.make({
|
|
122
120
|
reason: "malformed",
|
|
123
121
|
cause: browserFailure("browser.close.decode", cause),
|
|
124
122
|
}),
|
|
@@ -131,16 +129,15 @@ export class BrowserRunSessionLifecycle extends Context.Service<
|
|
|
131
129
|
|
|
132
130
|
if (Option.isSome(absent)) return true;
|
|
133
131
|
|
|
134
|
-
return yield*
|
|
132
|
+
return yield* BrowserRunCleanupError.make({ reason: "malformed" });
|
|
135
133
|
}
|
|
136
134
|
if (method === "DELETE") {
|
|
137
135
|
const result = yield* Schema.decodeEffect(Schema.fromJsonString(Closed))(body).pipe(
|
|
138
|
-
Effect.mapError(
|
|
139
|
-
(
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}),
|
|
136
|
+
Effect.mapError((cause) =>
|
|
137
|
+
BrowserRunCleanupError.make({
|
|
138
|
+
reason: "malformed",
|
|
139
|
+
cause: browserFailure("browser.close.response", cause),
|
|
140
|
+
}),
|
|
144
141
|
),
|
|
145
142
|
);
|
|
146
143
|
|
|
@@ -148,17 +145,16 @@ export class BrowserRunSessionLifecycle extends Context.Service<
|
|
|
148
145
|
}
|
|
149
146
|
|
|
150
147
|
const result = yield* Schema.decodeEffect(Schema.fromJsonString(Metadata))(body).pipe(
|
|
151
|
-
Effect.mapError(
|
|
152
|
-
(
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}),
|
|
148
|
+
Effect.mapError((cause) =>
|
|
149
|
+
BrowserRunCleanupError.make({
|
|
150
|
+
reason: "malformed",
|
|
151
|
+
cause: browserFailure("browser.close.response", cause),
|
|
152
|
+
}),
|
|
157
153
|
),
|
|
158
154
|
);
|
|
159
155
|
|
|
160
156
|
if (result.sessionId !== sessionId)
|
|
161
|
-
return yield*
|
|
157
|
+
return yield* BrowserRunCleanupError.make({ reason: "malformed" });
|
|
162
158
|
|
|
163
159
|
return result.endTime !== undefined && result.endTime > 0;
|
|
164
160
|
});
|
|
@@ -166,7 +162,7 @@ export class BrowserRunSessionLifecycle extends Context.Service<
|
|
|
166
162
|
const close = Effect.fnUntraced(
|
|
167
163
|
function* (sessionId: Redacted.Redacted<string>) {
|
|
168
164
|
const id = yield* Schema.decodeEffect(Identity)(Redacted.value(sessionId)).pipe(
|
|
169
|
-
Effect.mapError(() =>
|
|
165
|
+
Effect.mapError(() => BrowserRunCleanupError.make({ reason: "configuration" })),
|
|
170
166
|
);
|
|
171
167
|
|
|
172
168
|
if (yield* request("DELETE", id)) return;
|
|
@@ -174,11 +170,11 @@ export class BrowserRunSessionLifecycle extends Context.Service<
|
|
|
174
170
|
if (yield* request("GET", id)) return;
|
|
175
171
|
}
|
|
176
172
|
|
|
177
|
-
return yield*
|
|
173
|
+
return yield* BrowserRunCleanupError.make({ reason: "pending" });
|
|
178
174
|
},
|
|
179
175
|
Effect.timeoutOrElse({
|
|
180
176
|
duration: "10 seconds",
|
|
181
|
-
orElse: () => Effect.fail(
|
|
177
|
+
orElse: () => Effect.fail(BrowserRunCleanupError.make({ reason: "timeout" })),
|
|
182
178
|
}),
|
|
183
179
|
Effect.withTracerEnabled(false),
|
|
184
180
|
);
|
|
@@ -139,10 +139,9 @@ export const make = (storage: DurableObjectStorage) => {
|
|
|
139
139
|
|
|
140
140
|
// Nine bindings per lane stay below Durable Object SQLite's 100-parameter limit.
|
|
141
141
|
const write = (changes: ReadonlyArray<Change>) => {
|
|
142
|
-
const rows =
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
`WITH changes (id, revision, dueAt, stalls, progressKey, notBefore, state, reported, expected_revision) AS (
|
|
142
|
+
const rows = sql
|
|
143
|
+
.exec(
|
|
144
|
+
`WITH changes (id, revision, dueAt, stalls, progressKey, notBefore, state, reported, expected_revision) AS (
|
|
146
145
|
VALUES ${changes.map(() => "(?, ?, ?, ?, ?, ?, ?, ?, ?)").join(", ")}
|
|
147
146
|
) INSERT INTO platform_cloudflare_due_queue
|
|
148
147
|
(id, revision, dueAt, stalls, progressKey, notBefore, state, reported)
|
|
@@ -152,24 +151,24 @@ export const make = (storage: DurableObjectStorage) => {
|
|
|
152
151
|
notBefore = excluded.notBefore, state = excluded.state, reported = excluded.reported
|
|
153
152
|
WHERE platform_cloudflare_due_queue.revision = (
|
|
154
153
|
SELECT expected_revision FROM changes WHERE changes.id = excluded.id
|
|
155
|
-
) RETURNING
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
);
|
|
154
|
+
) RETURNING 1`,
|
|
155
|
+
...changes.flatMap(({ before, after }) => [
|
|
156
|
+
after.id,
|
|
157
|
+
after.revision,
|
|
158
|
+
after.dueAt,
|
|
159
|
+
after.stalls,
|
|
160
|
+
after.progressKey,
|
|
161
|
+
after.notBefore,
|
|
162
|
+
after.state,
|
|
163
|
+
after.reported,
|
|
164
|
+
before?.revision ?? -1,
|
|
165
|
+
]),
|
|
166
|
+
)
|
|
167
|
+
.toArray();
|
|
170
168
|
|
|
171
169
|
// Partial revision acceptance must reject the enclosing source transaction.
|
|
172
170
|
if (rows.length !== changes.length)
|
|
171
|
+
// oxlint-disable-next-line effect/no-untyped-throw, effect/no-native-error -- Native storage transaction must roll back partial revision acceptance; its Effect adapter maps the cause.
|
|
173
172
|
throw new Error("Maintenance due revision changed during transaction");
|
|
174
173
|
};
|
|
175
174
|
|
|
@@ -204,6 +203,7 @@ export const make = (storage: DurableObjectStorage) => {
|
|
|
204
203
|
return result;
|
|
205
204
|
} catch (cause) {
|
|
206
205
|
if (frame !== undefined) finish(frame, false);
|
|
206
|
+
// oxlint-disable-next-line effect/no-untyped-throw -- Native transaction Promise must reject with the original failure after invalidating speculative state.
|
|
207
207
|
throw cause;
|
|
208
208
|
}
|
|
209
209
|
};
|
|
@@ -302,6 +302,24 @@ export const make = (storage: DurableObjectStorage) => {
|
|
|
302
302
|
);
|
|
303
303
|
};
|
|
304
304
|
|
|
305
|
+
/** Native checkpoints fence the generation, not each source commit. Once progress
|
|
306
|
+
* resets a claimed lane, further native commits owe the same recovery opportunity.
|
|
307
|
+
* A new claim, deadline, or retry budget makes the next commit advance it again. */
|
|
308
|
+
const progressNative = (dueAt: number) => {
|
|
309
|
+
const row = read().find((row) => row.id === Native);
|
|
310
|
+
|
|
311
|
+
if (
|
|
312
|
+
row?.state === "pending" &&
|
|
313
|
+
row.dueAt !== null &&
|
|
314
|
+
row.dueAt <= dueAt &&
|
|
315
|
+
row.stalls === 0 &&
|
|
316
|
+
row.notBefore === 0 &&
|
|
317
|
+
row.reported === 0
|
|
318
|
+
)
|
|
319
|
+
return;
|
|
320
|
+
dirty(Native, dueAt, true);
|
|
321
|
+
};
|
|
322
|
+
|
|
305
323
|
/** A strictly increasing source cursor; old or repeated notices cannot renew a budget. */
|
|
306
324
|
const progress = (id: string, dueAt: number, cursor: bigint) => {
|
|
307
325
|
Schema.decodeSync(Schema.BigInt.check(Schema.isGreaterThanOrEqualToBigInt(0n)))(cursor);
|
|
@@ -416,6 +434,7 @@ export const make = (storage: DurableObjectStorage) => {
|
|
|
416
434
|
transaction,
|
|
417
435
|
read,
|
|
418
436
|
dirty,
|
|
437
|
+
progressNative,
|
|
419
438
|
progress,
|
|
420
439
|
register,
|
|
421
440
|
claim,
|
package/src/internal/layers.ts
CHANGED
|
@@ -134,7 +134,7 @@ import * as DueQueue from "./due-queue.ts";
|
|
|
134
134
|
import { LiveTextHub } from "./live-text.ts";
|
|
135
135
|
import {
|
|
136
136
|
guardedMessageDeliveryStoreLayer,
|
|
137
|
-
|
|
137
|
+
lazyThreadMessageDeliveryLayer,
|
|
138
138
|
} from "./message-delivery.ts";
|
|
139
139
|
import { cloudflarePreparedInputAdmissionLayer } from "./prepared-admission.ts";
|
|
140
140
|
import { ProgressWaitRegistry } from "./progress-wait.ts";
|
|
@@ -625,10 +625,14 @@ const sharedLayer = <A, E, R, PE = never, PR = never>(
|
|
|
625
625
|
verifyOnOpen: config.verifyOnOpen,
|
|
626
626
|
};
|
|
627
627
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
Layer.
|
|
628
|
+
// Memoize these shared compositions within this Object's build, so repeated
|
|
629
|
+
// consumers reuse their contexts as well as the scoped leaf services.
|
|
630
|
+
const infrastructure = Layer.suspend(() =>
|
|
631
|
+
Layer.mergeAll(
|
|
632
|
+
options.lifecyclePublication === undefined ? Layer.empty : lifecyclePublicationLayer,
|
|
633
|
+
storageConfigLayer(storageOptions),
|
|
634
|
+
Layer.effect(SqlClient)(SqlClient),
|
|
635
|
+
),
|
|
632
636
|
);
|
|
633
637
|
|
|
634
638
|
// Preserve the database owner's reader gate and rollback invalidators, while
|
|
@@ -638,6 +642,7 @@ const sharedLayer = <A, E, R, PE = never, PR = never>(
|
|
|
638
642
|
const owner = yield* SqlStorageOwner;
|
|
639
643
|
const mutations = yield* ThreadMutationGate;
|
|
640
644
|
|
|
645
|
+
// oxlint-disable-next-line effect/no-native-error -- Missing host SQL ownership is an assembly defect, not a recoverable workflow failure.
|
|
641
646
|
if (owner === undefined) return yield* Effect.die(new Error("SQL owner unavailable"));
|
|
642
647
|
|
|
643
648
|
return {
|
|
@@ -660,7 +665,7 @@ const sharedLayer = <A, E, R, PE = never, PR = never>(
|
|
|
660
665
|
}),
|
|
661
666
|
).pipe(Layer.provide(sqlOwnerLayer));
|
|
662
667
|
|
|
663
|
-
const localInfrastructure = Layer.mergeAll(infrastructure, sourceOwner);
|
|
668
|
+
const localInfrastructure = Layer.suspend(() => Layer.mergeAll(infrastructure, sourceOwner));
|
|
664
669
|
|
|
665
670
|
const sourceProgress = Layer.effect(SqlStorageProgress)(
|
|
666
671
|
Effect.gen(function* () {
|
|
@@ -708,41 +713,57 @@ const sharedLayer = <A, E, R, PE = never, PR = never>(
|
|
|
708
713
|
|
|
709
714
|
// The same local ports serve routed decorators and owner-side RPC execution.
|
|
710
715
|
// The RPC executor must never receive routed ports and bounce requests between Objects.
|
|
711
|
-
const rawLocalPorts = Layer.
|
|
712
|
-
Layer.
|
|
713
|
-
|
|
716
|
+
const rawLocalPorts = Layer.suspend(() =>
|
|
717
|
+
Layer.mergeAll(threadStoreLayer, submissionLedgerLayer).pipe(
|
|
718
|
+
Layer.provide(localInfrastructure),
|
|
719
|
+
Layer.provide(sourceProgress),
|
|
720
|
+
),
|
|
721
|
+
);
|
|
722
|
+
|
|
723
|
+
const base = Layer.suspend(() =>
|
|
724
|
+
Layer.mergeAll(DurableAlarmService.layer, ProgressWaitRegistry.layer),
|
|
714
725
|
);
|
|
715
726
|
|
|
716
|
-
const base = Layer.mergeAll(DurableAlarmService.layer, ProgressWaitRegistry.layer);
|
|
717
727
|
const wakes = cloudflareWakeSchedulerLayer.pipe(Layer.provide(base));
|
|
718
728
|
|
|
719
|
-
const messageStore =
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
729
|
+
const messageStore = Layer.suspend(() =>
|
|
730
|
+
guardedMessageDeliveryStoreLayer.pipe(
|
|
731
|
+
Layer.provide(
|
|
732
|
+
doMessageDeliveryStoreLayer().pipe(
|
|
733
|
+
Layer.provide(localInfrastructure),
|
|
734
|
+
Layer.provide(sourceProgress),
|
|
735
|
+
),
|
|
724
736
|
),
|
|
737
|
+
Layer.provide(wakes),
|
|
725
738
|
),
|
|
726
|
-
Layer.provide(wakes),
|
|
727
739
|
);
|
|
728
740
|
|
|
729
|
-
const messageRecovery =
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
741
|
+
const messageRecovery = Layer.suspend(() =>
|
|
742
|
+
lazyThreadMessageDeliveryLayer(
|
|
743
|
+
// Four parallel attempts per wave. Native completion stops new wake-driven waves;
|
|
744
|
+
// retained retry deadlines schedule future alarms.
|
|
745
|
+
MessageDeliveryDriver.layer({ batchSize: 4, concurrency: 4 }).pipe(
|
|
746
|
+
Layer.provide(cloudflarePreparedInputAdmissionLayer),
|
|
747
|
+
Layer.provide(CloudflareThreadClient.layer),
|
|
748
|
+
Layer.provide(messageStore),
|
|
749
|
+
Layer.provide(wakes),
|
|
750
|
+
),
|
|
751
|
+
).pipe(Layer.provide(messageStore)),
|
|
737
752
|
);
|
|
738
753
|
|
|
739
|
-
const publication =
|
|
740
|
-
|
|
741
|
-
|
|
754
|
+
const publication =
|
|
755
|
+
options.publication === undefined
|
|
756
|
+
? ThreadPublication.layer
|
|
757
|
+
: options.publication.pipe(
|
|
758
|
+
Layer.provide(Layer.mergeAll(rawLocalPorts, Layer.effect(SqlClient)(SqlClient))),
|
|
759
|
+
);
|
|
742
760
|
|
|
743
|
-
const projection =
|
|
744
|
-
|
|
745
|
-
|
|
761
|
+
const projection =
|
|
762
|
+
options.projection === undefined
|
|
763
|
+
? ThreadProjectionMaintenance.layer
|
|
764
|
+
: options.projection.pipe(
|
|
765
|
+
Layer.provide(Layer.mergeAll(rawLocalPorts, Layer.effect(SqlClient)(SqlClient))),
|
|
766
|
+
);
|
|
746
767
|
|
|
747
768
|
const localPorts =
|
|
748
769
|
options.publication === undefined &&
|
|
@@ -889,71 +910,71 @@ const sharedLayer = <A, E, R, PE = never, PR = never>(
|
|
|
889
910
|
return Context.make(ThreadStore, observedStore).pipe(
|
|
890
911
|
Context.add(StorageOwner, storageOwner),
|
|
891
912
|
Context.add(SettlementPublisher, {
|
|
892
|
-
publish: (input)
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
.
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
.
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
.
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
),
|
|
922
|
-
),
|
|
913
|
+
publish: Effect.fnUntraced(function* (input) {
|
|
914
|
+
const { request } = yield* validatePublication(input);
|
|
915
|
+
|
|
916
|
+
return yield* mutations
|
|
917
|
+
.withMutation(
|
|
918
|
+
sourceCommits
|
|
919
|
+
.withPermit(
|
|
920
|
+
settlementPublisher.publish(request).pipe(
|
|
921
|
+
Effect.tap((result) =>
|
|
922
|
+
result.replayed
|
|
923
|
+
? Effect.void
|
|
924
|
+
: index
|
|
925
|
+
.applyCommitted(
|
|
926
|
+
request.append,
|
|
927
|
+
AppendResult.make({
|
|
928
|
+
firstSequence: result.tailSequence,
|
|
929
|
+
lastSequence: result.tailSequence,
|
|
930
|
+
|
|
931
|
+
replayed: false,
|
|
932
|
+
}),
|
|
933
|
+
)
|
|
934
|
+
.pipe(
|
|
935
|
+
Effect.catchCause((cause) =>
|
|
936
|
+
Cause.hasInterrupts(cause)
|
|
937
|
+
? Effect.interrupt
|
|
938
|
+
: Effect.logError(
|
|
939
|
+
"Thread projection deferred after settlement publication",
|
|
940
|
+
cause,
|
|
941
|
+
),
|
|
923
942
|
),
|
|
924
|
-
|
|
943
|
+
),
|
|
925
944
|
),
|
|
926
|
-
)
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
),
|
|
945
|
-
)
|
|
946
|
-
|
|
945
|
+
),
|
|
946
|
+
)
|
|
947
|
+
.pipe(Effect.tap(() => afterCommit)),
|
|
948
|
+
{
|
|
949
|
+
lanes: [
|
|
950
|
+
...(options.projection === undefined ? [] : [DueQueue.Projection]),
|
|
951
|
+
...(options.lifecyclePublication === undefined
|
|
952
|
+
? []
|
|
953
|
+
: [DueQueue.Lifecycle]),
|
|
954
|
+
],
|
|
955
|
+
},
|
|
956
|
+
)
|
|
957
|
+
.pipe(
|
|
958
|
+
Effect.catchTag("DurableAlarmError", (cause) =>
|
|
959
|
+
ThreadStoreError.make({
|
|
960
|
+
operation: "prearm settlement publication",
|
|
961
|
+
message: cause.message,
|
|
962
|
+
cause,
|
|
963
|
+
}),
|
|
964
|
+
),
|
|
965
|
+
);
|
|
966
|
+
}),
|
|
947
967
|
}),
|
|
948
968
|
Context.add(SubmissionLedger, {
|
|
949
969
|
...ledger,
|
|
950
|
-
...(options.hostLanesForMutation === undefined
|
|
970
|
+
...(options.hostLanesForMutation === undefined &&
|
|
971
|
+
options.lifecyclePublication === undefined
|
|
951
972
|
? {}
|
|
952
973
|
: {
|
|
953
|
-
admit: (
|
|
974
|
+
admit: (...args: Parameters<SubmissionLedger["Service"]["admit"]>) =>
|
|
954
975
|
observeMutation(
|
|
955
|
-
{ _tag: "Admission", request },
|
|
956
|
-
ledger.admit(
|
|
976
|
+
{ _tag: "Admission", request: args[0] },
|
|
977
|
+
ledger.admit(...args),
|
|
957
978
|
false,
|
|
958
979
|
),
|
|
959
980
|
}),
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
MessageDeliveryStore,
|
|
6
6
|
} from "@yielded/agent/message-delivery";
|
|
7
7
|
import { WakeScheduler } from "@yielded/agent/wake-scheduler";
|
|
8
|
-
import { Clock, Context, Effect, Layer, Option } from "effect";
|
|
8
|
+
import { Clock, Context, Duration, Effect, Exit, Layer, Option } from "effect";
|
|
9
9
|
|
|
10
10
|
import { DurableAlarmError, ThreadMessageDelivery, ThreadMutationGate } from "../Alarm.ts";
|
|
11
11
|
import { ThreadObjectPlacement } from "../CloudflareBindings.ts";
|
|
@@ -76,45 +76,69 @@ export const guardedMessageDeliveryStoreLayer = Layer.effect(
|
|
|
76
76
|
}),
|
|
77
77
|
);
|
|
78
78
|
|
|
79
|
-
/**
|
|
80
|
-
export const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
79
|
+
/** Build the driver in the Object's Scope only when retained delivery work is due. */
|
|
80
|
+
export const lazyThreadMessageDeliveryLayer = <E, R>(
|
|
81
|
+
driver: Layer.Layer<MessageDeliveryDriver, E, R>,
|
|
82
|
+
) =>
|
|
83
|
+
Layer.fromBuildMemo((memoMap, scope) =>
|
|
84
|
+
Effect.gen(function* () {
|
|
85
|
+
const context = yield* Effect.context<R>();
|
|
86
|
+
const store = yield* MessageDeliveryStore;
|
|
84
87
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
// Failed builds release their Layer scopes; let a later wave build again.
|
|
89
|
+
const service = yield* Effect.cachedWithTTL(
|
|
90
|
+
Layer.buildWithMemoMap(driver, memoMap, scope).pipe(
|
|
91
|
+
Effect.provide(context),
|
|
92
|
+
Effect.map(Context.get(MessageDeliveryDriver)),
|
|
93
|
+
Effect.mapError((cause) =>
|
|
94
|
+
DurableAlarmError.make({
|
|
95
|
+
operation: "prepare message delivery",
|
|
96
|
+
message: "Durable message recovery remains pending",
|
|
97
|
+
cause,
|
|
98
|
+
}),
|
|
99
|
+
),
|
|
100
|
+
),
|
|
101
|
+
(exit) => (Exit.isSuccess(exit) ? Duration.infinity : Duration.zero),
|
|
102
|
+
);
|
|
90
103
|
|
|
91
|
-
|
|
92
|
-
|
|
104
|
+
const failure = (operation: string) => () =>
|
|
105
|
+
DurableAlarmError.make({
|
|
106
|
+
operation,
|
|
107
|
+
message: "Durable message recovery remains pending",
|
|
108
|
+
});
|
|
93
109
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
// The assembled driver has four permits: a wave is one parallel attempt window.
|
|
97
|
-
const keys = yield* store.due(yield* Clock.currentTimeMillis, 4);
|
|
98
|
-
const records = yield* Effect.forEach(keys, (key) => store.get(key));
|
|
110
|
+
const prepare = Effect.gen(function* () {
|
|
111
|
+
const deadline = yield* store.nextDeadline();
|
|
99
112
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
)
|
|
105
|
-
run: Effect.forEach(keys, (key) => driver.process(key), {
|
|
106
|
-
concurrency: 4,
|
|
107
|
-
discard: true,
|
|
108
|
-
}).pipe(
|
|
109
|
-
Effect.andThen(store.nextDeadline()),
|
|
110
|
-
Effect.map(Option.fromNullishOr),
|
|
111
|
-
Effect.mapError(failure("dispatch message delivery")),
|
|
112
|
-
),
|
|
113
|
-
};
|
|
114
|
-
}).pipe(Effect.mapError(failure("prepare message delivery")));
|
|
113
|
+
if (deadline === null || deadline > (yield* Clock.currentTimeMillis))
|
|
114
|
+
return { timeoutMillis: 1, run: Effect.succeed(Option.fromNullishOr(deadline)) };
|
|
115
|
+
// The assembled driver has four permits: a wave is one parallel attempt window.
|
|
116
|
+
const keys = yield* store.due(yield* Clock.currentTimeMillis, 4);
|
|
117
|
+
const records = yield* Effect.forEach(keys, (key) => store.get(key));
|
|
115
118
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
119
|
+
return {
|
|
120
|
+
timeoutMillis: Math.max(
|
|
121
|
+
1,
|
|
122
|
+
...records.map((record) => record?.policy.attemptTimeoutMillis ?? 1),
|
|
123
|
+
),
|
|
124
|
+
run: Effect.flatMap(service, (driver) =>
|
|
125
|
+
Effect.forEach(keys, (key) => driver.process(key), {
|
|
126
|
+
concurrency: 4,
|
|
127
|
+
discard: true,
|
|
128
|
+
}),
|
|
129
|
+
).pipe(
|
|
130
|
+
Effect.andThen(store.nextDeadline()),
|
|
131
|
+
Effect.map(Option.fromNullishOr),
|
|
132
|
+
Effect.mapError(failure("dispatch message delivery")),
|
|
133
|
+
),
|
|
134
|
+
};
|
|
135
|
+
}).pipe(Effect.mapError(failure("prepare message delivery")));
|
|
136
|
+
|
|
137
|
+
return Context.make(ThreadMessageDelivery, { prepare });
|
|
138
|
+
}),
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
/** Message progress shares the native alarm slot without blocking source runtime work. */
|
|
142
|
+
export const threadMessageDeliveryLayer = lazyThreadMessageDeliveryLayer(
|
|
143
|
+
Layer.effect(MessageDeliveryDriver, MessageDeliveryDriver),
|
|
120
144
|
);
|