effect-cf 0.25.3 → 0.27.0

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.
@@ -0,0 +1,216 @@
1
+ import { createGitClient, type GitClientFactory } from "@cloudflare/computer/git";
2
+ import {
3
+ type DurableObjectStorageLike,
4
+ type WithWorkspaceCtor,
5
+ type WorkspaceHandle,
6
+ type WorkspaceObserver,
7
+ type WorkspaceOptions,
8
+ type WorkspaceRegisteredBackend,
9
+ type WorkspaceRetryPendingSyncResult,
10
+ type WorkspaceSpan,
11
+ type WorkspaceAttributes,
12
+ type WorkspaceAttributeValue,
13
+ type SyncRetryIntent,
14
+ type SyncRetryOptions,
15
+ type SyncRetryScheduler,
16
+ withWorkspace,
17
+ } from "@cloudflare/computer";
18
+ import {
19
+ WorkerShellBackend,
20
+ type ShellModuleGroup,
21
+ type WorkerShellBackendOptions,
22
+ type WorkerShellLoader,
23
+ type WorkspaceEgressPolicy,
24
+ } from "@cloudflare/computer/backends/worker-shell";
25
+
26
+ import type * as Artifacts from "./Artifacts";
27
+ import * as HostRegistry from "./internal/ComputerWorkspaceHostRegistry";
28
+
29
+ /**
30
+ * The worker-shell backend resolves this exact export through
31
+ * `ctx.exports.WorkspaceServiceProxy`. Every Worker that configures
32
+ * {@link withComputerWorkspace} with `shell` must re-export it from its main
33
+ * module.
34
+ */
35
+ export { WorkspaceServiceProxy } from "@cloudflare/computer";
36
+
37
+ export type {
38
+ ShellModuleGroup,
39
+ SyncRetryIntent,
40
+ SyncRetryOptions,
41
+ SyncRetryScheduler,
42
+ WorkspaceAttributeValue,
43
+ WorkspaceAttributes,
44
+ WorkspaceEgressPolicy,
45
+ WorkspaceObserver,
46
+ WorkspaceRetryPendingSyncResult,
47
+ WorkspaceSpan,
48
+ };
49
+
50
+ /** The value of a `worker_loaders` binding. */
51
+ export type DynamicWorkerLoader = WorkerShellLoader;
52
+
53
+ /** Default author and committer identity for workspace Git operations. */
54
+ export interface ComputerWorkspaceGitIdentity {
55
+ readonly name: string;
56
+ readonly email: string;
57
+ }
58
+
59
+ /** Session-scoped Artifacts configuration passed through to `Workspace`. */
60
+ export interface ComputerWorkspaceArtifactsOptions {
61
+ readonly binding: Artifacts.ArtifactsBinding;
62
+ /**
63
+ * Session prefix for repository names. When omitted, `sessionId` from the
64
+ * enclosing host configuration is used.
65
+ */
66
+ readonly sessionId?: string;
67
+ }
68
+
69
+ /**
70
+ * Worker-shell backend options with the loopback workspace wiring named in
71
+ * Durable Object terms.
72
+ */
73
+ export interface ComputerWorkspaceShellOptions extends Omit<
74
+ WorkerShellBackendOptions,
75
+ "source" | "loader" | "workspace" | "ctx" | "egress"
76
+ > {
77
+ readonly loader: DynamicWorkerLoader;
78
+ /** Durable Object namespace binding that points back to the wrapped class. */
79
+ readonly hostBinding: string;
80
+ /** Current Durable Object id, normally `ctx.id.toString()`. */
81
+ readonly hostId: string;
82
+ /** Current Durable Object execution context, normally `ctx`. */
83
+ readonly executionContext: unknown;
84
+ /** Dynamic Worker egress policy. Defaults to `{ mode: "none" }`. */
85
+ readonly egress?: WorkspaceEgressPolicy;
86
+ }
87
+
88
+ /** Configuration accepted by {@link withComputerWorkspace}. */
89
+ export interface ComputerWorkspaceHostConfig {
90
+ /** Native SQLite-backed Durable Object storage (`ctx.storage`). */
91
+ readonly storage: globalThis.DurableObjectStorage | DurableObjectStorageLike;
92
+ /** Stable workspace id used by mounts and as the default Artifacts session. */
93
+ readonly sessionId?: string;
94
+ readonly gitIdentity?: ComputerWorkspaceGitIdentity;
95
+ /** Override the default `createGitClient()` factory. */
96
+ readonly git?: GitClientFactory;
97
+ /** Additional container, Worker JavaScript, shell, or custom backends. */
98
+ readonly backends?: ReadonlyArray<WorkspaceRegisteredBackend>;
99
+ /** Convenience wiring for one Worker Shell backend. */
100
+ readonly shell?: ComputerWorkspaceShellOptions;
101
+ readonly mounts?: WorkspaceOptions["mounts"];
102
+ readonly observer?: WorkspaceOptions["observer"];
103
+ readonly retryScheduler?: WorkspaceOptions["retryScheduler"];
104
+ readonly retry?: WorkspaceOptions["retry"];
105
+ readonly assets?: WorkspaceOptions["assets"];
106
+ readonly artifacts?: ComputerWorkspaceArtifactsOptions;
107
+ readonly now?: WorkspaceOptions["now"];
108
+ readonly useThink?: WorkspaceOptions["useThink"];
109
+ }
110
+
111
+ const toWorkspaceOptions = (config: ComputerWorkspaceHostConfig): WorkspaceOptions => {
112
+ const backends = [...(config.backends ?? [])];
113
+
114
+ if (config.shell !== undefined) {
115
+ const { egress, executionContext, hostBinding, hostId, loader, ...shellOptions } = config.shell;
116
+
117
+ backends.push(
118
+ new WorkerShellBackend({
119
+ ...shellOptions,
120
+ loader,
121
+ workspace: { binding: hostBinding, id: hostId },
122
+ ctx: executionContext,
123
+ egress: egress ?? { mode: "none" },
124
+ }),
125
+ );
126
+ }
127
+
128
+ return {
129
+ storage: config.storage as DurableObjectStorageLike,
130
+ git: config.git ?? createGitClient(),
131
+ ...(config.gitIdentity === undefined ? {} : { defaultGitIdentity: config.gitIdentity }),
132
+ ...(backends.length === 0 ? {} : { backends }),
133
+ ...(config.sessionId === undefined ? {} : { sessionId: config.sessionId }),
134
+ ...(config.mounts === undefined ? {} : { mounts: config.mounts }),
135
+ ...(config.observer === undefined ? {} : { observer: config.observer }),
136
+ ...(config.retryScheduler === undefined ? {} : { retryScheduler: config.retryScheduler }),
137
+ ...(config.retry === undefined ? {} : { retry: config.retry }),
138
+ ...(config.assets === undefined ? {} : { assets: config.assets }),
139
+ ...(config.artifacts === undefined
140
+ ? {}
141
+ : {
142
+ artifacts: {
143
+ binding: config.artifacts.binding as unknown as NonNullable<
144
+ WorkspaceOptions["artifacts"]
145
+ >["binding"],
146
+ ...(config.artifacts.sessionId === undefined
147
+ ? {}
148
+ : { sessionId: config.artifacts.sessionId }),
149
+ },
150
+ }),
151
+ ...(config.now === undefined ? {} : { now: config.now }),
152
+ ...(config.useThink === undefined ? {} : { useThink: config.useThink }),
153
+ };
154
+ };
155
+
156
+ // Cloudflare constructs Durable Objects with `(state, env)`. Keeping the
157
+ // remaining tuple open preserves arbitrary Effect-backed base constructors.
158
+ type DurableObjectConstructor = new (...args: Array<any>) => object;
159
+
160
+ const HostArguments = Symbol("effect-cf/ComputerWorkspaceHost/arguments");
161
+
162
+ interface CapturedHostArguments {
163
+ readonly state: globalThis.DurableObjectState;
164
+ readonly env: unknown;
165
+ }
166
+
167
+ interface HostArgumentsInstance {
168
+ readonly [HostArguments]: CapturedHostArguments;
169
+ }
170
+
171
+ /**
172
+ * Wraps a Durable Object class with one SQLite-backed Cloudflare Computer
173
+ * workspace and registers that local workspace for {@link ComputerWorkspace}
174
+ * to acquire through its Effect layer.
175
+ *
176
+ * The class must use a `new_sqlite_classes` migration. If `shell` is present,
177
+ * its Worker must also configure a `worker_loaders` binding, enable the
178
+ * `experimental` compatibility flag, and export `WorkspaceServiceProxy`.
179
+ */
180
+ export const withComputerWorkspace = <TBase extends DurableObjectConstructor>(
181
+ Base: TBase,
182
+ config: (
183
+ self: InstanceType<TBase>,
184
+ state: globalThis.DurableObjectState,
185
+ env: ConstructorParameters<TBase>[1],
186
+ ) => ComputerWorkspaceHostConfig,
187
+ ): WithWorkspaceCtor<TBase> => {
188
+ const CapturedBase = class extends (Base as DurableObjectConstructor) {
189
+ readonly [HostArguments]: CapturedHostArguments;
190
+
191
+ constructor(...args: Array<any>) {
192
+ super(...args);
193
+ this[HostArguments] = {
194
+ state: args[0] as globalThis.DurableObjectState,
195
+ env: args[1],
196
+ };
197
+ }
198
+ };
199
+
200
+ const WithWorkspace = withWorkspace(CapturedBase, (self) => {
201
+ const { env, state } = (self as HostArgumentsInstance)[HostArguments];
202
+
203
+ return toWorkspaceOptions(
204
+ config(self as InstanceType<TBase>, state, env as ConstructorParameters<TBase>[1]),
205
+ );
206
+ }) as unknown as DurableObjectConstructor;
207
+
208
+ class WithComputerWorkspace extends WithWorkspace {
209
+ constructor(...args: Array<any>) {
210
+ super(...args);
211
+ HostRegistry.register(args[0] as globalThis.DurableObjectState, this as WorkspaceHandle);
212
+ }
213
+ }
214
+
215
+ return WithComputerWorkspace as unknown as WithWorkspaceCtor<TBase>;
216
+ };
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Truthful WebTransport / HTTP/3 boundary for Cloudflare Workers.
3
+ *
4
+ * What Cloudflare supports today (verified against primary sources, August
5
+ * 2026):
6
+ *
7
+ * - Inbound HTTP/3 is a zone-level setting. Cloudflare's edge terminates the
8
+ * QUIC connection and invokes the Worker with an ordinary `fetch` Request;
9
+ * the inbound protocol is surfaced only as metadata on `request.cf`
10
+ * ({@link inboundTransport}).
11
+ * - There is no runtime API for inbound WebTransport sessions — nothing
12
+ * analogous to `WebSocketPair`. workerd contains no QUIC/HTTP-3 stack, and
13
+ * the maintainers state WebTransport support "is not currently on our
14
+ * priority list" (cloudflare/workerd#6451, discussion #6454).
15
+ * - There is no outbound WebTransport/QUIC/UDP client capability either;
16
+ * `cloudflare:sockets` `connect()` is TCP-only, and
17
+ * `@cloudflare/workers-types` contains no WebTransport types.
18
+ * - Workers and Durable Objects can accept inbound WebSocket connections;
19
+ * Durable Objects additionally support hibernation (`acceptWebSocket`,
20
+ * `webSocketMessage`, `webSocketClose`).
21
+ *
22
+ * Consequently a browser's WebTransport session can never reach Worker code:
23
+ * clients should attempt WebTransport only against origins that actually
24
+ * speak it, and fall back to WebSockets for Cloudflare-hosted endpoints (see
25
+ * the `effect-webtransport` package's `Fallback` module). This module gives
26
+ * that reality a typed shape: {@link capabilities} feature-detects what the
27
+ * current runtime provides, {@link inboundSessionsUnsupported} is the
28
+ * explicit typed boundary for the missing inbound API, and
29
+ * {@link inboundTransport} decodes the HTTP/3-relevant request metadata that
30
+ * the edge does provide.
31
+ */
32
+ import type { Request as CloudflareRequest } from "@cloudflare/workers-types";
33
+ import { Data, Effect, Option, Result, Schema as S } from "effect";
34
+
35
+ /**
36
+ * Metadata Cloudflare's edge reports about the inbound client connection.
37
+ *
38
+ * `httpProtocol` and `clientTcpRtt` are declared in
39
+ * `@cloudflare/workers-types`; `clientQuicRtt` is documented by Cloudflare
40
+ * ("only present when the client connected over QUIC (HTTP/3)") but not yet
41
+ * present in the published types, so it is decoded from the open
42
+ * `Record<string, unknown>` side of `request.cf`.
43
+ */
44
+ export class InboundTransport extends S.Class<InboundTransport>("InboundTransport")({
45
+ /** The HTTP protocol the client used, e.g. `"HTTP/2"` or `"HTTP/3"`. */
46
+ httpProtocol: S.String,
47
+ /** Client round-trip time; only present for QUIC (HTTP/3) connections. */
48
+ clientQuicRtt: S.optional(S.Number),
49
+ /** Client round-trip time; only present for TCP (HTTP/1.x, HTTP/2) connections. */
50
+ clientTcpRtt: S.optional(S.Number),
51
+ }) {}
52
+
53
+ const decodeInboundTransport = S.decodeUnknownResult(InboundTransport);
54
+
55
+ /**
56
+ * Decodes the inbound transport metadata from `request.cf`.
57
+ *
58
+ * Returns `None` when the metadata is absent (for example in local `wrangler
59
+ * dev` or when a Worker invokes another Worker directly) or does not match
60
+ * the expected shape.
61
+ */
62
+ export const inboundTransport = (
63
+ request: Pick<CloudflareRequest, "cf">,
64
+ ): Option.Option<InboundTransport> => Result.getSuccess(decodeInboundTransport(request.cf));
65
+
66
+ /**
67
+ * Returns `true` when the client reached Cloudflare's edge over QUIC
68
+ * (HTTP/3). Prefers the documented QUIC signal (`clientQuicRtt` is only
69
+ * present for QUIC connections) and falls back to the `httpProtocol` string.
70
+ *
71
+ * Note this describes the browser→edge hop only: the edge always terminates
72
+ * QUIC, and the Worker still handles an ordinary `fetch` Request.
73
+ */
74
+ export const isHttp3 = (transport: InboundTransport): boolean =>
75
+ transport.clientQuicRtt !== undefined || transport.httpProtocol === "HTTP/3";
76
+
77
+ /** WebTransport-related capabilities of the current Workers runtime. */
78
+ export interface Capabilities {
79
+ /**
80
+ * Whether Worker code can accept inbound WebTransport sessions. Typed as
81
+ * literally `false`: no such runtime API exists (cloudflare/workerd#6451),
82
+ * so this can only change through a breaking, deliberate update when
83
+ * Cloudflare ships one.
84
+ */
85
+ readonly inboundSessions: false;
86
+ /**
87
+ * Whether the runtime exposes a client `WebTransport` constructor global.
88
+ * Feature-detected at runtime; `false` in workerd today, which has no
89
+ * QUIC stack.
90
+ */
91
+ readonly outboundSessions: boolean;
92
+ }
93
+
94
+ /** Feature-detects the WebTransport capabilities of the current runtime. */
95
+ export const capabilities: Effect.Effect<Capabilities> = Effect.sync(() => ({
96
+ inboundSessions: false,
97
+ outboundSessions: typeof (globalThis as Record<string, unknown>)["WebTransport"] === "function",
98
+ }));
99
+
100
+ /** WebTransport capability missing from the current Workers runtime. */
101
+ export type WebTransportCapability = "inbound-sessions" | "outbound-sessions";
102
+
103
+ /** Raised when a WebTransport capability is unavailable in this runtime. */
104
+ export class WebTransportUnsupportedError extends Data.TaggedError("WebTransportUnsupportedError")<{
105
+ readonly capability: WebTransportCapability;
106
+ }> {
107
+ override get message(): string {
108
+ return this.capability === "inbound-sessions"
109
+ ? "Cloudflare Workers cannot accept inbound WebTransport sessions: the edge terminates QUIC and no runtime session API exists (cloudflare/workerd#6451). Use Durable Object WebSockets for bidirectional push."
110
+ : "This Workers runtime provides no WebTransport client: workerd has no QUIC stack and cloudflare:sockets is TCP-only.";
111
+ }
112
+ }
113
+
114
+ /**
115
+ * The explicit typed boundary for inbound WebTransport sessions.
116
+ *
117
+ * Always fails with {@link WebTransportUnsupportedError} today. Route the
118
+ * code path that would accept a session through this effect so the
119
+ * limitation is visible in the type system and greppable when Cloudflare
120
+ * ships a real API.
121
+ */
122
+ export const inboundSessionsUnsupported: Effect.Effect<never, WebTransportUnsupportedError> =
123
+ Effect.fail(new WebTransportUnsupportedError({ capability: "inbound-sessions" }));
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * as AiGateway from "./AiGateway";
2
2
  export * as AnalyticsEngine from "./AnalyticsEngine";
3
+ export * as Artifacts from "./Artifacts";
3
4
  export * as Binding from "./Binding";
4
5
  export * as BrowserRendering from "./BrowserRendering";
5
6
  export * as Cache from "./Cache";
@@ -28,6 +29,7 @@ export * as Rpc from "./Rpc";
28
29
  export * as RpcDefinition from "./RpcDefinition";
29
30
  export * as ServiceBinding from "./ServiceBinding";
30
31
  export * as Vectorize from "./Vectorize";
32
+ export * as WebTransport from "./WebTransport";
31
33
  export * as Worker from "./Worker";
32
34
  export * as WorkerDefinition from "./WorkerDefinition";
33
35
  export * as WorkersAi from "./WorkersAi";
@@ -0,0 +1,10 @@
1
+ import type { WorkspaceHandle } from "@cloudflare/computer";
2
+
3
+ const hosts = new WeakMap<globalThis.DurableObjectState, WorkspaceHandle>();
4
+
5
+ export const register = (state: globalThis.DurableObjectState, host: WorkspaceHandle): void => {
6
+ hosts.set(state, host);
7
+ };
8
+
9
+ export const lookup = (state: globalThis.DurableObjectState): WorkspaceHandle | undefined =>
10
+ hosts.get(state);