@effect-agent/platform-cloudflare 0.1.0-beta.37 → 0.1.0-beta.39
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/bindings-C00ZfjsX.d.mts +100 -0
- package/dist/index.d.mts +191 -206
- package/dist/index.mjs +157 -163
- package/dist/index.mjs.map +1 -1
- package/dist/interactive-browser.d.mts +23 -3
- package/dist/interactive-browser.mjs +110 -35
- package/dist/interactive-browser.mjs.map +1 -1
- package/dist/{scheduling-B-OFqoS9.mjs → prepared-admission-D5hQ-ofV.mjs} +121 -432
- package/dist/prepared-admission-D5hQ-ofV.mjs.map +1 -0
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/dist/scheduling.d.mts +47 -2
- package/dist/scheduling.mjs +339 -1
- package/dist/scheduling.mjs.map +1 -0
- package/dist/subscriptions.d.mts +52 -0
- package/dist/subscriptions.mjs +346 -0
- package/dist/subscriptions.mjs.map +1 -0
- package/package.json +18 -11
- package/src/alarm.ts +264 -265
- package/src/bindings.ts +30 -30
- package/src/browser-session-lifecycle.ts +142 -0
- package/src/client.ts +90 -98
- package/src/config.ts +7 -7
- package/src/index.ts +9 -11
- package/src/interactive-browser.ts +81 -70
- package/src/layers.ts +178 -257
- package/src/prepared-admission.ts +90 -0
- package/src/scheduling.ts +19 -55
- package/src/subscriptions.ts +661 -0
- package/src/{conversation-object.ts → thread-object.ts} +167 -136
- package/src/transport.ts +15 -15
- package/src/wake-scheduler.ts +17 -22
- package/dist/scheduling-B-OFqoS9.mjs.map +0 -1
- package/dist/scheduling-BJs_kHTx.d.mts +0 -142
package/src/bindings.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type { ProducerId } from "@effect-agent/
|
|
1
|
+
import type { ThreadId } from "@effect-agent/core";
|
|
2
|
+
import type { ProducerId } from "@effect-agent/thread";
|
|
3
3
|
import { Context, Effect, Layer, Predicate, Schema } from "effect";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Cloudflare platform bindings as Effect services (DEPLOY-010: "Cloudflare platform bindings
|
|
7
7
|
* are supplied as Effect services/Layers"). Application code never reads `env` or touches a
|
|
8
|
-
* `DurableObjectState` directly — the
|
|
8
|
+
* `DurableObjectState` directly — the Thread Object class constructs these Layers once
|
|
9
9
|
* per incarnation and everything downstream consumes the services.
|
|
10
10
|
*/
|
|
11
11
|
|
|
@@ -19,8 +19,8 @@ export class CloudflareBindingError extends Schema.TaggedError<CloudflareBinding
|
|
|
19
19
|
) {}
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
|
-
* The RPC surface one
|
|
23
|
-
*
|
|
22
|
+
* The RPC surface one Thread Durable Object exposes to Workers and to sibling
|
|
23
|
+
* Thread Objects. `ThreadObject.make` implements it; the Worker-side client
|
|
24
24
|
* and the cross-Object transport call it through `DurableObjectNamespace` stubs. Every
|
|
25
25
|
* `encoded` value is a Schema-encoded envelope (`client.ts` wire schemas for host entry
|
|
26
26
|
* points, `@effect-agent/storage-cloudflare` port envelopes for `portCall`), so the RPC
|
|
@@ -28,7 +28,7 @@ export class CloudflareBindingError extends Schema.TaggedError<CloudflareBinding
|
|
|
28
28
|
* transient native RPC metadata, stripped by an opted-in effect-cf receiver before decoding
|
|
29
29
|
* the host envelope. It never enters durable state.
|
|
30
30
|
*/
|
|
31
|
-
export interface
|
|
31
|
+
export interface ThreadObjectRpc extends Rpc.DurableObjectBranded {
|
|
32
32
|
/** Admission-limits gate + `DurableAgentRuntime.submit`; answers a `SubmitResponse`. */
|
|
33
33
|
submitEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;
|
|
34
34
|
/** Wake-hinted, poll-guaranteed settlement wait; answers an `AwaitSettlementResponse`. */
|
|
@@ -52,22 +52,22 @@ export interface ConversationObjectRpc extends Rpc.DurableObjectBranded {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
/**
|
|
55
|
-
* The `DurableObjectNamespace` binding that addresses
|
|
56
|
-
* identity rule is `namespace.idFromName(
|
|
55
|
+
* The `DurableObjectNamespace` binding that addresses Thread Objects. The Object
|
|
56
|
+
* identity rule is `namespace.idFromName(threadId)` (plan §1.2): Thread IDs are
|
|
57
57
|
* globally unique, so the mapping is total and deterministic and no directory service exists.
|
|
58
58
|
*/
|
|
59
|
-
export class
|
|
60
|
-
|
|
59
|
+
export class ThreadObjectNamespace extends Context.Service<
|
|
60
|
+
ThreadObjectNamespace,
|
|
61
61
|
{
|
|
62
|
-
readonly namespace: DurableObjectNamespace<
|
|
62
|
+
readonly namespace: DurableObjectNamespace<ThreadObjectRpc>;
|
|
63
63
|
/** Stable binding name for opted-in native RPC tracing; absent by default. */
|
|
64
64
|
readonly rpcTracing?: string;
|
|
65
65
|
}
|
|
66
|
-
>()("@effect-agent/platform-cloudflare/
|
|
66
|
+
>()("@effect-agent/platform-cloudflare/ThreadObjectNamespace") {
|
|
67
67
|
static layer(
|
|
68
|
-
namespace: DurableObjectNamespace<
|
|
69
|
-
): Layer.Layer<
|
|
70
|
-
return Layer.succeed(
|
|
68
|
+
namespace: DurableObjectNamespace<ThreadObjectRpc>,
|
|
69
|
+
): Layer.Layer<ThreadObjectNamespace> {
|
|
70
|
+
return Layer.succeed(ThreadObjectNamespace)({ namespace });
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
|
|
@@ -77,10 +77,10 @@ export class ConversationObjectNamespace extends Context.Service<
|
|
|
77
77
|
* narrowest-boundary check (structural probe for the namespace surface the transport uses);
|
|
78
78
|
* a missing or misshaped binding fails typed before any Layer is built.
|
|
79
79
|
*/
|
|
80
|
-
export const
|
|
80
|
+
export const threadNamespaceFromEnv = Effect.fn("threadNamespaceFromEnv")(function* (
|
|
81
81
|
env: unknown,
|
|
82
82
|
binding: string,
|
|
83
|
-
): Effect.fn.Return<DurableObjectNamespace<
|
|
83
|
+
): Effect.fn.Return<DurableObjectNamespace<ThreadObjectRpc>, CloudflareBindingError> {
|
|
84
84
|
if (!Predicate.isObjectKeyword(env)) {
|
|
85
85
|
return yield* CloudflareBindingError.make({
|
|
86
86
|
binding,
|
|
@@ -104,12 +104,12 @@ export const conversationNamespaceFromEnv = Effect.fn("conversationNamespaceFrom
|
|
|
104
104
|
if (candidate !== undefined) {
|
|
105
105
|
// The structural probe above is the entire runtime contract this package relies on;
|
|
106
106
|
// the assertion records that `idFromName`/`get` name a DurableObjectNamespace.
|
|
107
|
-
return candidate as unknown as DurableObjectNamespace<
|
|
107
|
+
return candidate as unknown as DurableObjectNamespace<ThreadObjectRpc>;
|
|
108
108
|
}
|
|
109
109
|
return yield* CloudflareBindingError.make({
|
|
110
110
|
binding,
|
|
111
111
|
message:
|
|
112
|
-
`env.${binding} is not a DurableObjectNamespace binding; declare the
|
|
112
|
+
`env.${binding} is not a DurableObjectNamespace binding; declare the Thread ` +
|
|
113
113
|
"Object class under this binding in the Worker configuration.",
|
|
114
114
|
});
|
|
115
115
|
});
|
|
@@ -118,13 +118,13 @@ export const conversationNamespaceFromEnv = Effect.fn("conversationNamespaceFrom
|
|
|
118
118
|
* Build the namespace from Worker `env` (fails typed). Enable `rpcTracing` only when the
|
|
119
119
|
* receiver also opts into the effect-cf native RPC trace-context contract.
|
|
120
120
|
*/
|
|
121
|
-
export const
|
|
121
|
+
export const threadNamespaceLayer = (
|
|
122
122
|
env: unknown,
|
|
123
123
|
binding: string,
|
|
124
124
|
options: { readonly rpcTracing?: boolean } = {},
|
|
125
|
-
): Layer.Layer<
|
|
126
|
-
Layer.effect(
|
|
127
|
-
Effect.map(
|
|
125
|
+
): Layer.Layer<ThreadObjectNamespace, CloudflareBindingError> =>
|
|
126
|
+
Layer.effect(ThreadObjectNamespace)(
|
|
127
|
+
Effect.map(threadNamespaceFromEnv(env, binding), (namespace) => ({
|
|
128
128
|
namespace,
|
|
129
129
|
...(options.rpcTracing === true ? { rpcTracing: binding } : {}),
|
|
130
130
|
})),
|
|
@@ -148,14 +148,14 @@ export class DurableObjectContext extends Context.Service<
|
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
/**
|
|
151
|
-
* The
|
|
152
|
-
* write with (`{producerPrefix}:{
|
|
153
|
-
* from `ctx.id.name` — the Object identity rule guarantees the name IS the
|
|
151
|
+
* The Thread identity this Object serializes and the producer identity its Attempts
|
|
152
|
+
* write with (`{producerPrefix}:{threadId}`, plan §1.4). Derived once per incarnation
|
|
153
|
+
* from `ctx.id.name` — the Object identity rule guarantees the name IS the Thread ID.
|
|
154
154
|
*/
|
|
155
|
-
export class
|
|
156
|
-
|
|
155
|
+
export class ThreadObjectIdentity extends Context.Service<
|
|
156
|
+
ThreadObjectIdentity,
|
|
157
157
|
{
|
|
158
|
-
readonly
|
|
158
|
+
readonly threadId: ThreadId;
|
|
159
159
|
readonly producerId: ProducerId;
|
|
160
160
|
}
|
|
161
|
-
>()("@effect-agent/platform-cloudflare/
|
|
161
|
+
>()("@effect-agent/platform-cloudflare/ThreadObjectIdentity") {}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { Context, Effect, Layer, Option, Redacted, Schema, Stream } from "effect";
|
|
2
|
+
import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http";
|
|
3
|
+
|
|
4
|
+
export class BrowserRunCleanupError extends Schema.TaggedError<BrowserRunCleanupError>()(
|
|
5
|
+
"BrowserRunCleanupError",
|
|
6
|
+
{
|
|
7
|
+
reason: Schema.Literals([
|
|
8
|
+
"configuration",
|
|
9
|
+
"authorization",
|
|
10
|
+
"rate-limited",
|
|
11
|
+
"provider",
|
|
12
|
+
"malformed",
|
|
13
|
+
"timeout",
|
|
14
|
+
"pending",
|
|
15
|
+
]),
|
|
16
|
+
status: Schema.optionalKey(Schema.Int),
|
|
17
|
+
},
|
|
18
|
+
) {}
|
|
19
|
+
|
|
20
|
+
export interface BrowserRunLifecycleOptions {
|
|
21
|
+
readonly accountId: string;
|
|
22
|
+
readonly apiToken: Redacted.Redacted<string>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const Identity = Schema.String.check(Schema.isUUID());
|
|
26
|
+
const Account = Schema.String.check(Schema.isPattern(/^[a-f0-9]{32}$/));
|
|
27
|
+
const Closed = Schema.Struct({ status: Schema.Literals(["closing", "closed"]) });
|
|
28
|
+
const Metadata = Schema.Struct({ sessionId: Identity, endTime: Schema.optionalKey(Schema.Finite) });
|
|
29
|
+
// This exact provider response is accepted only on a fixed-origin, authenticated,
|
|
30
|
+
// exact-session request. An arbitrary 404 or a listing omission is never absence.
|
|
31
|
+
const Absent = Schema.Struct({ error: Schema.Literal("Session not found") });
|
|
32
|
+
|
|
33
|
+
export class BrowserRunSessionLifecycle extends Context.Service<
|
|
34
|
+
BrowserRunSessionLifecycle,
|
|
35
|
+
{
|
|
36
|
+
readonly close: (
|
|
37
|
+
sessionId: Redacted.Redacted<string>,
|
|
38
|
+
) => Effect.Effect<void, BrowserRunCleanupError>;
|
|
39
|
+
}
|
|
40
|
+
>()("@effect-agent/platform-cloudflare/BrowserRunSessionLifecycle") {
|
|
41
|
+
static layer(options: BrowserRunLifecycleOptions) {
|
|
42
|
+
return Layer.effect(
|
|
43
|
+
this,
|
|
44
|
+
Effect.gen(function* () {
|
|
45
|
+
if (
|
|
46
|
+
!Schema.is(Account)(options.accountId) ||
|
|
47
|
+
Redacted.value(options.apiToken).length === 0
|
|
48
|
+
) {
|
|
49
|
+
return yield* new BrowserRunCleanupError({ reason: "configuration" });
|
|
50
|
+
}
|
|
51
|
+
const client = yield* HttpClient.HttpClient;
|
|
52
|
+
const request = Effect.fn("BrowserRunSessionLifecycle.request")(function* (
|
|
53
|
+
method: "GET" | "DELETE",
|
|
54
|
+
sessionId: string,
|
|
55
|
+
) {
|
|
56
|
+
const path = method === "DELETE" ? "browser" : "session";
|
|
57
|
+
const response = yield* client
|
|
58
|
+
.execute(
|
|
59
|
+
HttpClientRequest.make(method)(
|
|
60
|
+
`https://api.cloudflare.com/client/v4/accounts/${options.accountId}/browser-rendering/devtools/${path}/${sessionId}`,
|
|
61
|
+
).pipe(HttpClientRequest.bearerToken(options.apiToken)),
|
|
62
|
+
)
|
|
63
|
+
.pipe(
|
|
64
|
+
// workerd supports manual/follow, not error. Reject every redirect below.
|
|
65
|
+
Effect.provideService(FetchHttpClient.RequestInit, { redirect: "manual" }),
|
|
66
|
+
Effect.mapError(() => new BrowserRunCleanupError({ reason: "provider" })),
|
|
67
|
+
);
|
|
68
|
+
if (response.status === 401 || response.status === 403)
|
|
69
|
+
return yield* new BrowserRunCleanupError({
|
|
70
|
+
reason: "authorization",
|
|
71
|
+
status: response.status,
|
|
72
|
+
});
|
|
73
|
+
if (response.status === 429)
|
|
74
|
+
return yield* new BrowserRunCleanupError({
|
|
75
|
+
reason: "rate-limited",
|
|
76
|
+
status: response.status,
|
|
77
|
+
});
|
|
78
|
+
if (response.status !== 200 && response.status !== 404)
|
|
79
|
+
return yield* new BrowserRunCleanupError({
|
|
80
|
+
reason: "provider",
|
|
81
|
+
status: response.status,
|
|
82
|
+
});
|
|
83
|
+
if (response.headers["content-type"]?.split(";", 1)[0]?.trim() !== "application/json")
|
|
84
|
+
return yield* new BrowserRunCleanupError({ reason: "malformed" });
|
|
85
|
+
const bytes = yield* Stream.runFoldEffect(
|
|
86
|
+
response.stream,
|
|
87
|
+
() => new Uint8Array(),
|
|
88
|
+
(body, chunk) => {
|
|
89
|
+
if (body.byteLength + chunk.byteLength > 16_384)
|
|
90
|
+
return Effect.fail(new BrowserRunCleanupError({ reason: "malformed" }));
|
|
91
|
+
const combined = new Uint8Array(body.byteLength + chunk.byteLength);
|
|
92
|
+
combined.set(body);
|
|
93
|
+
combined.set(chunk, body.byteLength);
|
|
94
|
+
return Effect.succeed(combined);
|
|
95
|
+
},
|
|
96
|
+
).pipe(Effect.mapError(() => new BrowserRunCleanupError({ reason: "malformed" })));
|
|
97
|
+
const body = yield* Effect.try({
|
|
98
|
+
try: () => new TextDecoder("utf-8", { fatal: true, ignoreBOM: false }).decode(bytes),
|
|
99
|
+
catch: () => new BrowserRunCleanupError({ reason: "malformed" }),
|
|
100
|
+
});
|
|
101
|
+
if (response.status === 404) {
|
|
102
|
+
const absent = Schema.decodeUnknownOption(Schema.fromJsonString(Absent))(body, {
|
|
103
|
+
onExcessProperty: "error",
|
|
104
|
+
});
|
|
105
|
+
if (Option.isSome(absent)) return true;
|
|
106
|
+
return yield* new BrowserRunCleanupError({ reason: "malformed" });
|
|
107
|
+
}
|
|
108
|
+
if (method === "DELETE") {
|
|
109
|
+
const result = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(Closed))(
|
|
110
|
+
body,
|
|
111
|
+
).pipe(Effect.mapError(() => new BrowserRunCleanupError({ reason: "malformed" })));
|
|
112
|
+
return result.status === "closed";
|
|
113
|
+
}
|
|
114
|
+
const result = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(Metadata))(
|
|
115
|
+
body,
|
|
116
|
+
).pipe(Effect.mapError(() => new BrowserRunCleanupError({ reason: "malformed" })));
|
|
117
|
+
if (result.sessionId !== sessionId)
|
|
118
|
+
return yield* new BrowserRunCleanupError({ reason: "malformed" });
|
|
119
|
+
return result.endTime !== undefined && result.endTime > 0;
|
|
120
|
+
});
|
|
121
|
+
const close = Effect.fn("BrowserRunSessionLifecycle.close")(
|
|
122
|
+
function* (sessionId: Redacted.Redacted<string>) {
|
|
123
|
+
const id = yield* Schema.decodeUnknownEffect(Identity)(Redacted.value(sessionId)).pipe(
|
|
124
|
+
Effect.mapError(() => new BrowserRunCleanupError({ reason: "configuration" })),
|
|
125
|
+
);
|
|
126
|
+
if (yield* request("DELETE", id)) return;
|
|
127
|
+
for (let read = 0; read < 2; read++) {
|
|
128
|
+
if (yield* request("GET", id)) return;
|
|
129
|
+
}
|
|
130
|
+
return yield* new BrowserRunCleanupError({ reason: "pending" });
|
|
131
|
+
},
|
|
132
|
+
Effect.timeoutOrElse({
|
|
133
|
+
duration: "10 seconds",
|
|
134
|
+
orElse: () => Effect.fail(new BrowserRunCleanupError({ reason: "timeout" })),
|
|
135
|
+
}),
|
|
136
|
+
Effect.withTracerEnabled(false),
|
|
137
|
+
);
|
|
138
|
+
return { close };
|
|
139
|
+
}),
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|