@effect-agent/platform-cloudflare 0.1.0-beta.6 → 0.1.0-beta.60
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 +168 -0
- package/dist/Alarm.mjs +415 -0
- package/dist/Alarm.mjs.map +1 -0
- package/dist/BrowserRestCapture.d.mts +35 -0
- package/dist/BrowserRestCapture.mjs +238 -0
- package/dist/BrowserRestCapture.mjs.map +1 -0
- package/dist/BrowserRestCrawl.d.mts +17 -0
- package/dist/BrowserRestCrawl.mjs +244 -0
- package/dist/BrowserRestCrawl.mjs.map +1 -0
- package/dist/CloudflareBindings.d.mts +105 -0
- package/dist/CloudflareBindings.mjs +98 -0
- package/dist/CloudflareBindings.mjs.map +1 -0
- package/dist/CloudflareBrowser-Bj22nNUT.mjs +466 -0
- package/dist/CloudflareBrowser-Bj22nNUT.mjs.map +1 -0
- package/dist/CloudflareBrowser-DJq53niJ.d.mts +80 -0
- package/dist/CloudflareBrowser.d.mts +2 -0
- package/dist/CloudflareBrowser.mjs +2 -0
- package/dist/CloudflareCodeMode.d.mts +44 -0
- package/dist/CloudflareCodeMode.mjs +611 -0
- package/dist/CloudflareCodeMode.mjs.map +1 -0
- package/dist/CloudflareConfig-f3CqTel1.d.mts +101 -0
- package/dist/CloudflareConfig.d.mts +2 -0
- package/dist/CloudflareConfig.mjs +122 -0
- package/dist/CloudflareConfig.mjs.map +1 -0
- package/dist/CloudflareMemory.d.mts +140 -0
- package/dist/CloudflareMemory.mjs +168 -0
- package/dist/CloudflareMemory.mjs.map +1 -0
- package/dist/CloudflareScheduling.d.mts +52 -0
- package/dist/CloudflareScheduling.mjs +389 -0
- package/dist/CloudflareScheduling.mjs.map +1 -0
- package/dist/CloudflareSubscriptions.d.mts +87 -0
- package/dist/CloudflareSubscriptions.mjs +524 -0
- package/dist/CloudflareSubscriptions.mjs.map +1 -0
- package/dist/CloudflareThreadClient.d.mts +1214 -0
- package/dist/CloudflareThreadClient.mjs +386 -0
- package/dist/CloudflareThreadClient.mjs.map +1 -0
- package/dist/InteractiveBrowser.d.mts +144 -0
- package/dist/InteractiveBrowser.mjs +1090 -0
- package/dist/InteractiveBrowser.mjs.map +1 -0
- package/dist/ProtectedBrowser.d.mts +65 -0
- package/dist/ProtectedBrowser.mjs +859 -0
- package/dist/ProtectedBrowser.mjs.map +1 -0
- package/dist/ThreadObject-CvnEnVQH.d.mts +772 -0
- package/dist/ThreadObject-s5w3V_co.mjs +675 -0
- package/dist/ThreadObject-s5w3V_co.mjs.map +1 -0
- package/dist/ThreadObject.d.mts +2 -0
- package/dist/ThreadObject.mjs +3 -0
- package/dist/WakeScheduler.d.mts +23 -0
- package/dist/WakeScheduler.mjs +60 -0
- package/dist/WakeScheduler.mjs.map +1 -0
- package/dist/boundary-BguazVkh.mjs +42 -0
- package/dist/boundary-BguazVkh.mjs.map +1 -0
- package/dist/browser-session-lifecycle-CUbVfEgP.mjs +84 -0
- package/dist/browser-session-lifecycle-CUbVfEgP.mjs.map +1 -0
- package/dist/browser-session-lifecycle-DJpqLXy_.d.mts +21 -0
- package/dist/index.d.mts +12 -1543
- package/dist/index.mjs +12 -1636
- package/dist/prepared-admission-G7N4DDqS.mjs +73 -0
- package/dist/prepared-admission-G7N4DDqS.mjs.map +1 -0
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/package.json +1 -53
- package/src/Alarm.ts +862 -0
- package/src/BrowserRestCapture.ts +477 -0
- package/src/BrowserRestCrawl.ts +540 -0
- package/src/CloudflareBindings.ts +171 -0
- package/src/CloudflareBrowser.ts +15 -0
- package/src/{code-mode-executor.ts → CloudflareCodeMode.ts} +367 -204
- package/src/{config.ts → CloudflareConfig.ts} +9 -8
- package/src/CloudflareMemory.ts +359 -0
- package/src/CloudflareScheduling.ts +733 -0
- package/src/CloudflareSubscriptions.ts +1045 -0
- package/src/CloudflareThreadClient.ts +807 -0
- package/src/InteractiveBrowser.ts +2357 -0
- package/src/ProtectedBrowser.ts +2 -0
- package/src/{conversation-object.ts → ThreadObject.ts} +423 -239
- package/src/{wake-scheduler.ts → WakeScheduler.ts} +23 -23
- package/src/index.ts +11 -23
- package/src/internal/boundary.ts +55 -0
- package/src/internal/browser-quick-action.ts +818 -0
- package/src/internal/browser-session-lifecycle.ts +160 -0
- package/src/internal/layers.ts +615 -0
- package/src/internal/message-delivery.ts +90 -0
- package/src/internal/prepared-admission.ts +116 -0
- package/src/internal/progress-wait.ts +121 -0
- package/src/internal/transport.ts +42 -0
- package/src/protected-browser/binding.ts +187 -0
- package/src/protected-browser/inspect-frame.ts +125 -0
- package/src/protected-browser/native.ts +429 -0
- package/src/protected-browser/policy.ts +719 -0
- package/dist/index.mjs.map +0 -1
- package/src/alarm.ts +0 -334
- package/src/bindings.ts +0 -145
- package/src/client.ts +0 -646
- package/src/layers.ts +0 -376
- package/src/transport.ts +0 -38
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { Duration, Effect, Layer, Option, Redacted, Ref, Schedule, Schema, Scope, Stream } from "effect";
|
|
2
|
+
import { SandboxImplementation } from "@effect-agent/sandbox/Sandbox";
|
|
3
|
+
import { HttpClient, HttpClientRequest } from "effect/unstable/http";
|
|
4
|
+
import { PageCrawl, PageCrawlLimitError, PageCrawlProtocolError, PageCrawlRateLimitedError, PageCrawlRecord, PageCrawlTerminalError } from "@effect-agent/sandbox/PageCrawl";
|
|
5
|
+
//#region src/BrowserRestCrawl.ts
|
|
6
|
+
/** Node-safe REST crawl implementation. It never exposes or persists provider job identity. */
|
|
7
|
+
const browserRestCrawlImplementation = SandboxImplementation.make({
|
|
8
|
+
isolation: "isolated",
|
|
9
|
+
identity: "cloudflare-browser-rest-crawl"
|
|
10
|
+
});
|
|
11
|
+
const API_ORIGIN = "https://api.cloudflare.com";
|
|
12
|
+
const POLL_INTERVAL = Duration.seconds(1);
|
|
13
|
+
const CANCEL_TIMEOUT = Duration.seconds(30);
|
|
14
|
+
const MAX_CONTROL_RESPONSE_BYTES = 65536;
|
|
15
|
+
const MAX_RESULTS_RESPONSE_BYTES = 12582912;
|
|
16
|
+
const MAX_DIAGNOSTIC_LENGTH = 8e3;
|
|
17
|
+
const MAX_CURSOR_LENGTH = 1024;
|
|
18
|
+
const MAX_RECORDS_PER_RESPONSE = 1e4;
|
|
19
|
+
const BoundedJobId = Schema.NonEmptyString.check(Schema.isMaxLength(256));
|
|
20
|
+
const BoundedCursorString = Schema.NonEmptyString.check(Schema.isMaxLength(MAX_CURSOR_LENGTH));
|
|
21
|
+
const ProviderCursor = Schema.Union([BoundedCursorString, Schema.Natural]);
|
|
22
|
+
const ProviderJobStatus = Schema.Literals([
|
|
23
|
+
"running",
|
|
24
|
+
"completed",
|
|
25
|
+
"errored",
|
|
26
|
+
"cancelled_by_user",
|
|
27
|
+
"cancelled_due_to_timeout",
|
|
28
|
+
"cancelled_due_to_limits"
|
|
29
|
+
]);
|
|
30
|
+
const ProviderResult = Schema.Struct({
|
|
31
|
+
id: BoundedJobId,
|
|
32
|
+
status: ProviderJobStatus,
|
|
33
|
+
browserSecondsUsed: Schema.optionalKey(Schema.Finite.check(Schema.isGreaterThanOrEqualTo(0))),
|
|
34
|
+
total: Schema.optionalKey(Schema.Natural),
|
|
35
|
+
finished: Schema.optionalKey(Schema.Natural),
|
|
36
|
+
skipped: Schema.optionalKey(Schema.Natural),
|
|
37
|
+
records: Schema.Array(PageCrawlRecord).check(Schema.isMaxLength(MAX_RECORDS_PER_RESPONSE)),
|
|
38
|
+
cursor: Schema.optionalKey(ProviderCursor)
|
|
39
|
+
});
|
|
40
|
+
const CreateEnvelope = Schema.Struct({
|
|
41
|
+
success: Schema.Literal(true),
|
|
42
|
+
result: BoundedJobId
|
|
43
|
+
});
|
|
44
|
+
const ResultEnvelope = Schema.Struct({
|
|
45
|
+
success: Schema.Literal(true),
|
|
46
|
+
result: ProviderResult
|
|
47
|
+
});
|
|
48
|
+
const DeleteEnvelope = Schema.Struct({
|
|
49
|
+
success: Schema.Literal(true),
|
|
50
|
+
result: Schema.Struct({
|
|
51
|
+
job_id: BoundedJobId,
|
|
52
|
+
message: Schema.String.check(Schema.isMaxLength(MAX_DIAGNOSTIC_LENGTH))
|
|
53
|
+
})
|
|
54
|
+
});
|
|
55
|
+
const boundedDiagnostic = (message) => message.slice(0, MAX_DIAGNOSTIC_LENGTH);
|
|
56
|
+
const privateCause = (value, apiToken) => {
|
|
57
|
+
const raw = boundedDiagnostic(String(value));
|
|
58
|
+
if (raw.length === 0) return void 0;
|
|
59
|
+
const token = Redacted.value(apiToken);
|
|
60
|
+
return new Error(token.length === 0 ? raw : raw.replaceAll(token, "[REDACTED]"));
|
|
61
|
+
};
|
|
62
|
+
const protocolError = (message, cause) => PageCrawlProtocolError.make({
|
|
63
|
+
implementation: browserRestCrawlImplementation,
|
|
64
|
+
message: boundedDiagnostic(message),
|
|
65
|
+
...cause === void 0 ? {} : { cause }
|
|
66
|
+
});
|
|
67
|
+
const limitError = (request, limit, observed) => {
|
|
68
|
+
const maximum = limit === "pages" ? request.limits.maxPages : limit === "page-bytes" ? request.limits.maxPageBytes : limit === "total-bytes" ? request.limits.maxTotalBytes : request.limits.deadlineMillis;
|
|
69
|
+
return PageCrawlLimitError.make({
|
|
70
|
+
implementation: browserRestCrawlImplementation,
|
|
71
|
+
limit,
|
|
72
|
+
maximum,
|
|
73
|
+
observed,
|
|
74
|
+
message: `The crawl exceeded its ${limit} limit`
|
|
75
|
+
});
|
|
76
|
+
};
|
|
77
|
+
const retryAfterMillis = (headers) => {
|
|
78
|
+
const seconds = Number(headers["retry-after"]);
|
|
79
|
+
if (!Number.isSafeInteger(seconds) || seconds < 0) return void 0;
|
|
80
|
+
const millis = seconds * 1e3;
|
|
81
|
+
return Number.isSafeInteger(millis) ? millis : void 0;
|
|
82
|
+
};
|
|
83
|
+
const isJsonResponse = (headers) => {
|
|
84
|
+
const contentType = headers["content-type"];
|
|
85
|
+
if (contentType === void 0) return false;
|
|
86
|
+
const mediaType = contentType.split(";", 1)[0]?.trim().toLowerCase();
|
|
87
|
+
return mediaType === "application/json" || mediaType?.endsWith("+json") === true;
|
|
88
|
+
};
|
|
89
|
+
const isQuotaMessage = (bodyText) => /time limit|daily|quota/i.test(bodyText);
|
|
90
|
+
const endpoint = (options, jobId) => `${API_ORIGIN}/client/v4/accounts/${encodeURIComponent(options.accountId)}/browser-rendering/crawl${jobId === void 0 ? "" : `/${encodeURIComponent(jobId)}`}`;
|
|
91
|
+
const readBoundedResponse = Effect.fn("BrowserRestCrawl.readBoundedResponse")(function* (response, maximum) {
|
|
92
|
+
const decoder = new TextDecoder("utf-8", {
|
|
93
|
+
fatal: true,
|
|
94
|
+
ignoreBOM: false
|
|
95
|
+
});
|
|
96
|
+
const state = yield* Stream.runFoldEffect(response.stream, () => ({
|
|
97
|
+
observed: 0,
|
|
98
|
+
text: ""
|
|
99
|
+
}), (current, chunk) => {
|
|
100
|
+
const observed = current.observed + chunk.byteLength;
|
|
101
|
+
if (observed > maximum) return Effect.fail(protocolError(`The Browser Run crawl response exceeded ${String(maximum)} transport bytes`));
|
|
102
|
+
return Effect.try({
|
|
103
|
+
try: () => ({
|
|
104
|
+
observed,
|
|
105
|
+
text: current.text + decoder.decode(chunk, { stream: true })
|
|
106
|
+
}),
|
|
107
|
+
catch: (cause) => protocolError("Decoding the Browser Run crawl response failed", cause)
|
|
108
|
+
});
|
|
109
|
+
}).pipe(Effect.mapError((cause) => Schema.is(PageCrawlProtocolError)(cause) ? cause : protocolError("Reading the Browser Run crawl response failed", cause)));
|
|
110
|
+
return yield* Effect.try({
|
|
111
|
+
try: () => state.text + decoder.decode(),
|
|
112
|
+
catch: (cause) => protocolError("Decoding the Browser Run crawl response failed", cause)
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
const deadlineFailure = Effect.fn("BrowserRestCrawl.deadlineFailure")(function* (request, startedAt) {
|
|
116
|
+
const now = yield* Effect.clockWith((clock) => clock.currentTimeMillis);
|
|
117
|
+
return yield* limitError(request, "deadline", Math.max(0, now - startedAt));
|
|
118
|
+
});
|
|
119
|
+
const withinDeadline = Effect.fn("BrowserRestCrawl.withinDeadline")(function* (effect, request, startedAt) {
|
|
120
|
+
const now = yield* Effect.clockWith((clock) => clock.currentTimeMillis);
|
|
121
|
+
const elapsed = Math.max(0, now - startedAt);
|
|
122
|
+
const remaining = request.limits.deadlineMillis - elapsed;
|
|
123
|
+
if (remaining <= 0) return yield* limitError(request, "deadline", elapsed);
|
|
124
|
+
return yield* effect.pipe(Effect.timeoutOrElse({
|
|
125
|
+
duration: Duration.millis(remaining),
|
|
126
|
+
orElse: () => deadlineFailure(request, startedAt)
|
|
127
|
+
}));
|
|
128
|
+
});
|
|
129
|
+
const checkDeadline = Effect.fn("BrowserRestCrawl.checkDeadline")(function* (request, startedAt) {
|
|
130
|
+
const now = yield* Effect.clockWith((clock) => clock.currentTimeMillis);
|
|
131
|
+
const elapsed = Math.max(0, now - startedAt);
|
|
132
|
+
if (elapsed >= request.limits.deadlineMillis) return yield* limitError(request, "deadline", elapsed);
|
|
133
|
+
});
|
|
134
|
+
const normalizedCursor = (cursor) => String(cursor);
|
|
135
|
+
const sameHost = (expectedHost, url) => new URL(url).host === expectedHost;
|
|
136
|
+
const makeCrawl = (client, options) => (input) => Stream.unwrap(Effect.gen(function* () {
|
|
137
|
+
const startedAt = yield* Effect.clockWith((clock) => clock.currentTimeMillis);
|
|
138
|
+
const startHost = new URL(input.startUrl).host;
|
|
139
|
+
const executeJson = Effect.fn("BrowserRestCrawl.executeJson")(function* (request, schema, maximum) {
|
|
140
|
+
const authorized = HttpClientRequest.bearerToken(request, options.apiToken).pipe(HttpClientRequest.acceptJson);
|
|
141
|
+
const response = yield* client.execute(authorized).pipe(Effect.mapError((cause) => protocolError("Calling the Browser Run crawl REST API failed", privateCause(cause, options.apiToken))));
|
|
142
|
+
const bodyText = yield* readBoundedResponse(response, maximum);
|
|
143
|
+
if (response.status === 429) {
|
|
144
|
+
const reason = isQuotaMessage(bodyText) ? "quota" : "rate";
|
|
145
|
+
return yield* PageCrawlRateLimitedError.make({
|
|
146
|
+
implementation: browserRestCrawlImplementation,
|
|
147
|
+
reason,
|
|
148
|
+
...retryAfterMillis(response.headers) === void 0 ? {} : { retryAfterMillis: retryAfterMillis(response.headers) },
|
|
149
|
+
message: reason === "quota" ? "Browser Run exceeded its browser quota" : "Browser Run crawl was rate limited",
|
|
150
|
+
...privateCause(bodyText, options.apiToken) === void 0 ? {} : { cause: privateCause(bodyText, options.apiToken) }
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
if (response.status < 200 || response.status >= 300) return yield* protocolError(`Browser Run crawl answered HTTP ${String(response.status)}`, privateCause(bodyText, options.apiToken));
|
|
154
|
+
if (!isJsonResponse(response.headers)) return yield* protocolError("The Browser Run crawl success response was not JSON", privateCause(bodyText, options.apiToken));
|
|
155
|
+
return yield* Schema.decodeUnknownEffect(Schema.fromJsonString(schema))(bodyText).pipe(Effect.mapError((cause) => protocolError("Browser Run returned a malformed crawl response", privateCause(cause, options.apiToken))));
|
|
156
|
+
});
|
|
157
|
+
const createJob = Effect.gen(function* () {
|
|
158
|
+
const requestWithBody = yield* HttpClientRequest.post(endpoint(options)).pipe(HttpClientRequest.bodyJson({
|
|
159
|
+
url: input.startUrl,
|
|
160
|
+
crawlPurposes: [...input.purposes],
|
|
161
|
+
limit: input.limits.maxPages,
|
|
162
|
+
depth: input.limits.maxDepth,
|
|
163
|
+
formats: ["markdown"],
|
|
164
|
+
render: true,
|
|
165
|
+
options: {
|
|
166
|
+
includeExternalLinks: false,
|
|
167
|
+
includeSubdomains: false
|
|
168
|
+
}
|
|
169
|
+
}), Effect.mapError((cause) => protocolError("Encoding the Browser Run crawl request failed", cause)));
|
|
170
|
+
return (yield* withinDeadline(executeJson(requestWithBody, CreateEnvelope, MAX_CONTROL_RESPONSE_BYTES), input, startedAt)).result;
|
|
171
|
+
});
|
|
172
|
+
const job = yield* Effect.uninterruptibleMask((restore) => restore(createJob).pipe(Effect.flatMap((id) => Effect.gen(function* () {
|
|
173
|
+
const state = yield* Ref.make("running");
|
|
174
|
+
const scope = yield* Effect.scope;
|
|
175
|
+
const cancelIfRunning = Ref.modify(state, (current) => current === "running" ? [true, "terminal"] : [false, "terminal"]).pipe(Effect.flatMap((shouldCancel) => {
|
|
176
|
+
if (!shouldCancel) return Effect.void;
|
|
177
|
+
return executeJson(HttpClientRequest.delete(endpoint(options, id)), DeleteEnvelope, MAX_CONTROL_RESPONSE_BYTES).pipe(Effect.flatMap((deleted) => deleted.result.job_id === id ? Effect.void : protocolError("Browser Run cancelled a different crawl job"))).pipe(Effect.timeoutOrElse({
|
|
178
|
+
duration: CANCEL_TIMEOUT,
|
|
179
|
+
orElse: () => protocolError("Cancelling the Browser Run crawl exceeded 30 seconds")
|
|
180
|
+
}));
|
|
181
|
+
}));
|
|
182
|
+
yield* Scope.addFinalizer(scope, cancelIfRunning.pipe(Effect.catchCause(() => Effect.logWarning("Browser Run crawl cancellation failed"))));
|
|
183
|
+
return {
|
|
184
|
+
id,
|
|
185
|
+
state
|
|
186
|
+
};
|
|
187
|
+
}))));
|
|
188
|
+
const fetchResult = Effect.fn("BrowserRestCrawl.fetchResult")(function* (cursor, statusOnly) {
|
|
189
|
+
let request = HttpClientRequest.get(endpoint(options, job.id));
|
|
190
|
+
if (statusOnly) request = HttpClientRequest.setUrlParam(request, "limit", "1");
|
|
191
|
+
if (Option.isSome(cursor)) request = HttpClientRequest.setUrlParam(request, "cursor", cursor.value);
|
|
192
|
+
const envelope = yield* withinDeadline(executeJson(request, ResultEnvelope, MAX_RESULTS_RESPONSE_BYTES), input, startedAt);
|
|
193
|
+
if (envelope.result.id !== job.id) return yield* protocolError("Browser Run returned a different crawl job identity");
|
|
194
|
+
return envelope.result;
|
|
195
|
+
});
|
|
196
|
+
const terminal = yield* Effect.uninterruptibleMask((restore) => restore(withinDeadline(Effect.repeat(fetchResult(Option.none(), true), {
|
|
197
|
+
schedule: Schedule.spaced(POLL_INTERVAL),
|
|
198
|
+
until: (result) => result.status !== "running"
|
|
199
|
+
}), input, startedAt)).pipe(Effect.tap(() => Ref.set(job.state, "terminal"))));
|
|
200
|
+
if (terminal.status === "running") return yield* protocolError("Browser Run polling stopped before a terminal status");
|
|
201
|
+
if (terminal.status !== "completed") return yield* PageCrawlTerminalError.make({
|
|
202
|
+
implementation: browserRestCrawlImplementation,
|
|
203
|
+
status: terminal.status,
|
|
204
|
+
message: `Browser Run crawl ended with status ${terminal.status}`
|
|
205
|
+
});
|
|
206
|
+
return Stream.paginate({
|
|
207
|
+
cursor: Option.none(),
|
|
208
|
+
seen: []
|
|
209
|
+
}, (state) => Effect.gen(function* () {
|
|
210
|
+
const result = yield* fetchResult(state.cursor, false);
|
|
211
|
+
if (result.status !== "completed") return yield* protocolError("Browser Run changed crawl status during result pagination");
|
|
212
|
+
if (result.cursor === void 0) return [result.records, Option.none()];
|
|
213
|
+
const cursor = normalizedCursor(result.cursor);
|
|
214
|
+
if (state.seen.includes(cursor)) return yield* protocolError("Browser Run repeated a crawl result cursor");
|
|
215
|
+
if (state.seen.length >= input.limits.maxPages) return yield* protocolError("Browser Run returned too many crawl result cursors");
|
|
216
|
+
return [result.records, Option.some({
|
|
217
|
+
cursor: Option.some(cursor),
|
|
218
|
+
seen: [...state.seen, cursor]
|
|
219
|
+
})];
|
|
220
|
+
})).pipe(Stream.rechunk(1), Stream.mapAccumEffect(() => ({
|
|
221
|
+
pages: 0,
|
|
222
|
+
totalBytes: 0
|
|
223
|
+
}), (state, record) => Effect.gen(function* () {
|
|
224
|
+
yield* checkDeadline(input, startedAt);
|
|
225
|
+
const pages = state.pages + 1;
|
|
226
|
+
if (pages > input.limits.maxPages) return yield* limitError(input, "pages", pages);
|
|
227
|
+
if (!sameHost(startHost, record.url)) return yield* protocolError("Browser Run returned an off-host crawl record");
|
|
228
|
+
if (record.metadata !== void 0 && !sameHost(startHost, record.metadata.url)) return yield* protocolError("Browser Run returned off-host crawl metadata");
|
|
229
|
+
const pageBytes = record.markdown === void 0 ? 0 : new TextEncoder().encode(record.markdown).byteLength;
|
|
230
|
+
if (pageBytes > input.limits.maxPageBytes) return yield* limitError(input, "page-bytes", pageBytes);
|
|
231
|
+
const totalBytes = state.totalBytes + pageBytes;
|
|
232
|
+
if (totalBytes > input.limits.maxTotalBytes) return yield* limitError(input, "total-bytes", totalBytes);
|
|
233
|
+
return [{
|
|
234
|
+
pages,
|
|
235
|
+
totalBytes
|
|
236
|
+
}, [record]];
|
|
237
|
+
})));
|
|
238
|
+
}));
|
|
239
|
+
/** Cloudflare REST PageCrawl Layer for Node and other non-Worker composition roots. */
|
|
240
|
+
const browserRestCrawlLayer = (options) => Layer.effect(PageCrawl, Effect.map(HttpClient.HttpClient, (client) => PageCrawl.of({ crawl: makeCrawl(client, options) })));
|
|
241
|
+
//#endregion
|
|
242
|
+
export { browserRestCrawlImplementation, browserRestCrawlLayer };
|
|
243
|
+
|
|
244
|
+
//# sourceMappingURL=BrowserRestCrawl.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BrowserRestCrawl.mjs","names":[],"sources":["../src/BrowserRestCrawl.ts"],"sourcesContent":["import {\n PageCrawl,\n PageCrawlLimitError,\n PageCrawlProtocolError,\n PageCrawlRateLimitedError,\n PageCrawlRecord,\n PageCrawlTerminalError,\n type PageCrawlCrawl,\n type PageCrawlError,\n type PageCrawlRequest,\n} from \"@effect-agent/sandbox/PageCrawl\";\nimport { SandboxImplementation } from \"@effect-agent/sandbox/Sandbox\";\nimport {\n Duration,\n Effect,\n Layer,\n Option,\n Redacted,\n Ref,\n Schedule,\n Schema,\n Scope,\n Stream,\n} from \"effect\";\nimport {\n HttpClient,\n HttpClientRequest,\n type HttpClientError,\n type HttpClientResponse,\n} from \"effect/unstable/http\";\n\n/** Node-safe REST crawl implementation. It never exposes or persists provider job identity. */\nexport const browserRestCrawlImplementation = SandboxImplementation.make({\n isolation: \"isolated\",\n identity: \"cloudflare-browser-rest-crawl\",\n});\n\nconst API_ORIGIN = \"https://api.cloudflare.com\";\nconst POLL_INTERVAL = Duration.seconds(1);\nconst CANCEL_TIMEOUT = Duration.seconds(30);\nconst MAX_CONTROL_RESPONSE_BYTES = 64 * 1024;\nconst MAX_RESULTS_RESPONSE_BYTES = 12 * 1024 * 1024;\nconst MAX_DIAGNOSTIC_LENGTH = 8_000;\nconst MAX_CURSOR_LENGTH = 1_024;\nconst MAX_RECORDS_PER_RESPONSE = 10_000;\nconst BoundedJobId = Schema.NonEmptyString.check(Schema.isMaxLength(256));\nconst BoundedCursorString = Schema.NonEmptyString.check(Schema.isMaxLength(MAX_CURSOR_LENGTH));\n// The generated API response Schema says string; the current product example returns a number.\nconst ProviderCursor = Schema.Union([BoundedCursorString, Schema.Natural]);\n\nconst ProviderJobStatus = Schema.Literals([\n \"running\",\n \"completed\",\n \"errored\",\n \"cancelled_by_user\",\n \"cancelled_due_to_timeout\",\n \"cancelled_due_to_limits\",\n]);\n\nconst ProviderResult = Schema.Struct({\n id: BoundedJobId,\n status: ProviderJobStatus,\n browserSecondsUsed: Schema.optionalKey(Schema.Finite.check(Schema.isGreaterThanOrEqualTo(0))),\n total: Schema.optionalKey(Schema.Natural),\n finished: Schema.optionalKey(Schema.Natural),\n skipped: Schema.optionalKey(Schema.Natural),\n records: Schema.Array(PageCrawlRecord).check(Schema.isMaxLength(MAX_RECORDS_PER_RESPONSE)),\n cursor: Schema.optionalKey(ProviderCursor),\n});\n\nconst CreateEnvelope = Schema.Struct({\n success: Schema.Literal(true),\n result: BoundedJobId,\n});\n\nconst ResultEnvelope = Schema.Struct({\n success: Schema.Literal(true),\n result: ProviderResult,\n});\n\nconst DeleteEnvelope = Schema.Struct({\n success: Schema.Literal(true),\n result: Schema.Struct({\n job_id: BoundedJobId,\n message: Schema.String.check(Schema.isMaxLength(MAX_DIAGNOSTIC_LENGTH)),\n }),\n});\n\n/** Explicit credentials. The token is projected only into fixed-origin Authorization headers. */\nexport interface BrowserRestCrawlOptions {\n readonly accountId: string;\n readonly apiToken: Redacted.Redacted<string>;\n}\n\nconst boundedDiagnostic = (message: string): string => message.slice(0, MAX_DIAGNOSTIC_LENGTH);\n\nconst privateCause = (value: unknown, apiToken: Redacted.Redacted<string>): Error | undefined => {\n const raw = boundedDiagnostic(String(value));\n\n if (raw.length === 0) return undefined;\n const token = Redacted.value(apiToken);\n\n return new Error(token.length === 0 ? raw : raw.replaceAll(token, \"[REDACTED]\"));\n};\n\nconst protocolError = (message: string, cause?: unknown): PageCrawlProtocolError =>\n PageCrawlProtocolError.make({\n implementation: browserRestCrawlImplementation,\n message: boundedDiagnostic(message),\n ...(cause === undefined ? {} : { cause }),\n });\n\nconst limitError = (\n request: PageCrawlRequest,\n limit: PageCrawlLimitError[\"limit\"],\n observed: number,\n): PageCrawlLimitError => {\n const maximum =\n limit === \"pages\"\n ? request.limits.maxPages\n : limit === \"page-bytes\"\n ? request.limits.maxPageBytes\n : limit === \"total-bytes\"\n ? request.limits.maxTotalBytes\n : request.limits.deadlineMillis;\n\n return PageCrawlLimitError.make({\n implementation: browserRestCrawlImplementation,\n limit,\n maximum,\n observed,\n message: `The crawl exceeded its ${limit} limit`,\n });\n};\n\nconst retryAfterMillis = (\n headers: Readonly<Record<string, string | undefined>>,\n): number | undefined => {\n const seconds = Number(headers[\"retry-after\"]);\n\n if (!Number.isSafeInteger(seconds) || seconds < 0) return undefined;\n const millis = seconds * 1_000;\n\n return Number.isSafeInteger(millis) ? millis : undefined;\n};\n\nconst isJsonResponse = (headers: Readonly<Record<string, string | undefined>>): boolean => {\n const contentType = headers[\"content-type\"];\n\n if (contentType === undefined) return false;\n const mediaType = contentType.split(\";\", 1)[0]?.trim().toLowerCase();\n\n return mediaType === \"application/json\" || mediaType?.endsWith(\"+json\") === true;\n};\n\nconst isQuotaMessage = (bodyText: string): boolean => /time limit|daily|quota/i.test(bodyText);\n\nconst endpoint = (options: BrowserRestCrawlOptions, jobId?: string): string =>\n `${API_ORIGIN}/client/v4/accounts/${encodeURIComponent(options.accountId)}/browser-rendering/crawl${\n jobId === undefined ? \"\" : `/${encodeURIComponent(jobId)}`\n }`;\n\nconst readBoundedResponse = Effect.fn(\"BrowserRestCrawl.readBoundedResponse\")(function* (\n response: HttpClientResponse.HttpClientResponse,\n maximum: number,\n): Effect.fn.Return<string, PageCrawlProtocolError> {\n const decoder = new TextDecoder(\"utf-8\", { fatal: true, ignoreBOM: false });\n\n const state = yield* Stream.runFoldEffect<\n Uint8Array,\n HttpClientError.HttpClientError,\n never,\n { readonly observed: number; readonly text: string },\n PageCrawlProtocolError,\n never\n >(\n response.stream,\n () => ({ observed: 0, text: \"\" }),\n (current, chunk) => {\n const observed = current.observed + chunk.byteLength;\n\n if (observed > maximum) {\n return Effect.fail(\n protocolError(\n `The Browser Run crawl response exceeded ${String(maximum)} transport bytes`,\n ),\n );\n }\n\n return Effect.try({\n try: () => ({ observed, text: current.text + decoder.decode(chunk, { stream: true }) }),\n catch: (cause) => protocolError(\"Decoding the Browser Run crawl response failed\", cause),\n });\n },\n ).pipe(\n Effect.mapError((cause) =>\n Schema.is(PageCrawlProtocolError)(cause)\n ? cause\n : protocolError(\"Reading the Browser Run crawl response failed\", cause),\n ),\n );\n\n return yield* Effect.try({\n try: () => state.text + decoder.decode(),\n catch: (cause) => protocolError(\"Decoding the Browser Run crawl response failed\", cause),\n });\n});\n\nconst deadlineFailure = Effect.fn(\"BrowserRestCrawl.deadlineFailure\")(function* (\n request: PageCrawlRequest,\n startedAt: number,\n) {\n const now = yield* Effect.clockWith((clock) => clock.currentTimeMillis);\n\n return yield* limitError(request, \"deadline\", Math.max(0, now - startedAt));\n});\n\nconst withinDeadline = Effect.fn(\"BrowserRestCrawl.withinDeadline\")(function* <A, E, R>(\n effect: Effect.Effect<A, E, R>,\n request: PageCrawlRequest,\n startedAt: number,\n): Effect.fn.Return<A, E | PageCrawlLimitError, R> {\n const now = yield* Effect.clockWith((clock) => clock.currentTimeMillis);\n const elapsed = Math.max(0, now - startedAt);\n const remaining = request.limits.deadlineMillis - elapsed;\n\n if (remaining <= 0) return yield* limitError(request, \"deadline\", elapsed);\n\n return yield* effect.pipe(\n Effect.timeoutOrElse({\n duration: Duration.millis(remaining),\n orElse: () => deadlineFailure(request, startedAt),\n }),\n );\n});\n\nconst checkDeadline = Effect.fn(\"BrowserRestCrawl.checkDeadline\")(function* (\n request: PageCrawlRequest,\n startedAt: number,\n) {\n const now = yield* Effect.clockWith((clock) => clock.currentTimeMillis);\n const elapsed = Math.max(0, now - startedAt);\n\n if (elapsed >= request.limits.deadlineMillis) {\n return yield* limitError(request, \"deadline\", elapsed);\n }\n});\n\nconst normalizedCursor = (cursor: typeof ProviderCursor.Type): string => String(cursor);\n\ninterface PaginationState {\n readonly cursor: Option.Option<string>;\n readonly seen: ReadonlyArray<string>;\n}\n\ninterface RecordLimitsState {\n readonly pages: number;\n readonly totalBytes: number;\n}\n\nconst sameHost = (expectedHost: string, url: string): boolean => new URL(url).host === expectedHost;\n\nconst makeCrawl =\n (client: HttpClient.HttpClient, options: BrowserRestCrawlOptions): PageCrawlCrawl =>\n (input) =>\n Stream.unwrap(\n Effect.gen(function* () {\n const startedAt = yield* Effect.clockWith((clock) => clock.currentTimeMillis);\n const startHost = new URL(input.startUrl).host;\n\n const executeJson = Effect.fn(\"BrowserRestCrawl.executeJson\")(function* <\n S extends Schema.Top,\n >(request: HttpClientRequest.HttpClientRequest, schema: S, maximum: number) {\n const authorized = HttpClientRequest.bearerToken(request, options.apiToken).pipe(\n HttpClientRequest.acceptJson,\n );\n\n const response = yield* client\n .execute(authorized)\n .pipe(\n Effect.mapError((cause) =>\n protocolError(\n \"Calling the Browser Run crawl REST API failed\",\n privateCause(cause, options.apiToken),\n ),\n ),\n );\n\n const bodyText = yield* readBoundedResponse(response, maximum);\n\n if (response.status === 429) {\n const reason = isQuotaMessage(bodyText) ? \"quota\" : \"rate\";\n\n return yield* PageCrawlRateLimitedError.make({\n implementation: browserRestCrawlImplementation,\n reason,\n ...(retryAfterMillis(response.headers) === undefined\n ? {}\n : { retryAfterMillis: retryAfterMillis(response.headers) }),\n message:\n reason === \"quota\"\n ? \"Browser Run exceeded its browser quota\"\n : \"Browser Run crawl was rate limited\",\n ...(privateCause(bodyText, options.apiToken) === undefined\n ? {}\n : { cause: privateCause(bodyText, options.apiToken) }),\n });\n }\n if (response.status < 200 || response.status >= 300) {\n return yield* protocolError(\n `Browser Run crawl answered HTTP ${String(response.status)}`,\n privateCause(bodyText, options.apiToken),\n );\n }\n if (!isJsonResponse(response.headers)) {\n return yield* protocolError(\n \"The Browser Run crawl success response was not JSON\",\n privateCause(bodyText, options.apiToken),\n );\n }\n\n return yield* Schema.decodeUnknownEffect(Schema.fromJsonString(schema))(bodyText).pipe(\n Effect.mapError((cause) =>\n protocolError(\n \"Browser Run returned a malformed crawl response\",\n privateCause(cause, options.apiToken),\n ),\n ),\n );\n });\n\n const createJob = Effect.gen(function* () {\n const requestWithBody = yield* HttpClientRequest.post(endpoint(options)).pipe(\n HttpClientRequest.bodyJson({\n url: input.startUrl,\n crawlPurposes: [...input.purposes],\n limit: input.limits.maxPages,\n depth: input.limits.maxDepth,\n formats: [\"markdown\"],\n render: true,\n options: {\n includeExternalLinks: false,\n includeSubdomains: false,\n },\n }),\n Effect.mapError((cause) =>\n protocolError(\"Encoding the Browser Run crawl request failed\", cause),\n ),\n );\n\n const created = yield* withinDeadline(\n executeJson(requestWithBody, CreateEnvelope, MAX_CONTROL_RESPONSE_BYTES),\n input,\n startedAt,\n );\n\n return created.result;\n });\n\n const job = yield* Effect.uninterruptibleMask((restore) =>\n restore(createJob).pipe(\n Effect.flatMap((id) =>\n Effect.gen(function* () {\n const state = yield* Ref.make<\"running\" | \"terminal\">(\"running\");\n const scope = yield* Effect.scope;\n\n const cancelIfRunning = Ref.modify(state, (current) =>\n current === \"running\"\n ? [true, \"terminal\" as const]\n : [false, \"terminal\" as const],\n ).pipe(\n Effect.flatMap((shouldCancel) => {\n if (!shouldCancel) return Effect.void;\n\n const cancellation = executeJson(\n HttpClientRequest.delete(endpoint(options, id)),\n DeleteEnvelope,\n MAX_CONTROL_RESPONSE_BYTES,\n ).pipe(\n Effect.flatMap((deleted) =>\n deleted.result.job_id === id\n ? Effect.void\n : protocolError(\"Browser Run cancelled a different crawl job\"),\n ),\n );\n\n return cancellation.pipe(\n Effect.timeoutOrElse({\n duration: CANCEL_TIMEOUT,\n orElse: () =>\n protocolError(\"Cancelling the Browser Run crawl exceeded 30 seconds\"),\n }),\n );\n }),\n );\n\n yield* Scope.addFinalizer(\n scope,\n cancelIfRunning.pipe(\n Effect.catchCause(() =>\n Effect.logWarning(\"Browser Run crawl cancellation failed\"),\n ),\n ),\n );\n\n return { id, state } as const;\n }),\n ),\n ),\n );\n\n const fetchResult = Effect.fn(\"BrowserRestCrawl.fetchResult\")(function* (\n cursor: Option.Option<string>,\n statusOnly: boolean,\n ) {\n let request = HttpClientRequest.get(endpoint(options, job.id));\n\n if (statusOnly) request = HttpClientRequest.setUrlParam(request, \"limit\", \"1\");\n if (Option.isSome(cursor)) {\n request = HttpClientRequest.setUrlParam(request, \"cursor\", cursor.value);\n }\n\n // `limit=1` still permits one complete record, so poll GETs use the bounded result cap.\n const envelope = yield* withinDeadline(\n executeJson(request, ResultEnvelope, MAX_RESULTS_RESPONSE_BYTES),\n input,\n startedAt,\n );\n\n if (envelope.result.id !== job.id) {\n return yield* protocolError(\"Browser Run returned a different crawl job identity\");\n }\n\n return envelope.result;\n });\n\n const terminal = yield* Effect.uninterruptibleMask((restore) =>\n restore(\n withinDeadline(\n Effect.repeat(fetchResult(Option.none(), true), {\n schedule: Schedule.spaced(POLL_INTERVAL),\n until: (result) => result.status !== \"running\",\n }),\n input,\n startedAt,\n ),\n ).pipe(Effect.tap(() => Ref.set(job.state, \"terminal\"))),\n );\n\n if (terminal.status === \"running\") {\n return yield* protocolError(\"Browser Run polling stopped before a terminal status\");\n }\n if (terminal.status !== \"completed\") {\n return yield* PageCrawlTerminalError.make({\n implementation: browserRestCrawlImplementation,\n status: terminal.status,\n message: `Browser Run crawl ended with status ${terminal.status}`,\n });\n }\n\n const pages = Stream.paginate<PaginationState, PageCrawlRecord, PageCrawlError>(\n { cursor: Option.none(), seen: [] },\n (state) =>\n Effect.gen(function* () {\n const result = yield* fetchResult(state.cursor, false);\n\n if (result.status !== \"completed\") {\n return yield* protocolError(\n \"Browser Run changed crawl status during result pagination\",\n );\n }\n if (result.cursor === undefined) {\n return [result.records, Option.none()] as const;\n }\n const cursor = normalizedCursor(result.cursor);\n\n if (state.seen.includes(cursor)) {\n return yield* protocolError(\"Browser Run repeated a crawl result cursor\");\n }\n if (state.seen.length >= input.limits.maxPages) {\n return yield* protocolError(\"Browser Run returned too many crawl result cursors\");\n }\n\n return [\n result.records,\n Option.some({ cursor: Option.some(cursor), seen: [...state.seen, cursor] }),\n ] as const;\n }),\n );\n\n return pages.pipe(\n Stream.rechunk(1),\n Stream.mapAccumEffect(\n (): RecordLimitsState => ({ pages: 0, totalBytes: 0 }),\n (state, record) =>\n Effect.gen(function* () {\n yield* checkDeadline(input, startedAt);\n const pages = state.pages + 1;\n\n if (pages > input.limits.maxPages) {\n return yield* limitError(input, \"pages\", pages);\n }\n if (!sameHost(startHost, record.url)) {\n return yield* protocolError(\"Browser Run returned an off-host crawl record\");\n }\n if (record.metadata !== undefined && !sameHost(startHost, record.metadata.url)) {\n return yield* protocolError(\"Browser Run returned off-host crawl metadata\");\n }\n\n const pageBytes =\n record.markdown === undefined\n ? 0\n : new TextEncoder().encode(record.markdown).byteLength;\n\n if (pageBytes > input.limits.maxPageBytes) {\n return yield* limitError(input, \"page-bytes\", pageBytes);\n }\n const totalBytes = state.totalBytes + pageBytes;\n\n if (totalBytes > input.limits.maxTotalBytes) {\n return yield* limitError(input, \"total-bytes\", totalBytes);\n }\n\n return [{ pages, totalBytes }, [record]] as const;\n }),\n ),\n );\n }),\n );\n\n/** Cloudflare REST PageCrawl Layer for Node and other non-Worker composition roots. */\nexport const browserRestCrawlLayer = (\n options: BrowserRestCrawlOptions,\n): Layer.Layer<PageCrawl, never, HttpClient.HttpClient> =>\n Layer.effect(\n PageCrawl,\n Effect.map(HttpClient.HttpClient, (client) =>\n PageCrawl.of({ crawl: makeCrawl(client, options) }),\n ),\n );\n"],"mappings":"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"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { Context, Effect, Layer, Schema } from "effect";
|
|
2
|
+
import { ThreadId } from "@effect-agent/core/Identifiers";
|
|
3
|
+
import { ProducerId } from "@effect-agent/thread/Records";
|
|
4
|
+
declare namespace CloudflareBindings_d_exports {
|
|
5
|
+
export { CloudflareBindingError, DurableObjectContext, ThreadObjectIdentity, ThreadObjectNamespace, ThreadObjectRpc, threadNamespaceFromEnv, threadNamespaceLayer };
|
|
6
|
+
}
|
|
7
|
+
declare const CloudflareBindingError_base: Schema.Class<CloudflareBindingError, Schema.TaggedStruct<"CloudflareBindingError", {
|
|
8
|
+
readonly binding: Schema.String;
|
|
9
|
+
readonly message: Schema.String;
|
|
10
|
+
}>, import("effect/Cause").YieldableError>;
|
|
11
|
+
/**
|
|
12
|
+
* Cloudflare platform bindings as Effect services (DEPLOY-010: "Cloudflare platform bindings
|
|
13
|
+
* are supplied as Effect services/Layers"). Application code never reads `env` or touches a
|
|
14
|
+
* `DurableObjectState` directly — the Thread Object class constructs these Layers once
|
|
15
|
+
* per incarnation and everything downstream consumes the services.
|
|
16
|
+
*/
|
|
17
|
+
/** A Cloudflare platform binding was missing or carried the wrong shape (DEPLOY-003/010). */
|
|
18
|
+
declare class CloudflareBindingError extends CloudflareBindingError_base {}
|
|
19
|
+
/**
|
|
20
|
+
* The RPC surface one Thread Durable Object exposes to Workers and to sibling
|
|
21
|
+
* Thread Objects. `ThreadObject.make` implements it; the Worker-side client
|
|
22
|
+
* and the cross-Object transport call it through `DurableObjectNamespace` stubs. Every
|
|
23
|
+
* `encoded` value is a Schema-encoded envelope (`client.ts` wire schemas for host entry
|
|
24
|
+
* points, `@effect-agent/storage-cloudflare` port envelopes for `portCall`), so the RPC
|
|
25
|
+
* boundary carries only structured-cloneable JSON. The optional trailing trace context is
|
|
26
|
+
* transient native RPC metadata, stripped by an opted-in effect-cf receiver before decoding
|
|
27
|
+
* the host envelope. It never enters durable state.
|
|
28
|
+
*/
|
|
29
|
+
interface ThreadObjectRpc extends Rpc.DurableObjectBranded {
|
|
30
|
+
/** Admission-limits gate + `DurableAgentRuntime.submit`; answers a `SubmitResponse`. */
|
|
31
|
+
submitEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;
|
|
32
|
+
/** Wake-hinted, poll-guaranteed settlement wait; answers an `AwaitSettlementResponse`. */
|
|
33
|
+
submissionStatusEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;
|
|
34
|
+
awaitSettlementEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;
|
|
35
|
+
/** Event-driven durable progress wait; answers a `ProgressObserved` host response. */
|
|
36
|
+
awaitProgressEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;
|
|
37
|
+
/** Best-effort cancellation for one in-flight progress wait. */
|
|
38
|
+
cancelProgressEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;
|
|
39
|
+
/** One bounded page of canonical records; answers an `ObservePageResponse`. */
|
|
40
|
+
observePage(encoded: unknown, traceContext?: unknown): Promise<unknown>;
|
|
41
|
+
/** Durable abort intent; answers an `AbortResponse`. */
|
|
42
|
+
abortEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;
|
|
43
|
+
/** Durable approval decision (plan §2.6); answers a `ResolveApprovalResponse`. */
|
|
44
|
+
resolveApprovalEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;
|
|
45
|
+
/** Authorized DUR-017 Unknown-Outcome resolution; answers a `ResolveUnknownResponse`. */
|
|
46
|
+
resolveUnknownEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;
|
|
47
|
+
/** Owner-side cross-Object port endpoint (WP2 envelopes, executed on LOCAL facets). */
|
|
48
|
+
portCall(encoded: unknown): Promise<unknown>;
|
|
49
|
+
/** Droppable liveness hint from another Object: arms an immediate alarm. */
|
|
50
|
+
wake(): Promise<void>;
|
|
51
|
+
}
|
|
52
|
+
declare const ThreadObjectNamespace_base: Context.ServiceClass<ThreadObjectNamespace, "@effect-agent/platform-cloudflare/ThreadObjectNamespace", {
|
|
53
|
+
readonly namespace: DurableObjectNamespace<ThreadObjectRpc>;
|
|
54
|
+
/** Stable binding name for opted-in native RPC tracing; absent by default. */
|
|
55
|
+
readonly rpcTracing?: string;
|
|
56
|
+
}>;
|
|
57
|
+
/**
|
|
58
|
+
* The `DurableObjectNamespace` binding that addresses Thread Objects. The Object
|
|
59
|
+
* identity rule is `namespace.idFromName(threadId)` (plan §1.2): Thread IDs are
|
|
60
|
+
* globally unique, so the mapping is total and deterministic and no directory service exists.
|
|
61
|
+
*/
|
|
62
|
+
declare class ThreadObjectNamespace extends ThreadObjectNamespace_base {
|
|
63
|
+
static layer(namespace: DurableObjectNamespace<ThreadObjectRpc>, options?: {
|
|
64
|
+
readonly rpcTracing?: string;
|
|
65
|
+
}): Layer.Layer<ThreadObjectNamespace>;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Narrow one `env` member to a `DurableObjectNamespace`. `env` is an untyped platform value,
|
|
69
|
+
* and a namespace binding is a host object no Schema can decode, so this is the documented
|
|
70
|
+
* narrowest-boundary check (structural probe for the namespace surface the transport uses);
|
|
71
|
+
* a missing or misshaped binding fails typed before any Layer is built.
|
|
72
|
+
*/
|
|
73
|
+
declare const threadNamespaceFromEnv: (env: unknown, binding: string) => Effect.Effect<DurableObjectNamespace<ThreadObjectRpc>, CloudflareBindingError, never>;
|
|
74
|
+
/**
|
|
75
|
+
* Build the namespace from Worker `env` (fails typed). Enable `rpcTracing` only when the
|
|
76
|
+
* receiver also opts into the effect-cf native RPC trace-context contract.
|
|
77
|
+
*/
|
|
78
|
+
declare const threadNamespaceLayer: (env: unknown, binding: string, options?: {
|
|
79
|
+
readonly rpcTracing?: boolean;
|
|
80
|
+
}) => Layer.Layer<ThreadObjectNamespace, CloudflareBindingError>;
|
|
81
|
+
declare const DurableObjectContext_base: Context.ServiceClass<DurableObjectContext, "@effect-agent/platform-cloudflare/DurableObjectContext", {
|
|
82
|
+
readonly ctx: DurableObjectState;
|
|
83
|
+
readonly env: unknown;
|
|
84
|
+
}>;
|
|
85
|
+
/**
|
|
86
|
+
* The live Durable Object execution context of THIS incarnation. Only Layer construction and
|
|
87
|
+
* the alarm service consume it; important state never lives on it (`ctx.storage` is truth,
|
|
88
|
+
* everything in memory is a cache — deployment spec §11).
|
|
89
|
+
*/
|
|
90
|
+
declare class DurableObjectContext extends DurableObjectContext_base {
|
|
91
|
+
static layer(ctx: DurableObjectState, env: unknown): Layer.Layer<DurableObjectContext>;
|
|
92
|
+
}
|
|
93
|
+
declare const ThreadObjectIdentity_base: Context.ServiceClass<ThreadObjectIdentity, "@effect-agent/platform-cloudflare/ThreadObjectIdentity", {
|
|
94
|
+
readonly threadId: ThreadId;
|
|
95
|
+
readonly producerId: ProducerId;
|
|
96
|
+
}>;
|
|
97
|
+
/**
|
|
98
|
+
* The Thread identity this Object serializes and the producer identity its Attempts
|
|
99
|
+
* write with (`{producerPrefix}:{threadId}`, plan §1.4). Derived once per incarnation
|
|
100
|
+
* from `ctx.id.name` — the Object identity rule guarantees the name IS the Thread ID.
|
|
101
|
+
*/
|
|
102
|
+
declare class ThreadObjectIdentity extends ThreadObjectIdentity_base {}
|
|
103
|
+
//#endregion
|
|
104
|
+
export { CloudflareBindingError, DurableObjectContext, ThreadObjectIdentity, ThreadObjectNamespace, ThreadObjectRpc, CloudflareBindings_d_exports as t, threadNamespaceFromEnv, threadNamespaceLayer };
|
|
105
|
+
//# sourceMappingURL=CloudflareBindings.d.mts.map
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
|
|
2
|
+
import { Context, Effect, Layer, Predicate, Schema } from "effect";
|
|
3
|
+
import "@effect-agent/core/Identifiers";
|
|
4
|
+
import "@effect-agent/thread/Records";
|
|
5
|
+
//#region src/CloudflareBindings.ts
|
|
6
|
+
var CloudflareBindings_exports = /* @__PURE__ */ __exportAll({
|
|
7
|
+
CloudflareBindingError: () => CloudflareBindingError,
|
|
8
|
+
DurableObjectContext: () => DurableObjectContext,
|
|
9
|
+
ThreadObjectIdentity: () => ThreadObjectIdentity,
|
|
10
|
+
ThreadObjectNamespace: () => ThreadObjectNamespace,
|
|
11
|
+
threadNamespaceFromEnv: () => threadNamespaceFromEnv,
|
|
12
|
+
threadNamespaceLayer: () => threadNamespaceLayer
|
|
13
|
+
});
|
|
14
|
+
/**
|
|
15
|
+
* Cloudflare platform bindings as Effect services (DEPLOY-010: "Cloudflare platform bindings
|
|
16
|
+
* are supplied as Effect services/Layers"). Application code never reads `env` or touches a
|
|
17
|
+
* `DurableObjectState` directly — the Thread Object class constructs these Layers once
|
|
18
|
+
* per incarnation and everything downstream consumes the services.
|
|
19
|
+
*/
|
|
20
|
+
/** A Cloudflare platform binding was missing or carried the wrong shape (DEPLOY-003/010). */
|
|
21
|
+
var CloudflareBindingError = class extends Schema.TaggedError()("CloudflareBindingError", {
|
|
22
|
+
binding: Schema.String,
|
|
23
|
+
message: Schema.String
|
|
24
|
+
}) {};
|
|
25
|
+
/**
|
|
26
|
+
* The `DurableObjectNamespace` binding that addresses Thread Objects. The Object
|
|
27
|
+
* identity rule is `namespace.idFromName(threadId)` (plan §1.2): Thread IDs are
|
|
28
|
+
* globally unique, so the mapping is total and deterministic and no directory service exists.
|
|
29
|
+
*/
|
|
30
|
+
var ThreadObjectNamespace = class ThreadObjectNamespace extends Context.Service()("@effect-agent/platform-cloudflare/ThreadObjectNamespace") {
|
|
31
|
+
static layer(namespace, options = {}) {
|
|
32
|
+
return Layer.succeed(ThreadObjectNamespace)({
|
|
33
|
+
namespace,
|
|
34
|
+
...options.rpcTracing === void 0 ? {} : { rpcTracing: options.rpcTracing }
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Narrow one `env` member to a `DurableObjectNamespace`. `env` is an untyped platform value,
|
|
40
|
+
* and a namespace binding is a host object no Schema can decode, so this is the documented
|
|
41
|
+
* narrowest-boundary check (structural probe for the namespace surface the transport uses);
|
|
42
|
+
* a missing or misshaped binding fails typed before any Layer is built.
|
|
43
|
+
*/
|
|
44
|
+
const threadNamespaceFromEnv = Effect.fn("threadNamespaceFromEnv")(function* (env, binding) {
|
|
45
|
+
if (!Predicate.isObjectKeyword(env)) return yield* CloudflareBindingError.make({
|
|
46
|
+
binding,
|
|
47
|
+
message: "The Worker environment is not an object; no bindings are available."
|
|
48
|
+
});
|
|
49
|
+
const candidate = yield* Effect.try({
|
|
50
|
+
try: () => {
|
|
51
|
+
const value = Reflect.get(env, binding);
|
|
52
|
+
if (!Predicate.isObjectKeyword(value)) return void 0;
|
|
53
|
+
const idFromName = Reflect.get(value, "idFromName");
|
|
54
|
+
const get = Reflect.get(value, "get");
|
|
55
|
+
return typeof idFromName === "function" && typeof get === "function" ? value : void 0;
|
|
56
|
+
},
|
|
57
|
+
catch: () => CloudflareBindingError.make({
|
|
58
|
+
binding,
|
|
59
|
+
message: `env.${binding} could not be inspected as a DurableObjectNamespace binding.`
|
|
60
|
+
})
|
|
61
|
+
});
|
|
62
|
+
if (candidate !== void 0) return candidate;
|
|
63
|
+
return yield* CloudflareBindingError.make({
|
|
64
|
+
binding,
|
|
65
|
+
message: `env.${binding} is not a DurableObjectNamespace binding; declare the Thread Object class under this binding in the Worker configuration.`
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
/**
|
|
69
|
+
* Build the namespace from Worker `env` (fails typed). Enable `rpcTracing` only when the
|
|
70
|
+
* receiver also opts into the effect-cf native RPC trace-context contract.
|
|
71
|
+
*/
|
|
72
|
+
const threadNamespaceLayer = (env, binding, options = {}) => Layer.effect(ThreadObjectNamespace)(Effect.map(threadNamespaceFromEnv(env, binding), (namespace) => ({
|
|
73
|
+
namespace,
|
|
74
|
+
...options.rpcTracing === true ? { rpcTracing: binding } : {}
|
|
75
|
+
})));
|
|
76
|
+
/**
|
|
77
|
+
* The live Durable Object execution context of THIS incarnation. Only Layer construction and
|
|
78
|
+
* the alarm service consume it; important state never lives on it (`ctx.storage` is truth,
|
|
79
|
+
* everything in memory is a cache — deployment spec §11).
|
|
80
|
+
*/
|
|
81
|
+
var DurableObjectContext = class DurableObjectContext extends Context.Service()("@effect-agent/platform-cloudflare/DurableObjectContext") {
|
|
82
|
+
static layer(ctx, env) {
|
|
83
|
+
return Layer.succeed(DurableObjectContext)({
|
|
84
|
+
ctx,
|
|
85
|
+
env
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* The Thread identity this Object serializes and the producer identity its Attempts
|
|
91
|
+
* write with (`{producerPrefix}:{threadId}`, plan §1.4). Derived once per incarnation
|
|
92
|
+
* from `ctx.id.name` — the Object identity rule guarantees the name IS the Thread ID.
|
|
93
|
+
*/
|
|
94
|
+
var ThreadObjectIdentity = class extends Context.Service()("@effect-agent/platform-cloudflare/ThreadObjectIdentity") {};
|
|
95
|
+
//#endregion
|
|
96
|
+
export { CloudflareBindingError, DurableObjectContext, ThreadObjectIdentity, ThreadObjectNamespace, CloudflareBindings_exports as t, threadNamespaceFromEnv, threadNamespaceLayer };
|
|
97
|
+
|
|
98
|
+
//# sourceMappingURL=CloudflareBindings.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CloudflareBindings.mjs","names":[],"sources":["../src/CloudflareBindings.ts"],"sourcesContent":["import { type ThreadId } from \"@effect-agent/core/Identifiers\";\nimport { type ProducerId } from \"@effect-agent/thread/Records\";\nimport { Context, Effect, Layer, Predicate, Schema } from \"effect\";\n\n/**\n * Cloudflare platform bindings as Effect services (DEPLOY-010: \"Cloudflare platform bindings\n * are supplied as Effect services/Layers\"). Application code never reads `env` or touches a\n * `DurableObjectState` directly — the Thread Object class constructs these Layers once\n * per incarnation and everything downstream consumes the services.\n */\n\n/** A Cloudflare platform binding was missing or carried the wrong shape (DEPLOY-003/010). */\nexport class CloudflareBindingError extends Schema.TaggedError<CloudflareBindingError>()(\n \"CloudflareBindingError\",\n {\n binding: Schema.String,\n message: Schema.String,\n },\n) {}\n\n/**\n * The RPC surface one Thread Durable Object exposes to Workers and to sibling\n * Thread Objects. `ThreadObject.make` implements it; the Worker-side client\n * and the cross-Object transport call it through `DurableObjectNamespace` stubs. Every\n * `encoded` value is a Schema-encoded envelope (`client.ts` wire schemas for host entry\n * points, `@effect-agent/storage-cloudflare` port envelopes for `portCall`), so the RPC\n * boundary carries only structured-cloneable JSON. The optional trailing trace context is\n * transient native RPC metadata, stripped by an opted-in effect-cf receiver before decoding\n * the host envelope. It never enters durable state.\n */\nexport interface ThreadObjectRpc extends Rpc.DurableObjectBranded {\n /** Admission-limits gate + `DurableAgentRuntime.submit`; answers a `SubmitResponse`. */\n submitEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;\n /** Wake-hinted, poll-guaranteed settlement wait; answers an `AwaitSettlementResponse`. */\n submissionStatusEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;\n awaitSettlementEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;\n /** Event-driven durable progress wait; answers a `ProgressObserved` host response. */\n awaitProgressEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;\n /** Best-effort cancellation for one in-flight progress wait. */\n cancelProgressEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;\n /** One bounded page of canonical records; answers an `ObservePageResponse`. */\n observePage(encoded: unknown, traceContext?: unknown): Promise<unknown>;\n /** Durable abort intent; answers an `AbortResponse`. */\n abortEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;\n /** Durable approval decision (plan §2.6); answers a `ResolveApprovalResponse`. */\n resolveApprovalEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;\n /** Authorized DUR-017 Unknown-Outcome resolution; answers a `ResolveUnknownResponse`. */\n resolveUnknownEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;\n /** Owner-side cross-Object port endpoint (WP2 envelopes, executed on LOCAL facets). */\n portCall(encoded: unknown): Promise<unknown>;\n /** Droppable liveness hint from another Object: arms an immediate alarm. */\n wake(): Promise<void>;\n}\n\n/**\n * The `DurableObjectNamespace` binding that addresses Thread Objects. The Object\n * identity rule is `namespace.idFromName(threadId)` (plan §1.2): Thread IDs are\n * globally unique, so the mapping is total and deterministic and no directory service exists.\n */\nexport class ThreadObjectNamespace extends Context.Service<\n ThreadObjectNamespace,\n {\n readonly namespace: DurableObjectNamespace<ThreadObjectRpc>;\n /** Stable binding name for opted-in native RPC tracing; absent by default. */\n readonly rpcTracing?: string;\n }\n>()(\"@effect-agent/platform-cloudflare/ThreadObjectNamespace\") {\n static layer(\n namespace: DurableObjectNamespace<ThreadObjectRpc>,\n options: { readonly rpcTracing?: string } = {},\n ): Layer.Layer<ThreadObjectNamespace> {\n return Layer.succeed(ThreadObjectNamespace)({\n namespace,\n ...(options.rpcTracing === undefined ? {} : { rpcTracing: options.rpcTracing }),\n });\n }\n}\n\n/**\n * Narrow one `env` member to a `DurableObjectNamespace`. `env` is an untyped platform value,\n * and a namespace binding is a host object no Schema can decode, so this is the documented\n * narrowest-boundary check (structural probe for the namespace surface the transport uses);\n * a missing or misshaped binding fails typed before any Layer is built.\n */\nexport const threadNamespaceFromEnv = Effect.fn(\"threadNamespaceFromEnv\")(function* (\n env: unknown,\n binding: string,\n): Effect.fn.Return<DurableObjectNamespace<ThreadObjectRpc>, CloudflareBindingError> {\n if (!Predicate.isObjectKeyword(env)) {\n return yield* CloudflareBindingError.make({\n binding,\n message: \"The Worker environment is not an object; no bindings are available.\",\n });\n }\n\n const candidate = yield* Effect.try({\n try: () => {\n const value: unknown = Reflect.get(env, binding);\n\n if (!Predicate.isObjectKeyword(value)) return undefined;\n const idFromName: unknown = Reflect.get(value, \"idFromName\");\n const get: unknown = Reflect.get(value, \"get\");\n\n return typeof idFromName === \"function\" && typeof get === \"function\" ? value : undefined;\n },\n catch: () =>\n CloudflareBindingError.make({\n binding,\n message: `env.${binding} could not be inspected as a DurableObjectNamespace binding.`,\n }),\n });\n\n if (candidate !== undefined) {\n // The structural probe above is the entire runtime contract this package relies on;\n // the assertion records that `idFromName`/`get` name a DurableObjectNamespace.\n return candidate as unknown as DurableObjectNamespace<ThreadObjectRpc>;\n }\n\n return yield* CloudflareBindingError.make({\n binding,\n message:\n `env.${binding} is not a DurableObjectNamespace binding; declare the Thread ` +\n \"Object class under this binding in the Worker configuration.\",\n });\n});\n\n/**\n * Build the namespace from Worker `env` (fails typed). Enable `rpcTracing` only when the\n * receiver also opts into the effect-cf native RPC trace-context contract.\n */\nexport const threadNamespaceLayer = (\n env: unknown,\n binding: string,\n options: { readonly rpcTracing?: boolean } = {},\n): Layer.Layer<ThreadObjectNamespace, CloudflareBindingError> =>\n Layer.effect(ThreadObjectNamespace)(\n Effect.map(threadNamespaceFromEnv(env, binding), (namespace) => ({\n namespace,\n ...(options.rpcTracing === true ? { rpcTracing: binding } : {}),\n })),\n );\n\n/**\n * The live Durable Object execution context of THIS incarnation. Only Layer construction and\n * the alarm service consume it; important state never lives on it (`ctx.storage` is truth,\n * everything in memory is a cache — deployment spec §11).\n */\nexport class DurableObjectContext extends Context.Service<\n DurableObjectContext,\n {\n readonly ctx: DurableObjectState;\n readonly env: unknown;\n }\n>()(\"@effect-agent/platform-cloudflare/DurableObjectContext\") {\n static layer(ctx: DurableObjectState, env: unknown): Layer.Layer<DurableObjectContext> {\n return Layer.succeed(DurableObjectContext)({ ctx, env });\n }\n}\n\n/**\n * The Thread identity this Object serializes and the producer identity its Attempts\n * write with (`{producerPrefix}:{threadId}`, plan §1.4). Derived once per incarnation\n * from `ctx.id.name` — the Object identity rule guarantees the name IS the Thread ID.\n */\nexport class ThreadObjectIdentity extends Context.Service<\n ThreadObjectIdentity,\n {\n readonly threadId: ThreadId;\n readonly producerId: ProducerId;\n }\n>()(\"@effect-agent/platform-cloudflare/ThreadObjectIdentity\") {}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAYA,IAAa,yBAAb,cAA4C,OAAO,YAAoC,CAAC,CACtF,0BACA;CACE,SAAS,OAAO;CAChB,SAAS,OAAO;AAClB,CACF,CAAC,CAAC,CAAC;;;;;;AAyCH,IAAa,wBAAb,MAAa,8BAA8B,QAAQ,QAOjD,CAAC,CAAC,yDAAyD,CAAC,CAAC;CAC7D,OAAO,MACL,WACA,UAA4C,CAAC,GACT;EACpC,OAAO,MAAM,QAAQ,qBAAqB,CAAC,CAAC;GAC1C;GACA,GAAI,QAAQ,eAAe,KAAA,IAAY,CAAC,IAAI,EAAE,YAAY,QAAQ,WAAW;EAC/E,CAAC;CACH;AACF;;;;;;;AAQA,MAAa,yBAAyB,OAAO,GAAG,wBAAwB,CAAC,CAAC,WACxE,KACA,SACmF;CACnF,IAAI,CAAC,UAAU,gBAAgB,GAAG,GAChC,OAAO,OAAO,uBAAuB,KAAK;EACxC;EACA,SAAS;CACX,CAAC;CAGH,MAAM,YAAY,OAAO,OAAO,IAAI;EAClC,WAAW;GACT,MAAM,QAAiB,QAAQ,IAAI,KAAK,OAAO;GAE/C,IAAI,CAAC,UAAU,gBAAgB,KAAK,GAAG,OAAO,KAAA;GAC9C,MAAM,aAAsB,QAAQ,IAAI,OAAO,YAAY;GAC3D,MAAM,MAAe,QAAQ,IAAI,OAAO,KAAK;GAE7C,OAAO,OAAO,eAAe,cAAc,OAAO,QAAQ,aAAa,QAAQ,KAAA;EACjF;EACA,aACE,uBAAuB,KAAK;GAC1B;GACA,SAAS,OAAO,QAAQ;EAC1B,CAAC;CACL,CAAC;CAED,IAAI,cAAc,KAAA,GAGhB,OAAO;CAGT,OAAO,OAAO,uBAAuB,KAAK;EACxC;EACA,SACE,OAAO,QAAQ;CAEnB,CAAC;AACH,CAAC;;;;;AAMD,MAAa,wBACX,KACA,SACA,UAA6C,CAAC,MAE9C,MAAM,OAAO,qBAAqB,CAAC,CACjC,OAAO,IAAI,uBAAuB,KAAK,OAAO,IAAI,eAAe;CAC/D;CACA,GAAI,QAAQ,eAAe,OAAO,EAAE,YAAY,QAAQ,IAAI,CAAC;AAC/D,EAAE,CACJ;;;;;;AAOF,IAAa,uBAAb,MAAa,6BAA6B,QAAQ,QAMhD,CAAC,CAAC,wDAAwD,CAAC,CAAC;CAC5D,OAAO,MAAM,KAAyB,KAAiD;EACrF,OAAO,MAAM,QAAQ,oBAAoB,CAAC,CAAC;GAAE;GAAK;EAAI,CAAC;CACzD;AACF;;;;;;AAOA,IAAa,uBAAb,cAA0C,QAAQ,QAMhD,CAAC,CAAC,wDAAwD,CAAC,CAAC,CAAC"}
|